搜尋開發指南

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

文件目錄

NEXUSPRO / HANDBOOK

外部系統與平台適配層

將 Keycloak、eHRMS、Temporal、物件儲存、Google 與其他供應商隔離在 platform,讓 domain 只依賴穩定的本地語意。

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

本章目標

看懂 NexusPro 如何把外部 wire contract、credential、timeout、重試與 provider error 收斂到 internal/platform/,並在上層使用 domain-neutral port。本文只整理目前已讀取的 adapter 與設定;配置鍵存在不等於整合已啟用,本輪未執行外部服務聯調。

前置條件

先讀技術架構、Outbox、Jobs 與 Temporal與部署設定。取得任何秘密都應走既有 credential 管道;文件和 log 只寫鍵名、非敏感預設與 evidence,不寫 API key、private key、cookie、token 或 service-account JSON。

適配層邊界

架構流程圖 · Mermaid

正在繪製架構圖…

查看圖表原始碼
flowchart LR
  Service["Domain service"] --> Port["Local port / domain-neutral type"]
  Port --> Adapter["internal/platform adapter"]
  Adapter --> Provider[("Keycloak / eHRMS / Google / SFTPGo / Temporal / LiteLLM")]
  Service --> DB[("PostgreSQL facts + receipts")]

Service 不應出現供應商欄位名、status code、帳號 provisioning 語意或 SDK 型別;depguard 規則 service-cannot-import-platform 直接禁止 internal/service/** import internal/platform/**。adapter 負責 URL、header、redirect、response size、retry/timeout、credential 和 provider error 分類,再回傳本地 typed result。

前端也有一個直接呼叫外部服務的 BFF:app/api/geo/current-context 先用 httpOnly token 呼叫後端 /v1/me 驗證身分並做記憶體內限流,再把座標降精度後呼叫 Open-Meteo(天氣)與 Nominatim(反向地理,可用 NOMINATIM_BASE_URL 覆寫),這段不經後端 platform。

Keycloak 身份整合

後端 internal/platform/keycloak 的 admin client 以 tenant attribute(tenant claim 的唯一來源)做 ownership source;EnsureUser 對 email natural key 做 create-or-get,tenant attribute 相同才 no-op,缺少才補寫;既有使用者已屬於其他 tenant 或已停用時回 ErrConflict,不會強行搬家。DisableUser 也會撤銷 session,避免只停用 local account 仍能用 refresh token。

執行期只有 worker 組 admin-side directory(internal/wiring/identity.go 的 NewIdentityProvisioner),API 只組 JWT verifier。佈建、停用與重設密碼信都以 outbox 事件交給 worker(identity.provision_requested、identity.account_disabled、identity.password_reset_requested;員工帳號開通先發 hr.employee.account_requested),所以重設密碼的 API 回 202,而不宣稱信已寄出。cmd/ops 的 bootstrap 工具是另外直接連 Keycloak 的操作入口。

Google 登入走 Keycloak identity broker:deploy/keycloak/apply-realm.sh 把 Google IdP 的 first broker login 指向 first broker login link-only(先偵測既有使用者再自動連結,建立帳號的子流程停用),因此已開通帳號的員工第一次用 Google 登入即可連結,未知 email 不會自動建帳號。

前端 app/api/auth 以 OIDC Authorization Code + PKCE 為主,另有站內密碼登入 password-login(Keycloak Direct Access Grants,realm client 需開 directAccessGrantsEnabled),以及只在 mock 模式存在的開發用 dev-login(關閉時回 404);token 一律寫 httpOnly cookie 並支援 refresh。proxy.ts 在 server-side 將 access token 注入 /v1/**。client JS 不碰 token,前端頁面守衛只做 cookie presence check,真正 JWT、tenant claim、permission 和 RLS 驗證仍在 backend。

Keycloak 的 KEYCLOAK_BASE_URL、KEYCLOAK_PUBLIC_BASE_URL、client、JWKS cache、admin client 與 invite 設定在 backend .env.example/api/config-reference.md 有 schema;production/staging 的 HTTPS、初始密碼和 provisioning 條件以設定 validator 為準。不要把 KEYCLOAK_INITIAL_PASSWORD 當一般測試帳密寫進文件。

eHRMS 與資料同步

internal/platform/ehrms/client.go 是 eHRMS 上游的 transport/normalization boundary,包含固定 path、response size 上限、共享 concurrency cap(10)、redirect 禁止與 request timeout。它同時服務考勤/請假(/attendance、/leave、/leave-entitlement、/leave-types)與 HR 目錄同步(/employees、/departments、/positions,含部門的來源層級欄位);nested rows、日期、enum 和 dirty item 在 adapter 先正規化,service 只接收 domain records。production 組裝會呼叫 RequireAdmission(),之後每次出站前都先 sourceguard.Check,沒有有效准入的請求不會送出。

同步不是「呼叫成功就等於本地政策完整」。應以 GET /v1/hr/source-sync 的 last_status/initialization_status、逐筆錯誤、sync run 狀態、手動 dry-run summary 與 PostgreSQL projection 判斷狀態。單筆 dangling FK 或格式錯誤需可記錄並跳過,不讓整個批次永久卡住;若是要整批原子,必須寫出理由。

統一來源同步與准入

自動同步由每個租戶的持久化政策控制,預設關閉,不再由環境變數開啟:

端點作用
GET /v1/hr/source-sync回 enabled、state(disabled/stopping/enabled)、version、upstream_configured、最近與下次執行時間、last_status、initialization_status、last_error_code
PUT /v1/hr/source-syncbody 為 enabled 與 expected_version;版本不符回 409/20202,上游未設定卻要開啟回 503/10500;開啟後下次執行為當下加 30 分鐘
POST /v1/hr/source-sync/initialize需 Idempotency-Key(1–160 字元)與空物件 body,回 202;同 key 重送只回目前狀態;匯入年份為受理當下的 Asia/Taipei 年度

三者權限都是 hr.employee 的 sync(high、tenant-wide、頁面 hr.employees),前端入口是 features/hr-employees/SourceSyncControls.tsx。worker 的 ehrms_sync job 每分鐘檢查各租戶是否到期(排隊中的初始化優先),以資料庫 lease 認領一輪後依序執行:

  1. HR 目錄:員工、部門、職位。
  2. 帳號開通:EHRMS_SYNC_ACTIVATE_ACCOUNTS=true 時,把本輪同步的在職員工送進與「開通員工帳號」按鈕相同的 governor 批次。
  3. 假別目錄與假勤餘額:初始化時,或每個 Asia/Taipei 日的第一輪(前一輪未成功也會補跑)。
  4. 考勤/請假:建立並執行涵蓋當年 1 月 1 日起一整年的考勤同步批次(初始化沿用受理年份)。

逐筆錯誤、帳號開通的略過類別或批次未完成都讓結果記為 partial;初始化只有全部成功才算 succeeded。

所有來源工作都先經 internal/service/sourcesync 取得有限 lease(30 分鐘預算),包括上面的排程、POST /v1/hr/ehrms/sync、考勤 sync run 的建立/重試/消費與 cmd/ehrms-sync。政策關閉時排程直接略過,手動入口回 409/30400(source_sync_disabled);已有在途 lease 或初始化排隊中回 409/30401(source_sync_active);初始化已成功再送回 409/30402(source_sync_initialized)。lease 的 id 與 generation 經 internal/sourceguard 放進 context:repository 的每個 tenant transaction 先鎖准入列,eHRMS client 每次出站前檢查,執行中每秒複查;關閉政策會讓舊 generation 失效並取消在途工作。關閉時若仍有在途 lease,state 會是 stopping,此時重新開啟回 30401。

手動入口與設定鍵

  • POST /v1/hr/ehrms/sync:body 省略 dry_run 時預設 dry-run,可帶 include_resigned、leave、balances_year、activate_accounts;上游未設定時回 503。
  • POST /v1/attendance/ehrms/sync-runs、GET /v1/attendance/ehrms/sync-runs/:runId、GET /v1/attendance/ehrms/sync-runs/:runId/items、POST /v1/attendance/ehrms/sync-runs/:runId/retry:考勤同步批次。
  • go run ./cmd/ehrms-sync:預設 dry-run,加 --apply 才寫入;--tenant/--actor 預設取 EHRMS_SYNC_TENANT_ID/EHRMS_SYNC_ACCOUNT_ID。

HR 手動入口、CLI 與 worker 共用 internal/wiring/ehrms.go 的 EHRMSSync orchestration;考勤批次走 attendance.SyncService,兩者受同一准入規則約束。

設定鍵:EHRMS_BASE_URL 與 EHRMS_API_KEY 皆非空時,API/worker 才組 eHRMS client 與兩個同步 job,upstream_configured 才為 true;EHRMS_SYNC_ACTIVATE_ACCOUNTS 決定排程是否跑帳號開通階段。EHRMS_SYNC_ENABLED、EHRMS_SYNC_FULL_ON_START 已是相容舊設定:runtime 不再依它們啟用或初始化同步(前者只剩設定檢查),請改用租戶總開關與 initialize。URL/key 等要求依 api/config-reference.md;本文件不提供任何值。

物件儲存與附件

internal/platform/objectstore/objectstore.go 定義 driver-neutral ObjectStore:Stage(io.Reader)、Finalize、Open、Stat、Delete。它以 stream 計算 size/SHA256,不把整個檔案讀成 []byte;SFTPGo path、token、HTTP status 與 account semantics 停在 adapter,PostgreSQL 才保存授權、immutable reference、checksum 與 lifecycle。

附件流程先授權並寫 pending DB reference;上傳請求在 API 內同步完成 Stage → Finalize(含 SHA-256 的 immutable key)→ MarkStored,checksum 或 key 衝突時標為 quarantined。只有刪除走 outbox:撤銷先改 row,再由 attachment.object.delete_requested 讓 worker 刪 bytes,attachment_reaper 也用同一事件清逾期上傳的 staging。不要先刪 bytes 再刪 row,也不要把 object path 當 ACL。

OBJECT_STORE_PROVIDER 必須顯式指定 sftpgo、local 或 memory;production/staging 只允許 sftpgo,且 HTTP endpoint 必須是 HTTPS;local/memory 只在合適的 development/test 形狀使用。SFTPGo 目前只走 HTTP(S) API:sftp:// endpoint 在開啟時就被拒絕(SFTP transport 未實作),OBJECT_STORE_SFTP_HOST_KEY、OBJECT_STORE_SFTP_INSECURE_SKIP_HOST_KEY 仍是 reserved。

Google Calendar 與 Geocoding

Google Calendar adapter 只暴露 attendance CalendarEventProvider,內部處理 service-account JSON、domain-wide delegation(每次查詢以該員工 email 為 subject)、固定 Google endpoint、page/event/response-size 上限、redirect 禁止與 bounded retry。Geocoder 則對地址/座標做輸入驗證、固定 endpoint、有限重試、程序內負 cache 與 provider failure classification;不把 location、URL 或 API key 寫進錯誤訊息。

Calendar 的租戶設定

  • GET /v1/attendance/calendar-integration:只回安全欄位(enabled、workspace_domain、delegated_admin_email、service_account_email、credential_configured、status、status_checked_at、last_error_code),不回金鑰。
  • PUT /v1/attendance/calendar-integration:寫 enabled、workspace_domain、delegated_admin_email(網域須與 workspace 相同),可選 service_account_json(原始 JSON 或 Base64)。金鑰用 ENCRYPTION_KEY(internal/platform/secrets 的 AES-256-GCM)封裝後存入 attendance_calendar_integrations;ENCRYPTION_KEY 未設定或無效時,帶金鑰的寫入回 503,不退回明文。啟用狀態、網域、delegated admin 或金鑰有變更時狀態會重設(啟用且有金鑰時為 error/calendar_unverified,需重新驗證);原樣儲存則保留既有驗證結果。
  • POST /v1/attendance/calendar-integration/verify:需 Idempotency-Key,以 delegated admin 查詢一小時內的事件驗證連線,結果寫回 status(如 active、unauthorized、policy_blocked、error)。

權限為 attendance.calendar_integration 的 read/update(頁面 attendance.policy),前端呼叫在 features/attendance-settings/api.ts。打卡流程以 IntegrationService.ForTenant 取得該租戶的 provider。已落地的 Google 設定只有 GOOGLE_CALENDAR_FETCH_TIMEOUT 與 GOOGLE_GEOCODING_ENABLED、GOOGLE_GEOCODING_API_KEY、GOOGLE_GEOCODING_TIMEOUT;geocoding 開啟時 startup validator 要求提供 API key。

LiteLLM、Redis、OpenFGA 與 Temporal

目前 config registry 收錄 LLM/LiteLLM、Redis、Temporal、NATS 等設定,但是否有 runtime consumer 要看 Key.Landed()(里程碑已落地或標了 ConsumerLanded)、wiring 與實際程式碼。Agent chat 已接通:LLM_PROVIDER(預設 litellm,fake 不得用於 staging/production)、LITELLM_BASE_URL、LITELLM_API_KEY、LITELLM_CHAT_MODEL 已落地,POST /v1/agents/runs 建立 run 後由 worker 消費 agent.run.requested(見AI Agent);只有 LITELLM_EMBEDDING_MODEL 與 LITELLM_MASTER_KEY 仍是 reserved,不能寫成 embedding 已可用。NATS_* 是未來保留,不代表 current event bus。

Temporal 是長流程 adapter,不是業務讀模型;TEMPORAL_ENABLED 預設 false(改用 in-process fake),詳見非同步工作。Redis 只在 REDIS_ENABLED=true 時由 API 開啟,用於授權判定 snapshot cache 與 rate limit(RATE_LIMIT_ENABLED 時),不把 cache 當 PostgreSQL fact source。OpenFGA 尚未接入:OPENFGA_* 全部是 reserved,internal/platform/ 下沒有 OpenFGA adapter,授權由 internal/authz 依 PostgreSQL facts 判定;日後接入時 service 仍只用本地 permission port,供應商 tuple/API error 不應穿越 domain。

操作、驗證與預期結果

  1. 先在 api/config-reference.md(由 internal/config 產生)確認設定是否 landed、是否 required、是否 secret。原始定義分散在 internal/config/keys_*.go:keys_identity.go(Keycloak、ENCRYPTION_KEY、OpenFGA)、keys_attendance.go(eHRMS、Google、Geocode)、keys_platform.go(物件儲存、feature flag)、keys_integrations.go(LLM、Redis、Temporal、NATS)。
  2. 找 adapter constructor、domain port、wiring 與 caller,確認 timeout、redirect、response cap、retry、log redaction 和 error vocabulary。
  3. 用 fake/contract test 驗證 provider response;需要真聯調時指定 base URL、租戶、版本和 credential scope,且不把秘密寫入測試結果。
  4. 對於 side effect,驗證 outbox receipt、replay、dead-letter、projection 和 shutdown;對於 read-only provider,驗證 page cap、empty/partial/invalid response 的差異;對來源同步,驗證政策關閉時出站為零、stopping 狀態與 30400/30401/30402。
bash
# 工作目錄:nexus-pro-be-plus;以下是按來源核實的聚焦入口,本輪未跑外部聯調
go test ./internal/platform/ehrms ./internal/platform/keycloak ./internal/platform/objectstore ./internal/platform/temporal/... -count=1
go test ./internal/platform/googlecalendar ./internal/platform/geocode ./internal/platform/secrets -count=1
go test ./cmd/worker -run 'SourceSync' -count=1
go test ./internal/config -count=1

預期結果: provider 失敗能分成 invalid、unauthorized、timeout、unavailable 或 permanent;domain 不需要知道供應商 wire 欄位;秘密不出現在 log;缺少必需設定時 process fail fast;reserved 設定不會被誤報為已啟用功能;來源同步關閉後不再有新的出站請求。

常見錯誤與相關文件

常見錯誤:service 直接 import vendor SDK 或 internal/platform/**、把 HTTP 200 當資料有效、無上限讀取 response、對 POST 做盲目 retry、把第三方 ID 當 tenant authority、把 object path 當 ACL、在 frontend NEXT_PUBLIC_* 放秘密、以為設定 GOOGLE_CALENDAR_ENABLED 或 EHRMS_SYNC_ENABLED 就能啟用整合、繞過 sourcesync 准入直接呼叫 eHRMS client,或只驗 /healthz 就宣稱整合 ready。

路由索引把 OPTIONS /v1/*path 歸在本章,但它只是 CORS preflight 的 catch-all:internal/api/v1/api.go 只在設定 CORS_ALLOWED_ORIGINS 時註冊並回 204,與供應商整合無關;瀏覽器經 proxy.ts 同源轉發時不會用到它。

相關閱讀:技術架構、Outbox、Jobs 與 Temporal、前端認證與資料流、組織與員工、考勤與出勤、附件與兩段式物件儲存、AI Agent、部署與交付驗證。

內容來源與核實範圍

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

  • nexus-pro-be-plus/internal/platform/ehrms/client.go
  • nexus-pro-be-plus/internal/platform/ehrms/hr.go
  • nexus-pro-be-plus/internal/platform/keycloak/admin.go
  • nexus-pro-be-plus/internal/platform/objectstore/objectstore.go
  • nexus-pro-be-plus/internal/platform/objectstore/open.go
  • nexus-pro-be-plus/internal/platform/googlecalendar/client.go
  • nexus-pro-be-plus/internal/platform/geocode/google.go
  • nexus-pro-be-plus/internal/platform/httpclient/factory.go
  • nexus-pro-be-plus/internal/platform/secrets/box.go
  • nexus-pro-be-plus/internal/platform/temporal/client.go
  • nexus-pro-be-plus/internal/service/sourcesync/service.go
  • nexus-pro-be-plus/internal/service/sourcesync/runner.go
  • nexus-pro-be-plus/internal/sourceguard/context.go
  • nexus-pro-be-plus/internal/service/attendance/integration.go
  • nexus-pro-be-plus/internal/service/attachment/service.go
  • nexus-pro-be-plus/internal/wiring/ehrms.go
  • nexus-pro-be-plus/internal/wiring/attendance_integration.go
  • nexus-pro-be-plus/internal/wiring/identity.go
  • nexus-pro-be-plus/internal/api/v1/source_sync.go
  • nexus-pro-be-plus/internal/api/v1/api.go
  • nexus-pro-be-plus/cmd/worker/ehrms_sync.go
  • nexus-pro-be-plus/internal/config/keys_integrations.go
  • nexus-pro-be-plus/internal/config/keys_attendance.go
  • nexus-pro-be-plus/internal/config/keys_identity.go
  • nexus-pro-be-plus/internal/config/keys_platform.go
  • nexus-pro-be-plus/api/config-reference.md
  • nexus-pro-web-plus/proxy.ts
  • nexus-pro-web-plus/app/api/auth/_session.ts
  • nexus-pro-web-plus/app/api/auth/password-login/route.ts
  • nexus-pro-web-plus/app/api/geo/current-context/route.ts
NexusPro 開發指南以程式碼為準 · 以驗證為據