Files
video/docs/02-技术文档.md
T
2026-10-15 20:36:59 +08:00

685 lines
39 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.
# 短视频应用技术文档
- 版本:v1.0
- 日期:2026-10-14
- 阅读对象:开发、测试、运维
---
## 1. 项目架构
### 1.1 总体架构
```
┌─────────────────────────────────────────────────────────────┐
│ 客户端层 │
│ Flutter App (iOS / Android) + Web 管理后台 (Vue3) │
└──────────────────────────┬──────────────────────────────────┘
│ HTTPS (JSON / 文件上传)
▼
┌─────────────────────────────────────────────────────────────┐
│ 接入层:Nginx │
│ TLS 终止 / 反向代理 / 静态与HLS分片加速 / 限流 │
└───────┬──────────────────────────────┬──────────────────────┘
▼ ▼
┌───────────────────┐ ┌──────────────────────────────┐
│ FastAPI 应用服务 │ │ 对象存储 OSS / MinIO │
│ (Uvicorn x N) │ │ 源视频 / m3u8+ts / 封面/头像 │
│ 无状态水平扩展 │ └──────────────▲───────────────┘
└──┬────┬────┬──────┘ │ 转码产物上传
│ │ │ │
│ │ └───────────────┐ │
▼ ▼ ▼ │
┌───────┐ ┌──────────────────────┐ ┌──────┴───────────────┐
│MySQL 8│ │ Redis 7 │ │ Celery Worker │
│主数据 │ │ 缓存/计数/队列(Broker) │ │ FFmpeg 转码 / 异步任务 │
└───────┘ └──────────────────────┘ └──────────────────────┘
▲ ▲
│ 支付异步回调 │ USDT 链上监控轮询
┌───────┴───────────────────────────────────┴───────────────┐
│ 支付宝开放平台 / 微信支付V3 / TronGrid(TRC20 节点服务) │
└───────────────────────────────────────────────────────────┘
```
### 1.2 部署架构(单机起步版,docker-compose)
| 容器 | 镜像 | 说明 |
|---|---|---|
| nginx | nginx:1.25 | 443 端口;/api 反代到 app;/hls 直出 OSS 回源或本地 |
| app | 自建 python:3.11-slim | uvicorn --workers 4,FastAPI 服务 |
| worker | 同 app 镜像 | celery worker -Q transcode.high,transcode.normal,default |
| mysql | mysql:8.0 | 主数据,挂载数据卷,开启 binlog |
| redis | redis:7 | 缓存 + Celery Broker,开启 AOF |
| admin | node 构建产物,nginx 托管静态文件 | 管理后台(Vue3),建议仅内网/VPN 访问 |
扩展路径:App 无状态 → 多副本 + SLB;MySQL → 云 RDS 主从;Redis → 云 Redis;转码 → 独立 GPU/多核 Worker 机器或云转码服务。
### 1.3 关键链路
- **播放链路**:App → `GET /hls/{vid}/index.m3u8` → Nginx → OSS/CDN(m3u8 与 ts 均为静态文件,天然适合 CDN 加速)
- **上传链路**:App → 获取上传凭证 → 直传 OSS(推荐,节省服务端带宽)或经服务端中转 → 回调发布 → 转码 → 审核 → 上架
- **支付链路**:App 下单 → 调起支付 SDK → 平台异步回调服务端 → 验签 → 事务入账 → App 轮询查单确认结果
---
## 2. 技术栈
### 2.1 选型总表
| 层 | 技术 | 版本 | 选型理由 |
|---|---|---|---|
| 后端框架 | **FastAPI** | ≥0.110 | 原生异步、Pydantic 校验、自动生成 OpenAPI 文档、性能高 |
| ASGI 服务器 | Uvicorn / Gunicorn+UvicornWorker | latest | 生产多进程 |
| ORM | SQLAlchemy (async) | 2.0 | 生态成熟,2.0 异步支持完善 |
| 迁移 | Alembic | latest | 与 SQLAlchemy 官方配套 |
| 校验 | Pydantic | v2 | 性能强,与 FastAPI 深度集成 |
| 数据库 | MySQL | 8.0 | 团队熟悉度高、事务/唯一约束完备;utf8mb4 |
| 缓存/队列 | Redis | 7 | 计数、Set 去重、Lua 原子脚本、Celery Broker |
| 异步任务 | Celery | 5.x | 转码、落库、关单等重任务异步化 |
| 转码 | FFmpeg | ≥6.0 | HLS 转码事实标准 |
| 对象存储 | 阿里云 OSS / 腾讯云 COS / MinIO | - | 国内上线选云 OSS;自建测试用 MinIO(S3 兼容) |
| 支付 SDK | alipay-sdk-python / wechatpay-python / tronpy | latest | 官方/主流 SDK |
| 短信/邮件 | 阿里云短信 / SMTP | - | 验证码通道 |
| 前端框架 | Flutter | 3.x (Dart 3) | 单代码库双端,生态成熟 |
| 状态管理 | flutter_riverpod | 2.x | 编译安全、可测试、适合中大型项目 |
| 网络 | dio | 5.x | 拦截器体系(Token 注入/刷新/设备ID) |
| 播放器 | media_kit | latest | 全平台 HLS(m3u8) 支持好,可定制 UI;备选 video_player+chewie |
| 路由 | go_router | latest | 声明式路由、深链支持 |
| 本地存储 | shared_preferences + flutter_secure_storage | - | 配置 / Token 安全存储 |
| 图片缓存 | cached_network_image | - | 封面、头像 |
| 管理后台 | Vue 3 + Element Plus + Vite | 3.x | 一期人工审核与运营后台 |
| DNS 解析 | DoH(阿里 223.5.5.5 / 腾讯 119.29.29.29) | - | App 域名发现,避免本地 DNS 污染 |
### 2.2 后端框架对比(为什么是 FastAPI)
| 维度 | FastAPI | Django REST Framework | Flask |
|---|---|---|---|
| 异步 IO | ★★★★★ 原生 | ★★★ 部分 | ★★ 需 gevent 等 |
| 开发效率 | ★★★★ 类型即文档 | ★★★ 全家桶但重 | ★★★ 简单但都要自己装 |
| 自动 API 文档 | 内置 Swagger/ReDoc | 第三方 | 第三方 |
| 项目规模匹配 | 中小团队微服务/单 API 服务 | 大型全栈站 | 玩具到中型 |
| 生态(支付/任务等) | 与 Django 相当的可组合性 | 最丰富 | 丰富 |
结论:本项目为纯 API 后端 + 异步任务场景,选 **FastAPI**;Django 适合还需要全功能 Admin 的场景(若一期就要管理后台,可并行评估 Django Admin 或独立前端 + FastAPI 管理接口)。
---
## 3. 数据库设计(技术实现)
### 3.1 通用约定
- 引擎 InnoDB,字符集 utf8mb4 / utf8mb4_unicode_ci
- 主键统一 `BIGINT UNSIGNED AUTO_INCREMENT`
- 时间字段 `DATETIME`,默认 `CURRENT_TIMESTAMP` / `ON UPDATE CURRENT_TIMESTAMP`,全部存服务器本地时区(Asia/Shanghai)
- 金额一律整型存储:人民币用**分**(amount_fen);积分 INT
- 逻辑删除仅在评论等少数表使用(status 字段),其余物理保留
### 3.2 核心表 DDL
```sql
CREATE TABLE users (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
phone VARCHAR(20) NULL COMMENT '手机号',
email VARCHAR(100) NULL COMMENT '邮箱',
password_hash VARCHAR(255) NOT NULL,
nickname VARCHAR(50) NOT NULL DEFAULT '',
avatar_url VARCHAR(500) NOT NULL DEFAULT '',
gender TINYINT NOT NULL DEFAULT 0,
bio VARCHAR(200) NOT NULL DEFAULT '',
status TINYINT NOT NULL DEFAULT 1 COMMENT '1正常 2封禁',
register_type TINYINT NOT NULL COMMENT '1手机 2邮箱',
invite_code VARCHAR(10) NOT NULL DEFAULT '' COMMENT '我的邀请码',
register_ip VARCHAR(45) NOT NULL DEFAULT '' COMMENT '注册IP',
last_login_at DATETIME NULL,
last_login_ip VARCHAR(45) NOT NULL DEFAULT '' COMMENT '最后登录IP',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_phone (phone),
UNIQUE KEY uk_email (email),
UNIQUE KEY uk_invite_code (invite_code),
KEY idx_status_created (status, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE user_wallets (
user_id BIGINT UNSIGNED PRIMARY KEY,
points INT UNSIGNED NOT NULL DEFAULT 0,
total_recharge INT UNSIGNED NOT NULL DEFAULT 0,
total_earned INT UNSIGNED NOT NULL DEFAULT 0,
total_consumed INT UNSIGNED NOT NULL DEFAULT 0,
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_wallet_user FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE videos (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
category_id INT NOT NULL DEFAULT 0,
title VARCHAR(100) NOT NULL DEFAULT '',
description VARCHAR(500) NOT NULL DEFAULT '',
cover_url VARCHAR(500) NOT NULL DEFAULT '',
video_url VARCHAR(500) NOT NULL DEFAULT '' COMMENT 'm3u8地址',
duration INT NOT NULL DEFAULT 0,
file_size BIGINT NOT NULL DEFAULT 0,
width INT NOT NULL DEFAULT 0,
height INT NOT NULL DEFAULT 0,
status TINYINT NOT NULL DEFAULT 0 COMMENT '0转码中 1待审核 2已发布 3拒绝 4下架 5转码失败',
reject_reason VARCHAR(200) NOT NULL DEFAULT '',
reviewer_id BIGINT UNSIGNED NULL COMMENT '审核人(admin_users.id)',
reviewed_at DATETIME NULL COMMENT '审核时间',
is_priority TINYINT NOT NULL DEFAULT 0,
view_count BIGINT NOT NULL DEFAULT 0,
like_count INT NOT NULL DEFAULT 0,
comment_count INT NOT NULL DEFAULT 0,
favorite_count INT NOT NULL DEFAULT 0,
published_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY idx_user_status (user_id, status),
KEY idx_feed (status, published_at),
KEY idx_category (category_id, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE watch_records (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
subject_type TINYINT NOT NULL COMMENT '1用户 2游客',
subject_id VARCHAR(64) NOT NULL COMMENT 'user_id或device_id',
video_id BIGINT UNSIGNED NOT NULL,
watch_date DATE NOT NULL,
cost_type TINYINT NOT NULL COMMENT '0免费 1积分 2会员 3本人',
cost_points INT NOT NULL DEFAULT 0,
watch_duration INT NOT NULL DEFAULT 0,
position INT NOT NULL DEFAULT 0 COMMENT '最后播放进度(秒),用于续播',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_dedup (subject_type, subject_id, video_id, watch_date),
KEY idx_subject (subject_type, subject_id, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE point_transactions (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
change_amount INT NOT NULL COMMENT '正增负减',
balance_after INT NOT NULL,
biz_type TINYINT NOT NULL COMMENT '1充值 2观看 3评论奖励 4会员 5赠送 6调整 7扣回',
biz_id VARCHAR(64) NOT NULL DEFAULT '',
remark VARCHAR(200) NOT NULL DEFAULT '',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY idx_user_created (user_id, created_at),
KEY idx_biz (biz_type, biz_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE recharge_orders (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(32) NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
amount_fen INT NOT NULL,
points INT NOT NULL,
pay_method TINYINT NOT NULL COMMENT '1支付宝 2微信 3USDT',
status TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2已关闭 3已退款',
third_trade_no VARCHAR(64) NULL,
usdt_amount DECIMAL(18,6) NULL,
usdt_rate DECIMAL(10,4) NULL,
paid_at DATETIME NULL,
expire_at DATETIME NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_order_no (order_no),
UNIQUE KEY uk_third_trade_no (third_trade_no),
KEY idx_user_status (user_id, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- likes / favorites 同构
CREATE TABLE likes (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
video_id BIGINT UNSIGNED NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_user_video (user_id, video_id),
KEY idx_video (video_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
```sql
-- 标签
CREATE TABLE tags (
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20) NOT NULL,
video_count INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_name (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE video_tags (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
video_id BIGINT UNSIGNED NOT NULL,
tag_id INT UNSIGNED NOT NULL,
UNIQUE KEY uk_video_tag (video_id, tag_id),
KEY idx_tag (tag_id, video_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 充值套餐(管理后台可配)
CREATE TABLE recharge_packages (
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
amount_yuan INT NOT NULL,
points INT NOT NULL COMMENT '基础积分=金额x汇率',
bonus_points INT NOT NULL DEFAULT 0,
sort INT NOT NULL DEFAULT 0,
status TINYINT NOT NULL DEFAULT 1 COMMENT '1上架 0下架',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 管理员与操作日志
CREATE TABLE admin_users (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
name VARCHAR(50) NOT NULL DEFAULT '',
role VARCHAR(20) NOT NULL COMMENT 'super/reviewer/operator',
status TINYINT NOT NULL DEFAULT 1,
last_login_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_username (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE admin_logs (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
admin_id BIGINT UNSIGNED NOT NULL,
action VARCHAR(50) NOT NULL,
target VARCHAR(100) NOT NULL DEFAULT '',
detail VARCHAR(1000) NOT NULL DEFAULT '',
ip VARCHAR(45) NOT NULL DEFAULT '',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY idx_admin_created (admin_id, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 平台配置(后台可改,Redis 缓存 60s)
CREATE TABLE system_configs (
config_key VARCHAR(50) PRIMARY KEY,
config_value VARCHAR(500) NOT NULL DEFAULT '',
description VARCHAR(200) NOT NULL DEFAULT '',
updated_by BIGINT UNSIGNED NULL,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 预置:register_gift_trial.enabled=1(注册赠送体验会员开关)、register_gift_trial.days=1(赠送天数)
```
```sql
-- 激励域:推荐关系 / 资产钱包 / 资产流水 / 提现 / 视频收益
CREATE TABLE referrals (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
inviter_id BIGINT UNSIGNED NOT NULL COMMENT '推荐人',
invitee_id BIGINT UNSIGNED NOT NULL COMMENT '被推荐人',
invite_code VARCHAR(10) NOT NULL,
status TINYINT NOT NULL DEFAULT 0 COMMENT '0待激活 1有效 2无效 3风控冻结',
risk_flag VARCHAR(50) NOT NULL DEFAULT '',
register_ip VARCHAR(45) NOT NULL DEFAULT '',
activated_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_invitee (invitee_id),
KEY idx_inviter (inviter_id, created_at),
KEY idx_status (status, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE asset_wallets (
user_id BIGINT UNSIGNED PRIMARY KEY,
assets DECIMAL(12,1) NOT NULL DEFAULT 0 COMMENT '当前资产(允许为负)',
total_earned DECIMAL(14,1) NOT NULL DEFAULT 0,
total_withdrawn DECIMAL(14,1) NOT NULL DEFAULT 0,
version INT NOT NULL DEFAULT 0,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE asset_transactions (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
change_amount DECIMAL(12,1) NOT NULL,
balance_after DECIMAL(12,1) NOT NULL,
biz_type TINYINT NOT NULL COMMENT '1充值返利 2完整观看 3点赞 4提现 5提现退回 6返利扣回 7后台调整',
biz_id VARCHAR(64) NOT NULL DEFAULT '',
remark VARCHAR(200) NOT NULL DEFAULT '',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY idx_user_created (user_id, created_at),
KEY idx_biz (biz_type, biz_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE withdrawals (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
withdraw_no VARCHAR(32) NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
assets DECIMAL(12,1) NOT NULL,
amount_fen INT NOT NULL COMMENT '折合人民币(分)',
channel TINYINT NOT NULL COMMENT '1支付宝 2微信 3USDT',
account VARCHAR(200) NOT NULL,
status TINYINT NOT NULL DEFAULT 0 COMMENT '0待审核 1已打款 2已拒绝',
reason VARCHAR(200) NOT NULL DEFAULT '',
admin_id BIGINT UNSIGNED NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
processed_at DATETIME NULL,
UNIQUE KEY uk_withdraw_no (withdraw_no),
KEY idx_user (user_id, created_at),
KEY idx_status (status, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE video_earnings (
video_id BIGINT UNSIGNED PRIMARY KEY,
author_id BIGINT UNSIGNED NOT NULL,
complete_views INT NOT NULL DEFAULT 0,
views_settled INT NOT NULL DEFAULT 0,
likes_accrued INT NOT NULL DEFAULT 0,
likes_settled INT NOT NULL DEFAULT 0,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY idx_author (author_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
```sql
-- 通用域:通知 / 举报 / 敏感词
CREATE TABLE notifications (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL COMMENT '接收人',
type TINYINT NOT NULL COMMENT '1审核结果 2提现进度 3系统公告 4评论回复',
title VARCHAR(100) NOT NULL DEFAULT '',
content VARCHAR(500) NOT NULL DEFAULT '',
biz_id VARCHAR(64) NOT NULL DEFAULT '' COMMENT '关联单据(视频ID/提现单号/评论ID)',
is_read TINYINT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY idx_user_read (user_id, is_read, id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE reports (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL COMMENT '举报人',
target_type TINYINT NOT NULL COMMENT '1视频 2评论',
target_id BIGINT UNSIGNED NOT NULL,
reason_type TINYINT NOT NULL COMMENT '1色情低俗 2暴力血腥 3侵权 4广告营销 5其他',
remark VARCHAR(200) NOT NULL DEFAULT '',
status TINYINT NOT NULL DEFAULT 0 COMMENT '0待处理 1已处理 2已驳回',
handle_result VARCHAR(50) NOT NULL DEFAULT '' COMMENT '处理动作(offline_video/delete_comment)',
admin_id BIGINT UNSIGNED NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
handled_at DATETIME NULL,
UNIQUE KEY uk_user_target (user_id, target_type, target_id),
KEY idx_status (status, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE sensitive_words (
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
word VARCHAR(50) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_word (word)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
其余表(comments、favorites、membership_orders、user_membership、categories、transcode_tasks)结构见《01-开发文档》第 3 章,DDL 随代码工程提供。
### 3.3 索引与查询策略
| 查询场景 | 命中索引 |
|---|---|
| Feed 列表 | `videos.idx_feed(status, published_at)` + 游标分页 |
| 我的作品 | `videos.idx_user_status(user_id, status)` |
| 观看去重 | `watch_records.uk_dedup` 唯一索引(冲突即已看) |
| 点赞防重 | `likes.uk_user_video` 唯一索引,`INSERT IGNORE`/捕获 1062 |
| 积分流水翻页 | `point_transactions.idx_user_created` |
| 回调幂等 | `recharge_orders.uk_third_trade_no` |
- 计数列(like_count 等)为**冗余计数**:写路径"明细表 + 计数列 +1/-1"同事务;Redis 仅作显示缓冲时以 DB 为准定期校准
- 大字段(description)不参与列表 SELECT,列表接口只取必要列
- watch_records 增长快:按月归档(二期分区表)
### 3.4 并发与一致性设计
1. **积分变更**(统一入口 `wallet_service.change_points`):
- `SELECT ... FROM user_wallets WHERE user_id=? FOR UPDATE` 行锁
- 校验余额(扣减场景)→ 更新余额/累计字段 → 插入流水 → 提交
- 可选叠加乐观锁 version 防应用层并发
2. **免费额度 + 去重**(Redis Lua 原子脚本):
```
-- KEYS[1]=free:watch:{subject}:{date} ARGV[1]=video_id ARGV[2]=limit
if redis.call('SISMEMBER', KEYS[1], ARGV[1]) == 1 then return 1 end -- 已看过
local n = redis.call('SCARD', KEYS[1])
if n < tonumber(ARGV[2]) then redis.call('SADD', KEYS[1], ARGV[1]) return 2 end -- 免费占用
return 0 -- 额度用尽
```
3. **支付幂等**:订单状态机 `0→1` 条件更新(`UPDATE ... WHERE status=0`)+ 第三方流水号唯一索引,双保险
4. **评论奖励**:Redis 进度增减 + 发放走积分事务;删除回退以 `reward_counted` 标记防重复回退
### 3.5 Redis 详细设计
| Key | 类型 | 过期 | 说明 |
|---|---|---|---|
| `sms:code:{phone}` | String | 300s | 短信验证码;同号 60s 内禁止重发(另建 `sms:limit:{phone}`) |
| `email:code:{email}` | String | 600s | 邮箱验证码 |
| `auth:refresh:{user_id}` | String | 30d | refresh_token 白名单(支持多端+踢下线) |
| `token:blacklist:{jti}` | String | token 剩余 TTL | 登出/改密失效 |
| `watched:{subject}:{yyyyMMdd}` | Set(video_id) | 48h | 当日已看视频(推荐去重) |
| `rec:seed:{yyyyMMdd}` | String | 48h | 当日推荐随机种子 |
| `cview:{subject}:{video_id}:{yyyyMMdd}` | String | 48h | 完整观看当日去重 |
| `video:cview:{video_id}` | String(计数) | 异步落库 | 完整观看计数缓冲 |
| `ref:device:{device_id}` | String | - | 设备已注册标记(防小号绑定) |
| `ref:risk:{inviter_id}:{yyyyMMdd}` | String(计数) | 48h | 邀请人当日绑定计数(风控) |
| `invite:ip:{ip}` | ZSet(code→点击时间戳) | 24h | 推广点击埋点,按 IP 提供归因候选 |
| `invite:clk:{code}:{ip}` | String(JSON) | 24h | 点击详情(UA/时间戳),匹配时校验存在性 |
| `invite:dup:{ip}:{code}` | String | 60s | 同 IP+code 埋点去重(页面刷新不重复计数) |
| `invite:attr:{device_id}` | String(邀请码) | 7d | 首启归因结果缓存;注册时 GETDEL 式一次性消费 |
| `wd:pending:{user_id}` | String | 审核期间 | 提现在途唯一锁 |
| `free:watch:{subject}:{yyyyMMdd}` | Set | 48h | 当日免费观看集合(去重+计数) |
| `watch:reported:{subject}:{video_id}:{yyyyMMdd}` | String | 48h | watch 接口幂等(防客户端重复上报) |
| `comment:prog:{user_id}` | Hash{count,daily,date} | 7d | 奖励进度与当日已发 |
| `comment:counted:{user_id}:{video_id}` | String | 30d | 同视频首评去重 |
| `view:video:{video_id}` | String | - | 播放计数缓冲,定时任务批量落库 |
| `feed:latest` | ZSet(score=publish_ts) | - | 最新发布视频 ID 缓存,Feed 加速 |
| `video:detail:{video_id}` | String(JSON) | 5min | 视频详情缓存,状态变更时失效 |
| `search:hot` | ZSet(score=搜索次数) | - | 热搜榜,搜索接口计数,定时衰减 |
| `notif:unread:{user_id}` | String(计数) | - | 消息未读数,读/写通知时增减 |
| `words:sensitive` | String(JSON) | 60s | 敏感词库缓存(DFA 词源),后台增删后失效重建 |
| `config:system` | Hash | 60s | system_configs 全量缓存,后台修改即时失效 |
---
## 4. 接口设计(技术规范)
### 4.1 通用规范
- 风格:RESTful,版本化前缀 `/api/v1`
- 编码:UTF-8 JSON;时间格式 `YYYY-MM-DD HH:mm:ss`;金额字段人民币为**分**,USDT 为字符串(防精度丢失)
- 认证:`Authorization: Bearer <access_token>`;游客必带 `X-Device-ID`
- 统一响应:
```json
{ "code": 0, "message": "ok", "data": { }, "request_id": "uuid" }
```
- 错误码:0 成功;1xxx 通用/参数/认证;2xxx 业务(计费/积分/会员);3xxx 支付;5xxx 服务端。完整码表见《03-接口文档》附录
- 分页:Feed 用游标(`cursor=上次返回的下一游标`);管理/列表用 `page/page_size`(≤50)
- 幂等:写接口客户端带 `X-Request-ID`(或 watch 用天然键),服务端去重
- 限流:Nginx limit_req + Redis 滑动窗口双层;触发返回 429
### 4.2 认证体系(JWT)
- access_token:HS256,2h,payload `{sub:user_id, jti, exp, type:"access"}`
- refresh_token:30d,Redis 白名单存储;刷新时旋转(旧的作废)
- 登出:jti 入黑名单 + 删 refresh 白名单
- 中间件解析顺序:Bearer Token 有效 → 用户态;否则取 X-Device-ID → 游客态;都无 → 401
### 4.3 签名与回调安全
- 支付宝回调:RSA2 验签(支付宝公钥)
- 微信回调:V3 平台证书验签 + AES-GCM 解密回调资源
- USDT:无回调,服务端主动轮询 TronGrid,校验收款地址、金额(含尾差)、确认数 ≥ 19、时间窗在订单有效期内
---
### 4.4 管理后台接口规范
- 前缀 `/api/v1/admin`,与 App 接口隔离注册
- 管理员独立账号体系(admin_users 表),Token payload `type:"admin"`,有效期 12h
- 角色:super(全部)/ reviewer(仅视频审核)/ operator(套餐、活动、看板),中间件按角色鉴权
- 所有写操作异步写入 admin_logs(操作人、动作、参数、IP)
- 敏感操作(积分调整、补单、封禁、下架)要求 `reason` 必填,前端二次确认
- 建议 Nginx 层对 `/api/v1/admin` 加 IP 白名单
### 4.5 接口限流
Nginx limit_req(IP 维度)+ Redis 滑动窗口(主体维度)双层限流;触发返回 HTTP 429(业务码 1005)。
| 接口 | 限流规则 | 维度 |
|---|---|---|
| 发送验证码(短信/邮箱) | 1 条/分钟 + 5 条/小时(另有同号每天 10 条上限) | 手机号/邮箱 + IP |
| 登录 | 10 次/分钟;连续失败 5 次锁定 15 分钟 | IP |
| 观看上报 | 60 次/分钟 | 计费主体(用户/设备) |
| 发表评论 | 10 条/分钟 | 用户 |
| 上传(凭证 + 发布) | 5 次/小时 | 用户 |
| 申请提现 | 1 次/分钟 | 用户 |
| 提交举报 | 10 次/天 | 用户 |
| 搜索 | 30 次/分钟 | IP + 用户 |
| 其它写接口 | 30 次/分钟(默认兜底) | 用户 |
---
## 5. 前端设计(Flutter 技术实现)
### 5.1 工程结构(分层架构)
```
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/ # 格式化、埋点、防抖
```
### 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,同视频当日只报一次,本地记录) |
| 状态 | Riverpod:`authStateProvider`(游客/登录)、`walletProvider`、`vipProvider`、`feedProvider`(分页游标) |
| 本地持久 | device_id(首启 UUID 写 secure_storage)、Token(secure_storage)、观看已上报集合(sp,按日清理) |
| 上传 | 优先 OSS STS 直传(后端签 STS,App 分片上传,失败续传);兜底服务端 multipart |
| 支付 | 支付宝/微信调起官方 SDK(alipay_kit / fluwx 等社区插件或自建 MethodChannel);USDT 展示金额+收款地址+二维码(qr_flutter)+ 倒计时,轮询订单状态 |
| 构建 | flutter flavor 分 dev/prod;Android minSdk 21+;iOS 12+ |
| 域名发现 | 启动时经 DoH(223.5.5.5)解析 `txt.256kb.top` 的 TXT 记录获取 API 域名;失败则按 `md5(yyyyMM) + "_jsnbhh.vip"` 计算兜底域名;结果写入本地缓存,请求连续失败达阈值自动重新发现;dio BaseOptions 的 baseUrl 由该服务动态提供 |
| 设备指纹 | 首启生成 UUID(v4),Android 写 Keystore / iOS 写 Keychain 持久化(重装不丢失);dio 拦截器统一注入公共 Header `X-Device-ID` / `X-App-Version` / `X-Platform`,用于游客计费与风控 |
### 5.3 关键交互实现细节
- **游客拦截点**:观看上报返回 `FREE_LIMIT_EXCEEDED` → 全局拦截器弹登录引导(Router Guard 不做强制,保证可浏览前 10 个)
- **扣费提示**:watch 响应含 `cost_type/cost_points/remaining` → Toast 差异化提示
- **会员 UI**:`vipProvider` 驱动昵称旁金标、会员中心状态、上传"优先"标识
- **支付结果确认**:调起 SDK 返回 + 轮询 `GET /payments/orders/{order_no}`(2s×15 次),成功刷新 `walletProvider`
---
## 6. 后端设计(技术实现)
### 6.1 目录结构与依赖注入
- `api/v1/*` 只负责参数解析与响应组装;业务全在 `services/`
- 依赖注入用 FastAPI `Depends`:DB session(async)、Redis、当前身份(用户/游客)
- 配置:`pydantic-settings` 读取 `.env` + `config.yaml`(业务参数热重载预留)
- 注册赠送体验会员:注册成功事务内读取 `system_configs`(`register_gift_trial.enabled` / `register_gift_trial.days`,Redis 缓存 60s,后台修改实时生效),开启则赠送体验会员(membership_orders:plan_code=trial、source=2、points_cost=0)
### 6.2 异步任务设计(Celery)
| 队列 | 任务 | 说明 |
|---|---|---|
| transcode.high | 会员视频转码 | 优先消费 |
| transcode.normal | 普通视频转码 | |
| default | 播放量落库、超时关单、通知 | 轻任务 |
- Broker/Backend 均用 Redis;任务幂等(转码以 video_id 状态机推进)
- 超时关单:每分钟扫 `recharge_orders status=0 AND expire_at<now` → 关闭
### 6.3 FFmpeg 转码实现
```bash
# 720p HLS,4s 分片,快start优化
ffmpeg -i input.mp4 \
-vf "scale=-2:720" -c:v libx264 -preset veryfast -crf 23 \
-c:a aac -b:a 128k \
-hls_time 4 -hls_playlist_type vod \
-hls_segment_filename "out/%03d.ts" out/index.m3u8
# 封面
ffmpeg -ss 00:00:01 -i input.mp4 -frames:v 1 -q:v 2 out/cover.jpg
```
- 转码产出目录:`oss://bucket/hls/{video_id}/`
- 多码率(可选):生成 master.m3u8 引用各档 variant
- 失败处理:捕获非零退出码 → retry_count+1(指数退避),>3 次置失败
### 6.4 推荐与标签服务实现
- Feed 接口参数 `type=recommend|latest|hot`,默认 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(标签名全局唯一、大小写不敏感);标签视频页按热度或最新排序
### 6.5 支付适配层
```
payment/
├── base.py # PayAdapter 抽象:create_order / verify_callback / parse_callback
├── alipay.py # alipay-sdk-python:app_pay 下单、RSA2 验签
├── wechat.py # wechatpay-python:V3 下单、平台证书验签解密
└── usdt.py # tronpy+TronGrid:生成唯一金额、轮询匹配、确认数判定
```
- 三渠道产出统一的 `PaymentResult(order_no, third_trade_no, paid_amount)` 交给同一入账服务 `settle_service`(事务幂等入账)
- USDT 尾差策略:`base_amount + random(0.001,0.999)`,有效期内金额唯一;轮询间隔 15s
- 充值档位与赠送积分从 `recharge_packages` 表读取(管理后台实时可配);自定义金额按 `points_per_yuan`(默认 10)换算
- **退款流程**:商户后台(支付宝/微信)人工发起退款 → 服务端接收退款回调或运营登记 → 订单状态置 `3已退款` → 事务内扣回已发放积分(余额允许为负,后续充值抵扣)→ 若该订单产生过推荐返利,按原记录扣回推荐人资产(biz_type=6,资产允许为负)→ 全程写积分/资产流水与 admin_logs
### 6.6 激励结算与防作弊
Celery 周期任务(default 队列):
| 任务 | 频率 | 逻辑 |
|---|---|---|
| referral_activate_scan | 每 10 分钟 | 扫描 72h 内"待激活"绑定:满足观看 ≥3 个视频且次日有登录 → 置"有效"并发放邀请积分(积分事务,biz_type=8,校验推荐人为会员);超期未达标置"无效" |
| video_earning_settle | 每 5 分钟 | 遍历 video_earnings:`新增完整观看 = complete_views - views_settled`,每满 1000 发 1 资产;`新增点赞 = likes_accrued - likes_settled`,每满 100 发 1 资产;**结算时点校验作者为会员**,是则发资产(资产事务+流水)并推进 settled 刻度,否则仅保留计数 |
| referral_rebate | 事件触发 | 支付入账成功后检查订单用户是否存在"有效"推荐关系 → 推荐人为会员则发放"充值积分 × 5%"资产(异步补偿任务,不阻塞支付主流程);退款时按原返利记录扣回(biz_type=6,资产允许为负) |
关键实现:
- 完整观看计数先入 Redis(`video:cview:{video_id}`),随播放量落库任务批量入库后结算
- 资产钱包与积分钱包同一套"行锁 + 流水 + 乐观锁"变更规范(asset_transactions 记录 balance_after)
- 提现申请:事务内冻结扣减 → 生成提现单 + Redis 在途锁(`wd:pending:{user_id}`);后台拒绝时退回并释放锁
- 风控埋点:注册/绑定时记录 device_id 与 IP;命中 same_device、same_ip、batch_register、inviter_burst 规则 → referrals.status=3 冻结,仅人工审核后可发放
**推广归因(三段式自建方案,invite_service)**:
1. **分享口令(主路径)**:推广文案含 `#短视频邀请码XXXXXXXX#`;App 首启解析剪贴板正则提取,本地持久化 pending 邀请码并在注册页预填,准确率 100%
2. **H5 点击埋点 + 模糊匹配(辅助路径)**:`/r/{code}` 落地页加载即调 `POST /invite/click`(写入 `invite:ip:{ip}` zset,score=时间戳,24h TTL;`invite:dup` 60s 去重);App 首启无口令时调 `POST /invite/match` 上报 device_id,服务端按 IP 取最近点击(最多 5 个候选,就近优先)匹配,命中缓存 `invite:attr:{device_id}`(7 天)
3. **注册手填(兜底)**:注册接口 `invite_code` 选填;注册后 24h 内可经 `/referral/bind` 补绑
4. **安全设计**:归因结果仅存服务端,注册时 `pop_attribution` 一次性消费(读后即删),客户端无法伪造归因;绑定优先级 **显式 invite_code(口令/手填)> 模糊归因缓存**;进入绑定后复用推荐风控全部规则
### 6.7 内容安全与敏感词
- 采用 **DFA(确定性有穷自动机)** 算法做敏感词匹配:启动时从 `sensitive_words` 表加载全量词库构建 DFA 树,词库存入 Redis(`words:sensitive`,60s 缓存);后台增删词后缓存失效重建,实时生效
- 应用范围:评论内容、用户昵称、视频标题/描述/标签、搜索关键词
- 命中处理:写接口命中敏感词 → 拒绝并返回错误码 **2105**;搜索词命中 → 返回空结果并记录日志
- 词库来源:合规基础词库初始化 + 运营后台持续维护(`uk_word` 唯一索引去重)
### 6.8 日志与监控
- 结构化日志(JSON):request_id 贯穿;慢查询 >500ms 告警
- 指标:Prometheus(QPS、转码队列长度、支付成功率、积分发放量)
- 关键业务告警:回调验签失败、入账失败、转码失败率、USDT 超时未匹配
### 6.9 测试与质量
- pytest + httpx AsyncClient:核心用例覆盖计费判定矩阵(会员/免费/去重/扣费/不足/游客超限)
- 支付回调:构造签名报文做幂等与篡改用例
- 积分服务:并发扣费压测(locust)验证无负余额、无重复扣
### 6.10 部署与配置
- `.env`:DB/Redis 连接、JWT 密钥、支付商户密钥、OSS 密钥(严禁入库/入 git)
- `config.yaml`:业务参数(积分/会员/套餐/上传限制),见《01-开发文档》6.4
- CI/CD:GitHub Actions(lint → pytest → build image → deploy)
- 备份:MySQL 每日全量 + binlog 增量;OSS 跨区复制(可选)
- **兜底域名与证书**:`jsnbhh.vip` 系列兜底域名配置 DNS 泛解析 + 通配符证书(`*.jsnbhh.vip`),接入 CDN 回源;主域名失效时 App 按域名发现策略自动切换(见 5.2)