本卡屬 FR-150(母卡見上方連結),第 ① 棒:後端全部。做完用 curl 就能列出候選任務、寄出提醒、在通知紀錄看到手動列。FE 卡依賴本卡。
讓後端能回答兩件事:「這個專案本輪有哪些未完成任務、各自該催誰、上次手動催是什麼時候」,以及「PM 勾了這幾張,幫我寄提醒信、寫紀錄、在卡上留言」。自動到期提醒的行為不能變。
scripts/sql/2026-10-xx-fr150-task-alert-triggered-by.sql
ALTER TABLE compliance.task_alert_notifications ADD COLUMN IF NOT EXISTS triggered_by varchar(64);
COMMENT ON COLUMN ... IS '手動觸發者 login_name;排程自動寄的為 NULL';
infra/task_alert/models/ 對應 model 加欄(帶 comment=);entity domain/task_alert/entity/task_alert_notification_entity.py 加 triggered_by: Optional[str] = None;repo 的 claim 要寫入它。object_type=job_due、stage=manual_<epoch 秒>,同一批共用同一個 stage 值。唯一索引是 (object_type, COALESCE(scan_uid, object_uid), COALESCE(stage,''), recipient_email),每批 stage 不同所以可重複寄。stage 欄 varchar(32) 夠用。api/task_alert/routes/,route 不碰 DB)GET /project/<project_uid>/job-due-reminder/candidates
POST /project/<project_uid>/job-due-reminder body {job_uids: [1..200], message: 可空 ≤500}
job_uid、job_name、control_no、control_title、status、due_at、due_at_manual、recipients([{user_id, login_name, nickname}],空陣列=FE 灰掉)、review_step_label(「第 N 關 · 關名」,零關 null)、last_manual_reminded_at。排序 BE 做:逾期 → 3 天內 → 其他有期限 → 沒期限;同級期限近到遠、再依任務名。天數照自動版用台北日曆天算。{jobs, sent, failed, skipped_no_recipient}。ForbiddenError(GRC_NOT_MANAGER);本輪已結束 PreconditionFailedError 新碼 GRC_412xxx;job_uids 有不屬於本專案本輪、或已完成/取消的卡 → BadRequestError 新碼 GRC_400xxx,整批退回不部分寄。新碼加在 common/code/flow_control_error_code.py(task_alert 模組既有碼就在這),實作時查當下各段最大序號往下接(FR-148 平行在加碼)。新檔 app/task_alert/service/manual_job_due_reminder_service.py,list_candidates 與 send 兩個 public method 都 @transaction。DI 在 di_containers/ 的 task_alert container wiring。
common.authz.project.assert_project_manager(參考 app/flow_control/service/job_service.py:146 _require_manager 的寫法)。輪次已結束判斷比照改期限那條。app/task_alert/service/job_due_reminder_service.py::_notify_job 內「claim 紀錄 → 寄信 → mark sent/failed」那段抽成新檔 app/task_alert/service/job_due_mail_sender.py,自動版改呼叫它,自動版行為不變。SMTP 未啟用時 send_mail_notification 回 False 要記 failed(照自動版)。force_locale("zh_Hant_TW")):主旨新 key job_due_manual_mail_subject「[%(system)s] 專案管理者提醒:%(job)s」;內文沿用 job_due_mail_content,when 用新 key job_due_manual_mail_when「專案管理者 %(pm)s 請您儘速處理」;有留言接新 key job_due_manual_mail_message「<br><br>管理者留言:<br>%(message)s」,留言先 html.escape 再換行轉 <br>。_due_text(None) 回空字串會留一行「完成期限:」空白。手動版期限要印「未設定」。app/flow_control/service/not_met_job_redo_service.py:80 的寫法 job_comment_domain_service.create(job_uid, author_id=None, author_nickname=<系統名>, content),作者名用 not_met_redo_comment.author_nickname()。內容新 key job_due_manual_comment「管理者 %(pm)s 已寄出提醒」,有留言接「:<留言>」。留言寫入失敗只記 warning,不影響寄信結果。