Files
kevinandClaude c527a9fd9a 重构:拆出 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>
2026-06-18 14:23:56 +08:00

107 lines
2.9 KiB
Go

package help
import (
"errors"
"net/http"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"meshtastic_mqtt_server/internal/auth"
storepkg "meshtastic_mqtt_server/internal/store"
)
type helpContentRequest struct {
Markdown string `json:"markdown"`
}
// RegisterPublicRoutes 把对外可见的 GET /help 挂到给定路由组下。
func RegisterPublicRoutes(r gin.IRouter, store *storepkg.Store) {
r.GET("/help", func(c *gin.Context) {
item, err := latestHelpContentDTO(store)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"item": item})
})
}
// RegisterAdminRoutes 注册管理员侧 /help、/help、/help/preview 这三条路由。
func RegisterAdminRoutes(r gin.IRouter, store *storepkg.Store) {
r.GET("/help", func(c *gin.Context) {
item, err := latestHelpContentDTO(store)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"item": item})
})
r.POST("/help", func(c *gin.Context) {
var req helpContentRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid help content request"})
return
}
claims := c.MustGet(auth.AdminClaimsKey).(*auth.SessionClaims)
row, err := store.InsertHelpContent(req.Markdown, claims.Username)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
item, err := helpContentDTO(row.ID, row.Markdown, row.CreatedBy, &row.CreatedAt)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"item": item})
})
r.POST("/help/preview", func(c *gin.Context) {
var req helpContentRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid help preview request"})
return
}
html, err := RenderMarkdown(req.Markdown)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"html": html})
})
}
func latestHelpContentDTO(store *storepkg.Store) (gin.H, error) {
row, err := store.GetLatestHelpContent()
if errors.Is(err, gorm.ErrRecordNotFound) {
return helpContentDTO(0, storepkg.DefaultHelpMarkdown, "", nil)
}
if err != nil {
return nil, err
}
return helpContentDTO(row.ID, row.Markdown, row.CreatedBy, &row.CreatedAt)
}
func helpContentDTO(id uint64, markdown, createdBy string, createdAt *time.Time) (gin.H, error) {
html, err := RenderMarkdown(markdown)
if err != nil {
return nil, err
}
return gin.H{"id": ptrHelpID(id), "markdown": markdown, "html": html, "created_by": createdBy, "created_at": ptrTime(createdAt)}, nil
}
func ptrHelpID(id uint64) any {
if id == 0 {
return nil
}
return id
}
func ptrTime(value *time.Time) any {
if value == nil {
return nil
}
return *value
}