Files
leaudit-platform-backend/.claude/skills/coding-standards.md
T
wren 72a9b8e393 chore: commit .claude/ project-level config and skills
Remove .claude/ from .gitignore — project-level Claude Code
configuration should be shared with the team.
2026-04-27 16:58:19 +08:00

35 KiB
Raw Blame History

name, description
name description
coding-standards 项目编码规范。在编写任何业务代码(控制器、服务层、模型、DTO/VO、配置)时必须遵循此规范。当用户要求"新增功能"、"写业务代码"、"添加服务"、"创建模型"、"写接口"、"添加 DTO"等涉及编码实现的场景时,都应使用此技能确保代码风格一致。即使用户没有明确提到"规范",只要是在本项目中编写代码,就应当参照此技能。

项目编码规范

基础约定

  • Python 3.12,使用现代类型语法:list[str]str | None(不用 List[str]Optional[str]
  • 文件编码 UTF-8
  • Ruff 规则:行长 130__init__.py 中忽略 F401
  • 模块顶部第一行写 docstring(三引号),所有类和公开方法都写中文 docstring
  • 完全面向对象编程,不允许类外部的模块级业务函数(配置模块除外)
  • domian 是项目约定拼写(不是 domain),绝对不要"修正"

项目分层架构

Controller → ServiceIXxxService 接口 + XxxServiceImpl 实现)→ Model(充血模型)

每一层的职责清晰:

  • 控制器:负责 HTTP 路由,从 DTO 拆值 → 调 Service → 返回响应。显式声明 response_model
  • 服务层:负责业务编排,调用 Model 的充血方法做持久化,从 Model 实例组装 VO 返回
  • 模型层(充血模型):SQLAlchemy 实体 + 数据库操作方法(@classmethod 查询/写入),禁止引用 DTO/VO/BO,禁止转换方法

DTO / VO / BO 分层规则

层级 DTO VO BO
Controller 入参类型注解 response_model 声明 + 接收 Service 返回
Service 禁止 组装并返回(对接 Controller 组装并返回(对接 Task/Handler
Task / Handler 禁止 禁止 入参接收
Model 禁止 禁止 禁止
  • DTOData Transfer Object):请求入参,只允许出现在控制器层。控制器负责从 DTO 中取值,以原始类型传给服务层
  • VOView Object):响应出参,由服务层负责组装并返回。控制器通过 response_model=Result[list[XxxVO]] 显式声明响应模型
  • BOBusiness Object):业务对象,当服务层需要向 Task / Handler 等其他层级传递数据时,必须用 BO 包裹做验证
  • Model 层充血模型:字段定义 + 数据库操作方法(@classmethod),禁止引用 DTO/VO/BO,禁止转换方法(如 ToVoFromDto 等)
  • 服务层入参使用原始类型(str、int 等),控制器负责 DTO → 原始类型的拆解;服务层返回值使用 VO(对接控制器)或 BO(对接 Task/Handler

导入规范

所有导入必须使用完整路径,禁止裸名导入(如 from models import Userfrom domian.Dto import XxxDTO)。即使 sys.path 中已包含模块目录,也必须写完整的包路径,确保代码可读性和 IDE 可追踪。

# fastapi_common 子模块——完整路径
from fastapi_common.fastapi_common_logger import logger
from fastapi_common.fastapi_common_web.controller import BaseController
from fastapi_common.fastapi_common_web.domain.responses import Result, StatusCodeEnum
from fastapi_common.fastapi_common_web.exception.Base.BusinessException import BusinessException
from fastapi_common.fastapi_common_sqlalchemy.base import Base
from fastapi_common.fastapi_common_sqlalchemy.database import GetAsyncSession

# 业务模块内部——也必须完整路径(以 fastapi_xxx 开头)
from fastapi_admin.config.app import APP_NAME
from fastapi_xxx.models import User
from fastapi_xxx.services import IUserService
from fastapi_xxx.services.impl.userServiceImpl import UserServiceImpl

# DTO 只在控制器层导入,VO 允许在控制器层和服务层导入
from fastapi_xxx.domian.Dto.userDto import UserCreateDTO       # ← 仅 controllers 层
from fastapi_xxx.domian.vo.userVo import UserVO                # ← controllers 层 + services 层

打包与目录约定

  • 业务主包目录:fastapi_modules/fastapi_xxx/xxx 为实际业务模块名)
  • 业务分层包以裸名导入:controllersservicesmodelsdomianhandlertasks
  • pyproject.toml 使用 setuptools.packages.find 自动发现包,不再手工维护 packages = [...]
  • 新增业务模块时,目录命名必须使用下划线(snake_case),禁止连字符(-

控制器层 (fastapi_modules/fastapi_xxx/controllers/)

继承 BaseController,在 Init 中用 @self.router.get/post 注册路由。放入 fastapi_modules/fastapi_xxx/controllers/ 目录后自动被扫描注册,无需手动配置。

控制器类内部除了 Init 外不允许定义任何额外方法(包括私有方法)。控制器只负责:取参数 → 调 Service → 返回响应。

依赖获取方式

服务层依赖有两种来源:

  1. 无状态服务:在控制器 Init 中直接实例化,endpoint 通过闭包访问
  2. 有状态 / lifespan 初始化的服务:在 endpoint 内通过 RequestObj.app.state.xxx 直接获取,不需要额外方法
# 示例 1:无状态服务——闭包访问
# fastapi_modules/fastapi_xxx/controllers/userController.py
"""用户控制器。"""

from fastapi_common.fastapi_common_web.controller import BaseController
from fastapi_common.fastapi_common_web.domain.responses import Result

from domian.Dto import UserCreateDTO
from domian.vo import UserVO
from services import IUserService
from services.impl.userServiceImpl import UserServiceImpl


class UserController(BaseController):
    """用户控制器。"""

    def __init__(self):
        super().__init__(prefix="/user", tags=["用户管理"])
        self.UserService: IUserService = UserServiceImpl()

        @self.router.get("/{UserId}", response_model=Result[list[UserVO]])
        async def GetUser(UserId: int):
            """获取用户详情

            根据用户 ID 查询用户信息。
            """
            # Service 返回 VO
            Data = await self.UserService.GetById(UserId)
            return Result.success(data=Data.model_dump())

        @self.router.post("/", response_model=Result[list[UserVO]])
        async def CreateUser(body: UserCreateDTO):
            """创建用户

            DTO 在控制器层拆解为原始类型传入 ServiceService 返回 VO。
            """
            Data = await self.UserService.Create(
                Name=body.name,
                Account=body.account,
                Password=body.password,
                Phone=body.phone,
            )
            return Result.success(data=Data.model_dump())
# 示例 2:有状态服务——通过 RequestObj.app.state 直接获取
# fastapi_modules/fastapi_xxx/controllers/callbackController.py
"""回调控制器。"""

from fastapi import Query, Request, Response

from fastapi_common.fastapi_common_web.controller import BaseController


class CallbackController(BaseController):
    """回调控制器。"""

    def Init(self):
        super().Init(prefix="/callback", tags=["回调"])

        @self.router.get("/verify")
        async def VerifyCallback(
            RequestObj: Request,
            Signature: str = Query(..., description="签名"),
            Timestamp: str = Query(..., description="时间戳"),
            Nonce: str = Query(..., description="随机数"),
        ):
            """回调 URL 验证"""
            Service = RequestObj.app.state.callback_service
            Result = await Service.Verify(Signature, Timestamp, Nonce)
            return Response(content=Result, media_type="text/plain")

要点:

  • endpoint 函数是普通函数(不带 self),通过闭包访问控制器实例属性
  • 无状态服务在 Init 中直接实例化;有状态服务在 endpoint 内通过 RequestObj.app.state 直接获取
  • 控制器类内部不允许任何额外方法——不需要 _get_xxx 辅助方法,直接在 endpoint 中一行取出即可
  • docstring 第一行 → OpenAPI summary,后续行 → description
  • 所有路由统一挂载在 /api 前缀下

服务层 (fastapi_modules/fastapi_xxx/services/)

这是最核心的规范,必须严格遵守。

服务层采用接口/实现分层架构:

目录结构

fastapi_modules/fastapi_xxx/services/
├── __init__.py
├── userService.py            # IUserService 接口(抽象类)
├── orderService.py           # IOrderService 接口
└── impl/
    ├── __init__.py
    ├── userServiceImpl.py    # UserServiceImpl 实现
    └── orderServiceImpl.py   # OrderServiceImpl 实现

接口层命名:IXxxService

接口层使用 I 前缀,继承 ABC,所有方法标记 @abstractmethod服务层禁止引用 DTO,入参使用原始类型;返回值使用 VO(对接控制器)或 BO(对接 Task/Handler)。

# fastapi_modules/fastapi_xxx/services/userService.py
"""用户服务接口。"""

from abc import ABC, abstractmethod

from domian.vo import UserVO


class IUserService(ABC):
    """用户服务接口。"""

    @abstractmethod
    async def GetById(self, UserId: int) -> UserVO | None:
        """根据 ID 获取用户。"""
        ...

    @abstractmethod
    async def Create(self, Name: str, Account: str, Password: str, Phone: str | None = None) -> UserVO:
        """创建用户。"""
        ...

    @abstractmethod
    async def Update(self, UserId: int, **Fields) -> UserVO:
        """更新用户。"""
        ...

    @abstractmethod
    async def Delete(self, UserId: int) -> bool:
        """删除用户。"""
        ...

实现层命名:XxxServiceImpl

实现层放在 services/impl/ 下,类名为 XxxServiceImpl,继承对应接口。方法数必须与接口层严格一致,不允许多余的辅助方法。

# fastapi_modules/fastapi_xxx/services/impl/userServiceImpl.py
"""用户服务实现。"""

from fastapi_common.fastapi_common_logger import logger
from fastapi_common.fastapi_common_sqlalchemy.database import GetAsyncSession
from fastapi_common.fastapi_common_web.domain.responses import StatusCodeEnum
from fastapi_common.fastapi_common_web.exception.XxxException import XxxException
from fastapi_common.fastapi_common_utils.datetime_utils import FormatDatetime

from domian.vo import UserVO
from models import User
from services import IUserService


class UserServiceImpl(IUserService):
    """用户服务实现。"""

    async def GetById(self, UserId: int) -> UserVO | None:
        """根据 ID 获取用户。"""
        async with GetAsyncSession() as session:
            user = await session.get(User, UserId)
            if not user:
                raise XxxException(StatusCodeEnum.HTTP_404_NOT_FOUND, "用户不存在")
            # Service 层负责从 Model 实例组装 VO
            return UserVO(
                id=user.Id,
                name=user.Name,
                account=user.Account,
                phone=user.Phone,
                status=user.Status,
                createdAt=FormatDatetime(user.create_time) if user.create_time else None,
            )

    async def Create(self, Name: str, Account: str, Password: str, Phone: str | None = None) -> UserVO:
        """创建用户。"""
        async with GetAsyncSession() as session:
            # 调用 Model 充血方法创建用户
            user = await User.create(session, name=Name, account=Account, password=Password, phone=Phone)
            await session.commit()
            logger.info(f"创建用户成功: {user.Id}")
            return UserVO(
                id=user.Id,
                name=user.Name,
                account=user.Account,
                phone=user.Phone,
                status=user.Status,
                createdAt=FormatDatetime(user.create_time) if user.create_time else None,
            )

    async def Update(self, UserId: int, **Fields) -> UserVO:
        """更新用户。"""
        async with GetAsyncSession() as session:
            user = await session.get(User, UserId)
            if not user:
                raise XxxException(StatusCodeEnum.HTTP_404_NOT_FOUND, "用户不存在")
            for FieldName, Value in Fields.items():
                setattr(user, FieldName, Value)
            await session.commit()
            await session.refresh(user)
            return UserVO(
                id=user.Id,
                name=user.Name,
                account=user.Account,
                phone=user.Phone,
                status=user.Status,
                createdAt=FormatDatetime(user.create_time) if user.create_time else None,
            )

    async def Delete(self, UserId: int) -> bool:
        """删除用户。"""
        async with GetAsyncSession() as session:
            user = await session.get(User, UserId)
            if not user:
                raise XxxException(StatusCodeEnum.HTTP_404_NOT_FOUND, "用户不存在")
            await session.delete(user)
            await session.commit()
            logger.info(f"删除用户成功: {UserId}")
            return True

服务层铁律

  1. 接口层定义了多少个方法,实现层就精确对应多少个方法——不能增减
  2. 不允许在实现类中添加任何私有方法(如 _build_query_validate_on_xxx 等),除非用户主动要求。如果逻辑复杂需要拆分,应当拆到独立的工具类或 Model 的方法中;如果需要内部回调,使用 Init 中的闭包函数代替私有方法
  3. 不允许类外部的模块级函数——from fastapi_common.fastapi_common_logger import logger 是唯一的例外
  4. 日志通道由调用栈自动推断,无需手动指定

模型层 (fastapi_modules/fastapi_xxx/models/) — 充血模型

SQLAlchemy 2.x 声明式模型,必须继承 BaseModel(来自 fastapi_common.fastapi_common_web.models),不能直接继承 BaseBaseModel 是抽象基类,自动提供三个公共时间字段:

  • create_timeINSERT 时由数据库写入当前时间
  • update_timeINSERT 和 UPDATE 时自动更新
  • delete_time:默认 NULL,非 NULL 表示已软删除

模型是充血模型,包含字段定义 + 数据库操作方法(@classmethod 查询/写入)。但禁止引用 DTO/VO/BO,禁止转换方法(如 ToVoFromDto 等)。方法参数和返回值只使用原始类型或模型实例。VO 的组装由服务层负责。

# fastapi_modules/fastapi_xxx/models/user.py
"""用户模型。"""

from sqlalchemy import String, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, mapped_column

from fastapi_common.fastapi_common_web.models import BaseModel


class User(BaseModel):
    """用户表(充血模型)。"""

    __tablename__ = "sys_user"

    Id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    TenantId: Mapped[int] = mapped_column(comment="租户ID")
    Name: Mapped[str] = mapped_column(String(100), comment="用户名")
    Account: Mapped[str] = mapped_column(String(100), unique=True, comment="登录账号")
    Password: Mapped[str] = mapped_column(String(255), comment="密码哈希")
    Phone: Mapped[str | None] = mapped_column(String(20), comment="手机号")
    Status: Mapped[int] = mapped_column(default=1, comment="状态: 1启用 0禁用")
    # create_time / update_time / delete_time 自动继承自 BaseModel

    @classmethod
    async def get_by_account(cls, session: AsyncSession, account: str) -> "User | None":
        """按登录账号查询用户。"""
        return await session.scalar(select(cls).where(cls.Account == account))

    @classmethod
    async def create(cls, session: AsyncSession, name: str, account: str, password: str, phone: str | None = None) -> "User":
        """创建用户并刷新返回。"""
        user = cls(Name=name, Account=account, Password=password, Phone=phone)
        session.add(user)
        await session.flush()
        await session.refresh(user)
        return user

模型层铁律

  1. 充血方法只做数据库操作——查询、插入、更新、删除,不包含业务逻辑
  2. 禁止引用 DTO/VO/BO——方法参数使用原始类型(str、int 等)+ session,返回值为模型实例或原始类型
  3. 禁止转换方法——不允许 ToVo()FromDto()UpdateFromDto() 等,VO 组装由服务层负责
  4. 充血方法使用 @classmethod——接收 session 作为第一个参数(除 self 外),不在模型内部管理 session 生命周期

异常层 (fastapi_common/fastapi_common_web/exception/)

每个业务模块必须在 exception/ 下创建自己的异常类,继承 BusinessException,文件名和类名都使用大驼峰命名。模块内抛异常时只使用自己的异常类,最终由全局异常中间件统一捕获并返回 Result 错误响应。

目录结构

fastapi_common/fastapi_common_web/exception/
├── __init__.py
├── Base/
│   ├── __init__.py
│   ├── BusinessException.py      ← 基类,所有模块异常继承它
│   └── status_code.py            ← StatusCode / StatusCodeEnum
├── InterceptorException.py        ← interceptor 模块异常(大驼峰文件名)
├── WxkfException.py               ← wxkf 模块异常(大驼峰文件名)
└── ...

异常类定义

# fastapi_common/fastapi_common_web/exception/InterceptorException.py
"""拦截器模块异常。"""

from fastapi_common.fastapi_common_web.exception.Base.BusinessException import BusinessException


class InterceptorException(BusinessException):
    """拦截器模块业务异常。"""

使用方式

# 在服务层实现中使用模块自己的异常类
from fastapi_common.fastapi_common_web.exception.InterceptorException import InterceptorException
from fastapi_common.fastapi_common_web.exception.Base.status_code import StatusCodeEnum

# 抛异常时使用模块专属异常类,而不是直接用 BusinessException
raise InterceptorException(StatusCodeEnum.HTTP_404_NOT_FOUND, "用户不存在")
raise InterceptorException(StatusCodeEnum.HTTP_401_UNAUTHORIZED, "用户名或密码错误")

异常层铁律

  1. 禁止直接使用 BusinessException 抛异常——每个模块必须用自己的 XxxException
  2. 异常文件放在 fastapi_common/fastapi_common_web/exception/ 下,不放在业务模块内部
  3. 异常类只继承,不添加额外属性或方法(除非有特殊需求)
  4. 全局异常中间件会捕获所有 BusinessException 及其子类,自动转换为 Result 错误响应

DTO/VO/BO (fastapi_modules/fastapi_xxx/domian/)

使用 Pydantic v2 BaseModel。DTO/VO/BO 的字段必须使用小驼峰命名(lowerCamelCase,这是强制规范,与 fastapi_modules 下的文件命名规范一致,也与前端 JSON 字段命名对齐。

DTO(请求入参)— 只在控制器层

DTO 通过 endpoint 函数参数的类型注解声明,FastAPI 自动识别为请求体模型。DTO 不允许进入服务层,控制器负责从 DTO 中取值,以原始类型传给服务层。字段必须小驼峰命名。

# fastapi_modules/fastapi_xxx/domian/Dto/userDto.py
"""用户 DTO。"""

from pydantic import BaseModel, Field


class UserCreateDTO(BaseModel):
    """创建用户请求。"""

    name: str = Field(..., description="用户名", max_length=100)
    account: str = Field(..., description="登录账号", max_length=100)
    password: str = Field(..., description="密码", min_length=6)
    phone: str | None = Field(None, description="手机号")

VO(响应出参)— 控制器层 + 服务层

VO 通过路由装饰器的 response_model=Result[list[XxxVO]] 声明,由服务层组装返回。VO 字段必须使用小驼峰命名(lowerCamelCase,这是强制规范,确保返回给前端的 JSON 字段统一为小驼峰。

# fastapi_modules/fastapi_xxx/domian/vo/userVo.py
"""用户 VO。"""

from pydantic import BaseModel, Field


class UserVO(BaseModel):
    """用户响应。"""

    id: int = Field(..., description="用户ID")
    name: str = Field(..., description="用户名")
    account: str = Field(..., description="登录账号")
    phone: str | None = Field(None, description="手机号")
    status: int = Field(..., description="状态")
    createdAt: str | None = Field(None, description="创建时间")

BO(业务对象)— 用于跨层传递验证

当数据需要从 Service 层传递到 Task / Handler 等其他层级时,必须用 BO 包裹做验证。Service 层内部如果不跨层传递,直接使用原始类型即可,不需要强制使用 BO。

# fastapi_modules/fastapi_xxx/domian/bo/taskSubmitBo.py
"""任务提交 BO。"""

from pydantic import BaseModel, Field


class TaskSubmitBO(BaseModel):
    """任务提交业务对象(Service → Task 跨层传递时使用)。"""

    taskId: int = Field(..., description="任务ID")
    content: str = Field(..., description="提交内容")
    userId: int = Field(..., description="提交用户ID")

DTO/VO/BO 铁律

  1. DTO 只允许出现在控制器层——服务层、模型层、Task/Handler 层禁止引用 DTO
  2. VO 允许在控制器层和服务层使用——服务层负责从 Model 实例组装 VO 并返回给控制器
  3. Model 层充血但不转换——允许数据库操作方法(@classmethod),但禁止引用 DTO/VO/BO,禁止 ToVo()FromDto() 等转换方法
  4. 服务层入参使用原始类型——控制器负责从 DTO 中拆解出原始类型传给服务层
  5. BO 用于跨层传递——Service → Task/Handler 传递数据时必须用 BO 包裹验证,BO 不进入控制器层
  6. 控制器路由必须显式声明 response_model——response_model=Result[list[XxxVO]],确保 OpenAPI 文档准确
  7. DTO/VO/BO 字段必须使用小驼峰命名(lowerCamelCase——如 userIduserNamecreatedAt,禁止蛇形命名(user_id)或大驼峰命名(UserId

统一响应格式

所有接口统一返回 Result 结构。HTTP 标准码条目的 HTTP 响应码为真实状态码,纯业务码条目 HTTP 响应码为 200。

from fastapi_common.fastapi_common_web.domain.responses import Result, PageResult, StatusCodeEnum

# 成功
return Result.success(Data=UserVo)

# 失败(使用预定义状态码,自带默认消息)
return Result.error(StatusCodeEnum.HTTP_404_NOT_FOUND)

# 失败(自定义消息)
return Result.error(StatusCodeEnum.HTTP_400_BAD_REQUEST, "手机号格式不正确")

# 业务异常(使用模块专属异常类,会被全局异常处理器捕获并自动转换为 Result 响应)
from fastapi_common.fastapi_common_web.exception.XxxException import XxxException

raise XxxException(StatusCodeEnum.HTTP_404_NOT_FOUND, "用户不存在")

# 分页响应
return Result.success(Data=PageResult(list=UserList, pagination=PaginationInfo))

日志规范

from fastapi_common.fastapi_common_logger import logger

# 直接使用,通道由调用栈自动推断
logger.info(f"创建用户成功: {user.Id}")
logger.error(f"查询失败: {str(e)}")
logger.warning(f"用户已存在: {Account}")

通道自动推断规则:/controllers/ → CONTROLLER、/services/ → SERVICE、/middleware/ → MIDDLEWARE、/models/ → MODEL、postgrest → POSTGREST、uvicorn → UVICORN、/db/database → DB 等。 日志同时写入 App.log(全量)和 app-error.logWARNING+),按天滚动 + zip 压缩。

配置层 (fastapi_admin/config/)

配置采用 TOML + .env 环境选定 + Pydantic Settings 校验 三层架构:

TOML 文件 → os.environ 注入 → Pydantic Settings 校验 → 模块级变量导出

配置文件结构

backend/
├── .env                          ← 仅选定环境:APP_ENV=development
├── app.toml                      ← 基础公共配置(所有环境通用)
├── app.development.toml          ← 开发环境差异配置
├── app.production.toml           ← 生产环境差异配置
└── app.ai.toml                   ← AI 专用配置(最高优先级)

加载优先级(后覆盖前)

  1. app.toml — 基础配置
  2. app.{APP_ENV}.toml — 环境特定配置
  3. app.ai.toml — AI 专用配置(最高优先级)
  4. Docker -e 等已有环境变量 — 最高优先级(setdefault 不覆盖)

TOML 配置文件格式

# app.toml — 使用 [SECTION] + KEY 结构,展平后变为 SECTION_KEY 环境变量
[APP]
HOST = "0.0.0.0"
PORT = 8000
CORS_ORIGINS = ["*"]

[JWT]
SECRET_KEY = "your-secret"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

[DB]
HOST = "localhost"
PORT = 5432
NAME = "mydb"

Pydantic Settings 定义(_settings.py

# fastapi_admin/config/_settings.py
class _Base(BaseSettings):
    """所有 Settings 的基类。"""
    model_config = {"env_file": None, "extra": "ignore"}

class AppSettings(_Base):
    """应用基础配置。"""
    APP_HOST: str = "0.0.0.0"
    APP_PORT: int = 8000

class DbSettings(_Base):
    """数据库配置。"""
    DB_HOST: str = ""
    DB_PORT: int = 5432
    DB_NAME: str = ""
    DB_USER: str = ""
    DB_PASSWORD: str = ""

    @property
    def ASYNCPG_DATABASE_URL(self) -> str:
        """计算属性:动态构建数据库 URL。"""
        return f"postgresql://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"

模块级变量导出(__init__.py

所有 Settings 实例的字段和 @property 会被自动导出为模块级变量,业务代码直接导入使用:

# 任何模块中直接导入配置值
from fastapi_admin.config import APP_NAME, APP_PORT, JWT_SECRET_KEY, ASYNCPG_DATABASE_URL

# 或从特定子模块代理导入
from fastapi_admin.config.db import ASYNCPG_DATABASE_URL
from fastapi_admin.config.app import APP_PORT

新增配置项流程

  1. app.toml 中添加 [SECTION].KEY(展平后为 SECTION_KEY
  2. _settings.py 中对应的 Settings 类添加字段(含类型和默认值)
  3. 需要环境差异的配置在 app.development.toml / app.production.toml 中覆盖
  4. 敏感值在环境特定 TOML 中配置或通过 Docker -e 注入,不写默认值
  5. 如需计算属性(如 URL 拼接),在 Settings 类中用 @property 定义
  6. 必须在 __init__.pyi 类型存根中声明新变量,否则 IDE 无法识别动态导出的模块级变量

类型存根 (__init__.pyi)

由于 __init__.py 通过 _export_settings() 动态导出变量,IDE 无法静态推断类型。因此 __init__.pyi 必须同步维护,为每个导出变量提供类型声明:

# fastapi_admin/config/__init__.pyi
"""类型存根文件 —— 为 IDE 静态分析提供动态导出变量的类型信息。"""

# APP
APP_ENV: str
APP_NAME: str
APP_PORT: int

# JWT
JWT_SECRET_KEY: str
JWT_ACCESS_TOKEN_EXPIRE_HOURS: int

# DB (PostgreSQL)
DB_HOST: str
DB_PORT: int
ASYNCPG_DATABASE_URL: str    # @property 计算属性也要声明

# 常量
ROOT_PATH: str

铁律:在 _settings.py 中新增字段或 @property 后,必须同步在 __init__.pyi 中添加对应的类型声明,否则 IDE 会报 "Cannot find reference" 错误

新增完整功能模块的清单

当新增一个完整的业务模块(如"用户管理")时,需要创建以下文件:

序号 文件路径 作用
1 fastapi_common/fastapi_common_web/exception/XxxException.py 模块专属异常类(继承 BusinessException
2 fastapi_modules/fastapi_xxx/models/xxx.py 充血模型(字段定义 + 数据库操作方法,禁止引用 DTO/VO/BO)
3 fastapi_modules/fastapi_xxx/domian/Dto/xxxDto.py 请求 DTO(小驼峰文件名)
4 fastapi_modules/fastapi_xxx/domian/vo/xxxVo.py 响应 VO(小驼峰文件名)
5 fastapi_modules/fastapi_xxx/services/xxxService.py 服务接口 IXxxService(小驼峰文件名)
6 fastapi_modules/fastapi_xxx/services/impl/xxxServiceImpl.py 服务实现 XxxServiceImpl(小驼峰文件名)
7 fastapi_modules/fastapi_xxx/controllers/xxxController.py 控制器(小驼峰文件名)

补充规范

以下规范是对通用规范的补充和覆盖,与通用规范冲突时以本节为准

1. 新增业务模块注册流程

新增一个业务模块(如 fastapi_xxx)需要修改三处:

1.1 backend/fastapi_admin/app.py — 添加 sys.path

必须同时添加 两级 路径,缺一不可:

_FASTAPI_MODULES_DIR = _PROJECT_ROOT / "fastapi_modules"                          # 使 importlib 能 import fastapi_xxx.controllers
_FASTAPI_XXX_DIR = _PROJECT_ROOT / "fastapi_modules" / "fastapi_xxx"              # 使模块内部裸名导入生效(from services import ...
  • fastapi_modules 目录:让控制器注册器 importlib.import_module("fastapi_xxx.controllers") 能找到包
  • fastapi_modules/fastapi_xxx 目录:让模块内部的裸名导入生效(from services import IAuthServicefrom domian.Dto import LoginDTO

1.2 backend/pyproject.toml — setuptools include

[tool.setuptools.packages.find]include 列表中添加 "fastapi_xxx*"

1.3 backend/fastapi_admin/bootstrap_parts/controllers.py — 注册控制器包

controller_packages 列表中添加入口:

controller_packages = [
    "fastapi_xxx.controllers",
]

2. controllers 包级鉴权机制

控制器注册器 register_controllers 的扫描逻辑:

  1. 导入 controller_packages 中的每个包
  2. 如果包的 __init__.py 中有 router = APIRouter(...) 变量,将其记录为包级路由器
  3. 递归遍历子包和模块,子包有 router 也会被记录
  4. 注册 BaseController 子类时,通过 resolve_target_router 沿包路径向上查找最近的包级路由器
  5. 包级路由器上的 dependencies 会自动应用到其下所有控制器的路由

需要鉴权的控制器

controllers/__init__.py 中定义带 dependenciesrouter

"""控制器包(需要 JWT 鉴权)。"""

from typing import Any

from fastapi import APIRouter, Depends, Request

from fastapi_common.fastapi_common_security.security import verify_access_token


async def jwt_auth_dependency(RequestObj: Request) -> dict[str, Any]:
    """JWT 鉴权依赖。"""
    return verify_access_token(RequestObj)


router = APIRouter(dependencies=[Depends(jwt_auth_dependency)])

绕过鉴权的子包

在需要免鉴权的子包(如 controllers/auth/)的 __init__.py 中定义一个空 router(无 dependencies),拦截 resolve_target_router 的向上查找,使该子包下的控制器不走父包的鉴权 router:

"""认证控制器包(无鉴权)。"""

from fastapi import APIRouter

router = APIRouter()

原理resolve_target_router 从当前模块的包路径逐级向上查找 package_routers。如果 controllers/auth/__init__.py 没有 router,查找会继续向上命中 controllers/__init__.py 的带鉴权 router,导致 login 等接口也被鉴权拦截。定义空 router 即可在当前层级截断查找。

3. 文件命名规范

fastapi_modules 下的业务模块 — 统一小驼峰文件名

fastapi_modules/ 下所有业务模块统一使用小驼峰命名(lowerCamelCase,包括:

  • .py 文件名userInfoController.pyauthServiceImpl.py
  • DTO/VO/BO 的 Pydantic 字段名userIduserNamecreatedAt(禁止蛇形 user_id 或大驼峰 UserId

类名使用大驼峰命名(UpperCamelCaseUserInfoControllerLoginTokenVOAuthServiceImpl

controllers/
├── __init__.py
├── userInfoController.py     ← 小驼峰
└── auth/
    ├── __init__.py
    └── authController.py     ← 小驼峰

services/
├── __init__.py
├── userService.py            ← 小驼峰(IUserService 接口)
├── impl/
│   └── userServiceImpl.py    ← 小驼峰(UserServiceImpl 实现)
└── auth/
    ├── __init__.py
    ├── authService.py        ← 小驼峰
    └── impl/
        └── authServiceImpl.py

models/
├── __init__.py
└── user.py                   ← 小驼峰(单词本身就一个词的不需要驼峰)

domian/
├── Dto/
│   ├── __init__.py
│   ├── loginDto.py           ← 小驼峰
│   └── refreshTokenDto.py    ← 小驼峰
├── vo/
│   ├── __init__.py
│   ├── loginTokenVo.py       ← 小驼峰
│   └── userInfoVo.py         ← 小驼峰
└── auth/
    ├── Dto/
    │   ├── loginDto.py
    │   └── refreshTokenDto.py
    └── vo/
        └── loginTokenVo.py
# controllers/userInfoController.py  ← 文件名小驼峰
class UserInfoController(BaseController):  # ← 类名大驼峰
    ...

# services/userService.py  ← 文件名小驼峰
class IUserService(ABC):  # ← 类名大驼峰
    ...

# services/impl/userServiceImpl.py  ← 文件名小驼峰
class UserServiceImpl(IUserService):  # ← 类名大驼峰
    ...

# domian/vo/userInfoVo.py  ← 文件名小驼峰
class UserInfoVO(BaseModel):  # ← 类名大驼峰
    ...

fastapi_modules 以外的代码 — Python 蛇形命名

不在 fastapi_modules 下的代码(如 fastapi_commonfastapi_admin 等),除非 skills 中有特定规范,否则必须使用 Python 标准的蛇形命名snake_case)。

4. 业务模块标准目录结构

fastapi_modules/fastapi_xxx/
├── __init__.py
├── controllers/                    ← 包级 __init__.py 挂 JWT 鉴权 router
│   ├── __init__.py                 ← router = APIRouter(dependencies=[Depends(jwt_auth)])
│   ├── userInfoController.py       ← 小驼峰,走包级鉴权
│   └── auth/                       ← 免鉴权子包
│       ├── __init__.py             ← router = APIRouter()(空,拦截向上查找)
│       └── authController.py       ← 小驼峰
├── services/
│   ├── __init__.py                 ← 导出 IUserService
│   ├── userService.py              ← 小驼峰
│   ├── impl/
│   │   └── userServiceImpl.py      ← 小驼峰
│   └── auth/                       ← 认证子模块
│       ├── __init__.py             ← 导出 IAuthService
│       ├── authService.py          ← 小驼峰
│       └── impl/
│           └── authServiceImpl.py
├── models/                         ← 充血模型
│   ├── __init__.py                 ← 导出 User 等模型
│   └── user.py                     ← 小驼峰(单词即一个词时无需驼峰)
├── domian/
│   ├── __init__.py
│   ├── Dto/
│   │   └── __init__.py
│   ├── vo/
│   │   ├── __init__.py
│   │   └── userInfoVo.py           ← 小驼峰
│   ├── bo/                          ← BO(按需,跨层传递时使用)
│   │   ├── __init__.py
│   │   └── taskSubmitBo.py          ← 小驼峰
│   └── auth/                       ← 认证子模块
│       ├── __init__.py
│       ├── Dto/
│       │   ├── __init__.py
│       │   ├── loginDto.py          ← 小驼峰
│       │   └── refreshTokenDto.py   ← 小驼峰
│       └── vo/
│           ├── __init__.py
│           └── loginTokenVo.py      ← 小驼峰