# Dramivio 消费者与后台边界复审

复审日期：2026-10-03。独立复审只修改本报告与本地复现工具，没有修改业务源码、发布服务或连接生产数据库。

本轮发现 1 个 P1、3 个 P2。作者修复后，四个问题均完成独立复测并关闭。这个结论只覆盖当前邮箱身份、消费者查询、配置发布和 App 凭据边界，不代表额度、邀请、离线缓存等后续模块已完成。

## 发现及复测

| 级别 | 问题 | 修复后的独立复测 |
| --- | --- | --- |
| P1 | Payload 公开首个用户注册绕过管理员权限 | 空库与删除最后管理员后均返回 403；公开请求没有创建用户；可信本地初始化仍能创建管理员 |
| P2 | 部分渠道展示统一版本的总集数 | 统一版本共 3 集、选中渠道仅 1 集时，返回 `episodeCount=1`、`canonicalEpisodeCount=3` |
| P2 | 配置发布与查询快照创建并发，旧条件仍能分页 | 在快照插入前暂停请求、发布新标签含义后恢复，请求返回 `409 invalid_cursor` |
| P2 | 退出前的迟到 SDK 会话响应恢复旧登录 | 模拟退出失败与迟到续期响应，本地 Cookie 始终为空，旧会话结果返回 `credentials_changed` |

### P1：公开首个用户注册

位置：`短剧库/services/admin/src/config.mjs:43`。

在已迁移但没有管理员的隔离数据库上，匿名调用官方 REST 入口 `POST /api/admins/first-register`，提交邮箱、密码及 `role=admin`。修复前返回 200，随后保存用户端配置和发布均返回 200；预期的可信初始化反而被已有用户阻止，返回 409。

原因是 Payload 的首个用户注册操作使用 `overrideAccess=true`，同时绕过集合创建权限和角色字段权限。框架默认行为不能仅靠 `access.create` 限制。

现在 `admins` 集合显式覆盖公开 `/first-register`，返回 `403 bootstrap_required`。使用同一个官方 `handleEndpoints` REST 入口和真实 PostgreSQL 复测：空库拒绝、公开创建数量为 0；可信 `bootstrapAdministrator` 创建成功；删除最后管理员后公开注册仍拒绝。

### P2：渠道集数与版本集数混用

位置：`短剧库/services/platform/src/catalog.mjs:151`。

将渠道 B 的 1 集，经明确的集数映射，合并到渠道 A 的 3 集版本。仅授权渠道 B 的查询在修复前返回 B 的播放入口，却展示 3 集，后两集无法通过该入口播放。

现在查询按选中渠道的 `source_episode` 数量返回可播集数，另用 `canonicalEpisodeCount` 表达统一版本总集数。独立真实数据库复测得到 1 与 3，两个含义分开。

### P2：发布与快照插入竞态

位置：`短剧库/services/platform/src/catalog.mjs:126`。

修复前，查询先读旧标签含义和匹配结果，在快照插入前暂停；后台把同一标签 ID 从 `love` 改为 `action` 并发布、删除已有快照；恢复查询插入后，旧游标仍能继续返回 Love，而新查询已经返回 Action。

现在快照复用既有配置 `revision`，续页校验版本，并在返回前再次检查发布版本。独立真实数据库再次暂停插入、跨发布恢复，得到 `409 invalid_cursor`。没有新增内容哈希或自制发布基线。

### P2：迟到会话恢复已退出身份

位置：`juku-mobile/src/auth/client.ts:108`、`:118`。

修复前，先发起 SDK `getSession` 并延迟其带续期 Cookie 的响应，再执行退出。退出请求返回 503，原始只读对账确认服务端会话尚未撤销，此时本地 Cookie 已清空。放行旧 SDK 响应后，Expo 插件再次把旧用户 Cookie 写回 SecureStore。

现在使用框架的 `disableRefresh` 参数；退出取消旧请求并使旧客户端失效；SecureStore 写入按同一来源排队，拒绝旧客户端的迟到写入。Cookie 解析与登录协议仍由官方 SDK 负责。

使用真实 Better Auth 与 Expo 1.7.7 SDK，配合本地协议响应夹具独立复测：`revoked=false`，退出时和迟到响应后的 Cookie 均为空，旧会话返回 `credentials_changed`。这证明本地退出保持有效，并没有把服务端尚未撤销的会话说成已撤销。

可复用复现工具位于：`/Users/alienmu/Documents/Codex/2026-10-02/bang/work/existing-project/consumer-auth-race-review.cjs`，导出 `runConsumerAuthRaceReview`。

## 额外确认

- 真实管理员在数据库中降为运营后，原 JWT 的 `/api/admins/me` 返回最新运营角色，发布立即返回 403。未发现角色撤销后仍能发布的问题。
- 消费者身份由 Better Auth 的有效、已验证邮箱会话产生；Go 没有把消费者身份提升为旧管理账号。查询服务不采用请求体中的伪造用户身份或 `authenticated` 标志。
- 内部服务要求桥接凭据，Go 配置限制为回环地址；渠道权限由 Go 传入，查询会校验游标所属用户、查询条件及当前渠道集合。
- 后台发布入口使用 Payload 实际 `req.user`，Local API 读取配置时保留 `overrideAccess=false`。消费者身份及只读运营角色不能发布。

## 验证范围

数据库复现使用独立 PostgreSQL 实例和官方迁移；后台权限复现经过 Payload 官方 REST 请求处理器，未依赖伪造的管理员对象。App 复现使用真实官方 SDK，但网络响应为本地协议夹具，没有替代真机、真实邮件投递或公网部署验收。

本轮没有测试邀请码奖励、播放额度、历史删除后的额度保持、完整离线下载或收费广告流程。它们仍按产品开发任务逐项实施和验收。

渠道重新归并时，如果旧统一剧集已经存在追剧或观看状态，当前实现返回 `409 mapping_state_review_required`，要求进入明确的状态迁移流程。现阶段不能宣称这些状态会自动迁移；本报告没有完成该后续迁移模块的验收。内容导入集数上限已与现有 Go 接口的 10000 保持一致。
