日期:2026-03-24 狀態:Approved
讓現有 GRC API 支援「一個專案 = 多個 AP(Assessment Plan)」,每個 AP 代表一輪稽核。 為 Phase 3-4(外部稽核、矯正)打下基礎。
project_assessment_plan_mapping 已支援一個 project 對應多筆 APGET /api/1.0/grc/project/<project_uid>/assessment-plans/menu
| 層 | 檔案 | 新增/修改 |
|---|---|---|
| Route | api/grc/routes/assessment_plan_route.py |
新增 |
| Serializer | api/grc/serializers/assessment_plan.py |
新增 |
| App Service | 複用 ProjectService 新增方法 |
修改 |
| Repo | infra/grc/repository/grc_project_repo_impl.py 新增方法 |
修改 |
| DTO | app/grc/dto/assessment_plan_dto.py |
新增 |
| Entity | domain/grc/entities/grc_assessment_plan_entity.py |
新增 |
| Blueprint | api/grc/__init__.py 註冊新 route |
修改 |
{
"code": 1,
"data": [
{
"uid": "string",
"name": "string",
"round_number": 1,
"status": "preparing | in_progress | auditing | remediation | closed",
"created_at": "2025-03-15T00:00:00"
}
]
}session.query(OscalAssessmentPlan)
.join(ProjectAssessmentPlanMapping)
.join(Project)
.filter(Project.uid == project_uid)
.order_by(OscalAssessmentPlan.created_at.desc())
.all()round_number 由查詢結果的順序推導(按 created_at 排序後的序號),不需要額外欄位。
ap_uid所有查詢 AP 控制項結構的 route,URL 從:
/grc/project/<project_uid>/...
改為:
/grc/project/<project_uid>/ap/<ap_uid>/...
| 檔案 | Resource Class | 現有 URL | 新 URL |
|---|---|---|---|
control_group_route.py |
ProjectControlGroupListResource |
POST .../control-groups/list |
POST .../ap/<ap_uid>/control-groups/list |
control_group_route.py |
ProjectControlGroupDetailResource |
GET .../control-group/<gid> |
GET .../ap/<ap_uid>/control-group/<gid> |
control_route.py |
ProjectControlListResource |
POST .../control-group/<gid>/controls/list |
POST .../ap/<ap_uid>/control-group/<gid>/controls/list |
control_route.py |
ProjectControlDetailResource |
GET .../control-group/<gid>/control/<cid> |
GET .../ap/<ap_uid>/control-group/<gid>/control/<cid> |
control_route.py |
ProjectControlWithAoDetailResource |
GET .../control-group/<gid>/control/<cid>/detail |
GET .../ap/<ap_uid>/control-group/<gid>/control/<cid>/detail |
assessment_object_route.py |
ProjectAOListResource |
POST .../assessment-objects/list |
POST .../ap/<ap_uid>/.../assessment-objects/list |
job_route.py |
ProjectJobListResource |
POST .../assessment-object/<ao>/jobs/list |
POST .../ap/<ap_uid>/.../assessment-object/<ao>/jobs/list |
job_route.py |
ProjectJobCreateResource |
POST .../assessment-object/<ao>/jobs |
POST .../ap/<ap_uid>/assessment-object/<ao>/jobs |
task_setup_route.py |
ProjectTaskSetupTreeResource |
GET .../task-setup/tree |
GET .../ap/<ap_uid>/task-setup/tree |
review_route.py |
ControlReviewResource |
POST/DELETE .../control/<cid>/review |
POST/DELETE .../ap/<ap_uid>/control/<cid>/review |
review_route.py |
AoReviewResource |
POST/DELETE .../control/<cid>/ao/<ao>/review |
POST/DELETE .../ap/<ap_uid>/control/<cid>/ao/<ao>/review |
每個 Resource class 的改動一致:
ap/<ap_uid> 路徑段ap_uid: str 參數ap_uid 到 service 層# 舊
class ProjectControlGroupListResource(MethodResource):
def post(self, project_uid: str, ...):
result = service.list_control_groups(project_uid=project_uid, ...)
# 新
class ProjectControlGroupListResource(MethodResource):
def post(self, project_uid: str, ap_uid: str, ...):
result = service.list_control_groups(ap_uid=ap_uid, ...)| Route | 原因 |
|---|---|
project_route.py 全部 |
專案層級,不涉及 AP 結構 |
job_route.py — Detail/Update/Delete/MyJobs |
by job_uid 直查 |
job_comment_route.py |
by job_execution_uid |
dashboard_route.py |
後端自動取最新 AP |
所有受影響的 service method 簽名:project_uid 改為 ap_uid。
# 舊
def list_control_groups(self, user_id, is_admin, project_uid, pager, sorts, **filters):
# 新
def list_control_groups(self, user_id, is_admin, ap_uid, pager, sorts, **filters):| Service | 方法 |
|---|---|
ControlGroupService |
list_control_groups(), get_control_group() |
ControlService |
list_controls(), get_control(), get_control_with_ao_detail() |
AssessmentObjectService |
list_assessment_objects() |
JobService |
list_jobs(), create_job() |
TaskSetupService |
get_task_setup_tree() |
ReviewService |
mark_control(), unmark_control(), mark_ao(), unmark_ao() |
DashboardService |
get_dashboard_summary() — 內部改為取最新 AP |
所有 repo 的查詢入口從:
# 舊:從 project_uid 推導 ap_id(假設 1:1)
mapping = session.query(ProjectAssessmentPlanMapping) \
.join(Project) \
.filter(Project.uid == project_uid) \
.first()
ap_id = mapping.assessment_plan_id改為:
# 新:直接用 ap_uid 查
ap = session.query(OscalAssessmentPlan) \
.filter(OscalAssessmentPlan.uid == ap_uid) \
.first()
if not ap:
raise NotFound(GrcErrorCode.GRC_AP_NOT_FOUND)
ap_id = ap.id| Repo | 需改的方法 |
|---|---|
grc_control_group_repo_impl.py |
list_control_groups(), get_control_group_by_uid() |
grc_control_repo_impl.py |
list_controls(), get_control_by_uid(), get_control_with_ao_detail() |
grc_assessment_object_repo_impl.py |
list_assessment_objects() |
grc_job_repo_impl.py |
list_jobs(), create_job() |
grc_task_setup_repo_impl.py |
get_task_setup_tree() |
grc_review_repo_impl.py |
add_review(), remove_review() |
grc_dashboard_repo_impl.py |
get_dashboard_summary() |
Dashboard 不接收 ap_uid,內部改為取每個 project 最新的 AP:
# 取最新 AP(created_at 最大的那筆)
latest_mapping = session.query(ProjectAssessmentPlanMapping) \
.filter(ProjectAssessmentPlanMapping.project_id == project_id) \
.join(OscalAssessmentPlan) \
.order_by(OscalAssessmentPlan.created_at.desc()) \
.first()# common/code/grc_error_code.py
GRC_AP_NOT_FOUND = ("稽核計畫不存在", "GRC_404015")ap_uid 為 URL path 必填參數,不做 fallback。
理由:
舊 URL 保留一段時間(不刪除),但內部一律 fallback 到最新 AP,並 log warning。 前端完成遷移後移除舊 URL。
| 類別 | 說明 |
|---|---|
| DB schema | project_assessment_plan_mapping 已支援多筆,不需 migration |
| 專案 CRUD | project_route.py、ProjectService、grc_project_repo_impl 不動 |
| My Jobs | 透過 workflow_execution_uid 直查,不涉及 AP 選擇 |
| Job Detail/Update/Delete | by job_uid 直查 |
| Job Comment | by job_execution_uid |
| Job Evidence | by job_execution_uid |
| SSP 編輯 | by ssp_uid |
| Participant CRUD | by project_id |
| DI Container | 不需要調整(service 簽名變更不影響 DI) |
| 檔案 | 用途 |
|---|---|
api/grc/routes/assessment_plan_route.py |
AP 列表 route |
api/grc/serializers/assessment_plan.py |
AP response schema |
app/grc/dto/assessment_plan_dto.py |
AP DTO |
domain/grc/entities/grc_assessment_plan_entity.py |
AP entity |
| 檔案 | 改動 |
|---|---|
api/grc/__init__.py |
註冊 AP route |
api/grc/routes/control_group_route.py |
URL + handler 簽名 |
api/grc/routes/control_route.py |
URL + handler 簽名 |
api/grc/routes/assessment_object_route.py |
URL + handler 簽名 |
api/grc/routes/job_route.py |
URL + handler 簽名(list + create) |
api/grc/routes/task_setup_route.py |
URL + handler 簽名 |
api/grc/routes/review_route.py |
URL + handler 簽名 |
app/grc/service/control_group_service.py |
method 簽名 |
app/grc/service/control_service.py |
method 簽名 |
app/grc/service/assessment_object_service.py |
method 簽名 |
app/grc/service/job_service.py |
method 簽名 |
app/grc/service/task_setup_service.py |
method 簽名 |
app/grc/service/review_service.py |
method 簽名 |
app/grc/service/dashboard_service.py |
內部取最新 AP |
infra/grc/repository/grc_control_group_repo_impl.py |
查詢入口 |
infra/grc/repository/grc_control_repo_impl.py |
查詢入口 |
infra/grc/repository/grc_assessment_object_repo_impl.py |
查詢入口 |
infra/grc/repository/grc_job_repo_impl.py |
查詢入口 |
infra/grc/repository/grc_task_setup_repo_impl.py |
查詢入口 |
infra/grc/repository/grc_review_repo_impl.py |
查詢入口 |
infra/grc/repository/grc_dashboard_repo_impl.py |
查詢入口 |
domain/grc/service/grc_control_group_domain_service.py |
method 簽名 |
domain/grc/service/grc_control_domain_service.py |
method 簽名 |
domain/grc/service/grc_assessment_object_domain_service.py |
method 簽名 |
domain/grc/service/grc_job_domain_service.py |
method 簽名 |
domain/grc/service/grc_task_setup_domain_service.py |
method 簽名 |
domain/grc/service/grc_review_domain_service.py |
method 簽名 |
domain/grc/service/grc_dashboard_domain_service.py |
method 簽名 |
common/code/grc_error_code.py |
新增 GRC_AP_NOT_FOUND |