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

871 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 → 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 | **禁止** | **禁止** | **禁止** |
- **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 ← 小驼峰
```