package printer import ( "crypto/rand" "encoding/hex" "fmt" "strings" "github.com/gin-gonic/gin" "wails-app/internal/dao" "wails-app/internal/global" commonModel "wails-app/internal/model/common" "wails-app/internal/model/common/response" printService "wails-app/internal/modules/printer/service" "wails-app/internal/service" ) var printSvc = printService.NewPrintService() type ticketMachinePassage interface { HandlePassage(commonModel.PassageRequest) (*commonModel.PassageResult, error) } type ticketMachinePrinter interface { PrintTicket(printerID uint, lotName, channelCode, plateNo string, ticketID uint, ticketNo string) error } type buttonReq struct { PrinterID uint `json:"printer_id"` ChannelCode string `json:"channel_code"` DeviceCode string `json:"device_code"` PlateNo string `json:"plate_number"` } type rawPrintDebugReq struct { DeviceName string `json:"device_name"` } type ticketMachineResult struct { SessionID uint `json:"session_id"` TicketID uint `json:"ticket_id"` TicketNo string `json:"ticket_no"` SlipNo uint `json:"slip_no"` EntryTime int64 `json:"entry_time"` PlateNumber string `json:"plate_number"` AnonymousVehicle bool `json:"anonymous_vehicle"` ParkingLotID uint `json:"parking_lot_id"` ParkingLotName string `json:"parking_lot_name"` ChannelCode string `json:"channel_code"` ChannelName string `json:"channel_name"` DeviceCode string `json:"device_code"` DeviceName string `json:"device_name"` BusinessStatus string `json:"business_status"` GateStatus string `json:"gate_status"` PrintStatus string `json:"print_status"` } func generateTicketMachineIdentity(prefix string) (string, error) { data := make([]byte, 8) if _, err := rand.Read(data); err != nil { return "", fmt.Errorf("生成票机车辆标识失败: %w", err) } return prefix + strings.ToUpper(hex.EncodeToString(data)), nil } func resolveTicketMachineLocation(req buttonReq) (*dao.Channel, *dao.UHFReader, *dao.ParkingLot, error) { if global.GVA_DB == nil { return nil, nil, nil, fmt.Errorf("数据库未初始化") } channelCode := strings.TrimSpace(req.ChannelCode) if channelCode == "" { return nil, nil, nil, fmt.Errorf("通道编码不能为空") } var channel dao.Channel if err := global.GVA_DB.Where("channel_code = ?", channelCode).First(&channel).Error; err != nil { return nil, nil, nil, fmt.Errorf("票机通道不存在: %s", channelCode) } if channel.Direction != "in" && channel.Direction != "inout" { return nil, nil, nil, fmt.Errorf("票机只能绑定入口或双向通道: %s", channelCode) } if channel.ParkingLotID == 0 { return nil, nil, nil, fmt.Errorf("票机通道未配置停车场: %s", channelCode) } var devices []dao.UHFReader deviceCode := strings.TrimSpace(req.DeviceCode) query := global.GVA_DB.Where("channel_id = ? AND is_active = ?", channel.ID, true) if deviceCode != "" { query = query.Where("device_code = ?", deviceCode) } if err := query.Order("id").Find(&devices).Error; err != nil { return nil, nil, nil, err } if len(devices) == 0 { if deviceCode == "" { return nil, nil, nil, fmt.Errorf("票机通道没有绑定启用的闸机设备: %s", channelCode) } return nil, nil, nil, fmt.Errorf("设备未启用或不属于票机通道: %s", deviceCode) } if len(devices) > 1 { return nil, nil, nil, fmt.Errorf("票机通道绑定了多个启用设备,必须指定 device_code") } var lot dao.ParkingLot if err := global.GVA_DB.First(&lot, channel.ParkingLotID).Error; err != nil { return nil, nil, nil, fmt.Errorf("票机通道所属停车场不存在") } return &channel, &devices[0], &lot, nil } func processTicketMachineButton(req buttonReq, passage ticketMachinePassage, printer ticketMachinePrinter) (*ticketMachineResult, error) { if req.PrinterID == 0 { return nil, fmt.Errorf("打印机ID不能为空") } channel, device, lot, err := resolveTicketMachineLocation(req) if err != nil { return nil, err } displayPlate := strings.TrimSpace(req.PlateNo) plateNumber := displayPlate anonymous := plateNumber == "" if anonymous { plateNumber, err = generateTicketMachineIdentity("TKT") if err != nil { return nil, err } } passageResult, passageErr := passage.HandlePassage(commonModel.PassageRequest{ PlateNumber: plateNumber, DeviceCode: device.DeviceCode, Direction: "in", TriggerSource: "ticket_machine", }) if passageResult == nil { if passageErr == nil { passageErr = fmt.Errorf("统一通行入口未返回处理结果") } return nil, passageErr } result := &ticketMachineResult{ SessionID: passageResult.SessionID, TicketID: passageResult.TicketID, TicketNo: passageResult.TicketNo, SlipNo: passageResult.TicketID, EntryTime: passageResult.EntryTime, PlateNumber: passageResult.PlateNumber, AnonymousVehicle: anonymous, ParkingLotID: lot.ID, ParkingLotName: lot.LotName, ChannelCode: channel.ChannelCode, ChannelName: channel.ChannelName, DeviceCode: device.DeviceCode, DeviceName: device.DeviceName, BusinessStatus: "completed", GateStatus: passageResult.GateStatus, PrintStatus: "pending", } if !passageResult.BusinessCompleted { result.BusinessStatus = "failed" if passageErr == nil { passageErr = fmt.Errorf("统一通行入口未完成入场业务") } return result, passageErr } if result.SessionID == 0 || result.TicketID == 0 || result.TicketNo == "" { result.PrintStatus = "skipped" return result, fmt.Errorf("统一通行入口未返回新建会话和数字票") } if err := printer.PrintTicket(req.PrinterID, lot.LotName, channel.ChannelCode, displayPlate, result.TicketID, result.TicketNo); err != nil { result.PrintStatus = "failed" if passageErr != nil { return result, fmt.Errorf("%v;打印失败: %w", passageErr, err) } return result, fmt.Errorf("打印失败: %w", err) } result.PrintStatus = "printed" if passageErr != nil { return result, passageErr } return result, nil } func handleTicketMachineButton(c *gin.Context, req buttonReq) { result, err := processTicketMachineButton( req, &service.ServiceGroupApp.ParkingServiceGroup.PassageService, printSvc, ) if err != nil { if result != nil { response.FailWithDetailed(result, err.Error(), c) return } response.FailWithMessage(err.Error(), c) return } response.OkWithData(result, c) } // TicketMachineButton 处理真实票机按钮事件,业务统一进入 PassageService。 func TicketMachineButton(c *gin.Context) { var req buttonReq if err := c.ShouldBindJSON(&req); err != nil { response.FailWithMessage(err.Error(), c) return } handleTicketMachineButton(c, req) } func TicketMachineButtonTest(c *gin.Context) { var req buttonReq if err := c.ShouldBindJSON(&req); err != nil { response.FailWithMessage(err.Error(), c) return } plateNumber, err := generateTicketMachineIdentity("TEST") if err != nil { response.FailWithMessage(err.Error(), c) return } req.PlateNo = plateNumber handleTicketMachineButton(c, req) } func ticketMachineRawPrintDebug(c *gin.Context) { var req rawPrintDebugReq if err := c.ShouldBindJSON(&req); err != nil { response.FailWithMessage(err.Error(), c) return } req.DeviceName = strings.TrimSpace(req.DeviceName) if req.DeviceName == "" { response.FailWithMessage("打印设备名称不能为空", c) return } if err := printService.InitCSN(printService.FindCsnDllPath()); err != nil { response.FailWithMessage(err.Error(), c) return } printService.Csn_EnumUSB() printService.Csn_EnumPRN() h, err := printService.Csn_OpenUSB(req.DeviceName) if err != nil { h, err = printService.Csn_OpenPRN(req.DeviceName) } if err != nil { response.FailWithMessage(err.Error(), c) return } defer printService.Csn_Close(h) printService.Csn_SetPort(h) testData := []byte{0x1B, 0x40, 'T', 'E', 'S', 'T', ' ', 'P', 'R', 'I', 'N', 'T', '\n', '\n', '\n', '\n', 0x1B, 0x69} n := printService.Csn_WriteData(testData) if n != len(testData) { response.FailWithDetailed(gin.H{"bytes_written": n, "bytes_expected": len(testData)}, "打印数据未完整写入", c) return } response.OkWithData(gin.H{ "device_name": req.DeviceName, "bytes_written": n, }, c) } func ticketMachineUSBList(c *gin.Context) { if err := printService.InitCSN(printService.FindCsnDllPath()); err != nil { response.FailWithMessage(err.Error(), c) return } usb := printService.Csn_EnumUSBNames() prn := printService.Csn_EnumPRNNames() response.OkWithData(gin.H{"usb": usb, "prn": prn}, c) } func SetupPrinterRouter(router *gin.RouterGroup) { router.POST("/ticket-machine/button", TicketMachineButton) } // SetupPrinterDebugRoutes 仅在显式开发配置开启时注册,调用方必须使用私有鉴权路由组。 func SetupPrinterDebugRoutes(router *gin.RouterGroup, enabled bool) { if !enabled { return } router.POST("/ticket-machine/test", TicketMachineButtonTest) router.POST("/ticket-machine/debug", ticketMachineRawPrintDebug) router.GET("/ticket-machine/usb-list", ticketMachineUSBList) }