---
title: 部署速查(本地 / 测试 / 生产)
description: 三套环境的连接方式、部署命令、部署后动作与踩过的坑 —— 照着敲就能部署。
icon: Terminal
---

三套环境**同一套代码、同一个部署脚本**,差别只在:**在哪台机、哪个分支、哪份 `.env`**。
架构与首次搭建见 [部署](/docs/deployment);本文是日常部署的操作手册。

---

## 零、三套环境速查

| | 本地 | 测试 | 生产 |
|---|---|---|---|
| 位置 | 自己电脑 | `47.251.104.47` | `47.99.62.30` |
| 目录 | `~/gitlab/pac` | `~/pac` | `/data/pac` |
| 分支 | 任意 | **test** | **main** |
| DB / Redis | docker 本地 | docker 同机 | **阿里云托管 RDS** |
| 部署方式 | `pnpm dev` 原生跑 | `deploy-prod.sh` | `deploy-prod.sh` + `COMPOSE_MANAGED=1` |
| 域名 | localhost | — | `pac.friday.tech` |

**分支规约**:`main` = 生产(只接受合并,不直接改)· `test` = 测试(可聚合测试专用代码,**永不回流 main**)· 开发分支 → main。
部署脚本**不写死分支**,部署的是"该机当前 checkout 的分支",所以生产机停在 main、测试机停在 test 即可。
需要临时部署别的分支(或首次把某台机切到某分支)用 `DEPLOY_BRANCH` 覆盖:
`DEPLOY_BRANCH=test bash deploy/deploy-prod.sh`。

---

## 一、本地开发

基础设施用 docker,应用**原生跑**(不要用 docker 跑 service/web,依赖变更会踩坑)。

```bash
# 1) 基础设施(只需第一次 / 重启后)
docker start pac-postgres pac-redis pac-clickhouse
#   没有容器就 docker compose -f docker-compose.dev.yml up -d

# 2) 装依赖 + 建类型包(切换分支后也要重跑)
pnpm install
pnpm --filter @pac/types build

# 3) 起服务(各开一个终端)
pnpm --filter @pac/service dev      # :3101
pnpm --filter @pac/web dev          # :3100
pnpm --filter @pac/docs dev         # :3102(文档站,可选)
```

打开 [http://localhost:3100](http://localhost:3100) → 会弹**快速登录**(模拟身份,选角色 + 数据范围)。

<Callout type="warn">
**别用根目录 `pnpm dev`** —— 它会一起拉起 pac-docs,docs 挂掉会连坐终止其他服务。
</Callout>

**本地常见问题**

| 症状 | 原因 / 处理 |
|---|---|
| 页面提示「登录已过期」且没有快速登录弹窗 | pac-service 没起来 → 看它的终端报错 |
| service 起不来,报 `ECONNREFUSED 5532/6479` | docker 基础设施没起 → `docker start pac-postgres pac-redis` |
| 文档站新增文件 404 / 整树 500 | 清缓存重建:`rm -rf apps/pac-docs/.next apps/pac-docs/.source && pnpm --filter @pac/docs exec fumadocs-mdx` |
| 切分支后 service 编译报缺模块 | `pnpm install && pnpm --filter @pac/types build` |

---

## 二、测试环境

```bash
# 本地:把要测的代码合进 test 并推送
git checkout test
git pull origin test
git merge <你的开发分支>        # 或 git merge main(同步生产代码)
pnpm --filter @pac/types build && pnpm --filter @pac/service exec tsc --noEmit   # 自查
git push origin test

# 部署
ssh -p 19822 root@47.251.104.47 'cd ~/pac && bash deploy/deploy-prod.sh'
```

脚本自己会 `git pull`,跑完打印 **部署成功 ✅ + commit** 才算数。

---

## 三、生产环境

```bash
# 本地:开发分支 → main(禁止用 test 合 main)
git checkout main
git pull origin main
git merge <你的开发分支>
pnpm --filter @pac/types build && pnpm --filter @pac/service exec tsc --noEmit
pnpm --filter @pac/service exec jest                # 全量测试
git push origin main

# 部署(注意 COMPOSE_MANAGED=1,托管 RDS/Redis 必须带)
ssh -p 3335 -i ~/.ssh/qi.luo.pem root@47.99.62.30 \
  'cd /data/pac && COMPOSE_MANAGED=1 bash deploy/deploy-prod.sh'
```

生产脚本比测试多一道 **验证 3b:私网 IP 可达** —— 拦"只绑 loopback 导致网关 502"的老坑。
四道验证(镜像一致 / 迁移无 pending / health 200 / 私网 200)全过才算成功。

<Callout type="warn">
**手动跑 docker compose 前必须先 source .env**,否则 DB 口令 fallback 成 `pac` → pac-service crash-loop:

```bash
cd /data/pac && set -a && . apps/pac-service/.env && set +a
docker compose -f docker-compose.prod.yml -f docker-compose.managed.yml <命令>
```

用 `deploy-prod.sh` 部署则不用管,脚本已处理。
</Callout>

---

## 四、部署后要不要跑重算?

**大多数改动不用**。按改了什么对照:

| 改了什么 | reparse | persona | plan |
|---|---|---|---|
| 纯前端 / UI | — | — | — |
| assembler yaml(字段映射、口径) | ✅ | ✅ | — |
| persona 特征算法 | — | ✅ | ✅ |
| 召回引擎 / 优先级 / 诊所归属 | — | — | ✅ |
| 话术 / 简报 prompt | — | — | — |
| 定向补数据(污染修复等) | — | ✅ | ✅ |

> 改了话术 / 简报 prompt 不用重算,但**已缓存的旧内容不会自动重生成** ——
> 要立刻全量换新,清一次缓存即可(下次访问按需重生成):
> `delete from plan_summaries where type='recall_brief';`

**顺序固定:reparse → persona → plan**(persona 读事实、plan 读 persona)。

```bash
# 进容器跑(生产;测试环境去掉 managed.yml 那段)
cd /data/pac && set -a && . apps/pac-service/.env && set +a
DC="docker compose -f docker-compose.prod.yml -f docker-compose.managed.yml"

# ① reparse:改了 yaml 口径才要(先 --dry-run 看量)
$DC exec -T pac-service node --max-old-space-size=8192 \
  dist/cli/reparse.cli.js --host=jvs-dw --subject-type=payment,refund --no-recompute

# ② persona 全量重算
$DC exec -T pac-service node --max-old-space-size=8192 \
  dist/cli/recompute-persona.cli.js --host=jvs-dw

# ③ plan 全量重算
$DC exec -T pac-service node --max-old-space-size=8192 \
  dist/cli/recompute-plans.cli.js --host=jvs-dw
```

**耗时参考(生产 40 万患者)**:reparse ~2h · persona ~4.5h · plan ~1h。
用 `nohup ... &` 后台跑,别挂在 SSH 上。

---

## 五、定向补数据(只重摄部分患者)

数据污染修复、补摄特定患者时用。**不会推进增量游标**(名单外患者的水位不受影响)。

```bash
# 名单:每行一个 DW patient_id
$DC cp /tmp/pids.txt pac-service:/tmp/pids.txt

$DC exec -T \
  -e PAC_COHORT_ONLY_PATIENT=@/tmp/pids.txt \
  -e PAC_COHORT_CONCURRENCY=4 \
  -e PAC_COHORT_BATCH_SIZE=2000 \
  pac-service node --max-old-space-size=8192 dist/cli/cold-import.cli.js --dir=./data/jvs-dw
```

| 参数 | 建议 | 说明 |
|---|---|---|
| `PAC_COHORT_ONLY_PATIENT` | `@文件` | 大名单必须走文件(环境变量单值上限 128KB) |
| `PAC_COHORT_CONCURRENCY` | `4` | 按 CPU 核数;连接池自动跟随 |
| `PAC_COHORT_BATCH_SIZE` | `2000` | **别超 8000** —— ClickHouse 单条 SQL 上限 256KB |

**名单超过 8000 个要分批**:`split -l 2000 -d --additional-suffix=.txt pids.txt batch_` 然后循环跑。
跑完记得按上一节做 persona + plan 重算。

---

## 六、部署失败怎么办

脚本任一验证不过会**非 0 退出**,不会留下半吊子状态。

```bash
# 看服务日志
$DC logs --since 10m pac-service | tail -50

# 看容器状态
$DC ps

# 回滚:切回上一个 commit 重新部署
cd /data/pac && git log --oneline -5      # 找到要回滚的 commit
git checkout <commit>
COMPOSE_MANAGED=1 bash deploy/deploy-prod.sh --no-pull   # ⚠️ 必须带 --no-pull
```

<Callout type="warn">
回滚**一定要带 `--no-pull`**:不带的话脚本会先 `git fetch + pull` 当前分支,
把你刚 checkout 的旧 commit 又拉回分支最新 —— 等于回滚没生效(旧版脚本会**静默**发生;
现版本在 detached HEAD 下会取到分支名 `HEAD` 导致 fetch 失败退出,至少不会假成功)。
回滚完记得 `git checkout main` 切回来,否则下次部署仍在 detached HEAD。
</Callout>

**数据层回滚**:改 yaml 口径的可以改回去再 reparse 一遍(`rawPayload` 一直保留,可逆);
删除类操作不可逆,执行前务必先 `--dry-run` 或 `select count(*)` 确认范围。

---

## 七、容易踩的坑

| 坑 | 后果 | 避免 |
|---|---|---|
| 生产手动 compose 忘了 source .env | DB 口令 fallback → crash-loop | 用 `deploy-prod.sh`,或先 `set -a && . apps/pac-service/.env` |
| `pkill -f "xxx"` 在 SSH 命令里 | **模式匹配到自己的命令行 → 杀掉自己的会话**,后续命令不执行 | 用更精确的模式,或分两条命令 |
| 用 test 分支合 main | 测试专用代码混进生产 | 开发分支 → main;main → test 单向 |
| 定向重摄名单 > 8000 | ClickHouse `Max query size exceeded` | 分批 2000 |
| 长任务挂在 SSH 前台 | 断连即中断 | `nohup ... &` + 日志文件 |
| 部署完就跑重算 | 和增量 cron 抢锁(会被 skip,不丢数据但延迟) | 避开整点后 15 分,或接受跳过一轮 |

---

## 八、日常巡检

```bash
# 健康
curl -s -o /dev/null -w "%{http_code}\n" https://pac.friday.tech/api/health

# 最近同步(看 status / 耗时)
$DC exec -T postgres psql -U pac -d pac -c \
  "select started_at::timestamp(0), extract(epoch from (ended_at-started_at))::int as 秒, status, left(triggered_by,40) from sync_logs order by started_at desc limit 5;"

# 增量 cron 有没有注册上
$DC logs --since 1h pac-service | grep "sync-incremental"
```

增量 cron 当前为 **每 2 小时**(`15 0,8-22/2 * * *`,北京时间)。被并发锁 skip 是正常的,
游标不推进 + 48h 回看窗兜底,数据不会丢。
