Files
FruitServer/docs/plans/2026-10-16-K1.md
T

96 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# K1 客户端对接:详情 + 播放 + 进度续看 + 收藏(看剧闭环)+ UI 调整 —— 计划(2026-10-16)
> 依据:`docs/plans/INBOX.md` 19:40 条目。用户指令:「现在你要做的事对接客户端接口及 UI 调整,
> 仍然严格按照 specs 执行」→ 本轮做客户端 `ReelShort`(Flutter),目标是一条完整看剧闭环:
> **首页点进剧 → 详情正确 → 能起播 → 进度能续看 → 能收藏**,全链路走真接口。
> 契约以 `FruitServer/docs/api-phase1.md` §1.4 / §1.5 为准。
## 一、勘察更正(先说清,免得后面对不上)
条目里写「客户端只调了 6 个接口、没有详情和票据」—— 这条**不准确**,实测:
| 接口 | 现状 |
| --- | --- |
| `GET /app/dramas/{id}` | **已接**:`CatalogApi.dramaDetail` + `CatalogController.requireDetail`,详情页在用 |
| `GET /app/episodes/{id}/play` | **半接**:`CatalogApi.playTicket` + `CatalogController.resolvePlay` 存在,但**没有任何页面调用**;详情下发的 `playUrl`(免费集已带签名)被直接使用 |
| `/app/watch/*` | **没接**:`WatchHistoryRepository` 是 `SharedPreferences` 本地实现,`demoProfileId` 假身份 |
| `/app/favorites/*` | **没接**:收藏走本地 `EngagementController`(SharedPreferences),点赞也是本地(点赞属 K3,不动) |
所以本轮真正要做的是:**播放票据接上并带过期重取**、**进度从本地换远程**、**收藏换服务端**、
**三态 UI**,以及把详情页已有的加载/失败态补齐(空态、续看进度条)。
## 二、方案(分层:api → repository → logic → view)
### 1. 新增 api 类(`lib/core/data/remote/`)
| 文件 | 方法 | 对应接口 |
| --- | --- | --- |
| `playback_api.dart`(新增) | `ticket(episodeId)` | `GET /app/episodes/{id}/play` |
| `watch_api.dart`(新增) | `saveProgress` / `progressOfDrama` / `recent` | `POST /app/watch/progress`、`GET /app/watch/progress?dramaId=`、`GET /app/watch/recent?limit=` |
| `library_api.dart`(新增) | `toggleFavorite` / `favorites` / `removeFavorite` | `POST /app/favorites/toggle`、`GET /app/favorites`、`DELETE /app/favorites/{dramaId}` |
票据调用从 `CatalogApi` **搬**到 `PlaybackApi`(条目要求新增 playback_api),
`CatalogRepository.resolvePlay` 随之移到新的 `PlaybackRepository`——这是本轮唯一"动到既有目录层"的地方,最小接线。
### 2. repository(保留抽象,测试注入假实现)
- `playback_repository.dart`(新增):`ticketFor(episodeId, {forceRefresh})`。
`HttpPlaybackRepository` 内含**短时缓存**:默认 TTL 10 分钟(签名有效期契约是 15 分钟,留余量),
**时钟与 TTL 可注入** → 过期自动重取,测试用假时钟钉死。
- `watch_history_repository.dart`(**改签名**):服务端按 `(user, drama)` 存一条续看记录,
所以接口从 `(profileId, dramaId, episodeId)` 变成
`progressFor(dramaId)` / `save({episodeId, position, duration, finished})` / `recent(limit)`;
`HttpWatchHistoryRepository` + `InMemoryWatchHistoryRepository`(测试用);
`demoProfileId` 删除;`clampResumePosition` 保留(续看钳位规则不变)。
- `favorite_repository.dart`(新增):`toggle` / `list` / `remove`;HTTP + 内存实现。
### 3. controller / logic
- `favorite_controller.dart`(新增):`savedIds` + `cards` + `loading/error`,
`toggle(dramaId)` **乐观更新 → 失败回滚**(`DELETE`/`toggle` 任一失败都回到原状态)。
- `DramaDetailLogic` / `MineLogic` / `PlayerLogic`:改用新的 `WatchHistoryRepository`,
续看点从 `WatchProgress(episodeId, positionMs)` 来(不再是本地 episodeId 字符串)。
- `PlayerLogic`:
- 起播前 `ticketFor(episode)`,把签名地址写回 session 的剧集对象;**地址原样交给播放器**
(`AppConfig.mediaUrl` 只补 host,不动 path/query —— 新增测试钉住 `?e=&s=` 原样保留);
- 票据拿不到 / `accessType=COIN_REQUIRED|NO_MEDIA` → 由**后端返回值**驱动 UI(锁定/无片源提示 + 重试);
- 进度上报**节流 10 秒**(原实现是 5 秒),切集/退出/切后台各补一次,`finished` 在播完时上报。
### 4. UI
- 详情页:加载中 / 失败(可重试)/ 空剧集三态;续看位置在"继续观看"按钮与对应剧集行上给进度条。
- 播放页:票据加载中 / 失败(可重试)/ 锁定(`accessType` 驱动)三态;收藏按钮真实状态 + 回滚 +
失败提示(未登录先引导登录,沿用现有 `ensureSignedIn`)。
- 我的页:最近观看卡片三态(服务端续看位置驱动)、收藏列表三态,去掉 `demoProfileId` 假身份。
- 文案:新增键写进 `app_en.arb` / `app_zh.arb` / `app_id.arb`,`flutter gen-l10n` 重新生成。
## 三、影响面清单
新增:`playback_api.dart`、`watch_api.dart`、`library_api.dart`、`watch_progress.dart`(模型)、
`playback_repository.dart`、`favorite_repository.dart`、`favorite_controller.dart`。
改:`watch_history_repository.dart`(签名)、`catalog_api.dart`(去掉 playTicket)、
`catalog_repository.dart`(去掉 resolvePlay)、`catalog_controller.dart`(同步)、
`play_ticket.dart`(补 dramaId/number/previewSeconds)、`app.dart`(注入三个仓库 + FavoriteController)、
`drama_detail_{logic,view,binding}.dart`、`mine_{logic,view,binding}.dart`、
`player_{logic,view,binding}.dart`、`discover_{logic,binding}.dart`(续看点来源)、
`app_{en,zh,id}.arb` + 生成的 4 个 l10n 文件。
测试:改 `test/support/fake_watch_history.dart`、`test/playback/watch_history_test.dart`、
`test/catalog/catalog_controller_test.dart`、`test/support/fake_catalog.dart`、
`test/{discover,mine,drama_detail,player}/…`(续看点改签名);新增
`playback_ticket_expiry_test.dart`、`progress_throttle_test.dart`、`watch_history_remote_test.dart`、
`favorite_controller_test.dart`、`player_ticket_gate_test.dart`、`integration/live_watch_flow_test.dart`。
## 四、风险 / 待确认
1. **改动面比"只加接口"大**:`WatchHistoryRepository` 的签名必须改(服务端是"每剧一条续看记录",
没有 profile 概念),这会连带改 4 个 logic + 6 个测试文件。这是接口模型决定的,不是顺手重构。
2. **未登录的边界**:`/app/watch/*` 与 `/app/favorites/*` 需要登录态。客户端策略:
未登录时**不发**这些请求(浏览/试看仍可匿名),收藏按钮沿用"先引导登录"。
3. **票据过期无法从响应判断**(契约里没有 expiresAt 字段),所以用**客户端 TTL(10 分钟)**保守失效重取;
若后端后续补 `expiresAt`,直接换成按字段判断即可(口径已写进计划)。
4. **`/app/watch/recent` 只给 dramaId/episodeId/位置**,没有剧名封面 —— K1 里"我的"页用
已加载的首页/发现卡片按 id 映射显示;映射不到就不显示(K2 的记录页再做成独立请求)。
5. 联调测试需要登录态:新增的 live 测试会**注册一个一次性邮箱账号**,跑完在库里清掉(回执写明)。