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.ts3. 核心依賴方向
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 Implementation4. 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 |
| 單一醫院 API | Hospital Controller |
平台不得使用醫院名稱判斷。
6. Access 範圍
Access 負責:
- Login、logout、refresh token
- Users
- Roles、permissions
- Session
- User activity
- Account lock/unlock
- Audit log
Hospital Authentication Adapter 將外部身分轉為:
Actor
├── id
└── rolesOperations 只使用 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 mapping8. 最小 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
PlayRobotVideoQueries
GetTask
SearchTaskHistory
GetRobotAvailability
GetRobotCurrentTask
GetAvailableChargingStations
GetFleetState
GetMapEvents
TaskCreated
TaskDispatched
TaskStarted
TaskCompleted
TaskCancelled
TaskFailed
EmergencyStarted
EmergencyCleared
RobotUnavailableCommand、Query、Event 不包含:
- RMF payload
- MongoDB ObjectId
- Mongoose Document
- Express Request
- CSH/HIS 格式
目前透過 NestJS DI 直接呼叫,不使用 HTTP 或 Message Broker。
10. RMF 整合
Outbound
Application
↓
FleetCommandPort
↑
RmfFleetAdapter
↓
Open-RMFRMF Adapter 負責:
- RMF payload
- HTTP request
- API key
- Timeout
- Response mapping
- Error mapping
Inbound
RMF / Socket.IO
↓
RmfStateAdapter
↓
UpdateRobotStateService
↓
RobotStateStore
↓
RedisRMF 原始資料不得進入 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、queue | MongoDB |
| Task 與 Robot ownership | MongoDB |
| Robot、Fleet、Map、Device 設定 | MongoDB |
| Robot 實際物理狀態 | RMF |
| 最新 Robot runtime state | Redis |
| Busy、HMI reservation | Redis,具備 TTL |
| Dashboard 即時資料 | Redis / Application Query |
派工判斷:
Mongo Task Ownership
+ Redis Runtime State
+ Redis Busy Lease
= Robot AvailabilityRedis 無法讀取時停止新派工。
MongoDB 可以保存最後一次 Robot snapshot,但不能單獨作為即時派工依據。
13. Worker 與派工可靠性
Queue 狀態:
PENDING
→ PROCESSING
→ DISPATCHING
→ IN_PROGRESS
→ COMPLETED / FAILEDQueue 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 ResponseDomain/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
└── CshHospitalModuleAppModule 只負責:
- 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.022. 最終驗收標準
- 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)
現況與調整判斷
dev、origin/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、
fullCharging、obstacleDetected、obstacleDetectedSec: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
refactor(architecture): introduce fleet command port- 完成現有 Phase 1 草稿:Fleet command contracts、RMF adapter、fake/test adapter。
- Task dispatch/cancel 改依賴
FleetCommandPort。 - 保留 emergency cancel options:
notifyAmr與allowDuringEmergency。 - 修正最新 dev 既有的 3 個 Prettier lint errors。
- Gate:build、ESLint、RMF adapter、task analysis、task cancel tests。
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。
refactor(adapters): isolate robot and fleet integrations- 建立
RobotControlPort、FleetRegistryPort、FacilityControlPort。 - 移出所有 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 注入。
- 建立
feat(queue): add atomic dispatch leasing and idempotency- Mongo atomic claim,寫入
workerId、leaseExpiresAt、attemptCount。 - 狀態流程:
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。
- Mongo atomic claim,寫入
refactor(runtime): isolate normalized robot state store- RMF raw Socket.IO types 留在 inbound adapter。
- 建立
RobotRuntimeState與 mapper,包含:- Physical/business status、battery、location、commission。
hasActiveTask、isBusy。fullCharging、obstacleDetected、obstacleDetectedSec。
- 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。
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。
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,不以醫院名稱寫條件。
refactor(modules): enforce modular monolith boundaries- 完成
OperationsModule、AccessModule、ProductApiModule、AdaptersModule、CshHospitalModule。 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、無
--fixESLint、相關 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。