2026-07-28 15:03:41 +08:00
|
|
|
|
# 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` 通过。
|
|
|
|
|
|
|
|
|
|
|
|
## 状态
|
|
|
|
|
|
|
2026-07-28 15:14:48 +08:00
|
|
|
|
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`:通过。
|