Skip to content

开发与正式发布 ​

本文对应开发环境迁移到独立 Docker Compose 服务器后的发布脚本。 命令从 WSL Ubuntu 的仓库根目录执行。最近一次实操验证:2026-09-11。

标准顺序:发布前检查 → 开发版构建与发布 → 开发验收 → 正式候选准备 → 候选验收 → 正式切流与 Web 发布 → 调度及公网验收 → 保存发布记录。

发布范围是 API、AdminWeb、MerchantWeb,不包括微信小程序上传、审核和发布。 所有步骤按顺序执行,失败时先确认实际状态,不要继续下一步或并发执行另一轮发布。

环境默认服务器目录与方式默认 SSH 密钥
开发root@8.134.105.163/opt/xiedao-dev,Docker Compose~/.ssh/xieli.pem
正式root@47.121.197.139/opt/xiedao/prod,systemd 蓝绿;Web 使用版本目录~/.ssh/XieDao.pem

分别使用 XIEDAO_DEV_SERVER、XIEDAO_DEV_SSH_KEY 和 XIEDAO_PROD_SERVER、XIEDAO_PROD_SSH_KEY 覆盖目标。新的发布入口不读取共用的 XIEDAO_SERVER,避免开发目标配置误用于正式环境。 正式密钥保留对旧 XIEDAO_SSH_KEY 的兼容。

1. 发布前检查 ​

在同一个专用 WSL Bash 终端中执行。下面的辅助变量供本文检查命令使用; 实际发布脚本读取相同的环境变量。失败即停止设置可能退出当前 Bash, 处理问题后重新进入终端、初始化变量,再根据远端实际状态决定从哪一步继续。

bash
set -euo pipefail
cd /home/ubuntu/projects/xiedao
export XIEDAO_DEV_SERVER="${XIEDAO_DEV_SERVER:-root@8.134.105.163}"
export XIEDAO_DEV_SSH_KEY="${XIEDAO_DEV_SSH_KEY:-$HOME/.ssh/xieli.pem}"
export XIEDAO_PROD_SERVER="${XIEDAO_PROD_SERVER:-root@47.121.197.139}"
export XIEDAO_PROD_SSH_KEY="${XIEDAO_PROD_SSH_KEY:-${XIEDAO_SSH_KEY:-$HOME/.ssh/XieDao.pem}}"
SSH_DEV=(ssh -o BatchMode=yes -o ConnectTimeout=10 -i "$XIEDAO_DEV_SSH_KEY" "$XIEDAO_DEV_SERVER")
SSH_PROD=(ssh -o BatchMode=yes -o ConnectTimeout=10 -i "$XIEDAO_PROD_SSH_KEY" "$XIEDAO_PROD_SERVER")
LOG_DIR="$(mktemp -d /tmp/xiedao-release-logs.XXXXXX)"
printf 'Local deployment logs: %s\n' "$LOG_DIR"

git status --short
git log -1 --format='%H %s'
java -version
mvn -version
node --version
npm --version
python3 Deploy/docker-dev/test-deployment.py

放行条件:

  • Git 工作区干净,确认发布提交;构建期间不要编辑文件、切换提交或让其他任务改动仓库。
  • 使用 WSL PATH 上的 Java 21、Maven;两个前端当前要求 Node 22、npm 10。
  • 发布脚本测试通过;本次变更相关的后端、前端测试和业务验收已安排。
  • 确认本次数据库、接口变更兼容旧 API、旧 Web、小程序及仍可能运行的旧调度实例。
  • 日志保存在 /tmp,不要在仓库根目录添加临时脚本、日志或产物,避免污染发布源。
  • 本文或其他文档的未提交修改同样会使工作区变脏,正式发布前也要妥善提交或另行保存。

检查远端空间、服务和当前版本,不要输出完整 .env、env.sh 或其他密钥配置:

bash
"${SSH_DEV[@]}" 'cd /opt/xiedao-dev &&
  docker compose config --quiet &&
  docker compose ps &&
  grep -E "^(RELEASE_ID|API_RELEASE_ID|ADMIN_RELEASE_ID|MERCHANT_RELEASE_ID)=" .env &&
  df -h .'

"${SSH_PROD[@]}" 'bash -s' <<'REMOTE'
set -eu
cat /etc/nginx/conf.d/xiedao_prod_api_upstream.conf
if [ -f /opt/xiedao/prod/active-traffic.env ]; then
  cat /opt/xiedao/prod/active-traffic.env
fi
for slot in blue green; do
  printf '\nSLOT %s\n' "$slot"
  readlink -f "/opt/xiedao/prod/$slot/ruoyi-admin.jar"
  systemctl show "xiedao-prod-$slot.service" -p ActiveState -p SubState -p NRestarts
  grep -E '^export (SERVER_PORT|APP_SCHEDULING_ENABLED|SPRING_QUARTZ_AUTO_STARTUP|FLYWAY_DEPLOY_ENABLED)=' \
    "/opt/xiedao/prod/$slot/env.sh"
done
for web in admin merchant; do
  readlink -f "/opt/xiedao/$web/dist"
done
df -h /opt/xiedao/prod /root/db-backups
REMOTE

记录当前 API 槽位、两个 JAR 路径、两个 Web 版本目录以及唯一调度槽位,供回滚参考。 以 Nginx 实际 upstream 为准:8080 为 blue,8081 为 green; 若状态文件与 upstream 不一致,先排查,不要直接切流。

2. 发布开发版 ​

bash
./deploy-dev.sh 2>&1 | tee "$LOG_DIR/dev.log"

初始化时的 pipefail 用于保留发布命令的失败状态,避免被 tee 的成功退出掩盖。

脚本在工作站执行 Maven 测试和打包、两个前端的 npm ci 和 staging 构建, 将文件上传到开发服务器独立的 incoming/<release>,校验 SHA-256 后在 releases/<release> 构建镜像。镜像构建上下文只包含该版本产物和 Dockerfile。

后端发布前备份开发数据,执行 Flyway migrate 和 validate,再更新应用并检查健康状态。 所有检查通过后保存版本清单、验证标记和 .env 中的镜像版本号。 不会覆盖服务器的密钥、证书、业务配置、网关配置或数据库数据目录。

也可以只发布一个组件:

bash
./Api/deploy-dev.sh test
./AdminWeb/deploy-dev.sh
./MerchantWeb/deploy-dev.sh

独立发布会保留其他组件的版本。前端构建使用 /dev-api 同源代理。 只有显式用于开发验证时才执行:

bash
./deploy-dev.sh skip-tests

跳过测试或从未提交的工作区构建的后端版本不能晋级正式环境。 自动 repair-flyway 已移除:校验失败时停止发布,查明迁移历史差异后单独处理。 开发版仍为单个 API 实例,重启有短暂中断。

版本号采用 YYYYMMDD_HHMMSS_NNNN。保存脚本最终打印的版本号。 不要手工复用版本号、修改验证清单或重新给既有镜像打同一个标签。 Deploy/docker-dev/publish.sh 是服务器内部入口,要求已经接收完整的版本目录; 旧的“覆盖 artifacts 后直接运行 publish.sh”流程已被替代。

3. 开发验收与版本记录 ​

看到 [OK] Development all published: ... 后,输入本轮实际版本号, 不能沿用本文末尾的历史版本:

bash
read -r -p '本轮开发 API 版本号: ' RELEASE
[[ "$RELEASE" =~ ^[0-9]{8}_[0-9]{6}_[0-9]{4}$ ]]
"${SSH_DEV[@]}" "test -f '/opt/xiedao-dev/releases/$RELEASE/.verified' &&
  grep -E '^(ARTIFACT_RELEASE_ID|GIT_COMMIT|GIT_DIRTY|TESTS_PASSED|MIGRATIONS_PASSED|ARTIFACT_SHA256|MIGRATION_SHA256)=' \
    '/opt/xiedao-dev/releases/$RELEASE/release.env'"

for url in \
  https://dev-api.xiedao888.cn/captchaImage \
  https://dev-api.xiedao888.cn/api/app/region/options \
  https://dev-admin.xiedao888.cn/dev-api/captchaImage \
  https://dev-merchant.xiedao888.cn/dev-api/captchaImage; do
  printf '%s ' "$url"
  curl -fsS --max-time 15 "$url" |
    python3 -c 'import json,sys; d=json.load(sys.stdin); assert d.get("code")==200,d.get("msg"); print("code=200")'
done

必须看到 GIT_DIRTY=false、TESTS_PASSED=true、MIGRATIONS_PASSED=true, 并确认 GIT_COMMIT 是本轮提交。晋升时导出器还会检查它是当前运行的开发 API 版本。

打开 dev-admin.xiedao888.cn、dev-merchant.xiedao888.cn,检查登录页、 静态资源及浏览器错误;使用获准的测试账号验收本次功能。登录页和验证码成功 只证明基础可用,不能替代登录后的业务验收,也不要在正式库随意创建订单或修改价格。

测试结果需区分“通过”和“跳过”。TESTS_PASSED=true 表示 Maven 测试构建成功, 不意味着所有集成测试都运行了;Docker 不可用时 Flyway 容器集成测试可能按配置跳过。 发现失败要定位原因;确认为历史断言失效时记录证据和处置,不能统一当作可忽略告警。

4. 准备正式候选版本 ​

再次检查当前正式流量,并选择没有流量的槽位,不要根据上次发布推断:

bash
"${SSH_PROD[@]}" 'cat /etc/nginx/conf.d/xiedao_prod_api_upstream.conf'
read -r -p '本轮非流量槽位(blue 或 green): ' SLOT
[[ "$SLOT" == blue || "$SLOT" == green ]]
./deploy-prod.sh prepare "$SLOT" "$RELEASE" 2>&1 | tee "$LOG_DIR/prepare.log"

终端要求输入 PROMOTE <槽位> <开发版本号>,核对提示后手工确认,不要用 yes 或自动输入绕过核对。脚本将:

  1. 从新开发服务器导出当前 API 版本,核对容器健康状态、镜像版本及容器内两个 JAR 的 SHA-256。
  2. 要求版本清单记录干净的 Git 工作区、测试通过、开发库迁移通过。
  3. 通过工作站中转,将同一份 JAR 和迁移 JAR 上传至正式服务器的 candidates/<release>。
  4. 检查正式目标槽位、Java、配置及调度唯一性,备份正式库并执行迁移。
  5. 启动候选实例并检查健康状态,失败时尝试恢复该槽位旧版本。

这个步骤不切换 Nginx。开发数据库、Docker .env、开发凭据不会传入正式环境。 历史开发初始版本 20260909-initial 没有新的版本证明,不能用来晋升。

注意:非流量槽位不一定是空闲实例,它可能承担定时任务。 如果目标本来就是 唯一调度槽位,prepare 重启它时任务调度会短暂停顿,随后以新版本启动; 新调度逻辑会先于 Nginx 切流生效。执行前必须确认这个时间窗口可接受。

如果准备过程失败,可以对同一版本重试;已存在且内容完全相同的候选目录会复用, 内容不同时拒绝覆盖。不要通过修改 .verified 或 release.env 绕过守卫。

5. 候选实例验收 ​

准备成功后检查候选端口、JAR 路径及启动日志:

bash
case "$SLOT" in blue) PORT=8080 ;; green) PORT=8081 ;; esac
"${SSH_PROD[@]}" "set -eu
  systemctl show xiedao-prod-$SLOT.service -p ActiveState -p SubState -p NRestarts
  readlink -f /opt/xiedao/prod/$SLOT/ruoyi-admin.jar"
for path in /captchaImage /api/app/region/options; do
  "${SSH_PROD[@]}" "curl -fsS --max-time 10 'http://127.0.0.1:$PORT$path'" |
    python3 -c 'import json,sys; d=json.load(sys.stdin); assert d.get("code")==200,d.get("msg"); print("candidate code=200")'
done
"${SSH_PROD[@]}" "journalctl -u xiedao-prod-$SLOT.service --since '15 minutes ago' --no-pager"

确认候选 JAR 指向本轮版本、服务处于运行状态、启动没有异常,并完成需要的候选业务验证。 备份路径从 prepare.log 获取;必要时在正式服务器对该备份运行 sha256sum -c <备份文件>.sha256 和 gzip -t <备份文件> 检查完整性。 如果有新增迁移,确认迁移结果及影响,不要只看应用启动成功。

6. 切流并发布正式 Web ​

保持工作区干净且 HEAD 等于候选清单的 GIT_COMMIT,执行:

bash
./deploy-prod.sh release "$SLOT" "$RELEASE" 2>&1 | tee "$LOG_DIR/release.log"

脚本要求本地提交与候选清单一致,先完成两个正式 Web 构建,再调用已有切流脚本。 终端要求输入 SWITCH <槽位> <开发版本号>。切流成功后发布两个已经构建的 Web 产物。 Web 采用正式 API 地址;不能直接把开发 dist 上传到正式站点。 两个 Web 会各自生成发布目录版本号,不要求它们与 API 版本号相同,三者均需记录。

如果只需要后端,可继续使用:

bash
./Api/deploy-prod.sh "$SLOT" "$RELEASE"
./Api/switch-prod.sh "$SLOT" "$RELEASE"

Web 也保留各自的 deploy-prod.sh。根目录 deploy-prod.bat 将参数转发给 WSL, 例如 deploy-prod.bat prepare blue <开发版本号>。

正式 API、两个 Web 的发布是分步执行,不是跨服务事务。 某个 Web 发布失败时,其自身脚本尝试回滚;已成功切换的 API 和另一个 Web 不会自动回滚。 后端和数据库变更必须兼容仍在运行的旧 Web 与旧小程序。

7. 切流后验收 ​

bash
"${SSH_PROD[@]}" 'set -eu
  cat /opt/xiedao/prod/active-traffic.env
  cat /etc/nginx/conf.d/xiedao_prod_api_upstream.conf
  readlink -f /opt/xiedao/admin/dist
  readlink -f /opt/xiedao/merchant/dist'

for path in /captchaImage /api/app/region/options; do
  curl -fsS --max-time 15 "https://api.xiedao888.cn$path" |
    python3 -c 'import json,sys; d=json.load(sys.stdin); assert d.get("code")==200,d.get("msg"); print("production code=200")'
done

# 在本轮正式构建的 dist 尚未被其他构建覆盖时核对公网首页。
for web in AdminWeb MerchantWeb; do
  if [[ "$web" == AdminWeb ]]; then
    host=admin.xiedao888.cn
  else
    host=merchant.xiedao888.cn
  fi
  local_sha="$(sha256sum "$web/dist/index.html" | cut -d' ' -f1)"
  public_sha="$(curl -fsS --max-time 15 "https://$host/index.html" | sha256sum | cut -d' ' -f1)"
  if [[ "$local_sha" != "$public_sha" ]]; then
    printf '[ERROR] %s index mismatch\n' "$host" >&2
    exit 1
  fi
  printf '[OK] %s index matches release\n' "$host"
done
git status --short

还需打开两个正式后台,确认登录页可用、主要资源加载成功且没有页面脚本错误。 需要浏览器自动化时,等待具体表单或控件出现,不把 networkidle 当作唯一成功条件。 有 IM/WASM 等网络请求的页面可能已可用,但迟迟达不到网络空闲。

重复发布前的双槽位检查,核对真实 JAR 版本、调度开关、服务状态及本次启动日志。 保留旧实例与旧 Web 版本,在约定的观察窗口内检查新增错误和实际任务执行记录。 仅有 Quartz “started” 日志不能证明每个定时任务都已成功执行。

8. 定时任务与发布的关系 ​

API 切流、应用版本升级、调度归属是三件不同的事。 prepare 和 release 均不会自动修改 APP_SCHEDULING_ENABLED / SPRING_QUARTZ_AUTO_STARTUP, 而是检查两个槽位的配置中恰好一个同时为 true,另一个同时为 false。 文件中的开关不等于进程已经加载该值,调整配置后必须结合重启、日志和执行记录确认。

发布前状态按标准流程发布后必须明确的结论
调度在本轮目标槽位prepare 重启目标,新版本开始调度归属没切换,但任务代码已升级,且先于流量切换生效
调度在原流量槽位新 API 承接流量,原槽位继续运行旧版任务不能声称“定时任务也已升级”
两槽位都开、都关,或单槽位两个开关不一致发布守卫拒绝操作先排查配置与实际运行状态,不绕过检查

如果调度仍在旧版本,需要在发布前确定是否兼容。本轮涉及调度或其调用的服务逻辑时, 另行安排以下一种操作,不能把它作为切流脚本已经自动完成的步骤:

  • 保持调度归属,待原槽位变为非流量槽位后,单独将同一验证版本部署到该槽位。 这会重启调度实例并覆盖该槽位当前 JAR,可能使 active-traffic.env 记录的旧版 API 回滚目标不再匹配;先保留旧产物、重新明确回滚方案,不要直接覆盖后仍承诺一键回滚。
  • 显式交接调度归属:另行安排维护窗口,先让原实例停止接收新调度并确认在途任务, 再停用原槽位两个调度开关、启用新槽位两个开关并受控重启相关实例。 两个 JVM 的配置和重启并非原子操作,需控制短暂无调度窗口,禁止两个实例同时调度。

现有脚本没有调度交接子命令。交接前检查 Quartz 持久化任务、在途支付/退款/分账任务、 幂等和补偿能力,保存配置备份;完成后重新验证唯一调度者、版本及任务执行结果。 不要为了“两个槽位版本一致”直接覆盖仍需保留的回滚实例。

9. 回滚与中断处理 ​

bash
./Api/switch-prod.sh rollback

终端要求输入 ROLLBACK PRODUCTION。该命令使用记录的上一槽位和版本恢复正式 API 流量,要求该槽位仍指向记录的旧版本且健康。

该命令不回滚 Web、不恢复数据库、不交接或回滚调度。 例如 API 从新版 green 回退到旧版 blue 后,green 上的新版本定时任务仍可能运行, 必须单独评估是否也需要回退任务逻辑。两个 Web 的人工回退需使用发布前记录的版本目录, 按各自部署机制独立处理并验收,不能认为 API 回滚已覆盖它们。

应用或流量回滚不会撤销已经执行的数据库迁移;使用兼容旧版本的增量迁移, 不能用开发库全量覆盖正式库。

异常处理原则
开发构建或测试失败不晋升;定位问题后重新发布新版本
Flyway 校验失败停止,检查历史和迁移文件;不自动 repair、不改校验标记
候选启动失败脚本尝试恢复目标槽位旧版本;复核其健康和调度状态,流量应仍在原槽位
切流失败脚本在相应失败分支恢复 Nginx;复核实际 upstream、公网接口及状态文件,不仅看退出码
API 已切流,但某个 Web 失败先确认三个组件实际版本;在匹配 API 的干净提交上单独重试失败 Web,不重新跑完整 release
WSL/SSH 超时或本地终端中断远端操作可能继续;先检查容器、版本标记、槽位、Web 链接和日志,不盲目重发、不删除发布锁
导出开发 JAR 长时间无输出可能仍在传输;检查 /tmp/xiedao-promotion.*/release.tar.gz 是否增长及远端导出进程
npm ci 后本地 Vite 依赖请求 504可能是旧开发服务依赖缓存失效;协调重启该服务,或在独立端口 --force 启动测试服务,结束后停止自建服务

完整 release 不是断点续传入口:若 API 已在目标槽位,重新执行会在切流步骤被拒绝。 单独重试 Web 前要确认工作区干净且提交与已上线 API 清单一致。 任何中断后的版本状态不明确时,先排查再操作,不修改 .verified、清单或锁来强行通过。

开发更新失败会恢复所更新组件的旧镜像,保留迁移后的数据库。 服务器持久化数据在 /opt/xiedao-dev/data,不能用 docker compose down -v 或清理数据目录作为升级方法。

10. 小程序 ​

Docker/Web 发布不包含 App、MerchantApp 的微信上传、审核和正式发布。 两个小程序现有 envVersion 配置会为开发/体验版选择开发 API,为正式版选择正式 API。 涉及小程序代码改动时,先验证开发/体验包,再按微信发布流程单独发布。

11. 本次发布记录(2026-09-11) ​

以下是历史记录,不是下次部署时应直接复用的参数。时间为北京时间。

项目实际结果
Git 提交4b1b9b390bed91efd6eac311f6a224c5acca0f4e
功能商户平台售价管理、价格日历、价格变更记录与并发保护
开发 API / AdminWeb / MerchantWeb20260911_112058_5444
正式 APIgreen / 20260911_112058_5444
正式 AdminWeb20260911_113227_3597
正式 MerchantWeb20260911_113232_6522
正式流量切换11:32:24,从 blue 切到 green
保留的 API 回滚目标blue / 20260910_180312_4857
调度原本在 green;本次升级后仍在 green,Quartz 于 11:30:22 启动;blue 两个调度开关关闭
数据库开发 33 条、正式 31 条迁移校验通过,新增迁移执行数均为 0
正式备份/root/db-backups/xiedao_prod_before_flyway_20260911_112058_5444_20260911T032956Z.sql.gz,SHA-256 与 gzip 完整性通过
测试发布脚本 14 项通过;后端 1,403 项通过、13 项跳过;售价及结算价前端契约 4 项通过;售价浏览器回归通过
既有测试问题AdminWeb/tests/order-buyer-source-contract.test.mjs 期待 startDate/endDate,但当时前后端实际均使用 queryStartDate/queryEndDate,本次未改测试或源码
发布验收开发与正式验证码、区域查询通过;开发 Web API 代理通过;四个后台公网登录页渲染正常;正式公网首页哈希匹配本地构建;检查时 green 无新增 ERROR、无自动重启
不包含微信小程序上传/发布、真实生产订单或调价写操作验收

本次 API JAR SHA-256: 9e5c1e22a5cb897f913d9ca97a04000388eb005cd38545fe056033b74cae17ee。 历史备份和日志可能按保留策略清理,未来回滚前必须重新确认文件仍存在。

下一次应重点确认:如果正式流量和调度仍在 green,非流量目标将是 blue; 发布 blue 并切流后,green 仍可能运行这次版本的定时任务,需要按第 8 节决定是否同步升级。

12. 每次发布结束清单 ​

  • [ ] 记录提交、开发 API 版本、正式槽位与版本、两个正式 Web 版本。
  • [ ] 保存发布日志、测试通过/跳过/失败说明、业务验收范围和备份位置。
  • [ ] 确认开发和正式 API、两个环境的后台入口与代理符合预期。
  • [ ] 记录唯一调度实例、其真实代码版本、启动及执行检查结果。
  • [ ] 确认旧 API 回滚目标与 Web 旧版本仍在,说明数据库和调度回滚边界。
  • [ ] 停止本次新建的临时测试服务,确认没有遗留发布进程或源码改动。
  • [ ] 向使用方说明发布结果、遗留问题及小程序是否另行发布。

携岛 XieDao · 内部文档