295 lines
8.5 KiB
Go
295 lines
8.5 KiB
Go
package report
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"time"
|
||
|
||
"auto-check/pkg/stress"
|
||
)
|
||
|
||
// PhaseResult 单阶段结果
|
||
type PhaseResult struct {
|
||
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"`
|
||
}
|
||
|
||
// HostResult 单台设备结果(MAC 为唯一标识)
|
||
type HostResult struct {
|
||
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"`
|
||
}
|
||
|
||
// StressResult 单项压力测试结果
|
||
type StressResult struct {
|
||
Type string `json:"type" yaml:"type"`
|
||
Status string `json:"status" yaml:"status"` // pass / fail / skip
|
||
Duration string `json:"duration" yaml:"duration"`
|
||
Score float64 `json:"score,omitempty" yaml:"score,omitempty"`
|
||
ScoreUnit string `json:"score_unit,omitempty" yaml:"score_unit,omitempty"`
|
||
Metrics []stress.MetricItem `json:"metrics,omitempty" yaml:"metrics,omitempty"`
|
||
Benchmarks []stress.BenchItem `json:"benchmarks,omitempty" yaml:"benchmarks,omitempty"` // 性能/温度基准评级
|
||
OverallLevel string `json:"overall_level,omitempty" yaml:"overall_level,omitempty"`
|
||
OverallText string `json:"overall_text,omitempty" yaml:"overall_text,omitempty"`
|
||
Output string `json:"output,omitempty" yaml:"output,omitempty"`
|
||
Error string `json:"error,omitempty" yaml:"error,omitempty"`
|
||
}
|
||
|
||
// Report 检测报告
|
||
type Report struct {
|
||
StartTime time.Time `json:"start_time" yaml:"start_time"`
|
||
EndTime time.Time `json:"end_time" yaml:"end_time"`
|
||
Duration string `json:"duration" yaml:"duration"`
|
||
Config ConfigSummary `json:"config" yaml:"config"`
|
||
Phases []PhaseResult `json:"phases" yaml:"phases"`
|
||
// QRURL HTML 报告访问链接(用于热敏打印中的二维码与模板回退)
|
||
QRURL string `json:"qr_url,omitempty" yaml:"qr_url,omitempty"`
|
||
}
|
||
|
||
// ConfigSummary 配置摘要(脱敏)
|
||
type ConfigSummary struct {
|
||
CIDR string `json:"cidr" yaml:"cidr"`
|
||
Port int `json:"port" yaml:"port"`
|
||
User string `json:"user" yaml:"user"`
|
||
Concurrency int `json:"concurrency" yaml:"concurrency"`
|
||
StressTypes string `json:"stress_types,omitempty" yaml:"stress_types,omitempty"`
|
||
}
|
||
|
||
// Builder 报告构建器
|
||
type Builder struct {
|
||
report *Report
|
||
phase *PhaseResult
|
||
}
|
||
|
||
// NewBuilder 创建报告构建器
|
||
func NewBuilder(cfg ConfigSummary) *Builder {
|
||
return &Builder{
|
||
report: &Report{
|
||
StartTime: time.Now(),
|
||
Config: cfg,
|
||
},
|
||
}
|
||
}
|
||
|
||
// StartPhase 开始新阶段
|
||
func (b *Builder) StartPhase(name string) {
|
||
b.phase = &PhaseResult{
|
||
Phase: name,
|
||
}
|
||
}
|
||
|
||
// AddHost 添加主机结果到当前阶段
|
||
func (b *Builder) AddHost(h HostResult) {
|
||
if b.phase != nil {
|
||
b.phase.Hosts = append(b.phase.Hosts, h)
|
||
}
|
||
}
|
||
|
||
// 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].MAC == mac {
|
||
b.phase.Hosts[i].Stress = append(b.phase.Hosts[i].Stress, sr)
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
// EndPhase 结束当前阶段,设置状态和摘要
|
||
func (b *Builder) EndPhase(summary string) {
|
||
if b.phase == nil {
|
||
return
|
||
}
|
||
b.phase.Summary = summary
|
||
|
||
total := len(b.phase.Hosts)
|
||
success := 0
|
||
for _, h := range b.phase.Hosts {
|
||
switch b.phase.Phase {
|
||
case "设备发现":
|
||
if h.Alive {
|
||
success++
|
||
}
|
||
case "SSH 登录":
|
||
if h.SSHLogin {
|
||
success++
|
||
}
|
||
case "压力测试":
|
||
if len(h.Stress) > 0 {
|
||
success++
|
||
}
|
||
}
|
||
}
|
||
|
||
switch {
|
||
case success == total:
|
||
b.phase.Status = "success"
|
||
case success > 0:
|
||
b.phase.Status = "partial"
|
||
default:
|
||
b.phase.Status = "failed"
|
||
}
|
||
|
||
b.report.Phases = append(b.report.Phases, *b.phase)
|
||
b.phase = nil
|
||
}
|
||
|
||
// Build 生成最终报告
|
||
func (b *Builder) Build() *Report {
|
||
b.report.EndTime = time.Now()
|
||
b.report.Duration = b.report.EndTime.Sub(b.report.StartTime).Round(time.Millisecond).String()
|
||
return b.report
|
||
}
|
||
|
||
// ============================
|
||
// 输出格式化
|
||
// ============================
|
||
|
||
// ToText 输出纯文本报告
|
||
func (r *Report) ToText() string {
|
||
var sb strings.Builder
|
||
|
||
sb.WriteString("╔══════════════════════════════════════════════╗\n")
|
||
sb.WriteString("║ auto-check 检测报告 ║\n")
|
||
sb.WriteString("╚══════════════════════════════════════════════╝\n\n")
|
||
|
||
sb.WriteString(fmt.Sprintf(" 生成时间: %s\n", r.StartTime.Format("2006-01-02 15:04:05")))
|
||
sb.WriteString(fmt.Sprintf(" 总耗时: %s\n\n", r.Duration))
|
||
|
||
// 配置摘要
|
||
sb.WriteString("── 配置摘要 ──\n")
|
||
sb.WriteString(fmt.Sprintf(" 目标: %s\n", r.Config.CIDR))
|
||
sb.WriteString(fmt.Sprintf(" SSH端口: %d\n", r.Config.Port))
|
||
sb.WriteString(fmt.Sprintf(" SSH用户: %s\n", r.Config.User))
|
||
sb.WriteString(fmt.Sprintf(" 并发数: %d\n", r.Config.Concurrency))
|
||
if r.Config.StressTypes != "" {
|
||
sb.WriteString(fmt.Sprintf(" 压测类型: %s\n", r.Config.StressTypes))
|
||
}
|
||
sb.WriteString("\n")
|
||
|
||
// 各阶段结果
|
||
for _, phase := range r.Phases {
|
||
statusIcon := "✅"
|
||
switch phase.Status {
|
||
case "partial":
|
||
statusIcon = "⚠️"
|
||
case "failed":
|
||
statusIcon = "❌"
|
||
}
|
||
|
||
sb.WriteString(fmt.Sprintf("══ %s %s ══\n", phase.Phase, statusIcon))
|
||
sb.WriteString(fmt.Sprintf(" %s\n\n", phase.Summary))
|
||
|
||
if phase.Phase == "设备发现" || phase.Phase == "SSH 登录" {
|
||
for _, h := range phase.Hosts {
|
||
if phase.Phase == "设备发现" && !h.Alive {
|
||
continue
|
||
}
|
||
if phase.Phase == "SSH 登录" && !h.SSHLogin {
|
||
sb.WriteString(fmt.Sprintf(" ✗ %-19s %-15s %s\n", h.MAC, h.IP, h.SSHErr))
|
||
continue
|
||
}
|
||
sb.WriteString(fmt.Sprintf(" ✓ %-19s %-15s\n", h.MAC, h.IP))
|
||
}
|
||
}
|
||
|
||
if phase.Phase == "压力测试" {
|
||
for _, h := range phase.Hosts {
|
||
if len(h.Stress) == 0 {
|
||
continue
|
||
}
|
||
sb.WriteString(fmt.Sprintf(" ┌─ %s (%s)\n", h.MAC, h.IP))
|
||
for _, s := range h.Stress {
|
||
icon := "✓"
|
||
if s.Status != "pass" {
|
||
icon = "✗"
|
||
}
|
||
sb.WriteString(fmt.Sprintf(" │ %s %-10s 耗时: %s\n", icon, s.Type, s.Duration))
|
||
if s.Score > 0 {
|
||
sb.WriteString(fmt.Sprintf(" │ 跑分: %.1f %s\n", s.Score, s.ScoreUnit))
|
||
}
|
||
for _, m := range s.Metrics {
|
||
sb.WriteString(fmt.Sprintf(" │ %s: %.1f%s\n", m.Label, m.Value, m.Unit))
|
||
}
|
||
if s.Error != "" {
|
||
sb.WriteString(fmt.Sprintf(" │ 错误: %s\n", s.Error))
|
||
}
|
||
}
|
||
sb.WriteString(" └──────────────\n")
|
||
}
|
||
}
|
||
sb.WriteString("\n")
|
||
}
|
||
|
||
return sb.String()
|
||
}
|
||
|
||
// ToJSON 输出 JSON 报告
|
||
func (r *Report) ToJSON() (string, error) {
|
||
data, err := json.MarshalIndent(r, "", " ")
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
return string(data), nil
|
||
}
|
||
|
||
// SaveFile 保存报告到文件
|
||
func (r *Report) SaveFile(path string, format string) error {
|
||
dir := filepath.Dir(path)
|
||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||
return fmt.Errorf("创建目录失败: %w", err)
|
||
}
|
||
|
||
var content string
|
||
switch format {
|
||
case "json":
|
||
var err error
|
||
content, err = r.ToJSON()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
default:
|
||
content = r.ToText()
|
||
}
|
||
|
||
return os.WriteFile(path, []byte(content), 0644)
|
||
}
|
||
|
||
// DefaultReportPath 默认报告路径
|
||
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
|
||
}
|