完成前端缓存监控适配

This commit is contained in:
wkc
2026-03-28 11:18:36 +08:00
parent 65fe3d4605
commit 7075bee8f4
4 changed files with 242 additions and 80 deletions

View File

@@ -0,0 +1,27 @@
# 前端移除 Redis 实施记录
## 实施时间
- 2026-03-28
## 修改内容
- 将缓存概览页从 Redis 专属字段改为本地缓存统计字段展示
- 将第二张图表改为本地缓存统计柱状图,并保留原有页面布局
- 为缓存概览页补充空数据保护、命中率计算和监控采样时间展示
- 为缓存列表页补充空列表兜底、清理后的详情重置和全部清理后的界面清空
- 复核缓存监控 API 路径不变,仅调整接口注释为缓存语义
## 文档路径
- `doc/2026-03-28-remove-redis-frontend-plan.md`
- `doc/implementation-report-2026-03-28-remove-redis-frontend.md`
## 验证结果
- 已两次验证 `npm --prefix ruoyi-ui run build:prod` 通过,输出为 `Build complete.`
- 已验证 `npm --prefix ruoyi-ui run dev` 可成功启动,前端开发服务地址为 `http://localhost:1025`
- 已在浏览器中完成登录并打开 `/monitor/cache``/monitor/cacheList` 路由,确认页面可正常进入
- 当前环境下真实接口 `/dev-api/monitor/cache``/dev-api/monitor/cache/getNames``/dev-api/monitor/cache/getKeys/*` 请求均出现 10 秒超时,未能完成真实后端联调
- 已使用与后端实现一致的返回结构进行浏览器侧 mock 验证,确认缓存概览字段映射、图表渲染、缓存名称到键名联动、缓存内容展示以及清理后表单清空行为正常
## 说明
- 按仓库要求,本次前端开发直接在当前分支进行,未使用 `git worktree`
- 计划明确不引入新的前端测试框架,因此本次以前端生产构建与本地联调作为主要验证手段
- 联调与验证结束后,已主动停止本次任务启动的 Node 前端进程

View File

@@ -1,6 +1,6 @@
import request from '@/utils/request'
// 查询缓存详细
// 查询缓存概览
export function getCache() {
return request({
url: '/monitor/cache',
@@ -32,7 +32,7 @@ export function getCacheValue(cacheName, cacheKey) {
})
}
// 清理指定名称缓存
// 清理指定名称下的缓存
export function clearCacheName(cacheName) {
return request({
url: '/monitor/cache/clearCacheName/' + cacheName,

View File

@@ -8,34 +8,34 @@
<table cellspacing="0" style="width: 100%">
<tbody>
<tr>
<td class="el-table__cell is-leaf"><div class="cell">Redis版本</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.redis_version }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">缓存类型</div></td>
<td class="el-table__cell is-leaf"><div class="cell">{{ cacheTypeText }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">运行模式</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.redis_mode == "standalone" ? "单机" : "集群" }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">端口</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.tcp_port }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">客户端</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.connected_clients }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">{{ cacheModeText }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">总键数</div></td>
<td class="el-table__cell is-leaf"><div class="cell">{{ displayDbSize }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">写入次</div></td>
<td class="el-table__cell is-leaf"><div class="cell">{{ cache.info.write_count }}</div></td>
</tr>
<tr>
<td class="el-table__cell is-leaf"><div class="cell">运行时间()</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.uptime_in_days }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">使用内存</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.used_memory_human }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">使用CPU</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ parseFloat(cache.info.used_cpu_user_children).toFixed(2) }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">内存配置</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.maxmemory_human }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">命中次数</div></td>
<td class="el-table__cell is-leaf"><div class="cell">{{ cache.info.hit_count }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">未命中次数</div></td>
<td class="el-table__cell is-leaf"><div class="cell">{{ cache.info.miss_count }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">过期清理次数</div></td>
<td class="el-table__cell is-leaf"><div class="cell">{{ cache.info.expired_count }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">命中率</div></td>
<td class="el-table__cell is-leaf"><div class="cell">{{ hitRateText }}</div></td>
</tr>
<tr>
<td class="el-table__cell is-leaf"><div class="cell">AOF是否开启</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.aof_enabled == "0" ? "否" : "是" }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">RDB是否成功</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.rdb_last_bgsave_status }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">Key数量</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.dbSize">{{ cache.dbSize }} </div></td>
<td class="el-table__cell is-leaf"><div class="cell">网络入口/出口</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.instantaneous_input_kbps }}kps/{{cache.info.instantaneous_output_kbps}}kps</div></td>
<td class="el-table__cell is-leaf"><div class="cell">统计说明</div></td>
<td class="el-table__cell is-leaf"><div class="cell">进程内累计统计</div></td>
<td class="el-table__cell is-leaf"><div class="cell">监控范围</div></td>
<td class="el-table__cell is-leaf"><div class="cell">当前应用实例</div></td>
<td class="el-table__cell is-leaf"><div class="cell">图表数据项</div></td>
<td class="el-table__cell is-leaf"><div class="cell">{{ commandStats.length }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">监控采样时间</div></td>
<td class="el-table__cell is-leaf"><div class="cell">{{ sampleTime }}</div></td>
</tr>
</tbody>
</table>
@@ -54,7 +54,7 @@
<el-col :span="12" class="card-box">
<el-card>
<div slot="header"><span><i class="el-icon-odometer"></i> 内存信息</span></div>
<div slot="header"><span><i class="el-icon-odometer"></i> 缓存命中概览</span></div>
<div class="el-table el-table--enable-row-hover el-table--medium">
<div ref="usedmemory" style="height: 420px" />
</div>
@@ -74,74 +74,197 @@ export default {
return {
// 统计命令信息
commandstats: null,
// 使用内存
// 缓存统计
usedmemory: null,
// cache信息
cache: []
cache: {
info: {
cache_type: "IN_MEMORY",
cache_mode: "single-instance",
key_size: 0,
hit_count: 0,
miss_count: 0,
expired_count: 0,
write_count: 0
},
dbSize: 0,
commandStats: []
},
sampleTime: "-",
resizeHandler: null
}
},
created() {
this.getList()
this.openLoading()
this.getList()
},
beforeDestroy() {
if (this.resizeHandler) {
window.removeEventListener("resize", this.resizeHandler)
}
if (this.commandstats) {
this.commandstats.dispose()
}
if (this.usedmemory) {
this.usedmemory.dispose()
}
},
computed: {
cacheTypeText() {
return this.formatCacheType(this.cache.info.cache_type)
},
cacheModeText() {
return this.formatCacheMode(this.cache.info.cache_mode)
},
displayDbSize() {
return this.toNumber(this.cache.dbSize || this.cache.info.key_size)
},
hitRateText() {
const hitCount = this.toNumber(this.cache.info.hit_count)
const missCount = this.toNumber(this.cache.info.miss_count)
const total = hitCount + missCount
if (!total) {
return "0.00%"
}
return ((hitCount / total) * 100).toFixed(2) + "%"
},
commandStats() {
return this.normalizeCommandStats(this.cache.commandStats)
}
},
methods: {
/** 查缓存询信息 */
getList() {
getCache().then((response) => {
this.cache = response.data
this.cache = this.normalizeCacheData(response.data)
this.sampleTime = this.formatSampleTime(new Date())
this.$modal.closeLoading()
this.commandstats = echarts.init(this.$refs.commandstats, "macarons")
this.commandstats.setOption({
tooltip: {
trigger: "item",
formatter: "{a} <br/>{b} : {c} ({d}%)",
},
series: [
{
name: "命令",
type: "pie",
roseType: "radius",
radius: [15, 95],
center: ["50%", "38%"],
data: response.data.commandStats,
animationEasing: "cubicInOut",
animationDuration: 1000,
}
]
})
this.usedmemory = echarts.init(this.$refs.usedmemory, "macarons")
this.usedmemory.setOption({
tooltip: {
formatter: "{b} <br/>{a} : " + this.cache.info.used_memory_human,
},
series: [
{
name: "峰值",
type: "gauge",
min: 0,
max: 1000,
detail: {
formatter: this.cache.info.used_memory_human,
},
data: [
{
value: parseFloat(this.cache.info.used_memory_human),
name: "内存消耗",
}
]
}
]
})
window.addEventListener("resize", () => {
this.commandstats.resize()
this.usedmemory.resize()
this.$nextTick(() => {
this.renderCharts()
})
}).catch(() => {
this.cache = this.normalizeCacheData()
this.sampleTime = "-"
this.$modal.closeLoading()
})
},
// 打开加载层
openLoading() {
this.$modal.loading("正在加载缓存监控数据,请稍候!")
},
normalizeCacheData(data) {
const source = data || {}
const info = source.info || {}
return {
info: {
cache_type: info.cache_type || "IN_MEMORY",
cache_mode: info.cache_mode || "single-instance",
key_size: this.toNumber(info.key_size),
hit_count: this.toNumber(info.hit_count),
miss_count: this.toNumber(info.miss_count),
expired_count: this.toNumber(info.expired_count),
write_count: this.toNumber(info.write_count)
},
dbSize: this.toNumber(source.dbSize || info.key_size),
commandStats: source.commandStats || []
}
},
normalizeCommandStats(commandStats) {
const stats = Array.isArray(commandStats) ? commandStats : []
return stats.map((item) => ({
name: this.statLabel(item.name),
value: this.toNumber(item.value)
}))
},
statLabel(name) {
const labelMap = {
hit_count: "命中次数",
miss_count: "未命中次数",
expired_count: "过期清理次数",
write_count: "写入次数"
}
return labelMap[name] || name || "未命名统计"
},
formatCacheType(type) {
const typeMap = {
IN_MEMORY: "进程内缓存"
}
return typeMap[type] || type || "-"
},
formatCacheMode(mode) {
const modeMap = {
"single-instance": "单实例"
}
return modeMap[mode] || mode || "-"
},
formatSampleTime(date) {
const current = date || new Date()
const pad = (value) => String(value).padStart(2, "0")
return `${current.getFullYear()}-${pad(current.getMonth() + 1)}-${pad(current.getDate())} ${pad(current.getHours())}:${pad(current.getMinutes())}:${pad(current.getSeconds())}`
},
toNumber(value) {
const parsedValue = Number(value)
return Number.isFinite(parsedValue) ? parsedValue : 0
},
renderCharts() {
if (!this.commandstats) {
this.commandstats = echarts.init(this.$refs.commandstats, "macarons")
}
if (!this.usedmemory) {
this.usedmemory = echarts.init(this.$refs.usedmemory, "macarons")
}
this.commandstats.setOption({
tooltip: {
trigger: "item",
formatter: "{a} <br/>{b} : {c} ({d}%)"
},
series: [
{
name: "缓存统计",
type: "pie",
roseType: "radius",
radius: [15, 95],
center: ["50%", "38%"],
data: this.commandStats,
animationEasing: "cubicInOut",
animationDuration: 1000
}
]
})
this.usedmemory.setOption({
tooltip: {
trigger: "axis",
axisPointer: {
type: "shadow"
}
},
xAxis: {
type: "category",
data: this.commandStats.map((item) => item.name),
axisTick: {
alignWithLabel: true
}
},
yAxis: {
type: "value",
minInterval: 1
},
series: [
{
name: "统计次数",
type: "bar",
barMaxWidth: 48,
data: this.commandStats.map((item) => item.value)
}
]
})
if (!this.resizeHandler) {
this.resizeHandler = () => {
this.commandstats && this.commandstats.resize()
this.usedmemory && this.usedmemory.resize()
}
window.addEventListener("resize", this.resizeHandler)
}
}
}
}

View File

@@ -175,7 +175,10 @@ export default {
getCacheNames() {
this.loading = true
listCacheName().then(response => {
this.cacheNames = response.data
this.cacheNames = response.data || []
this.loading = false
}).catch(() => {
this.cacheNames = []
this.loading = false
})
},
@@ -188,7 +191,7 @@ export default {
handleClearCacheName(row) {
clearCacheName(row.cacheName).then(response => {
this.$modal.msgSuccess("清理缓存名称[" + row.cacheName + "]成功")
this.getCacheKeys()
this.getCacheKeys(row)
})
},
/** 查询缓存键名列表 */
@@ -199,7 +202,13 @@ export default {
}
this.subLoading = true
listCacheKey(cacheName).then(response => {
this.cacheKeys = response.data
this.cacheKeys = response.data || []
this.cacheForm = {}
this.subLoading = false
this.nowCacheName = cacheName
}).catch(() => {
this.cacheKeys = []
this.cacheForm = {}
this.subLoading = false
this.nowCacheName = cacheName
})
@@ -227,13 +236,16 @@ export default {
/** 查询缓存内容详细 */
handleCacheValue(cacheKey) {
getCacheValue(this.nowCacheName, cacheKey).then(response => {
this.cacheForm = response.data
this.cacheForm = response.data || {}
})
},
/** 清理全部缓存 */
handleClearCacheAll() {
clearCacheAll().then(response => {
this.$modal.msgSuccess("清理全部缓存成功")
this.cacheKeys = []
this.cacheForm = {}
this.nowCacheName = ""
})
}
}