"""用户操作日志上报:仅特定账号(如 256kb@gmail.com)开启,用于排查 bug""" import logging from fastapi import APIRouter, Depends, Request from pydantic import BaseModel, Field from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.deps import Identity, require_user from app.core import errors from app.core.response import BizException, ok from app.models.user import User from app.models.user_action_log import UserActionLog logger = logging.getLogger(__name__) router = APIRouter() # 仅这些邮箱的用户允许上报操作日志(白名单,避免给所有用户上传) ALLOWED_EMAILS = {"256kb@gmail.com"} # 单条 detail 最大长度(防御性截断) MAX_DETAIL_LEN = 800 # 单次批量最大条数 MAX_BATCH = 100 class ActionLogItem(BaseModel): action: str = Field(..., max_length=50) page: str = Field("", max_length=100) detail: str = Field("", max_length=MAX_DETAIL_LEN) device_info: str = Field("", max_length=200) app_version: str = Field("", max_length=20) # 客户端本地时间戳(毫秒),用于在服务端时钟漂移时仍能排序 client_ts: int = 0 class ActionLogBatch(BaseModel): items: list[ActionLogItem] = Field(..., max_length=MAX_BATCH) @router.post("/logs/action") async def report_action_log( body: ActionLogBatch, request: Request, db: AsyncSession = Depends(get_db), identity: Identity = Depends(require_user), ): """接收 App 上报的用户操作日志。仅白名单邮箱可上报,其他账号返回 403 不上报""" user = (await db.execute( select(User).where(User.id == identity.user_id) )).scalar_one_or_none() if not user or user.email not in ALLOWED_EMAILS: # 静默拒绝:不报错,不写入,避免在普通用户端暴露此通道存在 raise BizException(errors.FORBIDDEN, "forbidden") rows = [ UserActionLog( user_id=user.id, action=item.action[:50], page=item.page[:100], detail=item.detail[:MAX_DETAIL_LEN], device_info=item.device_info[:200], app_version=item.app_version[:20], ) for item in body.items ] db.add_all(rows) await db.commit() logger.info( "user_action_log user=%s count=%s ip=%s", user.id, len(rows), request.client.host if request.client else "-", ) return ok({"received": len(rows)})