# T-632 会员订阅三阶段启用与简化运营 ## 背景 cmhub 已具备软件套餐、用户权益、软件订单、支付回调、授权事件和设备观测能力,但开发与线上测试阶段若要求运营逐个给存量用户绑定长期套餐,操作成本高且容易误伤已有用户。 当前 `CMSHOPEE_SUBSCRIPTION_ENFORCEMENT=false` 只保证产品专属生成接口不阻断;订阅状态接口仍返回真实的 `required` / `expired`,且响应缺少桌面端已依赖的账号、套餐代码、顶层有效期、会员中心和通知字段,因此桌面端可能把有效 API Key 误判为“需要配置会员账号”。 ## 目标 1. 用 `open`、`shadow`、`enforce` 三种明确模式分阶段启用账号订阅。 2. 开发测试阶段无需批量创建虚假 `SoftwareEntitlement`,存量有效 API Key 可继续使用。 3. 订阅状态接口返回桌面端可直接消费的完整、向后兼容结构。 4. django-admin 软件授权首页只展示“会员套餐”和“用户会员”两个日常入口,其他模型保留数据与直接管理地址。 ## 范围 ### 配置 - 新增 `CMSHOPEE_SUBSCRIPTION_MODE`,仅接受 `open` / `shadow` / `enforce`。 - 新配置缺失时兼容旧 `CMSHOPEE_SUBSCRIPTION_ENFORCEMENT`:旧值 `true` 等价于 `enforce`,否则等价于 `open`。 - 非法模式必须在 Django 启动配置阶段明确报错,不能静默回退。 ### 授权 - `open`:有效 API Key 统一获得开发测试访问,不要求数据库权益,不写入虚假权益。 - `shadow`:有真实有效权益时返回真实套餐;无权益或过期时继续放行并返回兼容访问,同时记录真实权益状态。 - `enforce`:只有真实有效或仍处于宽限期的权益可提交产品专属任务。 - 通用 `/api/v1/generate/*` 不应用软件订阅限制。 - 已接受的异步任务详情、轮询和下载不追加订阅拦截。 ### 状态接口 `GET /api/v1/cmshopee/subscription/status` 保留现有 `product_code`、`status`、`allowed`、`code`,并补齐: - `account.display_name` / `account.username` - `plan.code` / `plan.display_name` - 顶层 `expires_at` / `grace_expires_at` - `manage_url` - `notice_id` - `access_source` - `entitlement_status` 其中 `status` 表示当前运行模式下的最终访问状态,`entitlement_status` 表示数据库真实权益状态;两者在 `open` / `shadow` 下允许不同。 ### admin - 将 `SoftwarePlan` 展示名改为“会员套餐”。 - 将 `SoftwareEntitlement` 展示名改为“用户会员”。 - admin 应用索引只展示上述两个模型。 - `SoftwareOrder`、`LicenseEvent`、`ClientDevice`、`DeviceSession`、`DeviceBindingAudit` 以及 T-631 已隐藏的历史授权模型继续注册,保留权限控制、历史数据和直接管理地址。 ## 实施约束 - 模式切换不得创建、续期、撤销或批量修改用户权益。 - 不修改用户 API Key、钱包、点数流水、充值订单或软件订单状态。 - 不删除授权、订单、设备或审计表。 - 日志不得记录 API Key、prompt、原始设备标识或图片。 - 响应新增字段保持向后兼容,不能删除 T-630 已发布字段。 - `manage_url` 只能由服务端 `PUBLIC_BASE_URL` 构造。 ## 验收条件 1. `open` 下无真实权益用户的状态接口返回有效访问,产品专属 submit 可用,数据库不新增权益。 2. `shadow` 下无真实权益用户继续可用,响应和结构化日志能区分最终放行与真实权益缺失。 3. `enforce` 下无权益与过期用户分别返回 `subscription_required` / `subscription_expired`,且拒绝发生在预扣与上游调用前。 4. 有效与宽限期权益返回真实套餐、稳定套餐代码和正确顶层有效期。 5. 状态接口包含桌面端要求的账号、套餐、有效期、会员中心和通知字段;旧字段仍存在。 6. 新配置缺失时兼容旧布尔开关;非法模式不能通过配置检查。 7. admin 首页仅显示“会员套餐”和“用户会员”;隐藏模型的直接管理地址和历史数据仍可访问。 8. 通用生成、点数、充值、软件订单和异步任务读取回归通过。 9. `manage.py check`、目标测试、迁移一致性、编译检查和 `git diff --check` 通过。 ## 状态 DONE。 ## 实施结果 - 配置层新增 `CMSHOPEE_SUBSCRIPTION_MODE` 严格枚举校验;新配置缺失时按旧 `CMSHOPEE_SUBSCRIPTION_ENFORCEMENT` 映射,运行时代码统一读取有效模式。 - 授权服务将数据库真实权益判定与最终访问判定分离;`open` / `shadow` 不写 `SoftwareEntitlement`,`enforce` 才在产品专属 submit 预扣前拒绝。 - 订阅状态接口已补齐账号、套餐代码/显示名、顶层有效期、会员中心、通知 ID、访问来源和真实权益状态;保留 T-630 的 `plan.name` 与套餐内有效期字段。 - 授权日志同时记录真实 `would_reject`、最终 `final_allowed`、模式、来源和真实权益状态,不记录 API Key、prompt 或图片。 - admin 应用索引只保留“会员套餐”和“用户会员”;其他模型仍注册并可通过直接管理地址访问。新增 `licensing.0005_alter_softwareentitlement_options_and_more`,仅修改模型显示名称。 ## 验证结果 - `manage.py check`:通过,0 issues。 - `manage.py test apps.licensing.tests.SubscriptionModeUnitTests --noinput --verbosity 2`:5 tests OK,覆盖旧开关映射、open 合约、shadow 观测、真实宽限期和 admin 导航。 - `manage.py makemigrations --check --dry-run`:No changes detected;检查迁移历史时旧远程 MySQL `43.128.3.240` 拒绝连接并给出 RuntimeWarning,不影响迁移文件一致性结果。 - `compileall config apps/licensing apps/api`:通过。 - 非法 `CMSHOPEE_SUBSCRIPTION_MODE` 启动检查:按预期抛 `ImproperlyConfigured`。 - 依赖数据库的 70 条 API/admin 定向测试已尝试,但本地 `.env` 指向的 `43.128.3.240` 拒绝连接;改用本机 MySQL 时项目账号无本机权限,因此本轮未取得数据库集成测试通过结果,未将其误记为通过。