package eventlog import ( "context" "fmt" "log/slog" "sync" "time" "bj_power_mes/ent" enteventlog "bj_power_mes/ent/eventlog" "bj_power_mes/internal/eventbus" ) // EventLogWriter 异步批量写入事件日志到 PostgreSQL type EventLogWriter struct { entClient *ent.Client ch chan eventbus.Event batchSize int flushMs int mu sync.Mutex running bool cancel context.CancelFunc wg sync.WaitGroup } // NewEventLogWriter 创建事件日志写入器 func NewEventLogWriter(entClient *ent.Client, batchSize, flushMs int) *EventLogWriter { if batchSize <= 0 { batchSize = 20 } if flushMs <= 0 { flushMs = 1000 } return &EventLogWriter{ entClient: entClient, ch: make(chan eventbus.Event, 256), batchSize: batchSize, flushMs: flushMs, } } // Start 启动异步写入循环 func (w *EventLogWriter) Start(ctx context.Context) { w.mu.Lock() if w.running { w.mu.Unlock() return } w.running = true ctx, w.cancel = context.WithCancel(ctx) w.mu.Unlock() w.wg.Add(1) go w.writeLoop(ctx) } // Stop 停止写入器 func (w *EventLogWriter) Stop() { w.mu.Lock() if !w.running { w.mu.Unlock() return } w.cancel() w.mu.Unlock() w.wg.Wait() } // Write 写入事件(非阻塞,满时丢弃) func (w *EventLogWriter) Write(event eventbus.Event) { select { case w.ch <- event: default: slog.Warn("eventlog: channel full, dropping event", "type", event.Type) } } func (w *EventLogWriter) writeLoop(ctx context.Context) { defer w.wg.Done() ticker := time.NewTicker(time.Duration(w.flushMs) * time.Millisecond) defer ticker.Stop() var batch []eventbus.Event for { select { case <-ctx.Done(): w.flush(batch) return case event := <-w.ch: batch = append(batch, event) if len(batch) >= w.batchSize { w.flush(batch) batch = batch[:0] } case <-ticker.C: if len(batch) > 0 { w.flush(batch) batch = batch[:0] } } } } func (w *EventLogWriter) flush(events []eventbus.Event) { if len(events) == 0 { return } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() var builders []*ent.EventLogCreate for _, e := range events { // 连接状态事件仅用于 SSE 实时推送,不持久化 if e.Type == eventbus.EventConnectionStatusUpdated { continue } // 报警确认仅用于 SSE 实时推送,不持久化 if e.Type == eventbus.EventAlarmAcked { continue } // MACHINE_DONE: 创建独立事件记录(与 MACHINE_START 配对) // 同时更新对应 MACHINE_START 的 endedAt 用于耗时查询 if e.Type == eventbus.EventMachineDone { if endedAt, ok := e.Payload["endedAt"].(time.Time); ok { jobId, _ := getPayloadInt(e.Payload, "jobId") equipId, _ := getPayloadInt(e.Payload, "equipmentId") if jobId > 0 && equipId > 0 { w.updateMachineStartEndedAt(ctx, jobId, equipId, endedAt) } } } // INSPECTION_RESULT: 同样更新对应 MACHINE_START 的 endedAt if e.Type == eventbus.EventInspectionResult { if endedAt, ok := e.Payload["endedAt"].(time.Time); ok { jobId, _ := getPayloadInt(e.Payload, "jobId") equipId, _ := getPayloadInt(e.Payload, "equipmentId") if jobId > 0 && equipId > 0 { w.updateMachineStartEndedAt(ctx, jobId, equipId, endedAt) } } } b := w.entClient.EventLog.Create(). SetEventType(string(e.Type)). SetSourceId(e.Source). SetEntityType(e.EntityType). SetEntityId(e.EntityID). SetEntityVersion(e.EntityVersion). SetDescription(eventDescription(e)). SetPayload(e.Payload) if v, ok := getPayloadInt(e.Payload, "jobId"); ok { b.SetJobId(v) } if v, ok := e.Payload["workpieceNo"].(string); ok && v != "" { b.SetWorkpieceNo(v) } if v, ok := getPayloadInt(e.Payload, "equipmentId"); ok { b.SetEquipmentId(v) } if v, ok := e.Payload["equipmentName"].(string); ok && v != "" { b.SetEquipmentName(v) } if v, ok := getPayloadInt(e.Payload, "tempSlotNo"); ok { b.SetTempSlotNo(v) } if v, ok := e.Payload["startedAt"].(time.Time); ok { b.SetStartedAt(v) } if v, ok := e.Payload["endedAt"].(time.Time); ok { b.SetEndedAt(v) } builders = append(builders, b) } if len(builders) > 0 { if _, err := w.entClient.EventLog.CreateBulk(builders...).Save(ctx); err != nil { slog.Error("eventlog: bulk save failed", "count", len(builders), "error", err) } } } // updateMachineStartEndedAt 更新对应 MACHINE_START 事件日志的 endedAt func (w *EventLogWriter) updateMachineStartEndedAt(ctx context.Context, jobId, equipId int, endedAt time.Time) { n, err := w.entClient.EventLog.Update(). Where( enteventlog.EventTypeEQ(string(eventbus.EventMachineStart)), enteventlog.JobIdEQ(jobId), enteventlog.EquipmentIdEQ(equipId), enteventlog.EndedAtIsNil(), ). SetEndedAt(endedAt). Save(ctx) if err != nil { slog.Error("eventlog: update MACHINE_START endedAt failed", "jobId", jobId, "equipmentId", equipId, "error", err) return } if n == 0 { slog.Warn("eventlog: no matching MACHINE_START found for endedAt update", "jobId", jobId, "equipmentId", equipId) } } // eventDescription 根据事件类型生成中文描述 // EventDescription 根据事件类型生成中文描述(供 SSE 桥接等复用) func EventDescription(e eventbus.Event) string { return eventDescription(e) } func eventDescription(e eventbus.Event) string { switch e.Type { case eventbus.EventLoad: return fmt.Sprintf("%s 从 暂存台#%s 上料至 %s", jobLabel(e), tempSlotLabel(e), equipLabel(e)) case eventbus.EventUnload: return fmt.Sprintf("%s 从 %s 下料至 暂存台#%s", jobLabel(e), equipLabel(e), tempSlotLabel(e)) case eventbus.EventExchange: return fmt.Sprintf("%s 在 %s 换料", jobLabel(e), equipLabel(e)) case eventbus.EventScan: return jobLabel(e) + " 扫码" case eventbus.EventMark: return jobLabel(e) + " 打标" case eventbus.EventReplenish: return fmt.Sprintf("%s 从 %s 补料至 暂存台#%s", jobLabel(e), dockLabel(e), tempSlotLabel(e)) case eventbus.EventMachineStart: return fmt.Sprintf("%s 在 %s 加工", jobLabel(e), equipLabel(e)) case eventbus.EventMachineDone: desc := fmt.Sprintf("%s 在 %s 加工完成", jobLabel(e), equipLabel(e)) if dur, ok := e.Payload["duration"].(string); ok && dur != "" { desc = fmt.Sprintf("%s 在 %s 加工完成(耗时%s)", jobLabel(e), equipLabel(e), dur) } return desc case eventbus.EventInspectionResult: if pass, ok := e.Payload["pass"].(bool); ok { if pass { return fmt.Sprintf("%s 在 %s 检测合格", jobLabel(e), equipLabel(e)) } return fmt.Sprintf("%s 在 %s 检测不合格", jobLabel(e), equipLabel(e)) } return fmt.Sprintf("%s 在 %s 检测", jobLabel(e), equipLabel(e)) case eventbus.EventJobCompleted: return jobLabel(e) + " 全部工序完成" case eventbus.EventJobScrapped: return jobLabel(e) + " 报废" case eventbus.EventJobDischarged: return fmt.Sprintf("%s 从 暂存台#%s 下线至 %s", jobLabel(e), tempSlotLabel(e), dockLabel(e)) case eventbus.EventOrderStarted: return fmt.Sprintf("%s 启动", orderLabel(e)) case eventbus.EventOrderPaused: return fmt.Sprintf("%s 暂停", orderLabel(e)) case eventbus.EventOrderResumed: return fmt.Sprintf("%s 恢复", orderLabel(e)) case eventbus.EventOrderCancelled: return fmt.Sprintf("%s 取消", orderLabel(e)) case eventbus.EventOrderCompleted: return fmt.Sprintf("%s 完成", orderLabel(e)) case eventbus.EventPalletCompleted: dockNo, _ := getPayloadInt(e.Payload, "dockNo") passQty, _ := getPayloadInt(e.Payload, "passQty") ngQty, _ := getPayloadInt(e.Payload, "ngQty") return fmt.Sprintf("接驳台#%d 托盘完工(良品%d 不良%d)", dockNo, passQty, ngQty) case eventbus.EventAlarmRaised: alarmCode, _ := e.Payload["alarmCode"].(string) alarmMsg, _ := e.Payload["alarmMessage"].(string) entityLabel := entityLabel(e.EntityType, e.EntityID) return fmt.Sprintf("%s 报警: [%s] %s", entityLabel, alarmCode, alarmMsg) case eventbus.EventAlarmAcked: entityLabel := entityLabel(e.EntityType, e.EntityID) return fmt.Sprintf("%s 报警已确认", entityLabel) case eventbus.EventEquipmentAvailabilityChanged: enabled, _ := e.Payload["enabled"].(bool) if enabled { return fmt.Sprintf("%s 已设为调度可用", equipLabel(e)) } return fmt.Sprintf("%s 已设为调度不可用", equipLabel(e)) case eventbus.EventManualActionCreated: actionType, _ := e.Payload["actionType"].(string) entityLabel := entityLabel(e.EntityType, e.EntityID) return fmt.Sprintf("%s 创建人工处理: %s", entityLabel, manualActionTypeDesc(actionType)) case eventbus.EventManualActionResolved: entityLabel := entityLabel(e.EntityType, e.EntityID) return fmt.Sprintf("%s 人工处理已解决", entityLabel) default: return fmt.Sprintf("%s: %s", entityLabel(e.EntityType, e.EntityID), string(e.Type)) } } // jobLabel 生成 "工件 #344 (M2026...)" 格式 func jobLabel(e eventbus.Event) string { if v, ok := getPayloadInt(e.Payload, "jobId"); ok && v > 0 { if wn, ok := e.Payload["workpieceNo"].(string); ok && wn != "" { return fmt.Sprintf("工件 #%d (%s)", v, wn) } return fmt.Sprintf("工件 #%d", v) } return fmt.Sprintf("%s #%s", entityLabel(e.EntityType, e.EntityID), e.EntityID) } // equipLabel 生成 "CNC#3" 格式 func equipLabel(e eventbus.Event) string { en, _ := e.Payload["equipmentName"].(string) if v, ok := getPayloadInt(e.Payload, "equipmentId"); ok && v > 0 { if en != "" { return fmt.Sprintf("%s#%d", en, v) } return fmt.Sprintf("设备#%d", v) } if en != "" { return en } return "设备" } // tempSlotLabel 生成 "7" 格式(纯槽位号) func tempSlotLabel(e eventbus.Event) string { if v, ok := getPayloadInt(e.Payload, "tempSlotNo"); ok && v > 0 { return fmt.Sprintf("%d", v) } if v, ok := getPayloadInt(e.Payload, "slotNo"); ok && v > 0 { return fmt.Sprintf("%d", v) } return "" } func getPayloadInt(payload map[string]any, key string) (int, bool) { if v, ok := payload[key].(float64); ok { return int(v), true } if v, ok := payload[key].(int); ok { return v, true } return 0, false } // dockLabel 生成 接驳台1-3 格式 func dockLabel(e eventbus.Event) string { dn, dok := getPayloadInt(e.Payload, "dockNo") ds, sok := getPayloadInt(e.Payload, "dockSlotNo") if dok && dn > 0 && sok && ds > 0 { return fmt.Sprintf("接驳台#%d-%d", dn, ds) } return "" } // orderLabel 生成 "工单 #42" 格式 func orderLabel(e eventbus.Event) string { if v, ok := getPayloadInt(e.Payload, "orderId"); ok && v > 0 { return fmt.Sprintf("工单 #%d", v) } return fmt.Sprintf("%s #%s", entityLabel(e.EntityType, e.EntityID), e.EntityID) } func entityLabel(entityType, entityID string) string { switch entityType { case "job": return fmt.Sprintf("工件 #%s", entityID) case "workorder": return fmt.Sprintf("工单 #%s", entityID) case "equipment": return fmt.Sprintf("设备 #%s", entityID) default: if entityType != "" && entityID != "" { return fmt.Sprintf("%s #%s", entityType, entityID) } return "系统" } } func manualActionTypeDesc(s string) string { switch s { case "SCAN_FAILED": return "扫码失败" case "RECOVERY_REQUIRED": return "需恢复确认" case "INSPECTION_DECISION": return "检测判定" case "FAULT_CONFIRM": return "故障确认" default: return s } }