You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
12 KiB
12 KiB
Folder Tree API 完整修复
日期: 2026-02-06
类型: Bug 修复 + 功能增强
接口: GET /api/v1/folders/tree
问题描述
/api/v1/folders/tree 接口实现与文档规范不符:
- ❌ 参数默认值错误(
include_projects默认False,应为True) - ❌ 缺少
include_subprojects参数 - ❌ 返回格式错误(项目在单独的
projects字段,应在children数组) - ❌ 缺少
type字段标识节点类型 - ❌ 缺少子项目支持
- ❌ 根节点格式不符合规范
- ❌
projectCount和subfolderCount始终返回 0(UUID 类型转换问题)
修复内容
1. API 路由层修复 (folders.py)
修复参数默认值和新增参数:
@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)
新增参数并修复返回格式:
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)
修复核心逻辑:
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
修复统计方法:
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()
新增辅助方法:
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())
修复后的响应格式
请求示例
# 包含项目和子项目(默认)
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
响应格式(符合规范)
{
"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 |
两种格式都可以正常使用!
测试用例
-
测试默认行为(应包含项目和子项目)
curl "http://localhost:6170/api/v1/folders/tree" \ -H "Authorization: Bearer YOUR_TOKEN" -
测试仅文件夹
curl "http://localhost:6170/api/v1/folders/tree?include_projects=false" \ -H "Authorization: Bearer YOUR_TOKEN" -
测试包含项目但不含子项目
curl "http://localhost:6170/api/v1/folders/tree?include_projects=true&include_subprojects=false" \ -H "Authorization: Bearer YOUR_TOKEN" -
验证节点类型
- 检查
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- 文档规范