Job 批次完成任務 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.

Goal: 讓使用者在待辦任務頁面勾選多個 PROCESSING 狀態的任務,填寫統一的 comment,一次批次完成。

Architecture: 新增一支批次完成 API,接收 job_uid 陣列 + comment,後端逐筆查詢 job 的 template_job_idworkflow_execution_uid,再逐筆呼叫現有 WorkflowExecutionService.complete_job()。每筆獨立處理成功/失敗,回傳彙總結果。

Tech Stack: Flask-RESTful、Marshmallow、現有 WorkflowExecutionService


§1

設計

API 規格

POST /api/1.0/grc/project/<project_uid>/jobs/batch-complete
Authorization: Bearer <token>
Content-Type: application/json

{
  "comment": "批次完成說明(選填)",
  "job_uids": [
    "job-uid-1",
    "job-uid-2",
    "job-uid-3"
  ]
}

Response:

{
  "status": true,
  "data": {
    "total": 3,
    "completed_count": 2,
    "failed_count": 1,
    "results": [
      {"job_uid": "job-uid-1", "success": true},
      {"job_uid": "job-uid-2", "success": true},
      {"job_uid": "job-uid-3", "success": false, "error": "任務不在處理中狀態"}
    ]
  }
}

設計決策

決策 說明
逐筆 complete 每個 job 屬於不同 workflow,有各自的狀態機,不能共用 transaction
部分成功 不因單筆失敗而回滾全部,回傳每筆的成功/失敗結果
comment 共用 所有被選的 job 套用同一個 comment
狀態過濾 只處理 PROCESSING 狀態的 job,其他狀態的報錯但不中斷
無需 template_job_id 前端只需送 job_uid,後端從 DB 反查 template_job_idworkflow_execution_uid

查詢路徑

job_uid → JobExecution (查 template_job_id, workflow_execution_id)
       → WorkflowExecution (查 uid)
       → complete_job(workflow_execution_uid, template_job_id, comment)

§2

File Structure

檔案 職責 操作
app/grc/service/job_batch_complete_service.py 批次完成業務邏輯 Create
api/grc/routes/job_batch_complete_route.py API route Create
api/grc/serializers/job_batch_complete.py Marshmallow schemas Create
api/grc/__init__.py 註冊 route Modify
di_containers/grc/grc_containers.py DI wiring Modify
docs/changelog/2026-03-28-batch-complete-jobs.md 變更紀錄 Create

§3

Tasks

Task 1: Serializers

Create api/grc/serializers/job_batch_complete.py:

"""Job 批次完成 Serializers"""
from marshmallow import Schema, fields


class BatchCompleteRequestSchema(Schema):
    comment = fields.String(allow_none=True, load_default="")
    job_uids = fields.List(fields.String(), required=True)


class BatchCompleteResultItemSchema(Schema):
    job_uid = fields.String()
    success = fields.Boolean()
    error = fields.String(allow_none=True, load_default=None)


class BatchCompleteResponseSchema(Schema):
    total = fields.Integer()
    completed_count = fields.Integer()
    failed_count = fields.Integer()
    results = fields.List(fields.Nested(BatchCompleteResultItemSchema))

Task 2: Service

Create app/grc/service/job_batch_complete_service.py:

需要注入:

  • WorkflowExecutionService — 呼叫 complete_job()
  • infra 層查詢 job_uid → template_job_id + workflow_execution_uid

由於 complete_job 內部有自己的 @transaction,批次完成不應包在外層 transaction 裡(每筆獨立 commit/rollback)。

"""Job 批次完成 Service"""
import logging

from jedi_common.session.database.session_context import get_session
from jedi_flow_engine.common.enum.job_code import JobType
from jedi_flow_engine.infra.models.job_execution import JobExecution
from infra.flow_engine.models.ext_workflow_execution import ExtWorkflowExecution
from app.flow_engine.service.workflow_execution_service import WorkflowExecutionService

logger = logging.getLogger(__name__)


class JobBatchCompleteService:

    def __init__(self, workflow_execution_service: WorkflowExecutionService):
        self._wf_svc = workflow_execution_service

    def batch_complete(self, job_uids: list, comment: str, login_name: str, nickname: str) -> dict:
        """批次完成任務

        逐筆呼叫 complete_job,每筆獨立處理成功/失敗。
        """
        session = get_session()

        # 批次查詢 job_uid → (template_job_id, workflow_execution_id)
        jobs = (
            session.query(
                JobExecution.uid,
                JobExecution.template_job_id,
                JobExecution.workflow_execution_id,
            )
            .filter(
                JobExecution.uid.in_(job_uids),
                JobExecution.type == JobType.USER,
            )
            .all()
        )
        job_map = {str(j.uid): (j.template_job_id, j.workflow_execution_id) for j in jobs}

        # 批次查詢 workflow_execution_id → uid
        wf_exec_ids = list({v[1] for v in job_map.values() if v[1]})
        wf_uid_map = {}
        if wf_exec_ids:
            wf_rows = (
                session.query(ExtWorkflowExecution.id, ExtWorkflowExecution.uid)
                .filter(ExtWorkflowExecution.id.in_(wf_exec_ids))
                .all()
            )
            wf_uid_map = {r.id: str(r.uid) for r in wf_rows}

        results = []
        completed_count = 0

        for job_uid in job_uids:
            if job_uid not in job_map:
                results.append({"job_uid": job_uid, "success": False, "error": "任務不存在"})
                continue

            template_job_id, wf_exec_id = job_map[job_uid]
            wf_uid = wf_uid_map.get(wf_exec_id)
            if not wf_uid or not template_job_id:
                results.append({"job_uid": job_uid, "success": False, "error": "找不到對應的工作流程"})
                continue

            try:
                self._wf_svc.complete_job(
                    workflow_execution_uid=wf_uid,
                    job_id=template_job_id,
                    user=login_name,
                    comment=comment,
                    user_nickname=nickname,
                )
                completed_count += 1
                results.append({"job_uid": job_uid, "success": True, "error": None})
            except Exception as e:
                logger.warning(f"Batch complete failed for job {job_uid}: {e}")
                results.append({"job_uid": job_uid, "success": False, "error": str(e)})

        return {
            "total": len(job_uids),
            "completed_count": completed_count,
            "failed_count": len(job_uids) - completed_count,
            "results": results,
        }

Task 3: Route

Create api/grc/routes/job_batch_complete_route.py:

"""Job 批次完成 Route"""
from dependency_injector.wiring import inject, Provide
from flask import request
from flask_apispec import MethodResource, doc, use_kwargs, marshal_with
from flask_jwt_extended import jwt_required
from jedi_common.session.auth.auth_context import get_user_context

from api.grc.serializers.job_batch_complete import (
    BatchCompleteRequestSchema,
    BatchCompleteResponseSchema,
)
from app.grc.service.job_batch_complete_service import JobBatchCompleteService
from common.util.response_util import return_response
from di_containers.containers import Containers

AUTH_PARAMS = {"Authorization": {"description": "Bearer token", "in": "header", "type": "string"}}


class JobBatchCompleteRoute(MethodResource):
    """POST /grc/project/<project_uid>/jobs/batch-complete"""

    @doc(description="批次完成任務", tags=["GRC Projects"], params=AUTH_PARAMS)
    @use_kwargs(BatchCompleteRequestSchema, location="json", apply=False)
    @marshal_with(BatchCompleteResponseSchema, apply=False)
    @jwt_required()
    @inject
    def post(
        self,
        project_uid: str,
        batch_complete_service: JobBatchCompleteService = Provide[
            Containers.grc_container.job_batch_complete_service
        ],
    ):
        payload = request.get_json(silent=True) or {}
        data = BatchCompleteRequestSchema().load(payload)
        user = get_user_context()
        result = batch_complete_service.batch_complete(
            job_uids=data["job_uids"],
            comment=data.get("comment", ""),
            login_name=user.login_name,
            nickname=user.nickname or "",
        )
        return return_response(True, BatchCompleteResponseSchema().dump(result))

Task 4: Blueprint + DI

api/grc/__init__.py — import + register:

from api.grc.routes.job_batch_complete_route import JobBatchCompleteRoute

api.add_resource(
    JobBatchCompleteRoute,
    "/project/<project_uid>/jobs/batch-complete",
)

di_containers/grc/grc_containers.py — wiring:

from app.grc.service.job_batch_complete_service import JobBatchCompleteService

job_batch_complete_service = providers.Factory(
    JobBatchCompleteService,
    workflow_execution_service=workflow_execution_container.workflow_execution_service,
)