passage.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. package parking
  2. import (
  3. "errors"
  4. "fmt"
  5. "sync"
  6. "time"
  7. "wails-app/internal/dao"
  8. "wails-app/internal/global"
  9. common "wails-app/internal/model/common"
  10. "wails-app/internal/model/vehicle/request"
  11. "wails-app/internal/service/vehicle"
  12. )
  13. // PassageService 统一的进出场服务(所有触发方式共用:RFID、摄像头、手动录入)
  14. type PassageService struct{}
  15. // ===================== 防抖与状态 =====================
  16. var (
  17. passageLastTime = make(map[string]int64)
  18. passageStatus = make(map[string]bool) // true=在场内
  19. passageMutex sync.Mutex
  20. passageDebounceSec = int64(3) // 3秒防抖
  21. )
  22. // getIdentifier 从请求中提取唯一标识(优先 RFID,其次车牌)
  23. func getIdentifier(req common.PassageRequest) string {
  24. if req.RFIDTag != "" {
  25. return req.RFIDTag
  26. }
  27. return req.PlateNumber
  28. }
  29. // HandlePassage 统一的进出场处理入口
  30. // 调用方只需传入车辆标识 + 设备编码,其余逻辑全部在此处理:
  31. //
  32. // 查询设备/通道 → 查询车辆 → 黑名单检查 → 临时车规则 → 进场/出场 → 开闸
  33. func (s *PassageService) HandlePassage(req common.PassageRequest) (*common.PassageResult, error) {
  34. identifier := getIdentifier(req)
  35. if identifier == "" {
  36. return nil, fmt.Errorf("缺少车辆标识(RFIDTag 或 PlateNumber)")
  37. }
  38. // ===================== 1. 防抖 =====================
  39. passageMutex.Lock()
  40. if last, ok := passageLastTime[identifier]; ok && time.Now().Unix()-last < passageDebounceSec {
  41. passageMutex.Unlock()
  42. return nil, fmt.Errorf("防抖中: %s", identifier)
  43. }
  44. passageLastTime[identifier] = time.Now().Unix()
  45. passageMutex.Unlock()
  46. // ===================== 2. 查询设备 + 通道 =====================
  47. var device dao.UHFReader
  48. if err := global.GVA_DB.Preload("Channel").First(&device, "device_code = ?", req.DeviceCode).Error; err != nil {
  49. return nil, fmt.Errorf("设备不存在: %s", req.DeviceCode)
  50. }
  51. if !device.IsActive {
  52. return nil, fmt.Errorf("设备已停用: %s", req.DeviceCode)
  53. }
  54. if device.Channel == nil {
  55. return nil, fmt.Errorf("设备未绑定通道: %s", req.DeviceCode)
  56. }
  57. channel := device.Channel
  58. direction := channel.Direction
  59. if direction == "inout" {
  60. if req.Direction != "in" && req.Direction != "out" {
  61. return nil, errors.New("双向通道必须明确本次通行方向")
  62. }
  63. direction = req.Direction
  64. } else if req.Direction != "" && req.Direction != direction {
  65. return nil, fmt.Errorf("所选设备属于%s通道,不能执行%s操作", direction, req.Direction)
  66. }
  67. allowTemporary := channel.AllowTemporary
  68. // ===================== 3. 查询车辆 =====================
  69. vehicleSvc := &vehicle.VehicleService{}
  70. shortlistSvc := &vehicle.ShortlistService{}
  71. v, err := vehicleSvc.GetVehicleByPlateNumber(req.PlateNumber, req.RFIDTag)
  72. isTempVehicle := false
  73. if err != nil || v.ID == 0 {
  74. isTempVehicle = true
  75. } else if v.VehicleType != nil {
  76. isTempVehicle = v.VehicleType.IsSystem
  77. } else {
  78. isTempVehicle = true
  79. }
  80. // ===================== 4. 黑名单检查 =====================
  81. isBlack, _ := shortlistSvc.CheckVehicleShortlist(req.PlateNumber, req.RFIDTag)
  82. if isBlack {
  83. fmt.Printf("🚫 禁止通行 | 车辆在黑名单中 | 设备:%s 车牌:%s RFID:%s\n", req.DeviceCode, req.PlateNumber, req.RFIDTag)
  84. return nil, fmt.Errorf("车辆在黑名单中")
  85. }
  86. // ===================== 5. 临时车规则 =====================
  87. if isTempVehicle && !allowTemporary {
  88. fmt.Printf("🚫 禁止通行 | 临时车不允许此通道 | 设备:%s 车牌:%s RFID:%s\n", req.DeviceCode, req.PlateNumber, req.RFIDTag)
  89. return nil, fmt.Errorf("临时车不允许此通道")
  90. }
  91. // ===================== 6. 根据方向处理 =====================
  92. switch direction {
  93. case "in":
  94. parkingLotID := channel.ParkingLotID
  95. if parkingLotID == 0 {
  96. parkingLotID = req.ParkingLotID
  97. }
  98. if parkingLotID == 0 {
  99. return nil, errors.New("通道未配置停车场")
  100. }
  101. if req.ParkingLotID != 0 && req.ParkingLotID != parkingLotID {
  102. return nil, errors.New("所选停车场与通道配置不一致")
  103. }
  104. return s.handleEntry(req, identifier, parkingLotID, channel, &device)
  105. case "out":
  106. return s.handleExit(req, identifier, channel, &device)
  107. default:
  108. return nil, fmt.Errorf("未知通道方向: %s", direction)
  109. }
  110. }
  111. // handleEntry 处理入场
  112. func (s *PassageService) handleEntry(req common.PassageRequest, identifier string, parkingLotID uint, channel *dao.Channel, device *dao.UHFReader) (*common.PassageResult, error) {
  113. passageMutex.Lock()
  114. canIn := !passageStatus[identifier]
  115. passageMutex.Unlock()
  116. if !canIn {
  117. return nil, fmt.Errorf("车辆已在场内: %s", identifier)
  118. }
  119. // 先创建停车会话和数字票,避免业务失败时车辆已经放行。
  120. vehicleSvc := &vehicle.VehicleService{}
  121. resp, err := vehicleSvc.VehicleEntry(request.VehicleEntry{
  122. PlateNumber: req.PlateNumber,
  123. RFIDTag: req.RFIDTag,
  124. ParkingLotID: parkingLotID,
  125. EntryImage: req.Image,
  126. EntryChannelID: channel.ID,
  127. EntryChannelCode: channel.ChannelCode,
  128. EntryChannelName: channel.ChannelName,
  129. EntryDeviceCode: req.DeviceCode,
  130. EntryDeviceName: device.DeviceName,
  131. TriggerMode: req.TriggerSource,
  132. })
  133. if err != nil {
  134. return nil, err
  135. }
  136. passageMutex.Lock()
  137. passageStatus[identifier] = true
  138. passageMutex.Unlock()
  139. result := &common.PassageResult{
  140. Success: true,
  141. Message: "入场成功",
  142. SessionID: resp.Data.SessionID,
  143. TicketID: resp.Data.TicketID,
  144. TicketNo: resp.Data.TicketNo,
  145. Direction: "in",
  146. PlateNumber: resp.Data.PlateNumber,
  147. RFIDTag: req.RFIDTag,
  148. EntryTime: resp.Data.EntryTime,
  149. BusinessCompleted: true,
  150. GateStatus: "opening",
  151. DeviceCode: req.DeviceCode,
  152. }
  153. fmt.Printf("🟢 入口开闸 | 设备:%s 车牌:%s RFID:%s\n", req.DeviceCode, req.PlateNumber, req.RFIDTag)
  154. if err := s.OpenGateByDeviceCode(req.DeviceCode, defaultGateValidTime); err != nil {
  155. result.Success = false
  156. result.Message = "入场记录已创建,但开闸失败"
  157. result.GateStatus = "failed"
  158. return result, gateOperationError("开闸", req.DeviceCode, err)
  159. }
  160. result.GateOpened = true
  161. result.GateStatus = "opened"
  162. return result, nil
  163. }
  164. // handleExit 处理出场(使用新的收费流程 ExitConfirm)
  165. func (s *PassageService) handleExit(req common.PassageRequest, identifier string, channel *dao.Channel, device *dao.UHFReader) (*common.PassageResult, error) {
  166. // UHF 自动出场默认走免密;人工入口会显式传入支付参数。
  167. paymentMethod := req.PaymentMethod
  168. if paymentMethod == "" {
  169. paymentMethod = "free"
  170. }
  171. vehicleSvc := &vehicle.VehicleService{}
  172. confirmReq := request.ExitConfirmRequest{
  173. SessionID: req.SessionID,
  174. TicketNo: req.TicketNo,
  175. PlateNumber: req.PlateNumber,
  176. RFIDTag: req.RFIDTag,
  177. PaymentEntry: req.PaymentEntry,
  178. PaymentMethod: paymentMethod,
  179. PaidAmount: req.PaidAmount,
  180. ExitChannelID: channel.ID,
  181. ExitChannelCode: channel.ChannelCode,
  182. ExitChannelName: channel.ChannelName,
  183. ExitDeviceCode: req.DeviceCode,
  184. ExitDeviceName: device.DeviceName,
  185. }
  186. resp, err := vehicleSvc.ExitConfirm(confirmReq, req.OperatorID)
  187. if err != nil {
  188. return nil, err
  189. }
  190. passageMutex.Lock()
  191. passageStatus[identifier] = false
  192. passageMutex.Unlock()
  193. result := &common.PassageResult{
  194. Success: true,
  195. Message: "出场成功",
  196. Direction: "out",
  197. PlateNumber: resp.PlateNumber,
  198. RFIDTag: req.RFIDTag,
  199. Fee: resp.Fee,
  200. StayTime: resp.StayTime,
  201. EntryTime: resp.EntryTime,
  202. ExitTime: resp.ExitTime,
  203. BusinessCompleted: true,
  204. GateStatus: "opening",
  205. DeviceCode: req.DeviceCode,
  206. }
  207. fmt.Printf("🔴 出口开闸 | 设备:%s 车牌:%s RFID:%s\n", req.DeviceCode, req.PlateNumber, req.RFIDTag)
  208. if err := s.OpenGateByDeviceCode(req.DeviceCode, defaultGateValidTime); err != nil {
  209. result.Success = false
  210. result.Message = "出场结算已完成,但开闸失败"
  211. result.GateStatus = "failed"
  212. return result, gateOperationError("开闸", req.DeviceCode, err)
  213. }
  214. result.GateOpened = true
  215. result.GateStatus = "opened"
  216. return result, nil
  217. }
  218. // SetEpcExit 手动重置离场状态
  219. func SetEpcExit(identifier string) {
  220. passageMutex.Lock()
  221. defer passageMutex.Unlock()
  222. passageStatus[identifier] = false
  223. fmt.Printf("🚙 手动重置离场 | 标识: %s\n", identifier)
  224. }