1. 架構定位

採用:

Modular Monolith + Hexagonal Architecture + Clean Dependency Rule

原則:

  • 目前維持單一 NestJS 應用。
  • 使用模組邊界隔離業務能力。
  • 使用 Ports & Adapters 隔離外部系統。
  • 程式碼依賴只能指向 Application、Domain 與 Ports。
  • 不拆微服務、不拆資料庫。
  • 第二家醫院出現前,不建立額外 npm packages 或 repositories。

2. 目標結構

src/
├── operations/
│   ├── task/
│   ├── robot/
│   ├── fleet/
│   ├── device/
│   ├── maps/
│   ├── facility/
│   ├── charging/
│   ├── application/
│   ├── domain/
│   ├── ports/
│   └── operations.module.ts
│
├── access/
│   ├── authentication/
│   ├── authorization/
│   ├── users/
│   ├── sessions/
│   ├── audit/
│   └── access.module.ts
│
├── product-api/
│   ├── controllers/
│   ├── gateways/
│   ├── dto/
│   └── product-api.module.ts
│
├── adapters/
│   ├── rmf/
│   ├── robot-control/
│   ├── fleet-manager/
│   ├── mongo/
│   ├── redis/
│   ├── websocket/
│   ├── scheduler/
│   └── adapters.module.ts
│
├── hospital/
│   └── csh/
│       ├── controllers/
│       ├── workflows/
│       ├── policies/
│       ├── integrations/
│       ├── config/
│       └── csh-hospital.module.ts
│
├── app.module.ts
└── main.ts

3. 核心依賴方向

flowchart TB
    API["Product API"] --> APP["Application Services"]
    HOSPITAL["Hospital Workflows"] --> APP
    WORKER["Workers / Schedulers"] --> APP
    RMF_IN["RMF State Listener"] --> APP

    APP --> DOMAIN["Domain Rules"]
    APP --> PORTS["Ports"]

    RMF_OUT["RMF Adapter"] --> PORTS
    ROBOT["Robot Control Adapter"] --> PORTS
    FLEET["Fleet Manager Adapter"] --> PORTS
    MONGO["Mongo Adapter"] --> PORTS
    REDIS["Redis Adapter"] --> PORTS
    WS["WebSocket Adapter"] --> PORTS

允許:

Controller       → Application
Hospital Workflow → Application
Worker           → Application
Application      → Domain、Ports
Adapter          → Ports
AppModule        → Modules、Adapters

禁止:

Controller  → Repository / RMF / Redis
Application → Concrete Adapter / Mongo Document
Domain      → NestJS / MongoDB / Redis / HTTP / RMF
Port        → Concrete Adapter
Operations  → Hospital Implementation

4. Operations 範圍

Operations 負責跨醫院共用的機器人產品能力:

Task

  • 建立、取消、繼續任務
  • Task lifecycle
  • Task queue
  • Priority
  • Retry
  • Task history
  • Multi-station task
  • Return、charging task

Robot 與 Fleet

  • Robot、Fleet
  • Robot availability
  • Robot assignment
  • Battery gate
  • Busy、reservation
  • Task ownership
  • Runtime state normalization

Maps、Device、Facility

  • Map、Floor、Waypoint、Route
  • Charging Station
  • Device state
  • Door、Elevator 操作
  • Facility availability

Orchestration

  • Dispatch、cancel、reset pose
  • Auto charging
  • Emergency return lifecycle
  • Queue processing
  • Robot state ingestion
  • Operations events

Operations 不包含:

  • CSH、CMP、HIS
  • 特定醫院或 Station 名稱
  • SSO provider
  • 醫院審批及通知規則
  • RMF payload
  • HTTP DTO

5. Hospital 範圍

Hospital 負責:

  • HIS、Pharmacy、Employee integration
  • CSH、CMP
  • Azure AD、LDAP、Keycloak
  • Hospital Controller
  • Hospital Workflow
  • Approval、Emergency、Escalation Policy
  • Hospital Config
  • Station alias
  • Medication status mapping
  • HMI/HIS 通知規則

分類方式:

類型歸屬
跨醫院相同能力Operations
只有設定值不同Hospital Config
決策規則不同Hospital Policy
外部協定不同Hospital Adapter
單一醫院 APIHospital Controller

平台不得使用醫院名稱判斷。


6. Access 範圍

Access 負責:

  • Login、logout、refresh token
  • Users
  • Roles、permissions
  • Session
  • User activity
  • Account lock/unlock
  • Audit log

Hospital Authentication Adapter 將外部身分轉為:

Actor
├── id
└── roles

Operations 只使用 Actor 執行 audit 與記錄 trigger source;RBAC 判斷留在 Access Guard/API 層。


7. Product API 範圍

共用 API

  • Tasks
  • Robots
  • Fleet
  • Devices
  • Maps
  • Dashboard
  • System settings
  • WebSocket gateways

Hospital API

  • CMP messages
  • Employee sync
  • Medication
  • HIS robots
  • Hospital SSO callback

Controller 只負責:

驗證輸入
→ DTO mapping
→ 呼叫 Application Service
→ Response mapping

8. 最小 Ports

FleetCommandPort

負責:

  • Dispatch
  • Cancel
  • Reset pose

FleetRegistryPort

負責:

  • 從 Fleet Manager 取得 Robot 清單

RobotControlPort

負責:

  • 發送 Robot/HMI task event
  • 發送 ROS stop signal
  • 播放 Robot video

FacilityControlPort

負責:

  • Open
  • Close

Door 與 Elevator 暫時共用同一個 Port。

RobotStateStore

負責:

  • 讀取 Robot runtime state
  • 儲存正規化 runtime state
  • 列出 Fleet/Robot state

OperationsEventPublisher

負責發布:

  • Task completed/cancelled/failed
  • Robot unavailable
  • Emergency started/cleared
  • Fleet state changed

Repository Ports

依 use case 逐步建立:

  • TaskRepository
  • TaskQueueRepository
  • RobotRepository
  • FleetRepository
  • MapRepository
  • DeviceRepository

普通內部 Service 不建立 interface。


9. Operations 與 Hospital 溝通

Commands

CreateTask
CancelTask
ContinueTask
UpdateTaskProgress
EmergencyReturn
RequestCharging
ControlFacility
PlayRobotVideo

Queries

GetTask
SearchTaskHistory
GetRobotAvailability
GetRobotCurrentTask
GetAvailableChargingStations
GetFleetState
GetMap

Events

TaskCreated
TaskDispatched
TaskStarted
TaskCompleted
TaskCancelled
TaskFailed
EmergencyStarted
EmergencyCleared
RobotUnavailable

Command、Query、Event 不包含:

  • RMF payload
  • MongoDB ObjectId
  • Mongoose Document
  • Express Request
  • CSH/HIS 格式

目前透過 NestJS DI 直接呼叫,不使用 HTTP 或 Message Broker。


10. RMF 整合

Outbound

Application
    ↓
FleetCommandPort
    ↑
RmfFleetAdapter
    ↓
Open-RMF

RMF Adapter 負責:

  • RMF payload
  • HTTP request
  • API key
  • Timeout
  • Response mapping
  • Error mapping

Inbound

RMF / Socket.IO
    ↓
RmfStateAdapter
    ↓
UpdateRobotStateService
    ↓
RobotStateStore
    ↓
Redis

RMF 原始資料不得進入 Domain。


11. Realtime 架構

業務事件

OperationsEventPublisher 發送:

  • Task status change
  • Emergency state
  • Robot availability change

Dashboard Snapshot

留在 Product API / WebSocket Adapter:

  • Fleet status
  • Task overview
  • Live task queue
  • IoT health
  • Active alerts

Snapshot 不需要包裝成 Domain Event。


12. 資料權威來源

資料權威來源
Task lifecycle、history、queueMongoDB
Task 與 Robot ownershipMongoDB
Robot、Fleet、Map、Device 設定MongoDB
Robot 實際物理狀態RMF
最新 Robot runtime stateRedis
Busy、HMI reservationRedis,具備 TTL
Dashboard 即時資料Redis / Application Query

派工判斷:

Mongo Task Ownership
+ Redis Runtime State
+ Redis Busy Lease
= Robot Availability

Redis 無法讀取時停止新派工。

MongoDB 可以保存最後一次 Robot snapshot,但不能單獨作為即時派工依據。


13. Worker 與派工可靠性

Queue 狀態:

PENDING
→ PROCESSING
→ DISPATCHING
→ IN_PROGRESS
→ COMPLETED / FAILED

Queue record 包含:

workerId
leaseExpiresAt
attemptCount
lastError
idempotencyKey

規則:

  • 使用 MongoDB findOneAndUpdate 原子領取。
  • 同一任務只能由一個 Worker 取得。
  • Worker 中斷後,lease 到期允許重試。
  • taskHistoryId 作為 dispatch idempotency key。
  • 呼叫 RMF 前先記錄 DISPATCHING
  • RMF inbound task state 負責狀態 reconciliation。
  • 提供 stuck DISPATCHING 恢復流程。
  • Robot Keeper 使用唯一條件避免重複建立充電任務。
  • 外部 RMF 呼叫不放進 Mongo transaction。

暫不導入 Kafka、RabbitMQ、Outbox 或額外 Worker framework。


14. Persistence

Mongo Adapter 負責:

  • Mongoose schema
  • Mongo query
  • Index
  • ObjectId 轉換
  • Document mapping
  • Atomic claim
  • Transaction

Redis Adapter 負責:

  • Robot runtime state
  • Busy lease
  • Reservation
  • Task tracking
  • Cache

規則:

  • Domain/Application 使用字串 ID。
  • ObjectId 只存在 Mongo Adapter。
  • Mongoose Document 不離開 Adapter。
  • Feature 不直接存取其他 Feature 的 Repository。
  • 初期共用 MongoDB 與 Redis。

15. Error Handling

External Error
→ Adapter Error Mapping
→ Application Error
→ API Exception Filter
→ HTTP Response

Domain/Application 使用與 transport 無關的錯誤碼。

Domain 不建立 NestJS HttpException


16. Configuration

Operations Config

  • Queue limit
  • Retry count
  • Worker lease
  • Battery threshold
  • Idle threshold
  • RMF timeout

Hospital Config

  • Station names
  • HIS/Pharmacy URL
  • SSO
  • Cron
  • Emergency destination
  • Medication mapping
  • HMI endpoint

環境變數由 Composition Root 或 Adapter 讀取並注入。Application/Domain 不直接存取 process.env


17. Observability

關鍵 log 欄位:

traceId
taskHistoryId
robotId
fleetId
operation
attempt
outcome
duration

最低監控項目:

  • Queue latency
  • Dispatch success/failure
  • Retry count
  • Expired lease
  • Stuck dispatch
  • RMF state age
  • Redis failure
  • Unavailable Robot count

沿用現有 logger 與 trace,不建立額外 observability package。


18. NestJS Modules

AppModule
├── OperationsModule
├── AccessModule
├── ProductApiModule
├── RmfAdapterModule
├── PersistenceModule
└── CshHospitalModule

AppModule 只負責:

  • Module composition
  • Port/Adapter binding
  • Global middleware
  • Global exception filter
  • Configuration

使用現有 ESLint no-restricted-imports 管理依賴,不增加 Nx 或 dependency-cruiser。


19. 導入順序

Phase 1:Fleet Port

  • 建立 FleetCommandPort
  • 將 OpenRmfApiService 改為 Adapter。
  • 遷移 dispatch、cancel、reset pose。
  • 建立 FakeFleetCommandPort 測試。

Phase 2:Controller 瘦身

  • Repository 呼叫移入 Application。
  • Auto-charge 移入 Operations。
  • Device open/close 經過 Application。

Phase 3:Robot 與 Fleet 外部邊界

  • 建立 RobotControlPort
  • 建立 FleetRegistryPort
  • 移除 Service 內直接 Axios/ROS 呼叫。

Phase 4:Worker 可靠性

  • Atomic claim
  • Processing lease
  • Idempotency key
  • Dispatch reconciliation
  • Robot Keeper 去重

Phase 5:Runtime State

  • 建立 RobotStateStore
  • 正規化 RMF inbound state。
  • 分離 Redis runtime state 與 Mongo durable state。

Phase 6:Persistence 隔離

  • 移除 Application/Domain 的 ObjectId
  • 移除 Mongo Document。
  • 建立 model mapping。
  • RMF payload 移入 Adapter。

Phase 7:Hospital 與 Access 邊界

  • 集中 CSH/CMP/HIS/Medication。
  • 建立 AccessModule。
  • Station 改用 Config。
  • 共用 lifecycle 留在 Operations。

Phase 8:Modules 與依賴規則

  • 建立主要 NestJS Modules。
  • 精簡 AppModule。
  • 加入 ESLint import restrictions。

20. 第二家醫院

先進行需求分類:

相同能力 → Operations
值不同   → Hospital Config
規則不同 → Hospital Policy
協定不同 → Hospital Adapter
單院 API → Hospital Controller

預設模式

同一 monorepo、多個 Hospital Apps:

hospital-platform/
├── packages/
│   └── operations/
├── apps/
│   ├── hospital-1-api/
│   └── hospital-2-api/
└── adapters/

獨立 Repository 條件

只有符合以下條件才拆:

  • 不同維護團隊
  • 程式碼存取隔離
  • Hospital application 明顯分歧
  • 不同技術棧
  • 獨立審查及發布流程

21. Package 與版本策略

第二家醫院證明共用面後建立:

@company/operations
@company/product-api
@company/contracts

@company/contracts 只在有獨立契約消費者時建立。

所有 packages 採同步版本:

@company/operations   1.0.0
@company/product-api  1.0.0
@company/contracts    1.0.0

各醫院可部署不同版本:

Hospital 1 → Platform 1.2.0
Hospital 2 → Platform 1.0.0

22. 最終驗收標準

  • Controller 只呼叫 Application Service。
  • Application 不依賴 Concrete Adapter。
  • Domain 不依賴 NestJS、MongoDB、Redis、HTTP、RMF。
  • ObjectId 和 Mongo Document 只存在 Adapter。
  • RMF outbound 只經過 FleetCommandPort
  • Robot HMI/ROS 只經過 RobotControlPort
  • RMF inbound state 已正規化。
  • Mongo、Redis、RMF 的權威範圍明確。
  • 多 Worker 不會重複派送。
  • Dispatch、cancel、events 具備 idempotency。
  • CSH/CMP 只存在於 Hospital 邊界。
  • Access 與 Operations 分離。
  • AppModule 只負責 Composition。
  • 不拆微服務或資料庫。
  • 不建立尚未被實際重用的 package、repository 或 Policy。

Modular Monolith 重構計畫(已對齊最新 dev)

現況與調整判斷

  • devorigin/dev 與重構 branch 的 base 都是 5e06422,不需要 rebase 或 merge。
  • aaac50b → 5e06422 新增約 1,382 行,主要包含:
    • 全 fleet emergency pause、HMI emergency broadcast。
    • Emergency 期間禁止人工取消、允許返藥局任務穿越特定 reservation。
    • Robot obstacle/full-charging telemetry。
    • Door configuration seed。
    • 任務站點不得連續重複。
    • Docking arrival 的 decision-manager activity。
  • 原計畫方向不變,但 Phase 3、4、5、7 必須納入上述 invariants,避免重構造成 emergency、telemetry 或 docking regression。
  • 現有 Phase 1 草稿位於獨立 worktree,尚未 commit;它已基於 5e06422,不需重做。
  • 原工作樹目前乾淨,但仍繼續使用獨立 worktree,確保後續工作不干擾 dev

介面與資料權威

  • 建立並以 token 注入:
    • FleetCommandPort:dispatch、cancel、reset pose。
    • FacilityControlPort:door/lift control。
    • RobotControlPort:HMI task/emergency event、ROS stop、video。
    • FleetRegistryPort:Fleet Manager robot discovery。
    • RobotStateStore:正規化 runtime state。
    • OperationsEventPublisher:task、robot、fleet、emergency events。
    • Use-case 所需 repository ports。
  • HTTP route、DTO shape、錯誤碼及 notifiedRobots 等既有 response 欄位保持不變。
  • 資料權威補充:
    • taskAcceptancePausedAt 與 task ownership:MongoDB。
    • RMF status、fullChargingobstacleDetectedobstacleDetectedSec:Redis runtime state。
    • Availability 必須合併 Mongo ownership/emergency pause、Redis runtime state 與 busy lease。
  • RMF payload、Axios response、Mongoose document、ObjectId 不得穿越 adapter。
  • TaskQueue 加入 lease、attempt、idempotency 與完整 lifecycle;multi-station 以 taskHistoryId + dispatchSequence 區分合法的多次 dispatch。

八階段 commits

  1. refactor(architecture): introduce fleet command port

    • 完成現有 Phase 1 草稿:Fleet command contracts、RMF adapter、fake/test adapter。
    • Task dispatch/cancel 改依賴 FleetCommandPort
    • 保留 emergency cancel options:notifyAmrallowDuringEmergency
    • 修正最新 dev 既有的 3 個 Prettier lint errors。
    • Gate:build、ESLint、RMF adapter、task analysis、task cancel tests。
  2. refactor(api): route controllers through application services

    • Controllers 移除 repository、RMF adapter 直接依賴。
    • 建立 task、robot、fleet、device、map、settings commands/queries。
    • Auto-charge 與 facility control 移入 Operations application。
    • Door seed/upload 繼續沿用既有 CLI、Compose 與 HTTP contract,但經 facility application service。
    • 保留 user 欄位 64 字元限制與 consecutive station validation。
  3. refactor(adapters): isolate robot and fleet integrations

    • 建立 RobotControlPortFleetRegistryPortFacilityControlPort
    • 移出所有 application 中的 Axios、ROSLIB、Fleet Manager HTTP。
    • Hospital emergency fleet broadcast 與 cancel HMI notification 統一走 RobotControlPort
    • RMF payload builder 移至 RMF adapter,完整保留:
      • HMI arrived/continue callbacks。
      • Simulation routing/token。
      • Charging docking activity。
      • 最新 docking-arrival decision-manager activity 與 activity 順序。
    • 外部 URL、token、timeout 改由 typed config 注入。
  4. feat(queue): add atomic dispatch leasing and idempotency

    • Mongo atomic claim,寫入 workerIdleaseExpiresAtattemptCount
    • 狀態流程:PENDING → PROCESSING → DISPATCHING → IN_PROGRESS → COMPLETED/FAILED
    • 過期 PROCESSING 可重領;結果不明的 DISPATCHING 不自動重派。
    • 保留最新 emergency 規則:
      • Fleet pause 阻擋一般任務與 Robot Keeper。
      • Emergency return 可越過沒有 TaskTracking 支撐的 stale reservation。
      • 不可越過明確屬於另一任務的 tracking。
      • Manual cancel 在 active robot 被 emergency pause 時維持拒絕。
    • Robot Keeper charging task 使用原子 upsert/唯一條件去重。
    • Migration 提供 dry-run 與 --confirm-clear;只清空 TaskQueues 並建立 indexes,不修改 TaskHistory、Redis reservation 或 robot emergency pause。
  5. refactor(runtime): isolate normalized robot state store

    • RMF raw Socket.IO types 留在 inbound adapter。
    • 建立 RobotRuntimeState 與 mapper,包含:
      • Physical/business status、battery、location、commission。
      • hasActiveTaskisBusy
      • fullChargingobstacleDetectedobstacleDetectedSec
    • Redis adapter 明確區分 missing state 與 read failure;派工、availability、auto-charge 在 Redis failure 時 fail closed。
    • 保留 obstacle duration transition logging 與 GET robots 的 Redis telemetry overlay。
    • RMF inbound event 重送必須維持 idempotent。
  6. refactor(persistence): isolate mongo and redis models

    • Mongo schema/query/index/mapping 與 Redis key/TTL/serialization 集中到 adapters。
    • Application/Domain 使用字串 ID,不接觸 Mongoose documents 或 ObjectId
    • 依 use case 建立 repository ports,不建立 generic repository abstraction。
    • Swagger DTO 移到 Product API/Hospital API;validation 規則留在 DTO,業務 invariant 留在 Domain/Application。
    • Queue、emergency pause、return-to-pharmacy 與 telemetry mappings 全部加入 adapter contract tests。
  7. refactor(boundaries): separate hospital and access modules

    • CSH/CMP/HIS、employee、medication、return-to-pharmacy 與 emergency workflow 移至 CSH hospital boundary。
    • Hospital workflow 透過 Operations commands 執行:
      • Fleet-wide task acceptance pause/clear。
      • Cancel-and-create emergency return。
      • Emergency event broadcast。
    • notifiedRobots、simulation broadcast、全狀態 robot 通知與 fire-and-forget failure isolation 保持不變。
    • Authentication、users、sessions、RBAC、activity、audit 移入 Access;JWT principal 映射成 Actor
    • Station name、HMI endpoint、SSO/CSH 設定集中到 hospital config,不以醫院名稱寫條件。
  8. refactor(modules): enforce modular monolith boundaries

    • 完成 OperationsModuleAccessModuleProductApiModuleAdaptersModuleCshHospitalModule
    • AppModule 僅保留 config、composition、port bindings、global middleware/filter 與 lifecycle。
    • ESLint import restrictions:
      • Domain 禁止 NestJS、Swagger、Mongo/Mongoose、Redis、Axios、ROSLIB、Socket.IO。
      • Application/Ports 禁止 concrete adapters、infra、Product API、Hospital implementation。
      • Controllers 禁止 repository/adapters。
      • Operations 禁止 Hospital implementation。
    • 更新 architecture、migration 與 deployment runbook;Swagger spec 只在驗證時暫時生成,不重新追蹤已從 dev 移除的 api-spec.json

驗證與 assumptions

  • Node 24.13.0 baseline:
    • Build 已通過。
    • 現有 24 個 service suites、211 tests 已通過。
    • E2E 目前因缺 .env/Mongo 而無法在裸環境執行,不屬於程式 regression。
  • 每階段 commit 前執行 build、無 --fix ESLint、相關 service tests。
  • Phase 4 加測 worker competition、lease expiry、dispatch uncertainty、emergency bypass、keeper dedupe、migration guard。
  • Phase 5 加測 telemetry normalization、Redis fail-closed、obstacle transition 與 inbound idempotency。
  • 最終以 Mongo/Redis 測試環境執行完整 service/e2e suite,並比對 Swagger routes、status codes、response shapes。
  • 每個 Phase 維持一個可建置、可單獨回退的 commit。
  • branch 固定以 5e06422 為 base;實作期間不自動吸收後續 dev commits。