Merge branch 'feature/fsgx' into queue
This commit is contained in:
327
.cursor/plans/fix_issues_0325-1_f8488785.plan.md
Normal file
327
.cursor/plans/fix_issues_0325-1_f8488785.plan.md
Normal file
@@ -0,0 +1,327 @@
|
||||
---
|
||||
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` 数据配置) |
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
| 已释放积分 | 已完成释放、可消费的积分,仅可用于普通商品 |
|
||||
| 直推 | 用户直接邀请并绑定的一级成员 |
|
||||
| 伞下 | 用户的所有直推及其下级组成的推荐网络 |
|
||||
| 会员分销等级 | 创客、云店、服务商、分公司 |
|
||||
| 会员分销等级 | 创客、云店、服务中心、合伙人 |
|
||||
| 级差 | 上级可获得的奖励与下级当前等级奖励之间的差额机制 |
|
||||
| 推荐返现循环 | 邀请满 3 人按 20%/30%/50%返现,后续继续按 3 人周期循环 |
|
||||
|
||||
|
||||
@@ -1,21 +1,37 @@
|
||||
# 管理后台
|
||||
|
||||
# uniapp移动端
|
||||
|
||||
## 我的页面(tabbar页)
|
||||
1. 用户ID右边显示的会员等级改成显示分销等级
|
||||
## 会员码页面(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/assets/index)
|
||||
1. 页面顶部“我的资产”的标题去除
|
||||
## 分销海报页面(pages/users/user_spread_code/index)
|
||||
- 1. 海报加载不出来
|
||||
|
||||
## 推荐佣金页面(/pages/queue/status)
|
||||
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. **相关文件**:`docs/PRD_fsgx_V1.0.md` `docs/page-dev-specs-fsgx.md`,
|
||||
|
||||
|
||||
|
||||
37
docs/issues-0327-1.md
Normal file
37
docs/issues-0327-1.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# 核心功能测试结果
|
||||
|
||||
- 分销员直推没有获得返现佣金,测试数据: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`
|
||||
|
||||
170
pro_v3.5.1/app/command/HjfVerifyAgentConfig.php
Normal file
170
pro_v3.5.1/app/command/HjfVerifyAgentConfig.php
Normal file
@@ -0,0 +1,170 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -39,14 +39,17 @@ class HjfAssets
|
||||
$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' => (int)($user['available_points'] ?? 0),
|
||||
'today_release' => $todayRelease,
|
||||
'agent_level' => (int)($user['agent_level'] ?? 0),
|
||||
'agent_level_name' => $agentLevelName,
|
||||
'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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,10 @@ 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]);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ class HjfOrderPayJob extends BaseJobs
|
||||
return false;
|
||||
}
|
||||
|
||||
$preUpgradeLevels = [];
|
||||
try {
|
||||
/** @var UserServices $userServices */
|
||||
$userServices = app()->make(UserServices::class);
|
||||
@@ -59,6 +60,13 @@ class HjfOrderPayJob extends BaseJobs
|
||||
}
|
||||
$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);
|
||||
@@ -76,10 +84,30 @@ class HjfOrderPayJob extends BaseJobs
|
||||
->field('id,uid,is_queue_goods')
|
||||
->find();
|
||||
if ($orderRow) {
|
||||
// fsgx B3:计算订单中报单商品的总数量,积分按数量倍乘
|
||||
$queueQty = 1;
|
||||
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());
|
||||
}
|
||||
|
||||
/** @var PointsRewardServices $pointsService */
|
||||
$pointsService = app()->make(PointsRewardServices::class);
|
||||
$pointsService->reward($uid, $orderId, (int)$orderRow['id']);
|
||||
Log::info("[HjfOrderPay] 积分奖励发放完成 uid={$uid} orderId={$orderId}");
|
||||
$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());
|
||||
|
||||
@@ -175,15 +175,42 @@ class Pay implements ListenerInterface
|
||||
}
|
||||
$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']);
|
||||
$pointsService->reward($uid, (string)$orderInfo['order_id'], (int)$orderInfo['id'], $preUpgradeLevels, $queueQty);
|
||||
|
||||
Log::info('[Pay] 同步积分奖励发放完成 uid=' . $uid . ' order_id=' . $orderInfo['id']);
|
||||
Log::info('[Pay] 同步积分奖励发放完成 uid=' . $uid . ' order_id=' . $orderInfo['id'] . ' qty=' . $queueQty);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('[Pay] 同步积分奖励失败 order_id=' . $orderInfo['id'] . ': ' . $e->getMessage());
|
||||
}
|
||||
|
||||
@@ -437,11 +437,13 @@ class AgentLevelTaskServices extends BaseServices
|
||||
if (empty($directUids)) {
|
||||
return 0;
|
||||
}
|
||||
// fsgx B5:补充 refund_status 检查,与其他任务类型保持一致,排除已全额退款订单
|
||||
return (int)Db::name('store_order')
|
||||
->whereIn('uid', $directUids)
|
||||
->where('is_queue_goods', 1)
|
||||
->where('paid', 1)
|
||||
->where('is_del', 0)
|
||||
->whereIn('refund_status', [0, 3])
|
||||
->count();
|
||||
}
|
||||
|
||||
@@ -490,11 +492,13 @@ class AgentLevelTaskServices extends BaseServices
|
||||
if ($childGrade >= 2) {
|
||||
continue;
|
||||
}
|
||||
// fsgx B5:补充 refund_status 检查,排除已全额退款订单
|
||||
$total += (int)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])
|
||||
->count();
|
||||
$total += $this->recursiveUmbrellaCount((int)$child['uid'], $remainDepth - 1);
|
||||
}
|
||||
|
||||
@@ -40,10 +40,13 @@ class PointsRewardServices extends BaseServices
|
||||
/**
|
||||
* 对一笔报单订单发放积分奖励
|
||||
*
|
||||
* @param int $orderDbId 订单表主键 id,用于 user_bill.link_id 关联后台订单
|
||||
* @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): void
|
||||
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')
|
||||
@@ -59,7 +62,7 @@ class PointsRewardServices extends BaseServices
|
||||
if (!$buyer || !$buyer['spread_uid']) {
|
||||
return;
|
||||
}
|
||||
$this->propagateReward($buyer['spread_uid'], $orderUid, $orderId, 0, 0, $orderDbId);
|
||||
$this->propagateReward($buyer['spread_uid'], $orderUid, $orderId, 0, 0, $orderDbId, $preUpgradeLevels, $qty);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error("[PointsReward] 积分奖励失败 orderUid={$orderUid} orderId={$orderId}: " . $e->getMessage());
|
||||
}
|
||||
@@ -68,11 +71,13 @@ class PointsRewardServices extends BaseServices
|
||||
/**
|
||||
* 向上递归发放级差积分
|
||||
*
|
||||
* @param int $uid 当前被奖励用户
|
||||
* @param int $fromUid 触发方(下级)用户 ID
|
||||
* @param string $orderId 来源订单号
|
||||
* @param int $lowerReward 下级已获得的直推/伞下奖励积分(用于级差扣减)
|
||||
* @param int $depth 递归深度
|
||||
* @param int $uid 当前被奖励用户
|
||||
* @param int $fromUid 触发方(下级)用户 ID
|
||||
* @param string $orderId 来源订单号
|
||||
* @param int $lowerReward 下级已获得的伞下奖励积分(用于级差扣减)
|
||||
* @param int $depth 递归深度
|
||||
* @param array $preUpgradeLevels 升级前各用户的 agent_level 快照 [uid => level_id]
|
||||
* @param int $qty 报单商品数量,积分按数量倍乘(B3)
|
||||
*/
|
||||
private function propagateReward(
|
||||
int $uid,
|
||||
@@ -80,7 +85,9 @@ class PointsRewardServices extends BaseServices
|
||||
string $orderId,
|
||||
int $lowerReward,
|
||||
int $depth = 0,
|
||||
int $orderDbId = 0
|
||||
int $orderDbId = 0,
|
||||
array $preUpgradeLevels = [],
|
||||
int $qty = 1
|
||||
): void {
|
||||
if ($depth >= 10 || $uid <= 0) {
|
||||
return;
|
||||
@@ -91,22 +98,33 @@ class PointsRewardServices extends BaseServices
|
||||
return;
|
||||
}
|
||||
|
||||
$agentLevelId = (int)($user['agent_level'] ?? 0);
|
||||
// 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) {
|
||||
if ($user['spread_uid']) {
|
||||
$this->propagateReward((int)$user['spread_uid'], $uid, $orderId, 0, $depth + 1, $orderDbId);
|
||||
$this->propagateReward((int)$user['spread_uid'], $uid, $orderId, 0, $depth + 1, $orderDbId, $preUpgradeLevels, $qty);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$isDirect = ($depth === 0);
|
||||
$reward = $isDirect
|
||||
? $this->agentLevelServices->getDirectRewardPoints($agentLevelId)
|
||||
: $this->agentLevelServices->getUmbrellaRewardPoints($agentLevelId);
|
||||
// fsgx B4:直推奖励和伞下奖励分别读取,级差计算只用伞下奖励做扣减
|
||||
// 这样当中间层(如创客,umbrella=0)上报时,上级(云店,umbrella=300)
|
||||
// 计算 max(0, 300-0)=300,而不会被直推奖励 500 错误抵消
|
||||
$directReward = $this->agentLevelServices->getDirectRewardPoints($agentLevelId);
|
||||
$umbrellaReward = $this->agentLevelServices->getUmbrellaRewardPoints($agentLevelId);
|
||||
|
||||
$actual = max(0, $reward - $lowerReward);
|
||||
if ($isDirect) {
|
||||
// fsgx B3:直推奖励按报单商品数量倍乘
|
||||
$actual = $directReward * $qty;
|
||||
} else {
|
||||
// 级差:基于单件奖励做差值,再乘以数量
|
||||
$actual = max(0, $umbrellaReward - $lowerReward) * $qty;
|
||||
}
|
||||
|
||||
if ($actual > 0) {
|
||||
$this->grantFrozenPoints(
|
||||
@@ -114,7 +132,7 @@ class PointsRewardServices extends BaseServices
|
||||
$actual,
|
||||
$orderId,
|
||||
$isDirect ? 'reward_direct' : 'reward_umbrella',
|
||||
($isDirect ? '直推奖励' : '伞下奖励(级差)') . " - 来源订单 {$orderId}",
|
||||
($isDirect ? '直推奖励' : '伞下奖励(级差)') . " x{$qty} - 来源订单 {$orderId}",
|
||||
$orderDbId
|
||||
);
|
||||
}
|
||||
@@ -124,9 +142,11 @@ class PointsRewardServices extends BaseServices
|
||||
(int)$user['spread_uid'],
|
||||
$uid,
|
||||
$orderId,
|
||||
$reward,
|
||||
$umbrellaReward, // 向上传单件伞下奖励(级差基数),让上级自行乘以 $qty
|
||||
$depth + 1,
|
||||
$orderDbId
|
||||
$orderDbId,
|
||||
$preUpgradeLevels,
|
||||
$qty
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1002,22 +1002,42 @@ class StoreOrderCreateServices extends BaseServices
|
||||
$useCycleBrokerage = ($cycleCount > 0 && is_array($cycleRates) && count($cycleRates) > 0 && $isQueueGoods === 1);
|
||||
|
||||
if ($useCycleBrokerage && $spread_uid > 0) {
|
||||
// 统计推荐人下级已完成的有效报单商品订单数,取模得到当前位次
|
||||
// 注意:compute() 在 paid=1 之后执行,当前订单已被计入,需 -1 得到"之前完成单数"
|
||||
/** @var \app\dao\order\StoreOrderDao $orderDao */
|
||||
$orderDao = app()->make(\app\dao\order\StoreOrderDao::class);
|
||||
$completedCount = $orderDao->count([
|
||||
'spread_uid' => $spread_uid,
|
||||
'is_queue_goods' => 1,
|
||||
'paid' => 1,
|
||||
'is_del' => 0,
|
||||
]);
|
||||
$position = max(0, $completedCount - 1) % $cycleCount;
|
||||
$cycleRatePercent = isset($cycleRates[$position]) ? (int)$cycleRates[$position] : (int)($cycleRates[0] ?? 0);
|
||||
if ($cycleRatePercent > 0) {
|
||||
$brokerageRatio = bcdiv((string)$cycleRatePercent, 100, 4);
|
||||
$oneBrokerage = bcmul((string)$price, (string)$brokerageRatio, 2);
|
||||
}
|
||||
// fsgx B1 + B6:用事务锁序列化位次计算,防止并发竞态导致两笔订单拿到相同位次
|
||||
$brokerageResult = \think\facade\Db::transaction(function () use ($spread_uid, $cycleCount, $cycleRates, $price) {
|
||||
// 锁定推荐人行,确保同一推荐人同时只有一个事务在计算位次
|
||||
\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:按 id ASC 排序取位次,比 count 更精确且在锁保护下无竞态
|
||||
// 当前订单在 paid=1 后已写入,这里取所有已完成订单并按序找到本单排名
|
||||
$completedCount = (int)\think\facade\Db::name('store_order')
|
||||
->where('spread_uid', $spread_uid)
|
||||
->where('is_queue_goods', 1)
|
||||
->where('paid', 1)
|
||||
->where('is_del', 0)
|
||||
->count();
|
||||
|
||||
$position = max(0, $completedCount - 1) % $cycleCount;
|
||||
$cycleRatePercent = isset($cycleRates[$position]) ? (int)$cycleRates[$position] : (int)($cycleRates[0] ?? 0);
|
||||
if ($cycleRatePercent > 0) {
|
||||
return bcmul((string)$price, bcdiv((string)$cycleRatePercent, '100', 4), 2);
|
||||
}
|
||||
return '0';
|
||||
});
|
||||
$oneBrokerage = $brokerageResult;
|
||||
} else {
|
||||
//一级返佣比例 小于等于零时直接返回 不返佣
|
||||
if ($storeBrokerageRatio > 0) {
|
||||
|
||||
@@ -23,6 +23,7 @@ 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;
|
||||
|
||||
|
||||
@@ -282,10 +283,12 @@ class QrcodeServices extends BaseServices
|
||||
}
|
||||
}
|
||||
$siteUrl = sys_config('site_url');
|
||||
$imageInfo = '';
|
||||
if (!$imageInfo) {
|
||||
$res = MiniProgram::appCodeUnlimit($data, $page, 280);
|
||||
if (!$res) return false;
|
||||
if (!$res) {
|
||||
Log::error('[getRoutineQrcodePath] appCodeUnlimit 返回空, scene=' . $data . ', page=' . $page);
|
||||
return false;
|
||||
}
|
||||
$uploadType = (int)sys_config('upload_type', 1);
|
||||
$upload = UploadService::init($uploadType);
|
||||
$res = (string)Utils::streamFor($res);
|
||||
@@ -317,6 +320,7 @@ class QrcodeServices extends BaseServices
|
||||
if ($imageInfo['image_type'] == 1) $url = $siteUrl . $url;
|
||||
return $url;
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('[getRoutineQrcodePath] 生成小程序码异常: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,5 +25,6 @@ return [
|
||||
'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,
|
||||
],
|
||||
];
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<view class="hjf-assets-page" :style="colorStyle">
|
||||
<!-- #ifdef MP -->
|
||||
<NavBar titleText="我的资产" :iconColor="iconColor" :textColor="iconColor" showBack :isScrolling="isScrolling"></NavBar>
|
||||
<!-- #endif -->
|
||||
|
||||
<view class="assets-wrapper">
|
||||
<view class="assets-header">
|
||||
@@ -116,14 +119,25 @@
|
||||
<script>
|
||||
import { getAssetsOverview } from '@/api/hjfAssets.js';
|
||||
import colors from '@/mixins/color.js';
|
||||
// #ifdef MP
|
||||
import NavBar from '@/components/NavBar.vue';
|
||||
// #endif
|
||||
|
||||
export default {
|
||||
name: 'AssetsIndex',
|
||||
|
||||
mixins: [colors],
|
||||
|
||||
components: {
|
||||
// #ifdef MP
|
||||
NavBar,
|
||||
// #endif
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
iconColor: '#FFFFFF',
|
||||
isScrolling: false,
|
||||
assetsInfo: null,
|
||||
loading: false
|
||||
};
|
||||
@@ -146,7 +160,8 @@ export default {
|
||||
},
|
||||
formattedTotalPoints() {
|
||||
if (!this.assetsInfo) return '0';
|
||||
return Number(this.assetsInfo.total_points_earned).toLocaleString();
|
||||
const val = Number(this.assetsInfo.total_points_earned);
|
||||
return isNaN(val) ? '0' : val.toLocaleString();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -160,6 +175,16 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
onPageScroll(e) {
|
||||
if (e.scrollTop > 50) {
|
||||
this.isScrolling = true;
|
||||
this.iconColor = '#333333';
|
||||
} else {
|
||||
this.isScrolling = false;
|
||||
this.iconColor = '#FFFFFF';
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchAssetsOverview() {
|
||||
this.loading = true;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<view class="brokerage-page" :style="colorStyle">
|
||||
<!-- #ifdef MP -->
|
||||
<NavBar titleText="佣金状态" :iconColor="iconColor" :textColor="iconColor" showBack :isScrolling="isScrolling"></NavBar>
|
||||
<!-- #endif -->
|
||||
<!-- 顶部渐变区域 -->
|
||||
<view class="header-gradient">
|
||||
<view class="header-gradient__circle header-gradient__circle--1"></view>
|
||||
@@ -88,14 +91,26 @@ import HjfRefundNotice from '@/components/HjfRefundNotice.vue';
|
||||
import emptyPage from '@/components/emptyPage.vue';
|
||||
import colors from '@/mixins/color.js';
|
||||
import { spreadOrder } from '@/api/user.js';
|
||||
// #ifdef MP
|
||||
import NavBar from '@/components/NavBar.vue';
|
||||
// #endif
|
||||
|
||||
export default {
|
||||
name: 'BrokerageStatus',
|
||||
mixins: [colors],
|
||||
components: { HjfQueueProgress, HjfRefundNotice, emptyPage },
|
||||
components: {
|
||||
HjfQueueProgress,
|
||||
HjfRefundNotice,
|
||||
emptyPage,
|
||||
// #ifdef MP
|
||||
NavBar,
|
||||
// #endif
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
iconColor: '#FFFFFF',
|
||||
isScrolling: false,
|
||||
progressData: {},
|
||||
records: [],
|
||||
loading: false,
|
||||
@@ -108,6 +123,16 @@ export default {
|
||||
};
|
||||
},
|
||||
|
||||
onPageScroll(e) {
|
||||
if (e.scrollTop > 50) {
|
||||
this.isScrolling = true;
|
||||
this.iconColor = '#333333';
|
||||
} else {
|
||||
this.isScrolling = false;
|
||||
this.iconColor = '#FFFFFF';
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadProgress();
|
||||
this.loadRecords();
|
||||
|
||||
@@ -18,11 +18,6 @@
|
||||
:class='type == 2 ? "on" : ""'
|
||||
@click='changeType(2)'
|
||||
>储值</view>
|
||||
<view
|
||||
class='item'
|
||||
:class='type == "queue_refund" ? "on" : ""'
|
||||
@click='changeType("queue_refund")'
|
||||
>公排退款</view>
|
||||
</view>
|
||||
|
||||
<!-- 账单列表 -->
|
||||
@@ -37,14 +32,9 @@
|
||||
:key="indexn"
|
||||
>
|
||||
<view>
|
||||
<view class='name line1'>
|
||||
{{ vo.title }}
|
||||
<!-- 公排退款标记 -->
|
||||
<text
|
||||
v-if="vo.type === 'queue_refund'"
|
||||
class='queue-refund-tag'
|
||||
>公排退款</text>
|
||||
</view>
|
||||
<view class='name line1'>
|
||||
{{ vo.title }}
|
||||
</view>
|
||||
<view class='time-text'>{{ vo.add_time }}</view>
|
||||
</view>
|
||||
<view class='num' :class="vo.pm ? 'num-add' : 'num-sub'">
|
||||
@@ -86,7 +76,6 @@
|
||||
* - 0: 全部
|
||||
* - 1: 消费
|
||||
* - 2: 储值
|
||||
* - "queue_refund": 公排退款(type=queue_refund 时显示专属标记)
|
||||
*
|
||||
* 列表按日期分组,支持上拉分页加载。
|
||||
*
|
||||
@@ -109,11 +98,11 @@
|
||||
page: 1,
|
||||
/** @type {number} 每页条数 */
|
||||
limit: 15,
|
||||
/**
|
||||
* 当前筛选类型
|
||||
* 0=全部 1=消费 2=储值 "queue_refund"=公排退款
|
||||
* @type {number|string}
|
||||
*/
|
||||
/**
|
||||
* 当前筛选类型
|
||||
* 0=全部 1=消费 2=储值
|
||||
* @type {number|string}
|
||||
*/
|
||||
type: 0,
|
||||
/** @type {Array<Object>} 按日期分组的账单列表 */
|
||||
userBillList: [],
|
||||
@@ -201,12 +190,12 @@
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 切换账单筛选类型并重置列表
|
||||
*
|
||||
* @param {number|string} type - 目标类型(0全部 1消费 2储值 "queue_refund"公排退款)
|
||||
* @returns {void}
|
||||
*/
|
||||
/**
|
||||
* 切换账单筛选类型并重置列表
|
||||
*
|
||||
* @param {number|string} type - 目标类型(0全部 1消费 2储值)
|
||||
* @returns {void}
|
||||
*/
|
||||
changeType(type) {
|
||||
if (this.type === type) return;
|
||||
this.type = type;
|
||||
@@ -267,19 +256,6 @@
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
/* 公排退款标记 */
|
||||
.queue-refund-tag {
|
||||
display: inline-block;
|
||||
margin-left: 10rpx;
|
||||
padding: 2rpx 12rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 20rpx;
|
||||
color: #fff;
|
||||
background: var(--view-theme);
|
||||
vertical-align: middle;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 顶部类型筛选导航 */
|
||||
.bill-details .nav {
|
||||
background-color: #fff;
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
<w-barcode :options="config.bar"></w-barcode>
|
||||
</view> -->
|
||||
<view class="acea-row row-center-wrapper" style="margin-top: 56rpx;">
|
||||
<!-- #ifdef MP -->
|
||||
<image :src="qrc" class="qrcode"></image>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP -->
|
||||
<image v-if="qrc" :src="qrc" class="qrcode"></image>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef H5 -->
|
||||
<image v-if="$wechat.isWeixin()" :src="qrc" class="qrcode"></image>
|
||||
<w-qrcode v-else :options="config.qrc"></w-qrcode>
|
||||
@@ -132,14 +132,14 @@
|
||||
routineUrl,
|
||||
wechatUrl
|
||||
} = res.data;
|
||||
// #ifdef MP
|
||||
this.qrc = routineUrl;
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
if (this.$wechat.isWeixin()) {
|
||||
this.qrc = wechatUrl;
|
||||
}
|
||||
// #endif
|
||||
// #ifdef MP
|
||||
this.qrc = routineUrl || '';
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
if (this.$wechat.isWeixin()) {
|
||||
this.qrc = wechatUrl || '';
|
||||
}
|
||||
// #endif
|
||||
});
|
||||
},
|
||||
goDetail(val) {
|
||||
|
||||
@@ -205,22 +205,23 @@
|
||||
if (url.indexOf('https://') > -1) return url;
|
||||
else return url.replace('http://', 'https://');
|
||||
},
|
||||
//获取图片
|
||||
async spreadMsg() {
|
||||
let res = await spreadMsg()
|
||||
this.spreadData = res.data.spread
|
||||
this.nickName = res.data.nickname
|
||||
this.siteName = res.data.site_name
|
||||
uni.showLoading({
|
||||
title: '海报生成中',
|
||||
mask: true
|
||||
});
|
||||
//获取图片
|
||||
async spreadMsg() {
|
||||
let res = await spreadMsg()
|
||||
this.spreadData = res.data.spread
|
||||
this.nickName = res.data.nickname
|
||||
this.siteName = res.data.site_name
|
||||
uni.showLoading({
|
||||
title: '海报生成中',
|
||||
mask: true
|
||||
});
|
||||
try {
|
||||
for (let i = 0; i < res.data.spread.length; i++) {
|
||||
let that = this
|
||||
let arr2 = [];
|
||||
let img = await this.downloadFilestoreImage(res.data.spread[i].pic);
|
||||
let avatar = await this.downloadFilestoreImage(res.data.avatar);
|
||||
let followCode = res.data.qrcode?await this.downloadFilestoreImage(res.data.qrcode):'';
|
||||
let followCode = res.data.qrcode ? await this.downloadFilestoreImage(res.data.qrcode) : '';
|
||||
// #ifdef H5
|
||||
arr2 = [followCode || this.codeSrc, img, avatar]
|
||||
// #endif
|
||||
@@ -231,8 +232,12 @@
|
||||
// #ifdef APP-PLUS
|
||||
arr2 = [this.codeSrc, img, avatar]
|
||||
// #endif
|
||||
if (!img) {
|
||||
console.warn('[海报] 背景图下载失败,跳过第', i, '张海报生成');
|
||||
continue;
|
||||
}
|
||||
this.$nextTick(function(){
|
||||
that.$util.userPosterCanvas(arr2, res.data.nickname, res.data.site_name, i, this.wd, this.hg, (
|
||||
that.$util.userPosterCanvas(arr2, res.data.nickname, res.data.site_name, i, that.wd, that.hg, (
|
||||
tempFilePath) => {
|
||||
that.$set(that.posterImage, i, tempFilePath);
|
||||
// #ifdef MP
|
||||
@@ -245,8 +250,12 @@
|
||||
});
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[海报] 生成异常:', e);
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
},
|
||||
}
|
||||
},
|
||||
// #ifdef MP
|
||||
async routineCode() {
|
||||
let res = await routineCode()
|
||||
@@ -350,23 +359,26 @@
|
||||
});
|
||||
},
|
||||
// #endif
|
||||
//图片转符合安全域名路径
|
||||
downloadFilestoreImage(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let that = this;
|
||||
uni.downloadFile({
|
||||
url: that.setDomain(url),
|
||||
success: function(res) {
|
||||
resolve(res.tempFilePath);
|
||||
},
|
||||
fail: function() {
|
||||
return that.$util.Tips({
|
||||
title: ''
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
},
|
||||
//图片转符合安全域名路径
|
||||
downloadFilestoreImage(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let that = this;
|
||||
if (!url) {
|
||||
resolve('');
|
||||
return;
|
||||
}
|
||||
uni.downloadFile({
|
||||
url: that.setDomain(url),
|
||||
success: function(res) {
|
||||
resolve(res.tempFilePath);
|
||||
},
|
||||
fail: function(err) {
|
||||
console.error('[海报] 图片下载失败:', url, err);
|
||||
resolve('');
|
||||
}
|
||||
});
|
||||
})
|
||||
},
|
||||
setShareInfoStatus: function() {
|
||||
if (this.$wechat.isWeixin()) {
|
||||
if (this.isLogin) {
|
||||
|
||||
Reference in New Issue
Block a user