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() }