package parking import ( "errors" "fmt" "strings" "sync" "time" "wails-app/internal/dao" "wails-app/internal/global" common "wails-app/internal/model/common" "wails-app/internal/model/vehicle/request" incidentService "wails-app/internal/modules/incident/service" sessionService "wails-app/internal/modules/parking-session/service" "wails-app/internal/service/vehicle" ) // PassageService 统一的进出场服务(所有触发方式共用:RFID、摄像头、手动录入) type PassageService struct{} // ===================== 防抖与状态 ===================== var ( passageLastTime = make(map[string]int64) passageStatus = make(map[string]bool) // true=在场内 passageMutex sync.Mutex passageDebounceSec = int64(3) // 3秒防抖 ) // getIdentifier 从请求中提取唯一标识(优先 RFID,其次车牌) func getIdentifier(req common.PassageRequest) string { if req.RFIDTag != "" { return req.RFIDTag } return req.PlateNumber } // HandlePassage 统一的进出场处理入口 // 调用方只需传入车辆标识 + 设备编码,其余逻辑全部在此处理: // // 查询设备/通道 → 查询车辆 → 黑名单检查 → 临时车规则 → 进场/出场 → 开闸 func (s *PassageService) HandlePassage(req common.PassageRequest) (*common.PassageResult, error) { identifier := getIdentifier(req) if identifier == "" { return nil, fmt.Errorf("缺少车辆标识(RFIDTag 或 PlateNumber)") } // ===================== 1. 防抖 ===================== passageMutex.Lock() if last, ok := passageLastTime[identifier]; ok && time.Now().Unix()-last < passageDebounceSec { passageMutex.Unlock() return nil, fmt.Errorf("防抖中: %s", identifier) } passageLastTime[identifier] = time.Now().Unix() passageMutex.Unlock() // ===================== 2. 查询设备 + 通道 ===================== var device dao.UHFReader if err := global.GVA_DB.Preload("Channel").First(&device, "device_code = ?", req.DeviceCode).Error; err != nil { return nil, fmt.Errorf("设备不存在: %s", req.DeviceCode) } if !device.IsActive { return nil, fmt.Errorf("设备已停用: %s", req.DeviceCode) } if device.Channel == nil { return nil, fmt.Errorf("设备未绑定通道: %s", req.DeviceCode) } channel := device.Channel direction := channel.Direction if direction == "inout" { if req.Direction != "in" && req.Direction != "out" { return nil, errors.New("双向通道必须明确本次通行方向") } direction = req.Direction } else if req.Direction != "" && req.Direction != direction { return nil, fmt.Errorf("所选设备属于%s通道,不能执行%s操作", direction, req.Direction) } allowTemporary := channel.AllowTemporary // ===================== 3. 查询车辆 ===================== vehicleSvc := &vehicle.VehicleService{} shortlistSvc := &vehicle.ShortlistService{} v, err := vehicleSvc.GetVehicleByPlateNumber(req.PlateNumber, req.RFIDTag) isTempVehicle := false if err != nil || v.ID == 0 { isTempVehicle = true } else if v.VehicleType != nil { isTempVehicle = v.VehicleType.IsSystem } else { isTempVehicle = true } // ===================== 4. 黑名单检查 ===================== isBlack, _ := shortlistSvc.CheckVehicleShortlist(req.PlateNumber, req.RFIDTag) if isBlack { fmt.Printf("🚫 禁止通行 | 车辆在黑名单中 | 设备:%s 车牌:%s RFID:%s\n", req.DeviceCode, req.PlateNumber, req.RFIDTag) incidentSvc.RecordIncident(incidentService.RecordIncidentRequest{ Category: incidentService.CategoryBlacklist, Source: incidentService.SourcePassage, PlateNumber: req.PlateNumber, RFIDTag: req.RFIDTag, ParkingLotID: channel.ParkingLotID, ChannelID: channel.ID, ChannelCode: channel.ChannelCode, DeviceCode: req.DeviceCode, OperatorID: req.OperatorID, Description: "黑名单车辆通行拦截", }) return nil, fmt.Errorf("车辆在黑名单中") } // ===================== 5. 临时车规则 ===================== if isTempVehicle && !allowTemporary { fmt.Printf("🚫 禁止通行 | 临时车不允许此通道 | 设备:%s 车牌:%s RFID:%s\n", req.DeviceCode, req.PlateNumber, req.RFIDTag) return nil, fmt.Errorf("临时车不允许此通道") } // ===================== 6. 根据方向处理 ===================== switch direction { case "in": parkingLotID := channel.ParkingLotID if parkingLotID == 0 { parkingLotID = req.ParkingLotID } if parkingLotID == 0 { return nil, errors.New("通道未配置停车场") } if req.ParkingLotID != 0 && req.ParkingLotID != parkingLotID { return nil, errors.New("所选停车场与通道配置不一致") } return s.handleEntry(req, identifier, parkingLotID, channel, &device) case "out": return s.handleExit(req, identifier, channel, &device) default: return nil, fmt.Errorf("未知通道方向: %s", direction) } } // handleEntry 处理入场 func (s *PassageService) handleEntry(req common.PassageRequest, identifier string, parkingLotID uint, channel *dao.Channel, device *dao.UHFReader) (*common.PassageResult, error) { passageMutex.Lock() canIn := !passageStatus[identifier] passageMutex.Unlock() if !canIn { incidentSvc.RecordIncident(incidentService.RecordIncidentRequest{ Category: incidentService.CategoryDuplicateEntry, Source: incidentService.SourcePassage, PlateNumber: req.PlateNumber, RFIDTag: req.RFIDTag, ParkingLotID: parkingLotID, ChannelID: channel.ID, ChannelCode: channel.ChannelCode, DeviceCode: req.DeviceCode, OperatorID: req.OperatorID, Description: "车辆已在场内,重复入场拦截", }) return nil, fmt.Errorf("车辆已在场内: %s", identifier) } // 先创建停车会话和数字票,避免业务失败时车辆已经放行。 vehicleSvc := &vehicle.VehicleService{} resp, err := vehicleSvc.VehicleEntry(request.VehicleEntry{ PlateNumber: req.PlateNumber, RFIDTag: req.RFIDTag, ParkingLotID: parkingLotID, EntryImage: req.Image, EntryChannelID: channel.ID, EntryChannelCode: channel.ChannelCode, EntryChannelName: channel.ChannelName, EntryDeviceCode: req.DeviceCode, EntryDeviceName: device.DeviceName, TriggerMode: req.TriggerSource, }) if err != nil { if errors.Is(err, sessionService.ErrParkingLotFull) { incidentSvc.RecordIncident(incidentService.RecordIncidentRequest{ Category: incidentService.CategoryLotFull, Source: incidentService.SourcePassage, PlateNumber: req.PlateNumber, RFIDTag: req.RFIDTag, ParkingLotID: parkingLotID, ChannelID: channel.ID, ChannelCode: channel.ChannelCode, DeviceCode: req.DeviceCode, OperatorID: req.OperatorID, Description: "停车场满位,入场被拒绝", Detail: err.Error(), }) } return nil, err } passageMutex.Lock() passageStatus[identifier] = true passageMutex.Unlock() result := &common.PassageResult{ Success: true, Message: "入场成功", SessionID: resp.Data.SessionID, TicketID: resp.Data.TicketID, TicketNo: resp.Data.TicketNo, Direction: "in", PlateNumber: resp.Data.PlateNumber, RFIDTag: req.RFIDTag, EntryTime: resp.Data.EntryTime, BusinessCompleted: true, GateStatus: "opening", DeviceCode: req.DeviceCode, } fmt.Printf("🟢 入口开闸 | 设备:%s 车牌:%s RFID:%s\n", req.DeviceCode, req.PlateNumber, req.RFIDTag) commandLogID, err := s.OpenGateWithContext(GateCommandContext{ Source: incidentService.SourcePassage, SessionID: resp.Data.SessionID, }, req.DeviceCode, defaultGateValidTime) if err != nil { result.Success = false result.Message = "入场记录已创建,但开闸失败" result.GateStatus = "failed" incidentID := incidentSvc.RecordIncident(incidentService.RecordIncidentRequest{ Category: incidentService.CategoryGateFailed, Source: incidentService.SourcePassage, VehicleRecordID: resp.Data.SessionID, TicketNo: resp.Data.TicketNo, PlateNumber: resp.Data.PlateNumber, RFIDTag: req.RFIDTag, ParkingLotID: parkingLotID, ChannelID: channel.ID, ChannelCode: channel.ChannelCode, DeviceCode: req.DeviceCode, OperatorID: req.OperatorID, Description: "入场记录已创建,但开闸失败", Detail: err.Error(), }) incidentSvc.LinkIncidentToCommand(commandLogID, incidentID) return result, gateOperationError("开闸", req.DeviceCode, err) } result.GateOpened = true result.GateStatus = "opened" return result, nil } // handleExit 处理出场(使用新的收费流程 ExitConfirm) func (s *PassageService) handleExit(req common.PassageRequest, identifier string, channel *dao.Channel, device *dao.UHFReader) (*common.PassageResult, error) { // UHF 自动出场默认走免密;人工入口会显式传入支付参数。 paymentMethod := req.PaymentMethod if paymentMethod == "" { paymentMethod = "free" } vehicleSvc := &vehicle.VehicleService{} confirmReq := request.ExitConfirmRequest{ SessionID: req.SessionID, TicketNo: req.TicketNo, PlateNumber: req.PlateNumber, RFIDTag: req.RFIDTag, PaymentEntry: req.PaymentEntry, PaymentMethod: paymentMethod, PaidAmount: req.PaidAmount, ExitChannelID: channel.ID, ExitChannelCode: channel.ChannelCode, ExitChannelName: channel.ChannelName, ExitDeviceCode: req.DeviceCode, ExitDeviceName: device.DeviceName, } resp, err := vehicleSvc.ExitConfirm(confirmReq, req.OperatorID) if err != nil { // 无入场记录/丢票出场(resolveExitRecordTx 将所有定位失败统一包装为"车辆未入场") if strings.Contains(err.Error(), "车辆未入场") { incidentSvc.RecordIncident(incidentService.RecordIncidentRequest{ Category: incidentService.CategoryNoEntryExit, Source: incidentService.SourcePassage, TicketNo: req.TicketNo, PlateNumber: req.PlateNumber, RFIDTag: req.RFIDTag, ParkingLotID: channel.ParkingLotID, ChannelID: channel.ID, ChannelCode: channel.ChannelCode, DeviceCode: req.DeviceCode, OperatorID: req.OperatorID, Description: "无入场记录出场或票据不存在", Detail: err.Error(), }) } return nil, err } passageMutex.Lock() passageStatus[identifier] = false passageMutex.Unlock() result := &common.PassageResult{ Success: true, Message: "出场成功", Direction: "out", PlateNumber: resp.PlateNumber, RFIDTag: req.RFIDTag, Fee: resp.Fee, StayTime: resp.StayTime, EntryTime: resp.EntryTime, ExitTime: resp.ExitTime, BusinessCompleted: true, GateStatus: "opening", DeviceCode: req.DeviceCode, } fmt.Printf("🔴 出口开闸 | 设备:%s 车牌:%s RFID:%s\n", req.DeviceCode, req.PlateNumber, req.RFIDTag) commandLogID, err := s.OpenGateWithContext(GateCommandContext{ Source: incidentService.SourcePassage, SessionID: req.SessionID, }, req.DeviceCode, defaultGateValidTime) if err != nil { result.Success = false result.Message = "出场结算已完成,但开闸失败" result.GateStatus = "failed" incidentID := incidentSvc.RecordIncident(incidentService.RecordIncidentRequest{ Category: incidentService.CategoryGateFailed, Source: incidentService.SourcePassage, VehicleRecordID: req.SessionID, TicketNo: req.TicketNo, PlateNumber: resp.PlateNumber, RFIDTag: req.RFIDTag, ParkingLotID: channel.ParkingLotID, ChannelID: channel.ID, ChannelCode: channel.ChannelCode, DeviceCode: req.DeviceCode, OperatorID: req.OperatorID, Description: "出场结算已完成,但开闸失败", Detail: err.Error(), }) incidentSvc.LinkIncidentToCommand(commandLogID, incidentID) return result, gateOperationError("开闸", req.DeviceCode, err) } result.GateOpened = true result.GateStatus = "opened" return result, nil } // SetEpcExit 手动重置离场状态 func SetEpcExit(identifier string) { passageMutex.Lock() defer passageMutex.Unlock() passageStatus[identifier] = false fmt.Printf("🚙 手动重置离场 | 标识: %s\n", identifier) }