|
|
@@ -0,0 +1,658 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+/**
|
|
|
+ * mallApp 微信小程序发货信息录入。
|
|
|
+ * 零售同城:花店自配送事件监听器调用;充值:payUtil xhRecharge 回调调用。
|
|
|
+ * 内部复用 shippingUtil,失败只记日志,不打断发货/入账主流程。
|
|
|
+ */
|
|
|
+
|
|
|
+namespace bizMall\order\classes;
|
|
|
+
|
|
|
+use biz\shop\classes\ShopClass;
|
|
|
+use biz\wx\classes\WxOpenClass as PlatformWxOpenClass;
|
|
|
+use bizHd\wx\classes\WxBaseClass;
|
|
|
+use bizHd\wx\classes\WxMiniBaseClass;
|
|
|
+use bizMall\user\classes\UserClass;
|
|
|
+use common\components\dict;
|
|
|
+use common\components\shippingUtil;
|
|
|
+use PhpAmqpLib\Wire\AMQPTable;
|
|
|
+use Yii;
|
|
|
+
|
|
|
+class WxOrderShippingClass
|
|
|
+{
|
|
|
+ const LOGISTICS_INTRA_CITY = 2;
|
|
|
+ const LOGISTICS_VIRTUAL = 3;
|
|
|
+ const ACTION_SHIPPING = 'shipping';
|
|
|
+ const SHIP_STATUS_NONE = 0;
|
|
|
+ const SHIP_STATUS_OK = 1;
|
|
|
+ const SHIP_STATUS_FAIL = 2;
|
|
|
+ const SETTLE_WAIT = 1;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 同城自配送:mall 微信支付单录入 logistics_type=2。
|
|
|
+ * 非商城/非微信/已录入成功则跳过;缺 openid / 微信单号 记失败不阻断发货。
|
|
|
+ * transaction_id 取 xhOrder.thirdOrderId(拉卡拉 acc_trade_no),不要用 thirdNo。
|
|
|
+ */
|
|
|
+ public static function uploadIntraCityByOrder($order)
|
|
|
+ {
|
|
|
+ $order = self::modelToArray($order);
|
|
|
+ $orderId = (int)($order['id'] ?? 0);
|
|
|
+ if ($orderId <= 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $mallFromType = (int)dict::getDict('fromType', 'mall');
|
|
|
+ $wxPayWay = (int)dict::getDict('payWay', 'wxPay');
|
|
|
+ // 只有 mallApp 微信支付才进该小程序订单管理
|
|
|
+ if ((int)($order['fromType'] ?? 0) !== $mallFromType) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if ((int)($order['payWay'] ?? -1) !== $wxPayWay) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $ext = OrderWxExtClass::getByOrderId($orderId);
|
|
|
+ if (!empty($ext) && (int)($ext['shipStatus'] ?? 0) === self::SHIP_STATUS_OK) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $orderSn = (string)($order['orderSn'] ?? '');
|
|
|
+ // 拉卡拉 acc_trade_no 才是微信 transaction_id,支付回调已写入 thirdOrderId
|
|
|
+ $transactionId = trim((string)($order['thirdOrderId'] ?? ''));
|
|
|
+ $openId = self::resolveMallOpenId($order);
|
|
|
+ $itemDesc = self::buildOrderItemDesc($orderSn);
|
|
|
+ $payload = self::buildShippingPayload($transactionId, $openId, $itemDesc, self::LOGISTICS_INTRA_CITY);
|
|
|
+
|
|
|
+ $merchant = self::getMallMerchantOrEmpty();
|
|
|
+ if (empty($merchant)) {
|
|
|
+ self::persistOrderShip($orderId, $orderSn, $payload, ['errcode' => -1, 'errmsg' => '没有找到商城小程序配置'], self::LOGISTICS_INTRA_CITY);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if ($openId === '') {
|
|
|
+ self::persistOrderShip($orderId, $orderSn, $payload, ['errcode' => -1, 'errmsg' => '缺少商城小程序openid'], self::LOGISTICS_INTRA_CITY);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if ($transactionId === '') {
|
|
|
+ self::persistOrderShip($orderId, $orderSn, $payload, ['errcode' => -1, 'errmsg' => '缺少微信transaction_id(thirdOrderId)'], self::LOGISTICS_INTRA_CITY);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $ptStyle = (int)dict::getDict('ptStyle', 'mall');
|
|
|
+ // token 查找里可能 util::fail 直接结束请求,临时改成抛异常以便本单发货仍成功
|
|
|
+ $oldReport = Yii::$app->params['errorReport'] ?? 0;
|
|
|
+ Yii::$app->params['errorReport'] = 1;
|
|
|
+ try {
|
|
|
+ $result = shippingUtil::shipping($merchant, $payload, $ptStyle);
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ $result = ['errcode' => -1, 'errmsg' => $e->getMessage()];
|
|
|
+ }
|
|
|
+ Yii::$app->params['errorReport'] = $oldReport;
|
|
|
+ if (!is_array($result)) {
|
|
|
+ $result = ['errcode' => -1, 'errmsg' => '微信接口返回异常'];
|
|
|
+ }
|
|
|
+ self::persistOrderShip($orderId, $orderSn, $payload, $result, self::LOGISTICS_INTRA_CITY);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * mallApp 余额充值支付成功后录入虚拟发货(logistics_type=3)。
|
|
|
+ * 仅商城回调(ptStyle=mall)+ 微信支付;商品信息=门店展示名+余额充值。
|
|
|
+ * transaction_id 用 thirdPayNo(拉卡拉 acc_trade_no),不要用 returnCode(那是 trade_no,退款仍用它)。
|
|
|
+ */
|
|
|
+ public static function uploadVirtualByRecharge($recharge, $accTradeNo = '')
|
|
|
+ {
|
|
|
+ $mallPtStyle = (int)dict::getDict('ptStyle', 'mall');
|
|
|
+ if ((int)(Yii::$app->params['ptStyle'] ?? 0) !== $mallPtStyle) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $recharge = self::modelToArray($recharge);
|
|
|
+ $rechargeId = (int)($recharge['id'] ?? 0);
|
|
|
+ if ($rechargeId <= 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $wxPayWay = (int)dict::getDict('payWay', 'wxPay');
|
|
|
+ if ((int)($recharge['payWay'] ?? -1) !== $wxPayWay) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if ((int)($recharge['payStatus'] ?? 0) !== 1) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $capitalType = (int)dict::getDict('capitalType', 'xhRecharge', 'id');
|
|
|
+ $orderSn = (string)($recharge['orderSn'] ?? '');
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (WxOrderShippingLogClass::exists([
|
|
|
+ 'capitalType' => $capitalType,
|
|
|
+ 'relateId' => $rechargeId,
|
|
|
+ 'errcode' => 0,
|
|
|
+ ])) {
|
|
|
+ // 日志已成功但扩展表可能仍失败态,补齐状态供列表/结算使用
|
|
|
+ try {
|
|
|
+ RechargeWxExtClass::markShipResult($rechargeId, $orderSn, true, '', self::LOGISTICS_VIRTUAL);
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ Yii::error('补齐充值发货成功态失败: ' . $e->getMessage(), 'wx-order-shipping');
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ Yii::error('查充值发货日志失败: ' . $e->getMessage(), 'wx-order-shipping');
|
|
|
+ }
|
|
|
+
|
|
|
+ $transactionId = trim((string)$accTradeNo);
|
|
|
+ if ($transactionId === '') {
|
|
|
+ $transactionId = trim((string)($recharge['thirdPayNo'] ?? ''));
|
|
|
+ }
|
|
|
+ $openId = self::resolveMallOpenId($recharge);
|
|
|
+ $itemDesc = self::buildRechargeItemDesc($recharge);
|
|
|
+ $payload = self::buildShippingPayload($transactionId, $openId, $itemDesc, self::LOGISTICS_VIRTUAL);
|
|
|
+
|
|
|
+ if ((int)($recharge['userId'] ?? 0) <= 0) {
|
|
|
+ self::persistShippingLog($capitalType, $rechargeId, $orderSn, $payload, ['errcode' => -1, 'errmsg' => '缺少商城用户userId'], self::LOGISTICS_VIRTUAL);
|
|
|
+ RechargeWxExtClass::markShipResult($rechargeId, $orderSn, false, '缺少商城用户userId', self::LOGISTICS_VIRTUAL);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if ($openId === '') {
|
|
|
+ self::persistShippingLog($capitalType, $rechargeId, $orderSn, $payload, ['errcode' => -1, 'errmsg' => '缺少商城小程序openid'], self::LOGISTICS_VIRTUAL);
|
|
|
+ RechargeWxExtClass::markShipResult($rechargeId, $orderSn, false, '缺少商城小程序openid', self::LOGISTICS_VIRTUAL);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if ($transactionId === '') {
|
|
|
+ self::persistShippingLog($capitalType, $rechargeId, $orderSn, $payload, ['errcode' => -1, 'errmsg' => '缺少微信transaction_id(acc_trade_no)'], self::LOGISTICS_VIRTUAL);
|
|
|
+ RechargeWxExtClass::markShipResult($rechargeId, $orderSn, false, '缺少微信transaction_id(acc_trade_no)', self::LOGISTICS_VIRTUAL);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $merchant = self::getMallMerchantOrEmpty();
|
|
|
+ if (empty($merchant)) {
|
|
|
+ self::persistShippingLog($capitalType, $rechargeId, $orderSn, $payload, ['errcode' => -1, 'errmsg' => '没有找到商城小程序配置'], self::LOGISTICS_VIRTUAL);
|
|
|
+ RechargeWxExtClass::markShipResult($rechargeId, $orderSn, false, '没有找到商城小程序配置', self::LOGISTICS_VIRTUAL);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $oldReport = Yii::$app->params['errorReport'] ?? 0;
|
|
|
+ Yii::$app->params['errorReport'] = 1;
|
|
|
+ try {
|
|
|
+ $result = shippingUtil::shipping($merchant, $payload, $mallPtStyle);
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ $result = ['errcode' => -1, 'errmsg' => $e->getMessage()];
|
|
|
+ }
|
|
|
+ Yii::$app->params['errorReport'] = $oldReport;
|
|
|
+ if (!is_array($result)) {
|
|
|
+ $result = ['errcode' => -1, 'errmsg' => '微信接口返回异常'];
|
|
|
+ }
|
|
|
+ self::persistShippingLog($capitalType, $rechargeId, $orderSn, $payload, $result, self::LOGISTICS_VIRTUAL);
|
|
|
+ // 扩展表记发货结果;失败不回滚充值,由 RabbitMQ 延迟补录
|
|
|
+ $errcode = isset($result['errcode']) ? (int)$result['errcode'] : -1;
|
|
|
+ $errmsg = (string)($result['errmsg'] ?? '');
|
|
|
+ try {
|
|
|
+ RechargeWxExtClass::markShipResult(
|
|
|
+ $rechargeId,
|
|
|
+ $orderSn,
|
|
|
+ $errcode === 0,
|
|
|
+ $errmsg,
|
|
|
+ self::LOGISTICS_VIRTUAL
|
|
|
+ );
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ Yii::error('回写充值发货扩展失败: ' . $e->getMessage(), 'wx-order-shipping');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 可重试失败:投递 RabbitMQ 延迟消息(方案 B),等微信支付单入库后再补录
|
|
|
+ if ($errcode !== 0 && !RechargeWxExtClass::isPermanentShipError($errmsg)) {
|
|
|
+ $ext = RechargeWxExtClass::getByRechargeId($rechargeId);
|
|
|
+ $retryCount = (int)($ext['shipRetryCount'] ?? 0);
|
|
|
+ if ($retryCount < RechargeWxExtClass::MAX_SHIP_RETRY && !empty($ext['nextRetryTime'])) {
|
|
|
+ $delaySec = max(RechargeWxExtClass::RETRY_DELAY_SECONDS, strtotime($ext['nextRetryTime']) - time());
|
|
|
+ self::scheduleRechargeShipDelay($rechargeId, $delaySec);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * mallApp 扫码付款支付成功后录入虚拟发货(logistics_type=3)。
|
|
|
+ * transaction_id 用 thirdPayNo(拉卡拉 acc_trade_no),不要用 returnCode。
|
|
|
+ */
|
|
|
+ public static function uploadVirtualByScanPay($scanPay, $accTradeNo = '')
|
|
|
+ {
|
|
|
+ $mallPtStyle = (int)dict::getDict('ptStyle', 'mall');
|
|
|
+ if ((int)(Yii::$app->params['ptStyle'] ?? 0) !== $mallPtStyle) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $scanPay = self::modelToArray($scanPay);
|
|
|
+ $scanPayId = (int)($scanPay['id'] ?? 0);
|
|
|
+ if ($scanPayId <= 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $wxPayWay = (int)dict::getDict('payWay', 'wxPay');
|
|
|
+ if ((int)($scanPay['payWay'] ?? -1) !== $wxPayWay) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if ((int)($scanPay['payStatus'] ?? 0) !== 1) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $capitalType = (int)dict::getDict('capitalType', 'scanPay', 'id');
|
|
|
+ $orderSn = (string)($scanPay['orderSn'] ?? '');
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (WxOrderShippingLogClass::exists([
|
|
|
+ 'capitalType' => $capitalType,
|
|
|
+ 'relateId' => $scanPayId,
|
|
|
+ 'errcode' => 0,
|
|
|
+ ])) {
|
|
|
+ try {
|
|
|
+ ScanPayWxExtClass::markShipResult($scanPayId, $orderSn, true, '', self::LOGISTICS_VIRTUAL);
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ Yii::error('补齐扫码发货成功态失败: ' . $e->getMessage(), 'wx-order-shipping');
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ Yii::error('查扫码发货日志失败: ' . $e->getMessage(), 'wx-order-shipping');
|
|
|
+ }
|
|
|
+
|
|
|
+ $transactionId = trim((string)$accTradeNo);
|
|
|
+ if ($transactionId === '') {
|
|
|
+ $transactionId = trim((string)($scanPay['thirdPayNo'] ?? ''));
|
|
|
+ }
|
|
|
+ $openId = self::resolveMallOpenId($scanPay);
|
|
|
+ $itemDesc = self::buildScanPayItemDesc($scanPay);
|
|
|
+ $payload = self::buildShippingPayload($transactionId, $openId, $itemDesc, self::LOGISTICS_VIRTUAL);
|
|
|
+
|
|
|
+ if ((int)($scanPay['userId'] ?? 0) <= 0) {
|
|
|
+ self::persistShippingLog($capitalType, $scanPayId, $orderSn, $payload, ['errcode' => -1, 'errmsg' => '缺少商城用户userId'], self::LOGISTICS_VIRTUAL);
|
|
|
+ ScanPayWxExtClass::markShipResult($scanPayId, $orderSn, false, '缺少商城用户userId', self::LOGISTICS_VIRTUAL);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if ($openId === '') {
|
|
|
+ self::persistShippingLog($capitalType, $scanPayId, $orderSn, $payload, ['errcode' => -1, 'errmsg' => '缺少商城小程序openid'], self::LOGISTICS_VIRTUAL);
|
|
|
+ ScanPayWxExtClass::markShipResult($scanPayId, $orderSn, false, '缺少商城小程序openid', self::LOGISTICS_VIRTUAL);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if ($transactionId === '') {
|
|
|
+ self::persistShippingLog($capitalType, $scanPayId, $orderSn, $payload, ['errcode' => -1, 'errmsg' => '缺少微信transaction_id(acc_trade_no)'], self::LOGISTICS_VIRTUAL);
|
|
|
+ ScanPayWxExtClass::markShipResult($scanPayId, $orderSn, false, '缺少微信transaction_id(acc_trade_no)', self::LOGISTICS_VIRTUAL);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $merchant = self::getMallMerchantOrEmpty();
|
|
|
+ if (empty($merchant)) {
|
|
|
+ self::persistShippingLog($capitalType, $scanPayId, $orderSn, $payload, ['errcode' => -1, 'errmsg' => '没有找到商城小程序配置'], self::LOGISTICS_VIRTUAL);
|
|
|
+ ScanPayWxExtClass::markShipResult($scanPayId, $orderSn, false, '没有找到商城小程序配置', self::LOGISTICS_VIRTUAL);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $oldReport = Yii::$app->params['errorReport'] ?? 0;
|
|
|
+ Yii::$app->params['errorReport'] = 1;
|
|
|
+ try {
|
|
|
+ $result = shippingUtil::shipping($merchant, $payload, $mallPtStyle);
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ $result = ['errcode' => -1, 'errmsg' => $e->getMessage()];
|
|
|
+ }
|
|
|
+ Yii::$app->params['errorReport'] = $oldReport;
|
|
|
+ if (!is_array($result)) {
|
|
|
+ $result = ['errcode' => -1, 'errmsg' => '微信接口返回异常'];
|
|
|
+ }
|
|
|
+ self::persistShippingLog($capitalType, $scanPayId, $orderSn, $payload, $result, self::LOGISTICS_VIRTUAL);
|
|
|
+ $errcode = isset($result['errcode']) ? (int)$result['errcode'] : -1;
|
|
|
+ $errmsg = (string)($result['errmsg'] ?? '');
|
|
|
+ try {
|
|
|
+ ScanPayWxExtClass::markShipResult(
|
|
|
+ $scanPayId,
|
|
|
+ $orderSn,
|
|
|
+ $errcode === 0,
|
|
|
+ $errmsg,
|
|
|
+ self::LOGISTICS_VIRTUAL
|
|
|
+ );
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ Yii::error('回写扫码发货扩展失败: ' . $e->getMessage(), 'wx-order-shipping');
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($errcode !== 0 && !ScanPayWxExtClass::isPermanentShipError($errmsg)) {
|
|
|
+ $ext = ScanPayWxExtClass::getByScanPayId($scanPayId);
|
|
|
+ $retryCount = (int)($ext['shipRetryCount'] ?? 0);
|
|
|
+ if ($retryCount < ScanPayWxExtClass::MAX_SHIP_RETRY && !empty($ext['nextRetryTime'])) {
|
|
|
+ $delaySec = max(ScanPayWxExtClass::RETRY_DELAY_SECONDS, strtotime($ext['nextRetryTime']) - time());
|
|
|
+ self::scheduleScanPayShipDelay($scanPayId, $delaySec);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 投递扫码付款发货延迟补录(复用 rechargeShip 队列路由)。
|
|
|
+ */
|
|
|
+ public static function scheduleScanPayShipDelay($scanPayId, $delaySeconds = 60)
|
|
|
+ {
|
|
|
+ $scanPayId = (int)$scanPayId;
|
|
|
+ if ($scanPayId <= 0) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ $delayMs = max(1000, (int)$delaySeconds * 1000);
|
|
|
+ $message = serialize([
|
|
|
+ 'type' => 'scan_pay_ship_retry',
|
|
|
+ 'scanPayId' => $scanPayId,
|
|
|
+ ]);
|
|
|
+ $producer = Yii::$app->rabbitmq->getProducer('notifyProducer');
|
|
|
+ $producer->publish($message, 'limitBuyDelayExchange', 'rechargeShipDelayRoute', [
|
|
|
+ 'delivery_mode' => 2,
|
|
|
+ 'content_type' => 'application/octet-stream',
|
|
|
+ 'application_headers' => new AMQPTable([
|
|
|
+ 'x-delay' => intval($delayMs),
|
|
|
+ ]),
|
|
|
+ ]);
|
|
|
+ Yii::info('scan pay ship delay: scanPayId=' . $scanPayId . ' delayMs=' . $delayMs, 'wx-order-shipping');
|
|
|
+ return true;
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ Yii::error('scheduleScanPayShipDelay fail: ' . $e->getMessage(), 'wx-order-shipping');
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 延迟消息消费:补录扫码付款虚拟发货。
|
|
|
+ */
|
|
|
+ public static function handleScanPayShipDelayMessage($scanPayId)
|
|
|
+ {
|
|
|
+ $scanPayId = (int)$scanPayId;
|
|
|
+ if ($scanPayId <= 0) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ $mallPtStyle = (int)dict::getDict('ptStyle', 'mall');
|
|
|
+ Yii::$app->params['ptStyle'] = $mallPtStyle;
|
|
|
+
|
|
|
+ $scanPay = \bizHd\order\classes\ScanPayClass::getById($scanPayId, true);
|
|
|
+ if (empty($scanPay)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if ((int)($scanPay->payStatus ?? 0) !== 1) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ // 已全额退款则不再补录
|
|
|
+ $act = bcadd((string)($scanPay->actPrice ?? '0'), '0', 2);
|
|
|
+ $tk = bcadd((string)($scanPay->tkPrice ?? '0'), '0', 2);
|
|
|
+ if (bccomp($act, '0', 2) > 0 && bccomp($tk, $act, 2) >= 0) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ $ext = ScanPayWxExtClass::getByScanPayId($scanPayId);
|
|
|
+ if (!empty($ext) && (int)($ext['shipStatus'] ?? 0) === ScanPayWxExtClass::SHIP_OK) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ self::uploadVirtualByScanPay($scanPay, (string)($scanPay->thirdPayNo ?? ''));
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 投递充值发货延迟补录消息(复用 limitBuyDelayExchange)。
|
|
|
+ * 失败只打日志,不阻断支付回调。
|
|
|
+ */
|
|
|
+ public static function scheduleRechargeShipDelay($rechargeId, $delaySeconds = 60)
|
|
|
+ {
|
|
|
+ $rechargeId = (int)$rechargeId;
|
|
|
+ if ($rechargeId <= 0) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ $delayMs = max(1000, (int)$delaySeconds * 1000);
|
|
|
+ $message = serialize([
|
|
|
+ 'type' => 'recharge_ship_retry',
|
|
|
+ 'rechargeId' => $rechargeId,
|
|
|
+ ]);
|
|
|
+ $producer = Yii::$app->rabbitmq->getProducer('notifyProducer');
|
|
|
+ $producer->publish($message, 'limitBuyDelayExchange', 'rechargeShipDelayRoute', [
|
|
|
+ 'delivery_mode' => 2,
|
|
|
+ 'content_type' => 'application/octet-stream',
|
|
|
+ 'application_headers' => new AMQPTable([
|
|
|
+ 'x-delay' => intval($delayMs),
|
|
|
+ ]),
|
|
|
+ ]);
|
|
|
+ Yii::info('recharge ship delay: rechargeId=' . $rechargeId . ' delayMs=' . $delayMs, 'wx-order-shipping');
|
|
|
+ return true;
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ Yii::error('scheduleRechargeShipDelay fail: ' . $e->getMessage(), 'wx-order-shipping');
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 延迟消息消费:补录充值虚拟发货;仍失败且可重试时由 uploadVirtualByRecharge 再投递下一次。
|
|
|
+ * @return bool 消费是否视为处理完成(始终 true,避免死循环 requeue)
|
|
|
+ */
|
|
|
+ public static function handleRechargeShipDelayMessage($rechargeId)
|
|
|
+ {
|
|
|
+ $rechargeId = (int)$rechargeId;
|
|
|
+ if ($rechargeId <= 0) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ $mallPtStyle = (int)dict::getDict('ptStyle', 'mall');
|
|
|
+ Yii::$app->params['ptStyle'] = $mallPtStyle;
|
|
|
+
|
|
|
+ $recharge = \bizHd\recharge\classes\RechargeClass::getById($rechargeId, true);
|
|
|
+ if (empty($recharge)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if ((int)($recharge->payStatus ?? 0) !== 1) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if ((int)($recharge->isRefund ?? 0) === 1) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ $ext = RechargeWxExtClass::getByRechargeId($rechargeId);
|
|
|
+ if (!empty($ext) && (int)($ext['shipStatus'] ?? 0) === RechargeWxExtClass::SHIP_OK) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ self::uploadVirtualByRecharge($recharge, (string)($recharge->thirdPayNo ?? ''));
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 组装微信发货录入报文。
|
|
|
+ * order_number_type=2:微信 transaction_id(拉卡拉 acc_trade_no)。
|
|
|
+ */
|
|
|
+ private static function buildShippingPayload($transactionId, $openId, $itemDesc, $logisticsType)
|
|
|
+ {
|
|
|
+ $payload = [
|
|
|
+ 'order_key' => [
|
|
|
+ 'order_number_type' => 2,
|
|
|
+ 'transaction_id' => (string)$transactionId,
|
|
|
+ ],
|
|
|
+ 'delivery_mode' => 1,
|
|
|
+ 'logistics_type' => (int)$logisticsType,
|
|
|
+ 'shipping_list' => [
|
|
|
+ ['item_desc' => $itemDesc],
|
|
|
+ ],
|
|
|
+ 'upload_time' => date(DATE_ATOM),
|
|
|
+ ];
|
|
|
+ if ($openId !== '') {
|
|
|
+ $payload['payer'] = ['openid' => $openId];
|
|
|
+ }
|
|
|
+ return $payload;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 零售发货:写调用日志 + 更新 xhOrderWxExt 当前发货状态。
|
|
|
+ * 日志字段与现网表一致(capitalType/action/errMsg)。
|
|
|
+ */
|
|
|
+ private static function persistOrderShip($orderId, $orderSn, $payload, $result, $logisticsType)
|
|
|
+ {
|
|
|
+ $capitalType = (int)dict::getDict('capitalType', 'xhOrder', 'id');
|
|
|
+ self::persistShippingLog($capitalType, $orderId, $orderSn, $payload, $result, $logisticsType);
|
|
|
+
|
|
|
+ $errcode = isset($result['errcode']) ? (int)$result['errcode'] : -1;
|
|
|
+ $errmsg = (string)($result['errmsg'] ?? '');
|
|
|
+ $ok = $errcode === 0;
|
|
|
+ $now = date('Y-m-d H:i:s');
|
|
|
+ $extData = [
|
|
|
+ 'orderId' => (int)$orderId,
|
|
|
+ 'orderSn' => (string)$orderSn,
|
|
|
+ 'shipStatus' => $ok ? self::SHIP_STATUS_OK : self::SHIP_STATUS_FAIL,
|
|
|
+ 'logisticsType' => (int)$logisticsType,
|
|
|
+ 'errMsg' => mb_substr($errmsg, 0, 500),
|
|
|
+ ];
|
|
|
+ if ($ok) {
|
|
|
+ $extData['settleStatus'] = self::SETTLE_WAIT;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ $ext = OrderWxExtClass::getByOrderId($orderId);
|
|
|
+ if (empty($ext)) {
|
|
|
+ $extData['addTime'] = $now;
|
|
|
+ OrderWxExtClass::add($extData);
|
|
|
+ } else {
|
|
|
+ OrderWxExtClass::updateById($ext['id'], $extData);
|
|
|
+ }
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ Yii::error('写xhOrderWxExt失败: ' . $e->getMessage(), 'wx-order-shipping');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 现网 xhWxOrderShippingLog 字段是 capitalType/action/logisticsType/errMsg。
|
|
|
+ */
|
|
|
+ private static function persistShippingLog($capitalType, $relateId, $orderSn, $payload, $result, $logisticsType)
|
|
|
+ {
|
|
|
+ $errcode = isset($result['errcode']) ? (int)$result['errcode'] : -1;
|
|
|
+ $errmsg = (string)($result['errmsg'] ?? '');
|
|
|
+ try {
|
|
|
+ WxOrderShippingLogClass::add([
|
|
|
+ 'capitalType' => (int)$capitalType,
|
|
|
+ 'relateId' => (int)$relateId,
|
|
|
+ 'orderSn' => (string)$orderSn,
|
|
|
+ 'action' => self::ACTION_SHIPPING,
|
|
|
+ 'logisticsType' => (int)$logisticsType,
|
|
|
+ 'errcode' => $errcode,
|
|
|
+ 'errMsg' => mb_substr($errmsg, 0, 500),
|
|
|
+ 'request' => json_encode($payload, JSON_UNESCAPED_UNICODE),
|
|
|
+ 'response' => json_encode($result, JSON_UNESCAPED_UNICODE),
|
|
|
+ 'addTime' => date('Y-m-d H:i:s'),
|
|
|
+ ]);
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ Yii::error('写xhWxOrderShippingLog失败: ' . $e->getMessage(), 'wx-order-shipping');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 商城用户 miniOpenId,对应支付时的 payer.openid
|
|
|
+ private static function resolveMallOpenId($row)
|
|
|
+ {
|
|
|
+ $userId = (int)($row['userId'] ?? 0);
|
|
|
+ if ($userId <= 0) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ $user = UserClass::getById($userId);
|
|
|
+ if (empty($user)) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ return trim((string)($user['miniOpenId'] ?? ''));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 零售商品短描述;xhOrderItem/xhOrderGoods 按 orderSn 关联
|
|
|
+ private static function buildOrderItemDesc($orderSn)
|
|
|
+ {
|
|
|
+ if ($orderSn === '') {
|
|
|
+ return '鲜花';
|
|
|
+ }
|
|
|
+ $names = [];
|
|
|
+ try {
|
|
|
+ $goodsList = OrderGoodsClass::getAllByCondition(['orderSn' => $orderSn]);
|
|
|
+ $itemList = OrderItemClass::getAllByCondition(['orderSn' => $orderSn]);
|
|
|
+ $rows = array_merge(empty($goodsList) ? [] : $goodsList, empty($itemList) ? [] : $itemList);
|
|
|
+ foreach ($rows as $item) {
|
|
|
+ $name = trim((string)($item['name'] ?? $item['itemName'] ?? $item['title'] ?? ''));
|
|
|
+ if ($name !== '') {
|
|
|
+ $names[] = $name;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ Yii::error('组装微信发货item_desc失败: ' . $e->getMessage(), 'wx-order-shipping');
|
|
|
+ }
|
|
|
+ $desc = empty($names) ? '鲜花' : implode('、', array_unique($names));
|
|
|
+ return mb_substr($desc, 0, 120);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 商品信息:门店展示名称 + 余额充值,对齐 mallApp 充值页门店
|
|
|
+ private static function buildRechargeItemDesc($recharge)
|
|
|
+ {
|
|
|
+ $shopName = '';
|
|
|
+ $shopId = (int)($recharge['shopId'] ?? 0);
|
|
|
+ if ($shopId > 0) {
|
|
|
+ try {
|
|
|
+ $shop = ShopClass::getById($shopId, true);
|
|
|
+ if (!empty($shop)) {
|
|
|
+ $shopName = ShopClass::formatDisplayShopName($shop);
|
|
|
+ }
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ Yii::error('组装充值发货门店名失败: ' . $e->getMessage(), 'wx-order-shipping');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if ($shopName === '') {
|
|
|
+ $shopName = trim((string)($recharge['hdName'] ?? ''));
|
|
|
+ }
|
|
|
+ if ($shopName === '') {
|
|
|
+ $shopName = '门店';
|
|
|
+ }
|
|
|
+ return mb_substr($shopName . '余额充值', 0, 120);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 商品信息:门店展示名称 + 扫码付款
|
|
|
+ private static function buildScanPayItemDesc($scanPay)
|
|
|
+ {
|
|
|
+ $shopName = '';
|
|
|
+ $shopId = (int)($scanPay['shopId'] ?? 0);
|
|
|
+ if ($shopId > 0) {
|
|
|
+ try {
|
|
|
+ $shop = ShopClass::getById($shopId, true);
|
|
|
+ if (!empty($shop)) {
|
|
|
+ $shopName = ShopClass::formatDisplayShopName($shop);
|
|
|
+ }
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ Yii::error('组装扫码发货门店名失败: ' . $e->getMessage(), 'wx-order-shipping');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if ($shopName === '') {
|
|
|
+ $shopName = trim((string)($scanPay['hdName'] ?? ''));
|
|
|
+ }
|
|
|
+ if ($shopName === '') {
|
|
|
+ $shopName = '门店';
|
|
|
+ }
|
|
|
+ return mb_substr($shopName . '扫码付款', 0, 120);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 自己查商城小程序配置,避免 getMallWxInfo 里 util::fail 把发货接口直接结束掉
|
|
|
+ private static function getMallMerchantOrEmpty()
|
|
|
+ {
|
|
|
+ $open = PlatformWxOpenClass::getMallOpen();
|
|
|
+ if (empty($open) || empty($open['wxMiniBaseId'])) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ $mini = WxMiniBaseClass::getById($open['wxMiniBaseId']);
|
|
|
+ if (empty($mini)) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ $wx = [];
|
|
|
+ if (!empty($open['wxBaseId'])) {
|
|
|
+ $wxBase = WxBaseClass::getById($open['wxBaseId']);
|
|
|
+ $wx = empty($wxBase) ? [] : $wxBase;
|
|
|
+ }
|
|
|
+ return array_merge($wx, $mini);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static function modelToArray($model)
|
|
|
+ {
|
|
|
+ if (is_array($model)) {
|
|
|
+ return $model;
|
|
|
+ }
|
|
|
+ if (is_object($model) && method_exists($model, 'getAttributes')) {
|
|
|
+ return $model->getAttributes();
|
|
|
+ }
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+}
|