From e12659faa0b065be37db67cde06c100b05edad51 Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Thu, 23 Jul 2026 17:30:16 +0800 Subject: [PATCH] docs: add expired subscription repurchase task --- docs/06-tasks.md | 2 ++ docs/current-state.md | 6 ++-- docs/tasks/T-634.md | 66 +++++++++++++++++++++++++++++++++++++++++++ progress.md | 8 ++++++ 4 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 docs/tasks/T-634.md diff --git a/docs/06-tasks.md b/docs/06-tasks.md index a6c2300..9a29af5 100644 --- a/docs/06-tasks.md +++ b/docs/06-tasks.md @@ -112,6 +112,7 @@ | T-631 | 隐藏旧设备授权模型的 admin 菜单入口 | T-630 | 已完成:从 django-admin 应用索引隐藏 `LicenseSeat`、`LegacyMigrationGrant`、`MigrationRequest`、`DeviceCredential` 四个旧方案模型;保留数据库表、历史数据和只读模型。当前订阅、订单、授权事件、设备和会话观测模型继续可见,不改变 API、授权和账务行为。定向 admin 测试、`check`、迁移一致性和编译检查通过。详见 [`tasks/T-631.md`](tasks/T-631.md)。 | DONE | | T-632 | 会员订阅三阶段启用与简化运营 | T-630, T-631 | **已完成。** 新增 `CMSHOPEE_SUBSCRIPTION_MODE=open|shadow|enforce`,将真实权益状态与最终访问结果分离;`open` / `shadow` 不写虚假权益,`enforce` 才拦截产品专属 submit。状态接口补齐桌面端账号、套餐、顶层有效期、会员中心、通知及观测字段,并保留 T-630 旧字段。admin 首页仅保留“会员套餐”和“用户会员”,其他模型只隐藏不删除。5 条无数据库单元测试、`check`、迁移一致性和编译通过;数据库集成测试因旧远程 MySQL 拒绝连接未取得结果。详见 [`tasks/T-632.md`](tasks/T-632.md)。 | DONE | | T-633 | 存量用户批量授予过渡测试套餐 | T-632, T-626 | **已完成。** 新增默认只预演的 `grant_existing_users_plan`,通过明确 `plan_id`、非空原因、`--execute` 与预期人数双重确认,为启用的非后台账号批量授予套餐;已有同产品有效/宽限期权益会跳过。执行在单事务内复用 `grant_software_entitlement()` 生成权益快照、席位和授权事件,任一失败整批回滚。线上已备份 MySQL 后为 30 个存量用户授予“测试”套餐,反向预演待授予为 0;后台账号、API Key、点数、充值、软件订单和生成记录未由命令修改。详见 [`tasks/T-633.md`](tasks/T-633.md)。 | DONE | +| T-634 | 过期会员允许购买其他套餐 | T-629, T-632 | 修复软件订单只按 `status=active` 判断当前套餐的口径差异:下单与支付入账都按 `status=active + grace_expires_at > now` 识别当前可用权益;支付发放时把同产品已超过宽限期但仍为 `active` 的历史权益收敛为 `expired`,并为目标套餐创建新权益。有效/宽限期内的跨套餐仍拒绝;同套餐续订、支付幂等、金额校验和点数账本隔离保持不变。详见 [`tasks/T-634.md`](tasks/T-634.md)。 | TODO | ## 里程碑 @@ -135,6 +136,7 @@ - M18:图片生成任务单图 / 多图筛选(T-623)。 - M19:蝦皮圈设备授权与订阅迁移(T-624~T-629)。 - M20:存量用户过渡测试套餐安全批量授予(T-633)。 +- M21:过期会员可自助购买新套餐(T-634)。 ## 待办池(Backlog) diff --git a/docs/current-state.md b/docs/current-state.md index 3de407d..c3f700d 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -76,10 +76,10 @@ - 已完成:T-001 初始化 Django + DRF 项目骨架;T-002 建立 apps 目录、自定义 User 与配置;T-003 接通 django-admin 与最小测试;T-004 Phase 0 骨架审核修补;T-101 Provider 适配器层 + 移植 cmbot 调用;T-102 AiModel + ModelAlias 模型 + 别名解析;T-103 配置变更审计;T-104 跑通一次录制标题生成;T-105 Phase 1 AI 层审核修补;T-201 User / UserWallet / ApiKey / PointsLedger / CallRecord 模型;T-202 PricingRule / ExchangeRate 模型 + 计费计算;T-203 并发安全扣点 / 退点;T-204 Phase 2 计费核心审核加固;T-301 API Key 鉴权;T-302 生成标题 / 图片接口;T-303 余额查询接口;T-304 充值回调;T-305 扫码充值下单 + 轮询;T-306 Phase 3 对外 API 安全加固;T-501 注册 / 登录(allauth);T-502 API Key 自助管理页;T-503 个人中心 / 记录页;T-504 充值页(扫码 + 轮询到账);T-505 Phase 4 用户端审核优化;T-401 运营后台完善;T-402 完整验收 MVP;T-403 部署 / 运行文档;T-601 可用别名发现;T-602 django-admin 中文化(第 1-3 层);T-603 django-admin 中文化(第 4 层·字段级);T-604 中文敏感词本地过滤;T-605 免邮箱验证策略落地;T-606 公开首页 + 客户端下载入口;T-607 桌面端最新版本检查接口;T-608 新用户注册赠送试用点数(当前 10 点);T-609 桌面端版本检查接口增加强制更新标记;T-610 首页导入模板下载入口;T-611 用户端品牌名统一为虾皮圈;T-612 生图同步接口止血(上游硬截止 + 长请求池校准);T-613 抽生成核心 service(计费+审核+上游共享 core);T-614 生图异步任务化接口(提交+轮询,新增不动旧接口);T-615 旧同步生图接口用量遥测 + 弃用口径;T-616 生图失败自动重试 2 次;T-617 桌面端版本检查接口增加文件大小字段;T-618 客户端发布版本后台必填文件校验元数据。 - 已完成补充:T-619 多张图片理解并返回文字;T-620 图生图支持单图 / 多图主图与参考图;T-622 图片生成任务后台图片缩略预览;T-623 图片生成任务单图 / 多图筛选;T-624 蝦皮圈设备登记与会话观测;T-625 蝦皮圈设备使用关联与迁移观测;T-626 软件套餐、权益与设备席位基础模型及凭证续期/撤销清理;T-627 存量用户迁移权益、网页确认与设备凭证;T-628 蝦皮圈专属授权入口与影子校验;T-629 软件套餐购买、续订订单与权益入账;T-630 账号订阅授权与默认多设备模式;T-631 隐藏旧设备授权模型的 admin 菜单入口;T-632 会员订阅三阶段启用、状态接口补全与 admin 简化;T-633 存量用户批量授予过渡测试套餐。 -- 正在进行:无。 +- 正在进行:无;T-634 已完成任务登记,尚未开始代码实现。 - T-621 注册赠点运营后台配置继续留在 Backlog。真实支付回调到账闭环、客户端发布、生产多图理解模型配置和线上旧同步接口用量观察仍可继续拆任务。 - 当前 blocker:支付商户真实密钥/证书与生产 SDK 依赖仍待提供;微信回调到账闭环仍需真实支付验收;真实 AI 标题生成已在线上跑通,图片生成慢 / 504 / 客户端超时风险已拆为 T-612~T-616 并完成工程侧处理。 -- 下一个可领取任务:按业务优先级从 Backlog 拆分后续任务。T-621 保留在 Backlog,真实微信商户支付验收仍是线上 blocker;软件订阅订单已完成代码与 mock/SDK 契约验证,但未进行真实付款验收。具体范围见 [`06-tasks.md`](06-tasks.md)。 +- 下一个可领取任务:T-634 过期会员允许购买其他套餐。T-621 保留在 Backlog,真实微信商户支付验收仍是线上 blocker;软件订阅订单已完成代码与 mock/SDK 契约验证,但未进行真实付款验收。具体范围见 [`06-tasks.md`](06-tasks.md) 与 [`tasks/T-634.md`](tasks/T-634.md)。 ## 当前可运行内容 @@ -158,7 +158,7 @@ T-633 已新增默认只预演的 `grant_existing_users_plan`:按套餐 ID、 1. 读仓库级 `AGENTS.md` / `CLAUDE.md`。 2. 读 `docs/00-ai-start-here.md`。 3. 读 `docs/05-coding-rules.md`(尤其第 8 节资金安全)。 -4. 在 `docs/06-tasks.md` 领取第一个 `TODO` 且依赖均 `DONE` 的任务;当前没有可直接领取的任务。 +4. 在 `docs/06-tasks.md` 领取第一个 `TODO` 且依赖均 `DONE` 的任务;当前可领取 T-634。 5. 代码默认开发测试模式为 `open`;线上当前为 `shadow` 且存量普通用户已有“测试”套餐。切到 `enforce` 前仍须完成桌面端回归,并用真实微信商户完成充值和订阅两条回调闭环验收。 ## 维护规则 diff --git a/docs/tasks/T-634.md b/docs/tasks/T-634.md new file mode 100644 index 0000000..ad26753 --- /dev/null +++ b/docs/tasks/T-634.md @@ -0,0 +1,66 @@ +# T-634 过期会员允许购买其他套餐 + +## 背景 + +线上 `dev` 用户的“测试”权益已经超过宽限截止时间,但 +`SoftwareEntitlement.status` 仍为 `active`。用户购买“99元/月”套餐时, +软件订单服务只按数据库状态查找已有权益,没有同时判断 +`grace_expires_at`,因此把时间上已经失效的权益误判为当前套餐,并返回 +`plan_change_not_supported`(“当前套餐变更请联系运营处理”)。 + +订阅授权服务已经按 `status + grace_expires_at` 判断真实有效性,下单与支付 +入账使用不同口径,导致状态接口显示已过期,但用户仍无法购买新套餐。 + +## 目标 + +1. 已超过宽限截止时间的权益不得阻止用户购买其他套餐。 +2. 尚在有效期或宽限期内的不同套餐仍拒绝自助切换,继续由运营处理。 +3. 支付回调首次发放新套餐时,把同产品已超过宽限期但仍标记为 `active` + 的历史权益收敛为 `expired`。 +4. 同套餐有效权益的正常续订、支付幂等、金额校验和点数账本隔离保持不变。 + +## 范围 + +### 下单前校验 + +- `create_software_order()` 查询当前套餐时必须同时满足: + `status=active` 且 `grace_expires_at > now`。 +- 只有当前仍可用的权益与目标套餐不同,才返回 + `plan_change_not_supported`。 +- 已超过宽限期的 `active` 历史权益不阻止创建其他套餐的待支付订单。 +- 创建支付订单阶段不提前改写历史权益;只有支付确认并发放权益时才完成 + 状态收敛,避免用户未付款就改变历史数据。 + +### 支付入账 + +- `apply_software_payment()` 继续在数据库事务内锁软件订单和同产品权益。 +- 发放前把 `status=active` 且 `grace_expires_at <= now` 的同产品历史权益 + 更新为 `expired`,再查询当前仍可用权益。 +- 当前没有可用权益时,按订单快照创建一条新权益;不得续期已过期的旧权益。 +- 当前存在同套餐可用权益时沿用现有续订逻辑。 +- 当前存在其他套餐可用权益时继续拒绝入账,不实现升级、降级、差价、退款 + 或剩余天数折算。 + +## 安全与兼容 + +- 不修改 API 路由、请求或响应结构,不新增数据库字段或迁移。 +- 不修改 `RechargeOrder`、`UserWallet`、`PointsLedger` 或点数余额。 +- 不修改微信验签、金额比对、交易号唯一约束和同订单幂等逻辑。 +- 历史权益只做自然到期状态收敛,不删除权益、席位、事件或订单。 +- 通用生成接口、产品专属生成接口和订阅强制模式保持不变。 + +## 验收条件 + +1. 已超过宽限期但状态仍为 `active` 的旧套餐不阻止创建其他套餐订单。 +2. 上述订单支付成功后创建目标套餐新权益,旧权益状态变为 `expired`。 +3. 已超过宽限期的同套餐权益也创建新权益,不在旧权益上续期。 +4. 仍在有效期或宽限期内的其他套餐在下单和支付入账两个阶段均继续返回 + `plan_change_not_supported`。 +5. 仍可用的同套餐权益继续正常续订,重复回调最多发放一次。 +6. 支付失败或校验失败不提前修改旧权益状态。 +7. `manage.py check`、目标测试、licensing 回归、迁移一致性、编译检查和 + `git diff --check` 通过。 + +## 状态 + +TODO。 diff --git a/progress.md b/progress.md index 94c50d2..b45e994 100644 --- a/progress.md +++ b/progress.md @@ -2164,3 +2164,11 @@ - 线上结果:30 个不同普通用户获得“测试”权益,产生 30 条授予事件和 3030 条套餐席位快照;后台账号为 0,目标用户缺失为 0,重复有效权益为 0。反向预演为 `skipped_existing=30`、`grant_count=0`。 - API 抽样:订阅状态返回 `active`、`access_source=entitlement`、`entitlement_status=active` 和套餐“测试”,抽样账号点数保持 148。 - 并发说明:执行前后有 4 个生图任务运行,点数聚合从 22221 变为 22219、流水从 10180 变为 10181;最新流水为关联调用的 `consume -2`,属于并发生图业务,不是 T-633 授权命令写账。 + +## 2026-07-23 规划:T-634 过期会员允许购买其他套餐 + +- 线上复现:`dev` 的“测试”权益已经超过宽限截止时间,但数据库状态仍为 `active`;购买另一套餐“99元/月”时,`create_software_order()` 只按状态查到旧权益,并返回 `plan_change_not_supported`,请求在创建新订单前结束。 +- 根因:订阅授权按 `status + grace_expires_at` 判断真实有效性,软件下单与支付发放只按 `status=active` 判断当前套餐,两条链路口径不一致。 +- 方案:下单和支付都只把仍在有效期或宽限期内的权益视为当前权益;支付发放时在同一事务内把同产品自然到期但仍标记为 `active` 的历史权益收敛为 `expired`,再创建目标套餐新权益。 +- 兼容边界:有效/宽限期内的跨套餐仍拒绝;同套餐有效权益继续续订;不实现升级、降级、差价或退款,不改支付验签、点数账本、API 契约或数据库结构。 +- 基线:执行 `./init.ps1` 通过,Python 3.12.3,依赖已满足,`manage.py check` 0 issues。本阶段只登记任务,尚未修改代码。