Files
backend/docs/10-生产部署文档.md
T
34047007@qq.com a6cd99a4ca
CI / backend (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
feat: initial commit - oncology literature search platform
OncoLit: a multi-tenant oncology literature search, feed, and
collaboration platform. Built with FastAPI + Vue 3 + PostgreSQL.
Includes PubMed pipeline, drug approvals, AI summaries, and
systematic review tools.
2026-07-27 07:59:18 +08:00

1245 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.
# SciLit Oncology — 生产部署文档
## 目录
1. [前置准备](#1-前置准备)
2. [服务器基础环境](#2-服务器基础环境)
3. [中间件安装](#3-中间件安装)
4. [域名与SSL](#4-域名与ssl)
5. [应用部署](#5-应用部署)
6. [数据库初始化](#6-数据库初始化)
7. [第三方服务注册](#7-第三方服务注册)
8. [上线验证清单](#8-上线验证清单)
9. [日常运维](#9-日常运维)
10. [应急预案](#10-应急预案)
---
## 1. 前置准备
### 1.1 你需要准备好的东西
| 项目 | 说明 | 在哪里获取 |
|------|------|----------|
| ☁️ 服务器 | Linux (Ubuntu 22.04 推荐), 2C4G 起步,50G SSD | 阿里云/腾讯云/AWS |
| 🔑 SSH 密钥 | 用于免密登录服务器 | `ssh-keygen` 本地生成,公钥上传到云控制台 |
| 🌐 域名 | `oncolit.gonsun.com` 或你的域名 | 阿里云/腾讯云/Namecheap |
| 📧 邮箱 | 用于 SMTP 发件和系统通知 | 阿里云邮件推送 / SendGrid |
### 1.2 本地先确认项目能跑
```bash
cd d:/ClaudeCode/backend
python scripts/seed_data.py
uvicorn app.main:app --port 8000
cd d:/ClaudeCode/frontend
npm run build
```
---
## 2. 服务器基础环境
### 2.1 登录服务器
```bash
ssh root@<你的服务器IP>
# 创建非 root 用户
adduser scilit
usermod -aG sudo scilit
su - scilit
```
### 2.2 更新系统 + 安装基础工具
```bash
sudo apt update && sudo apt upgrade -y
sudo apt install -y curl wget git vim htop net-tools ufw
# 防火墙:只开放需要的端口
sudo ufw allow 22 # SSH
sudo ufw allow 80 # HTTP
sudo ufw allow 443 # HTTPS
sudo ufw enable
```
### 2.3 安装 Docker
```bash
curl -fsSL https://get.docker.com | sudo bash
sudo usermod -aG docker $USER
newgrp docker
# 安装 Docker Compose
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
# 验证
docker --version
docker-compose --version
```
### 2.4 安装 Nginx(宿主机,用于反向代理和 SSL)
```bash
sudo apt install -y nginx certbot python3-certbot-nginx
```
---
## 3. 中间件安装
### 3.1 方案选择
| 方案 | 适用场景 | 运维成本 |
|------|---------|:--:|
| **A: 阿里云 RDS + Redis + ES** | 有预算,想省运维 | 低 |
| **B: Docker Compose 自建全部** | 小规模起步,成本敏感 | 中 |
下面按**方案 B(自建)**编写。方案 A 请参考各云服务商的快速入门文档。
### 3.2 PostgreSQL(自建,使用 pgvector 镜像支持未来向量搜索)
Docker Compose 中已定义,无需额外操作:
```yaml
# docker-compose.prod.yml 中
postgres:
image: pgvector/pgvector:pg16
environment:
POSTGRES_DB: scilit
POSTGRES_USER: scilit
POSTGRES_PASSWORD: ${PG_PASSWORD} # 在 .env 中设置
volumes:
- /data/postgres:/var/lib/postgresql/data # 持久化到宿主机
restart: unless-stopped
```
### 3.3 Redis(自建)
```yaml
redis:
image: redis:7-alpine
command: redis-server --appendonly yes # 开启持久化
volumes:
- /data/redis:/data
restart: unless-stopped
```
### 3.4 Elasticsearch(可选,搜索增强)
```yaml
elasticsearch:
image: elasticsearch:8.11.0
environment:
discovery.type: single-node
xpack.security.enabled: "false"
"ES_JAVA_OPTS": "-Xms512m -Xmx512m"
bootstrap.memory_lock: "true"
volumes:
- /data/es:/usr/share/elasticsearch/data
restart: unless-stopped
ulimits:
memlock: { soft: -1, hard: -1 }
```
> 小规模起步可以先不启 ES(`ES_URL` 为空时系统自动回退 PostgreSQL tsvector 搜索),等数据量达到 200-300 万条再启。启动方式:取消注释 docker-compose 中的 `elasticsearch` 服务段,然后运行 `python scripts/es_index.py` 全量重建索引。
### 3.5 MinIO(对象存储,存储用户文件 + 文献 PDF)
MinIO 是兼容 AWS S3 协议的自托管对象存储,用于存储用户头像、上传文件以及未来的文献 PDF 原文。
```yaml
minio:
image: minio/minio:latest
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: minioadmin # 在 .env 中用 MINIO_ROOT_USER 覆盖
MINIO_ROOT_PASSWORD: minioadmin # 在 .env 中用 MINIO_ROOT_PASSWORD 覆盖
volumes:
- /data/minio:/data
restart: unless-stopped
```
> MinIO Web 控制台端口为 9001API 端口为 9000。生产环境不建议对外暴露端口,通过后端代理访问。首次部署后登录控制台创建 `scilit-files` 桶(或后端自动创建)。
#### MinIO vs 阿里云 OSS 对比
| | MinIO(自建) | 阿里云 OSS |
|--|:--:|:--:|
| 成本 | 仅占磁盘空间 | ¥0.12/GB/月 + 流量费 |
| 延迟 | ~0.1ms(内网) | 2-5ms |
| 运维 | 需管理磁盘 | 托管 |
| 带宽 | 受服务器带宽限制 | 不限速 |
| 适用场景 | 小规模起步、内部使用 | 大规模、需 CDN 分发 |
> **决策记录(2026-07-09):** 先用 MinIO 自托。主要考量:用户量初期不大、PDF 存储主要为内部解析 + 用户自行下载(需 VPN),MinIO 内网延迟更低,省云费用。
### 3.5 关于 Elasticsearch
docker-compose.prod.yml 中已配置 ES 8.11.0 容器,但**生产环境默认不启用**(`ES_URL` 为空时系统自动回退 PostgreSQL tsvector 搜索)。如需启用,在 `.env` 中设置 `ES_URL=http://elasticsearch:9200` 即可。
> ES 服务至少需要 2GB 内存(JVM heap 512MB-1GB),小规模部署(<200 万条文献)不需要 ES。
---
## 4. 域名与SSL
### 4.1 DNS 解析
在域名服务商后台添加 A 记录:
```
类型 主机记录 记录值
A @ 你的服务器IP
A www 你的服务器IP
```
### 4.2 Nginx 配置
**方案 A(推荐):前端 Nginx 容器**`docker-compose.prod.yml``frontend` 服务使用 `Dockerfile.prod`(基于 Nginx),自动处理静态文件托管 + API 反向代理。无需宿主机 Nginx。
```bash
# docker-compose.prod.yml 启动后,frontend 容器监听 80 端口
# 宿主机只需安装 Nginx 做 SSL 终止和域名转发:
sudo apt install -y nginx certbot python3-certbot-nginx
```
```nginx
# /etc/nginx/sites-available/scilit-oncology
server {
listen 80;
server_name oncolit.gonsun.com www.oncolit.gonsun.com;
location / {
proxy_pass http://127.0.0.1:80; # 转发到 frontend Nginx 容器
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
**方案 B(不推荐):宿主机 Nginx 直接托管前端文件**
```bash
sudo vim /etc/nginx/sites-available/scilit-oncology
```
```nginx
# /etc/nginx/sites-available/scilit-oncology
server {
listen 80;
server_name oncolit.gonsun.com www.oncolit.gonsun.com;
# 前端静态文件
location / {
root /home/scilit/sci-lit-manager/frontend/dist;
try_files $uri $uri/ /index.html;
}
# API 反代到 Docker 容器内的 FastAPI
location /api/ {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 120s;
}
# WebSocket
location /api/v1/ws/ {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
# Swagger 文档
location /docs {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
}
# 静态资源缓存
location /assets/ {
root /home/scilit/sci-lit-manager/frontend/dist;
expires 30d;
add_header Cache-Control "public, immutable";
}
}
```
```bash
# 启用站点
sudo ln -s /etc/nginx/sites-available/scilit-oncology /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
```
### 4.3 SSL 证书(Let's Encrypt 免费)
```bash
sudo certbot --nginx -d oncolit.gonsun.com -d www.oncolit.gonsun.com
# 按提示输入邮箱,同意条款
# 设置自动续期
sudo certbot renew --dry-run
```
---
## 5. 应用部署
### 5.1 上传项目
```bash
# 本地打包(排除 node_modules 和 __pycache__
cd d:/ClaudeCode
tar -czf scilit-oncology.tar.gz \
--exclude='node_modules' --exclude='__pycache__' \
--exclude='*.db' --exclude='dist' \
--exclude='.git' \
backend/ frontend/ docker-compose.yml docker-compose.prod.yml \
scripts/ CLAUDE.md README.md .github/ .env.example
# 上传到服务器
scp scilit-oncology.tar.gz scilit@<IP>:/home/scilit/
# 服务器端解压
ssh scilit@<IP>
cd /home/scilit
mkdir -p sci-lit-manager
tar -xzf scilit-oncology.tar.gz -C sci-lit-manager
cd sci-lit-manager
```
### 5.2 创建生产环境变量
```bash
# /home/scilit/sci-lit-manager/.env
vim .env
```
```bash
# ===== 数据库(必须修改) =====
PG_PASSWORD=<生成强密码: openssl rand -hex 16>
REDIS_PASSWORD=<生成强密码: openssl rand -hex 16>
# ===== JWT(必须修改) =====
JWT_SECRET=<运行 openssl rand -hex 32 生成>
# ===== S3 / MinIO =====
S3_ENDPOINT=http://minio:9000
S3_ACCESS_KEY=minioadmin
S3_SECRET_KEY=minioadmin
S3_BUCKET=scilit-files
# ===== 邮件(腾讯云邮件推送 SES =====
SMTP_HOST=smtp.qcloudmail.com
SMTP_PORT=465
SMTP_USER=service@oncolit.gonsun.com
SMTP_PASSWORD=<腾讯云SES SMTP密码>
SMTP_FROM=service@oncolit.gonsun.com
# ===== AI 摘要(DeepSeek =====
AI_API_KEY=sk-your-deepseek-api-key
AI_BASE_URL=https://api.deepseek.com/v1
AI_MODEL=deepseek-chat
# ===== PubMed =====
PUBMED_API_KEY=your-ncbi-api-key
# ===== 搜索 =====
ES_URL=http://elasticsearch:9200
# ===== URL 配置 =====
PUBLIC_BASE_URL=https://oncolit.gonsun.com
CORS_ORIGINS=["https://oncolit.gonsun.com"]
# ===== 专科配置 =====
SPECIALTY=oncology
DEBUG=false
```
### 5.3 构建 Docker 镜像并启动
```bash
cd /home/scilit/sci-lit-manager
# 使用生产环境 Docker Compose 文件
docker compose -f docker-compose.prod.yml --env-file .env up -d postgres redis
# 等待数据库就绪
sleep 10
# 运行数据库迁移(migrate 服务会自动执行,也可手动)
docker compose -f docker-compose.prod.yml run --rm migrate
# 加载种子数据
docker compose -f docker-compose.prod.yml run --rm backend python scripts/seed_data.py
# 启动全部服务
docker compose -f docker-compose.prod.yml --env-file .env up -d
# 查看日志确认正常
docker compose -f docker-compose.prod.yml logs -f backend
```
### 5.4 验证服务
```bash
# 健康检查
curl https://oncolit.gonsun.com/api/v1/health
# 应该返回
# {"status":"ok","specialty":"oncology","db":"ok","version":"0.1.0"}
# 公开API
curl https://oncolit.gonsun.com/api/v1/public/feed?page_size=1
# 登录测试
curl -X POST https://oncolit.gonsun.com/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"demo@test.cn","password":"123456"}'
```
---
## 6. 数据库初始化
### 6.1 首次部署
服务器上已经用 Docker Compose 启动了 PostgreSQL。首次初始化:
```bash
COMPOSE="docker compose -f /home/scilit/sci-lit-manager/docker-compose.prod.yml --env-file /home/scilit/sci-lit-manager/.env"
# 运行迁移
$COMPOSE run --rm migrate
# 导入种子数据
$COMPOSE run --rm backend python scripts/seed_data.py
```
### 6.2 数据备份
```bash
# 每日备份脚本
# /home/scilit/backup.sh
#!/bin/bash
BACKUP_DIR=/home/scilit/backups
mkdir -p $BACKUP_DIR
DATE=$(date +%Y%m%d_%H%M%S)
COMPOSE="docker compose -f /home/scilit/sci-lit-manager/docker-compose.prod.yml"
# 备份 PostgreSQL
$COMPOSE exec -T postgres pg_dump -U scilit scilit > $BACKUP_DIR/scilit_$DATE.sql
# 保留最近 7 天
find $BACKUP_DIR -name "*.sql" -mtime +7 -delete
echo "Backup completed: $DATE"
```
```bash
# 添加定时任务(每天凌晨 3 点备份)
crontab -e
# 添加:
0 3 * * * /home/scilit/backup.sh >> /home/scilit/backups/backup.log 2>&1
```
### 6.3 PubMed 基线数据导入
```bash
# 下载 PubMed 2025 基线(约 40GB
# 服务器上使用 aria2 多连接下载
aria2c -x 8 -s 8 https://ftp.ncbi.nlm.nih.gov/pubmed/baseline/pubmed25n0001.xml.gz
# 导入(仅肿瘤科相关,会自动过滤)
COMPOSE="docker compose -f /home/scilit/sci-lit-manager/docker-compose.prod.yml --env-file /home/scilit/sci-lit-manager/.env"
$COMPOSE exec backend python scripts/pubmed_baseline.py \
--dir /data/pubmed/baseline \
--demo # 先试 5 个文件,确认没问题后去掉 --demo
```
---
## 7. 第三方服务注册
### 7.1 SMTP 邮件(腾讯云邮件推送 SES)
使用腾讯云邮件推送(Simple Email Service)发送通知/邀请邮件。SMTP 服务器 `smtp.qcloudmail.com:465`,密码需在腾讯云 SES 控制台生成 SMTP 密码。验证方式:
```bash
# 发送测试邮件
docker compose exec backend python -c "
from app.services.email import send_email
import asyncio
asyncio.run(send_email('your@email.com', 'Test', '<p>OK</p>'))
"
### 7.2 Stripe 支付
1. 注册 [stripe.com](https://stripe.com)(需要真实的公司/个人信息)
2. 先在 Test Mode 测试:
- 获取 `sk_test_xxx``pk_test_xxx`
- 在 Stripe Dashboard 创建 Products
- Pro Monthly: ¥35/month
- Team Monthly: ¥70/user/month
- Enterprise: Custom quote
- 获取 Price IDs
3. 生产上线前切换到 Live Mode
4. 配置 Webhook
- URL: `https://oncolit.gonsun.com/api/v1/webhooks/stripe`
- Events: `checkout.session.completed`, `customer.subscription.*`, `invoice.*`
- 获取 `whsec_xxx` Signing Secret
### 7.3 AI APIDeepSeek,已配置)
已在 `.env` 中配置 DeepSeek API
```bash
AI_API_KEY=sk-f59fabc2884d49328505567518729685
AI_BASE_URL=https://api.deepseek.com/v1
AI_MODEL=deepseek-chat
```
如需切换为 OpenAI 或其他模型,修改 `.env` 中对应项即可。
### 7.4 对象存储(MinIO 自建)
已在 `docker-compose.prod.yml` 中配置 MinIO 服务。首次部署后操作:
```bash
# 访问 MinIO Web 控制台(先暴露端口调试)
# 默认:http://<服务器IP>:9001,账号 minioadmin / minioadmin
# 或通过 mc 客户端配置
docker compose exec minio mc alias local http://localhost:9000 minioadmin minioadmin
docker compose exec minio mc mb local/scilit-files
# 验证
docker compose exec minio mc ls local/scilit-files
```
> **安全提示:** 初始账号密码(minioadmin/minioadmin)在生产环境应立即通过 `.env` 中的 `MINIO_ROOT_USER` 和 `MINIO_ROOT_PASSWORD` 修改。
### 7.5 PubMed API Key + 数据填充(部署后一次性执行)
#### 7.5.1 PubMed API Key
已在 `.env` 中配置,速率 10 req/s
```bash
PUBMED_API_KEY=692e319f33129b7a5d2d94c57b67739e0a08
```
如 Key 过期或被轮换,登录 [NCBI Account](https://account.ncbi.nlm.nih.gov/settings/) → API Key Management 更新。
#### 7.5.2 部署后一次性数据填充
按以下顺序执行(越靠前的越关键):
```bash
# 0. 获取管理员 Token
admin_token=$(curl -s -X POST https://oncolit.gonsun.com/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@yourhospital.com","password":"你的密码"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['token']['access_token'])")
# 1. 导入种子标签 + 期刊
COMPOSE="docker compose -f /home/scilit/sci-lit-manager/docker-compose.prod.yml --env-file /home/scilit/sci-lit-manager/.env"
$COMPOSE exec backend python scripts/seed_tags_only.py
# 2. 回填 MeSH 标签(将已有 mesh_headings 匹配到 GlobalTag
# 预期:~50% 的文献获得标签
curl -X POST "https://oncolit.gonsun.com/api/v1/admin/pipeline/retag-tags" \
-H "Authorization: Bearer $admin_token"
# 3. 刷新引用次数(全库跑一次,约 15 分钟,3 req/s)
curl -X POST "https://oncolit.gonsun.com/api/v1/admin/pipeline/refresh-citations" \
-H "Authorization: Bearer $admin_token"
# 4. 提取 PICO 要素(从摘要中抽取,调用 DeepSeek API3 并发)
curl -X POST "https://oncolit.gonsun.com/api/v1/admin/pipeline/extract-pico?limit=500" \
-H "Authorization: Bearer $admin_token"
# 5. 生成 AI 一句话摘要(每次处理最近 30 篇有摘要的文献)
# 可运行多次,每次处理 30 篇
curl -X POST "https://oncolit.gonsun.com/api/v1/admin/ai/summarize" \
-H "Authorization: Bearer $admin_token"
# 6. 回填 OA 全文(仅 ~0.5% 的文献有 PMC 全文,轻量)
curl -X POST "https://oncolit.gonsun.com/api/v1/admin/pipeline/backfill-oa-text?limit=5000" \
-H "Authorization: Bearer $admin_token"
# 7. 回填研究设计分类(基于 PubType 映射)
curl -X POST "https://oncolit.gonsun.com/api/v1/admin/pipeline/backfill-study-designs" \
-H "Authorization: Bearer $admin_token"
# 8. 触发一次完整的文献检索管道
curl -X POST "https://oncolit.gonsun.com/api/v1/admin/pipeline/run" \
-H "Authorization: Bearer $admin_token"
```
**部署后首次填充预期效果(Dashboard 数据质量面板可验证):**
| 指标 | 预期值 | 说明 |
|------|--------|------|
| 已打标文献 | ~50% | MeSH UI 匹配精度 |
| PICO 覆盖 | ~36% | 有摘要的文献 |
| 引用次数 | 依文献年代 | PubMed elink 实时查询。2026 年新文献引用数多为 0 |
| OA 全文 | ~0.5% | PMC 平台全文 |
| AI 摘要 | 30篇/次 | 可重复触发 |
> **定时任务说明:** ARQ Worker 在 Docker Compose 启动后自动运行,无需手动配置。
> - 每日 03:07 UTC — 精搜
> - 每周日 03:37 UTC — 宽搜
> - 每日 05:13 UTC — 引用刷新
> - 每日 22:30 UTC — 摘要邮件
### 7.6 微信服务号
1. 注册 [微信公众平台](https://mp.weixin.qq.com/)
2. 选择"服务号" → 提交营业执照 → 等待审核(1-7天)
3. 认证费用 ¥300/年
4. 认证后在"开发 → 基本配置"获取 AppID + AppSecret
5. 在"功能 → 模板消息"申请模板(审核 1-3 天)
6. 在项目中配置:
```bash
WECHAT_APP_ID=wx...
WECHAT_APP_SECRET=...
WECHAT_TEMPLATE_ID=...
```
---
## 8. 上线验证清单
### 8.1 安全
- [ ] `JWT_SECRET` 已随机生成,不是 `dev-secret-...`
- [ ] SSL 证书生效(浏览器显示 🔒)
- [ ] 防火墙只开放 22/80/443,数据库端口不对外
- [ ] PostgreSQL 密码已改(非默认),Redis 密码已设
- [ ] `/admin` 普通用户无法访问
- [ ] S3/MinIO 桶已创建,应用可读写
- [ ] 忘记密码功能发真实的邮件(不是 dev 模式的 reset_link
### 8.2 功能
- [ ] 公开首页可访问,能浏览文献
- [ ] 注册流程走通(验证码 → 注册 → 设置领域 → 看Feed)
- [ ] Demo 账号能登录并看到 42 篇个性化推送
- [ ] 管理员能登录 /admin 并看到看板数据
- [ ] 搜索返回正确结果
- [ ] 引用导出 BibTeX/RIS 正常
- [ ] API 文档 /docs 可访问
### 8.3 数据管道
- [ ] 种子标签已导入(`seed_tags_only.py`
- [ ] MeSH 标签回填已执行(`retag-tags`
- [ ] 引用次数已刷新(`refresh-citations`
- [ ] PICO 提取已执行(`extract-pico`
- [ ] AI 摘要已生成(`ai/summarize`
- [ ] OA 全文已回填(`backfill-oa-text`
- [ ] 管道定时任务已启动(ARQ Worker 存活)
- [ ] Dashboard 数据质量面板各指标在预期范围内
- [ ] 首次完整管道运行无异常
### 8.3 性能
- [ ] 公开 Feed 首屏加载 < 1 秒
- [ ] 登录后 Feed < 2 秒
- [ ] API 响应有 `X-Response-Time-Ms`
- [ ] 静态资源有 Cache-Control 头
---
## 8.5 数据库连接池
当前使用 SQLAlchemy 内置连接池([backend/app/db.py](backend/app/db.py)):
- `pool_size=10`, `max_overflow=20` → 单 worker 最大 30 连接
- 4 个 uvicorn worker → 最大 120 个 PG 连接
- PG 16 默认 `max_connections=100`,已超限
> **决定:暂不引入 PgBouncer。** 当前用户量未达瓶颈,后续并发增长后需在应用层与 PG 之间添加 PgBouncertransaction 模式)。届时需下调 pool_size 避免连接堆积。
## 8.6 首页 Feed 缓存
首页文献列表使用**预缓存**策略(类似热搜缓存 `hot_articles_cache.py`)。
| 项 | 值 |
|---|---|
| 缓存 Key | `homepage:feed` |
| TTL | 1800s30 分钟) |
| 刷新任务 | `refresh_homepage_feed`ARQ cron 每 0/30 分 |
| 降级策略 | 缓存未命中时走实时简化查询 |
| 预计算条数 | 50 条(返回 20 条) |
**设计意图:** 避免每次首页加载走 `POST /features/search/advanced` 高级搜索全套流程(COUNT+SELECT+标签JOIN+期刊JOIN)。预计算只取 `pub_date` 索引的最新 50 条,不取 JSON 大字段(authors/mesh_headings/ai_summary),传输量从 400KB+ 降到 ~10KB。
---
## 9. 日常运维
### 9.1 查看日志
```bash
# 所有服务(使用生产 Compose 文件)
COMPOSE="docker compose -f /home/scilit/sci-lit-manager/docker-compose.prod.yml --env-file /home/scilit/sci-lit-manager/.env"
$COMPOSE logs -f
# 只看后端
$COMPOSE logs -f backend
# 最近 100 行
$COMPOSE logs --tail=100 backend
```
### 9.2 重启服务
```bash
COMPOSE="docker compose -f /home/scilit/sci-lit-manager/docker-compose.prod.yml --env-file /home/scilit/sci-lit-manager/.env"
$COMPOSE restart backend
```
### 9.3 更新代码
```bash
# 服务器
SSH_HOST=scilit@<服务器IP>
COMPOSE="docker compose -f /home/scilit/sci-lit-manager/docker-compose.prod.yml --env-file /home/scilit/sci-lit-manager/.env"
# 更新后端代码并重建
ssh $SSH_HOST "cd /home/scilit/sci-lit-manager && git pull"
ssh $SSH_HOST "$COMPOSE build backend && $COMPOSE up -d backend"
# 更新前端代码并重建
ssh $SSH_HOST "$COMPOSE build frontend && $COMPOSE up -d frontend"
```
### 9.4 数据库迁移(新增表/字段)
```bash
# 本地先写 Alembic 迁移
cd backend
alembic revision --autogenerate -m "描述"
# 服务器上运行
COMPOSE="docker compose -f /home/scilit/sci-lit-manager/docker-compose.prod.yml --env-file /home/scilit/sci-lit-manager/.env"
$COMPOSE run --rm migrate
```
### 9.5 监控
```bash
# 系统资源
htop
# Docker 容器资源
docker stats
# 磁盘
df -h
```
---
## 10. 应急预案
### 10.1 服务挂了
```bash
# 查看日志
docker compose logs --tail=50 backend
# 重启
docker compose restart backend
# 如果起不来
docker compose down && docker compose up -d
```
### 10.2 数据库满了
```bash
# 清理 user_feed 旧分区
docker compose exec backend python -c "
from app.db import engine
from sqlalchemy import text
# 删除 3 个月前的 feed 分区表
"
```
### 10.3 回滚
```bash
# 如果新版本有问题
COMPOSE="docker compose -f /home/scilit/sci-lit-manager/docker-compose.prod.yml --env-file /home/scilit/sci-lit-manager/.env"
# 回滚后端
git revert <commit_hash>
$COMPOSE build backend && $COMPOSE up -d backend
```
### 10.4 紧急联系
| 服务 | 控制台 |
|------|------|
| 阿里云 | https://ecs.console.aliyun.com/ |
| Stripe | https://dashboard.stripe.com/ |
| 域名 DNS | 你的域名服务商后台 |
| SSL 证书 | `certbot certificates` 查看 |
---
## 附录 A:成本估算(小规模)
| 项目 | 月费 | 年费 |
|------|:--:|:--:|
| 阿里云 ECS 2C4G 40G SSD | ¥100 | ¥1,200 |
| 域名 oncolit.gonsun.com | ¥5 | ¥60 |
| SSL (Let's Encrypt) | ¥0 | ¥0 |
| MinIO 对象存储(额外磁盘) | ¥0(用 ECS 磁盘) | ¥0 |
| SMTP(日<200封) | ¥0 | ¥0 |
| DeepSeek(日200篇摘要,约5M tokens | ~¥5 | ~¥60 |
| Stripe 交易费 | 按交易 2.9% | — |
| **合计** | **~¥110/月** | **~¥1,320/年** |
## 附录 B:常用命令速查
```bash
# SSH 登录
ssh scilit@<IP>
# 进入项目目录
cd /home/scilit/sci-lit-manager
# 定义 Compose 别名
COMPOSE="docker compose -f docker-compose.prod.yml --env-file .env"
# 查看服务状态
$COMPOSE ps
# 日志
$COMPOSE logs -f backend
# 重启
$COMPOSE restart backend
# 查看所有容器资源占用
docker stats
# 数据库备份
bash /home/scilit/backup.sh
# SSL 证书续期
sudo certbot renew
# 系统更新
sudo apt update && sudo apt upgrade -y
# 磁盘/内存
df -h && free -h
```
---
## 11. 实际部署流程(每日代码更新)
> 本项目不使用 git flow 部署,采用 SCP + docker cp 直接替换文件。
### 11.1 如何判断是否需要重建镜像
| 场景 | 部署方式 | 原因 |
|------|----------|------|
| 改 `.py` / `.vue` / `.ts` | `docker cp` | 文件替换即可 |
| 加/改 pip 包(`requirements.txt` | `docker compose build` | 容器内没有新包 |
| 加/改 npm 包(`package.json` | `docker compose build` | 容器内没有新包 |
| 改 `Dockerfile` / `nginx.conf` | `docker compose build` | 容器配置变了 |
| 改环境变量(`.env` | `docker compose up -d` | 不需要重建,重启读取即可 |
需要重建的唯一情况是:**容器内需要的东西不在容器里**(新依赖、新配置)。
### 11.2 确认 requirements.txt 是否有变更
```bash
# 拉取生产上的文件对比
ssh -i ~/.ssh/id_ed25519 root@123.207.9.209 \
"docker exec scilit-backend-1 cat /app/requirements.txt" \
> /tmp/requirements_prod.txt
diff d:/ClaudeCode/backend/requirements.txt /tmp/requirements_prod.txt
```
有输出 → 有变更,需要重建镜像。无输出 → 没变,直接 cp。
### 11.3 前端部署(Vue/TypeScript — 需要编译)
```bash
# 1. 构建生产包(编译 .vue/.ts → dist/
cd frontend && npm run build
# 2. 复制到服务器
ssh -i ~/.ssh/id_ed25519 root@123.207.9.209 "mkdir -p /tmp/frontend-dist"
scp -i ~/.ssh/id_ed25519 -r frontend/dist/* root@123.207.9.209:/tmp/frontend-dist/
# 3. 替换容器内文件 + 重载 nginx
ssh -i ~/.ssh/id_ed25519 root@123.207.9.209 \
"docker cp /tmp/frontend-dist/. scilit-frontend-1:/usr/share/nginx/html/ \
&& docker exec scilit-frontend-1 nginx -s reload \
&& rm -rf /tmp/frontend-dist"
```
### 11.4 后端部署(Python — 不需要编译)
#### 单个文件
```bash
scp -i ~/.ssh/id_ed25519 backend/app/services/xxx.py root@123.207.9.209:/tmp/
ssh -i ~/.ssh/id_ed25519 root@123.207.9.209 \
"docker cp /tmp/xxx.py scilit-backend-1:/app/app/services/xxx.py \
&& docker restart scilit-backend-1 && rm /tmp/xxx.py"
```
#### 多个文件(tar 打包一次性覆盖)
```bash
tar czf /tmp/backend_update.tar.gz -C backend app/services/ app/api/ app/schemas/
scp /tmp/backend_update.tar.gz root@123.207.9.209:/tmp/
ssh root@123.207.9.209 \
"docker cp /tmp/backend_update.tar.gz scilit-backend-1:/tmp/ \
&& docker exec scilit-backend-1 tar xzf /tmp/backend_update.tar.gz -C /app/ \
&& docker restart scilit-backend-1"
```
#### 新增依赖时(必须重建镜像)
```bash
# 在服务器上执行
cd /home/scilit/sci-lit-manager
docker compose -f docker-compose.prod.yml build backend
docker compose -f docker-compose.prod.yml up -d backend
```
### 11.5 关于 Docker 构建缓存
Dockerfile 层序决定了缓存策略:
```
1. FROM python:3.10 ← 缓存(镜像标签没变)
2. COPY requirements.txt ← 缓存(文件没变)
3. RUN pip install -r ... ← 缓存命中(requirements.txt 不变时跳过)
4. COPY app/ ← 不命中(代码改了)
5. CMD uvicorn ... ← 跟随上层,重建
```
不改 `requirements.txt` 时重建很快(~3-5spip install 直接跳过),只改代码没必要重建,`docker cp` 秒级。
---
## 12. 生产部署严格规则(2026-07-14 总结)
> 以下规则来自实际踩坑,每次部署前必须逐条对照。
### 规则 1:永远要先清理 `__pycache__`
```bash
# docker cp 后,务必执行:
docker exec scilit-backend-1 find /app/app -name __pycache__ -exec rm -rf {} + 2>/dev/null
```
否则 Python 会加载旧的 `.pyc` 字节码,导致神秘的 ImportError(如 `cap_pub_date``is_superuser` 等问题)。
### 规则 2:后端部署后必须同时重启 worker
```bash
docker restart scilit-backend-1
docker restart scilit-worker-1
```
Worker 使用独立进程,不重启则继续执行旧代码,导致定时任务(PubMed 管道、引用刷新等)行为不一致。
### 规则 3:前端部署必须先清理旧 assets
```bash
# 先清空旧文件,再 docker cp,否则多轮构建产物污染
ssh root@123.207.9.209 \
"rm -rf /usr/share/nginx/html/assets /usr/share/nginx/html/index.html /usr/share/nginx/html/favicon* /usr/share/nginx/html/icon-*.png \
&& docker cp /tmp/frontend-dist/. scilit-frontend-1:/usr/share/nginx/html/ \
&& docker exec scilit-frontend-1 nginx -s reload"
```
不清理会导致 assets 目录混入多个构建版本的 chunk 文件(同组件 5 个不同 hash 版本),浪费磁盘且可能加载错误资源。
### 规则 4:部署后必须验证
```bash
# 4.1 健康检查
docker exec scilit-backend-1 curl -sf http://localhost:8000/health
# 4.2 新模块导入测试(新增文件时)
docker exec scilit-backend-1 python -c "from app.api.v1 import admin_roles, journals; print('OK')"
docker exec scilit-backend-1 python -c "from app.core import audit; print('OK')"
# 4.3 md5 抽查(确认容器与本地一致)
docker exec scilit-backend-1 md5sum /app/app/api/v1/router.py
# 与本地比对:md5sum backend/app/api/v1/router.py
# 4.4 检查是否有放错位置的文件
docker exec scilit-backend-1 ls /app/app/models/auth.py 2>/dev/null && echo "WARNING: misplaced file!" || echo "clean"
```
### 规则 5:本地新文件必须先同步到服务器项目目录
服务器项目目录 `/root/scilit/` 是 Docker 镜像的构建源目录。新文件(如 `models/audit.py`)必须同时:
1. 同步到服务器项目目录(scp
2. 复制到容器(docker cp
否则容器重建后会丢失文件。
### 规则 6:永远不要 docker cp 到错误的目录
`models/` 目录只放数据库模型类,不要放路由代码或安全工具函数。
```bash
# ❌ 错误的放法(曾经踩坑):
# 把 api/v1/auth.py → docker cp 到了 /app/app/models/auth.py
# ✅ 正确的目录对应关系:
# backend/app/api/v1/xxx.py → /app/app/api/v1/xxx.py
# backend/app/core/xxx.py → /app/app/core/xxx.py
# backend/app/models/xxx.py → /app/app/models/xxx.py
# backend/app/services/xxx.py → /app/app/services/xxx.py
# backend/app/schemas/xxx.py → /app/app/schemas/xxx.py
```
### 规则 7:确认 pyproject.toml 无变化才用 docker cp
```bash
# 对比容器与本地 pyproject.toml
docker exec scilit-backend-1 cat /app/pyproject.toml | md5sum
md5sum backend/pyproject.toml
# 不一致 → 必须重建镜像
```
有变化(新增 pip 依赖)时,`docker compose build backend` 重建。依赖不存在于容器中会导致 ModuleNotFoundError。
### 规则 8:多文件部署一律用 tar 打包,不倒腾单个文件
```bash
# ✅ 正确:tar 打包整个目录
tar czf /tmp/backend_update.tar.gz -C backend app/services/ app/api/ app/core/ app/models/ app/schemas/ app/tasks/
scp /tmp/backend_update.tar.gz root@123.207.9.209:/tmp/
ssh root@123.207.9.209 \
"docker cp /tmp/backend_update.tar.gz scilit-backend-1:/tmp/ \
&& docker exec scilit-backend-1 tar xzf /tmp/backend_update.tar.gz -C /app/ \
&& docker exec scilit-backend-1 find /app/app -name __pycache__ -exec rm -rf {} + 2>/dev/null \
&& docker restart scilit-backend-1 && docker restart scilit-worker-1"
```
单文件 scp 容易漏复制依赖文件,且文件较小时 tar 的开销可忽略。
### 规则 8b:不确定改了什么文件时,全量同步 `app/`
```bash
# ✅ 最安全:整个 app/ 目录打包
cd d:/ClaudeCode/backend
tar czf /tmp/backend-app.tar.gz --exclude="__pycache__" --exclude="*.pyc" app/
scp /tmp/backend-app.tar.gz root@123.207.9.209:/tmp/
ssh root@123.207.9.209 \
"docker cp /tmp/backend-app.tar.gz scilit-backend-1:/tmp/ \
&& docker exec scilit-backend-1 tar xzf /tmp/backend-app.tar.gz -C /app/ \
&& docker exec scilit-backend-1 find /app/app -name __pycache__ -exec rm -rf {} + 2>/dev/null \
&& docker restart scilit-backend-1 && docker restart scilit-worker-1"
```
**为什么要全量?** 模型文件(`models/literature.py` 等)可能被多个路由和 services 引用。只 cp 改了的 API 文件而漏掉模型更新,启动后出现 `AttributeError: type object 'GlobalTag' has no attribute 'source'``'User' object has no attribute 'admin_note'`,症状是后台大面积请求失败。
**何时用:**
- 改的是 `models/``schemas/` 下的文件 → 全量
- 改的是 `core/` 下的基础设施类 → 全量
- 只改单个 API 路由(如 `admin.py`),无公共依赖 → 单个 tar 可接受
- 不确改了什么 → 全量,10s 的事比诊断 AttributeError 快
### 规则 9:服务器不是 git 仓库,手动同步代替 git pull
服务器 `/root/scilit/` 不是 git 仓库。同步方式:
```bash
# 方式 A:全量 tar 同步
tar czf /tmp/scilit_sync.tar.gz --exclude=node_modules --exclude='__pycache__' --exclude=.git backend/ frontend/ docker-compose* .env.example
scp /tmp/scilit_sync.tar.gz root@123.207.9.209:/root/
ssh root@123.207.9.209 "cd /root/scilit && tar xzf /tmp/scilit_sync.tar.gz"
# 方式 B:仅增量同步后端 Python 文件
# 由 Claude 自动完成,使用 tar + scp + docker cp 流程
```
### 规则 10:容器内文件用 `scilit` 用户身份运行
Dockerfile 中有 `USER scilit`,容器内 Python 进程以 scilit 用户运行。docker cp 复制进去的文件属主为 root,但只要文件对 scilit 可读(`-rw-r--r--`),Python 就能正常使用。如果遇到权限问题:
```bash
docker exec scilit-backend-1 chown -R scilit:scilit /app/app
```
### 规则 11:新增 Alembic 迁移时,迁移文件必须在容器内
```bash
# 本地生成迁移后:
scp -r backend/alembic/versions/xxx.py root@123.207.9.209:/tmp/
ssh root@123.207.9.209 \
"docker cp /tmp/xxx.py scilit-backend-1:/app/alembic/versions/xxx.py \
&& docker exec scilit-backend-1 alembic -c alembic/alembic.ini upgrade head"
```
没有迁移文件在容器内,`alembic upgrade head` 会报 `Target database is not up to date`
---
## 13. 事故记录:2026-07-17 部署回退问题复盘
### 背景
一次正常的安全加固部署(CSP 统一、Refresh token 告警、验证码集成、JWT 校验、注册限速、Nginx 日志持久化),先后出现多个问题,反复修复。
### 问题链(按时间顺序)
#### 问题 1docker build cache 被清除 → 25min 重建
**现象:** `docker compose build backend` 重新下载所有 pip 包,构建超 25 分钟。
**根因:** 上一轮部署执行了 `docker builder prune -a -f`,所有构建缓存层被清除。
**教训:** 不改 `requirements.txt` 时,pip install 是缓存的,构建只要几秒。永远不要在生产服务器上 `prune -a`。如需清理磁盘空间,指定 `docker builder prune`(不加 -a,只清 dangling 层)。
**正确做法:** 不能构建时用 `docker cp` 热更新,不需要构建。能构建时用缓存,几秒完事。
---
#### 问题 2:tar 路径前缀嵌套
**现象:** `tar xzf backend.tar.gz -C /root/scilit/backend/` 后,文件到了 `/root/scilit/backend/backend/app/...`
**根因:** tar 包内的路径前缀是 `backend/``tar czf backend.tar.gz backend/app/`),而解压目标已经是 `backend/`,导致嵌套。
**教训:** 打包时用 `--strip-components=1`,或直接在 `backend/` 目录内打包。
**正确做法:**
```bash
# 方式 A:在子目录打包
cd backend && tar czf /tmp/backend-app.tar.gz app/
# 方式 Bstrip-components
tar czf /tmp/backend-app.tar.gz -C backend app/
ssh ... "tar xzf /tmp/backend-app.tar.gz --strip-components=1 -C /root/scilit/backend/"
```
---
#### 问题 3Backend 容器和 Postgres 在不同 Docker 网络
**现象:** backend 容器启动后 `Health check DB connection failed``alembic upgrade head``Name or service not known`
**根因:** compose 文件定义了两个 network`scilit``scilit_default`)。postgres 在 `scilit_default`backend 在 `scilit_scilit`
**教训:** 生产 compose 文件定义了 `networks: scilit`,但 postgres 服务没有指定 `networks:`Docker 自动为其创建默认的 `scilit_default` 网络。该问题在容器重启后反复出现。
**正确做法:** 所有服务统一在同一网络。compose 文件中每个服务都显式指定 `networks: - scilit`(已修复)。
---
#### 问题 4Nginx 502 Bad GatewayBackend 容器 IP 变化)
**现象:** backend 容器重启后,nginx 代理报 502。直接访问 backend127.0.0.1:8000)正常。
**根因:** Nginx `proxy_pass http://backend:8000` 使用固定字符串解析,只在启动时解析一次 DNS。Backend 容器重启后 Docker 为它分配了新 IP,nginx 仍用旧 IP 连接。
**教训:** Docker 环境中,nginx 代理上游必须用变量形式 + resolver 指令触发动态 DNS 解析。
**正确做法:**
```nginx
resolver 127.0.0.11 ipv6=off valid=30s;
server {
set $backend_upstream http://backend:8000;
location /api/ {
proxy_pass $backend_upstream;
}
}
```
---
#### 问题 5`docker compose up -d --no-deps frontend` 覆盖了 `docker cp` 的文件
**现象:** 前端标签显示空白。`docker cp` 进去的新前端文件(RegisterView 等)消失了。
**根因:** 修改 compose 文件(加日志 volume)后执行 `docker compose up -d --no-deps frontend`,Docker 重建了容器,从**旧镜像**启动。之前 `docker cp` 进去的文件不在镜像中,所以全部丢失。
**教训:** `docker cp` 修改的是运行中的容器文件系统,不是镜像。容器重建后镜像内容胜出。容器不重建时 cp 永久有效。
**正确做法:**
```bash
# 方案一:cp 后立即保存为新镜像(最简单)
docker cp dist/. frontend:/usr/share/nginx/html/
docker commit scilit-frontend-1 scilit-frontend:latest
# 方案二:改 compose 前先构建新镜像
docker compose build frontend
# 再改 compose 文件 → up -d
# 方案三:不改 compose,只在运行中容器 cp
```
---
### 根本原因
全部问题有一个共同模式:**每次部署依赖"上下文常识",而不是依赖流程。** 具体来说:
1. 不知道 `proxy_pass` 固定字符串会缓存 DNS → nginx 502
2. 不知道 `docker compose up -d` 会重建容器 → cp 的内容丢失
3. 不知道 prune -a 会清 pip 缓存 → 多等 25 分钟
4. 不知道 tar 路径前缀 → 文件放到错误目录
每个问题单独看都是 Docker 基础常识,但在多步骤部署中,每步引入一个"我不知道这里还有这个坑"的盲点,累积后大面积出错。
### 改进点
| 问题 | 预防措施 | 状态 |
|------|---------|:--:|
| 构建缓存被清 | 不执行 `docker builder prune -a` | ✅ 已记录 memory |
| tar 嵌套 | 统一用 `--strip-components=1` 或先 cd 再打包 | ✅ 规则 8 已更新 |
| 网络不一致 | 所有服务显式指定 networks | ✅ compose 已修 |
| nginx 502 | 变量形式 proxy_pass + resolver | ✅ nginx.conf 已修 |
| `docker cp` 被覆盖 | 改 compose 前先 build,或 cp 后立即 commit | ✅ 规则 12 见下 |
### 规则 12:前端 docker cp 后立即 commit 镜像
```bash
# 前端部署后必须保存为新镜像
docker commit scilit-frontend-1 scilit-frontend:latest
```
否则任何导致容器重建的操作(`docker compose up -d`、改 compose 文件、服务器重启后 `compose up`)都会丢失 cp 的文件。后端容器(`scilit-backend-1`)很少重建,不需要 commit,但前端容器需要。
### 规则 13:改 compose 文件前先构建镜像
```bash
# 先确保镜像包含最新代码
docker compose build frontend # 前端(npm,几秒)
# 或
docker compose build backend # 后端(pip 缓存命中时几秒)
# 再改 compose 配置 → up -d 重建容器
```
`docker-compose.prod.yml` 必然触发容器重建,镜像必须包含最新代码。