---
url: /docs/ai-contexts/module-conventions.md
---
# 模块约定

> 📍 DDD 4 层 + 跨模块共享 + 命名规范
> 给 Claude / Cursor / Cody 等 AI 生成代码时遵守

***

## 1. DDD 4 层目录结构（强制）

```
src/app/<module>/
├── __init__.py
│
├── domain/                         # 纯领域（零运行时依赖）
│   ├── entities/                   # 实体（有 id 的领域对象）
│   ├── value_objects/              # 值对象（无 id 的不可变对象）
│   ├── events/                     # 领域事件（PascalCase PastTense）
│   ├── services/                   # 领域服务（纯函数）
│   └── exceptions.py                # 业务异常 + 错误码 enum
│
├── application/                    # 用例编排
│   ├── ports/                      # 端口契约（abstract class）
│   ├── usecase/                    # 1 action = 1 class（execute 方法）
│   └── factories/                  # create_<module>_usecases() 工厂
│
├── infrastructure/                 # 适配器实现
│   ├── persistence/                # SQLModel + Redis 仓储
│   ├── adapters/                   # 外部服务适配器（SMS/WeChat/JWT/Captcha）
│   └── eventbus/                   # 领域事件 → Celery chain 事件总线
│
└── interface/                      # FastAPI 路由层
    ├── api/                        # FastAPI router
    ├── schemas/                    # Pydantic v2 IO 契约
    ├── dependencies.py             # FastAPI Depends
    ├── middleware.py               # 中间件
    └── handlers.py                 # ExceptionHandler
```

***

## 2. DDD 4 层依赖规则（CI 卡控）

| 层 | 允许依赖 | 禁止依赖 |
|---|---|---|
| **Domain** | 纯 Python 类型；其他 Domain 模块 Entity/VO/Event | SQLModel / Redis / FastAPI / Pydantic runtime schema / 任何 IO |
| **Application** | Domain + Infrastructure 接口（port）+ Pydantic schema | FastAPI / SQLModel client / Redis client |
| **Infrastructure** | Application 接口 + SQLModel/Redis/三方 SDK | Domain 反向依赖 / FastAPI 路由 |
| **Interface** | Application + Pydantic + FastAPI 类型 | SQLModel client / 任何持久化操作 |

**CI 卡控脚本**：

```python
# scripts/check_layers.py（参见 docs/internal-wiki/architecture/ddd-layers.md §2）
```

***

## 3. 模块命名规范

### 3.1 模块路径（顶级）

| 类别 | 命名 | 示例 |
|---|---|---|
| 鉴权 | `<verb>` | `auth`, `oauth` |
| 用户 | `<noun>` | `user` |
| 业务领域 | `<noun>` | `wallet`, `goal`, `training` |
| 设备/健康 | `<noun>` | `device`, `polar`, `garmin-cn` |
| 内容/社交 | `<noun>` | `feed`, `follow`, `family` |
| AI/三方 | `<noun>` | `ai-coach`, `image-gen`, `ludong` |
| MIS | `admin` | `admin` |
| 内容 | `<noun>` | `cms`, `content`, `ranking` |
| 电商 | `<noun>` | `cart`, `points`, `mall`, `recipe` |
| 支付 | `<noun>` | `wxpay` |
| 上传 | `upload` | `upload` |
| 基础/运维 | `<noun>` | `jobs`, `audit` |

### 3.2 文件命名

```
src/app/oauth/
├── domain/
│   ├── entities/
│   │   ├── captcha.py               # Captcha 实体
│   │   ├── sms_code.py              # SmsCode 实体
│   │   └── user_session.py          # UserSession 实体
│   ├── value_objects/
│   │   ├── phone.py                 # Phone VO（带校验）
│   │   └── client_id.py             # ClientId VO
│   ├── events/
│   │   ├── user_registered.py       # UserRegistered 事件
│   │   └── sms_code_sent.py         # SmsCodeSent 事件
│   ├── services/
│   │   ├── captcha_state_machine.py # 状态机纯函数
│   │   └── jwt_mint.py              # JWT 签发纯函数
│   └── exceptions.py                # OAuthErrorCode + OAuthError
```

**规则**：

* 类名 = 文件名 PascalCase
* 单类单文件（除非工厂聚合）
* 测试文件 = `test_<被测文件>.py`

***

## 4. 端口契约约定

### 4.1 端口最小化（1-3 方法）

```python
# ✅ 推荐：3 方法
class SmsCodeRepositoryPort(ABC):
    @abstractmethod
    async def save(self, sms_code: SmsCode) -> None: ...
    @abstractmethod
    async def verify_and_consume(...) -> VerifyResult: ...
    @abstractmethod
    async def consume(self, phone: Phone, client_id: ClientId) -> None: ...

# ❌ 反模式：把多个 port 合并
class OAuthRepositoryPort(ABC):
    async def save_captcha(...): ...
    async def save_sms_code(...): ...
    async def save_user(...): ...
```

### 4.2 端口命名

| 类型 | 命名 | 示例 |
|---|---|---|
| 仓储 port | `<Entity>RepositoryPort` | `SmsCodeRepositoryPort` |
| 服务 port | `<Verb>Port` 或 `<Noun>ServicePort` | `TokenServicePort` |
| 适配器 port | `<External>ClientPort` | `SmsSenderPort`, `WechatClientPort` |

### 4.3 端口方法命名

| 操作 | 命名 | 示例 |
|---|---|---|
| 查询单个 | `find_by_<field>` | `find_by_phone`, `find_by_cid` |
| 查询多个 | `list_by_<field>` | `list_by_user_id` |
| 创建 | `create` | `create(...)` |
| 更新 | `update` | `update(id, ...)` |
| Upsert | `upsert` | `upsert(...)` |
| 删除 | `delete` / `delete_by_<field>` | `delete_by_cid` |
| 原子校验 + 副作用 | `verify_and_<action>` | `verify_and_consume` |

***

## 5. 实体 vs 值对象

### 5.1 实体（有 id，可变）

```python
@dataclass
class SmsCode:
    phone: Phone                  # 复合主键一部分
    client_id: ClientId           # 复合主键一部分
    code_hash: str                # 可变
    attempts: int = 0             # 可变（会随 verify 递增）
    sent_at: datetime
```

### 5.2 值对象（无 id，不可变）

```python
@dataclass(frozen=True)
class Phone:
    value: str
    
    def __post_init__(self):
        if not re.match(r"^1[3-9]\d{9}$", self.value):
            raise ValueError(f"invalid phone: {self.value}")
```

### 5.3 何时用实体 vs 值对象

| 特征 | 实体 | 值对象 |
|---|---|---|
| 有 id | ✅ | ❌ |
| 可变 | ✅ | ❌（frozen） |
| 自有行为 | ✅ | ✅（构造时校验） |
| 用 == 比较 | ❌（用 id） | ✅（按值） |

***

## 6. 领域事件约定

### 6.1 命名（PascalCase PastTense）

```python
class UserRegistered(DomainEvent):
    user_id: str
    phone: str
    occurred_at: datetime

class SmsCodeSent(DomainEvent):
    phone: Phone
    client_id: ClientId
    occurred_at: datetime

class TokenRotated(DomainEvent):
    user_id: str
    client_id: ClientId
    occurred_at: datetime

class TokenRotationReplayed(DomainEvent):  # ⚠️ 重放连坐
    user_id: str
    client_id: ClientId
    occurred_at: datetime
```

### 6.2 事件发布

```python
# src/app/oauth/infrastructure/eventbus/celery_event_publisher.py
class CeleryEventPublisher(EventPublisherPort):
    async def publish(self, event: DomainEvent) -> None:
        celery_app.send_task(
            f"oauth.event.{type(event).__name__}",
            kwargs=event.model_dump(),
        )
```

### 6.3 事件订阅（Celery task）

```python
# src/app/oauth/application/events/handlers.py
@celery_app.task(name="oauth.event.UserRegistered")
def handle_user_registered(user_id: str, phone: str, **kwargs):
    # 发送欢迎短信
    # 初始化用户 profile
    ...
```

***

## 7. 跨模块共享约定

### 7.1 Port / VO 跨模块归属

| Port / VO | 拥有模块 | 使用模块 | 跨模块引用方式 |
|---|---|---|---|
| `UserRepository` | `user/` | `oauth/` | `from src.app.user.application.ports.user_repository import UserRepositoryPort` |
| `JwtClaims` VO | `oauth/` | `auth/` | `from src.app.oauth.domain.value_objects.jwt_claims import JwtClaims` |
| `Phone` VO | `oauth/` | `user/` | `from src.app.oauth.domain.value_objects.phone import Phone` |

### 7.2 禁止的跨模块依赖

* ❌ `oauth/infrastructure` 直接 import `user/infrastructure`（用 port）
* ❌ `oauth/interface` 直接 import `user/interface`（用 port）
* ❌ Domain 层 import 任何其他模块的 infrastructure / interface

### 7.3 共享模型 vs 重复定义

| 情况 | 处理 |
|---|---|
| 两个模块需要同一个 VO | 拥有模块定义，其他模块 import |
| 两个模块需要同一个 Port 接口 | 拥有模块定义，其他模块 import |
| 概念相似但不完全相同 | **各自定义**，不要为了"统一"强行共享 |

***

## 8. 路由挂载约定

### 8.1 OAuth 走单端点（特例）

```python
# src/app/oauth/interface/api/oauth.py
router = APIRouter(prefix="/api/v1/auth", tags=["OAuth"])

@router.post("/oauth")
async def oauth_endpoint(req: OAuthRequest) -> JSONResponse:
    # 按 action 字段派发到 9 use case
    ...
```

### 8.2 业务模块走 RESTful（默认）

```python
# src/app/user/interface/api/users.py
router = APIRouter(prefix="/api/v1/users", tags=["Users"])

@router.post("/")           # POST /api/v1/users/
@router.get("/{user_id}")   # GET /api/v1/users/{user_id}
@router.put("/{user_id}")   # PUT /api/v1/users/{user_id}
```

### 8.3 Bootstrap 注册

```python
# src/app/main.py
def create_app() -> FastAPI:
    app = FastAPI()
    app.include_router(oauth_router)  # OAuth 单端点
    app.include_router(users_router)  # 用户 RESTful
    app.include_router(wallet_router) # 钱包 RESTful
    ...
    register_oauth_exception_handlers(app)
    return app
```

***

## 9. 测试目录约定

```
tests/
├── conftest.py                      # 全局 fixture（reset stores、redis client）
├── fixtures/
│   ├── oauth_errors.py              # 错误码 fixture
│   └── user_factory.py              # User 实体工厂
│
├── unit/                            # 单元测试
│   ├── domain/                      # 纯函数测试（零 mock）
│   │   ├── test_captcha_state_machine.py
│   │   ├── test_sms_code_state_machine.py
│   │   └── test_jwt_mint_verify.py
│   └── application/                 # use case 测试（mock 所有 port）
│       ├── test_sms_login_usecase.py
│       └── test_rotate_usecase.py
│
├── integration/                     # 集成测试（真 Redis/PG）
│   ├── infrastructure/
│   │   ├── test_redis_captcha_repository.py
│   │   ├── test_redis_sms_code_repository.py
│   │   └── test_authlib_jwt_token_service.py
│   └── interface/                   # FastAPI TestClient
│       ├── test_oauth_captcha.py
│       └── test_oauth_sms_login.py
│
└── e2e/                             # E2E 测试（RUN_E2E=1）
    └── test_oauth_full_flow.py
```

***

## 10. 命名反例（❌ 禁止）

| ❌ 禁止 | ✅ 推荐 | 理由 |
|---|---|---|
| `src/app/oauth/OAuthService.py` | `src/app/oauth/application/usecase/sms_login.py` | 类名应与文件路径匹配；service 不是合适命名 |
| `src/app/oauth/data/user.py` | `src/app/user/domain/entities/user.py` | 跨模块数据放对方模块 |
| `src/app/oauth/misc.py` | 各自单一职责文件 | 杂物文件不清晰 |
| `class OAuthHelper` | 具体类名 | Helper/Util 太泛 |
| `function1`、`doStuff` | `verify_and_consume`、`publish_user_registered` | 命名要表意 |

***

## 11. 相关文档

* [代码风格](/ai-contexts/code-style) — Ruff / mypy / Pydantic v2 / structlog / OAuth 错误码
* [Internal Wiki → DDD 4 层](/internal-wiki/architecture/ddd-layers) — 范式沉淀
* [Internal Wiki → OAuth 实施总览](/internal-wiki/architecture/oauth-implementation) — OAuth 完整实施
