Files
video/docs/02-技术文档.md
2026-10-18 21:08:54 +08:00

44 KiB
Raw Permalink Blame History

短视频应用技术文档

  • 版本: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 节点服务)  │
└───────────────────────────────────────────────────────────┘

客户端层补充: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)

容器 镜像 说明
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 机器或云转码服务。

当前生产形态(与上表演进关系):单台云主机。后端由 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 将网络 m3u8 地址改写为内嵌本地代理(127.0.0.1)→ 代理回源取 m3u8(签名时效)→ ts 分片命中本地缓存直出,未命中直连源站 CDN 下载并落盘(1GB LRU)→ 全程支持 Range/206 拖动
  • 上传链路: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

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;
-- 标签
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(赠送天数)
-- 激励域:推荐关系 / 资产钱包 / 资产流水 / 提现 / 视频收益
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;
-- 通用域:通知 / 举报 / 敏感词
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 白名单(支持多端+踢下线)
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 当日推荐随机种子
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 全量缓存,后台修改即时失效

连接注意事项:Redis 客户端必须显式指定 protocol=2(RESP2)。线上服务端为 Redis 5.x,不支持 RESP3 的 HELLO 握手,而 redis-py 8.x 默认尝试 protocol=3,不显式指定会在连接期直接报错——这是踩过的坑(见 server/app/redis_client.py)。


4. 接口设计(技术规范)

4.1 通用规范

  • 风格:RESTful,版本化前缀 /api/v1
  • 编码:UTF-8 JSON;时间格式 YYYY-MM-DD HH:mm:ss;金额字段人民币为分,USDT 为字符串(防精度丢失)
  • 认证:Authorization: Bearer <access_token>;游客必带 X-Device-ID
  • 统一响应:
{ "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
├── 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 防并发刷新);无 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
支付 支付宝/微信调起官方 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 转码实现

# 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|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) / (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 支付适配层

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)

发版流水线(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 各启动一份,任务逻辑必须保持幂等(状态机推进 + 唯一约束),新增周期任务同样遵守