---
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      ← 小驼峰
```
