Sfoglia il codice sorgente

花掌柜-花店邀请花店 功能开发

ouyang 1 giorno fa
parent
commit
1fa0c5b745

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

@@ -267,4 +267,4 @@ class RenewController extends BaseController
         util::success($newParams);
     }
 
-}
+}

+ 131 - 2
app-hd/controllers/ApplyController.php

@@ -20,11 +20,12 @@ use Yii;
 use common\components\util;
 use bizHd\saas\classes\ApplyClass;
 use bizHd\saas\services\RegionService;
+use bizHd\shop\classes\MainInviteClass;
 
 class ApplyController extends BaseController
 {
 
-    public $guestAccess = ['register', 'get-auth-code'];
+    public $guestAccess = ['register', 'get-auth-code', 'register-fee-preview', 'register-invite-context'];
 
     //申请开通批发店 ssh 20231125
     public function actionOpenPf()
@@ -73,6 +74,87 @@ class ApplyController extends BaseController
         util::complete();
     }
 
+    /**
+     * 花店注册页:会员费、邀请减免、应付(未登录可访问)
+     */
+    public function actionRegisterFeePreview()
+    {
+        $get = Yii::$app->request->get();
+        $inviteCode = trim($get['inviteCode'] ?? '');
+        $data = MainInviteClass::resolveRegisterFeePreview($inviteCode);
+        util::success($data);
+    }
+
+    /**
+     * 邀请开通页上下文:是否已有 xhShop、xhApply 邀请码与减免快照(未登录返回 hasShop=0)
+     */
+    public function actionRegisterInviteContext()
+    {
+        $hasShop = $this->shopId > 0 ? 1 : 0;
+        $shopName = '';
+        $shopMobile = '';
+        if ($hasShop && !empty($this->shop)) {
+            $shopAttrs = is_object($this->shop) ? $this->shop->attributes : (array) $this->shop;
+            $shopName = (string) ($shopAttrs['shopName'] ?? ($shopAttrs['name'] ?? ''));
+            $shopMobile = (string) ($shopAttrs['mobile'] ?? '');
+        }
+        $data = [
+            'hasShop' => $hasShop,
+            'shopId' => (int) $this->shopId,
+            'applyId' => 0,
+            'inviteCode' => '',
+            'hdDiscountAmount' => 0,
+            'inviteCommissionAmount' => 0,
+            'shopName' => $shopName,
+            'shopMobile' => $shopMobile,
+        ];
+        if ($this->adminId > 0) {
+            $apply = $this->findRegisterApplyForAdmin($this->adminId, $this->shopId);
+            if (!empty($apply)) {
+                $data['applyId'] = (int) ($apply['id'] ?? 0);
+                $data['inviteCode'] = trim((string) ($apply['inviteCode'] ?? ''));
+                $data['hdDiscountAmount'] = round((float) ($apply['hdDiscountAmount'] ?? 0), 2);
+                $data['inviteCommissionAmount'] = round((float) ($apply['inviteCommissionAmount'] ?? 0), 2);
+            }
+        }
+        util::success($data);
+    }
+
+    /**
+     * 已开店用户:支付前保存邀请码并刷新 xhApply 减免快照
+     */
+    public function actionSaveRegisterInvite()
+    {
+        if ($this->adminId <= 0 || $this->shopId <= 0) {
+            util::fail('请先登录并选择门店');
+        }
+        $post = Yii::$app->request->post();
+        $inviteCode = trim((string) ($post['inviteCode'] ?? ''));
+        $apply = $this->findRegisterApplyForAdmin($this->adminId, $this->shopId);
+        if (empty($apply)) {
+            util::fail('未找到注册申请记录');
+        }
+        $applyId = (int) ($apply['id'] ?? 0);
+        $fee = MainInviteClass::resolveRegisterFeeSubmit($inviteCode);
+        if ($inviteCode !== '' && !empty($fee['inviteNotFound'])) {
+            util::fail('邀请人不存在');
+        }
+        $hdDiscountAmount = round((float) ($fee['hdDiscountAmount'] ?? 0), 2);
+        $inviteCommissionAmount = round((float) ($fee['inviteCommissionAmount'] ?? 0), 2);
+        ApplyClass::updateById($applyId, [
+            'inviteCode' => $inviteCode,
+            'hdDiscountAmount' => $hdDiscountAmount,
+            'inviteCommissionAmount' => $inviteCommissionAmount,
+        ]);
+        $preview = MainInviteClass::resolveRegisterFeePreview($inviteCode);
+        util::success(array_merge($preview, [
+            'applyId' => $applyId,
+            'inviteCode' => $inviteCode,
+            'hdDiscountAmount' => $hdDiscountAmount,
+            'inviteCommissionAmount' => $inviteCommissionAmount,
+        ]));
+    }
+
     //邀请花店海报 ssh 20210610
     public function actionInviteHdPoster()
     {
@@ -167,7 +249,6 @@ class ApplyController extends BaseController
         }
 
         try {
-
             $post['adminId'] = $this->adminId;
             $authCode = $post['authCode'] ?? '';
             $mobile = $post['mobile'] ?? '';
@@ -228,6 +309,19 @@ class ApplyController extends BaseController
             if (empty($id)) {
                 util::fail('申请失败');
             }
+
+            // 邀请码快照写入 xhApply(选填)
+            $inviteCode = trim((string) ($post['inviteCode'] ?? ''));
+            $fee = MainInviteClass::resolveRegisterFeeSubmit($inviteCode);
+            if ($inviteCode !== '' && !empty($fee['inviteNotFound'])) {
+                util::fail('邀请人不存在');
+            }
+            ApplyClass::updateById($id, [
+                'inviteCode' => $inviteCode,
+                'hdDiscountAmount' => round((float) ($fee['hdDiscountAmount'] ?? 0), 2),
+                'inviteCommissionAmount' => round((float) ($fee['inviteCommissionAmount'] ?? 0), 2),
+            ]);
+
             $passInfo = ApplyService::pass($id);
             $mobile = $post['mobile'] ?? '';
             //输出登录信息
@@ -237,6 +331,10 @@ class ApplyController extends BaseController
             $pfShopId = $newShop->pfShopId ?? 0;
             $onlyCg = $newShop->onlyCg ?? 1;
 
+            ApplyClass::updateById($id, ['shopId' => $newShopId]);
+            $inviterMainId = MainInviteClass::resolveInviterMainIdByInviteCode($inviteCode);
+            MainInviteClass::createInviteeRecord($newMainId, $inviterMainId);
+
             $shareLogo = imgUtil::groupImg('buy_logo.jpg');
             if (!empty($newShop->shareLogo)) {
                 $shareLogo = imgUtil::groupImg($newShop->shareLogo);
@@ -281,6 +379,7 @@ class ApplyController extends BaseController
                 'token' => $token,
                 'admin' => $admin,
                 'shopId' => $newShopId,
+                'applyId' => (int) $id,
                 'shopAdminId' => $shopAdminId,
                 'switchShop' => $switchShop,
                 'pfShopId' => $pfShopId,
@@ -326,4 +425,34 @@ class ApplyController extends BaseController
         util::success(['code' => rand(1111, 9999)]);
     }
 
+    /**
+     * 按 adminId / shopId / 手机号查找 xhApply 注册记录
+     */
+    private function findRegisterApplyForAdmin(int $adminId, int $shopId): array
+    {
+        $apply = [];
+        if ($adminId > 0) {
+            $row = ApplyClass::getByCondition(['adminId' => $adminId], false);
+            if (!empty($row)) {
+                $apply = is_array($row) ? $row : (array) $row;
+            }
+        }
+        if (empty($apply) && $shopId > 0) {
+            $row = ApplyClass::getByCondition(['shopId' => $shopId], false);
+            if (!empty($row)) {
+                $apply = is_array($row) ? $row : (array) $row;
+            }
+        }
+        if (empty($apply) && $adminId > 0 && !empty($this->admin)) {
+            $mobile = $this->admin->mobile ?? '';
+            if ($mobile !== '') {
+                $row = ApplyClass::getByCondition(['mobile' => $mobile], false);
+                if (!empty($row)) {
+                    $apply = is_array($row) ? $row : (array) $row;
+                }
+            }
+        }
+        return $apply;
+    }
+
 }

+ 2 - 0
app-hd/controllers/MainController.php

@@ -345,9 +345,11 @@ class MainController extends BaseController
         $sjName = $sj['name'] ?? '';
         $shopName = $shop->shopName ?? '';
         $cashAccount = $main->cashAccount ?? '';
+        $walletBalance = $main->walletBalance ?? '0.00';
         $data = [
             'balance' => $balance,
             'txBalance' => $txBalance,
+            'walletBalance' => $walletBalance,
             'shopImg' => $shopImg,
             'deadline' => $deadline,
             'adminName' => $adminName,

+ 89 - 0
app-hd/controllers/MainInviteController.php

@@ -0,0 +1,89 @@
+<?php
+/**
+ * hdApp 门店邀请 / 我的分佣
+ */
+namespace hd\controllers;
+
+use bizHd\shop\classes\MainInviteClass;
+use bizHd\shop\classes\MainInviteCommissionClass;
+use common\components\util;
+use Yii;
+
+class MainInviteController extends BaseController
+{
+
+    public $guestAccess = [];
+
+    /**
+     * 我的分佣首页:xhMainInvite + 待结算 + 最近佣金
+     */
+    public function actionIndex()
+    {
+        $mainId = (int) $this->mainId;
+        $invite = MainInviteClass::getByMainId($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,
+        ]);
+    }
+
+    /**
+     * 邀约花店列表(xhMainInviteCommission 按 inviterMainId)
+     */
+    public function actionInviteShopList()
+    {
+        $get = Yii::$app->request->get();
+        $keyword = trim($get['keyword'] ?? '');
+        $data = MainInviteCommissionClass::getInviteShopList($this->mainId, $keyword);
+        util::success($data);
+    }
+
+    /**
+     * 佣金明细分页
+     */
+    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;
+        util::success($list);
+    }
+
+}

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

@@ -0,0 +1,53 @@
+<?php
+/**
+ * hdApp 中央钱包(可用余额)接口
+ */
+namespace hd\controllers;
+
+use bizHd\shop\classes\MainWalletChangeClass;
+use common\components\dateUtil;
+use common\components\util;
+use Yii;
+
+class MainWalletController extends BaseController
+{
+
+    public $guestAccess = [];
+
+    /**
+     * 钱包变动明细分页 + 区间汇总
+     * GET:page、searchTime、startTime、endTime、capitalType(可选)
+     */
+    public function actionChangeList()
+    {
+        $get = Yii::$app->request->get();
+        $where = [];
+        $where['mainId'] = $this->mainId;
+
+        if (isset($get['capitalType']) && $get['capitalType'] !== '') {
+            $where['capitalType'] = (int) $get['capitalType'];
+        }
+
+        $searchTime = $get['searchTime'] ?? 'today';
+        if (!empty($searchTime)) {
+            $startTime = $get['startTime'] ?? '';
+            $endTime = $get['endTime'] ?? '';
+            $period = dateUtil::formatTime($searchTime, $startTime, $endTime);
+            $where['addTime'] = ['between', [$period['startTime'], $period['endTime']]];
+        }
+
+        $list = MainWalletChangeClass::getChangeList($where);
+        $summaryAmount = MainWalletChangeClass::getSummaryAmount($where);
+        $main = $this->main;
+        $walletBalance = $main->walletBalance ?? '0.00';
+
+        $list['summary'] = [
+            'totalRecharge' => $summaryAmount['recharge'],
+            'totalConsume' => $summaryAmount['consume'],
+            'walletBalance' => round((float) $walletBalance, 2),
+        ];
+
+        util::success($list);
+    }
+
+}

+ 114 - 1
app-hd/controllers/RenewController.php

@@ -5,8 +5,10 @@ namespace hd\controllers;
 use biz\set\classes\SetClass;
 use biz\sj\services\MerchantExtendService;
 use biz\sj\services\MerchantService;
+use bizHd\saas\classes\ApplyClass;
 use bizHd\saas\services\RenewService;
 use bizHd\saas\services\SetMealService;
+use bizHd\shop\classes\MainInviteClass;
 use bizHd\user\services\UserService;
 use bizHd\wx\classes\WxOpenClass;
 use common\components\dict;
@@ -91,4 +93,115 @@ class RenewController extends BaseController
         util::success($newParams);
     }
 
-}
+    /**
+     * 花店邀请注册:创建年度会员 xhRenew 订单并唤起微信支付(注册成功后调用,applyId 可选)
+     */
+    public function actionCreate()
+    {
+        ini_set('date.timezone', 'Asia/Shanghai');
+        $post = Yii::$app->request->post();
+        $applyId = (int) ($post['applyId'] ?? 0);
+
+        $registerConfig = dict::getDict('hdRegisterConfig');
+        $prePrice = round((float) ($registerConfig['price'] ?? 0), 2);
+        $hdDiscountAmount = 0;
+        if ($applyId > 0) {
+            $apply = ApplyClass::getById($applyId);
+            if (empty($apply)) {
+                util::fail('申请不存在');
+            }
+            if ((int) ($apply['adminId'] ?? 0) !== (int) $this->adminId) {
+                util::fail('申请与当前账号不匹配');
+            }
+            $hdDiscountAmount = round((float) ($apply['hdDiscountAmount'] ?? 0), 2);
+        }
+        $actPrice = round(max(0, $prePrice - $hdDiscountAmount), 2);
+
+        $shopAdmin = $this->shopAdmin->attributes;
+        $adminName = $shopAdmin['name'] ?? '';
+        $shop = $this->shop;
+        $shopName = $shop['shopName'] ?? ($shop['name'] ?? '');
+        $sjName = is_array($this->sj) ? ($this->sj['name'] ?? '') : ($this->sj->name ?? '');
+
+        $now = time();
+        $expireTime = $now + 300;
+        $addData = [
+            'shopAdminId' => $this->shopAdminId,
+            'shopAdminName' => $adminName,
+            'prePrice' => $prePrice,
+            'actPrice' => $actPrice,
+            'sjId' => $this->sjId,
+            'sjName' => $sjName,
+            'mainId' => $this->mainId,
+            'shopId' => $this->shopId,
+            'shopName' => $shopName,
+            'deadline' => date('Y-m-d H:i:s', $expireTime),
+            'status' => 0,
+            'type' => 1,
+            'ptStyle' => dict::getDict('ptStyle', 'hd'),
+            'remark' => $applyId > 0 ? 'applyId=' . $applyId : '',
+            'beforeDeadline'=>$shop->beforeDeadline ?? '',
+        ];
+
+        $order = RenewService::addOrder($addData);
+        $orderId = (int) ($order['id'] ?? 0);
+        $orderSn = (string) ($order['orderSn'] ?? '');
+        if ($orderId <= 0 || $orderSn === '') {
+            util::fail('创建订单失败');
+        }
+
+        if ($actPrice <= 0) {
+            RenewService::completeRegisterRenewPay(
+                dict::getDict('payWay', 'wxPay'),
+                $orderSn,
+                $actPrice,
+                'capitalType=' . dict::getDict('capitalType', 'xhRenew', 'id') . '&applyId=' . $applyId,
+                ''
+            );
+            util::success([
+                'needPay' => 0,
+                'paid' => 1,
+                'applyId' => $applyId,
+                'orderId' => $orderId,
+                'actPrice' => $actPrice,
+            ]);
+        }
+
+        $admin = $this->admin->attributes;
+        $openId = trim((string) ($post['miniOpenId'] ?? ($admin['miniOpenId'] ?? '')));
+        if ($openId === '') {
+            util::fail('请先授权微信登录');
+        }
+
+        $capitalType = dict::getDict('capitalType', 'xhRenew', 'id');
+        $attach = 'capitalType=' . $capitalType . '&applyId=' . $applyId . '&sjId=' . $this->sjId;
+
+        $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(round($actPrice * 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['applyId'] = $applyId;
+        $newParams['needPay'] = 1;
+        $newParams['paid'] = 0;
+        $newParams['actPrice'] = $actPrice;
+        util::success($newParams);
+    }
+}

+ 13 - 4
biz-hd/saas/classes/RenewClass.php

@@ -45,9 +45,18 @@ class RenewClass extends BaseClass
     public static function addOrder($data)
     {
         $data['orderSn'] = orderSn::getRenewSn();
-        $data['payStatus'] = 0;
-        $return = self::add($data);
-        return $return;
+        if (!isset($data['status'])) {
+            $data['status'] = 0;
+        }
+        return self::add($data);
+    }
+
+    /**
+     * 邀请注册会员费:0 元单直接走支付完成逻辑
+     */
+    public static function completeRegisterRenewPay($payWay, $orderSn, $totalFee, $attach, $transactionId = '')
+    {
+        return \biz\renew\classes\RenewClass::thirdPay($payWay, $orderSn, $totalFee, $attach, $transactionId);
     }
 
-}
+}

+ 6 - 0
biz-hd/saas/services/RenewService.php

@@ -22,4 +22,10 @@ class RenewService extends BaseService
 		return RenewClass::addOrder($data);
 	}
 
+	/** 邀请注册 0 元订单直接完成 */
+	public static function completeRegisterRenewPay($payWay, $orderSn, $totalFee, $attach, $transactionId = '')
+	{
+		return RenewClass::completeRegisterRenewPay($payWay, $orderSn, $totalFee, $attach, $transactionId);
+	}
+
 }

+ 227 - 0
biz-hd/shop/classes/MainInviteClass.php

@@ -0,0 +1,227 @@
+<?php
+/**
+ * 门店邀请主数据业务
+ */
+
+namespace bizHd\shop\classes;
+
+use bizHd\base\classes\BaseClass;
+use bizHd\shop\classes\MainInviteCommissionClass;
+use common\components\dict;
+use Yii;
+
+class MainInviteClass extends BaseClass
+{
+
+    public static $baseFile = '\bizHd\shop\models\MainInvite';
+
+    /**
+     * 按中央 id 取邀请主数据,无记录返回 null
+     */
+    public static function getByMainId($mainId)
+    {
+        if (empty($mainId)) {
+            return null;
+        }
+        return self::getByCondition(['mainId' => (int)$mainId], false);
+    }
+
+    /**
+     * 按邀请码查 xhMainInvite
+     */
+    public static function getByInviteCode($inviteCode)
+    {
+        $code = trim((string)$inviteCode);
+        if ($code === '') {
+            return null;
+        }
+        return self::getByCondition(['inviteCode' => $code], false);
+    }
+
+    /**
+     * 花店注册页:会员价与邀请减免(dict + 可选邀请码)
+     */
+    public static function resolveRegisterFeePreview($inviteCode = '')
+    {
+        $config = dict::getDict('hdRegisterConfig');
+        $price = round((float)($config['price'] ?? 0), 2);
+        $configDiscount = round((float)($config['hdDiscountAmount'] ?? 0), 2);
+
+        $out = [
+            'price' => $price,
+            'memberTypeName' => '年度会员',
+            'inviteCode' => trim((string)$inviteCode),
+            'inviteValid' => 0,
+            'inviteNotFound' => 0,
+            'hdDiscountAmount' => 0,
+            'showInviteDiscount' => 0,
+            'payAmount' => $price,
+        ];
+
+        $code = trim((string)$inviteCode);
+        if ($code === '') {
+            return $out;
+        }
+
+        $invite = self::getByInviteCode($code);
+        if (empty($invite)) {
+            $out['inviteNotFound'] = 1;
+            return $out;
+        }
+
+        $out['inviteValid'] = 1;
+        $comm = (float)($invite['commissionAmount'] ?? 0);
+        $rowDiscount = (float)($invite['hdDiscountAmount'] ?? 0);
+
+// 记录上 commissionAmount、hdDiscountAmount 均大于 0 时用记录优惠;否则 hdDiscountAmount 为空/0 时用 dict 默认
+        if ($comm > 0 && $rowDiscount > 0) {
+            $discount = round($rowDiscount, 2);
+        } else {
+            $discount = $configDiscount;
+        }
+
+        if ($discount > 0) {
+            $out['hdDiscountAmount'] = $discount;
+            $out['showInviteDiscount'] = 1;
+            $out['payAmount'] = round(max(0, $price - $discount), 2);
+        }
+
+        return $out;
+    }
+
+    /**
+     * 注册提交:费用 + 邀请人 mainId + 佣金快照
+     */
+    public static function resolveRegisterFeeSubmit($inviteCode = '')
+    {
+        $preview = self::resolveRegisterFeePreview($inviteCode);
+        $config = dict::getDict('hdRegisterConfig');
+        $configComm = round((float) ($config['commissionAmount'] ?? 0), 2);
+
+        $preview['originalPrice'] = $preview['price'];
+        $preview['actPrice'] = $preview['payAmount'];
+        $preview['inviterMainId'] = 0;
+        $preview['inviteCommissionAmount'] = 0;
+
+        if (!empty($preview['inviteNotFound'])) {
+            return $preview;
+        }
+        if (empty($preview['inviteValid'])) {
+            return $preview;
+        }
+
+        $invite = self::getByInviteCode($inviteCode);
+        if (empty($invite)) {
+            return $preview;
+        }
+        $preview['inviterMainId'] = (int) ($invite['mainId'] ?? 0);
+        $comm = (float) ($invite['commissionAmount'] ?? 0);
+        $rowDiscount = (float) ($invite['hdDiscountAmount'] ?? 0);
+        if ($comm > 0 && $rowDiscount > 0) {
+            $preview['inviteCommissionAmount'] = round($comm, 2);
+        } elseif ($preview['inviterMainId'] > 0) {
+            $preview['inviteCommissionAmount'] = $configComm;
+        }
+
+        return $preview;
+    }
+
+    /**
+     * 新花店邀请码:当前最大数值码 + 10
+     */
+    public static function generateNextInviteCode()
+    {
+        $table = \bizHd\shop\models\MainInvite::tableName();
+        $sql = "SELECT MAX(CAST(`inviteCode` AS UNSIGNED)) FROM `{$table}` WHERE `inviteCode` REGEXP '^[0-9]+$'";
+        $max = (int) Yii::$app->db->createCommand($sql)->queryScalar();
+        if ($max <= 0) {
+            $max = 100000;
+        }
+        return (string) ($max + 10);
+    }
+
+    /**
+     * 支付成功后为受邀花店创建 xhMainInvite
+     */
+    public static function createInviteeRecord($mainId, $inviterMainId = 0)
+    {
+        $mainId = (int) $mainId;
+        if ($mainId <= 0) {
+            return null;
+        }
+        if (!empty(self::getByMainId($mainId))) {
+            return self::getByMainId($mainId);
+        }
+        $config = dict::getDict('hdRegisterConfig');
+        return self::add([
+            'mainId' => $mainId,
+            'inviteCode' => self::generateNextInviteCode(),
+            'inviterId' => (int) $inviterMainId,
+            'commissionAmount' => round((float) ($config['commissionAmount'] ?? 0), 2),
+            'hdDiscountAmount' => round((float) ($config['hdDiscountAmount'] ?? 0), 2),
+            'inviteTime' => date('Y-m-d H:i:s'),
+        ], false);
+    }
+
+    /**
+     * 根据邀请码解析邀请人 mainId(xhApply 不落库 inviterMainId)
+     */
+    public static function resolveInviterMainIdByInviteCode($inviteCode)
+    {
+        $inviteCode = trim((string) $inviteCode);
+        if ($inviteCode === '') {
+            return 0;
+        }
+        $invite = self::getByInviteCode($inviteCode);
+        return (int) ($invite['mainId'] ?? 0);
+    }
+
+    /**
+     * 注册支付成功:写邀请人佣金流水并累加 inviteCount
+     *
+     * @param float|null $prePrice 会员原价(xhRenew.prePrice)
+     * @param float|null $actPrice 实付(xhRenew.actPrice)
+     */
+    public static function grantInviterCommission($apply, $inviteeMainId, $inviteeShopName, $inviteeMobile, $prePrice = null, $actPrice = null)
+    {
+        $inviterMainId = self::resolveInviterMainIdByInviteCode($apply['inviteCode'] ?? '');
+        $commissionAmount = round((float) ($apply['inviteCommissionAmount'] ?? 0), 2);
+        if ($inviterMainId <= 0 || $commissionAmount <= 0) {
+            return;
+        }
+
+        $inviter = self::getByMainId($inviterMainId);
+        if (empty($inviter)) {
+            return;
+        }
+
+        $applyId = (int) ($apply['id'] ?? 0);
+        $commissionSn = 'FY' . date('YmdHis') . $applyId;
+
+        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);
+
+        $inviteCount = (int) ($inviter['inviteCount'] ?? 0) + 1;
+        $totalCommission = round((float) ($inviter['totalCommission'] ?? 0) + $commissionAmount, 2);
+        self::updateById((int) $inviter['id'], [
+            'inviteCount' => $inviteCount,
+            'totalCommission' => $totalCommission,
+        ]);
+    }
+
+}

+ 384 - 0
biz-hd/shop/classes/MainInviteCommissionClass.php

@@ -0,0 +1,384 @@
+<?php
+/**
+ * 门店邀请分佣流水 xhMainInviteCommission:列表、汇总、格式化
+ */
+namespace bizHd\shop\classes;
+
+use biz\shop\classes\ShopClass;
+use bizHd\base\classes\BaseClass;
+use common\components\dateUtil;
+use Yii;
+
+class MainInviteCommissionClass extends BaseClass
+{
+
+    public static $baseFile = '\bizHd\shop\models\MainInviteCommission';
+
+    /** payTime 无效视为待缴费 */
+    public static function isPendingPay($payTime)
+    {
+        if ($payTime === null || $payTime === '') {
+            return true;
+        }
+        $str = (string) $payTime;
+        if (strpos($str, '0000-00-00') === 0) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * 最近分佣记录
+     */
+    public static function getRecentList($inviterMainId, $limit = 5)
+    {
+        if (empty($inviterMainId)) {
+            return [];
+        }
+        return self::getLimitList('*', [
+            'inviterMainId' => (int) $inviterMainId,
+        ], (int) $limit, 'addTime DESC,id DESC');
+    }
+
+    /**
+     * 分佣明细分页(默认近30天,按 addTime 筛选)
+     */
+    public static function getCommissionList($inviterMainId, $settleStatus = null)
+    {
+        $where = self::buildCommissionListWhere($inviterMainId, $settleStatus);
+        return self::getList('*', $where, 'addTime DESC,id DESC');
+    }
+
+    /**
+     * 佣金明细列表查询条件:inviterMainId + 可选结算状态 + 时间
+     */
+    protected static function buildCommissionListWhere($inviterMainId, $settleStatus = null)
+    {
+        $where = ['inviterMainId' => (int) $inviterMainId];
+        if ($settleStatus !== null && $settleStatus !== '') {
+            $where['settleStatus'] = (int) $settleStatus;
+        }
+        $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['addTime'] = ['between', [$period['startTime'], $period['endTime']]];
+            }
+        }
+        return $where;
+    }
+
+    /**
+     * 邀约花店列表:按 inviterMainId 分页查 xhMainInviteCommission
+     */
+    public static function getInviteShopList($inviterMainId, $keyword = '')
+    {
+        $mainId = (int) $inviterMainId;
+        if ($mainId <= 0) {
+            return [
+                'list' => [],
+                'totalNum' => 0,
+                'totalPage' => 0,
+                'moreData' => 0,
+            ];
+        }
+
+        $keyword = trim((string) $keyword);
+        if ($keyword === '') {
+            $result = self::getList('*', ['inviterMainId' => $mainId], 'addTime DESC,id DESC');
+        } else {
+            // 店名 / 冗余字段 inviteeMobile 模糊匹配
+            $result = self::getInviteShopListByKeyword($mainId, $keyword);
+        }
+        $rows = $result['list'] ?? [];
+        $list = [];
+        foreach ($rows as $row) {
+            $list[] = self::formatInviteShopListItem($row);
+        }
+        $result['list'] = $list;
+        return $result;
+    }
+
+    /**
+     * 关键词搜索邀约列表(店名 OR 零售店手机号),分页与 getList 一致
+     */
+    protected static function getInviteShopListByKeyword($inviterMainId, $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 = self::getModel();
+        $query = $model->conditionQuery(['inviterMainId' => (int) $inviterMainId]);
+        $query->andWhere([
+            'or',
+            ['like', 'inviteeShopName', $keyword],
+            ['like', 'inviteeMobile', $keyword],
+        ]);
+
+        $clone = clone $query;
+        $count = (int) $clone->count();
+        $totalPage = $pageSize > 0 ? (int) ceil($count / $pageSize) : 0;
+        $offset = ($page - 1) * $pageSize;
+        $rows = $query
+            ->select('*')
+            ->orderBy('addTime DESC,id DESC')
+            ->offset($offset)
+            ->limit($pageSize)
+            ->asArray()
+            ->all();
+
+        return [
+            'list' => $rows,
+            'totalNum' => $count,
+            'totalPage' => $totalPage,
+            'moreData' => $page < $totalPage ? 1 : 0,
+        ];
+    }
+
+    /**
+     * 受邀人手机号:优先读表冗余 inviteeMobile,空则回退 xhShop.mobile
+     */
+    protected static function resolveInviteeMobile($row)
+    {
+        $mobile = trim((string) ($row['inviteeMobile'] ?? ''));
+        if ($mobile !== '') {
+            return $mobile;
+        }
+        $lsShopId = (int) ($row['lsShopId'] ?? 0);
+        if ($lsShopId <= 0) {
+            return '';
+        }
+        $shop = ShopClass::getById($lsShopId, false);
+        return is_array($shop) ? trim((string) ($shop['mobile'] ?? '')) : '';
+    }
+
+    /**
+     * 邀约花店列表单行(与 hdApp 卡片字段一致)
+     */
+    public static function formatInviteShopListItem($row)
+    {
+        $lsShopId = (int) ($row['lsShopId'] ?? 0);
+        $shop = [];
+        if ($lsShopId > 0) {
+            $shop = ShopClass::getById($lsShopId, false) ?: [];
+        }
+        $shopName = trim((string) ($row['inviteeShopName'] ?? ''));
+        if ($shopName === '') {
+            $shopName = trim((string) ($row['lsShopName'] ?? ''));
+        }
+        $mobile = self::resolveInviteeMobile($row);
+
+        $pendingPay = self::isPendingPay($row['payTime'] ?? '');
+        $memberStatus = $pendingPay ? 'pending' : 'opened';
+        if ((int) ($row['settleStatus'] ?? 0) === 2) {
+            $memberStatus = 'refund';
+        }
+
+        $openTime = $row['payTime'] ?? '';
+        if (self::isPendingPay($openTime)) {
+            $openTime = $row['addTime'] ?? '';
+        }
+
+        $fee = (float) ($row['memberPayAmount'] ?? 0);
+        if ($fee <= 0) {
+            $fee = (float) ($row['amount'] ?? 0);
+        }
+
+        return [
+            'id' => $row['id'] ?? 0,
+            'lsShopId' => $lsShopId,
+            'shopName' => $shopName,
+            'shopImg' => self::resolveShopImg($shop),
+            'mobile' => $mobile !== '' ? $mobile : '-',
+            'openTime' => $openTime,
+            'inviteTime' => $row['addTime'] ?? '',
+            'memberFee' => round($fee, 2),
+            'earnedCommission' => round((float) ($row['commissionAmount'] ?? 0), 2),
+            'memberStatus' => $memberStatus,
+            'memberStatusText' => self::memberStatusText($memberStatus),
+        ];
+    }
+
+    protected static function memberStatusText($status)
+    {
+        if ($status === 'pending') {
+            return '待缴费';
+        }
+        if ($status === 'refund') {
+            return '已退款';
+        }
+        return '已开通';
+    }
+
+    public static function maskMobile($mobile)
+    {
+        $mobile = trim((string) $mobile);
+        if (strlen($mobile) < 7) {
+            return $mobile ?: '-';
+        }
+        return substr($mobile, 0, 3) . '****' . substr($mobile, -4);
+    }
+
+    public static function maskInviteCode($code)
+    {
+        $code = trim((string) $code);
+        if (strlen($code) === 11 && ctype_digit($code)) {
+            return self::maskMobile($code);
+        }
+        if (strlen($code) > 7) {
+            return substr($code, 0, 3) . '****' . substr($code, -4);
+        }
+        return $code;
+    }
+
+    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;
+    }
+
+    /**
+     * 佣金变动明细单行(hdApp shopInviteCommissionList 设计稿)
+     */
+    public static function formatCommissionListItem($row)
+    {
+        $lsShopId = (int) ($row['lsShopId'] ?? 0);
+        $shop = [];
+        if ($lsShopId > 0) {
+            $shop = ShopClass::getById($lsShopId, false) ?: [];
+        }
+        $shopName = trim((string) ($row['inviteeShopName'] ?? ''));
+        if ($shopName === '') {
+            $shopName = trim((string) ($row['lsShopName'] ?? ''));
+        }
+        $mobile = self::resolveInviteeMobile($row);
+        $mobileMask = $mobile !== '' ? self::maskMobile($mobile) : '-';
+
+        $recordType = self::resolveCommissionRecordType($row);
+        if ($recordType === 'deposit') {
+            $orderSnLabel = '存入编号';
+            $baseAmountLabel = '存余额金额';
+        } else {
+            $orderSnLabel = '会员订单号';
+            $baseAmountLabel = '会员实付金额';
+        }
+
+        $orderSn = trim((string) ($row['commissionSn'] ?? ''));
+        if ($orderSn === '' && !empty($row['relateOrderId'])) {
+            $orderSn = (string) $row['relateOrderId'];
+        }
+
+        $baseAmount = (float) ($row['memberPayAmount'] ?? 0);
+        if ($baseAmount <= 0) {
+            $baseAmount = (float) ($row['amount'] ?? 0);
+        }
+        if ($recordType === 'deposit' && $baseAmount <= 0) {
+            $baseAmount = (float) ($row['commissionAmount'] ?? 0);
+        }
+
+        $settleStatus = (int) ($row['settleStatus'] ?? 0);
+        $commissionAmount = round(abs((float) ($row['commissionAmount'] ?? 0)), 2);
+        // 失效/扣回展示负号
+        $commissionSign = ($settleStatus === 2) ? -1 : 1;
+        if ((float) ($row['commissionAmount'] ?? 0) < 0) {
+            $commissionSign = -1;
+        }
+
+        $createTime = trim((string) ($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' => round($baseAmount, 2),
+            'createTime' => $createTime !== '' ? $createTime : '-',
+            'commissionAmount' => $commissionAmount,
+            'commissionSign' => $commissionSign,
+            'recordType' => $recordType,
+            'settleStatus' => $settleStatus,
+        ];
+    }
+
+    /**
+     * 明细类型:会员开通分佣 vs 佣金存入余额(按快照字段推断)
+     */
+    protected static function resolveCommissionRecordType($row)
+    {
+        $typeName = trim((string) ($row['memberTypeName'] ?? ''));
+        if ($typeName !== '' && mb_strpos($typeName, '存入') !== false) {
+            return 'deposit';
+        }
+        $sn = trim((string) ($row['commissionSn'] ?? ''));
+        if ($sn !== '' && (strpos($sn, 'D') === 0 || strpos($sn, 'CR') === 0)) {
+            return 'deposit';
+        }
+        return 'member';
+    }
+
+    /**
+     * 格式化最近佣金行(供 hub 最近邀请)
+     */
+    public static function formatRecentRow($row)
+    {
+        $settleStatus = (int) ($row['settleStatus'] ?? 0);
+        $statusText = '待结算';
+        if ($settleStatus === 1) {
+            $statusText = '已结算';
+        } elseif ($settleStatus === 2) {
+            $statusText = '已失效';
+        }
+        $shopName = trim((string) ($row['inviteeShopName'] ?? ''));
+        if ($shopName === '') {
+            $shopName = trim((string) ($row['lsShopName'] ?? ''));
+        }
+        $mobile = self::resolveInviteeMobile($row);
+        $payTime = $row['payTime'] ?? '';
+        $openTime = $payTime;
+        if (self::isPendingPay($payTime)) {
+            $openTime = $row['addTime'] ?? '';
+        }
+        if (!empty($openTime) && strlen($openTime) >= 10) {
+            $openTime = substr($openTime, 0, 10);
+        }
+        $time = $row['settleTime'] ?? ($row['addTime'] ?? '');
+        if (!empty($time) && strlen($time) >= 10) {
+            $time = substr($time, 0, 10);
+        }
+        $mobileDisplay = $mobile !== '' ? $mobile : '-';
+        return [
+            'id' => $row['id'] ?? 0,
+            'shopName' => $shopName,
+            'commissionSn' => $row['commissionSn'] ?? '',
+            'orderSnLabel' => $row['commissionSn'] ?? '',
+            'commissionAmount' => round((float) ($row['commissionAmount'] ?? 0), 2),
+            'settleStatus' => $settleStatus,
+            'statusText' => $statusText,
+            'settleTime' => $time,
+            'mobile' => $mobileDisplay,
+            'mobileMask' => $mobileDisplay,
+            'openTime' => $openTime ?: '-',
+        ];
+    }
+
+}

+ 51 - 0
biz-hd/shop/classes/MainWalletChangeClass.php

@@ -0,0 +1,51 @@
+<?php
+/**
+ * 中央钱包流水业务类
+ * 职责:分页列表、区间内充值/消费汇总(供 hdApp 变动明细顶栏)
+ */
+namespace bizHd\shop\classes;
+
+use bizHd\base\classes\BaseClass;
+
+class MainWalletChangeClass extends BaseClass
+{
+
+    public static $baseFile = '\bizHd\shop\models\MainWalletChange';
+
+    /**
+     * 分页变动列表
+     * @param array $where 须含 mainId;可选 addTime between、capitalType、io
+     */
+    public static function getChangeList($where)
+    {
+        return self::getList('*', $where, 'addTime DESC,id DESC');
+    }
+
+    /**
+     * 同一筛选条件下汇总充值(io=1)、消费(io=0)金额
+     * @param array $where 与列表一致,不含 io
+     * @return array{recharge: float, consume: float}
+     */
+    public static function getSummaryAmount($where)
+    {
+        $rechargeWhere = $where;
+        $rechargeWhere['io'] = 1;
+        $consumeWhere = $where;
+        $consumeWhere['io'] = 0;
+        return [
+            'recharge' => round((float) self::sumAmountByWhere($rechargeWhere), 2),
+            'consume' => round((float) self::sumAmountByWhere($consumeWhere), 2),
+        ];
+    }
+
+    /**
+     * 按条件 sum amount
+     */
+    protected static function sumAmountByWhere($where)
+    {
+        $model = self::getModel();
+        $query = $model->conditionQuery($where);
+        return $query->sum('amount') ?: 0;
+    }
+
+}

+ 17 - 0
biz-hd/shop/models/MainInvite.php

@@ -0,0 +1,17 @@
+<?php
+/**
+ * 门店邀请主数据 xhMainInvite
+ */
+namespace bizHd\shop\models;
+
+use bizHd\base\models\Base;
+
+class MainInvite extends Base
+{
+
+    public static function tableName()
+    {
+        return 'xhMainInvite';
+    }
+
+}

+ 17 - 0
biz-hd/shop/models/MainInviteCommission.php

@@ -0,0 +1,17 @@
+<?php
+/**
+ * 门店邀请分佣流水 xhMainInviteCommission
+ * 快照字段含 inviteeShopName、inviteeMobile 等,写入分佣时需一并落库
+ */namespace bizHd\shop\models;
+
+use bizHd\base\models\Base;
+
+class MainInviteCommission extends Base
+{
+
+    public static function tableName()
+    {
+        return 'xhMainInviteCommission';
+    }
+
+}

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

@@ -0,0 +1,18 @@
+<?php
+/**
+ * 中央钱包变动记录 xhMainWalletChange
+ * 用途:hdApp 可用余额变动明细列表数据源
+ */
+namespace bizHd\shop\models;
+
+use bizHd\base\models\Base;
+
+class MainWalletChange extends Base
+{
+
+    public static function tableName()
+    {
+        return 'xhMainWalletChange';
+    }
+
+}

+ 26 - 1
biz/renew/classes/RenewClass.php

@@ -5,6 +5,8 @@ namespace biz\renew\classes;
 use biz\base\classes\BaseClass;
 use biz\shop\classes\ShopClass;
 use biz\sj\classes\SjClass;
+use bizHd\saas\classes\ApplyClass;
+use bizHd\shop\classes\MainInviteClass;
 use common\components\noticeUtil;
 use common\components\util;
 use Yii;
@@ -39,6 +41,13 @@ class RenewClass extends BaseClass
             util::fail($msg);
         }
 
+        $attachParams = [];
+        if (!empty($attach)) {
+            parse_str($attach, $attachParams);
+        }
+        $applyId = (int) ($attachParams['applyId'] ?? 0);
+        $isRegisterMember = $applyId > 0;
+
         $shopId = $renew->shopId ?? 0;
         $shop = ShopClass::getById($shopId, true);
         if (empty($shop)) {
@@ -74,7 +83,23 @@ class RenewClass extends BaseClass
         $renew->payTime = date("Y-m-d H:i:s");
         $renew->status = 1;
         $renew->save();
+
+        if ($isRegisterMember) {
+            $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)
+                );
+            }
+        }
+
+        return true;
     }
 
 
-}
+}

+ 7 - 0
common/components/dict.php

@@ -511,6 +511,8 @@ class dict
             'customRechargeRefund' => ['id' => 82, 'name' => 'customRechargeRefund'],
             // 商城分红存入花店余额(xhDistributionDepositLog)
             'distributionDeposit' => ['id' => 83, 'name' => 'distributionDeposit'],
+            // 花店注册年度会员支付
+            'hdRegisterOrder' => ['id' => 84, 'name' => 'hdRegisterOrder'],
         ],
         "capitalTypeList" => [//流水类型的对应链接,后台收支明细查看时跳转的链接
             0 => ['link' => '/capital/order-detail', 'name' => '网店', 'orderLink' => '/order/detail', 'id' => 0,],
@@ -622,6 +624,11 @@ class dict
             ['id' => 4, 'name' => '房租'],
             ['id' => 5, 'name' => '其它'],
         ],
+        'hdRegisterConfig' => [
+            'price' => 1, //花店开通费用
+            'commissionAmount' => 88.00, //默认分佣金额 (默认)
+            'hdDiscountAmount' => 88.00 //花店优惠金额 (默认)
+        ]
     ];
 
     //取配置文件的值

+ 16 - 0
common/components/orderSn.php

@@ -626,4 +626,20 @@ class orderSn
         return $prefix . $id;
     }
 
+    /**
+     * 花店注册付费订单号
+     */
+    public static function getHdRegisterSn($applyId)
+    {
+        $prefix = 'HR_CS';
+        if (getenv('YII_ENV', 'local') == 'production') {
+            $prefix = 'HR';
+        }
+        $applyId = (int) $applyId;
+        if ($applyId <= 0) {
+            util::fail('申请 id 无效');
+        }
+        return $prefix . date('ymdHis') . $applyId;
+    }
+
 }