--- name: coding-standards description: 项目编码规范。在编写任何业务代码(控制器、服务层、模型、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 → Service(IXxxService 接口 + 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 | **禁止** | **禁止** | **禁止** | - **DTO**(Data Transfer Object):请求入参,**只允许出现在控制器层**。控制器负责从 DTO 中取值,以原始类型传给服务层 - **VO**(View Object):响应出参,**由服务层负责组装并返回**。控制器通过 `response_model=Result[list[XxxVO]]` 显式声明响应模型 - **BO**(Business Object):业务对象,当服务层需要向 Task / Handler 等其他层级传递数据时,必须用 BO 包裹做验证 - **Model 层充血模型**:字段定义 + 数据库操作方法(`@classmethod`),**禁止引用 DTO/VO/BO,禁止转换方法**(如 `ToVo`、`FromDto` 等) - **服务层入参使用原始类型**(str、int 等),控制器负责 DTO → 原始类型的拆解;**服务层返回值使用 VO**(对接控制器)或 **BO**(对接 Task/Handler) ## 导入规范 **所有导入必须使用完整路径**,禁止裸名导入(如 `from models import User`、`from domian.Dto import XxxDTO`)。即使 `sys.path` 中已包含模块目录,也必须写完整的包路径,确保代码可读性和 IDE 可追踪。 ```python # 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` 为实际业务模块名) - 业务分层包以裸名导入:`controllers`、`services`、`models`、`domian`、`handler`、`tasks` - `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` 直接获取,不需要额外方法 ```python # 示例 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 在控制器层拆解为原始类型传入 Service,Service 返回 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()) ``` ```python # 示例 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)。** ```python # 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`,继承对应接口。**方法数必须与接口层严格一致,不允许多余的辅助方法。** ```python # 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`),不能直接继承 `Base`。`BaseModel` 是抽象基类,自动提供三个公共时间字段: - `create_time`:INSERT 时由数据库写入当前时间 - `update_time`:INSERT 和 UPDATE 时自动更新 - `delete_time`:默认 NULL,非 NULL 表示已软删除 **模型是充血模型**,包含字段定义 + 数据库操作方法(`@classmethod` 查询/写入)。但**禁止引用 DTO/VO/BO,禁止转换方法**(如 `ToVo`、`FromDto` 等)。方法参数和返回值只使用原始类型或模型实例。VO 的组装由服务层负责。 ```python # 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 模块异常(大驼峰文件名) └── ... ``` ### 异常类定义 ```python # fastapi_common/fastapi_common_web/exception/InterceptorException.py """拦截器模块异常。""" from fastapi_common.fastapi_common_web.exception.Base.BusinessException import BusinessException class InterceptorException(BusinessException): """拦截器模块业务异常。""" ``` ### 使用方式 ```python # 在服务层实现中使用模块自己的异常类 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 中取值,以原始类型传给服务层。**字段必须小驼峰命名。** ```python # 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 字段统一为小驼峰。 ```python # 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。 ```python # 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)**——如 `userId`、`userName`、`createdAt`,禁止蛇形命名(`user_id`)或大驼峰命名(`UserId`) ## 统一响应格式 所有接口统一返回 `Result` 结构。HTTP 标准码条目的 HTTP 响应码为真实状态码,纯业务码条目 HTTP 响应码为 200。 ```python 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)) ``` ## 日志规范 ```python 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.log`(WARNING+),按天滚动 + 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 配置文件格式 ```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`) ```python # 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` 会被自动导出为模块级变量,业务代码直接导入使用: ```python # 任何模块中直接导入配置值 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` 必须同步维护,为每个导出变量提供类型声明: ```python # 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 必须同时添加 **两级** 路径,缺一不可: ```python _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 IAuthService`、`from 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` 列表中添加入口: ```python 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` 中定义带 `dependencies` 的 `router`: ```python """控制器包(需要 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: ```python """认证控制器包(无鉴权)。""" 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.py`、`authServiceImpl.py` - **DTO/VO/BO 的 Pydantic 字段名**:`userId`、`userName`、`createdAt`(禁止蛇形 `user_id` 或大驼峰 `UserId`) **类名使用大驼峰命名(UpperCamelCase)**:`UserInfoController`、`LoginTokenVO`、`AuthServiceImpl` ``` 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 ``` ```python # 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_common`、`fastapi_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 ← 小驼峰 ```