在本次创新实训项目的当前阶段中,我负责的是系统中 Service模块的设计与实现。这里的Service并不是后端服务进程,而是平台中可被注册、管理、调用的一类算法以及功能服务。每个Service都需要描述自身的基本信息,例如名称、版本、接口地址、参数模板、输入文件要求、输出结果配置以及可见性等。

在项目初期,前后端框架虽然已经搭好,但service模块后端缺少完整的CRUD路由与业务逻辑;前端也没有完成对应的 API 封装、管理页面和表单页面。因此,当前阶段我的任务如下:

1、完善service模块的数据建模

2、实现后端完整的增删改查接口

3、实现前端列表页与新建/编辑页

4、完成前后端联调,使Service模块具备可实际运行的CRUD能力

这部分工作是后续Service执行、文件输入输出集成、执行记录管理等功能的基础。

一、需求分析与设计思路

在真正开始写代码之前,我先梳理了service模块需要的信息。一个Service至少要包含以下内容:

  • 唯一标识service_id
  • 服务名称name
  • 描述信息description
  • 服务版本version
  • 服务基础地址baseurl
  • 实际调用路径service_suffix
  • 下载路径download_suffix
  • 参数模板parameter_template
  • 参数说明parameter_schema
  • 可接受输入文件配置accepted_files
  • 输出结果配置output_config
  • 可见性 visibility

从架构上来看,后端依旧遵循项目既有的分层结构:

  • API层:负责路由与请求/响应格式
  • Service层:负责业务逻辑、权限校验和流程组织
  • Repository层:负责数据库访问
  • Model层:负责领域模型定义

前端则采用模块化结构:

  • api:统一封装 service 相关请求
  • composables:抽离列表、表单等状态逻辑
  • ui/pages:承担页面与组件渲染

二、后端实现

1. Service数据模型

后端要定义Service领域模型,因为这个模块不仅仅保存几个字符串字段,还要保存参数schema、输入文件约束和输出配置,所以模型设计必须兼顾可扩展性和结构清晰性。

我在模型层中主要定义了Service的核心字段,并约定所有服务记录都使用统一的数据结构存储在数据库中。这样后续无论是列表展示、详情查询,还是服务执行时读取输入输出配置,都可以复用同一套模型。

2. API请求与响应Schema

仅有模型还不够,接口层还需要请求和响应格式。因此schema层有:

  • ServiceCreateRequest
  • ServiceUpdateRequest
  • ServiceResponse
  • ServiceListResponse

3. Repository层实现基础数据操作

完成模型与schema后,下一步是实现数据库层的基本CRUD。Repository 层主要负责:

  • 新建service
  • 根据service_id查询单个service
  • 按条件分页查询service列表
  • 更新service
  • 删除service

这一层只做数据访问,不掺杂业务规则。

async def list_services(
        self,
        query: Optional[Dict[str, Any]] = None,
        skip: int = 0,
        limit: int = 100,
        sort_by: str = "created_at",
        sort_order: int = -1,
    ) -> tuple[List[ServiceInfo], int]:
        query = query or {}
        total = await self.collection.count_documents(query)
        cursor = self.collection.find(query).sort(sort_by, sort_order).skip(skip).limit(limit)
        services: List[ServiceInfo] = []
        async for document in cursor:
            services.append(service_info_from_dict(document))
        return services, total
async def update_service(self, service_id: str, update_data: Dict[str, Any]) -> Optional[ServiceInfo]:
        update_payload = dict(update_data)
        update_payload["updated_at"] = datetime.now(timezone.utc)
        result = await self.collection.update_one({"service_id": service_id}, {"$set": update_payload})
        if result.matched_count == 0:
            return None
        return await self.get_service_by_id(service_id)

4. Service层组织业务逻辑

repository层解决的是怎么查,service层解决的是能不能查、能不能改、为什么这样改。在ServiceService中,我主要补充了这些逻辑:

  • 创建service时补充创建者等信息
  • 查询列表时根据可见性过滤数据
  • 更新service时判断当前用户是否有权限
  • 删除service时保证只允许合法删除

这一步让CRUD不再只是数据库操作,而是具备了真正的业务意义。

async def update_service(self, service_id: str, update_data: Dict[str, Any], user_id: str) -> Dict[str, Any]:
        existing = await self.repository.get_service_by_id(service_id)
        if existing is None:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="service not found")
        if existing.created_by != user_id:
            raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="only the creator can update a service")

        merged = existing.to_dict()
        merged.update({key: value for key, value in update_data.items() if value is not None})
        merged["updated_at"] = datetime.now(timezone.utc)
        validated = ServiceInfo(**merged)
        updated = await self.repository.update_service(service_id, validated.to_dict())
        if updated is None:
            raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="failed to update service")
        return self._service_to_response(updated)

5. 路由层暴露HTTP接口

在完成service层之后,我补全了service路由文件,并将其注册到主应用中。本阶段CRUD相关接口主要包括:

  • POST /api/service/
  • GET /api/service/list
  • GET /api/service/{service_id}
  • PUT /api/service/{service_id}
  • DELETE /api/service/{service_id}

到这里,后端的Service CRUD能力就闭合了。

@router.post("/", response_model=ServiceResponse, status_code=status.HTTP_201_CREATED)
async def create_service(
    request: ServiceCreateRequest,
    user_id: str = Depends(get_current_user),
    service_service: ServiceService = Depends(get_service_service),
) -> ServiceResponse:
    return ServiceResponse(**await service_service.create_service(request.model_dump(exclude_none=True), user_id))

三、前端实现

后端接口有了之后,需要开始编写前端。我把前端工作分成了三个部分:

(1)统一请求层

(2)service API封装

(3)页面与状态管理

1. 统一请求层补齐PUT/DELETE

原有前端框架中,请求层只有最基础的GET/POST封装。但service模块的严格CRUD需要真实使用PUT和DELETE,不能再用POST假装更新或删除。因此我先在统一请求层里补充了put和delete。

同时,为了和当前后端认证方式一致,我还在请求拦截器中统一注入了X-User-Id请求头。这样所有service/file请求都走统一入口,而不是在模块内部零散拼接header。

export const put = <T>(url: string, data?: unknown) =>
  http.put<T>(url, data).then(res => res.data)

export const del = <T>(url: string, params?: object) =>
  http.delete<T>(url, { params }).then(res => res.data)
http.interceptors.request.use(config => {
  const userId = storage.get(AUTH_USER_ID_STORAGE_KEY)

  if (userId) {
    config.headers = config.headers ?? {}
    config.headers["X-User-Id"] = userId
  }

  return config
})

2. 重写service API契约

接着,我在前端service.ts中重新定义了Service的前端类型和请求方法,使其严格对齐后端,主要封装了:

  • getServiceList
  • getServiceById
  • createService
  • updateService
  • deleteService

同时统一了前端使用的字段名,全部采用后端返回的snake_case字段,这样就不会再出现一会儿fileId、一会儿file_id这种混乱情况。

export interface Service {
  service_id: string
  name: string
  description?: string
  version: string
  baseurl: string
  service_suffix: string
  download_suffix?: string
  request_config: Record<string, unknown>
  parameter_template: Record<string, unknown>
  parameter_schema?: Record<string, ParameterSchema>
  accepted_files?: Record<string, AcceptedFileConfig>
  output_config?: OutputConfig
  visibility: ServiceVisibility
  status?: string
  created_at?: string
  updated_at?: string
  created_by?: string
}

3. 实现Service列表页

列表页的核心需求如下:

  • 加载 service 列表
  • 按可见性筛选
  • 删除service
  • 修改公开/私有状态
  • 选择某个service查看详情

我将这部分状态逻辑抽到了useServiceList中,页面本身只负责渲染和交互触发。这样做的好处是,页面更清晰,也方便后续复用和维护。

4. 实现Service新建/编辑表单

相比列表页,表单页更复杂,因为它不仅有基础信息,还包含参数模板、参数schema、accepted files、output config、配置文本。为了避免页面逻辑过于臃肿,我将表单状态统一放入 useServiceForm中,并让ServiceFormPage与ServiceForm共享同一套状态源。

但是,如果页面头部的保存按钮和表单主体不是同一份状态,用户看到的内容和最终提交的数据就可能不一致。因此这一阶段除了功能开发,我也修正了表单状态管理结构,让创建页和编辑页真正可用。

四、前后端联调中遇到的问题

在联调过程中,我主要解决了以下问题:

1. 前端请求路径与后端实际路由不一致

前端最开始使用的是/services,但后端实际注册的被我写成了/service。

如果不统一,列表、详情、更新、删除都会直接请求失败。

2. 更新和删除方法不规范

原本前端倾向于用POST来做更新和删除,但严格的CRUD应该使用PUT更新、DELETE删除,

因此我先改造了统一请求层,再改造service API。

3. 路由参数命名不一致

前端表单页中有的地方取id,有的地方传serviceId。如果不统一,编辑页面会出现页面打开了,但数据没有正确加载的问题。

4. 表单状态来源重复

这是这次前端部分比较隐蔽但影响很大的问题。之前页面和内部组件各自维护一份form状态,看上去都能渲染,但保存时很可能不是用户正在编辑的那份数据。因此我把它统一成了单一状态源。

5. 统一认证头缺失

后端当前依赖X-User-Id来识别用户身份,前端必须在请求层统一注入,不能在 service 模块里单独特殊处理。

解决以上问题后,CRUD基础功能测试成功。

test_create_service_passes_normalized_payload_to_repository (tests.services.test_service_service_crud.TestServiceServiceCrud.test_create_service_passes_normalized_payload_to_repository) ... ok
test_create_service_rejects_duplicate_service_id (tests.services.test_service_service_crud.TestServiceServiceCrud.test_create_service_rejects_duplicate_service_id) ... ok
test_create_service_rejects_unreachable_service_url (tests.services.test_service_service_crud.TestServiceServiceCrud.test_create_service_rejects_unreachable_service_url) ... ok
test_create_service_success_returns_created_service (tests.services.test_service_service_crud.TestServiceServiceCrud.test_create_service_success_returns_created_service) ... ok
test_create_service_uses_given_service_id_when_provided (tests.services.test_service_service_crud.TestServiceServiceCrud.test_create_service_uses_given_service_id_when_provided) ... ok
test_delete_service_raises_403_for_non_creator (tests.services.test_service_service_crud.TestServiceServiceCrud.test_delete_service_raises_403_for_non_creator) ... ok
test_delete_service_raises_404_when_service_not_found (tests.services.test_service_service_crud.TestServiceServiceCrud.test_delete_service_raises_404_when_service_not_found) ... ok
test_delete_service_returns_repository_result (tests.services.test_service_service_crud.TestServiceServiceCrud.test_delete_service_returns_repository_result) ... ok
test_delete_service_success_for_creator (tests.services.test_service_service_crud.TestServiceServiceCrud.test_delete_service_success_for_creator) ... ok
test_get_execution_raises_403_for_other_user (tests.services.test_service_service_crud.TestServiceServiceCrud.test_get_execution_raises_403_for_other_user) ... ok
test_get_execution_raises_404_when_not_found (tests.services.test_service_service_crud.TestServiceServiceCrud.test_get_execution_raises_404_when_not_found) ... ok
test_get_execution_success_for_owner (tests.services.test_service_service_crud.TestServiceServiceCrud.test_get_execution_success_for_owner) ... ok
test_get_queue_status_returns_running_and_queued_counts (tests.services.test_service_service_crud.TestServiceServiceCrud.test_get_queue_status_returns_running_and_queued_counts) ... ok
test_get_service_allows_public_service_for_other_user (tests.services.test_service_service_crud.TestServiceServiceCrud.test_get_service_allows_public_service_for_other_user) ... ok
test_get_service_allows_system_service_for_other_user (tests.services.test_service_service_crud.TestServiceServiceCrud.test_get_service_allows_system_service_for_other_user) ... ok
test_get_service_raises_403_for_other_user_private_service (tests.services.test_service_service_crud.TestServiceServiceCrud.test_get_service_raises_403_for_other_user_private_service) ... ok
test_get_service_raises_404_when_not_found (tests.services.test_service_service_crud.TestServiceServiceCrud.test_get_service_raises_404_when_not_found) ... ok
test_get_service_success_for_creator_private_service (tests.services.test_service_service_crud.TestServiceServiceCrud.test_get_service_success_for_creator_private_service) ... ok
test_list_executions_builds_query_with_service_status_project_and_user_filters (tests.services.test_service_service_crud.TestServiceServiceCrud.test_list_executions_builds_query_with_service_status_project_and_user_filters) ... ok
test_list_services_for_anonymous_user_only_includes_public_and_system (tests.services.test_service_service_crud.TestServiceServiceCrud.test_list_services_for_anonymous_user_only_includes_public_and_system) ... ok
test_list_services_for_logged_in_user_includes_public_system_and_own_private (tests.services.test_service_service_crud.TestServiceServiceCrud.test_list_services_for_logged_in_user_includes_public_system_and_own_private) ... ok
test_list_services_returns_total_and_serialized_services (tests.services.test_service_service_crud.TestServiceServiceCrud.test_list_services_returns_total_and_serialized_services) ... ok
test_list_services_with_explicit_visibility_filter_uses_that_filter (tests.services.test_service_service_crud.TestServiceServiceCrud.test_list_services_with_explicit_visibility_filter_uses_that_filter) ... ok
test_update_service_preserves_existing_fields_when_update_data_missing (tests.services.test_service_service_crud.TestServiceServiceCrud.test_update_service_preserves_existing_fields_when_update_data_missing) ... ok
test_update_service_raises_403_for_non_creator (tests.services.test_service_service_crud.TestServiceServiceCrud.test_update_service_raises_403_for_non_creator) ... ok    
test_update_service_raises_404_when_service_not_found (tests.services.test_service_service_crud.TestServiceServiceCrud.test_update_service_raises_404_when_service_not_found) ... ok
test_update_service_raises_500_when_repository_returns_none (tests.services.test_service_service_crud.TestServiceServiceCrud.test_update_service_raises_500_when_repository_returns_none) ... ok
test_update_service_success_for_creator (tests.services.test_service_service_crud.TestServiceServiceCrud.test_update_service_success_for_creator) ... ok

----------------------------------------------------------------------
Ran 28 tests in 0.552s

OK

五、最终实现效果

完成本阶段开发后,service模块已经具备了完整的基础管理能力。可以创建新的service,可以查看service列表,可以查询单个service详情,可以编辑已有service,可以删除指定service,可以在前端页面完成增删改查操作。

六、总结

回顾这部分工作,我最大的体会是,一个看似普通的CRUD模块,真正难的并不是把增删改查四个动作写出来,而是把这一套能力放进真实项目架构中,保证模型、接口、业务逻辑、页面状态和前后端契约都能真正统一。

后续我会继续在这个模块上推进服务执行、输入输出文件处理以及执行记录管理等功能,让它从可管理进一步走向可执行。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐