Files
bj_power/bj_power_mes/internal/svc/service_context.go
T

279 lines
8.1 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"
"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
}
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)
// 事件总线:始终 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,
}
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()
}