fengketrade/addons/shopro/library/ccblife/CcbOrderService.php
2025-10-22 20:32:53 +08:00

617 lines
24 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('建行生活配置文件不存在');
}
// ✅ 修复: 删除processPemKeys()调用
// 密钥格式化统一由CcbRSA类处理避免重复格式化导致OpenSSL ASN1解析错误
// CcbRSA::formatPublicKey/formatPrivateKey 会在加密/解密时自动处理密钥格式
$this->httpClient = new CcbHttpClient($this->config);
}
/**
* 推送订单到建行生活平台
* 当用户下单后调用此方法同步订单信息
*
* @param int $orderId Shopro订单ID
* @param string $payFlowId 支付流水号(由控制器统一生成)
* @return array ['status' => bool, 'message' => string, 'data' => array]
* @throws \Exception
*/
public function pushOrder($orderId, $payFlowId)
{
$startTime = microtime(true);
try {
// ✅ 验证支付流水号
if (empty($payFlowId)) {
throw new \Exception('支付流水号不能为空');
}
// 获取订单信息
$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, $payFlowId);
// 记录请求数据(同步日志)
$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|null $status 支付状态0-待支付 1-支付成功 2-已过期 3-支付失败 4-取消)
* @param string|null $refundStatus 退款状态0-无退款 1-退款申请 2-已退款 3-部分退款)
* @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('订单不存在');
}
// 获取支付流水号
$payFlowId = $order['pay_flow_id'] ?? null;
if (empty($payFlowId)) {
throw new \Exception('订单缺少支付流水号,无法更新状态到建行');
}
// 获取支付商户号
$payMrchId = $this->config['merchant_id'] ?? null;
if (empty($payMrchId)) {
throw new \Exception('配置中缺少支付商户号merchant_id');
}
// 映射订单状态
$payStatus = $status ?: $this->mapOrderStatus($order['status']);
$mappedRefundStatus = $refundStatus ?: $this->mapRefundStatus($order['refund_status'] ?? 0);
// 确定通知类型0-支付状态修改 1-退款状态修改)
$informId = !empty($refundStatus) && $refundStatus != '0' ? '1' : '0';
// 构建额外参数
$additionalParams = [];
if ($informId == '0') {
// 支付状态修改
$additionalParams['PAY_STATUS'] = $payStatus;
$additionalParams['PAY_AMT'] = number_format($order['pay_fee'] ?? 0, 2, '.', '');
} else {
// 退款状态修改
$additionalParams['REFUND_STATUS'] = $mappedRefundStatus;
$additionalParams['TOTAL_REFUND_AMT'] = number_format($order['refund_fee'] ?? 0, 2, '.', '');
}
// 添加其他可选参数
$additionalParams['DISCOUNT_AMT'] = number_format($order['total_discount_fee'] ?? 0, 2, '.', '');
if (!empty($order['goods_name'])) {
$additionalParams['GOODS_NM'] = mb_substr($order['goods_name'], 0, 200);
}
// 记录请求
$requestData = [
'order_id' => $order['order_sn'],
'inform_id' => $informId,
'pay_flow_id' => $payFlowId,
'pay_mrch_id' => $payMrchId,
'additional_params' => $additionalParams
];
$this->recordSyncLog($orderId, 'A3341TP02', $txSeq, $requestData, 'request');
// 调用建行API更新状态使用新接口
$response = $this->httpClient->updateOrderStatus(
$order['order_sn'], // 订单编号
$informId, // 通知类型
$payFlowId, // 支付流水号
$payMrchId, // 支付商户号
$additionalParams // 额外参数
);
// 记录响应
$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 订单号支付流水号对应收银台ORDERID字段
* @param string|null $startTime 开始时间格式yyyyMMddHHmmss默认7天前
* @param string|null $endTime 结束时间格式yyyyMMddHHmmss默认当前时间
* @param int $page 页码默认1
* @param string $txType 交易类型0-支付交易 1-退款交易 a-查询可退款订单)
* @param string $txnStatus 交易状态00-成功 01-失败 02-不确定)
* @return array
*/
public function queryOrder($orderSn, $startTime = null, $endTime = null, $page = 1, $txType = '0', $txnStatus = '00')
{
try {
// 调用建行API查询订单使用新接口
$response = $this->httpClient->queryOrder(
$orderSn,
$startTime,
$endTime,
$page,
$txType,
$txnStatus
);
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|null $refundCode 退款流水号(可选,建议填写用于查询退款结果)
* @return array
*/
public function refundOrder($orderId, $refundAmount, $refundCode = null)
{
try {
// 获取订单信息(需要支付流水号和支付时间)
$order = Db::name('shopro_order')->where('id', $orderId)->find();
if (!$order) {
throw new \Exception('订单不存在');
}
// 验证退款金额
if ($refundAmount > $order['order_amount']) {
throw new \Exception('退款金额不能超过订单总额');
}
// 获取支付流水号(必须)
$payFlowId = $order['pay_flow_id'] ?? null;
if (empty($payFlowId)) {
throw new \Exception('订单缺少支付流水号,无法执行退款');
}
// 获取支付时间(用于计算查询时间范围)
$payTime = $order['pay_time'] ?? $order['createtime'];
if (empty($payTime)) {
throw new \Exception('订单缺少支付时间,无法执行退款');
}
// 调用建行API发起退款使用新接口
$response = $this->httpClient->refund(
$payFlowId, // 支付流水号对应收银台ORDERID
$refundAmount, // 退款金额
$payTime, // 支付时间(用于计算查询时间范围)
$refundCode // 退款流水号(可选)
);
// 更新订单退款状态
$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
];
}
}
/**
* 构建符合建行 A3341TP01 接口规范的订单数据
*
* 📋 建行生活订单推送接口规范说明v1.1.6
*
* 必填字段11个
* - USER_ID: 客户编号建行用户ID
* - ORDER_ID: 订单号
* - ORDER_DT: 订单日期yyyyMMddHHmmss格式
* - TOTAL_AMT: 订单原金额
* - ORDER_STATUS: 订单状态
* - REFUND_STATUS: 退款状态
* - MCT_NM: 商户名称
* - CUS_ORDER_URL: 订单详情链接
* - PAY_FLOW_ID: 支付流水号
* - PAY_MRCH_ID: 支付商户号
* - SKU_LIST: 商品信息JSON字符串
*
* 重要可选字段(建议必填):
* - PAY_AMT: 订单实际支付金额(文档要求:如为空必须在状态变更时推送)
* - DISCOUNT_AMT: 第三方平台优惠金额(文档要求:如为空必须在状态变更时推送)
* - DISCOUNT_AMT_DESC: 第三方平台优惠说明
* - INV_DT: 订单过期日期
* - GOODS_NM: 商品名称
* - PREFTL_MRCH_ID: 门店商户号
* - PLAT_MCT_ID: 服务商门店编号
* - PLAT_ORDER_TYPE: 服务方订单类型
* - PLATFORM: 下单场景
*
* ⚠️ 注意Shopro字段映射
* - pay_fee → PAY_AMT实际支付金额
* - order_amount → TOTAL_AMT订单总金额
* - total_discount_fee → DISCOUNT_AMT优惠总金额
* - createtime → ORDER_DT毫秒时间戳需除以1000
* - expiry_time → INV_DT过期时间
*
* @param array $order 订单数组
* @param array $orderItems 订单商品列表
* @param string $ccbUserId 建行用户ID
* @return array
*/
private function buildOrderData($order, $orderItems, $ccbUserId, $payFlowId)
{
// ✅ 使用控制器传入的统一支付流水号(确保与支付串生成使用同一流水号)
if (empty($payFlowId)) {
throw new \Exception('支付流水号不能为空');
}
// 构建SKU商品列表JSON字符串格式
$skuList = $this->buildSkuList($orderItems);
// 计算各项金额保留2位小数
$totalAmount = number_format($order['order_amount'] ?? 0, 2, '.', '');
$payAmount = number_format($order['pay_fee'] ?? $order['order_amount'] ?? 0, 2, '.', '');
$discountAmount = number_format($order['total_discount_fee'] ?? 0, 2, '.', '');
$totalRefundAmount = number_format($order['refund_fee'] ?? 0, 2, '.', '');
// 处理订单时间(自动判断秒级或毫秒级时间戳)
$createTimeValue = $order['createtime'] ?? null;
if (empty($createTimeValue) || !is_numeric($createTimeValue)) {
$createTimeValue = time(); // 使用当前秒级时间戳
}
// 判断时间戳类型大于9999999999说明是毫秒级13位数否则是秒级10位数
if ($createTimeValue > 9999999999) {
// 毫秒级时间戳除以1000转为秒
$timestamp = intval($createTimeValue / 1000);
} else {
// 秒级时间戳,直接使用
$timestamp = intval($createTimeValue);
}
$orderDt = date('YmdHis', $timestamp);
// 处理订单过期时间
$invDt = '';
if (!empty($order['expiry_time'])) {
// Shopro 的 expiry_time 可能是时间戳或日期字符串
if (is_numeric($order['expiry_time'])) {
// 如果是毫秒时间戳需要除以1000
$timestamp = intval($order['expiry_time']);
if ($timestamp > 9999999999) {
$timestamp = intval($timestamp / 1000);
}
$invDt = date('YmdHis', $timestamp);
} else {
$invDt = date('YmdHis', strtotime($order['expiry_time']));
}
}
// 获取商品名称(取第一个商品)
$goodsName = '';
if (!empty($orderItems)) {
$goodsName = $orderItems[0]['goods_title'] ?? '';
// 如果有多个商品,可以拼接
if (count($orderItems) > 1) {
$goodsName .= ' 等' . count($orderItems) . '件商品';
}
}
// 构建优惠说明(如果有优惠金额)
$discountAmtDesc = '';
if ($discountAmount > 0) {
// 格式:名称=金额|@|名称=金额
// 这里简化处理,实际应该根据具体优惠券信息构建
$discountAmtDesc = '平台优惠=' . $discountAmount;
}
// 构建符合A3341TP01接口规范的订单数据
$orderData = [
// ========== 必填字段 ==========
'USER_ID' => $ccbUserId, // 客户编号
'ORDER_ID' => $order['order_sn'], // 订单号
'ORDER_DT' => $orderDt, // 订单日期yyyyMMddHHmmss
'TOTAL_AMT' => $totalAmount, // 订单原金额
'ORDER_STATUS' => $this->mapOrderStatus($order['status']), // 订单状态
'REFUND_STATUS' => $this->mapRefundStatus($order['refund_status'] ?? 0), // 退款状态
'MCT_NM' => $this->config['merchant']['name'] ?? '商户名称', // 商户名称
'PAY_FLOW_ID' => $payFlowId, // ✅ 支付流水号(使用控制器传入的统一流水号)
'PAY_MRCH_ID' => $this->config['merchant_id'], // 支付商户号(必填!)
'SKU_LIST' => $skuList, // 商品信息JSON字符串必填
// ========== 重要可选字段(强烈建议填写) ==========
'PAY_AMT' => $payAmount, // 订单实际支付金额
'DISCOUNT_AMT' => $discountAmount, // 第三方平台优惠金额
'PLAT_ORDER_TYPE' => 'T0000', // 服务方订单类型T0000-普通类型)
'PLATFORM' => '99', // 下单场景99-建行生活APP
];
// ========== 条件可选字段(有值才添加) ==========
// 优惠说明
if (!empty($discountAmtDesc)) {
$orderData['DISCOUNT_AMT_DESC'] = $discountAmtDesc;
}
// 订单过期时间
if (!empty($invDt)) {
$orderData['INV_DT'] = $invDt;
}
// 商品名称
if (!empty($goodsName)) {
$orderData['GOODS_NM'] = mb_substr($goodsName, 0, 200); // 限制长度200字符
}
// 累计退款金额(如果有退款)
if ($totalRefundAmount > 0) {
$orderData['TOTAL_REFUND_AMT'] = $totalRefundAmount;
}
return $orderData;
}
/**
* 构建符合建行规范的SKU商品列表JSON字符串格式
*
* 📋 建行 SKU_LIST 字段规范:
*
* 必填字段4个
* - SKU_NAME: 商品名称(必填)
* - SKU_REF_PRICE: 商品参考价必填支持小数最多2位
* - SKU_NUM: 商品数量必填支持小数最多1位
* - SKU_SELL_PRICE: 商品售价必填支持小数最多2位
*
* ⚠️ 注意Shopro字段映射
* - goods_title → SKU_NAME商品名称
* - goods_original_price → SKU_REF_PRICE商品原价作为参考价
* - goods_num → SKU_NUM购买数量
* - goods_price → SKU_SELL_PRICE商品实际售价
*
* @param array $items 订单商品项数组
* @return string JSON字符串格式的SKU列表
*/
private function buildSkuList($items)
{
$skuList = [];
foreach ($items as $item) {
$skuList[] = [
'SKU_NAME' => $item['goods_title'], // 商品名称(必填)
'SKU_REF_PRICE' => number_format($item['goods_original_price'] ?? $item['goods_price'], 2, '.', ''), // 商品参考价(必填)
'SKU_NUM' => $item['goods_num'], // 商品数量(必填)
'SKU_SELL_PRICE' => number_format($item['goods_price'], 2, '.', ''), // 商品售价(必填)
];
}
// 返回JSON字符串不转义Unicode保持中文可读
return json_encode($skuList, JSON_UNESCAPED_UNICODE);
}
/**
* 记录同步日志
*
* @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';
}
}