# 宠小它生产发布与回滚 Runbook

> 版本：2026-08-01
>
> 适用范围：3～5 家独立宠物洗护单店试点；backend、admin、H5、微信小程序。
>
> 当前结论：仓库交付到 `Ready for Release`，未取得真实生产域名、凭据、备份和线上验收证据前，不得标记 `Released`。

## 1. 发布原则

1. 发布单元必须冻结为四元组：`backend commit + admin commit + frontend commit + docs/RC manifest commit`，同时记录 jar/静态包校验和。
2. 生产数据库只通过已审核 SQL 迁移；应用固定 `ddl-auto=validate`，禁止启动期改表或修复数据。
3. 后端预检和自动 smoke 均为只读；创建预约、开始服务、提交报告等写路径只在明确指定的试点门店和人工验收窗口执行。
4. 应用、静态站点采用版本目录加 `current` 软链接切换；数据库迁移为向前兼容的追加式变更，不做自动向下迁移。
5. Petstore 使用独立域名、Nginx `server` 块、systemd unit 和发布目录。不得覆盖、重启或复用同机 Gitea/GitLab 的配置与服务。
6. 任何命令输出都不得包含数据库密码、微信 AppSecret、session secret、完整报告 token、手机号或私密媒体 URL。
7. CORS 只由后端全局 `CorsFilter` 控制；Controller 不得声明局部 `@CrossOrigin`，生产 smoke 必须同时验证白名单 Origin 与拒绝分支。
8. Session token 必须携带未来 `exp`；`PETSTORE_SESSION_TTL_SECONDS` 仅允许 300～2592000 秒，非法值不得静默回退。

## 2. 本次发布任务卡

| 字段 | 值 |
|---|---|
| Task Name | 建立可重复的生产发布与回滚闭环 |
| Role | Backend Ops / Frontend Release Ops |
| Owner Agent | Backend Ops（后端与 Nginx）/ Frontend Release Ops（admin、H5、小程序） |
| Paired QA | Core Flow QA / Report Share QA |
| Workstream | Release |
| Track | Ops |
| Status | Ready for Release（生产证据待补） |
| Priority | P0 |
| Repo/Path | `backend/deploy/**`、前端发布配置、`docs/**` |
| Service Lock | Petstore backend unit + Petstore 专用 Nginx server blocks + Petstore static roots |
| Depends On | 冻结 RC、真实域名/TLS、备份、八个迁移、微信合法域名 |
| Acceptance | 预检、构建、readiness、只读 smoke、三角色人工 smoke、回滚演练均有证据 |
| RC Included | Yes |
| Due Date | TBD |
| Output Link | 当前 Runbook + RC manifest |
| Blocker Owner | 生产环境负责人（域名/凭据/变更窗口） |
| Commit / RC | Not Frozen，提交后写入 RC manifest |
| Architecture Review Required | Yes |
| Data Model Review Required | Yes（迁移顺序与回滚边界） |
| Access / Privacy Review Required | Yes |

## 3. 立即停止条件

出现任一情况立即停止，不继续切流：

- 四仓 commit、构建产物或校验和与 RC manifest 不一致；
- 生产域名仍为 localhost、`.invalid`、`api.s-good.com` 或 `www.s-good.com` 等非本项目域名；
- 数据库备份未完成、校验和缺失或未在隔离库验证可恢复；
- 任一迁移末尾验证查询非 0，或预检的 31 项只读不变量非 0；
- `nginx -t` 失败，或 diff 显示将修改 Gitea/GitLab 的 server block；
- readiness 非 `UP`，上传目录/FFmpeg/数据库任一依赖失败；
- 发现凭据、完整 token、手机号或私密媒体地址进入日志/工单/截图；
- 任一 Controller 出现局部 `@CrossOrigin`，或非白名单 Origin 未返回 403；
- `PETSTORE_SESSION_TTL_SECONDS` 非整数、短于 5 分钟或长于 30 天；
- 无明确值班人、回滚执行人或生产变更授权。

## 4. 发布前冻结

RC Steward 在 `docs/qa-reports` 创建当次 manifest，至少记录：

- 四仓 commit、分支、`git status --short`（必须为空）；
- Java/Node/npm/Maven 版本；
- jar、admin `dist`、H5 `dist`、小程序构建目录的 SHA-256；
- 目标环境、变更窗口、操作者、审核人；
- 本次包含/排除项、已知风险、旧版本回滚点；
- 域名只记 host，不记 query/token；配置只记变量名和“已配置”，不记值。

冻结后重新运行本地门禁：

```bash
cd /Users/apple/_src/petstore/backend
mvn test
mvn -DskipTests package
bash -n deploy/release-preflight.sh deploy/production-smoke.sh

cd /Users/apple/_src/petstore/admin
npm run build

cd /Users/apple/_src/petstore/frontend
npm run build:h5
npm run build:mp-weixin

cd /Users/apple/_src/petstore
python3 docs/graph/validate_ontology.py docs
python3 docs/graph/audit_drift.py
```

## 5. 数据库备份与迁移

### 5.1 备份证据

使用权限受控的 MySQL option file，避免把密码写在命令行。备份必须包含表结构、数据、触发器、事件和存储过程，并记录文件大小、SHA-256、开始/结束时间。示例：

```bash
mysqldump --defaults-extra-file=/secure/path/petstore-mysql.cnf \
  --single-transaction --routines --triggers --events --hex-blob \
  petstore > /secure/backup/petstore-before-<rc>-<timestamp>.sql
shasum -a 256 /secure/backup/petstore-before-<rc>-<timestamp>.sql
```

上线前必须把该备份恢复到隔离数据库并完成基础计数核对；“命令退出 0”不能替代恢复演练。备份保留周期由数据负责人决定，本流程不自动删除任何备份。

### 5.2 固定迁移顺序

在维护窗口中按以下顺序人工执行，每一步都保存执行时间和脚本 SHA-256，并检查脚本末尾验证查询：

1. `backend/db/migrations/20260801_split_service_identity.sql`
2. `backend/db/migrations/20260801_create_store_customer.sql`
3. `backend/db/migrations/20260801_create_business_event.sql`
4. `backend/db/migrations/20260801_create_booking_capacity.sql`
5. `backend/db/migrations/20260801_create_report_send_status.sql`
6. `backend/db/migrations/20260802_create_store_onboarding.sql`
7. `backend/db/migrations/20260802_create_store_customer_timeline.sql`
8. `backend/db/migrations/20260802_create_follow_up_task.sql`

禁止跳步、交换顺序或让应用自动补表。任一验证项非 0，停止发布并由 Data Model/业务 owner 判断修复方案；不得猜测性回填客户身份、报告作者、事件、历史发送事实或历史开通状态，也不得改写既有 BusinessEvent 来迁移客户归属。

### 5.3 生产只读预检

把 `backend/deploy/petstore-backend.env.example` 复制到服务器权限受控的 `/etc/petstore/backend.env` 并填入真实值；文件权限建议 `0600`，所有者为服务账号。不要在交互 shell 中 `source` 该文件，也不要用 `env $(cat ...)`，避免特殊字符被 shell 解释或泄露。通过一次性 systemd unit 以与正式服务相同的 `EnvironmentFile` 和服务账号执行：

```bash
systemd-run --wait --pipe --collect \
  --unit=petstore-preflight-<rc> \
  --property=Type=oneshot \
  --property=User=petstore \
  --property=Group=petstore \
  --property=WorkingDirectory=/opt/petstore/backend/releases/<rc> \
  --property=EnvironmentFile=/etc/petstore/backend.env \
  /usr/bin/env BUILD_FIRST=0 \
  JAR_PATH=/opt/petstore/backend/releases/<rc>/petstore-backend.jar \
  ./deploy/release-preflight.sh
```

通过条件：静态配置检查（含 session TTL）通过、JPA `validate` 通过、31 项数据不变量全部为 0，并以退出码 0 结束。预检不会执行迁移或修复数据。

## 6. 构建发布产物

### 6.1 后端

将已冻结 jar 放入 `/opt/petstore/backend/releases/<rc>/petstore-backend.jar`，同时归档 commit、SHA-256 和 `deploy/` 模板。systemd unit 以 `backend/deploy/petstore-backend.service.example` 为基线，由服务器管理员安装为 Petstore 独立 unit。

### 6.2 Admin 与 H5

真实域名配置保存在权限受控文件，不提交仓库。门禁和构建必须使用同一份内容：

```bash
cd /opt/build/petstore/admin
PETSTORE_ADMIN_ENV_FILE=/secure/path/petstore-admin.env npm run preflight:release
cp /secure/path/petstore-admin.env .env.production.local
npm run build

cd /opt/build/petstore/frontend
PETSTORE_FRONTEND_ENV_FILE=/secure/path/petstore-h5.env npm run preflight:release
cp /secure/path/petstore-h5.env .env.production.local
npm run build:h5
```

产物进入版本目录后再切换 `current` 软链接。构建结束应清除工作区内临时 `.env.*.local`，但保留受控配置源和构建证据。

### 6.3 微信小程序

```bash
cd /opt/build/petstore/frontend
PETSTORE_FRONTEND_ENV_FILE=/secure/path/petstore-mp.env npm run preflight:release
cp /secure/path/petstore-mp.env .env.mp-weixin.local
npm run build:mp-weixin
```

微信公众平台的 `request` / `downloadFile` 合法域名必须与门禁通过的实际域名一致。先上传体验版并完成真机三角色验收，再决定提审/发布；后端或 H5 发布不会自动更新用户手机里的小程序。

## 7. 切换应用与 Nginx

1. 将后端 `current` 指向新版本目录，执行 `systemctl daemon-reload`，只重启 Petstore 独立 unit。
2. 确认进程稳定并通过本机 readiness，再切 admin/H5 静态目录 `current`。
3. 以 `backend/deploy/nginx-petstore.conf.example` 新建 Petstore 专用配置文件；替换域名、证书路径和静态根目录。
4. 保留模板中的 `petstore_safe` access log 格式，只记录 `$uri`，不得改回会包含 query 的 `$request` / `$request_uri`，避免报告 token 与邀请 token 进入访问日志。
5. 执行 `nginx -t`。检查配置 diff 仅增加/修改 Petstore 的日志格式和三个 server block，不改 Gitea/GitLab upstream、端口、证书和 location。
6. 只执行 Nginx reload，不停止 Nginx；reload 后同时检查 Petstore、Gitea、GitLab 的既有健康入口。

证书续签必须沿用服务器现有 ACME/证书管理方式，为 Petstore 新域名单独申请/续签；不要替换其他产品证书。续签后先 `nginx -t` 再 reload，并记录证书到期时间。

## 8. 上线验证

### 8.1 自动只读 smoke

```bash
cd /opt/petstore/backend/current
API_ORIGIN=https://<api-domain> \
SMOKE_ALLOWED_ORIGIN=https://<admin-or-report-domain> \
./deploy/production-smoke.sh
```

脚本验证 liveness、readiness、公开门店、服务时长、可约容量、白名单 Origin 精确回显和非白名单 Origin 403；提供受控的 `SMOKE_BEARER_TOKEN` 时才增加两个 protected GET。脚本不会创建或修改业务数据。

### 8.2 三角色人工 smoke

逐项执行与签署以《试点三角色端到端验收清单-2026-08-02.md》为准；下列为发布窗口最小摘要。

仅在预先标记的试点门店和测试宠主上执行，记录匿名化预约 ID/报告 ID，不记录完整 token/手机号：

1. **boss**：用微信核验手机号创建试点门店；在 Admin/小程序补齐资料、容量、服务并显式完成开通；创建一次性员工邀请、保存创建证据后用小程序分享卡片；查看工作台、客户、日程。
2. **staff**：从分享卡片进入，核对门店/姓名/脱敏手机号；用受邀微信手机号成功加入。用不匹配手机号验证被拒绝；加入后代客预约、开始服务、提交一约一报告并确认发送。
3. **customer**：小程序自助预约并验证长服务跨时段占号；打开匿名报告页；提交提醒留资；再次预约。
4. **权限反例**：customer 不可进入后台；A 店账号不可读取 B 店预约/报告/线索/邀请；legacy 永久邀请码与直接建员工均返回停用码；删除员工后旧 session 立即 401。
5. **异常反例**：邀请手机号不匹配/过期/撤销/重复使用、无容量、非法状态迁移、缺前/后照片、错误报告 token、FFmpeg 失败均有可理解提示。

### 8.3 试点指标基线

角色 smoke 通过后才能开始计入真实样本。指标口径与 Phase 2 决策带见《试点指标采集与Phase2决策方案-2026-08-02.md》；只读采集脚本为 `backend/db/queries/pilot_metrics.sql`。首次结果必须显式填写 3～5 个 `@pilot_store_ids`，保存 `[from,to)`、门店内部 ID 集合、四仓 commit、数据库快照时间和结果文件 SHA-256，且四项指标可靠性反例为 0。

## 9. 回滚

### 9.1 应用与静态资源回滚

- 保留前一版本目录和软链接目标。若新版本 readiness、错误率或主链路失败，先停止切流，把 backend/admin/H5 的 `current` 恢复到 RC manifest 中的前一版本，再只重启 Petstore unit、`nginx -t` 并 reload。
- 微信小程序不能靠服务器软链接回滚。应在公众平台保留上一稳定版本，按微信流程回退；同时确认其 API 契约仍受当前后端兼容。
- 回滚后重新执行只读 smoke，并记录故障窗口、影响门店、回滚时间和 owner。

### 9.2 数据库回滚边界

本批八个迁移以新增列/表/索引和兼容回填为主，旧应用应忽略新增结构。因此常规回滚只回应用，不删除列、表、索引，不执行自动 down migration，也不把已产生的新业务数据覆盖回旧备份。

只有确认发生灾难性数据破坏、已冻结所有写入、明确接受备份时间点之后的数据丢失，并取得用户/业务负责人、DBA 和隐私责任人的书面授权后，才可执行整库恢复。恢复必须先在隔离库验证，再制定专门变更单；本 Runbook 不授权该操作。

## 10. 发布完成定义

只有以下证据全部归档，RC 才能由 `Ready for Release` 改为 `Released`：

- 四仓冻结 commit 与产物校验和；
- 备份及隔离恢复验证；
- 八个迁移执行和末尾验证结果；
- 最终配置门禁与后端只读预检；
- `nginx -t`、readiness、自动只读 smoke；
- boss/staff/customer 三角色人工 smoke；
- 试点指标查询基线、时间窗口与首份低敏结果（开始计样本时）；
- Gitea/GitLab 回归未受影响；
- 监控、告警联系人、回滚点和值班窗口；
- 未解决问题清单及 Go/No-Go 决策人。
