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

> **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 <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`:

```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 `<script setup>` of `frontend/src/view/parking/entryExit.vue`:

```js
// 扫码枪监听（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: 构建+提交**

```bash
cd frontend && npm run build && cd ..
git add frontend/src/view/parking/entryExit.vue
git commit -m "feat: 出场Tab扫码枪监听——自动识别ticket_no/车牌并查询"
```
