建議 model:opus/effort:medium — 跨套件與主專案兩側、要定 port 契約、要接 DI,簽章對不上只在端點被打時才炸。
本卡屬 FR-107(母卡 CM-1843)第 .1 棒(子需求卡 CM-1844),子任務 T-1.3:做出建批次/列批次/批次詳情/上傳檔/刪檔/完成上傳的 API,定義「取檔放檔」插座,主專案接上現有儲存後端。
有了資料表(T-1.2)之後,這張卡把使用者真的能用的 API 做出來:建一個批次、把檔傳進去、列出有哪些批次、看某個批次裡有什麼檔、刪掉其中一檔、按「完成上傳」。
同時定義 IEvidenceStorage 這個插座(port)——套件只說「我需要有人幫我存檔/取檔/刪檔」,實際存在主機硬碟、物件儲存還是客戶內網的遠端代理,由主專案插上去的**轉接頭(adapter)**決定。這張卡把插座定義好,並在主專案寫第一版轉接頭(接現有的 jedi-file-upload 的 IUploadFileProvider)。
兩個守門要一起做:① 防呆——系統還沒設定「檔案存到哪」時,建批次/上傳要回 412 並明確說「請先到系統設定完成儲存設定」,不能偷偷寫到 /tmp(重開機就不見)。② 權限——專案經理(manager)可以建/傳/刪,稽核員(auditor)只能看(D8)。
套件(走 path dependency 開發)
~/Projects/Jedicogy/module/jedi-python-package/jedi-evidence-classification/
.../domain/ports.py:45 IProjectDirectory / :56 IProjectRoleGuard / :73 IEvidenceSource
← 加 IEvidenceStorage(見下方簽名)
← IProjectRoleGuard 加 is_project_participant(D8 auditor 只看)
.../api/routing.py:42 起 ← 加批次系列 route
.../app/service/evidence_batch_service.py ← T-1.2 建的,這裡補 API 對應的 method
.../error_code.py ← 加四支 error code
主專案
core/plugins/evidence_classification.py
:97 DriveEvidenceSourceAdapter ← 不動(舊線)
:127 LivingSspControlCatalogAdapter ← 不動
:170 build_adapters() ← 多一個參數
← 新增 UploadProviderEvidenceStorageAdapter
← ExactProjectManagerGuard 加 is_project_participant
di_containers/evidence_classification/evidence_classification_containers.py ← 同步注入
參考
common/authz/project.py:16 assert_project_manager / :101 assert_project_participant
app/upload_file/service/managed_file_upload_service.py:324 upload_files_for_tenant()
@dataclass(frozen=True)
class StagedFile:
file_id: int # 內部 int(upload_files.id)
file_uid: str # 對外 uid
original_name: str
local_path: Path # 已拉到 job 目錄的實體路徑
content_hash: str | None
class IEvidenceStorage(Protocol):
"""證據檔的取檔/放檔。主專案接 jedi-file-upload 的 IUploadFileProvider。
套件與容器只認這個;後端是 local/minio/seaweedfs/remote_agent 由宿主決定。"""
def is_configured(self, tenant_id: int) -> bool: ...
# D6:未設 STORAGE_CONFIG 回 False,套件在批次上傳路徑拋 412 EC_STORAGE_NOT_CONFIGURED
def save(self, tenant_id: int, owner_user_id: int, file: FileStorage) -> tuple[int, str, str | None]: ...
# 存一份檔,回 (file_id, file_uid, sha256);宿主帶 tenant 脈絡寫 upload_files
def get_bytes(self, tenant_id: int, file_uid: str) -> bytes: ...
# 拉一份檔的內容(審閱頁預覽用)
def stage_to_dir(self, tenant_id: int, file_uids: list[str], work_dir: Path) -> list[StagedFile]: ...
# ⚠️ 本卡只定義簽名,實作留 T-2.2
def delete(self, tenant_id: int, file_uid: str) -> bool: ...
# D7 實刪;宿主走 IUploadFileProvider.delete_file
# IProjectRoleGuard 加一支(現有只有 is_project_manager / is_any_project_manager)
def is_project_participant(self, project_id: int, user_id: int) -> bool: ...
# 宿主 adapter 委派 common/authz/project.py:101 assert_project_participant
方法 路徑 守門 說明
POST /evidence-batches manager body {round_uid} → 建批次(uploading)
先 is_configured 否則 412
GET /evidence-batches?round_uid=&status= participant 列批次(分頁,繼承 RequestMetaSchema)
GET /evidence-batches/<batch_uid> participant 批次詳情+檔案列表
POST /evidence-batches/<batch_uid>/files manager multipart 單檔或多檔
classifying 起回 409
DELETE /evidence-batches/<batch_uid>/files/<file_uid> manager 刪檔(實刪+列標 removed)
POST /evidence-batches/<batch_uid>/seal manager uploading → ready
新增 error code(套件 error_code.py,前綴沿用套件既有的 EC_):EC_STORAGE_NOT_CONFIGURED(412)、EC_BATCH_SEALED(409)、EC_BATCH_INVALID_TRANSITION(409)、EC_BATCH_NOT_FOUND(404)。BE 新增 error code 必須同步前端的 error-code.json(否則前端顯示空白訊息且不報錯)——這步併在 T-1.4 做,本卡回寫時要交代清楚新增了哪幾支。
domain/ports.py 加 StagedFile dataclass 與 IEvidenceStorage Protocol(照上面簽名逐字),IProjectRoleGuard 加 is_project_participant。UploadProviderEvidenceStorageAdapter,接 jedi-file-upload 的 IUploadFileProvider:is_configured 查該租戶的 STORAGE_CONFIG 有沒有設;save 走 managed_file_upload_service.py:324 upload_files_for_tenant() 那條路徑(canonical,不要自己另寫一隻)並帶上 T-1.1 新加的 tenant_id/owner_user_id;get_bytes 走 get_file;delete 走 delete_file。stage_to_dir 本卡先 raise NotImplementedError,T-2.2 補。IProjectRoleGuard,宿主 adapter 委派 common/authz/。絕不在套件裡另寫一套守門。build_adapters() 多一個參數 + DI container 同步注入。🔴 DI 簽章漂移是靜默的——Factory 是 lazy 的,__init__ 參數與 container 對不上時不會在啟動時炸,只在該端點被打時才 500。所以驗收一定要實際打過每一支新 route,不能只看程式跑得起來。RequestMetaSchema、response 繼承 EnvelopeSchema;命名 <Resource>ListRequestSchema/<Resource>ResponseSchema/<Resource>ListResponseSchema。參考 docs/claude/api-patterns.md。raise ValueError(...) 或裸字串:from jedi_common.handler.exception import ...,412 用 PreconditionFailedError、409 用 ConflictError、404 用 NotFound、403 用 ForbiddenError。