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.
- Top chips: 推荐(重置) / 丹阳本地(regionCode=321181) / 年龄 / 学历 / 婚姻
- Each chip has real active state bound to applied filter value
- Panel expanded to 4 sections: age slider + region + education + maritalStatus
- Param names fixed: minAge/maxAge/regionCode/educationCode/maritalStatus
- Data flow: panel temp -> onApplyFilter -> applied -> _buildParams -> API
- Panel opens with current applied values, closes with openFilterPanel/closeFilterPanel
- Top chip '推荐' now active only when no active filter (was hardcoded)
- Top chip '丹阳本地' uses applied.region (was tempRegion)
- Top chip '年龄' uses applied age range
- New onTopRegionTap applies region filter immediately and refreshes results
- Panel onRegionTap preserved for filter-panel multi-select flow