feat: 飞牛NAS成本审核更新 - 新增SKU、更新价格表、清理归档及订单记录,同步auto-check改动
This commit is contained in:
@@ -2,22 +2,32 @@ package report
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/skip2/go-qrcode"
|
||||
|
||||
"auto-check/pkg/config"
|
||||
"auto-check/pkg/model"
|
||||
"auto-check/pkg/stress"
|
||||
)
|
||||
|
||||
// BuildDeviceReport 由压测结果构建设备报告(MAC 唯一标识)
|
||||
func BuildDeviceReport(dev *model.Device, rpt *stress.Report) *Report {
|
||||
// qrURL 非空时会附加到报告,供 80mm 模板在末尾显示"扫码查看详细报告"提示与 URL 文本兜底
|
||||
func BuildDeviceReport(dev *model.Device, rpt *stress.Report, qrURL string) *Report {
|
||||
// 从 sysInfo 提取 CPU 型号用于按硬件库匹配基准
|
||||
cpuModel := ""
|
||||
if rpt.SysInfo != nil {
|
||||
cpuModel = rpt.SysInfo["cpu"]
|
||||
}
|
||||
b := NewBuilder(ConfigSummary{})
|
||||
b.StartPhase("压力测试")
|
||||
b.AddHost(HostResult{MAC: dev.MAC, IP: dev.IP})
|
||||
for _, res := range rpt.Results {
|
||||
b.AddStressResult(dev.MAC, StressResult{
|
||||
sr := StressResult{
|
||||
Type: string(res.Type),
|
||||
Status: res.Status,
|
||||
Duration: res.Duration.String(),
|
||||
@@ -26,10 +36,21 @@ func BuildDeviceReport(dev *model.Device, rpt *stress.Report) *Report {
|
||||
Metrics: res.OrderedMetrics(),
|
||||
Error: res.Error,
|
||||
Output: res.Output,
|
||||
})
|
||||
}
|
||||
// 基准评级(让用户对结果有感性认知;按硬件库匹配专属阈值,未匹配全部"达到正常")
|
||||
if items := stress.EvaluateResult(res, cpuModel); len(items) > 0 {
|
||||
sr.Benchmarks = items
|
||||
if lvl := stress.OverallLevel(items); lvl != "" {
|
||||
sr.OverallLevel = string(lvl)
|
||||
sr.OverallText = stress.LevelMeta(lvl).Text
|
||||
}
|
||||
}
|
||||
b.AddStressResult(dev.MAC, sr)
|
||||
}
|
||||
b.EndPhase(rpt.Summary())
|
||||
return b.Build()
|
||||
r := b.Build()
|
||||
r.QRURL = qrURL
|
||||
return r
|
||||
}
|
||||
|
||||
// SaveDeviceReport 保存设备报告(文件名含 MAC/IP/时间戳)
|
||||
@@ -40,17 +61,119 @@ func SaveDeviceReport(dev *model.Device, rpt *stress.Report, dir string) error {
|
||||
ts := time.Now().Format("20060102-150405")
|
||||
name := strings.ReplaceAll(dev.MAC, ":", "")
|
||||
path := filepath.Join(dir, fmt.Sprintf("report-%s-%s-%s.txt", name, dev.IP, ts))
|
||||
return BuildDeviceReport(dev, rpt).SaveFile(path, "text")
|
||||
return BuildDeviceReport(dev, rpt, "").SaveFile(path, "text")
|
||||
}
|
||||
|
||||
// PrintDeviceReport 渲染 80mm 模板并送打印机
|
||||
func PrintDeviceReport(dev *model.Device, rpt *stress.Report, cfg config.PrintConfig) error {
|
||||
// SaveDeviceQR 保存设备报告对应的二维码 PNG(与 txt/html 同目录)
|
||||
// url 为空时不生成;返回 PNG 完整路径,失败时返回错误
|
||||
func SaveDeviceQR(dev *model.Device, url string, dir string) (string, error) {
|
||||
if url == "" {
|
||||
return "", nil
|
||||
}
|
||||
if dir == "" {
|
||||
dir = "reports"
|
||||
}
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
ts := time.Now().Format("20060102-150405")
|
||||
mac := strings.ReplaceAll(dev.MAC, ":", "")
|
||||
filename := fmt.Sprintf("report-%s-%s-%s.qr.png", mac, dev.IP, ts)
|
||||
path := filepath.Join(dir, filename)
|
||||
|
||||
// 512 字节足够;高纠错级别 H 提升扫码容错
|
||||
png, err := qrcode.New(url, qrcode.High)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// 512x512 PNG;二维码扫描距离更友好
|
||||
if err := png.WriteFile(512, path); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// SaveDeviceHTMLReport 保存设备 HTML 报告(文件名按 config.HTML.Filename 模板生成)
|
||||
// 返回: (filename, fullPath, error)
|
||||
func SaveDeviceHTMLReport(dev *model.Device, rpt *stress.Report, dir string, htmlCfg config.HTMLConfig) (string, string, error) {
|
||||
dir = htmlDir(dir, htmlCfg)
|
||||
ts := time.Now().Format("20060102-150405")
|
||||
mac := strings.ReplaceAll(dev.MAC, ":", "")
|
||||
filename := renderHTMLFilename(htmlCfg.Filename, mac, dev.IP, ts)
|
||||
if filename == "" {
|
||||
filename = fmt.Sprintf("report-%s-%s-%s.html", mac, dev.IP, ts)
|
||||
}
|
||||
|
||||
path, err := stress.WriteReportHTMLTo(dev.IP, rpt, dir, filename)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return filename, path, nil
|
||||
}
|
||||
|
||||
// BuildDeviceReportURL 构建设备 HTML 报告的访问 URL(用于二维码)
|
||||
// 优先级:qr_url 模板 > base_url + filename > 空字符串
|
||||
// 占位符:{filename} {mac} {ip} {ts}
|
||||
func BuildDeviceReportURL(dev *model.Device, filename string, htmlCfg config.HTMLConfig) string {
|
||||
mac := strings.ReplaceAll(dev.MAC, ":", "")
|
||||
ts := time.Now().Format("20060102-150405")
|
||||
if tpl := strings.TrimSpace(htmlCfg.QRURL); tpl != "" {
|
||||
return fillPlaceholders(tpl, map[string]string{
|
||||
"filename": filename,
|
||||
"mac": mac,
|
||||
"ip": dev.IP,
|
||||
"ts": ts,
|
||||
})
|
||||
}
|
||||
base := strings.TrimRight(strings.TrimSpace(htmlCfg.BaseURL), "/")
|
||||
if base == "" || filename == "" {
|
||||
return ""
|
||||
}
|
||||
return base + "/" + filename
|
||||
}
|
||||
|
||||
// saveDeviceHTMLAndQR 保存 HTML 报告 + 二维码 PNG(一体化),返回 (filename, qrPath, error)
|
||||
// - HTML 失败:仅返回错误,不生成 QR
|
||||
// - QR 失败:只打日志,不影响 HTML(用户至少能用 URL 文本兜底)
|
||||
func saveDeviceHTMLAndQR(dev *model.Device, rpt *stress.Report, dir string, htmlCfg config.HTMLConfig) (string, string, error) {
|
||||
filename, _, err := SaveDeviceHTMLReport(dev, rpt, dir, htmlCfg)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
url := BuildDeviceReportURL(dev, filename, htmlCfg)
|
||||
if url == "" && filename != "" {
|
||||
// 兜底:base_url 未配置时,至少打印本地路径(便于手工打开)
|
||||
url = filename
|
||||
}
|
||||
qrPath, qerr := SaveDeviceQR(dev, url, dir)
|
||||
if qerr != nil {
|
||||
fmt.Printf(" [报告] %s 二维码 PNG 生成失败: %v\n", dev.IP, qerr)
|
||||
}
|
||||
return filename, qrPath, nil
|
||||
}
|
||||
|
||||
// PrintDeviceReport 渲染 80mm 模板并送打印机,启用 HTML 时自动追加二维码
|
||||
func PrintDeviceReport(dev *model.Device, rpt *stress.Report, cfg config.PrintConfig, htmlCfg config.HTMLConfig) error {
|
||||
width := cfg.Width
|
||||
if width <= 0 {
|
||||
width = 42
|
||||
}
|
||||
|
||||
text, err := BuildDeviceReport(dev, rpt).Render80mmWithConfig(TemplateConfig{Width: width})
|
||||
// 先保存 HTML + 二维码 PNG,并计算二维码 URL
|
||||
filename, qrPath, herr := saveDeviceHTMLAndQR(dev, rpt, "", htmlCfg)
|
||||
if herr != nil {
|
||||
fmt.Printf(" [报告] %s HTML 保存失败: %v\n", dev.IP, herr)
|
||||
}
|
||||
url := BuildDeviceReportURL(dev, filename, htmlCfg)
|
||||
if url == "" && filename != "" {
|
||||
// 兜底:base_url 未配置时,至少打印本地路径(便于手工打开)
|
||||
url = filename
|
||||
}
|
||||
if qrPath != "" {
|
||||
fmt.Printf(" [报告] %s 二维码 PNG: %s\n", dev.IP, qrPath)
|
||||
}
|
||||
|
||||
text, err := BuildDeviceReport(dev, rpt, url).Render80mmWithConfig(TemplateConfig{Width: width})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -61,20 +184,76 @@ func PrintDeviceReport(dev *model.Device, rpt *stress.Report, cfg config.PrintCo
|
||||
}
|
||||
defer printer.Close()
|
||||
|
||||
return printer.Print(text)
|
||||
if err := printer.Print(text); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 追加二维码(HTML 报告访问链接);后端不支持或无 URL 时 PrintQR 内部静默跳过
|
||||
if printer != nil && url != "" {
|
||||
if qerr := printer.PrintQR(url); qerr != nil {
|
||||
fmt.Printf(" [打印] %s 二维码失败: %v\n", dev.IP, qerr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveAndPrintDeviceReport 保存报告并(启用时)打印,含错误输出(一站式业务入口)
|
||||
func SaveAndPrintDeviceReport(dev *model.Device, rpt *stress.Report, dir string, printCfg config.PrintConfig) {
|
||||
func SaveAndPrintDeviceReport(dev *model.Device, rpt *stress.Report, dir string, printCfg config.PrintConfig, htmlCfg config.HTMLConfig) {
|
||||
if err := SaveDeviceReport(dev, rpt, dir); err != nil {
|
||||
fmt.Printf(" [报告] %s 保存失败: %v\n", dev.IP, err)
|
||||
}
|
||||
|
||||
// 总是保存 HTML + 二维码 PNG(便于不打印时也能扫码查看详细报告)
|
||||
_, qrPath, herr := saveDeviceHTMLAndQR(dev, rpt, dir, htmlCfg)
|
||||
if herr != nil {
|
||||
fmt.Printf(" [报告] %s HTML 保存失败: %v\n", dev.IP, herr)
|
||||
} else if qrPath != "" {
|
||||
fmt.Printf(" [报告] %s 二维码 PNG: %s\n", dev.IP, qrPath)
|
||||
}
|
||||
|
||||
if printCfg.Enabled {
|
||||
if err := PrintDeviceReport(dev, rpt, printCfg); err != nil {
|
||||
if err := PrintDeviceReport(dev, rpt, printCfg, htmlCfg); err != nil {
|
||||
fmt.Printf(" [打印] %s 失败: %v\n", dev.IP, err)
|
||||
} else {
|
||||
fmt.Printf(" [打印] %s 已送出 (%s)\n", dev.IP, printCfg.Backend)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================
|
||||
// 辅助
|
||||
// ============================
|
||||
|
||||
// htmlDir 解析 HTML 报告目录
|
||||
func htmlDir(defaultDir string, cfg config.HTMLConfig) string {
|
||||
if p := strings.TrimSpace(cfg.Path); p != "" {
|
||||
return p
|
||||
}
|
||||
if defaultDir == "" {
|
||||
return "reports"
|
||||
}
|
||||
return defaultDir
|
||||
}
|
||||
|
||||
// renderHTMLFilename 渲染文件名模板,缺省时返回默认
|
||||
func renderHTMLFilename(tpl, mac, ip, ts string) string {
|
||||
tpl = strings.TrimSpace(tpl)
|
||||
if tpl == "" {
|
||||
return ""
|
||||
}
|
||||
return fillPlaceholders(tpl, map[string]string{
|
||||
"filename": "", // 文件名模板内不应再嵌 filename
|
||||
"mac": mac,
|
||||
"ip": ip,
|
||||
"ts": ts,
|
||||
})
|
||||
}
|
||||
|
||||
// fillPlaceholders 简单占位符替换 {key}
|
||||
func fillPlaceholders(s string, kv map[string]string) string {
|
||||
for k, v := range kv {
|
||||
s = strings.ReplaceAll(s, "{"+k+"}", url.PathEscape(v))
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@ var Printed = make(map[string]string)
|
||||
// Printer 报告打印机
|
||||
type Printer interface {
|
||||
Print(text string) error
|
||||
// PrintQR 在打印流末尾追加二维码(ESC/POS GS ( k 指令)
|
||||
// 后端不支持时返回 nil(不打印二维码)
|
||||
PrintQR(url string) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
@@ -66,6 +69,63 @@ func writeEscpos(w io.Writer, text string) error {
|
||||
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()
|
||||
}
|
||||
|
||||
// ============================
|
||||
// 后端实现
|
||||
// ============================
|
||||
@@ -120,6 +180,9 @@ func (p *SystemPrinter) Print(text string) error {
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// PrintQR 系统打印后端无二维码指令能力,no-op
|
||||
func (p *SystemPrinter) PrintQR(url string) error { return nil }
|
||||
|
||||
// Close 系统打印无需关闭
|
||||
func (p *SystemPrinter) Close() error { return nil }
|
||||
|
||||
@@ -147,6 +210,20 @@ 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 {
|
||||
@@ -193,6 +270,18 @@ 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 {
|
||||
@@ -226,6 +315,12 @@ func (p *FilePrinter) Print(text string) error {
|
||||
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()
|
||||
|
||||
131
auto-check/pkg/report/print_test.go
Normal file
131
auto-check/pkg/report/print_test.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package report
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestEscposQR_EmptyData 空数据应直接返回 nil,不写入任何指令
|
||||
func TestEscposQR_EmptyData(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
if err := escposQR(&buf, "", 6, 49); err != nil {
|
||||
t.Fatalf("空数据应返回 nil,得到 err=%v", err)
|
||||
}
|
||||
if buf.Len() != 0 {
|
||||
t.Errorf("空数据不应输出指令,实际写入 %d 字节", buf.Len())
|
||||
}
|
||||
}
|
||||
|
||||
// TestEscposQR_BasicCommands 验证基本 GS ( k 指令流
|
||||
func TestEscposQR_BasicCommands(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
data := "http://192.168.1.10:8080/reports/report-abc-10.0.0.1-20260824.html"
|
||||
if err := escposQR(&buf, data, 6, 49); err != nil {
|
||||
t.Fatalf("生成指令失败: %v", err)
|
||||
}
|
||||
|
||||
got := buf.Bytes()
|
||||
// 必须包含 GS ( k 起始
|
||||
if !bytes.Contains(got, []byte{0x1d, 0x28, 0x6b}) {
|
||||
t.Error("缺少 GS ( k 起始指令")
|
||||
}
|
||||
// 必须包含模型选择 cn=49 fn=65 n=50
|
||||
if !bytes.Contains(got, []byte{0x31, 0x41, 0x32}) {
|
||||
t.Error("缺少 QR 模型选择指令 (31 41 32)")
|
||||
}
|
||||
// 必须包含模块大小 n=6
|
||||
if !bytes.Contains(got, []byte{0x31, 0x43, 0x06}) {
|
||||
t.Error("缺少模块大小指令 (31 43 06)")
|
||||
}
|
||||
// 必须包含纠错级别 M (49)
|
||||
if !bytes.Contains(got, []byte{0x31, 0x45, 49}) {
|
||||
t.Error("缺少纠错级别指令 (31 45 49)")
|
||||
}
|
||||
// 必须包含数据存储 49 80 48
|
||||
if !bytes.Contains(got, []byte{0x31, 0x50, 0x30}) {
|
||||
t.Error("缺少数据存储指令 (31 50 30)")
|
||||
}
|
||||
// 必须包含打印指令 49 81 48
|
||||
if !bytes.Contains(got, []byte{0x31, 0x51, 0x30}) {
|
||||
t.Error("缺少打印指令 (31 51 30)")
|
||||
}
|
||||
// 数据本体
|
||||
if !bytes.Contains(got, []byte(data)) {
|
||||
t.Error("缺少 URL 数据本体")
|
||||
}
|
||||
}
|
||||
|
||||
// TestEscposQR_ClampSizeAndECC 验证 size/ecc 参数越界时自动夹紧
|
||||
func TestEscposQR_ClampSizeAndECC(t *testing.T) {
|
||||
var buf1, buf2, buf3 bytes.Buffer
|
||||
// size 越界 0 → 夹紧为 1
|
||||
_ = escposQR(&buf1, "abc", 0, 49)
|
||||
// size 越界 99 → 夹紧为 16
|
||||
_ = escposQR(&buf2, "abc", 99, 49)
|
||||
// ecc 非法 99 → 夹紧为 49 (M)
|
||||
_ = escposQR(&buf3, "abc", 6, 99)
|
||||
|
||||
if !bytes.Contains(buf1.Bytes(), []byte{0x31, 0x43, 0x01}) {
|
||||
t.Error("size=0 应夹紧为 1,未生效")
|
||||
}
|
||||
if !bytes.Contains(buf2.Bytes(), []byte{0x31, 0x43, 0x10}) {
|
||||
t.Error("size=99 应夹紧为 16,未生效")
|
||||
}
|
||||
if !bytes.Contains(buf3.Bytes(), []byte{0x31, 0x45, 49}) {
|
||||
t.Error("ecc=99 应夹紧为 49(M),未生效")
|
||||
}
|
||||
}
|
||||
|
||||
// TestEscposQR_DataLengthEncoding pL/pH 编码正确(大数据场景)
|
||||
// 指令布局: 1d 28 6b pL pH 31 50 30 <data>
|
||||
func TestEscposQR_DataLengthEncoding(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
// 200 字节数据 → pL = 203 (0xCB), pH = 0
|
||||
data := strings.Repeat("a", 200)
|
||||
_ = escposQR(&buf, data, 6, 49)
|
||||
got := buf.Bytes()
|
||||
|
||||
wantPL := byte((200 + 3) & 0xff) // 203 = 0xCB
|
||||
wantPH := byte(((200 + 3) >> 8) & 0xff)
|
||||
|
||||
// 数据存储标记 "31 50 30" 在 got 中的位置
|
||||
dataMarker := []byte{0x31, 0x50, 0x30}
|
||||
idx := bytes.Index(got, dataMarker)
|
||||
if idx < 0 {
|
||||
t.Fatal("未找到数据存储指令 31 50 30")
|
||||
}
|
||||
// 期望布局:1d(GS) 28 ( 6b k pL pH 31 50 30 ...
|
||||
// got[idx-5] = 0x1d, got[idx-4] = 0x28, got[idx-3] = 0x6b
|
||||
// got[idx-2] = pL, got[idx-1] = pH
|
||||
if got[idx-5] != 0x1d || got[idx-4] != 0x28 || got[idx-3] != 0x6b {
|
||||
t.Fatalf("pL/pH 前不是 GS ( k,实际前 5 字节 = % x", got[idx-5:idx])
|
||||
}
|
||||
if got[idx-2] != wantPL || got[idx-1] != wantPH {
|
||||
t.Errorf("pL/pH 编码错误: got=0x%02x 0x%02x, want=0x%02x 0x%02x",
|
||||
got[idx-2], got[idx-1], wantPL, wantPH)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSystemPrinter_PrintQR 系统打印后端 no-op
|
||||
func TestSystemPrinter_PrintQR(t *testing.T) {
|
||||
p := &SystemPrinter{}
|
||||
if err := p.PrintQR("http://example.com"); err != nil {
|
||||
t.Errorf("SystemPrinter.PrintQR 应返回 nil,得到 %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFilePrinter_PrintQR 文件后端写入提示行 + URL
|
||||
func TestFilePrinter_PrintQR(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
p, err := NewFilePrinter(tmp + "/out.log")
|
||||
if err != nil {
|
||||
t.Fatalf("创建 FilePrinter 失败: %v", err)
|
||||
}
|
||||
defer p.Close()
|
||||
|
||||
url := "http://192.168.1.10/reports/r.html"
|
||||
if err := p.PrintQR(url); err != nil {
|
||||
t.Fatalf("PrintQR 失败: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -32,14 +32,17 @@ type HostResult struct {
|
||||
|
||||
// StressResult 单项压力测试结果
|
||||
type StressResult struct {
|
||||
Type string `json:"type" yaml:"type"`
|
||||
Status string `json:"status" yaml:"status"` // pass / fail / skip
|
||||
Duration string `json:"duration" yaml:"duration"`
|
||||
Score float64 `json:"score,omitempty" yaml:"score,omitempty"`
|
||||
ScoreUnit string `json:"score_unit,omitempty" yaml:"score_unit,omitempty"`
|
||||
Metrics []stress.MetricItem `json:"metrics,omitempty" yaml:"metrics,omitempty"`
|
||||
Output string `json:"output,omitempty" yaml:"output,omitempty"`
|
||||
Error string `json:"error,omitempty" yaml:"error,omitempty"`
|
||||
Type string `json:"type" yaml:"type"`
|
||||
Status string `json:"status" yaml:"status"` // pass / fail / skip
|
||||
Duration string `json:"duration" yaml:"duration"`
|
||||
Score float64 `json:"score,omitempty" yaml:"score,omitempty"`
|
||||
ScoreUnit string `json:"score_unit,omitempty" yaml:"score_unit,omitempty"`
|
||||
Metrics []stress.MetricItem `json:"metrics,omitempty" yaml:"metrics,omitempty"`
|
||||
Benchmarks []stress.BenchItem `json:"benchmarks,omitempty" yaml:"benchmarks,omitempty"` // 性能/温度基准评级
|
||||
OverallLevel string `json:"overall_level,omitempty" yaml:"overall_level,omitempty"`
|
||||
OverallText string `json:"overall_text,omitempty" yaml:"overall_text,omitempty"`
|
||||
Output string `json:"output,omitempty" yaml:"output,omitempty"`
|
||||
Error string `json:"error,omitempty" yaml:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Report 检测报告
|
||||
@@ -49,6 +52,8 @@ type Report struct {
|
||||
Duration string `json:"duration" yaml:"duration"`
|
||||
Config ConfigSummary `json:"config" yaml:"config"`
|
||||
Phases []PhaseResult `json:"phases" yaml:"phases"`
|
||||
// QRURL HTML 报告访问链接(用于热敏打印中的二维码与模板回退)
|
||||
QRURL string `json:"qr_url,omitempty" yaml:"qr_url,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigSummary 配置摘要(脱敏)
|
||||
|
||||
@@ -18,9 +18,14 @@
|
||||
{{ end }}{{ end }}{{ if eq .Phase "SSH 登录" }}{{ range .Hosts }}{{ if .SSHLogin }}✓{{ else }}✗{{ end }} {{ padRight .MAC 19 }} {{ padRight .IP 15 }} {{ .SSHErr }}
|
||||
{{ end }}{{ end }}{{ if eq .Phase "压力测试" }}{{ range .Hosts }}{{ padRight .MAC 19 }} {{ padRight .IP 15 }}
|
||||
{{ range .Stress }}{{ if eq .Status "pass" }}✓{{ else }}✗{{ end }} {{ padRight .Type 12 }} {{ .Status }} {{ .Duration }}
|
||||
{{ if .Score }} 跑分: {{ printf "%.1f" .Score }} {{ .ScoreUnit }}{{ end }}{{ range .Metrics }} {{ .Label }}: {{ printf "%.1f" .Value }}{{ .Unit }}
|
||||
{{ if .Score }} 跑分: {{ printf "%.1f" .Score }} {{ .ScoreUnit }}{{ end }}{{ if .OverallText }} 评级: {{ .OverallText }}{{ end }}{{ range .Benchmarks }} {{ padRight .Label 8 }} {{ .Value }} {{ .LevelText }}
|
||||
{{ end }}{{ range .Metrics }} {{ .Label }}: {{ printf "%.1f" .Value }}{{ .Unit }}
|
||||
{{ end }}{{ if .Error }} ERR: {{ trunc .Error 34 }}{{ end }}{{ end }}{{ end }}{{ end }}{{ end }}
|
||||
{{ "" }}
|
||||
{{ hr '═' }}
|
||||
报告结束
|
||||
{{ hr '═' }}
|
||||
{{ if .QRURL }}{{ "" }}
|
||||
———— 扫码查看详细报告 ————
|
||||
{{ .QRURL }}
|
||||
{{ end }}
|
||||
|
||||
Reference in New Issue
Block a user