1 Commits

Author SHA1 Message Date
apple
fd33eeb21a chore(admin): hjfshop title and HTTPS/WSS production API URLs
Made-with: Cursor
2026-03-21 17:15:58 +08:00
897 changed files with 1177 additions and 197389 deletions

View File

@@ -1,327 +0,0 @@
---
name: Fix Issues 0325-1
overview: 修复 UniApp 移动端前端5 个 UI 问题)和 PHP 后端7 个业务逻辑问题)共 12 个问题,涉及会员码图片、海报生成、账单筛选、导航、资产数据展示、佣金周期轮巡、积分奖励、分销等级升级和 e2e 验收测试。
todos:
- id: a1-member-code-image
content: 修复会员码页面:小程序 image 标签加 v-if 防护API 返回值兜底
status: completed
- id: a2-spread-poster
content: 修复分销海报downloadFilestoreImage 失败时 Promise 需 resolve/reject增加错误处理
status: completed
- id: a3-bill-remove-queue-refund
content: 账单明细页移除"公排退款"标签和筛选项
status: completed
- id: a4-queue-status-back-btn
content: 佣金状态页左上角增加返回按钮NavBar
status: completed
- id: a5-assets-data-and-back
content: 后端 HjfAssets 接口补充 total_points_earned 字段 + 资产页增加返回按钮
status: completed
- id: b1-commission-guard
content: 增加校验:推荐人自己必须有报单订单才能获得推荐返现佣金
status: completed
- id: b2-points-pre-upgrade
content: 升级为会员分销等级前,不给直推/伞下奖励积分
status: completed
- id: b3-points-by-qty
content: 积分奖励按订单中报单商品数量(而非订单数)发放
status: completed
- id: b4-umbrella-points
content: 修复伞下积分:创客无伞下积分,云店及以上才有伞下奖励积分,检查配置与级差逻辑
status: completed
- id: b5-upgrade-count
content: 修复创客升级getDirectQueueOrderCount 补充 refund_status 检查,核实任务配置
status: completed
- id: b6-cycle-race
content: 修复推荐返现循环并发竞态:用数据库锁或原子计数器序列化位次计算
status: completed
- id: b7-e2e-verify
content: e2e 测试验收分销员等级配置的奖励积分(直推/伞下)和等级升级任务是否正确
status: completed
isProject: false
---
# 修复 docs/issues-0325-1.md 中的问题
## 第一部分UniApp 移动端前端问题
所有前端文件位于 `pro_v3.5.1/view/uniapp/` 目录下。
---
### A1. 会员码页面图片报错 (pages/users/user_member_code/index.vue)
**问题反馈:** 前端页面报错 `Failed to load local image resource /pages/users/user_member_code/false`
**问题分析:** 小程序端 `<image :src="qrc">` 没有 `v-if` 防护。当 `activityCodeApi` 接口返回 `routineUrl: false`(布尔值)时,`this.qrc` 被赋值为 `false`,运行时将其字符串化为路径 `/pages/users/user_member_code/false`,导致图片加载 500 错误。
**代码定位:**
```17:18:pro_v3.5.1/view/uniapp/pages/users/user_member_code/index.vue
<!-- #ifdef MP -->
<image :src="qrc" class="qrcode"></image>
```
以及 API 回调处(约第 135 行):
```javascript
this.qrc = routineUrl; // routineUrl 可能为 false/null
```
**修复方案:**
- 在小程序的 `<image>` 标签上添加 `v-if="qrc"`(第 18 行),与 H5 分支保持一致
- 在 `activityCodeApi()` 中添加兜底:`this.qrc = routineUrl || ''`
---
### A2. 分销海报加载不出来 (pages/users/user_spread_code/index.vue)
**问题反馈:** 海报加载不出来
**问题分析:** `downloadFilestoreImage()`(约第 354 行)返回的 Promise 在下载失败时**既不 resolve 也不 reject**,只调用了 `Tips()`。这导致 `await` 永久挂起,阻塞了整个海报生成流程。
```363:366:pro_v3.5.1/view/uniapp/pages/users/user_spread_code/index.vue
fail: function() {
return that.$util.Tips({
title: ''
});
```
**修复方案:**
- 在 `downloadFilestoreImage` 的 `fail` 回调中添加 `reject()`(或 `resolve('')`),让 Promise 正常结束
- 在 `spreadMsg` 中,将下载后的图片路径传入 `userPosterCanvas` 前,增加空值/空字符串检查
- 可选:在海报生成循环外包 try/catch确保 `uni.hideLoading()` 始终执行
---
### A3. 账单明细页 — 移除"公排退款" (pages/users/user_bill/index.vue)
**问题反馈:** 去掉公排退款
**问题分析:** 导航栏有"公排退款"筛选标签,列表项中也显示"公排退款"标记。需要全部移除。
**修复方案:** 从模板中删除以下内容:
- 导航栏中 `queue_refund` 类型对应的 `<view>`(第 23-27 行)
- 列表项中的 `<text v-if="vo.type === 'queue_refund'" ...>公排退款</text>` 标记(第 47-50 行)
---
### A4. 佣金状态页 — 增加返回按钮 (pages/queue/status.vue)
**问题反馈:** 左上角增加返回按钮
**问题分析:** 页面顶部没有 NavBar 和返回按钮。
**修复方案:** 参照其他页面的已有模式(如 `user_spread_user/index.vue`
- 从 `@/components/NavBar.vue` 导入 `NavBar`(小程序条件编译)
- 在 `components` 中注册
- 在模板中添加 `<NavBar titleText="佣金状态" :iconColor="iconColor" :textColor="iconColor" showBack :isScrolling="isScrolling" />`,用 `<!-- #ifdef MP -->` 条件编译包裹
- 添加对应的 data 属性(`iconColor`、`isScrolling`)和滚动事件处理
---
### A5. 资产页 — 数据不显示 + 增加返回按钮 (pages/assets/index.vue)
**问题反馈:**
1. "累计获得积分"不显示数据,是否没读取到数据?
2. 左上角增加返回按钮
**问题 1 分析:** "累计获得积分"显示为 `NaN`。前端读取 `assetsInfo.total_points_earned`,但后端 `HjfAssets::overview` 接口未返回该字段。
- 前端计算属性(约第 148 行):`Number(this.assetsInfo.total_points_earned).toLocaleString()` => `NaN`
- 后端响应([HjfAssets.php](pro_v3.5.1/app/controller/api/v1/hjf/HjfAssets.php) 第 42-50 行):仅返回 `frozen_points`、`available_points` 等,缺少 `total_points_earned`
**修复方案(后端):** 在 `HjfAssets::overview()` 的响应中增加 `total_points_earned` 字段。其值应为 `frozen_points + available_points`(累计获得 = 当前待释放 + 已释放):
```php
'total_points_earned' => $frozenPoints + (int)($user['available_points'] ?? 0),
```
**问题 2** 左上角没有返回按钮(同 A4
**修复方案:** 按照 A4 相同的模式添加带 `showBack` 的 NavBar。
---
## 第二部分:后端业务逻辑问题
所有后端文件位于 `pro_v3.5.1/app/` 目录下。
---
### B1. 自己不报单,推荐没有返现佣金(测试问题 #1
**问题反馈:** "自己不报单(没有购买过报单商品的订单),推荐没有返现佣金的"
**问题分析:** [StoreOrderCreateServices.php](pro_v3.5.1/app/services/order/StoreOrderCreateServices.php)(约第 998-1028 行)中的佣金周期逻辑在**被推荐人**下单报单商品时计算佣金,但没有校验**推荐人自己**是否也购买过报单商品。按业务规则,推荐人自己必须先报单(购买过报单商品),推荐下级报单才能获得返现佣金。
**修复方案:** 在 `computeOrderProductTruePrice` 中计算周期佣金之前(约第 1004 行),增加校验:若推荐人(`$spread_uid`)自己没有任何已支付的 `is_queue_goods=1` 订单,则跳过佣金计算(`$oneBrokerage = 0`)。或者在 `Pay.php::compute()` 中写入 `one_brokerage` 前做此校验。
---
### B2. 升级成为会员分销等级前,不给奖励积分(测试问题 #2
**问题反馈:** "判断升级成为**会员分销等级**前,不给奖励积分(直推奖励积分、伞下奖励积分)"
**问题分析:** 在 [Pay.php](pro_v3.5.1/app/listener/order/Pay.php)(第 180-184 行)中,流程是:先检查升级、再发放积分。这意味着如果当前订单使推荐人从 grade 0 升级为创客,`reward()` 调用时已经看到了新等级,会在触发升级的同一笔订单上就发放**直推奖励积分**或**伞下奖励积分**。
根据 PRD直推奖励积分和伞下奖励积分均应从推荐人**已成为会员分销等级之后**的订单才开始发放。触发升级的那笔订单本身不应给新升级的用户发积分。
**修复方案:** 在升级检查执行前,记录各上级用户的"升级前等级"快照,然后在 `reward()` 中使用**升级前的等级**判断是否有资格获得直推/伞下奖励积分。具体方式:
- 在 Pay.php 中 `checkUserLevelFinish` 之前,快照相关用户的 `agent_level`
- 将升级前的等级信息传入 `reward()`,用于资格判断
- 或者在 `reward()` 中检查用户的 `agent_level` 是否在同一事务/时间窗口内被更新,若是则跳过
---
### B3. 积分应按订单中报单商品数量发放,而非按订单数(测试问题 #3
**问题反馈:** "目前积分奖励是按订单数量来给了需要调整为按报单商品数量比如一次性下一个订单含3个报单商品应该给1500积分而不是500"
**问题分析:** `PointsRewardServices::reward()` 每笔订单只调用一次,发放基于等级的固定积分(如创客 500 分)。若**一个订单内**包含 3 个报单商品(商品数量=3应发放 3x500=1500 分,而非 500 分。
**修复方案:**
- 修改 `reward()` 增加 `$qty` 参数(默认为 1表示该订单中报单商品的数量
- 在 `grantFrozenPoints` 中将 `$points * $qty` 计算实际积分
- 在 [Pay.php](pro_v3.5.1/app/listener/order/Pay.php)(第 184 行)中,从 `$orderInfo` 的购物车信息计算订单内报单商品数量(`is_queue_goods=1` 的商品 `cart_num` 之和),传入 `reward()`
- 在 [HjfOrderPayJob.php](pro_v3.5.1/app/jobs/hjf/HjfOrderPayJob.php) 中做同样的修改
---
### B4. 伞下奖励积分规则:创客无伞下积分,云店及以上才有(测试问题 #4
**问题反馈:** "创客级别的分销员,伞下下单没有**伞下奖励积分**,升级为云店级别分销员之后,只要级别低于自己的,伞下下单都应有**伞下奖励积分**"
**问题分析:** 按 PRD 各等级积分配置:
| 等级 | 直推奖励积分 | 伞下奖励积分 |
| --------------- | ------ | ------ |
| 创客 | 500 | 0 |
| 云店 | 800 | 300 |
| 服务中心原PRD名服务商 | 1000 | 200 |
| 合伙人原PRD名分公司 | 1300 | 300 |
> **注意:** 数据库中等级名称已调整,"服务商"改为"服务中心""分公司"改为"合伙人"。后续代码及配置中以数据库实际名称为准。
创客的伞下奖励积分为 **0**,这是**正确的设计**(非 Bug。问题焦点是**云店及以上**级别的会员在伞下下单后应获得伞下奖励积分,但目前可能未正确发放。
在 [PointsRewardServices.php](pro_v3.5.1/app/services/hjf/PointsRewardServices.php) 第 109 行,级差逻辑 `$actual = max(0, $reward - $lowerReward)` 可能导致云店的伞下积分被下级已获得的奖励抵消为 0。
**修复方案:**
- 核查 `eb_agent_level` 表中各等级的 `umbrella_reward_points` 是否与 PRD 一致(创客=0、云店=300、服务中心=200、合伙人=300
- 确认 `AgentLevelServices` 的 `getUmbrellaRewardPoints()` 和 `getDirectRewardPoints()` 正确读取该字段并返回
- 如果数据配置正确但云店仍未拿到伞下积分,检查级差计算中 `$lowerReward` 的传递链是否正确——特别注意当中间层(如创客)的伞下积分为 0 时,传给上级的 `$lowerReward` 应为 0云店的 `$actual` 应为 `max(0, 300 - 0) = 300`
- 若中间层是以**直推奖励积分**500作为 `$lowerReward` 传递的,则需修复 `propagateReward` 使其在递归中区分直推与伞下,确保向上传递的是**伞下**奖励而非直推奖励
---
### B5. 用户 ID=14 只有 2 单就升级为创客(测试问题 #5
**问题反馈:** "用户ID=14直推了2单就升级成为创客的分销等级了但是目前分销等级中创客的升级任务配置的是直推3单"
**问题分析:** [AgentLevelTaskServices.php](pro_v3.5.1/app/services/agent/AgentLevelTaskServices.php)(第 431 行)中 `getDirectQueueOrderCount()` 统计推荐人直推下级的报单订单数。可能原因:
- 统计包含了已退款的订单(查询条件有 `is_del=0`,但缺少 `refund_status` 检查)
- `no_assess=0` 过滤条件未正确生效
- `eb_agent_level_task` 表中创客的任务 `number` 被配置为 2 而非 3
- 竞态条件:并发订单处理可能触发两次升级检查
**修复方案:**
- 检查 `eb_agent_level_task` 表确认创客的任务配置为 `type=6`、`number=3`
- 在 `getDirectQueueOrderCount` 查询中补充 `refund_status` 条件检查(目前缺失,而其他任务类型均有 `refund_status => [0, 3]` 的检查)
- 在升级检查处增加日志,记录升级时刻的精确计数
---
### B6. 推荐返现循环两次都给了 20%(测试问题 #6
**问题反馈:** "**推荐返现循环**目前配置的是'[20,30,50]'。为何用户ID=14直推了2单出现了2次20%的返现佣金?"
**问题分析:** 在 [StoreOrderCreateServices.php](pro_v3.5.1/app/services/order/StoreOrderCreateServices.php) 第 1009-1015 行,位次计算通过统计推荐人下级所有已支付的报单订单数来确定。若两笔订单几乎同时处理,两次 `compute()` 调用可能看到相同的 `completedCount`,导致位次相同(都得到 20%)。正确行为应是:第 1 单 20%、第 2 单 30%、第 3 单 50%,然后循环。
```1015:1015:pro_v3.5.1/app/services/order/StoreOrderCreateServices.php
$position = max(0, $completedCount - 1) % $cycleCount;
```
**修复方案:**
- 添加数据库级别的锁(如对 spread_uid 维度加 `SELECT ... FOR UPDATE`),序列化周期位次计算
- 或者在用户表中增加显式周期计数器字段(如 `eb_user` 表添加 `brokerage_cycle_position` 列),原子递增,而非依赖 count 查询
- 或者使用 `ORDER BY id ASC` 按订单排名确定位次,而非仅统计总数
---
### B7. e2e 测试验收分销员等级配置(测试问题 #7.
**问题反馈:** "e2e测试验收一下分销员等级中配置的奖励积分直推奖励积分、伞下奖励积分和等级任务是否正确"
**问题分析:** 需要端到端验证 `eb_agent_level` 和 `eb_agent_level_task` 表中的数据配置与 PRD 一致。
**验收清单:**
**1) `eb_agent_level` 表 — 各等级奖励积分配置PRD 3.2**
| 等级 | grade | direct_reward_points直推 | umbrella_reward_points伞下 |
| ---- | ----- | ------------------------ | -------------------------- |
| 创客 | 1 | 500 | 0 |
| 云店 | 2 | 800 | 300 |
| 服务中心 | 3 | 1000 | 200 |
| 合伙人 | 4 | 1300 | 300 |
**2) `eb_agent_level_task` 表 — 各等级升级任务配置PRD 3.2**
| 等级 | 任务类型 type | 任务要求 number | 说明 |
| ---- | --------- | ----------- | ---------- |
| 创客 | 6直推报单单数 | 3 | 直推满 3 单 |
| 云店 | 7伞下报单业绩 | 30 | 伞下满 30 单 |
| 云店 | 8最低直推人数 | 3 | 至少 3 个直推 |
| 服务中心 | 7伞下报单业绩 | 100 | 伞下满 100 单 |
| 服务中心 | 8最低直推人数 | 3 | 至少 3 个直推 |
| 合伙人 | 7伞下报单业绩 | 1000 | 伞下满 1000 单 |
| 合伙人 | 8最低直推人数 | 3 | 至少 3 个直推 |
**3) 功能验证场景(手动或脚本):**
- 创客升级:模拟 3 笔直推报单订单,确认准确在第 3 笔时升级
- 积分发放:确认创客直推积分 500/单、伞下积分 0云店直推 800/单、伞下 300/单
- 佣金轮巡:模拟 3 笔直推报单订单,确认佣金比例依次为 20%→30%→50%
- 批量报单:一个订单含多个报单商品时,积分按商品数量乘算
**修复方案:** 编写 SQL 查询脚本或 PHP 命令行工具验证上述配置,不一致则修正数据。
---
## 文件修改汇总
| 问题编号 | 主要需修改的文件 |
| ---- | ---------------------------------------------------------------------------------------------------------- |
| A1 | `view/uniapp/pages/users/user_member_code/index.vue` |
| A2 | `view/uniapp/pages/users/user_spread_code/index.vue` |
| A3 | `view/uniapp/pages/users/user_bill/index.vue` |
| A4 | `view/uniapp/pages/queue/status.vue` |
| A5 | `view/uniapp/pages/assets/index.vue`、`app/controller/api/v1/hjf/HjfAssets.php` |
| B1 | `app/services/order/StoreOrderCreateServices.php` 或 `app/listener/order/Pay.php` |
| B2 | `app/listener/order/Pay.php`、`app/services/hjf/PointsRewardServices.php` |
| B3 | `app/services/hjf/PointsRewardServices.php`、`app/listener/order/Pay.php`、`app/jobs/hjf/HjfOrderPayJob.php` |
| B4 | `app/services/hjf/PointsRewardServices.php`,核查 `eb_agent_level` 数据 |
| B5 | `app/services/agent/AgentLevelTaskServices.php`,核查 `eb_agent_level_task` 数据 |
| B6 | `app/services/order/StoreOrderCreateServices.php` |
| B7 | 验证脚本 / SQL 查询(验收 `eb_agent_level` + `eb_agent_level_task` 数据配置) |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 606 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,315 +0,0 @@
# 范氏国香商城小程序 · 产品需求文档PRDV1.0
> 技术底座CRMEB Pro v3.5 二次开发
> 文档日期2026-03-23
> 文档状态:当前分支产品需求基线(对齐 fsgx 需求)
---
## 1. 文档说明
### 1.1 文档目的
本文档基于《范氏国香小程序fsgx-V1.0.docx》整理定义范氏国香商城小程序微信端 + 管理后台)的功能范围、核心业务逻辑、参数配置和交付验收标准,并明确当前分支与目标需求的差异,作为后续研发、测试与运营的统一依据。
### 1.2 文档范围
- 用户端(微信小程序)功能需求
- 管理后台PC端功能需求
- 核心业务逻辑(推荐返现、会员等级、积分释放)
- 数据改造与非功能性要求
- 当前版本不一致/不满足项清单
### 1.3 术语定义
| 术语 | 定义 |
|---|---|
| 报单商品 | 参与推荐奖励与等级业绩统计的商品当前目标主商品为艾制品三条套餐4333元/单) |
| 普通商品 | 不参与推荐奖励的商品,可支持积分支付 |
| 待释放积分 | 推荐奖励入账后冻结的积分,按日释放 |
| 已释放积分 | 已完成释放、可消费的积分,仅可用于普通商品 |
| 直推 | 用户直接邀请并绑定的一级成员 |
| 伞下 | 用户的所有直推及其下级组成的推荐网络 |
| 会员分销等级 | 创客、云店、服务中心、合伙人 |
| 级差 | 上级可获得的奖励与下级当前等级奖励之间的差额机制 |
| 推荐返现循环 | 邀请满 3 人按 20%/30%/50%返现,后续继续按 3 人周期循环 |
### 1.4 版本记录
| 版本 | 日期 | 变更说明 | 负责人 |
|---|---|---|---|
| V1.0 | 2026-03-23 | 首版:按 fsgx 需求重编写,并纳入当前分支差异清单 | AI + 产品/研发 |
---
## 2. 产品概述
### 2.1 产品背景
范氏国香商城定位于大健康艾制品电商场景,通过小程序承载商品销售、社交裂变与会员激励,形成“购买-推荐-复购”的增长闭环。
### 2.2 产品定位
| 维度 | 描述 |
|---|---|
| 产品形态 | 微信小程序 + PC 管理后台 |
| 核心商品 | 艾制品三条套餐4333元/单)及周边商品 |
| 商业模式 | 社交裂变 + 推荐返现 + 会员积分体系 |
| 目标用户 | 有健康消费需求且具备社交分享意愿的用户 |
| 核心差异化 | 邀请三人返现免单机制 + 分级积分激励 |
### 2.3 目标用户
| 用户类型 | 特征 | 核心诉求 |
|---|---|---|
| 普通消费者 | 首购用户,关注商品价值 | 流畅下单与可见返现规则 |
| 推广用户(创客) | 有一定社交资源 | 明确返现进度和积分收益 |
| 团队用户(云店/服务商) | 有管理下级需求 | 团队数据透明、收益可追踪 |
| 平台运营人员 | 负责运营与风控 | 高效配置、可追溯财务与订单 |
### 2.4 产品目标
- 为用户提供明确、可预期的推荐返现与积分成长体验
- 通过返现循环与等级激励提高裂变转化与复购
- 为运营提供“参数可配、过程可查、结果可审计”的后台系统
- 保持 CRMEB 复用能力,降低二开复杂度与维护成本
---
## 3. 核心业务逻辑
### 3.1 推荐返现机制(核心)
#### 3.1.1 基础规则
- 用户 A 邀请的直推成员按“购买报单商品并支付成功”计入有效推荐
- 同一推荐周期按 3 人结算:
- 第 1 人:返现 20%
- 第 2 人:返现 30%
- 第 3 人:返现 50%
- 累计返现 100% 后进入下一轮 3 人循环
- 返现金额进入用户现金余额,可提现(扣除手续费)
#### 3.1.2 可配置参数
- 周期人数(默认 3
- 周期内每位返现比例(默认 20/30/50
- 返现基数(按实付金额/按固定金额)
- 返现发放时机(支付即发放/确认收货后)
#### 3.1.3 返现佣金记录
- 同时记录报单商品订单产生的推荐人佣金明细记录eb_user_brokerage表中status=1type=get_brokeragetitle=推广佣金
#### 3.1.4 异常与边界
- 退款/撤单后,需回滚对应返现及进度
- 同设备/同账号异常刷单需支持风控拦截
- 后台人工取消订单时,需同步返现逆操作并记录审计日志
### 3.2 分销等级体系
| 等级 | 升级条件 | 直推积分奖励 | 伞下积分奖励 | 说明 |
|---|---|---:|---:|---|
| 创客 | 直推满3单 | 500 | 0 | 阈值可配置 |
| 云店 | 伞下满30单至少3直推 | 800 | 300 | 级差计算 |
| 服务商 | 伞下满100单至少3直推 | 1000 | 200 | 级差计算 |
| 分公司 | 伞下满1000单至少3直推 | 1300 | 300 | 级差计算 |
规则补充:
- 等级默认自动升级,允许后台手动调整
- 支持“不考核”开关,标记用户不参与自动考核
- 批量购买触发升级时,先升级后结算剩余单据奖励
### 3.3 账户与积分体系
| 账户 | 来源 | 用途 | 提现 |
|---|---|---|---|
| 现金余额 | 推荐返现、后台充值 | 消费、提现 | 可提现默认7%手续费) |
| 待释放积分 | 等级后推荐奖励 | 按日释放 | 不可提现 |
| 已释放积分 | 每日释放所得 | 购买普通商品 | 不可提现 |
积分规则:
- 日释放比例默认 0.4%(千分之四),后台可调
- 积分不可转赠,不可直接提现
- 报单商品不允许积分支付
- 更新相关推荐会员(直推积分奖励、伞下积分奖励获得者)的待释放积分(`frozen_points`
- 同时记录报单商品订单产生的待释放积分明细记录eb_user_bill表中status=0type=integraltitle=直推积分奖励 | 伞下积分奖励
### 3.4 多单购买与升级联动
- 用户一次购买 N 单报单商品时:
1. 先按新增有效业绩判断是否升级
2. 从达标后剩余单数起,按新等级发放积分奖励
3. 推荐返现按周期规则逐单累计
---
## 4. 用户端功能需求(小程序)
### 4.1 登录与注册
- 微信授权登录、手机号快捷绑定P0
- 推荐关系参数绑定且不可篡改P0
- 新用户规则引导页P1
### 4.2 首页
- Banner 与活动专区P0
- 报单商品与普通商品推荐区P0
- 公告通知P1
### 4.3 商品与购买
- 商品分为报单商品与普通商品
- 订单提交与支付成功后触发:推荐返现 + 等级积分逻辑
- 支付方式:
- 微信/支付宝:所有商品
- 余额:按后台商品配置
- 积分:仅普通商品,且受后台配置控制
### 4.4 推荐裂变
- 专属分享海报和邀请链接
- 推荐关系树可视化展示(直推/伞下)
- 推荐收益列表(返现 + 积分)可追踪到订单
- 推荐返现循环进度展示(当前周期第几人、已返现金额、下一档比例)
### 4.5 个人中心
- 我的订单:状态筛选、详情、物流、售后
- 我的资产:现金余额、待释放积分、已释放积分
- 我的推荐:人数、订单、等级、收益统计
- 提现申请:显示手续费、预计到账金额
---
## 5. 管理后台功能需求PC端
### 5.1 数据看板
- 今日新增、订单、销售额、返现金额
- 推荐转化漏斗(邀请点击 -> 绑定 -> 首购)
- 会员等级分布与升级趋势
### 5.2 用户管理
- 用户查询、详情、关系树
- 等级手动调整、不考核开关
- 账户资金与积分人工调整(带原因与日志)
### 5.3 商品管理
- 报单商品标记 `is_queue_goods`
- 支付方式可配置(余额/积分开关)
- 上下架、库存、预警
### 5.4 订单管理
- 订单检索、详情、发货、售后
- 支持后台取消订单并触发全额返还逻辑
- 取消/退款对返现与积分的冲销记录
### 5.5 财务管理
- 推荐返现流水(按用户/订单/周期)
- 提现申请审核与手续费管理
- 积分发放与释放日志
- 日/月/年报表导出
### 5.6 营销配置中心
- 推荐返现周期人数
- 分段返现比例(如 20/30/50
- 等级升级门槛与奖励值
- 积分释放比例
- 提现手续费
- 返佣范围配置:所有商品 / 仅报单商品
### 5.7 内容与活动管理
- Banner、文章、公告管理
- 活动发布、报名、核销
---
## 6. 与当前版本不一致/不满足功能点(重点)
说明:以下“当前版本”基于当前分支代码与既有 HJF 方案,对照 fsgx V1.0 目标形成。
| 序号 | 功能点 | fsgx 目标要求 | 当前版本现状 | 差异结论 | 优先级 |
|---:|---|---|---|---|---|
| 1 | 核心激励模型 | 邀请三人返现 20/30/50 循环 | 以“公排进四退一”为主展示与规则 | 重大不一致 | P0 |
| 2 | 返现来源定义 | 现金余额主要来源为推荐返现 | 当前语义与实现更偏公排退款 | 业务口径不一致 | P0 |
| 3 | 普通会员奖励 | 普通会员可参与返现(升级后叠加积分) | 当前描述与逻辑偏“升级后才有主要奖励” | 奖励起点不一致 | P0 |
| 4 | 批量购买升级结算 | 先升级再结算剩余单奖励 | 当前未形成明确落地逻辑 | 功能缺失 | P0 |
| 5 | 后台取消订单返还 | 支持后台取消后全额返还 | 当前规则与流程未完整覆盖 | 功能缺失 | P0 |
| 6 | 营销参数模型 | 返现比例与人数可自由配置 | 当前仍有公排触发倍数相关配置 | 参数模型不一致 | P0 |
| 7 | 返佣范围配置 | 支持“所有商品/报单商品” | 已提出问题项,未确认完整落地 | 功能不完整 | P1 |
| 8 | 商品编辑 is_queue_goods | 保存后需正确落库 | 已有缺陷记录:修改后未更新数据库 | 缺陷待修复 | P0 |
| 9 | 前端文案与页面结构 | 以推荐返现为主叙事 | 当前存在 queue 公排页面及文案 | 交互叙事不一致 | P1 |
| 10 | Mock 到真实接口收敛 | 关键模块应接入真实 API | 部分 HJF 模块仍为 USE_MOCK | 集成不完整 | P1 |
建议验收标准(针对差异):
- 返现配置改动后,下一笔有效订单按新规则生效并可追溯
- 推荐三人循环在用户端可见进度且与财务流水一致
- 取消订单时,返现与积分冲销一致且无脏账
- `is_queue_goods` 在商品编辑保存后稳定落库并影响支付/奖励逻辑
---
## 7. 数据库与接口改造方案
### 7.1 数据模型建议
- 返现进度表:记录用户返现周期状态(周期索引、当前位次、累计返现金额)
- 返现流水表:记录返现发放/冲销明细(来源订单、比例、金额、状态)
- 奖励配置表:支持“周期人数 + 分段比例”可配置
- 订单扩展字段:记录是否参与返现、返现计算版本
### 7.2 关键接口建议
- `GET /api/rebate/progress`:用户当前返现周期进度
- `GET /api/rebate/logs`:返现流水列表
- `PUT /admin/rebate/config`:返现配置管理
- `POST /admin/order/cancel_refund`:后台取消并触发返还/冲销
### 7.3 兼容策略
- 保留旧数据结构,新增版本号字段区分公排历史与返现新逻辑
- 新逻辑灰度开关,允许按时间或商品范围切换
- 财务报表提供历史口径与新口径并行查询
---
## 8. 非功能性需求
### 8.1 性能
- 下单后奖励计算链路接口 P95 < 300ms异步结算除外
- 财务流水分页查询在 10 万级数据下响应 < 2s
- 每日积分释放任务在 5 分钟内完成
### 8.2 一致性与安全
- 奖励发放、冲销、提现须事务一致
- 订单状态变化与奖励状态双向校验,避免重复发放
- 所有金额计算使用高精度方案,禁止浮点误差
- 关键操作(配置变更、人工调账、取消返还)必须审计留痕
### 8.3 可运维性
- 关键任务提供失败重试与报警
- 提供“按订单重算奖励”工具接口(仅管理员可用)
- 配置变更需记录操作者、变更前后值、生效时间
---
## 附录 A本版本实施优先级建议
- P0首批上线核心返现引擎、订单取消返还、商品报单标记修复、财务流水闭环
- P1第二阶段推荐进度可视化、营销配置增强、Mock 全量切换真实接口
- P2优化阶段风控规则、运营分析看板深化、批量重算工具

View File

@@ -1,24 +0,0 @@
#!/usr/bin/env bash
# 诊断 H5 部署问题Unexpected token '<'
# 在服务器上检查文件是否存在、index.html 引用、Nginx root
set -e
REMOTE="root@182.92.142.158"
REMOTE_PUBLIC="/www/wwwroot/hjf.suzhouyuqi.com/public"
echo "=== 1. 检查 index.html 引用的 JS 文件 ==="
ssh "$REMOTE" "grep -oE 'src=[^>]+\.js' $REMOTE_PUBLIC/index.html 2>/dev/null || echo 'index.html 不存在或无法读取'"
echo ""
echo "=== 2. 检查 static/js 下实际存在的文件 ==="
ssh "$REMOTE" "ls $REMOTE_PUBLIC/static/js/index.*.js $REMOTE_PUBLIC/static/js/chunk-vendors.*.js 2>/dev/null || echo '文件不存在'"
echo ""
echo "=== 3. 直接请求 JS 看返回类型(应为 application/javascript ==="
curl -sI "https://hjf.suzhouyuqi.com/static/js/chunk-vendors.54d49a5a.js" | head -5
echo ""
echo "=== 4. 若上面 Content-Type 不是 javascript说明 Nginx 未正确提供静态文件 ==="
echo "请在宝塔/面板中检查站点配置:"
echo " - root 必须为: $REMOTE_PUBLIC"
echo " - 添加 location ~ ^/(static|assets|pages)/ { try_files \$uri =404; }"
echo "参考: docs/nginx-hjf-cloud.conf.example"

View File

@@ -1,35 +0,0 @@
#!/usr/bin/env bash
# H5 前端一键部署到云服务器(需 sshpass或已配置 SSH 免密)
# 支持 HBuilder 导出 web 或 h5 路径
set -e
cd "$(dirname "$0")/.."
REMOTE="root@182.92.142.158"
REMOTE_PUBLIC="/www/wwwroot/hjf.suzhouyuqi.com/public"
BACKUP_DIR="/www/wwwroot/backup"
# 优先使用 web其次 h5
if [ -d "pro_v3.5.1/view/uniapp/unpackage/dist/build/web" ]; then
H5_SRC="pro_v3.5.1/view/uniapp/unpackage/dist/build/web"
elif [ -d "pro_v3.5.1/view/uniapp/unpackage/dist/build/h5" ]; then
H5_SRC="pro_v3.5.1/view/uniapp/unpackage/dist/build/h5"
else
echo "错误:未找到 H5 构建产物web 或 h5请先用 HBuilder 或 npm run build:h5 打包"
exit 1
fi
echo "备份云服务器 public 目录(上一版本)..."
ssh "$REMOTE" "mkdir -p $BACKUP_DIR && tar -czf $BACKUP_DIR/hjf_public_\$(date +%Y%m%d_%H%M%S).tar.gz -C /www/wwwroot/hjf.suzhouyuqi.com public && echo '备份完成'"
echo "上传 H5 到 $REMOTE:$REMOTE_PUBLIC ..."
# 先删除旧的 index.html、static、assets、pages避免 /h5/ 路径残留
ssh "$REMOTE" "cd $REMOTE_PUBLIC && rm -f index.html && rm -rf static assets pages 2>/dev/null; true"
tar czf - -C "$H5_SRC" . | ssh "$REMOTE" "cd $REMOTE_PUBLIC && tar xzf -"
echo "修改权限 ..."
ssh "$REMOTE" "chown -R www:www $REMOTE_PUBLIC && chmod -R 755 $REMOTE_PUBLIC"
echo ""
echo "验证 index.html 引用路径(应为 /static/ 而非 /h5/static/"
ssh "$REMOTE" "grep -oE 'src=[^>]+\.js' $REMOTE_PUBLIC/index.html 2>/dev/null | head -3"
echo ""
echo "部署完成。访问 https://hjf.suzhouyuqi.com/ 验证。若仍报 Unexpected token '<',见 docs/deploy.md 4.7 节"

View File

@@ -17,49 +17,7 @@
|--------|----------|----------------|
| 服务器 API | `pro_v3.5.1/`(排除 view、public/admin | `/www/wwwroot/hjf.suzhouyuqi.com/` |
| 管理后台前端 | `pro_v3.5.1/view/admin/dist/` | `/www/wwwroot/hjf.suzhouyuqi.com/public/admin/` |
| H5 前端 | `pro_v3.5.1/view/uniapp/unpackage/dist/build/web/``.../h5/` | `/www/wwwroot/hjf.suzhouyuqi.com/public/` |
### 云服务器 Nginx 配置要求H5 根路径访问必备)
域名根路径 `https://hjf.suzhouyuqi.com/` 需正确返回 H5 页面。若出现 `Unexpected token '<'`(请求 JS 时返回 HTML通常是 Nginx 未正确提供静态文件。
**必须满足:**
1. **站点 root 指向 public**`root /www/wwwroot/hjf.suzhouyuqi.com/public;`
2. **index 顺序**`index index.html index.php;`(优先 index.html
3. **静态文件优先**:对 `/static/``/assets/``/pages/` 等路径Nginx 应先尝试本地文件,**仅当文件不存在**时才转发到 PHP/Swoole。
**推荐配置示例**(宝塔/ LNMP 站点):
```nginx
server {
listen 80;
listen 443 ssl http2;
server_name hjf.suzhouyuqi.com;
root /www/wwwroot/hjf.suzhouyuqi.com/public;
index index.html index.php;
# 静态资源:直接由 Nginx 提供,不转发到 PHP
location ~ ^/(static|assets|pages)/ {
try_files $uri =404;
expires 7d;
}
# PHP 请求
location ~ \.php$ {
# 转发到 Swoole 或 php-fpm按现有配置
}
# 其余请求:先找静态文件,再走 PHP
location / {
try_files $uri $uri/ /index.php?$query_string;
}
}
```
若使用 `if (!-e $request_filename) { proxy_pass ... }`,需确保**文件存在时不会进入 proxy**,否则 `/static/js/xxx.js` 会被错误地返回 HTML。
完整示例见 `docs/nginx-hjf-cloud.conf.example`
| H5 前端 | `pro_v3.5.1/view/uniapp/unpackage/dist/build/h5/` | `/www/wwwroot/hjf.suzhouyuqi.com/public/` |
---
@@ -194,8 +152,6 @@ tar -czvf /www/backup/hjf_admin_$(date +%Y%m%d_%H%M%S).tar.gz \
### 3.3 上传(本地)
**方式 Arsync**(服务器需已安装 rsync
```bash
cd /Users/apple/scott2026/huangjingfen
@@ -204,15 +160,6 @@ rsync -avz --delete \
root@182.92.142.158:/www/wwwroot/hjf.suzhouyuqi.com/public/admin/
```
**方式 Btar + ssh**(服务器无 rsync 时使用)
```bash
cd /Users/apple/scott2026/huangjingfen
tar czf - -C pro_v3.5.1/view/admin/dist . | \
ssh root@182.92.142.158 "cd /www/wwwroot/hjf.suzhouyuqi.com/public && rm -rf admin && mkdir -p admin && tar xzf - -C admin"
```
### 3.4 上传后修改权限(服务器)
```bash
@@ -226,56 +173,13 @@ chmod -R 775 /www/wwwroot/hjf.suzhouyuqi.com/public/admin
浏览器访问 `http://hjf.suzhouyuqi.com/admin/login`,确认页面正常。
### 3.6 本地环境部署
将管理后台前端打包并部署到本机,配合 `nginx-crmeb.conf` 与 Swoole 使用。
**前置条件:**
- Nginx 已加载 `pro_v3.5.1/nginx-crmeb.conf``root` 指向 `pro_v3.5.1/public`80 反代到 20199
- Swoole API 已启动:`./help/start-api.sh``php -d memory_limit=300M think swoole`
**步骤:**
```bash
# 1. 本地构建(同 3.1
cd /Users/apple/scott2026/huangjingfen/pro_v3.5.1/view/admin
npm install # 依赖有变更时执行
npm run build
# 2. 将构建产物复制到 public/admin覆盖旧文件
cd /Users/apple/scott2026/huangjingfen
rm -rf pro_v3.5.1/public/admin
cp -r pro_v3.5.1/view/admin/dist pro_v3.5.1/public/admin
```
或使用 rsync保留权限、便于增量更新
```bash
cd /Users/apple/scott2026/huangjingfen
rsync -av --delete pro_v3.5.1/view/admin/dist/ pro_v3.5.1/public/admin/
```
**验证:**
浏览器访问 `http://127.0.0.1/admin/``http://127.0.0.1/admin/login`,确认管理后台页面正常。
---
## 四、子项目三H5 前端
仅部署 H5 构建产物到 `public/`(站点根目录)。部署时仅覆盖 H5 相关文件,不删除 `admin/``index.php` 等。
仅部署 H5 构建产物到 `public/`(站点根目录)。
### 4.1 本地打包HBuilder 或 npm
**方式 AHBuilder 打包(推荐)**
1. 用 HBuilderX 打开 `pro_v3.5.1/view/uniapp` 项目
2. 确认 `config/app.js``BASE_HOST` 已设为云服务器域名(如 `hjf.suzhouyuqi.com`
3. 菜单:发行 → 网站-H5 手机版(或 Web → 填写网站标题 → 发行
4. 构建产物在 `view/uniapp/unpackage/dist/build/web/``.../build/h5/`
**方式 B命令行构建**
### 4.1 本地构建
```bash
cd /Users/apple/scott2026/huangjingfen/pro_v3.5.1/view/uniapp
@@ -286,24 +190,18 @@ npm run build:h5
构建产物在 `view/uniapp/unpackage/dist/build/h5/`
### 4.2 备份(服务器,上传前执行
上传前在服务器备份 `public` 目录,便于回滚:
### 4.2 备份(服务器)
```bash
ssh root@182.92.142.158
mkdir -p /www/wwwroot/backup
tar -czvf /www/wwwroot/backup/hjf_public_$(date +%Y%m%d_%H%M%S).tar.gz \
mkdir -p /www/backup
tar -czvf /www/backup/hjf_public_$(date +%Y%m%d_%H%M%S).tar.gz \
-C /www/wwwroot/hjf.suzhouyuqi.com public
```
> 一键部署脚本 `deploy-h5.sh` 会自动执行此备份后再上传。
### 4.3 上传(本地)
**方式 Arsync**(服务器需已安装 rsync
```bash
cd /Users/apple/scott2026/huangjingfen
@@ -314,122 +212,9 @@ rsync -avz \
> 不使用 `--delete`,避免覆盖或删除 `admin/`、`index.php` 等文件。
**方式 Btar + ssh**(服务器无 rsync 时使用)
### 4.4 验证
```bash
cd /Users/apple/scott2026/huangjingfen
tar czf - -C pro_v3.5.1/view/uniapp/unpackage/dist/build/h5 . | \
ssh root@182.92.142.158 "cd /www/wwwroot/hjf.suzhouyuqi.com/public && tar xzf -"
```
> 仅解压覆盖 H5 文件index.html、static/ 等),不删除 `admin/`。
**方式 Cscp**
```bash
cd /Users/apple/scott2026/huangjingfen
scp -r pro_v3.5.1/view/uniapp/unpackage/dist/build/h5/* \
root@182.92.142.158:/www/wwwroot/hjf.suzhouyuqi.com/public/
```
### 4.4 上传后修改权限(服务器)
```bash
ssh root@182.92.142.158
chown -R www:www /www/wwwroot/hjf.suzhouyuqi.com/public
chmod -R 755 /www/wwwroot/hjf.suzhouyuqi.com/public
```
> 若仅更新 H5 根目录下的 index.html、static 等,可只对相关路径执行:
> `chown -R www:www /www/wwwroot/hjf.suzhouyuqi.com/public/index.html /www/wwwroot/hjf.suzhouyuqi.com/public/static`
### 4.5 一键部署脚本(本地执行)
脚本 `docs/deploy-h5.sh` 会依次执行:**备份上一版本 → 上传 → 修改权限**。执行前确保已打包好 H5`unpackage/dist/build/web/``h5/` 存在):
```bash
#!/usr/bin/env bash
# H5 前端一键部署到云服务器(需 sshpass或已配置 SSH 免密)
# 支持 HBuilder 导出 web 或 h5 路径
set -e
cd "$(dirname "$0")/.."
REMOTE="root@182.92.142.158"
REMOTE_PUBLIC="/www/wwwroot/hjf.suzhouyuqi.com/public"
BACKUP_DIR="/www/wwwroot/backup"
# 优先使用 web其次 h5
if [ -d "pro_v3.5.1/view/uniapp/unpackage/dist/build/web" ]; then
H5_SRC="pro_v3.5.1/view/uniapp/unpackage/dist/build/web"
elif [ -d "pro_v3.5.1/view/uniapp/unpackage/dist/build/h5" ]; then
H5_SRC="pro_v3.5.1/view/uniapp/unpackage/dist/build/h5"
else
echo "错误:未找到 H5 构建产物,请先用 HBuilder 或 npm run build:h5 打包"
exit 1
fi
echo "备份云服务器 public 目录(上一版本)..."
ssh "$REMOTE" "mkdir -p $BACKUP_DIR && tar -czf $BACKUP_DIR/hjf_public_\$(date +%Y%m%d_%H%M%S).tar.gz -C /www/wwwroot/hjf.suzhouyuqi.com public && echo '备份完成'"
echo "上传 H5 到 $REMOTE:$REMOTE_PUBLIC ..."
tar czf - -C "$H5_SRC" . | ssh "$REMOTE" "cd $REMOTE_PUBLIC && tar xzf -"
echo "修改权限 ..."
ssh "$REMOTE" "chown -R www:www $REMOTE_PUBLIC && chmod -R 755 $REMOTE_PUBLIC"
echo "部署完成,请访问 https://hjf.suzhouyuqi.com/ 验证"
```
用法:
```bash
cd /Users/apple/scott2026/huangjingfen
chmod +x docs/deploy-h5.sh
./docs/deploy-h5.sh
```
若需密码,可安装 `sshpass` 后使用:`SSHPASS='A@123456' sshpass -e ./docs/deploy-h5.sh`
### 4.6 验证
浏览器访问 `http://hjf.suzhouyuqi.com/``https://hjf.suzhouyuqi.com/`,确认 H5 页面正常。
### 4.7 H5 报错 `Unexpected token '<'` 排查
**现象**:访问根路径时控制台报 `index.xxx.js:1 Uncaught SyntaxError: Unexpected token '<'`
**原因**:请求 JS 时服务器返回了 HTML404 页或 index.php 输出)。常见情况:
- **index.html 引用 `/h5/static/...`**:旧版 index 与当前部署路径不一致,`/h5/static/js/xxx.js` 返回 404 → HTML
- **Nginx 未正确提供静态文件**root 未指向 `.../public` 或缺少 `location ~ ^/(static|assets|pages)/`
**排查步骤**
1. **检查 index.html 引用路径**
```bash
curl -s "https://hjf.suzhouyuqi.com/" | grep -oE 'src=[^>]+\.js'
```
若出现 `/h5/static/`,说明服务器 index.html 为旧版,需重新部署。
2. **重新构建并部署**(部署脚本会先清理旧 index.html、static 等):
```bash
# 本地HBuilder 发行 H5 或 npm run build:h5
./docs/deploy-h5.sh
```
3. **确认 Nginx root**:站点 root 必须为 `.../public`,见上文「云服务器 Nginx 配置要求」。
4. **清除浏览器缓存**强制刷新Ctrl+Shift+R或无痕模式访问。
---
## 四(补充)、管理后台登录滑块:`NOAUTH Authentication required`
若访问 `/adminapi/ajcaptcha` 或 `/adminapi/is_captcha` 返回 `{"msg":"NOAUTH Authentication required."}`,说明 **Redis 需要密码但 `.env` 中 `[REDIS]` 的 `PASSWORD` 与服务器 Redis 不一致**(或 Redis 开启了 `requirepass` 而 `.env` 为空)。
**处理:**
1. 在服务器上核对 Redis`redis-cli -h <主机> -p 6379 -a '<实际密码>' ping`,将正确密码写入站点根目录 `.env` 的 `PASSWORD = ...`。
2. 重启 Swoole / PHP 进程使配置生效。
3. 项目已把 **滑块验证码ajcaptcha缓存改为本地 file**,不依赖 Redis 认证;部署后请同步更新 `config/ajcaptcha.php`。登录次数统计等仍走 Redis**长期仍须修正 Redis 配置**。
浏览器访问 `http://hjf.suzhouyuqi.com/`,确认 H5 页面正常。
---
@@ -462,5 +247,5 @@ tar -xzvf /www/backup/hjf_admin_YYYYMMDD_HHMMSS.tar.gz -C /www/wwwroot/hjf.suzho
# 回滚 H5恢复 public/ 目录)
rm -rf /www/wwwroot/hjf.suzhouyuqi.com/public
tar -xzvf /www/wwwroot/backup/hjf_public_YYYYMMDD_HHMMSS.tar.gz -C /www/wwwroot/hjf.suzhouyuqi.com
tar -xzvf /www/backup/hjf_public_YYYYMMDD_HHMMSS.tar.gz -C /www/wwwroot/hjf.suzhouyuqi.com
```

View File

@@ -1,289 +0,0 @@
# 黄精粉健康商城 · 剩余开发任务执行方案
> 基于 PRD_V2.md + openclaw-frontend-tasks.md 的现状分析
> 制定日期2026-03-15
> 当前分支claude/hjf-queue-admin-apis-hsymG
---
## 一、现状盘点(已完成 vs 待完成)
### ✅ 已完成的任务
| 阶段 | 任务 | 说明 |
|------|------|------|
| Phase 0 | P0-01, P0-02 | UniApp + Admin Mock 数据文件 |
| Stage 1A | P1A-01~06 | 全部6个 API 模块uniapp + admin |
| Stage 1B | P1B-01~04 | 全部4个公共组件QueueProgress / AssetCard / MemberBadge / RefundNotice |
| Stage 1C | P1C-01~06 | 全部6个新 UniApp 页面(公排状态/历史/规则 + 资产总览/积分明细 + 引导页) |
| Stage 1D | P1D-02 | 商品详情页:`is_queue_goods` 角标 + 公排提示条 |
| Stage 1D | P1D-03 | 购买确认页:多单拆分提示 + 公排入队说明 |
| Stage 1D | P1D-04 | 支付结果页:公排入队成功提示 + 查看公排入口 |
| Stage 1D | P1D-06 | 提现页7% 手续费动态计算 + 提示文案 |
| Stage 1E | P1E-01~06 | 全部6个 Admin 新页面(公排订单/财务/配置 + 会员管理/配置 + 积分日志) |
| Stage 1F | P1F-01~07 | 全部路由注册pages.json + Admin hjfQueue.js 路由模块 + index.js 导入) |
| Stage 1G | P1G-01 | Admin 用户管理列表:`member_level``no_assess` 列和筛选项 |
---
### ⏳ 待完成的任务(本方案覆盖范围)
```
Phase 1 尾单4项
├── P1D-01 首页:报单商品角标 + 公排入口Banner
├── P1D-05 推荐收益页:积分替换佣金显示
├── P1D-07 个人中心HjfMemberBadge等级徽章嵌入
└── P1G-02 Admin商品编辑报单标记 + 积分支付白名单
Phase 2 数据库迁移5项
Phase 3 后端 API 开发16项
Phase 4 前后端联调集成5项
Phase 5 完整测试8项
```
---
## 二、执行方案
### 阶段 APhase 1 收尾(前端,可立即执行)
> 依赖:无。可在当前 Mock 模式下独立完成。
> 目标:让 Phase 1 所有38个任务全部 `[x]`,解锁 CP-01 评审检查点。
---
#### 任务 A1 — P1D-01首页报单商品角标
**文件**`pro_v3.5.1/view/uniapp/pages/index/index.vue`
**改造内容**
1. 在商品列表卡片的商品名/图片处,检查 `item.is_queue_goods == 1`,叠加渲染 `报单` 角标(红色标签,参考 goods_details 中已有的 `.queue-goods-tag` 样式)。
2. 在首页 Banner 区或活动专区下方,增加"公排进度"快捷入口行(复用 `HjfQueueProgress` 组件缩略版,或仅放文字按钮跳转 `/pages/queue/status`)。
3. 无需新增 API 调用,角标从商品列表字段 `is_queue_goods` 读取即可。
**验收标准**:报单商品卡片右上角出现红色"报单"角标;点击公排入口可跳转公排状态页。
---
#### 任务 A2 — P1D-05推荐收益页积分替换佣金
**文件**`pro_v3.5.1/view/uniapp/pages/users/user_spread_money/index.vue`
**改造内容**
1. 将列表中"佣金"字样统一替换为"积分",金额字段从 `money`/`commission` 改为读取 `points`
2. 展示积分类型标签:`reward_direct`(直推奖励)/ `reward_umbrella`(伞下奖励)。
3. 导入 `import { getTeamIncome } from '@/api/hjfMember.js'`,替换原有 API 调用。
4. 数值格式:整数积分,不保留小数;去掉 `¥` 符号,改为"积分"后缀。
**验收标准**:推荐收益列表显示积分数量而非金额,类型标签正确区分直推/伞下。
---
#### 任务 A3 — P1D-07个人中心会员等级徽章
**文件**`pro_v3.5.1/view/uniapp/pages/user/index.vue`
**改造内容**
1. 引入 `HjfMemberBadge` 组件,在用户头像/昵称旁嵌入等级徽章。
```js
import HjfMemberBadge from '@/components/HjfMemberBadge.vue'
```
2. 从 `getMemberInfo` API已有 Mock获取 `member_level`,传入组件 `:level` prop。
3. 在资产快捷入口区域已有的 `hjf-nav-row` 基础上,补充"待释放积分"数值预览(展示 `frozen_points`,不换页即可看到大数字)。
4. 已有的公排查询 + 资产入口导航行保持不变,不重复建设。
**验收标准**:昵称旁出现对应等级的彩色徽章;资产行显示待释放积分数。
---
#### 任务 A4 — P1G-02Admin 商品编辑-报单标记与支付方式
**文件**`pro_v3.5.1/view/admin/src/pages/product/creatProduct/index.vue`
**改造内容**
1. 在商品基本信息 Tab 中增加"报单商品"开关iView `i-switch`),绑定 `formValidate.is_queue_goods`,默认 `false`。
2. 在支付方式 Tab / 销售设置区域,增加复选框组"允许积分支付"`allow_pay_types`),选项:`待释放积分`、`已释放积分`;报单商品开关开启时,此项置灰并强制清空。
3. 表单提交时将 `is_queue_goods`0/1和 `allow_pay_types`(数组序列化)一并提交。
4. 编辑回显时正确反填两个字段。
**验收标准**:新建/编辑商品可设置报单标记;报单商品自动禁用积分支付选项。
---
### 阶段 BPhase 2 数据库迁移
> 依赖后端开发环境就绪ThinkPHP 8 + MySQL 8.0)。
> 建议由后端工程师执行,前端工程师无需等待此阶段。
| 任务 | 操作 | 目标 |
|------|------|------|
| P2-01 | CREATE TABLE | `eb_queue_pool`公排池9个字段含复合索引 |
| P2-02 | CREATE TABLE | `eb_points_release_log`积分释放日志7个字段 |
| P2-03 | ALTER TABLE | `eb_user` 增加4字段`member_level`、`no_assess`、`frozen_points`、`available_points` |
| P2-04 | ALTER TABLE | `eb_store_product` 增加2字段`is_queue_goods`、`allow_pay_types` |
| P2-05 | INSERT | `eb_system_config` 插入9项系统配置键值对公排倍数、释放比例、手续费率、各等级门槛和奖励 |
**关键索引P2-01**
```sql
INDEX idx_uid (uid),
INDEX idx_status_add_time (status, add_time),
INDEX idx_queue_no (queue_no),
INDEX idx_trigger_batch (trigger_batch)
```
---
### 阶段 CPhase 3 后端 API 开发
> 依赖Phase 2 完成。
> 开发顺序:先核心引擎,再外围接口,最后定时任务。
#### C1 — 公排引擎(优先级最高)
| 任务 | 文件/类 | 内容 |
|------|---------|------|
| P3-01 | `QueuePool` Service | 入队逻辑(写 `eb_queue_pool`Redis 分布式锁防并发) |
| P3-02 | `QueueRefund` Service | 退款触发逻辑每入N单检查退款使用 think-queue 异步处理) |
| P3-03 | `QueueController` | 用户端接口:`GET /hjf/queue/status`、`GET /hjf/queue/history` |
| P3-04 | `AdminQueueController` | Admin接口`GET /hjf/queue/order`、`GET /hjf/queue/config`、`POST /hjf/queue/config`、`GET /hjf/queue/finance` |
**核心逻辑要点**
- 支付回调成功后:判断 `is_queue_goods` → 多单拆分 → 逐单调用 `QueuePool::enqueue()` → 检查触发条件
- Redis Key`hjf:queue:lock`(分布式锁),`hjf:queue:pending_count`(待触发计数)
- 退款写入 `eb_user.now_money`(复用 CRMEB 余额字段),记录 `eb_user_bill`
#### C2 — 积分奖励引擎
| 任务 | 文件/类 | 内容 |
|------|---------|------|
| P3-05 | `PointsReward` Service | 级差计算:按会员等级发放直推/伞下积分,写入 `frozen_points` |
| P3-06 | `PointsRelease` Job | 每日凌晨定时任务:`frozen_points × 0.4‰ → available_points`,写 `eb_points_release_log` |
| P3-07 | `PointsController` | 用户端接口:`GET /hjf/points/detail`5类型筛选分页 |
| P3-08 | `AdminPointsController` | Admin接口`GET /hjf/points/release-log` |
**每日释放公式**`release_amount = FLOOR(frozen_points × rate / 1000)``rate` 取系统配置 `hjf_release_rate`(默认 4
#### C3 — 会员等级体系
| 任务 | 文件/类 | 内容 |
|------|---------|------|
| P3-09 | `MemberLevel` Service | 升级判断:直推单数 / 伞下业绩单数达标后自动升级;伞下业绩分离逻辑 |
| P3-10 | `AdminMemberController` | Admin接口`GET /hjf/member/list`、`PUT /hjf/member/level/:uid`、`GET/POST /hjf/member/config` |
**升级触发时机**:每次订单支付回调完成后,对推荐链上的所有上级异步检查升级条件。
#### C4 — 资产接口
| 任务 | 文件/类 | 内容 |
|------|---------|------|
| P3-11 | `AssetsController` | `GET /hjf/assets/overview`:返回余额 + 积分汇总(复用 `eb_user` 字段) |
| P3-12 | `AssetsController` | `GET /hjf/assets/cash/detail`:现金流水(分页,复用 `eb_user_bill` |
#### C5 — 路由注册
| 任务 | 内容 |
|------|------|
| P3-13 | `route/api.php`:注册用户端全部 hjf 路由(含鉴权中间件) |
| P3-14 | `route/admin.php`:注册 Admin 端全部 hjf 路由(含权限中间件) |
#### C6 — 单元测试桩
| 任务 | 内容 |
|------|------|
| P3-15 | 公排引擎单元测试:入队/触发退款/分布式锁 |
| P3-16 | 积分计算单元测试:级差计算/每日释放精度bcmath |
---
### 阶段 DPhase 4 前后端联调集成
> 依赖Phase 2 + Phase 3 完成,测试环境可访问真实 API。
| 任务 | 内容 | 操作 |
|------|------|------|
| P4-01 | 关闭 Mock 开关 | 将所有 `const USE_MOCK = true` 改为 `false`UniApp + Admin 共8个文件 |
| P4-02 | UniApp 冒烟测试 | 登录 → 查看公排状态 → 资产总览 → 积分明细 → 推荐收益 |
| P4-03 | Admin 冒烟测试 | 公排订单列表 → 公排配置保存 → 会员等级调整 → 积分日志查询 |
| P4-04 | 支付回调联调 | 测试购买报单商品 → 公排入队 → 积分发放 → 等级升级完整链路 |
| P4-05 | 定时任务验证 | 手动触发每日积分释放任务,验证 `release_log` 记录正确 |
**Mock 关闭检查清单**
```
uniapp/api/hjfQueue.js USE_MOCK → false
uniapp/api/hjfAssets.js USE_MOCK → false
uniapp/api/hjfMember.js USE_MOCK → false
admin/src/api/hjfQueue.js USE_MOCK → false
admin/src/api/hjfMember.js USE_MOCK → false
admin/src/api/hjfPoints.js USE_MOCK → false
```
---
### 阶段 EPhase 5 完整测试
> 依赖Phase 4 联调通过。
| 任务 | 类型 | 内容 |
|------|------|------|
| P5-01 | 前端渲染测试 | 所有新页面在3个Mock场景(A/B/C)下截图验收 |
| P5-02 | 后端接口测试 | 用 Postman/Apifox 验证所有 P3 接口的响应格式和边界值 |
| P5-03 | 公排边界测试 | 精确触发第4单入队时退款到第1单多人同时入队并发锁 |
| P5-04 | 积分精度测试 | bcmath 计算:`1000000 × 4 / 1000 = 4000`(无浮点误差) |
| P5-05 | 会员升级测试 | 直推3单后自动升级创客伞下30单升云店业绩分离逻辑 |
| P5-06 | 并发压测 | 1000并发用户同时访问公排状态页公排入队 200 TPS |
| P5-07 | E2E 全流程 | 新用户注册 → 引导页 → 购买报单商品 → 等待公排退款 → 申请提现 |
| P5-08 | 回归测试 | CRMEB 原有功能(登录/商品/订单/支付)未被改造影响 |
---
## 三、执行优先级与分工建议
```
立即可执行无依赖Agent 可直接实施)
├── A1 首页报单角标 ← 最简单约30分钟
├── A2 推荐收益页积分替换 ← 约45分钟
├── A3 个人中心等级徽章 ← 约30分钟
└── A4 Admin商品编辑改造 ← 约60分钟
等待后端就绪(并行推进)
├── B 数据库迁移 ← DBA/后端工程师
├── C 后端API开发 ← 后端工程师C1优先
└── D 联调集成 ← 前后端协作
最终验收
└── E 完整测试 ← 测试工程师
```
**关键路径**`A1~A4 完成` → `CP-01 评审` → `B+C 并行` → `D 联调` → `E 测试`
---
## 四、风险点与注意事项
| 风险 | 描述 | 应对措施 |
|------|------|---------|
| 公排并发竞争 | 多单同时入队可能重复触发退款 | Redis `SET NX EX` 分布式锁,退款前二次检查状态 |
| 积分浮点误差 | `3600 × 0.4‰` 在 PHP 中存在精度问题 | 全程使用 `bcmath``bcmul($points, '4', 0)` → `bcdiv(..., '1000', 0)` |
| 伞下业绩分离 | 下级升级后业绩需从上级扣除 | 升级事件写入消息队列,异步重算上级业绩;加数据库事务 |
| Admin 路由权限 | hjf 新路由需配置到角色权限表 | P3-14 后端路由注册时同步写 `eb_system_menus` |
| CRMEB 原生字段冲突 | `eb_user` 新增字段可能影响原有查询 | ALTER TABLE 使用 `DEFAULT 0`,不破坏现有 NULL 约束 |
---
## 五、当前可立即下达的指令Agent 参考)
按优先级排序,每条指令对应一个独立任务,完成后 `git commit` 即可:
```
1. feat(P1D-01): 首页报单商品角标与公排快捷入口
文件: pages/index/index.vue
2. feat(P1D-05): 推荐收益页积分替换佣金
文件: pages/users/user_spread_money/index.vue
3. feat(P1D-07): 个人中心嵌入HjfMemberBadge等级徽章
文件: pages/user/index.vue
4. feat(P1G-02): Admin商品编辑报单标记与积分支付配置
文件: admin/src/pages/product/creatProduct/index.vue
```

View File

@@ -1,35 +0,0 @@
# uniapp移动端
## 佣金记录页面
- 1. **已修复**点击“查看全部”时出现参数错误,是不是路径错误?
## 提现页面pages/users/user_cash/index
- 1. **已修复**提现到余额,不收取手续费
## 佣金状态页面pages/queue/status
- 1. **已修复**顶部保持和pages/users/user_cash/index页面中的class="cash-withdrawal"的背影色样式一致
## 我的资产页面pages/assets/index
- 1. **已修复**顶部保持和pages/users/user_cash/index页面中的class="cash-withdrawal"的背影色样式一致
# API接口
## fsgx每日积分释放
- 1. 手动触发接口就释放一次当日积分,没有看到用户的冻结积分被释放,没有看到任务执行记录
请求接口https://www.fsgx.cn/adminapi/system/timer/run_now/21
响应 ```{"status":200,"msg":"任务已触发并执行成功","data":{"result":null}}```
---
# 其他测试问题
## **测试问题**
- 1. **已修复**返现佣金计算不对
A. **已修复**目前新的分销员佣金是按20%20%30%50%来计算的前2个直推订单返现佣金都是20%,不对,应该是按配置的佣金分档比例(JSON)”[20,30,50]“的来轮巡执行佣金计算
B. **已修复**一次下单购买多个报单商品的情况佣金计算不对目前按当前佣金比例的N倍来计算应该按配置的佣金分档比例(JSON)”[20,30,50]“的来执行轮巡执行佣金计算
# 相关文件
- 1. **相关文件**`docs/PRD_fsgx_V1.0.md` `docs/page-dev-specs-fsgx.md``.cursor/plans/fix_issues_0325-1_f8488785.plan.md`

View File

@@ -1,46 +0,0 @@
# 测试问题
## 分销员的**直推积分奖励**问题1
- **已检查**当前用户UID=30推荐UID=34推荐UID=31现在UID=31购买的报单商品订单
检查UID=30创客是否有直推积分奖励如果有是不对的原因UID=34UID=30都是创客分销员一个订单在同一个级别的分销员只能获得一次直推积分奖励且是直推关系。
## 分销员的**直推积分奖励**问题2
- **已检查**case1: 当前用户UID=2推荐UID=46推荐UID=47现在UID=47购买的报单商品订单情况下目前数据库中有订单和用户数据验证如下
检查UID=46是创客
UID=2是否有直推积分奖励如果没有是不对的原因UID=2分销等级>UID=46,是应该获得直推积分奖励的差额部分,使用**直推积分奖励**的递归计算方法,逐一查询上级的直推积分奖励**直到最高分销等级结束**
检查UID=46还不是创客
UID=2是否有直推积分奖励如果有是不对的原因不是直推关系
- **已检查**case2: 当前用户UID=46推荐UID=47推荐UID=48现在UID=48购买的报单商品订单情况下目前数据库中有订单和用户数据验证如下
检查UID=46应该没有直推积分奖励才对因为UID=46不是UID=48的直接推荐人
- **已检查** **直推积分奖励**落库的时候一定要检查直推关系。
## 分销员的**直推积分奖励**问题3
- **已修复****重大调整** **直推积分奖励**落库的时候,上级的分销等级是创客的时候检查必须是直推关系才发放积分奖励, 如果分销员级别大于创客则不需要是直推关系,只要是伞下关系就可以获得级差的**积分奖励**(级差的积分奖励同样更新到“直推积分奖励”的表字段中)。
- **已修复**范氏国香mysql47.94.76.64用户推荐关系uid=1->uid=2->uid=54->uid=55->uid=56,
用户行为发生目前数据库中有订单和用户数据Uid=55和Uid=56有2笔最新的购买报单商品的订单记录后检查uid=1和Uid=2的直推积分奖励情况.
## 分销员的**直推积分奖励**问题4
- 用户推荐关系uid=1->uid=2->uid=65->uid=66->uid=67, U67购买报单商品的订单记录后, 发现一个NG项
1.Uid=66佣金OK
2.Uid=65没有直推积分奖励OK
3.Uid=2得800直推积分奖励NG
4.Uid=1得200直推积分奖励OK
uid=2应该拿全部的级差积分奖励800才对
## 新增**伞下积分奖励**开关功能
- **已修复**新增伞下积分奖励开关功能,默认是关闭的
## 我的页面pages/user/index
- **已修复**隐藏“pages/users/user_member_code/index”入口
## 我的资产页面pages/assets/index
- **已修复**积分明细跳转路径修改为pages/users/user_integral_detail/index
- **已修复**顶部navbar改成和pages/users/user_spread_user/index页面一样的样式同时美化页面其他部分的样式
## 佣金状态页面pages/queue/status
- **已修复**顶部navbar改成和pages/users/user_spread_user/index页面一样的样式同时美化页面其他部分的样式

View File

@@ -1,11 +0,0 @@
# 测试问题
## 提现页面pages/users/user_cash/index
- **已修复**选择支付宝提现点击“立即提现”提交后跳转到pages/users/user_spread_money/index?type=1
## 保单商品一次购买多份下的分润计算
- **已修复**推荐返佣按照一次购买多单的分润计算如购买5份则返佣5份而不是1份。
- **已修复**积分奖励同上
- **已修复**分销等级任务中订单数统计改为按购买保单商品份数计算如购买5份保单商品则订单数统计为5份而不是1份。
- **已修复**公排入队按件数拆分为 N 条独立记录PRD §3.1.2),每条单份金额,逐条触发退款检测
- **已修复**周期佣金位次统计改为按报单商品总件数(而非订单数),确保跨订单轮巡位次连续

View File

@@ -1,144 +0,0 @@
# 范氏国香商城 — Phase 7 后台配置与验收清单
> 本文档为运营人员在部署后,通过 CRMEB 管理后台完成配置的操作指南,同时提供全链路测试步骤。
---
## 7.1 执行数据库迁移
在服务器上执行以下 SQL 迁移脚本(每次部署后执行一次,使用 `INSERT IGNORE``ADD COLUMN IF NOT EXISTS` 保证幂等性):
```bash
mysql -u root -p fsgx-shop < pro_v3.5.1/help/migrations/fsgx_v1.sql
```
**迁移内容:**
- `eb_store_product` 新增 `is_queue_goods` 字段
- `eb_store_order` 新增 `is_queue_goods` 字段(冗余,加速佣金计数)
- `eb_user` 新增 `frozen_points``available_points``no_assess` 字段
- `eb_system_config` 插入 4 个返佣配置键
- `eb_system_timer` 插入每日积分释放定时任务
---
## 7.2 后台分销等级配置
路径:**营销 → 分销 → 分销等级**
创建以下 4 个等级按顺序level 值 1~4
| 等级 | 名称 | 直推人数条件 | 伞下有效订单数 | 佣金上浮比例 |
|------|-------|------------|--------------|------------|
| 1 | 创客 | 直推 ≥ 1 人 | - | +0% |
| 2 | 云店 | 直推 ≥ 3 人 | - | +5% |
| 3 | 服务商 | 直推 ≥ 10 人 | 伞下 ≥ 30 单 | +10% |
| 4 | 分公司 | 直推 ≥ 30 人 | 伞下 ≥ 100 单 | +15% |
> 注意:等级升级任务条件可根据业务调整,以上为推荐默认值。
---
## 7.3 后台运营配置
### 7.3.1 开启人人分销
路径:**营销 → 分销 → 分销设置**
- 分销功能:**开启**
- 分销类型:**人人分销**(所有用户均可参与)
### 7.3.2 返佣设置fsgx 周期佣金)
路径:**营销 → 分销 → 返佣设置 → 推荐佣金fsgxTab**
| 配置项 | 推荐值 | 说明 |
|--------------|----------------|----------------------------------|
| 佣金周期人数 | `3` | 推荐3人为一个完整周期 |
| 各档佣金比例 | `[20,30,50]` | 第1人20%第2人30%第3人50%JSON格式 |
| 返佣范围 | 仅报单商品 | 仅 `is_queue_goods=1` 的商品参与 |
| 佣金发放时机 | 支付即发放 | 用户付款后立即发放佣金 |
> 如果返佣设置页面没有"推荐佣金fsgx"Tab请确认已运行 `fsgx_v1.sql` 迁移脚本。
### 7.3.3 提现设置
路径:**财务 → 分销财务 → 提现设置(已存在)**
- 提现手续费率:`7%`
- 最低提现金额:`100 元`
- 支持提现方式:微信零钱、支付宝、银行卡
### 7.3.4 报单商品配置
路径:**商品 → 商品列表 → 编辑目标商品 → 其他设置**
- 将参与周期佣金的商品标记为"报单商品"`is_queue_goods = 1`
- 建议在商品名称/描述中注明"报单商品"
---
## 7.4 全链路验收测试
### 测试环境准备
1. 准备 3 个测试账号:`用户A`(推荐人)、`用户B/C/D`(被推荐人)
2. 确保用户B/C/D 通过用户A 的邀请链接注册(绑定 `spread_uid = A.uid`
3. 准备至少 1 个标记了 `is_queue_goods=1` 的报单商品
### 测试步骤
#### Step 1注册与推荐关系绑定
- [ ] 用户B 通过用户A 邀请链接打开小程序并注册
- [ ] 后台验证:`eb_user.spread_uid = A.uid`
#### Step 2购买报单商品第1人
- [ ] 用户B 购买报单商品并支付成功
- [ ] 验证用户A 获得第1周期佣金应为商品价格的 **20%**
- [ ] 验证用户A 的 `frozen_points` 增加
- [ ] 小程序"推荐佣金"页:`cycle_current = 1/3`
#### Step 3购买报单商品第2人
- [ ] 用户C 通过用户A 邀请链接注册并购买报单商品
- [ ] 验证用户A 获得第2周期佣金应为商品价格的 **30%**
- [ ] 小程序"推荐佣金"页:`cycle_current = 2/3`
#### Step 4购买报单商品第3人完成一个周期
- [ ] 用户D 通过用户A 邀请链接注册并购买报单商品
- [ ] 验证用户A 获得第3周期佣金应为商品价格的 **50%**
- [ ] 验证:一个周期累计佣金 = 商品价格的 **100%**
- [ ] 小程序"推荐佣金"页:`cycle_current = 0/3`(新周期开始)
#### Step 5分销等级自动升级验证
- [ ] 用户A 直推了3人B/C/D满足"云店"升级条件直推≥3人
- [ ] 后台验证:`eb_user.agent_level = 2`(云店)
- [ ] 小程序个人中心:等级标签显示"云店"
#### Step 6积分释放验证
- [ ] 等待次日 2:00 AM 定时任务执行(或手动触发测试)
- [ ] 验证用户A 的 `frozen_points` 减少 0.4‰,`available_points` 对应增加
- [ ] 后台积分日志页(/admin/hjf/points/log可查看释放记录
#### Step 7提现流程验证
- [ ] 用户A 在小程序申请提现(输入金额)
- [ ] 验证手续费率 7% 正确扣除
- [ ] 后台审核通过后,`brokerage_price` 对应减少
---
## 7.5 异常场景验收
- [ ] 非报单商品(`is_queue_goods=0`)下单:验证不触发周期佣金计算(`brokerage_scope=queue_only` 时)
- [ ] 报单商品订单确认页:验证"积分抵扣"入口不显示
- [ ] 不考核用户购买报单商品:佣金仍正常发放给推荐人
- [ ] 管理后台用户列表:可见 `frozen_points``available_points` 两列,并可操作"不考核"
---
*以上配置完成后,范氏国香商城 fsgx 改造即告完成,可正式上线运营。*

View File

@@ -1,63 +0,0 @@
# 管理后台
## 编辑商品页面路径admin/product/add_product?id=18&from_page=1
1. **已修复**是否报单商品1=是,修改保存后没有更新数据库中的值
## 返佣设置页面,路径:/admin/setting/system_config_rake_back
1. **已修复**”返佣范围、佣金发放时机“修改后保存不落库(数据库没有修改)
2. **已修复**推荐佣金fsgxtab页中”返佣范围、佣金发放时机“没有显示选中项
## 用户列表页面,路径: /admin/user/list
1. **已修复**操作“不考核”,提交后报错
2. **已修复**HJF等级改为分销等级但是没有关联用户的分销等级没显示数据queue分支下该功能是ok的
## 分销等级页面路径admin/setting/membership_level/index
1. **已修复**列表中显示“直推奖励积分、伞下奖励积分”和升级等级任务queue分支下该功能是ok的可以直接合并过来
## 定时任务页面,路径:/admin/system/crontab
1. **已修复**增加“手动触发”功能按钮,可以手动触发即执行任务立,
2. **已修复**手动触发“fsgx每日积分释放”任务失败
## 用户列表页面,路径:/admin/user/list
1. **已修复**“直推人数满、伞下订单数”没有显示数据,参考分销员管理页面实现逻辑(/admin/agent/agent_manage/index实现数据显示。
---
# uniapp移动端
## 修改密码页面
1. **已修复**点击获取验证,去除安全验证,直接发送验证码
## 佣金记录页面uniapp/pages/users/user_spread_money
1. **已修复**团队业绩统计数据的伞下人数不显示
---
# 测试
## 测试数据:
1. 测试账号UID1 手机号1860001111 UID2 手机号18621813282 UID3 手机号17887996868
UID4 手机号15324401259;UID5 手机号17887996868; UID6 手机号15821676725; 测试账号密码默认A123456
2. 推荐关系: uid=2推荐uid=4uid=5, uid=6
3. **已修复**uid=4,5,6购买报单商品后推荐人uid=2没有佣金/返现产生,
4. 分销会员uid=2的积分奖励“待释放冻结积分”没有
## 手动测试问题
1. 排查原因eb_store_order报单商品订单完成后在管理后台中的2个页面看不到奖励积分明细
2. 积分日志页面/admin/marketing/user_point/index看不到奖励积分明细
3. 推荐人会员(直推积分奖励、伞下积分奖励获得者)的待释放积分(`frozen_points`的值没有update
4. 同时没有记录报单商品订单产生的待释放积分明细记录eb_user_bill表中status=0type=integraltitle=直推积分奖励 | 伞下积分奖励
5. **已修复**佣金记录页面/admin/finance/finance/commission中用户返现佣金记录详情中看不到返现佣金明细
6. **相关文件**`docs/PRD_fsgx_V1.0.md` `docs/page-dev-specs-fsgx.md`

View File

@@ -1,37 +0,0 @@
# uniapp移动端
## 会员码页面pages/users/user_member_code/index
- 1. 前端页面报错:
` [渲染层网络层错误] Failed to load local image resource /pages/users/user_member_code/false
the server responded with a status of 500 (HTTP/1.1 500 Internal Server Error) `
## 分销海报页面pages/users/user_spread_code/index
- 1. 海报加载不出来
## 我的资产-》账单明细页面pages/users/user_bill/index
- 1. 去掉公排退款
## 页面: pages/queue/status
- 1. 左上角增加返回按钮
## 页面pages/assets/index
- 1. "累计获得积分"不显示数据,是否没读取到数据?
- 2. 左上角增加返回按钮
---
# 其他测试问题
- 1. 自己不报单(没有购买过报单商品的订单),推荐没有返现佣金的,
- 2. 判断升级成为**会员分销等级**前,不给奖励积分(直推奖励积分、伞下奖励积分),
- 3. 目前积分奖励是按订单数量来给了需要调整为按报单商品数量比如一次性下一个订单含3个报单商品应该给1500积分而不是500
- 4. 创客级别的分销员,伞下下单没有**伞下奖励积分**,升级为云店级别分销员之后,只要级别低于自己的,伞下下单都应有**伞下奖励积分**
- 5. 用户ID=14直推了2单就升级成为创客的**分销等级**了但是目前分销等级中创客的升级任务配置的是直推3单
- 6. **推荐返现循环**目前配置的是"[20,30,50]"。为何用户ID=14直推了2单出现了2次20%的返现佣金?
- 7. e2e测试验收一下分销员等级中配置的奖励积分直推奖励积分、伞下奖励积分和等级任务是否正确
# 相关文件
- 1. **相关文件**`docs/PRD_fsgx_V1.0.md` `docs/page-dev-specs-fsgx.md`

View File

@@ -1,37 +0,0 @@
# 核心功能测试结果
- 分销员直推没有获得返现佣金测试数据uid=20升级为创客分销等级后下级用户下报单商品订单支付后uid=20并没有eb_user_brokerage返现佣金记录。
- 分销海报页面, pages/users/user_spread_code/index 获取不到二维码
fetch("https://www.fsgx.cn/api/user/routine_code", {
"headers": {
"accept": "*/*",
"accept-language": "zh-CN,zh;q=0.9",
"authori-zation": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdXRoIjoiMTRlMWI2MDBiMWZkNTc5ZjQ3NDMzYjg4ZThkODUyOTEiLCJpc3MiOiJ3d3cuZnNneC5jbiIsImF1ZCI6Ind3dy5mc2d4LmNuIiwiaWF0IjoxNzc0NjY2NzA1LCJuYmYiOjE3NzQ2NjY3MDUsImV4cCI6MTc3NTI3MTUwNSwianRpIjp7ImlkIjoyMCwidHlwZSI6InJvdXRpbmUifX0.lbxfmmPVMNFpZmx9EXcb_7viz-YeMC_QymX6tiuCkD4",
"cache-control": "no-cache",
"content-type": "application/json",
"form-type": "routine",
"pragma": "no-cache",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "cross-site",
"x-source": "370474988fa2275c"
},
"referrer": "https://servicewechat.com/wx998d9e0a925a1a13/devtools/page-frame.html",
"referrerPolicy": "strict-origin-when-cross-origin",
"body": null,
"method": "GET",
"mode": "cors",
"credentials": "omit"
});
{"status":200,"msg":"ok","data":{"url":false}}
- `pro_v3.5.1/.cursor/plans/fix_issues_0325-1_f8488785.plan.md`对文档中“B7. e2e 测试验收分销员等级配置(测试问题 #7”执行e2e测试
---
# 测试
# 相关文件
1. **相关文件**`docs/PRD_fsgx_V1.0.md` `docs/page-dev-specs-fsgx.md``pro_v3.5.1/.cursor/plans/fix_issues_0325-1_f8488785.plan.md`

View File

@@ -1,34 +0,0 @@
# 云服务器 hjf.suzhouyuqi.com 站点 Nginx 配置示例
# 用于宝塔/LNMP复制到站点配置或 include 使用
# 关键root 指向 public静态文件 /static/、/assets/、/pages/ 由 Nginx 直接提供
server {
listen 80;
listen 443 ssl http2;
server_name hjf.suzhouyuqi.com;
root /www/wwwroot/hjf.suzhouyuqi.com/public;
index index.html index.php;
# SSL 证书(宝塔通常自动配置)
# ssl_certificate ...
# ssl_certificate_key ...
# H5 静态资源:直接由 Nginx 提供,避免返回 HTML 导致 "Unexpected token '<'"
location ~ ^/(static|assets|pages)/ {
try_files $uri =404;
expires 7d;
}
# PHP 请求(按现有 Swoole/php-fpm 配置)
location ~ \.php$ {
proxy_pass http://127.0.0.1:20199;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 其余请求:先找静态文件,再走入口
location / {
try_files $uri $uri/ /index.php?$query_string;
}
}

View File

@@ -1,846 +0,0 @@
# 范氏国香商城 · 页面开发说明文档V2 — 基于分销模块复用)
> 对应 PRD`docs/PRD_fsgx_V1.0.md`
> 技术底座CRMEB Pro v3.5 二次开发UniApp + iView Admin
> 文档日期2026-03-23
> 文档状态V2 — 基于分销模块最大化复用原则修订
---
## 核心原则
> **最大限度复用 CRMEB 分销模块**
> 1. "创客/云店/服务商/分公司"四级体系 → 使用 CRMEB **分销等级**`agent_level` 表)
> 2. "推荐佣金 20/30/50"→ 在 CRMEB **分销佣金**`brokerage`)计算链路上改造
> 3. 佣金配置 → 扩展 CRMEB **返佣设置**`rake_back`
> 4. 佣金记录 → 复用 CRMEB **佣金记录**`UserBrokerage` / `UserBill`
> 5. 推荐关系 → 复用 CRMEB **推广关系**`spread_uid`
> 6. 提现 → 复用 CRMEB **提现功能**`extract`
> 7. 仅在 CRMEB 不覆盖的功能点上做自定义扩展(积分释放、报单标记等)
---
## 目录
- [Part 1: 小程序端UniApp](#part-1-小程序端uniapp)
- [1.1 新用户引导页](#11-新用户引导页)
- [1.2 登录页](#12-登录页)
- [1.3 首页](#13-首页)
- [1.4 商品详情页](#14-商品详情页)
- [1.5 订单确认页](#15-订单确认页)
- [1.6 支付结果页](#16-支付结果页)
- [1.7 推荐佣金进度页](#17-推荐佣金进度页)
- [1.8 佣金收益页](#18-佣金收益页)
- [1.9 推荐佣金规则说明页](#19-推荐佣金规则说明页)
- [1.10 个人中心页](#110-个人中心页)
- [1.11 我的资产页](#111-我的资产页)
- [1.12 积分明细页](#112-积分明细页)
- [1.13 推广中心/团队页](#113-推广中心团队页)
- [1.14 提现页](#114-提现页)
- [Part 2: 管理后台Admin Vue](#part-2-管理后台admin-vue)
- [2.1 分销员管理](#21-分销员管理)
- [2.2 佣金记录](#22-佣金记录)
- [2.3 返佣设置](#23-返佣设置)
- [2.4 分销等级管理](#24-分销等级管理)
- [2.5 积分日志](#25-积分日志)
- [2.6 商品编辑 - 报单标记](#26-商品编辑---报单标记)
- [2.7 商品列表](#27-商品列表)
- [2.8 提现申请管理](#28-提现申请管理)
- [2.9 用户管理](#29-用户管理)
- [Part 3: 后端改造要点](#part-3-后端改造要点)
- [3.1 佣金计算链路改造](#31-佣金计算链路改造)
- [3.2 分销等级映射](#32-分销等级映射)
- [3.3 积分体系(自定义扩展)](#33-积分体系自定义扩展)
- [Part 4: 公共组件与 API 层](#part-4-公共组件与-api-层)
- [4.1 UniApp 组件](#41-uniapp-组件)
- [4.2 API 文件](#42-api-文件)
- [4.3 Mock 数据](#43-mock-数据)
---
## 模块映射总览
| fsgx 需求 | CRMEB 分销模块对应 | 改造方式 |
|---|---|---|
| 推荐关系绑定 | `spread_uid`(推广关系) | 直接复用 |
| 推荐佣金 20/30/50 | 一级佣金 `one_brokerage` | 改造计算逻辑:从固定比例改为周期循环比例 |
| 创客/云店/服务商/分公司 | `agent_level`(分销等级) | 配置 4 个分销等级,升级条件对齐 |
| 等级积分奖励 | 暂无直接对应 | 在佣金发放链路上新增积分奖励逻辑 |
| 佣金提现 | `extract`(提现模块) | 直接复用,手续费配置为 7% |
| 佣金记录 | `UserBrokerage` / `UserBill` | 直接复用 |
| 推广中心 | `user_spread_user` / `user_spread_money` | 复用 + 文案改造 |
| 返佣配置 | `rake_back`(返佣设置) | 扩展:增加周期人数、分段比例、返佣范围 |
| 分销员管理 | `agentManage`(分销员管理) | 直接复用 |
| 佣金财务流水 | `finance/commission`(佣金记录) | 直接复用 |
| 报单商品标记 | `is_queue_goods`(自定义字段) | 自定义扩展 |
| 积分释放 | 无对应 | 自定义扩展 |
---
## Part 1: 小程序端UniApp
基础路径:`pro_v3.5.1/view/uniapp/`
---
### 1.1 新用户引导页
| 属性 | 内容 |
|---|---|
| 页面路径 | `pages/guide/hjf_intro.vue` |
| 路由 | `pages/guide/hjf_intro`pages.jsoncustom nav |
| 当前状态 | **需改造(文案)** |
| PRD 章节 | 4.1 登录与注册 — 新用户引导P1 |
| 优先级 | P1 |
**改造要点**
仅更新 `MOCK_GUIDE_DATA` 中的轮播文案:
| slide | 当前文案 | 目标文案 |
|---|---|---|
| 1 | 欢迎来到黄精粉健康商城 | 欢迎来到范氏国香商城 |
| 2 | 公排返利机制 — 每进4单退1单 | 推荐佣金 — 邀请3人购买即可免单 |
| 3 | 会员积分体系 — 推荐好友即获积分 | 分级积分奖励 — 升级后推荐再得积分 |
**验收标准**
- 文案为推荐佣金叙事,品牌名为"范氏国香"
- 阅读完成后写入本地标记不再重复展示
---
### 1.2 登录页
| 属性 | 内容 |
|---|---|
| 页面路径 | `pages/users/login/index.vue` |
| 当前状态 | **CRMEB 直接复用** |
| 优先级 | P0 |
**复用说明**
CRMEB 分销模块的 `spread` 参数绑定机制完全满足需求。确认以下逻辑正常:
- 分享链接携带 `spread` / `spid` 参数
- 首次登录自动绑定 `spread_uid`,不可篡改
- 绑定推荐关系后自动成为推广人(需后台"人人分销"开启)
**依赖 API**
- CRMEB 原有授权/手机号绑定接口
**验收标准**
- 推荐关系绑定正确
- 后台"人人分销"开启后,新注册用户自动成为分销员
---
### 1.3 首页
| 属性 | 内容 |
|---|---|
| 页面路径 | `pages/index/index.vue` |
| 当前状态 | **CRMEB DIY 直接复用** |
| 优先级 | P0 |
**复用说明**
无代码改动,仅通过后台 DIY 配置报单商品4333 元)推荐位。
---
### 1.4 商品详情页
| 属性 | 内容 |
|---|---|
| 页面路径 | `pages/goods_details/index.vue` |
| 当前状态 | **需改造(文案 + 逻辑)** |
| PRD 章节 | 4.3 商品与购买P0 |
| 优先级 | P0 |
**改造要点**
| 改造项 | 说明 |
|---|---|
| 报单标记文案 | `is_queue_goods` 标记文案从"公排商品"改为"报单商品" |
| 佣金提示 | 报单商品展示"推荐好友购买,可获佣金奖励" |
| 佣金展示 | 复用 CRMEB 商品详情中的"预计佣金"展示(`product.attrInfo.brokerage` |
| 积分支付 | 报单商品隐藏积分支付入口 |
**依赖 API**
- `GET /api/product/detail/{id}` — CRMEB 原有(含 `is_queue_goods`、佣金信息)
**验收标准**
- 报单商品展示"报单商品"角标 + 佣金提示
- 普通商品展示标准详情
- 佣金金额显示正确
---
### 1.5 订单确认页
| 属性 | 内容 |
|---|---|
| 页面路径 | `pages/goods/order_confirm/index.vue` |
| 当前状态 | **需改造** |
| 优先级 | P0 |
**改造要点**
| 改造项 | 说明 |
|---|---|
| 积分支付过滤 | 报单商品(`is_queue_goods === 1`)禁用积分支付 |
| 佣金预览 | 复用 CRMEB 订单确认中的佣金计算,展示"本单推荐人可获佣金 ¥X" |
**依赖 API**
- `POST /api/order/confirm` / `POST /api/order/create` — CRMEB 原有
---
### 1.6 支付结果页
| 属性 | 内容 |
|---|---|
| 页面路径 | `pages/goods/order_pay_status/index.vue` |
| 当前状态 | **需改造(文案)** |
| 优先级 | P1 |
**改造要点**
| 改造项 | 当前 | 目标 |
|---|---|---|
| 报单商品提示 | "您的订单已进入公排队列" | "支付成功!推荐好友购买可获得佣金" |
| 跳转按钮 | "查看公排状态" | "查看推荐佣金"→ 跳转 `pages/queue/status` |
---
### 1.7 推荐佣金进度页
| 属性 | 内容 |
|---|---|
| 页面路径 | `pages/queue/status.vue` |
| 当前状态 | **核心改造** |
| PRD 章节 | 4.4 推荐裂变 — 佣金进度P0 |
| 优先级 | P0 |
**功能描述(目标)**
展示当前用户的推荐佣金周期进度:
- 顶部:周期进度环(第 N 人 / 共 3 人,已获佣金 X%
- 中部三段进度条20% → 30% → 50%),标注每档完成状态
- 底部:我的佣金记录列表
**实现方案 — 基于分销模块**
| 数据来源 | CRMEB 模块 | 说明 |
|---|---|---|
| 佣金进度 | 统计用户下级购买报单商品数量 | 对 `spread_uid` 下级的报单商品订单计数,取模周期人数得当前位次 |
| 佣金记录 | `UserBrokerage` 佣金记录 | 筛选类型 `get_brokerage`(一级佣金),即为推荐佣金记录 |
| 累计佣金 | `brokerage_price`(佣金余额) | CRMEB 用户表已有字段 |
**API 接口**
复用 CRMEB 原有接口 + 新增 1 个进度聚合接口:
```
GET /api/hjf/brokerage/progress (新增)
Response:
{
cycle_total: 3, // 周期人数(来自 rake_back 配置)
cycle_current: 1, // 当前周期已完成人数
cycle_rates: [20, 30, 50], // 各档比例(来自 rake_back 配置)
total_brokerage: "4333.00" // 累计佣金收入
}
GET /api/spread/commission/detail?page=1&limit=15 (CRMEB 原有,佣金明细)
```
**依赖组件**
- `HjfQueueProgress` → 改造为周期进度环组件
**验收标准**
- 进度环数据与用户实际推荐报单商品订单数一致
- 佣金记录列表复用佣金明细数据
- 空态引导文案
---
### 1.8 佣金收益页
| 属性 | 内容 |
|---|---|
| 页面路径 | `pages/users/user_spread_money/index.vue` |
| 当前状态 | **CRMEB 复用 + 文案确认** |
| PRD 章节 | 4.4 推荐裂变 — 推荐收益列表P1 |
| 优先级 | P1 |
**复用说明**
CRMEB 原有"佣金收益"页面已包含:佣金明细列表、收入/支出 Tab、分页加载。直接复用无需改造文案本身就叫"佣金")。
**注意**:原 `pages/queue/history.vue` 不再作为独立入口,改为跳转到此页面,或将 `history.vue` 重定向到 `user_spread_money`
**依赖 API**
- `GET /api/spread/commission/detail` — CRMEB 原有佣金明细
**验收标准**
- 列表展示佣金收入记录
- 分页正常
---
### 1.9 推荐佣金规则说明页
| 属性 | 内容 |
|---|---|
| 页面路径 | `pages/queue/rules.vue` |
| 当前状态 | **核心改造(静态文案)** |
| 优先级 | P1 |
**改造要点**
| 改造项 | 当前 | 目标 |
|---|---|---|
| 机制标题 | "进四退一流程" | "推荐佣金流程" |
| 流程图 | 4 人入队触发退款 | A 推荐 B/C/DA 获 20%/30%/50% 佣金 |
| 示例金额 | ¥3,600 | ¥4,333 |
| 等级说明 | 无 | 新增"升级创客后,每单额外获得 500 积分"说明 |
---
### 1.10 个人中心页
| 属性 | 内容 |
|---|---|
| 页面路径 | `pages/user/index.vue` |
| 当前状态 | **需改造(文案 + 入口)** |
| 优先级 | P0 |
**改造要点**
| 改造项 | 当前 | 目标 |
|---|---|---|
| 入口文案 | "公排查询" | "推荐佣金" |
| 跳转路径 | `/pages/queue/status` | 保持不变 |
| 等级展示 | class `vip` 区域 | 改为从 CRMEB `agent_level_name`(分销等级名称)获取,显示"创客/云店/服务商/分公司" |
| 推广中心入口 | CRMEB 原有"分销中心"入口 | 确认可见,文案可改为"推广中心" |
**依赖 API**
- `GET /api/user` — CRMEB 原有(含 `agent_level``brokerage_price` 等字段)
---
### 1.11 我的资产页
| 属性 | 内容 |
|---|---|
| 页面路径 | `pages/assets/index.vue` |
| 当前状态 | **需改造** |
| 优先级 | P0 |
**改造要点**
| 改造项 | 当前 | 目标 |
|---|---|---|
| 现金余额 | 展示 `now_money` | 改为展示 `brokerage_price`(佣金余额),或两者都展示 |
| 来源文案 | "公排累计退款" | "推荐累计佣金" |
| 统计字段 | `total_queue_refund` | 改为累计佣金收入(从 `UserBrokerage` 汇总) |
**API 接口**
```
GET /api/hjf/assets/overview (保留的自定义聚合接口)
Response:
{
brokerage_price: "7200.00", // 佣金余额(可提现)= CRMEB user.brokerage_price
frozen_points: 15000, // 待释放积分(自定义)
available_points: 3200, // 已释放积分(自定义)
today_release: 6, // 今日释放量(自定义)
total_brokerage: "14400.00", // 累计佣金收入
total_points_earned: 18200,
agent_level: 2,
agent_level_name: "云店" // 来自 CRMEB agent_level
}
```
---
### 1.12 积分明细页
| 属性 | 内容 |
|---|---|
| 页面路径 | `pages/assets/points_detail.vue` |
| 当前状态 | **保持(自定义模块)** |
| 优先级 | P1 |
**复用说明**
积分释放体系(待释放/已释放/每日释放)为自定义扩展,不属于 CRMEB 分销模块。保持现有实现,集成时 `USE_MOCK` 改为 `false`
---
### 1.13 推广中心/团队页
| 属性 | 内容 |
|---|---|
| 页面路径 | `pages/users/user_spread_user/index.vue` |
| 当前状态 | **CRMEB 复用 + 增强** |
| PRD 章节 | 4.4 推荐裂变P1 |
| 优先级 | P1 |
**复用说明**
CRMEB 原有"推广中心"页面已包含:直推成员列表、推广人数统计、推广海报。
**改造要点**
| 改造项 | 说明 |
|---|---|
| 顶部增加 | 嵌入佣金周期进度摘要组件(复用 `HjfQueueProgress` |
| 数据增强 | 每个成员增加"是否已购报单商品"标记(需后端接口增加字段) |
**依赖 API**
- `GET /api/spread/people` — CRMEB 推广人列表
- `GET /api/spread/count` — CRMEB 推广统计
- `GET /api/hjf/brokerage/progress` — 自定义进度聚合
---
### 1.14 提现页
| 属性 | 内容 |
|---|---|
| 页面路径 | `pages/users/user_cash/index.vue` |
| 当前状态 | **CRMEB 直接复用** |
| 优先级 | P0 |
**复用说明**
CRMEB 提现功能完全满足需求:提现金额输入、手续费计算(后台配 7%)、渠道选择、提交审核。
确认后台 `rake_back` 配置中 `extract_time`(佣金冻结天数)和手续费率配置正确即可。
---
## Part 2: 管理后台Admin Vue
基础路径:`pro_v3.5.1/view/admin/src/`
---
### 2.1 分销员管理
| 属性 | 内容 |
|---|---|
| 页面路径 | `pages/agent/agentManage.vue` |
| 路由 | `/admin/agent/agent_manage/index`CRMEB 原有) |
| 当前状态 | **CRMEB 直接复用** |
| PRD 章节 | 5.2 用户管理P0 |
| 优先级 | P0 |
**复用说明**
CRMEB 原有"分销员管理"页面已包含:分销员列表、筛选(等级/昵称/手机号)、推广人列表弹窗(含推广订单、佣金字段)、统计卡片。
**改造要点**
| 改造项 | 说明 |
|---|---|
| 等级筛选 | 确认下拉中包含"创客/云店/服务商/分公司"选项(来自 `agent_level` 配置) |
**验收标准**
- 分销员列表展示正确
- 推广人列表弹窗可查看推荐关系与佣金明细
---
### 2.2 佣金记录
| 属性 | 内容 |
|---|---|
| 页面路径 | `pages/finance/commission/index.vue` |
| 路由 | `/admin/finance/finance/commission`CRMEB 原有) |
| 当前状态 | **CRMEB 直接复用** |
| PRD 章节 | 5.5 财务管理 — 佣金流水P0 |
| 优先级 | P0 |
**复用说明**
CRMEB 原有"佣金记录"页面已包含:佣金流水列表、用户/时间/金额筛选、明细弹窗。所有通过分销佣金发放的记录自动出现在此页面。
**注意**:原 `pages/hjf/queueFinance` **不再需要**,其功能完全由 CRMEB 佣金记录覆盖。
---
### 2.3 返佣设置
| 属性 | 内容 |
|---|---|
| 页面路径 | 系统配置 `rake_back` Tab通过 `SystemConfigServices::rakeBackFormBuild` 生成) |
| 路由 | `/admin/setting/system_config_rake_back`CRMEB 原有) |
| 当前状态 | **需改造(核心)** |
| PRD 章节 | 5.6 营销配置中心P0 |
| 优先级 | P0 |
**功能描述**
CRMEB 返佣设置页面,通过 `rakeBackFormBuild` 方法动态生成表单。
**改造要点**
| 改造项 | 当前 | 目标 |
|---|---|---|
| 一级返佣比例 | 固定百分比 `store_brokerage_ratio` | 改为多段比例:支持配置周期人数 + 各档比例(如 3 人循环 20/30/50 |
| 二级返佣 | `store_brokerage_two` | 保留,或设为 0fsgx 无二级返佣需求) |
| 新增配置 | — | `brokerage_cycle_count`(周期人数,默认 3 |
| 新增配置 | — | `brokerage_cycle_rates`(各档比例 JSON`[20,30,50]` |
| 新增配置 | — | `brokerage_scope`(返佣范围:`all` 所有商品 / `queue_only` 仅报单商品) |
| 新增配置 | — | `brokerage_timing`(发放时机:`on_pay` 支付即发 / `on_confirm` 确认收货后) |
**后端改造**
- `SystemConfigServices::rakeBackFormBuild()` — 新增上述配置字段的表单项
- `SystemConfigValidate` — 增加对应验证规则
- `eb_system_config` 表 — 新增配置键值对
**验收标准**
- 返佣设置页展示新增配置项
- 保存后配置值正确持久化
- 佣金计算链路读取新配置生效
---
### 2.4 分销等级管理
| 属性 | 内容 |
|---|---|
| 页面路径 | `pages/setting/membershipLevel/index.vue` |
| 路由 | `/admin/setting/membership_level/index`CRMEB 原有) |
| 当前状态 | **CRMEB 复用 + 配置对齐** |
| PRD 章节 | 5.6 营销配置 — 等级门槛与奖励P0 |
| 优先级 | P0 |
**复用说明**
使用 CRMEB 分销等级(`agent_level` 表)管理"创客/云店/服务商/分公司"。
**配置要求**
在后台配置 4 个分销等级:
| 等级 | 名称 | 升级条件(等级任务) | 一级佣金上浮 | 二级佣金上浮 |
|---|---|---|---|---|
| Lv.1 | 创客 | 直推满 3 单 | 按需配置 | 0 |
| Lv.2 | 云店 | 伞下满 30 单(至少 3 直推) | 按需配置 | 按需配置 |
| Lv.3 | 服务商 | 伞下满 100 单(至少 3 直推) | 按需配置 | 按需配置 |
| Lv.4 | 分公司 | 伞下满 1000 单(至少 3 直推) | 按需配置 | 按需配置 |
**等级任务配置**
通过 CRMEB 的"等级任务"机制(`agent_level_task` 表)设置升级条件:
- 任务类型:推广人数(直推)、推广订单数(伞下)
- 升级条件自动判定,满足后自动升级
**改造要点**
| 改造项 | 说明 |
|---|---|
| 等级数据 | 在后台创建 4 个等级 + 对应等级任务 |
| 佣金上浮 | 等级的 `one_brokerage`/`two_brokerage` 字段配置上浮百分比,通过 `AgentLevelServices::getAgentLevelBrokerage` 自动叠加到基础比例 |
| 积分奖励 | 需在佣金发放链路中新增积分逻辑(见 Part 3 |
**注意**:原 `pages/hjf/memberLevel``pages/hjf/memberConfig` **不再需要**,其功能由 CRMEB 分销等级管理覆盖。
**依赖 API**
- `GET /adminapi/agent/level` — CRMEB 分销等级列表
- `POST /adminapi/agent/level` — 创建等级
- `PUT /adminapi/agent/level/{id}` — 编辑等级
- `GET /adminapi/agent/level_task` — 等级任务管理
---
### 2.5 积分日志
| 属性 | 内容 |
|---|---|
| 页面路径 | `pages/hjf/pointsLog/index.vue` |
| 路由 | `/admin/hjf/points/log`(自定义路由,保留) |
| 当前状态 | **保持(自定义模块)** |
| 优先级 | P1 |
**复用说明**
积分释放体系为自定义扩展CRMEB 分销模块不覆盖。保留现有 `pointsLog` 页面,集成时 `USE_MOCK``false`
---
### 2.6 商品编辑 - 报单标记
| 属性 | 内容 |
|---|---|
| 页面路径 | `pages/product/productAdd/components/otherSet.vue` |
| 当前状态 | **缺陷修复P0** |
| 优先级 | P0 |
**缺陷说明**
`is_queue_goods` 修改保存后未更新数据库(`issues-0323-1.md`)。
**改造要点**
| 改造项 | 说明 |
|---|---|
| 前端 | 确认 `is_queue_goods` 包含在商品保存 payload |
| 后端 | 确认 `StoreProductServices` 保存时处理 `is_queue_goods` |
| 佣金联动 | 当 `brokerage_scope = queue_only` 时,仅报单商品参与佣金计算 |
---
### 2.7 商品列表
| 属性 | 内容 |
|---|---|
| 页面路径 | `pages/product/productList/index.vue` |
| 当前状态 | **已修复,确认稳定** |
| 优先级 | P0 |
**复用说明**
报单商品 Tab 筛选已修复。确认 `is_queue_goods` 筛选生效即可。
---
### 2.8 提现申请管理
| 属性 | 内容 |
|---|---|
| 页面路径 | `pages/finance/userExtract/index.vue` |
| 路由 | `/admin/finance/user_extract/index`CRMEB 原有) |
| 当前状态 | **CRMEB 直接复用** |
| PRD 章节 | 5.5 财务管理 — 提现审核P0 |
| 优先级 | P0 |
**复用说明**
CRMEB 提现管理完全满足需求。确认手续费率 7% 在 `rake_back` 配置中正确设置。
---
### 2.9 用户管理
| 属性 | 内容 |
|---|---|
| 页面路径 | `pages/user/list/index.vue` |
| 当前状态 | **需增强** |
| 优先级 | P0 |
**改造要点**
| 改造项 | 说明 |
|---|---|
| 新增列 | 分销等级名称(`agent_level_name`CRMEB 已支持) |
| 新增列 | 待释放积分(`frozen_points`)、已释放积分(`available_points`)— 自定义字段 |
| 操作增强 | "调整分销等级"按钮 → 调用 CRMEB `agentManage/giveAgentLevel` 接口 |
| 操作增强 | "设置不考核"开关 → 自定义接口 `hjf/member/{uid}/no_assess` |
**依赖 API**
- `GET /adminapi/user/user` — CRMEB 用户列表
- `POST /adminapi/agent/manage/give_agent_level` — CRMEB 赋予分销等级
- `PUT /adminapi/hjf/member/{uid}/no_assess` — 自定义不考核设置
---
## Part 3: 后端改造要点
---
### 3.1 佣金计算链路改造(核心)
**改造位置**`app/services/order/StoreOrderCreateServices.php`(计算佣金金额)+ `app/services/order/StoreOrderTakeServices.php`(发放佣金)
**当前逻辑**
```
一级佣金 = 商品价格 × (store_brokerage_ratio + 等级上浮)
```
**目标逻辑**
```
1. 读取 brokerage_scope 配置:
- 若 queue_only仅 is_queue_goods=1 的商品参与
2. 查询推荐人当前周期位置:
- 统计推荐人下级购买报单商品的有效订单数
- position = count % cycle_count (0-indexed)
3. 当前单的佣金比例 = cycle_rates[position]
4. 一级佣金 = 商品实付金额 × 当前比例%
5. 若推荐人有分销等级(创客及以上),额外发放积分奖励
```
**关键文件**
| 文件 | 改造说明 |
|---|---|
| `app/services/order/StoreOrderCreateServices.php` | 修改佣金金额计算:从固定比例改为查询周期位次后取对应比例 |
| `app/services/order/StoreOrderTakeServices.php` | `backOrderBrokerage` 方法:发放佣金时,若有分销等级则额外调用积分奖励逻辑 |
| `app/services/agent/AgentLevelServices.php` | `getAgentLevelBrokerage`:确认等级佣金上浮逻辑与周期比例的叠加方式(加法 or 替代) |
| `app/services/system/config/SystemConfigServices.php` | `rakeBackFormBuild`:新增周期配置表单 |
**数据表变更**
| 表 | 变更 |
|---|---|
| `eb_system_config` | 新增键:`brokerage_cycle_count``brokerage_cycle_rates``brokerage_scope``brokerage_timing` |
| `eb_user`(可选) | 新增字段:`frozen_points``available_points``no_assess`(若积分模块需要) |
---
### 3.2 分销等级映射
使用 CRMEB `agent_level` 表,无需新建等级表。
| fsgx 等级 | agent_level 配置 | 等级任务 |
|---|---|---|
| 创客 | Lv.1, name="创客" | 任务:直推报单商品订单 ≥ 3 |
| 云店 | Lv.2, name="云店" | 任务:伞下报单商品订单 ≥ 30且直推 ≥ 3 |
| 服务商 | Lv.3, name="服务商" | 任务:伞下报单商品订单 ≥ 100且直推 ≥ 3 |
| 分公司 | Lv.4, name="分公司" | 任务:伞下报单商品订单 ≥ 1000且直推 ≥ 3 |
**注意**CRMEB 等级任务可能不直接支持"仅计报单商品订单"的条件,需确认 `AgentLevelTaskServices` 是否支持自定义筛选,或需改造任务判定逻辑。
---
### 3.3 积分体系(自定义扩展)
积分释放(待释放/已释放/每日释放)不属于 CRMEB 分销模块,保持为自定义扩展:
- 在佣金发放时(`StoreOrderTakeServices::backOrderBrokerage`),若推荐人有分销等级,同步发放积分到 `frozen_points`
- 定时任务:每日将 `frozen_points` 的 0.4‰ 转入 `available_points`
- 积分消费:购买普通商品时扣减 `available_points`
---
## Part 4: 公共组件与 API 层
---
### 4.1 UniApp 组件
基础路径:`pro_v3.5.1/view/uniapp/components/`
| 组件 | 文件 | 改造说明 | 优先级 |
|---|---|---|---|
| HjfQueueProgress | `HjfQueueProgress.vue` | 改为佣金周期进度环:接收 `cycleCurrent/cycleTotal/cycleRates` | P0 |
| HjfRefundNotice | `HjfRefundNotice.vue` | 改为佣金到账通知弹窗 | P0 |
| HjfAssetCard | `HjfAssetCard.vue` | 文案"公排退款"→"推荐佣金";余额来源改为 `brokerage_price` | P1 |
| HjfMemberBadge | `HjfMemberBadge.vue` | 保持,等级名称来源改为 `agent_level_name` | P1 |
| HjfDemoPanel | `HjfDemoPanel.vue` | 保持不变(调试用) | — |
---
### 4.2 API 文件
#### 可移除/简化的 API 文件
由于分销模块复用,以下自定义 API 可简化:
| 文件 | 当前 | 目标 |
|---|---|---|
| UniApp `hjfQueue.js` | 公排状态/历史Mock | 仅保留 `getBrokerageProgress()` 一个聚合接口;佣金记录由 CRMEB 佣金明细接口代替 |
| UniApp `hjfMember.js` | 会员信息/团队Mock | 由 CRMEB `spread` 系列接口代替,可移除 |
| Admin `hjfQueue.js` | 公排订单/配置/财务Mock | 移除:佣金订单由 CRMEB 佣金记录代替;配置由 `rake_back` 代替 |
| Admin `hjfMember.js` | 会员列表/配置/等级Mock | 移除:由 CRMEB `agent_level` 接口代替 |
#### 保留的自定义 API 文件
| 文件 | 用途 |
|---|---|
| UniApp `hjfAssets.js` | 资产聚合接口(余额+积分+佣金统计),`USE_MOCK``false` |
| Admin `hjfPoints.js` | 积分释放日志,`USE_MOCK``false` |
#### 新增接口汇总
| 接口 | 方法 | 说明 |
|---|---|---|
| `/api/hjf/brokerage/progress` | GET | 佣金周期进度聚合(周期位次、比例、累计佣金) |
| `/api/hjf/assets/overview` | GET | 资产总览聚合(佣金余额+积分+等级) |
---
### 4.3 Mock 数据
| 文件 | 改造说明 |
|---|---|
| UniApp `hjfMockData.js` | 公排模块数据替换为佣金数据(字段对齐佣金结构);`total_queue_refund``total_brokerage`;引导文案更新 |
| Admin `hjfMockData.js` | 移除公排 Mock`MOCK_QUEUE_*`);会员 Mock 保持(`MOCK_MEMBER_*`)直到 CRMEB 接口完全对接 |
---
## 附录pages.json 路由改造
| 路由 | 当前标题 | 目标标题 |
|---|---|---|
| `pages/queue/status` | `公排状态` | `推荐佣金` |
| `pages/queue/history` | `公排历史` | `佣金记录`(或重定向到 `user_spread_money` |
| `pages/queue/rules` | `公排规则` | `佣金规则` |
---
## 附录Admin 路由改造
`hjfQueue.js` 路由精简:
| 路由 | 当前 | 目标 |
|---|---|---|
| `queue/order` | 公排订单管理 | **移除**,使用 CRMEB `/admin/finance/finance/commission` |
| `queue/finance` | 公排财务流水 | **移除**,使用 CRMEB `/admin/finance/finance/commission` |
| `queue/config` | 公排配置 | **移除**,使用 CRMEB `/admin/setting/system_config_rake_back` |
| `member/level` | 会员等级管理 | **移除**,使用 CRMEB `/admin/agent/agent_manage/index` |
| `member/config` | 会员配置 | **移除**,使用 CRMEB `/admin/setting/membership_level/index` |
| `points/log` | 积分日志 | **保留** |
精简后 `hjfQueue.js` 路由仅保留 `points/log`,建议重命名文件为 `hjfCustom.js`
---
## 附录:开发优先级总览
| 优先级 | 页面/任务 | 数量 |
|---|---|---|
| **P0** | 佣金计算链路改造后端核心、返佣设置扩展、商品编辑修复、分销等级配置、推荐佣金进度页、订单确认页、个人中心、我的资产、登录、提现、商品列表、用户管理增强、HjfQueueProgress 组件 | 13 |
| **P1** | 佣金规则说明页、佣金收益页确认、积分明细、推广中心增强、积分日志 Mock 切换、HjfAssetCard/HjfMemberBadge 文案、引导页文案、支付结果文案 | 8 |
| **P2** | 首页 DIY 配置 | 1 |
---
## 附录:被移除/合并的自定义页面清单
以下原 `pages/hjf/` 页面因功能被 CRMEB 分销模块覆盖而**不再需要独立维护**
| 原页面 | 替代方案 |
|---|---|
| `pages/hjf/queueOrder` | CRMEB `finance/commission`(佣金记录) |
| `pages/hjf/queueFinance` | CRMEB `finance/commission`(佣金记录) |
| `pages/hjf/queueConfig` | CRMEB `setting/system_config_rake_back`(返佣设置,扩展) |
| `pages/hjf/memberLevel` | CRMEB `agent/agentManage`(分销员管理) |
| `pages/hjf/memberConfig` | CRMEB `setting/membershipLevel`(分销等级管理) |
| UniApp `pages/queue/history` | CRMEB `users/user_spread_money`(佣金收益),或保留并重定向 |

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,289 +0,0 @@
---
name: fsgx development tasks
overview: 基于 page-dev-specs-fsgx.mdV2 分销模块复用方案),将全部开发工作拆解为 7 个阶段、24 个具体任务,按依赖关系排序,后端核心先行、前端改造跟进、最后清理与验收。
todos:
- id: p1-db-schema
content: "Phase1: 数据表变更 — eb_system_config 新增 4 个配置键 + eb_user 新增 frozen_points/available_points/no_assess"
status: completed
- id: p1-rake-back
content: "Phase1: 返佣设置扩展 — SystemConfigServices::rakeBackFormBuild() 新增周期人数、分段比例、返佣范围、发放时机表单项"
status: completed
- id: p1-brokerage-calc
content: "Phase1: 佣金计算链路改造(核心)— StoreOrderCreateServices 周期循环比例 + StoreOrderTakeServices 积分发放"
status: completed
- id: p1-product-fix
content: "Phase1: 商品报单标记修复 — otherSet.vue payload + StoreProductServices 保存 is_queue_goods"
status: completed
- id: p2-progress-api
content: "Phase2: 新增 GET /api/hjf/brokerage/progress 佣金周期进度聚合接口"
status: completed
- id: p2-assets-api
content: "Phase2: 新增 GET /api/hjf/assets/overview 资产总览聚合接口"
status: completed
- id: p2-points-cron
content: "Phase2: 积分释放定时任务 — 每日 frozen_points 0.4‰ 转 available_points + 日志"
status: completed
- id: p2-no-assess-api
content: "Phase2: 新增 PUT /adminapi/hjf/member/{uid}/no_assess 不考核接口"
status: completed
- id: p3-admin-route
content: "Phase3: Admin 路由精简 — hjfQueue.js 移除 5 条旧路由,仅保留 points/log"
status: completed
- id: p3-admin-api-clean
content: "Phase3: Admin API 清理 — 移除 hjfQueue.js 和 hjfMember.js"
status: completed
- id: p3-uniapp-api-clean
content: "Phase3: UniApp API 简化 — hjfQueue.js 仅保留 getBrokerageProgress(),移除 hjfMember.js"
status: completed
- id: p3-pages-json
content: "Phase3: pages.json 路由标题改造 — 公排状态/历史/规则 → 推荐佣金/佣金记录/佣金规则"
status: completed
- id: p4-queue-status
content: "Phase4: 推荐佣金进度页核心改造 — pages/queue/status.vue 整体重写"
status: completed
- id: p4-goods-detail
content: "Phase4: 商品详情页改造 — 报单标记文案 + 佣金提示 + 隐藏积分支付"
status: completed
- id: p4-order-confirm
content: "Phase4: 订单确认页改造 — 报单商品禁用积分支付 + 佣金预览"
status: completed
- id: p4-user-center
content: "Phase4: 个人中心页改造 — 入口文案 + 等级展示改为 agent_level_name"
status: completed
- id: p4-assets-page
content: "Phase4: 我的资产页改造 — 余额来源 + 文案 + 对接聚合接口"
status: completed
- id: p4-components
content: "Phase4: 组件改造 — HjfQueueProgress/HjfRefundNotice/HjfAssetCard"
status: completed
- id: p5-guide-text
content: "Phase5: 引导页 + 规则页 + 支付结果页文案改造"
status: completed
- id: p5-spread-enhance
content: "Phase5: 推广中心增强 — 嵌入佣金进度摘要 + 报单商品标记"
status: completed
- id: p5-mock-update
content: "Phase5: Mock 数据更新 — 公排字段→佣金字段"
status: completed
- id: p6-admin-user
content: "Phase6: Admin 用户管理增强 — 新增列 + 操作按钮"
status: completed
- id: p6-points-mock
content: "Phase6: 积分日志 USE_MOCK 改 false"
status: completed
- id: p7-config-test
content: "Phase7: 后台配置(分销等级 + 人人分销 + 返佣设置 + 提现费率)+ 全链路验收测试"
status: completed
isProject: false
---
# 范氏国香商城 — 开发任务计划
基于 [docs/page-dev-specs-fsgx.md](docs/page-dev-specs-fsgx.md) 制定,按依赖关系分 7 个阶段执行。
---
## 依赖关系总览
```mermaid
flowchart TD
Phase1[Phase1_后端核心] --> Phase2[Phase2_后端接口]
Phase1 --> Phase3[Phase3_前端清理]
Phase2 --> Phase4[Phase4_前端P0改造]
Phase3 --> Phase4
Phase4 --> Phase5[Phase5_前端P1改造]
Phase4 --> Phase6[Phase6_Admin增强]
Phase5 --> Phase7[Phase7_配置验收]
Phase6 --> Phase7
```
---
## Phase 1: 后端核心改造P0所有前端依赖此阶段
### 1.1 数据表变更
- `eb_system_config` 新增 4 个配置键:`brokerage_cycle_count``brokerage_cycle_rates``brokerage_scope``brokerage_timing`
- `eb_user` 新增字段:`frozen_points``available_points``no_assess`(积分模块用)
### 1.2 返佣设置扩展
- 文件:[app/services/system/config/SystemConfigServices.php](pro_v3.5.1/app/services/system/config/SystemConfigServices.php)
-`rakeBackFormBuild()` 方法中新增表单项:周期人数、各档比例 JSON、返佣范围所有/仅报单)、发放时机(支付即发/确认收货后)
- 增加验证规则
### 1.3 佣金计算链路改造(核心中的核心)
- 文件:[app/services/order/StoreOrderCreateServices.php](pro_v3.5.1/app/services/order/StoreOrderCreateServices.php)
- 修改一级佣金计算:固定比例 → 查询推荐人下级报单商品有效订单数 → 取模周期人数得位次 → `cycle_rates[position]` 取当前比例
- 判断 `brokerage_scope`:若 `queue_only` 则仅 `is_queue_goods=1` 的商品参与
- 文件:[app/services/order/StoreOrderTakeServices.php](pro_v3.5.1/app/services/order/StoreOrderTakeServices.php)
- `backOrderBrokerage` 方法:佣金发放后,若推荐人有分销等级,同步发放积分到 `frozen_points`
### 1.4 商品报单标记修复
- 前端:[view/admin/src/pages/product/productAdd/components/otherSet.vue](pro_v3.5.1/view/admin/src/pages/product/productAdd/components/otherSet.vue) — 确认 `is_queue_goods` 在保存 payload 中
- 后端:`StoreProductServices` — 确认保存逻辑处理 `is_queue_goods` 字段写入
---
## Phase 2: 后端新增接口 + 积分体系
### 2.1 佣金周期进度聚合接口
- 新增 `GET /api/hjf/brokerage/progress`
- 逻辑:统计 `spread_uid` 下级购买报单商品的有效订单数,取模 `brokerage_cycle_count`,返回 `cycle_total`/`cycle_current`/`cycle_rates`/`total_brokerage`
### 2.2 资产总览聚合接口
- 新增 `GET /api/hjf/assets/overview`
- 返回 `brokerage_price`CRMEB 字段)+ `frozen_points`/`available_points`/`today_release`(自定义)+ `agent_level_name`CRMEB 字段)
### 2.3 积分释放定时任务
- 定时任务:每日将 `frozen_points` 的 0.4‰ 转入 `available_points`
- 积分消费扣减逻辑
- 积分日志写入
### 2.4 不考核接口
- 新增 `PUT /adminapi/hjf/member/{uid}/no_assess`
---
## Phase 3: 前端清理 — 移除旧模块
### 3.1 Admin 路由精简
- [view/admin/src/router/modules/hjfQueue.js](pro_v3.5.1/view/admin/src/router/modules/hjfQueue.js) — 移除 `queue/order``queue/finance``queue/config``member/level``member/config` 路由,仅保留 `points/log`
- 建议重命名文件为 `hjfCustom.js`
### 3.2 Admin API 文件清理
- 移除 `admin/src/api/hjfQueue.js`(佣金订单/配置/财务由 CRMEB 佣金记录覆盖)
- 移除 `admin/src/api/hjfMember.js`(由 CRMEB `agent_level` 接口覆盖)
### 3.3 UniApp API 文件简化
- `uniapp/api/hjfQueue.js` — 仅保留 `getBrokerageProgress()` 一个聚合接口,移除公排相关函数
- `uniapp/api/hjfMember.js` — 可移除,由 CRMEB `spread` 系列接口替代
### 3.4 UniApp pages.json 路由标题改造
- `pages/queue/status`:公排状态 → 推荐佣金
- `pages/queue/history`:公排历史 → 佣金记录(或重定向到 `user_spread_money`
- `pages/queue/rules`:公排规则 → 佣金规则
---
## Phase 4: 前端 P0 改造
### 4.1 推荐佣金进度页(核心改造)
- [view/uniapp/pages/queue/status.vue](pro_v3.5.1/view/uniapp/pages/queue/status.vue) — 整体改造
- 顶部:周期进度环(调用 `/api/hjf/brokerage/progress`
- 底部:佣金记录列表(调用 CRMEB `/api/spread/commission/detail`
- 组件 `HjfQueueProgress.vue` 改为佣金周期进度环
### 4.2 商品详情页改造
- [view/uniapp/pages/goods_details/index.vue](pro_v3.5.1/view/uniapp/pages/goods_details/index.vue)
- "公排商品"→"报单商品"标记文案
- 报单商品增加佣金提示
- 报单商品隐藏积分支付入口
### 4.3 订单确认页改造
- [view/uniapp/pages/goods/order_confirm/index.vue](pro_v3.5.1/view/uniapp/pages/goods/order_confirm/index.vue)
- 报单商品禁用积分支付
- 展示"本单推荐人可获佣金 ¥X"
### 4.4 个人中心页改造
- [view/uniapp/pages/user/index.vue](pro_v3.5.1/view/uniapp/pages/user/index.vue)
- "公排查询"→"推荐佣金"入口文案
- 等级展示改为从 CRMEB `agent_level_name` 获取
### 4.5 我的资产页改造
- [view/uniapp/pages/assets/index.vue](pro_v3.5.1/view/uniapp/pages/assets/index.vue)
- 余额来源改为 `brokerage_price`
- "公排累计退款"→"推荐累计佣金"
- 调用 `/api/hjf/assets/overview` 聚合接口
### 4.6 组件改造
- `HjfQueueProgress.vue` → 佣金周期进度环props: `cycleCurrent/cycleTotal/cycleRates`
- `HjfRefundNotice.vue` → 佣金到账通知弹窗
- `HjfAssetCard.vue` → 文案改为"推荐佣金",余额来源改为 `brokerage_price`
---
## Phase 5: 前端 P1 改造
### 5.1 新用户引导页文案
- [view/uniapp/pages/guide/hjf_intro.vue](pro_v3.5.1/view/uniapp/pages/guide/hjf_intro.vue) + `hjfMockData.js`
- 品牌名改为"范氏国香",公排叙事改为佣金叙事
### 5.2 佣金规则说明页改造
- [view/uniapp/pages/queue/rules.vue](pro_v3.5.1/view/uniapp/pages/queue/rules.vue)
- "进四退一"→"推荐佣金流程",示例金额 ¥4,333
- 新增等级积分说明
### 5.3 支付结果页文案
- [view/uniapp/pages/goods/order_pay_status/index.vue](pro_v3.5.1/view/uniapp/pages/goods/order_pay_status/index.vue)
- "进入公排队列"→"推荐好友可获佣金"
### 5.4 推广中心增强
- [view/uniapp/pages/users/user_spread_user/index.vue](pro_v3.5.1/view/uniapp/pages/users/user_spread_user/index.vue)
- 顶部嵌入佣金周期进度摘要
- 成员列表增加"是否已购报单商品"标记
### 5.5 Mock 数据更新
- `hjfMockData.js`UniApp + Admin— 公排字段替换为佣金字段
---
## Phase 6: Admin 后台增强
### 6.1 用户管理增强
- [view/admin/src/pages/user/list/index.vue](pro_v3.5.1/view/admin/src/pages/user/list/index.vue)
- 新增列:分销等级名称、待释放积分、已释放积分
- 操作列增加"调整分销等级"和"设置不考核"按钮
### 6.2 积分日志 Mock 切换
- [view/admin/src/pages/hjf/pointsLog/index.vue](pro_v3.5.1/view/admin/src/pages/hjf/pointsLog/index.vue)
- `USE_MOCK` 改为 `false`,对接真实接口
---
## Phase 7: 配置与验收
### 7.1 后台分销等级配置
- 通过 CRMEB 后台创建 4 个分销等级(创客/云店/服务商/分公司)
- 配置等级任务(直推人数、伞下订单数)
- 配置各等级佣金上浮比例
### 7.2 后台运营配置
- 开启"人人分销"
- 返佣设置:配置周期人数=3比例=[20,30,50],范围=仅报单商品
- 提现手续费率 7%
- 首页 DIY 配置报单商品推荐位
### 7.3 全链路验收测试
- 注册→绑定推荐关系→购买报单商品→佣金计算→佣金发放→积分奖励→提现
- 分销等级自动升级验证
- 佣金周期进度正确性验证

View File

@@ -1,95 +0,0 @@
---
name: fsgx page dev specs
overview: 基于 PRD_fsgx_V1.0.md创建一份以页面为单位的完整开发说明文档 docs/page-dev-specs-fsgx.md涵盖小程序端和管理后台端所有需要新建或改造的页面每页标注当前状态、目标功能、API 依赖、改造要点和验收标准。
todos:
- id: write-page-dev-specs
content: 在 docs/page-dev-specs-fsgx.md 编写完整的页面开发说明文档,涵盖小程序 14 页 + 管理后台 10 页 + 公共组件与 API 层
status: completed
isProject: false
---
# 范氏国香页面开发说明文档
## 输出文件
创建 [docs/page-dev-specs-fsgx.md](docs/page-dev-specs-fsgx.md),以页面为单位组织,分为小程序端和管理后台端两大部分。
## 文档结构
### Part 1: 小程序端UniApp-- 共 14 个页面
按功能域分组:
**引导与登录2 页)**
- `pages/guide/hjf_intro` -- 新用户引导轮播(当前:公排文案,改造为推荐返现叙事)
- `pages/users/login/index` -- 登录页CRMEB 复用,需确认推荐参数绑定逻辑)
**首页与商品4 页)**
- `pages/index` -- 首页CRMEB DIY 复用,需配置报单商品推荐区)
- `pages/goods_details/index` -- 商品详情(改造:展示"报单商品"标记与返现提示)
- `pages/goods/order_confirm/index` -- 订单确认(改造:积分支付逻辑、报单商品不可积分支付校验)
- `pages/goods/order_pay_status/index` -- 支付结果(改造:支付成功后展示返现信息)
**推荐返现3 页,核心改造区,原 queue/ 目录)**
- `pages/queue/status` → 改造为"推荐返现进度"(当前:公排状态)
- `pages/queue/history` → 改造为"返现历史记录"(当前:公排历史)
- `pages/queue/rules` → 改造为"推荐返现规则说明"(当前:进四退一规则)
**个人中心与资产5 页)**
- `pages/user/index` -- 个人中心 Tab 页(改造:入口文案从"公排查询"改为"推荐返现"
- `pages/assets/index` -- 我的资产(改造:余额来源从公排退款改为推荐返现)
- `pages/assets/points_detail` -- 积分明细(保持,确认字段对齐)
- `pages/users/user_spread_user/index` -- 我的推荐/团队(改造:增加返现周期进度展示)
- `pages/users/user_cash/index` -- 提现页CRMEB 复用,确认手续费 7%
### Part 2: 管理后台Admin Vue-- 共 10 个页面
**自定义业务页hjf/ 目录6 页)**
- `pages/hjf/queueOrder` → 改造为"推荐返现订单管理"
- `pages/hjf/queueFinance` → 改造为"返现财务流水"
- `pages/hjf/queueConfig` → 改造为"返现配置"(周期人数 + 分段比例)
- `pages/hjf/memberLevel` -- 会员等级管理(保持,确认接口对接)
- `pages/hjf/memberConfig` -- 会员配置(保持,确认参数模型)
- `pages/hjf/pointsLog` -- 积分日志(保持,确认日释放记录)
**CRMEB 改造页4 页)**
- `pages/product/productAdd/components/otherSet` -- 商品编辑(修复 is_queue_goods 落库缺陷)
- `pages/product/productList` -- 商品列表(保持,确认报单筛选)
- `pages/setting/membershipLevel` -- 返佣设置(改造:返佣范围配置)
- `pages/user/list` -- 用户管理(改造:增加等级调整、不考核开关)
### Part 3: 公共组件与 API 层
**UniApp 组件5 个,需改造或新增)**
- `HjfQueueProgress` → 改为返现周期进度组件
- `HjfRefundNotice` → 改为返现到账通知
- `HjfAssetCard` -- 资产卡片(改造:余额来源文案)
- `HjfMemberBadge` -- 等级徽章(保持)
- `HjfDemoPanel` -- 演示面板(保持)
**API 文件改造6 个文件)**
- UniApp: `hjfQueue.js` → 改为 rebate 接口, `hjfAssets.js`, `hjfMember.js`
- Admin: `hjfQueue.js` → 改为 rebate 接口, `hjfMember.js`, `hjfPoints.js`
### 每个页面的说明模板
每页包含以下字段:
- 页面路径与路由
- 当前状态(已有/需新建/需改造)
- PRD 对应章节
- 功能描述与交互流程
- 依赖的 API 接口(含请求/响应格式)
- 依赖的组件
- 与当前版本的差异说明(如有)
- 验收标准
- 优先级P0/P1/P2

View File

@@ -1,94 +0,0 @@
---
name: fsgx PRD document
overview: 根据《范氏国香小程序fsgx-V1.0》需求文档,对照当前代码库(基于黄精粉 HJF PRD V2.0),编写一份新的 PRD 文档 `docs/PRD_fsgx_V1.0.md`,标注所有与当前版本不一致或当前版本不满足的功能点。
todos:
- id: write-prd
content: 在 docs/PRD_fsgx_V1.0.md 编写完整的 PRD 文档,包含所有章节
status: completed
isProject: false
---
# 范氏国香小程序 PRD 文档编写计划
## 文档输出
在 [docs/PRD_fsgx_V1.0.md](docs/PRD_fsgx_V1.0.md) 创建新的 PRD 文档。
## 关键差异分析结果
通过对比 fsgx V1.0 需求文档与当前代码库HJF PRD V2.0 + 实际实现),识别出以下核心差异:
### 1. 品牌与商品变更
- 产品名称:黄精粉健康商城 → **范氏国香商城**
- 核心商品:黄精粉套餐 3600 元/单 → **艾制品三条套餐 4333 元/单**
- 产品定位:黄精粉(健康食品) → **艾制品(健康艾灸产品)**
### 2. 核心业务逻辑 -- 奖励机制(重大变更)
**当前版本HJF**"公排进四退一"全局排队池机制,每进 4 单退 1 单全额返还。
**fsgx V1.0 要求**"邀请三人自己免单"直推返现机制:
- 推荐第 1 人购买 → 推荐人获 **20%** 现金返还
- 推荐第 2 人购买 → 推荐人获 **30%** 现金返还
- 推荐第 3 人购买 → 推荐人获 **50%** 现金返还(合计 100% 免单)
- 第 4 人起循环上述比例 + 额外积分奖励
- 推荐人数和返现比例后台**可自由设置**
**影响范围**
- 后端:需重写奖励计算引擎,从全局队列退款改为直推层级返现
- 前端 uniapp`pages/queue/` 下的公排页面需改造为推荐返现展示
- 前端 admin`pages/hjf/queueOrder/``queueConfig/``queueFinance/` 需改造
- Mock 数据:`hjfMockData.js` 中的公排相关数据需替换为返现数据
### 3. 现金余额来源变更
- **当前版本**:公排退款 + 后台手动充值
- **fsgx V1.0****推荐返现** + 后台手动充值(无公排退款概念)
### 4. 普通会员即可获得推荐奖励(变更)
- **当前版本**:普通会员无奖励,升级创客后才有直推积分
- **fsgx V1.0**:普通会员升级为分销会员后直推即可获得 20%+30%+50% **现金**奖励;创客及以上额外获得**积分**奖励
### 5. 批量购买升级规则(新增)
- fsgx V1.0 新增:"一次购买多单时,如果符合升级条件,先升级,然后 N-1 单可额外获得积分奖励"
- 当前版本无此逻辑
### 6. 订单取消方式(变更)
- **当前版本**:已付款订单不可取消,退款仅通过公排
- **fsgx V1.0****可通过后台取消订单,实现全额返还**
### 7. 返佣配置参数(变更)
- **当前版本**公排触发倍数N=4
- **fsgx V1.0**:推荐奖励比例 = 3 人循环20%+30%+50%),人数和比例均可配置
### 8. 一致保留的功能
以下功能 fsgx V1.0 与当前版本**一致**,无需变更:
- 四级分销等级体系(创客/云店/服务商/分公司)及升级门槛
- 积分体系(待释放积分 → 按日 0.4% 释放 → 已释放积分)
- 积分支付(仅普通商品)
- 提现手续费 7%
- 报单商品标记 `is_queue_goods`
- 登录注册、首页、商品管理、订单管理、活动管理、内容管理等基础模块
## 文档结构
PRD 文档将包含以下章节:
1. **文档说明** -- 术语定义、版本信息
2. **产品概述** -- 产品背景、定位、目标用户
3. **核心业务逻辑** -- 推荐返现机制、会员等级、账户积分体系
4. **用户端功能需求** -- 登录注册、首页、商品购买、推荐裂变、个人中心
5. **管理后台功能需求** -- 仪表盘、用户管理、商品管理、订单管理、财务管理、活动管理、营销配置、内容管理、数据统计
6. **与当前版本差异总结** -- 专门章节,表格列出所有差异项及影响评估
7. **数据库改造方案**
8. **非功能性需求**

View File

@@ -322,7 +322,7 @@ php think swoole
5. 后台登录:
http://域名/admin
默认账号admin 密码A@123456 或 A123456
默认账号admin 密码A@123456
## 启动命令

View File

@@ -1,133 +0,0 @@
<?php
declare(strict_types=1);
namespace app\command;
use app\services\agent\AgentLevelServices;
use app\services\hjf\PointsRewardServices;
use app\services\user\UserServices;
use think\console\Command;
use think\console\Input;
use think\console\input\Option;
use think\console\Output;
use think\facade\Db;
use think\facade\Log;
/**
* 补偿历史报单订单缺失的积分奖励
*
* 用法:
* php think hjf:patch-rewards # 扫描全部报单订单
* php think hjf:patch-rewards --order-id=11 # 仅补偿指定订单
* php think hjf:patch-rewards --dry-run # 仅扫描不执行
*/
class HjfPatchMissingRewards extends Command
{
protected function configure(): void
{
$this->setName('hjf:patch-rewards')
->setDescription('补偿历史报单订单缺失的冻结积分奖励')
->addOption('order-id', null, Option::VALUE_OPTIONAL, '指定订单ID不传则扫描全部')
->addOption('dry-run', null, Option::VALUE_NONE, '仅扫描打印,不实际执行');
}
protected function execute(Input $input, Output $output): int
{
$orderId = $input->getOption('order-id');
$dryRun = $input->getOption('dry-run');
$output->writeln('[HjfPatchRewards] 开始扫描缺失积分奖励的报单订单...');
$query = Db::name('store_order')
->where('is_queue_goods', 1)
->where('paid', 1)
->where('is_del', 0)
->where('is_system_del', 0);
if ($orderId) {
$query->where('id', (int)$orderId);
}
$orders = $query->field('id,order_id,uid,spread_uid,one_brokerage')->select()->toArray();
if (empty($orders)) {
$output->writeln('[HjfPatchRewards] 没有找到符合条件的报单订单');
return 0;
}
$output->writeln(sprintf('[HjfPatchRewards] 找到 %d 笔报单订单', count($orders)));
/** @var PointsRewardServices $pointsService */
$pointsService = app()->make(PointsRewardServices::class);
/** @var AgentLevelServices $agentLevelServices */
$agentLevelServices = app()->make(AgentLevelServices::class);
/** @var UserServices $userServices */
$userServices = app()->make(UserServices::class);
$patched = 0;
$skipped = 0;
foreach ($orders as $order) {
// 幂等检查points_release_log 中是否已有 reward_direct/reward_umbrella 记录
$hasRewardLog = Db::name('points_release_log')
->where('order_id', $order['order_id'])
->whereIn('type', ['reward_direct', 'reward_umbrella'])
->count();
if ($hasRewardLog > 0) {
$skipped++;
$output->writeln(sprintf(' [SKIP] 订单 #%d (%s) 已有积分奖励记录', $order['id'], $order['order_id']));
continue;
}
if (!$order['spread_uid'] || $order['spread_uid'] <= 0) {
$skipped++;
$output->writeln(sprintf(' [SKIP] 订单 #%d (%s) 无推荐人', $order['id'], $order['order_id']));
continue;
}
if ($dryRun) {
$output->writeln(sprintf(
' [DRY-RUN] 订单 #%d (%s) uid=%d spread_uid=%d 需要补发积分',
$order['id'], $order['order_id'], $order['uid'], $order['spread_uid']
));
$patched++;
continue;
}
try {
$uid = (int)$order['uid'];
// 先执行等级升级检查,确保积分奖励使用正确的 agent_level
$userCacheInfo = $userServices->getUserCacheInfo($uid);
$spreadUid = $userCacheInfo ? (int)($userCacheInfo['spread_uid'] ?? 0) : (int)$order['spread_uid'];
$twoSpreadUid = 0;
if ($spreadUid > 0 && $oneUserInfo = $userServices->getUserCacheInfo($spreadUid)) {
$twoSpreadUid = $userServices->getSpreadUid($spreadUid, $oneUserInfo, false);
}
$uids = array_values(array_filter(array_unique([$uid, $spreadUid, $twoSpreadUid])));
$agentLevelServices->checkUserLevelFinish($uid, $uids);
// 发放积分奖励PointsRewardServices 内部已做幂等检查)
$pointsService->reward($uid, (string)$order['order_id'], (int)$order['id']);
$patched++;
$output->writeln(sprintf(
' [PATCHED] 订单 #%d (%s) uid=%d 等级检查+积分奖励已补发',
$order['id'], $order['order_id'], $uid
));
} catch (\Throwable $e) {
$output->writeln(sprintf(
' [ERROR] 订单 #%d (%s): %s',
$order['id'], $order['order_id'], $e->getMessage()
));
Log::error("[HjfPatchRewards] 订单 #{$order['id']} 补发失败: " . $e->getMessage());
}
}
$output->writeln(sprintf(
'[HjfPatchRewards] 完成:补发 %d 笔,跳过 %d 笔%s',
$patched, $skipped, $dryRun ? ' (dry-run 模式)' : ''
));
return 0;
}
}

View File

@@ -1,49 +0,0 @@
<?php
declare(strict_types=1);
namespace app\command;
use app\services\hjf\PointsReleaseServices;
use think\console\Command;
use think\console\Input;
use think\console\Output;
/**
* 积分每日释放命令
*
* 用法:
* php think hjf:release-points
*
* 触发时机:
* - 每天凌晨 00:01 由 crontab 或 Swoole Timer 调用
* - P4-05 联调验证时手动执行
*
* Class HjfReleasePoints
* @package app\command
*/
class HjfReleasePoints extends Command
{
protected function configure(): void
{
$this->setName('hjf:release-points')
->setDescription('执行黄精粉健康商城每日积分释放frozen_points × 4‰ → available_points');
}
protected function execute(Input $input, Output $output): int
{
$output->writeln('[HjfReleasePoints] 开始执行积分释放...');
/** @var PointsReleaseServices $service */
$service = app()->make(PointsReleaseServices::class);
$result = $service->executeRelease();
$output->writeln(sprintf(
'[HjfReleasePoints] 完成:处理 %d 人,共释放 %d 积分,日期 %s',
$result['processed'],
$result['total_released'],
$result['release_date']
));
return 0;
}
}

View File

@@ -1,170 +0,0 @@
<?php
declare(strict_types=1);
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\facade\Db;
/**
* e2e 验收:分销员等级配置检验命令
*
* 用法:
* php think hjf:verify-agent-config
*
* 说明:
* 验证 eb_agent_level 和 eb_agent_level_task 表中的数据配置与 PRD 3.2 一致。
* 若不一致则输出差异明细并自动修正(--fix 参数),修正后输出最终结果。
*
* Class HjfVerifyAgentConfig
* @package app\command
*/
class HjfVerifyAgentConfig extends Command
{
protected function configure(): void
{
$this->setName('hjf:verify-agent-config')
->setDescription('e2e 验收分销员等级奖励积分与升级任务配置是否与 PRD 一致,传入 --fix 自动修正')
->addOption('fix', null, \think\console\input\Option::VALUE_NONE, '自动修正不一致的配置');
}
protected function execute(Input $input, Output $output): int
{
$fix = (bool)$input->getOption('fix');
$hasError = false;
$output->writeln('');
$output->writeln('========================================================');
$output->writeln(' HJF 分销员等级配置 e2e 验收');
$output->writeln('========================================================');
// ----------------------------------------------------------------
// 1) eb_agent_level — 奖励积分配置PRD 3.2
// ----------------------------------------------------------------
$output->writeln('');
$output->writeln('【1】eb_agent_level 奖励积分配置');
$output->writeln('------------------------------------------------------------');
$expectedLevels = [
1 => ['name_hint' => '创客', 'direct_reward_points' => 500, 'umbrella_reward_points' => 0],
2 => ['name_hint' => '云店', 'direct_reward_points' => 800, 'umbrella_reward_points' => 300],
3 => ['name_hint' => '服务中心', 'direct_reward_points' => 1000, 'umbrella_reward_points' => 200],
4 => ['name_hint' => '合伙人', 'direct_reward_points' => 1300, 'umbrella_reward_points' => 300],
];
$levels = Db::name('agent_level')
->whereIn('grade', array_keys($expectedLevels))
->field('id,name,grade,direct_reward_points,umbrella_reward_points')
->select()
->toArray();
$levelsByGrade = [];
foreach ($levels as $level) {
$levelsByGrade[(int)$level['grade']] = $level;
}
foreach ($expectedLevels as $grade => $expected) {
if (!isset($levelsByGrade[$grade])) {
$output->writeln(" [MISS] grade={$grade} ({$expected['name_hint']}) 行不存在!");
$hasError = true;
continue;
}
$row = $levelsByGrade[$grade];
$errors = [];
if ((int)$row['direct_reward_points'] !== $expected['direct_reward_points']) {
$errors[] = "direct_reward_points={$row['direct_reward_points']}(期望 {$expected['direct_reward_points']}";
}
if ((int)$row['umbrella_reward_points'] !== $expected['umbrella_reward_points']) {
$errors[] = "umbrella_reward_points={$row['umbrella_reward_points']}(期望 {$expected['umbrella_reward_points']}";
}
if ($errors) {
$hasError = true;
$output->writeln(" [FAIL] grade={$grade} {$row['name']}" . implode('', $errors));
if ($fix) {
Db::name('agent_level')->where('id', $row['id'])->update([
'direct_reward_points' => $expected['direct_reward_points'],
'umbrella_reward_points' => $expected['umbrella_reward_points'],
]);
$output->writeln(" [FIX] 已修正 grade={$grade} {$row['name']}");
}
} else {
$output->writeln(" [OK] grade={$grade} {$row['name']} direct={$row['direct_reward_points']} umbrella={$row['umbrella_reward_points']}");
}
}
// ----------------------------------------------------------------
// 2) eb_agent_level_task — 升级任务配置PRD 3.2
// ----------------------------------------------------------------
$output->writeln('');
$output->writeln('【2】eb_agent_level_task 升级任务配置');
$output->writeln('------------------------------------------------------------');
// PRD 任务配置:[grade => [[type, number, description], ...]]
$expectedTasks = [
1 => [[6, 3, '直推满 3 单']],
2 => [[7, 30, '伞下满 30 单'], [8, 3, '至少 3 个直推']],
3 => [[7, 100, '伞下满 100 单'], [8, 3, '至少 3 个直推']],
4 => [[7, 1000, '伞下满 1000 单'], [8, 3, '至少 3 个直推']],
];
foreach ($expectedTasks as $grade => $tasks) {
if (!isset($levelsByGrade[$grade])) {
$output->writeln(" [SKIP] grade={$grade} 等级行不存在,跳过任务检查");
continue;
}
$levelId = $levelsByGrade[$grade]['id'];
$levelName = $levelsByGrade[$grade]['name'];
foreach ($tasks as [$type, $number, $desc]) {
$taskRow = Db::name('agent_level_task')
->where('level_id', $levelId)
->where('type', $type)
->field('id,number')
->find();
if (!$taskRow) {
$hasError = true;
$output->writeln(" [MISS] grade={$grade} {$levelName} type={$type}{$desc})行不存在!");
if ($fix) {
Db::name('agent_level_task')->insert([
'level_id' => $levelId,
'type' => $type,
'number' => $number,
]);
$output->writeln(" [FIX] 已插入 grade={$grade} type={$type} number={$number}");
}
} elseif ((int)$taskRow['number'] !== $number) {
$hasError = true;
$output->writeln(" [FAIL] grade={$grade} {$levelName} type={$type}{$desc}number={$taskRow['number']}(期望 {$number}");
if ($fix) {
Db::name('agent_level_task')->where('id', $taskRow['id'])->update(['number' => $number]);
$output->writeln(" [FIX] 已修正 grade={$grade} type={$type} number={$number}");
}
} else {
$output->writeln(" [OK] grade={$grade} {$levelName} type={$type}{$desc}number={$taskRow['number']}");
}
}
}
// ----------------------------------------------------------------
// 输出汇总
// ----------------------------------------------------------------
$output->writeln('');
$output->writeln('========================================================');
if ($hasError) {
if ($fix) {
$output->writeln(' 结果:检测到配置不一致,已自动修正。');
} else {
$output->writeln(' 结果:检测到配置不一致,请使用 --fix 自动修正,或手动更新数据库。');
}
} else {
$output->writeln(' 结果:所有配置与 PRD 一致,验收通过 ✓');
}
$output->writeln('========================================================');
$output->writeln('');
return $hasError ? 1 : 0;
}
}

View File

@@ -109,8 +109,7 @@ class Login
try {
aj_captcha_check_two($captchaType, $captchaVerification);
} catch (\Throwable $e) {
$msg = method_exists($e, 'getError') ? $e->getError() : $e->getMessage();
return app('json')->fail($msg);
return app('json')->fail($e->getError());
}
}
validate(\app\validate\admin\setting\SystemAdminValidate::class)->scene('get')->check(['account' => $account, 'pwd' => $password]);

View File

@@ -1,32 +0,0 @@
<?php
// +----------------------------------------------------------------------
// | 范氏国香 fsgx — Admin 会员管理接口
// +----------------------------------------------------------------------
namespace app\controller\admin\v1\hjf;
use app\controller\admin\AuthController;
use app\Request;
use app\services\user\UserServices;
class HjfMember extends AuthController
{
/**
* PUT /adminapi/hjf/member/{uid}/no_assess
* 设置/取消用户不考核状态
*/
public function setNoAssess(Request $request, int $uid)
{
if ($uid <= 0) return $this->fail('参数错误');
/** @var UserServices $userServices */
$userServices = app()->make(UserServices::class);
$user = $userServices->get($uid, ['uid', 'no_assess']);
if (!$user) return $this->fail('用户不存在');
$noAssess = (int)$request->post('no_assess', 1);
$userServices->update($uid, ['no_assess' => $noAssess ? 1 : 0], 'uid');
return $this->success($noAssess ? '已设置不考核' : '已取消不考核');
}
}

View File

@@ -1,65 +0,0 @@
<?php
// +----------------------------------------------------------------------
// | 范氏国香 fsgx — Admin 积分日志接口
// +----------------------------------------------------------------------
namespace app\controller\admin\v1\hjf;
use app\controller\admin\AuthController;
use app\Request;
use think\facade\Db;
class HjfPoints extends AuthController
{
/**
* GET /adminapi/hjf/points/release_log
* 积分释放日志(来自 UserBilltype = integralmark 包含"积分"
*/
public function releaseLog(Request $request)
{
$page = (int)$request->get('page', 1);
$limit = (int)$request->get('limit', 20);
$keyword = trim((string)$request->get('keyword', ''));
$type = trim((string)$request->get('type', ''));
$query = Db::table('eb_user_bill')
->alias('b')
->leftJoin('eb_user u', 'u.uid = b.uid')
->where('b.category', 'integral')
->field('b.id, b.uid, b.title, b.mark, b.number, b.balance, b.pm, b.add_time, u.nickname, u.avatar');
if ($keyword !== '') {
$query->where(function ($q) use ($keyword) {
$q->whereLike('u.nickname', "%{$keyword}%")
->whereOr('b.uid', $keyword);
});
}
if ($type !== '') {
$typeMap = [
'release' => 'frozen_points_release',
'brokerage'=> 'frozen_points_brokerage',
'consume' => '',
];
if ($type === 'consume') {
$query->where('b.pm', 0);
} elseif (isset($typeMap[$type]) && $typeMap[$type] !== '') {
$query->where('b.link_type', $typeMap[$type]);
}
}
$count = $query->count();
$list = $query->order('b.id', 'desc')
->page($page, $limit)
->select()
->toArray();
foreach ($list as &$item) {
$item['add_time_str'] = date('Y-m-d H:i:s', (int)$item['add_time']);
$item['pm_label'] = $item['pm'] ? '收入' : '支出';
}
unset($item);
return $this->success(['list' => $list, 'count' => $count]);
}
}

View File

@@ -1,161 +0,0 @@
<?php
declare(strict_types=1);
namespace app\controller\admin\v1\hjf;
use app\controller\admin\AuthController;
use app\dao\user\UserDao;
use app\services\agent\AgentLevelServices;
use app\services\hjf\MemberLevelServices;
use think\annotation\Inject;
/**
* Admin · 会员管理接口(改造复用版)
*
* 复用 eb_agent_level 体系,使用 eb_user.agent_level 字段。
*
* GET /adminapi/hjf/member/list — 会员列表
* PUT /adminapi/hjf/member/level/:uid — 手动调整会员等级
* GET /adminapi/hjf/member/config — 获取会员等级配置(从 eb_agent_level 读取)
* POST /adminapi/hjf/member/config — 保存会员等级配置(写入 eb_agent_level
*
* Class MemberController
* @package app\controller\admin\v1\hjf
*/
class MemberController extends AuthController
{
#[Inject]
protected UserDao $userDao;
#[Inject]
protected MemberLevelServices $levelServices;
#[Inject]
protected AgentLevelServices $agentLevelServices;
/**
* 会员列表(分页)
*/
public function memberList(): mixed
{
$where = $this->request->getMore([
['keyword', ''],
['member_level', ''],
['page', 1],
['limit', 20],
]);
$page = (int)$where['page'];
$limit = (int)$where['limit'];
$condition = [];
if ($where['keyword'] !== '') {
$condition['uid|nickname|phone'] = ['like', '%' . $where['keyword'] . '%'];
}
if ($where['member_level'] !== '') {
$grade = (int)$where['member_level'];
if ($grade === 0) {
$condition['agent_level'] = 0;
} else {
$agentLevelId = $this->agentLevelServices->getLevelIdByGrade($grade);
$condition['agent_level'] = $agentLevelId ?: -1;
}
}
$count = $this->userDao->count($condition);
$list = $this->userDao->selectList(
$condition,
'uid,nickname,avatar,phone,agent_level,frozen_points,available_points,now_money,spread_uid,add_time',
$page,
$limit,
'uid',
'desc'
);
$levelList = $this->agentLevelServices->dao->getList(['is_del' => 0, 'status' => 1]);
$levelMap = array_column($levelList, null, 'id');
foreach ($list as &$item) {
$agentLevelId = (int)($item['agent_level'] ?? 0);
$levelInfo = $levelMap[$agentLevelId] ?? null;
$item['member_level'] = $levelInfo ? (int)$levelInfo['grade'] : 0;
$item['member_level_name'] = $levelInfo ? $levelInfo['name'] : '普通会员';
$item['direct_order_count'] = $this->levelServices->getDirectQueueOrderCount((int)$item['uid']);
$item['umbrella_order_count'] = $this->levelServices->getUmbrellaQueueOrderCount((int)$item['uid']);
$item['direct_spread_count'] = $this->levelServices->getDirectSpreadCount((int)$item['uid']);
}
unset($item);
return $this->success(compact('list', 'count'));
}
/**
* 手动调整会员等级
*/
public function updateLevel(int $uid): mixed
{
$data = $this->request->getMore([
['member_level', 0],
]);
$grade = (int)$data['member_level'];
if ($grade < 0 || $grade > 4) {
return $this->fail('等级范围 0-4');
}
$user = $this->userDao->get($uid);
if (!$user) {
return $this->fail('用户不存在');
}
$this->levelServices->setUserLevel($uid, $grade);
return $this->success('更新成功');
}
/**
* 获取会员等级配置(从 eb_agent_level 表读取)
*/
public function getConfig(): mixed
{
$levelList = $this->agentLevelServices->dao->getList(['is_del' => 0, 'status' => 1]);
$config = [];
foreach ($levelList as $level) {
$config[] = [
'id' => $level['id'],
'name' => $level['name'],
'grade' => $level['grade'],
'direct_reward_points' => $level['direct_reward_points'] ?? 0,
'umbrella_reward_points' => $level['umbrella_reward_points'] ?? 0,
];
}
return $this->success($config);
}
/**
* 保存会员等级配置(写入 eb_agent_level 表)
*/
public function saveConfig(): mixed
{
$levels = $this->request->post('levels', []);
if (!is_array($levels)) {
return $this->fail('参数格式错误');
}
foreach ($levels as $item) {
if (empty($item['id'])) continue;
$updateData = [];
if (isset($item['direct_reward_points'])) {
$updateData['direct_reward_points'] = (int)$item['direct_reward_points'];
}
if (isset($item['umbrella_reward_points'])) {
$updateData['umbrella_reward_points'] = (int)$item['umbrella_reward_points'];
}
if ($updateData) {
$this->agentLevelServices->dao->update((int)$item['id'], $updateData);
}
}
return $this->success('保存成功');
}
}

View File

@@ -1,42 +0,0 @@
<?php
declare(strict_types=1);
namespace app\controller\admin\v1\hjf;
use app\controller\admin\AuthController;
use app\dao\hjf\PointsReleaseLogDao;
use think\annotation\Inject;
/**
* Admin · 积分管理接口
*
* GET /adminapi/hjf/points/release-log — 积分释放日志(分页)
*
* Class PointsController
* @package app\controller\admin\v1\hjf
*/
class PointsController extends AuthController
{
#[Inject]
protected PointsReleaseLogDao $dao;
/**
* 积分释放日志(分页)
*/
public function releaseLog(): mixed
{
$where = $this->request->getMore([
['keyword', ''],
['type', ''],
['start_time', ''],
['end_time', ''],
['page', 1],
['limit', 20],
]);
$page = (int)$where['page'];
$limit = (int)$where['limit'];
unset($where['page'], $where['limit']);
return $this->success($this->dao->getAdminList($where, $page, $limit));
}
}

View File

@@ -1,108 +0,0 @@
<?php
declare(strict_types=1);
namespace app\controller\admin\v1\hjf;
use app\controller\admin\AuthController;
use app\dao\hjf\QueuePoolDao;
use app\services\system\config\SystemConfigServices;
use crmeb\services\SystemConfigService;
use think\annotation\Inject;
/**
* Admin · 公排管理接口
*
* GET /adminapi/hjf/queue/order — 公排订单列表
* GET /adminapi/hjf/queue/config — 获取公排配置
* POST /adminapi/hjf/queue/config — 保存公排配置
* GET /adminapi/hjf/queue/finance — 公排退款财务流水
*
* Class QueueController
* @package app\controller\admin\v1\hjf
*/
class QueueController extends AuthController
{
#[Inject]
protected QueuePoolDao $dao;
/**
* 公排订单列表(分页 + 筛选)
*/
public function orderList(): mixed
{
$where = $this->request->getMore([
['keyword', ''],
['status', ''],
['start_time', ''],
['end_time', ''],
['page', 1],
['limit', 20],
]);
$page = (int)$where['page'];
$limit = (int)$where['limit'];
unset($where['page'], $where['limit']);
return $this->success($this->dao->getAdminList($where, $page, $limit));
}
/**
* 获取公排配置
*/
public function getConfig(): mixed
{
$config = [
'trigger_multiple' => (int)SystemConfigService::get('hjf_trigger_multiple', 4),
'release_rate' => (int)SystemConfigService::get('hjf_release_rate', 4),
'withdraw_fee_rate' => (int)SystemConfigService::get('hjf_withdraw_fee_rate', 7),
'enabled' => (bool)SystemConfigService::get('hjf_queue_pool_enable', 0),
'umbrella_reward_enable' => (bool)SystemConfigService::get('hjf_umbrella_reward_enable', 0),
];
return $this->success($config);
}
/**
* 保存公排配置
*/
public function saveConfig(SystemConfigServices $configServices): mixed
{
$data = $this->request->getMore([
['trigger_multiple', 4],
['release_rate', 4],
['withdraw_fee_rate', 7],
['enabled', 1],
['umbrella_reward_enable', 0],
]);
$map = [
'hjf_trigger_multiple' => (int)$data['trigger_multiple'],
'hjf_release_rate' => (int)$data['release_rate'],
'hjf_withdraw_fee_rate' => (int)$data['withdraw_fee_rate'],
'hjf_queue_pool_enable' => (int)$data['enabled'],
'hjf_umbrella_reward_enable' => (int)$data['umbrella_reward_enable'],
];
foreach ($map as $key => $value) {
$configServices->setConfig($key, (string)$value);
}
return $this->success('保存成功');
}
/**
* 公排退款财务流水(分页)
*/
public function financeList(): mixed
{
$where = $this->request->getMore([
['start_time', ''],
['end_time', ''],
['page', 1],
['limit', 20],
]);
$page = (int)$where['page'];
$limit = (int)$where['limit'];
unset($where['page'], $where['limit']);
return $this->success($this->dao->getFinanceList($where, $page, $limit));
}
}

View File

@@ -642,8 +642,7 @@ class StoreProduct extends AuthController
['presale_status', 0],//预售结束后状态
['is_send_gift', 0],//商品是否支持送礼
['send_gift_price', 0],//商品送礼附加费用
['level_type', 1],
['is_queue_goods', 0],//报单商品标记
['level_type', 1]
]);
if ($this->supplierId != 0) {
$data['supplier_id'] = $this->supplierId;

View File

@@ -106,26 +106,6 @@ class SystemTimer extends AuthController
return $this->success('添加定时器成功!');
}
/**
* 手动立即触发一个定时任务
* @param $id
* @return mixed
*/
public function run_now($id)
{
$timer = $this->services->getOneTimer($id);
$mark = $timer['mark'] ?? '';
if (!$mark) {
return $this->fail('定时任务标识不存在');
}
try {
$result = $this->services->runNow($mark);
return $this->success('任务已触发并执行成功', ['result' => $result]);
} catch (\Throwable $e) {
return $this->fail($e->getMessage());
}
}
/**
* 更新定时任务
* @param $id

View File

@@ -421,13 +421,7 @@ class SystemConfig extends AuthController
$is_store_stock = isset($post['store_stock']) && $post['store_stock'] != sys_config('store_stock');
// radio 类型字段:若历史 bug 导致 value 为单元素数组,自动展开为标量
$radioScalarFields = ['brokerage_scope', 'brokerage_timing'];
foreach ($post as $k => $v) {
if (in_array($k, $radioScalarFields) && is_array($v) && count($v) === 1) {
$v = $v[0];
$post[$k] = $v;
}
$config_one = $this->services->getOne(['menu_name' => $k]);
if ($config_one) {
$config_one['value'] = $v;

View File

@@ -85,8 +85,6 @@ class User extends AuthController
['isMember', ''],
['label_ids', ''],
['is_channel', ''],
/** HJF按分销等级 grade04筛选对应 eb_user.agent_level */
['hjf_member_level', ''],
]);
if ($where['label_ids']) {
$where['label_id'] = stringToIntArray($where['label_ids']);

View File

@@ -429,7 +429,7 @@ class RoutineTemplate extends AuthController
}
$data['code'] = $urlCode;
$data['appId'] = $appid;
$data['help'] = 'https://www.uj345.cn/web/pro/prov2/1192';
$data['help'] = 'https://doc.crmeb.com/web/pro/crmebprov2/1192';
return $this->success($data);
}
}

View File

@@ -1,64 +0,0 @@
<?php
declare(strict_types=1);
namespace app\controller\api\v1\hjf;
use app\Request;
use app\services\hjf\HjfAssetsServices;
use think\annotation\Inject;
/**
* 用户端 · 资产接口
*
* GET /api/hjf/assets/overview — 资产总览(余额 + 积分)
* GET /api/hjf/assets/cash/detail — 现金流水(分页)
*
* Class AssetsController
* @package app\controller\api\v1\hjf
*/
class AssetsController
{
#[Inject]
protected HjfAssetsServices $assetsServices;
/**
* 资产总览
*
* @param Request $request
* @return mixed
*/
public function overview(Request $request): mixed
{
$uid = (int)$request->uid();
return app('json')->success(
$this->assetsServices->getOverview($uid)
);
}
/**
* 现金流水(分页)
*
* 查询参数:
* - type: '' | queue_refund | withdraw | recharge
* - page, limit
*
* @param Request $request
* @return mixed
*/
public function cashDetail(Request $request): mixed
{
$uid = (int)$request->uid();
$type = (string)$request->param('type', '');
$page = max(1, (int)$request->param('page', 1));
$limit = min(50, max(1, (int)$request->param('limit', 15)));
$validTypes = ['', 'queue_refund', 'withdraw', 'recharge', 'pay'];
if (!in_array($type, $validTypes, true)) {
$type = '';
}
return app('json')->success(
$this->assetsServices->getCashDetail($uid, $type, $page, $limit)
);
}
}

View File

@@ -1,55 +0,0 @@
<?php
// +----------------------------------------------------------------------
// | 范氏国香 fsgx — 资产总览 API
// +----------------------------------------------------------------------
namespace app\controller\api\v1\hjf;
use app\Request;
use app\services\user\UserServices;
use app\services\agent\AgentLevelServices;
class HjfAssets
{
/**
* GET /api/hjf/assets/overview
* 资产总览聚合接口
* 返回:佣金余额、待释放积分、已释放积分、今日释放、分销等级名称
*/
public function overview(Request $request)
{
$uid = (int)$request->uid();
/** @var UserServices $userServices */
$userServices = app()->make(UserServices::class);
$user = $userServices->get($uid, ['uid', 'brokerage_price', 'frozen_points', 'available_points', 'agent_level', 'now_money']);
if (!$user) {
return app('json')->fail('用户不存在');
}
$agentLevelName = '';
if (!empty($user['agent_level'])) {
/** @var AgentLevelServices $agentLevelServices */
$agentLevelServices = app()->make(AgentLevelServices::class);
$levelInfo = $agentLevelServices->get((int)$user['agent_level'], ['id', 'name']);
$agentLevelName = $levelInfo ? $levelInfo['name'] : '';
}
// 今日释放积分frozen_points * 0.4‰(与定时任务逻辑一致)
$frozenPoints = (int)($user['frozen_points'] ?? 0);
$todayRelease = (int)floor($frozenPoints * 0.0004);
$availablePoints = (int)($user['available_points'] ?? 0);
return app('json')->successful([
'brokerage_price' => (string)($user['brokerage_price'] ?? '0.00'),
'now_money' => (string)($user['now_money'] ?? '0.00'),
'frozen_points' => $frozenPoints,
'available_points' => $availablePoints,
'today_release' => $todayRelease,
'total_points_earned' => $frozenPoints + $availablePoints,
'agent_level' => (int)($user['agent_level'] ?? 0),
'agent_level_name' => $agentLevelName,
]);
}
}

View File

@@ -1,53 +0,0 @@
<?php
// +----------------------------------------------------------------------
// | 范氏国香 fsgx — 推荐佣金周期进度 API
// +----------------------------------------------------------------------
namespace app\controller\api\v1\hjf;
use app\Request;
use app\services\user\UserServices;
class HjfBrokerage
{
/**
* GET /api/hjf/brokerage/progress
* 推荐佣金周期进度聚合接口
*/
public function progress(Request $request)
{
$uid = (int)$request->uid();
/** @var UserServices $userServices */
$userServices = app()->make(UserServices::class);
$userInfo = $userServices->get($uid, ['uid', 'brokerage_price', 'spread_uid', 'agent_level']);
// 周期配置
$cycleCount = (int)sys_config('brokerage_cycle_count', 3);
$cycleRatesRaw = sys_config('brokerage_cycle_rates', '[20,30,50]');
$cycleRates = json_decode($cycleRatesRaw, true) ?: [20, 30, 50];
// 统计当前用户作为 spread_uid 的下级已完成报单商品有效订单数
/** @var \app\dao\order\StoreOrderDao $orderDao */
$orderDao = app()->make(\app\dao\order\StoreOrderDao::class);
$totalCompleted = $orderDao->count([
'spread_uid' => $uid,
'is_queue_goods' => 1,
'paid' => 1,
'is_del' => 0,
]);
// 当前周期内完成人数
$cycleCurrent = $totalCompleted % $cycleCount;
// 累计佣金
$totalBrokerage = $userInfo ? (string)($userInfo['brokerage_price'] ?? '0.00') : '0.00';
return app('json')->successful([
'cycle_total' => $cycleCount,
'cycle_current' => $cycleCurrent,
'cycle_rates' => $cycleRates,
'total_brokerage' => $totalBrokerage,
'total_completed' => $totalCompleted,
]);
}
}

View File

@@ -1,178 +0,0 @@
<?php
declare(strict_types=1);
namespace app\controller\api\v1\hjf;
use app\Request;
use app\services\agent\AgentLevelServices;
use app\services\agent\AgentLevelTaskServices;
use app\services\hjf\MemberLevelServices;
use app\services\hjf\PointsRewardServices;
use app\dao\hjf\PointsReleaseLogDao;
use app\services\user\UserServices;
use think\annotation\Inject;
use think\facade\Db;
/**
* 用户端 · 会员信息接口(改造复用版)
*
* 复用 eb_agent_level 体系,使用 eb_user.agent_level 字段。
*
* GET /api/hjf/member/info — 当前用户等级信息
* GET /api/hjf/member/team — 团队成员列表
* GET /api/hjf/member/income — 团队收益明细
*
* Class MemberController
* @package app\controller\api\v1\hjf
*/
class MemberController
{
#[Inject]
protected MemberLevelServices $memberLevelServices;
#[Inject]
protected AgentLevelServices $agentLevelServices;
#[Inject]
protected AgentLevelTaskServices $agentLevelTaskServices;
/**
* 获取当前用户会员信息
*/
public function info(Request $request): \think\Response
{
$uid = (int)$request->uid();
$agentLevel = (int)Db::name('user')->where('uid', $uid)->value('agent_level');
// 直接从 eb_agent_level 取 name避免 grade 解析失败时等级徽章不显示
$levelRow = $agentLevel > 0 ? $this->agentLevelServices->getLevelInfo($agentLevel) : null;
$grade = $levelRow ? (int)$levelRow['grade'] : 0;
$levelName = $levelRow ? ($levelRow['name'] ?? '普通会员') : '普通会员';
$directCount = $this->memberLevelServices->getDirectSpreadCount($uid);
$umbrellaCount = $this->memberLevelServices->getUmbrellaQueueOrderCount($uid);
$directOrderCount = $this->memberLevelServices->getDirectQueueOrderCount($uid);
// 用已修正的 level row ID 查找下一等级,避免旧 status=0 记录导致 grade 被误判为 0
$effectiveLevelId = $levelRow ? (int)$levelRow['id'] : 0;
$nextLevel = $this->agentLevelServices->getNextLevelInfo($effectiveLevelId);
$nextLevelName = $nextLevel ? $nextLevel['name'] : null;
$upgradeProgress = [];
if ($nextLevel) {
$taskList = $this->agentLevelTaskServices->getUpgradeTasksForLevel((int)$nextLevel['id']);
foreach ($taskList as $task) {
$item = ['name' => $task['name'], 'number' => $task['number']];
switch ($task['type']) {
case 6:
$item['current'] = $directOrderCount;
break;
case 7:
$item['current'] = $umbrellaCount;
break;
case 8:
$item['current'] = $directCount;
break;
default:
$item['current'] = 0;
}
$item['completed'] = $item['current'] >= $item['number'];
$upgradeProgress[] = $item;
}
}
return app('json')->success([
'agent_level' => $agentLevel, // eb_user.agent_level 原始 ID供前端判断是否有等级
'member_level' => $grade,
'member_level_name' => $levelName, // eb_agent_level.name 直接值
'direct_count' => $directCount,
'umbrella_count' => $umbrellaCount,
'direct_order_count' => $directOrderCount,
'next_level_name' => $nextLevelName,
'upgrade_progress' => $upgradeProgress,
]);
}
/**
* 团队成员列表(直推/伞下)
*/
public function team(Request $request): \think\Response
{
$uid = (int)$request->uid();
$page = (int)$request->get('page', 1);
$limit = (int)$request->get('limit', 20);
$type = $request->get('type', 'direct');
/** @var UserServices $userServices */
$userServices = app()->make(UserServices::class);
if ($type === 'direct') {
$where = ['spread_uid' => $uid];
} else {
$directUids = $userServices->getColumn(['spread_uid' => $uid], 'uid');
if (empty($directUids)) {
return app('json')->success(['list' => [], 'count' => 0]);
}
$where = [['spread_uid', 'in', $directUids]];
}
$count = $userServices->count($where);
$list = Db::name('user')
->where($where)
->field('uid,nickname,avatar,phone,agent_level,add_time')
->page($page, $limit)
->order('uid desc')
->select()
->toArray();
$maps = $this->agentLevelServices->loadHjfUserListLevelMaps();
foreach ($list as &$item) {
$alId = (int)($item['agent_level'] ?? 0);
$levelInfo = $this->agentLevelServices->pickHjfLevelRowForUserListDisplay($alId, $maps);
$item['member_level'] = $levelInfo ? (int)$levelInfo['grade'] : 0;
$item['member_level_name'] = $levelInfo ? $levelInfo['name'] : '普通会员';
$item['join_time'] = date('Y-m-d', (int)$item['add_time']);
$item['direct_orders'] = $this->agentLevelTaskServices->getDirectQueueOrderCount((int)$item['uid']);
unset($item['agent_level'], $item['add_time']);
}
unset($item);
return app('json')->success(compact('list', 'count'));
}
/**
* 团队收益明细(积分奖励记录)
*/
public function income(Request $request): \think\Response
{
$uid = (int)$request->uid();
$page = (int)$request->get('page', 1);
$limit = (int)$request->get('limit', 20);
/** @var PointsReleaseLogDao $logDao */
$logDao = app()->make(PointsReleaseLogDao::class);
$where = [
'uid' => $uid,
'pm' => 1,
];
$where[] = ['type', 'in', ['reward_direct', 'reward_umbrella']];
$count = $logDao->count($where);
$list = Db::name('points_release_log')
->where($where)
->field('id,uid,points,type,title,mark,order_id,add_time')
->page($page, $limit)
->order('id desc')
->select()
->toArray();
foreach ($list as &$item) {
$item['time'] = date('Y-m-d H:i:s', (int)$item['add_time']);
}
unset($item);
return app('json')->success(compact('list', 'count'));
}
}

View File

@@ -1,49 +0,0 @@
<?php
declare(strict_types=1);
namespace app\controller\api\v1\hjf;
use app\Request;
use app\dao\hjf\PointsReleaseLogDao;
use think\annotation\Inject;
/**
* 用户端 · 积分明细接口
*
* GET /api/hjf/points/detail — 积分明细分页支持5种类型筛选
*
* Class PointsController
* @package app\controller\api\v1\hjf
*/
class PointsController
{
#[Inject]
protected PointsReleaseLogDao $dao;
/**
* 积分明细(分页)
*
* 查询参数:
* - type: '' | reward_direct | reward_umbrella | release | consume
* - page, limit
*
* @param Request $request
* @return mixed
*/
public function detail(Request $request): mixed
{
$uid = (int)$request->uid();
$type = (string)$request->param('type', '');
$page = max(1, (int)$request->param('page', 1));
$limit = min(50, max(1, (int)$request->param('limit', 15)));
$validTypes = ['', 'reward_direct', 'reward_umbrella', 'release', 'consume'];
if (!in_array($type, $validTypes, true)) {
$type = '';
}
return app('json')->success(
$this->dao->getDetailList($uid, $type, $page, $limit)
);
}
}

View File

@@ -1,60 +0,0 @@
<?php
declare(strict_types=1);
namespace app\controller\api\v1\hjf;
use app\Request;
use app\services\hjf\QueuePoolServices;
use think\annotation\Inject;
/**
* 用户端 · 公排接口
*
* GET /api/hjf/queue/status — 公排状态摘要
* GET /api/hjf/queue/history — 公排历史记录(分页)
*
* Class QueueController
* @package app\controller\api\v1\hjf
*/
class QueueController
{
#[Inject]
protected QueuePoolServices $services;
/**
* 公排状态摘要
* 返回:全平台总单数、当前批次进度、用户自己的订单列表(含预估等待)
*
* @param Request $request
* @return mixed
*/
public function status(Request $request): mixed
{
$uid = (int)$request->uid();
return app('json')->success($this->services->getUserStatus($uid));
}
/**
* 公排历史记录(分页)
*
* @param Request $request
* @return mixed
*/
public function history(Request $request): mixed
{
$uid = (int)$request->uid();
$status = (int)$request->param('status', -1); // -1=全部, 0=排队中, 1=已退款
[$page, $limit] = $this->getPage($request);
return app('json')->success(
$this->services->getUserHistory($uid, $status, $page, $limit)
);
}
private function getPage(Request $request): array
{
$page = max(1, (int)$request->param('page', 1));
$limit = min(50, max(1, (int)$request->param('limit', 15)));
return [$page, $limit];
}
}

View File

@@ -96,10 +96,6 @@ class UserBill
$urlCode = $imageInfo['att_dir'];
if ($imageInfo['image_type'] == 1) $urlCode = sys_config('site_url') . $urlCode;
}
if (!$urlCode) {
$siteUrl = sys_config('site_url', '');
$urlCode = $siteUrl . '?spread=' . $uid;
}
return app('json')->success(['url' => $urlCode]);
}

View File

@@ -1,84 +0,0 @@
<?php
declare(strict_types=1);
namespace app\dao\hjf;
use app\dao\BaseDao;
use app\model\hjf\PointsReleaseLog;
/**
* 积分释放日志 DAO
* Class PointsReleaseLogDao
* @package app\dao\hjf
*/
class PointsReleaseLogDao extends BaseDao
{
protected function setModel(): string
{
return PointsReleaseLog::class;
}
/**
* 查询积分明细列表(分页,支持按 type 筛选)
* type: reward_direct | reward_umbrella | release | consume | ''(全部)
*/
public function getDetailList(int $uid, string $type, int $page, int $limit): array
{
$model = $this->getModel()->where('uid', $uid);
if ($type !== '') {
$model = $model->where('type', $type);
}
$count = (clone $model)->count();
$list = $model->order('add_time', 'desc')
->page($page, $limit)
->select()
->toArray();
return compact('list', 'count');
}
/**
* Admin 积分释放日志(分页)
*/
public function getAdminList(array $where, int $page, int $limit): array
{
$model = $this->getModel();
if (!empty($where['keyword'])) {
$model = $model->where('uid', 'like', '%' . $where['keyword'] . '%');
}
if (!empty($where['type'])) {
$model = $model->where('type', $where['type']);
}
if (!empty($where['start_time'])) {
$model = $model->where('add_time', '>=', strtotime($where['start_time']));
}
if (!empty($where['end_time'])) {
$model = $model->where('add_time', '<=', strtotime($where['end_time']) + 86399);
}
$count = (clone $model)->count();
$list = $model->order('add_time', 'desc')
->page($page, $limit)
->select()
->toArray();
// 今日统计
$todayStart = strtotime(date('Y-m-d'));
$todayReleased = $this->getModel()
->where('type', 'release')
->where('add_time', '>=', $todayStart)
->sum('points');
$todayUsers = $this->getModel()
->where('type', 'release')
->where('add_time', '>=', $todayStart)
->group('uid')
->count();
return [
'list' => $list,
'count' => $count,
'statistics' => [
'total_released_today' => (int)$todayReleased,
'total_users_released' => (int)$todayUsers,
],
];
}
}

View File

@@ -1,140 +0,0 @@
<?php
declare(strict_types=1);
namespace app\dao\hjf;
use app\dao\BaseDao;
use app\model\hjf\QueuePool;
use crmeb\basic\BaseModel;
/**
* 公排池 DAO
* Class QueuePoolDao
* @package app\dao\hjf
*/
class QueuePoolDao extends BaseDao
{
protected function setModel(): string
{
return QueuePool::class;
}
/**
* 获取用户的公排记录列表(分页)
*/
public function getUserList(int $uid, int $status, int $page, int $limit): array
{
$model = $this->getModel()->where('uid', $uid);
if ($status >= 0) {
$model = $model->where('status', $status);
}
$count = (clone $model)->count();
$list = $model->order('add_time', 'desc')
->page($page, $limit)
->select()
->toArray();
return compact('list', 'count');
}
/**
* 获取全局公排列表Admin 分页)
*/
public function getAdminList(array $where, int $page, int $limit): array
{
$model = $this->getModel();
if (!empty($where['keyword'])) {
$model = $model->where('order_id|uid', 'like', '%' . $where['keyword'] . '%');
}
if (isset($where['status']) && $where['status'] !== '') {
$model = $model->where('status', (int)$where['status']);
}
if (!empty($where['start_time'])) {
$model = $model->where('add_time', '>=', strtotime($where['start_time']));
}
if (!empty($where['end_time'])) {
$model = $model->where('add_time', '<=', strtotime($where['end_time']) + 86399);
}
$count = (clone $model)->count();
$list = $model->order('queue_no', 'asc')
->page($page, $limit)
->select()
->toArray();
return compact('list', 'count');
}
/**
* 获取最早尚未退款的一条记录
*/
public function getEarliestPending(): ?array
{
$row = $this->getModel()
->where('status', 0)
->order('queue_no', 'asc')
->find();
return $row ? $row->toArray() : null;
}
/**
* 当前排队中总单数
*/
public function countPending(): int
{
return $this->getModel()->where('status', 0)->count();
}
/**
* 获取全局总单数(含已退款)
*/
public function countTotal(): int
{
return $this->getModel()->count();
}
/**
* 获取下一个全局排队序号MAX queue_no + 1
*/
public function nextQueueNo(): int
{
$max = $this->getModel()->max('queue_no');
return (int)$max + 1;
}
/**
* 标记一条记录为已退款
*/
public function markRefunded(int $id, int $batchNo): bool
{
return (bool)$this->getModel()
->where('id', $id)
->update([
'status' => 1,
'refund_time' => time(),
'trigger_batch' => $batchNo,
]);
}
/**
* 获取退款财务流水Admin 分页)
*/
public function getFinanceList(array $where, int $page, int $limit): array
{
$model = $this->getModel()->where('status', 1);
if (!empty($where['start_time'])) {
$model = $model->where('refund_time', '>=', strtotime($where['start_time']));
}
if (!empty($where['end_time'])) {
$model = $model->where('refund_time', '<=', strtotime($where['end_time']) + 86399);
}
$count = (clone $model)->count();
$totalRefund = (clone $model)->sum('amount');
$list = $model->order('refund_time', 'desc')
->page($page, $limit)
->select()
->toArray();
return [
'list' => $list,
'count' => $count,
'total_refund' => number_format((float)$totalRefund, 2, '.', ''),
];
}
}

View File

@@ -187,10 +187,6 @@ class UserWechatUserDao extends BaseDao
}
}
// HJF / 分销等级eb_user.agent_level由 hjf_member_level 归一化得到)
if (isset($where['hjf_agent_level_id']) && $where['hjf_agent_level_id'] !== '' && $where['hjf_agent_level_id'] !== null) {
$model = $model->where($userAlias . 'agent_level', (int)$where['hjf_agent_level_id']);
}
//用户等级
if (isset($where['level']) && $where['level']) {
$model = $model->where($userAlias . 'level', $where['level']);

View File

@@ -24,10 +24,6 @@ class InstallMiddleware implements MiddlewareInterface
public function handle(Request $request, \Closure $next)
{
// CORS 预检请求不重定向,交给后续 AllowOriginMiddleware 返回 200 + CORS 头
if (strtoupper($request->method()) === 'OPTIONS') {
return $next($request);
}
//检测是否已安装CRMEB系统
if (!is_dir(root_path() . "public/install/") || !is_file(root_path() . "public/install/install.lock")) {
return redirect('/install/index');

View File

@@ -1,126 +0,0 @@
<?php
declare(strict_types=1);
namespace app\jobs\hjf;
use app\services\agent\AgentLevelServices;
use app\services\hjf\PointsRewardServices;
use app\services\hjf\QueuePoolServices;
use app\services\user\UserServices;
use crmeb\basic\BaseJobs;
use crmeb\traits\QueueTrait;
use think\exception\ValidateException;
use think\facade\Db;
use think\facade\Log;
/**
* 报单商品支付成功异步处理 Job
*
* 触发时机Pay 监听器检测到 is_queue_goods=1 时派发。
*
* 执行流程:
* 1. 调用 QueuePoolServices::enqueue() 将订单写入公排池
* 2. 调用 AgentLevelServices::checkUserLevelFinish() 检查等级升级
* (复用 CRMEB 分销等级升级流程,已支持 HJF 任务类型 6/7/8
* 3. 等级升级完成后调用 PointsRewardServices::reward() 发放积分奖励
* (必须在升级后执行,确保级差计算使用正确的 agent_level
*
* Class HjfOrderPayJob
* @package app\jobs\hjf
*/
class HjfOrderPayJob extends BaseJobs
{
use QueueTrait;
public function doJob(int $uid, string $orderId, float $amount = 3600.00): bool
{
// 先查订单与购物车,计算报单商品总件数(公排入队 + 积分奖励共用)
$orderRow = Db::name('store_order')
->where('order_id', $orderId)
->where('is_queue_goods', 1)
->field('id,uid,is_queue_goods')
->find();
$queueQty = 1;
if ($orderRow) {
try {
$cartRows = Db::name('store_order_cart_info')
->where('oid', (int)$orderRow['id'])
->column('cart_info');
$qtySum = 0;
foreach ($cartRows as $row) {
$item = is_string($row) ? json_decode($row, true) : $row;
if (!empty($item['productInfo']['is_queue_goods'])) {
$qtySum += (int)($item['cart_num'] ?? 1);
}
}
if ($qtySum > 0) {
$queueQty = $qtySum;
}
} catch (\Throwable $qe) {
Log::warning("[HjfOrderPay] 计算报单商品数量异常使用默认值1: " . $qe->getMessage());
}
}
// PRD §3.1.2:一次购买多份时,拆分为多个独立记录分别进入公排池
$unitAmount = $queueQty > 1 ? round($amount / $queueQty, 2) : $amount;
try {
/** @var QueuePoolServices $queueServices */
$queueServices = app()->make(QueuePoolServices::class);
for ($i = 0; $i < $queueQty; $i++) {
$subOrderId = $queueQty > 1 ? $orderId . '-' . ($i + 1) : $orderId;
$queueServices->enqueue($uid, $subOrderId, $unitAmount);
}
Log::info("[HjfOrderPay] 公排入队成功 uid={$uid} orderId={$orderId} qty={$queueQty} unitAmount={$unitAmount}");
} catch (ValidateException $e) {
Log::warning("[HjfOrderPay] 入队被锁,延迟重试 uid={$uid} orderId={$orderId}: " . $e->getMessage());
static::dispatchSece(5, [$uid, $orderId, $amount]);
return true;
} catch (\Throwable $e) {
Log::error("[HjfOrderPay] 公排入队异常 uid={$uid} orderId={$orderId}: " . $e->getMessage());
return false;
}
$preUpgradeLevels = [];
try {
/** @var UserServices $userServices */
$userServices = app()->make(UserServices::class);
$userInfo = $userServices->getUserCacheInfo($uid);
$spreadUid = $userInfo ? (int)($userInfo['spread_uid'] ?? 0) : 0;
$twoSpreadUid = 0;
if ($spreadUid > 0 && $oneUserInfo = $userServices->getUserCacheInfo($spreadUid)) {
$twoSpreadUid = $userServices->getSpreadUid($spreadUid, $oneUserInfo, false);
}
$uids = array_unique([$uid, $spreadUid, $twoSpreadUid]);
// fsgx B2在升级前快照各上级用户的 agent_level避免触发升级的那笔订单发放积分
foreach ($uids as $u) {
if ($u <= 0) continue;
$uInfo = $userServices->get((int)$u, ['uid', 'agent_level']);
$preUpgradeLevels[(int)$u] = $uInfo ? (int)($uInfo['agent_level'] ?? 0) : 0;
}
/** @var AgentLevelServices $agentLevelServices */
$agentLevelServices = app()->make(AgentLevelServices::class);
$agentLevelServices->checkUserLevelFinish($uid, $uids);
Log::info("[HjfOrderPay] 等级升级检查完成 uid={$uid}");
} catch (\Throwable $e) {
Log::error("[HjfOrderPay] 等级升级检查失败 uid={$uid}: " . $e->getMessage());
}
// 等级升级完成后发放积分奖励(确保使用升级后的 agent_level
if ($orderRow) {
try {
/** @var PointsRewardServices $pointsService */
$pointsService = app()->make(PointsRewardServices::class);
$pointsService->reward($uid, $orderId, (int)$orderRow['id'], $preUpgradeLevels, $queueQty);
Log::info("[HjfOrderPay] 积分奖励发放完成 uid={$uid} orderId={$orderId} qty={$queueQty}");
} catch (\Throwable $e) {
Log::error("[HjfOrderPay] 积分奖励发放失败 uid={$uid} orderId={$orderId}: " . $e->getMessage());
}
}
return true;
}
}

View File

@@ -1,39 +0,0 @@
<?php
declare(strict_types=1);
namespace app\jobs\hjf;
use app\services\agent\AgentLevelServices;
use crmeb\basic\BaseJobs;
use crmeb\traits\QueueTrait;
/**
* 会员等级异步检查 Job改造复用版
*
* 委托给 AgentLevelServices::checkUserLevelFinish() 复用 CRMEB 分销等级升级流程。
*
* Class MemberLevelCheckJob
* @package app\jobs\hjf
*/
class MemberLevelCheckJob extends BaseJobs
{
use QueueTrait;
public function doJob(int $uid): bool
{
try {
/** @var AgentLevelServices $levelServices */
$levelServices = app()->make(AgentLevelServices::class);
$levelServices->checkUserLevelFinish($uid);
} catch (\Throwable $e) {
response_log_write([
'message' => "会员等级检查失败 uid={$uid}: " . $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
]);
return false;
}
return true;
}
}

View File

@@ -1,47 +0,0 @@
<?php
declare(strict_types=1);
namespace app\jobs\hjf;
use app\services\hjf\PointsReleaseServices;
use crmeb\basic\BaseJobs;
use crmeb\traits\QueueTrait;
use think\facade\Log;
/**
* 积分每日释放 Job
*
* 由定时任务crontab 或 Swoole Timer在每天凌晨 00:01 触发。
* 调用方式PointsReleaseJob::dispatch()
*
* Class PointsReleaseJob
* @package app\jobs\hjf
*/
class PointsReleaseJob extends BaseJobs
{
use QueueTrait;
/**
* 执行积分释放
* @return bool
*/
public function doJob(): bool
{
try {
/** @var PointsReleaseServices $releaseServices */
$releaseServices = app()->make(PointsReleaseServices::class);
$result = $releaseServices->executeRelease();
Log::info('[PointsReleaseJob] 执行完成', $result);
} catch (\Throwable $e) {
response_log_write([
'message' => '积分每日释放任务失败: ' . $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
]);
return false;
}
return true;
}
}

View File

@@ -1,95 +0,0 @@
<?php
declare(strict_types=1);
namespace app\jobs\hjf;
use app\dao\hjf\QueuePoolDao;
use app\dao\user\UserBillDao;
use app\dao\user\UserDao;
use crmeb\basic\BaseJobs;
use crmeb\traits\QueueTrait;
use think\facade\Db;
use think\facade\Log;
/**
* 公排退款异步 Job
*
* 由 QueuePoolServices::checkAndTriggerRefund() 派发。
* 执行流程:
* 1. 二次检查记录状态(防止重复退款)
* 2. 在数据库事务中:标记记录已退款 + 写入用户余额 + 写 user_bill 流水
*
* Class QueueRefundJob
* @package app\jobs\hjf
*/
class QueueRefundJob extends BaseJobs
{
use QueueTrait;
/**
* 执行退款
*
* @param int $queueId eb_queue_pool.id
* @param int $uid 用户 ID
* @param float $amount 退款金额
* @param int $batchNo 批次号
* @return bool
*/
public function doJob(int $queueId, int $uid, float $amount, int $batchNo): bool
{
try {
/** @var QueuePoolDao $queueDao */
$queueDao = app()->make(QueuePoolDao::class);
// 二次检查:防止重复退款
$record = $queueDao->get($queueId);
if (!$record || (int)$record['status'] === 1) {
Log::info("[QueueRefund] 记录 {$queueId} 已退款或不存在,跳过");
return true;
}
Db::transaction(function () use ($queueId, $uid, $amount, $batchNo, $queueDao) {
// 1. 标记公排记录为已退款
$queueDao->markRefunded($queueId, $batchNo);
// 2. 写入用户余额(使用 bcadd 避免浮点误差)
/** @var UserDao $userDao */
$userDao = app()->make(UserDao::class);
$user = $userDao->get($uid);
if (!$user) {
throw new \RuntimeException("用户 {$uid} 不存在");
}
$newMoney = bcadd((string)$user['now_money'], (string)$amount, 2);
$userDao->update($uid, ['now_money' => $newMoney], 'uid');
// 3. 写 user_bill 流水记录
/** @var UserBillDao $billDao */
$billDao = app()->make(UserBillDao::class);
$billDao->save([
'uid' => $uid,
'link_id' => $queueId,
'pm' => 1,
'title' => '公排退款',
'type' => 'queue_refund',
'category' => 'now_money',
'number' => $amount,
'balance' => $newMoney,
'mark' => "公排触发退款,批次#{$batchNo}",
'status' => 1,
'add_time' => time(),
]);
});
Log::info("[QueueRefund] 退款成功 uid={$uid} amount={$amount} batch={$batchNo}");
} catch (\Throwable $e) {
response_log_write([
'message' => '公排退款失败: ' . $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
]);
return false;
}
return true;
}
}

View File

@@ -13,9 +13,6 @@ namespace app\listener\order;
use app\jobs\agent\AgentJob;
use app\jobs\hjf\HjfOrderPayJob;
use app\services\agent\AgentLevelServices;
use app\services\hjf\PointsRewardServices;
use app\jobs\order\OrderCreateAfterJob;
use app\jobs\order\OrderDeliveryJob;
use app\jobs\order\OrderJob;
@@ -34,7 +31,6 @@ use app\services\order\StoreOrderCartInfoServices;
use app\services\order\StoreOrderComputedServices;
use app\services\order\StoreOrderCreateServices;
use app\services\order\StoreOrderInvoiceServices;
use app\services\order\StoreOrderTakeServices;
use app\services\user\channel\ChannelMerchantServices;
use app\services\user\UserMoneyServices;
use app\services\user\UserServices;
@@ -55,26 +51,6 @@ class Pay implements ListenerInterface
//计算订单实际金额
// OrderJob::dispatchDo('compute', [$orderInfo['uid'], $orderInfo['id']]);
$this->compute($userInfo['uid'] ?? 0, $orderInfo['id']);
// fsgx: brokerage_timing=on_pay 时,支付即发放佣金(跳过确认收货流程)
$brokerageTiming = sys_config('brokerage_timing', 'on_confirm');
if ($brokerageTiming === 'on_pay' && !empty($orderInfo['uid'])) {
try {
// compute() 已将 one_brokerage 写入 DB重新从 DB 读取最新订单数据再传给 backOrderBrokerage
/** @var \app\services\order\StoreOrderCreateServices $createSvc */
$createSvc = app()->make(StoreOrderCreateServices::class);
$freshOrder = $createSvc->get($orderInfo['id']);
if ($freshOrder) {
$orderInfo = $freshOrder->toArray();
}
/** @var \app\services\order\StoreOrderTakeServices $takeServices */
$takeServices = app()->make(StoreOrderTakeServices::class);
$takeServices->backOrderBrokerage($orderInfo, $userInfo);
} catch (\Throwable $e) {
Log::error('[Pay] brokerage_timing=on_pay 佣金发放失败 order_id=' . $orderInfo['id'] . ': ' . $e->getMessage());
}
}
//创建拼团
if ($orderInfo['activity_id'] && !$orderInfo['refund_status']) {
//拼团
@@ -150,72 +126,6 @@ class Pay implements ListenerInterface
event('notice.notice', [$orderInfo, 'admin_pay_success_code']);
//对外接口推送事件
event('out.outPush', ['order_pay_push', ['order_id' => (int)$orderInfo['id']]]);
// 报单商品订单:等级检查 + 积分奖励 + 公排入队(受 hjf_queue_pool_enable 控制)
if (!empty($orderInfo['is_queue_goods'])) {
$queuePoolEnable = (int)sys_config('hjf_queue_pool_enable', 0);
if ($queuePoolEnable) {
// 公排模式(开启):异步派发,含公排入队 + 等级检查 + 积分奖励
HjfOrderPayJob::dispatch([
(int)$orderInfo['uid'],
(string)$orderInfo['order_id'],
(float)($orderInfo['pay_price'] ?? 3600.00),
]);
} else {
// 非公排模式(默认关闭):同步执行等级检查 + 积分奖励,不入公排池,不依赖队列
try {
$uid = (int)$orderInfo['uid'];
/** @var UserServices $userServices */
$userServices = app()->make(UserServices::class);
$userCacheInfo = $userServices->getUserCacheInfo($uid);
$spreadUid = $userCacheInfo ? (int)($userCacheInfo['spread_uid'] ?? 0) : 0;
$twoSpreadUid = 0;
if ($spreadUid > 0 && $oneUserInfo = $userServices->getUserCacheInfo($spreadUid)) {
$twoSpreadUid = $userServices->getSpreadUid($spreadUid, $oneUserInfo, false);
}
$uids = array_filter(array_unique([$uid, $spreadUid, $twoSpreadUid]));
// fsgx B2在升级前快照各上级用户的 agent_level避免触发升级的那笔订单发放积分
$preUpgradeLevels = [];
foreach ($uids as $u) {
$uInfo = $userServices->get((int)$u, ['uid', 'agent_level']);
$preUpgradeLevels[(int)$u] = $uInfo ? (int)($uInfo['agent_level'] ?? 0) : 0;
}
/** @var AgentLevelServices $agentLevelServices */
$agentLevelServices = app()->make(AgentLevelServices::class);
$agentLevelServices->checkUserLevelFinish($uid, array_values($uids));
// fsgx B3计算订单中报单商品的总数量积分按数量倍乘
$queueQty = 1;
try {
/** @var StoreOrderCartInfoServices $cartSvc */
$cartSvc = app()->make(StoreOrderCartInfoServices::class);
$cartRows = $cartSvc->getColumn(['oid' => (int)$orderInfo['id']], 'cart_info');
$qtySum = 0;
foreach ($cartRows as $row) {
$item = is_string($row) ? json_decode($row, true) : $row;
if (!empty($item['productInfo']['is_queue_goods'])) {
$qtySum += (int)($item['cart_num'] ?? 1);
}
}
if ($qtySum > 0) {
$queueQty = $qtySum;
}
} catch (\Throwable $qe) {
Log::warning('[Pay] 计算报单商品数量异常使用默认值1: ' . $qe->getMessage());
}
/** @var PointsRewardServices $pointsService */
$pointsService = app()->make(PointsRewardServices::class);
$pointsService->reward($uid, (string)$orderInfo['order_id'], (int)$orderInfo['id'], $preUpgradeLevels, $queueQty);
Log::info('[Pay] 同步积分奖励发放完成 uid=' . $uid . ' order_id=' . $orderInfo['id'] . ' qty=' . $queueQty);
} catch (\Throwable $e) {
Log::error('[Pay] 同步积分奖励失败 order_id=' . $orderInfo['id'] . ': ' . $e->getMessage());
}
}
}
//自动打标签
event('user.auto.label', [$orderInfo['uid'], '', [], []]);
@@ -297,19 +207,11 @@ class Pay implements ListenerInterface
if ($spread_two_uid > 0) {
$orderData['spread_two_uid'] = $spread_two_uid;
}
// fsgx: 报单商品is_queue_goods=1需要参与周期佣金计算不受 type=8 限制
$isQueueOrder = !empty($orderInfo['is_queue_goods']);
// 人人分销(2) 或 报单订单:一级/二级佣金写入订单不因推荐人未标记推广员而丢失PRD 返现可审计)
$relaxBrokeragePromoter = $isQueueOrder || (int)sys_config('store_brokerage_statu', 1) === 2;
if ($cartInfo && (isset($orderInfo['type']) && (!in_array($orderInfo['type'], [4, 5, 7, 8]) || $isQueueOrder))) {
if ($cartInfo && (isset($orderInfo['type']) && !in_array($orderInfo['type'], [4, 5, 7, 8]))) {
/** @var StoreOrderComputedServices $orderComputed */
$orderComputed = app()->make(StoreOrderComputedServices::class);
if ($spread_uid > 0 && ($relaxBrokeragePromoter || $userServices->checkUserPromoter($spread_uid))) {
$orderData['one_brokerage'] = $orderComputed->getOrderSumPrice($cartInfo, 'one_brokerage', false);
}
if ($spread_two_uid > 0 && ($relaxBrokeragePromoter || $userServices->checkUserPromoter($spread_two_uid))) {
$orderData['two_brokerage'] = $orderComputed->getOrderSumPrice($cartInfo, 'two_brokerage', false);
}
if ($userServices->checkUserPromoter($spread_uid)) $orderData['one_brokerage'] = $orderComputed->getOrderSumPrice($cartInfo, 'one_brokerage', false);
if ($userServices->checkUserPromoter($spread_two_uid)) $orderData['two_brokerage'] = $orderComputed->getOrderSumPrice($cartInfo, 'two_brokerage', false);
$orderData['division_staff_brokerage'] = $orderComputed->getOrderSumPrice($cartInfo, 'division_staff_brokerage', false);
$orderData['division_agent_brokerage'] = $orderComputed->getOrderSumPrice($cartInfo, 'division_agent_brokerage', false);
$orderData['division_brokerage'] = $orderComputed->getOrderSumPrice($cartInfo, 'division_brokerage', false);

View File

@@ -193,20 +193,17 @@ class SystemTimer extends Cron implements ListenerInterface
break;
case 'holiday_gift_push_task':
return app()->make(HolidayGiftPushServices::class)->handleHolidayGiftTask();
case 'fsgx_release_frozen_points': // fsgx: 每日释放待释放积分 (0.4‰)
return app()->make(\app\services\hjf\PointsReleaseServices::class)->executeRelease();
}
} catch (\Throwable $e) {
/** @var SystemTimerServices $timerServices */
$timerServices = app()->make(SystemTimerServices::class);
$taskName = $timerServices->getTasKName();
response_log_write([
'message' => '定时任务:[' . ($taskName[$mark] ?? '未知') . '],失败原因:[' . class_basename($this) . ']',
'message' => '定时任务:[' . $taskName[$mark] ?? '未知' . '],失败原因:[' . class_basename($this) . ']',
'file' => $e->getFile(),
'line' => $e->getLine(),
'msg' => $e->getMessage()
]);
throw $e;
}
}
}

View File

@@ -1,30 +0,0 @@
<?php
declare(strict_types=1);
namespace app\model\hjf;
use crmeb\basic\BaseModel;
use crmeb\traits\ModelTrait;
/**
* 积分释放日志模型
* Class PointsReleaseLog
* @package app\model\hjf
*/
class PointsReleaseLog extends BaseModel
{
use ModelTrait;
protected $pk = 'id';
protected $name = 'points_release_log';
protected $autoWriteTimestamp = 'int';
protected $createTime = 'add_time';
public function setAddTimeAttr(): int
{
return time();
}
}

View File

@@ -1,40 +0,0 @@
<?php
declare(strict_types=1);
namespace app\model\hjf;
use crmeb\basic\BaseModel;
use crmeb\traits\ModelTrait;
/**
* 公排池模型
* Class QueuePool
* @package app\model\hjf
*/
class QueuePool extends BaseModel
{
use ModelTrait;
protected $pk = 'id';
protected $name = 'queue_pool';
protected $autoWriteTimestamp = 'int';
protected $createTime = 'add_time';
public function setAddTimeAttr(): int
{
return time();
}
/**
* 状态文本
* @param int $value
* @return string
*/
public function getStatusTextAttr(mixed $value, array $data): string
{
return ($data['status'] ?? 0) === 1 ? '已退款' : '排队中';
}
}

View File

@@ -39,11 +39,6 @@ class UserPointServices extends BaseServices
'storeIntegral_use' => '积分兑换商品',
'pay_product_integral_back' => '返还下单使用积分',
'sign' => '签到获得积分',
'hjf_frozen_direct' => '直推积分奖励',
'hjf_frozen_umbrella' => '伞下积分奖励',
'frozen_points_brokerage' => '佣金奖励积分(待释放)',
'frozen_points_release' => '每日积分释放',
'holiday_gift_integral' => '节日有礼赠送积分',
];
[$page, $limit] = $this->getPageValue();
$list = $this->dao->getList($where, '*', $page, $limit);
@@ -64,9 +59,9 @@ class UserPointServices extends BaseServices
} elseif ($item['type'] == 'storeIntegral_use') {
$item['relation'] = $integralOrderServices->value(['id' => $item['link_id']], 'order_id');
} else {
$item['relation'] = $status[$item['type']] ?? $item['type'];
$item['relation'] = $status[$item['type']];
}
$item['type_name'] = $status[$item['type']] ?? $item['type'];
$item['type_name'] = $status[$item['type']];
}
$count = $this->dao->count($where);
return compact('list', 'count', 'status');

View File

@@ -40,133 +40,6 @@ class AgentLevelServices extends BaseServices
#[Inject]
protected AgentLevelDao $dao;
/**
* HJF 官方会员等级名称(与数据库插入数据一致)
* 用于区分 CRMEB 默认「等级一/等级二…」与 HJF 创客/云店…
*/
public const HJF_OFFICIAL_LEVEL_NAMES = ['创客', '云店', '服务商', '分公司'];
/**
* 一次查询并返回用户列表展示所需等级索引(供 UserServices::index() 调用)
*
* @return array{byId: array<int,array>, byGradeAny: array<int,array>, byGradeOfficial: array<int,array>}
*/
public function loadHjfUserListLevelMaps(): array
{
$rows = $this->dao->getList(['is_del' => 0]);
return $this->buildHjfUserListLevelMaps($rows);
}
/**
* 从等级行列表构建用户列表展示用索引
*
* @param array $hjfLevelRows
* @return array{byId: array<int,array>, byGradeAny: array<int,array>, byGradeOfficial: array<int,array>}
*/
public function buildHjfUserListLevelMaps(array $hjfLevelRows): array
{
$byId = [];
$byGradeAny = [];
$byGradeOfficial = [];
$official = self::HJF_OFFICIAL_LEVEL_NAMES;
foreach ($hjfLevelRows as $hjfRow) {
$lid = (int)($hjfRow['id'] ?? 0);
if ($lid > 0) {
$byId[$lid] = $hjfRow;
}
$g = (int)($hjfRow['grade'] ?? 0);
if ($g > 0 && !isset($byGradeAny[$g])) {
$byGradeAny[$g] = $hjfRow;
}
$nm = (string)($hjfRow['name'] ?? '');
if ($g > 0 && in_array($nm, $official, true) && !isset($byGradeOfficial[$g])) {
$byGradeOfficial[$g] = $hjfRow;
}
}
return ['byId' => $byId, 'byGradeAny' => $byGradeAny, 'byGradeOfficial' => $byGradeOfficial];
}
/**
* 用户列表场景:解析应展示的等级行
* - agent_level 指向 CRMEB 默认行时,按 grade 改用 HJF 官方行
* - 旧 id 已软删或误把 grade 写入 agent_level 时,按 byGradeAny 回退
*
* @param int $agentLevelId 用户的 agent_level 字段值
* @param array $maps loadHjfUserListLevelMaps() 返回的索引
* @return array|null
*/
public function pickHjfLevelRowForUserListDisplay(int $agentLevelId, array $maps): ?array
{
if ($agentLevelId <= 0) {
return null;
}
$byId = $maps['byId'] ?? [];
$byGradeAny = $maps['byGradeAny'] ?? [];
$byGradeOfficial = $maps['byGradeOfficial'] ?? [];
$official = self::HJF_OFFICIAL_LEVEL_NAMES;
$row = $byId[$agentLevelId] ?? null;
if ($row === null) {
return $byGradeAny[$agentLevelId] ?? null;
}
$nm = (string)($row['name'] ?? '');
$g = (int)($row['grade'] ?? 0);
if ($g > 0 && !in_array($nm, $official, true) && isset($byGradeOfficial[$g])) {
return $byGradeOfficial[$g];
}
return $row;
}
/**
* 根据 grade 获取 agent_level ID用于筛选条件转换
*
* @param int $grade 等级数字 (1=创客, 2=云店, 3=服务商, 4=分公司)
* @return int agent_level ID找不到返回 0
*/
public function getLevelIdByGrade(int $grade): int
{
if ($grade <= 0) {
return 0;
}
return (int)$this->dao->value(['grade' => $grade, 'is_del' => 0, 'status' => 1], 'id') ?: 0;
}
/**
* 根据 agent_level ID 获取等级 gradeHJF 会员等级数字 0-4
*/
public function getGradeByLevelId(int $agentLevelId): int
{
if ($agentLevelId <= 0) {
return 0;
}
$levelInfo = $this->getLevelInfo($agentLevelId);
return (int)($levelInfo['grade'] ?? 0);
}
/**
* 根据 agent_level ID 获取直推奖励积分
*/
public function getDirectRewardPoints(int $agentLevelId): int
{
if ($agentLevelId <= 0) {
return 0;
}
$levelInfo = $this->getLevelInfo($agentLevelId);
return (int)($levelInfo['direct_reward_points'] ?? 0);
}
/**
* 根据 agent_level ID 获取伞下奖励积分
*/
public function getUmbrellaRewardPoints(int $agentLevelId): int
{
if ($agentLevelId <= 0) {
return 0;
}
$levelInfo = $this->getLevelInfo($agentLevelId);
return (int)($levelInfo['umbrella_reward_points'] ?? 0);
}
/**
* 获取某一个等级信息
* @param int $id

View File

@@ -20,7 +20,6 @@ use crmeb\services\FormBuilder as Form;
use FormBuilder\Factory\Iview;
use think\annotation\Inject;
use think\exception\ValidateException;
use think\facade\Db;
use think\facade\Route as Url;
@@ -39,11 +38,6 @@ class AgentLevelTaskServices extends BaseServices
* max_number 最大设定数值 0为不限定
* min_number 最小设定数值
* unit 单位
*
* type 6-8: HJF 会员等级升级任务类型(改造新增)
* 6 = 直推报单单数(直推下级购买报单商品的订单数)
* 7 = 伞下报单业绩(含业绩分离逻辑)
* 8 = 最低直推人数
* */
protected array $TaskType = [
[
@@ -96,36 +90,6 @@ class AgentLevelTaskServices extends BaseServices
'unit' => '单',
'image' => '/uploads/system/agent_spread_order.png'
],
[
'type' => 6,
'method' => 'directQueueOrderCount',
'name' => '直推报单满{$num}',
'real_name' => '直推报单单数',
'max_number' => 0,
'min_number' => 1,
'unit' => '单',
'image' => '/uploads/system/agent_spread_order.png'
],
[
'type' => 7,
'method' => 'umbrellaQueueOrderCount',
'name' => '伞下报单满{$num}',
'real_name' => '伞下报单业绩',
'max_number' => 0,
'min_number' => 1,
'unit' => '单',
'image' => '/uploads/system/agent_spread_order.png'
],
[
'type' => 8,
'method' => 'directSpreadCount',
'name' => '至少{$num}个直推',
'real_name' => '最低直推人数',
'max_number' => 0,
'min_number' => 1,
'unit' => '人',
'image' => '/uploads/system/agent_spread.png'
],
];
/**
@@ -392,20 +356,6 @@ class AgentLevelTaskServices extends BaseServices
$userNumber = $storeOrderServices->count($where);
}
break;
case 6:
// 直推下级购买报单商品的订单数
$userNumber = $this->getDirectQueueOrderCount($uid);
break;
case 7:
// 伞下报单业绩(含业绩分离)
$userNumber = $this->getUmbrellaQueueOrderCount($uid);
break;
case 8:
// 最低直推人数
/** @var UserServices $userServices */
$userServices = app()->make(UserServices::class);
$userNumber = $userServices->count(['spread_uid' => $uid]);
break;
default:
return false;
}
@@ -422,113 +372,6 @@ class AgentLevelTaskServices extends BaseServices
return [$msg, $userNumber, $isComplete];
}
/**
* 根据订单 ID 列表统计其中报单商品的总件数cart_num 之和)
*
* 一笔订单购买 N 份报单商品时 cart_num=N本方法返回所有订单的 N 之和,
* 而非订单行数。与 HjfOrderPayJob / StoreOrderCreateServices 中的 B3/B6 逻辑一致。
*/
private function sumQueueGoodsQty(array $orderIds): int
{
if (empty($orderIds)) {
return 0;
}
$cartRows = Db::name('store_order_cart_info')
->whereIn('oid', $orderIds)
->column('cart_info');
$total = 0;
foreach ($cartRows as $row) {
$item = is_string($row) ? json_decode($row, true) : $row;
if (!empty($item['productInfo']['is_queue_goods'])) {
$total += (int)($item['cart_num'] ?? 1);
}
}
return $total;
}
/**
* 统计直推下级的报单商品总份数type=6 任务)
*
* @param int $uid 用户 ID
* @return int
*/
public function getDirectQueueOrderCount(int $uid): int
{
/** @var UserServices $userServices */
$userServices = app()->make(UserServices::class);
// no_assess=1 的用户不计入升级任务,仅统计正常考核下级
$directUids = $userServices->getColumn(['spread_uid' => $uid, 'no_assess' => 0], 'uid');
if (empty($directUids)) {
return 0;
}
$orderIds = Db::name('store_order')
->whereIn('uid', $directUids)
->where('is_queue_goods', 1)
->where('paid', 1)
->where('is_del', 0)
->whereIn('refund_status', [0, 3])
->column('id');
return $this->sumQueueGoodsQty($orderIds);
}
/**
* 统计伞下报单业绩type=7 任务,含业绩分离逻辑)
*
* 业绩分离若某直推下级已升级为云店或更高grade≥2
* 则该下级及其团队的订单不计入本用户的伞下业绩。
*
* @param int $uid 用户 ID
* @param int $maxDepth 递归最大深度
* @return int
*/
public function getUmbrellaQueueOrderCount(int $uid, int $maxDepth = 8): int
{
return $this->recursiveUmbrellaCount($uid, $maxDepth);
}
/**
* 递归统计伞下业绩DFS云店及以上等级的下级团队业绩被分离
*/
private function recursiveUmbrellaCount(int $uid, int $remainDepth): int
{
if ($remainDepth <= 0) {
return 0;
}
$directChildren = Db::name('user')
->where('spread_uid', $uid)
->where('no_assess', 0) // 不考核用户不计入伞下业绩
->field('uid,agent_level')
->select()
->toArray();
if (empty($directChildren)) {
return 0;
}
/** @var AgentLevelServices $levelServices */
$levelServices = app()->make(AgentLevelServices::class);
$total = 0;
foreach ($directChildren as $child) {
$childGrade = 0;
if (!empty($child['agent_level'])) {
$childLevelInfo = $levelServices->getLevelInfo((int)$child['agent_level']);
$childGrade = (int)($childLevelInfo['grade'] ?? 0);
}
// 云店及以上业绩分离,不计入本级伞下
if ($childGrade >= 2) {
continue;
}
$childOrderIds = Db::name('store_order')
->where('uid', $child['uid'])
->where('is_queue_goods', 1)
->where('paid', 1)
->where('is_del', 0)
->whereIn('refund_status', [0, 3])
->column('id');
$total += $this->sumQueueGoodsQty($childOrderIds);
$total += $this->recursiveUmbrellaCount((int)$child['uid'], $remainDepth - 1);
}
return $total;
}
/**
* 检测等级任务
* @param int $id

View File

@@ -1,73 +0,0 @@
<?php
// +----------------------------------------------------------------------
// | 范氏国香 fsgx — 待释放积分每日释放服务
// +----------------------------------------------------------------------
namespace app\services\hjf;
use app\dao\user\UserDao;
use app\services\BaseServices;
use app\services\user\UserBillServices;
use think\facade\Db;
use think\facade\Log;
class FrozenPointsReleaseServices extends BaseServices
{
/**
* 每日将所有用户 frozen_points 的 0.4‰ 转入 available_points
* 由定时任务 fsgx_release_points每日执行触发
* @return bool
*/
public function dailyRelease(): bool
{
// 分批处理,避免一次性大量更新
$page = 1;
$limit = 200;
$releaseRatio = 0.0004; // 0.4‰
/** @var UserBillServices $billServices */
$billServices = app()->make(UserBillServices::class);
while (true) {
$users = Db::name('user')
->where('frozen_points', '>', 0)
->field('uid, frozen_points, available_points')
->limit($limit)
->page($page)
->select()
->toArray();
if (empty($users)) break;
foreach ($users as $user) {
try {
$releaseAmount = (int)floor($user['frozen_points'] * $releaseRatio);
if ($releaseAmount <= 0) continue;
Db::name('user')->where('uid', $user['uid'])->update([
'frozen_points' => max(0, $user['frozen_points'] - $releaseAmount),
'available_points' => $user['available_points'] + $releaseAmount,
]);
// 记录积分变动日志
$newBalance = $user['available_points'] + $releaseAmount;
$billServices->income('frozen_points_release', $user['uid'], [
'number' => $releaseAmount,
'mark' => date('Y-m-d') . ' 每日积分释放待释放积分的0.4‰)',
'balance' => $newBalance,
'type' => 'integral',
'title' => '积分释放',
'pm' => 1,
], $newBalance, 0);
} catch (\Throwable $e) {
Log::error('fsgx dailyRelease uid=' . $user['uid'] . ' error: ' . $e->getMessage());
}
}
if (count($users) < $limit) break;
$page++;
}
return true;
}
}

View File

@@ -1,89 +0,0 @@
<?php
declare(strict_types=1);
namespace app\services\hjf;
use app\dao\user\UserBillDao;
use app\dao\user\UserDao;
use app\services\BaseServices;
use think\annotation\Inject;
/**
* HJF 资产服务
*
* 提供用户资产总览和现金流水查询功能。
* 复用 CRMEB 原有 eb_user余额/积分)和 eb_user_bill流水字段。
*
* Class HjfAssetsServices
* @package app\services\hjf
*/
class HjfAssetsServices extends BaseServices
{
#[Inject]
protected UserDao $userDao;
#[Inject]
protected UserBillDao $billDao;
/**
* 获取用户资产总览
*
* 返回:
* - now_money 现金余额
* - frozen_points 待释放积分
* - available_points 已释放积分(可消费)
* - total_points 总积分frozen + available
*
* @param int $uid
* @return array
*/
public function getOverview(int $uid): array
{
$user = $this->userDao->get($uid, 'uid,now_money,frozen_points,available_points');
if (!$user) {
return [
'now_money' => '0.00',
'frozen_points' => 0,
'available_points' => 0,
'total_points' => 0,
];
}
$frozen = (int)($user['frozen_points'] ?? 0);
$available = (int)($user['available_points'] ?? 0);
return [
'now_money' => number_format((float)($user['now_money'] ?? 0), 2, '.', ''),
'frozen_points' => $frozen,
'available_points' => $available,
'total_points' => $frozen + $available,
];
}
/**
* 获取现金流水(分页)
*
* 复用 eb_user_bill 表,筛选 category='now_money' 的记录。
*
* @param int $uid
* @param string $type 流水类型筛选('' = 全部,'queue_refund' 公排退款,'withdraw' 提现等)
* @param int $page
* @param int $limit
* @return array
*/
public function getCashDetail(int $uid, string $type, int $page, int $limit): array
{
$where = [
'uid' => $uid,
'category' => 'now_money',
];
if ($type !== '') {
$where['type'] = $type;
}
$count = $this->billDao->count($where);
$list = $this->billDao->getBalanceRecord($where, $page, $limit);
return compact('list', 'count');
}
}

View File

@@ -1,111 +0,0 @@
<?php
// +----------------------------------------------------------------------
// | 范氏国香 fsgx — 积分释放服务
// +----------------------------------------------------------------------
namespace app\services\hjf;
use app\dao\user\UserDao;
use app\services\BaseServices;
use app\services\user\UserBillServices;
use think\facade\Db;
use think\facade\Log;
/**
* 待释放积分每日释放服务
* 规则:每日将 frozen_points 的 0.4‰ 转入 available_points
* 触发:定时任务 mark=fsgx_release_frozen_points每天执行一次
*/
class HjfPointsServices extends BaseServices
{
protected float $releaseRate = 0.0004; // 0.4‰
public function __construct(UserDao $dao)
{
$this->dao = $dao;
}
/**
* 每日批量释放待释放积分
* 批量处理,每批 200 条,避免内存溢出
*/
public function dailyReleasePoints(): bool
{
$page = 1;
$limit = 200;
$processedCount = 0;
do {
$users = Db::table('eb_user')
->where('frozen_points', '>', 0)
->field('uid, frozen_points, available_points')
->page($page, $limit)
->select()
->toArray();
foreach ($users as $user) {
try {
$release = (int)floor($user['frozen_points'] * $this->releaseRate);
if ($release <= 0) continue;
$newFrozen = $user['frozen_points'] - $release;
$newAvailable = $user['available_points'] + $release;
Db::table('eb_user')->where('uid', $user['uid'])->update([
'frozen_points' => max(0, $newFrozen),
'available_points' => $newAvailable,
]);
// 写积分日志
/** @var UserBillServices $billServices */
$billServices = app()->make(UserBillServices::class);
$billServices->income('frozen_points_release', $user['uid'], [
'number' => $release,
'mark' => '待释放积分每日释放',
'balance' => $newAvailable,
'type' => 'integral',
'title' => '积分释放',
'pm' => 1,
], $newAvailable, 0);
$processedCount++;
} catch (\Throwable $e) {
Log::error('fsgx dailyReleasePoints uid=' . $user['uid'] . ' error: ' . $e->getMessage());
}
}
$page++;
} while (count($users) === $limit);
Log::info("fsgx dailyReleasePoints done, processed={$processedCount}");
return true;
}
/**
* 扣减已释放积分(用于积分消费)
* @param int $uid
* @param int $points 消费积分数
* @return bool
*/
public function deductAvailablePoints(int $uid, int $points): bool
{
$user = Db::table('eb_user')->where('uid', $uid)->field('uid, available_points')->find();
if (!$user || $user['available_points'] < $points) return false;
$newAvailable = $user['available_points'] - $points;
Db::table('eb_user')->where('uid', $uid)->update(['available_points' => $newAvailable]);
/** @var UserBillServices $billServices */
$billServices = app()->make(UserBillServices::class);
$billServices->expenditure('available_points_use', $uid, [
'number' => $points,
'mark' => '积分消费',
'balance' => $newAvailable,
'type' => 'integral',
'title' => '积分消费',
'pm' => 0,
], $newAvailable, 0);
return true;
}
}

View File

@@ -1,162 +0,0 @@
<?php
declare(strict_types=1);
namespace app\services\hjf;
use app\services\agent\AgentLevelServices;
use app\services\agent\AgentLevelTaskServices;
use app\services\BaseServices;
use app\services\user\UserServices;
use think\annotation\Inject;
use think\facade\Db;
use think\facade\Log;
/**
* 会员等级升级服务(改造复用版)
*
* 基于 CRMEB Pro 的团队分销等级功能进行改造:
* - 使用 eb_user.agent_level (FK → eb_agent_level.id) 代替独立的 member_level
* - 升级条件通过 eb_agent_level_task 的 type 6/7/8 定义
* - 升级逻辑委托给 AgentLevelServices::checkUserLevelFinish()
*
* 本服务保留为薄封装层,提供 HJF 特有的查询方法供控制器调用。
*
* Class MemberLevelServices
* @package app\services\hjf
*/
class MemberLevelServices extends BaseServices
{
#[Inject]
protected AgentLevelServices $agentLevelServices;
#[Inject]
protected AgentLevelTaskServices $agentLevelTaskServices;
/**
* 检查并执行升级(异步触发入口)
*
* 委托给 CRMEB 的 AgentLevelServices 复用原有升级检测流程,
* 该流程已支持 type 6/7/8 的 HJF 任务类型。
*/
public function checkUpgrade(int $uid): void
{
try {
/** @var UserServices $userServices */
$userServices = app()->make(UserServices::class);
$userInfo = $userServices->getUserCacheInfo($uid);
if (!$userInfo) {
return;
}
$spreadUid = $userServices->getSpreadUid($uid, $userInfo);
$twoSpreadUid = 0;
if ($spreadUid > 0 && $oneUserInfo = $userServices->getUserCacheInfo($spreadUid)) {
$twoSpreadUid = $userServices->getSpreadUid($spreadUid, $oneUserInfo, false);
}
$uids = array_unique([$uid, $spreadUid, $twoSpreadUid]);
$this->agentLevelServices->checkUserLevelFinish($uid, $uids);
} catch (\Throwable $e) {
Log::error("[MemberLevel] checkUpgrade uid={$uid}: " . $e->getMessage());
}
}
/**
* 获取用户当前会员等级 grade0=普通, 1=创客, 2=云店, 3=服务商, 4=分公司)
*/
public function getUserGrade(int $uid): int
{
$agentLevel = (int)Db::name('user')->where('uid', $uid)->value('agent_level');
return $this->agentLevelServices->getGradeByLevelId($agentLevel);
}
/**
* 获取用户当前等级名称
*/
public function getUserLevelName(int $uid): string
{
$agentLevel = (int)Db::name('user')->where('uid', $uid)->value('agent_level');
if ($agentLevel <= 0) {
return '普通会员';
}
$maps = $this->agentLevelServices->loadHjfUserListLevelMaps();
$info = $this->agentLevelServices->pickHjfLevelRowForUserListDisplay($agentLevel, $maps);
return $info['name'] ?? '普通会员';
}
/**
* 获取直推用户的报单订单数
*/
public function getDirectQueueOrderCount(int $uid): int
{
return $this->agentLevelTaskServices->getDirectQueueOrderCount($uid);
}
/**
* 获取直推人数
*/
public function getDirectSpreadCount(int $uid): int
{
/** @var UserServices $userServices */
$userServices = app()->make(UserServices::class);
return (int)$userServices->count(['spread_uid' => $uid]);
}
/**
* 获取伞下总报单订单数(含业绩分离逻辑)
*/
public function getUmbrellaQueueOrderCount(int $uid): int
{
return $this->agentLevelTaskServices->getUmbrellaQueueOrderCount($uid);
}
/**
* 递归统计伞下总人数DFS最大深度 8 层,不含本人)
*/
public function getUmbrellaMemberCount(int $uid, int $maxDepth = 8): int
{
return $this->recursiveUmbrellaMemberCount($uid, $maxDepth);
}
private function recursiveUmbrellaMemberCount(int $uid, int $remainDepth): int
{
if ($remainDepth <= 0) {
return 0;
}
$children = Db::name('user')
->where('spread_uid', $uid)
->column('uid');
if (empty($children)) {
return 0;
}
$count = count($children);
foreach ($children as $childUid) {
$count += $this->recursiveUmbrellaMemberCount((int)$childUid, $remainDepth - 1);
}
return $count;
}
/**
* 手动设置会员等级(管理后台使用)
*
* @param int $uid 用户 ID
* @param int $grade 目标等级 grade (0-4)
*/
public function setUserLevel(int $uid, int $grade): void
{
$agentLevelId = 0;
if ($grade > 0) {
$agentLevelId = $this->agentLevelServices->getLevelIdByGrade($grade);
if ($agentLevelId <= 0) {
throw new \think\exception\ValidateException("等级 grade={$grade} 在 eb_agent_level 中不存在");
}
}
/** @var UserServices $userServices */
$userServices = app()->make(UserServices::class);
$userServices->update($uid, ['agent_level' => $agentLevelId]);
Log::info("[MemberLevel] 手动设置 uid={$uid} agent_level={$agentLevelId} (grade={$grade})");
}
}

View File

@@ -1,123 +0,0 @@
<?php
declare(strict_types=1);
namespace app\services\hjf;
use app\dao\user\UserDao;
use app\services\BaseServices;
use app\services\user\UserBillServices;
use crmeb\services\SystemConfigService;
use think\facade\Db;
use think\facade\Log;
/**
* 积分每日释放服务
*
* 由定时任务每天凌晨00:01或 Command 触发。
* 计算公式release_amount = FLOOR(frozen_points × rate / 1000)
* 其中 rate = hjf_release_rate默认 4即 4‰
*
* Class PointsReleaseServices
* @package app\services\hjf
*/
class PointsReleaseServices extends BaseServices
{
public function __construct(UserDao $dao)
{
$this->dao = $dao;
}
/**
* 执行今日积分释放(批量)
*
* @return array 统计:['processed' => int, 'total_released' => int]
*/
public function executeRelease(): array
{
$rate = (int)SystemConfigService::get('hjf_release_rate', 4);
$releaseDate = date('Y-m-d');
$processed = 0;
$totalReleased = 0;
$page = 1;
$limit = 200;
do {
$users = Db::table('eb_user')
->where('frozen_points', '>', 0)
->field('uid, frozen_points, available_points')
->page($page, $limit)
->select()
->toArray();
if (empty($users)) {
break;
}
foreach ($users as $user) {
$frozenBefore = (int)$user['frozen_points'];
$releaseAmount = (int)bcdiv(bcmul((string)$frozenBefore, (string)$rate), '1000');
if ($releaseAmount <= 0) {
continue;
}
$frozenAfter = $frozenBefore - $releaseAmount;
$newAvailable = (int)$user['available_points'] + $releaseAmount;
// 主更新:只更新积分字段,失败则跳过本用户
try {
Db::table('eb_user')->where('uid', $user['uid'])->update([
'frozen_points' => max(0, $frozenAfter),
'available_points' => $newAvailable,
]);
$totalReleased += $releaseAmount;
$processed++;
} catch (\Throwable $e) {
Log::error("[PointsRelease] uid={$user['uid']} 积分更新失败: " . $e->getMessage());
continue;
}
// 日志写入独立处理,失败不影响已提交的积分更新
try {
Db::table('eb_points_release_log')->insert([
'uid' => $user['uid'],
'points' => $releaseAmount,
'pm' => 1,
'type' => 'release',
'title' => '每日释放',
'mark' => "积分每日自动解冻,释放日期 {$releaseDate}",
'status' => 'released',
'release_date' => $releaseDate,
'add_time' => time(),
]);
/** @var UserBillServices $billServices */
$billServices = app()->make(UserBillServices::class);
$billServices->income(
'frozen_points_release',
(int)$user['uid'],
(int)$releaseAmount,
$newAvailable,
0,
0,
"积分每日自动解冻,释放日期 {$releaseDate}"
);
} catch (\Throwable $e) {
Log::warning("[PointsRelease] uid={$user['uid']} 日志写入失败(积分已释放): " . $e->getMessage());
}
}
$page++;
} while (count($users) === $limit);
Log::info("[PointsRelease] 完成processed={$processed} total_released={$totalReleased}");
return [
'processed' => $processed,
'total_released' => $totalReleased,
'release_date' => $releaseDate,
];
}
}

View File

@@ -1,219 +0,0 @@
<?php
declare(strict_types=1);
namespace app\services\hjf;
use app\dao\hjf\PointsReleaseLogDao;
use app\dao\user\UserDao;
use app\services\agent\AgentLevelServices;
use app\services\BaseServices;
use app\services\user\UserBillServices;
use think\annotation\Inject;
use think\facade\Db;
use think\facade\Log;
/**
* 积分奖励服务(级差计算)—— 改造复用版
*
* 改造要点PRD 3.2.2
* - 使用 eb_user.agent_level (FK → eb_agent_level.id) 获取会员等级
* - 从 eb_agent_level 表的 direct_reward_points / umbrella_reward_points 字段读取奖励积分
* - 不再使用独立的 member_level 字段和系统配置表中的 hjf_reward_* 键
*
* Class PointsRewardServices
* @package app\services\hjf
*/
class PointsRewardServices extends BaseServices
{
#[Inject]
protected PointsReleaseLogDao $logDao;
#[Inject]
protected UserDao $userDao;
#[Inject]
protected AgentLevelServices $agentLevelServices;
#[Inject]
protected UserBillServices $userBillServices;
/**
* 对一笔报单订单发放积分奖励
*
* @param int $orderDbId 订单表主键 id用于 user_bill.link_id 关联后台订单
* @param array $preUpgradeLevels 升级前各用户的 agent_level 快照 [uid => level_id],用于 B2 校验
* @param int $qty 订单中报单商品数量积分按数量倍乘B3
*/
public function reward(int $orderUid, string $orderId, int $orderDbId = 0, array $preUpgradeLevels = [], int $qty = 1): void
{
$qty = max(1, $qty);
try {
// 幂等检查:若该订单已有积分奖励记录则跳过,防止重复发放
$exists = Db::name('points_release_log')
->where('order_id', $orderId)
->whereIn('type', ['reward_direct', 'reward_umbrella'])
->count();
if ($exists > 0) {
Log::info("[PointsReward] 订单 {$orderId} 已有积分奖励记录,跳过重复发放");
return;
}
$buyer = $this->userDao->get($orderUid);
if (!$buyer || !$buyer['spread_uid']) {
return;
}
// 取买家自身的直推奖励积分作为级差下限,确保第一级父节点只拿差额
$buyerLevelId = array_key_exists($orderUid, $preUpgradeLevels)
? $preUpgradeLevels[$orderUid]
: (int)($buyer['agent_level'] ?? 0);
$buyerDirectReward = $this->agentLevelServices->getDirectRewardPoints($buyerLevelId);
$this->propagateReward((int)$buyer['spread_uid'], $orderUid, $orderId, $buyerDirectReward, 0, $orderDbId, $preUpgradeLevels, $qty);
} catch (\Throwable $e) {
Log::error("[PointsReward] 积分奖励失败 orderUid={$orderUid} orderId={$orderId}: " . $e->getMessage());
}
}
/**
* 向上递归发放直推积分奖励(标准逐级级差)
*
* 发放规则fsgx 问题3
* - grade=0非分销员跳过不获奖继续向上$lowerDirectReward 不变
* - grade=1创客仅当 depth=0买家直接推荐人才获得级差 reward_direct
* depth>0 时不获奖,但其 direct_reward_points 仍计入级差下限
* - grade>1云店/服务中心/合伙人):无论 depth只要在伞下链上就获得级差 reward_direct
*
* @param int $uid 当前被奖励用户
* @param int $fromUid 触发方(下级)用户 ID
* @param string $orderId 来源订单号
* @param int $lowerDirectReward 链中已被下级拿走的最高 direct_reward_points级差下限
* @param int $depth 递归深度(防止无限递归)
* @param int $orderDbId 订单表主键 id
* @param array $preUpgradeLevels 升级前各用户的 agent_level 快照 [uid => level_id]B2
* @param int $qty 报单商品数量积分按数量倍乘B3
*/
private function propagateReward(
int $uid,
int $fromUid,
string $orderId,
int $lowerDirectReward,
int $depth = 0,
int $orderDbId = 0,
array $preUpgradeLevels = [],
int $qty = 1
): void {
if ($depth >= 10 || $uid <= 0) {
return;
}
$user = $this->userDao->get($uid);
if (!$user) {
return;
}
// fsgx B2使用升级前的 agent_level 判断资格,避免触发升级的那笔订单就给新等级积分
$agentLevelId = array_key_exists($uid, $preUpgradeLevels)
? $preUpgradeLevels[$uid]
: (int)($user['agent_level'] ?? 0);
$grade = $this->agentLevelServices->getGradeByLevelId($agentLevelId);
if ($grade === 0) {
// 非分销员:跳过奖励,继续向上传递,$lowerDirectReward 保持不变
if ($user['spread_uid']) {
$this->propagateReward((int)$user['spread_uid'], $uid, $orderId, $lowerDirectReward, $depth + 1, $orderDbId, $preUpgradeLevels, $qty);
}
return;
}
$directReward = $this->agentLevelServices->getDirectRewardPoints($agentLevelId);
// 创客(grade=1)必须是买家直推人(depth=0)才获得级差奖励;
// 高于创客(grade>1)则伞下关系即可获得级差奖励
$isEligibleForDirect = ($grade > 1) || ($grade === 1 && $depth === 0);
if ($isEligibleForDirect) {
$actual = max(0, $directReward - $lowerDirectReward) * $qty;
if ($actual > 0) {
$this->grantFrozenPoints(
$uid,
$actual,
$orderId,
'reward_direct',
'直推奖励(级差)' . " x{$qty} - 来源订单 {$orderId}",
$orderDbId
);
}
}
// 伞下积分奖励独立逻辑仅对间接上级depth>0生效受开关控制
// 使用 umbrella_reward_points 平额发放(非级差),各等级独立拿自身额度
if ($depth > 0) {
$umbrellaRewardEnable = (int)sys_config('hjf_umbrella_reward_enable', 0);
if ($umbrellaRewardEnable) {
$umbrellaPoints = $this->agentLevelServices->getUmbrellaRewardPoints($agentLevelId);
if ($umbrellaPoints > 0) {
$this->grantFrozenPoints(
$uid,
$umbrellaPoints * $qty,
$orderId,
'reward_umbrella',
'伞下奖励' . " x{$qty} - 来源订单 {$orderId}",
$orderDbId
);
}
}
}
// 级差下限仅在本节点实际有资格获奖时才更新:
// 若节点被跳过(创客在 depth>0其 directReward 不应计入下限,
// 否则上级会被扣减一笔从未发出的积分fsgx 问题4
$nextLower = $isEligibleForDirect
? max($directReward, $lowerDirectReward)
: $lowerDirectReward;
if ($user['spread_uid']) {
$this->propagateReward(
(int)$user['spread_uid'],
$uid,
$orderId,
$nextLower,
$depth + 1,
$orderDbId,
$preUpgradeLevels,
$qty
);
}
}
/**
* 写入待释放积分frozen_points并记录明细
*/
private function grantFrozenPoints(
int $uid,
int $points,
string $orderId,
string $type,
string $mark,
int $orderDbId = 0
): void {
Db::transaction(function () use ($uid, $points, $orderId, $type, $mark, $orderDbId) {
$this->userDao->bcInc($uid, 'frozen_points', (string)$points, 'uid');
$this->logDao->save([
'uid' => $uid,
'points' => $points,
'pm' => 1,
'type' => $type,
'title' => ($type === 'reward_direct') ? '直推奖励' : '伞下奖励',
'mark' => $mark,
'status' => 'frozen',
'order_id' => $orderId,
]);
// PRD §3.3:营销后台积分日志读 eb_user_bill双写待释放积分明细不增加可消费 integral
$integralBalance = (int)($this->userDao->value(['uid' => $uid], 'integral') ?: 0);
$billType = ($type === 'reward_direct') ? 'hjf_reward_direct_integral' : 'hjf_reward_umbrella_integral';
$this->userBillServices->income($billType, $uid, $points, $integralBalance, $orderDbId, 0, $mark);
});
}
}

View File

@@ -1,193 +0,0 @@
<?php
declare(strict_types=1);
namespace app\services\hjf;
use app\dao\hjf\QueuePoolDao;
use app\jobs\hjf\QueueRefundJob;
use app\services\BaseServices;
use app\services\user\UserServices;
use crmeb\services\CacheService;
use crmeb\services\SystemConfigService;
use think\annotation\Inject;
use think\exception\ValidateException;
use think\facade\Db;
use think\facade\Log;
/**
* 公排池服务
*
* 负责入队enqueue+ 退款触发条件判断 + 统计信息查询。
* 退款的实际执行委托给 QueueRefundJob异步以避免支付回调阻塞。
*
* Class QueuePoolServices
* @package app\services\hjf
* @mixin QueuePoolDao
*/
class QueuePoolServices extends BaseServices
{
#[Inject]
protected QueuePoolDao $dao;
/** Redis 分布式锁 Key */
const LOCK_KEY = 'hjf:queue:enqueue_lock';
/** 锁超时(秒) */
const LOCK_TTL = 10;
/**
* 报单商品订单入队
*
* 使用 Redis SET NX EX 分布式锁保证同一时刻只有一个入队+触发检测操作执行。
*
* @param int $uid 用户 ID
* @param string $orderId 原始订单号
* @param float $amount 金额(默认 3600.00
* @return array 新入队记录数组
* @throws ValidateException
*/
public function enqueue(int $uid, string $orderId, float $amount = 3600.00): array
{
$lockKey = self::LOCK_KEY;
$lockValue = uniqid('', true);
// 获取 Redis 实例
/** @var \Redis $redis */
$redis = CacheService::getRedis();
// SET NX EX 原子锁
$acquired = $redis->set($lockKey, $lockValue, ['NX', 'EX' => self::LOCK_TTL]);
if (!$acquired) {
throw new ValidateException('公排入队繁忙,请稍后重试');
}
try {
return Db::transaction(function () use ($uid, $orderId, $amount, $redis, $lockKey, $lockValue) {
$queueNo = $this->dao->nextQueueNo();
$record = $this->dao->save([
'uid' => $uid,
'order_id' => $orderId,
'amount' => $amount,
'queue_no' => $queueNo,
'status' => 0,
'refund_time' => 0,
'trigger_batch' => 0,
]);
$data = $record->toArray();
// 检查是否触发退款条件
$this->checkAndTriggerRefund();
return $data;
});
} finally {
// 释放锁Lua 原子删除,防止误删他人的锁)
$script = <<<'LUA'
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
LUA;
$redis->eval($script, [$lockKey, $lockValue], 1);
}
}
/**
* 检查是否达到退款触发条件,若是则派发异步退款 Job
*
* 触发条件:当前排队中总单数 ≥ triggerMultiple默认4
* 即每进入4单就对最早的1单触发退款。
*/
public function checkAndTriggerRefund(): void
{
$multiple = (int)SystemConfigService::get('hjf_trigger_multiple', 4);
$pending = $this->dao->countPending();
if ($pending < $multiple) {
return;
}
$earliest = $this->dao->getEarliestPending();
if (!$earliest) {
return;
}
// 批次号 = 历史已退款总数 + 1
$batchNo = $this->dao->count(['status' => 1]) + 1;
// 派发异步退款 Job
QueueRefundJob::dispatch($earliest['id'], $earliest['uid'], $earliest['amount'], $batchNo);
}
/**
* 获取用户的公排状态摘要(用于状态页)
*/
public function getUserStatus(int $uid): array
{
$multiple = (int)SystemConfigService::get('hjf_trigger_multiple', 4);
$pending = $this->dao->countPending();
$total = $this->dao->countTotal();
// 当前批次已入队单数(本批次进度)
$batchCount = $pending % $multiple;
// 用户自己的订单
$myOrders = $this->dao->getModel()
->where('uid', $uid)
->order('add_time', 'desc')
->select()
->toArray();
foreach ($myOrders as &$item) {
$item['estimated_wait'] = $item['status'] === 1
? '已退款'
: $this->estimateWait((int)$item['queue_no'], $pending, $multiple);
}
unset($item);
return [
'total_orders' => $total,
'my_orders' => $myOrders,
'progress' => [
'current_batch_count' => $batchCount,
'trigger_multiple' => $multiple,
'next_refund_queue_no' => $this->dao->getEarliestPending()['queue_no'] ?? 0,
],
];
}
/**
* 获取用户公排历史(分页,支持按状态筛选)
*/
public function getUserHistory(int $uid, int $status, int $page, int $limit): array
{
$result = $this->dao->getUserList($uid, $status, $page, $limit);
foreach ($result['list'] as &$item) {
$item['time_key'] = date('Y-m-d', (int)$item['add_time']);
}
unset($item);
return $result;
}
/**
* 简单估算等待时间(基于队列位置)
*/
private function estimateWait(int $queueNo, int $pending, int $multiple): string
{
$earliest = $this->dao->getEarliestPending();
if (!$earliest) {
return '--';
}
$positionFromFront = $queueNo - (int)$earliest['queue_no'];
if ($positionFromFront <= 0) {
return '即将退款';
}
$waitCycles = (int)ceil($positionFromFront / $multiple);
return "约等待 {$waitCycles}";
}
}

View File

@@ -235,8 +235,6 @@ class StoreOrderCreateServices extends BaseServices
'province' => '',
'spread_uid' => 0,
'spread_two_uid' => 0,
// fsgx: 若购物车中含有报单商品则标记订单,方便后续佣金周期计数
'is_queue_goods' => (int)(count(array_filter($cartInfo, fn($c) => (int)($c['productInfo']['is_queue_goods'] ?? 0) === 1)) > 0),
'custom_form' => json_encode($customForm),
'promotions_give' => json_encode($promotions_give),
'give_integral' => $promotions_give['give_integral'] ?? 0,
@@ -926,9 +924,6 @@ class StoreOrderCreateServices extends BaseServices
$storeBrokerageTwo = $spread_two_uid = 0;
}
// fsgx B-2B: 同一订单内多个报单商品件数累计偏移,保证位次轮巡
$queueGoodsOffset = 0;
foreach ($cartInfo as &$cart) {
$oneBrokerage = '0';//一级返佣金额
$twoBrokerage = '0';//二级返佣金额
@@ -957,11 +952,7 @@ class StoreOrderCreateServices extends BaseServices
} else {
$is_brokerage = $productInfo['is_brokerage'] ?? 0;
}
// fsgx: is_queue_goods=1 的报单商品,即使 is_brokerage=0 也参与周期佣金计算
$isQueueGoodsProduct = (int)($productInfo['is_queue_goods'] ?? 0);
$brokerageScopeCheck = sys_config('brokerage_scope', 'all');
$queueGoodsBypassBrokerage = ($brokerageScopeCheck === 'queue_only' && $isQueueGoodsProduct === 1);
if ($is_brokerage == 0 && !$queueGoodsBypassBrokerage) {
if ($is_brokerage == 0) {
continue;
}
//指定返佣金额
@@ -986,91 +977,12 @@ class StoreOrderCreateServices extends BaseServices
break;
}
if ($price > 0) {
// fsgx: 判断返佣范围配置,若为仅报单商品则跳过非报单商品
$brokerageScope = sys_config('brokerage_scope', 'all');
$isQueueGoods = (int)($productInfo['is_queue_goods'] ?? 0);
if ($brokerageScope === 'queue_only' && $isQueueGoods !== 1) {
$cart['one_brokerage'] = '0';
$cart['two_brokerage'] = '0';
$cart['division_brokerage'] = '0';
$cart['division_agent_brokerage'] = '0';
$cart['division_staff_brokerage'] = '0';
continue;
//一级返佣比例 小于等于零时直接返回 不返佣
if ($storeBrokerageRatio > 0) {
//计算获取一级返佣比例
$brokerageRatio = bcdiv($storeBrokerageRatio, 100, 4);
$oneBrokerage = bcmul((string)$price, (string)$brokerageRatio, 2);
}
// fsgx: 周期循环佣金比例计算(仅影响一级佣金)
$cycleCount = (int)sys_config('brokerage_cycle_count', 3);
$cycleRatesRaw = sys_config('brokerage_cycle_rates', '[20,30,50]');
$cycleRates = json_decode($cycleRatesRaw, true);
$useCycleBrokerage = ($cycleCount > 0 && is_array($cycleRates) && count($cycleRates) > 0 && $isQueueGoods === 1);
if ($useCycleBrokerage && $spread_uid > 0) {
// fsgx B1 + B6 + B-2B用事务锁序列化位次计算支持多件商品逐件轮巡
$cartNumInt = (int)$cartNum;
// 计算单件价格,用于逐件计算佣金
$unitPrice = $cartNumInt > 0 ? bcdiv((string)$price, (string)$cartNumInt, 4) : '0';
$currentOffset = $queueGoodsOffset;
$brokerageResult = \think\facade\Db::transaction(function () use ($spread_uid, $cycleCount, $cycleRates, $unitPrice, $cartNumInt, $currentOffset) {
// 锁定推荐人行,确保同一推荐人同时只有一个事务在计算位次
\think\facade\Db::name('user')
->where('uid', $spread_uid)
->lock(true)
->value('uid');
// fsgx B1推荐人自己必须有报单商品订单才能获得推荐返现佣金
$spreaderOwnCount = (int)\think\facade\Db::name('store_order')
->where('uid', $spread_uid)
->where('is_queue_goods', 1)
->where('paid', 1)
->where('is_del', 0)
->count();
if ($spreaderOwnCount <= 0) {
return '0';
}
// fsgx B6统计推荐人已完成的报单商品总件数非订单数作为起始位次基准
$completedOrderIds = \think\facade\Db::name('store_order')
->where('spread_uid', $spread_uid)
->where('is_queue_goods', 1)
->where('paid', 1)
->where('is_del', 0)
->column('id');
$completedCount = 0;
if ($completedOrderIds) {
$completedCartRows = \think\facade\Db::name('store_order_cart_info')
->whereIn('oid', $completedOrderIds)
->column('cart_info');
foreach ($completedCartRows as $ccRow) {
$ccItem = is_string($ccRow) ? json_decode($ccRow, true) : $ccRow;
if (!empty($ccItem['productInfo']['is_queue_goods'])) {
$completedCount += (int)($ccItem['cart_num'] ?? 1);
}
}
}
// fsgx B-2B逐件轮巡每件商品取下一个位次的佣金比例后累加
$total = '0';
for ($i = 0; $i < $cartNumInt; $i++) {
$position = ($completedCount + $currentOffset + $i) % $cycleCount;
$cycleRatePercent = isset($cycleRates[$position]) ? (int)$cycleRates[$position] : (int)($cycleRates[0] ?? 0);
if ($cycleRatePercent > 0) {
$total = bcadd($total, bcmul((string)$unitPrice, bcdiv((string)$cycleRatePercent, '100', 4), 2), 2);
}
}
return $total;
});
$oneBrokerage = $brokerageResult;
// 当前购物车项的件数已消费,累加到偏移量
$queueGoodsOffset += $cartNumInt;
} else {
//一级返佣比例 小于等于零时直接返回 不返佣
if ($storeBrokerageRatio > 0) {
//计算获取一级返佣比例
$brokerageRatio = bcdiv($storeBrokerageRatio, 100, 4);
$oneBrokerage = bcmul((string)$price, (string)$brokerageRatio, 2);
}
}
//二级返佣比例小于等于0 直接返回
if ($storeBrokerageTwo > 0) {
//计算获取二级返佣比例

View File

@@ -22,7 +22,6 @@ use app\services\BaseServices;
use app\services\message\service\StoreServiceServices;
use app\services\message\sms\SmsSendServices;
use app\services\user\member\MemberCardServices;
use app\services\hjf\PointsRewardServices;
use app\services\user\UserBillServices;
use app\services\user\UserBrokerageServices;
use app\services\user\UserServices;
@@ -140,9 +139,8 @@ class StoreOrderTakeServices extends BaseServices
$res = $this->transaction(function () use ($order, $userInfo, $storeTitle) {
//赠送积分
$res1 = $this->gainUserIntegral($order, $userInfo, $storeTitle);
// fsgx: brokerage_timing=on_pay 时佣金已在支付时发放,此处跳过
$brokerageTiming = sys_config('brokerage_timing', 'on_confirm');
$res2 = ($brokerageTiming === 'on_pay') ? true : $this->backOrderBrokerage($order, $userInfo);
//返佣
$res2 = $this->backOrderBrokerage($order, $userInfo);
//经验
$res3 = $this->gainUserExp($order, $userInfo);
//事业部
@@ -241,10 +239,8 @@ class StoreOrderTakeServices extends BaseServices
//商城分销功能是否开启 0关闭1开启
if (!sys_config('brokerage_func_status')) return true;
// 营销产品不返佣金但报单商品is_queue_goods=1无论 type 均需参与返佣
$isQueueOrder = !empty($orderInfo['is_queue_goods']);
$relaxBrokeragePromoter = $isQueueOrder || (int)sys_config('store_brokerage_statu', 1) === 2;
if (!$isQueueOrder && (!isset($orderInfo['type']) || in_array($orderInfo['type'], [4, 5, 7, 8]))) {
// 营销产品不返佣金
if (!isset($orderInfo['type']) || in_array($orderInfo['type'], [4, 5, 7, 8])) {
return true;
}
//绑定失效
@@ -267,18 +263,14 @@ class StoreOrderTakeServices extends BaseServices
$broken_time = intval(sys_config('extract_time'));
$frozen_time = time() + $broken_time * 86400;
//检测是否是分销员(报单/人人分销时不强制要求 is_promoter否则周期返现与积分奖励无法落库
//检测是否是分销员
/** @var UserServices $userServices */
$userServices = app()->make(UserServices::class);
if (!$relaxBrokeragePromoter && !$userServices->checkUserPromoter($one_spread_uid)) {//一级不是分销员 直接二级返佣
if (!$userServices->checkUserPromoter($one_spread_uid)) {//一级不是分销员 直接二级返佣
return $this->backOrderBrokerageTwo($orderInfo, $userInfo, $isSelfBrokerage, $frozen_time);
}
//订单中取出
$brokeragePrice = $orderInfo['one_brokerage'] ?? 0;
// fsgx: 积分奖励已移至 HjfOrderPayJob等级升级完成后触发此处不再触发
// 避免推荐人升级前 grade=0 导致积分被跳过的时序问题
// 返佣金额小于等于0 直接返回不返佣金
if ($brokeragePrice <= 0) {
return true;
@@ -302,43 +294,12 @@ class StoreOrderTakeServices extends BaseServices
$res2 = $userServices->bcInc($one_spread_uid, 'brokerage_price', $brokeragePrice, 'uid');
//给上级发送获得佣金的模板消息
$this->sendBackOrderBrokerage($orderInfo, $one_spread_uid, $brokeragePrice);
// 一级返佣成功 跳转二级返佣
$res = $res1 && $res2 && $this->backOrderBrokerageTwo($orderInfo, $userInfo, $isSelfBrokerage, $frozen_time);
return $res;
}
/**
* fsgx: 按照 eb_agent_level 配置的「直推/伞下奖励积分」发放 frozen_points
*
* 积分奖励规则:
* - 直推上级:获得其等级 direct_reward_points 配置的积分
* - 更上级(伞下):按级差规则获得 umbrella_reward_points 积分
* - 具体计算由 PointsRewardServices::reward() 统一实现
*
* @param int $spreadUid 直推上级uid仅用于前置校验
* @param string|float $brokeragePrice 本次佣金金额(仅日志参考,不再作为前置条件)
* @param array $orderInfo 订单信息(需含 uid、order_id
*/
protected function grantFrozenPointsByBrokerage(int $spreadUid, $brokeragePrice, array $orderInfo): void
{
try {
if ($spreadUid <= 0) return;
$buyerUid = (int)($orderInfo['uid'] ?? 0);
$orderId = (string)($orderInfo['order_id'] ?? '');
if ($buyerUid <= 0 || $orderId === '') return;
$orderDbId = (int)($orderInfo['id'] ?? 0);
/** @var PointsRewardServices $pointsServices */
$pointsServices = app()->make(PointsRewardServices::class);
$pointsServices->reward($buyerUid, $orderId, $orderDbId);
} catch (\Throwable $e) {
Log::error('fsgx grantFrozenPointsByBrokerage error: ' . $e->getMessage());
}
}
/**
* 二级推广返佣
* @param $orderInfo
@@ -371,9 +332,8 @@ class StoreOrderTakeServices extends BaseServices
$spread_two_uid = $isSelfbrokerage ? $userInfoTwo['uid'] : $userInfoTwo['spread_uid'];
}
$spread_two_uid = (int)$spread_two_uid;
$isQueueOrder = !empty($orderInfo['is_queue_goods']);
$relaxBrokeragePromoter = $isQueueOrder || (int)sys_config('store_brokerage_statu', 1) === 2;
if (!$relaxBrokeragePromoter && !$userServices->checkUserPromoter($spread_two_uid)) {
// 获取后台分销类型 1 指定分销 2 人人分销
if (!$userServices->checkUserPromoter($spread_two_uid)) {
return true;
}
//订单中取出

View File

@@ -23,7 +23,6 @@ use crmeb\services\wechat\MiniProgram;
use crmeb\services\wechat\OfficialAccount;
use GuzzleHttp\Psr7\Utils;
use think\annotation\Inject;
use think\facade\Log;
use think\exception\ValidateException;
@@ -283,12 +282,10 @@ class QrcodeServices extends BaseServices
}
}
$siteUrl = sys_config('site_url');
$imageInfo = '';
if (!$imageInfo) {
$res = MiniProgram::appCodeUnlimit($data, $page, 280);
if (!$res) {
Log::error('[getRoutineQrcodePath] appCodeUnlimit 返回空, scene=' . $data . ', page=' . $page);
return false;
}
if (!$res) return false;
$uploadType = (int)sys_config('upload_type', 1);
$upload = UploadService::init($uploadType);
$res = (string)Utils::streamFor($res);
@@ -320,7 +317,6 @@ class QrcodeServices extends BaseServices
if ($imageInfo['image_type'] == 1) $url = $siteUrl . $url;
return $url;
} catch (\Throwable $e) {
Log::error('[getRoutineQrcodePath] 生成小程序码异常: ' . $e->getMessage());
return false;
}
}

View File

@@ -877,7 +877,6 @@ class StoreProductServices extends BaseServices
$data['brand_id'] = $data['brand_id'] ? end($data['brand_id']) : 0;
$data['product_type'] = intval($data['product_type']);
$data['is_brokerage'] = intval($data['is_brokerage']);
$data['is_queue_goods'] = intval($data['is_queue_goods'] ?? 0);
$data['is_sub'] = intval($data['is_sub']);
$data['is_vip'] = intval($data['is_vip'] ?? 0);
$data['is_vip_product'] = intval($data['is_vip_product']);

View File

@@ -1478,7 +1478,6 @@ class SystemConfigServices extends BaseServices implements ServeConfigInterface
$data = $this->getConfigAllField([
'brokerage_compute_type', 'store_brokerage_ratio', 'store_brokerage_two', 'extract_time', 'is_self_brokerage', 'brokerage_user_status',
'uni_brokerage_price', 'day_brokerage_price_upper',
'brokerage_cycle_count', 'brokerage_cycle_rates', 'brokerage_scope', 'brokerage_timing',
]);
$build->rule([
Build::tabs()->option('返佣设置', [
@@ -1492,11 +1491,6 @@ class SystemConfigServices extends BaseServices implements ServeConfigInterface
Build::inputNum('uni_brokerage_price', $data['uni_brokerage_price']['info'], $data['uni_brokerage_price']['value'])->min(0)->info($data['uni_brokerage_price']['desc']),
Build::inputNum('day_brokerage_price_upper', $data['day_brokerage_price_upper']['info'], $data['day_brokerage_price_upper']['value'])->min(-1)->info($data['day_brokerage_price_upper']['desc']),
])->trueValue('开启', 1)->falseValue('关闭', 0)->info($data['brokerage_user_status']['desc']),
])->option('推荐佣金fsgx', [
Build::inputNum('brokerage_cycle_count', $data['brokerage_cycle_count']['info'] ?: '佣金周期人数', (int)($data['brokerage_cycle_count']['value'] ?: 3))->min(1)->info('推荐N人为一个周期循环计算各档佣金比例'),
Build::input('brokerage_cycle_rates', $data['brokerage_cycle_rates']['info'] ?: '各档佣金比例(JSON)', is_array($data['brokerage_cycle_rates']['value']) ? json_encode($data['brokerage_cycle_rates']['value']) : ($data['brokerage_cycle_rates']['value'] ?: '[20,30,50]'))->info('JSON数组元素为百分比整数如[20,30,50]表示第1人20%、第2人30%、第3人50%'),
Build::radio('brokerage_scope', $data['brokerage_scope']['info'] ?: '返佣范围', is_array($data['brokerage_scope']['value']) ? ($data['brokerage_scope']['value'][0] ?? 'queue_only') : ($data['brokerage_scope']['value'] ?: 'queue_only'))->options([['label' => '所有商品', 'value' => 'all'], ['label' => '仅报单商品', 'value' => 'queue_only']])->info('queue_only=仅is_queue_goods=1的商品参与佣金计算'),
Build::radio('brokerage_timing', $data['brokerage_timing']['info'] ?: '佣金发放时机', is_array($data['brokerage_timing']['value']) ? ($data['brokerage_timing']['value'][0] ?? 'on_pay') : ($data['brokerage_timing']['value'] ?: 'on_pay'))->options([['label' => '支付即发放', 'value' => 'on_pay'], ['label' => '确认收货后发放', 'value' => 'on_confirm']])->info('on_pay=订单支付后立即发放 on_confirm=用户确认收货后发放'),
])
]);
@@ -2153,7 +2147,7 @@ class SystemConfigServices extends BaseServices implements ServeConfigInterface
Build::switch('offline_pay_status', $data['offline_pay_status']['info'], (int)$data['offline_pay_status']['value'])->trueValue('开启', 1)->falseValue('关闭', 2)->info($data['offline_pay_status']['desc']),
])->option('微信支付', [
Build::alert('登录微信商户(地址https://pay.weixin.qq.com支付授权目录、回调链接' . $site_url . ' http,https最好都配置)帮助文档地址https://www.uj345.cn/web/pro/prov2/1203', Alert::WARNING)->showIcon(true),
Build::alert('登录微信商户(地址https://pay.weixin.qq.com支付授权目录、回调链接' . $site_url . ' http,https最好都配置)帮助文档地址https://doc.crmeb.com/web/pro/crmebprov2/1203', Alert::WARNING)->showIcon(true),
Build::input('pay_weixin_mchid', $data['pay_weixin_mchid']['info'], $data['pay_weixin_mchid']['value'])->info($data['pay_weixin_mchid']['desc']),
Build::radio('pay_wechat_type', $data['pay_wechat_type']['info'], (int)$data['pay_wechat_type']['value'])->control(1, [
Build::input('pay_weixin_serial_no', $data['pay_weixin_serial_no']['info'], $data['pay_weixin_serial_no']['value'])->info($data['pay_weixin_serial_no']['desc']),
@@ -2176,7 +2170,7 @@ class SystemConfigServices extends BaseServices implements ServeConfigInterface
Build::input('pay_routine_mchid', $data['pay_routine_mchid']['info'], $data['pay_routine_mchid']['value'])->info($data['pay_routine_mchid']['desc'])
])->trueValue('开启', 1)->falseValue('关闭', 0)->info($data['pay_routine_open']['desc'])
])->option('支付宝支付', [
Build::alert('登录支付宝商家(地址https://b.alipay.com需要配置ip白名单以及回调地址回调地址' . $site_url . ')帮助文档地址https://www.uj345.cn/web/pro/prov2/1204', Alert::WARNING)->showIcon(true),
Build::alert('登录支付宝商家(地址https://b.alipay.com需要配置ip白名单以及回调地址回调地址' . $site_url . ')帮助文档地址https://doc.crmeb.com/web/pro/crmebprov2/1204', Alert::WARNING)->showIcon(true),
Build::input('ali_pay_appid', $data['ali_pay_appid']['info'], $data['ali_pay_appid']['value'])->info($data['ali_pay_appid']['desc']),
Build::input('alipay_public_key', $data['alipay_public_key']['info'], $data['alipay_public_key']['value'])->rows(5)->type('textarea')->info($data['alipay_public_key']['desc']),
Build::input('alipay_merchant_private_key', $data['alipay_merchant_private_key']['info'], $data['alipay_merchant_private_key']['value'])->rows(5)->type('textarea')->info($data['alipay_merchant_private_key']['desc']),

View File

@@ -14,7 +14,6 @@ namespace app\services\system\timer;
use app\services\BaseServices;
use crmeb\exceptions\AdminException;
use app\dao\system\timer\SystemTimerDao;
use app\listener\system\timer\SystemTimer as SystemTimerListener;
use think\annotation\Inject;
/**
@@ -51,7 +50,6 @@ class SystemTimerServices extends BaseServices
'auto_presale_product' => '预售商品到期处理数据',
'auto_card_code' => '清理到期礼品卡',
'holiday_gift_push_task'=>'节日有礼赠送礼品',
'fsgx_release_frozen_points' => 'fsgx每日积分释放',
];
/**
@@ -233,24 +231,6 @@ class SystemTimerServices extends BaseServices
return $timer->toArray();
}
/**
* 手动立即执行指定定时任务HTTP 请求上下文,绕过 Swoole Cron 构造,用反射直接调用 implement_timer
* @param string $mark
* @return mixed 任务返回值(如 executeRelease 返回的处理统计数组),可为 null
*/
public function runNow(string $mark): mixed
{
$this->update(['mark' => $mark], ['last_execution_time' => time()]);
try {
$ref = new \ReflectionClass(SystemTimerListener::class);
$instance = $ref->newInstanceWithoutConstructor();
return $instance->implement_timer($mark);
} catch (\Throwable $e) {
$taskName = $this->taskName[$mark] ?? $mark;
throw new \RuntimeException("定时任务[{$taskName}]执行失败: " . $e->getMessage(), 0, $e);
}
}
/**获取下次执行时间
* @param $type
* @param $cycle

View File

@@ -204,41 +204,6 @@ class UserBillServices extends BaseServices
'status' => 1,
'pm' => 1
],
// fsgx: 佣金转冻结积分
'frozen_points_brokerage' => [
'title' => '佣金奖励积分(待释放)',
'category' => 'integral',
'type' => 'frozen_points_brokerage',
'mark' => '获得待释放积分{%num%}',
'status' => 1,
'pm' => 1
],
// fsgx: 每日释放冻结积分
'frozen_points_release' => [
'title' => '积分每日释放',
'category' => 'integral',
'type' => 'frozen_points_release',
'mark' => '每日释放积分{%num%}',
'status' => 1,
'pm' => 1
],
// fsgx PRD §3.3:报单直推/伞下待释放积分明细(不计入可消费积分汇总)
'hjf_reward_direct_integral' => [
'title' => '直推积分奖励',
'category' => 'integral',
'type' => 'hjf_frozen_direct',
'mark' => '待释放积分{%num%}点',
'status' => 0,
'pm' => 1
],
'hjf_reward_umbrella_integral' => [
'title' => '伞下积分奖励',
'category' => 'integral',
'type' => 'hjf_frozen_umbrella',
'mark' => '待释放积分{%num%}点',
'status' => 0,
'pm' => 1
],
];
/**
@@ -583,10 +548,6 @@ class UserBillServices extends BaseServices
'integral_refund',
'order_integral_refund',
'pay_product_integral_back',
'frozen_points_brokerage',
'frozen_points_release',
'hjf_frozen_direct',
'hjf_frozen_umbrella',
]]));
$where_data['type'] = 'sign';
$data['CountSign'] = $this->dao->getUserSignPoint($where_data);

View File

@@ -770,21 +770,6 @@ class UserServices extends BaseServices
// 添加过滤条件
$where['is_filter_del'] = 1;
// HJF按分销等级 grade 筛选,转换为 agent_level ID 范围
if (isset($where['hjf_member_level']) && $where['hjf_member_level'] !== '') {
$grade = (int)$where['hjf_member_level'];
/** @var AgentLevelServices $agentLevelSvc */
$agentLevelSvc = app()->make(AgentLevelServices::class);
if ($grade > 0) {
$levelId = $agentLevelSvc->getLevelIdByGrade($grade);
$where['agent_level'] = $levelId > 0 ? $levelId : -1;
} else {
// grade=0 表示"无分销等级"
$where['agent_level'] = 0;
}
}
unset($where['hjf_member_level']);
/** @var UserWechatuserServices $userWechatUser */
$userWechatUser = app()->make(UserWechatuserServices::class);
$fields = 'u.*,w.country,w.province,w.city,w.sex,w.unionid,w.openid,w.user_type as w_user_type,w.groupid,w.tagid_list,w.subscribe,w.subscribe_time';
@@ -814,43 +799,6 @@ class UserServices extends BaseServices
$clientData = $workClientService->getList(['uid' => $uids], ['id', 'uid', 'name', 'external_userid', 'corp_id', 'unionid'], false);
$clientlist = $clientData['list'] ?? [];
/** HJF分销等级展示索引is_del=0按 id/grade 双索引,优先 HJF 官方等级名称) */
/** @var AgentLevelServices $agentLevelServices */
$agentLevelServices = app()->make(AgentLevelServices::class);
$hjfLevelMaps = $agentLevelServices->loadHjfUserListLevelMaps();
// 直推人数:统计 spread_uid 在 $uids 中的用户数
$directCountRaw = $this->dao->search(['spread_uid' => $uids])
->group('spread_uid')
->field('spread_uid, count(*) as cnt')
->select()->toArray();
$directCountMap = array_column($directCountRaw, 'cnt', 'spread_uid');
// 伞下订单数:先取 spread_uid 在 $uids 中的下级用户 uid → spread_uid 映射,再统计订单
$subUsersRaw = $this->dao->search(['spread_uid' => $uids])
->field('uid, spread_uid')
->select()->toArray();
// subUidToSpread: [sub_uid => spread_uid]
$subUidToSpread = array_column($subUsersRaw, 'spread_uid', 'uid');
$subUids = array_keys($subUidToSpread);
$umbrellaMap = [];
if ($subUids) {
$umbrellaOrdersRaw = \think\facade\Db::name('store_order')
->whereIn('uid', $subUids)
->where('paid', 1)
->where('pid', 0)
->whereIn('refund_status', [0, 3])
->field('uid, count(*) as cnt')
->group('uid')
->select()->toArray();
foreach ($umbrellaOrdersRaw as $row) {
$spreadUid = $subUidToSpread[$row['uid']] ?? null;
if ($spreadUid !== null) {
$umbrellaMap[$spreadUid] = ($umbrellaMap[$spreadUid] ?? 0) + (int)$row['cnt'];
}
}
}
// 补充信息
$extendInfo = SystemConfigService::get('user_extend_info', []);
$is_extend_info = false;
@@ -930,18 +878,6 @@ class UserServices extends BaseServices
$item['svip_over_day'] = 0;
}
// 分销等级HJF 扩展member_level=grade 数值member_level_name=等级名称)
$agentLevelId = (int)($item['agent_level'] ?? 0);
$hjfLevelInfo = $agentLevelServices->pickHjfLevelRowForUserListDisplay($agentLevelId, $hjfLevelMaps);
$item['member_level'] = $hjfLevelInfo ? (int)$hjfLevelInfo['grade'] : null;
$item['member_level_name'] = $hjfLevelInfo ? ($hjfLevelInfo['name'] ?? '') : '';
$item['available_points'] = (int)($item['available_points'] ?? 0);
$item['frozen_points'] = (int)($item['frozen_points'] ?? 0);
// 直推人数 & 伞下订单数
$item['direct_count'] = (int)($directCountMap[$item['uid']] ?? 0);
$item['umbrella_orders'] = (int)($umbrellaMap[$item['uid']] ?? 0);
// 标签
$item['labels'] = $userlabel[$item['uid']] ?? '';
@@ -2533,12 +2469,9 @@ class UserServices extends BaseServices
}
$spread_one_ids = $this->getUserSpredadUids($uid, 1);
$spread_two_ids = $this->getUserSpredadUids($uid, 2);
/** @var \app\services\hjf\MemberLevelServices $memberLevelServices */
$memberLevelServices = app()->make(\app\services\hjf\MemberLevelServices::class);
$data = [
'total' => count($spread_one_ids),
'totalLevel' => count($spread_two_ids),
'umbrellaCount' => $memberLevelServices->getUmbrellaMemberCount($uid),
'list' => []
];
/** @var UserStoreOrderServices $userStoreOrder */
@@ -2559,7 +2492,6 @@ class UserServices extends BaseServices
}
$data['list'] = $list;
$data['brokerage_level'] = (int)sys_config('brokerage_level', 2);
$data['umbrellaOrderCount'] = $memberLevelServices->getUmbrellaQueueOrderCount($uid);
$data['count'] = 0;
$data['price'] = 0;
$data['order_count'] = 0;

View File

@@ -12,7 +12,6 @@ declare (strict_types=1);
namespace app\services\user;
use app\services\agent\AgentLevelServices;
use app\services\BaseServices;
use app\dao\user\UserWechatUserDao;
use think\annotation\Inject;
@@ -49,7 +48,6 @@ class UserWechatuserServices extends BaseServices
*/
public function getWhereUserList(array $where, string $field): array
{
$where = $this->normalizeHjfMemberLevelWhere($where);
[$page, $limit] = $this->getPageValue();
$order_string = '';
$order_arr = ['asc', 'desc'];
@@ -60,40 +58,4 @@ class UserWechatuserServices extends BaseServices
$count = $this->dao->getCountByWhere($where);
return [$list, $count];
}
/**
* 将会员列表筛选「HJF 等级grade」转为 eb_user.agent_level 条件,供 UserWechatUserDao 使用。
*/
protected function normalizeHjfMemberLevelWhere(array $where): array
{
if (!array_key_exists('hjf_member_level', $where)) {
return $where;
}
$raw = $where['hjf_member_level'];
if ($raw === null) {
unset($where['hjf_member_level']);
return $where;
}
if (is_string($raw)) {
$raw = trim($raw);
}
// 空串/仅空白:不按分销等级筛选(避免 (int)' '=>0 误加 agent_level=0
if ($raw === '') {
unset($where['hjf_member_level']);
return $where;
}
$grade = (int)$raw;
/** @var AgentLevelServices $agentLevel */
$agentLevel = app()->make(AgentLevelServices::class);
if ($grade === 0) {
$where['hjf_agent_level_id'] = 0;
} else {
$where['hjf_agent_level_id'] = $agentLevel->getLevelIdByGrade($grade) ?: -1;
}
unset($where['hjf_member_level']);
return $where;
}
}

View File

@@ -35,10 +35,8 @@ return [
'text' => ''
],
'cache' => [
// file 存储:生产环境 Redis 密码错误时避免 ajcaptcha 报 NOAUTH业务主缓存仍用 config/cache.php
'constructor' => static function ($options) {
return app()->make(\think\Cache::class)->store('file');
},
//若您使用了框架并且想使用类似于redis这样的缓存驱动则应换成框架的中的缓存驱动
'constructor' => app()->make(\think\Cache::class),
'method' => [
//遵守PSR-16规范不需要设置此项tp6, laravel,hyperf。如tp5就不支持tp5缓存方法是rm,所以要配置为"delete" => "rm"
/**

View File

@@ -23,8 +23,5 @@ return [
'clear:cache' => \app\command\ClearCache::class,
'reset:password' => \app\command\ResetAdminPwd::class,
'holiday_gift_push_task' => \app\command\HolidayGiftPushTask::class,
'hjf:release-points' => \app\command\HjfReleasePoints::class,
'hjf:patch-rewards' => \app\command\HjfPatchMissingRewards::class,
'hjf:verify-agent-config' => \app\command\HjfVerifyAgentConfig::class,
],
];

View File

@@ -29,8 +29,8 @@ return [
'type' => env('DATABASE_TYPE', 'mysql'),
// 服务器地址
'hostname' => env('DATABASE_HOSTNAME', '127.0.0.1'),
// 数据库名(与 .env 中 [DATABASE] DATABASE= 一致键名database.database
'database' => env('database.database', 'fsgx-shop'),
// 数据库名
'database' => env('DATABASE_DATABASE', ''),
// 用户名
'username' => env('DATABASE_USERNAME', 'root'),
// 密码

View File

@@ -1,54 +0,0 @@
/*
Navicat Premium Dump SQL
Source Server : jxy-hjf-db
Source Server Type : MySQL
Source Server Version : 50740 (5.7.40-log)
Source Host : 182.92.142.158:3306
Source Schema : hjfshop
Target Server Type : MySQL
Target Server Version : 50740 (5.7.40-log)
File Encoding : 65001
Date: 21/03/2026 22:42:49
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for eb_agent_level
-- ----------------------------
DROP TABLE IF EXISTS `eb_agent_level`;
CREATE TABLE `eb_agent_level` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL DEFAULT '' COMMENT '等级名称',
`image` varchar(255) NOT NULL DEFAULT '' COMMENT '背景图',
`color` varchar(32) NOT NULL DEFAULT '' COMMENT ' 字体颜色',
`one_brokerage` smallint(5) NOT NULL DEFAULT '0' COMMENT '一级分拥比例',
`two_brokerage` smallint(5) NOT NULL DEFAULT '0' COMMENT '二级分拥比例',
`grade` smallint(5) NOT NULL DEFAULT '0' COMMENT '等级',
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态',
`is_del` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除',
`add_time` int(10) NOT NULL DEFAULT '0' COMMENT '添加时间',
`direct_reward_points` int(11) NOT NULL DEFAULT '0' COMMENT '直推奖励积分(每单)',
`umbrella_reward_points` int(11) NOT NULL DEFAULT '0' COMMENT '伞下奖励积分(每单,级差基数)',
PRIMARY KEY (`id`),
KEY `status` (`status`,`is_del`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COMMENT='分销员等级表';
-- ----------------------------
-- Records of eb_agent_level
-- ----------------------------
BEGIN;
INSERT INTO `eb_agent_level` (`id`, `name`, `image`, `color`, `one_brokerage`, `two_brokerage`, `grade`, `status`, `is_del`, `add_time`, `direct_reward_points`, `umbrella_reward_points`) VALUES (1, '创客', '/uploads/system/agent_level_1.png', '#D97E1D', 1, 0, 1, 1, 0, 1700126550, 500, 0);
INSERT INTO `eb_agent_level` (`id`, `name`, `image`, `color`, `one_brokerage`, `two_brokerage`, `grade`, `status`, `is_del`, `add_time`, `direct_reward_points`, `umbrella_reward_points`) VALUES (2, '云店', '/uploads/system/agent_level_2.png', '#5D7DAC', 5, 3, 2, 1, 0, 1700126572, 800, 300);
INSERT INTO `eb_agent_level` (`id`, `name`, `image`, `color`, `one_brokerage`, `two_brokerage`, `grade`, `status`, `is_del`, `add_time`, `direct_reward_points`, `umbrella_reward_points`) VALUES (3, '服务商', '/uploads/system/agent_level_3.png', '#5856D6', 10, 5, 3, 1, 0, 1700126595, 1000, 200);
INSERT INTO `eb_agent_level` (`id`, `name`, `image`, `color`, `one_brokerage`, `two_brokerage`, `grade`, `status`, `is_del`, `add_time`, `direct_reward_points`, `umbrella_reward_points`) VALUES (4, '分公司', '/uploads/system/agent_level_4.png', '#1DB0FC', 12, 7, 4, 1, 0, 1700126621, 1300, 300);
INSERT INTO `eb_agent_level` (`id`, `name`, `image`, `color`, `one_brokerage`, `two_brokerage`, `grade`, `status`, `is_del`, `add_time`, `direct_reward_points`, `umbrella_reward_points`) VALUES (5, '等级五', '/uploads/system/agent_level_5.png', '#AF52DE', 19, 12, 5, 0, 1, 1701764897, 0, 0);
INSERT INTO `eb_agent_level` (`id`, `name`, `image`, `color`, `one_brokerage`, `two_brokerage`, `grade`, `status`, `is_del`, `add_time`, `direct_reward_points`, `umbrella_reward_points`) VALUES (6, '服务商1', '', '#9C27B0', 0, 0, 3, 0, 1, 1774091023, 1000, 200);
INSERT INTO `eb_agent_level` (`id`, `name`, `image`, `color`, `one_brokerage`, `two_brokerage`, `grade`, `status`, `is_del`, `add_time`, `direct_reward_points`, `umbrella_reward_points`) VALUES (7, '分公司2', '', '#F44336', 0, 0, 4, 0, 1, 1774091023, 1300, 300);
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -1,42 +0,0 @@
/*
Navicat Premium Dump SQL
Source Server : jxy-hjf-db
Source Server Type : MySQL
Source Server Version : 50740 (5.7.40-log)
Source Host : 182.92.142.158:3306
Source Schema : hjfshop
Target Server Type : MySQL
Target Server Version : 50740 (5.7.40-log)
File Encoding : 65001
Date: 24/03/2026 11:17:55
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for eb_points_release_log
-- ----------------------------
DROP TABLE IF EXISTS `eb_points_release_log`;
CREATE TABLE `eb_points_release_log` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`uid` int(11) NOT NULL DEFAULT '0' COMMENT '用户 ID',
`points` int(11) NOT NULL DEFAULT '0' COMMENT '积分数量(绝对值)',
`pm` tinyint(1) NOT NULL DEFAULT '1' COMMENT '收支方向1=收入 0=支出',
`type` varchar(50) NOT NULL DEFAULT '' COMMENT '类型reward_direct/reward_umbrella/release/consume',
`title` varchar(255) NOT NULL DEFAULT '' COMMENT '标题',
`mark` varchar(500) NOT NULL DEFAULT '' COMMENT '备注',
`status` varchar(30) NOT NULL DEFAULT 'frozen' COMMENT '状态frozen=冻结 released=已释放 consumed=已消费',
`order_id` varchar(50) NOT NULL DEFAULT '' COMMENT '关联订单号(奖励来源),释放记录为空',
`release_date` date DEFAULT NULL COMMENT '释放日期(每日释放时填写)',
`add_time` int(11) NOT NULL DEFAULT '0' COMMENT '记录时间Unix 时间戳)',
PRIMARY KEY (`id`),
KEY `idx_uid_type` (`uid`,`type`),
KEY `idx_uid_add_time` (`uid`,`add_time`),
KEY `idx_release_date` (`release_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='积分释放明细日志';
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -1,397 +0,0 @@
-- ============================================================
-- 黄精粉健康商城 HJF 数据库迁移脚本
-- 版本Phase 3改造复用版
-- 日期2026-03-21
-- 执行说明:
-- 1. 兼容 MySQL 5.7+,数据库前缀为 eb_
-- 2. 按顺序执行 P3-01 ~ P3-07
-- 3. 所有操作均做幂等处理,可重复执行
-- 4. 遵循 PRD 改造复用原则:会员等级复用 eb_agent_level 体系,
-- 使用 eb_user.agent_level (FK) 代替独立的 member_level 字段
-- ============================================================
-- ============================================================
-- P3-01: 公排池表
-- ============================================================
CREATE TABLE IF NOT EXISTS `eb_queue_pool` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`uid` int(11) NOT NULL DEFAULT 0 COMMENT '用户 ID',
`order_id` varchar(50) NOT NULL DEFAULT '' COMMENT '来源订单号eb_store_order.order_id',
`amount` decimal(10,2) NOT NULL DEFAULT 3600.00 COMMENT '报单金额(元)',
`queue_no` int(11) NOT NULL DEFAULT 0 COMMENT '全局排队序号(自增,唯一)',
`status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '状态0=排队中 1=已退款',
`refund_time` int(11) NOT NULL DEFAULT 0 COMMENT '退款时间Unix 时间戳)',
`trigger_batch` int(11) NOT NULL DEFAULT 0 COMMENT '触发退款的批次号',
`add_time` int(11) NOT NULL DEFAULT 0 COMMENT '入队时间Unix 时间戳)',
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_queue_no` (`queue_no`),
INDEX `idx_uid` (`uid`),
INDEX `idx_status_add_time` (`status`, `add_time`),
INDEX `idx_trigger_batch` (`trigger_batch`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='公排池';
-- ============================================================
-- P3-02: 积分释放日志表
-- ============================================================
CREATE TABLE IF NOT EXISTS `eb_points_release_log` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`uid` int(11) NOT NULL DEFAULT 0 COMMENT '用户 ID',
`points` int(11) NOT NULL DEFAULT 0 COMMENT '积分数量(绝对值)',
`pm` tinyint(1) NOT NULL DEFAULT 1 COMMENT '收支方向1=收入 0=支出',
`type` varchar(50) NOT NULL DEFAULT '' COMMENT '类型reward_direct/reward_umbrella/release/consume',
`title` varchar(255) NOT NULL DEFAULT '' COMMENT '标题',
`mark` varchar(500) NOT NULL DEFAULT '' COMMENT '备注',
`status` varchar(30) NOT NULL DEFAULT 'frozen' COMMENT '状态frozen=冻结 released=已释放 consumed=已消费',
`order_id` varchar(50) NOT NULL DEFAULT '' COMMENT '关联订单号(奖励来源),释放记录为空',
`release_date` date DEFAULT NULL COMMENT '释放日期(每日释放时填写)',
`add_time` int(11) NOT NULL DEFAULT 0 COMMENT '记录时间Unix 时间戳)',
PRIMARY KEY (`id`),
INDEX `idx_uid_type` (`uid`, `type`),
INDEX `idx_uid_add_time` (`uid`, `add_time`),
INDEX `idx_release_date` (`release_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='积分释放明细日志';
-- ============================================================
-- P3-03: eb_agent_level 扩展字段(改造复用:增加积分奖励字段)
-- ============================================================
DROP PROCEDURE IF EXISTS `hjf_migrate_agent_level`;
DELIMITER $$
CREATE PROCEDURE `hjf_migrate_agent_level`()
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_agent_level' AND COLUMN_NAME = 'direct_reward_points'
) THEN
ALTER TABLE `eb_agent_level`
ADD COLUMN `direct_reward_points` int(11) NOT NULL DEFAULT 0 COMMENT '直推奖励积分(每单)';
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_agent_level' AND COLUMN_NAME = 'umbrella_reward_points'
) THEN
ALTER TABLE `eb_agent_level`
ADD COLUMN `umbrella_reward_points` int(11) NOT NULL DEFAULT 0 COMMENT '伞下奖励积分(每单,级差基数)';
END IF;
END$$
DELIMITER ;
CALL `hjf_migrate_agent_level`();
DROP PROCEDURE IF EXISTS `hjf_migrate_agent_level`;
-- ============================================================
-- P3-04: eb_user / eb_store_product / eb_store_order 扩展字段
--
-- 注意:不再新增 member_level 字段,复用已有的 agent_level (FK→eb_agent_level.id)
-- ============================================================
DROP PROCEDURE IF EXISTS `hjf_migrate_columns`;
DELIMITER $$
CREATE PROCEDURE `hjf_migrate_columns`()
BEGIN
-- ---- eb_user 字段(不含 member_level复用 agent_level----
IF NOT EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_user' AND COLUMN_NAME = 'no_assess'
) THEN
ALTER TABLE `eb_user`
ADD COLUMN `no_assess` tinyint(1) NOT NULL DEFAULT 0 COMMENT '不计入伞下业绩1=不计入';
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_user' AND COLUMN_NAME = 'frozen_points'
) THEN
ALTER TABLE `eb_user`
ADD COLUMN `frozen_points` int(11) NOT NULL DEFAULT 0 COMMENT '待释放(冻结)积分';
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_user' AND COLUMN_NAME = 'available_points'
) THEN
ALTER TABLE `eb_user`
ADD COLUMN `available_points` int(11) NOT NULL DEFAULT 0 COMMENT '可用积分';
END IF;
-- ---- eb_store_product 字段 ----
IF NOT EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_store_product' AND COLUMN_NAME = 'is_queue_goods'
) THEN
ALTER TABLE `eb_store_product`
ADD COLUMN `is_queue_goods` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否报单商品1=是';
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_store_product' AND COLUMN_NAME = 'allow_pay_types'
) THEN
ALTER TABLE `eb_store_product`
ADD COLUMN `allow_pay_types` varchar(255) NOT NULL DEFAULT '' COMMENT '允许积分支付类型JSON数组';
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_store_product' AND INDEX_NAME = 'idx_is_queue_goods'
) THEN
ALTER TABLE `eb_store_product` ADD INDEX `idx_is_queue_goods` (`is_queue_goods`);
END IF;
-- ---- eb_store_order 字段 ----
IF NOT EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_store_order' AND COLUMN_NAME = 'is_queue_goods'
) THEN
ALTER TABLE `eb_store_order`
ADD COLUMN `is_queue_goods` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否报单商品订单1=是';
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_store_order' AND INDEX_NAME = 'idx_is_queue_goods'
) THEN
ALTER TABLE `eb_store_order` ADD INDEX `idx_is_queue_goods` (`is_queue_goods`);
END IF;
END$$
DELIMITER ;
CALL `hjf_migrate_columns`();
DROP PROCEDURE IF EXISTS `hjf_migrate_columns`;
-- ============================================================
-- P3-05: 初始化会员等级数据到 eb_agent_level改造复用
--
-- 将原分销员等级改为五级会员等级体系:
-- grade=1 → 创客 (direct=500, umbrella=0)
-- grade=2 → 云店 (direct=800, umbrella=300)
-- grade=3 → 服务商 (direct=1000, umbrella=200)
-- grade=4 → 分公司 (direct=1300, umbrella=300)
--
-- 注意:普通会员 = agent_level=0无记录不需要插入
--
-- 先将 CRMEB 原有 demo 等级软删除,然后插入 HJF 会员等级
-- ============================================================
UPDATE `eb_agent_level`
SET `is_del` = 1
WHERE `name` NOT IN ('创客', '云店', '服务商', '分公司')
AND `is_del` = 0;
INSERT INTO `eb_agent_level`
(`name`, `grade`, `image`, `color`, `one_brokerage`, `two_brokerage`,
`direct_reward_points`, `umbrella_reward_points`, `status`, `is_del`, `add_time`)
SELECT '创客', 1, '', '#FF9800', 0, 0, 500, 0, 1, 0, UNIX_TIMESTAMP()
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `eb_agent_level` WHERE `name` = '创客' AND `is_del` = 0
);
INSERT INTO `eb_agent_level`
(`name`, `grade`, `image`, `color`, `one_brokerage`, `two_brokerage`,
`direct_reward_points`, `umbrella_reward_points`, `status`, `is_del`, `add_time`)
SELECT '云店', 2, '', '#2196F3', 0, 0, 800, 300, 1, 0, UNIX_TIMESTAMP()
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `eb_agent_level` WHERE `name` = '云店' AND `is_del` = 0
);
INSERT INTO `eb_agent_level`
(`name`, `grade`, `image`, `color`, `one_brokerage`, `two_brokerage`,
`direct_reward_points`, `umbrella_reward_points`, `status`, `is_del`, `add_time`)
SELECT '服务商', 3, '', '#9C27B0', 0, 0, 1000, 200, 1, 0, UNIX_TIMESTAMP()
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `eb_agent_level` WHERE `name` = '服务商' AND `is_del` = 0
);
INSERT INTO `eb_agent_level`
(`name`, `grade`, `image`, `color`, `one_brokerage`, `two_brokerage`,
`direct_reward_points`, `umbrella_reward_points`, `status`, `is_del`, `add_time`)
SELECT '分公司', 4, '', '#F44336', 0, 0, 1300, 300, 1, 0, UNIX_TIMESTAMP()
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `eb_agent_level` WHERE `name` = '分公司' AND `is_del` = 0
);
-- ============================================================
-- P3-06: 初始化等级升级任务到 eb_agent_level_task改造复用
--
-- 新增任务类型:
-- type=6 → 直推报单单数
-- type=7 → 伞下报单业绩(含业绩分离)
-- type=8 → 最低直推人数
--
-- 各等级任务配置:
-- 创客(grade=1): type=6, number=3 (直推3单)
-- 云店(grade=2): type=7, number=30 (伞下30单) + type=8, number=3 (至少3直推)
-- 服务商(grade=3): type=7, number=100 + type=8, number=3
-- 分公司(grade=4): type=7, number=1000 + type=8, number=3
-- ============================================================
DROP PROCEDURE IF EXISTS `hjf_init_agent_tasks`;
DELIMITER $$
CREATE PROCEDURE `hjf_init_agent_tasks`()
BEGIN
DECLARE v_level_id_1 INT DEFAULT 0;
DECLARE v_level_id_2 INT DEFAULT 0;
DECLARE v_level_id_3 INT DEFAULT 0;
DECLARE v_level_id_4 INT DEFAULT 0;
SELECT id INTO v_level_id_1 FROM eb_agent_level WHERE grade = 1 AND is_del = 0 LIMIT 1;
SELECT id INTO v_level_id_2 FROM eb_agent_level WHERE grade = 2 AND is_del = 0 LIMIT 1;
SELECT id INTO v_level_id_3 FROM eb_agent_level WHERE grade = 3 AND is_del = 0 LIMIT 1;
SELECT id INTO v_level_id_4 FROM eb_agent_level WHERE grade = 4 AND is_del = 0 LIMIT 1;
-- 创客直推报单3单
IF v_level_id_1 > 0 AND NOT EXISTS (
SELECT 1 FROM eb_agent_level_task WHERE level_id = v_level_id_1 AND type = 6 AND is_del = 0
) THEN
INSERT INTO eb_agent_level_task (level_id, `name`, type, number, `desc`, sort, status, is_del, add_time)
VALUES (v_level_id_1, '直推报单满3单', 6, 3, '直推下级购买报单商品满3单升级为创客', 1, 1, 0, UNIX_TIMESTAMP());
END IF;
-- 云店伞下报单30单 + 至少3个直推
IF v_level_id_2 > 0 THEN
IF NOT EXISTS (
SELECT 1 FROM eb_agent_level_task WHERE level_id = v_level_id_2 AND type = 7 AND is_del = 0
) THEN
INSERT INTO eb_agent_level_task (level_id, `name`, type, number, `desc`, sort, status, is_del, add_time)
VALUES (v_level_id_2, '伞下报单满30单', 7, 30, '伞下业绩含分离达到30单升级为云店', 1, 1, 0, UNIX_TIMESTAMP());
END IF;
IF NOT EXISTS (
SELECT 1 FROM eb_agent_level_task WHERE level_id = v_level_id_2 AND type = 8 AND is_del = 0
) THEN
INSERT INTO eb_agent_level_task (level_id, `name`, type, number, `desc`, sort, status, is_del, add_time)
VALUES (v_level_id_2, '至少3个直推', 8, 3, '需至少3个直推下级才可升级为云店', 2, 1, 0, UNIX_TIMESTAMP());
END IF;
END IF;
-- 服务商伞下报单100单 + 至少3个直推
IF v_level_id_3 > 0 THEN
IF NOT EXISTS (
SELECT 1 FROM eb_agent_level_task WHERE level_id = v_level_id_3 AND type = 7 AND is_del = 0
) THEN
INSERT INTO eb_agent_level_task (level_id, `name`, type, number, `desc`, sort, status, is_del, add_time)
VALUES (v_level_id_3, '伞下报单满100单', 7, 100, '伞下业绩含分离达到100单升级为服务商', 1, 1, 0, UNIX_TIMESTAMP());
END IF;
IF NOT EXISTS (
SELECT 1 FROM eb_agent_level_task WHERE level_id = v_level_id_3 AND type = 8 AND is_del = 0
) THEN
INSERT INTO eb_agent_level_task (level_id, `name`, type, number, `desc`, sort, status, is_del, add_time)
VALUES (v_level_id_3, '至少3个直推', 8, 3, '需至少3个直推下级才可升级为服务商', 2, 1, 0, UNIX_TIMESTAMP());
END IF;
END IF;
-- 分公司伞下报单1000单 + 至少3个直推
IF v_level_id_4 > 0 THEN
IF NOT EXISTS (
SELECT 1 FROM eb_agent_level_task WHERE level_id = v_level_id_4 AND type = 7 AND is_del = 0
) THEN
INSERT INTO eb_agent_level_task (level_id, `name`, type, number, `desc`, sort, status, is_del, add_time)
VALUES (v_level_id_4, '伞下报单满1000单', 7, 1000, '伞下业绩含分离达到1000单升级为分公司', 1, 1, 0, UNIX_TIMESTAMP());
END IF;
IF NOT EXISTS (
SELECT 1 FROM eb_agent_level_task WHERE level_id = v_level_id_4 AND type = 8 AND is_del = 0
) THEN
INSERT INTO eb_agent_level_task (level_id, `name`, type, number, `desc`, sort, status, is_del, add_time)
VALUES (v_level_id_4, '至少3个直推', 8, 3, '需至少3个直推下级才可升级为分公司', 2, 1, 0, UNIX_TIMESTAMP());
END IF;
END IF;
END$$
DELIMITER ;
CALL `hjf_init_agent_tasks`();
DROP PROCEDURE IF EXISTS `hjf_init_agent_tasks`;
-- ============================================================
-- P3-07: eb_system_config 初始化配置项
-- ============================================================
INSERT IGNORE INTO `eb_system_config`
(`is_store`, `menu_name`, `type`, `input_type`, `config_tab_id`,
`parameter`, `upload_type`, `required`, `width`, `high`,
`value`, `info`, `desc`, `sort`, `status`)
VALUES
(0, 'hjf_trigger_multiple', 'text', 'input', 0,
'', 0, '', 100, 0,
'4', '公排触发倍数', '每进入N单公排触发退款第1单默认4', 10, 1),
(0, 'hjf_release_rate', 'text', 'input', 0,
'', 0, '', 100, 0,
'4', '积分每日释放比例(‰)', '每日释放frozen_points × N / 1000默认4即4‰', 20, 1),
(0, 'hjf_fee_rate', 'text', 'input', 0,
'', 0, '', 100, 0,
'7', '提现手续费率(%)', '申请提现时收取的手续费比例默认7%', 30, 1),
(0, 'hjf_queue_pool_enable', 'text', 'input', 0,
'', 0, '', 100, 0,
'0', '公排入队开关', '0=关闭(积分同步发放) 1=开启(通过队列异步处理)', 5, 1);
-- ============================================================
-- P3-08: 如果已有旧的 member_level 字段,将数据迁移到 agent_level
-- ============================================================
DROP PROCEDURE IF EXISTS `hjf_migrate_member_to_agent_level`;
DELIMITER $$
CREATE PROCEDURE `hjf_migrate_member_to_agent_level`()
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_user' AND COLUMN_NAME = 'member_level'
) THEN
-- 将 member_level 数值映射到 agent_level (FK → eb_agent_level.id)
UPDATE eb_user u
INNER JOIN eb_agent_level al ON al.grade = u.member_level AND al.is_del = 0
SET u.agent_level = al.id
WHERE u.member_level > 0 AND (u.agent_level = 0 OR u.agent_level IS NULL);
END IF;
END$$
DELIMITER ;
CALL `hjf_migrate_member_to_agent_level`();
DROP PROCEDURE IF EXISTS `hjf_migrate_member_to_agent_level`;
-- ============================================================
-- 迁移完成校验(可手动执行检查)
-- ============================================================
-- SELECT id, name, grade, direct_reward_points, umbrella_reward_points
-- FROM eb_agent_level WHERE is_del = 0 ORDER BY grade;
-- SELECT alt.id, al.name AS level_name, alt.type, alt.number, alt.name AS task_name
-- FROM eb_agent_level_task alt
-- JOIN eb_agent_level al ON al.id = alt.level_id
-- WHERE alt.is_del = 0 AND al.is_del = 0
-- ORDER BY al.grade, alt.type;
-- SELECT COLUMN_NAME FROM information_schema.COLUMNS
-- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_user'
-- AND COLUMN_NAME IN ('no_assess','frozen_points','available_points');
-- SELECT menu_name, value FROM eb_system_config
-- WHERE menu_name LIKE 'hjf_%' ORDER BY sort;

File diff suppressed because it is too large Load Diff

View File

@@ -1,95 +0,0 @@
-- ============================================================
-- 范氏国香商城 fsgx 改造数据库迁移脚本
-- 执行顺序:按 Step 编号依次执行
-- ============================================================
-- Step 1: eb_store_product 新增报单商品标记字段
-- 注MySQL 5.7 不支持 ADD COLUMN IF NOT EXISTS重复执行会报错已存在时跳过即可
ALTER TABLE `eb_store_product`
ADD COLUMN `is_queue_goods` tinyint(1) NOT NULL DEFAULT 0 COMMENT '报单商品:1=是,0=否' AFTER `is_brokerage`;
-- Step 1b: eb_store_order 新增报单商品标记(冗余存储,加速佣金周期计数)
ALTER TABLE `eb_store_order`
ADD COLUMN `is_queue_goods` tinyint(1) NOT NULL DEFAULT 0 COMMENT '报单商品订单:1=是,0=否' AFTER `spread_two_uid`;
-- Step 2: eb_user 新增积分字段与不考核字段
ALTER TABLE `eb_user`
ADD COLUMN `frozen_points` int(11) NOT NULL DEFAULT 0 COMMENT '待释放积分' AFTER `integral`,
ADD COLUMN `available_points` int(11) NOT NULL DEFAULT 0 COMMENT '已释放积分' AFTER `frozen_points`,
ADD COLUMN `no_assess` tinyint(1) NOT NULL DEFAULT 0 COMMENT '不考核:1=是,0=否' AFTER `available_points`;
-- Step 2b: eb_agent_level 新增直推/伞下奖励积分字段 + 返佣比例展示字段
ALTER TABLE `eb_agent_level`
ADD COLUMN `direct_reward_points` int(11) NOT NULL DEFAULT 0 COMMENT '直推奖励积分' AFTER `two_brokerage`,
ADD COLUMN `umbrella_reward_points` int(11) NOT NULL DEFAULT 0 COMMENT '伞下奖励积分' AFTER `direct_reward_points`,
ADD COLUMN `one_brokerage_ratio` decimal(5,2) NOT NULL DEFAULT 0 COMMENT '一级返佣比例(上浮后)' AFTER `umbrella_reward_points`,
ADD COLUMN `two_brokerage_ratio` decimal(5,2) NOT NULL DEFAULT 0 COMMENT '二级返佣比例(上浮后)' AFTER `one_brokerage_ratio`;
-- Step 3: eb_system_timer 新增每日积分释放定时任务
-- type=4 表示"每天"cycle 格式为"小时/分钟"(如 2/0 = 凌晨2点整
-- 表中无 status 字段is_open 控制是否启用mark 无唯一索引,先删再插保证幂等
DELETE FROM `eb_system_timer` WHERE `mark` = 'fsgx_release_frozen_points';
INSERT INTO `eb_system_timer` (`name`, `title`, `mark`, `type`, `cycle`, `is_open`, `add_time`)
VALUES ('fsgx每日积分释放', 'fsgx每日释放待释放积分0.4‰转入可用积分)', 'fsgx_release_frozen_points', 4, '2/0', 1, UNIX_TIMESTAMP());
-- Step 4: eb_system_config 新增返佣周期配置键和提现手续费
-- 实际表字段value非 config_value、config_tab_id非 group_iddesc 为保留字需加反引号
-- 使用 DELETE+INSERT 保证幂等
DELETE FROM `eb_system_config` WHERE `menu_name` IN ('brokerage_cycle_count','brokerage_cycle_rates','brokerage_scope','brokerage_timing','extract_fee');
-- value 字段存储 JSON 编码后的值(与 save_basics 的 json_encode 行为一致)
-- 字符串类型:用双引号括起来,如 "queue_only";数字直接写;数组写 JSON 数组
INSERT INTO `eb_system_config` (`menu_name`, `info`, `config_tab_id`, `type`, `input_type`, `value`, `desc`, `sort`, `status`)
VALUES
('brokerage_cycle_count', '佣金周期人数', 0, 'text', 'input', '3', '推荐N人为一个周期循环计算各档佣金比例', 10, 1),
('brokerage_cycle_rates', '佣金分档比例(JSON)', 0, 'text', 'input', '[20,30,50]', '各档佣金比例JSON数组如[20,30,50]表示20%/30%/50%', 9, 1),
('brokerage_scope', '返佣范围', 0, 'text', 'input', '"queue_only"', '返佣范围all=所有商品 queue_only=仅报单商品', 8, 1),
('brokerage_timing', '佣金发放时机', 0, 'text', 'input', '"on_pay"', '发放时机on_pay=支付即发 on_confirm=确认收货后', 7, 1),
('extract_fee', '提现手续费率(%)', 0, 'text', 'input', '7', '提现时扣除的手续费百分比默认7%', 6, 1);
-- Step 5: 新建公排池表和积分释放日志表
CREATE TABLE IF NOT EXISTS `eb_queue_pool` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` int(11) NOT NULL DEFAULT 0,
`order_id` varchar(50) NOT NULL DEFAULT '',
`amount` decimal(10,2) NOT NULL DEFAULT 3600.00,
`queue_no` int(11) NOT NULL DEFAULT 0,
`status` tinyint(1) NOT NULL DEFAULT 0,
`refund_time` int(11) NOT NULL DEFAULT 0,
`trigger_batch` int(11) NOT NULL DEFAULT 0,
`add_time` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_queue_no` (`queue_no`),
INDEX `idx_uid` (`uid`),
INDEX `idx_status_add_time` (`status`, `add_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='公排池';
CREATE TABLE IF NOT EXISTS `eb_points_release_log` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` int(11) NOT NULL DEFAULT 0,
`points` int(11) NOT NULL DEFAULT 0,
`pm` tinyint(1) NOT NULL DEFAULT 1,
`type` varchar(50) NOT NULL DEFAULT '',
`title` varchar(255) NOT NULL DEFAULT '',
`mark` varchar(500) NOT NULL DEFAULT '',
`status` varchar(30) NOT NULL DEFAULT 'frozen',
`order_id` varchar(50) NOT NULL DEFAULT '',
`release_date` date DEFAULT NULL,
`add_time` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
INDEX `idx_uid_type` (`uid`, `type`),
INDEX `idx_uid_add_time` (`uid`, `add_time`),
INDEX `idx_release_date` (`release_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='积分释放明细日志';
-- Step 6: 更新分销等级升级任务配置(对齐验收清单)
DELETE FROM `eb_agent_level_task`;
INSERT INTO `eb_agent_level_task` (`level_id`, `name`, `type`, `number`, `is_must`, `sort`, `status`, `is_del`, `add_time`)
VALUES
(1, '直推人数>=1人', 8, 1, 1, 1, 1, 0, UNIX_TIMESTAMP()),
(2, '直推人数>=3人', 8, 3, 1, 1, 1, 0, UNIX_TIMESTAMP()),
(3, '直推人数>=10人', 8, 10, 1, 1, 1, 0, UNIX_TIMESTAMP()),
(3, '伞下队列订单>=30', 7, 30, 1, 2, 1, 0, UNIX_TIMESTAMP()),
(4, '直推人数>=30人', 8, 30, 1, 1, 1, 0, UNIX_TIMESTAMP()),
(4, '伞下队列订单>=100',7, 100, 1, 2, 1, 0, UNIX_TIMESTAMP());

View File

@@ -4,6 +4,4 @@
set -e
cd "$(dirname "$0")/.."
# 使用 PHP 8.0Swoole Loader 仅支持 8.0
PHP_BIN="${PHP_BIN:-/usr/local/opt/php@8.0/bin/php}"
"$PHP_BIN" -d memory_limit=300M think swoole
php -d memory_limit=300M think swoole

View File

@@ -1,6 +0,0 @@
; 供 PHP 8.0 加载 Swoole Loader在 conf.d 中软链或复制此文件,或 php.ini 中 include 此路径)
; 使用方式(任选其一):
; 1. 复制到 PHP 8.0 conf.dcp help/swoole-loader-php80.ini /usr/local/etc/php/8.0/conf.d/99-swoole-loader.ini
; 并确保 extension= 指向的 .so 路径存在(如下方为项目内绝对路径,需按本机修改)。
; 2. 或将 .so 复制到 PHP 8.0 的 extension_dir 后改为extension = swoole_loader_80_nts.so
extension = /Users/apple/scott2026/huangjingfen/pro_v3.5.1/help/swoole_loader_mac/swoole_loader_80_nts.so

Some files were not shown because too many files have changed in this diff Show More