# PAC 服务器部署 + 数据摄入手册

> **W4 起试部署:服务器架构 + 数据从 DW 一次冷启 → 日级增量自动跑 → 客服可见的全链路 SOP**
> **目标读者**:运维 / 后端值班
> **状态**:🟢 W4 末投产基线版

---

## 一、服务架构(单机最小版)

```
┌────────────────────────────────────────────────────────────────┐
│ Server(2 核 8G 起步,生产建议 4 核 16G + SSD)                  │
│                                                                 │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐             │
│  │ Postgres 15 │  │  Redis 7    │  │  pac-web    │             │
│  │  :5532      │  │  :6479      │  │  :3100      │  Next.js    │
│  │  pac DB     │  │ BullMQ /    │  │             │   prod build│
│  │             │  │ session     │  │             │             │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘             │
│         │                │                │                     │
│         └────────────────┼────────────────┘                     │
│                          │                                       │
│                  ┌───────▼────────┐                              │
│                  │ pac-service    │  NestJS                      │
│                  │  :3101         │   - HTTP API                 │
│                  │                │   - BullMQ Worker            │
│                  │                │   - Cron(增量同步/监控)    │
│                  └───────┬────────┘                              │
└──────────────────────────┼─────────────────────────────────────┘
                           │ TCP/9000(ClickHouse 协议)
                           ▼
              ┌────────────────────────┐
              │ 瑞尔 DW(阿里云 ADS)    │
              │ cc-xxx.clickhouse.aliyuncs.com:9000 │
              │ dw_group.fact_*_out     │
              └────────────────────────┘
```

### 组件清单

| 组件 | 端口 | 用途 | 数据敏感度 |
|---|---|---|---|
| Postgres 15 | 5532 | PAC 主数据(patients / facts / personas / plans / sync_logs) | 🔴 PII |
| Redis 7 | 6479 | BullMQ 队列 + 会话 | 🟡 session token |
| pac-service | 3101 | API + Cron + Worker | — |
| pac-web | 3100 | 前端(Next.js) | — |
| (远程)瑞尔 DW | 9000 | 数据源(只读),PAC 主动连 | 🔴 PII(只读) |

**NOT 部署在 PAC 这边**:
- 瑞尔 DW(host 那边维护)

---

## 二、首次部署 SOP

### 2.1 环境准备

```bash
# 1. Docker / Docker Compose
docker --version  # >= 24
docker compose version

# 2. Node.js 跟 pnpm(若不用 docker 跑 pac-service)
node --version  # >= 20
pnpm --version  # >= 9
```

### 2.2 拉代码 + 配 env

```bash
git clone <repo> /opt/pac && cd /opt/pac
cp apps/pac-service/.env.example apps/pac-service/.env
```

**必填 env**(`apps/pac-service/.env`):

```ini
# ─── 基础 ───
NODE_ENV=production
PORT=3101
LOG_LEVEL=info
DATABASE_URL=postgresql://pac:<pwd>@localhost:5532/pac?schema=public
REDIS_URL=redis://localhost:6479
JWT_SECRET=<openssl rand -hex 32>
JWT_REFRESH_SECRET=<openssl rand -hex 32>
CORS_ORIGINS=https://<your-pac-web-domain>

# ─── DW 直连 ───
JVS_DW_CLICKHOUSE_PASSWORD=<DW 团队给的密码>
# manifest.yaml 里 password_env: JVS_DW_CLICKHOUSE_PASSWORD

# ─── AI(DeepSeek)───
DEEPSEEK_API_KEY=<vendor 给的 key>
DEEPSEEK_BASE_URL=https://api.deepseek.com
AI_DEFAULT_MODEL=deepseek-v4-flash

# ─── 增量同步定时 ───
PAC_INCREMENTAL_CRON=30 2 * * *           # 每天 02:30(DW 02:00 刷完后)
PAC_INCREMENTAL_HOSTS=jvs-dw              # 多 host 用逗号分隔
PAC_INCREMENTAL_DATA_DIR=/opt/pac/apps/pac-service/data

# ─── 数据滞后告警阈值 ───
PAC_LAG_WARN_HOURS=24                     # > 24h 黄色 WARN
PAC_LAG_ERROR_HOURS=48                    # > 48h 红色 ERROR
```

### 2.3 起 Postgres + Redis

```bash
docker run -d --name pac-postgres -p 5532:5432 \
  -e POSTGRES_USER=pac -e POSTGRES_PASSWORD=<pwd> -e POSTGRES_DB=pac \
  -v pac-pg-data:/var/lib/postgresql/data postgres:15

docker run -d --name pac-redis -p 6479:6379 \
  -v pac-redis-data:/data redis:7-alpine
```

### 2.4 构建 + 启动

```bash
cd /opt/pac
pnpm install --frozen-lockfile
pnpm --filter @pac/types build
pnpm --filter @pac/service exec prisma migrate deploy
pnpm --filter @pac/service exec prisma seed   # 写入 demo / jvs-dw host 行
pnpm --filter @pac/service build              # 编译 NestJS
pnpm --filter @pac/web build                  # Next.js prod build

# 起 pac-service(systemd / pm2 二选一)
cd apps/pac-service && node --enable-source-maps dist/src/main &

# 起 pac-web
cd apps/pac-web && pnpm start &
```

---

## 三、数据摄入 SOP

### 3.1 首次冷启动(Cold Import)— 把全量历史拉进 PAC

**用途**:首次部署 / 重建库 / yaml 改大版本时

**配置**:`apps/pac-service/data/jvs-dw/manifest.yaml` 里 `sql_source.queries` 各表 SQL。Dev 期间带 `LIMIT 100 OFFSET 100` 控量,**生产首跑前必须去掉 LIMIT**。

```bash
cd /opt/pac/apps/pac-service
pnpm cold-import -- --dir=./data/jvs-dw
# → 患者 100% 拉 / facts 全量 / 各表去重
# 预估:13 万 patients ≈ 10-30 min,内存峰值 ~2GB
```

**verify**:
```sql
SELECT 'patients' AS t, count(*) FROM patients
UNION ALL SELECT 'facts', count(*) FROM patient_facts
UNION ALL SELECT 'failed_in_sync_log', failed FROM sync_logs ORDER BY started_at DESC LIMIT 1;
-- failed 应当 = 0
```

冷启完成后,**手动触发一次 persona + plan**:
```bash
pnpm recompute-persona -- --host=jvs-dw
pnpm recompute-plans -- --host=jvs-dw
```

### 3.2 日级增量(Incremental Sync)— 自动跑,无需人工

**机制**:`SyncIncrementalSchedulerService` `@Cron` 注解,服务起来自动注册。

```
每天 02:30(可改 PAC_INCREMENTAL_CRON env)
   ↓
ColdImportService.importDirectory({ incremental: true })
   - 读 sync_logs 上次 cursor_after → SQL 注入 WHERE updated_date > '...'
   - 拉到新/改的 fact 行 + 反向拉主档(C 方案)+ stub auto-create(A 方案)
   - 写新 cursor 到 sync_logs
   ↓
Persona recompute(本次涉及的 distinct patient)
   ↓
Plan recompute(per tenant — SQL 召回 + 6 因子打分)
   ↓
Sync log status=success,带每表 cursor_after JSON
```

**结果可见**:DW 今天的新数据 → 明天早晨 03:00 ± 客服工作台可见。

**手动触发**(debug / 紧急补跑):
```bash
pnpm sync-incremental -- --dir=./data/jvs-dw
# 跳过 persona/plan(只拉数据):
pnpm sync-incremental -- --dir=./data/jvs-dw --no-recompute
# dry-run(不写库,看 SQL cursor 注入预览):
pnpm sync-incremental -- --dir=./data/jvs-dw --dry-run
```

### 3.3 数据完整性保证 — A+C 双修

**A:Patient Stub Auto-Create**
- 增量拉 fact 但 patient 主档 cursor 没动 → 用 `(hostId, tenantId, externalId)` 三段建空 stub
- 真实主档 upsert 时填上姓名/电话(W4 加固)

**C:反向拉主档**
- 每次跑完 fact 表,收集 `(patient_id, brand)` 集合 → 追加 `WHERE (patient_id, brand) IN (...)` 强拉主档
- 跟 fact 同 run,主档跟事实 100% 同步

**实测**(W3 末):
- 修前:51% transactions patient_id 为 null
- 修后:**0%** patient_id 为 null

---

## 四、监控 / 告警

### 4.1 DW 数据滞后监控

`DwLagMonitorService` `@Cron(HOURLY)` 自动跑:

```
读最近一次 incremental_bundle.cursor_after JSON
   ↓
算 max(cursor) vs now() 差 (小时)
   ↓
🟢 < 24h    LOG info
🟡 24-48h   LOG warn  (DW 没刷新,提醒值班)
🔴 > 48h    LOG ERROR (严重滞后,事件可能漏召)
```

阈值改 env:`PAC_LAG_WARN_HOURS` / `PAC_LAG_ERROR_HOURS`。

**接告警通道**(W5+ TODO):log ERROR 后接 webhook → 钉钉/飞书。当前阶段值班看 `journalctl -u pac-service | grep dw-lag` 即可。

### 4.2 BullMQ Bull Board(看队列健康度)

`/admin/queues` 路径(需登录 admin),实时看:
- persona-recompute / plan-recompute / plan-asset-generate 三队列
- 各 worker 处理速度 / 失败重试 / 死信

### 4.3 关键 SQL 检查

```sql
-- 最近一次 sync 结果(应当 success / failed=0)
SELECT to_char(started_at,'YYYY-MM-DD HH24:MI') AS t, resource, status, fetched, failed, error_message
FROM sync_logs ORDER BY started_at DESC LIMIT 5;

-- 数据规模
SELECT 'patients' AS k, count(*) FROM patients
UNION ALL SELECT 'facts', count(*) FROM patient_facts
UNION ALL SELECT 'active_plans', count(*) FROM followup_plans WHERE status IN ('active','assigned');

-- 数据滞后(跟 monitor 一致)
SELECT to_char(started_at,'HH24:MI') AS sync_t, cursor_after
FROM sync_logs WHERE resource='incremental_bundle' AND status='success'
ORDER BY started_at DESC LIMIT 1;
```

---

## 五、运维常见操作

| 场景 | 命令 |
|---|---|
| **重启 pac-service** | `systemctl restart pac-service`(或 pm2 reload pac-service) |
| **手动补跑昨日增量** | `pnpm sync-incremental -- --dir=./data/jvs-dw` |
| **某 patient 召回不对,临时单刷** | `pnpm recompute-plans -- --host=jvs-dw`(全 host)/ 详情页 "刷新" 按钮(单 patient) |
| **看某 patient 治疗链** | `pnpm exec ts-node src/cli/verify-chain.cli.ts --id=<uuid>` |
| **改 yaml/scenario 后强制重算 plan** | 删旧 plan: `TRUNCATE followup_plans CASCADE;` + `pnpm recompute-plans -- --host=jvs-dw` |
| **改 yaml 后强制 reparse 旧数据** | (task #46 待做)目前只能 truncate facts + 重 cold-import |

---

## 六、Yaml 变更管理 SOP

数据/术语 yaml 改了(`apps/pac-service/data/<host>/`)?

| 改动类型 | 影响 | 处理 |
|---|---|---|
| **enum_mapping 加 host 术式** | 新数据生效,老数据需要 reparse | 配 task #46 / 暂时 truncate facts 重导 |
| **field_mapping 改字段名** | 同上 | 同上 |
| **transforms 改 split/derive** | 影响管道 | 同上 |
| **scenario SQL 改召回口径** | 立即生效(下次 cron 跑就用) | 删旧 plan + 等 cron 或手动 recompute-plans |
| **canonical-codes.ts 改字典** | 立即生效(chain composer 用) | 重启 pac-service |

---

## 七、灾备 / 回滚

### 7.1 数据备份

```bash
# 每天凌晨 04:00 备份(增量 03:30 完成后)
pg_dump -U pac -d pac -F c -f /backup/pac-$(date +%F).dump
# 保留 7 天
find /backup -name 'pac-*.dump' -mtime +7 -delete
```

### 7.2 回滚代码版本

```bash
cd /opt/pac && git fetch && git checkout <last-known-good-tag>
pnpm install && pnpm --filter @pac/types build && pnpm --filter @pac/service build
systemctl restart pac-service
```

### 7.3 cursor 倒退(数据要重跑某窗口)

```sql
-- 把最近一次 sync_log 的 cursor_after 改到目标时间
UPDATE sync_logs SET cursor_after = '{"fact_emr_treatment_out":"2026-04-01",...}'
WHERE id = (SELECT id FROM sync_logs WHERE resource='incremental_bundle' AND status='success' ORDER BY started_at DESC LIMIT 1);
```

下次 cron 自动从这个 cursor 拉。

---

## 八、当前已知边界(W4 末)

| 项 | 现状 | 影响 | 何时解决 |
|---|---|---|---|
| 首跑全量 cohort LIMIT 100 | dev manifest 里写死 LIMIT | 生产首跑必须去掉 LIMIT(可能 30 min 跑 13 万 patient) | 部署前手动改 |
| sync-incremental reparse 模式缺 | yaml 改后历史数据需要 truncate 重导 | 改 yaml 不能"原地补"老 facts | task #46(W5+) |
| 告警接通道 | log ERROR + 人工看 | 滞后无即时告警 | W5+ 加 webhook 钉钉/飞书 |
| 多 host 部署 | 支持(`PAC_INCREMENTAL_HOSTS=jvs-dw,friday`)| — | 已就绪 |
| 单机 vs 集群 | 当前单机 | 高 QPS 时 pac-service 可水平扩展(无状态) | 试点期不需要 |

---

## ▎一句话归纳

> **PAC 部署 = Postgres + Redis + pac-service + pac-web 四件套。数据摄入 = 首次 cold-import(一次性)+ 日级 sync-incremental(自动 cron)。监控 = lag monitor + Bull Board + 日常 SQL 检查。**
