# Dramivio 播放进度边界复审

2026 年 10 月 3 日。范围为当前工作区 H00 的逐集记录、续播、删除 epoch、会话 revision/CAS、App 离线队列，以及相关 Go 和 Better Auth 入口。发现并复现 3 个 P2；主线修复后，3 项均经独立复验关闭。本轮检查范围内没有遗留的已证实 P1/P2。

复审期间业务代码只读。复现使用隔离的 PostgreSQL 18 RAM 容器、真实服务端函数和当前 App 代码；未发布、未连接生产、未访问外部媒体。所有本轮测试容器均通过清理路径关闭。复现文件保存在当前任务的 `work/progress-review/`。

1. **P2：读取状态与 begin 之间的映射变化可把进度记到另一集。已关闭。**

   原实现只携带源剧 ID、源集号和 epoch/revision。真实 PostgreSQL 复现了两条路径：首次播放尚无历史时，把源映射到另一部 canonical 剧；以及同一 edition 把源第 1 集重排到 canonical 第 2 集。两者的旧请求都曾被接受，37 秒位置最终写入新身份。单纯增加事务内表锁不能约束此前读取的身份。

   当前 begin 校验 强制要求并核对 `canonicalId` 和 `episodeId`；App begin 输入 携带相同身份，原始播放集在首次读取时也核对 `episodeId`；resume/open 已传递该字段。目录映射 也阻止对已有状态进行未迁移的集身份重排。

   修复后，旧 canonical 返回 `drama_identity_mismatch`，旧 episode 返回 `episode_identity_changed`，缺少身份返回 `invalid_playback_session`；旧请求没有创建 session/history，新读取的有效身份仍可开始播放。原始证据：`mapping-race.mjs`、`mapping-race-result.json`。复验：`mapping-fix-verify.mjs`、`mapping-fix-result.json`。

2. **P2：追剧页继续观看会重播已完成的一集。已关闭。**

   原 FollowingScreen 续播入口 读取指定单集状态，并在 `completed` 时将位置改为 0。用真实 report 完成第 1 集后，服务端 resume 已返回第 2 集 0 秒，但执行真实页面的点击处理函数仍导航到第 1 集 0 秒。

   当前入口调用 `fetchConsumerResume`，使用返回的集号、位置、暂停状态和身份 capture，并拒绝已删除的历史。相同复验现在导航到第 2 集 0 秒。此检查执行了实际页面函数和点击处理逻辑；React Native 渲染与导航为测试替身，状态接口连接真实 PostgreSQL。原始证据：`client-pg-boundaries.cjs`、`client-pg-result.json`。复验：`client-pg-fix-verify.cjs`、`client-pg-fix-result.json`。

3. **P2：下一未完成集缺失时会跳集，或阻止重新打开当前集。已关闭。**

   canonical 集序为 1、2、3，仅授权部分源时，完成第 1 集后的行为曾为：映射 `[1,3]` 直接跳到第 3 集；映射 `[1]` 返回 409。后一结果会阻断默认使用 consumer resume 的 Go open，尽管当前完成集仍可重看。这不符合续播规则要求的“下一集不可用时停留当前完成状态”。

   当前 resume 选择 只检查首个下一未完成集。缺失时保留当前集并暂停。两种部分源的真实 PostgreSQL 复验均返回第 1 集、10 秒、`completed=true`、`resumePaused=true`。原始证据：`resume-gap.mjs`、`resume-gap-result.json`。复验：`resume-gap-fix-verify.mjs`、`resume-gap-fix-result.json`。

本轮独立验证的其他边界如下。

| 检查 | 结果与证据范围 |
| --- | --- |
| 旧 open 后清历史，再收到首帧 | 实际 AccountProgress → PostgreSQL 联动：不 begin，不复活历史 |
| 新设备接管后旧页面重试或推进旧 run | 同一联动：旧 report 被拒，旧页面不重新抢回；新设备 50 秒位置和 revision 2 保留 |
| 离线恢复位置 | 恢复的 10 秒不增加覆盖，也不使紧随其后的首次 live 区间增加覆盖；再下一段正常 live 增加 1 秒 |
| 乱序与后退 seek | sequence 2 不覆盖已接收的 sequence 3；sequence 4 可把位置从 30 秒拖回 10 秒 |
| 不同 owner 上报 | 真实服务端按 session + owner 拒绝；请求体不能指定其他账号身份 |
| owner/origin 队列和退出后的迟到 ack | App 确定性测试通过；分区、清除和较新 bookmark 不被迟到 ack 破坏 |
| 幂等、过期与维护 | 当前 PostgreSQL 测试确认相同 begin 重试不重复创建；过期 session 不能上报或复活；维护只删过期且未被历史引用的会话 |
| 实际认证入口 | Better Auth 1.7.7 + PostgreSQL HTTP fixture 通过，使用动态回环端口；OTP 邮件仅在内存捕获；伪造 userId、无登录写入、退出后写入均未获授权 |
| Go 路由 | 独立运行 verified identity/revocation、auth proxy 和 consumer config 三个实际 router 测试，均通过；源码核对消费者 legacy 文件进度写入已拒绝 |

修复后独立回归结果：`services/platform/tests/state.test.mjs` 35/35；App `consumer-progress.test.cjs` 10/10；上述三个 Go 路由测试通过；动态端口的实际认证 HTTP fixture 1/1。原始复现脚本保留原失败断言用于审计，修复后的成功断言位于单独的 `*-fix-verify` 文件。

此结论不等于 H00 全端验收完成。复审时 Web H00 接入仍由主线标记为未完成；本轮未声称 Web 播放进度已通过。本轮也未测真实 Android/iOS 媒体播放、系统强杀及“约 10 秒退出保存误差”。`validWatchSeconds` 仍是保守的客户端位置估计，不构成邀请或额度证明。
