Compare commits
No commits in common. "3ae85f68eabfc8301a95cafa486fd5e744a7a301" and "9db4762904a1dacc02be327738434756d8339c7b" have entirely different histories.
3ae85f68ea
...
9db4762904
Binary file not shown.
@ -1,862 +0,0 @@
|
|||||||
# 邀请赠送与购买订阅逻辑说明
|
|
||||||
|
|
||||||
本文档说明当前代码中的购买订阅、订单激活、邀请佣金、邀请赠送时间、家庭组归属逻辑。重点覆盖每个主要分支,方便排查“重复订单/重复订阅/邀请未赠时/赠时落点错误”等问题。
|
|
||||||
|
|
||||||
涉及核心文件:
|
|
||||||
|
|
||||||
- `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
|
|
||||||
```
|
|
||||||
|
|
||||||
@ -126,9 +126,7 @@ func (l *EmailLoginLogic) EmailLogin(req *types.EmailLoginRequest) (resp *types.
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
rc := l.svcCtx.Config.Register
|
rc := l.svcCtx.Config.Register
|
||||||
if ShouldAutoGrantTrialOnPublicEmailFlows(rc) &&
|
if ShouldGrantTrialForEmail(rc, req.Email) && !NormalizedEmailHasTrial(l.ctx, l.svcCtx.DB, req.Email, rc.TrialSubscribe) {
|
||||||
ShouldGrantTrialForEmail(rc, req.Email) &&
|
|
||||||
!NormalizedEmailHasTrial(l.ctx, l.svcCtx.DB, req.Email, rc.TrialSubscribe) {
|
|
||||||
if err = l.activeTrial(userInfo.Id); err != nil {
|
if err = l.activeTrial(userInfo.Id); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -396,10 +396,7 @@ func (l *OAuthLoginGetTokenLogic) register(email, avatar, method, openid, reques
|
|||||||
}
|
}
|
||||||
|
|
||||||
rc := l.svcCtx.Config.Register
|
rc := l.svcCtx.Config.Register
|
||||||
shouldActivateTrial := email != "" &&
|
shouldActivateTrial := email != "" && authlogic.ShouldGrantTrialForEmail(rc, email) && !authlogic.NormalizedEmailHasTrial(l.ctx, l.svcCtx.DB, email, rc.TrialSubscribe)
|
||||||
authlogic.ShouldAutoGrantTrialOnPublicEmailFlows(rc) &&
|
|
||||||
authlogic.ShouldGrantTrialForEmail(rc, email) &&
|
|
||||||
!authlogic.NormalizedEmailHasTrial(l.ctx, l.svcCtx.DB, email, rc.TrialSubscribe)
|
|
||||||
|
|
||||||
if shouldActivateTrial {
|
if shouldActivateTrial {
|
||||||
l.Debugw("activating trial subscription",
|
l.Debugw("activating trial subscription",
|
||||||
|
|||||||
@ -54,14 +54,6 @@ func ShouldGrantTrialForEmail(register config.RegisterConfig, email string) bool
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShouldAutoGrantTrialOnPublicEmailFlows defines whether browser/email-originated
|
|
||||||
// flows may auto-create a trial subscription. The current policy disables trial
|
|
||||||
// creation for email registration, email login auto-register, OAuth-with-email,
|
|
||||||
// and email binding/verification to avoid abuse through public email channels.
|
|
||||||
func ShouldAutoGrantTrialOnPublicEmailFlows(register config.RegisterConfig) bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsDisposableAlias detects Gmail dot trick and + alias abuse.
|
// IsDisposableAlias detects Gmail dot trick and + alias abuse.
|
||||||
// For Gmail-like domains, local part containing "." or "+" is rejected.
|
// For Gmail-like domains, local part containing "." or "+" is rejected.
|
||||||
// For all other domains, only "+" alias is rejected.
|
// For all other domains, only "+" alias is rejected.
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/config"
|
"github.com/perfect-panel/server/internal/config"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNormalizeEmail(t *testing.T) {
|
func TestNormalizeEmail(t *testing.T) {
|
||||||
@ -163,15 +162,6 @@ func TestShouldGrantTrialForEmail(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldAutoGrantTrialOnPublicEmailFlows(t *testing.T) {
|
|
||||||
assert.False(t, ShouldAutoGrantTrialOnPublicEmailFlows(config.RegisterConfig{}))
|
|
||||||
assert.False(t, ShouldAutoGrantTrialOnPublicEmailFlows(config.RegisterConfig{
|
|
||||||
EnableTrial: true,
|
|
||||||
EnableTrialEmailWhitelist: true,
|
|
||||||
TrialEmailDomainWhitelist: "gmail.com,example.com",
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIsEmailDomainWhitelisted(t *testing.T) {
|
func TestIsEmailDomainWhitelisted(t *testing.T) {
|
||||||
whitelist := "gmail.com,edu.cn,outlook.com"
|
whitelist := "gmail.com,edu.cn,outlook.com"
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
|||||||
@ -148,9 +148,7 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
|
|||||||
|
|
||||||
// Activate trial subscription after transaction success (moved outside transaction to reduce lock time)
|
// Activate trial subscription after transaction success (moved outside transaction to reduce lock time)
|
||||||
rc := l.svcCtx.Config.Register
|
rc := l.svcCtx.Config.Register
|
||||||
if ShouldAutoGrantTrialOnPublicEmailFlows(rc) &&
|
if ShouldGrantTrialForEmail(rc, req.Email) && !NormalizedEmailHasTrial(l.ctx, l.svcCtx.DB, req.Email, rc.TrialSubscribe) {
|
||||||
ShouldGrantTrialForEmail(rc, req.Email) &&
|
|
||||||
!NormalizedEmailHasTrial(l.ctx, l.svcCtx.DB, req.Email, rc.TrialSubscribe) {
|
|
||||||
trialSubscribe, err = l.activeTrial(userInfo.Id)
|
trialSubscribe, err = l.activeTrial(userInfo.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorw("Failed to activate trial subscription", logger.Field("error", err.Error()))
|
l.Errorw("Failed to activate trial subscription", logger.Field("error", err.Error()))
|
||||||
|
|||||||
@ -1,108 +0,0 @@
|
|||||||
package common
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
ordermodel "github.com/perfect-panel/server/internal/model/order"
|
|
||||||
usermodel "github.com/perfect-panel/server/internal/model/user"
|
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
SubscriptionTraceType = "subscription_flow"
|
|
||||||
SubscriptionTraceFlowOrder = "order_subscription"
|
|
||||||
SubscriptionTraceFlowEmailBind = "email_bind_subscription"
|
|
||||||
)
|
|
||||||
|
|
||||||
func SubscriptionTraceFields(flow string, stage string, fields ...logger.LogField) []logger.LogField {
|
|
||||||
base := []logger.LogField{
|
|
||||||
logger.Field("trace_type", SubscriptionTraceType),
|
|
||||||
logger.Field("flow", flow),
|
|
||||||
logger.Field("stage", stage),
|
|
||||||
}
|
|
||||||
|
|
||||||
return append(base, fields...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SubscriptionTraceInfo(log logger.Logger, flow string, stage string, msg string, fields ...logger.LogField) {
|
|
||||||
log.Infow(msg, SubscriptionTraceFields(flow, stage, fields...)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SubscriptionTraceError(log logger.Logger, flow string, stage string, msg string, fields ...logger.LogField) {
|
|
||||||
log.Errorw(msg, SubscriptionTraceFields(flow, stage, fields...)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func OrderTraceFields(orderInfo *ordermodel.Order) []logger.LogField {
|
|
||||||
if orderInfo == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
effectiveUserID := orderInfo.UserId
|
|
||||||
if orderInfo.SubscriptionUserId > 0 {
|
|
||||||
effectiveUserID = orderInfo.SubscriptionUserId
|
|
||||||
}
|
|
||||||
|
|
||||||
fields := []logger.LogField{
|
|
||||||
logger.Field("order_id", orderInfo.Id),
|
|
||||||
logger.Field("order_no", orderInfo.OrderNo),
|
|
||||||
logger.Field("order_type", orderInfo.Type),
|
|
||||||
logger.Field("order_status", orderInfo.Status),
|
|
||||||
logger.Field("user_id", orderInfo.UserId),
|
|
||||||
logger.Field("subscription_user_id", orderInfo.SubscriptionUserId),
|
|
||||||
logger.Field("effective_user_id", effectiveUserID),
|
|
||||||
logger.Field("order_subscribe_id", orderInfo.SubscribeId),
|
|
||||||
logger.Field("payment_id", orderInfo.PaymentId),
|
|
||||||
logger.Field("payment_method", orderInfo.Method),
|
|
||||||
logger.Field("parent_order_id", orderInfo.ParentId),
|
|
||||||
logger.Field("quantity", orderInfo.Quantity),
|
|
||||||
logger.Field("is_new_order", orderInfo.IsNew),
|
|
||||||
}
|
|
||||||
|
|
||||||
if tail := SensitiveTail(orderInfo.SubscribeToken); tail != "" {
|
|
||||||
fields = append(fields, logger.Field("subscribe_token_tail", tail))
|
|
||||||
}
|
|
||||||
if tail := SensitiveTail(orderInfo.TradeNo); tail != "" {
|
|
||||||
fields = append(fields, logger.Field("trade_no_tail", tail))
|
|
||||||
}
|
|
||||||
if tail := SensitiveTail(orderInfo.AppAccountToken); tail != "" {
|
|
||||||
fields = append(fields, logger.Field("app_account_token_tail", tail))
|
|
||||||
}
|
|
||||||
|
|
||||||
return fields
|
|
||||||
}
|
|
||||||
|
|
||||||
func UserSubscribeTraceFields(userSub *usermodel.Subscribe) []logger.LogField {
|
|
||||||
if userSub == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
fields := []logger.LogField{
|
|
||||||
logger.Field("user_subscribe_id", userSub.Id),
|
|
||||||
logger.Field("subscribe_owner_user_id", userSub.UserId),
|
|
||||||
logger.Field("user_subscribe_plan_id", userSub.SubscribeId),
|
|
||||||
logger.Field("subscribe_order_id", userSub.OrderId),
|
|
||||||
logger.Field("subscribe_status", userSub.Status),
|
|
||||||
logger.Field("expire_time", userSub.ExpireTime),
|
|
||||||
}
|
|
||||||
|
|
||||||
if tail := SensitiveTail(userSub.Token); tail != "" {
|
|
||||||
fields = append(fields, logger.Field("subscribe_token_tail", tail))
|
|
||||||
}
|
|
||||||
if tail := SensitiveTail(userSub.UUID); tail != "" {
|
|
||||||
fields = append(fields, logger.Field("subscribe_uuid_tail", tail))
|
|
||||||
}
|
|
||||||
|
|
||||||
return fields
|
|
||||||
}
|
|
||||||
|
|
||||||
func SensitiveTail(value string) string {
|
|
||||||
value = strings.TrimSpace(value)
|
|
||||||
if value == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if len(value) <= 8 {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
return value[len(value)-8:]
|
|
||||||
}
|
|
||||||
@ -6,7 +6,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
commonLogic "github.com/perfect-panel/server/internal/logic/common"
|
|
||||||
"github.com/perfect-panel/server/pkg/constant"
|
"github.com/perfect-panel/server/pkg/constant"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/pkg/xerr"
|
"github.com/perfect-panel/server/pkg/xerr"
|
||||||
@ -57,12 +56,6 @@ func (l *AlipayNotifyLogic) AlipayNotify(r *http.Request) error {
|
|||||||
l.Logger.Error("[AlipayNotify] Decode notification failed", logger.Field("error", err.Error()))
|
l.Logger.Error("[AlipayNotify] Decode notification failed", logger.Field("error", err.Error()))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_notify_received",
|
|
||||||
"[SubscriptionFlow] alipay notify received",
|
|
||||||
logger.Field("order_no", notify.OrderNo),
|
|
||||||
logger.Field("payment_platform", data.Platform),
|
|
||||||
logger.Field("notify_status", string(notify.Status)),
|
|
||||||
)
|
|
||||||
if notify.Status == alipay.Success {
|
if notify.Status == alipay.Success {
|
||||||
orderInfo, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, notify.OrderNo)
|
orderInfo, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, notify.OrderNo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -80,12 +73,6 @@ func (l *AlipayNotifyLogic) AlipayNotify(r *http.Request) error {
|
|||||||
l.Logger.Error("[AlipayNotify] Update order status failed", logger.Field("error", err.Error()), logger.Field("orderNo", notify.OrderNo))
|
l.Logger.Error("[AlipayNotify] Update order status failed", logger.Field("error", err.Error()), logger.Field("orderNo", notify.OrderNo))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_settled",
|
|
||||||
"[SubscriptionFlow] alipay notify marked order as paid",
|
|
||||||
append(commonLogic.OrderTraceFields(orderInfo),
|
|
||||||
logger.Field("payment_platform", data.Platform),
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
l.Logger.Info("[AlipayNotify] Notify status success", logger.Field("orderNo", notify.OrderNo))
|
l.Logger.Info("[AlipayNotify] Notify status success", logger.Field("orderNo", notify.OrderNo))
|
||||||
payload := types.ForthwithActivateOrderPayload{
|
payload := types.ForthwithActivateOrderPayload{
|
||||||
OrderNo: notify.OrderNo,
|
OrderNo: notify.OrderNo,
|
||||||
@ -101,13 +88,6 @@ func (l *AlipayNotifyLogic) AlipayNotify(r *http.Request) error {
|
|||||||
l.Logger.Error("[AlipayNotify] Enqueue task failed", logger.Field("error", err.Error()))
|
l.Logger.Error("[AlipayNotify] Enqueue task failed", logger.Field("error", err.Error()))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "activation_task_enqueued",
|
|
||||||
"[SubscriptionFlow] activation task enqueued from alipay notify",
|
|
||||||
append(commonLogic.OrderTraceFields(orderInfo),
|
|
||||||
logger.Field("payment_platform", data.Platform),
|
|
||||||
logger.Field("queue_task_id", taskInfo.ID),
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
l.Logger.Info("[AlipayNotify] Enqueue task success", logger.Field("taskInfo", taskInfo))
|
l.Logger.Info("[AlipayNotify] Enqueue task success", logger.Field("taskInfo", taskInfo))
|
||||||
} else {
|
} else {
|
||||||
l.Logger.Error("[AlipayNotify] Notify status failed", logger.Field("status", string(notify.Status)))
|
l.Logger.Error("[AlipayNotify] Notify status failed", logger.Field("status", string(notify.Status)))
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
commonLogic "github.com/perfect-panel/server/internal/logic/common"
|
|
||||||
iapmodel "github.com/perfect-panel/server/internal/model/iap/apple"
|
iapmodel "github.com/perfect-panel/server/internal/model/iap/apple"
|
||||||
"github.com/perfect-panel/server/internal/model/subscribe"
|
"github.com/perfect-panel/server/internal/model/subscribe"
|
||||||
"github.com/perfect-panel/server/internal/model/user"
|
"github.com/perfect-panel/server/internal/model/user"
|
||||||
@ -58,13 +57,6 @@ func (l *AppleIAPNotifyLogic) Handle(signedPayload string) error {
|
|||||||
}
|
}
|
||||||
// 验签通过,记录通知类型与关键交易标识
|
// 验签通过,记录通知类型与关键交易标识
|
||||||
l.Infow("iap notify verified", logger.Field("type", ntype), logger.Field("productId", txPayload.ProductId), logger.Field("originalTransactionId", txPayload.OriginalTransactionId))
|
l.Infow("iap notify verified", logger.Field("type", ntype), logger.Field("productId", txPayload.ProductId), logger.Field("originalTransactionId", txPayload.OriginalTransactionId))
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_notify_received",
|
|
||||||
"[SubscriptionFlow] apple iap server notification received",
|
|
||||||
logger.Field("notify_type", ntype),
|
|
||||||
logger.Field("product_id", txPayload.ProductId),
|
|
||||||
logger.Field("original_transaction_tail", commonLogic.SensitiveTail(txPayload.OriginalTransactionId)),
|
|
||||||
logger.Field("transaction_id_tail", commonLogic.SensitiveTail(txPayload.TransactionId)),
|
|
||||||
)
|
|
||||||
return l.svcCtx.DB.Transaction(func(db *gorm.DB) error {
|
return l.svcCtx.DB.Transaction(func(db *gorm.DB) error {
|
||||||
var existing *iapmodel.Transaction
|
var existing *iapmodel.Transaction
|
||||||
existing, _ = iapmodel.NewModel(l.svcCtx.DB, l.svcCtx.Redis).FindByOriginalId(l.ctx, txPayload.OriginalTransactionId)
|
existing, _ = iapmodel.NewModel(l.svcCtx.DB, l.svcCtx.Redis).FindByOriginalId(l.ctx, txPayload.OriginalTransactionId)
|
||||||
@ -209,13 +201,6 @@ func (l *AppleIAPNotifyLogic) Handle(signedPayload string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
l.Infow("iap notify fallback updated subscribe", logger.Field("userSubscribeId", candidate.Id), logger.Field("status", candidate.Status))
|
l.Infow("iap notify fallback updated subscribe", logger.Field("userSubscribeId", candidate.Id), logger.Field("status", candidate.Status))
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "subscription_updated_from_notify",
|
|
||||||
"[SubscriptionFlow] apple iap notify updated fallback subscription candidate",
|
|
||||||
append(commonLogic.UserSubscribeTraceFields(candidate),
|
|
||||||
logger.Field("notify_type", ntype),
|
|
||||||
logger.Field("product_id", txPayload.ProductId),
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -241,13 +226,6 @@ func (l *AppleIAPNotifyLogic) Handle(signedPayload string) error {
|
|||||||
}
|
}
|
||||||
// 更新成功,输出订阅状态
|
// 更新成功,输出订阅状态
|
||||||
l.Infow("iap notify updated subscribe", logger.Field("userSubscribeId", sub.Id), logger.Field("status", sub.Status))
|
l.Infow("iap notify updated subscribe", logger.Field("userSubscribeId", sub.Id), logger.Field("status", sub.Status))
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "subscription_updated_from_notify",
|
|
||||||
"[SubscriptionFlow] apple iap notify updated subscription",
|
|
||||||
append(commonLogic.UserSubscribeTraceFields(sub),
|
|
||||||
logger.Field("notify_type", ntype),
|
|
||||||
logger.Field("product_id", txPayload.ProductId),
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
commonLogic "github.com/perfect-panel/server/internal/logic/common"
|
|
||||||
"github.com/perfect-panel/server/pkg/constant"
|
"github.com/perfect-panel/server/pkg/constant"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/pkg/xerr"
|
"github.com/perfect-panel/server/pkg/xerr"
|
||||||
@ -45,18 +44,12 @@ func (l *EPayNotifyLogic) EPayNotify(req *types.EPayNotifyRequest) error {
|
|||||||
l.Logger.Error("[EPayNotify] Payment not found in context")
|
l.Logger.Error("[EPayNotify] Payment not found in context")
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "payment config not found")
|
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "payment config not found")
|
||||||
}
|
}
|
||||||
|
l.Infof("[EPayNotify] Payment config: %+v", data)
|
||||||
orderInfo, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OutTradeNo)
|
orderInfo, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OutTradeNo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Logger.Error("[EPayNotify] Find order failed", logger.Field("error", err.Error()), logger.Field("orderNo", req.OutTradeNo))
|
l.Logger.Error("[EPayNotify] Find order failed", logger.Field("error", err.Error()), logger.Field("orderNo", req.OutTradeNo))
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.OrderNotExist), "order not exist: %v", req.OutTradeNo)
|
return errors.Wrapf(xerr.NewErrCode(xerr.OrderNotExist), "order not exist: %v", req.OutTradeNo)
|
||||||
}
|
}
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_notify_received",
|
|
||||||
"[SubscriptionFlow] epay notify received",
|
|
||||||
append(commonLogic.OrderTraceFields(orderInfo),
|
|
||||||
logger.Field("payment_platform", data.Platform),
|
|
||||||
logger.Field("trade_status", req.TradeStatus),
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
|
|
||||||
var config payment.EPayConfig
|
var config payment.EPayConfig
|
||||||
if err := json.Unmarshal([]byte(data.Config), &config); err != nil {
|
if err := json.Unmarshal([]byte(data.Config), &config); err != nil {
|
||||||
@ -82,12 +75,6 @@ func (l *EPayNotifyLogic) EPayNotify(req *types.EPayNotifyRequest) error {
|
|||||||
l.Logger.Error("[EPayNotify] Update order status failed", logger.Field("error", err.Error()), logger.Field("orderNo", req.OutTradeNo))
|
l.Logger.Error("[EPayNotify] Update order status failed", logger.Field("error", err.Error()), logger.Field("orderNo", req.OutTradeNo))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_settled",
|
|
||||||
"[SubscriptionFlow] epay notify marked order as paid",
|
|
||||||
append(commonLogic.OrderTraceFields(orderInfo),
|
|
||||||
logger.Field("payment_platform", data.Platform),
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
// Create activate order task
|
// Create activate order task
|
||||||
payload := queueType.ForthwithActivateOrderPayload{
|
payload := queueType.ForthwithActivateOrderPayload{
|
||||||
OrderNo: req.OutTradeNo,
|
OrderNo: req.OutTradeNo,
|
||||||
@ -103,13 +90,6 @@ func (l *EPayNotifyLogic) EPayNotify(req *types.EPayNotifyRequest) error {
|
|||||||
l.Logger.Error("[EPayNotify] Enqueue task failed", logger.Field("error", err.Error()))
|
l.Logger.Error("[EPayNotify] Enqueue task failed", logger.Field("error", err.Error()))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "activation_task_enqueued",
|
|
||||||
"[SubscriptionFlow] activation task enqueued from epay notify",
|
|
||||||
append(commonLogic.OrderTraceFields(orderInfo),
|
|
||||||
logger.Field("payment_platform", data.Platform),
|
|
||||||
logger.Field("queue_task_id", taskInfo.ID),
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
l.Logger.Info("[EPayNotify] Enqueue task success", logger.Field("taskInfo", taskInfo))
|
l.Logger.Info("[EPayNotify] Enqueue task success", logger.Field("taskInfo", taskInfo))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
commonLogic "github.com/perfect-panel/server/internal/logic/common"
|
|
||||||
"github.com/perfect-panel/server/pkg/constant"
|
"github.com/perfect-panel/server/pkg/constant"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/pkg/xerr"
|
"github.com/perfect-panel/server/pkg/xerr"
|
||||||
@ -68,13 +67,6 @@ func (l *StripeNotifyLogic) StripeNotify(r *http.Request, w http.ResponseWriter)
|
|||||||
l.Logger.Error("[StripeNotify] Find order failed", logger.Field("error", err.Error()), logger.Field("orderNo", notify.OrderNo))
|
l.Logger.Error("[StripeNotify] Find order failed", logger.Field("error", err.Error()), logger.Field("orderNo", notify.OrderNo))
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.OrderNotExist), "order not exist: %v", notify.OrderNo)
|
return errors.Wrapf(xerr.NewErrCode(xerr.OrderNotExist), "order not exist: %v", notify.OrderNo)
|
||||||
}
|
}
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_notify_received",
|
|
||||||
"[SubscriptionFlow] stripe notify received",
|
|
||||||
append(commonLogic.OrderTraceFields(orderInfo),
|
|
||||||
logger.Field("payment_platform", stripeConfig.Platform),
|
|
||||||
logger.Field("stripe_event_type", notify.EventType),
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
if notify.EventType == "payment_intent.succeeded" {
|
if notify.EventType == "payment_intent.succeeded" {
|
||||||
if orderInfo.Status == 5 {
|
if orderInfo.Status == 5 {
|
||||||
return nil
|
return nil
|
||||||
@ -84,13 +76,6 @@ func (l *StripeNotifyLogic) StripeNotify(r *http.Request, w http.ResponseWriter)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_settled",
|
|
||||||
"[SubscriptionFlow] stripe notify marked order as paid",
|
|
||||||
append(commonLogic.OrderTraceFields(orderInfo),
|
|
||||||
logger.Field("payment_platform", stripeConfig.Platform),
|
|
||||||
logger.Field("stripe_event_type", notify.EventType),
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
// create ActivateOrder task
|
// create ActivateOrder task
|
||||||
payload := types.ForthwithActivateOrderPayload{
|
payload := types.ForthwithActivateOrderPayload{
|
||||||
OrderNo: notify.OrderNo,
|
OrderNo: notify.OrderNo,
|
||||||
@ -101,19 +86,11 @@ func (l *StripeNotifyLogic) StripeNotify(r *http.Request, w http.ResponseWriter)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
task := asynq.NewTask(types.ForthwithActivateOrder, bytes, asynq.MaxRetry(5))
|
task := asynq.NewTask(types.ForthwithActivateOrder, bytes, asynq.MaxRetry(5))
|
||||||
taskInfo, err := l.svcCtx.Queue.Enqueue(task)
|
_, err = l.svcCtx.Queue.Enqueue(task)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorw("[StripeNotify] Enqueue error", logger.Field("errors", err.Error()))
|
l.Errorw("[StripeNotify] Enqueue error", logger.Field("errors", err.Error()))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "activation_task_enqueued",
|
|
||||||
"[SubscriptionFlow] activation task enqueued from stripe notify",
|
|
||||||
append(commonLogic.OrderTraceFields(orderInfo),
|
|
||||||
logger.Field("payment_platform", stripeConfig.Platform),
|
|
||||||
logger.Field("stripe_event_type", notify.EventType),
|
|
||||||
logger.Field("queue_task_id", taskInfo.ID),
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
l.Infow("[StripeNotify] success", logger.Field("orderNo", notify.OrderNo))
|
l.Infow("[StripeNotify] success", logger.Field("orderNo", notify.OrderNo))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@ -82,13 +82,6 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest
|
|||||||
l.Errorw("订单与当前用户不匹配", logger.Field("orderNo", req.OrderNo), logger.Field("orderUserId", orderInfo.UserId), logger.Field("userId", u.Id))
|
l.Errorw("订单与当前用户不匹配", logger.Field("orderNo", req.OrderNo), logger.Field("orderUserId", orderInfo.UserId), logger.Field("userId", u.Id))
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "order owner mismatch")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "order owner mismatch")
|
||||||
}
|
}
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "iap_attach_start",
|
|
||||||
"[SubscriptionFlow] apple iap attach flow started",
|
|
||||||
append(commonLogic.OrderTraceFields(orderInfo),
|
|
||||||
logger.Field("request_user_id", u.Id),
|
|
||||||
logger.Field("effective_user_id", entitlement.EffectiveUserID),
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
isNewPurchaseOrder := orderInfo.Type == orderTypeSubscribe
|
isNewPurchaseOrder := orderInfo.Type == orderTypeSubscribe
|
||||||
if isNewPurchaseOrder {
|
if isNewPurchaseOrder {
|
||||||
l.Infow("首购订单将只由订单激活流程创建订阅", logger.Field("orderNo", req.OrderNo), logger.Field("orderType", orderInfo.Type))
|
l.Infow("首购订单将只由订单激活流程创建订阅", logger.Field("orderNo", req.OrderNo), logger.Field("orderType", orderInfo.Type))
|
||||||
@ -100,14 +93,6 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest
|
|||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "invalid jws")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "invalid jws")
|
||||||
}
|
}
|
||||||
l.Infow("JWS 验签成功", logger.Field("productId", txPayload.ProductId), logger.Field("originalTransactionId", txPayload.OriginalTransactionId), logger.Field("purchaseAt", txPayload.PurchaseDate))
|
l.Infow("JWS 验签成功", logger.Field("productId", txPayload.ProductId), logger.Field("originalTransactionId", txPayload.OriginalTransactionId), logger.Field("purchaseAt", txPayload.PurchaseDate))
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "iap_attach_verified",
|
|
||||||
"[SubscriptionFlow] apple iap transaction verified",
|
|
||||||
append(commonLogic.OrderTraceFields(orderInfo),
|
|
||||||
logger.Field("product_id", txPayload.ProductId),
|
|
||||||
logger.Field("original_transaction_tail", commonLogic.SensitiveTail(txPayload.OriginalTransactionId)),
|
|
||||||
logger.Field("transaction_id_tail", commonLogic.SensitiveTail(txPayload.TransactionId)),
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
tradeNoCandidates := l.getAppleTradeNoCandidates(txPayload)
|
tradeNoCandidates := l.getAppleTradeNoCandidates(txPayload)
|
||||||
existingOrderNo, validateErr := l.validateOrderTradeNoBinding(orderInfo, tradeNoCandidates)
|
existingOrderNo, validateErr := l.validateOrderTradeNoBinding(orderInfo, tradeNoCandidates)
|
||||||
if validateErr != nil {
|
if validateErr != nil {
|
||||||
@ -405,12 +390,6 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest
|
|||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
l.Infow("写入用户订阅成功", logger.Field("userId", u.Id), logger.Field("subscribeId", subscribeId), logger.Field("expireUnix", exp.Unix()))
|
l.Infow("写入用户订阅成功", logger.Field("userId", u.Id), logger.Field("subscribeId", subscribeId), logger.Field("expireUnix", exp.Unix()))
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "subscription_created",
|
|
||||||
"[SubscriptionFlow] apple iap attach created a subscription placeholder before queue activation",
|
|
||||||
append(commonLogic.OrderTraceFields(orderInfo),
|
|
||||||
commonLogic.UserSubscribeTraceFields(&userSub)...,
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
l.Infow("首购订单跳过 attach 阶段订阅写入", logger.Field("orderNo", orderInfo.OrderNo), logger.Field("orderType", orderInfo.Type))
|
l.Infow("首购订单跳过 attach 阶段订阅写入", logger.Field("orderNo", orderInfo.OrderNo), logger.Field("orderType", orderInfo.Type))
|
||||||
@ -474,12 +453,6 @@ func (l *AttachTransactionLogic) syncOrderStatusAndEnqueue(orderInfo *ordermodel
|
|||||||
}
|
}
|
||||||
orderInfo.Status = orderStatusPaid
|
orderInfo.Status = orderStatusPaid
|
||||||
l.Infow("更新订单状态成功", logger.Field("orderNo", orderInfo.OrderNo), logger.Field("status", orderStatusPaid))
|
l.Infow("更新订单状态成功", logger.Field("orderNo", orderInfo.OrderNo), logger.Field("status", orderStatusPaid))
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_settled",
|
|
||||||
"[SubscriptionFlow] apple iap attach marked order as paid",
|
|
||||||
append(commonLogic.OrderTraceFields(orderInfo),
|
|
||||||
logger.Field("iap_expire_at", iapExpireAt),
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
// enqueue activation regardless (idempotent handler downstream)
|
// enqueue activation regardless (idempotent handler downstream)
|
||||||
payload := queueType.ForthwithActivateOrderPayload{OrderNo: orderInfo.OrderNo, IAPExpireAt: iapExpireAt}
|
payload := queueType.ForthwithActivateOrderPayload{OrderNo: orderInfo.OrderNo, IAPExpireAt: iapExpireAt}
|
||||||
@ -490,12 +463,6 @@ func (l *AttachTransactionLogic) syncOrderStatusAndEnqueue(orderInfo *ordermodel
|
|||||||
l.Errorw("enqueue activate task error", logger.Field("error", err.Error()))
|
l.Errorw("enqueue activate task error", logger.Field("error", err.Error()))
|
||||||
} else {
|
} else {
|
||||||
l.Infow("已加入订单激活队列", logger.Field("orderNo", orderInfo.OrderNo))
|
l.Infow("已加入订单激活队列", logger.Field("orderNo", orderInfo.OrderNo))
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "activation_task_enqueued",
|
|
||||||
"[SubscriptionFlow] apple iap attach enqueued activation task",
|
|
||||||
append(commonLogic.OrderTraceFields(orderInfo),
|
|
||||||
logger.Field("iap_expire_at", iapExpireAt),
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,17 +63,6 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
|
|||||||
return nil, entErr
|
return nil, entErr
|
||||||
}
|
}
|
||||||
|
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "order_create_start",
|
|
||||||
"[SubscriptionFlow] purchase order creation started",
|
|
||||||
logger.Field("order_kind", "purchase"),
|
|
||||||
logger.Field("user_id", u.Id),
|
|
||||||
logger.Field("effective_user_id", entitlement.EffectiveUserID),
|
|
||||||
logger.Field("requested_subscribe_id", req.SubscribeId),
|
|
||||||
logger.Field("quantity", req.Quantity),
|
|
||||||
logger.Field("payment_id", req.Payment),
|
|
||||||
logger.Field("coupon", req.Coupon),
|
|
||||||
)
|
|
||||||
|
|
||||||
if req.Quantity <= 0 {
|
if req.Quantity <= 0 {
|
||||||
l.Debugf("[Purchase] Quantity is less than or equal to 0, setting to 1")
|
l.Debugf("[Purchase] Quantity is less than or equal to 0, setting to 1")
|
||||||
req.Quantity = 1
|
req.Quantity = 1
|
||||||
@ -113,15 +102,12 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
|
|||||||
parentOrderID = decision.Anchor.OrderId
|
parentOrderID = decision.Anchor.OrderId
|
||||||
subscribeToken = decision.Anchor.Token
|
subscribeToken = decision.Anchor.Token
|
||||||
anchorUserSubscribeID = decision.Anchor.Id
|
anchorUserSubscribeID = decision.Anchor.Id
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "order_route_selected",
|
l.Infow("[Purchase] single mode purchase routed to renewal",
|
||||||
"[SubscriptionFlow] purchase routed to renewal before order creation",
|
logger.Field("mode", "single"),
|
||||||
logger.Field("route_mode", "single"),
|
|
||||||
logger.Field("route", "purchase_to_renewal"),
|
logger.Field("route", "purchase_to_renewal"),
|
||||||
logger.Field("anchor_user_subscribe_id", decision.Anchor.Id),
|
logger.Field("anchor_user_subscribe_id", decision.Anchor.Id),
|
||||||
|
logger.Field("order_no", "pending"),
|
||||||
logger.Field("user_id", u.Id),
|
logger.Field("user_id", u.Id),
|
||||||
logger.Field("effective_user_id", entitlement.EffectiveUserID),
|
|
||||||
logger.Field("requested_subscribe_id", req.SubscribeId),
|
|
||||||
logger.Field("resolved_subscribe_id", targetSubscribeID),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -140,15 +126,11 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
|
|||||||
orderType = 2
|
orderType = 2
|
||||||
parentOrderID = existSub.OrderId
|
parentOrderID = existSub.OrderId
|
||||||
subscribeToken = existSub.Token
|
subscribeToken = existSub.Token
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "order_route_selected",
|
l.Infow("[Purchase] purchase routed to renewal/change plan (existing subscription found)",
|
||||||
"[SubscriptionFlow] purchase routed to renewal because an existing subscription was found",
|
|
||||||
logger.Field("route_mode", "global_single_subscription"),
|
|
||||||
logger.Field("route", "purchase_to_existing_subscription"),
|
|
||||||
logger.Field("existing_subscribe_id", existSub.Id),
|
logger.Field("existing_subscribe_id", existSub.Id),
|
||||||
logger.Field("existing_status", existSub.Status),
|
logger.Field("existing_status", existSub.Status),
|
||||||
logger.Field("user_id", u.Id),
|
logger.Field("user_id", u.Id),
|
||||||
logger.Field("effective_user_id", entitlement.EffectiveUserID),
|
logger.Field("subscribe_id", targetSubscribeID),
|
||||||
logger.Field("resolved_subscribe_id", targetSubscribeID),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -319,13 +301,13 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
|
|||||||
AppAccountToken: uuid.New().String(),
|
AppAccountToken: uuid.New().String(),
|
||||||
}
|
}
|
||||||
if isSingleModeRenewal {
|
if isSingleModeRenewal {
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "order_created",
|
l.Infow("[Purchase] single mode purchase order created as renewal",
|
||||||
"[SubscriptionFlow] purchase order persisted as renewal",
|
logger.Field("mode", "single"),
|
||||||
append(commonLogic.OrderTraceFields(orderInfo),
|
|
||||||
logger.Field("route_mode", "single"),
|
|
||||||
logger.Field("route", "purchase_to_renewal"),
|
logger.Field("route", "purchase_to_renewal"),
|
||||||
logger.Field("anchor_user_subscribe_id", anchorUserSubscribeID),
|
logger.Field("anchor_user_subscribe_id", anchorUserSubscribeID),
|
||||||
)...,
|
logger.Field("order_no", orderInfo.OrderNo),
|
||||||
|
logger.Field("parent_id", orderInfo.ParentId),
|
||||||
|
logger.Field("user_id", u.Id),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Database transaction
|
// Database transaction
|
||||||
@ -422,16 +404,6 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
|
|||||||
}
|
}
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert order error: %v", err.Error())
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert order error: %v", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isSingleModeRenewal {
|
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "order_created",
|
|
||||||
"[SubscriptionFlow] purchase order persisted",
|
|
||||||
append(commonLogic.OrderTraceFields(orderInfo),
|
|
||||||
logger.Field("route_mode", "standard"),
|
|
||||||
logger.Field("resolved_subscribe_id", targetSubscribeID),
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// Deferred task
|
// Deferred task
|
||||||
payload := queue.DeferCloseOrderPayload{
|
payload := queue.DeferCloseOrderPayload{
|
||||||
OrderNo: orderInfo.OrderNo,
|
OrderNo: orderInfo.OrderNo,
|
||||||
|
|||||||
@ -54,17 +54,6 @@ func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.Rene
|
|||||||
if entErr != nil {
|
if entErr != nil {
|
||||||
return nil, entErr
|
return nil, entErr
|
||||||
}
|
}
|
||||||
|
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "order_create_start",
|
|
||||||
"[SubscriptionFlow] renewal order creation started",
|
|
||||||
logger.Field("order_kind", "renewal"),
|
|
||||||
logger.Field("user_id", u.Id),
|
|
||||||
logger.Field("effective_user_id", entitlement.EffectiveUserID),
|
|
||||||
logger.Field("requested_user_subscribe_id", req.UserSubscribeID),
|
|
||||||
logger.Field("quantity", req.Quantity),
|
|
||||||
logger.Field("payment_id", req.Payment),
|
|
||||||
logger.Field("coupon", req.Coupon),
|
|
||||||
)
|
|
||||||
if req.Quantity <= 0 {
|
if req.Quantity <= 0 {
|
||||||
l.Debugf("[Renewal] Quantity is less than or equal to 0, setting to 1")
|
l.Debugf("[Renewal] Quantity is less than or equal to 0, setting to 1")
|
||||||
req.Quantity = 1
|
req.Quantity = 1
|
||||||
@ -246,14 +235,6 @@ func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.Rene
|
|||||||
l.Errorw("[Renewal] Database insert error", logger.Field("error", err.Error()), logger.Field("order", orderInfo))
|
l.Errorw("[Renewal] Database insert error", logger.Field("error", err.Error()), logger.Field("order", orderInfo))
|
||||||
return nil, errors.Wrapf(err, "insert order error: %v", err.Error())
|
return nil, errors.Wrapf(err, "insert order error: %v", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "order_created",
|
|
||||||
"[SubscriptionFlow] renewal order persisted",
|
|
||||||
append(commonLogic.OrderTraceFields(&orderInfo),
|
|
||||||
logger.Field("requested_user_subscribe_id", req.UserSubscribeID),
|
|
||||||
logger.Field("resolved_user_subscribe_id", userSubscribe.Id),
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
// Deferred task
|
// Deferred task
|
||||||
payload := queue.DeferCloseOrderPayload{
|
payload := queue.DeferCloseOrderPayload{
|
||||||
OrderNo: orderInfo.OrderNo,
|
OrderNo: orderInfo.OrderNo,
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
commonLogic "github.com/perfect-panel/server/internal/logic/common"
|
|
||||||
"github.com/perfect-panel/server/internal/model/log"
|
"github.com/perfect-panel/server/internal/model/log"
|
||||||
"github.com/perfect-panel/server/internal/report"
|
"github.com/perfect-panel/server/internal/report"
|
||||||
"github.com/perfect-panel/server/pkg/constant"
|
"github.com/perfect-panel/server/pkg/constant"
|
||||||
@ -76,14 +75,6 @@ func (l *PurchaseCheckoutLogic) PurchaseCheckout(req *types.CheckoutOrderRequest
|
|||||||
l.Logger.Error("[PurchaseCheckout] Database query error", logger.Field("error", err.Error()), logger.Field("payment", orderInfo.Method))
|
l.Logger.Error("[PurchaseCheckout] Database query error", logger.Field("error", err.Error()), logger.Field("payment", orderInfo.Method))
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find payment method error: %v", err.Error())
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find payment method error: %v", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "checkout_start",
|
|
||||||
"[SubscriptionFlow] checkout started",
|
|
||||||
append(commonLogic.OrderTraceFields(orderInfo),
|
|
||||||
logger.Field("payment_platform", paymentConfig.Platform),
|
|
||||||
logger.Field("has_return_url", req.ReturnUrl != ""),
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
// Route to appropriate payment handler based on payment platform
|
// Route to appropriate payment handler based on payment platform
|
||||||
switch paymentPlatform.ParsePlatform(orderInfo.Method) {
|
switch paymentPlatform.ParsePlatform(orderInfo.Method) {
|
||||||
case paymentPlatform.AppleIAP:
|
case paymentPlatform.AppleIAP:
|
||||||
@ -92,14 +83,6 @@ func (l *PurchaseCheckoutLogic) PurchaseCheckout(req *types.CheckoutOrderRequest
|
|||||||
Type: "apple_iap",
|
Type: "apple_iap",
|
||||||
ProductIds: []string{productId},
|
ProductIds: []string{productId},
|
||||||
}
|
}
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "checkout_response_ready",
|
|
||||||
"[SubscriptionFlow] checkout response prepared",
|
|
||||||
append(commonLogic.OrderTraceFields(orderInfo),
|
|
||||||
logger.Field("payment_platform", paymentConfig.Platform),
|
|
||||||
logger.Field("checkout_type", resp.Type),
|
|
||||||
logger.Field("product_ids", resp.ProductIds),
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
return resp, nil
|
return resp, nil
|
||||||
case paymentPlatform.EPay:
|
case paymentPlatform.EPay:
|
||||||
// Process EPay payment - generates payment URL for redirect
|
// Process EPay payment - generates payment URL for redirect
|
||||||
@ -174,16 +157,6 @@ func (l *PurchaseCheckoutLogic) PurchaseCheckout(req *types.CheckoutOrderRequest
|
|||||||
l.Errorw("[PurchaseCheckout] payment method not found", logger.Field("method", orderInfo.Method))
|
l.Errorw("[PurchaseCheckout] payment method not found", logger.Field("method", orderInfo.Method))
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "payment method not found")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "payment method not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp != nil {
|
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "checkout_response_ready",
|
|
||||||
"[SubscriptionFlow] checkout response prepared",
|
|
||||||
append(commonLogic.OrderTraceFields(orderInfo),
|
|
||||||
logger.Field("payment_platform", paymentConfig.Platform),
|
|
||||||
logger.Field("checkout_type", resp.Type),
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -530,9 +503,6 @@ func (l *PurchaseCheckoutLogic) queryExchangeRate(to string, src int64) (amount
|
|||||||
func (l *PurchaseCheckoutLogic) balancePayment(u *user.User, o *order.Order) error {
|
func (l *PurchaseCheckoutLogic) balancePayment(u *user.User, o *order.Order) error {
|
||||||
var userInfo user.User
|
var userInfo user.User
|
||||||
var err error
|
var err error
|
||||||
var giftUsed int64
|
|
||||||
var balanceUsed int64
|
|
||||||
paymentPath := "balance"
|
|
||||||
if o.Amount == 0 {
|
if o.Amount == 0 {
|
||||||
// No payment required for zero-amount orders
|
// No payment required for zero-amount orders
|
||||||
l.Logger.Info(
|
l.Logger.Info(
|
||||||
@ -548,13 +518,6 @@ func (l *PurchaseCheckoutLogic) balancePayment(u *user.User, o *order.Order) err
|
|||||||
logger.Field("userId", u.Id))
|
logger.Field("userId", u.Id))
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Update order status error: %s", err.Error())
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Update order status error: %s", err.Error())
|
||||||
}
|
}
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_settled",
|
|
||||||
"[SubscriptionFlow] order marked paid without external payment",
|
|
||||||
append(commonLogic.OrderTraceFields(o),
|
|
||||||
logger.Field("payment_path", "zero_amount"),
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
paymentPath = "zero_amount"
|
|
||||||
goto activation
|
goto activation
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -573,6 +536,7 @@ func (l *PurchaseCheckoutLogic) balancePayment(u *user.User, o *order.Order) err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate payment distribution: prioritize gift amount first
|
// Calculate payment distribution: prioritize gift amount first
|
||||||
|
var giftUsed, balanceUsed int64
|
||||||
remainingAmount := o.Amount
|
remainingAmount := o.Amount
|
||||||
|
|
||||||
if userInfo.GiftAmount >= remainingAmount {
|
if userInfo.GiftAmount >= remainingAmount {
|
||||||
@ -657,15 +621,6 @@ func (l *PurchaseCheckoutLogic) balancePayment(u *user.User, o *order.Order) err
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_settled",
|
|
||||||
"[SubscriptionFlow] balance payment settled and order marked paid",
|
|
||||||
append(commonLogic.OrderTraceFields(o),
|
|
||||||
logger.Field("payment_path", "balance"),
|
|
||||||
logger.Field("gift_used", giftUsed),
|
|
||||||
logger.Field("balance_used", balanceUsed),
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
|
|
||||||
activation:
|
activation:
|
||||||
// Enqueue order activation task for immediate processing
|
// Enqueue order activation task for immediate processing
|
||||||
payload := queueType.ForthwithActivateOrderPayload{
|
payload := queueType.ForthwithActivateOrderPayload{
|
||||||
@ -684,13 +639,6 @@ activation:
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "activation_task_enqueued",
|
|
||||||
"[SubscriptionFlow] activation task enqueued after checkout payment",
|
|
||||||
append(commonLogic.OrderTraceFields(o),
|
|
||||||
logger.Field("payment_path", paymentPath),
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
|
|
||||||
l.Logger.Info("[PurchaseCheckout] Balance payment completed successfully",
|
l.Logger.Info("[PurchaseCheckout] Balance payment completed successfully",
|
||||||
logger.Field("orderNo", o.OrderNo),
|
logger.Field("orderNo", o.OrderNo),
|
||||||
logger.Field("userId", u.Id))
|
logger.Field("userId", u.Id))
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/config"
|
"github.com/perfect-panel/server/internal/config"
|
||||||
commonLogic "github.com/perfect-panel/server/internal/logic/common"
|
|
||||||
"github.com/perfect-panel/server/internal/model/user"
|
"github.com/perfect-panel/server/internal/model/user"
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
"github.com/perfect-panel/server/internal/types"
|
"github.com/perfect-panel/server/internal/types"
|
||||||
@ -44,12 +43,6 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi
|
|||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||||
}
|
}
|
||||||
|
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "bind_start",
|
|
||||||
"[SubscriptionFlow] email bind with verification started",
|
|
||||||
logger.Field("device_user_id", u.Id),
|
|
||||||
logger.Field("email", req.Email),
|
|
||||||
)
|
|
||||||
|
|
||||||
type payload struct {
|
type payload struct {
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
LastAt int64 `json:"lastAt"`
|
LastAt int64 `json:"lastAt"`
|
||||||
@ -76,12 +69,6 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi
|
|||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error or expired")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error or expired")
|
||||||
}
|
}
|
||||||
|
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "bind_code_verified",
|
|
||||||
"[SubscriptionFlow] email verification code accepted",
|
|
||||||
logger.Field("device_user_id", u.Id),
|
|
||||||
logger.Field("email", req.Email),
|
|
||||||
)
|
|
||||||
|
|
||||||
familyHelper := newFamilyBindingHelper(l.ctx, l.svcCtx)
|
familyHelper := newFamilyBindingHelper(l.ctx, l.svcCtx)
|
||||||
currentEmailMethod, err := familyHelper.getUserEmailMethod(u.Id)
|
currentEmailMethod, err := familyHelper.getUserEmailMethod(u.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -128,13 +115,6 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi
|
|||||||
return nil, txErr
|
return nil, txErr
|
||||||
}
|
}
|
||||||
|
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "email_owner_created",
|
|
||||||
"[SubscriptionFlow] new email owner account created for bind flow",
|
|
||||||
logger.Field("device_user_id", u.Id),
|
|
||||||
logger.Field("owner_user_id", emailUser.Id),
|
|
||||||
logger.Field("email", req.Email),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Join family: email user as owner, device user as member
|
// Join family: email user as owner, device user as member
|
||||||
if err = familyHelper.validateJoinFamily(emailUser.Id, u.Id); err != nil {
|
if err = familyHelper.validateJoinFamily(emailUser.Id, u.Id); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -143,32 +123,11 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "family_joined",
|
|
||||||
"[SubscriptionFlow] device user joined email owner family",
|
|
||||||
logger.Field("device_user_id", u.Id),
|
|
||||||
logger.Field("owner_user_id", emailUser.Id),
|
|
||||||
logger.Field("family_id", joinResult.FamilyId),
|
|
||||||
logger.Field("email", req.Email),
|
|
||||||
)
|
|
||||||
token, err := l.refreshBindSessionToken(u.Id)
|
token, err := l.refreshBindSessionToken(u.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_requested",
|
|
||||||
"[SubscriptionFlow] evaluating trial grant after email bind",
|
|
||||||
logger.Field("device_user_id", u.Id),
|
|
||||||
logger.Field("owner_user_id", emailUser.Id),
|
|
||||||
logger.Field("family_id", joinResult.FamilyId),
|
|
||||||
logger.Field("email", req.Email),
|
|
||||||
)
|
|
||||||
tryGrantTrialOnEmailBind(l.ctx, l.svcCtx, l.Logger, emailUser.Id, req.Email)
|
tryGrantTrialOnEmailBind(l.ctx, l.svcCtx, l.Logger, emailUser.Id, req.Email)
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "bind_complete",
|
|
||||||
"[SubscriptionFlow] email bind with verification completed",
|
|
||||||
logger.Field("device_user_id", u.Id),
|
|
||||||
logger.Field("owner_user_id", emailUser.Id),
|
|
||||||
logger.Field("family_id", joinResult.FamilyId),
|
|
||||||
logger.Field("email", req.Email),
|
|
||||||
)
|
|
||||||
return &types.BindEmailWithVerificationResponse{
|
return &types.BindEmailWithVerificationResponse{
|
||||||
Success: true,
|
Success: true,
|
||||||
Message: "email user created and joined family",
|
Message: "email user created and joined family",
|
||||||
@ -187,44 +146,16 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi
|
|||||||
if err = familyHelper.validateJoinFamily(existingMethod.UserId, u.Id); err != nil {
|
if err = familyHelper.validateJoinFamily(existingMethod.UserId, u.Id); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "email_owner_resolved",
|
|
||||||
"[SubscriptionFlow] existing email owner resolved for bind flow",
|
|
||||||
logger.Field("device_user_id", u.Id),
|
|
||||||
logger.Field("owner_user_id", existingMethod.UserId),
|
|
||||||
logger.Field("email", req.Email),
|
|
||||||
)
|
|
||||||
joinResult, err := familyHelper.joinFamily(existingMethod.UserId, u.Id, "bind_email_with_verification")
|
joinResult, err := familyHelper.joinFamily(existingMethod.UserId, u.Id, "bind_email_with_verification")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "family_joined",
|
|
||||||
"[SubscriptionFlow] device user joined existing email owner family",
|
|
||||||
logger.Field("device_user_id", u.Id),
|
|
||||||
logger.Field("owner_user_id", existingMethod.UserId),
|
|
||||||
logger.Field("family_id", joinResult.FamilyId),
|
|
||||||
logger.Field("email", req.Email),
|
|
||||||
)
|
|
||||||
token, err := l.refreshBindSessionToken(u.Id)
|
token, err := l.refreshBindSessionToken(u.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_requested",
|
|
||||||
"[SubscriptionFlow] evaluating trial grant after existing email owner bind",
|
|
||||||
logger.Field("device_user_id", u.Id),
|
|
||||||
logger.Field("owner_user_id", existingMethod.UserId),
|
|
||||||
logger.Field("family_id", joinResult.FamilyId),
|
|
||||||
logger.Field("email", req.Email),
|
|
||||||
)
|
|
||||||
tryGrantTrialOnEmailBind(l.ctx, l.svcCtx, l.Logger, existingMethod.UserId, req.Email)
|
tryGrantTrialOnEmailBind(l.ctx, l.svcCtx, l.Logger, existingMethod.UserId, req.Email)
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "bind_complete",
|
|
||||||
"[SubscriptionFlow] email bind with verification completed",
|
|
||||||
logger.Field("device_user_id", u.Id),
|
|
||||||
logger.Field("owner_user_id", existingMethod.UserId),
|
|
||||||
logger.Field("family_id", joinResult.FamilyId),
|
|
||||||
logger.Field("email", req.Email),
|
|
||||||
)
|
|
||||||
|
|
||||||
return &types.BindEmailWithVerificationResponse{
|
return &types.BindEmailWithVerificationResponse{
|
||||||
Success: true,
|
Success: true,
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/logic/auth"
|
"github.com/perfect-panel/server/internal/logic/auth"
|
||||||
commonLogic "github.com/perfect-panel/server/internal/logic/common"
|
|
||||||
"github.com/perfect-panel/server/internal/model/user"
|
"github.com/perfect-panel/server/internal/model/user"
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
"github.com/perfect-panel/server/pkg/logger"
|
||||||
@ -15,28 +14,11 @@ import (
|
|||||||
|
|
||||||
func tryGrantTrialOnEmailBind(ctx context.Context, svcCtx *svc.ServiceContext, log logger.Logger, ownerUserId int64, email string) {
|
func tryGrantTrialOnEmailBind(ctx context.Context, svcCtx *svc.ServiceContext, log logger.Logger, ownerUserId int64, email string) {
|
||||||
rc := svcCtx.Config.Register
|
rc := svcCtx.Config.Register
|
||||||
commonLogic.SubscriptionTraceInfo(log, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_evaluating",
|
|
||||||
"[SubscriptionFlow] evaluating email bind trial grant",
|
|
||||||
logger.Field("owner_user_id", ownerUserId),
|
|
||||||
logger.Field("email", email),
|
|
||||||
logger.Field("trial_subscribe_id", rc.TrialSubscribe),
|
|
||||||
)
|
|
||||||
if !auth.ShouldAutoGrantTrialOnPublicEmailFlows(rc) {
|
|
||||||
commonLogic.SubscriptionTraceInfo(log, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_skipped",
|
|
||||||
"[SubscriptionFlow] auto trial on public email flow disabled",
|
|
||||||
logger.Field("email", email),
|
|
||||||
logger.Field("owner_user_id", ownerUserId),
|
|
||||||
logger.Field("skip_reason", "public_email_trial_disabled"),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !auth.ShouldGrantTrialForEmail(rc, email) {
|
if !auth.ShouldGrantTrialForEmail(rc, email) {
|
||||||
if rc.EnableTrial && rc.EnableTrialEmailWhitelist {
|
if rc.EnableTrial && rc.EnableTrialEmailWhitelist {
|
||||||
commonLogic.SubscriptionTraceInfo(log, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_skipped",
|
log.Infow("email domain not in trial whitelist, skip",
|
||||||
"[SubscriptionFlow] email domain not in trial whitelist",
|
|
||||||
logger.Field("email", email),
|
logger.Field("email", email),
|
||||||
logger.Field("owner_user_id", ownerUserId),
|
logger.Field("owner_user_id", ownerUserId),
|
||||||
logger.Field("skip_reason", "trial_whitelist_rejected"),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@ -47,20 +29,12 @@ func tryGrantTrialOnEmailBind(ctx context.Context, svcCtx *svc.ServiceContext, l
|
|||||||
Model(&user.Subscribe{}).
|
Model(&user.Subscribe{}).
|
||||||
Where("user_id = ? AND subscribe_id = ?", ownerUserId, rc.TrialSubscribe).
|
Where("user_id = ? AND subscribe_id = ?", ownerUserId, rc.TrialSubscribe).
|
||||||
Count(&count).Error; err != nil {
|
Count(&count).Error; err != nil {
|
||||||
commonLogic.SubscriptionTraceError(log, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_error",
|
log.Errorw("failed to check existing trial", logger.Field("error", err.Error()))
|
||||||
"[SubscriptionFlow] failed to query existing trial subscription",
|
|
||||||
logger.Field("owner_user_id", ownerUserId),
|
|
||||||
logger.Field("email", email),
|
|
||||||
logger.Field("error", err.Error()),
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
commonLogic.SubscriptionTraceInfo(log, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_skipped",
|
log.Infow("trial already granted, skip",
|
||||||
"[SubscriptionFlow] trial already exists for owner",
|
|
||||||
logger.Field("owner_user_id", ownerUserId),
|
logger.Field("owner_user_id", ownerUserId),
|
||||||
logger.Field("email", email),
|
|
||||||
logger.Field("skip_reason", "trial_already_exists"),
|
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -68,24 +42,16 @@ func tryGrantTrialOnEmailBind(ctx context.Context, svcCtx *svc.ServiceContext, l
|
|||||||
// Cross-user check: prevent the same real inbox (via dot trick / + alias) from
|
// Cross-user check: prevent the same real inbox (via dot trick / + alias) from
|
||||||
// getting multiple trials across different accounts.
|
// getting multiple trials across different accounts.
|
||||||
if auth.NormalizedEmailHasTrial(ctx, svcCtx.DB, email, rc.TrialSubscribe) {
|
if auth.NormalizedEmailHasTrial(ctx, svcCtx.DB, email, rc.TrialSubscribe) {
|
||||||
commonLogic.SubscriptionTraceInfo(log, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_skipped",
|
log.Infow("normalized email already has trial via another account, skip",
|
||||||
"[SubscriptionFlow] normalized email already received a trial elsewhere",
|
|
||||||
logger.Field("email", email),
|
logger.Field("email", email),
|
||||||
logger.Field("owner_user_id", ownerUserId),
|
logger.Field("owner_user_id", ownerUserId),
|
||||||
logger.Field("skip_reason", "normalized_email_has_trial"),
|
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sub, err := svcCtx.SubscribeModel.FindOne(ctx, rc.TrialSubscribe)
|
sub, err := svcCtx.SubscribeModel.FindOne(ctx, rc.TrialSubscribe)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
commonLogic.SubscriptionTraceError(log, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_error",
|
log.Errorw("failed to find trial subscribe template", logger.Field("error", err.Error()))
|
||||||
"[SubscriptionFlow] failed to load trial subscription template",
|
|
||||||
logger.Field("owner_user_id", ownerUserId),
|
|
||||||
logger.Field("email", email),
|
|
||||||
logger.Field("trial_subscribe_id", rc.TrialSubscribe),
|
|
||||||
logger.Field("error", err.Error()),
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,13 +69,9 @@ func tryGrantTrialOnEmailBind(ctx context.Context, svcCtx *svc.ServiceContext, l
|
|||||||
Status: 1,
|
Status: 1,
|
||||||
}
|
}
|
||||||
if err = svcCtx.UserModel.InsertSubscribe(ctx, userSub); err != nil {
|
if err = svcCtx.UserModel.InsertSubscribe(ctx, userSub); err != nil {
|
||||||
commonLogic.SubscriptionTraceError(log, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_error",
|
log.Errorw("failed to insert trial subscribe",
|
||||||
"[SubscriptionFlow] failed to create trial subscription for email bind",
|
|
||||||
append(commonLogic.UserSubscribeTraceFields(userSub),
|
|
||||||
logger.Field("error", err.Error()),
|
logger.Field("error", err.Error()),
|
||||||
logger.Field("owner_user_id", ownerUserId),
|
logger.Field("owner_user_id", ownerUserId),
|
||||||
logger.Field("email", email),
|
|
||||||
)...,
|
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -120,12 +82,9 @@ func tryGrantTrialOnEmailBind(ctx context.Context, svcCtx *svc.ServiceContext, l
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commonLogic.SubscriptionTraceInfo(log, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_succeeded",
|
log.Infow("trial granted on email bind",
|
||||||
"[SubscriptionFlow] trial subscription granted after email bind",
|
|
||||||
append(commonLogic.UserSubscribeTraceFields(userSub),
|
|
||||||
logger.Field("owner_user_id", ownerUserId),
|
logger.Field("owner_user_id", ownerUserId),
|
||||||
logger.Field("email", email),
|
logger.Field("email", email),
|
||||||
logger.Field("trial_subscribe_id", sub.Id),
|
logger.Field("subscribe_id", sub.Id),
|
||||||
)...,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/config"
|
"github.com/perfect-panel/server/internal/config"
|
||||||
commonLogic "github.com/perfect-panel/server/internal/logic/common"
|
|
||||||
"github.com/perfect-panel/server/internal/model/user"
|
"github.com/perfect-panel/server/internal/model/user"
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
"github.com/perfect-panel/server/internal/types"
|
"github.com/perfect-panel/server/internal/types"
|
||||||
@ -40,10 +39,6 @@ type CacheKeyPayload struct {
|
|||||||
|
|
||||||
func (l *VerifyEmailLogic) VerifyEmail(req *types.VerifyEmailRequest) error {
|
func (l *VerifyEmailLogic) VerifyEmail(req *types.VerifyEmailRequest) error {
|
||||||
req.Email = strings.ToLower(strings.TrimSpace(req.Email))
|
req.Email = strings.ToLower(strings.TrimSpace(req.Email))
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "verify_email_start",
|
|
||||||
"[SubscriptionFlow] email verification started",
|
|
||||||
logger.Field("email", req.Email),
|
|
||||||
)
|
|
||||||
cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.Security, req.Email)
|
cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.Security, req.Email)
|
||||||
value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result()
|
value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -64,10 +59,6 @@ func (l *VerifyEmailLogic) VerifyEmail(req *types.VerifyEmailRequest) error {
|
|||||||
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code expired")
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code expired")
|
||||||
}
|
}
|
||||||
l.svcCtx.Redis.Del(l.ctx, cacheKey)
|
l.svcCtx.Redis.Del(l.ctx, cacheKey)
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "verify_email_code_verified",
|
|
||||||
"[SubscriptionFlow] email verification code accepted",
|
|
||||||
logger.Field("email", req.Email),
|
|
||||||
)
|
|
||||||
|
|
||||||
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -86,12 +77,6 @@ func (l *VerifyEmailLogic) VerifyEmail(req *types.VerifyEmailRequest) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "UpdateUserAuthMethods error")
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "UpdateUserAuthMethods error")
|
||||||
}
|
}
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "verify_email_completed",
|
|
||||||
"[SubscriptionFlow] email verification completed and trial evaluation will run",
|
|
||||||
logger.Field("user_id", u.Id),
|
|
||||||
logger.Field("owner_user_id", method.UserId),
|
|
||||||
logger.Field("email", req.Email),
|
|
||||||
)
|
|
||||||
tryGrantTrialOnEmailBind(l.ctx, l.svcCtx, l.Logger, method.UserId, req.Email)
|
tryGrantTrialOnEmailBind(l.ctx, l.svcCtx, l.Logger, method.UserId, req.Email)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -447,8 +447,7 @@ func (m *customUserModel) QueryMonthlyUserStatisticsList(ctx context.Context, da
|
|||||||
func (m *customUserModel) FindActiveSubscribe(ctx context.Context, userId int64) (*Subscribe, error) {
|
func (m *customUserModel) FindActiveSubscribe(ctx context.Context, userId int64) (*Subscribe, error) {
|
||||||
var subscribe Subscribe
|
var subscribe Subscribe
|
||||||
err := m.QueryNoCacheCtx(ctx, &subscribe, func(conn *gorm.DB, v interface{}) error {
|
err := m.QueryNoCacheCtx(ctx, &subscribe, func(conn *gorm.DB, v interface{}) error {
|
||||||
now := time.Now()
|
return conn.Where("user_id = ? AND status IN (0, 1) AND expire_time > ?", userId, 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").
|
Order("expire_time DESC").
|
||||||
First(v).Error
|
First(v).Error
|
||||||
})
|
})
|
||||||
|
|||||||
@ -36,13 +36,9 @@ func NewService(svc *svc.ServiceContext) *Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func initServer(svc *svc.ServiceContext) *gin.Engine {
|
func initServer(svc *svc.ServiceContext) *gin.Engine {
|
||||||
|
|
||||||
// start init system config
|
// start init system config
|
||||||
initStart := time.Now()
|
|
||||||
logger.Info("system initialization start")
|
|
||||||
initialize.StartInitSystemConfig(svc)
|
initialize.StartInitSystemConfig(svc)
|
||||||
logger.Infow("system initialization complete",
|
|
||||||
logger.Field("duration", time.Since(initStart).String()),
|
|
||||||
)
|
|
||||||
// init gin server
|
// init gin server
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
r.RemoteIPHeaders = []string{"X-Original-Forwarded-For", "X-Forwarded-For", "X-Real-IP"}
|
r.RemoteIPHeaders = []string{"X-Original-Forwarded-For", "X-Forwarded-For", "X-Real-IP"}
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type GormLogger struct {
|
type GormLogger struct {
|
||||||
SlowThreshold time.Duration
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TAG = "[GORM]"
|
const TAG = "[GORM]"
|
||||||
@ -28,25 +27,24 @@ func (l *GormLogger) LogMode(logger.LogLevel) logger.Interface {
|
|||||||
default:
|
default:
|
||||||
sysLevel = "unknown"
|
sysLevel = "unknown"
|
||||||
}
|
}
|
||||||
Debugf("%s System Log Level is %s", TAG, sysLevel)
|
Infof("%s System Log Level is %s", TAG, sysLevel)
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *GormLogger) Info(ctx context.Context, str string, args ...interface{}) {
|
func (l *GormLogger) Info(ctx context.Context, str string, args ...interface{}) {
|
||||||
WithContext(ctx).WithCallerSkip(6).Debugf("%s Info: %s", TAG, fmt.Sprintf(str, args...))
|
WithContext(ctx).WithCallerSkip(6).Infof("%s Info: %s", TAG, str, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *GormLogger) Warn(ctx context.Context, str string, args ...interface{}) {
|
func (l *GormLogger) Warn(ctx context.Context, str string, args ...interface{}) {
|
||||||
WithContext(ctx).WithCallerSkip(6).Debugf("%s Warn: %s", TAG, fmt.Sprintf(str, args...))
|
WithContext(ctx).WithCallerSkip(6).Infof("%s Warn: %s", TAG, str, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *GormLogger) Error(ctx context.Context, str string, args ...interface{}) {
|
func (l *GormLogger) Error(ctx context.Context, str string, args ...interface{}) {
|
||||||
WithContext(ctx).WithCallerSkip(6).Errorf("%s Error: %s", TAG, fmt.Sprintf(str, args...))
|
WithContext(ctx).WithCallerSkip(6).Errorf("%s Error: %s", TAG, str, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
|
func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
|
||||||
sql, rowsAffected := fc()
|
sql, rowsAffected := fc()
|
||||||
duration := time.Since(begin)
|
|
||||||
fields := []LogField{
|
fields := []LogField{
|
||||||
{
|
{
|
||||||
Key: "sql",
|
Key: "sql",
|
||||||
@ -62,16 +60,8 @@ func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql
|
|||||||
Key: "error",
|
Key: "error",
|
||||||
Value: err.Error(),
|
Value: err.Error(),
|
||||||
})
|
})
|
||||||
WithContext(ctx).WithCallerSkip(6).WithDuration(duration).Errorw(TAG, fields...)
|
WithContext(ctx).WithCallerSkip(6).WithDuration(time.Since(begin)).Errorw(TAG, fields...)
|
||||||
return
|
} else {
|
||||||
}
|
WithContext(ctx).WithCallerSkip(6).WithDuration(time.Since(begin)).Infow(fmt.Sprintf("%s SQL Executed", TAG), fields...)
|
||||||
|
|
||||||
if l.SlowThreshold > 0 && duration >= l.SlowThreshold {
|
|
||||||
WithContext(ctx).WithCallerSkip(6).WithDuration(duration).Sloww(fmt.Sprintf("%s SQL Slow", TAG), fields...)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if shallLog(DebugLevel) {
|
|
||||||
WithContext(ctx).WithCallerSkip(6).WithDuration(duration).Debugw(fmt.Sprintf("%s SQL Executed", TAG), fields...)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,9 +46,7 @@ func ConnectMysql(m Mysql) (*gorm.DB, error) {
|
|||||||
DSN: m.Dsn(),
|
DSN: m.Dsn(),
|
||||||
}
|
}
|
||||||
db, err := gorm.Open(mysql.New(mysqlCfg), &gorm.Config{
|
db, err := gorm.Open(mysql.New(mysqlCfg), &gorm.Config{
|
||||||
Logger: &logger.GormLogger{
|
Logger: new(logger.GormLogger),
|
||||||
SlowThreshold: m.GetSlowThreshold(),
|
|
||||||
},
|
|
||||||
NamingStrategy: schema.NamingStrategy{
|
NamingStrategy: schema.NamingStrategy{
|
||||||
SingularTable: true,
|
SingularTable: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -28,7 +28,6 @@ import (
|
|||||||
"github.com/perfect-panel/server/pkg/uuidx"
|
"github.com/perfect-panel/server/pkg/uuidx"
|
||||||
queueTypes "github.com/perfect-panel/server/queue/types"
|
queueTypes "github.com/perfect-panel/server/queue/types"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Order type constants define the different types of orders that can be processed
|
// Order type constants define the different types of orders that can be processed
|
||||||
@ -72,10 +71,8 @@ func NewActivateOrderLogic(svc *svc.ServiceContext) *ActivateOrderLogic {
|
|||||||
// It handles the complete workflow of activating a paid order including validation,
|
// It handles the complete workflow of activating a paid order including validation,
|
||||||
// processing based on order type, and finalization.
|
// processing based on order type, and finalization.
|
||||||
func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task) error {
|
func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task) error {
|
||||||
commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "activation_task_received",
|
logger.WithContext(ctx).Info("[ActivateOrderLogic] 开始处理订单激活任务",
|
||||||
"[SubscriptionFlow] activation task received",
|
logger.Field("payload", string(task.Payload())))
|
||||||
logger.Field("payload", string(task.Payload())),
|
|
||||||
)
|
|
||||||
|
|
||||||
payload, err := l.parsePayload(ctx, task.Payload())
|
payload, err := l.parsePayload(ctx, task.Payload())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -84,11 +81,8 @@ func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task)
|
|||||||
return nil // payload 解析失败不重试,因为重试也会失败
|
return nil // payload 解析失败不重试,因为重试也会失败
|
||||||
}
|
}
|
||||||
|
|
||||||
commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "activation_order_lookup",
|
logger.WithContext(ctx).Info("[ActivateOrderLogic] 正在验证订单",
|
||||||
"[SubscriptionFlow] activation task is loading order",
|
logger.Field("order_no", payload.OrderNo))
|
||||||
logger.Field("order_no", payload.OrderNo),
|
|
||||||
logger.Field("iap_expire_at", payload.IAPExpireAt),
|
|
||||||
)
|
|
||||||
|
|
||||||
orderInfo, err := l.claimAndGetOrder(ctx, payload.OrderNo)
|
orderInfo, err := l.claimAndGetOrder(ctx, payload.OrderNo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -110,10 +104,10 @@ func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "activation_order_claimed",
|
logger.WithContext(ctx).Info("[ActivateOrderLogic] 订单验证通过,开始处理",
|
||||||
"[SubscriptionFlow] activation worker claimed paid order",
|
logger.Field("order_no", orderInfo.OrderNo),
|
||||||
commonLogic.OrderTraceFields(orderInfo)...,
|
logger.Field("order_type", orderInfo.Type),
|
||||||
)
|
logger.Field("user_id", orderInfo.UserId))
|
||||||
|
|
||||||
if err = l.processOrderByType(ctx, orderInfo, payload.IAPExpireAt); err != nil {
|
if err = l.processOrderByType(ctx, orderInfo, payload.IAPExpireAt); err != nil {
|
||||||
l.releaseClaim(ctx, orderInfo.OrderNo)
|
l.releaseClaim(ctx, orderInfo.OrderNo)
|
||||||
@ -124,21 +118,12 @@ func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task)
|
|||||||
return err // 返回 err 允许 asynq 重试
|
return err // 返回 err 允许 asynq 重试
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = l.reconcilePostOrderSubscriptions(ctx, orderInfo); err != nil {
|
|
||||||
l.releaseClaim(ctx, orderInfo.OrderNo)
|
|
||||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] 订单订阅兜底合并失败,将重试",
|
|
||||||
logger.Field("order_no", orderInfo.OrderNo),
|
|
||||||
logger.Field("order_type", orderInfo.Type),
|
|
||||||
logger.Field("error", err.Error()))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
l.finalizeCouponAndOrder(ctx, orderInfo)
|
l.finalizeCouponAndOrder(ctx, orderInfo)
|
||||||
|
|
||||||
commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "activation_finished",
|
logger.WithContext(ctx).Info("[ActivateOrderLogic] 订单激活成功",
|
||||||
"[SubscriptionFlow] order activation completed",
|
logger.Field("order_no", orderInfo.OrderNo),
|
||||||
commonLogic.OrderTraceFields(orderInfo)...,
|
logger.Field("order_type", orderInfo.Type),
|
||||||
)
|
logger.Field("user_id", orderInfo.UserId))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,311 +217,6 @@ func (l *ActivateOrderLogic) processOrderByType(ctx context.Context, orderInfo *
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *ActivateOrderLogic) reconcilePostOrderSubscriptions(ctx context.Context, orderInfo *order.Order) error {
|
|
||||||
if !shouldReconcilePostOrderSubscriptions(orderInfo) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
effectiveUserID := orderInfo.UserId
|
|
||||||
if orderInfo.SubscriptionUserId > 0 {
|
|
||||||
effectiveUserID = orderInfo.SubscriptionUserId
|
|
||||||
}
|
|
||||||
if effectiveUserID == 0 || orderInfo.Id == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
survivor user.Subscribe
|
|
||||||
survivorBefore user.Subscribe
|
|
||||||
losers []user.Subscribe
|
|
||||||
mergedIDs []int64
|
|
||||||
subscribeIDsToClear = make(map[int64]struct{})
|
|
||||||
missingSurvivor bool
|
|
||||||
ownerMismatchSkipped bool
|
|
||||||
identitySourceID int64
|
|
||||||
)
|
|
||||||
|
|
||||||
err := l.svc.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
||||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
|
||||||
Model(&user.Subscribe{}).
|
|
||||||
Where("order_id = ?", orderInfo.Id).
|
|
||||||
First(&survivor).Error; err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
missingSurvivor = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
survivorBefore = survivor
|
|
||||||
|
|
||||||
var ownerSubs []user.Subscribe
|
|
||||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
|
||||||
Model(&user.Subscribe{}).
|
|
||||||
Where("user_id = ?", effectiveUserID).
|
|
||||||
Order("id ASC").
|
|
||||||
Find(&ownerSubs).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if survivor.UserId != effectiveUserID {
|
|
||||||
if len(ownerSubs) == 0 {
|
|
||||||
ownerMismatchSkipped = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Model(&user.Subscribe{}).
|
|
||||||
Where("id = ?", survivor.Id).
|
|
||||||
Update("user_id", effectiveUserID).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
survivor.UserId = effectiveUserID
|
|
||||||
|
|
||||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
|
||||||
Model(&user.Subscribe{}).
|
|
||||||
Where("user_id = ?", effectiveUserID).
|
|
||||||
Order("id ASC").
|
|
||||||
Find(&ownerSubs).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ownerSubs) <= 1 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
maxExpire := survivor.ExpireTime
|
|
||||||
for i := range ownerSubs {
|
|
||||||
item := ownerSubs[i]
|
|
||||||
if item.Id == survivor.Id {
|
|
||||||
if item.ExpireTime.After(maxExpire) {
|
|
||||||
maxExpire = item.ExpireTime
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
losers = append(losers, item)
|
|
||||||
mergedIDs = append(mergedIDs, item.Id)
|
|
||||||
if item.ExpireTime.After(maxExpire) {
|
|
||||||
maxExpire = item.ExpireTime
|
|
||||||
}
|
|
||||||
if item.SubscribeId > 0 {
|
|
||||||
subscribeIDsToClear[item.SubscribeId] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(losers) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if survivor.SubscribeId > 0 {
|
|
||||||
subscribeIDsToClear[survivor.SubscribeId] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
identitySource := pickSubscriptionIdentitySource(losers)
|
|
||||||
if identitySource != nil {
|
|
||||||
identitySourceID = identitySource.Id
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFields := map[string]interface{}{
|
|
||||||
"status": 1,
|
|
||||||
"finished_at": nil,
|
|
||||||
}
|
|
||||||
if maxExpire.After(survivor.ExpireTime) {
|
|
||||||
survivor.ExpireTime = maxExpire
|
|
||||||
updateFields["expire_time"] = maxExpire
|
|
||||||
}
|
|
||||||
if identitySource != nil {
|
|
||||||
if identitySource.Token != "" {
|
|
||||||
survivor.Token = identitySource.Token
|
|
||||||
updateFields["token"] = identitySource.Token
|
|
||||||
}
|
|
||||||
if identitySource.UUID != "" {
|
|
||||||
survivor.UUID = identitySource.UUID
|
|
||||||
updateFields["uuid"] = identitySource.UUID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loserIDs := make([]int64, 0, len(losers))
|
|
||||||
for i := range losers {
|
|
||||||
loserIDs = append(loserIDs, losers[i].Id)
|
|
||||||
}
|
|
||||||
if len(loserIDs) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// user_subscribe 当前没有 deleted_at 字段,这里沿用项目现有删除语义清理 loser 记录。
|
|
||||||
if err := tx.Where("id IN ?", loserIDs).Delete(&user.Subscribe{}).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Model(&user.Subscribe{}).
|
|
||||||
Where("id = ?", survivor.Id).
|
|
||||||
Updates(updateFields).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
survivor.Status = 1
|
|
||||||
survivor.FinishedAt = nil
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if missingSurvivor {
|
|
||||||
commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "post_order_reconcile_skipped",
|
|
||||||
"[SubscriptionFlow] post-order reconcile skipped because survivor subscription was not found",
|
|
||||||
append(commonLogic.OrderTraceFields(orderInfo),
|
|
||||||
logger.Field("reason", "post_order_reconcile"),
|
|
||||||
logger.Field("effective_user_id", effectiveUserID),
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if ownerMismatchSkipped {
|
|
||||||
commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "post_order_reconcile_skipped",
|
|
||||||
"[SubscriptionFlow] post-order reconcile skipped because survivor owner mismatch had no duplicates",
|
|
||||||
append(commonLogic.OrderTraceFields(orderInfo),
|
|
||||||
logger.Field("reason", "post_order_reconcile"),
|
|
||||||
logger.Field("effective_user_id", effectiveUserID),
|
|
||||||
logger.Field("survivor_subscribe_id", survivor.Id),
|
|
||||||
logger.Field("survivor_user_id", survivorBefore.UserId),
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(losers) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
l.clearPostOrderReconcileCache(ctx, &survivorBefore, &survivor, losers, subscribeIDsToClear)
|
|
||||||
|
|
||||||
commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "post_order_reconciled",
|
|
||||||
"[SubscriptionFlow] post-order reconcile merged duplicate subscriptions",
|
|
||||||
append(commonLogic.OrderTraceFields(orderInfo),
|
|
||||||
logger.Field("reason", "post_order_reconcile"),
|
|
||||||
logger.Field("effective_user_id", effectiveUserID),
|
|
||||||
logger.Field("survivor_subscribe_id", survivor.Id),
|
|
||||||
logger.Field("identity_source_subscribe_id", identitySourceID),
|
|
||||||
logger.Field("merged_subscribe_ids", mergedIDs),
|
|
||||||
logger.Field("merged_count", len(mergedIDs)),
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func shouldReconcilePostOrderSubscriptions(orderInfo *order.Order) bool {
|
|
||||||
if orderInfo == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
switch orderInfo.Type {
|
|
||||||
case OrderTypeSubscribe, OrderTypeRenewal, OrderTypeRedemption:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func pickSubscriptionIdentitySource(candidates []user.Subscribe) *user.Subscribe {
|
|
||||||
if len(candidates) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
best := &candidates[0]
|
|
||||||
for i := 1; i < len(candidates); i++ {
|
|
||||||
candidate := &candidates[i]
|
|
||||||
if subscriptionIdentityPriority(candidate, best) {
|
|
||||||
best = candidate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return best
|
|
||||||
}
|
|
||||||
|
|
||||||
func subscriptionIdentityPriority(candidate *user.Subscribe, current *user.Subscribe) bool {
|
|
||||||
if candidate == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if current == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
candidateUsable := candidate.Token != "" || candidate.UUID != ""
|
|
||||||
currentUsable := current.Token != "" || current.UUID != ""
|
|
||||||
if candidateUsable != currentUsable {
|
|
||||||
return candidateUsable
|
|
||||||
}
|
|
||||||
|
|
||||||
if candidate.ExpireTime.After(current.ExpireTime) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if current.ExpireTime.After(candidate.ExpireTime) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if candidate.UpdatedAt.After(current.UpdatedAt) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if current.UpdatedAt.After(candidate.UpdatedAt) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return candidate.Id > current.Id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *ActivateOrderLogic) clearPostOrderReconcileCache(
|
|
||||||
ctx context.Context,
|
|
||||||
survivorBefore *user.Subscribe,
|
|
||||||
survivorAfter *user.Subscribe,
|
|
||||||
losers []user.Subscribe,
|
|
||||||
subscribeIDs map[int64]struct{},
|
|
||||||
) {
|
|
||||||
cacheModels := make([]*user.Subscribe, 0, len(losers)+2)
|
|
||||||
if survivorBefore != nil {
|
|
||||||
cacheModels = append(cacheModels, survivorBefore)
|
|
||||||
}
|
|
||||||
if survivorAfter != nil {
|
|
||||||
cacheModels = append(cacheModels, survivorAfter)
|
|
||||||
}
|
|
||||||
for i := range losers {
|
|
||||||
loser := losers[i]
|
|
||||||
cacheModels = append(cacheModels, &loser)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cacheModels) > 0 {
|
|
||||||
if err := l.svc.UserModel.ClearSubscribeCache(ctx, cacheModels...); err != nil {
|
|
||||||
logger.WithContext(ctx).Error("Post-order reconcile clear subscribe cache failed",
|
|
||||||
logger.Field("reason", "post_order_reconcile"),
|
|
||||||
logger.Field("error", err.Error()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if l.svc.SubscribeModel != nil {
|
|
||||||
for subscribeID := range subscribeIDs {
|
|
||||||
if err := l.svc.SubscribeModel.ClearCache(ctx, subscribeID); err != nil {
|
|
||||||
logger.WithContext(ctx).Error("Post-order reconcile clear plan cache failed",
|
|
||||||
logger.Field("reason", "post_order_reconcile"),
|
|
||||||
logger.Field("subscribe_id", subscribeID),
|
|
||||||
logger.Field("error", err.Error()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if l.svc.NodeModel != nil {
|
|
||||||
if err := l.svc.NodeModel.ClearServerAllCache(ctx); err != nil {
|
|
||||||
logger.WithContext(ctx).Error("Post-order reconcile clear node cache failed",
|
|
||||||
logger.Field("reason", "post_order_reconcile"),
|
|
||||||
logger.Field("error", err.Error()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// finalizeCouponAndOrder handles post-processing tasks including coupon updates
|
// finalizeCouponAndOrder handles post-processing tasks including coupon updates
|
||||||
// and order status finalization
|
// and order status finalization
|
||||||
func (l *ActivateOrderLogic) finalizeCouponAndOrder(ctx context.Context, orderInfo *order.Order) {
|
func (l *ActivateOrderLogic) finalizeCouponAndOrder(ctx context.Context, orderInfo *order.Order) {
|
||||||
@ -558,10 +238,6 @@ func (l *ActivateOrderLogic) finalizeCouponAndOrder(ctx context.Context, orderIn
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
orderInfo.Status = OrderStatusFinished
|
orderInfo.Status = OrderStatusFinished
|
||||||
commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "order_status_finished",
|
|
||||||
"[SubscriptionFlow] order status updated to finished",
|
|
||||||
commonLogic.OrderTraceFields(orderInfo)...,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPurchase handles new subscription purchase including user creation,
|
// NewPurchase handles new subscription purchase including user creation,
|
||||||
@ -572,13 +248,6 @@ func (l *ActivateOrderLogic) NewPurchase(ctx context.Context, orderInfo *order.O
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "activation_user_resolved",
|
|
||||||
"[SubscriptionFlow] activation resolved subscription recipient user",
|
|
||||||
append(commonLogic.OrderTraceFields(orderInfo),
|
|
||||||
logger.Field("resolved_user_id", userInfo.Id),
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
|
|
||||||
sub, err := l.getSubscribeInfo(ctx, orderInfo.SubscribeId)
|
sub, err := l.getSubscribeInfo(ctx, orderInfo.SubscribeId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -619,14 +288,12 @@ func (l *ActivateOrderLogic) NewPurchase(ctx context.Context, orderInfo *order.O
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
userSub = anchorSub
|
userSub = anchorSub
|
||||||
commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "subscription_reused",
|
logger.WithContext(ctx).Infow("Single mode purchase routed to renewal in activation",
|
||||||
"[SubscriptionFlow] activation reused single-mode anchor subscription",
|
logger.Field("mode", "single"),
|
||||||
append(commonLogic.OrderTraceFields(orderInfo),
|
logger.Field("route", "purchase_to_renewal"),
|
||||||
append(commonLogic.UserSubscribeTraceFields(anchorSub),
|
|
||||||
logger.Field("reuse_reason", "single_mode_purchase_to_renewal"),
|
|
||||||
logger.Field("plan_changed", anchorSub.SubscribeId != orderInfo.SubscribeId),
|
logger.Field("plan_changed", anchorSub.SubscribeId != orderInfo.SubscribeId),
|
||||||
)...,
|
logger.Field("anchor_user_subscribe_id", anchorSub.Id),
|
||||||
)...,
|
logger.Field("order_no", orderInfo.OrderNo),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
case errors.Is(anchorErr, gorm.ErrRecordNotFound):
|
case errors.Is(anchorErr, gorm.ErrRecordNotFound):
|
||||||
@ -692,15 +359,11 @@ func (l *ActivateOrderLogic) NewPurchase(ctx context.Context, orderInfo *order.O
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
userSub = &existingSub
|
userSub = &existingSub
|
||||||
commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "subscription_reused",
|
logger.WithContext(ctx).Infow("Fallback: renewed existing subscription instead of creating duplicate",
|
||||||
"[SubscriptionFlow] activation renewed an existing subscription instead of creating a duplicate",
|
logger.Field("existing_subscribe_id", existingSub.Id),
|
||||||
append(commonLogic.OrderTraceFields(orderInfo),
|
logger.Field("order_no", orderInfo.OrderNo),
|
||||||
append(commonLogic.UserSubscribeTraceFields(&existingSub),
|
|
||||||
logger.Field("reuse_reason", "fallback_existing_subscription"),
|
|
||||||
logger.Field("candidate_user_ids", candidateUserIds),
|
logger.Field("candidate_user_ids", candidateUserIds),
|
||||||
logger.Field("owner_corrected_to", effectiveOwner),
|
logger.Field("owner_corrected_to", effectiveOwner),
|
||||||
)...,
|
|
||||||
)...,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -723,12 +386,7 @@ func (l *ActivateOrderLogic) NewPurchase(ctx context.Context, orderInfo *order.O
|
|||||||
// Clear cache
|
// Clear cache
|
||||||
l.clearServerCache(ctx, sub)
|
l.clearServerCache(ctx, sub)
|
||||||
|
|
||||||
commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "subscription_issued",
|
logger.WithContext(ctx).Info("Insert user subscribe success")
|
||||||
"[SubscriptionFlow] activation finished issuing subscription entitlement",
|
|
||||||
append(commonLogic.OrderTraceFields(orderInfo),
|
|
||||||
commonLogic.UserSubscribeTraceFields(userSub)...,
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -799,14 +457,6 @@ func (l *ActivateOrderLogic) createGuestUser(ctx context.Context, orderInfo *ord
|
|||||||
logger.Field("identifier", tempOrder.Identifier),
|
logger.Field("identifier", tempOrder.Identifier),
|
||||||
logger.Field("auth_type", tempOrder.AuthType),
|
logger.Field("auth_type", tempOrder.AuthType),
|
||||||
)
|
)
|
||||||
commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "guest_user_created",
|
|
||||||
"[SubscriptionFlow] guest user created during order activation",
|
|
||||||
append(commonLogic.OrderTraceFields(orderInfo),
|
|
||||||
logger.Field("created_user_id", userInfo.Id),
|
|
||||||
logger.Field("identifier", tempOrder.Identifier),
|
|
||||||
logger.Field("auth_type", tempOrder.AuthType),
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
|
|
||||||
return userInfo, nil
|
return userInfo, nil
|
||||||
}
|
}
|
||||||
@ -920,13 +570,6 @@ func (l *ActivateOrderLogic) createUserSubscription(ctx context.Context, orderIn
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "subscription_created",
|
|
||||||
"[SubscriptionFlow] new user subscription record created",
|
|
||||||
append(commonLogic.OrderTraceFields(orderInfo),
|
|
||||||
commonLogic.UserSubscribeTraceFields(userSub)...,
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
|
|
||||||
return userSub, nil
|
return userSub, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -979,15 +622,11 @@ func (l *ActivateOrderLogic) extendGiftSubscription(ctx context.Context, giftSub
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "subscription_reused",
|
logger.WithContext(ctx).Info("Extended gift subscription successfully",
|
||||||
"[SubscriptionFlow] paid order extended an existing gift subscription",
|
logger.Field("subscribe_id", giftSub.Id),
|
||||||
append(commonLogic.OrderTraceFields(orderInfo),
|
|
||||||
append(commonLogic.UserSubscribeTraceFields(giftSub),
|
|
||||||
logger.Field("reuse_reason", "gift_subscription_promoted"),
|
|
||||||
logger.Field("old_expire_time", baseTime),
|
logger.Field("old_expire_time", baseTime),
|
||||||
logger.Field("new_expire_time", newExpireTime),
|
logger.Field("new_expire_time", newExpireTime),
|
||||||
)...,
|
logger.Field("order_id", orderInfo.Id),
|
||||||
)...,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return giftSub, nil
|
return giftSub, nil
|
||||||
@ -1090,9 +729,7 @@ func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *use
|
|||||||
// 有佣金路径:邀请人拿佣金,被邀请用户(首单)拿天数
|
// 有佣金路径:邀请人拿佣金,被邀请用户(首单)拿天数
|
||||||
if orderInfo.IsNew {
|
if orderInfo.IsNew {
|
||||||
giftTarget := l.resolveGiftTargetUser(ctx, userInfo, orderInfo.SubscriptionUserId)
|
giftTarget := l.resolveGiftTargetUser(ctx, userInfo, orderInfo.SubscriptionUserId)
|
||||||
if giftErr := l.grantGiftDays(ctx, giftTarget, int(l.svc.Config.Invite.GiftDays), orderInfo.OrderNo, "邀请赠送"); giftErr != nil {
|
_ = l.grantGiftDays(ctx, giftTarget, int(l.svc.Config.Invite.GiftDays), orderInfo.OrderNo, "邀请赠送")
|
||||||
l.logGiftDaysError(ctx, giftErr, giftTarget, orderInfo, "commission_path_referee")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1102,9 +739,7 @@ func (l *ActivateOrderLogic) grantGiftDaysToBothParties(ctx context.Context, ref
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
refereeTarget := l.resolveGiftTargetUser(ctx, referee, orderInfo.SubscriptionUserId)
|
refereeTarget := l.resolveGiftTargetUser(ctx, referee, orderInfo.SubscriptionUserId)
|
||||||
if err := l.grantGiftDays(ctx, refereeTarget, int(giftDays), orderInfo.OrderNo, "邀请赠送"); err != nil {
|
_ = l.grantGiftDays(ctx, refereeTarget, int(giftDays), orderInfo.OrderNo, "邀请赠送")
|
||||||
l.logGiftDaysError(ctx, err, refereeTarget, orderInfo, "no_commission_referee")
|
|
||||||
}
|
|
||||||
if referee.RefererId == 0 {
|
if referee.RefererId == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1113,9 +748,7 @@ func (l *ActivateOrderLogic) grantGiftDaysToBothParties(ctx context.Context, ref
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
refererTarget := l.resolveGiftTargetUser(ctx, referer, 0)
|
refererTarget := l.resolveGiftTargetUser(ctx, referer, 0)
|
||||||
if err = l.grantGiftDays(ctx, refererTarget, int(giftDays), orderInfo.OrderNo, "邀请赠送"); err != nil {
|
_ = l.grantGiftDays(ctx, refererTarget, int(giftDays), orderInfo.OrderNo, "邀请赠送")
|
||||||
l.logGiftDaysError(ctx, err, refererTarget, orderInfo, "no_commission_referer")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *ActivateOrderLogic) resolveGiftTargetUser(ctx context.Context, source *user.User, forcedOwnerID int64) *user.User {
|
func (l *ActivateOrderLogic) resolveGiftTargetUser(ctx context.Context, source *user.User, forcedOwnerID int64) *user.User {
|
||||||
@ -1142,26 +775,6 @@ func (l *ActivateOrderLogic) resolveGiftTargetUser(ctx context.Context, source *
|
|||||||
return target
|
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 {
|
func (l *ActivateOrderLogic) grantGiftDays(ctx context.Context, u *user.User, days int, orderNo string, remark string) error {
|
||||||
if u == nil || days <= 0 {
|
if u == nil || days <= 0 {
|
||||||
return nil
|
return nil
|
||||||
@ -1205,9 +818,7 @@ func (l *ActivateOrderLogic) grantGiftDays(ctx context.Context, u *user.User, da
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !activeSubscribe.ExpireTime.Equal(time.UnixMilli(0)) {
|
|
||||||
activeSubscribe.ExpireTime = activeSubscribe.ExpireTime.Add(time.Duration(days) * 24 * time.Hour)
|
activeSubscribe.ExpireTime = activeSubscribe.ExpireTime.Add(time.Duration(days) * 24 * time.Hour)
|
||||||
}
|
|
||||||
err = l.svc.UserModel.UpdateSubscribe(ctx, activeSubscribe)
|
err = l.svc.UserModel.UpdateSubscribe(ctx, activeSubscribe)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -1386,15 +997,6 @@ func (l *ActivateOrderLogic) Renewal(ctx context.Context, orderInfo *order.Order
|
|||||||
// Handle commission
|
// Handle commission
|
||||||
go l.handleCommission(context.Background(), userInfo, orderInfo)
|
go l.handleCommission(context.Background(), userInfo, orderInfo)
|
||||||
|
|
||||||
commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "subscription_renewed",
|
|
||||||
"[SubscriptionFlow] renewal order updated existing subscription",
|
|
||||||
append(commonLogic.OrderTraceFields(orderInfo),
|
|
||||||
append(commonLogic.UserSubscribeTraceFields(userSub),
|
|
||||||
logger.Field("iap_expire_at", iapExpireAt),
|
|
||||||
)...,
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -313,293 +312,6 @@ func TestInviteFlow_OrderThenBind_NoGiftDays(t *testing.T) {
|
|||||||
assertExpireIncreasedByDays(t, db, refererSub.Id, baseExpire, 0)
|
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 TestHandleCommission_IdempotentForRepeatedActivation(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
|
|
||||||
|
|
||||||
baseExpire := time.Now().Add(96 * time.Hour).Truncate(time.Second)
|
|
||||||
refereeSub := seedActiveSubscribe(t, db, referee.Id, baseExpire)
|
|
||||||
orderInfo := &order.Order{
|
|
||||||
OrderNo: "ORD-IDEMPOTENT-001",
|
|
||||||
Type: OrderTypeSubscribe,
|
|
||||||
IsNew: true,
|
|
||||||
Amount: 100,
|
|
||||||
FeeAmount: 0,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
logic.handleCommission(context.Background(), referee, orderInfo)
|
|
||||||
logic.handleCommission(context.Background(), referee, orderInfo)
|
|
||||||
|
|
||||||
assertExpireIncreasedByDays(t, db, refereeSub.Id, baseExpire, 2)
|
|
||||||
assertUserCommission(t, db, referer.Id, 10)
|
|
||||||
assertLogCountForOrder(t, db, modelLog.TypeCommission.Uint8(), orderInfo.OrderNo, 1)
|
|
||||||
assertLogCountForOrder(t, db, modelLog.TypeGift.Uint8(), orderInfo.OrderNo, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandleCommission_NoActiveSubscriptionWritesSkippedGiftLog(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
|
|
||||||
orderInfo := &order.Order{
|
|
||||||
OrderNo: "ORD-SKIPPED-001",
|
|
||||||
Type: OrderTypeSubscribe,
|
|
||||||
IsNew: true,
|
|
||||||
Amount: 100,
|
|
||||||
FeeAmount: 0,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
logic.handleCommission(context.Background(), referee, orderInfo)
|
|
||||||
|
|
||||||
assertUserCommission(t, db, referer.Id, 10)
|
|
||||||
assertLogCountForOrder(t, db, modelLog.TypeCommission.Uint8(), orderInfo.OrderNo, 1)
|
|
||||||
assertLogCountForOrder(t, db, modelLog.TypeGift.Uint8(), orderInfo.OrderNo, 1)
|
|
||||||
assertGiftLogRemarkContains(t, db, orderInfo.OrderNo, "skipped: no active subscription")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandleCommission_GiftDaysZeroDoesNotWriteGiftLog(t *testing.T) {
|
|
||||||
logic, db, cleanup := setupInviteTestLogic(t, config.InviteConfig{
|
|
||||||
ReferralPercentage: 10,
|
|
||||||
OnlyFirstPurchase: false,
|
|
||||||
GiftDays: 0,
|
|
||||||
})
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
referee := seedUser(t, db, 0, false)
|
|
||||||
referer := seedUser(t, db, 0, false)
|
|
||||||
referee.RefererId = referer.Id
|
|
||||||
baseExpire := time.Now().Add(96 * time.Hour).Truncate(time.Second)
|
|
||||||
refereeSub := seedActiveSubscribe(t, db, referee.Id, baseExpire)
|
|
||||||
orderInfo := &order.Order{
|
|
||||||
OrderNo: "ORD-GIFT-ZERO-001",
|
|
||||||
Type: OrderTypeSubscribe,
|
|
||||||
IsNew: true,
|
|
||||||
Amount: 100,
|
|
||||||
FeeAmount: 0,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
logic.handleCommission(context.Background(), referee, orderInfo)
|
|
||||||
|
|
||||||
assertExpireIncreasedByDays(t, db, refereeSub.Id, baseExpire, 0)
|
|
||||||
assertUserCommission(t, db, referer.Id, 10)
|
|
||||||
assertLogCountForOrder(t, db, modelLog.TypeCommission.Uint8(), orderInfo.OrderNo, 1)
|
|
||||||
assertLogCountForOrder(t, db, modelLog.TypeGift.Uint8(), orderInfo.OrderNo, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandleCommission_GlobalOnlyFirstPurchaseBlocksCommissionAndGiftForNonFirstOrder(t *testing.T) {
|
|
||||||
logic, db, cleanup := setupInviteTestLogic(t, config.InviteConfig{
|
|
||||||
ReferralPercentage: 10,
|
|
||||||
OnlyFirstPurchase: true,
|
|
||||||
GiftDays: 2,
|
|
||||||
})
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
referee := seedUser(t, db, 0, false)
|
|
||||||
referer := seedUser(t, db, 0, false)
|
|
||||||
referee.RefererId = referer.Id
|
|
||||||
baseExpire := time.Now().Add(96 * time.Hour).Truncate(time.Second)
|
|
||||||
refereeSub := seedActiveSubscribe(t, db, referee.Id, baseExpire)
|
|
||||||
orderInfo := &order.Order{
|
|
||||||
OrderNo: "ORD-NONFIRST-GLOBAL-001",
|
|
||||||
Type: OrderTypeRenewal,
|
|
||||||
IsNew: false,
|
|
||||||
Amount: 100,
|
|
||||||
FeeAmount: 0,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
logic.handleCommission(context.Background(), referee, orderInfo)
|
|
||||||
|
|
||||||
assertExpireIncreasedByDays(t, db, refereeSub.Id, baseExpire, 0)
|
|
||||||
assertUserCommission(t, db, referer.Id, 0)
|
|
||||||
assertLogCountForOrder(t, db, modelLog.TypeCommission.Uint8(), orderInfo.OrderNo, 0)
|
|
||||||
assertLogCountForOrder(t, db, modelLog.TypeGift.Uint8(), orderInfo.OrderNo, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandleCommission_BothFamilySidesUseCorrectGiftOwners(t *testing.T) {
|
|
||||||
logic, db, cleanup := setupInviteTestLogic(t, config.InviteConfig{
|
|
||||||
ReferralPercentage: 0,
|
|
||||||
OnlyFirstPurchase: false,
|
|
||||||
GiftDays: 2,
|
|
||||||
})
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
refereeOwner := seedUser(t, db, 0, false)
|
|
||||||
referee := seedUser(t, db, 0, false)
|
|
||||||
refererOwner := seedUser(t, db, 0, false)
|
|
||||||
referer := seedUser(t, db, 0, false)
|
|
||||||
seedFamily(t, db, refereeOwner.Id, referee.Id)
|
|
||||||
seedFamily(t, db, refererOwner.Id, referer.Id)
|
|
||||||
referee.RefererId = referer.Id
|
|
||||||
|
|
||||||
refereeOwnerBaseExpire := time.Now().Add(72 * time.Hour).Truncate(time.Second)
|
|
||||||
refererOwnerBaseExpire := time.Now().Add(96 * time.Hour).Truncate(time.Second)
|
|
||||||
refereeOwnerSub := seedActiveSubscribe(t, db, refereeOwner.Id, refereeOwnerBaseExpire)
|
|
||||||
refererOwnerSub := seedActiveSubscribe(t, db, refererOwner.Id, refererOwnerBaseExpire)
|
|
||||||
orderInfo := &order.Order{
|
|
||||||
OrderNo: "ORD-BOTH-FAMILIES-001",
|
|
||||||
Type: OrderTypeSubscribe,
|
|
||||||
IsNew: true,
|
|
||||||
Amount: 100,
|
|
||||||
FeeAmount: 0,
|
|
||||||
SubscriptionUserId: refereeOwner.Id,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
logic.handleCommission(context.Background(), referee, orderInfo)
|
|
||||||
|
|
||||||
assertExpireIncreasedByDays(t, db, refereeOwnerSub.Id, refereeOwnerBaseExpire, 2)
|
|
||||||
assertExpireIncreasedByDays(t, db, refererOwnerSub.Id, refererOwnerBaseExpire, 2)
|
|
||||||
assertNoSubscribeForUser(t, db, referee.Id)
|
|
||||||
assertNoSubscribeForUser(t, db, referer.Id)
|
|
||||||
assertLogCountForOrder(t, db, modelLog.TypeGift.Uint8(), orderInfo.OrderNo, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupInviteTestLogic(t *testing.T, inviteCfg config.InviteConfig) (*ActivateOrderLogic, *gorm.DB, func()) {
|
func setupInviteTestLogic(t *testing.T, inviteCfg config.InviteConfig) (*ActivateOrderLogic, *gorm.DB, func()) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@ -624,7 +336,7 @@ func setupInviteTestLogic(t *testing.T, inviteCfg config.InviteConfig) (*Activat
|
|||||||
t.Fatalf("open test database failed: %v", err)
|
t.Fatalf("open test database failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := db.AutoMigrate(&user.User{}, &user.Device{}, &user.AuthMethods{}, &user.Subscribe{}, &user.UserFamily{}, &user.UserFamilyMember{}, &modelLog.SystemLog{}); err != nil {
|
if err := db.AutoMigrate(&user.User{}, &user.Device{}, &user.AuthMethods{}, &user.Subscribe{}, &modelLog.SystemLog{}); err != nil {
|
||||||
t.Fatalf("auto migrate failed: %v", err)
|
t.Fatalf("auto migrate failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -680,40 +392,6 @@ func seedUser(t *testing.T, db *gorm.DB, referralPercentage uint8, onlyFirstPurc
|
|||||||
return u
|
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 {
|
func seedActiveSubscribe(t *testing.T, db *gorm.DB, userID int64, expireAt time.Time) *user.Subscribe {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
sub := &user.Subscribe{
|
sub := &user.Subscribe{
|
||||||
@ -745,54 +423,6 @@ 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 assertLogCountForOrder(t *testing.T, db *gorm.DB, logType uint8, orderNo string, expected int64) {
|
|
||||||
t.Helper()
|
|
||||||
var count int64
|
|
||||||
if err := db.Model(&modelLog.SystemLog{}).
|
|
||||||
Where("type = ? AND content LIKE ?", logType, "%"+orderNo+"%").
|
|
||||||
Count(&count).Error; err != nil {
|
|
||||||
t.Fatalf("count logs failed: %v", err)
|
|
||||||
}
|
|
||||||
if count != expected {
|
|
||||||
t.Fatalf("expected log type %d count=%d for order %s, got %d", logType, expected, orderNo, count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertGiftLogRemarkContains(t *testing.T, db *gorm.DB, orderNo string, want string) {
|
|
||||||
t.Helper()
|
|
||||||
var row modelLog.SystemLog
|
|
||||||
if err := db.Model(&modelLog.SystemLog{}).
|
|
||||||
Where("type = ? AND content LIKE ?", modelLog.TypeGift.Uint8(), "%"+orderNo+"%").
|
|
||||||
First(&row).Error; err != nil {
|
|
||||||
t.Fatalf("query gift log failed: %v", err)
|
|
||||||
}
|
|
||||||
if !strings.Contains(row.Content, want) {
|
|
||||||
t.Fatalf("expected gift log content to contain %q, got %s", want, row.Content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertNoSubscribeForUser(t *testing.T, db *gorm.DB, userID int64) {
|
|
||||||
t.Helper()
|
|
||||||
var count int64
|
|
||||||
if err := db.Model(&user.Subscribe{}).Where("user_id = ?", userID).Count(&count).Error; err != nil {
|
|
||||||
t.Fatalf("count subscribes failed: %v", err)
|
|
||||||
}
|
|
||||||
if count != 0 {
|
|
||||||
t.Fatalf("expected user %d to have no direct subscriptions, got %d", userID, count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func boolPtr(v bool) *bool {
|
func boolPtr(v bool) *bool {
|
||||||
return &v
|
return &v
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,185 +0,0 @@
|
|||||||
//go:build ignore
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/md5"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/forgoer/openssl"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ===== AES 加解密(与 pkg/aes/aes.go 一致)=====
|
|
||||||
|
|
||||||
func generateKey(key string) []byte {
|
|
||||||
hash := sha256.Sum256([]byte(key))
|
|
||||||
return hash[:32]
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateIv(iv, key string) []byte {
|
|
||||||
h := md5.New()
|
|
||||||
h.Write([]byte(iv))
|
|
||||||
return generateKey(hex.EncodeToString(h.Sum(nil)) + key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func aesEncrypt(plainText []byte, keyStr string) (string, string, error) {
|
|
||||||
nonce := fmt.Sprintf("%x", time.Now().UnixNano())
|
|
||||||
key := generateKey(keyStr)
|
|
||||||
iv := generateIv(nonce, keyStr)
|
|
||||||
dst, err := openssl.AesCBCEncrypt(plainText, key, iv, openssl.PKCS7_PADDING)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
return base64.StdEncoding.EncodeToString(dst), nonce, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func aesDecrypt(cipherText, keyStr, ivStr string) (string, error) {
|
|
||||||
decode, err := base64.StdEncoding.DecodeString(cipherText)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
key := generateKey(keyStr)
|
|
||||||
iv := generateIv(ivStr, keyStr)
|
|
||||||
dst, err := openssl.AesCBCDecrypt(decode, key, iv, openssl.PKCS7_PADDING)
|
|
||||||
return string(dst), err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== 主逻辑 =====
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
deviceID := flag.String("id", "", "设备 ID (identifier)")
|
|
||||||
secret := flag.String("secret", "", "security_secret (device.security_secret)")
|
|
||||||
host := flag.String("host", "https://api.hifast.biz", "API 地址")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if *deviceID == "" || *secret == "" {
|
|
||||||
fmt.Println("用法: go run scripts/debug_device_login.go -id <设备ID> -secret <security_secret>")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 构造登录请求体
|
|
||||||
loginBody := map[string]interface{}{
|
|
||||||
"identifier": *deviceID,
|
|
||||||
"user_agent": "DebugScript/1.0",
|
|
||||||
}
|
|
||||||
loginJSON, _ := json.Marshal(loginBody)
|
|
||||||
|
|
||||||
// 2. AES 加密请求体
|
|
||||||
encData, nonce, err := aesEncrypt(loginJSON, *secret)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("❌ 加密失败: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
encBody := map[string]interface{}{
|
|
||||||
"data": encData,
|
|
||||||
"time": nonce,
|
|
||||||
}
|
|
||||||
encBodyJSON, _ := json.Marshal(encBody)
|
|
||||||
|
|
||||||
fmt.Printf("📤 登录请求体(加密): %s\n\n", encBodyJSON)
|
|
||||||
|
|
||||||
// 3. 发起设备登录请求
|
|
||||||
loginURL := *host + "/v1/auth/login/device"
|
|
||||||
req, _ := http.NewRequest("POST", loginURL, bytes.NewReader(encBodyJSON))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
req.Header.Set("Login-Type", "device")
|
|
||||||
|
|
||||||
client := &http.Client{Timeout: 10 * time.Second}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("❌ 登录请求失败: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
respBody, _ := io.ReadAll(resp.Body)
|
|
||||||
fmt.Printf("📥 登录响应(原始): %s\n\n", respBody)
|
|
||||||
|
|
||||||
// 4. 解密响应
|
|
||||||
var respMap map[string]interface{}
|
|
||||||
if err := json.Unmarshal(respBody, &respMap); err != nil {
|
|
||||||
fmt.Printf("❌ 解析响应 JSON 失败: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var token string
|
|
||||||
if dataField, ok := respMap["data"]; ok {
|
|
||||||
switch d := dataField.(type) {
|
|
||||||
case map[string]interface{}:
|
|
||||||
// 加密响应
|
|
||||||
encResp, _ := d["data"].(string)
|
|
||||||
ivResp, _ := d["time"].(string)
|
|
||||||
if encResp != "" && ivResp != "" {
|
|
||||||
decrypted, err := aesDecrypt(encResp, *secret, ivResp)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("❌ 解密响应失败: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("📥 登录响应(解密): %s\n\n", decrypted)
|
|
||||||
var loginData map[string]interface{}
|
|
||||||
if err := json.Unmarshal([]byte(decrypted), &loginData); err == nil {
|
|
||||||
token, _ = loginData["token"].(string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case string:
|
|
||||||
// 未加密直接是 token 字符串
|
|
||||||
token = d
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if token == "" {
|
|
||||||
fmt.Println("❌ 未获取到 token,登录失败")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("✅ Token: %s\n\n", token)
|
|
||||||
|
|
||||||
// 5. 查询订阅
|
|
||||||
subURL := *host + "/v1/public/user/subscribe"
|
|
||||||
subReq, _ := http.NewRequest("GET", subURL, nil)
|
|
||||||
subReq.Header.Set("Authorization", "Bearer "+token)
|
|
||||||
subReq.Header.Set("Login-Type", "device")
|
|
||||||
subReq.Header.Set("X-App-Id", "debug")
|
|
||||||
|
|
||||||
subResp, err := client.Do(subReq)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("❌ 查询订阅失败: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer subResp.Body.Close()
|
|
||||||
|
|
||||||
subBody, _ := io.ReadAll(subResp.Body)
|
|
||||||
fmt.Printf("📥 订阅响应(原始): %s\n\n", subBody)
|
|
||||||
|
|
||||||
// 6. 解密订阅响应
|
|
||||||
var subRespMap map[string]interface{}
|
|
||||||
if err := json.Unmarshal(subBody, &subRespMap); err == nil {
|
|
||||||
if dataField, ok := subRespMap["data"]; ok {
|
|
||||||
if d, ok := dataField.(map[string]interface{}); ok {
|
|
||||||
encResp, _ := d["data"].(string)
|
|
||||||
ivResp, _ := d["time"].(string)
|
|
||||||
if encResp != "" && ivResp != "" {
|
|
||||||
decrypted, err := aesDecrypt(encResp, *secret, ivResp)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("❌ 解密订阅响应失败: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 格式化输出
|
|
||||||
var pretty interface{}
|
|
||||||
json.Unmarshal([]byte(decrypted), &pretty)
|
|
||||||
out, _ := json.MarshalIndent(pretty, "", " ")
|
|
||||||
fmt.Printf("📋 订阅信息(解密):\n%s\n", out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user