重构:拆出 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
+233
View File
@@ -0,0 +1,233 @@
package blocking
import (
"errors"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
storepkg "meshtastic_mqtt_server/internal/store"
"meshtastic_mqtt_server/internal/webutil"
)
type nodeBlockingRequest struct {
NodeID string `json:"node_id"`
NodeNum *int64 `json:"node_num"`
Reason string `json:"reason"`
Enabled bool `json:"enabled"`
}
type ipBlockingRequest struct {
IPValue string `json:"ip_value"`
Reason string `json:"reason"`
Enabled bool `json:"enabled"`
}
type forbiddenWordBlockingRequest struct {
Word string `json:"word"`
MatchType string `json:"match_type"`
CaseSensitive bool `json:"case_sensitive"`
Reason string `json:"reason"`
Enabled bool `json:"enabled"`
}
func RegisterRoutes(r gin.IRouter, store *storepkg.Store, blocking *Cache) {
reloadBlocking := func() error {
if blocking == nil {
return nil
}
return blocking.Reload(store)
}
r.GET("/blocking/nodes", func(c *gin.Context) {
opts, ok := webutil.ParseListOptions(c)
if !ok {
return
}
rows, err := store.ListNodeBlocking(opts)
if err != nil {
webutil.WriteListResponse(c, rows, opts, err, nodeBlockingDTO)
return
}
total, err := store.CountNodeBlocking(opts)
webutil.WriteListResponseWithTotal(c, rows, opts, total, err, nodeBlockingDTO)
})
r.POST("/blocking/nodes", func(c *gin.Context) {
var req nodeBlockingRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid node blocking request"})
return
}
row, err := store.CreateNodeBlocking(req.NodeID, req.NodeNum, req.Reason, req.Enabled)
writeBlockingMutationResponse(c, http.StatusCreated, row, err, nodeBlockingDTO, reloadBlocking)
})
r.PUT("/blocking/nodes/:id", func(c *gin.Context) {
id, ok := parseBlockingID(c)
if !ok {
return
}
var req nodeBlockingRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid node blocking request"})
return
}
row, err := store.UpdateNodeBlocking(id, req.NodeID, req.NodeNum, req.Reason, req.Enabled)
writeBlockingMutationResponse(c, http.StatusOK, row, err, nodeBlockingDTO, reloadBlocking)
})
r.DELETE("/blocking/nodes/:id", func(c *gin.Context) {
id, ok := parseBlockingID(c)
if !ok {
return
}
writeBlockingDeleteResponse(c, store.DeleteNodeBlocking(id), reloadBlocking)
})
r.GET("/blocking/ips", func(c *gin.Context) {
opts, ok := webutil.ParseListOptions(c)
if !ok {
return
}
rows, err := store.ListIPBlocking(opts)
if err != nil {
webutil.WriteListResponse(c, rows, opts, err, ipBlockingDTO)
return
}
total, err := store.CountIPBlocking(opts)
webutil.WriteListResponseWithTotal(c, rows, opts, total, err, ipBlockingDTO)
})
r.POST("/blocking/ips", func(c *gin.Context) {
var req ipBlockingRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ip blocking request"})
return
}
row, err := store.CreateIPBlocking(req.IPValue, req.Reason, req.Enabled)
writeBlockingMutationResponse(c, http.StatusCreated, row, err, ipBlockingDTO, reloadBlocking)
})
r.PUT("/blocking/ips/:id", func(c *gin.Context) {
id, ok := parseBlockingID(c)
if !ok {
return
}
var req ipBlockingRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ip blocking request"})
return
}
row, err := store.UpdateIPBlocking(id, req.IPValue, req.Reason, req.Enabled)
writeBlockingMutationResponse(c, http.StatusOK, row, err, ipBlockingDTO, reloadBlocking)
})
r.DELETE("/blocking/ips/:id", func(c *gin.Context) {
id, ok := parseBlockingID(c)
if !ok {
return
}
writeBlockingDeleteResponse(c, store.DeleteIPBlocking(id), reloadBlocking)
})
r.GET("/blocking/words", func(c *gin.Context) {
opts, ok := webutil.ParseListOptions(c)
if !ok {
return
}
rows, err := store.ListForbiddenWordBlocking(opts)
if err != nil {
webutil.WriteListResponse(c, rows, opts, err, forbiddenWordBlockingDTO)
return
}
total, err := store.CountForbiddenWordBlocking(opts)
webutil.WriteListResponseWithTotal(c, rows, opts, total, err, forbiddenWordBlockingDTO)
})
r.POST("/blocking/words", func(c *gin.Context) {
var req forbiddenWordBlockingRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid forbidden word blocking request"})
return
}
row, err := store.CreateForbiddenWordBlocking(req.Word, req.MatchType, req.CaseSensitive, req.Reason, req.Enabled)
writeBlockingMutationResponse(c, http.StatusCreated, row, err, forbiddenWordBlockingDTO, reloadBlocking)
})
r.PUT("/blocking/words/:id", func(c *gin.Context) {
id, ok := parseBlockingID(c)
if !ok {
return
}
var req forbiddenWordBlockingRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid forbidden word blocking request"})
return
}
row, err := store.UpdateForbiddenWordBlocking(id, req.Word, req.MatchType, req.CaseSensitive, req.Reason, req.Enabled)
writeBlockingMutationResponse(c, http.StatusOK, row, err, forbiddenWordBlockingDTO, reloadBlocking)
})
r.DELETE("/blocking/words/:id", func(c *gin.Context) {
id, ok := parseBlockingID(c)
if !ok {
return
}
writeBlockingDeleteResponse(c, store.DeleteForbiddenWordBlocking(id), reloadBlocking)
})
}
func parseBlockingID(c *gin.Context) (uint64, bool) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil || id == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid blocking rule id"})
return 0, false
}
return id, true
}
func writeBlockingMutationResponse[T any](c *gin.Context, status int, row *T, err error, convert func(T) gin.H, afterSuccess func() error) {
if errors.Is(err, storepkg.ErrBlockingAlreadyExists) {
c.JSON(http.StatusConflict, gin.H{"error": "blocking rule already exists"})
return
}
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "blocking rule not found"})
return
}
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if afterSuccess != nil {
if err := afterSuccess(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "blocking rule saved but cache reload failed: " + err.Error()})
return
}
}
c.JSON(status, gin.H{"item": convert(*row)})
}
func writeBlockingDeleteResponse(c *gin.Context, err error, afterSuccess func() error) {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "blocking rule not found"})
return
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if afterSuccess != nil {
if err := afterSuccess(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "blocking rule deleted but cache reload failed: " + err.Error()})
return
}
}
c.JSON(http.StatusOK, gin.H{"status": "ok"})
}
func nodeBlockingDTO(row storepkg.NodeBlockingRecord) gin.H {
return gin.H{"id": row.ID, "node_id": row.NodeID, "node_num": webutil.PtrInt64(row.NodeNum), "reason": row.Reason, "enabled": row.Enabled, "created_at": row.CreatedAt, "updated_at": row.UpdatedAt}
}
func ipBlockingDTO(row storepkg.IPBlockingRecord) gin.H {
return gin.H{"id": row.ID, "ip_value": row.IPValue, "reason": row.Reason, "enabled": row.Enabled, "created_at": row.CreatedAt, "updated_at": row.UpdatedAt}
}
func forbiddenWordBlockingDTO(row storepkg.ForbiddenWordBlockingRecord) gin.H {
return gin.H{"id": row.ID, "word": row.Word, "match_type": row.MatchType, "case_sensitive": row.CaseSensitive, "reason": row.Reason, "enabled": row.Enabled, "created_at": row.CreatedAt, "updated_at": row.UpdatedAt}
}