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:
@@ -1,10 +1,16 @@
|
||||
/**
|
||||
* 红娘工作台主页
|
||||
*
|
||||
*
|
||||
* 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');
|
||||
const { serviceTypeLabel } = require('../../constants/profileLabels');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -13,7 +19,8 @@ Page({
|
||||
inProgressCount: 0,
|
||||
todayCount: 0,
|
||||
completedCount: 0,
|
||||
pendingServices: [],
|
||||
myAssignments: [],
|
||||
assignmentCount: 0,
|
||||
loading: true,
|
||||
currentDate: '',
|
||||
},
|
||||
@@ -23,12 +30,12 @@ Page({
|
||||
// 获取操作员信息
|
||||
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();
|
||||
},
|
||||
|
||||
@@ -46,10 +53,10 @@ Page({
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载工作台数据
|
||||
const [dashRes, pendingRes] = await Promise.all([
|
||||
// 加载工作台数据 + 本人服务对象分配(ZC-MM-002:不再调用 /pending,普通红娘无该权限)
|
||||
const [dashRes, assignmentRes] = await Promise.all([
|
||||
api.get('/operator/matchmaker/dashboard', {}, { Authorization: `Bearer ${token}` }),
|
||||
api.get('/operator/matchmaker/pending', {}, { Authorization: `Bearer ${token}` }),
|
||||
api.get('/operator/matchmaker/assignments/my', {}, { Authorization: `Bearer ${token}` }),
|
||||
]);
|
||||
|
||||
if (dashRes.success) {
|
||||
@@ -61,9 +68,11 @@ Page({
|
||||
});
|
||||
}
|
||||
|
||||
if (pendingRes.success) {
|
||||
if (assignmentRes.success) {
|
||||
const assignments = assignmentRes.data || [];
|
||||
this.setData({
|
||||
pendingServices: pendingRes.data || [],
|
||||
myAssignments: assignments,
|
||||
assignmentCount: assignments.length,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -75,30 +84,29 @@ Page({
|
||||
|
||||
onViewService(e) {
|
||||
const serviceId = e.currentTarget.dataset.id;
|
||||
if (!serviceId) {
|
||||
// 纯用户级分配(无关联服务单)无处可跳,不拼空参数
|
||||
return;
|
||||
}
|
||||
wx.navigateTo({ url: `/pages/matchmaker-service-detail/matchmaker-service-detail?id=${serviceId}` });
|
||||
},
|
||||
|
||||
onClaimService(e) {
|
||||
const serviceId = e.currentTarget.dataset.id;
|
||||
wx.showLoading({ title: '接单中...' });
|
||||
|
||||
const token = wx.getStorageSync('operatorToken');
|
||||
// 注意:api.post 签名为 (url, data, headers, idempotencyKey),Authorization 必须放第 3 参(headers),
|
||||
// 此前误放到第 4 参(idempotencyKey),导致实际请求不携带任何鉴权头、接单必 401。
|
||||
api.post(`/operator/matchmaker/services/${serviceId}/claim`, {}, { Authorization: `Bearer ${token}` })
|
||||
.then(res => {
|
||||
wx.hideLoading();
|
||||
if (res.success) {
|
||||
wx.showToast({ title: '接单成功', icon: 'success' });
|
||||
this.loadDashboard();
|
||||
} else {
|
||||
wx.showToast({ title: res.message || '接单失败', icon: 'none' });
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: '网络错误', icon: 'none' });
|
||||
});
|
||||
// 分配状态 → 中文(避免枚举码直接上屏,见 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) {
|
||||
@@ -115,11 +123,6 @@ Page({
|
||||
return m + '月' + d + '日';
|
||||
},
|
||||
|
||||
// 红娘服务类型 → 中文(避免枚举码直接上屏,见 profileLabels 铁律)
|
||||
serviceTypeLabel(code) {
|
||||
return serviceTypeLabel(code);
|
||||
},
|
||||
|
||||
onLogout() {
|
||||
wx.showModal({
|
||||
title: '退出登录',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- 红娘工作台 -->
|
||||
<!-- 红娘工作台(ZC-MM-002:普通红娘仅见本人有效分配,无接单) -->
|
||||
<view class="page">
|
||||
<!-- 欢迎区 -->
|
||||
<view class="header">
|
||||
@@ -17,7 +17,7 @@
|
||||
<view class="stat-card" hover-class="stat-hover">
|
||||
<view class="stat-icon pending">📋</view>
|
||||
<view class="stat-value">{{pendingCount}}</view>
|
||||
<view class="stat-label">待处理</view>
|
||||
<view class="stat-label">待分配</view>
|
||||
</view>
|
||||
<view class="stat-card" hover-class="stat-hover">
|
||||
<view class="stat-icon progress">⏳</view>
|
||||
@@ -36,39 +36,39 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 待处理列表 -->
|
||||
<!-- 我的服务对象分配 -->
|
||||
<view class="section" wx:if="{{!loading}}">
|
||||
<view class="section-header">
|
||||
<view class="section-title-wrap">
|
||||
<text class="section-dot"></text>
|
||||
<text class="section-title">待处理服务</text>
|
||||
<text class="section-title">我的服务对象分配</text>
|
||||
</view>
|
||||
<text class="section-count" wx:if="{{pendingCount > 0}}">共{{pendingCount}}件</text>
|
||||
<text class="section-count" wx:if="{{assignmentCount > 0}}">共{{assignmentCount}}人</text>
|
||||
</view>
|
||||
|
||||
<view class="service-list" wx:if="{{pendingServices.length > 0}}">
|
||||
<view
|
||||
wx:for="{{pendingServices}}"
|
||||
wx:key="serviceId"
|
||||
<view class="service-list" wx:if="{{myAssignments.length > 0}}">
|
||||
<view
|
||||
wx:for="{{myAssignments}}"
|
||||
wx:key="id"
|
||||
class="service-card"
|
||||
data-id="{{item.serviceId}}"
|
||||
bindtap="onViewService"
|
||||
>
|
||||
<view class="service-left">
|
||||
<view class="service-header">
|
||||
<view class="service-tag">{{serviceTypeLabel(item.serviceType)}}</view>
|
||||
<view class="service-time">{{formatTime(item.createdAt)}}</view>
|
||||
<view class="service-tag">{{assignmentScopeLabel(item.assignmentScope)}}</view>
|
||||
<view class="service-tag">{{assignmentStatusLabel(item.status)}}</view>
|
||||
<view class="service-time">{{formatTime(item.assignedAt)}}</view>
|
||||
</view>
|
||||
<view class="service-desc">{{item.description}}</view>
|
||||
<view class="service-desc">{{item.assignmentReason}}</view>
|
||||
</view>
|
||||
<button class="btn-claim" catchtap="onClaimService" data-id="{{item.serviceId}}" hover-class="btn-hover">接单</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="empty-state" wx:else>
|
||||
<view class="empty-illustration">📭</view>
|
||||
<view class="empty-text">暂无待处理服务</view>
|
||||
<view class="empty-hint">用户申请的服务将在这里展示</view>
|
||||
<view class="empty-text">暂无服务对象分配</view>
|
||||
<view class="empty-hint">分配给你的服务对象将在这里展示</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user