Files
cmhub/docs/tasks/T-633.md
T

83 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# T-633 存量用户批量授予过渡测试套餐
## 背景
线上已创建用于开发和联调阶段的“测试”会员套餐。当前订阅仍运行在 `shadow` 模式,存量用户不会被强制拦截,但在正式切换到 `enforce` 前,需要用真实 `SoftwareEntitlement` 验证桌面端会员状态、到期时间和后续套餐切换流程。
直接在数据库批量插入权益会绕过套餐快照、席位和 `LicenseEvent`,逐个在 admin 操作又容易漏发或重复发放。因此需要一个默认只预演、显式确认后才执行的批量命令。
## 目标
1. 给执行时已有的启用非后台账号批量授予指定测试套餐。
2. 复用现有授权服务,完整生成权益快照、席位和授权事件。
3. 提供预演、人数校验、事务回滚和重复执行保护,降低生产批处理风险。
4. 不影响已有正式会员、API Key、点数、充值、软件订单和生成任务。
## 范围
### 批量命令
- 新增 `grant_existing_users_plan` management command。
- 套餐必须通过数据库 `plan_id` 精确指定,且执行时仍为启用状态;不按可能重名的套餐名称模糊匹配。
- 默认只输出预演统计,不写数据库。
- 实际执行必须同时传入 `--execute` 和预演得到的 `--expected-grant-count`;人数不一致时整批拒绝。
- `--reason` 必填并写入每一条授权事件,不能生成无原因的批量权益。
### 用户范围和幂等
- 默认只处理 `is_active=True`、`is_staff=False`、`is_superuser=False` 的账号。
- 对指定套餐所属产品,用户只要已有仍处于有效期或宽限期的权益,就跳过本次授予;不得用测试套餐覆盖或制造与正式套餐并存的有效权益。
- 已过期或已撤销权益不阻止重新授予。
- 执行阶段锁定套餐和目标用户,并在同一数据库事务内重新计算范围;任何一名用户授予失败都必须整批回滚。
### 授权与留痕
- 每名实际授予用户必须复用 `grant_software_entitlement()`,不得直接创建 `SoftwareEntitlement`。
- 权益继续复制套餐名称、价格、时长、设备数和宽限期快照,并按既有服务生成席位。
- 每项授予写 `LicenseEvent(granted)`,记录统一的非空批处理原因。
- 命令输出只给出符合条件、跳过和实际授予数量,不输出邮箱、API Key 或其他敏感数据。
## 实施约束
- 不修改 `CMSHOPEE_SUBSCRIPTION_MODE`,本任务执行期间线上继续保持 `shadow`。
- 不撤销、续期或改写用户已有权益。
- 不修改 `UserWallet`、`PointsLedger`、`RechargeOrder`、`SoftwareOrder` 或生成记录。
- 不新增数据迁移,不直接运行原始 SQL。
- 本任务只完成测试套餐的安全批量授予;测试结束后的正式套餐切换另行落任务,不通过编辑套餐反向修改权益快照。
## 验收条件
1. 默认预演不创建权益、席位或授权事件,并返回待授予和跳过数量。
2. 未提供 `--execute` 时不能写数据;执行时缺少或错误的预期授予人数必须拒绝且不产生部分数据。
3. 启用的普通用户获得指定套餐;后台账号、停用账号和已有同产品有效权益用户不受影响。
4. 每个新权益具有正确套餐快照、到期时间和宽限截止时间,并存在对应 `granted` 授权事件。
5. 同一命令重复执行时已获有效权益用户被跳过,不重复授予。
6. 任一授予失败时整批回滚。
7. `manage.py check`、目标测试、迁移一致性、编译检查和 `git diff --check` 通过。
8. 线上执行前完成数据库备份;预演人数与实际执行人数一致;执行后权益数、事件数和订阅状态接口抽样验收通过。
## 状态
DONE。
## 实施结果
- 新增 `grant_plan_to_existing_users()` 批量服务:执行时锁定套餐和启用的非后台用户,按同产品仍有效或处于宽限期的权益跳过已有会员,并在单事务中复用 `grant_software_entitlement()`。
- 新增 `grant_existing_users_plan` management command。默认只预演;实际执行必须同时提供 `--execute` 和与当前范围一致的 `--expected-grant-count`。
- 命令按套餐 ID 精确选择启用套餐,统一要求非空原因;输出只包含套餐、产品和数量统计。
- 新增 5 条专项测试,覆盖只读预演、普通用户范围、正式权益保护、重复执行、人数不匹配拒绝和异常整批回滚。
## 验证结果
- `manage.py check`:通过,0 issues。
- `manage.py makemigrations --check --dry-run`:No changes detected;本任务无迁移。
- `manage.py test apps.licensing.tests.ExistingUserPlanGrantCommandTests`:5 tests OK。
- SQLite 完整 `apps.licensing` 回归共 35 条,其中 33 条通过;2 条既有多连接并发测试因 SQLite 表级锁返回 `database table is locked`,与本任务断言无关,未误记为全量通过。
- `compileall apps/licensing` 与 `git diff --check`:通过。
- 生产 MySQL 8.4 执行前备份:`/root/cmhub-t633-backup-20260722_164956/cmhub-before-t633.sql.gz`,SHA256 为 `ae53d3edd329a3ed16e55958f2a93453f420b5026bbb3294edb6b4e91ad17162`,本地副本校验一致。
- 线上预演:`eligible=30`、`skipped_existing=0`、`grant_count=30`;按预期人数执行成功。
- 执行后:测试套餐权益 30 条、不同用户 30 个、`granted` 事件 30 条、席位 3030 条;后台账号权益 0,缺少有效权益的目标用户 0,重复有效权益用户 0。
- 反向预演:`eligible=30`、`skipped_existing=30`、`grant_count=0`,证明重复执行不会重复授予。
- 真实 API Key 抽样返回 `access_source=entitlement`、`entitlement_status=active`、套餐“测试”,点数余额保持 148。
- 执行期间线上有 4 个生图任务运行,聚合点数同期减少 2 点并新增一条关联调用的 `consume -2` 流水;这是并发生成业务变化,不是授权命令写账。