1. 页面概览
1.1 是什么
审计管理页面是 LightFoundry 面向平台管理员与安全审计人员的操作入口,路由为 /foundry/audit。它提供四类能力:审计事件列表查询、审计统计摘要、审计日志导出(CSV/JSON)与审计热数据手动归档。所有能力都对接 Foundry 后端 18081 端口的 /api/v1/audit/* 路由组,与 AIP 管理台的 AuditLogPage 相对独立,二者共用同一套平台级审计存储,但查询入口与管理能力各自归属所属产品。
从数据流看,页面的数据来源是 platform/audit 审计服务写入的 audit_log 表。系统中任何关键业务动作(登录、数据源操作、SQL 执行、AI 决策、报表派发、工作流触发等)都会经 LogEventRef 记录一条审计事件,包含用户、事件类型、结果、引用对象与详情。审计管理页把这些事件以表格形式呈现,并支持按事件类型过滤、按数量分页读取、按 CSV/JSON 两种格式导出,以及把超过保留天数的热数据归档为 JSON Lines 文件。
1.2 核心价值
| 价值点 | 说明 |
|---|---|
| 事件可见性 | 审计事件列表展示时间、用户、事件类型、结果、引用与详情,便于回溯"谁在什么时候对什么做了什么" |
| 统计摘要 | 按事件类型(by_category)、动作(by_action)、结果(by_status)三个维度分组计数,一眼掌握系统活跃度与失败面 |
| 合规导出 | 支持 CSV(UTF-8 BOM + 中文表头,Excel 直接打开不乱码)与 JSON 两种格式,满足外审与归档留痕 |
| 热数据治理 | 手动归档可将超过保留天数的热数据导出为 JSON Lines 文件并从热库删除,控制审计表体积 |
| 独立治理 | 与 AIP 审计页各自归属产品治理,本页专管 Foundry 域内审计事件 |
1.3 一句话总结
审计管理页是 Foundry 审计事件的"查询台 + 出口",把平台级审计留痕以列表、摘要、导出、归档四种形态交付给管理员与安全审计人员。
2. 访问入口
2.1 路由与菜单
| 项目 | 值 |
|---|---|
| 路由 path | /foundry/audit |
| 路由 name | FoundryAudit |
| 路由 title | Foundry 审计管理 |
| requiresAuth | true |
| 菜单位置 | Foundry 左侧边栏(FoundryLayout.vue)菜单项「审计管理」 |
| 前端源码 | action/web/src/views/FoundryAuditPage.vue |
| 路由注册 | action/web/src/router/index.js |
侧边栏菜单在 action/web/src/views/FoundryLayout.vue 中注册,菜单项为 { path: '/foundry/audit', label: '审计管理' }。点击后进入页面,同时页面标题区展示「Foundry 审计管理」标题。
2.2 认证与权限
页面 meta 配置 requiresAuth: true,未登录访问会被前端路由守卫拦到登录页。页面所有请求通过 action/web/src/api/client.js 的 axios 实例发出:
- baseURL 为
/api/v1,由 Vite 开发服务器代理到 Foundry 后端 18081; - 请求拦截器从
localStorage读取aip_token,以Authorization: Bearer <token>附带 JWT Token; - 响应拦截器在收到 HTTP 401 时清除
aip_token与aip_username,并跳转/login(登录页自身不重复跳转)。
所以审计管理页的鉴权标识是 aip_token(与 AIP 共用同一登录体系,Foundry 挂载在 FoundryLayout 下复用同一 Token)。若后端返回 401,页面本身不做特殊提示,而是整体跳转登录页。
2.3 端口与 API 前缀
Foundry 后端默认端口 18081,API 前缀 /api(实际请求路径均带 /v1,即 /api/v1/audit/*)。开发环境下由 Vite(默认 5173)代理转发。
3. 界面布局
+--------------------------------------------------------------------+
| Foundry 审计管理 |
+--------------------------------------------------------------------+
| [操作结果提示 alert(有消息时显示,可关闭)] |
+--------------------------------------------------------------------+
| 统计摘要 summary(card,有数据时显示) |
| + 事件总数(大数字) | 按事件类型 by_category | 按动作 by_action | |
| | | 按结果 by_status |
+--------------------------------------------------------------------+
| 审计事件 events(card) |
| 筛选栏:□事件类型(可多选) [50条▾] [刷新] [导出CSV] [导出JSON] |
| 表格:时间 | 用户 | 事件类型 | 结果 | 引用 | 详情 action_details |
+--------------------------------------------------------------------+
| 归档 archive(card) |
| 保留天数[90] [手动归档] |
| 表格:归档时间 | 范围 | 条数 | 大小 | 文件路径 |
+--------------------------------------------------------------------+
各板块职责:
- 操作结果提示:页面顶部单例 alert,所有成功(alert-success)/失败(alert-error)/信息提示统一经
showAlert写入,右上角「关闭」按钮清空消息。 - 统计摘要:展示
GET /audit/events/summary返回的总数、按事件类型计数、按动作计数、按结果计数;结果标签按tag-success/tag-error着色。 - 审计事件列表:核心查询区,支持事件类型勾选过滤、每页条数选择(50/100/200/500/1000)、真 offset 翻页(上一页/下一页 + 页码)、刷新、导出;加载中显示"加载中...",空结果显示"暂无审计事件。"。
- 归档操作:输入保留天数后手动触发归档,下方展示归档记录(每次归档的时间范围、条数、文件大小、文件路径)。
4. 交互元素详解
4.1 审计事件卡片
| 元素 | 位置 | 含义 | 必填与默认值 | 操作效果 | 触发的后端调用 |
|---|---|---|---|---|---|
| 事件类型复选框 | 筛选栏 | 按事件类型过滤 | 默认全不选(不过滤) | 勾选后点「刷新」按逗号拼接传入 event_type | GET /audit/events?event_type=... |
| 每页条数下拉框 | 筛选栏 | 单次加载条数(即后端 limit,上限 1000) | 默认「50 条」 | 切换后回到第 1 页并按新条数查询 | GET /audit/events?limit=50|100|200|500|1000 |
| 上一页 / 下一页 | 列表下方翻页条 | offset 翻页 | 第 1 页时「上一页」禁用;最后一页时「下一页」禁用 | 按 offset=(页码-1)×每页条数 重查 | GET /audit/events?limit=&offset= |
| 刷新按钮 | 筛选栏 | 重新加载摘要与事件 | — | 并行刷新摘要与事件列表 | GET /audit/events/summary + GET /audit/events |
| 导出全量 CSV 按钮 | 筛选栏 | 导出当前过滤范围内全量日志为 CSV(不跟随分页) | 加载/导出中禁用 | blob 下载 audit_export_<时间戳>.csv | GET /audit/events/export?format=csv |
| 导出全量 JSON 按钮 | 筛选栏 | 导出当前过滤范围内全量日志为 JSON(不跟随分页) | 加载/导出中禁用 | blob 下载 audit_export_<时间戳>.json | GET /audit/events/export?format=json |
4.2 审计事件表格列
| 列名 | 渲染逻辑 | 说明 |
|---|---|---|
| 时间 | formatTime(log.timestamp),T 替换为空格、截前 19 位 | 服务器本地时间的文本形式 |
| 用户 | log.user_id | 操作者用户 ID |
| 事件类型 | log.event_type | 如 USER_LOGIN、NLQ_QUERY、AI_DECISION 等 |
| 结果 | log.result,标签着色(tag-success/tag-error) | 成功/失败等结果状态 |
| 引用 | ref_type:ref_id(无则显示 -) | 关联对象类型与对象 ID |
| 详情 action_details | formatDetails:对象转 JSON 字符串,字符串原样 | 动作详情,超宽省略号截断 |
4.3 归档卡片
| 元素 | 位置 | 含义 | 必填与默认值 | 操作效果 | 触发的后端调用 |
|---|---|---|---|---|---|
| 保留天数输入框 | 归档表单 | 超过该天数的热数据将被归档,0 = 全部热数据 | 默认 90,type=number min=0 | 改动后点「手动归档」生效 | POST /audit/archive,body {retention_days} |
| 手动归档按钮 | 归档表单 | 触发归档 | 归档进行中显示「归档中...」并禁用 | 成功后提示"归档完成:共 N 条,文件 ...",并刷新归档状态、摘要、事件列表 | POST /audit/archive |
| 归档记录表格 | 归档卡片下方 | 展示历史归档 | — | 只读展示 | GET /audit/archive/status |
归档记录表列:归档时间(archived_at)、范围(start_time ~ end_time)、条数(count)、大小(formatSize(size_bytes),自动 B/KB/MB)、文件路径(file_path)。
5. 后端关联
5.1 API 客户端
页面使用 action/web/src/api/client.js 的默认导出 apiClient:
- baseURL:
/api/v1(Vite 代理 → Foundry 18081); - 超时:30000ms;
- 请求头:
Content-Type: application/json; - 请求拦截器:附加
Authorization: Bearer ${localStorage.aip_token}; - 响应拦截器:401 清 Token 并跳
/login。
页面中所有 apiClient.get/post 均不带全路径前缀,直接写 /audit/... 相对路径。
5.2 端点表
| 方法 | 路径 | 请求参数 | 超时 |
|---|---|---|---|
| GET | /audit/events | query:limit(默认 50,上限 1000)、offset(默认 0)、event_type(逗号分隔列表)、user_id、start_time/end_time(RFC3339) | 30s |
| GET | /audit/events/summary | query:start_time/end_time(可选,缺省统计全部) | 30s |
| GET | /audit/events/export | query:format(csv|json,默认 json)、event_type、user_id、action、resource_type、result、start_time/end_time;前端带 responseType: 'blob' | 30s |
| POST | /audit/archive | body:{retention_days}(或 query retention_days,默认 90) | 30s |
| GET | /audit/archive/status | 无 | 30s |
format 外,还按筛选栏的 event_type、user_id、start_time/end_time 一并传参;导出不跟随列表分页 offset,始终导出当前过滤条件下的全量(后端 QueryEvents 仅在 filter.Limit>0 时截断,导出侧未设 Limit,见 platform/repository/crud.go:913)。后端导出另支持的 action/resource_type/result 过滤暂未在 UI 暴露,不随导出携带。5.3 响应结构
统一成功包裹:{"code": 0, "data": ...};错误:{"code": "<错误码>", "error": "<详情>"},HTTP 状态码取错误类型。
事件列表响应 data:
{
"logs": [
{
"id": "audit_log_xxxx",
"user_id": "alice",
"timestamp": "2026-08-30T10:15:00+08:00",
"event_type": "USER_LOGIN",
"action_details": {"ip": "10.0.0.5", "method": "password"},
"result": "SUCCESS",
"ref_type": "user",
"ref_id": "u_1"
}
]
}
摘要响应 data:
{
"total": 1024,
"by_category": {"USER_LOGIN": 512, "NLQ_QUERY": 400},
"by_action": {"USER_LOGIN": 512, "NLQ_QUERY": 400},
"by_status": {"SUCCESS": 980, "FAILURE": 44}
}
归档结果响应 data:
{
"count": 360,
"file_path": "data/archives/audit_20260830T...jsonl",
"size_bytes": 245760,
"start_time": "2026-06-01T00:00:00+08:00",
"end_time": "2026-08-30T10:15:00+08:00",
"archived_at": "2026-08-30T10:20:00+08:00"
}
归档状态响应 data:{"archives": [ ...同上 ArchiveResult 数组... ]}。
5.4 关联模块表
| 后端包 | 职责 |
|---|---|
action/products/foundry/server/audit_handlers.go | 审计五个 handler:列表/摘要/导出/归档/归档状态 |
action/platform/audit | 审计服务:GetAuditLogs、Summary、ExportCSV/ExportJSON、Archive/ArchiveStatus |
action/platform/audit/hashchain.go | 审计哈希链:每条记录按前一条 hash 链式计算,防篡改 |
action/products/foundry/server/server.go | protected 组路由注册(1039-1043 行)与审计服务组装 |
5.5 关键机制
- 哈希链防篡改:审计日志写入串行化经过
hashMu互斥锁,先取上一条记录 hash 再写入当前记录,保证哈希链不被并发写打断;导出的 JSON 行内包含链式校验字段,供外部复核。 - 时间范围解析:
parseAuditTimeRange把 RFC3339 参数统一转为服务器本地时区,避免"UTC 参数窗口 0 行、本地参数 1 行"的跨天比较错位问题(SQLite 按本地文本存储)。 - 分页上限:
limit默认 50、上限 1000,超出按 1000 截断;offset负值归 0。 - 响应无 total:
handleListAuditEvents只回{logs:[...]}(audit_handlers.go:95),不含总条数,前端故无法得知总页数,下一页改用「本页行数 == 每页条数」的满页启发式判断。 - 导出编码:CSV 输出带 UTF-8 BOM 与中文表头,Excel 可直接打开;文件名
audit_export_<yyyyMMddTHHmmss>.csv/.json,经Content-Disposition下发。 - 归档语义:
retention_days=0表示归档全部热数据;归档把超期事件导出为 JSON Lines 文件后删除热库记录;归档目录由auditArchiveDir计算(优先app.data_path/archives)。 - 前端导出是 blob:
responseType: 'blob',前端用URL.createObjectURL生成临时链接点击下载,下载名audit_export_${Date.now()}.${format}(浏览器端时间戳)。
6. 核心流程详解
6.1 页面加载主流程
onMounted触发handleRefresh()(并行fetchSummary+fetchEvents)与fetchArchiveStatus()。fetchSummary调GET /audit/events/summary,成功后summary.value = data.data;失败showAlert('加载审计摘要失败:...', 'alert-error')。fetchEvents置loading=true,按filters.limit与勾选事件类型拼event_type逗号串调GET /audit/events,成功取data.data.logs;失败弹错;finally 复位 loading。fetchArchiveStatus调GET /audit/archive/status,成功取data.data.archives。
事件类型选项是动态计算的:eventTypeOptions = 摘要 by_category 的键 ∪ 当前日志中出现的事件类型,去重后作为复选框集合。因此首次进入时如果摘要为空,选项集合可能为空,刷新出数据后选项自然增长。
6.2 过滤与查询分支
- 勾选事件类型 → 点「刷新」:
params.event_type = filters.eventTypes.join(',')(逗号分隔),后端按列表过滤。 - 切换每页条数(50/100/200/500/1000)→ 立即回到第 1 页重查:
limit变更,offset = (页码-1)×limit。 - 点「上一页」/「下一页」→ 页码增减后按新
offset重查;因响应无total,下一页仅在「本页行数 == 每页条数」时可用(满页启发式),若翻到空页前端自动回退一页。 - 页面加载与手动归档成功后都会触发刷新,保证列表与摘要联动一致。
6.3 导出流程(CSV/JSON)
- 点「导出 CSV」或「导出 JSON」→
handleExport(format)置exporting=true。 - 组参:
{format}+ 与列表一致的过滤条件(event_type/user_id/start_time/end_time),不带limit/offset——导出为当前过滤条件下的全量,不跟随列表分页(按钮已如实标注「导出全量」)。 GET /audit/events/export,responseType: 'blob'。- 成功后用
Blob建 URL,创建<a download="audit_export_<ts>.<format>">触发点击下载,随后revokeObjectURL释放。 - 提示"审计日志已导出为 CSV/JSON";失败提示"导出失败:..."。
6.4 归档流程
- 输入保留天数(默认 90),点「手动归档」→
handleArchive置archiving=true。 POST /audit/archive,body{retention_days: archiveDays.value}。- 成功:提示"归档完成:共 N 条,文件 <path>",随后并行刷新归档状态、摘要、事件列表。
- 失败:提示"归档失败:...";无论成败 finally 复位
archiving。
6.5 状态与终态语义
审计事件没有前端状态机;其"终态"体现在 result 字段(SUCCESS/FAILURE 等),由各业务模块在写入审计时确定。归档是一次性任务:调 POST /audit/archive 同步返回结果(条数、文件路径),无任务轮询;归档记录长期保留在 audit/archive/status 列表中作为历史留痕。
7. 权限与安全
- 认证:JWT Bearer Token(
aip_token),401 自动登出跳转/login。 - 路由守卫:
requiresAuth: true,未授权不可见。 - 数据级安全:审计事件属于平台级安全数据,本页无额外的 RLS/CLS 前台筛选;可见范围取决于后端审计服务的查询实现与用户是否在 protected 鉴权组内。后端所有审计路由均挂在
protected组,未带有效 Token 一律 401。 - 写操作防护:手动归档是唯一的写操作,直接调
POST /audit/archive,无二次确认弹窗;导出与查询均为只读。 - 防篡改:审计哈希链保证历史记录被篡改可被外部复核发现。
8. 常见问题与排错
问题 1:页面提示「加载审计事件失败:...」
- 现象:审计事件表格不出数,顶部红色 alert 提示失败。
- 原因:常见为 Token 失效(后端 401)、后端未启动、或 Vite 代理未指向 18081。
- 排查步骤:
- 1. 打开浏览器 DevTools Network 看请求是否落在
/api/v1/audit/events; - 2. 确认响应状态:401 说明 Token 过期(会被全局拦截跳登录);404 说明代理前缀错误,检查 Vite 配置
/api是否代理到http://localhost:18081; - 3. 确认 Foundry 后端进程存活(端口 18081),必要时重新登录换取新
aip_token。
问题 2:导出 CSV 在 Excel 打开乱码或下载名不对
- 现象:下载的 CSV 中文乱码,或文件名不是预期。
- 原因:CSV 已带 UTF-8 BOM,一般 Excel 可直接打开;乱码多发生在用旧 Excel/其他工具读取无 BOM 的 CSV,或浏览器下载被拦截。
- 排查步骤:
- 1. 用记事本/编辑器打开 CSV 文件确认内容编码(应含 BOM);
- 2. 确认导出请求返回的
Content-Disposition文件名(后端为audit_export_<时间戳>,前端 blob 下载名带Date.now()时间戳); - 3. 检查浏览器是否拦截了自动下载,放行后重试。
问题 3:手动归档后热数据没有减少
- 现象:点「手动归档」提示成功,但审计事件列表仍有大量旧记录。
- 原因:归档按
retention_days计算"超过该天数的记录才归档";若输入 90 但最旧记录距今不足 90 天,则归档条数为 0;也可能归档被后端报错(未提示)。 - 排查步骤:
- 1. 看归档提示的"共 N 条"是否大于 0,以及归档记录表的范围时间;
- 2. 想验证全量归档可把保留天数填 0(0 = 全部热数据)再执行一次;
- 3. 仍无效果则查看后端日志中
Archive的报错(归档目录写入权限、磁盘空间等)。
问题 4:事件类型复选框无法勾选/为空
- 现象:筛选栏没有可勾选的事件类型。
- 原因:
eventTypeOptions由摘要by_category与当前日志事件类型推导;若摘要与日志均为空,选项自然为空。 - 排查步骤:
- 1. 确认系统确有审计事件产生(登录一次再回来刷新);
- 2. 确认摘要请求
GET /audit/events/summary返回非空by_category; - 3. 为空则说明当前数据库审计记录为空或查询失败,先处理摘要加载失败问题。
9. 已知缺陷与边界
| 项 | 说明 |
|---|---|
| 无编辑/删除单条 | 审计记录只读,仅支持导出与归档,不提供单条修改(符合审计不可篡改原则) |
| 分页无 total(边界) | /audit/events 响应只有 {logs:[...]}、无 total(audit_handlers.go:95)。已实现真 offset 分页:每页条数 50/100/200/500/1000,offset=(页码-1)×每页条数;因总条数不可知,上一页按 page>1、下一页按「本页行数 == 每页条数」满页启发式判断(最后一页恰满页时会多出一次空翻,前端自动回退一页) |
| 与 AIP 审计页独立 | 与 AIP AuditLogPage 各自独立实现,能力集合不同,勿混用接口前缀 |
注:时间过滤未暴露已于 2026-09-06 修复(筛选栏新增用户 ID 与开始/结束日期控件,直接对接后端 user_id/start_time/end_time 参数)。
注:归档无二次确认已于 2026-09-06 修复(「手动归档」提交前弹确认框、按钮为 danger 样式,保留天数 0 = 全量归档时提示更强文案)。
注:导出过滤子集已于 2026-09-06 修复(导出与列表共用同一套过滤条件,勾选的 event_type 及填写的用户/时间范围一并带入导出请求)。