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 @@
+
+
+
+ {{ scope.row.clientCount }} 个
+
+ 无
+
+
变更历史
@@ -92,11 +100,29 @@
+
+
+
+
+
+
+ 全量拉取
+ 增量拉取
+
+
+
+
+ {{ formatTime(scope.row.lastFetchAt) }}
+
+
+
+