352 lines
8.9 KiB
Go
352 lines
8.9 KiB
Go
package report
|
||
|
||
import (
|
||
"bufio"
|
||
"fmt"
|
||
"io"
|
||
"net"
|
||
"os"
|
||
"os/exec"
|
||
"runtime"
|
||
"strings"
|
||
"time"
|
||
|
||
"auto-check/pkg/config"
|
||
)
|
||
|
||
// Printed 打印状态表(IP → 上次打印的状态),包级公开
|
||
// 用于"状态变化才重新打印"策略
|
||
var Printed = make(map[string]string)
|
||
|
||
// ============================
|
||
// Printer 接口 — 打印后端抽象
|
||
// ============================
|
||
|
||
// Printer 报告打印机
|
||
type Printer interface {
|
||
Print(text string) error
|
||
// PrintQR 在打印流末尾追加二维码(ESC/POS GS ( k 指令)
|
||
// 后端不支持时返回 nil(不打印二维码)
|
||
PrintQR(url string) error
|
||
Close() error
|
||
}
|
||
|
||
// ============================
|
||
// 打印配置
|
||
// ============================
|
||
|
||
// DefaultPrintConfig 默认打印配置(80mm 热敏)
|
||
func DefaultPrintConfig() config.PrintConfig {
|
||
return config.PrintConfig{
|
||
Backend: "system",
|
||
Width: 42,
|
||
}
|
||
}
|
||
|
||
// ============================
|
||
// ESC/POS 指令生成(USB 与网络后端共用)
|
||
// ============================
|
||
|
||
// writeEscpos 将文本转为 ESC/POS 指令流写入 writer
|
||
func writeEscpos(w io.Writer, text string) error {
|
||
bw := bufio.NewWriter(w)
|
||
|
||
// 初始化打印机
|
||
bw.WriteString("\x1b@") // ESC @ 复位
|
||
bw.WriteString("\x1b3\x18") // 行距 24点
|
||
bw.WriteString("\x1bM\x00") // 标准字体
|
||
bw.WriteString("\x1ba\x00") // 左对齐
|
||
|
||
// 文本内容(UTF-8 直发,打印机需支持)
|
||
for _, line := range strings.Split(text, "\n") {
|
||
bw.WriteString(line)
|
||
bw.WriteString("\n")
|
||
}
|
||
|
||
// 走纸 + 切纸
|
||
bw.WriteString("\n\n") // 尾部空行
|
||
bw.WriteString("\x1di") // 切纸(部分机型)
|
||
return bw.Flush()
|
||
}
|
||
|
||
// ============================
|
||
// ESC/POS 二维码(GS ( k 指令)
|
||
// ============================
|
||
//
|
||
// 适配 EPSON / Star / 芯烨(Xprinter) / 佳博 等主流 ESC/POS 热敏打印机。
|
||
// 指令格式:GS ( k pL pH cn fn [n...]
|
||
// cn = 49 (0x31) QR Code
|
||
// fn = 65 (A) 选择模型 / 67 (C) 模块大小 / 69 (E) 纠错 / 80 (P) 数据 / 81 (Q) 打印
|
||
//
|
||
// 参考:EPSON TM-T20 QR Code 规范
|
||
// 数据长度限制(Model 2 + Level M 纠错):URL 场景 < 512 字节完全够用。
|
||
|
||
// escposQR 写入 ESC/POS 二维码指令流(不切纸,由调用方决定结尾)
|
||
// size: 1-16(模块像素数),默认 6
|
||
// ecc : 48/49/50/51 (L/M/Q/H)
|
||
func escposQR(w io.Writer, data string, size byte, ecc byte) error {
|
||
if size < 1 {
|
||
size = 1
|
||
}
|
||
if size > 16 {
|
||
size = 16
|
||
}
|
||
if ecc < 48 || ecc > 51 {
|
||
ecc = 49 // 默认 M
|
||
}
|
||
if len(data) == 0 {
|
||
return nil
|
||
}
|
||
bw := bufio.NewWriter(w)
|
||
|
||
// 1) 选择模型:Model 2(cn=49 fn=65 n=50)
|
||
bw.WriteString("\x1d(k\x04\x00\x31\x41\x32")
|
||
|
||
// 2) 模块大小(cn=49 fn=67 n=size)
|
||
bw.WriteString("\x1d(k\x03\x00\x31\x43")
|
||
bw.WriteByte(size)
|
||
|
||
// 3) 纠错级别(cn=49 fn=69 n=ecc)
|
||
bw.WriteString("\x1d(k\x03\x00\x31\x45")
|
||
bw.WriteByte(ecc)
|
||
|
||
// 4) 存储数据:GS ( k pL pH 49 80 48 <data>
|
||
// pL pH = (data_len + 3) 低/高字节
|
||
pL := byte((len(data) + 3) & 0xff)
|
||
pH := byte(((len(data) + 3) >> 8) & 0xff)
|
||
bw.WriteString("\x1d(k")
|
||
bw.WriteByte(pL)
|
||
bw.WriteByte(pH)
|
||
bw.WriteString("\x31\x50\x30")
|
||
bw.WriteString(data)
|
||
|
||
// 5) 打印(cn=49 fn=81 n=48 不叠加文字)
|
||
bw.WriteString("\x1d(k\x03\x00\x31\x51\x30")
|
||
|
||
return bw.Flush()
|
||
}
|
||
|
||
// ============================
|
||
// 后端实现
|
||
// ============================
|
||
|
||
// SystemPrinter 系统打印后端(lp/lpr/print)
|
||
type SystemPrinter struct {
|
||
printerName string
|
||
command string
|
||
}
|
||
|
||
// NewSystemPrinter 创建系统打印机
|
||
// printerName 为空使用系统默认打印机
|
||
func NewSystemPrinter(printerName string) *SystemPrinter {
|
||
command := "lpr"
|
||
if runtime.GOOS == "windows" {
|
||
command = "print"
|
||
}
|
||
return &SystemPrinter{printerName: printerName, command: command}
|
||
}
|
||
|
||
// Print 发送文本到系统打印机
|
||
func (p *SystemPrinter) Print(text string) error {
|
||
var cmd *exec.Cmd
|
||
switch runtime.GOOS {
|
||
case "windows":
|
||
// 临时文件方式(print 命令需要文件)
|
||
tmp, err := os.CreateTemp("", "auto-check-*.txt")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
name := tmp.Name()
|
||
if _, err := tmp.WriteString(text); err != nil {
|
||
tmp.Close()
|
||
return err
|
||
}
|
||
tmp.Close()
|
||
defer os.Remove(name)
|
||
|
||
if p.printerName != "" {
|
||
cmd = exec.Command("cmd", "/c", "print", "/D:"+p.printerName, name)
|
||
} else {
|
||
cmd = exec.Command("cmd", "/c", "print", name)
|
||
}
|
||
default:
|
||
args := []string{}
|
||
if p.printerName != "" {
|
||
args = append(args, "-P", p.printerName)
|
||
}
|
||
cmd = exec.Command(p.command, append(args, "-")...)
|
||
cmd.Stdin = strings.NewReader(text)
|
||
}
|
||
return cmd.Run()
|
||
}
|
||
|
||
// PrintQR 系统打印后端无二维码指令能力,no-op
|
||
func (p *SystemPrinter) PrintQR(url string) error { return nil }
|
||
|
||
// Close 系统打印无需关闭
|
||
func (p *SystemPrinter) Close() error { return nil }
|
||
|
||
// ============================
|
||
// ESCPOSPrinter — 网络热敏直连(9100)
|
||
// ============================
|
||
|
||
// ESCPOSPrinter 网络 ESC/POS 打印机
|
||
type ESCPOSPrinter struct {
|
||
conn net.Conn
|
||
}
|
||
|
||
// NewESCPOSPrinter 连接网络 ESC/POS 打印机
|
||
// address 格式: 192.168.1.100:9100
|
||
func NewESCPOSPrinter(address string) (*ESCPOSPrinter, error) {
|
||
conn, err := net.DialTimeout("tcp", address, 5*time.Second)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("连接打印机 %s 失败: %w", address, err)
|
||
}
|
||
return &ESCPOSPrinter{conn: conn}, nil
|
||
}
|
||
|
||
// Print 发送文本(自动转 ESC/POS 指令)
|
||
func (p *ESCPOSPrinter) Print(text string) error {
|
||
return writeEscpos(p.conn, text)
|
||
}
|
||
|
||
// PrintQR 发送 ESC/POS 二维码指令到网络打印机
|
||
// 走纸居中追加一行提示,模块大小 6,纠错级别 M
|
||
func (p *ESCPOSPrinter) PrintQR(url string) error {
|
||
if p.conn == nil {
|
||
return nil
|
||
}
|
||
// 居中(ESC a 1)后写一行"扫码查看详细报告",然后发二维码
|
||
center := "\x1ba\x01———— 扫码查看详细报告 ————\n\x1ba\x00"
|
||
if _, err := p.conn.Write([]byte(center)); err != nil {
|
||
return err
|
||
}
|
||
return escposQR(p.conn, url, 6, 49)
|
||
}
|
||
|
||
// Close 关闭连接
|
||
func (p *ESCPOSPrinter) Close() error {
|
||
if p.conn != nil {
|
||
return p.conn.Close()
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ============================
|
||
// USBPrinter — USB 热敏直连
|
||
// ============================
|
||
|
||
// USBPrinter USB ESC/POS 打印机
|
||
// Windows: \\.\usb001(或打印机端口名)
|
||
// Linux: /dev/usb/lp0(或 lp1...)
|
||
type USBPrinter struct {
|
||
dev *os.File
|
||
}
|
||
|
||
// defaultUSBDevice 默认 USB 设备路径
|
||
func defaultUSBDevice() string {
|
||
if runtime.GOOS == "windows" {
|
||
return `\\.\usb001`
|
||
}
|
||
return "/dev/usb/lp0"
|
||
}
|
||
|
||
// NewUSBPrinter 打开 USB 打印机设备
|
||
// device 为空时自动探测(Linux /dev/usb/lp0,Windows \\.\usb001)
|
||
func NewUSBPrinter(device string) (*USBPrinter, error) {
|
||
if device == "" || device == "auto" {
|
||
device = defaultUSBDevice()
|
||
}
|
||
|
||
f, err := os.OpenFile(device, os.O_WRONLY, 0)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("打开 USB 打印机 %s 失败: %w", device, err)
|
||
}
|
||
return &USBPrinter{dev: f}, nil
|
||
}
|
||
|
||
// Print 发送文本(自动转 ESC/POS 指令)
|
||
func (p *USBPrinter) Print(text string) error {
|
||
return writeEscpos(p.dev, text)
|
||
}
|
||
|
||
// PrintQR 发送 ESC/POS 二维码指令到 USB 打印机
|
||
func (p *USBPrinter) PrintQR(url string) error {
|
||
if p.dev == nil {
|
||
return nil
|
||
}
|
||
center := "\x1ba\x01———— 扫码查看详细报告 ————\n\x1ba\x00"
|
||
if _, err := p.dev.Write([]byte(center)); err != nil {
|
||
return err
|
||
}
|
||
return escposQR(p.dev, url, 6, 49)
|
||
}
|
||
|
||
// Close 关闭设备
|
||
func (p *USBPrinter) Close() error {
|
||
if p.dev != nil {
|
||
return p.dev.Close()
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ============================
|
||
// FilePrinter — 文件后端(调试/重定向)
|
||
// ============================
|
||
|
||
// FilePrinter 文件打印后端
|
||
type FilePrinter struct {
|
||
path string
|
||
f *os.File
|
||
}
|
||
|
||
// NewFilePrinter 创建文件打印机
|
||
func NewFilePrinter(path string) (*FilePrinter, error) {
|
||
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &FilePrinter{path: path, f: f}, nil
|
||
}
|
||
|
||
// Print 追加写入文件
|
||
func (p *FilePrinter) Print(text string) error {
|
||
_, err := p.f.WriteString(text + "\n\n")
|
||
return err
|
||
}
|
||
|
||
// PrintQR 文件后端:将"扫码查看详细报告"提示行 + URL 写入文件(便于在文件中复制链接)
|
||
func (p *FilePrinter) PrintQR(url string) error {
|
||
_, err := fmt.Fprintf(p.f, "\n———— 扫码查看详细报告 ————\nURL: %s\n\n", url)
|
||
return err
|
||
}
|
||
|
||
// Close 关闭文件
|
||
func (p *FilePrinter) Close() error {
|
||
return p.f.Close()
|
||
}
|
||
|
||
// ============================
|
||
// 工厂
|
||
// ============================
|
||
|
||
// NewPrinter 按配置创建打印机
|
||
func NewPrinter(cfg config.PrintConfig) (Printer, error) {
|
||
if !cfg.Enabled {
|
||
return nil, nil
|
||
}
|
||
|
||
switch cfg.Backend {
|
||
case "system":
|
||
return NewSystemPrinter(cfg.Device), nil
|
||
case "escpos":
|
||
return NewESCPOSPrinter(cfg.Device)
|
||
case "usb":
|
||
return NewUSBPrinter(cfg.Device)
|
||
case "file":
|
||
return NewFilePrinter(cfg.Device)
|
||
default:
|
||
return nil, fmt.Errorf("未知打印后端: %s", cfg.Backend)
|
||
}
|
||
}
|