Files
track/backend/app/schemas/audit.py
duxingchen 1fea30b03b feat(audit): 日活使用统计 + 双 CSV 导出(审计/上下线,列可自定义)
【日活统计:GET /audit/daily-usage】
按【北京时间自然日 × 操作人】聚合,单个 GROUP BY 完成(count(*) FILTER),
不用窗口函数。指标:上线/下线时间、操作次数、登录/登出次数。

⚠️ 上线/下线时间取【当天首次/末次活动】,刻意不取登录时间:
   Refresh Token 有效期 7 天,用户不必每天重新登录。按登录算会出现
   「登录次数 0、上线时间空,但操作次数 35」——报表自相矛盾。
   时间一律按 +08:00 分日与渲染,否则早班(00:00~08:00)操作会掉到前一天。

【CSV 导出:两个端点 + 列自定义】
- /audit/logs/export        整体审计导出,筛选维度与列表页完全一致
- /audit/daily-usage/export 上下线导出,每人一行
- 列清单由后端统一维护并经 /audit/options 下发(log_export_columns /
  usage_export_columns),前端不硬编码表头,避免两端漂移
- ⚠️ 响应带 UTF-8 BOM:Excel 靠它识别编码,否则中文表头全乱码
- 单次上限 5 万行,超出经 X-Export-Truncated 头告知前端明确提示
  (静默截断比报错更危险)
- list_audit_logs 与 export_audit_logs 共用 _log_filters,
  保证「看到的」与「导出的」永远是同一批数据

【前端】
- 审计页页头新增「人员统计」「导出 CSV」两个按钮,现有表格与筛选零改动
- 人员统计走抽屉(AuditUsagePanel):日期范围+快捷键、日活表格、
  上下线次数彩色标签、北京时间渲染、导出前弹列勾选面板
- ExportColumnsModal 为两处导出共用,默认全选
2026-09-21 12:06:43 +08:00

89 lines
2.9 KiB
Python
Raw 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.

"""审计日志 Pydantic Schema"""
from __future__ import annotations
import uuid
from datetime import datetime
from pydantic import BaseModel
class AuditLogResponse(BaseModel):
"""单条审计记录"""
id: uuid.UUID
user_id: str | None = None
display_name: str | None = None
role: str | None = None
action: str
action_label: str | None = None # 服务端补的中文标签,避免前端各处硬编码
module: str
module_label: str | None = None
target_type: str | None = None
target_id: str | None = None
target_name: str | None = None
details: dict | None = None
ip_address: str | None = None
user_agent: str | None = None
method: str | None = None
url: str | None = None
status_code: int | None = None
error_message: str | None = None
# 与结构化日志对账用:拿着它就能捞到对应的接口日志
request_id: str | None = None
created_at: datetime
model_config = {"from_attributes": True}
class AuditLogListResponse(BaseModel):
"""审计日志分页列表"""
items: list[AuditLogResponse]
total: int
class DailyUsageRow(BaseModel):
"""某个操作人在某一天的用量汇总(北京时间自然日)"""
day: str # YYYY-MM-DD(北京时间)
user_id: str | None = None
display_name: str | None = None
role: str | None = None
login_count: int = 0 # 登录次数(当天成功登录)
logout_count: int = 0 # 登出次数(当天成功登出)
op_count: int = 0 # 操作次数(当天全部审计记录数)
# ⚠️ 上线/下线时间取【当天首次/末次活动】,不是登录/登出时间:
# token 有效期内(refresh 7 天)用户不会重新登录,按登录算会导致
# 「登录次数 0 但操作 35 次」这种自相矛盾。
first_active_at: datetime | None = None # 上线时间(当天首次活动)
last_active_at: datetime | None = None # 下线时间(当天末次活动)
class DailyUsageResponse(BaseModel):
"""日活 / 使用统计"""
start_date: str
end_date: str
items: list[DailyUsageRow]
total: int # 行数(= 天数 × 人数),不是审计记录数
class AuditOption(BaseModel):
"""筛选项(value/label 结构,直接喂给前端下拉)"""
value: str
label: str
class AuditOptionsResponse(BaseModel):
"""筛选项集合"""
modules: list[AuditOption]
actions: list[AuditOption]
# 导出可选的列(value=后端列 key,label=中文表头)。
# 由后端下发而非前端硬编码:列的中文名与取值口径都在后端,
# 两端各写一份迟早会出现"导出的列和页面上的对不上"。
log_export_columns: list[AuditOption] = []
usage_export_columns: list[AuditOption] = []