# Device-Job **Repository Path**: dxycc/device-job ## Basic Information - **Project Name**: Device-Job - **Description**: Device-Job 是结合 DDD 思想的工业设备实时调度引擎,全程 AI 辅助设计,实现高实时、可靠、可扩展的任务调度。 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 0 - **Created**: 2026-01-18 - **Last Updated**: 2026-06-14 ## Categories & Tags **Categories**: task-schedule **Tags**: None ## README # ⚙️ Device-Job — 工业设备任务调度引擎 > 基于 **DDD + 六边形架构 + CQRS + 事件驱动** 的工业设备实时任务调度系统。 --- ## 📦 项目概览 Device-Job 专为光盘刻录产线场景设计,通过 **领域驱动设计** 将设备任务抽象为聚合、命令、事件,配合 **CQRS 读写分离** 保证命令侧的业务严谨性与查询侧的展示灵活性。 | 维度 | 选型 | |------|------| | 语言 | Java 17 | | 框架 | Spring Boot 3.5 | | 架构 | DDD + 六边形架构 | | 模式 | CQRS(命令/查询分离) | | 事件 | Guava EventBus(基础设施) + Spring `@EventListener`(领域编排) | | 持久化 | MyBatis-Plus + Flyway | | 前端 | Vue 3 + Element Plus + TypeScript | --- ## 🧱 DDD 分层架构 ``` ┌─────────────────────────────────────────────────────────┐ │ device-job-trigger │ │ HTTP 控制器 · Guava EventBus 监听器 · 设备事件回调 │ ├─────────────────────────────────────────────────────────┤ │ device-job-application │ │ Saga 编排 · 命令调度 · 流程策略 · 应用端口定义 │ ├─────────────────────────────────────────────────────────┤ │ device-job-domain │ │ 聚合(Task/Disc/Drive/Robot) · 命令 · 领域事件 · 端口 │ ├─────────────────────────────────────────────────────────┤ │ device-job-infrastructure │ │ 适配器(Repository/Device) · MyBatis · 设备驱动 │ ├─────────────────────────────────────────────────────────┤ │ device-job-type (共享常量/DTO/PO) │ └─────────────────────────────────────────────────────────┘ ``` ### 各模块职责 | 模块 | 职责 | |------|------| | `device-job-type` | 共享常量(`TaskType`/`TaskStep`/`TaskStatus`)、DTO、PO、设备事件 | | `device-job-domain` | 领域核心:聚合根(`Task`)、命令(`*Command`)、领域事件(`*Event`)、端口定义 | | `device-job-application` | 应用编排:Saga(`TaskSaga`)、命令处理器(`CommandDispatcher`)、流程策略(`TaskFlowStrategy`) | | `device-job-infrastructure` | 适配器实现:Repository、`DeviceExecutionAdapter`、`TaskReadSyncListener`、`DeviceOperator` | | `device-job-trigger` | 外部入口:HTTP Controller(`TaskGroupController`/`TaskController`)、Guava 监听器 | | `device-job-app` | Spring Boot 启动类 + Guava EventBus 配置 | ### 依赖方向 ``` Trigger → Application → Domain ← Infrastructure ↓ 端口接口 ↓ 适配器实现 ``` --- ## 🎯 CQRS 模式 系统将 **命令侧(写)** 与 **查询侧(读)** 完全分离: ### 命令侧(Command Model)— `task` 表 - **聚合根** `Task` 封装完整的业务状态机,校验每一步的合法性 - 接收 `*Command` → 校验状态 → 变更聚合状态 → 发布 `*Event` - 事件驱动 Saga 进行下一步编排 - 使用 **Guava EventBus** 在领域层与应用层之间传递事件 **典型命令流:** ``` TaskCreatedCommand → Task.handle() → TaskCreatedEvent ↓ TaskSaga 监听到事件 ↓ ResourcesAllocatedCommand → Task.handle() → ... ``` ### 查询侧(Query Model)— `task_read` 表 - 独立的 **读模型**,通过 `TaskReadSyncListener` 订阅领域事件,实时同步 - 树形结构,`level` 字段区分层级:`level=1` 任务组、`level=2` 子任务 - 按 `parentId` 建立父子关系,前端展开时按需加载子节点 **同步流:** ``` TaskCreatedEvent → TaskReadSyncListener → INSERT task_read (level=1 组 + level=2 子任务) ResourcesAllocatedEvent → TaskReadSyncListener → UPDATE disc_id / drive_id BurnSucceededEvent → TaskReadSyncListener → UPDATE 子任务状态 ``` --- ## 🔄 任务类型与状态机 ### 三种任务类型 | 类型 | 说明 | 步骤链 | |------|------|--------| | `BURN` | 标准刻录 | `INIT → RES_ALLOC → MOVE_IN → BURN → MOVE_OUT` | | `MIGRATE` | 迁移刻录 | `INIT → RES_ALLOC → MOVE_IN → BURN → MIGRATE → MOVE_OUT` | | `CLEANUP` | 清理 | `INIT → RES_ALLOC → CLEAN` | ### 核心步骤 | 步骤 | 含义 | 业务动作 | |------|------|----------| | `INIT` | 初始 | 任务创建,等待资源分配 | | `RES_ALLOC` | 资源分配 | 分配光盘(disc)与光驱(drive) | | `MOVE_IN` | **移盘** | 机械臂将光盘从盘匣移至光驱 | | `BURN` | **刻录** | 光驱执行刻录操作 | | `MIGRATE` | **迁移** | (仅 MIGRATE 类型) 数据迁移 | | `MOVE_OUT` | **还盘** | 机械臂将光盘从光驱归回盘匣 | | `CLEAN` | 清理 | 清理光驱/设备 | ### 任务状态 | 状态 | 含义 | |------|------| | `INIT` | 任务已创建 | | `READY` | 资源已分配,待执行 | | `RUNNING` | 正在执行 | | `COMPLETED` | 全部步骤完成 | | `FAILED` | 步骤失败,任务终止 | --- --- ## 🧭 Saga 编排层 `TaskSaga` 是整个任务流转的 **编排中枢**,位于应用层(`device-job-application`),通过 Guava EventBus `@Subscribe` 订阅所有领域事件,驱动任务逐步推进。 ### Saga 职责 | 能力 | 说明 | |------|------| | **事件监听** | 订阅 17 个领域事件,覆盖任务全生命周期 | | **命令调度** | 通过 `CommandDispatcher` 将下一步命令发回领域层 | | **策略委托** | 调用 `BurnTaskFlowStrategy` 根据当前事件 + 任务类型决定下一步 | | **设备调用** | 通过 `DeviceExecution` 端口调用物理设备执行移盘/刻录/还盘 | | **错误处理** | 任一步骤失败,Saga 终止流程并标记任务 FAILED | ### 编排示例 — BURN 类型 ``` TaskSaga 监听 → 派发命令 → 下一个事件 ──────────────────────────────────────────────────────── TaskCreatedEvent → ResourcesAllocatedCommand → ResourcesAllocatedEvent ResourcesAllocatedEvent → DiscMoveStartedCommand → DiscMoveStartedEvent DiscMoveStartedEvent → [调用 DeviceExecution.moveDisc()] DiscMoveSucceededEvent → BurnStartedCommand → BurnStartedEvent BurnStartedEvent → [调用 DeviceExecution.burn()] BurnSucceededEvent → DiscReturnStartedCommand → DiscReturnStartedEvent DiscReturnStartedEvent → [调用 DeviceExecution.returnDisc()] DiscReturnSucceededEvent → 任务完成(COMPLETED) ``` ### 分支决策 — `BurnTaskFlowStrategy` ```java // 资源分配完成后,CLEANUP 走清理,其余走移盘 afterResourcesAllocated → CLEANUP ? CleanStartedCommand : DiscMoveStartedCommand // 刻录完成后,MIGRATE 走迁移,其余走还盘 afterBurnSucceeded → MIGRATE ? MigrateStartedCommand : DiscReturnStartedCommand ``` --- ## 🗄️ Flyway 数据库迁移 项目使用 **Flyway** 自动管理数据库版本,启动时自动检测并执行未运行的迁移脚本,无需手动建表。 ### 配置 ```yaml # application-dev.yml spring: flyway: enabled: true # 启用 Flyway baseline-on-migrate: true # 非空库也允许基线 clean-disabled: true # 禁止误删数据 locations: classpath:db/migration table: flyway_schema_history ``` ### 迁移脚本 `device-job-app/src/main/resources/db/migration/` | 版本 | 文件 | 内容 | |------|------|------| | V1 | `V1__device.sql` | 创建设备表 | | V2 | `V2__disc.sql` | 创建光盘表 | | V3 | `V3__drive.sql` | 创建光驱表 | | V4 | `V4__magazine.sql` | 创建盘匣表 | | V5 | `V5__task.sql` | 创建任务表(命令模型) | | V6 | `V6__domain_event.sql` | 创建领域事件表 | | V7 | `V7__task_read.sql` | 创建读模型表(CQRS 查询侧) | 启动时 Flyway 自动按版本号顺序执行,`flyway_schema_history` 表记录执行历史。如需种子数据,可在对应 V 版本中直接编写 INSERT 语句。 --- ## ⚡ 核心流程详解 ### 刻录流程(BURN) ``` 用户创建任务 │ ▼ TaskCreatedEvent ──→ TaskReadSyncListener ──→ INSERT task_read (组+子任务) │ ▼ TaskSaga ──→ ResourcesAllocatedCommand │ ▼ ResourcesAllocatedEvent ──→ TaskReadSyncListener ──→ UPDATE disc_id/drive_id │ ▼ TaskSaga ──→ DiscMoveStartedCommand │ ▼ DiscMoveStartedEvent ──→ TaskSaga ──→ DeviceExecution.moveDisc() │ │ │ ▼ │ DeviceOperator.placeDisc() → DeviceResult │ │ │ ▼ │ eventBus.post(DiscMoveCompleted) │ │ ▼ ▼ DiscMoveSucceededEvent ◄────────── DiscMoveCommandListener │ ├──→ TaskReadSyncListener ──→ UPDATE MOVE_IN → SUCCESS │ └──→ BurnTaskFlowStrategy ──→ BurnStartedCommand │ ▼ ...(刻录 → 还盘) ``` ### 完整链路对照表 ``` 用户触发 → TaskCreatedEvent → Saga: 分配资源 → ResourcesAllocatedEvent → SyncListener: 写入 discId/driveId → Strategy: 下一步 = DiscMoveStartedCommand → Saga: 通知设备移盘 → DeviceExecution.moveDisc() → Adapter: deviceOperator.placeDisc() → 设备事件: DiscMoveCompleted → DiscMoveCommandListener → Saga: 移盘成功 → DiscMoveSucceededEvent → SyncListener: 更新 MOVE_IN 状态 → Strategy: 下一步 = BurnStartedCommand → Saga: 通知设备刻录 → DeviceExecution.burn() → Adapter: deviceOperator.startBurn() → 设备事件: BurnCompleted → listener → Saga: 刻录成功 → BurnSucceededEvent → SyncListener: 更新 BURN 状态 → Strategy: 下一步 = DiscReturnStartedCommand(或 MigrateStartedCommand) → Saga: 通知设备还盘 → DeviceExecution.returnDisc() → Adapter: deviceOperator.returnDisc() → 设备事件: DiscReturnCompleted → listener → Saga: 还盘成功 → DiscReturnSucceededEvent → SyncListener: 更新 MOVE_OUT/MIGRATE 状态 + 组状态 = COMPLETED ``` ### 事件总线模型 系统使用 **两个事件总线** 协同工作: ``` ┌────────────────────────────────────────────────────┐ │ 领域事件总线 │ │ (Guava EventBus) │ │ │ │ TaskCreatedEvent → [TaskSaga, TaskReadSyncListener] │ │ ResourcesAllocatedEvent → [同上] │ │ DiscMoveSucceededEvent → [同上] │ │ BurnSucceededEvent → [同上] │ │ ... │ └────────────────────────────────────────────────────┘ ↕ 事件驱动 ┌────────────────────────────────────────────────────┐ │ 设备事件总线 │ │ (Guava EventBus) │ │ │ │ DiscMoveCompleted → DiscMoveCommandListener │ │ BurnCompleted → [相应监听器] │ │ DiscReturnCompleted → [相应监听器] │ └────────────────────────────────────────────────────┘ ``` --- ## 🗄️ CQRS 读模型 — 树形展示 ### 表结构 `task_read` ```sql task_read ( id BIGINT -- 节点ID task_group_id BIGINT -- 任务组ID,同组共享 parent_id BIGINT -- 父节点ID,根节点为 NULL task_name VARCHAR(256) -- 节点名称 task_type VARCHAR(50) -- 任务类型 step_code VARCHAR(50) -- 步骤码 status VARCHAR(20) -- 状态 error_message VARCHAR(500) -- 错误信息 device_id BIGINT -- 设备ID disc_id BIGINT -- 光盘ID drive_id BIGINT -- 光驱ID level TINYINT -- 层级: 1=任务组, 2=子任务 sort_order INT -- 排序 ) ``` ### 前端展示 ``` ┌─ 任务组 (level=1) ─────────────────────────────────┐ │ 任务组ID │ 类型 │ 状态 │ 设备ID │ 创建时间 │ 展开 ▶ │ ├────────────────────────────────────────────────────┤ │ 展开后显示子任务 (level=2): │ │ ┌─ 子表格 ───────────────────────────────────────┐ │ │ │ 子任务(移盘) │ 状态 │ 光盘ID │ 光驱ID │ 错误信息 │ │ │ │ 子任务(刻录) │ 状态 │ 光盘ID │ 光驱ID │ 错误信息 │ │ │ │ 子任务(回迁) │ 状态 │ 光盘ID │ 光驱ID │ 错误信息 │ │ │ └─────────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────┘ ``` - 根节点列表只查 `level=1` 的数据 - 展开行时按 `parentId` 动态加载子节点 - 支持按状态(INIT/READY/RUNNING/COMPLETED/FAILED)和类型(BURN/MIGRATE/CLEANUP)筛选 --- ## 🧩 关键代码位置 | 关注点 | 文件 | |--------|------| | 任务聚合状态机 | `device-job-domain/.../aggregate/Task.java` | | 领域事件定义 | `device-job-domain/.../event/burn/*.java` | | 命令定义 | `device-job-domain/.../command/burn/*.java` | | Saga 编排 | `device-job-application/.../saga/TaskSaga.java` | | 流程策略 | `device-job-application/.../strategy/impl/BurnTaskFlowStrategy.java` | | CQRS 读模型同步 | `device-job-infrastructure/.../listener/TaskReadSyncListener.java` | | 设备执行适配器 | `device-job-infrastructure/.../adapter/DeviceExecutionAdapter.java` | | 设备驱动抽象 | `device-job-infrastructure/.../device/DeviceOperator.java` | | 读模型 REST API | `device-job-trigger/.../http/TaskGroupController.java` | | EventBus 配置 | `device-job-app/.../config/GuavaConfig.java` | | 前端任务页面 | `device-job-ui/src/views/task/index.vue` | | 前端 API 定义 | `device-job-ui/src/api/modules/system.ts` | --- ## 🔧 快速开始 ```bash # 1. 创建数据库 mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS device_job DEFAULT CHARSET utf8mb4;" # 2. 启动后端 — Flyway 自动建表(无需手动执行 SQL) cd device-job-app mvn spring-boot:run -Dspring-boot.run.profiles=dev # 启动前端 cd device-job-ui npm install npm run dev # 访问 http://localhost:8848 ``` ### 环境配置 - 后端端口:`8081` - 前端端口:`8848` - 前端代理:`/api/device` → `localhost:8081`,`/api/admin` → `localhost:80` - 数据库:`device_job`,用户 `root`,密码 `123456` --- ## 📐 架构原则 1. **领域层零依赖** — domain 层不依赖任何框架或基础设施 2. **端口适配器** — 所有外部依赖通过端口接口反转 3. **事件驱动** — 聚合状态变更通过事件通知下游,不直接调用 4. **CQRS 读写分离** — 命令走 `Task` 聚合 + `task` 表,查询走 `task_read` 读模型 5. **Saga 编排** — 跨步骤流程由 Saga 统一编排,保证最终一致性