feat: add client subscription startup policy

This commit is contained in:
QiuSW
2026-07-28 15:14:48 +08:00
parent f0f34156da
commit 1c100c5d7e
15 changed files with 290 additions and 21 deletions
+20 -7
View File
@@ -43,7 +43,7 @@ T-629 已完成:软件套餐订单使用独立 `SoftwareOrder`、`POST /api/v1
T-630/T-632 已完成:蝦皮圈产品专属接口按 API Key 对应用户查询 `SoftwareEntitlement`,默认允许同一账号在多台电脑使用;设备登记、设备会话和历史凭证不再作为授权前置条件。`CMSHOPEE_SUBSCRIPTION_MODE` 支持 `open`(开发测试统一放行)、`shadow`(放行并记录真实权益)和 `enforce`(按真实权益拦截);通用生成接口不受影响。旧 `CMSHOPEE_SUBSCRIPTION_ENFORCEMENT` 仅在新配置缺失时作为兼容回退。
`GET /api/v1/cmshopee/subscription/status` 使用 API Key 鉴权。响应中的 `status` / `allowed` 表示当前模式下的最终访问结果,`entitlement_status` 表示数据库真实权益状态;二者在 `open` / `shadow` 下可能不同。示例:
`GET /api/v1/cmshopee/subscription/status` 使用 API Key 鉴权。响应中的 `status` / `allowed` 表示当前模式下的最终访问结果,`entitlement_status` 表示数据库真实权益状态;二者在 `open` / `shadow` 下可能不同。T-635 新增的 `real_entitlement_allowed` 只由真实权益决定:仅 `active` / `grace` 为 `true`,不受当前运行模式影响。客户端强制门禁必须使用该字段,不得以兼容性 `allowed=true` 代替。示例:
```json
{
@@ -67,11 +67,12 @@ T-630/T-632 已完成:蝦皮圈产品专属接口按 API Key 对应用户查
"manage_url": "https://cmhub.example.com/subscription",
"notice_id": "cmshopee-subscription-open-required-v1",
"access_source": "open_mode",
"entitlement_status": "required"
"entitlement_status": "required",
"real_entitlement_allowed": false
}
```
`plan.name`、`plan.expires_at`、`plan.grace_expires_at` 是 T-630 旧客户端兼容字段;新客户端使用 `plan.code` / `plan.display_name` 和顶层有效期。`access_source` 取 `open_mode`、`shadow_fallback` 或 `entitlement`。真实权益已进入宽限期时 `status` / `entitlement_status` 为 `grace`;无权益和过期在 `enforce` 模式分别返回 `required` / `expired`,对应产品 submit 的 `subscription_required` / `subscription_expired` 403。
`plan.name`、`plan.expires_at`、`plan.grace_expires_at` 是 T-630 旧客户端兼容字段;新客户端使用 `plan.code` / `plan.display_name` 和顶层有效期。`access_source` 取 `open_mode`、`shadow_fallback` 或 `entitlement`。真实权益状态当前只承诺 `active`、`grace`、`required`、`expired`:宽限期为 `grace`,无权益为 `required`,超过宽限期为 `expired`。禁用账号或 API Key 不返回此成功 JSON,而是在认证层返回 HTTP 403 `account_disabled`。产品 submit 在 `enforce` 模式对无权益 / 过期分别返回 `subscription_required` / `subscription_expired` 403。
T-306 已实现对外 API 安全加固:`image_url` 下载只允许 `http` / `https` 公网地址,拒绝私有/回环/链路本地/保留等地址,重定向后重新校验并限制响应大小;全局 DRF 默认认证为空且默认权限为 `IsAuthenticated`,外部 API 与用户端 API 必须显式 opt-in 认证类;生成接口按 API Key / 用户限流,认证失败按 IP 限流;充值创建有 `RECHARGE_MAX_AMOUNT_CNY` 单笔上限。
@@ -89,7 +90,7 @@ T-606 已实现公开首页与客户端下载入口:`GET /` 匿名返回 200
T-610 已在公开首页下载区新增“下载导入模板”链接:模板由 django-admin 维护 `ImportTemplate` 当前记录,首页直接渲染公开下载链接;该能力不新增对外 JSON API,不需要 API Key、不读取用户、不扣点。生产本地模板文件复用 Nginx 直接服务 `MEDIA_ROOT/import_templates/`,避免文件下载占用 Gunicorn worker;`external_url` 优先于本地文件 URL。
T-607/T-609/T-617 已实现 `GET /api/v1/client/releases/latest?platform=windows`,给桌面端自动检查更新使用。该接口公开匿名可访问,不需要 API Key,不读取用户、不扣点、不占用生成接口限流;只返回 `DownloadRelease` 的公开发布元数据。`release.force_update` 表示该版本是否必须升级,来源于后台 `DownloadRelease.force_update`,默认 `false`;`release.size_bytes` 表示安装包文件大小字节数,来源于后台 `DownloadRelease.size_bytes`,用于桌面端下载后校验文件大小。
T-607/T-609/T-617/T-635 已实现 `GET /api/v1/client/releases/latest?platform=windows`,给桌面端自动检查更新和读取启动订阅策略使用。该接口公开匿名可访问,不需要 API Key,不读取用户、套餐或点数,不扣点、不占用生成接口限流;只返回 `DownloadRelease` 的公开发布元数据和无账号上下文的 `client_policy`。`release.force_update` 表示该版本是否必须升级,来源于后台 `DownloadRelease.force_update`,默认 `false`;`release.size_bytes` 表示安装包文件大小字节数,来源于后台 `DownloadRelease.size_bytes`,用于桌面端下载后校验文件大小。
通用错误响应:
@@ -174,6 +175,12 @@ GET /api/v1/client/releases/latest?platform=windows
"force_update": true,
"size_bytes": 45678901,
"published_at": "2026-07-06T18:00:00+08:00"
},
"client_policy": {
"policy_version": 1,
"subscription_check_enabled": true,
"subscription_enforcement_enabled": false,
"updated_at": "2026-07-28T10:00:00+08:00"
}
}
```
@@ -184,13 +191,19 @@ GET /api/v1/client/releases/latest?platform=windows
{
"platform": "windows",
"release": null,
"message": "暂未发布"
"message": "暂未发布",
"client_policy": {
"policy_version": 1,
"subscription_check_enabled": false,
"subscription_enforcement_enabled": false,
"updated_at": "2026-07-28T00:00:00+08:00"
}
}
```
字段说明:`force_update` 为布尔值,表示桌面端是否必须升级到该版本;未勾选时返回 `false`。`size_bytes` 为整数或 `null`,单位字节;T-618 起 django-admin 新增 / 编辑客户端发布版本时要求填写 `sha256` 和 `size_bytes`,但历史记录或非 admin 导入数据仍可能为空,客户端应只在该值为正整数时校验下载后的本地文件大小。客户端可先做版本号比较,再按 `force_update=true` 阻止继续使用旧版本或进入强制升级流程,下载完成后同时校验 `size_bytes` 和 `sha256`。
字段说明:`force_update` 为布尔值,表示桌面端是否必须升级到该版本;未勾选时返回 `false`。`size_bytes` 为整数或 `null`,单位字节;T-618 起 django-admin 新增 / 编辑客户端发布版本时要求填写 `sha256` 和 `size_bytes`,但历史记录或非 admin 导入数据仍可能为空,客户端应只在该值为正整数时校验下载后的本地文件大小。客户端可先做版本号比较,再按 `force_update=true` 阻止继续使用旧版本或进入强制升级流程,下载完成后同时校验 `size_bytes` 和 `sha256`。`client_policy.policy_version` 当前固定为整数 `1`;两个 `subscription_*_enabled` 均为 JSON boolean;`updated_at` 是带时区 ISO 8601 的策略最后变更时间,不是请求时间。`off` 映射为 `false/false`,`observe` 映射为 `true/false`,`enforce` 映射为 `true/true`。
要点:接口只查 `DownloadRelease(platform, is_current=True)`;`download_url` 优先使用 `external_url`,否则用 `file.url` 生成绝对 HTTPS URL;`published_at` MVP 可使用 `DownloadRelease.updated_at`;`force_update` 使用后台发布版本上的布尔配置,默认 `false`;`size_bytes` 使用后台填写的安装包字节数,可为空以兼容历史记录。响应不得包含本地 `MEDIA_ROOT`、文件系统路径、后台 ID、`is_current`、用户信息、API Key、模型配置或任何密钥字段。成功和“暂未发布”均返回 HTTP 200,方便桌面端静默检查;非法平台返回 `400 bad_request`。无当前版本或当前版本没有下载地址时返回 `release:null`,不返回独立的 `force_update` / `size_bytes` 顶层字段。
要点:接口只查 `DownloadRelease(platform, is_current=True)` 和纯环境配置;`download_url` 优先使用 `external_url`,否则用 `file.url` 生成绝对 HTTPS URL;`published_at` MVP 可使用 `DownloadRelease.updated_at`;`force_update` 使用后台发布版本上的布尔配置,默认 `false`;`size_bytes` 使用后台填写的安装包字节数,可为空以兼容历史记录。成功和“暂未发布”均返回 `client_policy`,发布记录和策略互不依赖;成功及错误响应带 `Cache-Control: no-store`。响应不得包含本地 `MEDIA_ROOT`、文件系统路径、后台 ID、`is_current`、用户信息、API Key、套餐、点数、模型配置或任何密钥字段。成功和“暂未发布”均返回 HTTP 200,方便桌面端静默检查;非法平台返回 `400 bad_request`。无当前版本或当前版本没有下载地址时返回 `release:null`,不返回独立的 `force_update` / `size_bytes` 顶层字段。客户端仅在识别 `policy_version=1` 且字段类型合法时采用策略;策略请求失败或字段异常时应回退到“检测开启、强制关闭”,服务端授权仍独立生效。
### `POST /api/v1/client/devices/register`