問卷填寫 / 預覽(Survey Fill / Preview)
事實基準:2026-07-05 從 FE(
views/survey-v2/SurveyPreview.vue、composables/useSurveySocket.js)/ BE(api/task_survey、app/task_survey)code 掃出;表結構取自db_schema.json(surveyschema);error code 對common/code/error_code.py
1. 功能描述
本頁是問卷消費端:同一個
SurveyPreview.vue 依 route.query.mode
服務五種模式,涵蓋兩條 route——
- 預覽
/survey/preview/:id(作者端):唯讀渲染問卷結構,不落資料、不查權限、不連 socket(由設計器/管理頁的預覽鈕開啟)。 - 填寫
/survey/fill?taskSurveyUid=…&mode=fill(消費端):稽核任務執行者對綁定的問卷快照作答;載入task_survey+ 既有答案 + 權限,連 socket 即時協作,答案持久化。另有review(審核人給分 / 回饋)、view/history(唯讀檢視 / 看歷史版本)子模式。
填寫的是綁定任務時的問卷快照(is_snapshot=1),非
live 範本(見 _overview §4)。
主要使用者:預覽 = 問卷作者;填寫 =
任務執行者(受稽方);審核 =
任務指派的審核人(is_approver)。
角色速覽(消費端角色,見 _overview §5):
| 角色 | 一句話 |
|---|---|
| 填寫者 | 任務執行者(受稽方)——canEdit;作答、送審、補充 |
| 審核人 | 任務指派時 is_approver
的人——canVerify;給分 / 回饋 / 結案 |
| 預覽者 | 問卷作者——preview 模式唯讀,無持久化 |
1.1 功能總覽(本頁全部功能)
| # | 功能 | 說明 | 位置 | 詳述 |
|---|---|---|---|---|
| 1 | 模式分流 | preview / fill / review / view / history 五模式共用一個 view | 進頁 | §10 |
| 2 | 題目渲染(依型別) | text / textarea / radio / checkbox / dropdown / fileupload / link | 中欄 | §10 |
| 3 | 選項補充輸入 | 選項勾選後展開補充欄(ext_answer) | 中欄 | §10 |
| 4 | 檔案上傳題 | 上傳(≤50MB)/ 下載 / 預覽(PDF·文字 iframe) | 中欄 | §6.1 |
| 5 | 進度導覽 | 完成度百分比 + 頁面樹(已答 / 未答) | 左欄 | §5 |
| 6 | 頁面切換 | 上一頁 / 下一頁 / 頁碼點 | 中欄底 | §5 |
| 7 | 儲存草稿 | checkpoint status=1 暫存 | 頂列 | UC-SVF-01 |
| 8 | 即時暫存(單題) | patch 單題(無歷史、不改狀態;socket 同路徑) | 作答時 | UC-SVF-04 |
| 9 | 送出 / 送補充 | 填寫者送審(→2)/ 補充後重送(3→2) | 頂列 | UC-SVF-01 |
| 10 | 審核給分 / 回饋 | 審核人評分(★)+ 回饋,退回補充(2→3) | 中欄審核區 / 頂列 | UC-SVF-01 |
| 11 | 結案 | 免審核直接結案 / 審核通過結案(→9) | 頂列 | UC-SVF-01 |
| 12 | 即時協作 | 多人同填:presence、編輯鎖、答案廣播 | socket | UC-SVF-02 |
| 13 | 歷史版本 / 還原 | 看版本、載入檢視、還原到某版 | 歷史側欄 | UC-SVF-03 |
| 14 | 討論串 | 問卷討論訊息 | 討論側欄 | §6.1 |
| 15 | 匯入答案 / 範本 | Excel 批次匯入答案、下載範本 | (任務層) | §6.1 |
UC(§2)已展開本頁所有具實質後端寫入邏輯的功能:7-11(狀態機 checkpoint,UC-SVF-01)、8(單題 patch,UC-SVF-04)、12(協作,UC-SVF-02)、13(歷史還原,UC-SVF-03)。純顯示 / 導覽類(1 模式分流、2 題目渲染、3 選項補充、5 進度導覽、6 頁面切換)見 §5 / §10;檔案上傳(4)、討論串(14)、答案 Excel 匯入 / 範本(15)屬常規 CRUD / 任務層,細節見 §6.1 / GAI-SD-02;preview 模式無作答 / 持久化。
2. Use Case
角色速覽(消費端角色,見 _overview §5):
| 角色 | 一句話 |
|---|---|
| 填寫者 | 任務執行者(受稽方)——canEdit;作答、送審、補充 |
| 審核人 | 任務指派時 is_approver
的人——canVerify;給分 / 回饋 / 結案 |
| 預覽者 | 問卷作者——preview 模式唯讀,無持久化 |
canEdit / canVerify 由
POST /task-assignees
載入指派名單後判定。流程圖視覺慣例:菱形 = 判斷、橘底 = 例外、紅框 =
錯誤(標 error code)、綠框 = 成功終點、虛線 = 可選路徑。
UC-SVF-01 填答 → 送審 → 審核 → 結案
| 項目 | 內容 |
|---|---|
| 角色 | 填寫者(作答 / 送審 / 補充)、審核人(回饋 / 結案) |
| 前置條件 | fill 模式、task_survey 存在;動作鈕依 status +
canEdit / canVerify +
approval_required 顯示 |
| 產出 / 後置條件 | 暫存走 patch(無歷史);狀態變更走 checkpoint(交易內補齊空列 + 寫歷史 + 通知) |
UC-SVF-02 多人即時協作
| 項目 | 內容 |
|---|---|
| 角色 | 填寫者 / 審核人(`canEdit |
| 前置條件 | fill 模式;room = taskSurveyUid |
| 產出 / 後置條件 | join 時 dirty-aware 合併快照(本地未存答案不被覆蓋);update 廣播權威覆蓋;還原觸發全房 reload |
UC-SVF-03 歷史版本檢視 / 還原
| 項目 | 內容 |
|---|---|
| 角色 | 填寫者 / 審核人(非最新且 status≠9 才可還原) |
| 前置條件 | 有歷史版本(每次 checkpoint 寫一版) |
| 產出 / 後置條件 | 還原套回當前答案 + 寫新歷史(type=R)+ 廣播 reload |
UC-SVF-04 單題即時暫存(patch,不改狀態 / 不寫歷史)
| 項目 | 內容 |
|---|---|
| 角色 | 填寫者 /
審核人(canEdit || canVerify;作答變更即觸發) |
| 前置條件 | fill 模式、task_survey 存在;題目屬本問卷 |
| 產出 / 後置條件 | 單題 upsert 到 question_answers;不寫歷史、不改
task_survey.status;socket update
廣播走同一路徑 |
3. 權限矩陣
| 操作 | FE 判定 | BE 強制 | BE 檢查位置 |
|---|---|---|---|
| 預覽(preview) | route meta.requiresAuth |
@jwt_required();只讀問卷結構 |
api/survey/routes/survey_route.py::SurveyRoute.get |
| 載入填答 | fill 模式帶 taskSurveyUid |
@jwt_required();task_survey 不存在擋 404 |
api/task_survey/routes/task_survey_route.py |
| 作答 / 儲存 / 送審 | canEdit(填寫者,依 task-assignees) |
@jwt_required();狀態由送入的 status
決定(無 BE 狀態轉換守門,見 §12) |
question_answer_service.py::checkpoint_task_survey_answer |
| 給分 / 回饋 / 結案 | canVerify(審核人 is_approver)+
status===2 |
@jwt_required() |
同上 |
| 歷史還原 | 非最新版且 status≠9 |
@jwt_required() |
question_answer_history_service.py::revert_question_answer_from_history |
⚠️ 可否編輯 / 審核的判定在 FE(
canEdit/canVerify由前端算),BE 端點僅@jwt_required()、不重新驗證角色與狀態合法性——繞過 FE 可送任意status。收進 §12。
4. 狀態機與前置條件
消費端填答狀態機(task_surveys.status,TaskSurveyStatusCode):
| 狀態值 | 名稱 | 誰可動作 | 出處 |
|---|---|---|---|
| 0 | 待填寫 UNFILLED | 填寫者開始作答 | app/task_survey/common/task_survey_code.py |
| 1 | 編輯中 EDITING | 填寫者作答 / 儲存草稿 / 送審 | 同上 |
| 2 | 待審核 UNDER_REVIEW | 審核人給分 / 回饋 / 結案 | 同上 |
| 3 | 待補充說明 ADD_NOTES | 填寫者補充後重送審 | 同上 |
| 9 | 完成 COMPLETED | 終態,動作鈕停用 | 同上 |
轉換:0/1→2(送審,需
approval_required)、1→9(免審核結案)、2→3(退回補充)、3→2(重送審)、2→9(審核通過結案)。轉換合法性只在
FE 動作鈕條件把關,BE checkpoint 直接寫入送入的
status(§12)。
| gate | 效果 | 出處 |
|---|---|---|
| checkpoint 交易 | 套 patch → 補齊所有題的空列(score=0/answer=null,供乾淨還原)→ 改 status → 每題寫歷史 + 明細 | question_answer_service.py::checkpoint_task_survey_answer |
| status 變更 | 呼叫 notify_task_survey_assignee_status_change
通知對應角色 |
同上 |
| 審核區顯示 | status>1 且 mode≠preview 且
approval_required 才顯示評分 / 回饋;可編輯需
canVerify && status===2 |
SurveyPreview.vue showReviewFields |
5. UI 設計
📸 實際畫面截圖待補(一律亮色模式拍攝;截圖另有專責 session)。預計補到
assets/img/: ① fill 整頁(左進度 + 中題目)fill-overview.png② 各題型渲染(單選 + 補充 / 檔案上傳 / 連結)fill-question-types.png③ 審核區(評分 + 回饋)fill-review-section.png④ 歷史側欄 / 討論側欄fill-sidebars.png⑤ preview 模式唯讀preview-readonly.png
版面骨架與區塊職責(頂列狀態+動作、左進度導覽、中題目作答、歷史/討論側欄;彩色區塊可點跳對應小節錨點,API 僅標代表性):
模式分流:
狀態呈現:頂列動作鈕依 status + 角色動態顯示;題目依
type 分支渲染(唯讀 vs 可編輯由 canEdit /
disabled 控);連結題唯讀時 sanitizeHref 擋
javascript: / data:;線上使用者頭像來自 socket
presence。有 socket(fill 模式即時協作)。
6. API 規格
Envelope:成功 {"status": true, "data": …}、失敗
{"error_code": "…", "msg": "…"}。消費端走
api.task_survey
模組(/task-survey*、/project-survey/*)。
6.1 總清單(本頁呼叫的全部 endpoint)
| 分類 | Method + Path | 說明 | 完整規格 |
|---|---|---|---|
| 結構 | GET /survey/{uid} |
問卷(快照)結構供渲染 | →designer §6.2 |
| 任務問卷 | GET /task-survey/{uid} |
task_survey 中繼(survey_uid / status / approval_required / task_uid) | §6.2 |
| 任務問卷 | PUT /task-survey/{uid}・PUT
/task-survey/configured/{uid} |
更新 task_survey / 審核設定 | GAI-SD-02 |
| 答案 | GET /project-survey/answers/{uid} |
載入全部答案 {question_uid: 答案} |
§6.2 |
| 答案 | POST /project-survey/answers/{uid}/patch |
單題暫存(無歷史 / 不改狀態) | §6.2 |
| 答案 | POST /project-survey/answers/{uid}/checkpoint |
批次 + 狀態(寫歷史) | §6.2 |
| 歷史 | GET /project-survey/histories/menu/{uid}・POST
/project-survey/histories |
版本清單 | GAI-SD-02 |
| 歷史 | POST /project-survey/history-details |
某版明細 | §6.2 |
| 歷史 | POST /project-survey/history/revert |
還原到某版 | §6.2 |
| 討論 | POST /survey-discussions(載入)・POST
/survey-discussion(送出) |
問卷討論串 | GAI-SD-02 |
| 指派 | POST /task-assignees |
載入指派名單 → 判 canEdit / canVerify | GAI-SD-02 |
| 匯入 | GET /task-survey/import-template/{task_uid}・POST
/task-survey/import-answers |
答案 Excel 範本 / 匯入 | GAI-SD-02 |
| 檔案 | POST /upload-file・GET
/download-file/{uid}・GET pdf-preview |
檔案上傳題 | GAI-SD-02 |
6.2 核心 endpoint(欄位對照 serializer / service,出處標段末)
[GET] /project-survey/answers/{uid} — 載入答案
回一個 map:key = question_uid,value =
該題答案物件(取最新,DISTINCT ON 去重)。value 物件欄位:
| 欄位 | 型別 | 說明 |
|---|---|---|
| answer | jsonb|null | 答案 |
| ext_answer | jsonb|null | 補充答案 |
| feedback | jsonb|null | 回饋 |
| ext_feedback | jsonb|null | 補充回饋 |
| score | int | 得分(null 正規化為 0) |
task_survey 不存在擋
TASK_SURVEY_404001。出處:app/task_survey/service/question_answer_service.py::get_answer_list_by_task_survey_uid:47。
[POST] /project-survey/answers/{uid}/patch — 單題暫存
Request:
{
"patches": [
{
"question_uid": "string",
"answer": {
"answer": "string", // 選填
"ext_answer": "string", // 選填
"feedback": "string", // 選填
"ext_feedback": "string", // 選填
"score": 0 // 選填
}
}
]
}單題 upsert,不寫歷史、不改狀態(socket
update 也走此邏輯)。題目不存在擋
TASK_SURVEY_404006。出處:question_answer_service.py::patch_task_survey_answer:313、serializer
api/task_survey/serializers/question_answer_patch.py。
[POST] /project-survey/answers/{uid}/checkpoint — 批次 + 狀態
Request:
{
"status": "string",
"patches": [] // 同上(見上一個 endpoint 的 patches 結構)
}交易內:① 套用 patch ② 為每題補空列(score=0 /
answer=null,確保「所有題都有列」利於乾淨還原)③ 更新
task_survey.status ④ 每題寫
question_answer_histories + _details ⑤ status
變更時通知指派人。分數 null → 0
正規化。出處:question_answer_service.py::checkpoint_task_survey_answer:167、serializer
api/task_survey/serializers/question_answer_checkpoint.py。
[POST] /project-survey/history/revert — 還原
Request:
{
"task_survey_uid": "string",
"history_uid": "string"
}把該版明細套回當前答案(現存題才套),寫一筆新歷史
type=R,socket 廣播 reload。歷史主檔 /
明細查無擋 TASK_SURVEY_404003 /
404004。出處:app/task_survey/service/question_answer_history_service.py::revert_question_answer_from_history:44。
7. 前端檔案地圖(compliance-manager-fe/)
| 檔案 | 角色 |
|---|---|
src/views/survey-v2/SurveyPreview.vue |
主檢視(五模式:預覽 / 填寫 / 審核 / 檢視 / 歷史) |
src/views/survey-v2/composables/useSurveySocket.js |
即時協作 socket(join / lock / update / reload;dirty-aware 合併) |
src/views/survey-v2/composables/useSurveyApi.js |
問卷結構 / 題型常數 |
src/config/router/index.js:725-739 |
route
survey-v2-preview(/survey/preview/:id)+
survey-v2-fill(/survey/fill) |
src/config/api/api.js |
endpoint 常數(TASK_SURVEY /
PROJECT_SURVEY_ANSWERS* / ..._CHECKPOINT /
..._HISTORY_* / SOCKET_FILL_SURVEY …) |
src/config/locales/i18n/*/survey-v2.json |
頁面 i18n |
8. 後端檔案地圖(本頁核心鏈路)
| 鏈路 | Route | App Service | 底層 |
|---|---|---|---|
| task_survey | api/task_survey/routes/task_survey_route.py |
app/task_survey/service/task_survey_service.py(含
link_task_survey 綁定、通知) |
domain/task_survey +
infra/task_survey |
| 答案 load / patch / checkpoint | api/task_survey/routes/question_answer_route.py |
app/task_survey/service/question_answer_service.py |
question_answers repo |
| 歷史 / 還原 | question_answer_history_route.py・..._detail_route.py |
question_answer_history_service.py・..._detail_service.py |
question_answer_histories(_details) repo |
| socket 協作 | app/task_survey/handler/fill_survey_socketio_handler.py |
(同 question_answer_service patch) | Redis presence + question_answers |
DDD 提醒:消費端在主專案自有 DDD 分層(非
jedi-survey);answer / history 走 domain service → repo。checkpoint 以
@transaction 包住多表寫入(答案 + 歷史 + 明細 +
status)。
9. DB
9.1 資料表總清單(本頁讀寫的全部表)
| 表 | 讀/寫 | 說明 | 欄位詳述 |
|---|---|---|---|
| survey.surveys(快照列) | 讀 | 問卷結構(is_snapshot=1,題目來源) |
manage §9.3 |
| survey.task_surveys | 讀 / 寫(status) | 任務×問卷×設備執行期追蹤 | §9.3 |
| survey.question_answers | 讀 / 寫 | 每題答案(answer / feedback / score jsonb) | §9.3 |
| survey.question_answer_histories | 寫 | 答案版本主檔(type U / R) | §9.3 |
| survey.question_answer_history_details | 寫 | 每版每題答案明細 | GAI-SD-03 |
| survey.survey_discussions | 讀 / 寫 | 問卷討論串 | GAI-SD-03 |
9.2 ER 圖(本頁讀寫範圍)
9.3 核心表欄位(取自 DEV DB dump,含 DB comment)
survey.task_surveys — 任務對應問卷(17 欄)
| 欄位 | 型別 | 說明 |
|---|---|---|
| uid | varchar(36) | 唯一識別碼 |
| main_project_id | int | 主專案 ID |
| task_id / task_uid | int / — | 稽核任務(GRC job) |
| process_id / definition_id | int|null | 流程 / 定義 ID |
| survey_id | int | 問卷 ID(指向快照列) |
| status | int | 填答狀態(0/1/2/3/9) |
| approval_required | bool|null | 是否需審核(決定是否送審路徑) |
| device_id / department_id | int|null | 設備 / 部門(survey×device 展開) |
| job_evidence_id | int|null | GRC job evidence soft-ref |
| is_delete | bool|null | 軟刪除 |
survey.question_answers — 答案(15 欄)
| 欄位 | 型別 | 說明 |
|---|---|---|
| uid | varchar(36) | 唯一識別碼 |
| task_survey_id | int | 任務問卷 ID |
| question_id | int | 題目 ID(→ survey_questions.id) |
| survey_id / page_id | int|null | 問卷 / 頁面 ID(冗餘) |
| score | int | 得分(null 正規化為 0) |
| answer / ext_answer | jsonb|null | 答案 / 補充答案 |
| feedback / ext_feedback | jsonb|null | 回饋 / 補充回饋 |
survey.question_answer_histories — 答案版本主檔
uid / task_survey_id /
type(U=更新 / R=還原)/
審計欄位。明細在
question_answer_history_details(history_id +
每題 question_id / score / answer
/ feedback …)。
10. 頁面邏輯與資料對應
模式分流:mode = route.query.mode || 'preview'。preview
只 getSurveyById(id) 唯讀;fill / review / view / history
載入 task_survey + answers +
task-assignees(判 canEdit/canVerify)+ socket。
題型渲染 ↔︎
答案(answers[questionId] = {answer, ext_answer, feedback, ext_feedback, score}):
| 題型 | 輸入元件 | 答案欄 |
|---|---|---|
| text / textarea | InputText / Textarea | answer(字串) |
| radio / dropdown | RadioButton / Dropdown(+ 選項補充) | answer(選項)+ ext_answer(補充) |
| checkbox | 多選 | answer(陣列) |
| fileupload | 上傳 / 下載 / 預覽 | answer(檔案參照) |
| link | 唯讀 <a> / InputText |
answer(URL) |
| 審核區 | Rating(★)+ Textarea | score + feedback |
持久化:作答變更 → markDirty + debounce
→ 單題 patch(socket update 同路徑);頂列動作
→ checkpoint(帶目標 status,寫歷史)。
錯誤對應:TASK_SURVEY_404001(任務問卷不存在)、TASK_SURVEY_404006(題目不存在)、TASK_SURVEY_404003/404004(歷史主檔
/ 明細不存在)、TASK_SURVEY_400002(legacy PUT 缺
status/answers)→ toast。
11. 背景行為與外部依賴
| 類型 | 內容 |
|---|---|
| 通知 | checkpoint 改 status 時
notify_task_survey_assignee_status_change 通知填寫者 /
審核人 |
| Socket | 有:/socket/fill-survey(room =
taskSurveyUid)——presence、編輯鎖、答案廣播、還原 reload;presence 存
Redis fill-survey:{room}:users |
| Job | 無背景 job;匯入答案為同步解析 |
| jedi-* 套件 | jedi-auth(JWT);消費端 answer / history domain
在主專案(非 jedi-survey) |
| 系統參數 | 無 feature flag;socket 僅 `canEdit |
12. 邊界情況與已知坑
- 狀態轉換合法性只在 FE:checkpoint 直接寫入送入的
status,BE 不驗證0→9、2→1等是否合法——繞過 FE 動作鈕可送任意狀態。改狀態機時需補 BE 守門。 - canEdit / canVerify 在 FE 算:可否作答 /
審核由前端依 task-assignees 判定,BE 端點僅
@jwt_required()不重驗——與作者端一致的守門缺口。 - checkpoint 補齊空列:每次 checkpoint 為所有題補空列(score=0 / answer=null),確保還原乾淨;統計 / 匯出時「有列 ≠ 有作答」,需以 answer 是否為 null 判斷實際填答。
- 分數 null→0 不對稱:FE 未評分送
null,BE 正規化為0;「0 分」與「未評分」在 DB 無法區分,報表勿把 0 當已評分。 - socket dirty-aware 只在 joined 快照:join 時的 DB
快照合併會保護本地未存答案;但
updated廣播是權威、直接覆蓋——兩人同時改同題,後到的廣播覆蓋前者,靠編輯鎖(lock/unlock)降低衝突而非強一致。 - 填的是快照非 live
範本:
task_survey.survey_id指向綁定當下的快照(is_snapshot=1);作者事後改範本 / 改版不影響進行中的填答(見 _overview §4)。 - preview 模式無持久化:預覽路徑不載 task_survey、不落資料——別把 preview 當「試填」,作答不會存。
- 還原寫新歷史(type=R):還原不是刪除後續版本,而是把舊版套回並記一筆
R歷史;版本鏈只增不減。
13. 開發與驗證
- 跑起來:BE
python main_socketio.py(port 8000,socket 需經 eventlet,log 在log/app.log);FE 在compliance-manager-fe/起 Vite dev server。BE 改 service code 後必須重啟(無 hot reload) - 測試帳號:dev 環境登入帳號密碼見
.env/ 部署文件 - 導航路徑:預覽 = 設計器 /
管理頁「預覽」鈕(
/survey/preview/:id);填寫 = 專案任務抽屜「填寫問卷」新分頁開/survey/fill?taskSurveyUid=…&mode=fill - 前置資料:填寫需先有綁定問卷的稽核任務(規劃頁
job_type=survey綁問卷 → 產生 task_survey 快照,見 project-task-edit.md §6.2);多人協作需兩個帳號同開同 taskSurveyUid - E2E:
compliance-manager-test/repo(Cucumber + Playwright);以問卷填寫/survey fill搜尋 feature 檔 - 相關文件:範本設計見 問卷設計器、任務綁定與快照見 project-task-edit.md §6.2 / §12、資料模型見 功能群總覽;DB / API 全量見 GAI-SD-02/03