Appearance
模块约定
📍 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 # ExceptionHandler2. 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: datetime5.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: datetime6.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直接 importuser/infrastructure(用 port) - ❌
oauth/interface直接 importuser/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 app9. 测试目录约定
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.py10. 命名反例(❌ 禁止)
| ❌ 禁止 | ✅ 推荐 | 理由 |
|---|---|---|
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. 相关文档
- 代码风格 — Ruff / mypy / Pydantic v2 / structlog / OAuth 错误码
- Internal Wiki → DDD 4 层 — 范式沉淀
- Internal Wiki → OAuth 实施总览 — OAuth 完整实施