Files
meida/docs/生产部署手册-elane.md

282 lines
13 KiB
Markdown
Raw Permalink 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.
# 生产部署手册:elane.xiangsilian.com
> 创建日期:2026-09-14 | 适用:meida_backend 首次公网部署
> 目标服务器:elane.xiangsilian.com → 175.178.100.240(腾讯云 Ubuntu,已有 nginx)
> 前置:代码 review 已完成(12 项结论全部核实),本文所有命令均按当前代码核实过。
---
## 0. 部署前必读(三个硬提醒)
1. **⚠️ 华为登录还差一步 AGC 配置。** 登录系统已开发完成:邮箱验证码(前后端全
链路可用)+ 华为账号登录(端侧已接入,后端验签早就绪)。唯一缺口:AGC 开通
Account Kit 后把 **client_id** 填进 App 的 `entry/src/main/module.json5`
(替换 `TODO_AGC_CLIENT_ID` 占位值),并确保 AGC 上的包名/证书指纹与本机
签名一致。在此之前华为登录按钮会提示「暂未开放」,邮箱登录可正常使用。
2. **⚠️ 备案**:`elane.xiangsilian.com` 是现有备案下**新增的子域名**。需在腾讯云
控制台「备案 → 变更备案」中提交变更(主体不变、免费、线上办理、约 1~2 周)。
部署不依赖备案完成,但华为应用市场审核会核对。
3. **⚠️ 服务器安全组**:放行 443;**不要**对公网开放 55432/56379(数据库端口)。
## 1. 目标架构
```
App ──https──▶ elane.xiangsilian.com:443 (nginx + Let's Encrypt 证书)
└─ /api/v1/ ──▶ 127.0.0.1:8666 (gunicorn/uvicorn)
└─ /healthz、/readyz ──▶ 127.0.0.1:8666(监控用,可只内网)
本机进程:PostgreSQL 16+pgvector(docker, 127.0.0.1:55432)
Redis 7(docker, 127.0.0.1:56379)
celery worker -Q cpu,io(systemd)
数据卷: pgdata / redisdata(docker 卷)+ /home/ubuntu/meida_data/{uploads,exports}
```
说明:本部署为**单进程 api + 单 worker**(当前业务全部走 cpu/io 队列,gpu 队列
尚未启用,见 review §10)。不上 K8s,与架构文档 §14.1 的 V1 形态一致。
## 2. 服务器初始化
```bash
# 系统要求:Ubuntu 22.04+,2C4G 起步(报告生成是 LLM IO 密集型,不吃 CPU)
# 2.1 Docker(如未装)
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER && newgrp docker
# 2.2 uv(Python 包管理,项目要求 Python >= 3.12)
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
# 2.3 weasyprint 系统依赖(PDF 导出用,缺了首次导出必炸——review 重点项)
sudo apt-get update && sudo apt-get install -y \
libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz-subset0
# 2.4 nginx + certbot(机器上已有 nginx 则跳过安装,直接进 §5)
sudo apt-get install -y nginx certbot python3-certbot-nginx
```
## 3. 拉代码 + 数据库
```bash
git clone https://git.xiangsilian.com/xsl/meida_backend.git ~/meida_backend
cd ~/meida_backend
# 3.1 改数据库口令:编辑 deploy/docker-compose.dev.yml
# POSTGRES_PASSWORD: meida → 换成强口令(记为 <PG_PASS>)
# 并把两个端口映射绑到本机(防公网暴露):
# "55432:5432" → "127.0.0.1:55432:5432"
# "56379:6379" → "127.0.0.1:56379:6379"
# 3.2 起 PG + Redis(只起数据层,不起应用)
docker compose -f deploy/docker-compose.dev.yml up -d postgres redis
# 3.3 应用依赖(生产:不装 dev 依赖、锁死版本)
uv sync --no-dev --frozen
```
## 4. 生产 .env
`cp .env.example .env` 后按下表修改。**带 ⚠️ 的行漏配会导致安全事故或拒启动**。
```bash
# ── 环境(最关键:prod 自动关闭 dev-token 登录和 /api/v1/docs)──
APP_ENV=prod # ⚠️ 不设这行 = dev-token 对外敞开
LOG_LEVEL=INFO
LOG_JSON=true
# ── 数据依赖(默认值指向 localhost:55432/56379,口令与 3.1 一致)──
DATABASE_URL=postgresql+asyncpg://meida:<PG_PASS>@127.0.0.1:55432/meida
REDIS_URL=redis://127.0.0.1:56379/0
CELERY_BROKER_URL=redis://127.0.0.1:56379/1
CELERY_RESULT_BACKEND=redis://127.0.0.1:56379/2
# ── 鉴权 ──
JWT_SECRET=<执行 openssl rand -hex 32 生成> # ⚠️ <32 字符或占位符会拒启动(有意保护)
DEV_AUTH_ENABLED=false # 双保险(APP_ENV=prod 本身已关)
# ── LLM(DeepSeek 主 / 智谱备,key 从开发机 .env 抄)──
DEEPSEEK_API_KEY=sk-...
ZHIPU_API_KEY=...
# LLM_DAILY_BUDGET_CENTS=100000 # ⚠️ 当前是占位值(¥1000/天),按实测成本重算后再定
# ── 存储:无 COS 走本地磁盘降级(当前形态)──
# COS_* 全部留空;上传走 POST /api/v1/uploads/direct
UPLOAD_DIR=/home/ubuntu/meida_data/uploads # ⚠️ 绝对路径,相对 cwd 在 systemd 下不可靠
REPORT_EXPORT_DIR=/home/ubuntu/meida_data/exports
# ── 登录通道 ──
# 华为账号(后端已支持;HUAWEI_AUDIENCE 不配则华为登录 40102,是有意设计)
# HUAWEI_AUDIENCE=6917613748100335131
# HUAWEI_AUDIENCE 对应 AGC 应用的 App ID(前置开通清单已登记)
#
# 邮箱验证码(正式兜底通道,前后端已实现,2026-09-14):
# ⚠️ EMAIL_PROVIDER=fake 会把验证码明文打进日志,仅限本地开发,生产必须 smtp
EMAIL_PROVIDER=smtp
SMTP_HOST=smtp.qq.com # 或 smtp.163.com
SMTP_PORT=465
SMTP_USER=your_sender@qq.com
SMTP_PASS=<邮箱 SMTP 授权码> # QQ/163 邮箱设置里生成,不是登录密码
SMTP_USE_SSL=true
MAIL_FROM=your_sender@qq.com # 留空则用 SMTP_USER
# 频控默认值(对齐短信口径):同邮箱 60s 冷却 / 1h≤5 / 24h≤10,IP 24h≤20,设备 24h≤10
#
# 短信通道:已废弃(个人主体办不下签名),SMS_* 保持默认不启用即可
```
> 注:短信通道已废弃不做。`SMS_PROVIDER` 保持默认 fake 即可——短信端点未接路由、
> App 端无入口,永远不会被触发;但生产日志审计时留意别让任何人把它接到路由上。
## 5. 迁移 + 内测配额口径
```bash
cd ~/meida_backend
# 5.1 迁移(alembic 走 DATABASE_URL;0003 会自动灌 bl_quota_rule 种子,
# 含 full_report 行——全新库不需要手动补)
uv run alembic upgrade head
# 5.2 (可选,内测期放宽;上架前必须改回)开发库当前口径:
docker exec meida-postgres psql -U meida -d meida -c \
"UPDATE bl_quota_rule SET base_limit=base_limit*10, task_bonus_max=task_bonus_max*10, hard_cap=hard_cap*10;"
```
## 6. 进程常驻(systemd)
```bash
sudo tee /etc/systemd/system/meida-api.service <<'EOF'
[Unit]
Description=meida api
After=network-online.target docker.service
Wants=network-online.target
[Service]
User=ubuntu
WorkingDirectory=/home/ubuntu/meida_backend
Environment=PATH=/home/ubuntu/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
Environment=PYTHONUNBUFFERED=1
ExecStart=/home/ubuntu/.local/bin/uv run --no-sync gunicorn apps.api.main:app \
--worker-class uvicorn.workers.UvicornWorker --workers 2 \
--bind 127.0.0.1:8666 --timeout 120 --graceful-timeout 30 --access-logfile -
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
sudo tee /etc/systemd/system/meida-worker.service <<'EOF'
[Unit]
Description=meida celery worker (cpu,io)
After=network-online.target docker.service
Wants=network-online.target
[Service]
User=ubuntu
WorkingDirectory=/home/ubuntu/meida_backend
Environment=PATH=/home/ubuntu/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
Environment=PYTHONUNBUFFERED=1
ExecStart=/home/ubuntu/.local/bin/uv run --no-sync celery -A apps.worker.celery_app worker \
-Q cpu,io --pool=solo --loglevel=INFO
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now meida-api meida-worker
# 验证(本机)
curl -s http://127.0.0.1:8666/healthz # {"code":0,...,"status":"ok"}
curl -s http://127.0.0.1:8666/readyz # 依赖就绪(查 PG+Redis)
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://127.0.0.1:8666/api/v1/auth/dev-token \
-H 'Content-Type: application/json' -d '{"device_key":"x"}' # 必须是 404(dev-token 已关)
```
要点:
- `--no-sync` 防止 systemd 环境下 uv 试图联网解析依赖;依赖已在 §3.3 装好。
- 不需要 celery beat(全仓库无定时任务;temp 目录清理等上线后再说,见 §8)。
- worker 与 api 的 UPLOAD_DIR/REPORT_EXPORT_DIR 是同一绝对路径,天然共享,无需挂卷。
## 7. nginx + HTTPS
```bash
# 7.1 签证书(自动改 nginx 配置 + 配置续期)
sudo certbot --nginx -d elane.xiangsilian.com
# 7.2 编辑 /etc/nginx/sites-enabled/ 下 elane 的 server 块,追加反代:
```
```nginx
server {
listen 443 ssl;
server_name elane.xiangsilian.com;
# certbot 自动填 ssl_certificate / ssl_certificate_key
location /api/v1/ {
proxy_pass http://127.0.0.1:8666;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto https;
client_max_body_size 20m; # 上传上限与 UPLOAD_MAX_BYTES 对齐
proxy_read_timeout 120s; # 报告生成等长请求
}
# 监控探活(可选;不配则只在服务器本机 curl)
location = /healthz { proxy_pass http://127.0.0.1:8666/healthz; }
location = /readyz { proxy_pass http://127.0.0.1:8666/readyz; }
# /metrics 不要暴露公网(代码里它不受 PROMETHEUS_ENABLED 控制,见 §8)
}
```
```bash
sudo nginx -t && sudo systemctl reload nginx
# 7.3 安全组:腾讯云控制台放行 443/tcp;确认 55432/56379/8666 未对公网开放
# 7.4 公网验证
curl -s https://elane.xiangsilian.com/healthz
curl -s -o /dev/null -w '%{http_code}\n' https://elane.xiangsilian.com/api/v1/quota # 401 = 服务活了
curl -s -o /dev/null -w '%{http_code}\n' https://elane.xiangsilian.com/api/v1/docs # 404 = swagger 已关
```
## 8. 已知限制与待办(代码 review 结论,部署后按此跟进)
| # | 事项 | 处置 |
|---|---|---|
| 1 | **华为登录差 client_id**:端侧已接入(占位拦截不 crash),需 AGC 开通 Account Kit 后填 `module.json5` | 用户在 AGC 操作(前置开通清单 §1),拿到 client_id 替换 `TODO_AGC_CLIENT_ID` |
| 2 | ~~SMS_PROVIDER 默认 fake~~ 短信已废弃;邮箱通道已上线 | 生产 `.env` 用 `EMAIL_PROVIDER=smtp` + 真实邮箱授权码 |
| 3 | CORS 中间件不存在 | 原生 App 不受影响;admin 后台浏览器访问前必须加白名单 |
| 4 | COS temp 桶 2 小时生命周期 | 未接 COS 前, uploads 目录只增不减,需 cron 清理:如 `find /home/ubuntu/meida_data/uploads -type f -mtime +2 -delete`(temp 场景文件 2 小时后可删) |
| 5 | 数据备份脚本缺失(FR-SYS-09) | 最低限度:`pg_dump` 每日 cron + `meida_data/` 快照 |
| 6 | `LLM_DAILY_BUDGET_CENTS` 是占位值 | 用真实 token 成本重算后改 .env |
| 7 | `/metrics` 无条件暴露 | 已按 §7 不反代公网;要监控可后期加 IP 白名单 |
| 8 | DEBUG 配置项是死代码 | 无行为影响,忽略即可 |
| 9 | 本地上传的照片无对外 URL(已知缺口) | 报告场景服务端自用不受影响;需回显时接 COS |
| 10 | 运营后台(行为分析)需设 `ADMIN_TOKEN` 才装配 | 端点 `/admin/api/v1/users`、`/users/{id}`、`/events`、`/stats/overview`(X-Admin-Token 鉴权,PII 掩码后输出);admin 是独立应用需单独进程:`uv run uvicorn "apps.admin.app:create_admin_app" --host 127.0.0.1 --port 8001`,nginx 按需反代(注意先解决 #3 CORS) |
## 9. 故障速查
| 症状 | 排查 |
|---|---|
| `systemctl status meida-api` 启动即失败 | 多半是 JWT_SECRET 太短/占位符(config.py 启动校验)——`journalctl -u meida-api -n 50` 看原因 |
| readyz 非 2xx | PG/Redis 没起:`docker compose -f deploy/docker-compose.dev.yml ps` |
| 报告卡在生成中 | 看 worker:`journalctl -u meida-worker -f`;LLM key 失效是最常见原因 |
| PDF 导出失败报 weasyprint 相关错 | §2.3 的系统库没装 |
| nginx 502 | api 没起 / 8666 端口不对;`ss -ltnp \| grep 8666` |
| 华为登录 40102 | HUAWEI_AUDIENCE 没配(预期行为,配 AGC App ID) |
## 10. 上线前最终检查单
- [ ] 变更备案通过(elane.xiangsilian.com 在备案系统可见)
- [ ] dev-token 返回 404、docs 返回 404、metrics 不公网可达
- [ ] JWT_SECRET / PG 口令均为强随机值,`.env` 权限 `chmod 600`
- [ ] 华为登录真机回归通过(client_id 已填入 module.json5、AGC 指纹与签名一致)
- [ ] 邮箱验证码登录真机回归通过(生产 EMAIL_PROVIDER=smtp 实测收到邮件)
- [ ] 账号注销功能可用(华为审核硬性要求)
- [ ] 隐私政策/用户协议可公开访问,App 内有入口
- [ ] 配额规则从内测口径改回产品口径(若执行过 §5.2)
- [ ] pg_dump 定时备份已配并演练过一次恢复
- [ ] App `DEFAULT_BASE_URL` 切换为 `https://elane.xiangsilian.com`,Release 签名出包