11 KiB
DEF-01 定向整改:GET /jobs 公开列表关键词规范化
日期:2026-09-20
分支:refactor/r01-structure-performance
基线 HEAD:2319f97203a2cafcf8012869fda8c30599ffb305(整改前的 upstream 同点)
方案:方案 A(服务入口局部规范化),不采用方案 B
本轮性质:定向整改 + 独立复验;未暂存、未提交、未推送
1. 修复前最小复现
真实 HTTP(整改前,mvn 于 2319f97):
GET /api/v1/jobs?keyword=%20AlphaEngineer%20 → items=0 (期望 1:trim 后应命中)
GET /api/v1/jobs?keyword=%20%20%20 → items=0 (期望 20:空白应等同未传)
GET /api/v1/jobs/ch?keyword=AlphaEngineer → items=1
GET /api/v1/jobs → items=20
GET /api/v1/jobs/search?keyword=%20AlphaEngineer%20 → 1 (正确)
GET /api/v1/jobs/search?keyword=%20%20%20 → 20 (正确)
2. 两个端点的调用链
| 端点 | 调用链 | 关键词处理 |
|---|---|---|
GET /jobs |
JobController.listJobs:97 → JobService.listPublishedJobs:123 → JobRepository.findPublishedJobs(JPQL) |
只判 :keyword IS NULL,无 trim、无 blank 归一 |
GET /jobs/search |
JobController.searchJobs:134 → JobService.searchJobs → JobSearchSpecification:52-53 |
!keyword.isBlank() 判空 + keyword.trim() |
3. 根因
JobRepository.findPublishedJobs 的 JPQL 为:
AND (:keyword IS NULL OR j.title LIKE %:keyword% OR j.description LIKE %:keyword%)
" AlphaEngineer "不是 null → 生成LIKE '% AlphaEngineer %'→ 前导/尾随空格进入匹配串 → 0 条;" "不是 null → 生成LIKE '% %'→ 无意义的过滤条件 → 0 条;/jobs/search已做trim()与isBlank()归一 → 两条公开查询路径的输入语义不一致。
4. 为什么选方案 A
- 缺陷是输入规范化缺失,不是查询实现缺陷;
- 方案 A 把规范化放在唯一入口(
listPublishedJobs),一处生效、语义显式、可被单测直接覆盖; - 改动面最小(1 个文件、+7/−1),不触碰 Repository / Specification / Controller / DTO / OpenAPI / 排序 / 分页 / 薪资 / 城市 / 状态,回归风险最低。
5. 为什么不选方案 B
- 方案 B 让
/jobs改走JobSearchSpecification,会改变/jobs的分页 DTO 形态、排序实现与 查询计划(JobSearchItem与现有toPublicView字段并不相同),属于超出缺陷范围的语义扩张; - 两套实现并存是既有事实(
/jobs与/jobs/search本就服务不同调用方), 合并应作为独立重构批次评估,不应与本次缺陷修复捆绑。
6. 精确生产修改
唯一文件:mydreamjob-server/src/main/java/com/mydreamjob/job/JobService.java
(git diff --stat:1 file changed, 7 insertions(+), 1 deletion(-))
public Page<Map<String, Object>> listPublishedJobs(String keyword, String city, ...) {
if (salaryMin != null && salaryMax != null && salaryMin > salaryMax) {
throw new BusinessException(ErrorCode.BAD_REQUEST);
}
String normalizedKeyword = keyword == null || keyword.isBlank() ? null : keyword.trim(); // ← 新增
Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt"));
Page<Job> result = jobRepository.findPublishedJobs(normalizedKeyword, city, employmentType, // ← 原为 keyword
salaryMin, salaryMax, pageable);
return result.map(this::toPublicView);
}
(另含一段 Javadoc 说明 DEF-01 的语义契约;不改变任何行为。)
未新增通用工具类:项目内不存在语义完全相同的既有规范化工具,故保持这段局部逻辑。
7. 输入规范化决策表
| 输入 | 规范化结果 |
|---|---|
null |
null |
"" |
null |
" " |
null |
"AlphaEngineer" |
"AlphaEngineer" |
" AlphaEngineer " |
"AlphaEngineer" |
"Alpha Engineer" |
"Alpha Engineer"(中间空格不折叠) |
未做:大小写转换、中间空格折叠、SQL 拼接、Repository 改动、Specification 改动、Controller 改动、 排序/分页/薪资/城市/状态语义改动、公开岗位范围改动。
8. 新增测试方法名
mydreamjob-server/src/test/java/com/mydreamjob/job/JobListKeywordNormalizationIntegrationTest.java
(9 项,走真实链路 HTTP → SecurityFilterChain → JobController → JobService → JobRepository):
plainKeywordFindsPublishedJob、trimmedKeywordFindsSamePublishedJob、blankKeywordBehavesAsNoKeyword、
emptyKeywordBehavesAsNoKeyword、nullOrMissingKeywordListsPublishedJobs、
draftJobNeverAppearsAfterNormalization、normalizationDoesNotChangePagination、
normalizationDoesNotChangeExistingSortContract、listJobsIsReadOnly
夹具:1 条标题含 AlphaEngineer 的 PUBLISHED + 2 条其它 PUBLISHED + 1 条 DRAFT + 1 条 CLOSED;
唯一前缀 DEF01KEYWORD_;清理只按显式登记的主键,不使用 deleteAll();不依赖执行顺序。
9. 修复前失败结果
以变异验证给出权威的修复前证据(把 normalizedKeyword 改回 keyword,其余不动):
Tests run: 9, Failures: 4, Errors: 0, Skipped: 0
JobListKeywordNormalizationIntegrationTest.trimmedKeywordFindsSamePublishedJob FAIL
JobListKeywordNormalizationIntegrationTest.blankKeywordBehavesAsNoKeyword FAIL
JobListKeywordNormalizationIntegrationTest.normalizationDoesNotChangePagination FAIL
JobListKeywordNormalizationIntegrationTest.normalizationDoesNotChangeExistingSortContract FAIL
即 trim 与 blank 两类断言在缺失修复时必然失败(≥2 项,满足要求)。
10. 修复后结果
| 命令 | 结果 |
|---|---|
mvn -Dtest="JobListKeywordNormalizationIntegrationTest" test |
9 / 0 / 0 / 0 |
mvn -Dtest="*Job*Test,*JobSearch*Test" test |
219 / 0 / 0 / 0 |
11. 真实 MySQL HTTP 结果
最新 JAR(mvn clean verify 产物)+ 真实 MySQL/Redis/RabbitMQ,前缀 DEF01_20260920_:
11 / 11 通过。
| 断言 | 实测 |
|---|---|
| 普通关键词 HTTP 200 且只命中目标 | 200, [target] ✔ |
| 两侧空白与普通关键词:ID/顺序/数量一致 | pad=[T] plain=[T] ✔ |
| 纯空白与无关键词:ID/顺序/总数一致 | blank == none ✔ |
| 空字符串与无关键词:一致 | empty == none ✔ |
| DRAFT 不进入公开结果 | statuses=['PUBLISHED'] ✔ |
| 分页 metadata 不因规范化变化 | total=1,page=0,size=1,totalPages=1 ✔ |
| 默认排序不变(夹具 createdAt 降序) | none == blank,desc ✔ |
| 不返回 4xx/5xx | [200]×7 ✔ |
| 响应不泄露 SQL/异常/路径/堆栈 | leaks=[] ✔ |
对照 /jobs/search 的 trim/blank 语义一致 |
pad==plain、blank==全部 ✔ |
| 请求前后数据库逐字段零变化 | changed=none ✔ |
12. 变异测试结果
- 保存
JobService.java原始字节(sha256 前 16 位b4fab0034b34d569,14 699 字节); - 变异:
findPublishedJobs(normalizedKeyword, …)→findPublishedJobs(keyword, …); - 结果:4 项失败(见 §9);
- 按原始字节精确恢复(未使用
git restore/git checkout),恢复后 sha 一致; - 复跑新增测试 9/9 绿;校验无变异残留(
findPublishedJobs(keyword,计数 = 0,String normalizedKeyword =计数 = 1)。
13. 岗位关联回归
mvn -Dtest="*Job*Test,*JobSearch*Test" test → 219 / 0 / 0 / 0(未修改或删除任何既有断言)。
14. 全量测试数
| 命令 | Tests | Failures | Errors | Skipped | BUILD |
|---|---|---|---|---|---|
mvn test |
2797 | 0 | 0 | 0 | SUCCESS |
mvn clean verify |
2797 | 0 | 0 | 0 | SUCCESS |
2797 = 2788(基线)+ 9(本轮新增),满足「不低于 2788 + 新增数」。
Testcontainers MySQL 用例实际执行(Skipped=0)。
15. OpenAPI
python ..\docs\verify_openapi.py → paths: 176 / operations: 214 / All checks passed!
docs/openapi-rc.json 未修改(git diff 为空)。
16. 数据副作用
- 黑盒前后 7 张表(jobs / companies / users / user_roles / audit_logs / notifications / applications) 逐字段零变化;
- 单测
listJobsIsReadOnly对 5 张表逐字段零变化; - 夹具残留 0;历史未跟踪文件与历史数据(含
users.id7938/7939 的user_roles)完好; audit_logs未删除任何行。
17. 精确修改文件
M mydreamjob-server/src/main/java/com/mydreamjob/job/JobService.java (+7 −1)
?? mydreamjob-server/src/test/java/com/mydreamjob/job/JobListKeywordNormalizationIntegrationTest.java (新增,9 项)
?? docs/def-01-job-keyword-normalization-remediation-20260920.md (本报告)
?? docs/final-independent-acceptance-20260918.md (追加 DEF-01 章节)
18. Git 状态(整改与复验阶段)
| 项 | 结果 |
|---|---|
| 生产代码改动 | 仅 JobService.java 1 个文件 |
JobRepository / JobSearchSpecification / JobController |
零修改 |
其他生产模块 / Flyway / pom.xml / openapi-rc.json |
零修改 |
| 验收脚本与原始产物 | 零修改 |
19. 未涉及范围
未进入:简历隐私矩阵、管理员初始化矩阵、Q01 H2/MySQL 对账与 EXPLAIN 分表、写侧刷新链路 (四项 SKIP 仍待验收)。
20. 本轮测试自身缺陷(非产品缺陷,已修正并记录)
| # | 现象 | 判定 | 修正 |
|---|---|---|---|
| T-1 | 「带空白关键词」用例恒返回空 | 测试缺陷:MockMvc 对 URL 模板二次编码(%20 → %2520),测的是字面量 %20 而非空白参数 |
改用 .param() 传参 |
| T-2 | 分页/排序断言随更大范围运行而失败 | 测试缺陷:断言了既有契约不保证的顺序 —— 默认排序 createdAt DESC 无 tiebreaker,并列时入选子集与顺序均不稳定 |
夹具改用显式互异 created_at;分页用例只断言确定性元数据 |
| T-3 | 黑盒夹具未创建 | 脚本缺陷:seed_sms_codes/login 使用 lib.phone()(199000 号段),而建公司语句另写了 199002 号段,两者是不同号码,公司被挂到不存在的用户上(号码为本地验收自建夹具,报告中脱敏) |
统一为 lib.phone(CO) |
| T-4 | 黑盒「默认排序」断言失败 | 脚本缺陷:把「关键词过滤后的结果」与「无关键词的完整结果」做相等比较,语义不同 | 只比较同为未过滤的 none 与 blank |
21. 明确声明
未修改 /jobs/search、JobSearchSpecification、JobRepository JPQL、Controller 路由、
排序/分页/薪资/城市/状态过滤、DTO、OpenAPI、ErrorCode、SecurityConfig、V1–V45、pom.xml;
未新增 V46;未修改任何既有测试断言;未执行 git reset/restore/checkout/clean;
未使用 git add -A / git add . / 目录级暂存;未删除或移动历史未跟踪文件。
本报告与 docs/final-independent-acceptance-20260918.md 的 DEF-01 追加章节于提交前保持待提交状态。