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 |
修改 |
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{},
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
}
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.
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
}
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"
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".
Edit internal/initialize/router.go, import "wails-app/internal/modules/printer", add:
printer.SetupPrinterRouter(PrivateGroup)
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——创建无车牌入场+打印小票"
扫码枪 = 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) })
cd frontend && npm run build && cd ..
git add frontend/src/view/parking/entryExit.vue
git commit -m "feat: 出场Tab扫码枪监听——自动识别ticket_no/车牌并查询"