Files
meshtastic_mqtt_server/internal/store/store_query.go
T
2026-06-19 20:03:25 +08:00

390 lines
10 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 store
import (
"fmt"
"math"
"time"
"gorm.io/gorm"
)
type ListOptions struct {
Limit int
Offset int
NodeID string
ChannelID string
Since *time.Time
Until *time.Time
MinLat *float64
MaxLat *float64
MinLng *float64
MaxLng *float64
}
type MapReportViewportOptions struct {
ListOptions ListOptions
Zoom int
Limit int
ClusterThreshold int
TargetCells int
}
type MapReportViewportResult struct {
Mode string
Total int64
Points []MapReportRecord
Clusters []MapReportClusterRecord
Limit int
Zoom int
}
type MapReportClusterRecord struct {
ClusterID string
Latitude float64
Longitude float64
Count int64
}
func (s *Store) Ping() error {
db, err := s.db.DB()
if err != nil {
return err
}
return db.Ping()
}
func NormalizeListOptions(opts ListOptions) ListOptions {
if opts.Limit <= 0 {
opts.Limit = 100
}
if opts.Limit > 500 {
opts.Limit = 500
}
if opts.Offset < 0 {
opts.Offset = 0
}
return opts
}
func (s *Store) ListNodeInfo(opts ListOptions) ([]NodeInfoRecord, error) {
opts = NormalizeListOptions(opts)
var rows []NodeInfoRecord
q := applyNodeFilters(s.db.Model(&NodeInfoRecord{}), opts).
Order("updated_at DESC").
Limit(opts.Limit).
Offset(opts.Offset)
return rows, q.Find(&rows).Error
}
func (s *Store) CountNodeInfo(opts ListOptions) (int64, error) {
var total int64
q := applyNodeFilters(s.db.Model(&NodeInfoRecord{}), opts)
return total, q.Count(&total).Error
}
func (s *Store) GetNodeInfo(nodeID string) (*NodeInfoRecord, error) {
var row NodeInfoRecord
if err := s.db.Where("node_id = ?", nodeID).Take(&row).Error; err != nil {
return nil, err
}
return &row, nil
}
func (s *Store) ListMapReports(opts ListOptions) ([]MapReportRecord, error) {
opts = NormalizeListOptions(opts)
var rows []MapReportRecord
q := applyMapReportFilters(s.db.Model(&MapReportRecord{}), opts).
Order("updated_at DESC").
Limit(opts.Limit).
Offset(opts.Offset)
return rows, q.Find(&rows).Error
}
func (s *Store) CountMapReports(opts ListOptions) (int64, error) {
var total int64
q := applyMapReportFilters(s.db.Model(&MapReportRecord{}), opts)
return total, q.Count(&total).Error
}
func (s *Store) GetMapReport(nodeID string) (*MapReportRecord, error) {
var row MapReportRecord
if err := s.db.Where("node_id = ?", nodeID).Take(&row).Error; err != nil {
return nil, err
}
return &row, nil
}
func (s *Store) ListMapReportViewport(opts MapReportViewportOptions) (*MapReportViewportResult, error) {
opts = NormalizeMapReportViewportOptions(opts)
total, err := s.CountMapReports(opts.ListOptions)
if err != nil {
return nil, err
}
result := &MapReportViewportResult{Total: total, Limit: opts.Limit, Zoom: opts.Zoom}
if total <= int64(opts.ClusterThreshold) {
var points []MapReportRecord
q := applyMapReportFilters(s.db.Model(&MapReportRecord{}), opts.ListOptions).
Order("updated_at DESC").
Limit(opts.Limit)
if err := q.Find(&points).Error; err != nil {
return nil, err
}
result.Mode = "points"
result.Points = points
return result, nil
}
clusters, err := s.ListMapReportClusters(opts)
if err != nil {
return nil, err
}
result.Mode = "clusters"
result.Clusters = clusters
return result, nil
}
func (s *Store) ListMapReportClusters(opts MapReportViewportOptions) ([]MapReportClusterRecord, error) {
opts = NormalizeMapReportViewportOptions(opts)
cellSize := mapReportClusterCellSize(opts.ListOptions, opts.TargetCells)
var rows []struct {
LatBucket int64
LngBucket int64
Latitude float64
Longitude float64
Count int64
}
q := applyMapReportFilters(s.db.Model(&MapReportRecord{}), opts.ListOptions).
Select("CAST((latitude + 90.0) / ? AS INTEGER) AS lat_bucket, CAST((longitude + 180.0) / ? AS INTEGER) AS lng_bucket, AVG(latitude) AS latitude, AVG(longitude) AS longitude, COUNT(*) AS count", cellSize, cellSize).
Group("lat_bucket, lng_bucket").
Order("count DESC").
Limit(opts.Limit)
if err := q.Scan(&rows).Error; err != nil {
return nil, err
}
clusters := make([]MapReportClusterRecord, 0, len(rows))
for _, row := range rows {
clusters = append(clusters, MapReportClusterRecord{
ClusterID: fmt.Sprintf("%d:%d", row.LatBucket, row.LngBucket),
Latitude: row.Latitude,
Longitude: row.Longitude,
Count: row.Count,
})
}
return clusters, nil
}
func NormalizeMapReportViewportOptions(opts MapReportViewportOptions) MapReportViewportOptions {
if opts.Limit <= 0 {
opts.Limit = 1000
}
if opts.Limit > 2000 {
opts.Limit = 2000
}
if opts.ClusterThreshold <= 0 {
opts.ClusterThreshold = 500
}
if opts.ClusterThreshold > 5000 {
opts.ClusterThreshold = 5000
}
if opts.TargetCells <= 0 {
opts.TargetCells = 64
}
if opts.TargetCells > 256 {
opts.TargetCells = 256
}
return opts
}
func mapReportClusterCellSize(opts ListOptions, targetCells int) float64 {
latSpan := 180.0
if opts.MinLat != nil && opts.MaxLat != nil {
latSpan = *opts.MaxLat - *opts.MinLat
}
lngSpan := 360.0
if opts.MinLng != nil && opts.MaxLng != nil {
if *opts.MinLng <= *opts.MaxLng {
lngSpan = *opts.MaxLng - *opts.MinLng
} else {
lngSpan = 180 - *opts.MinLng + *opts.MaxLng + 180
}
}
span := math.Max(latSpan, lngSpan)
cellSize := span / float64(targetCells)
if cellSize < 0.0001 {
cellSize = 0.0001
}
return cellSize
}
func (s *Store) DeleteNode(nodeID string) error {
return s.db.Transaction(func(tx *gorm.DB) error {
nodeResult := tx.Where("node_id = ?", nodeID).Delete(&NodeInfoRecord{})
if nodeResult.Error != nil {
return nodeResult.Error
}
reportResult := tx.Where("node_id = ?", nodeID).Delete(&MapReportRecord{})
if reportResult.Error != nil {
return reportResult.Error
}
if nodeResult.RowsAffected+reportResult.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
})
}
// PurgeNode 在「删除节点」菜单触发时执行:除了 nodeinfo + map_report
// 还要把 text_message(频道聊天)以及 position/telemetry/routing/traceroute
// 这些以 from_id 关联的数据包记录一起清理。
//
// 任一表删到记录就视为成功;全部为空才返回 ErrRecordNotFound。
func (s *Store) PurgeNode(nodeID string) error {
if nodeID == "" {
return gorm.ErrRecordNotFound
}
return s.db.Transaction(func(tx *gorm.DB) error {
var totalAffected int64
nodeResult := tx.Where("node_id = ?", nodeID).Delete(&NodeInfoRecord{})
if nodeResult.Error != nil {
return nodeResult.Error
}
totalAffected += nodeResult.RowsAffected
reportResult := tx.Where("node_id = ?", nodeID).Delete(&MapReportRecord{})
if reportResult.Error != nil {
return reportResult.Error
}
totalAffected += reportResult.RowsAffected
// 以 from_id 关联:聊天消息 + 数据包流水
fromIDTargets := []any{
&TextMessageRecord{},
&PositionRecord{},
&TelemetryRecord{},
&RoutingRecord{},
&TracerouteRecord{},
}
for _, model := range fromIDTargets {
res := tx.Where("from_id = ?", nodeID).Delete(model)
if res.Error != nil {
return res.Error
}
totalAffected += res.RowsAffected
}
if totalAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
})
}
func applyNodeFilters(q *gorm.DB, opts ListOptions) *gorm.DB {
if opts.NodeID != "" {
q = q.Where("node_id = ?", opts.NodeID)
}
if opts.Since != nil {
q = q.Where("updated_at >= ?", *opts.Since)
}
if opts.Until != nil {
q = q.Where("updated_at <= ?", *opts.Until)
}
return q
}
func applyMapReportFilters(q *gorm.DB, opts ListOptions) *gorm.DB {
q = applyNodeFilters(q, opts)
if opts.MinLat != nil && opts.MaxLat != nil {
q = q.Where("latitude IS NOT NULL AND latitude >= ? AND latitude <= ?", *opts.MinLat, *opts.MaxLat)
}
if opts.MinLng != nil && opts.MaxLng != nil {
if *opts.MinLng <= *opts.MaxLng {
q = q.Where("longitude IS NOT NULL AND longitude >= ? AND longitude <= ?", *opts.MinLng, *opts.MaxLng)
} else {
q = q.Where("longitude IS NOT NULL AND (longitude >= ? OR longitude <= ?)", *opts.MinLng, *opts.MaxLng)
}
}
return q
}
func (s *Store) ListTextMessages(opts ListOptions) ([]TextMessageRecord, error) {
var rows []TextMessageRecord
return rows, s.listAppendRows(opts, &rows).Error
}
func (s *Store) ListDiscardDetails(opts ListOptions) ([]DiscardDetailsRecord, error) {
opts = NormalizeListOptions(opts)
var rows []DiscardDetailsRecord
q := applyDiscardDetailsFilters(s.db.Model(&DiscardDetailsRecord{}), opts).
Order("created_at DESC").
Order("id DESC").
Limit(opts.Limit).
Offset(opts.Offset)
return rows, q.Find(&rows).Error
}
func (s *Store) CountDiscardDetails(opts ListOptions) (int64, error) {
var total int64
q := applyDiscardDetailsFilters(s.db.Model(&DiscardDetailsRecord{}), opts)
return total, q.Count(&total).Error
}
func applyDiscardDetailsFilters(q *gorm.DB, opts ListOptions) *gorm.DB {
if opts.Since != nil {
q = q.Where("created_at >= ?", *opts.Since)
}
if opts.Until != nil {
q = q.Where("created_at <= ?", *opts.Until)
}
return q
}
func (s *Store) DeleteTextMessage(id uint64) error {
result := s.db.Where("id = ?", id).Delete(&TextMessageRecord{})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
func (s *Store) ListPositions(opts ListOptions) ([]PositionRecord, error) {
var rows []PositionRecord
return rows, s.listAppendRows(opts, &rows).Error
}
func (s *Store) ListTelemetry(opts ListOptions) ([]TelemetryRecord, error) {
var rows []TelemetryRecord
return rows, s.listAppendRows(opts, &rows).Error
}
func (s *Store) ListRouting(opts ListOptions) ([]RoutingRecord, error) {
var rows []RoutingRecord
return rows, s.listAppendRows(opts, &rows).Error
}
func (s *Store) ListTraceroute(opts ListOptions) ([]TracerouteRecord, error) {
var rows []TracerouteRecord
return rows, s.listAppendRows(opts, &rows).Error
}
func (s *Store) listAppendRows(opts ListOptions, dest any) *gorm.DB {
opts = NormalizeListOptions(opts)
q := s.db.Order("created_at DESC").Order("id DESC").Limit(opts.Limit).Offset(opts.Offset)
if opts.NodeID != "" {
q = q.Where("from_id = ?", opts.NodeID)
}
if opts.ChannelID != "" {
q = q.Where("channel_id = ?", opts.ChannelID)
}
if opts.Since != nil {
q = q.Where("created_at >= ?", *opts.Since)
}
if opts.Until != nil {
q = q.Where("created_at <= ?", *opts.Until)
}
return q.Find(dest)
}