把工程根目录中和"配置加载"、"数据存储"两个领域相关的全部 Go 文件迁到 internal/ 下的子包,按功能分组的第一阶段成果。 internal/config/ - 从 config.go 抽出 Config / MQTTConfig / WebConfig / DatabaseConfig 等 类型并大写导出,函数改名 Default/Load/Write/Validate/BuildTLS 等。 - 测试随被测代码迁移成 package config 的内部测试。 - 根目录留 config.go 作桥接:用 type alias 让旧的小写名(config / mqttConfig / databaseConfig 等)继续可用,避免修改 30+ 处调用方。 internal/store/ - 把 db.go、store_query.go、db_write_queue.go 与 13 个 *_store.go 一并 迁入;26 个 *Record 类型与 store 同包以避免循环依赖。 - store -> Store;50+ 标识符从小写未导出改为大写导出(包括 record、 ListOptions、错误变量、bot/llm/runtime 常量、helpers 等)。 - 新增 DB() / Driver() 访问器供 ai 子系统使用,避免直接访问私有字段。 - bot_pki_store.go 独立出来,把 PKI 解密所需的 store 方法集中归类。 - helpers.go 提供 hashPassword / uint32FromRecord / printJSON 等以前在 其他根目录文件中的辅助;test_helpers_test.go 提供 verifyPassword 与 publicMapTileSourceDTO 让测试可以本地运行而不依赖 main 包。 根目录新增: - store_bridge.go:完整 type-alias / 函数包装层,把 internal/store 的 导出名映射回旧的小写名,让 admin_*_routes.go、web.go、bot_service.go 等仍未迁出的文件继续编译。后续步骤把它们迁到各自领域包后可逐步删除。 - test_helpers_test.go:根目录测试沿用 openTestStore 的入口。 go build ./... 与 go test ./... 全部通过;测试数量与重构前一致。 Co-Authored-By: Claude <noreply@anthropic.com>
343 lines
8.9 KiB
Go
343 lines
8.9 KiB
Go
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
|
|
})
|
|
}
|
|
|
|
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)
|
|
}
|