Jelajahi Sumber

充值、扫码付款订单确认收货回调

ouyang 1 hari lalu
induk
melakukan
158e1f8d84

+ 40 - 6
biz-hd/order/classes/ScanPayClass.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 
 namespace bizHd\order\classes;
 
@@ -108,13 +108,36 @@ class ScanPayClass extends BaseClass
         $order->status = 2;
         $order->payTime = $date;
         $order->payWay = $payWay;
-        // returnCode 仍存拉卡拉 trade_no(退款用);thirdPayNo 存微信 transaction_id(发货用)
+        // returnCode ← 拉卡拉 trade_no(= 结算事件 merchant_trade_no,退款用)
+        // thirdPayNo ← 拉卡拉 acc_trade_no(= 结算事件 transaction_id,微信发货用)
         $order->returnCode = mb_substr(trim((string)$transactionId), 0, 50);
         $accTradeNo = trim((string)$accTradeNo);
+        $saveAttrs = ['payStatus', 'status', 'payTime', 'payWay', 'returnCode'];
         if ($accTradeNo !== '') {
-            $order->thirdPayNo = mb_substr($accTradeNo, 0, 50);
+            // 表缺列时 Yii 不会落库,需显式检查并告警
+            if (!$order->hasAttribute('thirdPayNo')) {
+                try {
+                    Yii::$app->db->getSchema()->refreshTableSchema($order::tableName());
+                    $order->refresh();
+                } catch (\Throwable $e) {
+                    // ignore
+                }
+            }
+            if ($order->hasAttribute('thirdPayNo')) {
+                $order->thirdPayNo = mb_substr($accTradeNo, 0, 50);
+                $saveAttrs[] = 'thirdPayNo';
+            } else {
+                Yii::error(
+                    'xhScanPay 缺少 thirdPayNo 列,无法写入 acc_trade_no=' . $accTradeNo . ' orderSn=' . $orderSn,
+                    'wx-order-shipping'
+                );
+                noticeUtil::push(
+                    'xhScanPay缺少thirdPayNo列,请执行ALTER。orderSn=' . $orderSn . ' acc_trade_no=' . $accTradeNo,
+                    '15280215347'
+                );
+            }
         }
-        $order->save(false);
+        $order->save(false, $saveAttrs);
         self::payAfter($order);
         self::tryWxVirtualShipping($order, $accTradeNo);
     }
@@ -142,9 +165,20 @@ class ScanPayClass extends BaseClass
             return;
         }
         $accTradeNo = trim((string)$accTradeNo);
+        // 去重回调补写 thirdPayNo(与 thirdPay 字段约定一致)
         if ($accTradeNo !== '' && trim((string)($order->thirdPayNo ?? '')) === '') {
-            $order->thirdPayNo = mb_substr($accTradeNo, 0, 50);
-            $order->save(false);
+            if (!$order->hasAttribute('thirdPayNo')) {
+                try {
+                    Yii::$app->db->getSchema()->refreshTableSchema($order::tableName());
+                    $order->refresh();
+                } catch (\Throwable $e) {
+                    // ignore
+                }
+            }
+            if ($order->hasAttribute('thirdPayNo')) {
+                $order->thirdPayNo = mb_substr($accTradeNo, 0, 50);
+                $order->save(false, ['thirdPayNo']);
+            }
         }
         self::tryWxVirtualShipping($order, $accTradeNo);
     }

+ 84 - 7
biz-hd/recharge/classes/RechargeClass.php

@@ -13,6 +13,9 @@ use bizHd\hb\classes\HbClass;
 use bizHd\hb\classes\HbManageClass;
 use bizHd\order\classes\OrderClass;
 use bizHd\order\classes\SettleClass;
+use bizMall\order\classes\WxFreezeBalanceClass;
+use bizMall\order\classes\WxOrderShippingClass;
+use bizMall\order\classes\RechargeWxExtClass;
 use bizHd\shop\classes\ShopExtClass;
 use bizHd\shop\classes\MainClass;
 use bizHd\user\classes\UserGrowthClass;
@@ -170,8 +173,12 @@ class RechargeClass extends BaseClass
         return self::add($data, true);
     }
 
-    //第三支付后流程 ssh 2021.4.27
-    public static function thirdPay($payWay, $orderSn, $totalFee, $attach, $transactionId = '')
+    /**
+     * 拉卡拉/微信充值回调入账。
+     * $transactionId 写入 returnCode(拉卡拉 trade_no,退款用);
+     * $accTradeNo 写入 thirdPayNo(账户端单号/微信 transaction_id,虚拟发货用)。
+     */
+    public static function thirdPay($payWay, $orderSn, $totalFee, $attach, $transactionId = '', $accTradeNo = '')
     {
         $recharge = RechargeClass::getByCondition(['orderSn' => $orderSn], true);
         if (empty($recharge)) {
@@ -184,6 +191,9 @@ class RechargeClass extends BaseClass
         }
         if ($recharge->payStatus == 1) {
             noticeUtil::push("充值付款成功,收到回调,但订单是已经支付过了,orderSn:" . $orderSn, '15280215347');
+            // 已入账仍补 thirdPayNo 并尝试虚拟发货:首次回调发货失败时后续通知可补录
+            self::saveThirdPayNoIfEmpty($recharge, $accTradeNo);
+            self::tryWxVirtualShipping($recharge, $accTradeNo);
             return false;
         }
         $id = $recharge->id;
@@ -192,12 +202,60 @@ class RechargeClass extends BaseClass
             'onlinePay' => 2,
             'side' => 1,
         ];
-        $recharge->returnCode = $transactionId;
+        $recharge->returnCode = mb_substr(trim((string)$transactionId), 0, 50);
+        $accTradeNo = trim((string)$accTradeNo);
+        if ($accTradeNo !== '') {
+            $recharge->thirdPayNo = mb_substr($accTradeNo, 0, 50);
+        }
         $recharge->save(false);
         self::complete($recharge, $payWay, $params);
+        self::tryWxVirtualShipping($recharge, $accTradeNo);
         return $recharge;
     }
 
+    /**
+     * 已入账记录补写 acc_trade_no。thirdPayNo 已有值则不覆盖,避免把退款用的 returnCode 搞混。
+     */
+    protected static function saveThirdPayNoIfEmpty($recharge, $accTradeNo)
+    {
+        $accTradeNo = trim((string)$accTradeNo);
+        if ($accTradeNo === '' || empty($recharge)) {
+            return;
+        }
+        $current = trim((string)($recharge->thirdPayNo ?? ''));
+        if ($current !== '') {
+            return;
+        }
+        $recharge->thirdPayNo = mb_substr($accTradeNo, 0, 50);
+        $recharge->save(false);
+    }
+
+    /**
+     * mallApp 微信支付充值成功后向微信录入虚拟发货。
+     * 失败只记日志,不回滚开户入账。
+     */
+    public static function tryWxVirtualShipping($recharge, $accTradeNo = '')
+    {
+        try {
+            WxOrderShippingClass::uploadVirtualByRecharge($recharge, $accTradeNo);
+        } catch (\Throwable $e) {
+            Yii::error('充值微信虚拟发货异常: ' . $e->getMessage(), 'wx-order-shipping');
+        }
+    }
+
+    /**
+     * 支付回调 10 秒去重命中时补录发货:此时入账可能已完成,只再走微信虚拟发货。
+     */
+    public static function tryWxVirtualShippingByOrderSn($orderSn, $accTradeNo = '')
+    {
+        $recharge = self::getByCondition(['orderSn' => $orderSn], true);
+        if (empty($recharge)) {
+            return;
+        }
+        self::saveThirdPayNoIfEmpty($recharge, $accTradeNo);
+        self::tryWxVirtualShipping($recharge, $accTradeNo);
+    }
+
     //处理充值流程 ssh 20250321
     public static function complete($recharge, $payWay, $params)
     {
@@ -550,10 +608,14 @@ class RechargeClass extends BaseClass
         }
 
         if ($recharge->onlinePay == 2) {
-            //如果线上充值,门店余额增加
+            // 线上充值:mallApp 微信先进冻结池,支付宝等仍立即增加可提现余额
             $shop = ShopClass::getLockById($shopId);
             $main = MainClass::getLockById($mainId);
-            ShopClass::skRechargeAddBalance($main, $shop, $recharge);
+            if (WxFreezeBalanceClass::shouldFreezeMallWxRecharge($recharge)) {
+                WxFreezeBalanceClass::freezeOnRechargePay($main, $shop, $recharge);
+            } else {
+                ShopClass::skRechargeAddBalance($main, $shop, $recharge);
+            }
         } else {
             if ($recharge->payWay == 4) {
                 //现在现金充值要增加现金
@@ -650,6 +712,8 @@ class RechargeClass extends BaseClass
         $aliPay = intval(dict::getDict('payWay', 'alipay'));
         $cashPay = intval(dict::getDict('payWay', 'cash'));
         $onlineMain = null;
+        // 未结算入账的 mall 微信充值:退款只解冻,不要求 balance 够
+        $refundFromFreeze = false;
 
         if ($isOnline) {
             if ($payWay !== $wxPay && $payWay !== $aliPay) {
@@ -663,7 +727,14 @@ class RechargeClass extends BaseClass
             if (empty($onlineMain)) {
                 util::fail('没有找到资产信息');
             }
-            if (bccomp((string)($onlineMain->balance ?? '0'), $amount, 2) < 0) {
+            if ($payWay === $wxPay && WxFreezeBalanceClass::shouldFreezeMallWxRecharge($recharge)) {
+                $ext = \bizMall\order\classes\RechargeWxExtClass::getByRechargeId($rechargeId);
+                if (!empty($ext) && (int)($ext['balancePosted'] ?? 0) !== 1
+                    && bccomp((string)($ext['freezeAmount'] ?? '0'), '0', 2) > 0) {
+                    $refundFromFreeze = true;
+                }
+            }
+            if (!$refundFromFreeze && bccomp((string)($onlineMain->balance ?? '0'), $amount, 2) < 0) {
                 util::fail('门店余额不足,无法退款');
             }
             self::refundOnlineByLakala($recharge, $shop, $amount, $returnCode);
@@ -743,12 +814,18 @@ class RechargeClass extends BaseClass
             if (empty($main)) {
                 util::fail('没有找到资产信息');
             }
-            ShopClass::customRechargeRefundReduceBalance($main, $shop, $recharge, dict::getDict('capitalType', 'xhRecharge', 'id'), $staffName);
+            // 未入账:只解冻写冻结明细;已入账或非冻结单:仍扣可提现余额
+            $needReduce = WxFreezeBalanceClass::unfreezeOnRechargeRefund($main, $shop, $recharge);
+            if (bccomp($needReduce, '0', 2) > 0) {
+                ShopClass::customRechargeRefundReduceBalance($main, $shop, $recharge, dict::getDict('capitalType', 'xhRecharge', 'id'), $staffName);
+            }
         } elseif ($payWay === $cashPay) {
             self::cashRechargeRefundReduceMoney($recharge, $shop, $staffName, $amount);
         }
 
         self::markRechargeRefunded($recharge);
+        // 未录入成功时取消发货补录;队列里已有消息消费时也会因 isRefund 跳过
+        RechargeWxExtClass::cancelShipRetry((int)$recharge->id, '已退款,跳过发货补录');
         $recharge->balance = $hd->balance;
         $recharge->save(false);
 

+ 26 - 2
biz-mall/order/classes/RechargeWxExtClass.php

@@ -198,6 +198,7 @@ class RechargeWxExtClass extends BaseClass
 
     /**
      * 未入账退款解冻:本单 freezeAmount 清零并标已解冻,不改 balancePosted。
+     * 同时取消发货补录(nextRetryTime 置空),队列消费也会因 isRefund 跳过。
      */
     public static function markUnfreezeOnRefund($rechargeId, $unfreezeAmount)
     {
@@ -215,12 +216,35 @@ class RechargeWxExtClass extends BaseClass
             $remain = '0.00';
         }
         $freezeStatus = bccomp($remain, '0', 2) <= 0 ? self::FREEZE_DONE : self::FREEZE_ING;
-        self::updateById($ext['id'], [
+        $data = [
             'freezeAmount' => $remain,
             'freezeStatus' => $freezeStatus,
             // 退款后不再补录发货
             'nextRetryTime' => null,
-        ]);
+        ];
+        // 尚未录入成功:记下跳过原因,避免运维误以为仍在排队
+        if ((int)($ext['shipStatus'] ?? 0) !== self::SHIP_OK) {
+            $data['errMsg'] = '已退款,跳过发货补录';
+        }
+        self::updateById($ext['id'], $data);
         return $remain;
     }
+
+    /**
+     * 取消发货补录队列预约(已退款/无需再录时调用)。
+     */
+    public static function cancelShipRetry($rechargeId, $reason = '已退款,跳过发货补录')
+    {
+        $ext = self::getByRechargeId($rechargeId);
+        if (empty($ext)) {
+            return;
+        }
+        if ((int)($ext['shipStatus'] ?? 0) === self::SHIP_OK) {
+            return;
+        }
+        self::updateById($ext['id'], [
+            'nextRetryTime' => null,
+            'errMsg' => mb_substr((string)$reason, 0, 500),
+        ]);
+    }
 }

+ 28 - 2
biz-mall/order/classes/ScanPayWxExtClass.php

@@ -196,6 +196,7 @@ class ScanPayWxExtClass extends BaseClass
 
     /**
      * 未入账退款解冻:本单 freezeAmount 减少,不改 balancePosted。
+     * 全额退后取消发货补录;队列消费也会因全额退款跳过。
      */
     public static function markUnfreezeOnRefund($scanPayId, $unfreezeAmount)
     {
@@ -213,11 +214,36 @@ class ScanPayWxExtClass extends BaseClass
             $remain = '0.00';
         }
         $freezeStatus = bccomp($remain, '0', 2) <= 0 ? self::FREEZE_DONE : self::FREEZE_ING;
-        self::updateById($ext['id'], [
+        $data = [
             'freezeAmount' => $remain,
             'freezeStatus' => $freezeStatus,
+        ];
+        // 仅全额解冻(通常对应全额退)时取消发货补录;部分退仍可能要录入微信单
+        if (bccomp($remain, '0', 2) <= 0) {
+            $data['nextRetryTime'] = null;
+            if ((int)($ext['shipStatus'] ?? 0) !== self::SHIP_OK) {
+                $data['errMsg'] = '已全额退款,跳过发货补录';
+            }
+        }
+        self::updateById($ext['id'], $data);
+        return $remain;
+    }
+
+    /**
+     * 取消发货补录队列预约(已全额退款/无需再录时调用)。
+     */
+    public static function cancelShipRetry($scanPayId, $reason = '已全额退款,跳过发货补录')
+    {
+        $ext = self::getByScanPayId($scanPayId);
+        if (empty($ext)) {
+            return;
+        }
+        if ((int)($ext['shipStatus'] ?? 0) === self::SHIP_OK) {
+            return;
+        }
+        self::updateById($ext['id'], [
             'nextRetryTime' => null,
+            'errMsg' => mb_substr((string)$reason, 0, 500),
         ]);
-        return $remain;
     }
 }

+ 111 - 25
biz-mall/order/classes/WxFreezeBalanceClass.php

@@ -227,7 +227,10 @@ class WxFreezeBalanceClass
 
     /**
      * 处理 mall 微信 trade_manage_order_settlement。
-     * 先匹配充值扩展,再匹配扫码付款扩展;无 settlement_time 只记待结算。
+     * 字段对应(拉卡拉):
+     * - transaction_id → 本库 thirdPayNo(acc_trade_no,发货用)
+     * - merchant_trade_no → 本库 returnCode(trade_no,退款用);业务 orderSn 仍是我方单号
+     * 无 settlement_time 只记待结算。
      */
     public static function handleMallSettlementEvent($postObj)
     {
@@ -248,8 +251,8 @@ class WxFreezeBalanceClass
             'raw' => json_decode(json_encode($postObj), true),
         ], 'wx-event');
 
-        if ($merchantTradeNo === '') {
-            noticeUtil::push('mall结算事件缺少 merchant_trade_no', '15280215347');
+        if ($merchantTradeNo === '' && $transactionId === '') {
+            noticeUtil::push('mall结算事件缺少 merchant_trade_no 与 transaction_id', '15280215347');
             return;
         }
 
@@ -259,25 +262,17 @@ class WxFreezeBalanceClass
         Yii::debug([
             'scene' => 'mall-settlement-normalized',
             'merchant_trade_no' => $merchantTradeNo,
+            'transaction_id' => $transactionId,
             'settlementTime' => $settlementTime,
             'estimatedTime' => $estimatedTime,
             'willPostBalance' => $settlementTime !== '',
         ], 'wx-event');
 
         // 1) 充值
-        $ext = RechargeWxExtClass::getByOrderSn($merchantTradeNo);
-        if (!empty($ext) || !empty(RechargeClass::getByCondition(['orderSn' => $merchantTradeNo], true))) {
-            if (empty($ext)) {
-                $recharge = RechargeClass::getByCondition(['orderSn' => $merchantTradeNo], true);
-                $rechargeId = (int)$recharge->id;
-            } else {
-                $rechargeId = (int)$ext['rechargeId'];
-                $recharge = RechargeClass::getById($rechargeId, true);
-                if (empty($recharge)) {
-                    noticeUtil::push('mall结算有扩展表但充值单不存在 id=' . $rechargeId, '15280215347');
-                    return;
-                }
-            }
+        $recharge = self::resolveRechargeForSettlement($merchantTradeNo, $transactionId);
+        if (!empty($recharge)) {
+            self::backfillThirdPayNoIfEmpty($recharge, $transactionId);
+            $rechargeId = (int)$recharge->id;
             if ($settlementTime === '') {
                 RechargeWxExtClass::markEstimatedSettlement($rechargeId, $estimatedTime);
                 return;
@@ -287,18 +282,17 @@ class WxFreezeBalanceClass
         }
 
         // 2) 扫码付款
-        $scanExt = ScanPayWxExtClass::getByOrderSn($merchantTradeNo);
-        $scanPay = null;
-        if (!empty($scanExt)) {
-            $scanPay = \bizHd\order\classes\ScanPayClass::getById((int)$scanExt['scanPayId'], true);
-        }
-        if (empty($scanPay)) {
-            $scanPay = \bizHd\order\classes\ScanPayClass::getByCondition(['orderSn' => $merchantTradeNo], true);
-        }
+        $scanPay = self::resolveScanPayForSettlement($merchantTradeNo, $transactionId);
         if (empty($scanPay)) {
-            Yii::info('mall结算未匹配充值/扫码单 orderSn=' . $merchantTradeNo, 'wx-settle');
+            Yii::info(
+                'mall结算未匹配充值/扫码单 merchant_trade_no=' . $merchantTradeNo
+                . ' transaction_id=' . $transactionId,
+                'wx-settle'
+            );
             return;
         }
+        // 支付回调若未写入 thirdPayNo,结算时用 transaction_id 补齐,便于发货/对账
+        self::backfillThirdPayNoIfEmpty($scanPay, $transactionId);
         $scanPayId = (int)$scanPay->id;
         if ($settlementTime === '') {
             ScanPayWxExtClass::markEstimatedSettlement($scanPayId, $estimatedTime);
@@ -307,6 +301,98 @@ class WxFreezeBalanceClass
         self::settleScanPay($scanPay, $settlementTime);
     }
 
+    /**
+     * 结算命中后回填 thirdPayNo(=微信 transaction_id),修复支付回调 acc_trade_no 为空或未落库的历史单。
+     */
+    private static function backfillThirdPayNoIfEmpty($model, $transactionId)
+    {
+        $transactionId = trim((string)$transactionId);
+        if ($transactionId === '' || empty($model)) {
+            return;
+        }
+        if (!$model->hasAttribute('thirdPayNo')) {
+            return;
+        }
+        if (trim((string)($model->thirdPayNo ?? '')) !== '') {
+            return;
+        }
+        $model->thirdPayNo = mb_substr($transactionId, 0, 50);
+        $model->save(false, ['thirdPayNo']);
+    }
+
+    /**
+     * 结算定位充值单。
+     * 优先 thirdPayNo=transaction_id,再 returnCode=merchant_trade_no,最后 orderSn。
+     */
+    private static function resolveRechargeForSettlement($merchantTradeNo, $transactionId)
+    {
+        if ($transactionId !== '') {
+            $row = RechargeClass::getByCondition(['thirdPayNo' => $transactionId], true);
+            if (!empty($row)) {
+                return $row;
+            }
+        }
+        if ($merchantTradeNo !== '') {
+            $row = RechargeClass::getByCondition(['returnCode' => $merchantTradeNo], true);
+            if (!empty($row)) {
+                return $row;
+            }
+            $ext = RechargeWxExtClass::getByOrderSn($merchantTradeNo);
+            if (!empty($ext)) {
+                $row = RechargeClass::getById((int)$ext['rechargeId'], true);
+                if (!empty($row)) {
+                    return $row;
+                }
+            }
+            $row = RechargeClass::getByCondition(['orderSn' => $merchantTradeNo], true);
+            if (!empty($row)) {
+                return $row;
+            }
+            // 兼容旧数据误把 trade_no 写进 thirdPayNo
+            $row = RechargeClass::getByCondition(['thirdPayNo' => $merchantTradeNo], true);
+            if (!empty($row)) {
+                return $row;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 结算定位扫码付款单。
+     * 优先 thirdPayNo=transaction_id,再 returnCode=merchant_trade_no,最后 orderSn。
+     */
+    private static function resolveScanPayForSettlement($merchantTradeNo, $transactionId)
+    {
+        if ($transactionId !== '') {
+            $row = \bizHd\order\classes\ScanPayClass::getByCondition(['thirdPayNo' => $transactionId], true);
+            if (!empty($row)) {
+                return $row;
+            }
+        }
+        if ($merchantTradeNo !== '') {
+            $row = \bizHd\order\classes\ScanPayClass::getByCondition(['returnCode' => $merchantTradeNo], true);
+            if (!empty($row)) {
+                return $row;
+            }
+            $ext = ScanPayWxExtClass::getByOrderSn($merchantTradeNo);
+            if (!empty($ext)) {
+                $row = \bizHd\order\classes\ScanPayClass::getById((int)$ext['scanPayId'], true);
+                if (!empty($row)) {
+                    return $row;
+                }
+            }
+            $row = \bizHd\order\classes\ScanPayClass::getByCondition(['orderSn' => $merchantTradeNo], true);
+            if (!empty($row)) {
+                return $row;
+            }
+            $row = \bizHd\order\classes\ScanPayClass::getByCondition(['thirdPayNo' => $merchantTradeNo], true);
+            if (!empty($row)) {
+                return $row;
+            }
+        }
+        return null;
+    }
+
     /**
      * 扫码付款已结算:解冻 + customScanPayAddBalance 入账;balancePosted 防重。
      */

+ 41 - 0
biz-mall/order/classes/WxOrderShippingClass.php

@@ -118,6 +118,12 @@ class WxOrderShippingClass
         if ((int)($recharge['payStatus'] ?? 0) !== 1) {
             return;
         }
+        // 已退款:未录入则不再调微信,并取消补录预约(队列消息消费时同样跳过)
+        if ((int)($recharge['isRefund'] ?? 0) === 1) {
+            RechargeWxExtClass::cancelShipRetry($rechargeId, '已退款,跳过发货录入');
+            Yii::info('充值已退款,跳过虚拟发货 rechargeId=' . $rechargeId, 'wx-order-shipping');
+            return;
+        }
 
         $capitalType = (int)dict::getDict('capitalType', 'xhRecharge', 'id');
         $orderSn = (string)($recharge['orderSn'] ?? '');
@@ -200,6 +206,16 @@ class WxOrderShippingClass
 
         // 可重试失败:投递 RabbitMQ 延迟消息(方案 B),等微信支付单入库后再补录
         if ($errcode !== 0 && !RechargeWxExtClass::isPermanentShipError($errmsg)) {
+            // 投递前再查一次:期间若已退款则不再入队
+            try {
+                $fresh = \bizHd\recharge\classes\RechargeClass::getById($rechargeId, true);
+                if (!empty($fresh) && (int)($fresh->isRefund ?? 0) === 1) {
+                    RechargeWxExtClass::cancelShipRetry($rechargeId, '已退款,跳过发货补录');
+                    return;
+                }
+            } catch (\Throwable $e) {
+                // 查退款失败仍按原逻辑尝试投递,避免误伤
+            }
             $ext = RechargeWxExtClass::getByRechargeId($rechargeId);
             $retryCount = (int)($ext['shipRetryCount'] ?? 0);
             if ($retryCount < RechargeWxExtClass::MAX_SHIP_RETRY && !empty($ext['nextRetryTime'])) {
@@ -233,6 +249,14 @@ class WxOrderShippingClass
         if ((int)($scanPay['payStatus'] ?? 0) !== 1) {
             return;
         }
+        // 已全额退款:未录入则不再调微信,并取消补录预约
+        $actPrice = bcadd((string)($scanPay['actPrice'] ?? '0'), '0', 2);
+        $tkPrice = bcadd((string)($scanPay['tkPrice'] ?? '0'), '0', 2);
+        if (bccomp($actPrice, '0', 2) > 0 && bccomp($tkPrice, $actPrice, 2) >= 0) {
+            ScanPayWxExtClass::cancelShipRetry($scanPayId, '已全额退款,跳过发货录入');
+            Yii::info('扫码付款已全额退款,跳过虚拟发货 scanPayId=' . $scanPayId, 'wx-order-shipping');
+            return;
+        }
 
         $capitalType = (int)dict::getDict('capitalType', 'scanPay', 'id');
         $orderSn = (string)($scanPay['orderSn'] ?? '');
@@ -312,6 +336,20 @@ class WxOrderShippingClass
         }
 
         if ($errcode !== 0 && !ScanPayWxExtClass::isPermanentShipError($errmsg)) {
+            // 投递前再查:期间若已全额退款则不再入队
+            try {
+                $fresh = \bizHd\order\classes\ScanPayClass::getById($scanPayId, true);
+                if (!empty($fresh)) {
+                    $act = bcadd((string)($fresh->actPrice ?? '0'), '0', 2);
+                    $tk = bcadd((string)($fresh->tkPrice ?? '0'), '0', 2);
+                    if (bccomp($act, '0', 2) > 0 && bccomp($tk, $act, 2) >= 0) {
+                        ScanPayWxExtClass::cancelShipRetry($scanPayId, '已全额退款,跳过发货补录');
+                        return;
+                    }
+                }
+            } catch (\Throwable $e) {
+                // ignore
+            }
             $ext = ScanPayWxExtClass::getByScanPayId($scanPayId);
             $retryCount = (int)($ext['shipRetryCount'] ?? 0);
             if ($retryCount < ScanPayWxExtClass::MAX_SHIP_RETRY && !empty($ext['nextRetryTime'])) {
@@ -442,6 +480,9 @@ class WxOrderShippingClass
             return true;
         }
         if ((int)($recharge->isRefund ?? 0) === 1) {
+            // 已在队列里的消息:ACK 并取消后续补录预约
+            RechargeWxExtClass::cancelShipRetry($rechargeId, '已退款,跳过发货补录');
+            Yii::info('延迟补录跳过:充值已退款 rechargeId=' . $rechargeId, 'wx-order-shipping');
             return true;
         }