# R01-E 批查询与会话未读数:**独立复验报告**(第三~十一节) 日期:2026-09-18 分支:`refactor/r01-structure-performance` HEAD / upstream:`b893ed67f078eedb0c4b8a03341311bd79342354`(ahead/behind = **0 / 0**) 本轮性质:**独立复验** —— 不改生产代码、不改迁移、不暂存、不提交、不推送 上游依据:`docs/r01-e-batch-query-unread-count-performance-report.md`(实施与测试加固报告) --- ## 0. 结论 > **R01-E独立代码与真实MySQL性能复验通过,可以进入精确暂存与独立提交检查点。** | 复验项 | 结论 | |---|---| | 独立查询预算(E1 = 20/20/20,E2 = 3/3/3) | **通过**(H2 与真实 MySQL 双侧实测) | | E1 collector 完整输出 H2/MySQL 深比较 | **通过**(逐字段、含 null/时间/集合顺序/关联数据) | | E2 会话未读 H2/MySQL 深比较(10 项语义) | **通过** | | MySQL `EXPLAIN FORMAT=JSON` | **通过**(基础表命中既有索引,无需新增 V46) | | 副作用与隔离(25 张表快照 + 反证 + 零残留) | **通过** | | OBS-E01 | **保留既有语义**,作为产品负责人待决策项上报 | | 独立复验时全量 `mvn test` / `mvn clean verify` | **2787 / 0 / 0 / 0** | | 提交前收口 `mvn clean verify` | **2788 / 0 / 0 / 0**(增加 1 个 EXPLAIN 变异门禁) | | OpenAPI | **176 paths / 214 operations**,`openapi-rc.json` 未改 | | 生产代码 diff | **0 个文件** | **独立复验新发现(3 项,均如实记录)**: **F1** 实施测试的 EXPLAIN 断言 `doesNotContain("\"table_name\": \"messages\", ...")` 是**空断言**—— MySQL 的 EXPLAIN JSON 里 `table_name` 存的是**别名**(本语句为 `m` / `mr`), `"table_name": "messages"` 永不出现,故该断言无论如何都会通过。结论本身(当时夹具下确实未全扫)成立, 但**断言不具备鉴别力**。提交前收口已将实施测试改为解析 JSON 树,按别名分别定位 `m` / `mr` / `ranked`,校验 access_type、key、temporary 与 filesort,并以错误索引、 基础表 `ALL`、缺失节点三类变异证明断言有效。 **F2** H2 与 MySQL 对同一 JSON 列的回读**表示不同**(详见 §4.2),已记录原始值并按语义归一后比较。 **F3** 实施报告 §9 记 `using_filesort=false`;本轮在**真实规模夹具**下实测 `using_filesort=true` (窗口函数 `ROW_NUMBER()` 的排序代价),详见 §6.4。 --- ## 1. 复验范围与不重叠声明 - 从第三~十一节执行,**未重做第一、二节库存**,未修改任何生产代码。 - 复用既有文件(**未重复创建**): - `mydreamjob-server/src/test/java/com/mydreamjob/common/r01e/R01EQueryBudgetAcceptanceTest.java`(实施测试,11 项) - `mydreamjob-server/src/test/java/com/mydreamjob/common/r01e/R01EMySqlQueryCountAndExplainTest.java`(实施测试,4 项) - `docs/r01-e-batch-query-unread-count-performance-report.md` - 独立测试**不复用**实施测试的任何夹具、断言或辅助方法:夹具、快照(25 张表、列集合不同)、 计数 DataSource、EXPLAIN 解析均为独立实现。 --- ## 2. 本轮新增文件(未跟踪) ``` mydreamjob-server/src/test/java/com/mydreamjob/common/r01e/R01EIndependentCrossDatabaseVerificationTest.java 独立测试,16 项 mydreamjob-server/src/test/java/com/mydreamjob/common/r01e/R01EIndependentSideEffectIsolationTest.java 独立测试,10 项 docs/r01-e-independent-verification-20260918.md 本报告 ``` **生产代码:0 个文件**(`git diff --stat` 为空)。 --- ## 3. 独立查询预算验证(任务 1) **方法**:独立实现的 `CountingDataSource` 包装真实 `DataSource`,记录**真实 JDBC 语句文本与条数**; 两侧在**夹具灌入完成之后**才 `reset()`,故计数**不含测试准备与清理 SQL**(断言实测语句全部为 `SELECT`)。 ### 3.1 E1:1 / 10 / 50 条投递 | 夹具 | H2 实测 | MySQL 实测 | 目标 | |---|---|---|---| | 与实施报告同基线的最小夹具(2 用户 + 1 公司 + N 岗位 + N 投递) | **[20, 20, 20]** | **[20, 20, 20]** | **20/20/20 ✔** | | 本类丰富夹具(12 个 section 全部有数据)+ N 投递 | [22, 22, 22] | [22, 22, 22] | 恒定 ✔ | - 两者都**不随记录数增长**;与实施报告「20/20/20」的结论**独立复现一致**。 - 丰富夹具为 22(比最小夹具多 2):多出的是「本人持有公司 → offers 企业侧分支」以及 「存在会话 → conversations/messages 两条批量查询」这两个**按分支固定**的查询,仍与 N 无关。 - 证据:`target/r01e-independent/e1-budget.txt`、`e1-budget-rich.txt`。 ### 3.2 E2:1 / 20 / 100 会话 | 会话数 | H2 实测 | MySQL 实测 | 目标 | |---|---|---|---| | 1 | **3** | **3** | 3 ✔ | | 20 | **3** | **3** | 3 ✔ | | 100 | **3** | **3** | 3 ✔ | - 实测 SQL 文本**恰好为 3 条**,形态固定且可辨认: 1. `select ... from conversation_members ... where user_id=?` 2. `select ... from conversations ... where id in (?,...)` 3. 分组未读查询(含 `ROW_NUMBER()`)——**只出现 1 次** - 证据:`target/r01e-independent/e2-budget.txt`(含两侧完整 SQL 文本)。 ### 3.3 空会话页不执行未读查询 | 断言 | H2 | MySQL | |---|---|---| | `listConversations(无会话用户)` 返回空 | ✔ | ✔ | | 实测语句数 | **1** | **1** | | 该语句不含 `ROW_NUMBER` | ✔ | ✔ | | 含 `row_number` 的语句数 | **0** | **0** | 即:空会话页只发出一条成员关系查询后短路返回,**不下发未读聚合查询**。 --- ## 4. E1 H2 / MySQL 深比较(任务 2) ### 4.1 方法 同一个 JVM 内同时持有两个 Spring 上下文: - **H2 侧**:测试自身上下文,`jdbc:h2:mem:r01e_ind;MODE=MySQL`(**本类独占**的内存库,Flyway 全量迁移); - **MySQL 侧**:`@BeforeAll` 手工引导的第二个上下文(Testcontainers `mysql:8.4`,`mysql-it` 配置, `WebApplicationType.NONE`,命令行参数覆盖数据源),Flyway 在容器内真实执行 45 个迁移。 两侧灌入**同一份等价夹具**(显式时间戳、无 `CURRENT_TIMESTAMP` 依赖),覆盖 **全部 12 个 section**, 并额外放入一个「外人」用户的同类数据。 **先决条件已实证**:`bothEnginesAreLiveAndFixtureIdsAgree` 断言两侧 27 个自然键 → 主键映射**完全相等** (同一 JVM、两侧均为全新建库),故后续可直接比较含主键的完整输出。 ### 4.2 深比较结果 `canonicalResult(...)` 把 collector 的**完整 section 输出**规范化为确定性文本:显式呈现 `null`、 时间按 `LocalDateTime` 呈现、List 保持原序(集合顺序)、Map 按 key 排序、`BigDecimal` 去尾零。 ``` e1-h2.txt == e1-mysql.txt (均 6213 字节,逐字符相同) ``` **JSON 列的表示差异(F2,如实记录)**——`e1-raw-json.txt` 原文: ``` h2: profile.skills="{\"java\":3,\"sql\":2}" | preferences.preferredCities="[\"北京\",\"上海\"]" mysql: profile.skills={"sql": 2, "java": 3} | preferences.preferredCities=["北京", "上海"] ``` - **H2(MODE=MySQL)**把 JSON 列回读为**被转义的 JSON 字符串标量**; - **MySQL 8**回读为 **JSON 对象字面量**(键重排、冒号后加空格)。 - 两者**语义相同、表示不同**:比较的是语义。规范化对确实是 JSON 的字符串递归解码后按键排序, 非 JSON 字符串原样保留(`content`、`coverLetter` 等不受影响)。 **反证**:把 dump 中任意一个字段值(`"张三"` → `"张四"`)置换后断言必须不再相等——通过, 说明规范化是**逐字段敏感**的,而不是只比条数、hash 或长度。 ### 4.3 其他断言 | 断言 | 结果 | |---|---| | section 顺序 | 精确等于 `profile, preferences, resumes, applications, application-timelines, interviews, interview-timelines, offers, offer-timelines, feedback, favorites, notifications, conversations, messages, candidate-scoring, scoring-appeals` ✔ | | 关联数据 | 应用↔时间线、面试↔时间线、Offer↔时间线、会话↔消息的分组关系随主键一并跨库一致 ✔ | | 其他用户数据不得混入 | dump 中不含 `李四 / 外人的简介 / 外人简历 / 外人求职信 / 外人评价 / 外人通知 / 外人会话 / out-m1 / 外人Offer / 外人理由 / 外人面试 / outsider` ✔ | | 跨库自有行数 | 23 张表的自有行数两侧完全一致 ✔ | | 非 ACTIVE 简历、`read_at` 为 null / 非 null 的通知、`score` 为 null 的维度分、`tags` 为 null 的评价、`cover_letter` 为 null 的投递 | 均已覆盖并通过 ✔ | --- ## 5. E2 H2 / MySQL 深比较(任务 3) 10 项语义逐条覆盖,**两侧结果逐值一致**(会话列表完整视图规范化后逐字符相同): | # | 语义 | H2 | MySQL | 结果 | |---|---|---|---|---| | 1 | **本人发送未读按现状计入(OBS-E01)** | c1 = 2(本人 1 + 他人 1) | c1 = 2 | ✔ 一致 | | 1b | 同上,把本人那条标记已读后 | 1 | 1 | ✔ | | 2 | 他人发送未读计入 | ✔ | ✔ | ✔ | | 3 | `message_reads` 排除 | 读 1 条后 2 → 1 | 同 | ✔ | | 3b | 另一读者的已读记录不影响本人 | 不受影响 | 不受影响 | ✔ | | 4 | `RECALLED` 排除 | 撤回 1 条后 2 → 1 | 同 | ✔ | | 5 | 多会话分组 | c1=2, c2=1, c3=1 | 同 | ✔ | | 6 | 非当前用户会话不返回 | 只返回 c1/c2/c3;cOutsider 不在 | 同 | ✔ | | 7 | 聚合缺失补 0 | 全部撤回/已读时聚合缺行 → 0 | 同 | ✔ | | 8 | **最近第 500 条计入、第 501 条排除** | 501 条未读 → **500**;把最旧一条标已读 → **仍 500**;把最新一条标已读 → **499** | 同 | ✔ | | 9 | **`created_at` 相同的排序语义** | 同刻 3 条 → 3;其中 1 条已读 → 2;同刻 501 条 → **500** | 同 | ✔ | | 10 | **空及重复 `conversationIds`** | 重复 `[c1,c1]` 只产 1 行且与 `[c1]` 逐值相同;空列表由 Service 短路、不下发 `IN ()` | 同 | ✔ | **第 8 项证明方式**:不止断言「等于 500」,而是通过「标已读最旧一条 → 不变」证明**最旧一条本就在窗口之外**, 「标已读最新一条 → 499」证明**窗口保留的是最新一侧**。 **第 9 项的诚实边界**:`ROW_NUMBER() ... ORDER BY m.created_at DESC` **没有 tiebreaker**, 故当 501 条 `created_at` 完全相同时,「保留哪 500 条」**由引擎决定**; 但**窗口计数与引擎无关,恒为 500**——两侧实测一致。此点已计入遗留风险(R2)。 --- ## 6. MySQL `EXPLAIN FORMAT=JSON`(任务 4) **被测语句**与 `MessageRepository.countUnreadByConversationIds` **逐字相同**,绑定同一批参数。 **夹具**:先铺入 **500 个背景会话 × 20 条 = 10 000 条消息**(不属于查询范围),再替换目标会话—— 否则 `messages` 表几乎只含查询范围内的行,MySQL 选择全扫是**正确**的代价决策, 却会让 EXPLAIN 失去验收意义。 ### 6.1 逐表结果(`messages` / `message_reads` / 派生结果 `ranked` 分开报告) > **注意**:MySQL 的 `EXPLAIN FORMAT=JSON` 中 `table_name` 是**别名**。 > 被测语句的别名为 `messages m`、`message_reads mr`、派生表 `ranked`, > 故表节点分别为 `m` / `mr` / `ranked`。 | 规模 | selectivity | `ranked`(派生结果) | `messages`(别名 `m`) | `message_reads`(别名 `mr`) | temporary | filesort | |---|---|---|---|---|---|---| | 1 会话 × 3 条 | 0.01% | **ALL**(派生结果顺序扫描)· rows=3 | **ref** · `idx_messages_conversation_created` · rows=3 | **eq_ref** · `uk_message_reads` · rows=1 | true | true | | 20 会话 × 3 条 | 0.27% | **ALL** · rows=60 | **range** · `idx_messages_conversation_created` · rows=60 | **eq_ref** · `uk_message_reads` · rows=1 | true | true | | 100 会话 × 3 条 | 1.36% | **ALL** · rows=300 | **range** · `idx_messages_conversation_created` · rows=300 | **eq_ref** · `uk_message_reads` · rows=1 | true | true | | **1 会话 × 600 条(> 500 窗口)** | 2.73% | **ALL** · rows=600 | **ref** · `idx_messages_conversation_created` · rows=600 | **eq_ref** · `uk_message_reads` · rows=1 | true | true | **判读**: - **派生结果 `ranked` 的 `access_type = ALL` 必须准确表述为「派生结果顺序扫描」**—— 这是 MySQL 对 `ROW_NUMBER()` 派生表**物化**后顺序读取物化结果的固有形态, `possible_keys`/`key` 均为 `null`(派生表上不存在索引)。**不得**笼统写成「无全表扫描」。 - **`messages` 基础表在四个验收断言规模下命中既有索引 `idx_messages_conversation_created`(`ref` / `range`)**; 更高选择性探测会转为 `ALL`,详见下一节,不能笼统表述为“没有全表扫描” ✔ - **`message_reads` 基础表在所有规模下均为 `eq_ref` 命中 `uk_message_reads`** ✔ (`possible_keys` 同时列出 `idx_message_reads_message` / `idx_message_reads_reader`) - `possible_keys`、`key`、`rows_examined_per_scan`、`filtered`、`using_temporary_table`、`using_filesort` 均已逐规模记录,原文见 `target/r01e-independent/e2-explain.txt`。 ### 6.2 代价拐点(选择性探测,只记录不判定) | 规模 | selectivity | `messages` 访问方式 | |---|---|---| | 2 会话 × 600 条(1 200 行) | 5.45% | **range** · `idx_messages_conversation_created`(命中索引) | | 5 会话 × 600 条(3 000 行) | 13.63% | **ALL** · key=null · rows=13 093(转为全扫) | | 20 会话 × 600 条(12 000 行) | 54.53% | **ALL** · key=null · rows=22 016(转为全扫) | **结论**:拐点被夹逼在 **5.45% 与 13.63%** 之间。也就是说, 只有当「被查询的会话集合占 `messages` 全表 6% 以上」时,MySQL 才会放弃索引改走全扫—— 这是**代价决策,不是缺索引**。真实场景中一个用户的会话列表远低于此比例, 故本项判定 **PASS**,**不新增 V46**(严格遵守指令)。 ### 6.3 `using_temporary_table = true` 的归属 `true` 出现在 `grouping_operation` 层,是 `GROUP BY ranked.conversation_id` 的固有代价, 与派生表物化同源,**不是**新增索引可以消除的。 ### 6.4 `using_filesort` 的更正(F3) 实施报告 §9 记录 `using_filesort=false`(其夹具为 1 会话 × 200 条)。 本轮在**带背景数据的真实规模夹具**下,1/20/100/600 四档**全部实测 `using_filesort=true`**—— 来源是窗口函数 `ROW_NUMBER() OVER (PARTITION BY conversation_id ORDER BY created_at DESC)` 的排序。 这是**既有实现的固有代价**,本轮不改动,仅如实更正记录。 --- ## 7. 副作用与隔离(任务 5) 独立测试 `R01EIndependentSideEffectIsolationTest`,运行在**共享的 integration H2 库**上(与实施测试同库, 因此可以直接观测实施测试的夹具残留)。 ### 7.1 逐字段快照(**25 张表**,独立于实施测试的 11 张) `users`(6 列)、`user_roles`、`companies`、`jobs`、`applications`、`application_status_history`、 `interviews`、`interview_status_history`、`offers`、`offer_status_history`、`candidate_feedback`、 `job_favorites`、`notifications`、`audit_logs`、`auth_tokens`、`conversations`、`conversation_members`、 `messages`(含 `version`)、`message_reads`、`student_profiles`、`student_preferences`、`resume_versions`、 `candidate_dimension_scores`、`candidate_scoring_appeals`、`candidate_scoring_appeal_history` | 用例 | 结论 | |---|---| | `collector.collect()` 前后 | **25 张表逐字段零变化** ✔ | | `listConversations()` 前后 | **25 张表逐字段零变化** ✔ | | `listConversations + getConversation + collect` 组合 | **25 张表逐字段零变化** ✔ | **读取路径零写**:夹具覆盖全部 12 个 section(collect 走满所有分支),快照仍零变化。 ### 7.2 通知 / 审计 / RabbitMQ | 项 | 结论 | |---|---| | `notifications` 行数与逐字段内容 | 读前后**完全相等** ✔ | | `audit_logs` 行数与逐字段内容 | 读前后**完全相等** ✔ | | **RabbitMQ** | 以 `@MockBean RabbitTemplate` 注入,读路径结束后 `verifyNoInteractions(rabbitTemplate)` **通过**——**零 AMQP 交互** ✔ | | MQ 配置 | `mysql-it` / `integration-test` 均置 `app.notification.mq-enabled: false`、`spring.rabbitmq.enabled: false`,`NotificationConsumer` 条件装配关闭 ✔ | ### 7.3 反证:快照必须真的能发现变化 | 反证用例 | 期望 | 结果 | |---|---|---| | 修改一行(`users.nickname`) | 断言必须失败 | **AssertionError,含 `users`** ✔ | | 新增一行(`notifications`) | 断言必须失败 | **AssertionError,含 `notifications`** ✔ | | 删除一行(`message_reads`) | 断言必须失败 | **AssertionError,含 `message_reads`** ✔ | | **仅单列变化**(`messages.status` → `RECALLED`) | 断言必须失败 | **AssertionError,含 `messages`** ✔ | 若无此反证,「零变化」可能只是**没在比**。 ### 7.4 夹具零残留 | 检查 | 结果 | |---|---| | 本类自有夹具(`r01eiso-%`)退场后残留 | **0**(`@AfterEach` 精确清理 + `@AfterAll` 门禁)✔ | | **实施测试的夹具(`r01e-%`,排除本类前缀)残留** | **0** ✔ | | 实施测试遗留的 `messages` / `r01e-co` / `r01e-job` | **0** ✔ | **关于此前「缺 `@AfterEach` 导致 60 项失败」的专项复验**:H2 内存库按 JVM 共享, 只有让 `R01EQueryBudgetAcceptanceTest` **先于**独立类运行,残留检查才具备鉴别力。 为消除运行顺序的不确定性,本轮**追加一次逆序运行**: ``` mvn -o -Dsurefire.runOrder=reversealphabetical -Dtest="R01EQueryBudgetAcceptanceTest,R01EMySqlQueryCountAndExplainTest,R01EIndependentCrossDatabaseVerificationTest,R01EIndependentSideEffectIsolationTest" test → 实际顺序:R01EQueryBudgetAcceptanceTest(11) → R01EMySqlQueryCountAndExplainTest(4) → R01EIndependentSideEffectIsolationTest(10) → R01EIndependentCrossDatabaseVerificationTest(16) → 独立复验:Tests run: 41, Failures: 0, Errors: 0, Skipped: 0 → 提交前收口:Tests run: 42, Failures: 0, Errors: 0, Skipped: 0(新增 EXPLAIN 变异门禁) ``` 即:实施测试跑完后,独立类的 `implementationFixturesLeaveNoResidue` 与 `@AfterAll` 门禁 **直接确认其夹具残留为 0**,旧问题已修复且可被独立观测。 --- ## 8. OBS-E01(任务 6) **观察项(本轮不改变既有语义,作为产品负责人待决策项上报)**: > **本人发送且没有当前用户 `message_reads` 记录的消息,计入本人未读数。** | 证据 | H2 | MySQL | |---|---|---| | 会话内「本人 1 条 + 他人 2 条,均未读」 | 未读 = **3**(不是 2) | **3** | | 会话内「他人 1 条 + 本人 1 条 + 1 条撤回」 | 未读 = **2** | **2** | | 把本人发送的那条写入 `message_reads` | 未读减为 **1** | **1** | - 该行为源自未读查询的 `WHERE` 中**没有发送者过滤**(只按 `conversation_id` 与读者 `message_reads` 判定)。 - 本轮**未修改 SQL、未修改任何生产代码**,仅将既有语义**如实锁定并双侧取证**。 - 若产品确认应排除本人发送的消息,属**业务语义变更**,需另立批次,不在本次性能收束范围内。 --- ## 9. 门禁结果(任务完成后的规定命令) | # | 命令 | Tests | Failures | Errors | Skipped | 结果 | |---|---|---|---|---|---|---| | 1 | `mvn -Dtest="R01EQueryBudgetAcceptanceTest,R01EMySqlQueryCountAndExplainTest,R01EIndependentCrossDatabaseVerificationTest,R01EIndependentSideEffectIsolationTest" test` | **41** | 0 | 0 | **0** | SUCCESS | | 1b | 同上 + `-Dsurefire.runOrder=reversealphabetical`(残留门禁) | **41** | 0 | 0 | **0** | SUCCESS | | 2 | `mvn -Dtest="*PersonalData*Test,*Export*Test,*Messaging*Test,*Message*Test,*Conversation*Test,*Recall*Test" test` | **150** | 0 | 0 | **0** | SUCCESS | | 3 | 独立复验 `mvn test` | **2787** | **0** | **0** | **0** | SUCCESS | | 4 | 独立复验 `mvn clean verify` | **2787** | **0** | **0** | **0** | SUCCESS | | 5 | 提交前收口 `mvn clean verify` | **2788** | **0** | **0** | **0** | SUCCESS | | 5 | `python ..\docs\verify_openapi.py` | paths **176** / operations **214** | — | — | — | `All checks passed!` | **测试计数对账**:独立复验为 `2761(R01-C 阶段二基线)+ 26(独立新增:16 跨库 + 10 副作用)= 2787`; 提交前收口再增加 1 个 EXPLAIN 变异门禁,最终为 **2788** ✔ (实施测试的 11 + 4 = 15 已含在 2761 内,本轮未新增实施测试。) ### 9.1 实施测试 / 独立测试 明细 | 类别 | 测试类 | 项数 | 引擎 | |---|---|---|---| | **实施测试** | `R01EQueryBudgetAcceptanceTest` | 11 | H2 | | **实施测试** | `R01EMySqlQueryCountAndExplainTest` | 4 | **MySQL(Testcontainers)** | | **独立测试** | `R01EIndependentCrossDatabaseVerificationTest` | 16 | **H2 + MySQL(同 JVM 双上下文)** | | **独立测试** | `R01EIndependentSideEffectIsolationTest` | 10 | H2(共享 integration 库) | | | **合计** | **41** | | ### 9.2 Testcontainers / MySQL **实际执行**证据 - Docker Server **29.6.2**;`mysql:8.4` 容器日志实测:`Container mysql:8.4 started in PT7.4093471S`。 - `R01EMySqlQueryCountAndExplainTest` 标注 `@Testcontainers(disabledWithoutDocker = true)`—— 若 Docker 不可用该类的 4 项会被 **skip**;实测 **Skipped = 0**,故容器**真实启动并执行**。 - `R01EIndependentCrossDatabaseVerificationTest` 断言 `MySql.container.isRunning()` 为 `true`, 并在 MySQL 上真实执行了 Flyway 全量迁移、跨库深比较、`ANALYZE TABLE` 与 `EXPLAIN FORMAT=JSON`。 - 两类 MySQL 用例耗时分别为 **122.5 s** 与 **17.2 s**(`clean verify` 单次运行),与容器冷启动 + 万级夹具灌入相符。 --- ## 10. 生产代码与契约不变性 | 项 | 结果 | |---|---| | 生产代码 diff | **0 个文件**(`git diff --stat` 空) | | V1–V45 迁移 / Flyway | **未修改**(目录 `git status` 0 项,仍为 45 个版本文件) ✔ | | `pom.xml` / 依赖 | **未修改** ✔ | | `ErrorCode` | **未修改** ✔ | | 公网接口 / DTO / OpenAPI 契约 | **未修改**(176 / 214) ✔ | | `docs/openapi-rc.json` | **未修改** ✔ | | 新增迁移 V46 | **未新增**(EXPLAIN 未发现必须新增索引的情形) ✔ | | Security / Token / 权限 / requestId | **未修改** | | 评分、排名、推荐、申诉、邀约、Offer、撤回与已读语义 | **未修改** | | 分布式缓存 / 冗余未读列 / 异步最终一致未读数 | **未引入** | --- ## 11. Git 状态(未暂存 / 未提交 / 未推送) | 项 | 结果 | |---|---| | 分支 / HEAD | `refactor/r01-structure-performance` / `b893ed67f078eedb0c4b8a03341311bd79342354`(**未变**) | | HEAD 与 upstream | **0 / 0** ✔ | | 暂存区 `git diff --cached --name-only` | **空** ✔ | | 本轮新增(未跟踪) | 2 个独立测试类 + 本报告 | | 既有未跟踪文件 | 完整保留,未删除、未移动 | **确认:全程未执行 `git add` / `commit` / `push`,未执行 `reset` / `restore` / `checkout` / `clean`。** --- ## 12. 遗留风险 | # | 风险 | 说明 | |---|---|---| | R1 | **OBS-E01 语义** | 本人发送且无本人 `message_reads` 的消息计入本人未读数。既有实现如此,本轮未改;如需变更属业务语义变更,须另立批次 | | R2 | **`created_at` 并列无 tiebreaker** | 未读窗口的 `ORDER BY m.created_at DESC` 无二级排序键。并列跨越 500 边界时「保留哪 500 条」由引擎决定(**计数恒为 500**,两侧一致)。`messages.created_at` 为秒级 `DATETIME`,高并发同秒写入可触发 | | R3 | **派生表物化** | MySQL 对 `ROW_NUMBER()` 派生表物化,`ranked` 为派生结果顺序扫描(`access_type=ALL`),且 `using_temporary_table=true`、`using_filesort=true` 为固有代价。大会话(> 每表 6% 选择性)下 `messages` 转为全扫,属代价决策 | | R4 | **测试自污染** | 本轮测试均按前缀精确清理;若将来新增同类夹具而不清理,仍可能污染共享 H2 库(R01-C 已建隔离门禁) | | R5 | **实施测试断言鉴别力(F1)** | 原 EXPLAIN 断言为空断言;提交前收口已改为 JSON 结构断言,并以三类变异证明有效 | --- > **R01-E独立代码与真实MySQL性能复验通过,可以进入精确暂存与独立提交检查点。**