Przeglądaj źródła

feat: ESC/POS打印模块——EMD807串口通信+QRCode+58mm小票模板

lq 1 miesiąc temu
rodzic
commit
1ffa28822b

+ 14 - 0
internal/dao/printer.go

@@ -0,0 +1,14 @@
+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" }

+ 1 - 0
internal/initialize/gorm.go

@@ -62,6 +62,7 @@ func RegisterTables() {
 		dao.MonthlyCard{},
 		dao.DigitalTicket{},
 		dao.ShiftRecord{},
+		dao.Printer{},
 	)
 	if err != nil {
 		global.GVA_LOG.Error("register table failed", zap.Error(err))

+ 49 - 0
internal/modules/printer/service/escpos.go

@@ -0,0 +1,49 @@
+package service
+
+// ESC/POS commands for 58mm thermal printer (EMD807 compatible)
+
+const (
+	ESC = 0x1B
+	GS  = 0x1D
+)
+
+var (
+	InitCmd       = []byte{ESC, '@'}        // 初始化打印机
+	AlignLeftCmd  = []byte{ESC, 'a', 0}     // 左对齐
+	AlignCenterCmd = []byte{ESC, 'a', 1}    // 居中对齐
+	CutPaperCmd   = []byte{GS, 'V', 66, 0}  // 切纸
+	DoubleHeightCmd = []byte{ESC, '!', 0x11} // 双倍高度
+	NormalCmd     = []byte{ESC, '!', 0x00}   // 正常大小
+	BoldOnCmd     = []byte{ESC, 'E', 1}      // 加粗
+	BoldOffCmd    = []byte{ESC, 'E', 0}      // 取消加粗
+)
+
+// ESCPOS_Text 打印一行文字(自动加换行符)
+func ESCPOS_Text(text string) []byte {
+	return append([]byte(text), '\n')
+}
+
+// ESCPOS_QRCode 打印二维码(GS ( k 指令,兼容 EMD807)
+func ESCPOS_QRCode(data string) []byte {
+	// GS ( k pL pH cn fn [params]
+	// cn=49 (QR Code), fn=65 (model), fn=67 (size), fn=80 (store), fn=81 (print)
+
+	pL := byte(len(data) + 3)
+	pH := byte((len(data) + 3) >> 8)
+
+	cmd := []byte{GS, '('} // 模型选择
+
+	cmd = append(cmd, 0x04, 0x00, 0x31, 0x41, 0x32, 0x00) // QR Model 2
+	cmd = append(cmd, 0x03, 0x00, 0x31, 0x43, 0x08)         // Cell size 8
+
+	// Store data
+	cmd = append(cmd, GS, '(')
+	cmd = append(cmd, pL, pH, 0x31, 0x50, 0x30) // fn=80 store
+	cmd = append(cmd, []byte(data)...)
+
+	// Print QR
+	cmd = append(cmd, GS, '(')
+	cmd = append(cmd, 0x03, 0x00, 0x31, 0x51, 0x30) // fn=81 print
+
+	return cmd
+}

+ 31 - 0
internal/modules/printer/service/serial.go

@@ -0,0 +1,31 @@
+package service
+
+import (
+	"fmt"
+	"github.com/tarm/serial"
+)
+
+type SerialPrinter struct {
+	Port *serial.Port
+}
+
+func NewSerialPrinter(portName string, baudRate int) (*SerialPrinter, error) {
+	if baudRate <= 0 {
+		baudRate = 9600 // EMD807 factory default
+	}
+	c := &serial.Config{Name: portName, Baud: baudRate}
+	port, err := serial.OpenPort(c)
+	if err != nil {
+		return nil, fmt.Errorf("打开串口失败 %s: %w", portName, 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()
+}

+ 82 - 0
internal/modules/printer/service/service.go

@@ -0,0 +1,82 @@
+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("打印机未配置: id=%d", printerID)
+	}
+
+	sp, err := NewSerialPrinter(printer.PortName, printer.BaudRate)
+	if err != nil {
+		global.GVA_DB.Model(&printer).Update("status", "offline")
+		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) }
+
+	// Print ticket
+	write(InitCmd)
+	write(AlignCenterCmd)
+	write(DoubleHeightCmd)
+	write(BoldOnCmd)
+	write(ESCPOS_Text("PARKING TICKET"))
+	write(BoldOffCmd)
+	write(NormalCmd)
+	write(ESCPOS_Text(""))
+	write(ESCPOS_Text(lotName))
+	write(ESCPOS_Text("A-General Car"))
+	write(ESCPOS_Text(""))
+	write(BoldOnCmd)
+	write(ESCPOS_Text("PARK AT YOUR OWN RISK"))
+	write(BoldOffCmd)
+
+	write(InitCmd)
+	write(AlignLeftCmd)
+	write(ESCPOS_Text("In-Time::" + now))
+	if channelCode != "" {
+		write(ESCPOS_Text("InGate::" + channelCode))
+	}
+	if plateNo != "" {
+		write(ESCPOS_Text("Veh No::" + plateNo))
+	}
+
+	write(InitCmd)
+	write(ESCPOS_Text(""))
+	write(ESCPOS_Text("Slip No::" + slipNo))
+
+	write(InitCmd)
+	write(AlignCenterCmd)
+	write(ESCPOS_Text(""))
+	write(ESCPOS_QRCode(ticketNo))
+	write(ESCPOS_Text(""))
+
+	write(AlignCenterCmd)
+	write(ESCPOS_Text("SCAN AND PAY WITH"))
+	write(ESCPOS_Text("NEW THE CANADIA BANK APP"))
+
+	write(InitCmd)
+	write(ESCPOS_Text(""))
+	write(ESCPOS_Text(""))
+	write(CutPaperCmd)
+
+	global.GVA_DB.Model(&printer).Update("status", "online")
+	return nil
+}