Compare commits
1 Commits
feature/fs
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd33eeb21a |
@@ -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` 数据配置) |
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 295 KiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 606 KiB |
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,315 +0,0 @@
|
||||
# 范氏国香商城小程序 · 产品需求文档(PRD)V1.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=1,type=get_brokerage,title=推广佣金
|
||||
|
||||
|
||||
#### 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=0,type=integral,title=直推积分奖励 | 伞下积分奖励
|
||||
|
||||
### 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(优化阶段):风控规则、运营分析看板深化、批量重算工具
|
||||
@@ -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"
|
||||
@@ -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 节"
|
||||
233
docs/deploy.md
@@ -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 上传(本地)
|
||||
|
||||
**方式 A:rsync**(服务器需已安装 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/
|
||||
```
|
||||
|
||||
**方式 B:tar + 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)
|
||||
|
||||
**方式 A:HBuilder 打包(推荐)**
|
||||
|
||||
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 上传(本地)
|
||||
|
||||
**方式 A:rsync**(服务器需已安装 rsync)
|
||||
|
||||
```bash
|
||||
cd /Users/apple/scott2026/huangjingfen
|
||||
|
||||
@@ -314,122 +212,9 @@ rsync -avz \
|
||||
|
||||
> 不使用 `--delete`,避免覆盖或删除 `admin/`、`index.php` 等文件。
|
||||
|
||||
**方式 B:tar + 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/`。
|
||||
|
||||
**方式 C:scp**
|
||||
|
||||
```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 时服务器返回了 HTML(404 页或 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
|
||||
```
|
||||
|
||||
@@ -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项)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、执行方案
|
||||
|
||||
### 阶段 A:Phase 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-02:Admin 商品编辑-报单标记与支付方式
|
||||
|
||||
**文件**:`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. 编辑回显时正确反填两个字段。
|
||||
|
||||
**验收标准**:新建/编辑商品可设置报单标记;报单商品自动禁用积分支付选项。
|
||||
|
||||
---
|
||||
|
||||
### 阶段 B:Phase 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)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 阶段 C:Phase 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) |
|
||||
|
||||
---
|
||||
|
||||
### 阶段 D:Phase 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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 阶段 E:Phase 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
|
||||
```
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
# 测试问题
|
||||
|
||||
## 分销员的**直推积分奖励**问题1
|
||||
- **已检查**当前用户UID=30推荐UID=34推荐UID=31,现在UID=31购买的报单商品订单,
|
||||
检查UID=30创客是否有直推积分奖励,如果有是不对的,原因:UID=34,UID=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
|
||||
|
||||
- **已修复****重大调整**: **直推积分奖励**落库的时候,上级的分销等级是创客的时候检查必须是直推关系才发放积分奖励, 如果分销员级别大于创客则不需要是直推关系,只要是伞下关系就可以获得级差的**积分奖励**(级差的积分奖励同样更新到“直推积分奖励”的表字段中)。
|
||||
|
||||
- **已修复**范氏国香(mysql:47.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页面一样的样式,同时美化页面其他部分的样式
|
||||
@@ -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),每条单份金额,逐条触发退款检测
|
||||
- **已修复**周期佣金位次统计改为按报单商品总件数(而非订单数),确保跨订单轮巡位次连续
|
||||
@@ -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 周期佣金)
|
||||
|
||||
路径:**营销 → 分销 → 返佣设置 → 推荐佣金(fsgx)Tab**
|
||||
|
||||
| 配置项 | 推荐值 | 说明 |
|
||||
|--------------|----------------|----------------------------------|
|
||||
| 佣金周期人数 | `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 改造即告完成,可正式上线运营。*
|
||||
@@ -1,63 +0,0 @@
|
||||
# 管理后台
|
||||
|
||||
## 编辑商品页面,路径:admin/product/add_product?id=18&from_page=1
|
||||
|
||||
1. **已修复**是否报单商品:1=是,修改保存后没有更新数据库中的值
|
||||
|
||||
## 返佣设置页面,路径:/admin/setting/system_config_rake_back
|
||||
|
||||
1. **已修复**”返佣范围、佣金发放时机“修改后保存不落库(数据库没有修改)
|
||||
2. **已修复**推荐佣金(fsgx)tab页中”返佣范围、佣金发放时机“没有显示选中项
|
||||
|
||||
## 用户列表页面,路径: /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. 测试账号:UID:1, 手机号:1860001111; UID:2, 手机号:18621813282 ;UID:3, 手机号:17887996868 ;
|
||||
UID:4, 手机号:15324401259;UID:5, 手机号:17887996868; UID:6, 手机号:15821676725; 测试账号密码默认:A123456
|
||||
2. 推荐关系: uid=2推荐uid=4,uid=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=0,type=integral,title=直推积分奖励 | 伞下积分奖励
|
||||
5. **已修复**佣金记录页面/admin/finance/finance/commission中用户返现佣金记录详情中看不到返现佣金明细
|
||||
6. **相关文件**:`docs/PRD_fsgx_V1.0.md` `docs/page-dev-specs-fsgx.md`,
|
||||
|
||||
@@ -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`,
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.json,custom 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/D,A 获 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` | 保留,或设为 0(fsgx 无二级返佣需求) |
|
||||
| 新增配置 | — | `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`(佣金收益),或保留并重定向 |
|
||||
@@ -1,289 +0,0 @@
|
||||
---
|
||||
name: fsgx development tasks
|
||||
overview: 基于 page-dev-specs-fsgx.md(V2 分销模块复用方案),将全部开发工作拆解为 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 全链路验收测试
|
||||
|
||||
- 注册→绑定推荐关系→购买报单商品→佣金计算→佣金发放→积分奖励→提现
|
||||
- 分销等级自动升级验证
|
||||
- 佣金周期进度正确性验证
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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. **非功能性需求**
|
||||
|
||||
@@ -322,7 +322,7 @@ php think swoole
|
||||
|
||||
5. 后台登录:
|
||||
http://域名/admin
|
||||
默认账号:admin 密码:A@123456 或 A123456
|
||||
默认账号:admin 密码:A@123456
|
||||
|
||||
|
||||
## 启动命令
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
|
||||
@@ -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 ? '已设置不考核' : '已取消不考核');
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
* 积分释放日志(来自 UserBill,type = integral,mark 包含"积分")
|
||||
*/
|
||||
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]);
|
||||
}
|
||||
}
|
||||
@@ -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('保存成功');
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -85,8 +85,6 @@ class User extends AuthController
|
||||
['isMember', ''],
|
||||
['label_ids', ''],
|
||||
['is_channel', ''],
|
||||
/** HJF:按分销等级 grade(0–4)筛选,对应 eb_user.agent_level */
|
||||
['hjf_member_level', ''],
|
||||
]);
|
||||
if ($where['label_ids']) {
|
||||
$where['label_id'] = stringToIntArray($where['label_ids']);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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, '.', ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 ? '已退款' : '排队中';
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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 获取等级 grade(HJF 会员等级数字 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户当前会员等级 grade(0=普通, 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})");
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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} 轮";
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
//计算获取二级返佣比例
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
//订单中取出
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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']),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
];
|
||||
|
||||
@@ -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'),
|
||||
// 密码
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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_id),desc 为保留字需加反引号
|
||||
-- 使用 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());
|
||||
@@ -4,6 +4,4 @@
|
||||
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
# 使用 PHP 8.0(Swoole 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
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
; 供 PHP 8.0 加载 Swoole Loader(在 conf.d 中软链或复制此文件,或 php.ini 中 include 此路径)
|
||||
; 使用方式(任选其一):
|
||||
; 1. 复制到 PHP 8.0 conf.d:cp 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
|
||||