搜尋開發指南

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

文件目錄

NEXUSPRO / HANDBOOK

組織與員工

從組織樹、職位目錄到員工生命週期與在職分析,沿著目前已存在的 HR 契約閱讀資料範圍。

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

本章目標與前置條件

讀完後,你應能找到員工、組織、職位、上游同步與在職分析的程式碼,並知道每個操作的資料範圍。前置條件是能閱讀 REST API、UUID、分頁與基本組織樹;不需要先接觸 eHRMS 帳號。

功能現況

區塊目前來源可核實的功能前端入口與現況
員工list(q、status、include_offboarded、org_unit_id、category)、create、detail、update、profile、stats、leave-records、transitions、password-reset、activate-accounts、bulk-reassign;沒有 delete,離職與復聘走 transitions/workspace/employees → features/hr-employees:列表、詳情、profile、請假紀錄、帳號開通、CSV 匯出與 source-sync 開關,附多組元件測試
組織org chart(含上游 source_hierarchy)、org units 的 list/create/update(沒有 delete,以 closed 關閉),以及員工掛接與批次改派/hr/org → features/hr-organization:樹狀轉換、上游層級顯示與 SWR mutation
職位positions 的 list/get/create/update/delete;停用透過 status=disabled/hr/positions → features/hr-positions:表格、編輯、刪除、cache revalidation,以及送出 dry_run=false 的 eHRMS 正式同步按鈕
上游同步手動 POST /v1/hr/ehrms/sync(預設 dry-run);租戶排程開關 GET/PUT /v1/hr/source-sync 與 POST /v1/hr/source-sync/initialize(正式寫入)/workspace/employees 的同步開關與初始化、/hr/positions 的正式同步;真實上游連線仍需受控環境
分析/hr/turnover/monthly 與 /hr/turnover/annual/workspace/turnover → features/hr-turnover:月份/年度查詢與未來期間防呆

HR 路由都註冊在 internal/api/v1/routes.go 的 registerHRRoutes。每條路由的 permission point、risk、tenant_wide 與 target_employee 以 internal/route/testdata/registry.golden 為準;api/openapi.yaml 只有部分端點在描述文字提到權限,不能當依據。HR 路由分成兩類:

  • tenant-wide:org chart、org units、positions、turnover、/hr/ehrms/sync、source-sync,以及 bulk-reassign(它沒有逐員工的範圍判定,只能以 tenant-wide 收斂)。
  • 依資料範圍過濾(tenant_wide=false):員工 list、stats、create、activate-accounts,以及所有帶 :employeeId 的路由;單筆路由另外帶 TargetEmployee。因此不能把「持有 hr.employee.read」誤讀成可以讀全租戶。

核心概念

組織與職位是租戶治理資料

hr.org_unit、hr.position、hr.employee 是授權資源鍵;實際資料表是 public.org_units、public.positions、public.employees,都在 tenant transaction 內讀寫。組織樹的 parent_id 與推導出的 path 決定 department/department subtree 資料範圍,重掛節點不是單純的 UI 排序。

PUT /v1/hr/org-units/:orgUnitId 是整筆替換:name、closed、show_in_org_chart 必填,其餘欄位可為 null,未宣告的鍵會被拒。它在 org tree lock 下於同一 transaction 改寫整棵子樹的 path,沒有版本欄位或 CAS。組織沒有 delete,closed=true 會向下傳染,也不能把節點搬到自己的後代之下。職位同樣沒有版本:刪除是 high-risk,帶上游 source(如 eHRMS)或仍被組織的 manager_position_id 引用時會被拒;停用(status=disabled)則保留歷史引用。HR 裡有樂觀鎖的只有 employee profile 與 source-sync 開關(兩者都用 expected_version)。

員工資料分成 identity、profile 與 assignment

Employee 是 HR 聚合的主體,profile 端點處理可編輯欄位與 profile contract(PUT .../profile 必須帶 expected_version),assignment 及生命週期操作則由 service 的 transition 規則處理;離職、復聘都走 POST .../transitions,復聘會建立新員工列並回 201。

帳號開通不等於建立員工。activate-accounts 一次最多 200 人,逐筆套用資料範圍並回 outcome(not_found、offboarded、already_linked、company_email_missing、email_in_use、accepted),全部被拒仍回 200;accepted 只代表受理,帳號建立與邀請信由 worker 在交易外完成。bulk-reassign 同樣最多 200 人,在一筆交易內執行,任一員工失敗就整批回滾。

GET /v1/hr/employees/:employeeId/leave-records 除了 hr.employee.read,還會對同一 target 再檢查 attendance.leave.read,兩個範圍取交集。員工 CSV 匯出沒有後端端點:前端 features/hr-employees/employeeExportApi.ts 逐員工先以 POST /v1/authz/check 檢查 hr.employee/export,再讀 detail、profile、/v1/leave/balances 與 leave-records 組成報表;入口需要 hr.employee.export 與 hr.employee.read。

搜尋與上游組織層級

GET /v1/hr/employees?q= 修剪後為空視為未帶、超過 64 字回 400,以不分大小寫子字串比對 display_name、員工檔案的 english_name、company_email 與 employee_no(%、_、\ 視為字面)。GET /v1/hr/org-chart?q= 比對 display_name、english_name、employee_no,只過濾回傳的員工,不影響主管解析。前端 hooks/api/useEmployeeNameMatches.ts 以 q 加 include_offboarded=true 取回全部分頁,組織頁、/workspace/attendance 與 /workspace/clock 的關鍵字搜尋靠它補上英文名比對(需要 hr.employee.read)。

Migration 000068_org_source_hierarchy.sql 在 public.org_units 保存上游的 source_depth、source_parent_code、source_manager_employee_no(以 source_hierarchy_present 標示)。只有 source sync 會寫入,手動 PUT 會保留既有快照;遷移前的列要等下一次成功的 source sync 才有值。GET /v1/hr/org-chart 的每個 unit 以 source_hierarchy 回傳這些事實與 manager_resolution(not_assigned/resolved/unresolved;同租戶、同 source 的外部員工編號恰好對到一人才算 resolved)。這是呈現用的上游事實,與授權用的 parent_id/path 分開,不能拿來判斷資料範圍。

eHRMS 同步是來源重建,不是名稱比對

SyncEHRMS 先載入本地 positions、org units、employees,再依 position code、unit code、external employee id 或 employee number 做確定性比對。同步會依序處理職位、開放組織、員工、manager,再處理來源不再出現的資料;source-owned 資料與手動資料不可混為一談。

入口有兩條,行為不同:

  • 手動 POST /v1/hr/ehrms/sync:同步執行、上限 30 分鐘;空物件 body 等於只 dry-run HR 目錄。選項為 dry_run(預設 true)、include_resigned、leave、balances_year(2000–2100)與 activate_accounts(dry-run 時忽略)。回應只有計數、row_errors 與問題摘要,不含上游欄位值;/hr/positions 的同步按鈕送的是 dry_run=false,屬正式同步。
  • 租戶排程 source-sync:PUT /v1/hr/source-sync 必須同時帶 enabled 與 expected_version(不符回 409 stale_version);POST /v1/hr/source-sync/initialize 要求開關已開(否則 409 source_sync_disabled)並帶 1–160 字的 Idempotency-Key,回 202 排入首次匯入;已成功初始化或仍有工作執行中時會被拒。開啟後 worker 每 30 分鐘一輪:eHRMS HR 同步 →(EHRMS_SYNC_ACTIVATE_ACCOUNTS=true 時)帳號開通 → 在初始化與每日維護輪(當日第一輪或上一輪未成功)加跑假別、請假與年度餘額 → 當年整年的出勤同步(初始化用排入時的年度)。這條路徑沒有 dry-run,全部是正式寫入;EHRMS_SYNC_ENABLED、EHRMS_SYNC_FULL_ON_START 只是相容舊設定。

程式位置:手動路由 internal/api/v1/hr_ehrms.go → 編排 internal/wiring/ehrms.go(fetch → HR → accounts → leave → balances)→ internal/service/hr/ehrms_sync.go 的 SyncEHRMS;上游 client 在 internal/platform/ehrms;排程開關在 internal/api/v1/source_sync.go 與 internal/service/sourcesync,worker 在 cmd/worker/ehrms_sync.go。

一條請求鏈

架構流程圖 · Mermaid

正在繪製架構圖…

查看圖表原始碼
flowchart LR
  UI["Next page"] --> SWR["feature api.ts + SWR"]
  SWR --> ME["/v1/me identity"]
  SWR --> Gin["Gin /v1/hr/*"]
  Gin --> Auth["route point + target scope"]
  Auth --> Service["service/hr"]
  Service --> Repo["Repository + tenant tx"]
  Repo --> DB[("PostgreSQL + RLS")]

前端的 cache key 會包含 identity;例如員工 detail 的查詢要等 /v1/me 得到 tenant/account 後才發送。後端仍需重新判斷 scope,不能把前端是否顯示按鈕當作授權。

實作步驟與示例

  1. 先在 api/openapi.yaml 確認 operation、欄位與錯誤,權限與資料範圍則對 internal/route/testdata/registry.golden。handler 都在 internal/api/v1/:員工 hr_employees.go、hr_employee_*.go(stats、bulk-reassign、帳號開通、password reset)、hr_profile*.go、hr_leave_records.go;組織 hr_org.go、hr_org_chart.go;職位 hr_positions.go;同步 hr_ehrms.go、source_sync.go;分析 hr_turnover.go。
  2. 若是新欄位,按 migration、query/sqlc、Repository、service、handler、測試的順序落地;不要直接在 handler 寫 SQL。
  3. 針對單筆員工操作確認 TargetEmployee 與資料範圍;跨租戶或 scope 外的結果不能透過改寫 ID 繞過。
  4. 前端在 features/hr-employees/api.ts 以 schema 解析 wire response,再讓頁面元件呈現;欄位新增要同步 adapter、表單與測試。
http
GET /v1/hr/employees?status=active&q=chen&page=1&page_size=20
Authorization: Bearer <由執行環境注入>

上例只展示契約形狀,不提供 token,也不代表本輪已打 API。q 會同時比對中文姓名、英文名、公司信箱與員編;status 與 include_offboarded 不能同時帶(400)。要驗證,先在有授權的本地環境讀回 tenant 內資料,再檢查分頁 total、欄位策略與 audit;不要用別人的 .env 或共用資料庫做試驗。

驗證矩陣

驗證問題程式碼/測試證據仍需另外做的驗收
員工 CRUD 與欄位 contractinternal/api/v1/hr_employees_test.go、features/hr-employees/api.test.tsx有授權帳號的 API happy path 與負向 scope
雙名搜尋tests/integration/postgres/hr_employee_name_search_test.go、hooks/api/useEmployeeNameMatches.test.tsx真實資料在組織頁與出勤頁的搜尋結果
組織樹 mapping 與改派internal/service/hr/org_chart_test.go、internal/service/hr/org_source_test.go、tests/integration/postgres/hr_org_test.go、tests/integration/postgres/hr_employee_bulk_reassign_test.go、features/hr-organization/tree.test.ts真實租戶樹與跨租戶阻擋
職位生命週期internal/api/v1/hr_positions_filter_test.go、tests/integration/postgres/hr_org_test.go、features/hr-positions/api.test.ts停用後的歷史引用與被引用時刪除被拒
eHRMS row isolationinternal/service/hr/ehrms_sync_rows_test.go、internal/api/v1/hr_ehrms_test.go受控上游快照、dry-run 影響數與正式同步
source-sync 開關與排程internal/api/v1/source_sync_test.go、cmd/worker/ehrms_sync_test.go、tests/integration/postgres/source_sync_test.go受控上游的首次初始化與 30 分鐘排程
turnover 月/年計算internal/service/hr/turnover_metrics_test.go、features/hr-turnover/api.test.tsxruntime 期間、時區與實際資料核對

常見錯誤

  • 看到 /v1/hr/employees/:employeeId 就直接在 SQL 以 ID 查詢:應先完成 principal、target 與 scope 判斷。
  • 把 employee profile、登入 account、外部 eHRMS row 當成一張表;它們的 ownership 與生命週期不同。
  • 用姓名匹配同步資料;同步實作使用 code/external id,姓名只可作顯示與搜尋。
  • 把 dry_run=true 的計數當成已寫入;dry-run 必須確認沒有 mutation,再按授權執行正式同步。source-sync 的開關與初始化沒有 dry-run,一律是正式寫入。
  • 以為 org unit 或 position 的 PUT 有版本保護;兩者都是整筆替換、沒有 CAS,送出前要以最新資料為基礎。
  • 把 source_hierarchy 當成授權階層;資料範圍只看本地 parent_id/path。
  • 以為只給 hr.org_unit.read 就看不到員工:GET /v1/hr/org-chart 是 tenant-wide,會回傳全租戶(預設不含離職)的員工名冊與主管關係,不受 hr.employee 範圍限制。
  • 用前端 fixture 或單元測試結果宣稱真實 HR 已可用;目前尚未在本文執行後端 API、eHRMS 或部署驗證。

相關文件

內容來源與核實範圍

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

  • nexus-pro-be-plus/internal/api/v1/routes.go
  • nexus-pro-be-plus/internal/api/v1/hr_employees.go
  • nexus-pro-be-plus/internal/api/v1/hr_org.go
  • nexus-pro-be-plus/internal/api/v1/hr_org_chart.go
  • nexus-pro-be-plus/internal/api/v1/hr_positions.go
  • nexus-pro-be-plus/internal/api/v1/hr_ehrms.go
  • nexus-pro-be-plus/internal/api/v1/source_sync.go
  • nexus-pro-be-plus/internal/route/testdata/registry.golden
  • nexus-pro-be-plus/internal/service/hr/employees.go
  • nexus-pro-be-plus/internal/service/hr/org.go
  • nexus-pro-be-plus/internal/service/hr/org_chart.go
  • nexus-pro-be-plus/internal/service/hr/org_chart_source.go
  • nexus-pro-be-plus/internal/service/hr/ehrms_sync.go
  • nexus-pro-be-plus/internal/service/hr/turnover.go
  • nexus-pro-be-plus/internal/service/sourcesync/service.go
  • nexus-pro-be-plus/internal/wiring/ehrms.go
  • nexus-pro-be-plus/cmd/worker/ehrms_sync.go
  • nexus-pro-be-plus/db/queries/employees.sql
  • nexus-pro-be-plus/db/migrations/000068_org_source_hierarchy.sql
  • nexus-pro-web-plus/features/hr-employees/api.ts
  • nexus-pro-web-plus/features/hr-employees/sourceSyncApi.ts
  • nexus-pro-web-plus/features/hr-employees/employeeExportApi.ts
  • nexus-pro-web-plus/features/hr-organization/api.ts
  • nexus-pro-web-plus/features/hr-positions/api.ts
  • nexus-pro-web-plus/features/hr-turnover/api.ts
  • nexus-pro-web-plus/hooks/api/useEmployeeNameMatches.ts
NexusPro 開發指南以程式碼為準 · 以驗證為據