fix(order): align invite gift ownership
Some checks failed
Build docker and publish / build (20.15.1) (push) Has been cancelled
Some checks failed
Build docker and publish / build (20.15.1) (push) Has been cancelled
This commit is contained in:
parent
ab38cd4943
commit
0ec0e2b9d2
862
doc/invite-purchase-flow.md
Normal file
862
doc/invite-purchase-flow.md
Normal file
@ -0,0 +1,862 @@
|
||||
# 邀请赠送与购买订阅逻辑说明
|
||||
|
||||
本文档说明当前代码中的购买订阅、订单激活、邀请佣金、邀请赠送时间、家庭组归属逻辑。重点覆盖每个主要分支,方便排查“重复订单/重复订阅/邀请未赠时/赠时落点错误”等问题。
|
||||
|
||||
涉及核心文件:
|
||||
|
||||
- `internal/logic/public/order/purchaseLogic.go`
|
||||
- `queue/logic/order/activateOrderLogic.go`
|
||||
- `internal/logic/common/familyEntitlement.go`
|
||||
- `internal/model/user/model.go`
|
||||
|
||||
## 1. 核心概念
|
||||
|
||||
### 1.1 订单状态
|
||||
|
||||
| 状态 | 含义 |
|
||||
| --- | --- |
|
||||
| `1` | pending,已创建未支付 |
|
||||
| `2` | paid,已支付待激活 |
|
||||
| `3` | close,已关闭 |
|
||||
| `4` | failed/claimed,代码里同时用于失败和 worker 临时领取 |
|
||||
| `5` | finished,激活完成 |
|
||||
|
||||
### 1.2 订单类型
|
||||
|
||||
| 类型 | 含义 |
|
||||
| --- | --- |
|
||||
| `1` | 新购套餐 |
|
||||
| `2` | 续费/换套餐 |
|
||||
| `3` | 重置流量 |
|
||||
| `4` | 余额充值 |
|
||||
| `5` | 兑换码激活 |
|
||||
|
||||
### 1.3 用户 ID 与订阅归属
|
||||
|
||||
订单有两个重要用户字段:
|
||||
|
||||
| 字段 | 含义 |
|
||||
| --- | --- |
|
||||
| `user_id` | 发起订单/付款的用户 |
|
||||
| `subscription_user_id` | 订阅权益归属用户;`0` 表示同 `user_id` |
|
||||
|
||||
家庭组规则:
|
||||
|
||||
- 普通用户下单:`subscription_user_id = user_id`。
|
||||
- 家庭组成员下单:`subscription_user_id = 家主用户 ID`。
|
||||
- 家庭组主账号下单:`subscription_user_id = 家主用户 ID`。
|
||||
|
||||
当前代码使用 `ResolveEntitlementUser` 判断家庭归属:
|
||||
|
||||
- 只有有效家庭组、有效成员关系、角色为 member 时,权益归家主。
|
||||
- 家主本人不会被改写到别人名下。
|
||||
|
||||
## 2. 购买订阅下单逻辑
|
||||
|
||||
入口:`Purchase(req *types.PurchaseOrderRequest)`
|
||||
|
||||
这里只是创建订单和安排关闭任务,不直接发放订阅。真正发放订阅在订单支付后由队列激活处理。
|
||||
|
||||
### 2.1 登录用户检查
|
||||
|
||||
分支:
|
||||
|
||||
- 上下文没有当前用户:返回 `InvalidAccess`。
|
||||
- 当前用户存在:继续。
|
||||
|
||||
### 2.2 解析订阅权益归属
|
||||
|
||||
调用 `ResolveEntitlementUser`:
|
||||
|
||||
| 场景 | `effective_user_id` | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 普通用户 | 本人 ID | 订阅归本人 |
|
||||
| 家庭组主账号 | 本人 ID | 订阅归本人 |
|
||||
| 家庭组成员 | 家主 ID | 订阅归家主 |
|
||||
|
||||
后续查询已有订阅、quota、创建订单里的 `subscription_user_id` 都会使用这个归属结果。
|
||||
|
||||
### 2.3 数量校验
|
||||
|
||||
分支:
|
||||
|
||||
- `quantity <= 0`:自动改成 `1`。
|
||||
- `quantity > MaxQuantity`:返回参数错误。
|
||||
- 合法:继续。
|
||||
|
||||
### 2.4 单订阅模式路由
|
||||
|
||||
先默认:
|
||||
|
||||
```text
|
||||
order_type = 1
|
||||
target_subscribe_id = req.subscribe_id
|
||||
parent_order_id = 0
|
||||
subscribe_token = ""
|
||||
```
|
||||
|
||||
如果开启 `Subscribe.SingleModel`:
|
||||
|
||||
| 查询结果 | 行为 |
|
||||
| --- | --- |
|
||||
| 找到已有 anchor 订阅 | 下单路由为续费:`order_type=2`,保留新请求套餐 ID,设置 parent/order token |
|
||||
| 没找到已有订阅 | 保持新购:`order_type=1` |
|
||||
| 查询异常 | 返回数据库错误 |
|
||||
|
||||
说明:即使是换套餐,只要单订阅模式已有订阅,也走续费语义,后续激活会更新套餐 ID 和流量配置。
|
||||
|
||||
### 2.5 非 SingleModel 的全局单订阅兜底
|
||||
|
||||
如果未开启 `SingleModel`,且当前还是新购 `order_type=1`:
|
||||
|
||||
- 查询 `effective_user_id` 名下已有付费订阅:
|
||||
|
||||
```sql
|
||||
user_id = effective_user_id
|
||||
AND token != ''
|
||||
AND (order_id > 0 OR token LIKE 'iap:%')
|
||||
```
|
||||
|
||||
| 查询结果 | 行为 |
|
||||
| --- | --- |
|
||||
| 找到已有订阅 | 路由为续费:`order_type=2`,用已有订阅 token |
|
||||
| 没找到 | 仍然新购 |
|
||||
|
||||
目的:避免同一个权益归属用户购买不同套餐后出现多条订阅权益。
|
||||
|
||||
### 2.6 pending 订单处理
|
||||
|
||||
当前只有一个分支会主动关闭旧 pending 单:
|
||||
|
||||
```text
|
||||
SingleModel = true
|
||||
AND order_type = 1
|
||||
AND 存在同 user_id + subscribe_id + status=1 的订单
|
||||
```
|
||||
|
||||
行为:
|
||||
|
||||
- 关闭旧 pending 订单。
|
||||
- 继续创建新订单。
|
||||
|
||||
注意:
|
||||
|
||||
- 如果订单已被路由为 `order_type=2`,这里不会关闭旧 pending。
|
||||
- 非 `SingleModel` 下也不会走这段 pending 关闭逻辑。
|
||||
|
||||
### 2.7 套餐校验
|
||||
|
||||
分支:
|
||||
|
||||
| 条件 | 行为 |
|
||||
| --- | --- |
|
||||
| 套餐不存在 | 返回数据库错误 |
|
||||
| `sell=false` | 返回套餐不可售 |
|
||||
| 新购且库存为 `0` | 返回库存不足 |
|
||||
| 续费/换套餐 | 不检查库存为 0 的拦截分支 |
|
||||
|
||||
### 2.8 新用户优惠与新用户限定
|
||||
|
||||
调用 `resolveNewUserDiscountEligibility`:
|
||||
|
||||
| 分支 | 行为 |
|
||||
| --- | --- |
|
||||
| 解析失败 | 返回错误 |
|
||||
| 有折扣且符合条件 | 按折扣计算金额 |
|
||||
| 有折扣但不符合条件 | 按原价 |
|
||||
| 新用户限定且不是新用户窗口 | 新购事务内再次校验,不通过则失败 |
|
||||
|
||||
### 2.9 优惠券逻辑
|
||||
|
||||
如果 `req.coupon` 为空:跳过。
|
||||
|
||||
如果不为空:
|
||||
|
||||
| 校验 | 不通过行为 |
|
||||
| --- | --- |
|
||||
| 优惠券存在 | 返回 `CouponNotExist` |
|
||||
| 总使用次数未超限 | 返回使用次数不足 |
|
||||
| 用户使用次数未超限 | 返回用户次数不足 |
|
||||
| 套餐适用 | 返回不适用 |
|
||||
|
||||
通过后计算 `coupon_discount`,从订单金额中扣除。
|
||||
|
||||
### 2.10 支付手续费与礼品余额抵扣
|
||||
|
||||
流程:
|
||||
|
||||
1. 找支付方式。
|
||||
2. 如果金额大于 0,计算手续费并加到订单金额。
|
||||
3. 如果用户 `gift_amount > 0`,继续抵扣订单金额。
|
||||
4. 抵扣金额记录到订单 `gift_amount`。
|
||||
|
||||
事务内如果有礼品余额抵扣:
|
||||
|
||||
- 扣减用户 `gift_amount`。
|
||||
- 写 `system_logs` 的 gift reduce 日志。
|
||||
|
||||
### 2.11 `is_new` 首单标记
|
||||
|
||||
创建订单前调用:
|
||||
|
||||
```sql
|
||||
SELECT COUNT(*)
|
||||
FROM `order`
|
||||
WHERE user_id = 当前付款用户
|
||||
AND status IN (2, 5)
|
||||
```
|
||||
|
||||
| 结果 | `is_new` |
|
||||
| --- | --- |
|
||||
| count = 0 | `true` |
|
||||
| count > 0 | `false` |
|
||||
|
||||
注意:
|
||||
|
||||
- 判断口径是付款用户 `user_id`,不是 `subscription_user_id`。
|
||||
- pending/closed 订单不影响 `is_new`。
|
||||
- 续费订单也可能是 `is_new=true`,例如单订阅模式下首次购买被路由为续费。
|
||||
|
||||
### 2.12 事务内创建订单
|
||||
|
||||
事务内执行:
|
||||
|
||||
1. 新购且套餐有 quota 时,再查一次 `effective_user_id` 名下订阅数量防并发。
|
||||
2. 新购且新用户限定时,再查一次新用户资格。
|
||||
3. 如有礼品余额抵扣,扣减余额并写日志。
|
||||
4. 新购且库存不是 `-1` 时扣库存。
|
||||
5. 插入订单。
|
||||
|
||||
订单核心字段:
|
||||
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| `user_id` | 当前付款用户 |
|
||||
| `subscription_user_id` | 权益归属用户 |
|
||||
| `type` | 新购 `1` 或续费 `2` |
|
||||
| `subscribe_id` | 本次购买的套餐 ID |
|
||||
| `subscribe_token` | 续费时已有订阅 token |
|
||||
| `is_new` | 首单标记 |
|
||||
| `status` | `1` pending |
|
||||
|
||||
### 2.13 延迟关单任务
|
||||
|
||||
订单创建成功后,发送 `DeferCloseOrder` 任务:
|
||||
|
||||
- 延迟时间:15 分钟。
|
||||
- 作用:未支付订单自动关闭。
|
||||
|
||||
## 3. 订单支付后的激活逻辑
|
||||
|
||||
入口:`ActivateOrderLogic.ProcessTask`
|
||||
|
||||
### 3.1 任务解析和订单领取
|
||||
|
||||
分支:
|
||||
|
||||
| 分支 | 行为 |
|
||||
| --- | --- |
|
||||
| payload 解析失败 | 记录错误,不重试 |
|
||||
| 订单不存在 | 返回错误,允许重试 |
|
||||
| 订单已 finished | 幂等跳过 |
|
||||
| 订单不是 paid | 跳过 |
|
||||
| paid 订单 | 原子更新为 claimed 状态后处理 |
|
||||
|
||||
### 3.2 按订单类型分发
|
||||
|
||||
| 订单类型 | 处理函数 |
|
||||
| --- | --- |
|
||||
| 新购 `1` | `NewPurchase` |
|
||||
| 续费 `2` | `Renewal` |
|
||||
| 重置流量 `3` | `ResetTraffic` |
|
||||
| 充值 `4` | `Recharge` |
|
||||
| 兑换码 `5` | `RedemptionActivate` |
|
||||
|
||||
处理成功后:
|
||||
|
||||
1. 执行订阅合并兜底 `reconcilePostOrderSubscriptions`。
|
||||
2. 更新优惠券使用次数。
|
||||
3. 更新订单为 `finished`。
|
||||
|
||||
如果处理失败:
|
||||
|
||||
- 把订单从 claimed 释放回 paid。
|
||||
- 返回错误给队列重试。
|
||||
|
||||
## 4. 新购订单激活与订阅发放
|
||||
|
||||
入口:`NewPurchase`
|
||||
|
||||
### 4.1 获取用户
|
||||
|
||||
分支:
|
||||
|
||||
| 订单 user_id | 行为 |
|
||||
| --- | --- |
|
||||
| 不为 0 | 查询已有用户 |
|
||||
| 为 0 | 从 Redis 临时订单创建游客用户 |
|
||||
|
||||
游客订单创建用户时:
|
||||
|
||||
- 创建用户和 auth method。
|
||||
- 生成 refer code。
|
||||
- 把订单 `user_id` 更新为新用户 ID。
|
||||
- 如果临时订单有邀请码,绑定 `referer_id`。
|
||||
|
||||
### 4.2 新用户限定激活时复查
|
||||
|
||||
如果套餐是新用户限定,激活时再次校验:
|
||||
|
||||
- 不符合则激活失败,订单回到 paid 等待重试/处理。
|
||||
- 符合继续。
|
||||
|
||||
### 4.3 SingleModel 下复用 anchor 订阅
|
||||
|
||||
如果 `Subscribe.SingleModel=true`:
|
||||
|
||||
| 分支 | 行为 |
|
||||
| --- | --- |
|
||||
| 找到 anchor 订阅 | 更新订单 parent_id;用续费逻辑延长/换套餐 |
|
||||
| 找不到 | 继续后续分支 |
|
||||
| 查询异常 | 记录错误,继续后续分支 |
|
||||
|
||||
家庭组场景下查 anchor 的用户 ID:
|
||||
|
||||
- 优先 `subscription_user_id`。
|
||||
- 没有则用 `user_id`。
|
||||
|
||||
### 4.4 复用赠送订阅
|
||||
|
||||
如果还没有可复用订阅:
|
||||
|
||||
- 查找权益归属用户名下 `order_id=0` 的赠送订阅。
|
||||
- 找到后将它升级为付费订阅:
|
||||
- `order_id` 改为当前订单 ID。
|
||||
- 延长到期时间。
|
||||
- 状态改为 active。
|
||||
- 如果套餐变更,更新套餐 ID、流量额度,并清空已用流量。
|
||||
|
||||
### 4.5 兜底复用已有订阅
|
||||
|
||||
如果仍未复用到订阅:
|
||||
|
||||
- 候选用户 ID:
|
||||
- `order.user_id`
|
||||
- 如果 `subscription_user_id` 存在且不同,也加入候选。
|
||||
- 查找这些用户名下 `token != ''` 的订阅。
|
||||
|
||||
找到后:
|
||||
|
||||
- 如果订阅 owner 不是当前 `subscription_user_id`,先把 `user_id` 修正为权益归属用户。
|
||||
- 用续费逻辑延长/换套餐。
|
||||
|
||||
目的:家庭组绑定前后 owner 变化时,也尽量复用旧记录,避免创建重复订阅。
|
||||
|
||||
### 4.6 创建新订阅
|
||||
|
||||
如果以上都没有复用成功,才创建新 `user_subscribe`:
|
||||
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| `user_id` | `subscription_user_id`,没有则 `order.user_id` |
|
||||
| `order_id` | 当前订单 ID |
|
||||
| `subscribe_id` | 当前订单套餐 ID |
|
||||
| `start_time` | 当前时间 |
|
||||
| `expire_time` | 按套餐时间单位和数量计算 |
|
||||
| `traffic` | 套餐流量 |
|
||||
| `token` | 基于订单号生成 |
|
||||
| `uuid` | 新 UUID |
|
||||
| `status` | `1` active |
|
||||
|
||||
创建前如果套餐有 quota,会再按订阅 owner 统计数量。
|
||||
|
||||
### 4.7 新购激活后的异步逻辑
|
||||
|
||||
订阅发放后:
|
||||
|
||||
1. 后台触发用户分组重算。
|
||||
2. 后台异步处理邀请佣金和赠送时间。
|
||||
3. 清套餐缓存。
|
||||
|
||||
注意:邀请逻辑在 goroutine 中执行,不阻塞订单激活。
|
||||
|
||||
## 5. 续费/换套餐激活逻辑
|
||||
|
||||
入口:`Renewal`
|
||||
|
||||
### 5.1 获取用户和订阅
|
||||
|
||||
- 查询订单 `user_id` 对应用户。
|
||||
- 通过 `subscribe_token` 查订阅。
|
||||
- 查询订单 `subscribe_id` 对应套餐。
|
||||
|
||||
### 5.2 Apple IAP 与普通续费分支
|
||||
|
||||
| 分支 | 行为 |
|
||||
| --- | --- |
|
||||
| `iap_expire_at > 0` | 使用 IAP 到期时间兜底,但仍按累计加时语义 |
|
||||
| 普通续费 | `updateSubscriptionForRenewal` |
|
||||
|
||||
### 5.3 普通续费/换套餐规则
|
||||
|
||||
`updateSubscriptionForRenewal`:
|
||||
|
||||
- 如果当前订阅已过期,先把基准时间改为现在。
|
||||
- 如果套餐 ID 变化:
|
||||
- 更新订阅套餐 ID。
|
||||
- 更新流量额度。
|
||||
- 清空已用流量。
|
||||
- 如果套餐没变:
|
||||
- 如果套餐设置 renewal reset,或今天是重置日,则清空已用流量。
|
||||
- 清理 `finished_at`。
|
||||
- `order_id` 改为当前订单 ID。
|
||||
- 按套餐时间单位和数量延长到期时间。
|
||||
- 状态改为 active。
|
||||
- 清空过期流量字段。
|
||||
|
||||
### 5.4 续费后的邀请逻辑
|
||||
|
||||
续费成功后也会调用 `handleCommission`:
|
||||
|
||||
- 是否发佣金由邀请配置和 `order.is_new` 决定。
|
||||
- 是否赠时同样由邀请配置和 `order.is_new` 决定。
|
||||
|
||||
注意:如果 `OnlyFirstPurchase=true`,非首单续费通常不会发佣金,也不会赠首单时间。
|
||||
|
||||
## 6. 邀请关系绑定逻辑
|
||||
|
||||
### 6.1 注册/登录时的邀请码
|
||||
|
||||
新用户注册、游客订单创建用户时,如果带邀请码:
|
||||
|
||||
- 根据邀请码查邀请人。
|
||||
- 设置新用户 `referer_id = 邀请人 ID`。
|
||||
|
||||
### 6.2 用户后绑邀请码
|
||||
|
||||
入口:`BindInviteCode`
|
||||
|
||||
分支:
|
||||
|
||||
| 分支 | 行为 |
|
||||
| --- | --- |
|
||||
| 当前用户不存在 | 返回无权限 |
|
||||
| 当前用户已有 `referer_id` | 返回已绑定 |
|
||||
| 邀请码不存在 | 返回邀请码错误 |
|
||||
| 邀请码属于自己 | 返回不允许绑定自己 |
|
||||
| 通过 | 更新当前用户 `referer_id` |
|
||||
|
||||
注意:
|
||||
|
||||
- `referer_id` 始终记录实际邀请码所有者。
|
||||
- 邀请人是家庭成员时,`referer_id` 仍然是该成员 ID,不自动改为家主 ID。
|
||||
|
||||
## 7. 邀请佣金与赠送时间逻辑
|
||||
|
||||
入口:`handleCommission(userInfo, orderInfo)`
|
||||
|
||||
这里的 `userInfo` 是订单付款用户,也就是被邀请人。
|
||||
|
||||
### 7.1 总入口分支
|
||||
|
||||
先调用 `shouldProcessCommission(userInfo, orderInfo.IsNew)`。
|
||||
|
||||
| 结果 | 行为 |
|
||||
| --- | --- |
|
||||
| `false` | 不发佣金;如果 `is_new=true`,走双方赠时 |
|
||||
| `true` | 发佣金;如果 `is_new=true`,被邀请人赠时 |
|
||||
|
||||
### 7.2 什么时候发佣金
|
||||
|
||||
`shouldProcessCommission` 规则:
|
||||
|
||||
| 条件 | 结果 |
|
||||
| --- | --- |
|
||||
| 被邀请人为空 | 不发 |
|
||||
| 被邀请人 `referer_id=0` | 不发 |
|
||||
| 查不到邀请人 | 不发 |
|
||||
| 邀请人自定义 `referral_percentage > 0`,且只首购但不是首单 | 不发 |
|
||||
| 邀请人自定义 `referral_percentage > 0`,且通过首购限制 | 发佣金 |
|
||||
| 邀请人无自定义比例,系统 `ReferralPercentage=0` | 不发 |
|
||||
| 系统 `OnlyFirstPurchase=true` 且不是首单 | 不发 |
|
||||
| 系统有比例且通过首购限制 | 发佣金 |
|
||||
|
||||
### 7.3 发佣金路径
|
||||
|
||||
如果 `shouldProcessCommission=true`:
|
||||
|
||||
1. 查询邀请人,也就是 `userInfo.referer_id` 对应用户。
|
||||
2. 佣金比例:
|
||||
- 邀请人自定义比例优先。
|
||||
- 否则用系统配置 `Invite.ReferralPercentage`。
|
||||
3. 佣金金额:
|
||||
|
||||
```text
|
||||
(order.amount - order.fee_amount) * referral_percentage / 100
|
||||
```
|
||||
|
||||
4. 事务内幂等检查:
|
||||
- 如果已有同订单佣金日志,则跳过。
|
||||
- 否则增加邀请人的 `commission`。
|
||||
- 写 `system_logs type=33` 佣金日志。
|
||||
5. 更新邀请人缓存。
|
||||
6. 如果 `order.is_new=true`:
|
||||
- 给被邀请人赠送订阅时间。
|
||||
|
||||
当前保持不变的行为:
|
||||
|
||||
- 邀请人是家庭成员时,佣金仍然给实际邀请人成员本人。
|
||||
- 佣金不归并到家主。
|
||||
- 有佣金路径下,邀请人不额外赠送订阅时间。
|
||||
|
||||
### 7.4 不发佣金路径
|
||||
|
||||
如果 `shouldProcessCommission=false`:
|
||||
|
||||
| `order.is_new` | 行为 |
|
||||
| --- | --- |
|
||||
| `true` | 被邀请人和邀请人双方赠送订阅时间 |
|
||||
| `false` | 不赠送时间 |
|
||||
|
||||
双方赠时具体为:
|
||||
|
||||
1. 被邀请人赠时:
|
||||
- 如果被邀请人是家庭成员,加到被邀请人家主套餐。
|
||||
- 否则加到被邀请人本人套餐。
|
||||
2. 邀请人赠时:
|
||||
- 如果邀请人是家庭成员,加到邀请人家主套餐。
|
||||
- 否则加到邀请人本人套餐。
|
||||
|
||||
## 8. 赠送时间目标解析
|
||||
|
||||
入口:`resolveGiftTargetUser(source, forcedOwnerID)`
|
||||
|
||||
### 8.1 强制 owner 分支
|
||||
|
||||
如果 `forcedOwnerID > 0`:
|
||||
|
||||
- 赠送目标直接使用 `forcedOwnerID`。
|
||||
- 典型场景:订单里已有 `subscription_user_id`。
|
||||
- 这保证了家庭成员购买时,被邀请人的赠时落到家主。
|
||||
|
||||
### 8.2 自动家庭组解析分支
|
||||
|
||||
如果没有强制 owner:
|
||||
|
||||
- 调用 `ResolveEntitlementUser(source.Id)`。
|
||||
- 如果 source 是有效家庭成员,目标改为家主。
|
||||
- 否则目标为本人。
|
||||
|
||||
典型场景:
|
||||
|
||||
- 无佣金路径下,邀请人也赠时。
|
||||
- 邀请人如果是家庭成员,赠时会加到邀请人家主套餐。
|
||||
|
||||
### 8.3 目标用户查询失败
|
||||
|
||||
如果解析出来的目标用户查不到:
|
||||
|
||||
- 记录错误日志。
|
||||
- 回退为 source 本人。
|
||||
|
||||
## 9. 赠送时间落库逻辑
|
||||
|
||||
入口:`grantGiftDays(u, days, orderNo, remark)`
|
||||
|
||||
### 9.1 空值和配置分支
|
||||
|
||||
| 条件 | 行为 |
|
||||
| --- | --- |
|
||||
| 目标用户为空 | 直接返回,不写日志 |
|
||||
| `days <= 0` | 直接返回,不写日志 |
|
||||
|
||||
### 9.2 幂等检查
|
||||
|
||||
按下面条件查 gift 日志:
|
||||
|
||||
```sql
|
||||
type = 34
|
||||
AND object_id = 目标用户 ID
|
||||
AND content LIKE '%订单号%'
|
||||
```
|
||||
|
||||
| 结果 | 行为 |
|
||||
| --- | --- |
|
||||
| 已存在 | 跳过,不重复赠时 |
|
||||
| 不存在 | 继续 |
|
||||
|
||||
### 9.3 查目标用户活跃订阅
|
||||
|
||||
调用 `FindActiveSubscribe`。
|
||||
|
||||
当前活跃口径:
|
||||
|
||||
```sql
|
||||
user_id = 目标用户 ID
|
||||
AND status IN (0, 1)
|
||||
AND (
|
||||
expire_time > NOW()
|
||||
OR expire_time = FROM_UNIXTIME(0)
|
||||
)
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `expire_time > NOW()` 是普通未过期订阅。
|
||||
- `expire_time = FROM_UNIXTIME(0)` 是永久/不限时订阅。
|
||||
|
||||
### 9.4 没有活跃订阅
|
||||
|
||||
如果查不到活跃订阅:
|
||||
|
||||
- 不创建新订阅。
|
||||
- 写一条 `system_logs type=34` 日志。
|
||||
- 日志 remark 为:
|
||||
|
||||
```text
|
||||
邀请赠送 skipped: no active subscription
|
||||
```
|
||||
|
||||
这表示邀请赠时触发过,但目标用户当时没有可加时的套餐。
|
||||
|
||||
### 9.5 找到普通活跃订阅
|
||||
|
||||
如果目标订阅不是永久订阅:
|
||||
|
||||
- `expire_time += days * 24h`
|
||||
- 更新订阅。
|
||||
- 写 `system_logs type=34` gift increase 日志。
|
||||
|
||||
### 9.6 找到永久订阅
|
||||
|
||||
如果目标订阅 `expire_time = FROM_UNIXTIME(0)`:
|
||||
|
||||
- 不改变 `expire_time`,因为永久订阅没有可延长的到期时间。
|
||||
- 仍写 `system_logs type=34` gift increase 日志,表示赠送逻辑已识别并处理。
|
||||
|
||||
### 9.7 赠时失败日志
|
||||
|
||||
发佣金路径和无佣金路径都会检查 `grantGiftDays` 返回错误。
|
||||
|
||||
如果出错,会写应用日志:
|
||||
|
||||
```text
|
||||
Grant invite gift days failed
|
||||
```
|
||||
|
||||
附带字段:
|
||||
|
||||
- `stage`
|
||||
- `target_user_id`
|
||||
- `order_no`
|
||||
- `error`
|
||||
|
||||
## 10. 家庭组下的完整分支示例
|
||||
|
||||
### 10.1 被邀请人是普通用户,邀请人普通用户,有佣金
|
||||
|
||||
条件:
|
||||
|
||||
- 被邀请人 `referer_id != 0`
|
||||
- 系统或邀请人佣金比例大于 0
|
||||
- `order.is_new=true`
|
||||
|
||||
结果:
|
||||
|
||||
- 佣金给邀请人本人。
|
||||
- 被邀请人本人套餐加赠送时间。
|
||||
- 邀请人不加赠送时间。
|
||||
|
||||
### 10.2 被邀请人是家庭成员,邀请人普通用户,有佣金
|
||||
|
||||
结果:
|
||||
|
||||
- 佣金给邀请人本人。
|
||||
- 被邀请人的赠送时间加到被邀请人家主套餐。
|
||||
- 被邀请人成员本人不单独加订阅时间。
|
||||
|
||||
### 10.3 被邀请人普通用户,邀请人是家庭成员,有佣金
|
||||
|
||||
结果:
|
||||
|
||||
- 佣金给邀请人成员本人。
|
||||
- 被邀请人本人套餐加赠送时间。
|
||||
- 邀请人不加赠送时间。
|
||||
- 邀请人家主不拿佣金,也不因该佣金路径加赠时。
|
||||
|
||||
### 10.4 被邀请人是家庭成员,邀请人也是家庭成员,有佣金
|
||||
|
||||
结果:
|
||||
|
||||
- 佣金给邀请人成员本人。
|
||||
- 被邀请人的赠送时间加到被邀请人家主套餐。
|
||||
- 邀请人不加赠送时间。
|
||||
- 邀请人家主不拿佣金。
|
||||
|
||||
### 10.5 无佣金路径,被邀请人普通用户,邀请人普通用户
|
||||
|
||||
触发条件示例:
|
||||
|
||||
- `ReferralPercentage=0`
|
||||
- 或因首购限制导致不发佣金
|
||||
- 且 `order.is_new=true`
|
||||
|
||||
结果:
|
||||
|
||||
- 被邀请人本人套餐加赠送时间。
|
||||
- 邀请人本人套餐加赠送时间。
|
||||
|
||||
### 10.6 无佣金路径,被邀请人是家庭成员
|
||||
|
||||
结果:
|
||||
|
||||
- 被邀请人的赠送时间加到被邀请人家主套餐。
|
||||
- 邀请人的赠时按邀请人自己的家庭归属解析。
|
||||
|
||||
### 10.7 无佣金路径,邀请人是家庭成员
|
||||
|
||||
结果:
|
||||
|
||||
- 被邀请人的赠时按被邀请人的家庭归属解析。
|
||||
- 邀请人的赠送时间加到邀请人家主套餐。
|
||||
- 邀请人成员本人不单独加订阅时间。
|
||||
|
||||
### 10.8 被邀请人没有活跃订阅
|
||||
|
||||
结果:
|
||||
|
||||
- 不创建新订阅。
|
||||
- 写 skipped gift 日志。
|
||||
- 后续即使用户后来有订阅,也不会自动补赠,除非另行补偿。
|
||||
|
||||
### 10.9 被邀请人或目标家主是永久订阅
|
||||
|
||||
结果:
|
||||
|
||||
- 识别为活跃订阅。
|
||||
- 不改变到期时间。
|
||||
- 写 gift increase 日志。
|
||||
|
||||
## 11. 排查 SQL
|
||||
|
||||
### 11.1 查邀请配置
|
||||
|
||||
```sql
|
||||
SELECT `key`, `value`, `updated_at`
|
||||
FROM system
|
||||
WHERE category = 'invite'
|
||||
AND `key` IN ('GiftDays', 'OnlyFirstPurchase', 'ReferralPercentage');
|
||||
```
|
||||
|
||||
### 11.2 查某邀请人的被邀请用户
|
||||
|
||||
```sql
|
||||
SELECT id, referer_id, created_at
|
||||
FROM `user`
|
||||
WHERE referer_id = 23944
|
||||
ORDER BY id DESC
|
||||
LIMIT 100;
|
||||
```
|
||||
|
||||
### 11.3 查被邀请人的订单和首单标记
|
||||
|
||||
```sql
|
||||
SELECT u.id AS invited_user_id,
|
||||
o.id AS order_id,
|
||||
o.order_no,
|
||||
o.type,
|
||||
o.status,
|
||||
o.amount,
|
||||
o.is_new,
|
||||
o.subscribe_id,
|
||||
o.subscription_user_id,
|
||||
o.created_at
|
||||
FROM `user` u
|
||||
LEFT JOIN `order` o
|
||||
ON o.user_id = u.id
|
||||
AND o.type IN (1, 2)
|
||||
WHERE u.referer_id = 23944
|
||||
ORDER BY u.id DESC, o.id ASC
|
||||
LIMIT 200;
|
||||
```
|
||||
|
||||
### 11.4 查某订单佣金和赠时日志
|
||||
|
||||
```sql
|
||||
SELECT id, type, object_id, content, created_at
|
||||
FROM system_logs
|
||||
WHERE content LIKE '%202604281812556044982351822%'
|
||||
ORDER BY id DESC;
|
||||
```
|
||||
|
||||
### 11.5 查某用户订阅
|
||||
|
||||
```sql
|
||||
SELECT id, user_id, order_id, subscribe_id, status,
|
||||
expire_time, finished_at, token, created_at, updated_at
|
||||
FROM user_subscribe
|
||||
WHERE user_id = 24425
|
||||
ORDER BY id DESC;
|
||||
```
|
||||
|
||||
### 11.6 查首单但没有赠时日志的被邀请人
|
||||
|
||||
```sql
|
||||
SELECT first_orders.user_id AS invited_user_id,
|
||||
first_orders.order_no,
|
||||
first_orders.is_new,
|
||||
first_orders.status,
|
||||
first_orders.subscription_user_id,
|
||||
first_orders.created_at,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM system_logs sl
|
||||
WHERE sl.type = 34
|
||||
AND sl.content LIKE CONCAT('%', first_orders.order_no, '%')
|
||||
) AS gift_log_count,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM system_logs sl
|
||||
WHERE sl.type = 33
|
||||
AND sl.content LIKE CONCAT('%', first_orders.order_no, '%')
|
||||
) AS commission_log_count
|
||||
FROM (
|
||||
SELECT o.*
|
||||
FROM `order` o
|
||||
JOIN (
|
||||
SELECT user_id, MIN(id) AS first_order_id
|
||||
FROM `order`
|
||||
WHERE type IN (1, 2)
|
||||
AND status IN (2, 5)
|
||||
GROUP BY user_id
|
||||
) fo ON fo.first_order_id = o.id
|
||||
) first_orders
|
||||
JOIN `user` u ON u.id = first_orders.user_id
|
||||
WHERE u.referer_id = 23944
|
||||
ORDER BY first_orders.created_at DESC
|
||||
LIMIT 100;
|
||||
```
|
||||
|
||||
## 12. 部署注意事项
|
||||
|
||||
邀请配置存在两层状态:
|
||||
|
||||
1. Redis 缓存:`system:invite_config`
|
||||
2. 服务进程内存:`svc.Config.Invite`
|
||||
|
||||
如果直接修改数据库或 Redis,已经运行的 `ppanel-server` 进程不会自动刷新内存配置。订单激活和赠时发生在服务进程/队列 worker 内,所以修改邀请配置或部署赠时逻辑后,需要重启服务。
|
||||
|
||||
推荐步骤:
|
||||
|
||||
```bash
|
||||
docker exec ppanel-redis redis-cli DEL system:invite_config system:global_config
|
||||
docker restart ppanel-server
|
||||
```
|
||||
|
||||
确认启动时间:
|
||||
|
||||
```bash
|
||||
docker inspect --format '{{.Name}} {{.State.StartedAt}} {{.Config.Image}}' ppanel-server
|
||||
docker ps --filter name=ppanel-server
|
||||
```
|
||||
|
||||
@ -447,7 +447,8 @@ func (m *customUserModel) QueryMonthlyUserStatisticsList(ctx context.Context, da
|
||||
func (m *customUserModel) FindActiveSubscribe(ctx context.Context, userId int64) (*Subscribe, error) {
|
||||
var subscribe Subscribe
|
||||
err := m.QueryNoCacheCtx(ctx, &subscribe, func(conn *gorm.DB, v interface{}) error {
|
||||
return conn.Where("user_id = ? AND status IN (0, 1) AND expire_time > ?", userId, time.Now()).
|
||||
now := time.Now()
|
||||
return conn.Where("user_id = ? AND status IN (0, 1) AND (expire_time > ? OR expire_time = ?)", userId, now, time.UnixMilli(0)).
|
||||
Order("expire_time DESC").
|
||||
First(v).Error
|
||||
})
|
||||
|
||||
@ -1090,7 +1090,9 @@ func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *use
|
||||
// 有佣金路径:邀请人拿佣金,被邀请用户(首单)拿天数
|
||||
if orderInfo.IsNew {
|
||||
giftTarget := l.resolveGiftTargetUser(ctx, userInfo, orderInfo.SubscriptionUserId)
|
||||
_ = l.grantGiftDays(ctx, giftTarget, int(l.svc.Config.Invite.GiftDays), orderInfo.OrderNo, "邀请赠送")
|
||||
if giftErr := l.grantGiftDays(ctx, giftTarget, int(l.svc.Config.Invite.GiftDays), orderInfo.OrderNo, "邀请赠送"); giftErr != nil {
|
||||
l.logGiftDaysError(ctx, giftErr, giftTarget, orderInfo, "commission_path_referee")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1100,7 +1102,9 @@ func (l *ActivateOrderLogic) grantGiftDaysToBothParties(ctx context.Context, ref
|
||||
return
|
||||
}
|
||||
refereeTarget := l.resolveGiftTargetUser(ctx, referee, orderInfo.SubscriptionUserId)
|
||||
_ = l.grantGiftDays(ctx, refereeTarget, int(giftDays), orderInfo.OrderNo, "邀请赠送")
|
||||
if err := l.grantGiftDays(ctx, refereeTarget, int(giftDays), orderInfo.OrderNo, "邀请赠送"); err != nil {
|
||||
l.logGiftDaysError(ctx, err, refereeTarget, orderInfo, "no_commission_referee")
|
||||
}
|
||||
if referee.RefererId == 0 {
|
||||
return
|
||||
}
|
||||
@ -1109,7 +1113,9 @@ func (l *ActivateOrderLogic) grantGiftDaysToBothParties(ctx context.Context, ref
|
||||
return
|
||||
}
|
||||
refererTarget := l.resolveGiftTargetUser(ctx, referer, 0)
|
||||
_ = l.grantGiftDays(ctx, refererTarget, int(giftDays), orderInfo.OrderNo, "邀请赠送")
|
||||
if err = l.grantGiftDays(ctx, refererTarget, int(giftDays), orderInfo.OrderNo, "邀请赠送"); err != nil {
|
||||
l.logGiftDaysError(ctx, err, refererTarget, orderInfo, "no_commission_referer")
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ActivateOrderLogic) resolveGiftTargetUser(ctx context.Context, source *user.User, forcedOwnerID int64) *user.User {
|
||||
@ -1136,6 +1142,26 @@ func (l *ActivateOrderLogic) resolveGiftTargetUser(ctx context.Context, source *
|
||||
return target
|
||||
}
|
||||
|
||||
func (l *ActivateOrderLogic) logGiftDaysError(ctx context.Context, err error, target *user.User, orderInfo *order.Order, stage string) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
var targetID int64
|
||||
if target != nil {
|
||||
targetID = target.Id
|
||||
}
|
||||
var orderNo string
|
||||
if orderInfo != nil {
|
||||
orderNo = orderInfo.OrderNo
|
||||
}
|
||||
logger.WithContext(ctx).Error("Grant invite gift days failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("stage", stage),
|
||||
logger.Field("target_user_id", targetID),
|
||||
logger.Field("order_no", orderNo),
|
||||
)
|
||||
}
|
||||
|
||||
func (l *ActivateOrderLogic) grantGiftDays(ctx context.Context, u *user.User, days int, orderNo string, remark string) error {
|
||||
if u == nil || days <= 0 {
|
||||
return nil
|
||||
@ -1179,7 +1205,9 @@ func (l *ActivateOrderLogic) grantGiftDays(ctx context.Context, u *user.User, da
|
||||
}
|
||||
return err
|
||||
}
|
||||
activeSubscribe.ExpireTime = activeSubscribe.ExpireTime.Add(time.Duration(days) * 24 * time.Hour)
|
||||
if !activeSubscribe.ExpireTime.Equal(time.UnixMilli(0)) {
|
||||
activeSubscribe.ExpireTime = activeSubscribe.ExpireTime.Add(time.Duration(days) * 24 * time.Hour)
|
||||
}
|
||||
err = l.svc.UserModel.UpdateSubscribe(ctx, activeSubscribe)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@ -312,6 +312,134 @@ func TestInviteFlow_OrderThenBind_NoGiftDays(t *testing.T) {
|
||||
assertExpireIncreasedByDays(t, db, refererSub.Id, baseExpire, 0)
|
||||
}
|
||||
|
||||
func TestHandleCommission_GiftDaysToRefereeFamilyOwnerWhenChannelFirstOrder(t *testing.T) {
|
||||
logic, db, cleanup := setupInviteTestLogic(t, config.InviteConfig{
|
||||
ReferralPercentage: 10,
|
||||
OnlyFirstPurchase: false,
|
||||
GiftDays: 2,
|
||||
})
|
||||
defer cleanup()
|
||||
|
||||
refereeOwner := seedUser(t, db, 0, false)
|
||||
referee := seedUser(t, db, 0, false)
|
||||
referer := seedUser(t, db, 0, false)
|
||||
seedFamily(t, db, refereeOwner.Id, referee.Id)
|
||||
referee.RefererId = referer.Id
|
||||
|
||||
ownerBaseExpire := time.Now().Add(96 * time.Hour).Truncate(time.Second)
|
||||
ownerSub := seedActiveSubscribe(t, db, refereeOwner.Id, ownerBaseExpire)
|
||||
|
||||
logic.handleCommission(context.Background(), referee, &order.Order{
|
||||
OrderNo: "ORD-FAMILY-REFEREE-001",
|
||||
Type: OrderTypeSubscribe,
|
||||
IsNew: true,
|
||||
Amount: 100,
|
||||
FeeAmount: 0,
|
||||
SubscriptionUserId: refereeOwner.Id,
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
|
||||
assertExpireIncreasedByDays(t, db, ownerSub.Id, ownerBaseExpire, 2)
|
||||
assertUserCommission(t, db, referer.Id, 10)
|
||||
}
|
||||
|
||||
func TestHandleCommission_GiftDaysToRefererFamilyOwnerWhenCommissionDisabled(t *testing.T) {
|
||||
logic, db, cleanup := setupInviteTestLogic(t, config.InviteConfig{
|
||||
ReferralPercentage: 0,
|
||||
OnlyFirstPurchase: false,
|
||||
GiftDays: 2,
|
||||
})
|
||||
defer cleanup()
|
||||
|
||||
referee := seedUser(t, db, 0, false)
|
||||
refererOwner := seedUser(t, db, 0, false)
|
||||
referer := seedUser(t, db, 0, false)
|
||||
seedFamily(t, db, refererOwner.Id, referer.Id)
|
||||
referee.RefererId = referer.Id
|
||||
|
||||
refereeBaseExpire := time.Now().Add(72 * time.Hour).Truncate(time.Second)
|
||||
refererOwnerBaseExpire := time.Now().Add(96 * time.Hour).Truncate(time.Second)
|
||||
refereeSub := seedActiveSubscribe(t, db, referee.Id, refereeBaseExpire)
|
||||
refererOwnerSub := seedActiveSubscribe(t, db, refererOwner.Id, refererOwnerBaseExpire)
|
||||
|
||||
logic.handleCommission(context.Background(), referee, &order.Order{
|
||||
OrderNo: "ORD-FAMILY-REFERER-001",
|
||||
Type: OrderTypeSubscribe,
|
||||
IsNew: true,
|
||||
Amount: 100,
|
||||
FeeAmount: 0,
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
|
||||
assertExpireIncreasedByDays(t, db, refereeSub.Id, refereeBaseExpire, 2)
|
||||
assertExpireIncreasedByDays(t, db, refererOwnerSub.Id, refererOwnerBaseExpire, 2)
|
||||
}
|
||||
|
||||
func TestHandleCommission_RefererFamilyMemberCommissionBehaviorUnchanged(t *testing.T) {
|
||||
logic, db, cleanup := setupInviteTestLogic(t, config.InviteConfig{
|
||||
ReferralPercentage: 10,
|
||||
OnlyFirstPurchase: false,
|
||||
GiftDays: 2,
|
||||
})
|
||||
defer cleanup()
|
||||
|
||||
referee := seedUser(t, db, 0, false)
|
||||
refererOwner := seedUser(t, db, 0, false)
|
||||
referer := seedUser(t, db, 0, false)
|
||||
seedFamily(t, db, refererOwner.Id, referer.Id)
|
||||
referee.RefererId = referer.Id
|
||||
|
||||
refereeBaseExpire := time.Now().Add(96 * time.Hour).Truncate(time.Second)
|
||||
refereeSub := seedActiveSubscribe(t, db, referee.Id, refereeBaseExpire)
|
||||
|
||||
logic.handleCommission(context.Background(), referee, &order.Order{
|
||||
OrderNo: "ORD-FAMILY-COMMISSION-001",
|
||||
Type: OrderTypeSubscribe,
|
||||
IsNew: true,
|
||||
Amount: 100,
|
||||
FeeAmount: 0,
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
|
||||
assertExpireIncreasedByDays(t, db, refereeSub.Id, refereeBaseExpire, 2)
|
||||
assertUserCommission(t, db, referer.Id, 10)
|
||||
assertUserCommission(t, db, refererOwner.Id, 0)
|
||||
}
|
||||
|
||||
func TestHandleCommission_GiftDaysRecognizesUnlimitedSubscription(t *testing.T) {
|
||||
logic, db, cleanup := setupInviteTestLogic(t, config.InviteConfig{
|
||||
ReferralPercentage: 10,
|
||||
OnlyFirstPurchase: false,
|
||||
GiftDays: 2,
|
||||
})
|
||||
defer cleanup()
|
||||
|
||||
referee := seedUser(t, db, 0, false)
|
||||
referer := seedUser(t, db, 0, false)
|
||||
referee.RefererId = referer.Id
|
||||
|
||||
unlimitedExpire := time.UnixMilli(0)
|
||||
refereeSub := seedActiveSubscribe(t, db, referee.Id, unlimitedExpire)
|
||||
|
||||
logic.handleCommission(context.Background(), referee, &order.Order{
|
||||
OrderNo: "ORD-UNLIMITED-001",
|
||||
Type: OrderTypeSubscribe,
|
||||
IsNew: true,
|
||||
Amount: 100,
|
||||
FeeAmount: 0,
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
|
||||
assertExpireIncreasedByDays(t, db, refereeSub.Id, unlimitedExpire, 0)
|
||||
var giftCount int64
|
||||
if err := db.Model(&modelLog.SystemLog{}).Where("type = ? AND object_id = ?", modelLog.TypeGift.Uint8(), referee.Id).Count(&giftCount).Error; err != nil {
|
||||
t.Fatalf("count gift logs failed: %v", err)
|
||||
}
|
||||
if giftCount != 1 {
|
||||
t.Fatalf("expected 1 gift log for unlimited subscription, got %d", giftCount)
|
||||
}
|
||||
}
|
||||
|
||||
func setupInviteTestLogic(t *testing.T, inviteCfg config.InviteConfig) (*ActivateOrderLogic, *gorm.DB, func()) {
|
||||
t.Helper()
|
||||
|
||||
@ -336,7 +464,7 @@ func setupInviteTestLogic(t *testing.T, inviteCfg config.InviteConfig) (*Activat
|
||||
t.Fatalf("open test database failed: %v", err)
|
||||
}
|
||||
|
||||
if err := db.AutoMigrate(&user.User{}, &user.Device{}, &user.AuthMethods{}, &user.Subscribe{}, &modelLog.SystemLog{}); err != nil {
|
||||
if err := db.AutoMigrate(&user.User{}, &user.Device{}, &user.AuthMethods{}, &user.Subscribe{}, &user.UserFamily{}, &user.UserFamilyMember{}, &modelLog.SystemLog{}); err != nil {
|
||||
t.Fatalf("auto migrate failed: %v", err)
|
||||
}
|
||||
|
||||
@ -392,6 +520,40 @@ func seedUser(t *testing.T, db *gorm.DB, referralPercentage uint8, onlyFirstPurc
|
||||
return u
|
||||
}
|
||||
|
||||
func seedFamily(t *testing.T, db *gorm.DB, ownerID int64, memberID int64) {
|
||||
t.Helper()
|
||||
family := &user.UserFamily{
|
||||
OwnerUserId: ownerID,
|
||||
MaxMembers: 3,
|
||||
Status: user.FamilyStatusActive,
|
||||
}
|
||||
if err := db.Create(family).Error; err != nil {
|
||||
t.Fatalf("seed family failed: %v", err)
|
||||
}
|
||||
now := time.Now()
|
||||
members := []user.UserFamilyMember{
|
||||
{
|
||||
FamilyId: family.Id,
|
||||
UserId: ownerID,
|
||||
Role: user.FamilyRoleOwner,
|
||||
Status: user.FamilyMemberActive,
|
||||
JoinSource: "test",
|
||||
JoinedAt: now,
|
||||
},
|
||||
{
|
||||
FamilyId: family.Id,
|
||||
UserId: memberID,
|
||||
Role: user.FamilyRoleMember,
|
||||
Status: user.FamilyMemberActive,
|
||||
JoinSource: "test",
|
||||
JoinedAt: now,
|
||||
},
|
||||
}
|
||||
if err := db.Create(&members).Error; err != nil {
|
||||
t.Fatalf("seed family members failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func seedActiveSubscribe(t *testing.T, db *gorm.DB, userID int64, expireAt time.Time) *user.Subscribe {
|
||||
t.Helper()
|
||||
sub := &user.Subscribe{
|
||||
@ -423,6 +585,17 @@ func assertExpireIncreasedByDays(t *testing.T, db *gorm.DB, subscribeID int64, b
|
||||
}
|
||||
}
|
||||
|
||||
func assertUserCommission(t *testing.T, db *gorm.DB, userID int64, expected int64) {
|
||||
t.Helper()
|
||||
var u user.User
|
||||
if err := db.First(&u, userID).Error; err != nil {
|
||||
t.Fatalf("query user failed: %v", err)
|
||||
}
|
||||
if u.Commission != expected {
|
||||
t.Fatalf("expected user %d commission=%d, got %d", userID, expected, u.Commission)
|
||||
}
|
||||
}
|
||||
|
||||
func boolPtr(v bool) *bool {
|
||||
return &v
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user