feat(audit): 操作人筛选改为下拉选择,过滤由模糊改为精确

原来的「操作人」是自由文本输入,后端按 `LIKE %值%` 模糊匹配。

后端:
  · 新增 GET /audit/operators,返回 [{value: 账号, label: '名(账号)'}],
    按记录数降序(最活跃的排最前),排除 system;/logs 也内联一份省一次请求。
  · username 过滤由 LIKE 改**精确匹配**。选出来的是完整账号,再模糊匹配就是错的:
    实测 `LIKE '%gao%'` 会多带出 2439 条非 gaoxue 的记录,将来出现
    gaoxue / gaoxue2 两个账号时,选前者会连带查出后者的记录。
    部分匹配的需求由下拉的 filterable(在选项里搜)承担,不落到 SQL。

★ 选项从 **audit_logs** 取,不是从 sys_user:审计记的是操作发生时的账号,
  用户被删/改名后 sys_user 就查不到,而历史审计仍需能按他筛选 ——
  实测 32 个操作人里有 1 个在 sys_user 中已不存在。
  display_name 本来就在审计行上('杜邢宸(duxingchen)'),不必 join。

★ 排除 system:它不是人,「操作来源」那组单选(真实用户/系统操作/全部)
  已经专门管它,混进下拉会让两个控件语义打架。

★ /operators 加了 system_audit 权限码,与 /modules(仅 JWT)**故意不同**:
  /modules 给的是模块名,这个给的是**人员账号清单**。它唯一的消费者就是
  审计页,而审计页本身要 system_audit —— 没道理让人绕开页面直接拉全员名单。

前端 AuditLog.vue:
  · 操作人由 el-input 改为 el-select(clearable + filterable),
    选项由后端下发,前端不硬编。
  · 拉取失败(无权限 403 等)时 catch 住,下拉优雅退化为空,不抛未捕获异常。

验证(21 + 20 项断言全过):
  · 32 个操作人全覆盖、排除 system、label 带中文名、按记录数降序
  · 精确匹配与 SQL count 逐一核对(gaoxue 2519 / duxingchen 16863 / liuqi 1236),
    且结果里不含他人
  · 导出口径在与列表一致的 4 组筛选下逐一对齐(含操作人筛选)
  · 权限矩阵:/logs 与 /export 无 token 401、无权限 403;/operators 无权限 403;
    /labels 保持仅 JWT(纯静态标签)
  · 日报三天附件 490.5K/193.4K/92.3K 与 6 列结构未变;聚合行仍可读
  · vue-tsc 与 vite build exit=0
This commit is contained in:
yueli
2026-09-28 11:15:15 +08:00
parent 0b87a72a41
commit 52a3c4079c
3 changed files with 124 additions and 8 deletions

View File

@ -25,6 +25,17 @@ export function getAuditModules() {
})
}
// 获取可选操作人列表
//
// ★ 返回 [{value: 账号, label: '名(账号)'}],按记录数降序,已排除 system。
// 从审计日志本身取而非用户表:用户被删/改名后仍能按历史账号筛选。
export function getAuditOperators() {
return request({
url: '/audit/operators',
method: 'get'
})
}
// 获取操作类型 / 字段名的中文映射
// ★ 后端为唯一来源,前端不再自带副本(见 views/system/AuditLog.vue 的说明)
export function getAuditLabels() {

View File

@ -18,13 +18,27 @@
</el-radio-group>
</el-form-item>
<el-form-item label="操作人">
<el-input
<!--
★ 下拉选择而不是自由输入:选出来的是**完整账号**,后端按精确匹配过滤。
filterable 让用户敲几个字就能在选项里搜到人(部分匹配的需求在这里
承担,不必落到 SQL 的 LIKE —— 那会在出现 gaoxue / gaoxue2 时误命中)。
可选人员清单由后端 /audit/operators 下发,已排除 system
(它不是人,「操作来源」那组单选专门管它)。
-->
<el-select
v-model="queryParams.username"
placeholder="输入姓名或账号"
placeholder="全部操作人"
clearable
style="width: 150px"
@keyup.enter="handleQuery"
/>
filterable
style="width: 180px"
>
<el-option
v-for="o in operatorOptions"
:key="o.value"
:label="o.label"
:value="o.value"
/>
</el-select>
</el-form-item>
<el-form-item label="模块">
<!--
@ -326,7 +340,13 @@
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { Search, Refresh, Download, EditPen, Document } from '@element-plus/icons-vue'
import { getAuditLogs, getAuditModules, getAuditLabels, exportAuditLogs } from '@/api/audit'
import {
getAuditLogs,
getAuditModules,
getAuditOperators,
getAuditLabels,
exportAuditLogs,
} from '@/api/audit'
// 表格数据
const tableLoading = ref(false)
@ -385,6 +405,8 @@ const booleanFields = ref<string[]>([])
const hiddenFields = ref<string[]>([])
// 快照键 → 标题(后端下发,见「快照的分层渲染」一节)
const snapshotViews = ref<Array<{ key: string; title: string }>>([])
// 操作人下拉选项(后端下发,按记录数降序)
const operatorOptions = ref<Array<{ value: string; label: string }>>([])
// 字段中文名解析:命中映射取中文,否则原样返回
const fieldLabel = (key: string | number): string => {
@ -710,6 +732,9 @@ const getList = async () => {
if (res.data.actions) {
actionOptions.value = res.data.actions
}
if (res.data.operators) {
operatorOptions.value = res.data.operators
}
}
} catch (error) {
console.error('获取审计日志失败:', error)
@ -784,6 +809,16 @@ onMounted(() => {
moduleOptions.value = res.data
}
})
// 操作人选项:不依赖列表结果,首次进页面就能选。
// 接口带 system_audit 权限码,无权限时会 403 —— 兜住它,
// 让下拉优雅退化(列表接口也会顺带返回一份,不致命)。
getAuditOperators().then(res => {
if (res.code === 200 && res.data) {
operatorOptions.value = res.data
}
}).catch(e => {
console.warn('[AuditLog] 操作人选项拉取失败,下拉暂为空', e)
})
})
</script>