This commit is contained in:
@@ -12,7 +12,12 @@
|
||||
<template #default="{ row }">{{ formatDateTime(row.occurred_at) }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="动作" min-width="180">
|
||||
<template #default="{ row }">{{ row.action_name || '-' }}</template>
|
||||
<template #default="{ row }">
|
||||
<ElButton v-if="detailEnabled" link type="primary" @click="$emit('detail', row)">
|
||||
{{ row.action_name || '-' }}
|
||||
</ElButton>
|
||||
<span v-else>{{ row.action_name || '-' }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="摘要" prop="summary" min-width="240" show-overflow-tooltip />
|
||||
<ElTableColumn label="操作者" min-width="150">
|
||||
@@ -30,11 +35,6 @@
|
||||
}}</ElTag></template
|
||||
>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" width="110" fixed="right">
|
||||
<template #default="{ row }"
|
||||
><ElButton link type="primary" @click="$emit('detail', row)">查看详情</ElButton></template
|
||||
>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</template>
|
||||
@@ -44,7 +44,12 @@
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { auditResultMeta, auditRiskMeta } from '@/utils/business/audit'
|
||||
|
||||
defineProps<{ items: AuditEventView[]; loading?: boolean }>()
|
||||
withDefaults(
|
||||
defineProps<{ items: AuditEventView[]; loading?: boolean; detailEnabled?: boolean }>(),
|
||||
{
|
||||
detailEnabled: true
|
||||
}
|
||||
)
|
||||
defineEmits<{ detail: [row: AuditEventView] }>()
|
||||
const resultMeta = (row: AuditEventView) =>
|
||||
auditResultMeta[row.result] || { label: row.result, type: 'info' as const }
|
||||
|
||||
@@ -21,12 +21,172 @@
|
||||
<article class="timeline-node">
|
||||
<div class="node-head">
|
||||
<strong>{{ item.action_name }}</strong>
|
||||
<ElTag size="small" :type="auditResultMeta[item.result]?.type">{{
|
||||
auditResultMeta[item.result]?.label || item.result
|
||||
}}</ElTag>
|
||||
<span class="node-tags">
|
||||
<ElTag size="small" :type="auditResultMeta[item.result]?.type">{{
|
||||
auditResultMeta[item.result]?.label || item.result
|
||||
}}</ElTag>
|
||||
<ElTag
|
||||
v-if="isFullEventTimeline"
|
||||
size="small"
|
||||
effect="plain"
|
||||
:type="auditRiskMeta[item.risk_level]?.type"
|
||||
>
|
||||
{{ auditRiskMeta[item.risk_level]?.label || item.risk_level }}风险
|
||||
</ElTag>
|
||||
</span>
|
||||
</div>
|
||||
<p>{{ item.summary || '-' }}</p>
|
||||
<div class="node-meta">
|
||||
<p v-if="!isFullEventTimeline">{{ item.summary || '-' }}</p>
|
||||
<template v-if="isFullEventTimeline">
|
||||
<ElDescriptions class="actor-event-summary" :column="4" size="small" border>
|
||||
<ElDescriptionsItem label="分类">
|
||||
{{ auditCategoryLabels[item.category] || item.category }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="来源">
|
||||
{{ auditSourceLabels[item.source] || item.source }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="操作者">
|
||||
{{ actorDisplay(item) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="业务范围">
|
||||
{{ scopeDisplay(item) }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<ElCollapse class="actor-event-sections">
|
||||
<ElCollapseItem
|
||||
v-if="userStore.isSuperAdmin && hasRequestContext(item)"
|
||||
title="请求上下文"
|
||||
name="request"
|
||||
>
|
||||
<ElDescriptions :column="1" size="small" border>
|
||||
<ElDescriptionsItem label="请求">
|
||||
{{
|
||||
[item.request_method, item.request_path].filter(Boolean).join(' ') || '-'
|
||||
}}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="request_id">{{
|
||||
item.request_id || '-'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="correlation_id">{{
|
||||
item.correlation_id || '-'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="IP 地址">{{
|
||||
item.ip_address || '-'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="User-Agent">{{
|
||||
item.user_agent || '-'
|
||||
}}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</ElCollapseItem>
|
||||
<ElCollapseItem v-if="hasBatchData(item)" title="批次执行统计" name="batch">
|
||||
<ElDescriptions :column="3" size="small" border>
|
||||
<ElDescriptionsItem label="处理总数">{{
|
||||
item.batch_total || 0
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="成功数">{{
|
||||
item.success_count || 0
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="失败数">{{
|
||||
item.fail_count || 0
|
||||
}}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</ElCollapseItem>
|
||||
<ElCollapseItem
|
||||
v-if="item.error_code || item.error_summary"
|
||||
title="错误信息"
|
||||
name="error"
|
||||
>
|
||||
<ElDescriptions :column="1" size="small" border>
|
||||
<ElDescriptionsItem label="错误码">{{
|
||||
item.error_code || '-'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="错误摘要">{{
|
||||
item.error_summary || '-'
|
||||
}}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</ElCollapseItem>
|
||||
<ElCollapseItem
|
||||
v-if="item.resources?.length"
|
||||
title="相关资源与快照"
|
||||
name="resources"
|
||||
>
|
||||
<div class="actor-resource-list">
|
||||
<section
|
||||
v-for="resource in item.resources"
|
||||
:key="resourceKey(resource)"
|
||||
class="actor-resource"
|
||||
>
|
||||
<div class="actor-resource__title">
|
||||
<strong>{{
|
||||
resource.display_name ||
|
||||
resource.resource_key ||
|
||||
resource.resource_id ||
|
||||
'-'
|
||||
}}</strong>
|
||||
<ElTag size="small" effect="plain">
|
||||
{{ auditResourceRelationLabels[resource.relation] || resource.relation }}
|
||||
</ElTag>
|
||||
</div>
|
||||
<ElDescriptions :column="2" size="small">
|
||||
<ElDescriptionsItem label="资源类型">
|
||||
{{
|
||||
auditResourceTypeLabels[resource.resource_type] ||
|
||||
resource.resource_type
|
||||
}}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="业务角色">{{
|
||||
resource.role || '-'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="资源标识">
|
||||
{{ resource.resource_key || resource.resource_id || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="主体摘要">{{
|
||||
resource.subject_summary || '-'
|
||||
}}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
<ElCollapse class="resource-json-sections">
|
||||
<ElCollapseItem
|
||||
v-if="hasJsonContent(resource.identity_snapshot)"
|
||||
title="身份快照"
|
||||
name="identity"
|
||||
>
|
||||
<pre>{{ auditJson(resource.identity_snapshot) }}</pre>
|
||||
</ElCollapseItem>
|
||||
<ElCollapseItem
|
||||
v-if="hasJsonContent(resource.subject_data)"
|
||||
title="主体安全数据"
|
||||
name="subject"
|
||||
>
|
||||
<pre>{{ auditJson(resource.subject_data) }}</pre>
|
||||
</ElCollapseItem>
|
||||
<ElCollapseItem
|
||||
v-if="hasJsonContent(resource.before_data)"
|
||||
title="变更前"
|
||||
name="before"
|
||||
>
|
||||
<pre>{{ auditJson(resource.before_data) }}</pre>
|
||||
</ElCollapseItem>
|
||||
<ElCollapseItem
|
||||
v-if="hasJsonContent(resource.after_data)"
|
||||
title="变更后"
|
||||
name="after"
|
||||
>
|
||||
<pre>{{ auditJson(resource.after_data) }}</pre>
|
||||
</ElCollapseItem>
|
||||
</ElCollapse>
|
||||
</section>
|
||||
</div>
|
||||
</ElCollapseItem>
|
||||
<ElCollapseItem
|
||||
v-if="userStore.isSuperAdmin && hasJsonContent(item.metadata)"
|
||||
title="扩展元数据"
|
||||
name="metadata"
|
||||
>
|
||||
<pre>{{ auditJson(item.metadata) }}</pre>
|
||||
</ElCollapseItem>
|
||||
</ElCollapse>
|
||||
</template>
|
||||
<div v-else class="node-meta">
|
||||
<span>{{ item.actor_name || item.actor_id || '-' }}</span>
|
||||
<span>{{ auditSourceLabels[item.source] || item.source }}</span>
|
||||
</div>
|
||||
@@ -45,16 +205,65 @@
|
||||
<article class="timeline-node">
|
||||
<div class="node-head">
|
||||
<strong>{{ node.title }}</strong>
|
||||
<ElTag size="small" effect="plain">{{
|
||||
auditResultDisplay(node.result, node.result_name)
|
||||
}}</ElTag>
|
||||
<span class="node-tags">
|
||||
<ElTag size="small" effect="plain">{{
|
||||
auditResultDisplay(node.result, node.result_name)
|
||||
}}</ElTag>
|
||||
<ElTag v-if="node.reference_only" size="small" type="info" effect="plain">
|
||||
只读引用
|
||||
</ElTag>
|
||||
</span>
|
||||
</div>
|
||||
<p>{{ node.summary || '-' }}</p>
|
||||
<div class="node-meta">
|
||||
<p v-if="!isStructuredLinkTimeline">{{ node.summary || '-' }}</p>
|
||||
<template v-if="isStructuredLinkTimeline">
|
||||
<ElDescriptions class="actor-event-summary" :column="4" size="small" border>
|
||||
<ElDescriptionsItem label="事实来源">
|
||||
{{ recordSourceLabels[node.record_source] || node.record_source }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="稳定编码">{{ node.code || '-' }}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
<ElCollapse class="actor-event-sections">
|
||||
<ElCollapseItem
|
||||
v-if="userStore.isSuperAdmin && hasJsonContent(node.fidelity)"
|
||||
title="数据完整性"
|
||||
name="fidelity"
|
||||
>
|
||||
<pre>{{ auditJson(node.fidelity) }}</pre>
|
||||
</ElCollapseItem>
|
||||
<ElCollapseItem v-if="node.resources?.length" title="相关资源" name="resources">
|
||||
<div class="actor-resource-list">
|
||||
<section
|
||||
v-for="(resource, index) in node.resources"
|
||||
:key="
|
||||
[
|
||||
resource.resource_type,
|
||||
resource.resource_id || resource.resource_key,
|
||||
index
|
||||
].join(':')
|
||||
"
|
||||
class="actor-resource"
|
||||
>
|
||||
<div class="actor-resource__title">
|
||||
<strong>{{ resource.display_name || resource.resource_key || '-' }}</strong>
|
||||
</div>
|
||||
<ElDescriptions :column="2" size="small">
|
||||
<ElDescriptionsItem label="资源类型">
|
||||
{{
|
||||
auditResourceTypeLabels[resource.resource_type] ||
|
||||
resource.resource_type
|
||||
}}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem v-if="resource.resource_key" label="资源业务 Key">
|
||||
{{ resource.resource_key }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</section>
|
||||
</div>
|
||||
</ElCollapseItem>
|
||||
</ElCollapse>
|
||||
</template>
|
||||
<div v-else class="node-meta">
|
||||
<span>{{ recordSourceLabels[node.record_source] || node.record_source }}</span>
|
||||
<ElTag v-if="node.reference_only" size="small" type="info" effect="plain">
|
||||
只读引用
|
||||
</ElTag>
|
||||
</div>
|
||||
</article>
|
||||
</ElTimelineItem>
|
||||
@@ -75,13 +284,63 @@
|
||||
{{ auditResultDisplay(item.result, item.result_name) }}
|
||||
</ElTag>
|
||||
</div>
|
||||
<p v-if="item.amount !== null && item.amount !== undefined">
|
||||
金额:{{ formatMoney(item.amount, item.currency) }}
|
||||
</p>
|
||||
<div class="node-meta">
|
||||
<span>{{ item.code || '-' }}</span>
|
||||
<span>{{ item.record_source || '-' }}</span>
|
||||
</div>
|
||||
<ElDescriptions class="actor-event-summary" :column="4" size="small" border>
|
||||
<ElDescriptionsItem
|
||||
v-if="item.amount !== null && item.amount !== undefined"
|
||||
label="金额"
|
||||
>
|
||||
{{ formatMoney(item.amount, item.currency) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem
|
||||
v-if="item.balance_before !== null && item.balance_before !== undefined"
|
||||
label="变更前余额"
|
||||
>
|
||||
{{ formatMoney(item.balance_before, item.currency) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem
|
||||
v-if="item.balance_after !== null && item.balance_after !== undefined"
|
||||
label="变更后余额"
|
||||
>
|
||||
{{ formatMoney(item.balance_after, item.currency) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="币种">{{ item.currency || '-' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="事实来源">
|
||||
{{ financeRecordSourceLabel(item.record_source) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="稳定编码">{{ item.code || '-' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="钱包类型">
|
||||
{{ financeWalletTypeLabel(item.wallet?.resource_type) }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
<ElCollapse
|
||||
v-if="item.amount_authority || hasJsonContent(item.facts)"
|
||||
class="actor-event-sections"
|
||||
>
|
||||
<ElCollapseItem v-if="item.amount_authority" title="金额权威来源" name="authority">
|
||||
<ElDescriptions :column="1" size="small" border>
|
||||
<ElDescriptionsItem label="是否权威">
|
||||
<ElTag
|
||||
size="small"
|
||||
:type="item.amount_authority.authoritative ? 'success' : 'info'"
|
||||
>
|
||||
{{ item.amount_authority.authoritative ? '是' : '否' }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem v-if="item.amount_authority.table" label="业务表">
|
||||
{{ item.amount_authority.table }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem v-if="item.amount_authority.field" label="金额字段">
|
||||
{{ item.amount_authority.field }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem v-if="item.amount_authority.conflict_rule" label="冲突规则">
|
||||
{{ item.amount_authority.conflict_rule }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</ElCollapseItem>
|
||||
<ElCollapseItem v-if="hasJsonContent(item.facts)" title="安全结构化事实" name="facts">
|
||||
<pre>{{ auditJsonWithoutIds(item.facts) }}</pre>
|
||||
</ElCollapseItem>
|
||||
</ElCollapse>
|
||||
</article>
|
||||
</ElTimelineItem>
|
||||
</ElTimeline>
|
||||
@@ -138,63 +397,176 @@
|
||||
</template>
|
||||
|
||||
<template v-else-if="integrationDetail">
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="提供方">
|
||||
{{ integrationDetail.identity.provider_name || integrationDetail.identity.provider }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="方向">
|
||||
{{ integrationDetail.identity.direction_name || integrationDetail.identity.direction }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="操作">
|
||||
{{ integrationDetail.identity.operation_name || integrationDetail.identity.operation }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="结果">
|
||||
<section class="integration-hero">
|
||||
<div>
|
||||
<span class="integration-hero__eyebrow">
|
||||
{{ integrationDetail.identity.provider_name || integrationDetail.identity.provider }}
|
||||
·
|
||||
{{
|
||||
integrationDetail.identity.direction_name || integrationDetail.identity.direction
|
||||
}}
|
||||
</span>
|
||||
<h3>{{
|
||||
integrationDetail.identity.operation_name || integrationDetail.identity.operation
|
||||
}}</h3>
|
||||
<p>{{ formatDateTime(integrationDetail.timestamps.created_at) }}</p>
|
||||
</div>
|
||||
<div class="integration-hero__result">
|
||||
<ElTag
|
||||
size="small"
|
||||
:type="integrationCategoryMeta[integrationDetail.result.category]?.type || 'info'"
|
||||
>
|
||||
{{ auditResultDisplay(integrationDetail.result.code, integrationDetail.result.name) }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="耗时">
|
||||
{{ integrationDetail.result.duration_ms }} ms
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="HTTP 状态">
|
||||
{{ integrationDetail.result.http_status ?? '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem v-if="integrationDetail.identity.external_id" label="外部业务标识">
|
||||
{{ integrationDetail.identity.external_id }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem v-if="integrationDetail.resource?.key" label="资源业务标识">
|
||||
{{ integrationDetail.resource.key }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem
|
||||
v-if="integrationDetail.result.provider_message"
|
||||
label="外部结果摘要"
|
||||
:span="2"
|
||||
>
|
||||
{{ integrationDetail.result.provider_message }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
<section
|
||||
v-if="
|
||||
hasJsonContent(integrationDetail.content.request_summary) ||
|
||||
hasJsonContent(integrationDetail.content.response_summary) ||
|
||||
hasJsonContent(integrationDetail.content.metadata)
|
||||
"
|
||||
class="json-section"
|
||||
>
|
||||
<h4>交互内容摘要</h4>
|
||||
<pre v-if="hasJsonContent(integrationDetail.content.request_summary)">{{
|
||||
auditJson(integrationDetail.content.request_summary)
|
||||
}}</pre>
|
||||
<pre v-if="hasJsonContent(integrationDetail.content.response_summary)">{{
|
||||
auditJson(integrationDetail.content.response_summary)
|
||||
}}</pre>
|
||||
<pre v-if="hasJsonContent(integrationDetail.content.metadata)">{{
|
||||
auditJson(integrationDetail.content.metadata)
|
||||
}}</pre>
|
||||
<strong>{{ integrationDetail.result.duration_ms ?? '-' }} ms</strong>
|
||||
<span>处理耗时</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="integration-detail-grid">
|
||||
<section class="integration-panel">
|
||||
<h4>交互信息</h4>
|
||||
<ElDescriptions :column="2" size="small">
|
||||
<ElDescriptionsItem label="提供方">
|
||||
{{
|
||||
integrationDetail.identity.provider_name || integrationDetail.identity.provider
|
||||
}}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="方向">
|
||||
{{
|
||||
integrationDetail.identity.direction_name || integrationDetail.identity.direction
|
||||
}}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="操作" :span="2">
|
||||
{{
|
||||
integrationDetail.identity.operation_name || integrationDetail.identity.operation
|
||||
}}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem
|
||||
v-if="integrationDetail.result.http_status != null"
|
||||
label="HTTP 状态"
|
||||
>
|
||||
{{ integrationDetail.result.http_status }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="状态是否变化">
|
||||
{{ integrationDetail.result.state_changed ? '是' : '否' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem
|
||||
v-if="integrationDetail.result.provider_message"
|
||||
label="外部结果说明"
|
||||
:span="2"
|
||||
>
|
||||
{{ integrationDetail.result.provider_message }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</section>
|
||||
|
||||
<section class="integration-panel">
|
||||
<h4>触发信息</h4>
|
||||
<ElDescriptions :column="1" size="small">
|
||||
<ElDescriptionsItem label="触发来源">
|
||||
{{ triggerSourceLabel(integrationDetail.trigger.source) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="业务场景">
|
||||
{{ triggerSceneLabel(integrationDetail.trigger.scene) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="开始时间">
|
||||
{{ formatDateTime(integrationDetail.timestamps.started_at) }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section v-if="hasVisibleIntegrationContent" class="integration-panel integration-content">
|
||||
<h4>交互内容摘要</h4>
|
||||
<div class="integration-content-grid">
|
||||
<div
|
||||
v-if="hasJsonContentWithoutIds(integrationDetail.content.request_summary)"
|
||||
class="content-block"
|
||||
>
|
||||
<span>请求摘要</span>
|
||||
<pre>{{ auditJsonWithoutIds(integrationDetail.content.request_summary) }}</pre>
|
||||
</div>
|
||||
<div
|
||||
v-if="hasJsonContentWithoutIds(integrationDetail.content.response_summary)"
|
||||
class="content-block"
|
||||
>
|
||||
<span>响应摘要</span>
|
||||
<pre>{{ auditJsonWithoutIds(integrationDetail.content.response_summary) }}</pre>
|
||||
</div>
|
||||
<div
|
||||
v-if="hasJsonContentWithoutIds(integrationDetail.content.metadata)"
|
||||
class="content-block"
|
||||
>
|
||||
<span>扩展信息</span>
|
||||
<pre>{{ auditJsonWithoutIds(integrationDetail.content.metadata) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<template v-if="integrationDetail && false">
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="提供方">
|
||||
{{ integrationDetail.identity.provider_name || integrationDetail.identity.provider }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="方向">
|
||||
{{
|
||||
integrationDetail.identity.direction_name || integrationDetail.identity.direction
|
||||
}}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="操作">
|
||||
{{
|
||||
integrationDetail.identity.operation_name || integrationDetail.identity.operation
|
||||
}}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="结果">
|
||||
<ElTag
|
||||
size="small"
|
||||
:type="integrationCategoryMeta[integrationDetail.result.category]?.type || 'info'"
|
||||
>
|
||||
{{
|
||||
auditResultDisplay(integrationDetail.result.code, integrationDetail.result.name)
|
||||
}}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="耗时">
|
||||
{{ integrationDetail.result.duration_ms }} ms
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="HTTP 状态">
|
||||
{{ integrationDetail.result.http_status ?? '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem v-if="integrationDetail.identity.external_id" label="外部业务标识">
|
||||
{{ integrationDetail.identity.external_id }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem v-if="integrationDetail.resource?.key" label="资源业务标识">
|
||||
{{ integrationDetail.resource.key }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem
|
||||
v-if="integrationDetail.result.provider_message"
|
||||
label="外部结果摘要"
|
||||
:span="2"
|
||||
>
|
||||
{{ integrationDetail.result.provider_message }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
<section
|
||||
v-if="
|
||||
hasJsonContent(integrationDetail.content.request_summary) ||
|
||||
hasJsonContent(integrationDetail.content.response_summary) ||
|
||||
hasJsonContent(integrationDetail.content.metadata)
|
||||
"
|
||||
class="json-section"
|
||||
>
|
||||
<h4>交互内容摘要</h4>
|
||||
<pre v-if="hasJsonContent(integrationDetail.content.request_summary)">{{
|
||||
auditJson(integrationDetail.content.request_summary)
|
||||
}}</pre>
|
||||
<pre v-if="hasJsonContent(integrationDetail.content.response_summary)">{{
|
||||
auditJson(integrationDetail.content.response_summary)
|
||||
}}</pre>
|
||||
<pre v-if="hasJsonContent(integrationDetail.content.metadata)">{{
|
||||
auditJson(integrationDetail.content.metadata)
|
||||
}}</pre>
|
||||
</section>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<ElEmpty v-else-if="!loading" description="当前在线窗口内没有记录" />
|
||||
@@ -226,13 +598,19 @@
|
||||
} from '@/types/api'
|
||||
import {
|
||||
auditJson,
|
||||
auditCategoryLabels,
|
||||
auditResourceRelationLabels,
|
||||
auditResourceTypeLabels,
|
||||
auditResultDisplay,
|
||||
auditResultMeta,
|
||||
auditRiskMeta,
|
||||
auditScopeTypeLabels,
|
||||
auditSourceLabels,
|
||||
integrationCategoryMeta
|
||||
} from '@/utils/business/audit'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { canLoadAuditInvestigation } from '@/utils/business/auditAccess'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import type { AuditInvestigationTarget } from './types'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -240,6 +618,7 @@
|
||||
target?: AuditInvestigationTarget | null
|
||||
}>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
@@ -268,6 +647,12 @@
|
||||
const eventDetail = ref<AuditEventDetail>()
|
||||
const integrationDetail = ref<IntegrationDetailResponse>()
|
||||
let loadSequence = 0
|
||||
const isFullEventTimeline = computed(() =>
|
||||
['actor', 'resource'].includes(props.target?.mode || '')
|
||||
)
|
||||
const isStructuredLinkTimeline = computed(() =>
|
||||
['request', 'correlation'].includes(props.target?.mode || '')
|
||||
)
|
||||
|
||||
const recordSourceLabels: Record<string, string> = {
|
||||
audit_event: '审计事件',
|
||||
@@ -276,6 +661,21 @@
|
||||
asynq_task: '异步任务引用',
|
||||
domain_ledger_ref: '业务台账引用'
|
||||
}
|
||||
const financeRecordSourceLabels: Record<string, string> = {
|
||||
audit_event: '审计事件',
|
||||
domain_ledger_ref: '业务台账引用',
|
||||
agent_wallet_transaction: '代理钱包流水',
|
||||
asset_wallet_transaction: '资产钱包流水',
|
||||
agent_wallet_reservation: '代理钱包预留',
|
||||
order: '订单',
|
||||
payment: '支付',
|
||||
refund: '退款',
|
||||
agent_recharge: '代理充值',
|
||||
recharge_order: '充值订单',
|
||||
commission_record: '佣金记录',
|
||||
commission_withdrawal: '佣金提现',
|
||||
approval_instance: '审批实例'
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
eventItems.value = []
|
||||
@@ -288,12 +688,96 @@
|
||||
}
|
||||
const hasJsonContent = (value?: Record<string, unknown> | null) =>
|
||||
Boolean(value && Object.keys(value).length)
|
||||
const auditJsonWithoutIds = (value?: Record<string, unknown> | null) => {
|
||||
const removeIds = (current: unknown): unknown => {
|
||||
if (Array.isArray(current)) return current.map(removeIds)
|
||||
if (!current || typeof current !== 'object') return current
|
||||
return Object.fromEntries(
|
||||
Object.entries(current as Record<string, unknown>)
|
||||
.filter(([key]) => !/(^|[_-])ids?$/i.test(key))
|
||||
.map(([key, nested]) => [key, removeIds(nested)])
|
||||
)
|
||||
}
|
||||
return auditJson(removeIds(value))
|
||||
}
|
||||
const withoutIds = (value?: Record<string, unknown> | null) => {
|
||||
const removeIds = (current: unknown): unknown => {
|
||||
if (Array.isArray(current)) return current.map(removeIds)
|
||||
if (!current || typeof current !== 'object') return current
|
||||
return Object.fromEntries(
|
||||
Object.entries(current as Record<string, unknown>)
|
||||
.filter(([key]) => !/(^|[_-])ids?$/i.test(key))
|
||||
.map(([key, nested]) => [key, removeIds(nested)])
|
||||
)
|
||||
}
|
||||
return removeIds(value) as Record<string, unknown> | null | undefined
|
||||
}
|
||||
const hasJsonContentWithoutIds = (value?: Record<string, unknown> | null) =>
|
||||
hasJsonContent(withoutIds(value))
|
||||
const hasVisibleIntegrationContent = computed(() =>
|
||||
Boolean(
|
||||
integrationDetail.value &&
|
||||
[
|
||||
integrationDetail.value.content.request_summary,
|
||||
integrationDetail.value.content.response_summary,
|
||||
integrationDetail.value.content.metadata
|
||||
].some(hasJsonContentWithoutIds)
|
||||
)
|
||||
)
|
||||
const triggerSourceLabel = (source?: string | null) =>
|
||||
({ polling: '定时轮询', webhook: '外部回调', manual: '人工触发' })[source || ''] ||
|
||||
source ||
|
||||
'-'
|
||||
const triggerSceneLabel = (scene?: string | null) =>
|
||||
({ network_polling: '网络状态轮询' })[scene || ''] || scene || '-'
|
||||
const financeRecordSourceLabel = (source: string) =>
|
||||
financeRecordSourceLabels[source] || source || '-'
|
||||
const financeWalletTypeLabel = (type?: 'agent_wallet' | 'asset_wallet') =>
|
||||
type === 'agent_wallet' ? '代理钱包' : type === 'asset_wallet' ? '资产钱包' : '-'
|
||||
const actorDisplay = (item: AuditEventView) => {
|
||||
const actor = item.actor_name || item.actor_id || '-'
|
||||
const organizations = [
|
||||
item.actor_shop_name || (item.actor_shop_id ? `店铺 ID: ${item.actor_shop_id}` : ''),
|
||||
item.actor_enterprise_name ||
|
||||
(item.actor_enterprise_id ? `企业 ID: ${item.actor_enterprise_id}` : '')
|
||||
].filter(Boolean)
|
||||
return organizations.length ? `${actor}(${organizations.join(' · ')})` : actor
|
||||
}
|
||||
const scopeDisplay = (item: AuditEventView) => {
|
||||
const scope = item.scope_name || item.scope_id
|
||||
const type = auditScopeTypeLabels[item.scope_type] || item.scope_type
|
||||
return scope ? `${type} · ${scope}` : type || '-'
|
||||
}
|
||||
const hasRequestContext = (item: AuditEventView) =>
|
||||
Boolean(
|
||||
item.request_method ||
|
||||
item.request_path ||
|
||||
item.request_id ||
|
||||
item.correlation_id ||
|
||||
item.ip_address ||
|
||||
item.user_agent
|
||||
)
|
||||
const hasBatchData = (item: AuditEventView) =>
|
||||
Boolean(
|
||||
(item.batch_total || 0) > 0 || (item.success_count || 0) > 0 || (item.fail_count || 0) > 0
|
||||
)
|
||||
const resourceKey = (resource: AuditEventView['resources'][number]) =>
|
||||
[resource.resource_type, resource.resource_id || resource.resource_key, resource.sort_order]
|
||||
.filter((value) => value !== null && value !== undefined && value !== '')
|
||||
.join(':')
|
||||
const formatMoney = (amount: number, currency?: string | null) => {
|
||||
const value = (amount / 100).toFixed(2)
|
||||
return currency === 'CNY' || !currency ? `¥${value}` : `${value} ${currency}`
|
||||
}
|
||||
const load = async () => {
|
||||
if (!props.target) return
|
||||
if (
|
||||
!canLoadAuditInvestigation(props.target, userStore.info.user_type, userStore.isSuperAdmin)
|
||||
) {
|
||||
clear()
|
||||
ElMessage.error('当前账号无权访问该审计调查')
|
||||
return
|
||||
}
|
||||
const sequence = ++loadSequence
|
||||
clear()
|
||||
loading.value = true
|
||||
@@ -430,10 +914,143 @@
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.node-tags {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.actor-event-summary {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.actor-event-sections {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.actor-resource-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.actor-resource {
|
||||
padding: 12px;
|
||||
background: var(--el-fill-color-lighter);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.actor-resource__title {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.resource-json-sections {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.json-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.integration-hero {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 18px 20px;
|
||||
margin-bottom: 16px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--el-color-primary-light-9),
|
||||
var(--el-fill-color-light)
|
||||
);
|
||||
border: 1px solid var(--el-color-primary-light-7);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.integration-hero__eyebrow,
|
||||
.integration-hero p,
|
||||
.integration-hero__result span,
|
||||
.content-block > span {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.integration-hero h3 {
|
||||
margin: 6px 0;
|
||||
font-size: 18px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.integration-hero p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.integration-hero__result {
|
||||
display: grid;
|
||||
justify-items: end;
|
||||
gap: 5px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.integration-hero__result strong {
|
||||
font-size: 20px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.integration-hero__result span {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.integration-detail-grid,
|
||||
.integration-content-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.integration-panel {
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.integration-panel h4 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.integration-panel :deep(.el-descriptions__label) {
|
||||
width: 76px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.integration-content {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.integration-content-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.content-block {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.content-block > span {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.json-section h4 {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
@@ -448,6 +1065,24 @@
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
@media (width <= 640px) {
|
||||
.integration-hero,
|
||||
.integration-detail-grid,
|
||||
.integration-content-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.integration-hero {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.integration-hero__result {
|
||||
justify-items: start;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<div v-if="hasLinks" class="investigation-links" aria-label="调查入口">
|
||||
<ElButton v-if="eventId" link type="primary" @click="openEvent">事件详情</ElButton>
|
||||
<ElButton v-if="actorRef" link type="primary" @click="openActor"> 操作者行为时间线 </ElButton>
|
||||
<ElButton
|
||||
v-show="showResources"
|
||||
@@ -36,7 +35,7 @@
|
||||
业务关联链路
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-for="item in stableIntegrationRefs"
|
||||
v-for="item in visibleIntegrationRefs"
|
||||
:key="item.integration_id"
|
||||
link
|
||||
type="primary"
|
||||
@@ -58,15 +57,16 @@
|
||||
import { openAuditInvestigation, openAuditResourceSearch } from './investigationController'
|
||||
import { auditResourceTypeLabels } from '@/utils/business/audit'
|
||||
import { resolveInvestigationReference } from '@/utils/business/auditNavigation'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { AUDIT_PERMISSIONS } from '@/config/constants/audit'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
refs?: AuditInvestigationRefs | null
|
||||
currentEventId?: string
|
||||
showEmpty?: boolean
|
||||
showResources?: boolean
|
||||
}>(),
|
||||
{ currentEventId: '', showEmpty: true, showResources: true }
|
||||
{ showEmpty: true, showResources: true }
|
||||
)
|
||||
const searchableTypes = new Set<AuditSearchResourceType>([
|
||||
'iot_card',
|
||||
@@ -75,12 +75,7 @@
|
||||
'order',
|
||||
'refund'
|
||||
])
|
||||
const eventId = computed(
|
||||
() =>
|
||||
resolveInvestigationReference(props.refs?.event_id, {
|
||||
currentId: props.currentEventId
|
||||
}) || ''
|
||||
)
|
||||
const { hasAuth } = useAuth()
|
||||
const requestId = computed(() => resolveInvestigationReference(props.refs?.request_id) || '')
|
||||
const correlationId = computed(
|
||||
() => resolveInvestigationReference(props.refs?.correlation_id) || ''
|
||||
@@ -111,21 +106,20 @@
|
||||
const stableIntegrationRefs = computed(() =>
|
||||
(props.refs?.integration_refs || []).filter((item) => Boolean(item.integration_id))
|
||||
)
|
||||
const visibleIntegrationRefs = computed(() =>
|
||||
hasAuth(AUDIT_PERMISSIONS.auditEventIntegrationDetail) ? stableIntegrationRefs.value : []
|
||||
)
|
||||
const hasLinks = computed(() =>
|
||||
Boolean(
|
||||
eventId.value ||
|
||||
actorRef.value ||
|
||||
actorRef.value ||
|
||||
requestId.value ||
|
||||
correlationId.value ||
|
||||
(props.showResources &&
|
||||
(stableResourceRefs.value.length || searchableResourceRefs.value.length)) ||
|
||||
stableIntegrationRefs.value.length
|
||||
visibleIntegrationRefs.value.length
|
||||
)
|
||||
)
|
||||
|
||||
const openEvent = () => {
|
||||
if (eventId.value) openAuditInvestigation({ mode: 'event', id: eventId.value })
|
||||
}
|
||||
const openIntegration = (id: string) => openAuditInvestigation({ mode: 'integration', id })
|
||||
const openTimeline = (mode: 'request' | 'correlation', id: string) =>
|
||||
openAuditInvestigation({ mode, id })
|
||||
|
||||
Reference in New Issue
Block a user