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。