package repository import ( "encoding/json" "time" "wails-app/internal/dao" "wails-app/internal/global" "gorm.io/gorm" ) // appendEventLog 向事件日志 JSON 数组追加一条事件(沿用数字票 event_log 约定)。 func appendEventLog(log, eventJSON string) string { var events []map[string]interface{} json.Unmarshal([]byte(log), &events) var event map[string]interface{} json.Unmarshal([]byte(eventJSON), &event) events = append(events, event) data, _ := json.Marshal(events) return string(data) } type IncidentRepository struct{} // Create 创建异常记录。 func (r *IncidentRepository) Create(record *dao.IncidentRecord) error { return global.GVA_DB.Create(record).Error } // GetByID 查询异常详情(含软删除过滤)。 func (r *IncidentRepository) GetByID(id uint) (*dao.IncidentRecord, error) { var record dao.IncidentRecord err := global.GVA_DB.First(&record, id).Error if err != nil { return nil, err } return &record, nil } // UpdateStateIfCurrent CAS 状态更新:仅当记录仍处于调用方观察到的状态时生效。 func (r *IncidentRepository) UpdateStateIfCurrent(id uint, fromStatus, toStatus string, updates map[string]interface{}) (bool, error) { if updates == nil { updates = make(map[string]interface{}) } updates["status"] = toStatus result := global.GVA_DB.Model(&dao.IncidentRecord{}). Where("id = ? AND status = ?", id, fromStatus). Updates(updates) return result.RowsAffected == 1, result.Error } // UpdateIncidentID 建立异常与指令流水的双向关联(开闸失败场景)。 func (r *IncidentRepository) UpdateIncidentID(logID, incidentID uint) error { if logID == 0 || incidentID == 0 { return nil } return global.GVA_DB.Transaction(func(tx *gorm.DB) error { if err := tx.Model(&dao.DeviceCommandLog{}). Where("id = ?", logID).Update("incident_id", incidentID).Error; err != nil { return err } return tx.Model(&dao.IncidentRecord{}). Where("id = ?", incidentID).Update("gate_command_id", logID).Error }) } // HasOpenDeviceOffline 同设备是否已有未关闭(待处理/处理中)的离线异常。 func (r *IncidentRepository) HasOpenDeviceOffline(deviceCode string) (bool, error) { if deviceCode == "" { return false, nil } var count int64 err := global.GVA_DB.Model(&dao.IncidentRecord{}). Where("device_code = ? AND category = ? AND status IN (?, ?)", deviceCode, "device_offline", "pending", "processing"). Count(&count).Error return count > 0, err } // ExistsRecent 通行类异常去重:同设备 + 同分类 + 同车辆标识,在时间窗口内不重复生成。 // 无车辆标识(车牌和 RFID 均为空)时不去重。 func (r *IncidentRepository) ExistsRecent(deviceCode, category, plateNumber, rfidTag string, minutes int) (bool, error) { if plateNumber == "" && rfidTag == "" { return false, nil } db := global.GVA_DB.Model(&dao.IncidentRecord{}). Where("device_code = ? AND category = ? AND status IN (?, ?) AND created_at >= ?", deviceCode, category, "pending", "processing", time.Now().Add(-time.Duration(minutes)*time.Minute)) if plateNumber != "" { db = db.Where("plate_number = ?", plateNumber) } else { // GORM maps RFIDTag to rf_id_tag. Keep this condition model-based so it // cannot drift from the actual database column name. db = db.Where(&dao.IncidentRecord{RFIDTag: rfidTag}) } var count int64 err := db.Count(&count).Error return count > 0, err } // CreateCommandLog 写入指令流水。 func (r *IncidentRepository) CreateCommandLog(log *dao.DeviceCommandLog) error { return global.GVA_DB.Create(log).Error } // ResolveDeviceOfflineByDevice 设备恢复在线时批量关闭该设备未处理的离线异常。 func (r *IncidentRepository) ResolveDeviceOfflineByDevice(deviceCode, remark string) error { if deviceCode == "" { return nil } var open []dao.IncidentRecord if err := global.GVA_DB.Where("device_code = ? AND category = ? AND status IN (?, ?)", deviceCode, "device_offline", "pending", "processing").Find(&open).Error; err != nil { return err } now := time.Now() for i := range open { event, _ := json.Marshal(map[string]interface{}{ "event": "resolved", "handle_type": "device_repaired", "remark": remark, "at": now.Format(time.RFC3339), }) if err := global.GVA_DB.Model(&open[i]).Updates(map[string]interface{}{ "status": "resolved", "handle_type": "device_repaired", "handled_at": now, "handle_remark": remark, "handler_id": 0, "event_log": appendEventLog(open[i].EventLog, string(event)), }).Error; err != nil { return err } } return nil } type IncidentListQuery struct { Status string `form:"status"` Category string `form:"category"` Level string `form:"level"` Source string `form:"source"` PlateNumber string `form:"plate_number"` TicketNo string `form:"ticket_no"` ParkingLotID uint `form:"parking_lot_id"` DeviceCode string `form:"device_code"` StartDate string `form:"start_date"` EndDate string `form:"end_date"` Page int `form:"page"` PageSize int `form:"page_size"` } type IncidentListItem struct { dao.IncidentRecord OperatorName string `json:"operator_name"` HandlerName string `json:"handler_name"` LotName string `json:"lot_name"` } // List 分页筛选异常记录(上报人/处理人昵称、停车场名称联查)。 func (r *IncidentRepository) List(q IncidentListQuery) ([]IncidentListItem, int64, error) { db := global.GVA_DB.Table("incident_record ir"). Select("ir.*, op.nick_name as operator_name, hd.nick_name as handler_name, COALESCE(ir.parking_lot_name, pl.lot_name) as lot_name"). Joins("LEFT JOIN sys_users op ON op.id = ir.operator_id"). Joins("LEFT JOIN sys_users hd ON hd.id = ir.handler_id"). Joins("LEFT JOIN parking_lot pl ON pl.id = ir.parking_lot_id") if q.Status != "" { db = db.Where("ir.status = ?", q.Status) } if q.Category != "" { db = db.Where("ir.category = ?", q.Category) } if q.Level != "" { db = db.Where("ir.level = ?", q.Level) } if q.Source != "" { db = db.Where("ir.source = ?", q.Source) } if q.PlateNumber != "" { db = db.Where("ir.plate_number LIKE ?", "%"+q.PlateNumber+"%") } if q.TicketNo != "" { db = db.Where("ir.ticket_no LIKE ?", "%"+q.TicketNo+"%") } if q.ParkingLotID > 0 { db = db.Where("ir.parking_lot_id = ?", q.ParkingLotID) } if q.DeviceCode != "" { db = db.Where("ir.device_code LIKE ?", "%"+q.DeviceCode+"%") } if q.StartDate != "" { db = db.Where("ir.created_at >= ?", q.StartDate+" 00:00:00") } if q.EndDate != "" { db = db.Where("ir.created_at < ?", q.EndDate+" 23:59:59") } var total int64 db.Count(&total) if q.Page <= 0 { q.Page = 1 } if q.PageSize <= 0 { q.PageSize = 10 } var list []IncidentListItem err := db.Order("ir.id DESC").Offset((q.Page - 1) * q.PageSize).Limit(q.PageSize).Scan(&list).Error return list, total, err } type IncidentStats struct { Pending int64 `json:"pending"` Processing int64 `json:"processing"` Resolved int64 `json:"resolved"` Closed int64 `json:"closed"` TodayCreated int64 `json:"today_created"` OpenFundRelated int64 `json:"open_fund_related"` TodayForceFreeAmount float64 `json:"today_force_free_amount"` } // Stats 异常统计:状态分布、今日新增、未关闭资金类数量、今日强制免费金额。 func (r *IncidentRepository) Stats() (IncidentStats, error) { var stats IncidentStats // 每次 Count 使用全新的查询构建器,避免 GORM Where 条件在复用实例上累积 base := func() *gorm.DB { return global.GVA_DB.Model(&dao.IncidentRecord{}) } if err := base().Where("status = ?", "pending").Count(&stats.Pending).Error; err != nil { return stats, err } if err := base().Where("status = ?", "processing").Count(&stats.Processing).Error; err != nil { return stats, err } if err := base().Where("status = ?", "resolved").Count(&stats.Resolved).Error; err != nil { return stats, err } if err := base().Where("status = ?", "closed").Count(&stats.Closed).Error; err != nil { return stats, err } if err := base().Where("created_at >= ?", startOfDay()).Count(&stats.TodayCreated).Error; err != nil { return stats, err } // 资金相关分类(支付失败/不确定、开闸失败、无入场记录出场)中未关闭的数量 if err := base().Where("status IN (?, ?) AND category IN (?, ?, ?, ?)", "pending", "processing", "payment_failed", "payment_uncertain", "gate_failed", "no_entry_exit"). Count(&stats.OpenFundRelated).Error; err != nil { return stats, err } // 今日强制免费金额合计 if err := global.GVA_DB.Model(&dao.IncidentRecord{}). Where("handle_type = ? AND handled_at >= ?", "force_free", startOfDay()). Select("COALESCE(SUM(force_free_amount), 0)").Scan(&stats.TodayForceFreeAmount).Error; err != nil { return stats, err } return stats, nil } func startOfDay() time.Time { now := time.Now() return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) }