package report import ( "encoding/json" "fmt" "os" "path/filepath" "strings" "time" ) // 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"` Curves map[string][]float64 `json:"curves,omitempty" yaml:"curves,omitempty"` // 指标名 → 时间序列(渲染为 sparkline) } // 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"` 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"` } // 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.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 }