feat(matchmaker,contactauth): 受控联系授权机制立项落地(OQ-MATCHMAKER-003 / DEC-MATCHMAKER-003 B1 例外通道)

后端:
- V056 matchmaker_contact_authorizations 表:channel CHECK(phone/offline_meeting/full_contact)、六态状态机(pending_approval→pending_consent→active→expired/revoked, →rejected)、部分唯一索引 uq_mm_contact_auth_active 防同红娘-同用户-同渠道多 ACTIVE、双态惰性收敛(active 过期 + pending_consent 超同意窗口)
- ContactAuthorizationConfig 五 canonical key(matchmaker.contact.*):enabled 代码缺省 false fail-safe(yml 显式 true)/approver-authorities 双岗并列/consent-deadline-hours 72/default-validity-days 7/retention-days 365,全部 DEFAULT+CONFIG 待产品+法务终审
- ContactAuthorizationPolicy.assertContactAllowed(404 化 deny 先审计后 404) + canContact 徽标查询
- ContactAuthorizationService:request(须有效分配+purpose 必填)/approve(权限码动态判定∈配置+组长组织范围)/consent(用户本人,仅 pending_consent)/decline/revoke(reason 必填)/disclose(唯一披露出口:enabled fail-safe→assertActiveAssignment→assertContactAllowed→仅返回 phone/disclosedAt/authorizationId 三键,每次落 SensitiveAccessLog user_phone_disclosure,phone 不落应用日志)
- 双控制器:运营端 /operator/matchmaker/contact-authorizations + C 端 /contact-authorizations(UserPrincipal 归属校验,独立端点无预同意参数),不碰 SecurityConfig

守卫翻转(机械锁强制复审机制按设计生效):
- MatchmakerContactGuardTest 由「锁不存在」翻转为「锁新契约」五条:回显白名单类收敛/端点前缀收敛(受治理三包)/到期语义/consent 独立性三端扫描(后端+miniapp+admin-web,修复原只扫后端的预同意参数逃逸缺口)/ReferralService 白名单与无 @Scheduled 保留
- run-backend-contracts CURATED 18→20

前端:
- miniapp 新页 pages/contact-consent(目的/渠道/窗口明示+独立同意拒绝按钮无预勾选,app.json 第 64 页) + 红娘工作台申请/列表/撤销
- admin-web「联系授权审批」tab(审批岗队列+本人授权撤销,枚举码中文映射)

验证:mvn verify 895/895(3 skip)、UI 契约 24/24(64 页)、后端契约门禁 228/CURATED 20、小程序真编译 87 WXML 0 失败;负向注入 5(工程师)+2(QA 不同落点)判红→还原复绿;QA 抓 disclose 键数收敛断言缺口已补并回归;QA fresh eyes PASS 0 P0/P1
This commit is contained in:
WorkBuddy
2026-10-11 13:08:02 +08:00
parent 4248e9574e
commit d3735e3afa
27 changed files with 1996 additions and 144 deletions
+35
View File
@@ -550,6 +550,41 @@ CREATE TABLE operators (
);
CREATE UNIQUE INDEX uk_operator_username_h2 ON operators(username);
-- V056(ZC-MM-003 B1 立项):matchmaker_contact_authorizations 受控联系授权表。
-- 生产为部分唯一索引 uq_mm_contact_auth_active WHERE status='active';
-- H2 不支持部分索引,用「生成列 + 唯一索引」等价替代(对照 V053 范式):
-- active_contact_key 仅在 status='active' 时取 operator|user|channel 复合键,否则 NULL。
-- 去外键(operators/users 本表均已存在,与既有「去外键」惯例一致)。
CREATE TABLE matchmaker_contact_authorizations (
id VARCHAR(36) PRIMARY KEY,
operator_id VARCHAR(36) NOT NULL,
user_id VARCHAR(36) NOT NULL,
channel VARCHAR(32) NOT NULL,
purpose VARCHAR(200) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'pending_approval',
approved_by VARCHAR(36),
approved_at TIMESTAMP WITH TIME ZONE,
user_consented_at TIMESTAMP WITH TIME ZONE,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
revoked_at TIMESTAMP WITH TIME ZONE,
revoked_by VARCHAR(36),
revoke_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_contact_auth_channel CHECK (channel IN ('phone', 'offline_meeting', 'full_contact')),
CONSTRAINT chk_contact_auth_status CHECK (status IN (
'pending_approval', 'pending_consent', 'active', 'rejected', 'expired', 'revoked'
)),
active_contact_key VARCHAR(200) GENERATED ALWAYS AS (
CASE WHEN status = 'active'
THEN operator_id || '|' || user_id || '|' || channel END
)
);
CREATE UNIQUE INDEX uq_mm_contact_auth_active_h2 ON matchmaker_contact_authorizations(active_contact_key);
CREATE INDEX idx_contact_auth_operator_status_h2 ON matchmaker_contact_authorizations(operator_id, status);
CREATE INDEX idx_contact_auth_user_status_h2 ON matchmaker_contact_authorizations(user_id, status);
-- V001 配套(ZC-MM-006 / T02):权限四表 —— JwtAuthFilter 运营分支实时查库
-- (PermissionMapper join role_permissions/operator_roles)在 H2 上的真实 DB 路径支撑;
-- 此前缺失导致所有 operator token 测试只能 @MockBean PermissionService 绕过。