350 lines
17 KiB
Markdown
350 lines
17 KiB
Markdown
# 04-部署运行文档
|
||
|
||
> 本文记录**真实生产环境**的部署架构与运维操作手册(截至 2026-10-18)。
|
||
> 服务器:腾讯云 CVM(14G 内存),已装宝塔面板;系统 Linux。
|
||
> 项目根目录:`/root/project/video`,三个业务域名均启用 HTTPS。
|
||
|
||
## 1. 总体架构
|
||
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
用户浏览器/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
|
||
└─────────────────────────────────────────┘
|
||
|
||
视频 ts 分片流量:App → 直连源站 CDN(不经过本服务器,见 02-技术文档)
|
||
```
|
||
|
||
| 域名 | 用途 | 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 容器 |
|
||
|
||
证书: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
|
||
# 日常连接
|
||
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
|
||
```
|
||
|
||
### 3.2 Redis(容器名 my-redis)
|
||
|
||
- 镜像 `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
|
||
```
|
||
|
||
## 4. 后端服务(video-api.service)
|
||
|
||
### 4.1 systemd 单元(/etc/systemd/system/video-api.service)
|
||
|
||
```ini
|
||
[Unit]
|
||
Description=Video App FastAPI Backend (uvicorn)
|
||
After=network.target docker.service redis.service
|
||
Requires=docker.service
|
||
|
||
[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
|
||
```
|
||
|
||
- 只监听 **127.0.0.1:8000**,对外全靠 nginx 反代
|
||
- `--workers 2`:⚠️ 结算周期任务(推荐激活/创作收益/超时关单)随每个 worker 的 lifespan 各启动一份(`app/services/settle_service.py`)。当前任务均为条件更新、重复执行无副作用;**修改周期任务逻辑时必须保证幂等**
|
||
- 日志走 journald:`journalctl -u video-api -f`
|
||
|
||
常用命令:
|
||
|
||
```bash
|
||
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
|
||
from app.config import settings
|
||
from app.core.security import hash_password
|
||
from app.models.admin import AdminUser
|
||
|
||
async def main():
|
||
engine = create_async_engine(settings.database_url)
|
||
async with async_sessionmaker(engine)() as s:
|
||
s.add(AdminUser(username="admin",
|
||
password_hash=hash_password("请设置强密码"),
|
||
name="超管", role="super"))
|
||
await s.commit()
|
||
await engine.dispose()
|
||
|
||
asyncio.run(main())
|
||
EOF
|
||
```
|
||
|
||
后台角色:`super`(全部权限)、`operator`(仪表盘/套餐/赠送)、`reviewer`(视频审核)。
|
||
|
||
### 4.4 服务暴露路径(反代依据)
|
||
|
||
| 路径 | 说明 |
|
||
|---|---|
|
||
| `/api/v1/*` | App 业务接口(9 个路由文件 + admin,共 100+ 端点) |
|
||
| `/api/v1/admin/*` | 管理后台接口 |
|
||
| `/r/{code}` | 邀请落地页 |
|
||
| `/static/*` | 本地媒体文件 |
|
||
| `/health` | 健康检查 |
|
||
|
||
## 5. 管理后台发布(admin/)
|
||
|
||
```bash
|
||
cd /root/project/video/admin
|
||
npm ci
|
||
npm run build # 产物直接覆盖 admin/dist/,即 vm 站点根目录
|
||
```
|
||
|
||
- 纯静态站点,**构建即发布,无需重启任何服务**;浏览器强刷(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
|
||
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; }
|
||
```
|
||
|
||
**vm.256kb.top**
|
||
|
||
```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; }
|
||
```
|
||
|
||
**vapi.256kb.top**:`location / { proxy_pass http://127.0.0.1:8000; }` 全量反代。
|
||
|
||
站点日志在 `/www/wwwlogs/<域名>.log`(排查 App 接口 401/500 时非常管用)。
|
||
|
||
## 7. App 编译与发版
|
||
|
||
### 7.1 域名发现(不硬编码 API 地址)
|
||
|
||
启动时优先级:`--dart-define=API_BASE` > 本地缓存 > DNS TXT(`txt.256kb.top`) > 兜底域名(md5(yyyyMM)+后缀)。
|
||
正式包**不带 dart-define 直接构建**,走 TXT 发现(当前指向 `vapi.256kb.top/api/v1`)。
|
||
|
||
### 7.2 一键发版 deploy.sh
|
||
|
||
```bash
|
||
# 1. 改 app/pubspec.yaml 的 version(如 1.3.7+23,+后为 versionCode)
|
||
# 2. 执行
|
||
bash /root/project/video/deploy.sh
|
||
```
|
||
|
||
脚本自动完成:`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` 调大于用户版本。
|
||
|
||
### 7.3 正式签名(2026-10-17 起,v1.3.7+23 为首个正式签名版)
|
||
|
||
| 项 | 值 |
|
||
|---|---|
|
||
| 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) |
|
||
|
||
`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
|
||
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 # 服务/容器/接口探测
|
||
```
|
||
|
||
- 管理范围:`video-api.service` + `mysql8` + `my-redis`;**不含 nginx**(系统组件)
|
||
- 服务器重启后如项目处于"运行态",docker 与 systemd 会自动拉起全部组件,无需人工干预
|
||
- 项目当前(2026-10-18 起)处于**暂停态**,恢复运行执行 `start` 即可
|
||
|
||
## 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 验证清单
|