28 KiB
工业自动化生产线管控系统 — 详细设计方案
文档信息
- 版本:V0.6(全配置化流程引擎)
- 日期:2026-08-10
- 状态:与代码同步
- 说明:新增全配置化流程引擎(
internal/engine/),引擎作为纯执行器,所有业务逻辑通过数据库配置驱动。详细设计见流程文档.md。
目录
1. 系统架构总览
本系统是一个单体应用,内部按职责分层模块化,打包为单个 Go 可执行文件。
┌──────────────────────────────────────────────────────────┐
│ 人工终端 (React) │
└──────────────────────┬───────────────────────────────────┘
│ SSE / HTTP
┌──────────────────────▼───────────────────────────────────┐
│ go-zero HTTP Server (API层) │
│ ┌──────────┐ ┌───────────┐ ┌──────────────────────┐ │
│ │ 工单管理 │ │ 看板/监控 │ │ 人工交互(暂停/恢复)│ │
│ └────┬─────┘ └─────┬─────┘ └──────────┬───────────┘ │
│ │ │ │ │
│ ┌────▼──────────────▼───────────────────▼────────────┐ │
│ │ 业务逻辑层 │ │
│ │ ┌────────────┐ ┌───────────┐ ┌──────────────┐ │ │
│ │ │ 工单处理器 │ │ 事件循环 │ │ 调度器 │ │ │
│ │ │ (processor) │ │(eventloop)│ │ (scheduler) │ │ │
│ │ └────────────┘ └───────────┘ └──────────────┘ │ │
│ └────────────────────────┬──────────────────────────┘ │
│ │ │
│ ┌────────────────────────▼──────────────────────────┐ │
│ │ 基础设施层 │ │
│ │ ┌──────────┐ ┌──────────┐ ┌────────────────┐ │ │
│ │ │ 事件总线 │ │ PLC 管理 │ │ 机器人管理 │ │ │
│ │ │(eventbus)│ │(plc) │ │(robot) │ │ │
│ │ └──────────┘ └──────────┘ └────────────────┘ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌────────────────┐ │ │
│ │ │ 设备代理 │ │ 信号路由 │ │ SSE 推送 │ │ │
│ │ │ (actor) │ │(signal) │ │(sse) │ │ │
│ │ └──────────┘ └──────────┘ └────────────────┘ │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ PostgreSQL (持久化) │
└──────────────────────────────────────────────────────────┘
分层职责
| 层 | 职责 | 关键约束 |
|---|---|---|
| API 层 | HTTP 入口,请求校验,响应组装 | 不写业务逻辑 |
| 业务逻辑层 | 工单管理、调度决策、事件处理 | 核心状态机,所有决策在此 |
| 基础设施层 | 事件总线、PLC 通信、机器人控制 | 可替换实现,不包含业务逻辑 |
| 设备代理层 | Actor 管理槽位状态,处理设备信号 | 不写数据库,不调度 |
2. 核心组件职责
2.1 事件循环 (EventLoop) — 产线大脑
文件位置: internal/processor/eventloop/
职责:
- 处理所有生产事件:PLC 信号、调度结果、前端指令
- 驱动工件状态迁移(Job 状态机)
- 协调调度器下发任务
- 统一写入数据库(唯一写 DB 的入口)
启动方式: 独立 goroutine,go eventLoop.Run(ctx)
关键行为:
- 接收 PLC 信号 → 更新对应 Actor 槽位状态
- 触发调度器 → 决策下一步动作
- 下发动作指令 → PlcWorker 执行
- 处理执行结果 → 更新 Job 状态
2.2 工单处理器 (JobProcessor) — 订单管家
文件位置: internal/processor/
职责:
- 工单创建(校验料位、生成 Job)
- 工单暂停/恢复
- 暂存台补料策略触发
- 工单完成统计
2.3 调度器 (Scheduler) — 决策者
文件位置: internal/processor/scheduler/
职责:
- 从就绪工件中选择下一个要处理的
- 选择目标设备(约束过滤 + 策略排序)
- 生成搬运任务(取→放)
三层架构:
- 任务生成层 — 根据工件当前步骤生成候选动作
- 约束过滤层 — 排除不可用设备、暂停工单、冲突等
- 策略选择层 — 按优先级排序
2.4 Actor — 设备代理
文件位置: internal/processor/actor/
职责:
- 管理设备槽位状态(占用/空闲)
- 处理 PLC 发来的设备信号
- 不写数据库,不调度
2.5 信号路由 (SignalRouter) — 信号分拣
文件位置: internal/processor/actor/
职责:
- 每秒轮询 PLC 输入寄存器
- 检测信号变化(上升沿/下降沿)
- 根据信号找到对应 Actor,分发处理
2.6 PLC 管理器 (PLC Manager) — 硬件通信
文件位置: internal/plc/
职责:
- 建立与西门子 S1200 的通信连接
- 读写 PLC 寄存器(Modbus TCP / OPC-UA)
- 发送动作指令(取料、放料、换料等)
2.7 事件总线 (EventBus) — 消息通道
文件位置: internal/eventbus/
职责:
- 组件间解耦通信
- 当前实现:
LocalBus(内存 channel)
2.8 机器人管理器 (Robot Manager) — 搬运执行
文件位置: internal/robot/
职责:
- 控制地轨机器人执行取/放动作
- 管理手持工具(扫码枪、激光打标机)
- 任务执行结果回传
3. 启动流程
启动顺序分三个阶段,按依赖关系排列:
阶段一:基础设施初始化
1. PostgreSQL 连接 → ent ORM + 原生 SQL 可用
2. 预加载 → 信号地址表 + 产品类型加载到内存
3. 事件总线创建 → 组件间通信通道(此时未启动,防止事件丢失)
4. 告警服务注册 → 订阅异常事件
6. PLC 管理器创建 → 硬件通信通道建立
7. 机器人管理器创建 → 上下料能力就绪
阶段二:业务组件组装
8. 工艺配方加载 → 各工件型号的加工步骤序列可用
9. 调度器创建 → 决策策略就绪
10. 产线 Actor 构建 → 每个设备对应一个 Actor 管理槽位
11. 工单处理器创建 → 可管理工单全生命周期
12. PLC 动作执行器创建 → 可将动作指令转为 PLC 信号
13. 事件循环创建 → 产线状态机核心(此时未运行)
14. 信号路由绑定事件循环 → PLC 信号可直接驱动状态机
15. 信号轮询启动(goroutine)→ 每秒读取 PLC 信号
阶段三:启动与恢复
16. 事件总线启动 → 之前注册的订阅开始生效
17. 事件日志写入器启动 → 所有事件异步写入数据库
18. 总线全量订阅 → 日志写入 → 审计追溯能力就绪
19. SSE 桥接器创建 → 事件实时推送到前端
20. 断电恢复 → 从数据库读取未完成工单
├─ L1 自动恢复 → CREATED/ON_BUFFER 直接恢复
├─ L2 半自动恢复 → PROCESSING 且 PLC 确认完成
└─ L3 人工确认 → 发布事件通知前端
21. Actor 槽位同步 → 内存状态与 DB 一致
22. 事件循环启动(goroutine)→ 产线状态机开始运行
4. Actor 模型
4.1 什么是 Actor
每个 Actor 对应一台物理设备,管理该设备的所有槽位。Actor 是统一实现(MachineActor),通过 Type 字段适配不同设备类型。Actor 不调度、不写数据库,只做两件事:
- 维护槽位状态(占用/空闲/哪个工件在上面)
- 处理 PLC 发来的信号(加工完成 → 把槽位状态改为 DONE)
Actor 是 mutex 保护的同步状态结构体,由 EventLoop 同步调用,不启动独立 goroutine。
4.2 设备类型与槽位配置
| 设备类型 | 槽位数 | 支持 Exchange | 说明 |
|---|---|---|---|
| GRINDER | 1 | 是 | 单槽位,支持换料(取成品+放毛坯一次完成) |
| TURN_TABLE | 1 | 是 | 单槽位,支持换料,用于工件换向 |
| CACHE | 1 | 否 | 单槽位暂存缓冲,仅由槽位占用状态表达语义 |
| FULL_CHECK | 2 | 否 | 双槽位检测台,OK/NG 双结果信号 |
| AIR_BLOW | 1 | 否 | 单槽位,气吹清洁 |
| DOCK | 84 | 否 | 不建模为 MachineActor,由 PlcExecutor 直接驱动 |
4.3 Actor 槽位状态机
每个 Actor 的槽位经历以下状态:
EMPTY → OCCUPIED → DONE → EMPTY
EMPTY:空闲,可放工件OCCUPIED:有工件,设备正在加工/处理中DONE:处理完成,等待机器人取走
4.4 Actor 与 PlcExecutor 的关系
Actor 只管槽位状态(纯内存),PlcExecutor 管 PLC 信号握手。两者分离:
- SignalRouter 检测到 PLC 完成信号 → 投递 SignalEvent 到 EventLoop
- EventLoop.handleMachineSignal → Actor.MarkSlotDone → 更新槽位状态
- 调度器决定动作 → PlcWorker 直接调用 PlcExecutor 方法执行 PLC 握手
5. 工艺配方系统
5.1 配方结构
每种工件型号(Product Type)对应一份 Recipe(工艺配方),Recipe 包含一组有序的 Step(工序步骤)。
Recipe 表:
recipe {
id // 配方编号,如 "R-A6VM107"
name // 名称
version // 版本号
}
RecipeStep 表:
recipe_step {
step_id // 步骤编号,如 "10", "20"
recipe_id // 所属配方
step_name // 步骤名称
step_index // 步骤序号
step_type // 工序类型(见下表)
resource_type // 设备资源类型
next_step_default // 默认下一步
desc // 步骤描述
}
5.2 工序类型
工序类型(step_type)由 agent业务.md 定义,不同项目可不同。框架支持以下通用工序类型:
| 工序类型 | 含义 | 需要什么资源 |
|---|---|---|
| LOAD | 上料并扫码 | 搬运设备 + 接驳台 |
| BUFFER_STAGE | 暂存台等待 | 暂存台槽位 |
| MACHINING | 加工 | 加工设备 |
| INSPECTION | 检测 | 检测设备 |
| PLACE | 下料 | 搬运设备 + 接驳台 |
具体工序类型和步骤内容见 agent业务.md 和 doc/data.sql。
5.3 配方加载时机
系统启动时从数据库加载所有 Recipe 到内存(RecipeLoader),运行期间不查库。新增配方需要重启或手动刷新。
6. 信号路由系统
6.1 信号流向
PLC 输入寄存器
│
▼
SignalRouter.Run() ─── 每秒轮询
│
▼
检测信号变化(上升沿/下降沿)
│
▼
根据信号找到对应 Actor
│
▼
Actor.HandleSignal() ─── 更新槽位状态,发布事件
│
▼
EventLoop 收到事件 ─── 触发调度器
6.2 信号类型
设备完成信号(PLC → 系统):
每种设备类型可定义自己的完成信号,由 done_signal_name 字段指定。信号按设备类型分组:
| 设备类型 | 完成信号 | 处理方 | 触发效果 |
|---|---|---|---|
| GRINDER | GrinderMachiningDone | MachineActor | 槽位状态 PROCESSING → DONE |
| TURN_TABLE | TurnTableExchangeDone | MachineActor | 槽位状态 PROCESSING → DONE |
| AIR_BLOW | WashCompleteDone | MachineActor | 槽位状态 PROCESSING → DONE |
| FULL_CHECK | FullCheckOK / FullCheckNG | MachineActor | 检测结果分支(OK→继续 / NG→报废) |
请求信号(系统 → PLC):
系统通过 PlcExecutor 向 PLC 发送请求信号,信号命名规范为 {设备}{动作}Req,等待对应的 {设备}{动作}Done 完成信号。
6.3 信号 vs 状态
重要原则:PLC 信号是输入,不是状态源。
- 系统以数据库中的状态为准
- PLC 信号只用来触发状态迁移
- 信号可能丢失或重复,系统通过幂等设计保证正确性
7. 事件系统
7.1 事件总线
当前使用 LocalBus(内存实现),基于 Go channel 的发布/订阅模式。
事件结构:
type Event struct {
ID string // UUID
Type string // 事件类型
Source string // 来源
Timestamp time.Time
Payload map[string]interface{}
}
7.2 事件类型
| 事件类型 | 发布者 | 订阅者 | 作用 |
|---|---|---|---|
| 设备信号事件 | SignalRouter | EventLoop | 驱动状态机 |
| 工单创建事件 | 前端 API | JobProcessor | 开始生产 |
| 槽位状态变更 | Actor | EventLoop | 触发调度 |
| 调度任务下发 | EventLoop | PlcWorker | 执行动作 |
| 告警事件 | 各组件 | AlarmService | 通知 |
| 人工确认事件 | RecoveryCoordinator | 前端 | 断电恢复 |
7.3 事件订阅者
当前所有订阅者:
- EventLoop — 订阅 PLC 信号和调度结果
- AlarmService — 订阅告警事件
- EventLogWriter — 订阅所有事件,写入数据库审计
- SSEBridge — 订阅所有事件,推送到前端看板
7.4 启动顺序保证
事件总线先创建、后启动:
- 创建总线(
NewLocalBus)— 此时可以订阅,但不分发事件 - 各组件注册订阅
- 启动总线(
bus.Start)— 开始分发事件
防止"事件发了但没人处理"的竞态条件。
8. 调度器
8.1 调度触发
事件循环收到"槽位状态变化"事件后,触发调度器:
- 工件变为
ON_BUFFER(暂存台上就绪) - 工件变为
WAITING_UNLOAD(设备加工完成待取走)
8.2 决策过程
输入:就绪工件列表
│
▼
任务生成层 ─── 根据工件当前步骤,生成候选动作
│
▼
约束过滤层 ─── 排除不可用的设备和冲突
│
▼
策略选择层 ─── 按优先级排序,选择最优方案
│
▼
输出:搬运任务(从哪取,放到哪)
8.3 优先级策略
- 紧急转移 — WAITING_UNLOAD 的工件必须尽快取走,否则堵住设备
- 成组装卸 — 清洗机需要凑够 2 件才关门
- 设备上料 — 给空闲设备上料
- 批量补料 — 暂存台空位 > 4 时触发
- 手持作业 — 扫码、打标
- 普通搬运 — 常规的暂存台 ↔ 设备搬运
9. 数据流与写入路径
9.1 写入路径
事件源(PLC 信号 / 前端指令 / 调度结果)
│
▼
EventLoop 处理事件
│
├──→ 更新 Actor 内存状态
│
├──→ 触发调度器
│
└──→ 写入数据库(唯一写 DB 入口)
关键约束: EventLoop 是唯一写数据库的入口。Actor 不能直接操作数据库。
9.2 数据一致性
EventLoop 处理事件
│
├──1. 更新 PostgreSQL(持久化)
│
└──2. 发布事件到总线(通知其他组件)
先写数据库,后发事件。如果写库失败,事件不发布。
9.3 事件日志
所有总线事件通过 EventLogWriter 异步写入 event_log 表:
- 用于审计追溯
- 用于断电恢复重放
10. 断电恢复
10.1 恢复分级
| 级别 | 条件 | 处理方式 |
|---|---|---|
| L1 自动 | 工件在接驳台(CREATED)、暂存台(ON_BUFFER)、等待资源 | 直接恢复,无需人工 |
| L2 半自动 | 设备加工中(PROCESSING),PLC 确认完成 | 转为 WAITING_UNLOAD,自动恢复 |
| L3 人工确认 | 机器人操作中(IN_HANDLING)、关门中、位置不明 | 发布人工确认事件,前端通知操作员 |
10.2 恢复流程
1. 从数据库读取所有未完成的 Job 和 Task
2. 按分级分类
3. L1 → 直接恢复状态
4. L2 → 查询 PLC 确认设备状态,恢复
5. L3 → 发布 ManualActionCreatedEvent 到总线
6. 前端收到 → 弹出人工确认对话框
7. 操作员确认后 → 恢复 Job
10.3 事件重放
从 event_log 表按顺序重放事件,版本号和幂等性保证正确性:
- 版本号校验:事件版本必须等于当前实体版本
- 幂等:重复事件(相同
last_event_id)直接跳过
11. 数据库设计
11.1 主数据表
| 表名 | 用途 | 备注 |
|---|---|---|
product_type |
产品类型定义 | 关联 Recipe |
recipe |
工艺配方 | 静态配置 |
recipe_step |
配方步骤 | 步骤类型、资源类型、默认下一步 |
equipment |
设备定义 | 来自 PLC 配置 |
equipment_slot |
设备槽位(运行时) | 唯一槽位真相源,包含 dock_no 区分左右托盘 |
11.2 业务数据表
| 表名 | 用途 | 备注 |
|---|---|---|
work_order |
工单 | 关联产品类型、数量、状态 |
job |
工件实例 | 当前步骤、状态、位置、版本 |
job_step_instance |
工序执行记录 | 每次工序的执行记录 |
task |
搬运任务 | 机器人执行的任务记录 |
event_log |
事件日志 | 审计和恢复用 |
11.3 关键设计
equipment_slot是槽位状态的唯一真相源,包含status、current_job_id、occupied_at等字段job表使用version字段实现乐观锁,防止并发冲突job表使用last_event_id字段实现幂等
12. PLC 信号映射
12.1 信号模型
PLC 信号分为两类,全部定义在 doc/signals.sql 中:
状态信号(PLC → 系统):
PLC 向系统上报设备状态变化,由 SignalRouter 轮询检测上升沿后投递给 EventLoop。
指令信号(系统 → PLC):
系统向 PLC 下发动作指令,由 PlcExecutor 按握手协议执行(写 Req → 等 Done → 复位 Req)。
具体信号地址和映射关系见 doc/signals.sql,不同项目替换此文件即可。
12.2 信号握手协议
所有 PLC 动作遵循统一的四步握手协议:
1. 写参数字节(如位置号、托盘号),非必须
2. 置位 Req 信号 = true,触发 PLC 执行动作
3. 轮询等待 Done 信号 = true(上升沿检测,间隔 100ms,超时 30s)
4. 复位 Req 信号 = false
5. 等待 Done 信号 = false(PLC 看到 Req 为 false 后复位 Done,握手完成)
信号命名规范:{设备名}{动作}Req / {设备名}{动作}Done 成对出现。
13. 关键技术选型
| 组件 | 技术 | 选型理由 |
|---|---|---|
| 后端语言 | Go 1.23.4 | 并发性能好,编译快,单二进制部署 |
| 框架 | go-zero v1.3.8 | 轻量级,支持 HTTP/API 代码生成 |
| ORM | ent v0.14.5 | 类型安全,代码生成 |
| 数据库 | PostgreSQL | 稳定可靠,支持 JSONB |
| Token 存储 | 内存(InMemoryTokenStore) | 单进程部署,无需外部依赖 |
| PLC 通信 | Snap7(西门子 S7 协议) | 工业标准协议 |
| 前端 | React 19 + Semi Design | 企业级 UI 组件库 |
14. 已知问题与待办
14.1 GrinderMachiningDone 信号初始化
doc/signals.sql 中已定义该信号,但需执行 go run data.go signals 导入到数据库。
14.2 RecipeLoader 调用方
RecipeLoader 提供按产品类型加载工艺路线的缓存能力,目前仅用于 cmd/mockrun 调试工具。核心业务路径(EventLoop / DBState)直接通过 entClient.RecipeStep.Query() 查库。
15. 安全防护架构
15.1 宕机风险防护
事件循环是产线的核心状态机,一旦崩溃将导致生产中断。系统采用多层次防护机制:
┌──────────────────────────────────────────────────────┐
│ 应用层防护 │
│ ├── panic recover(EventLoop.Run + goroutine) │
│ ├── workerBusy 超时保护(5分钟自动重置) │
│ └── 通道背压保护(非阻塞发送 + 告警) │
├──────────────────────────────────────────────────────┤
│ 业务层防护 │
│ ├── WorkerResult 幂等性(taskID 去重) │
│ ├── Exchange LoadJob 失败同步处理 │
│ ├── 槽位分配 fail-fast(不 fallback 到槽位1) │
│ └── 全检台 RequestInspect 去重 │
├──────────────────────────────────────────────────────┤
│ 数据层防护 │
│ ├── 数据库查询超时(context.WithTimeout) │
│ ├── DB 写操作 error 必须处理 │
│ ├── Exchange 原子化操作 │
│ └── 断电恢复数据一致性校验 │
└──────────────────────────────────────────────────────┘
15.2 并发安全设计
| 场景 | 风险 | 防护措施 |
|---|---|---|
| goroutine 访问共享 map | concurrent map read/write |
启动前提取局部变量传入 |
| 类型断言失败 | panic | 带 ok 检查的类型断言 |
| JSON 反序列化类型不匹配 | panic | intFromPayload 兼容 int/int64/float64 |
| map key 不存在 | 零值静默使用 | v, ok := m[key] 模式 |
15.3 待修复的安全问题
| 优先级 | 问题 | 位置 | 修复方案 |
|---|---|---|---|
| P0 | EventLoop.Run() 缺少 panic recover | loop.go:274-300 |
添加 defer recover |
| P0 | dispatchWorker goroutine 缺少 recover | worker_dispatch.go:223 |
添加 defer recover |
| P0 | workerBusy 缺少超时保护 | loop.go:64 |
添加 workerStartedAt + 超时检查 |
| P0 | snap 空指针访问 | worker_dispatch.go:79 |
添加 nil 检查 |
| P1 | DB 查询缺少超时 | scheduler_bridge.go:47 |
添加 context.WithTimeout |
| P1 | handleWorkerResult 缺少幂等性 | worker_dispatch.go:189-220 |
添加 taskID 去重 |
| P1 | Exchange LoadJob 失败未处理 | worker_dispatch.go:428-462 |
检查 LoadJobID > 0 |
| P1 | 槽位分配失败 fallback 到槽位1 | worker_dispatch.go:319-321 |
改为 fail-fast |
| P2 | 全检台 RequestInspect 重复触发 | worker_dispatch.go:471-507 |
添加原子标志去重 |
| P2 | 全检台容量硬编码为6 | worker_dispatch.go:483 |
从 Actor SlotCount 获取 |
| P2 | msgCh 通道满时阻塞 | loop.go:199 |
改为 select + default |
16. 代码结构优化建议
16.1 函数拆分
| 函数 | 当前行数 | 问题 | 拆分建议 |
|---|---|---|---|
dispatchWorker |
~130 行 | 超过 80 行约束 | 拆分为 fillActionSlots、executeAndSyncActor、buildWorkerResultMessage |
trySchedule |
~200 行 | 承担 5 个职责 | 拆分为 cleanupExpiredSnapshots、refreshRuntimeSnapshots、buildJobViews、buildSystemState、dispatchCandidates |
buildSystemState |
~150 行 | 承担 6 个子逻辑 | 拆分为 buildMachineBusyState、buildMachineSlotState、buildCacheSlotState、buildDockSlotState、buildOrderState、buildExchangePairState |
handleMachineDone |
~70 行 | 嵌套过深 | 拆分为 handleInspectionResult、recordCNCTime、advanceJobStep |
16.2 代码质量改进
| 问题 | 位置 | 改进方案 |
|---|---|---|
| Actor 查找重复 5 次 | worker_dispatch.go:102-144 |
分支前一次取出 Actor |
| handleWorkerSuccess 分支条件风格不一致 | worker_dispatch.go:242-264 |
统一为 switch payload.Action |
| tryRequestFullCheckInspect 嵌套过深 | worker_dispatch.go:486-497 |
拆为 isFullCheckFull、isLastPieceOfOrder |
| handleExchangeLoadJob 混合两条路径 | worker_dispatch.go:389-426 |
拆为 handleExchangeLoadJobForOperation、handleExchangeLoadJobForAdvance |
| tryRequestFullCheckInspect 文件归属不当 | worker_dispatch.go:471-507 |
移到 candidate_handlers.go 或新建 fullcheck_handler.go |
| success 判断逻辑分散 | worker_dispatch.go:203 |
在 types.go 增加 func (t MessageType) IsSuccess() bool |
| UpdateOneID 影响行数被忽略 | 多处 | 检查 n == 0 时 slog.Warn |
17. BuildProductionLine 详细流程
BuildProductionLine 是产线初始化的核心入口,位于 internal/processor/factory.go。
执行步骤
1. buildSignalMaps(ctx, entClient)
├── 查询 equipment 表,读取 done_signal_name 字段
├── 调用 preload.GetMAddress(signalName) 查找 PLC 地址
├── 成功 → 写入 doneSignals[equipmentID] = PLC地址
└── 失败 → slog.Warn
2. BuildActors(ctx, entClient)
├── 查询 equipment 表,遍历每台设备
├── NewMachineActor(cfg) → 创建带锁状态结构体
└── machineActors[eq.ID] = actor
3. NewSignalRouter(plc) → 创建信号路由器
├── 遍历 equipment 表,对每台设备注册完成信号
└── signalRouter.Watch(eq.ID, doneSignals[eq.ID])
4. 返回 ProductionLine{Actors, SignalRouter}
关键设计决策
| 决策 | 说明 |
|---|---|
| Actor 不启动 goroutine | Actor 是纯状态结构体,由 EventLoop 同步调用 |
| 设备 PLC 握手由 PlcExecutor 统一管理 | 不同设备类型的 PLC 操作通过 PlcExecutor 方法实现,PlcWorker 直接调用,无中间 Station 抽象 |
| SignalRouter 在 Build 时不绑定 sink | EventLoop 尚未创建,由 service_context.go 后续绑定 |
| 不在此处写 DB | 只读 equipment 表,所有写入由 EventLoop 统一完成 |
本方案与代码保持同步,修改代码前请先更新此文档。