fix(activity): close Phase 15 KEEP-VERIFY/EXTEND — 2 critical + 14 other defects, 3 test-fidelity fixes

Phase 15 (activity registration / review / checkin) verification closure.

Two CRITICAL defects, both "production always had it, test schema never did":
- D1: generateCheckinToken wrote the token to Redis with value "used", while
  scanCheckin rejects any token marked "used" -> every freshly issued check-in
  code was dead on arrival, staff scanning failed 100% of the time.
  Issuing now writes "issued"; "used" is set only after a successful scan.
- D9: UNIQUE(activity_id, user_id) exists in production, but re-registering
  after cancel/reject went through INSERT -> users who cancelled once could
  never register for that activity again (duplicate key 500). The H2 test
  schema lacked the UNIQUE, so the path had zero coverage. Now the original
  row is reused via an atomic conditional UPDATE.

Checkin (D2-D8):
- D2: duplicate check-in was read-then-write; now atomic checkinIfNotCheckedIn
  and repeat scans are idempotent (alreadyCheckedIn) without duplicate audit.
- D3: no data scope at all -> added activity staff assignment (V040) and
  assertCheckinScope; INV-020 (org relation grants nothing).
- D4: manualCheckin did not validate activity existence/cancellation.
- D5: getCheckinStats NPE on missing activity; remainingCount was semantically
  "free seats" -> replaced with notCheckedInCount.
- D6: malformed timestamp caused 500 -> 400. D7: constant-time signature compare.

Registration / review / cancel (D10-D13):
- D10: backend registration list only queried status='approved' -> the review
  screen never saw pending rows (review unusable).
- D11: registration_start_at / registration_end_at were never enforced.
- D12: cancel had no audit log (the only write without one).
- D13: reject/cancel were read-then-full-update -> rejectIfPending/cancelIfActive.

Capacity groups (D14, plan item 2): groups were stored and read back but never
participated in capacity -> incrementIfAvailable / decrementIfPositive now make
approved_count real; activities without groups are unaffected.

Frontend contract (F1-F2):
- F1: admin-web had no check-in surface at all -> ActivityCheckinView.vue
  (paste/scan token, manual check-in, stats) + /activities/checkin route & nav.
- F2: miniapp had no cancel-registration wrapper although the endpoint existed.

Test fidelity (T1-T3):
- T1: test schema gained UNIQUE(activity_id,user_id) + chk_reg_status +
  chk_checkin_status; two new guards parse the real V010 DDL and compare.
- T2: 3 assertTrue(true) placeholders in CheckinServiceTest (covering duplicate
  check-in, expiry, no-sensitive-plaintext) replaced with 5 real guards that
  parse the real source and real production DDL.
- T3: activity_registrations + activity_staff_assignments moved into
  REQUIRED_IN_TEST_SCHEMA; MigrationValidationTest baseline 39 -> 40.

Note: ActivityStaffAssignment must NOT extend BaseEntity - the table is a
composite PK (activity_id, operator_id); BaseEntity would add id/updated_at/
deleted_at to every INSERT and break on PostgreSQL (caught by
EntityMigrationParityTest immediately).

Tests: ActivityPhase15Test (21), CheckinServiceTest (5), 2 new constraint
guards. Negative verification: 3 injected defects -> 8 assertions turned red,
all restored.

Regression: 474 tests / 0 failures / 0 errors / 3 skipped / BUILD SUCCESS
(baseline 450). admin-web vue-tsc 0 error, vite build OK.
This commit is contained in:
WorkBuddy
2026-09-17 15:07:52 +08:00
parent 755a570849
commit 29112eed6e
21 changed files with 1602 additions and 116 deletions
+16 -1
View File
@@ -95,7 +95,13 @@ CREATE TABLE activity_registrations (
reject_reason VARCHAR(500), checkin_status VARCHAR(32), checkin_at TIMESTAMP WITH TIME ZONE,
emergency_contact_name VARCHAR(100), emergency_contact_phone_enc VARCHAR(500), source VARCHAR(64),
request_source VARCHAR(64), created_by_operator_id VARCHAR(36), user_confirmed_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE,
-- Phase 15(V010):生产约束此前测试库完全没有 —— 约束漂移盲区(与 Phase 11/13/14 同族根因)。
-- 缺 UNIQUE(activity_id, user_id) 使「取消/拒绝后重新报名」撞唯一键的必崩缺陷在测试期完全不可见;
-- 缺两个 CHECK 使非法 status / checkin_status 只在 PostgreSQL 上炸。
CONSTRAINT uk_activity_registration UNIQUE (activity_id, user_id),
CONSTRAINT chk_reg_status CHECK (status IN ('pending', 'approved', 'rejected', 'cancelled')),
CONSTRAINT chk_checkin_status CHECK (checkin_status IN ('not_checked_in', 'checked_in'))
);
CREATE TABLE service_appointments (
id VARCHAR(36) PRIMARY KEY, user_id VARCHAR(36), service_location_id VARCHAR(36), service_type VARCHAR(64),
@@ -267,6 +273,15 @@ CREATE TABLE activity_capacity_groups (
CONSTRAINT uk_capacity_group UNIQUE (activity_id, group_code)
);
-- V040: activity_staff_assignments 活动工作人员指派表(Phase 15 签到数据范围)
-- 主办/协办组织关系不授予操作权限(INV-020),签到必须来自显式指派。
CREATE TABLE activity_staff_assignments (
activity_id VARCHAR(36) NOT NULL, operator_id VARCHAR(36) NOT NULL,
assignment_role VARCHAR(32) NOT NULL DEFAULT 'checkin_staff',
created_by VARCHAR(36), created_at TIMESTAMP WITH TIME ZONE,
PRIMARY KEY (activity_id, operator_id),
CONSTRAINT chk_activity_staff_role CHECK (assignment_role IN ('checkin_staff', 'activity_manager'))
);
-- ============================================================================
-- Phase 13(V009 + V021 + V037 + V038):红娘服务三表
-- 此前测试库完全缺失,导致 matchmaker 模块零真实行为覆盖、只能写假测试。