feat: 实现客户端实例上报功能
- 新增 ClientInstance 模型,记录客户端 IP 与配置项的关联 - 配置拉取接口(fetchAll/fetchChanged)自动记录客户端信息 - 配置列表页面展示每个配置的在线客户端数量 - 点击可查看具体 IP 地址、拉取方式、最后拉取时间 - 超过 5 分钟未拉取的记录自动清理 - 注释掉 /api/ping 接口
This commit is contained in:
+5
-1
@@ -42,6 +42,7 @@ func main() {
|
|||||||
userHandler := handler.NewUserHandler(permService)
|
userHandler := handler.NewUserHandler(permService)
|
||||||
groupHandler := handler.NewGroupHandler()
|
groupHandler := handler.NewGroupHandler()
|
||||||
profileHandler := handler.NewProfileHandler()
|
profileHandler := handler.NewProfileHandler()
|
||||||
|
clientHandler := handler.NewClientHandler()
|
||||||
|
|
||||||
// init gin
|
// init gin
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
@@ -101,6 +102,9 @@ func main() {
|
|||||||
admin.POST("/profile/delete/:id", profileHandler.Delete)
|
admin.POST("/profile/delete/:id", profileHandler.Delete)
|
||||||
admin.POST("/profile/switch/:id", profileHandler.Switch)
|
admin.POST("/profile/switch/:id", profileHandler.Switch)
|
||||||
admin.GET("/profile/options", profileHandler.Options)
|
admin.GET("/profile/options", profileHandler.Options)
|
||||||
|
|
||||||
|
// client instance management
|
||||||
|
admin.GET("/client/listByConfig", clientHandler.ListByConfigId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// user info routes (JWT auth required)
|
// user info routes (JWT auth required)
|
||||||
@@ -116,7 +120,7 @@ func main() {
|
|||||||
{
|
{
|
||||||
api.POST("/fetchAllConfigs", configApiHandler.FetchAllConfigs)
|
api.POST("/fetchAllConfigs", configApiHandler.FetchAllConfigs)
|
||||||
api.POST("/fetchChangedConfigs", configApiHandler.FetchChangedConfigs)
|
api.POST("/fetchChangedConfigs", configApiHandler.FetchChangedConfigs)
|
||||||
api.GET("/ping", configApiHandler.Ping)
|
// api.GET("/ping", configApiHandler.Ping)
|
||||||
}
|
}
|
||||||
|
|
||||||
// start server
|
// start server
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ func Init() error {
|
|||||||
new(model.AppConfigHistory),
|
new(model.AppConfigHistory),
|
||||||
new(model.User),
|
new(model.User),
|
||||||
new(model.UserPermission),
|
new(model.UserPermission),
|
||||||
|
new(model.ClientInstance),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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})
|
||||||
|
}
|
||||||
@@ -80,9 +80,31 @@ func (h *ConfigHandler) List(c *gin.Context) {
|
|||||||
total, _ := dao.Engine.Where(whereClause).Count(&model.AppConfig{})
|
total, _ := dao.Engine.Where(whereClause).Count(&model.AppConfig{})
|
||||||
dao.Engine.Where(whereClause).Limit(req.PageSize, (req.PageNo-1)*req.PageSize).Find(&configs)
|
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 {
|
for i := range configs {
|
||||||
configs[i].AppNames = h.buildAppNames(&configs[i])
|
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})
|
c.JSON(http.StatusOK, model.PageResult{PageNo: req.PageNo, PageSize: req.PageSize, Total: total, Data: configs})
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ func (h *ConfigApiHandler) FetchAllConfigs(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
result := make(map[string]interface{})
|
result := make(map[string]interface{})
|
||||||
|
var configIds []int
|
||||||
|
|
||||||
// load global configs
|
// load global configs
|
||||||
if !req.IgnoreGlobal && app.GroupId != nil {
|
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)",
|
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)
|
req.Env, version, *app.GroupId).Find(&globalConfigs)
|
||||||
for _, cfg := range globalConfigs {
|
for _, cfg := range globalConfigs {
|
||||||
|
configIds = append(configIds, cfg.Id)
|
||||||
kvMap := h.parseService.ParseConfigToKVMap(&cfg)
|
kvMap := h.parseService.ParseConfigToKVMap(&cfg)
|
||||||
for k, v := range kvMap {
|
for k, v := range kvMap {
|
||||||
result[k] = v
|
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 = ?",
|
dao.Engine.Where("env = ? AND version = ? AND enabled = 1 AND app_id = ?",
|
||||||
req.Env, version, *app.ParentId).Find(&parentConfigs)
|
req.Env, version, *app.ParentId).Find(&parentConfigs)
|
||||||
for _, cfg := range parentConfigs {
|
for _, cfg := range parentConfigs {
|
||||||
|
configIds = append(configIds, cfg.Id)
|
||||||
kvMap := h.parseService.ParseConfigToKVMap(&cfg)
|
kvMap := h.parseService.ParseConfigToKVMap(&cfg)
|
||||||
for k, v := range kvMap {
|
for k, v := range kvMap {
|
||||||
result[k] = v
|
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 = ?",
|
dao.Engine.Where("env = ? AND version = ? AND enabled = 1 AND app_id = ?",
|
||||||
req.Env, version, app.Id).Find(&appConfigs)
|
req.Env, version, app.Id).Find(&appConfigs)
|
||||||
for _, cfg := range appConfigs {
|
for _, cfg := range appConfigs {
|
||||||
|
configIds = append(configIds, cfg.Id)
|
||||||
kvMap := h.parseService.ParseConfigToKVMap(&cfg)
|
kvMap := h.parseService.ParseConfigToKVMap(&cfg)
|
||||||
for k, v := range kvMap {
|
for k, v := range kvMap {
|
||||||
result[k] = v
|
result[k] = v
|
||||||
@@ -115,6 +119,9 @@ func (h *ConfigApiHandler) FetchAllConfigs(c *gin.Context) {
|
|||||||
|
|
||||||
result["latest.fetch.timestamp"] = strconv.FormatInt(time.Now().UnixMilli(), 10)
|
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)
|
c.JSON(http.StatusOK, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,6 +163,7 @@ func (h *ConfigApiHandler) FetchChangedConfigs(c *gin.Context) {
|
|||||||
lastUpdateTime := time.UnixMilli(req.LastTime)
|
lastUpdateTime := time.UnixMilli(req.LastTime)
|
||||||
|
|
||||||
result := make(map[string]string)
|
result := make(map[string]string)
|
||||||
|
var configIds []int
|
||||||
|
|
||||||
// load global changed configs
|
// load global changed configs
|
||||||
if !req.IgnoreGlobal && app.GroupId != nil {
|
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 > ?",
|
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)
|
req.Env, version, *app.GroupId, lastUpdateTime).Find(&globalConfigs)
|
||||||
for _, cfg := range globalConfigs {
|
for _, cfg := range globalConfigs {
|
||||||
|
configIds = append(configIds, cfg.Id)
|
||||||
h.addChangedConfigs(result, &cfg, req.FetchAll)
|
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 > ?",
|
dao.Engine.Where("env = ? AND version = ? AND enabled = 1 AND app_id = ? AND updated_at > ?",
|
||||||
req.Env, version, *app.ParentId, lastUpdateTime).Find(&parentConfigs)
|
req.Env, version, *app.ParentId, lastUpdateTime).Find(&parentConfigs)
|
||||||
for _, cfg := range parentConfigs {
|
for _, cfg := range parentConfigs {
|
||||||
|
configIds = append(configIds, cfg.Id)
|
||||||
h.addChangedConfigs(result, &cfg, req.FetchAll)
|
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 > ?",
|
dao.Engine.Where("env = ? AND version = ? AND enabled = 1 AND app_id = ? AND updated_at > ?",
|
||||||
req.Env, version, app.Id, lastUpdateTime).Find(&appConfigs)
|
req.Env, version, app.Id, lastUpdateTime).Find(&appConfigs)
|
||||||
for _, cfg := range appConfigs {
|
for _, cfg := range appConfigs {
|
||||||
|
configIds = append(configIds, cfg.Id)
|
||||||
h.addChangedConfigs(result, &cfg, req.FetchAll)
|
h.addChangedConfigs(result, &cfg, req.FetchAll)
|
||||||
}
|
}
|
||||||
|
|
||||||
result["latest.fetch.timestamp"] = strconv.FormatInt(time.Now().UnixMilli(), 10)
|
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})
|
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) {
|
func (h *ConfigApiHandler) recordClientInstances(ip string, configIds []int, appName, env, fetchType string) {
|
||||||
c.String(http.StatusOK, "200")
|
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")
|
||||||
|
// }
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type AppConfig struct {
|
|||||||
UpdatedBy string `xorm:"varchar(32)" json:"updatedBy"`
|
UpdatedBy string `xorm:"varchar(32)" json:"updatedBy"`
|
||||||
Global bool `xorm:"-" json:"global"`
|
Global bool `xorm:"-" json:"global"`
|
||||||
AppNames string `xorm:"-" json:"appNames,omitempty"`
|
AppNames string `xorm:"-" json:"appNames,omitempty"`
|
||||||
|
ClientCount int `xorm:"-" json:"clientCount,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppConfigHistory struct {
|
type AppConfigHistory struct {
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import request from './request'
|
||||||
|
|
||||||
|
export function getClientListByConfig(configId) {
|
||||||
|
return request({
|
||||||
|
url: '/admin/client/listByConfig',
|
||||||
|
method: 'get',
|
||||||
|
params: { configId }
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -25,6 +25,14 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="version" label="版本" width="100"></el-table-column>
|
<el-table-column prop="version" label="版本" width="100"></el-table-column>
|
||||||
|
<el-table-column prop="clientCount" label="在线客户端" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button v-if="scope.row.clientCount > 0" type="text" @click="showClients(scope.row)">
|
||||||
|
{{ scope.row.clientCount }} 个
|
||||||
|
</el-button>
|
||||||
|
<span v-else style="color: #909399">无</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="200">
|
<el-table-column label="操作" width="200">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-button type="text" @click="showHistory(scope.row)">变更历史</el-button>
|
<el-button type="text" @click="showHistory(scope.row)">变更历史</el-button>
|
||||||
@@ -92,11 +100,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog title="在线客户端" :visible.sync="clientVisible" width="700px">
|
||||||
|
<el-table :data="clientList" border stripe>
|
||||||
|
<el-table-column prop="ip" label="IP 地址" width="160"></el-table-column>
|
||||||
|
<el-table-column prop="lastFetchType" label="拉取方式" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span v-if="scope.row.lastFetchType === 'fetchAll'">全量拉取</span>
|
||||||
|
<span v-else>增量拉取</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="最后拉取时间">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ formatTime(scope.row.lastFetchAt) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-dialog>
|
||||||
</el-card>
|
</el-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getConfigList, deleteConfig, getConfigHistory, rollbackConfig, getConfig } from '../../api/config'
|
import { getConfigList, deleteConfig, getConfigHistory, rollbackConfig, getConfig } from '../../api/config'
|
||||||
|
import { getClientListByConfig } from '../../api/client'
|
||||||
import request from '../../api/request'
|
import request from '../../api/request'
|
||||||
|
|
||||||
function computeDiff(oldText, newText) {
|
function computeDiff(oldText, newText) {
|
||||||
@@ -204,7 +230,9 @@ export default {
|
|||||||
contentVisible: false,
|
contentVisible: false,
|
||||||
currentHistory: {},
|
currentHistory: {},
|
||||||
leftLines: [],
|
leftLines: [],
|
||||||
rightLines: []
|
rightLines: [],
|
||||||
|
clientVisible: false,
|
||||||
|
clientList: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
@@ -305,6 +333,13 @@ export default {
|
|||||||
this.historyVisible = false
|
this.historyVisible = false
|
||||||
this.loadData()
|
this.loadData()
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
},
|
||||||
|
async showClients(row) {
|
||||||
|
try {
|
||||||
|
const res = await getClientListByConfig(row.id)
|
||||||
|
this.clientList = res.data || []
|
||||||
|
this.clientVisible = true
|
||||||
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user