2026-07-30-ticket-printer-plan.md 9.2 KB

取票打印 + 扫码出场 — 实现计划

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:

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{}):

dao.ShiftRecord{},
dao.Printer{},
  • Step 2: ESC/POS 协议

Write internal/modules/printer/service/escpos.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 <fn> <cn> <a> <m> <nL> <nH> <d1...dk>
	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:

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:

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: 构建+提交
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:

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:

printer.SetupPrinterRouter(PrivateGroup)
  • Step 3: 构建+提交
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 <script setup> of frontend/src/view/parking/entryExit.vue:

// 扫码枪监听(USB键盘模拟输入)
let scanBuffer = '', scanTimer = null
function onScanKey(e) {
  if (e.key === 'Enter' && scanBuffer.length > 10) {
    queryForm.rfid_tag = ''; queryForm.plate_number = ''
    // 判断是 ticket_no (hex) 还是车牌
    if (/^[0-9a-f]{32}$/.test(scanBuffer)) {
      // ticket_no → 查 digital_ticket
      queryForm.rfid_tag = scanBuffer
    } else {
      queryForm.plate_number = scanBuffer
    }
    scanBuffer = ''; queryVehicle()
  } else if (e.key.length === 1) {
    scanBuffer += e.key; clearTimeout(scanTimer)
    scanTimer = setTimeout(() => { scanBuffer = '' }, 100)
  }
}
onMounted(() => document.addEventListener('keydown', onScanKey))
onUnmounted(() => { document.removeEventListener('keydown', onScanKey); clearInterval(eventTimer) })
  • Step 2: 构建+提交
cd frontend && npm run build && cd ..
git add frontend/src/view/parking/entryExit.vue
git commit -m "feat: 出场Tab扫码枪监听——自动识别ticket_no/车牌并查询"