# 证书系统正式部署操作手册

本文档用于把“培训结业证书管理与查询系统”部署到正式服务器，并完成短信验证码配置、上线验收、日常运维和回滚准备。

适用范围：

- 正式工程：`backend/`、`frontend/`、`docker-compose.yml`
- 不适用于本地验收版：`local_app/`

## 1. 部署架构

推荐一期使用单机 Docker Compose：

```text
用户浏览器
  -> 域名 HTTPS
  -> 宿主机 Nginx
  -> frontend 容器，端口 8080，内置 Nginx，负责前端页面和 /api 代理
  -> backend 容器，端口 8000，FastAPI
  -> MySQL 8，Compose 内置 MySQL 或阿里云 RDS MySQL
```

正式环境不要直接运行：

```bash
python local_app/server.py
```

正式环境只运行：

```bash
docker compose up -d --build
```

## 2. 服务器准备

建议配置：

- 操作系统：Ubuntu 22.04 LTS 或同类 Linux。
- 规格：2 核 4G 起步。
- 磁盘：建议至少 40G，最好把 `/data` 放到数据盘。
- 域名：已解析到服务器公网 IP。
- HTTPS 证书：已准备好证书文件。

安装 Docker：

```bash
apt update
apt install -y docker.io docker-compose-plugin
systemctl enable docker
systemctl start docker
docker --version
docker compose version
```

创建数据目录：

```bash
mkdir -p /data/certificate-system/uploads
mkdir -p /data/certificate-system/error-reports
mkdir -p /data/certificate-system/exports
mkdir -p /data/certificate-system/pdf-cache
mkdir -p /data/backups/mysql
```

## 3. 上传代码

推荐目录：

```text
/opt/certificate-system
```

如果用 Git：

```bash
cd /opt
git clone <你的仓库地址> certificate-system
cd /opt/certificate-system
```

如果手动上传压缩包：

```bash
cd /opt
unzip certificate-system.zip
cd /opt/certificate-system
```

## 4. 配置 .env

Docker Compose 使用项目根目录的 `.env`，不是 `backend/.env`。

```bash
cd /opt/certificate-system
cp .env.example .env
vim .env
```

### 4.1 使用 Compose 内置 MySQL

`.env` 示例：

```env
APP_ENV=production
DATABASE_URL=mysql+pymysql://certificate:请改成业务库强密码@mysql:3306/certificate_system
SECRET_KEY=请改成至少32字节随机字符串
PUBLIC_BASE_URL=https://你的域名
CERTIFICATE_NO_SECRET=请改成另一串随机字符串
PDF_CACHE_DAYS=30
PDF_CLEANUP_DAYS=5
DATA_ROOT=/data/certificate-system
CORS_ORIGINS=https://你的域名

SMS_MODE=local
ALIYUN_ACCESS_KEY_ID=
ALIYUN_ACCESS_KEY_SECRET=
ALIYUN_SMS_SIGN_NAME=
ALIYUN_SMS_TEMPLATE_CODE=
```

生成随机密钥：

```bash
openssl rand -hex 32
openssl rand -hex 32
```

同时修改 `docker-compose.yml` 里的 MySQL 密码，保证和 `DATABASE_URL` 一致：

```yaml
MYSQL_ROOT_PASSWORD: 请改成root强密码
MYSQL_DATABASE: certificate_system
MYSQL_USER: certificate
MYSQL_PASSWORD: 请改成业务库强密码
```

### 4.2 使用阿里云 RDS MySQL

`.env` 示例：

```env
APP_ENV=production
DATABASE_URL=mysql+pymysql://RDS账号:RDS密码@RDS内网地址:3306/certificate_system
SECRET_KEY=请改成至少32字节随机字符串
PUBLIC_BASE_URL=https://你的域名
CERTIFICATE_NO_SECRET=请改成另一串随机字符串
PDF_CACHE_DAYS=30
PDF_CLEANUP_DAYS=5
DATA_ROOT=/data/certificate-system
CORS_ORIGINS=https://你的域名

SMS_MODE=local
ALIYUN_ACCESS_KEY_ID=
ALIYUN_ACCESS_KEY_SECRET=
ALIYUN_SMS_SIGN_NAME=
ALIYUN_SMS_TEMPLATE_CODE=
```

RDS 检查点：

- ECS 和 RDS 在同一个 VPC。
- RDS 白名单加入 ECS 内网 IP。
- RDS 中已创建数据库 `certificate_system`。
- 不建议开放 RDS 公网访问。

如果使用 RDS，可以保留 `docker-compose.yml` 里的 `mysql` 服务但不使用；更干净的做法是拆一个生产 compose 文件去掉 `mysql`，一期也可以先保持现状，只要 `DATABASE_URL` 指向 RDS，业务后端就不会连接 Compose 内置 MySQL。

## 5. 短信验证码配置

系统支持两种短信模式：

```env
SMS_MODE=local
```

本地测试模式。验证码会打印在后端日志里，不会真的发短信。

```env
SMS_MODE=aliyun
```

阿里云短信模式。系统会调用阿里云短信服务发送验证码。

### 5.1 开通阿里云短信服务

在阿里云控制台操作：

1. 登录阿里云控制台。
2. 搜索“短信服务”，开通短信服务。
3. 进入“国内消息”。
4. 申请短信签名。
5. 申请短信模板。
6. 创建 RAM 用户或 RAM 角色，并授予短信服务发送权限。
7. 创建 AccessKey，保存 `AccessKey ID` 和 `AccessKey Secret`。

安全建议：

- 不要使用主账号 AccessKey。
- 使用 RAM 用户，权限只给短信服务需要的最小权限。
- AccessKey Secret 只会显示一次，要保存到密码管理工具或服务器安全配置中。

### 5.2 申请短信签名

签名是短信开头的 `【xxx】`。

建议填写：

```text
签名名称：与你公司/品牌/备案主体一致，例如 慧愈文化
适用场景：验证码
签名来源：企事业单位全称或简称
申请说明：用于学员查询培训结业证书时进行手机号验证码验证
```

审核通过后，把签名名称填到：

```env
ALIYUN_SMS_SIGN_NAME=审核通过的签名名称
```

注意：这里不要写方括号，例如写 `慧愈文化`，不要写 `【慧愈文化】`。

### 5.3 申请短信模板

当前代码发送的模板变量是：

```json
{"code":"123456"}
```

所以阿里云模板必须包含变量：

```text
${code}
```

推荐模板内容：

```text
您的验证码是${code}，5分钟内有效，请勿泄露给他人。
```

审核通过后，拿到模板 Code，例如：

```text
SMS_123456789
```

填到：

```env
ALIYUN_SMS_TEMPLATE_CODE=SMS_123456789
```

### 5.4 配置生产 .env

正式发短信时，将 `.env` 改成：

```env
SMS_MODE=aliyun
ALIYUN_ACCESS_KEY_ID=你的AccessKeyID
ALIYUN_ACCESS_KEY_SECRET=你的AccessKeySecret
ALIYUN_SMS_SIGN_NAME=你的短信签名
ALIYUN_SMS_TEMPLATE_CODE=你的短信模板Code
```

配置完成后必须重建后端镜像，因为阿里云 SDK 是后端依赖：

```bash
docker compose up -d --build backend
docker compose restart frontend
```

或者整体重建：

```bash
docker compose up -d --build
```

### 5.5 验证短信配置

先看配置是否进入容器：

```bash
docker compose exec backend env | grep -E "SMS_MODE|ALIYUN"
```

直接从后端容器发一条测试短信到自己的手机号：

```bash
docker compose exec backend python - <<'PY'
from app.services.sms import send_sms

phone = "请改成你的手机号"
ok = send_sms(phone, "123456")
print("send result:", ok)
PY
```

再看日志：

```bash
docker compose logs -f backend | grep -E "阿里云短信|短信验证码|未安装依赖"
```

成功时通常能看到：

```text
[阿里云短信] 发送成功: 138xxxx0000
```

然后做完整业务验证：

1. 打开 `https://你的域名/query`。
2. 切到“短信验证码查询”。
3. 输入测试手机号。
4. 获取验证码。
5. 输入短信验证码。
6. 查询证书。

### 5.6 短信常见问题

如果日志出现：

```text
未安装依赖
```

说明镜像里没有安装阿里云 SDK。确认 `backend/requirements.txt` 包含：

```text
alibabacloud-dysmsapi20170525==4.5.1
```

然后执行：

```bash
docker compose up -d --build backend
```

如果发送失败，优先检查：

- `SMS_MODE` 是否等于 `aliyun`。
- `ALIYUN_ACCESS_KEY_ID` 是否正确。
- `ALIYUN_ACCESS_KEY_SECRET` 是否正确。
- `ALIYUN_SMS_SIGN_NAME` 是否是审核通过的签名名称，不带 `【】`。
- `ALIYUN_SMS_TEMPLATE_CODE` 是否是审核通过的模板 Code。
- 模板变量是否是 `${code}`。
- 阿里云账户是否欠费。
- RAM 用户是否有短信发送权限。
- 手机号是否触发阿里云风控。

系统自身限制：

- 同一 IP：每分钟最多 5 次发送短信。
- 同一手机号：5 分钟最多 3 次发送短信。
- 短信验证码有效期：5 分钟。
- 当前验证码保存在后端进程内存中。单机部署没问题；如果后续扩成多个 backend 实例，需要把验证码存储迁移到 Redis 或数据库，否则不同实例之间验证码不共享。

## 6. 首次启动

在项目根目录：

```bash
cd /opt/certificate-system
docker compose up -d --build
docker compose ps
```

看后端日志：

```bash
docker compose logs -f backend
```

看前端日志：

```bash
docker compose logs -f frontend
```

## 7. 初始化数据库

首次部署执行：

```bash
docker compose exec backend python -m app.cli init-db --admin-username admin --admin-password "请换成强密码"
```

这个命令会：

- 执行 Alembic 数据库迁移。
- 创建基础角色。
- 创建默认管理员。
- 初始化默认项目配置。
- 初始化系统设置，`PDF 生成并发限制` 默认值为 `2`。

以后升级代码时，一般执行：

```bash
docker compose exec backend alembic upgrade head
```

## 8. 配置 HTTPS 和域名

### 8.1 临时访问

如果只做内部验收，可以临时访问：

```text
http://服务器IP:8080/admin/login
```

此时安全组需要开放 `8080`。

### 8.2 正式访问

正式建议宿主机 Nginx 负责 HTTPS，反向代理到前端容器 `127.0.0.1:8080`。

宿主机 Nginx 示例：

```nginx
server {
  listen 80;
  server_name 你的域名;
  return 301 https://$host$request_uri;
}

server {
  listen 443 ssl http2;
  server_name 你的域名;

  ssl_certificate /etc/nginx/certs/fullchain.pem;
  ssl_certificate_key /etc/nginx/certs/privkey.pem;

  client_max_body_size 20m;

  location / {
    proxy_pass http://127.0.0.1:8080;
    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_connect_timeout 30s;
    proxy_send_timeout 120s;
    proxy_read_timeout 120s;
  }
}
```

检查并重载：

```bash
nginx -t
systemctl reload nginx
```

安全组建议开放：

```text
22   SSH，最好限制办公 IP
80   HTTP
443  HTTPS
```

正式环境不建议公网开放：

```text
3306 MySQL
8000 后端
8080 前端容器端口
```

如果宿主机 Nginx 已经代理到 `8080`，可以在安全组关闭公网 `8080`。

## 9. 上线验收

### 9.1 后台登录

访问：

```text
https://你的域名/admin/login
```

检查：

- 管理员账号可以登录。
- 默认密码已改为强密码。
- 进入“数据概览”没有 401、403、502。

### 9.2 项目管理

检查：

- 能新增项目。
- 能修改项目。
- 能启用/停用项目。
- 默认课程名称、阶段名称、发证单位保存后生效。

### 9.3 学员管理

检查：

- 能新增学员。
- 手机号格式正确。
- 能修改学员。
- 能删除测试学员。

### 9.4 证书管理和 PDF

检查：

- 能新增证书。
- 证书编号自动生成。
- 能预览证书。
- 能下载 PDF。
- 能按导入批次或 PDF 状态筛选证书，并勾选多张证书批量预生成 PDF。
- PDF 底图、姓名、日期、课程、导师、二维码清晰。
- 作废证书后不能下载正常 PDF。
- 重置链接后旧链接失效。

### 9.5 Excel 导入

检查模板字段：

```text
姓名
手机号
项目代码
课程开始日期
课程结束日期
发证日期
```

检查：

- 能下载模板。
- 模板“填写说明”页包含 `YYYY-MM-DD` 示例，课程结束日期早于开始日期时校验失败。
- 能上传 Excel。
- 错误数据能生成错误报告。
- 校验通过后能确认导入。
- 确认导入后能生成学员和证书。
- 已导入批次能点击“预生成PDF”，进度弹窗显示总数、已完成和失败数。
- 能下载导入原文件。

### 9.6 公开查询

检查：

- `/query` 可以打开。
- 证书编号 + 姓名 + 图形验证码可以查询。
- 手机号 + 短信验证码可以查询。
- 有效证书可以下载 PDF。
- 作废证书显示作废状态。
- 证书直达链接 `/cert/xxx` 可以打开。
- 二维码核验链接 `/verify/xxx` 可以打开。

## 10. 日常运维命令

查看容器：

```bash
docker compose ps
```

查看后端日志：

```bash
docker compose logs -f backend
```

查看前端日志：

```bash
docker compose logs -f frontend
```

重启后端：

```bash
docker compose restart backend
```

重启全部服务：

```bash
docker compose restart
```

更新代码：

```bash
cd /opt/certificate-system
git pull
docker compose up -d --build
docker compose exec backend alembic upgrade head
```

停止服务：

```bash
docker compose down
```

不要随便执行：

```bash
docker compose down -v
```

`-v` 会删除 Docker volume，可能导致 Compose 内置 MySQL 数据丢失。

## 11. 备份

### 11.1 后台备份管理

正式后台已经提供可视化备份入口：

- 登录后台。
- 进入「备份管理」。
- 点击「立即备份」。
- 页面会列出本地已有数据库备份、备份时间、文件大小和来源。
- 需要恢复时，点击对应备份的「回滚」，按弹窗输入确认语。

后台创建的数据库备份保存在：

```text
/data/certificate-system/db-backups
```

回滚前系统会自动再创建一份当前数据库备份，来源显示为「回滚前自动备份」。这份备份是为了防止选错时间点后还能退回。

### 11.2 Compose 内置 MySQL 命令兜底

一般不用手写命令。只有在后台打不开、需要紧急从服务器层面导出数据库时，再使用下面的兜底方式：

```bash
mkdir -p /data/backups/mysql
cd /opt/certificate-system
docker compose exec -T mysql mysqldump -ucertificate -p你的业务库密码 certificate_system > /data/backups/mysql/certificate_system_$(date +%F).sql
```

如果后续仍希望服务器自动每天备份，可以加定时任务：

```bash
crontab -e
```

加入：

```cron
30 2 * * * cd /opt/certificate-system && docker compose exec -T mysql mysqldump -ucertificate -p你的业务库密码 certificate_system > /data/backups/mysql/certificate_system_$(date +\%F).sql
10 3 * * * find /data/backups/mysql -name "certificate_system_*.sql" -mtime +30 -delete
```

### 11.3 RDS MySQL

建议：

- 开启自动备份。
- 上线前手动创建一次快照。
- 大版本升级前手动创建一次快照。
- 保留至少 7 到 30 天备份。

### 11.4 文件数据

需要备份：

```text
/data/certificate-system/uploads
/data/certificate-system/error-reports
/data/certificate-system/exports
```

可以不做长期备份：

```text
/data/certificate-system/pdf-cache
```

PDF 缓存可以重新生成，核心数据仍以数据库和上传原文件为准。

## 12. 回滚

上线前记录当前版本：

```bash
cd /opt/certificate-system
git rev-parse HEAD
```

普通代码回滚：

```bash
cd /opt/certificate-system
git checkout 上一个稳定commit
docker compose up -d --build
```

如果本次上线执行过数据库迁移，先不要盲目回滚数据库。正确顺序：

1. 上线前先备份数据库。
2. 如果代码回滚后仍能兼容新表结构，只回滚代码。
3. 如果迁移破坏兼容性，从备份恢复数据库。

恢复 Compose MySQL 备份示例：

```bash
cat /data/backups/mysql/certificate_system_YYYY-MM-DD.sql | docker compose exec -T mysql mysql -ucertificate -p你的业务库密码 certificate_system
```

如果后台可以正常打开，优先使用后台「备份管理」里的可视化回滚，不要直接执行恢复命令。

## 13. 常见问题排查

### 13.1 页面打不开

```bash
docker compose ps
docker compose logs -f frontend
```

检查：

- 前端容器是否启动。
- 服务器安全组是否开放 80/443。
- 域名是否解析到服务器。
- 宿主机 Nginx 是否重载成功。

### 13.2 后台接口 502

```bash
docker compose logs -f backend
docker compose ps
```

检查：

- backend 容器是否启动。
- 数据库是否连接成功。
- `DATABASE_URL` 是否正确。
- MySQL 密码是否和 `docker-compose.yml` 一致。

### 13.3 登录后 401 或 403

检查：

- `.env` 的 `SECRET_KEY` 是否重启前后变化。
- 管理员账号是否 active。
- 角色是否存在。
- 是否执行过 `init-db`。

### 13.4 PDF 中文乱码或不清楚

检查：

- `backend/Dockerfile` 是否安装 `fonts-noto-cjk`。
- `backend/app/assets/certificate-template.png` 是否存在。
- 是否重建了后端镜像。

命令：

```bash
docker compose exec backend ls -lh /app/app/assets
docker compose up -d --build backend
```

### 13.5 短信收不到

先确认是不是还在本地模式：

```bash
docker compose exec backend env | grep SMS_MODE
```

如果输出：

```text
SMS_MODE=local
```

说明不会真实发送短信，只会打印到日志。

如果是阿里云模式：

```bash
docker compose logs -f backend | grep -E "阿里云短信|未安装依赖|发送失败"
```

重点检查签名、模板、AccessKey、账户余额和 RAM 权限。

## 14. 交接清单

交接给运维或客户前，至少交付：

- 服务器 IP。
- 域名。
- HTTPS 证书位置。
- 项目部署目录：`/opt/certificate-system`。
- 数据目录：`/data/certificate-system`。
- 数据库类型：Compose MySQL 或 RDS。
- 数据库备份策略。
- 管理员账号，不要明文写在文档里。
- `.env` 变量说明，不要泄露真实密钥。
- 阿里云短信签名名称和模板 Code。
- 阿里云 RAM 用户权限说明。
- 回滚版本 commit。
