up
This commit is contained in:
+108
-18
@@ -56,14 +56,13 @@ video/ # 项目根目录(当前目录)
|
||||
- 游客不可点赞、收藏、评论、上传
|
||||
|
||||
#### 2.1.2 注册
|
||||
- **优先手机号注册**:手机号 + 短信验证码 + 密码
|
||||
- 备选邮箱注册:邮箱 + 邮箱验证码 + 密码
|
||||
- 注册页**手机号 / 邮箱 Tab 切换**:手机号 + 短信验证码 + 密码,或 邮箱 + 邮箱验证码 + 密码
|
||||
- 邮箱验证码由服务端**真实 SMTP 发送**(`POST /auth/email/send`),验证码写 Redis(`email:code:{email}`,600s),与短信验证码同一套校验机制
|
||||
- 注册即登录,返回 Token
|
||||
- 约束:手机号/邮箱全局唯一;密码 ≥ 8 位,含字母和数字
|
||||
|
||||
#### 2.1.3 登录
|
||||
- 手机号 + 密码,或 手机号 + 短信验证码
|
||||
- 邮箱 + 密码
|
||||
- 支持**手机号或邮箱**作为账号:手机号 + 密码 / 手机号 + 短信验证码 / 邮箱 + 密码
|
||||
- 登录成功后,游客时期的设备 ID 与账号可关联(观看记录合并,可选)
|
||||
|
||||
#### 2.1.4 资料管理
|
||||
@@ -189,7 +188,10 @@ App 调用「观看上报」接口
|
||||
|
||||
#### 2.5.4 视频播放
|
||||
- m3u8(HLS)流媒体播放,App 竖屏全屏沉浸式播放
|
||||
- 上滑/下滑切换视频,预加载下一个视频
|
||||
- 上滑/下滑切换视频;**3 槽播放器池**预加载:当前页播放 + 下一页预缓冲,划走即 stop(断音 + 中断缓存下载)
|
||||
- 切到"我的"等页面时播放器池**挂起(suspend)静音断流**,回到 Feed 且首页 Tab 可见才恢复(resume),修复"切页仍出声"问题
|
||||
- 网络 m3u8 播放地址改写为 **App 内嵌本地媒体代理**(127.0.0.1 HTTP 服务):m3u8 每次回源保证签名时效,ts 分片落盘缓存(1GB LRU),支持拖动进度条(Range/206);ts 流量直连源站 CDN,不经过业务服务器
|
||||
- 进度条拖动时显示**时间气泡**(x时xx分xx秒),标题后带**时长角标**
|
||||
- 记录播放量(进入播放即 +1,Redis 计数异步落库)
|
||||
|
||||
#### 2.5.5 视频标签(Tag)
|
||||
@@ -197,15 +199,20 @@ App 调用「观看上报」接口
|
||||
- 播放页在**标题后展示标签**(#标签 样式)
|
||||
- 点击标签进入"标签视频列表页":展示该标签下全部已发布视频(支持 热度/最新 排序)
|
||||
- 标签是推荐策略中个性化加权的主要特征
|
||||
- 存量视频标签由**离线脚本批量生成**(server/scripts/build_tags_from_titles.py:jieba TF-IDF topK=6 + 番号/书名号提取 + 噪音过滤,幂等可重跑;已生成 2.2 万+ 标签、9.9 万+ 关联),后台提供"标签管理"页维护
|
||||
|
||||
#### 2.5.6 视频推荐策略
|
||||
Feed 支持三种策略,客户端通过接口参数切换:
|
||||
Feed 支持五种策略,客户端通过接口参数切换(App 顶部 Tab:推荐 / 最新 / 热度 / 口碑 / 热播,各策略独立保活分页状态):
|
||||
|
||||
| 策略 | 说明 |
|
||||
|---|---|
|
||||
| recommend(默认) | 个性化推荐 |
|
||||
| latest | 最新发布倒序 |
|
||||
| hot | 热度倒序(播放/点赞/评论加权) |
|
||||
| hot | 热度倒序:热度分 = (播放×1 + 点赞×5 + 评论×10) / (1 + 发布天数) |
|
||||
| liked(口碑) | 按点赞量倒序 |
|
||||
| viewed(热播) | 按播放量倒序 |
|
||||
|
||||
> liked / viewed 为纯数值排序,游标采用(值, id)双键避免同值翻页丢失。
|
||||
|
||||
**个性化推荐(recommend)规则:**
|
||||
1. 候选池:全部"已发布"视频
|
||||
@@ -224,6 +231,13 @@ Feed 支持三种策略,客户端通过接口参数切换:
|
||||
- 搜索历史:App 端本地存储最近 10 条,不上报服务端
|
||||
- 搜索词先过敏感词过滤,命中直接返回空结果
|
||||
|
||||
#### 2.5.9 视频下载(离线缓存)
|
||||
- **入口**:Feed 页 / 播放页长按弹出菜单 →「下载」(已下载/下载中置灰)
|
||||
- HLS 视频:4 并发下载 ts 分片与 AES-128 密钥,改写为本地 index.m3u8;mp4 整文件直接下载
|
||||
- 下载过程展示进度,支持取消;失败自动清理半成品文件
|
||||
- 下载列表在「我的 → 已下载」;离线播放走本地媒体代理,支持拖动进度条
|
||||
- 已下载视频离线观看**不重复扣积分**(线上播放时已按正常规则计费)
|
||||
|
||||
### 2.6 互动模块
|
||||
|
||||
| 功能 | 规则 |
|
||||
@@ -330,7 +344,7 @@ Feed 支持三种策略,客户端通过接口参数切换:
|
||||
|---|---|
|
||||
| 仪表盘 | 今日/累计:DAU、新增注册、充值金额、积分发放与消耗、播放量、待审核数 |
|
||||
| 视频审核(人工) | 待审核列表(在线预览 m3u8)、通过、拒绝(必填原因)、已发布视频下架 |
|
||||
| 用户管理 | 搜索(ID/手机号/邮箱)、用户详情(钱包/会员/作品/流水)、封禁与解封 |
|
||||
| 用户管理 | 搜索(ID/手机号/邮箱)、用户详情(钱包/会员/作品/流水)、封禁与解封、**修改密码**(重置后 `auth:epoch` 递增,该用户全端 Token 立即失效) |
|
||||
| 积分管理 | 用户积分流水查询、手动加/减积分(原因必填 + 二次确认,写流水 biz_type=6) |
|
||||
| 订单管理 | 充值订单查询(状态/渠道/时间筛选)、异常订单人工补单(核实已收款后手动入账) |
|
||||
| 提现审核 | 提现申请列表、审核通过(线下打款后确认)/ 拒绝(自动退回资产) |
|
||||
@@ -338,6 +352,8 @@ Feed 支持三种策略,客户端通过接口参数切换:
|
||||
| 平台配置 | 注册赠送体验会员开关与天数等运营参数(system_configs 键值对,实时生效) |
|
||||
| 充值套餐管理 | 档位与赠送积分增删改、上下架、排序,实时生效 |
|
||||
| 活动运营 | 赠送体验会员/积分(支持单个或批量用户) |
|
||||
| 标签管理 | 标签列表查询(关联视频数)、重命名、删除(联动清理 video_tags) |
|
||||
| 采集源管理 | 采集源 CRUD(分类名 + 列表 URL 模板)、批量添加、单源/全部同步(后台异步采集外部单视频入库) |
|
||||
| 管理员管理 | 管理员账号增删、角色分配(超级管理员/审核员/运营)、操作日志查询 |
|
||||
|
||||
角色权限:超级管理员(全部)、审核员(仅视频审核)、运营(套餐/活动/看板)。
|
||||
@@ -369,6 +385,8 @@ Feed 支持三种策略,客户端通过接口参数切换:
|
||||
|
||||
### 3.1 表清单
|
||||
|
||||
现共 **30 张业务表**。Alembic 迁移链:`init_27_tables` → `add_collection_records` → `add_collection_sources`;线上数据量约 1.8 万视频。
|
||||
|
||||
| 表 | 说明 |
|
||||
|---|---|
|
||||
| users | 用户表 |
|
||||
@@ -397,6 +415,9 @@ Feed 支持三种策略,客户端通过接口参数切换:
|
||||
| notifications | 消息通知表 |
|
||||
| reports | 举报表 |
|
||||
| sensitive_words | 敏感词表 |
|
||||
| series | 系列(合集)表:多条视频组织成集数列表 |
|
||||
| collection_sources | 采集源配置表(分类名 + 列表 URL 模板) |
|
||||
| collection_records | 采集记录表(外部内容源 → 本站系列/视频映射,幂等导入) |
|
||||
|
||||
### 3.2 用户域
|
||||
|
||||
@@ -792,6 +813,47 @@ Feed 支持三种策略,客户端通过接口参数切换:
|
||||
| word | VARCHAR(50) | 敏感词,**唯一索引** |
|
||||
| created_at | DATETIME | |
|
||||
|
||||
### 3.10 采集域
|
||||
|
||||
#### series 系列(合集)表
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | BIGINT PK AUTO_INCREMENT | |
|
||||
| title | VARCHAR(100) | 系列标题 |
|
||||
| cover_url | VARCHAR(500) | 封面 |
|
||||
| description | VARCHAR(500) | 简介 |
|
||||
| author_id | BIGINT NULL | 作者 |
|
||||
| category_id | INT | 分类 |
|
||||
| total_episodes | INT DEFAULT 0 | 总集数 |
|
||||
| view_count | BIGINT DEFAULT 0 | 播放量 |
|
||||
| status | SMALLINT DEFAULT 1 | 1正常 0隐藏 |
|
||||
| created_at / updated_at | DATETIME | |
|
||||
|
||||
索引:`idx_series_cat_status(category_id, status)`、`idx_series_status_created(status, created_at)`;videos 表以 `series_id` 关联所属系列
|
||||
|
||||
#### collection_sources 采集源配置表
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | BIGINT PK AUTO_INCREMENT | |
|
||||
| name | VARCHAR(50) | 分类名(添加时自动建/找本站分类) |
|
||||
| list_url | VARCHAR(500) | 列表 URL 模板,`{{page}}` 为页码占位 |
|
||||
| category_id | INT UNSIGNED | 本站分类 id |
|
||||
| status | SMALLINT DEFAULT 1 | 1启用 0停用 |
|
||||
| last_sync_at | DATETIME NULL | 最近同步时间 |
|
||||
| created_at / updated_at | DATETIME | |
|
||||
|
||||
#### collection_records 采集记录表
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | BIGINT PK AUTO_INCREMENT | |
|
||||
| source_key | VARCHAR(50) | 内容源标识 |
|
||||
| source_id | BIGINT | 源站内容 id |
|
||||
| series_id | BIGINT NULL | 本站系列 id |
|
||||
| video_id | BIGINT NULL | 本站视频 id(单视频类) |
|
||||
| created_at | DATETIME | |
|
||||
|
||||
约束:唯一索引 `uk_source(source_key, source_id)` 保证幂等导入(重复同步不产生重复视频);索引 `idx_record_series(series_id)`、`idx_record_video(video_id)`
|
||||
|
||||
---
|
||||
|
||||
## 4. 接口设计(概览)
|
||||
@@ -842,16 +904,19 @@ Feed 支持三种策略,客户端通过接口参数切换:
|
||||
| /mine/history | 观看历史 | |
|
||||
| /upload | 上传视频 | 选片→剪辑信息→发布 |
|
||||
| /search | 搜索 | |
|
||||
| /category | 分类页 | 按分类浏览视频 |
|
||||
| /tags/:name | 标签视频页 | 某标签下的视频列表 |
|
||||
| /mine/downloads | 已下载 | 离线视频列表(进度/删除/播放) |
|
||||
| /notifications | 消息通知 | 通知列表、未读标记 |
|
||||
| /settings | 设置 | 退出登录等 |
|
||||
|
||||
### 5.2 核心页面布局
|
||||
|
||||
#### 5.2.1 首页(视频 Feed)
|
||||
- 全屏竖向 PageView,每个页面一个视频播放器
|
||||
- 顶部:Feed 策略 Tab(推荐 / 最新 / 热度)+ 搜索图标
|
||||
- 全屏竖向 PageView,每个页面一个视频播放器(3 槽播放器池:当前页播放 + 下一页预缓冲,划走即停)
|
||||
- 顶部:Feed 策略 Tab(推荐 / 最新 / 热度 / 口碑 / 热播)+ 搜索图标;各策略分页状态独立保活,切 Tab 不丢进度
|
||||
- 右侧互动栏(自上而下):作者头像(+关注预留)、点赞(红心+计数)、评论(气泡+计数)、收藏(星标+计数)、分享
|
||||
- 左下:作者昵称、标题(标题后展示 #标签,点击跳转标签视频页)、描述(展开/收起)
|
||||
- 左下:作者昵称、标题(标题后展示 #标签,点击跳转标签视频页;并带时长角标)、描述(展开/收起)
|
||||
- 底部(游客/非会员场景):今日免费剩余次数提示条(剩余 ≤ 3 次时显示)
|
||||
|
||||
#### 5.2.2 评论页(底部弹层)
|
||||
@@ -885,6 +950,8 @@ Feed 支持三种策略,客户端通过接口参数切换:
|
||||
| 切换视频 | 垂直滑动,松手吸附;滑出即暂停,滑入自动播放并**上报观看计费** |
|
||||
| 单击屏幕 | 暂停 / 继续播放(暂停时显示播放图标) |
|
||||
| 双击屏幕 | 点赞动画(大爱心飘出) |
|
||||
| 长按屏幕 | 弹出操作菜单:下载视频 / 倍速 / 分享(已下载或下载中置灰) |
|
||||
| 拖动进度条 | 跟随 thumb 显示时间气泡(x时xx分xx秒,两端自动夹在屏内) |
|
||||
| 游客超限 | 播放第 11 个视频时弹出登录引导弹窗("注册解锁更多视频,今日已看 10/10") |
|
||||
| 用户积分不足 | 弹窗"积分不足":去充值 / 开通会员 两个按钮 |
|
||||
| 扣费提示 | 每次扣积分播放时顶部 Toast"本次消耗 1 积分" |
|
||||
@@ -895,7 +962,7 @@ Feed 支持三种策略,客户端通过接口参数切换:
|
||||
|
||||
| 项 | 规范 |
|
||||
|---|---|
|
||||
| 主题 | 深色为主(#121212 背景),保证视频沉浸感;浅色可选(二期) |
|
||||
| 主题 | 深色为主(#121212 背景),保证视频沉浸感;浅色可选(二期);**弹窗/底部菜单统一白底亮色**(局部包 `ThemeData.light()`,避免深色背景下文字看不清) |
|
||||
| 主色 | #FE2C55(红),辅助色 #25F4EE(青) |
|
||||
| 会员色 | 金色渐变 #F7D794 → #D4A017 |
|
||||
| 文字 | 主 #FFFFFF,次 #B0B0B0,弱 #6E6E6E |
|
||||
@@ -924,6 +991,25 @@ App 不内置固定 API 域名,启动时按以下流程动态获取:
|
||||
- App 启动及回到前台时调用 `GET /app/version`:返回最新版本号、是否强制更新、下载地址、维护公告
|
||||
- 强制更新:弹窗阻断使用并跳转商店/下载页;非强制:可跳过的提示弹窗;维护公告:全 App 只读提示
|
||||
|
||||
### 5.8 工程结构(与当前代码对齐)
|
||||
|
||||
```
|
||||
app/lib/
|
||||
├── screens/ # 页面:splash / home(HomeShell+IndexedStack) / feed / search / category
|
||||
│ # / player / membership / payment / wallet / assets / upload
|
||||
│ # / profile / auth / settings / notification / downloads
|
||||
├── providers/ # Riverpod 状态(12 个):auth / feed / comment / wallet / member
|
||||
│ # / payment / profile / search / upload / asset / notification
|
||||
│ # / tag_videos
|
||||
└── core/ # 子系统:config(环境配置)/ media(local_media_server 本地代理、
|
||||
# download_manager 下载)/ network(api_client、域名发现)/
|
||||
# player(feed_player_pool 播放器池)/ storage(token、本地存储)/
|
||||
# theme / utils
|
||||
```
|
||||
|
||||
- Feed 策略状态按策略保活:`feedProviderFamily(strategy)` 每个策略一个独立 FeedNotifier;`activeFeedStrategyProvider` + `feedTabVisibleProvider` 配合 HomeShell(IndexedStack)在切页/回前台时挂起与恢复正确的播放器池
|
||||
- 401 兜底:无 refresh token 或刷新被拒时清登录态并跳登录页,防止"僵尸登录态"
|
||||
|
||||
## 6. 后端设计
|
||||
|
||||
### 6.1 框架推荐:**FastAPI**
|
||||
@@ -945,17 +1031,21 @@ App 不内置固定 API 域名,启动时按以下流程动态获取:
|
||||
```
|
||||
server/
|
||||
├── app/
|
||||
│ ├── main.py # 应用入口、中间件、路由注册
|
||||
│ ├── main.py # 应用入口、中间件、路由注册、lifespan(settle 周期任务启动)
|
||||
│ ├── core/ # 配置、日志、异常、安全(JWT/密码)
|
||||
│ ├── api/v1/ # App 路由层:auth/users/videos/interact/vip/pay
|
||||
│ ├── api/admin/ # 管理后台路由(独立管理员鉴权)
|
||||
│ ├── api/v1/ # 路由层(11 个文件):admin / auth / common / incentives
|
||||
│ │ # / interactions / invite / media / membership / payments
|
||||
│ │ # / users / videos(管理后台同文件 /admin 前缀、独立管理员鉴权)
|
||||
│ ├── schemas/ # Pydantic 请求/响应模型
|
||||
│ ├── models/ # SQLAlchemy ORM 模型
|
||||
│ ├── services/ # 业务逻辑:计费、积分、会员、视频、互动
|
||||
│ ├── models/ # SQLAlchemy ORM 模型(30 张表)
|
||||
│ ├── services/ # 业务逻辑(17 个):user/auth/wallet/video/interaction/membership
|
||||
│ │ # /payment/incentive/invite/admin/common/config/sensitive
|
||||
│ │ # /storage/transcode/settle/collection
|
||||
│ ├── payment/ # 支付适配层:alipay / wechat / usdt
|
||||
│ ├── tasks/ # Celery 任务:转码、播放量落库、订单关单
|
||||
│ └── utils/ # 短信、邮件、OSS、设备指纹
|
||||
├── alembic/ # 数据库迁移
|
||||
├── scripts/ # 离线脚本:build_tags_from_titles.py(标签生成)、采集脚本
|
||||
├── alembic/ # 数据库迁移(init_27_tables → collection_records → collection_sources)
|
||||
├── config.yaml # 业务参数配置(积分/会员/支付/上传限制)
|
||||
├── .env # 密钥类配置(不入库)
|
||||
└── docker-compose.yml
|
||||
|
||||
+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 各启动一份,任务逻辑必须保持幂等(状态机推进 + 唯一约束),新增周期任务同样遵守
|
||||
|
||||
+174
-9
@@ -1,7 +1,7 @@
|
||||
# 短视频应用接口文档
|
||||
|
||||
- 版本:v1.0
|
||||
- 日期:2026-10-14
|
||||
- 版本:v1.1
|
||||
- 日期:2026-10-18
|
||||
- Base URL:`https://api.example.com/api/v1`
|
||||
|
||||
> 注意:App 不硬编码 Base URL。启动时优先解析 `txt.256kb.top` 的 DNS TXT 记录获取真实 API 域名;解析失败时按 `md5(yyyyMM) + "_jsnbhh.vip"` 计算兜底域名(详见《01-开发文档》5.5 节)。本文示例域名仅为占位。
|
||||
@@ -90,7 +90,7 @@
|
||||
| 13 | 用户 | POST | /users/me/deactivate | 注销账号(7天冻结期) | 是 |
|
||||
| 14 | 用户 | GET | /users/{user_id} | 用户公开主页 | 否 |
|
||||
| 15 | 用户 | GET | /users/{user_id}/videos | 他人作品列表 | 否 |
|
||||
| 16 | 视频 | GET | /videos/feed | 推荐视频流(支持 recommend/latest/hot) | 否 |
|
||||
| 16 | 视频 | GET | /videos/feed | 视频流(recommend/latest/hot/liked/viewed) | 否 |
|
||||
| 17 | 视频 | GET | /videos/{video_id} | 视频详情(登录返回续播进度) | 否 |
|
||||
| 18 | 视频 | POST | /videos/{video_id}/watch | 观看上报(计费,可带播放进度) | 否 |
|
||||
| 19 | 视频 | POST | /videos/{video_id}/watch/complete | 完整观看上报(创作收益计数) | 否 |
|
||||
@@ -133,7 +133,10 @@
|
||||
| 56 | 消息 | GET | /notifications/unread_count | 未读数 | 是 |
|
||||
| 57 | 消息 | POST | /notifications/read | 标记已读 | 是 |
|
||||
| 58 | 通用 | POST | /reports | 提交举报 | 是 |
|
||||
| 59~90 | 后台管理 | * | /admin/** | 登录、看板、审核、视频/评论管理、用户、积分、订单、补单、套餐、赠送、提现审核、推荐风控、举报处理、敏感词、平台配置、日志,见第 11 章 | 管理员 |
|
||||
| 59 | 视频 | GET | /videos/series/{series_id} | 系列详情 + 全部集数列表 | 否 |
|
||||
| 60 | 视频 | GET | /videos/{video_id}/series | 当前视频所属系列 + 上下集 | 否 |
|
||||
| 61 | 通用 | GET | /m3u8/proxy | m3u8 播放列表代理(回源拉取,TS 分片不经过服务器) | 否 |
|
||||
| 62~105 | 后台管理 | * | /admin/** | 登录、看板、审核、视频/系列/评论管理、用户(含重置密码)、积分、订单、补单/退款、套餐、赠送、提现审核、推荐风控、举报处理、敏感词、平台配置、采集源/采集同步、分类、标签、日志,见第 11 章 | 管理员 |
|
||||
|
||||
---
|
||||
|
||||
@@ -160,7 +163,7 @@
|
||||
{ "code": 0, "message": "ok", "data": { "expire_seconds": 300, "retry_after": 60 } }
|
||||
```
|
||||
|
||||
**限制**:同号码 60 秒 1 条,同号码每天 10 条;注册场景下已注册号码返回错误码 1001("该手机号已注册")。
|
||||
**限制**:同号码 60 秒 1 条,同号码每天 10 条,同 IP 每小时 5 条;注册场景下已注册号码返回错误码 1001("该账号已注册"),login/reset 场景下未注册号码同样返回 1001。未配置短信通道时(开发环境)响应额外返回 `dev_code` 并仅记日志。
|
||||
|
||||
### 3.2 发送邮箱验证码 `POST /auth/email/send`
|
||||
|
||||
@@ -169,7 +172,7 @@
|
||||
| email | string | 是 | 邮箱地址 |
|
||||
| scene | string | 是 | `register` / `login` / `reset` |
|
||||
|
||||
响应同 3.1,`expire_seconds=600`。
|
||||
验证码由服务端通过 SMTP 真实发送(STARTTLS),并写入 Redis(`email:code:{scene}:{email}`,10 分钟有效,验证通过即删除)。响应结构同 3.1,`expire_seconds=600`;限流规则同 3.1(同邮箱 60 秒 1 条、每天 10 条、同 IP 每小时 5 条)。未配置 SMTP 时按开发模式处理:响应额外返回 `dev_code` 并仅记日志。
|
||||
|
||||
### 3.3 注册 `POST /auth/register`
|
||||
|
||||
@@ -223,6 +226,7 @@
|
||||
| email | string | 邮箱方式必填 | |
|
||||
| password | string | 密码方式必填 | |
|
||||
| code | string | 验证码方式必填 | |
|
||||
| revoke | bool | 否 | 注销冻结期内确认撤销注销(默认 false,见 4.6) |
|
||||
|
||||
**请求示例**
|
||||
|
||||
@@ -230,7 +234,9 @@
|
||||
{ "type": "phone_password", "phone": "13800138000", "password": "abc12345" }
|
||||
```
|
||||
|
||||
**响应**:同 3.3。连续失败 5 次锁定 15 分钟(错误码 1005)。
|
||||
**响应**:同 3.3。连续失败 5 次锁定 15 分钟(错误码 1005,按 IP 维度计数)。
|
||||
|
||||
> 登录账号仅支持**手机号或邮箱**(按 type 使用对应字段),不支持昵称登录。冻结期内账号登录返回 1009 且 `data.can_revoke=true`;确认撤销后带 `"revoke": true` 重试即恢复账号并正常签发 Token。
|
||||
|
||||
### 3.5 刷新令牌 `POST /auth/token/refresh`
|
||||
|
||||
@@ -240,6 +246,8 @@
|
||||
|
||||
**响应**:同 3.3 的 token 部分;refresh_token 旋转(旧值立即失效)。
|
||||
|
||||
> Token 体系:access_token 有效期 2 小时,refresh_token 30 天(Redis 白名单 Set,支持多端同时在线)。服务端按用户维护令牌纪元 `auth:epoch:{user_id}`,签发时写入 Token 的 `ep` 声明,每次请求都校验:修改/重置密码、后台重置密码(11.34)后纪元递增并清空 refresh 白名单,该用户所有旧 Token(含未过期的 access)立即失效。
|
||||
|
||||
### 3.6 退出登录 `POST /auth/logout`(需登录)
|
||||
|
||||
无参数。响应 `{ "code": 0 }`。服务端将 access_token 的 jti 加入黑名单并删除 refresh 白名单。
|
||||
@@ -401,7 +409,7 @@
|
||||
| cursor | string | 否 | 上一页返回的 next_cursor |
|
||||
| limit | int | 否 | 默认 10,最大 20 |
|
||||
| category_id | int | 否 | 分类筛选 |
|
||||
| type | string | 否 | 策略:`recommend`(默认,个性化)/ `latest` / `hot` |
|
||||
| type | string | 否 | 策略:`recommend`(默认,个性化)/ `latest`(最新发布)/ `hot`(热度)/ `liked`(口碑,按点赞量倒序)/ `viewed`(热播,按播放量倒序) |
|
||||
|
||||
**响应示例**
|
||||
|
||||
@@ -434,6 +442,11 @@
|
||||
> `is_liked/is_favorited` 仅登录用户返回真实值,游客恒为 false。
|
||||
>
|
||||
> **recommend 策略**:以"当天日期 + 用户/设备"为随机种子每日重排(同一用户每天的推荐顺序不同、同一天内多次刷新顺序稳定);当日已观看的视频尽量不再出现(候选不足时放开过滤兜底);个性化基于近 7 天观看/点赞/收藏视频的标签偏好加权,游客及新用户退化为热度排序。
|
||||
>
|
||||
> **游标格式**(均为 base64url 编码的 JSON,客户端无需解析,原样回传即可):
|
||||
> - `liked` / `viewed`:「值,id」双键游标 `{"n": 末条点赞数/播放数, "i": 末条 video_id}`,按"值 DESC, id DESC"严格翻页,点赞/播放数变化不影响已翻过的页
|
||||
> - `latest`:`{"p": 末条发布时间戳, "i": 末条 video_id}`
|
||||
> - `recommend` / `hot`:当日种子下的偏移游标
|
||||
|
||||
### 5.2 视频详情 `GET /videos/{video_id}`(游客可)
|
||||
|
||||
@@ -651,6 +664,30 @@
|
||||
|
||||
**错误码**:2107 无权操作(非作者本人);2003 视频不存在。
|
||||
|
||||
### 5.14 系列详情 `GET /videos/series/{series_id}`(游客可)
|
||||
|
||||
返回系列信息 + 全部已发布集数(按 `episode_no` 升序),用于短剧播放页选集。
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"series": { "id": 12, "title": "霸道总裁", "cover_url": "https://...", "description": "...", "total_episodes": 80 },
|
||||
"episodes": [
|
||||
{ "video_id": 88001, "episode_no": 1, "title": "第1集", "cover_url": "https://...", "duration": 95, "video_url": "https://.../index.m3u8" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**错误码**:1006 系列不存在或已隐藏。
|
||||
|
||||
### 5.15 当前视频所属系列 `GET /videos/{video_id}/series`(游客可)
|
||||
|
||||
用于 PlayerScreen 系列抽屉入口。视频不属于任何系列(或未发布)时返回 `{ "has_series": false }`;否则返回系列信息、全部集数(字段同 5.14)及 `current_episode_no` / `prev_video_id` / `next_video_id`(无上下集时为 null)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 互动模块
|
||||
@@ -1197,6 +1234,20 @@ App 我的页消息入口红点依赖本接口(直接读 Redis `notif:unread:{
|
||||
|
||||
**错误码**:2106 请勿重复举报(同一用户对同一对象仅一次有效举报,唯一索引 `uk_user_target` 保证);限流 10 次/天。
|
||||
|
||||
### 10.6 m3u8 播放列表代理 `GET /m3u8/proxy`(游客可)
|
||||
|
||||
采集内容源站的 m3u8 内 TS 链接带时效签名,需经服务端每次回源拉取新鲜播放列表;**TS 分片不经过本服务器**,由客户端按播放列表中的源站地址直连下载。
|
||||
|
||||
**查询参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| path | string | 是 | 源站播放列表的相对资源路径(防 SSRF:仅允许字母数字及 `/._-`,禁止 `..`),1-300 字 |
|
||||
|
||||
**响应**:非统一 JSON 包装,直接返回 m3u8 文本(`Content-Type: application/vnd.apple.mpegurl`,`Cache-Control: no-cache`,`Access-Control-Allow-Origin: *`)。
|
||||
|
||||
**错误码**:1001 非法路径;1006 播放列表获取失败(源站非 200 或内容非 m3u8)。
|
||||
|
||||
---
|
||||
|
||||
## 11. 管理后台接口
|
||||
@@ -1204,7 +1255,7 @@ App 我的页消息入口红点依赖本接口(直接读 Redis `notif:unread:{
|
||||
通用约定:
|
||||
- 前缀 `/api/v1/admin`,统一响应结构同 App 接口
|
||||
- 认证:`Authorization: Bearer <admin_token>`(Token payload `type="admin"`,有效期 12h,与 App 用户体系互不相通)
|
||||
- 角色:`super`(全部)/ `reviewer`(仅审核)/ `operator`(套餐、活动、看板);越权返回 4001
|
||||
- 角色:`super`(全部)/ `reviewer`(仅审核)/ `operator`(套餐、活动、看板、采集源/采集同步、系列/分类/标签查询);越权返回 4001
|
||||
- 所有写操作记录操作日志;标注【敏感】的接口 `reason` 必填,缺失返回 4002
|
||||
|
||||
### 11.1 管理员登录 `POST /admin/auth/login`
|
||||
@@ -1466,6 +1517,120 @@ App 我的页消息入口红点依赖本接口(直接读 Redis `notif:unread:{
|
||||
|
||||
响应 `{ "code": 0 }`,同样触发词库缓存重建。
|
||||
|
||||
### 11.33 重置用户密码 `PUT /admin/users/{user_id}/password`【敏感】(仅 super)
|
||||
|
||||
客服场景下由后台直接重置某个 App 用户的登录密码。
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| new_password | string | 是 | 新密码,6-100 位(bcrypt 入库,不校验字母数字组合) |
|
||||
| reason | string | 是 | 操作原因(写入 admin_logs,action=`user_password_reset`) |
|
||||
|
||||
```json
|
||||
// 请求
|
||||
{ "new_password": "newpass123", "reason": "用户申诉核实通过" }
|
||||
// 响应
|
||||
{ "code": 0 }
|
||||
```
|
||||
|
||||
重置后该用户**全端 Token 立即失效**:服务端递增 `auth:epoch:{user_id}` 并删除 refresh 白名单(机制见 3.5),用户需用新密码重新登录。
|
||||
|
||||
**错误码**:1006 用户不存在;1001 账号已注销不可操作。
|
||||
|
||||
### 11.34 退款登记 `POST /admin/orders/{order_no}/refund`【敏感】(仅 super)
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| reason | string | 是 | 退款原因 |
|
||||
|
||||
### 11.35 修改管理员密码 `PUT /admin/auth/password`(仅 super)
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| old_password | string | 是 | 当前密码 |
|
||||
| new_password | string | 是 | 新密码,6-100 位 |
|
||||
|
||||
响应 `{ "code": 0 }`。
|
||||
|
||||
### 11.36 后台新增视频 `POST /admin/videos`(仅 super)
|
||||
|
||||
两种模式二选一:
|
||||
- **upload_token 模式**:先调 `POST /admin/videos/upload` 拿凭证 → `POST /admin/videos/upload/file` 上传 mp4/mov/mkv/avi(multipart,字段 `file` + query `token`)→ 拿到 `upload_token` 后调本接口(服务端自动转码)
|
||||
- **video_url 模式**:直接填已转码的 m3u8 地址(外站/CDN 资源)
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| title | string | 是 | 1-100 字 |
|
||||
| description | string | 否 | ≤500 字 |
|
||||
| category_id | int | 是 | 分类 ID |
|
||||
| tag_names | array[string] | 否 | 标签 |
|
||||
| upload_token | string | 模式A必填 | upload/file 返回的凭证(封面可选 cover_upload_token) |
|
||||
| video_url | string | 模式B必填 | m3u8 地址 |
|
||||
| cover_url | string | 模式B可选 | 封面 |
|
||||
| duration | int | 否 | 时长(秒),1-36000 |
|
||||
| status | int | 否 | 1待审核(默认)/ 2直接发布 |
|
||||
|
||||
配套接口:
|
||||
- `POST /admin/videos/upload`:获取 admin 上传凭证,返回 `{ upload_url, method, token, expire_seconds: 3600 }`
|
||||
- `POST /admin/videos/upload/file?token=xxx`:接收文件落盘 server/uploads,返回 `{ upload_token, object_key, file_size }`(凭证一次性,upload_token 24h 内有效)
|
||||
- `POST /admin/upload/image`:通用图片上传(封面/头像/系列图,jpg/jpeg/png/webp/gif),返回可直接访问的完整 URL `{ url, file_size }`
|
||||
|
||||
### 11.37 后台编辑视频 `PUT /admin/videos/{video_id}`(仅 super)
|
||||
|
||||
均可选,传哪个改哪个:`title`(1-100)/ `description` / `cover_url` / `video_url` / `category_id` / `series_id` / `episode_no`(≥1)/ `status`(0-5)。`series_id` 显式传 `null` 表示移出系列(同时清空 episode_no)。响应 `{ "code": 0, "data": { "video_id": 88001 } }`。
|
||||
|
||||
### 11.38 后台删除视频 `DELETE /admin/videos/{video_id}`(仅 super)
|
||||
|
||||
**物理删除**:先解绑系列,再级联删除标签/点赞/收藏/评论/观看记录/收益/举报等全部关联数据。响应 `{ "code": 0, "data": { "video_id": 88001, "deleted": true } }`。
|
||||
|
||||
### 11.39 系列管理
|
||||
|
||||
| 方法 | 路径 | 角色 | 说明 |
|
||||
|---|---|---|---|
|
||||
| GET | /admin/series | operator | 系列分页列表(keyword 按标题模糊),含 `source_id`(采集来源标记,供"同步更新"按钮) |
|
||||
| POST | /admin/series | super | 新建系列:`title`(1-100 必填)、`cover_url`、`description`、`category_id` |
|
||||
| PUT | /admin/series/{series_id} | super | 改 `title` / `cover_url` / `description` / `category_id` / `status`(0隐藏 1正常) |
|
||||
| DELETE | /admin/series/{series_id} | super | 删除系列(集数视频的 series_id 需先解绑) |
|
||||
| POST | /admin/videos/{video_id}/series | super | 绑定视频到系列:`{ "series_id": 12, "episode_no": 3 }`,自动重算 series.total_episodes(仅计已发布) |
|
||||
|
||||
### 11.40 短剧采集同步(operator)
|
||||
|
||||
- `POST /admin/collection/sync/{series_id}`:手动同步单部采集短剧(补充源站新出的集数);非采集内容返回 1006。响应为同步统计 `{新增集数等}`,写 admin_logs(action=`collection_sync`)
|
||||
- `POST /admin/collection/sync-all`:触发内置短剧源增量同步,**后台异步执行,立即返回** `{ "started": true, "pages": 2 }`。入参 `pages`(1-20,默认 2)为增量同步的列表页数;已有任务进行中返回 1005
|
||||
|
||||
### 11.41 采集源管理(单视频类,自助添加)
|
||||
|
||||
采集源 = "分类名 + 列表页 URL 模板",URL 必须包含 `{{page}}` 页码占位符;创建/改名时按名称自动建/找分类。
|
||||
|
||||
| 方法 | 路径 | 角色 | 说明 |
|
||||
|---|---|---|---|
|
||||
| GET | /admin/collection/sources | operator | 全部源列表(id/name/list_url/category_id/status/last_sync_at/created_at) |
|
||||
| POST | /admin/collection/sources | operator | 新增源:`name`(1-50)、`list_url`(须含 `{{page}}`,否则 1001) |
|
||||
| PUT | /admin/collection/sources/{source_id} | operator | 改 `name` / `list_url` / `status`(0停用 1启用) |
|
||||
| DELETE | /admin/collection/sources/{source_id} | super | 删除源 |
|
||||
| POST | /admin/collection/sources/batch | operator | 批量添加:body `{ "text": "分类名 URL\n..." }` 每行一条(URL 可带反引号);返回 `created/skipped/failed` 清单,新建源自动触发后台采集 |
|
||||
| POST | /admin/collection/sources/{source_id}/sync | operator | 同步单个源(后台异步):入参 `max_pages`(0=不限,增量自动提前停止,上限 200);该源同步中返回 1005 |
|
||||
| POST | /admin/collection/sources/sync-all | operator | 同步全部启用源(后台异步),返回 `{ "started": [...], "skipped_running": [...] }` |
|
||||
|
||||
### 11.42 分类管理
|
||||
|
||||
| 方法 | 路径 | 角色 | 说明 |
|
||||
|---|---|---|---|
|
||||
| GET | /admin/categories | operator | 全部启用分类(不分页,供下拉框):`[{id, name}]` |
|
||||
| GET | /admin/categories/admin | operator | 分页列表(keyword 模糊),含 `sort` / `status` / 实时统计的 `video_count` |
|
||||
| POST | /admin/categories | super | 新建:`name`、`sort`(默认0)、`status`(默认1);重名返回错误 |
|
||||
| PUT | /admin/categories/{cid} | super | 改 `name` / `sort` / `status` |
|
||||
| DELETE | /admin/categories/{cid} | super | 删除;分类下仍有视频时拒绝并提示数量 |
|
||||
|
||||
### 11.43 标签管理
|
||||
|
||||
| 方法 | 路径 | 角色 | 说明 |
|
||||
|---|---|---|---|
|
||||
| GET | /admin/tags | operator | 分页列表(keyword 模糊,page_size 默认 50 最大 200),`video_count` 由 video_tags 实时统计 |
|
||||
| POST | /admin/tags | super | 新建:`name`(去空格,空名报错);已存在返回错误 |
|
||||
| PUT | /admin/tags/{tid} | super | 改名 |
|
||||
| DELETE | /admin/tags/{tid} | super | 删除标签并清理全部视频关联 |
|
||||
|
||||
---
|
||||
|
||||
## 12. App 端关键调用时序
|
||||
|
||||
+270
-124
@@ -1,62 +1,166 @@
|
||||
# 04-部署运行文档
|
||||
|
||||
> 本文说明如何在服务器上把项目跑起来。项目共三端:**服务端**(FastAPI,无需编译)、**管理后台**(Vue3,构建为静态文件)、**App**(Flutter,**必须编译**出 APK/IPA)。
|
||||
> 本文记录**真实生产环境**的部署架构与运维操作手册(截至 2026-10-18)。
|
||||
> 服务器:腾讯云 CVM(14G 内存),已装宝塔面板;系统 Linux。
|
||||
> 项目根目录:`/root/project/video`,三个业务域名均启用 HTTPS。
|
||||
|
||||
## 1. 组成与依赖
|
||||
## 1. 总体架构
|
||||
|
||||
| 端 | 目录 | 技术栈 | 运行/构建产物 |
|
||||
|---|---|---|---|
|
||||
| 服务端 | `server/` | Python ≥ 3.11 + FastAPI + MySQL 8 + Redis 7 | 直接运行,无需编译 |
|
||||
| 管理后台 | `admin/` | Node ≥ 18 + Vue3 + Vite | `npm run build` → `admin/dist/` 静态文件 |
|
||||
| App | `app/` | Flutter 3.x(Dart ≥ 3.9) | `flutter build apk` / `flutter build ipa` |
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
用户浏览器/App │ 腾讯云服务器 │
|
||||
│ │
|
||||
h5.256kb.top ───────► │ nginx(宝塔) ──► /root/project/video/h5 │
|
||||
(下载页/邀请落地页) │ ├─ /downloads /videos /uploads │
|
||||
│ │ → /root/project/video/runtime/* │
|
||||
│ └─ /r/ /api/v1/invite/ ──┐ │
|
||||
vm.256kb.top ───────► │ root:/root/project/video/admin/dist │
|
||||
(管理后台 SPA) │ /api/ /static/ /r/ ──────┤ │
|
||||
│ ▼ │
|
||||
vapi.256kb.top ──────► │ 全量反代 ──────────► uvicorn 127.0.0.1:8000
|
||||
(App API 入口) │ (video-api.service,│
|
||||
│ 2 workers) │
|
||||
│ │ │ │
|
||||
│ ┌───────────┘ └────┐ │
|
||||
│ ▼ ▼ │
|
||||
│ docker: mysql8 docker: my-redis
|
||||
│ 127.0.0.1:3307 127.0.0.1:6379
|
||||
│ 数据卷 /opt/mysql8-data 配置/数据 /home/redis
|
||||
└─────────────────────────────────────────┘
|
||||
|
||||
服务器需先装好:MySQL 8(utf8mb4)、Redis 7、Python 3.11+、Nginx(托管后台 + 反向代理)。
|
||||
视频 ts 分片流量:App → 直连源站 CDN(不经过本服务器,见 02-技术文档)
|
||||
```
|
||||
|
||||
## 2. 服务端部署(server/)
|
||||
| 域名 | 用途 | nginx 要点 |
|
||||
|---|---|---|
|
||||
| `h5.256kb.top` | App 下载页 + 邀请落地页 + 静态媒体 | root=`/root/project/video/h5`;`/downloads/` `/videos/` `/uploads/` alias 到 `runtime/`;`/r/`、`/api/v1/invite/` 反代 8000 |
|
||||
| `vm.256kb.top` | 管理后台 | root=`/root/project/video/admin/dist`(SPA 回退 index.html);`/api/` `/static/` `/r/` 反代 8000 |
|
||||
| `vapi.256kb.top` | App 业务 API | 全量反代 127.0.0.1:8000 |
|
||||
| `codeserver.256kb.top` | code-server(运维用,默认容器未自启) | 反代 code-server 容器 |
|
||||
|
||||
### 2.1 安装依赖
|
||||
证书:Let's Encrypt ECC,由 acme.sh/宝塔申请到 `/www/server/panel/vhost/cert/<域名>/fullchain.pem`,crontab 有续期任务(`5 4,10,16,22 * * * acme.sh --cron` + 宝塔 acme_v2.py),80 端口仅用于 acme 挑战与跳转 HTTPS。
|
||||
|
||||
## 2. 目录规划
|
||||
|
||||
| 路径 | 用途 |
|
||||
|---|---|
|
||||
| `/root/project/video/server` | FastAPI 后端源码 + `.venv` 虚拟环境 + `.env` + `config.yaml` |
|
||||
| `/root/project/video/admin` | Vue3 后台源码;构建产物 `admin/dist/` 即 vm 站点根目录 |
|
||||
| `/root/project/video/app` | Flutter App 源码(编译机即本服务器) |
|
||||
| `/root/project/video/h5` | H5 下载页(`index.html`) |
|
||||
| `/root/project/video/runtime` | 运行期产出:`downloads/`(APK)、`videos/`(转码视频)、`uploads/`(上传文件) |
|
||||
| `/root/project/video/docs` | 项目文档(本目录) |
|
||||
| `/root/project/video/deploy.sh` | App 一键发版脚本(见 §7) |
|
||||
| `/root/project/video/project-ctl.sh` | 项目一键启停脚本(见 §8) |
|
||||
| `/opt/mysql8-data` | MySQL 8 数据卷(Docker 挂载) |
|
||||
| `/home/redis/conf`、`/home/redis/data` | Redis 配置与数据(Docker 挂载) |
|
||||
| `/root/backups/video-app-signing` | App 签名 keystore 备份(**极其重要**,见 §7.3) |
|
||||
|
||||
## 3. Docker 组件(MySQL / Redis)
|
||||
|
||||
两个容器均为 `--restart unless-stopped`:服务器重启自动拉起;**手动 `docker stop` 后重启不会恢复**(project-ctl.sh stop 利用此语义实现"暂停")。
|
||||
|
||||
### 3.1 MySQL 8(容器名 mysql8)
|
||||
|
||||
- 镜像 `mysql:8`(当前 8.4.11),端口映射 **宿主机 3307 → 容器 3306**
|
||||
- 数据卷 `/opt/mysql8-data:/var/lib/mysql`,时区 Asia/Shanghai
|
||||
- 业务库 `video`,账号 `root` / 密码 `256kb.cn`
|
||||
- **连接必须加 `-h 127.0.0.1`**:不加时 mysql 客户端走 unix socket 会连到宿主机旧版 MySQL 5.7,曾导致 deploy.sh 把版本号写错库
|
||||
|
||||
```bash
|
||||
cd server
|
||||
python3.11 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install . # 核心依赖
|
||||
pip install ".[pay]" # 可选:支付渠道 SDK(支付宝/微信/Tron),不接支付可跳过
|
||||
# 日常连接
|
||||
mysql -h 127.0.0.1 -uroot -p256kb.cn -P 3307 video
|
||||
# 或容器内
|
||||
docker exec -it mysql8 mysql -uroot -p256kb.cn video
|
||||
|
||||
# 如需重建容器(数据在 /opt/mysql8-data,不丢)
|
||||
docker run -d --name mysql8 --restart unless-stopped \
|
||||
-p 3307:3306 -v /opt/mysql8-data:/var/lib/mysql \
|
||||
-e TZ=Asia/Shanghai -e MYSQL_ROOT_PASSWORD=256kb.cn mysql:8
|
||||
```
|
||||
|
||||
### 2.2 准备数据库与 .env
|
||||
### 3.2 Redis(容器名 my-redis)
|
||||
|
||||
```sql
|
||||
CREATE DATABASE video CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
- 镜像 `redis:latest`(7.x),端口 **6379**,挂载 `/home/redis/conf` 与 `/home/redis/data`
|
||||
- 启动命令 `redis-server /usr/local/etc/redis/redis.conf`
|
||||
- **注意:宿主机曾装过 Redis 5.0.3 并已禁用自启**。若它抢占了 6379,新版 redis-py 客户端默认 RESP3 的 `HELLO` 握手会失败 → 后端全接口 500。排查时先确认 `docker ps` 里 my-redis 在跑、且 6379 被 docker-proxy 占用
|
||||
- 后端 redis-py 客户端显式指定 `protocol=2`(RESP2),新旧服务端都兼容
|
||||
- 清业务缓存一律容器内执行:`docker exec my-redis redis-cli DEL config:system`
|
||||
|
||||
```bash
|
||||
# 如需重建容器
|
||||
docker run -d --name my-redis --restart unless-stopped \
|
||||
-p 6379:6379 -v /home/redis/conf:/usr/local/etc/redis -v /home/redis/data:/data \
|
||||
redis:latest redis-server /usr/local/etc/redis/redis.conf
|
||||
```
|
||||
|
||||
在 `server/.env` 配置环境项(不入库的连接串/密钥):
|
||||
## 4. 后端服务(video-api.service)
|
||||
|
||||
### 4.1 systemd 单元(/etc/systemd/system/video-api.service)
|
||||
|
||||
```ini
|
||||
APP_ENV=prod # prod 时 /docs 自动关闭
|
||||
APP_HOST=0.0.0.0
|
||||
APP_PORT=8000
|
||||
DATABASE_URL=mysql+aiomysql://user:pass@127.0.0.1:3306/video?charset=utf8mb4
|
||||
REDIS_URL=redis://127.0.0.1:6379/0
|
||||
JWT_SECRET=换成足够长的随机串 # 生产必须更换
|
||||
[Unit]
|
||||
Description=Video App FastAPI Backend (uvicorn)
|
||||
After=network.target docker.service redis.service
|
||||
Requires=docker.service
|
||||
|
||||
# 以下按需配置(不配置则对应功能自动降级:短信/邮件发不出去、支付走 dev_mock、USDT 不启用)
|
||||
# ALIPAY_APP_ID / ALIPAY_PRIVATE_KEY / ALIPAY_PUBLIC_KEY / ALIPAY_NOTIFY_URL
|
||||
# WECHAT_MCHID / WECHAT_APP_ID / WECHAT_API_V3_KEY / WECHAT_PRIVATE_KEY / WECHAT_CERT_SERIAL_NO / WECHAT_PLATFORM_CERT / WECHAT_NOTIFY_URL
|
||||
# TRON_API_KEY(USDT 链上查账)
|
||||
# SMS_ACCESS_KEY_ID / SMS_ACCESS_KEY_SECRET / SMS_SIGN_NAME(短信验证码)
|
||||
# SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASSWORD(邮箱验证码)
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/root/project/video/server
|
||||
ExecStart=/root/project/video/server/.venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000 --workers 2
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
业务参数(积分/会员/激励/提现规则)在 `server/config.yaml`,其中部分项上线后可在管理后台「系统配置」热修改。
|
||||
- 只监听 **127.0.0.1:8000**,对外全靠 nginx 反代
|
||||
- `--workers 2`:⚠️ 结算周期任务(推荐激活/创作收益/超时关单)随每个 worker 的 lifespan 各启动一份(`app/services/settle_service.py`)。当前任务均为条件更新、重复执行无副作用;**修改周期任务逻辑时必须保证幂等**
|
||||
- 日志走 journald:`journalctl -u video-api -f`
|
||||
|
||||
### 2.3 初始化表结构 + 首个管理员
|
||||
常用命令:
|
||||
|
||||
```bash
|
||||
cd server
|
||||
.venv/bin/alembic upgrade head # 建 27 张表
|
||||
systemctl restart video-api # 改后端代码后重启
|
||||
systemctl enable --now video-api # 启动并恢复开机自启
|
||||
journalctl -u video-api -n 100 --no-pager
|
||||
```
|
||||
|
||||
# 创建后台管理员(代码不自动播种,需手动执行一次)
|
||||
### 4.2 环境配置(server/.env)
|
||||
|
||||
键清单(敏感值不入文档;`.env` 本身不入 git):
|
||||
|
||||
```
|
||||
APP_ENV=prod APP_HOST APP_PORT
|
||||
DATABASE_URL=mysql+aiomysql://root:256kb.cn@127.0.0.1:3307/video?charset=utf8mb4
|
||||
REDIS_URL=redis://127.0.0.1:6379/0
|
||||
CELERY_BROKER_URL CELERY_RESULT_BACKEND # 预留,当前未启用
|
||||
JWT_SECRET ACCESS_TOKEN_TTL REFRESH_TOKEN_TTL ADMIN_TOKEN_TTL
|
||||
OSS_ENDPOINT OSS_BUCKET OSS_ACCESS_KEY_ID OSS_ACCESS_KEY_SECRET # 未启用则为占位
|
||||
ALIPAY_APP_ID ALIPAY_PRIVATE_KEY ALIPAY_PUBLIC_KEY # 支付占位→dev_mock
|
||||
WECHAT_MCHID WECHAT_APP_ID WECHAT_PRIVATE_KEY
|
||||
TRON_API_KEY # USDT 查账
|
||||
SMSBAO_USERNAME SMSBAO_API_KEY SMS_SIGN_NAME # 短信宝验证码
|
||||
SMTP_HOST SMTP_PORT SMTP_USER SMTP_PASSWORD # 邮箱验证码(已启用)
|
||||
```
|
||||
|
||||
业务参数在 `server/config.yaml`(顶层键:`points` / `membership` / `recharge` / `referral` / `asset` / `withdraw` / `upload`),其中白名单项可在后台「系统配置」热改。
|
||||
|
||||
### 4.3 数据库初始化(新环境一次性)
|
||||
|
||||
```bash
|
||||
cd /root/project/video/server
|
||||
python3.11 -m venv .venv && .venv/bin/pip install .
|
||||
docker exec mysql8 mysql -uroot -p256kb.cn -e \
|
||||
"CREATE DATABASE IF NOT EXISTS video CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
|
||||
.venv/bin/alembic upgrade head # 4 个迁移,建 30 张业务表(含 series 补漏迁移 e7a1f3c58d02)
|
||||
```
|
||||
|
||||
创建首个超管(代码不自动播种):
|
||||
|
||||
```bash
|
||||
.venv/bin/python - <<'EOF'
|
||||
import asyncio
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
|
||||
@@ -77,127 +181,169 @@ asyncio.run(main())
|
||||
EOF
|
||||
```
|
||||
|
||||
### 2.4 启动
|
||||
后台角色:`super`(全部权限)、`operator`(仪表盘/套餐/赠送)、`reviewer`(视频审核)。
|
||||
|
||||
```bash
|
||||
cd server
|
||||
.venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
- 健康检查:`curl http://127.0.0.1:8000/health` → `{"status":"ok"}`
|
||||
- **生产请单 worker 运行**(不要加 `--workers > 1`):结算周期任务(推荐激活/创作收益/超时关单)随进程内 lifespan 启动,多 worker 会重复执行。
|
||||
- 生产建议用 systemd/supervisor 托管进程。
|
||||
|
||||
服务暴露的路径(Nginx 反代时用):
|
||||
### 4.4 服务暴露路径(反代依据)
|
||||
|
||||
| 路径 | 说明 |
|
||||
|---|---|
|
||||
| `/api/v1/*` | App 全部业务接口 |
|
||||
| `/api/v1/*` | App 业务接口(9 个路由文件 + admin,共 100+ 端点) |
|
||||
| `/api/v1/admin/*` | 管理后台接口 |
|
||||
| `/r/{code}` | 推广 H5 落地页(邀请链接) |
|
||||
| `/static/*` | 本地文件存储(头像/封面/视频,未接 OSS 时) |
|
||||
| `/r/{code}` | 邀请落地页 |
|
||||
| `/static/*` | 本地媒体文件 |
|
||||
| `/health` | 健康检查 |
|
||||
|
||||
## 3. 管理后台部署(admin/)
|
||||
## 5. 管理后台发布(admin/)
|
||||
|
||||
```bash
|
||||
cd admin
|
||||
cd /root/project/video/admin
|
||||
npm ci
|
||||
npm run build # 产物在 admin/dist/
|
||||
npm run build # 产物直接覆盖 admin/dist/,即 vm 站点根目录
|
||||
```
|
||||
|
||||
Nginx 参考配置(同一站点托管后台 + 反代 API):
|
||||
- 纯静态站点,**构建即发布,无需重启任何服务**;浏览器强刷(Ctrl+F5)生效
|
||||
- 前端 API baseURL 为 `/api/v1`,由 vm 站点的 `/api/` 反代到后端
|
||||
- 菜单按角色显示:仪表盘/套餐/赠送=operator,视频审核=reviewer,其余=super
|
||||
|
||||
## 6. nginx 配置(宝塔管理)
|
||||
|
||||
配置文件:`/www/server/panel/vhost/nginx/{h5,vm,vapi,codeserver}.256kb.top.conf`。
|
||||
改配置在宝塔面板「网站 → 设置 → 配置文件」编辑,或改文件后 `nginx -t && nginx -s reload`。
|
||||
|
||||
关键规则摘要:
|
||||
|
||||
**h5.256kb.top**
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name admin.example.com;
|
||||
# ssl_certificate / ssl_certificate_key ...
|
||||
|
||||
root /path/to/video/admin/dist;
|
||||
index index.html;
|
||||
|
||||
location / { try_files $uri $uri/ /index.html; } # SPA 回退
|
||||
location /api/ { proxy_pass http://127.0.0.1:8000; } # 后台接口
|
||||
location /static/ { proxy_pass http://127.0.0.1:8000; } # 媒体文件
|
||||
location /r/ { proxy_pass http://127.0.0.1:8000; } # 推广落地页
|
||||
}
|
||||
root /root/project/video/h5; # index.html 下载页
|
||||
location /downloads/ { alias /root/project/video/runtime/downloads/; } # APK
|
||||
location /videos/ { alias /root/project/video/runtime/videos/; }
|
||||
location /uploads/ { alias /root/project/video/runtime/uploads/; }
|
||||
location /r/ { proxy_pass http://127.0.0.1:8000; } # 邀请落地
|
||||
location /api/v1/invite/ { proxy_pass http://127.0.0.1:8000; }
|
||||
```
|
||||
|
||||
浏览器访问 `https://admin.example.com`,用 2.3 创建的账号登录。
|
||||
**vm.256kb.top**
|
||||
|
||||
## 4. App 编译(app/)—— 需要编译
|
||||
```nginx
|
||||
root /root/project/video/admin/dist;
|
||||
location / { try_files $uri $uri/ /index.html; } # SPA 回退
|
||||
location /api/ { proxy_pass http://127.0.0.1:8000; }
|
||||
location /static/ { proxy_pass http://127.0.0.1:8000; }
|
||||
location /r/ { proxy_pass http://127.0.0.1:8000; }
|
||||
```
|
||||
|
||||
App 是 Flutter 工程,**必须编译出安装包**分发给用户,服务器上不运行 App。
|
||||
**vapi.256kb.top**:`location / { proxy_pass http://127.0.0.1:8000; }` 全量反代。
|
||||
|
||||
### 4.1 关键:App 如何找到服务器(不硬编码域名)
|
||||
站点日志在 `/www/wwwlogs/<域名>.log`(排查 App 接口 401/500 时非常管用)。
|
||||
|
||||
启动时按优先级确定 API Base URL([api_domain.dart](../app/lib/core/network/api_domain.dart)):
|
||||
## 7. App 编译与发版
|
||||
|
||||
1. **`--dart-define=API_BASE` 编译期指定**(测试/自建最简单);
|
||||
2. 本地缓存的上次成功域名;
|
||||
3. DNS TXT 发现:解析 `txt.256kb.top` 的 TXT 记录,记录内容即 Base URL 主体;
|
||||
4. 兜底域名:`md5(yyyyMM) + "_jsnbhh.vip"`(每月变化)。
|
||||
### 7.1 域名发现(不硬编码 API 地址)
|
||||
|
||||
> 注意:客户端业务路径**不带** `/api/v1` 前缀,因此 Base URL 必须包含前缀,例如 `https://api.example.com/api/v1`(TXT 记录内容写 `api.example.com/api/v1` 即可,客户端自动补 `https://`)。
|
||||
启动时优先级:`--dart-define=API_BASE` > 本地缓存 > DNS TXT(`txt.256kb.top`) > 兜底域名(md5(yyyyMM)+后缀)。
|
||||
正式包**不带 dart-define 直接构建**,走 TXT 发现(当前指向 `vapi.256kb.top/api/v1`)。
|
||||
|
||||
上线二选一:
|
||||
|
||||
- **方式 A(推荐,免改代码)**:把 `txt.256kb.top` 的 TXT 记录指到你的 API 入口(内容:`api.example.com/api/v1`),并按第 3 节方式用 Nginx 把 `/api/v1/` 反代到 uvicorn。
|
||||
- **方式 B(自己的域名体系)**:修改 [app_config.dart](../app/lib/core/config/app_config.dart) 的 `dnsTxtHost` / `fallbackDomainSuffix` 为你控制的域名后重新编译。
|
||||
|
||||
### 4.2 Android
|
||||
### 7.2 一键发版 deploy.sh
|
||||
|
||||
```bash
|
||||
cd app
|
||||
flutter pub get
|
||||
# 联调/内测:直连服务器
|
||||
flutter build apk --release --dart-define=API_BASE=https://api.example.com/api/v1
|
||||
# 正式(走 TXT 域名发现):不带 dart-define 直接构建
|
||||
flutter build apk --release
|
||||
# 1. 改 app/pubspec.yaml 的 version(如 1.3.7+23,+后为 versionCode)
|
||||
# 2. 执行
|
||||
bash /root/project/video/deploy.sh
|
||||
```
|
||||
|
||||
产物:`build/app/outputs/flutter-apk/app-release.apk`,直接分发安装即可。
|
||||
脚本自动完成:`flutter analyze`(warning 不阻断)→ `flutter build apk --release` → `gradlew --stop` 清理 Gradle Daemon(防内存泄漏)→ 覆盖 `runtime/downloads/video-app.apk` → 同步 DB `system_configs.app.version.android`(latest_version/code 更新、**min_version_code 保留**、连接指定 `-h 127.0.0.1`)→ `docker exec my-redis` 清配置缓存。
|
||||
老用户打开 App 触发 `/api/v1/app/version` 检查 → 弹更新提示 → 跳 h5 下载页。强制更新:把 `min_version_code` 调大于用户版本。
|
||||
|
||||
### 4.3 iOS
|
||||
### 7.3 正式签名(2026-10-17 起,v1.3.7+23 为首个正式签名版)
|
||||
|
||||
```bash
|
||||
flutter build ipa --release
|
||||
```
|
||||
|
||||
需要 macOS + Xcode + Apple 开发者证书签名(本机未装 Xcode 时可只交付 Android)。iOS 端上架前在管理后台「系统配置」确认 `ios_payment.enabled` 开关(规避苹果虚拟支付审核)。
|
||||
|
||||
### 4.4 支付渠道打包注意
|
||||
|
||||
`app/pubspec.yaml` 中 `tobias.url_scheme`、`fluwx.app_id`、两处 `universal_link` 是占位值,**正式支付上线前必须替换为商户申请的真实值**并重新编译;未接真实支付时服务端返回 `dev_mock`,App 自动走轮询查单兜底。
|
||||
|
||||
## 5. 上线后必改的后台配置
|
||||
|
||||
登录管理后台「系统配置」:
|
||||
|
||||
| 配置 key | 说明 |
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| `app.invite_base_url` | 推广落地页地址,改为 `https://你的域名/r/`(影响 App 邀请链接) |
|
||||
| `app.version.android` / `app.version.ios` | JSON:`latest_version` / `latest_version_code` / `min_version_code` / `update_url` / `notice`,控制版本检查与强制更新 |
|
||||
| `ios_payment.enabled` | iOS 支付开关(审核期可关闭) |
|
||||
| 敏感词、轮播公告等 | 按需 |
|
||||
| keystore | `app/android/app/upload-keystore.jks`(PKCS12,RSA 2048,有效期 30 年) |
|
||||
| 密码配置 | `app/android/key.properties`(alias=`video_release`) |
|
||||
| 证书指纹 SHA-256 | `8fb75f6a:3a9f:bc39:...:aef4`(完整值见 `apksigner verify --print-certs`) |
|
||||
| git | 两者均已 gitignore,**不入库** |
|
||||
| 备份 | `/root/backups/video-app-signing/`(keystore + key.properties) |
|
||||
|
||||
## 6. 端到端验证清单
|
||||
`build.gradle.kts` 逻辑:key.properties 存在 → release 签名;否则回退 debug(保证新环境可编译)。
|
||||
|
||||
> ⚠️ **keystore 丢失 = 永远无法给老用户推更新**。除服务器备份外,务必另存一份到本地电脑/网盘。
|
||||
> ⚠️ 首个正式签名版与旧 debug 签名不一致,**老用户必须先卸载再安装**(版本 notice 已注明)。
|
||||
> 侧载安装时小米等厂商会提示"未备案",勾选"已了解风险"即可继续安装;消除该提示需办理 App ICP 备案。
|
||||
|
||||
### 7.4 iOS
|
||||
|
||||
`flutter build ipa --release`(需 macOS + Xcode + 开发者证书,当前仅交付 Android)。上架前确认后台 `ios_payment.enabled` 开关规避虚拟支付审核。
|
||||
|
||||
## 8. 项目启停 project-ctl.sh
|
||||
|
||||
```bash
|
||||
# 服务端
|
||||
curl http://127.0.0.1:8000/health # ok
|
||||
curl "http://127.0.0.1:8000/api/v1/videos/feed?type=hot&limit=1" # code:0
|
||||
curl "http://127.0.0.1:8000/api/v1/search/hot" # code:0
|
||||
|
||||
# 管理后台:浏览器打开 → 登录 → 视频审核/用户列表有数据
|
||||
|
||||
# App:安装 APK → 启动页过版本检查 → Feed 出视频 → 注册/登录 → 观看计费
|
||||
bash /root/project/video/project-ctl.sh start # docker mysql8/my-redis → 等待就绪 → video-api(enable+start) → 健康检查
|
||||
bash /root/project/video/project-ctl.sh stop # video-api 停止并 disable + 停两个容器(重启服务器也不会拉起)
|
||||
bash /root/project/video/project-ctl.sh restart # 停 → 启
|
||||
bash /root/project/video/project-ctl.sh status # 服务/容器/接口探测
|
||||
```
|
||||
|
||||
## 7. 已知限制(当前版本)
|
||||
- 管理范围:`video-api.service` + `mysql8` + `my-redis`;**不含 nginx**(系统组件)
|
||||
- 服务器重启后如项目处于"运行态",docker 与 systemd 会自动拉起全部组件,无需人工干预
|
||||
- 项目当前(2026-10-18 起)处于**暂停态**,恢复运行执行 `start` 即可
|
||||
|
||||
- **媒体存储为本地文件**(`server/uploads`,经 `/static` 访问),接口返回相对路径;生产接入 OSS/CDN 使服务端返回绝对 URL 需二次开发(`storage_service` 已按此边界隔离)。
|
||||
- 短信/邮件/USDT/支付宝/微信未配置密钥时自动降级(验证码发不出、支付走 dev_mock),不影响其余功能。
|
||||
- CORS 当前为 `*`,生产可在 `app/main.py` 按需收紧。
|
||||
- 结算周期任务在 API 进程内运行,见 2.4 单 worker 说明。
|
||||
## 9. 备份策略
|
||||
|
||||
| 对象 | 方式 | 频率建议 |
|
||||
|---|---|---|
|
||||
| MySQL 数据 | `docker exec mysql8 mysqldump -uroot -p256kb.cn video \| gzip > backup-$(date +%F).sql.gz`(或宝塔计划任务) | 每日 |
|
||||
| keystore + key.properties | 已备份 `/root/backups/video-app-signing/`,另离线保存 | 变更即备 |
|
||||
| `runtime/`(视频/APK/上传) | 文件级备份(rsync/对象存储) | 每周 |
|
||||
| `server/.env`、`config.yaml` | 纳入文件备份 | 变更即备 |
|
||||
|
||||
恢复:导入 sql → `alembic upgrade head` 校验版本 → 启动服务。
|
||||
|
||||
## 10. 日常运维与故障排查
|
||||
|
||||
### 10.1 常用命令
|
||||
|
||||
```bash
|
||||
journalctl -u video-api -f # 后端日志
|
||||
docker logs -f mysql8 / my-redis # 容器日志
|
||||
tail -f /www/wwwlogs/vapi.256kb.top.log # App 接口访问日志
|
||||
docker exec my-redis redis-cli DEL config:system # 清系统配置缓存
|
||||
docker exec my-redis redis-cli KEYS 'auth:*' # 查登录态键
|
||||
free -h && docker stats --no-stream # 资源占用
|
||||
```
|
||||
|
||||
### 10.2 历史故障速查表
|
||||
|
||||
| 症状 | 根因 | 处置 |
|
||||
|---|---|---|
|
||||
| 后端全接口 500,Redis 报错 | 服务器重启后 my-redis 未自启,宿主机 Redis 5.0.3 抢 6379,RESP3 握手失败 | 停本机 redis 自启;docker start my-redis;客户端已固定 protocol=2 |
|
||||
| 重启后服务缺失/端口错乱 | Docker 容器未配 restart 策略 | 两容器均已 `unless-stopped` |
|
||||
| App 更新提示反复弹 | 硬编码版本号与 APK 不一致 | 已改用 package_info_plus 读真实版本号 |
|
||||
| 发版后 DB 版本没更新 | deploy.sh mysql 未加 `-h 127.0.0.1` 写错宿主机库 | 已修复;注意检查 |
|
||||
| 编译后内存爆满 | Gradle Daemon 常驻 | deploy.sh 已自动 `gradlew --stop` |
|
||||
| IDE 里 OpenJDK 吃内存 | Trae 的 fwcd.kotlin 扩展语言服务器 | 禁用该扩展并重载窗口 |
|
||||
| 编译卡死无输出 | Gradle 进程僵死 | 杀掉重跑即可 |
|
||||
| 会员/钱包页加载失败 | 僵尸登录态(access 过期+无 refresh) | App 端已修复(401 清态+启动检测);临时:退出重登 |
|
||||
| 切"我的"仍听到视频声 | 播放器池 open 竞态 | 已修复(suspend/resume) |
|
||||
| 进度条不走/拖动无效 | 本地代理未支持 Range | 已修复(Accept-Ranges + 206) |
|
||||
|
||||
### 10.3 端到端验证清单(发版/重启后)
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:8000/health # {"status":"ok"}
|
||||
curl "http://127.0.0.1:8000/api/v1/videos/feed?type=hot&limit=1" # code:0
|
||||
curl -I https://h5.256kb.top/downloads/video-app.apk # 200
|
||||
# 浏览器开 https://vm.256kb.top → 登录 → 仪表盘有数据
|
||||
# App:启动过版本检查 → Feed 出视频 → 登录 → 播放/计费正常
|
||||
```
|
||||
|
||||
## 11. 新环境从零部署速查
|
||||
|
||||
1. 装 Docker / Python3.11 / Node18+ / Flutter / nginx(或宝塔)
|
||||
2. 建 mysql8、my-redis 容器(§3 命令)
|
||||
3. `server/` 建 venv、装依赖、写 `.env`、`alembic upgrade head`、建超管(§4.3)
|
||||
4. 写 systemd unit(§4.1)→ `systemctl enable --now video-api`
|
||||
5. `admin/` 构建 dist;配三个 nginx 站点 + 证书(§6)
|
||||
6. App:放入 keystore + key.properties → `deploy.sh` 发版(§7)
|
||||
7. 后台「系统配置」:`app.invite_base_url`、`app.version.android/ios` 等(见 03 接口文档配置键)
|
||||
8. 跑 §10.3 验证清单
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
# 05-功能流程文档
|
||||
|
||||
> 本文按"角色 → 端到端流程"梳理当前项目的全部业务功能(截至 2026-10-18,App v1.3.7)。
|
||||
> 技术实现细节见《02-技术文档》,接口字段见《03-接口文档》,数值参数以 `server/config.yaml` 与后台「系统配置」为准。
|
||||
|
||||
## 1. 角色与端
|
||||
|
||||
| 角色 | 使用端 | 能力 |
|
||||
|---|---|---|
|
||||
| 游客 | App | 每日免费观看 10 部、浏览/搜索(部分接口需登录) |
|
||||
| 注册用户 | App | 观看计费、点赞/收藏/评论、充值/会员、邀请、上传(若开放)、提现 |
|
||||
| 会员 | App | 有效期内观看不扣积分 |
|
||||
| 管理员 | 后台 vm.256kb.top | super=全部;operator=仪表盘/套餐/赠送;reviewer=视频审核 |
|
||||
|
||||
## 2. 账号体系流程
|
||||
|
||||
### 2.1 注册
|
||||
|
||||
```
|
||||
App 注册页(手机号/邮箱 Tab 切换)
|
||||
├─ 手机号注册 → /auth/sms/send(短信宝)→ 填验证码+密码(+邀请码) → 注册
|
||||
└─ 邮箱注册 → /auth/email/send(SMTP)→ 填验证码+密码(+邀请码) → 注册
|
||||
│
|
||||
├─ 验证码写 Redis(5 分钟有效,防刷频控)
|
||||
├─ 填邀请码 → 建立 referral 绑定(风控:同人单日绑定≤10、同 IP 单日注册≤5)
|
||||
└─ 可选「注册送试用会员」:后台开关 register_gift_trial.enabled / .days
|
||||
```
|
||||
|
||||
### 2.2 登录与登录态
|
||||
|
||||
- 登录账号支持**手机号或邮箱**(不支持昵称);密码 bcrypt 校验
|
||||
- 令牌体系:`access_token`(短效)+ `refresh_token`(长效,Redis 白名单)
|
||||
- App 收到 401 → 自动用 refresh 换新 access;**refresh 也失效/缺失 → 清登录态回游客**(防僵尸登录态)
|
||||
- 全端下线机制:改密/后台改密后 `auth:epoch:{uid}` 递增并删除 refresh,旧 token 全部立即失效
|
||||
- 后台管理员独立 `admin_token`(含角色, super 放行一切)
|
||||
|
||||
## 3. 观看与计费流程(核心)
|
||||
|
||||
```
|
||||
打开 App → Splash(版本检查/app/version → 必要时弹更新)
|
||||
→ Feed(推荐/最新/热度/口碑/热播 五 Tab,各自保活、独立翻页)
|
||||
→ 进入某视频:
|
||||
1. 播放器池加载(当前播放 + 下一个预缓冲;ts 走 App 内嵌缓存代理,命中免下载)
|
||||
2. 计费判定(仅登录用户):
|
||||
├─ 会员有效期内 → 免费
|
||||
├─ 今日免费额度未用完(10 部/日)→ 免费,额度-1
|
||||
├─ 该视频今日已付过费(去重) → 免费
|
||||
└─ 否则扣 1 积分/部;积分不足 → 弹充值引导
|
||||
3. 观看上报 → watch_records(完播判定:进度≥95%)
|
||||
```
|
||||
|
||||
- 数值:`points.daily_free_count=10`、`points.watch_cost=1`、`asset.complete_progress=0.95`
|
||||
- 热度公式:`(播放×1 + 点赞×5 + 评论×10) ÷ (1 + 发布天数)`;口碑=按点赞量、热播=按播放量
|
||||
- 评论奖励:每评论 10 个不同视频奖 1 积分(≥5 字,日限 5 次)
|
||||
|
||||
## 4. 充值与会员流程
|
||||
|
||||
### 4.1 积分充值
|
||||
|
||||
```
|
||||
App「钱包/充值」→ 选套餐(后台可配 recharge_packages,1 元=10 积分,支持自定义金额)
|
||||
→ 下单(recharge_orders,30 分钟超时自动关单)
|
||||
→ 支付渠道:支付宝 / 微信 / USDT-TRC20(rate 7.2,19 确认)
|
||||
(未配置真实密钥时服务端返回 dev_mock,App 走轮询查单兜底)
|
||||
→ 回调/查单到账 → 积分入账(point_transactions)
|
||||
→ 若该用户有邀请人 → 充值额的 5% 以资产返利给邀请人
|
||||
```
|
||||
|
||||
### 4.2 会员
|
||||
|
||||
- 档位:试用 1 天 5 积分 / 月 30 天 300 / 年 365 天 3650 / 永久 10000(均可后台开关)
|
||||
- 用**积分**兑换 → 写 user_membership + membership_orders;有效期内观看不扣积分
|
||||
- 管理员可在后台「赠送管理」直接赠送会员/积分(operator 权限)
|
||||
|
||||
## 5. 邀请裂变流程(referral)
|
||||
|
||||
```
|
||||
用户 A 分享邀请链接(h5.256kb.top/r/{code})→ 用户 B 打开落地页下载 App
|
||||
→ B 注册时带邀请码 → 绑定 referral
|
||||
→ A 立即得 10 积分(register_reward_points,不限量)
|
||||
→ B 在 72h 内观看≥3 个视频 → referral「激活」(周期任务结算)
|
||||
→ B 每次充值 → A 得充值积分 5% 的资产返利
|
||||
风控:A 单日新绑定>10 → 冻结审核;同 IP 单日注册>5 → 标记风险;后台「推荐风控」可人工处理
|
||||
```
|
||||
|
||||
## 6. 创作激励与提现流程
|
||||
|
||||
```
|
||||
作者发布视频(见 §8)→ 视频产生数据
|
||||
→ 激励结算(周期任务):
|
||||
每 1000 次完整观看 = 1 资产;每 100 次点赞 = 1 资产(video_earnings → asset_wallets)
|
||||
→ 资产价值:10 资产 = 1 元
|
||||
→ 提现:满 100 资产(=10 元)可提,渠道 支付宝/微信/USDT,当前手续费 0
|
||||
→ withdrawals 提交 → 后台「提现审核」人工通过/驳回 → 打款登记(asset_transactions 全程流水)
|
||||
```
|
||||
|
||||
## 7. 互动功能流程
|
||||
|
||||
- **点赞/收藏**:即时生效,计入视频计数与热度分;收藏还用于推荐策略的标签偏好(收藏+5 > 点赞+3 > 观看+1)
|
||||
- **评论**:发布前过**敏感词过滤**(后台可配词库与开关 `sensitive.filter.enabled`);后台可删评
|
||||
- **举报**:用户举报视频/评论 → reports → 后台「举报处理」(待处理/已处理/已驳回)
|
||||
- **通知**:审核结果、结算、系统公告 → notifications → App 消息中心(未读数)
|
||||
|
||||
## 8. 内容供给流程(三条路)
|
||||
|
||||
### 8.1 用户/管理员上传
|
||||
|
||||
```
|
||||
上传 mp4/mov/mkv/avi(≤500MB,时长 5~600s)
|
||||
→ transcode_tasks 转码 HLS 720p(4s 分片)
|
||||
→ videos.status=1 待审核 → 后台「视频审核」(reviewer) 通过/拒绝(拒绝填原因回显作者)
|
||||
→ 通过:status=2 上架、通知作者、进入各 Feed 池
|
||||
```
|
||||
|
||||
### 8.2 采集源同步(当前内容主力,约 1.8 万部)
|
||||
|
||||
```
|
||||
后台「采集源管理」配置源站(URL 模板等)→ 手动「同步单源/全部同步」
|
||||
→ collection_service 按模板拉取 → collection_records 记录明细(成功/失败/关联 video_id)
|
||||
→ 入库存量视频直接上架(status=2)
|
||||
```
|
||||
|
||||
### 8.3 标题关键词标签(离线脚本)
|
||||
|
||||
```
|
||||
server/scripts/build_tags_from_titles.py(幂等,可重复跑)
|
||||
→ jieba TF-IDF topK=6 + 番号/书名号/括号提取 - 噪音词过滤
|
||||
→ 写 tags / video_tags,回算 tags.video_count
|
||||
→ App 搜索热词/标签视频页、后台「标签管理」均消费此数据
|
||||
```
|
||||
|
||||
## 9. 视频与后台管理状态机
|
||||
|
||||
**视频 status**:0 草稿 / 1 待审核 / 2 已上架 / 3 审核拒绝 / 4 已下架(后台可编辑标题/封面/分类/系列/剧集、上下架、删除——删除会级联清理标签/点赞/收藏/评论/观看/收益/举报)
|
||||
|
||||
**系列 series**:视频可绑定 series_id + episode_no 组成剧集;App 播放页提供选集;`total_episodes` 自动维护
|
||||
|
||||
**用户管理**:列表/详情(观看、充值、推荐关系)→ 可 封禁/解封(status 1/2/3)、调整积分(正负流水+原因)、**修改密码**(全端立即下线,记 admin_logs)
|
||||
|
||||
**其余后台能力**:仪表盘(今日 DAU/新增/充值/积分/播放+待审数)、积分流水、充值订单、套餐 CRUD、分类管理、提现审核、推荐风控、举报处理、敏感词、系统配置(白名单热改)、操作日志(admin_logs 全量留痕)
|
||||
|
||||
## 10. App 版本更新流程
|
||||
|
||||
```
|
||||
deploy.sh 发版 → APK 覆盖 h5.256kb.top/downloads/video-app.apk
|
||||
+ DB app.version.android 更新(latest/min/update_url/notice)
|
||||
App 每次启动 → /app/version 比对本地真实 versionCode(package_info_plus)
|
||||
├─ local < min_version_code → 强制更新弹窗(不可跳过)
|
||||
├─ local < latest → 提示更新(可跳过),装新版后自动消失
|
||||
└─ notice 非空 → 弹窗附带公告文案
|
||||
```
|
||||
|
||||
## 11. 本地缓存与下载流程(App 端体验层)
|
||||
|
||||
```
|
||||
播放:网络 m3u8 → 改写走 127.0.0.1 本地代理
|
||||
├─ m3u8 每次回源(签名时效)→ ts 地址改写指向本地
|
||||
├─ ts 按「去签名路径」磁盘缓存,命中直接返回(回看/重开 App 免下载)
|
||||
└─ LRU 上限 1GB(超限按访问时间清到 900MB)
|
||||
|
||||
下载:Feed/播放页长按 →「下载」→ 拉 m3u8 → 4 并发下 ts+密钥 → 生成本地 index.m3u8
|
||||
→「我的 → 已下载」列表(封面/时长/大小)→ 点击经本地代理离线播放(支持拖动)→ 可删除
|
||||
```
|
||||
Reference in New Issue
Block a user