建議 model:Opus,effort:medium——12 張表兩種方案要實查 FK 鏈與讀取端才能選,選錯是問卷作答整條靜默壞;且動 jedi-survey model。本卡屬 FR-094(母卡見兄弟卡段),.3 問卷裸表的唯一一棒(整案第 8 棒)。無硬前置,可平行。方案要先回寫首腦定案再動手。
問卷模組有 14 張表,只有問卷主表(surveys)與資料夾(survey_folders)開了隔離,其餘 12 張——題目、頁面、任務對應問卷、作答、作答歷史、討論、翻譯——全部沒開、也沒有「屬於哪個租戶」的欄位。也就是說別家客戶問卷的題目與作答內容,只要直查這 12 張表就看得到。D2 已裁:問卷不加公版旗標(引用是複製進任務,DEV 跨租戶引用 0),要修的就是這 12 張裸表。
首腦核對(2026-09-13 DEV localhost 實查 pg_class.relrowsecurity/pg_policy):
survey.surveys、survey.survey_folders(policy 含 is_super_admin OR,02-schema.sql:24819-24880)。survey_questions(1,848 筆)、survey_pages、task_surveys(23 筆)、question_answers(670 筆)、question_answer_histories、question_answer_history_details、survey_discussions、task_survey_ref_items、surveys_trans、survey_pages_trans、survey_questions_trans、survey_folders_trans。jedi_survey/infra/models/*.py):survey_pages.survey_id → surveys;survey_questions.page_id → survey_pages(+自關聯 parent);question_answers 有 task_survey_id/survey_id/page_id/question_id;question_answer_histories.task_survey_id → task_surveys;history_details → histories+question_id;survey_discussions.survey_id → surveys;task_survey_ref_items.task_survey_id;四張 _trans 各 FK 到主表。task_surveys 有 main_project_id/task_id/survey_id/department_id——沒有 tenant_id,但 main_project_id → compliance.projects.tenant_id 可反查。Survey model 混 TenantScopedMixinModel(survey.py:16),其餘 model 無 tenant 欄位。~/Projects/Jedicogy/module/jedi-python-package/jedi-survey/jedi_survey/infra/models/ 14 個 model(survey.py 有 TenantScopedMixinModel,其餘沒有)
~/Projects/Jedicogy/module/jedi-python-package/jedi-survey/jedi_survey/infra/repository/ 讀取端(選方案 A 時要確認 insert 路徑會填 tenant_id)
~/Projects/Billows/Audit-Manager/compliance-manager-be/scripts/sql/2026-09-XX-fr094-cm<本卡號>-survey-tables-rls.sql (新)本棒 migration(或落 jedi-survey migrations/ 由 FR-093 攤平——先查該套件有無 migrations 目錄,2026-09-13 查無)
~/Projects/Billows/Audit-Manager/compliance-manager-be/scripts/sql/manifest.tsv 加一列
~/Projects/Billows/Audit-Manager/compliance-manager-be/scripts/init/02-schema.sql:24819-24880 surveys/survey_folders 現有 policy(樣板)
ADD COLUMN tenant_id integer,由 FK 鏈回填(pages←surveys、questions←pages、task_surveys←projects.main_project_id、answers←task_surveys、histories←task_surveys、details←histories、discussions←surveys、ref_items←task_surveys、四張 trans←各主表),model 混 TenantScopedMixinModel,insert 路徑要填;優點與其他表同形、policy 便宜;缺點動 12 個 model+12 條 ADD COLUMN、insert 漏填會 500。(B) JOIN-based policy——不加欄位,policy 用 EXISTS (select 1 from survey.surveys s where s.id = <FK 路徑> and (is_super_admin OR app_tenant_allowed_for_session(s.tenant_id))),深層表(history_details)要 join 三層;優點不動 model、帶舊資料升級零風險;缺點每次查都 join、policy 不同形難維護。評估要點貼回卡:各表筆數(效能)、insert 路徑數(A 的漏填風險)、surveys 本身有 RLS 時 B 的 EXISTS 子查詢是否會被 surveys 的 policy 再擋一次(會,但語意正確——看得到 survey 才看得到題目)。surveys_*(02-schema.sql:24859-24880);12 張 ENABLE ROW LEVEL SECURITY。方案 A 的回填 UPDATE 一律 WHERE tenant_id IS NULL 冪等。is_super_admin='t' 全量。貼數字。tenant_id 等於 survey 的(這是「insert 路徑會漏填」的防線,要寫)。方案 B:以③對照替代。question_answers policy 的 EXISTS 拿掉改 true → 子租戶查應從 0 變全量。