feat: 飞牛NAS成本审核更新 - 新增SKU、更新价格表、清理归档及订单记录,同步auto-check改动

This commit is contained in:
12600k-rog-d4
2026-08-27 00:44:44 +08:00
parent bde60dbea0
commit a172a9bb5e
82 changed files with 1804 additions and 1180 deletions

View File

@@ -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
}