搜尋開發指南

試試「多租戶」、「啟動」或「outbox」。
搜尋僅使用本站文件,不會傳送至外部服務。

文件目錄

NEXUSPRO / HANDBOOK

考勤與出勤

了解打卡證據、每日投影、制度版本、工作地點、行事曆與同步批次如何分工。

內容核對 2026-09-28·圖文指南

本章目標與前置條件

讀完後,你應能區分原始打卡證據與 daily projection,知道哪一些端點屬於本人、哪一些是 tenant-wide 管理操作,並能為一次考勤改動安排驗證。前置條件是了解日期區間、時區、分頁、授權與 Idempotency-Key。

功能現況

區塊已在後端路由/服務中出現前端入口與現況
本人打卡GET /v1/attendance/clock-status、POST /v1/attendance/clock打卡卡片 app/(platform)/_components/AttendanceClock.tsx(首頁、/tasks、/myleave 共用),經 features/gps 處理 location、risk remark、status 與 clock flow
證據與投影GET /v1/attendance/clock-records、/daily-records、/days、/monthly-summary/workspace/clock(ClockTimeView:clock-records 與 daily-records)、/workspace/attendance(WorkHoursPanel:/days)、/myleave 月出勤(/days 與 clock-records);/monthly-summary 目前沒有前端呼叫端
制度與地點policy、policy/options、worksites、calendar-integration(含 verify)/workspace/leave-policy 的 AttendanceSettingsPanel:版本編輯、初始制度設定、worksite picker 與 Google integration UI
行事曆calendar、years、days(新增/刪除租戶覆寫)、import(JSON 與 CSV)同頁的 AttendanceCalendarPanel:年度讀取、覆寫、刪除,政府年度與 CSV 匯入都先預覽再確認;沒有匯出
digest/同步digest-rules、/attendance/ehrms/sync-runs(建立、查詢、items、retry)與 worker沒有前端 UI;定期同步由 HR source-sync worker 觸發,digest job 要 ATTENDANCE_DIGEST_ENABLED=true(預設 false)才建立

權限以 internal/route/testdata/registry.golden 為準:clock-status 與 clock 只作用於呼叫者本人;clock-records、days、daily-records、monthly-summary 可帶 employee_id,必須落在 decision scope 內;policy、worksites、calendar、calendar-integration、digest-rules 與 sync-runs 都是 tenant-wide 管理操作。/workspace/clock 與 /workspace/attendance 的員工關鍵字搜尋透過 hooks/api/useEmployeeNameMatches.ts 呼叫 GET /v1/hr/employees?q=,同時比對中文名與英文名(見組織與員工)。

核心概念

原始證據與讀模型

clock 寫入的是帶方向、時間、位置及風險結果的原始 evidence(public.attendance_clock_records);days、daily-records 與 summary 是供頁面讀取的投影(public.attendance_day_projections)。daily-records 另外合併已同步的 eHRMS 日資料(public.attendance_external_days),讀取時不呼叫 eHRMS,並以 effective_source、differing_fields 標示來源差異。投影不是讓前端補算規則的邀請,資料缺失、敏感欄位 null 與「零」必須依契約分辨。

日狀態與投影更新

flexible 制的出勤時間包含午休,達每日應出勤時數就不判遲到或早退(原始 evidence 與 geofence 風險仍保留);fixed 制與「請假日固定班表」(leave_day_fixed_schedule_enabled)仍以扣除午休的淨工時計算。eHRMS 日資料若上下班卡涵蓋整個排班(含午休)也判為正常,但不改寫來源工時。

投影有三條更新路徑:

  • 同步寫入:POST /v1/attendance/clock 在同一 transaction 寫 evidence、重算當日 projection 並寫 audit。
  • 讀時重算:/days、/monthly-summary 與 digest 讀取時會重算已過預期下班時間仍沒有下班卡的 pending 日、行事曆已變更的日子,以及 flexible 制的遲到/早退日(eHRMS 排班日除過期 pending 外以來源為準);結果只回應、不回寫,所以資料庫裡的 projection 列可能落後於 API 回應。
  • outbox/worker:表單核准的補卡、請假、加班及其撤銷(form.leave_cancellation.approved、form.overtime_cancellation.approved)由 internal/worker/attendance.go 消費後重算投影;撤銷只在 attendance_approved_intervals.cancelled_at 留痕並從投影排除(migration 000066_attendance_cancellations.sql)。eHRMS sync run 由 sync worker 寫入外部日資料與投影,地標解析也走 outbox。

打卡的冪等與風險確認

POST /v1/attendance/clock 必須帶 Idempotency-Key,但伺服器只檢查它存在;真正的去重鍵是 body 的 client_event_id,比對的是「actor 加完整 body」的 SHA-256 指紋。同一 client_event_id 且指紋相同會回放原結果(200;低精度紀錄重送仍回 422);改 body 會得到 409 idempotency_conflict。風險確認保留相同 client_event_id 與原座標,依契約改用新的 Idempotency-Key(前端以 client_event_id 加 :risk 後綴),伺服器會重新檢查時窗與 policy。風險挑戰與時窗拒絕都不會消耗 client_event_id。

policy、calendar 與 worksite

policy 的時間選項只接受每半小時(HH:00 或 HH:30),版本更新使用 base_version compare-and-set(不符回 409 stale_version);跨午夜的標準/break interval 回 422 policy_overnight_not_supported。租戶還沒有制度時,GET /v1/attendance/policy 回 404(10050 not_found),clock-status 的 blocked_reason 是 policy_unavailable,打卡回 503;設定頁會顯示「尚未建立出勤制度」,持有 attendance.policy.update 的人檢查前端預設草稿後,以 base_version 為 0 明確發布第一版,系統不會自動建立。

tenant calendar override(POST /v1/attendance/calendar/days)不修改 government row:同日已有租戶列回 409 calendar_day_exists,刪除 government 列回 409 calendar_day_not_tenant。停用 worksite(status=disabled,沒有 delete)會保留歷史引用,不再出現在 clock-status 的 worksites 與新的 geofence 判定。

Google Calendar 設定是租戶層級:PUT /v1/attendance/calendar-integration 保存 enabled、workspace_domain、delegated_admin_email,service_account_json 只寫不讀、以 ENCRYPTION_KEY 加密保存(未設定時帶 JSON 會回 503),GET 只回安全的中繼資料。啟用且已有憑證時,設定一有變更狀態就回到 error(calendar_unverified),要再呼叫 POST /v1/attendance/calendar-integration/verify(需 Idempotency-Key);原樣再存一次則保留上次檢查結果。

行事曆匯入

POST /v1/attendance/calendar/import 需要 Idempotency-Key,三種 body 的 dry_run 都預設 true:

  • JSON(year、dry_run):只匯入程式內嵌的政府行事曆(目前 2026、2027 年),正式匯入只替換該年 government 列。
  • multipart/form-data(year、source、選填 dry_run、一個 file)或 raw text/csv(year、source、dry_run 放 query):source 為 government 或 tenant,正式匯入只替換該 source 的列。CSV 必須是 UTF-8、表頭恰為 date,kind,name、上限 256 KiB,日期不可重複且須屬於該年,空檔會被拒;tenant 匯入不會建立 government 年度覆蓋。

前端不論 JSON 或 CSV 都先以 dry-run 預覽 diff,確認時才送 dry_run=false,每次請求使用新的冪等鍵。目前沒有行事曆匯出。

來源同步與 digest

  • 手動:POST /v1/attendance/ehrms/sync-runs 需 Idempotency-Key(最多 160 字),dry_run 與 include_offboarded 都預設 true,日期是 [start_date,end_date) 半開區間、最多 366 天,employee_ids 可帶 1–500 人,回 202;再以 GET .../sync-runs/:runId、.../items 追蹤,POST .../retry 重試。
  • 定期:HR 的 source-sync 開啟後,worker 每 30 分鐘一輪,在 HR 同步之後為當年整年(含未來已核准的請假)建立並執行 sync run;開關與初始化見組織與員工。
  • digest:GET/PUT /v1/attendance/digest-rules,PUT 以 revision 做樂觀鎖;排程 job 只在 ATTENDANCE_DIGEST_ENABLED=true 時建立,但規則寫入不受開關影響。

一條請求鏈

架構流程圖 · Mermaid

正在繪製架構圖…

查看圖表原始碼
flowchart LR
  Card["打卡卡片"] --> Status["GET clock-status"]
  Card --> Punch["POST clock"]
  Punch --> Gate["replay、時窗、policy、worksite、risk"]
  Gate --> Evidence[("clock evidence")]
  Evidence -->|"同一 transaction"| Projection[("day projection")]
  Forms["表單核准與撤銷 outbox"] --> Projection
  Sync["eHRMS sync run worker"] --> Projection
  Sync --> External[("eHRMS 日資料")]
  Projection --> Read["days、daily-records、monthly-summary"]
  External --> Read

前端在 /v1/me 完成後才建立 SWR key,team 查詢還要判斷 hr.employee.read 等明確權限。後端會在 route、service、Repository transaction 再判斷 tenant/employee 範圍;不要只靠 UI 把 employee selector 隱藏。讀取端的重算只影響回應內容,不會回寫投影。

實作步驟與示例

  1. 先在 api/openapi.yaml 確認日期、sort、範圍與 response status,權限 point 與 tenant_wide 則對 internal/route/testdata/registry.golden。handler 都在 internal/api/v1/:attendance.go(policy、worksites、calendar 與 /leave/*)、attendance_calendar_csv.go、attendance_clock.go(clock-status、clock、clock-records、days)、attendance_sync.go(sync-runs、daily-records)、attendance_digest.go(digest-rules、monthly-summary)與 attendance_integration.go。
  2. 將 policy/calendar/worksite 寫入留在 attendance service 的單一 transaction;打卡的投影在同一 transaction 同步寫入,跨域效果(表單核准、撤銷、地標)走 outbox/worker,讀取端的重算不得回寫。
  3. 打卡寫入前檢查 idempotency、hard window、accuracy、worksite 與 risk;不要在前端先判定「一定成功」。
  4. 查詢頁使用 server projection,並對上限做明確 UI 提示:/days 必填日期、含首尾最多 62 天;/clock-records 日期選填、含首尾最多 366 天;/daily-records 是 [start_date,end_date) 半開區間、最多 62 天;/monthly-summary 單月超過 6200 列時要帶 employee_id 縮小範圍。
http
GET /v1/attendance/daily-records?start_date=2026-09-01&end_date=2026-09-08&page=1&page_size=50&sort=work_date%20asc,employee_id%20asc

daily-records 的 end_date 不含當天,上例讀的是 9/1 到 9/7;同一週改打 /v1/attendance/days 時要帶 end_date=2026-09-07(含首尾),sort 固定為 work_date desc,employee_id asc。正式打卡 request 的欄位與 status 以 OpenAPI AttendanceClockRequest 為準;本文不填入座標、帳號或 token。驗證時應分別測成功 evidence、風險確認、低精度 GPS、時窗外 409、同鍵 replay 與改 body conflict。

驗證矩陣

驗證問題程式碼/測試證據仍需另外做的驗收
clock status 與 GPS flowinternal/api/v1/attendance_clock.go、features/gps/clockFlow.test.ts、features/gps/clockStatus.test.tsx真實位置服務與 policy 時窗
idempotency/riskinternal/service/attendance/punch_test.go、internal/service/attendance/geofence_test.go、tests/integration/postgres/attendance_punch_test.go、features/gps/useAttendancePunchFlow.test.tsx受控 DB 的 replay、conflict、audit
daily/hours projectioninternal/api/v1/attendance_sync_test.go、internal/service/attendance/day_status_credit_test.go、internal/service/attendance/listed_days_deadline_test.go、tests/integration/postgres/attendance_calendar_refresh_test.go、features/attendance-daily-records/api.test.ts、features/work-hours/WorkHoursPanel.test.tsx跨日、時區、敏感欄位 field policy
policy/worksite/integrationinternal/service/attendance/policy_test.go、internal/service/attendance/integration_update_test.go、tests/integration/postgres/attendance_test.go、tests/integration/postgres/attendance_integration_test.go、features/attendance-settings/AttendanceSettingsPanel.test.tsx真實 Google credential 與受控連線檢查
calendar importinternal/api/v1/attendance_calendar_csv_test.go、tests/integration/postgres/calendar_csv_test.go、features/attendance-calendar/api.test.tsxdry-run diff、正式替換與資料量上限
表單效果與撤銷internal/service/attendance/effects_cancellation_test.go、tests/integration/postgres/attendance_cancellation_test.go受控環境核准/撤銷後的投影
sync run 與 source-syncinternal/api/v1/attendance_sync_test.go、internal/service/attendance/sync_worker_test.go、cmd/worker/ehrms_sync_test.go受控上游的整年同步結果

常見錯誤

  • 用 clock-status 的最新紀錄自己算下一步,忽略 server policy、late evidence 與 employee_ineligible。
  • 重送風險確認仍沿用舊 Idempotency-Key;確認 request 需要新的 transport key,但 business client_event_id 不變。伺服器目前不會因沿用舊 key 報錯,這仍是契約要求。
  • 把低精度 GPS 的拒絕當作「沒有紀錄」;契約會保存 rejected raw evidence,422 gps_accuracy_insufficient 另有語意。
  • 把 calendar imported=false 當成錯誤;未匯入年仍可成功回應,summary 需以 calendar_covered 標示 calendar coverage。
  • 以為 daily-records 的 end_date 含當天;它是半開區間,/days 與 /clock-records 才含首尾。
  • 以為 public.attendance_day_projections 的列一定等於 API 回應;過期 pending、行事曆覆寫與 flexible 遲到/早退是讀時重算,不回寫。
  • 在新租戶直接測打卡卻沒先發布制度;GET /v1/attendance/policy 會回 404、打卡回 503,要先由有權限的人在設定頁建立第一版。
  • 把 CSV 的 source=tenant 匯入當成已建立政府年度覆蓋;只有 government 匯入會寫 imported_at。
  • 把 dry_run 或 fixture UI 當成正式資料庫驗收;目前本文未啟動 attendance service,也未操作真實位置/行事曆。

相關文件

內容來源與核實範圍

以下路徑相對於所列業務倉庫;核實層級為「已讀原始碼」,不是本輪業務測試或部署驗收。

  • nexus-pro-be-plus/internal/api/v1/attendance.go
  • nexus-pro-be-plus/internal/api/v1/attendance_calendar_csv.go
  • nexus-pro-be-plus/internal/api/v1/attendance_clock.go
  • nexus-pro-be-plus/internal/api/v1/attendance_sync.go
  • nexus-pro-be-plus/internal/api/v1/attendance_digest.go
  • nexus-pro-be-plus/internal/api/v1/attendance_integration.go
  • nexus-pro-be-plus/internal/route/testdata/registry.golden
  • nexus-pro-be-plus/internal/service/attendance
  • nexus-pro-be-plus/internal/domain/attendance
  • nexus-pro-be-plus/internal/worker/attendance.go
  • nexus-pro-be-plus/cmd/worker/ehrms_sync.go
  • nexus-pro-be-plus/db/migrations/000066_attendance_cancellations.sql
  • nexus-pro-web-plus/app/(platform)/_components/AttendanceClock.tsx
  • nexus-pro-web-plus/app/(platform)/workspace/clock/_components/ClockTimeView.tsx
  • nexus-pro-web-plus/features/gps/api.ts
  • nexus-pro-web-plus/features/clock-records/api.ts
  • nexus-pro-web-plus/features/attendance-daily-records/api.ts
  • nexus-pro-web-plus/features/attendance-settings/api.ts
  • nexus-pro-web-plus/features/attendance-calendar/api.ts
  • nexus-pro-web-plus/features/work-hours/api.ts
NexusPro 開發指南以程式碼為準 · 以驗證為據