release: freeze 2026-10-15 validated release baseline
CI/CD Pipeline / UI Contracts (source-structure gates) (push) Canceled after 0s
CI/CD Pipeline / E2E (live stack) (push) Canceled after 0s
CI/CD Pipeline / Backend Build & Test (push) Canceled after 0s
CI/CD Pipeline / Admin Web Build (push) Canceled after 0s
CI/CD Pipeline / Mini Program Compile Check (push) Canceled after 0s
CI/CD Pipeline / Migration Validation (push) Canceled after 0s
CI/CD Pipeline / Test Shell Guard (Phase 23 Gate) (push) Canceled after 0s

This commit is contained in:
WorkBuddy
2026-10-15 09:47:31 +08:00
parent 827c23db1d
commit 3c2b967274
149 changed files with 4324 additions and 348 deletions
@@ -33,6 +33,17 @@ Page({
contactAuths: [],
contactAuthCount: 0,
submittingAuth: false,
referralTasks: [],
recommendations: [],
referralDialogVisible: false,
referralTargetIndex: 0,
referralTargetLabels: [],
referralTypeIndex: 0,
referralTypes: ['服务站', '公益活动'],
referralTypeCodes: ['service_station', 'activity'],
referralTargetId: '',
referralNote: '',
submittingReferral: false,
},
onLoad() {
@@ -64,26 +75,28 @@ Page({
}
// 加载工作台数据 + 本人服务对象分配(ZC-MM-002:不再调用 /pending,普通红娘无该权限)
const [dashRes, assignmentRes] = await Promise.all([
const [dashRes, assignmentRes, referralRes, recommendationRes] = await Promise.all([
api.get('/operator/matchmaker/dashboard', {}, { Authorization: `Bearer ${token}` }),
api.get('/operator/matchmaker/assignments/my', {}, { Authorization: `Bearer ${token}` }),
api.get('/referrals/operator/history', {}, { Authorization: `Bearer ${token}` }),
api.get('/operator/matchmaker/recommendations', {}, { Authorization: `Bearer ${token}` }),
]);
if (dashRes.success) {
if (dashRes) {
this.setData({
pendingCount: dashRes.data.pendingCount || 0,
inProgressCount: dashRes.data.inProgressCount || 0,
todayCount: dashRes.data.todayCount || 0,
completedCount: dashRes.data.completedCount || 0,
pendingCount: dashRes.pendingCount || 0,
inProgressCount: dashRes.inProgressCount || 0,
todayCount: dashRes.todayCount || 0,
completedCount: dashRes.completedCount || 0,
});
}
if (assignmentRes.success) {
const assignments = assignmentRes.data || [];
if (assignmentRes) {
const assignments = assignmentRes || [];
// 联系授权申请目标 = 本人生效中的分配对象(B1:只能对分配对象发起)
const targets = assignments.map((a) => ({
userId: a.userId,
label: '用户 ' + String(a.userId).slice(0, 8) + '…(' + (a.assignmentReason || '服务对象') + ')',
label: (a.userDisplayNumber || '待实名编号') + '(' + (a.userDisplayName || '服务对象') + ')',
}));
this.setData({
myAssignments: assignments,
@@ -93,6 +106,25 @@ Page({
});
this.loadContactAuthorizations();
}
if (referralRes) {
this.setData({
referralTasks: (referralRes || []).map((item) => ({
...item,
typeLabel: item.type === 'activity' ? '公益活动' : '服务站',
targetLabel: item.type === 'activity'
? '公益活动转介'
: this.serviceTypeLabel(item.serviceType),
statusLabel: item.userConfirmedAt ? '用户已确认' : '待用户确认',
timeText: this.formatTime(item.createdAt || item.registeredAt),
})),
});
}
if (recommendationRes) {
this.setData({ recommendations: (recommendationRes || []).map((item) => ({
...item,
statusLabel: this.recommendationStatusLabel(item.status),
})) });
}
} catch (err) {
console.error('加载工作台数据失败:', err);
} finally {
@@ -103,12 +135,66 @@ Page({
onViewService(e) {
const serviceId = e.currentTarget.dataset.id;
if (!serviceId) {
// 纯用户级分配(无关联服务单)无处可跳,不拼空参数
const index = Number(e.currentTarget.dataset.index);
const item = this.data.myAssignments[index] || {};
wx.showModal({
title: item.userDisplayNumber || '服务对象',
content: `${item.userDisplayName || '未设置姓名'}${item.userAge ? `,${item.userAge}岁` : ''}${item.userRegionName ? `,${item.userRegionName}` : ''}\n当前分配未关联服务单,请联系组长补建服务记录。`,
showCancel: false,
});
return;
}
wx.navigateTo({ url: `/pages/matchmaker-service-detail/matchmaker-service-detail?id=${serviceId}` });
},
openReferral() {
if (!this.data.myAssignments.length) {
wx.showToast({ title: '暂无可转介的服务对象', icon: 'none' });
return;
}
this.setData({
referralDialogVisible: true,
referralTargetIndex: 0,
referralTargetLabels: this.data.myAssignments.map((item) =>
`${item.userDisplayNumber || '待实名编号'}(${item.userDisplayName || '服务对象'})`),
referralTypeIndex: 0,
referralTargetId: '',
referralNote: '',
});
},
closeReferral() { this.setData({ referralDialogVisible: false }); },
noop() {},
onReferralTargetChange(e) { this.setData({ referralTargetIndex: Number(e.detail.value) }); },
onReferralTypeChange(e) { this.setData({ referralTypeIndex: Number(e.detail.value) }); },
onReferralTargetIdInput(e) { this.setData({ referralTargetId: e.detail.value }); },
onReferralNoteInput(e) { this.setData({ referralNote: e.detail.value }); },
async submitReferral() {
if (this.data.submittingReferral) return;
const assignment = this.data.myAssignments[this.data.referralTargetIndex];
const targetId = (this.data.referralTargetId || '').trim();
if (!assignment || !assignment.userId) { wx.showToast({ title: '请选择服务对象', icon: 'none' }); return; }
if (!targetId) { wx.showToast({ title: '请填写活动或服务站编号', icon: 'none' }); return; }
this.setData({ submittingReferral: true });
try {
const token = wx.getStorageSync('operatorToken');
const type = this.data.referralTypeCodes[this.data.referralTypeIndex];
const path = type === 'activity' ? '/referrals/activity' : '/referrals/service-station';
const payload = type === 'activity'
? { userId: assignment.userId, activityId: targetId }
: { userId: assignment.userId, locationId: targetId, serviceType: 'counselor_consultation', note: this.data.referralNote.trim() };
await api.post(path, payload, { Authorization: `Bearer ${token}` }, api.idempotencyKey(`matchmaker-referral:${assignment.userId}:${targetId}`));
wx.showToast({ title: '转介已提交,等待用户确认', icon: 'none' });
this.setData({ referralDialogVisible: false });
await this.loadDashboard();
} catch (error) {
wx.showToast({ title: (error && error.message) || '转介提交失败', icon: 'none' });
} finally {
this.setData({ submittingReferral: false });
}
},
// ==================== ZC-MM-003 / B1:受控联系授权 ====================
/** 本人授权列表(GET /operator/matchmaker/contact-authorizations/my) */
@@ -117,8 +203,8 @@ Page({
const token = wx.getStorageSync('operatorToken');
const res = await api.get('/operator/matchmaker/contact-authorizations/my',
{}, { Authorization: `Bearer ${token}` });
if (res.success) {
const items = (res.data.list || []).map((item) => Object.assign({}, item, {
if (res) {
const items = ((res.list || [])).map((item) => Object.assign({}, item, {
channelLabel: this.contactChannelLabel(item.channel),
statusLabel: this.contactStatusLabel(item.status),
expireText: item.expiresAt ? this.formatTime(item.expiresAt) : '',
@@ -145,6 +231,20 @@ Page({
return labels[code] || code || '';
},
serviceTypeLabel(code) {
const labels = {
counselor_consultation: '婚恋咨询',
premarital_counseling: '婚前辅导',
legal_consultation: '法律咨询',
};
return labels[code] || '服务站服务';
},
recommendationStatusLabel(code) {
const labels = { pending_view: '待用户查看', viewed: '用户已查看', liked: '用户已喜欢', passed: '用户已跳过', confirmed_mutual: '已双向确认', expired: '已过期' };
return labels[code] || code || '未知状态';
},
onContactTargetChange(e) { this.setData({ contactTargetIndex: Number(e.detail.value) }); },
onContactChannelChange(e) { this.setData({ contactChannelIndex: Number(e.detail.value) }); },
onContactPurposeInput(e) { this.setData({ contactPurpose: e.detail.value }); },
@@ -163,7 +263,7 @@ Page({
const res = await api.post('/operator/matchmaker/contact-authorizations', {
userId: target.userId, channel: channel, purpose: purpose,
}, { Authorization: `Bearer ${token}` });
if (res.success) {
if (res) {
wx.showToast({ title: '申请已提交,等待审批', icon: 'none' });
this.setData({ contactPurpose: '' });
this.loadContactAuthorizations();
@@ -190,7 +290,7 @@ Page({
const token = wx.getStorageSync('operatorToken');
const resp = await api.post(`/operator/matchmaker/contact-authorizations/${id}/revoke`,
{ reason: reason }, { Authorization: `Bearer ${token}` });
if (resp.success) {
if (resp) {
wx.showToast({ title: '已撤销', icon: 'none' });
this.loadContactAuthorizations();
}
@@ -16,28 +16,36 @@
<view class="stats-container">
<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-info">
<view class="stat-value">{{pendingCount}}</view>
<view class="stat-label">待分配</view>
</view>
</view>
<view class="stat-card" hover-class="stat-hover">
<view class="stat-icon progress">⏳</view>
<view class="stat-value">{{inProgressCount}}</view>
<view class="stat-label">进行中</view>
<view class="stat-info">
<view class="stat-value">{{inProgressCount}}</view>
<view class="stat-label">进行中</view>
</view>
</view>
<view class="stat-card" hover-class="stat-hover">
<view class="stat-icon completed">✅</view>
<view class="stat-value">{{completedCount}}</view>
<view class="stat-label">已完成</view>
<view class="stat-info">
<view class="stat-value">{{completedCount}}</view>
<view class="stat-label">已完成</view>
</view>
</view>
<view class="stat-card" hover-class="stat-hover">
<view class="stat-icon today">📅</view>
<view class="stat-value">{{todayCount}}</view>
<view class="stat-label">今日</view>
<view class="stat-info">
<view class="stat-value">{{todayCount}}</view>
<view class="stat-label">今日</view>
</view>
</view>
</view>
<!-- 我的服务对象分配 -->
<view class="section" wx:if="{{!loading}}">
<view class="section section-compact" wx:if="{{!loading}}">
<view class="section-header">
<view class="section-title-wrap">
<text class="section-dot"></text>
@@ -52,6 +60,7 @@
wx:key="id"
class="service-card"
data-id="{{item.serviceId}}"
data-index="{{index}}"
bindtap="onViewService"
>
<view class="service-left">
@@ -60,18 +69,78 @@
<view class="service-tag">{{assignmentStatusLabel(item.status)}}</view>
<view class="service-time">{{formatTime(item.assignedAt)}}</view>
</view>
<view class="service-desc">{{item.assignmentReason}}</view>
<view class="service-name">{{item.userDisplayName || '未设置姓名'}} <text wx:if="{{item.userDisplayNumber}}">{{item.userDisplayNumber}}</text></view>
<view class="service-desc">{{item.userAge ? item.userAge + '岁 · ' : ''}}{{item.userRegionName || '地区未设置'}}</view>
</view>
<button class="detail-btn" size="mini" catchtap="onViewService" data-id="{{item.serviceId}}" data-index="{{index}}">查看详情</button>
</view>
</view>
<view class="empty-state" wx:else>
<view class="empty-state empty-compact" wx:else>
<view class="empty-illustration">📭</view>
<view class="empty-text">暂无服务对象分配</view>
<view class="empty-hint">分配给你的服务对象将在这里展示</view>
</view>
</view>
<view class="section section-compact" wx:if="{{!loading}}">
<view class="section-header">
<view class="section-title-wrap">
<text class="section-dot"></text>
<text class="section-title">转介任务</text>
</view>
<text class="section-count">共{{referralTasks.length}}条</text>
</view>
<view class="auth-list" wx:if="{{referralTasks.length}}">
<view class="auth-card" wx:for="{{referralTasks}}" wx:key="id">
<view class="auth-row">
<text class="auth-channel">{{item.typeLabel}}</text>
<text class="auth-status">{{item.statusLabel}}</text>
</view>
<view class="auth-purpose">{{item.targetLabel}}</view>
<view class="auth-meta">{{item.timeText}}</view>
</view>
</view>
<view class="empty-state empty-compact" wx:else>
<view class="empty-text">暂无转介记录</view>
<view class="empty-hint">可从服务对象分配发起公益活动或服务站转介</view>
</view>
<button class="submit-btn referral-open-btn" bindtap="openReferral">发起转介</button>
</view>
<view wx:if="{{referralDialogVisible}}" class="dialog-mask">
<view class="dialog-card" catchtap="noop">
<view class="dialog-title">发起转介</view>
<picker mode="selector" range="{{referralTargetLabels}}" value="{{referralTargetIndex}}" bindchange="onReferralTargetChange">
<view class="form-row"><text class="form-label">服务对象</text><text class="form-value">{{referralTargetLabels[referralTargetIndex]}}</text></view>
</picker>
<picker mode="selector" range="{{referralTypes}}" value="{{referralTypeIndex}}" bindchange="onReferralTypeChange">
<view class="form-row"><text class="form-label">转介类型</text><text class="form-value">{{referralTypes[referralTypeIndex]}}</text></view>
</picker>
<input class="form-input" value="{{referralTargetId}}" bindinput="onReferralTargetIdInput" placeholder="填写活动或服务站编号" />
<input wx:if="{{referralTypeIndex === 0}}" class="form-input" value="{{referralNote}}" bindinput="onReferralNoteInput" placeholder="备注(可选)" />
<view class="dialog-actions">
<button size="mini" bindtap="closeReferral">取消</button>
<button size="mini" type="primary" loading="{{submittingReferral}}" bindtap="submitReferral">提交转介</button>
</view>
</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></view>
<text class="section-count">共{{recommendations.length}}条</text>
</view>
<view class="auth-list" wx:if="{{recommendations.length}}">
<view class="auth-card" wx:for="{{recommendations}}" wx:key="id">
<view class="auth-row"><text class="auth-channel">候选建议</text><text class="auth-status">{{item.statusLabel}}</text></view>
<view class="auth-purpose">推荐对象:{{item.targetUserDisplayNumber || '待生成编号'}}</view>
<view class="auth-meta">推荐理由:{{item.reason || '未填写'}}</view>
</view>
</view>
<view class="empty-state" wx:else><view class="empty-hint">暂无推荐进展,用户将自行决定喜欢或暂不考虑</view></view>
</view>
<!-- 联系授权(ZC-MM-003 / B1:受控联系,用户单独同意后才可生效) -->
<view class="section" wx:if="{{!loading}}">
<view class="section-header">
@@ -54,7 +54,7 @@
font-size:22rpx;
}
/* 数据概览 - 2x2 网格 */
/* 数据概览 - 2x2 紧凑网格(原高度 ~234rpx → 压缩到 ~80rpx,约 1/3) */
.stats-container {
display:flex;
flex-wrap:wrap;
@@ -66,31 +66,51 @@
.stat-card {
width:calc(50% - 16rpx);
margin:0 8rpx 24rpx 8rpx;
margin:0 8rpx 16rpx 8rpx;
background:var(--color-bg-card);
border-radius:24rpx;
padding:40rpx 20rpx;
text-align:center;
box-shadow:0 8rpx 32rpx rgba(0,0,0,0.1);
border-radius:20rpx;
padding:16rpx 20rpx;
display:flex;
align-items:center;
gap:16rpx;
box-shadow:0 4rpx 16rpx rgba(0,0,0,0.08);
box-sizing:border-box;
}
.stat-icon {
font-size:48rpx;
margin-bottom:16rpx;
font-size:36rpx;
flex:none;
width:64rpx;
height:64rpx;
display:flex;
align-items:center;
justify-content:center;
background:var(--color-primary-light);
border-radius:16rpx;
}
.stat-card .stat-icon.progress { background:#FFF6E6; }
.stat-card .stat-icon.completed { background:#EDF8F1; }
.stat-card .stat-icon.today { background:#EEF5FF; }
.stat-info {
flex:1;
min-width:0;
display:flex;
flex-direction:column;
}
.stat-value {
font-size:56rpx;
font-size:40rpx;
font-weight:700;
color:var(--color-text-strong);
line-height:1.2;
margin-bottom:8rpx;
}
.stat-label {
font-size:26rpx;
font-size:22rpx;
color:var(--color-text-tertiary);
margin-top:4rpx;
}
/* 列表区块 */
@@ -152,6 +172,7 @@
border-radius:16rpx;
border:1rpx solid var(--color-divider);
}
.detail-btn{flex:none;margin:0;background:#2f7df6;color:#fff;font-size:22rpx;padding:8rpx 16rpx;line-height:1.6;border-radius:8rpx}
.service-left {
flex:1;
@@ -222,6 +243,27 @@
color:var(--color-text-disabled);
}
/* 紧凑区块:仅“我的服务对象分配 / 转介任务”两栏,垂直高度约为原来的一半 */
.section-compact {
padding:24rpx 32rpx;
}
.section-compact .section-header {
margin-bottom:12rpx;
}
.empty-compact {
padding:12rpx 40rpx;
}
.empty-compact .empty-illustration {
font-size:48rpx;
margin-bottom:4rpx;
}
.empty-compact .empty-text {
margin-bottom:2rpx;
}
.section-compact .referral-open-btn {
margin-top:8rpx;
}
/* 加载中 */
.loading-wrap {
display:flex;
@@ -281,3 +323,8 @@ min-width:var(--tap-min);}
.auth-purpose { font-size: 26rpx; color: #5e6d82; margin-top: 8rpx; word-break: break-all; }
.auth-meta { font-size: 22rpx; color: #b0b3b8; margin-top: 8rpx; }
.revoke-btn { margin-top: 12rpx; background: #f4f4f5; color: #606266; }
.referral-open-btn{margin-top:20rpx;width:100%}
.dialog-mask{position:fixed;z-index:99;left:0;right:0;top:0;bottom:0;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;padding:32rpx;box-sizing:border-box}
.dialog-card{width:100%;background:#fff;border-radius:20rpx;padding:28rpx;box-sizing:border-box}
.dialog-title{font-size:32rpx;font-weight:600;color:#20243a;margin-bottom:20rpx}
.dialog-actions{display:flex;justify-content:flex-end;gap:16rpx;margin-top:20rpx}