feat: tag refactor & other

This commit is contained in:
meilin.huang
2024-11-26 17:32:44 +08:00
parent 2b712cd548
commit 6cc15ebeda
57 changed files with 538 additions and 314 deletions

View File

@@ -30,7 +30,12 @@ export function isTrue(condition: boolean, msg: string) {
* @param msg 错误消息
*/
export function notBlank(obj: any, msg: string) {
isTrue(obj, msg);
if (obj == null || obj == undefined || obj == '') {
throw new AssertError(msg);
}
if (Array.isArray(obj) && obj.length == 0) {
throw new AssertError(msg);
}
}
/**

View File

@@ -19,15 +19,22 @@ export const ResourceTypeEnum = {
// 标签关联的资源类型
export const TagResourceTypeEnum = {
AuthCert: EnumValue.of(-2, '公共凭证').setExtra({ icon: 'Ticket' }),
PublicAuthCert: EnumValue.of(-2, '公共凭证').setExtra({ icon: 'Ticket' }),
Tag: EnumValue.of(-1, '标签').setExtra({ icon: 'CollectionTag' }),
Machine: ResourceTypeEnum.Machine,
Db: ResourceTypeEnum.Db,
DbInstance: ResourceTypeEnum.Db,
Redis: ResourceTypeEnum.Redis,
Mongo: ResourceTypeEnum.Mongo,
AuthCert: EnumValue.of(5, '授权凭证').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
MachineAuthCert: EnumValue.of(11, '机器-授权凭证').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
DbAuthCert: EnumValue.of(21, '数据库-授权凭证').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
DbName: EnumValue.of(22, '数据库').setExtra({ icon: 'Coin' }),
Db: EnumValue.of(22, '数据库').setExtra({ icon: 'Coin' }),
};
// 标签关联的资源类型路径
export const TagResourceTypePath = {
MachineAuthCert: `${TagResourceTypeEnum.Machine.value}/${TagResourceTypeEnum.AuthCert.value}`,
DbInstanceAuthCert: `${TagResourceTypeEnum.DbInstance.value}/${TagResourceTypeEnum.AuthCert.value}`,
Db: `${TagResourceTypeEnum.DbInstance.value}/${TagResourceTypeEnum.AuthCert.value}/${TagResourceTypeEnum.Db.value}`,
};

View File

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

View File

@@ -37,7 +37,7 @@
</el-form-item>
<el-form-item ref="tagSelectRef" prop="codePaths" :label="$t('tag.relateTag')">
<tag-tree-check height="300px" v-model="form.codePaths" :tag-type="[TagResourceTypeEnum.DbName.value, TagResourceTypeEnum.Redis.value]" />
<tag-tree-check height="300px" v-model="form.codePaths" :tag-type="[TagResourceTypePath.Db, TagResourceTypeEnum.Redis.value]" />
</el-form-item>
<el-divider content-position="left">{{ $t('flow.approvalNode') }}</el-divider>
@@ -94,7 +94,7 @@ import Sortable from 'sortablejs';
import { randomUuid } from '../../common/utils/string';
import { ProcdefStatus } from './enums';
import TagTreeCheck from '../ops/component/TagTreeCheck.vue';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { TagResourceTypeEnum, TagResourceTypePath } from '@/common/commonEnum';
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
import { useI18nFormValidate, useI18nPleaseInput, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
import { useI18n } from 'vue-i18n';

View File

@@ -95,7 +95,7 @@ const parseBizForm = async (bizFormStr: string) => {
const dbRes = await dbApi.dbs.request({ id: bizForm.dbId });
state.db = dbRes.list?.[0];
tagApi.listByQuery.request({ type: TagResourceTypeEnum.DbName.value, codes: state.db.code }).then((res) => {
tagApi.listByQuery.request({ type: TagResourceTypeEnum.Db.value, codes: state.db.code }).then((res) => {
state.db.codePaths = res.map((item: any) => item.codePath);
});
};

View File

@@ -68,7 +68,7 @@ watch(
);
const changeResourceCode = async (db: any) => {
emit('changeResourceCode', TagResourceTypeEnum.DbName.value, db.code);
emit('changeResourceCode', TagResourceTypeEnum.Db.value, db.code);
};
const validateBizForm = async () => {

View File

@@ -122,7 +122,12 @@
<template #header>
<el-row justify="center">
<div class="resource-num pointer-icon" @click="toPage('db')">
<SvgIcon class="mb5 mr5" :size="28" :name="TagResourceTypeEnum.Db.extra.icon" :color="TagResourceTypeEnum.Db.extra.iconColor" />
<SvgIcon
class="mb5 mr5"
:size="28"
:name="TagResourceTypeEnum.DbInstance.extra.icon"
:color="TagResourceTypeEnum.DbInstance.extra.iconColor"
/>
<span class="">{{ state.db.num }}</span>
</div>
</el-row>
@@ -388,7 +393,7 @@ const handleAvatarSuccess = (response: any, uploadFile: any) => {
// 初始化数字滚动
const initData = async () => {
resourceOpLogApi.getAccountResourceOpLogs
.request({ resourceType: TagResourceTypeEnum.MachineAuthCert.value, pageSize: state.defaultLogSize })
.request({ resourceType: TagResourceTypeEnum.Machine.value, pageSize: state.defaultLogSize })
.then(async (res: any) => {
const tagInfos = await getAllTagInfoByCodePaths(res.list?.map((item: any) => item.codePath));
state.machine.tagInfos = tagInfos;
@@ -396,7 +401,7 @@ const initData = async () => {
});
resourceOpLogApi.getAccountResourceOpLogs
.request({ resourceType: TagResourceTypeEnum.DbName.value, pageSize: state.defaultLogSize })
.request({ resourceType: TagResourceTypeEnum.DbInstance.value, pageSize: state.defaultLogSize })
.then(async (res: any) => {
const tagInfos = await getAllTagInfoByCodePaths(res.list?.map((item: any) => item.codePath));
state.db.tagInfos = tagInfos;

View File

@@ -38,7 +38,11 @@
:value="TagResourceTypeEnum.Machine.value"
/>
<el-option :key="TagResourceTypeEnum.Db.value" :label="TagResourceTypeEnum.Db.label" :value="TagResourceTypeEnum.Db.value" />
<el-option
:key="TagResourceTypeEnum.DbInstance.value"
:label="TagResourceTypeEnum.DbInstance.label"
:value="TagResourceTypeEnum.DbInstance.value"
/>
<el-option
:key="TagResourceTypeEnum.Redis.value"
:label="$t(TagResourceTypeEnum.Redis.label)"
@@ -154,7 +158,7 @@ const DefaultForm = {
username: '',
ciphertextType: AuthCertCiphertextTypeEnum.Password.value,
type: AuthCertTypeEnum.Private.value,
resourceType: TagResourceTypeEnum.AuthCert.value,
resourceType: TagResourceTypeEnum.PublicAuthCert.value,
resourceCode: '',
ciphertext: '',
extra: {} as any,

View File

@@ -57,7 +57,7 @@ import { isPrefixSubsequence } from '@/common/utils/string';
const props = defineProps({
resourceType: {
type: [Number],
type: [Number, String],
required: true,
},
defaultExpandedKeys: {

View File

@@ -58,7 +58,7 @@ const props = defineProps({
default: 'calc(100vh - 330px)',
},
tagType: {
type: [Number, Array<Number>],
type: [Number, Array<Number>, String, Array<String>],
default: TagResourceTypeEnum.Tag.value,
},
nodeKey: {

View File

@@ -43,7 +43,7 @@ import { tagApi } from '../tag/api';
const props = defineProps({
resourceType: {
type: [Number],
type: [Number, String],
required: true,
},
tagPathNodeType: {

View File

@@ -1,7 +1,7 @@
import {OptionsApi, SearchItem} from '@/components/SearchForm';
import {ContextmenuItem} from '@/components/contextmenu';
import {TagResourceTypeEnum} from '@/common/commonEnum';
import {tagApi} from '../tag/api';
import { OptionsApi, SearchItem } from '@/components/SearchForm';
import { ContextmenuItem } from '@/components/contextmenu';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { tagApi } from '../tag/api';
export class TagTreeNode {
/**
@@ -160,9 +160,9 @@ export class NodeType {
* @param resourceType 资源类型
* @returns
*/
export function getTagPathSearchItem(resourceType: number) {
export function getTagPathSearchItem(resourceType: any) {
return SearchItem.select('tagPath', 'common.tag').withOptionsApi(
OptionsApi.new(tagApi.getResourceTagPaths, {resourceType}).withConvertFn((res: any) => {
OptionsApi.new(tagApi.getResourceTagPaths, { resourceType }).withConvertFn((res: any) => {
return res.map((x: any) => {
return {
label: x,
@@ -180,7 +180,7 @@ export function getTagPathSearchItem(resourceType: number) {
*/
export function getTagTypeCodeByPath(codePath: string) {
const result: any = {};
if (!codePath) return result
if (!codePath) return result;
const parts = codePath.split('/'); // 切分字符串并保留数字和对应的值部分
for (let part of parts) {
@@ -208,7 +208,7 @@ export function getTagTypeCodeByPath(codePath: string) {
* @returns
*/
export async function getAllTagInfoByCodePaths(codePaths: string[]) {
if (!codePaths) return
if (!codePaths) return;
const allTypeAndCode: any = {};
for (let codePath of codePaths) {
@@ -222,7 +222,7 @@ export async function getAllTagInfoByCodePaths(codePaths: string[]) {
if (type == TagResourceTypeEnum.Tag.value) {
continue;
}
const tagInfo = await tagApi.listByQuery.request({type: type, codes: allTypeAndCode[type]});
const tagInfo = await tagApi.listByQuery.request({ type: type, codes: allTypeAndCode[type] });
allTypeAndCode[type] = tagInfo;
}

View File

@@ -198,7 +198,7 @@ const getAuthCerts = async () => {
const inst: any = props.instance;
const res = await resourceAuthCertApi.listByQuery.request({
resourceCode: inst.code,
resourceType: TagResourceTypeEnum.Db.value,
resourceType: TagResourceTypeEnum.DbInstance.value,
pageSize: 100,
});
state.authCerts = res.list || [];

View File

@@ -89,7 +89,7 @@
<ResourceAuthCertTableEdit
v-model="form.authCerts"
:resource-code="form.code"
:resource-type="TagResourceTypeEnum.Db.value"
:resource-type="TagResourceTypeEnum.DbInstance.value"
:test-conn-btn-loading="testConnBtnLoading"
@test-conn="testConn"
:disable-ciphertext-type="[AuthCertCiphertextTypeEnum.PrivateKey.value]"
@@ -128,6 +128,7 @@ import { AuthCertCiphertextTypeEnum } from '../tag/enums';
import TagTreeSelect from '../component/TagTreeSelect.vue';
import { useI18nFormValidate, useI18nPleaseInput, useI18nPleaseSelect, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
import { useI18n } from 'vue-i18n';
import { notBlankI18n } from '@/common/assert';
const { t } = useI18n();
@@ -258,6 +259,7 @@ const testConn = async (authCert: any) => {
const btnOk = async () => {
await useI18nFormValidate(dbForm);
state.submitForm = await getReqForm();
notBlankI18n(state.submitForm.authCerts, 'db.acName');
await saveInstanceExec();
useI18nSaveSuccessMsg();
state.form.id = saveInstanceRes as any;

View File

@@ -13,9 +13,9 @@
>
<template #tableHeader>
<el-button v-auth="perms.saveInstance" type="primary" icon="plus" @click="editInstance(false)">{{ $t('common.create') }}</el-button>
<el-button v-auth="perms.delInstance" :disabled="selectionData.length < 1" @click="deleteInstance()" type="danger" icon="delete">{{
$t('common.delete')
}}</el-button>
<el-button v-auth="perms.delInstance" :disabled="selectionData.length < 1" @click="deleteInstance()" type="danger" icon="delete">
{{ $t('common.delete') }}
</el-button>
</template>
<template #tagPath="{ data }">
@@ -76,7 +76,6 @@
<script lang="ts" setup>
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { dbApi } from './api';
import { formatDate } from '@/common/utils/format';
import PageTable from '@/components/pagetable/PageTable.vue';
@@ -88,7 +87,7 @@ import { SearchItem } from '@/components/SearchForm';
import ResourceAuthCert from '../component/ResourceAuthCert.vue';
import ResourceTags from '../component/ResourceTags.vue';
import { getTagPathSearchItem } from '../component/tag';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { TagResourceTypePath } from '@/common/commonEnum';
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle } from '@/hooks/useI18n';
import { useI18n } from 'vue-i18n';
@@ -110,10 +109,7 @@ const perms = {
saveDb: 'db:save',
};
const searchItems = [
SearchItem.input('keyword', 'common.keyword').withPlaceholder('db.keywordPlaceholder'),
getTagPathSearchItem(TagResourceTypeEnum.DbAuthCert.value),
];
const searchItems = [SearchItem.input('keyword', 'common.keyword').withPlaceholder('db.keywordPlaceholder'), getTagPathSearchItem(TagResourceTypePath.Db)];
const columns = ref([
TableColumn.new('tags[0].tagPath', 'tag.relateTag').isSlot('tagPath').setAddWidth(20),

View File

@@ -4,7 +4,7 @@
<Pane size="20" max-size="30">
<tag-tree
:default-expanded-keys="state.defaultExpendKey"
:resource-type="TagResourceTypeEnum.DbName.value"
:resource-type="TagResourceTypePath.Db"
:tag-path-node-type="NodeTypeTagPath"
ref="tagTreeRef"
>
@@ -246,7 +246,7 @@ import SvgIcon from '@/components/svgIcon/index.vue';
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
import { getDbDialect, schemaDbTypes } from './dialect/index';
import { sleep } from '@/common/utils/loading';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { TagResourceTypeEnum, TagResourceTypePath } from '@/common/commonEnum';
import { Pane, Splitpanes } from 'splitpanes';
import { useEventListener, useStorage } from '@vueuse/core';
import SqlExecBox from '@/views/ops/db/component/sqleditor/SqlExecBox';
@@ -597,7 +597,7 @@ const autoOpenDb = (codePath: string) => {
const typeAndCodes: any = getTagTypeCodeByPath(codePath);
const tagPath = typeAndCodes[TagResourceTypeEnum.Tag.value].join('/') + '/';
const dbCode = typeAndCodes[TagResourceTypeEnum.DbName.value][0];
const dbCode = typeAndCodes[TagResourceTypeEnum.Db.value][0];
state.defaultExpendKey = [tagPath, dbCode];
setTimeout(() => {

View File

@@ -3,7 +3,7 @@
v-bind="$attrs"
v-model="selectNode"
@change="changeNode"
:resource-type="TagResourceTypeEnum.Db.value"
:resource-type="TagResourceTypePath.Db"
:tag-path-node-type="NodeTypeTagPath"
>
<template #iconPrefix>
@@ -17,7 +17,7 @@
</template>
<script setup lang="ts">
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { TagResourceTypeEnum, TagResourceTypePath } from '@/common/commonEnum';
import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
import { dbApi } from '@/views/ops/db/api';
import { sleep } from '@/common/utils/loading';

View File

@@ -309,7 +309,7 @@ const onRunSql = async (newTab = false) => {
sqlPrefix.startsWith('update') ||
sqlPrefix.startsWith('insert') ||
sqlPrefix.startsWith('delete') ||
sqlPrefix.startsWith('alert') ||
sqlPrefix.startsWith('alter') ||
sqlPrefix.startsWith('drop') ||
sqlPrefix.startsWith('create');

View File

@@ -483,7 +483,7 @@ const setTableColumns = (columns: any) => {
x.dataType = dbDialect.getDataType(x.columnType);
x.dataTypeSubscript = ColumnTypeSubscript[x.dataType];
x.remark = `${x.columnType} ${x.columnComment ? ' | ' + x.columnComment : ''}`;
console.log(x);
return {
...x,
key: columnName,

View File

@@ -64,7 +64,7 @@
<el-input v-else-if="item.prop === 'remark'" size="small" v-model="scope.row.remark" />
<el-popconfirm v-else-if="item.prop === 'action'" :title="$t('common.delete')" @confirm="deleteRow(scope.$index)">
<el-popconfirm v-else-if="item.prop === 'action'" :title="$t('common.deleteConfirm')" @confirm="deleteRow(scope.$index)">
<template #reference>
<el-link type="danger" plain size="small" :underline="false">{{ $t('common.delete') }}</el-link>
</template>

View File

@@ -1,15 +1,15 @@
/* eslint-disable no-unused-vars */
import {dbApi} from './api';
import {getTextWidth} from '@/common/utils/string';
import { dbApi } from './api';
import { getTextWidth } from '@/common/utils/string';
import SqlExecBox from './component/sqleditor/SqlExecBox';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import {editor, languages, Position} from 'monaco-editor';
import { editor, languages, Position } from 'monaco-editor';
import {registerCompletionItemProvider} from '@/components/monaco/completionItemProvider';
import {DbDialect, EditorCompletionItem, getDbDialect} from './dialect';
import {type RemovableRef, useLocalStorage} from '@vueuse/core';
import {DbGetDbNamesMode} from './enums';
import {ElMessage} from 'element-plus';
import { registerCompletionItemProvider } from '@/components/monaco/completionItemProvider';
import { DbDialect, EditorCompletionItem, getDbDialect } from './dialect';
import { type RemovableRef, useLocalStorage } from '@vueuse/core';
import { DbGetDbNamesMode } from './enums';
import { ElMessage } from 'element-plus';
const hintsStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-table-hints', new Map());
const tableStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-tables', new Map());
@@ -101,13 +101,12 @@ export class DbInst {
// 重置列信息缓存与表提示信息
db.columnsMap?.clear();
console.log(`load tables -> dbName: ${dbName}`);
tables = await dbApi.tableInfos.request({id: this.id, db: dbName});
tables = await dbApi.tableInfos.request({ id: this.id, db: dbName });
tableStorage.value.set(key, tables);
db.tables = tables;
// 异步加载表提示信息
this.loadDbHints(dbName, true).then(() => {
});
this.loadDbHints(dbName, true).then(() => {});
return tables;
}
@@ -116,7 +115,7 @@ export class DbInst {
// 表名联想
let suggestions: languages.CompletionItem[] = [];
tables?.forEach((tableMeta: any, index: any) => {
const {tableName, tableComment} = tableMeta;
const { tableName, tableComment } = tableMeta;
suggestions.push({
label: {
label: tableName + ' - ' + tableComment,
@@ -129,7 +128,7 @@ export class DbInst {
sortText: 300 + index + '',
});
});
return {suggestions};
return { suggestions };
}
/** 加载列信息提示 */
@@ -154,7 +153,7 @@ export class DbInst {
});
});
return {suggestions};
return { suggestions };
}
/**
@@ -212,7 +211,7 @@ export class DbInst {
return hints;
}
console.log(`load db-hits -> dbName: ${dbName}`);
hints = await dbApi.hintTables.request({id: this.id, db: db.name});
hints = await dbApi.hintTables.request({ id: this.id, db: db.name });
db.tableHints = hints;
hintsStorage.value.set(key, hints);
return hints;
@@ -410,7 +409,7 @@ export class DbInst {
dbInst.databases = inst.databases;
if (dbInst.databases?.[0]) {
dbInst.version = await dbApi.getCompatibleDbVersion.request({id: inst.id, db: dbInst.databases?.[0]});
dbInst.version = await dbApi.getCompatibleDbVersion.request({ id: inst.id, db: dbInst.databases?.[0] });
}
dbInstCache.set(dbInst.id, dbInst);
@@ -447,7 +446,7 @@ export class DbInst {
return Promise.resolve(dbInst);
}
const dbInfoRes = await dbApi.dbs.request({id: dbId});
const dbInfoRes = await dbApi.dbs.request({ id: dbId });
const db = dbInfoRes.list[0];
return Promise.resolve(DbInst.getOrNewInst(db));
}
@@ -465,7 +464,7 @@ export class DbInst {
* @returns
*/
static isNumber(columnType: string) {
return columnType && columnType.match(/^(int|uint|double|float|number|decimal|byte|bit)/i);
return columnType && columnType.match(/(int|uint|double|float|number|decimal|byte|bit)/gi);
}
/**
@@ -545,7 +544,7 @@ export class DbInst {
return db.database.split(' ');
}
return await dbApi.getDbNamesByAc.request({authCertName: db.authCertName});
return await dbApi.getDbNamesByAc.request({ authCertName: db.authCertName });
}
}
@@ -653,9 +652,9 @@ function registerCompletions(
) {
// mysql关键字
completions.forEach((item: EditorCompletionItem) => {
let {label, insertText, description} = item;
let { label, insertText, description } = item;
suggestions.push({
label: {label, description},
label: { label, description },
kind,
insertText: insertText || label,
range,
@@ -674,20 +673,20 @@ function registerCompletions(
export function registerDbCompletionItemProvider(dbId: number, db: string, dbs: any[] = [], dbType: string) {
let dbDialect = getDbDialect(dbType);
let dbDialectInfo = dbDialect.getInfo();
let {keywords, operators, functions, variables} = dbDialectInfo.editorCompletions;
let { keywords, operators, functions, variables } = dbDialectInfo.editorCompletions;
registerCompletionItemProvider('sql', {
triggerCharacters: ['.', ' '],
provideCompletionItems: async (model: editor.ITextModel, position: Position): Promise<languages.CompletionList | null | undefined> => {
let word = model.getWordUntilPosition(position);
const dbInst = await DbInst.getInstA(dbId);
const {lineNumber, column} = position;
const {startColumn, endColumn} = word;
const { lineNumber, column } = position;
const { startColumn, endColumn } = word;
// 当前行文本
let lineContent = model.getLineContent(lineNumber);
// 注释行不需要代码提示
if (lineContent.startsWith('--')) {
return {suggestions: []};
return { suggestions: [] };
}
let range = {
@@ -812,7 +811,7 @@ export function registerDbCompletionItemProvider(dbId: number, db: string, dbs:
// 当前库的表名联想
const tables = await dbInst.loadTables(db);
tables.forEach((tableMeta: any, index: any) => {
const {tableName, tableComment} = tableMeta;
const { tableName, tableComment } = tableMeta;
suggestions.push({
label: {
label: tableName + ' - ' + tableComment,
@@ -839,11 +838,17 @@ export function registerDbCompletionItemProvider(dbId: number, db: string, dbs:
});
}
function getTableName4SqlCtx(sql: string, alias: string = '', defaultDb: string): {
tableName: string;
tableAlias: string;
db: string
} | undefined {
function getTableName4SqlCtx(
sql: string,
alias: string = '',
defaultDb: string
):
| {
tableName: string;
tableAlias: string;
db: string;
}
| undefined {
// 去除多余的换行、空格和制表符
sql = sql.replace(/[\r\n\s\t]+/g, ' ');
@@ -865,7 +870,7 @@ function getTableName4SqlCtx(sql: string, alias: string = '', defaultDb: string)
tableName = info[1];
}
const tableAlias = matches[2] ? matches[2].replace(/[`"]/g, '') : tableName;
tables.push({tableName, tableAlias, db});
tables.push({ tableName, tableAlias, db });
}
if (alias) {

View File

@@ -269,7 +269,7 @@ import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth';
import { formatByteSize, formatDate } from '@/common/utils/format';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { TagResourceTypePath } from '@/common/commonEnum';
import { SearchItem } from '@/components/SearchForm';
import { getTagPathSearchItem } from '../component/tag';
import MachineFile from '@/views/ops/machine/file/MachineFile.vue';
@@ -311,7 +311,7 @@ const perms = {
const searchItems = [
SearchItem.input('keyword', 'common.keyword').withPlaceholder('machine.keywordPlaceholder'),
getTagPathSearchItem(TagResourceTypeEnum.MachineAuthCert.value),
getTagPathSearchItem(TagResourceTypePath.MachineAuthCert),
];
const columns = [

View File

@@ -6,7 +6,7 @@
<tag-tree
class="machine-terminal-tree"
ref="tagTreeRef"
:resource-type="TagResourceTypeEnum.MachineAuthCert.value"
:resource-type="TagResourceTypeEnum.Machine.value"
:tag-path-node-type="NodeTypeTagPath"
:default-expanded-keys="state.defaultExpendKey"
>
@@ -378,7 +378,7 @@ const autoOpenTerminal = (codePath: string) => {
const machineCode = typeAndCodes[TagResourceTypeEnum.Machine.value][0];
state.defaultExpendKey = [tagPath, machineCode];
const authCertName = typeAndCodes[TagResourceTypeEnum.MachineAuthCert.value][0];
const authCertName = typeAndCodes[TagResourceTypeEnum.PublicAuthCert.value][0];
setTimeout(() => {
// 置空
autoOpenResourceStore.setMachineCodePath('');

View File

@@ -39,7 +39,11 @@
/></el-form-item>
<el-form-item ref="tagSelectRef" prop="codePaths" :label="$t('machine.relateMachine')">
<tag-tree-check height="200px" :tag-type="TagResourceTypeEnum.Machine.value" v-model="form.codePaths" />
<tag-tree-check
height="200px"
:tag-type="`${TagResourceTypeEnum.Machine.value}/${TagResourceTypeEnum.AuthCert.value}`"
v-model="form.codePaths"
/>
</el-form-item>
</el-form>

View File

@@ -33,7 +33,7 @@
:title="$t('machine.cmdConfig')"
v-model="dialogVisible"
:show-close="false"
width="600px"
size="40%"
:destroy-on-close="true"
:close-on-click-modal="false"
>
@@ -78,7 +78,11 @@
</el-form-item>
<el-form-item ref="tagSelectRef" prop="codePaths" :label="$t('machine.relateMachine')">
<tag-tree-check height="calc(100vh - 430px)" :tag-type="TagResourceTypeEnum.MachineAuthCert.value" v-model="form.codePaths" />
<tag-tree-check
height="calc(100vh - 430px)"
:tag-type="`${TagResourceTypeEnum.Machine.value}/${TagResourceTypeEnum.AuthCert.value}`"
v-model="form.codePaths"
/>
</el-form-item>
</el-form>
<template #footer>

View File

@@ -1,6 +1,6 @@
<template>
<div>
<el-drawer :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false">
<el-drawer :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
<template #header>
<DrawerHeader :header="title" :back="cancel" />
</template>

View File

@@ -344,7 +344,7 @@ const allowDrag = (node: any) => {
const tagType = node.data.type;
return (
tagType == TagResourceTypeEnum.Tag.value ||
tagType == TagResourceTypeEnum.Db.value ||
tagType == TagResourceTypeEnum.DbInstance.value ||
tagType == TagResourceTypeEnum.Redis.value ||
tagType == TagResourceTypeEnum.Machine.value ||
tagType == TagResourceTypeEnum.Mongo.value

View File

@@ -7,7 +7,7 @@ export const tagApi = {
delTagTree: Api.newDelete('/tag-trees/{id}'),
movingTag: Api.newPost('/tag-trees/moving'),
getResourceTagPaths: Api.newGet('/tag-trees/resources/{resourceType}/tag-paths'),
getResourceTagPaths: Api.newGet('/tag-trees/resources/tag-paths'),
countTagResource: Api.newGet('/tag-trees/resources/count'),
getRelateTagIds: Api.newGet('/tag-trees/relate/{relateType}/{relateId}'),

View File

@@ -49,7 +49,7 @@ func (d *Db) Dbs(rc *req.Ctx) {
// 不存在可访问标签id即没有可操作数据
tags := d.TagApp.GetAccountTags(rc.GetLoginAccount().Id, &tagentity.TagTreeQuery{
Types: collx.AsArray(tagentity.TagTypeDb),
TypePaths: collx.AsArray(tagentity.NewTypePaths(tagentity.TagTypeDbInstance, tagentity.TagTypeAuthCert, tagentity.TagTypeDb)),
CodePathLikes: collx.AsArray(queryCond.TagPath),
})
if len(tags) == 0 {

View File

@@ -32,7 +32,7 @@ func (d *Instance) Instances(rc *req.Ctx) {
queryCond, page := req.BindQueryAndPage[*entity.InstanceQuery](rc, new(entity.InstanceQuery))
tags := d.TagApp.GetAccountTags(rc.GetLoginAccount().Id, &tagentity.TagTreeQuery{
Types: collx.AsArray(tagentity.TagTypeDbAuthCert),
TypePaths: collx.AsArray(tagentity.NewTypePaths(tagentity.TagTypeDbInstance, tagentity.TagTypeAuthCert)),
CodePathLikes: collx.AsArray(queryCond.TagPath),
})
// 不存在可操作的数据库,即没有可操作数据
@@ -50,7 +50,7 @@ func (d *Instance) Instances(rc *req.Ctx) {
biz.ErrIsNil(err)
// 填充授权凭证信息
d.ResourceAuthCertApp.FillAuthCertByAcNames(tagentity.GetCodesByCodePaths(tagentity.TagTypeDbAuthCert, tagCodePaths...), collx.ArrayMap(instvos, func(vos *vo.InstanceListVO) tagentity.IAuthCert {
d.ResourceAuthCertApp.FillAuthCertByAcNames(tagentity.GetCodesByCodePaths(tagentity.TagTypeAuthCert, tagCodePaths...), collx.ArrayMap(instvos, func(vos *vo.InstanceListVO) tagentity.IAuthCert {
return vos
})...)

View File

@@ -158,10 +158,21 @@ func (d *DbTransferTask) fileRun(la *model.LoginAccount, fm *form.DbTransferFile
ticker := time.NewTicker(time.Second * 1)
defer ticker.Stop()
defer func() {
if err := recover(); err != nil {
errInfo := anyx.ToString(err)
if len(errInfo) > 500 {
errInfo = errInfo[:500] + "..."
}
d.MsgApp.CreateAndSend(la, msgdto.ErrSysMsg(i18n.T(imsg.SqlScriptRunFail), errInfo).WithClientId(fm.ClientId))
}
}()
if err != nil {
biz.ErrIsNilAppendErr(err, "failed to connect to the target database: %s")
}
errSql := ""
err = sqlparser.SQLSplit(reader, func(sql string) error {
select {
case <-ticker.C:
@@ -175,11 +186,14 @@ func (d *DbTransferTask) fileRun(la *model.LoginAccount, fm *form.DbTransferFile
}
executedStatements++
_, err = targetDbConn.Exec(sql)
if err != nil {
errSql = sql
}
return err
})
if err != nil {
biz.ErrIsNilAppendErr(err, "sql execution failed: %s")
biz.ErrIsNil(err, "[%s] execution failed: %s", errSql, err)
}
d.MsgApp.CreateAndSend(la, msgdto.SuccessSysMsg(i18n.T(imsg.SqlScriptRunSuccess), fmt.Sprintf("sql execution successfully: %s", filename)).WithClientId(fm.ClientId))

View File

@@ -98,7 +98,7 @@ func (d *dbAppImpl) SaveDb(ctx context.Context, dbEntity *entity.Db) error {
Name: dbEntity.Name,
}},
ParentTagCode: authCert.Name,
ParentTagType: tagentity.TagTypeDbAuthCert,
ParentTagType: tagentity.TagTypeAuthCert,
})
})
}
@@ -141,7 +141,7 @@ func (d *dbAppImpl) SaveDb(ctx context.Context, dbEntity *entity.Db) error {
}
}
if authCert.Name != old.AuthCertName {
return d.tagApp.ChangeParentTag(ctx, tagentity.TagTypeDb, old.Code, tagentity.TagTypeDbAuthCert, authCert.Name)
return d.tagApp.ChangeParentTag(ctx, tagentity.TagTypeDb, old.Code, tagentity.TagTypeAuthCert, authCert.Name)
}
return nil
})

View File

@@ -260,7 +260,7 @@ func (m *instanceAppImpl) genDbInstanceResourceTag(me *entity.DbInstance, authCe
return &tagdto.ResourceTag{
Code: val.Name,
Name: val.Username,
Type: tagentity.TagTypeDbAuthCert,
Type: tagentity.TagTypeAuthCert,
}
})

View File

@@ -3,16 +3,10 @@ package dbi
import (
"context"
"database/sql"
"encoding/hex"
"fmt"
"gitee.com/chunanyong/dm"
"mayfly-go/internal/machine/mcm"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/utils/anyx"
"reflect"
"strconv"
"strings"
)
// 游标遍历查询结果集处理函数
@@ -30,6 +24,16 @@ type DbConn struct {
type QueryColumn struct {
Name string `json:"name"` // 列名
Type string `json:"type"` // 类型
SqlColType *sql.ColumnType `json:"-"`
}
func NewQueryColumn(colName string, col *sql.ColumnType) *QueryColumn {
return &QueryColumn{
Name: col.Name(),
Type: col.DatabaseTypeName(),
SqlColType: col,
}
}
func (d *DbConn) GetDb() *sql.DB {
@@ -76,7 +80,7 @@ func (d *DbConn) Query2Struct(execSql string, dest any) error {
// WalkQueryRows 游标方式遍历查询结果集, walkFn返回error不为nil, 则跳出遍历并取消查询
func (d *DbConn) WalkQueryRows(ctx context.Context, querySql string, walkFn WalkQueryRowsFunc, args ...any) ([]*QueryColumn, error) {
return walkQueryRows(ctx, d.db, querySql, walkFn, args...)
return walkQueryRows(ctx, d.GetDialect(), d.db, querySql, walkFn, args...)
}
// WalkTableRows 游标方式遍历指定表的结果集, walkFn返回error不为nil, 则跳出遍历并取消查询
@@ -154,7 +158,7 @@ func (d *DbConn) Close() {
}
// 游标方式遍历查询rows, walkFn error不为nil, 则跳出遍历
func walkQueryRows(ctx context.Context, db *sql.DB, selectSql string, walkFn WalkQueryRowsFunc, args ...any) ([]*QueryColumn, error) {
func walkQueryRows(ctx context.Context, dialect Dialect, db *sql.DB, selectSql string, walkFn WalkQueryRowsFunc, args ...any) ([]*QueryColumn, error) {
cancelCtx, cancelFunc := context.WithCancel(ctx)
defer cancelFunc()
@@ -166,6 +170,8 @@ func walkQueryRows(ctx context.Context, db *sql.DB, selectSql string, walkFn Wal
// 后面的链接过来直接报错或拒绝,实际上也没有起效果
defer rows.Close()
columnHelper := dialect.GetColumnHelper()
colTypes, err := rows.ColumnTypes()
if err != nil {
return nil, err
@@ -182,15 +188,9 @@ func walkQueryRows(ctx context.Context, db *sql.DB, selectSql string, walkFn Wal
if colName == "" {
colName = fmt.Sprintf("<anonymous%d>", k+1)
}
cols[k] = &QueryColumn{Name: colName, Type: colType.DatabaseTypeName()}
// 这里scans引用values把数据填充到[]byte里
if cols[k].Type == "st_point" { // 达梦的空间坐标数据
var point dm.DmStruct
scans[k] = &point
} else {
var s = make([]byte, 0)
scans[k] = &s
}
qc := NewQueryColumn(colName, colType)
cols[k] = qc
scans[k] = columnHelper.GetScanDestPtr(qc)
}
for rows.Next() {
@@ -202,10 +202,10 @@ func walkQueryRows(ctx context.Context, db *sql.DB, selectSql string, walkFn Wal
rowData := make(map[string]any, lenCols)
// 把values中的数据复制到row中
for i, v := range scans {
rowData[cols[i].Name] = valueConvert(v, colTypes[i])
rowData[cols[i].Name] = columnHelper.ConvertScanDestValue(v, cols[i])
}
if err = walkFn(rowData, cols); err != nil {
logx.ErrorfContext(ctx, "[%s]游标遍历查询结果集出错, 退出遍历: %s", selectSql, err.Error())
logx.ErrorfContext(ctx, "[%s] cursor traversal query result set error, exit traversal: %s", selectSql, err.Error())
cancelFunc()
return cols, err
}
@@ -214,108 +214,13 @@ func walkQueryRows(ctx context.Context, db *sql.DB, selectSql string, walkFn Wal
return cols, nil
}
func ParseDmStruct(dmStruct *dm.DmStruct) string {
if !dmStruct.Valid {
return ""
}
name, _ := dmStruct.GetSQLTypeName()
attributes, _ := dmStruct.GetAttributes()
arr := make([]string, len(attributes))
arr = append(arr, name, "(")
for i, v := range attributes {
if blb, ok1 := v.(*dm.DmBlob); ok1 {
if blb.Valid {
length, _ := blb.GetLength()
var dest = make([]byte, length)
_, _ = blb.Read(dest)
// 2进制转16进制字符串
hexStr := hex.EncodeToString(dest)
arr = append(arr, "0x", strings.ToUpper(hexStr))
}
} else {
arr = append(arr, anyx.ToString(v))
}
if i < len(attributes)-1 {
arr = append(arr, ",")
}
}
arr = append(arr, ")")
return strings.Join(arr, "")
}
// 将查询的值转为对应列类型的实际值,不全部转为字符串
func valueConvert(data interface{}, colType *sql.ColumnType) any {
if data == nil {
return nil
}
// 达梦特殊数据类型
if dmStruct, ok := data.(*dm.DmStruct); ok {
return ParseDmStruct(dmStruct)
}
// 列的数据库类型名
colDatabaseTypeName := strings.ToLower(colType.DatabaseTypeName())
// 这里把[]byte数据转成string
stringV := ""
if slicePtr, ok := data.(*[]uint8); ok {
stringV = string(*slicePtr)
// 如果类型是bit则直接返回第一个字节即可
if strings.Contains(colDatabaseTypeName, "bit") {
return (*slicePtr)[0]
}
if colDatabaseTypeName == "blob" {
return hex.EncodeToString(*slicePtr)
}
}
if colType == nil || colType.ScanType() == nil {
return stringV
}
colScanType := strings.ToLower(colType.ScanType().Name())
if strings.Contains(colScanType, "int") {
// 如果长度超过16位则返回字符串因为前端js长度大于16会丢失精度
if len(stringV) > 16 {
return stringV
}
intV, _ := strconv.Atoi(stringV)
switch colType.ScanType().Kind() {
case reflect.Int8:
return int8(intV)
case reflect.Uint8:
return uint8(intV)
case reflect.Int64:
return int64(intV)
case reflect.Uint64:
return uint64(intV)
case reflect.Uint:
return uint(intV)
default:
return intV
}
}
if strings.Contains(colScanType, "float") || strings.Contains(colDatabaseTypeName, "decimal") {
floatV, _ := strconv.ParseFloat(stringV, 64)
return floatV
}
return stringV
}
// 包装sql执行相关错误
func wrapSqlError(err error) error {
if err == context.Canceled {
return errorx.NewBiz("取消执行")
return errorx.NewBiz("execution cancel")
}
if err == context.DeadlineExceeded {
return errorx.NewBiz("执行超时")
return errorx.NewBiz("execution timeout")
}
return err
}

View File

@@ -2,13 +2,17 @@ package dbi
import (
"database/sql"
"encoding/hex"
"errors"
"io"
"mayfly-go/internal/db/dbm/sqlparser"
"mayfly-go/internal/db/dbm/sqlparser/pgsql"
"reflect"
"strconv"
"strings"
pq "gitee.com/liuzongyang/libpq"
"github.com/may-fly/cast"
)
const DefaultQuoter = `"`
@@ -157,6 +161,12 @@ type ColumnHelper interface {
// FixColumn 根据数据库类型修复字段长度、精度等
FixColumn(column *Column)
// GetScanDestPtr 获取scan列目标值指针用于在*sql.Rows.Scan()填充该值
GetScanDestPtr(*QueryColumn) any
// ConvertScanDestValue 将scan的填充的原始值data转为可阅读的值如[]byte -> string or number...
ConvertScanDestValue(data any, qc *QueryColumn) any
}
type DefaultColumnHelper struct {
@@ -168,6 +178,74 @@ func (dd *DefaultColumnHelper) ToColumn(commonColumn *Column) {}
func (dd *DefaultColumnHelper) FixColumn(column *Column) {}
func (dd *DefaultColumnHelper) GetScanDestPtr(qc *QueryColumn) any {
return &[]byte{}
}
func (dd *DefaultColumnHelper) ConvertScanDestValue(data any, qc *QueryColumn) any {
if data == nil {
return nil
}
colType := qc.SqlColType
// 列的数据库类型名
colDatabaseTypeName := strings.ToLower(colType.DatabaseTypeName())
stringV := ""
if slicePtr, ok := data.(*[]uint8); ok {
bytes := *slicePtr
if bytes == nil {
return nil
}
// 如果类型是bit则直接返回第一个字节即可
if strings.Contains(colDatabaseTypeName, "bit") {
return (bytes)[0]
}
if colDatabaseTypeName == "blob" {
return hex.EncodeToString(bytes)
}
// 把[]byte数据转成string
stringV = string(bytes)
} else {
stringV = cast.ToString(data)
}
if colType == nil || colType.ScanType() == nil {
return stringV
}
colScanType := strings.ToLower(colType.ScanType().Name())
if strings.Contains(colScanType, "int") {
// 如果长度超过16位则返回字符串因为前端js长度大于16会丢失精度
if len(stringV) > 16 {
return stringV
}
intV, _ := strconv.Atoi(stringV)
switch colType.ScanType().Kind() {
case reflect.Int8:
return int8(intV)
case reflect.Uint8:
return uint8(intV)
case reflect.Int64:
return int64(intV)
case reflect.Uint64:
return uint64(intV)
case reflect.Uint:
return uint(intV)
default:
return intV
}
}
if strings.Contains(colScanType, "float") || strings.Contains(colDatabaseTypeName, "decimal") {
floatV, _ := strconv.ParseFloat(stringV, 64)
return floatV
}
return stringV
}
// DumpHelper 导出辅助方法
type DumpHelper interface {
BeforeInsert(writer io.Writer, tableName string)

View File

@@ -1,6 +1,7 @@
package dm
import (
"encoding/hex"
"fmt"
"io"
"mayfly-go/internal/db/dbm/dbi"
@@ -9,6 +10,8 @@ import (
"regexp"
"strings"
"time"
"gitee.com/chunanyong/dm"
)
var (
@@ -174,6 +177,7 @@ func (dc *DataHelper) WrapValue(dbColumnValue any, dataType dbi.DataType) string
}
type ColumnHelper struct {
dbi.DefaultColumnHelper
}
func (ch *ColumnHelper) ToCommonColumn(dialectColumn *dbi.Column) {
@@ -212,6 +216,58 @@ func (ch *ColumnHelper) FixColumn(column *dbi.Column) {
}
}
func (dd *ColumnHelper) GetScanDestPtr(qc *dbi.QueryColumn) any {
if qc.Type == "st_point" {
return &dm.DmStruct{}
}
return dd.DefaultColumnHelper.GetScanDestPtr(qc)
}
func (dd *ColumnHelper) ConvertScanDestValue(data any, qc *dbi.QueryColumn) any {
if data == nil {
return nil
}
// 达梦特殊数据类型
if dmStruct, ok := data.(*dm.DmStruct); ok {
return ParseDmStruct(dmStruct)
}
return dd.DefaultColumnHelper.ConvertScanDestValue(data, qc)
}
func ParseDmStruct(dmStruct *dm.DmStruct) string {
if !dmStruct.Valid {
return ""
}
name, _ := dmStruct.GetSQLTypeName()
attributes, _ := dmStruct.GetAttributes()
arr := make([]string, len(attributes))
arr = append(arr, name, "(")
for i, v := range attributes {
if blb, ok1 := v.(*dm.DmBlob); ok1 {
if blb.Valid {
length, _ := blb.GetLength()
var dest = make([]byte, length)
_, _ = blb.Read(dest)
// 2进制转16进制字符串
hexStr := hex.EncodeToString(dest)
arr = append(arr, "0x", strings.ToUpper(hexStr))
}
} else {
arr = append(arr, anyx.ToString(v))
}
if i < len(attributes)-1 {
arr = append(arr, ",")
}
}
arr = append(arr, ")")
return strings.Join(arr, "")
}
type DumpHelper struct {
}

View File

@@ -178,6 +178,7 @@ func (dc *DataHelper) WrapValue(dbColumnValue any, dataType dbi.DataType) string
}
type ColumnHelper struct {
dbi.DefaultColumnHelper
}
func (ch *ColumnHelper) ToCommonColumn(dialectColumn *dbi.Column) {

View File

@@ -177,6 +177,7 @@ func (dc *DataHelper) WrapValue(dbColumnValue any, dataType dbi.DataType) string
}
type ColumnHelper struct {
dbi.DefaultColumnHelper
}
func (ch *ColumnHelper) ToCommonColumn(dialectColumn *dbi.Column) {

View File

@@ -141,6 +141,7 @@ func (dc *DataHelper) WrapValue(dbColumnValue any, dataType dbi.DataType) string
}
type ColumnHelper struct {
dbi.DefaultColumnHelper
}
func (ch *ColumnHelper) ToCommonColumn(dialectColumn *dbi.Column) {

View File

@@ -173,6 +173,7 @@ func (dc *DataHelper) WrapValue(dbColumnValue any, dataType dbi.DataType) string
}
type ColumnHelper struct {
dbi.DefaultColumnHelper
}
func (ch *ColumnHelper) ToCommonColumn(column *dbi.Column) {

View File

@@ -18,8 +18,6 @@ var (
dataTypeRegexp = regexp.MustCompile(`(\w+)\((\d*),?(\d*)\)`)
dateHelper = new(DataHelper)
// sqlite数据类型 映射 公共数据类型
commonColumnTypeMap = map[string]dbi.ColumnDataType{
"int": dbi.CommonTypeInt,
@@ -100,27 +98,27 @@ func (dc *DataHelper) FormatData(dbColumnValue any, dataType dbi.DataType) strin
switch dataType {
case dbi.DataTypeDateTime: // "2024-01-02T22:08:22.275697+08:00"
// 尝试用时间格式解析
res, err := time.Parse(time.DateTime, str)
_, err := time.Parse(time.DateTime, str)
if err == nil {
return str
}
res, _ = time.Parse(time.RFC3339, str)
res, _ := time.Parse(time.RFC3339, str)
return res.Format(time.DateTime)
case dbi.DataTypeDate: // "2024-01-02T00:00:00+08:00"
// 尝试用时间格式解析
res, err := time.Parse(time.DateOnly, str)
_, err := time.Parse(time.DateOnly, str)
if err == nil {
return str
}
res, _ = time.Parse(time.RFC3339, str)
res, _ := time.Parse(time.RFC3339, str)
return res.Format(time.DateOnly)
case dbi.DataTypeTime: // "0000-01-01T22:08:22.275688+08:00"
// 尝试用时间格式解析
res, err := time.Parse(time.TimeOnly, str)
_, err := time.Parse(time.TimeOnly, str)
if err == nil {
return str
}
res, _ = time.Parse(time.RFC3339, str)
res, _ := time.Parse(time.RFC3339, str)
return res.Format(time.TimeOnly)
}
return str
@@ -152,6 +150,7 @@ func (dc *DataHelper) WrapValue(dbColumnValue any, dataType dbi.DataType) string
}
type ColumnHelper struct {
dbi.DefaultColumnHelper
}
func (ch *ColumnHelper) ToCommonColumn(dialectColumn *dbi.Column) {
@@ -176,10 +175,6 @@ func (ch *ColumnHelper) ToColumn(commonColumn *dbi.Column) {
}
}
func (ch *ColumnHelper) FixColumn(column *dbi.Column) {
}
type DumpHelper struct {
dbi.DefaultDumpHelper
}

View File

@@ -3,6 +3,7 @@ package api
import (
"mayfly-go/internal/machine/application"
tagapp "mayfly-go/internal/tag/application"
"mayfly-go/internal/tag/domain/entity"
tagentity "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/collx"
@@ -16,7 +17,7 @@ type Dashbord struct {
func (m *Dashbord) Dashbord(rc *req.Ctx) {
accountId := rc.GetLoginAccount().Id
tagCodePaths := m.TagTreeApp.GetAccountTags(accountId, &tagentity.TagTreeQuery{Types: collx.AsArray(tagentity.TagTypeMachineAuthCert)}).GetCodePaths()
tagCodePaths := m.TagTreeApp.GetAccountTags(accountId, &tagentity.TagTreeQuery{TypePaths: collx.AsArray(entity.NewTypePaths(tagentity.TagTypeMachine, tagentity.TagTypeAuthCert))}).GetCodePaths()
machineCodes := tagentity.GetCodesByCodePaths(tagentity.TagTypeMachine, tagCodePaths...)
rc.ResData = collx.M{

View File

@@ -44,7 +44,7 @@ func (m *Machine) Machines(rc *req.Ctx) {
condition, pageParam := req.BindQueryAndPage(rc, new(entity.MachineQuery))
tags := m.TagApp.GetAccountTags(rc.GetLoginAccount().Id, &tagentity.TagTreeQuery{
Types: collx.AsArray(tagentity.TagTypeMachineAuthCert),
TypePaths: collx.AsArray(tagentity.NewTypePaths(tagentity.TagTypeMachine, tagentity.TagTypeAuthCert)),
CodePathLikes: collx.AsArray(condition.TagPath),
})
// 不存在可操作的机器-授权凭证标签,即没有可操作数据
@@ -71,7 +71,7 @@ func (m *Machine) Machines(rc *req.Ctx) {
})...)
// 填充授权凭证信息
m.ResourceAuthCertApp.FillAuthCertByAcNames(tagentity.GetCodesByCodePaths(tagentity.TagTypeMachineAuthCert, tagCodePaths...), collx.ArrayMap(machinevos, func(mvo *vo.MachineVO) tagentity.IAuthCert {
m.ResourceAuthCertApp.FillAuthCertByAcNames(tagentity.GetCodesByCodePaths(tagentity.TagTypeAuthCert, tagCodePaths...), collx.ArrayMap(machinevos, func(mvo *vo.MachineVO) tagentity.IAuthCert {
return mvo
})...)

View File

@@ -337,7 +337,7 @@ func (m *machineAppImpl) toMi(me *entity.Machine, authCert *tagentity.ResourceAu
mi.Name = me.Name
mi.Ip = me.Ip
mi.Port = me.Port
mi.CodePath = m.tagApp.ListTagPathByTypeAndCode(int8(tagentity.TagTypeMachineAuthCert), authCert.Name)
mi.CodePath = m.tagApp.ListTagPathByTypeAndCode(int8(tagentity.TagTypeAuthCert), authCert.Name)
mi.EnableRecorder = me.EnableRecorder
mi.Protocol = me.Protocol
@@ -363,7 +363,7 @@ func (m *machineAppImpl) genMachineResourceTag(me *entity.Machine, authCerts []*
return &tagdto.ResourceTag{
Code: val.Name,
Name: val.Username,
Type: tagentity.TagTypeMachineAuthCert,
Type: tagentity.TagTypeAuthCert,
}
})

View File

@@ -34,7 +34,7 @@ func (m *Mongo) Mongos(rc *req.Ctx) {
// 不存在可访问标签id即没有可操作数据
tags := m.TagApp.GetAccountTags(rc.GetLoginAccount().Id, &tagentity.TagTreeQuery{
Types: []tagentity.TagType{tagentity.TagTypeMongo},
TypePaths: collx.AsArray(tagentity.NewTypePaths(tagentity.TagTypeMongo)),
CodePathLikes: []string{queryCond.TagPath},
})
if len(tags) == 0 {

View File

@@ -2,7 +2,6 @@ package api
import (
"context"
"fmt"
"mayfly-go/internal/common/consts"
"mayfly-go/internal/common/utils"
"mayfly-go/internal/redis/api/form"
@@ -34,7 +33,7 @@ func (r *Redis) RedisList(rc *req.Ctx) {
// 不存在可访问标签id即没有可操作数据
tags := r.TagApp.GetAccountTags(rc.GetLoginAccount().Id, &tagentity.TagTreeQuery{
Types: collx.AsArray(tagentity.TagTypeRedis),
TypePaths: collx.AsArray(tagentity.NewTypePaths(tagentity.TagTypeRedis)),
CodePathLikes: collx.AsArray(queryCond.TagPath),
})
if len(tags) == 0 {
@@ -59,15 +58,22 @@ func (r *Redis) TestConn(rc *req.Ctx) {
form := &form.Redis{}
redis := req.BindJsonAndCopyTo[*entity.Redis](rc, form, new(entity.Redis))
authCert := &tagentity.ResourceAuthCert{
Username: form.Username,
Ciphertext: form.Password,
CiphertextType: tagentity.AuthCertCiphertextTypePassword,
Type: tagentity.AuthCertTypePrivate,
}
if form.Mode == string(rdm.SentinelMode) {
encPwd, err := utils.PwdAesEncrypt(form.RedisNodePassword)
biz.ErrIsNil(err)
authCert.SetExtraValue("redisNodePassword", encPwd)
}
biz.ErrIsNil(r.RedisApp.TestConn(&dto.SaveRedis{
Redis: redis,
AuthCert: &tagentity.ResourceAuthCert{
Name: fmt.Sprintf("redis_%s_ac", redis.Code),
Username: form.Username,
Ciphertext: form.Password,
CiphertextType: tagentity.AuthCertCiphertextTypePassword,
Type: tagentity.AuthCertTypePrivate,
},
Redis: redis,
AuthCert: authCert,
}))
}

View File

@@ -12,8 +12,6 @@ import (
"mayfly-go/pkg/utils/collx"
"sort"
"strings"
"github.com/may-fly/cast"
)
type TagTree struct {
@@ -23,14 +21,14 @@ type TagTree struct {
func (p *TagTree) GetTagTree(rc *req.Ctx) {
tagTypesStr := rc.Query("type")
var tagTypes []entity.TagType
var typePaths []entity.TypePath
if tagTypesStr != "" {
tagTypes = collx.ArrayMap[string, entity.TagType](strings.Split(tagTypesStr, ","), func(val string) entity.TagType {
return entity.TagType(cast.ToInt8(val))
typePaths = collx.ArrayMap[string, entity.TypePath](strings.Split(tagTypesStr, ","), func(val string) entity.TypePath {
return entity.TypePath(val)
})
}
accountTags := p.TagTreeApp.GetAccountTags(rc.GetLoginAccount().Id, &entity.TagTreeQuery{Types: tagTypes})
accountTags := p.TagTreeApp.GetAccountTags(rc.GetLoginAccount().Id, &entity.TagTreeQuery{TypePaths: typePaths})
if len(accountTags) == 0 {
rc.ResData = []any{}
return
@@ -114,9 +112,9 @@ func (p *TagTree) MovingTag(rc *req.Ctx) {
// 获取用户可操作的标签路径
func (p *TagTree) TagResources(rc *req.Ctx) {
resourceType := int8(rc.PathParamInt("rtype"))
accountId := rc.GetLoginAccount().Id
tagResources := p.TagTreeApp.GetAccountTags(accountId, &entity.TagTreeQuery{Types: collx.AsArray(entity.TagType(resourceType))})
resourceType := rc.Query("resourceType")
biz.NotEmpty(resourceType, "resourceType cannot be empty")
tagResources := p.TagTreeApp.GetAccountTags(rc.GetLoginAccount().Id, &entity.TagTreeQuery{TypePaths: collx.AsArray(entity.TypePath(resourceType))})
tagPath2Resource := collx.ArrayToMap[*dto.SimpleTagTree, string](tagResources, func(tagResource *dto.SimpleTagTree) string {
return string(entity.CodePath(tagResource.CodePath).GetTag())
@@ -133,11 +131,11 @@ func (p *TagTree) CountTagResource(rc *req.Ctx) {
accountId := rc.GetLoginAccount().Id
machineCodes := entity.GetCodesByCodePaths(entity.TagTypeMachine, p.TagTreeApp.GetAccountTags(accountId, &entity.TagTreeQuery{
Types: collx.AsArray(entity.TagTypeMachineAuthCert),
TypePaths: collx.AsArray(entity.NewTypePaths(entity.TagTypeMachine, entity.TagTypeAuthCert)),
CodePathLikes: collx.AsArray(tagPath),
}).GetCodePaths()...)
dbCodes := entity.GetCodesByCodePaths(entity.TagTypeDbInstance, p.TagTreeApp.GetAccountTags(accountId, &entity.TagTreeQuery{
dbCodes := entity.GetCodesByCodePaths(entity.TagTypeDb, p.TagTreeApp.GetAccountTags(accountId, &entity.TagTreeQuery{
Types: collx.AsArray(entity.TagTypeDb),
CodePathLikes: collx.AsArray(tagPath),
}).GetCodePaths()...)

View File

@@ -152,7 +152,6 @@ func (r *resourceAuthCertAppImpl) RelateAuthCert(ctx context.Context, params *dt
oldName2AuthCert := collx.ArrayToMap(oldAuthCert, func(ac *entity.ResourceAuthCert) string {
return ac.Name
})
acTagType := GetResourceAuthCertTagType(params.ResourceType)
for _, unmodifyAcName := range unmodifyAcNames {
unmodifyAc := name2AuthCert[unmodifyAcName]
@@ -163,8 +162,8 @@ func (r *resourceAuthCertAppImpl) RelateAuthCert(ctx context.Context, params *dt
}
// 如果修改了用户名且该凭证关联至标签则需要更新对应的标签名资源授权凭证类型的标签名为username
if oldAuthCert.Username != unmodifyAc.Username && acTagType != 0 {
r.tagTreeApp.UpdateTagName(ctx, acTagType, unmodifyAcName, unmodifyAc.Username)
if oldAuthCert.Username != unmodifyAc.Username {
r.tagTreeApp.UpdateTagName(ctx, entity.TagTypeAuthCert, unmodifyAcName, unmodifyAc.Username)
}
logx.DebugfContext(ctx, "RelateAuthCert[%d-%s] - Update Authorization credential - [%v]", resourceType, resourceCode, unmodifyAcName)
if err := r.UpdateByCond(ctx, unmodifyAc, &entity.ResourceAuthCert{Name: unmodifyAcName, ResourceCode: resourceCode, ResourceType: resourceType}); err != nil {
@@ -328,25 +327,16 @@ func (r *resourceAuthCertAppImpl) addAuthCert(ctx context.Context, rac *entity.R
resourceCode := rac.ResourceCode
resourceType := rac.ResourceType
// 资源对应的授权凭证标签类型若为0则说明该资源不需要关联至资源tagTree
authCertTagType := GetResourceAuthCertTagType(entity.TagType(resourceType))
var resourceTagCodePaths []string
// 如果该资源存在对应的授权凭证标签类型则说明需要关联至tagTree否则直接从授权凭证库中验证资源编号是否正确即可一个资源最少有一个授权凭证
if authCertTagType != 0 {
// 获取资源编号对应的资源标签信息
resourceTags, _ := r.tagTreeApp.ListByCond(&entity.TagTree{Type: entity.TagType(resourceType), Code: resourceCode})
// 资源标签tagPath相当于父tag
resourceTagCodePaths = collx.ArrayMap(resourceTags, func(tag *entity.TagTree) string {
return tag.CodePath
})
if len(resourceTagCodePaths) == 0 {
return errorx.NewBizI(ctx, imsg.ErrResourceTagNotExist, "resourceCode", resourceCode)
}
} else {
if r.CountByCond(&entity.ResourceAuthCert{ResourceCode: resourceCode, ResourceType: resourceType}) == 0 {
return errorx.NewBizI(ctx, imsg.ErrResourceNotExist)
}
// 获取资源编号对应的资源标签信息
resourceTags, _ := r.tagTreeApp.ListByCond(&entity.TagTree{Type: entity.TagType(resourceType), Code: resourceCode})
// 资源标签tagPath相当于父tag
resourceTagCodePaths = collx.ArrayMap(resourceTags, func(tag *entity.TagTree) string {
return tag.CodePath
})
if len(resourceTagCodePaths) == 0 {
return errorx.NewBizI(ctx, imsg.ErrResourceTagNotExist, "resourceCode", resourceCode)
}
// 如果密文类型不为公共凭证,则进行加密。公共凭证密文内容存的是明文的公共凭证名
@@ -357,11 +347,11 @@ func (r *resourceAuthCertAppImpl) addAuthCert(ctx context.Context, rac *entity.R
return r.Tx(ctx, func(ctx context.Context) error {
// 若存在需要关联到的资源标签,则关联到对应的资源标签下
if len(resourceTagCodePaths) > 0 {
logx.DebugfContext(ctx, "[%d-%s] - AC tag [%d-%s] associated to the resource tag [%v] ", resourceType, resourceCode, authCertTagType, rac.Name, resourceTagCodePaths)
logx.DebugfContext(ctx, "[%d-%s] - AC tag [%d-%s] associated to the resource tag [%v] ", resourceType, resourceCode, entity.TagTypeAuthCert, rac.Name, resourceTagCodePaths)
return r.tagTreeApp.SaveResourceTag(ctx, &dto.SaveResourceTag{
ResourceTag: &dto.ResourceTag{
Code: rac.Name,
Type: GetResourceAuthCertTagType(entity.TagType(resourceType)),
Type: entity.TagTypeAuthCert,
Name: rac.Username,
},
ParentTagCodePaths: resourceTagCodePaths,
@@ -402,11 +392,8 @@ func (r *resourceAuthCertAppImpl) updateAuthCert(ctx context.Context, rac *entit
// 修改了用户名,则需要同步更新对应授权凭证标签里的名称
if rac.Username != oldRac.Username && rac.ResourceType == int8(entity.TagTypeMachine) {
authCertTagType := GetResourceAuthCertTagType(entity.TagType(oldRac.ResourceType))
if authCertTagType != 0 {
if err := r.tagTreeApp.UpdateTagName(ctx, authCertTagType, oldRac.Name, rac.Username); err != nil {
return errorx.NewBiz("Synchronously updating the authorization credential tag name failed")
}
if err := r.tagTreeApp.UpdateTagName(ctx, entity.TagTypeAuthCert, oldRac.Name, rac.Username); err != nil {
return errorx.NewBiz("Synchronously updating the authorization credential tag name failed")
}
}
}
@@ -444,17 +431,3 @@ func (r *resourceAuthCertAppImpl) decryptAuthCert(authCert *entity.ResourceAuthC
}
return authCert, nil
}
// GetResourceAuthCertTagType 根据资源类型获取对应的授权凭证标签类型return 0 说明该资源授权凭证不关联至tagTree
func GetResourceAuthCertTagType(resourceType entity.TagType) entity.TagType {
if resourceType == entity.TagTypeMachine {
return entity.TagTypeMachineAuthCert
}
if resourceType == entity.TagTypeDbInstance {
return entity.TagTypeDbAuthCert
}
// 该资源不存在对应的授权凭证标签即tag_tree不关联该资源的授权凭证
return 0
}

View File

@@ -49,12 +49,23 @@ func (rol *resourceOpLogAppImpl) AddResourceOpLog(ctx context.Context, codePath
}
tagTree := &entity.TagTree{CodePath: codePath}
if err := rol.tagTreeApp.GetByCond(tagTree); err != nil {
return errorx.NewBiz("resource not found")
return errorx.NewBiz("tag resource not found")
}
resourceType := tagTree.Type
// 获取第一段资源类型即可
pathSections := entity.CodePath(tagTree.CodePath).GetPathSections()
for _, ps := range pathSections {
if ps.Type == entity.TagTypeTag {
continue
}
resourceType = ps.Type
break
}
return rol.Save(ctx, &entity.ResourceOpLog{
ResourceCode: tagTree.Code,
ResourceType: int8(tagTree.Type),
ResourceType: int8(resourceType),
CodePath: tagTree.CodePath,
})
}

View File

@@ -15,7 +15,10 @@ import (
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/utils/collx"
"slices"
"strings"
"github.com/may-fly/cast"
)
type TagTree interface {
@@ -342,29 +345,110 @@ func (p *tagTreeAppImpl) GetAccountTags(accountId uint64, query *entity.TagTreeQ
var tagResources []*dto.SimpleTagTree
var accountTagPaths []string
if accountId != consts.AdminId {
if accountId == consts.AdminId {
// admin账号获取所有root tag进行查找过滤
tagTypeTags, _ := p.ListByCond(&entity.TagTree{Type: entity.TagTypeTag}, "code_path")
accountTagPaths = collx.ArrayFilter(collx.ArrayMap(tagTypeTags, func(item *entity.TagTree) string {
return item.CodePath
}), func(path string) bool {
return len(entity.CodePath(path).GetPathSections()) == 1
})
} else {
// 获取账号有权限操作的标签路径列表
accountTagPaths = p.ListTagByAccountId(accountId)
if len(accountTagPaths) == 0 {
return tagResources
}
}
if len(accountTagPaths) == 0 {
return tagResources
}
// 去除空字符串标签
tagPaths := collx.ArrayRemoveBlank(query.CodePathLikes)
// 如果需要查询指定标签下的资源标签,则需要与用户拥有的权限进行过滤,避免越权
if len(tagPaths) > 0 {
// 为空则说明为admin 则直接赋值需要获取的标签
if len(accountTagPaths) == 0 {
accountTagPaths = tagPaths
} else {
accountTagPaths = filterCodePaths(accountTagPaths, tagPaths)
accountTagPaths = filterCodePaths(accountTagPaths, tagPaths)
}
codePathLikes := accountTagPaths
needFilterAccountTagPaths := accountTagPaths
needFilter := false
typePaths := query.TypePaths
if len(typePaths) > 0 {
needFilterAccountTagPaths = make([]string, 0)
codePathLikes = []string{}
for _, typePath := range typePaths {
childOrderTypes := typePath.ToTagTypes()
// 如果不是获取所有子节点则需要追加Type进行过滤
if !query.GetAllChildren {
tagResourceQuery.Types = append(tagResourceQuery.Types, childOrderTypes[len(childOrderTypes)-1])
}
// 资源类型模糊匹配若childTypes = [machineType, authcertType] => machineType|%/authcertType|%/
// 标签加上路径即可过滤出需要的标签,-> tag1/tag2/machineType|%/authcertType|%/
childOrderTypesMatch := strings.Join(collx.ArrayMap(childOrderTypes, func(tt entity.TagType) string {
return cast.ToString(int8(tt)) + entity.CodePathResourceSeparator + "%"
}), entity.CodePathSeparator) + entity.CodePathSeparator
// 根据用户拥有的标签路径,赋值要过滤匹配的标签路径条件
for _, accountTag := range accountTagPaths {
accountTagCodePath := entity.CodePath(accountTag)
// 标签路径不包含资源段如tag1/tag2/1|xxx => tag1/tag2/
tagPath := accountTagCodePath.GetTag()
// 纯纯的标签类型(不包含资源段),则直接在该标签路径上补上对应的子资源类型匹配表达式
if tagPath == accountTagCodePath {
needFilterAccountTagPaths = append(needFilterAccountTagPaths, accountTag)
// 查询标签类型为标签时,特殊处理
if len(childOrderTypes) == 1 && childOrderTypes[0] == entity.TagTypeTag {
codePathLikes = append(codePathLikes, accountTag)
continue
}
// 纯标签可能还有其他子标签的纯标签故需要多加一个匹配如tagPath = tag1/,而系统还有 tag1/tag2/ tag1/tag2/tag3等故需要多一个tag模糊匹配即tag1/%/xxx
codePathLikes = append(codePathLikes, accountTag+childOrderTypesMatch, accountTag+"%"+entity.CodePathSeparator+childOrderTypesMatch)
continue
}
// 将用户有权限操作的标签如 tag1/tag2/1|xxx 替换为tag1/tag2/1|%,并与需要查询的资源类型进行匹配
accountTagCodePathSections := accountTagCodePath.GetPathSections()
for _, section := range accountTagCodePathSections {
if section.Type == entity.TagTypeTag {
continue
}
section.Code = "%"
}
codePathLike := string(tagPath) + childOrderTypesMatch
if entity.CodePath(accountTagCodePathSections.ToCodePath()).CanAccess(codePathLike) {
codePathLikes = append(codePathLikes, codePathLike)
needFilterAccountTagPaths = append(needFilterAccountTagPaths, accountTag)
}
}
}
// 去重处理
codePathLikes = collx.ArrayDeduplicate(codePathLikes)
needFilter = true
}
// 账号权限经过处理为空,则说明没有用户可以操作的标签,直接返回即可
if needFilter && len(needFilterAccountTagPaths) == 0 {
return tagResources
}
tagResourceQuery.Codes = query.Codes
tagResourceQuery.CodePathLikes = accountTagPaths
tagResourceQuery.CodePathLikes = codePathLikes
p.ListByQuery(tagResourceQuery, &tagResources)
if needFilter {
tagResources = collx.ArrayFilter(tagResources, func(tr *dto.SimpleTagTree) bool {
return slices.ContainsFunc(needFilterAccountTagPaths, func(accountTagPath string) bool {
return entity.CodePath(accountTagPath).CanAccess(tr.CodePath)
})
})
}
return tagResources
}
@@ -391,8 +475,9 @@ func (p *tagTreeAppImpl) CanAccess(accountId uint64, tagPath ...string) error {
tagPaths := p.ListTagByAccountId(accountId)
// 判断该资源标签是否为该账号拥有的标签或其子标签
for _, v := range tagPaths {
accountTagCodePath := entity.CodePath(v)
for _, tp := range tagPath {
if strings.HasPrefix(tp, v) {
if accountTagCodePath.CanAccess(tp) {
return nil
}
}

View File

@@ -0,0 +1,38 @@
package application
import (
"fmt"
"mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/utils/collx"
"strings"
"testing"
"github.com/may-fly/cast"
)
func TestTagPath(t *testing.T) {
childOrderTypes := []int{1, 5}
childOrderTypesMatch := strings.Join(collx.ArrayMap(childOrderTypes, func(tt int) string {
return cast.ToString(tt) + entity.CodePathResourceSeparator + "%"
}), entity.CodePathSeparator) + entity.CodePathSeparator
fmt.Println(childOrderTypesMatch)
}
func TestTagPathMatch(t *testing.T) {
// accountCodePath := "tag1/tag2/2|xxdd/"
// resourceCodePath := "tag1/tag2/1|%/11|%/%"
codePathLike := "default/2|%/22|%/"
accountCodePath := "default/2|db_local/5|db_local_root/"
sections := entity.CodePath(accountCodePath).GetPathSections()
for _, section := range sections {
if section.Type == entity.TagTypeTag {
continue
}
section.Code = "%"
}
accountTagPathPattern := sections.ToCodePath()
match := strings.HasPrefix(codePathLike, accountTagPathPattern)
fmt.Println(match)
}

View File

@@ -1,15 +1,36 @@
package entity
import "mayfly-go/pkg/model"
import (
"mayfly-go/pkg/model"
"mayfly-go/pkg/utils/collx"
"strings"
"github.com/may-fly/cast"
)
type TypePath string
// ToTagTypes 转为路径对应的TagType数组
func (t TypePath) ToTagTypes() []TagType {
return collx.ArrayMap(strings.Split(string(t), CodePathSeparator), func(val string) TagType { return TagType(cast.ToInt8(val)) })
}
// NewTypePaths
func NewTypePaths(types ...TagType) TypePath {
return TypePath(strings.Join(collx.ArrayMap[TagType, string](types, func(val TagType) string { return cast.ToString(int8(val)) }), CodePathSeparator))
}
type TagTreeQuery struct {
model.Model
Types []TagType
TypePaths []TypePath // 类型路径。如 machineType/authcertType即获取机器下的授权凭证
Codes []string
CodePaths []string // 标识路径
Name string `json:"name"` // 名称
CodePathLikes []string
GetAllChildren bool // 是否获取所有子节点
}
type TeamQuery struct {

View File

@@ -34,13 +34,9 @@ const (
TagTypeDbInstance TagType = TagType(consts.ResourceTypeDbInstance) // 数据库实例
TagTypeRedis TagType = TagType(consts.ResourceTypeRedis)
TagTypeMongo TagType = TagType(consts.ResourceTypeMongo)
TagTypeAuthCert TagType = 5 // 授权凭证类型
// ----- (单独声明各个资源的授权凭证类型而不统一使用一个授权凭证类型是为了获取登录账号的授权凭证标签(ResourceAuthCertApp.GetAccountAuthCert)时,避免查出所有资源的授权凭证)
TagTypeMachineAuthCert TagType = 11 // 机器-授权凭证
TagTypeDbAuthCert TagType = 21 // 数据库-授权凭证
TagTypeDb TagType = 22 // 数据库名
TagTypeDb TagType = 22 // 数据库名
)
// 标签接口资源,如果要实现资源结构体填充标签信息,则资源结构体需要实现该接口
@@ -175,6 +171,14 @@ func (cp CodePath) GetAllPath() []string {
})
}
// CanAccess 判断该标签路径是否允许访问操作指定标签路径即是否为指定标签路径的父级路径。cp通常为用户拥有的标签路径
//
// // cp = tag1/tag2/ codePath = tag1/tag2/test/ -> true
// // cp = tag1/tag2/ codePath = tag1/ -> false
func (cp CodePath) CanAccess(codePath string) bool {
return strings.HasPrefix(codePath, string(cp))
}
// PathSection 标签路径段
type PathSection struct {
Type TagType `json:"type"` // 类型: -1.普通标签; 其他值则为对应的资源类型

View File

@@ -29,7 +29,7 @@ func InitTagTreeRouter(router *gin.RouterGroup) {
req.NewPost("/moving", m.MovingTag).Log(req.NewLogSaveI(imsg.LogTagMove)).RequiredPermissionCode("tag:save"),
req.NewGet("/resources/:rtype/tag-paths", m.TagResources),
req.NewGet("/resources/tag-paths", m.TagResources),
req.NewGet("/resources/count", m.CountTagResource),

View File

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

View File

@@ -0,0 +1,3 @@
UPDATE `t_tag_tree` SET type = 5 WHERE type = 21 or type = 11;
UPDATE `t_tag_tree` SET code_path = REPLACE(code_path, '/11|', '/5|') WHERE type = 5;
UPDATE `t_tag_tree` SET code_path = REPLACE(code_path, '/21|', '/5|') WHERE type = 5 or type = 22;