本文为使用ruoyi-cloud框架构建一个三级查询redis信息的页面,仅用于练手。
一、需求概述
在若依(RuoYi)前后端分离框架中,系统缓存管理是后台管理系统的常见功能模块。本文将以一个完整的实战案例,演示如何从零开发一个三级联动的缓存管理模块,实现 Redis Hash 结构数据的可视化浏览与删除操作。
该模块采用「缓存列表 → key 名列表 → 缓存内容」的三级联动设计,前端基于 Vue 3 + Element Plus,后端基于 Spring Boot,数据存储使用 Redis Hash 结构。
二、前端菜单配置
在若依框架中,新增页面功能的第一步是在系统菜单中注册路由。菜单配置信息如下:
| 配置项 | 值 |
|---|---|
| 菜单名称 | cacheList |
| 路由地址 | system/cache/index |
| 权限标识 | system:cache:index |
在系统管理 → 菜单管理页面中,点击「新增」按钮,填写上述信息。组件路径填写 system/cache/index,菜单类型选择「目录」或「菜单」,根据实际需要分配访问权限。

三、前端页面开发
在 src/views/system/ 目录下新建 cache 目录,并创建 index.vue 文件。页面采用三栏布局,分别展示缓存列表、key 名列表和缓存内容。
html
<template>
<div class="app-container cache-container">
<el-row :gutter="20">
<!-- 一级:Redis Hash 的 key -->
<el-col :xs="24" :sm="24" :md="8">
<el-card class="cache-card" shadow="never">
<template #header>
<div class="card-header">
<span>缓存列表</span>
<el-tag size="small" type="info" round>{{ cacheList.length }} 项</el-tag>
</div>
</template>
<el-table
ref="cacheTableRef"
:data="cacheList"
height="440"
highlight-current-row
@row-click="handleCacheClick"
>
<el-table-column prop="num" label="序号" width="60" align="center" />
<el-table-column prop="name" label="缓存名称" min-width="110" show-overflow-tooltip />
<el-table-column prop="info" label="备注" min-width="90" show-overflow-tooltip />
<el-table-column label="操作" width="64" align="center">
<template #default="{ row }">
<el-button link type="danger" @click.stop="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
<!-- 二级:Hash 的 field -->
<el-col :xs="24" :sm="24" :md="8">
<el-card
class="cache-card"
shadow="never"
v-loading="fieldLoading"
element-loading-text="加载中..."
>
<template #header>
<div class="card-header">
<span>key名列表</span>
<el-tag v-if="selectedCache" size="small" type="success" round>
{{ fieldList.length }} 个
</el-tag>
</div>
</template>
<template v-if="fieldList.length">
<el-table
ref="fieldTableRef"
:data="fieldList"
height="440"
highlight-current-row
@row-click="handleFieldClick"
>
<el-table-column prop="num" label="序号" width="60" align="center" />
<el-table-column prop="field" label="key 名称" min-width="150" show-overflow-tooltip />
</el-table>
</template>
<div v-else class="empty-wrap">
<el-empty :description="selectedCache ? '该缓存下暂无数据' : '请先选择左侧缓存'" :image-size="80" />
</div>
</el-card>
</el-col>
<!-- 三级:Hash 的 value -->
<el-col :xs="24" :sm="24" :md="8">
<el-card
class="cache-card content-card"
shadow="never"
v-loading="valueLoading"
element-loading-text="加载中..."
>
<template #header>
<div class="card-header">
<span>缓存内容</span>
<el-tag v-if="selectedCache" size="small" round>{{ selectedCache }}</el-tag>
</div>
</template>
<div v-if="selectedField" class="value-panel">
<el-descriptions :column="1" size="small" border>
<el-descriptions-item label="缓存名称">{{ selectedCache }}</el-descriptions-item>
<el-descriptions-item label="key 名称">{{ selectedField }}</el-descriptions-item>
<el-descriptions-item label="数据类型">Hash / String</el-descriptions-item>
</el-descriptions>
<div class="value-title">Value</div>
<pre class="value-content">{{ formattedValue || "接口暂未返回该 key 的内容" }}</pre>
</div>
<div v-else class="empty-wrap">
<el-empty
:description="selectedCache ? '请选择左侧 key 查看内容' : '请依次选择缓存和 key'"
:image-size="80"
/>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { computed, ref } from "vue"
import { ElMessage, ElMessageBox } from "element-plus"
import { getCacheKey, getCachelist } from "@/api/system/cache.js";
defineOptions({ name: "Cache" })
/**
* 静态 Redis Hash 结构数据:
* 第一级 key -> 第二级 field -> 第三级 value
*/
const redisHashData = ref({
login_tokens: {
info: "用户登录令牌",
fields: {
"b1a2c3d4e5f64780a9b0c1d2e3f4a5b6":
'{"userId":1,"userName":"admin","nickName":"若依","deptName":"研发部门","ipaddr":"127.0.0.1","loginTime":"2026-10-19 09:30:00","expireTime":"2026-10-19 09:40:00"}',
"c7d8e9f0a1b24356c7d8e9f0a1b24356":
'{"userId":2,"userName":"ry","nickName":"普通用户","deptName":"测试部门","ipaddr":"192.168.1.20","loginTime":"2026-10-19 08:15:00","expireTime":"2026-10-19 08:25:00"}',
"d2e3f4a5b6c74890d1e2f3a4b5c6d7e8":
'{"userId":3,"userName":"test","nickName":"测试账号","deptName":"测试部门","ipaddr":"10.0.0.15","loginTime":"2026-10-18 21:05:00","expireTime":"2026-10-18 21:15:00"}'
}
},
sys_config: {
info: "系统配置",
fields: {
"sys.index.skinName": "skin-blue",
"sys.index.sideTheme": "theme-dark",
"sys.user.initPassword": "123456",
"sys.account.captchaEnabled": "true",
"sys.account.registerUser": "false"
}
},
sys_dict: {
info: "数据字典",
fields: {
sys_normal_disable:
'[{"dictLabel":"正常","dictValue":"0","dictCode":"sys_normal_disable","listClass":"primary"},{"dictLabel":"停用","dictValue":"1","dictCode":"sys_normal_disable","listClass":"danger"}]',
sys_user_sex:
'[{"dictLabel":"男","dictValue":"0","dictCode":"sys_user_sex"},{"dictLabel":"女","dictValue":"1","dictCode":"sys_user_sex"},{"dictLabel":"未知","dictValue":"2","dictCode":"sys_user_sex"}]',
sys_yes_no:
'[{"dictLabel":"是","dictValue":"Y","dictCode":"sys_yes_no"},{"dictLabel":"否","dictValue":"N","dictCode":"sys_yes_no"}]'
}
},
sys_data_scope: {
info: "数据权限范围",
fields: {
1: "全部数据权限",
2: "自定义数据权限",
3: "本部门数据权限",
4: "本部门及以下数据权限",
5: "仅本人数据权限"
}
}
})
const cacheTableRef = ref(null)
const fieldTableRef = ref(null)
const selectedCache = ref("")
const selectedField = ref("")
/** 二级 key 列表(由 getCachelist 返回的 rows 加载) */
const cacheKeys = ref([])
const fieldLoading = ref(false)
let loadSeq = 0
/** 三级 value(由 getCacheKey 加载) */
const cacheValue = ref("")
const valueLoading = ref(false)
let valueSeq = 0
/** 一级列表 */
const cacheList = computed(() =>
Object.entries(redisHashData.value).map(([name, item], index) => ({
num: index + 1,
name,
info: item.info
}))
)
/** 二级列表:接口返回的 rows 为 key 名称字符串数组 */
const fieldList = computed(() =>
cacheKeys.value.map((field, index) => ({
num: index + 1,
field
}))
)
/** JSON 字符串自动格式化,普通字符串原样展示 */
const formattedValue = computed(() => {
const raw = cacheValue.value
if (raw === undefined || raw === null || raw === "") return ""
if (typeof raw !== "string") {
return JSON.stringify(raw, null, 2)
}
try {
return JSON.stringify(JSON.parse(raw), null, 2)
} catch {
return raw
}
})
/** 选择一级缓存:通过 getCachelist 加载二级列表 */
async function handleCacheClick(row) {
if (selectedCache.value === row.name) return
selectedCache.value = row.name
selectedField.value = ""
cacheKeys.value = []
cacheValue.value = ""
valueSeq++
valueLoading.value = false
fieldTableRef.value?.setCurrentRow()
fieldLoading.value = true
try {
const res = await getCachelist(row.name)
cacheKeys.value = Array.isArray(res.rows) ? res.rows : []
} finally {
fieldLoading.value = false
}
}
/** 选择二级 key:通过 getCacheKey 加载三级内容 */
async function handleFieldClick(row) {
selectedField.value = row.field
cacheValue.value = ""
// 防止快速切换 key 时旧请求覆盖新请求的内容
const seq = ++valueSeq
valueLoading.value = true
try {
const res = await getCacheKey(row.field)
if (seq !== valueSeq) return
// 后端 success(jsonStr) 会命中 success(String msg) 重载,值放在 msg 中
cacheValue.value = res.data ?? res.msg ?? ""
} catch {
if (seq === valueSeq) cacheValue.value = ""
} finally {
if (seq === valueSeq) valueLoading.value = false
}
}
/** 仅从静态数据中移除,刷新页面后恢复 */
function handleDelete(row) {
ElMessageBox.confirm(`确定从当前视图删除缓存「${row.name}」吗?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
delete redisHashData.value[row.name]
if (selectedCache.value === row.name) {
selectedCache.value = ""
selectedField.value = ""
cacheKeys.value = []
cacheValue.value = ""
loadSeq++
valueSeq++
fieldLoading.value = false
valueLoading.value = false
cacheTableRef.value?.setCurrentRow()
}
ElMessage.success("已从静态数据中移除")
})
.catch(() => {})
}
</script>
<style scoped lang="scss">
.cache-container {
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.cache-card {
display: flex;
flex-direction: column;
height: 540px;
margin-bottom: 20px;
:deep(.el-card__body) {
flex: 1;
min-height: 0;
overflow: hidden;
}
:deep(.el-table__row) {
cursor: pointer;
}
}
.content-card {
:deep(.el-card__body) {
overflow: auto;
}
}
.empty-wrap {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.value-panel {
.value-title {
margin: 16px 0 8px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.value-content {
margin: 0;
padding: 12px;
min-height: 180px;
max-height: 260px;
overflow: auto;
font-family: Consolas, Monaco, "Courier New", monospace;
font-size: 12px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
color: var(--el-text-color-regular);
background: var(--el-fill-color-light);
border-radius: 4px;
}
}
}
</style>
四、API 接口封装
在 src/api/system/ 目录下新建 cache.js 文件,封装缓存相关的接口请求。实际项目中,这些接口会调用后端 Controller 提供的 RESTful API。
javascript
import request from '@/utils/request'
// 查询缓存列表
export function getCachelist(name) {
return request({
url: /system/test/cache/${name},
method: 'get',
})
}
// 查询缓存 key 列表
export function getCacheKey(name) {
return request({
url: /system/test/cache/key/${name},
method: 'get',
})
}
五、后端接口设计
后端基于 Spring Boot 提供缓存管理接口,核心思路是使用 RedisTemplate 操作 Redis Hash 结构。Controller 层负责接收前端请求并返回统一格式的响应数据。
java
package com.ruoyi.system.controller;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.web.controller.BaseController;
import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.common.core.web.page.TableDataInfo;
import com.ruoyi.common.redis.service.RedisService;
import com.ruoyi.common.security.annotation.InnerAuth;
import com.ruoyi.system.api.domain.SysUser;
import com.ruoyi.system.api.model.LoginUser;
import com.ruoyi.system.service.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.*;
/**
* 用户信息
*
* @author ruoyi
*/
@RestController
@RequestMapping("/test")
public class SysTestController extends BaseController
{
@Autowired
private ISysUserService userService;
@Autowired
private ISysPermissionService permissionService;
@Autowired
private RedisService redisService;
/**
* 获取当前用户信息
*/
// @InnerAuth 从页面访问,需要去掉 @InnerAuth,否则会报没有权限。
@GetMapping("/cache/{name}")
public TableDataInfo cache(@PathVariable("name") String name)
{
Collection<String> keys = redisService.keys(name+"*");
List<String> list = new ArrayList<>(keys);
return getDataTable(list);
}
@GetMapping("/cache/key/{name}")
public AjaxResult cacheValue(@PathVariable("name") String name)
{
Object o =redisService.getCacheObject(name);
String jsonStr = JSON.toJSONString(o);
return success(jsonStr);
}
/**
* 获取当前用户信息
*/
@InnerAuth
@GetMapping("/info/{username}")
public R<LoginUser> info(@PathVariable("username") String username)
{
SysUser sysUser = userService.selectUserByUserName(username);
if (StringUtils.isNull(sysUser))
{
return R.fail("用户名或密码错误");
}
// 角色集合
Set<String> roles = permissionService.getRolePermission(sysUser);
// 权限集合
Set<String> permissions = permissionService.getMenuPermission(sysUser);
LoginUser sysUserVo = new LoginUser();
sysUserVo.setSysUser(sysUser);
sysUserVo.setRoles(roles);
sysUserVo.setPermissions(permissions);
return R.ok(sysUserVo);
}
}
六、接口测试验证
启动后端服务后,通过浏览器或 Postman 访问以下接口进行验证。
6.1 查询缓存列表
访问 localhost:8080/system/test/cache/login_tokens,返回该缓存前缀下的所有 key 列表:
json
{
"total": 1,
"rows": [
"login_tokens:3a82bda1-0e32-43fc-b490-c3d7c8b9e101"
],
"code": 200,
"msg": "查询成功"
}
6.2 查询缓存内容
访问 localhost:8080/system/test/cache/key/login_tokens:3a82bda1-0e32-43fc-b490-c3d7c8b9e101,返回该 key 对应的 value 内容:
json
{
"msg": "{\"expireTime\":1792504275770,\"ipaddr\":\"127.0.0.1\",\"loginTime\":1792461075770,\"permissions\":[\"*:*:*\"],\"roles\":[\"admin\"],\"sysUser\":{\"admin\":true,\"createBy\":\"admin\",\"createTime\":\"2026-10-17 01:15:17\",\"delFlag\":\"0\",\"dept\":{\"ancestors\":\"0,100,101\",\"children\":[],\"deptId\":103,\"deptName\":\"研发部门\",\"leader\":\"若依\",\"orderNum\":1,\"params\":{\"@type\":\"java.util.HashMap\"},\"parentId\":101,\"status\":\"0\"},\"deptId\":103,\"email\":\"ry@163.com\",\"loginDate\":\"2026-10-19 19:37:57\",\"loginIp\":\"127.0.0.1\",\"nickName\":\"若依\",\"params\":{\"@type\":\"java.util.HashMap\"},\"password\":\"$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2\",\"phonenumber\":\"15888888888\",\"pwdUpdateDate\":\"2026-10-16 17:15:17\",\"remark\":\"管理员\",\"roles\":[{\"admin\":true,\"dataScope\":\"1\",\"deptCheckStrictly\":false,\"flag\":false,\"menuCheckStrictly\":false,\"params\":{\"@type\":\"java.util.HashMap\"},\"roleId\":1,\"roleKey\":\"admin\",\"roleName\":\"超级管理员\",\"roleSort\":1,\"status\":\"0\"}],\"sex\":\"1\",\"status\":\"0\",\"userId\":1,\"userName\":\"admin\"},\"token\":\"3a82bda1-0e32-43fc-b490-c3d7c8b9e101\",\"userid\":1,\"username\":\"admin\"}",
"code": 200
}
七、整体效果
完成上述前后端开发后,整体效果如下:
- 左侧「缓存列表」展示所有 Redis 缓存 key,点击某一项后,中间栏加载该缓存下的所有 key 名。
- 中间「key名列表」展示选中缓存下的所有 field,点击某一项后,右侧展示对应的 value 内容。
- 右侧「缓存内容」以格式化 JSON 的形式展示缓存数据,便于开发人员快速定位问题。
- 每项缓存支持「删除」操作,点击后弹出确认框,确认后从 Redis 中删除对应缓存。
该模块通过三级联动的方式,将 Redis 中散落的缓存数据组织成清晰的树形浏览结构。效果图如下
