You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Golang实现分钟级转小时级数据:聚合求和与均值计算问题

问题描述

我有一张存储分钟级数据的数据库表,需要按小时对数据做聚合:求和相关字段,同时计算PH、COD列的均值。自己编写了Golang函数,但运行后出现数据重复问题,不确定是代码逻辑还是实现出错。因担心服务器开销,不想用原生SQL,希望通过逐行累加生成小时级新记录,同时算出PH、COD的均值。


代码示例

func (server *Server) DataPerHour(w http.ResponseWriter, r *http.Request) {
    var PH, COD float64
    var countEntry float64

    sparingData := models.SparingReport{}
    sparingHourData := models.SparingReportHour{}

    countsparingdatalast, _ := sparingData.CountLastSparingReportHourByID(server.DB, 2)

    if *countsparingdatalast > 0 {
        sparingDataPerHour, _ := sparingData.FindAllSparingReport(server.DB)

        if len(*sparingDataPerHour) > 0 {
            startHour := (*sparingDataPerHour)[0].RecordedAt.Hour()

            for _, dataReceived := range *sparingDataPerHour {
                nowHour := dataReceived.RecordedAt.Hour()

                if startHour == nowHour {
                    if dataReceived.PH > 0 {
                        PH += dataReceived.PH
                    }
                    if dataReceived.COD > 0 {
                        COD += dataReceived.COD
                    }

                    countEntry++
                } else if startHour != nowHour {
                    sparingHourData.Prepare()

                    sparingHourData.ID = dataReceived.ID
                    sparingHourData.RecordedAt = dataReceived.RecordedAt
                    if countEntry > 0 {
                        sparingHourData.PH = PH / countEntry
                        sparingHourData.COD = COD / countEntry
                    }

                    sparingHourData.Prepare()
                    sparingHourData.IsProcess = true
                    _, _ = sparingHourData.SaveSparingReportHour(server.DB)

                    PH = 0
                    COD = 0
                    countEntry = 0

                    startHour = nowHour
                }
            }
        }
    }
}

当前输出

当前输出数据重复


预期输出

IDRecordedAtPHCOD
12023-11-17 19:00:00+007.125.5
22023-11-17 20:00:00+007.824.7

问题分析与修复方案

你的代码存在几个核心逻辑问题,导致数据重复或不符合预期:

  1. 未处理最后一小时的数据:循环结束后,最后一组累加的小时数据没有写入数据库
  2. 仅按小时分组,忽略日期:跨天的同一小时(如11月17日23点和11月18日0点)会被错误区分或合并
  3. 时间未对齐到整点:直接使用原数据的RecordedAt,导致小时级记录时间不是整点
  4. 结构体复用导致数据残留:循环中复用同一个sparingHourData实例,若Prepare()未完全重置字段,会携带旧数据
  5. ID赋值错误:直接复用原分钟级数据的ID,导致小时级记录ID重复
  6. 错误处理缺失:忽略所有错误,无法排查数据读写失败的问题

修复后的代码

import (
    "log"
    "net/http"
    "time"
)

func (server *Server) DataPerHour(w http.ResponseWriter, r *http.Request) {
    var PH, COD float64
    var countEntry int

    sparingData := models.SparingReport{}

    // 计数查询添加错误处理
    countsparingdatalast, err := sparingData.CountLastSparingReportHourByID(server.DB, 2)
    if err != nil {
        http.Error(w, "获取小时数据计数失败", http.StatusInternalServerError)
        return
    }

    if *countsparingdatalast > 0 {
        sparingDataPerHour, err := sparingData.FindAllSparingReport(server.DB)
        if err != nil {
            http.Error(w, "获取分钟数据失败", http.StatusInternalServerError)
            return
        }

        if len(*sparingDataPerHour) == 0 {
            return
        }

        // 用日期+小时作为分组键,解决跨天问题
        currentGroup := getHourGroup((*sparingDataPerHour)[0].RecordedAt)

        for _, dataReceived := range *sparingDataPerHour {
            dataGroup := getHourGroup(dataReceived.RecordedAt)

            if dataGroup == currentGroup {
                // 累加有效数据,单独判断避免无效值影响均值
                if dataReceived.PH > 0 {
                    PH += dataReceived.PH
                    countEntry++
                }
                if dataReceived.COD > 0 {
                    COD += dataReceived.COD
                }
            } else {
                // 写入上一小时的聚合数据
                saveHourData(currentGroup, PH, COD, countEntry, server.DB)

                // 重置累加器
                PH = 0
                COD = 0
                countEntry = 0
                currentGroup = dataGroup

                // 处理当前数据的首次累加
                if dataReceived.PH > 0 {
                    PH += dataReceived.PH
                    countEntry++
                }
                if dataReceived.COD > 0 {
                    COD += dataReceived.COD
                }
            }
        }

        // 处理最后一小时的数据
        saveHourData(currentGroup, PH, COD, countEntry, server.DB)
    }
}

// 定义分组键,区分不同日期的同一小时
type hourGroup struct {
    year  int
    month time.Month
    day   int
    hour  int
}

// 生成时间对应的分组键
func getHourGroup(t time.Time) hourGroup {
    t = t.UTC() // 统一使用UTC时间,避免时区差异问题
    return hourGroup{
        year:  t.Year(),
        month: t.Month(),
        day:   t.Day(),
        hour:  t.Hour(),
    }
}

// 封装小时数据保存逻辑
func saveHourData(group hourGroup, PH, COD float64, count int, db *gorm.DB) {
    if count == 0 {
        return
    }

    // 每次创建新实例,避免数据残留
    sparingHourData := models.SparingReportHour{}
    sparingHourData.Prepare()

    // 生成整点时间
    sparingHourData.RecordedAt = time.Date(group.year, group.month, group.day, group.hour, 0, 0, 0, time.UTC)
    sparingHourData.PH = PH / float64(count)
    sparingHourData.COD = COD / float64(count)
    sparingHourData.IsProcess = true

    // 保存时处理错误
    if _, err := sparingHourData.SaveSparingReportHour(db); err != nil {
        log.Printf("保存小时数据失败: %v", err)
    }
}

关键修复点说明

  • 跨天分组处理:用日期+小时作为分组依据,避免不同日期的同一小时被错误合并
  • 最后一小时数据处理:循环结束后单独处理最后一组累加数据
  • 时间对齐:生成严格的整点时间作为小时级记录的RecordedAt
  • 结构体实例隔离:每次保存时创建新的sparingHourData实例,避免旧数据残留
  • 错误处理:添加HTTP错误返回和日志记录,方便排查问题
  • ID逻辑修正:移除手动赋值ID的代码,依赖数据库自增逻辑(若表结构支持)

内容的提问来源于stack exchange,提问作者Cybercand

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 06:17:40