Files
membank/auto-check/pkg/report/device.go

260 lines
8.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 唯一标识)
// 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 {
sr := StressResult{
Type: string(res.Type),
Status: res.Status,
Duration: res.Duration.String(),
Score: res.Score,
ScoreUnit: stress.ScoreUnit(res.Type),
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())
r := b.Build()
r.QRURL = qrURL
return r
}
// SaveDeviceReport 保存设备报告(文件名含 MAC/IP/时间戳)
func SaveDeviceReport(dev *model.Device, rpt *stress.Report, dir string) error {
if dir == "" {
dir = "reports"
}
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")
}
// 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
}
// 先保存 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
}
printer, err := NewPrinter(config.PrintConfig{Enabled: true, Backend: cfg.Backend, Device: cfg.Device, Width: width})
if err != nil {
return err
}
defer printer.Close()
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, 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, 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
}