加固部署隔离与备份来源契约(07-26-internal-ops-recovery-baseline)

这个提交包含在:
2026-07-27 06:13:02 +08:00
父节点 130518f471
当前提交 23342035ee
共修改 16 个文件,包含 304 行新增和 80 行删除
+37 -28
查看文件
@@ -20,6 +20,7 @@
- 部署目录通过 `REMOTE_DIR` 显式传入。
- 远端目录当前不是 git 工作树;发布应优先从本地执行 `bash deploy/trialrun/deploy.sh`,由脚本 `rsync` 到服务器
- 远端容器编排当前实际为 `docker-compose 1.29.2`;本 runbook 命令以 `docker-compose` 为准
- 所有 Compose 命令必须显式使用同一个 `COMPOSE_PROJECT_NAME` 和 `-p`;不得依赖目录名推导项目身份,也不得写死容器名。
- 当前公网 `.env` 已确认:`APP_ORIGIN=https://oa.hyinda.com`、`COOKIE_SECURE=true`、`PUBLIC_SECURITY_HARDENING_ENABLED=true`
- 企业微信与 OCR 已进入公网试运行配置。核查时只检查开关、长度和 smoke 结果,不在日志、Issue 或文档中输出 secret 明文。
@@ -46,6 +47,7 @@ cp deploy/trialrun/.env.example deploy/trialrun/.env
- `BUILD_SHA` / `VITE_BUILD_SHA` 由发布入口按已批准提交临时注入,不写入长期 `.env`;前者供后端运行时核对,后者只允许 12 位短 SHA 进入公网 bundle。
- 推荐使用无引号值,例如 `COOKIE_SECURE=true`、`PORT=3000`。
- `PORT=3000` 是容器内端口;`APP_HOST_PORT` 是宿主回环端口。发布脚本通过 Compose 动态解析后者,Caddy 必须指向相同值。
- `POSTGRES_DATA_DIR` 和 `STORAGE_HOST_DIR` 是宿主数据事实源;默认相对 `deploy/trialrun/`。生产环境更换项目名或任一数据目录只能在同一次获批停机窗口完成,禁止两个 PostgreSQL 实例同时挂载同一 PGDATA。
- 历史上已经写成带引号的 `.env` 当前仍兼容,但建议后续逐步改成无引号。
- 正式部署前可先执行 `node deploy/trialrun/validate-env-contract.js` 做静态契约检查。
- **EXTERNAL_GATED**:涉及认证安全迁移时,发布或回滚前必须由操作人先在边缘暂停 `/api/auth/forgot-password`、`/api/auth/reset-password`、`/api/auth/email-login/code`、`/api/auth/email-login/verify` 四个 challenge 端点,再执行“新镜像配置预检 -> additive migration -> 新应用切换”。部署脚本不会自动修改边缘配置;未人工确认四端点已关闭不得发布或回滚开放。仅在安全版本及 smoke 全部通过后人工恢复;若回滚到旧认证代码,四端点必须继续关闭。
@@ -65,10 +67,11 @@ cd /path/to/hyb-oa
node deploy/trialrun/validate-env-contract.js
export BUILD_SHA="<approved-full-commit>"
export VITE_BUILD_SHA="<matching-12-char-commit>"
docker-compose -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env up -d db
docker-compose -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env build init
docker-compose -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env run --rm init
docker-compose -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env up -d --build server
export COMPOSE_PROJECT_NAME="<approved-compose-project>"
docker-compose -p "$COMPOSE_PROJECT_NAME" -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env up -d db
docker-compose -p "$COMPOSE_PROJECT_NAME" -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env build init
docker-compose -p "$COMPOSE_PROJECT_NAME" -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env run --rm init
docker-compose -p "$COMPOSE_PROJECT_NAME" -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env up -d --build server
```
以上直接 Compose 命令仅用于首次初始化或受控应急;正式发布优先使用 `deploy/trialrun/deploy.sh`,由脚本从 clean、已进入 upstream 的提交自动注入两个构建标识。缺少任一标识时 Compose 必须拒绝解析。
@@ -76,7 +79,7 @@ docker-compose -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/
可选:启用 MinIO(对象存储,F21):
```bash
cd /path/to/hyb-oa
docker-compose -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env --profile minio up -d minio minio_init
docker-compose -p "$COMPOSE_PROJECT_NAME" -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env --profile minio up -d minio minio_init
```
可选:启用 Worker 进程(推荐在试运行/多进程模式使用):
@@ -84,13 +87,13 @@ docker-compose -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/
cd /path/to/hyb-oa
# 1) 在 deploy/trialrun/.env 中设置:JOBS_ENABLED="false"
# 2) 启用 worker profile(会额外启动 worker 容器)
docker-compose -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env --profile worker up -d --build server worker
docker-compose -p "$COMPOSE_PROJECT_NAME" -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env --profile worker up -d --build server worker
```
### 1.3 初始化(迁移 + 配置 seed + 试运行管理员)
```bash
cd /path/to/hyb-oa
docker-compose -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env run --rm init
docker-compose -p "$COMPOSE_PROJECT_NAME" -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env run --rm init
```
说明:
@@ -103,12 +106,12 @@ docker-compose -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/
### 1.4 验收
```bash
SERVER_HOST_PORT="$(docker-compose -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env port server 3000)"
SERVER_HOST_PORT="$(docker-compose -p "$COMPOSE_PROJECT_NAME" -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env port server 3000)"
SERVER_HOST_PORT="${SERVER_HOST_PORT##*:}"
curl -fsS "http://127.0.0.1:${SERVER_HOST_PORT}/health"
curl -fsS "http://127.0.0.1:${SERVER_HOST_PORT}/ready"
curl -fsS "http://127.0.0.1:${SERVER_HOST_PORT}/metrics" | head
docker-compose -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env exec -T server node src/scripts/smoke-trialrun-runtime.js
docker-compose -p "$COMPOSE_PROJECT_NAME" -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env exec -T server node src/scripts/smoke-trialrun-runtime.js
```
说明:
@@ -122,7 +125,7 @@ docker-compose -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/
- 2026-05-05 公网复核显示:`OCR_PROVIDER=tencent` 可用,真实发票上传后进入 `available`,provider 为 `tencent`;后续发布不能再把 OCR 回退到 mock 而不记录原因。
浏览器访问:
- `docker-compose ... port server 3000` 返回的宿主回环端口(单域托管前端)。
- `docker-compose -p "$COMPOSE_PROJECT_NAME" ... port server 3000` 返回的宿主回环端口(单域托管前端)。
### 1.4.1 腾讯 OCR 试运行核查
远端 `.env` 是 OCR 配置事实源,禁止提交或复制密钥明文。只做以下脱敏检查:
@@ -147,8 +150,8 @@ printf 'TENCENT_OCR_REGION=%s\n' "$TENCENT_OCR_REGION"
发布后 smoke:
```bash
docker-compose -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env exec -T server node src/scripts/smoke-external-deps.js
docker-compose -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env exec -T server node src/scripts/smoke-trialrun-runtime.js
docker-compose -p "$COMPOSE_PROJECT_NAME" -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env exec -T server node src/scripts/smoke-external-deps.js
docker-compose -p "$COMPOSE_PROJECT_NAME" -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env exec -T server node src/scripts/smoke-trialrun-runtime.js
```
真实 OCR 辅助复核(本地执行,不提交发票文件):
@@ -164,7 +167,7 @@ npx playwright test tests/e2e/manual/live-invoice-ocr.spec.ts --workers=1 --repo
回滚到 mock 只允许作为应急降级:
1. 在远端 `.env` 将 `OCR_PROVIDER=tencent` 改为 `OCR_PROVIDER=mock`。
2. 执行 `docker-compose -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env up -d server`。
2. 执行 `docker-compose -p "$COMPOSE_PROJECT_NAME" -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env up -d server`。
3. 在 Issue/发布记录中写明降级原因、预计恢复时间和影响范围。
## 1.5 备份与恢复演练(正式启用前必须完成)
@@ -177,12 +180,17 @@ set -a
source deploy/trialrun/.env
set +a
mapfile -t DB_CONTAINERS < <(docker-compose -p "$COMPOSE_PROJECT_NAME" -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env ps -q db)
test "${#DB_CONTAINERS[@]}" -eq 1
DB_CONTAINER="${DB_CONTAINERS[0]}"
DB_TOOL_MODE=docker \
DB_CONTAINER=hyb-oa-trialrun-db \
DB_CONTAINER="$DB_CONTAINER" \
STORAGE_DIR_HOST="<verified-active-storage-host-directory>" \
./scripts/backup-all.sh
```
`backup-all.sh` 可使用宿主 Node.js 或 Python 3 生成同结构 manifest;Docker-only 主机没有 Node.js 时会自动使用 Python 3,两者都没有则失败且不发布完整批次。真实运行环境执行前,不得只相信可能漂移的 `.env`:先脱敏确认所选 PostgreSQL 容器的数据库/用户与至少一个活动应用连接一致,再把已验证的运行参数传给脚本;不在日志中输出参数值。
`backup-all.sh` 可使用宿主 Node.js 或 Python 3 生成同结构 manifest;Docker-only 主机没有 Node.js 时会自动使用 Python 3,两者都没有则失败且不发布完整批次。`DB_CONTAINER` 和 `STORAGE_DIR_HOST` 均无隐式默认值:前者必须由已核验 Compose 项目的 `ps -q db` 解析,后者必须与活动 server 的附件挂载源一致。真实运行环境执行前,不得只相信可能漂移的 `.env`:先脱敏确认所选 PostgreSQL 容器的数据库/用户与至少一个活动应用连接一致,再把已验证的运行参数传给脚本;不在日志中输出参数值。
输出默认在 `deploy/trialrun/backups/hyb-oa-backup-<run-id>/`,其中包含:
@@ -267,12 +275,13 @@ RESTORE_SCOPE=shadow \
步骤:
1) 先做备份(见 E14 脚本)
2) 从本地仓库执行 `DEPLOY_SERVER=<ssh-target> REMOTE_DIR=<remote-dir> bash deploy/trialrun/deploy.sh`(推荐);脚本只同步已验证提交的 Git archive,不依赖远端 `git pull`,也不会把本地 ignored 文件带入发布。
2) 从本地仓库执行 `DEPLOY_SERVER=<ssh-target> REMOTE_DIR=<remote-dir> COMPOSE_PROJECT_NAME=<approved-compose-project> bash deploy/trialrun/deploy.sh`(推荐);脚本只同步已验证提交的 Git archive,不依赖远端 `git pull`,也不会把本地 ignored 文件带入发布。
- 在现网目录统一任务中,新的 `COMPOSE_PROJECT_NAME`、`POSTGRES_DATA_DIR`、`STORAGE_HOST_DIR` 必须与活动数据目录迁移在同一停机窗口启用;本地提交本身不授权切换。
- `artifacts/` 和 `output/` 是运行期证据/截图目录,不作为源码发布输入,也不由 `rsync --delete` 清理远端历史证据。
3) 构建并重启:
```bash
docker-compose -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env up -d --build server
docker-compose -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env run --rm init
docker-compose -p "$COMPOSE_PROJECT_NAME" -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env up -d --build server
docker-compose -p "$COMPOSE_PROJECT_NAME" -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env run --rm init
```
### 2.2 前端部署一致性排障
@@ -283,13 +292,13 @@ docker-compose -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/
```bash
# 1) 确认 Caddy 指向 Compose 实际发布的宿主回环端口
sudo sed -n '/oa.hyinda.com/,/}/p' /etc/caddy/Caddyfile
docker-compose -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env port server 3000
docker-compose -p "$COMPOSE_PROJECT_NAME" -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env port server 3000
# 2) 比对公网首页引用的 asset hash
curl -fsS https://oa.hyinda.com/ | grep -o '/assets/[^" ]*' | head
# 3) 在容器内查构建产物是否包含关键 test id
docker exec hyb-oa-trialrun-server sh -lc \
# 3) 在当前 Compose 项目的 server 容器内查构建产物是否包含关键 test id
docker-compose -p "$COMPOSE_PROJECT_NAME" -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env exec -T server sh -lc \
'for pat in invoice-pool-mobile-list reimbursements-mobile-list mobile-capture-file multiple:!0; do printf "%s " "$pat"; grep -Rao "$pat" /app/client/dist/assets /app/client/dist/index.html 2>/dev/null | wc -l; done'
```
@@ -297,9 +306,9 @@ docker exec hyb-oa-trialrun-server sh -lc \
```bash
cd /opt/hyb-oa-trialrun
docker-compose -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env up -d --build server
docker-compose -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env run --rm -T init </dev/null
SERVER_HOST_PORT="$(docker-compose -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env port server 3000)"
docker-compose -p "$COMPOSE_PROJECT_NAME" -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env up -d --build server
docker-compose -p "$COMPOSE_PROJECT_NAME" -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env run --rm -T init </dev/null
SERVER_HOST_PORT="$(docker-compose -p "$COMPOSE_PROJECT_NAME" -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env port server 3000)"
curl -fsS "http://127.0.0.1:${SERVER_HOST_PORT##*:}/ready"
```
@@ -318,13 +327,13 @@ curl -fsS "http://127.0.0.1:${SERVER_HOST_PORT##*:}/ready"
```bash
# 远端目录不是 git 工作树,推荐在本地切到目标 tag 后重新执行 deploy.sh
# git checkout <previous-tag>
docker-compose -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env up -d --build server
docker-compose -p "$COMPOSE_PROJECT_NAME" -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env up -d --build server
```
### 3.2 需要 DB 回滚(推荐做法)
1) 停止服务:
```bash
docker-compose -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env stop server
docker-compose -p "$COMPOSE_PROJECT_NAME" -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env stop server
```
2) 按 E14 恢复 DB + 文件存储备份
3) 启动服务并验收 `/ready` 与登录
@@ -332,7 +341,7 @@ docker-compose -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/
## 4. 日志与排障
- server 容器日志:
```bash
docker logs -f hyb-oa-trialrun-server
docker-compose -p "$COMPOSE_PROJECT_NAME" -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env logs -f server
```
- `/ready` 失败通常为 DB 不可达或连接串错误。
@@ -453,7 +462,7 @@ npm run test:e2e -- tests/e2e/manual/live-invoice-ocr-batch.spec.ts
- `OPENAPI_DOCS_ENABLED=false`
3. 重新执行:
```bash
docker-compose -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env up -d --build server
docker-compose -p "$COMPOSE_PROJECT_NAME" -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env up -d --build server
```
4. 验证浏览器和企业微信 WebView 都能正常登录、刷新 token 与退出登录。