# 后端架构规范指南 **版本**: 1.0 **更新时间**: 2026-02-04 **适用范围**: FastAPI + SQLModel + PostgreSQL --- ## 📋 目录 - [架构概览](#架构概览) - [分层架构](#分层架构) - [Repository 层规范](#repository-层规范) - [Service 层规范](#service-层规范) - [API 层规范](#api-层规范) - [测试规范](#测试规范) - [最佳实践](#最佳实践) --- ## 架构概览 ### 分层结构 ``` ┌─────────────────────────────────────┐ │ API Layer (FastAPI) │ ← 接口层:请求验证、响应格式化 ├─────────────────────────────────────┤ │ Service Layer (Business) │ ← 业务层:事务管理、业务逻辑 ├─────────────────────────────────────┤ │ Repository Layer (Data Access) │ ← 数据层:数据访问、查询构建 ├─────────────────────────────────────┤ │ Model Layer (SQLModel) │ ← 模型层:数据模型定义 ├─────────────────────────────────────┤ │ Database (PostgreSQL 17) │ ← 数据库 └─────────────────────────────────────┘ ``` ### 核心原则 1. **单一职责原则 (SRP)** - 每层只负责自己的职责 2. **依赖倒置原则 (DIP)** - 高层不依赖低层,都依赖抽象 3. **开闭原则 (OCP)** - 对扩展开放,对修改关闭 4. **事务一致性** - Service 层统一管理事务 5. **测试友好** - 每层都可以独立测试 --- ## 分层架构 ### 1. Model 层 **职责**: 定义数据模型 **位置**: `server/app/models/` **示例**: ```python from sqlmodel import SQLModel, Field from uuid import UUID from datetime import datetime from typing import Optional class Project(SQLModel, table=True): """项目模型""" __tablename__ = "projects" id: UUID = Field(default_factory=uuid7, primary_key=True) name: str = Field(max_length=255, index=True) description: Optional[str] = None owner_id: UUID = Field(foreign_key="users.id", index=True) status: int = Field(default=0) # 使用 SMALLINT 存储枚举 created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) ``` **规范**: - ✅ 使用 SQLModel 定义表结构 - ✅ 主键使用 UUID v7 - ✅ 枚举使用 SMALLINT 存储 - ✅ 时间戳使用 `datetime.timezone.utc` - ❌ 不包含业务逻辑方法 ### 2. Repository 层 **职责**: 数据访问逻辑 **位置**: `server/app/repositories/` **完整示例**: ```python from typing import Optional, List from uuid import UUID from sqlmodel import select, and_ from sqlmodel.ext.asyncio.session import AsyncSession class ProjectRepository: """项目数据访问层 职责: - 数据访问逻辑(CRUD) - 查询构建 - 数据转换 不负责: - 事务管理(不调用 commit) - 业务逻辑判断 - 调用其他 Repository """ def __init__(self, session: AsyncSession): self.session = session # ======================================================================== # 基础 CRUD # ======================================================================== async def create(self, project: Project) -> Project: """创建项目 注意: - 使用 flush() 而非 commit() - 不调用 refresh() - 返回对象供调用方使用 """ self.session.add(project) await self.session.flush() # ✅ 仅 flush,不 commit return project async def get_by_id(self, project_id: str) -> Optional[Project]: """通过 ID 查询项目""" statement = select(Project).where( Project.id == UUID(project_id), Project.status != ProjectStatus.SOFT_DELETED ) result = await self.session.exec(statement) return result.first() async def update(self, project_id: str, data: dict) -> Optional[Project]: """更新项目""" project = await self.get_by_id(project_id) if not project: return None for key, value in data.items(): if hasattr(project, key): setattr(project, key, value) project.updated_at = datetime.now(timezone.utc) self.session.add(project) await self.session.flush() # ✅ 仅 flush return project async def delete(self, project_id: str) -> bool: """删除项目(软删除)""" project = await self.get_by_id(project_id) if not project: return False project.status = ProjectStatus.SOFT_DELETED project.deleted_at = datetime.now(timezone.utc) self.session.add(project) await self.session.flush() # ✅ 仅 flush return True # ======================================================================== # 查询方法 # ======================================================================== async def get_by_user( self, user_id: str, filters: Optional[dict] = None, page: int = 1, page_size: int = 20 ) -> List[Project]: """获取用户项目列表(支持筛选、分页)""" conditions = [ Project.owner_id == UUID(user_id), Project.status != ProjectStatus.SOFT_DELETED ] # 应用筛选条件 if filters: if filters.get('status'): conditions.append(Project.status == filters['status']) if filters.get('search'): search_pattern = f"%{filters['search']}%" conditions.append(Project.name.ilike(search_pattern)) # 构建查询 statement = select(Project).where(and_(*conditions)) statement = statement.offset((page - 1) * page_size).limit(page_size) statement = statement.order_by(Project.updated_at.desc()) result = await self.session.exec(statement) return list(result.all()) async def exists_by_name( self, name: str, owner_id: str, exclude_id: Optional[str] = None ) -> bool: """检查项目名称是否存在""" conditions = [ Project.name == name, Project.owner_id == UUID(owner_id), Project.status != ProjectStatus.SOFT_DELETED ] if exclude_id: conditions.append(Project.id != UUID(exclude_id)) statement = select(func.count(Project.id)).where(and_(*conditions)) result = await self.session.exec(statement) count = result.one() return count > 0 ``` **关键规范**: ✅ **应该做**: - 使用 `flush()` 确保数据在当前事务内可见 - 返回对象供调用方使用 - 构建复杂查询 - 数据转换 ❌ **不应该做**: - 调用 `commit()` - 事务管理是 Service 层的职责 - 调用 `refresh()` - 让调用方决定是否需要 - 业务逻辑判断 - 应该在 Service 层 - 调用其他 Repository - 应该在 Service 层编排 ### 3. Service 层 **职责**: 业务逻辑和事务管理 **位置**: `server/app/services/` **完整示例**: ```python from typing import Optional from sqlmodel.ext.asyncio.session import AsyncSession from sqlalchemy.exc import IntegrityError class ProjectService: """项目业务逻辑层 职责: - 事务管理(commit/rollback) - 业务逻辑编排 - 调用多个 Repository - 异常处理和转换 不负责: - 数据访问细节 - HTTP 请求处理 """ def __init__( self, session: AsyncSession, project_repo: ProjectRepository, folder_repo: FolderRepository, log_repo: LogRepository ): self.session = session self.project_repo = project_repo self.folder_repo = folder_repo self.log_repo = log_repo async def create_project( self, data: ProjectCreate, user_id: str ) -> Project: """创建项目 业务流程: 1. 验证项目名称唯一性 2. 验证文件夹存在(如果指定) 3. 创建项目 4. 记录操作日志 5. 提交事务 """ try: # 1. 业务验证 if await self.project_repo.exists_by_name( data.name, user_id ): raise DuplicateProjectNameError(data.name) # 2. 验证文件夹 if data.folder_id: folder = await self.folder_repo.get_by_id(data.folder_id) if not folder: raise FolderNotFoundError(data.folder_id) # 检查文件夹权限 if folder.owner_id != UUID(user_id): raise PermissionDeniedError() # 3. 创建项目 project = Project( **data.dict(), owner_id=UUID(user_id) ) created = await self.project_repo.create(project) # 4. 记录日志 await self.log_repo.create_log( action="project.created", user_id=user_id, resource_id=str(created.id) ) # 5. ✅ 提交事务 await self.session.commit() return created except (DuplicateProjectNameError, FolderNotFoundError, PermissionDeniedError): # 业务异常:回滚并重新抛出 await self.session.rollback() raise except IntegrityError as e: # 数据库约束错误:转换为业务异常 await self.session.rollback() raise DatabaseConstraintError() from e except Exception as e: # 未知错误:回滚并重新抛出 await self.session.rollback() logger.error(f"创建项目失败: {e}") raise async def update_project( self, project_id: str, data: ProjectUpdate, user_id: str ) -> Project: """更新项目""" try: # 1. 权限检查 if not await self.check_permission(user_id, project_id, 'editor'): raise PermissionDeniedError() # 2. 业务验证 if data.name: if await self.project_repo.exists_by_name( data.name, user_id, exclude_id=project_id ): raise DuplicateProjectNameError(data.name) # 3. 更新数据 project = await self.project_repo.update( project_id, data.dict(exclude_unset=True) ) if not project: raise ProjectNotFoundError(project_id) # 4. 记录日志 await self.log_repo.create_log( action="project.updated", user_id=user_id, resource_id=project_id ) # 5. ✅ 提交事务 await self.session.commit() return project except Exception as e: await self.session.rollback() raise async def move_to_folder( self, project_id: str, folder_id: Optional[str], user_id: str ) -> Project: """移动项目到文件夹 演示:跨 Repository 调用 """ try: # 1. 权限检查 if not await self.check_permission(user_id, project_id, 'editor'): raise PermissionDeniedError() # 2. 验证目标文件夹 if folder_id: folder = await self.folder_repo.get_by_id(folder_id) if not folder: raise FolderNotFoundError(folder_id) if folder.owner_id != UUID(user_id): raise PermissionDeniedError() # 3. 移动项目 project = await self.project_repo.move_to_folder( project_id, folder_id ) if not project: raise ProjectNotFoundError(project_id) # 4. 记录日志 await self.log_repo.create_log( action="project.moved", user_id=user_id, resource_id=project_id, metadata={"folder_id": folder_id} ) # 5. ✅ 所有操作在一个事务中提交 await self.session.commit() return project except Exception as e: await self.session.rollback() raise async def check_permission( self, user_id: str, project_id: str, required_role: str = 'viewer' ) -> bool: """检查用户权限(业务逻辑)""" project = await self.project_repo.get_by_id(project_id) if not project: return False # 所有者拥有所有权限 if str(project.owner_id) == user_id: return True # 检查成员权限 # ... return False ``` **关键规范**: ✅ **应该做**: - 所有方法都有 `try-except` 块 - 成功时调用 `await self.session.commit()` - 异常时调用 `await self.session.rollback()` - 编排多个 Repository 调用 - 业务逻辑验证 - 异常转换(数据库异常 → 业务异常) ❌ **不应该做**: - 直接构建 SQL 查询 - 处理 HTTP 请求/响应 - 直接访问数据库 ### 4. API 层 **职责**: HTTP 接口处理 **位置**: `server/app/api/v1/` **完整示例**: ```python from fastapi import APIRouter, Depends, HTTPException, status from typing import List router = APIRouter(prefix="/projects", tags=["projects"]) # ======================================================================== # 依赖注入 # ======================================================================== def get_project_service( session: AsyncSession = Depends(get_session) ) -> ProjectService: """获取 ProjectService 实例""" project_repo = ProjectRepository(session) folder_repo = FolderRepository(session) log_repo = LogRepository(session) return ProjectService(session, project_repo, folder_repo, log_repo) # ======================================================================== # API 端点 # ======================================================================== @router.post( "", response_model=ProjectResponse, status_code=status.HTTP_201_CREATED, summary="创建项目", description="创建一个新项目" ) async def create_project( data: ProjectCreate, # ✅ Pydantic 自动验证 current_user: User = Depends(get_current_user), service: ProjectService = Depends(get_project_service) ): """创建项目 - **name**: 项目名称(必填,1-255字符) - **description**: 项目描述(可选) - **folder_id**: 文件夹ID(可选) """ try: project = await service.create_project(data, str(current_user.id)) return project except DuplicateProjectNameError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"项目名称已存在: {e.name}" ) except FolderNotFoundError as e: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"文件夹不存在: {e.folder_id}" ) except PermissionDeniedError: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="没有权限访问该文件夹" ) @router.get( "/{project_id}", response_model=ProjectResponse, summary="获取项目详情" ) async def get_project( project_id: str, current_user: User = Depends(get_current_user), service: ProjectService = Depends(get_project_service) ): """获取项目详情""" # 权限检查 if not await service.check_permission( str(current_user.id), project_id, 'viewer' ): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="没有权限访问该项目" ) project = await service.get_project(project_id) if not project: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在" ) return project @router.get( "", response_model=List[ProjectResponse], summary="获取项目列表" ) async def list_projects( folder_id: Optional[str] = None, search: Optional[str] = None, page: int = 1, page_size: int = 20, current_user: User = Depends(get_current_user), service: ProjectService = Depends(get_project_service) ): """获取项目列表(支持筛选、搜索、分页)""" filters = {} if folder_id: filters['folder_id'] = folder_id if search: filters['search'] = search projects = await service.list_projects( str(current_user.id), filters, page, page_size ) return projects @router.put( "/{project_id}", response_model=ProjectResponse, summary="更新项目" ) async def update_project( project_id: str, data: ProjectUpdate, current_user: User = Depends(get_current_user), service: ProjectService = Depends(get_project_service) ): """更新项目""" try: project = await service.update_project( project_id, data, str(current_user.id) ) return project except ProjectNotFoundError: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在" ) except PermissionDeniedError: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="没有权限修改该项目" ) except DuplicateProjectNameError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"项目名称已存在: {e.name}" ) @router.delete( "/{project_id}", status_code=status.HTTP_204_NO_CONTENT, summary="删除项目" ) async def delete_project( project_id: str, current_user: User = Depends(get_current_user), service: ProjectService = Depends(get_project_service) ): """删除项目(软删除)""" try: await service.delete_project(project_id, str(current_user.id)) except ProjectNotFoundError: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在" ) except PermissionDeniedError: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="没有权限删除该项目" ) ``` **关键规范**: ✅ **应该做**: - 使用 Pydantic 模型验证请求 - 使用依赖注入获取 Service - 将业务异常转换为 HTTP 异常 - 提供清晰的 API 文档 - 权限检查 ❌ **不应该做**: - 直接调用 Repository - 包含业务逻辑 - 直接访问数据库 --- ## 测试规范 ### Repository 单元测试 **位置**: `server/tests/unit/repositories/` **示例**: ```python import pytest from uuid import UUID @pytest.mark.asyncio class TestProjectRepository: """Repository 单元测试""" async def test_create_project(self, db_session): """测试创建项目""" repo = ProjectRepository(db_session) # ✅ 在测试方法内创建数据 project = Project( name="测试项目", type=ProjectType.MINE, owner_type="user", owner_id=UUID("00000000-0000-0000-0000-000000000001"), status=ProjectStatus.ACTIVE ) created = await repo.create(project) assert created.id is not None assert created.name == "测试项目" # ✅ 不需要手动 commit,conftest 会自动回滚 async def test_get_by_id(self, db_session): """测试查询项目""" repo = ProjectRepository(db_session) # 创建测试数据 project = Project(...) created = await repo.create(project) # 查询 found = await repo.get_by_id(str(created.id)) assert found is not None assert str(found.id) == str(created.id) # ✅ 使用字符串比较 ``` **关键规范**: - ✅ 使用 `db_session` fixture - ✅ 在测试方法内创建数据 - ✅ 依赖 conftest 的自动回滚 - ❌ 不在 fixture 中创建需要 flush 的数据 - ❌ 不手动 commit ### Service 单元测试 **位置**: `server/tests/unit/services/` **示例**: ```python @pytest.mark.asyncio class TestProjectService: """Service 单元测试""" async def test_create_project(self, db_session): """测试创建项目""" # 准备依赖 project_repo = ProjectRepository(db_session) folder_repo = FolderRepository(db_session) log_repo = LogRepository(db_session) service = ProjectService(db_session, project_repo, folder_repo, log_repo) # 测试数据 data = ProjectCreate( name="测试项目", type="mine", owner_id="00000000-0000-0000-0000-000000000001" ) # ✅ Service 会调用 commit() project = await service.create_project(data, data.owner_id) assert project.id is not None assert project.name == "测试项目" # ✅ conftest 会在测试结束后回滚 async def test_create_project_duplicate_name(self, db_session): """测试创建重复名称的项目""" service = ... # 创建第一个项目 await service.create_project(data, user_id) # 尝试创建重复名称的项目 with pytest.raises(DuplicateProjectNameError): await service.create_project(data, user_id) ``` ### API 集成测试 **位置**: `server/tests/integration/api/` **示例**: ```python @pytest.mark.asyncio class TestProjectAPI: """API 集成测试""" async def test_create_project(self, async_client, auth_headers): """测试创建项目 API""" data = { "name": "测试项目", "type": "mine", "description": "项目描述" } response = await async_client.post( "/api/v1/projects", json=data, headers=auth_headers ) assert response.status_code == 201 result = response.json() assert result["name"] == "测试项目" assert "id" in result ``` --- ## 最佳实践 ### 1. 事务管理 ✅ **正确**: ```python # Service 层统一管理事务 async def create_project(self, data): try: project = await self.repo.create(Project(**data)) await self.log_repo.create_log(...) await self.session.commit() # ✅ 统一提交 return project except Exception: await self.session.rollback() # ✅ 统一回滚 raise ``` ❌ **错误**: ```python # Repository 层不应该管理事务 async def create(self, project): self.session.add(project) await self.session.commit() # ❌ 错误! return project ``` ### 2. 异常处理 ✅ **正确**: ```python # Service 层转换异常 try: project = await self.repo.create(project) await self.session.commit() except IntegrityError as e: await self.session.rollback() raise DuplicateProjectNameError() from e # ✅ 转换为业务异常 ``` ### 3. 依赖注入 ✅ **正确**: ```python # API 层使用依赖注入 @router.post("/projects") async def create_project( data: ProjectCreate, service: ProjectService = Depends(get_project_service) # ✅ ): return await service.create_project(data) ``` ❌ **错误**: ```python # 不要在 API 层直接创建 Service @router.post("/projects") async def create_project(data: ProjectCreate): service = ProjectService(...) # ❌ 错误! return await service.create_project(data) ``` ### 4. 查询优化 ✅ **正确**: ```python # Repository 层构建高效查询 async def get_by_user(self, user_id, filters): conditions = [Project.owner_id == UUID(user_id)] # 使用索引字段筛选 if filters.get('status'): conditions.append(Project.status == filters['status']) # 使用 ILIKE 进行模糊搜索 if filters.get('search'): conditions.append(Project.name.ilike(f"%{filters['search']}%")) statement = select(Project).where(and_(*conditions)) statement = statement.order_by(Project.updated_at.desc()) result = await self.session.exec(statement) return list(result.all()) ``` --- ## 参考文档 - [ADR 010: Repository 与 Service 层事务管理职责划分](../../architecture/adrs/010-repository-service-transaction-management.md) - [Changelog: Project Repository 事务管理架构修复](../changelogs/2026-02-04-project-repository-transaction-management-fix.md) - [测试指南](../../tests/README.md) --- ## 变更历史 - 2026-02-04: 初始版本,基于 ADR 010 和实际项目经验总结