package parking import ( "errors" "fmt" "testing" "time" "github.com/glebarez/sqlite" "github.com/stretchr/testify/require" "go.uber.org/zap" "gorm.io/gorm" "wails-app/internal/dao" "wails-app/internal/global" incidentService "wails-app/internal/modules/incident/service" ) // failingGateController 注入失败的闸机控制器,模拟开闸失败。 type failingGateController struct{} func (c *failingGateController) OpenGate(deviceCode string, _ byte) error { return errors.New("relay timeout") } func (c *failingGateController) CloseGate(deviceCode string, _ byte) error { return errors.New("relay timeout") } func (c *failingGateController) IsGateConnected(string) bool { return true } func setupIncidentHookTest(t *testing.T) { t.Helper() dsn := fmt.Sprintf("file:incident-hook-%d?mode=memory&cache=shared", time.Now().UnixNano()) db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) require.NoError(t, err) sqlDB, err := db.DB() require.NoError(t, err) t.Cleanup(func() { _ = sqlDB.Close() if global.GVA_DB == db { global.GVA_DB = nil } }) global.GVA_DB = db global.GVA_LOG = zap.NewNop() // 避免 nil 日志 t.Cleanup(func() { global.GVA_LOG = nil }) require.NoError(t, db.AutoMigrate(&dao.IncidentRecord{}, &dao.DeviceCommandLog{}, &dao.UHFReader{})) } // TestOpenGateFailureCreatesIncidentAndCommandLog 开闸失败时: // 指令流水(failed) + 异常事件(gate_failed) 同时产生,且流水回填异常关联。 func TestOpenGateFailureCreatesIncidentAndCommandLog(t *testing.T) { setupIncidentHookTest(t) previous := currentGateController() SetGateController(&failingGateController{}) t.Cleanup(func() { SetGateController(previous) }) require.NoError(t, global.GVA_DB.Create(&dao.UHFReader{DeviceCode: "GATE-01", DeviceName: "东门道闸"}).Error) svc := &PassageService{} commandLogID, err := svc.OpenGateWithContext(GateCommandContext{ Source: incidentService.SourcePassage, SessionID: 7, }, "GATE-01", 0) require.Error(t, err) require.Contains(t, err.Error(), "relay timeout") // 调用方(passage.go 同款逻辑):记录异常并回填指令流水关联 incidentID := incidentSvc.RecordIncident(incidentService.RecordIncidentRequest{ Category: incidentService.CategoryGateFailed, Source: incidentService.SourcePassage, VehicleRecordID: 7, DeviceCode: "GATE-01", Description: "入场记录已创建,但开闸失败", Detail: err.Error(), }) require.NotZero(t, incidentID) incidentSvc.LinkIncidentToCommand(commandLogID, incidentID) // 指令流水已写入且为 failed require.NotZero(t, commandLogID) var cmd dao.DeviceCommandLog require.NoError(t, global.GVA_DB.First(&cmd, commandLogID).Error) require.Equal(t, "open", cmd.Action) require.Equal(t, incidentService.CommandFailed, cmd.Result) require.Equal(t, "东门道闸", cmd.DeviceName) require.Equal(t, uint(7), cmd.SessionID) // 异常事件已生成(gate_failed) var incident dao.IncidentRecord require.NoError(t, global.GVA_DB.Where("category = ?", incidentService.CategoryGateFailed).First(&incident).Error) require.Equal(t, incidentService.StatusPending, incident.Status) require.Equal(t, incidentService.SourcePassage, incident.Source) require.Equal(t, incidentService.LevelCritical, incident.Level) require.Equal(t, "GATE-01", incident.DeviceCode) // 指令流水回填了异常关联 require.NoError(t, global.GVA_DB.First(&cmd, commandLogID).Error) require.Equal(t, incident.ID, cmd.IncidentID) } // TestOpenGateSuccessRecordsCommandLog 开闸成功只写指令流水,不产生异常。 func TestOpenGateSuccessRecordsCommandLog(t *testing.T) { setupIncidentHookTest(t) previous := currentGateController() SetGateController(&gateControllerStub{}) t.Cleanup(func() { SetGateController(previous) }) svc := &PassageService{} commandLogID, err := svc.OpenGateWithContext(GateCommandContext{ Source: incidentService.SourceManual, OperatorID: 5, }, "GATE-01", 0) require.NoError(t, err) require.NotZero(t, commandLogID) var cmd dao.DeviceCommandLog require.NoError(t, global.GVA_DB.First(&cmd, commandLogID).Error) require.Equal(t, incidentService.CommandSuccess, cmd.Result) require.Equal(t, incidentService.SourceManual, cmd.Source) require.Equal(t, uint(5), cmd.OperatorID) var count int64 require.NoError(t, global.GVA_DB.Model(&dao.IncidentRecord{}).Count(&count).Error) require.Equal(t, int64(0), count) } // TestManualRaiseRecordedAsResolved 人工抬杆(带原因)应生成已解决的 manual_raise 记录。 func TestManualRaiseRecordedAsResolved(t *testing.T) { setupIncidentHookTest(t) // 模拟 API 层人工开闸成功后的埋点调用 incidentID := incidentService.NewIncidentService().RecordIncident(incidentService.RecordIncidentRequest{ Category: incidentService.CategoryManualRaise, Source: incidentService.SourceManual, VehicleRecordID: 3, PlateNumber: "SU-001", DeviceCode: "GATE-01", OperatorID: 5, Description: "人工开闸", HandleType: incidentService.HandleManualGate, HandleRemark: "车主称设备未识别", }) require.NotZero(t, incidentID) var incident dao.IncidentRecord require.NoError(t, global.GVA_DB.First(&incident, incidentID).Error) require.Equal(t, incidentService.StatusResolved, incident.Status) require.Equal(t, incidentService.HandleManualGate, incident.HandleType) require.Equal(t, "车主称设备未识别", incident.HandleRemark) require.NotNil(t, incident.HandledAt) require.Equal(t, uint(5), incident.HandlerID) }