fix(analytics): close Phase 20 KEEP-VERIFY — 20 defects (fake metric definitions, hardcoded zeros, systemic soft-delete leak, module-wide 403, fake-green tests); add MetricCatalog + unified calculator + V043 grants + 51 guard tests; full regression 648 green
口径造假: - D1 实名人数用 user_profiles.profile_status != 'rejected' 冒充 → in_progress/need_edit 被算成已实名 - D3 Chat Started 直接数会话行数 → system/risk_notice 系统通知计入 - D4 Match 不分来源 → 活动 10 对互选被当 10 对成功引用(Gate) - D6 Banner 曝光/点击硬编码 0 → 改返回 null + NOT_INSTRUMENTED 标记 死代码 / 占位: - D5 活动漏斗全部硬编码 0 + 一行把"用户数"当"活动数"的死代码 - D20 getDailySummary(days) 无边界校验 系统性软删缺陷(D12): - BaseEntity.deletedAt 无 @TableLogic 且全局无逻辑删除配置 → selectCount 把软删数据全部计入统计且无任何报错 - 修复 45 处统计调用显式 deleted_at IS NULL(2 轮复验,首轮漏扫 5 处次级路径: 服务用户数分域明细 / 服务站统计 / 性别与年龄分布) 结构类: - D15 Dashboard/用户漏斗/专项页各实现一遍同名指标且互不相同 → 抽 AnalyticsMetricsCalculator 统一口径 权限(Gate ⑩): - data_viewer 在 V001 定义却全仓从未授权(空壳角色) - platform_admin 的 V015 扩展条件 id >= '20000000-…' 漏掉 V1.0 老权限段 → 整个 Analytics 模块除 super_admin 外全员 403 - V043 授予两角色;刻意不授予 organization_admin(相关表无组织维度,无范围全量即越权) 假绿测试: - 删除 AnalyticsServiceTest(5× assertTrue(true),零断言) 新增: - 指标字典 MetricCatalog(36 指标 × 12 字段)+ MetricStatus/MetricDefinition + docs/ANALYTICS_METRICS.md - 守卫 51 用例:MetricCatalogGuardTest(11) / AnalyticsMetricsPhase20IntegrationTest(21) / AnalyticsSoftDeleteGuardTest(4,结构性) / AnalyticsPermissionGrantTest(6) / LocalEligibilityStatusVocabularyGuardTest(9) 跨阶段修复(缺陷本体属账号生命周期模块,按"发现即修"处理): - X1(Critical)注销写入 user_identities 状态域的 'failed' → 违反生产 V017 chk_elig_verify_status → 真实 PG 上 closeAccount 事务回滚、账号注销 100% 不可用(负向验证实测 DataIntegrityViolation) - X2 checkCanWithdraw 资格拦截用 submitted/pending(该表永不可能出现)→ 死代码 - X3 测试库补 chk_elig_type/chk_elig_verify_status,暴露 ActivityPhase14Test 夹具违规 验证: - 负向验证两轮共 7 处注入 → 7 个用例精准变红且无无关失败(一处被源码结构+运行时行为两层捕获) - 还原后 md5sum 逐字节一致,NEG-VAL 残留 0 - 后端全量 648 tests / 0 failures / 0 errors / BUILD SUCCESS;admin-web vue-tsc EXIT=0
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
# 善缘丹阳 · 统计指标字典(ANALYTICS_METRICS)
|
||||
|
||||
> 版本:v1 | 时区:`Asia/Shanghai` | 生效日期:2026-09-17
|
||||
> 代码载体:`backend/src/main/java/com/danyang/match/platform/analytics/metric/MetricCatalog.java`
|
||||
> 一致性守卫:`MetricCatalogGuardTest`(文档缺任何已登记指标即失败)
|
||||
|
||||
---
|
||||
|
||||
## 一、这份文档解决什么问题
|
||||
|
||||
Phase 20 之前,平台的统计数字**没有口径**。具体表现为:
|
||||
|
||||
| 现象 | 后果 |
|
||||
|---|---|
|
||||
| 「实名人数」实际算的是 `user_profiles.profile_status != 'rejected'` | 把「资料还在填」(in_progress)、「被退回待改」(need_edit) 都算成已实名,对外汇报失真 |
|
||||
| 「开启聊天数」直接数会话行数,从不看消息类型 | 一条系统通知(「你们已互相喜欢」)就让一个会话计入,指标被系统性高估 |
|
||||
| 「Match 数」不分来源 | 用户自主匹配、活动互选、红娘促成的量混成一个数字,「活动 10 对互选」被当成「10 对成功」引用 |
|
||||
| 「服务用户数」与「服务人次」只有一个概念 | 无法回答"服务了多少人"与"承载了多少次服务"这两个不同问题 |
|
||||
| Dashboard / 漏斗 / 专项页各算一遍同名指标 | 同一指标在不同页面显示不同的值,且没有任何测试能发现 |
|
||||
| 活动漏斗硬编码返回 `0` | 后台永远显示 0,不是没数据而是没查询 |
|
||||
| Banner 曝光/点击硬编码返回 `0` | 被读成"真的没人看到",而实际是"根本没有记录的地方" |
|
||||
|
||||
**本文档 + `MetricCatalog` 的作用**:让每个指标有唯一的、可被机器校验的定义,
|
||||
消除"同名不同义"和"同义不同值"。
|
||||
|
||||
---
|
||||
|
||||
## 二、口径状态(`MetricStatus`)
|
||||
|
||||
| 状态 | 含义 | 使用约束 |
|
||||
|---|---|---|
|
||||
| `OFFICIAL` | 口径已确认 | 可作为正式统计口径使用与对外引用 |
|
||||
| `PROVISIONAL` | 口径**未**确认 | **禁止**当作正式成功口径,**禁止**对外宣传;接口响应中必须带 provisional 标记 |
|
||||
| `NOT_INSTRUMENTED` | 数据源尚未埋点 | **禁止**返回 `0` 冒充真实值;必须显式返回不可用并说明原因 |
|
||||
|
||||
> **为什么状态本身要建模**:`0` 与 `不可用` 对决策的含义完全相反。
|
||||
> 「Banner 点击 0」读起来是"投了没人点",而真相是"没有任何地方记录点击"。
|
||||
> 把这两种情况混为一谈,比算错一个数字更危险。
|
||||
|
||||
---
|
||||
|
||||
## 三、指标总表
|
||||
|
||||
> 分子 / 分母为口径的可执行定义;`—` 表示该指标不是比率、无分母。
|
||||
> 去重键说明该指标的「1 个单位」是什么,**是防止口径混淆的关键字段**。
|
||||
|
||||
### 3.1 用户域
|
||||
|
||||
| 指标编码 | 名称 | 分子 | 分母 | 去重键 | 时间字段 | 时区 | 版本 | 负责人 | 状态 |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| `user.registered.total` | 累计注册用户数 | `COUNT(users.id)` | — | `users.id` | `users.created_at` | Asia/Shanghai | v1 | 平台运营 | OFFICIAL |
|
||||
| `user.identity.verified` | 实名认证通过用户数 | `COUNT(DISTINCT user_identities.user_id) WHERE verification_status='verified'` | — | `user_identities.user_id` | `user_identities.verified_at` | Asia/Shanghai | v1 | 平台运营 | OFFICIAL |
|
||||
| `user.profile.approved` | 资料审核通过人数 | `COUNT(DISTINCT user_profiles.user_id) WHERE profile_status='approved'` | — | `user_profiles.user_id` | `user_profiles.updated_at` | Asia/Shanghai | v1 | 审核员 | OFFICIAL |
|
||||
| `user.active.7d` | 近 7 日活跃用户数 | `COUNT(users.id) WHERE last_login_at >= NOW() - 7d` | — | `users.id` | `users.last_login_at` | Asia/Shanghai | v1 | 平台运营 | OFFICIAL |
|
||||
|
||||
> ⚠️ **`user.identity.verified` 与 `user.profile.approved` 是两件事**:
|
||||
> 前者是**实名认证**(身份证核验,来源 `user_identities`),
|
||||
> 后者是**资料审核**(来源 `user_profiles`)。
|
||||
> 一个人可以已实名但资料被打回,也可以资料通过但尚未实名。二者不可互相替代。
|
||||
> Gate「注册10 / 实名7 / 审核通过4」正是要求这三者分别正确且可同时成立。
|
||||
|
||||
### 3.2 推荐 / 匹配域
|
||||
|
||||
| 指标编码 | 名称 | 分子 | 分母 | 去重键 | 时间字段 | 时区 | 版本 | 负责人 | 状态 |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| `recommendation.shown.total` | 已展示推荐总数 | `COUNT(recommendation_records.id) WHERE shown_at IS NOT NULL` | — | `recommendation_records.id` | `recommendation_records.shown_at` | Asia/Shanghai | v1 | 平台运营 | OFFICIAL |
|
||||
| `recommendation.shown.matchmaker` | 红娘发出的推荐数 | `COUNT(...) WHERE source='matchmaker' AND shown_at IS NOT NULL` | — | `recommendation_records.id` | `recommendation_records.shown_at` | Asia/Shanghai | v1 | 红娘主管 | OFFICIAL |
|
||||
| `like.total` | 用户 Like 总数 | `COUNT(...) WHERE action='liked'` | — | `recommendation_records.id` | `recommendation_records.action_at` | Asia/Shanghai | v1 | 平台运营 | OFFICIAL |
|
||||
| `match.total` | Match 总数 | `COUNT(matches.id)` | — | `matches.id` | `matches.matched_at` | Asia/Shanghai | v1 | 平台运营 | OFFICIAL |
|
||||
| `match.source.user_like` | 用户互相 Like 形成的 Match 数 | `COUNT(matches.id) WHERE source='user_like'` | — | `matches.id` | `matches.matched_at` | Asia/Shanghai | v1 | 平台运营 | OFFICIAL |
|
||||
| `match.source.activity_mutual` | 活动互选形成的 Match 数 | `COUNT(matches.id) WHERE source='activity_mutual'` | — | `matches.id` | `matches.matched_at` | Asia/Shanghai | v1 | 活动管理员 | OFFICIAL |
|
||||
| `match.source.matchmaker` | 红娘促成的 Match 数 | `COUNT(matches.id) WHERE source='matchmaker'` | — | `matches.id` | `matches.matched_at` | Asia/Shanghai | v1 | 红娘主管 | OFFICIAL |
|
||||
| `match.successful` | 成功牵手数 | `COUNT(matches.id) WHERE status='successful'` | — | `matches.id` | `matches.successful_at` | Asia/Shanghai | v1 | 平台运营 | OFFICIAL |
|
||||
| `match.effective` | 有效交友匹配数 | `COUNT(matches.id) WHERE status IN ('active','getting_to_know','successful') AND EXISTS(真人消息)` | `match.total` | `matches.id` | `matches.matched_at` | Asia/Shanghai | v1 | 平台运营 | **PROVISIONAL** |
|
||||
|
||||
> ⚠️ **Gate:「2 个 Like = 1 个 Mutual Match」**。
|
||||
> `like.total` 是**单向**行为(A 喜欢 B 记 1 条),而 `match.total` 是**双向**结果。
|
||||
> 因此 Like 数天然约为 Match 数的 2 倍量级,两者**不可直接相除当作转化率的分母分子组合**去解读"效率"。
|
||||
> 若某天 Like 数与 Match 数接近 1:1,说明存在单向 Like 直接建 Match 的路径(缺陷信号)。
|
||||
|
||||
> ⚠️ **`match.effective` 处于 PROVISIONAL**:「有效」是否要求线下见面、是否要求关系持续 N 天,
|
||||
> 业务方尚未裁决。当前实现是保守近似(真人会话数与 Match 数取较小值)。
|
||||
> **禁止**将其作为正式成功率口径,**禁止**写入对外宣传材料。
|
||||
|
||||
### 3.3 聊天域
|
||||
|
||||
| 指标编码 | 名称 | 分子 | 分母 | 去重键 | 时间字段 | 时区 | 版本 | 负责人 | 状态 |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| `chat.started` | 开启聊天会话数 | `COUNT(DISTINCT messages.conversation_id) WHERE message_type NOT IN ('system','risk_notice')` | — | `messages.conversation_id` | `messages.created_at` | Asia/Shanghai | v1 | 平台运营 | OFFICIAL |
|
||||
| `chat.conversation.total` | 会话总数 | `COUNT(conversations.id)` | — | `conversations.id` | `conversations.created_at` | Asia/Shanghai | v1 | 平台运营 | OFFICIAL |
|
||||
|
||||
> ⚠️ **Gate:「System Message 不算 Chat Started」**。
|
||||
> `message_type` 的值域由生产 `chk_msg_type` 限定为 `text / image / system / risk_notice`。
|
||||
> 后两者是平台下发的通知(含风险提示),用户本人没有任何发言,**不计入**开启聊天。
|
||||
> `chat.conversation.total` 与 `chat.started` 的差额,即「建了会话但双方从未真正开口」的数量。
|
||||
|
||||
### 3.4 活动域
|
||||
|
||||
| 指标编码 | 名称 | 分子 | 分母 | 去重键 | 时间字段 | 时区 | 版本 | 负责人 | 状态 |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| `activity.total` | 活动总数 | `COUNT(activities.id)` | — | `activities.id` | `activities.created_at` | Asia/Shanghai | v1 | 活动管理员 | OFFICIAL |
|
||||
| `activity.registration.total` | 活动报名人次 | `COUNT(activity_registrations.id)` | — | `activity_registrations.id` | `activity_registrations.registered_at` | Asia/Shanghai | v1 | 活动管理员 | OFFICIAL |
|
||||
| `activity.registration.approved` | 活动报名通过人次 | `COUNT(...) WHERE status='approved'` | — | `activity_registrations.id` | `activity_registrations.reviewed_at` | Asia/Shanghai | v1 | 活动管理员 | OFFICIAL |
|
||||
| `activity.registration.checked_in` | 活动实际签到人次 | `COUNT(...) WHERE checkin_status='checked_in'` | — | `activity_registrations.id` | `activity_registrations.checkin_at` | Asia/Shanghai | v1 | 活动管理员 | OFFICIAL |
|
||||
| `activity.mutual.both_confirmed` | 活动互选成立对数 | `COUNT(...) WHERE user_a_status='accepted' AND user_b_status='accepted'` | — | `activity_mutual_connection_consents.id` | `both_confirmed_at` | Asia/Shanghai | v1 | 活动管理员 | OFFICIAL |
|
||||
| `activity.mutual.match` | 互选后建立 Match 数 | `COUNT(matches.id) WHERE source='activity_mutual'` | `activity.mutual.both_confirmed` | `matches.id` | `matches.matched_at` | Asia/Shanghai | v1 | 活动管理员 | OFFICIAL |
|
||||
| `activity.mutual.successful` | 互选后成功牵手数 | `COUNT(matches.id) WHERE source='activity_mutual' AND status='successful'` | `activity.mutual.both_confirmed` | `matches.id` | `matches.successful_at` | Asia/Shanghai | v1 | 活动管理员 | OFFICIAL |
|
||||
|
||||
> ⚠️ **Gate:「活动 10 对互选 ≠ 10 对 Success」**。
|
||||
> `activity.mutual.both_confirmed` 是**互选环节**的产出(严格双确认,不是"有人点了同意");
|
||||
> `activity.mutual.successful` 是**关系推进**的产出。
|
||||
> 中间还要经过聊天、相处、确认关系。两者绝不可互相替代引用。
|
||||
|
||||
### 3.5 公益服务域
|
||||
|
||||
| 指标编码 | 名称 | 分子 | 分母 | 去重键 | 时间字段 | 时区 | 版本 | 负责人 | 状态 |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| `service.visit.total` | 公益服务人次 | `Σ(预约 + 咨询 + 项目参与 + 资格办理) 记录数` | — | 各域记录 id(**不去重**) | 各域 `created_at` | Asia/Shanghai | v1 | 平台运营 | OFFICIAL |
|
||||
| `service.user.unique` | 服务用户数 | `COUNT(DISTINCT user_id)` 跨全部服务域 | — | `user_id`(**跨域去重**) | 各域 `created_at` | Asia/Shanghai | v1 | 平台运营 | OFFICIAL |
|
||||
| `service.appointment.total` | 服务预约人次 | `COUNT(...) WHERE status<>'cancelled'` | — | `service_appointments.id` | `created_at` | Asia/Shanghai | v1 | 服务站运营 | OFFICIAL |
|
||||
| `counseling.total` | 婚恋咨询受理量 | `COUNT(counseling_services.id)` | — | `counseling_services.id` | `created_at` | Asia/Shanghai | v1 | 咨询管理员 | OFFICIAL |
|
||||
| `service.program.member.approved` | 专项服务项目在册成员数 | `COUNT(...) WHERE status='approved'` | — | `service_program_members.id` | `created_at` | Asia/Shanghai | v1 | 平台运营 | OFFICIAL |
|
||||
| `content.published.total` | 已发布内容数 | `COUNT(...) WHERE status='published'` | — | `content_articles.id` | `published_at` | Asia/Shanghai | v1 | 内容编辑 | OFFICIAL |
|
||||
| `content.views.total` | 内容累计阅读量 | `SUM(view_count) WHERE status='published'` | — | `content_articles.id` | `published_at` | Asia/Shanghai | v1 | 内容编辑 | OFFICIAL |
|
||||
|
||||
> ⚠️ **`service.visit.total` 与 `service.user.unique` 必须分别汇报**(Phase 20 计划明确要求「分离服务用户数与服务人次」):
|
||||
> 前者衡量**服务承载量**(同一用户来 3 次算 3 人次),后者衡量**服务覆盖面**(同一用户只算 1 人)。
|
||||
> 两者比值即人均服务次数。把两者混用会同时高估覆盖面和低估承载量。
|
||||
|
||||
### 3.6 Banner 域
|
||||
|
||||
| 指标编码 | 名称 | 分子 | 分母 | 去重键 | 时间字段 | 时区 | 版本 | 负责人 | 状态 |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| `banner.total` | Banner 总数 | `COUNT(home_banners.id) WHERE deleted_at IS NULL` | — | `home_banners.id` | `created_at` | Asia/Shanghai | v1 | 内容编辑 | OFFICIAL |
|
||||
| `banner.active` | 生效中的 Banner 数 | `COUNT(...) WHERE status='active' AND NOW() BETWEEN start_at AND end_at` | — | `home_banners.id` | `NOW()` | Asia/Shanghai | v1 | 内容编辑 | OFFICIAL |
|
||||
| `banner.impression` | Banner 曝光量 | —(无数据源) | — | —(尚未埋点) | — | Asia/Shanghai | v1 | 内容编辑 | **NOT_INSTRUMENTED** |
|
||||
| `banner.click` | Banner 点击量 | —(无数据源) | — | —(尚未埋点) | — | Asia/Shanghai | v1 | 内容编辑 | **NOT_INSTRUMENTED** |
|
||||
|
||||
> ⚠️ `banner.impression` / `banner.click` **当前不可计算**:
|
||||
> 项目内不存在曝光/点击的上报端点,也不存在事件表。
|
||||
> 接口返回 `impressions: null` + `impressionsAvailable: false` + 原因说明,
|
||||
> **不返回 `0`**。要启用这两个指标,需要先建事件表与上报端点(见"待办")。
|
||||
|
||||
### 3.7 安全域
|
||||
|
||||
| 指标编码 | 名称 | 分子 | 分母 | 去重键 | 时间字段 | 时区 | 版本 | 负责人 | 状态 |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| `risk.report.pending` | 待处理举报数 | `COUNT(reports.id) WHERE status='pending'` | — | `reports.id` | `reports.created_at` | Asia/Shanghai | v1 | 风控人员 | OFFICIAL |
|
||||
| `risk.report.resolved` | 已处理举报数 | `COUNT(reports.id) WHERE status='resolved'` | — | `reports.id` | `reports.resolved_at` | Asia/Shanghai | v1 | 风控人员 | OFFICIAL |
|
||||
| `risk.user.high_risk` | 高风险用户数 | `COUNT(risk_actions.id) WHERE action_type IN ('ban','temporary_suspend') AND status='active'` | — | `risk_actions.id` | `risk_actions.created_at` | Asia/Shanghai | v1 | 风控人员 | OFFICIAL |
|
||||
|
||||
---
|
||||
|
||||
## 四、验收用的固定数据集
|
||||
|
||||
Phase 20 Gate 要求「用固定测试数据人工复算一致」。对应守卫测试 `AnalyticsMetricsPhase20IntegrationTest`:
|
||||
|
||||
| 场景 | 构造数据 | 期望 |
|
||||
|---|---|---|
|
||||
| 用户漏斗 | 注册 10、实名 7、资料审核通过 4 | 三个数字分别显示 10 / 7 / 4,且**互不干扰** |
|
||||
| Like 与 Match | A→B、B→A 各一条 Like | `like.total` = 2,`match.total` = 1 |
|
||||
| Chat Started | 会话内仅有 `system` 消息 | 不计入 `chat.started` |
|
||||
| 活动互选 | 10 对双确认,其中 3 对 `successful` | `activity.mutual.both_confirmed` = 10,`activity.mutual.successful` = 3,**不相等** |
|
||||
| 红娘推荐 | 候选池 20 人,实际发出 3 条 | `recommendation.shown.matchmaker` = 3(不是 20) |
|
||||
| 服务去重 | 同一用户 3 次预约、1 次咨询 | `service.user.unique` 计 1 人,`service.visit.total` 计 4 人次 |
|
||||
| 跨视图一致 | 同一数据集 | Dashboard、用户漏斗、专项页的同一指标值完全相等 |
|
||||
|
||||
---
|
||||
|
||||
## 五、软删口径(所有指标的强制前置条件)
|
||||
|
||||
**每一个指标都隐含一个 WHERE 条件:`deleted_at IS NULL`。** 这不是可选项。
|
||||
|
||||
### 为什么必须显式写
|
||||
|
||||
`BaseEntity.deletedAt` **没有** `@TableLogic` 注解,项目也没有配置 MyBatis-Plus 的全局逻辑删除字段。
|
||||
因此 `selectCount(new LambdaQueryWrapper<T>())` 生成的 SQL 是 `SELECT COUNT(*) FROM t` ——
|
||||
**软删除的数据会被完整计入统计,且不会产生任何报错或警告**。
|
||||
|
||||
后果是隐蔽而持续的:
|
||||
|
||||
| 场景 | 不写软删过滤的结果 |
|
||||
|---|---|
|
||||
| 用户被删除 | 「累计注册用户数」永久只增不减 |
|
||||
| Banner 下架删除 | 「Banner 总数」把已删的算在内 |
|
||||
| 活动软删 | 活动数、报名数偏高 |
|
||||
| 服务记录软删 | 服务用户数与分域明细对不上(总数已过滤、明细未过滤) |
|
||||
|
||||
这类偏差**不会有任何异常**,只会让运营看到的数字系统性偏高。
|
||||
|
||||
### 如何被强制
|
||||
|
||||
| 守卫 | 作用 |
|
||||
|---|---|
|
||||
| `AnalyticsSoftDeleteGuardTest` | 结构性约束:统计包内每一处 `*Mapper.selectCount(..)` / `selectList(..)` 的实参中**必须**出现 `isNull(..)`。覆盖尚未被想到的新查询,而不只是当前这几个。 |
|
||||
| `AnalyticsMetricsPhase20IntegrationTest` | 行为断言:软删的用户 / 实名记录 / 专项成员 / 本地资格 / 预约,均不得改变对应指标。 |
|
||||
|
||||
> 结构性守卫比逐条数字断言更有效:数字断言只能覆盖"被构造出来的固定数据集",
|
||||
> 而漏写软删过滤的高发区恰恰是**新写的、还没被想到的**统计查询。
|
||||
|
||||
### 唯一口径来源
|
||||
|
||||
所有指标计算集中在 `AnalyticsMetricsCalculator`(以及 `AnalyticsService` 中少量按状态拆分的查询),
|
||||
**不允许**在别处再实现一遍同名指标 —— 修复前 Dashboard / 用户漏斗 / 专项页各有一份实现且互不相同,
|
||||
"改了其中一处忘了另两处"是必然发生的缺陷。
|
||||
|
||||
---
|
||||
|
||||
## 六、变更流程(强制)
|
||||
|
||||
1. **新增指标**:先在 `MetricCatalog` 登记(含全部 12 个字段),再在本文件补表行,最后实现计算。
|
||||
`MetricCatalogGuardTest` 会拦截"只写代码不登记"或"登记了但文档没写"。
|
||||
2. **修改口径**:必须递增 `version`(如 v1 → v2),并在本文件记录变更原因。
|
||||
未递增版本会让历史数据与新数据被误当成同一口径比较。
|
||||
3. **降级为 PROVISIONAL**:接口响应必须带 provisional 标记,
|
||||
且**禁止**出现在任何对外的成功率/宣传口径中。
|
||||
4. **不得返回硬编码 `0` 掩盖缺失数据源**:无数据源一律用 `NOT_INSTRUMENTED`。
|
||||
|
||||
---
|
||||
|
||||
## 七、已知待办
|
||||
|
||||
| 项 | 说明 |
|
||||
|---|---|
|
||||
| Banner 曝光/点击埋点 | 需新建 `home_banner_events` 事件表 + 上报端点 + 小程序端埋点,闭环后才可把两个指标从 `NOT_INSTRUMENTED` 提升为 `OFFICIAL` |
|
||||
| `match.effective` 口径裁决 | 需业务方确认「有效」的判定标准(是否要求见面 / 持续 N 天),确认后提升为 `OFFICIAL` |
|
||||
| 组织 / 站点维度拆分 | 当前统计为平台级;按组织或服务站拆分需叠加 `DataScope`(见 Phase 20 权限部分) |
|
||||
| 大数据量优化 | `service.user.unique` 等跨域去重指标目前为内存去重;数据量达十万级时应改为数据库侧 `UNION` 聚合 |
|
||||
+32
-2
@@ -24,7 +24,36 @@
|
||||
> 证据:`docs/Phase-18-V11-Recovery-Completion-Report-2026-09-17.md`。
|
||||
>
|
||||
> **2026-09-17 Phase 19 复验关闭:** EPIC-P19 已完成。复验不采信任何「历史 ✅」,逐条核对运行时路径与生产约束,查出并修复 **25 处真实缺陷**。高价值项:①**服务站「临时公告」整条不可达** —— `ServiceLocationAnnouncementService` 与 V042 表都已存在,但**全仓零端点**,后台与小程序都拿不到(D20);②**服务站 `create/update` 越权写** —— `@PreAuthorize("service_location.view")` 挂在**类**上,V015 下发的 `service_location.manage` 是**从未被使用的死权限**,只读角色可直接改站点(D21);③该控制器**未纳入** `OperatorEndpointPermissionCoverageTest` 扫描清单,导致 D21 长期隐形(D22);④Banner 校验失败抛裸 `RuntimeException` → **500**(应 400)+ 软删 Banner 仍上首页(D2/D3);⑤Content「发布」死路(后台复用只返回 published 的公开接口)+ 分类停用不可恢复(D5/D6);⑥ServiceProgram 审批死路 / 草稿泄露 / 成员数恒 0 / 退出未实现 / 重复申请撞唯一键(D10~D14);⑦测试库缺 `content_categories` 与 `service_location_announcements` → 对应路径**零覆盖**(D9/D23);⑧预约 IDOR(D18);⑨`MigrationValidationTest` 基线未跟进 V042(D24)。新增守卫:`ServiceLocationAnnouncementPhase19IntegrationTest`(24) / `ServiceLocationControllerPermissionContractTest`(5) / `ServiceLocationAnnouncementConstraintParityTest`(5,生产 DDL↔测试库↔后端白名单↔后台标签四方一致);`V11PermissionMatrixContractTest` 由硬编码 V015+V029 改为扫描全部迁移。负向验证:注入 3 处缺陷(移除软删过滤 / 移除测试库 CHECK / 写端点降级为 `view`)→ **4 个用例精准变红且无无关失败**,还原后 `NEG-VAL` 残留 0。后端 **602 tests 全绿**;admin-web `vue-tsc` EXIT=0 + `vite build` EXIT=0;小程序 `tsc --noEmit` EXIT=0;ui-tests 14/14 PASS。
|
||||
> 证据:`docs/Phase-19-V11-Recovery-Completion-Report-2026-09-17.md`。**Phase 20 尚未获准。**
|
||||
> 证据:`docs/Phase-19-V11-Recovery-Completion-Report-2026-09-17.md`。
|
||||
>
|
||||
> **2026-09-17 Phase 20 复验关闭:** EPIC-P20 已完成。`STATUS.md` 早已把 Phase 20 标为「✅ 已完成」,
|
||||
> 复验起点是 `AnalyticsController` 15 端点 + `AnalyticsService` 546 行**确实存在**,
|
||||
> 但其中同时含有**硬编码 0**、**占位死代码**、**字母级口径造假**、**全员 403**、**假绿测试**。
|
||||
> 逐条核对运行时路径与生产约束后修复:①**口径造假** —— 实名人数用 `user_profiles.profile_status != 'rejected'`
|
||||
> 冒充(把"资料还在填"算成已实名);Chat Started 直接数会话行数(系统通知计入);
|
||||
> Match 不分来源(10 对互选被当 10 对成功引用);**Banner 曝光/点击硬编码 0**;②**死代码** ——
|
||||
> 活动漏斗**全部硬编码 0**(不是没数据,是根本没有查询)+ 一行把"用户数"当"活动数"的死代码;
|
||||
> ③**系统性软删缺陷(D12)** —— `BaseEntity.deletedAt` 无 `@TableLogic`、全局无逻辑删除配置,
|
||||
> `selectCount` 把**软删数据全部计入统计且无任何报错**,修复 45 处统计调用(**2 轮复验,首轮漏扫 5 处次级路径**);
|
||||
> ④**权限授予脱节(Gate ⑩)** —— `data_viewer` 角色被定义却**从未授权**(空壳角色),
|
||||
> `platform_admin` 因 `id >= '20000000-…'` 漏掉 V1.0 老权限段 →
|
||||
> **整个模块除 super_admin 外全员 403**(含专为它设计的 `data_viewer`);⑤**三套独立实现** ——
|
||||
> Dashboard/用户漏斗/专项页各实现一遍同名指标且互不相同、字段名还不一致(`verifiedUsers` vs `verifiedCount`);
|
||||
> ⑥**假绿测试** —— `AnalyticsServiceTest` = 5×`assertTrue(true)`(零断言)。
|
||||
> 建立指标字典(`MetricCatalog` 36 指标 × 12 字段 + `docs/ANALYTICS_METRICS.md`)与统一口径层
|
||||
> `AnalyticsMetricsCalculator`;新增 V043 打通 `analytics.view` 授权(刻意不授予 `organization_admin` ——
|
||||
> 相关表无组织维度,无范围的全量数据即越权);新增守卫 `MetricCatalogGuardTest`(11) /
|
||||
> `AnalyticsMetricsPhase20IntegrationTest`(21) / `AnalyticsSoftDeleteGuardTest`(4,**结构性**约束:统计包内
|
||||
> 每处 Mapper 统计调用必须显式含 `isNull(..)`,含防假绿下界) / `AnalyticsPermissionGrantTest`(6)。
|
||||
> **跨阶段发现(本轮已修 X1~X3)**:`AccountLifecycleService` 注销时把 `user_identities` 状态域的 `'failed'`
|
||||
> 写入本地资格表 → 违反生产 `V017 chk_elig_verify_status` → **真实 PG 上 `closeAccount` 事务回滚、
|
||||
> 账号注销 100% 不可用**(负向验证实测报 `DataIntegrityViolation`);`checkCanWithdraw` 的资格拦截用
|
||||
> `submitted`/`pending`(该表永不可能出现的值)→ **死代码**;测试库补 `chk_elig_type`/`chk_elig_verify_status`
|
||||
> 后立即暴露 `ActivityPhase14Test` 夹具违规(`household` → `household_registration`)。新增
|
||||
> `LocalEligibilityStatusVocabularyGuardTest`(9)。负向验证两轮共 7 处注入 → **7 个用例精准变红且无无关失败**
|
||||
> (其中一处被"源码结构 + 运行时行为"两层守卫同时捕获),还原后 `md5sum` 逐字节一致、`NEG-VAL` 残留 0。
|
||||
> 后端 **648 tests 全绿**;admin-web `vue-tsc --noEmit` EXIT=0;**Phase 21 尚未获准。**
|
||||
> 证据:`docs/Phase-20-V11-Recovery-Completion-Report-2026-09-17.md`。
|
||||
|
||||
> **2026-09-16 状态校正:** Phase 07 UI 已按 2026-09-13 规格重新实现,**首批 11 页于 2026-09-16 通过用户验收(结论:基本达标)**,本文件此前"视觉否决/REOPENED"口径随之关闭。
|
||||
>
|
||||
@@ -148,6 +177,7 @@
|
||||
| Phase 17 举报与风控复验 | 已完成(2026-09-17) | 见 `Phase-17-V11-Recovery-Completion-Report-2026-09-17.md`;修复 **6 处真实缺陷**(**D1(Critical,必崩级盲区)测试库缺 `reports`/`risk_events`/`risk_actions` 三表 → 风控/举报真实 DB 路径零覆盖,`ReportServiceTest` 全 `assertTrue(true)` 假绿**;**D3(High)敏感访问(查看他人风险画像/举报)无审计(违反 AGENTS.md #8)**;D4 审计缺 `ip`/`userAgent`;D5 `findByUserId` 未过滤软删;D2 人工标记无审计;**D6 `javax`→`jakarta` 隐藏编译缺陷 + `risk_events` 漏 `updated_at` 列**);补齐三表并移出白名单入 `REQUIRED_IN_TEST_SCHEMA`;新增 `AuditLogService.recordSensitiveAccess` 并接线三端点;新增 `ReportAndRiskPhase17IntegrationTest`(7 用例);负向验证(注释 getReportDetail 审计 → D3 守卫精准变红并还原);后端 **488 tests 全绿**;**Phase 18 尚未获准** |
|
||||
| Phase 18 申诉复验 | 已完成(2026-09-17) | 见 `Phase-18-V11-Recovery-Completion-Report-2026-09-17.md`;修复 **8 处真实缺陷**(**D1(Critical,必崩级盲区)测试库缺 `appeals` 表 + `AppealServiceTest` 9 例中 6 例 `assertTrue(true)` 假绿 → 申诉真实 DB 路径零覆盖**;**D2(High,只在 PG 上炸)`escalateAppeal` 向 `operatorId` 发用户通知撞 `notifications.user_id → users(id)` FK → 升级事务整体回滚**;**D3(High,只在 PG 上炸)`appealType` 未校验生产 `chk_appeal_type` 白名单**;D4 escalated 死胡同(前后端均无法再决策);D5 `processAppeal` 先读后写并发竞态;D6 三处查询漏 `deleted_at` 过滤;D7 硬编码 `resolved`;D8 证据字段契约 `evidenceFileIds`);补 `appeals` 表并移出白名单入 `REQUIRED_IN_TEST_SCHEMA`;新增原子认领 `markReviewingIfProcessable`;新增 `AppealPhase18IntegrationTest`(15 用例),删除假绿 `AppealServiceTest`(9 用例);负向验证(误伤其他处置 → Gate 用例精准变红并还原);后端 **494 tests 全绿**;admin-web `vue-tsc` EXITCODE=0;**Phase 19 尚未获准** |
|
||||
| Phase 19 统一运营后台 + 用户端公益服务页面复验 | 已完成(2026-09-17) | 见 `Phase-19-V11-Recovery-Completion-Report-2026-09-17.md`;修复 **25 处真实缺陷**(**D20 服务站公告能力整条不可达:Service + V042 表都在、全仓零端点**;**D21 服务站 `create/update` 越权写:类级 `service_location.view` 兜底,`service_location.manage` 是从未使用的死权限**;D22 该控制器未纳入权限覆盖守卫;**D2/D3 Banner 校验 500 + 软删 Banner 仍上首页**;**D5/D6 Content 发布死路 + 分类停用不可恢复**;D10~D14 ServiceProgram 审批死路/草稿泄露/成员数恒 0/退出未实现/撞唯一键;D9/D23 测试库缺 `content_categories` 与 `service_location_announcements` → 零覆盖;D18 预约 IDOR;D24 迁移基线未跟进 V042);补 V042 公告表 + 运营端五端点 + 用户端只读端点 + 后台公告管理 UI + 小程序 U50 公告区块;新增 3 组守卫(24+5+5 用例)并把服务站运营端收入权限覆盖扫描;负向验证 3 注入 → 4 用例精准变红;后端 **602 tests 全绿**;admin-web `vue-tsc`/`vite build` 双绿;小程序 `tsc` EXIT=0;ui-tests 14/14;**Phase 20 尚未获准** |
|
||||
| Phase 20 Analytics 数据统计与管理驾驶舱复验 | 已完成(2026-09-17) | 见 `Phase-20-V11-Recovery-Completion-Report-2026-09-17.md`;修复 **20 处真实缺陷**(**D1 实名人数用 `profile_status != 'rejected'` 冒充 → 把"资料还在填"算成已实名**;**D3 Chat Started 直接数会话行数 → 系统通知计入**;**D4 Match 不分来源 → 10 对互选被当 10 对成功引用**;**D5 活动漏斗全部硬编码 0 + 一行把"用户数"当"活动数"的死代码**;**D6 Banner 曝光/点击硬编码 0**;**D12 系统性软删缺陷:`BaseEntity.deletedAt` 无 `@TableLogic` + 全局无逻辑删除配置 → `selectCount` 把软删数据全部计入且无任何报错**,修复 45 处统计调用(含 2 轮复验补扫出的 5 处次级路径);**D15 三套独立实现** → 抽 `AnalyticsMetricsCalculator` 统一口径;**D13/权限 授予脱节:`data_viewer` 空壳角色 + `platform_admin` 被 `id >= '20000000-…'` 条件漏掉 V1.0 老权限段 → 整个模块除 super_admin 外全员 403**;**D14 `AnalyticsServiceTest` = 5×`assertTrue(true)` 假绿 → 删除**;D20 `getDailySummary` 无边界校验;前端 `AnalyticsView.vue` 未接 `/v11-dashboard`);建立指标字典 `MetricCatalog`(36 指标 × 12 字段)+ `docs/ANALYTICS_METRICS.md`;新增 V043 授权(**刻意排除 `organization_admin`** —— 相关表无组织维度,无范围全量数据即越权);新增守卫 `MetricCatalogGuardTest`(11) / `AnalyticsMetricsPhase20IntegrationTest`(21) / `AnalyticsSoftDeleteGuardTest`(4,结构性) / `AnalyticsPermissionGrantTest`(6);**跨阶段修复 X1~X3**(**注销写入 `user_identities` 状态域的 `'failed'` → 违反 V017 `chk_elig_verify_status` → 真实 PG 上账号注销 100% 不可用**;`checkCanWithdraw` 资格拦截死代码;测试库补两条 CHECK 后暴露 `ActivityPhase14Test` 夹具违规)并新增 `LocalEligibilityStatusVocabularyGuardTest`(9);负向验证两轮 7 注入 → 7 用例精准变红并还原(`md5sum` 逐字节一致);后端 **648 tests 全绿**;admin-web `vue-tsc` EXIT=0;**Phase 21 尚未获准** |
|
||||
| UI-CLEANUP-20260913 | 已完成 | 废弃 UI 代码、冲突规范、旧计划和无效验收证据已清理;`ui-document-baseline-contracts` 锁定唯一视觉入口 |
|
||||
| UI-ADMIN-20260913 | 已完成(待桌面端目视复验) | 管理端全局框架与工作台已按规格交付;18 modules + 22 routes,vue-tsc 与 vite build 全绿 |
|
||||
| UI-MINIAPP-20260913 | 已完成 | 小程序 11 个核心页面已重构;11/11 页 TS-only,2026-09-16 通过用户验收 |
|
||||
@@ -559,7 +589,7 @@
|
||||
- P19-V11-T27 Program CRUD ✅ (GET/POST/PUT /service-programs)
|
||||
- P19-V11-T28 Member application ✅ (POST /service-programs/{id}/apply)
|
||||
- P19-V11-T29 Member approval ✅ (POST /operator/members/{id}/approve)
|
||||
- P19-V11-T30 Program statistics basic ⏳ 待Phase 20 Analytics
|
||||
- P19-V11-T30 Program statistics basic ✅ (Phase 20 已提供 `service.program.member.approved` 等统一口径)
|
||||
|
||||
### Banner管理(后端API)
|
||||
- P19-V11-T01~T07 ✅ (V019已有HomeBannerService + HomeController,后续补充Admin CRUD API)
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
# 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`(新增)
|
||||
+36
-8
@@ -1,8 +1,8 @@
|
||||
# 项目状态跟踪 — STATUS.md
|
||||
|
||||
> **2026-09-17 当前有效结论:`V1.1 INCREMENTAL RECOVERY PHASE 19 COMPLETE / UI REDESIGN ACCEPTED BY USER / GIT CLEAN`。**
|
||||
> **2026-09-17 当前有效结论:`V1.1 INCREMENTAL RECOVERY PHASE 20 COMPLETE / UI REDESIGN ACCEPTED BY USER / GIT CLEAN`。**
|
||||
>
|
||||
> 阶段 0(在途收口)、阶段 1(小程序 C 组 + discover 筛选重设计)、阶段 2(管理端 Vue 新框架适配)、阶段 3(CapabilityService 独立化)、阶段 4(双端总验收)全部完成。三端构建通过、**13/13 契约全绿**、核心 11 页面**全部 TS-only(11/11)**。**UI 重设计首批 11 页已于 2026-09-16 通过用户验收(结论:基本达标)**;`/me/*` 前后端契约同轮收敛,17 个 404 调用已恢复。**Phase 08(Like / Match / Block)、Phase 09(Text Chat)、Phase 10(Image & Chat Safety)、Phase 11(Notification)、Phase 12(Relationship Progress)、Phase 13(公益红娘转介)、Phase 13A(婚恋咨询与婚前辅导)、Phase 14(多类型公益活动)、Phase 15(活动报名、审核与签到)、Phase 16(活动互选与双方开启沟通确认)、Phase 17(举报与风控)、Phase 18(申诉)、Phase 19(统一运营后台 + 用户端公益服务页面)复验已关闭**,十三轮共修复 110+ 处真实缺陷(含 3 处隐藏编译缺陷),并各自补齐测试保真度与守卫。**Phase 20 起需用户确认后启动。**
|
||||
> 阶段 0(在途收口)、阶段 1(小程序 C 组 + discover 筛选重设计)、阶段 2(管理端 Vue 新框架适配)、阶段 3(CapabilityService 独立化)、阶段 4(双端总验收)全部完成。三端构建通过、**13/13 契约全绿**、核心 11 页面**全部 TS-only(11/11)**。**UI 重设计首批 11 页已于 2026-09-16 通过用户验收(结论:基本达标)**;`/me/*` 前后端契约同轮收敛,17 个 404 调用已恢复。**Phase 08(Like / Match / Block)、Phase 09(Text Chat)、Phase 10(Image & Chat Safety)、Phase 11(Notification)、Phase 12(Relationship Progress)、Phase 13(公益红娘转介)、Phase 13A(婚恋咨询与婚前辅导)、Phase 14(多类型公益活动)、Phase 15(活动报名、审核与签到)、Phase 16(活动互选与双方开启沟通确认)、Phase 17(举报与风控)、Phase 18(申诉)、Phase 19(统一运营后台 + 用户端公益服务页面)、Phase 20(Analytics 数据统计与管理驾驶舱)复验已关闭**,十四轮共修复 130+ 处真实缺陷(含 3 处隐藏编译缺陷),并各自补齐测试保真度与守卫。**Phase 21 起需用户确认后启动。**
|
||||
|
||||
最后更新: 2026-09-17 — 分支 `main`(`feat-ui-v11` 保留为冗余锚点)
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
| 阶段 2 管理端 Vue 适配 | 已完成 | 18 modules + 22 routes + vue-tsc 全绿 + vite build 11.68s |
|
||||
| 阶段 3 CapabilityService 独立化 | 已完成 | New `capability` 模块:CapabilityService(getCapabilities + 公共判断方法)+ CapabilityController(GET /api/v1/capabilities);HomeApplicationService 完全委托 |
|
||||
| 阶段 4 双端总验收 | **已完成**(2026-09-14) | 三端构建全绿 + 13/13 契约全绿 + 源码健康检查通过;验收报告见 docs/superpowers/plans/2026-09-14-phase4-acceptance-report.md |
|
||||
| Flyway 迁移 | V001~V039 | V028~V038 已纳入 STATUS;V037 为 Phase 12 新增(实体列与生产迁移对齐,23 表 50 列);V038 为 Phase 13 新增(`matchmaker_recommendations.status` + `matchmaker_services.service_type` 的 CHECK 白名单扩展);V039 为 Phase 13A 新增(counseling 幂等键 + 三个 CHECK 白名单显式固化) |
|
||||
| Backend 全量回归 | **已清偿**(2026-09-17) | 离线容器(`danyang-match-backend-test`)`mvn -o test` 实测:**494 tests / 0 failures / 0 errors / 3 skipped / BUILD SUCCESS**(约 4.4 分);含 `CanonicalRouteContractTest`、`ChatContractJsonTest`、`MeRouteContractTest`、`DiscoverBlockInvisibilityIntegrationTest`、`ChatPhase09IntegrationTest`、`ChatWebSocketHandlerTest`、`PhotoUploadValidationTest`、`NotificationPhase11IntegrationTest`、`NotificationTemplateTypeGuardTest`、`NotificationPrivacyCopyTest`、`EntityMigrationParityTest`、`RelationshipProgressPhase12Test`、`HomePremaritalGuidancePhase12Test`、`MatchmakerReferralPhase13Test`、`MatchmakerServiceTest`、`ReportAndRiskPhase17IntegrationTest`、`AppealPhase18IntegrationTest`(Phase 18 新增 15 例:申诉真实落库+审计、仅 ban/suspend 可申诉、幂等、**Gate A 通过只解除目标处置**、**Gate B 拒绝维持原处置**、原子认领唯一赢家、二次处理被拒、escalate 后可再决策、appealType 白名单/一致性/缺省推导、证据字段映射、软删过滤、Feature Flag 关闭拦截) |
|
||||
| Flyway 迁移 | V001~V043 | V028~V043 已纳入 STATUS;V037 为 Phase 12 新增(实体列与生产迁移对齐,23 表 50 列);V038 为 Phase 13 新增(`matchmaker_recommendations.status` + `matchmaker_services.service_type` 的 CHECK 白名单扩展);V039 为 Phase 13A 新增(counseling 幂等键 + 三个 CHECK 白名单显式固化);V042 为 Phase 19 新增(`service_location_announcements` 服务站临时公告表);V043 为 Phase 20 新增(授予 `data_viewer` / `platform_admin` 的 `analytics.view`) |
|
||||
| Backend 全量回归 | **已清偿**(2026-09-17,Phase 20) | 离线容器(`danyang-match-backend-test`)`mvn -o test` 实测:**648 tests / 0 failures / 0 errors / 3 skipped / BUILD SUCCESS**(约 6.5 分);含 `CanonicalRouteContractTest`、`ChatContractJsonTest`、`MeRouteContractTest`、`DiscoverBlockInvisibilityIntegrationTest`、`ChatPhase09IntegrationTest`、`ChatWebSocketHandlerTest`、`PhotoUploadValidationTest`、`NotificationPhase11IntegrationTest`、`NotificationTemplateTypeGuardTest`、`NotificationPrivacyCopyTest`、`EntityMigrationParityTest`、`RelationshipProgressPhase12Test`、`HomePremaritalGuidancePhase12Test`、`MatchmakerReferralPhase13Test`、`MatchmakerServiceTest`、`ReportAndRiskPhase17IntegrationTest`、`AppealPhase18IntegrationTest`(Phase 18 新增 15 例:申诉真实落库+审计、仅 ban/suspend 可申诉、幂等、**Gate A 通过只解除目标处置**、**Gate B 拒绝维持原处置**、原子认领唯一赢家、二次处理被拒、escalate 后可再决策、appealType 白名单/一致性/缺省推导、证据字段映射、软删过滤、Feature Flag 关闭拦截)、`MetricCatalogGuardTest`(11)、`AnalyticsMetricsPhase20IntegrationTest`(21)、`AnalyticsSoftDeleteGuardTest`(4)、`AnalyticsPermissionGrantTest`(6)、`LocalEligibilityStatusVocabularyGuardTest`(9) |
|
||||
| 路由/字段契约收敛 | **已完成**(2026-09-15) | 字段:会话 `target*` + `sentAt` + `relationStatus`(commit `687a156`);路径:采用 2026-09-14 canonical(`/likes` `/passes` `/blocks` `/matches`,缘分保留 `/relations/fate/*`),旧路径与 `success` 桩实现已移除;裁决由 `CanonicalRouteContractTest` 锁定 |
|
||||
| `/me/*` 路径族 | **已完成**(2026-09-16) | 小程序按 REST 规范(§16 / §22-§40 / §80 / §83-§84)调用 `/me/*`,后端此前**无 `/me` 基路径**致 17 个调用 404。新增 `MeController` 委托既有 service 暴露规范路径,旧路径全部保留,小程序三条历史写法(`/me/privacy-settings`、`/me/photos/{id}/main`、`/me/appeals`)一并兼容;由 `MeRouteContractTest` 锁定 |
|
||||
| Admin Web | **验证通过** | 2026-09-14 阶段 4:vue-tsc ✅、vite build ✅ 11.68s;18 modules 齐全;壳层+工作台通过参考图 QA |
|
||||
@@ -518,13 +518,41 @@ MinIO: localhost:9003 (凭证由本地环境变量提供)
|
||||
新增守卫:`ServiceLocationAnnouncementPhase19IntegrationTest`(24) / `ServiceLocationControllerPermissionContractTest`(5) /
|
||||
`ServiceLocationAnnouncementConstraintParityTest`(5)。负向验证 3 处注入 → 4 个用例精准变红并还原(`NEG-VAL` 残留 0)。
|
||||
|
||||
### Phase 20 — Analytics数据统计扩展 ✅ 已完成
|
||||
### Phase 20 — Analytics 数据统计与管理驾驶舱 ✅ 已完成(2026-09-17 复验关闭)
|
||||
|
||||
> 复验报告:`docs/Phase-20-V11-Recovery-Completion-Report-2026-09-17.md`
|
||||
> 指标字典:`docs/ANALYTICS_METRICS.md`(与 `MetricCatalog` 逐条对应)
|
||||
> 后端全量回归:`Tests run: 648, Failures: 0, Errors: 0, Skipped: 3 / BUILD SUCCESS`
|
||||
> admin-web:`vue-tsc --noEmit` EXIT=0;负向验证 2 轮 7 处注入 → 7 用例精准变红,还原后 0 残留
|
||||
|
||||
| 任务 | 状态 |
|
||||
|------|------|
|
||||
| P20-T01~T11 全部11项统计指标 | ✅ |
|
||||
| GET /operator/analytics/v11-dashboard 综合接口 | ✅ |
|
||||
| P20-T01 指标字典(`MetricCatalog` 36 指标 × 12 字段 + 文档 + 守卫) | ✅ |
|
||||
| P20-T02~T09 User / Review / Recommendation / Match / Chat Started / Matchmaker / Activity / Risk Metrics | ✅ |
|
||||
| P20-T10 Daily Summary(补 days 1~365 边界校验) | ✅ |
|
||||
| P20-T11/T12 Platform Dashboard(统一口径层 `AnalyticsMetricsCalculator`) | ✅ |
|
||||
| P20-T13 Organization Dashboard | ✅(受 `analytics.view` + DataScope;`organization_admin` 待组织维度建模) |
|
||||
| P20-T14 Analytics 权限(V043 打通 `data_viewer` / `platform_admin`) | ✅ |
|
||||
| `GET /operator/analytics/v11-dashboard` 综合接口 | ✅ |
|
||||
|
||||
**Phase 20(Analytics数据统计扩展)— 已完成** ✅
|
||||
**本轮复验修复的真实缺陷(高价值项)**:
|
||||
|
||||
- **口径造假**:实名人数用 `profile_status != 'rejected'` 冒充(把"资料还在填"算成已实名);
|
||||
Chat Started 直接数会话行数(系统通知计入);Match 不分来源(10 对互选被当 10 对成功引用);
|
||||
Banner 曝光/点击**硬编码 0**(被读成"投了没人点",实为无埋点)。
|
||||
- **死代码**:活动漏斗**全部硬编码 0**(不是没数据,是根本没有查询)+ 一行把"用户数"当"活动数"的死代码。
|
||||
- **系统性软删缺陷(D12)**:`BaseEntity.deletedAt` 无 `@TableLogic`、全局无逻辑删除配置
|
||||
→ `selectCount` 把**软删数据全部计入统计且无任何报错**。修复 45 处统计调用(2 轮复验,首轮漏扫 5 处次级路径)。
|
||||
- **权限授予脱节(Gate ⑩)**:`data_viewer` 角色被定义却**从未授权**(空壳角色);
|
||||
`platform_admin` 因 `id >= '20000000-…'` 条件漏掉 V1.0 老权限段 → **整个模块除 super_admin 外全员 403**。
|
||||
- **假绿测试**:`AnalyticsServiceTest` = 5×`assertTrue(true)`(零断言),已删除并替换为 21 用例真实 DB 集成测试。
|
||||
|
||||
**跨阶段发现**(缺陷本体属 Phase 21,本轮已修 X1~X3):
|
||||
`AccountLifecycleService` 注销时把 `user_identities` 状态域的 `'failed'` 写入本地资格表
|
||||
→ 违反生产 `V017 chk_elig_verify_status` → **真实 PG 上 closeAccount 事务回滚、账号注销 100% 不可用**;
|
||||
`checkCanWithdraw` 的资格拦截用 `submitted`/`pending`(该表永不可能出现的值)→ 死代码。
|
||||
测试库补齐 `chk_elig_type` / `chk_elig_verify_status` 后立即暴露 `ActivityPhase14Test` 夹具违规。
|
||||
新增 `LocalEligibilityStatusVocabularyGuardTest`(9)。
|
||||
|
||||
### Phase 21 — 账户生命周期增量 ✅ 已完成
|
||||
| 任务 | 状态 |
|
||||
|
||||
Reference in New Issue
Block a user