# Folder Tree API 完整修复 **日期**: 2026-02-06 **类型**: Bug 修复 + 功能增强 **接口**: `GET /api/v1/folders/tree` ## 问题描述 `/api/v1/folders/tree` 接口实现与文档规范不符: 1. ❌ 参数默认值错误(`include_projects` 默认 `False`,应为 `True`) 2. ❌ 缺少 `include_subprojects` 参数 3. ❌ 返回格式错误(项目在单独的 `projects` 字段,应在 `children` 数组) 4. ❌ 缺少 `type` 字段标识节点类型 5. ❌ 缺少子项目支持 6. ❌ 根节点格式不符合规范 7. ❌ `projectCount` 和 `subfolderCount` 始终返回 0(UUID 类型转换问题) ## 修复内容 ### 1. API 路由层修复 (`folders.py`) **修复参数默认值和新增参数**: ```python @router.get("/tree") async def get_folder_tree( max_depth: Optional[int] = Query(None, ge=1, description="最大深度"), include_projects: bool = Query(True, description="是否包含项目节点"), # ✅ 默认 True include_subprojects: bool = Query(True, description="是否包含子项目节点"), # ✅ 新增 current_user: User = Depends(get_current_user), session: AsyncSession = Depends(get_session) ): service = FolderService(session) tree = await service.get_folder_tree( current_user.user_id, max_depth, include_projects, include_subprojects # ✅ 传递新参数 ) return success_response(data=tree) ``` ### 2. Service 层修复 (`folder_service.py`) **新增参数并修复返回格式**: ```python async def get_folder_tree( self, user_id: str, max_depth: Optional[int] = None, include_projects: bool = True, # ✅ 默认 True include_subprojects: bool = True # ✅ 新增 ) -> Dict[str, Any]: tree = await self.repository.get_tree_structure( user_id, None, max_depth, 0, include_projects, include_subprojects ) # ✅ 返回根节点包装格式 return { "id": "root", "name": "根目录", "children": tree } ``` ### 3. Repository 层完整重构 (`folder_repository.py`) **修复核心逻辑**: ```python async def get_tree_structure( self, owner_id: str, parent_id: Optional[str] = None, max_depth: Optional[int] = None, current_depth: int = 0, include_projects: bool = True, # ✅ 默认 True include_subprojects: bool = True # ✅ 新增 ) -> List[Dict[str, Any]]: """递归获取文件夹树形结构(包含项目和子项目)""" # ... 查询文件夹 ... for folder in folders: # 1. 文件夹节点(包含 type 字段) folder_node = { "id": str(folder.id), "name": folder.name, "type": "folder", # ✅ 新增 type 字段 "description": folder.description, "color": folder.color, "icon": folder.icon, "level": folder.level, "folderCategory": folder.folder_category, "projectCount": await self.count_projects(str(folder.id)), "subfolderCount": await self.count_subfolders(str(folder.id)), "children": children } # 2. 添加项目节点到 children 数组(与文件夹平级) if include_projects: projects = await self._get_projects_in_folder(str(folder.id), owner_id) for project in projects: project_node = { "id": str(project.id), "name": project.name, "type": "project", # ✅ 项目类型 "isSubproject": False, "parentProjectId": None, "screenplayId": None, "children": [] } # 3. 添加子项目(嵌套在父项目 children 中) if include_subprojects: subprojects = await self._get_subprojects(str(project.id), owner_id) for subproject in subprojects: subproject_node = { "id": str(subproject.id), "name": subproject.name, "type": "subproject", # ✅ 子项目类型 "isSubproject": True, "parentProjectId": str(subproject.parent_project_id), "screenplayId": str(subproject.screenplay_id) if subproject.screenplay_id else None, "children": [] } project_node["children"].append(subproject_node) folder_node["children"].append(project_node) # ✅ 项目添加到 children tree.append(folder_node) return tree ``` **修复统计方法**: ```python async def count_projects(self, folder_id: str) -> int: """统计文件夹内父项目数量(不包含子项目)""" from app.models.project import Project from uuid import UUID # ✅ 转换为 UUID 类型 folder_uuid = UUID(folder_id) if isinstance(folder_id, str) else folder_id statement = select(func.count(Project.id)).where( Project.folder_id == folder_uuid, # ✅ UUID 类型匹配 Project.parent_project_id.is_(None), # ✅ 只统计父项目 Project.deleted_at.is_(None) ) result = await self.session.exec(statement) return result.one() async def count_subfolders(self, folder_id: str) -> int: """统计子文件夹数量""" from uuid import UUID # ✅ 转换为 UUID 类型 folder_uuid = UUID(folder_id) if isinstance(folder_id, str) else folder_id statement = select(func.count(Folder.id)).where( Folder.parent_folder_id == folder_uuid, # ✅ UUID 类型匹配 Folder.deleted_at.is_(None) ) result = await self.session.exec(statement) return result.one() ``` **新增辅助方法**: ```python async def _get_projects_in_folder(self, folder_id: str, owner_id: str): """获取文件夹内的父项目(parent_project_id = NULL)""" from app.models.project import Project from uuid import UUID folder_uuid = UUID(folder_id) if isinstance(folder_id, str) else folder_id statement = select(Project).where( Project.folder_id == folder_uuid, Project.parent_project_id.is_(None), # ✅ 只查父项目 Project.deleted_at.is_(None) ).order_by(Project.display_order, Project.created_at) result = await self.session.exec(statement) return list(result.all()) async def _get_subprojects(self, parent_project_id: str, owner_id: str): """获取父项目的子项目""" from app.models.project import Project from uuid import UUID parent_uuid = UUID(parent_project_id) if isinstance(parent_project_id, str) else parent_project_id statement = select(Project).where( Project.parent_project_id == parent_uuid, # ✅ 子项目条件 Project.deleted_at.is_(None) ).order_by(Project.display_order, Project.created_at) result = await self.session.exec(statement) return list(result.all()) ``` ## 修复后的响应格式 ### 请求示例 ```bash # 包含项目和子项目(默认) GET /api/v1/folders/tree # 仅包含文件夹 GET /api/v1/folders/tree?include_projects=false # 包含项目但不包含子项目 GET /api/v1/folders/tree?include_projects=true&include_subprojects=false # 限制深度 GET /api/v1/folders/tree?max_depth=3 ``` ### 响应格式(符合规范) ```json { "success": true, "code": 200, "message": "Success", "data": { "id": "root", "name": "根目录", "children": [ { "id": "019c2d6e-8fc4-7c63-b875-8760c3221917", "name": "文件夹1", "type": "folder", "description": null, "color": "#3B82F6", "icon": null, "level": 0, "folderCategory": 1, "projectCount": 2, "subfolderCount": 1, "children": [ { "id": "019c318e-49e6-7ee3-8c5d-5fc9c407eabd", "name": "子文件夹", "type": "folder", "level": 1, "projectCount": 0, "subfolderCount": 0, "children": [] }, { "id": "550e8400-e29b-41d4-a716-446655440001", "name": "电影项目A", "type": "project", "isSubproject": false, "parentProjectId": null, "screenplayId": null, "children": [ { "id": "550e8400-e29b-41d4-a716-446655440002", "name": "第一集剧本", "type": "subproject", "isSubproject": true, "parentProjectId": "550e8400-e29b-41d4-a716-446655440001", "screenplayId": "550e8400-e29b-41d4-a716-446655440003", "children": [] } ] } ] } ] }, "timestamp": "2026-02-06T12:45:00+00:00" } ``` ## 关键改进 ### 1. 树形结构统一 - ✅ 文件夹、项目、子项目都使用 `children` 数组 - ✅ 项目节点与文件夹节点平级 - ✅ 子项目嵌套在父项目的 `children` 中 ### 2. 节点类型标识 - ✅ `type: "folder"` - 文件夹节点 - ✅ `type: "project"` - 父项目节点 - ✅ `type: "subproject"` - 子项目节点 ### 3. 字段命名规范 - ✅ 所有字段使用 camelCase(驼峰命名) - ✅ `folderCategory`、`projectCount`、`subfolderCount` - ✅ `isSubproject`、`parentProjectId`、`screenplayId` ### 4. 参数默认值 - ✅ `include_projects` 默认 `true` - ✅ `include_subprojects` 默认 `true` - ✅ 符合文档规范 ## 对比修复前后 | 项目 | 修复前 | 修复后 | |------|--------|--------| | `include_projects` 默认值 | ❌ `False` | ✅ `True` | | `include_subprojects` 参数 | ❌ 不存在 | ✅ 存在,默认 `True` | | 项目位置 | ❌ `node["projects"]` | ✅ `node["children"]` | | `type` 字段 | ❌ 不存在 | ✅ `folder/project/subproject` | | 子项目支持 | ❌ 不支持 | ✅ 完全支持 | | 根节点格式 | ❌ `{"tree": [...]}` | ✅ `{"id": "root", "name": "根目录", "children": [...]}` | | UUID 类型转换 | ❌ 不一致 | ✅ 统一字符串格式 | | `projectCount` | ❌ 始终返回 0 | ✅ 正确统计父项目数量 | | `subfolderCount` | ❌ UUID 类型不匹配 | ✅ 正确统计子文件夹数量 | ## 符合的文档规范 - ✅ `docs/requirements/backend/04-services/project/folder-service.md` 第 5.2 节 - ✅ `.claude/skills/jointo-tech-stack/references/api-design.md` 命名规范 - ✅ API 响应字段使用 camelCase ## 测试建议 ### 参数别名支持 API 现在同时支持 camelCase 和 snake_case 参数: | 前端(camelCase) | 后端(snake_case) | |------------------|-------------------| | `includeProjects` | `include_projects` | | `includeSubprojects` | `include_subprojects` | | `maxDepth` | `max_depth` | 两种格式都可以正常使用! ### 测试用例 1. **测试默认行为**(应包含项目和子项目) ```bash curl "http://localhost:6170/api/v1/folders/tree" \ -H "Authorization: Bearer YOUR_TOKEN" ``` 2. **测试仅文件夹** ```bash curl "http://localhost:6170/api/v1/folders/tree?include_projects=false" \ -H "Authorization: Bearer YOUR_TOKEN" ``` 3. **测试包含项目但不含子项目** ```bash curl "http://localhost:6170/api/v1/folders/tree?include_projects=true&include_subprojects=false" \ -H "Authorization: Bearer YOUR_TOKEN" ``` 4. **验证节点类型** - 检查 `type` 字段是否正确:`folder`, `project`, `subproject` - 检查项目是否在 `children` 数组中 - 检查子项目是否嵌套在父项目的 `children` 中 ## 相关文件 - `server/app/api/v1/folders.py` - API 路由层 - `server/app/services/folder_service.py` - 业务逻辑层 - `server/app/repositories/folder_repository.py` - 数据访问层 - `docs/requirements/backend/04-services/project/folder-service.md` - 文档规范