From 72a9b8e3932a0cea3680acbe5866fb2c159db42f Mon Sep 17 00:00:00 2001 From: wren <“porlong@qq.com”> Date: Mon, 27 Apr 2026 16:58:19 +0800 Subject: [PATCH] chore: commit .claude/ project-level config and skills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove .claude/ from .gitignore — project-level Claude Code configuration should be shared with the team. --- .claude/skills/coding-standards.md | 870 +++++++++++++++++++++++++++++ .gitignore | 5 +- 2 files changed, 872 insertions(+), 3 deletions(-) create mode 100644 .claude/skills/coding-standards.md diff --git a/.claude/skills/coding-standards.md b/.claude/skills/coding-standards.md new file mode 100644 index 0000000..c7ce306 --- /dev/null +++ b/.claude/skills/coding-standards.md @@ -0,0 +1,870 @@ +--- +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 ← 小驼峰 +``` diff --git a/.gitignore b/.gitignore index 692ecba..9168963 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,5 @@ coverage.xml # Rules cache rules/**/__pycache__/ -# Claude -.claude/ -CLAUDE.md +# Claude (committed — project-level config) +# CLAUDE.md is committed