# PAC 部署运维手册

生产用 **docker-compose 模式**(单服务器隔离)。所有部署/运维走 `deploy/` 下三个脚本,不手敲 compose。

- 代码根:**任意目录**(clone 到哪都行 —— 脚本按自身位置定位仓库,compose 用相对 context,均不写死路径;下文示例里的 `/opt/pac` 只是举例,换成你的路径即可)
- 配置:`apps/pac-service/.env` + `apps/pac-web/.env`(单一真相,`chmod 600`,永不进 git)
- compose 文件:`docker-compose.prod.yml`

| 脚本 | 干什么 | 幂等 |
|---|---|---|
| `deploy/gen-env.sh` | 生成 `.env`(密钥随机 + 生产开关 + 业务值注入 + 缺失报告) | 拒覆盖已有(`FORCE=1` 强制) |
| `deploy/deploy-prod.sh` | 拉码 + build + 重建容器 + 硬验证(镜像/迁移/health) | ✅ 反复跑 |
| `deploy/first-import.sh` | 数据首灌:seed + 存量 + 重算画像/计划 | ✅ 可重跑 |

---

## 1. 日常:代码更新部署(最常用)

```bash
ssh <server> && cd /opt/pac
bash deploy/deploy-prod.sh
```

`git pull` → build → force-recreate → 验证(镜像 ID 一致 / 迁移无 pending / health 200 / web 200),任一不过即失败。只动代码,**不碰数据**。

⚠️ **期间有约 16 秒停机**(2026-08-04 实测三次:15.97 / 16.86 / ~16s,0.5s 间隔探 `/health`)。
`force-recreate` **不是**滚动更新 —— 它就是停旧容器、起新容器,中间必然有真空期。
build 阶段不影响服务(老容器一直跑),停机只发生在重建那一刻,所以**部署总耗时长短与停机无关**。

影响:那 16 秒内 API 报错、工作台加载失败,刷新即恢复;登录态不掉(JWT 在浏览器),无数据风险
(迁移由 pac-migrate 单独跑完才起 service,写操作要么已完成、要么失败可重试)。建议挑非高峰时段。

要真正零停机得引入网关层蓝绿切换(两套容器 + 上游切换),不在本脚本范围内。

> 改了 `NEXT_PUBLIC_*`(前端 build-time 变量)必须走本脚本重 build;只改后端 `.env` 用 `docker compose -f docker-compose.prod.yml restart pac-service` 即可。

---

## 2. 新环境从零上线(换服务器 / 首次生产)

前置:装 docker;起或接托管 Postgres+Redis;`git clone` 到 `/opt/pac`。

```bash
cd /opt/pac

# ① 生成 env —— 业务值一次传进去,JWT 密钥就地随机,生产开关自动设
PAC_DOMAIN=pac.<域名> \
POSTGRES_PASSWORD='<强密码>' REDIS_URL='redis://redis:6379' \
DW_CLICKHOUSE_URL='http://<ch>:8123' DW_CLICKHOUSE_USERNAME='jvs_pac' DW_CLICKHOUSE_PASSWORD='<密码>' \
DEEPSEEK_API_KEY='sk-...' ALERT_WEBHOOK_URL='https://qyapi.weixin.qq.com/...' \
bash deploy/gen-env.sh
# 末尾报 __FILL_ME__ 的项要补齐(或带全环境变量 + FORCE=1 重跑)

# ② 部署 App(容器都只绑 127.0.0.1,还不能公网访问)
bash deploy/deploy-prod.sh

# ③ 反向代理(nginx)—— compose 全绑本地,必须加反代做 TLS + 路由,见下方「反向代理」

# ④ 数据首灌(增量自动注册,不用管)
HOST=jvs-dw DRY_RUN=1 bash deploy/first-import.sh   # 先 dry-run 验翻译
HOST=jvs-dw            bash deploy/first-import.sh   # 真灌前:去掉 manifest cohort 的 LIMIT/OFFSET

# ⑤ 给宿主换票凭据(appId 稳定不变 + 轮换出的新 appSecret,一起交给对方后端)
docker compose -f docker-compose.prod.yml exec pac-service \
  node dist/cli/host-admin.cli.js rotate-secret jvs-dw
#   ↑ 只换 appSecret;appId 不变(查:… psql -tAc "SELECT name,app_id FROM hosts")
```

### 反向代理(nginx,必需)

compose 里 pac-service(3101)/ pac-web(3100)都只绑 `127.0.0.1`,不加反代无法公网访问。示例:

```nginx
server {
  listen 443 ssl;
  server_name pac.<域名>;
  ssl_certificate     /etc/ssl/pac/fullchain.pem;
  ssl_certificate_key /etc/ssl/pac/privkey.pem;

  # 后端 API(pac-service)
  location /pac/ {
    proxy_pass http://127.0.0.1:3101;
    proxy_http_version 1.1;
    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_buffering off;               # SSE 流式端点(script:stream / summary:stream)必须关缓冲
    proxy_read_timeout 300s;
  }
  # 前端(pac-web,含 iframe 嵌入)
  location / {
    proxy_pass http://127.0.0.1:3100;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    # 不要设 X-Frame-Options DENY —— PAC 要被宿主 iframe 嵌;
    # 嵌入白名单由应用侧 CSP frame-ancestors + CORS_ORIGINS 控制。
  }
}
```
TLS 证书自备(Let's Encrypt / 云证书);域名要和 `.env` 的 `PAC_DOMAIN` 一致。

### 托管 DB/Redis(阿里云 RDS + Redis,可选)

默认自带 PG/Redis(compose 内置)。要走托管:①gen-env 时传托管连接串,②部署时加 `COMPOSE_MANAGED=1`。应用代码零改动。

```bash
# ① 生成 env 时指向托管端点(不启自带 PG/Redis)
DATABASE_URL='postgresql://pac:<pwd>@<rds-host>:5432/pac?schema=public' \
REDIS_URL='redis://:<pwd>@<redis-host>:6379' \
PAC_DOMAIN=... DW_CLICKHOUSE_URL=... DEEPSEEK_API_KEY=... QWEN_API_KEY=... ALERT_WEBHOOK_URL=... \
bash deploy/gen-env.sh

# ② 部署叠加托管 override(docker-compose.managed.yml:不启自带 PG/Redis)
COMPOSE_MANAGED=1 bash deploy/deploy-prod.sh
# 同理增量/首灌等 compose 命令要托管时都加 -f docker-compose.managed.yml
```
⚠️ Redis 用**标准版(单机/主从)**,别用集群版(集群要改代码)。⚠️ RDS 与应用**同 VPC / 同可用区**(重算含大量 DB 往返,跨网络会明显变慢)。

**必须换成生产值**:`PAC_DOMAIN`、`DATABASE_URL`/`POSTGRES_PASSWORD`、`REDIS_URL`、`DW_CLICKHOUSE_*`、`DEEPSEEK_API_KEY`、`ALERT_WEBHOOK_URL`。
**红线**:JWT 两把密钥切勿复用其他环境(gen-env 已随机);生产 `PAC_ENABLE_MOCK_LOGIN=false`(gen-env 已设,开着=任何人可假冒登录)。PG 密码含 `@:/?#&` 会破坏 `DATABASE_URL`,用 URL 安全字符或直接传完整 `DATABASE_URL=`。

---

## 3. 常用运维

```bash
C="docker compose -f docker-compose.prod.yml"

$C ps                                   # 容器状态
$C logs -f pac-service                  # 跟日志
$C logs pac-service | grep -E "ERROR|dw-lag|会话失效"   # 错误 / 滞后 / 鉴权告警
$C restart pac-service                  # 只重启后端(不重建)
$C exec pac-service sh                  # 进容器

# 手动跑数据 CLI(按需)
$C exec pac-service node dist/cli/sync-incremental.cli.js              # 手动补一次增量
$C exec pac-service node dist/cli/recompute-persona.cli.js --host=jvs-dw
$C exec pac-service node dist/cli/refresh-clinic-names.cli.js          # 刷诊所名字典
```

**增量**由 pac-service 进程内调度器按各 `data/<host>/manifest.yaml` 的 `incremental_cron` 自动注册(jvs-dw = 每天 08:15 沪),部署后自动生效,无需外部 cron。

---

## 4. 回滚

```bash
cd /opt/pac
git log --oneline -10
git checkout <last-good-sha>
bash deploy/deploy-prod.sh
```

⚠️ **DB schema 只前向**:Prisma 迁移不回退,回滚老代码可能跟新 schema 不兼容。生产改 schema 前先评估前向兼容。

---

## 5. 监控

- **健康**:`curl -s localhost:3101/pac/v1/health`(应 200)
- **每日日报**:PAC哨兵 企微群(`ALERT_WEBHOOK_URL`)每天推,看 `摄入 近7日失败 0` = 绿灯
- **实时告警**:摄入失败 / DW 滞后 / 嵌入首开会话失效,自动进同一企微群
- **同步成功率**:`$C exec pac-service node -e "…"` 查 `sync_logs`,或看 `/admin/queues`(Bull Board)

数据/监控细节见 `apps/pac-docs/content/docs/operations/deployment.mdx`。

---

<details><summary>附:systemd 裸跑模式(仅历史 / 特殊场景)</summary>

宿主不想用 docker、直接 systemctl 管理时用 `deploy.sh` + `systemd/*.service`。生产统一走 compose,此模式不再维护,需要时见 git 历史或该目录旧文件。
</details>
