Commit c0151ca8 authored by drigle's avatar drigle

feat: add forward OA and app notifications

parent a2314f0c
...@@ -27,6 +27,7 @@ import { ...@@ -27,6 +27,7 @@ import {
loadForwardEditableRecords, loadForwardEditableRecords,
loadForwardRecords, loadForwardRecords,
saveForwardEditableRecords, saveForwardEditableRecords,
dispatchForwardStateNotification,
updateForwardRecordStatus, updateForwardRecordStatus,
} from './modelMonitorService'; } from './modelMonitorService';
import { useContext, useEffect, useMemo, useState } from 'react'; import { useContext, useEffect, useMemo, useState } from 'react';
...@@ -94,6 +95,7 @@ export default function ForwardApprovals() { ...@@ -94,6 +95,7 @@ export default function ForwardApprovals() {
const [editSaving, setEditSaving] = useState(false); const [editSaving, setEditSaving] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [notice, setNotice] = useState(''); const [notice, setNotice] = useState('');
const [notificationWarning, setNotificationWarning] = useState('');
const [activeRecordId, setActiveRecordId] = useState(''); const [activeRecordId, setActiveRecordId] = useState('');
const [isDetailDialogOpen, setIsDetailDialogOpen] = useState(false); const [isDetailDialogOpen, setIsDetailDialogOpen] = useState(false);
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
...@@ -186,8 +188,22 @@ export default function ForwardApprovals() { ...@@ -186,8 +188,22 @@ export default function ForwardApprovals() {
async function handleUpdateRecord(record, nextStatuses) { async function handleUpdateRecord(record, nextStatuses) {
setSaving(true); setSaving(true);
setNotificationWarning('');
try { try {
await updateForwardRecordStatus(record.recordName, nextStatuses); const updatedRecord = await updateForwardRecordStatus(record.recordName, nextStatuses);
const notificationEvent = nextStatuses.approval_status === 'approved'
? 'first_approval_pending'
: nextStatuses.final_approval_status === 'approved'
? 'final_approval_pending'
: nextStatuses.approval_status === 'rejected'
? 'first_rejected'
: nextStatuses.final_approval_status === 'rejected'
? 'final_rejected'
: '';
const notificationResult = notificationEvent
? await dispatchForwardStateNotification(notificationEvent, updatedRecord, userInfo)
: { warning: '' };
setNotificationWarning(notificationResult.warning);
await refreshRecords('审批状态已更新。'); await refreshRecords('审批状态已更新。');
setError(''); setError('');
} catch (updateError) { } catch (updateError) {
...@@ -287,6 +303,7 @@ export default function ForwardApprovals() { ...@@ -287,6 +303,7 @@ export default function ForwardApprovals() {
} }
setEditSaving(true); setEditSaving(true);
setNotificationWarning('');
try { try {
if (editableRecords.length > 0) { if (editableRecords.length > 0) {
const saved = await saveForwardEditableRecords( const saved = await saveForwardEditableRecords(
...@@ -309,6 +326,12 @@ export default function ForwardApprovals() { ...@@ -309,6 +326,12 @@ export default function ForwardApprovals() {
}); });
setEditingRecord(updatedForwardRecord); setEditingRecord(updatedForwardRecord);
const notificationResult = await dispatchForwardStateNotification(
'first_approval_pending',
updatedForwardRecord,
userInfo,
);
setNotificationWarning(notificationResult.warning);
setIsEditDialogOpen(false); setIsEditDialogOpen(false);
await refreshRecords('填写已完成,当前批次已进入待审批。'); await refreshRecords('填写已完成,当前批次已进入待审批。');
setError(''); setError('');
...@@ -460,6 +483,7 @@ export default function ForwardApprovals() { ...@@ -460,6 +483,7 @@ export default function ForwardApprovals() {
{loading ? <NoticeBanner>正在加载转发审批记录...</NoticeBanner> : null} {loading ? <NoticeBanner>正在加载转发审批记录...</NoticeBanner> : null}
{!loading && error ? <NoticeBanner tone="error">{error}</NoticeBanner> : null} {!loading && error ? <NoticeBanner tone="error">{error}</NoticeBanner> : null}
{!error && notice ? <NoticeBanner tone="success">{notice}</NoticeBanner> : null} {!error && notice ? <NoticeBanner tone="success">{notice}</NoticeBanner> : null}
{notificationWarning ? <NoticeBanner>{notificationWarning}</NoticeBanner> : null}
<div className="min-w-0 max-w-full"> <div className="min-w-0 max-w-full">
<Tabs className="w-fit max-w-full min-w-0" selectedKey={activeFilter} variant="primary" onSelectionChange={(key) => { setActiveFilter(String(key)); setPage(0); }}> <Tabs className="w-fit max-w-full min-w-0" selectedKey={activeFilter} variant="primary" onSelectionChange={(key) => { setActiveFilter(String(key)); setPage(0); }}>
......
...@@ -39,6 +39,7 @@ import { ...@@ -39,6 +39,7 @@ import {
loadModelBatch, loadModelBatch,
resolveForwardRowStatus, resolveForwardRowStatus,
saveForwardRecord, saveForwardRecord,
dispatchForwardStateNotification,
updateBatchHiddenRecords, updateBatchHiddenRecords,
} from './modelMonitorService'; } from './modelMonitorService';
...@@ -60,6 +61,7 @@ export default function ModelBatchData() { ...@@ -60,6 +61,7 @@ export default function ModelBatchData() {
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [notice, setNotice] = useState(''); const [notice, setNotice] = useState('');
const [notificationWarning, setNotificationWarning] = useState('');
const [isHiddenDialogOpen, setIsHiddenDialogOpen] = useState(false); const [isHiddenDialogOpen, setIsHiddenDialogOpen] = useState(false);
const [isForwardDialogOpen, setIsForwardDialogOpen] = useState(false); const [isForwardDialogOpen, setIsForwardDialogOpen] = useState(false);
const [forwardForm, setForwardForm] = useState(createEmptyForwardDraft); const [forwardForm, setForwardForm] = useState(createEmptyForwardDraft);
...@@ -209,6 +211,8 @@ export default function ModelBatchData() { ...@@ -209,6 +211,8 @@ export default function ModelBatchData() {
}, userInfo); }, userInfo);
const deliverRecords = selectedRecords.filter((item) => deliverIds.includes(item.recordName || item.id)); const deliverRecords = selectedRecords.filter((item) => deliverIds.includes(item.recordName || item.id));
await ensureForwardRecordItems(record, deliverRecords, userInfo); await ensureForwardRecordItems(record, deliverRecords, userInfo);
const notificationResult = await dispatchForwardStateNotification('requested', record, userInfo);
setNotificationWarning(notificationResult.warning);
await refresh(); await refresh();
setSelectedRecordIds([]); setSelectedRecordIds([]);
setIsForwardDialogOpen(false); setIsForwardDialogOpen(false);
...@@ -227,6 +231,7 @@ export default function ModelBatchData() { ...@@ -227,6 +231,7 @@ export default function ModelBatchData() {
{loading ? <NoticeBanner>正在加载批次数据和转发状态...</NoticeBanner> : null} {loading ? <NoticeBanner>正在加载批次数据和转发状态...</NoticeBanner> : null}
{error ? <NoticeBanner tone="error">{error}</NoticeBanner> : null} {error ? <NoticeBanner tone="error">{error}</NoticeBanner> : null}
{!error && notice ? <NoticeBanner tone="success">{notice}</NoticeBanner> : null} {!error && notice ? <NoticeBanner tone="success">{notice}</NoticeBanner> : null}
{notificationWarning ? <NoticeBanner>{notificationWarning}</NoticeBanner> : null}
{batch?.syncStatus === 'partial' || batch?.syncStatus === 'failed' ? <NoticeBanner tone="error">最近一次同步未完整成功:{batch.errorMessage || '该批次不可重试,请联系管理员排查。'}</NoticeBanner> : null} {batch?.syncStatus === 'partial' || batch?.syncStatus === 'failed' ? <NoticeBanner tone="error">最近一次同步未完整成功:{batch.errorMessage || '该批次不可重试,请联系管理员排查。'}</NoticeBanner> : null}
{!loading && batch ? ( {!loading && batch ? (
......
import {
createNotice,
executeAction,
loadUser,
sendNotice,
} from '@/services/DataService';
import { APP_BASE_PATH } from '@/constants/app';
export const FORWARD_NOTIFICATION_TASK_FIELDS = {
recipients: 'oa_recipient_task_ids',
firstApprover: 'oa_first_approver_task_id',
finalApprover: 'oa_final_approver_task_id',
};
const FORWARD_TARGET_PATH = `${APP_BASE_PATH}/models/forwards`;
function uniqueIds(values = []) {
return [...new Set(values.filter(Boolean).map(String))];
}
function forwardTargetUrl() {
return `${window.location.origin}${FORWARD_TARGET_PATH}`;
}
function unwrapResponse(response) {
return response?.data || response?.result || response;
}
function unpackOaResult(result) {
return result.status === 'fulfilled'
? result.value
: { taskIds: [], errors: [result.reason?.message || 'OA 待办处理失败'] };
}
function notificationAudience(record, role) {
if (role === 'recipients') return uniqueIds(record.recipientIds);
if (role === 'firstApprover') return uniqueIds([record.firstApprover]);
if (role === 'finalApprover') return uniqueIds([record.finalApprover]);
if (role === 'participants') {
return uniqueIds([
...(record.recipientIds || []),
record.initiatorId,
record.firstApprover,
record.finalApprover,
]);
}
return [];
}
function contentLink(prefix) {
return `${prefix},请打开:${forwardTargetUrl()}`;
}
async function sendInternalNotice({
title,
content,
actor,
audience,
}) {
const to = uniqueIds(audience);
if (!to.length) return null;
const notice = unwrapResponse(await createNotice({
title,
content: contentLink(content),
type: 'internal',
category: 'message',
from: actor.id,
to,
settings: { url: FORWARD_TARGET_PATH },
}));
if (!notice?.name) throw new Error('站内通知创建结果缺少 name');
await sendNotice(notice.name);
return notice;
}
async function loadOaUser(userId) {
const user = unwrapResponse(await loadUser(userId));
if (!user) throw new Error(`OA 接收人不存在:${userId}`);
return user;
}
async function createOaProgress({
title,
actor,
userId,
}) {
const receiver = await loadOaUser(userId);
const result = await executeAction('oa_notice:createProgress', {
title,
classify: '流程待办',
contentType: '流程待办',
receiveUserId: receiver._id || receiver.id || userId,
receiveUserName: receiver.username || '',
receiveDisplayName: receiver.display_name || receiver.displayName || receiver.username || '',
sendUserId: actor.id,
sendUserName: actor.username || '',
sendDisplayName: actor.name || '',
returnUrl: forwardTargetUrl(),
});
const taskId = unwrapResponse(result)?.taskId;
if (!taskId) throw new Error(`OA 待办创建结果缺少 taskId:${userId}`);
return taskId;
}
async function completeOaTasks(taskIds = []) {
const results = await Promise.allSettled(
uniqueIds(taskIds).map((taskId) => executeAction('oa_notice:updateProgressStatus', { taskId })),
);
return results
.filter((result) => result.status === 'rejected')
.map((result) => result.reason?.message || result.reason || 'OA 待办关闭失败');
}
async function createOaProgressList(context, userIds) {
const targetIds = uniqueIds(userIds);
if (!targetIds.length) return { taskIds: [], errors: [] };
const results = await Promise.allSettled(targetIds.map((userId) => createOaProgress({
...context,
userId,
})));
const taskIds = [];
const errors = [];
results.forEach((result, index) => {
if (result.status === 'fulfilled') taskIds.push(result.value);
else errors.push(result.reason?.message || `OA 待办创建失败:${targetIds[index]}`);
});
return { taskIds, errors };
}
export async function dispatchForwardNotification(event, record, user) {
const actor = {
id: user?._id || user?.id || user?.username || '',
username: user?.username || '',
name: user?.display_name || user?.displayName || user?.username || '',
};
const modelLabel = `${record.modelName || '模型'} / ${record.batchName || '批次'}`;
const context = { actor };
const errors = [];
const taskUpdates = {};
if (event === 'requested') {
const [oaResult, noticeResult] = await Promise.allSettled([
createOaProgressList({
...context,
title: `模型监控数据填写:${modelLabel}`,
}, record.recipientIds),
sendInternalNotice({
title: '模型监控数据填写',
content: `${user?.display_name || user?.username || '发起人'} 已向您分发 ${modelLabel} 的整改数据`,
actor,
audience: record.recipientIds,
}),
]);
const { taskIds, errors: oaErrors } = unpackOaResult(oaResult);
if (noticeResult.status === 'rejected') errors.push(noticeResult.reason?.message || '站内通知发送失败');
if (noticeResult.status === 'fulfilled' && Array.isArray(noticeResult.value?.errors)) errors.push(...noticeResult.value.errors);
if (oaErrors) errors.push(...oaErrors);
if (taskIds) taskUpdates[FORWARD_NOTIFICATION_TASK_FIELDS.recipients] = taskIds;
return { taskUpdates, errors };
}
if (event === 'first_approval_pending') {
const closeErrors = await completeOaTasks(record.oaRecipientTaskIds);
errors.push(...closeErrors);
const [oaResult, noticeResult] = await Promise.allSettled([
createOaProgressList({
...context,
title: `模型监控数据一级审批:${modelLabel}`,
}, [record.firstApprover]),
sendInternalNotice({
title: '模型监控数据待一级审批',
content: `${modelLabel} 已完成填写,等待您一级审批`,
actor,
audience: [record.firstApprover],
}),
]);
const { taskIds, errors: oaErrors } = unpackOaResult(oaResult);
if (noticeResult.status === 'rejected') errors.push(noticeResult.reason?.message || '站内通知发送失败');
if (oaErrors) errors.push(...oaErrors);
if (taskIds) taskUpdates[FORWARD_NOTIFICATION_TASK_FIELDS.firstApprover] = taskIds[0] || '';
return { taskUpdates, errors };
}
if (event === 'final_approval_pending') {
const closeErrors = await completeOaTasks([record.oaFirstApproverTaskId]);
errors.push(...closeErrors);
const [oaResult, noticeResult] = await Promise.allSettled([
createOaProgressList({
...context,
title: `模型监控数据终审:${modelLabel}`,
}, [record.finalApprover]),
sendInternalNotice({
title: '模型监控数据待终审',
content: `${modelLabel} 已通过一级审批,等待您终审`,
actor,
audience: [record.finalApprover],
}),
]);
const { taskIds, errors: oaErrors } = unpackOaResult(oaResult);
if (noticeResult.status === 'rejected') errors.push(noticeResult.reason?.message || '站内通知发送失败');
if (oaErrors) errors.push(...oaErrors);
if (taskIds) taskUpdates[FORWARD_NOTIFICATION_TASK_FIELDS.finalApprover] = taskIds[0] || '';
return { taskUpdates, errors };
}
if (event === 'first_rejected') {
const closeErrors = await completeOaTasks([
...record.oaRecipientTaskIds,
record.oaFirstApproverTaskId,
]);
errors.push(...closeErrors);
const [oaResult, noticeResult] = await Promise.allSettled([
createOaProgressList({
...context,
title: `模型监控数据重新填写:${modelLabel}`,
}, record.recipientIds),
sendInternalNotice({
title: '模型监控数据已驳回',
content: `${modelLabel} 一级审批已驳回,请重新填写`,
actor,
audience: uniqueIds([...record.recipientIds, record.initiatorId]),
}),
]);
const { taskIds, errors: oaErrors } = unpackOaResult(oaResult);
if (noticeResult.status === 'rejected') errors.push(noticeResult.reason?.message || '站内通知发送失败');
if (oaErrors) errors.push(...oaErrors);
if (taskIds) taskUpdates[FORWARD_NOTIFICATION_TASK_FIELDS.recipients] = taskIds;
return { taskUpdates, errors };
}
if (event === 'final_rejected') {
const closeErrors = await completeOaTasks([
...record.oaRecipientTaskIds,
record.oaFinalApproverTaskId,
]);
errors.push(...closeErrors);
const [oaResult, noticeResult] = await Promise.allSettled([
createOaProgressList({
...context,
title: `模型监控数据重新填写:${modelLabel}`,
}, record.recipientIds),
sendInternalNotice({
title: '模型监控数据终审驳回',
content: `${modelLabel} 终审已驳回,请重新填写`,
actor,
audience: uniqueIds([...record.recipientIds, record.initiatorId]),
}),
]);
const { taskIds, errors: oaErrors } = unpackOaResult(oaResult);
if (noticeResult.status === 'rejected') errors.push(noticeResult.reason?.message || '站内通知发送失败');
if (oaErrors) errors.push(...oaErrors);
if (taskIds) taskUpdates[FORWARD_NOTIFICATION_TASK_FIELDS.recipients] = taskIds;
return { taskUpdates, errors };
}
if (event === 'final_approved') {
const [closeResult, noticeResult] = await Promise.allSettled([
completeOaTasks(record.oaFinalApproverTaskId),
sendInternalNotice({
title: '模型监控流程已结束',
content: `${modelLabel} 已通过终审`,
actor,
audience: notificationAudience(record, 'participants'),
}),
]);
if (closeResult.status === 'fulfilled') errors.push(...closeResult.value);
else errors.push(closeResult.reason?.message || 'OA 待办关闭失败');
if (noticeResult.status === 'rejected') errors.push(noticeResult.reason?.message || '站内通知发送失败');
return { taskUpdates: {}, errors };
}
throw new Error(`未知的通知环节:${event}`);
}
export function formatForwardNotificationIssues(result) {
const errors = [...new Set(result?.errors || [])].filter(Boolean);
return errors.length ? `业务状态已更新,但通知处理存在异常:${errors.join(';')}` : '';
}
...@@ -132,7 +132,8 @@ MySQL 联调数据可通过 `scripts/mysql/model-monitor-source-fixtures.sql` ...@@ -132,7 +132,8 @@ MySQL 联调数据可通过 `scripts/mysql/model-monitor-source-fixtures.sql`
3. 填写接收人、转发说明、一级审批人和终审人。 3. 填写接收人、转发说明、一级审批人和终审人。
4. 创建 `model_monitor_forward_record`,并为实际下发记录创建 `model_monitor_forward_item`;发起时不写入尚未填写的是否属实、整改方案和预计整改完成时间字段。 4. 创建 `model_monitor_forward_record`,并为实际下发记录创建 `model_monitor_forward_item`;发起时不写入尚未填写的是否属实、整改方案和预计整改完成时间字段。
5. 初始化 `editor_status = editing`、`approval_status = pending`、`final_approval_status = pending`。 5. 初始化 `editor_status = editing`、`approval_status = pending`、`final_approval_status = pending`。
6. 未结束流程中的 `excluded_record_ids` 在来源批次页暂时隐藏,终审通过后恢复展示。 6. 发起成功后为每位接收人创建 OA 流程待办,并给接收人发送站内通知;接收人 OA 任务 ID 写入 `oa_recipient_task_ids`。
7. 未结束流程中的 `excluded_record_ids` 在来源批次页暂时隐藏,终审通过后恢复展示。
### 4.8 填写与两级审批 ### 4.8 填写与两级审批
...@@ -143,8 +144,11 @@ MySQL 联调数据可通过 `scripts/mysql/model-monitor-source-fixtures.sql` ...@@ -143,8 +144,11 @@ MySQL 联调数据可通过 `scripts/mysql/model-monitor-source-fixtures.sql`
5. 一级审批人通过或驳回;只有一级通过后终审人才可操作。 5. 一级审批人通过或驳回;只有一级通过后终审人才可操作。
6. 任一审批驳回后,填写状态恢复为 `editing`,清空上次完成人和完成时间,接收人可修改原回填内容。 6. 任一审批驳回后,填写状态恢复为 `editing`,清空上次完成人和完成时间,接收人可修改原回填内容。
7. 接收人重新完成填写时,`approval_status` 和 `final_approval_status` 均重置为 `pending`,流程从一级审批重新开始。 7. 接收人重新完成填写时,`approval_status` 和 `final_approval_status` 均重置为 `pending`,流程从一级审批重新开始。
8. 终审通过后状态为已最终审批,流程结束。 8. 终审通过后状态为已最终审批,流程结束;终审 OA 待办关闭,并给发起人、接收人和两级审批人发送结束通知。
9. 接收人填写、完成填写、一级审批和终审在前端同时执行按钮禁用与提交事件校验;用户身份或流程状态不满足时不发送更新请求,并提示状态已经变化或当前用户无权操作。 9. 接收人完成填写后关闭接收人 OA 待办,给一级审批人发送站内通知并创建其 OA 待办;一级通过后关闭一级 OA 待办,给终审人发送站内通知并创建其 OA 待办。
10. 一级或终审驳回后关闭当前环节 OA 待办,重新为接收人创建 OA 待办,并向接收人和发起人发送驳回站内通知。
11. 接收人填写、完成填写、一级审批和终审在前端同时执行按钮禁用与提交事件校验;用户身份或流程状态不满足时不发送更新请求,并提示状态已经变化或当前用户无权操作。
12. 通知和 OA 调用发生在业务状态成功更新之后;单个通知失败不会回滚填写、审批或转发状态,页面会显示“业务状态已更新,但通知处理存在异常”的提示,便于补发或排查。
## 5. 页面状态与异常处理 ## 5. 页面状态与异常处理
......
...@@ -17,6 +17,10 @@ import { ...@@ -17,6 +17,10 @@ import {
listSourceModels, listSourceModels,
loadSourceBatchData, loadSourceBatchData,
} from './ModelMonitorConnectorService'; } from './ModelMonitorConnectorService';
import {
dispatchForwardNotification,
formatForwardNotificationIssues,
} from './forwardNotificationService';
export const MODEL_GROUP_SUBJECT = 'model_monitor_group'; export const MODEL_GROUP_SUBJECT = 'model_monitor_group';
export const MODEL_SUBJECT = 'model_monitor_model'; export const MODEL_SUBJECT = 'model_monitor_model';
...@@ -172,6 +176,9 @@ const FIXED_SUBJECTS = [ ...@@ -172,6 +176,9 @@ const FIXED_SUBJECTS = [
{ name: 'final_approver_name', label: '终审人名称', type: 'text' }, { name: 'final_approver_name', label: '终审人名称', type: 'text' },
{ name: 'initiator_id', label: '发起人 ID', type: 'text' }, { name: 'initiator_id', label: '发起人 ID', type: 'text' },
{ name: 'initiator_name', label: '发起人名称', type: 'text' }, { name: 'initiator_name', label: '发起人名称', type: 'text' },
{ name: 'oa_recipient_task_ids', label: '接收人 OA 任务 ID', type: 'text', multiple: true },
{ name: 'oa_first_approver_task_id', label: '一级审批人 OA 任务 ID', type: 'text' },
{ name: 'oa_final_approver_task_id', label: '终审人 OA 任务 ID', type: 'text' },
], ],
}, },
{ {
...@@ -429,6 +436,11 @@ function normalizeForwardRecord(record = {}) { ...@@ -429,6 +436,11 @@ function normalizeForwardRecord(record = {}) {
finalApproverName: metadata.final_approver_name || '', finalApproverName: metadata.final_approver_name || '',
initiatorId: metadata.initiator_id || '', initiatorId: metadata.initiator_id || '',
initiatorName: metadata.initiator_name || '', initiatorName: metadata.initiator_name || '',
oaRecipientTaskIds: Array.isArray(metadata.oa_recipient_task_ids)
? metadata.oa_recipient_task_ids
: [],
oaFirstApproverTaskId: metadata.oa_first_approver_task_id || '',
oaFinalApproverTaskId: metadata.oa_final_approver_task_id || '',
createdAt: record.created_at || '', createdAt: record.created_at || '',
updatedAt: record.updated_at || '', updatedAt: record.updated_at || '',
}; };
...@@ -1141,6 +1153,9 @@ function createForwardPayload(values, user) { ...@@ -1141,6 +1153,9 @@ function createForwardPayload(values, user) {
final_approver_name: values.finalApproverName || '', final_approver_name: values.finalApproverName || '',
initiator_id: values.initiatorId || actor.id, initiator_id: values.initiatorId || actor.id,
initiator_name: values.initiatorName || actor.name, initiator_name: values.initiatorName || actor.name,
oa_recipient_task_ids: values.oaRecipientTaskIds || [],
oa_first_approver_task_id: values.oaFirstApproverTaskId || '',
oa_final_approver_task_id: values.oaFinalApproverTaskId || '',
}, },
}; };
} }
...@@ -1157,6 +1172,29 @@ export async function saveForwardRecord(values, user) { ...@@ -1157,6 +1172,29 @@ export async function saveForwardRecord(values, user) {
return normalizeForwardRecord(unwrapRecord(await createRecord(createForwardPayload(values, user)))); return normalizeForwardRecord(unwrapRecord(await createRecord(createForwardPayload(values, user))));
} }
async function saveForwardNotificationTasks(recordName, taskUpdates = {}) {
if (!Object.keys(taskUpdates).length) return normalizeForwardRecord(await loadRecord(recordName));
const current = normalizeForwardRecord(await loadRecord(recordName));
return normalizeForwardRecord(unwrapRecord(await updateRecord(recordName, createForwardPayload({
...current,
...taskUpdates,
}))));
}
async function dispatchForwardStateNotification(event, record, user) {
const result = await dispatchForwardNotification(event, record, user);
let updatedRecord = record;
try {
updatedRecord = await saveForwardNotificationTasks(record.recordName, result.taskUpdates);
} catch (saveError) {
result.errors.push(saveError?.message || 'OA 任务标识保存失败');
}
return {
record: updatedRecord,
warning: formatForwardNotificationIssues(result),
};
}
function createForwardItemPayload(values, user) { function createForwardItemPayload(values, user) {
const actor = currentUserValues(user); const actor = currentUserValues(user);
const hasFill = Boolean(values.truthStatus || values.rectificationPlan || values.expectedRectificationCompletedAt); const hasFill = Boolean(values.truthStatus || values.rectificationPlan || values.expectedRectificationCompletedAt);
...@@ -1309,3 +1347,8 @@ export async function updateForwardRecordStatus(recordName, nextMetadata = {}) { ...@@ -1309,3 +1347,8 @@ export async function updateForwardRecordStatus(recordName, nextMetadata = {}) {
}); });
return normalizeForwardRecord(unwrapRecord(await updateRecord(recordName, payload))); return normalizeForwardRecord(unwrapRecord(await updateRecord(recordName, payload)));
} }
export {
dispatchForwardStateNotification,
formatForwardNotificationIssues,
};
...@@ -285,3 +285,62 @@ Navbar 读取真实未读数;0 不显示 Badge,1-99 显示原值,100 及 ...@@ -285,3 +285,62 @@ Navbar 读取真实未读数;0 不显示 Badge,1-99 显示原值,100 及
``` ```
`:action` 与 `:methodName` 必须来自通知返回的动态 `actions` 配置,前端不得写死流程动作。 `:action` 与 `:methodName` 必须来自通知返回的动态 `actions` 配置,前端不得写死流程动作。
## 8. 模型监控流程通知
模型监控转发流程复用平台通知与已配置的 `oa_notice` Action。业务 Record 状态更新成功后才调用本节接口;接口异常不回滚业务状态,但页面必须展示通知异常摘要。
### 8.1 创建站内通知
`POST /notice`
```json
{
"title": "模型监控数据填写",
"content": "接收人已收到转发,请打开:https://example.com/model_analysis/models/forwards",
"type": "internal",
"category": "message",
"from": "发起人 _id",
"to": ["接收人 _id"],
"settings": {
"url": "/model_analysis/models/forwards"
}
}
```
### 8.2 发送站内通知
`PUT /notice/{name}/send`
请求路径中的 `name` 必须来自 `POST /notice` 返回值。`internal` 通知不可重复发送。
### 8.3 创建 OA 待办
`POST /action/execute/oa_notice:createProgress`
```json
{
"title": "模型监控数据填写:模型 / 批次",
"classify": "流程待办",
"contentType": "流程待办",
"receiveUserId": "接收人 _id",
"receiveUserName": "接收人账号",
"receiveDisplayName": "接收人显示名",
"sendUserId": "发起人 _id",
"sendUserName": "发起人账号",
"sendDisplayName": "发起人显示名",
"returnUrl": "https://example.com/model_analysis/models/forwards"
}
```
成功结果必须包含 `taskId`,并按环节保存到 `model_monitor_forward_record.metadata.oa_recipient_task_ids`、`oa_first_approver_task_id` 或 `oa_final_approver_task_id`。
### 8.4 关闭 OA 待办
`POST /action/execute/oa_notice:updateProgressStatus`
```json
{
"taskId": "待关闭的 OA 任务 ID"
}
```
...@@ -150,7 +150,7 @@ type Record = { ...@@ -150,7 +150,7 @@ type Record = {
### `model_monitor_forward_record`:模型转发审批记录 ### `model_monitor_forward_record`:模型转发审批记录
- 类型:`normal` - 类型:`normal`
- 字段数量:23 - 字段数量:26
- 说明:模型转发审批记录自动配置 - 说明:模型转发审批记录自动配置
| 字段标签 | 字段别名 | 字段类型 | 多值 | 必填 | 主键 | 关联/配置 | | 字段标签 | 字段别名 | 字段类型 | 多值 | 必填 | 主键 | 关联/配置 |
...@@ -178,6 +178,9 @@ type Record = { ...@@ -178,6 +178,9 @@ type Record = {
| 终审人名称 | `final_approver_name` | text | 否 | 否 | 否 | | | 终审人名称 | `final_approver_name` | text | 否 | 否 | 否 | |
| 发起人 ID | `initiator_id` | text | 否 | 否 | 否 | | | 发起人 ID | `initiator_id` | text | 否 | 否 | 否 | |
| 发起人名称 | `initiator_name` | text | 否 | 否 | 否 | | | 发起人名称 | `initiator_name` | text | 否 | 否 | 否 | |
| 接收人 OA 任务 ID | `oa_recipient_task_ids` | text | 是 | 否 | 否 | |
| 一级审批人 OA 任务 ID | `oa_first_approver_task_id` | text | 否 | 否 | 否 | |
| 终审人 OA 任务 ID | `oa_final_approver_task_id` | text | 否 | 否 | 否 | |
### `model_monitor_forward_item`:模型转发回填明细 ### `model_monitor_forward_item`:模型转发回填明细
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment