Files
membank/auto-check/pkg/report/report.go
12600k-rog-d4 2aaaa16ae2 0813
2026-08-13 22:49:50 +08:00

290 lines
8.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"`
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.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
}