feat: perf chart

This commit is contained in:
2026-05-20 08:58:47 +08:00
parent 3f499ddf27
commit df27dfdb03
+247 -62
View File
@@ -4,6 +4,7 @@ import (
"errors" "errors"
"hr_receiver/config" "hr_receiver/config"
"hr_receiver/models" "hr_receiver/models"
"math"
"net/http" "net/http"
"sort" "sort"
"strconv" "strconv"
@@ -73,6 +74,13 @@ type mqttMeasurementHistorySummary struct {
ValueLabel string `json:"valueLabel"` ValueLabel string `json:"valueLabel"`
} }
const (
mqttChartRawPointLimit int64 = 5000
mqttChartTargetPointsTotal int64 = 6000
mqttChartMinPointsPerAddr int64 = 120
mqttChartMaxPointsPerAddr int64 = 600
)
// --- 查询接口 --- // --- 查询接口 ---
// @Summary 获取AI分析记录列表 // @Summary 获取AI分析记录列表
@@ -278,14 +286,6 @@ func (sc *StatisticsController) chartHeartRateMeasurementHistory(c *gin.Context,
baseQuery := sc.DB.Model(&models.MqttHeartRateRecord{}) baseQuery := sc.DB.Model(&models.MqttHeartRateRecord{})
baseQuery = applyMqttMeasurementFilters(baseQuery, params, "belt_addr", "heart_rate", "received_at") baseQuery = applyMqttMeasurementFilters(baseQuery, params, "belt_addr", "heart_rate", "received_at")
var records []models.MqttHeartRateRecord
if err := baseQuery.Session(&gorm.Session{}).
Order("received_at ASC").
Find(&records).Error; err != nil {
writeError(c, http.StatusInternalServerError, "failed to query mqtt heart rate chart records")
return
}
var summary mqttMeasurementHistorySummary var summary mqttMeasurementHistorySummary
summary.ValueLabel = "心率" summary.ValueLabel = "心率"
if err := buildMqttMeasurementSummary(baseQuery.Session(&gorm.Session{}), "heart_rate", "belt_addr", &summary); err != nil { if err := buildMqttMeasurementSummary(baseQuery.Session(&gorm.Session{}), "heart_rate", "belt_addr", &summary); err != nil {
@@ -293,32 +293,17 @@ func (sc *StatisticsController) chartHeartRateMeasurementHistory(c *gin.Context,
return return
} }
items := make([]mqttMeasurementHistoryItem, 0, len(records)) items, bucketSizeMs, sampled, err := sc.buildHeartRateChartItems(baseQuery, params, summary)
for _, record := range records { if err != nil {
items = append(items, mqttMeasurementHistoryItem{ writeError(c, http.StatusInternalServerError, "failed to query mqtt heart rate chart records")
ID: record.ID, return
DataType: "heart_rate",
Identifier: record.Identifier,
Topic: record.Topic,
RegionID: record.RegionID,
Addr: record.BeltAddr,
Value: int64(record.HeartRate),
ValueLabel: "心率",
PacketNum: record.PacketNum,
GatewayMAC: record.GatewayMAC,
Battery: record.Battery,
IsActive: record.IsActive,
IsOnSkin: record.IsOnSkin,
SignalRSSINeg: record.SignalRSSINeg,
SNR: record.SNR,
ReceivedAt: record.ReceivedAt,
CreatedAtMilli: record.CreatedAt.UnixMilli(),
})
} }
writeSuccess(c, http.StatusOK, "query success", gin.H{ writeSuccess(c, http.StatusOK, "query success", gin.H{
"list": items, "list": items,
"summary": summary, "summary": summary,
"sampled": sampled,
"bucketSizeMs": bucketSizeMs,
}) })
} }
@@ -389,14 +374,6 @@ func (sc *StatisticsController) chartStepCountMeasurementHistory(c *gin.Context,
baseQuery := sc.DB.Model(&models.MqttStepCountRecord{}) baseQuery := sc.DB.Model(&models.MqttStepCountRecord{})
baseQuery = applyMqttMeasurementFilters(baseQuery, params, "belt_addr", "step_count", "received_at") baseQuery = applyMqttMeasurementFilters(baseQuery, params, "belt_addr", "step_count", "received_at")
var records []models.MqttStepCountRecord
if err := baseQuery.Session(&gorm.Session{}).
Order("received_at ASC").
Find(&records).Error; err != nil {
writeError(c, http.StatusInternalServerError, "failed to query mqtt step count chart records")
return
}
var summary mqttMeasurementHistorySummary var summary mqttMeasurementHistorySummary
summary.ValueLabel = "步数" summary.ValueLabel = "步数"
if err := buildMqttMeasurementSummary(baseQuery.Session(&gorm.Session{}), "step_count", "belt_addr", &summary); err != nil { if err := buildMqttMeasurementSummary(baseQuery.Session(&gorm.Session{}), "step_count", "belt_addr", &summary); err != nil {
@@ -404,32 +381,17 @@ func (sc *StatisticsController) chartStepCountMeasurementHistory(c *gin.Context,
return return
} }
items := make([]mqttMeasurementHistoryItem, 0, len(records)) items, bucketSizeMs, sampled, err := sc.buildStepCountChartItems(baseQuery, params, summary)
for _, record := range records { if err != nil {
items = append(items, mqttMeasurementHistoryItem{ writeError(c, http.StatusInternalServerError, "failed to query mqtt step count chart records")
ID: record.ID, return
DataType: "step_count",
Identifier: record.Identifier,
Topic: record.Topic,
RegionID: record.RegionID,
Addr: record.BeltAddr,
Value: int64(record.StepCount),
ValueLabel: "步数",
PacketNum: record.PacketNum,
GatewayMAC: record.GatewayMAC,
Battery: 0,
IsActive: false,
IsOnSkin: false,
SignalRSSINeg: record.SignalRSSINeg,
SNR: record.SNR,
ReceivedAt: record.ReceivedAt,
CreatedAtMilli: record.CreatedAt.UnixMilli(),
})
} }
writeSuccess(c, http.StatusOK, "query success", gin.H{ writeSuccess(c, http.StatusOK, "query success", gin.H{
"list": items, "list": items,
"summary": summary, "summary": summary,
"sampled": sampled,
"bucketSizeMs": bucketSizeMs,
}) })
} }
@@ -487,6 +449,229 @@ func buildMqttMeasurementSummary(query *gorm.DB, valueField, addrField string, s
return nil return nil
} }
func estimateChartBucketSizeMs(params mqttMeasurementHistoryParams, summary mqttMeasurementHistorySummary) int64 {
if params.StartTime <= 0 || params.EndTime <= params.StartTime {
return 0
}
if summary.RecordCount <= mqttChartRawPointLimit {
return 0
}
uniqueAddrs := summary.UniqueAddrs
if uniqueAddrs < 1 {
uniqueAddrs = 1
}
pointsPerAddr := mqttChartTargetPointsTotal / uniqueAddrs
if pointsPerAddr < mqttChartMinPointsPerAddr {
pointsPerAddr = mqttChartMinPointsPerAddr
}
if pointsPerAddr > mqttChartMaxPointsPerAddr {
pointsPerAddr = mqttChartMaxPointsPerAddr
}
rangeMs := params.EndTime - params.StartTime
if rangeMs <= 0 {
return 0
}
bucketSizeMs := int64(math.Ceil(float64(rangeMs) / float64(pointsPerAddr)))
return normalizeBucketSizeMs(bucketSizeMs)
}
func normalizeBucketSizeMs(bucketSizeMs int64) int64 {
if bucketSizeMs <= 1000 {
return 1000
}
candidates := []int64{
1000,
2000,
5000,
10 * 1000,
15 * 1000,
30 * 1000,
60 * 1000,
2 * 60 * 1000,
5 * 60 * 1000,
10 * 60 * 1000,
15 * 60 * 1000,
30 * 60 * 1000,
60 * 60 * 1000,
2 * 60 * 60 * 1000,
6 * 60 * 60 * 1000,
12 * 60 * 60 * 1000,
24 * 60 * 60 * 1000,
}
for _, candidate := range candidates {
if bucketSizeMs <= candidate {
return candidate
}
}
return 24 * 60 * 60 * 1000
}
func buildChartBucketExpr(bucketSizeMs int64, timeField string) string {
return "((" + timeField + " / " + strconv.FormatInt(bucketSizeMs, 10) + ") * " + strconv.FormatInt(bucketSizeMs, 10) + ")"
}
func (sc *StatisticsController) buildHeartRateChartItems(baseQuery *gorm.DB, params mqttMeasurementHistoryParams, summary mqttMeasurementHistorySummary) ([]mqttMeasurementHistoryItem, int64, bool, error) {
bucketSizeMs := estimateChartBucketSizeMs(params, summary)
if bucketSizeMs == 0 {
var records []models.MqttHeartRateRecord
if err := baseQuery.Session(&gorm.Session{}).
Order("received_at ASC").
Find(&records).Error; err != nil {
return nil, 0, false, err
}
items := make([]mqttMeasurementHistoryItem, 0, len(records))
for _, record := range records {
items = append(items, mqttMeasurementHistoryItem{
ID: record.ID,
DataType: "heart_rate",
Identifier: record.Identifier,
Topic: record.Topic,
RegionID: record.RegionID,
Addr: record.BeltAddr,
Value: int64(record.HeartRate),
ValueLabel: "心率",
PacketNum: record.PacketNum,
GatewayMAC: record.GatewayMAC,
Battery: record.Battery,
IsActive: record.IsActive,
IsOnSkin: record.IsOnSkin,
SignalRSSINeg: record.SignalRSSINeg,
SNR: record.SNR,
ReceivedAt: record.ReceivedAt,
CreatedAtMilli: record.CreatedAt.UnixMilli(),
})
}
return items, 0, false, nil
}
bucketExpr := buildChartBucketExpr(bucketSizeMs, "received_at")
type rawChartItem struct {
RegionID uint32
Addr string
Value float64
PacketNum int64
SignalRSSINeg float64
SNR float64
ReceivedAt int64
CreatedAtMilli int64
}
var rows []rawChartItem
if err := baseQuery.Session(&gorm.Session{}).
Select(
"region_id, belt_addr as addr, ROUND(AVG(heart_rate)) as value, COUNT(*) as packet_num, AVG(signal_rssi_neg) as signal_rssi_neg, AVG(snr) as snr, " + bucketExpr + " as received_at, MIN(EXTRACT(EPOCH FROM created_at) * 1000)::bigint as created_at_milli",
).
Group("region_id, belt_addr, " + bucketExpr).
Order("received_at ASC, addr ASC").
Scan(&rows).Error; err != nil {
return nil, bucketSizeMs, true, err
}
items := make([]mqttMeasurementHistoryItem, 0, len(rows))
for _, row := range rows {
items = append(items, mqttMeasurementHistoryItem{
DataType: "heart_rate",
RegionID: row.RegionID,
Addr: row.Addr,
Value: int64(math.Round(row.Value)),
ValueLabel: "心率",
PacketNum: uint32(maxInt64(row.PacketNum, 0)),
SignalRSSINeg: row.SignalRSSINeg,
SNR: row.SNR,
ReceivedAt: row.ReceivedAt,
CreatedAtMilli: row.CreatedAtMilli,
})
}
return items, bucketSizeMs, true, nil
}
func (sc *StatisticsController) buildStepCountChartItems(baseQuery *gorm.DB, params mqttMeasurementHistoryParams, summary mqttMeasurementHistorySummary) ([]mqttMeasurementHistoryItem, int64, bool, error) {
bucketSizeMs := estimateChartBucketSizeMs(params, summary)
if bucketSizeMs == 0 {
var records []models.MqttStepCountRecord
if err := baseQuery.Session(&gorm.Session{}).
Order("received_at ASC").
Find(&records).Error; err != nil {
return nil, 0, false, err
}
items := make([]mqttMeasurementHistoryItem, 0, len(records))
for _, record := range records {
items = append(items, mqttMeasurementHistoryItem{
ID: record.ID,
DataType: "step_count",
Identifier: record.Identifier,
Topic: record.Topic,
RegionID: record.RegionID,
Addr: record.BeltAddr,
Value: int64(record.StepCount),
ValueLabel: "步数",
PacketNum: record.PacketNum,
GatewayMAC: record.GatewayMAC,
Battery: 0,
IsActive: false,
IsOnSkin: false,
SignalRSSINeg: record.SignalRSSINeg,
SNR: record.SNR,
ReceivedAt: record.ReceivedAt,
CreatedAtMilli: record.CreatedAt.UnixMilli(),
})
}
return items, 0, false, nil
}
bucketExpr := buildChartBucketExpr(bucketSizeMs, "received_at")
type rawChartItem struct {
RegionID uint32
Addr string
Value float64
PacketNum int64
SignalRSSINeg float64
SNR float64
ReceivedAt int64
CreatedAtMilli int64
}
var rows []rawChartItem
if err := baseQuery.Session(&gorm.Session{}).
Select(
"region_id, belt_addr as addr, ROUND(AVG(step_count)) as value, COUNT(*) as packet_num, AVG(signal_rssi_neg) as signal_rssi_neg, AVG(snr) as snr, " + bucketExpr + " as received_at, MIN(EXTRACT(EPOCH FROM created_at) * 1000)::bigint as created_at_milli",
).
Group("region_id, belt_addr, " + bucketExpr).
Order("received_at ASC, addr ASC").
Scan(&rows).Error; err != nil {
return nil, bucketSizeMs, true, err
}
items := make([]mqttMeasurementHistoryItem, 0, len(rows))
for _, row := range rows {
items = append(items, mqttMeasurementHistoryItem{
DataType: "step_count",
RegionID: row.RegionID,
Addr: row.Addr,
Value: int64(math.Round(row.Value)),
ValueLabel: "步数",
PacketNum: uint32(maxInt64(row.PacketNum, 0)),
SignalRSSINeg: row.SignalRSSINeg,
SNR: row.SNR,
ReceivedAt: row.ReceivedAt,
CreatedAtMilli: row.CreatedAtMilli,
})
}
return items, bucketSizeMs, true, nil
}
func maxInt64(value, floor int64) int64 {
if value < floor {
return floor
}
return value
}
// --- 删除接口 --- // --- 删除接口 ---
// @Summary 删除AI分析记录 // @Summary 删除AI分析记录