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.
This commit is contained in:
WorkBuddy
2026-09-17 14:02:00 +08:00
parent 7a2693b239
commit 755a570849
22 changed files with 2164 additions and 136 deletions
@@ -351,12 +351,51 @@ D2——`RelationshipService.createMatch` 不写 episode/lastActivityAt,与 `M
**Files:** activity 模块、Miniapp activity 页面、Admin activity 页面。
- [ ] 保留活动基础,完成 ActivityType 分类、按类型报名策略、capacity groups 和多组织关系。
- [ ] 完成小程序分类 Tabs、类型化卡片、动态资格提示和后台动态表单。
- [ ] 验证主办组织关系不自动授予用户库访问权限。
- [x] 保留活动基础,完成 ActivityType 分类、按类型报名策略、capacity groups 和多组织关系。
- [x] 完成小程序分类 Tabs、类型化卡片、动态资格提示和后台动态表单。
- [x] 验证主办组织关系不自动授予用户库访问权限。
**Gate:** matchmaking 与课堂/反诈类活动显示和资格策略不同;容量按配置正确扣减。
> **复验完成(2026-09-17)** — Tag:`phase14-recovery-2026-09-17`
>
> **11 处后端缺陷(D1~D11)**:
> - D1 **Critical 生产故障**:`ActivityMapper.countRemainingCapacity` 无 `@Select` 无 XML
> → 活动详情/报名/审核三条路径必抛 `Invalid bound statement`。
> - D2 **Critical 并发**:报名与审核均为「先读后写」→ 并发超卖;改
> `insertIfCapacityAvailable` / `approveIfCapacityAvailable` 单条原子 SQL。
> - D3 High 生产故障:`activityType` / `orgRole` 未校验生产 `chk_activity_type` /
> `chk_org_role` 白名单("只在 PG 上炸")。
> - D4 High 契约:`ActivityListItemDTO` 无 `activityType` → 小程序分类 Tabs 恒无数据。
> - D5 Medium:男女人数为 `approvedCount/2` 编造 → 改按 `user_profiles.gender` 真实统计。
> - D6 Medium:年龄门槛为 TODO 空壳,完全不生效。
> - D7 Medium:`registration_policy` 只写不读,按类型资格策略为空(Gate 未落地)。
> - D8 Medium:`POST /operator/activities` 丢弃类型/多组织/容量分组,且把
> `organizer_org_id` 错设为操作者 userId → 后台动态表单无法创建带类型活动。
> - D9 Medium:三个运营写入端点错挂在**用户端** `ActivityController`(双实现、零调用方)。
> - D10 Medium NPE:软删活动致 `getMyActivities` NPE;后台漏填字段致创建活动 500。
> - D11 Medium 契约:DTO 缺 `registeredCount`/`status`/`coverFileId`/`description`
> → 卡片报名人数恒 0、按钮恒"查看详情"、封面与摘要恒不显示。
>
> **3 处前端契约缺陷(F1~F3)**:小程序分类 code `classroom` 不在生产白名单
> (应为 `relationship_education`)且缺三类入口;卡片读取 7 个后端根本不存在的字段;
> admin-web 完全没有活动管理页(已新增 `ActivityManagementView.vue` 类型驱动动态表单)。
>
> **2 处测试保真度缺陷(T1~T2)**:`testNoOverbooking` /
> `testNoDuplicateRegistration` 为 `assertTrue(true)` 占位假绿(已删除);
> 测试库缺 V023 列与三张表,且 activity 五表在 `EntityMigrationParityTest`
> 只属"可选白名单"(零覆盖)→ 已补齐并移入必选集合。
>
> **负向验证两轮**:① 后端 —— 从 V023 删 `anti_fraud` + 报名退化为无容量检查
> → 3 条用例精准变红;② 前端契约 —— 分类 code 改回 `classroom` + 卡片加回
> `activity.suitableFor` → 3 条守卫精准变红。均已还原。
>
> **全量回归实测**:`[WARNING] Tests run: 450, Failures: 0, Errors: 0, Skipped: 3` /
> `BUILD SUCCESS`(4 分 52 秒)。Phase 13A 基线 420 → 450(净 +30)。
> admin-web `vue-tsc --noEmit` 0 error、`vite build` 通过。
> **Phase 14 已关闭。Phase 15 未自动进入,等待用户确认。**
> 报告:`docs/Phase-14-V11-Recovery-Completion-Report-2026-09-17.md`
### Phase 15:活动报名、审核与签到 — KEEP-VERIFY/EXTEND
**Files:** activity registration、checkin 服务和 U29~U32 页面。