Selaa lähdekoodia

Merge branch 'redesign‌-260706' of git.huaml.com:zhh/huahuibao into redesign‌-260706

shizhongqi 5 päivää sitten
vanhempi
commit
669fe04ba3

+ 1 - 1
app-ghs/controllers/ConsoleController.php

@@ -52,7 +52,7 @@ class ConsoleController extends BaseController
         util::success(['overview' => $overview]);
     }
 
-    //系统收入、客服收入、欠款
+    //系统收入、客服收入、欠款
     public function actionStrokeCount()
     {
 

+ 118 - 0
app-ghs/controllers/MainInviteAdminController.php

@@ -0,0 +1,118 @@
+<?php
+/**
+ * ghs Web 门店邀请分佣管理
+ * 基础分佣只读 dict;批发商独立比例与佣金明细分页/详情
+ */
+namespace ghs\controllers;
+
+use bizHd\shop\classes\MainInviteAdminClass;
+use common\components\util;
+use Yii;
+
+class MainInviteAdminController extends BaseController
+{
+
+    public $guestAccess = [];
+
+    /**
+     * 基础分佣设置(只读 hdRegisterConfig)
+     */
+    public function actionRegisterConfig()
+    {
+        util::success(MainInviteAdminClass::getRegisterConfigForAdmin());
+    }
+
+    /**
+     * 批发商独立比例列表
+     */
+    public function actionWholesalerRatioList()
+    {
+        $keyword = Yii::$app->request->get('keyword', '');
+        util::success(MainInviteAdminClass::getWholesalerRatioList($keyword));
+    }
+
+    /**
+     * 批发商搜索(新增比例下拉)
+     */
+    public function actionWholesalerSearch()
+    {
+        $keyword = Yii::$app->request->get('keyword', '');
+        $list = MainInviteAdminClass::searchWholesaler($keyword, 30);
+        util::success(['list' => $list]);
+    }
+
+    /**
+     * 保存批发商独立比例
+     */
+    public function actionSaveWholesalerRatio()
+    {
+        $post = Yii::$app->request->post();
+        $mainId = (int) ($post['mainId'] ?? 0);
+        $price = MainInviteAdminClass::getRegisterConfigForAdmin()['price'];
+
+        // 支持传固定金额或等效比例
+        if (isset($post['commissionRatio']) && $post['commissionRatio'] !== '') {
+            $commissionAmount = MainInviteAdminClass::ratioToAmount($post['commissionRatio'], $price);
+        } else {
+            $commissionAmount = (float) ($post['commissionAmount'] ?? 0);
+        }
+        if (isset($post['discountRatio']) && $post['discountRatio'] !== '') {
+            $hdDiscountAmount = MainInviteAdminClass::ratioToAmount($post['discountRatio'], $price);
+        } else {
+            $hdDiscountAmount = (float) ($post['hdDiscountAmount'] ?? 0);
+        }
+
+        try {
+            $row = MainInviteAdminClass::saveWholesalerRatio($mainId, $commissionAmount, $hdDiscountAmount);
+            util::success($row);
+        } catch (\Throwable $e) {
+            util::fail($e->getMessage());
+        }
+    }
+
+    /**
+     * 佣金汇总卡片
+     */
+    public function actionCommissionSummary()
+    {
+        $filters = Yii::$app->request->get();
+        util::success(MainInviteAdminClass::getCommissionSummary($filters));
+    }
+
+    /**
+     * 佣金分成明细分页
+     */
+    public function actionCommissionList()
+    {
+        $get = Yii::$app->request->get();
+        $export = (int) ($get['export'] ?? 0);
+        $filters = [
+            'searchTime' => $get['searchTime'] ?? '',
+            'startTime' => $get['startTime'] ?? '',
+            'endTime' => $get['endTime'] ?? '',
+            'inviterKeyword' => $get['inviterKeyword'] ?? ($get['inviterName'] ?? ''),
+            'inviteeKeyword' => $get['inviteeKeyword'] ?? ($get['inviteeName'] ?? ''),
+            'inviteCode' => $get['inviteCode'] ?? '',
+        ];
+        if ($export === 1) {
+            $file = MainInviteAdminClass::exportCommissionList($filters);
+            util::success($file);
+            return;
+        }
+        util::success(MainInviteAdminClass::getCommissionAdminList($filters));
+    }
+
+    /**
+     * 佣金分成详情
+     */
+    public function actionCommissionDetail()
+    {
+        $id = (int) Yii::$app->request->get('id', 0);
+        try {
+            util::success(MainInviteAdminClass::getCommissionDetail($id));
+        } catch (\Throwable $e) {
+            util::fail($e->getMessage());
+        }
+    }
+
+}

+ 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);
+    }
+
 }

+ 5 - 1
app-hd/controllers/ShopController.php

@@ -120,7 +120,7 @@ class ShopController extends BaseController
     {
         $get = Yii::$app->request->get();
         $mobile = $get['mobile'] ?? '';
-        if(empty($mobile)){
+        if (empty($mobile)) {
             util::success(['has' => 0]);
         }
         $shop = ShopClass::getByCondition(['mobile' => $mobile], true);
@@ -136,6 +136,10 @@ class ShopController extends BaseController
         $shopExt = ShopExtClass::getByCondition(['shopId' => $this->shop->id], false, false, 'reachVip');
         $shop = $this->shop->attributes;
         $shop['reachVip'] = $shopExt['reachVip'];
+
+        $shop['warning'] = '';
+        //$shop['warning'] = '明天凌晨1:00~2:00系统升级,暂停使用';
+
         util::success($shop);
     }
 

+ 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);
-    }
-
 }

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

@@ -6,7 +6,11 @@ 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\order\classes\PsMethodClass;
+use bizHd\refund\classes\MallRefundApplyClass;
 use bizHd\hb\classes\HbClass;
 use bizHd\hb\classes\HbScopeClass;
 use bizHd\homePageConfig\classes\HomePageModuleClass;
@@ -1286,11 +1290,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,
@@ -1329,20 +1330,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'],
@@ -1441,7 +1439,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]);
             }
 
@@ -1552,7 +1550,6 @@ class OrderController extends BaseController
                     if ($order->fromType == 2) {
                         //来自商城的新订单微信通知
                         WxMessageClass::hdNewOrderInform($shop, $order);
-                        \bizHd\order\classes\PsMethodClass::markUnMeetFeePaidFromOrder($order);
                     }
                 }
             }
@@ -1924,9 +1921,67 @@ 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;
+        // 配送方式名称:xhPsMethod.name(style = 订单 sendType)
+        $detail['psMethodName'] = $this->resolvePsMethodName($detail);
         util::success($detail);
     }
 
+    /**
+     * 解析订单配送方式名称(xhPsMethod.name,无记录时回退字典默认名)
+     * @param array $order 订单详情
+     * @return string
+     */
+    protected function resolvePsMethodName($order)
+    {
+        $mainId = (int)($order['mainId'] ?? 0);
+        $style = (int)($order['sendType'] ?? 0);
+        if ($mainId > 0) {
+            $psMethod = PsMethodClass::getByCondition([
+                'mainId' => $mainId,
+                'style' => $style,
+            ]);
+            if (!empty($psMethod['name'])) {
+                return (string)$psMethod['name'];
+            }
+        }
+        $default = dict::getDict('shMethod', $style);
+        return (string)($default['name'] ?? '');
+    }
+
+    /**
+     * 订单详情是否可申请售后(与 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;

+ 51 - 27
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);
@@ -233,36 +235,34 @@ class DistributionCommissionClass
 
     /**
      * 统一计佣:商品实付、范围、比例、落库快照
+     * 计佣基数 = xhOrder.goodsPrice - xhOrder.tkPrice(不再拆 OrderGoods/OrderItem)
      */
     protected static function computeCommissionSnapshot(array $context)
     {
         $order = $context['order'];
         $rule = $context['rule'];
         $shopId = $context['shopId'];
-        $mainId = $context['mainId'];
         $buyerId = $context['buyerId'];
         $distId = $context['distId'];
         $bindTime = $context['bindTime'];
         $orderTime = $context['orderTime'];
 
         $bindDays = self::calcBindDaysAt($bindTime, $orderTime);
-        $payAmount = round((float)($order->actPrice ?? 0), 2);
-        $sendCost = round((float)($order->sendCost ?? 0), 2);
-        $goodsPayAmount = self::resolveGoodsPayAmount($order);
-        $orderAmount = $goodsPayAmount;
-
-        $commissionBase = self::adjustCommissionBaseByScope(
-            $rule,
-            $order->orderSn ?? '',
-            $goodsPayAmount,
-            $shopId,
-            $mainId
-        );
-        // 订单有累计退款时,计佣基数扣减 tkPrice(与 xhOrder.tkPrice 一致)
-        $commissionBase = self::applyRefundToCommissionBase($commissionBase, $order);
+        $payAmount = (float)self::bcMoney($order->actPrice ?? 0);
+        $sendCost = (float)self::bcMoney($order->sendCost ?? 0);
+
+        // 计佣基数:直接用商品金额减累计退款,避免按行明细/优惠比例二次折算导致误差
+        $goodsPrice = self::bcMoney($order->goodsPrice ?? 0);
+        $tkPrice = self::bcMoney($order->tkPrice ?? 0);
+        $commissionBase = self::bcMoney(bcsub($goodsPrice, $tkPrice, 2));
+        if (bccomp($commissionBase, '0', 2) < 0) {
+            $commissionBase = '0.00';
+        }
+        $orderAmount = (float)$goodsPrice;
+        $goodsPayAmount = (float)$commissionBase;
 
         $calc = self::resolveCommissionRate($rule, $bindDays);
-        $commissionRate = $calc['rate'];
+        $commissionRate = self::bcMoney($calc['rate'] ?? 0);
         $invalidReason = $calc['invalidReason'];
         $settleStatus = 0;
         $commissionAmount = 0;
@@ -277,9 +277,16 @@ class DistributionCommissionClass
 
         if ($invalidReason !== '') {
             $settleStatus = 2;
-        } elseif ($commissionBase > 0 && $commissionRate > 0) {
-            $commissionAmount = round($commissionBase * $commissionRate / 100, 2);
+        } elseif (bccomp($commissionBase, '0', 2) > 0 && bccomp($commissionRate, '0', 2) > 0) {
+            // 佣金 = 基数 × 比例 / 100,全程 bcmath,保留两位小数
+            $commissionAmount = (float)bcmul(
+                $commissionBase,
+                bcdiv($commissionRate, '100', 6),
+                2
+            );
         }
+        $commissionBase = (float)$commissionBase;
+        $commissionRate = (float)$commissionRate;
         // 完成不等于已结算:须 finishTime + settleDays 到期后才 settleStatus=1
         if ($settleStatus !== 2 && $commissionAmount > 0) {
             $canSettle = false;
@@ -340,7 +347,8 @@ class DistributionCommissionClass
                 'commissionRate' => $commissionRate,
                 'commissionBase' => $commissionBase,
                 'goodsPayAmount' => $goodsPayAmount,
-                'tkPrice' => round((float)($order->tkPrice ?? 0), 2),
+                'goodsPrice' => (float)$goodsPrice,
+                'tkPrice' => (float)$tkPrice,
                 'commissionAmount' => $commissionAmount,
                 'settleStatus' => $settleStatus,
                 'invalidReason' => $invalidReason,
@@ -702,20 +710,36 @@ class DistributionCommissionClass
     }
 
     /**
-     * 计佣基数扣减订单累计退款金额
+     * 金额规范为两位小数字符串(计佣加减乘除用,避免 float 精度误差)
+     * @param mixed $amount
+     * @return string
+     */
+    protected static function bcMoney($amount)
+    {
+        if ($amount === null || $amount === '') {
+            return '0.00';
+        }
+        return bcadd((string)$amount, '0', 2);
+    }
+
+    /**
+     * 计佣基数扣减订单累计退款金额(历史方法;现已改为 goodsPrice-tkPrice 直接算)
      * @param float $commissionBase 范围调整后的基数
      * @param object $order xhOrder
      * @return float
      */
     protected static function applyRefundToCommissionBase($commissionBase, $order)
     {
-        $commissionBase = round((float)$commissionBase, 2);
-        $tkPrice = round((float)($order->tkPrice ?? 0), 2);
-        if ($tkPrice <= 0 || $commissionBase <= 0) {
-            return $commissionBase;
+        $commissionBase = self::bcMoney($commissionBase);
+        $tkPrice = self::bcMoney($order->tkPrice ?? 0);
+        if (bccomp($tkPrice, '0', 2) <= 0 || bccomp($commissionBase, '0', 2) <= 0) {
+            return (float)$commissionBase;
+        }
+        $after = bcsub($commissionBase, $tkPrice, 2);
+        if (bccomp($after, '0', 2) < 0) {
+            return 0.0;
         }
-        $after = bcsub((string)$commissionBase, (string)$tkPrice, 2);
-        return round(max(0, (float)$after), 2);
+        return (float)$after;
     }
 
     protected static function resolveCommissionRate($rule, $bindDays)

+ 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' => 附加包装费]

+ 564 - 0
biz-hd/shop/classes/MainInviteAdminClass.php

@@ -0,0 +1,564 @@
+<?php
+/**
+ * 门店邀请分佣 — 管理后台业务
+ * 用途:ghs Web 分成比例管理、佣金分成明细;全局配置只读 dict,批发商独立比例写 xhMainInvite
+ */
+
+namespace bizHd\shop\classes;
+
+use biz\shop\classes\ShopClass;
+use bizHd\base\classes\BaseClass;
+use biz\renew\classes\RenewClass;
+use common\components\dateUtil;
+use common\components\dict;
+use common\components\util;
+use Yii;
+
+class MainInviteAdminClass extends BaseClass
+{
+
+    /**
+     * 管理端只读:hdRegisterConfig + 等效比例
+     */
+    public static function getRegisterConfigForAdmin()
+    {
+        $config = dict::getDict('hdRegisterConfig');
+        $price = round((float) ($config['price'] ?? 0), 2);
+        $commissionAmount = round((float) ($config['commissionAmount'] ?? 0), 2);
+        $hdDiscountAmount = round((float) ($config['hdDiscountAmount'] ?? 0), 2);
+
+        return [
+            'price' => $price,
+            'commissionAmount' => $commissionAmount,
+            'hdDiscountAmount' => $hdDiscountAmount,
+            'commissionRatio' => self::calcRatio($commissionAmount, $price),
+            'discountRatio' => self::calcRatio($hdDiscountAmount, $price),
+            'enabled' => $commissionAmount > 0 ? 1 : 0,
+            'commissionBaseLabel' => '会员实际支付金额',
+        ];
+    }
+
+    /**
+     * 金额反算等效分成比例(%)
+     */
+    public static function calcRatio($amount, $price)
+    {
+        $price = round((float) $price, 2);
+        $amount = round((float) $amount, 2);
+        if ($price <= 0) {
+            return 0;
+        }
+        return round($amount / $price * 100, 2);
+    }
+
+    /**
+     * 比例换算为固定分佣金额
+     */
+    public static function ratioToAmount($ratio, $price)
+    {
+        $price = round((float) $price, 2);
+        $ratio = round((float) $ratio, 2);
+        if ($price <= 0 || $ratio < 0) {
+            return 0;
+        }
+        return round($price * $ratio / 100, 2);
+    }
+
+    /**
+     * 批发商独立比例分页:xhMainInvite 全表数据(仅支持关键词筛选)
+     */
+    public static function getWholesalerRatioList($keyword = '')
+    {
+        $config = self::getRegisterConfigForAdmin();
+
+        $keyword = trim((string) $keyword);
+        $get = Yii::$app->request->get();
+        $page = isset($get['page']) ? max(1, (int) $get['page']) : 1;
+        $pageSize = !empty($get['pageSize'])
+            ? (int) $get['pageSize']
+            : (int) (Yii::$app->params['pageSize'] ?? 20);
+
+        $model = MainInviteClass::getModel();
+        $query = $model->find()->orderBy('id DESC');
+
+        if ($keyword !== '') {
+            $mainIds = self::searchWholesalerMainIds($keyword);
+            if (empty($mainIds)) {
+                return self::emptyPage($page, $pageSize);
+            }
+            $query->andWhere(['mainId' => $mainIds]);
+        }
+
+        $count = (int) (clone $query)->count();
+        $rows = $query->offset(($page - 1) * $pageSize)->limit($pageSize)->asArray()->all();
+        $list = [];
+        foreach ($rows as $row) {
+            $list[] = self::formatWholesalerRatioRow($row, $config['price']);
+        }
+
+        $totalPage = $pageSize > 0 ? (int) ceil($count / $pageSize) : 0;
+        return [
+            'list' => $list,
+            'totalNum' => $count,
+            'totalPage' => $totalPage,
+            'moreData' => $page < $totalPage ? 1 : 0,
+        ];
+    }
+
+    protected static function emptyPage($page, $pageSize)
+    {
+        return [
+            'list' => [],
+            'totalNum' => 0,
+            'totalPage' => 0,
+            'moreData' => 0,
+        ];
+    }
+
+    /**
+     * 关键词查批发商 mainId 列表(xhShop ptStyle=2)
+     */
+    protected static function searchWholesalerMainIds($keyword)
+    {
+        $keyword = trim((string) $keyword);
+        $shopModel = ShopClass::getModel();
+        $query = $shopModel->find()
+            ->select(['mainId'])
+            ->where([
+                'delStatus' => 0,
+                'ptStyle' => 2,
+            ])
+            ->andWhere(['>', 'mainId', 0]);
+
+        if ($keyword !== '') {
+            $query->andWhere([
+                'or',
+                ['like', 'shopName', $keyword],
+                ['like', 'merchantName', $keyword],
+                ['like', 'mobile', $keyword],
+            ]);
+        }
+
+        $shopRows = $query->limit(200)->asArray()->all();
+        $mainIds = [];
+        foreach ($shopRows as $shop) {
+            $mid = (int) ($shop['mainId'] ?? 0);
+            if ($mid > 0) {
+                $mainIds[] = $mid;
+            }
+        }
+        return array_values(array_unique($mainIds));
+    }
+
+    /**
+     * 批发商下拉搜索:xhShop 表 ptStyle=2(供货商)
+     */
+    public static function searchWholesaler($keyword = '', $limit = 20)
+    {
+        $keyword = trim((string) $keyword);
+        $shopModel = ShopClass::getModel();
+        $query = $shopModel->find()
+            ->select(['mainId', 'shopName', 'merchantName', 'mobile'])
+            ->where([
+                'delStatus' => 0,
+                'ptStyle' => 2,
+            ])
+            ->andWhere(['>', 'mainId', 0])
+            ->orderBy('id DESC')
+            ->limit((int) $limit);
+
+        if ($keyword !== '') {
+            $query->andWhere([
+                'or',
+                ['like', 'shopName', $keyword],
+                ['like', 'merchantName', $keyword],
+                ['like', 'mobile', $keyword],
+            ]);
+        }
+
+        $shopRows = $query->asArray()->all();
+        $list = [];
+        $seenMainIds = [];
+        foreach ($shopRows as $shop) {
+            $mainId = (int) ($shop['mainId'] ?? 0);
+            if ($mainId <= 0 || isset($seenMainIds[$mainId])) {
+                continue;
+            }
+            $seenMainIds[$mainId] = true;
+            $list[] = [
+                'mainId' => $mainId,
+                'shopName' => ShopClass::formatDisplayShopName($shop),
+                'mobile' => trim((string) ($shop['mobile'] ?? '')),
+            ];
+        }
+        return $list;
+    }
+
+    /**
+     * sjId 取首店 mainId
+     */
+    public static function resolveMainIdBySjId($sjId)
+    {
+        $sjId = (int) $sjId;
+        if ($sjId <= 0) {
+            return 0;
+        }
+        $shop = ShopClass::getByCondition(['sjId' => $sjId, 'delStatus' => 0], false, 'id ASC');
+        return (int) ($shop['mainId'] ?? 0);
+    }
+
+    protected static function resolveShopByMainId($mainId)
+    {
+        $mainId = (int) $mainId;
+        if ($mainId <= 0) {
+            return [];
+        }
+        $shop = ShopClass::getByCondition(['mainId' => $mainId, 'delStatus' => 0], false, 'id ASC');
+        return is_array($shop) ? $shop : [];
+    }
+
+    /**
+     * 按 mainId + ptStyle 取 xhShop 首店
+     */
+    protected static function resolveShopByMainIdPtStyle($mainId, $ptStyle)
+    {
+        $mainId = (int) $mainId;
+        $ptStyle = (int) $ptStyle;
+        if ($mainId <= 0 || $ptStyle <= 0) {
+            return [];
+        }
+        $shop = ShopClass::getByCondition([
+            'mainId' => $mainId,
+            'ptStyle' => $ptStyle,
+            'delStatus' => 0,
+        ], false, 'id ASC');
+        return is_array($shop) ? $shop : [];
+    }
+
+    protected static function formatWholesalerRatioRow($row, $price)
+    {
+        $mainId = (int) ($row['mainId'] ?? 0);
+        $shop = self::resolveShopByMainId($mainId);
+        $mobile = trim((string) ($shop['mobile'] ?? ''));
+        $commissionAmount = round((float) ($row['commissionAmount'] ?? 0), 2);
+        $hdDiscountAmount = round((float) ($row['hdDiscountAmount'] ?? 0), 2);
+
+        return [
+            'id' => (int) ($row['id'] ?? 0),
+            'mainId' => $mainId,
+            'shopName' => ShopClass::formatDisplayShopName($shop),
+            // 管理端列表展示完整手机号,不做脱敏
+            'mobile' => $mobile !== '' ? $mobile : '-',
+            'inviteCode' => trim((string) ($row['inviteCode'] ?? '')),
+            'commissionAmount' => $commissionAmount,
+            'hdDiscountAmount' => $hdDiscountAmount,
+            'commissionRatio' => self::calcRatio($commissionAmount, $price),
+            'discountRatio' => self::calcRatio($hdDiscountAmount, $price),
+            'effectiveTime' => $row['addTime'] ?? '',
+            'updateTime' => $row['updateTime'] ?? '',
+        ];
+    }
+
+    /**
+     * 保存批发商独立比例
+     */
+    public static function saveWholesalerRatio($mainId, $commissionAmount, $hdDiscountAmount)
+    {
+        $mainId = (int) $mainId;
+        if ($mainId <= 0) {
+            throw new \Exception('请选择批发商');
+        }
+        $commissionAmount = round((float) $commissionAmount, 2);
+        $hdDiscountAmount = round((float) $hdDiscountAmount, 2);
+        if ($commissionAmount <= 0 || $hdDiscountAmount <= 0) {
+            throw new \Exception('分佣金额与优惠金额须大于 0');
+        }
+        MainInviteClass::ensureByMainId($mainId);
+        MainInviteClass::updateByCondition(['mainId' => $mainId], [
+            'commissionAmount' => $commissionAmount,
+            'hdDiscountAmount' => $hdDiscountAmount,
+        ]);
+        $row = MainInviteClass::getByMainId($mainId);
+        $config = self::getRegisterConfigForAdmin();
+        return self::formatWholesalerRatioRow($row, $config['price']);
+    }
+
+    /**
+     * 佣金汇总卡片
+     */
+    public static function getCommissionSummary($where = [])
+    {
+        $baseWhere = self::buildCommissionAdminWhere($where);
+
+        $model = MainInviteCommissionClass::getModel();
+        $query = $model->conditionQuery($baseWhere);
+        $totalGenerated = (float) $query->sum('commissionAmount');
+
+        $settledQuery = $model->conditionQuery($baseWhere);
+        $settledAmount = (float) $settledQuery->andWhere(['settleStatus' => 1])->sum('commissionAmount');
+
+        $countQuery = $model->conditionQuery($baseWhere);
+        $inviterCount = (int) $countQuery->select('inviterMainId')->distinct()->count();
+
+        return [
+            'totalGenerated' => round($totalGenerated, 2),
+            'settledAmount' => round($settledAmount, 2),
+            'inviterCount' => $inviterCount,
+        ];
+    }
+
+    /**
+     * 管理端佣金明细分页
+     */
+    public static function getCommissionAdminList($filters = [])
+    {
+        $where = self::buildCommissionAdminWhere($filters);
+        $result = MainInviteCommissionClass::getList('*', $where, 'addTime DESC,id DESC');
+        $rows = $result['list'] ?? [];
+        $list = [];
+        $pageTotal = 0;
+        foreach ($rows as $row) {
+            $item = self::formatCommissionAdminRow($row);
+            $list[] = $item;
+            $pageTotal += (float) ($item['commissionAmount'] ?? 0);
+        }
+        $result['list'] = $list;
+        $result['pageCommissionTotal'] = round($pageTotal, 2);
+        return $result;
+    }
+
+    protected static function buildCommissionAdminWhere($filters)
+    {
+        // 管理端明细分页:仅展示已关联批发门店(pfShopId>0)的获佣记录
+        $where = [
+            'inviterMainId>' => 0,
+            'inviteeMainId>' => 0,
+            'pfShopId>' => 0,
+        ];
+
+        $inviterKeyword = trim((string) ($filters['inviterKeyword'] ?? ''));
+        $inviteeKeyword = trim((string) ($filters['inviteeKeyword'] ?? ''));
+        $inviteCode = trim((string) ($filters['inviteCode'] ?? ''));
+
+        if ($inviteCode !== '') {
+            $where['inviteCode'] = ['like', $inviteCode];
+        }
+        if ($inviterKeyword !== '') {
+            $where['pfShopName'] = ['like', $inviterKeyword];
+        }
+        if ($inviteeKeyword !== '') {
+            $where['inviteeShopName'] = ['like', $inviteeKeyword];
+        }
+
+        $searchTime = trim((string) ($filters['searchTime'] ?? ''));
+        $startTime = trim((string) ($filters['startTime'] ?? ''));
+        $endTime = trim((string) ($filters['endTime'] ?? ''));
+        if ($searchTime !== '' && $searchTime !== 'all') {
+            $period = dateUtil::formatTime($searchTime, $startTime, $endTime);
+            if (!empty($period['startTime']) && !empty($period['endTime'])) {
+                $where['addTime'] = ['between', [$period['startTime'], $period['endTime']]];
+            }
+        }
+
+        return $where;
+    }
+
+    protected static function formatCommissionAdminRow($row)
+    {
+        $memberPay = round((float) ($row['memberPayAmount'] ?? 0), 2);
+        if ($memberPay <= 0) {
+            $memberPay = round((float) ($row['amount'] ?? 0), 2);
+        }
+        $commissionAmount = round((float) ($row['commissionAmount'] ?? 0), 2);
+        $inviterMainId = (int) ($row['inviterMainId'] ?? 0);
+        // 列表获佣人展示批发门店快照名 pfShopName
+        $inviterName = trim((string) ($row['pfShopName'] ?? ''));
+        if ($inviterName === '' || $inviterName === '0') {
+            $inviterName = trim((string) ($row['inviterShopName'] ?? ''));
+        }
+
+        $settleStatus = (int) ($row['settleStatus'] ?? 0);
+        return [
+            'id' => (int) ($row['id'] ?? 0),
+            'commissionSn' => $row['commissionSn'] ?? '',
+            'inviterMainId' => $inviterMainId,
+            'inviterShopName' => $inviterName !== '' ? $inviterName : '-',
+            'inviteeShopName' => trim((string) ($row['inviteeShopName'] ?? '')) ?: '-',
+            'inviteCode' => MainInviteCommissionClass::maskInviteCode($row['inviteCode'] ?? ''),
+            'memberTypeName' => trim((string) ($row['memberTypeName'] ?? '')) ?: '年度会员',
+            'memberPayAmount' => $memberPay,
+            'commissionRatio' => self::calcRatio($commissionAmount, $memberPay),
+            'commissionAmount' => $commissionAmount,
+            'hdDiscountAmount' => round((float) ($row['hdDiscountAmount'] ?? 0), 2),
+            'settleStatus' => $settleStatus,
+            'settleStatusText' => self::settleStatusText($settleStatus),
+            'addTime' => $row['addTime'] ?? '',
+            'settleTime' => $row['settleTime'] ?? '',
+        ];
+    }
+
+    protected static function settleStatusText($status)
+    {
+        if ((int) $status === 1) {
+            return '已结算';
+        }
+        if ((int) $status === 2) {
+            return '已失效';
+        }
+        return '待结算';
+    }
+
+    /**
+     * 佣金分成详情(管理端)
+     */
+    public static function getCommissionDetail($id)
+    {
+        $id = (int) $id;
+        if ($id <= 0) {
+            throw new \Exception('参数无效');
+        }
+        $row = MainInviteCommissionClass::getById($id);
+        if (empty($row)) {
+            throw new \Exception('记录不存在');
+        }
+
+        $memberPay = round((float) ($row['memberPayAmount'] ?? 0), 2);
+        if ($memberPay <= 0) {
+            $memberPay = round((float) ($row['amount'] ?? 0), 2);
+        }
+        $prePrice = round((float) ($row['amount'] ?? 0), 2);
+        $commissionAmount = round((float) ($row['commissionAmount'] ?? 0), 2);
+        $settleStatus = (int) ($row['settleStatus'] ?? 0);
+
+        $inviterMainId = (int) ($row['inviterMainId'] ?? 0);
+        $inviteeMainId = (int) ($row['inviteeMainId'] ?? 0);
+        $pfShopId = (int) ($row['pfShopId'] ?? 0);
+        $lsShopId = (int) ($row['lsShopId'] ?? 0);
+        $inviterShop = $pfShopId > 0
+            ? (ShopClass::getById($pfShopId, false) ?: [])
+            : self::resolveShopByMainIdPtStyle($inviterMainId, 2);
+        $inviteeShop = $lsShopId > 0
+            ? (ShopClass::getById($lsShopId, false) ?: [])
+            : self::resolveShopByMainIdPtStyle($inviteeMainId, 1);
+        $inviterInvite = MainInviteClass::getByMainId($inviterMainId);
+
+        $inviterDisplayName = trim((string) ($row['inviterShopName'] ?? ''));
+        if ($inviterDisplayName === '' && !empty($inviterShop)) {
+            $inviterDisplayName = ShopClass::formatDisplayShopName($inviterShop);
+        }
+        $inviteeDisplayName = trim((string) ($row['inviteeShopName'] ?? ''));
+        if ($inviteeDisplayName === '' && !empty($inviteeShop)) {
+            $inviteeDisplayName = ShopClass::formatDisplayShopName($inviteeShop);
+        }
+
+        $renew = self::resolveMemberRenew($inviteeMainId, $row['payTime'] ?? '');
+
+        return [
+            'commission' => [
+                'commissionSn' => $row['commissionSn'] ?? '',
+                'addTime' => $row['addTime'] ?? '',
+                'settleStatus' => $settleStatus,
+                'settleStatusText' => self::settleStatusText($settleStatus),
+                'settleTime' => $row['settleTime'] ?? '',
+                'commissionAmount' => $commissionAmount,
+            ],
+            'inviter' => [
+                'shopName' => $inviterDisplayName !== '' ? $inviterDisplayName : '-',
+                'inviteCode' => trim((string) ($inviterInvite['inviteCode'] ?? ($row['inviteCode'] ?? ''))) ?: '-',
+                'contactName' => trim((string) ($inviterShop['contact'] ?? '')) ?: '-',
+                'mobile' => trim((string) ($inviterShop['mobile'] ?? '')) ?: '-',
+                'creditedAmount' => $commissionAmount,
+            ],
+            'invitee' => [
+                'shopName' => $inviteeDisplayName !== '' ? $inviteeDisplayName : '-',
+                'contactName' => trim((string) ($inviteeShop['contact'] ?? '')) ?: '-',
+                'registerTime' => $inviteeShop['addTime'] ?? ($inviteeShop['createTime'] ?? ($row['addTime'] ?? '')),
+                'mobile' => trim((string) ($row['inviteeMobile'] ?? ($inviteeShop['mobile'] ?? ''))) ?: '-',
+                'inviteRelation' => $inviterDisplayName !== '' ? ($inviterDisplayName . '邀请') : '-',
+            ],
+            'memberOrder' => [
+                'orderSn' => $renew['orderSn'] ?? ('HY' . ($renew['id'] ?? '')),
+                'actPrice' => $memberPay,
+                'memberTypeName' => trim((string) ($row['memberTypeName'] ?? '')) ?: '年度会员',
+                'payTime' => $row['payTime'] ?? ($renew['payTime'] ?? ''),
+                'prePrice' => $prePrice > 0 ? $prePrice : round((float) ($renew['prePrice'] ?? 0), 2),
+                'payWayText' => self::payWayText((int) ($row['payWay'] ?? ($renew['payWay'] ?? 0))),
+                'hdDiscountAmount' => round((float) ($row['hdDiscountAmount'] ?? 0), 2),
+                'orderStatus' => 1,
+                'orderStatusText' => '已完成',
+            ],
+        ];
+    }
+
+    protected static function resolveMemberRenew($inviteeMainId, $payTime)
+    {
+        $inviteeMainId = (int) $inviteeMainId;
+        if ($inviteeMainId <= 0) {
+            return [];
+        }
+        $where = ['mainId' => $inviteeMainId, 'status' => 1];
+        $list = RenewClass::getLimitList('*', $where, 1, 'payTime DESC,id DESC');
+        return !empty($list[0]) ? $list[0] : [];
+    }
+
+    protected static function payWayText($payWay)
+    {
+        if ((int) $payWay === 1) {
+            return '支付宝';
+        }
+        if ((int) $payWay === 2) {
+            return '余额';
+        }
+        return '微信支付';
+    }
+
+    /**
+     * 导出佣金明细 CSV
+     */
+    public static function exportCommissionList($filters = [])
+    {
+        $savedGet = Yii::$app->request->get();
+        $exportGet = array_merge($savedGet, ['page' => 1, 'pageSize' => 5000, 'export' => 0]);
+        Yii::$app->request->setQueryParams($exportGet);
+
+        $result = self::getCommissionAdminList($filters);
+        $list = $result['list'] ?? [];
+        if (empty($list)) {
+            util::fail('没有数据需要导出');
+        }
+
+        $filename = 'invite_commission_' . date('YmdHis') . '.csv';
+        $dir = './priceTable/invite_admin';
+        if (!is_dir($dir)) {
+            mkdir($dir, 0777, true);
+        }
+        $path = $dir . '/' . $filename;
+        $fp = fopen($path, 'w');
+        fprintf($fp, chr(0xEF) . chr(0xBB) . chr(0xBF));
+        fputcsv($fp, ['分佣单号', '批发商', '受邀花店', '邀请码', '会员类型', '会员实付', '分成比例%', '产生佣金', '结算状态', '产生时间', '结算时间']);
+        foreach ($list as $row) {
+            fputcsv($fp, [
+                $row['commissionSn'] ?? '',
+                $row['inviterShopName'] ?? '',
+                $row['inviteeShopName'] ?? '',
+                $row['inviteCode'] ?? '',
+                $row['memberTypeName'] ?? '',
+                $row['memberPayAmount'] ?? '',
+                $row['commissionRatio'] ?? '',
+                $row['commissionAmount'] ?? '',
+                $row['settleStatusText'] ?? '',
+                $row['addTime'] ?? '',
+                $row['settleTime'] ?? '',
+            ]);
+        }
+        fclose($fp);
+
+        Yii::$app->request->setQueryParams($savedGet);
+        $host = Yii::$app->params['ghsHost'] ?? (Yii::$app->params['hdImgHost'] ?? '');
+        return [
+            'file' => rtrim($host, '/') . '/priceTable/invite_admin/' . $filename,
+            'shortFile' => $filename,
+        ];
+    }
+
+}

+ 42 - 5
biz-hd/shop/classes/MainInviteClass.php

@@ -6,6 +6,7 @@
 
 namespace bizHd\shop\classes;
 
+use biz\shop\classes\ShopClass as BizShopClass;
 use bizHd\base\classes\BaseClass;
 use bizHd\shop\classes\MainInviteCommissionClass;
 use bizHd\shop\classes\MainInviteFlowClass;
@@ -243,8 +244,13 @@ class MainInviteClass extends BaseClass
         $applyId = (int) ($apply['id'] ?? 0);
         $commissionSn = 'FY' . date('YmdHis') . $applyId;
         $flowTime = date('Y-m-d H:i:s');
-        $lsShopId = (int) ($apply['shopId'] ?? 0);
-        $lsShopName = trim((string) ($apply['name'] ?? ''));
+        // pf/ls 均按邀请人 mainId + ptStyle 取 xhShop 快照
+        $pfShop = self::resolveShopSnapshotByMainIdPtStyle($inviterMainId, 2);
+        $lsShop = self::resolveShopSnapshotByMainIdPtStyle($inviterMainId, 1);
+        $pfShopId = (int) ($pfShop['shopId'] ?? 0);
+        $pfShopName = trim((string) ($pfShop['shopName'] ?? ''));
+        $lsShopId = (int) ($lsShop['shopId'] ?? 0);
+        $lsShopName = trim((string) ($lsShop['shopName'] ?? ''));
         $inviteeShopName = trim((string) $inviteeShopName);
         $inviteeMobile = trim((string) $inviteeMobile);
         $memberPayAmount = round((float) ($actPrice ?? 0), 2);
@@ -259,10 +265,12 @@ class MainInviteClass extends BaseClass
 
             $beforeAble = round((float) ($inviteRow->ableCommission ?? 0), 2);
 
-            // 1. 邀约记录(受邀花店维度)
+            // 1. 邀约记录(受邀花店维度;pf/ls 为邀请人门店快照,发佣即已结算
             $commissionLog = MainInviteCommissionClass::add([
                 'commissionSn' => $commissionSn,
                 'inviterMainId' => $inviterMainId,
+                'pfShopId' => $pfShopId,
+                'pfShopName' => $pfShopName,
                 'lsShopId' => $lsShopId,
                 'lsShopName' => $lsShopName,
                 'inviteeMainId' => (int) $inviteeMainId,
@@ -274,7 +282,8 @@ class MainInviteClass extends BaseClass
                 'memberPayAmount' => $memberPayAmount,
                 'commissionAmount' => $commissionAmount,
                 'hdDiscountAmount' => round((float) ($apply['hdDiscountAmount'] ?? 0), 2),
-                'settleStatus' => 0,
+                'settleStatus' => 1,
+                'settleTime' => $flowTime,
                 'payTime' => $flowTime,
             ], true);
             $commissionId = (int) ($commissionLog->id ?? 0);
@@ -298,7 +307,7 @@ class MainInviteClass extends BaseClass
                 'refType' => MainInviteFlowClass::REF_TYPE_COMMISSION,
                 'refId' => $commissionId,
                 'refSn' => $commissionSn,
-                'inviteeShopName' => $inviteeShopName !== '' ? $inviteeShopName : $lsShopName,
+                'inviteeShopName' => $inviteeShopName,
                 'inviteeMobile' => $inviteeMobile,
                 'lsShopId' => $lsShopId,
                 'baseAmount' => $memberPayAmount > 0 ? $memberPayAmount : $prePriceAmount,
@@ -313,6 +322,34 @@ class MainInviteClass extends BaseClass
         }
     }
 
+    /**
+     * 按 mainId + ptStyle 取 xhShop 首店快照(id ASC,delStatus=0)
+     *
+     * @param int $mainId xhMain.id
+     * @param int $ptStyle 1零售 2批发
+     * @return array{shopId:int,shopName:string}
+     */
+    private static function resolveShopSnapshotByMainIdPtStyle($mainId, $ptStyle)
+    {
+        $mainId = (int) $mainId;
+        $ptStyle = (int) $ptStyle;
+        if ($mainId <= 0 || $ptStyle <= 0) {
+            return ['shopId' => 0, 'shopName' => ''];
+        }
+        $shop = ShopClass::getByCondition([
+            'mainId' => $mainId,
+            'ptStyle' => $ptStyle,
+            'delStatus' => 0,
+        ], false, 'id ASC');
+        if (empty($shop)) {
+            return ['shopId' => 0, 'shopName' => ''];
+        }
+        return [
+            'shopId' => (int) ($shop['id'] ?? 0),
+            'shopName' => BizShopClass::formatDisplayShopName($shop),
+        ];
+    }
+
     /**
      * 将全部可提现佣金存入 xhMain.walletBalance(全额存入,非部分)
      *

+ 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';
+    }
+
+}

+ 2 - 1
biz-mall/order/services/OrderService.php

@@ -226,7 +226,8 @@ class OrderService extends BaseService
     //获取订单信息 TODO 重新实现,要限制查询字段与返回字段,性能第一???? -- 商品项(商品规格)、商品数量
     public static function getOrderList($where)
     {
-        $fields = 'id, shopId, hdId, orderSn, groupBuyId, orderType, actPrice, goodsNum, reachDate, reachPeriod, status, addTime';
+        // customId:订单列表「联系客服」进 chatPage 所需
+        $fields = 'id, shopId, hdId, customId, orderSn, groupBuyId, orderType, actPrice, goodsNum, reachDate, reachPeriod, status, addTime';
         $data = self::getList($fields, $where, 'addTime DESC');
         if (empty($data['list'])) {
             return $data;

+ 29 - 0
biz/shop/classes/ShopClass.php

@@ -1060,4 +1060,33 @@ class ShopClass extends BaseClass
         PtYeChangeClass::addChange($change, true);
     }
 
+    /**
+     * xhShop 门店展示名称:首店仅返回 merchantName,分店返回 merchantName-分店名
+     *
+     * @param array|object|string $shop 门店记录(含 shopName、merchantName),或 shopName 字符串
+     * @param string $merchantName $shop 为 shopName 字符串时传入商家名称
+     * @return string
+     */
+    public static function formatDisplayShopName($shop, $merchantName = '')
+    {
+        if (is_array($shop)) {
+            $shopName = trim((string) ($shop['shopName'] ?? ''));
+            $merchantName = trim((string) ($shop['merchantName'] ?? ''));
+        } elseif (is_object($shop)) {
+            $shopName = trim((string) ($shop->shopName ?? ''));
+            $merchantName = trim((string) ($shop->merchantName ?? ''));
+        } else {
+            $shopName = trim((string) $shop);
+            $merchantName = trim((string) $merchantName);
+        }
+
+        if ($merchantName === '') {
+            return $shopName;
+        }
+        if ($shopName === '' || $shopName === '首店') {
+            return $merchantName;
+        }
+        return $merchantName . '-' . $shopName;
+    }
+
 }

+ 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:
         }
     }

+ 17 - 0
sql/20260706_redesign.sql

@@ -415,3 +415,20 @@ ALTER TABLE `xhRenew`
 ALTER TABLE xhShopExt
   ADD COLUMN serviceMobile varchar(20) NOT NULL DEFAULT '' COMMENT '客服电话' AFTER rechargeRemark,
   ADD COLUMN serviceWx varchar(300) NOT NULL DEFAULT '' COMMENT '客服微信二维码' AFTER serviceMobile;
+
+CREATE TABLE IF NOT EXISTS `xhMainWalletRecharge` (
+    `id` int(11) NOT NULL AUTO_INCREMENT,
+    `orderSn` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '充值单号',
+    `mainId` int(11) NOT NULL DEFAULT '0' COMMENT '中央 id',
+    `shopId` int(11) NOT NULL DEFAULT '0' COMMENT '门店 id',
+    `amount` decimal(15,2) NOT NULL DEFAULT '0.00' COMMENT '充值金额',
+    `payStatus` tinyint(4) NOT NULL DEFAULT '0' COMMENT '支付状态 0未付 1已付',
+    `payWay` tinyint(4) NOT NULL DEFAULT '0' COMMENT '支付方式',
+    `returnCode` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '微信 transaction_id',
+    `remark` varchar(500) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '备注',
+    `addTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间',
+    `updateTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
+    PRIMARY KEY (`id`) USING BTREE,
+    UNIQUE KEY `uk_orderSn` (`orderSn`) USING BTREE,
+    KEY `idx_mainId_addTime` (`mainId`, `addTime`) USING BTREE
+    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='中央钱包微信充值订单';