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

636 lines
23 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()
{
// 加载插件配置文件
$configFile = __DIR__ . '/../../config/ccblife.php';
if (file_exists($configFile)) {
$this->config = include $configFile;
} else {
throw new \Exception('建行生活配置文件不存在');
}
// 处理BASE64格式的密钥
$this->config = $this->processPemKeys($this->config);
$this->httpClient = new CcbHttpClient($this->config);
}
/**
* 处理PEM格式密钥
*
* @param array $config
* @return array
*/
private function processPemKeys($config)
{
if (!empty($config['private_key']) && strpos($config['private_key'], '-----BEGIN') === false) {
$config['private_key'] = "-----BEGIN PRIVATE KEY-----\n"
. chunk_split($config['private_key'], 64, "\n")
. "-----END PRIVATE KEY-----";
}
if (!empty($config['public_key']) && strpos($config['public_key'], '-----BEGIN') === false) {
$config['public_key'] = "-----BEGIN PUBLIC KEY-----\n"
. chunk_split($config['public_key'], 64, "\n")
. "-----END PUBLIC KEY-----";
}
if (empty($config['merchant_public_key'])) {
$config['merchant_public_key'] = $config['public_key'];
}
// 处理平台公钥
if (!empty($config['platform_public_key'])) {
if (strpos($config['platform_public_key'], '-----BEGIN') === false) {
$config['platform_public_key'] = "-----BEGIN PUBLIC KEY-----\n"
. chunk_split($config['platform_public_key'], 64, "\n")
. "-----END PUBLIC KEY-----";
}
} else {
$config['platform_public_key'] = $config['public_key'];
}
return $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字段说明
* - pay_fee: 实际支付金额Shopro字段
* - order_amount: 订单总金额Shopro字段
* - total_discount_fee: 优惠总金额Shopro字段
* - paid_time: 支付时间毫秒时间戳需除以1000
* - createtime: 创建时间毫秒时间戳需除以1000
*
* @param array $order 订单数组
* @param array $orderItems 订单商品列表
* @param string $ccbUserId 建行用户ID
* @return array
*/
private function buildOrderData($order, $orderItems, $ccbUserId)
{
// 构建商品列表
$goodsList = $this->buildGoodsList($orderItems);
// 计算各项金额Shopro字段pay_fee=实付金额order_amount=订单总金额)
$totalAmount = number_format($order['order_amount'] ?? 0, 2, '.', '');
$payAmount = number_format($order['pay_fee'] ?? 0, 2, '.', '');
$discountAmount = number_format($order['total_discount_fee'] ?? 0, 2, '.', '');
// 处理支付时间Shopro的paid_time是毫秒时间戳需要除以1000
$payTime = '';
if (!empty($order['paid_time']) && is_numeric($order['paid_time'])) {
$payTime = date('YmdHis', intval($order['paid_time'] / 1000));
}
// 处理创建时间Shopro的createtime是毫秒时间戳需要除以1000
$createTimeValue = $order['createtime'] ?? null;
if (empty($createTimeValue) || !is_numeric($createTimeValue)) {
$createTimeValue = time() * 1000; // 当前时间的毫秒时间戳
}
$createTime = date('YmdHis', intval($createTimeValue / 1000));
// 获取订单地址信息Shopro将地址存储在单独的表中
$orderAddress = Db::name('shopro_order_address')
->where('order_id', $order['id'])
->find();
// 获取支付方式Shopro将支付信息存储在单独的表中
$payInfo = Db::name('shopro_pay')
->where('order_id', $order['id'])
->where('status', 'paid')
->find();
// 获取快递信息Shopro将快递信息存储在单独的表中
$expressInfo = Db::name('shopro_order_express')
->where('order_id', $order['id'])
->find();
// 构建订单数据34个必填字段
return [
'USER_ID' => $ccbUserId, // 建行用户ID
'ORDER_ID' => $order['order_sn'], // 订单号
'ORDER_DT' => $createTime, // 订单时间
'TOTAL_AMT' => $totalAmount, // 订单原金额
'PAY_AMT' => $payAmount, // 实付金额
'DISCOUNT_AMT' => $discountAmount, // 优惠金额
'ORDER_STATUS' => $this->mapOrderStatus($order['status']), // 订单状态
'REFUND_STATUS' => '0', // 退款状态(默认无退款)
'MCT_NM' => $this->config['merchant']['name'] ?? '商户名称', // 商户名称
'MCT_ORDER_ID' => $order['id'], // 商户订单ID
'GOODS_LIST' => json_encode($goodsList, JSON_UNESCAPED_UNICODE), // 商品列表
'PAY_TYPE' => $this->mapPayType($payInfo['pay_type'] ?? ''), // 支付方式(从支付表获取)
'PAY_TIME' => $payTime, // 支付时间(毫秒转秒)
'DELIVERY_TYPE' => '01', // 配送方式01快递
'DELIVERY_STATUS' => $this->mapDeliveryStatus($order['status']), // 配送状态
'DELIVERY_TIME' => !empty($expressInfo['createtime']) ? date('YmdHis', intval($expressInfo['createtime'] / 1000)) : '', // 发货时间
'RECEIVE_NAME' => $orderAddress['consignee'] ?? '', // 收货人姓名(从地址表获取)
'RECEIVE_PHONE' => $orderAddress['mobile'] ?? '', // 收货人电话(从地址表获取)
'RECEIVE_ADDRESS' => $this->buildAddress($order), // 收货地址(从地址表获取)
'EXPRESS_COMPANY' => $expressInfo['express_name'] ?? '', // 快递公司(从快递表获取)
'EXPRESS_NO' => $expressInfo['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' => number_format($order['coupon_discount_fee'] ?? 0, 2, '.', ''), // 优惠券金额
'FREIGHT_AMT' => number_format($order['dispatch_amount'] ?? 0, 2, '.', ''), // 运费Shopro字段名为dispatch_amount
];
}
/**
* 构建商品列表
*
* @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;
}
/**
* 构建收货地址
*
* ⚠️ 注意Shopro的收货地址存储在单独的表 shopro_order_address 中
*
* @param array $order 订单数组
* @return string
*/
private function buildAddress($order)
{
// 从订单地址表获取地址信息
$orderAddress = Db::name('shopro_order_address')
->where('order_id', $order['id'])
->find();
if (!$orderAddress) {
return '';
}
$address = '';
if (!empty($orderAddress['province_name'])) $address .= $orderAddress['province_name'];
if (!empty($orderAddress['city_name'])) $address .= $orderAddress['city_name'];
if (!empty($orderAddress['district_name'])) $address .= $orderAddress['district_name'];
if (!empty($orderAddress['address'])) $address .= $orderAddress['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
];
}
}
}