diff --git a/internal/store/db.go b/internal/store/db.go index afaf074..f7d87c4 100644 --- a/internal/store/db.go +++ b/internal/store/db.go @@ -474,6 +474,16 @@ func (SchemaMigration) TableName() string { return "schema_migrations" } +type ChannelRecord struct { + ID uint `gorm:"column:id;primaryKey;autoIncrement"` + ChannelID string `gorm:"column:channel_id;type:varchar(255);not null;uniqueIndex"` + CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"` +} + +func (ChannelRecord) TableName() string { + return "channels" +} + // LLMProviderRecord 保存 LLM API 配置,支持多个 AI 提供商 type LLMProviderRecord struct { Name string `gorm:"column:name;primaryKey;size:64;not null"` // 配置名称,如 "default"、"openai"、"ark" 等 @@ -700,6 +710,7 @@ func (s *Store) migrate() error { {label: "nodeinfo", model: &NodeInfoRecord{}}, {label: "map_report", model: &MapReportRecord{}}, {label: "text_message", model: &TextMessageRecord{}}, + {label: "channels", model: &ChannelRecord{}}, {label: "position", model: &PositionRecord{}}, {label: "telemetry", model: &TelemetryRecord{}}, {label: "routing", model: &RoutingRecord{}}, @@ -897,6 +908,7 @@ type DBMigration struct { var dbMigrations = []DBMigration{ {Version: 1, Up: migrateDB1}, + {Version: 2, Up: migrateDB2}, } func migrateDB1(tx *gorm.DB, driver string) error { @@ -908,6 +920,19 @@ func migrateDB1(tx *gorm.DB, driver string) error { return nil } +func migrateDB2(tx *gorm.DB, driver string) error { + var sql string + if driver == config.DriverSQLite { + sql = "INSERT OR IGNORE INTO channels (channel_id, created_at) SELECT DISTINCT channel_id, datetime('now') FROM text_message WHERE channel_id IS NOT NULL AND channel_id != ''" + } else { + sql = "INSERT IGNORE INTO channels (channel_id, created_at) SELECT DISTINCT channel_id, NOW() FROM text_message WHERE channel_id IS NOT NULL AND channel_id != ''" + } + if err := tx.Exec(sql).Error; err != nil { + return fmt.Errorf("backfill channels: %w", err) + } + return nil +} + func runDBMigrations(tx *gorm.DB, driver string) error { if !tx.Migrator().HasTable(&SchemaMigration{}) { if err := tx.Migrator().CreateTable(&SchemaMigration{}); err != nil { @@ -1080,6 +1105,9 @@ func (s *Store) InsertTextMessage(record map[string]any, clientInfo MQTTClientIn if err := s.db.Create(message).Error; err != nil { return fmt.Errorf("insert text_message from %s: %w", message.FromID, err) } + if message.ChannelID != nil && *message.ChannelID != "" { + s.db.FirstOrCreate(&ChannelRecord{}, ChannelRecord{ChannelID: *message.ChannelID}) + } return nil } diff --git a/internal/store/store_query.go b/internal/store/store_query.go index 70412cd..7de7487 100644 --- a/internal/store/store_query.go +++ b/internal/store/store_query.go @@ -387,3 +387,8 @@ func (s *Store) listAppendRows(opts ListOptions, dest any) *gorm.DB { } return q.Find(dest) } + +func (s *Store) ListChannels() ([]ChannelRecord, error) { + var rows []ChannelRecord + return rows, s.db.Order("channel_id ASC").Find(&rows).Error +} diff --git a/internal/web/web.go b/internal/web/web.go index 37eb908..279a0be 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -139,6 +139,18 @@ func registerAPIRoutes(r gin.IRouter, store *storepkg.Store, mapTileCacheDir str rows, err := store.ListTextMessages(opts) writeListResponse(c, rows, opts, err, textMessageDTO) }) + r.GET("/channels", func(c *gin.Context) { + rows, err := store.ListChannels() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + items := make([]gin.H, 0, len(rows)) + for _, row := range rows { + items = append(items, gin.H{"channel_id": row.ChannelID}) + } + c.JSON(http.StatusOK, gin.H{"items": items}) + }) r.GET("/discard-details", func(c *gin.Context) { opts, ok := parseListOptions(c) if !ok { diff --git a/meshmap_frontend/src/App.vue b/meshmap_frontend/src/App.vue index 0ef26f8..9588441 100644 --- a/meshmap_frontend/src/App.vue +++ b/meshmap_frontend/src/App.vue @@ -1,6 +1,6 @@