فهرست منبع

Merge branch 'master' of http://192.168.1.30:3000/niezenghua/dsbqj_admin

DESKTOP-HN5QP3V\Administrator 2 ماه پیش
والد
کامیت
8f1e913798

+ 6 - 0
.idea/thriftCompilerAssist.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ThriftCompilerAssist">
+    <compilers />
+  </component>
+</project>

+ 159 - 0
app/api/hotupdate/internal.go

@@ -0,0 +1,159 @@
+/**
+ * @author chengliang
+ * @date 2026/1/26 15:40
+ * @brief
+ *
+ **/
+
+package hotupdate
+
+import (
+	"dsbqj-admin/app/service"
+	"dsbqj-admin/pkg/app"
+	"dsbqj-admin/pkg/e"
+	"dsbqj-admin/pkg/logger"
+	"dsbqj-admin/tool"
+	"encoding/json"
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"net/http"
+	"time"
+)
+
+const DEF_REQ_TIMEOUT_SEC = 30 * 60
+
+// 内部请求
+func AddVersion(c *gin.Context) {
+	var appG = app.Gin{C: c}
+	logger.Info("uri %s", c.Request.RequestURI)
+	req := service.TAddVersionReq{}
+	err := c.ShouldBind(&req)
+	if err == nil {
+		//err = checkAddVersionHash(req, true)
+		//if err != nil {
+		//	appG.Response(http.StatusOK, e.ERROR_UPLOAD_SIGNURL_FAIL, err.Error())
+		//	return
+		//}
+		var err = service.GetTHotUpdateVerManager().AddVersion(&req)
+		if err != nil {
+			appG.Response(http.StatusOK, e.NO_RECORD, err.Error())
+		} else {
+			appG.Response(http.StatusOK, e.SUCCESS, "")
+		}
+	} else {
+		appG.Response(http.StatusOK, e.INVALID_PARAMS, err.Error())
+	}
+}
+
+// 获取最大version
+func GetMaxVerInfo(c *gin.Context) {
+	logger.Info("uri %s", c.Request.RequestURI)
+	var appG = app.Gin{C: c}
+	req := service.TGetMaxVersionReq{}
+	err := c.ShouldBind(&req)
+	if err == nil {
+		//err = checkAddVersionHash(req, false)
+		//if err != nil {
+		//	appG.Response(http.StatusOK, e.ERROR_UPLOAD_SIGNURL_FAIL, err.Error())
+		//	return
+		//}
+		var rsp, err = service.GetTHotUpdateVerManager().GetMaxVerInfo(req.Os)
+		if err != nil {
+			appG.Response(http.StatusOK, e.NO_RECORD, err.Error())
+		} else {
+			appG.Response(http.StatusOK, e.SUCCESS, rsp)
+		}
+	} else {
+		appG.Response(http.StatusOK, e.INVALID_PARAMS, err.Error())
+	}
+}
+
+// 获取version list
+func GetVersionList(c *gin.Context) {
+	logger.Info("uri %s", c.Request.RequestURI)
+	var appG = app.Gin{C: c}
+	req := service.TGetVersionListReq{}
+	err := c.ShouldBind(&req)
+	if err == nil {
+		//err = checkAddVersionHash(req, false)
+		//if err != nil {
+		//	appG.Response(http.StatusOK, e.ERROR_UPLOAD_SIGNURL_FAIL, err.Error())
+		//	return
+		//}
+		var rsp, err = service.GetTHotUpdateVerManager().GetVersionList(req.Start, req.Limit)
+		if err != nil {
+			appG.Response(http.StatusOK, e.NO_RECORD, err.Error())
+		} else {
+			appG.Response(http.StatusOK, e.SUCCESS, rsp)
+		}
+	} else {
+		appG.Response(http.StatusOK, e.INVALID_PARAMS, err.Error())
+	}
+}
+
+// 修改状态
+func ChangeStatus(c *gin.Context) {
+	logger.Info("uri %s", c.Request.RequestURI)
+	var appG = app.Gin{C: c}
+	req := service.TChangeStautsReq{}
+	err := c.ShouldBind(&req)
+	if err == nil {
+		//err = checkAddVersionHash(req, false)
+		//if err != nil {
+		//	appG.Response(http.StatusOK, e.ERROR_UPLOAD_SIGNURL_FAIL, err.Error())
+		//	return
+		//}
+		var rsp, err = service.GetTHotUpdateVerManager().ChangeStatus(req.ID, req.Status)
+		if err != nil {
+			appG.Response(http.StatusOK, e.NO_RECORD, err.Error())
+		} else {
+			appG.Response(http.StatusOK, e.SUCCESS, rsp)
+		}
+	} else {
+		appG.Response(http.StatusOK, e.INVALID_PARAMS, err.Error())
+	}
+}
+
+func checkAddVersionHash(tReq any, checkTimeout bool) error {
+
+	jsonBytes, err := json.Marshal(tReq)
+	if err != nil {
+		return err
+	}
+	var res map[string]any
+	err = json.Unmarshal(jsonBytes, &res)
+	if err != nil {
+		return err
+	}
+	if checkTimeout {
+		reqTimeStr, ok := res["timesec"]
+		if !ok {
+			return fmt.Errorf("time is empty")
+		}
+		curTime := time.Now().Unix()
+		reqTime, ok := reqTimeStr.(int64) //strconv.ParseInt(reqTimeStr, 10, 64)
+		if !ok {
+			return fmt.Errorf("timesec:%s is not int64", reqTimeStr)
+		}
+		deltaTime := curTime - reqTime
+		if deltaTime < 0 {
+			deltaTime = -deltaTime
+		}
+		if deltaTime > DEF_REQ_TIMEOUT_SEC {
+			return fmt.Errorf("time:%d, nowtime:%d", reqTime, curTime)
+		}
+	}
+
+	reqSign, ok := res["sign"]
+	if !ok {
+		return fmt.Errorf("sign is empty")
+	}
+	delete(res, "sign")
+
+	sign := tool.GenSign(res)
+	if sign != reqSign {
+		return fmt.Errorf("reqSign:%s, but now sign:%s, sign error", reqSign, sign)
+	}
+
+	return nil
+}

+ 3 - 20
app/api/hotupdate/hotupdate.go → app/api/hotupdate/public.go

@@ -16,18 +16,18 @@ import (
 	"net/http"
 )
 
-/**
+/*
+*
 hotupdate/getversion?os=ios&proj=bzst
 获取最新的version内容
 */
-
 func GetVersion(c *gin.Context) {
 	logger.Info("uri %s", c.Request.RequestURI)
 	var appG = app.Gin{C: c}
 	req := service.TGetVersionReq{}
 	err := c.ShouldBind(&req)
 	if err == nil {
-		var rsp, err = service.GetTHotUpdateVerManager().GetMaxVerInfo(req.Proj, req.Os)
+		var rsp, err = service.GetTHotUpdateVerManager().GetMaxPubVerInfo(req.Proj, req.Os)
 		if err != nil {
 			appG.Response(http.StatusOK, e.NO_RECORD, err.Error())
 		} else {
@@ -38,20 +38,3 @@ func GetVersion(c *gin.Context) {
 		appG.Response(http.StatusOK, e.INVALID_PARAMS, err.Error())
 	}
 }
-
-func AddVersion(c *gin.Context) {
-	var appG = app.Gin{C: c}
-	logger.Info("uri %s", c.Request.RequestURI)
-	req := service.TAddVersionReq{}
-	err := c.ShouldBind(&req)
-	if err == nil {
-		var err = service.GetTHotUpdateVerManager().AddVersion(&req)
-		if err != nil {
-			appG.Response(http.StatusOK, e.NO_RECORD, err.Error())
-		} else {
-			appG.Response(http.StatusOK, e.SUCCESS, "")
-		}
-	} else {
-		appG.Response(http.StatusOK, e.INVALID_PARAMS, err.Error())
-	}
-}

+ 10 - 3
app/router/router.go

@@ -4,9 +4,8 @@ import (
 	"dsbqj-admin/app/api/hotupdate"
 	v1 "dsbqj-admin/app/api/v1"
 	"dsbqj-admin/middleware"
-	"os"
-
 	"github.com/gin-gonic/gin"
+	"os"
 )
 
 // NewRouter 路由配置
@@ -53,7 +52,15 @@ func NewRouter() *gin.Engine {
 	hotupdateR := r.Group("/hotupdate")
 	{
 		hotupdateR.GET("/getversion", hotupdate.GetVersion)
-		hotupdateR.GET("/addversion", hotupdate.AddVersion)
+	}
+
+	// TODO 内网请求,需要添加ip白名单等
+	internalR := r.Group("/internal")
+	{
+		internalR.GET("/addversion", hotupdate.AddVersion)
+		internalR.GET("/getmaxverinfo", hotupdate.GetMaxVerInfo)
+		internalR.GET("/getversionlist", hotupdate.GetVersionList)
+		internalR.GET("/changestatus", hotupdate.ChangeStatus)
 	}
 	//webv1.Use(middleware.BodyHandler())
 	//{

+ 125 - 37
app/service/hotupdate.go

@@ -12,11 +12,23 @@ import (
 	"dsbqj-admin/pkg/logger"
 	"errors"
 	"fmt"
+	"go.mongodb.org/mongo-driver/bson"
+	"go.mongodb.org/mongo-driver/mongo/options"
 	"strconv"
 	"strings"
 	"sync"
 )
 
+var validProj = map[string]bool{
+	"bzst": true,
+	"ds20": true,
+}
+
+var validOs = map[string]bool{
+	"ios":     true,
+	"android": true,
+}
+
 type THotUpdateVerManager struct {
 	mu         sync.RWMutex
 	versionMap map[string]map[string]*TGetVersionRsp // proj:os:versionInfo
@@ -39,26 +51,33 @@ func GetTHotUpdateVerManager() *THotUpdateVerManager {
 }
 
 func (this *THotUpdateVerManager) Init() {
-	_, err := this.GetMaxVerInfo("bzst", "ios")
-	if err != nil {
-		logger.Error(err.Error())
-	}
-	_, err = this.GetMaxVerInfo("bzst", "android")
-	if err != nil {
-		logger.Error(err.Error())
+
+	for proj, _ := range validProj {
+		for os, _ := range validOs {
+			_, err := this.GetMaxPubVerInfo(proj, os)
+			if err != nil {
+				logger.Error(err.Error())
+			}
+		}
 	}
+	logger.Info("version manager init success, version info:%#v", this.versionMap)
 }
 
-func (this *THotUpdateVerManager) GetMaxVerInfo(proj, os string) (*TGetVersionRsp, error) {
-
+func (this *THotUpdateVerManager) GetMaxPubVerInfo(proj, os string) (*TGetVersionRsp, error) {
 	if proj == "" || os == "" {
 		return nil, errors.New("project or os can't be empty")
 	}
+	if !validProj[proj] {
+		return nil, fmt.Errorf("project:%s invalid", proj)
+	}
+	if !validOs[os] {
+		return nil, fmt.Errorf("os:%s invalid", os)
+	}
 
 	if ver, err := this.getVersionMapBy(proj, os); err == nil {
 		return ver, nil
 	} else {
-		versionRsp, err := this.findMaxVersion(proj, os)
+		versionRsp, err := this.findDBMaxPubVersion(proj, os)
 		if err != nil {
 			return nil, err
 		}
@@ -67,6 +86,24 @@ func (this *THotUpdateVerManager) GetMaxVerInfo(proj, os string) (*TGetVersionRs
 	}
 }
 
+func (this *THotUpdateVerManager) reloadPubVerBy(proj, os string) error {
+	versionRsp, err := this.findDBMaxPubVersion(proj, os)
+	if err != nil {
+		return err
+	}
+	this.updateVersionMap(proj, os, versionRsp)
+	return nil
+}
+
+// 内部API调用
+func (this *THotUpdateVerManager) GetMaxVerInfo(os string) (*TGetVersionRsp, error) {
+	if os == "" {
+		return nil, errors.New("project or os can't be empty")
+	}
+	return this.findDBMaxVersion("", os)
+}
+
+// 内部API调用
 func (this *THotUpdateVerManager) AddVersion(req *TAddVersionReq) error {
 	if !this.isInvalid(req) {
 		return errors.New("version is invalid")
@@ -78,20 +115,52 @@ func (this *THotUpdateVerManager) AddVersion(req *TAddVersionReq) error {
 		PackageUrl:        req.PackageUrl,
 		RemoteVersionUrl:  req.RemoteVersionUrl,
 		RemoteManifestUrl: req.RemoteManifestUrl,
+		Status:            hotupdate.DEF_VER_STATUS_NONE,
+	}
+	return this.versionDB.Create(mVersion)
+}
+
+func (this *THotUpdateVerManager) ChangeStatus(id string, status int16) (*TGetVersionRsp, error) {
+	if id == "" {
+		return nil, errors.New("id can't be empty")
+	}
+	if status != hotupdate.DEF_VER_STATUS_NONE && status != hotupdate.DEF_VER_STATUS_PUBLIC {
+		return nil, fmt.Errorf("invalid status %d", status)
 	}
-	err := this.versionDB.Create(mVersion)
+	updateMap := make(map[string]interface{})
+	updateMap["status"] = status
+	mVersion, err := this.versionDB.Update(id, updateMap)
 	if err != nil {
-		return err
+		return nil, err
 	}
-	verRsp := &TGetVersionRsp{
-		Version:           req.Version,
-		PackageUrl:        req.PackageUrl,
-		RemoteVersionUrl:  req.RemoteVersionUrl,
-		RemoteManifestUrl: req.RemoteManifestUrl,
+	logger.Info("after update mVersion", mVersion)
+	err = this.reloadPubVerBy(mVersion.Proj, mVersion.Os)
+	if err != nil {
+		return nil, fmt.Errorf("reloadPubVerBy error: %v", err)
 	}
-	this.updateVersionMap(req.Proj, req.Os, verRsp)
+	vRsp := &TGetVersionRsp{}
+	vRsp.FromMVersion(mVersion)
+	return vRsp, nil
+}
 
-	return nil
+// 内部API调用 分页获取version list
+func (this *THotUpdateVerManager) GetVersionList(start, limit int64) ([]*TGetVersionRsp, error) {
+	opts := options.Find().
+		SetSort(bson.M{"created_at": -1}). // 按版本降序
+		SetSkip(start - 1).
+		SetLimit(limit)
+
+	dbVersionArr, err := this.versionDB.Find(nil, opts)
+	if err != nil {
+		return nil, err
+	}
+	rspArr := make([]*TGetVersionRsp, 0, len(dbVersionArr))
+	for _, mVersion := range dbVersionArr {
+		vRsp := &TGetVersionRsp{}
+		vRsp.FromMVersion(&mVersion)
+		rspArr = append(rspArr, vRsp)
+	}
+	return rspArr, nil
 }
 
 func (this *THotUpdateVerManager) getVersionMapBy(proj, os string) (*TGetVersionRsp, error) {
@@ -110,16 +179,18 @@ func (this *THotUpdateVerManager) getVersionMapBy(proj, os string) (*TGetVersion
 func (this *THotUpdateVerManager) updateVersionMap(proj, os string, versionRsp *TGetVersionRsp) {
 	this.mu.Lock()
 	defer this.mu.Unlock()
-	logger.Info(fmt.Sprintf("updateVersionMap <UNK>%s<UNK>%s<UNK>%s", proj, os, versionRsp.Version))
+	logger.Info(fmt.Sprintf("updateVersionMap proj:%s, os:%s, ver:%s", proj, os, versionRsp.Version))
 	if _, ok := this.versionMap[proj]; !ok {
 		this.versionMap[proj] = make(map[string]*TGetVersionRsp)
 	}
+	logger.Info("before updateVersionMap %#v", this.versionMap[proj][os])
 	this.versionMap[proj][os] = versionRsp
+	logger.Info("after updateVersionMap %#v", this.versionMap[proj][os])
 }
 
 // 版本号是递增的
 func (this *THotUpdateVerManager) isInvalid(req *TAddVersionReq) bool {
-	curVersion, err := this.GetMaxVerInfo(req.Proj, req.Os)
+	curVersion, err := this.findDBMaxVersion(req.Proj, req.Os)
 	if err != nil {
 		// 默认只有系统初始化时
 		return true
@@ -128,28 +199,45 @@ func (this *THotUpdateVerManager) isInvalid(req *TAddVersionReq) bool {
 	return CompareVersion(req.Version, curVersion.Version) == 1
 }
 
-func (this *THotUpdateVerManager) findMaxVersion(proj, os string) (*TGetVersionRsp, error) {
-	if proj == "" || os == "" {
-		return nil, errors.New("project or os can't be empty")
+// 对外的最大version
+func (this *THotUpdateVerManager) findDBMaxPubVersion(proj, os string) (*TGetVersionRsp, error) {
+	return this.findMaxVersionByStatus(proj, os, hotupdate.DEF_VER_STATUS_PUBLIC)
+}
+
+// 查找数据库中最大的version
+func (this *THotUpdateVerManager) findDBMaxVersion(proj, os string) (*TGetVersionRsp, error) {
+	if os == "" {
+		return nil, errors.New("os can't be empty")
 	}
+	return this.findMaxVersionByStatus(proj, os, hotupdate.DEF_VER_STATUS_INVALID)
+}
 
-	versions, num, err := this.versionDB.ListByProjectAndOS(proj, os, 1, 20)
-	// 因为是按时间降序 潜规则最近一条的version最大
-	if err != nil {
-		return nil, err
+func (this *THotUpdateVerManager) findMaxVersionByStatus(proj, os string, status int16) (*TGetVersionRsp, error) {
+
+	filte := bson.M{}
+	if proj != "" {
+		filte["project"] = proj
+	}
+	if os != "" {
+		filte["os"] = os
 	}
-	logger.Debug("versions:", num, versions)
-	if num == 0 {
-		return nil, errors.New(fmt.Sprintf("proj:%s os:%s not find", proj, os))
+	if status == hotupdate.DEF_VER_STATUS_PUBLIC || status == hotupdate.DEF_VER_STATUS_NONE {
+		filte["status"] = status
 	}
+	opts := options.Find().
+		SetSort(bson.M{"created_at": -1})
 
-	versionObj := versions[0]
-	versionRsp := &TGetVersionRsp{
-		Version:           versionObj.Version,
-		PackageUrl:        versionObj.PackageUrl,
-		RemoteManifestUrl: versionObj.RemoteManifestUrl,
-		RemoteVersionUrl:  versionObj.RemoteVersionUrl,
+	logger.Info("filte:%v", filte)
+	versionArr, err := this.versionDB.Find(filte, opts)
+	if err != nil {
+		return nil, err
+	}
+	if len(versionArr) == 0 {
+		return nil, fmt.Errorf("version not found proj:%s, os:%s, status:%d", proj, os, status)
 	}
+	mVersion := versionArr[0]
+	versionRsp := &TGetVersionRsp{}
+	versionRsp.FromMVersion(&mVersion)
 	return versionRsp, nil
 }
 

+ 37 - 0
app/service/type.go

@@ -7,16 +7,41 @@
 
 package service
 
+import "dsbqj-admin/model/mongo/hotupdate"
+
 type TGetVersionReq struct {
 	Proj string `form:"proj" binding:"required" json:"proj"`
 	Os   string `form:"os" binding:"required" json:"os"`
 }
 
+type TGetMaxVersionReq struct {
+	Os string `form:"os" binding:"required" json:"os"`
+}
+
 type TGetVersionRsp struct {
+	ID                string `json:"id"`
+	Proj              string `json:"proj"`
+	Os                string `json:"os"`
+	CreateTimeSec     int64  `json:"createTime"`
+	UpdateTimeSec     int64  `json:"updateTime"`
 	Version           string `json:"version"`
 	PackageUrl        string `json:"packageUrl"`
 	RemoteManifestUrl string `json:"remoteManifestUrl"`
 	RemoteVersionUrl  string `json:"remoteVersionUrl"`
+	Status            int16  `json:"status"`
+}
+
+func (this *TGetVersionRsp) FromMVersion(mVersion *hotupdate.MVersionInfo) {
+	this.ID = mVersion.ID.Hex()
+	this.CreateTimeSec = mVersion.CreatedAt.Unix()
+	this.UpdateTimeSec = mVersion.UpdatedAt.Unix()
+	this.Version = mVersion.Version
+	this.PackageUrl = mVersion.PackageUrl
+	this.RemoteVersionUrl = mVersion.RemoteVersionUrl
+	this.RemoteManifestUrl = mVersion.RemoteManifestUrl
+	this.Status = mVersion.Status
+	this.Proj = mVersion.Proj
+	this.Os = mVersion.Os
 }
 
 type TAddVersionReq struct {
@@ -26,4 +51,16 @@ type TAddVersionReq struct {
 	PackageUrl        string `form:"packageUrl" binding:"required" json:"packageUrl"`
 	RemoteManifestUrl string `form:"remoteManifestUrl" binding:"required" json:"remoteManifestUrl"`
 	RemoteVersionUrl  string `form:"remoteVersionUrl" binding:"required" json:"remoteVersionUrl"`
+	TimeSec           int64  `form:"timesec" json:"timesec"`
+	Sign              string `form:"sign" json:"sign"`
+}
+
+type TGetVersionListReq struct {
+	Start int64 `form:"start" binding:"required" json:"start"` // Start是闭区间 包含的,列如去前5条 start=1,limit=5
+	Limit int64 `form:"limit" binding:"required" json:"limit"`
+}
+
+type TChangeStautsReq struct {
+	ID     string `form:"id" binding:"required" json:"id"`
+	Status int16  `form:"status" binding:"required" json:"status"`
 }

+ 306 - 0
middleware/whiteip/whiteipmgr.go

@@ -0,0 +1,306 @@
+/**
+ * @author chengliang
+ * @date 2026/1/27 11:18
+ * @brief
+ *
+ **/
+
+package whiteip
+
+import (
+	"fmt"
+	//"github.com/dlclark/regexp2" // 支持更复杂的正则表达式
+	"github.com/gin-gonic/gin"
+	"net"
+	"regexp"
+	"strings"
+)
+
+// 支持正则的IP白名单中间件
+type TWhiteIpMgr struct {
+	// 预编译的正则表达式列表
+	ipPatterns []*regexp.Regexp
+	// 是否允许内网IP(默认允许)
+	allowInternal bool
+	// 是否启用IP检查(默认启用)
+	enabled bool
+}
+
+// 创建白名单中间件
+func NewIPWhiteListMiddleware(patterns []string, allowInternal bool) *TWhiteIpMgr {
+	m := &TWhiteIpMgr{
+		allowInternal: allowInternal,
+		enabled:       true,
+	}
+
+	// 编译正则表达式
+	for _, pattern := range patterns {
+		// 支持简写语法
+		compiledPattern := normalizePattern(pattern)
+
+		re, err := regexp.Compile(compiledPattern)
+		if err != nil {
+			// 如果编译失败,记录错误但仍继续
+			fmt.Printf("warning:IP white list compile failed: %s, error: %v\n", pattern, err)
+			continue
+		}
+
+		m.ipPatterns = append(m.ipPatterns, re)
+	}
+
+	return m
+}
+
+func (this *TWhiteIpMgr) UpdatePattern(patterns []string, allowInternal bool, enabled bool) {
+	this.enabled = enabled
+	this.allowInternal = allowInternal
+	this.ipPatterns = make([]*regexp.Regexp, len(patterns))
+	for _, pattern := range patterns {
+		compiledPattern := normalizePattern(pattern)
+
+		re, err := regexp.Compile(compiledPattern)
+		if err != nil {
+			// 如果编译失败,记录错误但仍继续
+			fmt.Printf("warning:IP white list compile failed: %s, error: %v\n", pattern, err)
+			continue
+		}
+
+		this.ipPatterns = append(this.ipPatterns, re)
+	}
+
+}
+
+// 标准化IP模式
+func normalizePattern(pattern string) string {
+	// 支持CIDR表示法转换成正则表达式
+	if strings.Contains(pattern, "/") {
+		return cidrToRegex(pattern)
+	}
+
+	// 支持IP段简写,如: 192.168.1.* 或 192.168.1.1-100
+	pattern = strings.ReplaceAll(pattern, "*", `\d+`)
+
+	// 支持IP范围,如: 192.168.1.1-100
+	if strings.Contains(pattern, "-") {
+		parts := strings.Split(pattern, ".")
+		for i, part := range parts {
+			if strings.Contains(part, "-") {
+				rangeParts := strings.Split(part, "-")
+				if len(rangeParts) == 2 {
+					parts[i] = fmt.Sprintf(`(%s)`, buildRangeRegex(rangeParts[0], rangeParts[1]))
+				}
+			}
+		}
+		return "^" + strings.Join(parts, `\.`) + "$"
+	}
+
+	// 如果是普通IP,确保完全匹配
+	if !strings.HasPrefix(pattern, "^") {
+		pattern = "^" + pattern
+	}
+	if !strings.HasSuffix(pattern, "$") {
+		pattern = pattern + "$"
+	}
+
+	return pattern
+}
+
+// cidrToRegex 将CIDR表示法转换为正则表达式
+func cidrToRegex(cidr string) string {
+	ip, ipNet, err := net.ParseCIDR(cidr)
+	if err != nil {
+		return cidr // 如果解析失败,返回原字符串
+	}
+
+	// 获取网络掩码
+	mask := ipNet.Mask
+	ones, bits := mask.Size()
+
+	if bits != 32 {
+		return cidr // 仅支持IPv4
+	}
+
+	// 将IP转换为整数
+	ipInt := ipToInt(ip.To4())
+
+	// 计算网络地址和广播地址
+	network := ipInt & (^uint32(0) << uint32(bits-ones))
+	broadcast := network | (^uint32(0) >> uint32(ones))
+
+	// 生成正则表达式
+	return fmt.Sprintf(`^%s$`, ipRangeToRegex(intToIP(network), intToIP(broadcast)))
+}
+
+// ipToInt IP转整数
+func ipToInt(ip net.IP) uint32 {
+	if len(ip) == 16 {
+		return uint32(ip[12])<<24 | uint32(ip[13])<<16 | uint32(ip[14])<<8 | uint32(ip[15])
+	}
+	return uint32(ip[0])<<24 | uint32(ip[1])<<16 | uint32(ip[2])<<8 | uint32(ip[3])
+}
+
+// intToIP 整数转IP
+func intToIP(n uint32) net.IP {
+	return net.IPv4(byte(n>>24), byte(n>>16), byte(n>>8), byte(n))
+}
+
+// ipRangeToRegex IP范围转正则表达式
+func ipRangeToRegex(start, end net.IP) string {
+	startParts := strings.Split(start.String(), ".")
+	endParts := strings.Split(end.String(), ".")
+
+	var regexParts []string
+	for i := 0; i < 4; i++ {
+		startNum := parseInt(startParts[i])
+		endNum := parseInt(endParts[i])
+
+		if startNum == endNum {
+			regexParts = append(regexParts, fmt.Sprintf("%d", startNum))
+		} else {
+			regexParts = append(regexParts, fmt.Sprintf("(%s)", buildRangeRegex(startParts[i], endParts[i])))
+		}
+	}
+
+	return strings.Join(regexParts, `\.`)
+}
+
+// buildRangeRegex 构建数字范围的正则表达式
+func buildRangeRegex(start, end string) string {
+	startNum := parseInt(start)
+	endNum := parseInt(end)
+
+	if startNum == endNum {
+		return start
+	}
+
+	// 简单处理:如果范围小,直接列举
+	if endNum-startNum < 10 {
+		var options []string
+		for i := startNum; i <= endNum; i++ {
+			options = append(options, fmt.Sprintf("%d", i))
+		}
+		return strings.Join(options, "|")
+	}
+
+	// 复杂范围,使用正则表达式模式
+	return fmt.Sprintf("%d|%d|[1-9]\\d{0,2}", startNum, endNum) // 简化处理
+}
+
+// parseInt 字符串转整数
+func parseInt(s string) int {
+	var result int
+	fmt.Sscanf(s, "%d", &result)
+	return result
+}
+
+// 检查是否为内网IP
+func isInternalIP(ipStr string) bool {
+	ip := net.ParseIP(ipStr)
+	if ip == nil {
+		return false
+	}
+
+	// IPv4 检查
+	if ip4 := ip.To4(); ip4 != nil {
+		return ip4[0] == 10 ||
+			(ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31) ||
+			(ip4[0] == 192 && ip4[1] == 168) ||
+			ip4[0] == 127
+	}
+
+	// IPv6 检查
+	return ip.IsLoopback() || ip.IsPrivate()
+}
+
+// GetClientIP 获取客户端真实IP
+func GetClientIP(c *gin.Context) string {
+	// 从代理头获取
+	if ip := c.GetHeader("X-Forwarded-For"); ip != "" {
+		ips := strings.Split(ip, ",")
+		if len(ips) > 0 {
+			return strings.TrimSpace(ips[0])
+		}
+	}
+
+	if ip := c.GetHeader("X-Real-IP"); ip != "" {
+		return ip
+	}
+
+	// 直接获取 RemoteAddr
+	remoteAddr := c.Request.RemoteAddr
+	if ip, _, err := net.SplitHostPort(remoteAddr); err == nil {
+		return ip
+	}
+	return remoteAddr
+}
+
+// Middleware 返回Gin中间件函数
+func (m *TWhiteIpMgr) Middleware() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		if !m.enabled {
+			c.Next()
+			return
+		}
+
+		clientIP := GetClientIP(c)
+
+		// 检查内网IP
+		if m.allowInternal && isInternalIP(clientIP) {
+			c.Next()
+			return
+		}
+
+		// 检查白名单
+		if m.isIPAllowed(clientIP) {
+			c.Next()
+			return
+		}
+
+		// 拒绝访问
+		c.JSON(403, gin.H{
+			"code":    403,
+			"message": fmt.Sprintf("IP %s 不在白名单中", clientIP),
+			"data":    nil,
+		})
+		c.Abort()
+	}
+}
+
+// isIPAllowed 检查IP是否被允许
+func (m *TWhiteIpMgr) isIPAllowed(ip string) bool {
+	// 如果没有设置任何模式,拒绝所有(除非是内网)
+	if len(m.ipPatterns) == 0 {
+		return false
+	}
+
+	for _, pattern := range m.ipPatterns {
+		if pattern.MatchString(ip) {
+			return true
+		}
+	}
+
+	return false
+}
+
+// Enable 启用中间件
+func (m *TWhiteIpMgr) Enable() {
+	m.enabled = true
+}
+
+// Disable 禁用中间件
+func (m *TWhiteIpMgr) Disable() {
+	m.enabled = false
+}
+
+// AddPattern 动态添加IP模式
+func (m *TWhiteIpMgr) AddPattern(pattern string) error {
+	compiledPattern := normalizePattern(pattern)
+
+	re, err := regexp.Compile(compiledPattern)
+	if err != nil {
+		return err
+	}
+
+	m.ipPatterns = append(m.ipPatterns, re)
+	return nil
+}

+ 3 - 3
model/mongo/hotupdate/hotupdate.go

@@ -112,7 +112,7 @@ func (s *TVersionDB) Find(filter bson.M, opts ...*options.FindOptions) ([]MVersi
 }
 
 // ListByProjectAndOS 根据项目和系统列出版本
-func (s *TVersionDB) ListByProjectAndOS(project, os string, page, size int64) ([]MVersionInfo, int64, error) {
+func (s *TVersionDB) ListByProjectAndOS(project, os string, start, limit int64) ([]MVersionInfo, int64, error) {
 	filter := bson.M{"project": project}
 	if os != "" {
 		filter["os"] = os
@@ -130,8 +130,8 @@ func (s *TVersionDB) ListByProjectAndOS(project, os string, page, size int64) ([
 	// 分页查询
 	opts := options.Find().
 		SetSort(bson.M{"created_at": -1}). // 按版本降序
-		SetSkip((page - 1) * size).
-		SetLimit(size)
+		SetSkip(start).
+		SetLimit(limit)
 
 	results, err := s.Find(filter, opts)
 	if err != nil {

+ 8 - 2
model/mongo/hotupdate/type.go

@@ -9,16 +9,22 @@ package hotupdate
 
 import "github.com/kamva/mgm/v3"
 
-// MVersionInfo
+const (
+	DEF_VER_STATUS_INVALID int16 = -1
+	DEF_VER_STATUS_NONE    int16 = 0 // 默认不生效
+	DEF_VER_STATUS_PUBLIC  int16 = 1 // 对外
+)
+
+// mongo model
 type MVersionInfo struct {
 	mgm.DefaultModel  `bson:",inline"`
 	Proj              string `json:"project" bson:"project"`
 	Os                string `json:"os" bson:"os"`
+	Status            int16  `json:"status" bson:"status"`
 	Version           string `json:"version" bson:"version"`
 	PackageUrl        string `json:"packageUrl" bson:"packageUrl"`
 	RemoteManifestUrl string `json:"remoteManifestUrl" bson:"remoteManifestUrl"`
 	RemoteVersionUrl  string `json:"remoteVersionUrl" bson:"remoteVersionUrl"`
-	Status            string `json:"status" bson:"status"`
 }
 
 // 定义集合名称

+ 17 - 0
model/mongo/ipwhite/ipwhite.go

@@ -0,0 +1,17 @@
+/**
+ * @author chengliang
+ * @date 2026/1/27 11:38
+ * @brief
+ *
+ **/
+
+package ipwhite
+
+import "github.com/kamva/mgm/v3"
+
+type TIpWhiteList struct {
+	mgm.DefaultModel `bson:",inline"`
+	Enabled          bool           `json:"enabled" bson:"enabled"`             // 白名单是否开启
+	AllowInternal    bool           `json:"allowInternal" bson:"allowInternal"` // 是否允许内网
+	IpPatternsMap    map[string]int `json:"ipPatternsMap" bson:"ipPatternsMap"` // ip 列表 支持正则
+}

+ 50 - 0
tool/signweb.go

@@ -0,0 +1,50 @@
+/**
+ * @author chengliang
+ * @date 2026/1/23 18:38
+ * @brief
+ *
+ **/
+
+package tool
+
+import (
+	"crypto/md5"
+	"encoding/hex"
+	"fmt"
+	"hash"
+	"sort"
+	"strings"
+	"sync"
+)
+
+const KEY_SECRET = "secret"
+
+var md5Pool = sync.Pool{
+	New: func() interface{} {
+		return md5.New()
+	},
+}
+
+func GenSign(params map[string]any) string {
+	var keys []string
+	for k := range params {
+		keys = append(keys, k)
+	}
+	sort.Strings(keys)
+	fmt.Println("keys:", keys)
+	var paramParts []string
+	for _, k := range keys {
+		paramParts = append(paramParts, fmt.Sprintf("%s=%s", k, params[k]))
+	}
+	paramStr := strings.Join(paramParts, "&")
+	paramStr = fmt.Sprintf("%s&%s", paramStr, KEY_SECRET)
+
+	md5Obj := md5Pool.Get().(hash.Hash)
+	defer md5Pool.Put(md5Obj)
+
+	md5Obj.Reset()
+	md5Obj.Write([]byte(paramStr))
+	md5ObjSum := md5Obj.Sum(nil)
+
+	return hex.EncodeToString(md5ObjSum)
+}