7.1 KiB
K1 客户端对接:详情 + 播放 + 进度续看 + 收藏(看剧闭环)+ UI 调整 —— 计划(2026-10-16)
依据:
docs/plans/INBOX.md19: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。
四、风险 / 待确认
- 改动面比"只加接口"大:
WatchHistoryRepository的签名必须改(服务端是"每剧一条续看记录", 没有 profile 概念),这会连带改 4 个 logic + 6 个测试文件。这是接口模型决定的,不是顺手重构。 - 未登录的边界:
/app/watch/*与/app/favorites/*需要登录态。客户端策略: 未登录时不发这些请求(浏览/试看仍可匿名),收藏按钮沿用"先引导登录"。 - 票据过期无法从响应判断(契约里没有 expiresAt 字段),所以用**客户端 TTL(10 分钟)**保守失效重取;
若后端后续补
expiresAt,直接换成按字段判断即可(口径已写进计划)。 /app/watch/recent只给 dramaId/episodeId/位置,没有剧名封面 —— K1 里"我的"页用 已加载的首页/发现卡片按 id 映射显示;映射不到就不显示(K2 的记录页再做成独立请求)。- 联调测试需要登录态:新增的 live 测试会注册一个一次性邮箱账号,跑完在库里清掉(回执写明)。