feat(config): 增强配置历史管理并统一数据库时间

- 新增 dao.Now() 函数获取数据库毫秒精度时间,避免多实例时钟偏差
- 增量同步查询增加上界(updated_at <= upperBound)防止竞态条件
- 配置创建、更新、删除、回滚统一使用数据库时间替代 time.Now()
- 配置历史新增 operate 字段记录操作类型(update/delete/rollback)
- 删除配置和回滚操作前增加历史快照记录
- 新增 DeleteHistory 接口支持手动删除单条历史记录
- 移除 GetHistory 的 Limit(5) 限制,返回完整历史列表
- 删除应用时同步更新关联配置的 updated_at 以便增量同步识别
- version 字段改为必填,前后端同步增加校验规则
- 前端历史列表增加操作类型列、删除按钮及表格最大高度限制
- 数据库表结构 datetime 升级为 datetime(3) 支持毫秒精度
This commit is contained in:
2026-09-06 21:04:25 +08:00
parent c0a7631d5c
commit 92c09df103
12 changed files with 224 additions and 63 deletions
+1
View File
@@ -67,6 +67,7 @@ func main() {
admin.POST("/config/update", configHandler.Update) admin.POST("/config/update", configHandler.Update)
admin.POST("/config/delete/:id", configHandler.Delete) admin.POST("/config/delete/:id", configHandler.Delete)
admin.GET("/config/configHistories/:id", configHandler.GetHistory) admin.GET("/config/configHistories/:id", configHandler.GetHistory)
admin.POST("/config/history/delete/:id", configHandler.DeleteHistory)
admin.POST("/config/rollback/:id", configHandler.Rollback) admin.POST("/config/rollback/:id", configHandler.Rollback)
// app management // app management
+23
View File
@@ -1,6 +1,7 @@
package dao package dao
import ( import (
"errors"
"kenaito-conf/server/internal/config" "kenaito-conf/server/internal/config"
"kenaito-conf/server/internal/model" "kenaito-conf/server/internal/model"
"kenaito-conf/server/internal/util" "kenaito-conf/server/internal/util"
@@ -98,3 +99,25 @@ func initSeedData() error {
return nil return nil
} }
// Now 返回数据库当前时间(毫秒精度),用于增量同步游标和 updated_at 写入。
// 统一使用数据库时间可以避免多实例应用服务器之间的时钟偏差。
func Now() (time.Time, error) {
rows, err := Engine.DB().Query("SELECT NOW(3)")
if err != nil {
return time.Time{}, err
}
defer rows.Close()
var now time.Time
if !rows.Next() {
if err := rows.Err(); err != nil {
return time.Time{}, err
}
return time.Time{}, errors.New("no database time result")
}
if err := rows.Scan(&now); err != nil {
return time.Time{}, err
}
return now, nil
}
+11 -3
View File
@@ -4,6 +4,7 @@ import (
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"kenaito-conf/server/internal/dao" "kenaito-conf/server/internal/dao"
"kenaito-conf/server/internal/logger"
"kenaito-conf/server/internal/middleware" "kenaito-conf/server/internal/middleware"
"kenaito-conf/server/internal/model" "kenaito-conf/server/internal/model"
"net/http" "net/http"
@@ -258,14 +259,21 @@ func (h *AppHandler) Delete(c *gin.Context) {
return return
} }
now, err := dao.Now()
if err != nil {
logger.SugaredLogger.Errorf("[AppHandler.Delete] get database time error: %v", err)
c.JSON(http.StatusInternalServerError, model.WrapperResponse{Code: 500, Msg: "删除失败"})
return
}
app.Enabled = false app.Enabled = false
app.UpdatedBy = userInfo.Name app.UpdatedBy = userInfo.Name
now := time.Now()
app.UpdatedAt = &now app.UpdatedAt = &now
dao.Engine.ID(app.Id).Cols("enabled", "updated_by", "updated_at").Update(&app) dao.Engine.ID(app.Id).Cols("enabled", "updated_by", "updated_at").Update(&app)
// soft delete related configs // soft delete related configs,同时更新 updated_at 让增量同步识别到删除
dao.Engine.Where("app_id = ?", id).Cols("enabled").Update(&model.AppConfig{Enabled: false}) dao.Engine.Where("app_id = ?", id).Cols("enabled", "updated_at").
Update(&model.AppConfig{Enabled: false, UpdatedAt: &now})
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0}) c.JSON(http.StatusOK, model.WrapperResponse{Code: 0})
} }
+71 -9
View File
@@ -2,6 +2,7 @@ package handler
import ( import (
"kenaito-conf/server/internal/dao" "kenaito-conf/server/internal/dao"
"kenaito-conf/server/internal/logger"
"kenaito-conf/server/internal/middleware" "kenaito-conf/server/internal/middleware"
"kenaito-conf/server/internal/model" "kenaito-conf/server/internal/model"
"kenaito-conf/server/internal/service" "kenaito-conf/server/internal/service"
@@ -162,7 +163,7 @@ type AddConfigRequest struct {
GroupId *int `json:"groupId"` GroupId *int `json:"groupId"`
AppId *int `json:"appId"` AppId *int `json:"appId"`
Env string `json:"env" binding:"required"` Env string `json:"env" binding:"required"`
Version string `json:"version"` Version string `json:"version" binding:"required"`
Name string `json:"name"` Name string `json:"name"`
Type int `json:"type" binding:"required"` Type int `json:"type" binding:"required"`
Contents string `json:"contents"` Contents string `json:"contents"`
@@ -201,6 +202,13 @@ func (h *ConfigHandler) Add(c *gin.Context) {
} }
} }
now, err := dao.Now()
if err != nil {
logger.SugaredLogger.Errorf("[ConfigHandler.Add] get database time error: %v", err)
c.JSON(http.StatusInternalServerError, model.WrapperResponse{Code: 500, Msg: "保存失败"})
return
}
config := model.AppConfig{ config := model.AppConfig{
GroupId: req.GroupId, GroupId: req.GroupId,
AppId: req.AppId, AppId: req.AppId,
@@ -210,9 +218,9 @@ func (h *ConfigHandler) Add(c *gin.Context) {
Type: req.Type, Type: req.Type,
Contents: req.Contents, Contents: req.Contents,
Enabled: true, Enabled: true,
CreatedAt: func() *time.Time { t := time.Now(); return &t }(), CreatedAt: &now,
CreatedBy: userInfo.Name, CreatedBy: userInfo.Name,
UpdatedAt: func() *time.Time { t := time.Now(); return &t }(), UpdatedAt: &now,
UpdatedBy: userInfo.Name, UpdatedBy: userInfo.Name,
} }
@@ -257,13 +265,18 @@ func (h *ConfigHandler) Update(c *gin.Context) {
} }
// save history // save history
h.saveHistory(&config, userInfo.Name) h.saveHistory(&config, userInfo.Name, "update")
originalContents := config.Contents originalContents := config.Contents
config.Contents = req.Contents config.Contents = req.Contents
config.Version = req.Version config.Version = req.Version
config.UpdatedBy = userInfo.Name config.UpdatedBy = userInfo.Name
now := time.Now() now, err := dao.Now()
if err != nil {
logger.SugaredLogger.Errorf("[ConfigHandler.Update] get database time error: %v", err)
c.JSON(http.StatusInternalServerError, model.WrapperResponse{Code: 500, Msg: "保存失败"})
return
}
config.UpdatedAt = &now config.UpdatedAt = &now
dao.Engine.ID(config.Id).Cols("contents", "version", "updated_by", "updated_at").Update(&config) dao.Engine.ID(config.Id).Cols("contents", "version", "updated_by", "updated_at").Update(&config)
@@ -289,9 +302,17 @@ func (h *ConfigHandler) Delete(c *gin.Context) {
return return
} }
// 记录删除前的配置内容
h.saveHistory(&config, userInfo.Name, "delete")
config.Enabled = false config.Enabled = false
config.UpdatedBy = userInfo.Name config.UpdatedBy = userInfo.Name
now := time.Now() now, err := dao.Now()
if err != nil {
logger.SugaredLogger.Errorf("[ConfigHandler.Delete] get database time error: %v", err)
c.JSON(http.StatusInternalServerError, model.WrapperResponse{Code: 500, Msg: "删除失败"})
return
}
config.UpdatedAt = &now config.UpdatedAt = &now
dao.Engine.ID(config.Id).Cols("enabled", "updated_by", "updated_at").Update(&config) dao.Engine.ID(config.Id).Cols("enabled", "updated_by", "updated_at").Update(&config)
@@ -301,7 +322,7 @@ func (h *ConfigHandler) Delete(c *gin.Context) {
func (h *ConfigHandler) GetHistory(c *gin.Context) { func (h *ConfigHandler) GetHistory(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id")) id, _ := strconv.Atoi(c.Param("id"))
var histories []model.AppConfigHistory var histories []model.AppConfigHistory
dao.Engine.Where("origin_id = ?", id).OrderBy("id desc").Limit(5).Find(&histories) dao.Engine.Where("origin_id = ?", id).OrderBy("id desc").Find(&histories)
if len(histories) > 0 { if len(histories) > 0 {
var currentConfig model.AppConfig var currentConfig model.AppConfig
@@ -315,6 +336,38 @@ func (h *ConfigHandler) GetHistory(c *gin.Context) {
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: histories}) c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: histories})
} }
// DeleteHistory 手动删除单条配置历史
func (h *ConfigHandler) DeleteHistory(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var history model.AppConfigHistory
has, _ := dao.Engine.ID(id).Get(&history)
if !has {
c.JSON(http.StatusOK, model.WrapperResponse{Code: 404, Msg: "历史记录不存在"})
return
}
userInfo := middleware.GetUserInfo(c)
var config model.AppConfig
if has, _ := dao.Engine.ID(history.OriginId).Get(&config); has {
if err := h.permService.RequirePermission(userInfo, config.GroupId, config.AppId, "RW"); err != nil {
c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: err.Error()})
return
}
} else if !userInfo.IsSuperAdmin() {
// 原配置已不存在时,只有超级管理员可以清理历史
c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: "只有超级管理员才能删除该历史记录"})
return
}
if _, err := dao.Engine.ID(id).Delete(&model.AppConfigHistory{}); err != nil {
logger.SugaredLogger.Errorf("[ConfigHandler.DeleteHistory] delete history error: %v", err)
c.JSON(http.StatusInternalServerError, model.WrapperResponse{Code: 500, Msg: "删除失败"})
return
}
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: true})
}
func (h *ConfigHandler) Rollback(c *gin.Context) { func (h *ConfigHandler) Rollback(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id")) id, _ := strconv.Atoi(c.Param("id"))
var history model.AppConfigHistory var history model.AppConfigHistory
@@ -332,9 +385,17 @@ func (h *ConfigHandler) Rollback(c *gin.Context) {
return return
} }
// 记录回滚前的配置内容
h.saveHistory(&config, userInfo.Name, "rollback")
config.Contents = history.Contents config.Contents = history.Contents
config.UpdatedBy = userInfo.Name config.UpdatedBy = userInfo.Name
now := time.Now() now, err := dao.Now()
if err != nil {
logger.SugaredLogger.Errorf("[ConfigHandler.Rollback] get database time error: %v", err)
c.JSON(http.StatusInternalServerError, model.WrapperResponse{Code: 500, Msg: "回滚失败"})
return
}
config.UpdatedAt = &now config.UpdatedAt = &now
dao.Engine.ID(config.Id).Cols("contents", "updated_by", "updated_at").Update(&config) dao.Engine.ID(config.Id).Cols("contents", "updated_by", "updated_at").Update(&config)
} }
@@ -353,7 +414,7 @@ func (h *ConfigHandler) buildAppNames(config *model.AppConfig) string {
return "" return ""
} }
func (h *ConfigHandler) saveHistory(config *model.AppConfig, operator string) { func (h *ConfigHandler) saveHistory(config *model.AppConfig, operator, operate string) {
history := model.AppConfigHistory{ history := model.AppConfigHistory{
OriginId: config.Id, OriginId: config.Id,
AppId: config.AppId, AppId: config.AppId,
@@ -362,6 +423,7 @@ func (h *ConfigHandler) saveHistory(config *model.AppConfig, operator string) {
Version: config.Version, Version: config.Version,
Name: config.Name, Name: config.Name,
Type: config.Type, Type: config.Type,
Operate: operate,
Contents: config.Contents, Contents: config.Contents,
CreatedAt: func() *time.Time { t := time.Now(); return &t }(), CreatedAt: func() *time.Time { t := time.Now(); return &t }(),
CreatedBy: operator, CreatedBy: operator,
+27 -10
View File
@@ -91,6 +91,14 @@ func (h *ConfigApiHandler) FetchAllConfigs(c *gin.Context) {
return return
} }
// 游标上界取查询前的数据库时间,保证查询期间发生的更新会在下次增量中返回
upperBound, err := dao.Now()
if err != nil {
logger.SugaredLogger.Errorf("[FetchAllConfigs] get database time error: %v", err)
c.JSON(http.StatusInternalServerError, model.WrapperResponse{Code: 500, Msg: "服务暂时不可用"})
return
}
result := make(map[string]interface{}) result := make(map[string]interface{})
var configIds []int var configIds []int
@@ -125,7 +133,7 @@ func (h *ConfigApiHandler) FetchAllConfigs(c *gin.Context) {
return return
} }
result["latest.fetch.timestamp"] = strconv.FormatInt(time.Now().UnixMilli(), 10) result["latest.fetch.timestamp"] = strconv.FormatInt(upperBound.UnixMilli(), 10)
// record client instances asynchronously // record client instances asynchronously
go h.recordClientInstances(getClientIP(c), configIds, req.AppName, req.Env, "fetchAll") go h.recordClientInstances(getClientIP(c), configIds, req.AppName, req.Env, "fetchAll")
@@ -170,6 +178,15 @@ func (h *ConfigApiHandler) FetchChangedConfigs(c *gin.Context) {
lastUpdateTime := time.UnixMilli(req.LastTime) lastUpdateTime := time.UnixMilli(req.LastTime)
// 以数据库时间为本次查询的固定上界,避免查询与游标生成之间的竞态,
// 同时避免多实例应用服务器的时钟偏差。
upperBound, err := dao.Now()
if err != nil {
logger.SugaredLogger.Errorf("[FetchChangedConfigs] get database time error: %v", err)
c.JSON(http.StatusInternalServerError, model.WrapperResponse{Code: 500, Msg: "服务暂时不可用"})
return
}
result := make(map[string]string) result := make(map[string]string)
var configIds []int var configIds []int
@@ -177,8 +194,8 @@ func (h *ConfigApiHandler) FetchChangedConfigs(c *gin.Context) {
var deletedKeys []string var deletedKeys []string
if !req.IgnoreGlobal && app.GroupId != nil { if !req.IgnoreGlobal && app.GroupId != nil {
var deletedGlobalConfigs []model.AppConfig var deletedGlobalConfigs []model.AppConfig
dao.Engine.Where("env = ? AND version = ? AND enabled = 0 AND group_id = ? AND (app_id is null OR app_id = 0) AND updated_at > ?", dao.Engine.Where("env = ? AND version = ? AND enabled = 0 AND group_id = ? AND (app_id is null OR app_id = 0) AND updated_at > ? AND updated_at <= ?",
req.Env, version, *app.GroupId, lastUpdateTime).Find(&deletedGlobalConfigs) req.Env, version, *app.GroupId, lastUpdateTime, upperBound).Find(&deletedGlobalConfigs)
for _, cfg := range deletedGlobalConfigs { for _, cfg := range deletedGlobalConfigs {
kvMap := h.parseService.ParseConfigToKVMap(&cfg) kvMap := h.parseService.ParseConfigToKVMap(&cfg)
for k := range kvMap { for k := range kvMap {
@@ -187,8 +204,8 @@ func (h *ConfigApiHandler) FetchChangedConfigs(c *gin.Context) {
} }
} }
var deletedAppConfigs []model.AppConfig var deletedAppConfigs []model.AppConfig
dao.Engine.Where("env = ? AND version = ? AND enabled = 0 AND app_id = ? AND updated_at > ?", dao.Engine.Where("env = ? AND version = ? AND enabled = 0 AND app_id = ? AND updated_at > ? AND updated_at <= ?",
req.Env, version, app.Id, lastUpdateTime).Find(&deletedAppConfigs) req.Env, version, app.Id, lastUpdateTime, upperBound).Find(&deletedAppConfigs)
for _, cfg := range deletedAppConfigs { for _, cfg := range deletedAppConfigs {
kvMap := h.parseService.ParseConfigToKVMap(&cfg) kvMap := h.parseService.ParseConfigToKVMap(&cfg)
for k := range kvMap { for k := range kvMap {
@@ -202,8 +219,8 @@ func (h *ConfigApiHandler) FetchChangedConfigs(c *gin.Context) {
// load global changed configs // load global changed configs
if !req.IgnoreGlobal && app.GroupId != nil { if !req.IgnoreGlobal && app.GroupId != nil {
var globalConfigs []model.AppConfig var globalConfigs []model.AppConfig
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 > ? AND updated_at <= ?",
req.Env, version, *app.GroupId, lastUpdateTime).Find(&globalConfigs) req.Env, version, *app.GroupId, lastUpdateTime, upperBound).Find(&globalConfigs)
for _, cfg := range globalConfigs { for _, cfg := range globalConfigs {
configIds = append(configIds, cfg.Id) configIds = append(configIds, cfg.Id)
h.addChangedConfigs(result, &cfg, req.FetchAll) h.addChangedConfigs(result, &cfg, req.FetchAll)
@@ -212,14 +229,14 @@ func (h *ConfigApiHandler) FetchChangedConfigs(c *gin.Context) {
// load app changed configs // load app changed configs
var appConfigs1 []model.AppConfig var appConfigs1 []model.AppConfig
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 > ? AND updated_at <= ?",
req.Env, version, app.Id, lastUpdateTime).Find(&appConfigs1) req.Env, version, app.Id, lastUpdateTime, upperBound).Find(&appConfigs1)
for _, cfg := range appConfigs1 { for _, cfg := range appConfigs1 {
configIds = append(configIds, cfg.Id) 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(upperBound.UnixMilli(), 10)
// query all config ids for this app to record client instances // query all config ids for this app to record client instances
var allConfigIds []int var allConfigIds []int
+3 -2
View File
@@ -13,9 +13,9 @@ type AppConfig struct {
Type int `xorm:"smallint(1)" json:"type"` // 1:file, 2:kv, 3:json Type int `xorm:"smallint(1)" json:"type"` // 1:file, 2:kv, 3:json
Contents string `xorm:"text" json:"contents"` Contents string `xorm:"text" json:"contents"`
Enabled bool `xorm:"tinyint(1) default 1" json:"enabled"` Enabled bool `xorm:"tinyint(1) default 1" json:"enabled"`
CreatedAt *time.Time `xorm:"datetime" json:"createdAt"` CreatedAt *time.Time `xorm:"datetime(3)" json:"createdAt"`
CreatedBy string `xorm:"varchar(32)" json:"createdBy"` CreatedBy string `xorm:"varchar(32)" json:"createdBy"`
UpdatedAt *time.Time `xorm:"datetime" json:"updatedAt"` UpdatedAt *time.Time `xorm:"datetime(3)" json:"updatedAt"`
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"`
@@ -36,6 +36,7 @@ type AppConfigHistory struct {
Version string `xorm:"varchar(32)" json:"version"` Version string `xorm:"varchar(32)" json:"version"`
Name string `xorm:"varchar(32)" json:"name"` Name string `xorm:"varchar(32)" json:"name"`
Type int `xorm:"smallint(1)" json:"type"` Type int `xorm:"smallint(1)" json:"type"`
Operate string `xorm:"varchar(16)" json:"operate"` // update, delete, rollback
Contents string `xorm:"text" json:"contents"` Contents string `xorm:"text" json:"contents"`
ActiveContents string `xorm:"-" json:"activeContents,omitempty"` ActiveContents string `xorm:"-" json:"activeContents,omitempty"`
CreatedAt *time.Time `xorm:"datetime" json:"createdAt"` CreatedAt *time.Time `xorm:"datetime" json:"createdAt"`
+3 -2
View File
@@ -99,9 +99,9 @@ CREATE TABLE `app_config` (
`type` smallint DEFAULT NULL, `type` smallint DEFAULT NULL,
`contents` text, `contents` text,
`enabled` tinyint(1) DEFAULT '1', `enabled` tinyint(1) DEFAULT '1',
`created_at` datetime DEFAULT NULL, `created_at` datetime(3) DEFAULT NULL,
`created_by` varchar(32) DEFAULT NULL, `created_by` varchar(32) DEFAULT NULL,
`updated_at` datetime DEFAULT NULL, `updated_at` datetime(3) DEFAULT NULL,
`updated_by` varchar(32) DEFAULT NULL, `updated_by` varchar(32) DEFAULT NULL,
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8mb3; ) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8mb3;
@@ -150,6 +150,7 @@ CREATE TABLE `app_config_history` (
`version` varchar(32) DEFAULT NULL, `version` varchar(32) DEFAULT NULL,
`name` varchar(32) DEFAULT NULL, `name` varchar(32) DEFAULT NULL,
`type` smallint DEFAULT NULL, `type` smallint DEFAULT NULL,
`operate` varchar(16) DEFAULT NULL,
`contents` text, `contents` text,
`created_at` datetime DEFAULT NULL, `created_at` datetime DEFAULT NULL,
`created_by` varchar(32) DEFAULT NULL, `created_by` varchar(32) DEFAULT NULL,
+7
View File
@@ -51,3 +51,10 @@ export function rollbackConfig(id) {
method: 'post' method: 'post'
}) })
} }
export function deleteConfigHistory(id) {
return request({
url: `/admin/config/history/delete/${id}`,
method: 'post'
})
}
+21 -3
View File
@@ -62,9 +62,17 @@
></el-pagination> ></el-pagination>
<el-dialog title="变更历史" :visible.sync="historyVisible" width="900px"> <el-dialog title="变更历史" :visible.sync="historyVisible" width="900px">
<el-table :data="historyList" border stripe> <el-table :data="historyList" border stripe max-height="500">
<el-table-column prop="id" label="ID" width="60"></el-table-column> <el-table-column prop="id" label="ID" width="60"></el-table-column>
<el-table-column prop="createdBy" label="操作人" width="100"></el-table-column> <el-table-column prop="createdBy" label="操作人" width="100"></el-table-column>
<el-table-column label="操作类型" width="100">
<template slot-scope="scope">
<span v-if="scope.row.operate === 'delete'" style="color: #F56C6C">删除</span>
<span v-else-if="scope.row.operate === 'rollback'">回滚</span>
<span v-else-if="scope.row.operate === 'update'">更新</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="操作时间" width="180"> <el-table-column label="操作时间" width="180">
<template slot-scope="scope"> <template slot-scope="scope">
{{ formatTime(scope.row.createdAt) }} {{ formatTime(scope.row.createdAt) }}
@@ -73,7 +81,8 @@
<el-table-column label="操作"> <el-table-column label="操作">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button type="text" @click="viewHistoryContent(scope.row)">查看内容</el-button> <el-button type="text" @click="viewHistoryContent(scope.row)">查看内容</el-button>
<el-button type="text" @click="handleRollback(scope.row)">回滚到此版本</el-button> <el-button v-if="scope.row.operate !== 'delete'" type="text" @click="handleRollback(scope.row)">回滚到此版本</el-button>
<el-button type="text" style="color: #F56C6C" @click="handleDeleteHistory(scope.row)">删除</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -133,7 +142,7 @@
</template> </template>
<script> <script>
import { getConfigList, deleteConfig, getConfigHistory, rollbackConfig, getConfig } from '../../api/config' import { getConfigList, deleteConfig, getConfigHistory, rollbackConfig, getConfig, deleteConfigHistory } from '../../api/config'
import { getClientListByConfig } from '../../api/client' import { getClientListByConfig } from '../../api/client'
import request from '../../api/request' import request from '../../api/request'
@@ -313,6 +322,15 @@ export default {
this.historyVisible = true this.historyVisible = true
} catch (e) {} } catch (e) {}
}, },
async handleDeleteHistory(row) {
try {
await this.$confirm('确定删除这条历史记录?删除后不可恢复。')
await deleteConfigHistory(row.id)
this.$message.success('删除成功')
const res = await getConfigHistory(row.originId)
this.historyList = res.data || []
} catch (e) {}
},
async viewHistoryContent(row) { async viewHistoryContent(row) {
this.currentHistory = row this.currentHistory = row
const historyContent = row.contents || '' const historyContent = row.contents || ''
+2 -1
View File
@@ -23,7 +23,7 @@
<el-option v-for="p in profiles" :key="p.value" :label="p.label" :value="p.value"></el-option> <el-option v-for="p in profiles" :key="p.value" :label="p.label" :value="p.value"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="版本"> <el-form-item label="版本" prop="version">
<el-input v-model="form.version" placeholder="latest"></el-input> <el-input v-model="form.version" placeholder="latest"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="配置格式"> <el-form-item label="配置格式">
@@ -66,6 +66,7 @@ export default {
}, },
rules: { rules: {
env: [{ required: true, message: '请选择环境', trigger: 'change' }], env: [{ required: true, message: '请选择环境', trigger: 'change' }],
version: [{ required: true, message: '请输入版本', trigger: 'blur' }],
contents: [{ required: true, message: '请输入配置内容', trigger: 'blur' }] contents: [{ required: true, message: '请输入配置内容', trigger: 'blur' }]
}, },
groups: [], groups: [],
+6 -2
View File
@@ -1,7 +1,7 @@
<template> <template>
<el-card> <el-card>
<div slot="header">编辑配置</div> <div slot="header">编辑配置</div>
<el-form :model="form" ref="form" label-width="100px" v-loading="loading"> <el-form :model="form" :rules="rules" ref="form" label-width="100px" v-loading="loading">
<el-form-item label="配置名称"> <el-form-item label="配置名称">
<el-input v-model="form.name" disabled></el-input> <el-input v-model="form.name" disabled></el-input>
</el-form-item> </el-form-item>
@@ -11,7 +11,7 @@
<el-form-item label="所属应用"> <el-form-item label="所属应用">
<el-input :value="form.appNames" disabled></el-input> <el-input :value="form.appNames" disabled></el-input>
</el-form-item> </el-form-item>
<el-form-item label="版本"> <el-form-item label="版本" prop="version">
<el-input v-model="form.version"></el-input> <el-input v-model="form.version"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="配置内容"> <el-form-item label="配置内容">
@@ -32,6 +32,9 @@ export default {
data() { data() {
return { return {
form: {}, form: {},
rules: {
version: [{ required: true, message: '请输入版本', trigger: 'blur' }]
},
loading: false loading: false
} }
}, },
@@ -50,6 +53,7 @@ export default {
}, },
async handleSubmit() { async handleSubmit() {
try { try {
await this.$refs.form.validate()
await updateConfig(this.form) await updateConfig(this.form)
this.$message.success('保存成功') this.$message.success('保存成功')
if (this.form.appId) { if (this.form.appId) {
+21 -3
View File
@@ -57,9 +57,17 @@
></el-pagination> ></el-pagination>
<el-dialog title="变更历史" :visible.sync="historyVisible" width="900px"> <el-dialog title="变更历史" :visible.sync="historyVisible" width="900px">
<el-table :data="historyList" border stripe> <el-table :data="historyList" border stripe max-height="500">
<el-table-column prop="id" label="ID" width="60"></el-table-column> <el-table-column prop="id" label="ID" width="60"></el-table-column>
<el-table-column prop="createdBy" label="操作人" width="100"></el-table-column> <el-table-column prop="createdBy" label="操作人" width="100"></el-table-column>
<el-table-column label="操作类型" width="100">
<template slot-scope="scope">
<span v-if="scope.row.operate === 'delete'" style="color: #F56C6C">删除</span>
<span v-else-if="scope.row.operate === 'rollback'">回滚</span>
<span v-else-if="scope.row.operate === 'update'">更新</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="操作时间" width="180"> <el-table-column label="操作时间" width="180">
<template slot-scope="scope"> <template slot-scope="scope">
{{ formatTime(scope.row.createdAt) }} {{ formatTime(scope.row.createdAt) }}
@@ -68,7 +76,8 @@
<el-table-column label="操作"> <el-table-column label="操作">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button type="text" @click="viewHistoryContent(scope.row)">查看内容</el-button> <el-button type="text" @click="viewHistoryContent(scope.row)">查看内容</el-button>
<el-button type="text" @click="handleRollback(scope.row)">回滚到此版本</el-button> <el-button v-if="scope.row.operate !== 'delete'" type="text" @click="handleRollback(scope.row)">回滚到此版本</el-button>
<el-button type="text" style="color: #F56C6C" @click="handleDeleteHistory(scope.row)">删除</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -128,7 +137,7 @@
</template> </template>
<script> <script>
import { getConfigList, deleteConfig, getConfigHistory, rollbackConfig, getConfig } from '../../api/config' import { getConfigList, deleteConfig, getConfigHistory, rollbackConfig, getConfig, deleteConfigHistory } from '../../api/config'
import { getClientListByConfig } from '../../api/client' import { getClientListByConfig } from '../../api/client'
import request from '../../api/request' import request from '../../api/request'
@@ -299,6 +308,15 @@ export default {
this.historyVisible = true this.historyVisible = true
} catch (e) {} } catch (e) {}
}, },
async handleDeleteHistory(row) {
try {
await this.$confirm('确定删除这条历史记录?删除后不可恢复。')
await deleteConfigHistory(row.id)
this.$message.success('删除成功')
const res = await getConfigHistory(row.originId)
this.historyList = res.data || []
} catch (e) {}
},
async viewHistoryContent(row) { async viewHistoryContent(row) {
this.currentHistory = row this.currentHistory = row
const historyContent = row.contents || '' const historyContent = row.contents || ''