Quellcode durchsuchen

refactor: 抽取进出场公共逻辑为PassageService——RFID/摄像头/手动共用同一入口

lq vor 1 Monat
Ursprung
Commit
688b06063c

+ 22 - 0
internal/model/common/passage.go

@@ -0,0 +1,22 @@
+package common
+
+// PassageRequest 统一的进出场请求(所有触发方式共用:RFID、摄像头、手动录入)
+type PassageRequest struct {
+	RFIDTag     string // RFID 标签(UHF 读卡器触发时)
+	PlateNumber string // 车牌号(摄像头/手动触发时)
+	DeviceCode  string // UHF 设备编码(用于定位通道 + 控制道闸)
+	Image       string // 抓拍图片路径
+}
+
+// PassageResult 进出场结果
+type PassageResult struct {
+	Success     bool    `json:"success"`
+	Message     string  `json:"message"`
+	Direction   string  `json:"direction"`    // in / out
+	PlateNumber string  `json:"plate_number"` // 车牌号
+	RFIDTag     string  `json:"rfid_tag"`     // RFID 标签
+	Fee         float64 `json:"fee"`          // 出场费用(入场时为0)
+	StayTime    int64   `json:"stay_time"`    // 停留时长(分钟)
+	EntryTime   int64   `json:"entry_time"`   // 入场时间戳
+	ExitTime    int64   `json:"exit_time"`    // 出场时间戳
+}

+ 2 - 0
internal/service/enter.go

@@ -2,6 +2,7 @@ package service
 
 import (
 	"wails-app/internal/service/example"
+	"wails-app/internal/service/parking"
 	"wails-app/internal/service/system"
 	"wails-app/internal/service/vehicle"
 )
@@ -10,6 +11,7 @@ type ServiceGroup struct {
 	SystemServiceGroup  system.ServiceGroup
 	ExampleServiceGroup example.ServiceGroup
 	VehicleServiceGroup vehicle.ServiceGroup
+	ParkingServiceGroup parking.ServiceGroup
 }
 
 var ServiceGroupApp = new(ServiceGroup)

+ 5 - 0
internal/service/parking/enter.go

@@ -0,0 +1,5 @@
+package parking
+
+type ServiceGroup struct {
+	PassageService
+}

+ 192 - 0
internal/service/parking/passage.go

@@ -0,0 +1,192 @@
+package parking
+
+import (
+	"fmt"
+	"sync"
+	"time"
+	"wails-app/internal/dao"
+	"wails-app/internal/global"
+	common "wails-app/internal/model/common"
+	"wails-app/internal/model/vehicle/request"
+	"wails-app/internal/service/vehicle"
+)
+
+// GateOpener 道闸控制函数(由 UHF 模块在初始化时注入,避免循环依赖)
+var GateOpener func(deviceCode string, validTime byte) error
+
+// 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)
+	}
+	channel := device.Channel
+	direction := channel.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.VehicleTypeID == 1) // 1=临时车
+	} else {
+		isTempVehicle = true
+	}
+
+	// ===================== 4. 黑名单检查 =====================
+	isBlack, _ := shortlistSvc.CheckVehicleShortlist(req.RFIDTag)
+	if isBlack {
+		fmt.Printf("🚫 禁止通行 | 车辆在黑名单中 | 设备:%s 车牌:%s RFID:%s\n", req.DeviceCode, req.PlateNumber, req.RFIDTag)
+		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":
+		return s.handleEntry(req, identifier, channel.ParkingLotID)
+	case "out":
+		return s.handleExit(req, identifier)
+	default:
+		return nil, fmt.Errorf("未知通道方向: %s", direction)
+	}
+}
+
+// handleEntry 处理入场
+func (s *PassageService) handleEntry(req common.PassageRequest, identifier string, parkingLotID uint) (*common.PassageResult, error) {
+	passageMutex.Lock()
+	canIn := !passageStatus[identifier]
+	passageMutex.Unlock()
+
+	if !canIn {
+		return nil, fmt.Errorf("车辆已在场内: %s", identifier)
+	}
+
+	fmt.Printf("🟢 入口开闸 | 设备:%s 车牌:%s RFID:%s\n", req.DeviceCode, req.PlateNumber, req.RFIDTag)
+
+	// 开闸
+	if GateOpener != nil {
+		if err := GateOpener(req.DeviceCode, 2); err != nil {
+			fmt.Printf("⚠️ 开闸失败: %v\n", err)
+		}
+	}
+
+	// 执行入场
+	vehicleSvc := &vehicle.VehicleService{}
+	resp, err := vehicleSvc.VehicleEntry(request.VehicleEntry{
+		PlateNumber:  req.PlateNumber,
+		RFIDTag:      req.RFIDTag,
+		ParkingLotID: parkingLotID,
+		EntryImage:   req.Image,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	passageMutex.Lock()
+	passageStatus[identifier] = true
+	passageMutex.Unlock()
+
+	return &common.PassageResult{
+		Success:     true,
+		Message:     "入场成功",
+		Direction:   "in",
+		PlateNumber: resp.Data.PlateNumber,
+		RFIDTag:     req.RFIDTag,
+		EntryTime:   resp.Data.EntryTime,
+	}, nil
+}
+
+// handleExit 处理出场
+func (s *PassageService) handleExit(req common.PassageRequest, identifier string) (*common.PassageResult, error) {
+	fmt.Printf("🔴 出口开闸 | 设备:%s 车牌:%s RFID:%s\n", req.DeviceCode, req.PlateNumber, req.RFIDTag)
+
+	// 开闸
+	if GateOpener != nil {
+		if err := GateOpener(req.DeviceCode, 2); err != nil {
+			fmt.Printf("⚠️ 开闸失败: %v\n", err)
+		}
+	}
+
+	// 执行出场
+	vehicleSvc := &vehicle.VehicleService{}
+	resp, err := vehicleSvc.VehicleExit(request.VehicleExit{
+		PlateNumber: req.PlateNumber,
+		RFIDTag:     req.RFIDTag,
+		ExitImage:   req.Image,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	passageMutex.Lock()
+	passageStatus[identifier] = false
+	passageMutex.Unlock()
+
+	return &common.PassageResult{
+		Success:     true,
+		Message:     "出场成功",
+		Direction:   "out",
+		PlateNumber: req.PlateNumber,
+		RFIDTag:     req.RFIDTag,
+		Fee:         resp.Data.Fee,
+		StayTime:    resp.Data.StayTime,
+		EntryTime:   resp.Data.EntryTime,
+		ExitTime:    resp.Data.ExitTime,
+	}, nil
+}
+
+// SetEpcExit 手动重置离场状态
+func SetEpcExit(identifier string) {
+	passageMutex.Lock()
+	defer passageMutex.Unlock()
+	passageStatus[identifier] = false
+	fmt.Printf("🚙 手动重置离场 | 标识: %s\n", identifier)
+}

+ 29 - 112
internal/service/uhf/reader.go

@@ -8,8 +8,9 @@ import (
 	"fmt"
 	"wails-app/internal/dao"
 	"wails-app/internal/global"
-	"wails-app/internal/model/vehicle/request"
+	common "wails-app/internal/model/common"
 	"wails-app/internal/service"
+	"wails-app/internal/service/parking"
 	"sync"
 	"time"
 )
@@ -44,6 +45,11 @@ var DeviceManager = &Manager{
 	devices: make(map[string]*DeviceHandler),
 }
 
+// init 注入道闸控制回调(避免循环依赖)
+func init() {
+	parking.GateOpener = DeviceManager.OpenGateByDeviceCode
+}
+
 type Manager struct {
 	mu      sync.RWMutex
 	devices map[string]*DeviceHandler
@@ -81,6 +87,20 @@ func (m *Manager) Get(code string) (*DeviceHandler, bool) {
 	return h, ok
 }
 
+// OpenGate 开闸(公开方法,供外部触发源调用,如摄像头识别、手动进出场)
+func (h *DeviceHandler) OpenGate(validTime byte) error {
+	return h.reader.CloseRelay1(validTime)
+}
+
+// OpenGateByDeviceCode 通过设备编码直接开闸(便捷方法)
+func (m *Manager) OpenGateByDeviceCode(deviceCode string, validTime byte) error {
+	h, ok := m.Get(deviceCode)
+	if !ok {
+		return fmt.Errorf("设备未连接: %s", deviceCode)
+	}
+	return h.OpenGate(validTime)
+}
+
 // ===================== 复用原有CRC和解析函数(无需修改)=====================
 func uiCrc16Cal(pucY []byte, ucX uint8) uint16 {
 	const PRESET_VALUE = 0xFFFF
@@ -226,15 +246,7 @@ func parseReportData(deviceCode string, buf []byte) (*ReportData, error) {
 	}, nil
 }
 
-// 防抖与状态
-var (
-	epcLastTime    = make(map[string]int64)
-	epcStatus      = make(map[string]bool)
-	epcMutex       sync.Mutex
-	debounceSecond = int64(3)
-)
-
-// handleReportData 处理UHF读取到的标签 + 完整权限判断
+// handleReportData 处理UHF读取到的标签(精简版:只做解析+防抖,业务逻辑委托给 PassageService)
 func handleReportData(report *ReportData) {
 	if len(report.Epcs) == 0 {
 		return
@@ -242,113 +254,18 @@ func handleReportData(report *ReportData) {
 
 	epc := report.Epcs[0]
 	fmt.Println("<UNK>", epc)
-	now := time.Now().Unix()
 
-	// ===================== 【修复】锁只包防抖,绝不包业务!=====================
-	epcMutex.Lock()
-	// 3秒防抖
-	if lastTime, ok := epcLastTime[epc]; ok && now-lastTime < debounceSecond {
-		epcMutex.Unlock()
-		return
-	}
-	epcLastTime[epc] = now
-	epcMutex.Unlock()
-	// ===================== 锁已释放,后面逻辑不会阻塞 =====================
-
-	// 获取设备处理器
-	handler, exists := DeviceManager.Get(report.DeviceCode)
-	if !exists {
-		return
-	}
-
-	// ===================== 查询设备 + 通道 =====================
-	var device dao.UHFReader
-	err := global.GVA_DB.Preload("Channel").First(&device, "device_code = ?", report.DeviceCode).Error
+	// 委托给统一的进出场服务(所有触发方式共用同一入口)
+	result, err := service.ServiceGroupApp.ParkingServiceGroup.PassageService.HandlePassage(common.PassageRequest{
+		RFIDTag:    epc,
+		DeviceCode: report.DeviceCode,
+	})
 	if err != nil {
+		fmt.Printf("⚠️ 进出场处理失败 | 设备:%s EPC:%s 错误:%v\n", report.DeviceCode, epc, err)
 		return
 	}
-	channel := device.Channel
-	direction := channel.Direction
-	allowTemporary := channel.AllowTemporary
-
-	vehicleService := service.ServiceGroupApp.VehicleServiceGroup.VehicleService
-	shortlistService := service.ServiceGroupApp.VehicleServiceGroup.ShortlistService
-
-	// ===================== 1. 根据EPC查询车辆 =====================
-	vehicle, err := vehicleService.GetVehicleByPlateNumber("", epc)
-	isTempVehicle := false
-	if err != nil || vehicle.ID == 0 {
-		isTempVehicle = true
-	} else {
-		if vehicle.VehicleType != nil {
-			isTempVehicle = (vehicle.VehicleTypeID == 1)
-		} else {
-			isTempVehicle = true
-		}
-	}
-
-	// ===================== 2. 查询黑白名单 =====================
-	isBlack, _ := shortlistService.CheckVehicleShortlist(epc)
-
-	// ===================== 3. 黑名单优先判断 =====================
-	if isBlack {
-		fmt.Printf("🚫 禁止通行 | 车辆在黑名单中 | 设备:%s EPC:%s\n", report.DeviceCode, epc)
-		return
-	}
-
-	// ===================== 4. 临时车规则 =====================
-	// 临时车:只看通道是否允许临停
-	if isTempVehicle && !allowTemporary {
-		fmt.Printf("🚫 禁止通行 | 临时车不允许此通道 | 设备:%s EPC:%s\n", report.DeviceCode, epc)
-		return
-	}
-
-	// ===================== 5. 入口 =====================
-	if direction == "in" {
-		epcMutex.Lock()
-		canIn := !epcStatus[epc]
-		epcMutex.Unlock()
-
-		if canIn {
-			fmt.Printf("🟢 入口开闸 | 设备:%s EPC:%s\n", report.DeviceCode, epc)
-			handler.reader.CloseRelay1(2)
-
-			var vehicleEntry request.VehicleEntry
-			vehicleEntry.RFIDTag = epc
-			vehicleEntry.ParkingLotID = channel.ParkingLotID
-			_, err := vehicleService.VehicleEntry(vehicleEntry)
-
-			if err == nil {
-				epcMutex.Lock()
-				epcStatus[epc] = true
-				epcMutex.Unlock()
-			}
-		}
-	}
-
-	// ===================== 6. 出口 =====================
-	if direction == "out" {
-		fmt.Printf("🔴 出口开闸 | 设备:%s EPC:%s\n", report.DeviceCode, epc)
-		handler.reader.CloseRelay1(2)
-
-		var vehicleExit request.VehicleExit
-		vehicleExit.RFIDTag = epc
-		_, err := vehicleService.VehicleExit(vehicleExit)
-
-		if err == nil {
-			epcMutex.Lock()
-			epcStatus[epc] = false
-			epcMutex.Unlock()
-		}
-	}
-}
 
-// ===================== 手动重置离场 =====================
-func SetEpcExit(epc string) {
-	epcMutex.Lock()
-	defer epcMutex.Unlock()
-	epcStatus[epc] = false
-	fmt.Printf("🚙 手动重置离场 | EPC: %s\n", epc)
+	fmt.Printf("✅ 进出场成功 | 方向:%s 车牌:%s 费用:%.2f\n", result.Direction, result.PlateNumber, result.Fee)
 }
 
 // ==============================