feat(approval): add task workflow and waiver decisions

This commit is contained in:
caoxiaozhu
2026-07-16 16:52:12 +08:00
parent 28b834edd3
commit 242d68c36f
89 changed files with 16313 additions and 294 deletions

View File

@@ -0,0 +1,535 @@
import { apiRequest } from './api.js'
const APPROVAL_TASKS_PATH = '/approval-tasks'
const DEFAULT_PAGE_SIZE = 20
const MAX_PAGE_SIZE = 200
const DEFAULT_TIMEOUT_MS = 5000
function toObject(value) {
return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
}
function toArray(value) {
return Array.isArray(value) ? value : []
}
function toText(value) {
return String(value ?? '').trim()
}
function toNumber(value, fallback = 0) {
const number = Number(value)
return Number.isFinite(number) ? number : fallback
}
function toInteger(value, fallback = 0) {
return Math.trunc(toNumber(value, fallback))
}
function toPositiveInteger(value, fallback = 1, maximum = Number.MAX_SAFE_INTEGER) {
return Math.min(maximum, Math.max(1, toInteger(value, fallback)))
}
function toNullableObject(value) {
const object = toObject(value)
return Object.keys(object).length ? object : null
}
function uniqueTexts(value) {
return [...new Set(toArray(value).map(toText).filter(Boolean))]
}
function normalizePriorityReason(item = {}) {
return {
code: toText(item.code),
label: toText(item.label),
weight: toNumber(item.weight),
tone: toText(item.tone || 'normal') || 'normal'
}
}
export function createApprovalTaskRequestId(action = 'action', taskId = 'task') {
const normalizedAction = toText(action).replace(/[^a-z0-9_-]+/gi, '-') || 'action'
const normalizedTaskId = toText(taskId).replace(/[^a-z0-9_-]+/gi, '-') || 'task'
const suffix = typeof globalThis.crypto?.randomUUID === 'function'
? globalThis.crypto.randomUUID()
: `${Date.now()}-${Math.random().toString(16).slice(2)}`
return `approval:${normalizedAction}:${normalizedTaskId}:${suffix}`.slice(0, 120)
}
export function normalizeApprovalTask(item = {}) {
const task = toObject(item?.task || item)
if (!toText(task.id)) {
return null
}
return {
id: toText(task.id),
tenantId: toText(task.tenant_id ?? task.tenantId),
claimId: toText(task.claim_id ?? task.claimId),
expenseCaseId: toText(task.expense_case_id ?? task.expenseCaseId),
nodeInstanceId: toText(task.node_instance_id ?? task.nodeInstanceId),
nodeEntryKey: toText(task.node_entry_key ?? task.nodeEntryKey),
parentTaskId: toText(task.parent_task_id ?? task.parentTaskId),
taskKind: toText(task.task_kind ?? task.taskKind ?? 'root') || 'root',
nodeKey: toText(task.node_key ?? task.nodeKey),
nodeLabel: toText(task.node_label ?? task.nodeLabel),
nodeSequence: toInteger(task.node_sequence ?? task.nodeSequence),
sequenceOrder: toInteger(task.sequence_order ?? task.sequenceOrder),
coordinationMode: toText(task.coordination_mode ?? task.coordinationMode ?? 'single') || 'single',
ownerKind: toText(task.owner_kind ?? task.ownerKind),
ownerKey: toText(task.owner_key ?? task.ownerKey),
ownerEmployeeId: toText(task.owner_employee_id ?? task.ownerEmployeeId),
ownerName: toText(task.owner_name ?? task.ownerName),
assigneeKind: toText(task.assignee_kind ?? task.assigneeKind),
assigneeKey: toText(task.assignee_key ?? task.assigneeKey),
assigneeEmployeeId: toText(task.assignee_employee_id ?? task.assigneeEmployeeId),
assigneeName: toText(task.assignee_name ?? task.assigneeName),
delegatedBy: toText(task.delegated_by ?? task.delegatedBy),
delegationExpiresAt: toText(task.delegation_expires_at ?? task.delegationExpiresAt),
status: toText(task.status),
decision: toText(task.decision),
opinion: toText(task.opinion),
version: Math.max(0, toInteger(task.version)),
claimStatusSnapshot: toText(task.claim_status_snapshot ?? task.claimStatusSnapshot),
claimStageSnapshot: toText(task.claim_stage_snapshot ?? task.claimStageSnapshot),
enteredAt: toText(task.entered_at ?? task.enteredAt),
enteredAtSource: toText(task.entered_at_source ?? task.enteredAtSource),
activatedAt: toText(task.activated_at ?? task.activatedAt),
slaHoursSnapshot: Math.max(0, toInteger(task.sla_hours_snapshot ?? task.slaHoursSnapshot)),
dueAt: toText(task.due_at ?? task.dueAt),
completedAt: toText(task.completed_at ?? task.completedAt),
cancelledAt: toText(task.cancelled_at ?? task.cancelledAt),
escalationLevel: Math.max(0, toInteger(task.escalation_level ?? task.escalationLevel)),
escalatedAt: toText(task.escalated_at ?? task.escalatedAt),
nextEscalationAt: toText(task.next_escalation_at ?? task.nextEscalationAt),
priorityScore: Math.max(0, Math.min(100, toNumber(task.priority_score ?? task.priorityScore))),
priorityTier: toText(task.priority_tier ?? task.priorityTier ?? 'normal') || 'normal',
priorityReasons: toArray(task.priority_reasons_json ?? task.priorityReasons)
.map(normalizePriorityReason)
.filter((reason) => reason.code || reason.label),
riskLevel: toText(task.risk_level ?? task.riskLevel ?? 'low') || 'low',
openRiskCount: Math.max(0, toInteger(task.open_risk_count ?? task.openRiskCount)),
evidenceCompleteness: Math.max(
0,
Math.min(1, toNumber(task.evidence_completeness ?? task.evidenceCompleteness))
),
batchEligible: Boolean(task.batch_eligible ?? task.batchEligible),
batchBlockReasons: uniqueTexts(task.batch_block_reasons_json ?? task.batchBlockReasons),
projectionUpdatedAt: toText(task.projection_updated_at ?? task.projectionUpdatedAt),
createdAt: toText(task.created_at ?? task.createdAt),
updatedAt: toText(task.updated_at ?? task.updatedAt),
canAct: Boolean(task.can_act ?? task.canAct),
availableActions: uniqueTexts(task.available_actions ?? task.availableActions),
readOnlyReason: toText(task.read_only_reason ?? task.readOnlyReason)
}
}
export function normalizeApprovalTaskQueueItem(item = {}) {
const source = toObject(item)
const task = normalizeApprovalTask(source.task || source)
if (!task) {
return null
}
return {
task,
claim: toNullableObject(source.claim),
id: task.id,
claimId: task.claimId
}
}
export function normalizeApprovalTaskEvent(item = {}) {
const event = toObject(item)
if (!toText(event.id)) {
return null
}
return {
id: toText(event.id),
tenantId: toText(event.tenant_id ?? event.tenantId),
taskId: toText(event.task_id ?? event.taskId),
nodeInstanceId: toText(event.node_instance_id ?? event.nodeInstanceId),
eventType: toText(event.event_type ?? event.eventType),
actorId: toText(event.actor_id ?? event.actorId),
actorName: toText(event.actor_name ?? event.actorName),
actorType: toText(event.actor_type ?? event.actorType),
requestId: toText(event.request_id ?? event.requestId),
expectedTaskVersion: Math.max(
0,
toInteger(event.expected_task_version ?? event.expectedTaskVersion)
),
resultTaskVersion: Math.max(
0,
toInteger(event.result_task_version ?? event.resultTaskVersion)
),
payload: toObject(event.payload_json ?? event.payload),
before: toObject(event.before_json ?? event.before),
after: toObject(event.after_json ?? event.after),
approvalActionLedgerId: toText(
event.approval_action_ledger_id ?? event.approvalActionLedgerId
),
businessEventId: toText(event.business_event_id ?? event.businessEventId),
correlationId: toText(event.correlation_id ?? event.correlationId),
causationId: toText(event.causation_id ?? event.causationId),
occurredAt: toText(event.occurred_at ?? event.occurredAt)
}
}
export function normalizeApprovalTaskMutation(payload = {}) {
const envelope = toObject(payload)
const mutation = toObject(envelope.mutation || envelope)
return {
task: normalizeApprovalTask(mutation.task),
event: normalizeApprovalTaskEvent(mutation.event),
relatedTasks: toArray(mutation.related_tasks ?? mutation.relatedTasks)
.map(normalizeApprovalTask)
.filter(Boolean),
replayed: Boolean(mutation.replayed),
claim: toNullableObject(envelope.claim ?? mutation.claim)
}
}
export function normalizeApprovalTaskList(payload = {}) {
const source = toObject(payload)
const items = toArray(source.items).map(normalizeApprovalTaskQueueItem).filter(Boolean)
const total = Math.max(items.length, toInteger(source.total, items.length))
const pageSize = toPositiveInteger(
source.page_size ?? source.pageSize,
DEFAULT_PAGE_SIZE,
MAX_PAGE_SIZE
)
const page = toPositiveInteger(source.page, 1)
const totalPages = Math.max(
total ? 1 : 0,
toInteger(source.total_pages ?? source.totalPages, Math.ceil(total / pageSize))
)
return {
items,
total,
page,
pageSize,
totalPages,
generatedAt: toText(source.generated_at ?? source.generatedAt)
}
}
export function normalizeApprovalTaskCandidate(item = {}) {
const source = toObject(item)
const employeeId = toText(source.employee_id ?? source.employeeId)
if (!employeeId) {
return null
}
return {
employeeId,
employeeNo: toText(source.employee_no ?? source.employeeNo),
name: toText(source.name),
email: toText(source.email),
qualified: Boolean(source.qualified),
reason: toText(source.reason)
}
}
export function normalizeApprovalTaskCandidateList(payload = {}) {
const source = Array.isArray(payload) ? { items: payload } : toObject(payload)
const items = toArray(source.items).map(normalizeApprovalTaskCandidate).filter(Boolean)
return {
items,
total: Math.max(items.length, toInteger(source.total, items.length))
}
}
export function normalizeApprovalTaskBatchResult(payload = {}) {
const source = toObject(payload)
const items = toArray(source.items).map((item) => ({
taskId: toText(item?.task_id ?? item?.taskId),
claimId: toText(item?.claim_id ?? item?.claimId),
status: toText(item?.status || 'failed') || 'failed',
code: toText(item?.code),
message: toText(item?.message),
claim: toNullableObject(item?.claim)
})).filter((item) => item.taskId)
const countStatus = (status) => items.filter((item) => item.status === status).length
return {
batchRequestId: toText(source.batch_request_id ?? source.batchRequestId),
status: toText(source.status || 'failed') || 'failed',
succeededCount: Math.max(0, toInteger(source.succeeded_count ?? source.succeededCount, countStatus('succeeded'))),
replayedCount: Math.max(0, toInteger(source.replayed_count ?? source.replayedCount, countStatus('replayed'))),
conflictCount: Math.max(0, toInteger(source.conflict_count ?? source.conflictCount, countStatus('conflict'))),
blockedCount: Math.max(0, toInteger(source.blocked_count ?? source.blockedCount, countStatus('blocked'))),
forbiddenCount: Math.max(0, toInteger(source.forbidden_count ?? source.forbiddenCount, countStatus('forbidden'))),
failedCount: Math.max(0, toInteger(source.failed_count ?? source.failedCount, countStatus('failed'))),
items
}
}
function appendQueryValue(search, key, value) {
if (Array.isArray(value)) {
value.map(toText).filter(Boolean).forEach((item) => search.append(key, item))
return
}
const normalized = toText(value)
if (normalized) {
search.set(key, normalized)
}
}
function buildApprovalTaskListQuery(params = {}) {
const search = new URLSearchParams()
search.set('page', String(toPositiveInteger(params.page, 1)))
search.set(
'page_size',
String(toPositiveInteger(params.pageSize ?? params.page_size, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE))
)
appendQueryValue(search, 'status', params.status)
appendQueryValue(search, 'node_key', params.nodeKey ?? params.node_key)
appendQueryValue(search, 'task_kind', params.taskKind ?? params.task_kind)
appendQueryValue(search, 'risk_level', params.riskLevel ?? params.risk_level)
appendQueryValue(search, 'sla_state', params.slaState ?? params.sla_state)
appendQueryValue(search, 'keyword', params.keyword)
appendQueryValue(search, 'sort', params.sort)
appendQueryValue(search, 'assignee', params.assignee)
if (typeof (params.batchEligible ?? params.batch_eligible) === 'boolean') {
search.set(
'batch_eligible',
String(Boolean(params.batchEligible ?? params.batch_eligible))
)
}
return search.toString()
}
function requestOptions(options, timeoutMessage) {
return {
...options,
timeoutMs: toNumber(options?.timeoutMs, DEFAULT_TIMEOUT_MS),
timeoutMessage
}
}
function requireTaskId(taskId) {
const normalized = toText(taskId)
if (!normalized) {
throw new Error('审批任务缺少任务标识。')
}
return normalized
}
function buildBaseActionPayload(action, taskId, payload = {}) {
const expectedTaskVersion = toInteger(
payload.expectedTaskVersion ?? payload.expected_task_version
)
if (expectedTaskVersion < 1) {
throw new Error('审批动作缺少有效的任务版本。')
}
const reason = toText(payload.reason)
if (reason.length < 2) {
throw new Error('审批动作需要填写至少 2 个字的原因。')
}
return {
request_id: toText(payload.requestId ?? payload.request_id)
|| createApprovalTaskRequestId(action, taskId),
expected_task_version: expectedTaskVersion,
reason
}
}
function buildAssignmentActionPayload(action, taskId, payload) {
const targetEmployeeId = toText(payload.targetEmployeeId ?? payload.target_employee_id)
if (!targetEmployeeId) {
throw new Error('审批任务需要选择目标员工。')
}
const body = {
...buildBaseActionPayload(action, taskId, payload),
target_employee_id: targetEmployeeId
}
const expiresAt = toText(payload.expiresAt ?? payload.expires_at)
if (expiresAt) {
body.expires_at = expiresAt
}
return body
}
function buildParticipantsActionPayload(action, taskId, payload) {
const participantEmployeeIds = uniqueTexts(
payload.participantEmployeeIds ?? payload.participant_employee_ids
)
if (!participantEmployeeIds.length) {
throw new Error('审批任务需要至少选择一名参与人。')
}
return {
...buildBaseActionPayload(action, taskId, payload),
participant_employee_ids: participantEmployeeIds
}
}
async function mutateApprovalTask(taskId, path, payload, options = {}) {
const normalizedTaskId = requireTaskId(taskId)
const response = await apiRequest(
`${APPROVAL_TASKS_PATH}/${encodeURIComponent(normalizedTaskId)}/${path}`,
{
...requestOptions(options, '审批任务操作超时,请刷新任务状态后重试。'),
method: 'POST',
body: JSON.stringify(payload)
}
)
return normalizeApprovalTaskMutation(response)
}
export async function fetchApprovalTasks(params = {}, options = {}) {
const payload = await apiRequest(
`${APPROVAL_TASKS_PATH}?${buildApprovalTaskListQuery(params)}`,
requestOptions(options, '审批任务列表加载超时,请稍后重试。')
)
return normalizeApprovalTaskList(payload)
}
export async function fetchApprovalTaskDetail(taskId, options = {}) {
const normalizedTaskId = requireTaskId(taskId)
const payload = await apiRequest(
`${APPROVAL_TASKS_PATH}/${encodeURIComponent(normalizedTaskId)}`,
requestOptions(options, '审批任务详情加载超时,请稍后重试。')
)
return normalizeApprovalTaskQueueItem(payload)
}
export async function fetchApprovalTaskCandidates(taskId, params = {}, options = {}) {
const normalizedTaskId = requireTaskId(taskId)
const search = new URLSearchParams()
appendQueryValue(search, 'action', params.action)
appendQueryValue(search, 'keyword', params.keyword)
search.set('limit', String(toPositiveInteger(params.limit, 20, 100)))
const payload = await apiRequest(
`${APPROVAL_TASKS_PATH}/${encodeURIComponent(normalizedTaskId)}/candidates?${search}`,
requestOptions(options, '审批候选人加载超时,请稍后重试。')
)
return normalizeApprovalTaskCandidateList(payload)
}
export function delegateApprovalTask(taskId, payload = {}, options = {}) {
const normalizedTaskId = requireTaskId(taskId)
return mutateApprovalTask(
normalizedTaskId,
'delegate',
buildAssignmentActionPayload('delegate', normalizedTaskId, payload),
options
)
}
export function revokeApprovalTaskDelegation(taskId, payload = {}, options = {}) {
const normalizedTaskId = requireTaskId(taskId)
return mutateApprovalTask(
normalizedTaskId,
'delegation/revoke',
buildBaseActionPayload('delegation-revoke', normalizedTaskId, payload),
options
)
}
export function transferApprovalTask(taskId, payload = {}, options = {}) {
const normalizedTaskId = requireTaskId(taskId)
return mutateApprovalTask(
normalizedTaskId,
'transfer',
buildAssignmentActionPayload('transfer', normalizedTaskId, payload),
options
)
}
export function addSignApprovalTask(taskId, payload = {}, options = {}) {
const normalizedTaskId = requireTaskId(taskId)
return mutateApprovalTask(
normalizedTaskId,
'add-sign',
buildParticipantsActionPayload('add-sign', normalizedTaskId, payload),
options
)
}
export function countersignApprovalTask(taskId, payload = {}, options = {}) {
const normalizedTaskId = requireTaskId(taskId)
return mutateApprovalTask(
normalizedTaskId,
'countersign',
buildParticipantsActionPayload('countersign', normalizedTaskId, payload),
options
)
}
export function escalateApprovalTask(taskId, payload = {}, options = {}) {
const normalizedTaskId = requireTaskId(taskId)
return mutateApprovalTask(
normalizedTaskId,
'escalate',
buildBaseActionPayload('sla-escalate', normalizedTaskId, payload),
options
)
}
export function approveApprovalTask(taskId, payload = {}, options = {}) {
const normalizedTaskId = requireTaskId(taskId)
const body = buildBaseActionPayload('approve', normalizedTaskId, payload)
const opinion = toText(payload.opinion)
if (opinion) {
body.opinion = opinion
}
return mutateApprovalTask(normalizedTaskId, 'approve', body, options)
}
export function returnApprovalTask(taskId, payload = {}, options = {}) {
const normalizedTaskId = requireTaskId(taskId)
const body = {
...buildBaseActionPayload('return', normalizedTaskId, payload),
reason_codes: uniqueTexts(payload.reasonCodes ?? payload.reason_codes)
}
return mutateApprovalTask(normalizedTaskId, 'return', body, options)
}
function normalizeBatchApproveItem(item = {}) {
const source = toObject(item?.task || item)
const taskId = toText(item.task_id ?? item.taskId ?? source.id)
const expectedTaskVersion = toInteger(
item.expected_task_version ?? item.expectedTaskVersion ?? source.version
)
const expectedStatus = toText(
item.expected_status ?? item.expectedStatus
?? source.claim_status_snapshot ?? source.claimStatusSnapshot
)
const expectedApprovalStage = toText(
item.expected_approval_stage ?? item.expectedApprovalStage
?? source.claim_stage_snapshot ?? source.claimStageSnapshot
)
if (!taskId || expectedTaskVersion < 1 || !expectedStatus || !expectedApprovalStage) {
throw new Error('批量审批项缺少任务、版本、单据状态或审批节点快照。')
}
const body = {
task_id: taskId,
expected_task_version: expectedTaskVersion,
expected_status: expectedStatus,
expected_approval_stage: expectedApprovalStage
}
const opinion = toText(item.opinion)
if (opinion) {
body.opinion = opinion
}
return body
}
export async function batchApproveApprovalTasks(payload = {}, options = {}) {
const items = toArray(payload.items).map(normalizeBatchApproveItem)
if (!items.length || items.length > 20) {
throw new Error('批量审批每次需要选择 1 至 20 项任务。')
}
if (new Set(items.map((item) => item.task_id)).size !== items.length) {
throw new Error('批量审批不能包含重复任务。')
}
const batchRequestId = toText(payload.batchRequestId ?? payload.batch_request_id)
|| createApprovalTaskRequestId('batch-approve', 'batch').slice(0, 80)
const response = await apiRequest(`${APPROVAL_TASKS_PATH}/batch-approve`, {
...requestOptions(options, '批量审批超时,请刷新任务状态后查看实际结果。'),
method: 'POST',
body: JSON.stringify({
batch_request_id: batchRequestId,
items
})
})
return normalizeApprovalTaskBatchResult(response)
}

View File

@@ -13,6 +13,14 @@ function toObject(value) {
return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
}
function uniqueTexts(value, limit = Number.POSITIVE_INFINITY) {
return [...new Set(
toArray(value)
.map((item) => String(item || '').trim())
.filter(Boolean)
)].slice(0, limit)
}
export function createRiskDispositionRequestId(action, observationId) {
const normalizedAction = String(action || 'action').trim() || 'action'
const normalizedObservationId = String(observationId || 'risk').trim() || 'risk'
@@ -35,14 +43,48 @@ export function normalizeRiskDisposition(item = {}) {
assignee: String(item.assignee || '').trim(),
dueAt: String(item.due_at || item.dueAt || '').trim(),
resolution: String(item.resolution || '').trim(),
availableActions: uniqueTexts(item.available_actions ?? item.availableActions),
readOnlyReason: String(item.read_only_reason ?? item.readOnlyReason ?? '').trim(),
waiverRequesterId: String(
item.waiver_requester_id || item.waiverRequesterId || ''
).trim(),
waiverRequesterName: String(
item.waiver_requester_name || item.waiverRequesterName || ''
).trim(),
waiverRequestedAt: String(
item.waiver_requested_at || item.waiverRequestedAt || ''
).trim(),
waiverReason: String(item.waiver_reason || item.waiverReason || '').trim(),
waiverScope: String(item.waiver_scope || item.waiverScope || '').trim(),
waiverExpiresAt: String(
item.waiver_expires_at || item.waiverExpiresAt || ''
).trim(),
waiverConditions: uniqueTexts(
item.waiver_conditions_json
?? item.waiver_conditions
?? item.waiverConditions
),
waiverDecision: String(item.waiver_decision || item.waiverDecision || '').trim(),
waiverDeciderId: String(item.waiver_decider_id || item.waiverDeciderId || '').trim(),
waiverDeciderName: String(
item.waiver_decider_name || item.waiverDeciderName || ''
).trim(),
waiverDecidedAt: String(item.waiver_decided_at || item.waiverDecidedAt || '').trim(),
waiverDecisionReason: String(
item.waiver_decision_reason || item.waiverDecisionReason || ''
).trim(),
createdAt: String(item.created_at || item.createdAt || '').trim(),
updatedAt: String(item.updated_at || item.updatedAt || '').trim(),
events: toArray(item.events).map((event) => ({
id: String(event?.id || '').trim(),
action: String(event?.action || '').trim(),
version: Math.max(0, toNumber(event?.version)),
actorId: String(event?.actor_id || event?.actorId || '').trim(),
actorName: String(event?.actor_name || event?.actorName || '').trim(),
requestId: String(event?.request_id || event?.requestId || '').trim(),
comment: String(event?.comment || '').trim(),
beforeState: toObject(event?.before_json || event?.beforeState),
afterState: toObject(event?.after_json || event?.afterState),
createdAt: String(event?.created_at || event?.createdAt || '').trim()
}))
}
@@ -91,6 +133,8 @@ export function normalizeRiskObservation(item = {}) {
),
feedbackItems: toArray(item.feedback_items || item.feedbackItems),
disposition: normalizeRiskDisposition(item.disposition),
availableActions: uniqueTexts(item.available_actions ?? item.availableActions),
readOnlyReason: String(item.read_only_reason ?? item.readOnlyReason ?? '').trim(),
createdAt: String(item.created_at || item.createdAt || '').trim(),
updatedAt: String(item.updated_at || item.updatedAt || '').trim()
}
@@ -211,10 +255,23 @@ export async function executeRiskDispositionAction(observationId, payload = {})
const resolution = String(payload.resolution || '').trim()
const assignee = String(payload.assignee || '').trim()
const dueAt = String(payload.dueAt || payload.due_at || '').trim()
const waiverReason = String(payload.waiverReason || payload.waiver_reason || '').trim()
const waiverScope = String(payload.waiverScope || payload.waiver_scope || '').trim()
const waiverExpiresAt = String(
payload.waiverExpiresAt || payload.waiver_expires_at || ''
).trim()
const waiverConditions = uniqueTexts(
payload.waiverConditions ?? payload.waiver_conditions,
20
)
if (comment) body.comment = comment
if (resolution) body.resolution = resolution
if (assignee) body.assignee = assignee
if (dueAt) body.due_at = dueAt
if (waiverReason) body.waiver_reason = waiverReason
if (waiverScope) body.waiver_scope = waiverScope
if (waiverExpiresAt) body.waiver_expires_at = waiverExpiresAt
if (waiverConditions.length) body.waiver_conditions = waiverConditions
const result = await apiRequest(
`/risk-observations/${encodeURIComponent(normalizedObservationId)}/disposition/actions`,
@@ -232,3 +289,55 @@ export async function executeRiskDispositionAction(observationId, payload = {})
requestId
}
}
export function normalizeRiskDispositionError(error) {
const code = String(error?.code || '').trim()
const status = Number(error?.status || 0)
const serverMessage = String(error?.message || '').trim()
if (code === 'RISK_DISPOSITION_VERSION_CONFLICT') {
return {
code,
title: '风险状态已更新',
message: serverMessage || '其他处理人已更新该风险,请刷新后重新确认。',
shouldRefresh: true
}
}
if (code === 'RISK_WAIVER_DECISION_FORBIDDEN' || status === 403) {
return {
code: code || 'RISK_DISPOSITION_FORBIDDEN',
title: '当前账号不能执行此操作',
message: serverMessage || '服务端未授予当前风险处置动作。',
shouldRefresh: true
}
}
if (code === 'REQUEST_TIMEOUT') {
return {
code,
title: '提交结果暂未确认',
message: serverMessage || '请求已超时,请刷新状态后再决定是否重试。',
shouldRefresh: true
}
}
if (status === 409) {
return {
code: code || 'RISK_DISPOSITION_CONFLICT',
title: '当前状态不允许该操作',
message: serverMessage || '风险处置状态发生冲突,请刷新后重试。',
shouldRefresh: true
}
}
if (status === 422) {
return {
code: code || 'RISK_DISPOSITION_VALIDATION_FAILED',
title: '提交内容不完整',
message: serverMessage || '请检查必填项、有效期和输入长度。',
shouldRefresh: false
}
}
return {
code: code || 'RISK_DISPOSITION_REQUEST_FAILED',
title: '风险处置提交失败',
message: serverMessage || '请稍后重试;如持续失败,请联系系统管理员。',
shouldRefresh: false
}
}