Files
live-hub-py/web/backend/routers/audit.py
T

131 lines
4.8 KiB
Python

"""审计日志查询接口,仅超级管理员可访问。"""
from __future__ import annotations
import json
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from ..database import get_db
from ..deps import get_current_user
from ..models import Account, AuditLog, DouyuTask, User
router = APIRouter(prefix="/api/audit-logs", tags=["审计日志"])
def require_super_admin(current: User = Depends(get_current_user)) -> User:
"""审计日志属于高敏感运维数据,只允许超级管理员查看。"""
if current.role != "super_admin":
raise HTTPException(status_code=403, detail="仅超级管理员可查看审计日志")
return current
def _audit_log_out(db: Session, row: AuditLog) -> dict:
"""序列化审计记录,并为旧斗鱼直充批次补齐可安全展示的账号身份。"""
detail_text = row.detail or ""
if row.action == "recharge:douyu:create" and row.target.startswith("douyu_batch:"):
try:
detail = json.loads(detail_text) if detail_text else {}
except json.JSONDecodeError:
detail = None
if isinstance(detail, dict) and not detail.get("recharge_accounts"):
batch_id = row.target.removeprefix("douyu_batch:")
accounts = (
db.query(DouyuTask, Account)
.join(Account, Account.id == DouyuTask.account_id)
.filter(
DouyuTask.batch_id == batch_id,
DouyuTask.task_type == "create_gold_qr",
)
.order_by(DouyuTask.id.asc())
.limit(100)
.all()
)
if accounts:
detail["recharge_accounts"] = [
{
"task_id": task.id,
"username": account.username,
"douyu_uid": account.uid or "",
"douyu_nickname": account.nickname or "",
}
for task, account in accounts
]
detail["recharge_accounts_truncated"] = len(accounts) == 100
if isinstance(detail, dict) and not detail.get("payment_method"):
batch_id = row.target.removeprefix("douyu_batch:")
task = (
db.query(DouyuTask)
.filter(
DouyuTask.batch_id == batch_id,
DouyuTask.task_type == "create_gold_qr",
)
.order_by(DouyuTask.id.asc())
.first()
)
task_result = task.result if task and isinstance(task.result, dict) else {}
recharge_channel = str(task_result.get("recharge_channel") or "wechat_qr")
detail["recharge_channel"] = recharge_channel
detail["payment_method"] = (
"API 直充支付" if recharge_channel == "supplier_api" else "微信扫码支付"
)
if isinstance(detail, dict):
detail_text = json.dumps(detail, ensure_ascii=False, separators=(",", ":"))
return {
"id": row.id,
"user_id": row.user_id,
"username": row.username or "",
"action": row.action,
"target": row.target or "",
"detail": detail_text,
"success": row.success,
"created_at": row.created_at,
}
@router.get("")
def list_audit_logs(
page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=200),
username: str | None = Query(None, max_length=64),
action: str | None = Query(None, max_length=128),
keyword: str | None = Query(None, max_length=128),
success: bool | None = Query(None),
start_time: datetime | None = Query(None),
end_time: datetime | None = Query(None),
db: Session = Depends(get_db),
_: User = Depends(require_super_admin),
):
"""分页查询审计记录,不提供修改和删除能力。"""
query = db.query(AuditLog)
if username:
query = query.filter(AuditLog.username.ilike(f"%{username.strip()}%"))
if action:
query = query.filter(AuditLog.action == action.strip())
if keyword:
pattern = f"%{keyword.strip()}%"
query = query.filter(
(AuditLog.target.ilike(pattern)) | (AuditLog.detail.ilike(pattern))
)
if success is not None:
query = query.filter(AuditLog.success == success)
if start_time:
query = query.filter(AuditLog.created_at >= start_time)
if end_time:
query = query.filter(AuditLog.created_at <= end_time)
total = query.count()
rows = (
query.order_by(AuditLog.id.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
return {
"items": [_audit_log_out(db, row) for row in rows],
"total": total,
"page": page,
"page_size": page_size,
}