数据库(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 失败
140 lines
4.2 KiB
JavaScript
140 lines
4.2 KiB
JavaScript
/**
|
||
* 红娘工作台主页
|
||
*
|
||
* ZC-MM-002(A1 / DEC-MATCHMAKER-002 锁定决策):
|
||
* - 普通红娘禁止认领(原 POST /services/{id}/claim 端点已删除),本页不再有任何接单逻辑;
|
||
* - 列表数据源从 GET /operator/matchmaker/pending(现仅分配权持有者可调,普通红娘 403)
|
||
* 改为 GET /operator/matchmaker/assignments/my(仅返回本人生效中的服务对象分配)。
|
||
* 响应为 assignment 字段(userId/assignmentReason/status/assignedAt/serviceId 等),
|
||
* 无服务类型/诉求描述字段,故按实际字段精简展示,不前端拼空值。
|
||
*
|
||
* 展示待处理服务、工作台数据概览
|
||
*/
|
||
const api = require('../../utils/api');
|
||
|
||
Page({
|
||
data: {
|
||
operatorName: '',
|
||
pendingCount: 0,
|
||
inProgressCount: 0,
|
||
todayCount: 0,
|
||
completedCount: 0,
|
||
myAssignments: [],
|
||
assignmentCount: 0,
|
||
loading: true,
|
||
currentDate: '',
|
||
},
|
||
|
||
onLoad() {
|
||
wx.setNavigationBarTitle({ title: '红娘工作台' });
|
||
// 获取操作员信息
|
||
const operatorName = wx.getStorageSync('operatorName') || '红娘';
|
||
this.setData({ operatorName });
|
||
|
||
// 设置当前日期
|
||
const now = new Date();
|
||
const dateStr = `${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日`;
|
||
this.setData({ currentDate: dateStr });
|
||
|
||
this.loadDashboard();
|
||
},
|
||
|
||
onShow() {
|
||
// 每次显示刷新数据
|
||
this.loadDashboard();
|
||
},
|
||
|
||
async loadDashboard() {
|
||
this.setData({ loading: true });
|
||
try {
|
||
const token = wx.getStorageSync('operatorToken');
|
||
if (!token) {
|
||
wx.redirectTo({ url: '/pages/matchmaker-login/matchmaker-login' });
|
||
return;
|
||
}
|
||
|
||
// 加载工作台数据 + 本人服务对象分配(ZC-MM-002:不再调用 /pending,普通红娘无该权限)
|
||
const [dashRes, assignmentRes] = await Promise.all([
|
||
api.get('/operator/matchmaker/dashboard', {}, { Authorization: `Bearer ${token}` }),
|
||
api.get('/operator/matchmaker/assignments/my', {}, { Authorization: `Bearer ${token}` }),
|
||
]);
|
||
|
||
if (dashRes.success) {
|
||
this.setData({
|
||
pendingCount: dashRes.data.pendingCount || 0,
|
||
inProgressCount: dashRes.data.inProgressCount || 0,
|
||
todayCount: dashRes.data.todayCount || 0,
|
||
completedCount: dashRes.data.completedCount || 0,
|
||
});
|
||
}
|
||
|
||
if (assignmentRes.success) {
|
||
const assignments = assignmentRes.data || [];
|
||
this.setData({
|
||
myAssignments: assignments,
|
||
assignmentCount: assignments.length,
|
||
});
|
||
}
|
||
} catch (err) {
|
||
console.error('加载工作台数据失败:', err);
|
||
} finally {
|
||
this.setData({ loading: false });
|
||
}
|
||
},
|
||
|
||
onViewService(e) {
|
||
const serviceId = e.currentTarget.dataset.id;
|
||
if (!serviceId) {
|
||
// 纯用户级分配(无关联服务单)无处可跳,不拼空参数
|
||
return;
|
||
}
|
||
wx.navigateTo({ url: `/pages/matchmaker-service-detail/matchmaker-service-detail?id=${serviceId}` });
|
||
},
|
||
|
||
// 分配状态 → 中文(避免枚举码直接上屏,见 profileLabels 铁律)
|
||
assignmentStatusLabel(code) {
|
||
const labels = {
|
||
active: '生效中',
|
||
ended: '已结束',
|
||
revoked: '已撤销',
|
||
transferred: '已转分配',
|
||
expired: '已过期',
|
||
};
|
||
return labels[code] || code || '';
|
||
},
|
||
|
||
// 分配范围 → 中文
|
||
assignmentScopeLabel(code) {
|
||
const labels = { primary: '主责', collaborating: '协同' };
|
||
return labels[code] || code || '';
|
||
},
|
||
|
||
formatTime(timeStr) {
|
||
if (!timeStr) return '';
|
||
const date = new Date(timeStr);
|
||
const now = new Date();
|
||
const diff = now - date;
|
||
const days = Math.floor(diff / 86400000);
|
||
if (days === 0) return '今天';
|
||
if (days === 1) return '昨天';
|
||
if (days < 7) return days + '天前';
|
||
const m = date.getMonth() + 1;
|
||
const d = date.getDate();
|
||
return m + '月' + d + '日';
|
||
},
|
||
|
||
onLogout() {
|
||
wx.showModal({
|
||
title: '退出登录',
|
||
content: '确定要退出登录吗?',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
wx.removeStorageSync('operatorToken');
|
||
wx.removeStorageSync('operatorName');
|
||
wx.reLaunch({ url: '/pages/matchmaker-login/matchmaker-login' });
|
||
}
|
||
},
|
||
});
|
||
},
|
||
});
|