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

151 lines
8.3 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-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` 通过。
## 状态
DONE。
## 实施结果
- 新增 `config/client_subscription_policy.py`,以单一
`CMSHOPEE_CLIENT_SUBSCRIPTION_POLICY=off|observe|enforce` 解析并发布策略,
固定输出 `policy_version=1` 与两个 JSON 布尔字段;非法模式、非 `off` 缺更新时间、
无时区或非法时间均抛出 `ImproperlyConfigured`。
- `GET /api/v1/client/releases/latest` 的当前发布、无发布和无下载地址路径均返回
顶层 `client_policy`,所有响应设置 `Cache-Control: no-store`;接口仍只查询
`DownloadRelease` 和环境配置。
- `GET /api/v1/cmshopee/subscription/status` 新增
`real_entitlement_allowed`,仅在真实权益为 `active` 或 `grace` 时为 `true`,
保留既有最终访问 `allowed` 兼容字段。
- 未新增模型、迁移、admin 配置或运行时写入,不改通用 / 专属生成、点数账本、
充值支付、软件订单或服务端订阅强制模式。
## 验证结果
- `py -3.12 -m py_compile config\client_subscription_policy.py config\settings.py apps\api\views.py apps\api\tests.py apps\licensing\services.py apps\licensing\tests.py`:通过。
- `py -3.12 manage.py check`:通过,0 issues。
- 隔离 SQLite 目标回归:`ClientLatestReleaseApiTests`、订阅状态核心场景和
`SubscriptionModeUnitTests` 共 27 tests OK,覆盖三种策略、无发布策略、
非法配置、`no-store`、真实权益 active / grace / required / expired 语义。
- `py -3.12 manage.py makemigrations --check --dry-run`:No changes detected;
旧本地 MySQL 地址拒绝连接,仅产生迁移历史检查 warning,本任务无迁移。
- `./init.ps1` 和 `git diff --check`:通过。
## 生产部署
- 2026-07-28 已部署到 `185.216.248.75`,线上 `.deploy_revision` 为 `1c100c5`。
- 部署前备份位于
`/root/cmhub_deploy_backups/cmhub-before-t635-20260728_151900`,包含发布前应用代码和 `.env`。
- 线上保持 `CMSHOPEE_SUBSCRIPTION_MODE=enforce`,新增客户端策略明确设置为
`CMSHOPEE_CLIENT_SUBSCRIPTION_POLICY=off`,更新时间为
`2026-07-28T15:19:19+08:00`;不改变服务端授权。
- `cmhub-web`、`cmhub-generate`、Nginx、MySQL 8.4 均为 active;66 个图片 worker
未重启且均 active,failed units 为 0。
- 公网 `GET /api/v1/client/releases/latest?platform=windows` 返回 HTTP 200、
`Cache-Control: no-store` 和 `client_policy=false/false`。