feat(matchmaker): ZC-MM-002 A1 服务对象分配建模(DEC-MATCHMAKER-002 LOCKED 落地)

数据库(V053/V054):
- 新表 matchmaker_assignments:独立分配关系(assigned_by/reason/scope/
  expires_at/ended_at/end_reason),状态机 active/ended/revoked/transferred/
  expired(CHECK 白名单),部分唯一索引防同用户多 ACTIVE 主责分配,
  存量进行中服务单幂等回填
- 新角色 matchmaker_leader(014) + 新权限 matchmaker.team.assign(10000000-…-023);
  组长授 view/service/team.assign(不给平台级 matchmaker.assign)

后端:
- ServiceAssignmentPolicy 有效分配判定器(读前惰性过期,失效分配即时拒绝)
- 分配/转分配/撤销/结束四操作:状态机非法转换拒绝、理由必填、
  同用户主责冲突拒绝;组织范围校验(team.assign 同组织,NULL=NULL 视为
  平台直属,技术默认安全值待治理登记);transfer 同时校验原持有人与新目标范围
- 全操作审计 MATCHMAKER_ASSIGNMENT_CREATE/TRANSFER/REVOKE/END,
  operatorId=当前操作者,afterData 仅记摘要(无敏感内容)
- 删除认领端点 POST /services/{id}/claim(A1 禁止普通红娘认领,MM-AUTH-004=404)
- GET /pending 收敛为分配权持有者(assign or team.assign);
  dashboard today/completed 改本人口径,inProgress 口径修正 'processing'→'in_service'
- recommend/followup/complete/detail 归属判定接入判定器,拒绝走
  DataScopeGuard(自动 DATA_SCOPE_DENIED 审计),不再裸 IllegalStateException;
  assignService 与创建分配同事务联动;删除死查询 findAssignedTo

前端:
- admin-web 分配弹窗去硬编码假数据(operator_wang/chen)→ assignable-matchmakers
  真实接口(仅 id/name/organizationId,无联系方式)+ 理由必填;
  队列 tab 按 assign/team.assign 显隐;新增「服务对象分配」tab(转分配/撤销/结束)
- 小程序红娘工作台:删除接单入口,数据源切 GET /operator/matchmaker/assignments/my,
  枚举码不上屏(中文映射),空 serviceId 不跳转

测试:
- MatchmakerAssignmentA1Test 14 用例(MM-AUTH-001~004、MM-AUDIT-001、
  MM-LIFECYCLE-001、组长跨组织拒绝、NULL=NULL 平台直属边界、transfer 原持有人
  范围负向、/pending 403+审计),负向注入自证有效
- MatchmakerAssignmentPermissionTest 3 用例(端点权限契约 + claim 残留扫描)
- MatchmakerServiceTest 补 assignment 状态/scope 白名单守卫;迁移基线 LATEST=54
- 验证:定向 38/38 全绿;UI 契约 24/24;小程序 wcc/wcsc 真编译 0 失败
This commit is contained in:
WorkBuddy
2026-10-09 22:46:32 +08:00
parent 6187ec69df
commit 861c78efb5
21 changed files with 1868 additions and 136 deletions
+55
View File
@@ -492,6 +492,61 @@ CREATE TABLE matchmaker_followups (
deleted_at TIMESTAMP WITH TIME ZONE
);
-- V053: matchmaker_assignments 服务对象分配表(ZC-MM-002 / A1)
-- 生产为部分唯一索引 uq_mm_assignment_active_primary WHERE status='active' AND scope='primary';
-- H2 不支持部分索引,用「生成列 + 唯一索引」等价替代:
-- active_primary_key 仅在 (active, primary) 时取 user_id,否则为 NULL,
-- 标准唯一索引对多行 NULL 互不冲突 → 终态历史行可共存,语义与生产一致。
-- 注:H2 无 operators 表(见 Phase 17 注),去掉生产 DDL 的外键;CHECK 白名单保留。
CREATE TABLE matchmaker_assignments (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
matchmaker_operator_id VARCHAR(36) NOT NULL,
service_id VARCHAR(36),
assigned_by VARCHAR(36) NOT NULL,
assignment_scope VARCHAR(32) NOT NULL DEFAULT 'primary',
assignment_reason VARCHAR(500) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'active',
assigned_at TIMESTAMP WITH TIME ZONE,
expires_at TIMESTAMP WITH TIME ZONE,
ended_at TIMESTAMP WITH TIME ZONE,
end_reason VARCHAR(500),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE,
CONSTRAINT chk_mm_assignment_status CHECK (status IN (
'active', 'ended', 'revoked', 'transferred', 'expired'
)),
CONSTRAINT chk_mm_assignment_scope CHECK (assignment_scope IN ('primary', 'collaborating')),
active_primary_key VARCHAR(36) GENERATED ALWAYS AS (
CASE WHEN status = 'active' AND assignment_scope = 'primary' THEN user_id END
)
);
CREATE UNIQUE INDEX uq_mm_assignment_active_primary_h2 ON matchmaker_assignments(active_primary_key);
CREATE INDEX idx_mm_assignment_operator_h2 ON matchmaker_assignments(matchmaker_operator_id, status);
CREATE INDEX idx_mm_assignment_user_h2 ON matchmaker_assignments(user_id, status);
CREATE INDEX idx_mm_assignment_service_h2 ON matchmaker_assignments(service_id);
-- V054 配套:operators 运营人员表(ZC-MM-002 A1 测试需要:组长/红娘按 organization_id
-- 划定团队边界;此前 operators 一直在 EntityMigrationParityTest 可选白名单里 = 零覆盖)。
-- 注意:与生产 V001 对齐但去掉 REFERENCES organizations(id) 外键(organizations 本表已存在,
-- 保留外键亦可;为与既有「去外键」惯例一致,不建外键)。
CREATE TABLE operators (
id VARCHAR(36) PRIMARY KEY,
organization_id VARCHAR(36),
name VARCHAR(100) NOT NULL,
phone_enc CLOB,
phone_hash VARCHAR(128),
username VARCHAR(100) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'active',
last_login_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE
);
CREATE UNIQUE INDEX uk_operator_username_h2 ON operators(username);
-- Phase 17:举报与风控三表(与 V012__add_risk_and_report_tables.sql 生产 DDL 对齐)。
-- 此前完全缺失 → ReportService / RiskReferralService 整条真实 DB 路径零覆盖,
-- 且 ReportServiceTest 只能写 assertTrue(true) 假绿。已补入并移出 TEST_SCHEMA_OPTIONAL_TABLES 白名单。