{
+ return [
+ {
+ title: '时间 / 编号',
+ dataIndex: 'created_at',
+ width: 180,
+ fixed: 'left',
+ render: (_, row) => (
+
+ {formatDateTime(row.created_at)}
+ #{row.id}
+
+ ),
+ },
+ {
+ title: '操作人',
+ key: 'actor',
+ width: 210,
+ render: (_, row) => (
+
+
+
+ {actorTypeLabel(row.actor_type)}
+ {row.actor_username ? ` · ${row.actor_username}` : ''}
+
+ ID {row.actor_id}
+
+ ),
+ },
+ {
+ title: '动作',
+ dataIndex: 'action',
+ width: 230,
+ render: (_, row) => (
+
+ {auditActionMap[row.action] ?? row.action}
+ {row.action}
+
+ ),
+ },
+ {
+ title: '业务对象',
+ key: 'biz',
+ width: 150,
+ render: (_, row) => (
+
+ {bizTypeLabel(row.biz_type)}
+ {row.biz_id != null ? `#${row.biz_id}` : '无关联 ID'}
+
+ ),
+ },
+ {
+ title: 'IP',
+ dataIndex: 'ip',
+ width: 140,
+ render: value => {formatText(value)},
+ },
+ {
+ title: '变更摘要',
+ key: 'summary',
+ width: 320,
+ render: (_, row) => (
+
+ {summarizeDetail(row.detail)}
+
+ ),
+ },
+ {
+ title: '操作',
+ key: 'actions',
+ width: 92,
+ fixed: 'right',
+ render: (_, row) => (
+ } onClick={() => onOpen(row)}>
+ 详情
+
+ ),
+ },
+ ]
+}
+
+function buildSections(row: AdminAuditLog): DetailSection[] {
+ return [
+ {
+ key: 'actor',
+ title: '操作信息',
+ icon: ,
+ fields: [
+ {
+ key: 'time',
+ label: '发生时间',
+ value: formatDateTime(row.created_at),
+ },
+ {
+ key: 'log_id',
+ label: '日志编号',
+ value: #{row.id},
+ },
+ {
+ key: 'actor',
+ label: '操作人',
+ value: (
+
+ {actorName(row)}
+ {actorTypeLabel(row.actor_type)}
+
+ ),
+ },
+ {
+ key: 'actor_id',
+ label: '操作人 ID',
+ value: {row.actor_id},
+ },
+ {
+ key: 'action',
+ label: '动作',
+ value: (
+
+ {auditActionMap[row.action] ?? row.action}
+ {row.action}
+
+ ),
+ },
+ {
+ key: 'biz',
+ label: '业务对象',
+ value: (
+
+ {bizTypeLabel(row.biz_type)}
+ {row.biz_id != null ? `#${row.biz_id}` : '—'}
+
+ ),
+ },
+ ],
+ },
+ {
+ key: 'request',
+ title: '请求信息',
+ fields: [
+ {
+ key: 'ip',
+ label: '来源 IP',
+ value: {formatText(row.ip)},
+ },
+ {
+ key: 'user_agent',
+ label: 'User-Agent',
+ span: 2,
+ value: {formatText(row.user_agent)},
+ },
+ ],
+ },
+ {
+ key: 'detail',
+ title: '变更明细',
+ fields: [
+ {
+ key: 'json',
+ label: '原始 JSON',
+ span: 2,
+ value: {normalizeDetail(row.detail)},
+ },
+ ],
+ },
+ ]
+}
+
+export function AdminAuditLogsPage() {
+ const [draft, setDraft] = useState(emptyFilters)
+ const [applied, setApplied] = useState(emptyFilters)
+ const [page, setPage] = useState(1)
+ const [pageSize, setPageSize] = useState(20)
+ const [selected, setSelected] = useState(null)
+ const [detailOpen, setDetailOpen] = useState(false)
+ const permissions = useAdminSessionStore(state => state.admin?.permissions)
+ const canView = Boolean(
+ permissions?.includes('*') || permissions?.includes('audit_log:view')
+ )
+
+ const { data, isPending, isFetching, isError, error, refetch } = useQuery({
+ queryKey: ['admin-audit-logs', applied, page, pageSize],
+ queryFn: () => fetchAdminAuditLogs({ ...applied, page, page_size: pageSize }),
+ placeholderData: keepPreviousData,
+ enabled: canView,
+ })
+
+ const columns = useMemo(
+ () =>
+ buildColumns(row => {
+ setSelected(row)
+ setDetailOpen(true)
+ }),
+ []
+ )
+
+ const patchFilter = (patch: Partial) => {
+ setDraft(current => ({ ...current, ...patch }))
+ }
+
+ const applyFilters = () => {
+ setApplied({ ...draft })
+ setPage(1)
+ }
+
+ const resetFilters = () => {
+ setDraft(emptyFilters)
+ setApplied(emptyFilters)
+ setPage(1)
+ }
+
+ const hasActiveFilters = Boolean(draft.actor_id || draft.action || draft.biz_type)
+ const errorText = error instanceof Error ? error.message : '请稍后重试或重新登录后台。'
+
+ return (
+
+
+ 共 {data?.total ?? 0} 条
+ {isFetching && !isPending ? 正在更新 : null}
+ 只读
+
+ }
+ extra={
+ }
+ onClick={() => void refetch()}
+ loading={isFetching}
+ disabled={!canView}
+ >
+ 刷新数据
+
+ }
+ />
+
+ {!canView ? (
+
+ ) : null}
+
+
+
+
+ patchFilter({ actor_id: typeof value === 'number' ? value : undefined })
+ }
+ />
+
+
+
+ String(option?.value ?? '').includes(input) ||
+ String(option?.label ?? '').includes(input)
+ }
+ onChange={value => patchFilter({ action: value ?? '' })}
+ />
+
+
+
+ String(option?.value ?? '').includes(input) ||
+ String(option?.label ?? '').includes(input)
+ }
+ onChange={value => patchFilter({ biz_type: value ?? '' })}
+ />
+
+
+
+ {isError ? (
+ void refetch()}>
+ 重试
+
+ }
+ />
+ ) : null}
+
+
+ title={操作记录}
+ extra={
+
+ {data ? `第 ${data.page} 页 · 当前 ${data.items.length} 条` : '加载中'}
+
+ }
+ rowKey="id"
+ columns={columns}
+ dataSource={data?.items ?? []}
+ loading={isFetching}
+ total={data?.total ?? 0}
+ page={data?.page ?? page}
+ pageSize={data?.page_size ?? pageSize}
+ onPageChange={(nextPage, nextPageSize) => {
+ setPage(nextPageSize !== pageSize ? 1 : nextPage)
+ setPageSize(nextPageSize)
+ }}
+ onRowClick={row => {
+ setSelected(row)
+ setDetailOpen(true)
+ }}
+ emptyDescription={canView ? '没有匹配的审计记录' : '当前账号没有查看权限'}
+ />
+
+ setDetailOpen(false)}
+ title={selected ? auditActionMap[selected.action] ?? selected.action : '审计详情'}
+ subtitle={
+ selected
+ ? `#${selected.id} · ${formatDateTime(selected.created_at)} · ${actorName(selected)}`
+ : undefined
+ }
+ tags={
+ selected ? (
+
+ {bizTypeLabel(selected.biz_type)}
+ {actorTypeLabel(selected.actor_type)}
+
+ ) : undefined
+ }
+ sections={selected ? buildSections(selected) : []}
+ width={820}
+ emptyDescription="没有可展示的审计详情"
+ />
+
+ )
+}
diff --git a/frontend/src/features/admin/api.ts b/frontend/src/features/admin/api.ts
index 769709d..e9f8714 100644
--- a/frontend/src/features/admin/api.ts
+++ b/frontend/src/features/admin/api.ts
@@ -5,6 +5,9 @@ import type {
AdminAnnouncementPage,
AdminAnnouncementPayload,
AdminAnnouncementQuery,
+ AdminAuditLog,
+ AdminAuditPage,
+ AdminAuditQuery,
AdminCaptcha,
AdminDispute,
AdminDisputePage,
@@ -405,3 +408,17 @@ export async function unarchiveAdminAnnouncement(id: number | string) {
export async function deleteAdminAnnouncement(id: number | string) {
await adminClient.delete(`/admin/announcements/${id}`)
}
+
+export async function fetchAdminAuditLogs(query: AdminAuditQuery) {
+ const { data } = await adminClient.get>>('/admin/audit-logs', {
+ params: compactParams(query),
+ })
+ return {
+ items: data.data.items ?? [],
+ total: data.data.total ?? 0,
+ page: data.data.page ?? query.page ?? 1,
+ page_size: data.data.page_size ?? query.page_size ?? 20,
+ }
+}
+
+export type { AdminAuditLog }
diff --git a/frontend/src/features/admin/labels.ts b/frontend/src/features/admin/labels.ts
index ba79efd..00c53f4 100644
--- a/frontend/src/features/admin/labels.ts
+++ b/frontend/src/features/admin/labels.ts
@@ -218,3 +218,95 @@ export const announcementStatusOptions = Object.entries(announcementStatusMap).m
export const announcementCategoryOptions = Object.entries(announcementCategoryMap).map(
([value, label]) => ({ value, label })
)
+
+export const auditActorTypeMap: Record = {
+ admin: '管理员',
+ user: '用户',
+ system: '系统',
+}
+
+export const auditBizTypeMap: Record = {
+ order: '订单',
+ listing: '商品',
+ dispute: '申诉',
+ admin_user: '用户资料',
+ admin: '后台账号',
+ system_config: '系统配置',
+ payment_config: '支付配置',
+ manual_disbursement: '人工打款',
+ operating_expense: '运营支出',
+ pickup: '捡漏订单',
+ pickup_shop: '捡漏店铺',
+ distribution: '分销',
+ announcement: '公告',
+}
+
+export const auditActionMap: Record = {
+ 'order.admin_close': '后台关闭订单',
+ 'order.admin_seal': '订单封存',
+ 'order.deposit_hold': '冻结押金',
+ 'order.deposit_release': '释放押金',
+ 'order.force_handoff': '强制交接',
+ 'order.mark_abnormal': '标记异常订单',
+ 'order.offline_settlement': '线下结算',
+ 'order.platform_handoff': '平台代交接',
+ 'order.platform_checkout_confirm': '平台代确认结账',
+ 'order.platform_checkout_counter': '平台修正结账',
+ 'order.platform_checkout_dispute': '平台发起结账争议',
+ 'order.reset_checkout_confirm': '重置结账确认',
+ 'order.reset_handoff': '重置交接状态',
+ 'order.reset_renter_confirm': '重置租客确认',
+ 'order.reset_return_overdue': '重置归还逾期',
+ 'listing.adjust_external': '调整外部同步',
+ 'listing.adjust_review_price': '调整审核价',
+ 'listing.admin_offline': '后台下架商品',
+ 'listing.mark_abnormal': '标记异常商品',
+ 'listing.transfer_owner': '转移商品归属',
+ 'listing.update_screenshots': '更新商品截图',
+ 'dispute.admin_create': '代建申诉',
+ 'dispute.arbitrate': '提交仲裁',
+ 'admin_user.freeze': '冻结用户',
+ 'admin_user.unfreeze': '解冻用户',
+ 'admin_user.set_deposit_free_quota': '设置免押额度',
+ 'admin_user.adjust_growth_points': '调整成长值',
+ 'admin_user.manual_realname': '人工实名',
+ 'admin_user.revoke_realname': '撤销实名',
+ 'admin_user.wallet_adjust': '调整钱包余额',
+ 'system_config.create': '新增系统配置',
+ 'system_config.update': '更新系统配置',
+ 'payment_config.create': '新增支付配置',
+ 'payment_config.update': '更新支付配置',
+ 'payment_config.delete': '删除支付配置',
+ 'payment_config.export': '导出支付配置',
+ 'payment_config.import': '导入支付配置',
+ 'payment_config.view_secret': '查看支付密钥',
+ 'manual_disbursement_create': '新建人工打款',
+ 'manual_disbursement_void': '作废人工打款',
+ 'operating_expense_create': '登记运营支出',
+ 'operating_expense_void': '作废运营支出',
+ 'pickup_create': '创建捡漏订单',
+ 'pickup_cancel': '取消捡漏订单',
+ 'pickup_complete': '完成捡漏订单',
+ 'pickup_profit_update': '更新捡漏利润',
+ 'pickup_offline_settlement': '捡漏线下结算',
+ 'pickup_financial_adjustment_create': '新建捡漏财务调整',
+ 'pickup_financial_adjustment_settle': '结算捡漏财务调整',
+ 'pickup_shop_create': '新增捡漏店铺',
+ 'pickup_shop_update': '更新捡漏店铺',
+ 'pickup_shop_delete': '删除捡漏店铺',
+ 'pickup_shop_map_historical': '关联历史捡漏店铺',
+ 'pickup_shop_unmap_historical': '解除历史捡漏店铺',
+ 'pickup_third_party_order_no_update': '更新第三方单号',
+ 'distribution_financial_snapshot_create': '生成分销财务快照',
+ 'auth.token_version.bump': '刷新登录令牌版本',
+}
+
+export const auditActionOptions = Object.entries(auditActionMap).map(([value, label]) => ({
+ value,
+ label,
+}))
+
+export const auditBizTypeOptions = Object.entries(auditBizTypeMap).map(([value, label]) => ({
+ value,
+ label,
+}))
diff --git a/frontend/src/features/admin/types.ts b/frontend/src/features/admin/types.ts
index abfa4a7..5c7df59 100644
--- a/frontend/src/features/admin/types.ts
+++ b/frontend/src/features/admin/types.ts
@@ -577,3 +577,33 @@ export interface AdminAnnouncementPayload {
is_important: boolean
is_popup: boolean
}
+
+export interface AdminAuditQuery {
+ page?: number
+ page_size?: number
+ actor_id?: number
+ action?: string
+ biz_type?: string
+}
+
+export interface AdminAuditLog {
+ id: number
+ actor_type: string
+ actor_id: number
+ actor_username: string
+ actor_nickname: string
+ action: string
+ biz_type: string
+ biz_id?: number | null
+ ip: string
+ user_agent: string
+ detail?: unknown
+ created_at: string
+}
+
+export interface AdminAuditPage {
+ items: AdminAuditLog[]
+ total: number
+ page: number
+ page_size: number
+}
diff --git a/frontend/src/layouts/AdminLayout.tsx b/frontend/src/layouts/AdminLayout.tsx
index dfe0e6f..96985e5 100644
--- a/frontend/src/layouts/AdminLayout.tsx
+++ b/frontend/src/layouts/AdminLayout.tsx
@@ -107,7 +107,7 @@ const allNavGroups: NavGroup[] = [
icon: ,
children: [
{ key: '/system-configs', label: '系统配置', permission: 'systemconfig:view' },
- { key: '/audit-logs', label: '审计日志', permission: 'audit:view' },
+ { key: '/audit-logs', label: '审计日志', permission: 'audit_log:view' },
],
},
]
diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css
index 2289c48..f0dbbfb 100644
--- a/frontend/src/styles/global.css
+++ b/frontend/src/styles/global.css
@@ -1469,6 +1469,26 @@ a {
word-break: break-word;
}
+.admin-detail__pre--tall {
+ max-height: 440px;
+}
+
+.admin-audit-summary {
+ display: inline-block;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ vertical-align: bottom;
+}
+
+.admin-audit-ua {
+ color: var(--ink-muted);
+ font-family: 'Inter', monospace;
+ font-size: 11px;
+ word-break: break-all;
+}
+
.admin-detail-drawer .ant-drawer-header {
align-items: flex-start;
}