Эх сурвалжийг харах

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

shish 2 долоо хоног өмнө
parent
commit
3e3bb02a04

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

+ 189 - 0
app-ghs/controllers/MainInviteController.php

@@ -0,0 +1,189 @@
+<?php
+/**
+ * ghsApp 门店邀请 / 我的分佣(邀请花店开店)
+ * 数据与 hd 共用 xhMainInvite、xhMainInviteFlow 等表
+ */
+namespace ghs\controllers;
+
+use bizHd\shop\classes\MainInviteClass;
+use bizHd\shop\classes\MainInviteCommissionClass;
+use bizHd\shop\classes\MainInviteFlowClass;
+use bizHd\wx\classes\WxOpenClass;
+use common\components\dict;
+use common\components\dirUtil;
+use common\components\imgUtil;
+use common\components\miniUtil;
+use common\components\stringUtil;
+use common\components\util;
+use Yii;
+
+class MainInviteController extends BaseController
+{
+
+    public $guestAccess = [];
+
+    /**
+     * 我的分佣首页
+     */
+    public function actionIndex()
+    {
+        $mainId = (int) $this->mainId;
+        $invite = MainInviteClass::ensureByMainId($mainId);
+        $inviteData = [
+            'inviteCode' => '',
+            'inviteCodeMask' => '',
+            'totalCommission' => '0.00',
+            'ableCommission' => '0.00',
+            'commissionAmount' => '0.00',
+            'hdDiscountAmount' => '0.00',
+            'inviteCount' => 0,
+        ];
+        if (!empty($invite)) {
+            $code = $invite['inviteCode'] ?? '';
+            $inviteData = [
+                'inviteCode' => $code,
+                'inviteCodeMask' => MainInviteCommissionClass::maskInviteCode($code),
+                'totalCommission' => round((float) ($invite['totalCommission'] ?? 0), 2),
+                'ableCommission' => round((float) ($invite['ableCommission'] ?? 0), 2),
+                'commissionAmount' => round((float) ($invite['commissionAmount'] ?? 0), 2),
+                'hdDiscountAmount' => round((float) ($invite['hdDiscountAmount'] ?? 0), 2),
+                'inviteCount' => (int) ($invite['inviteCount'] ?? 0),
+            ];
+        }
+
+        $recentRaw = MainInviteCommissionClass::getRecentList($mainId, 5);
+        $recentList = [];
+        foreach ($recentRaw as $row) {
+            $recentList[] = MainInviteCommissionClass::formatRecentRow($row);
+        }
+
+        util::success([
+            'invite' => $inviteData,
+            'recentList' => $recentList,
+        ]);
+    }
+
+    /**
+     * 邀约花店列表
+     */
+    public function actionInviteShopList()
+    {
+        $get = Yii::$app->request->get();
+        $keyword = trim($get['keyword'] ?? '');
+        $data = MainInviteCommissionClass::getInviteShopList($this->mainId, $keyword);
+        util::success($data);
+    }
+
+    /**
+     * 佣金变动明细分页
+     */
+    public function actionCommissionList()
+    {
+        $list = MainInviteFlowClass::getFlowList($this->mainId);
+        util::success($list);
+    }
+
+    /**
+     * 可提现佣金全额存入中央钱包余额
+     */
+    public function actionDepositBalance()
+    {
+        $mainId = (int) $this->mainId;
+        $shopId = (int) $this->shopId;
+        $shopName = trim((string) ($this->shop->name ?? ''));
+        try {
+            $result = MainInviteClass::depositAbleCommissionToWallet($mainId, $shopId, $shopName);
+            $invite = MainInviteClass::getByMainId($mainId);
+            $inviteData = [
+                'totalCommission' => round((float) ($invite['totalCommission'] ?? 0), 2),
+                'ableCommission' => round((float) ($invite['ableCommission'] ?? 0), 2),
+            ];
+            util::success(array_merge($result, ['invite' => $inviteData]));
+        } catch (\Exception $e) {
+            util::fail($e->getMessage());
+        }
+    }
+
+    /**
+     * 生成 hdApp 邀请开店海报(参考 main/get-mall-poster:太阳码 + OSS 背景合成)
+     * GET:可选 env_version=develop|trial|release
+     */
+    public function actionGetInviteHdPoster()
+    {
+        $mainId = (int) $this->mainId;
+        $invite = MainInviteClass::ensureByMainId($mainId);
+        $inviteCode = trim((string) ($invite['inviteCode'] ?? ''));
+        if ($inviteCode === '') {
+            util::fail('暂无邀请码');
+        }
+
+        $envVersion = miniUtil::normalizeMiniEnvVersion(Yii::$app->request->get('env_version', 'release'));
+
+        $posterCacheKey = 'wx_mini_invite_hd_poster_v3_' . $mainId . '_' . $inviteCode . '_' . $envVersion;
+        $cachedPoster = Yii::$app->redis->executeCommand('GET', [$posterCacheKey]);
+        if (!empty($cachedPoster)) {
+            $respond = json_decode($cachedPoster, true);
+            if (!empty($respond['imgUrl']) && !empty($respond['miniCodeUrl'])) {
+                util::success($respond);
+            }
+        }
+
+        $merchant = WxOpenClass::getWxInfo();
+        $page = 'pagesClient/official/applyInvite';
+        $ptStyle = dict::getDict('ptStyle', 'hd');
+        $scene = 'inviteCode=' . $inviteCode;
+        // 开通页可能尚未发布到体验版,check_path=false 仍可生成码图;并校验本地 JPEG 避免缓存错误 JSON
+        $imgUrl = $this->ensureValidInviteHdMiniCode($merchant, $page, $scene, $ptStyle, $envVersion);
+        $miniCodeUrl = imgUtil::groupImg($imgUrl);
+        $titleBase64 = stringUtil::ossBase64('邀请开店');
+        $prefix = imgUtil::getPrefix();
+        $url = $prefix . 'shop/miniCodeBg.jpg?x-oss-process=image';
+        $miniCodeBase64 = stringUtil::ossBase64($imgUrl);
+        $url .= '/watermark,image_' . $miniCodeBase64 . ',g_north,x_0,y_40';
+        $url .= '/watermark,text_' . $titleBase64 . ',g_north,x_0,y_530,size_50';
+        $respond = [
+            'imgUrl' => $url,
+            'miniCodeUrl' => $miniCodeUrl,
+            'inviteCode' => $inviteCode,
+        ];
+
+        Yii::$app->redis->executeCommand('SETEX', [$posterCacheKey, 86400 * 3, json_encode($respond)]);
+
+        util::success($respond);
+    }
+
+    /**
+     * 生成 hd 邀请开店小程序码相对路径,无效缓存文件时删除并重试一次
+     */
+    private function ensureValidInviteHdMiniCode($merchant, $page, $scene, $ptStyle, $envVersion)
+    {
+        $relative = miniUtil::generateUnlimitedMiniCode($merchant, $page, $scene, $ptStyle, $envVersion, false);
+        $fullPath = dirUtil::getImgDir() . $relative;
+        if ($this->isValidJpegFile($fullPath)) {
+            return $relative;
+        }
+        if (is_file($fullPath)) {
+            @unlink($fullPath);
+        }
+        $relative = miniUtil::generateUnlimitedMiniCode($merchant, $page, $scene, $ptStyle, $envVersion, false);
+        $fullPath = dirUtil::getImgDir() . $relative;
+        if (!$this->isValidJpegFile($fullPath)) {
+            if (is_file($fullPath)) {
+                @unlink($fullPath);
+            }
+            util::fail('小程序码生成失败,请确认花店端已上传体验版并包含开通页');
+        }
+        return $relative;
+    }
+
+    /** 判断本地文件是否为有效 JPEG(微信失败时可能写入 JSON 文本) */
+    private function isValidJpegFile($fullPath)
+    {
+        if (!is_file($fullPath) || filesize($fullPath) < 100) {
+            return false;
+        }
+        $head = @file_get_contents($fullPath, false, null, 0, 2);
+        return $head === "\xFF\xD8";
+    }
+
+}

+ 26 - 14
app-hd/controllers/MainInviteController.php

@@ -6,6 +6,7 @@ namespace hd\controllers;
 
 use bizHd\shop\classes\MainInviteClass;
 use bizHd\shop\classes\MainInviteCommissionClass;
+use bizHd\shop\classes\MainInviteFlowClass;
 use common\components\util;
 use Yii;
 
@@ -20,7 +21,7 @@ class MainInviteController extends BaseController
     public function actionIndex()
     {
         $mainId = (int) $this->mainId;
-        $invite = MainInviteClass::getByMainId($mainId);
+        $invite = MainInviteClass::ensureByMainId($mainId);
         $inviteData = [
             'inviteCode' => '',
             'inviteCodeMask' => '',
@@ -67,23 +68,34 @@ class MainInviteController extends BaseController
     }
 
     /**
-     * 佣金明细分页
+     * 佣金变动明细分页(xhMainInviteFlow)
      */
     public function actionCommissionList()
     {
-        $get = Yii::$app->request->get();
-        $settleStatus = $get['settleStatus'] ?? null;
-        if ($settleStatus === '') {
-            $settleStatus = null;
-        }
-        $list = MainInviteCommissionClass::getCommissionList($this->mainId, $settleStatus);
-        $rows = $list['list'] ?? [];
-        $formatted = [];
-        foreach ($rows as $row) {
-            $formatted[] = MainInviteCommissionClass::formatCommissionListItem($row);
-        }
-        $list['list'] = $formatted;
+        $list = MainInviteFlowClass::getFlowList($this->mainId);
         util::success($list);
     }
 
+    /**
+     * 可提现佣金全额存入中央钱包余额
+     * POST:无参,按当前登录 mainId 处理
+     */
+    public function actionDepositBalance()
+    {
+        $mainId = (int) $this->mainId;
+        $shopId = (int) $this->shopId;
+        $shopName = trim((string) ($this->shop->name ?? ''));
+        try {
+            $result = MainInviteClass::depositAbleCommissionToWallet($mainId, $shopId, $shopName);
+            $invite = MainInviteClass::getByMainId($mainId);
+            $inviteData = [
+                'totalCommission' => round((float) ($invite['totalCommission'] ?? 0), 2),
+                'ableCommission' => round((float) ($invite['ableCommission'] ?? 0), 2),
+            ];
+            util::success(array_merge($result, ['invite' => $inviteData]));
+        } catch (\Exception $e) {
+            util::fail($e->getMessage());
+        }
+    }
+
 }

+ 29 - 0
app-hd/controllers/ShopController.php

@@ -169,11 +169,30 @@ class ShopController extends BaseController
         //手机号具有唯一性,暂时不允许修改 --- 同批发端 app-ghs/controllers/ShopController.php 保持一致
         unset($data['mobile']);
 
+        // 客服电话/微信二维码存 xhShopExt,不能混入主表更新
+        $serviceMobile = isset($data['serviceMobile']) ? trim((string)$data['serviceMobile']) : '';
+        $serviceWx = isset($data['serviceWx']) ? trim((string)$data['serviceWx']) : '';
+        unset($data['serviceMobile'], $data['serviceWx']);
+
         // 添加事务处理
         $connection = Yii::$app->db;
         $transaction = $connection->beginTransaction();
         try {
             ShopClass::updateShop($shop, $data);
+            // 老门店可能没有 ext 记录:有则更新,无则补建
+            $ext = ShopExtClass::getByCondition(['shopId' => $id], true);
+            if (!empty($ext)) {
+                ShopExtClass::updateByCondition(['shopId' => $id], [
+                    'serviceMobile' => $serviceMobile,
+                    'serviceWx' => $serviceWx,
+                ]);
+            } else {
+                ShopExtClass::add([
+                    'shopId' => $id,
+                    'serviceMobile' => $serviceMobile,
+                    'serviceWx' => $serviceWx,
+                ]);
+            }
             $transaction->commit();
             util::complete();
         } catch (\Exception $exception) {
@@ -430,6 +449,16 @@ class ShopController extends BaseController
         }
         $shop = ShopClass::getShopInfo($shopId);
         $shop['hasMap'] = dict::getDict('hasMap'); //添加 hasMap 属性(字典中的 hasMap)
+
+        // 合并 xhShopExt 客服电话/微信二维码,供门店编辑页回填
+        $ext = ShopExtClass::getByCondition(['shopId' => $shopId], true);
+        $shortServiceWx = !empty($ext) ? ($ext->serviceWx ?? '') : '';
+        $shop['serviceMobile'] = !empty($ext) ? ($ext->serviceMobile ?? '') : '';
+        $shop['shortServiceWx'] = $shortServiceWx;
+        $shop['serviceWx'] = empty($shortServiceWx)
+            ? ''
+            : imgUtil::groupImg($shortServiceWx) . "?x-oss-process=image/resize,m_fill,h_700,w_700";
+
         util::success(['info' => $shop]);
     }
 

+ 10 - 0
app-mall/controllers/ShopController.php

@@ -2,6 +2,7 @@
 
 namespace mall\controllers;
 
+use biz\shop\classes\ShopExtClass;
 use bizHd\stat\classes\StatVisitClass;
 use bizHd\custom\classes\CustomClass;
 use bizHd\custom\classes\HdClass;
@@ -34,6 +35,15 @@ class ShopController extends BaseController
         $info['avatar'] = $avatar;
         $info['shortAvatar'] = $shortAvatar;
 
+        // 合并 xhShopExt 客服电话/微信二维码,供商城客服页展示与拨号
+        $ext = !empty($shop) ? ShopExtClass::getByCondition(['shopId' => $shop->id], true) : null;
+        $shortServiceWx = !empty($ext) ? ($ext->serviceWx ?? '') : '';
+        $info['serviceMobile'] = !empty($ext) ? ($ext->serviceMobile ?? '') : '';
+        $info['shortServiceWx'] = $shortServiceWx;
+        $info['serviceWx'] = empty($shortServiceWx)
+            ? ''
+            : imgUtil::groupImg($shortServiceWx) . "?x-oss-process=image/resize,m_fill,h_700,w_700";
+
         $user = $this->user;
         $hd = [];
         $custom = [];

+ 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,
+        ];
+    }
+
+}

+ 273 - 23
biz-hd/shop/classes/MainInviteClass.php

@@ -1,12 +1,17 @@
 <?php
 /**
  * 门店邀请主数据业务
+ * 职责:邀请码、注册减免、发佣、可提现佣金存入中央钱包
  */
 
 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;
+use bizHd\shop\classes\MainWalletChangeClass;
+use bizHd\shop\classes\MainClass;
 use common\components\dict;
 use Yii;
 
@@ -26,6 +31,34 @@ class MainInviteClass extends BaseClass
         return self::getByCondition(['mainId' => (int)$mainId], false);
     }
 
+    /**
+     * 确保 xhMainInvite 存在(邀请人账户);无记录则按 dict 默认规则自动初始化
+     * 供 shopInviteHub 首页、生成邀请码等入口调用;并发下 uk_mainId 冲突时再查一次
+     *
+     * @param int $mainId 当前登录 xhMain.id
+     * @return array|null 邀请主数据行
+     */
+    public static function ensureByMainId($mainId)
+    {
+        $mainId = (int) $mainId;
+        if ($mainId <= 0) {
+            return null;
+        }
+        $row = self::getByMainId($mainId);
+        if (!empty($row)) {
+            return $row;
+        }
+        try {
+            return self::createInviteeRecord($mainId, 0);
+        } catch (\Throwable $e) {
+            $row = self::getByMainId($mainId);
+            if (!empty($row)) {
+                return $row;
+            }
+            throw $e;
+        }
+    }
+
     /**
      * 按邀请码查 xhMainInvite
      */
@@ -203,33 +236,250 @@ class MainInviteClass extends BaseClass
             return;
         }
 
+        $inviteId = (int) ($inviter['id'] ?? 0);
+        if ($inviteId <= 0) {
+            return;
+        }
+
         $applyId = (int) ($apply['id'] ?? 0);
         $commissionSn = 'FY' . date('YmdHis') . $applyId;
+        $flowTime = date('Y-m-d H:i:s');
+        // 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);
+        $prePriceAmount = round((float) ($prePrice ?? 0), 2);
 
-        MainInviteCommissionClass::add([
-            'commissionSn' => $commissionSn,
-            'inviterMainId' => $inviterMainId,
-            'lsShopId' => (int) ($apply['shopId'] ?? 0),
-            'lsShopName' => trim((string) ($apply['name'] ?? '')),
-            'inviteeMainId' => (int) $inviteeMainId,
-            'inviteeShopName' => trim((string) $inviteeShopName),
-            'inviteeMobile' => trim((string) $inviteeMobile),
-            'inviteCode' => trim((string) ($apply['inviteCode'] ?? '')),
-            'memberTypeName' => '年度会员',
-            'amount' => round((float) ($prePrice ?? 0), 2),
-            'memberPayAmount' => round((float) ($actPrice ?? 0), 2),
-            'commissionAmount' => $commissionAmount,
-            'hdDiscountAmount' => round((float) ($apply['hdDiscountAmount'] ?? 0), 2),
-            'settleStatus' => 0,
-            'payTime' => date('Y-m-d H:i:s'),
-        ], false);
+        $transaction = Yii::$app->db->beginTransaction();
+        try {
+            $inviteRow = self::getLockById($inviteId);
+            if (empty($inviteRow) || (int) ($inviteRow->mainId ?? 0) !== $inviterMainId) {
+                throw new \Exception('邀请账户无效');
+            }
+
+            $beforeAble = round((float) ($inviteRow->ableCommission ?? 0), 2);
+
+            // 1. 邀约记录(受邀花店维度;pf/ls 为邀请人门店快照,发佣即已结算)
+            $commissionLog = MainInviteCommissionClass::add([
+                'commissionSn' => $commissionSn,
+                'inviterMainId' => $inviterMainId,
+                'pfShopId' => $pfShopId,
+                'pfShopName' => $pfShopName,
+                'lsShopId' => $lsShopId,
+                'lsShopName' => $lsShopName,
+                'inviteeMainId' => (int) $inviteeMainId,
+                'inviteeShopName' => $inviteeShopName,
+                'inviteeMobile' => $inviteeMobile,
+                'inviteCode' => trim((string) ($apply['inviteCode'] ?? '')),
+                'memberTypeName' => '年度会员',
+                'amount' => $prePriceAmount,
+                'memberPayAmount' => $memberPayAmount,
+                'commissionAmount' => $commissionAmount,
+                'hdDiscountAmount' => round((float) ($apply['hdDiscountAmount'] ?? 0), 2),
+                'settleStatus' => 1,
+                'settleTime' => $flowTime,
+                'payTime' => $flowTime,
+            ], true);
+            $commissionId = (int) ($commissionLog->id ?? 0);
+
+            // 2. 累加 inviteCount / totalCommission / ableCommission
+            $inviteCount = (int) ($inviteRow->inviteCount ?? 0) + 1;
+            $totalCommission = round((float) ($inviteRow->totalCommission ?? 0) + $commissionAmount, 2);
+            $ableCommission = round($beforeAble + $commissionAmount, 2);
+            self::updateById($inviteId, [
+                'inviteCount' => $inviteCount,
+                'totalCommission' => $totalCommission,
+                'ableCommission' => $ableCommission,
+            ]);
+
+            // 3. 佣金变动流水(+获佣)
+            MainInviteFlowClass::add([
+                'mainId' => $inviterMainId,
+                'flowType' => MainInviteFlowClass::FLOW_TYPE_EARN,
+                'amount' => $commissionAmount,
+                'ableAfter' => $ableCommission,
+                'refType' => MainInviteFlowClass::REF_TYPE_COMMISSION,
+                'refId' => $commissionId,
+                'refSn' => $commissionSn,
+                'inviteeShopName' => $inviteeShopName,
+                'inviteeMobile' => $inviteeMobile,
+                'lsShopId' => $lsShopId,
+                'baseAmount' => $memberPayAmount > 0 ? $memberPayAmount : $prePriceAmount,
+                'flowTime' => $flowTime,
+                'remark' => '邀请开通获佣',
+            ], true);
+
+            $transaction->commit();
+        } catch (\Throwable $e) {
+            $transaction->rollBack();
+            throw $e;
+        }
+    }
+
+    /**
+     * 按 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),
+        ];
+    }
 
-        $inviteCount = (int) ($inviter['inviteCount'] ?? 0) + 1;
-        $totalCommission = round((float) ($inviter['totalCommission'] ?? 0) + $commissionAmount, 2);
-        self::updateById((int) $inviter['id'], [
-            'inviteCount' => $inviteCount,
-            'totalCommission' => $totalCommission,
-        ]);
+    /**
+     * 将全部可提现佣金存入 xhMain.walletBalance(全额存入,非部分)
+     *
+     * @param int $mainId 邀请人中央 id
+     * @param int $shopId 当前门店 id(写佣金流水快照)
+     * @param string $shopName 门店名称快照
+     * @return array commissionSn、depositAmount、ableCommission、walletBalance
+     * @throws \Throwable
+     */
+    public static function depositAbleCommissionToWallet($mainId, $shopId, $shopName = '')
+    {
+        $mainId = (int) $mainId;
+        $shopId = (int) $shopId;
+        if ($mainId <= 0) {
+            throw new \Exception('账户无效');
+        }
+
+        $lockKey = 'main_invite_deposit_' . $mainId;
+        $locked = Yii::$app->redis->executeCommand('SET', [$lockKey, '1', 'NX', 'EX', 15]);
+        if ($locked === false || $locked === null) {
+            throw new \Exception('操作进行中,请稍后再试');
+        }
+
+        try {
+            return self::depositAbleCommissionToWalletInTransaction($mainId, $shopId, trim((string) $shopName));
+        } finally {
+            Yii::$app->redis->executeCommand('DEL', [$lockKey]);
+        }
+    }
+
+    /**
+     * 事务内:扣减 ableCommission、写佣金存入流水、增加钱包余额并记 xhMainWalletChange
+     */
+    protected static function depositAbleCommissionToWalletInTransaction($mainId, $shopId, $shopName)
+    {
+        $preview = self::ensureByMainId($mainId);
+        if (empty($preview)) {
+            throw new \Exception('暂无邀请账户');
+        }
+        $inviteId = (int) ($preview['id'] ?? 0);
+        if ($inviteId <= 0) {
+            throw new \Exception('暂无邀请账户');
+        }
+
+        $transaction = Yii::$app->db->beginTransaction();
+        try {
+            $invite = self::getLockById($inviteId);
+            if (empty($invite) || (int) ($invite->mainId ?? 0) !== $mainId) {
+                throw new \Exception('邀请账户无效');
+            }
+
+            $beforeAble = round((float) ($invite->ableCommission ?? 0), 2);
+            if ($beforeAble <= 0) {
+                throw new \Exception('暂无可存入佣金');
+            }
+            $amount = $beforeAble;
+            $afterAble = 0;
+
+            $main = MainClass::getLockById($mainId);
+            if (empty($main)) {
+                throw new \Exception('账户无效');
+            }
+
+            $flowTime = date('Y-m-d H:i:s');
+            $depositSn = 'CR' . date('YmdHis') . $mainId;
+
+            // 1. 扣减可提现佣金(累计 totalCommission 不变)
+            self::updateById($inviteId, ['ableCommission' => $afterAble]);
+
+            // 2. 中央钱包余额增加
+            $beforeWallet = round((float) ($main->walletBalance ?? 0), 2);
+            $afterWallet = round($beforeWallet + $amount, 2);
+            MainClass::updateById($mainId, ['walletBalance' => $afterWallet]);
+
+            // 3. 钱包变动流水
+            $capitalType = dict::getDict('capitalType', 'inviteCommissionDeposit', 'id');
+            $payWay = dict::getDict('payWay', 'unknown');
+            $ptStyle = dict::getDict('ptStyle', 'hd');
+            $fromType = dict::getDict('fromType', 'shop');
+            $ioIncome = dict::getDict('io', 'income');
+
+            $walletChange = MainWalletChangeClass::add([
+                'relateId' => 0,
+                'ptStyle' => $ptStyle,
+                'capitalType' => $capitalType,
+                'amount' => $amount,
+                'balance' => $afterWallet,
+                'io' => $ioIncome,
+                'payWay' => $payWay,
+                'fromType' => $fromType,
+                'event' => '佣金存入余额' . $depositSn,
+                'mainId' => $mainId,
+                'remark' => '邀请佣金存入中央钱包',
+            ], true);
+            $walletChangeId = (int) ($walletChange->id ?? 0);
+
+            // 4. 佣金变动流水(-存入,同步 ableAfter)
+            $flowLog = MainInviteFlowClass::add([
+                'mainId' => $mainId,
+                'flowType' => MainInviteFlowClass::FLOW_TYPE_DEPOSIT,
+                'amount' => round(0 - $amount, 2),
+                'ableAfter' => $afterAble,
+                'refType' => MainInviteFlowClass::REF_TYPE_WALLET_CHANGE,
+                'refId' => $walletChangeId,
+                'refSn' => $depositSn,
+                'inviteeShopName' => $shopName,
+                'inviteeMobile' => '',
+                'lsShopId' => $shopId,
+                'baseAmount' => $amount,
+                'flowTime' => $flowTime,
+                'remark' => '佣金存入余额',
+            ], true);
+            $flowId = (int) ($flowLog->id ?? 0);
+
+            if ($walletChangeId > 0 && $flowId > 0) {
+                MainWalletChangeClass::updateById($walletChangeId, ['relateId' => $flowId]);
+            }
+
+            $transaction->commit();
+
+            return [
+                'commissionSn' => $depositSn,
+                'depositAmount' => $amount,
+                'ableCommission' => $afterAble,
+                'walletBalance' => $afterWallet,
+            ];
+        } catch (\Throwable $e) {
+            $transaction->rollBack();
+            throw $e;
+        }
     }
 
 }

+ 10 - 2
biz-hd/shop/classes/MainInviteCommissionClass.php

@@ -37,6 +37,7 @@ class MainInviteCommissionClass extends BaseClass
         }
         return self::getLimitList('*', [
             'inviterMainId' => (int) $inviterMainId,
+            'inviteeMainId>' => 0,
         ], (int) $limit, 'addTime DESC,id DESC');
     }
 
@@ -87,8 +88,12 @@ class MainInviteCommissionClass extends BaseClass
         }
 
         $keyword = trim((string) $keyword);
+        $baseWhere = [
+            'inviterMainId' => $mainId,
+            'inviteeMainId>' => 0,
+        ];
         if ($keyword === '') {
-            $result = self::getList('*', ['inviterMainId' => $mainId], 'addTime DESC,id DESC');
+            $result = self::getList('*', $baseWhere, 'addTime DESC,id DESC');
         } else {
             // 店名 / 冗余字段 inviteeMobile 模糊匹配
             $result = self::getInviteShopListByKeyword($mainId, $keyword);
@@ -114,7 +119,10 @@ class MainInviteCommissionClass extends BaseClass
             : (int) (Yii::$app->params['pageSize'] ?? 20);
 
         $model = self::getModel();
-        $query = $model->conditionQuery(['inviterMainId' => (int) $inviterMainId]);
+        $query = $model->conditionQuery([
+            'inviterMainId' => (int) $inviterMainId,
+            'inviteeMainId>' => 0,
+        ]);
         $query->andWhere([
             'or',
             ['like', 'inviteeShopName', $keyword],

+ 144 - 0
biz-hd/shop/classes/MainInviteFlowClass.php

@@ -0,0 +1,144 @@
+<?php
+/**
+ * 邀请佣金变动流水 xhMainInviteFlow
+ * 职责:佣金变动明细分页、格式化;配合 xhMainInvite.ableCommission 落 ableAfter 快照
+ */
+namespace bizHd\shop\classes;
+
+use biz\shop\classes\ShopClass;
+use bizHd\base\classes\BaseClass;
+use common\components\dateUtil;
+use Yii;
+
+class MainInviteFlowClass extends BaseClass
+{
+
+    public static $baseFile = '\bizHd\shop\models\MainInviteFlow';
+
+    /** 邀请开通获佣 */
+    const FLOW_TYPE_EARN = 1;
+
+    /** 佣金存入中央钱包 */
+    const FLOW_TYPE_DEPOSIT = 2;
+
+    /** 退款扣回佣金 */
+    const FLOW_TYPE_REFUND = 3;
+
+    /** 关联 xhMainInviteCommission.id */
+    const REF_TYPE_COMMISSION = 1;
+
+    /** 关联 xhMainWalletChange.id */
+    const REF_TYPE_WALLET_CHANGE = 2;
+
+    /**
+     * 佣金变动明细分页(默认近30天)
+     */
+    public static function getFlowList($mainId)
+    {
+        $where = self::buildFlowListWhere($mainId);
+        $result = self::getList('*', $where, 'flowTime DESC,id DESC');
+        $rows = $result['list'] ?? [];
+        $list = [];
+        foreach ($rows as $row) {
+            $list[] = self::formatFlowListItem($row);
+        }
+        $result['list'] = $list;
+        return $result;
+    }
+
+    /**
+     * 列表查询条件:mainId + 时间筛选
+     */
+    protected static function buildFlowListWhere($mainId)
+    {
+        $where = ['mainId' => (int) $mainId];
+        $get = Yii::$app->request->get();
+        $searchTime = isset($get['searchTime']) ? trim((string) $get['searchTime']) : 'last30Days';
+        $startTime = isset($get['startTime']) ? trim((string) $get['startTime']) : '';
+        $endTime = isset($get['endTime']) ? trim((string) $get['endTime']) : '';
+        if ($searchTime !== '' && $searchTime !== 'all') {
+            $period = dateUtil::formatTime($searchTime, $startTime, $endTime);
+            if (!empty($period['startTime']) && !empty($period['endTime'])) {
+                $where['flowTime'] = ['between', [$period['startTime'], $period['endTime']]];
+            }
+        }
+        return $where;
+    }
+
+    /**
+     * 格式化单行,字段与 hdApp shopInviteCommissionList 一致
+     */
+    public static function formatFlowListItem($row)
+    {
+        $flowType = (int) ($row['flowType'] ?? 0);
+        $lsShopId = (int) ($row['lsShopId'] ?? 0);
+        $shop = [];
+        if ($lsShopId > 0) {
+            $shop = ShopClass::getById($lsShopId, false) ?: [];
+        }
+
+        $shopName = trim((string) ($row['inviteeShopName'] ?? ''));
+        $mobile = trim((string) ($row['inviteeMobile'] ?? ''));
+        $mobileMask = $mobile !== '' ? MainInviteCommissionClass::maskMobile($mobile) : '-';
+
+        if ($flowType === self::FLOW_TYPE_DEPOSIT) {
+            $orderSnLabel = '存入编号';
+            $baseAmountLabel = '存余额金额';
+            $recordType = 'deposit';
+            $shopName = '存入钱包余额';
+            $mobile = '';
+            $mobileMask = '-';
+            $shop = [];
+        } else {
+            $orderSnLabel = '会员订单号';
+            $baseAmountLabel = '会员实付金额';
+            $recordType = 'member';
+        }
+
+        $orderSn = trim((string) ($row['refSn'] ?? ''));
+        $baseAmount = round((float) ($row['baseAmount'] ?? 0), 2);
+        $rawAmount = round((float) ($row['amount'] ?? 0), 2);
+        $commissionAmount = round(abs($rawAmount), 2);
+        $commissionSign = $rawAmount < 0 ? -1 : 1;
+        if ($flowType === self::FLOW_TYPE_REFUND) {
+            $commissionSign = -1;
+        }
+
+        $createTime = trim((string) ($row['flowTime'] ?? ($row['addTime'] ?? '')));
+        if ($createTime !== '' && strlen($createTime) === 10) {
+            $createTime .= ' 00:00:00';
+        }
+
+        return [
+            'id' => $row['id'] ?? 0,
+            'shopName' => $shopName !== '' ? $shopName : '-',
+            'shopImg' => self::resolveShopImg($shop),
+            'mobile' => $mobile !== '' ? $mobile : '-',
+            'mobileMask' => $mobileMask,
+            'orderSnLabel' => $orderSnLabel,
+            'orderSn' => $orderSn !== '' ? $orderSn : '-',
+            'baseAmountLabel' => $baseAmountLabel,
+            'baseAmount' => $baseAmount,
+            'createTime' => $createTime !== '' ? $createTime : '-',
+            'commissionAmount' => $commissionAmount,
+            'commissionSign' => $commissionSign,
+            'recordType' => $recordType,
+            'flowType' => $flowType,
+            'ableAfter' => round((float) ($row['ableAfter'] ?? 0), 2),
+        ];
+    }
+
+    protected static function resolveShopImg($shop)
+    {
+        $avatar = is_array($shop) ? ($shop['avatar'] ?? '') : '';
+        if (empty($avatar)) {
+            return '';
+        }
+        if (strpos($avatar, 'http') === 0) {
+            return $avatar;
+        }
+        $host = Yii::$app->params['hdImgHost'] ?? '';
+        return $host . $avatar;
+    }
+
+}

+ 10 - 1
biz-hd/shop/classes/ShopClass.php

@@ -191,9 +191,18 @@ class ShopClass extends BaseClass
         if (isset($data['ptStyle']) == false) {
             util::fail('请先择平台类型');
         }
+        // 客服电话/微信二维码落在 xhShopExt,写入主表前先取出,避免未知字段干扰
+        $serviceMobile = isset($data['serviceMobile']) ? trim((string)$data['serviceMobile']) : '';
+        $serviceWx = isset($data['serviceWx']) ? trim((string)$data['serviceWx']) : '';
+        unset($data['serviceMobile'], $data['serviceWx']);
+
         $shop = self::add($data, true);
         $newShopId = $shop->id;
-        $extData = ['shopId' => $newShopId];
+        $extData = [
+            'shopId' => $newShopId,
+            'serviceMobile' => $serviceMobile,
+            'serviceWx' => $serviceWx,
+        ];
         ShopExtClass::add($extData);
 
         //商品和分类初始化

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

@@ -0,0 +1,18 @@
+<?php
+/**
+ * 邀请佣金变动流水 xhMainInviteFlow
+ * 用途:hdApp 佣金变动明细;与 xhMainInviteCommission(邀约记录)分离
+ */
+namespace bizHd\shop\models;
+
+use bizHd\base\models\Base;
+
+class MainInviteFlow extends Base
+{
+
+    public static function tableName()
+    {
+        return 'xhMainInviteFlow';
+    }
+
+}

+ 16 - 12
biz/renew/classes/RenewClass.php

@@ -51,16 +51,15 @@ class RenewClass extends BaseClass
             return false;
         }
         $ptStyle = dict::getDict('ptStyle', 'hd');
-        $list = self::getRenewList(['mainId' => $mainId, 'status' => 1, 'ptStyle' => $ptStyle]);
+        $list = self::getAllByCondition(['mainId' => $mainId, 'status' => 1, 'ptStyle' => $ptStyle]);
         if (empty($list)) {
             return false;
         }
         foreach ($list as $item) {
             $sn = is_array($item) ? (string) ($item['orderSn'] ?? '') : (string) ($item->orderSn ?? '');
-            if ($excludeOrderSn !== '' && $sn === $excludeOrderSn) {
-                continue;
+            if ($excludeOrderSn !== '' && $sn !== $excludeOrderSn) {
+                return true;
             }
-            return true;
         }
         return false;
     }
@@ -130,14 +129,19 @@ class RenewClass extends BaseClass
         if ($isRegisterMember && !self::hasOtherPaidMemberRenew((int) $mainId, (string) $orderSn)) {
             $apply = ApplyClass::getById($applyId);
             if (!empty($apply)) {
-                MainInviteClass::grantInviterCommission(
-                    $apply,
-                    (int) $mainId,
-                    trim((string) ($apply['name'] ?? '')),
-                    trim((string) ($apply['mobile'] ?? '')),
-                    round((float) ($renew->prePrice ?? 0), 2),
-                    round((float) ($renew->actPrice ?? 0), 2)
-                );
+                try {
+                    MainInviteClass::grantInviterCommission(
+                        $apply,
+                        (int) $mainId,
+                        trim((string) ($apply['name'] ?? '')),
+                        trim((string) ($apply['mobile'] ?? '')),
+                        round((float) ($renew->prePrice ?? 0), 2),
+                        round((float) ($renew->actPrice ?? 0), 2)
+                    );
+                } catch (\Throwable $e) {
+                    $msg = '邀请发佣失败 orderSn:' . $orderSn . ' ' . $e->getMessage();
+                    noticeUtil::push($msg, '15280215347');
+                }
             }
         }
 

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

+ 2 - 0
common/components/dict.php

@@ -513,6 +513,8 @@ class dict
             'distributionDeposit' => ['id' => 83, 'name' => 'distributionDeposit'],
             // 花店注册年度会员支付
             'hdRegisterOrder' => ['id' => 84, 'name' => 'hdRegisterOrder'],
+            // 邀请佣金存入中央钱包(xhMainInviteFlow + xhMainWalletChange)
+            'inviteCommissionDeposit' => ['id' => 85, 'name' => 'inviteCommissionDeposit'],
         ],
         "capitalTypeList" => [//流水类型的对应链接,后台收支明细查看时跳转的链接
             0 => ['link' => '/capital/order-detail', 'name' => '网店', 'orderLink' => '/order/detail', 'id' => 0,],

+ 4 - 1
common/components/miniUtil.php

@@ -1609,7 +1609,7 @@ class miniUtil
         return $envVersion;
     }
 
-    public static function generateUnlimitedMiniCode($merchant, $page, $scene = '', $ptStyle = 0, $envVersion = 'release')
+    public static function generateUnlimitedMiniCode($merchant, $page, $scene = '', $ptStyle = 0, $envVersion = 'release', $checkPath = null)
     {
         $envVersion = self::normalizeMiniEnvVersion($envVersion);
         $id = isset($merchant['id']) ? $merchant['id'] : 0;
@@ -1631,6 +1631,9 @@ class miniUtil
             $data["page"] = $page;
         }
         $data["scene"] = $scene;
+        if ($checkPath === false) {
+            $data["check_path"] = false;
+        }
         if ($envVersion !== 'release') {
             $data["env_version"] = $envVersion;
         }

+ 26 - 1
sql/20260706_redesign.sql

@@ -383,10 +383,35 @@ CREATE TABLE IF NOT EXISTS `xhMainInviteCommission` (
     KEY `idx_relateOrderId` (`relateOrderId`) USING BTREE
     ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='门店邀请记录';
 
+CREATE TABLE IF NOT EXISTS `xhMainInviteFlow` (
+    `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+    `mainId` int(11) NOT NULL DEFAULT 0 COMMENT '邀请人 xhMain.id',
+    `flowType` tinyint(3) unsigned NOT NULL DEFAULT 1 COMMENT '1邀请获佣 2存入余额 3退款扣回',
+    `amount` decimal(15,2) NOT NULL DEFAULT 0.00 COMMENT '变动金额:获佣为正,存入/扣回为负',
+    `ableAfter` decimal(15,2) NOT NULL DEFAULT 0.00 COMMENT '变动后可提现佣金',
+    `refType` tinyint(3) unsigned NOT NULL DEFAULT 0 COMMENT '1=xhMainInviteCommission 2=xhMainWalletChange',
+    `refId` bigint(20) unsigned NOT NULL DEFAULT 0 COMMENT '关联业务 id',
+    `refSn` varchar(32) NOT NULL DEFAULT '' COMMENT '分佣单号 FY… / 存入编号 CR…',
+    `inviteeShopName` varchar(80) NOT NULL DEFAULT '' COMMENT '受邀花店名快照 flowType=1',
+    `inviteeMobile` varchar(20) NOT NULL DEFAULT '' COMMENT '受邀手机号快照',
+    `lsShopId` int(11) NOT NULL DEFAULT 0 COMMENT '零售店 shopId',
+    `baseAmount` decimal(15,2) NOT NULL DEFAULT 0.00 COMMENT '会员实付或存余额金额',
+    `flowTime` datetime NOT NULL COMMENT '业务时间',
+    `remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
+    `addTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    PRIMARY KEY (`id`),
+    KEY `idx_main_flowTime` (`mainId`, `flowTime`),
+    KEY `idx_ref` (`refType`, `refId`)
+    ) COMMENT='邀请佣金变动流水';
 -- 花店注册付费开通:xhApply 扩展字段
 ALTER TABLE `xhApply`
   ADD COLUMN `inviteCode` varchar(32) NOT NULL DEFAULT '' COMMENT '注册时填写的邀请码' AFTER `status`,
   ADD COLUMN `hdDiscountAmount` decimal(15,2) NOT NULL DEFAULT '0.00' COMMENT '邀请优惠金额' AFTER `inviteCode`,
   ADD COLUMN `inviteCommissionAmount` decimal(15,2) NOT NULL DEFAULT '0.00' COMMENT '邀请人佣金快照' AFTER `hdDiscountAmount`;
 ALTER TABLE `xhRenew`
-    ADD COLUMN `ptStyle` tinyint(4) NOT NULL DEFAULT '2' COMMENT '所属平台 1花店 2供货商 请查看dict.php',
+    ADD COLUMN `ptStyle` tinyint(4) NOT NULL DEFAULT '2' COMMENT '所属平台 1花店 2供货商 请查看dict.php';
+
+-- xhShopExt 表添加客服电话与微信二维码
+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;