# 业务前缀重构第 8 步迁移与发布前验收 Runbook ## 范围 本文用于业务前缀、客户网关匹配和落地号码改写重构的发布前准备。当前仍以本地 KVM/Tailscale 环境为准,不进入阿里云生产迁移。 本步骤覆盖: - B 执行新增 Prisma migration 前后的检查。 - 旧客户网关单 IP 和旧 `customer_gateway_policies` 到二期模型的安全迁移。 - Redis 配置发布检查。 - A OpenSIPS/Lua 替换前语法检查和回滚点。 - A/B/T 端到端验收清单。 ## 影响、风险与回滚点 影响范围: - MySQL:新增二期表/列后,`customer_gateways.line_group_id`、`customer_gateway_ips`、`customer_gateway_caller_prefixes` 会被迁移脚本写入。 - Redis:迁移脚本成功应用后会写 `customer_gateway_config` outbox,config-publisher 会发布新版本配置。 - OpenSIPS:后续发布 A 侧 `opensips.cfg` 与 `lisglosips_hotpath.lua` 后,INVITE 鉴权、路由、主被叫改写进入新热路径。 - CDR:第 7 步已让 Worker 可写入原始被叫、业务前缀、落地主被叫字段。 主要风险: - 多条旧策略无法自动合并到“一个客户网关一个线路组”,必须人工拆分或确认保留规则。 - 旧 `calleeMode=EQUALS/PREFIX` 不能安全推断为业务前缀,必须先在“业务前缀管理”中建立前缀并重新绑定客户网关。 - 旧 `callerMode=EQUALS` 在新模型下没有严格等值语义,不能自动迁移为前缀,否则可能扩大匹配范围。 - A 侧 OpenSIPS 配置替换会影响现有通话,必须维护窗口或确认可接受。 回滚点: - B 数据库:执行 migration 前必须按 S30 Runbook 备份 MySQL;迁移脚本 apply 前需保存 dry-run 报告。 - B release:保留当前 `/opt/lisglosips/current` 指向,可通过 S30 rollback 脚本切回旧 release。 - Redis:记录 `cfg:active_version` 与 `cfg:previous_version`,必要时切回上一版本。 - A OpenSIPS:替换前备份 `/etc/opensips/opensips.cfg` 与 `/etc/opensips/lisglosips_hotpath.lua`;语法检查失败不得重启;重启后失败则恢复备份并再次 `opensips -C`。 ## 迁移脚本 本仓库新增: ```bash scripts/phase2-gateway-migration.mjs ``` 默认 dry-run,不写库: ```bash node scripts/phase2-gateway-migration.mjs --json ``` 显式 apply 才写库: ```bash node scripts/phase2-gateway-migration.mjs --apply --json ``` 脚本只自动迁移满足以下条件的客户网关: - 每个客户网关恰好一条启用旧策略。 - 旧策略 `calleeMode=ANY`。 - 旧策略 `callerMode=ANY`,或 `callerMode=PREFIX` 且 `callerValue` 非空。 脚本会拒绝自动迁移以下情况: - 多条启用旧策略。 - 没有启用旧策略。 - 旧被叫等值或前缀匹配。 - 旧主叫等值匹配。 - 新旧线路组字段冲突。 apply 成功后,脚本会写入一个 `customer_gateway_config` outbox,用于触发配置发布。 ## B 侧发布前步骤 1. 记录状态: ```bash readlink -f /opt/lisglosips/current mysql -NBe "SELECT migration_name, finished_at FROM _prisma_migrations ORDER BY finished_at DESC LIMIT 5;" redis-cli --no-auth-warning GET cfg:active_version ``` 2. 执行 S30 备份: ```bash sudo /opt/lisglosips/current/infra/server-b/s30/lisglosips-release-preflight.sh sudo /usr/local/sbin/lisglosips-mysql-backup sudo /usr/local/sbin/lisglosips-redis-backup ``` 3. 部署新 release 到独立目录,不在线修改 current。 4. 执行数据库 migration: ```bash export DATABASE_URL='mysql://@127.0.0.1:3306/lisglosips' pnpm exec prisma migrate deploy pnpm exec prisma migrate status ``` 5. 迁移脚本 dry-run: ```bash export DATABASE_URL='mysql://@127.0.0.1:3306/lisglosips' node scripts/phase2-gateway-migration.mjs --json > /tmp/phase2-gateway-migration.dry-run.json ``` 6. 若 `blockers` 非空,先人工处理,不执行 apply。 7. 确认 dry-run 后执行 apply: ```bash node scripts/phase2-gateway-migration.mjs --apply --json > /tmp/phase2-gateway-migration.apply.json ``` 8. 等待或触发 config-publisher,检查新配置发布: ```bash systemctl status lisglosips@config-publisher --no-pager redis-cli --no-auth-warning GET cfg:active_version ``` ## A 侧 OpenSIPS 发布前步骤 1. 备份: ```bash sudo install -d -m 0750 /var/backups/lisglosips-phase2 sudo cp -a /etc/opensips/opensips.cfg /var/backups/lisglosips-phase2/opensips.cfg.$(date +%Y%m%d%H%M%S) sudo cp -a /etc/opensips/lisglosips_hotpath.lua /var/backups/lisglosips-phase2/lisglosips_hotpath.lua.$(date +%Y%m%d%H%M%S) ``` 2. 替换候选文件后,加载 Lua 并确认 SHA 与 `opensips.cfg` 中 `EVALSHA` 一致。 3. 必须先执行语法检查: ```bash sudo opensips -C -f /etc/opensips/opensips.cfg ``` 4. 语法检查通过后才可重启: ```bash sudo systemctl restart opensips sudo systemctl is-active opensips ``` ## Release Artifact 规范 发布包必须由固定脚本生成,禁止临时手工复制目录作为 release: ```bash pnpm release:artifact -- --release-id sXX-name-YYYYMMDDHHmmss ``` 脚本会执行构建、校验发布必需目录、生成 `RELEASE_MANIFEST.json`,并输出 `dist/releases/.tar.gz` 与 `.sha256`。正式给 B 机使用的 artifact 必须在 Linux 环境生成;在本地 Windows 仅允许使用 `--check` 或带 `--allow-non-linux` 生成检查包,不能作为服务器最终发布包。 ## A/B/T 验收清单 至少验证以下呼叫: - 命中业务前缀:例如客户送 `67118822190000`,话单 `callee=18822190000`,`raw_callee=67118822190000`,`business_prefix=671`。 - 任意被叫规则:客户网关 `calleeMatchMode=ANY` 时,旧无业务前缀呼叫仍可路由。 - 匹配优先级:业务前缀优先于主叫前缀,精确规则优先于空业务前缀兜底;命中业务前缀但主叫不通过时,不应退回裸号兜底网关。 - 唯一性约束:同一 IP + 同一业务前缀组合只能归属一个客户网关;同一 IP 下主叫前缀不能互相覆盖;IP 可跨客户复用但最终匹配必须唯一。 - 计费口径:客户网关决定客户侧计费,落地网关决定供应商侧成本,业务前缀不参与计费产品、报表维度或权限隔离。 - 主叫前缀命中:配置 `callerPrefixes` 后,命中前缀允许呼叫。 - 主叫前缀不命中:返回 `CALLER_PREFIX_NOT_MATCHED`。 - 业务前缀不命中:返回 `BUSINESS_PREFIX_NOT_MATCHED`。 - 落地被叫前缀:落地侧实际 Request-URI 被叫包含 `landingCalleePrefix`。 - 落地指定主叫:落地侧 From 主叫按权重池选择,话单 `landing_caller` 有值。 - 屏蔽地区跳过:命中落地网关屏蔽地区时尝试同线路组下一网关。 - 当前通话页:呼叫过程中可看到当前通话,强制挂断可用。 - 话单详情:可看到原始被叫、业务前缀、落地主叫、落地被叫、地级市、运营商。 ## 回滚 B 应用回滚: ```bash sudo infra/server-b/s30/lisglosips-release-rollback.sh /opt/lisglosips/releases/ ``` Redis 配置回滚: ```bash # 使用当前系统已验证的 Redis 运维方式切回 cfg:previous_version ``` A OpenSIPS 回滚: ```bash sudo cp -a /var/backups/lisglosips-phase2/ /etc/opensips/opensips.cfg sudo cp -a /var/backups/lisglosips-phase2/ /etc/opensips/lisglosips_hotpath.lua sudo opensips -C -f /etc/opensips/opensips.cfg sudo systemctl restart opensips ``` 数据库回滚原则: - 本次 migration 为向前兼容新增表/列,不做破坏性 down migration。 - 若 apply 后发现业务规则错误,优先通过客户网关页面/API 修正 `lineGroupId`、主叫规则和业务前缀绑定,再重新发布 Redis 配置。 - 若必须恢复数据,使用执行前 MySQL 备份在隔离环境验证后再恢复。