一、项目背景IDC带宽计费普遍采用95百分位计费法。原理很简单：一个月内每5分钟采集一次带宽数据，按值从大到小排序，去掉最高的5%，剩余数据中的最大值即为计费带宽。这种计费方式对客户有利——偶尔的流量突发不会被计入费用。但对运维来说，每月手动统计、制表、发邮件是重复劳动，以前每月我都需要拿出1天时间，来导出zabbix的监控数据，并计算95值，然后做报表，发邮件。开发这个系统的目的就是把这些事情自动化，提高效率节省时间。二、系统介绍首页首页展示系统概况，包括项目数量、报告统计、快捷入口等。项目管理项目是系统的核心概念，每个项目对应一个计费主体（比如一个客户或一条专线）。项目下挂载多个端口，端口信息从Zabbix同步过来。新建项目时配置基本信息：趋势方向（UP/DOWN，决定取流入还是流出）、数据源、Grafana Dashboard等。数据同步流量数据存储在Zabbix的MySQL数据库中。系统定时从Zabbix同步数据到本地表，避免直接查询生产库造成压力。95计费结果选择时间范围后，系统自动计算各项目的95值。支持一键复制为Markdown表格，方便粘贴到汇报文档。报告管理报告是月度统计的产出物，包含95值、峰值、Grafana截图、CSV数据文件。报告支持批量创建、状态管理、一键发送邮件。数据管理支持项目配置的导入导出、流量数据的导出，便于备份和迁移。系统设置配置通过环境变量管理，包括数据库连接、Grafana地址、SMTP信息等。邮件通知报告发送后，收件人会收到包含统计结果、图表附件的邮件。三、架构设计整体架构┌─────────────────────────────────────────────────────────────┐ │ 前端 (Vue 3) │ │ Element Plus + Vue Router + Axios │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 后端 (Go + Gin) │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ Handler │──│ Service │──│Repository│──│ Model │ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ └─────────────────────────────────────────────────────────────┘ │ ┌───────────────┼───────────────┐ ▼ ▼ ▼ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ MySQL │ │ Zabbix │ │ Grafana │ │ (zreport)│ │ (只读) │ │ (截图) │ └──────────┘ └──────────┘ └──────────┘目录结构aireport/ ├── cmd/server/main.go # 入口 ├── internal/ │ ├── config/ # 配置加载 │ ├── models/ # 数据模型 │ ├── repository/ # 数据访问层 │ ├── services/ # 业务逻辑层 │ ├── handlers/ # HTTP处理器 │ └── router/ # 路由配置 ├── pkg/ │ ├── utils/ # 工具函数（95计算） │ └── response/ # 统一响应格式 ├── web/ # 前端项目 │ └── src/ │ ├── api/ # 接口封装 │ ├── views/ # 页面组件 │ └── router/ # 前端路由 └── exports/ # 导出文件存储技术选型层面技术选择理由后端框架Gin轻量、性能好、社区活跃ORMGORM功能完善、开发效率高前端框架Vue 3组合式API、响应式设计方便UI组件库Element Plus组件丰富、文档完善数据库MySQL业务数据量小、关系型足够四、核心代码95计费算法95计费的核心是排序取百分位，实现并不复杂：// Calculate95thPercentile 计算95计费 // 排序后取第5%位置的值 func Calculate95thPercentile(data []DataPoint, unitBase int) BillingResult { if len(data) == 0 { return BillingResult{} } // 确定进制，影响Gbps转换 divisor := float64(1e9) // 1000进制 if unitBase == 1024 { divisor = 1073741824 // 1024进制 } // 按值降序排序 sort.Slice(data, func(i, j int) bool { return data[i].Value > data[j].Value }) // 计算要去掉的数据条数（5%） totalCount := len(data) removeCount := int(math.Round(float64(totalCount) / 20)) // 95值就是第removeCount位置的那个值 index95 := removeCount if index95 >= totalCount { index95 = totalCount - 1 } return BillingResult{ TotalCount: totalCount, RemoveCount: removeCount, Result95: data[index95].Value, Result95Gbps: float64(data[index95].Value) / divisor, PeakValue: data[0].Value, PeakGbps: float64(data[0].Value) / divisor, } }这里有个细节：进制选择。Grafana默认用1000进制（1Gbps = 10^9 bps），但有些场景用1024进制。系统支持在项目级别配置，保证计算结果和监控图表一致。多端口数据合并一个项目可能有多个端口，需要把所有端口的流量按时间点累加：// combineByTime 按时间点合并数据 func (s *AnalysisService) combineByTime(data []utils.DataPoint) []utils.DataPoint { timeMap := make(map[int]uint64) for _, d := range data { timeMap[d.Clock] += d.Value } result := make([]utils.DataPoint, 0, len(timeMap)) for clock, value := range timeMap { result = append(result, utils.DataPoint{ Clock: clock, Value: value, }) } return result }合并后再进行95计算，得到的是项目整体带宽。Grafana截图报告需要附带流量图表，直接调用Grafana的渲染接口：func (s *ReportService) downloadGrafanaImage(project *models.Project, startTs, endTs int) (string, error) { url := fmt.Sprintf("%s/render/d/%s?from=%d&to=%d&width=%d&height=%d", s.grafanaURL, project.GrafanaUID, startTs*1000, endTs*1000, project.ChartWidth, project.ChartHeight, ) req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", s.grafanaAPIKey) client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) // ... 保存图片 }邮件发送报告准备好后，调用SMTP发送：func (s *EmailService) SendReportWithAttachment( report *models.Report, imageData []byte, csvData []byte, csvFilename string, recipients []string, ) error { m := gomail.NewMessage() m.SetHeader("From", s.from) m.SetHeader("To", recipients...) m.SetHeader("Subject", report.Title) // HTML正文 body := fmt.Sprintf(` %s 月份: %s 95计费值: %.2f Gbps 峰值: %.2f Gbps `, report.Title, report.Month, report.Result95, report.PeakValue) m.SetBody("text/html", body) // 附件 if imageData != nil { m.Attach("chart.png", gomail.SetCopyFunc(func(w io.Writer) error { _, err := w.Write(imageData) return err })) } // 发送 d := gomail.NewDialer(s.host, s.port, s.user, s.password) return d.DialAndSend(m) }五、数据模型主要表结构// Project 项目 type Project struct { ID uint `gorm:"primaryKey"` Name string `gorm:"uniqueIndex"` Host string // 交换机主机 Trend string // UP/DOWN Source string // 数据源表名 UnitBase int // 进制: 1000 或 1024 GrafanaUID string // Grafana Dashboard ID Ports []Port `gorm:"foreignKey:ProjectID"` } // Port 端口 type Port struct { ID uint `gorm:"primaryKey"` ProjectID uint Name string ItemIDIn uint64 // Zabbix流入监控项ID ItemIDOut uint64 // Zabbix流出监控项ID } // Report 报告 type Report struct { ID uint ProjectID uint Month string // 2026-02 Result95 float64 // Gbps PeakValue float64 // Gbps Status string // draft/sent/failed }Zabbix数据表系统直接读取Zabbix的history_uint表，结构如下：-- Zabbix原始表 CREATE TABLE history_uint ( itemid BIGINT UNSIGNED, clock INT, -- Unix时间戳 value BIGINT UNSIGNED, ns INT -- 纳秒 );每个itemid对应一个监控项（比如某端口的入流量）。我们关心的是itemid、clock、value三个字段。六、部署系统采用单机部署，前后端打包在一起：# 构建前端 cd web && npm run build # 构建后端 go build -o aireport ./cmd/server # 运行 ./aireport配置通过.env文件管理：# 数据库 DB_HOST=127.0.0.1 DB_PORT=3306 DB_USER=root DB_PASSWORD=xxx DB_NAME=zreport # Zabbix数据库（只读） ZABBIX_DB_HOST=127.0.0.1 ZABBIX_DB_NAME=zabbix # Grafana GRAFANA_URL=http://localhost:3000 GRAFANA_API_KEY=xxx # 邮件 SMTP_HOST=smtp.163.com SMTP_PORT=465 SMTP_USER=xxx@163.com SMTP_PASSWORD=xxx RECIPIENTS=xxx@qq.com,yyy@163.com七、总结这个系统解决了一个具体的运维痛点：每月的带宽统计和报告发送。开发过程中几个关键点：数据源选择：直接读Zabbix数据库比调API效率高，但要注意不要影响Zabbix自身性能，所以加了同步机制。进制问题：带宽单位换算有1000和1024两种标准，必须和监控平台保持一致，否则数据对不上。多端口聚合：一个客户多条线路是常见情况，需要按时间点累加后再计算95值。报告自动化：最大的价值在于节省人工，一键生成报告并发送邮件。系统功能相对简单，但覆盖了完整的业务闭环：数据采集 → 计算 → 报告 → 通知。后续可以根据需求扩展，比如增加历史趋势分析、异常告警等功能。