Files
danyang-match/docs/Phase-20-V11-Recovery-Completion-Report-2026-09-17.md
T

339 lines
22 KiB
Markdown
Raw Normal View History

# Phase 20 V1.1 Recovery Completion Report — Analytics 数据统计与管理驾驶舱
> 日期:2026-09-17
> 阶段:Phase 20(Analytics — EXTEND)
> 分支:`main` | Tag:`phase20-recovery-2026-09-17`
> Bundle:`.workbuddy/backup/phase20-recovery-2026-09-17.bundle`(`complete history`,回读通过)
> 结论:**CLOSED(Gate 全项通过)**
---
## 一、计划目标与 Gate
官方计划(`WorkBuddy首轮启动提示词 + Phase 01~Phase 26 Jira级任务清单 + 每阶段验收条件.md` 第 1777 行起)
定义 `EPIC-P20 数据统计与管理驾驶舱`,任务 `P20-T01 ~ P20-T14`(指标字典 / User / Review / Recommendation /
Match / Chat Started / Matchmaker / Activity / Risk Metrics / Daily Summary / Rebuild / Platform Dashboard /
Organization Dashboard / Analytics 权限)。
### Gate 逐项结论
| Gate | 要求 | 结论 | 证据 |
|---|---|---|---|
| Gate ① | 每个指标登记名称、定义、分子分母、去重键、时间字段、时区、版本、负责人 | ✅ | `MetricCatalog`(36 指标 × 12 字段)+ `docs/ANALYTICS_METRICS.md`;`MetricCatalogGuardTest` 11 用例双向校验 |
| Gate ② | 固定数据集「注册10 / 实名7 / 审核通过4」显示一致 | ✅ | `AnalyticsMetricsPhase20IntegrationTest.gateUserFunnelCountsThreeDistinctConcepts` |
| Gate ③ | 2 个 Like = 1 个 Mutual Match | ✅ | `gateTwoLikesProduceOneMutualMatch` |
| Gate ④ | System Message 不算 Chat Started | ✅ | `gateSystemMessageDoesNotCountAsChatStarted` / `riskNoticeDoesNotCountAsChatStarted` |
| Gate ⑤ | 活动 10 对互选 ≠ 10 对 Success | ✅ | `gateMutualConfirmationDoesNotEqualSuccess`(互选 10 / 成功 3,且显式 `assertNotEquals`) |
| Gate ⑥ | 红娘看 20 候选、发 3 个 → Recommendation = 3 | ✅ | `gateMatchmakerRecommendationCountsOnlySent` |
| Gate ⑦ | Dashboard 与专项页相同指标必须一致 | ✅ | `gateDashboardAgreesWithDetailViews`;且由**统一口径层**从结构上保证(见 §三 D15) |
| Gate ⑧ | 服务用户数与服务人次分离 | ✅ | `gateServiceUsersDeduplicatedButVisitsAreNot`(1 人 / 4 人次) |
| Gate ⑨ | 有效匹配在口径确认前标 provisional,禁止对外宣传 | ✅ | `MetricCatalog` 标 `PROVISIONAL`;响应带 `effectiveMatchesProvisional` + 说明;`effectiveConnectionCarriesProvisionalFlag` |
| Gate ⑩ | 所有统计接口受 `analytics.view` 与 DataScope 约束,低权限 403 | ✅ | `V043` 授权 + `AnalyticsPermissionGrantTest` 6 用例(含低权限清单、`organization_admin` 刻意排除) |
| Gate ⑪ | 相同用户多次服务不污染去重人数 | ✅ | `gateServiceUsersDeduplicatedButVisitsAreNot` |
| Gate ⑫ | 软删数据不得计入统计 | ✅ | `AnalyticsSoftDeleteGuardTest`(结构性)+ 5 个行为用例 |
---
## 二、复验方法(KEEP-VERIFY)
本轮不采信任何「历史 ✅」。Phase 20 在 `STATUS.md` 里早已标注「✅ 已完成」,但复验起点是:
`AnalyticsController` 15 个端点、`AnalyticsService` 546 行**确实存在**,
其中却同时含有**硬编码 0**、**占位死代码**、**字母级口径造假**、**全员 403**、**假绿测试**。
逐项核对顺序:
1. **运行时路径**:Controller → Service → Mapper → DDL 逐跳确认无断点、无死路。
2. **生产约束**:核对迁移里的 `NOT NULL` / `CHECK` / `UNIQUE`,确认代码写入值合法。
3. **测试库保真度**:测试库是否与生产同约束(缺约束 = 该非法路径零覆盖 = 假绿)。
4. **守卫有效性**:守卫是否解析**真实 DDL/真实 SQL**、是否会**精准变红**。
5. **前端落点**:后端能力是否有真实前端入口。
---
## 三、复验查出的真实缺陷与修复
### 3.1 口径造假类(数字是"算"出来的,但算错了概念)
| 编号 | 指标 | 缺陷 | 修复 |
|---|---|---|---|
| **D1** | 实名认证通过用户数 | 用 `user_profiles.profile_status != 'rejected'`("资料没被退回")当作"已实名"。取值域里的 `in_progress`(资料还在填)、`need_edit`(被退回待改)全被算成已实名 | `UserIdentityMapper.countVerifiedUsers()`:`COUNT(DISTINCT user_id) FROM user_identities WHERE verification_status='verified'` |
| **D3** | 开启聊天会话数 | 直接数会话行数,从不看消息类型 —— 一条系统通知(「你们已互相喜欢」)就让会话计入 | `MessageMapper.countConversationsWithRealMessages()`:`COUNT(DISTINCT conversation_id) WHERE message_type NOT IN ('system','risk_notice')` |
| **D4** | Match 数 | 全来源混成一个数字,用户自主匹配 / 活动互选 / 红娘促成不可分。Gate「10 对互选 ≠ 10 对成功」在数据上无法体现 | 按 `source` 分离:`match.source.user_like` / `.activity_mutual` / `.matchmaker`,响应增 `matchesBySource` |
| **D6** | Banner 曝光量 / 点击量 | **硬编码 0**。0 被读成"投了没人点",真相是"项目里根本没有记录点击的地方" | 返回 `impressions: null` + `impressionsAvailable: false` + 原因说明;`MetricCatalog` 标 `NOT_INSTRUMENTED` |
> D1 是最危险的一条:对外汇报"实名 X 人"时,把"资料还在填"的人也算进去了,数字系统性偏高且无法自查。
### 3.2 死代码 / 占位类(后台永远显示 0)
| 编号 | 缺陷 | 修复 |
|---|---|---|
| **D5** | 活动漏斗**所有指标硬编码为 0**,并留着一行把「用户数」当「活动数」的死代码。后台"活动漏斗"不是没数据,是**根本没有查询** | 接入真实查询:`totalActivities` / `totalRegistrations` / `approvedRegistrations` / `checkedInCount`,并**三分离** `mutualBothConfirmed` / `mutualMatches` / `mutualSuccessful` |
| **D20** | `getDailySummary(days)` 无入参边界,越界静默返回 | 1~365 校验 → `BusinessException(VALIDATION_ERROR)` |
### 3.3 系统性缺陷(D12:软删数据全部计入统计)
**根因**:`BaseEntity.deletedAt` **没有** `@TableLogic` 注解,项目**也没有**配置 MyBatis-Plus 的全局逻辑删除字段。
因此 `selectCount(new LambdaQueryWrapper<T>())` 生成的 SQL 是 `SELECT COUNT(*) FROM t` ——
**软删除的数据被完整计入统计,且不产生任何报错或警告**。
| 影响 | 说明 |
|---|---|
| 累计注册用户数 | 用户删除后只增不减 |
| Banner / 活动 / 服务记录 | 软删后仍计入总数 |
| 每日汇总 | "新增用户/新增 Match"把已删记录继续算进当天 |
**修复**:`AnalyticsService` + `AnalyticsMetricsCalculator` 内**全部**统计查询显式 `deleted_at IS NULL`(共 45 处直接 Mapper 统计调用,0 处遗漏)。
**补扫(本轮第 2 轮复验发现,属 D12 未扫净)**:首轮只修了"统一口径层"这条主路径,以下 **5 处**次级路径仍在漏:
| 位置 | 后果 |
|---|---|
| `getUniqueServiceUsersStats` → `programUniqueUsers` | 分域明细与**已过滤的**总数同屏展示,各部分之和对不上总数 |
| `getUniqueServiceUsersStats` → `eligibilityUniqueUsers` | 同上 |
| `getServiceStationStats` → `totalAppointments` / `activeStations` | 服务站点统计偏高 |
| `getUserFunnel` → `genderRatio` | 分布与 `totalRegistered`(已过滤)对不上账 |
| `getUserFunnel` → `ageDistribution` | 同上 |
### 3.4 结构类(三套实现 → 无单一事实来源)
| 编号 | 缺陷 | 修复 |
|---|---|---|
| **D15** | Dashboard / 用户漏斗 / V11 专项仪表盘**各自实现了一遍**同名指标,且**互不相同**;字段名还不一致(`verifiedUsers` vs `verifiedCount`)。只要有一处漏改就永久不一致,且没有任何测试能发现 | 抽出 `AnalyticsMetricsCalculator` 作为**唯一口径来源**;`AnalyticsService` 全面委托,只保留组装与前端字段名兼容 |
### 3.5 权限授予脱节(Gate ⑩:全员 403)
| 缺陷 | 证据 | 修复 |
|---|---|---|
| V001 第 149 行定义了专看统计的角色 `data_viewer`(描述:"仅查看统计数据"),但**全仓迁移从未给它授予任何权限** —— 是个**空壳角色** | 角色存在 ≠ 角色可用 | `V043` 授予 `analytics.view` |
| `platform_admin` 的权限扩展在 V015 以 `id >= '20000000-...'`(V1.1 新增段)为条件,而 `analytics.view` 的 id 是 `10000000-...060`(**V1.0 老权限段**)→ 被漏掉 | 权限段划分造成的静默遗漏 | `V043` 一并授予 |
| **净效果**:整个 Analytics 模块(15 端点)除 `super_admin` 外**全员 403**,包括专门为它设计的 `data_viewer`。前端"数据统计"菜单对谁都是点了报错 | — | 见下 |
> 两个既有守卫(`AnalyticsController` 的 `@PreAuthorize`、`OperatorEndpointPermissionCoverageTest`)**都是绿的** ——
> 前者检查"端点有没有声明权限",后者检查"端点有没有权限注解",**都不检查"这个权限有没有授予任何角色"**。
> 这是"守卫覆盖不到的组合缺陷",故新增 `AnalyticsPermissionGrantTest` 补上「权限 → 角色」这一环。
**刻意不授予 `organization_admin`**:平台级统计接口所依赖的表(`users` / `matches` / `messages`)**没有 `organization_id` 维度**,
无法把机构管理员的数据范围限制在"本机构"。在没有范围限制的情况下授予全局统计权限 = 越权可见全平台数据。
**宁可返回 403,也不给无范围的全量数据**;待统计口径支持组织维度后再评估。
### 3.6 测试假绿(本轮替换)
| 缺陷 | 说明 |
|---|---|
| `AnalyticsServiceTest` | 由 **5 个 `assertTrue(true)`** 组成 —— **没有任何断言**,却让"Phase 20 已完成"看起来成立。测试通过不代表功能存在,只代表测试什么都没检查 | 已**删除**,替换为 `AnalyticsMetricsPhase20IntegrationTest`(21 用例,全部构造真实数据行、在 H2 上真正执行 SQL) |
### 3.7 前端落点
| 缺陷 | 修复 |
|---|---|
| `AnalyticsView.vue` 未接 `/v11-dashboard` 综合端点;公益服务数据区、活动互选vs成功、按来源分离 Match、provisional / 未埋点标注**全部缺失** | 补接端点 + 新增上述区块 + 对 `PROVISIONAL` / `NOT_INSTRUMENTED` 显式标注(避免运营把 provisional 当正式口径引用) |
### 3.8 其它修正
| 项 | 说明 |
|---|---|
| `CounselingServiceRecord` 注释过时 | 注释写 `counseling / premarital`,实际服务类型是 `relationship_guidance / emotional_support / premarital_guidance / anti_fraud_guidance` |
| `MigrationValidationTest` 基线 | `LATEST_MIGRATION` 42 → 43(新增 V043) |
---
## 四、跨阶段发现(复验 Phase 20 时发现,缺陷本体属账号生命周期模块)
按纪律「发现真实缺陷即修」,以下问题在核对**生产约束 vs 测试库**时暴露。其模块归属为 Phase 21,故单独列出。
### X1(**Critical**):账号注销写入了越界状态 → 生产必然 500
`AccountLifecycleService.invalidateLocalEligibility` 写入
`verification_status = 'failed'`(`Constants.VERIFICATION_STATUS_FAILED`)。
但生产 `V017 chk_elig_verify_status` 只允许
`declared / needs_evidence / pending_review / verified / rejected` —— **没有 `failed`**,
且**没有任何后续迁移放宽它**(已全量核对迁移目录)。
`failed` 实际属于 `user_identities` 的状态域 —— 生产 `V002` 的 CHECK 里才有它,
且 `IdentityVerificationService` 也正是这样用的。这是**跨状态域常量误用**。
**后果**:真实 PostgreSQL 上 CHECK 违约 → `closeAccount` **整个事务回滚** →
**账号注销 100% 不可用**(报 500)。而测试库此前**没有这两条 CHECK**,所以该必崩缺陷在测试期完全不可见。
> **负向验证实证**:把该行改回 `failed` 后,守卫报出的异常正是 **`DataIntegrityViolation`** ——
> 即数据库层真实拒绝了这次写入,直接证实了生产的失败模式。
**修复**:改用 `VERIFICATION_STATUS_REJECTED`(白名单内,语义为"资格失效")。
**未**选择"新增迁移把 `failed` 加进白名单" —— 那是把一个跨域误用的值固化进契约,
与本项目 Phase 19 D16「收敛权限膨胀」的处理原则一致:优先收敛,不扩契约。
### X2:`checkCanWithdraw` 的资格拦截是**死代码**
该方法用 `'submitted'` / `'pending'` 判断"本地资格审核进行中",
但这两个值在 `user_local_eligibilities` 里**永不可能出现**(同 X1 的状态域混用)
→ 拦截条件恒为 `false`,**从未生效过**:审核中的用户可以直接注销。
**修复**:改用 `needs_evidence` / `pending_review`(抽为 `LOCAL_ELIGIBILITY_IN_REVIEW` 常量并注明取值域来源)。
### X3:测试库约束漂移
测试库缺 `chk_elig_type` / `chk_elig_verify_status`。
补齐后**立即暴露出**一处既有夹具违规:`ActivityPhase14Test` 使用
`eligibility_type = "household"`(正确值是 `household_registration`)—— 已修正。
### X4 / X5:Phase 21 遗留(本轮**未**修,仅记录)
| 项 | 说明 |
|---|---|
| X4 测试假绿 | `AccountLifecycleServiceTest` 由 **7 个 `assertTrue(true)`** 组成(仅有的真实断言是几个常量字符串)。零真实覆盖,这正是 X1/X2 长期隐形的原因之一 |
| X5 未实现 | `notifyActiveMatches` / `notifyMatchmaker` / `cancelPendingActivityRegistrations` 均为 TODO 桩(只写日志,无任何查询);`checkCanWithdraw` 的 `activeMatchmakerServices` 硬编码 `0`(注释:"需要 MatchmakerServiceMapper")→ 该阻塞项永不生效 |
> X4/X5 属 Phase 21 交付范围(需产品确认通知语义与红娘服务口径),不在本轮静默扩权修复,留待 Phase 21 处理。
---
## 五、本轮新增能力(此前完全不存在)
### 5.1 指标字典代码化
| 文件 | 作用 |
|---|---|
| `MetricStatus` | `OFFICIAL` / `PROVISIONAL` / `NOT_INSTRUMENTED` —— **把"口径是否可用"本身建模** |
| `MetricDefinition` | 12 字段 record:code / name / definition / numerator / denominator / dedupeKey / timeField / timezone / version / owner / status / sourceTable |
| `MetricCatalog` | 36 个指标注册表;`of()` 未登记即抛异常;`provisional()` / `notInstrumented()` 检索 |
| `docs/ANALYTICS_METRICS.md` | 指标字典文档,与代码逐条对应;含软删强制约定、变更流程、已知待办 |
> **为什么状态本身要建模**:`0` 与「不可用」对决策的含义完全相反。
> 「Banner 点击 0」读起来是"投了没人点",真相是"没有任何地方记录点击"。
### 5.2 统一口径计算层
`AnalyticsMetricsCalculator` —— 消除三套独立实现,所有查询显式过滤软删,
方法上的 javadoc 标注其对应 `MetricCatalog` 指标编码。
### 5.3 新增指标与方法
| 层 | 新增 |
|---|---|
| `UserIdentityMapper` | `countVerifiedUsers()` |
| `MessageMapper` | `countConversationsWithRealMessages()` |
| `MatchMapper` | `countAllMatches` / `countBySource` / `countBySourceAndStatus` / `countByStatus` |
| `RecommendationRecordMapper` | `countShown` / `countShownBySource` / `countBySourceAndAction` / `countByAction` |
| `ActivityRegistrationMapper` | `countAllRegistrations` / `countRegistrationsByStatus` / `countCheckedIn` / `countRegistrationsBySource` / `countDistinctRegisteredUsers` |
| `ActivityMutualConsentMapper` | `countBothConfirmed` / `countOneSidedAccepted` |
| `V043` | `analytics.view` → `data_viewer` + `platform_admin` |
---
## 六、守卫与测试保真度(本轮新增)
| 守卫 | 用例数 | 作用 |
|---|---|---|
| `MetricCatalogGuardTest` | 11 | 代码 ↔ 文档**双向**一致;12 项元数据完整;状态诚实性(provisional/not-instrumented 必须真的被标注);去重键差异;未登记编码抛异常 |
| `AnalyticsMetricsPhase20IntegrationTest` | 21 | Gate 全部 8 类 + 软删 5 例 + 入参校验 + 可用性诚实性。**真实 DB、真实 SQL**,非 mock |
| `AnalyticsSoftDeleteGuardTest` | 4 | **结构性**约束:统计包内每处 `*Mapper.selectCount/selectList` 实参必须含 `isNull(..)`。含"防假绿"下界(扫不到足够调用点即失败) |
| `AnalyticsPermissionGrantTest` | 6 | 「权限 → 角色」授权链;低权限角色清单;`organization_admin` 刻意排除;授予的权限码必须真实存在;`super_admin` 兜底路径 |
| `LocalEligibilityStatusVocabularyGuardTest` | 9 | X1/X2 守卫:生产 DDL 白名单解析、测试库**一致性**(防漂移)、越界值被 DB 真实拒绝、注销后状态落在白名单内、审核中确实能拦截注销 |
`AnalyticsServiceTest`(5×`assertTrue(true)`)在本轮被**清除**;
`AnalyticsSoftDeleteGuardTest` 的"防假绿下界"与 `MetricCatalogGuardTest` 的解析自检,
共同保证守卫**不会因为解析失效而静默变绿**。
---
## 七、负向验证(证明守卫是承重的,而非装饰)
一次性注入缺陷(均以 `NEG-VAL` 标记),跑定向回归,确认**精准变红且无任何无关失败**,再还原。
### 第 1 轮
| 注入缺陷 | 精准变红的用例 | 断言信息 |
|---|---|---|
| 把实名口径改回 `user_profiles.profile_status <> 'rejected'` | `realNameIsNotProfileStatusProxy` | `expected: <0> but was: <5>` |
| 移除 `registeredUsers()` 的 `isNull(DeletedAt)` | `softDeletedUserIsExcludedFromRegisteredCount` | `expected: <0> but was: <1>` |
| 从 V043 移除 `data_viewer` 授权 | `dataViewerRoleIsActuallyUsable` | `当前实际授予: []` |
> `MetricCatalogGuardTest` 11/11 **保持绿** —— 口径守卫在 DB 层,目录守卫正确地**不**误报。
### 第 2 轮(针对本轮新增守卫)
| 注入缺陷 | 精准变红的用例 | 断言信息 |
|---|---|---|
| `invalidateLocalEligibility` 改回 `failed` | `closeAccountLeavesEligibilityStatusWithinWhitelist` | **`DataIntegrityViolation`** —— DB 真实拒绝 |
| `checkCanWithdraw` 改回 `submitted`/`pending` | `pendingEligibilityReviewBlocksWithdrawal` | `expected: <false> but was: <true>` |
| 移除 `programUsers` 的 `isNull(DeletedAt)` | `everyAnalyticsMapperQueryFiltersSoftDeletes`(结构性) | 列出未过滤的调用点 |
| 同上 | `softDeletedServiceRowsAreExcludedFromServiceMetrics`(行为) | `expected: <1> but was: <2>` |
> 同一注入被**两层守卫**同时捕获(源码结构 + 运行时行为),说明结构性守卫对新代码同样生效 ——
> 这正是它存在的意义:数字断言只能覆盖"被构造出来的固定数据集"。
**还原后**:`grep -rc "NEG-VAL" backend/src` = **0**;`md5sum` 与注入前**逐字节一致**。
---
## 八、回归证据
```
后端全量:Tests run: 648, Failures: 0, Errors: 0, Skipped: 3 / BUILD SUCCESS (约 6.5 分)
(复验起点 602 → 本轮 648,净 +46;其中新增守卫 9+4+6+11 = 30,集成用例 21)
前端 admin-web:vue-tsc --noEmit EXIT=0
负向验证:2 轮、7 处注入 → 7 个用例精准变红;还原后 0 残留
```
---
## 九、明确记录的设计决策(非缺陷,但需知情)
1. **`organization_admin` 不授予全局统计权限**(见 §3.5)。要在机构维度开放统计,
必须先给 `users` / `matches` / `messages` 等表建立组织维度,否则任何授予都是越权。
2. **`match.effective` 标 `PROVISIONAL`**:「有效」是否要求线下见面 / 持续 N 天,业务方未裁决。
当前是保守近似(真人会话数与 Match 数取较小值)。**禁止**作为正式成功率或写入对外宣传材料。
3. **Banner 曝光/点击保持 `NOT_INSTRUMENTED`**:不返回 `0`。要启用需先建事件表 + 上报端点 + 小程序埋点。
4. **软删过滤是每个指标的隐含前置条件**:`BaseEntity.deletedAt` 无 `@TableLogic`,
全局也无逻辑删除配置。本项目**不**建议为此加全局配置(会改变所有既有查询语义、风险外溢),
而是用结构性守卫强制显式过滤。
5. **功能开关关闭时的注册口径**:`user.registered.total` 统计**全部**未软删用户,
不按 `account_status` 过滤 —— 「注销用户」不是「从未注册用户」,两者对"累计注册"含义不同。
---
## 十、遗留与下一阶段
| 项 | 状态 |
|---|---|
| X4 `AccountLifecycleServiceTest` 的 7 个假绿断言 | ⏳ Phase 21(本轮已就资格失效路径补真实覆盖) |
| X5 通知/活动报名取消的 TODO 桩、`activeMatchmakerServices` 硬编码 0 | ⏳ Phase 21,需产品确认语义 |
| Banner 曝光/点击埋点闭环 | ⏳ 需新建事件表 + 上报端点 |
| `match.effective` 口径裁决 | ⏳ 需业务方确认 |
| `service.user.unique` 等跨域去重改为 DB 侧聚合 | ⏳ 数据量达十万级时 |
| 组织 / 站点维度统计拆分 | ⏳ 依赖组织维度建模 |
| 下一阶段 | Phase 21(等用户确认) |
---
## 附:本轮改动文件
**后端(新增)**
`analytics/metric/MetricStatus.java`、`analytics/metric/MetricDefinition.java`、
`analytics/metric/MetricCatalog.java`、`analytics/metric/AnalyticsMetricsCalculator.java`、
`db/migration/V043__grant_analytics_view_to_data_roles.sql`
**后端(修改)**
`analytics/AnalyticsService.java`(重写为委托模式)、
`account/service/AccountLifecycleService.java`(X1/X2)、
`verification/mapper/UserIdentityMapper.java`、`chat/mapper/MessageMapper.java`、
`match/mapper/MatchMapper.java`、`match/mapper/RecommendationRecordMapper.java`、
`activity/mapper/ActivityRegistrationMapper.java`、`activity_mutual/mapper/ActivityMutualConsentMapper.java`、
`counseling/entity/CounselingServiceRecord.java`
**前端**
`admin-web/src/modules/analytics/AnalyticsView.vue`
**测试**
`analytics/AnalyticsMetricsPhase20IntegrationTest.java`(新增,21)、
`analytics/MetricCatalogGuardTest.java`(新增,11)、
`analytics/AnalyticsSoftDeleteGuardTest.java`(新增,4)、
`security/AnalyticsPermissionGrantTest.java`(新增,6)、
`account/LocalEligibilityStatusVocabularyGuardTest.java`(新增,9)、
`activity/ActivityPhase14Test.java`、`deployment/MigrationValidationTest.java`、
`src/test/resources/schema-h2.sql`、~~`analytics/AnalyticsServiceTest.java`~~(删除)
**文档**
`docs/ANALYTICS_METRICS.md`(新增)