huoyan-enterprise/backend/services/audit_service.py

156 lines
5.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
知识库操作审计日志服务
action 枚举值
-------------
upload 上传文件到知识库
download 下载/查看文件(含 URL 预览)
delete 删除文件
archive 归档文件(预留)
create_kb 创建知识库
delete_kb 删除知识库
permission_change 修改下级用户权限(如关闭/开启上传权限)
"""
from typing import Any, Dict, List, Optional, Tuple
import asyncpg
from logger.logging import get_logger
logger = get_logger(__name__)
VALID_ACTIONS = frozenset({
"upload",
"download",
"delete",
"archive",
"create_kb",
"delete_kb",
"permission_change",
})
class AuditService:
@staticmethod
async def write(
conn: asyncpg.Connection,
*,
enterprise_id: int,
actor_id: int,
action: str,
target_user_id: Optional[int] = None,
department_id: Optional[int] = None,
kb_id: Optional[int] = None,
file_id: Optional[int] = None,
ip: Optional[str] = None,
user_agent: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
"""写入一条审计日志(非阻塞,异常仅记录不抛出)。"""
try:
import json
meta_json = json.dumps(metadata, ensure_ascii=False) if metadata else None
await conn.execute(
"""
INSERT INTO kb_audit_log
(enterprise_id, actor_id, action, target_user_id, department_id,
kb_id, file_id, ip, user_agent, metadata)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb)
""",
enterprise_id,
actor_id,
action,
target_user_id,
department_id,
kb_id,
file_id,
ip,
user_agent,
meta_json,
)
except Exception as e:
logger.warning(f"写入审计日志失败(不影响主流程): {e}")
@staticmethod
async def list_logs(
conn: asyncpg.Connection,
enterprise_id: int,
*,
department_ids: Optional[List[int]] = None, # None 表示不过滤admin 全量)
actor_id: Optional[int] = None,
kb_id: Optional[int] = None,
action: Optional[str] = None,
page: int = 1,
page_size: int = 20,
) -> Tuple[List[dict], int]:
"""
查询审计日志。
- department_ids=None -> 全企业admin 用)
- department_ids=[...] -> 限制在给定部门内leader 用,按 actor 所在部门过滤)
"""
offset = (page - 1) * page_size
conds = ["l.enterprise_id = $1"]
params: list = [enterprise_id]
if department_ids is not None:
if not department_ids:
return [], 0
conds.append(f"l.department_id = ANY(${len(params)+1}::int[])")
params.append(department_ids)
if actor_id is not None:
conds.append(f"l.actor_id = ${len(params)+1}")
params.append(actor_id)
if kb_id is not None:
conds.append(f"l.kb_id = ${len(params)+1}")
params.append(kb_id)
if action is not None:
conds.append(f"l.action = ${len(params)+1}")
params.append(action)
where = " AND ".join(conds)
count_sql = f"SELECT COUNT(*) FROM kb_audit_log l WHERE {where}"
total = await conn.fetchval(count_sql, *params)
lim_ph = len(params) + 1
off_ph = len(params) + 2
params.extend([page_size, offset])
rows = await conn.fetch(
f"""
SELECT
l.id, l.enterprise_id, l.actor_id, l.action,
l.target_user_id, l.department_id, l.kb_id, l.file_id,
l.ip, l.metadata, l.created_at,
COALESCE(NULLIF(TRIM(u.display_name),''), u.username) AS actor_name,
COALESCE(NULLIF(TRIM(tu.display_name),''), tu.username) AS target_name,
d.name AS department_name,
kb.name AS kb_name,
kf.file_name
FROM kb_audit_log l
LEFT JOIN user_list u ON u.id = l.actor_id
LEFT JOIN user_list tu ON tu.id = l.target_user_id
LEFT JOIN department d ON d.id = l.department_id
LEFT JOIN knowledge_base kb ON kb.id = l.kb_id
LEFT JOIN knowledge_base_file kf ON kf.id = l.file_id
WHERE {where}
ORDER BY l.created_at DESC
LIMIT ${lim_ph} OFFSET ${off_ph}
""",
*params,
)
import json as _json
result = []
for r in rows:
row = dict(r)
meta = row.get("metadata")
if isinstance(meta, str):
try:
row["metadata"] = _json.loads(meta)
except Exception:
row["metadata"] = None
result.append(row)
return result, int(total or 0)