# App 缓存订单、选集与离线播放

2026-10-04。本轮接通 O01 后端订单与 App 页面，沿用 Better Auth、现有额度账本、Zustand/MMKV、react-native-video、Media3 和 Apple 系统下载。网站只处理订单、许可和来源地址，不下载或保存视频字节。真实来源签发器尚未接入，默认拒绝交付，不先扣次数。

## 页面与流程

详情页「缓存」进入选集；四列集号，一次最多 20 集。已缓存集不可再选，不占选择名额。确认弹窗显示所选、新增集数和剩余次数；确认后才创建订单。下载页显示排队、授权确认、下载进度、暂停、文件缺失、过期与删除状态，提供播放、暂停、继续、重试和取消。

服务端许可可用不等于文件已下载。只有当前账号、同站点、有效许可和真实原生索引里的完整文件同时匹配，才显示「已缓存」和播放按钮。离线播放复用当前播放器、选集、倍速、App 长按倍速与进度条；只列本机可用集。

![中文选集](offline-app-preview/select-zh.png)

![英文队列](offline-app-preview/queue-en.png)

以上截图来自实际 RN 页面组件在 Chrome 中的渲染。原生索引、接口、封面和导航是明确的展示适配器，不能作为手机截图或真实媒体下载证据。

## 次数、队列与恢复

| 操作 | 行为 |
| --- | --- |
| 预览、加入队列 | 校验账号、批准的 App 会话、来源与 canonical 集映射；不扣次数，不占额度预留 |
| 开始下载 | 原生有空位才执行 prepare；同账号跨组件实例共用调度锁，安排前重新读取原生索引；App 自动队列最多同时安排两个任务，长队列不提前占当日次数 |
| 授权提交 | 使用同一个 canonical 解锁账本；在线与缓存同一集当天只记一次。额度提交与订单许可状态在同一 PostgreSQL 事务中完成 |
| 跨日等待、预留过期 | 真正准备下载时使用当前日额度；失效预留沿用资产更换，不能拿旧日的未提交预留继续扣款 |
| 供应商预检失败 | 释放未提交预留，可重试；设备上限导致提交回滚也释放预留 |
| 签发响应未知 | 保留已提交事实。先按原资产查询供应商，不盲目再次签发或退款；待核对时不提供播放地址 |
| 本地订单保存失败 | 不发创建请求；即使内存保留记录，恢复重放前仍需重新成功保存原订单号 |
| 原生入队结果未知 | 本轮停止安排新集；下一次读取实际索引确认槽位，列表采用真实原生状态，不把正在下载的任务显示为可破坏性重试的失败 |
| 订单请求超时 | 请求前保存原订单号与选集；重启用原号查询，只有权威 404 才重放原创建。恢复不会另造订单号 |
| 取消未知订单 | 同一个订单号确认创建后取消，防止超时请求晚到创建孤儿订单。任何失败保留待清理记录 |
| 取消与原生入队同时发生 | 等待在途入队，补偿删除迟到任务；删除失败阻止忘记订单。正在删除的原生记录继续可见以便重试，不返回可播放 URI |
| 退出、换号、注销 | 继续使用 origin + UID 的原生清理与失败重试；旧结果不能进入新账号队列。终止账号的后台清理删除订单与来源 payload，保留额度审计 |

许可最长七天，同时受来源权利期限、安装批准和实际官方会话期限限制。数据库限制同一账号最多两个有未到期许可的设备；这只是批准设备的账本限制，硬件证明仍待接入，不能当作复制文件防护。

来源、offline policy 或 E00 capability revision 改变后，旧签发的晚结果不交付。已提交的次数保留；不会声称能远程即时收回已经离线的文件。

## 复用与接口

身份仍由 Better Auth 官方邮箱 OTP 和 Expo 插件处理。Go 根据当前官方消费者登录、已签名的安装标识和来源权限组装 actor；用户 body 不能指定 owner、device、App 身份、issuer、URI 或退款。内部命令继续只由现有私有服务入口接收。

公开接口位于 `/api/consumer/offline/`：

| 接口 | 方法 | 用途 |
| --- | --- | --- |
| preview | POST | 校验选集与新增次数；不返回视频地址 |
| order | POST | 使用调用者已保存的 orderId 创建元数据订单 |
| status?orderId=… | GET | 当前账号、当前设备查询；不调用签发器、不返回来源地址 |
| prepare | POST | 重新核对官方会话与来源后，为一个资产恢复或签发许可 |
| cancel | POST | 禁止后续交付，释放未提交预留；已提交观看事实保留 |

后端迁移为 `006-offline.sql`。`publishOfflineSourcePolicy` 复用已有后台审核记录，只允许明确接受 private_app_policy 的非 DRM 来源及 MP4/HLS 格式；默认关闭。真实 DRM 不能通过改一个标记启用。

`createPlatformRuntime({ ..., offlineProviders })` 接受可信服务器适配器注册表。适配器提供普通 `preflight(input,{signal})`、`lookup(input,{signal})`、`issue(input,{signal})` 方法；资产标识稳定，lookup 必须明确区分 pending、not_issued 和已签发。调用有有限超时；超时不能证明第三方动作未发生。注册表不接受消费者请求传入。

App 使用 `createOfflineAPI(owner)` 和 `createOfflineQueue({ owner,current,read,write,api,native })`，可以普通导入和传参。订单沿用现有 `juku.downloads.v1` 存储，不另建下载器；只持久化剧名、选集、资产状态、许可摘要及本机位置。来源 URI 和请求头仅短暂交给原生下载器，不进入 JS 订单存储。

## 实际验证

| 检查 | 当前结果 | 范围 |
| --- | --- | --- |
| platform 全量 | 175/175 | 真实隔离 PostgreSQL 18、Better Auth OTP/session 与生产 HTTP/domain；邮件收集和签发 metadata 为受控适配器 |
| O01 作者后端 | 19 个领域、5 个 HTTP 实质子用例通过 | Node 含两个 suite 的计数为 26，已包含在平台全量内 |
| Go 定向回归 | 通过 | 消费者网关、额度/账号及直连/转发/代理边界；内部服务响应为适配器 |
| App 全量及类型 | 256/256，类型通过 | API、元数据队列、真实 React 页面及既有账号/播放回归；原生和网络故障使用明确适配器 |
| 中英文组件页面 | 8 个状态通过 | Chrome，选集/确认、队列/取消；不计真机 |
| Android 完整测试 APK | 已构建、实际产物检查通过 | 本机测试地址、现有 debug 签名；不安装、不发布 |

独立子 Agent 对后端和 App 做了对抗复审，复现、修正和复验见[本轮复审](dramivio-offline-order-review.md)。更早的 19 项实际 Android Media3 测试及 119 项 macOS Foundation/AVFoundation 检查仍按各自范围记录，不能混算为本轮真机测试。

## 后续拆解与验收

1. **真实标准 API 来源适配。** 接真实离线许可、幂等签发和 lookup；从已批准来源选一部可测试剧。先验证未启用默认拒绝、权利撤销、过期与错误目的地址，再验证真实 MP4/HLS 下载。真实 DRM 单独设计和验收。
2. **选清晰度与容量。** 依据来源实际版本增加 720p/1080p、预计容量、剩余空间和空间不足处理。不能把转码假作已有版本；当前尚未提供清晰度选择。
3. **后台与飞行模式。** 当前已入原生的任务由系统继续处理；剩余选集只有 App 活跃时由 JS 申请许可。继续接宿主后台生命周期、重启与任务唤醒，验证冷启动飞行模式的有效身份策略、强退、系统逐出、来电和低内存。
4. **离线进度同步。** 本轮记录的是本机每个缓存资产的集号、位置和时长，播放/暂停/退出可恢复。接 canonical 的离线待同步记录、跨设备冲突和补传；不得凭客户端位置生成邀请合格观看或额外奖励。
5. **完整 iOS 与两端真机。** 完整 UIKit/React 构建、iPhone/Android 实际播放、seek、断网、HLS 子请求和删除失败。macOS SDK 检查、语法 parse 和 Android APK 构建不能替代这些步骤。

广告、收费、真实邮件、后台完整运营配置与真实 App 资格继续按既有任务卡实施。网站直连优先、失败才转发的节约流量路线保持；本轮只更新本地代码与静态文档，没有发布 API、安装包或 OTA，也没有提交/推送仓库。
