Explorar el Código

feat:1.新增运维管理和配置管理

wangkun hace 1 semana
padre
commit
95ba5b85ba

+ 24 - 0
doc/sql/application_credentials.sql

@@ -0,0 +1,24 @@
+-- ============================================================
+-- 应用凭证模块建表脚本(移植自 sdf-serve t_application_credentials,ude 库)
+-- 执行目标: 10.10.10.30:3306 / ude
+-- 说明: DDL 与 sdf 库源表一致;仅去掉 AUTO_INCREMENT=24(row 起始归零)
+-- ============================================================
+
+CREATE TABLE IF NOT EXISTS `t_application_credentials` (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '凭证 ID',
+  `cred_iden` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '应用标识',
+  `name` varchar(255) NOT NULL COMMENT '应用名称',
+  `api_key` char(32) NOT NULL COMMENT '对外公钥',
+  `api_secret` char(64) NOT NULL COMMENT 'HMAC 原始密钥(明文存储)',
+  `hmac_algo` varchar(32) NOT NULL DEFAULT 'HMAC-SM3' COMMENT '签名算法',
+  `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '启用状态:1=启用,2=禁用',
+  `description` varchar(512) DEFAULT NULL COMMENT '应用描述',
+  `created_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '创建人',
+  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `updated_by` varchar(255) NOT NULL COMMENT '更新人',
+  `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `name` (`name`),
+  UNIQUE KEY `idx_api_key` (`api_key`) USING BTREE,
+  UNIQUE KEY `idx_cred_iden` (`cred_iden`) USING BTREE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='应用凭证表'

+ 2 - 1
go.mod

@@ -4,6 +4,7 @@ go 1.25.10
 
 require (
 	github.com/google/uuid v1.6.0
+	github.com/gorilla/websocket v1.5.3
 	github.com/mattheath/base62 v0.0.0-20150408093626-b80cdc656a7a
 	github.com/minio/minio-go/v7 v7.0.93
 	github.com/robfig/cron/v3 v3.0.1
@@ -11,6 +12,7 @@ require (
 	github.com/tjfoc/gmsm v1.4.1
 	github.com/zeromicro/go-zero v1.10.3
 	go.uber.org/zap v1.27.0
+	golang.org/x/text v0.34.0
 )
 
 require (
@@ -64,7 +66,6 @@ require (
 	golang.org/x/crypto v0.48.0 // indirect
 	golang.org/x/net v0.50.0 // indirect
 	golang.org/x/sys v0.42.0 // indirect
-	golang.org/x/text v0.34.0 // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
 	google.golang.org/grpc v1.80.0 // indirect

+ 2 - 1
go.sum

@@ -56,6 +56,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
+github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/grafana/pyroscope-go v1.3.0 h1:t3Jehad8vvqN4oRAB0LdmfQ5ZSUXQw3asoft+K4GAT8=
 github.com/grafana/pyroscope-go v1.3.0/go.mod h1:XA7I3usNx+UdjOZfQnl1WV8y924vsJo9KIVrKB+9jx4=
 github.com/grafana/pyroscope-go/godeltaprof v0.1.10 h1:dvhndEbyavTb59vFCd6PsrAG5qi69/qZZtegh/TJKSY=
@@ -116,7 +118,6 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
 github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
 github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
 github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
-github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
 github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
 github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
 github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=

+ 2 - 0
internal/audit/middleware.go

@@ -60,6 +60,8 @@ var skipLogPaths = map[string]bool{
 	"/api/v1/logRestore/list":     true,
 	"/api/v1/logArchive/list":     true,
 	"/api/v1/logArchive/download": true,
+	// WebSocket 运维:升级后的连接不经包装 writer,记多了也与 sdf 行为不一致
+	"/api/v1/ws/ops": true,
 }
 
 // LogMiddleware audits every HTTP request: it captures request/response

+ 34 - 0
internal/custompage/cache.go

@@ -0,0 +1,34 @@
+// Package custompage 存放自定义页面(title/logo)的全局内存缓存,
+// 移植自 sdf-serve pkg/common/global:进程内存态,重启即回默认空值。
+package custompage
+
+import "sync"
+
+var mu sync.RWMutex
+
+var (
+	cacheName string
+	cacheLogo string
+)
+
+// SetCache 覆盖页面标题与 logo。
+func SetCache(name, logo string) {
+	mu.Lock()
+	defer mu.Unlock()
+	cacheName, cacheLogo = name, logo
+}
+
+// GetCache 返回当前页面标题与 logo。
+func GetCache() (name, logo string) {
+	mu.RLock()
+	defer mu.RUnlock()
+	return cacheName, cacheLogo
+}
+
+// ClearCache 清空并返回复位后的默认值。
+func ClearCache() (name, logo string) {
+	mu.Lock()
+	defer mu.Unlock()
+	cacheName, cacheLogo = "", ""
+	return cacheName, cacheLogo
+}

+ 32 - 0
internal/handler/applicationconfig/addconfighandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package applicationconfig
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"udeServer/internal/logic/applicationconfig"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+)
+
+// 添加配置
+func AddConfigHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.ConfigAddReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+			return
+		}
+
+		l := applicationconfig.NewAddConfigLogic(r.Context(), svcCtx)
+		resp, err := l.AddConfig(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 32 - 0
internal/handler/applicationconfig/configlisthandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package applicationconfig
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"udeServer/internal/logic/applicationconfig"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+)
+
+// 配置分页列表
+func ConfigListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.ConfigListReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+			return
+		}
+
+		l := applicationconfig.NewConfigListLogic(r.Context(), svcCtx)
+		resp, err := l.ConfigList(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 32 - 0
internal/handler/applicationconfig/deleteconfighandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package applicationconfig
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"udeServer/internal/logic/applicationconfig"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+)
+
+// 删除配置
+func DeleteConfigHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.ConfigDeleteReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+			return
+		}
+
+		l := applicationconfig.NewDeleteConfigLogic(r.Context(), svcCtx)
+		resp, err := l.DeleteConfig(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 32 - 0
internal/handler/applicationconfig/updateconfighandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package applicationconfig
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"udeServer/internal/logic/applicationconfig"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+)
+
+// 更新配置(默认配置 edit_flag=1 不允许改配置项名)
+func UpdateConfigHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.ConfigUpdateReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+			return
+		}
+
+		l := applicationconfig.NewUpdateConfigLogic(r.Context(), svcCtx)
+		resp, err := l.UpdateConfig(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 25 - 0
internal/handler/custompage/clearcachehandler.go

@@ -0,0 +1,25 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package custompage
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"udeServer/internal/logic/custompage"
+	"udeServer/internal/svc"
+)
+
+// 复位自定义页面信息
+func ClearCacheHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		l := custompage.NewClearCacheLogic(r.Context(), svcCtx)
+		resp, err := l.ClearCache()
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 25 - 0
internal/handler/custompage/getcachehandler.go

@@ -0,0 +1,25 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package custompage
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"udeServer/internal/logic/custompage"
+	"udeServer/internal/svc"
+)
+
+// 获取自定义页面信息
+func GetCacheHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		l := custompage.NewGetCacheLogic(r.Context(), svcCtx)
+		resp, err := l.GetCache()
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 32 - 0
internal/handler/custompage/setcachehandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package custompage
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"udeServer/internal/logic/custompage"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+)
+
+// 设置自定义页面信息(标题/logo)
+func SetCacheHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.SetCacheReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+			return
+		}
+
+		l := custompage.NewSetCacheLogic(r.Context(), svcCtx)
+		resp, err := l.SetCache(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 76 - 0
internal/handler/ops/opshandler.go

@@ -0,0 +1,76 @@
+package ops
+
+import (
+	"encoding/json"
+	"net/http"
+
+	"github.com/gorilla/websocket"
+
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+	"udeServer/internal/ws"
+)
+
+// upGrader 允许跨源升级;ws/ops 在审计中间件 skip 名单中,
+// 升级后按 WSRequest/WSResponse 消息驱动,不走 goctl 常规 logic 链。
+var upGrader = websocket.Upgrader{
+	CheckOrigin: func(r *http.Request) bool { return true },
+}
+
+// OpsHandler 是 WebSocket 运维诊断入口(移植自 sdf-serve ops 模块):
+// 1. HTTP 升级成 WebSocket;2. 循环读 WSRequest;3. 校验目标后分发到 ws.Service。
+// 动作定义: ping(逐行回推)/traceroute(逐行回推)/telnet(连接成败+耗时)。
+func OpsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		conn, err := upGrader.Upgrade(w, r, nil)
+		if err != nil {
+			return
+		}
+		defer conn.Close()
+		for {
+			_, message, err := conn.ReadMessage()
+			if err != nil {
+				break
+			}
+			var req types.WSRequest
+			if err := json.Unmarshal(message, &req); err != nil {
+				sendErr(conn, req.Action, "invalid request")
+				continue
+			}
+
+			// 安全校验:防目标地址注入
+			if req.Action == "ping" || req.Action == "traceroute" {
+				if !ws.ValidateTarget(req.Target) {
+					sendErr(conn, req.Action, "非法目标地址")
+					continue
+				}
+			}
+			if req.Action == "telnet" {
+				if !ws.ValidateTarget(req.Host) || !ws.ValidatePort(req.Port) {
+					sendErr(conn, "telnet", "非法 host 或 port")
+					continue
+				}
+			}
+
+			// 执行操作
+			switch req.Action {
+			case "ping":
+				go svcCtx.WsServer.Ping(conn, req)
+			case "traceroute":
+				go svcCtx.WsServer.Traceroute(conn, req)
+			case "telnet":
+				go svcCtx.WsServer.Telnet(conn, req)
+			default:
+				sendErr(conn, req.Action, "unsupported action")
+			}
+		}
+	}
+}
+
+func sendErr(conn *websocket.Conn, action, msg string) {
+	_ = conn.WriteJSON(types.WSResponse{
+		Action: action,
+		Status: "error",
+		Output: msg,
+	})
+}

+ 69 - 0
internal/handler/routes.go

@@ -7,6 +7,9 @@ import (
 	"net/http"
 
 	application "udeServer/internal/handler/application"
+	applicationconfig "udeServer/internal/handler/applicationconfig"
+	custompage "udeServer/internal/handler/custompage"
+	ops "udeServer/internal/handler/ops"
 	systemlog "udeServer/internal/handler/systemlog"
 	"udeServer/internal/svc"
 
@@ -169,6 +172,72 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
 		rest.WithPrefix("/api/v1/applicationCredentials"),
 	)
 
+	server.AddRoutes(
+		[]rest.Route{
+			{
+				// 添加配置
+				Method:  http.MethodPost,
+				Path:    "/addConfig",
+				Handler: applicationconfig.AddConfigHandler(serverCtx),
+			},
+			{
+				// 配置分页列表
+				Method:  http.MethodPost,
+				Path:    "/configList",
+				Handler: applicationconfig.ConfigListHandler(serverCtx),
+			},
+			{
+				// 删除配置
+				Method:  http.MethodPost,
+				Path:    "/deleteConfig",
+				Handler: applicationconfig.DeleteConfigHandler(serverCtx),
+			},
+			{
+				// 更新配置(默认配置 edit_flag=1 不允许改配置项名)
+				Method:  http.MethodPost,
+				Path:    "/updateConfig",
+				Handler: applicationconfig.UpdateConfigHandler(serverCtx),
+			},
+		},
+		rest.WithPrefix("/api/v1/applicationConfig"),
+	)
+
+	server.AddRoutes(
+		[]rest.Route{
+			{
+				// 复位自定义页面信息
+				Method:  http.MethodPost,
+				Path:    "/clear",
+				Handler: custompage.ClearCacheHandler(serverCtx),
+			},
+			{
+				// 获取自定义页面信息
+				Method:  http.MethodGet,
+				Path:    "/get",
+				Handler: custompage.GetCacheHandler(serverCtx),
+			},
+			{
+				// 设置自定义页面信息(标题/logo)
+				Method:  http.MethodPost,
+				Path:    "/set",
+				Handler: custompage.SetCacheHandler(serverCtx),
+			},
+		},
+		rest.WithPrefix("/api/v1/customPage"),
+	)
+
+	server.AddRoutes(
+		[]rest.Route{
+			{
+				// WebSocket 运维诊断(ping/traceroute/telnet)
+				Method:  http.MethodGet,
+				Path:    "/ws/ops",
+				Handler: ops.OpsHandler(serverCtx),
+			},
+		},
+		rest.WithPrefix("/api/v1"),
+	)
+
 	server.AddRoutes(
 		[]rest.Route{
 			{

+ 73 - 0
internal/logic/applicationconfig/addconfiglogic.go

@@ -0,0 +1,73 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package applicationconfig
+
+import (
+	"context"
+	"strings"
+
+	"udeServer/internal/apierr"
+	"udeServer/internal/log"
+	"udeServer/internal/model"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+
+	"github.com/zeromicro/go-zero/core/logx"
+)
+
+type AddConfigLogic struct {
+	logx.Logger
+	ctx    context.Context
+	svcCtx *svc.ServiceContext
+}
+
+// 添加配置
+func NewAddConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AddConfigLogic {
+	return &AddConfigLogic{
+		Logger: logx.WithContext(ctx),
+		ctx:    ctx,
+		svcCtx: svcCtx,
+	}
+}
+
+func (l *AddConfigLogic) AddConfig(req *types.ConfigAddReq) (*types.ConfigAddResp, error) {
+	req.ConfigItem = strings.TrimSpace(req.ConfigItem)
+	if req.ConfigItem == "" {
+		return nil, apierr.BadRequest("配置项不能为空")
+	}
+	if strings.TrimSpace(req.ConfigValue) == "" {
+		return nil, apierr.BadRequest("配置值不能为空")
+	}
+
+	// 同名配置项查重
+	existing, err := l.svcCtx.ConfigModel.FindOneByConfigItem(l.ctx, req.ConfigItem)
+	if err != nil && err != model.ErrNotFound {
+		log.RecordError(l.ctx, err)
+		return nil, apierr.New(500, "添加系统配置失败")
+	}
+	if existing != nil {
+		return nil, apierr.BadRequest("已存在同名:[%s]的应用配置项", req.ConfigItem)
+	}
+
+	data := &model.TConfig{
+		ConfigItem:  req.ConfigItem,
+		ConfigValue: req.ConfigValue,
+		EditFlag:    editFlagAllowEdit, // 新增配置默认允许编辑
+		Description: nullStr(req.Description),
+		UpdatedBy:   configOperatorName,
+	}
+	result, err := l.svcCtx.ConfigModel.Insert(l.ctx, data)
+	if err != nil {
+		log.RecordError(l.ctx, err)
+		return nil, apierr.New(500, "添加系统配置失败")
+	}
+
+	insertId, err := result.LastInsertId()
+	if err != nil {
+		log.RecordError(l.ctx, err)
+		return nil, apierr.New(500, "添加系统配置失败")
+	}
+
+	return &types.ConfigAddResp{Id: insertId}, nil
+}

+ 64 - 0
internal/logic/applicationconfig/configlistlogic.go

@@ -0,0 +1,64 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package applicationconfig
+
+import (
+	"context"
+
+	"udeServer/internal/apierr"
+	"udeServer/internal/log"
+	"udeServer/internal/model"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+
+	"github.com/zeromicro/go-zero/core/logx"
+)
+
+type ConfigListLogic struct {
+	logx.Logger
+	ctx    context.Context
+	svcCtx *svc.ServiceContext
+}
+
+// 配置分页列表
+func NewConfigListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ConfigListLogic {
+	return &ConfigListLogic{
+		Logger: logx.WithContext(ctx),
+		ctx:    ctx,
+		svcCtx: svcCtx,
+	}
+}
+
+func (l *ConfigListLogic) ConfigList(req *types.ConfigListReq) (*types.ConfigListResp, error) {
+	page, pageSize := normalizePage(req.Page, req.PageSize)
+
+	rows, total, err := l.svcCtx.ConfigModel.FindPage(l.ctx, page, pageSize, req.ConfigItem, req.Description)
+	if err != nil {
+		log.RecordError(l.ctx, err)
+		return nil, apierr.New(500, "查询配置列表失败")
+	}
+
+	configs := make([]types.Config, len(rows))
+	for i := range rows {
+		configs[i] = toConfig(&rows[i])
+	}
+
+	return &types.ConfigListResp{
+		Total:   total,
+		Configs: configs,
+	}, nil
+}
+
+// toConfig 模型行转响应体,字段名 descritpion 沿袭 sdf 契约。
+func toConfig(row *model.TConfig) types.Config {
+	return types.Config{
+		Id:          row.Id,
+		ConfigItem:  row.ConfigItem,
+		ConfigValue: row.ConfigValue,
+		EditFlag:    row.EditFlag,
+		Description: row.Description.String,
+		UpdatedBy:   row.UpdatedBy,
+		UpdatedAt:   formatConfigTime(row.UpdatedAt),
+	}
+}

+ 59 - 0
internal/logic/applicationconfig/deleteconfiglogic.go

@@ -0,0 +1,59 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package applicationconfig
+
+import (
+	"context"
+	"errors"
+
+	"udeServer/internal/apierr"
+	"udeServer/internal/log"
+	"udeServer/internal/model"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+
+	"github.com/zeromicro/go-zero/core/logx"
+)
+
+type DeleteConfigLogic struct {
+	logx.Logger
+	ctx    context.Context
+	svcCtx *svc.ServiceContext
+}
+
+// 删除配置
+func NewDeleteConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteConfigLogic {
+	return &DeleteConfigLogic{
+		Logger: logx.WithContext(ctx),
+		ctx:    ctx,
+		svcCtx: svcCtx,
+	}
+}
+
+func (l *DeleteConfigLogic) DeleteConfig(req *types.ConfigDeleteReq) (*types.ConfigDeleteResp, error) {
+	if err := checkId(req.Id); err != nil {
+		return nil, err
+	}
+
+	config, err := l.svcCtx.ConfigModel.FindOne(l.ctx, req.Id)
+	if errors.Is(err, model.ErrNotFound) {
+		return nil, apierr.NotFound("config", req.Id)
+	}
+	if err != nil {
+		log.RecordError(l.ctx, err)
+		return nil, apierr.New(500, "查询系统配置失败")
+	}
+
+	// 默认配置不允许删除
+	if config.EditFlag == editFlagProhibited {
+		return nil, apierr.BadRequest("ID:[%d]的默认配置不能被删除!", req.Id)
+	}
+
+	if err := l.svcCtx.ConfigModel.Delete(l.ctx, req.Id); err != nil {
+		log.RecordError(l.ctx, err)
+		return nil, apierr.New(500, "删除系统配置失败")
+	}
+
+	return &types.ConfigDeleteResp{Id: req.Id}, nil
+}

+ 84 - 0
internal/logic/applicationconfig/helpers.go

@@ -0,0 +1,84 @@
+package applicationconfig
+
+import (
+	"database/sql"
+	"strings"
+	"time"
+
+	"udeServer/internal/apierr"
+)
+
+// editFlag 语义与 t_config.edit_flag 一致(移植自 sdf-serve constants):
+// 1=默认配置,禁止修改配置项名与删除;2=允许编辑。
+const (
+	editFlagProhibited = 1
+	editFlagAllowEdit  = 2
+)
+
+// configOperatorName 无用户体系时的操作人占位,与其他模块一致。
+const configOperatorName = "admin"
+
+// nullStr 非空字符串转合法 NullString,空串置 NULL。
+func nullStr(v string) sql.NullString {
+	return sql.NullString{String: v, Valid: v != ""}
+}
+
+// trimSpace 去首尾空白。
+func trimSpace(v string) string {
+	return strings.TrimSpace(v)
+}
+
+// normalizePage 页码/页大小兜底:1/10 起,上限 100。
+func normalizePage(page, pageSize int64) (int64, int64) {
+	if page < 1 {
+		page = 1
+	}
+	if pageSize < 10 {
+		pageSize = 10
+	}
+	if pageSize > 100 {
+		pageSize = 100
+	}
+	return page, pageSize
+}
+
+// checkId 主键合法性,与 logic 包同名 helper 行为一致。
+func checkId(id int64) error {
+	if id <= 0 {
+		return apierr.BadRequest("id must be greater than 0")
+	}
+
+	return nil
+}
+
+// formatConfigTime 时间展示格式与 sdf-serve 保持一致。
+func formatConfigTime(t time.Time) string {
+	if t.IsZero() {
+		return ""
+	}
+
+	return t.Format("2006-01-02 15:04:05")
+}
+
+// containsConfigKey 判断配置项是否在热加载白名单内(sdf ReloadableKeys 同款)。
+func containsConfigKey(slice []string, item string) bool {
+	for _, v := range slice {
+		if v == item {
+			return true
+		}
+	}
+
+	return false
+}
+
+// reloadableKeys 修改后需要重载 MinIO 客户端与审计日志配置的键,
+// 与 audit 包读取的 config_item 完全对应。
+var reloadableKeys = []string{
+	"file_endpoint",
+	"file_bucket",
+	"file_access_key",
+	"file_secret_key",
+	"file_use_ssl",
+	"log_record_level",
+	"archive_threshold",
+}

+ 94 - 0
internal/logic/applicationconfig/updateconfiglogic.go

@@ -0,0 +1,94 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package applicationconfig
+
+import (
+	"context"
+	"errors"
+	"strings"
+
+	"udeServer/internal/apierr"
+	"udeServer/internal/audit"
+	"udeServer/internal/log"
+	"udeServer/internal/model"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+
+	"github.com/zeromicro/go-zero/core/logx"
+)
+
+type UpdateConfigLogic struct {
+	logx.Logger
+	ctx    context.Context
+	svcCtx *svc.ServiceContext
+}
+
+// 更新配置(默认配置 edit_flag=1 不允许改配置项名)
+func NewUpdateConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateConfigLogic {
+	return &UpdateConfigLogic{
+		Logger: logx.WithContext(ctx),
+		ctx:    ctx,
+		svcCtx: svcCtx,
+	}
+}
+
+func (l *UpdateConfigLogic) UpdateConfig(req *types.ConfigUpdateReq) (*types.ConfigUpdateResp, error) {
+	if err := checkId(req.Id); err != nil {
+		return nil, err
+	}
+	req.ConfigItem = trimSpace(req.ConfigItem)
+	if req.ConfigItem == "" {
+		return nil, apierr.BadRequest("配置项不能为空")
+	}
+	if strings.TrimSpace(req.ConfigValue) == "" {
+		return nil, apierr.BadRequest("配置值不能为空")
+	}
+
+	config, err := l.svcCtx.ConfigModel.FindOne(l.ctx, req.Id)
+	if errors.Is(err, model.ErrNotFound) {
+		return nil, apierr.NotFound("config", req.Id)
+	}
+	if err != nil {
+		log.RecordError(l.ctx, err)
+		return nil, apierr.New(500, "查询系统配置失败")
+	}
+
+	// 默认配置只能改配置值,不允许换配置项名
+	if config.EditFlag == editFlagProhibited && config.ConfigItem != req.ConfigItem {
+		return nil, apierr.BadRequest("ID:[%d]的默认配置不能被编辑", req.Id)
+	}
+
+	// 更新之前校验这个配置项是否已经存在
+	existing, err := l.svcCtx.ConfigModel.FindOneByConfigItem(l.ctx, req.ConfigItem)
+	if err != nil && !errors.Is(err, model.ErrNotFound) {
+		log.RecordError(l.ctx, err)
+		return nil, apierr.New(500, "查询系统配置失败")
+	}
+	if existing != nil && existing.Id != req.Id {
+		return nil, apierr.BadRequest("已存在同名:[%s]的应用配置项", req.ConfigItem)
+	}
+
+	if req.ConfigItem != "" {
+		config.ConfigItem = req.ConfigItem
+	}
+	if req.ConfigValue != "" {
+		config.ConfigValue = req.ConfigValue
+	}
+	config.Description = nullStr(req.Description)
+	config.UpdatedBy = configOperatorName
+
+	if err := l.svcCtx.ConfigModel.Update(l.ctx, config); err != nil {
+		log.RecordError(l.ctx, err)
+		return nil, apierr.New(500, "更新系统配置失败")
+	}
+
+	// 处于热加载白名单里的键,更新后重载 MinIO 客户端与审计日志配置
+	if containsConfigKey(reloadableKeys, req.ConfigItem) {
+		minioClient, logConfig := audit.GetGlobalConfig(l.svcCtx.ConfigModel)
+		l.svcCtx.MinioClient = minioClient
+		l.svcCtx.LogConfigManager = logConfig
+	}
+
+	return &types.ConfigUpdateResp{Id: req.Id}, nil
+}

+ 35 - 0
internal/logic/custompage/clearcachelogic.go

@@ -0,0 +1,35 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package custompage
+
+import (
+	"context"
+
+	pagecache "udeServer/internal/custompage"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+
+	"github.com/zeromicro/go-zero/core/logx"
+)
+
+type ClearCacheLogic struct {
+	logx.Logger
+	ctx    context.Context
+	svcCtx *svc.ServiceContext
+}
+
+// 复位自定义页面信息
+func NewClearCacheLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ClearCacheLogic {
+	return &ClearCacheLogic{
+		Logger: logx.WithContext(ctx),
+		ctx:    ctx,
+		svcCtx: svcCtx,
+	}
+}
+
+func (l *ClearCacheLogic) ClearCache() (*types.ClearCacheResp, error) {
+	name, logo := pagecache.ClearCache()
+
+	return &types.ClearCacheResp{Name: name, Logo: logo}, nil
+}

+ 35 - 0
internal/logic/custompage/getcachelogic.go

@@ -0,0 +1,35 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package custompage
+
+import (
+	"context"
+
+	pagecache "udeServer/internal/custompage"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+
+	"github.com/zeromicro/go-zero/core/logx"
+)
+
+type GetCacheLogic struct {
+	logx.Logger
+	ctx    context.Context
+	svcCtx *svc.ServiceContext
+}
+
+// 获取自定义页面信息
+func NewGetCacheLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetCacheLogic {
+	return &GetCacheLogic{
+		Logger: logx.WithContext(ctx),
+		ctx:    ctx,
+		svcCtx: svcCtx,
+	}
+}
+
+func (l *GetCacheLogic) GetCache() (*types.GetCacheResp, error) {
+	name, logo := pagecache.GetCache()
+
+	return &types.GetCacheResp{Name: name, Logo: logo}, nil
+}

+ 41 - 0
internal/logic/custompage/setcachelogic.go

@@ -0,0 +1,41 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package custompage
+
+import (
+	"context"
+	"strings"
+
+	"udeServer/internal/apierr"
+	pagecache "udeServer/internal/custompage"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+
+	"github.com/zeromicro/go-zero/core/logx"
+)
+
+type SetCacheLogic struct {
+	logx.Logger
+	ctx    context.Context
+	svcCtx *svc.ServiceContext
+}
+
+// 设置自定义页面信息(标题/logo)
+func NewSetCacheLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SetCacheLogic {
+	return &SetCacheLogic{
+		Logger: logx.WithContext(ctx),
+		ctx:    ctx,
+		svcCtx: svcCtx,
+	}
+}
+
+func (l *SetCacheLogic) SetCache(req *types.SetCacheReq) (*types.SetCacheResp, error) {
+	if strings.TrimSpace(req.Name) == "" && strings.TrimSpace(req.Logo) == "" {
+		return nil, apierr.BadRequest("页面标题与 logo 不能同时为空")
+	}
+
+	pagecache.SetCache(req.Name, req.Logo)
+
+	return &types.SetCacheResp{Name: req.Name, Logo: req.Logo}, nil
+}

+ 36 - 0
internal/logic/ops/opslogic.go

@@ -0,0 +1,36 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package ops
+
+import (
+	"context"
+
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+
+	"github.com/zeromicro/go-zero/core/logx"
+)
+
+type OpsLogic struct {
+	logx.Logger
+	ctx    context.Context
+	svcCtx *svc.ServiceContext
+}
+
+// WebSocket 运维诊断(ping/traceroute/telnet)
+// 说明: /ws/ops 由 internal/handler/ops 的 WebSocket handler 直接处理,
+// 不走本 logic;此骨架仅为满足 goctl 路由生成约定。
+func NewOpsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *OpsLogic {
+	return &OpsLogic{
+		Logger: logx.WithContext(ctx),
+		ctx:    ctx,
+		svcCtx: svcCtx,
+	}
+}
+
+func (l *OpsLogic) Ops(req *types.WSRequest) (resp *types.WSResponse, err error) {
+	// todo: add your logic here and delete this line
+
+	return
+}

+ 121 - 8
internal/model/tconfigmodel.go

@@ -2,6 +2,9 @@ package model
 
 import (
 	"context"
+	"database/sql"
+	"fmt"
+	"strings"
 	"time"
 
 	"github.com/zeromicro/go-zero/core/stores/sqlx"
@@ -11,18 +14,25 @@ import (
 // (MinIO endpoint/keys, log level, archive threshold, HMAC secret) are
 // stored here and read back on every request by the audit middleware.
 type TConfig struct {
-	Id          int64     `db:"id"`           // 主键
-	ConfigItem  string    `db:"config_item"`  // 配置项
-	ConfigValue string    `db:"config_value"` // 配置值
-	EditFlag    int64     `db:"edit_flag"`    // 是否允许编辑,1是不可编辑configItem,2是所有均可编辑
-	Description string    `db:"description"`  // 描述
-	UpdatedBy   string    `db:"updated_by"`   // 最后更新人
-	UpdatedAt   time.Time `db:"updated_at"`   // 配置更新时间
+	Id          int64          `db:"id"`           // 主键
+	ConfigItem  string         `db:"config_item"`  // 配置项
+	ConfigValue string         `db:"config_value"` // 配置值
+	EditFlag    int64          `db:"edit_flag"`    // 是否允许编辑,1是不可编辑configItem,2是所有均可编辑
+	Description sql.NullString `db:"description"`  // 描述
+	UpdatedBy   string         `db:"updated_by"`   // 最后更新人
+	UpdatedAt   time.Time      `db:"updated_at"`   // 配置更新时间
 }
 
 type TConfigModel interface {
 	// FindAll returns every configuration row ordered by id.
 	FindAll(ctx context.Context) ([]TConfig, error)
+	// Config management APIs (ported from sdf-serve applicationConfig).
+	Insert(ctx context.Context, data *TConfig) (sql.Result, error)
+	FindOne(ctx context.Context, id int64) (*TConfig, error)
+	FindOneByConfigItem(ctx context.Context, configItem string) (*TConfig, error)
+	Update(ctx context.Context, data *TConfig) error
+	Delete(ctx context.Context, id int64) error
+	FindPage(ctx context.Context, page, pageSize int64, configItem, description string) ([]TConfig, int64, error)
 }
 
 type customTConfigModel struct {
@@ -40,11 +50,114 @@ func NewTConfigModel(conn sqlx.SqlConn) TConfigModel {
 	}
 }
 
+// tConfigRows is the full column list of t_config.
+var tConfigRows = strings.Join([]string{
+	"`id`", "`config_item`", "`config_value`", "`edit_flag`",
+	"`description`", "`updated_by`", "`updated_at`",
+}, ", ")
+
 func (m *customTConfigModel) FindAll(ctx context.Context) ([]TConfig, error) {
 	var resp []TConfig
-	query := "select `id`, `config_item`, `config_value`, `edit_flag`, `description`, `updated_by`, `updated_at` from t_config order by `id` asc"
+	query := fmt.Sprintf("select %s from %s order by `id` asc", tConfigRows, m.table)
 	if err := m.conn.QueryRowsCtx(ctx, &resp, query); err != nil {
 		return nil, err
 	}
 	return resp, nil
 }
+
+// Insert stores one configuration row; updated_at is filled by
+// DEFAULT CURRENT_TIMESTAMP and refreshes via ON UPDATE.
+func (m *customTConfigModel) Insert(ctx context.Context, data *TConfig) (sql.Result, error) {
+	query := fmt.Sprintf("insert into %s (`config_item`, `config_value`, `edit_flag`, `description`, `updated_by`) values (?, ?, ?, ?, ?)", m.table)
+	return m.conn.ExecCtx(ctx, query,
+		data.ConfigItem, data.ConfigValue, data.EditFlag, data.Description, data.UpdatedBy)
+}
+
+// FindOne looks a config row up by its auto-increment id.
+func (m *customTConfigModel) FindOne(ctx context.Context, id int64) (*TConfig, error) {
+	query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", tConfigRows, m.table)
+	var resp TConfig
+	switch err := m.conn.QueryRowCtx(ctx, &resp, query, id); err {
+	case nil:
+		return &resp, nil
+	case sqlx.ErrNotFound:
+		return nil, ErrNotFound
+	default:
+		return nil, err
+	}
+}
+
+// FindOneByConfigItem looks a config row up by its unique config_item.
+func (m *customTConfigModel) FindOneByConfigItem(ctx context.Context, configItem string) (*TConfig, error) {
+	query := fmt.Sprintf("select %s from %s where `config_item` = ? limit 1", tConfigRows, m.table)
+	var resp TConfig
+	switch err := m.conn.QueryRowCtx(ctx, &resp, query, configItem); err {
+	case nil:
+		return &resp, nil
+	case sqlx.ErrNotFound:
+		return nil, ErrNotFound
+	default:
+		return nil, err
+	}
+}
+
+// Update writes the mutable fields; updated_at refreshes via the table's
+// ON UPDATE CURRENT_TIMESTAMP.
+func (m *customTConfigModel) Update(ctx context.Context, data *TConfig) error {
+	query := fmt.Sprintf("update %s set `config_item` = ?, `config_value` = ?, `edit_flag` = ?, `description` = ?, `updated_by` = ? where `id` = ?", m.table)
+	_, err := m.conn.ExecCtx(ctx, query,
+		data.ConfigItem, data.ConfigValue, data.EditFlag, data.Description, data.UpdatedBy, data.Id)
+	return err
+}
+
+// Delete removes the configuration row by its id.
+func (m *customTConfigModel) Delete(ctx context.Context, id int64) error {
+	query := fmt.Sprintf("delete from %s where `id` = ?", m.table)
+	_, err := m.conn.ExecCtx(ctx, query, id)
+	return err
+}
+
+// FindPage returns one page of rows ordered by id with the total count.
+// Empty configItem means no item filter; description matches by LIKE.
+func (m *customTConfigModel) FindPage(ctx context.Context, page, pageSize int64, configItem, description string) ([]TConfig, int64, error) {
+	var conds []string
+	var args []any
+	if configItem != "" {
+		conds = append(conds, "`config_item` = ?")
+		args = append(args, configItem)
+	}
+	if keyword, ok := trimKeyword(description); ok {
+		conds = append(conds, "`description` like ? escape '\\\\'")
+		args = append(args, "%"+escapeLike(keyword)+"%")
+	}
+
+	where := ""
+	if len(conds) > 0 {
+		where = " where " + strings.Join(conds, " and ")
+	}
+
+	var total int64
+	if err := m.conn.QueryRowCtx(ctx, &total, fmt.Sprintf("select count(*) from %s%s", m.table, where), args...); err != nil {
+		return nil, 0, err
+	}
+
+	if total == 0 {
+		return nil, 0, nil
+	}
+
+	offset := (page - 1) * pageSize
+	args = append(args, pageSize, offset)
+	query := fmt.Sprintf("select %s from %s%s order by `id` limit ? offset ?", tConfigRows, m.table, where)
+	var resp []TConfig
+	if err := m.conn.QueryRowsCtx(ctx, &resp, query, args...); err != nil {
+		return nil, 0, err
+	}
+
+	return resp, total, nil
+}
+
+// trimKeyword reports whether a keyword carries any non-space character.
+func trimKeyword(s string) (string, bool) {
+	trimmed := strings.TrimSpace(s)
+	return trimmed, trimmed != ""
+}

+ 7 - 0
internal/svc/servicecontext.go

@@ -7,6 +7,7 @@ import (
 	"udeServer/internal/audit"
 	"udeServer/internal/config"
 	"udeServer/internal/model"
+	"udeServer/internal/ws"
 
 	"github.com/zeromicro/go-zero/core/logx"
 	"github.com/zeromicro/go-zero/core/stores/sqlx"
@@ -30,6 +31,9 @@ type ServiceContext struct {
 	LogConfigManager      *audit.LogConfigManager
 	LogMiddleware         rest.Middleware
 	SpanRecorder          *audit.SpanRecorder
+
+	// 运维诊断 WebSocket(移植自 sdf-serve)
+	WsServer *ws.Service
 }
 
 func NewServiceContext(c config.Config) *ServiceContext {
@@ -55,6 +59,9 @@ func NewServiceContext(c config.Config) *ServiceContext {
 
 		ApplicationCredModel: model.NewTApplicationCredentialsModel(conn),
 
+		// 运维诊断 WebSocket(移植自 sdf-serve)
+		WsServer: ws.NewService(),
+
 		SystemLogModel:        systemLogModel,
 		ConfigModel:           configModel,
 		LogArchiveRecordModel: model.NewTLogArchiveRecordModel(conn),

+ 91 - 0
internal/types/types.go

@@ -78,6 +78,62 @@ type AuditResult struct {
 	Reason  string `json:"reason"`
 }
 
+type ClearCacheResp struct {
+	Name string `json:"name"` // 页面标题(复位后为空)
+	Logo string `json:"logo"` // logo(复位后为空)
+}
+
+type Config struct {
+	Id          int64  `json:"id"`          // 主键
+	ConfigItem  string `json:"configItem"`  // 配置项
+	ConfigValue string `json:"configValue"` // 配置值
+	EditFlag    int64  `json:"editFlag"`    // 是否可编辑:1=禁止编辑(配置项名锁定);2=允许编辑
+	Description string `json:"descritpion"` // 描述(字段名 descritpion 沿袭 sdf 契约)
+	UpdatedBy   string `json:"updatedBy"`   // 更新人
+	UpdatedAt   string `json:"updatedAt"`   // 更新时间
+}
+
+type ConfigAddReq struct {
+	ConfigItem  string `json:"configItem"`           // 配置项
+	ConfigValue string `json:"configValue"`          // 配置值
+	Description string `json:"description,optional"` // 描述
+}
+
+type ConfigAddResp struct {
+	Id int64 `json:"id"`
+}
+
+type ConfigDeleteReq struct {
+	Id int64 `json:"id"`
+}
+
+type ConfigDeleteResp struct {
+	Id int64 `json:"id"`
+}
+
+type ConfigListReq struct {
+	ConfigItem  string `json:"configItem,optional"`  // 配置项精确筛选
+	Description string `json:"description,optional"` // 描述模糊筛选
+	Page        int64  `json:"page,optional"`
+	PageSize    int64  `json:"pageSize,optional"`
+}
+
+type ConfigListResp struct {
+	Total   int64    `json:"total"`
+	Configs []Config `json:"configs"`
+}
+
+type ConfigUpdateReq struct {
+	Id          int64  `json:"id"`                   // 配置ID
+	ConfigItem  string `json:"configItem"`           // 配置项
+	ConfigValue string `json:"configValue"`          // 配置值
+	Description string `json:"description,optional"` // 描述
+}
+
+type ConfigUpdateResp struct {
+	Id int64 `json:"id"`
+}
+
 type CreateDirectoryReq struct {
 	Id        int64  `json:"id,optional"`
 	ServiceId int64  `json:"serviceId,optional"`
@@ -131,6 +187,11 @@ type DirectoryResp struct {
 	Directory string `json:"directory"`
 }
 
+type GetCacheResp struct {
+	Name string `json:"name"` // 页面标题
+	Logo string `json:"logo"` // logo
+}
+
 type KeyIdReq struct {
 	Id int64 `json:"id"`
 }
@@ -238,6 +299,11 @@ type LogRestoreListResp struct {
 	LogRestores []LogRestore `json:"logRestores"`
 }
 
+type PageCache struct {
+	Name string `json:"name"` // 页面标题
+	Logo string `json:"logo"` // logo 路径或编码
+}
+
 type Request struct {
 	Name string `path:"name,options=you|me"`
 }
@@ -270,6 +336,16 @@ type ServiceResp struct {
 	Brush        int64  `json:"brush"`
 }
 
+type SetCacheReq struct {
+	Name string `json:"name"` // 页面标题
+	Logo string `json:"logo"` // logo
+}
+
+type SetCacheResp struct {
+	Name string `json:"name"` // 页面标题
+	Logo string `json:"logo"` // logo
+}
+
 type SystemLog struct {
 	Id                  int64  `json:"id"`                  // 主键
 	Iden                string `json:"iden"`                // 日志标识
@@ -381,3 +457,18 @@ type UpdateServiceReq struct {
 	Brush       int64    `json:"brush,optional"`
 	Directories []string `json:"directories,optional"`
 }
+
+type WSRequest struct {
+	Action  string `json:"action"`           // 命令类型:ping;traceroute;telnet
+	Target  string `json:"target"`           // IP或域名,ping/traceroute 用
+	Count   int    `json:"count,optional"`   // ping 次数,默认 4
+	Host    string `json:"host"`             // telnet host
+	Port    int    `json:"port"`             // telnet port
+	MaxHops int    `json:"maxHops,optional"` // traceroute 最大跳数,默认 30
+}
+
+type WSResponse struct {
+	Action string `json:"action"` // 命令类型
+	Status string `json:"status"` // 状态:running;done;error
+	Output string `json:"output"` // 输出信息
+}

+ 176 - 0
internal/ws/service.go

@@ -0,0 +1,176 @@
+// Package ws 提供运维诊断通道(移植自 sdf-serve internal/ws):
+// ping/traceroute 起子进程逐行推流,telnet 做 TCP 拨测。
+package ws
+
+import (
+	"bufio"
+	"bytes"
+	"context"
+	"fmt"
+	"io"
+	"net"
+	"os/exec"
+	"regexp"
+	"runtime"
+	"strconv"
+	"time"
+
+	"github.com/gorilla/websocket"
+	"golang.org/x/text/encoding/simplifiedchinese"
+	"golang.org/x/text/transform"
+
+	"udeServer/internal/types"
+)
+
+type Service struct{}
+
+func NewService() *Service {
+	return &Service{}
+}
+
+// ValidateTarget 校验 ping/traceroute/telnet 的目标地址:仅允许 IP 或
+// 字母/数字/点/连字符,阻断空格与特殊字符注入。
+func ValidateTarget(target string) bool {
+	if net.ParseIP(target) != nil {
+		return true
+	}
+	re := regexp.MustCompile(`^[a-zA-Z0-9.-]+$`)
+	return re.MatchString(target)
+}
+
+// ValidatePort 校验 telnet 端口范围。
+func ValidatePort(port int) bool {
+	return port > 0 && port <= 65535
+}
+
+// Ping 实时输出(跨平台)
+func (s *Service) Ping(conn *websocket.Conn, req types.WSRequest) {
+	count := req.Count
+	if count == 0 {
+		count = 4
+	}
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	defer cancel()
+	var args []string
+	if runtime.GOOS == "windows" {
+		args = []string{req.Target, "/n", fmt.Sprintf("%d", count)}
+	} else {
+		args = []string{"-c", fmt.Sprintf("%d", count), req.Target}
+	}
+	cmd := exec.CommandContext(ctx, "ping", args...)
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		_ = conn.WriteJSON(types.WSResponse{Action: "ping", Status: "error", Output: err.Error()})
+		return
+	}
+	if err := cmd.Start(); err != nil {
+		_ = conn.WriteJSON(types.WSResponse{Action: "ping", Status: "error", Output: err.Error()})
+		return
+	}
+	scanner := bufio.NewScanner(stdout)
+	for scanner.Scan() {
+		line := scanner.Text()
+		if runtime.GOOS == "windows" {
+			line = gbkToUtf8([]byte(line))
+		}
+		_ = conn.WriteJSON(types.WSResponse{Action: "ping", Status: "running", Output: line})
+	}
+
+	if err := cmd.Wait(); err != nil {
+		_ = conn.WriteJSON(types.WSResponse{Action: "ping", Status: "error", Output: err.Error()})
+		return
+	}
+
+	_ = conn.WriteJSON(types.WSResponse{Action: "ping", Status: "done", Output: "Ping finished"})
+}
+
+// Traceroute 实时输出(跨平台)
+func (s *Service) Traceroute(conn *websocket.Conn, req types.WSRequest) {
+	maxHops := req.MaxHops
+	if maxHops == 0 {
+		maxHops = 30
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
+	defer cancel()
+
+	var cmd *exec.Cmd
+	if runtime.GOOS == "windows" {
+		// Windows 使用 tracert,/h 指定最大跳数
+		cmd = exec.CommandContext(ctx, "tracert", "/h", fmt.Sprintf("%d", maxHops), req.Target)
+	} else {
+		// Linux/macOS 使用 traceroute
+		cmd = exec.CommandContext(ctx, "traceroute", "-m", fmt.Sprintf("%d", maxHops), req.Target)
+	}
+
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		_ = conn.WriteJSON(types.WSResponse{Action: "traceroute", Status: "error", Output: err.Error()})
+		return
+	}
+
+	if err := cmd.Start(); err != nil {
+		_ = conn.WriteJSON(types.WSResponse{Action: "traceroute", Status: "error", Output: err.Error()})
+		return
+	}
+
+	scanner := bufio.NewScanner(stdout)
+	for scanner.Scan() {
+		line := scanner.Text()
+		if runtime.GOOS == "windows" {
+			line = gbkToUtf8([]byte(line)) // Windows 输出是 GBK
+		}
+		_ = conn.WriteJSON(types.WSResponse{Action: "traceroute", Status: "running", Output: line})
+	}
+
+	if err := cmd.Wait(); err != nil {
+		_ = conn.WriteJSON(types.WSResponse{Action: "traceroute", Status: "error", Output: err.Error()})
+		return
+	}
+
+	_ = conn.WriteJSON(types.WSResponse{Action: "traceroute", Status: "done", Output: "Traceroute finished"})
+}
+
+// Telnet 实时输出
+func (s *Service) Telnet(conn *websocket.Conn, req types.WSRequest) {
+	address := net.JoinHostPort(req.Host, strconv.Itoa(req.Port))
+	_ = conn.WriteJSON(types.WSResponse{Action: "telnet", Status: "running", Output: fmt.Sprintf("尝试连接 %s ...", address)})
+
+	timeout := 5 * time.Second
+	start := time.Now()
+	type dialResult struct {
+		msg string
+		ok  bool
+	}
+	connCh := make(chan dialResult, 1)
+
+	go func() {
+		_, err := net.DialTimeout("tcp", address, timeout)
+		if err != nil {
+			connCh <- dialResult{fmt.Sprintf("连接 %s 失败: %v", address, err), false}
+		} else {
+			connCh <- dialResult{fmt.Sprintf("连接 %s 成功", address), true}
+		}
+	}()
+
+	select {
+	case res := <-connCh:
+		status := "done"
+		if !res.ok {
+			status = "error"
+		}
+		_ = conn.WriteJSON(types.WSResponse{Action: "telnet", Status: status, Output: res.msg})
+	case <-time.After(timeout + 1*time.Second):
+		_ = conn.WriteJSON(types.WSResponse{Action: "telnet", Status: "error", Output: "连接超时"})
+	}
+
+	duration := time.Since(start).Milliseconds()
+	_ = conn.WriteJSON(types.WSResponse{Action: "telnet", Status: "done", Output: fmt.Sprintf("耗时 %d ms", duration)})
+}
+
+// gbkToUtf8 转换 Windows 子进程 GBK 输出。
+func gbkToUtf8(data []byte) string {
+	reader := transform.NewReader(bytes.NewReader(data), simplifiedchinese.GBK.NewDecoder())
+	result, _ := io.ReadAll(reader)
+	return string(result)
+}

+ 147 - 0
udeServer.api

@@ -548,3 +548,150 @@ service udeServer-api {
 	post /updateApplicationStatus (ApplicationCredentialsUpdateStatusReq) returns (ApplicationCredentialsUpdateStatusResp)
 }
 
+// ===== 运维诊断(移植自 sdf-serve ops 模块,WebSocket)=====
+type WSRequest {
+	Action  string `json:"action"` // 命令类型:ping;traceroute;telnet
+	Target  string `json:"target"` // IP或域名,ping/traceroute 用
+	Count   int    `json:"count,optional"` // ping 次数,默认 4
+	Host    string `json:"host"` // telnet host
+	Port    int    `json:"port"` // telnet port
+	MaxHops int    `json:"maxHops,optional"` // traceroute 最大跳数,默认 30
+}
+
+type WSResponse {
+	Action string `json:"action"` // 命令类型
+	Status string `json:"status"` // 状态:running;done;error
+	Output string `json:"output"` // 输出信息
+}
+
+@server (
+	prefix: /api/v1
+	group:  ops
+)
+// 运维诊断模块(WebSocket,先 HTTP 升级再以 WSRequest/WSResponse 消息交互)
+service udeServer-api {
+	@doc "WebSocket 运维诊断(ping/traceroute/telnet)"
+	@handler OpsHandler
+	get /ws/ops (WSRequest) returns (WSResponse)
+}
+
+// ===== 系统配置(移植自 sdf-serve applicationConfig 模块,操作 t_config)=====
+type Config {
+	Id          int64  `json:"id"` // 主键
+	ConfigItem  string `json:"configItem"` // 配置项
+	ConfigValue string `json:"configValue"` // 配置值
+	EditFlag    int64  `json:"editFlag"` // 是否可编辑:1=禁止编辑(配置项名锁定);2=允许编辑
+	Description string `json:"descritpion"` // 描述(字段名 descritpion 沿袭 sdf 契约)
+	UpdatedBy   string `json:"updatedBy"` // 更新人
+	UpdatedAt   string `json:"updatedAt"` // 更新时间
+}
+
+type ConfigListReq {
+	ConfigItem  string `json:"configItem,optional"` // 配置项精确筛选
+	Description string `json:"description,optional"` // 描述模糊筛选
+	Page        int64  `json:"page,optional"`
+	PageSize    int64  `json:"pageSize,optional"`
+}
+
+type ConfigListResp {
+	Total   int64    `json:"total"`
+	Configs []Config `json:"configs"`
+}
+
+type ConfigAddReq {
+	ConfigItem  string `json:"configItem"` // 配置项
+	ConfigValue string `json:"configValue"` // 配置值
+	Description string `json:"description,optional"` // 描述
+}
+
+type ConfigAddResp {
+	Id int64 `json:"id"`
+}
+
+type ConfigDeleteReq {
+	Id int64 `json:"id"`
+}
+
+type ConfigDeleteResp {
+	Id int64 `json:"id"`
+}
+
+type ConfigUpdateReq {
+	Id          int64  `json:"id"` // 配置ID
+	ConfigItem  string `json:"configItem"` // 配置项
+	ConfigValue string `json:"configValue"` // 配置值
+	Description string `json:"description,optional"` // 描述
+}
+
+type ConfigUpdateResp {
+	Id int64 `json:"id"`
+}
+
+// ===== 自定义页面(移植自 sdf-serve customPage 模块,title/logo 内存缓存)=====
+type PageCache {
+	Name string `json:"name"` // 页面标题
+	Logo string `json:"logo"` // logo 路径或编码
+}
+
+type SetCacheReq {
+	Name string `json:"name"` // 页面标题
+	Logo string `json:"logo"` // logo
+}
+
+type SetCacheResp {
+	Name string `json:"name"` // 页面标题
+	Logo string `json:"logo"` // logo
+}
+
+type GetCacheResp {
+	Name string `json:"name"` // 页面标题
+	Logo string `json:"logo"` // logo
+}
+
+type ClearCacheResp {
+	Name string `json:"name"` // 页面标题(复位后为空)
+	Logo string `json:"logo"` // logo(复位后为空)
+}
+
+@server (
+	prefix: /api/v1/applicationConfig
+	group:  applicationconfig
+)
+// 系统配置管理模块
+service udeServer-api {
+	@doc "添加配置"
+	@handler AddConfigHandler
+	post /addConfig (ConfigAddReq) returns (ConfigAddResp)
+
+	@doc "配置分页列表"
+	@handler ConfigListHandler
+	post /configList (ConfigListReq) returns (ConfigListResp)
+
+	@doc "删除配置"
+	@handler DeleteConfigHandler
+	post /deleteConfig (ConfigDeleteReq) returns (ConfigDeleteResp)
+
+	@doc "更新配置(默认配置 edit_flag=1 不允许改配置项名)"
+	@handler UpdateConfigHandler
+	post /updateConfig (ConfigUpdateReq) returns (ConfigUpdateResp)
+}
+
+@server (
+	prefix: /api/v1/customPage
+	group:  custompage
+)
+// 自定义页面模块
+service udeServer-api {
+	@doc "设置自定义页面信息(标题/logo)"
+	@handler SetCacheHandler
+	post /set (SetCacheReq) returns (SetCacheResp)
+
+	@doc "获取自定义页面信息"
+	@handler GetCacheHandler
+	get /get returns (GetCacheResp)
+
+	@doc "复位自定义页面信息"
+	@handler ClearCacheHandler
+	post /clear returns (ClearCacheResp)
+}
+