Files
danyang-match/miniapp/pages/contact-consent/contact-consent.js
T
WorkBuddy d3735e3afa 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
2026-10-11 13:08:02 +08:00

103 lines
3.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 联系授权确认页(C 端用户,ZC-MM-003 / B1 例外通道)
*
* PIPL 单独同意契约:
* - 授权目的 / 渠道 / 有效期窗口逐条明示后才可操作;
* - 同意与拒绝是两个独立按钮,无预勾选、无捆绑(后端亦为独立 POST 端点,
* 守卫 MatchmakerContactGuardTest ④ 锁定);
* - 数据源 GET /contact-authorizations/my(pendingConsent 待同意队列 + 全部记录)。
*/
const api = require('../../utils/api');
const CHANNEL_LABELS = {
phone: '电话联系',
offline_meeting: '线下见面',
full_contact: '完整联系方式',
};
Page({
data: {
loading: true,
pendingConsent: [],
history: [],
},
onShow() {
this.loadAuthorizations();
},
async loadAuthorizations() {
this.setData({ loading: true });
try {
const res = await api.get('/contact-authorizations/my');
if (res.success) {
const pending = (res.data.pendingConsent || []).map((item) => this.decorate(item));
const history = (res.data.list || []).map((item) => this.decorate(item));
this.setData({ pendingConsent: pending, history });
}
} catch (err) {
console.error('加载联系授权失败:', err);
} finally {
this.setData({ loading: false });
}
},
/** 渠道/状态/有效期 → 中文展示(枚举码不上屏) */
decorate(item) {
const labels = {
pending_approval: '待审批', pending_consent: '待你确认', active: '生效中',
rejected: '已拒绝', expired: '已过期', revoked: '已撤销',
};
return Object.assign({}, item, {
channelLabel: CHANNEL_LABELS[item.channel] || item.channel,
statusLabel: labels[item.status] || item.status,
expireText: item.expiresAt ? this.formatTime(item.expiresAt) : '',
});
},
formatTime(timeStr) {
if (!timeStr) return '';
const d = new Date(timeStr);
return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()} ${d.getHours()}:${String(d.getMinutes()).padStart(2, '0')}`;
},
/** 独立同意(单独明示动作,无捆绑参数) */
async onConsent(e) {
const id = e.currentTarget.dataset.id;
const item = this.data.pendingConsent.find((a) => a.id === id);
if (!item) return;
wx.showModal({
title: '确认同意联系授权',
content: `同意「${item.channelLabel}」:${item.purpose}。同意后红娘可在有效期内通过该方式联系你。`,
confirmText: '同意',
cancelText: '再想想',
success: async (res) => {
if (!res.confirm) return;
try {
const resp = await api.post(`/contact-authorizations/${id}/consent`);
if (resp.success) {
wx.showToast({ title: '已同意', icon: 'success' });
this.loadAuthorizations();
}
} catch (err) {
wx.showToast({ title: '操作失败', icon: 'none' });
}
},
});
},
/** 独立拒绝 */
async onDecline(e) {
const id = e.currentTarget.dataset.id;
try {
const resp = await api.post(`/contact-authorizations/${id}/decline`);
if (resp.success) {
wx.showToast({ title: '已拒绝', icon: 'none' });
this.loadAuthorizations();
}
} catch (err) {
wx.showToast({ title: '操作失败', icon: 'none' });
}
},
});