# 服务器测试与本地数据库 本文件是 `talent-card-server` 的测试约定。目标是让**新会话**不需要重新摸索环境:一条命令跑测试, 一条命令重置库,并且不重蹈已知的坑。 ## 环境一览(2026-09-17 实测) | | 库 | 版本 | Flyway | 谁在连 | | --- | --- | --- | --- | --- | | **开发库** | `192.168.110.13:3306/talent_card`(局域网共享) | MySQL **5.7.44** | **关**(`flyway.enabled=false`) | 后端、后台管理页、两个小程序 | | **测试库** | `127.0.0.1:8134/talent_card_test`(本机实例) | MySQL **8.0.36** | **开** | 本文件的测试脚本 | 三条由此推出的硬约束: - **迁移必须同时兼容 5.7 与 8.0**:测试只在 8.0 上跑,跑绿不代表开发库能执行(`V151` 的存储过程写法 已在 5.7 上实跑验证过,但每次新增迁移都值得照做一遍)。 - **开发库的结构不由 Flyway 维护**:缺表缺列只能靠启动时的初始化器 best-effort 补,补不上就手工补(见下)。 - 本机 `8134` 实例上另有 `talent_card`、`talent_card_dryrun`、`talent_card_verify1` 三个库:它们是 2026-09-16 把本机库并入服务器库**之前**的本机副本,**不再是开发库**,不要拿它们判断开发库状态。 早先会话留在 `.scratch/` 下的一次性探针(`*Probe*.java`、`Inspect*.java`、`Apply*.java` 等)大多把 URL 写死成 `127.0.0.1:8134/talent_card`,指的就是这份旧副本;复用时要显式传开发库 URL,别照默认值跑。 ## 一条命令跑测试 ```powershell cd talent-card-server .\scripts\run-server-tests.ps1 # 全量 .\scripts\run-server-tests.ps1 -Test PaymentSettlementTests # 单个类 .\scripts\run-server-tests.ps1 -Test A,B # 多个类 .\scripts\run-server-tests.ps1 -Clean -Test A # 构建产物可疑时先 clean .\scripts\run-server-tests.ps1 -Reset # 重置库后再跑 ``` 脚本会设置 `TEST_DATASOURCE_*`(MySQL `127.0.0.1:8134`、`talent_card_test`、`root/123456`, 也可用环境变量覆盖),把日志写到 `target/server-tests.log`,并在失败时列出已知基线失败。 **用 `pwsh` 跑**(本机装了 PowerShell 7:`C:\Program Files\PowerShell\7\pwsh.exe`)。脚本也已在 Windows PowerShell 5.1 下实测通过——但如果 5.1 里出现"构建在编译中途莫名中断",先换 `pwsh` 再怀疑代码: 5.1 会把 Maven 打到 stderr 的警告(例如 JVM 的 `sun.misc.Unsafe`)当成终止错误。 另注:`mvnw.cmd` 是 batch/PowerShell 混合的 wrapper,它自己**总是**用 `powershell`(即 5.1)做引导, 与你在哪个 shell 里敲命令无关——这是 Apache Maven Wrapper 3.3.4 的既有实现,不要改成 `pwsh`。 (`zcode` 的 bash 工具跑在 Git Bash 里,PowerShell 只在显式点名或这个 wrapper 内部才出现。) **必须走这个脚本(或等价地只跑一个 Maven 进程)。** 同一个 checkout 上并发跑两个构建会写坏 `target/`,症状是看起来不可能的报错——例如 `No qualifying bean of type 'HuifuPaymentProperties'`。 遇到这类"不可能"的 bean/classpath 错误,先 `-Clean`,再怀疑代码。 ## 重置测试库 ```powershell .\scripts\reset-test-db.ps1 # 默认 talent_card_test .\scripts\reset-test-db.ps1 -Database talent_card ``` 脚本用项目已有的 MySQL JDBC 驱动(本仓库没有 mysql 客户端、没有 Docker),删掉该库里的全部表; 下一次测试由 Flyway 从 `db/migration` 重建。 **不要 `drop database`,也不要只删 `flyway_schema_history` 里的某一行。** - `drop database` 之后重建会触发 `BankBranchDirectoryInitializer` 导入 10 万行网点数据(几分钟, 且曾观察到卡死)。测试 profile 已关闭该导入,所以上面的重置脚本不会再有这个代价。 - 手工删 history 行会让已执行的 DDL 与 Flyway 记录不一致,后续迁移报错更难查。 ## 不要重建开发库 开发库是局域网共享的那一个(`192.168.110.13:3306/talent_card`,见上表),不是测试库。它里面有 一批**迁移从未创建**的表——当年是直接在库里手建的,例如 `dependent_card_apply`、`merchant_benefit_rule`。 所以「把测试库结构克隆到开发库」「`reset-test-db.ps1 -Database talent_card`」这类操作会把开发库里这些 表连同数据一起删掉:2026-09-13 就这样丢过一批,最后靠一份同源快照才回灌回来(`reset-test-db.ps1` 对名为 `talent_card` 的库一律拒绝,正是为此)。 注意开发库的 `spring.flyway.enabled=false`:它**不会**被迁移自动修正,缺表缺列只能手工补。 需要让开发库结构跟上迁移时: - 只补缺失的 DDL(`create table if not exists`、`add column`),**不要**删表重建。手工补 DDL 正是 2026-09-17「点开收款能力就 500 / `Unknown column 'capability_sync_until'`」的解药。 - **有一条命令可用了**:`.\scripts\apply-sql-file.ps1 -File <某个 .sql> -Yes` 用项目已有的 JDBC 驱动 逐条执行 SQL(默认连开发库,可 `-Database/-DatabaseHost/-Port` 覆盖;不带 `-Yes` 只打印不执行)。 它认得 `DELIMITER`,所以 `db/migration/V*.sql` 也能原样重放。默认指向**开发库**,别拿它去改测试库 (测试库由 Flyway 从 `db/migration` 重建,手工 DDL 会让迁移与 history 表对不上)。 开发者首次拿到新列:2026-09-17 的 `V152`(支付流水两列)与 `V153`(手输核销码计数表)就是这样落地的, 对应脚本 `scripts/dev-schema-v152-payment-attempt.sql`(写法可重复执行的那份在迁移里,两者等价)。 - 动结构之前先备份。本机**没有** mysql 客户端、也没有 Docker(README 里指向 `D:\MySQL\...` 和 `docker exec talent-card-mysql-1` 的两条老做法都已失效),要备份就在 DB 主机上做,或照 `scripts/reset-test-db.ps1` 的办法用项目已有的 JDBC 驱动写个单文件程序。 - 初始化器是 **best-effort** 的:它的 `alter table` 等 DDL 元数据锁的上限只有 5 秒,等不到就记一条 error 放行(宁可少一列,也不能让启动被 DDL 挂住——2026-09-15 为此瘫过一次)。所以「重启过就一定有 新列」不成立:启动日志里搜 `Merchant onboarding schema: could not add`,那里会给出可直接执行的手工补救语句。 ## 迁移是只追加的 已经执行过的 `V*__*.sql` **不要改**(改一个字符就会 checksum 不匹配,测试库直接起不来)。需要调整 就新增一个迁移文件,或用 `reset-test-db.ps1` 重建后再跑。历史上 `scripts/repair-local-mysql-v*.sql` 就是为此付出的代价。 ## 已知基线失败(不是回归) 整套测试里有 5 项在基线上就失败,与本轮改动无关:前 3 项用 HEAD worktree + 同一测试库复现过, 后 2 行是连内存 H2 的用例(原因见表内)。 | 测试 | 现象 | | --- | --- | | `MerchantWorkServiceTests.filtersOrdersByCategoryStatusAndDateWithoutExposingFullMobile` | `stays.total()` 期望 1 实得 0 | | `TalentAuthServiceTests.movesWechatBindingWhenSameWechatLogsInWithAnotherMobile` | `insert into talent_session` BadSqlGrammar | | `MerchantSettlementStatisticsMapperTests.includesHotelFreeOfflineSubsidyWithoutIncreasingTransactionAmount` | 持久化断言失败 | | `ManualConsumptionServiceTests` 2 项(`childcareCurrentBenefitUsesTheScannedLevelsRightsDescription`、`cultureCurrentBenefitUsesOnlyTheScannedLevelsStoreConsumptionDescription`) | 连的是内存 **H2**,H2 里没建 `merchant` 表 → `Table "merchant" not found`。与测试库无关,与支付域改动也无关 | | `MerchantVerificationFreeStayTests.scanReturnsConfiguredHotelSubsidyAmount` | 同上:H2 里 `select merchant_type_code, merchant_rights_config from merchant` 报表不存在 | 新会话不要为了证明它们"是既有的"而重建 worktree:直接看这张表。修好其中任何一项时,请删掉对应行。 **最后两行是 2026-09-24 补的**(跑全量时共 6 项失败:3 项基线 + 这 3 项):它们不在运行脚本打印的 `known baseline failures` 清单里,但全量与单独跑那 2 个类都同样失败。原因是这几个用例自己连内存 H2 (`jdbc:h2:mem:...`)而不是共享测试库,而 H2 里没有建 `merchant` 表——判断一次改动是否引入回归时, 先把它们排除;要修就得给这几条 H2 路径补上建表(属权益/手工消费那一域,跨域改动请交给对应负责人)。 ## 环境相关失败:已修(2026-09-17) `PaymentFundConfirmationTests.aPendingConfirmationOfAVerifiedOrderIsRetriedWithoutAnotherVerification` 原先会随测试库累积而**必然**失败:补偿任务扫全库、`order by o.id asc limit 50`,用例新建的那单 id 最大、 排在窗口之外。 2026-09-17 已按本文件的建议修好——该用例先把**自己之外**的相关行移出扫描条件,于是它从「环境相关失败」 变成稳定通过;同一天还把同一处扫描改为「每单只看最新一笔成功支付」,并把它扩展到 `reverting`(撤销核销 的回退在途)状态。以后新写这类「后台任务只处理了我这条」的用例,照它的做法先腾窗口,不要再依赖测试库的 累积状况。 ## 「Too many connections」:也是环境相关,已修(2026-10-12) 全量跑着跑着,**后加载**的那个用例类会整类报错,根因是: ``` Unable to obtain connection from database: ... message from server: "Too many connections" (SQL State 08004) ``` 每个 `@SpringBootTest` 的上下文都缓存到整个 JVM 结束、各自持有一个 Hikari 连接池,而 Hikari 的 `minimum-idle` 默认等于 `maximum-pool-size`(10),**启动时就把 10 条连接建满**。一次全量会累积十几个 上下文(2026-10-12 实测 15 个),150 条空闲连接正好把测试库那个 8.0 实例的 `max_connections=151` 吃光。测试 profile 现在把 `spring.datasource.hikari.minimum-idle` 钉成 `1`(见 `src/test/resources/application-test.yml`),空闲上下文只留一条、按需再涨,连接池语义不变。 **别把它改回去**:症状是「明明只加了几个测试,却有整整一个类的用例全挂」,很容易被误判成自己引入的回归。 判断依据:报错里出现 `Too many connections` 就是它,与代码无关(同一次全量里,挂的总是**靠后**加载的 上下文,而不是某个具体用例的逻辑)。 ## 测试库跑不起来时,先看 WSL(2026-09-17 实测) 测试库那个 8.0.36 实例跑在 **WSL 的 docker 里**(发行版 `Ubuntu-26.04`,容器 `talent-card-mysql-1`, `127.0.0.1:8134->3306`)。WSL 虚拟机会在空闲时自己关掉,容器跟着停——于是测试全部报 ``` Unable to obtain connection from database: Communications link failure Caused by: java.net.ConnectException: Connection refused: getsockopt ``` 或者更绕的一句(连接失败被 Hibernate 翻译过之后的表象): ``` Unable to determine Dialect without JDBC metadata (please set 'jakarta.persistence.jdbc.url' ...) ``` 判断与恢复: ```bash wsl.exe -d Ubuntu-26.04 -u root -- docker ps # 容器在不在 wsl.exe -d Ubuntu-26.04 -u root -- docker start talent-card-mysql-1 talent-card-redis-1 ``` **光起容器不够**:跑测试期间要有东西把 VM 挂住,否则命令一返回、VM 又空闲关掉,测试照样撞在停机缝里 (现象是容器每次 `docker ps` 都显示 `Up 2 seconds`)。用一个长命进程持有它,跑完再结束: ```bash wsl.exe -d Ubuntu-26.04 -u root -- bash -lc "sleep 1800" # 后台挂着,测试跑完再停 ``` 本机 Windows 侧没有 mysqld、没有 Docker——**别在 Windows 里找 8.0 实例**,它只在 WSL 里。 ## 测试库的状态是共享的 Spring 测试共用一个 schema,测试之间不做清理(没有 `@Transactional` 回滚)。因此: - 断言要按自己创建的 ID / 单号过滤,不要断言"全表只有一行"。 - 断言"后台任务只处理了我这条"时,先把自己之外的相关行挪出时间窗口(例如把历史 `payment_transaction.created_at` 置为 `now()`),否则前面的用例会漏进来。 - 需要真实 MySQL 语义(当前所有 Spring 测试都是)时,H2 只作为没有 MySQL 时的兜底。