文件

533 行
21 KiB
Markdown

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# hyb-oa Server Runbook (P0/P1)
## 1. 环境准备
- Node.js: `>=22 <23`
- Docker + Docker Compose
- 本地工具:`curl`、`jq`
- 配置文件:从 `server/.env.example` 复制为 `server/.env` 并按需修改
```bash
cd server
cp .env.example .env
```
## 2. Gate 基线命令(必须顺序执行)
### 2.1 启动数据库
```bash
cd ..
export POSTGRES_PASSWORD="<local-only-db-password>"
export JWT_SECRET="<local-only-jwt-secret>"
docker compose up -d db
```
根级 Compose 不再内置数据库口令或 JWT 密钥;请仅在本地 shell 或未提交的环境文件中设置,并让 `server/.env` 的 `DATABASE_URL` 使用同一数据库口令。
### 2.2 应用迁移
```bash
cd server
npm run prisma:migrate:deploy
```
### 2.3 执行 seed(幂等)
```bash
cd server
SEED_DEV_ALLOW=1 SEED_DEV_DATABASE_CONFIRM=hyb_oa npm run seed:dev
```
说明:
- `seed:dev` 永久拒绝 production,只接受 loopback PostgreSQL,并要求确认 URL 中的数据库名。
- 开发账号资料和口令可通过 `SEED_*` 环境变量覆盖;脚本不会打印口令。
- 默认保留已有账号密码。新建账号以及主动设置 `SEED_DEV_RESET_PASSWORDS=1` 后轮换的账号,首次登录都必须改密。
- 开发账号配置不得复用于试运行或生产。
试运行管理员引导(E20):
```bash
cd server
TRIALRUN_ADMIN_EMAIL="<admin-email>" \
TRIALRUN_ADMIN_PHONE="<admin-phone>" \
TRIALRUN_ADMIN_PASSWORD="" \
npm run seed:trialrun
```
说明:
- `TRIALRUN_ADMIN_PASSWORD` 为空时会生成并打印一次性口令(请立刻保存)。
- 若管理员已存在,脚本只确保其为 active 且具备 `admin+employee` 角色,不会改动密码。
### 2.3.1 认证安全说明(P5)
- `POST /api/auth/login` 成功响应包含 `mustChangePassword`:
- `true`:表示该账号处于“首次登录/管理员重置密码后”状态;前端应强制引导改密后再进入业务页。
- 改密接口:`POST /api/auth/change-password`(需要 `authorization: Bearer <token>` + `x-device-fingerprint`)。
- 登录失败锁定(默认):
- 15 分钟内失败 5 次会锁定 30 分钟,返回 `423 AUTH_002`(含 `lockedUntil`)。
- 解锁:`POST /api/users/:id/unlock`(权限:`system.users`)。
- 弱密码拦截:
- 采用 `zxcvbn` 评分,默认 `score < 2` 拒绝(返回 `VALID_001.details.password`)。
- 可通过 `WEAK_PASSWORD_CHECK_ENABLED=false` 关闭(不建议生产关闭)。
### 2.3.2 邮件通知(可选,F15)
- 默认关闭:`EMAIL_ENABLED=false`
- 开启后支持触发邮件(失败降级不阻塞主流程):
- 新待办分配(workflow.task_assigned)
- 审批结果(approved/rejected/returned)
- 付款完成(reimbursement.paid)
- 密码重置(forgot-password / reset-password)
- 需要配置 SMTP:
- `SMTP_HOST`、`SMTP_PORT`(默认 587)、`SMTP_SECURE`(465 通常为 true)
- `SMTP_USER`、`SMTP_PASS`(按 SMTP 服务要求)
- `EMAIL_FROM`(发件人地址)
- `EMAIL_BASE_URL`(可选,邮件内“查看详情”链接的站点前缀;为空则尝试使用 `APP_ORIGIN` 的第一个值)
### 2.3.3 作业系统(cron vs pg-boss)(F19~F19d)
- 默认:`JOB_DRIVER=cron`(同进程 node-cron 调度)
- 可选升级:`JOB_DRIVER=pgboss`(Postgres 队列,适配单机/多进程;避免引入 Redis)
- `PG_BOSS_SCHEMA`(默认 `pgboss`)
- `JOBS_ENABLED=false` 可整体关闭作业(仅用于排障/紧急回滚)
- 目前迁移的作业:
- `ocr.tick`(受 `OCR_PROVIDER` 控制,off 时不调度)
- `delegation.revoke-expired`
- `invoice.archive-unused`
- 回滚策略:
- 将 `JOB_DRIVER` 切回 `cron`(无需 schema 迁移回滚,风险低)
### 2.3.4 Worker 进程拆分(可选,F20)
> 目的:单机也能用“API 进程 + Worker 进程”模式运行,便于未来横向扩展;不改变对外 API。
- API 进程:只提供 HTTP(可选托管 `client/dist`),**不跑作业**
- Worker 进程:只跑作业(OCR/委派回收/超期归档),**不监听端口**
本地双进程示例:
```bash
# 终端 A:API(不跑作业)
cd <repo-root>/server
JOBS_ENABLED=false npm run dev
# 终端 B:Worker(只跑作业)
cd <repo-root>/server
JOBS_ENABLED=true npm run worker
```
试运行 compose(推荐,见 `deploy/trialrun/runbook.md`):
- 启用 worker profile:`--profile worker`
- 同时将 server 的 `JOBS_ENABLED=false`(worker 进程内强制为 true)
### 2.3.5 存储驱动(local vs S3/MinIO)(F21)
> 目标:把“票据文件存储”从本地目录可选演进到 S3 兼容(单机优先 MinIO),保持签名 URL 契约不变。
- 默认:`STORAGE_DRIVER=local`(写入 `STORAGE_DIR`)
- 可选:`STORAGE_DRIVER=s3`(写入 S3/MinIO)
- 需要配置:`S3_ENDPOINT/S3_REGION/S3_BUCKET/S3_ACCESS_KEY_ID/S3_SECRET_ACCESS_KEY/S3_FORCE_PATH_STYLE`
- 为兼容混合存储,`invoice_pool.storage_path` 对 S3 会写成 `s3:<key>`(无 schema 变更)
签名 URL 兼容策略:
- `/api/invoice-pool/:id/signed-url` 仍返回本系统 URL(带 token)
- `/api/invoice-pool/:id/file` 内部按 `storage_path` 自动从本地或 S3 读取并返回文件流
### 2.4 启动服务
```bash
cd <repo-root>/server
npm run dev
```
### 2.5 冒烟验证
```bash
cd <repo-root>/server
./scripts/smoke-test.sh
```
### 2.6 本地最小门禁(P1-b)
```bash
cd <repo-root>
./scripts/check-minimal.sh
```
说明:
- 该脚本只做静态/构建级门禁,不启动服务与数据库。
- 全量回归仍通过 `server/scripts/regression-all.sh` 执行。
## 3. 1 分钟快速验证
```bash
cd <repo-root>/server
./scripts/smoke-test.sh
```
也可直接探活:
```bash
curl -fsS http://127.0.0.1:3000/health
curl -fsS http://127.0.0.1:3000/ready
curl -fsS http://127.0.0.1:3000/metrics | head
```
说明:
- `/health`:liveness(不查 DB)
- `/ready`:readiness(`SELECT 1` 探活 DB,失败返回 503)
- `/metrics`:Prometheus 指标(可通过 `METRICS_ENABLED=false` 关闭)
---
## 4. 回归入口
### 4.1 脚本回归(推荐)
```bash
cd <repo-root>/server
BASE_URL=http://127.0.0.1:3000 ./scripts/smoke-test.sh
```
### 4.2 判重专项回归(dedup)
```bash
cd <repo-root>/server
DATABASE_URL='<local-postgresql-url>' \
BASE_URL=http://127.0.0.1:3000 ./scripts/regression-dedup.sh
```
### 4.3 工作流动作回归
```bash
cd <repo-root>/server
./scripts/regression-workflow-actions.sh
```
可选并发校验:
```bash
cd <repo-root>/server
RUN_DELEGATION_CONCURRENCY_CHECK=1 ./scripts/regression-workflow-actions.sh
```
覆盖场景:
- `reject`:实例=`rejected`,报销=`rejected`
- `return`:实例=`terminated`,报销=`draft`
- `withdraw`:审批未处理前可撤回
- `return` 后二次 `submit`:创建新实例
- `PERM_002`:非 assignee 无法操作
- `no_actionable_approver`:全链无可执行审批人阻断
- `negative_not_allowed`:负金额防御
- `manager skip`:直属上级缺失时跳过并记录
- `transfer`:转审成功(任务改派 + 任务/转审记录/history 三处证据)
- `transfer` 参数校验:reason<10 返回 `VALID_002`
- `transfer` 同人转审:返回 `VALID_001`(`same_assignee_not_allowed`)
- `transfer` 链路上限:同 task 第 4 次转审阻断(`chain_limit_exceeded`)
- `transfer` 目标校验:目标审批人须 active 且具备 `expense.approve`
- `pay`:财务付款成功(`approved -> paid`,发票 `bound -> archived(used_complete)`)
- `pay` 权限:非财务返回 `PERM_001`
- `pay` 状态校验:非 `approved/paid` 返回 `VALID_001`
- `pay` 幂等:重复付款返回 200(`archivedInvoiceCount` 可为 0)
- `pay` 幂等冲突:已付款后提交冲突凭证字段返回 `409 VALID_001`(`already_paid_conflict`)
- `delegation`:有效期内可代办并在 history 写入 delegation 证据
- `delegation` 过期:代理人返回 `PERM_002`,自动回收任务可撤销过期委派
- `delegation` 并发回收(可选):`RUN_DELEGATION_CONCURRENCY_CHECK=1` 时双并发只回收一次并仅写一条自动回收审计
### 4.4 一键全量回归
```bash
cd <repo-root>/server
./scripts/regression-all.sh
```
串联 `smoke-test.sh + regression-dedup.sh + regression-workflow-actions.sh + regression-audit.sh + regression-ocr.sh`,适用于 CI 或发版前检查。
可选关闭审计/OCR:
```bash
cd <repo-root>/server
RUN_AUDIT_REGRESSION=0 RUN_OCR_REGRESSION=0 ./scripts/regression-all.sh
```
可选打开组织/配置回归(org + settings):
```bash
cd <repo-root>/server
RUN_ORG_SETTINGS_REGRESSION=1 ./scripts/regression-all.sh
```
可选打开认证安全回归(auth security):
```bash
cd <repo-root>/server
RUN_AUTH_SECURITY_REGRESSION=1 ./scripts/regression-all.sh
```
独立执行:
```bash
cd <repo-root>/server
./scripts/regression-audit.sh
OCR_PROVIDER=mock ./scripts/regression-ocr.sh
./scripts/regression-org-settings.sh
./scripts/regression-auth-security.sh
```
### 4.5 Postman 回归
- Collection: `server/postman/hyb-oa.postman_collection.json`
- Environment: `server/postman/hyb-oa.postman_environment.json`
- 核心链路:
1. Login Employee
2. Create Reimbursement
3. Upload Invoice
4. Bind Invoice
5. Submit Reimbursement
6. Login CEO
7. Approve Workflow Task
8. Get Signed URL
### 4.6 前端壳联调(P1-5)
```bash
cd <repo-root>/client
npm install
npm run dev
```
说明:
- 前端开发地址默认 `http://127.0.0.1:5173`。
- 通过 Vite 代理将 `/api` 转发到 `http://127.0.0.1:3000`。
- Access Token 仅保存在内存,Refresh Token 通过 HttpOnly Cookie 续签。
### 4.7 单域托管 client/dist(可选,试运行建议)
```bash
cd <repo-root>/client
npm run build
cd <repo-root>/server
SERVE_CLIENT_DIST=true npm run dev
```
说明:
- 访问 `http://127.0.0.1:3000` 即可打开前端(同域 cookie,减少 CORS 复杂度)。
- SPA 路由(如 `/dashboard`)会回退到 `index.html`;`/api/*` 不参与回退。
### 4.8 试运行部署包(Docker Compose)
试运行/上线演练建议使用仓库内置部署包:
- `deploy/trialrun/runbook.md`
- `deploy/trialrun/docker-compose.yml`
- `deploy/trialrun/.env.example`
- `deploy/trialrun/checklist.md`
备份/恢复脚本(E14):
```bash
cd <repo-root>
DB_TOOL_MODE=docker DB_CONTAINER=hyb-oa-trialrun-db ./scripts/backup-all.sh
./scripts/qa/run-backup-restore-drill.sh
```
成对备份会生成 DB、附件和脱敏 manifest。恢复脚本只允许显式影子目标;真实事故恢复、异机副本和 `/ready` 验收按 `deploy/trialrun/runbook.md` 的授权门执行,不直接在 README 复制破坏性命令。
优雅停机(E21):
- `docker stop` / `Ctrl+C` 会触发 `SIGTERM/SIGINT`,server 会调用 `app.close()` 完成收敛关闭(定时任务/HTTP/DB)。
---
## 5. P2 API 摘要
### 5.1 付款标记(pay)
- `POST /api/reimbursements/:id/pay`
- 权限:`expense.finance.pay`
- 可选请求体:`paymentVoucherNo`、`paymentDate(YYYY-MM-DD)`、`paymentChannel(bank/alipay/wechat/cash/other)`
- 语义:
- `approved -> paid`:返回 200,并归档关联 `bound` 发票
- 已 `paid` 重复同值:返回 200(`payment.idempotent=true`)
- 已 `paid` 重复冲突值:返回 `409 VALID_001`,`details.payment=already_paid_conflict`
### 5.2 委派管理
- `POST /api/delegations`:创建委派(scope + validFrom/validTo)
- `GET /api/delegations/mine`:查看我发起/我接收的委派
- `POST /api/delegations/:id/revoke`:撤销委派
- 委派仅作用于 `assignee_id` 任务,不改写 `assignee_role` 语义。
- 自动回收实现采用 `FOR UPDATE SKIP LOCKED + UPDATE ... RETURNING`,多实例并发下确保同一委派仅回收一次。
### 5.3 OCR 异步状态查询
- 上传发票在 OCR 开启且 `ocr_type=vat` 时会进入 `pending_ocr`。
- `GET /api/invoice-pool/:id` 可轮询 OCR 处理状态(`pending_ocr/available/ocr_failed`)。
### 5.4 审计日志查询
- `GET /api/audit-logs`
- 权限:`system.audit.view`(finance_mgr/admin)
- 支持分页与筛选(actor/action/targetType/date range)。
### 5.5 工作流查询增强(P3)
- `GET /api/workflow/tasks`:支持 `status/page/pageSize`,返回 `page/pageSize/total/items`。
- `GET /api/workflow/tasks/:taskId`:任务详情(实例、报销摘要、可执行动作、处理历史)。
- `GET /api/workflow/approvers`:转审候选人列表(active + 具备 `expense.approve`)。
### 5.6 部门管理(P5 蓝图 F26)
- `GET /api/departments`:部门列表 + 树(登录即可;默认不含归档部门)
- 可选:`GET /api/departments?includeArchived=1`(包含归档部门)
- `POST /api/departments`:创建部门(`system.users`)
- `PATCH /api/departments/:id`:更新部门(`system.users`;已归档禁止修改;禁止形成父子环)
- `POST /api/departments/:id/archive`:归档部门(`system.users`)
- 归档约束:有子部门/有在职员工则禁止归档
- 幂等:已归档重复调用返回 `outcome=already_archived`
### 5.7 列表查询接口(P3)
- `GET /api/reimbursements`:支持 `page/pageSize/status/scope`。
- `GET /api/invoice-pool`:支持 `page/pageSize/status/ownerScope`。
### 5.8 定时任务(可手动执行)
```bash
cd <repo-root>/server
npm run job:revoke-expired-delegations
npm run job:ocr-once
```
### 5.9 账号与角色管理(E17,试运行运营)
- `GET /api/users`:员工列表(分页/搜索)
- `POST /api/users`:创建员工(含初始密码)
- `GET /api/users/:id`:员工详情(管理员可看全部;员工可看自己;敏感字段仅返回脱敏)
- `POST /api/users/:id/disable`:禁用员工(tokenVersion+1 + 撤销 refresh)
- `POST /api/users/:id/enable`:启用员工(tokenVersion+1 + 撤销 refresh)
- `POST /api/users/:id/reset-password`:重置密码(返回一次性口令,tokenVersion+1)
- `POST /api/users/:id/roles`:设置角色(tokenVersion+1 + 撤销 refresh)
- `GET /api/users/:id/sensitive`:敏感字段明文查询(F25)
- `PATCH /api/users/:id/sensitive`:敏感字段更新(F25)
权限:
- `system.users`(hr/admin;admin 具备 `*`)
- `system.users_sensitive`(仅特权账号;用于解密查看/更新银行卡、身份证等)
---
## 6. 关键环境变量
- `DATABASE_URL`: PostgreSQL 连接串
- `JWT_SECRET`: JWT & signed-url token 签名密钥
- `FIELD_ENCRYPTION_KEY`: 敏感字段加密密钥(AES-256-GCM;base64 32 bytes;用于银行卡/身份证等)
- `APP_TIMEZONE`: 默认 `Asia/Shanghai`
- `REIMBURSEMENT_MAX_AGE_DAYS`: 报销费用日期提示期限,默认 `365`,仅允许 `180` 或 `365`;超期和跨自然年度只预警,不自动阻断
- `STORAGE_DIR`: 默认 `generated/storage`(相对 `server/`)
- `SERVE_CLIENT_DIST`: 是否由 server 托管 `client/dist`(默认 `false`)
- `CLIENT_DIST_DIR`: 前端构建产物目录(默认 `../client/dist`,相对 `server/`)
- `SIGNED_URL_TTL_SECONDS`: 默认 `600`
- `MAX_UPLOAD_SIZE_BYTES`: 默认 `10485760`(10MB)
- `RATE_LIMIT_ENABLED`: 是否启用关键接口限流(默认:production=true,其它环境=false)
- `RATE_LIMIT_LOGIN_MAX`: `/api/auth/login` 等接口限流次数(默认 `10`)
- `RATE_LIMIT_LOGIN_WINDOW`: 限流时间窗(默认 `1 minute`,支持 ms 格式如 `30s`/`1m`)
- `DELEGATION_REVOKE_CRON`: 委派自动回收定时表达式(默认 `0 * * * *`)
- `OIDC_ENABLED`: 是否启用 OIDC 登录(可选;默认 `false`)
- `OIDC_ISSUER_URL`: OIDC issuer URL(发现地址)
- `OIDC_CLIENT_ID` / `OIDC_CLIENT_SECRET`: OIDC client 配置
- `OIDC_REDIRECT_URI`: 回调地址(例如 `http://127.0.0.1:3000/api/auth/oidc/callback`)
- `OIDC_SCOPES`: 默认 `openid profile email`
- `OCR_PROVIDER`: `off` / `mock` / `tencent`(默认 `off`)
- `OCR_BATCH_SIZE`: 每轮 OCR 认领数量(默认 `10`)
- `OCR_LOCK_TTL_SECONDS`: OCR 锁超时秒数(默认 `300`)
- `OCR_POLL_CRON`: OCR 轮询 cron(默认 `* * * * *`)
- `OCR_MAX_ATTEMPTS`: OCR 最大重试次数(默认 `3`)
- `OCR_TIMEOUT_MS`: 腾讯 OCR SDK 超时(默认 `5000`)
- `TENCENT_SECRET_ID` / `TENCENT_SECRET_KEY` / `TENCENT_OCR_REGION`: 腾讯 OCR 配置
安全基线(P4):
- 安全头:默认启用 `@fastify/helmet`(CSP 默认关闭,避免误伤 SPA 本地调试)。
- 限流:`/api/auth/login`、`/api/auth/forgot-password`、`/api/auth/reset-password` 在 `RATE_LIMIT_ENABLED=true` 时启用限流。
- 日志脱敏:`authorization/cookie/password/code/newPassword` 等敏感字段会被 redaction。
回滚策略:
- 若限流误伤,可先设置 `RATE_LIMIT_ENABLED=false`(无需改代码)。
- 若安全头造成兼容性问题,可回退 `server/src/app.js` 中 helmet 注册逻辑。
> 生产提示:大表加索引需评估 `CREATE INDEX CONCURRENTLY`,避免锁表影响。
---
## 7. 常见失败排查
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| `prisma migrate deploy` 失败 | `DATABASE_URL` 未配置/不可达 | 检查 `server/.env` 与数据库容器状态 |
| `npm run seed:dev` 重复执行报错 | seed 脚本被改坏,未保持幂等 | 检查 `seed-config.js`/`seed-dev.js` 的 `upsert` / `skipDuplicates` |
| signed-url 接口返回 403 | `JWT_SECRET` 不一致、token 过期、访问人无权限 | 统一服务密钥、确认 URL 在 TTL 内、检查账号角色 |
| 上传返回 `file_too_large` | 文件超 `MAX_UPLOAD_SIZE_BYTES` | 调整环境变量或使用更小文件 |
| 上传失败且报目录/权限错误 | `STORAGE_DIR` 不存在或不可写 | 创建目录并修正权限;Docker 需确认 volume 挂载 |
| OCR 长期停留 `pending_ocr` | `OCR_PROVIDER=off`、mock未开启或腾讯密钥无效 | 检查 OCR 环境变量,优先用 `OCR_PROVIDER=mock npm run job:ocr-once` 验证链路 |
| 委派过期后仍可操作 | 自动回收任务未运行 | 检查 `DELEGATION_REVOKE_CRON`,手动执行 `npm run job:revoke-expired-delegations` |
---
## 8. 手工故障注入(非阻塞)
### 8.1 `STORAGE_DIR` 不可写路径
- 临时将 `STORAGE_DIR` 指向只读目录,执行上传接口,确认错误可观测(日志+返回码)。
### 8.2 磁盘空间不足(ENOSPC)
- 在受控环境中制造磁盘满条件后重试上传。
- 预期:错误路径可观测(日志中能定位到 I/O 错误),不要求纳入日常 CI 冒烟。
---
## 9. P6-P10 增量能力(验真 / 看板 / 预算 / 集成 / 生产硬化)
### 9.1 特性开关(默认值)
- `INVOICE_VERIFICATION_ENABLED=false`
- `VERIFICATION_PROVIDER=mock`
- `BUDGET_CONTROL_MODE=warn`
- `ERP_SYNC_ENABLED=false`
- `ERP_SYNC_PROVIDER=mock`
- `ERP_SYNC_ENDPOINT=`
- `ERP_SYNC_TIMEOUT_MS=5000`
- `ERP_SYNC_AUTH_TOKEN=`
- `TRAVEL_SYNC_ENABLED=false`
- `IM_NOTIFIER_ENABLED=false`
### 9.1.1 Feature Flag 治理(P11)
- 新增统一管理 API:
- `GET /api/system/feature-flags`
- `PUT /api/system/feature-flags/:key`
- 首期纳管键:`erp_sync`、`invoice_verification`。
- 优先级规则:`Env硬闸` > `DB在线灰度`。即使 DB 放开,若 Env 为 false 仍会关闭。
- 部门灰度模式:`rolloutType=department_allowlist` 时,仅白名单部门生效。
建议:先在测试组织灰度开启,再逐步扩大范围。
### 9.2 新增 API 摘要
- 发票验真:
- `POST /api/invoice-pool/:id/verify`
- `GET /api/invoice-pool/:id/verification`
- `POST /api/invoice-pool/:id/verification/retry`
- 看板聚合:
- `GET /api/dashboard/expense-overview`
- `GET /api/dashboard/trends`
- `GET /api/dashboard/top-categories`
- `GET /api/dashboard/approval-efficiency`
- 预算管理:
- `GET /api/budgets`
- `POST /api/budgets`
- `PATCH /api/budgets/:id`
- `POST /api/budgets/:id/archive`
- 外部集成:
- `POST /api/integrations/erp/sync/:reimbursementId`
- `POST /api/travel/orders/import`
- `GET /api/notifications/channels`
- `POST /api/notifications/channels/test`
### 9.3 新增回归脚本
```bash
cd <repo-root>/server
./scripts/regression-verification.sh
./scripts/regression-dashboard.sh
./scripts/regression-budget.sh
./scripts/regression-integrations.sh
./scripts/regression-feature-flags.sh
```
可通过总回归开关启用:
```bash
cd <repo-root>/server
RUN_VERIFICATION_REGRESSION=1 \
RUN_DASHBOARD_REGRESSION=1 \
RUN_BUDGET_REGRESSION=1 \
RUN_INTEGRATIONS_REGRESSION=1 \
RUN_FEATURE_FLAGS_REGRESSION=1 \
./scripts/regression-all.sh
```
### 9.4 单次任务脚本(调试)
```bash
cd <repo-root>/server
VERIFICATION_RECORD_ID=<uuid> npm run job:verification-once
npm run job:integration-outbox-once
```
### 9.5 回滚建议
1. 先关闭特性开关(验真/ERP/差旅/IM)。
2. 再停止相关任务调度(verification / integration outbox)。
3. 不回滚审计与历史台账数据,保持可追溯。
### 9.6 P9/P10 维护性与硬化说明
- 票据路由拆分:验真端点已拆到 `src/routes/invoicePool/verification.js`,读路径端点已拆到 `src/routes/invoicePool/read.js`,主入口仍为 `src/routes/invoicePool.js`,API 路径不变。
- Shared 抽象:`src/routes/invoicePool/shared/{selects,mappers,schemas,helpers}.js` 统一验真映射与票据查询 `select`,避免字段漂移。
- 看板聚合:`/api/dashboard/trends` 与 `/api/dashboard/top-categories` 已改为数据库聚合(groupBy),降低大数据量下内存聚合开销。
- 看板回归:`./scripts/regression-dashboard.sh` 新增重复请求一致性断言;可通过 `DASHBOARD_PERF_CHECK=1` 打印趋势接口耗时探针。
- ERP硬化回归:`./scripts/regression-integrations.sh` 内嵌最小 Node Mock ERP 服务,覆盖 success / 4xx(dead_letter) / 5xx(retry) / timeout(retry)。
- 矩阵门禁:`scripts/validate-feature-matrix.py` 默认校验 P5 + P6-P8 + P9 + P10 四事实源;`./scripts/check-minimal.sh` 会自动执行该校验。