33b4c734aa
后端: - 新增认证(auth)、任务(tasks)、映射(mappings)、对账(reconciliation)、异常(exceptions)、导出(exports) API - 新增核心模块: database, security, permissions, tenant, exceptions, error_handlers - 新增数据模型: user, company, reconciliation_task, field_mapping, uploaded_file 等 - 新增服务层: ai_recognizer, file_parser, file_storage, mapping, reconciliation 等 - 添加数据库迁移脚本 前端: - 新增登录页面和仪表盘页面 - 新增任务列表、任务详情、字段映射页面 - 新增异常处理页面和规则设置页面 - 新增 API 代理路由 /api/[...path] - 新增 UI 组件库 (button, card, dialog, input, table 等) - 新增 auth 组件 (ProtectedRoute, PermissionGate) - 新增 layout 组件 (Header, Sidebar) - 新增 mapping 组件 (FieldMappingTable, AISuggestionPanel) - 新增 API 客户端和 hooks (useAsync, useToast, usePermission 等) - 新增状态管理 (auth-store, company-store, ui-store) - 集成 Tailwind CSS 和 shadcn/ui 组件库 其他: - 添加 Alembic 数据库迁移配置 - 添加初始化示例数据脚本 - 更新项目文档
180 lines
5.2 KiB
Python
180 lines
5.2 KiB
Python
from datetime import datetime
|
|
from functools import wraps
|
|
from typing import Any, Callable, Optional
|
|
|
|
from fastapi import Request
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.models.audit_log import AuditAction, AuditLog
|
|
from app.schemas.audit_log import AuditLogCreate, AuditLogQuery
|
|
|
|
|
|
class AuditService:
|
|
"""审计日志服务"""
|
|
|
|
@staticmethod
|
|
async def log_action(
|
|
db: AsyncSession,
|
|
company_id: int,
|
|
action: str,
|
|
resource_type: str,
|
|
user_id: Optional[int] = None,
|
|
resource_id: Optional[str] = None,
|
|
details: Optional[dict[str, Any]] = None,
|
|
ip_address: Optional[str] = None,
|
|
user_agent: Optional[str] = None,
|
|
) -> AuditLog:
|
|
"""
|
|
记录审计日志
|
|
|
|
Args:
|
|
db: 数据库会话
|
|
company_id: 企业 ID
|
|
action: 操作类型
|
|
resource_type: 资源类型
|
|
user_id: 用户 ID
|
|
resource_id: 资源 ID
|
|
details: 操作详情
|
|
ip_address: IP 地址
|
|
user_agent: User-Agent
|
|
|
|
Returns:
|
|
创建的审计日志对象
|
|
"""
|
|
log_data = AuditLogCreate(
|
|
company_id=company_id,
|
|
user_id=user_id,
|
|
action=action,
|
|
resource_type=resource_type,
|
|
resource_id=resource_id,
|
|
details=details,
|
|
ip_address=ip_address,
|
|
user_agent=user_agent,
|
|
)
|
|
|
|
audit_log = AuditLog(**log_data.model_dump())
|
|
db.add(audit_log)
|
|
await db.commit()
|
|
await db.refresh(audit_log)
|
|
return audit_log
|
|
|
|
@staticmethod
|
|
async def get_logs(
|
|
db: AsyncSession,
|
|
query_params: AuditLogQuery,
|
|
) -> list[AuditLog]:
|
|
"""
|
|
查询审计日志
|
|
|
|
Args:
|
|
db: 数据库会话
|
|
query_params: 查询参数
|
|
|
|
Returns:
|
|
审计日志列表
|
|
"""
|
|
stmt = select(AuditLog)
|
|
|
|
# 添加过滤条件
|
|
if query_params.company_id:
|
|
stmt = stmt.where(AuditLog.company_id == query_params.company_id)
|
|
if query_params.user_id:
|
|
stmt = stmt.where(AuditLog.user_id == query_params.user_id)
|
|
if query_params.action:
|
|
stmt = stmt.where(AuditLog.action == query_params.action)
|
|
if query_params.resource_type:
|
|
stmt = stmt.where(AuditLog.resource_type == query_params.resource_type)
|
|
if query_params.resource_id:
|
|
stmt = stmt.where(AuditLog.resource_id == query_params.resource_id)
|
|
if query_params.start_date:
|
|
stmt = stmt.where(AuditLog.created_at >= query_params.start_date)
|
|
if query_params.end_date:
|
|
stmt = stmt.where(AuditLog.created_at <= query_params.end_date)
|
|
|
|
# 排序和分页
|
|
stmt = stmt.order_by(AuditLog.created_at.desc())
|
|
stmt = stmt.offset(query_params.skip).limit(query_params.limit)
|
|
|
|
result = await db.execute(stmt)
|
|
return list(result.scalars().all())
|
|
|
|
@staticmethod
|
|
async def log_from_request(
|
|
db: AsyncSession,
|
|
request: Request,
|
|
company_id: int,
|
|
action: str,
|
|
resource_type: str,
|
|
user_id: Optional[int] = None,
|
|
resource_id: Optional[str] = None,
|
|
details: Optional[dict[str, Any]] = None,
|
|
) -> AuditLog:
|
|
"""
|
|
从 Request 对象中提取信息并记录审计日志
|
|
|
|
Args:
|
|
db: 数据库会话
|
|
request: FastAPI 请求对象
|
|
company_id: 企业 ID
|
|
action: 操作类型
|
|
resource_type: 资源类型
|
|
user_id: 用户 ID
|
|
resource_id: 资源 ID
|
|
details: 操作详情
|
|
|
|
Returns:
|
|
创建的审计日志对象
|
|
"""
|
|
# 提取 IP 地址
|
|
ip_address = request.client.host if request.client else None
|
|
|
|
# 提取 User-Agent
|
|
user_agent = request.headers.get("user-agent")
|
|
|
|
return await AuditService.log_action(
|
|
db=db,
|
|
company_id=company_id,
|
|
action=action,
|
|
resource_type=resource_type,
|
|
user_id=user_id,
|
|
resource_id=resource_id,
|
|
details=details,
|
|
ip_address=ip_address,
|
|
user_agent=user_agent,
|
|
)
|
|
|
|
|
|
# 单例实例
|
|
audit_service = AuditService()
|
|
|
|
|
|
def audit_log(
|
|
action: str,
|
|
resource_type: str,
|
|
get_resource_id: Optional[Callable] = None,
|
|
):
|
|
"""
|
|
审计日志装饰器
|
|
|
|
Args:
|
|
action: 操作类型
|
|
resource_type: 资源类型
|
|
get_resource_id: 从函数返回值中获取 resource_id 的函数
|
|
|
|
Example:
|
|
@audit_log(action=AuditAction.CREATE, resource_type="company")
|
|
async def create_company(...):
|
|
...
|
|
"""
|
|
def decorator(func: Callable):
|
|
@wraps(func)
|
|
async def wrapper(*args, **kwargs):
|
|
result = await func(*args, **kwargs)
|
|
|
|
# TODO: 在实现认证后,从上下文获取 company_id 和 user_id
|
|
# 目前暂时跳过实际记录
|
|
|
|
return result
|
|
return wrapper
|
|
return decorator |