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_id 和 workflow_execution_uid,再逐筆呼叫現有 WorkflowExecutionService.complete_job()。每筆獨立處理成功/失敗,回傳彙總結果。
Tech Stack: Flask-RESTful、Marshmallow、現有 WorkflowExecutionService
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_id 和 workflow_execution_uid |
job_uid → JobExecution (查 template_job_id, workflow_execution_id)
→ WorkflowExecution (查 uid)
→ complete_job(workflow_execution_uid, template_job_id, comment)
| 檔案 | 職責 | 操作 |
|---|---|---|
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 |
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))Create app/grc/service/job_batch_complete_service.py:
需要注入:
WorkflowExecutionService — 呼叫 complete_job()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,
}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))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,
)