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

13 KiB
Raw Permalink Blame History

生产部署手册: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. 服务器初始化

# 系统要求: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. 拉代码 + 数据库

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 后按下表修改。带 ⚠️ 的行漏配会导致安全事故或拒启动。

# ── 环境(最关键: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. 迁移 + 内测配额口径

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)

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

# 7.1 签证书(自动改 nginx 配置 + 配置续期)
sudo certbot --nginx -d elane.xiangsilian.com

# 7.2 编辑 /etc/nginx/sites-enabled/ 下 elane 的 server 块,追加反代:
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)
}
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 签名出包