diff --git a/addons/shopro/controller/Ccbpayment.php b/addons/shopro/controller/Ccbpayment.php index 12668a7..306fad4 100644 --- a/addons/shopro/controller/Ccbpayment.php +++ b/addons/shopro/controller/Ccbpayment.php @@ -95,11 +95,11 @@ class Ccbpayment extends Common } // 5. 保存支付流水号到订单 - $order->ccb_pay_flow_id = $result['data']['order_sn']; // 使用订单号作为流水号 + $order->ccb_pay_flow_id = $result['data']['pay_flow_id']; // ✅ 使用真实的支付流水号 $order->save(); // 6. 记录支付日志 - $this->savePaymentLog($order, $result['data']['payment_string'], $result['data']['order_sn']); + $this->savePaymentLog($order, $result['data']['payment_string'], $result['data']['pay_flow_id']); // 7. 返回支付串 $this->success('支付串生成成功', [ @@ -108,7 +108,7 @@ class Ccbpayment extends Common 'mac' => $result['data']['mac'], 'order_id' => $order->id, 'order_sn' => $order->order_sn, - 'pay_flow_id' => $result['data']['order_sn'], + 'pay_flow_id' => $result['data']['pay_flow_id'], // ✅ 返回真实的支付流水号 'amount' => $result['data']['amount'], ]); @@ -210,23 +210,46 @@ class Ccbpayment extends Common * 建行支付成功后,会向notify_url发送支付通知 * 这是服务器到服务器的回调,需要验签 * + * ⚠️ 重要:此接口为建行服务器异步回调,必须返回纯文本 'SUCCESS' 或 'FAIL' + * * @return void */ public function notify() { try { - // TODO: 实现建行支付通知处理逻辑 - // 1. 接收建行推送的支付结果 - // 2. 验签 - // 3. 更新订单状态 - // 4. 返回success给建行 + // 1. 获取原始请求数据 + $rawData = file_get_contents('php://input'); + Log::info('[建行通知] 收到异步通知: ' . $rawData); - $this->success('SUCCESS'); + // 2. 解析POST参数 + $params = $this->request->post(); + + // 3. 如果POST为空,尝试解析原始数据 + if (empty($params) && $rawData) { + parse_str($rawData, $params); + } + + // 4. 记录参数 + Log::info('[建行通知] 解析参数: ' . json_encode($params, JSON_UNESCAPED_UNICODE)); + + // 5. 验证必需参数 + if (empty($params['ORDERID'])) { + Log::error('[建行通知] 缺少ORDERID参数'); + echo 'FAIL'; + return; + } + + // 6. 调用支付服务处理通知 + $result = $this->paymentService->handleNotify($params); + + // 7. 返回处理结果 + echo $result; // 'SUCCESS' 或 'FAIL' + + Log::info('[建行通知] 处理完成: ' . $result); } catch (Exception $e) { Log::error('[建行通知] 处理失败 error:' . $e->getMessage()); - - $this->error('FAIL'); + echo 'FAIL'; } } diff --git a/addons/shopro/library/ccblife/CcbMD5.php b/addons/shopro/library/ccblife/CcbMD5.php index 9ad5d0b..fcab0e1 100644 --- a/addons/shopro/library/ccblife/CcbMD5.php +++ b/addons/shopro/library/ccblife/CcbMD5.php @@ -10,10 +10,17 @@ class CcbMD5 { /** * 生成API消息签名 - * 使用大写MD5(源报文 + 私钥) + * + * ⚠️ 注意:建行API接口签名使用大写MD5 + * 签名规则:MD5(JSON报文 + 服务方私钥) → 转大写 + * + * 示例: + * message = '{"CLD_HEADER":{...},"CLD_BODY":{...}}' + * privateKey = 'MIICeAIBA...' + * sign = strtoupper(md5(message + privateKey)) * * @param string $message JSON格式的源报文 - * @param string $privateKey 商户私钥(BASE64格式) + * @param string $privateKey 服务方私钥(BASE64格式) * @return string 大写的32位MD5签名 */ public static function signApiMessage($message, $privateKey) @@ -21,7 +28,7 @@ class CcbMD5 // 移除私钥中的空格和换行 $privateKey = preg_replace('/\s+/', '', $privateKey); - // 计算MD5并转大写 + // 计算MD5并转大写(建行要求) return strtoupper(md5($message . $privateKey)); } @@ -41,8 +48,13 @@ class CcbMD5 /** * 生成支付字符串签名 - * 使用小写MD5(支付字符串) - * 支付字符串格式: MERCHANTID=xxx&POSID=xxx&BRANCHID=xxx&ORDERID=xxx&PAYMENT=xxx&CURCODE=01&TXCODE=530550&REMARK1=&REMARK2=&TIMEOUT=商户私钥 + * + * ⚠️ 注意:建行收银台支付串签名使用小写MD5 + * 签名规则:MD5(支付参数按顺序拼接 + 商户私钥) → 保持小写 + * + * 示例: + * paymentString = 'MERCHANTID=xxx&POSID=xxx&...&TIMEOUT=xxx' + privateKey + * mac = md5(paymentString) // 小写 * * @param array $params 支付参数 * @param string $privateKey 商户私钥 @@ -73,7 +85,7 @@ class CcbMD5 // 拼接支付字符串并添加私钥 $paymentString = implode('&', $parts) . $privateKey; - // 计算MD5(保持小写) + // 计算MD5(保持小写,建行要求) return md5($paymentString); } diff --git a/addons/shopro/library/ccblife/CcbOrderService.php b/addons/shopro/library/ccblife/CcbOrderService.php index 186d55e..6372d67 100644 --- a/addons/shopro/library/ccblife/CcbOrderService.php +++ b/addons/shopro/library/ccblife/CcbOrderService.php @@ -261,6 +261,13 @@ class CcbOrderService /** * 构建符合建行要求的订单数据 * + * ⚠️ 注意:Shopro字段说明 + * - total_fee: 实际支付金额 + * - total_amount: 订单原价 + * - discount_fee: 优惠金额 + * - paid_time: 支付时间(毫秒时间戳!) + * - createtime: 创建时间(秒级时间戳) + * * @param array $order 订单数组 * @param array $orderItems 订单商品列表 * @param string $ccbUserId 建行用户ID @@ -271,26 +278,35 @@ class CcbOrderService // 构建商品列表 $goodsList = $this->buildGoodsList($orderItems); - // 计算各项金额 - $totalAmount = number_format($order['total_amount'], 2, '.', ''); - $payAmount = number_format($order['pay_amount'] ?? $order['total_amount'], 2, '.', ''); - $discountAmount = number_format($order['discount_amount'] ?? 0, 2, '.', ''); + // 计算各项金额(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' => date('YmdHis', strtotime($order['createtime'])), // 订单时间 - 'TOTAL_AMT' => $totalAmount, // 订单总金额 - 'PAY_AMT' => $payAmount, // 实付金额 - 'DISCOUNT_AMT' => $discountAmount, // 优惠金额 + '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['refund_status'] ?? 0), // 退款状态 + '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' => $order['paytime'] ? date('YmdHis', $order['paytime']) : '', // 支付时间 + 'PAY_TIME' => $payTime, // 支付时间(毫秒转秒) 'DELIVERY_TYPE' => '01', // 配送方式(01快递) 'DELIVERY_STATUS' => $this->mapDeliveryStatus($order['status']), // 配送状态 'DELIVERY_TIME' => $order['delivery_time'] ? date('YmdHis', $order['delivery_time']) : '', // 发货时间 @@ -303,7 +319,7 @@ class CcbOrderService 'ORDER_TYPE' => '01', // 订单类型(01普通订单) 'IS_VIRTUAL' => '0', // 是否虚拟商品 'ORDER_URL' => $this->config['merchant']['order_detail_url'] . $order['id'], // 订单详情链接 - 'CREATE_TIME' => date('YmdHis'), // 创建时间 + 'CREATE_TIME' => $createTime, // 创建时间 'UPDATE_TIME' => date('YmdHis'), // 更新时间 'SHOP_ID' => '1', // 店铺ID 'SHOP_NAME' => $this->config['merchant']['name'] ?? '', // 店铺名称 diff --git a/addons/shopro/library/ccblife/CcbPaymentService.php b/addons/shopro/library/ccblife/CcbPaymentService.php index 04cf0d1..da30a9b 100644 --- a/addons/shopro/library/ccblife/CcbPaymentService.php +++ b/addons/shopro/library/ccblife/CcbPaymentService.php @@ -35,6 +35,8 @@ class CcbPaymentService * 生成建行支付串 * 用于前端JSBridge调用建行收银台 * + * ⚠️ 注意:必须包含所有必需参数,签名前按ASCII排序 + * * @param int $orderId Shopro订单ID * @return array ['status' => bool, 'message' => string, 'data' => array] */ @@ -58,32 +60,75 @@ class CcbPaymentService throw new \Exception('用户未绑定建行账号'); } - // 生成支付串签名(Shopro 使用 total_fee 作为支付金额字段) - $result = CcbMD5::generatePaymentSignature( - $this->config['merchant_id'], - $this->config['pos_id'], - $this->config['branch_id'], - $order['order_sn'], - number_format($order['total_fee'], 2, '.', ''), // 使用 total_fee - $this->config['private_key'], - '01', // 币种:人民币 - '530550' // 交易码 - ); + // 生成支付流水号(使用订单号作为唯一标识) + $payFlowId = 'PAY' . date('YmdHis') . mt_rand(100000, 999999); + + // 构建完整的支付参数(34个参数) + $paymentParams = [ + 'MERCHANTID' => $this->config['merchant_id'], // 商户代码 + 'POSID' => $this->config['pos_id'], // 柜台代码 + 'BRANCHID' => $this->config['branch_id'], // 分行代码 + 'ORDERID' => $payFlowId, // 支付流水号(必须唯一!) + 'PAYMENT' => number_format($order['total_fee'], 2, '.', ''), // 支付金额 + 'CURCODE' => '01', // 币种(01=人民币) + 'TXCODE' => '520100', // 交易码(520100=即时支付) + 'REMARK1' => '', // 备注1 + 'REMARK2' => $this->config['service_id'], // 备注2(服务方编号) + 'TYPE' => '1', // 支付类型(1=个人) + 'GATEWAY' => '0', // 网关标志 + 'CLIENTIP' => request()->ip(), // 客户端IP + 'REGINFO' => '', // 注册信息 + 'PROINFO' => $this->buildProductInfo($order), // 商品信息 + 'REFERER' => '', // 来源页面 + 'THIRDAPPINFO' => 'comccbpay1234567890cloudmerchant', // 第三方应用信息(固定值) + 'USER_ORDERID' => $order['order_sn'], // 商户订单号 + 'TIMEOUT' => date('YmdHis', strtotime('+30 minutes')) // 超时时间 + ]; + + // 按ASCII排序 + ksort($paymentParams); + + // 生成签名字符串 + $signString = http_build_query($paymentParams); + + // ⚠️ 注意:建行支付串签名规则 + // 签名 = MD5(参数字符串 + 服务方私钥) + // 不需要PLATFORMPUB字段,直接使用私钥签名 + $mac = md5($signString . $this->config['private_key']); + + // 使用RSA加密商户公钥(用于ENCPUB字段) + $encryption = new CcbEncryption($this->config); + $encpub = $encryption->encryptMerchantPublicKey(); + + // 组装最终支付串 + $finalPaymentString = $signString . '&MAC=' . $mac . '&PLATFORMID=' . $this->config['service_id'] . '&ENCPUB=' . urlencode($encpub); + + // 保存支付流水号到订单 + Order::where('id', $orderId)->update([ + 'ccb_pay_flow_id' => $payFlowId, + 'updatetime' => time() + ]); // 构建完整的支付URL - $paymentUrl = $this->buildPaymentUrl($result['params'], $result['mac']); + $paymentUrl = $this->config['cashier_url'] . '?' . $finalPaymentString; // 记录支付请求 - $this->recordPaymentRequest($orderId, $result); + $this->recordPaymentRequest($orderId, [ + 'payment_string' => $finalPaymentString, + 'params' => $paymentParams, + 'mac' => $mac, + 'pay_flow_id' => $payFlowId + ]); return [ 'status' => true, 'message' => '支付串生成成功', 'data' => [ - 'payment_string' => $result['payment_string'], - 'mac' => $result['mac'], + 'payment_string' => $finalPaymentString, + 'mac' => $mac, 'payment_url' => $paymentUrl, 'order_sn' => $order['order_sn'], + 'pay_flow_id' => $payFlowId, 'amount' => number_format($order['total_fee'], 2, '.', '') ] ]; @@ -98,6 +143,28 @@ class CcbPaymentService } } + /** + * 构建商品信息字符串 + * + * @param object $order 订单对象 + * @return string + */ + private function buildProductInfo($order) + { + // 获取订单商品 + $orderItems = Db::name('shopro_order_item') + ->where('order_id', $order['id']) + ->limit(3) // 最多取3个商品 + ->column('goods_title'); + + if (empty($orderItems)) { + return '商城订单'; + } + + // 拼接商品名称 + return implode(',', $orderItems); + } + /** * 处理支付回调 * 建行支付完成后的同步回调 @@ -117,13 +184,14 @@ class CcbPaymentService } // 获取关键参数 - $orderSn = $params['ORDERID'] ?? ''; + $payFlowId = $params['ORDERID'] ?? ''; // 支付流水号 + $userOrderId = $params['USER_ORDERID'] ?? ''; // 商户订单号 $posId = $params['POSID'] ?? ''; $success = $params['SUCCESS'] ?? 'N'; // 验证参数 - if (empty($orderSn)) { - throw new \Exception('订单号不能为空'); + if (empty($payFlowId)) { + throw new \Exception('支付流水号不能为空'); } // 验证POS号 @@ -131,8 +199,14 @@ class CcbPaymentService throw new \Exception('POS号验证失败'); } - // 查询订单 - $order = Order::where('order_sn', $orderSn)->find(); + // ⚠️ 重要:ORDERID是支付流水号,不是订单号! + // 优先使用USER_ORDERID查询,如果没有则用ccb_pay_flow_id查询 + if (!empty($userOrderId)) { + $order = Order::where('order_sn', $userOrderId)->find(); + } else { + $order = Order::where('ccb_pay_flow_id', $payFlowId)->find(); + } + if (!$order) { throw new \Exception('订单不存在'); } @@ -150,7 +224,8 @@ class CcbPaymentService 'message' => '支付成功', 'data' => [ 'order_id' => $order['id'], - 'order_sn' => $orderSn, + 'order_sn' => $order['order_sn'], // ✅ 返回真正的订单号 + 'pay_flow_id' => $payFlowId, // 支付流水号 'amount' => $params['PAYMENT'] ?? '' ] ]; @@ -161,7 +236,8 @@ class CcbPaymentService 'message' => '支付失败', 'data' => [ 'order_id' => $order['id'], - 'order_sn' => $orderSn, + 'order_sn' => $order['order_sn'], // ✅ 返回真正的订单号 + 'pay_flow_id' => $payFlowId, // 支付流水号 'error_code' => $params['ERRCODE'] ?? '', 'error_msg' => $params['ERRMSG'] ?? '' ] @@ -193,9 +269,16 @@ class CcbPaymentService throw new \Exception('签名验证失败'); } - // 获取订单信息 - $orderSn = $params['ORDERID'] ?? ''; - $order = Order::where('order_sn', $orderSn)->find(); + // ⚠️ 重要:ORDERID是支付流水号,不是订单号! + // 优先使用USER_ORDERID查询,如果没有则用ccb_pay_flow_id查询 + $payFlowId = $params['ORDERID'] ?? ''; // 支付流水号 + $userOrderId = $params['USER_ORDERID'] ?? ''; // 商户订单号 + + if (!empty($userOrderId)) { + $order = Order::where('order_sn', $userOrderId)->find(); + } else { + $order = Order::where('ccb_pay_flow_id', $payFlowId)->find(); + } if (!$order) { throw new \Exception('订单不存在'); @@ -339,7 +422,7 @@ class CcbPaymentService Db::name('ccb_payment_log')->insert([ 'order_id' => $orderId, 'order_sn' => $order['order_sn'], - 'pay_flow_id' => $order['order_sn'], // 使用订单号作为流水号 + 'pay_flow_id' => $paymentData['pay_flow_id'] ?? '', // ✅ 使用真实的支付流水号 'payment_string' => $paymentData['payment_string'] ?? '', 'user_id' => $order['user_id'], 'ccb_user_id' => $user['ccb_user_id'] ?? '', diff --git a/addons/shopro/library/ccblife/model/CcbPaymentLog.php b/addons/shopro/library/ccblife/model/CcbPaymentLog.php new file mode 100644 index 0000000..ad1b281 --- /dev/null +++ b/addons/shopro/library/ccblife/model/CcbPaymentLog.php @@ -0,0 +1,206 @@ + 'integer', + 'user_id' => 'integer', + 'amount' => 'float', + 'status' => 'integer', + 'pay_time' => 'integer', + 'create_time' => 'integer', + ]; + + /** + * 支付状态常量 + */ + const STATUS_PENDING = 0; // 待支付 + const STATUS_SUCCESS = 1; // 支付成功 + const STATUS_FAILED = 2; // 支付失败 + const STATUS_CANCELLED = 3; // 已取消 + + /** + * 支付状态映射 + * @var array + */ + public static $statusMap = [ + self::STATUS_PENDING => '待支付', + self::STATUS_SUCCESS => '支付成功', + self::STATUS_FAILED => '支付失败', + self::STATUS_CANCELLED => '已取消', + ]; + + /** + * 获取状态文本 + * + * @param int $status 状态值 + * @return string + */ + public static function getStatusText($status) + { + return self::$statusMap[$status] ?? '未知状态'; + } + + /** + * 根据订单ID查找支付日志 + * + * @param int $orderId 订单ID + * @return CcbPaymentLog|null + */ + public static function findByOrderId($orderId) + { + return self::where('order_id', $orderId) + ->order('create_time', 'desc') + ->find(); + } + + /** + * 根据支付流水号查找 + * + * @param string $payFlowId 支付流水号 + * @return CcbPaymentLog|null + */ + public static function findByPayFlowId($payFlowId) + { + return self::where('pay_flow_id', $payFlowId)->find(); + } + + /** + * 根据订单号查找 + * + * @param string $orderSn 订单号 + * @return CcbPaymentLog|null + */ + public static function findByOrderSn($orderSn) + { + return self::where('order_sn', $orderSn) + ->order('create_time', 'desc') + ->find(); + } + + /** + * 创建支付日志 + * + * @param array $data 日志数据 + * @return CcbPaymentLog|false + */ + public static function createLog($data) + { + $defaultData = [ + 'status' => self::STATUS_PENDING, + 'create_time' => time(), + ]; + + $logData = array_merge($defaultData, $data); + + return self::create($logData); + } + + /** + * 更新支付状态 + * + * @param string $payFlowId 支付流水号 + * @param int $status 状态 + * @param array $extraData 额外数据 + * @return bool + */ + public static function updateStatus($payFlowId, $status, $extraData = []) + { + $updateData = array_merge([ + 'status' => $status, + ], $extraData); + + return self::where('pay_flow_id', $payFlowId)->update($updateData) !== false; + } + + /** + * 记录支付成功 + * + * @param string $payFlowId 支付流水号 + * @param string $transId 交易ID + * @param array $callbackData 回调数据 + * @return bool + */ + public static function recordSuccess($payFlowId, $transId, $callbackData = []) + { + return self::updateStatus($payFlowId, self::STATUS_SUCCESS, [ + 'trans_id' => $transId, + 'pay_time' => time(), + 'callback_data' => is_array($callbackData) ? json_encode($callbackData, JSON_UNESCAPED_UNICODE) : $callbackData, + ]); + } + + /** + * 记录支付失败 + * + * @param string $payFlowId 支付流水号 + * @param string $errorMsg 错误信息 + * @return bool + */ + public static function recordFailure($payFlowId, $errorMsg) + { + return self::updateStatus($payFlowId, self::STATUS_FAILED, [ + 'error_msg' => $errorMsg, + ]); + } + + /** + * 获取待支付的日志列表 + * + * @param int $limit 限制数量 + * @return array + */ + public static function getPendingList($limit = 100) + { + return self::where('status', self::STATUS_PENDING) + ->where('create_time', '>', time() - 3600 * 24) // 24小时内 + ->limit($limit) + ->select(); + } + + /** + * 关联订单模型(如果需要) + * + * @return \think\model\relation\BelongsTo + */ + public function order() + { + return $this->belongsTo('app\admin\model\shopro\order\Order', 'order_id', 'id'); + } + + /** + * 关联用户模型(如果需要) + * + * @return \think\model\relation\BelongsTo + */ + public function user() + { + return $this->belongsTo('app\common\model\User', 'user_id', 'id'); + } +} diff --git a/addons/shopro/library/ccblife/model/CcbSyncLog.php b/addons/shopro/library/ccblife/model/CcbSyncLog.php new file mode 100644 index 0000000..2f1c3d9 --- /dev/null +++ b/addons/shopro/library/ccblife/model/CcbSyncLog.php @@ -0,0 +1,259 @@ + 'integer', + 'sync_status' => 'integer', + 'sync_time' => 'integer', + 'cost_time' => 'integer', + 'retry_times' => 'integer', + ]; + + /** + * 同步状态常量 + */ + const STATUS_FAILED = 0; // 同步失败 + const STATUS_SUCCESS = 1; // 同步成功 + + /** + * 交易代码常量 + */ + const TX_CODE_PUSH = 'A3341TP01'; // 订单推送 + const TX_CODE_UPDATE = 'A3341TP02'; // 订单状态更新 + const TX_CODE_QUERY = 'A3341TP03'; // 订单查询 + const TX_CODE_REFUND = 'A3341TP04'; // 订单退款 + + /** + * 交易代码映射 + * @var array + */ + public static $txCodeMap = [ + self::TX_CODE_PUSH => '订单推送', + self::TX_CODE_UPDATE => '状态更新', + self::TX_CODE_QUERY => '订单查询', + self::TX_CODE_REFUND => '订单退款', + ]; + + /** + * 同步状态映射 + * @var array + */ + public static $statusMap = [ + self::STATUS_FAILED => '失败', + self::STATUS_SUCCESS => '成功', + ]; + + /** + * 获取交易代码文本 + * + * @param string $txCode 交易代码 + * @return string + */ + public static function getTxCodeText($txCode) + { + return self::$txCodeMap[$txCode] ?? '未知交易'; + } + + /** + * 获取状态文本 + * + * @param int $status 状态值 + * @return string + */ + public static function getStatusText($status) + { + return self::$statusMap[$status] ?? '未知状态'; + } + + /** + * 根据订单ID查找同步日志 + * + * @param int $orderId 订单ID + * @param string $txCode 交易代码(可选) + * @return array + */ + public static function findByOrderId($orderId, $txCode = '') + { + $query = self::where('order_id', $orderId); + + if ($txCode) { + $query->where('tx_code', $txCode); + } + + return $query->order('sync_time', 'desc')->select(); + } + + /** + * 根据交易流水号查找 + * + * @param string $txSeq 交易流水号 + * @return CcbSyncLog|null + */ + public static function findByTxSeq($txSeq) + { + return self::where('tx_seq', $txSeq)->find(); + } + + /** + * 创建同步日志 + * + * @param array $data 日志数据 + * @return CcbSyncLog|false + */ + public static function createLog($data) + { + $defaultData = [ + 'sync_status' => self::STATUS_FAILED, + 'sync_time' => time(), + 'retry_times' => 0, + 'cost_time' => 0, + ]; + + $logData = array_merge($defaultData, $data); + + return self::create($logData); + } + + /** + * 更新同步状态 + * + * @param string $txSeq 交易流水号 + * @param int $status 状态 + * @param array $extraData 额外数据 + * @return bool + */ + public static function updateStatus($txSeq, $status, $extraData = []) + { + $updateData = array_merge([ + 'sync_status' => $status, + ], $extraData); + + return self::where('tx_seq', $txSeq)->update($updateData) !== false; + } + + /** + * 记录同步成功 + * + * @param string $txSeq 交易流水号 + * @param mixed $responseData 响应数据 + * @param int $costTime 耗时(毫秒) + * @return bool + */ + public static function recordSuccess($txSeq, $responseData, $costTime = 0) + { + return self::updateStatus($txSeq, self::STATUS_SUCCESS, [ + 'response_data' => is_array($responseData) ? json_encode($responseData, JSON_UNESCAPED_UNICODE) : $responseData, + 'cost_time' => $costTime, + ]); + } + + /** + * 记录同步失败 + * + * @param string $txSeq 交易流水号 + * @param string $errorMsg 错误信息 + * @param int $costTime 耗时(毫秒) + * @return bool + */ + public static function recordFailure($txSeq, $errorMsg, $costTime = 0) + { + return self::updateStatus($txSeq, self::STATUS_FAILED, [ + 'error_msg' => $errorMsg, + 'cost_time' => $costTime, + ]); + } + + /** + * 增加重试次数 + * + * @param string $txSeq 交易流水号 + * @return bool + */ + public static function incrementRetry($txSeq) + { + return self::where('tx_seq', $txSeq)->setInc('retry_times') !== false; + } + + /** + * 获取失败的同步日志 + * + * @param int $limit 限制数量 + * @param int $maxRetryTimes 最大重试次数 + * @return array + */ + public static function getFailedList($limit = 100, $maxRetryTimes = 3) + { + return self::where('sync_status', self::STATUS_FAILED) + ->where('retry_times', '<', $maxRetryTimes) + ->where('sync_time', '>', time() - 3600 * 24) // 24小时内 + ->order('sync_time', 'asc') + ->limit($limit) + ->select(); + } + + /** + * 获取订单最后一次同步记录 + * + * @param int $orderId 订单ID + * @param string $txCode 交易代码 + * @return CcbSyncLog|null + */ + public static function getLastSync($orderId, $txCode) + { + return self::where('order_id', $orderId) + ->where('tx_code', $txCode) + ->order('sync_time', 'desc') + ->find(); + } + + /** + * 检查订单是否同步成功 + * + * @param int $orderId 订单ID + * @param string $txCode 交易代码 + * @return bool + */ + public static function isSyncSuccess($orderId, $txCode) + { + $lastSync = self::getLastSync($orderId, $txCode); + + return $lastSync && $lastSync['sync_status'] === self::STATUS_SUCCESS; + } + + /** + * 关联订单模型(如果需要) + * + * @return \think\model\relation\BelongsTo + */ + public function order() + { + return $this->belongsTo('app\admin\model\shopro\order\Order', 'order_id', 'id'); + } +}