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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user