897 lines
46 KiB
Markdown
897 lines
46 KiB
Markdown
# agent约束.md
|
||
|
||
> **内容范围:** 仅存放架构约定、代码规范、框架级安全原则等**非业务**内容。不写入具体设备、工序、场景等业务细节(业务内容归 `agent业务.md` 管理)。无新增架构/规范层面的发现,不强行写入。
|
||
|
||
本文档禁止agent更改任何内容,禁止人工更改。同架构新项目复刻开发的固定规则。AI 不猜测「能不能做」,只执行「固定约定 + 可修改范围」。人工提前确认所有基础基准文件后交付 AI,AI 仅读取使用、禁止私自修改基准文件内容。
|
||
|
||
---
|
||
|
||
## 一、新项目启动前,人工必须确认的基础基准文件
|
||
|
||
| # | 文件 | 说明 | 新项目行为 |
|
||
|---|------|------|-----------|
|
||
| 1 | `doc/signals.sql` | PLC 全部点位、寄存器地址、数据类型、读写方向 | 人工替换为新设备信号表 |
|
||
| 2 | `schema/*.go` | Ent ORM 表结构定义(运行 `go generate` 生成 `ent/` 代码) | 人工修改表结构后运行生成 |
|
||
| 3 | `doc/data.sql` | 数据库种子数据(设备类型、设备实例、槽位、配方、工序步骤) | 人工替换为新产线设备/配方 |
|
||
| 4 | `agent业务.md` | 业务规则文档(工艺步骤、调度规则、闭环) | 人工填写工艺步骤和业务规则 |
|
||
|
||
> 以上 4 项是 AI 的「只读数据源」,AI 不修改、不新增、不删除其中的任何内容。
|
||
> Mock 模式是配置项,每个新项目都保留模拟能力。
|
||
> `agent业务.md`: 1. 标记的说明,agent 要注释到相关代码里;2. 重要业务节点标记清楚:触发点、流向、结果;3. 业务要闭环:如何从 0 启动、多种收尾情况如何处理
|
||
> 数据库表注释很重要,开发人员会完整维护,agent 也要遵守。
|
||
|
||
---
|
||
|
||
## 二、业务文档编写约束(`agent业务.md` 编写规范)
|
||
|
||
### 文档定位
|
||
`agent业务.md` 是**业务人员视角**的文档,描述「产线要做什么」,不描述「代码怎么实现」。
|
||
|
||
**可以写:** 产线设备及工序、触发/完成条件、异常处理、业务规则、正常/收尾流程。
|
||
|
||
**禁止写(SQL/schema 已有,不许重复):** 设备槽位数、IP、信号地址、设备类型码、exchange/batch/dualGrip 字段值、Go 常量名/包名/函数名、工件/槽位状态机。
|
||
|
||
### 编写规则
|
||
1. **OP 是执行单元,非判断单元**:描述单设备上的完整操作,不描述条件分支逻辑。
|
||
2. **条件分支独立标记**:路由判断作为步骤间规则描述,由 `recipe_step.next_step_default` 承载。
|
||
3. **自然语言**:不使用代码中的常量名、表名、字段名、信号名。
|
||
4. **不重复数据**:基准文件中已有的信息不重复声明。
|
||
|
||
---
|
||
|
||
## 三、项目目录结构与包职责(固定,不可变)
|
||
|
||
```
|
||
spherical/
|
||
├── apis/ # API 定义 (.api),go-zero 代码生成源
|
||
├── cmd/ # 程序入口(debug、mockrun、engine_mockrun)
|
||
├── common/ # 通用工具(errorx、httpx、logx、token)
|
||
├── constants/ # 全局常量(槽位状态、工件状态、工序类型)
|
||
├── doc/ # 基准数据文件
|
||
├── etc/ # 配置文件
|
||
├── internal/
|
||
│ ├── action/ # 动作定义(RobotAction + Validate)
|
||
│ ├── alarm/ # 告警服务
|
||
│ ├── config/ # 配置结构体
|
||
│ ├── db/ # 数据库连接(Ent + 原生 SQL)
|
||
│ ├── engine/ # 全配置化流程引擎(ACQUIRE→EXECUTE→WAIT→COMPLETE,纯执行器)
|
||
│ ├── eventbus/ # 事件总线
|
||
│ ├── eventlog/ # 事件日志异步批量落库
|
||
│ ├── handler/ # HTTP Handler(go-zero 生成)
|
||
│ ├── logic/ # HTTP 业务逻辑
|
||
│ ├── middleware/ # HTTP 中间件
|
||
│ ├── mock/ # Mock PLC
|
||
│ ├── plc/ # PLC 连接管理(S7 连接/读写/重连,不含业务逻辑)
|
||
│ ├── plcactions/ # PLC 动作执行器(Req→等Done→复位 闭环)
|
||
│ ├── preload/ # 预加载(信号地址映射、产品类型)
|
||
│ ├── processor/ # 产线核心逻辑
|
||
│ │ ├── actor/ # 设备 Actor(锁保护槽位状态机,不写 DB)
|
||
│ │ ├── eventloop/ # 事件循环(唯一生产状态写路径)
|
||
│ │ ├── scheduler/ # 调度器(三层管道:生成→过滤→排序,只读 DB)
|
||
│ │ ├── steplog/ # 步骤日志
|
||
│ │ └── tool/ # 工具(扫码枪、激光打标)
|
||
│ ├── robot/ # 机器人控制器
|
||
│ ├── sse/ # SSE 推送
|
||
│ ├── svc/ # ServiceContext 根容器
|
||
│ └── upload/ # 文件上传
|
||
├── schema/ # Ent 表结构定义(Go 源文件)
|
||
├── ent/ # Ent 自动生成(禁止手动修改)
|
||
├── spherical.go # Windows 主入口
|
||
└── spherical_default.go # macOS/Linux 主入口
|
||
```
|
||
|
||
---
|
||
|
||
## 四、启动顺序(svc.NewServiceContext 内部,固定)
|
||
|
||
```
|
||
1. 解析配置 conf.MustLoad(yaml) → config.Config
|
||
2. 数据库连接 db.MustNewDB
|
||
3. 预加载数据 preload.Init → 信号地址表、产品类型到内存
|
||
4. 事件总线 eventbus.NewLocalBus()
|
||
5. 告警服务 alarm.NewService
|
||
6. SSE 处理器 sse.NewHandler()
|
||
7. PLC 连接 plc.Build 或 Mock
|
||
8. 机器人管理器 robot.MustNewManager
|
||
9. 设备 Actor BuildActors(ctx, entClient) → map[machineID]MachineActor
|
||
10. 工单处理器 NewJobProcessor(...)
|
||
11. PLC Worker NewPlcWorker(machineActors, plcExecutor)
|
||
12. 事件循环 NewProductionEventLoop(...) ← 产线大脑
|
||
13. SignalRouter BuildProductionLine(...) → 注入 EventLoop 为 sink
|
||
14. 信号轮询 SignalRouter.Run(ctx, 1s) → goroutine
|
||
15. 恢复协调器 NewRecoveryCoordinator(...)
|
||
16. 总线启动 bus.Start(ctx)
|
||
17. 日志写入器 eventLogWriter.Start(ctx)
|
||
18. SSE 桥接 sse.NewEventBridge(bus, sseHandler)
|
||
19. 事件订阅 bus.Subscribe(SCHEDULE_TICK, ...) → EventLoop.SendScheduleTick()
|
||
20. 断电恢复 DBState.RecoverOnStartup(ctx) → 恢复未完成工单
|
||
21. Actor 同步 InitActorsFromDB(ctx) → 内存状态对齐数据库
|
||
22. 事件循环启动 go eventLoop.Run(ctx) → goroutine
|
||
```
|
||
|
||
---
|
||
|
||
## 五、运行时信号流与数据流
|
||
|
||
### 5.1 PLC 信号 → 设备状态变更(完整链路)
|
||
|
||
```
|
||
PLC 硬件信号
|
||
→ SignalRouter(1s 轮询,上升沿检测)
|
||
→ EventLoop.msgCh(串行处理,单一 goroutine)
|
||
├── handleMachineSignal → Actor.MarkSlotDone → 投递 MACHINE_DONE
|
||
├── handleMachineDone → 推进步骤 / 设置 WAITING_UNLOAD
|
||
├── trySchedule → RuntimeSnapshot → Scheduler.ScheduleAll(生成→过滤→排序)
|
||
└── dispatchWorker → PlcWorker.Execute(RobotAction)
|
||
├── Actor 分配/查找槽位
|
||
├── PlcExecutor(写Req→等Done→复位Req→等Done清零)
|
||
└── WorkerResult → EventLoop
|
||
├── Actor.LoadComplete / UnloadComplete(更新内存)
|
||
├── DBState.AdvanceStep(推进工序,持久化)
|
||
└── trySchedule(闭环触发下一轮)
|
||
```
|
||
|
||
### 5.2 API 请求流程
|
||
|
||
```
|
||
前端 HTTP → handler 参数校验 → logic 业务逻辑
|
||
├── 读操作:直接查询 entClient
|
||
└── 写操作:通过 EventLoop 消息 → command_handlers → DBState.xxx()
|
||
```
|
||
|
||
---
|
||
|
||
## 六、数据库严格定义
|
||
|
||
### 6.1 核心约束
|
||
- 表定义源:`schema/*.go`,运行 `go generate` 生成 `ent/`。**禁止手动修改 `ent/`**
|
||
- `equipment_slot` 是设备槽位唯一数据源,写操作必须通过 EventLoop → DBState
|
||
- MachineActor 不直接写 DB,只操作内存状态
|
||
- **数据库字段定义清晰,不可随意改动语义或复用。** 每个字段有明确的业务含义(如 `done_signal_name` 仅用于 SignalRouter 轮询监听,不可随意写入其他信号名),禁止为临时需求借用已有字段,禁止随意扩大字段用途
|
||
|
||
### 6.2 数据库操作分层
|
||
| 层级 | 负责 | 位置 |
|
||
|------|------|------|
|
||
| 只读查询 | 直接使用 `entClient` | handler/logic 层 |
|
||
| 生产状态写入 | 必须通过 EventLoop → DBState | eventloop/dbstate.go |
|
||
| 复杂统计查询 | 原生 SQL + 注释说明用途 | logic 层 |
|
||
|
||
### 6.3 设备建模规则(框架级通用,数据驱动)
|
||
|
||
不区分工站/搬运设备的显式分类字段,由数据驱动,判别优先级从高到低:
|
||
|
||
| 优先级 | 条件 | 含义 | 行为 |
|
||
|--------|------|------|------|
|
||
| 1 | `equipmentType.code = ROBOT` | 搬运设备 | 串行互斥,slotCount 为车载暂存容量,不参与 MachineActor |
|
||
| 2 | 非 ROBOT + `slotCount >= 1` | 工站 | 持有工件,进入 MachineActor 状态机 |
|
||
| 3 | 非 ROBOT + `slotCount = 0` | 未就绪 | 视为退化/未配置 |
|
||
|
||
> 机器人 `slotCount > 0` 但优先级 1 命中,仍为 handler,槽位语义为装载→搬运→卸载(瞬时),由 `robot.Controller` 管理。
|
||
|
||
**信号驱动子类型区分(不存储为字段):** 有 `doneSignalName` → 加工/检测工站;无 → 缓冲工站。
|
||
|
||
**硬件能力字段(必须显式存储):**
|
||
|
||
| 字段 | 适用 | 说明 |
|
||
|------|------|------|
|
||
| `exchange` | 工站 | 支持换料协议:单次到达同时取成品放毛坯 |
|
||
| `batch` | 工站 | 批量完成模式:所有槽位同时报完工 |
|
||
| `dualGrip` | 搬运设备 | 双夹持能力:配合 exchange 单次往返换料 |
|
||
|
||
### 6.4 PLC 信号握手协议(框架级标准)
|
||
|
||
由 `plcactions.PlcExecutor` 实现,3 步时序:
|
||
```
|
||
1. 写参数字节(如位置号、托盘号),非必须
|
||
2. 置位 Req = true(触发 PLC 动作)
|
||
3. 返回(不等待 Done,由 SignalRouter 异步监控)
|
||
```
|
||
- 命名规范:`{设备名}{动作}Req` / `{设备名}{动作}Done` 成对
|
||
- 参数命名:`{设备名}{动作}{参数名}`
|
||
- 并发约束:同一设备 Req 不能并发下发,由 EventLoop 串行化保证
|
||
|
||
#### 6.4.1 PLC 信号硬约束
|
||
|
||
| # | 规则 | 原因 | 违反后果 |
|
||
|---|------|------|---------|
|
||
| 1 | **Go 只写 Req=true,绝不写 Req=false** | PLC 程序自行管理信号复位,Go 写 false 会与 PLC 自身逻辑冲突,干扰 PLC 正常运行 | 信号错乱、PLC 状态异常 |
|
||
| 2 | **任意两次 PLC 写操作之间必须间隔 ≥ 1 秒** | PLC 扫描周期有限,连续写入可能导致后一次覆盖前一次,或 PLC 漏读上升沿 | 信号丢失 |
|
||
| 3 | **所有 done 信号由 SignalRouter 统一异步监控**,不在 executor 中同步阻塞等待 | 同步等待超时后无人收信号,PLC 手动发也收不到;统一监控确保所有信号可追溯 | 信号丢失、排障无日志 |
|
||
| 4 | **每次 PLC 读写必须打印日志**,含信号名、地址、值、成功/失败,无论正常还是异常 | 排障需要完整链路,漏日志等于盲飞 | 排障无依据 |
|
||
| 5 | **禁止在 Go 代码中复位任何 PLC 信号**(包括 Req 和 Done) | PLC 是信号的主人,Go 只能触发,不能复位 | 与 PLC 程序冲突 |
|
||
|
||
### 6.5 调度器(框架级通用)
|
||
|
||
**只读**组件,三层管道:
|
||
```
|
||
活跃Job → TaskGenerator.Generate() → 候选动作
|
||
→ ConstraintFilter.Filter() → 过滤后候选
|
||
→ PolicyEngine.Rank() → 排序后候选
|
||
→ 取第一个 → dispatchWorker
|
||
```
|
||
- 触发时机:定时(60s)、Worker 完成后、信号处理完成后;机器人忙时跳过
|
||
- 一次调度只派遣一个动作,派遣前动态分配目标设备
|
||
- 并发控制:EventLoop 单 goroutine 串行 + robot.Controller sync.Mutex
|
||
|
||
#### 6.5.1 约束过滤器设计原则
|
||
|
||
| # | 规则 | 反模式 |
|
||
|---|------|--------|
|
||
| 1 | **状态输入来自真实系统**:`buildSystemState` 每个字段必须有填充逻辑,禁止仅 `make()` 后空置 | `ActiveExchanges` 空初始化导致 `hasExchangePair` 永远 false |
|
||
| 2 | **降级必须加设备占用守卫**:降级(如 Exchange→Load)必须检查目标设备空闲,空闲才降级,否则 `continue` | 无条件降级导致设备重复塞工件 |
|
||
| 3 | **覆盖三种设备状态**:A.空闲允许降级 B.有料+可配对走原逻辑 C.有料+无配对丢弃 | 提交修改时需在注释中说明三种场景 |
|
||
| 4 | **写入/读取路径格式对齐**:多动作类型走同一写入路径时,注释标出格式对齐点 | `PositionRefId` 格式不一致 |
|
||
| 5 | **过滤假设无状态**:不依赖「前一次已过滤了竞争候选」,`MachineHasJob` 从 DB 实时读 |
|
||
|
||
### 6.6 数据初始化流程
|
||
|
||
```
|
||
1. schema/tools/generate.go → 读取 schema/*.go → 生成 ent/
|
||
2. schema/tools/migrate.go → 创建/更新数据库表结构
|
||
3. schema/tools/data.go signals → 导入 doc/signals.sql
|
||
4. schema/tools/data.go seed → 导入 doc/data.sql
|
||
```
|
||
|
||
| 工具 | 命令 | 输入 | 输出 |
|
||
|------|------|------|------|
|
||
| generate | `go run generate.go` | `schema/*.go` | `ent/` |
|
||
| migrate | `go run migrate.go` | `ent/` + PG | 数据库表结构 |
|
||
| data signals | `go run data.go signals` | `doc/signals.sql` | `signal` 表 |
|
||
| data seed | `go run data.go seed` | `doc/data.sql` | equipment/recipe 等表 |
|
||
|
||
变更 schema 需执行步骤 1→2→3→4;变更 signals.sql 仅需步骤 3;变更 data.sql 仅需步骤 4。
|
||
|
||
---
|
||
|
||
## 七、架构总纲(框架不可变)
|
||
|
||
### 7.1 五大架构原则
|
||
|
||
| # | 原则 | 含义 | 违反后果 |
|
||
|---|------|------|---------|
|
||
| 1 | **单一写路径** | 所有生产状态变更必须经过 EventLoop → DBState → DB | 数据不一致、并发冲突 |
|
||
| 2 | **只读直通** | Scheduler/Logic 直接查询 entClient,读无需经 EventLoop | — |
|
||
| 3 | **信号驱动** | 不预排任务,下一步由 PLC 信号或 Worker 结果事件触发实时决策 | 状态脱节 |
|
||
| 4 | **串行化并发** | EventLoop 单 goroutine 串行,Robot 互斥锁 | 竞态条件 |
|
||
| 5 | **防御式编程** | 外部输入边界校验;函数 error 不许 `_` 忽略 | 隐性失败、panic |
|
||
|
||
### 7.2 编码架构原则
|
||
|
||
| # | 原则 | 含义 |
|
||
|---|------|------|
|
||
| 1 | **类型最小化** | 一个概念一个类型,禁止冗余翻译层 |
|
||
| 2 | **校验前置** | 外部输入边界处校验(零值、nil、空字符串) |
|
||
| 3 | **错误零容忍** | error 必须日志告警或向上抛出 |
|
||
| 4 | **直连不绕路** | 跨包引用直接 import 目标包,不通过中间别名 |
|
||
| 5 | **map 安全访问** | 加锁或单 goroutine,访问前检查 key 存在性 |
|
||
| 6 | **显式结构体优先** | 消息/事件/命令用显式结构体,禁止 `map[string]any` |
|
||
| 7 | **编译期接口验证** | 接口实现必须用 `var _ Interface = (*Impl)(nil)` 做编译期检查,确保编译期即发现接口不匹配 |
|
||
| 8 | **接口路径统一** | 已有接口抽象必须充分使用,禁止并行 switch 分发绕过接口;一个概念只走一条路径 |
|
||
|
||
### 7.3 实体关系
|
||
|
||
| 关系 | 基数 | 约束 |
|
||
|------|------|------|
|
||
| WorkOrder → Job | 1:N | Job 是工单实例化 |
|
||
| Job → Recipe | N:1 | 配方由产品类型决定,运行时不可变 |
|
||
| Recipe → RecipeStep | 1:N(有序) | `next_step_default` 决定流转 |
|
||
| Equipment → EquipmentSlot | 1:N | 1 槽位 = 1 原子资源,同持 1 个 Job |
|
||
| Equipment → MachineActor | 1:1 | 统一实现,Type 适配 |
|
||
| Equipment → PlcExecutor | 1:1 | 设备特有信号握手封装 |
|
||
|
||
### 7.4 写入路径约束
|
||
|
||
| 操作类型 | 路径 | 位置 |
|
||
|----------|------|------|
|
||
| 生产状态写入 | EventLoop → DBState → ent → PG | `eventloop/dbstate.go` |
|
||
| 设备槽位写入 | EventLoop → DBState.SetEquipmentSlot | `eventloop/dbstate.go` |
|
||
| 只读查询 | Logic/Scheduler 直接使用 entClient | `logic/`、`scheduler/` |
|
||
| 事件审计 | EventBus → EventLogWriter → ent(异步批量) | `eventlog/writer.go` |
|
||
|
||
> **Actor 不写 DB,Scheduler 不写 DB。只有 EventLoop 能修改生产状态。**
|
||
|
||
---
|
||
|
||
## 八、代码编写强制规范
|
||
|
||
### 8.1 依赖注入
|
||
- 所有组件通过 `New` 构造函数注入。`New` 只能做字段赋值和必要参数处理,**禁止调用实例方法**(如 `m.Init()`)
|
||
- 禁止 `SetXxx()` 后期绑定;结构体依赖字段一律小写私有
|
||
- 不允许单独的 `Init()` 方法:初始化逻辑内联在 `New` 内
|
||
- **依赖注入参数必须用接口类型**,不用具体类型:`NewXxx(worker HardwareWorker)` ✅,`NewXxx(worker *PlcWorker)` ❌
|
||
- **依赖图必须为纯树形单向**,禁止循环依赖、双向依赖、网状依赖:组件间只能 `svcCtx → A → B`,不允许 `A ↔ B` 互相引用
|
||
|
||
### 8.2 context.Context
|
||
- ctx 仅存放超时、取消信号、TraceID。**禁止 ctx.WithValue 存入常驻对象**
|
||
|
||
### 8.3 命名规范
|
||
- 接收者:单字母;禁止匈牙利命名法(`alarmService` 不写 `alarmSvc`)
|
||
- 导出字段:大写开头,完整单词;布尔变量用完整单词(`exists`/`ok`/`found`,不用 `exist`/`isOk`)
|
||
- PLC 信号:`XxxReq` / `XxxDone` 成对
|
||
|
||
### 8.4 事件订阅
|
||
- 事件订阅、handler 映射、路由注册必须在构造函数内完成,禁止跨文件 `init()` 零散注册
|
||
- **跨切面通知(告警、SSE 推送)必须通过 EventBus 解耦**,禁止组件间直接调用:`bus.Publish(event)` ✅,`alarmService.Notify()` ❌
|
||
|
||
### 8.5 事务
|
||
- 多表状态修改必须开启事务(`entClient.Tx()` 或 DBState 原子方法)
|
||
- `AdvanceStep` 所有 DB 写在同一 `ent.Tx` 内完成;`FinishJob` 提取 `finishJobTx` 方法接受 `*ent.Tx`,避免嵌套事务
|
||
|
||
### 8.6 超时
|
||
- 所有阻塞等待逻辑必须配置 Timeout,禁止无限等待
|
||
|
||
### 8.7 注释规范
|
||
- **常量必须有行内注释**;关键业务函数有文档注释(业务场景、执行流程、参数、返回)
|
||
- 导出结构体字段有行内注释;注释描述影响,不只写「做什么」
|
||
- 禁止无意义注释(函数名已自解释)、禁止过时注释
|
||
|
||
### 8.8 日志规范
|
||
- 消息格式:**中文业务描述 + 英文 key**(如 `"工件上料失败 jobId:%d"`)
|
||
- 关键字段必含:`jobId`、`orderId`、`machineId`、`equipmentName`、`action`、`slotNo`、`stepIndex`、`signal`
|
||
- 同一工件生命周期通过 `jobId` 串联所有日志;错误日志必须含所有上下文字段
|
||
|
||
#### 8.8.1 外部通信日志强制规范
|
||
|
||
**所有与外部系统的通信,无论正常还是异常,必须打印日志。** 禁止只打错误日志、不打正常日志。
|
||
|
||
| 外部系统 | 日志必须包含 |
|
||
|----------|------------|
|
||
| **PLC** | 每次读写:信号名、地址、数据类型、写入值/读取值、成功/失败。写 Req 必须打「开始」日志;收到 Done 必须打「结束」日志 |
|
||
| **MOM** | 每次请求/响应:接口名、请求参数、响应内容、成功/失败 |
|
||
| **AGV** | 每次进线/离场:AGV 编号、接驳台编号、时间、成功/失败 |
|
||
| **立库/WMS** | 每次交互:操作类型、参数、结果、成功/失败 |
|
||
|
||
**日志级别规则:**
|
||
- 正常操作:`Info`
|
||
- 读取失败/网络错误:`Warn`(可重试,非致命)
|
||
- 业务逻辑错误/数据异常:`Error`
|
||
|
||
#### 8.8.2 日志文件存储规范
|
||
|
||
- **按自然天切割**:每天一个日志文件,命名格式 `{基础名}-YYYY-MM-DD.log`(如 `spherical-2026-08-07.log`)
|
||
- **日志文件放独立目录**:日志文件必须放 `log/` 子目录,禁止放在 exe 同级目录
|
||
- 每天内仍按大小(MaxSize)滚动,保留 MaxBackups 个备份
|
||
- 历史日志保留 MaxAge 天,超期自动删除
|
||
- 禁止单文件无限增长
|
||
|
||
### 8.9 Git 规范(AI 辅助开发)
|
||
- AI 只能执行读操作(`status`/`diff`/`log`/`branch`/`show`)
|
||
- 禁止执行写操作(`add`/`commit`/`push`/`rebase`/`reset`/`stash`/`checkout`/`restore`/`clean`)
|
||
- 代码修改只通过 Edit/Write 工具,不通过 Git 回滚
|
||
|
||
### 8.10 值对象规范
|
||
|
||
所有 ID 类型用值对象封装(`internal/processor/types/id_types.go`),禁止裸 `int`/`string`:
|
||
|
||
| 值对象 | 底层 | 校验 | 方法 |
|
||
|--------|------|------|------|
|
||
| `JobID` | `int` | `> 0` | `Valid()`, `Int()`, `String()`, `ParseJobID()` |
|
||
| `OrderID` | `int` | `> 0` | 同上 |
|
||
| `MachineID` | `int` | `> 0` | 同上 |
|
||
| `SlotNo` | `int` | `> 0` | 同上 |
|
||
| `StepID` | `string` | 非空 | `Valid()`, `String()`, `ParseStepID()` |
|
||
| `ProductID` | `int` | `> 0` | 同上 |
|
||
| `WorkpieceType` | `string` | 非空 | 同上 |
|
||
|
||
`Valid()` 校验时机:外部输入边界、DB 写入前、公共方法入口。
|
||
|
||
禁止反模式:裸 `int` 参数、map key 用 `int`、日志直接输出值对象、跨层传递 `int`。
|
||
|
||
### 8.11 Validate 校验函数规范
|
||
- 结构体校验逻辑使用**独立函数** `ValidateXxx()` 或 `ValidateRobotAction()`,**禁止**在结构体上定义 `Validate()` 方法
|
||
- 原因:`Validate()` 方法与接口方法(如 `Validate() error`)容易混淆,独立函数名明确校验对象
|
||
- 校验必须在派单前、DB 写入前等边界处调用
|
||
|
||
---
|
||
|
||
## 九、新项目可修改的范围
|
||
|
||
### 9.1 基准文件(不可修改,AI 只读)
|
||
`doc/signals.sql`、`schema/*.go`、`doc/data.sql`、`doc/agent业务.md`
|
||
|
||
### 9.2 业务逻辑(完全重写)
|
||
`actor/`、`eventloop/command_handlers.go`、`eventloop/candidate_handlers.go`、`scheduler/filter.go`、`scheduler/policy.go`、`scheduler/generator.go`、`plc_worker.go`、`plcactions/executor.go`、`recipe_loader.go`、`robot/`、`alarm/`、`handler/`+`logic/`、`sse/`
|
||
|
||
### 9.3 可微调参数
|
||
轮询周期、消息队列长度、日志批量条数、Mock 清理范围、预加载数据类型
|
||
|
||
---
|
||
|
||
## 十、代码安全要求
|
||
|
||
1. 阻塞调用**必须带超时+ctx取消**
|
||
2. error **不许 `_` 忽略**,必须日志告警或向上抛出
|
||
3. 切片/map 取值前**判空/长度**,下标访问**校验索引**
|
||
4. 多协程共享内存**加锁或单队列串行**
|
||
5. 文件/连接/句柄/通道**defer 关闭**
|
||
6. 外部参数**合法性校验**(零值/null/空字符串)
|
||
7. 循环逻辑**必须有退出条件或最大重试**
|
||
8. 第三方调用/JSON 解析**异常捕获**
|
||
9. 定时任务/异步 goroutine **panic recover**
|
||
10. 批量 DB 操作**加事务或分批**
|
||
11. `mustGetAddr` 等查找函数返回错误**必须日志告警+处理**,不能 panic 导致程序退出;空地址返回前必须校验
|
||
|
||
---
|
||
|
||
## 十一、设备动作安全架构(防撞机 / 防误操作硬性约束)
|
||
|
||
### 11.1 核心原则
|
||
调度层约束过滤器是**软保护**(依赖 DB,有假阳性风险)。**Actor 带锁槽位检查是硬守卫**——所有 PLC 使能方法(Load/Exchange/Unload/AirBlow)在调用 Station 前必须过 Actor 守卫。
|
||
|
||
### 11.2 场景覆盖矩阵
|
||
|
||
| 场景 | 危险指令 | Actor 硬守卫 |
|
||
|------|---------|-------------|
|
||
| 装料到有料设备 | `load_to_machine` | `AllocateSlot` 无空槽即报错 |
|
||
| 空磨床换料 | `exchange_grinder` | `HasEmptySlot()→拒绝` |
|
||
| 缓存台没空位放 | `load_full_check` | `AllocateSlot` 遍历全槽 |
|
||
| 接驳台没货取 | `load_from_dock` | `FindSlotByJobID` + 槽位一致性 |
|
||
| 接驳台有货放成品 | `unload_to_dock` | `Snapshot` 检查目标槽位为空 |
|
||
| 磨床空误下换料 | `exchange_grinder` | `HasEmptySlot()→拒绝` |
|
||
|
||
### 11.3 Actor 守卫实现契约
|
||
|
||
- **装料类**:`a.AllocateSlot(act.JobID)` 带锁遍历空槽,无空槽不下发 PLC
|
||
- **取料类**:`a.FindSlotByJobID(act.JobID)` 确认工件存在 + 槽位一致(双校验),否则拒绝
|
||
- **换料类**:`a.HasEmptySlot()==true` → 拒绝(空设备无工件可换)
|
||
- **放料类**:`a.Snapshot()` 遍历目标槽位,`Status != Empty` → 拒绝(防撞料)
|
||
- **槽位状态比较必须用常量** `string(constants.SlotStatus_Empty)`,禁止 `""` 或字面量
|
||
|
||
### 11.4 新增动作安全检查清单
|
||
|
||
| 检查项 |
|
||
|--------|
|
||
| □ 是否有 Actor 守卫(`w.actors[id]` 访问)? |
|
||
| □ 守卫失败是否 `return error` 阻断后续? |
|
||
| □ 装料=调用 `AllocateSlot` / 取料=调用 `FindSlotByJobID` / 换料=调用 `HasEmptySlot` / 放料=检查目标为空? |
|
||
| □ 槽位比较用 `SlotStatus_Empty` 常量? |
|
||
| □ 覆盖 Actor 不存在的情况(`if a, ok := w.actors[id]; ok`)? |
|
||
| □ 错误信息含设备ID+动作类型? |
|
||
|
||
### 11.5 三层防御
|
||
|
||
```
|
||
调度层(软保护)→ Actor 层(硬守卫,不可绕过)→ PLC 层(信号握手)
|
||
```
|
||
|
||
---
|
||
|
||
## 十二、框架级可排查性原则
|
||
|
||
| # | 原则 | 含义 |
|
||
|---|------|------|
|
||
| 1 | **显式结构体取代 map** | 跨组件数据用显式结构体(`WorkerResultPayload`等),禁止 `map[string]any` |
|
||
| 2 | **带校验的解析类型** | 字符串格式封装为带 `Valid` 的结构体(如 `PositionRef`),禁止零值表示失败 |
|
||
| 3 | **函数单一职责** | 单函数超 80 行必须按动作类型拆分 |
|
||
| 4 | **错误日志含原始输入** | `slog.Error` 必须含触发错误的原始数据 |
|
||
| 5 | **防御性 nil 检查** | map 取值检查 nil;外部输入校验有效性 |
|
||
|
||
---
|
||
|
||
## 十三、框架级安全防护原则
|
||
|
||
### 13.1 宕机风险防护
|
||
|
||
| # | 原则 | 违反后果 |
|
||
|---|------|---------|
|
||
| 1 | 运行时 goroutine 必须 `defer recover()`(EventLoop.Run、dispatchWorker、EventLogWriter.Run、SignalRouter.Run) | 产线崩溃 |
|
||
| 2 | 启动路径 fail-fast 正确(`New`/`RecoverOnStartup`/`InitActorsFromDB` 不要求 recover) | — |
|
||
| 3 | `workerBusy` 标志必须配套超时自动重置(如 5 分钟) | 调度永久阻塞 |
|
||
| 4 | DB 操作必须 `context.WithTimeout` | EventLoop 阻塞 |
|
||
| 5 | `Send()` 必须 `select + default` 非阻塞,满时记告警 | 信号/API 阻塞 |
|
||
| 6 | DB 写 error 必须处理,禁止 `_` 忽略 | 状态不一致 |
|
||
|
||
### 13.2 并发安全
|
||
|
||
| # | 原则 |
|
||
|---|------|
|
||
| 1 | goroutine 启动前提取所有共享数据为局部变量 |
|
||
| 2 | map 取值必用 `v, ok := m[key]` |
|
||
| 3 | 类型断言必带 ok 检查 |
|
||
| 4 | JSON 反序列化 `intFromPayload` 兼容 int/int64/float64 |
|
||
|
||
### 13.3 业务安全
|
||
|
||
| # | 原则 | 违反后果 |
|
||
|---|------|---------|
|
||
| 1 | `handleWorkerResult` 必须幂等(记录已处理 taskID) | 重复推进步骤 |
|
||
| 2 | Exchange LoadJob 失败必须同步处理(挂起或标记重配对) | LoadJob 卡住 |
|
||
| 3 | 槽位分配失败 fail-fast(挂起+报警,禁 fallback 到槽位 1) | 撞料 |
|
||
| 4 | Execute 返回 error 禁止调用 LoadComplete/UnloadComplete | Actor 与硬件不一致 |
|
||
| 5 | Exchange 状态推进在同一事务内完成 | 断电后 LoadJob 卡住 |
|
||
| 6 | 全检台 RequestInspect 用原子标志去重,卸料后重置 | 重复信号 |
|
||
| 7 | 全检台容量从 Actor.SlotCount() 或配置获取,不硬编码 | 配置失效 |
|
||
| 8 | 槽位状态用 `SlotStatus_Empty` 常量比较,禁用 `""` | 空槽误判为非空 |
|
||
|
||
---
|
||
|
||
## 十四、业务流程完整性约束
|
||
|
||
### 14.1 三阶段核心问题
|
||
|
||
| 阶段 | 核心问题 | 解决 |
|
||
|------|---------|------|
|
||
| **开始** | 创建工单 DB 更新但 Actor 未同步 | 创建后触发调度;Actor 守卫降级为辅助检查 |
|
||
| **中间** | 设备状态变更后 Actor 与 DB 不一致 | EventLoop 串行处理,Worker 结果同步 Actor |
|
||
| **结尾** | 工单完成后资源未释放 | 取消时强制释放所有资源 |
|
||
|
||
### 14.2 工单开始阶段
|
||
|
||
创建流程:验证→检查→开事务(创建工单+工件+更新槽位)→提交→发布 SCHEDULE_TICK → SSE 通知。
|
||
|
||
**关键约束:**
|
||
|
||
| # | 约束 | 违反后果 |
|
||
|---|------|---------|
|
||
| 1 | `PositionRefId` 格式必须为 `equipmentId:slotNo` | 解析失败 |
|
||
| 2 | 创建工单后必须发布 `SCHEDULE_TICK` 触发调度 | 工件不启动 |
|
||
| 3 | Actor 守卫支持降级:`!found` 但 `SrcSlot > 0` 时允许执行 + WARN 日志 | 新工单无法启动 |
|
||
| 4 | 接驳台槽位号范围校验 | 越界信号错误 |
|
||
|
||
### 14.3 工单中间阶段
|
||
|
||
| 动作 | 执行前检查 | 执行后同步 | 特殊处理 |
|
||
|------|----------|----------|---------|
|
||
| `load_from_dock` | FindSlotByJobID | LoadComplete | 新工单 Actor 降级 |
|
||
| `load_to_machine` | AllocateSlot | LoadComplete | 失败 fail-fast |
|
||
| `unload_from_machine` | FindSlotByJobID | UnloadComplete | fail-fast |
|
||
| `exchange_grinder` | HasEmptySlot() | 同步卸料+上料 | 空磨床拒绝 |
|
||
| `unload_to_dock` | 检查目标空 | UnloadComplete | 非空拒绝 |
|
||
|
||
磨床换料约束:空→仅 `load_to_machine`;有工件→`exchange_grinder`;最后一件→`unload_from_machine`。
|
||
|
||
### 14.4 工单结尾阶段
|
||
|
||
- **完成**:所有工件 Completed → 释放接驳台+缓存台 → 更新工单状态 → SSE 通知
|
||
- **取消**:标记 Scrapped → 释放所有设备槽位+缓存台 → 更新状态 → 发布报废事件
|
||
|
||
**资源释放约束:**
|
||
- 取消必须释放所有设备槽位(磨床/转台/全检台)+ 缓存台占用
|
||
- 接驳台卸料完成后 `Status=Empty`
|
||
- 工单完成后清理 `jobRuntimes`
|
||
|
||
### 14.5 接驳台槽位索引规范(防 bug)
|
||
|
||
`buildSystemState` 查询 `equipment_slot` 构建 `DockSlots` map,**key 必须用 `d.SlotNo`,禁止用 `d.ID`**(`ID` 是自增主键,多设备混合自增会导致 key 错乱)。
|
||
|
||
---
|
||
|
||
## 十五、数据库字段默认值规范(防坑指南)
|
||
|
||
### 15.0 核心口诀
|
||
|
||
- **Nillable 字段(`*int`)**:判 nil → 判 > 0 → 才使用。三步缺一不可。
|
||
- **非 Nillable 字段**:创建时显式 `SetXxx()`,不依赖 schema 默认值。
|
||
- **枚举字段**:永远用 `constants.Xxx` 常量比较,不用 `""` 或字面量。
|
||
|
||
```
|
||
三层默认值各管各的:
|
||
Go 代码: 零值 (int=0, string="", *int=nil)
|
||
schema: Default() / Optional().Nillable()
|
||
PG: INSERT 结果
|
||
|
||
创建时:不调 SetXxx → schema 默认值生效;调了 → 你设的值生效
|
||
读取时:Nillable → *int (nil=NULL);非 Nillable → int (0=0)
|
||
头号杀手:*int nil 解引用 → 0 → "0" → 解析失败
|
||
二号杀手:忘 SetXxx → 依赖 schema 默认值 → 换库行为变
|
||
三号杀手:枚举 "" 当有效值 → 状态判断全错
|
||
```
|
||
|
||
### 15.1 关键字段三层对照
|
||
|
||
| 字段 | Go 类型 | schema 默认值 | Go 零值 | 易错点 |
|
||
|------|---------|-------------|--------|--------|
|
||
| `status` (Job) | `JobStatus`(string) | `"CREATED"` | `""` | `""` 非有效状态 |
|
||
| `positionRefId` | `string` | `""` | `""` | **禁止写 `"0"`** |
|
||
| `tempSlotNo` | `*int` | NULL | `nil` | **nil 解引用 panic** |
|
||
| `dockNo` | `*int` | NULL | `nil` | 同上 |
|
||
| `priority` | `int` | `2` | `0` | 0=最高优先级,意外抢占 |
|
||
| `currentJobId` (slot) | `*int` | NULL | `nil` | **nil 当 0 → 查询工件ID=0** |
|
||
| `stepType` (recipe_step) | `string` | **无默认值** | `""` | 创建时必设 |
|
||
| `recipeId` (product) | `*int` | NULL | `nil` | nil 解引用 |
|
||
|
||
### 15.2 强制规则
|
||
|
||
| # | 规则 | 违反后果 |
|
||
|---|------|---------|
|
||
| 1 | `*int` 字段使用前必须判 nil + 判 > 0 | panic 或写入 `"0"` |
|
||
| 2 | 禁止将 Nillable 的 Go 零值(0)当业务值 | 解析失败 |
|
||
| 3 | 创建时非零值含义字段必须显式 `SetXxx()` | 换库行为变 |
|
||
| 4 | 枚举字段用 `constants` 常量比较,禁用 `""` | 逻辑错误 |
|
||
| 5 | 新增字段确认三层默认值一致 | 行为不可预测 |
|
||
|
||
### 15.3 安全代码模板
|
||
|
||
```go
|
||
// 模板1:安全读取 Nillable
|
||
func safeInt(val *int) int {
|
||
if val == nil || *val <= 0 { return 0 }
|
||
return *val
|
||
}
|
||
// 模板2:创建时显式 Set 所有字段
|
||
jobCreate := entClient.Job.Create().
|
||
SetStatus(constants.JobStatus_Created). // 显式
|
||
SetPositionType(constants.PositionType_OnDock).
|
||
SetPriority(2).
|
||
SetPositionRefId(validOrEmpty).
|
||
SetVersion(0)
|
||
if dockNo > 0 { jobCreate.SetDockNo(dockNo) } // Nillable 有值才设
|
||
// 模板3:枚举比较用常量
|
||
if job.Status == constants.JobStatus_Created { ... } // ✅
|
||
// if job.Status == "" { ... } // ❌
|
||
// 模板4:安全 positionRefId
|
||
func buildPosRef(machineID, slotNo int) string {
|
||
if machineID <= 0 || slotNo <= 0 { return "" }
|
||
return fmt.Sprintf("%d:%d", machineID, slotNo)
|
||
}
|
||
```
|
||
|
||
### 15.4 创建必设字段清单
|
||
|
||
**Job**: WorkOrderId, ProductTypeId, RecipeID, CurrentStepId, Status, PositionType, PositionRefId, Priority, Version(Nillable: DockNo, DockSlotNo, TempSlotNo 有值才设)
|
||
|
||
**WorkOrder**: ProductTypeId, Quantity, Status, Source, WorkOrderNo
|
||
|
||
**EquipmentSlot**: EquipmentId, SlotNo, Status(Nillable: DockNo 接驳台才设)
|
||
|
||
### 15.5 值对象定义一致性原则
|
||
|
||
值对象注释只在 `id_types.go` 中唯一确定,业务结构体字段注释只能引用,不重新解释。特殊值(如 `0`)必须在值对象注释中说明业务含义。
|
||
|
||
---
|
||
|
||
## 十六、测试约束
|
||
|
||
### 16.1 mockrun 测试框架
|
||
|
||
`cmd/mockrun/` 不依赖真实 PLC,使用 Mock PLC 自动回复信号。
|
||
|
||
```bash
|
||
# 终端1:go run . -f etc/spherical-api.yaml
|
||
# 终端2:go run cmd/mockrun/main.go -debug -scenario round -jobs 12
|
||
```
|
||
|
||
### 16.2 场景覆盖
|
||
|
||
| 场景 | 参数 | 覆盖边界 |
|
||
|------|------|---------|
|
||
| `round` | `-jobs 12` | 圆形件完整流程 |
|
||
| `single_job` | 默认 | 单工件端到端 |
|
||
| `square` | `-jobs 6` | 方形件含转台换向 |
|
||
| `exchange` | `-jobs 4` | 磨床换料 |
|
||
| `cache_reflow` | `-jobs 4` | 全检台满→缓存→回流 |
|
||
| `no_material` | 默认 | 空接驳台→暂停 |
|
||
| `no_space` | `-jobs 4` | 填满→暂停 |
|
||
| `last_piece` | 默认 | 单工件收尾:只卸不换 |
|
||
|
||
覆盖验证:正常流程、换料、收尾、缓存回流、无料/无空间暂停、资源释放、并发安全。
|
||
|
||
### 16.3 验证项(`-verify=true`)
|
||
工单状态 COMPLETED、工件完成数、设备槽位 EMPTY、接驳台成品数量。
|
||
|
||
### 16.4 测试数据要求
|
||
从 `data.sql` 初始化,Mock 自动清空旧数据,每种产品类型至少 1 工单,每工单 ≥ 3 工件,缓存回流 ≥ 8,无空间 ≥ 13。
|
||
|
||
### 16.5 通过标准
|
||
```
|
||
✅ 工单: COMPLETED ✅ 工件: N完成 0报废 ✅ 设备槽位 EMPTY ✅ 接驳台成品到位
|
||
```
|
||
|
||
---
|
||
|
||
## 十七、positionRefId 字段决策
|
||
|
||
- **状态**:`job` 表 `String` 字段,存储如 `"1:42"`;业务已用独立字段解耦,不再依赖解析
|
||
- **决策**:保留,降级为展示/辅助字段(删除成本高、有调试价值、无危害)
|
||
- **规则**:
|
||
1. 它是**只写展示字段**,禁止业务逻辑解析它
|
||
2. 需要位置信息时用 `positionType` + `dockNo`/`dockSlotNo`/`tempSlotNo`
|
||
3. 写入格式 `"设备ID:槽位号"` 或空字符串,**禁止写入 `"0"`**
|
||
|
||
---
|
||
|
||
## 十八、业务并发与设备互斥占用机制
|
||
|
||
### 18.1 双层防护模型
|
||
|
||
```
|
||
内部并发(机器人动作、调度、信号处理)
|
||
└── EventLoop 单 goroutine 串行化处理
|
||
├── workerBusy 原子标志 → 防止并发派发 Worker 动作
|
||
├── Actor sync.Mutex → 保护单个设备槽位状态机
|
||
└── goroutine 启动前快照提取 → 防止共享内存并发读写
|
||
|
||
外部并发(HTTP API、AGV 回调)
|
||
└── equipment.occupied 字段 → 外部入口即时校验
|
||
├── 被占用 → 立即拒绝,不投递消息到 EventLoop
|
||
└── 空闲 → 先标记占用 → 再投递消息
|
||
```
|
||
|
||
**核心原则:EventLoop 串行化处理内部并发;`equipment.occupied` 字段处理外部并发。两者互补,不可互相替代。**
|
||
|
||
### 18.2 equipment 表占用字段
|
||
|
||
```sql
|
||
ALTER TABLE equipment ADD COLUMN occupied BOOLEAN NOT NULL DEFAULT false;
|
||
ALTER TABLE equipment ADD COLUMN occupied_by VARCHAR(20); -- ROBOT / AGV / MANUAL
|
||
ALTER TABLE equipment ADD COLUMN occupied_reason VARCHAR(100);
|
||
ALTER TABLE equipment ADD COLUMN occupied_at TIMESTAMPTZ;
|
||
ALTER TABLE equipment ADD COLUMN occupied_by_operator VARCHAR(100);
|
||
```
|
||
|
||
**仅接驳台需要互斥占用。** 缓存台、磨床、转向台等只有一个机器人操作,EventLoop 串行已保证安全。
|
||
|
||
### 18.3 互斥规则
|
||
|
||
| 当前占用 | 机器人取放 | AGV 进线 | AGV 离场 | 人工操作 |
|
||
|----------|:---:|:---:|:---:|:---:|
|
||
| IDLE | 允许 | 允许 | 允许 | 允许 |
|
||
| ROBOT | — | **拒绝** | 允许 | 拒绝 |
|
||
| AGV | **拒绝** | 拒绝 | 允许 | 拒绝 |
|
||
| MANUAL | **拒绝** | **拒绝** | 允许 | 允许(解锁) |
|
||
|
||
AGV 离场不受任何占用影响。
|
||
|
||
### 18.4 读写约束
|
||
|
||
| 操作 | 路径 | 说明 |
|
||
|------|------|------|
|
||
| 读取(调度器过滤) | `buildSystemState` → `EquipmentOccupied` map | 从 DB 读,仅对接驳台 |
|
||
| 读取(外部入口校验) | HTTP handler 直接查 `entClient.Equipment.Get()` | 投递消息前校验 |
|
||
| 写入(外部入口) | HTTP handler 先写 DB 再投递消息 | 防止投递排队期间的竞态 |
|
||
| 写入(内部) | EventLoop 消息处理中写 DB | 串行化保证安全 |
|
||
| 清除 | EventLoop 消息处理中写 DB | 统一清理 |
|
||
|
||
### 18.5 占用常量
|
||
|
||
```go
|
||
const (
|
||
OccupiedBy_Robot = "ROBOT" // 机器人取放(仅接驳台相关动作)
|
||
OccupiedBy_AGV = "AGV" // AGV 进线
|
||
OccupiedBy_Manual = "MANUAL" // 人工占用(故障排查/维护,仅解锁操作)
|
||
)
|
||
```
|
||
|
||
### 18.6 与现有并发机制的配合
|
||
|
||
| 现有机制 | 配合方式 |
|
||
|----------|---------|
|
||
| EventLoop 串行处理 | 占用字段作为消息处理输入之一 |
|
||
| `workerBusy` 原子标志 | 忙时不调度 + 占用字段额外过滤 |
|
||
| Actor `sync.Mutex` | 保护槽位状态机,与占用字段互不干扰 |
|
||
| `dispatchWorker` goroutine | 快照提取机制不变 |
|
||
|
||
---
|
||
|
||
## 十九、全配置化流程引擎(engine 包)
|
||
|
||
### 19.1 引擎定位
|
||
|
||
`internal/engine/` 是**纯执行器**,不包含任何设备类型或工序知识。所有业务逻辑通过数据库配置驱动。
|
||
|
||
**与 EventLoop 的关系**:EventLoop 保留为消息路由器,负责接收外部消息(HTTP/PLC/Worker)→ 更新 DB 事实 → 调用 Engine.TryAdvanceJob 推进 job 步骤。Engine 不直接处理 HTTP 消息或 PLC 信号,只通过 EventLoop 调用或 SignalRouter 回调触发。
|
||
|
||
### 19.2 引擎核心循环(4 阶段)
|
||
|
||
```
|
||
ACQUIRE(获取资源)→ EXECUTE(发送信号)→ WAIT(等待完成)→ COMPLETE(后置操作)
|
||
```
|
||
|
||
| 阶段 | 读取配置表 | 操作 |
|
||
|------|-----------|------|
|
||
| ACQUIRE | `recipe_step_resource` | SELECT FOR UPDATE 事务原子获取资源(robot/temp_slot 锁、设备槽位;`{dock_slot}` 解析 job 绑定槽位);决策步骤直接进入 DECISION 处理 |
|
||
| EXECUTE | `recipe_step_signal` | 按 sort_order 发送 param/req 信号到 PLC;信号间按 wait_after_ms 等待(毫秒,0=不等待) |
|
||
| WAIT | `recipe_step_signal`(done 角色) | SignalRouter 异步监控 Done 信号,引擎不主动轮询 |
|
||
| COMPLETE | `recipe_step_action` | 释放资源、设置设备槽位状态、发送后置信号、步骤跳转;**步骤切换时死循环防护计数**(见 19.4-4) |
|
||
|
||
### 19.3 引擎文件职责
|
||
|
||
| 文件 | 职责 |
|
||
|------|------|
|
||
| `engine.go` | 核心引擎,TryAdvanceJob 主循环,AlarmService 接口 |
|
||
| `acquirer.go` | 资源获取,SELECT FOR UPDATE 事务,死循环检测+报警 |
|
||
| `executor.go` | 信号发送,按 sort_order 写入 PLC,失败报警 |
|
||
| `completer.go` | 后置操作执行,资源释放/状态设置/跳转,Done 信号匹配 |
|
||
| `decision.go` | 决策步骤处理,按 priority 匹配条件分支 |
|
||
| `db_ops.go` | 数据库操作封装,加载配置、读写资源 |
|
||
| `adapter.go` | I/O 层适配器,桥接引擎与 SignalRouter/PLC |
|
||
| `types.go` | 类型定义,Job/JobStep/StepSignal/StepAction 等 |
|
||
|
||
### 19.4 引擎约束
|
||
|
||
| # | 约束 | 说明 |
|
||
|---|------|------|
|
||
| 1 | 引擎不包含业务知识 | 不知道磨床、接驳台、全检台的区别,只关心资源获取 |
|
||
| 2 | 资源获取原子性 | SELECT FOR UPDATE 事务,全部成功或全部回滚 |
|
||
| 3 | 快照隔离 | job_step 从 recipe 复制,recipe 变更不影响运行中 job |
|
||
| 4 | 死循环防护 | total_step_count 在**步骤切换时**(advanceToNextStep/jumpToStep 事务内)+1,超限(>= max_step_count,默认 200)→ job.status = ERROR + 报警。**资源等待/决策等待不计数**(如缓存台回流、无料暂停属正常阻塞),防止正常等待被误判为死循环 |
|
||
| 5 | 事件驱动触发 | Done 信号到达、资源释放时触发 tryAdvance,非轮询 |
|
||
| 6 | held_by_job_id | 支持同一 job 跨步骤持有资源(如机器人持料过清洗) |
|
||
| 7 | 日志规范 | 所有步骤日志包含 jobId/stepId/stepName/equipmentType,使用 slog 包 |
|
||
| 8 | 报警规范 | 所有不可恢复错误(死循环、PLC 写入失败、DB 写入失败、Done 标记失败)必须创建 alarm 记录 |
|
||
|
||
### 19.5 引擎报警编码
|
||
|
||
| alarmCode | 触发条件 | 级别 |
|
||
|-----------|---------|------|
|
||
| `STEP_LOOP_DETECTED` | 步骤切换计数超限(total_step_count >= max_step_count) | ERROR |
|
||
| `PLC_WRITE_FAILED` | 写入 param/req 信号到 PLC 失败 | ERROR |
|
||
| `DB_WRITE_FAILED` | 数据库写入失败(更新 job_step 状态、事务提交) | ERROR |
|
||
| `DONE_SIGNAL_MARK_FAILED` | 标记 Done 信号失败 | ERROR |
|
||
|
||
### 19.6 引擎测试
|
||
|
||
```bash
|
||
go run cmd/engine_mockrun/main.go -scenario single # 单工件完整流程
|
||
go run cmd/engine_mockrun/main.go -scenario concurrent -jobs 4 # 并发资源竞争
|
||
go run cmd/engine_mockrun/main.go -scenario decision -jobs 8 # 决策分支
|
||
go run cmd/engine_mockrun/main.go -scenario square -jobs 2 # 配油盘流程
|
||
```
|
||
|
||
### 19.7 数据库迁移
|
||
|
||
新引擎表结构通过 `doc/engine_schema.sql` 管理,`data.exe reset-all` 自动执行。配方种子数据在 `doc/new_recipe.sql`。
|
||
|
||
### 19.8 与 EventLoop 的调用关系
|
||
|
||
```
|
||
EventLoop(消息路由器) Engine(纯执行器)
|
||
│ │
|
||
├── 新 Job 创建 ──────────────→ TryAdvanceJob(jobID)
|
||
├── PLC Done 信号 ────────────→ OnDoneSignal(signalName)
|
||
│ └── TryAdvanceJob(affectedJobIDs)
|
||
├── 资源释放事件 ─────────────→ OnResourceReleased(type, key)
|
||
│ └── 唤醒等待资源的 pending job
|
||
└── 调度 Tick ────────────────→ TryAdvanceJob(pendingJobIDs)
|
||
```
|
||
|
||
EventLoop 不再包含业务逻辑(if 磨床/接驳台/全检台),只负责消息路由和 DB 事实更新。
|
||
|
||
### 19.9 洋葱结构(架构分层,依赖单向不可循环)
|
||
|
||
整个系统按"洋葱"分层,**每一层只允许依赖内层,禁止反向依赖或跨层调用**。这一结构是"不能死循环"的架构级保证:依赖单向 → 无循环依赖;写路径唯一 → 无并发写冲突。
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────┐
|
||
│ 第 0 层:输入源(HTTP API / MOM 轮询 / PLC 信号) │ ← 外部世界
|
||
├─────────────────────────────────────────────────────┤
|
||
│ 第 1 层:装配层 svc.NewServiceContext │ ← 依赖注入,只做组装,无业务逻辑
|
||
├─────────────────────────────────────────────────────┤
|
||
│ 第 2 层:业务处理(handler → logic) │ ← 无状态,校验输入 → 投递命令
|
||
├─────────────────────────────────────────────────────┤
|
||
│ 第 3 层:EventLoop(消息路由器 + 唯一写路径) │ ← 串行处理所有消息,更新 DB 事实
|
||
│ · 单 goroutine 消费消息通道(丢满即丢弃+告警) │
|
||
│ · 所有 DB 状态变更必须经过这里 │
|
||
│ · 引擎触发事件(Done/资源释放)回流到这里 │
|
||
├─────────────────────────────────────────────────────┤
|
||
│ 第 4 层:Engine(纯执行器,零业务知识) │ ← ACQUIRE→EXECUTE→WAIT→COMPLETE
|
||
│ · 事务原子性(SELECT FOR UPDATE) │
|
||
│ · 快照隔离(job_step 从 recipe 复制) │
|
||
│ · 死循环防护(步骤切换计数) │
|
||
├─────────────────────────────────────────────────────┤
|
||
│ 第 5 层:DB(唯一事实源) + PLC I/O(PlcExecutor) │ ← 最内层,被所有上层依赖
|
||
└─────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**分层规则(不可违反):**
|
||
1. 第 3 层 EventLoop 是**唯一生产状态写路径**:HTTP/PLC/Worker 消息全部汇入消息通道,串行处理,禁止在引擎之外直接写 job/job_step/equipment_slot 状态。
|
||
2. 第 4 层 Engine 不持有任何设备/工序知识,只读配置子表执行四阶段循环;引擎不直接持有 goplc.Client,I/O 经 `EngineSignalAdapter → PlcExecutor` 安全链路。
|
||
3. 依赖方向严格单向:Handler→EventLoop→Engine→DB/PLC。**禁止** Engine 回调 Handler、禁止 EventLoop 外的模块直接调用引擎内部方法。
|
||
4. 引擎触发外部调度只能通过 `EventTrigger` 接口(事件总线发布 → EventLoop 订阅 → trySchedule),形成闭环但不产生依赖环。
|
||
|
||
**"保证不能死循环"的三道防线:**
|
||
1. **步骤切换计数**:`total_step_count` 在步骤推进/跳转的事务内 +1,超限 → job=ERROR + `STEP_LOOP_DETECTED` 报警(防配置错误导致的 A→B→A→B 循环)。
|
||
2. **等待不计数**:资源获取失败、决策分支无匹配均属正常阻塞等待,不累计步骤数(防全检台满→缓存回流的正常等待被误杀)。
|
||
3. **写路径唯一**:EventLoop 单 goroutine 串行,任何时刻一个 job 只有一个推进上下文,不可能出现两个 goroutine 同时推进同一 job 形成的活锁/重复加工。
|
||
|
||
---
|
||
|
||
## 二十、本地开发环境约束(AI 工具轻量级运行)
|
||
|
||
> 开发机为 macOS。执行任何任务必须优先轻量级运行,防止全量构建/全量测试卡死开发机。
|
||
|
||
1. **禁止全量 `go build`**:本地验证不需要编译整个项目时,不得执行全量 `go build`(本项目依赖重,全量构建会卡死开发机)。
|
||
2. **禁止无差别全量测试**:不得执行 `go test ./...` 全量跑测试;只跑改动相关包的测试(如 `go test ./internal/engine/`)。
|
||
3. **优先轻量验证**:验证改动用 `go run <具体命令>`(只编译该命令的依赖闭包,如 `go run cmd/engine_mockrun/main.go`)或 `go test <指定包>`。
|
||
4. **本地跑不需要构建产物就不构建**:本地开发、回归验证一律 `go run` 直跑;只有打包发布(`build.sh` / `build.bat`)才需要真正构建。
|