From e8a93f25a66ac6b21ffbba8ffb54c67484d002ed Mon Sep 17 00:00:00 2001 From: wren <“porlong@qq.com”> Date: Sat, 9 May 2026 20:07:44 +0800 Subject: [PATCH] feat(audit): record login events and trigger users --- .../controllers/auditController.py | 8 +- .../controllers/auth/authController.py | 142 +++++++++++++++++- .../fastapi_leaudit/services/auditService.py | 1 + .../services/impl/auditServiceImpl.py | 4 +- .../services/impl/documentServiceImpl.py | 1 + 5 files changed, 152 insertions(+), 4 deletions(-) diff --git a/fastapi_modules/fastapi_leaudit/controllers/auditController.py b/fastapi_modules/fastapi_leaudit/controllers/auditController.py index 92c5ebc..05ec913 100644 --- a/fastapi_modules/fastapi_leaudit/controllers/auditController.py +++ b/fastapi_modules/fastapi_leaudit/controllers/auditController.py @@ -1,5 +1,10 @@ """评查控制器。""" +from typing import Any + +from fastapi import Depends + +from fastapi_common.fastapi_common_security.security import verify_access_token from fastapi_common.fastapi_common_web.controller import BaseController from fastapi_common.fastapi_common_web.domain.responses import Result @@ -17,7 +22,7 @@ class AuditController(BaseController): self.AuditService: IAuditService = AuditServiceImpl() @self.router.post("/run", response_model=Result[AuditRunVO]) - async def RunAudit(body: AuditRunDTO): + async def RunAudit(body: AuditRunDTO, payload: dict[str, Any] = Depends(verify_access_token)): """触发文档评查 对指定文档执行 LeAudit 完整评查链路。 @@ -27,6 +32,7 @@ class AuditController(BaseController): RuleType=body.ruleType, Force=body.force, Speed=body.speed, + TriggerUserId=int(payload["user_id"]), ) return Result.success(data=run) diff --git a/fastapi_modules/fastapi_leaudit/controllers/auth/authController.py b/fastapi_modules/fastapi_leaudit/controllers/auth/authController.py index 6270de1..8f4142a 100644 --- a/fastapi_modules/fastapi_leaudit/controllers/auth/authController.py +++ b/fastapi_modules/fastapi_leaudit/controllers/auth/authController.py @@ -22,6 +22,7 @@ from fastapi_common.fastapi_common_security.security import verify_access_token from fastapi_modules.fastapi_leaudit.domian.Dto.auth.loginDto import PasswordLoginDTO from fastapi_modules.fastapi_leaudit.services import IAuthService from fastapi_modules.fastapi_leaudit.services.impl.authServiceImpl import AuthServiceImpl +from fastapi_modules.fastapi_leaudit.services.impl.usageStatsServiceImpl import UsageStatsServiceImpl class AuthController(BaseController): @@ -30,6 +31,44 @@ class AuthController(BaseController): def __init__(self): super().__init__(prefix="/auth", tags=["认证"]) self.AuthService: IAuthService = AuthServiceImpl() + self.UsageStatsService = UsageStatsServiceImpl() + + def _extract_client_ip(request_obj: Request) -> str | None: + forwarded_for = request_obj.headers.get("x-forwarded-for", "").strip() + if forwarded_for: + return forwarded_for.split(",")[0].strip() or None + real_ip = request_obj.headers.get("x-real-ip", "").strip() + if real_ip: + return real_ip or None + if request_obj.client and request_obj.client.host: + return request_obj.client.host + return None + + def _extract_user_agent(request_obj: Request) -> str | None: + user_agent = request_obj.headers.get("user-agent", "").strip() + return user_agent or None + + async def _record_login_event( + *, + request_obj: Request, + user_info: dict[str, Any] | None, + sub: str | None, + login_result: str, + login_type: str, + failure_reason: str | None = None, + ) -> None: + try: + await self.UsageStatsService.RecordLoginEvent( + UserInfo=user_info, + Sub=sub, + LoginResult=login_result, + LoginType=login_type, + IpAddress=_extract_client_ip(request_obj), + UserAgent=_extract_user_agent(request_obj), + FailureReason=failure_reason, + ) + except Exception as record_error: + logger.warning("记录登录审计事件失败: %s", record_error) @self.router.post("/login") async def Login(RequestObj: Request): @@ -41,12 +80,16 @@ class AuthController(BaseController): """ try: requestData = await RequestObj.json() + sub: str | None = None + login_type = "password" if "userInfo" in requestData and isinstance(requestData["userInfo"], dict) and "sub" in requestData["userInfo"]: logger.info("检测到 OAuth 登录请求") ui = requestData["userInfo"] + sub = ui["sub"] + login_type = "oauth" vo = await self.AuthService.OAuthLogin( - Sub=ui["sub"], + Sub=sub, Username=ui.get("username"), Nickname=ui.get("nickname"), Email=ui.get("email"), @@ -59,16 +102,32 @@ class AuthController(BaseController): ) elif "username" in requestData and "password" in requestData: logger.info(f"检测到密码登录请求 - username={requestData['username']}") + sub = str(requestData["username"]) vo = await self.AuthService.PasswordLogin( - Sub=requestData["username"], + Sub=sub, Password=requestData["password"], ) else: + await _record_login_event( + request_obj=RequestObj, + user_info=None, + sub=None, + login_result="failed", + login_type="unknown", + failure_reason="invalid_request", + ) return JSONResponse( status_code=400, content={"success": False, "message": "无效的登录请求格式", "data": None}, ) + await _record_login_event( + request_obj=RequestObj, + user_info=vo.user_info if isinstance(vo.user_info, dict) else None, + sub=sub, + login_result="success", + login_type=login_type, + ) return JSONResponse(status_code=200, content={ "success": True, "message": "ok", @@ -83,12 +142,56 @@ class AuthController(BaseController): except LeauditException as e: logger.error(f"登录失败: {e.message}") + request_data = {} + try: + request_data = await RequestObj.json() + except Exception: + request_data = {} + request_user_info = request_data.get("userInfo") if isinstance(request_data.get("userInfo"), dict) else None + request_sub = None + login_type = "unknown" + if request_user_info and request_user_info.get("sub"): + request_sub = str(request_user_info["sub"]) + login_type = "oauth" + elif request_data.get("username"): + request_sub = str(request_data["username"]) + login_type = "password" + await _record_login_event( + request_obj=RequestObj, + user_info=None, + sub=request_sub, + login_result="failed", + login_type=login_type, + failure_reason=e.message, + ) return JSONResponse( status_code=e.status.value, content={"success": False, "message": e.message, "data": None}, ) except Exception as e: logger.error(f"登录失败: {e}") + request_data = {} + try: + request_data = await RequestObj.json() + except Exception: + request_data = {} + request_user_info = request_data.get("userInfo") if isinstance(request_data.get("userInfo"), dict) else None + request_sub = None + login_type = "unknown" + if request_user_info and request_user_info.get("sub"): + request_sub = str(request_user_info["sub"]) + login_type = "oauth" + elif request_data.get("username"): + request_sub = str(request_data["username"]) + login_type = "password" + await _record_login_event( + request_obj=RequestObj, + user_info=None, + sub=request_sub, + login_result="failed", + login_type=login_type, + failure_reason=str(e), + ) return JSONResponse(status_code=401, content={ "success": False, "message": "登录失败,请稍后重试", "data": None, }) @@ -100,17 +203,52 @@ class AuthController(BaseController): requestData = await RequestObj.json() dto = PasswordLoginDTO(**requestData) vo = await self.AuthService.PasswordLogin(Sub=dto.sub, Password=dto.password) + await _record_login_event( + request_obj=RequestObj, + user_info=vo.user_info if isinstance(vo.user_info, dict) else None, + sub=dto.sub, + login_result="success", + login_type="password", + ) return JSONResponse(status_code=200, content={ "success": True, "message": "ok", "data": vo.model_dump(), }) except LeauditException as e: logger.error(f"密码登录失败: {e.message}") + request_sub = None + try: + request_data = await RequestObj.json() + request_sub = request_data.get("sub") or request_data.get("username") + except Exception: + request_sub = None + await _record_login_event( + request_obj=RequestObj, + user_info=None, + sub=str(request_sub) if request_sub else None, + login_result="failed", + login_type="password", + failure_reason=e.message, + ) return JSONResponse( status_code=e.status.value, content={"success": False, "message": e.message, "data": None}, ) except Exception as e: logger.error(f"密码登录失败: {e}") + request_sub = None + try: + request_data = await RequestObj.json() + request_sub = request_data.get("sub") or request_data.get("username") + except Exception: + request_sub = None + await _record_login_event( + request_obj=RequestObj, + user_info=None, + sub=str(request_sub) if request_sub else None, + login_result="failed", + login_type="password", + failure_reason=str(e), + ) return JSONResponse(status_code=401, content={ "success": False, "message": "登录失败,请稍后重试", "data": None, }) diff --git a/fastapi_modules/fastapi_leaudit/services/auditService.py b/fastapi_modules/fastapi_leaudit/services/auditService.py index deb48b4..451cb8e 100644 --- a/fastapi_modules/fastapi_leaudit/services/auditService.py +++ b/fastapi_modules/fastapi_leaudit/services/auditService.py @@ -15,6 +15,7 @@ class IAuditService(ABC): RuleType: str | None = None, Force: bool = False, Speed: str = "normal", + TriggerUserId: int | None = None, ) -> AuditRunVO: """触发文档评查。""" ... diff --git a/fastapi_modules/fastapi_leaudit/services/impl/auditServiceImpl.py b/fastapi_modules/fastapi_leaudit/services/impl/auditServiceImpl.py index 4b60828..b3573c7 100644 --- a/fastapi_modules/fastapi_leaudit/services/impl/auditServiceImpl.py +++ b/fastapi_modules/fastapi_leaudit/services/impl/auditServiceImpl.py @@ -111,13 +111,14 @@ class AuditServiceImpl(IAuditService): RuleType: str | None = None, Force: bool = False, Speed: str = "normal", + TriggerUserId: int | None = None, ) -> AuditRunVO: """触发文档评查。 当前阶段只负责创建 run 并投递 worker,不在 HTTP 请求内同步执行。 """ async with GetAsyncSession() as session: - logger.info(f"触发评查: documentId={DocumentId}, ruleType={RuleType}") + logger.info(f"触发评查: documentId={DocumentId}, ruleType={RuleType}, triggerUserId={TriggerUserId}") normalizedSpeed = _normalize_speed(Speed) await session.execute( text( @@ -205,6 +206,7 @@ class AuditServiceImpl(IAuditService): documentFileId=documentFile.Id, runNo=int(latestRunNo) + 1, triggerSource=triggerSource, + triggerUserId=TriggerUserId, status="queued", phase="dispatch", ruleSetId=int(binding["rule_set_id"]), diff --git a/fastapi_modules/fastapi_leaudit/services/impl/documentServiceImpl.py b/fastapi_modules/fastapi_leaudit/services/impl/documentServiceImpl.py index 8d8e213..30c498c 100644 --- a/fastapi_modules/fastapi_leaudit/services/impl/documentServiceImpl.py +++ b/fastapi_modules/fastapi_leaudit/services/impl/documentServiceImpl.py @@ -261,6 +261,7 @@ class DocumentServiceImpl(IDocumentService): DocumentId=document.Id, Speed=Speed, Force=duplicateUpload, + TriggerUserId=CreatedBy, ) processingStatus = "running" if run.status in {"pending", "running"} else run.status