# Q01 岗位搜索真实查询覆盖设计 ## 1. 状态 设计已确认,待实施、专项测试、真实 MySQL 复验与独立验收。 ## 2. 背景与问题 `JobSearchSpecification` 承担公开岗位搜索的核心过滤规则,但当前 `JobSearchServiceTest` 将 `JobRepository.findAll(Specification, Pageable)` Mock 掉后直接返回预设数据。这些测试可以证明 参数校验、分页对象和 DTO 组装,却不能证明 Criteria 谓词、SQL 过滤、排序和分页真的正确。 Q01 的目标是补齐真实数据库证据,不以测试数量替代查询行为覆盖,也不改变现有公网接口和业务语义。 ## 3. 范围 ### 3.1 必须覆盖 - `JobSearchSpecification.searchJobs(...)` 的真实 Repository 查询; - `JobService.searchJobs(...)` 的 latest、relevance、salary 排序和分页; - PUBLISHED 强制过滤; - keyword 对 title、description、requirements 的匹配; - city、employmentType、companyId; - salaryMin、salaryMax 的区间重叠语义; - H2 快速测试与 MySQL 8 行为一致性; - 查询参数化和特殊字符边界; - 真实 SQL/EXPLAIN 证据及数据副作用检查。 ### 3.2 明确不做 - 不改变 `/jobs/search` 请求或响应; - 不新增、删除或重命名公网接口; - 不修改岗位状态机、岗位归属或角色权限; - 不把 relevance 改造成新的全文检索算法;当前 relevance 与 latest 等价,本轮锁定该语义; - 不接入 Elasticsearch、搜索引擎或缓存; - 不新增 Flyway、表或索引; - 不改 Candidate Scoring、推荐、排名、邀约、Offer、申诉、消息、简历和认证; - 不删除现有 Mock 测试。现有测试继续承担 Service 编排与参数校验职责; - 不进入 R01 性能和结构重构。 ## 4. 当前查询契约 | 参数 | 既有语义 | |---|---| | status | 无论调用方传什么条件,只返回 `PUBLISHED` | | keyword | trim 后忽略大小写,匹配 title、description、requirements 任一字段 | | city | trim 后忽略大小写精确匹配;显式归一化以保证 H2/MySQL 一致 | | employmentType | trim 后忽略大小写精确匹配;不做前缀匹配 | | salaryMin | 岗位 `salaryMax >= salaryMin` | | salaryMax | 岗位 `salaryMin <= salaryMax` | | companyId | 精确匹配 | | sort=latest/null | `createdAt DESC, id DESC` | | sort=relevance | 当前与 latest 相同,不改变 | | sort=salary | `salaryMax` 非空优先、降序;相同值按 `id DESC`;NULL 最后 | | page | 从 0 开始,不得为负数 | | size | 1–50 | 薪资过滤是区间重叠,不是“岗位上下限都完全落在用户区间内”。边界相等必须命中。 ## 5. 测试分层 ### 5.1 H2 Repository 快速层 新增 `JobSearchSpecificationIntegrationTest`: - 使用 Spring Data JPA、真实 `JobRepository` 和真实 H2; - 激活 `integration-test`,由真实 Flyway 建表; - 不 Mock Repository、EntityManager、Specification、CriteriaBuilder 或事务; - 每个用例创建最小隔离夹具,按精确主键清理; - 直接执行 `jobRepository.findAll(spec, pageable)`; - 断言返回 ID 集合与顺序,不能只断言数量。 至少覆盖: 1. title 关键词; 2. description 关键词; 3. requirements 关键词; 4. 关键词忽略大小写; 5. null、空字符串、纯空白关键词; 6. keyword 前后空格; 7. city 精确匹配与 trim; 8. employmentType 精确匹配与 trim; 9. companyId; 10. salaryMin; 11. salaryMax; 12. 两端薪资边界相等; 13. 薪资区间重叠; 14. 薪资区间不重叠; 15. 多条件 AND; 16. DRAFT、CLOSED 等非公开状态排除; 17. 无结果; 18. 排序与分页; 19. `%`、`_`、反斜杠、引号等特殊字符的实际既有语义; 20. null salary 字段的行为。 特殊字符测试首先锁定当前 Criteria `LIKE` 行为,不得在没有产品决策时把 `%` 和 `_` 自动改成字面量搜索。 ### 5.2 Service 真实查询层 新增或扩展 `JobSearchServiceIntegrationTest`: - 使用真实 `JobService`、`JobRepository`、`CompanyRepository`、EntityManager; - 不 Mock `findAll`; - latest/null/relevance 的排序结果一致且稳定; - salary 排序验证 NULL LAST、降序、相同薪资按 ID 倒序; - 验证 page/size、totalElements、totalPages; - 验证企业信息批量组装,不改变 DTO; - 保留现有非法 sort、非法分页、非法薪资范围的测试。 ### 5.3 MySQL 真实层 优先新增 `JobSearchSpecificationMySqlIntegrationTest`,使用项目已有 Testcontainers 依赖: - `@Testcontainers(disabledWithoutDocker = true)` 仅允许本地快速执行时环境性跳过; - 独立验收和最终冻结时 Docker/MySQL 核心用例不得 SKIP; - 使用 MySQL 8.x 容器、正式 Flyway 和 `ddl-auto=validate`; - 使用动态属性注入 JDBC URL、用户名和密码; - 不打印数据库凭据; - 至少运行一组与 H2 完全相同的参数化用例,并对返回 ID 与顺序逐项对账; - 重点对账大小写、LIKE 特殊字符、NULL 排序、边界比较、分页稳定性。 如果仓库约定不允许 Testcontainers 进入普通 `mvn test`,可以将其放入独立 profile;但最终报告必须给出真实执行命令和非 SKIP 结果,不能只提交一个从未运行的测试类。 ## 6. 夹具设计 使用明确区分的岗位: - 三个包含关键词的岗位,关键词分别只存在于 title、description、requirements; - 相同城市和不同城市; - FULL_TIME、PART_TIME 等不同 employmentType; - 至少两个企业; - 薪资区间覆盖边界相等、部分重叠、不重叠、NULL; - 相同 createdAt 的两个岗位,用 ID 验证稳定排序; - 相同 salaryMax 的两个岗位,用 ID 验证稳定排序; - PUBLISHED、DRAFT、CLOSED 等状态。 夹具必须以测试生成的唯一标识隔离,不能依赖 V1_1 演示数据或真实业务数据。 ## 7. 数据库与副作用 - 查询测试不得修改岗位状态、企业、用户或其他业务表; - 查询前后对相关稳定业务字段做行级快照,而不是只比较 COUNT; - Testcontainers 由测试生命周期销毁; - 使用负责人现有 MySQL 做独立黑盒时,只能精确清理本轮夹具; - 不允许清库、重置自增、删除 volume; - 不新增 Flyway 或索引。发现执行计划问题只记录到 R01,不在 Q01 修复。 ## 8. SQL 与 EXPLAIN 真实 MySQL 验收需记录: - 组合过滤 SQL; - latest 分页; - salary 排序; - count 查询; - 使用索引、access type、estimated rows、filesort/temporary 情况; - 1、20、50 条结果规模下查询次数。 本阶段以“语义正确且无明显发布阻断”为通过条件,不为了 EXPLAIN 修改索引。 ## 9. OpenAPI 与兼容性 - 不修改 path 和 operation; - 目标仍为 176 paths / 214 operations; - 不改变参数名、默认值、响应 DTO 或错误码; - `docs/openapi-rc.json` 不应因仅新增测试而变化; - 必须运行 `docs/verify_openapi.py` 防止意外契约漂移。 ## 10. 通过条件 只有同时满足以下条件才能通过 Q01: 1. `JobSearchSpecification` 不再是测试零引用; 2. H2 真实 Repository 测试覆盖全部过滤语义; 3. Service 自定义 salary 排序有真实数据库证据; 4. MySQL 核心用例真实运行且无 SKIP; 5. H2 与 MySQL 返回 ID、顺序和边界语义一致,或明确记录并解决差异; 6. 非公开岗位绝不进入公开搜索; 7. 分页稳定、total 正确; 8. 无业务数据副作用; 9. 原 Mock 测试保留且准确标注为编排测试; 10. 全量测试和 clean verify 通过; 11. OpenAPI 保持 176/214; 12. 无新增 Flyway、ErrorCode 或公网接口。 ## 11. 最终状态 实施完成但尚未独立复验时,只能写: > Q01 已完成真实查询测试实现与自动化验证,待独立代码复验及真实 MySQL/Docker 验收。