repo.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. package repository
  2. import (
  3. "encoding/json"
  4. "time"
  5. "wails-app/internal/dao"
  6. "wails-app/internal/global"
  7. "gorm.io/gorm"
  8. )
  9. // appendEventLog 向事件日志 JSON 数组追加一条事件(沿用数字票 event_log 约定)。
  10. func appendEventLog(log, eventJSON string) string {
  11. var events []map[string]interface{}
  12. json.Unmarshal([]byte(log), &events)
  13. var event map[string]interface{}
  14. json.Unmarshal([]byte(eventJSON), &event)
  15. events = append(events, event)
  16. data, _ := json.Marshal(events)
  17. return string(data)
  18. }
  19. type IncidentRepository struct{}
  20. // Create 创建异常记录。
  21. func (r *IncidentRepository) Create(record *dao.IncidentRecord) error {
  22. return global.GVA_DB.Create(record).Error
  23. }
  24. // GetByID 查询异常详情(含软删除过滤)。
  25. func (r *IncidentRepository) GetByID(id uint) (*dao.IncidentRecord, error) {
  26. var record dao.IncidentRecord
  27. err := global.GVA_DB.First(&record, id).Error
  28. if err != nil {
  29. return nil, err
  30. }
  31. return &record, nil
  32. }
  33. // UpdateStateIfCurrent CAS 状态更新:仅当记录仍处于调用方观察到的状态时生效。
  34. func (r *IncidentRepository) UpdateStateIfCurrent(id uint, fromStatus, toStatus string, updates map[string]interface{}) (bool, error) {
  35. if updates == nil {
  36. updates = make(map[string]interface{})
  37. }
  38. updates["status"] = toStatus
  39. result := global.GVA_DB.Model(&dao.IncidentRecord{}).
  40. Where("id = ? AND status = ?", id, fromStatus).
  41. Updates(updates)
  42. return result.RowsAffected == 1, result.Error
  43. }
  44. // UpdateIncidentID 建立异常与指令流水的双向关联(开闸失败场景)。
  45. func (r *IncidentRepository) UpdateIncidentID(logID, incidentID uint) error {
  46. if logID == 0 || incidentID == 0 {
  47. return nil
  48. }
  49. return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
  50. if err := tx.Model(&dao.DeviceCommandLog{}).
  51. Where("id = ?", logID).Update("incident_id", incidentID).Error; err != nil {
  52. return err
  53. }
  54. return tx.Model(&dao.IncidentRecord{}).
  55. Where("id = ?", incidentID).Update("gate_command_id", logID).Error
  56. })
  57. }
  58. // HasOpenDeviceOffline 同设备是否已有未关闭(待处理/处理中)的离线异常。
  59. func (r *IncidentRepository) HasOpenDeviceOffline(deviceCode string) (bool, error) {
  60. if deviceCode == "" {
  61. return false, nil
  62. }
  63. var count int64
  64. err := global.GVA_DB.Model(&dao.IncidentRecord{}).
  65. Where("device_code = ? AND category = ? AND status IN (?, ?)",
  66. deviceCode, "device_offline", "pending", "processing").
  67. Count(&count).Error
  68. return count > 0, err
  69. }
  70. // ExistsRecent 通行类异常去重:同设备 + 同分类 + 同车辆标识,在时间窗口内不重复生成。
  71. // 无车辆标识(车牌和 RFID 均为空)时不去重。
  72. func (r *IncidentRepository) ExistsRecent(deviceCode, category, plateNumber, rfidTag string, minutes int) (bool, error) {
  73. if plateNumber == "" && rfidTag == "" {
  74. return false, nil
  75. }
  76. db := global.GVA_DB.Model(&dao.IncidentRecord{}).
  77. Where("device_code = ? AND category = ? AND status IN (?, ?) AND created_at >= ?",
  78. deviceCode, category, "pending", "processing", time.Now().Add(-time.Duration(minutes)*time.Minute))
  79. if plateNumber != "" {
  80. db = db.Where("plate_number = ?", plateNumber)
  81. } else {
  82. // GORM maps RFIDTag to rf_id_tag. Keep this condition model-based so it
  83. // cannot drift from the actual database column name.
  84. db = db.Where(&dao.IncidentRecord{RFIDTag: rfidTag})
  85. }
  86. var count int64
  87. err := db.Count(&count).Error
  88. return count > 0, err
  89. }
  90. // CreateCommandLog 写入指令流水。
  91. func (r *IncidentRepository) CreateCommandLog(log *dao.DeviceCommandLog) error {
  92. return global.GVA_DB.Create(log).Error
  93. }
  94. // ResolveDeviceOfflineByDevice 设备恢复在线时批量关闭该设备未处理的离线异常。
  95. func (r *IncidentRepository) ResolveDeviceOfflineByDevice(deviceCode, remark string) error {
  96. if deviceCode == "" {
  97. return nil
  98. }
  99. var open []dao.IncidentRecord
  100. if err := global.GVA_DB.Where("device_code = ? AND category = ? AND status IN (?, ?)",
  101. deviceCode, "device_offline", "pending", "processing").Find(&open).Error; err != nil {
  102. return err
  103. }
  104. now := time.Now()
  105. for i := range open {
  106. event, _ := json.Marshal(map[string]interface{}{
  107. "event": "resolved", "handle_type": "device_repaired", "remark": remark, "at": now.Format(time.RFC3339),
  108. })
  109. if err := global.GVA_DB.Model(&open[i]).Updates(map[string]interface{}{
  110. "status": "resolved", "handle_type": "device_repaired",
  111. "handled_at": now, "handle_remark": remark, "handler_id": 0,
  112. "event_log": appendEventLog(open[i].EventLog, string(event)),
  113. }).Error; err != nil {
  114. return err
  115. }
  116. }
  117. return nil
  118. }
  119. type IncidentListQuery struct {
  120. Status string `form:"status"`
  121. Category string `form:"category"`
  122. Level string `form:"level"`
  123. Source string `form:"source"`
  124. PlateNumber string `form:"plate_number"`
  125. TicketNo string `form:"ticket_no"`
  126. ParkingLotID uint `form:"parking_lot_id"`
  127. DeviceCode string `form:"device_code"`
  128. StartDate string `form:"start_date"`
  129. EndDate string `form:"end_date"`
  130. Page int `form:"page"`
  131. PageSize int `form:"page_size"`
  132. }
  133. type IncidentListItem struct {
  134. dao.IncidentRecord
  135. OperatorName string `json:"operator_name"`
  136. HandlerName string `json:"handler_name"`
  137. LotName string `json:"lot_name"`
  138. }
  139. // List 分页筛选异常记录(上报人/处理人昵称、停车场名称联查)。
  140. func (r *IncidentRepository) List(q IncidentListQuery) ([]IncidentListItem, int64, error) {
  141. db := global.GVA_DB.Table("incident_record ir").
  142. 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").
  143. Joins("LEFT JOIN sys_users op ON op.id = ir.operator_id").
  144. Joins("LEFT JOIN sys_users hd ON hd.id = ir.handler_id").
  145. Joins("LEFT JOIN parking_lot pl ON pl.id = ir.parking_lot_id")
  146. if q.Status != "" {
  147. db = db.Where("ir.status = ?", q.Status)
  148. }
  149. if q.Category != "" {
  150. db = db.Where("ir.category = ?", q.Category)
  151. }
  152. if q.Level != "" {
  153. db = db.Where("ir.level = ?", q.Level)
  154. }
  155. if q.Source != "" {
  156. db = db.Where("ir.source = ?", q.Source)
  157. }
  158. if q.PlateNumber != "" {
  159. db = db.Where("ir.plate_number LIKE ?", "%"+q.PlateNumber+"%")
  160. }
  161. if q.TicketNo != "" {
  162. db = db.Where("ir.ticket_no LIKE ?", "%"+q.TicketNo+"%")
  163. }
  164. if q.ParkingLotID > 0 {
  165. db = db.Where("ir.parking_lot_id = ?", q.ParkingLotID)
  166. }
  167. if q.DeviceCode != "" {
  168. db = db.Where("ir.device_code LIKE ?", "%"+q.DeviceCode+"%")
  169. }
  170. if q.StartDate != "" {
  171. db = db.Where("ir.created_at >= ?", q.StartDate+" 00:00:00")
  172. }
  173. if q.EndDate != "" {
  174. db = db.Where("ir.created_at < ?", q.EndDate+" 23:59:59")
  175. }
  176. var total int64
  177. db.Count(&total)
  178. if q.Page <= 0 {
  179. q.Page = 1
  180. }
  181. if q.PageSize <= 0 {
  182. q.PageSize = 10
  183. }
  184. var list []IncidentListItem
  185. err := db.Order("ir.id DESC").Offset((q.Page - 1) * q.PageSize).Limit(q.PageSize).Scan(&list).Error
  186. return list, total, err
  187. }
  188. type IncidentStats struct {
  189. Pending int64 `json:"pending"`
  190. Processing int64 `json:"processing"`
  191. Resolved int64 `json:"resolved"`
  192. Closed int64 `json:"closed"`
  193. TodayCreated int64 `json:"today_created"`
  194. OpenFundRelated int64 `json:"open_fund_related"`
  195. TodayForceFreeAmount float64 `json:"today_force_free_amount"`
  196. }
  197. // Stats 异常统计:状态分布、今日新增、未关闭资金类数量、今日强制免费金额。
  198. func (r *IncidentRepository) Stats() (IncidentStats, error) {
  199. var stats IncidentStats
  200. // 每次 Count 使用全新的查询构建器,避免 GORM Where 条件在复用实例上累积
  201. base := func() *gorm.DB {
  202. return global.GVA_DB.Model(&dao.IncidentRecord{})
  203. }
  204. if err := base().Where("status = ?", "pending").Count(&stats.Pending).Error; err != nil {
  205. return stats, err
  206. }
  207. if err := base().Where("status = ?", "processing").Count(&stats.Processing).Error; err != nil {
  208. return stats, err
  209. }
  210. if err := base().Where("status = ?", "resolved").Count(&stats.Resolved).Error; err != nil {
  211. return stats, err
  212. }
  213. if err := base().Where("status = ?", "closed").Count(&stats.Closed).Error; err != nil {
  214. return stats, err
  215. }
  216. if err := base().Where("created_at >= ?", startOfDay()).Count(&stats.TodayCreated).Error; err != nil {
  217. return stats, err
  218. }
  219. // 资金相关分类(支付失败/不确定、开闸失败、无入场记录出场)中未关闭的数量
  220. if err := base().Where("status IN (?, ?) AND category IN (?, ?, ?, ?)",
  221. "pending", "processing",
  222. "payment_failed", "payment_uncertain", "gate_failed", "no_entry_exit").
  223. Count(&stats.OpenFundRelated).Error; err != nil {
  224. return stats, err
  225. }
  226. // 今日强制免费金额合计
  227. if err := global.GVA_DB.Model(&dao.IncidentRecord{}).
  228. Where("handle_type = ? AND handled_at >= ?", "force_free", startOfDay()).
  229. Select("COALESCE(SUM(force_free_amount), 0)").Scan(&stats.TodayForceFreeAmount).Error; err != nil {
  230. return stats, err
  231. }
  232. return stats, nil
  233. }
  234. func startOfDay() time.Time {
  235. now := time.Now()
  236. return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
  237. }