建議 model:opus/effort:medium — 要設計兩張新表、寫 RLS、實作狀態機轉換表,是整案的資料地基。
本卡屬 FR-107(母卡 CM-1843)第 .1 棒(子需求卡 CM-1844),子任務 T-1.2:把「暫存批次」與「批次裡的檔」兩張資料表建起來,加上對應的程式物件與狀態機骨架。
暫存批次=使用者一次上傳的一堆證據檔,還沒分類、還沒掛任何任務,先放在一個「籃子」裡。這張卡把籃子本身的資料結構做出來:一張表記籃子(evidence_batches),一張表記籃子裡有哪些檔(evidence_batch_files)。
同時做狀態機骨架:籃子會在「上傳中 → 就緒 → 分類中 → 審閱 → 已歸檔」之間走,什麼狀態可以做什麼動作要寫死在一張轉換表裡,非法轉換一律擋掉回 409。這張卡先做到「上傳中/就緒」兩個狀態,後面的狀態在 T-3.2 補。
這張卡不做 API(那是 T-1.3),只做表 + 資料物件 + 狀態機的規則。
套件(走 path dependency 開發)
~/Projects/Jedicogy/module/jedi-python-package/jedi-evidence-classification/
.../infra/model/evidence_batch_model.py ← 新增
.../infra/model/evidence_batch_file_model.py ← 新增
.../domain/entity/ 對應 entity + query entity + repo interface
.../infra/repository/ repo impl(繼承 BaseRepositoryImpl)
.../app/service/evidence_batch_service.py ← 新增,ALLOWED_TRANSITIONS
.../di_containers/ 註冊
主專案
scripts/sql/packages/jedi_evidence_classification/00N-evidence-batches.sql ← 新增
RLS 範本
scripts/sql/packages/jedi_asset/002-asset-rls-grants.sql:49-75
欄位 型別 說明
id serial PK
uid varchar(36) UNIQUE NOT NULL 對外定址
tenant_id / org_unit_id int NOT NULL RLS 與租戶隔離
(tenant-scoped 表必含 org_unit_id)
round_id int NOT NULL 批次掛稽核輪次。
🔴 軟參照、不建 FK——project_audit_rounds
屬另一支插件(jedi-compliance-audit),
插件互不相依;存在性由宿主
IClassificationContext.resolve_round 驗
project_id / project_uid int NOT NULL / varchar(36) 冗餘,列表與專案角色守門用
status varchar(16) NOT NULL uploading/ready/classifying/
review/archived/failed
model varchar(64) 本批實際用的 AI 模型
confidence_threshold numeric(4,2) 信心門檻
profile_id int NULL 本批解析到哪個 AI 分類設定(NULL=內建通用)
current_run_id int NULL 最新一次分類結果(重新分類會換)
job_started_at timestamptz NULL D4 心跳(本卡只建欄位,邏輯在 T-3.2)
job_heartbeat_at timestamptz NULL 同上
failure_reason text NULL 容器失敗/逾時/心跳逾時
file_count int DEFAULT 0 統計,各階段回寫
classified_count int DEFAULT 0
archived_count int DEFAULT 0
no_task_count int DEFAULT 0
archived_at / archived_by_user_id timestamptz / int
purged_at / purged_by_user_id timestamptz / int
created_user / updated_user / created_at / updated_at 慣例四欄
🔴 審計欄位回傳規範:API 回傳 created_user/updated_user(帳號)時,必須同時回傳 created_user_name/updated_user_name(暱稱),在 app service 層批次查轉換,不要在 infra 層 JOIN。canonical 是 common/util/audit_nickname.py 的 enrich_audit_nickname_objs。
欄位 型別 說明
id serial PK
batch_id int NOT NULL FK→evidence_batches.id 同套件內可建 FK
file_id int NOT NULL →upload_files.id(🔴 軟參照:另一支套件的表)
內部用 int、對外 API 一律 file_uid
file_uid varchar(50) NOT NULL 冗餘存一份,避免每次 join
original_name varchar(255) NOT NULL 上傳時的檔名
size bigint
content_hash varchar(64) NULL SHA-256;從 upload_files.sha256 抄
(D9 內容重複提示用)
status varchar(16) NOT NULL pending/classified/archived/no_task/removed
classification JSONB NULL 歸檔當下的判定快照
[{"part_id":..., "confidence":..., "source":"ai|manual"}]
removed 列靠這欄保留 AI 判定結果(D7)
archived_evidence_uids JSONB NULL 歸檔後寫入的 job_evidences.uid 清單(追溯)
removed_at / removed_by_user_id timestamptz / int D7 清理紀錄
tenant_id / org_unit_id int NOT NULL RLS
慣例四欄
UNIQUE (batch_id, file_id)
RLS 四條 policy
動作 允許的來源狀態 目標狀態 額外前置條件
加檔/刪檔 uploading、ready uploading 非 classifying
(刪檔另允許 review,只能刪 (否則 409 EC_BATCH_SEALED)
pending/classified 列)
完成上傳 uploading ready file_count >= 1
------- 以下在 T-3.2/T-4.2 補,本卡先把 dict 的骨架與擋法做好 -------
開始分類 ready、uploading(自動先 ready)、 classifying 儲存設定已設;目標集非空;
failed、review 同專案無其他 classifying 批次
容器回結果 classifying review
失敗 classifying failed
歸檔 review archived 至少一檔有判定
清理 archived archived 只動 pending/classified/no_task 的檔
實作方式:套件 EvidenceBatchService 內以一張 ALLOWED_TRANSITIONS dict 實作,非法轉換一律拋 ConflictError。不要散在各個 method 裡用 if 判斷(那樣新增狀態時會漏改)。
evidence_classification_runs 的 model/repo(infra/model/classification_run_model.py),照它的慣例寫(BaseModel 慣例欄位、repo 繼承 BaseRepositoryImpl)。不要自己另創一套風格。BaseRepositoryImpl(jedi_common.session.database.repository.base_repository_impl,內建 @property session)。絕對不要在 __init__ 寫 self.session = get_session()——DI 把 repo 註冊成 Singleton 時實例化早於 @transaction 開 scope,會直接 500。@transaction(from jedi_common.session.database.db import transaction)。被其他 @transaction method 呼叫的 helper 不再加,但 docstring 要標「caller 必須在 @transaction scope 內」。