Compare commits

4 Commits

98 changed files with 3659 additions and 1490 deletions

View File

@@ -11,40 +11,40 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@logicflow/core": "^2.1.3",
"@logicflow/extension": "^2.1.5",
"@vueuse/core": "^13.9.0",
"@logicflow/core": "^2.1.4",
"@logicflow/extension": "^2.1.6",
"@vueuse/core": "^14.1.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-search": "^0.15.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0",
"asciinema-player": "^3.11.1",
"asciinema-player": "^3.12.1",
"axios": "^1.6.2",
"clipboard": "^2.0.11",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.18",
"dayjs": "^1.11.19",
"echarts": "^6.0.0",
"element-plus": "^2.11.4",
"element-plus": "^2.12.0",
"js-base64": "^3.7.8",
"jsencrypt": "^3.5.4",
"monaco-editor": "^0.54.0",
"monaco-editor": "^0.55.1",
"monaco-sql-languages": "^0.15.1",
"monaco-themes": "^0.4.7",
"nprogress": "^0.2.0",
"pinia": "^3.0.3",
"pinia": "^3.0.4",
"qrcode.vue": "^3.6.0",
"screenfull": "^6.0.2",
"sortablejs": "^1.15.6",
"sql-formatter": "^15.6.8",
"sql-formatter": "^15.6.10",
"trzsz": "^1.1.5",
"uuid": "^13.0.0",
"vue": "^v3.5.22",
"vue-i18n": "^11.1.12",
"vue": "^v3.6.0-alpha.4",
"vue-i18n": "^11.2.2",
"vue-router": "^4.6.3",
"vuedraggable": "^4.1.0"
"vuedraggable": "^4.1.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.14",
"@tailwindcss/vite": "^4.1.17",
"@types/crypto-js": "^4.2.2",
"@types/node": "^22.13.14",
"@types/nprogress": "^0.2.0",
@@ -52,16 +52,16 @@
"@typescript-eslint/eslint-plugin": "^8.35.0",
"@typescript-eslint/parser": "^8.35.0",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/compiler-sfc": "^3.5.18",
"@vue/compiler-sfc": "^3.5.22",
"autoprefixer": "^10.4.21",
"code-inspector-plugin": "^1.0.4",
"eslint": "^9.29.0",
"eslint-plugin-vue": "^10.5.0",
"postcss": "^8.5.6",
"prettier": "^3.6.1",
"sass": "^1.93.2",
"tailwindcss": "^4.1.14",
"typescript": "^5.9.2",
"sass": "^1.94.1",
"tailwindcss": "^4.1.17",
"typescript": "^5.9.3",
"vite": "npm:rolldown-vite@latest",
"vite-plugin-progress": "0.0.7",
"vue-eslint-parser": "^10.2.0"

View File

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

View File

@@ -1,3 +1,11 @@
import * as XLSX from 'xlsx';
/**
* 导出CSV文件
* @param filename 文件名
* @param columns 列信息
* @param datas 数据
*/
export function exportCsv(filename: string, columns: string[], datas: []) {
// 二维数组
const cvsData = [columns];
@@ -30,6 +38,11 @@ export function exportCsv(filename: string, columns: string[], datas: []) {
exportFile(`${filename}.csv`, csvString);
}
/**
* 导出文件
* @param filename 文件名
* @param content 文件内容
*/
export function exportFile(filename: string, content: string) {
// 导出
let link = document.createElement('a');
@@ -44,3 +57,77 @@ export function exportFile(filename: string, content: string) {
link.click();
document.body.removeChild(link); // 下载完成后移除元素
}
/**
* 计算字符串显示宽度(考虑中英文字符差异)
* @param str 要计算的字符串
* @returns 计算后的宽度值
*/
function getStringWidth(str: string): number {
if (!str) return 0;
// 统计中文字符数量(包括中文标点)
const chineseChars = str.match(/[\u4e00-\u9fa5\u3000-\u303f\uff00-\uffef]/g);
const chineseCount = chineseChars ? chineseChars.length : 0;
// 英文字符数量
const englishCount = str.length - chineseCount;
// 中文字符按2个单位宽度计算英文字符按1个单位宽度计算
return chineseCount * 2 + englishCount;
}
/**
* 导出Excel文件
* @param filename 文件名
* @param sheets 多个工作表数据,每个工作表包含名称、列信息和数据
* 示例: [{name: 'Sheet1', columns: ['列1', '列2'], datas: [{col1: '值1', col2: '值2'}]}]
*/
export function exportExcel(filename: string, sheets: { name: string; columns: string[]; datas: any[] }[]) {
// 创建工作簿
const wb = XLSX.utils.book_new();
// 处理每个工作表
sheets.forEach((sheet) => {
// 准备表头
const headers: any = {};
sheet.columns.forEach((col) => {
headers[col] = col;
});
// 准备数据
const data = [headers, ...sheet.datas];
// 创建工作表
const ws = XLSX.utils.json_to_sheet(data, { skipHeader: true });
// 设置列宽自适应
const colWidths: { wch: number }[] = [];
sheet.columns.forEach((col, index) => {
// 计算列宽:取表头和前几行数据的最大宽度
let maxWidth = getStringWidth(col); // 表头宽度
const checkCount = Math.min(sheet.datas.length, 10); // 只检查前10行数据
for (let i = 0; i < checkCount; i++) {
const cellData = sheet.datas[i][col];
const cellStr = cellData ? String(cellData) : '';
const cellWidth = getStringWidth(cellStr);
if (cellWidth > maxWidth) {
maxWidth = cellWidth;
}
}
// 设置最小宽度为8最大宽度为80
colWidths.push({ wch: Math.min(Math.max(maxWidth + 2, 8), 80) });
});
// 应用列宽设置
ws['!cols'] = colWidths;
// 添加工作表到工作簿
XLSX.utils.book_append_sheet(wb, ws, sheet.name);
});
// 导出文件
XLSX.writeFile(wb, `${filename}.xlsx`);
}

View File

@@ -1,16 +1,30 @@
<template>
<el-tooltip :content="formatByteSize(fileDetail?.size)" placement="left">
<el-link v-if="props.canDownload" target="_blank" rel="noopener noreferrer" icon="Download" type="primary" :href="getFileUrl(props.fileKey)"></el-link>
</el-tooltip>
<el-button v-if="loading" :loading="loading" name="loading" link type="primary" />
{{ fileDetail?.filename }}
<template v-else>
<el-tooltip :content="fileSize" placement="left">
<el-link
v-if="props.canDownload"
target="_blank"
rel="noopener noreferrer"
icon="Download"
type="primary"
:href="getFileUrl(props.fileKey)"
></el-link>
</el-tooltip>
{{ fileDetail?.filename }}
<!-- 文件大小显示 -->
<span v-if="props.showFileSize && fileDetail?.size" class="file-size">({{ fileSize }})</span>
</template>
</template>
<script lang="ts" setup>
import { onMounted, ref, watch } from 'vue';
import { computed, onMounted, Ref, ref, watch } from 'vue';
import openApi from '@/common/openApi';
import { getFileUrl } from '@/common/request';
import { formatByteSize } from '@/common/utils/format';
const props = defineProps({
fileKey: {
type: String,
@@ -23,8 +37,14 @@ const props = defineProps({
type: Boolean,
default: true,
},
showFileSize: {
type: Boolean,
default: false,
},
});
const loading: Ref<boolean> = ref(false);
onMounted(async () => {
setFileInfo();
});
@@ -38,23 +58,38 @@ watch(
}
);
const fileSize = computed(() => {
return fileDetail.value.size ? formatByteSize(fileDetail.value.size) : '';
});
const fileDetail: any = ref({});
const setFileInfo = async () => {
if (!props.fileKey) {
return;
}
if (props.files && props.files.length > 0) {
const file: any = props.files.find((file: any) => {
return file.fileKey === props.fileKey;
});
fileDetail.value = file;
return;
}
try {
if (!props.fileKey) {
return;
}
loading.value = true;
if (props.files && props.files.length > 0) {
const file: any = props.files.find((file: any) => {
return file.fileKey === props.fileKey;
});
fileDetail.value = file;
return;
}
const files = await openApi.getFileDetail([props.fileKey]);
fileDetail.value = files?.[0];
const files = await openApi.getFileDetail([props.fileKey]);
fileDetail.value = files?.[0];
} finally {
loading.value = false;
}
};
</script>
<style lang="scss"></style>
<style lang="scss" scoped>
.file-size {
margin-left: 1px;
color: #909399;
font-size: 8px;
}
</style>

View File

@@ -34,15 +34,8 @@ import 'monaco-editor/esm/vs/editor/contrib/suggest/browser/suggestInlineComplet
import { editor, languages } from 'monaco-editor';
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker.js?worker';
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
// 主题仓库 https://github.com/brijeshb42/monaco-themes
// 主题例子 https://editor.bitwiser.in/
// import Monokai from 'monaco-themes/themes/Monokai.json'
// import Active4D from 'monaco-themes/themes/Active4D.json'
// import ahe from 'monaco-themes/themes/All Hallows Eve.json'
// import bop from 'monaco-themes/themes/Birds of Paradise.json'
// import krTheme from 'monaco-themes/themes/krTheme.json'
// import Dracula from 'monaco-themes/themes/Dracula.json'
import SolarizedLight from 'monaco-themes/themes/Solarized-light.json';
import SolarizedLight from './themes/Solarized-light.json';
import SolarizedDark from './themes/Solarized-dark.json';
import { language as shellLan } from 'monaco-editor/esm/vs/basic-languages/shell/shell.js';
import { ElOption, ElSelect } from 'element-plus';
@@ -226,6 +219,7 @@ const initMonacoEditorIns = () => {
// options参数参考 https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html#language
// 初始化一些主题
monaco.editor.defineTheme('SolarizedLight', SolarizedLight);
monaco.editor.defineTheme('SolarizedDark', SolarizedDark);
defaultOptions.language = state.languageMode;
defaultOptions.theme = themeConfig.value.editorTheme;
let options = Object.assign(defaultOptions, props.options as any);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,5 @@
import { exportExcel } from '@/common/utils/export';
export default {
db: {
// db instance
@@ -99,6 +101,7 @@ export default {
cancelFiexd: 'Cancel Fixed',
formView: 'Form View',
genJson: 'Generating JSON',
exportExcel: 'Export Excel',
exportCsv: 'Export CSV',
exportSql: 'Export SQL',
onlySelectOneData: 'Only one row can be selected',

View File

@@ -1,6 +1,7 @@
export default {
es: {
keywordPlaceholder: 'host / name / code',
protocol: 'Protocol',
port: 'Port',
size: 'size',
docs: 'docs',

View File

@@ -113,5 +113,9 @@ export default {
taskBeginTime: 'Start Time',
flowAudit: 'Process Audit',
notify: 'Notify',
aitask: 'AI Task',
aiAuditRule: 'Audit Rule',
aiAuditRuleTip: 'Please input the audit rule',
},
};

View File

@@ -190,6 +190,15 @@ export default {
loginFailCountPlaceholder: 'Disable login after n failed login attempts',
loginFainMin: 'Prohibited login time',
loginFailMinPlaceholder: 'After a specified number of login failures, re-login is prohibited within m minutes',
aiModelConf: 'AI Model Config',
aiModelType: 'Model Type',
aiModelTypePlaceholder: 'Please select a model type',
aiModel: 'Model',
aiModelPlaceholder: 'Please enter the model',
aiBaseUrl: 'Base URL',
aiBaseUrlPlaceholder: 'Please enter the model request URL',
aiApiKey: 'API Key',
},
syslog: {
operator: 'Operator',

View File

@@ -98,6 +98,7 @@ export default {
cancelFiexd: '取消固定',
formView: '表单视图',
genJson: '生成JSON',
exportExcel: '导出Excel',
exportCsv: '导出CSV',
exportSql: '导出SQL',
onlySelectOneData: '只能选择一行数据',

View File

@@ -1,6 +1,7 @@
export default {
es: {
keywordPlaceholder: 'host / 名称 / 编号',
protocol: '协议',
port: '端口',
size: '存储大小',
docs: '文档数',

View File

@@ -94,7 +94,7 @@ export default {
waitProcess: '待处理',
pass: '通过',
reject: '拒绝',
back: '退',
back: '退',
canceled: '取消',
// FlowBizType
dbSqlExec: 'DBMS-执行SQL',
@@ -113,5 +113,9 @@ export default {
taskBeginTime: '开始时间',
flowAudit: '流程审批',
notify: '通知',
aitask: 'AI任务',
aiAuditRule: '审核规则',
aiAuditRuleTip: '请输入审核规则',
},
};

View File

@@ -190,6 +190,15 @@ export default {
loginFailCountPlaceholder: '登录失败n次后禁止登录',
loginFainMin: '登录失败禁止登录时间',
loginFailMinPlaceholder: '登录失败指定次数后禁止m分钟内再次登录',
aiModelConf: 'AI模型配置',
aiModelType: '模型类型',
aiModelTypePlaceholder: '选择AI模型类型',
aiModel: '模型',
aiModelPlaceholder: '请输入模型',
aiBaseUrl: '地址',
aiBaseUrlPlaceholder: '请输入模型请求地址',
aiApiKey: 'API Key',
},
syslog: {
operator: '操作人',

View File

@@ -62,6 +62,7 @@
<el-option label="vs" value="vs"> </el-option>
<el-option label="vs-dark" value="vs-dark"> </el-option>
<el-option label="SolarizedLight" value="SolarizedLight"> </el-option>
<el-option label="SolarizedDark" value="SolarizedDark"> </el-option>
</el-select>
</div>
</div>

View File

@@ -5,47 +5,45 @@
<DrawerHeader :header="title" :back="cancel" />
</template>
<el-form :model="form" ref="formRef" :rules="rules" label-width="auto">
<el-form :model="modelValue" ref="formRef" :rules="rules" label-width="auto">
<el-form-item prop="bizType" :label="$t('flow.bizType')">
<EnumSelect v-model="form.bizType" :enums="FlowBizType" />
<EnumSelect v-model="modelValue.bizType" :enums="FlowBizType" @change="changeBizType" />
</el-form-item>
<el-form-item prop="remark" :label="$t('common.remark')">
<el-input v-model.trim="form.remark" type="textarea" auto-complete="off" clearable></el-input>
<el-input v-model.trim="modelValue.remark" type="textarea" auto-complete="off" clearable></el-input>
</el-form-item>
<el-divider content-position="left">{{ $t('flow.bizInfo') }}</el-divider>
<component
ref="bizFormRef"
v-if="form.bizType"
:is="bizComponents[form.bizType]"
v-model:bizForm="form.bizForm"
v-if="modelValue.bizType"
:is="bizComponents[modelValue.bizType]"
v-model:bizForm="modelValue.bizForm"
@changeResourceCode="changeResourceCode"
>
</component>
</el-form>
<span v-if="flowProcdef || !state.form.procdefId">
<span v-if="flowProcdef || !modelValue.procdefId">
<el-divider content-position="left">{{ $t('flow.approvalNode') }}</el-divider>
<FlowDesign height="300px" v-if="flowProcdef" :data="flowProcdef.flowDef" disabled center />
<el-result v-if="!state.form.procdefId" icon="error" :title="$t('flow.approvalNodeNotExist')" :sub-title="$t('flow.resourceNotExistFlow')">
<el-result v-if="!modelValue.procdefId" icon="error" :title="$t('flow.approvalNodeNotExist')" :sub-title="$t('flow.resourceNotExistFlow')">
</el-result>
</span>
<template #footer>
<div>
<el-button @click="cancel()">{{ $t('common.cancel') }}</el-button>
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk" :disabled="!state.form.procdefId">{{ $t('common.confirm') }}</el-button>
</div>
<el-button @click="cancel()">{{ $t('common.cancel') }}</el-button>
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk" :disabled="!modelValue.procdefId">{{ $t('common.confirm') }}</el-button>
</template>
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { toRefs, reactive, defineAsyncComponent, shallowReactive, useTemplateRef } from 'vue';
import { toRefs, reactive, defineAsyncComponent, shallowReactive, useTemplateRef, watch, onMounted } from 'vue';
import { procdefApi, procinstApi } from './api';
import { ElMessage } from 'element-plus';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
@@ -68,6 +66,17 @@ const props = defineProps({
const visible = defineModel<boolean>('visible', { default: false });
const modelValue = defineModel('modelValue', {
default: () => ({
bizType: FlowBizType.DbSqlExec.value,
procdefId: 0,
status: null,
remark: '',
bizKey: '',
bizForm: {},
}),
});
//定义事件
const emit = defineEmits(['cancel', 'val-change']);
@@ -85,34 +94,42 @@ const rules = {
remark: [Rules.requiredInput('common.remark')],
};
const defaultForm = {
bizType: FlowBizType.DbSqlExec.value,
procdefId: -1,
status: null,
remark: '',
bizForm: {},
};
const state = reactive({
tasks: [] as any,
form: { ...defaultForm },
flowProcdef: null as any,
sortable: '' as any,
});
const { form, flowProcdef } = toRefs(state);
const { flowProcdef } = toRefs(state);
const { isFetching: saveBtnLoading, execute: procinstStart } = procinstApi.start.useApi(form);
const { isFetching: saveBtnLoading, execute: procinstStart } = procinstApi.start.useApi(modelValue);
watch(
() => modelValue.value.procdefId,
async () => {
if (!modelValue.value.procdefId || state.flowProcdef) {
return;
}
state.flowProcdef = await procdefApi.detail.request({ id: modelValue.value.procdefId });
}
);
const changeResourceCode = async (resourceType: any, code: string) => {
state.flowProcdef = await procdefApi.getByResource.request({ resourceType, resourceCode: code });
if (!state.flowProcdef) {
state.form.procdefId = 0;
modelValue.value.procdefId = 0;
} else {
state.form.procdefId = state.flowProcdef.id;
modelValue.value.procdefId = state.flowProcdef.id;
}
};
const changeBizType = () => {
//重置流程定义ID
modelValue.value.procdefId = 0;
state.flowProcdef = null;
modelValue.value.bizForm = {};
};
const btnOk = async () => {
try {
await formRef.value.validate();
@@ -124,7 +141,7 @@ const btnOk = async () => {
await procinstStart();
ElMessage.success(t('flow.procinstStartSuccess'));
emit('val-change', state.form);
emit('val-change', modelValue.value);
//重置表单域
cancel();
};
@@ -136,7 +153,9 @@ const cancel = () => {
formRef.value.resetFields();
bizFormRef.value.resetBizForm();
state.form = { ...defaultForm };
setTimeout(() => {
modelValue.value = {} as any;
}, 500);
};
</script>
<style lang="scss"></style>

View File

@@ -7,6 +7,7 @@
size="50%"
body-class="!p-2"
header-class="!mb-2"
:destroy-on-close="true"
:close-on-click-modal="!props.instTaskId"
>
<template #header>
@@ -54,7 +55,7 @@
<el-form-item prop="status" :label="$t('flow.approveResult')" required>
<el-select v-model="form.status">
<el-option :label="$t(ProcinstTaskStatus.Pass.label)" :value="ProcinstTaskStatus.Pass.value"> </el-option>
<!-- <el-option :label="ProcinstTaskStatus.Back.label" :value="ProcinstTaskStatus.Back.value"> </el-option> -->
<el-option :label="$t(ProcinstTaskStatus.Back.label)" :value="ProcinstTaskStatus.Back.value"> </el-option>
<el-option :label="$t(ProcinstTaskStatus.Reject.label)" :value="ProcinstTaskStatus.Reject.value"> </el-option>
</el-select>
</el-form-item>
@@ -133,6 +134,9 @@ const { procinst, flowDef, form, saveBtnLoading } = toRefs(state);
watch(
() => props.procinstId,
async (newValue: any) => {
state.form.status = ProcinstTaskStatus.Pass.value;
state.form.remark = '';
if (!newValue) {
state.procinst = {};
state.flowDef = null;
@@ -155,7 +159,7 @@ watch(
{} as Record<string, typeof res>
);
const nodeKey2Tasks = state.procinst.procinstTasks.reduce(
const nodeKey2Tasks = state.procinst.procinstTasks?.reduce(
(acc: { [x: string]: any[] }, item: { nodeKey: any }) => {
const key = item.nodeKey;
if (!acc[key]) {
@@ -172,7 +176,7 @@ watch(
if (nodeKey2Ops[key]) {
// 将操作记录挂载到 node 下,例如命名为 historyList
node.extra.opLog = nodeKey2Ops[key][0];
node.extra.tasks = nodeKey2Tasks[key];
node.extra.tasks = nodeKey2Tasks?.[key];
}
});

View File

@@ -15,8 +15,18 @@
<template #action="{ data }">
<el-button link @click="showProcinst(data)" type="primary">{{ $t('common.detail') }}</el-button>
<el-button
v-if="data.status == ProcinstStatus.Back.value && data.creator == useUserInfo().userInfo.username"
link
@click="startProcInst(data)"
type="primary"
>{{ $t('common.edit') }}
</el-button>
<el-popconfirm
v-if="data.status == ProcinstStatus.Active.value || data.status == ProcinstStatus.Suspended.value"
v-if="
data.status == ProcinstStatus.Active.value || data.status == ProcinstStatus.Suspended.value || data.status == ProcinstStatus.Back.value
"
:title="$t('flow.cancelProcessConfirm')"
width="160"
@confirm="procinstCancel(data)"
@@ -37,7 +47,7 @@
@cancel="procinstDetail.procinstId = 0"
/>
<ProcInstEdit v-model:visible="procinstEdit.visible" :title="procinstEdit.title" @val-change="search" />
<ProcinstEdit v-model="procinstEdit.procinst" v-model:visible="procinstEdit.visible" :title="procinstEdit.title" @val-change="search" />
</div>
</template>
@@ -50,9 +60,10 @@ import { SearchItem } from '@/components/pagetable/SearchForm';
import ProcinstDetail from './ProcinstDetail.vue';
import { FlowBizType, ProcinstBizStatus, ProcinstStatus } from './enums';
import { formatTime } from '@/common/utils/format';
import ProcInstEdit from './ProcInstEdit.vue';
import { useI18nDetailTitle, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
import { useI18n } from 'vue-i18n';
import ProcinstEdit from '@/views/flow/ProcinstEdit.vue';
import { useUserInfo } from '@/store/userInfo';
const { t } = useI18n();
@@ -106,6 +117,7 @@ const state = reactive({
procinstEdit: {
title: '',
visible: false,
procinst: {},
},
});
@@ -127,8 +139,19 @@ const showProcinst = (data: any) => {
state.procinstDetail.visible = true;
};
const startProcInst = () => {
const startProcInst = (procinst: any = null) => {
state.procinstEdit.title = t('flow.startProcess');
if (procinst) {
const data = { ...procinst };
data.bizForm = JSON.parse(procinst.bizForm || {});
state.procinstEdit.procinst = data;
} else {
state.procinstEdit.procinst = {
bizType: FlowBizType.DbSqlExec.value,
bizForm: {},
};
}
state.procinstEdit.visible = true;
};

View File

@@ -1,6 +1,6 @@
<template>
<div :style="{ height: props.height }" class="flex flex-col" v-loading="saveing">
<div class="h-[100vh]" ref="flowContainerRef"></div>
<div class="h-screen" ref="flowContainerRef"></div>
</div>
<PropSettingDrawer

View File

@@ -0,0 +1,81 @@
<template>
<el-tabs v-model="activeTabName">
<el-tab-pane :name="approvalRecordTabName" v-if="activeTabName == approvalRecordTabName" :label="$t('flow.approvalRecord')">
<el-table :data="props.node?.properties?.tasks" stripe width="100%">
<el-table-column :label="$t('common.createTime')" min-width="135">
<template #default="scope">
{{ formatDate(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column :label="$t('common.time')" min-width="135">
<template #default="scope">
{{ formatDate(scope.row.endTime) }}
</template>
</el-table-column>
<el-table-column :label="$t('flow.approver')" min-width="100">
<template #default="scope">
{{ scope.row.handler || '' }}
</template>
</el-table-column>
<el-table-column :label="$t('flow.approveResult')" width="80">
<template #default="scope">
<EnumTag :enums="ProcinstTaskStatus" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column :label="$t('flow.approvalRemark')" min-width="150">
<template #default="scope">
{{ scope.row.remark }}
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane :label="$t('common.basic')" :name="basicTabName">
<el-form-item prop="auditRule" :label="$t('flow.aiAuditRule')">
<el-input v-model="form.auditRule" type="textarea" :rows="10" :placeholder="$t('flow.aiAuditRuleTip')" clearable />
</el-form-item>
</el-tab-pane>
</el-tabs>
</template>
<script lang="ts" setup>
import { notEmpty } from '@/common/assert';
import { formatDate } from '@/common/utils/format';
import EnumTag from '@/components/enumtag/EnumTag.vue';
import { useI18nPleaseInput } from '@/hooks/useI18n';
import { ProcinstTaskStatus } from '@/views/flow/enums';
import { computed } from 'vue';
const props = defineProps({
// 节点信息
node: {
type: Object,
default: false,
},
});
const basicTabName = 'basic';
const approvalRecordTabName = 'approvalRecord';
const activeTabName = computed(() => {
console.log(props.node);
// 如果存在审批记录 tasks 且长度大于0则激活审批记录 tab
if (props.node?.properties?.opLog) {
return approvalRecordTabName;
}
return basicTabName;
});
const form: any = defineModel<any>('modelValue', { required: true });
const confirm = () => {
notEmpty(form.value.auditRule, useI18nPleaseInput('flow.aiAuditRule'));
};
defineExpose({
confirm,
});
</script>

View File

@@ -0,0 +1,103 @@
import { RectNode, RectNodeModel, h } from '@logicflow/core';
import PropSetting from './PropSetting.vue';
import { NodeTypeEnum } from '../enums';
import { HisProcinstOpState, ProcinstTaskStatus } from '@/views/flow/enums';
class AiTaskNodeModel extends RectNodeModel {
initNodeData(data: any) {
super.initNodeData(data);
this.width = 100;
this.height = 60;
this.radius = 5;
}
getNodeStyle() {
const style = super.getNodeStyle();
const properties = this.properties;
const opLog: any = properties.opLog;
if (!opLog) {
return style;
}
if (opLog.state == HisProcinstOpState.Completed.value && opLog.extra) {
if (opLog.extra.approvalResult == ProcinstTaskStatus.Pass.value) {
style.stroke = 'green';
} else if (opLog.extra.approvalResult == ProcinstTaskStatus.Back.value) {
style.stroke = '#e6a23c';
} else {
style.stroke = 'red';
}
} else if (opLog.state == HisProcinstOpState.Failed.value) {
style.stroke = 'red';
} else {
style.stroke = 'rgb(100, 100, 255)'; // AI模型节点使用不同的颜色
}
return style;
}
}
class AiTaskNodeView extends RectNode {
// 获取标签形状的方法,用于在节点中添加一个自定义的 SVG 元素
getShape() {
// 获取XxxNodeModel中定义的形状属性
const { model } = this.props;
console.log(model.properties);
const { x, y, width, height, radius } = model;
// 获取XxxNodeModel中定义的样式属性
const style = model.getNodeStyle();
return h('g', {}, [
h('rect', {
...style,
x: x - width / 2,
y: y - height / 2,
width,
height,
rx: radius,
ry: radius,
}),
h(
'svg',
{
x: x - width / 2 + 5,
y: y - height / 2 + 5,
width: 20,
height: 20,
viewBox: '0 0 1024 1024',
},
[
h('path', {
d: 'M517.818182 23.272727a488.727273 488.727273 0 1 0 488.727273 488.727273 488.727273 488.727273 0 0 0-488.727273-488.727273z m0 930.909091a442.181818 442.181818 0 1 1 442.181818-442.181818 442.181818 442.181818 0 0 1-442.181818 442.181818z',
}),
h('path', {
d: 'M490.356364 346.298182l-40.029091-18.618182-162.909091 349.090909 42.123636 19.781818 47.941818-102.865454h162.909091v-25.6l48.174546 126.836363 43.52-16.523636-128-337.454545z m-91.229091 200.610909l73.774545-158.254546 60.043637 158.254546zM704 337.454545h46.545455v349.09091h-46.545455z',
}),
]
),
]);
}
}
const nodeType = NodeTypeEnum.AiTask;
const nodeTypeExtra = nodeType.extra;
export default {
order: nodeTypeExtra.order,
type: nodeType.value,
// 注册配置信息
registerConf: {
type: nodeType.value,
model: AiTaskNodeModel,
view: AiTaskNodeView,
},
dndPanelConf: {
type: nodeType.value,
text: nodeTypeExtra.text,
label: nodeType.label,
icon: 'data:image/svg+xml;charset=utf-8;base64,PHN2ZyB0PSIxNzY0NDkwMzI5ODU0IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjEzMTMxIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik01MTcuODE4MTgyIDIzLjI3MjcyN2E0ODguNzI3MjczIDQ4OC43MjcyNzMgMCAxIDAgNDg4LjcyNzI3MyA0ODguNzI3MjczIDQ4OC43MjcyNzMgNDg4LjcyNzI3MyAwIDAgMC00ODguNzI3MjczLTQ4OC43MjcyNzN6IG0wIDkzMC45MDkwOTFhNDQyLjE4MTgxOCA0NDIuMTgxODE4IDAgMSAxIDQ0Mi4xODE4MTgtNDQyLjE4MTgxOCA0NDIuMTgxODE4IDQ0Mi4xODE4MTggMCAwIDEtNDQyLjE4MTgxOCA0NDIuMTgxODE4eiIgcC1pZD0iMTMxMzIiPjwvcGF0aD48cGF0aCBkPSJNNDkwLjM1NjM2NCAzNDYuMjk4MTgybC00MC4wMjkwOTEtMTguNjE4MTgyLTE2Mi45MDkwOTEgMzQ5LjA5MDkwOSA0Mi4xMjM2MzYgMTkuNzgxODE4IDQ3Ljk0MTgxOC0xMDIuODY1NDU0aDE2Mi45MDkwOTF2LTI1LjZsNDguMTc0NTQ2IDEyNi44MzYzNjMgNDMuNTItMTYuNTIzNjM2LTEyOC0zMzcuNDU0NTQ1eiBtLTkxLjIyOTA5MSAyMDAuNjEwOTA5bDczLjc3NDU0NS0xNTguMjU0NTQ2IDYwLjA0MzYzNyAxNTguMjU0NTQ2ek03MDQgMzM3LjQ1NDU0NWg0Ni41NDU0NTV2MzQ5LjA5MDkxaC00Ni41NDU0NTV6IiBwLWlkPSIxMzEzMyI+PC9wYXRoPjwvc3ZnPg==',
properties: nodeTypeExtra.defaultProp,
},
propSettingComp: PropSetting,
};

View File

@@ -24,14 +24,20 @@ export const NodeTypeEnum = {
text: i18n.global.t('flow.usertask'),
}),
Serial: EnumValue.of('serial', i18n.global.t('flow.serial')).setExtra({
AiTask: EnumValue.of('aitask', i18n.global.t('flow.aitask')).setExtra({
order: 3,
type: 'aitask',
text: i18n.global.t('flow.aitask'),
}),
Serial: EnumValue.of('serial', i18n.global.t('flow.serial')).setExtra({
order: 4,
text: i18n.global.t('flow.serial'),
defaultProp: { condition: `{{ procinstTaskStatus == 1 }}` },
defaultProp: { condition: `{{ procinstTaskStatus == 1.0 }}` },
}),
Parallel: EnumValue.of('parallel', i18n.global.t('flow.parallel')).setExtra({
order: 4,
order: 5,
text: i18n.global.t('flow.parallel'),
defaultProp: {},
}),

View File

@@ -2,6 +2,12 @@
<el-tabs v-model="activeTabName">
<el-tab-pane :name="approvalRecordTabName" v-if="activeTabName == approvalRecordTabName" :label="$t('flow.approvalRecord')">
<el-table :data="props.node?.properties?.tasks" stripe width="100%">
<el-table-column :label="$t('common.createTime')" min-width="135">
<template #default="scope">
{{ formatDate(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column :label="$t('common.time')" min-width="135">
<template #default="scope">
{{ formatDate(scope.row.endTime) }}

View File

@@ -23,6 +23,8 @@ class UserTaskModel extends RectNodeModel {
if (opLog.state == HisProcinstOpState.Completed.value && opLog.extra) {
if (opLog.extra.approvalResult == ProcinstTaskStatus.Pass.value) {
style.stroke = 'green';
} else if (opLog.extra.approvalResult == ProcinstTaskStatus.Back.value) {
style.stroke = '#e6a23c';
} else {
style.stroke = 'red';
}

View File

@@ -15,6 +15,7 @@ export const ProcinstStatus = {
Active: EnumValue.of(1, 'flow.active').setTagType('primary'),
Completed: EnumValue.of(2, 'flow.completed').setTagType('success'),
Suspended: EnumValue.of(-1, 'flow.suspended').setTagType('warning'),
Back: EnumValue.of(-11, 'flow.back').setTagType('warning'),
Terminated: EnumValue.of(-2, 'flow.terminated').setTagType('danger'),
Cancelled: EnumValue.of(-3, 'flow.cancelled').setTagType('warning'),
};

View File

@@ -5,13 +5,15 @@
:placeholder="$t('flow.selectDbPlaceholder')"
v-model:db-id="bizForm.dbId"
v-model:db-name="bizForm.dbName"
v-model:db-type="dbType"
v-model:inst-name="bizForm.instName"
v-model:db-type="bizForm.dbType"
v-model:tag-path="bizForm.tagPath"
@select-db="changeResourceCode"
/>
</el-form-item>
<el-form-item prop="sql" label="SQL" required>
<div class="!w-full">
<div class="w-full!">
<monaco-editor height="300px" language="sql" v-model="bizForm.sql" />
</div>
</el-form-item>
@@ -19,7 +21,7 @@
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { onMounted, ref, watch } from 'vue';
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { registerDbCompletionItemProvider } from '@/views/ops/db/db';
@@ -38,17 +40,24 @@ const formRef: any = ref(null);
const bizForm = defineModel<any>('bizForm', {
default: {
dbId: 0,
instName: '',
dbName: '',
dbType: '',
tagPath: '',
sql: '',
},
});
const dbType = ref('');
onMounted(() => {
if (bizForm.value.dbId) {
registerDbCompletionItemProvider(bizForm.value.dbId, bizForm.value.dbName, [bizForm.value.dbName], bizForm.value.dbType);
}
});
watch(
() => bizForm.value.dbId,
() => {
registerDbCompletionItemProvider(bizForm.value.dbId, bizForm.value.dbName, [bizForm.value.dbName], dbType.value);
registerDbCompletionItemProvider(bizForm.value.dbId, bizForm.value.dbName, [bizForm.value.dbName], bizForm.value.dbType);
}
);

View File

@@ -104,15 +104,14 @@ const bizForm = defineModel<any>('bizForm', {
id: 0,
db: 0,
cmd: '',
tagPath: '',
redisName: '',
},
});
const redisName = ref('');
const tagPath = ref('');
const selectRedis = computed({
get: () => {
return redisName.value ? `${tagPath.value} > ${redisName.value} > db${bizForm.value.db}` : '';
return bizForm.value.redisName ? `${bizForm.value.tagPath} > ${bizForm.value.redisName} > db${bizForm.value.db}` : '';
},
set: () => {
//
@@ -121,8 +120,8 @@ const selectRedis = computed({
const changeRedis = (nodeData: TagTreeNode) => {
const params = nodeData.params;
tagPath.value = params.tagPath;
redisName.value = params.redisName;
bizForm.value.tagPath = params.tagPath;
bizForm.value.redisName = params.redisName;
bizForm.value.id = params.id;
bizForm.value.db = parseInt(params.db);

View File

@@ -7,13 +7,13 @@
</template>
<template #default="scope">
<el-button v-auth="'authcert:save'" @click="edit(scope.row, scope.$index)" type="primary" icon="edit" link></el-button>
<el-button class="!ml-0.5" v-auth="'authcert:del'" type="danger" @click="deleteRow(scope.$index)" icon="delete" link></el-button>
<el-button class="ml-0.5!" v-auth="'authcert:del'" type="danger" @click="deleteRow(scope.$index)" icon="delete" link></el-button>
<el-button
:title="$t('ac.testConn')"
:loading="props.testConnBtnLoading && scope.$index == state.idx"
:disabled="props.testConnBtnLoading"
class="!ml-0.5"
class="ml-0.5!"
type="success"
@click="testConn(scope.row, scope.$index)"
icon="Link"

View File

@@ -1,246 +0,0 @@
<template>
<div>
<el-dialog :title="title" :model-value="visible" :before-close="cancel" :close-on-click-modal="false" :destroy-on-close="true" width="38%">
<el-form :model="state.form" ref="backupForm" label-width="auto" :rules="rules">
<el-form-item prop="dbNames" label="数据库名称">
<el-select
v-model="state.dbNamesSelected"
multiple
clearable
collapse-tags
collapse-tags-tooltip
filterable
:disabled="state.editOrCreate"
:filter-method="filterDbNames"
placeholder="数据库名称"
style="width: 100%"
>
<template #header>
<el-checkbox v-model="checkAllDbNames" :indeterminate="indeterminateDbNames" @change="handleCheckAll"> 全选 </el-checkbox>
</template>
<el-option v-for="db in state.dbNamesFiltered" :key="db" :label="db" :value="db" />
</el-select>
</el-form-item>
<el-form-item prop="name" label="任务名称">
<el-input v-model="state.form.name" type="text" placeholder="任务名称"></el-input>
</el-form-item>
<el-form-item prop="startTime" label="开始时间">
<el-date-picker v-model="state.form.startTime" type="datetime" placeholder="开始时间" />
</el-form-item>
<el-form-item prop="intervalDay" label="备份周期(天)">
<el-input v-model.number="state.form.intervalDay" type="number" placeholder="单位:天"></el-input>
</el-form-item>
<el-form-item prop="maxSaveDays" label="备份历史保留天数">
<el-input v-model.number="state.form.maxSaveDays" type="number" placeholder="0: 永久保留"></el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel()"> </el-button>
<el-button type="primary" :loading="state.btnLoading" @click="btnOk"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, toRefs, watch } from 'vue';
import { dbApi } from './api';
import { ElMessage } from 'element-plus';
import type { CheckboxValueType } from 'element-plus';
const props = defineProps({
data: {
type: [Boolean, Object],
},
title: {
type: String,
},
dbId: {
type: [Number],
required: true,
},
});
const visible = defineModel<boolean>('visible', {
default: false,
});
//定义事件
const emit = defineEmits(['cancel', 'val-change']);
const rules = {
dbNames: [
{
required: true,
message: '请选择需要备份的数据库',
trigger: ['change', 'blur'],
},
],
intervalDay: [
{
required: true,
pattern: /^[1-9]\d*$/,
message: '请输入正整数',
trigger: ['change', 'blur'],
},
],
startTime: [
{
required: true,
message: '请选择开始时间',
trigger: ['change', 'blur'],
},
],
maxSaveDays: [
{
required: true,
pattern: /^[0-9]\d*$/,
message: '请输入非负整数',
trigger: ['change', 'blur'],
},
],
};
const backupForm: any = ref(null);
const state = reactive({
form: {
id: 0,
dbId: 0,
dbNames: '',
name: '',
intervalDay: 1,
startTime: null as any,
repeated: true,
maxSaveDays: 0,
},
btnLoading: false,
dbNamesSelected: [] as any,
dbNamesWithoutBackup: [] as any,
dbNamesFiltered: [] as any,
filterString: '',
editOrCreate: false,
});
const { dbNamesSelected, dbNamesWithoutBackup } = toRefs(state);
const checkAllDbNames = ref(false);
const indeterminateDbNames = ref(false);
watch(visible, (newValue: any) => {
if (newValue) {
init(props.data);
}
});
const init = (data: any) => {
state.dbNamesSelected = [];
state.form.dbId = props.dbId;
if (data) {
state.editOrCreate = true;
state.dbNamesWithoutBackup = [data.dbName];
state.dbNamesSelected = [data.dbName];
state.form.id = data.id;
state.form.dbNames = data.dbName;
state.form.name = data.name;
state.form.intervalDay = data.intervalDay;
state.form.startTime = data.startTime;
state.form.maxSaveDays = data.maxSaveDays;
} else {
state.editOrCreate = false;
state.form.name = '';
state.form.intervalDay = 1;
const now = new Date();
state.form.startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
state.form.maxSaveDays = 0;
getDbNamesWithoutBackup();
}
};
const getDbNamesWithoutBackup = async () => {
if (props.dbId > 0) {
state.dbNamesWithoutBackup = await dbApi.getDbNamesWithoutBackup.request({ dbId: props.dbId });
}
};
const btnOk = async () => {
backupForm.value.validate(async (valid: boolean) => {
if (!valid) {
ElMessage.error('请正确填写信息');
return false;
}
state.form.repeated = true;
const reqForm = { ...state.form };
let api = dbApi.createDbBackup;
if (props.data) {
api = dbApi.saveDbBackup;
}
try {
state.btnLoading = true;
await api.request(reqForm);
ElMessage.success('保存成功');
emit('val-change', state.form);
cancel();
} finally {
state.btnLoading = false;
}
});
};
const cancel = () => {
visible.value = false;
emit('cancel');
};
const checkDbSelect = (val: string[]) => {
const selected = val.filter((dbName: string) => {
return dbName.includes(state.filterString);
});
if (selected.length === 0) {
checkAllDbNames.value = false;
indeterminateDbNames.value = false;
return;
}
if (selected.length === state.dbNamesFiltered.length) {
checkAllDbNames.value = true;
indeterminateDbNames.value = false;
return;
}
indeterminateDbNames.value = true;
};
watch(dbNamesSelected, (val: string[]) => {
checkDbSelect(val);
state.form.dbNames = val.join(' ');
});
watch(dbNamesWithoutBackup, (val: string[]) => {
state.dbNamesFiltered = val.map((dbName: string) => dbName);
});
const handleCheckAll = (val: CheckboxValueType) => {
const selected = state.dbNamesSelected.filter((dbName: string) => {
return !state.dbNamesFiltered.includes(dbName);
});
if (val) {
state.dbNamesSelected = selected.concat(state.dbNamesFiltered);
} else {
state.dbNamesSelected = selected;
}
};
const filterDbNames = (filterString: string) => {
state.dbNamesFiltered = state.dbNamesWithoutBackup.filter((dbName: string) => {
return dbName.includes(filterString);
});
state.filterString = filterString;
checkDbSelect(state.dbNamesSelected);
};
</script>
<style lang="scss"></style>

View File

@@ -1,155 +0,0 @@
<template>
<div class="db-backup-history">
<page-table
height="100%"
ref="pageTableRef"
:page-api="dbApi.getDbBackupHistories"
:show-selection="true"
v-model:selection-data="state.selectedData"
:searchItems="searchItems"
:before-query-fn="beforeQueryFn"
v-model:query-form="query"
:columns="columns"
>
<template #dbSelect>
<el-select v-model="query.dbName" placeholder="请选择数据库" style="width: 200px" filterable clearable>
<el-option v-for="item in props.dbNames" :key="item" :label="`${item}`" :value="item"> </el-option>
</el-select>
</template>
<template #tableHeader>
<el-button type="primary" icon="back" @click="restoreDbBackupHistory(null)">立即恢复</el-button>
<el-button type="danger" icon="delete" @click="deleteDbBackupHistory(null)">删除</el-button>
</template>
<template #action="{ data }">
<div>
<el-button @click="restoreDbBackupHistory(data)" type="primary" link>立即恢复</el-button>
<el-button @click="deleteDbBackupHistory(data)" type="danger" link>删除</el-button>
</div>
</template>
</page-table>
</div>
</template>
<script lang="ts" setup>
import { toRefs, reactive, Ref, ref } from 'vue';
import { dbApi } from './api';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { SearchItem } from '@/components/pagetable/SearchForm';
import { ElMessage, ElMessageBox } from 'element-plus';
const pageTableRef: Ref<any> = ref(null);
const props = defineProps({
dbId: {
type: [Number],
required: true,
},
dbNames: {
type: [Array<String>],
required: true,
},
});
const searchItems = [SearchItem.slot('dbName', '数据库名称', 'dbSelect')];
const columns = [
TableColumn.new('dbName', '数据库名称'),
TableColumn.new('name', '备份名称'),
TableColumn.new('createTime', '创建时间').isTime(),
TableColumn.new('lastResult', '恢复结果'),
TableColumn.new('lastTime', '恢复时间').isTime(),
TableColumn.new('action', '操作').isSlot().setMinWidth(160).fixedRight(),
];
const emptyQuery = {
dbId: 0,
dbName: '',
pageNum: 1,
pageSize: 10,
};
const state = reactive({
data: [],
total: 0,
query: emptyQuery,
/**
* 选中的数据
*/
selectedData: [],
});
const { query } = toRefs(state);
const beforeQueryFn = (query: any) => {
query.dbId = props.dbId;
return query;
};
const search = async () => {
await pageTableRef.value.search();
};
const deleteDbBackupHistory = async (data: any) => {
let backupHistoryId: string;
if (data) {
backupHistoryId = data.id;
} else if (state.selectedData.length > 0) {
backupHistoryId = state.selectedData.map((x: any) => x.id).join(' ');
} else {
ElMessage.error('请选择需要删除的数据库备份历史');
return;
}
await ElMessageBox.confirm(`确定删除 “数据库备份历史” 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.deleteDbBackupHistory.request({ dbId: props.dbId, backupHistoryId: backupHistoryId });
await search();
ElMessage.success('删除成功');
};
const restoreDbBackupHistory = async (data: any) => {
let backupHistoryId: string;
if (data) {
backupHistoryId = data.id;
} else if (state.selectedData.length > 0) {
const pluralDbNames: string[] = [];
const dbNames: Map<string, boolean> = new Map();
state.selectedData.forEach((item: any) => {
if (!dbNames.has(item.dbName)) {
dbNames.set(item.dbName, false);
return;
}
if (!dbNames.get(item.dbName)) {
dbNames.set(item.dbName, true);
pluralDbNames.push(item.dbName);
}
});
if (pluralDbNames.length > 0) {
ElMessage.error('多次选择相同数据库:' + pluralDbNames.join(', '));
return;
}
backupHistoryId = state.selectedData.map((x: any) => x.id).join(' ');
} else {
ElMessage.error('请选择需要恢复的数据库备份历史');
return;
}
await ElMessageBox.confirm(`确定从 “数据库备份历史” 中恢复数据库吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.restoreDbBackupHistory.request({
dbId: props.dbId,
backupHistoryId: backupHistoryId,
});
await search();
ElMessage.success('成功创建数据库恢复任务');
};
</script>
<style lang="scss"></style>

View File

@@ -1,194 +0,0 @@
<template>
<div class="db-backup">
<page-table
height="100%"
ref="pageTableRef"
:page-api="dbApi.getDbBackups"
:show-selection="true"
v-model:selection-data="state.selectedData"
:searchItems="searchItems"
:before-query-fn="beforeQueryFn"
v-model:query-form="query"
:columns="columns"
>
<template #dbSelect>
<el-select v-model="query.dbName" placeholder="请选择数据库" style="width: 200px" filterable clearable>
<el-option v-for="item in props.dbNames" :key="item" :label="`${item}`" :value="item"> </el-option>
</el-select>
</template>
<template #tableHeader>
<el-button type="primary" icon="plus" @click="createDbBackup()">添加</el-button>
<el-button type="primary" icon="video-play" @click="enableDbBackup(null)">启用</el-button>
<el-button type="primary" icon="video-pause" @click="disableDbBackup(null)">禁用</el-button>
<el-button type="danger" icon="delete" @click="deleteDbBackup(null)">删除</el-button>
</template>
<template #action="{ data }">
<div>
<el-button @click="editDbBackup(data)" type="primary" link>编辑</el-button>
<el-button v-if="!data.enabled" @click="enableDbBackup(data)" type="primary" link>启用</el-button>
<el-button v-if="data.enabled" @click="disableDbBackup(data)" type="primary" link>禁用</el-button>
<el-button v-if="data.enabled" @click="startDbBackup(data)" type="primary" link>立即备份</el-button>
<el-button @click="deleteDbBackup(data)" type="danger" link>删除</el-button>
</div>
</template>
</page-table>
<db-backup-edit
@val-change="search"
:title="dbBackupEditDialog.title"
:dbId="dbId"
:data="dbBackupEditDialog.data"
v-model:visible="dbBackupEditDialog.visible"
></db-backup-edit>
</div>
</template>
<script lang="ts" setup>
import { toRefs, reactive, defineAsyncComponent, Ref, ref } from 'vue';
import { dbApi } from './api';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { SearchItem } from '@/components/pagetable/SearchForm';
import { ElMessage, ElMessageBox } from 'element-plus';
const DbBackupEdit = defineAsyncComponent(() => import('./DbBackupEdit.vue'));
const pageTableRef: Ref<any> = ref(null);
const props = defineProps({
dbId: {
type: [Number],
required: true,
},
dbNames: {
type: [Array<String>],
required: true,
},
});
const searchItems = [SearchItem.slot('dbName', '数据库名称', 'dbSelect')];
const columns = [
TableColumn.new('dbName', '数据库名称'),
TableColumn.new('name', '任务名称'),
TableColumn.new('startTime', '启动时间').isTime(),
TableColumn.new('intervalDay', '备份周期'),
TableColumn.new('enabledDesc', '是否启用'),
TableColumn.new('lastResult', '执行结果'),
TableColumn.new('lastTime', '执行时间').isTime(),
TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight(),
];
const emptyQuery = {
dbId: 0,
dbName: '',
pageNum: 1,
pageSize: 10,
repeated: true,
};
const state = reactive({
data: [],
total: 0,
query: emptyQuery,
dbBackupEditDialog: {
visible: false,
data: null as any,
title: '创建数据库备份任务',
},
/**
* 选中的数据
*/
selectedData: [],
});
const { query, dbBackupEditDialog } = toRefs(state);
const beforeQueryFn = (query: any) => {
query.dbId = props.dbId;
return query;
};
const search = async () => {
await pageTableRef.value.search();
};
const createDbBackup = async () => {
state.dbBackupEditDialog.data = null;
state.dbBackupEditDialog.title = '创建数据库备份任务';
state.dbBackupEditDialog.visible = true;
};
const editDbBackup = async (data: any) => {
state.dbBackupEditDialog.data = data;
state.dbBackupEditDialog.title = '修改数据库备份任务';
state.dbBackupEditDialog.visible = true;
};
const enableDbBackup = async (data: any) => {
let backupId: String;
if (data) {
backupId = data.id;
} else if (state.selectedData.length > 0) {
backupId = state.selectedData.map((x: any) => x.id).join(' ');
} else {
ElMessage.error('请选择需要启用的备份任务');
return;
}
await dbApi.enableDbBackup.request({ dbId: props.dbId, backupId: backupId });
await search();
ElMessage.success('启用成功');
};
const disableDbBackup = async (data: any) => {
let backupId: String;
if (data) {
backupId = data.id;
} else if (state.selectedData.length > 0) {
backupId = state.selectedData.map((x: any) => x.id).join(' ');
} else {
ElMessage.error('请选择需要禁用的备份任务');
return;
}
await dbApi.disableDbBackup.request({ dbId: props.dbId, backupId: backupId });
await search();
ElMessage.success('禁用成功');
};
const startDbBackup = async (data: any) => {
let backupId: String;
if (data) {
backupId = data.id;
} else if (state.selectedData.length > 0) {
backupId = state.selectedData.map((x: any) => x.id).join(' ');
} else {
ElMessage.error('请选择需要启用的备份任务');
return;
}
await dbApi.startDbBackup.request({ dbId: props.dbId, backupId: backupId });
await search();
ElMessage.success('备份任务启动成功');
};
const deleteDbBackup = async (data: any) => {
let backupId: string;
if (data) {
backupId = data.id;
} else if (state.selectedData.length > 0) {
backupId = state.selectedData.map((x: any) => x.id).join(' ');
} else {
ElMessage.error('请选择需要删除的数据库备份任务');
return;
}
await ElMessageBox.confirm(`确定删除 “数据库备份任务” 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.deleteDbBackup.request({ dbId: props.dbId, backupId: backupId });
await search();
ElMessage.success('删除成功');
};
</script>
<style lang="scss"></style>

View File

@@ -1,311 +0,0 @@
<template>
<div>
<el-dialog :title="title" :model-value="visible" :before-close="cancel" :close-on-click-modal="false" width="38%">
<el-form :model="state.form" ref="restoreForm" label-width="auto" :rules="rules">
<el-form-item label="恢复方式">
<el-radio-group :disabled="state.editOrCreate" v-model="state.restoreMode">
<el-radio label="point-in-time">指定时间点</el-radio>
<el-radio label="backup-history">指定备份</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item prop="dbName" label="数据库名称">
<el-select
:disabled="state.editOrCreate"
@change="changeDatabase"
v-model="state.form.dbName"
placeholder="数据库名称"
filterable
clearable
class="!w-full"
>
<el-option v-for="item in props.dbNames" :key="item" :label="`${item}`" :value="item"> </el-option>
</el-select>
</el-form-item>
<el-form-item v-if="state.restoreMode == 'point-in-time'" prop="pointInTime" label="恢复时间点">
<el-date-picker :disabled="state.editOrCreate" v-model="state.form.pointInTime" type="datetime" placeholder="恢复时间点" />
</el-form-item>
<el-form-item v-if="state.restoreMode == 'backup-history'" prop="dbBackupHistoryId" label="数据库备份">
<el-select
:disabled="state.editOrCreate"
@change="changeHistory"
v-model="state.history"
value-key="id"
placeholder="数据库备份"
filterable
clearable
class="!w-full"
>
<el-option
v-for="item in state.histories"
:key="item.id"
:label="item.name + (item.binlogFileName ? ' ' : ' 不') + '支持指定时间点恢复'"
:value="item"
>
</el-option>
</el-select>
</el-form-item>
<el-form-item prop="startTime" label="开始时间">
<el-date-picker :disabled="state.editOrCreate" v-model="state.form.startTime" type="datetime" placeholder="开始时间" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel()"> </el-button>
<el-button type="primary" :loading="state.btnLoading" @click="btnOk"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { onMounted, reactive, ref, watch } from 'vue';
import { dbApi } from './api';
import { ElMessage, ElMessageBox } from 'element-plus';
const props = defineProps({
data: {
type: [Boolean, Object],
},
title: {
type: String,
},
dbId: {
type: [Number],
required: true,
},
dbNames: {
type: Array,
required: true,
},
});
//定义事件
const emit = defineEmits(['cancel', 'val-change']);
const visible = defineModel<boolean>('visible', {
default: false,
});
const validatePointInTime = (rule: any, value: any, callback: any) => {
if (value > new Date()) {
callback(new Error('恢复时间点晚于当前时间'));
return;
}
if (!state.histories || state.histories.length == 0) {
callback(new Error('数据库没有备份记录'));
return;
}
let last = null;
for (const history of state.histories) {
if (!history.binlogFileName || history.binlogFileName.length === 0) {
break;
}
if (new Date(history.createTime) < value) {
callback();
return;
}
last = history;
}
if (!last) {
callback(new Error('现有数据库备份不支持指定时间恢复'));
return;
}
callback(last.name + ' 之前的数据库备份不支持指定时间恢复');
};
const rules = {
dbName: [
{
required: true,
message: '请选择需要恢复的数据库',
trigger: ['change', 'blur'],
},
],
pointInTime: [
{
required: true,
validator: validatePointInTime,
trigger: ['change', 'blur'],
},
],
dbBackupHistoryId: [
{
required: true,
message: '请选择数据库备份',
trigger: ['change', 'blur'],
},
],
intervalDay: [
{
required: true,
pattern: /^[1-9]\d*$/,
message: '请输入正整数',
trigger: ['change', 'blur'],
},
],
startTime: [
{
required: true,
message: '请选择开始时间',
trigger: ['change', 'blur'],
},
],
};
const restoreForm: any = ref(null);
const state = reactive({
form: {
id: 0,
dbId: 0,
dbName: null as any,
intervalDay: 0,
startTime: null as any,
repeated: null as any,
dbBackupId: null as any,
dbBackupHistoryId: null as any,
dbBackupHistoryName: null as any,
pointInTime: null as any,
},
btnLoading: false,
dbNamesSelected: [] as any,
dbNamesWithoutRestore: [] as any,
editOrCreate: false,
histories: [] as any,
history: null as any,
restoreMode: null as any,
});
onMounted(async () => {
await init(props.data);
});
watch(visible, (newValue: any) => {
if (newValue) {
init(props.data);
}
});
/**
* 改变表单中的数据库字段,方便表单错误提示。如全部删光,可提示请添加数据库
*/
const changeDatabase = async () => {
await getBackupHistories(props.dbId, state.form.dbName);
};
const changeHistory = async () => {
if (state.history) {
state.form.dbBackupId = state.history.dbBackupId;
state.form.dbBackupHistoryId = state.history.id;
state.form.dbBackupHistoryName = state.history.name;
}
};
const init = async (data: any) => {
state.dbNamesSelected = [];
state.form.dbId = props.dbId;
if (data) {
state.editOrCreate = true;
state.dbNamesWithoutRestore = [data.dbName];
state.dbNamesSelected = [data.dbName];
state.form.id = data.id;
state.form.dbName = data.dbName;
state.form.intervalDay = data.intervalDay;
state.form.pointInTime = data.pointInTime;
state.form.startTime = data.startTime;
state.form.dbBackupId = data.dbBackupId;
state.form.dbBackupHistoryId = data.dbBackupHistoryId;
state.form.dbBackupHistoryName = data.dbBackupHistoryName;
if (data.pointInTime) {
state.restoreMode = 'point-in-time';
} else {
state.restoreMode = 'backup-history';
}
state.history = {
dbBackupId: data.dbBackupId,
id: data.dbBackupHistoryId,
name: data.dbBackupHistoryName,
createTime: data.createTime,
};
await getBackupHistories(props.dbId, data.dbName);
} else {
state.form.dbName = '';
state.editOrCreate = false;
state.form.intervalDay = 0;
state.form.repeated = false;
state.form.pointInTime = new Date();
state.form.startTime = new Date();
state.histories = [];
state.history = null;
state.restoreMode = 'point-in-time';
await getDbNamesWithoutRestore();
}
};
const getDbNamesWithoutRestore = async () => {
if (props.dbId > 0) {
state.dbNamesWithoutRestore = await dbApi.getDbNamesWithoutRestore.request({ dbId: props.dbId });
}
};
const btnOk = async () => {
restoreForm.value.validate(async (valid: any) => {
if (valid) {
await ElMessageBox.confirm(`确定恢复数据库吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
if (state.restoreMode == 'point-in-time') {
state.form.dbBackupId = 0;
state.form.dbBackupHistoryId = 0;
state.form.dbBackupHistoryName = '';
} else {
state.form.pointInTime = null;
}
state.form.repeated = false;
state.form.intervalDay = 0;
const reqForm = { ...state.form };
let api = dbApi.createDbRestore;
if (props.data) {
api = dbApi.saveDbRestore;
}
api.request(reqForm).then(() => {
ElMessage.success('成功创建数据库恢复任务');
emit('val-change', state.form);
state.btnLoading = true;
setTimeout(() => {
state.btnLoading = false;
}, 1000);
cancel();
});
} else {
ElMessage.error('请正确填写信息');
return false;
}
});
};
const cancel = () => {
visible.value = false;
emit('cancel');
};
const getBackupHistories = async (dbId: Number, dbName: String) => {
if (!dbId || !dbName) {
state.histories = [];
return;
}
const data = await dbApi.getDbBackupHistories.request({ dbId, dbName });
if (!data || !data.list) {
ElMessage.error('该数据库没有备份记录,无法创建数据库恢复任务');
state.histories = [];
return;
}
state.histories = data.list;
};
</script>
<style lang="scss"></style>

View File

@@ -1,195 +0,0 @@
<template>
<div class="db-restore">
<page-table
height="100%"
ref="pageTableRef"
:page-api="dbApi.getDbRestores"
:show-selection="true"
v-model:selection-data="state.selectedData"
:searchItems="searchItems"
:before-query-fn="beforeQueryFn"
v-model:query-form="query"
:columns="columns"
>
<template #dbSelect>
<el-select v-model="query.dbName" placeholder="请选择数据库" style="width: 200px" filterable clearable>
<el-option v-for="item in dbNames" :key="item" :label="`${item}`" :value="item"> </el-option>
</el-select>
</template>
<template #tableHeader>
<el-button type="primary" icon="plus" @click="createDbRestore()">添加</el-button>
<el-button type="primary" icon="video-play" @click="enableDbRestore(null)">启用</el-button>
<el-button type="primary" icon="video-pause" @click="disableDbRestore(null)">禁用</el-button>
<el-button type="danger" icon="delete" @click="deleteDbRestore(null)">删除</el-button>
</template>
<template #action="{ data }">
<el-button @click="showDbRestore(data)" type="primary" link>详情</el-button>
<el-button @click="enableDbRestore(data)" v-if="!data.enabled" type="primary" link>启用</el-button>
<el-button @click="disableDbRestore(data)" v-if="data.enabled" type="primary" link>禁用</el-button>
<el-button @click="deleteDbRestore(data)" type="danger" link>删除</el-button>
</template>
</page-table>
<db-restore-edit
@val-change="search"
:title="dbRestoreEditDialog.title"
:dbId="dbId"
:dbNames="dbNames"
:data="dbRestoreEditDialog.data"
v-model:visible="dbRestoreEditDialog.visible"
></db-restore-edit>
<el-dialog v-model="infoDialog.visible" title="数据库恢复">
<el-descriptions :column="1" border>
<el-descriptions-item :span="1" label="数据库名称">{{ infoDialog.data.dbName }}</el-descriptions-item>
<el-descriptions-item v-if="infoDialog.data.pointInTime" :span="1" label="恢复时间点">{{
formatDate(infoDialog.data.pointInTime)
}}</el-descriptions-item>
<el-descriptions-item v-if="!infoDialog.data.pointInTime" :span="1" label="数据库备份">{{
infoDialog.data.dbBackupHistoryName
}}</el-descriptions-item>
<el-descriptions-item :span="1" label="开始时间">{{ formatDate(infoDialog.data.startTime) }}</el-descriptions-item>
<el-descriptions-item :span="1" label="是否启用">{{ infoDialog.data.enabledDesc }}</el-descriptions-item>
<el-descriptions-item :span="1" label="执行时间">{{ formatDate(infoDialog.data.lastTime) }}</el-descriptions-item>
<el-descriptions-item :span="1" label="执行结果">{{ infoDialog.data.lastResult }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { toRefs, reactive, defineAsyncComponent, Ref, ref } from 'vue';
import { dbApi } from './api';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { SearchItem } from '@/components/pagetable/SearchForm';
import { ElMessage, ElMessageBox } from 'element-plus';
import { formatDate } from '@/common/utils/format';
const DbRestoreEdit = defineAsyncComponent(() => import('./DbRestoreEdit.vue'));
const pageTableRef: Ref<any> = ref(null);
const props = defineProps({
dbId: {
type: [Number],
required: true,
},
dbNames: {
type: [Array<String>],
required: true,
},
});
// const queryConfig = [TableQuery.slot('dbName', '数据库名称', 'dbSelect')];
const searchItems = [SearchItem.slot('dbName', '数据库名称', 'dbSelect')];
const columns = [
TableColumn.new('dbName', '数据库名称'),
TableColumn.new('startTime', '启动时间').isTime(),
TableColumn.new('enabledDesc', '是否启用'),
TableColumn.new('lastTime', '执行时间').isTime(),
TableColumn.new('lastResult', '执行结果'),
TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter(),
];
const emptyQuery = {
dbId: props.dbId,
dbName: '',
pageNum: 1,
pageSize: 10,
repeated: false,
};
const state = reactive({
data: [],
total: 0,
query: emptyQuery,
dbRestoreEditDialog: {
visible: false,
data: null as any,
title: '创建数据库恢复任务',
},
infoDialog: {
visible: false,
data: null as any,
},
/**
* 选中的数据
*/
selectedData: [],
});
const { query, dbRestoreEditDialog, infoDialog } = toRefs(state);
const beforeQueryFn = (query: any) => {
query.dbId = props.dbId;
return query;
};
const search = async () => {
await pageTableRef.value.search();
};
const createDbRestore = async () => {
state.dbRestoreEditDialog.data = null;
state.dbRestoreEditDialog.title = '数据库恢复';
state.dbRestoreEditDialog.visible = true;
};
const deleteDbRestore = async (data: any) => {
let restoreId: string;
if (data) {
restoreId = data.id;
} else if (state.selectedData.length > 0) {
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
} else {
ElMessage.error('请选择需要删除的数据库恢复任务');
return;
}
await ElMessageBox.confirm(`确定删除 “数据库恢复任务” 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.deleteDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
await search();
ElMessage.success('删除成功');
};
const showDbRestore = async (data: any) => {
state.infoDialog.data = data;
state.infoDialog.visible = true;
};
const enableDbRestore = async (data: any) => {
let restoreId: string;
if (data) {
restoreId = data.id;
} else if (state.selectedData.length > 0) {
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
} else {
ElMessage.error('请选择需要启用的数据库恢复任务');
return;
}
await dbApi.enableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
await search();
ElMessage.success('启用成功');
};
const disableDbRestore = async (data: any) => {
let restoreId: string;
if (data) {
restoreId = data.id;
} else if (state.selectedData.length > 0) {
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
} else {
ElMessage.error('请选择需要禁用的数据库恢复任务');
return;
}
await dbApi.disableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
await search();
ElMessage.success('禁用成功');
};
</script>
<style lang="scss"></style>

View File

@@ -59,36 +59,13 @@ export const dbApi = {
enableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/enable'),
disableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/disable'),
saveDbRestore: Api.newPut('/dbs/{dbId}/restores/{id}'),
// 数据同步相关
datasyncTasks: Api.newGet('/datasync/tasks'),
saveDatasyncTask: Api.newPost('/datasync/tasks/save').withBeforeHandler(async (param: any) => await encryptField(param, 'dataSql')),
getDatasyncTask: Api.newGet('/datasync/tasks/{taskId}'),
deleteDatasyncTask: Api.newDelete('/datasync/tasks/{taskId}/del'),
updateDatasyncTaskStatus: Api.newPost('/datasync/tasks/{taskId}/status'),
runDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/run'),
stopDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/stop'),
datasyncLogs: Api.newGet('/datasync/tasks/{taskId}/logs'),
// 数据库迁移相关
dbTransferTasks: Api.newGet('/dbTransfer'),
saveDbTransferTask: Api.newPost('/dbTransfer/save'),
deleteDbTransferTask: Api.newDelete('/dbTransfer/{taskId}/del'),
updateDbTransferTaskStatus: Api.newPost('/dbTransfer/{taskId}/status'),
runDbTransferTask: Api.newPost('/dbTransfer/{taskId}/run'),
stopDbTransferTask: Api.newPost('/dbTransfer/{taskId}/stop'),
dbTransferTaskLogs: Api.newGet('/dbTransfer/{taskId}/logs'),
dbTransferFileList: Api.newGet('/dbTransfer/files/{taskId}'),
dbTransferFileDel: Api.newPost('/dbTransfer/files/del/{fileId}'),
dbTransferFileRun: Api.newPost('/dbTransfer/files/run'),
dbTransferFileDown: Api.newGet('/dbTransfer/files/down/{fileUuid}'),
};
export const dbSqlExecApi = {
// 根据业务key获取sql执行信息
getSqlExecByBizKey: Api.newGet('/dbs/sql-execs'),
};
const encryptField = async (param: any, field: string) => {
export const encryptField = async (param: any, field: string) => {
// sql编码处理
if (!param['_encrypted'] && param[field]) {
// 判断是开发环境就打印sql

View File

@@ -207,7 +207,7 @@ import { copyToClipboard } from '@/common/utils/string';
import { DbInst, DbThemeConfig } from '@/views/ops/db/db';
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
import SvgIcon from '@/components/svgIcon/index.vue';
import { exportCsv, exportFile } from '@/common/utils/export';
import { exportCsv, exportExcel, exportFile } from '@/common/utils/export';
import { formatDate } from '@/common/utils/format';
import { useIntervalFn, useStorage } from '@vueuse/core';
import { ColumnTypeSubscript, DataType, DbDialect, getDbDialect } from '../../dialect/index';
@@ -305,6 +305,8 @@ const cmDataGenJson = new ContextmenuItem('genJson', 'db.genJson').withIcon('tic
const cmDataExportCsv = new ContextmenuItem('exportCsv', 'db.exportCsv').withIcon('document').withOnClick(() => onExportCsv());
const cmDataExportExcel = new ContextmenuItem('exportExcel', 'db.exportExcel').withIcon('document').withOnClick(() => onExportExcel());
const cmDataExportSql = new ContextmenuItem('exportSql', 'db.exportSql')
.withIcon('document')
.withOnClick(() => onExportSql())
@@ -657,7 +659,7 @@ const dataContextmenuClick = (event: any, rowIndex: number, column: any, data: a
const { clientX, clientY } = event;
state.contextmenu.dropdown.x = clientX;
state.contextmenu.dropdown.y = clientY;
state.contextmenu.items = [cmDataCopyCell, cmDataDel, cmFormView, cmDataGenInsertSql, cmDataGenJson, cmDataExportCsv, cmDataExportSql];
state.contextmenu.items = [cmDataCopyCell, cmDataDel, cmFormView, cmDataGenInsertSql, cmDataGenJson, cmDataExportExcel, cmDataExportCsv, cmDataExportSql];
contextmenuRef.value.openContextmenu({ column, rowData: data });
};
@@ -738,6 +740,20 @@ const onExportCsv = () => {
exportCsv(`Data-${state.table}-${formatDate(new Date(), 'YYYYMMDDHHmm')}`, columnNames, dataList);
};
/**
* 导出当前页数据
*/
const onExportExcel = () => {
const dataList = state.datas as any;
let columnNames = [];
for (let column of state.columns) {
if (column.show) {
columnNames.push(column.columnName);
}
}
exportExcel(`Data-${state.table}-${formatDate(new Date(), 'YYYYMMDDHHmm')}`, [{ name: 'Data', columns: columnNames, datas: dataList }]);
};
const onExportSql = async () => {
const selectionDatas = state.datas;
exportFile(`Data-${state.table}-${formatDate(new Date(), 'YYYYMMDDHHmm')}.sql`, await getNowDbInst().genInsertSql(state.db, state.table, selectionDatas));

View File

@@ -128,10 +128,10 @@
</template>
<script lang="ts" setup>
import { computed, reactive, ref, toRefs, watch, useTemplateRef, nextTick } from 'vue';
import { computed, reactive, ref, toRefs, watch, useTemplateRef, nextTick, Ref } from 'vue';
import { ElMessage } from 'element-plus';
import SqlExecBox from '../sqleditor/SqlExecBox';
import { DbType, getDbDialect, IndexDefinition, RowDefinition } from '../../dialect/index';
import { DbDialect, DbType, getDbDialect, IndexDefinition, RowDefinition } from '../../dialect/index';
import { DbInst } from '../../db';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import { useI18n } from 'vue-i18n';
@@ -165,7 +165,7 @@ const props = defineProps({
//定义事件
const emit = defineEmits(['update:visible', 'cancel', 'val-change', 'submit-sql']);
let dbDialect: any = computed(() => getDbDialect(props.dbType!, props.version));
let dbDialect: Ref<DbDialect> = computed(() => getDbDialect(props.dbType!, props.version));
type ColName = {
prop: string;

View File

@@ -205,7 +205,10 @@ class MysqlDialect implements DbDialect {
genColumnBasicSql(cl: any): string {
let val = cl.value ? (cl.value === 'CURRENT_TIMESTAMP' ? cl.value : `'${cl.value}'`) : '';
let defVal = val ? `DEFAULT ${val}` : '';
let length = cl.length ? `(${cl.length})` : '';
let length = cl.length;
if (length) {
length = cl.numScale ? `(${cl.length},${cl.numScale})` : `(${cl.length})`;
}
let onUpdate = 'update_time' === cl.name ? ' ON UPDATE CURRENT_TIMESTAMP ' : '';
return ` ${this.quoteIdentifier(cl.name)} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : 'NULL'} ${
cl.auto_increment ? 'AUTO_INCREMENT' : ''

View File

@@ -19,39 +19,3 @@ export const DbSqlExecStatusEnum = {
Success: EnumValue.of(2, 'common.success').setTagType('success'),
Fail: EnumValue.of(-2, 'common.fail').setTagType('danger'),
};
export const DbDataSyncDuplicateStrategyEnum = {
None: EnumValue.of(-1, 'db.none'),
Ignore: EnumValue.of(1, 'db.ignore'),
Replace: EnumValue.of(2, 'db.replace'),
};
export const DbDataSyncRecentStateEnum = {
Success: EnumValue.of(1, 'common.success').setTagType('success'),
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
};
export const DbDataSyncLogStatusEnum = {
Success: EnumValue.of(1, 'common.success').setTagType('success'),
Running: EnumValue.of(2, 'db.running').setTagType('primary'),
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
};
export const DbDataSyncRunningStateEnum = {
Running: EnumValue.of(1, 'db.running').setTagType('success'),
WaitRun: EnumValue.of(2, 'db.waitRun').setTagType('primary'),
Stop: EnumValue.of(3, 'db.stop').setTagType('danger'),
};
export const DbTransferRunningStateEnum = {
Success: EnumValue.of(2, 'common.success').setTagType('success'),
Running: EnumValue.of(1, 'db.running').setTagType('primary'),
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
Stop: EnumValue.of(-2, 'db.stop').setTagType('warning'),
};
export const DbTransferFileStatusEnum = {
Running: EnumValue.of(1, 'db.running').setTagType('primary'),
Success: EnumValue.of(2, 'common.success').setTagType('success'),
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
};

View File

@@ -1,4 +1,4 @@
export default {
SyncTaskList: () => import('@/views/ops/db/SyncTaskList.vue'),
DbTransferList: () => import('@/views/ops/db/DbTransferList.vue'),
SyncTaskList: () => import('@/views/ops/db/sync/SyncTaskList.vue'),
DbTransferList: () => import('@/views/ops/db/transfer/DbTransferList.vue'),
};

View File

@@ -209,7 +209,6 @@
<script lang="ts" setup>
import { computed, reactive, ref, toRefs, watch } from 'vue';
import { dbApi } from './api';
import { ElMessage } from 'element-plus';
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
@@ -218,11 +217,13 @@ import { compatibleDuplicateStrategy, DbType, getDbDialect } from '@/views/ops/d
import CrontabInput from '@/components/crontab/CrontabInput.vue';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
import { DbDataSyncDuplicateStrategyEnum } from './enums';
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
import { useI18n } from 'vue-i18n';
import FormItemTooltip from '@/components/form/FormItemTooltip.vue';
import { Rules } from '@/common/rule';
import { DbDataSyncDuplicateStrategyEnum } from '@/views/ops/db/sync/enums';
import { dbSyncApi } from '@/views/ops/db/sync/api';
import { dbApi } from '@/views/ops/db/api';
const { t } = useI18n();
@@ -306,7 +307,7 @@ const state = reactive({
const { tabActiveName, form, submitForm, fieldMapTableHeight } = toRefs(state);
const { isFetching: saveBtnLoading, execute: saveExec } = dbApi.saveDatasyncTask.useApi(submitForm);
const { isFetching: saveBtnLoading, execute: saveExec } = dbSyncApi.saveDatasyncTask.useApi(submitForm);
//
const baseFieldCompleted = computed(() => {
@@ -326,7 +327,7 @@ watch(dialogVisible, async (newValue: boolean) => {
return;
}
let data = await dbApi.getDatasyncTask.request({ taskId: propsData?.id });
let data = await dbSyncApi.getDatasyncTask.request({ taskId: propsData?.id });
state.form = data;
if (!state.form.duplicateStrategy) {
state.form.duplicateStrategy = -1;

View File

@@ -2,7 +2,7 @@
<div class="h-full">
<page-table
ref="pageTableRef"
:page-api="dbApi.datasyncTasks"
:page-api="dbSyncApi.datasyncTasks"
:searchItems="searchItems"
v-model:query-form="query"
:show-selection="true"
@@ -50,13 +50,13 @@
<script lang="ts" setup>
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
import { dbApi } from './api';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth';
import { SearchItem } from '@/components/pagetable/SearchForm';
import { DbDataSyncRecentStateEnum, DbDataSyncRunningStateEnum } from './enums';
import { useI18nConfirm, useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
import { dbSyncApi } from '@/views/ops/db/sync/api';
import { DbDataSyncRecentStateEnum, DbDataSyncRunningStateEnum } from '@/views/ops/db/sync/enums';
const DataSyncTaskEdit = defineAsyncComponent(() => import('./SyncTaskEdit.vue'));
const DataSyncTaskLog = defineAsyncComponent(() => import('./SyncTaskLog.vue'));
@@ -143,14 +143,14 @@ const edit = async (data: any) => {
const run = async (id: any) => {
await useI18nConfirm('db.runConfirm');
await dbApi.runDatasyncTask.request({ taskId: id });
await dbSyncApi.runDatasyncTask.request({ taskId: id });
useI18nOperateSuccessMsg();
setTimeout(search, 1000);
};
const stop = async (id: any) => {
await useI18nConfirm('db.stopConfirm');
await dbApi.stopDatasyncTask.request({ taskId: id });
await dbSyncApi.stopDatasyncTask.request({ taskId: id });
useI18nOperateSuccessMsg();
search();
};
@@ -163,7 +163,7 @@ const log = async (data: any) => {
const updStatus = async (id: any, status: 1 | -1) => {
try {
await dbApi.updateDatasyncTaskStatus.request({ taskId: id, status });
await dbSyncApi.updateDatasyncTaskStatus.request({ taskId: id, status });
useI18nOperateSuccessMsg();
search();
} catch (err) {
@@ -174,7 +174,7 @@ const updStatus = async (id: any, status: 1 | -1) => {
const del = async () => {
try {
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.taskName).join('、'));
await dbApi.deleteDatasyncTask.request({ taskId: state.selectionData.map((x: any) => x.id).join(',') });
await dbSyncApi.deleteDatasyncTask.request({ taskId: state.selectionData.map((x: any) => x.id).join(',') });
useI18nDeleteSuccessMsg();
search();
} catch (err) {

View File

@@ -6,7 +6,7 @@
<el-switch v-model="realTime" @change="watchPolling" inline-prompt :active-text="$t('db.realTime')" :inactive-text="$t('db.noRealTime')" />
<el-button @click="search" icon="Refresh" circle size="small" :loading="realTime" class="ml-2"></el-button>
</template>
<page-table ref="logTableRef" :page-api="dbApi.datasyncLogs" v-model:query-form="query" :tool-button="false" :columns="columns" size="small">
<page-table ref="logTableRef" :page-api="dbSyncApi.datasyncLogs" v-model:query-form="query" :tool-button="false" :columns="columns" size="small">
</page-table>
</el-dialog>
</div>
@@ -14,10 +14,10 @@
<script lang="ts" setup>
import { reactive, Ref, ref, toRefs, watch } from 'vue';
import { dbApi } from '@/views/ops/db/api';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { DbDataSyncLogStatusEnum } from './enums';
import { dbSyncApi } from '@/views/ops/db/sync/api';
import { DbDataSyncLogStatusEnum } from '@/views/ops/db/sync/enums';
const props = defineProps({
taskId: {

View File

@@ -0,0 +1,14 @@
import Api from '@/common/Api';
import { encryptField } from '@/views/ops/db/api';
export const dbSyncApi = {
// 数据同步相关
datasyncTasks: Api.newGet('/datasync/tasks'),
saveDatasyncTask: Api.newPost('/datasync/tasks/save').withBeforeHandler(async (param: any) => await encryptField(param, 'dataSql')),
getDatasyncTask: Api.newGet('/datasync/tasks/{taskId}'),
deleteDatasyncTask: Api.newDelete('/datasync/tasks/{taskId}/del'),
updateDatasyncTaskStatus: Api.newPost('/datasync/tasks/{taskId}/status'),
runDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/run'),
stopDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/stop'),
datasyncLogs: Api.newGet('/datasync/tasks/{taskId}/logs'),
};

View File

@@ -0,0 +1,24 @@
import { EnumValue } from '@/common/Enum';
export const DbDataSyncDuplicateStrategyEnum = {
None: EnumValue.of(-1, 'db.none'),
Ignore: EnumValue.of(1, 'db.ignore'),
Replace: EnumValue.of(2, 'db.replace'),
};
export const DbDataSyncRecentStateEnum = {
Success: EnumValue.of(1, 'common.success').setTagType('success'),
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
};
export const DbDataSyncLogStatusEnum = {
Success: EnumValue.of(1, 'common.success').setTagType('success'),
Running: EnumValue.of(2, 'db.running').setTagType('primary'),
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
};
export const DbDataSyncRunningStateEnum = {
Running: EnumValue.of(1, 'db.running').setTagType('success'),
WaitRun: EnumValue.of(2, 'db.waitRun').setTagType('primary'),
Stop: EnumValue.of(3, 'db.stop').setTagType('danger'),
};

View File

@@ -0,0 +1 @@
Db sync (数据库迁移模块)

View File

@@ -12,7 +12,7 @@
<el-input v-model.trim="form.taskName" auto-complete="off" />
</el-form-item>
<el-row class="!w-full">
<el-row class="w-full!">
<el-col :span="12">
<el-form-item prop="status" :label="$t('common.status')" label-position="left">
<el-switch
@@ -40,7 +40,7 @@
<CrontabInput v-model="form.cron" />
</el-form-item>
<el-form-item prop="srcDbId" :label="$t('db.srcDb')" class="!w-full" required>
<el-form-item prop="srcDbId" :label="$t('db.srcDb')" class="w-full!" required>
<db-select-tree
v-model:db-id="form.srcDbId"
v-model:inst-name="form.srcInstName"
@@ -59,7 +59,7 @@
</el-form-item>
<el-form-item v-if="form.mode === 2">
<el-row class="!w-full">
<el-row class="w-full!">
<el-col :span="12">
<el-form-item prop="targetFileDbType" :label="$t('db.dbFileType')" :required="form.mode === 2">
<el-select v-model="form.targetFileDbType" clearable filterable>
@@ -98,7 +98,7 @@
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.mode == 1" prop="targetDbId" :label="$t('db.targetDb')" class="!w-full" :required="form.mode === 1">
<el-form-item v-if="form.mode == 1" prop="targetDbId" :label="$t('db.targetDb')" class="w-full!" :required="form.mode === 1">
<db-select-tree
v-model:db-id="form.targetDbId"
v-model:inst-name="form.targetInstName"
@@ -121,11 +121,11 @@
<el-form-item>
<el-input v-model="state.filterSrcTableText" placeholder="filter table" size="small" />
</el-form-item>
<el-form-item class="!w-full">
<el-form-item class="w-full!">
<el-tree
ref="srcTreeRef"
class="!w-full"
style="max-height: 200px; overflow-y: auto"
class="w-full! overflow-y-auto"
style="max-height: 200px"
default-expand-all
:expand-on-click-node="false"
:data="state.srcTableTree"
@@ -149,7 +149,7 @@
<script lang="ts" setup>
import { nextTick, reactive, ref, toRefs, watch } from 'vue';
import { dbApi } from './api';
import { ElMessage } from 'element-plus';
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
import CrontabInput from '@/components/crontab/CrontabInput.vue';
@@ -160,6 +160,8 @@ import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
import { useI18n } from 'vue-i18n';
import { Rules } from '@/common/rule';
import { deepClone } from '@/common/utils/object';
import { dbApi } from '@/views/ops/db/api';
import { dbTransferApi } from '@/views/ops/db/transfer/api';
const { t } = useI18n();
@@ -256,7 +258,7 @@ const state = reactive({
const { form, submitForm } = toRefs(state);
const { isFetching: saveBtnLoading, execute: saveExec } = dbApi.saveDbTransferTask.useApi(submitForm);
const { isFetching: saveBtnLoading, execute: saveExec } = dbTransferApi.saveDbTransferTask.useApi(submitForm);
watch(dialogVisible, async (newValue: boolean) => {
if (!newValue) {

View File

@@ -12,7 +12,7 @@
<page-table
ref="pageTableRef"
v-model:query-form="state.query"
:page-api="dbApi.dbTransferFileList"
:page-api="dbTransferApi.dbTransferFileList"
:lazy="true"
:show-selection="true"
v-model:selection-data="state.selectionData"
@@ -25,7 +25,7 @@
</template>
<template #fileKey="{ data }">
<FileInfo :fileKey="data.fileKey" :canDownload="actionBtns[perms.down] && data.status === 2" />
<FileInfo :fileKey="data.fileKey" show-file-size :canDownload="actionBtns[perms.down] && data.status === 2" />
</template>
<template #fileDbType="{ data }">
@@ -79,7 +79,6 @@
<script lang="ts" setup>
import { onMounted, reactive, Ref, ref, useTemplateRef, watch } from 'vue';
import { dbApi } from '@/views/ops/db/api';
import { getDbDialect } from '@/views/ops/db/dialect';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
@@ -89,10 +88,11 @@ import TerminalLog from '@/components/terminal/TerminalLog.vue';
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
import { getClientId } from '@/common/utils/storage';
import FileInfo from '@/components/file/FileInfo.vue';
import { DbTransferFileStatusEnum } from './enums';
import { useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nFormValidate, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
import { useI18n } from 'vue-i18n';
import { Rules } from '@/common/rule';
import { dbTransferApi } from '@/views/ops/db/transfer/api';
import { DbTransferFileStatusEnum } from '@/views/ops/db/transfer/enums';
const { t } = useI18n();
@@ -179,7 +179,7 @@ const state = reactive({
return false;
}
state.runDialog.runForm.clientId = getClientId();
await dbApi.dbTransferFileRun.request(state.runDialog.runForm);
await dbTransferApi.dbTransferFileRun.request(state.runDialog.runForm);
useI18nOperateSuccessMsg();
state.runDialog.onCancel();
await search();
@@ -196,7 +196,7 @@ const state = reactive({
const search = async () => {
pageTableRef.value?.search();
// const { total, list } = await dbApi.dbTransferFileList.request(state.query);
// const { total, list } = await dbTransferApi.dbTransferFileList.request(state.query);
// state.tableData = list;
// pageTableRef.value.total = total;
};
@@ -204,7 +204,7 @@ const search = async () => {
const onDel = async function () {
try {
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.fileKey).join('、'));
await dbApi.dbTransferFileDel.request({ fileId: state.selectionData.map((x: any) => x.id).join(',') });
await dbTransferApi.dbTransferFileDel.request({ fileId: state.selectionData.map((x: any) => x.id).join(',') });
useI18nDeleteSuccessMsg();
await search();
} catch (err) {

View File

@@ -2,7 +2,7 @@
<div class="h-full">
<page-table
ref="pageTableRef"
:page-api="dbApi.dbTransferTasks"
:page-api="dbTransferApi.dbTransferTasks"
:searchItems="searchItems"
v-model:query-form="query"
:show-selection="true"
@@ -84,19 +84,19 @@
<script lang="ts" setup>
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
import { dbApi } from './api';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth';
import { SearchItem } from '@/components/pagetable/SearchForm';
import { getDbDialect } from '@/views/ops/db/dialect';
import { DbTransferRunningStateEnum } from './enums';
import TerminalLog from '@/components/terminal/TerminalLog.vue';
import DbTransferFile from './DbTransferFile.vue';
import { useI18nConfirm, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
import { useI18n } from 'vue-i18n';
import { dbTransferApi } from '@/views/ops/db/transfer/api';
import { DbTransferRunningStateEnum } from '@/views/ops/db/transfer/enums';
const DbTransferEdit = defineAsyncComponent(() => import('./DbTransferEdit.vue'));
const DbTransferFile = defineAsyncComponent(() => import('./DbTransferFile.vue'));
const { t } = useI18n();
@@ -189,7 +189,7 @@ const edit = async (data: any) => {
const stop = async (id: any) => {
await useI18nConfirm('db.stopConfirm');
await dbApi.stopDbTransferTask.request({ taskId: id });
await dbTransferApi.stopDbTransferTask.request({ taskId: id });
useI18nOperateSuccessMsg();
search();
};
@@ -204,7 +204,7 @@ const onOpenLog = (data: any) => {
const onReRun = async (data: any) => {
await useI18nConfirm('db.runConfirm');
try {
let res = await dbApi.runDbTransferTask.request({ taskId: data.id });
let res = await dbTransferApi.runDbTransferTask.request({ taskId: data.id });
useI18nOperateSuccessMsg();
// id
onOpenLog({ logId: res, state: DbTransferRunningStateEnum.Running.value });
@@ -225,7 +225,7 @@ const openFiles = async (data: any) => {
};
const updStatus = async (id: any, status: 1 | -1) => {
try {
await dbApi.updateDbTransferTaskStatus.request({ taskId: id, status });
await dbTransferApi.updateDbTransferTaskStatus.request({ taskId: id, status });
useI18nOperateSuccessMsg();
search();
} catch (err) {
@@ -236,7 +236,7 @@ const updStatus = async (id: any, status: 1 | -1) => {
const del = async () => {
try {
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.taskName).join('、'));
await dbApi.deleteDbTransferTask.request({ taskId: state.selectionData.map((x: any) => x.id).join(',') });
await dbTransferApi.deleteDbTransferTask.request({ taskId: state.selectionData.map((x: any) => x.id).join(',') });
useI18nDeleteSuccessMsg();
search();
} catch (err) {

View File

@@ -0,0 +1,16 @@
import Api from '@/common/Api';
export const dbTransferApi = {
// 数据库迁移相关
dbTransferTasks: Api.newGet('/dbTransfer'),
saveDbTransferTask: Api.newPost('/dbTransfer/save'),
deleteDbTransferTask: Api.newDelete('/dbTransfer/{taskId}/del'),
updateDbTransferTaskStatus: Api.newPost('/dbTransfer/{taskId}/status'),
runDbTransferTask: Api.newPost('/dbTransfer/{taskId}/run'),
stopDbTransferTask: Api.newPost('/dbTransfer/{taskId}/stop'),
dbTransferTaskLogs: Api.newGet('/dbTransfer/{taskId}/logs'),
dbTransferFileList: Api.newGet('/dbTransfer/files/{taskId}'),
dbTransferFileDel: Api.newPost('/dbTransfer/files/del/{fileId}'),
dbTransferFileRun: Api.newPost('/dbTransfer/files/run'),
dbTransferFileDown: Api.newGet('/dbTransfer/files/down/{fileUuid}'),
};

View File

@@ -0,0 +1,14 @@
import { EnumValue } from '@/common/Enum';
export const DbTransferRunningStateEnum = {
Success: EnumValue.of(2, 'common.success').setTagType('success'),
Running: EnumValue.of(1, 'db.running').setTagType('primary'),
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
Stop: EnumValue.of(-2, 'db.stop').setTagType('warning'),
};
export const DbTransferFileStatusEnum = {
Running: EnumValue.of(1, 'db.running').setTagType('primary'),
Success: EnumValue.of(2, 'common.success').setTagType('success'),
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
};

View File

@@ -0,0 +1 @@
Db transfer (数据库迁移模块)

View File

@@ -19,6 +19,13 @@
<el-form-item prop="version" :label="t('common.version')">
<el-input v-model.trim="form.version" auto-complete="off" disabled></el-input>
</el-form-item>
<!-- 增加协议下拉框 http和https默认http-->
<el-form-item prop="protocol" :label="t('es.protocol')">
<el-select v-model="form.protocol" placeholder="http">
<el-option label="http" value="http"></el-option>
<el-option label="https" value="https"></el-option>
</el-select>
</el-form-item>
<el-form-item prop="host" label="Host" required>
<el-col :span="18">
@@ -105,6 +112,7 @@ const DefaultForm = {
id: null,
code: '',
name: null,
protocol: 'http',
host: '',
version: '',
port: 9200,

View File

@@ -569,9 +569,9 @@ const parseParams = () => {
// minimum_should_match 需要结合should使用默认为1表示至少一个should条件满足
if (should.length > 0) {
state.search['minimum_should_match'] = Math.max(1, state.minimum_should_match);
state.search.query.bool['minimum_should_match'] = Math.max(1, state.minimum_should_match);
} else {
delete state.search['minimum_should_match'];
delete state.search.query.bool['minimum_should_match'];
}
};
</script>

View File

@@ -11,7 +11,7 @@
>
<page-table ref="pageTableRef" :page-api="machineApi.termOpRecs" :lazy="true" height="100%" v-model:query-form="query" :columns="columns">
<template #fileKey="{ data }">
<FileInfo :fileKey="data.fileKey" />
<FileInfo :fileKey="data.fileKey" show-file-size />
</template>
<template #action="{ data }">

View File

@@ -120,7 +120,7 @@ const getNode = (nodeKey: any) => {
};
const changeNode = (val: any) => {
// 触发改变时间,并传递节点数据
// 触发改变事件,并传递节点数据
emit('change', getNode(val)?.data);
};
</script>

View File

@@ -1,16 +1,10 @@
<template>
<div class="tag-tree-list card !p-2 h-full flex">
<div class="tag-tree-list card p-2! h-full flex">
<el-splitter>
<el-splitter-panel size="24%" max="35%" class="flex flex-col flex-1">
<div class="card !p-1 !mr-1 flex flex-row items-center justify-between overflow-hidden">
<div class="card p-1! mr-1! flex flex-row items-center justify-between overflow-hidden">
<el-input v-model="filterTag" clearable :placeholder="$t('tag.nameFilterPlaceholder')" class="mr-2" />
<el-button
v-if="useUserInfo().userInfo.username == 'admin'"
v-auth="'tag:save'"
type="primary"
icon="plus"
@click="onShowSaveTagDialog(null)"
></el-button>
<el-button v-auth="'tag:save'" type="primary" icon="plus" @click="onShowSaveTagDialog(null)"></el-button>
<div>
<el-tooltip placement="top">
<template #content>

View File

@@ -38,10 +38,8 @@
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="onCloseSetConfigDialog()">{{ $t('common.cancel') }}</el-button>
<el-button v-auth="'config:save'" type="primary" @click="setConfig()">{{ $t('common.confirm') }}</el-button>
</span>
<el-button @click="onCloseSetConfigDialog()">{{ $t('common.cancel') }}</el-button>
<el-button v-auth="'config:save'" type="primary" @click="setConfig()">{{ $t('common.confirm') }}</el-button>
</template>
</el-dialog>

View File

@@ -4,28 +4,28 @@
<el-form :model="form" :inline="true" ref="menuFormRef" :rules="rules" label-width="auto">
<el-row :gutter="35">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item class="!w-full" prop="type" :label="$t('common.type')" required>
<el-form-item class="w-full!" prop="type" :label="$t('common.type')" required>
<enum-select :enums="ResourceTypeEnum" v-model="form.type" :disabled="typeDisabled" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item class="!w-full" prop="name" :label="$t('common.name')" required>
<el-form-item class="w-full!" prop="name" :label="$t('common.name')" required>
<el-input v-model.trim="form.name" auto-complete="off"></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<FormItemTooltip class="!w-full" label="path|code" prop="code" :tooltip="$t('system.menu.menuCodeTips')">
<FormItemTooltip class="w-full!" label="path|code" prop="code" :tooltip="$t('system.menu.menuCodeTips')">
<el-input v-model.trim="form.code" :placeholder="$t('system.menu.menuCodePlaceholder')" auto-complete="off"></el-input>
</FormItemTooltip>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue">
<el-form-item class="!w-full" :label="$t('system.menu.icon')">
<el-form-item class="w-full!" :label="$t('system.menu.icon')">
<icon-selector v-model="form.meta.icon" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue">
<FormItemTooltip
class="!w-full"
class="w-full!"
:label="$t('system.menu.routerName')"
prop="meta.routeName"
:tooltip="$t('system.menu.routerNameTips')"
@@ -34,34 +34,34 @@
</FormItemTooltip>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue">
<FormItemTooltip class="!w-full" :label="$t('system.menu.isCache')" prop="meta.isKeepAlive" :tooltip="$t('system.menu.isCacheTips')">
<el-select v-model="form.meta.isKeepAlive" class="!w-full">
<FormItemTooltip class="w-full!" :label="$t('system.menu.isCache')" prop="meta.isKeepAlive" :tooltip="$t('system.menu.isCacheTips')">
<el-select v-model="form.meta.isKeepAlive" class="w-full!">
<el-option v-for="item in trueFalseOption" :key="item.value" :label="item.label" :value="item.value"> </el-option>
</el-select>
</FormItemTooltip>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue">
<FormItemTooltip class="!w-full" :label="$t('system.menu.isHide')" prop="meta.isHide" :tooltip="$t('system.menu.isHideTips')">
<el-select v-model="form.meta.isHide" class="!w-full">
<FormItemTooltip class="w-full!" :label="$t('system.menu.isHide')" prop="meta.isHide" :tooltip="$t('system.menu.isHideTips')">
<el-select v-model="form.meta.isHide" class="w-full!">
<el-option v-for="item in trueFalseOption" :key="item.value" :label="item.label" :value="item.value"> </el-option>
</el-select>
</FormItemTooltip>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue">
<el-form-item class="!w-full" prop="meta.isAffix" :label="$t('system.menu.tagIsDelete')">
<el-select v-model="form.meta.isAffix" class="!w-full">
<el-form-item class="w-full!" prop="meta.isAffix" :label="$t('system.menu.tagIsDelete')">
<el-select v-model="form.meta.isAffix" class="w-full!">
<el-option v-for="item in trueFalseOption" :key="item.value" :label="item.label" :value="item.value"> </el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue">
<FormItemTooltip
class="!w-full"
class="w-full!"
:label="$t('system.menu.externalLink')"
prop="meta.linkType"
:tooltip="$t('system.menu.externalLinkTips')"
>
<el-select class="!w-full" @change="onChangeLinkType" v-model="form.meta.linkType">
<el-select class="w-full!" @change="onChangeLinkType" v-model="form.meta.linkType">
<el-option :key="0" :label="$t('system.menu.no')" :value="0"> </el-option>
<el-option :key="1" :label="$t('system.menu.inline')" :value="LinkTypeEnum.Iframes.value"> </el-option>
<el-option :key="2" :label="$t('system.menu.externalLink')" :value="LinkTypeEnum.Link.value"> </el-option>
@@ -69,7 +69,7 @@
</FormItemTooltip>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue && form.meta.linkType > 0">
<el-form-item prop="meta.link" :label="$t('system.menu.linkAddress')" class="!w-full">
<el-form-item prop="meta.link" :label="$t('system.menu.linkAddress')" class="w-full!">
<el-input v-model.trim="form.meta.link" :placeholder="$t('system.menu.linkPlaceholder')"></el-input>
</el-form-item>
</el-col>
@@ -85,7 +85,7 @@
</template>
<script lang="ts" setup>
import { toRefs, reactive, watchEffect, useTemplateRef, watch } from 'vue';
import { toRefs, reactive, useTemplateRef, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { resourceApi } from '../api';
import { ResourceTypeEnum } from '../enums';

View File

@@ -1,8 +1,8 @@
<template>
<div class="card !p-2 system-resource-list h-full flex">
<div class="card p-2! system-resource-list h-full flex">
<el-splitter>
<el-splitter-panel size="30%" max="35%" min="25%" class="flex flex-col flex-1">
<div class="card !p-1 mr-1 flex flex-row items-center justify-between overflow-hidden">
<div class="card p-1! mr-1 flex flex-row items-center justify-between overflow-hidden">
<el-input v-model="filterResource" clearable :placeholder="$t('system.menu.filterPlaceholder')" class="mr-2" />
<el-button v-auth="perms.addResource" type="primary" icon="plus" @click="onAddResource(false)"></el-button>
@@ -35,7 +35,7 @@
>
<template #default="{ data }">
<span class="custom-tree-node">
<SvgIcon :name="getMenuIcon(data)" class="!mb-0.5" />
<SvgIcon :name="getMenuIcon(data)" class="mb-0.5!" />
<span style="font-size: 13px" v-if="data.type === menuTypeValue">
<span style="color: #3c8dbc"></span>
@@ -180,7 +180,6 @@ const ResourceRoles = 'resourceRoles';
const contextmenuAdd = new ContextmenuItem('add', 'system.menu.addSubResource')
.withIcon('circle-plus')
.withPermission(perms.addResource)
.withHideFunc((data: any) => data.type !== menuTypeValue)
.withOnClick((data: any) => onAddResource(data));
const contextmenuEdit = new ContextmenuItem('edit', 'common.edit')
@@ -294,37 +293,24 @@ const onDeleteMenu = async (data: any) => {
const onAddResource = (data: any) => {
let dialog = state.dialogForm;
dialog.data = { pid: 0, type: 1 };
dialog.typeDisabled = false;
// 添加顶级菜单情况
if (!data) {
dialog.typeDisabled = true;
dialog.data.type = menuTypeValue;
dialog.title = t('system.menu.addTopMenu');
dialog.visible = true;
return;
}
// 父节点为权限类型,子节点也只允许添加权限类型
if (data.type === permissionTypeValue) {
dialog.typeDisabled = true;
dialog.data.type = permissionTypeValue;
}
// 添加子菜单把当前菜单id作为新增菜单pid
dialog.data.pid = data.id;
dialog.title = t('system.menu.addChildrenMenuTitle', { parentName: t(data.name) });
if (data.children === null || data.children.length === 0) {
// 如果子节点不存在,则资源类型可选择
dialog.typeDisabled = false;
} else {
dialog.typeDisabled = true;
let hasPermission = false;
for (let c of data.children) {
if (c.type === permissionTypeValue) {
hasPermission = true;
break;
}
}
// 如果子节点中存在权限资源,则只能新增权限资源,否则只能新增菜单资源
if (hasPermission) {
dialog.data.type = permissionTypeValue;
} else {
dialog.data.type = menuTypeValue;
}
}
dialog.visible = true;
};

View File

@@ -3,9 +3,11 @@ module mayfly-go
go 1.25
require (
gitee.com/chunanyong/dm v1.8.20
gitee.com/chunanyong/dm v1.8.21
gitee.com/liuzongyang/libpq v1.10.11
github.com/antlr4-go/antlr/v4 v4.13.1
github.com/cloudwego/eino v0.7.6
github.com/cloudwego/eino-ext/components/model/openai v0.1.5
github.com/docker/docker v28.5.0+incompatible
github.com/docker/go-connections v0.6.0
github.com/gin-gonic/gin v1.11.0
@@ -24,9 +26,9 @@ require (
github.com/mojocn/base64Captcha v1.3.8 //
github.com/opencontainers/image-spec v1.1.1
github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.13.9
github.com/pkg/sftp v1.13.10
github.com/pquerna/otp v1.5.0
github.com/redis/go-redis/v9 v9.14.0
github.com/redis/go-redis/v9 v9.17.2
github.com/robfig/cron/v3 v3.0.1 //
github.com/sijms/go-ora/v2 v2.9.0
github.com/spf13/cast v1.10.0
@@ -34,27 +36,30 @@ require (
github.com/tidwall/gjson v1.18.0
github.com/veops/go-ansiterm v0.0.5
go.mongodb.org/mongo-driver/v2 v2.3.0 // mongo
golang.org/x/crypto v0.43.0 // ssh
golang.org/x/oauth2 v0.32.0
golang.org/x/sync v0.17.0
golang.org/x/crypto v0.45.0 // ssh
golang.org/x/oauth2 v0.34.0
golang.org/x/sync v0.19.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
// gorm
gorm.io/driver/mysql v1.6.0
gorm.io/gorm v1.31.0
gorm.io/gorm v1.31.1
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/boombuler/barcode v1.1.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.2 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
@@ -63,6 +68,8 @@ require (
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/eino-contrib/jsonschema v1.0.3 // indirect
github.com/evanphx/json-patch v0.5.2 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
@@ -76,6 +83,7 @@ require (
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/goph/emperror v0.17.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
@@ -83,8 +91,10 @@ require (
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/meguminnnnnnnnn/go-openai v0.1.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
@@ -92,20 +102,25 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/nikolalohinski/gonja v1.5.3 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.55.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f // indirect
github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/yargevad/filepathx v1.0.0 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
@@ -117,11 +132,11 @@ require (
golang.org/x/arch v0.21.0 // indirect
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect
golang.org/x/image v0.31.0 // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/net v0.45.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/tools v0.37.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/tools v0.38.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
modernc.org/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect

View File

@@ -0,0 +1,111 @@
package agent
import (
"context"
"errors"
"io"
"mayfly-go/internal/ai/config"
aimodel "mayfly-go/internal/ai/model"
"mayfly-go/pkg/logx"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/flow/agent"
"github.com/cloudwego/eino/flow/agent/react"
"github.com/cloudwego/eino/schema"
)
// GetAiAgent 获取AI Agent
func GetAiAgent(ctx context.Context, aiConfig *config.AIModelConfig) (*react.Agent, error) {
aiModel := aimodel.GetAIModelByConfig(aiConfig)
if aiModel == nil {
return nil, errors.New("no supported AI model found")
}
toolableChatModel, err := aiModel.GetChatModel(ctx, aiConfig)
if err != nil {
return nil, err
}
// 初始化所需的 tools
aiTools := compose.ToolsNodeConfig{
Tools: []tool.BaseTool{},
}
// 创建 agent
return react.NewAgent(ctx, &react.AgentConfig{
ToolCallingModel: toolableChatModel,
ToolsConfig: aiTools,
MaxStep: len(aiTools.Tools)*1 + 3,
MessageModifier: func(ctx context.Context, input []*schema.Message) []*schema.Message {
return input
},
})
}
type AiAgent struct {
*react.Agent
}
// NewAiAgent 创建AI Agent
func NewAiAgent(ctx context.Context) (*AiAgent, error) {
agent, err := GetAiAgent(ctx, config.GetAiModel())
if err != nil {
return nil, err
}
return &AiAgent{
Agent: agent,
}, nil
}
// Chat 聊天,返回消息流通道
func (aiAgent *AiAgent) Chat(ctx context.Context, sysPrompt string, question string) chan *schema.Message {
ch := make(chan *schema.Message, 512)
if sysPrompt == "" {
sysPrompt = "你现在是一位拥有20年实战经验的顶级系统运维专家精通Linux操作系统、数据库管理如MySQL、PostgreSQL、NoSQL数据库如Redis、MongoDB以及搜索引擎如Elasticsearch。"
}
agentOption := []agent.AgentOption{}
go func() {
defer close(ch)
sr, err := aiAgent.Stream(ctx, []*schema.Message{
{
Role: schema.System,
Content: sysPrompt,
},
{
Role: schema.User,
Content: question,
},
}, agentOption...)
if err != nil {
logx.Errorf("agent stream error: %v", err)
return
}
defer sr.Close()
for {
msg, err := sr.Recv()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
logx.Errorf("failed to recv response: %v", err)
break
}
// logx.Debugf("stream: %s", msg.String())
ch <- msg
}
}()
return ch
}
// GetChatMsg 获取完整的聊天回复内容
func (aiAgent *AiAgent) GetChatMsg(ctx context.Context, sysPrompt string, question string) string {
msgChan := aiAgent.Chat(ctx, sysPrompt, question)
res := ""
for msg := range msgChan {
res += msg.Content
}
return res
}

View File

@@ -0,0 +1,37 @@
package config
import (
"cmp"
sysapp "mayfly-go/internal/sys/application"
"github.com/spf13/cast"
)
const (
ConfigKeyAi string = "AiModelConfig"
)
type AIModelConfig struct {
ModelType string `json:"modelType"`
Model string `json:"model"`
BaseUrl string `json:"baseUrl"`
ApiKey string `json:"apiKey"` // api key
TimeOut int `json:"timeOut"` // 请求超时时间,单位秒
Temperature float32 `json:"temperature"`
MaxTokens int `json:"maxTokens"`
}
func GetAiModel() *AIModelConfig {
c := sysapp.GetConfigApp().GetConfig(ConfigKeyAi)
jm := c.GetJsonMap()
conf := new(AIModelConfig)
conf.ModelType = cast.ToString(jm["modelType"])
conf.Model = cast.ToString(jm["model"])
conf.BaseUrl = cast.ToString(jm["baseUrl"])
conf.ApiKey = cast.ToString(jm["apiKey"])
conf.TimeOut = cmp.Or(cast.ToInt(jm["timeOut"]), 60)
conf.Temperature = cmp.Or(cast.ToFloat32(jm["temperature"]), 0.7)
conf.MaxTokens = cmp.Or(cast.ToInt(jm["maxTokens"]), 2048)
return conf
}

View File

@@ -0,0 +1,40 @@
package model
import (
"context"
"mayfly-go/internal/ai/config"
"github.com/cloudwego/eino/components/model"
)
func init() {
RegisterAIModel(new(Openai))
}
var (
aiModelMap = map[string]AIModel{}
)
type AIModel interface {
// SupportModel 支持的模型
SupportModel() string
// GetChatModel 获取聊天模型
GetChatModel(ctx context.Context, aiConfig *config.AIModelConfig) (model.ToolCallingChatModel, error)
}
// RegisterAIModel 注册AI模型
func RegisterAIModel(aiModel AIModel) {
aiModelMap[aiModel.SupportModel()] = aiModel
}
// GetAIModel 获取AI模型
func GetAIModel(name string) AIModel {
return aiModelMap[name]
}
// GetAIModelByConfig 根据配置获取AI模型
func GetAIModelByConfig(aiConfig *config.AIModelConfig) AIModel {
return GetAIModel(aiConfig.ModelType)
}

View File

@@ -0,0 +1,28 @@
package model
import (
"context"
"mayfly-go/internal/ai/config"
"time"
"github.com/cloudwego/eino-ext/components/model/openai"
"github.com/cloudwego/eino/components/model"
)
type Openai struct {
}
func (o *Openai) SupportModel() string {
return "openai"
}
func (o *Openai) GetChatModel(ctx context.Context, aiConfig *config.AIModelConfig) (model.ToolCallingChatModel, error) {
return openai.NewChatModel(ctx, &openai.ChatModelConfig{
BaseURL: aiConfig.BaseUrl,
Model: aiConfig.Model,
APIKey: aiConfig.ApiKey,
Timeout: time.Duration(aiConfig.TimeOut) * time.Second,
MaxTokens: &aiConfig.MaxTokens,
Temperature: &aiConfig.Temperature,
})
}

View File

@@ -0,0 +1,51 @@
package prompt
import (
"embed"
"fmt"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/utils/stringx"
"strings"
)
const (
FLOW_BIZ_AUDIT = "FLOW_BIZ_AUDIT"
)
//go:embed prompts.txt
var prompts embed.FS
// prompt缓存 key: XXX_YYY value: 内容
var promptCache = make(map[string]string, 20)
// 获取本地文件的prompt内容并进行解析获取对应key的prompt内容
func GetPrompt(key string, formatValues ...any) string {
prompt := promptCache[key]
if prompt != "" {
return fmt.Sprintf(prompt, formatValues...)
}
bytes, err := prompts.ReadFile("prompts.txt")
if err != nil {
logx.Error("failed to read prompt file: prompts.txt, err: %v", err)
return ""
}
allPrompts := string(bytes)
propmts := strings.Split(allPrompts, "---------------------------------------")
var res string
for _, keyAndPrompt := range propmts {
keyAndPrompt = stringx.TrimSpaceAndBr(keyAndPrompt)
// 获取第一行的Key信息如--XXX_YYY
info := strings.SplitN(keyAndPrompt, "\n", 2)
// prompt即去除第一行的key与备注信息
prompt := info[1]
// 获取keyXXX_YYY
promptKey := strings.Split(strings.Split(info[0], " ")[0], "--")[1]
if key == promptKey {
res = prompt
}
promptCache[promptKey] = prompt
}
return fmt.Sprintf(res, formatValues...)
}

View File

@@ -0,0 +1,17 @@
--FLOW_BIZ_AUDIT 流程业务审核
你现在是一位专业的数据库管理员、Redis管理员和安全审核专家。请根据以下审核规则分析用户提供的内容并以严格的JSON格式返回分析结果。
审核规则:
%s
待审核内容可能是SQL语句或Redis命令SQL和Redis命令可能都为多条请根据内容类型应用相应的审核规则。
如果是多条命令请逐一审核每条命令的安全性和合规性只要有一条命令不合规则allowExecute应为false。
请严格遵循以下要求:
1. 仅输出有效的JSON对象不要包含任何解释性文字
2. 禁止包含任何Markdown格式包括但不限于```json、```等代码引用符号)
3. JSON格式必须严格包含以下字段且无额外内容
{
"allowExecute": boolean, // 是否允许执行操作true或false
"suggestion": string // 具体的建议内容,如"通过"或"拒绝原因"等。如果是多条命令审核,请详细说明哪条命令存在问题
}

View File

@@ -336,7 +336,7 @@ func (d *dbAppImpl) DumpDb(ctx context.Context, reqParam *dto.DumpDb) error {
dataCount := 0
rows := make([][]any, 0)
_, err = dbConn.WalkTableRows(ctx, tableName, func(row map[string]any, _ []*dbi.QueryColumn) error {
_, err = dbConn.WalkTableRows(ctx, quoteTableName, func(row map[string]any, _ []*dbi.QueryColumn) error {
rowValues := make([]any, len(columns))
for i, col := range columns {
rowValues[i] = row[col.ColumnName]

View File

@@ -290,7 +290,7 @@ func (app *dbTransferAppImpl) transfer2File(ctx context.Context, taskId uint64,
}
_ = app.transferFileApp.Save(ctx, tFile)
filename := fmt.Sprintf("dtf_%s_%s.sql", task.TaskName, timex.TimeNo())
filename := fmt.Sprintf("dtf_%s.sql", timex.TimeNo())
fileKey, writer, saveFileFunc, err := app.fileApp.NewWriter(ctx, "", filename)
if err != nil {
app.EndTransfer(ctx, logId, taskId, "create file error", err, nil)
@@ -393,7 +393,7 @@ func (app *dbTransferAppImpl) addCronJob(ctx context.Context, taskEntity *entity
taskId := taskEntity.Id
if err := scheduler.AddFunByKey(key, taskEntity.Cron, func() {
logx.Infof("start the synchronization task: %d", taskId)
logx.Infof("start the transfer task: %d", taskId)
if _, err := app.Run(ctx, taskId); err != nil {
logx.Warn(err.Error())
}

View File

@@ -6,6 +6,7 @@ import (
type InstanceForm struct {
Id uint64 `json:"id"`
Protocol string `binding:"required" json:"protocol"`
Name string `binding:"required" json:"name"`
Host string `binding:"required" json:"host"`
Port int `binding:"required" json:"port"`

View File

@@ -9,13 +9,14 @@ type InstanceListVO struct {
tagentity.AuthCerts // 授权凭证信息
tagentity.ResourceTags
Id *int64 `json:"id"`
Code string `json:"code"`
Name *string `json:"name"`
Host *string `json:"host"`
Port *int `json:"port"`
Version *string `json:"version"`
Remark *string `json:"remark"`
Id *int64 `json:"id"`
Code string `json:"code"`
Name *string `json:"name"`
Protocol *string `json:"protocol"`
Host *string `json:"host"`
Port *int `json:"port"`
Version *string `json:"version"`
Remark *string `json:"remark"`
CreateTime *time.Time `json:"createTime"`
Creator *string `json:"creator"`

View File

@@ -10,6 +10,7 @@ type EsInstance struct {
Code string `json:"code" gorm:"size:32;not null;"`
Name string `json:"name" gorm:"size:32;not null;"`
Protocol string `json:"protocol" gorm:"size:10;not null;"`
Host string `json:"host" gorm:"size:255;not null;"`
Port int `json:"port"`
Network string `json:"network" gorm:"size:20;"`

View File

@@ -1,6 +1,7 @@
package esi
import (
"crypto/tls"
"fmt"
"mayfly-go/internal/machine/mcm"
"mayfly-go/pkg/logx"
@@ -52,6 +53,16 @@ func (d *EsConn) StartProxy() error {
d.proxy = httputil.NewSingleHostReverseProxy(targetURL)
// 设置 proxy buffer pool
d.proxy.BufferPool = NewBufferPool()
// Configure TLS to skip certificate verification for non-compliant certificates
if targetURL.Scheme == "https" {
d.proxy.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
}
return nil
}

View File

@@ -23,6 +23,7 @@ type EsInfo struct {
InstanceId uint64 // 实例id
Name string
Protocol string // 协议默认http
Host string
Port int
Network string
@@ -90,7 +91,14 @@ func (di *EsInfo) Ping() (map[string]any, error) {
// ExecApi 执行api
func (di *EsInfo) ExecApi(method, path string, data any, timeoutSecond ...int) (map[string]any, error) {
request := httpx.NewReq(di.baseUrl + path)
var request *httpx.Req
// Use insecure TLS client for HTTPS connections to handle non-compliant certificates
if di.Protocol == "https" {
request = httpx.NewReqWithInsecureTLS(di.baseUrl + path)
} else {
request = httpx.NewReq(di.baseUrl + path)
}
if di.authorization != "" {
request.Header("Authorization", di.authorization)
}
@@ -117,6 +125,11 @@ func (di *EsInfo) ExecApi(method, path string, data any, timeoutSecond ...int) (
// 如果使用了ssh隧道将其host port改变其本地映射host port
func (di *EsInfo) IfUseSshTunnelChangeIpPort(ctx context.Context) error {
// 设置默认协议
if di.Protocol == "" {
di.Protocol = "http"
}
// 开启ssh隧道
if di.SshTunnelMachineId > 0 {
stm, err := GetSshTunnel(ctx, di.SshTunnelMachineId)
@@ -130,9 +143,9 @@ func (di *EsInfo) IfUseSshTunnelChangeIpPort(ctx context.Context) error {
di.Host = exposedIp
di.Port = exposedPort
di.useSshTunnel = true
di.baseUrl = fmt.Sprintf("http://%s:%d", exposedIp, exposedPort)
di.baseUrl = fmt.Sprintf("%s://%s:%d", di.Protocol, exposedIp, exposedPort)
} else {
di.baseUrl = fmt.Sprintf("http://%s:%d", di.Host, di.Port)
di.baseUrl = fmt.Sprintf("%s://%s:%d", di.Protocol, di.Host, di.Port)
}
return nil
}

View File

@@ -6,9 +6,10 @@ import (
type ProcinstStart struct {
ProcdefId uint64 `json:"procdefId" binding:"required"` // 流程定义id
BizType string `json:"bizType" binding:"required"` // 业务类型
Remark string `json:"remark"` // 流程备注
BizForm collx.M `json:"bizForm" binding:"required"` // 业务表单
BizKey string `json:"bizKey"`
BizType string `json:"bizType" binding:"required"` // 业务类型
Remark string `json:"remark"` // 流程备注
BizForm collx.M `json:"bizForm" binding:"required"` // 业务表单
}
type ProcinstTaskAudit struct {

View File

@@ -50,6 +50,7 @@ func (p *Procinst) ProcinstStart(rc *req.Ctx) {
startForm := req.BindJson[*form.ProcinstStart](rc)
_, err := p.procinstApp.StartProc(rc.MetaCtx, startForm.ProcdefId, &dto.StarProc{
BizType: startForm.BizType,
BizKey: startForm.BizKey,
BizForm: jsonx.ToStr(startForm.BizForm),
Remark: startForm.Remark,
})

View File

@@ -96,7 +96,11 @@ func (p *ProcinstTask) RejectTask(rc *req.Ctx) {
func (p *ProcinstTask) BackTask(rc *req.Ctx) {
auditForm := req.BindJson[*form.ProcinstTaskAudit](rc)
rc.ReqParam = auditForm
biz.ErrIsNil(p.procinstTaskApp.BackTask(rc.MetaCtx, dto.UserTaskOp{TaskId: auditForm.Id, Remark: auditForm.Remark}))
la := rc.GetLoginAccount()
op := dto.UserTaskOp{TaskId: auditForm.Id, Remark: auditForm.Remark, Handler: la.Username}
op.Candidate = p.GetLaCandidates(la)
biz.ErrIsNil(p.procinstTaskApp.BackTask(rc.MetaCtx, op))
}
func (p *ProcinstTask) GetLaCandidates(la *model.LoginAccount) []string {

View File

@@ -14,10 +14,9 @@ type BizHandleParam struct {
// 业务流程处理器(流程状态变更后会根据流程业务类型获取对应的处理器进行回调处理)
type FlowBizHandler interface {
// 业务流程处理函数
// @param bizHandleParam 业务处理信息可获取实例状态、关联业务key等信息
// @return any 返回业务处理结果
// @return error 错误信息
// FlowBizHandle 业务流程处理函数
// bizHandleParam 业务处理信息可获取实例状态、关联业务key等信息
// return any 返回业务处理结果
FlowBizHandle(ctx context.Context, bizHandleParam *BizHandleParam) (any, error)
}
@@ -25,19 +24,28 @@ var (
handlers map[string]FlowBizHandler = make(map[string]FlowBizHandler, 0)
)
// 注册流程业务处理函数
// RegisterBizHandler 注册流程业务处理函数
func RegisterBizHandler(flowBizType string, handler FlowBizHandler) {
logx.Infof("flow register biz handelr: bizType=%s", flowBizType)
handlers[flowBizType] = handler
}
// 流程业务处理
// FlowBizHandle 流程业务处理
func FlowBizHandle(ctx context.Context, bizHandleParam *BizHandleParam) (any, error) {
if bizHandler, err := GetFlowBizHandler(bizHandleParam); err != nil {
return nil, err
} else {
return bizHandler.FlowBizHandle(ctx, bizHandleParam)
}
}
// GetFlowBizHandler 获取流程业务处理函数
func GetFlowBizHandler(bizHandleParam *BizHandleParam) (FlowBizHandler, error) {
flowBizType := bizHandleParam.Procinst.BizType
if handler, ok := handlers[flowBizType]; !ok {
logx.Warnf("flow biz handler not found: bizType=%s", flowBizType)
return nil, errorx.NewBiz("flow biz handler not found")
} else {
return handler.FlowBizHandle(ctx, bizHandleParam)
return handler, nil
}
}

View File

@@ -16,7 +16,7 @@ type SaveFlowDef struct {
// 启动流程实例请求入参
type StarProc struct {
BizType string // 业务类型
BizKey string // 业务key
BizKey string // 业务key,若已存在则为修改重新提交流程
Remark string // 备注
BizForm string // 业务表单信息
}

View File

@@ -43,6 +43,7 @@ func (e *executionAppImpl) Init() {
nodeBehaviorRegistry.Register(&StartNodeBehavior{})
nodeBehaviorRegistry.Register(&EndNodeBehavior{})
nodeBehaviorRegistry.Register(&UserTaskNodeBehavior{})
nodeBehaviorRegistry.Register(&AiTaskNodeBehavior{})
const subId = "ExecutionApp"
@@ -66,15 +67,20 @@ func (e *executionAppImpl) CreateExecution(ctx context.Context, procinst *entity
}
startNode := startNodes[0]
// 创建执行流
execution := &entity.Execution{
ProcinstId: procinst.Id,
ParentId: 0,
NodeKey: startNode.Key,
NodeName: startNode.Name,
NodeType: startNode.Type,
State: entity.ExectionStateActive,
IsConcurrent: -1,
ProcinstId: procinst.Id,
ParentId: 0,
NodeKey: startNode.Key,
NodeName: startNode.Name,
NodeType: startNode.Type,
State: entity.ExectionStateSuspended,
}
if err := e.GetByCond(execution); err == nil {
// 已存在挂起的流程实例,则直接激活继续执行
execution.State = entity.ExectionStateActive
} else {
execution.State = entity.ExectionStateActive
execution.IsConcurrent = -1
}
return e.Tx(ctx, func(ctx context.Context) error {
@@ -124,7 +130,12 @@ func (e *executionAppImpl) MoveTo(ctx *ExecutionCtx, nextNode *entity.FlowNode)
return err
}
return e.executeNode(ctx)
if execution.State == entity.ExectionStateActive {
// 继续推进执行节点
return e.executeNode(ctx)
}
return nil
})
}
@@ -170,5 +181,10 @@ func (e *executionAppImpl) executeNode(ctx *ExecutionCtx) error {
}
// 执行节点逻辑
if node.IsAsync() {
go node.Execute(ctx)
return nil
}
return node.Execute(ctx)
}

View File

@@ -118,6 +118,9 @@ type NodeBehavior interface {
// Leave 离开节点
Leave(*ExecutionCtx) error
// IsAsync 是否异步节点
IsAsync() bool
}
type DefaultNodeBehavior struct {
@@ -136,6 +139,10 @@ func (h *DefaultNodeBehavior) Leave(ctx *ExecutionCtx) error {
return GetExecutionApp().ContinueExecution(ctx)
}
func (h *DefaultNodeBehavior) IsAsync() bool {
return false
}
/******************* 节点注册器 *******************/
type NodeBehaviorRegistry struct {

View File

@@ -0,0 +1,156 @@
package application
import (
"context"
"fmt"
"mayfly-go/internal/ai/agent"
"mayfly-go/internal/ai/prompt"
"mayfly-go/internal/flow/domain/entity"
"mayfly-go/internal/flow/imsg"
"mayfly-go/internal/flow/infra/persistence"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/utils/jsonx"
"time"
"github.com/spf13/cast"
)
/******************* AI任务节点 *******************/
const (
FlowNodeTypeAiTask entity.FlowNodeType = "aitask"
)
// AiTaskNode AI任务节点
type AiTaskNode struct {
entity.FlowNode
AuditRule string `json:"auditRule" form:"auditRule"` // 审批规则
}
// ToUserTaskNode 将标准节点转换成用户任务节点(方便取值)
func ToAiTaskNode(node *entity.FlowNode) *AiTaskNode {
return &AiTaskNode{
FlowNode: *node,
AuditRule: node.GetExtraString("auditRule"),
}
}
type FlowNodeAiTaskApprovalMode string
// AiTaskNodeBehavior Ai任务节点行为处理器
type AiTaskNodeBehavior struct {
DefaultNodeBehavior
}
var _ NodeBehavior = (*AiTaskNodeBehavior)(nil)
func (h *AiTaskNodeBehavior) GetType() entity.FlowNodeType {
return FlowNodeTypeAiTask
}
func (h *AiTaskNodeBehavior) Validate(ctx context.Context, flowDef *entity.FlowDef, node *entity.FlowNode) error {
aitaskNode := ToAiTaskNode(node)
if aitaskNode.AuditRule == "" {
return errorx.NewBizI(ctx, imsg.ErrAiTaskNodeAuditRuleNotEmpty, "name", node.Name)
}
return nil
}
func (h *AiTaskNodeBehavior) IsAsync() bool {
return true
}
func (u *AiTaskNodeBehavior) Execute(ctx *ExecutionCtx) error {
ctx.parent = context.Background() // 该节点为异步操作,需重新赋值父上下文
flowNode := ctx.GetFlowNode()
aitaskNode := ToAiTaskNode(flowNode)
aiagent, err := agent.NewAiAgent(ctx)
if err != nil {
return err
}
auditRule := aitaskNode.AuditRule
sysPrompt := prompt.GetPrompt(prompt.FLOW_BIZ_AUDIT, auditRule)
procinst := ctx.Procinst
now := time.Now()
procinstTask := &entity.ProcinstTask{
ProcinstId: procinst.Id,
ExecutionId: ctx.Execution.Id,
NodeKey: flowNode.Key,
NodeName: flowNode.Name,
NodeType: flowNode.Type,
}
procinstTask.CreateTime = &now
cancelCtx, cancelFunc := context.WithTimeout(context.Background(), 20*time.Second)
defer cancelFunc()
res := aiagent.GetChatMsg(cancelCtx, sysPrompt, procinst.BizForm)
resJson, err := jsonx.ToMap(res)
allowExecute := false
suggestion := ""
if err != nil {
suggestion = fmt.Sprintf("AI agent response parsing to JSON failed: %v, response: %s", err, res)
logx.Error(suggestion)
// return err
} else {
allowExecute = cast.ToBool(resJson["allowExecute"])
suggestion = cast.ToString(resJson["suggestion"])
}
procinstTask.Remark = suggestion
procinstTask.SetEnd()
procinstApp := GetProcinstApp()
executionApp := GetExecutionApp()
procinstTaskApp := GetProcinstTaskApp()
return procinstTaskApp.Tx(ctx, func(c context.Context) error {
if !allowExecute {
// 流程实例退回
procinst.Status = entity.ProcinstStatusBack
ctx.OpExtra.Set("approvalResult", entity.ProcinstTaskStatusBack)
procinstTask.Status = entity.ProcinstTaskStatusBack
if err := procinstApp.Save(c, procinst); err != nil {
return err
}
} else {
ctx.OpExtra.Set("approvalResult", entity.ProcinstTaskStatusCompleted)
procinstTask.Status = entity.ProcinstTaskStatusCompleted
}
// 保存任务与任务候选者信息兼容usertask展示
if err := procinstTaskApp.Save(c, procinstTask); err != nil {
return err
}
handler := "AI"
procinstTaskCandidate := &entity.ProcinstTaskCandidate{
TaskId: procinstTask.Id,
ProcinstId: procinst.Id,
Candidate: handler,
Handler: &handler,
Status: procinstTask.Status,
}
procinstTaskCandidate.CreateTime = &now
procinstTaskCandidate.SetEnd()
if err := persistence.GetProcinstTaskCandidateRepo().Save(c, procinstTaskCandidate); err != nil {
return err
}
if !allowExecute {
// 跳转至开始节点,重新修改提交
ctx.Execution.State = entity.ExectionStateSuspended // 执行流挂起
return executionApp.MoveTo(ctx, ctx.GetFlowDef().GetNodeByType(FlowNodeTypeStart)[0])
}
return u.Leave(ctx)
})
}

View File

@@ -57,13 +57,8 @@ func (p *procinstAppImpl) StartProc(ctx context.Context, procdefId uint64, reqPa
return nil, errorx.NewBizI(ctx, imsg.ErrProcdefFlowNotExist)
}
bizKey := reqParam.BizKey
if bizKey == "" {
bizKey = stringx.RandUUID()
}
procinst := &entity.Procinst{
BizType: reqParam.BizType,
BizKey: bizKey,
BizForm: reqParam.BizForm,
BizStatus: entity.ProcinstBizStatusWait,
ProcdefId: procdef.Id,
@@ -73,6 +68,24 @@ func (p *procinstAppImpl) StartProc(ctx context.Context, procdefId uint64, reqPa
FlowDef: procdef.FlowDef,
}
bizKey := reqParam.BizKey
if bizKey == "" {
bizKey = stringx.RandUUID()
} else {
// 若业务key已存在则为修改重新提交流程
existProcinst := &entity.Procinst{BizKey: bizKey}
if err := p.GetByCond(existProcinst); err == nil {
if existProcinst.Status != entity.ProcinstStatusBack {
return nil, errorx.NewBiz("该工单非退回状态,无法修改")
}
if existProcinst.CreatorId != contextx.GetLoginAccount(ctx).Id {
return nil, errorx.NewBiz("该工单非当前用户创建,无法修改")
}
procinst.Id = existProcinst.Id
}
}
procinst.BizKey = bizKey
return procinst, p.Tx(ctx, func(ctx context.Context) error {
if err := p.Save(ctx, procinst); err != nil {
return err

View File

@@ -116,28 +116,54 @@ func (p *procinstTaskAppImpl) RejectTask(ctx context.Context, taskOp dto.UserTas
}
func (p *procinstTaskAppImpl) BackTask(ctx context.Context, taskOp dto.UserTaskOp) error {
// instTask, err := p.getAndValidInstTask(ctx, instTaskId)
// if err != nil {
// return err
// }
taskId := taskOp.TaskId
instTask, taskCandidates, procinst, execution, err := p.getAndValidInstTask(ctx, taskId, taskOp.Candidate)
if err != nil {
return err
}
// // 赋值状态和备注
// instTask.Status = entity.ProcinstTaskStatusBack
// instTask.Remark = remark
// 赋值状态和备注
instTask.Status = entity.ProcinstTaskStatusBack
instTask.Remark = taskOp.Remark
// procinst, _ := p.GetById(instTask.ProcinstId)
// 更新流程实例为退回状态,支持重新提交
procinst.Status = entity.ProcinstStatusBack
// // 更新流程实例为挂起状态,等待重新提交
// procinst.Status = entity.ProcinstStatusSuspended
// 执行流挂起
execution.State = entity.ExectionStateSuspended
// return p.Tx(ctx, func(ctx context.Context) error {
// return p.UpdateById(ctx, procinst)
// }, func(ctx context.Context) error {
// return p.procinstTaskRepo.UpdateById(ctx, instTask)
// }, func(ctx context.Context) error {
// return p.triggerProcinstStatusChangeEvent(ctx, procinst)
// })
return nil
procinstId := procinst.Id
return p.Tx(ctx, func(ctx context.Context) error {
executionCtx := NewExecutionCtx(ctx, procinst, execution)
executionCtx.OpExtra.Set("approvalResult", instTask.Status)
for _, taskCandidate := range taskCandidates {
taskCandidate.Status = entity.ProcinstTaskStatusBack
taskCandidate.SetEnd()
taskCandidate.Handler = &taskOp.Handler
if err := p.procinstTaskCandidateRepo.UpdateById(ctx, taskCandidate); err != nil {
return err
}
}
if err := p.procinstApp.Save(ctx, procinst); err != nil {
return err
}
if err := p.Save(ctx, instTask); err != nil {
return err
}
if err := p.UpdateByCond(ctx, &entity.ProcinstTask{Status: entity.ProcinstTaskStatusCanceled}, &entity.ProcinstTask{ProcinstId: procinstId, Status: entity.ProcinstTaskStatusProcess}); err != nil {
return err
}
// 跳转至开始节点
if err := p.executionApp.MoveTo(executionCtx, executionCtx.GetFlowDef().GetNodeByType(FlowNodeTypeStart)[0]); err != nil {
return err
}
// 删除待处理的其他候选人任务
return p.procinstTaskCandidateRepo.DeleteByCond(ctx, &entity.ProcinstTaskCandidate{ProcinstId: procinstId, Status: entity.ProcinstTaskStatusProcess})
})
}
func (p *procinstTaskAppImpl) CompleteTask(ctx context.Context, taskOp dto.UserTaskOp) error {

View File

@@ -55,17 +55,19 @@ func (p *Procinst) GetFlowDef() *FlowDef {
type ProcinstStatus int8
const (
ProcinstStatusActive ProcinstStatus = 1 // 流程实例正在执行中,当前有活动任务等待执行或者正在运行的流程节点
ProcinstStatusCompleted ProcinstStatus = 2 // 流程实例已经成功执行完成,没有剩余任务或者等待事件
ProcinstStatusSuspended ProcinstStatus = -1 // 流程实例被挂起,暂停执行,可能被驳回等待修改重新提交
ProcinstStatusTerminated ProcinstStatus = -2 // 流程实例被终止,可能是由于某种原因如被拒绝等导致流程无法正常执行
ProcinstStatusCancelled ProcinstStatus = -3 // 流程实例被取消,通常是用户手动操作取消了流程的执行
ProcinstStatusActive ProcinstStatus = 1 // 流程实例正在执行中,当前有活动任务等待执行或者正在运行的流程节点
ProcinstStatusCompleted ProcinstStatus = 2 // 流程实例已经成功执行完成,没有剩余任务或者等待事件
ProcinstStatusSuspended ProcinstStatus = -1 // 流程实例被挂起,暂停执行,可能被驳回等待修改重新提交
ProcinstStatusBack ProcinstStatus = -11 // 流程实例被驳回,等待重新提交或修改
ProcinstStatusTerminated ProcinstStatus = -2 // 流程实例被终止,可能是由于某种原因如被拒绝等导致流程无法正常执行
ProcinstStatusCancelled ProcinstStatus = -3 // 流程实例被取消,通常是用户手动操作取消了流程的执行
)
var ProcinstStatusEnum = enumx.NewEnum[ProcinstStatus]("流程状态").
Add(ProcinstStatusActive, "执行中").
Add(ProcinstStatusCompleted, "完成").
Add(ProcinstStatusSuspended, "挂起").
Add(ProcinstStatusBack, "退回").
Add(ProcinstStatusTerminated, "终止").
Add(ProcinstStatusCancelled, "取消")

View File

@@ -24,4 +24,6 @@ var En = map[i18n.MsgId]string{
ErrProcinstCancelSelf: "You can only cancel processes you initiated",
ErrProcinstCancelled: "Process has been cancelled",
ErrBizHandlerFail: "Business process failure",
ErrAiTaskNodeAuditRuleNotEmpty: "The audit rule of the AI task node [{{.name}}] cannot be empty",
}

View File

@@ -32,4 +32,6 @@ const (
ErrProcinstCancelSelf
ErrProcinstCancelled
ErrBizHandlerFail
ErrAiTaskNodeAuditRuleNotEmpty
)

View File

@@ -24,4 +24,6 @@ var Zh_CN = map[i18n.MsgId]string{
ErrProcinstCancelSelf: "只能取消自己发起的流程",
ErrProcinstCancelled: "流程已取消",
ErrBizHandlerFail: "业务处理失败",
ErrAiTaskNodeAuditRuleNotEmpty: "Ai任务节点 [{{.name}}] 的审核规则不能为空",
}

View File

@@ -195,16 +195,16 @@ func (m *msgTmplAppImpl) SendMsg(ctx context.Context, mts *dto.MsgTmplSend) erro
continue
}
go func(channel *entity.MsgChannel) {
go func(ch entity.MsgChannel) {
if err := msgx.Send(ctx, &msgx.Channel{
Type: channel.Type,
Name: channel.Name,
URL: channel.Url,
Extra: channel.Extra,
Type: ch.Type,
Name: ch.Name,
URL: ch.Url,
Extra: ch.Extra,
}, msg); err != nil {
logx.Errorf("send msg error => channel=%s, msg=%s, err -> %v", channel.Code, msg.Content, err)
logx.Errorf("send msg error => channel=%s, msg=%s, err -> %v", ch.Code, msg.Content, err)
}
}(channel)
}(*channel)
}
return nil

View File

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

View File

@@ -16,11 +16,11 @@ func initCache() {
redisCli := connRedis()
if redisCli == nil {
logx.Info("no redis configuration exists, local cache is used")
logx.Info("no redis configuration, using local cache")
return
}
logx.Info("redis connection is successful, redis cache is used")
logx.Info("redis connected successfully, using Redis for caching")
rediscli.SetCli(connRedis())
cache.SetCache(cache.NewRedisCache(redisCli))
}

View File

@@ -58,6 +58,8 @@ func runWebServer(ctx context.Context) {
router.Use(middleware.Cors())
}
router.Use(gin.Recovery())
srv := http.Server{
Addr: config.Conf.Server.GetPort(),
// 注册路由

View File

@@ -1,6 +1,7 @@
package migrations
import (
"errors"
dockerentity "mayfly-go/internal/docker/domain/entity"
esentity "mayfly-go/internal/es/domain/entity"
flowentity "mayfly-go/internal/flow/domain/entity"
@@ -20,6 +21,8 @@ func V1_10() []*gormigrate.Migration {
migrations = append(migrations, V1_10_1()...)
migrations = append(migrations, V1_10_2()...)
migrations = append(migrations, V1_10_3()...)
migrations = append(migrations, V1_10_4()...)
migrations = append(migrations, V1_10_5()...)
return migrations
}
@@ -326,3 +329,65 @@ func V1_10_3() []*gormigrate.Migration {
},
}
}
func V1_10_4() []*gormigrate.Migration {
return []*gormigrate.Migration{
{
ID: "20251023-v1.10.4",
Migrate: func(tx *gorm.DB) error {
// 给EsInstance表添加protocol列默认值为http, 20251023,fudawei
if !tx.Migrator().HasColumn(&esentity.EsInstance{}, "protocol") {
// 先添加可为空的列
if err := tx.Exec("ALTER TABLE t_es_instance ADD COLUMN protocol VARCHAR(10) DEFAULT 'http'").Error; err != nil {
return err
}
// 更新所有现有记录为默认值http
if err := tx.Exec("UPDATE t_es_instance SET protocol = 'http' WHERE protocol IS NULL OR protocol = ''").Error; err != nil {
return err
}
}
return nil
},
Rollback: func(tx *gorm.DB) error {
return nil
},
},
}
}
func V1_10_5() []*gormigrate.Migration {
return []*gormigrate.Migration{
{
ID: "20251207-v1.10.5",
Migrate: func(tx *gorm.DB) error {
config := &sysentity.Config{}
// 查询是否存在该配置
result := tx.Model(config).Where("key = ?", "AiModelConfig").First(config)
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
// 如果不存在,则创建默认配置
now := time.Now()
aiConfig := &sysentity.Config{
Key: "AiModelConfig",
Name: "system.sysconf.aiModelConf'",
Value: "{}", // 默认空JSON值
Params: `[{"model":"modelType","name":"system.sysconf.aiModelType","placeholder":"system.sysconf.aiModelTypePlaceholder","options":"openai"},{"model":"model","name":"system.sysconf.aiModel","placeholder":"system.sysconf.aiModelPlaceholder"},{"model":"baseUrl","name":"system.sysconf.aiBaseUrl","placeholder":"system.sysconf.aiBaseUrlPlaceholder"},{"model":"apiKey","name":"ApiKey","placeholder":"api key"}]`,
Permission: "all",
}
aiConfig.CreateTime = &now
aiConfig.Modifier = "admin"
aiConfig.ModifierId = 1
aiConfig.UpdateTime = &now
aiConfig.Creator = "admin"
aiConfig.CreatorId = 1
aiConfig.IsDeleted = 0
return tx.Create(aiConfig).Error
}
return nil
},
Rollback: func(tx *gorm.DB) error {
return nil
},
},
}
}

View File

@@ -2,6 +2,7 @@ package httpx
import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
@@ -41,6 +42,16 @@ func NewReq(url string) *Req {
return &Req{url: url, client: http.Client{}}
}
// 创建一个请求(不验证TLS证书)
func NewReqWithInsecureTLS(url string) *Req {
transport := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
return &Req{url: url, client: http.Client{Transport: transport}}
}
func (r *Req) Url(url string) *Req {
r.url = url
return r

View File

@@ -1,6 +1,7 @@
package scheduler
import (
"errors"
"mayfly-go/pkg/logx"
"sync"
@@ -51,6 +52,9 @@ func AddFun(spec string, cmd func()) (cron.EntryID, error) {
// AddFunByKey 根据key添加定时任务
func AddFunByKey(key, spec string, cmd func()) error {
logx.Debugf("add cron func => [key = %s]", key)
if key == "" {
return errors.New("scheduler key cannot be empty")
}
RemoveByKey(key)
id, err := AddFun(spec, cmd)
if err != nil {

View File

@@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"encoding/json"
"strings"
"time"
)
@@ -20,9 +21,11 @@ func DefaultFormatDate(time time.Time) string {
return time.Format(DefaultDateFormat)
}
// TimeNo 获取当前时间编号格式为20060102150405
// TimeNo 获取当前时间编号(毫秒)格式为20060102150405000
func TimeNo() string {
return time.Now().Format("20060102150405")
formatted := time.Now().Format("20060102150405.000")
// 移除小数点
return strings.Replace(formatted, ".", "", 1)
}
func NewNullTime(t time.Time) NullTime {

View File

@@ -1,9 +1,10 @@
package timex
import (
"github.com/stretchr/testify/require"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestNullTime_UnmarshalJSON(t *testing.T) {

View File

@@ -9,28 +9,60 @@ import (
// 心跳间隔
const heartbeatInterval = 25 * time.Second
// 单个用户的全部的连接, key->clientId, value->Client
type UserClients map[string]*Client
func (ucs UserClients) GetByCid(clientId string) *Client {
return ucs[clientId]
// UserClient 用户全部的连接
type UserClient struct {
clients map[string]*Client // key->clientId, value->Client
mutex sync.RWMutex
}
func (ucs UserClients) AddClient(client *Client) {
ucs[client.ClientId] = client
func NewUserClient() *UserClient {
return &UserClient{
clients: make(map[string]*Client),
}
}
func (ucs UserClients) DeleteByCid(clientId string) {
delete(ucs, clientId)
// AllClients 获取全部的连接
func (ucs *UserClient) AllClients() []*Client {
ucs.mutex.RLock()
defer ucs.mutex.RUnlock()
result := make([]*Client, 0, len(ucs.clients))
for _, client := range ucs.clients {
result = append(result, client)
}
return result
}
func (ucs UserClients) Count() int {
return len(ucs)
// GetByCid 获取指定客户端ID的客户端
func (ucs *UserClient) GetByCid(clientId string) *Client {
ucs.mutex.RLock()
defer ucs.mutex.RUnlock()
return ucs.clients[clientId]
}
// AddClient 添加客户端
func (ucs *UserClient) AddClient(client *Client) {
ucs.mutex.Lock()
defer ucs.mutex.Unlock()
ucs.clients[client.ClientId] = client
}
// DeleteByCid 删除指定客户端ID的客户端
func (ucs *UserClient) DeleteByCid(clientId string) {
ucs.mutex.Lock()
defer ucs.mutex.Unlock()
delete(ucs.clients, clientId)
}
// Count 返回客户端数量
func (ucs *UserClient) Count() int {
ucs.mutex.RLock()
defer ucs.mutex.RUnlock()
return len(ucs.clients)
}
// 连接管理
type ClientManager struct {
UserClientsMap sync.Map // 全部的用户连接, key->userid, value->UserClients
UserClientMap sync.Map // 全部的用户连接, key->userid, value->*UserClient
ConnectChan chan *Client // 连接处理
DisConnectChan chan *Client // 断开连接处理
@@ -39,7 +71,7 @@ type ClientManager struct {
func NewClientManager() (clientManager *ClientManager) {
return &ClientManager{
UserClientsMap: sync.Map{},
UserClientMap: sync.Map{},
ConnectChan: make(chan *Client, 10),
DisConnectChan: make(chan *Client, 10),
MsgChan: make(chan *Msg, 100),
@@ -77,18 +109,18 @@ func (manager *ClientManager) CloseClient(client *Client) {
// 根据用户id关闭客户端连接
func (manager *ClientManager) CloseByUid(userId UserId) {
userClients := manager.GetByUid(userId)
for _, client := range userClients {
userClient := manager.GetByUid(userId)
for _, client := range userClient.AllClients() {
manager.CloseClient(client)
}
}
// 获取所有的客户端
func (manager *ClientManager) AllUserClient() map[UserId]UserClients {
result := make(map[UserId]UserClients)
manager.UserClientsMap.Range(func(key, value any) bool {
func (manager *ClientManager) AllUserClient() map[UserId]*UserClient {
result := make(map[UserId]*UserClient)
manager.UserClientMap.Range(func(key, value any) bool {
userId := key.(UserId)
userClients := value.(UserClients)
userClients := value.(*UserClient)
result[userId] = userClients
return true
})
@@ -96,9 +128,9 @@ func (manager *ClientManager) AllUserClient() map[UserId]UserClients {
}
// 通过userId获取用户所有客户端信息
func (manager *ClientManager) GetByUid(userId UserId) UserClients {
if value, ok := manager.UserClientsMap.Load(userId); ok {
return value.(UserClients)
func (manager *ClientManager) GetByUid(userId UserId) *UserClient {
if value, ok := manager.UserClientMap.Load(userId); ok {
return value.(*UserClient)
}
return nil
}
@@ -114,7 +146,7 @@ func (manager *ClientManager) GetByUidAndCid(uid UserId, clientId string) *Clien
// 客户端数量
func (manager *ClientManager) Count() int {
count := 0
manager.UserClientsMap.Range(func(key, value any) bool {
manager.UserClientMap.Range(func(key, value any) bool {
count++
return true
})
@@ -148,9 +180,11 @@ func (manager *ClientManager) WriteMessage() {
// cid为空则向该用户所有客户端发送该消息
userClients := manager.GetByUid(uid)
for _, cli := range userClients {
if err := cli.WriteMsg(msg); err != nil {
logx.Warnf("ws send message failed - [uid=%d, cid=%s]: %s", uid, cli.ClientId, err.Error())
if userClients != nil {
for _, cli := range userClients.AllClients() {
if err := cli.WriteMsg(msg); err != nil {
logx.Warnf("ws send message failed - [uid=%d, cid=%s]: %s", uid, cli.ClientId, err.Error())
}
}
}
}
@@ -165,10 +199,10 @@ func (manager *ClientManager) HeartbeatTimer() {
for {
<-ticker.C
//发送心跳
manager.UserClientsMap.Range(func(key, value any) bool {
manager.UserClientMap.Range(func(key, value any) bool {
userId := key.(UserId)
clis := value.(UserClients)
for _, cli := range clis {
userClient := value.(*UserClient)
for _, cli := range userClient.AllClients() {
if cli == nil || cli.WsConn == nil {
continue
}
@@ -208,24 +242,24 @@ func (manager *ClientManager) doDisconnect(client *Client) {
func (manager *ClientManager) addUserClient2Map(client *Client) {
// 先尝试加载现有的UserClients
if value, ok := manager.UserClientsMap.Load(client.UserId); ok {
userClients := value.(UserClients)
userClients.AddClient(client)
if value, ok := manager.UserClientMap.Load(client.UserId); ok {
userClient := value.(*UserClient)
userClient.AddClient(client)
} else {
// 创建新的UserClients
userClients := make(UserClients)
userClients.AddClient(client)
manager.UserClientsMap.Store(client.UserId, userClients)
userClient := NewUserClient()
userClient.AddClient(client)
manager.UserClientMap.Store(client.UserId, userClient)
}
}
func (manager *ClientManager) delUserClient4Map(client *Client) {
if value, ok := manager.UserClientsMap.Load(client.UserId); ok {
userClients := value.(UserClients)
userClients.DeleteByCid(client.ClientId)
if value, ok := manager.UserClientMap.Load(client.UserId); ok {
userClient := value.(*UserClient)
userClient.DeleteByCid(client.ClientId)
// 如果用户所有客户端都关闭则移除manager中的UserClientsMap值
if userClients.Count() == 0 {
manager.UserClientsMap.Delete(client.UserId)
if userClient.Count() == 0 {
manager.UserClientMap.Delete(client.UserId)
}
}
}