passage.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. package parking
  2. import (
  3. "errors"
  4. "fmt"
  5. "strings"
  6. "sync"
  7. "time"
  8. "wails-app/internal/dao"
  9. "wails-app/internal/global"
  10. common "wails-app/internal/model/common"
  11. "wails-app/internal/model/vehicle/request"
  12. incidentService "wails-app/internal/modules/incident/service"
  13. sessionRequest "wails-app/internal/modules/parking-session/model/request"
  14. sessionService "wails-app/internal/modules/parking-session/service"
  15. "wails-app/internal/service/vehicle"
  16. )
  17. // PassageService 统一的进出场服务(所有触发方式共用:RFID、摄像头、手动录入)
  18. type PassageService struct{}
  19. // ===================== 防抖与状态 =====================
  20. var (
  21. passageLastTime = make(map[string]int64)
  22. passageStatus = make(map[string]bool) // true=在场内
  23. passageMutex sync.Mutex
  24. passageDebounceSec = int64(3) // 3秒防抖
  25. )
  26. // getIdentifier 从请求中提取唯一标识(优先 RFID,其次车牌)
  27. func getIdentifier(req common.PassageRequest) string {
  28. if req.RFIDTag != "" {
  29. return req.RFIDTag
  30. }
  31. return req.PlateNumber
  32. }
  33. // HandlePassage 统一的进出场处理入口
  34. // 调用方只需传入车辆标识 + 设备编码,其余逻辑全部在此处理:
  35. //
  36. // 查询设备/通道 → 查询车辆 → 黑名单检查 → 临时车规则 → 进场/出场 → 开闸
  37. func (s *PassageService) HandlePassage(req common.PassageRequest) (*common.PassageResult, error) {
  38. identifier := getIdentifier(req)
  39. if identifier == "" {
  40. return nil, fmt.Errorf("缺少车辆标识(RFIDTag 或 PlateNumber)")
  41. }
  42. // ===================== 1. 防抖 =====================
  43. passageMutex.Lock()
  44. if last, ok := passageLastTime[identifier]; ok && time.Now().Unix()-last < passageDebounceSec {
  45. passageMutex.Unlock()
  46. return nil, fmt.Errorf("防抖中: %s", identifier)
  47. }
  48. passageLastTime[identifier] = time.Now().Unix()
  49. passageMutex.Unlock()
  50. // ===================== 2. 查询设备 + 通道 =====================
  51. var device dao.UHFReader
  52. if err := global.GVA_DB.Preload("Channel").First(&device, "device_code = ?", req.DeviceCode).Error; err != nil {
  53. return nil, fmt.Errorf("设备不存在: %s", req.DeviceCode)
  54. }
  55. if !device.IsActive {
  56. return nil, fmt.Errorf("设备已停用: %s", req.DeviceCode)
  57. }
  58. if device.Channel == nil {
  59. return nil, fmt.Errorf("设备未绑定通道: %s", req.DeviceCode)
  60. }
  61. channel := device.Channel
  62. direction := channel.Direction
  63. if direction == "inout" {
  64. if req.Direction == "in" || req.Direction == "out" {
  65. direction = req.Direction
  66. } else if inferred, inferErr := s.inferInoutDirection(req); inferErr != nil {
  67. // 单一通道方向推断失败(如重复读卡):按失败事件处理,出口屏/工作台可见原因
  68. return nil, inferErr
  69. } else {
  70. direction = inferred
  71. }
  72. } else if req.Direction != "" && req.Direction != direction {
  73. return nil, fmt.Errorf("所选设备属于%s通道,不能执行%s操作", direction, req.Direction)
  74. }
  75. allowTemporary := channel.AllowTemporary
  76. // ===================== 3. 查询车辆 =====================
  77. vehicleSvc := &vehicle.VehicleService{}
  78. shortlistSvc := &vehicle.ShortlistService{}
  79. v, err := vehicleSvc.GetVehicleByPlateNumber(req.PlateNumber, req.RFIDTag)
  80. isTempVehicle := false
  81. if err != nil || v.ID == 0 {
  82. isTempVehicle = true
  83. } else if v.VehicleType != nil {
  84. isTempVehicle = v.VehicleType.IsSystem
  85. } else {
  86. isTempVehicle = true
  87. }
  88. // ===================== 4. 黑名单检查 =====================
  89. isBlack, _ := shortlistSvc.CheckVehicleShortlist(req.PlateNumber, req.RFIDTag)
  90. if isBlack {
  91. fmt.Printf("🚫 禁止通行 | 车辆在黑名单中 | 设备:%s 车牌:%s RFID:%s\n", req.DeviceCode, req.PlateNumber, req.RFIDTag)
  92. incidentSvc.RecordIncident(incidentService.RecordIncidentRequest{
  93. Category: incidentService.CategoryBlacklist,
  94. Source: incidentService.SourcePassage,
  95. PlateNumber: req.PlateNumber,
  96. RFIDTag: req.RFIDTag,
  97. ParkingLotID: channel.ParkingLotID,
  98. ChannelID: channel.ID,
  99. ChannelCode: channel.ChannelCode,
  100. DeviceCode: req.DeviceCode,
  101. OperatorID: req.OperatorID,
  102. Description: "黑名单车辆通行拦截",
  103. })
  104. return nil, fmt.Errorf("车辆在黑名单中")
  105. }
  106. // ===================== 5. 临时车规则 =====================
  107. if isTempVehicle && !allowTemporary {
  108. fmt.Printf("🚫 禁止通行 | 临时车不允许此通道 | 设备:%s 车牌:%s RFID:%s\n", req.DeviceCode, req.PlateNumber, req.RFIDTag)
  109. return nil, fmt.Errorf("临时车不允许此通道")
  110. }
  111. // ===================== 6. 根据方向处理 =====================
  112. switch direction {
  113. case "in":
  114. parkingLotID := channel.ParkingLotID
  115. if parkingLotID == 0 {
  116. parkingLotID = req.ParkingLotID
  117. }
  118. if parkingLotID == 0 {
  119. return nil, errors.New("通道未配置停车场")
  120. }
  121. if req.ParkingLotID != 0 && req.ParkingLotID != parkingLotID {
  122. return nil, errors.New("所选停车场与通道配置不一致")
  123. }
  124. return s.handleEntry(req, identifier, parkingLotID, channel, &device)
  125. case "out":
  126. return s.handleExit(req, identifier, channel, &device)
  127. default:
  128. return nil, fmt.Errorf("未知通道方向: %s", direction)
  129. }
  130. }
  131. // inferInoutDirection 单一通道(inout)方向推断:单一通道只装一个读卡器,管进也管出,
  132. // 事件本身不带方向,由业务状态决定——无在场会话即进场,已有在场会话即出场。
  133. // 防误判:入场后不足 system.inout-min-stay-minutes 的再次读卡按重复读卡忽略,
  134. // 避免车还没驶离读卡区就被推断成出场。显式携带方向的请求不会走到这里。
  135. func (s *PassageService) inferInoutDirection(req common.PassageRequest) (string, error) {
  136. sessionSvc := sessionService.NewParkingSessionService()
  137. record, err := sessionSvc.ResolveActiveSession(sessionRequest.ResolveActiveSessionRequest{
  138. PlateNumber: req.PlateNumber,
  139. RFIDTag: req.RFIDTag,
  140. })
  141. if errors.Is(err, sessionService.ErrSessionNotFound) {
  142. return "in", nil
  143. }
  144. if err != nil {
  145. return "", fmt.Errorf("单一通道方向推断失败: %w", err)
  146. }
  147. minStay := time.Duration(global.GVA_CONFIG.System.InoutMinStayMinutes) * time.Minute
  148. if minStay <= 0 {
  149. minStay = time.Minute
  150. }
  151. stay := time.Since(record.EntryTime)
  152. if stay < minStay {
  153. return "", fmt.Errorf("单一通道重复读卡忽略: 入场后需满 %v 才触发出场(当前 %v)",
  154. minStay, stay.Round(time.Second))
  155. }
  156. return "out", nil
  157. }
  158. // handleEntry 处理入场
  159. func (s *PassageService) handleEntry(req common.PassageRequest, identifier string, parkingLotID uint, channel *dao.Channel, device *dao.UHFReader) (*common.PassageResult, error) {
  160. passageMutex.Lock()
  161. canIn := !passageStatus[identifier]
  162. passageMutex.Unlock()
  163. if !canIn {
  164. incidentSvc.RecordIncident(incidentService.RecordIncidentRequest{
  165. Category: incidentService.CategoryDuplicateEntry,
  166. Source: incidentService.SourcePassage,
  167. PlateNumber: req.PlateNumber,
  168. RFIDTag: req.RFIDTag,
  169. ParkingLotID: parkingLotID,
  170. ChannelID: channel.ID,
  171. ChannelCode: channel.ChannelCode,
  172. DeviceCode: req.DeviceCode,
  173. OperatorID: req.OperatorID,
  174. Description: "车辆已在场内,重复入场拦截",
  175. })
  176. return nil, fmt.Errorf("车辆已在场内: %s", identifier)
  177. }
  178. // 先创建停车会话和数字票,避免业务失败时车辆已经放行。
  179. vehicleSvc := &vehicle.VehicleService{}
  180. resp, err := vehicleSvc.VehicleEntry(request.VehicleEntry{
  181. PlateNumber: req.PlateNumber,
  182. RFIDTag: req.RFIDTag,
  183. ParkingLotID: parkingLotID,
  184. EntryImage: req.Image,
  185. EntryChannelID: channel.ID,
  186. EntryChannelCode: channel.ChannelCode,
  187. EntryChannelName: channel.ChannelName,
  188. EntryDeviceCode: req.DeviceCode,
  189. EntryDeviceName: device.DeviceName,
  190. TriggerMode: req.TriggerSource,
  191. })
  192. if err != nil {
  193. if errors.Is(err, sessionService.ErrParkingLotFull) {
  194. incidentSvc.RecordIncident(incidentService.RecordIncidentRequest{
  195. Category: incidentService.CategoryLotFull,
  196. Source: incidentService.SourcePassage,
  197. PlateNumber: req.PlateNumber,
  198. RFIDTag: req.RFIDTag,
  199. ParkingLotID: parkingLotID,
  200. ChannelID: channel.ID,
  201. ChannelCode: channel.ChannelCode,
  202. DeviceCode: req.DeviceCode,
  203. OperatorID: req.OperatorID,
  204. Description: "停车场满位,入场被拒绝",
  205. Detail: err.Error(),
  206. })
  207. }
  208. return nil, err
  209. }
  210. passageMutex.Lock()
  211. passageStatus[identifier] = true
  212. passageMutex.Unlock()
  213. result := &common.PassageResult{
  214. Success: true,
  215. Message: "入场成功",
  216. SessionID: resp.Data.SessionID,
  217. TicketID: resp.Data.TicketID,
  218. TicketNo: resp.Data.TicketNo,
  219. Direction: "in",
  220. PlateNumber: resp.Data.PlateNumber,
  221. RFIDTag: req.RFIDTag,
  222. EntryTime: resp.Data.EntryTime,
  223. BusinessCompleted: true,
  224. GateStatus: "opening",
  225. DeviceCode: req.DeviceCode,
  226. }
  227. fmt.Printf("🟢 入口开闸 | 设备:%s 车牌:%s RFID:%s\n", req.DeviceCode, req.PlateNumber, req.RFIDTag)
  228. commandLogID, err := s.OpenGateWithContext(GateCommandContext{
  229. Source: incidentService.SourcePassage,
  230. SessionID: resp.Data.SessionID,
  231. }, req.DeviceCode, defaultGateValidTime)
  232. if err != nil {
  233. result.Success = false
  234. result.Message = "入场记录已创建,但开闸失败"
  235. result.GateStatus = "failed"
  236. incidentID := incidentSvc.RecordIncident(incidentService.RecordIncidentRequest{
  237. Category: incidentService.CategoryGateFailed,
  238. Source: incidentService.SourcePassage,
  239. VehicleRecordID: resp.Data.SessionID,
  240. TicketNo: resp.Data.TicketNo,
  241. PlateNumber: resp.Data.PlateNumber,
  242. RFIDTag: req.RFIDTag,
  243. ParkingLotID: parkingLotID,
  244. ChannelID: channel.ID,
  245. ChannelCode: channel.ChannelCode,
  246. DeviceCode: req.DeviceCode,
  247. OperatorID: req.OperatorID,
  248. Description: "入场记录已创建,但开闸失败",
  249. Detail: err.Error(),
  250. })
  251. incidentSvc.LinkIncidentToCommand(commandLogID, incidentID)
  252. return result, gateOperationError("开闸", req.DeviceCode, err)
  253. }
  254. result.GateOpened = true
  255. result.GateStatus = "opened"
  256. return result, nil
  257. }
  258. // handleExit 处理出场(使用新的收费流程 ExitConfirm)
  259. func (s *PassageService) handleExit(req common.PassageRequest, identifier string, channel *dao.Channel, device *dao.UHFReader) (*common.PassageResult, error) {
  260. // UHF 自动出场默认走免密;人工入口会显式传入支付参数。
  261. paymentMethod := req.PaymentMethod
  262. if paymentMethod == "" {
  263. paymentMethod = "free"
  264. }
  265. vehicleSvc := &vehicle.VehicleService{}
  266. confirmReq := request.ExitConfirmRequest{
  267. SessionID: req.SessionID,
  268. TicketNo: req.TicketNo,
  269. PlateNumber: req.PlateNumber,
  270. RFIDTag: req.RFIDTag,
  271. PaymentEntry: req.PaymentEntry,
  272. PaymentMethod: paymentMethod,
  273. PaidAmount: req.PaidAmount,
  274. ExitChannelID: channel.ID,
  275. ExitChannelCode: channel.ChannelCode,
  276. ExitChannelName: channel.ChannelName,
  277. ExitDeviceCode: req.DeviceCode,
  278. ExitDeviceName: device.DeviceName,
  279. TriggerSource: req.TriggerSource,
  280. }
  281. resp, err := vehicleSvc.ExitConfirm(confirmReq, req.OperatorID)
  282. if err != nil {
  283. // 无入场记录/丢票出场(resolveExitRecordTx 将所有定位失败统一包装为"车辆未入场")
  284. if strings.Contains(err.Error(), "车辆未入场") {
  285. incidentSvc.RecordIncident(incidentService.RecordIncidentRequest{
  286. Category: incidentService.CategoryNoEntryExit,
  287. Source: incidentService.SourcePassage,
  288. TicketNo: req.TicketNo,
  289. PlateNumber: req.PlateNumber,
  290. RFIDTag: req.RFIDTag,
  291. ParkingLotID: channel.ParkingLotID,
  292. ChannelID: channel.ID,
  293. ChannelCode: channel.ChannelCode,
  294. DeviceCode: req.DeviceCode,
  295. OperatorID: req.OperatorID,
  296. Description: "无入场记录出场或票据不存在",
  297. Detail: err.Error(),
  298. })
  299. }
  300. return nil, err
  301. }
  302. passageMutex.Lock()
  303. passageStatus[identifier] = false
  304. passageMutex.Unlock()
  305. result := &common.PassageResult{
  306. Success: true,
  307. Message: "出场成功",
  308. SessionID: resp.SessionID,
  309. Direction: "out",
  310. PlateNumber: resp.PlateNumber,
  311. RFIDTag: req.RFIDTag,
  312. Fee: resp.Fee,
  313. StayTime: resp.StayTime,
  314. EntryTime: resp.EntryTime,
  315. ExitTime: resp.ExitTime,
  316. BusinessCompleted: true,
  317. GateStatus: "opening",
  318. DeviceCode: req.DeviceCode,
  319. }
  320. fmt.Printf("🔴 出口开闸 | 设备:%s 车牌:%s RFID:%s\n", req.DeviceCode, req.PlateNumber, req.RFIDTag)
  321. commandLogID, err := s.OpenGateWithContext(GateCommandContext{
  322. Source: incidentService.SourcePassage,
  323. SessionID: resp.SessionID,
  324. }, req.DeviceCode, defaultGateValidTime)
  325. if err != nil {
  326. result.Success = false
  327. result.Message = "出场结算已完成,但开闸失败"
  328. result.GateStatus = "failed"
  329. incidentID := incidentSvc.RecordIncident(incidentService.RecordIncidentRequest{
  330. Category: incidentService.CategoryGateFailed,
  331. Source: incidentService.SourcePassage,
  332. VehicleRecordID: resp.SessionID,
  333. TicketNo: req.TicketNo,
  334. PlateNumber: resp.PlateNumber,
  335. RFIDTag: req.RFIDTag,
  336. ParkingLotID: channel.ParkingLotID,
  337. ChannelID: channel.ID,
  338. ChannelCode: channel.ChannelCode,
  339. DeviceCode: req.DeviceCode,
  340. OperatorID: req.OperatorID,
  341. Description: "出场结算已完成,但开闸失败",
  342. Detail: err.Error(),
  343. })
  344. incidentSvc.LinkIncidentToCommand(commandLogID, incidentID)
  345. return result, gateOperationError("开闸", req.DeviceCode, err)
  346. }
  347. result.GateOpened = true
  348. result.GateStatus = "opened"
  349. return result, nil
  350. }
  351. // SetEpcExit 手动重置离场状态
  352. func SetEpcExit(identifier string) {
  353. passageMutex.Lock()
  354. defer passageMutex.Unlock()
  355. passageStatus[identifier] = false
  356. fmt.Printf("🚙 手动重置离场 | 标识: %s\n", identifier)
  357. }