fix: 优化审计
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 3m42s

This commit is contained in:
luo
2026-08-11 16:47:58 +08:00
parent 9599a4d52b
commit 33485b137f
49 changed files with 1263 additions and 212 deletions

View File

@@ -1,4 +1,6 @@
import { BaseService } from '../BaseService'
import { useUserStore } from '@/store/modules/user'
import { isPlatformAuditAccount } from '@/utils/business/auditAccess'
import type {
AuditActorEventQuery,
AuditActorKind,
@@ -26,12 +28,28 @@ import type {
} from '@/types/api'
export class AuditService extends BaseService {
private static ensurePlatformAuditAccess() {
const userStore = useUserStore()
if (!isPlatformAuditAccount(userStore.info.user_type, userStore.isSuperAdmin)) {
throw new Error('当前账号无权访问平台审计接口')
}
}
private static ensureSubjectActivityAccess(subject: 'agent' | 'enterprise') {
const userType = Number(useUserStore().info.user_type)
if ((subject === 'agent' && userType !== 3) || (subject === 'enterprise' && userType !== 4)) {
throw new Error('当前账号无权访问主体活动接口')
}
}
static getEvents(params?: AuditEventQuery): Promise<BaseResponse<AuditEventPage>> {
this.ensurePlatformAuditAccess()
return this.get('/api/admin/audit/events', params)
}
static getEventDetail(eventId: string): Promise<BaseResponse<AuditEventDetail>> {
return this.get(`/api/admin/audit/events/${encodeURIComponent(eventId)}`)
this.ensurePlatformAuditAccess()
return this.get(`/api/admin/audit/events/${eventId}`)
}
static getActorEvents(
@@ -39,6 +57,7 @@ export class AuditService extends BaseService {
id: string,
params?: AuditActorEventQuery
): Promise<BaseResponse<AuditEventPage>> {
this.ensurePlatformAuditAccess()
return this.get(
`/api/admin/audit/actors/${encodeURIComponent(kind)}/${encodeURIComponent(id)}/events`,
params
@@ -48,6 +67,7 @@ export class AuditService extends BaseService {
static searchResources(
params: AuditResourceSearchQuery
): Promise<BaseResponse<AuditResourceSearchPage>> {
this.ensurePlatformAuditAccess()
return this.get('/api/admin/audit/resources/search', params)
}
@@ -56,6 +76,7 @@ export class AuditService extends BaseService {
resourceId: string,
params?: AuditResourceTimelineQuery
): Promise<BaseResponse<AuditEventPage>> {
this.ensurePlatformAuditAccess()
return this.get(
`/api/admin/audit/resources/${encodeURIComponent(resourceType)}/${encodeURIComponent(resourceId)}/timeline`,
params
@@ -63,40 +84,48 @@ export class AuditService extends BaseService {
}
static getRequestTimeline(requestId: string): Promise<BaseResponse<AuditLinkTimeline>> {
this.ensurePlatformAuditAccess()
return this.get(`/api/admin/audit/requests/${encodeURIComponent(requestId)}/timeline`)
}
static getCorrelationTimeline(correlationId: string): Promise<BaseResponse<AuditLinkTimeline>> {
this.ensurePlatformAuditAccess()
return this.get(`/api/admin/audit/correlations/${encodeURIComponent(correlationId)}/timeline`)
}
static getFinanceTimeline(
params: AuditFinanceQuery
): Promise<BaseResponse<AuditFinanceTimelinePage>> {
this.ensurePlatformAuditAccess()
return this.get('/api/admin/audit/finance/timeline', params)
}
static getRiskOverview(params?: AuditRiskQuery): Promise<BaseResponse<AuditRiskOverview>> {
this.ensurePlatformAuditAccess()
return this.get('/api/admin/audit/risks/overview', params)
}
static getRiskEvents(params?: AuditRiskEventQuery): Promise<BaseResponse<AuditRiskEventPage>> {
this.ensurePlatformAuditAccess()
return this.get('/api/admin/audit/risks/events', params)
}
static getIntegrationOverview(
params?: IntegrationQuery & { bucket?: 'hour' | 'day' }
): Promise<BaseResponse<IntegrationOverview>> {
this.ensurePlatformAuditAccess()
return this.get('/api/admin/audit/integrations/overview', params)
}
static getIntegrations(params?: IntegrationQuery): Promise<BaseResponse<IntegrationListPage>> {
this.ensurePlatformAuditAccess()
return this.get('/api/admin/audit/integrations', params)
}
static getIntegrationDetail(
integrationId: string
): Promise<BaseResponse<IntegrationDetailResponse>> {
this.ensurePlatformAuditAccess()
return this.get(`/api/admin/audit/integrations/${encodeURIComponent(integrationId)}`)
}
@@ -105,6 +134,7 @@ export class AuditService extends BaseService {
identifier: string,
params?: AuditSubjectActivityQuery
): Promise<BaseResponse<AuditSubjectActivityPage>> {
this.ensureSubjectActivityAccess('agent')
return this.get(
`/api/admin/agent/resource-activities/${encodeURIComponent(resourceType)}/${encodeURIComponent(identifier)}`,
params
@@ -116,6 +146,7 @@ export class AuditService extends BaseService {
identifier: string,
params?: AuditSubjectActivityQuery
): Promise<BaseResponse<AuditSubjectActivityPage>> {
this.ensureSubjectActivityAccess('enterprise')
return this.get(
`/api/admin/enterprise/resource-activities/${encodeURIComponent(resourceType)}/${encodeURIComponent(identifier)}`,
params

View File

@@ -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 }

View File

@@ -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;

View File

@@ -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 })

View File

@@ -3,6 +3,11 @@ export const AUDIT_PERMISSIONS = {
centerPage: 'audit:center_view',
eventList: 'audit:event_list',
eventDetail: 'audit:event_detail',
auditEventResourceTimeline: 'audit:audit_event_resource_timeline',
auditEventIntegrationDetail: 'audit:audit_event_integration_detail',
integrationDetailRequestTimeline: 'audit:integration_detail_request_timeline',
integrationDetailCorrelationTimeline: 'audit:integration_detail_correlation_timeline',
integrationDetailResourceTimeline: 'audit:integration_detail_resource_timeline',
actorTimeline: 'audit:actor_timeline',
resourceSearch: 'audit:resource_search',
resourceTimeline: 'audit:resource_timeline',
@@ -11,16 +16,44 @@ export const AUDIT_PERMISSIONS = {
financeTimeline: 'audit:finance_timeline',
riskPage: 'audit:risk_view',
riskEvents: 'audit:risk_events',
riskEventDetail: 'audit:risk_event_detail',
integrationPage: 'audit:integration_view',
integrationDetail: 'audit:integration_detail',
agentActivity: 'audit:agent_resource_activity',
enterpriseActivity: 'audit:enterprise_resource_activity',
agentCardActivity: 'audit:agent_card_activity',
agentDeviceActivity: 'audit:agent_device_activity',
enterpriseCardActivity: 'audit:enterprise_card_activity',
enterpriseDeviceActivity: 'audit:enterprise_device_activity',
agentExchangeActivity: 'audit:agent_exchange_activity',
cardEntry: 'audit:card_entry',
deviceEntry: 'audit:device_entry',
exchangeEntry: 'audit:exchange_entry',
exchangeOldAssetEntry: 'audit:exchange_old_asset_entry',
exchangeNewAssetEntry: 'audit:exchange_new_asset_entry',
agentAssetAllocationActivity: 'audit:agent_asset_allocation_activity',
assetAllocationEntry: 'audit:asset_allocation_entry',
assetAllocationAssetEntry: 'audit:asset_allocation_asset_entry',
assetInfoEntry: 'audit:asset_info_entry',
agentAssetInfoCardActivity: 'audit:agent_asset_info_card_activity',
agentAssetInfoDeviceActivity: 'audit:agent_asset_info_device_activity',
enterpriseAssetInfoCardActivity: 'audit:enterprise_asset_info_card_activity',
enterpriseAssetInfoDeviceActivity: 'audit:enterprise_asset_info_device_activity',
assetInfoBindingCardEntry: 'audit:asset_info_binding_card_entry',
agentAssetInfoBindingCardActivity: 'audit:agent_asset_info_binding_card_activity',
enterpriseAssetInfoBindingCardActivity: 'audit:enterprise_asset_info_binding_card_activity',
assetInfoWalletFinance: 'audit:asset_info_wallet_finance',
assetInfoWalletAsset: 'audit:asset_info_wallet_asset',
shopEntry: 'audit:shop_entry',
enterpriseEntry: 'audit:enterprise_entry',
orderEntry: 'audit:order_entry',
orderFinanceEntry: 'audit:order_finance_entry',
agentRechargeEntry: 'audit:agent_recharge_entry',
agentRechargeFinanceEntry: 'audit:agent_recharge_finance_entry',
agentRechargeApprovalEntry: 'audit:agent_recharge_approval_entry',
refundEntry: 'audit:refund_entry',
refundFinanceEntry: 'audit:refund_finance_entry',
refundApprovalEntry: 'audit:refund_approval_entry',
walletEntry: 'audit:wallet_entry'
} as const

View File

@@ -14,6 +14,7 @@ import { loadingService } from '@/utils/ui'
import { useCommon } from '@/composables/useCommon'
import { useWorktabStore } from '@/store/modules/worktab'
import { isInWhiteList, hasRoutePermission, isTokenValid, buildLoginRedirect } from './permission'
import { isPlatformAuditAccount } from '@/utils/business/auditAccess'
// 是否已注册动态路由
const isRouteRegistered = ref(false)
@@ -95,6 +96,14 @@ async function handleRouteGuard(
return
}
if (
to.path.startsWith('/audit') &&
!isPlatformAuditAccount(userStore.info.user_type, userStore.isSuperAdmin)
) {
next(RoutesAlias.Exception403 || '/exception/403')
return
}
// 处理动态路由注册
if (!isRouteRegistered.value && userStore.isLogin) {
await handleDynamicRoutes(to, router, next)

View File

@@ -51,7 +51,7 @@ export const useUserStore = defineStore(
}
// 检查是否是超级管理员
const isSuperAdmin = computed(() => info.value.user_type === 1)
const isSuperAdmin = computed(() => Number(info.value.user_type) === 1)
// 检查是否有某个权限
const hasPermission = (permission: string): boolean => {

View File

@@ -0,0 +1,14 @@
import type { AuditInvestigationTarget } from '@/components/business/audit/types'
export const isPlatformAuditAccount = (userType?: number | null, isSuperAdmin = false) =>
isSuperAdmin || [1, 2].includes(Number(userType))
export const canLoadAuditInvestigation = (
target: AuditInvestigationTarget,
userType?: number | null,
isSuperAdmin = false
) => {
if (target.mode === 'agent') return Number(userType) === 3
if (target.mode === 'enterprise') return Number(userType) === 4
return isPlatformAuditAccount(userType, isSuperAdmin)
}

View File

@@ -337,11 +337,14 @@
} from '@/config/constants'
import { AUDIT_PERMISSIONS } from '@/config/constants/audit'
import { resolveAuditResourceTarget } from '@/utils/business/auditNavigation'
import { isPlatformAuditAccount } from '@/utils/business/auditAccess'
import { openAuditInvestigation } from '@/components/business/audit/investigationController'
import { useUserStore } from '@/store/modules/user'
defineOptions({ name: 'Account' }) // 定义组件名称,用于 KeepAlive 缓存控制
const { hasAuth } = useAuth()
const userStore = useUserStore()
const canModifyAccountStatus = hasAuth('account:modify_status')
const route = useRoute()
@@ -673,8 +676,12 @@
const getActions = (row: any) => {
const actions: any[] = []
if (hasAuth(AUDIT_PERMISSIONS.resourceTimeline)) {
if (
isPlatformAuditAccount(userStore.info.user_type, userStore.isSuperAdmin) &&
hasAuth(AUDIT_PERMISSIONS.resourceTimeline)
) {
const auditTarget = resolveAuditResourceTarget({
userType: userStore.info.user_type,
resourceType: 'account',
internalId: row.id
})

View File

@@ -505,16 +505,19 @@
const getActions = (row: EnterpriseItem) => {
const actions: any[] = []
if (hasAuth(AUDIT_PERMISSIONS.enterpriseEntry)) {
const userType = Number(userStore.getUserInfo.user_type)
const auditPermission =
userType === 3 ? AUDIT_PERMISSIONS.agentActivity : AUDIT_PERMISSIONS.enterpriseEntry
if (hasAuth(auditPermission)) {
const auditTarget = resolveAuditResourceTarget({
userType: userStore.getUserInfo.user_type,
userType,
resourceType: 'enterprise',
internalId: row.id,
businessIdentifier: row.enterprise_code
})
if (auditTarget)
actions.push({
label: '审计记录',
label: userType === 3 ? '活动记录' : '审计记录',
handler: () => openAuditInvestigation(auditTarget),
type: 'primary'
})

View File

@@ -314,7 +314,7 @@
{{ scope.row.gateway_extend || '-' }}
</template>
</ElTableColumn>
<ElTableColumn label="操作" width="280" fixed="right">
<ElTableColumn label="操作" width="260" fixed="right">
<template #default="scope">
<!-- 根据网络状态显示启用或停用按钮 -->
<ElButton
@@ -357,15 +357,6 @@
>
卡审计
</ElButton>
<ElButton
v-if="getBindingAuditTarget(scope.row)"
type="primary"
size="small"
link
@click="openBindingAudit(scope.row)"
>
绑定审计
</ElButton>
</template>
</ElTableColumn>
</ElTable>
@@ -616,7 +607,6 @@
// Props
interface BindingCard {
id: number
card_id?: number
iccid: string
msisdn?: string
@@ -747,14 +737,17 @@
const isSubjectActivityView = computed(() => [3, 4].includes(Number(userStore.info.user_type)))
const auditTarget = computed(() => {
const userType = Number(userStore.info.user_type)
const isCard = props.cardInfo.asset_type === 'card'
const permission =
userType === 3
? AUDIT_PERMISSIONS.agentActivity
? isCard
? AUDIT_PERMISSIONS.agentAssetInfoCardActivity
: AUDIT_PERMISSIONS.agentAssetInfoDeviceActivity
: userType === 4
? AUDIT_PERMISSIONS.enterpriseActivity
: props.cardInfo.asset_type === 'card'
? AUDIT_PERMISSIONS.cardEntry
: AUDIT_PERMISSIONS.deviceEntry
? isCard
? AUDIT_PERMISSIONS.enterpriseAssetInfoCardActivity
: AUDIT_PERMISSIONS.enterpriseAssetInfoDeviceActivity
: AUDIT_PERMISSIONS.assetInfoEntry
if (!hasAuth(permission)) return null
return resolveAuditResourceTarget({
userType,
@@ -771,10 +764,10 @@
const userType = Number(userStore.info.user_type)
const permission =
userType === 3
? AUDIT_PERMISSIONS.agentActivity
? AUDIT_PERMISSIONS.agentAssetInfoBindingCardActivity
: userType === 4
? AUDIT_PERMISSIONS.enterpriseActivity
: AUDIT_PERMISSIONS.cardEntry
? AUDIT_PERMISSIONS.enterpriseAssetInfoBindingCardActivity
: AUDIT_PERMISSIONS.assetInfoBindingCardEntry
if (!hasAuth(permission)) return null
return resolveAuditResourceTarget({
userType,
@@ -783,25 +776,10 @@
businessIdentifier: card.iccid
})
}
const getBindingAuditTarget = (card: BindingCard) => {
if (
![1, 2].includes(Number(userStore.info.user_type)) ||
!hasAuth(AUDIT_PERMISSIONS.resourceTimeline)
)
return null
return resolveAuditResourceTarget({
resourceType: 'device_sim_binding',
internalId: card.id
})
}
const openBindingCardAudit = (card: BindingCard) => {
const target = getBindingCardAuditTarget(card)
if (target) openAuditInvestigation(target)
}
const openBindingAudit = (card: BindingCard) => {
const target = getBindingAuditTarget(card)
if (target) openAuditInvestigation(target)
}
const { width } = useWindowSize()
const CARD_MONTH_USAGE_PERMISSION = 'asset_info:view_card_month_usage'

View File

@@ -296,7 +296,7 @@
const descriptionsColumn = computed(() => {
if (width.value <= 640) return 1
if (width.value <= 1024) return 2
return 4
return 3
})
const emit = defineEmits<{

View File

@@ -45,15 +45,20 @@
<ElButton type="primary" @click="handleQuery">查询</ElButton>
<ElButton @click="handleReset()">重置</ElButton>
<ElButton
v-if="walletInfo?.wallet_id && hasAuth(AUDIT_PERMISSIONS.financeTimeline)"
v-if="
isPlatformAuditUser &&
walletInfo?.wallet_id &&
hasAuth(AUDIT_PERMISSIONS.assetInfoWalletFinance)
"
@click="openWalletAudit"
>资金链路</ElButton
>
<ElButton
v-if="
isPlatformAuditUser &&
walletInfo?.resource_type &&
walletInfo?.resource_id &&
hasAuth(AUDIT_PERMISSIONS.resourceTimeline)
hasAuth(AUDIT_PERMISSIONS.assetInfoWalletAsset)
"
@click="openAssetAudit"
>资产审计</ElButton
@@ -182,7 +187,9 @@
TransactionType
} from '@/types/api'
import { useAuth } from '@/composables/useAuth'
import { useUserStore } from '@/store/modules/user'
import { AUDIT_PERMISSIONS } from '@/config/constants/audit'
import { isPlatformAuditAccount } from '@/utils/business/auditAccess'
import {
resolveAuditResourceTarget,
resolveFinanceAuditTarget
@@ -204,6 +211,10 @@
boundDeviceNo: ''
})
const { hasAuth } = useAuth()
const userStore = useUserStore()
const isPlatformAuditUser = computed(() =>
isPlatformAuditAccount(userStore.info.user_type, userStore.isSuperAdmin)
)
const openWalletAudit = () => {
const target = resolveFinanceAuditTarget('wallet_id', props.walletInfo?.wallet_id)

View File

@@ -3161,9 +3161,9 @@
const userType = Number(userStore.getUserInfo.user_type)
const auditPermission =
userType === 3
? AUDIT_PERMISSIONS.agentActivity
? AUDIT_PERMISSIONS.agentDeviceActivity
: userType === 4
? AUDIT_PERMISSIONS.enterpriseActivity
? AUDIT_PERMISSIONS.enterpriseDeviceActivity
: AUDIT_PERMISSIONS.deviceEntry
if (hasAuth(auditPermission)) {
const auditTarget = resolveAuditResourceTarget({

View File

@@ -19,7 +19,7 @@
{{ userType === 3 ? '活动记录' : '换货审计' }}
</ElButton>
<ElButton
v-if="oldAssetAuditTarget && hasAuth(AUDIT_PERMISSIONS.resourceTimeline)"
v-if="oldAssetAuditTarget && hasAuth(AUDIT_PERMISSIONS.exchangeOldAssetEntry)"
type="primary"
plain
@click="openAuditInvestigation(oldAssetAuditTarget)"
@@ -27,7 +27,7 @@
旧资产审计
</ElButton>
<ElButton
v-if="newAssetAuditTarget && hasAuth(AUDIT_PERMISSIONS.resourceTimeline)"
v-if="newAssetAuditTarget && hasAuth(AUDIT_PERMISSIONS.exchangeNewAssetEntry)"
type="primary"
plain
@click="openAuditInvestigation(newAssetAuditTarget)"
@@ -77,10 +77,10 @@
const exchangeId = ref<number>(0)
const userType = computed(() => Number(userStore.info.user_type))
const exchangeAuditPermission = computed(() =>
userType.value === 3 ? AUDIT_PERMISSIONS.agentActivity : AUDIT_PERMISSIONS.resourceTimeline
userType.value === 3 ? AUDIT_PERMISSIONS.agentExchangeActivity : AUDIT_PERMISSIONS.exchangeEntry
)
const exchangeAuditTarget = computed(() => {
if (!exchangeDetail.value || userType.value === 4) return null
if (!exchangeDetail.value || ![1, 2, 3].includes(userType.value)) return null
return resolveAuditResourceTarget({
userType: userType.value,
resourceType: 'exchange_order',

View File

@@ -1555,8 +1555,8 @@
const flowType = row.flow_type || 'shipping' // 历史数据兼容
const userType = Number(userStore.info.user_type)
const entryPermission =
userType === 3 ? AUDIT_PERMISSIONS.agentActivity : AUDIT_PERMISSIONS.resourceTimeline
if (userType !== 4 && hasAuth(entryPermission)) {
userType === 3 ? AUDIT_PERMISSIONS.agentExchangeActivity : AUDIT_PERMISSIONS.exchangeEntry
if ([1, 2, 3].includes(userType) && hasAuth(entryPermission)) {
const exchangeTarget = resolveAuditResourceTarget({
userType,
resourceType: 'exchange_order',
@@ -1574,7 +1574,7 @@
resourceType: row.old_asset_type,
internalId: row.old_asset_id
})
if (oldAssetTarget)
if (oldAssetTarget && hasAuth(AUDIT_PERMISSIONS.exchangeOldAssetEntry))
actions.push({
label: '旧资产审计',
handler: () => openAuditInvestigation(oldAssetTarget),
@@ -1584,7 +1584,7 @@
resourceType: row.new_asset_type || '',
internalId: row.new_asset_id
})
if (newAssetTarget)
if (newAssetTarget && hasAuth(AUDIT_PERMISSIONS.exchangeNewAssetEntry))
actions.push({
label: '新资产审计',
handler: () => openAuditInvestigation(newAssetTarget),

View File

@@ -2315,9 +2315,9 @@
const userType = Number(userStore.getUserInfo.user_type)
const auditPermission =
userType === 3
? AUDIT_PERMISSIONS.agentActivity
? AUDIT_PERMISSIONS.agentCardActivity
: userType === 4
? AUDIT_PERMISSIONS.enterpriseActivity
? AUDIT_PERMISSIONS.enterpriseCardActivity
: AUDIT_PERMISSIONS.cardEntry
if (hasAuth(auditPermission)) {
const auditTarget = resolveAuditResourceTarget({

View File

@@ -19,7 +19,7 @@
{{ userType === 3 ? '活动记录' : '分配审计' }}
</ElButton>
<ElButton
v-if="assetAuditTarget && hasAuth(AUDIT_PERMISSIONS.resourceTimeline)"
v-if="assetAuditTarget && hasAuth(AUDIT_PERMISSIONS.assetAllocationAssetEntry)"
type="primary"
plain
@click="openAuditInvestigation(assetAuditTarget)"
@@ -67,10 +67,12 @@
const detailData = ref<AssetAllocationRecord | null>(null)
const userType = computed(() => Number(userStore.info.user_type))
const recordAuditPermission = computed(() =>
userType.value === 3 ? AUDIT_PERMISSIONS.agentActivity : AUDIT_PERMISSIONS.resourceTimeline
userType.value === 3
? AUDIT_PERMISSIONS.agentAssetAllocationActivity
: AUDIT_PERMISSIONS.assetAllocationEntry
)
const recordAuditTarget = computed(() => {
if (!detailData.value || userType.value === 4) return null
if (!detailData.value || ![1, 2, 3].includes(userType.value)) return null
return resolveAuditResourceTarget({
userType: userType.value,
resourceType: 'asset_allocation_record',

View File

@@ -232,8 +232,10 @@
const actions: Array<{ label: string; handler: () => void; type: 'primary' }> = []
const userType = Number(userStore.info.user_type)
const entryPermission =
userType === 3 ? AUDIT_PERMISSIONS.agentActivity : AUDIT_PERMISSIONS.resourceTimeline
if (!hasAuth(entryPermission) || userType === 4) return actions
userType === 3
? AUDIT_PERMISSIONS.agentAssetAllocationActivity
: AUDIT_PERMISSIONS.assetAllocationEntry
if (!hasAuth(entryPermission) || ![1, 2, 3].includes(userType)) return actions
const recordTarget = resolveAuditResourceTarget({
userType,
resourceType: 'asset_allocation_record',
@@ -252,7 +254,7 @@
resourceType: row.asset_type,
internalId: row.asset_id
})
if (assetTarget)
if (assetTarget && hasAuth(AUDIT_PERMISSIONS.assetAllocationAssetEntry))
actions.push({
label: '资产审计',
handler: () => openAuditInvestigation(assetTarget),

View File

@@ -96,10 +96,7 @@
</ElCard>
<ElCard shadow="never" class="section-card"
><template #header>调查引用</template
><AuditInvestigationLinks
:refs="detail.investigation_refs"
:current-event-id="detail.event_id"
:show-resources="false"
><AuditInvestigationLinks :refs="detail.investigation_refs" :show-resources="false"
/></ElCard>
<ElCard shadow="never" class="section-card">
<template #header>相关资源与快照</template>
@@ -143,6 +140,7 @@
<template #default="{ row }">
<ElButton link type="primary" @click="openSnapshotDialog(row)">查看快照</ElButton>
<ElButton
v-if="hasAuth(AUDIT_PERMISSIONS.auditEventResourceTimeline)"
link
type="primary"
:disabled="!row.resource_id"
@@ -217,6 +215,8 @@
} from '@/utils/business/audit'
import { formatDateTime } from '@/utils/business/format'
import { useUserStore } from '@/store/modules/user'
import { useAuth } from '@/composables/useAuth'
import { AUDIT_PERMISSIONS } from '@/config/constants/audit'
import AuditInvestigationLinks from '@/components/business/audit/AuditInvestigationLinks.vue'
import { openAuditInvestigation } from '@/components/business/audit/investigationController'
@@ -224,6 +224,7 @@
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const { hasAuth } = useAuth()
const loading = ref(false)
const detail = ref<AuditEventDetail>()
const snapshotDialogVisible = ref(false)

View File

@@ -171,8 +171,7 @@
{ label: '操作者', prop: 'actor_name' },
{ label: '来源', prop: 'source' },
{ label: '结果', prop: 'result' },
{ label: '风险', prop: 'risk_level' },
{ label: '操作', prop: 'actions' }
{ label: '风险', prop: 'risk_level' }
]
const { columnChecks, columns } = useCheckedColumns(() => [
{
@@ -185,7 +184,12 @@
prop: 'action_name',
label: '动作',
minWidth: 220,
formatter: (row: AuditEventView) => row.action_name || '-'
formatter: (row: AuditEventView) =>
h(
ElButton,
{ type: 'primary', link: true, onClick: () => openDetail(row) },
() => row.action_name || '-'
)
},
{ prop: 'summary', label: '摘要', minWidth: 240, showOverflowTooltip: true },
{
@@ -234,18 +238,6 @@
{ type: auditRiskMeta[row.risk_level]?.type || 'info', effect: 'plain' },
() => auditRiskMeta[row.risk_level]?.label || row.risk_level
)
},
{
prop: 'actions',
label: '操作',
width: 110,
fixed: 'right',
formatter: (row: AuditEventView) =>
h(
ElButton,
{ type: 'primary', link: true, onClick: () => openDetail(row) },
() => '查看详情'
)
}
])

View File

@@ -94,28 +94,28 @@
<template #header>调查入口</template>
<div v-if="hasAssociations" class="link-actions">
<ElButton
v-if="requestReference"
v-if="canViewRequestTimeline"
type="primary"
plain
@click="openAuditInvestigation({ mode: 'request', id: requestReference })"
@click="openAuditInvestigation({ mode: 'request', id: requestReference || '' })"
>
请求链路
</ElButton>
<ElButton
v-if="correlationReference"
v-if="canViewCorrelationTimeline"
type="primary"
plain
@click="
openAuditInvestigation({
mode: 'correlation',
id: correlationReference
id: correlationReference || ''
})
"
>
业务关联链路
</ElButton>
<ElButton
v-if="detail.resource?.type && resourceReference"
v-if="canViewResourceTimeline"
type="primary"
plain
@click="openResourceTimeline"
@@ -257,11 +257,14 @@
import { formatDateTime } from '@/utils/business/format'
import { openAuditInvestigation } from '@/components/business/audit/investigationController'
import { resolveInvestigationReference } from '@/utils/business/auditNavigation'
import { useAuth } from '@/composables/useAuth'
import { AUDIT_PERMISSIONS } from '@/config/constants/audit'
defineOptions({ name: 'AuditIntegrationDetail' })
const route = useRoute()
const router = useRouter()
const { hasAuth } = useAuth()
const loading = ref(false)
const detail = ref<IntegrationDetailResponse>()
@@ -284,11 +287,25 @@
fidelity: detail.value?.fidelity.resource_id_available
})
)
const canViewRequestTimeline = computed(
() =>
Boolean(requestReference.value) && hasAuth(AUDIT_PERMISSIONS.integrationDetailRequestTimeline)
)
const canViewCorrelationTimeline = computed(
() =>
Boolean(correlationReference.value) &&
hasAuth(AUDIT_PERMISSIONS.integrationDetailCorrelationTimeline)
)
const canViewResourceTimeline = computed(
() =>
Boolean(detail.value?.resource?.type && resourceReference.value) &&
hasAuth(AUDIT_PERMISSIONS.integrationDetailResourceTimeline)
)
const hasAssociations = computed(() =>
Boolean(
requestReference.value ||
correlationReference.value ||
(detail.value?.resource?.type && resourceReference.value)
canViewRequestTimeline.value ||
canViewCorrelationTimeline.value ||
canViewResourceTimeline.value
)
)
const hasJsonContent = (value?: Record<string, unknown> | null) =>

View File

@@ -180,9 +180,16 @@
><ElTableColumn label="方向" prop="direction_name" width="90" /><ElTableColumn
label="操作"
min-width="190"
><template #default="{ row }">{{
row.operation_name || row.operation
}}</template></ElTableColumn
><template #default="{ row }"
><ElButton
link
type="primary"
@click="
router.push(`/audit/integrations/${encodeURIComponent(row.integration_id)}`)
"
>{{ row.operation_name || row.operation || '-' }}</ElButton
></template
></ElTableColumn
><ElTableColumn label="结果" width="120"
><template #default="{ row }"
><ElTag :type="categoryMeta(row).type">{{
@@ -197,17 +204,6 @@
><template #default="{ row }">{{
row.resource?.key || row.resource?.id || '-'
}}</template></ElTableColumn
><ElTableColumn label="操作" width="100" fixed="right"
><template #default="{ row }"
><ElButton
link
type="primary"
@click="
router.push(`/audit/integrations/${encodeURIComponent(row.integration_id)}`)
"
>查看详情</ElButton
></template
></ElTableColumn
></template
></ArtTable
></ElCard

View File

@@ -137,6 +137,7 @@
><div class="risk-event-table">
<AuditEventTable
:items="events"
:detail-enabled="hasAuth(AUDIT_PERMISSIONS.riskEventDetail)"
@detail="(row) => router.push(`/audit/events/${encodeURIComponent(row.event_id)}`)"
/>
</div>
@@ -167,6 +168,8 @@
WarningFilled
} from '@element-plus/icons-vue'
import { AuditService } from '@/api/modules'
import { useAuth } from '@/composables/useAuth'
import { AUDIT_PERMISSIONS } from '@/config/constants/audit'
import type {
AuditEventView,
AuditNamedCount,
@@ -178,6 +181,7 @@
defineOptions({ name: 'AuditRisks' })
const router = useRouter()
const { hasAuth } = useAuth()
const loading = ref(false)
const headerColumns = ref([])
const dateRange = ref<string[]>([])

View File

@@ -11,7 +11,7 @@
</ElButton>
<h2 class="detail-title">{{ pageTitle }}</h2>
<ElButton
v-if="detailData && isPlatformUser && hasAuth(AUDIT_PERMISSIONS.resourceTimeline)"
v-if="detailData && isPlatformUser && hasAuth(AUDIT_PERMISSIONS.agentRechargeEntry)"
type="primary"
plain
@click="openRechargeAudit"
@@ -19,7 +19,9 @@
审计记录
</ElButton>
<ElButton
v-if="detailData && isPlatformUser && hasAuth(AUDIT_PERMISSIONS.financeTimeline)"
v-if="
detailData && isPlatformUser && hasAuth(AUDIT_PERMISSIONS.agentRechargeFinanceEntry)
"
type="primary"
plain
@click="openRechargeFinance"
@@ -30,7 +32,7 @@
v-if="
detailData?.approval_instance_id &&
isPlatformUser &&
hasAuth(AUDIT_PERMISSIONS.resourceTimeline)
hasAuth(AUDIT_PERMISSIONS.agentRechargeApprovalEntry)
"
type="primary"
plain

View File

@@ -777,7 +777,7 @@
{
prop: 'payment_method',
label: '支付方式',
width: 120,
width: 140,
formatter: (row: AgentRecharge) => getPaymentMethodText(row.payment_method)
},
{
@@ -1316,7 +1316,7 @@
onConfirmPayment: handleShowConfirmPay,
onReject: handleShowReject
})
if (isPlatformAccount.value && hasAuth(AUDIT_PERMISSIONS.resourceTimeline)) {
if (isPlatformAccount.value && hasAuth(AUDIT_PERMISSIONS.agentRechargeEntry)) {
const resourceTarget = resolveAuditResourceTarget({
resourceType: 'agent_recharge',
internalId: row.id
@@ -1328,7 +1328,7 @@
type: 'primary'
})
}
if (isPlatformAccount.value && hasAuth(AUDIT_PERMISSIONS.financeTimeline)) {
if (isPlatformAccount.value && hasAuth(AUDIT_PERMISSIONS.agentRechargeFinanceEntry)) {
const financeTarget = resolveFinanceAuditTarget('recharge_id', row.id)
if (financeTarget)
actions.unshift({
@@ -1337,7 +1337,7 @@
type: 'primary'
})
}
if (isPlatformAccount.value && hasAuth(AUDIT_PERMISSIONS.resourceTimeline)) {
if (isPlatformAccount.value && hasAuth(AUDIT_PERMISSIONS.agentRechargeApprovalEntry)) {
const approvalTarget = resolveAuditResourceTarget({
resourceType: 'approval_instance',
internalId: row.approval_instance_id

View File

@@ -19,7 +19,7 @@
审计记录
</ElButton>
<ElButton
v-if="refund && isPlatformUser && hasAuth(AUDIT_PERMISSIONS.financeTimeline)"
v-if="refund && isPlatformUser && hasAuth(AUDIT_PERMISSIONS.refundFinanceEntry)"
type="primary"
plain
@click="openRefundFinance"
@@ -30,7 +30,7 @@
v-if="
refund?.approval_instance_id &&
isPlatformUser &&
hasAuth(AUDIT_PERMISSIONS.resourceTimeline)
hasAuth(AUDIT_PERMISSIONS.refundApprovalEntry)
"
type="primary"
plain

View File

@@ -754,7 +754,7 @@
handler: () => openAuditInvestigation(auditTarget),
type: 'primary'
})
if (hasAuth(AUDIT_PERMISSIONS.financeTimeline)) {
if (hasAuth(AUDIT_PERMISSIONS.refundFinanceEntry)) {
const financeTarget = resolveFinanceAuditTarget('refund_id', row.id)
if (financeTarget)
actions.push({
@@ -768,7 +768,7 @@
resourceType: 'approval_instance',
internalId: row.approval_instance_id
})
if (approvalTarget)
if (approvalTarget && hasAuth(AUDIT_PERMISSIONS.refundApprovalEntry))
actions.push({
label: '审批审计',
handler: () => openAuditInvestigation(approvalTarget),

View File

@@ -19,7 +19,7 @@
审计记录
</ElButton>
<ElButton
v-if="detailData && isPlatformUser && hasAuth(AUDIT_PERMISSIONS.financeTimeline)"
v-if="detailData && isPlatformUser && hasAuth(AUDIT_PERMISSIONS.orderFinanceEntry)"
type="primary"
plain
@click="openOrderFinance"

View File

@@ -1372,7 +1372,7 @@
handler: () => openAuditInvestigation(auditTarget),
type: 'primary'
})
if (hasAuth(AUDIT_PERMISSIONS.financeTimeline)) {
if (hasAuth(AUDIT_PERMISSIONS.orderFinanceEntry)) {
const financeTarget = resolveFinanceAuditTarget('order_id', row.id)
if (financeTarget)
actions.push({

View File

@@ -815,16 +815,19 @@
const getActions = (row: ShopResponse) => {
const actions: any[] = []
if (hasAuth(AUDIT_PERMISSIONS.shopEntry)) {
const userType = Number(userStore.getUserInfo.user_type)
const auditPermission =
userType === 3 ? AUDIT_PERMISSIONS.agentActivity : AUDIT_PERMISSIONS.shopEntry
if (hasAuth(auditPermission)) {
const auditTarget = resolveAuditResourceTarget({
userType: userStore.getUserInfo.user_type,
userType,
resourceType: 'shop',
internalId: row.id,
businessIdentifier: row.shop_code
})
if (auditTarget)
actions.push({
label: '审计记录',
label: userType === 3 ? '活动记录' : '审计记录',
handler: () => openAuditInvestigation(auditTarget),
type: 'primary'
})