feat(approval): add safe risk disposition workflow

This commit is contained in:
caoxiaozhu
2026-07-16 15:34:58 +08:00
parent ee88a36baf
commit 4940ebc419
64 changed files with 6602 additions and 631 deletions

View File

@@ -58,6 +58,50 @@
</div>
</div>
<section class="risk-disposition-panel" aria-label="风险处置">
<div class="risk-disposition-head">
<div>
<span>风险处置</span>
<strong>{{ dispositionStateLabel }}</strong>
</div>
<div class="risk-disposition-badges">
<em :class="currentDisposition.adjudication">
{{ formatAdjudication(currentDisposition.adjudication) }}
</em>
<em :class="currentDisposition.lifecycleStatus">
{{ formatLifecycleStatus(currentDisposition.lifecycleStatus) }}
</em>
<em>v{{ currentDisposition.version }}</em>
</div>
</div>
<p v-if="currentDisposition.resolution" class="risk-disposition-resolution">
{{ currentDisposition.resolution }}
</p>
<template v-if="dispositionActions.length">
<textarea
v-model="dispositionNote"
:disabled="dispositionBusy"
maxlength="1000"
placeholder="填写复核依据、补充要求或解决说明(误报、补材料、豁免和解决时必填)"
></textarea>
<div class="risk-disposition-actions">
<button
v-for="action in dispositionActions"
:key="action.action"
type="button"
:class="action.tone"
:disabled="dispositionBusy"
@click="submitDispositionAction(action.action)"
>
<i :class="dispositionBusyAction === action.action ? 'mdi mdi-loading mdi-spin' : action.icon"></i>
{{ action.label }}
</button>
</div>
</template>
<p v-else class="risk-disposition-complete">{{ dispositionCompleteMessage }}</p>
<p v-if="dispositionError" class="risk-disposition-error">{{ dispositionError }}</p>
</section>
<div class="risk-evidence-grid">
<section class="risk-evidence-section">
<span class="risk-evidence-section-title">贡献分</span>
@@ -141,7 +185,11 @@
<script setup>
import { computed, ref, watch } from 'vue'
import { fetchClaimRiskObservations } from '../../services/riskObservations.js'
import {
createRiskDispositionRequestId,
executeRiskDispositionAction,
fetchClaimRiskObservations
} from '../../services/riskObservations.js'
const props = defineProps({
claimId: { type: String, default: '' }
@@ -151,6 +199,10 @@ const observations = ref([])
const loading = ref(false)
const errorMessage = ref('')
const activeObservationKey = ref('')
const dispositionNote = ref('')
const dispositionBusyAction = ref('')
const dispositionError = ref('')
const dispositionRequest = ref({ key: '', requestId: '' })
const detailRegionId = 'risk-observation-active-detail'
let loadSequence = 0
@@ -175,6 +227,92 @@ const activeObservationPosition = computed(() => {
)
return activeIndex >= 0 ? `${activeIndex + 1} / ${observations.value.length}` : '未选择'
})
const currentDisposition = computed(() => {
if (mainObservation.value?.disposition) {
return mainObservation.value.disposition
}
const feedbackStatus = String(mainObservation.value?.feedbackStatus || '').trim()
const status = String(mainObservation.value?.status || '').trim()
return {
adjudication: ['confirmed', 'false_positive'].includes(feedbackStatus)
? feedbackStatus
: 'unreviewed',
lifecycleStatus: status === 'resolved' ? 'resolved' : 'open',
version: 0,
resolution: ''
}
})
const dispositionBusy = computed(() => Boolean(dispositionBusyAction.value))
const dispositionStateLabel = computed(() => {
if (currentDisposition.value.adjudication === 'false_positive') {
return '该观察已判定为误报,不再阻断审批'
}
if (currentDisposition.value.lifecycleStatus === 'resolved') {
return '风险已完成处置,不再阻断审批'
}
if (['high', 'critical'].includes(String(mainObservation.value?.riskLevel || ''))) {
return '高风险未关闭,将阻断审批通过'
}
return '请结合证据完成人工裁决'
})
const dispositionActions = computed(() => {
const adjudication = currentDisposition.value.adjudication
const lifecycle = currentDisposition.value.lifecycleStatus
if (adjudication === 'false_positive' || lifecycle === 'resolved') {
return []
}
if (adjudication !== 'confirmed') {
const actions = [
{ action: 'confirm', label: '确认风险', icon: 'mdi mdi-shield-check', tone: 'danger' },
{ action: 'false_positive', label: '标记误报', icon: 'mdi mdi-shield-off-outline', tone: 'safe' }
]
if (lifecycle !== 'supplement_requested') {
actions.push({
action: 'request_supplement',
label: '请求补材料',
icon: 'mdi mdi-file-plus-outline',
tone: 'warning'
})
}
return actions
}
return [
{
action: 'request_supplement',
label: '请求补材料',
icon: 'mdi mdi-file-plus-outline',
tone: 'warning'
},
{
action: 'start_remediation',
label: '启动整改',
icon: 'mdi mdi-tools',
tone: 'warning'
},
{
action: 'request_waiver',
label: '申请豁免',
icon: 'mdi mdi-file-sign',
tone: 'neutral'
},
{
action: 'resolve',
label: '确认已解决',
icon: 'mdi mdi-check-decagram-outline',
tone: 'safe'
}
].filter((item) => (
item.action === 'resolve'
|| (item.action === 'request_supplement' && lifecycle !== 'supplement_requested')
|| (item.action === 'start_remediation' && lifecycle !== 'remediation_in_progress')
|| (item.action === 'request_waiver' && lifecycle !== 'waiver_requested')
))
})
const dispositionCompleteMessage = computed(() => (
currentDisposition.value.adjudication === 'false_positive'
? '该风险已标记为误报;如需更正,请由管理员通过审计流程处理。'
: '该风险已解决,处置事件会保留在追加式审计链中。'
))
const scoreItems = computed(() => {
const scores = mainObservation.value?.contributionScores || {}
return Object.entries(scores).map(([key, value]) => {
@@ -287,6 +425,50 @@ function selectObservation(item, index = -1) {
const key = observationIdentity(item, index)
if (key) {
activeObservationKey.value = key
dispositionNote.value = ''
dispositionError.value = ''
dispositionRequest.value = { key: '', requestId: '' }
}
}
async function submitDispositionAction(action) {
const observationId = String(mainObservation.value?.id || '').trim()
if (!observationId || dispositionBusy.value) {
return
}
const note = dispositionNote.value.trim()
if (['false_positive', 'request_supplement', 'request_waiver', 'resolve'].includes(action) && !note) {
dispositionError.value = '该处置动作需要填写复核依据或处理说明。'
return
}
const requestKey = [observationId, action, currentDisposition.value.version, note].join(':')
if (dispositionRequest.value.key !== requestKey) {
dispositionRequest.value = {
key: requestKey,
requestId: createRiskDispositionRequestId(action, observationId)
}
}
dispositionBusyAction.value = action
dispositionError.value = ''
try {
await executeRiskDispositionAction(observationId, {
action,
expectedVersion: currentDisposition.value.version,
requestId: dispositionRequest.value.requestId,
comment: note,
resolution: action === 'resolve' ? note : ''
})
dispositionNote.value = ''
dispositionRequest.value = { key: '', requestId: '' }
await loadObservations()
} catch (error) {
dispositionError.value = error?.message || '风险处置失败,请刷新状态后重试。'
if (error?.code === 'RISK_DISPOSITION_VERSION_CONFLICT') {
dispositionRequest.value = { key: '', requestId: '' }
await loadObservations()
}
} finally {
dispositionBusyAction.value = ''
}
}
@@ -376,6 +558,26 @@ function formatFeedbackStatus(value) {
}
return labels[String(value || '').trim()] || '未复核'
}
function formatAdjudication(value) {
const labels = {
unreviewed: '待裁决',
confirmed: '风险成立',
false_positive: '误报'
}
return labels[String(value || '').trim()] || '待裁决'
}
function formatLifecycleStatus(value) {
const labels = {
open: '待处置',
supplement_requested: '待补材料',
remediation_in_progress: '整改中',
waiver_requested: '豁免申请中',
resolved: '已解决'
}
return labels[String(value || '').trim()] || '待处置'
}
</script>
<style scoped src="../../assets/styles/components/risk-observation-evidence-card.css"></style>