
你刚打完 forge-admin-server.jar,本地 IDE 启动一切正常。把同一个产物丢到服务器,日志却刷:
text
FlywayValidateException: Migration checksum mismatch for migration version ...
更膈应的是:你把 JAR 解开一看,里面的 db/migration/*.sql 和库里 forge_schema_history 对得上。按常理不该炸。Forge Admin 在 2026-10-10 的 admin-jar-flyway-deployment 一组提交里,把这件事钉死成配置契约------默认只信 classpath,不信启动目录残留。下面按真实源码与仓库执行记录写;生产侧数字来自仓库内部署执行日志,文中标明出处,不另编用户反馈。
和 2026-10-11 那篇「插件独立 Flyway / FeatureGate」不是同一题:那篇讲插件与主库 历史表不能合租 ;本篇讲 Admin 主库迁移的 扫描来源(JAR 内资源 vs 服务器旧目录)。两篇可以连着读,但查重上互不替代。
现象:三件套同时成立时,人最容易误判
独立部署踩坑时,现场常常是下面三件事 同时 成立(与 code-copilot/memory/pitfalls/db-flyway.md 条目及仓库执行记录一致;推断处标「按源码推演」):
- 解压或列举 JAR 内
db/migration,脚本集合、版本号与目标库历史对得上; - 服务器 WorkingDirectory(或旁边挂载)还躺着一份更早拷贝的
db/migration; - Flyway 报错里的 Resolved locally ,指的是它 实际扫到的那份脚本,不一定是你刚上传的 JAR 里那份。
人脑直觉是:「产物对了 = 校验对象就是产物」。Flyway 不这么想。它只认配置里的 locations。旧默认若写的是一串相对 filesystem: 路径,进程当前目录下的旧文件就会变成「本地解析结果」。于是你看到的矛盾变成了:JAR 正确,Validate 仍失败。
仓库踩坑记录(发现日期 2026-10-10)把标题写得很直白:独立 JAR 正确但 filesystem 仍读取服务器残留旧迁移 。解决方向不是先 repair,而是先核对 systemd 工作目录、启动脚本、EnvironmentFile、活动 profile,再核对 实际迁移来源。
仓库 openspec/.../admin-jar-flyway-deployment/execution-log.md 记了一次「只改 EnvironmentFile、不换 JAR」的验收:当时历史表 217 条均成功,脚本名/版本/checksum 与生产 JAR 全部一致,0 pending ;向生效的 env 增加 FORGE_FLYWAY_LOCATIONS=classpath:db/migration 并重启后,日志出现 Successfully validated 217 migrations 与 No migration necessary(版本 1.0.217)。这说明来源修对以后,不必改业务代码也能恢复启动;也说明「校验失败」未必等于「库坏了」或「脚本写错了」。
根因一:旧默认把相对 filesystem 写进了 application.yml
修复前(eee9906b4 父提交中的配置)大致是:
yaml
spring:
flyway:
enabled: ${FORGE_FLYWAY_ENABLED:true}
locations: ${FORGE_FLYWAY_LOCATIONS:filesystem:./db/migration,filesystem:../db/migration,filesystem:forge-server/db/migration}
fail-on-missing-locations: false
table: forge_schema_history
本地能绿,是因为开发机工作目录碰巧能命中源码树里的 db/migration------相对路径在「从仓库根/模块目录启动」时常常碰巧正确。生产 WorkingDirectory 若仍挂着旧拷贝,或运维习惯把 SQL 目录放在 JAR 旁「方便热修」,Flyway 就会扫到 磁盘上那份 。更糟的是 fail-on-missing-locations: false:某个相对路径不存在时可以静默跳过,排障时更难一眼看出「到底扫了哪几个目录」。
当前默认(forge-server/forge-admin-server/src/main/resources/application.yml):
yaml
spring:
flyway:
enabled: ${FORGE_FLYWAY_ENABLED:true}
# 迁移跟随应用版本打包,避免读取启动目录中残留的旧 SQL;外部路径必须显式覆盖。
locations: ${FORGE_FLYWAY_LOCATIONS:classpath:db/migration}
fail-on-missing-locations: true
baseline-on-migrate: true
baseline-version: 1.0.0
table: forge_schema_history
placeholder-replacement: false
关键差异就两句:默认只扫 classpath:db/migration ,以及 目录缺失直接失败 。外部路径只能通过环境变量显式覆盖,不再靠相对路径猜工作目录。placeholder-replacement: false 则避免 SQL 被 Spring 占位符机制改写------迁移文件必须是「落库时的字节」,不能在打包期被悄悄替换。

根因二:YAML 改了,SQL 没进 JAR,一样会挂
只改 application.yml 不够。Admin 模块 POM 必须把源码树里的迁移 原样 拷进产物,并且 关闭 filtering ,防止资源过滤把 ${...} 之类片段改坏:
xml
<!-- forge-server/forge-admin-server/pom.xml -->
<resource>
<directory>${project.basedir}/../db/migration</directory>
<targetPath>db/migration</targetPath>
<filtering>false</filtering>
<includes><include>*.sql</include></includes>
</resource>
README 约定很清楚:SQL 仍维护在 forge-server/db/migration,不要复制第二套到 src/main/resources;Maven resources 阶段复制到 target/classes/db/migration。IDE 首次运行或新增 SQL 之后,要刷新 Maven 项目并构建资源------否则出现另一种「本地玄学」:源码目录里明明有 V1.0.xxx,运行时 classpath 却没有,校验或迁移行为与同事机器不一致。
按源码推演,Docker 侧也应停止「把主机旧 SQL 目录挂进容器覆盖产物」的习惯;仓库该变更集同步删掉了 Admin Docker 的主机 SQL 挂载(Flow 既有挂载未改)。独立 JAR 与容器若一边跟 classpath、一边仍挂旧目录,会把同一套版本策略撕成两半。
怎么改:契约测试钉死默认,运维清单挡住手滑
1)跟版本走的默认,而不是跟目录走的默认
合并进主线的关键提交(短哈希,可用 git show 复核):
| Commit | 说明 |
|---|---|
190e6e280 |
补充独立部署迁移配置与回归校验 |
e16610a72 |
默认使用打包迁移并统一部署配置 |
eee9906b4 |
合并迁移来源修复到主分支 |
文档入口:forge-server/scripts/deployment/admin-flyway.md。旧版兼容或要显式钉死来源时,向 实际生效 的 EnvironmentFile 增加一行即可(不要整文件覆盖掉数据源等既有变量):
dotenv
FORGE_FLYWAY_LOCATIONS=classpath:db/migration
升级时务必检查:外置配置、JVM 参数、SPRING_FLYWAY_LOCATIONS、命令行 --spring.flyway.locations 是否仍指向旧 filesystem:。它们可能盖过你以为「YAML 已经改了」的默认值。文档明确要求:不要 classpath 与旧目录混扫 ;不要靠 repair、关校验、删历史表、改历史 checksum 掩盖来源错误。
2)用单测锁默认,比「文档写一句」抗回归
FlywayClasspathConfigurationTest(com.mdframe.forge.admin.migration)刻意不启动完整 Spring Boot、不连真实库,只绑定处理后的 application.yml,并隔离宿主环境变量:
java
@Test
void defaultsToBundledSqlAndRequiresTheLocationToExist() throws IOException {
FlywayProperties properties = bind(Map.of());
assertEquals(List.of("classpath:db/migration"), properties.getLocations());
assertTrue(properties.isFailOnMissingLocations());
assertEquals("forge_schema_history", properties.getTable());
}
@Test
void explicitExternalLocationStillOverridesTheDefaultWithoutFallback() {
// FORGE_FLYWAY_LOCATIONS=filesystem:/explicit-release/db/migration
// 必须原样生效,且仍保持 fail-on-missing-locations=true
}
@Test
void everySourceSqlIsAvailableUnchangedWithoutExtraClasspathMigrations() {
// ../db/migration 与 classpath*:db/migration/*.sql 文件名集合一致,逐字节相等
}
仓库执行记录写明该定向测试曾跑到 Tests run: 4, Failures: 0 。另有 Node 侧 admin-flyway.test.mjs 做配置契约断言。有人把默认改回 filesystem,或过滤复制导致 SQL 字节变化,测试会先炸------这比发版后靠人肉回忆「我们是不是改过 locations」可靠得多。
3)上线自查清单(建议收藏)
| # | 检查项 | 通过标准 |
|---|---|---|
| 1 | 目标是不是 Admin | unit / WorkingDirectory / ExecStart / EnvironmentFile 指向 Admin,不是门户或其它模块 |
| 2 | 历史表名 | forge_schema_history;不要误用门户的 forge_website_schema_history |
| 3 | JAR ↔ 历史 | JAR 内已执行脚本与历史 checksum 一致;先列出 pending,有 pending 就当升级而非只读探活 |
| 4 | 备份 | 备份 EnvironmentFile、unit、启动脚本;记录当前 JAR 的 SHA-256 |
| 5 | 覆盖项 | 去掉旧 filesystem:;或显式 FORGE_FLYWAY_LOCATIONS=classpath:db/migration;禁止混扫 |
| 6 | 重启范围 | 只重启 Admin;日志出现 Validate 成功与应用启动完成;systemd active ≠ 应用已就绪 |
| 7 | 复查 | 历史无失败行;若仅改来源且无 pending,历史内容不应变化 |

回滚也要分清:恢复 EnvironmentFile / unit 是 配置回滚;若该版本已经执行过新迁移,必须按该版本的数据库回滚方案处理,不能只把配置改回去就当库也回去了。
4)顺带区分:同号双文件是另一类启动炸弹
2026-10-16 pitfalls 还记了分支合并后同时存在两个 V1.0.218__...,Flyway 无法启动。那是 版本号唯一性 问题,和本文的 locations 不同,但症状都是「服务起不来」。合并或上游同步后先查重:
bash
ls forge-server/db/migration/V*.sql | sed 's/__.*//' | sort | uniq -d
已执行的那份 文件名与内容都不动;冲突的另一方改到当前最大版本 + 1。不要用删已执行脚本或改 checksum 的方式「合并」同号文件。
和你自己项目怎么对齐(按源码推演)
若你的 Spring Boot 服务也是「开发靠相对路径、生产靠 fat JAR」,发布前可在自己环境验证三步(以下为推演检查项,请用本机配置实际打印确认):
- 打印绑定后的
spring.flyway.locations实际值,以及是否被环境变量/命令行覆盖; - 看进程 WorkingDirectory 下是否还有一份
db/migration,内容是否与 JAR 内不一致; jar tf your-app.jar | grep 'db/migration'与forge_schema_history(或你的历史表)是否同版本集合。
Forge Admin 的产品选择是:默认让迁移跟随应用版本进 JAR,目录缺失失败,外部路径必须显式说出来。本地、独立 JAR、Docker 共用同一套资源约定,减少「我以为扫的是产物」的错觉。对开源后台来说,这比再写一篇 Flyway 入门更值钱------读者要的是能复用的排障顺序,不是工具百科。
小结
- 故障反差:JAR 内 SQL 与库历史一致,仍可能 checksum mismatch------因为 Resolved locally 指向了服务器残留 filesystem。
- 修复要点:默认
classpath:db/migration+fail-on-missing-locations: true+ POM 原样打包 SQL + 契约单测。 - 运维要点:清掉旧覆盖、只重启 Admin、用 7 步清单,而不是 repair 遮羞;有 pending 就按升级流程备份与审查 SQL。
如果这篇帮你少踩一个「本地绿、生产红」的坑,麻烦点个赞和收藏;评论区可以丢你卡过的 Flyway 报错原文(打码连接串),我按报错选题写下一篇。系列会继续只写 Forge Admin 真实 diff 里的坑,不写空泛发版公告。