重构:拆出 auth / blocking / runtimesettings / help / mqttforward 包

第二批:把根目录中纯逻辑领域文件(cache、service、admin route)按业务边界
迁到 internal/ 下的子包。各子包暴露 RegisterRoutes 给 web 包调用,根目录
只留下一行 bridge 文件保留旧名字别名。

新增包
- internal/auth/        SessionClaims / Manager / RequireAdmin / HashPassword /
                        VerifyPassword / AdminUserResponse 等。原 auth.go 中
                        被两个 admin route 依赖的 sessionClaims 现在以 auth.
                        SessionClaims 形式被它们 import;不再被锁在 main 包。
- internal/blocking/    Cache + RegisterRoutes,以前散在 blocking_cache.go
                        和 admin_blocking_routes.go 里。
- internal/runtimesettings/  Cache + RegisterRoutes。
- internal/help/        RenderMarkdown / RegisterPublicRoutes /
                        RegisterAdminRoutes(拆分原来的 registerHelpRoutes
                        和 registerAdminHelpRoutes 两条入口)。
- internal/mqttforward/ Manager / Reloader / Stats / RegisterRoutes。
                        forwarder runner、循环抑制 cache 等运行时逻辑随之迁入。
- internal/webutil/     ParseListOptions / WriteListResponse[WithTotal] /
                        ParseMapReportListOptions / ParseMapReportViewportOptions
                        以及 PtrString/PtrInt64/... 等指针解引用 helper。
                        以前散在 web.go 中,现在被各 admin route 子包共享,
                        避免 internal/blocking → internal/web → internal/blocking
                        的循环依赖。
- internal/store/testutil/  OpenStore(t) helper,让其它包测试零样板拿到 store。

根目录新增 bridge 文件
- blocking_bridge.go / runtime_settings_bridge.go / help_bridge.go /
  mqttforward_bridge.go:用 type alias + thin wrapper 把上述子包的导出
  名映射到旧的小写名(blockingCache、registerAdminBlockingRoutes 等),
  让 main.go / web.go 等仍未迁出的文件无须改动。

修改
- auth.go 改为对 internal/auth 的 bridge;web.go 中 sessions.newCookie /
  clearCookie 改为 NewCookie / ClearCookie。
- main_test.go 中 BlockingViolationForRecord* 测试不再直接构造未导出字段,
  改成走 store.CreateNodeBlocking → newBlockingCache 的真实路径。
- internal/mqttforward 把以前 *_store.go 中没有方法依赖的运行时类型
  (forwarder runner、loop cache)和 admin route 一并归位;mqtt_status.go
  暂时仍留在根目录(依赖 main 中的 mqttClientInfoFromClient)。

go build ./... / go test ./... 全部通过;测试数量未变。

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-18 14:23:56 +08:00
co-authored by Claude
parent eff4972668
commit c527a9fd9a
23 changed files with 786 additions and 313 deletions
@@ -0,0 +1,71 @@
package runtimesettings
import (
"net/http"
"github.com/gin-gonic/gin"
storepkg "meshtastic_mqtt_server/internal/store"
)
const allowEncryptedForwardingLabel = "Allow encrypted MQTT packets to be forwarded when they cannot be decrypted"
const llmQueueEnabledLabel = "Enable LLM message queue"
const llmIncludeChannelLabel = "Include channel messages in LLM queue"
type runtimeSettingsRequest struct {
AllowEncryptedForwarding bool `json:"allow_encrypted_forwarding"`
LLMQueueEnabled bool `json:"llm_queue_enabled"`
LLMIncludeChannelMessages bool `json:"llm_include_channel_messages"`
}
// RegisterRoutes 把 GET /runtime-settings 与 PUT /runtime-settings 挂到给定路由组下。
func RegisterRoutes(r gin.IRouter, store *storepkg.Store, settings *Cache) {
r.GET("/runtime-settings", func(c *gin.Context) {
snapshot, err := store.GetRuntimeSettings()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"item": runtimeSettingsDTO(snapshot)})
})
r.PUT("/runtime-settings", func(c *gin.Context) {
var req runtimeSettingsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid runtime settings request"})
return
}
if _, err := store.SetBoolRuntimeSetting(storepkg.RuntimeSettingAllowEncryptedForwarding, req.AllowEncryptedForwarding, allowEncryptedForwardingLabel); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if _, err := store.SetBoolRuntimeSetting(storepkg.RuntimeSettingLLMQueueEnabled, req.LLMQueueEnabled, llmQueueEnabledLabel); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if _, err := store.SetBoolRuntimeSetting(storepkg.RuntimeSettingLLMQueueIncludeChannel, req.LLMIncludeChannelMessages, llmIncludeChannelLabel); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if settings != nil {
if err := settings.Reload(store); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
snapshot, err := store.GetRuntimeSettings()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"item": runtimeSettingsDTO(snapshot)})
})
}
func runtimeSettingsDTO(settings storepkg.RuntimeSettingsSnapshot) gin.H {
return gin.H{
"allow_encrypted_forwarding": settings.AllowEncryptedForwarding,
"llm_queue_enabled": settings.LLMQueueEnabled,
"llm_include_channel_messages": settings.LLMIncludeChannel,
}
}
@@ -0,0 +1,55 @@
package runtimesettings
import (
"fmt"
"sync"
storepkg "meshtastic_mqtt_server/internal/store"
)
// Cache 把 runtime_settings 表中常用的开关缓存到内存中,避免每次拦截热路径
// 都查 DB。AdminRoute 修改后通过 Reload 重新加载。
type Cache struct {
mu sync.RWMutex
settings storepkg.RuntimeSettingsSnapshot
}
// New 从 store 中加载初始快照并返回缓存。
func New(s *storepkg.Store) (*Cache, error) {
cache := &Cache{}
if err := cache.Reload(s); err != nil {
return nil, err
}
return cache, nil
}
// Reload 重新读取数据库快照覆盖当前值。
func (c *Cache) Reload(s *storepkg.Store) error {
if s == nil {
return fmt.Errorf("store is required")
}
settings, err := s.GetRuntimeSettings()
if err != nil {
return err
}
c.mu.Lock()
c.settings = settings
c.mu.Unlock()
return nil
}
// Snapshot 返回当前快照的副本(结构体拷贝,调用方可以安全持有)。
func (c *Cache) Snapshot() storepkg.RuntimeSettingsSnapshot {
if c == nil {
return storepkg.RuntimeSettingsSnapshot{}
}
c.mu.RLock()
defer c.mu.RUnlock()
return c.settings
}
// AllowEncryptedForwarding 是 mqtt 转发热路径上常被检查的标志位的快捷读法。
func (c *Cache) AllowEncryptedForwarding() bool {
return c.Snapshot().AllowEncryptedForwarding
}
@@ -0,0 +1,45 @@
package runtimesettings
import (
"testing"
storepkg "meshtastic_mqtt_server/internal/store"
"meshtastic_mqtt_server/internal/store/testutil"
)
func openTestStore(t *testing.T) *storepkg.Store {
return testutil.OpenStore(t)
}
func TestRuntimeSettingsCacheReload(t *testing.T) {
st := openTestStore(t)
defer st.Close()
cache, err := New(st)
if err != nil {
t.Fatalf("New() error = %v", err)
}
if cache.AllowEncryptedForwarding() {
t.Fatalf("AllowEncryptedForwarding() = true, want false")
}
if _, err := st.SetBoolRuntimeSetting(storepkg.RuntimeSettingAllowEncryptedForwarding, true, "test setting"); err != nil {
t.Fatalf("SetBoolRuntimeSetting(true) error = %v", err)
}
if err := cache.Reload(st); err != nil {
t.Fatalf("Reload() after true error = %v", err)
}
if !cache.AllowEncryptedForwarding() {
t.Fatalf("AllowEncryptedForwarding() = false, want true")
}
if _, err := st.SetBoolRuntimeSetting(storepkg.RuntimeSettingAllowEncryptedForwarding, false, "test setting"); err != nil {
t.Fatalf("SetBoolRuntimeSetting(false) error = %v", err)
}
if err := cache.Reload(st); err != nil {
t.Fatalf("Reload() after false error = %v", err)
}
if cache.AllowEncryptedForwarding() {
t.Fatalf("AllowEncryptedForwarding() = true, want false")
}
}