Skip to content

模块约定 ​

📍 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/EventSQLModel / Redis / FastAPI / Pydantic runtime schema / 任何 IO
ApplicationDomain + Infrastructure 接口(port)+ Pydantic schemaFastAPI / SQLModel client / Redis client
InfrastructureApplication 接口 + SQLModel/Redis/三方 SDKDomain 反向依赖 / FastAPI 路由
InterfaceApplication + 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
MISadminadmin
内容<noun>cms, content, ranking
电商<noun>cart, points, mall, recipe
支付<noun>wxpay
上传uploadupload
基础/运维<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>RepositoryPortSmsCodeRepositoryPort
服务 port<Verb>Port 或 <Noun>ServicePortTokenServicePort
适配器 port<External>ClientPortSmsSenderPort, WechatClientPort

4.3 端口方法命名 ​

操作命名示例
查询单个find_by_<field>find_by_phone, find_by_cid
查询多个list_by_<field>list_by_user_id
创建createcreate(...)
更新updateupdate(id, ...)
Upsertupsertupsert(...)
删除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拥有模块使用模块跨模块引用方式
UserRepositoryuser/oauth/from src.app.user.application.ports.user_repository import UserRepositoryPort
JwtClaims VOoauth/auth/from src.app.oauth.domain.value_objects.jwt_claims import JwtClaims
Phone VOoauth/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.pysrc/app/oauth/application/usecase/sms_login.py类名应与文件路径匹配;service 不是合适命名
src/app/oauth/data/user.pysrc/app/user/domain/entities/user.py跨模块数据放对方模块
src/app/oauth/misc.py各自单一职责文件杂物文件不清晰
class OAuthHelper具体类名Helper/Util 太泛
function1、doStuffverify_and_consume、publish_user_registered命名要表意

11. 相关文档 ​