Files
danyang-match/docs/Phase-14-V11-Recovery-Completion-Report-2026-09-17.md
T
WorkBuddy 755a570849 fix(activity): close Phase 14 KEEP-VERIFY/EXTEND — 11 backend + 3 frontend-contract + 2 test-fidelity defects
Phase 14 (multi-type public-welfare activities) verification closure.

Backend (D1-D11):
- D1 CRITICAL: ActivityMapper.countRemainingCapacity had no @Select and no XML
  -> activity detail / register / approve all threw Invalid bound statement.
  Removed the shell; unified on ActivityRegistrationMapper.
- D2 CRITICAL: register & approve were read-then-write -> oversold under
  concurrency. Replaced with single atomic SQL (insertIfCapacityAvailable /
  approveIfCapacityAvailable, affectedRows==0 => full).
- D3 HIGH: activityType / orgRole never validated against production
  chk_activity_type / chk_org_role -> 500 only on PostgreSQL.
- D4 HIGH: ActivityListItemDTO had no activityType -> miniapp category tabs
  always empty (Gate failure).
- D5: male/female counts were fabricated as approvedCount/2; now real
  aggregation over user_profiles.gender.
- D6: minAge/maxAge was a TODO no-op; added assertAgeInRange.
- D7: registration_policy write-only; per-type eligibility now enforced
  (identity / local eligibility / gender quota).
- D8: POST /operator/activities dropped type, multi-org and capacity groups
  and set organizer_org_id to the operator user id; now delegates to
  createActivityFromAdmin (single implementation).
- D9: three operator write endpoints were mounted on the user-side
  ActivityController (dual implementation, zero callers) -> moved.
- D10: NPE on soft-deleted activity in getMyActivities; NPE/500 on missing
  admin form fields (Instant.parse(null)); tolerant time parsing added.
- D11: DTO lacked registeredCount / status / coverFileId / description
  -> card always showed 0 registered, "view detail", no cover, no summary.

Frontend contract (F1-F3):
- F1: miniapp category code "classroom" is not in the production whitelist
  (should be relationship_education); three types had no tab at all.
- F2: ui-activity-card read 7 fields that do not exist on the backend;
  now reads only real DTO fields, plus per-type badge and eligibility hint.
- F3: admin-web had no activity management page at all; added
  ActivityManagementView.vue (type-driven dynamic form, multi-org,
  capacity groups, publish/cancel) + /activities route and nav entry.

Test fidelity (T1-T2):
- T1: deleted two assertTrue(true) placeholder tests (testNoOverbooking,
  testNoDuplicateRegistration) that covered the exact broken invariants.
- T2: test schema lacked V023 columns/constraints and three tables; the
  activity tables were only in the OPTIONAL set (zero coverage) -> schema
  completed and moved to REQUIRED_IN_TEST_SCHEMA.

Tests: ActivityPhase14Test (23 cases), ActivityConstraintGuardTest (9 cases,
parses real db/migration/V*.sql plus miniapp source). Two negative-verification
rounds proved both guard families actually turn red.

Regression: 450 tests / 0 failures / 0 errors / 3 skipped / BUILD SUCCESS
(baseline 420). admin-web vue-tsc 0 error, vite build OK.
2026-09-17 14:02:00 +08:00

12 KiB
Raw Permalink Blame History

Phase 14:多类型公益活动 — KEEP-VERIFY/EXTEND 复验完成报告

  • 日期:2026-09-17
  • 分支:main
  • Tag:phase14-recovery-2026-09-17
  • 基线:Phase 13A 关闭时后端 420 tests 全绿
  • 复验结论:发现并修复 11 处后端缺陷 + 3 处前端契约缺陷 + 2 处测试保真度缺陷;全量回归 450 tests / 0 failures / 0 errors / 3 skipped / BUILD SUCCESS
  • 计划出处:docs/善缘丹阳 V1.1 客户需求基线增量开发与修复计划.md Phase 14(第 337-347 行)

一、Gate 结论

Gate 结论 证据
matchmaking 与课堂/反诈类活动显示和资格策略不同 ✅ 通过 resolveEligibility 按类型返回三档策略;matchmakingAndAntiFraudPoliciesMustDiffer 守卫 + resolveEligibilityDiffersByType / matchmakingRequiresIdentityAndLocalEligibility / antiFraudDoesNotRequireIdentity 三条行为用例
容量按配置正确扣减 ✅ 通过 原子 SQL insertIfCapacityAvailable / approveIfCapacityAvailable;registerRespectsTotalLimitAtomically、approveRespectsTotalLimitAtomically、genderQuotaIsEnforcedForMatchmaking、antiFraudIgnoresGenderQuota
主办组织关系不自动授予用户库访问权限(INV-020) ✅ 通过 activityOrganizationsExposeNoUserData(组织接口零用户字段)+ registrationListRequiresOperatorPermission(运营端类级 @PreAuthorize('activity.view'),用户端无报名列表)

二、缺陷清单

2.1 后端(11 处)

编号 级别 缺陷 修复
D1 Critical 生产故障 ActivityMapper.countRemainingCapacity 既无 @Select 也无 XML,且项目未配置 mapper-locations → 活动详情 / 报名 / 审核三条路径必抛 Invalid bound statement (not found)。而 ActivityService 调用的正是这个空壳 删除空壳,统一走 ActivityRegistrationMapper(唯一有真实 SQL 的实现)
D2 Critical 并发 报名与审核都是「先 SELECT 余量 → 再 INSERT/UPDATE」,并发下必然超卖;且 needReview=false 时写入 approved 却完全不校验名额 改为单条原子 SQL:INSERT ... SELECT ... WHERE (SELECT COUNT...) < total_limit [AND 性别名额],affectedRows == 0 即报「名额已满」;审核同理 approveIfCapacityAvailable
D3 High 生产故障 创建活动 / 添加组织关系未校验 activityType、orgRole 是否落在生产 chk_activity_type / chk_org_role 白名单内。非法值在 H2 测试库静默通过,上 PostgreSQL 才 500(典型的"只在 PG 上炸") Service 侧白名单校验(VALID_ACTIVITY_TYPES / VALID_ORG_ROLES),常量由守卫解析真实 DDL 双向校验
D4 High 契约 ActivityListItemDTO 没有 activityType 字段,而小程序 pages/activity/activity.ts 正是读它做分类筛选 → 分类 Tabs 恒无数据,Gate ① 直接失败 补字段并在 toListItemDTO 回填
D5 Medium 数据造假 toListItemDTO 用 approvedCount / 2 与 approvedCount - approvedCount/2 凭空编造男女人数;countApprovedMaleRegistrations 的 SQL 忽略 gender 入参且零调用方 新增 countApprovedByGender,按 user_profiles.gender 真实聚合
D6 Medium 年龄门槛 minAge / maxAge 为 TODO 空壳,完全不生效 补 assertAgeInRange
D7 Medium Gate 未落地 registration_policy 字段只写不读,按类型的资格策略实际为空 改为按类型强制执行:实名(TYPES_REQUIRING_IDENTITY)、本地资格(TYPES_REQUIRING_LOCAL_ELIGIBILITY)、性别名额(TYPES_WITH_GENDER_QUOTA)
D8 Medium 生产故障 POST /operator/activities 丢弃 activityType / registrationPolicy / 多组织 / 容量分组,并把 organizer_org_id 错设为操作者 userId → 后台动态表单无法创建带类型活动 改为委托 createActivityFromAdmin(单一实现),并回填 organizer_org_id
D9 Medium 权限边界 三个运营写入端点(POST /activities/operator、/operator/{id}/organizations、/operator/{id}/capacity-groups)挂在用户端 ActivityController,与运营端双实现且零调用方 迁至 ActivityOperatorController(类级 @PreAuthorize('activity.view') + 方法级 activity.create)
D10 Medium NPE ① getMyActivities 对已软删活动 selectById 返回 null 直接 NPE;② createActivityFromAdmin 对缺字段请求 Instant.parse(null) / ((Number) null).intValue() 直接 500 ① 跳过软删活动;② 缺省值兜底 + 容忍时间戳 / 本地无时区文本 / ISO 文本三种时间格式
D11 Medium 契约 DTO 缺 registeredCount / status / coverFileId / description → 卡片"报名人数"恒 0、按钮恒显示"查看详情"、封面与摘要恒不显示 四个字段补齐并在 toListItemDTO 回填

2.2 前端契约(3 处)

编号 缺陷 修复
F1 小程序分类 Tabs 的 code 写的是 classroom,而生产白名单里根本没有该值(婚恋课堂是 relationship_education)→ 点「婚恋课堂」恒为空白列表;且 interest_social / premarital_guidance / marriage_custom_culture 三类完全没有入口 六个 code 逐字对齐生产白名单,补全全部六类
F2 ui-activity-card 读取 registeredCount / currentMaleCount / currentFemaleCount / coverImageUrl / activityStartAt / suitableFor / intro —— 后端一个都没有 改为只读 DTO 真实字段;封面按 coverUrl ?? uploadBase + '/files/' + coverFileId 拼接;时间本地格式化;新增类型标签与差异化资格提示(Gate ① 的前端落点)
F3 admin-web 完全没有活动管理页(仅有互选页),后台动态表单 100% 缺失 新增 ActivityManagementView.vue:类型驱动的动态表单(性别名额/互选开关按类型显隐)、多组织增删、容量分组增删、发布/取消;注册 /activities 路由与导航

2.3 测试保真度(2 处)

编号 缺陷 修复
T1 假绿占位测试:ActivityServiceTest#testNoOverbooking 与 testNoDuplicateRegistration 是 assertTrue(true); // 占位测试。名字覆盖两条核心不变量,实际什么都不验证 —— 而真实实现恰恰是坏的 删除(占位测试不是"待补",是持续误导的假信号),迁至 ActivityPhase14Test 走真实落库路径
T2 测试库漂移:activities 缺 V023 五列与 chk_activity_type;organizations / activity_organizations / activity_capacity_groups 三表完全缺失;且这五张表在 EntityMigrationParityTest 中只属"可选白名单"(= 零覆盖) schema-h2.sql 补齐列、CHECK 约束与三张表;五张表从 TEST_SCHEMA_OPTIONAL_TABLES 移入 REQUIRED_IN_TEST_SCHEMA

三、新增测试

测试类 用例数 性质
ActivityPhase14Test 23 Spring 上下文行为测试:原子容量扣减、按类型资格策略、白名单校验、年龄门槛、列表数据真实性、软删不 NPE、INV-020、多组织/容量分组落库、后台表单容错
ActivityConstraintGuardTest 9 纯反射 + 文件解析(无 Spring):解析真实 db/migration/V*.sql 校验 chk_activity_type / chk_org_role;解析小程序 activity.ts 分类 code 与卡片 activity.* 字段名

守卫自检:parserMustResolveActivityConstraints 断言迁移文件数 ≥ 30 且两个约束白名单非空 —— 解析器失效时守卫自己先红,避免恒真假绿。


四、负向验证(两轮,证明守卫非恒真)

第一轮:后端

注入的缺陷 变红的用例
从 V023 的 chk_activity_type 白名单删除 anti_fraud ActivityConstraintGuardTest.activityTypeConstantsWithinProductionWhitelist(精准命中,说明解析的是真实 DDL 而非副本)
报名退化为「不检查名额直接 INSERT」 ActivityPhase14Test.registerRespectsTotalLimitAtomically + genderQuotaIsEnforcedForMatchmaking

结果:Tests run: 26, Failures: 3 / BUILD FAILURE。已还原。

第二轮:前端契约

注入的缺陷 变红的用例
分类 code 改回 classroom 并删除 anti_fraud 入口 miniappCategoryCodesWithinProductionWhitelist + everyProductionTypeHasMiniappTab
卡片加回 activity.suitableFor cardFieldsExistInListItemDto

结果:Tests run: 9, Failures: 3 / BUILD FAILURE。已还原。

两轮负向验证中均出现过守卫自身的解析缺陷(categories: 先命中接口声明、正则扫到注释里的 activity.capacity),已一并修正 —— 守卫必须先剥注释再扫描,且匹配要精确到 categories: [。


五、回归证据

项目 结果
后端全量 mvn -o test Tests run: 450, Failures: 0, Errors: 0, Skipped: 3 / BUILD SUCCESS(4 分 52 秒)
基线对比 Phase 13A 420 → 450(净 +30 = 新增 23 + 9,删除 2 条占位测试)
admin-web vue-tsc --noEmit 通过,0 error
admin-web vite build ✓ built in 9.75s,新增 ActivityManagementView(13.28 kB)
后端 mvn test-compile BUILD SUCCESS(282 main + 75 test 源文件)

六、变更文件

后端

  • activity/mapper/ActivityMapper.java — 删除无 SQL 空壳;新增 findOpenActivitiesByType、countApprovedByGender
  • activity/mapper/ActivityRegistrationMapper.java — 新增原子 insertIfCapacityAvailable / approveIfCapacityAvailable
  • activity/service/ActivityService.java — 报名/审核改原子;按类型资格策略;白名单校验;年龄门槛;DTO 回填;后台表单容错
  • activity/dto/ActivityListItemDTO.java — 新增 activityType / registeredCount / coverFileId / description / status
  • activity/controller/ActivityOperatorController.java — 建活动改走 V1.1;新增组织/容量分组 4 个端点;列表补 activityType
  • activity/controller/ActivityController.java — 删除 3 个错挂在用户端的运营端点

前端

  • miniapp/pages/activity/activity.ts — 分类 code 对齐生产白名单(6 类)
  • miniapp/components/activity-card/{index.js,index.wxml,index.wxss} — 只读 DTO 真实字段;类型标签 + 差异化资格提示 + 封面/时间/报名人数
  • admin-web/src/modules/activity/ActivityManagementView.vue — 新增,类型驱动动态表单
  • admin-web/src/router/index.ts、admin-web/src/layouts/MainLayout.vue — 注册 /activities 路由与导航

测试

  • 新增 ActivityPhase14Test(23)、ActivityConstraintGuardTest(9)
  • ActivityServiceTest — 删除 2 条占位假绿测试
  • EntityMigrationParityTest — activity 五表移入必选集合
  • backend/src/test/resources/schema-h2.sql — 补 V023 列/约束与三张缺失表

七、遗留与建议(不在本阶段范围)

  1. 容量分组尚未参与名额计算:activity_capacity_groups 目前只做配置落库与回读,报名时的名额扣减仍走 total_limit + 男女名额。Phase 15(报名/审核/签到)应把分组规则真正接入扣减链路。
  2. registration_policy 目前仍只写不读:本阶段按 activityType 推导资格策略(可落地、可验证),registration_policy 作为更细粒度的覆盖项尚未接线。若产品需要"同类型不同策略",应在 Phase 15 明确其优先级。
  3. 首页活动卡片未复用 ui-activity-card:miniapp/pages/index/index.wxml 仍内联渲染,且把 coverFileId 直接当图片 src(应为 uploadBase + '/files/' + id)。建议后续统一走组件。

八、结论

Phase 14(多类型公益活动)复验关闭。三项计划任务均已完成,三项 Gate 全部达成并通过行为用例与守卫双重覆盖。

Phase 15(活动报名、审核与签到)未自动进入,等待用户确认。