重构 auto-check:拆分包架构,精简 workflow 为纯调度循环
各包职责: - config: 分类 YAML 配置 - model: Device/IP/MAC 公共类型 - discovery: 扫描 + ARP MAC 解析 + Devices 变量 - sshclient: SSH 连接 - stress: stress-ng/stressapptest 压测 + Results 变量 - report: 80mm 热敏模板/sparkline/打印 + Printed 变量 - workflow: 永久循环调度 + Start/Stop workflow 已精简为 100 行纯编排,所有业务逻辑下沉各包
This commit is contained in:
77
auto-check/pkg/report/device.go
Normal file
77
auto-check/pkg/report/device.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package report
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"auto-check/pkg/config"
|
||||
"auto-check/pkg/model"
|
||||
"auto-check/pkg/stress"
|
||||
)
|
||||
|
||||
// BuildDeviceReport 由压测结果构建设备报告(MAC 唯一标识)
|
||||
func BuildDeviceReport(dev *model.Device, rpt *stress.Report) *Report {
|
||||
b := NewBuilder(ConfigSummary{})
|
||||
b.StartPhase("压力测试")
|
||||
b.AddHost(HostResult{MAC: dev.MAC, IP: dev.IP})
|
||||
for _, res := range rpt.Results {
|
||||
b.AddStressResult(dev.MAC, StressResult{
|
||||
Type: string(res.Type),
|
||||
Status: res.Status,
|
||||
Duration: res.Duration.String(),
|
||||
Error: res.Error,
|
||||
Output: res.Output,
|
||||
})
|
||||
}
|
||||
b.EndPhase(rpt.Summary())
|
||||
return b.Build()
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
// PrintDeviceReport 渲染 80mm 模板并送打印机
|
||||
func PrintDeviceReport(dev *model.Device, rpt *stress.Report, cfg config.PrintConfig) error {
|
||||
width := cfg.Width
|
||||
if width <= 0 {
|
||||
width = 42
|
||||
}
|
||||
|
||||
text, err := BuildDeviceReport(dev, rpt).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()
|
||||
|
||||
return printer.Print(text)
|
||||
}
|
||||
|
||||
// SaveAndPrintDeviceReport 保存报告并(启用时)打印,含错误输出(一站式业务入口)
|
||||
func SaveAndPrintDeviceReport(dev *model.Device, rpt *stress.Report, dir string, printCfg config.PrintConfig) {
|
||||
if err := SaveDeviceReport(dev, rpt, dir); err != nil {
|
||||
fmt.Printf(" [报告] %s 保存失败: %v\n", dev.IP, err)
|
||||
}
|
||||
|
||||
if printCfg.Enabled {
|
||||
if err := PrintDeviceReport(dev, rpt, printCfg); err != nil {
|
||||
fmt.Printf(" [打印] %s 失败: %v\n", dev.IP, err)
|
||||
} else {
|
||||
fmt.Printf(" [打印] %s 已送出 (%s)\n", dev.IP, printCfg.Backend)
|
||||
}
|
||||
}
|
||||
}
|
||||
257
auto-check/pkg/report/print.go
Normal file
257
auto-check/pkg/report/print.go
Normal file
@@ -0,0 +1,257 @@
|
||||
package report
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"auto-check/pkg/config"
|
||||
)
|
||||
|
||||
// Printed 打印状态表(IP → 上次打印的状态),包级公开
|
||||
// 用于"状态变化才重新打印"策略
|
||||
var Printed = make(map[string]string)
|
||||
|
||||
// ============================
|
||||
// Printer 接口 — 打印后端抽象
|
||||
// ============================
|
||||
|
||||
// Printer 报告打印机
|
||||
type Printer interface {
|
||||
Print(text string) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
// ============================
|
||||
// 打印配置
|
||||
// ============================
|
||||
|
||||
// DefaultPrintConfig 默认打印配置(80mm 热敏)
|
||||
func DefaultPrintConfig() config.PrintConfig {
|
||||
return config.PrintConfig{
|
||||
Backend: "system",
|
||||
Width: 42,
|
||||
Charset: "utf-8",
|
||||
}
|
||||
}
|
||||
|
||||
// ============================
|
||||
// ESC/POS 指令生成(USB 与网络后端共用)
|
||||
// ============================
|
||||
|
||||
// writeEscpos 将文本转为 ESC/POS 指令流写入 writer
|
||||
func writeEscpos(w io.Writer, text string) error {
|
||||
bw := bufio.NewWriter(w)
|
||||
|
||||
// 初始化打印机
|
||||
bw.WriteString("\x1b@") // ESC @ 复位
|
||||
bw.WriteString("\x1b3\x18") // 行距 24点
|
||||
bw.WriteString("\x1bM\x00") // 标准字体
|
||||
bw.WriteString("\x1ba\x00") // 左对齐
|
||||
|
||||
// 文本内容(UTF-8 直发,打印机需支持)
|
||||
for _, line := range strings.Split(text, "\n") {
|
||||
bw.WriteString(line)
|
||||
bw.WriteString("\n")
|
||||
}
|
||||
|
||||
// 走纸 + 切纸
|
||||
bw.WriteString("\n\n") // 尾部空行
|
||||
bw.WriteString("\x1di") // 切纸(部分机型)
|
||||
return bw.Flush()
|
||||
}
|
||||
|
||||
// ============================
|
||||
// 后端实现
|
||||
// ============================
|
||||
|
||||
// SystemPrinter 系统打印后端(lp/lpr/print)
|
||||
type SystemPrinter struct {
|
||||
printerName string
|
||||
command string
|
||||
}
|
||||
|
||||
// NewSystemPrinter 创建系统打印机
|
||||
// printerName 为空使用系统默认打印机
|
||||
func NewSystemPrinter(printerName string) *SystemPrinter {
|
||||
command := "lpr"
|
||||
if runtime.GOOS == "windows" {
|
||||
command = "print"
|
||||
}
|
||||
return &SystemPrinter{printerName: printerName, command: command}
|
||||
}
|
||||
|
||||
// Print 发送文本到系统打印机
|
||||
func (p *SystemPrinter) Print(text string) error {
|
||||
var cmd *exec.Cmd
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
// 临时文件方式(print 命令需要文件)
|
||||
tmp, err := os.CreateTemp("", "auto-check-*.txt")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name := tmp.Name()
|
||||
if _, err := tmp.WriteString(text); err != nil {
|
||||
tmp.Close()
|
||||
return err
|
||||
}
|
||||
tmp.Close()
|
||||
defer os.Remove(name)
|
||||
|
||||
if p.printerName != "" {
|
||||
cmd = exec.Command("cmd", "/c", "print", "/D:"+p.printerName, name)
|
||||
} else {
|
||||
cmd = exec.Command("cmd", "/c", "print", name)
|
||||
}
|
||||
default:
|
||||
args := []string{}
|
||||
if p.printerName != "" {
|
||||
args = append(args, "-P", p.printerName)
|
||||
}
|
||||
cmd = exec.Command(p.command, append(args, "-")...)
|
||||
cmd.Stdin = strings.NewReader(text)
|
||||
}
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// Close 系统打印无需关闭
|
||||
func (p *SystemPrinter) Close() error { return nil }
|
||||
|
||||
// ============================
|
||||
// ESCPOSPrinter — 网络热敏直连(9100)
|
||||
// ============================
|
||||
|
||||
// ESCPOSPrinter 网络 ESC/POS 打印机
|
||||
type ESCPOSPrinter struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
// NewESCPOSPrinter 连接网络 ESC/POS 打印机
|
||||
// address 格式: 192.168.1.100:9100
|
||||
func NewESCPOSPrinter(address string) (*ESCPOSPrinter, error) {
|
||||
conn, err := net.DialTimeout("tcp", address, 5*time.Second)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("连接打印机 %s 失败: %w", address, err)
|
||||
}
|
||||
return &ESCPOSPrinter{conn: conn}, nil
|
||||
}
|
||||
|
||||
// Print 发送文本(自动转 ESC/POS 指令)
|
||||
func (p *ESCPOSPrinter) Print(text string) error {
|
||||
return writeEscpos(p.conn, text)
|
||||
}
|
||||
|
||||
// Close 关闭连接
|
||||
func (p *ESCPOSPrinter) Close() error {
|
||||
if p.conn != nil {
|
||||
return p.conn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ============================
|
||||
// USBPrinter — USB 热敏直连
|
||||
// ============================
|
||||
|
||||
// USBPrinter USB ESC/POS 打印机
|
||||
// Windows: \\.\usb001(或打印机端口名)
|
||||
// Linux: /dev/usb/lp0(或 lp1...)
|
||||
type USBPrinter struct {
|
||||
dev *os.File
|
||||
}
|
||||
|
||||
// defaultUSBDevice 默认 USB 设备路径
|
||||
func defaultUSBDevice() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return `\\.\usb001`
|
||||
}
|
||||
return "/dev/usb/lp0"
|
||||
}
|
||||
|
||||
// NewUSBPrinter 打开 USB 打印机设备
|
||||
// device 为空时自动探测(Linux /dev/usb/lp0,Windows \\.\usb001)
|
||||
func NewUSBPrinter(device string) (*USBPrinter, error) {
|
||||
if device == "" || device == "auto" {
|
||||
device = defaultUSBDevice()
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(device, os.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("打开 USB 打印机 %s 失败: %w", device, err)
|
||||
}
|
||||
return &USBPrinter{dev: f}, nil
|
||||
}
|
||||
|
||||
// Print 发送文本(自动转 ESC/POS 指令)
|
||||
func (p *USBPrinter) Print(text string) error {
|
||||
return writeEscpos(p.dev, text)
|
||||
}
|
||||
|
||||
// Close 关闭设备
|
||||
func (p *USBPrinter) Close() error {
|
||||
if p.dev != nil {
|
||||
return p.dev.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ============================
|
||||
// FilePrinter — 文件后端(调试/重定向)
|
||||
// ============================
|
||||
|
||||
// FilePrinter 文件打印后端
|
||||
type FilePrinter struct {
|
||||
path string
|
||||
f *os.File
|
||||
}
|
||||
|
||||
// NewFilePrinter 创建文件打印机
|
||||
func NewFilePrinter(path string) (*FilePrinter, error) {
|
||||
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &FilePrinter{path: path, f: f}, nil
|
||||
}
|
||||
|
||||
// Print 追加写入文件
|
||||
func (p *FilePrinter) Print(text string) error {
|
||||
_, err := p.f.WriteString(text + "\n\n")
|
||||
return err
|
||||
}
|
||||
|
||||
// Close 关闭文件
|
||||
func (p *FilePrinter) Close() error {
|
||||
return p.f.Close()
|
||||
}
|
||||
|
||||
// ============================
|
||||
// 工厂
|
||||
// ============================
|
||||
|
||||
// NewPrinter 按配置创建打印机
|
||||
func NewPrinter(cfg config.PrintConfig) (Printer, error) {
|
||||
if !cfg.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
switch cfg.Backend {
|
||||
case "system":
|
||||
return NewSystemPrinter(cfg.Device), nil
|
||||
case "escpos":
|
||||
return NewESCPOSPrinter(cfg.Device)
|
||||
case "usb":
|
||||
return NewUSBPrinter(cfg.Device)
|
||||
case "file":
|
||||
return NewFilePrinter(cfg.Device)
|
||||
default:
|
||||
return nil, fmt.Errorf("未知打印后端: %s", cfg.Backend)
|
||||
}
|
||||
}
|
||||
@@ -11,20 +11,22 @@ import (
|
||||
|
||||
// PhaseResult 单阶段结果
|
||||
type PhaseResult struct {
|
||||
Phase string `json:"phase" yaml:"phase"`
|
||||
Status string `json:"status" yaml:"status"` // success / partial / failed
|
||||
Phase string `json:"phase" yaml:"phase"`
|
||||
Status string `json:"status" yaml:"status"` // success / partial / failed
|
||||
Hosts []HostResult `json:"hosts" yaml:"hosts"`
|
||||
Summary string `json:"summary" yaml:"summary"`
|
||||
Summary string `json:"summary" yaml:"summary"`
|
||||
}
|
||||
|
||||
// HostResult 单台主机结果
|
||||
// HostResult 单台设备结果(MAC 为唯一标识)
|
||||
type HostResult struct {
|
||||
IP string `json:"ip" yaml:"ip"`
|
||||
Alive bool `json:"alive" yaml:"alive"`
|
||||
OpenPort int `json:"open_port,omitempty" yaml:"open_port,omitempty"`
|
||||
SSHLogin bool `json:"ssh_login" yaml:"ssh_login"`
|
||||
SSHErr string `json:"ssh_error,omitempty" yaml:"ssh_error,omitempty"`
|
||||
Stress []StressResult `json:"stress,omitempty" yaml:"stress,omitempty"`
|
||||
MAC string `json:"mac" yaml:"mac"`
|
||||
IP string `json:"ip" yaml:"ip"`
|
||||
Alive bool `json:"alive" yaml:"alive"`
|
||||
OpenPort int `json:"open_port,omitempty" yaml:"open_port,omitempty"`
|
||||
SSHLogin bool `json:"ssh_login" yaml:"ssh_login"`
|
||||
SSHErr string `json:"ssh_error,omitempty" yaml:"ssh_error,omitempty"`
|
||||
Stress []StressResult `json:"stress,omitempty" yaml:"stress,omitempty"`
|
||||
Curves map[string][]float64 `json:"curves,omitempty" yaml:"curves,omitempty"` // 指标名 → 时间序列(渲染为 sparkline)
|
||||
}
|
||||
|
||||
// StressResult 单项压力测试结果
|
||||
@@ -56,8 +58,8 @@ type ConfigSummary struct {
|
||||
|
||||
// Builder 报告构建器
|
||||
type Builder struct {
|
||||
report *Report
|
||||
phase *PhaseResult
|
||||
report *Report
|
||||
phase *PhaseResult
|
||||
}
|
||||
|
||||
// NewBuilder 创建报告构建器
|
||||
@@ -84,13 +86,13 @@ func (b *Builder) AddHost(h HostResult) {
|
||||
}
|
||||
}
|
||||
|
||||
// AddStressResult 为指定 IP 添加压力测试结果
|
||||
func (b *Builder) AddStressResult(ip string, sr StressResult) {
|
||||
// AddStressResult 为指定设备(MAC)添加压力测试结果
|
||||
func (b *Builder) AddStressResult(mac string, sr StressResult) {
|
||||
if b.phase == nil {
|
||||
return
|
||||
}
|
||||
for i := range b.phase.Hosts {
|
||||
if b.phase.Hosts[i].IP == ip {
|
||||
if b.phase.Hosts[i].MAC == mac {
|
||||
b.phase.Hosts[i].Stress = append(b.phase.Hosts[i].Stress, sr)
|
||||
return
|
||||
}
|
||||
@@ -188,14 +190,10 @@ func (r *Report) ToText() string {
|
||||
continue
|
||||
}
|
||||
if phase.Phase == "SSH 登录" && !h.SSHLogin {
|
||||
sb.WriteString(fmt.Sprintf(" ✗ %-15s %s\n", h.IP, h.SSHErr))
|
||||
sb.WriteString(fmt.Sprintf(" ✗ %-19s %-15s %s\n", h.MAC, h.IP, h.SSHErr))
|
||||
continue
|
||||
}
|
||||
icon := "✓"
|
||||
if phase.Phase == "SSH 登录" {
|
||||
icon = "✓"
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf(" %s %-15s\n", icon, h.IP))
|
||||
sb.WriteString(fmt.Sprintf(" ✓ %-19s %-15s\n", h.MAC, h.IP))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,7 +202,7 @@ func (r *Report) ToText() string {
|
||||
if len(h.Stress) == 0 {
|
||||
continue
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf(" ┌─ %s\n", h.IP))
|
||||
sb.WriteString(fmt.Sprintf(" ┌─ %s (%s)\n", h.MAC, h.IP))
|
||||
for _, s := range h.Stress {
|
||||
icon := "✓"
|
||||
if s.Status != "pass" {
|
||||
@@ -260,3 +258,22 @@ func DefaultReportPath() string {
|
||||
timestamp := time.Now().Format("20060102-150405")
|
||||
return filepath.Join(".", "reports", fmt.Sprintf("report-%s.txt", timestamp))
|
||||
}
|
||||
|
||||
// ============================
|
||||
// 渲染 + 打印便捷方法
|
||||
// ============================
|
||||
|
||||
// PrintTo 使用 80mm 模板渲染并打印
|
||||
// printer 为 nil 时仅返回渲染文本
|
||||
func (r *Report) PrintTo(printer Printer) (string, error) {
|
||||
text, err := r.Render80mm()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if printer != nil {
|
||||
if err := printer.Print(text); err != nil {
|
||||
return text, fmt.Errorf("打印失败: %w", err)
|
||||
}
|
||||
}
|
||||
return text, nil
|
||||
}
|
||||
|
||||
161
auto-check/pkg/report/template.go
Normal file
161
auto-check/pkg/report/template.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package report
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ============================
|
||||
// 模板渲染(80mm 热敏纸适配)
|
||||
// ============================
|
||||
|
||||
// TemplateConfig 模板渲染配置
|
||||
type TemplateConfig struct {
|
||||
Width int // 行宽(字符数),80mm 热敏纸建议 42
|
||||
}
|
||||
|
||||
// DefaultTemplateConfig 默认配置(80mm 热敏纸)
|
||||
func DefaultTemplateConfig() TemplateConfig {
|
||||
return TemplateConfig{Width: 42}
|
||||
}
|
||||
|
||||
// RenderTemplate 使用 Go template 渲染报告
|
||||
// tpl 为模板内容,data 为报告数据
|
||||
func RenderTemplate(tpl string, data interface{}, cfg TemplateConfig) (string, error) {
|
||||
if cfg.Width <= 0 {
|
||||
cfg.Width = 42
|
||||
}
|
||||
|
||||
funcMap := template.FuncMap{
|
||||
// 格式化辅助
|
||||
"timeFmt": func(t time.Time) string { return t.Format("2006-01-02 15:04:05") },
|
||||
// 字符串截断/填充到固定宽度
|
||||
"trunc": func(s string, n int) string {
|
||||
r := []rune(s)
|
||||
if len(r) <= n {
|
||||
return string(r)
|
||||
}
|
||||
return string(r[:n-1]) + "…"
|
||||
},
|
||||
// 左侧填充(右对齐)
|
||||
"padRight": func(s string, n int) string {
|
||||
r := []rune(s)
|
||||
if len(r) >= n {
|
||||
return string(r[:n])
|
||||
}
|
||||
return s + strings.Repeat(" ", n-len(r))
|
||||
},
|
||||
// 分隔线
|
||||
"hr": func(sharp rune) string { return strings.Repeat(string(sharp), cfg.Width) },
|
||||
// 曲线图(时间序列 → sparkline)
|
||||
"sparkline": func(data []float64, width int) string {
|
||||
return Sparkline(data, width)
|
||||
},
|
||||
// 曲线摘要: 最高/最低/平均
|
||||
"curveSummary": func(data []float64) string {
|
||||
if len(data) == 0 {
|
||||
return "无数据"
|
||||
}
|
||||
min, max := math.MaxFloat64, -math.MaxFloat64
|
||||
var sum float64
|
||||
for _, v := range data {
|
||||
if v < min {
|
||||
min = v
|
||||
}
|
||||
if v > max {
|
||||
max = v
|
||||
}
|
||||
sum += v
|
||||
}
|
||||
avg := sum / float64(len(data))
|
||||
return fmt.Sprintf("max=%.1f min=%.1f avg=%.1f", max, min, avg)
|
||||
},
|
||||
}
|
||||
|
||||
t, err := template.New("report").Funcs(funcMap).Parse(tpl)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("解析模板失败: %w", err)
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
if err := t.Execute(&sb, data); err != nil {
|
||||
return "", fmt.Errorf("渲染模板失败: %w", err)
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
// ============================
|
||||
// Sparkline — 80mm 热敏纸曲线图
|
||||
// ============================
|
||||
|
||||
// sparkChars 8 级块状字符(低→高)
|
||||
var sparkChars = []rune("▁▂▃▄▅▆▇█")
|
||||
|
||||
// Sparkline 将时间序列渲染为一行块状曲线
|
||||
// width 为字符宽度(80mm 纸 ≤42)
|
||||
func Sparkline(data []float64, width int) string {
|
||||
if len(data) == 0 {
|
||||
return "(无数据)"
|
||||
}
|
||||
if width <= 0 {
|
||||
width = 42
|
||||
}
|
||||
if width > 42 {
|
||||
width = 42
|
||||
}
|
||||
|
||||
// 单点
|
||||
if len(data) == 1 {
|
||||
return fmt.Sprintf("▊ %.1f", data[0])
|
||||
}
|
||||
|
||||
// 按宽度采样(超出宽度时等距抽取)
|
||||
step := float64(len(data)) / float64(width)
|
||||
sampled := make([]float64, 0, width)
|
||||
for i := 0; i < width && int(float64(i)*step) < len(data); i++ {
|
||||
idx := int(float64(i) * step)
|
||||
if idx >= len(data) {
|
||||
break
|
||||
}
|
||||
sampled = append(sampled, data[idx])
|
||||
}
|
||||
|
||||
// 归一化到 0-7
|
||||
min, max := math.MaxFloat64, -math.MaxFloat64
|
||||
for _, v := range sampled {
|
||||
if v < min {
|
||||
min = v
|
||||
}
|
||||
if v > max {
|
||||
max = v
|
||||
}
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
if max-min < 1e-9 {
|
||||
// 全相同值
|
||||
idx := 0
|
||||
if max > 0 {
|
||||
idx = 3
|
||||
}
|
||||
for range sampled {
|
||||
sb.WriteRune(sparkChars[idx])
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
for _, v := range sampled {
|
||||
idx := int((v - min) / (max - min) * 7)
|
||||
if idx < 0 {
|
||||
idx = 0
|
||||
}
|
||||
if idx > 7 {
|
||||
idx = 7
|
||||
}
|
||||
sb.WriteRune(sparkChars[idx])
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
35
auto-check/pkg/report/template_embed.go
Normal file
35
auto-check/pkg/report/template_embed.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package report
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed templates/*.tmpl
|
||||
var templateFS embed.FS
|
||||
|
||||
// embeddedTemplates 内嵌模板列表
|
||||
var embeddedTemplates = []string{
|
||||
"templates/report_80mm.tmpl", // 80mm 热敏纸文本报告
|
||||
}
|
||||
|
||||
// Render80mm 渲染 80mm 热敏纸文本报告
|
||||
func (r *Report) Render80mm() (string, error) {
|
||||
return r.renderTemplate("templates/report_80mm.tmpl", DefaultTemplateConfig())
|
||||
}
|
||||
|
||||
// Render80mmWithConfig 使用自定义宽度渲染
|
||||
func (r *Report) Render80mmWithConfig(cfg TemplateConfig) (string, error) {
|
||||
return r.renderTemplate("templates/report_80mm.tmpl", cfg)
|
||||
}
|
||||
|
||||
// renderTemplate 从内嵌模板渲染
|
||||
func (r *Report) renderTemplate(name string, cfg TemplateConfig) (string, error) {
|
||||
data, err := templateFS.ReadFile(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return RenderTemplate(string(data), r, cfg)
|
||||
}
|
||||
|
||||
// TemplateNames 返回可用模板列表
|
||||
func TemplateNames() []string {
|
||||
return embeddedTemplates
|
||||
}
|
||||
27
auto-check/pkg/report/templates/report_80mm.tmpl
Normal file
27
auto-check/pkg/report/templates/report_80mm.tmpl
Normal file
@@ -0,0 +1,27 @@
|
||||
{{ hr '═' }}
|
||||
auto-check 检测报告
|
||||
{{ hr '═' }}
|
||||
生成时间: {{ timeFmt .StartTime }}
|
||||
总耗时: {{ .Duration }}
|
||||
{{ "" }}
|
||||
── 配置 ──
|
||||
网段: {{ .Config.CIDR }}
|
||||
SSH: {{ .Config.User }}@:{{ .Config.Port }}
|
||||
压测: {{ .Config.StressTypes }}
|
||||
并发: {{ .Config.Concurrency }}
|
||||
{{ "" }}
|
||||
{{ range .Phases }}{{ hr '─' }}
|
||||
{{ .Phase }} [{{ .Status }}]
|
||||
{{ .Summary }}
|
||||
{{ hr '─' }}
|
||||
{{ if eq .Phase "设备发现" }}{{ range .Hosts }}{{ if .Alive }}✓{{ else }}✗{{ end }} {{ padRight .MAC 19 }} {{ padRight .IP 15 }}
|
||||
{{ 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 .Error }} ERR: {{ trunc .Error 34 }}{{ end }}{{ end }}{{ range $name, $data := .Curves }} [{{ $name }}] {{ curveSummary $data }}
|
||||
{{ sparkline $data 38 }}
|
||||
{{ end }}{{ end }}{{ end }}{{ end }}
|
||||
{{ "" }}
|
||||
{{ hr '═' }}
|
||||
报告结束
|
||||
{{ hr '═' }}
|
||||
Reference in New Issue
Block a user