Files
bj_power/bj_power_mes/agent约束.md
T

46 KiB
Raw Blame History

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 Handlergo-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 硬件信号
  → SignalRouter1s 轮询,上升沿检测)
    → 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 不写 DBScheduler 不写 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"
  • 关键字段必含:jobIdorderIdmachineIdequipmentNameactionslotNostepIndexsignal
  • 同一工件生命周期通过 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.sqlschema/*.godoc/data.sqldoc/agent业务.md

9.2 业务逻辑(完全重写)

actor/eventloop/command_handlers.goeventloop/candidate_handlers.goscheduler/filter.goscheduler/policy.goscheduler/generator.goplc_worker.goplcactions/executor.gorecipe_loader.gorobot/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 守卫支持降级:!foundSrcSlot > 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 mapkey 必须用 d.SlotNo,禁止用 d.IDID 是自增主键,多设备混合自增会导致 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 安全代码模板

// 模板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, VersionNillable: DockNo, DockSlotNo, TempSlotNo 有值才设)

WorkOrder: ProductTypeId, Quantity, Status, Source, WorkOrderNo

EquipmentSlot: EquipmentId, SlotNo, StatusNillable: DockNo 接驳台才设)

15.5 值对象定义一致性原则

值对象注释只在 id_types.go 中唯一确定,业务结构体字段注释只能引用,不重新解释。特殊值(如 0)必须在值对象注释中说明业务含义。


十六、测试约束

16.1 mockrun 测试框架

cmd/mockrun/ 不依赖真实 PLC,使用 Mock PLC 自动回复信号。

# 终端1go run . -f etc/spherical-api.yaml
# 终端2go 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 字段决策

  • 状态jobString 字段,存储如 "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 表占用字段

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 读写约束

操作 路径 说明
读取(调度器过滤) buildSystemStateEquipmentOccupied map 从 DB 读,仅对接驳台
读取(外部入口校验) HTTP handler 直接查 entClient.Equipment.Get() 投递消息前校验
写入(外部入口) HTTP handler 先写 DB 再投递消息 防止投递排队期间的竞态
写入(内部) EventLoop 消息处理中写 DB 串行化保证安全
清除 EventLoop 消息处理中写 DB 统一清理

18.5 占用常量

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_signaldone 角色) 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 引擎测试

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/OPlcExecutor     │  ← 最内层,被所有上层依赖
└─────────────────────────────────────────────────────┘

分层规则(不可违反):

  1. 第 3 层 EventLoop 是唯一生产状态写路径HTTP/PLC/Worker 消息全部汇入消息通道,串行处理,禁止在引擎之外直接写 job/job_step/equipment_slot 状态。
  2. 第 4 层 Engine 不持有任何设备/工序知识,只读配置子表执行四阶段循环;引擎不直接持有 goplc.ClientI/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)才需要真正构建。