From 9544aaf4cc341773f33ea8e21ce603436d3d04d3 Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Tue, 28 Jul 2026 15:28:01 +0800 Subject: [PATCH] docs: add admin client policy task --- docs/06-tasks.md | 2 + docs/tasks/T-636.md | 91 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 docs/tasks/T-636.md diff --git a/docs/06-tasks.md b/docs/06-tasks.md index 10dc087..e991c78 100644 --- a/docs/06-tasks.md +++ b/docs/06-tasks.md @@ -114,6 +114,7 @@ | 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 | **已完成。** 公开版本检查接口顶层新增 `client_policy`,有发布和 `release:null` 均返回,所有响应 `Cache-Control: no-store`;单一 `off|observe|enforce` 配置源输出双布尔字段,非 `off` 强制带时区更新时间。订阅状态新增不受运行模式影响的 `real_entitlement_allowed`,客户端不再把 `open` / `shadow` 的兼容 `allowed=true` 误判为真实会员。无迁移,不读用户、套餐或点数,不改生文、生图、支付、扣点和订阅订单。详见 [`tasks/T-635.md`](tasks/T-635.md)。 | DONE | +| T-636 | 客户端订阅策略 django-admin 运营开关 | T-635, T-632 | 将新版客户端启动策略从“改 `.env` + 重启”扩展为 django-admin 可运营的单例配置。**数据源**:新增全局单例 `ClientSubscriptionPolicy`,取值仅 `off|observe|enforce`;公开版本检查接口优先读取该记录,记录尚不存在时才回退 T-635 环境变量,确保本次上线和回滚不改变既有 `off` 行为。**后台**:日常导航只增加一个“客户端订阅策略”入口;仅超级管理员可新增 / 修改,不允许删除,首次创建后只能编辑该唯一记录。保存必须填写变更原因,自动记录操作者、前后模式、时间和原因;审计只在详情页只读展示,不另增菜单入口。`CMSHOPEE_SUBSCRIPTION_MODE` 只读展示为服务端授权模式,不能被该页修改。**即时生效**:每次公开版本检查实时读取该全局配置,不做进程内缓存,admin 保存后无需重启 Web / Generate;不创建策略记录仍沿用 `.env`,紧急回退可在后台改回 `off`。**安全与兼容**:公开响应仍无账号上下文,不读取用户权益、套餐、点数、订单或 API Key;不改通用 / 专属生成、预扣 / 退点、支付、软件订单及服务端授权模式。**迁移与部署**:迁移只建空表,不预置记录;生产先备份并执行 migrate,验收公开接口仍为当前 `client_policy=off`,再由超级管理员按 `off -> observe -> enforce` 灰度。**测试**:覆盖 `.env` 回退、后台策略优先、三种映射、保存后下一次公开请求立即生效、唯一性、超级管理员限制、原因和审计留痕、审计不出现在 admin 导航、`no-store` 与既有发布字段回归;`makemigrations` / `migrate` / `check` / 目标测试 / `init` / `git diff --check` 通过,并同步 API、架构、环境、部署、当前状态、进度和 Obsidian 客户端契约。详见 [`tasks/T-636.md`](tasks/T-636.md)。 | TODO | ## 里程碑 @@ -139,6 +140,7 @@ - M20:存量用户过渡测试套餐安全批量授予(T-633)。 - M21:过期会员可自助购买新套餐(T-634)。 - M22:cmshopee 客户端启动订阅策略接口(T-635)。 +- M23:客户端订阅策略 django-admin 运营开关(T-636)。 ## 待办池(Backlog) diff --git a/docs/tasks/T-636.md b/docs/tasks/T-636.md new file mode 100644 index 0000000..a10e0c0 --- /dev/null +++ b/docs/tasks/T-636.md @@ -0,0 +1,91 @@ +# T-636 客户端订阅策略 django-admin 运营开关 + +## 背景 + +T-635 已经通过公开版本检查接口向新版 cmshopee 客户端发布启动订阅策略, +但策略只能通过修改 `.env` 后重启 Web / Generate 服务变更。该方式不适合日常 +运营,也无法在 django-admin 中留下明确的变更原因和操作者。 + +客户端策略和 `CMSHOPEE_SUBSCRIPTION_MODE` 的服务端授权仍是两件事:前者只 +决定新版客户端是否检查 / 展示会员门禁,后者才决定产品专属生成接口是否按真实 +权益拦截。本任务不得混淆或合并这两套开关。 + +## 目标 + +1. 让超级管理员在 django-admin 中用单一枚举切换 `off`、`observe`、`enforce`。 +2. 每次变更必须留存原因、操作者、前后值和时间。 +3. 公开版本检查接口在下一次请求立即发布后台策略,无需重启服务。 +4. 未配置后台策略时完全保留 T-635 `.env` 行为,保证上线不改变当前客户端。 + +## 实施范围 + +### 数据模型与策略来源 + +- 新增全局单例 `ClientSubscriptionPolicy`,仅保存一个固定主键记录;字段至少有 + `mode`、`updated_by`、`created_at`、`updated_at`。 +- 模式只能是 `off|observe|enforce`,输出继续复用 T-635 的统一映射: + `off -> false/false`、`observe -> true/false`、`enforce -> true/true`。 +- 新增不可修改的策略审计记录,保存 `previous_mode`、`mode`、必填 `reason`、 + `changed_by`、`changed_at`。审计不保存 API Key、用户权益、点数、订单、prompt + 或图片。 +- 迁移只创建空表,不写预置策略记录。策略解析顺序固定为:数据库单例存在时优先 + 使用其 `mode + updated_at`;记录不存在时回退 + `CMSHOPEE_CLIENT_SUBSCRIPTION_POLICY` 与 + `CMSHOPEE_CLIENT_SUBSCRIPTION_POLICY_UPDATED_AT`。 +- 公开接口每次请求读取单例,不使用进程内缓存;因此 admin 保存成功后下一次请求 + 即生效。数据库不可用仍按既有 Django 请求失败语义处理,不得静默发布错误策略。 + +### django-admin + +- 日常导航只增加“客户端订阅策略”一个入口;审计仅作为该详情页的只读 inline, + 不单独出现在导航。 +- 仅超级管理员可新增或编辑;不允许删除。首次创建后禁止新增第二条记录。 +- 表单除 `mode` 外必须填写“变更原因”。服务层负责事务内写单例与审计,禁止通过 + admin 直接 `save()` 绕过审计。 +- 展示当前 `CMSHOPEE_SUBSCRIPTION_MODE` 为只读信息,明确它是服务端授权模式; + 本页不得修改该环境变量,也不得修改权益、套餐、席位、凭证、订单、钱包或点数。 +- 相同模式不应制造伪变更审计;要求运营选择不同模式后再保存。 + +### API 与兼容 + +- `GET /api/v1/client/releases/latest?platform=windows` 顶层 `client_policy` 结构、 + `policy_version=1`、字段类型和 `Cache-Control: no-store` 保持 T-635 合约。 +- 有发布、`release:null` 和无下载地址路径均使用同一策略来源;既有 + `force_update`、`size_bytes` 等发布字段不回归。 +- 此接口仍匿名只读、无账号上下文,不读取 `SoftwareEntitlement`、套餐、点数、 + 订单、API Key 或生成模型;新增的仅是全局策略配置读取。 +- 不修改 `CMSHOPEE_SUBSCRIPTION_MODE`,不改变通用或专属生成接口、计费预扣 / + 确认 / 退款、支付回调、软件订单、异步图片任务或旧客户端行为。 + +## 部署与回滚 + +1. 备份生产应用和数据库,发布代码并执行 `migrate`;迁移不会创建策略记录。 +2. 首次部署后确认 `.env` 兜底策略仍为 `off`,公开接口保持 + `subscription_check_enabled=false`、`subscription_enforcement_enabled=false`。 +3. 超级管理员可创建单例并按 `off -> observe -> enforce` 灰度;每次保存立即以新的 + `updated_at` 发布,无需重启。 +4. 需要紧急取消客户端门禁时,在后台将模式改为 `off`;若后台记录尚未创建,继续 + 使用 `.env` 兜底。不要把此操作误认为取消服务端授权。 +5. 若需恢复到纯 `.env` 来源,必须先在维护窗口以受控数据操作移除单例记录;本任务 + 的 admin 不提供删除入口,避免误删和审计丢失。 + +## 验收条件 + +1. 无后台记录时三种合法 `.env` 配置保持 T-635 映射与启动校验。 +2. 后台记录存在时优先于 `.env`;保存 `observe` / `enforce` 后下一次公开请求立即 + 返回正确双布尔值与稳定、带时区的 `updated_at`。 +3. 模型层和后台均不能创建第二条记录,后台不能删除;非超级管理员不能新增或编辑。 +4. 每次有效变更都写一条只读审计,包含前后模式、非空原因、操作者和时间;相同模式 + 被表单拒绝,不写伪审计。 +5. 审计不出现在 admin 日常导航,策略页可只读查看历史;服务端授权模式只读展示。 +6. 版本检查接口继续匿名、`Cache-Control: no-store`,且发布字段和无发布路径不回归; + 不查询或泄露用户、权益、套餐、点数、订单、API Key 或密钥。 +7. 同步更新 `docs/api.md`、`docs/env.md`、`docs/deployment.md`、 + `docs/04-architecture.md`、`docs/current-state.md`、任务状态、`progress.md` 和 + Obsidian 客户端契约。 +8. `makemigrations --check --dry-run`、`migrate`、`check`、目标 API / licensing / + admin 测试、`init.ps1` 与 `git diff --check` 通过。 + +## 状态 + +TODO。