diff --git a/docs/Phase1-验收报告.md b/docs/Phase1-验收报告.md index 79b9893..bb98e9a 100644 --- a/docs/Phase1-验收报告.md +++ b/docs/Phase1-验收报告.md @@ -194,12 +194,12 @@ dist/assets/AdminLayout-*.js 36.87 kB │ gzip 13.02 kB ## 5. 已知限制 1. 移动端订单列表/详情与消息中心已接入真实接口;下单、支付、交接、归还、结账等操作仍待迁移。 -2. 后台已完成仪表盘、订单列表/详情、退款审核、提现审核、商品列表/详情与审核、用户列表/详情与人工操作、申诉仲裁、财务看板、公告管理,其余页面仍为统一占位。 +2. 后台已完成仪表盘、订单列表/详情、退款审核、提现审核、商品列表/详情与审核、用户列表/详情与人工操作、申诉仲裁、财务看板、公告管理、审计日志,其余页面仍为统一占位。 3. 分销后台已完成主题、Logo、登录页与 Shell 轻改,页面结构未重写。 -4. 后台菜单权限码按后端逐个核对:订单 `order:list` / `order:view` / `order:close`,商品 `listing:view` / `listing:approve` / `listing:reject` / `listing:offline`,用户 `user:view` / `user:freeze` / `user:unfreeze` / `user:deposit_free` / `user:growth_points` / `user:manual_realname` / `user:revoke_realname` / `user:wallet_adjust`,提现 `withdrawal:list` / `withdrawal:detail` / `withdrawal:review` / `withdrawal:pay`,申诉 `dispute:view` / `dispute:arbitrate`,财务 `wallet:view`,公告 `announcement:view` / `announcement:manage`;其余页面仍需核对。 +4. 后台菜单权限码按后端逐个核对:订单 `order:list` / `order:view` / `order:close`,商品 `listing:view` / `listing:approve` / `listing:reject` / `listing:offline`,用户 `user:view` / `user:freeze` / `user:unfreeze` / `user:deposit_free` / `user:growth_points` / `user:manual_realname` / `user:revoke_realname` / `user:wallet_adjust`,提现 `withdrawal:list` / `withdrawal:detail` / `withdrawal:review` / `withdrawal:pay`,申诉 `dispute:view` / `dispute:arbitrate`,财务 `wallet:view`,公告 `announcement:view` / `announcement:manage`,审计日志 `audit_log:view`;系统配置与分销站点仍需核对。 5. SVG 中文尚未转曲。 6. 入口包已降至 495 kB(gzip 161 kB);AntD 共享基础设施仍在入口,后续可拆独立入口。 -7. 移动端、订单、分销轻改与首屏优化均已提交;商品管理与审核在本批次提交。 +7. 移动端、订单、分销轻改与首屏优化均已提交;商品管理、审核、退款、提现、用户、申诉、财务、公告与审计日志已按批次提交。 --- diff --git a/docs/Phase2-审计日志验收.md b/docs/Phase2-审计日志验收.md new file mode 100644 index 0000000..97146cb --- /dev/null +++ b/docs/Phase2-审计日志验收.md @@ -0,0 +1,86 @@ +# Phase 2 验收报告(后台审计日志) + +> 日期:2026-10-30 +> 项目:解忧商行(hfb_jysh) +> 范围:`/console-jy8k3m/audit-logs` +> 结论:**本切片通过,审计日志列表、筛选、分页与明细抽屉已接真实接口** + +--- + +## 1. 接口与权限 + +| 能力 | 接口 | 权限码 | +| --- | --- | --- | +| 审计日志列表 | `GET /api/admin/audit-logs` | `audit_log:view` | + +查询参数: + +```text +actor_id 管理员 ID,正整数 +action 动作代码,精确匹配 +biz_type 业务类型,精确匹配 +page 页码,默认 1 +page_size 每页条数,默认 20,最大 100 +``` + +响应字段:`items`、`total`、`page`、`page_size`。单条记录包含 `id`、`actor_type`、`actor_id`、`actor_username`、`actor_nickname`、`action`、`biz_type`、`biz_id`、`ip`、`user_agent`、`detail`、`created_at`。 + +## 2. 页面 + +`frontend/src/features/admin/AdminAuditLogsPage.tsx` + +- 筛选:管理员 ID、动作、业务类型 +- 动作与业务类型支持下拉建议,也允许直接输入后端动作代码 +- 列:时间/编号、操作人、动作、业务对象、IP、变更摘要、操作 +- 分页 10 / 20 / 50 +- 行点击或「详情」打开右侧抽屉 +- 无 `audit_log:view` 权限时显示提示并停止请求 + +## 3. 明细抽屉 + +- 操作信息:发生时间、日志编号、操作人、操作人类型、动作代码、业务对象 +- 请求信息:来源 IP、User-Agent +- 变更明细:`detail` 统一格式化为两空格缩进 JSON,等宽字体、可滚动 +- `detail` 兼容 JSON 对象、JSON 字符串与 `null`,无法解析的字符串按原文展示 + +## 4. 动作与业务类型文案 + +在 `frontend/src/features/admin/labels.ts` 增加: + +- `auditActorTypeMap`:管理员 / 用户 / 系统 +- `auditBizTypeMap`:订单、商品、申诉、用户资料、后台账号、系统配置、支付配置、人工打款、运营支出、捡漏、分销、公告 +- `auditActionMap`:订单、商品、申诉、用户、配置、打款、支出、捡漏、分销与令牌刷新等已知动作的中文名称 + +未知动作或业务类型保留原始代码,避免后端新增动作时前端丢失信息。 + +## 5. 菜单与路由 + +- 路由:`frontend/src/app/AppRouter.tsx` 新增 `audit-logs` +- 菜单:`frontend/src/layouts/AdminLayout.tsx` 权限码由错误的 `audit:view` 修正为后端实际使用的 `audit_log:view` +- 占位列表同步移除 `audit-logs` + +## 6. 验证结果 + +```text +npx tsc --noEmit 通过 +npm run build 通过(入口 497.18 kB,gzip 161.36 kB) +GET /api/admin/audit-logs?page=1&page_size=5 ok,total=1 +GET /api/admin/audit-logs?actor_id=1&action=... ok,筛选命中 1 条 +GET /console-jy8k3m/audit-logs 200 +``` + +开发库当前只有 1 条改密产生的审计记录;列表读取与筛选已实测,未触发新的资金或配置写操作。 + +## 7. 已知限制 + +1. 没有动作字典接口,筛选项来自前端维护的已知动作清单,后端新增动作需同步文案(未知动作仍可展示与按代码筛选)。 +2. 不支持时间范围筛选与关键字搜索,后端暂未提供对应参数。 +3. 没有导出 CSV 或日志归档能力。 +4. `detail` 仅在抽屉中展示,未做字段级 diff 高亮。 + +## 8. 下一步建议 + +1. 后台系统配置页。 +2. 线下出款与运营支出登记。 +3. 审计日志时间范围与关键字筛选(需后端补参数)。 +4. 移动端下单、支付与交接/归还/结账。 diff --git a/docs/项目计划.md b/docs/项目计划.md index a332077..01f7866 100644 --- a/docs/项目计划.md +++ b/docs/项目计划.md @@ -395,7 +395,7 @@ hfb_jysh/ ### Phase 4:管理后台核心(4–5 周) -> 状态:**进行中(2026-10-30)**。已完成仪表盘、订单、退款、提现、商品、用户、申诉、财务看板与公告管理;线下出款/运营支出、系统配置与审计日志待迁移。 +> 状态:**进行中(2026-10-30)**。已完成仪表盘、订单、退款、提现、商品、用户、申诉、财务看板、公告管理与审计日志;线下出款/运营支出与系统配置待迁移。 - 后台登录、工作台、订单列表、订单详情。 - 商品列表、商品详情、审核。 diff --git a/frontend/src/app/AppRouter.tsx b/frontend/src/app/AppRouter.tsx index 887c0f0..cffac23 100644 --- a/frontend/src/app/AppRouter.tsx +++ b/frontend/src/app/AppRouter.tsx @@ -103,6 +103,11 @@ const AdminAnnouncementsPage = lazy(() => default: module.AdminAnnouncementsPage, })) ) +const AdminAuditLogsPage = lazy(() => + import('@/features/admin/AdminAuditLogsPage').then(module => ({ + default: module.AdminAuditLogsPage, + })) +) const AdminLayout = lazy(() => import('@/layouts/AdminLayout').then(module => ({ default: module.AdminLayout })) ) @@ -113,7 +118,6 @@ const AdminSectionPlaceholder = lazy(() => const adminSections: Array<{ path: string; title: string; description: string }> = [ { path: 'distribution/sites', title: '分销站点', description: '管理分站、规则、结算与站点钱包。' }, { path: 'system-configs', title: '系统配置', description: '维护首页、发布、订单与支付配置。' }, - { path: 'audit-logs', title: '审计日志', description: '查看高风险操作与变更记录。' }, ] export function AppRouter() { @@ -154,6 +158,7 @@ export function AppRouter() { } /> } /> } /> + } /> {adminSections.map(section => ( ) + if (entries.length === 0) return '{}' + return entries + .slice(0, 4) + .map(([key, value]) => `${key}: ${summarizeValue(value)}`) + .join(' · ') + } + const text = summarizeValue(parsed) + return text.length > 120 ? `${text.slice(0, 120)}…` : text +} + +function actorName(row: AdminAuditLog) { + return row.actor_nickname || row.actor_username || `ID ${row.actor_id}` +} + +function actorTypeLabel(value?: string | null) { + if (!value) return '未知来源' + return auditActorTypeMap[value] ?? value +} + +function bizTypeLabel(value?: string | null) { + if (!value) return '未关联业务' + return auditBizTypeMap[value] ?? value +} + +function buildColumns(onOpen: (row: AdminAuditLog) => void): TableColumnsType { + 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) => ( + + ), + }, + ] +} + +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={ + + } + /> + + {!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; }