本卡屬 FR-137(母卡 CM-2714),實作卡 ②/5:每小時排程把到期任務複製成下一期並通知執行人。前置:卡 ①(任務週期欄位)。設計:
docs/features/FR-137-2610-recurring-tasks-and-conditional-notify/design.md§6.2、§6.3、§6.7 任務抽屜;雛形頁 2docs/features/FR-137-2610-recurring-tasks-and-conditional-notify/prototype/task-drawer.html。
讓週期真的轉起來:系統每小時檢查一次,任務的下次到期時間到了,就複製出一筆新任務當下一期(同一輪、同樣的人和設定、問卷是空白的),寄信通知執行人。上一期有沒有做完都照生(每期各自獨立)。這一輪稽核結案、或專案已經結束,就不再生(D19)。任務抽屜和「我的任務」清單要看得出是第幾期。自動開掃是卡 ③,這張卡生出來的新任務一律只通知人。
core/scheduler.py 的 init_scheduler 加一支 task_recurrence_tick:cron 每小時第 5 分(UTC),max_instances=1, coalesce=True,寫法照既有 @_job_scoped("task_recurrence_tick")+app.app_context()+system_context(參考同檔既有 12 支)。邏輯本體放新檔 app/flow_control/service/task_recurrence_app_service.py,排程只呼叫一行。system_context 下查 next_due_at <= now() 的任務(帶 tenant_id),依租戶分組。先濾停止條件(D19):任務所在輪次 project_audit_rounds.status = 'closed',或專案 projects.status 為 completed/archived → 不生、清空 next_due_at、記一行 info log。其餘每租戶進 tenant_context(<名稱>, tenant_id) 處理。period_no = 源.period_no + 1、source_job_id = 源.id;撞到 UNIQUE (source_job_id, period_no) 就跳過(多副本另一份已建)。每筆各包 SAVEPOINT,單筆失敗寫 logger.exception、保留源任務 next_due_at(下一小時再試)。一定要先 INSERT 搶,不能先查再寫。recur_unit、recur_interval、auto_execute、alert_recipients 搬到新任務;新任務 next_due_at 從源任務的 next_due_at(不是 now())依單位×間隔往後推一格,推完若仍 ≤ now() 繼續推到未來為止(只生一期);月以上用 dateutil.relativedelta。源任務 next_due_at 清空。notify_user_todo_job(job_id)(app/flow_engine/service/workflow_execution_service.py:1240-1300)寄執行人。排程沒有 request,信件字串以 flask_babel.force_locale("zh_Hant_TW") 取;操作者寫 system。auto_execute 為 true 的分支留一個 hook 點給卡 ③,本卡先一律走通知。新檔 app/flow_control/service/task_period_clone_service.py(@transaction 由排程呼叫端開,檔內 helper 標「caller 必須在交易內」)。
create_job(ao_uid, project_uid, name, created_user="system", description, guide, job_type, ...)(infra/flow_control/repository/flow_control_job_repo_impl.py:639),不帶 job_element_uid:在同一個 workflow 掛一條新的平行分支節點,落在同一輪(D2),初始狀態由拓樸判定(平行分支為 PROCESSING)。不走 JobService.create_job:它要登入者、且輪次離開規劃期後擋新增(_require_manager、_assert_round_in_planning)。ReverifyCloneQuery 的 clone_assignees(pair, "system")、clone_org_units(pair)、clone_devices(pair)(infra/flow_control/repository/reverify_clone_query.py),pair 自組 {old_job_id, new_job_id, new_job_uid, new_tpl_job_id};新 service 經 DI 取用,不在 app 層寫 SQL。若判斷要把三支搬到共用位置,搬完在 docs/claude/domain-capabilities.md 登記一列。get_by_job_execution_id),以 create_binding 建新綁定,tool_params 原樣帶過(含上次源碼包 uid 與已加密的敏感參數,不可再經 _encrypt_tool_secret_params);分派列(job_execution_detection_tool_agents)逐列複製,scheduled_at 只在仍是未來時間時保留,過去改 NULL。source_survey_uid 各建一份全新空白快照(create_snapshot),掛 job_evidences(type SURVEY),再跑 _reconcile_task_surveys 依設備展開(D13)。detection_executions)、問卷答案、留言、完成時間。src/components/grc/job-drawer/JobDrawerHeader.vue 標題後加「第 N 期」小標(period_no > 1 或有週期時顯示);「我的任務」清單任務名後同樣加。src/components/grc/job-drawer/JobRecurrenceRows.vue:週期、自動執行、高風險額外通知三行;JobExecutionDrawer.vue 檢測設定卡內只加一行引用。一般與問卷任務沒有檢測設定卡,週期一行放在「描述」上方。