# 取票打印 + 扫码出场 — 实现计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans. **Goal:** 临时车取票入场(按钮→打印二维码小票),扫码出场(扫码枪→查票→结算) **Architecture:** 新增 `internal/modules/printer/` 模块。ESC/POS 指令构建 + 串口通信。票机按钮POST创建无车牌入场记录并打印。前端监听扫码枪input自动填入ticket_no。 --- ## 文件结构 | 文件 | 操作 | |------|------| | `internal/dao/printer.go` | 新建 | | `internal/modules/printer/api.go` | 新建 | | `internal/modules/printer/service/service.go` | 新建 | | `internal/modules/printer/service/escpos.go` | 新建 | | `internal/modules/printer/service/serial.go` | 新建 | | `internal/initialize/gorm.go` | 修改 | | `internal/initialize/router.go` | 修改 | | `frontend/src/view/parking/entryExit.vue` | 修改 | --- ### Task 1: ESC/POS 打印模块 - [ ] **Step 1: DAO** Write `internal/dao/printer.go`: ```go package dao import "wails-app/internal/global" type Printer struct { global.GVA_MODEL Name string `gorm:"size:50" json:"name"` PortName string `gorm:"size:20" json:"port_name"` // COM3 BaudRate int `json:"baud_rate"` // 9600 Status string `gorm:"size:20;default:offline" json:"status"` Location string `gorm:"size:20" json:"location"` // entry / exit } func (Printer) TableName() string { return "printer" } ``` Add to `internal/initialize/gorm.go` AutoMigrate (after `dao.ShiftRecord{}`): ```go dao.ShiftRecord{}, dao.Printer{}, ``` - [ ] **Step 2: ESC/POS 协议** Write `internal/modules/printer/service/escpos.go`: ```go package service // ESC/POS commands for 58mm thermal printer const ( ESC = 0x1B GS = 0x1D ) var ( Init = []byte{ESC, '@'} // 初始化 AlignLeft = []byte{ESC, 'a', 0} // 左对齐 AlignCenter= []byte{ESC, 'a', 1} // 居中 FeedLine = []byte{ESC, 'd', 1} // 换行 CutPaper = []byte{GS, 'V', 66, 0} // 切纸 DoubleH = []byte{ESC, '!', 0x11} // 双倍高度 Normal = []byte{ESC, '!', 0x00} // 正常字体 ) func ESCPOS_Text(text string) []byte { return append([]byte(text), '\n') } func ESCPOS_QRCode(data string) []byte { // GS ( k n := len(data) + 3 cmd := []byte{GS, '(','k', byte(n), 0, 49, 80, 48} // QR model 2, ECC L cmd = append(cmd, byte(len(data)+3), 0, 49, 80, 48) // store cmd = append(cmd, []byte(data)...) cmd = append(cmd, GS,'(','k',3,0,49,81,48) // print QR return cmd } ``` - [ ] **Step 3: 串口通信** Write `internal/modules/printer/service/serial.go`: ```go package service import ( "fmt" "github.com/tarm/serial" ) type SerialPrinter struct { Port *serial.Port } func NewSerialPrinter(portName string, baudRate int) (*SerialPrinter, error) { c := &serial.Config{Name: portName, Baud: baudRate} port, err := serial.OpenPort(c) if err != nil { return nil, fmt.Errorf("打开串口失败: %w", err) } return &SerialPrinter{Port: port}, nil } func (p *SerialPrinter) Write(data []byte) error { _, err := p.Port.Write(data) return err } func (p *SerialPrinter) Close() error { return p.Port.Close() } ``` Note: `go get github.com/tarm/serial` required. - [ ] **Step 4: Print Service** Write `internal/modules/printer/service/service.go`: ```go package service import ( "fmt" "time" "wails-app/internal/dao" "wails-app/internal/global" ) type PrintService struct{} func NewPrintService() *PrintService { return &PrintService{} } // PrintTicket 打印入场小票 func (s *PrintService) PrintTicket(printerID uint, lotName, channelCode, plateNo string, ticketID uint, ticketNo string) error { var printer dao.Printer if err := global.GVA_DB.First(&printer, printerID).Error; err != nil { return fmt.Errorf("打印机未配置") } sp, err := NewSerialPrinter(printer.PortName, printer.BaudRate) if err != nil { return fmt.Errorf("连接打印机失败: %w", err) } defer sp.Close() now := time.Now().Format("02-01-2006 15:04:05") slipNo := fmt.Sprintf("%d", ticketID) write := func(data []byte) { sp.Write(data) } write(Init) write(AlignCenter) write(DoubleH) write(ESCPOS_Text("PARKING TICKET")) write(Normal) write(ESCPOS_Text("")) write(ESCPOS_Text(lotName)) write(ESCPOS_Text("A-General Car")) write(ESCPOS_Text("")) write(ESCPOS_Text("PARK AT YOUR OWN RISK")) write(FeedLine) write(AlignLeft) write(ESCPOS_Text("In-Time::" + now)) if channelCode != "" { write(ESCPOS_Text("InGate::" + channelCode)) } if plateNo != "" { write(ESCPOS_Text("Veh No::" + plateNo)) } write(FeedLine) write(ESCPOS_Text("Slip No::" + slipNo)) write(FeedLine) write(AlignCenter) write(ESCPOS_QRCode(ticketNo)) write(FeedLine) write(ESCPOS_Text("SCAN AND PAY WITH")) write(ESCPOS_Text("NEW THE CANADIA BANK APP")) write(FeedLine) write(CutPaper) global.GVA_DB.Model(&printer).Update("status", "online") return nil } ``` - [ ] **Step 5: 构建+提交** ```bash cd lc_garage && go get github.com/tarm/serial && go build -o build/bin/smart-parking.exe . git add internal/dao/printer.go internal/modules/printer/ internal/initialize/gorm.go go.mod go.sum git commit -m "feat: ESC/POS打印模块——串口通信+58mm小票模板+QRCode" ``` --- ### Task 2: 按钮事件 API + 入场集成 - [ ] **Step 1: api.go** Write `internal/modules/printer/api.go`: ```go package printer import ( "github.com/gin-gonic/gin" "wails-app/internal/global" "wails-app/internal/model/common/response" ticketSvc "wails-app/internal/modules/digital-ticket/service" "wails-app/internal/modules/printer/service" "wails-app/internal/service" utils "wails-app/internal/pkg" ) var printSvc = service.NewPrintService() var vehicleSvc = service.ServiceGroupApp.VehicleServiceGroup.VehicleService type buttonReq struct { PrinterID uint `json:"printer_id"` ChannelCode string `json:"channel_code"` PlateNo string `json:"plate_number"` // 可选,识别到则填 } func TicketMachineButton(c *gin.Context) { var req buttonReq if err := c.ShouldBindJSON(&req); err != nil { response.FailWithMessage(err.Error(), c); return } // 创建入场记录(无车牌临时车) entryResp, err := vehicleSvc.VehicleEntry(request.VehicleEntry{ PlateNumber: req.PlateNo, RFIDTag: "", ParkingLotID: 1, ParkingSpaceID: 1, }) if err != nil { response.FailWithMessage(err.Error(), c); return } // 获取最新 digital_ticket var tickets []dao.DigitalTicket global.GVA_DB.Where("plate_number = ?", entryResp.Data.PlateNumber).Order("id DESC").Limit(1).Find(&tickets) if len(tickets) == 0 { response.FailWithMessage("票创建失败", c); return } // 打印小票 var lot dao.ParkingLot global.GVA_DB.First(&lot, entryResp.Data.ParkingLotID) if err := printSvc.PrintTicket(req.PrinterID, lot.LotName, req.ChannelCode, req.PlateNo, tickets[0].ID, tickets[0].TicketNo); err != nil { response.FailWithMessage("打印失败: "+err.Error(), c); return } response.OkWithData(gin.H{"ticket_id": tickets[0].ID, "ticket_no": tickets[0].TicketNo}, c) } func SetupPrinterRouter(router *gin.RouterGroup) { router.POST("/ticket-machine/button", TicketMachineButton) } ``` Need imports: `"wails-app/internal/dao"`, `"wails-app/internal/model/vehicle/request"`. - [ ] **Step 2: 注册路由** Edit `internal/initialize/router.go`, import `"wails-app/internal/modules/printer"`, add: ```go printer.SetupPrinterRouter(PrivateGroup) ``` - [ ] **Step 3: 构建+提交** ```bash go build -o build/bin/smart-parking.exe . sqlite3 "$APPDATA/smart-parking/lc_garage.db" "INSERT INTO casbin_rule (ptype,v0,v1,v2) VALUES ('p','888','/ticket-machine/button','POST'),('p','618','/ticket-machine/button','POST');" git add internal/modules/printer/api.go internal/initialize/router.go git commit -m "feat: 票机按钮事件API——创建无车牌入场+打印小票" ``` --- ### Task 3: 前端扫码枪监听 - [ ] **Step 1: entryExit.vue 出场 Tab 添加扫码枪监听** 扫码枪 = USB键盘,快速输入一串字符后回车。在出场 Tab 添加全局 input 监听,检测快速输入(<50ms/字符)即为扫码枪,自动填入并查询。 Add to `