Files
bj_power/bj_power_mes/internal/svc/service_context.go
T
SunYF a2afd2f6dc feat: 五项目业务实现并对接完成
- MES(B): 工单/BOM/备料/工序字典/PLC下发/拧紧/扫码报工/半成品/AGV/追溯逻辑,WMS与海康RCS客户端,看板Redis缓存API(概览/设备/进度/报警/趋势)+SSE
- WMS(C): JWT滑动续签、Excel导入、盘点、内部API、种子数据、独立Postgres配置
- WMS客户端(E): Go网关8891反向代理+内嵌Vue3十页
- 工位终端(D): SQLite本地缓存+模拟拧紧源+内嵌Vue3页面
- Dashboard(A): 看板数据改接MES内部缓存API,SSE实时刷新+vite代理
- 清理各项目球形磨遗留代码,新增部署手册.md
2026-08-27 19:50:57 +08:00

290 lines
8.5 KiB
Go

package svc
import (
"context"
"database/sql"
"fmt"
"log/slog"
"time"
"bj_power_mes/constants"
"bj_power_mes/ent"
"bj_power_mes/internal/alarm"
"bj_power_mes/internal/config"
"bj_power_mes/internal/db"
"bj_power_mes/internal/eventbus"
"bj_power_mes/internal/eventlog"
"bj_power_mes/internal/mock"
"bj_power_mes/internal/plc"
"bj_power_mes/internal/preload"
"bj_power_mes/internal/processor"
"bj_power_mes/internal/processor/eventloop"
"bj_power_mes/internal/processor/scheduler"
"bj_power_mes/internal/processor/tool"
"bj_power_mes/internal/robot"
"bj_power_mes/internal/sse"
"bj_power_mes/internal/wmsstock"
"bj_power_mes/internal/hikrcs"
"bjhardman.cn/bjhardman/mom"
"github.com/zeromicro/go-zero/core/stores/redis"
)
type ServiceContext struct {
Config config.Config
RedisClient *redis.Redis
EntClient *ent.Client
DB *sql.DB
SSEHandler *sse.Handler
PLCManager *plc.Manager
RobotManager *robot.Manager
RecipeLoader *processor.RecipeLoader
OrderProcessor processor.OrderProcessorInterface
EventLoop *eventloop.ProductionEventLoop
EventBus eventbus.Bus
EventLogWriter *eventlog.EventLogWriter
SSEBridge *sse.EventBridge
RecoveryCoordinator *eventloop.RecoveryCoordinator
Mom *mom.Mom
EquipUploader *processor.EquipUploadService
WmsClient *wmsstock.Client
AgvClient *hikrcs.Client
}
func NewServiceContext(c config.Config) *ServiceContext {
rc := redis.MustNewRedis(c.Redis)
entClient, dbConn := db.MustNewDB(c.Database)
//tools.EnsureDataInitialized(context.Background(), entClient)
preload.Init(entClient)
preload.InitProductTypeCategories(entClient)
// Q9 种子数据:12 道工序字典(表为空时才写入)
preload.SeedProcessDict(entClient)
// 事件总线:始终 LocalBus(单进程模式,不再支持 RedisBus)
bus := eventbus.NewLocalBus()
slog.Info("svc: using LocalBus for event bus")
alarmSvc := alarm.NewService(entClient, bus)
sseHandler := sse.NewHandler()
var plcManager *plc.Manager
if c.Mock.Enable {
plcManager = plc.NewManager(mock.NewMockPLC())
} else {
plcManager = plc.Build(c.Plc, bus)
}
// Mock 模式:清空旧数据,避免恢复逻辑干扰
if c.Mock.MockRun {
ctx := context.Background()
entClient.Job.Delete().ExecX(ctx)
entClient.WorkOrder.Delete().ExecX(ctx)
slots, _ := entClient.EquipmentSlot.Query().All(ctx)
for _, s := range slots {
entClient.EquipmentSlot.UpdateOneID(s.ID).
SetStatus(constants.SlotStatus_Empty).
ClearCurrentJobId().
ClearOccupiedAt().
ExecX(ctx)
}
dockSlots := entClient.DockSlot.Query().AllX(ctx)
for _, d := range dockSlots {
entClient.DockSlot.UpdateOneID(d.ID).
SetStatus(0).
ClearCurrentJobId().
ClearWorkOrderId().
ClearWorkpieceTypeId().
ExecX(ctx)
}
slog.Info("svc: mock mode, cleared old data")
}
robotManager := robot.MustNewManager(entClient, plcManager, sseHandler, alarmSvc)
robotCtrl, robotExist := robotManager.GetRobot()
if !robotExist {
slog.Warn("svc: robot not available, stations will run without robot actions")
}
dbState := eventloop.NewDBState(entClient)
scanner := tool.NewScannerTool(robotManager)
marker := tool.NewMarkerTool(robotManager)
recipeLoader := processor.NewRecipeLoader(entClient)
sched := scheduler.DefaultScheduler()
// 提前声明供 Actor 的 OnDone/OnInspect 闭包捕获(EventLoop 随后创建)
var eventLoop *eventloop.ProductionEventLoop
line, err := processor.BuildProductionLine(context.Background(), entClient, dbState, plcManager.GetPlc(), robotCtrl,
func(machineID, slotNo, jobID int) {
if eventLoop != nil {
eventLoop.Send(eventloop.EventLoopMessage{
ID: fmt.Sprintf("actor-done-%d-%d", machineID, time.Now().UnixNano()),
Type: eventloop.EvtMachineDone,
Payload: map[string]any{
"machineId": machineID,
"slotNo": slotNo,
"jobId": jobID,
},
})
}
},
func(machineID int, pass bool) {
if eventLoop != nil {
eventLoop.Send(eventloop.EventLoopMessage{
ID: fmt.Sprintf("actor-insp-%d-%d", machineID, time.Now().UnixNano()),
Type: eventloop.EvtMachineDone,
Payload: map[string]any{
"machineId": machineID,
"inspectionPass": pass,
},
})
}
},
func(machineID int) {
if eventLoop != nil {
eventLoop.SendScheduleTick()
}
},
sseHandler,
)
if err != nil {
slog.Error("svc: build production line failed", "error", err)
return nil
}
orderProcessor := processor.NewJobProcessor(
entClient, line.Actors, bus, scanner, marker, robotCtrl, robotManager, line.TempStore,
)
worker := processor.NewRobotWorker(entClient, robotCtrl, line.Actors, line.Stations, scanner, marker)
// MOM 客户端初始化(必须在 EventLoop 构造之前)
var momClient *mom.Mom
if c.Mom.Enable {
var err error
momClient, err = mom.New(c.Mom.MomConf)
if err != nil {
slog.Error("svc: mom init failed", "error", err)
}
}
// DockStateReader adapter
var dockReader eventloop.DockStateReader
if robotCtrl != nil {
dockReader = dockStateAdapter{ctrl: robotCtrl}
}
momBiz := eventloop.MomBizConfig{
InstoreType: c.Mom.InstoreType,
}
eventLoop = eventloop.NewProductionEventLoop(
entClient, worker, line.Actors, line.TempStore, sched,
bus, orderProcessor, orderProcessor.GetReplenisher(),
plcManager.GetPlc(),
momClient, c.Mom.MomConf, momBiz, dockReader,
)
eventLoop.SetOnDockChange(func() {
sseHandler.Emit(sse.EventDockDataUpdate, "{}")
})
orderProcessor.SetEventLoop(eventLoop)
go line.SignalRouter.Run(context.Background(), 1*time.Second)
recoveryCoordinator := eventloop.NewRecoveryCoordinator(entClient, bus, orderProcessor)
if err := bus.Start(context.Background()); err != nil {
slog.Error("svc: bus start failed", "error", err)
}
// 事件日志写入器
eventLogWriter := eventlog.NewEventLogWriter(entClient, 20, 1000)
eventLogWriter.Start(context.Background())
bus.SubscribeAll(func(ctx context.Context, evt eventbus.Event) error {
eventLogWriter.Write(evt)
return nil
})
// SSE 桥接器
sseBridge := sse.NewEventBridge(bus, sseHandler)
// 启动恢复
recovered, pendingOrderIDs, manualActions, err := eventloop.NewDBState(entClient).RecoverOnStartup(context.Background())
if err != nil {
slog.Error("svc: startup recovery failed", "error", err)
} else {
slog.Info("svc: startup recovery complete", "recoveredJobs", recovered, "pendingRecovery", len(pendingOrderIDs), "manualActions", len(manualActions))
eventLoop.SetRecoveryPending(pendingOrderIDs)
// 通知前端:启动时发现的不一致项需要人工确认
for _, ma := range manualActions {
orderID := 0
if ma.OrderId != nil {
orderID = *ma.OrderId
}
bus.Publish(context.Background(), eventbus.NewManualActionCreatedEvent(
ma.ID, string(ma.ActionType), orderID,
))
}
}
// 从 DB 恢复所有 Actor 槽位状态(设备 + 暂存台)
eventLoop.InitActorsFromDB(context.Background())
// 启动事件循环
go eventLoop.Run(context.Background())
// 设备数据每日上传任务(仅 MOM 启用时)
var equipUploader *processor.EquipUploadService
if momClient != nil {
equipUploader = processor.NewEquipUploadService(
dbConn, momClient, c.Mom.EquipUploadHour, c.Mom.EquipUploadDeviceTypes,
)
equipUploader.Start(context.Background())
}
slog.Info("service context: initialized",
"mock", c.Mock.Enable,
"stations", len(line.Actors),
)
svcCtx := &ServiceContext{
Config: c,
SSEHandler: sseHandler,
RedisClient: rc,
EntClient: entClient,
DB: dbConn,
PLCManager: plcManager,
RobotManager: robotManager,
RecipeLoader: recipeLoader,
OrderProcessor: orderProcessor,
EventBus: bus,
EventLogWriter: eventLogWriter,
SSEBridge: sseBridge,
EventLoop: eventLoop,
RecoveryCoordinator: recoveryCoordinator,
Mom: momClient,
EquipUploader: equipUploader,
WmsClient: wmsstock.New(c.Wms.BaseURL, c.Wms.Token),
}
if !c.AgvRcs.Sim && c.AgvRcs.BaseURL != "" {
svcCtx.AgvClient = hikrcs.New(c.AgvRcs.BaseURL, c.AgvRcs.AppKey, c.AgvRcs.AppSecret, c.AgvRcs.Version)
}
return svcCtx
}
// dockStateAdapter 将 robot.Controller 适配为 DockStateReader
type dockStateAdapter struct {
ctrl *robot.Controller
}
func (a dockStateAdapter) ReadDockStates(ctx context.Context) ([]bool, error) {
if a.ctrl == nil {
return nil, fmt.Errorf("controller not available")
}
return a.ctrl.DockStates()
}