up
This commit is contained in:
+55
-16
@@ -41,6 +41,13 @@
|
||||
└───────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
> **客户端层补充:App 内嵌本地媒体代理**(app/lib/core/media/local_media_server.dart)。
|
||||
> App 进程内启动 127.0.0.1 环回 HTTP 服务(shelf),网络 m3u8 播放地址统一改写走该代理:
|
||||
> m3u8 每次回源(签名 URL 有时效,不落盘);ts 分片按「去签名路径」落盘缓存、AES-128 密钥同步缓存;
|
||||
> 已下载视频经 `/local/<videoId>/<file>` 直接读下载目录。统一声明 `Accept-Ranges` 并支持 Range/206
|
||||
> (这是进度条可拖动的根因:ffmpeg/mpv 依据该头判断流可 seek,否则按直播处理)。
|
||||
> ts 流量由代理直连源站 CDN,不经过业务服务器。
|
||||
|
||||
### 1.2 部署架构(单机起步版,docker-compose)
|
||||
|
||||
| 容器 | 镜像 | 说明 |
|
||||
@@ -54,9 +61,11 @@
|
||||
|
||||
扩展路径:App 无状态 → 多副本 + SLB;MySQL → 云 RDS 主从;Redis → 云 Redis;转码 → 独立 GPU/多核 Worker 机器或云转码服务。
|
||||
|
||||
**当前生产形态(与上表演进关系)**:单台云主机。后端由 systemd 托管(`video-api.service`:`uvicorn app.main:app --host 127.0.0.1 --port 8000 --workers 2`),MySQL 8(容器 `mysql8`)与 Redis(容器 `my-redis`)跑在 Docker;项目根目录 `project-ctl.sh` 提供 start/stop/restart/status 一键启停(同时管 video-api.service 与两个容器)。注意:**`--workers 2` 下 settle_service 周期任务随每个 worker 的 lifespan 各启动一份**,任务逻辑必须保持幂等(按状态机推进、唯一约束防重),避免重复结算。
|
||||
|
||||
### 1.3 关键链路
|
||||
|
||||
- **播放链路**:App → `GET /hls/{vid}/index.m3u8` → Nginx → OSS/CDN(m3u8 与 ts 均为静态文件,天然适合 CDN 加速)
|
||||
- **播放链路**:App 将网络 m3u8 地址改写为内嵌本地代理(127.0.0.1)→ 代理回源取 m3u8(签名时效)→ ts 分片命中本地缓存直出,未命中直连源站 CDN 下载并落盘(1GB LRU)→ 全程支持 Range/206 拖动
|
||||
- **上传链路**:App → 获取上传凭证 → 直传 OSS(推荐,节省服务端带宽)或经服务端中转 → 回调发布 → 转码 → 审核 → 上架
|
||||
- **支付链路**:App 下单 → 调起支付 SDK → 平台异步回调服务端 → 验签 → 事务入账 → App 轮询查单确认结果
|
||||
|
||||
@@ -451,6 +460,7 @@ CREATE TABLE sensitive_words (
|
||||
| `sms:code:{phone}` | String | 300s | 短信验证码;同号 60s 内禁止重发(另建 `sms:limit:{phone}`) |
|
||||
| `email:code:{email}` | String | 600s | 邮箱验证码 |
|
||||
| `auth:refresh:{user_id}` | String | 30d | refresh_token 白名单(支持多端+踢下线) |
|
||||
| `auth:epoch:{user_id}` | String(计数) | - | 用户令牌纪元:后台改密/强制下线时 INCR,旧 Token 全部失效 |
|
||||
| `token:blacklist:{jti}` | String | token 剩余 TTL | 登出/改密失效 |
|
||||
| `watched:{subject}:{yyyyMMdd}` | Set(video_id) | 48h | 当日已看视频(推荐去重) |
|
||||
| `rec:seed:{yyyyMMdd}` | String | 48h | 当日推荐随机种子 |
|
||||
@@ -475,6 +485,8 @@ CREATE TABLE sensitive_words (
|
||||
| `words:sensitive` | String(JSON) | 60s | 敏感词库缓存(DFA 词源),后台增删后失效重建 |
|
||||
| `config:system` | Hash | 60s | system_configs 全量缓存,后台修改即时失效 |
|
||||
|
||||
> **连接注意事项**:Redis 客户端必须显式指定 `protocol=2`(RESP2)。线上服务端为 Redis 5.x,不支持 RESP3 的 HELLO 握手,而 redis-py 8.x 默认尝试 protocol=3,不显式指定会在连接期直接报错——这是踩过的坑(见 server/app/redis_client.py)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 接口设计(技术规范)
|
||||
@@ -541,24 +553,34 @@ Nginx limit_req(IP 维度)+ Redis 滑动窗口(主体维度)双层限流
|
||||
```
|
||||
app/lib/
|
||||
├── main.dart
|
||||
├── config/ # 环境配置、常量、主题
|
||||
├── router/ # go_router 路由表、登录守卫
|
||||
├── models/ # 数据模型(json_serializable)
|
||||
├── services/ # api_client(dio)、auth_service、storage、device
|
||||
├── providers/ # Riverpod providers(auth/feed/wallet/vip)
|
||||
├── pages/ # 页面(home/login/mine/wallet/vip/upload/...)
|
||||
│ └── home/widgets/
|
||||
├── widgets/ # 通用组件(按钮/弹窗/空态/互动栏)
|
||||
└── utils/ # 格式化、埋点、防抖
|
||||
├── router.dart # go_router 路由表、登录守卫
|
||||
├── models/ # 数据模型
|
||||
├── services/ # 接口封装(VideoService 等,基于 core/network)
|
||||
├── providers/ # Riverpod(12 个):auth/feed/comment/wallet/member/payment
|
||||
│ # /profile/search/upload/asset/notification/tag_videos
|
||||
├── screens/ # 页面:splash/home(HomeShell+IndexedStack)/feed/search/category
|
||||
│ # /player/membership/payment/wallet/assets/upload/profile
|
||||
│ # /auth/settings/notification/downloads
|
||||
│ └── feed/widgets/
|
||||
└── core/ # 子系统:
|
||||
├── config/ # 环境配置、常量
|
||||
├── media/ # local_media_server(127.0.0.1 本地代理+HLS 缓存)、download_manager(下载)
|
||||
├── network/ # api_client(dio 拦截器)、api_domain(域名发现)、endpoints
|
||||
├── player/ # feed_player_pool(3 槽播放器池)
|
||||
├── storage/ # token_store、local_store、观看去重、搜索历史
|
||||
├── theme/ # 主题(深色主主题;弹窗局部亮色)
|
||||
└── utils/
|
||||
```
|
||||
|
||||
### 5.2 关键技术点
|
||||
|
||||
| 点 | 方案 |
|
||||
|---|---|
|
||||
| 网络层 | dio 单例 + 拦截器:注入 `Authorization`/`X-Device-ID`/`X-Request-ID`;401 时刷新 Token 并重放原请求(单飞 Future 防并发刷新) |
|
||||
| 播放器 | media_kit 封装 `ShortVideoPlayer`:预加载下一条(keepAlive 双缓冲)、滑出 dispose、首帧秒开(m3u8 低延迟配置);双击点赞手势区与播放手势分层 |
|
||||
| Feed 滑动 | PageView(scrollDirection: vertical),onPageChanged 触发**观看上报**(防抖 500ms,同视频当日只报一次,本地记录) |
|
||||
| 网络层 | dio 单例 + 拦截器:注入 `Authorization`/`X-Device-ID`/`X-Request-ID`;401 时刷新 Token 并重放原请求(单飞 Future 防并发刷新);**无 refresh token 或刷新被拒时清登录态跳登录页,防"僵尸登录态"** |
|
||||
| 播放器 | media_kit 封装 + **3 槽播放器池**(core/player/feed_player_pool.dart):当前页播放 + 下一页预缓冲,划走即 stop(断音 + 中断缓存下载);`suspend()`/`resume()` 挂起语义配合 `activeFeedStrategyProvider`/`feedTabVisibleProvider` 与 HomeShell(IndexedStack),修复"切到我的页面仍出声"竞态 |
|
||||
| 本地媒体代理 | App 内嵌 127.0.0.1 HTTP 服务(core/media/local_media_server.dart):网络 m3u8 改写走代理;m3u8 每次回源(签名时效)、ts 按「去签名路径」落盘缓存、AES-128 密钥缓存;**LRU 上限 1GB,超限按 mtime 清到 900MB**(系统临时目录,滞后防抖);统一 Range/206(修复进度条不走/拖动无效的根因) |
|
||||
| 视频下载 | core/media/download_manager.dart:m3u8 → 4 并发下载 ts + 密钥 → 改写本地 index.m3u8;mp4 整文件直下;进度/取消/失败清理;入口为 Feed/播放页长按菜单,列表在「我的 → 已下载」,离线播放走本地代理支持拖动,不重复扣积分 |
|
||||
| Feed 滑动 | PageView(scrollDirection: vertical),onPageChanged 触发**观看上报**(防抖 500ms,同视频当日只报一次,本地记录);5 个策略 Tab 各保活独立分页状态(family provider),liked/viewed 用(值,id)双键游标 |
|
||||
| 状态 | Riverpod:`authStateProvider`(游客/登录)、`walletProvider`、`vipProvider`、`feedProvider`(分页游标) |
|
||||
| 本地持久 | device_id(首启 UUID 写 secure_storage)、Token(secure_storage)、观看已上报集合(sp,按日清理) |
|
||||
| 上传 | 优先 OSS STS 直传(后端签 STS,App 分片上传,失败续传);兜底服务端 multipart |
|
||||
@@ -613,14 +635,15 @@ ffmpeg -ss 00:00:01 -i input.mp4 -frames:v 1 -q:v 2 out/cover.jpg
|
||||
|
||||
### 6.4 推荐与标签服务实现
|
||||
|
||||
- Feed 接口参数 `type=recommend|latest|hot`,默认 recommend
|
||||
- Feed 接口参数 `type=recommend|latest|hot|liked|viewed`,默认 recommend
|
||||
- **recommend(个性化)**:
|
||||
1. 候选池:近 30 天"已发布"视频 ID(Redis ZSet `feed:latest` 缓存,发布/下架时增删)
|
||||
2. 个性化分:统计用户近 7 天观看/点赞/收藏视频的标签,得到偏好标签及权重;候选视频命中偏好标签加权,未命中取热度归一分;游客/新用户退化为热度分
|
||||
3. **每日重排**:`seed = md5(yyyyMMdd + subject_id)`,以该固定种子对打分后列表做确定性洗牌(Fisher-Yates)—— 保证同一用户**每天顺序不同**,且同一天内多次刷新顺序稳定(游标翻页不错乱)
|
||||
4. **当日去重**:翻页时过滤 `watched:{subject}:{date}`(观看上报时写入);候选耗尽时放开过滤兜底,避免 Feed 刷空
|
||||
- **latest**:published_at 游标倒序;**hot**:score = view×1 + like×5 + comment×10(近 7 天,时间衰减)
|
||||
- 标签:发布时写入 tags / video_tags(标签名全局唯一、大小写不敏感);标签视频页按热度或最新排序
|
||||
- **latest**:published_at 游标倒序;**hot**:score = (view×1 + like×5 + comment×10) / (1 + 发布天数),严格倒序;**liked(口碑)**:like_count 倒序;**viewed(热播)**:view_count 倒序
|
||||
- liked / viewed 为纯数值排序,游标用**(值, id)双键**(`WHERE (v < ?) OR (v = ? AND id < ?)`),避免同值翻页丢失
|
||||
- 标签:发布时写入 tags / video_tags(标签名全局唯一、大小写不敏感);标签视频页按热度或最新排序;存量标签由离线脚本 build_tags_from_titles.py(jieba TF-IDF topK=6 + 番号/书名号提取 + 噪音过滤,幂等)批量生成
|
||||
|
||||
### 6.5 支付适配层
|
||||
|
||||
@@ -682,3 +705,19 @@ Celery 周期任务(default 队列):
|
||||
- CI/CD:GitHub Actions(lint → pytest → build image → deploy)
|
||||
- 备份:MySQL 每日全量 + binlog 增量;OSS 跨区复制(可选)
|
||||
- **兜底域名与证书**:`jsnbhh.vip` 系列兜底域名配置 DNS 泛解析 + 通配符证书(`*.jsnbhh.vip`),接入 CDN 回源;主域名失效时 App 按域名发现策略自动切换(见 5.2)
|
||||
|
||||
#### 发版流水线(deploy.sh)
|
||||
项目根目录 `deploy.sh` 一键发版,流程全自动:
|
||||
1. 解析 pubspec.yaml 版本号 → `flutter analyze`(warning 不阻断)→ `flutter build apk --release`
|
||||
2. 产物覆盖 `runtime/downloads/video-app.apk`(nginx h5.256kb.top 下载页 alias 到此)
|
||||
3. 同步 DB `system_configs.app.version.android` 的 latest_version / latest_version_code(**保留 min_version_code 不重置**,需强制更新时人工调大)
|
||||
4. `docker exec my-redis` 清 `config:system` 缓存使版本配置即时生效
|
||||
5. 自动 `gradlew --stop` 释放守护进程内存
|
||||
|
||||
#### Android 正式签名
|
||||
- keystore:`app/android/app/upload-keystore.jks`(PKCS12,alias `video_release`)+ `app/android/key.properties`,两者均 gitignore;备份在 `/root/backups/video-app-signing/`
|
||||
- `build.gradle.kts` 条件签名:key.properties 存在用 release 签名,否则回退 debug(保证 CI/新环境可编译)
|
||||
|
||||
#### 生产运行与启停
|
||||
- systemd `video-api.service`:`uvicorn 127.0.0.1:8000 --workers 2`;`project-ctl.sh start/stop/restart/status` 统一管 video-api.service + docker `mysql8`/`my-redis`
|
||||
- **注意**:`--workers 2` 时 settle_service 周期任务随每个 worker 的 lifespan 各启动一份,任务逻辑必须保持幂等(状态机推进 + 唯一约束),新增周期任务同样遵守
|
||||
|
||||
Reference in New Issue
Block a user