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 // 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) } }