repo.go 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  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. db = db.Where("rfid_tag = ?", rfidTag)
  83. }
  84. var count int64
  85. err := db.Count(&count).Error
  86. return count > 0, err
  87. }
  88. // CreateCommandLog 写入指令流水。
  89. func (r *IncidentRepository) CreateCommandLog(log *dao.DeviceCommandLog) error {
  90. return global.GVA_DB.Create(log).Error
  91. }
  92. // ResolveDeviceOfflineByDevice 设备恢复在线时批量关闭该设备未处理的离线异常。
  93. func (r *IncidentRepository) ResolveDeviceOfflineByDevice(deviceCode, remark string) error {
  94. if deviceCode == "" {
  95. return nil
  96. }
  97. var open []dao.IncidentRecord
  98. if err := global.GVA_DB.Where("device_code = ? AND category = ? AND status IN (?, ?)",
  99. deviceCode, "device_offline", "pending", "processing").Find(&open).Error; err != nil {
  100. return err
  101. }
  102. now := time.Now()
  103. for i := range open {
  104. event, _ := json.Marshal(map[string]interface{}{
  105. "event": "resolved", "handle_type": "device_repaired", "remark": remark, "at": now.Format(time.RFC3339),
  106. })
  107. if err := global.GVA_DB.Model(&open[i]).Updates(map[string]interface{}{
  108. "status": "resolved", "handle_type": "device_repaired",
  109. "handled_at": now, "handle_remark": remark, "handler_id": 0,
  110. "event_log": appendEventLog(open[i].EventLog, string(event)),
  111. }).Error; err != nil {
  112. return err
  113. }
  114. }
  115. return nil
  116. }
  117. type IncidentListQuery struct {
  118. Status string `form:"status"`
  119. Category string `form:"category"`
  120. Level string `form:"level"`
  121. Source string `form:"source"`
  122. PlateNumber string `form:"plate_number"`
  123. TicketNo string `form:"ticket_no"`
  124. ParkingLotID uint `form:"parking_lot_id"`
  125. DeviceCode string `form:"device_code"`
  126. StartDate string `form:"start_date"`
  127. EndDate string `form:"end_date"`
  128. Page int `form:"page"`
  129. PageSize int `form:"page_size"`
  130. }
  131. type IncidentListItem struct {
  132. dao.IncidentRecord
  133. OperatorName string `json:"operator_name"`
  134. HandlerName string `json:"handler_name"`
  135. LotName string `json:"lot_name"`
  136. }
  137. // List 分页筛选异常记录(上报人/处理人昵称、停车场名称联查)。
  138. func (r *IncidentRepository) List(q IncidentListQuery) ([]IncidentListItem, int64, error) {
  139. db := global.GVA_DB.Table("incident_record ir").
  140. 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").
  141. Joins("LEFT JOIN sys_users op ON op.id = ir.operator_id").
  142. Joins("LEFT JOIN sys_users hd ON hd.id = ir.handler_id").
  143. Joins("LEFT JOIN parking_lot pl ON pl.id = ir.parking_lot_id")
  144. if q.Status != "" {
  145. db = db.Where("ir.status = ?", q.Status)
  146. }
  147. if q.Category != "" {
  148. db = db.Where("ir.category = ?", q.Category)
  149. }
  150. if q.Level != "" {
  151. db = db.Where("ir.level = ?", q.Level)
  152. }
  153. if q.Source != "" {
  154. db = db.Where("ir.source = ?", q.Source)
  155. }
  156. if q.PlateNumber != "" {
  157. db = db.Where("ir.plate_number LIKE ?", "%"+q.PlateNumber+"%")
  158. }
  159. if q.TicketNo != "" {
  160. db = db.Where("ir.ticket_no LIKE ?", "%"+q.TicketNo+"%")
  161. }
  162. if q.ParkingLotID > 0 {
  163. db = db.Where("ir.parking_lot_id = ?", q.ParkingLotID)
  164. }
  165. if q.DeviceCode != "" {
  166. db = db.Where("ir.device_code LIKE ?", "%"+q.DeviceCode+"%")
  167. }
  168. if q.StartDate != "" {
  169. db = db.Where("ir.created_at >= ?", q.StartDate+" 00:00:00")
  170. }
  171. if q.EndDate != "" {
  172. db = db.Where("ir.created_at < ?", q.EndDate+" 23:59:59")
  173. }
  174. var total int64
  175. db.Count(&total)
  176. if q.Page <= 0 {
  177. q.Page = 1
  178. }
  179. if q.PageSize <= 0 {
  180. q.PageSize = 10
  181. }
  182. var list []IncidentListItem
  183. err := db.Order("ir.id DESC").Offset((q.Page - 1) * q.PageSize).Limit(q.PageSize).Scan(&list).Error
  184. return list, total, err
  185. }
  186. type IncidentStats struct {
  187. Pending int64 `json:"pending"`
  188. Processing int64 `json:"processing"`
  189. Resolved int64 `json:"resolved"`
  190. Closed int64 `json:"closed"`
  191. TodayCreated int64 `json:"today_created"`
  192. OpenFundRelated int64 `json:"open_fund_related"`
  193. TodayForceFreeAmount float64 `json:"today_force_free_amount"`
  194. }
  195. // Stats 异常统计:状态分布、今日新增、未关闭资金类数量、今日强制免费金额。
  196. func (r *IncidentRepository) Stats() (IncidentStats, error) {
  197. var stats IncidentStats
  198. // 每次 Count 使用全新的查询构建器,避免 GORM Where 条件在复用实例上累积
  199. base := func() *gorm.DB {
  200. return global.GVA_DB.Model(&dao.IncidentRecord{})
  201. }
  202. if err := base().Where("status = ?", "pending").Count(&stats.Pending).Error; err != nil {
  203. return stats, err
  204. }
  205. if err := base().Where("status = ?", "processing").Count(&stats.Processing).Error; err != nil {
  206. return stats, err
  207. }
  208. if err := base().Where("status = ?", "resolved").Count(&stats.Resolved).Error; err != nil {
  209. return stats, err
  210. }
  211. if err := base().Where("status = ?", "closed").Count(&stats.Closed).Error; err != nil {
  212. return stats, err
  213. }
  214. if err := base().Where("created_at >= ?", startOfDay()).Count(&stats.TodayCreated).Error; err != nil {
  215. return stats, err
  216. }
  217. // 资金相关分类(支付失败/不确定、开闸失败、无入场记录出场)中未关闭的数量
  218. if err := base().Where("status IN (?, ?) AND category IN (?, ?, ?, ?)",
  219. "pending", "processing",
  220. "payment_failed", "payment_uncertain", "gate_failed", "no_entry_exit").
  221. Count(&stats.OpenFundRelated).Error; err != nil {
  222. return stats, err
  223. }
  224. // 今日强制免费金额合计
  225. if err := global.GVA_DB.Model(&dao.IncidentRecord{}).
  226. Where("handle_type = ? AND handled_at >= ?", "force_free", startOfDay()).
  227. Select("COALESCE(SUM(force_free_amount), 0)").Scan(&stats.TodayForceFreeAmount).Error; err != nil {
  228. return stats, err
  229. }
  230. return stats, nil
  231. }
  232. func startOfDay() time.Time {
  233. now := time.Now()
  234. return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
  235. }