external.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. """
  2. External API Router.
  3. Provides endpoints for external system integration (e.g., Sample Center).
  4. All endpoints require admin authentication.
  5. """
  6. from fastapi import APIRouter, HTTPException, status, Request
  7. from schemas.external import (
  8. ProjectInitRequest, ProjectInitResponse,
  9. ProgressResponse,
  10. ExternalExportRequest, ExternalExportResponse,
  11. ErrorResponse
  12. )
  13. from services.external_service import ExternalService
  14. import logging
  15. logger = logging.getLogger(__name__)
  16. router = APIRouter(
  17. prefix="/api/external",
  18. tags=["external"]
  19. )
  20. def verify_admin(request: Request) -> dict:
  21. """
  22. 验证请求者是否为管理员
  23. Args:
  24. request: FastAPI Request对象
  25. Returns:
  26. 用户信息字典
  27. Raises:
  28. HTTPException: 401 如果未认证,403 如果不是管理员
  29. """
  30. user = getattr(request.state, 'user', None)
  31. if not user:
  32. raise HTTPException(
  33. status_code=status.HTTP_401_UNAUTHORIZED,
  34. detail={
  35. "error_code": "INVALID_TOKEN",
  36. "message": "Token无效或已过期"
  37. }
  38. )
  39. if user.get("role") != "admin":
  40. raise HTTPException(
  41. status_code=status.HTTP_403_FORBIDDEN,
  42. detail={
  43. "error_code": "PERMISSION_DENIED",
  44. "message": "权限不足,需要管理员权限"
  45. }
  46. )
  47. return user
  48. @router.post("/projects/init", response_model=ProjectInitResponse, status_code=status.HTTP_201_CREATED)
  49. async def init_project(request: Request, init_request: ProjectInitRequest):
  50. """
  51. 项目初始化接口
  52. 创建新的标注项目并批量导入任务数据。
  53. Args:
  54. request: FastAPI Request对象
  55. init_request: 项目初始化请求
  56. Returns:
  57. ProjectInitResponse: 项目初始化响应,包含项目ID
  58. Raises:
  59. HTTPException: 401 未授权,403 权限不足,400 请求参数错误
  60. """
  61. # 验证管理员权限
  62. user = verify_admin(request)
  63. # 验证请求数据
  64. if not init_request.data:
  65. raise HTTPException(
  66. status_code=status.HTTP_400_BAD_REQUEST,
  67. detail={
  68. "error_code": "INVALID_REQUEST",
  69. "message": "任务数据列表不能为空"
  70. }
  71. )
  72. try:
  73. # 调用服务创建项目
  74. result = ExternalService.init_project(init_request, user["id"])
  75. logger.info(f"外部系统创建项目成功: {result.project_id}, 任务数: {result.task_count}")
  76. return result
  77. except Exception as e:
  78. logger.error(f"创建项目失败: {str(e)}")
  79. raise HTTPException(
  80. status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
  81. detail={
  82. "error_code": "INTERNAL_ERROR",
  83. "message": f"创建项目失败: {str(e)}"
  84. }
  85. )
  86. @router.get("/projects/{project_id}/progress", response_model=ProgressResponse)
  87. async def get_project_progress(request: Request, project_id: str):
  88. """
  89. 项目进度查询接口
  90. 查询指定项目的标注进度和人员完成情况。
  91. Args:
  92. request: FastAPI Request对象
  93. project_id: 项目ID
  94. Returns:
  95. ProgressResponse: 项目进度响应
  96. Raises:
  97. HTTPException: 401 未授权,403 权限不足,404 项目不存在
  98. """
  99. # 验证管理员权限
  100. verify_admin(request)
  101. try:
  102. # 调用服务获取进度
  103. result = ExternalService.get_project_progress(project_id)
  104. if not result:
  105. raise HTTPException(
  106. status_code=status.HTTP_404_NOT_FOUND,
  107. detail={
  108. "error_code": "PROJECT_NOT_FOUND",
  109. "message": "项目不存在",
  110. "details": {"project_id": project_id}
  111. }
  112. )
  113. return result
  114. except HTTPException:
  115. raise
  116. except Exception as e:
  117. logger.error(f"查询项目进度失败: {str(e)}")
  118. raise HTTPException(
  119. status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
  120. detail={
  121. "error_code": "INTERNAL_ERROR",
  122. "message": f"查询进度失败: {str(e)}"
  123. }
  124. )
  125. @router.post("/projects/{project_id}/export", response_model=ExternalExportResponse)
  126. async def export_project_data(
  127. request: Request,
  128. project_id: str,
  129. export_request: ExternalExportRequest
  130. ):
  131. """
  132. 数据导出接口
  133. 导出项目的标注数据,支持多种格式。
  134. Args:
  135. request: FastAPI Request对象
  136. project_id: 项目ID
  137. export_request: 导出请求
  138. Returns:
  139. ExternalExportResponse: 导出响应,包含下载链接
  140. Raises:
  141. HTTPException: 401 未授权,403 权限不足,404 项目不存在
  142. """
  143. # 验证管理员权限
  144. verify_admin(request)
  145. # 检查项目是否存在
  146. if not ExternalService.check_project_exists(project_id):
  147. raise HTTPException(
  148. status_code=status.HTTP_404_NOT_FOUND,
  149. detail={
  150. "error_code": "PROJECT_NOT_FOUND",
  151. "message": "项目不存在",
  152. "details": {"project_id": project_id}
  153. }
  154. )
  155. try:
  156. # 获取基础URL
  157. base_url = str(request.base_url).rstrip('/')
  158. # 调用服务导出数据
  159. result = ExternalService.export_project_data(
  160. project_id,
  161. export_request,
  162. base_url
  163. )
  164. if not result:
  165. raise HTTPException(
  166. status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
  167. detail={
  168. "error_code": "EXPORT_FAILED",
  169. "message": "导出失败"
  170. }
  171. )
  172. logger.info(f"项目数据导出成功: {project_id}, 格式: {export_request.format.value}, 数量: {result.total_exported}")
  173. # 如果有回调URL,异步发送通知(这里简化处理,实际应使用后台任务)
  174. if export_request.callback_url:
  175. logger.info(f"回调URL已配置: {export_request.callback_url}")
  176. # TODO: 实现异步回调通知
  177. return result
  178. except HTTPException:
  179. raise
  180. except Exception as e:
  181. logger.error(f"导出项目数据失败: {str(e)}")
  182. raise HTTPException(
  183. status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
  184. detail={
  185. "error_code": "EXPORT_FAILED",
  186. "message": f"导出失败: {str(e)}"
  187. }
  188. )