diff --git a/docs/06-tasks.md b/docs/06-tasks.md index c835c0b..aa2c13e 100644 --- a/docs/06-tasks.md +++ b/docs/06-tasks.md @@ -113,6 +113,7 @@ | 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 + grace_expires_at > now` 识别当前可用权益;支付校验通过后在同一事务内把同产品自然到期但仍为 `active` 的历史权益收敛为 `expired`,并为目标套餐创建新权益。有效/宽限期内的跨套餐仍拒绝;同套餐有效权益续订、支付幂等、金额校验和点数账本隔离保持不变。新增 5 条缺陷回归,扩大非并发 licensing / 软件支付 API 回归 40 条通过;无迁移。详见 [`tasks/T-634.md`](tasks/T-634.md)。 | DONE | +| T-635 | cmshopee 客户端启动订阅策略接口 | T-607, T-632 | 在公开 `GET /api/v1/client/releases/latest` 顶层新增只读 `client_policy`,用稳定配置发布客户端订阅检测 / 门禁策略;有发布和 `release:null` 均返回,响应 `Cache-Control: no-store`。客户端策略与 `CMSHOPEE_SUBSCRIPTION_MODE` 服务端授权解耦;订阅状态接口新增真实权益是否有效的明确字段,避免 `open` / `shadow` 的兼容 `allowed=true` 被客户端误判。不得读取用户、套餐、点数或变更账本;不改生文、生图、支付和订阅订单。详见 [`tasks/T-635.md`](tasks/T-635.md)。 | TODO | ## 里程碑 @@ -137,6 +138,7 @@ - M19:蝦皮圈设备授权与订阅迁移(T-624~T-629)。 - M20:存量用户过渡测试套餐安全批量授予(T-633)。 - M21:过期会员可自助购买新套餐(T-634)。 +- M22:cmshopee 客户端启动订阅策略接口(T-635)。 ## 待办池(Backlog) diff --git a/docs/tasks/T-635.md b/docs/tasks/T-635.md new file mode 100644 index 0000000..f1b34d7 --- /dev/null +++ b/docs/tasks/T-635.md @@ -0,0 +1,111 @@ +# T-635 cmshopee 客户端启动订阅策略接口 + +## 背景 + +新版 cmshopee 客户端需要在启动时远程获知是否检查会员套餐、是否在 +客户端侧展示会员门禁。客户端已经匿名请求 +`GET /api/v1/client/releases/latest?platform=windows`,第一版应复用该公开 +版本检查接口发布策略,避免新增启动请求。 + +现有订阅状态接口的 `allowed` 是**当前服务端运行模式下的最终访问结果**。 +在 `open` 或 `shadow` 模式,无真实权益的账号仍可能得到 `allowed=true`。 +客户端不能把该字段当作真实会员有效性,否则在未来切换客户端门禁时会误放行。 + +## 目标 + +1. 版本检查响应顶层发布稳定、公开、无账号上下文的 `client_policy`。 +2. 用一个受校验的配置枚举表达客户端策略,输出兼容桌面端所需的两个布尔字段。 +3. 订阅状态接口明确返回真实权益是否可用,消除 `open` / `shadow` 的歧义。 +4. 策略仅影响客户端体验;服务端产品专属接口仍由 + `CMSHOPEE_SUBSCRIPTION_MODE` 独立授权。 + +## 范围 + +### 公开版本检查接口 + +在 `GET /api/v1/client/releases/latest?platform=windows` 的顶层新增: + +```json +{ + "platform": "windows", + "release": null, + "message": "暂未发布", + "client_policy": { + "policy_version": 1, + "subscription_check_enabled": true, + "subscription_enforcement_enabled": false, + "updated_at": "2026-07-28T10:00:00+08:00" + } +} +``` + +- `client_policy` 不得为 `null`,在有当前发布和 `release:null` 两条成功路径 + 都必须返回;`release` 的既有字段和语义不变。 +- `policy_version` 当前固定为整数 `1`;两个开关必须是 JSON 布尔值; + `updated_at` 必须是带时区的 ISO 8601 字符串,代表配置最后变化时间,不能 + 使用每次请求的当前时间。 +- 该接口继续匿名、只读、无 API Key / Cookie / 用户 / 套餐 / 点数查询,不扣点, + 不占用生成接口限流;响应不得含 API Key、用户、套餐、内部路径、后台 ID 或密钥。 +- 成功响应设置 `Cache-Control: no-store`,保证客户端紧急回退不会长期受 CDN + 或浏览器缓存影响。 + +### 配置 + +- 新增 `CMSHOPEE_CLIENT_SUBSCRIPTION_POLICY=off|observe|enforce`,默认 `off`。 + 非法值必须阻止 Django 启动,不能静默降级。 +- 输出映射固定为:`off -> false/false`、`observe -> true/false`、 + `enforce -> true/true`。不使用两个独立环境变量,避免出现非法的 + `false/true` 组合。 +- 新增 `CMSHOPEE_CLIENT_SUBSCRIPTION_POLICY_UPDATED_AT`,要求为带时区 ISO 8601 + 时间;每次把策略改为 `observe` 或 `enforce` 时必须同步显式更新。未配置时 + 只允许默认 `off` 使用仓库固定的基线时间;非 `off` 未配置或时间非法时阻止启动。 +- 该配置与已有 `CMSHOPEE_SUBSCRIPTION_MODE=open|shadow|enforce` 完全独立。 + 前者只控制新版客户端是否请求 / 展示门禁,后者才控制服务端专属生成接口的 + 最终放行。不得因客户端策略变更而创建、修改或删除权益、套餐、订单、点数、 + API Key 或异步任务。 + +### 订阅状态真实权益语义 + +- `GET /api/v1/cmshopee/subscription/status` 顶层新增 `real_entitlement_allowed`。 + 仅当 `entitlement_status` 为 `active` 或 `grace` 时为 `true`;该字段不受 + `open` / `shadow` / `enforce` 的最终放行模式影响。 +- 保留现有 `allowed`、`status`、`code`、`access_source` 和 + `entitlement_status`,不改变服务端当前授权或既有客户端兼容行为。 +- 文档删除未实现的 `legacy` / `revoked` 状态承诺:当前真实权益状态只以 + `active`、`grace`、`required`、`expired` 为准;禁用账号 / API Key 仍由认证层 + 返回 HTTP 403 `account_disabled`,不是状态接口的成功 JSON 状态。 + +## 非目标与兼容 + +- 不新增数据库模型、迁移、admin 配置页或运行时热修改能力;环境变量变更后 + 按既有部署方式重启 Web / Generate 服务生效。 +- 不改通用 `/api/v1/generate/*`、产品专属生成接口的授权逻辑、上游调用、 + 预扣 / 确认 / 退款、`UserWallet`、`PointsLedger`、充值支付或软件订单。 +- 旧客户端会忽略新增顶层字段,仍按自身内置逻辑工作;新客户端应只在 + `policy_version=1` 且字段类型合法时采纳策略。无法获取策略时客户端应进入 + “检测开启、强制关闭”的观察回退,不能提示用户重复付费。 + +## 验收条件 + +1. `off`、`observe`、`enforce` 三种合法配置分别返回 + `false/false`、`true/false`、`true/true`,类型均为 JSON boolean。 +2. 非法枚举、非 `off` 缺少更新时间、无时区或非法更新时间均阻止 Django 启动或 + 在配置读取处抛出明确 `ImproperlyConfigured`,不得向外发布矛盾策略。 +3. 有当前发布、没有当前发布、当前发布无下载地址三种成功响应均包含 + `client_policy`;原有 `release` 字段与 `force_update`、`size_bytes` 不回归。 +4. 版本检查接口匿名可用,忽略 Session / API Key,响应为 `Cache-Control: no-store`, + 且不访问或泄露用户、订阅、点数、模型、密钥、文件系统路径或后台 ID。 +5. `open` / `shadow` 下无权益账号仍可能保持现有 `allowed=true`,但 + `real_entitlement_allowed=false`;真实 active / grace 权益为 `true`,过期 / 未开通 + 为 `false`。 +6. `CMSHOPEE_SUBSCRIPTION_MODE` 与客户端策略任意组合均不改变既有产品专属接口 + 的服务端授权结论;通用生成、扣点、退款、充值和软件订单回归通过。 +7. 同步更新 `docs/api.md`、`docs/env.md`、`docs/deployment.md`、 + `docs/04-architecture.md`、`docs/current-state.md`、任务状态和 `progress.md`; + 及 Obsidian 的客户端接口契约。 +8. `manage.py check`、目标 API / licensing 测试、迁移一致性、编译检查和 + `git diff --check` 通过。 + +## 状态 + +TODO。