feat: redis支持zset、redis数据操作界面优化

This commit is contained in:
meilin.huang
2023-04-16 00:50:36 +08:00
parent 1d858118d5
commit af55193591
40 changed files with 2362 additions and 1332 deletions

View File

@@ -23,13 +23,13 @@
"monaco-sql-languages": "^0.11.0",
"monaco-themes": "^0.4.2",
"nprogress": "^0.2.0",
"pinia": "^2.0.33",
"screenfull": "^6.0.2",
"sortablejs": "^1.13.0",
"sql-formatter": "^12.1.2",
"vue": "^3.2.47",
"vue-clipboard3": "^1.0.1",
"vue-router": "^4.1.6",
"pinia": "^2.0.33",
"xterm": "^5.1.0",
"xterm-addon-fit": "^0.7.0"
},

View File

@@ -11,7 +11,7 @@ const config = {
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
// 系统版本
version: 'v1.4.1'
version: 'v1.4.2'
}
export default config

View File

@@ -1,6 +1,6 @@
<template>
<div class="monaco-editor" style="border: 1px solid #ccc;">
<div ref="monacoTextarea" :style="{ height: height }"></div>
<div class="monaco-editor-content" ref="monacoTextarea" :style="{ height: height }"></div>
<el-select v-if="canChangeMode" class="code-mode-select" v-model="languageMode" @change="changeLanguage">
<el-option v-for="mode in languages" :key="mode.value" :label="mode.label" :value="mode.value"> </el-option>
</el-select>

View File

@@ -244,6 +244,7 @@ export async function initRouter() {
}
let SysWs: any;
let loadRouter = false;
// 路由加载前
router.beforeEach(async (to, from, next) => {
@@ -283,8 +284,10 @@ router.beforeEach(async (to, from, next) => {
if (!SysWs && to.path != '/machine/terminal') {
SysWs = sockets.sysMsgSocket();
}
if (useRoutesList().routesList.length == 0) {
// 不存在路由避免刷新页面找不到路由并且未加载过避免token过期导致获取权限接口报权限不足无限获取则重新初始化路由
if (useRoutesList().routesList.length == 0 && !loadRouter) {
await initRouter();
loadRouter = true;
next({ path: to.path, query: to.query });
} else {
next();

View File

@@ -328,6 +328,10 @@ body,
float: left;
}
.fr {
float: right;
}
.search-form {
.el-form-item {
margin-bottom: 3px;

View File

@@ -46,26 +46,16 @@
<el-form-item>
<el-button @click="searchKey()" type="success" icon="search" plain></el-button>
<el-button @click="scan()" icon="bottom" plain>scan</el-button>
<el-popover placement="right" :width="200" trigger="click">
<template #reference>
<el-button type="primary" icon="plus" plain v-auth="'redis:data:save'"></el-button>
</template>
<el-tag @click="onAddData('string')" :color="getTypeColor('string')"
style="cursor: pointer">string</el-tag>
<el-tag @click="onAddData('hash')" :color="getTypeColor('hash')" class="ml5"
style="cursor: pointer">hash</el-tag>
<el-tag @click="onAddData('set')" :color="getTypeColor('set')" class="ml5"
style="cursor: pointer">set</el-tag>
<!-- <el-tag @click="onAddData('list')" :color="getTypeColor('list')" class="ml5" style="cursor: pointer">list</el-tag> -->
</el-popover>
<el-button @click="showNewKeyDialog" type="primary" icon="plus" plain
v-auth="'redis:data:save'"></el-button>
</el-form-item>
<div style="float: right">
<span>keys: {{ state.dbsize }}</span>
</div>
</el-form>
</el-col>
<el-table v-loading="state.loading" :data="state.keys" :height="tableHeight" stripe :highlight-current-row="true"
style="cursor: pointer">
<el-table v-loading="state.loading" :data="state.keys" :height="tableHeight" stripe
:highlight-current-row="true" style="cursor: pointer">
<el-table-column show-overflow-tooltip prop="key" label="key"></el-table-column>
<el-table-column prop="type" label="type" width="80">
<template #default="scope">
@@ -79,7 +69,8 @@
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button @click="getValue(scope.row)" type="success" icon="search" plain size="small">查看
<el-button @click="showKeyDetail(scope.row)" type="success" icon="search" plain
size="small">查看
</el-button>
<el-button v-auth="'redis:data:del'" @click="del(scope.row.key)" type="danger" icon="delete"
plain size="small">删除
@@ -93,21 +84,37 @@
<div style="text-align: center; margin-top: 10px"></div>
<hash-value v-model:visible="hashValueDialog.visible" :operationType="dataEdit.operationType"
:title="dataEdit.title" :keyInfo="dataEdit.keyInfo" :redisId="scanParam.id" :db="scanParam.db"
@cancel="onCancelDataEdit" @valChange="searchKey" />
<el-dialog title="Key详情" v-model="keyDetailDialog.visible" width="800px" :destroy-on-close="true"
:close-on-click-modal="false">
<key-detail :redisId="scanParam.id" :db="scanParam.db" :key-info="keyDetailDialog.keyInfo"
@change-key="searchKey()" />
</el-dialog>
<string-value v-model:visible="stringValueDialog.visible" :operationType="dataEdit.operationType"
:title="dataEdit.title" :keyInfo="dataEdit.keyInfo" :redisId="scanParam.id" :db="scanParam.db"
@cancel="onCancelDataEdit" @valChange="searchKey" />
<el-dialog title="新增Key" v-model="newKeyDialog.visible" width="500px" :destroy-on-close="true"
:close-on-click-modal="false">
<el-form ref="keyForm" label-width="50px">
<el-form-item prop="key" label="键名">
<el-input v-model.trim="keyDetailDialog.keyInfo.key" placeholder="请输入键名"></el-input>
</el-form-item>
<el-form-item prop="type" label="类型">
<el-select v-model="keyDetailDialog.keyInfo.type" default-first-option style="width: 100%"
placeholder="请选择类型">
<el-option key="string" label="string" value="string"></el-option>
<el-option key="hash" label="hash" value="hash"></el-option>
<el-option key="set" label="set" value="set"></el-option>
<el-option key="zset" label="zset" value="zset"></el-option>
<el-option key="list" label="list" value="list"></el-option>
</el-select>
</el-form-item>
</el-form>
<set-value v-model:visible="setValueDialog.visible" :title="dataEdit.title" :keyInfo="dataEdit.keyInfo"
:redisId="scanParam.id" :db="scanParam.db" :operationType="dataEdit.operationType" @valChange="searchKey"
@cancel="onCancelDataEdit" />
<list-value v-model:visible="listValueDialog.visible" :title="dataEdit.title" :keyInfo="dataEdit.keyInfo"
:redisId="scanParam.id" :db="scanParam.db" :operationType="dataEdit.operationType" @valChange="searchKey"
@cancel="onCancelDataEdit" />
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelNewKey()"> </el-button>
<el-button v-auth="'machine:script:save'" type="primary" @click="newKey"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
@@ -119,10 +126,7 @@ import { isTrue, notBlank, notNull } from '@/common/assert';
import { TagTreeNode } from '../component/tag';
import TagTree from '../component/TagTree.vue';
const HashValue = defineAsyncComponent(() => import('./HashValue.vue'));
const StringValue = defineAsyncComponent(() => import('./StringValue.vue'));
const SetValue = defineAsyncComponent(() => import('./SetValue.vue'));
const ListValue = defineAsyncComponent(() => import('./ListValue.vue'));
const KeyDetail = defineAsyncComponent(() => import('./KeyDetail.vue'));
/**
* 树节点类型
@@ -144,31 +148,20 @@ const state = reactive({
scanParam: {
id: null as any,
mode: '',
db: '',
db: 0,
match: null,
count: 10,
cursor: {},
},
dataEdit: {
keyDetailDialog: {
visible: false,
title: '新增数据',
operationType: 1,
keyInfo: {
type: 'string',
timed: -1,
key: '',
},
},
hashValueDialog: {
visible: false,
},
stringValueDialog: {
visible: false,
},
setValueDialog: {
visible: false,
},
listValueDialog: {
newKeyDialog: {
visible: false,
},
keys: [],
@@ -178,11 +171,8 @@ const state = reactive({
const {
tableHeight,
scanParam,
dataEdit,
hashValueDialog,
stringValueDialog,
setValueDialog,
listValueDialog,
keyDetailDialog,
newKeyDialog,
} = toRefs(state)
@@ -339,50 +329,56 @@ const resetScanParam = () => {
state.scanParam.cursor = {};
};
const getValue = async (row: any) => {
const showKeyDetail = async (row: any) => {
const type = row.type;
state.dataEdit.keyInfo.type = type;
state.dataEdit.keyInfo.timed = row.ttl;
state.dataEdit.keyInfo.key = row.key;
state.dataEdit.operationType = 2;
state.dataEdit.title = '查看数据';
if (type == 'hash') {
state.hashValueDialog.visible = true;
} else if (type == 'string') {
state.stringValueDialog.visible = true;
} else if (type == 'set') {
state.setValueDialog.visible = true;
} else if (type == 'list') {
state.listValueDialog.visible = true;
} else {
ElMessage.warning('暂不支持该类型');
}
state.keyDetailDialog.keyInfo.type = type;
state.keyDetailDialog.keyInfo.timed = row.ttl;
state.keyDetailDialog.keyInfo.key = row.key;
state.keyDetailDialog.visible = true;
};
const onAddData = (type: string) => {
const closeKeyDetail = () => {
// state.keyDetailDialog.visible = false;
}
const showNewKeyDialog = () => {
notNull(state.scanParam.id, '请先选择redis');
state.dataEdit.operationType = 1;
state.dataEdit.title = '新增数据';
state.dataEdit.keyInfo.type = type;
state.dataEdit.keyInfo.timed = -1;
if (type == 'hash') {
state.hashValueDialog.visible = true;
} else if (type == 'string') {
state.stringValueDialog.visible = true;
} else if (type == 'set') {
state.setValueDialog.visible = true;
} else if (type == 'list') {
state.listValueDialog.visible = true;
} else {
ElMessage.warning('暂不支持该类型');
}
};
notNull(state.scanParam.db, "请选择要操作的库")
resetKeyDetailInfo();
state.newKeyDialog.visible = true;
}
const onCancelDataEdit = () => {
state.dataEdit.keyInfo = {} as any;
};
const cancelNewKey = () => {
resetKeyDetailInfo();
state.newKeyDialog.visible = false;
}
const newKey = async () => {
const keyInfo = state.keyDetailDialog.keyInfo
const keyType = keyInfo.type
const key = keyInfo.key;
notBlank(key, "键名不能为空");
if (keyType == 'string') {
await redisApi.setString.request({
id: state.scanParam.id,
db: state.scanParam.db,
key: key,
value: '',
})
}
state.newKeyDialog.visible = false;
state.keyDetailDialog.visible = true;
searchKey();
}
const resetKeyDetailInfo = () => {
state.keyDetailDialog.keyInfo.key = '';
state.keyDetailDialog.keyInfo.type = 'string';
state.keyDetailDialog.keyInfo.timed = -1;
}
const del = (key: string) => {
ElMessageBox.confirm(`确定删除[ ${key} ] 该key?`, '提示', {

View File

@@ -1,108 +0,0 @@
<template>
<div style="width: 100%;">
<el-input @input="onInput" type="textarea" v-model="modelValue" :autosize="autosize" :rows="rows" />
<div style="padding: 3px; float: right" class="mr5 format-btns">
<div>
<el-button @click="showFormatDialog()" :underline="false" type="success" icon="MagicStick" size="small">
</el-button>
</div>
</div>
<el-dialog @opened="opened" width="60%" :title="title" v-model="formatDialog.visible"
:close-on-click-modal="false">
<monaco-editor ref="monacoEditorRef" :canChangeMode="true" v-model="formatDialog.value" language="json" />
<template #footer>
<div>
<el-button @click="formatDialog.visible = false"> </el-button>
<el-button @click="onConfirmValue" type="primary"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, watch, toRefs, onMounted } from 'vue';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
const props = defineProps({
title: {
type: String,
},
modelValue: {
type: String,
},
rows: {
type: Number,
},
autosize: {
type: Object
}
})
const emit = defineEmits(['update:modelValue'])
const monacoEditorRef: any = ref(null)
const state = reactive({
rows: 2,
autosize: {},
modelValue: '',
formatDialog: {
visible: false,
value: '',
}
});
const {
rows,
autosize,
modelValue,
formatDialog,
} = toRefs(state)
watch(
() => props.modelValue,
(val: any) => {
state.modelValue = val;
}
);
onMounted(() => {
state.modelValue = props.modelValue as any;
state.autosize = props.autosize as any;
state.rows = props.rows as any;
})
const showFormatDialog = () => {
state.formatDialog.visible = true;
state.formatDialog.value = state.modelValue;
}
const opened = () => {
monacoEditorRef.value.format();
};
const onConfirmValue = () => {
// 尝试压缩json
try {
state.modelValue = JSON.stringify(JSON.parse(state.formatDialog.value));
} catch (e) {
state.modelValue = state.formatDialog.value;
}
emit('update:modelValue', state.modelValue);
state.formatDialog.visible = false;
}
const onInput = (value: any) => {
emit('update:modelValue', value);
}
</script>
<style lang="scss">
.format-btns {
position: absolute;
z-index: 2;
right: 5px;
top: 4px;
max-width: 120px;
}
</style>

View File

@@ -0,0 +1,104 @@
<template>
<div class="format-viewer-container">
<div class="mb5 fr">
<el-select v-model="selectedView" class='format-selector' size='mini' placeholder='Text'>
<template #prefix>
<SvgIcon name="view" />
</template>
<el-option v-for="item of Object.keys(viewers)" :key="item" :label="item" :value="item">
</el-option>
</el-select>
</div>
<component ref='viewerRef' :is='components[viewerComponent]' :content='state.content' :name="selectedView">
</component>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, shallowReactive, watch, toRefs, onMounted } from 'vue';
import ViewerText from './ViewerText.vue';
import ViewerJson from './ViewerJson.vue';
const props = defineProps({
content: {
type: String,
},
})
const components = shallowReactive({
ViewerText, ViewerJson
})
const viewerRef: any = ref(null)
const state = reactive({
content: '',
selectedView: 'Text',
});
const viewers = {
"Text": {
value: 'ViewerText',
},
"Json": {
value: 'ViewerJson',
}
}
const {
selectedView,
} = toRefs(state)
const viewerComponent = computed(() => {
return viewers[state.selectedView].value;
});
watch(
() => props.content,
(val: any) => {
state.content = val;
}
);
onMounted(() => {
state.content = props.content as any;
})
const getContent = () => {
return viewerRef.value.getContent();
}
defineExpose({ getContent })
</script>
<style lang="scss">
.format-selector {
width: 130px;
}
.format-selector .el-input__inner {
height: 22px !important;
}
/*outline same with text viewer's .el-textarea__inner*/
.format-viewer-container .text-formated-container {
border: 1px solid #dcdfe6;
padding: 5px 10px;
border-radius: 4px;
clear: both
}
.format-viewer-container .formater-binary-tag {
font-size: 80%;
}
// 默认文本框样式
.format-viewer-container .el-textarea textarea {
font-size: 14px;
height: calc(100vh - 536px);
}
.format-viewer-container .monaco-editor-content {
height: calc(100vh - 550px) !important;
}
</style>

View File

@@ -1,266 +0,0 @@
<template>
<el-dialog class="el-table-z-index-inherit" :title="title" v-model="dialogVisible" :before-close="cancel" width="800px" :destroy-on-close="true">
<el-form label-width="85px">
<el-form-item prop="key" label="key:">
<el-input :disabled="operationType == 2" v-model="key.key"></el-input>
</el-form-item>
<el-form-item prop="timed" label="过期时间:">
<el-input v-model.number="key.timed" type="number"></el-input>
</el-form-item>
<el-form-item prop="dataType" label="数据类型:">
<el-input v-model="key.type" disabled></el-input>
</el-form-item>
<el-row class="mt10">
<el-form label-position="right" :inline="true">
<el-form-item label="field" label-width="40px" v-if="operationType == 2">
<el-input placeholder="支持*模糊field" style="width: 140px" v-model="scanParam.match" clearable
size="small"></el-input>
</el-form-item>
<el-form-item label="count" v-if="operationType == 2">
<el-input placeholder="count" style="width: 62px" v-model.number="scanParam.count" size="small">
</el-input>
</el-form-item>
<el-form-item>
<el-button v-if="operationType == 2" @click="reHscan()" type="success" icon="search" plain
size="small"></el-button>
<el-button v-if="operationType == 2" @click="hscan()" icon="bottom" plain size="small">scan
</el-button>
<el-button @click="onAddHashValue" icon="plus" size="small" plain>添加</el-button>
</el-form-item>
<div v-if="operationType == 2" class="mt10" style="float: right">
<span>fieldSize: {{ keySize }}</span>
</div>
</el-form>
</el-row>
<el-table :data="hashValues" stripe style="width: 100%;">
<el-table-column prop="field" label="field" width>
<template #default="scope">
<el-input v-model="scope.row.field" clearable size="small"></el-input>
</template>
</el-table-column>
<el-table-column prop="value" label="value" min-width="200">
<template #default="scope">
<format-input :title="`type:【${key.type}】key:【${key.key}】field:【${scope.row.field}】`" v-model="scope.row.value"
:autosize="{ minRows: 2, maxRows: 10 }" size="small"></format-input>
</template>
</el-table-column>
<el-table-column label="操作" width="120">
<template #default="scope">
<el-button v-if="operationType == 2" type="success" @click="hset(scope.row)" icon="check"
size="small" plain></el-button>
<el-button v-auth="'redis:data:del'" type="danger" @click="hdel(scope.row.field, scope.$index)" icon="delete" size="small"
plain></el-button>
</template>
</el-table-column>
</el-table>
</el-form>
<template #footer v-if="operationType == 1">
<div class="dialog-footer">
<el-button @click="cancel()"> </el-button>
<el-button @click="saveValue" type="primary" v-auth="'redis:data:save'"> </el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { reactive, watch, toRefs } from 'vue';
import { redisApi } from './api';
import { ElMessage, ElMessageBox } from 'element-plus';
import { isTrue, notEmpty } from '@/common/assert';
import FormatInput from './FormatInput.vue';
const props = defineProps({
visible: {
type: Boolean,
},
title: {
type: String,
},
// 操作类型1新增2修改
operationType: {
type: [Number],
require: true,
},
redisId: {
type: [Number],
require: true,
},
db: {
type: [String],
require: true,
},
keyInfo: {
type: [Object],
},
hashValue: {
type: [Array, Object],
},
})
const emit = defineEmits(['update:visible', 'cancel', 'valChange'])
const state = reactive({
dialogVisible: false,
operationType: 1,
redisId: 0,
db: '0',
key: {
key: '',
type: 'hash',
timed: -1,
},
scanParam: {
key: '',
id: 0,
db: '0',
cursor: 0,
match: '',
count: 10,
},
keySize: 0,
hashValues: [
{
field: '',
value: '',
},
],
});
const {
dialogVisible,
operationType,
key,
scanParam,
keySize,
hashValues,
} = toRefs(state)
const cancel = () => {
emit('update:visible', false);
emit('cancel');
setTimeout(() => {
state.hashValues = [];
state.key = {} as any;
}, 500);
};
watch(props, async (newValue: any) => {
const visible = newValue.visible;
state.redisId = newValue.redisId;
state.db = newValue.db;
state.key = newValue.keyInfo;
state.operationType = newValue.operationType;
if (visible && state.operationType == 2) {
state.scanParam.id = props.redisId as any;
state.scanParam.key = state.key.key;
await reHscan();
}
state.dialogVisible = visible;
});
const reHscan = async () => {
state.scanParam.id = state.redisId;
state.scanParam.db = state.db;
state.scanParam.cursor = 0;
hscan();
};
const hscan = async () => {
const match = state.scanParam.match;
if (!match || match == '' || match == '*') {
if (state.scanParam.count > 100) {
ElMessage.error('match为空或者*时, count不能超过100');
return;
}
} else {
if (state.scanParam.count > 1000) {
ElMessage.error('count不能超过1000');
return;
}
}
const scanRes = await redisApi.hscan.request(state.scanParam);
state.scanParam.cursor = scanRes.cursor;
state.keySize = scanRes.keySize;
const keys = scanRes.keys;
const hashValue = [];
const fieldCount = keys.length / 2;
let nextFieldIndex = 0;
for (let i = 0; i < fieldCount; i++) {
hashValue.push({ field: keys[nextFieldIndex++], value: keys[nextFieldIndex++] });
}
state.hashValues = hashValue;
};
const hdel = async (field: any, index: any) => {
// 如果是新增操作,则直接数组移除即可
if (state.operationType == 1) {
state.hashValues.splice(index, 1);
return;
}
await ElMessageBox.confirm(`确定删除[${field}]?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await redisApi.hdel.request({
id: state.redisId,
db: state.db,
key: state.key.key,
field,
});
ElMessage.success('删除成功');
reHscan();
};
const hset = async (row: any) => {
await redisApi.saveHashValue.request({
id: state.redisId,
db: state.db,
key: state.key.key,
timed: state.key.timed,
value: [
{
field: row.field,
value: row.value,
},
],
});
ElMessage.success('保存成功');
};
const onAddHashValue = () => {
state.hashValues.unshift({ field: '', value: '' });
};
const saveValue = async () => {
notEmpty(state.key.key, 'key不能为空');
isTrue(state.hashValues.length > 0, 'hash内容不能为空');
const sv = { value: state.hashValues, id: state.redisId, db: state.db };
Object.assign(sv, state.key);
await redisApi.saveHashValue.request(sv);
ElMessage.success('保存成功');
cancel();
emit('valChange');
};
</script>
<style lang="scss">
#string-value-text {
flex-grow: 1;
display: flex;
position: relative;
.text-type-select {
position: absolute;
z-index: 2;
right: 10px;
top: 10px;
max-width: 70px;
}
}
</style>

View File

@@ -0,0 +1,136 @@
<template>
<div>
<el-container direction="vertical" class="key-tab-container">
<!-- key info -->
<key-header ref="keyHeader" :redis-id="redisId" :db="db" :key-info="keyInfo" @refresh-content="refreshContent"
@change-key="changeKey" class="key-header-info">
</key-header>
<!-- key content -->
<component ref="keyValueRef" :is="components[componentName]" :redis-id="redisId" :db="db" :key-info="keyInfo">
</component>
</el-container>
</div>
</template>
<script lang="ts" setup>
import { defineAsyncComponent, ref, shallowReactive, reactive, computed, toRefs } from 'vue';
import { ElMessage } from 'element-plus';
import KeyHeader from './KeyHeader.vue'
const KeyValueString = defineAsyncComponent(() => import('./KeyValueString.vue'));
const KeyValueHash = defineAsyncComponent(() => import('./KeyValueHash.vue'));
const KeyValueSet = defineAsyncComponent(() => import('./KeyValueSet.vue'));
const KeyValueList = defineAsyncComponent(() => import('./KeyValueList.vue'));
const KeyValueZset = defineAsyncComponent(() => import('./KeyValueZset.vue'));
const components = shallowReactive({
KeyValueString, KeyValueHash, KeyValueSet, KeyValueList, KeyValueZset
})
const keyValueRef = ref(null) as any
const props = defineProps({
redisId: {
type: Number
},
db: {
type: Number
},
keyInfo: {
type: [Object],
},
})
const emit = defineEmits(['update:visible', 'changeKey', 'valChange'])
const state = reactive({
redisId: 0,
});
const componentMap = {
string: 'KeyValueString',
hash: 'KeyValueHash',
zset: 'KeyValueZset',
set: 'KeyValueSet',
list: 'KeyValueList',
};
const componentName = computed(() => {
const component = componentMap[props.keyInfo?.type]
if (!component) {
ElMessage.error("暂不支持该类型")
return ''
}
return component;
});
const refreshContent = () => {
keyValueRef.value?.initData();
}
const changeKey = () => {
emit('changeKey');
}
const {
} = toRefs(state)
// watch(
// () => props.keyInfo,
// (val) => {
// state.keyInfo = val;
// }
// );
</script>
<style lang="scss">
.key-tab-container {
/*padding-left: 5px;*/
}
.key-header-info {
// margin-top: 15px;
}
.key-content-container {
margin-top: 15px;
}
// .key-detail-filter-value {
// width: 90%;
// height: 24px;
// padding: 0 5px;
// }
/*tooltip in table width limit*/
.el-tooltip__popper {
max-width: 50%;
}
.content-more-container {
text-align: center;
margin-top: 10px;
}
.content-more-container .content-more-btn {
width: 95%;
padding-top: 5px;
padding-bottom: 5px;
}
/*data table list styles*/
.key-content-container .el-table {
border-radius: 3px;
}
/*table list height*/
.key-content-container .el-table .el-table__body td {
padding: 0px 0px;
}
/*table list border*/
.key-content-container .el-table--border td,
.key-content-container .el-table--border th {
border-right-width: 0;
}
</style>

View File

@@ -0,0 +1,200 @@
<template>
<div>
<!-- key name -->
<div class="key-header-item key-name-input">
<el-input ref="keyNameInput" v-model="keyInfo.key" title="点击重命名" placeholder="KeyName">
<template #prepend>
<span class="key-detail-type">{{ keyInfo.type }}</span>
</template>
<template #suffix>
<SvgIcon v-auth="'redis:data:save'" @click="renameKey" title="点击重命名" name="check"
class="cursor-pointer" />
</template>
</el-input>
</div>
<!-- key ttl -->
<div class="key-header-item key-ttl-input">
<el-input type="number" v-model.number="keyInfo.timed" placeholder="单位(秒),负数永久" title="点击修改过期时间">
<template #prepend>
<span slot="prepend">TTL</span>
</template>
<template #suffix>
<!-- save ttl -->
<SvgIcon v-auth="'redis:data:save'" @click="ttlKey" title="点击修改过期时间" name="check" />
</template>
</el-input>
</div>
<!-- del & refresh btn -->
<div class='key-header-item key-header-btn-con'>
<el-button slot="reference" ref='refreshBtn' type="success" @click="refreshKey" icon="refresh"
title="刷新"></el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive, watch, toRefs, onMounted } from 'vue';
import { redisApi } from './api';
import { ElMessage, ElMessageBox } from 'element-plus';
const props = defineProps({
redisId: {
type: Number
},
db: {
type: Number
},
keyInfo: {
type: [Object],
},
})
const emit = defineEmits(['refreshContent', 'changeKey', 'valChange'])
const state = reactive({
redisId: 0,
keyInfo: {
key: '',
type: '',
timed: -1,
} as any,
oldKey: '',
});
onMounted(() => {
state.keyInfo = props.keyInfo
state.oldKey = props.keyInfo?.key
})
const refreshKey = async () => {
const ttl = await redisApi.keyTtl.request({
id: props.redisId,
db: props.db,
key: state.oldKey,
})
state.keyInfo.timed = ttl;
emit('refreshContent');
}
const renameKey = async () => {
if (!state.oldKey || state.keyInfo.key == state.oldKey) {
return;
}
await redisApi.renameKey.request({
id: props.redisId,
db: props.db,
newKey: state.keyInfo.key,
key: state.oldKey
});
ElMessage.success("设置成功")
emit('changeKey');
}
const ttlKey = async () => {
if (!state.oldKey) {
return;
}
// ttl <= 0则持久化该key
if (state.keyInfo.timed <= 0) {
try {
await ElMessageBox.confirm(
'确定持久化该key?',
'Warning',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
)
} catch (err) {
return;
}
await persistKey();
state.keyInfo.timed = -1;
return
}
await redisApi.expireKey.request({
id: props.redisId,
db: props.db,
key: state.keyInfo.key,
seconds: state.keyInfo.timed
});
ElMessage.success("设置成功")
emit('changeKey');
}
const persistKey = async () => {
await redisApi.persistKey.request({
id: props.redisId,
db: props.db,
key: state.keyInfo.key,
});
ElMessage.success("设置成功")
emit('changeKey');
}
const {
keyInfo,
oldKey,
} = toRefs(state)
// watch(
// () => props.keyInfo,
// (val) => {
// state.keyInfo = val;
// state.keyName = state.keyInfo.key;
// }
// );
</script>
<style lang="scss">
.key-detail-type {
text-transform: capitalize;
text-align: center;
min-width: 34px;
display: inline-block;
}
.cursor-pointer {
cursor: pointer;
}
.key-header-item {
/*padding-right: 15px;*/
/*margin-bottom: 10px;*/
float: left;
}
.key-header-item.key-name-input {
width: calc(100% - 332px);
min-width: 220px;
max-width: 800px;
margin-right: 15px;
margin-bottom: 10px;
}
.key-header-item.key-ttl-input {
width: 220px;
margin-right: 15px;
margin-bottom: 10px;
}
/*hide number input button*/
.key-header-item.key-ttl-input input::-webkit-inner-spin-button,
.key-header-item.key-ttl-input input::-webkit-outer-spin-button {
appearance: none;
}
.key-header-item.key-header-btn-con .el-button+.el-button {
margin-left: 4px;
}
/*refresh btn rotating*/
.key-header-info .key-header-btn-con .rotating .el-icon-refresh {
animation: rotate 1.5s linear infinite;
}
</style>

View File

@@ -0,0 +1,241 @@
<template>
<div>
<el-button @click="showEditDialog(null)" icon="plus" size="small" plain type="primary" class="mb10">添加新行</el-button>
<el-table size="small" border :data="hashValues" min-height=300 stripe>
<el-table-column type="index" :label="'ID (Total: ' + total + ')'" sortable width="100">
</el-table-column>
<el-table-column resizable sortable prop="field" label="field" show-overflow-tooltip min-width="100">
</el-table-column>
<el-table-column resizable sortable prop="value" label="value" show-overflow-tooltip min-width="200">
</el-table-column>
<el-table-column label="操作">
<template #header>
<el-input class="key-detail-filter-value" v-model="state.filterValue" @keyup.enter='hscan(true, true)'
placeholder="输入关键词回车搜索" clearable size="small" />
</template>
<template #default="scope">
<el-link @click="showEditDialog(scope.row)" :underline="false" type="primary" icon="edit"
plain></el-link>
<el-popconfirm title="确定删除?" @confirm="hdel(scope.row.field, scope.$index)">
<template #reference>
<el-link v-auth="'redis:data:del'" :underline="false" type="danger" icon="delete" size="small"
plain class="ml5"></el-link>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<!-- load more content -->
<div class='content-more-container'>
<el-button size='small' @click='hscan()' :disabled='loadMoreDisable' class='content-more-btn'>
加载更多
</el-button>
</div>
<el-dialog title="添加新行" v-model="editDialog.visible" width="600px" :destroy-on-close="true"
:close-on-click-modal="false">
<el-form>
<el-form-item>
<el-input v-model="editDialog.field" placeholder="field" />
</el-form-item>
<el-form-item>
<format-viewer class="w100" ref="formatViewerRef" :content="editDialog.value"></format-viewer>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="editDialog.visible = false"> </el-button>
<el-button v-auth="'redis:data:save'" type="primary" @click="confirmEditData"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, reactive, watch, toRefs } from 'vue';
import { redisApi } from './api';
import { ElMessage } from 'element-plus';
import { notBlank } from '@/common/assert';
import FormatViewer from './FormatViewer.vue';
const props = defineProps({
redisId: {
type: [Number],
require: true,
default: 0
},
db: {
type: [Number],
require: true,
default: 0
},
keyInfo: {
type: [Object],
},
})
const formatViewerRef = ref(null) as any;
const state = reactive({
redisId: 0,
db: 0,
key: '',
scanParam: {
cursor: 0,
count: 50,
},
filterValue: '',
hashValues: [] as any,
total: 0,
loadMoreDisable: false,
editDialog: {
visible: false,
field: '',
value: '',
dataRow: null as any,
},
});
const {
hashValues,
total,
loadMoreDisable,
editDialog,
} = toRefs(state)
onMounted(() => {
state.redisId = props.redisId;
state.db = props.db;
state.key = props.keyInfo?.key;
initData();
})
const initData = () => {
state.filterValue = '';
hscan(true, true);
}
const getScanMatch = () => {
return state.filterValue ? `*${state.filterValue}*` : '*';
}
const hscan = async (resetTableData = false, resetCursor = false) => {
if (resetCursor) {
state.scanParam.cursor = 0;
}
const scanRes = await redisApi.hscan.request({
...getBaseReqParam(),
match: getScanMatch(),
...state.scanParam
});
state.scanParam.cursor = scanRes.cursor;
state.loadMoreDisable = scanRes.cursor == 0
state.total = scanRes.keySize;
const keys = scanRes.keys;
const hashValue = [];
const fieldCount = keys.length / 2;
let nextFieldIndex = 0;
for (let i = 0; i < fieldCount; i++) {
hashValue.push({ field: keys[nextFieldIndex++], value: keys[nextFieldIndex++] });
}
if (resetTableData) {
state.hashValues = hashValue;
} else {
state.hashValues.push(...hashValue)
}
};
const hdel = async (field: any, index: any) => {
await redisApi.hdel.request({
...getBaseReqParam(),
field,
});
ElMessage.success('删除成功');
state.hashValues.splice(index, 1)
state.total--;
};
const showEditDialog = (row: any) => {
state.editDialog.dataRow = row;
state.editDialog.field = row ? row.field : '';
state.editDialog.value = row ? row.value : '';
state.editDialog.visible = true;
}
const confirmEditData = async () => {
const param = getBaseReqParam();
const field = state.editDialog.field;
notBlank(field, "field不能为空");
// 存在数据行,则说明为修改,则要先删除旧数据后新增
const dataRow = state.editDialog.dataRow
if (dataRow) {
await redisApi.hdel.request({
...param,
field: dataRow.field,
});
}
// 获取hash value内容并新增
const value = formatViewerRef.value.getContent()
const res = await redisApi.hset.request({
...param,
value: [
{
field,
value: value,
},
],
});
ElMessage.success("保存成功");
if (dataRow) {
state.editDialog.dataRow.value = value;
state.editDialog.dataRow.field = field;
} else {
// 响应0则为被覆盖则重新scan
if (res == 0) {
hscan(true, true);
} else {
state.hashValues.unshift({ value, field });
state.total++;
}
}
state.editDialog.visible = false;
state.editDialog.dataRow = null;
}
const getBaseReqParam = () => {
return {
id: state.redisId,
db: state.db,
key: state.key
}
}
defineExpose({ initData })
</script>
<style lang="scss">
#string-value-text {
flex-grow: 1;
display: flex;
position: relative;
.text-type-select {
position: absolute;
z-index: 2;
right: 10px;
top: 10px;
max-width: 70px;
}
}
</style>

View File

@@ -0,0 +1,199 @@
<template>
<div>
<el-button @click="showEditDialog(null)" icon="plus" size="small" plain type="primary" class="mb10">添加新行</el-button>
<el-table size="small" border :data="values" min-height=300 stripe>
<el-table-column type="index" :label="'ID (Total: ' + total + ')'" sortable width="100">
</el-table-column>
<el-table-column resizable sortable prop="value" label="value" show-overflow-tooltip min-width="200">
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-link @click="showEditDialog(scope.row)" :underline="false" type="primary" icon="edit"
plain></el-link>
<el-popconfirm title="确定删除?" @confirm="lrem(scope.row, scope.$index)">
<template #reference>
<el-link v-auth="'redis:data:del'" :underline="false" type="danger" icon="delete" size="small"
plain class="ml5"></el-link>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<!-- load more content -->
<div class='content-more-container'>
<el-button size='small' @click='getListValue(false)' :disabled='loadMoreDisable' class='content-more-btn'>
加载更多
</el-button>
</div>
<el-dialog title="添加新行" v-model="editDialog.visible" width="600px" :destroy-on-close="true"
:close-on-click-modal="false">
<el-form>
<el-form-item>
<format-viewer class="w100" ref="formatViewerRef" :content="editDialog.content"></format-viewer>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="editDialog.visible = false"> </el-button>
<el-button v-auth="'redis:data:save'" type="primary" @click="confirmEditData"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, toRefs, onMounted } from 'vue';
import { redisApi } from './api';
import { ElMessage } from 'element-plus';
import FormatViewer from './FormatViewer.vue';
const props = defineProps({
redisId: {
type: [Number],
require: true,
default: 0,
},
db: {
type: [Number],
require: true,
default: 0
},
keyInfo: {
type: [Object],
},
})
const formatViewerRef = ref(null) as any;
const state = reactive({
redisId: 0,
db: 0,
key: '',
pageNum: 1,
pageSize: 50,
total: 0,
values: [] as any,
loadMoreDisable: false,
editDialog: {
visible: false,
content: '',
dataRow: null as any,
}
});
const {
total,
values,
loadMoreDisable,
editDialog,
} = toRefs(state)
onMounted(() => {
state.redisId = props.redisId;
state.db = props.db;
state.key = props.keyInfo?.key;
initData();
})
const initData = () => {
state.pageNum = 1;
getListValue(true);
}
const getListValue = async (resetTableData = false) => {
const pageNum = state.pageNum;
const pageSize = state.pageSize;
const res = await redisApi.getListValue.request({
...getBaseReqParam(),
start: (pageNum - 1) * pageSize,
stop: pageNum * pageSize - 1,
});
state.total = res.len;
const datas = res.list.map((x: any) => {
return {
value: x,
};
});
if (resetTableData) {
state.values = datas;
} else {
state.values.push(...datas)
}
state.pageNum++;
state.loadMoreDisable = state.values.length === state.total
};
const lset = async (row: any, rowIndex: number) => {
await redisApi.setListValue.request({
...getBaseReqParam(),
index: (state.pageNum - 1) * state.pageSize + rowIndex,
value: row.value,
});
ElMessage.success('数据保存成功');
};
const showEditDialog = (row: any) => {
state.editDialog.dataRow = row;
state.editDialog.content = row ? row.value : '';
state.editDialog.visible = true;
}
const confirmEditData = async () => {
const param = getBaseReqParam();
// 存在数据行,则说明为修改,则要先删除旧数据后新增
const dataRow = state.editDialog.dataRow
if (dataRow) {
await redisApi.lrem.request({
member: state.editDialog.dataRow.value,
count: 1,
...param
});
}
// 获取list member内容并新增
const member = formatViewerRef.value.getContent()
await redisApi.saveListValue.request({
value: [member],
...param
});
ElMessage.success("保存成功");
if (dataRow) {
state.editDialog.dataRow.value = member;
} else {
state.values.push({ value: member });
state.total++;
}
state.editDialog.visible = false;
state.editDialog.dataRow = null;
}
const lrem = async (row: any, index: any) => {
await redisApi.lrem.request({
...getBaseReqParam(),
member: row.value,
count: 1,
})
ElMessage.success("删除成功");
state.values.splice(index, 1)
state.total--;
}
const getBaseReqParam = () => {
return {
id: state.redisId,
db: state.db,
key: state.key
}
}
defineExpose({ initData })
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,205 @@
<template>
<div>
<el-button @click="showEditDialog(null)" icon="plus" size="small" plain type="primary" class="mb10">添加新行</el-button>
<el-table size="small" border :data="setDatas" min-height=300 stripe>
<el-table-column type="index" :label="'ID (Total: ' + total + ')'" sortable width="100">
</el-table-column>
<el-table-column resizable sortable prop="value" label="value" show-overflow-tooltip min-width="200">
</el-table-column>
<el-table-column label="操作">
<template #header>
<el-input class="key-detail-filter-value" v-model="state.filterValue"
@keyup.enter='sscanData(true, true)' placeholder="输入关键词回车搜索" clearable size="small" />
</template>
<template #default="scope">
<el-link @click="showEditDialog(scope.row)" :underline="false" type="primary" icon="edit"
plain></el-link>
<el-popconfirm title="确定删除?" @confirm="srem(scope.row, scope.$index)">
<template #reference>
<el-link v-auth="'redis:data:del'" :underline="false" type="danger" icon="delete" size="small"
plain class="ml5"></el-link>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<!-- load more content -->
<div class='content-more-container'>
<el-button size='small' @click='sscanData(false)' :disabled='loadMoreDisable' class='content-more-btn'>
加载更多
</el-button>
</div>
<el-dialog title="添加新行" v-model="editDialog.visible" width="600px" :destroy-on-close="true"
:close-on-click-modal="false">
<el-form>
<el-form-item>
<format-viewer class="w100" ref="formatViewerRef" :content="editDialog.content"></format-viewer>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="editDialog.visible = false"> </el-button>
<el-button v-auth="'redis:data:save'" type="primary" @click="confirmEditData"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, toRefs, onMounted } from 'vue';
import { redisApi } from './api';
import { ElMessage } from 'element-plus';
import FormatViewer from './FormatViewer.vue';
const props = defineProps({
redisId: {
type: [Number],
require: true,
default: 0,
},
db: {
type: [Number],
require: true,
default: 0
},
keyInfo: {
type: [Object],
},
})
const formatViewerRef = ref(null) as any;
const state = reactive({
redisId: 0,
db: 0,
key: '',
filterValue: '',
scanParam: {
count: 50,
cursor: 0,
},
total: 0,
setDatas: [] as any,
loadMoreDisable: false,
value: [{ value: '' }],
editDialog: {
visible: false,
content: '',
dataRow: null as any,
}
});
const {
total,
setDatas,
loadMoreDisable,
editDialog,
} = toRefs(state)
onMounted(() => {
state.redisId = props.redisId;
state.db = props.db;
state.key = props.keyInfo?.key;
initData();
})
const initData = () => {
state.filterValue = '';
sscanData(true, true);
getTotal();
}
const getScanMatch = () => {
return state.filterValue ? `*${state.filterValue}*` : '*';
}
const sscanData = async (resetDatas = true, resetCursor = false) => {
if (resetCursor) {
state.scanParam.cursor = 0;
}
const res = await redisApi.sscan.request({
...getBaseReqParam(),
match: getScanMatch(),
...state.scanParam
});
if (resetDatas) {
state.setDatas = [];
}
res.keys.forEach((x: any) => {
state.setDatas.push({
value: x,
})
})
state.scanParam.cursor = res.cursor;
state.loadMoreDisable = res.cursor == 0
};
const getTotal = () => {
redisApi.scard.request(getBaseReqParam()).then((res) => {
state.total = res;
});
}
const showEditDialog = (row: any) => {
state.editDialog.dataRow = row;
state.editDialog.content = row ? row.value : '';
state.editDialog.visible = true;
}
const confirmEditData = async () => {
const param = getBaseReqParam();
// 存在数据行,则说明为修改,则要先删除旧数据后新增
const dataRow = state.editDialog.dataRow
if (dataRow) {
await redisApi.srem.request({
member: state.editDialog.dataRow.value,
...param
});
}
// 获取set member内容并新增
const member = formatViewerRef.value.getContent()
await redisApi.sadd.request({
member,
...param
});
ElMessage.success("保存成功");
if (dataRow) {
state.editDialog.dataRow.value = member;
} else {
state.setDatas.unshift({ value: member });
state.total++;
}
state.editDialog.visible = false;
state.editDialog.dataRow = null;
}
const srem = async (row: any, index: any) => {
await redisApi.srem.request({
...getBaseReqParam(),
member: row.value,
})
ElMessage.success("删除成功");
state.setDatas.splice(index, 1)
state.total--;
}
const getBaseReqParam = () => {
return {
id: state.redisId,
db: state.db,
key: state.key
}
}
defineExpose({ initData })
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,111 @@
<template>
<div>
<el-form class='key-content-string' label-width="85px">
<div>
<format-viewer ref="formatViewerRef" :content="string.value"></format-viewer>
</div>
</el-form>
<div class="mt10 fr">
<el-button @click="saveValue" type="primary" v-auth="'redis:data:save'"> </el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, watch, toRefs, onMounted } from 'vue';
import { redisApi } from './api';
import { ElMessage } from 'element-plus';
import { notEmpty } from '@/common/assert';
import FormatViewer from './FormatViewer.vue';
const props = defineProps({
redisId: {
type: [Number],
require: true,
default: 0,
},
db: {
type: [Number],
require: true,
default: 0,
},
keyInfo: {
type: [Object],
},
})
const formatViewerRef = ref(null) as any
const state = reactive({
redisId: 0,
db: 0,
key: '',
keyInfo: {
key: '',
type: 'string',
timed: -1,
},
string: {
type: 'text',
value: '',
},
});
const {
string,
} = toRefs(state)
onMounted(() => {
state.redisId = props.redisId
state.db = props.db
state.key = props.keyInfo?.key;
initData();
})
const initData = () => {
getStringValue();
}
const getStringValue = async () => {
if (state.key) {
state.string.value = await redisApi.getString.request(getBaseReqParam());
}
};
const saveValue = async () => {
state.string.value = formatViewerRef.value.getContent();
notEmpty(state.string.value, 'value不能为空');
await redisApi.setString.request({
...getBaseReqParam(),
value: state.string.value,
});
ElMessage.success('数据保存成功');
};
const getBaseReqParam = () => {
return {
id: state.redisId,
db: state.db,
key: state.key
}
}
defineExpose({ initData })
</script>
<style lang="scss">
.key-content-string .format-viewer-container {
min-height: calc(100vh - 453px);
}
/*text viewer box*/
.key-content-string .el-textarea textarea {
font-size: 14px;
height: calc(100vh - 436px);
}
/*json in monaco editor*/
.key-content-string .monaco-editor-content {
height: calc(100vh - 450px) !important;
}
</style>

View File

@@ -0,0 +1,257 @@
<template>
<div>
<el-button @click="showEditDialog(null)" icon="plus" size="small" plain type="primary" class="mb10">添加新行</el-button>
<el-table size="small" border :data="values" min-height=300 stripe>
<el-table-column type="index" :label="'ID (Total: ' + total + ')'" sortable width="100">
</el-table-column>
<el-table-column resizable sortable prop="score" label="score" show-overflow-tooltip min-width="100">
</el-table-column>
<el-table-column resizable sortable prop="value" label="value" show-overflow-tooltip min-width="200">
</el-table-column>
<el-table-column label="操作">
<template #header>
<el-input class="key-detail-filter-value" v-model="state.filterValue" @keyup.enter='zscanData(true)'
placeholder="输入关键词回车搜索" clearable size="small" />
</template>
<template #default="scope">
<el-link @click="showEditDialog(scope.row)" :underline="false" type="primary" icon="edit"
plain></el-link>
<el-popconfirm title="确定删除?" @confirm="zrem(scope.row, scope.$index)">
<template #reference>
<el-link v-auth="'redis:data:del'" :underline="false" type="danger" icon="delete" size="small"
plain class="ml5"></el-link>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<!-- load more content -->
<div class='content-more-container'>
<el-button size='small' @click='loadDatas()' :disabled='loadMoreDisable' class='content-more-btn'>
加载更多
</el-button>
</div>
<el-dialog title="添加新行" v-model="editDialog.visible" width="600px" :destroy-on-close="true"
:close-on-click-modal="false">
<el-form>
<el-form-item>
<el-input type="number" v-model.number="editDialog.score" placeholder="score" />
</el-form-item>
<el-form-item>
<format-viewer class="w100" ref="formatViewerRef" :content="editDialog.content"></format-viewer>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="editDialog.visible = false"> </el-button>
<el-button v-auth="'redis:data:save'" type="primary" @click="confirmEditData"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, toRefs, onMounted } from 'vue';
import { redisApi } from './api';
import { ElMessage } from 'element-plus';
import FormatViewer from './FormatViewer.vue';
const props = defineProps({
redisId: {
type: [Number],
require: true,
default: 0,
},
db: {
type: [Number],
require: true,
default: 0
},
keyInfo: {
type: [Object],
},
})
const formatViewerRef = ref(null) as any;
const state = reactive({
redisId: 0,
db: 0,
key: '',
filterValue: '',
scanCursor: 0,
pageNum: 1,
pageSize: 50,
total: 0,
values: [] as any,
loadMoreDisable: false,
editDialog: {
visible: false,
score: 0,
content: '',
dataRow: null as any,
}
});
const {
total,
values,
loadMoreDisable,
editDialog,
} = toRefs(state)
onMounted(() => {
state.redisId = props.redisId;
state.db = props.db;
state.key = props.keyInfo?.key;
initData();
})
const initData = async () => {
state.pageNum = 1;
state.filterValue = '';
await getTotal();
await zrevrange(true);
}
const loadDatas = (resetTableData = false) => {
if (state.filterValue) {
zscanData(resetTableData);
return;
}
zrevrange(resetTableData);
}
const zrevrange = async (resetTableData = false) => {
const pageNum = state.pageNum;
const pageSize = state.pageSize;
const res = await redisApi.zrevrange.request({
...getBaseReqParam(),
start: (pageNum - 1) * pageSize,
stop: pageNum * pageSize - 1,
})
const vs = [];
for (let member of res) {
vs.push({
score: member.Score,
value: member.Member
})
}
if (resetTableData) {
state.values = vs;
} else {
state.values.push(...vs);
}
state.pageNum++;
state.loadMoreDisable = state.total <= state.values.length
}
const getScanMatch = () => {
return state.filterValue ? `*${state.filterValue}*` : '*';
}
const zscanData = async (resetTableData = true, resetCursor = false) => {
if (resetCursor) {
state.scanCursor = 0;
}
const res = await redisApi.zscan.request({
...getBaseReqParam(),
match: getScanMatch(),
cursor: state.scanCursor,
count: state.pageSize
});
const keys = res.keys;
const vs = [];
const memCount = keys.length / 2;
let nextMemndex = 0;
for (let i = 0; i < memCount; i++) {
vs.push({ value: keys[nextMemndex++], score: keys[nextMemndex++] });
}
if (resetTableData) {
state.values = vs;
} else {
state.values.push(...vs);
}
state.scanCursor = res.cursor;
state.loadMoreDisable = res.cursor == 0
};
const getTotal = () => {
redisApi.zcard.request({
id: state.redisId,
db: state.db,
key: state.key
}).then((res) => {
state.total = res;
});
}
const showEditDialog = (row: any) => {
state.editDialog.dataRow = row;
state.editDialog.content = row ? row.value : '';
state.editDialog.score = row ? row.score : null;
state.editDialog.visible = true;
}
const confirmEditData = async () => {
const param = getBaseReqParam();
// 存在数据行,则说明为修改,则要先删除旧数据后新增
const dataRow = state.editDialog.dataRow
if (dataRow) {
await redisApi.zrem.request({
member: state.editDialog.dataRow.value,
...param
});
}
const score = state.editDialog.score
// 获取zset member内容并新增
const member = formatViewerRef.value.getContent()
await redisApi.zadd.request({
score,
member,
...param
});
ElMessage.success("保存成功");
if (dataRow) {
state.editDialog.dataRow.value = member;
state.editDialog.dataRow.score = score;
} else {
state.values.unshift({ value: member, score });
state.total++;
}
state.editDialog.visible = false;
state.editDialog.dataRow = null;
}
const zrem = async (row: any, index: any) => {
await redisApi.zrem.request({
...getBaseReqParam(),
member: row.value,
})
ElMessage.success("删除成功");
state.values.splice(index, 1)
state.total--;
}
const getBaseReqParam = () => {
return {
id: state.redisId,
db: state.db,
key: state.key
}
}
defineExpose({ initData })
</script>
<style lang="scss"></style>

View File

@@ -1,199 +0,0 @@
<template>
<el-dialog class="el-table-z-index-inherit" :title="title" v-model="dialogVisible" :before-close="cancel" width="800px" :destroy-on-close="true">
<el-form label-width="85px">
<el-form-item prop="key" label="key:">
<el-input :disabled="operationType == 2" v-model="key.key"></el-input>
</el-form-item>
<el-form-item prop="timed" label="过期时间:">
<el-input v-model.number="key.timed" type="number"></el-input>
</el-form-item>
<el-form-item prop="dataType" label="数据类型:">
<el-input v-model="key.type" disabled></el-input>
</el-form-item>
<!-- <el-button @click="onAddListValue" icon="plus" size="small" plain class="mt10">添加</el-button> -->
<div v-if="operationType == 2" class="mt10" style="float: left">
<span>len: {{ len }}</span>
</div>
<el-table :data="value" stripe style="width: 100%">
<el-table-column prop="value" label="value" min-width="200">
<template #default="scope">
<format-input :title="`type:【${key.type}】key:【${key.key}】`" v-model="scope.row.value"
:autosize="{ minRows: 2, maxRows: 10 }" size="small"></format-input>
</template>
</el-table-column>
<el-table-column label="操作" width="140">
<template #default="scope">
<el-button v-if="operationType == 2" type="success" @click="lset(scope.row, scope.$index)"
icon="check" size="small" plain></el-button>
<!-- <el-button type="danger" @click="set.value.splice(scope.$index, 1)" icon="delete" size="small" plain></el-button> -->
</template>
</el-table-column>
</el-table>
<el-row style="margin-top: 20px" type="flex" justify="end">
<el-pagination style="text-align: right" :total="len" layout="prev, pager, next, total"
@current-change="handlePageChange" v-model:current-page="pageNum" :page-size="pageSize">
</el-pagination>
</el-row>
</el-form>
<!-- <template #footer>
<div class="dialog-footer">
<el-button @click="cancel()"> </el-button>
<el-button @click="saveValue" type="primary" v-auth="'redis:data:save'"> </el-button>
</div>
</template> -->
</el-dialog>
</template>
<script lang="ts" setup>
import { reactive, watch, toRefs } from 'vue';
import { redisApi } from './api';
import { ElMessage } from 'element-plus';
import FormatInput from './FormatInput.vue';
const props = defineProps({
visible: {
type: Boolean,
},
title: {
type: String,
},
redisId: {
type: [Number],
require: true,
},
db: {
type: [String],
require: true,
},
keyInfo: {
type: [Object],
},
// 操作类型1新增2修改
operationType: {
type: [Number],
},
listValue: {
type: [Array, Object],
},
})
const emit = defineEmits(['update:visible', 'cancel', 'valChange'])
const state = reactive({
dialogVisible: false,
operationType: 1,
redisId: '',
db: '0',
key: {
key: '',
type: 'string',
timed: -1,
},
value: [{ value: '' }],
len: 0,
start: 0,
stop: 0,
pageNum: 1,
pageSize: 10,
});
const {
dialogVisible,
operationType,
key,
value,
len,
pageNum,
pageSize,
} = toRefs(state)
const cancel = () => {
emit('update:visible', false);
emit('cancel');
setTimeout(() => {
state.key = {
key: '',
type: 'string',
timed: -1,
};
state.value = [];
}, 500);
};
watch(props, async (newValue: any) => {
state.dialogVisible = newValue.visible;
state.redisId = newValue.redisId;
state.db = newValue.db;
state.key = newValue.keyInfo;
state.operationType = newValue.operationType;
// 如果是查看编辑操作,则获取值
if (state.dialogVisible && state.operationType == 2) {
getListValue();
}
});
const getListValue = async () => {
const pageNum = state.pageNum;
const pageSize = state.pageSize;
const res = await redisApi.getListValue.request({
id: state.redisId,
db: state.db,
key: state.key.key,
start: (pageNum - 1) * pageSize,
stop: pageNum * pageSize - 1,
});
state.len = res.len;
state.value = res.list.map((x: any) => {
return {
value: x,
};
});
};
const lset = async (row: any, rowIndex: number) => {
await redisApi.setListValue.request({
id: state.redisId,
db: state.db,
key: state.key.key,
index: (state.pageNum - 1) * state.pageSize + rowIndex,
value: row.value,
});
ElMessage.success('数据保存成功');
};
// const saveValue = async () => {
// notEmpty(state.key.key, 'key不能为空');
// isTrue(state.value.length > 0, 'list内容不能为空');
// // const sv = { value: state.value.map((x) => x.value), id: state.redisId };
// // Object.assign(sv, state.key);
// // await redisApi.saveSetValue.request(sv);
// ElMessage.success('数据保存成功');
// cancel();
// emit('valChange');
// };
// const onAddListValue = () => {
// state.value.unshift({ value: '' });
// };
const handlePageChange = (curPage: number) => {
state.pageNum = curPage;
getListValue();
};
</script>
<style lang="scss">
#string-value-text {
flex-grow: 1;
display: flex;
position: relative;
.text-type-select {
position: absolute;
z-index: 2;
right: 10px;
top: 10px;
max-width: 70px;
}
}
</style>

View File

@@ -1,162 +0,0 @@
<template>
<el-dialog class="el-table-z-index-inherit" :title="title" v-model="dialogVisible" :before-close="cancel" width="800px" :destroy-on-close="true">
<el-form label-width="85px">
<el-form-item prop="key" label="key:">
<el-input :disabled="operationType == 2" v-model="key.key"></el-input>
</el-form-item>
<el-form-item prop="timed" label="过期时间:">
<el-input v-model.number="key.timed" type="number"></el-input>
</el-form-item>
<el-form-item prop="dataType" label="数据类型:">
<el-input v-model="key.type" disabled></el-input>
</el-form-item>
<el-button @click="onAddSetValue" icon="plus" size="small" plain class="mt10">添加</el-button>
<el-table :data="value" stripe style="width: 100%">
<el-table-column prop="value" label="value" min-width="200">
<template #default="scope">
<format-input :title="`type:【${key.type}】key:【${key.key}】`" v-model="scope.row.value" clearable type="textarea"
:autosize="{ minRows: 2, maxRows: 10 }" size="small"></format-input>
</template>
</el-table-column>
<el-table-column label="操作" width="90">
<template #default="scope">
<el-button v-auth="'redis:data:del'" type="danger" @click="value.splice(scope.$index, 1)" icon="delete" size="small"
plain>删除</el-button>
</template>
</el-table-column>
</el-table>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel()"> </el-button>
<el-button @click="saveValue" type="primary" v-auth="'redis:data:save'"> </el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { reactive, watch, toRefs } from 'vue';
import { redisApi } from './api';
import { ElMessage } from 'element-plus';
import { isTrue, notEmpty } from '@/common/assert';
import FormatInput from './FormatInput.vue';
const props = defineProps({
visible: {
type: Boolean,
},
title: {
type: String,
},
redisId: {
type: [Number],
require: true,
},
db: {
type: [String],
require: true,
},
keyInfo: {
type: [Object],
},
// 操作类型1新增2修改
operationType: {
type: [Number],
},
setValue: {
type: [Array, Object],
},
})
const emit = defineEmits(['update:visible', 'cancel', 'valChange'])
const state = reactive({
dialogVisible: false,
operationType: 1,
redisId: '',
db: '0',
key: {
key: '',
type: 'set',
timed: -1,
},
value: [{ value: '' }],
});
const {
dialogVisible,
operationType,
key,
value,
} = toRefs(state)
const cancel = () => {
emit('update:visible', false);
emit('cancel');
setTimeout(() => {
state.key = {
key: '',
type: 'string',
timed: -1,
};
state.value = [];
}, 500);
};
watch(props, async (newValue: any) => {
state.dialogVisible = newValue.visible;
state.redisId = newValue.redisId;
state.db = newValue.db;
state.key = newValue.keyInfo;
state.operationType = newValue.operationType;
// 如果是查看编辑操作,则获取值
if (state.dialogVisible && state.operationType == 2) {
getSetValue();
}
});
const getSetValue = async () => {
const res = await redisApi.getSetValue.request({
id: state.redisId,
db: state.db,
key: state.key.key,
});
state.value = res.map((x: any) => {
return {
value: x,
};
});
};
const saveValue = async () => {
notEmpty(state.key.key, 'key不能为空');
isTrue(state.value.length > 0, 'set内容不能为空');
const sv = { value: state.value.map((x) => x.value), id: state.redisId, db: state.db };
Object.assign(sv, state.key);
await redisApi.saveSetValue.request(sv);
ElMessage.success('数据保存成功');
cancel();
emit('valChange');
};
const onAddSetValue = () => {
state.value.unshift({ value: '' });
};
</script>
<style lang="scss">
#string-value-text {
flex-grow: 1;
display: flex;
position: relative;
.text-type-select {
position: absolute;
z-index: 2;
right: 10px;
top: 10px;
max-width: 70px;
}
}
</style>

View File

@@ -1,167 +0,0 @@
<template>
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" width="800px" :destroy-on-close="true">
<el-form label-width="85px">
<el-form-item prop="key" label="key:">
<el-input :disabled="operationType == 2" v-model="key.key"></el-input>
</el-form-item>
<el-form-item prop="timed" label="过期时间:">
<el-input v-model.number="key.timed" type="number"></el-input>
</el-form-item>
<el-form-item prop="dataType" label="数据类型:">
<el-input v-model="key.type" disabled></el-input>
</el-form-item>
<div id="string-value-text" style="width: 100%">
<format-input :title="`type:【${key.type}】key:【${key.key}】`" v-model="string.value" :autosize="{ minRows: 10, maxRows: 20 }"></format-input>
<!-- <el-select class="text-type-select" @change="onChangeTextType" v-model="string.type">
<el-option key="text" label="text" value="text"> </el-option>
<el-option key="json" label="json" value="json"> </el-option>
</el-select> -->
</div>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel()"> </el-button>
<el-button @click="saveValue" type="primary" v-auth="'redis:data:save'"> </el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { reactive, watch, toRefs } from 'vue';
import { redisApi } from './api';
import { ElMessage } from 'element-plus';
import { notEmpty } from '@/common/assert';
import FormatInput from './FormatInput.vue';
const props = defineProps({
visible: {
type: Boolean,
},
title: {
type: String,
},
redisId: {
type: [Number],
require: true,
},
db: {
type: [String],
require: true,
},
keyInfo: {
type: [Object],
},
// 操作类型1新增2修改
operationType: {
type: [Number],
},
})
const emit = defineEmits(['update:visible', 'cancel', 'valChange'])
const state = reactive({
dialogVisible: false,
operationType: 1,
redisId: '',
db: '0',
key: {
key: '',
type: 'string',
timed: -1,
},
string: {
type: 'text',
value: '',
},
});
const {
dialogVisible,
operationType,
key,
string,
} = toRefs(state)
const cancel = () => {
emit('update:visible', false);
emit('cancel');
setTimeout(() => {
state.key = {
key: '',
type: 'string',
timed: -1,
};
state.string.value = '';
state.string.type = 'text';
}, 500);
};
watch(
() => props.visible,
(val) => {
state.dialogVisible = val;
}
);
watch(
() => props.redisId,
(val) => {
state.redisId = val as any;
}
);
watch(
() => props.db,
(val) => {
state.db = val as any;
}
);
watch(props, async (newValue: any) => {
state.dialogVisible = newValue.visible;
state.redisId = newValue.redisId;
state.db = newValue.db;
state.key = newValue.keyInfo;
state.operationType = newValue.operationType;
// 如果是查看编辑操作,则获取值
if (state.dialogVisible && state.operationType == 2) {
getStringValue();
}
});
const getStringValue = async () => {
state.string.value = await redisApi.getStringValue.request({
id: state.redisId,
db: state.db,
key: state.key.key,
});
};
const saveValue = async () => {
notEmpty(state.key.key, 'key不能为空');
notEmpty(state.string.value, 'value不能为空');
const sv = { value: state.string.value, id: state.redisId, db: state.db };
Object.assign(sv, state.key);
await redisApi.saveStringValue.request(sv);
ElMessage.success('数据保存成功');
cancel();
emit('valChange');
};
</script>
<style lang="scss">
#string-value-text {
flex-grow: 1;
display: flex;
position: relative;
.text-type-select {
position: absolute;
z-index: 2;
right: 10px;
top: 10px;
max-width: 70px;
}
}
</style>

View File

@@ -0,0 +1,56 @@
<template>
<div class="text-formated-container">
<monaco-editor ref="monacoEditorRef" :canChangeMode="false" v-model="state.modelValue" language="json" />
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted } from 'vue';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
const props = defineProps({
content: {
type: String,
default: '',
},
})
const monacoEditorRef = ref(null) as any
const state = reactive({
modelValue: '',
content: null as any,
});
// 因为默认从Text viewer开始暂时不watch保存时会触发重新格式化
// watch(
// () => props.content,
// (val: any) => {
// setContent(val);
// }
// );
onMounted(() => {
setContent(props.content)
})
const setContent = (val: any) => {
state.modelValue = val;
setTimeout(() => {
monacoEditorRef.value.format();
}, 200)
}
const getContent = () => {
// 尝试压缩json
try {
state.content = JSON.stringify(JSON.parse(state.modelValue));
return state.content;
} catch (e) {
return state.modelValue
}
}
defineExpose({ getContent })
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,41 @@
<template>
<div>
<el-input type="textarea" v-model="modelValue" />
</div>
</template>
<script lang="ts" setup>
import { reactive, watch, toRefs, onMounted } from 'vue';
const props = defineProps({
content: {
type: String,
},
})
const state = reactive({
modelValue: '',
});
const {
modelValue,
} = toRefs(state)
watch(
() => props.content,
(val: any) => {
state.modelValue = val;
}
);
onMounted(() => {
state.modelValue = props.content as any;
})
const getContent = () => {
return state.modelValue
}
defineExpose({ getContent })
</script>
<style lang="scss"></style>

View File

@@ -7,20 +7,41 @@ export const redisApi = {
clusterInfo: Api.newGet("/redis/{id}/cluster-info"),
saveRedis: Api.newPost("/redis"),
delRedis: Api.newDelete("/redis/{id}"),
keyTtl: Api.newGet("/redis/{id}/{db}/key-ttl"),
renameKey: Api.newPost("/redis/{id}/{db}/rename-key"),
expireKey: Api.newPost("/redis/{id}/{db}/expire-key"),
persistKey: Api.newDelete("/redis/{id}/{db}/persist-key"),
// 获取权限列表
scan: Api.newPost("/redis/{id}/{db}/scan"),
getStringValue: Api.newGet("/redis/{id}/{db}/string-value"),
saveStringValue: Api.newPost("/redis/{id}/{db}/string-value"),
getString: Api.newGet("/redis/{id}/{db}/string-value"),
setString: Api.newPost("/redis/{id}/{db}/string-value"),
getHashValue: Api.newGet("/redis/{id}/{db}/hash-value"),
hscan: Api.newGet("/redis/{id}/{db}/hscan"),
hget: Api.newGet("/redis/{id}/{db}/hget"),
hset: Api.newPost("/redis/{id}/{db}/hset"),
hdel: Api.newDelete("/redis/{id}/{db}/hdel"),
saveHashValue: Api.newPost("/redis/{id}/{db}/hash-value"),
getSetValue: Api.newGet("/redis/{id}/{db}/set-value"),
scard: Api.newGet("/redis/{id}/{db}/scard"),
sscan: Api.newPost("/redis/{id}/{db}/sscan"),
sadd: Api.newPost("/redis/{id}/{db}/sadd"),
srem: Api.newPost("/redis/{id}/{db}/srem"),
saveSetValue: Api.newPost("/redis/{id}/{db}/set-value"),
del: Api.newDelete("/redis/{id}/{db}/scan/{cursor}/{count}"),
delKey: Api.newDelete("/redis/{id}/{db}/key"),
lrem: Api.newPost("/redis/{id}/{db}/lrem"),
getListValue: Api.newGet("/redis/{id}/{db}/list-value"),
saveListValue: Api.newPost("/redis/{id}/{db}/list-value"),
setListValue: Api.newPost("/redis/{id}/{db}/list-value/lset"),
zcard: Api.newGet("/redis/{id}/{db}/zcard"),
zscan: Api.newGet("/redis/{id}/{db}/zscan"),
zrevrange: Api.newGet("/redis/{id}/{db}/zrevrange"),
zadd: Api.newPost("/redis/{id}/{db}/zadd"),
zrem: Api.newPost("/redis/{id}/{db}/zrem"),
}

View File

@@ -14,7 +14,7 @@ func InitCommonRouter(router *gin.RouterGroup) {
// 获取公钥
common.GET("public-key", func(g *gin.Context) {
req.NewCtxWithGin(g).
WithNeedToken(false).
DontNeedToken().
Handle(c.RasPublicKey)
})
}

View File

@@ -13,6 +13,16 @@ type Redis struct {
Remark string `json:"remark"`
}
type Rename struct {
Key string `binding:"required" json:"key"`
NewKey string `binding:"required" json:"newKey"`
}
type Expire struct {
Key string `binding:"required" json:"key"`
Seconds int64 `binding:"required" json:"seconds"`
}
type KeyInfo struct {
Key string `binding:"required" json:"key"`
Timed int64
@@ -50,3 +60,27 @@ type RedisScanForm struct {
Match string `json:"match"`
Count int64 `json:"count"`
}
type ScanForm struct {
Key string `json:"key"`
Cursor uint64 `json:"cursor"`
Match string `json:"match"`
Count int64 `json:"count"`
}
type SmemberOption struct {
Key string `json:"key"`
Member any `json:"member"`
}
type LRemOption struct {
Key string `json:"key"`
Count int64 `json:"count"`
Member any `json:"member"`
}
type ZAddOption struct {
Key string `json:"key"`
Score float64 `json:"score"`
Member any `json:"member"`
}

View File

@@ -0,0 +1,79 @@
package api
import (
"context"
"mayfly-go/internal/redis/api/form"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/ginx"
"mayfly-go/pkg/req"
"time"
)
func (r *Redis) Hscan(rc *req.Ctx) {
ri, key := r.checkKeyAndGetRedisIns(rc)
g := rc.GinCtx
count := ginx.QueryInt(g, "count", 10)
match := g.Query("match")
cursor := ginx.QueryInt(g, "cursor", 0)
contextTodo := context.TODO()
cmdable := ri.GetCmdable()
keys, nextCursor, err := cmdable.HScan(contextTodo, key, uint64(cursor), match, int64(count)).Result()
biz.ErrIsNilAppendErr(err, "hcan err: %s")
keySize, err := cmdable.HLen(contextTodo, key).Result()
biz.ErrIsNilAppendErr(err, "hlen err: %s")
rc.ResData = map[string]interface{}{
"keys": keys,
"cursor": nextCursor,
"keySize": keySize,
}
}
func (r *Redis) Hdel(rc *req.Ctx) {
ri, key := r.checkKeyAndGetRedisIns(rc)
field := rc.GinCtx.Query("field")
delRes, err := ri.GetCmdable().HDel(context.TODO(), key, field).Result()
biz.ErrIsNilAppendErr(err, "hdel err: %s")
rc.ResData = delRes
}
func (r *Redis) Hget(rc *req.Ctx) {
ri, key := r.checkKeyAndGetRedisIns(rc)
field := rc.GinCtx.Query("field")
res, err := ri.GetCmdable().HGet(context.TODO(), key, field).Result()
biz.ErrIsNilAppendErr(err, "hget err: %s")
rc.ResData = res
}
func (r *Redis) Hset(rc *req.Ctx) {
g := rc.GinCtx
hashValue := new(form.HashValue)
ginx.BindJsonAndValid(g, hashValue)
hv := hashValue.Value[0]
res, err := r.getRedisIns(rc).GetCmdable().HSet(context.TODO(), hashValue.Key, hv["field"].(string), hv["value"]).Result()
biz.ErrIsNilAppendErr(err, "hset失败: %s")
rc.ResData = res
}
func (r *Redis) SetHashValue(rc *req.Ctx) {
g := rc.GinCtx
hashValue := new(form.HashValue)
ginx.BindJsonAndValid(g, hashValue)
ri := r.getRedisIns(rc)
cmd := ri.GetCmdable()
key := hashValue.Key
contextTodo := context.TODO()
for _, v := range hashValue.Value {
res := cmd.HSet(contextTodo, key, v["field"].(string), v["value"])
biz.ErrIsNilAppendErr(res.Err(), "保存hash值失败: %s")
}
if hashValue.Timed != 0 && hashValue.Timed != -1 {
cmd.Expire(context.TODO(), key, time.Second*time.Duration(hashValue.Timed))
}
}

View File

@@ -0,0 +1,189 @@
package api
import (
"context"
"fmt"
"mayfly-go/internal/redis/api/form"
"mayfly-go/internal/redis/api/vo"
"mayfly-go/internal/redis/domain/entity"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/ginx"
"mayfly-go/pkg/req"
"strconv"
"strings"
"sync"
"time"
"github.com/redis/go-redis/v9"
)
// scan获取redis的key列表信息
func (r *Redis) Scan(rc *req.Ctx) {
ri := r.getRedisIns(rc)
form := &form.RedisScanForm{}
ginx.BindJsonAndValid(rc.GinCtx, form)
cmd := ri.GetCmdable()
ctx := context.Background()
kis := make([]*vo.KeyInfo, 0)
var cursorRes map[string]uint64 = make(map[string]uint64)
mode := ri.Info.Mode
if mode == "" || mode == entity.RedisModeStandalone || mode == entity.RedisModeSentinel {
redisAddr := ri.Cli.Options().Addr
// 汇总所有的查询出来的键值
var keys []string
// 有通配符或空时使用scan非模糊匹配直接匹配key
if form.Match == "" || strings.ContainsAny(form.Match, "*") {
cursorRes[redisAddr] = form.Cursor[redisAddr]
for {
ks, cursor := ri.Scan(cursorRes[redisAddr], form.Match, form.Count)
cursorRes[redisAddr] = cursor
if len(ks) > 0 {
// 返回了数据则追加总集合中
keys = append(keys, ks...)
}
// 匹配的数量满足用户需求退出
if int32(len(keys)) >= int32(form.Count) {
break
}
// 匹配到最后退出
if cursor == 0 {
break
}
}
} else {
// 精确匹配
keys = append(keys, form.Match)
}
var keyInfoSplit []string
if len(keys) > 0 {
keyInfosLua := `local result = {}
-- KEYS[1]为第1个参数,lua数组下标从1开始
for i = 1, #KEYS do
local ttl = redis.call('ttl', KEYS[i]);
local keyType = redis.call('type', KEYS[i]);
table.insert(result, string.format("%d,%s", ttl, keyType['ok']));
end;
return table.concat(result, ".");`
// 通过lua获取 ttl,type.ttl2,type2格式以便下面切割获取ttl和type。避免多次调用ttl和type函数
keyInfos, err := cmd.Eval(ctx, keyInfosLua, keys).Result()
biz.ErrIsNilAppendErr(err, "执行lua脚本获取key信息失败: %s")
keyInfoSplit = strings.Split(keyInfos.(string), ".")
}
for i, k := range keys {
ttlType := strings.Split(keyInfoSplit[i], ",")
ttl, _ := strconv.Atoi(ttlType[0])
// 没有存在该key,则跳过
if ttl == -2 {
continue
}
ki := &vo.KeyInfo{Key: k, Type: ttlType[1], Ttl: int64(ttl)}
kis = append(kis, ki)
}
} else if mode == entity.RedisModeCluster {
var keys []string
// 有通配符或空时使用scan非模糊匹配直接匹配key
if form.Match == "" || strings.ContainsAny(form.Match, "*") {
mu := &sync.Mutex{}
// 遍历所有master节点并执行scan命令合并keys
ri.ClusterCli.ForEachMaster(ctx, func(ctx context.Context, client *redis.Client) error {
redisAddr := client.Options().Addr
nowCursor := form.Cursor[redisAddr]
for {
ks, cursor, _ := client.Scan(ctx, nowCursor, form.Match, form.Count).Result()
// 遍历节点的内部回调函数使用异步调用,如不加锁会导致集合并发错误
mu.Lock()
cursorRes[redisAddr] = cursor
nowCursor = cursor
if len(ks) > 0 {
// 返回了数据则追加总集合中
keys = append(keys, ks...)
}
mu.Unlock()
// 匹配的数量满足用户需求退出
if int32(len(keys)) >= int32(form.Count) {
break
}
// 匹配到最后退出
if cursor == 0 {
break
}
}
return nil
})
} else {
// 精确匹配
keys = append(keys, form.Match)
}
// 因为redis集群模式执行lua脚本key必须位于同一slot中故单机获取的方式不适合
// 使用lua获取key的ttl以及类型减少网络调用
keyInfoLua := `local ttl = redis.call('ttl', KEYS[1]);
local keyType = redis.call('type', KEYS[1]);
return string.format("%d,%s", ttl, keyType['ok'])`
for _, k := range keys {
keyInfo, err := cmd.Eval(ctx, keyInfoLua, []string{k}).Result()
biz.ErrIsNilAppendErr(err, "执行lua脚本获取key信息失败: %s")
ttlType := strings.Split(keyInfo.(string), ",")
ttl, _ := strconv.Atoi(ttlType[0])
// 没有存在该key,则跳过
if ttl == -2 {
continue
}
ki := &vo.KeyInfo{Key: k, Type: ttlType[1], Ttl: int64(ttl)}
kis = append(kis, ki)
}
}
size, _ := cmd.DBSize(context.TODO()).Result()
rc.ResData = &vo.Keys{Cursor: cursorRes, Keys: kis, DbSize: size}
}
func (r *Redis) TtlKey(rc *req.Ctx) {
ri, key := r.checkKeyAndGetRedisIns(rc)
ttl, err := ri.GetCmdable().TTL(context.Background(), key).Result()
biz.ErrIsNil(err, "ttl失败: %s")
if ttl == -1 {
rc.ResData = -1
} else {
rc.ResData = ttl.Seconds()
}
}
func (r *Redis) DeleteKey(rc *req.Ctx) {
ri, key := r.checkKeyAndGetRedisIns(rc)
rc.ReqParam = fmt.Sprintf("%s -> 删除key: %s", ri.Info.GetLogDesc(), key)
ri.GetCmdable().Del(context.Background(), key)
}
func (r *Redis) RenameKey(rc *req.Ctx) {
form := &form.Rename{}
ginx.BindJsonAndValid(rc.GinCtx, form)
ri := r.getRedisIns(rc)
rc.ReqParam = fmt.Sprintf("%s -> 重命名key[%s] -> [%s]", ri.Info.GetLogDesc(), form.Key, form.NewKey)
ri.GetCmdable().Rename(context.Background(), form.Key, form.NewKey)
}
func (r *Redis) ExpireKey(rc *req.Ctx) {
form := &form.Expire{}
ginx.BindJsonAndValid(rc.GinCtx, form)
ri := r.getRedisIns(rc)
rc.ReqParam = fmt.Sprintf("%s -> 重置key[%s]过期时间为%d", ri.Info.GetLogDesc(), form.Key, form.Seconds)
ri.GetCmdable().Expire(context.Background(), form.Key, time.Duration(form.Seconds)*time.Second)
}
// 移除过期时间
func (r *Redis) PersistKey(rc *req.Ctx) {
ri, key := r.checkKeyAndGetRedisIns(rc)
rc.ReqParam = fmt.Sprintf("%s -> 移除key[%s]的过期时间", ri.Info.GetLogDesc(), key)
ri.GetCmdable().Persist(context.Background(), key)
}

View File

@@ -0,0 +1,65 @@
package api
import (
"context"
"mayfly-go/internal/redis/api/form"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/ginx"
"mayfly-go/pkg/req"
)
func (r *Redis) GetListValue(rc *req.Ctx) {
ri, key := r.checkKeyAndGetRedisIns(rc)
ctx := context.TODO()
cmdable := ri.GetCmdable()
len, err := cmdable.LLen(ctx, key).Result()
biz.ErrIsNilAppendErr(err, "获取list长度失败: %s")
g := rc.GinCtx
start := ginx.QueryInt(g, "start", 0)
stop := ginx.QueryInt(g, "stop", 10)
res, err := cmdable.LRange(ctx, key, int64(start), int64(stop)).Result()
biz.ErrIsNilAppendErr(err, "获取list值失败: %s")
rc.ResData = map[string]interface{}{
"len": len,
"list": res,
}
}
func (r *Redis) Lrem(rc *req.Ctx) {
g := rc.GinCtx
option := new(form.LRemOption)
ginx.BindJsonAndValid(g, option)
cmd := r.getRedisIns(rc).GetCmdable()
res, err := cmd.LRem(context.TODO(), option.Key, int64(option.Count), option.Member).Result()
biz.ErrIsNilAppendErr(err, "lrem失败: %s")
rc.ResData = res
}
func (r *Redis) SaveListValue(rc *req.Ctx) {
g := rc.GinCtx
listValue := new(form.ListValue)
ginx.BindJsonAndValid(g, listValue)
cmd := r.getRedisIns(rc).GetCmdable()
key := listValue.Key
ctx := context.TODO()
for _, v := range listValue.Value {
cmd.RPush(ctx, key, v)
}
}
func (r *Redis) SetListValue(rc *req.Ctx) {
g := rc.GinCtx
listSetValue := new(form.ListSetValue)
ginx.BindJsonAndValid(g, listSetValue)
ri := r.getRedisIns(rc)
_, err := ri.GetCmdable().LSet(context.TODO(), listSetValue.Key, listSetValue.Index, listSetValue.Value).Result()
biz.ErrIsNilAppendErr(err, "list set失败: %s")
}

View File

@@ -2,7 +2,6 @@ package api
import (
"context"
"fmt"
"mayfly-go/internal/redis/api/form"
"mayfly-go/internal/redis/api/vo"
"mayfly-go/internal/redis/application"
@@ -13,11 +12,9 @@ import (
"mayfly-go/pkg/model"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils"
"strconv"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
)
@@ -194,314 +191,20 @@ func (r *Redis) ClusterInfo(rc *req.Ctx) {
}
}
// scan获取redis的key列表信息
func (r *Redis) Scan(rc *req.Ctx) {
g := rc.GinCtx
ri := r.RedisApp.GetRedisInstance(uint64(ginx.PathParamInt(g, "id")), ginx.PathParamInt(g, "db"))
biz.ErrIsNilAppendErr(r.TagApp.CanAccess(rc.LoginAccount.Id, ri.Info.TagPath), "%s")
form := &form.RedisScanForm{}
ginx.BindJsonAndValid(rc.GinCtx, form)
cmd := ri.GetCmdable()
ctx := context.Background()
kis := make([]*vo.KeyInfo, 0)
var cursorRes map[string]uint64 = make(map[string]uint64)
mode := ri.Info.Mode
if mode == "" || mode == entity.RedisModeStandalone || mode == entity.RedisModeSentinel {
redisAddr := ri.Cli.Options().Addr
// 汇总所有的查询出来的键值
var keys []string
// 有通配符或空时使用scan非模糊匹配直接匹配key
if form.Match == "" || strings.ContainsAny(form.Match, "*") {
cursorRes[redisAddr] = form.Cursor[redisAddr]
for {
ks, cursor := ri.Scan(cursorRes[redisAddr], form.Match, form.Count)
cursorRes[redisAddr] = cursor
if len(ks) > 0 {
// 返回了数据则追加总集合中
keys = append(keys, ks...)
}
// 匹配的数量满足用户需求退出
if int32(len(keys)) >= int32(form.Count) {
break
}
// 匹配到最后退出
if cursor == 0 {
break
}
}
} else {
// 精确匹配
keys = append(keys, form.Match)
}
var keyInfoSplit []string
if len(keys) > 0 {
keyInfosLua := `local result = {}
-- KEYS[1]为第1个参数,lua数组下标从1开始
for i = 1, #KEYS do
local ttl = redis.call('ttl', KEYS[i]);
local keyType = redis.call('type', KEYS[i]);
table.insert(result, string.format("%d,%s", ttl, keyType['ok']));
end;
return table.concat(result, ".");`
// 通过lua获取 ttl,type.ttl2,type2格式以便下面切割获取ttl和type。避免多次调用ttl和type函数
keyInfos, err := cmd.Eval(ctx, keyInfosLua, keys).Result()
biz.ErrIsNilAppendErr(err, "执行lua脚本获取key信息失败: %s")
keyInfoSplit = strings.Split(keyInfos.(string), ".")
}
for i, k := range keys {
ttlType := strings.Split(keyInfoSplit[i], ",")
ttl, _ := strconv.Atoi(ttlType[0])
// 没有存在该key,则跳过
if ttl == -2 {
continue
}
ki := &vo.KeyInfo{Key: k, Type: ttlType[1], Ttl: int64(ttl)}
kis = append(kis, ki)
}
} else if mode == entity.RedisModeCluster {
var keys []string
// 有通配符或空时使用scan非模糊匹配直接匹配key
if form.Match == "" || strings.ContainsAny(form.Match, "*") {
mu := &sync.Mutex{}
// 遍历所有master节点并执行scan命令合并keys
ri.ClusterCli.ForEachMaster(ctx, func(ctx context.Context, client *redis.Client) error {
redisAddr := client.Options().Addr
nowCursor := form.Cursor[redisAddr]
for {
ks, cursor, _ := client.Scan(ctx, nowCursor, form.Match, form.Count).Result()
// 遍历节点的内部回调函数使用异步调用,如不加锁会导致集合并发错误
mu.Lock()
cursorRes[redisAddr] = cursor
nowCursor = cursor
if len(ks) > 0 {
// 返回了数据则追加总集合中
keys = append(keys, ks...)
}
mu.Unlock()
// 匹配的数量满足用户需求退出
if int32(len(keys)) >= int32(form.Count) {
break
}
// 匹配到最后退出
if cursor == 0 {
break
}
}
return nil
})
} else {
// 精确匹配
keys = append(keys, form.Match)
}
// 因为redis集群模式执行lua脚本key必须位于同一slot中故单机获取的方式不适合
// 使用lua获取key的ttl以及类型减少网络调用
keyInfoLua := `local ttl = redis.call('ttl', KEYS[1]);
local keyType = redis.call('type', KEYS[1]);
return string.format("%d,%s", ttl, keyType['ok'])`
for _, k := range keys {
keyInfo, err := cmd.Eval(ctx, keyInfoLua, []string{k}).Result()
biz.ErrIsNilAppendErr(err, "执行lua脚本获取key信息失败: %s")
ttlType := strings.Split(keyInfo.(string), ",")
ttl, _ := strconv.Atoi(ttlType[0])
// 没有存在该key,则跳过
if ttl == -2 {
continue
}
ki := &vo.KeyInfo{Key: k, Type: ttlType[1], Ttl: int64(ttl)}
kis = append(kis, ki)
}
}
size, _ := cmd.DBSize(context.TODO()).Result()
rc.ResData = &vo.Keys{Cursor: cursorRes, Keys: kis, DbSize: size}
}
func (r *Redis) DeleteKey(rc *req.Ctx) {
g := rc.GinCtx
key := g.Query("key")
// 校验查询参数中的key为必填项并返回redis实例
func (r *Redis) checkKeyAndGetRedisIns(rc *req.Ctx) (*application.RedisInstance, string) {
key := rc.GinCtx.Query("key")
biz.NotEmpty(key, "key不能为空")
return r.getRedisIns(rc), key
}
ri := r.RedisApp.GetRedisInstance(uint64(ginx.PathParamInt(g, "id")), ginx.PathParamInt(g, "db"))
func (r *Redis) getRedisIns(rc *req.Ctx) *application.RedisInstance {
ri := r.RedisApp.GetRedisInstance(getIdAndDbNum(rc.GinCtx))
biz.ErrIsNilAppendErr(r.TagApp.CanAccess(rc.LoginAccount.Id, ri.Info.TagPath), "%s")
rc.ReqParam = fmt.Sprintf("%s -> 删除key: %s", ri.Info.GetLogDesc(), key)
ri.GetCmdable().Del(context.Background(), key)
return ri
}
func (r *Redis) checkKey(rc *req.Ctx) (*application.RedisInstance, string) {
g := rc.GinCtx
key := g.Query("key")
biz.NotEmpty(key, "key不能为空")
ri := r.RedisApp.GetRedisInstance(uint64(ginx.PathParamInt(g, "id")), ginx.PathParamInt(g, "db"))
biz.ErrIsNilAppendErr(r.TagApp.CanAccess(rc.LoginAccount.Id, ri.Info.TagPath), "%s")
return ri, key
}
func (r *Redis) GetStringValue(rc *req.Ctx) {
ri, key := r.checkKey(rc)
str, err := ri.GetCmdable().Get(context.TODO(), key).Result()
biz.ErrIsNilAppendErr(err, "获取字符串值失败: %s")
rc.ResData = str
}
func (r *Redis) SetStringValue(rc *req.Ctx) {
g := rc.GinCtx
keyValue := new(form.StringValue)
ginx.BindJsonAndValid(g, keyValue)
ri := r.RedisApp.GetRedisInstance(uint64(ginx.PathParamInt(g, "id")), ginx.PathParamInt(g, "db"))
biz.ErrIsNilAppendErr(r.TagApp.CanAccess(rc.LoginAccount.Id, ri.Info.TagPath), "%s")
rc.ReqParam = fmt.Sprintf("%s -> %s", ri.Info.GetLogDesc(), utils.ToString(keyValue))
str, err := ri.GetCmdable().Set(context.TODO(), keyValue.Key, keyValue.Value, time.Second*time.Duration(keyValue.Timed)).Result()
biz.ErrIsNilAppendErr(err, "保存字符串值失败: %s")
rc.ResData = str
}
func (r *Redis) Hscan(rc *req.Ctx) {
ri, key := r.checkKey(rc)
g := rc.GinCtx
count := ginx.QueryInt(g, "count", 10)
match := g.Query("match")
cursor := ginx.QueryInt(g, "cursor", 0)
contextTodo := context.TODO()
cmdable := ri.GetCmdable()
keys, nextCursor, err := cmdable.HScan(contextTodo, key, uint64(cursor), match, int64(count)).Result()
biz.ErrIsNilAppendErr(err, "hcan err: %s")
keySize, err := cmdable.HLen(contextTodo, key).Result()
biz.ErrIsNilAppendErr(err, "hlen err: %s")
rc.ResData = map[string]interface{}{
"keys": keys,
"cursor": nextCursor,
"keySize": keySize,
}
}
func (r *Redis) Hdel(rc *req.Ctx) {
ri, key := r.checkKey(rc)
field := rc.GinCtx.Query("field")
delRes, err := ri.GetCmdable().HDel(context.TODO(), key, field).Result()
biz.ErrIsNilAppendErr(err, "hdel err: %s")
rc.ResData = delRes
}
func (r *Redis) Hget(rc *req.Ctx) {
ri, key := r.checkKey(rc)
field := rc.GinCtx.Query("field")
res, err := ri.GetCmdable().HGet(context.TODO(), key, field).Result()
biz.ErrIsNilAppendErr(err, "hget err: %s")
rc.ResData = res
}
func (r *Redis) SetHashValue(rc *req.Ctx) {
g := rc.GinCtx
hashValue := new(form.HashValue)
ginx.BindJsonAndValid(g, hashValue)
ri := r.RedisApp.GetRedisInstance(uint64(ginx.PathParamInt(g, "id")), ginx.PathParamInt(g, "db"))
biz.ErrIsNilAppendErr(r.TagApp.CanAccess(rc.LoginAccount.Id, ri.Info.TagPath), "%s")
cmd := ri.GetCmdable()
key := hashValue.Key
contextTodo := context.TODO()
for _, v := range hashValue.Value {
res := cmd.HSet(contextTodo, key, v["field"].(string), v["value"])
biz.ErrIsNilAppendErr(res.Err(), "保存hash值失败: %s")
}
if hashValue.Timed != 0 && hashValue.Timed != -1 {
cmd.Expire(context.TODO(), key, time.Second*time.Duration(hashValue.Timed))
}
}
func (r *Redis) GetSetValue(rc *req.Ctx) {
ri, key := r.checkKey(rc)
res, err := ri.GetCmdable().SMembers(context.TODO(), key).Result()
biz.ErrIsNilAppendErr(err, "获取set值失败: %s")
rc.ResData = res
}
func (r *Redis) SetSetValue(rc *req.Ctx) {
g := rc.GinCtx
keyvalue := new(form.SetValue)
ginx.BindJsonAndValid(g, keyvalue)
ri := r.RedisApp.GetRedisInstance(uint64(ginx.PathParamInt(g, "id")), ginx.PathParamInt(g, "db"))
biz.ErrIsNilAppendErr(r.TagApp.CanAccess(rc.LoginAccount.Id, ri.Info.TagPath), "%s")
cmd := ri.GetCmdable()
key := keyvalue.Key
// 简单处理->先删除,后新增
cmd.Del(context.TODO(), key)
cmd.SAdd(context.TODO(), key, keyvalue.Value...)
if keyvalue.Timed != -1 {
cmd.Expire(context.TODO(), key, time.Second*time.Duration(keyvalue.Timed))
}
}
func (r *Redis) GetListValue(rc *req.Ctx) {
ri, key := r.checkKey(rc)
ctx := context.TODO()
cmdable := ri.GetCmdable()
len, err := cmdable.LLen(ctx, key).Result()
biz.ErrIsNilAppendErr(err, "获取list长度失败: %s")
g := rc.GinCtx
start := ginx.QueryInt(g, "start", 0)
stop := ginx.QueryInt(g, "stop", 10)
res, err := cmdable.LRange(ctx, key, int64(start), int64(stop)).Result()
biz.ErrIsNilAppendErr(err, "获取list值失败: %s")
rc.ResData = map[string]interface{}{
"len": len,
"list": res,
}
}
func (r *Redis) SaveListValue(rc *req.Ctx) {
g := rc.GinCtx
listValue := new(form.ListValue)
ginx.BindJsonAndValid(g, listValue)
ri := r.RedisApp.GetRedisInstance(uint64(ginx.PathParamInt(g, "id")), ginx.PathParamInt(g, "db"))
biz.ErrIsNilAppendErr(r.TagApp.CanAccess(rc.LoginAccount.Id, ri.Info.TagPath), "%s")
cmd := ri.GetCmdable()
key := listValue.Key
ctx := context.TODO()
for _, v := range listValue.Value {
cmd.RPush(ctx, key, v)
}
if listValue.Timed != -1 {
cmd.Expire(context.TODO(), key, time.Second*time.Duration(listValue.Timed))
}
}
func (r *Redis) SetListValue(rc *req.Ctx) {
g := rc.GinCtx
listSetValue := new(form.ListSetValue)
ginx.BindJsonAndValid(g, listSetValue)
ri := r.RedisApp.GetRedisInstance(uint64(ginx.PathParamInt(g, "id")), ginx.PathParamInt(g, "db"))
biz.ErrIsNilAppendErr(r.TagApp.CanAccess(rc.LoginAccount.Id, ri.Info.TagPath), "%s")
_, err := ri.GetCmdable().LSet(context.TODO(), listSetValue.Key, listSetValue.Index, listSetValue.Value).Result()
biz.ErrIsNilAppendErr(err, "list set失败: %s")
// 获取redis id与要操作的库号统一路径
func getIdAndDbNum(g *gin.Context) (uint64, int) {
return uint64(ginx.PathParamInt(g, "id")), ginx.PathParamInt(g, "db")
}

View File

@@ -0,0 +1,78 @@
package api
import (
"context"
"mayfly-go/internal/redis/api/form"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/ginx"
"mayfly-go/pkg/req"
"time"
)
func (r *Redis) GetSetValue(rc *req.Ctx) {
ri, key := r.checkKeyAndGetRedisIns(rc)
res, err := ri.GetCmdable().SMembers(context.TODO(), key).Result()
biz.ErrIsNilAppendErr(err, "获取set值失败: %s")
rc.ResData = res
}
func (r *Redis) SetSetValue(rc *req.Ctx) {
g := rc.GinCtx
keyvalue := new(form.SetValue)
ginx.BindJsonAndValid(g, keyvalue)
cmd := r.getRedisIns(rc).GetCmdable()
key := keyvalue.Key
// 简单处理->先删除,后新增
cmd.Del(context.TODO(), key)
cmd.SAdd(context.TODO(), key, keyvalue.Value...)
if keyvalue.Timed != -1 {
cmd.Expire(context.TODO(), key, time.Second*time.Duration(keyvalue.Timed))
}
}
func (r *Redis) Scard(rc *req.Ctx) {
ri, key := r.checkKeyAndGetRedisIns(rc)
total, err := ri.GetCmdable().SCard(context.TODO(), key).Result()
biz.ErrIsNilAppendErr(err, "scard失败: %s")
rc.ResData = total
}
func (r *Redis) Sscan(rc *req.Ctx) {
g := rc.GinCtx
scan := new(form.ScanForm)
ginx.BindJsonAndValid(g, scan)
cmd := r.getRedisIns(rc).GetCmdable()
keys, cursor, err := cmd.SScan(context.TODO(), scan.Key, scan.Cursor, scan.Match, scan.Count).Result()
biz.ErrIsNilAppendErr(err, "sscan失败: %s")
rc.ResData = map[string]interface{}{
"keys": keys,
"cursor": cursor,
}
}
func (r *Redis) Sadd(rc *req.Ctx) {
g := rc.GinCtx
option := new(form.SmemberOption)
ginx.BindJsonAndValid(g, option)
cmd := r.getRedisIns(rc).GetCmdable()
res, err := cmd.SAdd(context.TODO(), option.Key, option.Member).Result()
biz.ErrIsNilAppendErr(err, "sadd失败: %s")
rc.ResData = res
}
func (r *Redis) Srem(rc *req.Ctx) {
g := rc.GinCtx
option := new(form.SmemberOption)
ginx.BindJsonAndValid(g, option)
cmd := r.getRedisIns(rc).GetCmdable()
res, err := cmd.SRem(context.TODO(), option.Key, option.Member).Result()
biz.ErrIsNilAppendErr(err, "srem失败: %s")
rc.ResData = res
}

View File

@@ -0,0 +1,33 @@
package api
import (
"context"
"fmt"
"mayfly-go/internal/redis/api/form"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/ginx"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils"
"time"
)
func (r *Redis) GetStringValue(rc *req.Ctx) {
ri, key := r.checkKeyAndGetRedisIns(rc)
str, err := ri.GetCmdable().Get(context.TODO(), key).Result()
biz.ErrIsNilAppendErr(err, "获取字符串值失败: %s")
rc.ResData = str
}
func (r *Redis) SetStringValue(rc *req.Ctx) {
g := rc.GinCtx
keyValue := new(form.StringValue)
ginx.BindJsonAndValid(g, keyValue)
ri := r.getRedisIns(rc)
cmd := ri.GetCmdable()
rc.ReqParam = fmt.Sprintf("%s -> %s", ri.Info.GetLogDesc(), utils.ToString(keyValue))
str, err := cmd.Set(context.TODO(), keyValue.Key, keyValue.Value, time.Second*time.Duration(keyValue.Timed)).Result()
biz.ErrIsNilAppendErr(err, "保存字符串值失败: %s")
rc.ResData = str
}

View File

@@ -0,0 +1,72 @@
package api
import (
"context"
"mayfly-go/internal/redis/api/form"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/ginx"
"mayfly-go/pkg/req"
"github.com/redis/go-redis/v9"
)
func (r *Redis) ZCard(rc *req.Ctx) {
ri, key := r.checkKeyAndGetRedisIns(rc)
total, err := ri.GetCmdable().ZCard(context.TODO(), key).Result()
biz.ErrIsNilAppendErr(err, "zcard失败: %s")
rc.ResData = total
}
func (r *Redis) ZScan(rc *req.Ctx) {
g := rc.GinCtx
ri, key := r.checkKeyAndGetRedisIns(rc)
cursor := uint64(ginx.QueryInt(g, "cursor", 0))
match := ginx.Query(g, "match", "*")
count := ginx.QueryInt(g, "count", 50)
keys, cursor, err := ri.GetCmdable().ZScan(context.TODO(), key, cursor, match, int64(count)).Result()
biz.ErrIsNilAppendErr(err, "sscan失败: %s")
rc.ResData = map[string]interface{}{
"keys": keys,
"cursor": cursor,
}
}
func (r *Redis) ZRevRange(rc *req.Ctx) {
g := rc.GinCtx
ri, key := r.checkKeyAndGetRedisIns(rc)
start := ginx.QueryInt(g, "start", 0)
stop := ginx.QueryInt(g, "stop", 50)
res, err := ri.GetCmdable().ZRevRangeWithScores(context.TODO(), key, int64(start), int64(stop)).Result()
biz.ErrIsNilAppendErr(err, "ZRevRange失败: %s")
rc.ResData = res
}
func (r *Redis) ZRem(rc *req.Ctx) {
g := rc.GinCtx
option := new(form.SmemberOption)
ginx.BindJsonAndValid(g, option)
cmd := r.getRedisIns(rc).GetCmdable()
res, err := cmd.ZRem(context.TODO(), option.Key, option.Member).Result()
biz.ErrIsNilAppendErr(err, "zrem失败: %s")
rc.ResData = res
}
func (r *Redis) ZAdd(rc *req.Ctx) {
g := rc.GinCtx
option := new(form.ZAddOption)
ginx.BindJsonAndValid(g, option)
cmd := r.getRedisIns(rc).GetCmdable()
zm := redis.Z{
Score: option.Score,
Member: option.Member,
}
res, err := cmd.ZAdd(context.TODO(), option.Key, zm).Result()
biz.ErrIsNilAppendErr(err, "zadd失败: %s")
rc.ResData = res
}

View File

@@ -49,15 +49,47 @@ func InitRedisRouter(router *gin.RouterGroup) {
req.NewCtxWithGin(c).Handle(rs.Scan)
})
// 删除key
deleteKeyL := req.NewLogInfo("redis-删除key").WithSave(true)
deleteKeyP := req.NewPermission("redis:data:del")
redis.DELETE(":id/:db/key", func(c *gin.Context) {
req.NewCtxWithGin(c).WithLog(deleteKeyL).WithRequiredPermission(deleteKeyP).Handle(rs.DeleteKey)
redis.GET(":id/:db/key-ttl", func(c *gin.Context) {
req.NewCtxWithGin(c).Handle(rs.TtlKey)
})
// 保存数据权限
saveDataP := req.NewPermission("redis:data:save")
// 删除数据权限
deleteDataP := req.NewPermission("redis:data:del")
// 删除key
deleteKeyL := req.NewLogInfo("redis-删除key").WithSave(true)
redis.DELETE(":id/:db/key", func(c *gin.Context) {
req.NewCtxWithGin(c).
WithLog(deleteKeyL).
WithRequiredPermission(deleteDataP).
Handle(rs.DeleteKey)
})
renameKeyL := req.NewLogInfo("redis-重命名key").WithSave(true)
redis.POST(":id/:db/rename-key", func(c *gin.Context) {
req.NewCtxWithGin(c).
WithLog(renameKeyL).
WithRequiredPermission(saveDataP).
Handle(rs.RenameKey)
})
expireKeyL := req.NewLogInfo("redis-设置key过期时间").WithSave(true)
redis.POST(":id/:db/expire-key", func(c *gin.Context) {
req.NewCtxWithGin(c).
WithLog(expireKeyL).
WithRequiredPermission(saveDataP).
Handle(rs.ExpireKey)
})
persistKeyL := req.NewLogInfo("redis-移除key过期时间").WithSave(true)
redis.DELETE(":id/:db/persist-key", func(c *gin.Context) {
req.NewCtxWithGin(c).
WithLog(persistKeyL).
WithRequiredPermission(saveDataP).
Handle(rs.PersistKey)
})
// 获取string类型值
redis.GET(":id/:db/string-value", func(c *gin.Context) {
@@ -67,7 +99,10 @@ func InitRedisRouter(router *gin.RouterGroup) {
// 设置string类型值
setStringL := req.NewLogInfo("redis-setString").WithSave(true)
redis.POST(":id/:db/string-value", func(c *gin.Context) {
req.NewCtxWithGin(c).WithLog(setStringL).WithRequiredPermission(saveDataP).Handle(rs.SetStringValue)
req.NewCtxWithGin(c).
WithLog(setStringL).
WithRequiredPermission(saveDataP).
Handle(rs.SetStringValue)
})
// hscan
@@ -79,25 +114,60 @@ func InitRedisRouter(router *gin.RouterGroup) {
req.NewCtxWithGin(c).Handle(rs.Hget)
})
hsetL := req.NewLogInfo("redis-hset").WithSave(true)
redis.POST(":id/:db/hset", func(c *gin.Context) {
req.NewCtxWithGin(c).
WithLog(hsetL).
WithRequiredPermission(saveDataP).
Handle(rs.Hset)
})
hdelL := req.NewLogInfo("redis-hdel").WithSave(true)
redis.DELETE(":id/:db/hdel", func(c *gin.Context) {
req.NewCtxWithGin(c).WithLog(hdelL).WithRequiredPermission(deleteKeyP).Handle(rs.Hdel)
req.NewCtxWithGin(c).
WithLog(hdelL).
WithRequiredPermission(deleteDataP).
Handle(rs.Hdel)
})
// 设置hash类型值
setHashValueL := req.NewLogInfo("redis-setHashValue").WithSave(true)
redis.POST(":id/:db/hash-value", func(c *gin.Context) {
req.NewCtxWithGin(c).WithLog(setHashValueL).WithRequiredPermission(saveDataP).Handle(rs.SetHashValue)
req.NewCtxWithGin(c).
WithLog(setHashValueL).
WithRequiredPermission(saveDataP).
Handle(rs.SetHashValue)
})
// 获取set类型值
// set操作
redis.GET(":id/:db/set-value", func(c *gin.Context) {
req.NewCtxWithGin(c).Handle(rs.GetSetValue)
})
// 设置set类型值
redis.POST(":id/:db/set-value", func(c *gin.Context) {
req.NewCtxWithGin(c).WithRequiredPermission(saveDataP).Handle(rs.SetSetValue)
req.NewCtxWithGin(c).
WithRequiredPermission(saveDataP).
Handle(rs.SetSetValue)
})
redis.GET(":id/:db/scard", func(c *gin.Context) {
req.NewCtxWithGin(c).Handle(rs.Scard)
})
redis.POST(":id/:db/sscan", func(c *gin.Context) {
req.NewCtxWithGin(c).Handle(rs.Sscan)
})
redis.POST(":id/:db/sadd", func(c *gin.Context) {
req.NewCtxWithGin(c).
WithRequiredPermission(saveDataP).
Handle(rs.Sadd)
})
redis.POST(":id/:db/srem", func(c *gin.Context) {
req.NewCtxWithGin(c).
WithRequiredPermission(deleteDataP).
Handle(rs.Srem)
})
// 获取list类型值
@@ -112,5 +182,36 @@ func InitRedisRouter(router *gin.RouterGroup) {
redis.POST(":id/:db/list-value/lset", func(c *gin.Context) {
req.NewCtxWithGin(c).Handle(rs.SetListValue)
})
redis.POST(":id/:db/lrem", func(c *gin.Context) {
req.NewCtxWithGin(c).
WithRequiredPermission(deleteDataP).
Handle(rs.Lrem)
})
// zset操作
redis.GET(":id/:db/zcard", func(c *gin.Context) {
req.NewCtxWithGin(c).Handle(rs.ZCard)
})
redis.GET(":id/:db/zscan", func(c *gin.Context) {
req.NewCtxWithGin(c).Handle(rs.ZScan)
})
redis.GET(":id/:db/zrevrange", func(c *gin.Context) {
req.NewCtxWithGin(c).Handle(rs.ZRevRange)
})
redis.POST(":id/:db/zrem", func(c *gin.Context) {
req.NewCtxWithGin(c).
WithRequiredPermission(deleteDataP).
Handle(rs.ZRem)
})
redis.POST(":id/:db/zadd", func(c *gin.Context) {
req.NewCtxWithGin(c).
WithRequiredPermission(saveDataP).
Handle(rs.ZAdd)
})
}
}

View File

@@ -22,7 +22,7 @@ func InitAccountRouter(router *gin.RouterGroup) {
loginLog := req.NewLogInfo("用户登录").WithSave(true)
account.POST("login", func(g *gin.Context) {
req.NewCtxWithGin(g).
WithNeedToken(false).
DontNeedToken().
WithLog(loginLog).
Handle(a.Login)
})
@@ -35,7 +35,7 @@ func InitAccountRouter(router *gin.RouterGroup) {
changePwdLog := req.NewLogInfo("用户修改密码").WithSave(true)
account.POST("change-pwd", func(g *gin.Context) {
req.NewCtxWithGin(g).
WithNeedToken(false).
DontNeedToken().
WithLog(changePwdLog).
Handle(a.ChangePassword)
})

View File

@@ -11,7 +11,7 @@ func InitCaptchaRouter(router *gin.RouterGroup) {
captcha := router.Group("sys/captcha")
{
captcha.GET("", func(c *gin.Context) {
req.NewCtxWithGin(c).WithNeedToken(false).Handle(api.GenerateCaptcha)
req.NewCtxWithGin(c).DontNeedToken().Handle(api.GenerateCaptcha)
})
}
}

View File

@@ -17,7 +17,7 @@ func InitSysConfigRouter(router *gin.RouterGroup) {
})
db.GET("/value", func(c *gin.Context) {
req.NewCtxWithGin(c).WithNeedToken(false).Handle(r.GetConfigValueByKey)
req.NewCtxWithGin(c).DontNeedToken().Handle(r.GetConfigValueByKey)
})
saveConfig := req.NewLogInfo("保存系统配置信息").WithSave(true)

View File

@@ -4,7 +4,7 @@ import "fmt"
const (
AppName = "mayfly-go"
Version = "v1.4.1"
Version = "v1.4.2"
)
func GetAppInfo() string {

View File

@@ -31,6 +31,15 @@ func GetPageParam(g *gin.Context) *model.PageParam {
return &model.PageParam{PageNum: QueryInt(g, "pageNum", 1), PageSize: QueryInt(g, "pageSize", 10)}
}
// 获取查询参数,不存在则返回默认值
func Query(g *gin.Context, qm string, defaultStr string) string {
qv := g.Query(qm)
if qv == "" {
return defaultStr
}
return qv
}
// 获取查询参数中指定参数值并转为int
func QueryInt(g *gin.Context, qm string, defaultInt int) int {
qv := g.Query(qm)

View File

@@ -83,7 +83,7 @@ func GetByIdIn(model interface{}, list interface{}, ids []uint64, orderBy ...str
global.Db.Model(model).Where("id in (?)", ids).Order(orderByStr).Find(list)
}
// 根据id列表查询
// 根据map指定条件查询列表
func SelectByMap(model interface{}, list interface{}, where map[string]interface{}, orderBy ...string) {
var orderByStr string
if orderBy == nil {
@@ -94,7 +94,7 @@ func SelectByMap(model interface{}, list interface{}, where map[string]interface
global.Db.Model(model).Where(where).Order(orderByStr).Find(list)
}
// 根据id列表查询
// 根据model指定条件统计数量
func CountBy(model interface{}) int64 {
var count int64
global.Db.Model(model).Where(model).Count(&count)

View File

@@ -20,11 +20,6 @@ func NewPermission(code string) *Permission {
return &Permission{NeedToken: true, Code: code}
}
func (p *Permission) WithNeedToken(needToken bool) *Permission {
p.NeedToken = needToken
return p
}
var (
permissionCodeRegistry PermissionCodeRegistry
)

View File

@@ -80,8 +80,8 @@ func (r *Ctx) WithRequiredPermission(permission *Permission) *Ctx {
return r
}
// 是否需要token
func (r *Ctx) WithNeedToken(needToken bool) *Ctx {
// 需要token校验
func (r *Ctx) DontNeedToken() *Ctx {
r.RequiredPermission = &Permission{NeedToken: false}
return r
}