diff --git a/inventory-backend/app/api/v1/audit.py b/inventory-backend/app/api/v1/audit.py
index bb6c30e..96748bf 100644
--- a/inventory-backend/app/api/v1/audit.py
+++ b/inventory-backend/app/api/v1/audit.py
@@ -86,9 +86,14 @@ def _build_audit_query(args):
elif operator_type == 'system':
query = query.filter(AuditLog.username == SYSTEM_USERNAME)
+ # ---------- 操作人(精确匹配)----------
+ # ★ 由模糊(LIKE %值%)改为精确:前端的操作人已从自由输入改为**下拉选择**,
+ # 选出来的是完整账号,再模糊匹配就是错的 —— 将来出现 `gaoxue` 与
+ # `gaoxue2` 两个账号时,选前者会连带把后者的记录一起查出来。
+ # 部分匹配的需求由下拉的 filterable(在选项里搜)承担,不需要落到 SQL。
username = (args.get('username') or '').strip()
if username:
- query = query.filter(AuditLog.username.like(f'%{username}%'))
+ query = query.filter(AuditLog.username == username)
# ---------- 模块(支持多选 + 聚合别名)----------
# ★ 经 expand_modules 展开:「入库(全部)」会变成它名下的全部成员值。
@@ -243,6 +248,48 @@ def _serialize_logs(rows):
return out
+def _operator_options(company_limit):
+ """
+ 操作人下拉选项:[{'value': 账号, 'label': '中文名(账号)'}],按记录数降序。
+
+ ★ 从 **audit_logs** 取而不是从 sys_user:审计里记的是操作发生时的账号,
+ 用户被删/改名后 sys_user 就查不到了,而历史审计仍需要能按他筛选。
+ 实测 32 个操作人里有 1 个在 sys_user 中已不存在。
+
+ ★ display_name 本来就在审计行上('杜邢宸(duxingchen)'),不必 join。
+
+ ★ 按记录数降序:最活跃的人排最前,下拉一打开就是常用的那几个。
+
+ ★ 排除 system:它不是人,且「操作来源」那组单选已经专门管它
+ (真实用户 / 系统操作 / 全部)。混进来只会让两个控件语义打架。
+ """
+ q = db.session.query(
+ AuditLog.username,
+ func.max(AuditLog.display_name),
+ func.count().label('n'),
+ ).filter(AuditLog.username != SYSTEM_USERNAME)
+ if company_limit is not None:
+ q = q.join(SysUser, func.split_part(SysUser.username, '/', 2) == AuditLog.username) \
+ .filter(SysUser.department == company_limit)
+ q = q.group_by(AuditLog.username).order_by(func.count().desc())
+
+ out = []
+ for name, display, _n in q.all():
+ if not name:
+ continue
+ dn = (display or '').strip()
+ # display_name 库里存法不统一('高雪(gaoxue)' / '高闯/gaochuang' / 空),
+ # 统一成「名(账号)」;拿不到名字就只显示账号。
+ label = name
+ for sep in ('(', '/'):
+ if sep in dn:
+ dn = dn.split(sep)[0].strip()
+ if dn and dn != name:
+ label = f"{dn}({name})"
+ out.append({'value': name, 'label': label})
+ return out
+
+
def _distinct_with_company(column, company_limit):
"""
取某列的去重值(带公司隔离)。
@@ -298,7 +345,9 @@ def get_audit_logs():
# ★ [{value,label}],历史命名已折叠进聚合项且不再单独列出 ——
# 规则由后端 module_options() 统一决定,前端零硬编。
'modules': module_options(raw_modules),
- 'actions': actions
+ 'actions': actions,
+ # 操作人下拉选项(与 /operators 同源,顺带回一份省一次请求)
+ 'operators': _operator_options(company_limit),
}
}), 200
@@ -424,6 +473,27 @@ def get_modules():
return jsonify({'code': 500, 'msg': str(e)}), 500
+@audit_bp.route('/operators', methods=['GET'])
+@jwt_required()
+@permission_required('system_audit')
+def get_operators():
+ """
+ 获取操作人下拉选项(用于筛选)。
+
+ 返回 [{value: 账号, label: '名(账号)'}],按记录数降序,已排除 system。
+
+ ★ 加权限码,与 /modules(仅 JWT)**故意不同**:/modules 给的是模块名,
+ 这个给的是**人员账号清单**。它的唯一消费者就是审计页,而审计页本身
+ 要 system_audit —— 没道理让人绕开页面直接拉全员名单。
+ """
+ try:
+ company_limit = get_current_company_filter()
+ return jsonify({'code': 200, 'data': _operator_options(company_limit)}), 200
+ except Exception as e:
+ current_app.logger.error(f"获取操作人列表失败: {str(e)}")
+ return jsonify({'code': 500, 'msg': str(e)}), 500
+
+
@audit_bp.route('/labels', methods=['GET'])
@jwt_required()
def get_labels():
diff --git a/inventory-web/src/api/audit.ts b/inventory-web/src/api/audit.ts
index 77b2f89..10793cc 100644
--- a/inventory-web/src/api/audit.ts
+++ b/inventory-web/src/api/audit.ts
@@ -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() {
diff --git a/inventory-web/src/views/system/AuditLog.vue b/inventory-web/src/views/system/AuditLog.vue
index 1fc35d2..bffc77f 100644
--- a/inventory-web/src/views/system/AuditLog.vue
+++ b/inventory-web/src/views/system/AuditLog.vue
@@ -18,13 +18,27 @@
-
+
+ filterable
+ style="width: 180px"
+ >
+
+