fengketrade/addons/shopro/library/ccblife/CcbOrderService.php
2025-10-18 01:16:25 +08:00

556 lines
19 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace addons\shopro\library\ccblife;
use think\Db;
use think\Log;
/**
* 建行生活订单服务类
* 处理订单同步、状态更新、查询等业务逻辑
*/
class CcbOrderService
{
/**
* HTTP客户端实例
*/
private $httpClient;
/**
* 配置信息
*/
private $config;
/**
* 构造函数
*/
public function __construct()
{
$this->config = config('ccblife');
$this->httpClient = new CcbHttpClient($this->config);
}
/**
* 推送订单到建行生活平台
* 当用户下单后调用此方法同步订单信息
*
* @param int $orderId Shopro订单ID
* @return array ['status' => bool, 'message' => string, 'data' => array]
* @throws \Exception
*/
public function pushOrder($orderId)
{
$startTime = microtime(true);
try {
// 获取订单信息
$order = Db::name('shopro_order')
->alias('o')
->join('user u', 'o.user_id = u.id', 'LEFT')
->where('o.id', $orderId)
->field('o.*, u.ccb_user_id')
->find();
if (!$order) {
throw new \Exception('订单不存在');
}
// 获取建行用户ID
$ccbUserId = $order['ccb_user_id'];
if (!$ccbUserId) {
throw new \Exception('用户未绑定建行生活账号');
}
// 获取订单商品列表
$orderItems = Db::name('shopro_order_item')
->where('order_id', $orderId)
->select();
// 构建订单数据符合A3341TP01接口规范
$orderData = $this->buildOrderData($order, $orderItems, $ccbUserId);
// 记录请求数据(同步日志)
$txSeq = CcbMD5::generateTransactionSeq();
$this->recordSyncLog($orderId, 'A3341TP01', $txSeq, $orderData, 'request');
// 调用建行API推送订单
$response = $this->httpClient->pushOrder($orderData);
// 记录响应数据和耗时
$costTime = round((microtime(true) - $startTime) * 1000, 2);
$this->recordSyncLog($orderId, 'A3341TP01', $txSeq, $response, 'response', true, $costTime);
// 更新订单同步状态
$this->updateOrderSyncStatus($orderId, 1);
return [
'status' => true,
'message' => '订单推送成功',
'data' => $response
];
} catch (\Exception $e) {
// 记录错误
$costTime = round((microtime(true) - $startTime) * 1000, 2);
$this->recordSyncLog($orderId, 'A3341TP01', '', null, 'error', false, $costTime, $e->getMessage());
// 更新同步状态为失败
$this->updateOrderSyncStatus($orderId, 2);
Log::error('建行订单推送失败: ' . $e->getMessage());
return [
'status' => false,
'message' => $e->getMessage(),
'data' => null
];
}
}
/**
* 更新订单状态到建行生活
*
* @param int $orderId 订单ID
* @param string $status 订单状态
* @param string $refundStatus 退款状态
* @return array
*/
public function updateOrderStatus($orderId, $status = null, $refundStatus = null)
{
$startTime = microtime(true);
$txSeq = CcbMD5::generateTransactionSeq();
try {
// 获取订单信息
$order = Db::name('shopro_order')
->alias('o')
->join('user u', 'o.user_id = u.id', 'LEFT')
->where('o.id', $orderId)
->field('o.*, u.ccb_user_id')
->find();
if (!$order) {
throw new \Exception('订单不存在');
}
// 获取建行用户ID
$ccbUserId = $order['ccb_user_id'];
if (!$ccbUserId) {
throw new \Exception('用户未绑定建行生活账号');
}
// 映射订单状态
$orderStatus = $status ?: $this->mapOrderStatus($order['status']);
$refundStatus = $refundStatus ?: $this->mapRefundStatus($order['refund_status'] ?? 0);
// 记录请求
$requestData = [
'ccb_user_id' => $ccbUserId,
'order_sn' => $order['order_sn'],
'order_status' => $orderStatus,
'refund_status' => $refundStatus
];
$this->recordSyncLog($orderId, 'A3341TP02', $txSeq, $requestData, 'request');
// 调用建行API更新状态
$response = $this->httpClient->updateOrderStatus(
$ccbUserId,
$order['order_sn'],
$orderStatus,
$refundStatus
);
// 记录响应
$costTime = round((microtime(true) - $startTime) * 1000, 2);
$this->recordSyncLog($orderId, 'A3341TP02', $txSeq, $response, 'response', true, $costTime);
return [
'status' => true,
'message' => '订单状态更新成功',
'data' => $response
];
} catch (\Exception $e) {
$costTime = round((microtime(true) - $startTime) * 1000, 2);
$this->recordSyncLog($orderId, 'A3341TP02', $txSeq, null, 'error', false, $costTime, $e->getMessage());
Log::error('建行订单状态更新失败: ' . $e->getMessage());
return [
'status' => false,
'message' => $e->getMessage(),
'data' => null
];
}
}
/**
* 查询建行订单信息
*
* @param string $orderSn 订单号
* @return array
*/
public function queryOrder($orderSn)
{
try {
// 调用建行API查询订单
$response = $this->httpClient->queryOrder($orderSn);
return [
'status' => true,
'message' => '订单查询成功',
'data' => $response
];
} catch (\Exception $e) {
Log::error('建行订单查询失败: ' . $e->getMessage());
return [
'status' => false,
'message' => $e->getMessage(),
'data' => null
];
}
}
/**
* 处理订单退款
*
* @param int $orderId 订单ID
* @param float $refundAmount 退款金额
* @param string $refundReason 退款原因
* @return array
*/
public function refundOrder($orderId, $refundAmount, $refundReason = '')
{
try {
// 获取订单信息
$order = Order::find($orderId);
if (!$order) {
throw new \Exception('订单不存在');
}
// 验证退款金额
if ($refundAmount > $order['total_amount']) {
throw new \Exception('退款金额不能超过订单总额');
}
// 调用建行API发起退款
$response = $this->httpClient->refund(
$order['order_sn'],
number_format($refundAmount, 2, '.', ''),
$refundReason
);
// 更新订单退款状态
$this->updateOrderStatus($orderId, null, '2');
return [
'status' => true,
'message' => '退款申请成功',
'data' => $response
];
} catch (\Exception $e) {
Log::error('建行订单退款失败: ' . $e->getMessage());
return [
'status' => false,
'message' => $e->getMessage(),
'data' => null
];
}
}
/**
* 构建符合建行要求的订单数据
*
* ⚠️ 注意Shopro字段说明
* - total_fee: 实际支付金额
* - total_amount: 订单原价
* - discount_fee: 优惠金额
* - paid_time: 支付时间(毫秒时间戳!)
* - createtime: 创建时间(秒级时间戳)
*
* @param array $order 订单数组
* @param array $orderItems 订单商品列表
* @param string $ccbUserId 建行用户ID
* @return array
*/
private function buildOrderData($order, $orderItems, $ccbUserId)
{
// 构建商品列表
$goodsList = $this->buildGoodsList($orderItems);
// 计算各项金额Shopro使用total_fee作为实付金额
$totalAmount = number_format($order['total_amount'] ?? 0, 2, '.', '');
$payAmount = number_format($order['total_fee'] ?? 0, 2, '.', '');
$discountAmount = number_format($order['discount_fee'] ?? 0, 2, '.', '');
// 处理支付时间Shopro的paid_time是毫秒时间戳需要除以1000
$payTime = '';
if (!empty($order['paid_time'])) {
$payTime = date('YmdHis', intval($order['paid_time'] / 1000));
}
// 处理创建时间Shopro的createtime是秒级时间戳
$createTime = date('YmdHis', $order['createtime'] ?? time());
// 构建订单数据34个必填字段
return [
'USER_ID' => $ccbUserId, // 建行用户ID
'ORDER_ID' => $order['order_sn'], // 订单号
'ORDER_DT' => $createTime, // 订单时间
'TOTAL_AMT' => $totalAmount, // 订单原金额
'PAY_AMT' => $payAmount, // 实付金额total_fee
'DISCOUNT_AMT' => $discountAmount, // 优惠金额discount_fee
'ORDER_STATUS' => $this->mapOrderStatus($order['status']), // 订单状态
'REFUND_STATUS' => $this->mapRefundStatus($order['aftersale_status'] ?? 0), // 退款状态aftersale_status
'MCT_NM' => $this->config['merchant']['name'] ?? '商户名称', // 商户名称
'MCT_ORDER_ID' => $order['id'], // 商户订单ID
'GOODS_LIST' => json_encode($goodsList, JSON_UNESCAPED_UNICODE), // 商品列表
'PAY_TYPE' => $this->mapPayType($order['pay_type'] ?? ''), // 支付方式
'PAY_TIME' => $payTime, // 支付时间(毫秒转秒)
'DELIVERY_TYPE' => '01', // 配送方式01快递
'DELIVERY_STATUS' => $this->mapDeliveryStatus($order['status']), // 配送状态
'DELIVERY_TIME' => $order['delivery_time'] ? date('YmdHis', $order['delivery_time']) : '', // 发货时间
'RECEIVE_NAME' => $order['consignee'] ?? '', // 收货人姓名
'RECEIVE_PHONE' => $order['mobile'] ?? '', // 收货人电话
'RECEIVE_ADDRESS' => $this->buildAddress($order), // 收货地址
'EXPRESS_COMPANY' => $order['express_company'] ?? '', // 快递公司
'EXPRESS_NO' => $order['express_no'] ?? '', // 快递单号
'REMARK' => $order['remark'] ?? '', // 备注
'ORDER_TYPE' => '01', // 订单类型01普通订单
'IS_VIRTUAL' => '0', // 是否虚拟商品
'ORDER_URL' => $this->config['merchant']['order_detail_url'] . $order['id'], // 订单详情链接
'CREATE_TIME' => $createTime, // 创建时间
'UPDATE_TIME' => date('YmdHis'), // 更新时间
'SHOP_ID' => '1', // 店铺ID
'SHOP_NAME' => $this->config['merchant']['name'] ?? '', // 店铺名称
'ACTIVITY_ID' => '', // 活动ID
'ACTIVITY_NAME' => '', // 活动名称
'COUPON_AMT' => '0.00', // 优惠券金额
'FREIGHT_AMT' => number_format($order['freight_amount'] ?? 0, 2, '.', ''), // 运费
];
}
/**
* 构建商品列表
*
* @param array $items 订单商品项
* @return array
*/
private function buildGoodsList($items)
{
$goodsList = [];
foreach ($items as $item) {
$goodsList[] = [
'goods_id' => $item['goods_id'],
'goods_name' => $item['goods_title'],
'goods_price' => number_format($item['goods_price'], 2, '.', ''),
'goods_num' => $item['goods_num'],
'goods_amount' => number_format($item['goods_amount'], 2, '.', ''),
'goods_image' => $item['goods_image'] ?? '',
'goods_sku' => $item['goods_sku_text'] ?? ''
];
}
return $goodsList;
}
/**
* 构建收货地址
*
* @param object $order 订单对象
* @return string
*/
private function buildAddress($order)
{
$address = '';
if ($order['province']) $address .= $order['province'];
if ($order['city']) $address .= $order['city'];
if ($order['area']) $address .= $order['area'];
if ($order['address']) $address .= $order['address'];
return $address;
}
/**
* 记录同步日志
*
* @param int $orderId 订单ID
* @param string $txCode 交易代码
* @param string $txSeq 交易流水号
* @param mixed $data 数据
* @param string $type 类型request/response/error
* @param bool $success 是否成功
* @param float $costTime 耗时(毫秒)
* @param string $errorMsg 错误信息
*/
private function recordSyncLog($orderId, $txCode, $txSeq, $data, $type = 'request', $success = true, $costTime = 0, $errorMsg = '')
{
try {
// 获取订单号
$orderSn = Db::name('shopro_order')->where('id', $orderId)->value('order_sn');
$logData = [
'order_id' => $orderId,
'order_sn' => $orderSn ?: '',
'tx_code' => $txCode,
'tx_seq' => $txSeq,
'sync_status' => $success ? 1 : 0,
'sync_time' => time(),
'cost_time' => intval($costTime),
'retry_times' => 0
];
if ($type == 'request') {
$logData['request_data'] = is_array($data) ? json_encode($data, JSON_UNESCAPED_UNICODE) : $data;
} elseif ($type == 'response') {
$logData['response_data'] = is_array($data) ? json_encode($data, JSON_UNESCAPED_UNICODE) : $data;
} elseif ($type == 'error') {
$logData['error_msg'] = $errorMsg;
}
Db::name('ccb_sync_log')->insert($logData);
} catch (\Exception $e) {
Log::error('记录同步日志失败: ' . $e->getMessage());
}
}
/**
* 更新订单同步状态
*
* @param int $orderId 订单ID
* @param int $status 同步状态0-未同步 1-已同步 2-同步失败
*/
private function updateOrderSyncStatus($orderId, $status)
{
Db::name('shopro_order')->where('id', $orderId)->update([
'ccb_sync_status' => $status,
'ccb_sync_time' => time(),
'updatetime' => time()
]);
}
/**
* 映射订单状态
*
* @param string $status Shopro订单状态
* @return string 建行订单状态
*/
private function mapOrderStatus($status)
{
$statusMap = [
'unpaid' => '0', // 待支付
'paid' => '1', // 已支付
'shipped' => '2', // 已发货
'received' => '3', // 已收货
'completed' => '4', // 已完成
'cancelled' => '5', // 已取消
'refunded' => '6' // 已退款
];
return $statusMap[$status] ?? '0';
}
/**
* 映射退款状态
*
* @param int $refundStatus 退款状态
* @return string
*/
private function mapRefundStatus($refundStatus)
{
if ($refundStatus == 0) return '0'; // 无退款
if ($refundStatus == 1) return '1'; // 退款中
if ($refundStatus == 2) return '2'; // 已退款
return '0';
}
/**
* 映射支付方式
*
* @param string $payType 支付类型
* @return string
*/
private function mapPayType($payType)
{
$payMap = [
'wechat' => '01', // 微信支付
'alipay' => '02', // 支付宝
'ccb' => '03', // 建行支付
'balance' => '04' // 余额支付
];
return $payMap[$payType] ?? '00';
}
/**
* 映射配送状态
*
* @param string $status 订单状态
* @return string
*/
private function mapDeliveryStatus($status)
{
if (in_array($status, ['unpaid', 'paid'])) return '0'; // 未发货
if ($status == 'shipped') return '1'; // 已发货
if (in_array($status, ['received', 'completed'])) return '2'; // 已收货
return '0';
}
/**
* 批量同步订单
* 用于初始化或定时同步
*
* @param array $conditions 查询条件
* @param int $limit 批量数量
* @return array
*/
public function batchSync($conditions = [], $limit = 100)
{
$successCount = 0;
$failCount = 0;
$errors = [];
try {
// 构建查询
$query = Db::name('shopro_order');
if (!empty($conditions)) {
$query->where($conditions);
}
// 查询需要同步的订单
$orders = $query->where('status', '<>', 'cancelled')
->where('ccb_sync_status', 'in', [0, 2]) // 未同步或同步失败的
->limit($limit)
->select();
foreach ($orders as $order) {
$result = $this->pushOrder($order['id']);
if ($result['status']) {
$successCount++;
} else {
$failCount++;
$errors[] = "订单{$order['order_sn']}: {$result['message']}";
}
}
return [
'status' => true,
'message' => "批量同步完成",
'data' => [
'total' => count($orders),
'success' => $successCount,
'fail' => $failCount,
'errors' => $errors
]
];
} catch (\Exception $e) {
return [
'status' => false,
'message' => '批量同步失败: ' . $e->getMessage(),
'data' => null
];
}
}
}