服务、任务、日志与错误处理
Service 层
核心业务逻辑放在 app/service/。路由应该调用 service,而不是直接实现复杂业务。
适合放入 service 的逻辑包括:
- 多表查询和事务编排。
- 缓存读取、回填和失效。
- 邮件、通知、聊天消息、谱面下载、谱面集更新。
- 排行榜、用户统计、每日挑战、重算等领域逻辑。
- 需要复用在路由、任务、脚本之间的流程。
建议以类组织 service,依赖通过构造函数或方法参数传入,不要在全局初始化时创建请求级 session。
python
from app.log import service_logger
class ExampleService:
def __init__(self) -> None:
self.logger = service_logger("ExampleService")
async def run(self) -> None:
self.logger.info("running example service")后台任务
耗时逻辑不要阻塞路由响应:
- 来自 API 路由的后台动作:使用 FastAPI 的
BackgroundTasks。 - 非路由场景:使用
app.helpers.bg_tasks,它提供与 FastAPIBackgroundTasks类似的接口。 - CPU 密集或阻塞库调用:封装到可控的线程池或独立服务,避免直接阻塞事件循环。
python
from fastapi import APIRouter, BackgroundTasks
router = APIRouter(prefix="/api/private/example", tags=["g0v0"])
async def refresh_cache(user_id: int) -> None:
...
@router.post("/refresh-cache/{user_id}")
async def refresh_cache_api(user_id: int, background_tasks: BackgroundTasks):
background_tasks.add_task(refresh_cache, user_id)
return {"status": "queued"}定时任务、启动任务与关闭任务
任务放在 app/tasks/,并通过 __init__.py 导出。需要在应用启动或关闭时执行的任务由 main.py 的 lifespan 调用;定时任务通过 APScheduler 注册。
编写任务时必须满足:
- 幂等。 重复运行不会产生重复数据或错误状态。
- 可观测。 每个退出路径都有日志,尤其是跳过、失败、无数据、部分成功。
- 可重试。 外部网络失败、数据库临时失败应能安全重试。
- 不阻塞。 长耗时批处理要分页、分批、释放连接。
- 有误触发容忍。 cron 任务根据需要设置
misfire_grace_time。
示意:
python
from app.log import task_logger
logger = task_logger("ExampleJob")
async def example_job() -> None:
logger.info("example job started")
try:
...
except Exception:
logger.exception("example job failed")
raise
logger.info("example job finished")日志
按代码位置选择日志入口:
python
from app.log import fetcher_logger, log, service_logger, system_logger, task_logger
log("RouterName").info("router event")
system_logger("Startup").info("system event")
service_logger("RankingService").info("service event")
task_logger("DailyChallenge").info("task event")
fetcher_logger("BeatmapFetcher").info("fetcher event")实践要求:
- 服务端异常记录结构化日志,不把内部细节直接返回给客户端。
- 日志中不要输出密码、token、邮箱验证码、真实
.env值或对象存储密钥。 - 批处理任务应记录输入规模、成功数量、失败数量和耗时。
- 高频路径避免过量 info 日志,必要时使用 debug。
错误处理
核心应用注册了常见异常处理器:
RequestValidationError:返回 422。RequestError:返回结构化error、msg_key和 details。HTTPException:返回对应 status code 和error字段。
客户端错误:
python
from fastapi import HTTPException
if user is None:
raise HTTPException(status_code=404, detail="User not found")需要本地化 key 或结构化细节时:
python
from app.models.error import RequestError
raise RequestError(
status_code=400,
msg_key="invalid_request",
message="Invalid request",
details={"field": "beatmap_id"},
)服务端错误不应转换成 200 或吞掉异常。能恢复的错误应记录并降级;不能恢复的错误应记录后抛出,让上层统一处理。
缓存与失效
缓存相关逻辑优先复用已有 service,例如用户缓存、谱面缓存、排行榜缓存。新增缓存时要明确:
- key 命名。
- TTL。
- 数据来源。
- 失效触发点。
- 旧值是否允许短暂返回。
- 是否需要后台刷新。
对于用户头像、封面、谱面集、在线状态等会被多个路径消费的数据,修改写入逻辑时必须同步检查缓存失效路径。