一句話:本卡屬 FR-099(母卡 CM-母卡號),第 2 棒:把主專案的意見回饋與標籤兩個模組整組搬進 jedi-issue,做完主專案這兩塊目錄全刪。依賴第 1 棒(README 更正)先完成。
意見回饋這個功能(使用者填表回報問題、管理員看清單改狀態匯出 Excel)和它旁邊的標籤功能(提供表單上的下拉選單),程式現在放在主專案裡,但實際的資料和大半邏輯早就住在 jedi-issue 套件裡——主專案那份只是一層殼。本棒把這層殼也收進套件。
做完之後主專案少掉五個目錄,使用者完全感覺不到差別:網址不變、畫面不變、資料不變。好處是「意見回饋」變成一個完整零件,日後新產品要用直接裝套件就有。
主專案 → 套件,五個目錄:
api/feedback/ 4 條 route + serializer
├ FeedbacksRoute POST /feedbacks 清單(分頁)
├ FeedbackRoute GET/POST/PUT/DELETE /feedback[/<uid>]
├ FeedbackFileRoute DELETE /feedback/file/<uid>/<file_uid>
└ FeedbackExportRoute POST /feedback/export/<export_type> ← 帶 require_capability("feedback.export")
api/label/ 1 條 route(全檔 23 行,純轉發殼)
└ LabelsMenuRoute GET /labels/menu/<scope>
app/feedback/ feedback_service.py(16 個方法)+ issue_service.py(包套件的那層)+ dto/
domain/feedback/ entity + repository 介面 + feedback_issue_domain_service
infra/feedback/ model(feedback_issues 表)+ mapper + repo impl
di_containers/feedback/ 整個 FeedbackContainer
di_containers/issue/ MemberContainer(其中 label 那半與 feedback 重複,見下)
config/app_modules.py REGISTERED_APPS 移除 "feedback" 與 "label" 兩列
① 🔴 project_name 寫死。app/feedback/service/issue_service.py:41 有 self.project_name = "CM"(Guidant AI 的專案代號),並被當成 jedi_issue 呼叫的 project 參數傳進去(24 處都帶)。這是產品知識,不能進通用套件——必須變成 IssuePluginConfig 的一個欄位(例如 issue_project_name,預設值留空或給個中性預設),由主專案 core/plugins/issue.py 傳。漏了這步就是把 Guidant AI 的代號焊進通用套件。
② 🔴 label 的 DI 重複建。label_repo 與 label_domain_service 在 di_containers/feedback/feedback_containers.py 與 di_containers/issue/issue_containers.py 兩邊各建了一份。這違反 FR-090 第 6 棒的規則(主專案不重複建套件 service,要從套件取用口 host_services() 拿)。搬進套件時收成一份,不要兩份一起搬過去。
③ 🔴 feedback_issues 這張表的 owner 是 cmmgr,而套件的 issues/labels/members 等六張是 ammgr。搬進套件後這張表的歸屬要跟著改(它變成套件的表)。注意 jedi-issue 目前沒有 migrations/ 目錄——要新建,並照套件隨包 migration 的慣例寫(FR-093 正在做出貨升級鏈,寫法先查該案現況再動,不要自創)。
④ 能力點歸屬。主專案 capabilities 表現有 feedback.create/read/update/delete/export + feedback-view.read 六個;套件 plugin/contract.py 目前只宣告了 issue-integrate-config.* 四個。feedback 那六個要進套件的 CAPABILITIES 宣告(那份清單是唯一來源,主專案 seed 以它為準)。⚠️ 是否全部六個都該進、feedback-view.read 這個命名不同的要怎麼歸,先查 route 實際守哪幾個再定——目前只有 FeedbackExportRoute 掛了 require_capability("feedback.export"),其餘三條只有 @jwt_required()。搬遷紀律是逐字保留現有守門,不加也不減。
⑤ 套件 IssuePluginConfig 目前沒有能力點欄位(contract.py 的 docstring 明寫「本模組沒有能力點守門」)。要加欄位讓 route 能套 capability guard,照「只增欄位、不改簽名」的原則。
⑥ 附件走租戶儲存。di_containers/feedback 注入的 file_upload_service 是 package_service("file_upload", "file_upload_service"),CM-826 讓回饋附件依租戶 STORAGE_CONFIG 存到 remote_agent/minio/local。搬進套件後這條線要保住——套件得開一個 port 收這支,不能寫死成 local。
⑦ 共用設定讀 ROOT。issue_service.py:45-48 用 read_root_config_value("ISSUE_INTEGRATE_CONFIG", ...) 繞 RLS 讀 ROOT(1) 租戶的平台層設定(註解寫明「僅 admin 設定、全租戶共用,per-tenant 列已移除」)。這個「讀 ROOT」是主專案的多租戶規則,不能直接進通用套件——要走 port 或 config 由主專案答。
⑧ FeedbackIssue model 的 hybrid_property。infra/feedback/model/feedback_issue.py 有 title/created_user_name/updated_user_name 三個 hybrid_property,其中後兩個是自己 join User 表補暱稱——這違反 D8(補暱稱要走 identity port,主專案不寫 wrapper)。搬進套件時改走套件的 identity port(jedi-issue 目前沒有這個 port,要加;參考 jedi-iam 的 IdentityAdapters 形狀)。
⑨ 兩個 hybrid_property 各有 @xxx.expression 版本(讓 SQLAlchemy query 能用它當篩選條件)。改走 identity port 後 expression 那半怎麼辦要想清楚——port 是 Python 層的,進不了 SQL。先查清楚有沒有人真的拿 created_user_name 當查詢條件(grep get_feedbacks_and_pager 的 filters 處理),沒有就可以只保 Python 層。
步驟 0:確認第 1 棒已完成(README 已更正)。沒完成就停下回報——讀到舊 README 會先誤判一次。
步驟 1:先查再寫。動手前 grep 確認套件內有沒有同功能的既有東西(禁止重複造輪子,CLAUDE.md 鐵則)。特別查:套件有沒有現成的 identity port、有沒有現成的能力點守門機制、附件上傳的 port 長什麼樣。
步驟 2:照 core/plugins/issue.py 現有的三段形狀擴充(① 回答套件的提問、② 填表、③ 掛載)。那個檔就是終局形狀的樣板,不要另創新形狀。