重构:拆出 bot / sign / mapsource / llmadmin / web 包
第三批(最终批):把 root 中剩下所有领域文件按功能搬到 internal/ 下。
完成后根目录从 46 个 .go 文件降到 14 个,其中 11 个是 bridge(type alias
+ thin wrapper),仅供尚未改造的 main.go 引用。
新增包
- internal/bot/ Service / TextSender / NewPKIKeyResolver /
RegisterRoutes,含 PKI 直连发送、节点信息广播、
outbound DM 持久化等业务逻辑。
- internal/sign/ SignDTO / SignDayCountDTO / RegisterAdminRoutes,
把原来分散在 admin_sign_routes.go 与 web.go 中的
sign DTO/路由收拢到一处。
- internal/mapsource/ AdminDTO / PublicDTO / RegisterAdminRoutes /
RegisterPublicRoutes。
- internal/llmadmin/ LLM 消息队列、Provider、ToolRouter、PrimaryConfig 的
admin 路由。
- internal/web/ 路由总入口(NewRouter/NewHTTPServer/ServeUnixSocket)、
各资源的 GET API、admin 用户/登录/MQTT 状态、所有
DTO 函数。把 auth.go 的 sessionClaims 升级为
auth.SessionClaims;mqtt_status.go 重写成
MQTTRuntimeStatus / AdminMQTTStatus 结构体并把
client info 解析在 web 包内自带,不再依赖 main 包。
map_tile_proxy_routes 与测试一起搬入。
修改
- web.go 中 parseListOptions / writeListResponse / ptrString 等本地 helper
改为对 internal/webutil 的 thin wrapper,避免重复实现。
- internal/auth 在 step 4 已创建,本批中 web 包正式开始引用其 Manager /
RequireAdmin / SessionClaims。
根目录新增 bridge:bot_bridge.go / sign_bridge.go / mapsource_bridge.go /
llmadmin_bridge.go / web_bridge.go。后者把 mqttRuntimeStatus 包成
webpkg.MQTTStatusProvider 适配器,使 main.go 中旧字段名保持可用。
go build ./... / go test ./... 全部通过;测试数量未变。
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
// Package mapsource 提供地图瓦片源的 admin 与公开路由。
|
||||
package mapsource
|
||||
|
||||
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 mapTileSourceRequest struct {
|
||||
Name string `json:"name"`
|
||||
URLTemplate string `json:"url_template"`
|
||||
Attribution string `json:"attribution"`
|
||||
MaxZoom int `json:"max_zoom"`
|
||||
Enabled bool `json:"enabled"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
ProxyEnabled bool `json:"proxy_enabled"`
|
||||
}
|
||||
|
||||
// RegisterPublicRoutes 把对外可见的 GET /map-source/{default,enabled} 挂上去。
|
||||
func RegisterPublicRoutes(r gin.IRouter, store *storepkg.Store) {
|
||||
r.GET("/map-source/default", func(c *gin.Context) {
|
||||
row, err := store.GetDefaultMapTileSource()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"item": PublicDTO(*row)})
|
||||
})
|
||||
r.GET("/map-source/enabled", func(c *gin.Context) {
|
||||
rows, err := store.ListEnabledMapTileSources()
|
||||
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, PublicDTO(row))
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterAdminRoutes 注册管理员侧 CRUD 与设默认。
|
||||
func RegisterAdminRoutes(r gin.IRouter, store *storepkg.Store) {
|
||||
r.GET("/map-source", func(c *gin.Context) {
|
||||
opts, ok := webutil.ParseListOptions(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
rows, err := store.ListMapTileSources(opts)
|
||||
if err != nil {
|
||||
webutil.WriteListResponse(c, rows, opts, err, AdminDTO)
|
||||
return
|
||||
}
|
||||
total, err := store.CountMapTileSources(opts)
|
||||
webutil.WriteListResponseWithTotal(c, rows, opts, total, err, AdminDTO)
|
||||
})
|
||||
r.POST("/map-source", func(c *gin.Context) {
|
||||
var req mapTileSourceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid map source request"})
|
||||
return
|
||||
}
|
||||
row, err := store.CreateMapTileSource(mapTileSourceInputFromRequest(req))
|
||||
writeMapTileSourceMutationResponse(c, http.StatusCreated, row, err)
|
||||
})
|
||||
r.PUT("/map-source/:id", func(c *gin.Context) {
|
||||
id, ok := parseMapTileSourceID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req mapTileSourceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid map source request"})
|
||||
return
|
||||
}
|
||||
row, err := store.UpdateMapTileSource(id, mapTileSourceInputFromRequest(req))
|
||||
writeMapTileSourceMutationResponse(c, http.StatusOK, row, err)
|
||||
})
|
||||
r.DELETE("/map-source/:id", func(c *gin.Context) {
|
||||
id, ok := parseMapTileSourceID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
writeMapTileSourceDeleteResponse(c, store.DeleteMapTileSource(id))
|
||||
})
|
||||
r.POST("/map-source/:id/default", func(c *gin.Context) {
|
||||
id, ok := parseMapTileSourceID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
row, err := store.SetDefaultMapTileSource(id)
|
||||
writeMapTileSourceMutationResponse(c, http.StatusOK, row, err)
|
||||
})
|
||||
}
|
||||
|
||||
func mapTileSourceInputFromRequest(req mapTileSourceRequest) storepkg.MapTileSourceInput {
|
||||
return storepkg.MapTileSourceInput{
|
||||
Name: req.Name,
|
||||
URLTemplate: req.URLTemplate,
|
||||
Attribution: req.Attribution,
|
||||
MaxZoom: req.MaxZoom,
|
||||
Enabled: req.Enabled,
|
||||
IsDefault: req.IsDefault,
|
||||
ProxyEnabled: req.ProxyEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
func parseMapTileSourceID(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 map source id"})
|
||||
return 0, false
|
||||
}
|
||||
return id, true
|
||||
}
|
||||
|
||||
func writeMapTileSourceMutationResponse(c *gin.Context, status int, row *storepkg.MapTileSourceRecord, err error) {
|
||||
if errors.Is(err, storepkg.ErrMapTileSourceAlreadyExists) {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "map source already exists"})
|
||||
return
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "map source not found"})
|
||||
return
|
||||
}
|
||||
if errors.Is(err, storepkg.ErrMapTileSourceCannotDeleteDefault) || errors.Is(err, storepkg.ErrMapTileSourceCannotDisableDefault) || errors.Is(err, storepkg.ErrMapTileSourceDefaultMustBeEnabled) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(status, gin.H{"item": AdminDTO(*row)})
|
||||
}
|
||||
|
||||
func writeMapTileSourceDeleteResponse(c *gin.Context, err error) {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "map source not found"})
|
||||
return
|
||||
}
|
||||
if errors.Is(err, storepkg.ErrMapTileSourceCannotDeleteDefault) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
}
|
||||
|
||||
// AdminDTO 是管理后台展示的全字段视图。
|
||||
func AdminDTO(row storepkg.MapTileSourceRecord) gin.H {
|
||||
return gin.H{"id": row.ID, "name": row.Name, "url_template": row.URLTemplate, "attribution": row.Attribution, "max_zoom": row.MaxZoom, "enabled": row.Enabled, "is_default": row.IsDefault, "proxy_enabled": row.ProxyEnabled, "created_at": row.CreatedAt, "updated_at": row.UpdatedAt}
|
||||
}
|
||||
|
||||
// PublicDTO 是给前端用户使用的视图:当 ProxyEnabled 为 true 时,url 改写为
|
||||
// 通过本服务的 /api/map/{hash} 代理路径,避免暴露上游瓦片地址。
|
||||
func PublicDTO(row storepkg.MapTileSourceRecord) gin.H {
|
||||
urlTemplate := row.URLTemplate
|
||||
if row.ProxyEnabled {
|
||||
hash := row.URLTemplateHash
|
||||
if hash == "" {
|
||||
hash = storepkg.MapTileSourceHash(row.URLTemplate)
|
||||
}
|
||||
urlTemplate = "/api/map/" + hash + "?x={x}&y={y}&z={z}"
|
||||
}
|
||||
return gin.H{"id": row.ID, "name": row.Name, "url_template": urlTemplate, "attribution": row.Attribution, "max_zoom": row.MaxZoom}
|
||||
}
|
||||
Reference in New Issue
Block a user