diff --git a/server/cmd/main.go b/server/cmd/main.go index d6e432a..807df8f 100644 --- a/server/cmd/main.go +++ b/server/cmd/main.go @@ -42,6 +42,7 @@ func main() { userHandler := handler.NewUserHandler(permService) groupHandler := handler.NewGroupHandler() profileHandler := handler.NewProfileHandler() + clientHandler := handler.NewClientHandler() // init gin r := gin.Default() @@ -101,6 +102,9 @@ func main() { admin.POST("/profile/delete/:id", profileHandler.Delete) admin.POST("/profile/switch/:id", profileHandler.Switch) admin.GET("/profile/options", profileHandler.Options) + + // client instance management + admin.GET("/client/listByConfig", clientHandler.ListByConfigId) } // user info routes (JWT auth required) @@ -116,7 +120,7 @@ func main() { { api.POST("/fetchAllConfigs", configApiHandler.FetchAllConfigs) api.POST("/fetchChangedConfigs", configApiHandler.FetchChangedConfigs) - api.GET("/ping", configApiHandler.Ping) + // api.GET("/ping", configApiHandler.Ping) } // start server diff --git a/server/internal/dao/engine.go b/server/internal/dao/engine.go index aba7001..99d5459 100644 --- a/server/internal/dao/engine.go +++ b/server/internal/dao/engine.go @@ -33,6 +33,7 @@ func Init() error { new(model.AppConfigHistory), new(model.User), new(model.UserPermission), + new(model.ClientInstance), ); err != nil { return err } diff --git a/server/internal/handler/client.go b/server/internal/handler/client.go new file mode 100644 index 0000000..71ae5b5 --- /dev/null +++ b/server/internal/handler/client.go @@ -0,0 +1,29 @@ +package handler + +import ( + "kenaito-conf/server/internal/dao" + "kenaito-conf/server/internal/model" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" +) + +type ClientHandler struct{} + +func NewClientHandler() *ClientHandler { + return &ClientHandler{} +} + +func (h *ClientHandler) ListByConfigId(c *gin.Context) { + configId, _ := strconv.Atoi(c.Query("configId")) + if configId == 0 { + c.JSON(http.StatusBadRequest, model.WrapperResponse{Code: 400, Msg: "参数错误"}) + return + } + + var instances []model.ClientInstance + dao.Engine.Where("app_config_id = ?", configId).OrderBy("last_fetch_at DESC").Find(&instances) + + c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: instances}) +} diff --git a/server/internal/handler/config.go b/server/internal/handler/config.go index cf41078..c3c48f9 100644 --- a/server/internal/handler/config.go +++ b/server/internal/handler/config.go @@ -80,9 +80,31 @@ func (h *ConfigHandler) List(c *gin.Context) { total, _ := dao.Engine.Where(whereClause).Count(&model.AppConfig{}) dao.Engine.Where(whereClause).Limit(req.PageSize, (req.PageNo-1)*req.PageSize).Find(&configs) - // build app names + // build app names and client counts + configIds := make([]int, 0, len(configs)) for i := range configs { configs[i].AppNames = h.buildAppNames(&configs[i]) + configIds = append(configIds, configs[i].Id) + } + // batch query client counts + if len(configIds) > 0 { + type countResult struct { + AppConfigId int `xorm:"app_config_id"` + Cnt int `xorm:"cnt"` + } + var results []countResult + dao.Engine.Table("client_instance"). + Select("app_config_id, COUNT(DISTINCT ip) as cnt"). + In("app_config_id", configIds). + GroupBy("app_config_id"). + Find(&results) + countMap := make(map[int]int, len(results)) + for _, r := range results { + countMap[r.AppConfigId] = r.Cnt + } + for i := range configs { + configs[i].ClientCount = countMap[configs[i].Id] + } } c.JSON(http.StatusOK, model.PageResult{PageNo: req.PageNo, PageSize: req.PageSize, Total: total, Data: configs}) diff --git a/server/internal/handler/config_api.go b/server/internal/handler/config_api.go index 7a383a5..a8f0994 100644 --- a/server/internal/handler/config_api.go +++ b/server/internal/handler/config_api.go @@ -70,6 +70,7 @@ func (h *ConfigApiHandler) FetchAllConfigs(c *gin.Context) { } result := make(map[string]interface{}) + var configIds []int // load global configs if !req.IgnoreGlobal && app.GroupId != nil { @@ -77,6 +78,7 @@ func (h *ConfigApiHandler) FetchAllConfigs(c *gin.Context) { dao.Engine.Where("env = ? AND version = ? AND enabled = 1 AND group_id = ? AND (app_id is null OR app_id = 0)", req.Env, version, *app.GroupId).Find(&globalConfigs) for _, cfg := range globalConfigs { + configIds = append(configIds, cfg.Id) kvMap := h.parseService.ParseConfigToKVMap(&cfg) for k, v := range kvMap { result[k] = v @@ -90,6 +92,7 @@ func (h *ConfigApiHandler) FetchAllConfigs(c *gin.Context) { dao.Engine.Where("env = ? AND version = ? AND enabled = 1 AND app_id = ?", req.Env, version, *app.ParentId).Find(&parentConfigs) for _, cfg := range parentConfigs { + configIds = append(configIds, cfg.Id) kvMap := h.parseService.ParseConfigToKVMap(&cfg) for k, v := range kvMap { result[k] = v @@ -102,6 +105,7 @@ func (h *ConfigApiHandler) FetchAllConfigs(c *gin.Context) { dao.Engine.Where("env = ? AND version = ? AND enabled = 1 AND app_id = ?", req.Env, version, app.Id).Find(&appConfigs) for _, cfg := range appConfigs { + configIds = append(configIds, cfg.Id) kvMap := h.parseService.ParseConfigToKVMap(&cfg) for k, v := range kvMap { result[k] = v @@ -115,6 +119,9 @@ func (h *ConfigApiHandler) FetchAllConfigs(c *gin.Context) { result["latest.fetch.timestamp"] = strconv.FormatInt(time.Now().UnixMilli(), 10) + // record client instances asynchronously + go h.recordClientInstances(c.ClientIP(), configIds, req.AppName, req.Env, "fetchAll") + c.JSON(http.StatusOK, result) } @@ -156,6 +163,7 @@ func (h *ConfigApiHandler) FetchChangedConfigs(c *gin.Context) { lastUpdateTime := time.UnixMilli(req.LastTime) result := make(map[string]string) + var configIds []int // load global changed configs if !req.IgnoreGlobal && app.GroupId != nil { @@ -163,6 +171,7 @@ func (h *ConfigApiHandler) FetchChangedConfigs(c *gin.Context) { dao.Engine.Where("env = ? AND version = ? AND enabled = 1 AND group_id = ? AND (app_id is null OR app_id = 0) AND updated_at > ?", req.Env, version, *app.GroupId, lastUpdateTime).Find(&globalConfigs) for _, cfg := range globalConfigs { + configIds = append(configIds, cfg.Id) h.addChangedConfigs(result, &cfg, req.FetchAll) } } @@ -173,6 +182,7 @@ func (h *ConfigApiHandler) FetchChangedConfigs(c *gin.Context) { dao.Engine.Where("env = ? AND version = ? AND enabled = 1 AND app_id = ? AND updated_at > ?", req.Env, version, *app.ParentId, lastUpdateTime).Find(&parentConfigs) for _, cfg := range parentConfigs { + configIds = append(configIds, cfg.Id) h.addChangedConfigs(result, &cfg, req.FetchAll) } } @@ -182,11 +192,15 @@ func (h *ConfigApiHandler) FetchChangedConfigs(c *gin.Context) { dao.Engine.Where("env = ? AND version = ? AND enabled = 1 AND app_id = ? AND updated_at > ?", req.Env, version, app.Id, lastUpdateTime).Find(&appConfigs) for _, cfg := range appConfigs { + configIds = append(configIds, cfg.Id) h.addChangedConfigs(result, &cfg, req.FetchAll) } result["latest.fetch.timestamp"] = strconv.FormatInt(time.Now().UnixMilli(), 10) + // record client instances asynchronously + go h.recordClientInstances(c.ClientIP(), configIds, req.AppName, req.Env, "fetchChanged") + c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: result}) } @@ -197,6 +211,38 @@ func (h *ConfigApiHandler) addChangedConfigs(result map[string]string, config *m } } -func (h *ConfigApiHandler) Ping(c *gin.Context) { - c.String(http.StatusOK, "200") +func (h *ConfigApiHandler) recordClientInstances(ip string, configIds []int, appName, env, fetchType string) { + if len(configIds) == 0 || ip == "" { + return + } + now := time.Now() + for _, configId := range configIds { + var existing model.ClientInstance + has, _ := dao.Engine.Where("app_config_id = ? AND ip = ?", configId, ip).Get(&existing) + if has { + existing.LastFetchAt = &now + existing.LastFetchType = fetchType + existing.UpdatedAt = &now + dao.Engine.ID(existing.Id).Cols("last_fetch_at", "last_fetch_type", "updated_at").Update(&existing) + } else { + instance := model.ClientInstance{ + AppConfigId: configId, + AppName: appName, + Env: env, + IP: ip, + LastFetchAt: &now, + LastFetchType: fetchType, + CreatedAt: &now, + UpdatedAt: &now, + } + dao.Engine.Insert(&instance) + } + } + // cleanup records older than 5 minutes + fiveMinutesAgo := now.Add(-5 * time.Minute) + dao.Engine.Where("last_fetch_at < ?", fiveMinutesAgo).Delete(&model.ClientInstance{}) } + +// func (h *ConfigApiHandler) Ping(c *gin.Context) { +// c.String(http.StatusOK, "200") +// } diff --git a/server/internal/model/app_config.go b/server/internal/model/app_config.go index b0317bd..0b20327 100644 --- a/server/internal/model/app_config.go +++ b/server/internal/model/app_config.go @@ -19,6 +19,7 @@ type AppConfig struct { UpdatedBy string `xorm:"varchar(32)" json:"updatedBy"` Global bool `xorm:"-" json:"global"` AppNames string `xorm:"-" json:"appNames,omitempty"` + ClientCount int `xorm:"-" json:"clientCount,omitempty"` } type AppConfigHistory struct { diff --git a/server/internal/model/client_instance.go b/server/internal/model/client_instance.go new file mode 100644 index 0000000..46c3ef6 --- /dev/null +++ b/server/internal/model/client_instance.go @@ -0,0 +1,15 @@ +package model + +import "time" + +type ClientInstance struct { + Id int `xorm:"int autoincr pk" json:"id"` + AppConfigId int `xorm:"int index" json:"appConfigId"` + AppName string `xorm:"varchar(64) index" json:"appName"` + Env string `xorm:"varchar(32)" json:"env"` + IP string `xorm:"varchar(64)" json:"ip"` + LastFetchAt *time.Time `xorm:"datetime" json:"lastFetchAt"` + LastFetchType string `xorm:"varchar(16)" json:"lastFetchType"` + CreatedAt *time.Time `xorm:"datetime" json:"createdAt"` + UpdatedAt *time.Time `xorm:"datetime" json:"updatedAt"` +} diff --git a/web/src/api/client.js b/web/src/api/client.js new file mode 100644 index 0000000..2860dd0 --- /dev/null +++ b/web/src/api/client.js @@ -0,0 +1,9 @@ +import request from './request' + +export function getClientListByConfig(configId) { + return request({ + url: '/admin/client/listByConfig', + method: 'get', + params: { configId } + }) +} diff --git a/web/src/views/config/ConfigList.vue b/web/src/views/config/ConfigList.vue index 7c9475d..200c0ca 100644 --- a/web/src/views/config/ConfigList.vue +++ b/web/src/views/config/ConfigList.vue @@ -25,6 +25,14 @@ + + +