260 lines
8.1 KiB
Go
260 lines
8.1 KiB
Go
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
|
||
}
|
||
|