Browse Source

Merge branch 'dev' of http://git.huaml.com/zhh/huahuibao into dev

shish 3 days ago
parent
commit
9f62ad67cb

+ 71 - 0
app-hd/controllers/MainWalletController.php

@@ -1,11 +1,15 @@
 <?php
 /**
  * hdApp 中央钱包(可用余额)接口
+ * 用途:变动明细、微信充值下单
  */
 namespace hd\controllers;
 
 use bizHd\shop\classes\MainWalletChangeClass;
+use bizHd\shop\classes\MainWalletRechargeClass;
+use bizHd\wx\classes\WxOpenClass;
 use common\components\dateUtil;
+use common\components\dict;
 use common\components\util;
 use Yii;
 
@@ -50,4 +54,71 @@ class MainWalletController extends BaseController
         util::success($list);
     }
 
+    /**
+     * 中央钱包微信充值:建未支付单并返回 JSAPI 支付参数
+     * POST:amount、miniOpenId(可选,缺省取当前 admin)
+     */
+    public function actionRecharge()
+    {
+        ini_set('date.timezone', 'Asia/Shanghai');
+        $post = Yii::$app->request->post();
+        $amount = isset($post['amount']) ? floor(((float) $post['amount']) * 100) / 100 : 0;
+        if ($amount <= 0) {
+            util::fail('请输入充值金额');
+        }
+
+        $admin = !empty($this->admin) ? $this->admin->attributes : [];
+        $openId = trim((string) ($post['miniOpenId'] ?? ($admin['miniOpenId'] ?? '')));
+        if ($openId === '') {
+            // 前端可据此走 uni.login 补 openId 后重试
+            util::success(['hasNoMiniOpenId' => 1]);
+        }
+
+        $recharge = MainWalletRechargeClass::createPending(
+            (int) $this->mainId,
+            (int) $this->shopId,
+            $amount
+        );
+        $orderId = (int) ($recharge->id ?? 0);
+        $orderSn = (string) ($recharge->orderSn ?? '');
+        if ($orderId <= 0 || $orderSn === '') {
+            util::fail('创建充值单失败');
+        }
+
+        $capitalType = dict::getDict('capitalType', 'mainWalletRecharge', 'id');
+        $attach = 'capitalType=' . $capitalType . '&mainId=' . (int) $this->mainId;
+
+        $now = time();
+        $expireTime = $now + 300;
+
+        $wx = Yii::getAlias('@vendor/weixin');
+        require_once $wx . '/lib/WxPay.Api.php';
+        require_once $wx . '/example/WxPay.JsApiPay.php';
+        $input = new \WxPayUnifiedOrder();
+        $input->SetBody('中央钱包充值');
+        $input->SetOut_trade_no($orderSn);
+        $input->SetTotal_fee((int) round($amount * 100));
+        $input->SetTime_start(date('YmdHis', $now));
+        $input->SetAttach($attach);
+        $input->SetTime_expire(date('YmdHis', $expireTime));
+        $input->SetNotify_url(Yii::$app->params['hdHost'] . '/notice/wx-callback/');
+        $input->SetTrade_type('JSAPI');
+
+        $merchantExtend = WxOpenClass::getWxInfo();
+        $input->SetOpenid($openId);
+        $merchantExtend['wxAppId'] = $merchantExtend['miniAppId'];
+
+        $wxOrder = \WxPayApi::unifiedOrder($input, 6, $merchantExtend);
+        $tools = new \JsApiPay();
+        $jsApiParameters = $tools->GetJsApiParameters($wxOrder, $merchantExtend);
+        $newParams = json_decode($jsApiParameters, true) ?: [];
+        $newParams['orderId'] = $orderId;
+        $newParams['orderSn'] = $orderSn;
+        $newParams['needPay'] = 1;
+        $newParams['paid'] = 0;
+        $newParams['amount'] = $amount;
+        $newParams['hasNoMiniOpenId'] = 0;
+        util::success($newParams);
+    }
+
 }

+ 0 - 17
app-mall/controllers/NoticeController.php

@@ -22,7 +22,6 @@ use common\components\lakala\Lakala;
 use bizHd\recharge\classes\RechargeShbClass;
 use bizHd\hb\classes\HbClass;
 use bizHd\hb\classes\HbManageClass;
-use bizHd\order\classes\PsMethodClass;
 
 class NoticeController extends PublicController
 {
@@ -197,9 +196,6 @@ class NoticeController extends PublicController
 
                         //来自商城的新订单微信通知
                         WxMessageClass::hdNewOrderInform($shop, $order);
-
-                        // 标记 xhPsMethod 未达门槛附加费当日已收(每日每客户每配送方式只收一次)
-                        self::markPsUnMeetFeeAfterPay($order);
                     }
                 }
             }
@@ -299,10 +295,6 @@ class NoticeController extends PublicController
 
                 $shopExt = ShopExtClass::getByCondition(['shopId' => $shopId], true);
                 ShopExtClass::newWorkRemind($shopExt, $order);
-
-                if ($order->fromType == dict::getDict("fromType", "mall")) {
-                    self::markPsUnMeetFeeAfterPay($order);
-                }
             }
             exit();
         } catch (\Exception $e) {
@@ -417,7 +409,6 @@ class NoticeController extends PublicController
                 if ($order->fromType == dict::getDict("fromType", "mall")) {
                     //来自商城的新订单微信通知
                     WxMessageClass::hdNewOrderInform($shop, $order);
-                    self::markPsUnMeetFeeAfterPay($order);
                 }
             }
             exit();
@@ -560,12 +551,4 @@ class NoticeController extends PublicController
         }
     }
 
-    /**
-     * 商城订单支付成功后,标记 xhPsMethod 未达门槛附加费当日已收
-     */
-    protected static function markPsUnMeetFeeAfterPay($order)
-    {
-        PsMethodClass::markUnMeetFeePaidFromOrder($order);
-    }
-
 }

+ 51 - 21
app-mall/controllers/OrderController.php

@@ -6,7 +6,10 @@ use biz\product\classes\ProductClass;
 use biz\shop\classes\ShopClass;
 use biz\wx\classes\WxMessageClass;
 use bizHd\custom\classes\HdClass;
+use bizHd\distribution\classes\DistributionOrderClass;
 use bizHd\express\classes\HdDeliveryOrderClass;
+use bizHd\order\classes\OrderClass as HdOrderClass;
+use bizHd\refund\classes\MallRefundApplyClass;
 use bizHd\hb\classes\HbClass;
 use bizHd\homePageConfig\classes\HomePageModuleClass;
 use bizHd\order\classes\ScanPayClass;
@@ -1280,11 +1283,8 @@ class OrderController extends BaseController
             unset($post['sendCost'], $post['packCost'], $post['packingFee'], $post['unMeetSendCost']);
 
             $sendType = intval($post['sendType'] ?? 0);
-            $customInfo = array_merge(
-                is_array($custom) ? $custom : ($custom ? $custom->attributes : []),
-                is_array($hd) ? $hd : ($hd ? $hd->attributes : [])
-            );
-            $additionalCosts = \bizHd\order\classes\PsMethodClass::calcFee(
+            $customInfo = \bizHd\order\classes\PsMethodClass::buildCustomInfo($hd, $custom);
+            $additionalCosts = \bizHd\order\classes\PsMethodClass::calcFeeForMallContext(
                 $this->shopId,
                 $this->mainId,
                 $sendType,
@@ -1323,20 +1323,17 @@ class OrderController extends BaseController
                     if (empty($post['deliveryPlatform'])) {
                         util::fail('请选择跑腿');
                     }
-                    $quoteCustom = $this->user;
-                    if ($quoteGoodsType === 0) {
-                        $quoteCustom = (object)[
-                            'name' => $post['receiveUserName'] ?? '',
-                            'mobile' => $post['receiveMobile'] ?? '',
-                            'fullAddress' => $post['address'] ?? '',
-                            'floor' => $post['floor'] ?? '',
-                            'dist' => '',
-                            'lat' => $post['lat'],
-                            'long' => $post['long'],
-                            'address' => $post['address'] ?? '',
-                            'city' => $post['city'] ?? '',
-                        ];
-                    }
+                    $quoteCustom = (object)[
+                        'name' => $post['receiveUserName'] ?? '',
+                        'mobile' => $post['receiveMobile'] ?? '',
+                        'fullAddress' => $post['address'] ?? '',
+                        'floor' => $post['floor'] ?? '',
+                        'dist' => '',
+                        'lat' => $post['lat'],
+                        'long' => $post['long'],
+                        'address' => $post['address'] ?? '',
+                        'city' => $post['city'] ?? '',
+                    ];
                     $quoteResult = DeliveryQuoteUtil::getDeliveryQuote([
                         'productList' => $resolvedProduct,
                         'deliveryPlatform' => $post['deliveryPlatform'],
@@ -1436,7 +1433,7 @@ class OrderController extends BaseController
 
             $orderSn = $return->orderSn ?? '';
             if (!empty($orderSn) && (float)$unMeetSendCost > 0) {
-                $unMeetSendKey = 'hd_ps_order_unmeet_send_' . $orderSn;
+                $unMeetSendKey = 'hd_order_unmeet_send:' . $orderSn;
                 Yii::$app->redis->executeCommand('SETEX', [$unMeetSendKey, 864000, (string)$unMeetSendCost]);
             }
 
@@ -1547,7 +1544,6 @@ class OrderController extends BaseController
                     if ($order->fromType == 2) {
                         //来自商城的新订单微信通知
                         WxMessageClass::hdNewOrderInform($shop, $order);
-                        \bizHd\order\classes\PsMethodClass::markUnMeetFeePaidFromOrder($order);
                     }
                 }
             }
@@ -1919,9 +1915,43 @@ class OrderController extends BaseController
         $detail['balance'] = $balance;
 
         $detail['rechargeWeal'] = $this->shop->rechargeWeal; //是否开启余额支付
+        // 与 RefundController::canApplyRefund 对齐,供详情页「申请售后」显隐
+        $detail['distOrderSettled'] = DistributionOrderClass::isDistOrderSettled((int)$id) ? 1 : 0;
+        $detail['canApplyRefund'] = $this->canApplyRefundForDetail($detail) ? 1 : 0;
         util::success($detail);
     }
 
+    /**
+     * 订单详情是否可申请售后(与 mall RefundController::canApplyRefund 规则一致)
+     * @param array $order 订单详情
+     * @return bool
+     */
+    protected function canApplyRefundForDetail($order)
+    {
+        if ((int)($order['payStatus'] ?? 0) !== 1) {
+            return false;
+        }
+        $status = (int)($order['status'] ?? 0);
+        if (in_array($status, [HdOrderClass::ORDER_STATUS_UN_PAY, HdOrderClass::ORDER_STATUS_CANCEL, 6], true)) {
+            return false;
+        }
+        if ((int)($order['forward'] ?? 0) === 1) {
+            return false;
+        }
+        $apply = MallRefundApplyClass::getLatestByOrderId((int)($order['id'] ?? 0));
+        if (!empty($apply) && (int)($apply['status'] ?? -1) === MallRefundApplyClass::STATUS_PENDING) {
+            return false;
+        }
+        if (DistributionOrderClass::isDistOrderSettled((int)($order['id'] ?? 0))) {
+            return false;
+        }
+        $could = bcsub((string)($order['orderPrice'] ?? 0), (string)($order['tkPrice'] ?? 0), 2);
+        if (bccomp($could, '0', 2) <= 0) {
+            return false;
+        }
+        return true;
+    }
+
     //获取详情 ssh 20220512
     public function actionInfo()
     {

+ 83 - 39
app-mall/controllers/PsMethodController.php

@@ -8,12 +8,13 @@ use Yii;
 
 /**
  * 商城端配送方式接口(xhPsMethod / xhPsExplain)
- * 用途:混合结算页展示各购买方式对应的配送说明
+ * 用途:混合结算页展示各购买方式对应的配送说明;预览未达门槛附加费(与下单 calcFee 同源)
  */
 class PsMethodController extends BaseController
 {
     /**
      * 获取门店全部配送方式及说明项
+     * 可选 GET:itemTotalAmount、bigNum — 一并返回各 style 附加费预览(进页一次算好)
      */
     public function actionGetAllMethod()
     {
@@ -23,48 +24,91 @@ class PsMethodController extends BaseController
 
         try {
             $configs = PsMethodClass::getAllConfigs($this->shopId, $this->mainId);
+            $customInfo = PsMethodClass::buildCustomInfo($this->hd ?? null, $this->custom ?? null);
+            $configs = PsMethodClass::applyCustomHomeRuleToConfigs($configs, $customInfo);
+            $configs = PsMethodClass::applyDailyFeeLockFlags($configs, (int)($this->customId ?? 0));
 
-            // 客户单独送货上门规则覆盖 style=0
-            if (!empty($this->custom) || !empty($this->hd)) {
-                $customInfo = [];
-                if (!empty($this->hd)) {
-                    $customInfo = is_array($this->hd) ? $this->hd : $this->hd->toArray();
-                }
-                if (!empty($this->custom)) {
-                    $customInfo = array_merge($customInfo, is_array($this->custom) ? $this->custom : $this->custom->toArray());
-                }
-                foreach ($configs as &$config) {
-                    if ((int)($config['style'] ?? 0) === 0) {
-                        $config = PsMethodClass::applyCustomHomeRule($config, $customInfo);
-                    }
-                }
-                unset($config);
+            $result = ['method' => $configs];
+            $itemTotalAmount = Yii::$app->request->get('itemTotalAmount');
+            $bigNum = Yii::$app->request->get('bigNum');
+            if ($itemTotalAmount !== null && $itemTotalAmount !== '' && $bigNum !== null && $bigNum !== '') {
+                $result['fees'] = PsMethodClass::previewMixUnMeetFeeBatch(
+                    $this->shopId,
+                    $this->mainId,
+                    (float)$itemTotalAmount,
+                    (float)$bigNum,
+                    (int)($this->customId ?? 0),
+                    $this->hd ?? null,
+                    $this->custom ?? null
+                );
             }
 
-            // 今日已对附加费做过决策时,前端不再展示门槛加收(每日每客户每购买方式只收一次;首单未收则当日不再收)
-            $customId = (int)($this->customId ?? 0);
-            if ($customId > 0) {
-                foreach ($configs as &$config) {
-                    $style = (int)($config['style'] ?? 0);
-                    $unMeet = (int)($config['unMeet'] ?? 0);
-                    $hasSendDecision = PsMethodClass::hasSendFeeDecisionToday($customId, $style);
-                    $hasPackDecision = PsMethodClass::hasPackFeeDecisionToday($customId, $style);
-                    $config['todaySendFeeLocked'] = $hasSendDecision ? 1 : 0;
-                    $config['todayPackFeeLocked'] = $hasPackDecision ? 1 : 0;
-                    if ($unMeet === 0 && $hasSendDecision) {
-                        $config['minAmount'] = 0;
-                        $config['minNum'] = 0;
-                        $config['unMeetFee'] = 0;
-                    } elseif ($unMeet === 2 && $hasPackDecision) {
-                        $config['minAmount'] = 0;
-                        $config['minNum'] = 0;
-                        $config['unMeetFee'] = 0;
-                    }
-                }
-                unset($config);
-            }
+            util::success($result);
+        } catch (\Exception $e) {
+            util::fail($e->getMessage());
+        }
+    }
+
+    /**
+     * 混合结算页批量预览未达门槛附加费(商品金额/扎数变化时刷新,切换购买方式不再重复请求)
+     * POST: itemTotalAmount, bigNum
+     */
+    public function actionCalcMixFeeBatch()
+    {
+        if (empty($this->shopId) || empty($this->mainId)) {
+            util::fail('门店信息无效');
+        }
+
+        $itemTotalAmount = (float)Yii::$app->request->post('itemTotalAmount', 0);
+        $bigNum = (float)Yii::$app->request->post('bigNum', 0);
+
+        try {
+            $fees = PsMethodClass::previewMixUnMeetFeeBatch(
+                $this->shopId,
+                $this->mainId,
+                $itemTotalAmount,
+                $bigNum,
+                (int)($this->customId ?? 0),
+                $this->hd ?? null,
+                $this->custom ?? null
+            );
+            util::success(['fees' => $fees]);
+        } catch (\Exception $e) {
+            util::fail($e->getMessage());
+        }
+    }
 
-            util::success(['method' => $configs]);
+    /**
+     * 单购买方式预览(保留兼容)
+     * POST: sendType, itemTotalAmount, bigNum
+     */
+    public function actionCalcMixFee()
+    {
+        if (empty($this->shopId) || empty($this->mainId)) {
+            util::fail('门店信息无效');
+        }
+
+        $sendType = (int)Yii::$app->request->post('sendType', -1);
+        if ($sendType < 0 || $sendType > 4) {
+            util::fail('配送方式无效');
+        }
+
+        $itemTotalAmount = (float)Yii::$app->request->post('itemTotalAmount', 0);
+        $bigNum = (float)Yii::$app->request->post('bigNum', 0);
+        $customInfo = PsMethodClass::buildCustomInfo($this->hd ?? null, $this->custom ?? null);
+        $customId = (int)($this->customId ?? 0);
+
+        try {
+            $result = PsMethodClass::previewMixUnMeetFee(
+                $this->shopId,
+                $this->mainId,
+                $sendType,
+                $itemTotalAmount,
+                $bigNum,
+                $customId,
+                $customInfo
+            );
+            util::success($result);
         } catch (\Exception $e) {
             util::fail($e->getMessage());
         }

+ 18 - 11
app-mall/controllers/RefundController.php

@@ -43,17 +43,21 @@ class RefundController extends BaseController
 
         // 分销售后截止提示(仅提示,真正拦截在提交时)
         $refundDeadlineTip = '';
-        $distOrder = DistributionOrderClass::getByCondition(['orderId' => $id]);
-        if (!empty($distOrder)) {
-            $finishTime = $distOrder['finishTime'] ?? '';
-            if (!empty($finishTime) && $finishTime !== '0000-00-00 00:00:00') {
-                $rule = \bizHd\distribution\classes\DistributionRuleClass::getRule(
-                    (int)($order['shopId'] ?? 0),
-                    (int)($order['mainId'] ?? $this->mainId)
-                );
-                $settleDays = max(0, (int)($rule['settleDays'] ?? 0));
-                $deadline = date('Y-m-d H:i', strtotime($finishTime) + $settleDays * 86400);
-                $refundDeadlineTip = "分销订单完成后{$settleDays}天内可申请售后,截止:{$deadline}";
+        if (DistributionOrderClass::isDistOrderSettled($id)) {
+            $refundDeadlineTip = '该订单分销佣金已结算,无法申请售后';
+        } else {
+            $distOrder = DistributionOrderClass::getByCondition(['orderId' => $id]);
+            if (!empty($distOrder)) {
+                $finishTime = $distOrder['finishTime'] ?? '';
+                if (!empty($finishTime) && $finishTime !== '0000-00-00 00:00:00') {
+                    $rule = \bizHd\distribution\classes\DistributionRuleClass::getRule(
+                        (int)($order['shopId'] ?? 0),
+                        (int)($order['mainId'] ?? $this->mainId)
+                    );
+                    $settleDays = max(0, (int)($rule['settleDays'] ?? 0));
+                    $deadline = date('Y-m-d H:i', strtotime($finishTime) + $settleDays * 86400);
+                    $refundDeadlineTip = "分销订单完成后{$settleDays}天内可申请售后,截止:{$deadline}";
+                }
             }
         }
 
@@ -237,6 +241,9 @@ class RefundController extends BaseController
         if (!empty($apply) && (int)($apply['status'] ?? -1) === MallRefundApplyClass::STATUS_PENDING) {
             return 0;
         }
+        if (DistributionOrderClass::isDistOrderSettled((int)($order['id'] ?? 0))) {
+            return 0;
+        }
         $could = bcsub((string)($order['orderPrice'] ?? 0), (string)($order['tkPrice'] ?? 0), 2);
         if (bccomp($could, '0', 2) <= 0) {
             return 0;

+ 3 - 1
biz-hd/distribution/classes/DistributionCommissionClass.php

@@ -194,7 +194,9 @@ class DistributionCommissionClass
         if ((int)($order->status ?? 0) === OrderClass::ORDER_STATUS_CANCEL) {
             throw new \Exception('订单已取消,无法计佣');
         }
-
+        if ((int)($order->groupBuyId ?? 0) > 0 || (int)($order->hasSeckill ?? 0) == 1) {
+            throw new \Exception('特价订单不参与分佣');
+        }
         $shop = ShopClass::getById($shopId, true);
         $mainId = (int)($shop->mainId ?? 0);
         $rule = DistributionRuleClass::getRule($shopId, $mainId);

+ 21 - 0
biz-hd/distribution/classes/DistributionOrderClass.php

@@ -173,6 +173,24 @@ class DistributionOrderClass extends BaseClass
         return 'invalid';
     }
 
+    /**
+     * 订单是否因 xhDistributionOrder 已结算而不可申请售后
+     * @param int $orderId xhOrder.id
+     * @return bool true=不可申请
+     */
+    public static function isDistOrderSettled($orderId)
+    {
+        $orderId = (int)$orderId;
+        if ($orderId <= 0) {
+            return false;
+        }
+        $distOrder = self::getByCondition(['orderId' => $orderId]);
+        if (empty($distOrder)) {
+            return false;
+        }
+        return (int)($distOrder['settleStatus'] ?? 0) === 1;
+    }
+
     /**
      * 分销关联订单的售后期限校验:订单完成后超过 settleDays 天不可再发起退款
      * 无记录(与分销无关)或不存在 finishTime(尚未完成计佣)则放行
@@ -186,6 +204,9 @@ class DistributionOrderClass extends BaseClass
         if ($orderId <= 0) {
             return;
         }
+        if (self::isDistOrderSettled($orderId)) {
+            util::fail('该订单分销佣金已结算,无法申请售后');
+        }
         $distOrder = self::getByCondition(['orderId' => $orderId]);
         if (empty($distOrder)) {
             return;

+ 6 - 0
biz-hd/order/classes/OrderClass.php

@@ -676,6 +676,12 @@ class OrderClass extends BaseClass
         if ((int)($order->forward ?? 0) === 0) {
             DistributionCommissionClass::tryCalcDistributionAfterPay((int)$orderId);
         }
+
+        // 商城订单支付成功:标记 xhPsMethod 未达门槛附加费当日决策(与 NoticeController / 余额支付同源)
+        if ((int)($order->forward ?? 0) === 0
+            && (int)($order->fromType ?? 0) === (int)dict::getDict('fromType', 'mall')) {
+            PsMethodClass::markUnMeetFeePaidFromOrder($order);
+        }
     }
 
     //转化出送到时间 ssh 2020.3.15

+ 202 - 9
biz-hd/order/classes/PsMethodClass.php

@@ -344,7 +344,7 @@ class PsMethodClass extends BaseClass
             return false;
         }
         $current = date('Y_m_d');
-        $key = 'hd_ps_custom_today_has_get_pack_cost_' . (int)$sendType . '_' . $current . '_' . $customId;
+        $key = 'hdCustom_hasGetPackCost:' . (int)$sendType . '_' . $current . ':' . $customId;
         $has = Yii::$app->redis->executeCommand('GET', [$key]);
         return (!empty($has) && $has == 1);
     }
@@ -358,7 +358,7 @@ class PsMethodClass extends BaseClass
             return false;
         }
         $current = date('Y_m_d');
-        $key = 'hd_ps_custom_today_skip_pack_cost_' . (int)$sendType . '_' . $current . '_' . $customId;
+        $key = 'hdCustom_skipPackCost:' . (int)$sendType . '_' . $current . ':' . $customId;
         $has = Yii::$app->redis->executeCommand('GET', [$key]);
         return (!empty($has) && $has == 1);
     }
@@ -380,7 +380,7 @@ class PsMethodClass extends BaseClass
             return false;
         }
         $current = date('Y_m_d');
-        $key = 'hd_ps_custom_today_has_get_send_cost_' . (int)$sendType . '_' . $current . '_' . $customId;
+        $key = 'hdCustom_hasGetSendCost:' . (int)$sendType . '_' . $current . ':' . $customId;
         $has = Yii::$app->redis->executeCommand('GET', [$key]);
         return (!empty($has) && $has == 1);
     }
@@ -394,7 +394,7 @@ class PsMethodClass extends BaseClass
             return false;
         }
         $current = date('Y_m_d');
-        $key = 'hd_ps_custom_today_skip_send_cost_' . (int)$sendType . '_' . $current . '_' . $customId;
+        $key = 'hdCustom_skipSendCost:' . (int)$sendType . '_' . $current . ':' . $customId;
         $has = Yii::$app->redis->executeCommand('GET', [$key]);
         return (!empty($has) && $has == 1);
     }
@@ -429,20 +429,20 @@ class PsMethodClass extends BaseClass
 
         if (!self::hasSendFeeDecisionToday($customId, $style)) {
             if ((float)$unMeetSendCost > 0) {
-                $sendKey = 'hd_ps_custom_today_has_get_send_cost_' . $style . '_' . $current . '_' . $customId;
+                $sendKey = 'hdCustom_hasGetSendCost:' . $style . '_' . $current . ':' . $customId;
                 Yii::$app->redis->executeCommand('SETEX', [$sendKey, $ttl, '1']);
             } else {
-                $skipKey = 'hd_ps_custom_today_skip_send_cost_' . $style . '_' . $current . '_' . $customId;
+                $skipKey = 'hdCustom_skipSendCost:' . $style . '_' . $current . ':' . $customId;
                 Yii::$app->redis->executeCommand('SETEX', [$skipKey, $ttl, '1']);
             }
         }
 
         if (!self::hasPackFeeDecisionToday($customId, $style)) {
             if ((float)$packingFee > 0) {
-                $packKey = 'hd_ps_custom_today_has_get_pack_cost_' . $style . '_' . $current . '_' . $customId;
+                $packKey = 'hdCustom_hasGetPackCost:' . $style . '_' . $current . ':' . $customId;
                 Yii::$app->redis->executeCommand('SETEX', [$packKey, $ttl, '1']);
             } else {
-                $skipKey = 'hd_ps_custom_today_skip_pack_cost_' . $style . '_' . $current . '_' . $customId;
+                $skipKey = 'hdCustom_skipPackCost:' . $style . '_' . $current . ':' . $customId;
                 Yii::$app->redis->executeCommand('SETEX', [$skipKey, $ttl, '1']);
             }
         }
@@ -462,15 +462,208 @@ class PsMethodClass extends BaseClass
         $packingFee = (float)(is_object($order) ? ($order->packingFee ?? 0) : ($order['packingFee'] ?? 0));
         $unMeetSendCost = 0;
         if (!empty($orderSn)) {
-            $unMeetSendKey = 'hd_ps_order_unmeet_send_' . $orderSn;
+            $unMeetSendKey = 'hd_order_unmeet_send:' . $orderSn;
             $cached = Yii::$app->redis->executeCommand('GET', [$unMeetSendKey]);
             if (!empty($cached)) {
                 $unMeetSendCost = (float)$cached;
             }
         }
+        // 无附加包装费/附加运费时不写入(避免满足门槛的订单误记 skip)
+        if ($packingFee <= 0 && $unMeetSendCost <= 0) {
+            return;
+        }
         self::recordDailyUnMeetFeeState($customId, $sendType, $packingFee, $unMeetSendCost);
     }
 
+    /**
+     * 合并 hd + custom 为 calcFee / applyCustomHomeRule 使用的客户信息(与商城 getAllMethod 一致)
+     * @param mixed $hd 花店关系 xhHd
+     * @param mixed $custom 客户 xhCustom
+     * @return array
+     */
+    public static function buildCustomInfo($hd = null, $custom = null)
+    {
+        $customInfo = [];
+        if (!empty($hd)) {
+            if (is_array($hd)) {
+                $customInfo = $hd;
+            } elseif (is_object($hd) && method_exists($hd, 'toArray')) {
+                $customInfo = $hd->toArray();
+            } elseif (is_object($hd) && isset($hd->attributes)) {
+                $customInfo = $hd->attributes;
+            } else {
+                $customInfo = (array)$hd;
+            }
+        }
+        if (!empty($custom)) {
+            if (is_array($custom)) {
+                $customArr = $custom;
+            } elseif (is_object($custom) && method_exists($custom, 'toArray')) {
+                $customArr = $custom->toArray();
+            } elseif (is_object($custom) && isset($custom->attributes)) {
+                $customArr = $custom->attributes;
+            } else {
+                $customArr = (array)$custom;
+            }
+            $customInfo = array_merge($customInfo, $customArr);
+        }
+        return $customInfo;
+    }
+
+    /**
+     * 对配送方式列表应用 style=0 客户单独送货上门规则
+     * @param array $configs getAllConfigs 结果
+     * @param array $customInfo buildCustomInfo 结果
+     * @return array
+     */
+    public static function applyCustomHomeRuleToConfigs(array $configs, array $customInfo)
+    {
+        if (empty($customInfo)) {
+            return $configs;
+        }
+        foreach ($configs as &$config) {
+            if ((int)($config['style'] ?? 0) === 0) {
+                $config = self::applyCustomHomeRule($config, $customInfo);
+            }
+        }
+        unset($config);
+        return $configs;
+    }
+
+    /**
+     * 标记当日附加费是否已决策(供商城 getAllMethod 下发锁定态)
+     * @param array $configs 配送方式列表
+     * @param int $customId 花店客户 id
+     * @return array
+     */
+    public static function applyDailyFeeLockFlags(array $configs, $customId)
+    {
+        $customId = (int)$customId;
+        if ($customId <= 0) {
+            return $configs;
+        }
+        foreach ($configs as &$config) {
+            $style = (int)($config['style'] ?? 0);
+            $unMeet = (int)($config['unMeet'] ?? 0);
+            $hasSendDecision = self::hasSendFeeDecisionToday($customId, $style);
+            $hasPackDecision = self::hasPackFeeDecisionToday($customId, $style);
+            $config['todaySendFeeLocked'] = $hasSendDecision ? 1 : 0;
+            $config['todayPackFeeLocked'] = $hasPackDecision ? 1 : 0;
+            if ($unMeet === 0 && $hasSendDecision) {
+                $config['minAmount'] = 0;
+                $config['minNum'] = 0;
+                $config['unMeetFee'] = 0;
+            } elseif ($unMeet === 2 && $hasPackDecision) {
+                $config['minAmount'] = 0;
+                $config['minNum'] = 0;
+                $config['unMeetFee'] = 0;
+            }
+        }
+        unset($config);
+        return $configs;
+    }
+
+    /**
+     * 商城混合结算:统一入口调用 calcFee(预览与下单共用)
+     */
+    public static function calcFeeForMallContext($shopId, $mainId, $sendType, $itemTotalAmount, $bigNum, $customId = 0, $customInfo = [])
+    {
+        return self::calcFee($shopId, $mainId, $sendType, $itemTotalAmount, $bigNum, $customId, is_array($customInfo) ? $customInfo : []);
+    }
+
+    /**
+     * 商城结算页预览未达门槛附加运费/包装费(与 create-mix-order 内 calcFee 同源)
+     * @return array
+     */
+    public static function previewMixUnMeetFee($shopId, $mainId, $sendType, $itemTotalAmount, $bigNum, $customId = 0, $customInfo = [])
+    {
+        $costs = self::calcFeeForMallContext($shopId, $mainId, $sendType, $itemTotalAmount, $bigNum, $customId, $customInfo);
+        $style = (int)$sendType;
+        $config = self::getConfig($shopId, $mainId, $style);
+        if (empty($config)) {
+            return [
+                'sendCost' => 0,
+                'packCost' => 0,
+                'unMeetSendCost' => 0,
+                'packingFee' => 0,
+                'belowMinimum' => false,
+                'canOrder' => true,
+                'tip' => '',
+            ];
+        }
+        $config = self::mergeCustomHomeRule($config, $style, $customInfo);
+        $belowMinimum = self::isBelowMinimum($itemTotalAmount, $bigNum, $config);
+        $tip = '';
+        $canOrder = true;
+
+        if ($belowMinimum) {
+            $unMeet = (int)($config['unMeet'] ?? 0);
+            $unMeetFee = (float)($config['unMeetFee'] ?? 0);
+            $conditionText = self::buildUnMeetConditionText($config);
+            if ($unMeet === 1) {
+                $canOrder = false;
+                $tip = $conditionText !== '' ? "订单不满{$conditionText},不能下单" : '未达到最低消费,不能下单';
+            } elseif (($unMeet === 0 || $unMeet === 2) && $unMeetFee > 0) {
+                $locked = ($unMeet === 0 && self::hasSendFeeDecisionToday($customId, $style))
+                    || ($unMeet === 2 && self::hasPackFeeDecisionToday($customId, $style));
+                if (!$locked) {
+                    $feeLabel = $unMeet === 2 ? '包装费' : '运费';
+                    if ($conditionText !== '') {
+                        $tip = "订单不满{$conditionText},加{$feeLabel}{$unMeetFee}元";
+                    }
+                }
+            }
+        }
+
+        $checkErr = self::checkLimit(
+            $shopId,
+            $mainId,
+            $sendType,
+            $itemTotalAmount,
+            $bigNum,
+            $costs['packCost'],
+            $costs['sendCost'],
+            $customId,
+            $customInfo
+        );
+        if ($checkErr !== '') {
+            $canOrder = false;
+            $tip = $checkErr;
+        }
+
+        return [
+            'sendCost' => (float)$costs['sendCost'],
+            'packCost' => (float)$costs['packCost'],
+            'unMeetSendCost' => (float)$costs['sendCost'],
+            'packingFee' => (float)$costs['packCost'],
+            'belowMinimum' => $belowMinimum,
+            'canOrder' => $canOrder,
+            'tip' => $tip,
+        ];
+    }
+
+    /**
+     * 混合结算页:一次预览各购买方式未达门槛附加费(不含 style=2 跑腿距离运费)
+     * @return array 键为 style 字符串,如 ['0' => [...], '1' => [...]]
+     */
+    public static function previewMixUnMeetFeeBatch($shopId, $mainId, $itemTotalAmount, $bigNum, $customId = 0, $hd = null, $custom = null)
+    {
+        $customInfo = self::buildCustomInfo($hd, $custom);
+        $fees = [];
+        for ($style = 0; $style <= 4; $style++) {
+            $fees[(string)$style] = self::previewMixUnMeetFee(
+                $shopId,
+                $mainId,
+                $style,
+                $itemTotalAmount,
+                $bigNum,
+                (int)$customId,
+                $customInfo
+            );
+        }
+        return $fees;
+    }
+
     /**
      * 计算未达最低消费时应写入订单的附加运费/包装费
      * @return array ['sendCost' => 附加运费, 'packCost' => 附加包装费]

+ 140 - 0
biz-hd/shop/classes/MainWalletRechargeClass.php

@@ -0,0 +1,140 @@
+<?php
+/**
+ * 中央钱包微信充值业务类
+ * 谁用:MainWalletController 建单、payUtil 支付回调
+ * 解决:未支付单落库;回调后增加 walletBalance 并写 xhMainWalletChange
+ */
+namespace bizHd\shop\classes;
+
+use bizHd\base\classes\BaseClass;
+use common\components\dict;
+use common\components\noticeUtil;
+use common\components\orderSn;
+use common\components\util;
+use Yii;
+
+class MainWalletRechargeClass extends BaseClass
+{
+
+    public static $baseFile = '\bizHd\shop\models\MainWalletRecharge';
+
+    /** 单笔充值上限(元) */
+    const MAX_AMOUNT = 50000;
+
+    /**
+     * 创建未支付充值单
+     * @param int $mainId
+     * @param int $shopId
+     * @param float $amount
+     * @return object
+     */
+    public static function createPending($mainId, $shopId, $amount)
+    {
+        $mainId = (int) $mainId;
+        $shopId = (int) $shopId;
+        $amount = round((float) $amount, 2);
+        if ($mainId <= 0) {
+            util::fail('账户无效');
+        }
+        if ($amount <= 0) {
+            util::fail('请输入充值金额');
+        }
+        if ($amount > self::MAX_AMOUNT) {
+            util::fail('单笔充值不能超过' . self::MAX_AMOUNT . '元');
+        }
+
+        $orderSn = orderSn::getMainWalletRechargeSn($mainId);
+        $row = self::add([
+            'orderSn' => $orderSn,
+            'mainId' => $mainId,
+            'shopId' => $shopId,
+            'amount' => $amount,
+            'payStatus' => 0,
+            'payWay' => 0,
+            'returnCode' => '',
+            'remark' => '',
+        ], true);
+        if (empty($row) || empty($row->id)) {
+            util::fail('创建充值单失败');
+        }
+        return $row;
+    }
+
+    /**
+     * 微信支付回调入账
+     * @param int $payWay
+     * @param string $orderSn
+     * @param float $totalFee 回调实付金额(元)
+     * @param string $attach
+     * @param string $transactionId
+     * @return bool|object
+     */
+    public static function thirdPay($payWay, $orderSn, $totalFee, $attach, $transactionId = '')
+    {
+        $recharge = self::getByCondition(['orderSn' => $orderSn], true);
+        if (empty($recharge)) {
+            noticeUtil::push('中央钱包充值回调未找到订单,orderSn:' . $orderSn, '15280215347');
+            return false;
+        }
+        // 金额必须与建单一致,避免短款/长款误入账
+        if (bccomp((string) $totalFee, (string) $recharge->amount, 2) !== 0) {
+            noticeUtil::push(
+                "中央钱包充值金额不一致 {$totalFee} {$recharge->amount},orderSn:{$orderSn}",
+                '15280215347'
+            );
+            return false;
+        }
+        if ((int) $recharge->payStatus === 1) {
+            Yii::info('中央钱包充值已支付,忽略重复回调 orderSn:' . $orderSn);
+            return false;
+        }
+
+        $id = (int) $recharge->id;
+        $recharge = self::getLockById($id);
+        if (empty($recharge)) {
+            noticeUtil::push('中央钱包充值锁单失败,orderSn:' . $orderSn, '15280215347');
+            return false;
+        }
+        if ((int) $recharge->payStatus === 1) {
+            return false;
+        }
+
+        $mainId = (int) $recharge->mainId;
+        $amount = round((float) $recharge->amount, 2);
+        $main = MainClass::getLockById($mainId);
+        if (empty($main)) {
+            util::fail('中央账户不存在');
+        }
+
+        $beforeWallet = round((float) ($main->walletBalance ?? 0), 2);
+        $afterWallet = round($beforeWallet + $amount, 2);
+        MainClass::updateById($mainId, ['walletBalance' => $afterWallet]);
+
+        $capitalType = dict::getDict('capitalType', 'mainWalletRecharge', 'id');
+        $ptStyle = dict::getDict('ptStyle', 'hd');
+        $fromType = dict::getDict('fromType', 'shop');
+        $ioIncome = dict::getDict('io', 'income');
+
+        MainWalletChangeClass::add([
+            'relateId' => $id,
+            'ptStyle' => $ptStyle,
+            'capitalType' => $capitalType,
+            'amount' => $amount,
+            'balance' => $afterWallet,
+            'io' => $ioIncome,
+            'payWay' => $payWay,
+            'fromType' => $fromType,
+            'event' => '微信充值' . $orderSn,
+            'mainId' => $mainId,
+            'remark' => '中央钱包微信充值',
+        ]);
+
+        $recharge->payStatus = 1;
+        $recharge->payWay = $payWay;
+        $recharge->returnCode = (string) $transactionId;
+        $recharge->save(false);
+
+        return $recharge;
+    }
+
+}

+ 18 - 0
biz-hd/shop/models/MainWalletRecharge.php

@@ -0,0 +1,18 @@
+<?php
+/**
+ * 中央钱包微信充值订单 xhMainWalletRecharge
+ * 用途:hdApp 可用余额微信充值待支付/已支付单据
+ */
+namespace bizHd\shop\models;
+
+use bizHd\base\models\Base;
+
+class MainWalletRecharge extends Base
+{
+
+    public static function tableName()
+    {
+        return 'xhMainWalletRecharge';
+    }
+
+}

+ 0 - 1
common/components/delivery/util/DeliveryQuoteUtil.php

@@ -135,7 +135,6 @@ class DeliveryQuoteUtil
             }
         } else {
             //新免费规则走这里,欧阳写到这里有问题沟通 ssh 20260730
-
             $freightType = $params['order']['freightType'] ?? 0; //运费计算方式,0 按距离计算 1 免运费,包运费
             $property = $order['goodsType'] ?? 1;//订单是什么类型订单 0 花束订单 1 花材订单,多种商品,只要含一把花束,就是花束订单
             $totalPrice = $params['order']['itemTotalAmount'] ?? 0;

+ 2 - 0
common/components/dict.php

@@ -515,6 +515,8 @@ class dict
             'hdRegisterOrder' => ['id' => 84, 'name' => 'hdRegisterOrder'],
             // 邀请佣金存入中央钱包(xhMainInviteFlow + xhMainWalletChange)
             'inviteCommissionDeposit' => ['id' => 85, 'name' => 'inviteCommissionDeposit'],
+            // 中央钱包微信充值(xhMainWalletRecharge + xhMainWalletChange)
+            'mainWalletRecharge' => ['id' => 86, 'name' => 'mainWalletRecharge'],
         ],
         "capitalTypeList" => [//流水类型的对应链接,后台收支明细查看时跳转的链接
             0 => ['link' => '/capital/order-detail', 'name' => '网店', 'orderLink' => '/order/detail', 'id' => 0,],

+ 19 - 0
common/components/orderSn.php

@@ -642,4 +642,23 @@ class orderSn
         return $prefix . date('ymdHis') . $applyId;
     }
 
+    /**
+     * 中央钱包微信充值单号
+     * @param int $mainId
+     * @return string
+     */
+    public static function getMainWalletRechargeSn($mainId)
+    {
+        $prefix = 'MWR_CS';
+        if (getenv('YII_ENV', 'local') == 'production') {
+            $prefix = 'MWR';
+        }
+        $mainId = (int) $mainId;
+        if ($mainId <= 0) {
+            util::fail('中央 id 无效');
+        }
+        // 秒级时间 + mainId + 随机数,保证唯一且无需独立号段表
+        return $prefix . date('ymdHis') . $mainId . mt_rand(10, 99);
+    }
+
 }

+ 5 - 0
common/components/payUtil.php

@@ -12,6 +12,7 @@ use bizHd\purchase\classes\PurchaseClearClass;
 use bizHd\purchase\services\PurchaseService;
 use bizHd\px\services\PxApplyService;
 use bizHd\recharge\classes\RechargeClass;
+use bizHd\shop\classes\MainWalletRechargeClass;
 use Yii;
 
 class payUtil
@@ -66,6 +67,10 @@ class payUtil
                 //客户向供货商充值销账
                 CustomRechargeClass::thirdPay($payWay, $orderSn, $totalFee, $attach, $transactionId);
                 break;
+            case dict::getDict('capitalType', 'mainWalletRecharge', 'id'):
+                // 中央钱包微信充值
+                MainWalletRechargeClass::thirdPay($payWay, $orderSn, $totalFee, $attach, $transactionId);
+                break;
             default:
         }
     }