Parcourir la source

花掌柜、零售端 新增分销功能接口

ouyang il y a 1 semaine
Parent
commit
0934414d92

+ 153 - 0
app-hd/controllers/DistributionController.php

@@ -0,0 +1,153 @@
+<?php
+
+namespace hd\controllers;
+
+use bizHd\distribution\classes\DistributionRuleClass;
+use bizHd\distribution\classes\DistributionUserClass;
+use bizHd\distribution\classes\DistributionFlowClass;
+use bizHd\distribution\classes\DistributionOrderClass;
+use bizHd\distribution\classes\DistributionReportClass;
+use Yii;
+use common\components\util;
+
+/**
+ * 分销(hdApp 门店设置-分销规则 / 客户详情-分销统计)
+ */
+class DistributionController extends BaseController
+{
+    /**
+     * 获取当前门店分销规则
+     */
+    public function actionGetRule()
+    {
+        try {
+            $data = DistributionRuleClass::getRule($this->shopId, $this->mainId);
+            util::success($data);
+        } catch (\Exception $e) {
+            util::fail($e->getMessage());
+        }
+    }
+
+    /**
+     * 保存分销规则
+     */
+    public function actionSaveRule()
+    {
+        $post = Yii::$app->request->post();
+        try {
+            DistributionRuleClass::saveRule($this->shopId, $this->mainId, $post);
+            util::complete('保存成功');
+        } catch (\Exception $e) {
+            util::fail($e->getMessage());
+        }
+    }
+
+    /**
+     * 客户分销统计(客户详情页)
+     */
+    public function actionGetUserStat()
+    {
+        $customId = (int)Yii::$app->request->get('customId', 0);
+        try {
+            $data = DistributionUserClass::getUserStat($this->shopId, $customId);
+            util::success($data);
+        } catch (\Exception $e) {
+            util::fail($e->getMessage());
+        }
+    }
+
+    /**
+     * 客户分红变动明细(xhDistributionFlow)
+     * customId=0 且 scope=shop 时查门店全部流水
+     */
+    public function actionGetFlowList()
+    {
+        $customId = (int)Yii::$app->request->get('customId', 0);
+        $scope = trim(Yii::$app->request->get('scope', ''));
+        if ($scope === 'shop') {
+            $customId = 0;
+        }
+        try {
+            $data = DistributionFlowClass::getFlowList($this->shopId, $customId);
+            util::success($data);
+        } catch (\Exception $e) {
+            util::fail($e->getMessage());
+        }
+    }
+
+    /**
+     * 拉新客户列表(xhDistributionUser)
+     */
+    public function actionGetInviteList()
+    {
+        $customId = (int)Yii::$app->request->get('customId', 0);
+        try {
+            $data = DistributionUserClass::getInviteList($this->shopId, $customId);
+            util::success($data);
+        } catch (\Exception $e) {
+            util::fail($e->getMessage());
+        }
+    }
+
+    /**
+     * 下线贡献分红明细(xhDistributionOrder)
+     * distId/customId 可选;scope=shop 时查门店全部订单
+     */
+    public function actionGetContribOrderList()
+    {
+        $scope = trim(Yii::$app->request->get('scope', ''));
+        $distId = (int)Yii::$app->request->get('distId', 0);
+        if ($distId <= 0) {
+            $distId = (int)Yii::$app->request->get('customId', 0);
+        }
+        if ($scope === 'shop') {
+            $distId = 0;
+        }
+        $buyerId = (int)Yii::$app->request->get('buyerId', 0);
+        try {
+            $data = DistributionOrderClass::getContribOrderList($this->shopId, $distId, $buyerId);
+            util::success($data);
+        } catch (\Exception $e) {
+            util::fail($e->getMessage());
+        }
+    }
+
+    /**
+     * 门店分销报表汇总
+     */
+    public function actionGetReportStat()
+    {
+        try {
+            $data = DistributionReportClass::getReportStat($this->shopId);
+            util::success($data);
+        } catch (\Exception $e) {
+            util::fail($e->getMessage());
+        }
+    }
+
+    /**
+     * 门店获佣人数明细
+     */
+    public function actionGetShopDistList()
+    {
+        try {
+            $data = DistributionUserClass::getShopDistList($this->shopId);
+            util::success($data);
+        } catch (\Exception $e) {
+            util::fail($e->getMessage());
+        }
+    }
+
+    /**
+     * 门店拉新明细
+     */
+    public function actionGetShopInviteList()
+    {
+        try {
+            $data = DistributionUserClass::getShopInviteList($this->shopId);
+            util::success($data);
+        } catch (\Exception $e) {
+            util::fail($e->getMessage());
+        }
+    }
+}

+ 81 - 0
app-mall/controllers/DistributionController.php

@@ -0,0 +1,81 @@
+<?php
+/**
+ * 商城 C 端分销接口
+ * 用途:mallApp 我的分红页
+ */
+
+namespace mall\controllers;
+
+use bizHd\distribution\classes\DistributionFlowClass;
+use bizHd\distribution\classes\DistributionOrderClass;
+use bizHd\distribution\classes\DistributionUserClass;
+use common\components\util;
+use Yii;
+
+class DistributionController extends BaseController
+{
+    /**
+     * 我的分红汇总
+     */
+    public function actionGetMyStat()
+    {
+        if (empty($this->customId)) {
+            util::fail('请先选择花店');
+        }
+        try {
+            $data = DistributionUserClass::getMallMyStat($this->shopId, $this->customId);
+            util::success($data);
+        } catch (\Exception $e) {
+            util::fail($e->getMessage());
+        }
+    }
+
+    /**
+     * 分红变动明细
+     */
+    public function actionGetFlowList()
+    {
+        if (empty($this->customId)) {
+            util::fail('请先选择花店');
+        }
+        try {
+            $data = DistributionFlowClass::getFlowList($this->shopId, $this->customId);
+            util::success($data);
+        } catch (\Exception $e) {
+            util::fail($e->getMessage());
+        }
+    }
+
+    /**
+     * 拉新客户列表(当前登录客户为邀请人)
+     */
+    public function actionGetInviteList()
+    {
+        if (empty($this->customId)) {
+            util::fail('请先选择花店');
+        }
+        try {
+            $data = DistributionUserClass::getInviteList($this->shopId, $this->customId);
+            util::success($data);
+        } catch (\Exception $e) {
+            util::fail($e->getMessage());
+        }
+    }
+
+    /**
+     * 下线贡献分红明细(xhDistributionOrder)
+     */
+    public function actionGetContribOrderList()
+    {
+        if (empty($this->customId)) {
+            util::fail('请先选择花店');
+        }
+        $buyerId = (int)Yii::$app->request->get('buyerId', 0);
+        try {
+            $data = DistributionOrderClass::getContribOrderList($this->shopId, $this->customId, $buyerId);
+            util::success($data);
+        } catch (\Exception $e) {
+            util::fail($e->getMessage());
+        }
+    }
+}

+ 235 - 0
biz-hd/distribution/classes/DistributionFlowClass.php

@@ -0,0 +1,235 @@
+<?php
+/**
+ * 用途:分销流水 xhDistributionFlow 查询与展示格式化
+ * 谁用:hdApp 客户详情-分红变动明细
+ */
+
+namespace bizHd\distribution\classes;
+
+use bizHd\base\classes\BaseClass;
+use bizHd\custom\classes\CustomClass;
+use common\components\dateUtil;
+use Yii;
+use yii\db\Query;
+
+class DistributionFlowClass extends BaseClass
+{
+    public static $baseFile = '\bizHd\distribution\models\DistributionFlow';
+
+    /**
+     * 客户分红流水列表(分页,支持按月份筛选)
+     * @param int $shopId
+     * @param int $customId xhCustom.id
+     * @return array
+     * @throws \Exception
+     */
+    public static function getFlowList($shopId, $customId)
+    {
+        $customId = (int)$customId;
+        if ($customId < 0) {
+            throw new \Exception('客户ID无效');
+        }
+
+        $where = self::buildFlowWhere($shopId, $customId);
+        $data = self::getList('*', $where, 'flowTime DESC, id DESC');
+
+        $customIds = [];
+        foreach ($data['list'] as $row) {
+            $cid = (int)($row['xhCustomId'] ?? 0);
+            if ($cid > 0) {
+                $customIds[] = $cid;
+            }
+        }
+        $customMap = [];
+        if (!empty($customIds)) {
+            foreach (CustomClass::getCustomByIds(array_values(array_unique($customIds))) as $customItem) {
+                $customMap[(int)$customItem['id']] = $customItem;
+            }
+        }
+
+        $list = [];
+        foreach ($data['list'] as $row) {
+            $item = self::formatFlowRow($row);
+            $cid = (int)($row['xhCustomId'] ?? 0);
+            $custom = $customMap[$cid] ?? [];
+            $item['distName'] = $custom['name'] ?? '';
+            $item['distMobile'] = $custom['mobile'] ?? '';
+            $item['distDisplay'] = self::formatDistDisplay($custom);
+            $list[] = $item;
+        }
+        $data['list'] = $list;
+        $data['monthTotal'] = (int)self::getCount($where);
+        return $data;
+    }
+
+    /** 获佣人展示:优先姓名,无则手机尾号 */
+    protected static function formatDistDisplay($custom)
+    {
+        if (!empty($custom['name'])) {
+            return $custom['name'];
+        }
+        $mobile = isset($custom['mobile']) ? trim($custom['mobile']) : '';
+        if (strlen($mobile) >= 4) {
+            return '尾号' . substr($mobile, -4);
+        }
+        return '-';
+    }
+
+    /**
+     * 组装查询条件(门店 + 客户 + 可选月份)
+     */
+    protected static function buildFlowWhere($shopId, $customId)
+    {
+        $where = [
+            'shopId' => (int)$shopId,
+        ];
+        if ($customId > 0) {
+            $where['xhCustomId'] = (int)$customId;
+        }
+
+        $get = Yii::$app->request->get();
+        $flowType = isset($get['flowType']) ? trim($get['flowType']) : '';
+        if ($flowType !== '' && $flowType !== 'all') {
+            $where['flowType'] = (int)$flowType;
+        }
+        $searchTime = isset($get['searchTime']) ? trim($get['searchTime']) : '';
+        $startTime = isset($get['startTime']) ? trim($get['startTime']) : '';
+        $endTime = isset($get['endTime']) ? trim($get['endTime']) : '';
+
+        if ($searchTime === '') {
+            // 默认当前月
+            $searchTime = 'thisMonth';
+        }
+
+        if ($searchTime !== 'all') {
+            $period = dateUtil::formatTime($searchTime, $startTime, $endTime);
+            if (!empty($period['startTime']) && !empty($period['endTime'])) {
+                $where['flowTime'] = ['between', [$period['startTime'], $period['endTime']]];
+            }
+        }
+
+        // 查看某下线客户的贡献分红流水(仅指定分销员时)
+        $buyerId = isset($get['buyerId']) ? (int)$get['buyerId'] : 0;
+        if ($customId > 0 && $buyerId > 0) {
+            $orderIds = (new Query())
+                ->select('id')
+                ->from('xhDistributionOrder')
+                ->where([
+                    'shopId' => (int)$shopId,
+                    'distId' => (int)$customId,
+                    'buyerId' => $buyerId,
+                ])
+                ->column();
+            $where['flowType'] = 1;
+            $where['refType'] = 1;
+            // conditionQuery 的 IN 须写成 ['in', [...]];无订单时用不可能命中的 id
+            $where['refId'] = !empty($orderIds) ? ['in', $orderIds] : -1;
+        }
+
+        return $where;
+    }
+
+    /**
+     * 格式化单条流水供前端展示
+     */
+    protected static function formatFlowRow($row)
+    {
+        $flowType = (int)$row['flowType'];
+        $flowStatus = (int)$row['flowStatus'];
+        $amount = round((float)$row['amount'], 2);
+        $goodsName = isset($row['remark']) ? trim($row['remark']) : '';
+
+        $item = [
+            'id' => (int)$row['id'],
+            'flowType' => $flowType,
+            'flowTypeName' => self::flowTypeName($flowType),
+            'amount' => $amount,
+            'amountText' => self::formatAmountText($flowType, $amount),
+            'refSn' => $row['refSn'],
+            'buyerName' => $row['buyerName'],
+            'goodsName' => $goodsName,
+            'subTitle' => self::buildSubTitle($flowType, $row['buyerName'], $goodsName),
+            'flowTime' => $row['flowTime'],
+            'flowStatus' => $flowStatus,
+            'statusText' => self::statusText($flowType, $flowStatus),
+            'statusClass' => self::statusClass($flowType, $flowStatus),
+        ];
+        return $item;
+    }
+
+    protected static function flowTypeName($flowType)
+    {
+        $map = [
+            1 => '订单分红',
+            2 => '分红存入',
+            3 => '退款扣回',
+        ];
+        return isset($map[$flowType]) ? $map[$flowType] : '分销流水';
+    }
+
+    protected static function formatAmountText($flowType, $amount)
+    {
+        $abs = number_format(abs($amount), 2, '.', '');
+        if ($flowType === 1) {
+            return '+¥' . $abs;
+        }
+        return '-¥' . $abs;
+    }
+
+    protected static function buildSubTitle($flowType, $buyerName, $goodsName)
+    {
+        if ($flowType !== 1) {
+            return '';
+        }
+        $buyerName = trim($buyerName);
+        $goodsName = trim($goodsName);
+        if ($buyerName !== '' && $goodsName !== '') {
+            return $buyerName . ' · ' . $goodsName;
+        }
+        return $buyerName !== '' ? $buyerName : $goodsName;
+    }
+
+    /**
+     * 状态文案:按 flowType 区分,避免分红存入误用订单分红的「待结算」
+     * flowType=1 订单分红:1待结算 2已结算
+     * flowType=2 分红存入:3存入成功(测试/默认写 1 时展示待存入)
+     * flowType=3 退款扣回:4已扣回
+     */
+    protected static function statusText($flowType, $flowStatus)
+    {
+        if ($flowType === 1) {
+            $map = [
+                1 => '待结算',
+                2 => '已结算',
+                4 => '已扣回',
+            ];
+        } elseif ($flowType === 2) {
+            $map = [
+                1 => '待存入',
+                3 => '存入成功',
+            ];
+        } elseif ($flowType === 3) {
+            $map = [
+                4 => '已扣回',
+            ];
+        } else {
+            $map = [];
+        }
+        return isset($map[$flowStatus]) ? $map[$flowStatus] : '';
+    }
+
+    protected static function statusClass($flowType, $flowStatus)
+    {
+        if ($flowType === 1 && $flowStatus === 1) {
+            return 'pending';
+        }
+        if ($flowType === 2 && $flowStatus === 1) {
+            return 'pending';
+        }
+        if (($flowType === 1 && $flowStatus === 2)
+            || ($flowType === 2 && $flowStatus === 3)) {
+            return 'success';
+        }
+        return 'default';
+    }
+}

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

@@ -0,0 +1,171 @@
+<?php
+/**
+ * 用途:订单分销记录业务(xhDistributionOrder)
+ * 谁用:拉新客户-查看贡献明细(分红明细页)
+ */
+
+namespace bizHd\distribution\classes;
+
+use bizHd\base\classes\BaseClass;
+use common\components\dateUtil;
+use Yii;
+use yii\db\Query;
+
+class DistributionOrderClass extends BaseClass
+{
+    public static $baseFile = '\bizHd\distribution\models\DistributionOrder';
+
+    /**
+     * 分销员分红订单列表(xhDistributionOrder)
+     * @param int $shopId
+     * @param int $distId 获佣分销员 xhCustom.id
+     * @param int $buyerId 下单客户 xhCustom.id,0 表示不限下线
+     * @return array
+     * @throws \Exception
+     */
+    public static function getContribOrderList($shopId, $distId, $buyerId = 0)
+    {
+        $shopId = (int)$shopId;
+        $distId = (int)$distId;
+        $buyerId = (int)$buyerId;
+        if ($distId < 0) {
+            throw new \Exception('参数无效');
+        }
+
+        $get = Yii::$app->request->get();
+        $settleStatus = isset($get['settleStatus']) ? trim($get['settleStatus']) : '';
+        $keyword = isset($get['keyword']) ? trim($get['keyword']) : '';
+        $searchTime = isset($get['searchTime']) ? trim($get['searchTime']) : 'thisMonth';
+        $startTime = isset($get['startTime']) ? trim($get['startTime']) : '';
+        $endTime = isset($get['endTime']) ? trim($get['endTime']) : '';
+
+        $period = dateUtil::formatTime($searchTime, $startTime, $endTime);
+
+        // 本月分红合计(同筛选月份,不受 settleStatus Tab 影响)
+        $monthQuery = (new Query())
+            ->from('xhDistributionOrder')
+            ->where(['shopId' => $shopId]);
+        if ($distId > 0) {
+            $monthQuery->andWhere(['distId' => $distId]);
+        }
+        if ($buyerId > 0) {
+            $monthQuery->andWhere(['buyerId' => $buyerId]);
+        }
+        if (!empty($period['startTime']) && !empty($period['endTime'])) {
+            $monthQuery->andWhere(['between', 'orderTime', $period['startTime'], $period['endTime']]);
+        }
+        $monthCommission = (float)$monthQuery->sum('commissionAmount');
+
+        // Base::conditionQuery 不支持 or 组合,关键词搜索改用 Query 分页
+        $page = isset($get['page']) ? max(1, (int)$get['page']) : 1;
+        $pageSize = isset($get['pageSize']) && (int)$get['pageSize'] > 0
+            ? (int)$get['pageSize']
+            : (int)Yii::$app->params['pageSize'];
+
+        $query = (new Query())
+            ->from('xhDistributionOrder')
+            ->where(['shopId' => $shopId]);
+        if ($distId > 0) {
+            $query->andWhere(['distId' => $distId]);
+        }
+        if ($buyerId > 0) {
+            $query->andWhere(['buyerId' => $buyerId]);
+        }
+        if ($settleStatus !== '' && $settleStatus !== 'all') {
+            $query->andWhere(['settleStatus' => (int)$settleStatus]);
+        }
+        if (!empty($period['startTime']) && !empty($period['endTime'])) {
+            $query->andWhere(['between', 'orderTime', $period['startTime'], $period['endTime']]);
+        }
+        if ($keyword !== '') {
+            $query->andWhere([
+                'or',
+                ['like', 'orderSn', $keyword],
+                ['like', 'buyerName', $keyword],
+                ['like', 'distName', $keyword],
+            ]);
+        }
+
+        $total = (int)(clone $query)->count('*', Yii::$app->db);
+        $totalPage = $pageSize > 0 ? (int)ceil($total / $pageSize) : 0;
+        $rows = $query
+            ->orderBy(['orderTime' => SORT_DESC, 'id' => SORT_DESC])
+            ->offset(($page - 1) * $pageSize)
+            ->limit($pageSize)
+            ->all(Yii::$app->db);
+
+        $list = [];
+        foreach ($rows as $row) {
+            $list[] = self::formatContribOrderRow($row);
+        }
+
+        return [
+            'list' => $list,
+            'totalNum' => $total,
+            'totalPage' => $totalPage,
+            'moreData' => $page < $totalPage ? 1 : 0,
+            'monthCommission' => round($monthCommission, 2),
+        ];
+    }
+
+    /**
+     * 格式化贡献订单行(金额字段与库表一致,另附展示用文案)
+     */
+    protected static function formatContribOrderRow($row)
+    {
+        $settleStatus = (int)$row['settleStatus'];
+        $commissionAmount = round((float)$row['commissionAmount'], 2);
+        $isSettled = $settleStatus === 1;
+
+        return [
+            'id' => (int)$row['id'],
+            'orderId' => (int)$row['orderId'],
+            'orderSn' => $row['orderSn'],
+            'buyerId' => (int)$row['buyerId'],
+            'buyerName' => $row['buyerName'],
+            'distId' => (int)$row['distId'],
+            'distName' => $row['distName'],
+            'orderAmount' => round((float)$row['orderAmount'], 2),
+            'payAmount' => round((float)$row['payAmount'], 2),
+            'sendCost' => round((float)$row['sendCost'], 2),
+            'commissionBase' => round((float)$row['commissionBase'], 2),
+            'commissionRate' => round((float)$row['commissionRate'], 2),
+            'commissionAmount' => $commissionAmount,
+            'bonusType' => (int)$row['bonusType'],
+            'bindDays' => (int)$row['bindDays'],
+            'settleStatus' => $settleStatus,
+            'orderTime' => $row['orderTime'],
+            'payTime' => $row['payTime'],
+            'finishTime' => $row['finishTime'],
+            'settleTime' => $row['settleTime'],
+            'invalidTime' => $row['invalidTime'],
+            'invalidReason' => $row['invalidReason'],
+            'amountText' => '+¥' . number_format($commissionAmount, 2, '.', ''),
+            'statusText' => self::settleStatusText($settleStatus),
+            'statusClass' => self::settleStatusClass($settleStatus),
+            'timeLabel' => $isSettled ? '结算时间' : '下单时间',
+            'timeValue' => $isSettled ? ($row['settleTime'] ?: $row['orderTime']) : $row['orderTime'],
+        ];
+    }
+
+    protected static function settleStatusText($settleStatus)
+    {
+        $map = [
+            0 => '待结算',
+            1 => '已结算',
+            2 => '已失效',
+        ];
+        return isset($map[$settleStatus]) ? $map[$settleStatus] : '';
+    }
+
+    protected static function settleStatusClass($settleStatus)
+    {
+        if ($settleStatus === 0) {
+            return 'pending';
+        }
+        if ($settleStatus === 1) {
+            return 'success';
+        }
+        return 'invalid';
+    }
+}

+ 91 - 0
biz-hd/distribution/classes/DistributionReportClass.php

@@ -0,0 +1,91 @@
+<?php
+/**
+ * 用途:门店分销报表统计(xhDistributionOrder / xhDistributionFlow / xhDistributionUser)
+ * 谁用:hdApp 应用-门店设置-分销报表
+ */
+
+namespace bizHd\distribution\classes;
+
+use common\components\dateUtil;
+use Yii;
+use yii\db\Query;
+
+class DistributionReportClass
+{
+    /**
+     * 门店分销报表汇总
+     * @param int $shopId
+     * @return array
+     */
+    public static function getReportStat($shopId)
+    {
+        $shopId = (int)$shopId;
+        $get = Yii::$app->request->get();
+        $searchTime = isset($get['searchTime']) ? trim($get['searchTime']) : 'today';
+        $startTime = isset($get['startTime']) ? trim($get['startTime']) : '';
+        $endTime = isset($get['endTime']) ? trim($get['endTime']) : '';
+
+        $period = dateUtil::formatTime($searchTime, $startTime, $endTime);
+        $timeBetween = null;
+        if (!empty($period['startTime']) && !empty($period['endTime'])) {
+            $timeBetween = [$period['startTime'], $period['endTime']];
+        }
+
+        $orderQuery = (new Query())
+            ->from('xhDistributionOrder')
+            ->where(['shopId' => $shopId]);
+        if ($timeBetween) {
+            $orderQuery->andWhere(['between', 'orderTime', $timeBetween[0], $timeBetween[1]]);
+        }
+
+        $orderCount = (int)(clone $orderQuery)->count('*', Yii::$app->db);
+        $commissionAmount = (float)(clone $orderQuery)->sum('commissionAmount');
+        $distUserCount = (int)(clone $orderQuery)
+            ->select('distId')
+            ->distinct()
+            ->count('distId', Yii::$app->db);
+
+        $inviteQuery = (new Query())
+            ->from('xhDistributionUser')
+            ->where(['shopId' => $shopId])
+            ->andWhere(['>', 'inviterId', 0]);
+        if ($timeBetween) {
+            $inviteQuery->andWhere(['between', 'inviteTime', $timeBetween[0], $timeBetween[1]]);
+        }
+        $inviteCount = (int)$inviteQuery->count('*', Yii::$app->db);
+
+        $pendingQuery = (new Query())
+            ->from('xhDistributionOrder')
+            ->where(['shopId' => $shopId, 'settleStatus' => 0]);
+        if ($timeBetween) {
+            $pendingQuery->andWhere(['between', 'orderTime', $timeBetween[0], $timeBetween[1]]);
+        }
+        $pendingAmount = (float)$pendingQuery->sum('commissionAmount');
+
+        $settledQuery = (new Query())
+            ->from('xhDistributionOrder')
+            ->where(['shopId' => $shopId, 'settleStatus' => 1]);
+        if ($timeBetween) {
+            $settledQuery->andWhere(['between', 'settleTime', $timeBetween[0], $timeBetween[1]]);
+        }
+        $settledAmount = (float)$settledQuery->sum('commissionAmount');
+
+        $depositQuery = (new Query())
+            ->from('xhDistributionFlow')
+            ->where(['shopId' => $shopId, 'flowType' => 2, 'flowStatus' => 3]);
+        if ($timeBetween) {
+            $depositQuery->andWhere(['between', 'flowTime', $timeBetween[0], $timeBetween[1]]);
+        }
+        $depositAmount = (float)$depositQuery->sum('amount');
+
+        return [
+            'orderCount' => $orderCount,
+            'commissionAmount' => round($commissionAmount, 2),
+            'distUserCount' => $distUserCount,
+            'inviteCount' => $inviteCount,
+            'pendingAmount' => round($pendingAmount, 2),
+            'settledAmount' => round($settledAmount, 2),
+            'depositAmount' => round($depositAmount, 2),
+        ];
+    }
+}

+ 268 - 0
biz-hd/distribution/classes/DistributionRuleClass.php

@@ -0,0 +1,268 @@
+<?php
+/**
+ * 用途:分销规则业务(xhDistributionRule / Tier / Scope)
+ * 谁用:hdApp 应用-门店设置-分销规则
+ */
+
+namespace bizHd\distribution\classes;
+
+use bizHd\base\classes\BaseClass;
+use Yii;
+
+class DistributionRuleClass extends BaseClass
+{
+    public static $baseFile = '\bizHd\distribution\models\DistributionRule';
+
+    /**
+     * 获取门店分销规则(无则返回默认结构)
+     * @param int $shopId
+     * @param int $mainId
+     * @return array
+     */
+    public static function getRule($shopId, $mainId)
+    {
+        $rule = self::getByCondition(['shopId' => $shopId]);
+        if (empty($rule)) {
+            return self::defaultRule($shopId, $mainId);
+        }
+
+        $ruleId = (int)$rule['id'];
+        $tiers = DistributionRuleTierClass::getAllByCondition(['ruleId' => $ruleId], 'sort ASC, id ASC');
+        $scopes = DistributionRuleScopeClass::getAllByCondition(['ruleId' => $ruleId], 'id ASC');
+
+        $goodsIds = [];
+        $categoryIds = [];
+        foreach ($scopes as $row) {
+            if ((int)$row['scopeType'] === 1) {
+                $goodsIds[] = (int)$row['targetId'];
+            } elseif ((int)$row['scopeType'] === 2) {
+                $categoryIds[] = (int)$row['targetId'];
+            }
+        }
+
+        $rule['tiers'] = $tiers ?: self::defaultTiers(false);
+        $rule['goodsIds'] = $goodsIds;
+        $rule['categoryIds'] = $categoryIds;
+        return $rule;
+    }
+
+    /**
+     * 保存分销规则
+     * @param int $shopId
+     * @param int $mainId
+     * @param array $data
+     * @throws \Exception
+     */
+    public static function saveRule($shopId, $mainId, $data)
+    {
+        self::validateRulePayload($data);
+
+        $existing = self::getByCondition(['shopId' => $shopId], true);
+        $saveData = [
+            'shopId' => $shopId,
+            'mainId' => $mainId,
+            'status' => isset($data['status']) ? (int)$data['status'] : 0,
+            'settleDays' => isset($data['settleDays']) ? (int)$data['settleDays'] : 0,
+            'bonusType' => isset($data['bonusType']) ? (int)$data['bonusType'] : 1,
+            'bonusRate' => isset($data['bonusRate']) ? round((float)$data['bonusRate'], 2) : 0,
+            'durationType' => isset($data['durationType']) ? (int)$data['durationType'] : 1,
+            'durationDays' => isset($data['durationDays']) ? (int)$data['durationDays'] : 0,
+            'timeStartType' => isset($data['timeStartType']) ? (int)$data['timeStartType'] : 1,
+            'productScope' => isset($data['productScope']) ? (int)$data['productScope'] : 1,
+        ];
+
+        $transaction = Yii::$app->db->beginTransaction();
+        try {
+            if (empty($existing)) {
+                $model = self::add($saveData, true);
+                $ruleId = (int)$model->id;
+            } else {
+                self::updateByCondition(['id' => $existing->id], $saveData);
+                $ruleId = (int)$existing->id;
+            }
+
+            DistributionRuleTierClass::deleteByCondition(['ruleId' => $ruleId]);
+            if ((int)$saveData['bonusType'] === 2) {
+                $tierRows = [];
+                foreach ($data['tiers'] as $index => $tier) {
+                    $tierRows[] = [
+                        'ruleId' => $ruleId,
+                        'minDays' => (int)$tier['minDays'],
+                        'maxDays' => (int)$tier['maxDays'],
+                        'bonusRate' => round((float)$tier['bonusRate'], 2),
+                        'sort' => isset($tier['sort']) ? (int)$tier['sort'] : $index,
+                    ];
+                }
+                if (!empty($tierRows)) {
+                    DistributionRuleTierClass::batchAdd($tierRows);
+                }
+            }
+
+            DistributionRuleScopeClass::deleteByCondition(['ruleId' => $ruleId]);
+            $scopeRows = [];
+            $productScope = (int)$saveData['productScope'];
+            if ($productScope === 2 && !empty($data['goodsIds']) && is_array($data['goodsIds'])) {
+                foreach ($data['goodsIds'] as $gid) {
+                    $gid = (int)$gid;
+                    if ($gid > 0) {
+                        $scopeRows[] = ['ruleId' => $ruleId, 'scopeType' => 1, 'targetId' => $gid];
+                    }
+                }
+            }
+            if ($productScope === 3 && !empty($data['categoryIds']) && is_array($data['categoryIds'])) {
+                foreach ($data['categoryIds'] as $cid) {
+                    $cid = (int)$cid;
+                    if ($cid > 0) {
+                        $scopeRows[] = ['ruleId' => $ruleId, 'scopeType' => 2, 'targetId' => $cid];
+                    }
+                }
+            }
+            if (!empty($scopeRows)) {
+                DistributionRuleScopeClass::batchAdd($scopeRows);
+            }
+
+            $transaction->commit();
+        } catch (\Exception $e) {
+            $transaction->rollBack();
+            throw $e;
+        }
+    }
+
+    /**
+     * 校验提交参数
+     * @param array $data
+     * @throws \Exception
+     */
+    protected static function validateRulePayload($data)
+    {
+        $settleDays = isset($data['settleDays']) ? (int)$data['settleDays'] : 0;
+        if ($settleDays < 0) {
+            throw new \Exception('佣金发放天数不能为负');
+        }
+
+        $bonusType = isset($data['bonusType']) ? (int)$data['bonusType'] : 1;
+        if ($bonusType === 1) {
+            $rate = isset($data['bonusRate']) ? (float)$data['bonusRate'] : 0;
+            if ($rate <= 0 || $rate > 100) {
+                throw new \Exception('请填写正确的固定佣金比例(0-100)');
+            }
+            $durationType = isset($data['durationType']) ? (int)$data['durationType'] : 1;
+            if ($durationType === 2) {
+                $durationDays = isset($data['durationDays']) ? (int)$data['durationDays'] : 0;
+                if ($durationDays <= 0) {
+                    throw new \Exception('请填写分红限制天数');
+                }
+            }
+        } else {
+            $tiers = isset($data['tiers']) && is_array($data['tiers']) ? $data['tiers'] : [];
+            if (empty($tiers)) {
+                throw new \Exception('请至少添加一个时间阶梯');
+            }
+            self::validateTiers($tiers);
+        }
+
+        $productScope = isset($data['productScope']) ? (int)$data['productScope'] : 1;
+        if ($productScope === 2) {
+            $goodsIds = isset($data['goodsIds']) && is_array($data['goodsIds']) ? $data['goodsIds'] : [];
+            if (empty($goodsIds)) {
+                throw new \Exception('请选择参与分红的商品');
+            }
+        }
+        if ($productScope === 3) {
+            $categoryIds = isset($data['categoryIds']) && is_array($data['categoryIds']) ? $data['categoryIds'] : [];
+            if (empty($categoryIds)) {
+                throw new \Exception('请选择参与分红的分类');
+            }
+        }
+    }
+
+    /**
+     * 校验时间阶梯:不可重叠、不可留空
+     * @param array $tiers
+     * @throws \Exception
+     */
+    protected static function validateTiers($tiers)
+    {
+        $normalized = [];
+        foreach ($tiers as $tier) {
+            $minDays = isset($tier['minDays']) ? (int)$tier['minDays'] : -1;
+            $maxDays = isset($tier['maxDays']) ? (int)$tier['maxDays'] : -1;
+            $rate = isset($tier['bonusRate']) ? (float)$tier['bonusRate'] : 0;
+            if ($minDays < 0) {
+                throw new \Exception('时间阶梯起始天数不能为空');
+            }
+            if ($maxDays > 0 && $maxDays < $minDays) {
+                throw new \Exception('时间阶梯结束天数不能小于起始天数');
+            }
+            if ($rate <= 0 || $rate > 100) {
+                throw new \Exception('请填写正确的阶梯佣金比例');
+            }
+            $normalized[] = ['min' => $minDays, 'max' => $maxDays, 'rate' => $rate];
+        }
+
+        usort($normalized, function ($a, $b) {
+            return $a['min'] - $b['min'];
+        });
+
+        $unlimitedCount = 0;
+        for ($i = 0; $i < count($normalized); $i++) {
+            $cur = $normalized[$i];
+            if ($cur['max'] === 0) {
+                $unlimitedCount++;
+                if ($i !== count($normalized) - 1) {
+                    throw new \Exception('「及以上」阶梯只能放在最后一行');
+                }
+            }
+            if ($i > 0) {
+                $prev = $normalized[$i - 1];
+                if ($prev['max'] <= 0) {
+                    throw new \Exception('时间阶梯区间不可重叠');
+                }
+                if ($cur['min'] <= $prev['max']) {
+                    throw new \Exception('时间阶梯区间不可重叠');
+                }
+            }
+        }
+    }
+
+    /**
+     * 默认规则(未保存过)
+     */
+    protected static function defaultRule($shopId, $mainId)
+    {
+        return [
+            'id' => 0,
+            'shopId' => $shopId,
+            'mainId' => $mainId,
+            'status' => 0,
+            'settleDays' => 5,
+            'bonusType' => 1,
+            'bonusRate' => '0.00',
+            'durationType' => 1,
+            'durationDays' => 0,
+            'timeStartType' => 1,
+            'productScope' => 1,
+            'tiers' => self::defaultTiers(true),
+            'goodsIds' => [],
+            'categoryIds' => [],
+        ];
+    }
+
+    /**
+     * 默认递减阶梯示例
+     */
+    protected static function defaultTiers($asNew = true)
+    {
+        $rows = [
+            ['minDays' => 0, 'maxDays' => 30, 'bonusRate' => '10.00', 'sort' => 0],
+            ['minDays' => 31, 'maxDays' => 60, 'bonusRate' => '5.00', 'sort' => 1],
+            ['minDays' => 61, 'maxDays' => 0, 'bonusRate' => '2.00', 'sort' => 2],
+        ];
+        if (!$asNew) {
+            return $rows;
+        }
+        return array_map(function ($row) {
+            return array_merge($row, ['id' => 0]);
+        }, $rows);
+    }
+}

+ 13 - 0
biz-hd/distribution/classes/DistributionRuleScopeClass.php

@@ -0,0 +1,13 @@
+<?php
+/**
+ * 用途:分销规则商品范围 xhDistributionRuleScope 增删改查
+ */
+
+namespace bizHd\distribution\classes;
+
+use bizHd\base\classes\BaseClass;
+
+class DistributionRuleScopeClass extends BaseClass
+{
+    public static $baseFile = '\bizHd\distribution\models\DistributionRuleScope';
+}

+ 13 - 0
biz-hd/distribution/classes/DistributionRuleTierClass.php

@@ -0,0 +1,13 @@
+<?php
+/**
+ * 用途:分销规则阶梯 xhDistributionRuleTier 增删改查
+ */
+
+namespace bizHd\distribution\classes;
+
+use bizHd\base\classes\BaseClass;
+
+class DistributionRuleTierClass extends BaseClass
+{
+    public static $baseFile = '\bizHd\distribution\models\DistributionRuleTier';
+}

+ 407 - 0
biz-hd/distribution/classes/DistributionUserClass.php

@@ -0,0 +1,407 @@
+<?php
+/**
+ * 用途:分销用户统计(xhDistributionUser)
+ * 谁用:hdApp 客户详情-分销板块
+ */
+
+namespace bizHd\distribution\classes;
+
+use bizHd\base\classes\BaseClass;
+use bizHd\custom\classes\CustomClass;
+use common\components\dateUtil;
+use Yii;
+use yii\db\Query;
+
+class DistributionUserClass extends BaseClass
+{
+    public static $baseFile = '\bizHd\distribution\models\DistributionUser';
+
+    /**
+     * 获取客户分销统计(无记录返回默认 0)
+     * @param int $shopId 门店ID
+     * @param int $customId 客户 xhCustom.id
+     * @return array
+     * @throws \Exception
+     */
+    public static function getUserStat($shopId, $customId)
+    {
+        $customId = (int)$customId;
+        if ($customId <= 0) {
+            throw new \Exception('客户ID无效');
+        }
+
+        $row = self::getByCondition(['shopId' => (int)$shopId, 'id' => $customId]);
+        if (empty($row)) {
+            return self::defaultStat($customId);
+        }
+
+        return [
+            'customId' => $customId,
+            'totalCommission' => $row['totalCommission'],
+            'inviteCount' => (int)$row['inviteCount'],
+            'orderCount' => (int)$row['orderCount'],
+            'pendingCommission' => $row['pendingCommission'],
+            'settledCommission' => $row['settledCommission'],
+        ];
+    }
+
+    /**
+     * 商城 C 端我的分红汇总
+     * @param int $shopId
+     * @param int $customId
+     * @return array
+     * @throws \Exception
+     */
+    public static function getMallMyStat($shopId, $customId)
+    {
+        $stat = self::getUserStat($shopId, $customId);
+        $row = self::getByCondition(['shopId' => (int)$shopId, 'id' => (int)$customId]);
+        $stat['availDeposit'] = round((float)(!empty($row) ? ($row['availDeposit'] ?? 0) : 0), 2);
+
+        $period = dateUtil::formatTime('thisMonth', '', '');
+        $monthCommission = 0;
+        if (!empty($period['startTime']) && !empty($period['endTime'])) {
+            $monthCommission = (float)(new Query())
+                ->from('xhDistributionFlow')
+                ->where([
+                    'shopId' => (int)$shopId,
+                    'xhCustomId' => (int)$customId,
+                    'flowType' => 1,
+                ])
+                ->andWhere(['between', 'flowTime', $period['startTime'], $period['endTime']])
+                ->sum('amount');
+        }
+        $stat['monthCommission'] = round($monthCommission, 2);
+        return $stat;
+    }
+
+    /**
+     * 无分销记录时的默认统计
+     */
+    protected static function defaultStat($customId)
+    {
+        return [
+            'customId' => (int)$customId,
+            'totalCommission' => '0.00',
+            'inviteCount' => 0,
+            'orderCount' => 0,
+            'pendingCommission' => '0.00',
+            'settledCommission' => '0.00',
+            'availDeposit' => '0.00',
+            'monthCommission' => '0.00',
+        ];
+    }
+
+    /**
+     * 拉新客户列表(xhDistributionUser + xhCustom)
+     * 条件:du.inviterId = 当前分销员;绑定时间取 inviteTime
+     * @param int $shopId
+     * @param int $inviterId 邀请人 xhCustom.id
+     * @return array
+     * @throws \Exception
+     */
+    public static function getInviteList($shopId, $inviterId)
+    {
+        $shopId = (int)$shopId;
+        $inviterId = (int)$inviterId;
+        if ($inviterId <= 0) {
+            throw new \Exception('客户ID无效');
+        }
+
+        $get = Yii::$app->request->get();
+        $keyword = isset($get['keyword']) ? trim($get['keyword']) : '';
+        $sortField = isset($get['sortField']) ? trim($get['sortField']) : 'inviteTime';
+        $sortOrder = isset($get['sortOrder']) && strtolower($get['sortOrder']) === 'asc' ? 'ASC' : 'DESC';
+        $page = isset($get['page']) ? max(1, (int)$get['page']) : 1;
+        $pageSize = isset($get['pageSize']) && (int)$get['pageSize'] > 0
+            ? (int)$get['pageSize']
+            : (int)Yii::$app->params['pageSize'];
+
+        $query = (new Query())
+            ->from(['du' => 'xhDistributionUser'])
+            ->leftJoin(['c' => 'xhCustom'], 'c.id = du.id')
+            ->where(['du.shopId' => $shopId, 'du.inviterId' => $inviterId])
+            ->select([
+                'du.id',
+                'du.inviteTime',
+                'du.upOrderCount',
+                'du.upCommissionAmount',
+                'c.name',
+                'c.mobile',
+                'c.avatar',
+            ]);
+
+        if ($keyword !== '') {
+            $query->andWhere([
+                'or',
+                ['like', 'c.name', $keyword],
+                ['like', 'c.mobile', $keyword],
+            ]);
+        }
+
+        // 排序:贡献字段取 up*(本人下单给上级的贡献)
+        $orderMap = [
+            'inviteTime' => 'du.inviteTime',
+            'commission' => 'du.upCommissionAmount',
+            'orderCount' => 'du.upOrderCount',
+        ];
+        $orderColumn = isset($orderMap[$sortField]) ? $orderMap[$sortField] : 'du.inviteTime';
+        $query->orderBy([$orderColumn => $sortOrder === 'ASC' ? SORT_ASC : SORT_DESC, 'du.id' => SORT_DESC]);
+
+        $total = (int)(clone $query)->count('*', Yii::$app->db);
+        $totalPage = $pageSize > 0 ? (int)ceil($total / $pageSize) : 0;
+        $rows = $query->offset(($page - 1) * $pageSize)->limit($pageSize)->all(Yii::$app->db);
+
+        $customRows = [];
+        foreach ($rows as $row) {
+            $customRows[] = [
+                'id' => (int)$row['id'],
+                'name' => $row['name'] ?: '',
+                'mobile' => $row['mobile'] ?: '',
+                'avatar' => $row['avatar'] ?: '',
+            ];
+        }
+        $avatarMap = [];
+        if (!empty($customRows)) {
+            foreach (CustomClass::groupBaseInfo($customRows) as $customItem) {
+                $avatarMap[(int)$customItem['id']] = $customItem;
+            }
+        }
+
+        $list = [];
+        foreach ($rows as $row) {
+            $custom = $avatarMap[(int)$row['id']] ?? [];
+            $bindTime = $row['inviteTime'];
+            $list[] = [
+                'customId' => (int)$row['id'],
+                'name' => $custom['name'] ?? ($row['name'] ?: ''),
+                'mobile' => $custom['mobile'] ?? ($row['mobile'] ?: ''),
+                'smallAvatar' => $custom['smallAvatar'] ?? '',
+                'bindTime' => $bindTime,
+                'bindDays' => self::calcBindDays($bindTime),
+                'bindDaysText' => self::buildBindDaysText($bindTime),
+                'contribCommission' => round((float)$row['upCommissionAmount'], 2),
+                'contribOrderCount' => (int)$row['upOrderCount'],
+            ];
+        }
+
+        return [
+            'list' => $list,
+            'totalPage' => $totalPage,
+            'moreData' => $page < $totalPage ? 1 : 0,
+        ];
+    }
+
+    /**
+     * 门店获佣人数明细(报表跳转)
+     * @param int $shopId
+     * @return array
+     */
+    public static function getShopDistList($shopId)
+    {
+        $shopId = (int)$shopId;
+        $get = Yii::$app->request->get();
+        $keyword = isset($get['keyword']) ? trim($get['keyword']) : '';
+        $page = isset($get['page']) ? max(1, (int)$get['page']) : 1;
+        $pageSize = isset($get['pageSize']) && (int)$get['pageSize'] > 0
+            ? (int)$get['pageSize']
+            : (int)Yii::$app->params['pageSize'];
+        $period = self::buildReportPeriod($get);
+
+        $orderQuery = (new Query())
+            ->from('xhDistributionOrder')
+            ->where(['shopId' => $shopId])
+            ->andWhere(['>', 'distId', 0]);
+        if ($period) {
+            $orderQuery->andWhere(['between', 'orderTime', $period[0], $period[1]]);
+        }
+        if ($keyword !== '') {
+            $orderQuery->innerJoin(['c' => 'xhCustom'], 'c.id = xhDistributionOrder.distId')
+                ->andWhere([
+                    'or',
+                    ['like', 'c.name', $keyword],
+                    ['like', 'c.mobile', $keyword],
+                ]);
+        }
+
+        $subQuery = (clone $orderQuery)
+            ->select([
+                'distId',
+                'orderCount' => 'COUNT(*)',
+                'commissionAmount' => 'SUM(commissionAmount)',
+                'distName' => 'MAX(distName)',
+            ])
+            ->groupBy('distId');
+
+        $total = (int)(new Query())->from(['t' => $subQuery])->count('*', Yii::$app->db);
+        $totalPage = $pageSize > 0 ? (int)ceil($total / $pageSize) : 0;
+        $rows = (new Query())
+            ->from(['t' => $subQuery])
+            ->orderBy(['commissionAmount' => SORT_DESC, 'distId' => SORT_DESC])
+            ->offset(($page - 1) * $pageSize)
+            ->limit($pageSize)
+            ->all(Yii::$app->db);
+
+        $distIds = [];
+        foreach ($rows as $row) {
+            $distIds[] = (int)$row['distId'];
+        }
+        $avatarMap = [];
+        if (!empty($distIds)) {
+            // groupBaseInfo 仅处理头像,需先 getCustomByIds 拉取 name/mobile
+            foreach (CustomClass::getCustomByIds($distIds) as $customItem) {
+                $avatarMap[(int)$customItem['id']] = $customItem;
+            }
+        }
+
+        $list = [];
+        foreach ($rows as $row) {
+            $distId = (int)$row['distId'];
+            $custom = $avatarMap[$distId] ?? [];
+            $list[] = [
+                'customId' => $distId,
+                'name' => $custom['name'] ?? ($row['distName'] ?? ''),
+                'mobile' => $custom['mobile'] ?? '',
+                'smallAvatar' => $custom['smallAvatar'] ?? '',
+                'orderCount' => (int)$row['orderCount'],
+                'commissionAmount' => round((float)$row['commissionAmount'], 2),
+            ];
+        }
+
+        return [
+            'list' => $list,
+            'totalPage' => $totalPage,
+            'moreData' => $page < $totalPage ? 1 : 0,
+        ];
+    }
+
+    /**
+     * 门店拉新明细(报表跳转)
+     * @param int $shopId
+     * @return array
+     */
+    public static function getShopInviteList($shopId)
+    {
+        $shopId = (int)$shopId;
+        $get = Yii::$app->request->get();
+        $keyword = isset($get['keyword']) ? trim($get['keyword']) : '';
+        $page = isset($get['page']) ? max(1, (int)$get['page']) : 1;
+        $pageSize = isset($get['pageSize']) && (int)$get['pageSize'] > 0
+            ? (int)$get['pageSize']
+            : (int)Yii::$app->params['pageSize'];
+        $period = self::buildReportPeriod($get);
+
+        $query = (new Query())
+            ->from(['du' => 'xhDistributionUser'])
+            ->leftJoin(['c' => 'xhCustom'], 'c.id = du.id')
+            ->leftJoin(['inv' => 'xhCustom'], 'inv.id = du.inviterId')
+            ->where(['du.shopId' => $shopId])
+            ->andWhere(['>', 'du.inviterId', 0])
+            ->select([
+                'du.id',
+                'du.inviterId',
+                'du.inviteTime',
+                'du.upOrderCount',
+                'du.upCommissionAmount',
+                'c.name',
+                'c.mobile',
+                'c.avatar',
+                'inv.name AS inviterName',
+                'inv.mobile AS inviterMobile',
+            ]);
+
+        if ($period) {
+            $query->andWhere(['between', 'du.inviteTime', $period[0], $period[1]]);
+        }
+        if ($keyword !== '') {
+            $query->andWhere([
+                'or',
+                ['like', 'c.name', $keyword],
+                ['like', 'c.mobile', $keyword],
+                ['like', 'inv.name', $keyword],
+            ]);
+        }
+
+        $total = (int)(clone $query)->count('*', Yii::$app->db);
+        $totalPage = $pageSize > 0 ? (int)ceil($total / $pageSize) : 0;
+        $rows = $query
+            ->orderBy(['du.inviteTime' => SORT_DESC, 'du.id' => SORT_DESC])
+            ->offset(($page - 1) * $pageSize)
+            ->limit($pageSize)
+            ->all(Yii::$app->db);
+
+        $customRows = [];
+        foreach ($rows as $row) {
+            $customRows[] = [
+                'id' => (int)$row['id'],
+                'name' => $row['name'] ?: '',
+                'mobile' => $row['mobile'] ?: '',
+                'avatar' => $row['avatar'] ?: '',
+            ];
+        }
+        $avatarMap = [];
+        if (!empty($customRows)) {
+            foreach (CustomClass::groupBaseInfo($customRows) as $customItem) {
+                $avatarMap[(int)$customItem['id']] = $customItem;
+            }
+        }
+
+        $list = [];
+        foreach ($rows as $row) {
+            $custom = $avatarMap[(int)$row['id']] ?? [];
+            $bindTime = $row['inviteTime'];
+            $list[] = [
+                'customId' => (int)$row['id'],
+                'inviterId' => (int)$row['inviterId'],
+                'inviterName' => $row['inviterName'] ?: '',
+                'inviterMobile' => $row['inviterMobile'] ?: '',
+                'name' => $custom['name'] ?? ($row['name'] ?: ''),
+                'mobile' => $custom['mobile'] ?? ($row['mobile'] ?: ''),
+                'smallAvatar' => $custom['smallAvatar'] ?? '',
+                'bindTime' => $bindTime,
+                'bindDaysText' => self::buildBindDaysText($bindTime),
+                'contribCommission' => round((float)$row['upCommissionAmount'], 2),
+                'contribOrderCount' => (int)$row['upOrderCount'],
+            ];
+        }
+
+        return [
+            'list' => $list,
+            'totalPage' => $totalPage,
+            'moreData' => $page < $totalPage ? 1 : 0,
+        ];
+    }
+
+    /** 报表页时间范围 */
+    protected static function buildReportPeriod($get)
+    {
+        $searchTime = isset($get['searchTime']) ? trim($get['searchTime']) : 'today';
+        $startTime = isset($get['startTime']) ? trim($get['startTime']) : '';
+        $endTime = isset($get['endTime']) ? trim($get['endTime']) : '';
+        $period = dateUtil::formatTime($searchTime, $startTime, $endTime);
+        if (empty($period['startTime']) || empty($period['endTime'])) {
+            return null;
+        }
+        return [$period['startTime'], $period['endTime']];
+    }
+
+    /** 计算绑定天数 */
+    protected static function calcBindDays($bindTime)
+    {
+        if (empty($bindTime) || $bindTime === '0000-00-00 00:00:00') {
+            return 0;
+        }
+        $time = strtotime($bindTime);
+        if ($time <= 0) {
+            return 0;
+        }
+        return max(0, (int)floor((time() - $time) / 86400));
+    }
+
+    /** 绑定天数文案 */
+    protected static function buildBindDaysText($bindTime)
+    {
+        $days = self::calcBindDays($bindTime);
+        return '已绑定' . $days . '天';
+    }
+}

+ 17 - 0
biz-hd/distribution/models/DistributionFlow.php

@@ -0,0 +1,17 @@
+<?php
+/**
+ * 用途:分销流水表模型 xhDistributionFlow
+ * 谁用:hdApp 客户详情-分红变动明细
+ */
+
+namespace bizHd\distribution\models;
+
+use bizHd\base\models\Base;
+
+class DistributionFlow extends Base
+{
+    public static function tableName()
+    {
+        return 'xhDistributionFlow';
+    }
+}

+ 17 - 0
biz-hd/distribution/models/DistributionOrder.php

@@ -0,0 +1,17 @@
+<?php
+/**
+ * 用途:订单分销记录模型 xhDistributionOrder
+ * 谁用:拉新客户列表-贡献分红/订单数统计
+ */
+
+namespace bizHd\distribution\models;
+
+use bizHd\base\models\Base;
+
+class DistributionOrder extends Base
+{
+    public static function tableName()
+    {
+        return 'xhDistributionOrder';
+    }
+}

+ 17 - 0
biz-hd/distribution/models/DistributionRule.php

@@ -0,0 +1,17 @@
+<?php
+/**
+ * 用途:分销规则主表模型 xhDistributionRule
+ * 谁用:hdApp 门店设置-分销规则
+ */
+
+namespace bizHd\distribution\models;
+
+use bizHd\base\models\Base;
+
+class DistributionRule extends Base
+{
+    public static function tableName()
+    {
+        return 'xhDistributionRule';
+    }
+}

+ 16 - 0
biz-hd/distribution/models/DistributionRuleScope.php

@@ -0,0 +1,16 @@
+<?php
+/**
+ * 用途:分销规则参与商品/分类范围 xhDistributionRuleScope
+ */
+
+namespace bizHd\distribution\models;
+
+use bizHd\base\models\Base;
+
+class DistributionRuleScope extends Base
+{
+    public static function tableName()
+    {
+        return 'xhDistributionRuleScope';
+    }
+}

+ 16 - 0
biz-hd/distribution/models/DistributionRuleTier.php

@@ -0,0 +1,16 @@
+<?php
+/**
+ * 用途:分销规则时间递减阶梯 xhDistributionRuleTier
+ */
+
+namespace bizHd\distribution\models;
+
+use bizHd\base\models\Base;
+
+class DistributionRuleTier extends Base
+{
+    public static function tableName()
+    {
+        return 'xhDistributionRuleTier';
+    }
+}

+ 17 - 0
biz-hd/distribution/models/DistributionUser.php

@@ -0,0 +1,17 @@
+<?php
+/**
+ * 用途:分销用户统计表模型 xhDistributionUser
+ * 谁用:hdApp 客户详情-分销板块
+ */
+
+namespace bizHd\distribution\models;
+
+use bizHd\base\models\Base;
+
+class DistributionUser extends Base
+{
+    public static function tableName()
+    {
+        return 'xhDistributionUser';
+    }
+}