mirror of
				https://gitee.com/dromara/mayfly-go
				synced 2025-11-04 08:20:25 +08:00 
			
		
		
		
	feat: 新增sql脚本执行及其他优化
This commit is contained in:
		@@ -71,8 +71,8 @@ export default defineComponent({
 | 
			
		||||
        const state = reactive({
 | 
			
		||||
            captchaImage: '',
 | 
			
		||||
            loginForm: {
 | 
			
		||||
                username: 'test',
 | 
			
		||||
                password: '123456',
 | 
			
		||||
                username: '',
 | 
			
		||||
                password: '',
 | 
			
		||||
                captcha: '',
 | 
			
		||||
                cid: '',
 | 
			
		||||
            },
 | 
			
		||||
 
 | 
			
		||||
@@ -31,6 +31,24 @@
 | 
			
		||||
 | 
			
		||||
                        <el-button v-waves @click="saveSql" type="primary" icon="el-icon-document-add" size="mini" plain>保存</el-button>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div style="float: right" class="fl">
 | 
			
		||||
                        <el-upload
 | 
			
		||||
                            :before-upload="beforeUpload"
 | 
			
		||||
                            :on-success="execSqlFileSuccess"
 | 
			
		||||
                            :headers="{ Authorization: token }"
 | 
			
		||||
                            :data="{
 | 
			
		||||
                                dbId: 1,
 | 
			
		||||
                            }"
 | 
			
		||||
                            :action="getUploadSqlFileUrl()"
 | 
			
		||||
                            :show-file-list="false"
 | 
			
		||||
                            name="file"
 | 
			
		||||
                            multiple
 | 
			
		||||
                            :limit="100"
 | 
			
		||||
                        >
 | 
			
		||||
                            <el-button class="fr" v-waves type="success" icon="el-icon-video-play" size="mini" plain>sql脚本执行</el-button>
 | 
			
		||||
                        </el-upload>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <codemirror @beforeChange="onBeforeChange" class="codesql" ref="cmEditor" language="sql" v-model="sql" :options="cmOptions" />
 | 
			
		||||
            </el-aside>
 | 
			
		||||
@@ -95,6 +113,8 @@ import sqlFormatter from 'sql-formatter';
 | 
			
		||||
import { notNull, notEmpty } from '@/common/assert';
 | 
			
		||||
import { ElMessage } from 'element-plus';
 | 
			
		||||
import ProjectEnvSelect from '../component/ProjectEnvSelect.vue';
 | 
			
		||||
import config from '@/common/config';
 | 
			
		||||
import { getSession } from '@/common/utils/storage';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
    name: 'SqlExec',
 | 
			
		||||
@@ -104,7 +124,10 @@ export default defineComponent({
 | 
			
		||||
    },
 | 
			
		||||
    setup() {
 | 
			
		||||
        const cmEditor: any = ref(null);
 | 
			
		||||
        const token = getSession('token');
 | 
			
		||||
 | 
			
		||||
        const state = reactive({
 | 
			
		||||
            token: token,
 | 
			
		||||
            dbs: [],
 | 
			
		||||
            tables: [],
 | 
			
		||||
            dbId: null,
 | 
			
		||||
@@ -201,6 +224,26 @@ export default defineComponent({
 | 
			
		||||
            state.execRes.data = res.res;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const beforeUpload = (file: File) => {
 | 
			
		||||
            if (!state.dbId) {
 | 
			
		||||
                ElMessage.error('请先选择数据库');
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            ElMessage.success(`'${file.name}' 正在上传执行, 请关注结果通知`);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // 执行sql成功
 | 
			
		||||
        const execSqlFileSuccess = (res: any) => {
 | 
			
		||||
            if (res.code !== 200) {
 | 
			
		||||
                ElMessage.error(res.msg);
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // 获取sql文件上传执行url
 | 
			
		||||
        const getUploadSqlFileUrl = () => {
 | 
			
		||||
            return `${config.baseApiUrl}/dbs/${state.dbId}/exec-sql-file`;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const flexColumnWidth = (str: any, tableData: any, flag = 'equal') => {
 | 
			
		||||
            // str为该列的字段名(传字符串);tableData为该表格的数据源(传变量);
 | 
			
		||||
            // flag为可选值,可不传该参数,传参时可选'max'或'equal',默认为'max'
 | 
			
		||||
@@ -244,10 +287,10 @@ export default defineComponent({
 | 
			
		||||
                    flexWidth += 8;
 | 
			
		||||
                } else if (char >= '\u4e00' && char <= '\u9fa5') {
 | 
			
		||||
                    // 如果是中文字符,为字符分配15个单位宽度
 | 
			
		||||
                    flexWidth += 15;
 | 
			
		||||
                    flexWidth += 16;
 | 
			
		||||
                } else {
 | 
			
		||||
                    // 其他种类字符,为字符分配8个单位宽度
 | 
			
		||||
                    flexWidth += 8;
 | 
			
		||||
                    // 其他种类字符,为字符分配10个单位宽度
 | 
			
		||||
                    flexWidth += 10;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if (flexWidth < 80) {
 | 
			
		||||
@@ -367,6 +410,9 @@ export default defineComponent({
 | 
			
		||||
            inputRead,
 | 
			
		||||
            changeTable,
 | 
			
		||||
            runSql,
 | 
			
		||||
            beforeUpload,
 | 
			
		||||
            getUploadSqlFileUrl,
 | 
			
		||||
            execSqlFileSuccess,
 | 
			
		||||
            flexColumnWidth,
 | 
			
		||||
            saveSql,
 | 
			
		||||
            changeDb,
 | 
			
		||||
 
 | 
			
		||||
@@ -219,13 +219,31 @@ export default defineComponent({
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (script.type == enums.scriptTypeEnum['REAL_TIME'].value) {
 | 
			
		||||
                state.terminalDialog.cmd = script.script;
 | 
			
		||||
                script = script.script
 | 
			
		||||
                if (state.scriptParamsDialog.params) {
 | 
			
		||||
                    script = templateResolve(script, state.scriptParamsDialog.params)
 | 
			
		||||
                }
 | 
			
		||||
                state.terminalDialog.cmd = script;
 | 
			
		||||
                state.terminalDialog.visible = true;
 | 
			
		||||
                state.terminalDialog.machineId = props.machineId;
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * 解析 {{.param}} 形式模板字符串
 | 
			
		||||
         */
 | 
			
		||||
        function templateResolve(template: string, param: any) {
 | 
			
		||||
            return template.replace(/\{{.\w+\}}/g, (word) => {
 | 
			
		||||
                const key = word.substring(3, word.length - 2);
 | 
			
		||||
                const value = param[key];
 | 
			
		||||
                if (value != null || value != undefined) {
 | 
			
		||||
                    return value;
 | 
			
		||||
                }
 | 
			
		||||
                return '';
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const closeTermnial = () => {
 | 
			
		||||
            state.terminalDialog.visible = false;
 | 
			
		||||
            state.terminalDialog.machineId = 0;
 | 
			
		||||
 
 | 
			
		||||
@@ -50,11 +50,11 @@
 | 
			
		||||
                <el-card shadow="hover">
 | 
			
		||||
                    <template #header>
 | 
			
		||||
                        <span>消息通知</span>
 | 
			
		||||
                        <span class="personal-info-more">更多</span>
 | 
			
		||||
                        <span @click="showMsgs" class="personal-info-more">更多</span>
 | 
			
		||||
                    </template>
 | 
			
		||||
                    <div class="personal-info-box">
 | 
			
		||||
                        <ul class="personal-info-ul">
 | 
			
		||||
                            <li v-for="(v, k) in msgs" :key="k" class="personal-info-li">
 | 
			
		||||
                            <li v-for="(v, k) in msgDialog.msgs.list" :key="k" class="personal-info-li">
 | 
			
		||||
                                <a class="personal-info-li-title">{{ `[${getMsgTypeDesc(v.type)}] ${v.msg}` }}</a>
 | 
			
		||||
                            </li>
 | 
			
		||||
                        </ul>
 | 
			
		||||
@@ -62,6 +62,31 @@
 | 
			
		||||
                </el-card>
 | 
			
		||||
            </el-col>
 | 
			
		||||
 | 
			
		||||
            <el-dialog width="900px" title="消息" v-model="msgDialog.visible">
 | 
			
		||||
                <el-table border :data="msgDialog.msgs.list" size="small">
 | 
			
		||||
                    <el-table-column property="type" label="类型" width="60">
 | 
			
		||||
                        <template #default="scope">
 | 
			
		||||
                            {{ getMsgTypeDesc(scope.row.type) }}
 | 
			
		||||
                        </template>
 | 
			
		||||
                    </el-table-column>
 | 
			
		||||
                    <el-table-column property="msg" label="消息"></el-table-column>
 | 
			
		||||
                    <el-table-column property="createTime" label="时间" width="150">
 | 
			
		||||
                        <template #default="scope">
 | 
			
		||||
                            {{ $filters.dateFormat(scope.row.createTime) }}
 | 
			
		||||
                        </template>
 | 
			
		||||
                    </el-table-column>
 | 
			
		||||
                </el-table>
 | 
			
		||||
                <el-pagination
 | 
			
		||||
                    @current-change="getMsgs"
 | 
			
		||||
                    style="text-align: center"
 | 
			
		||||
                    background
 | 
			
		||||
                    layout="prev, pager, next, total, jumper"
 | 
			
		||||
                    :total="msgDialog.msgs.total"
 | 
			
		||||
                    v-model:current-page="msgDialog.query.pageNum"
 | 
			
		||||
                    :page-size="msgDialog.query.pageSize"
 | 
			
		||||
                />
 | 
			
		||||
            </el-dialog>
 | 
			
		||||
 | 
			
		||||
            <!-- 营销推荐 -->
 | 
			
		||||
            <!-- <el-col :span="24">
 | 
			
		||||
                <el-card shadow="hover" class="mt15" header="营销推荐">
 | 
			
		||||
@@ -172,6 +197,17 @@ export default {
 | 
			
		||||
                roles: [],
 | 
			
		||||
            },
 | 
			
		||||
            msgs: [],
 | 
			
		||||
            msgDialog: {
 | 
			
		||||
                visible: false,
 | 
			
		||||
                query: {
 | 
			
		||||
                    pageSize: 10,
 | 
			
		||||
                    pageNum: 1,
 | 
			
		||||
                },
 | 
			
		||||
                msgs: {
 | 
			
		||||
                    list: [],
 | 
			
		||||
                    total: null,
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            recommendList,
 | 
			
		||||
            accountForm: {
 | 
			
		||||
                password: '',
 | 
			
		||||
@@ -187,6 +223,10 @@ export default {
 | 
			
		||||
            return store.state.userInfos.userInfos;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const showMsgs = () => {
 | 
			
		||||
            state.msgDialog.visible = true;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const roleInfo = computed(() => {
 | 
			
		||||
            if (state.accountInfo.roles.length == 0) {
 | 
			
		||||
                return '';
 | 
			
		||||
@@ -209,8 +249,8 @@ export default {
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const getMsgs = async () => {
 | 
			
		||||
            const res = await personApi.getMsgs.request();
 | 
			
		||||
            state.msgs = res.list;
 | 
			
		||||
            const res = await personApi.getMsgs.request(state.msgDialog.query);
 | 
			
		||||
            state.msgDialog.msgs = res;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const getMsgTypeDesc = (type: number) => {
 | 
			
		||||
@@ -226,6 +266,7 @@ export default {
 | 
			
		||||
            getUserInfos,
 | 
			
		||||
            currentTime,
 | 
			
		||||
            roleInfo,
 | 
			
		||||
            showMsgs,
 | 
			
		||||
            getAccountInfo,
 | 
			
		||||
            getMsgs,
 | 
			
		||||
            getMsgTypeDesc,
 | 
			
		||||
 
 | 
			
		||||
@@ -2,15 +2,18 @@ package api
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"mayfly-go/base/biz"
 | 
			
		||||
	"mayfly-go/base/ctx"
 | 
			
		||||
	"mayfly-go/base/ginx"
 | 
			
		||||
	"mayfly-go/base/model"
 | 
			
		||||
	"mayfly-go/base/utils"
 | 
			
		||||
	"mayfly-go/base/ws"
 | 
			
		||||
	"mayfly-go/server/devops/api/form"
 | 
			
		||||
	"mayfly-go/server/devops/api/vo"
 | 
			
		||||
	"mayfly-go/server/devops/application"
 | 
			
		||||
	"mayfly-go/server/devops/domain/entity"
 | 
			
		||||
	sysApplication "mayfly-go/server/sys/application"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
@@ -18,7 +21,8 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Db struct {
 | 
			
		||||
	DbApp application.Db
 | 
			
		||||
	DbApp  application.Db
 | 
			
		||||
	MsgApp sysApplication.Msg
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// @router /api/dbs [get]
 | 
			
		||||
@@ -77,18 +81,14 @@ func (d *Db) ExecSql(rc *ctx.ReqCtx) {
 | 
			
		||||
	biz.NotEmpty(sql, "sql不能为空")
 | 
			
		||||
	if strings.HasPrefix(sql, "SELECT") || strings.HasPrefix(sql, "select") {
 | 
			
		||||
		colNames, res, err := d.DbApp.GetDbInstance(GetDbId(g)).SelectData(sql)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			panic(biz.NewBizErr(fmt.Sprintf("查询失败: %s", err.Error())))
 | 
			
		||||
		}
 | 
			
		||||
		biz.ErrIsNilAppendErr(err, "查询失败: %s")
 | 
			
		||||
		colAndRes := make(map[string]interface{})
 | 
			
		||||
		colAndRes["colNames"] = colNames
 | 
			
		||||
		colAndRes["res"] = res
 | 
			
		||||
		rc.ResData = colAndRes
 | 
			
		||||
	} else {
 | 
			
		||||
		rowsAffected, err := d.DbApp.GetDbInstance(GetDbId(g)).Exec(sql)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			panic(biz.NewBizErr(fmt.Sprintf("执行失败: %s", err.Error())))
 | 
			
		||||
		}
 | 
			
		||||
		biz.ErrIsNilAppendErr(err, "执行失败: %s")
 | 
			
		||||
		res := make([]map[string]string, 0)
 | 
			
		||||
		resData := make(map[string]string)
 | 
			
		||||
		resData["影响条数"] = fmt.Sprintf("%d", rowsAffected)
 | 
			
		||||
@@ -102,6 +102,37 @@ func (d *Db) ExecSql(rc *ctx.ReqCtx) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 执行sql文件
 | 
			
		||||
func (d *Db) ExecSqlFile(rc *ctx.ReqCtx) {
 | 
			
		||||
	g := rc.GinCtx
 | 
			
		||||
	fileheader, err := g.FormFile("file")
 | 
			
		||||
	biz.ErrIsNilAppendErr(err, "读取sql文件失败: %s")
 | 
			
		||||
 | 
			
		||||
	// 读取sql文件并根据;切割sql语句
 | 
			
		||||
	file, _ := fileheader.Open()
 | 
			
		||||
	filename := fileheader.Filename
 | 
			
		||||
	bytes, _ := ioutil.ReadAll(file)
 | 
			
		||||
	sqlContent := string(bytes)
 | 
			
		||||
	sqls := strings.Split(sqlContent, ";")
 | 
			
		||||
	dbId := GetDbId(g)
 | 
			
		||||
 | 
			
		||||
	go func() {
 | 
			
		||||
		db := d.DbApp.GetDbInstance(dbId)
 | 
			
		||||
		for _, sql := range sqls {
 | 
			
		||||
			sql = strings.Trim(sql, " ")
 | 
			
		||||
			if sql == "" || sql == "\n" {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			_, err := db.Exec(sql)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				d.MsgApp.CreateAndSend(rc.LoginAccount, ws.ErrMsg("sql脚本执行失败", fmt.Sprintf("[%s]执行失败: [%s]", filename, err.Error())))
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		d.MsgApp.CreateAndSend(rc.LoginAccount, ws.SuccessMsg("sql脚本执行成功", fmt.Sprintf("[%s]执行完成", filename)))
 | 
			
		||||
	}()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// @router /api/db/:dbId/t-metadata [get]
 | 
			
		||||
func (d *Db) TableMA(rc *ctx.ReqCtx) {
 | 
			
		||||
	rc.ResData = d.DbApp.GetDbInstance(GetDbId(rc.GinCtx)).GetTableMetedatas()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
package api
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"mayfly-go/base/biz"
 | 
			
		||||
	"mayfly-go/base/ctx"
 | 
			
		||||
	"mayfly-go/base/ginx"
 | 
			
		||||
@@ -30,6 +31,13 @@ func (m *Machine) Machines(rc *ctx.ReqCtx) {
 | 
			
		||||
	rc.ResData = res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *Machine) MachineStats(rc *ctx.ReqCtx) {
 | 
			
		||||
	writer := bytes.NewBufferString("")
 | 
			
		||||
	stats := m.MachineApp.GetCli(GetMachineId(rc.GinCtx)).GetAllStats()
 | 
			
		||||
	machine.ShowStats(writer, stats)
 | 
			
		||||
	rc.ResData = writer.String()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *Machine) SaveMachine(rc *ctx.ReqCtx) {
 | 
			
		||||
	g := rc.GinCtx
 | 
			
		||||
	machineForm := new(form.MachineForm)
 | 
			
		||||
 
 | 
			
		||||
@@ -151,7 +151,8 @@ func (m *machineFileAppImpl) UploadFile(la *model.LoginAccount, fileId uint64, p
 | 
			
		||||
 | 
			
		||||
	createfile.Write(content)
 | 
			
		||||
	// 保存消息并发送文件上传成功通知
 | 
			
		||||
	m.msgApp.CreateAndSend(la, ws.SuccessMsg("文件上传成功", fmt.Sprintf("[%s]文件已成功上传至[%s]", filename, path)))
 | 
			
		||||
	machine := m.machineRepo.GetById(machineId)
 | 
			
		||||
	m.msgApp.CreateAndSend(la, ws.SuccessMsg("文件上传成功", fmt.Sprintf("[%s]文件已成功上传至 %s[%s:%s]", filename, machine.Name, machine.Ip, path)))
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ import (
 | 
			
		||||
	"mayfly-go/base/ctx"
 | 
			
		||||
	"mayfly-go/server/devops/api"
 | 
			
		||||
	"mayfly-go/server/devops/application"
 | 
			
		||||
	sysApplication "mayfly-go/server/sys/application"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
@@ -11,7 +12,9 @@ import (
 | 
			
		||||
func InitDbRouter(router *gin.RouterGroup) {
 | 
			
		||||
	db := router.Group("dbs")
 | 
			
		||||
	{
 | 
			
		||||
		d := &api.Db{DbApp: application.DbApp}
 | 
			
		||||
		d := &api.Db{DbApp: application.DbApp,
 | 
			
		||||
			MsgApp: sysApplication.MsgApp,
 | 
			
		||||
		}
 | 
			
		||||
		// 获取所有数据库列表
 | 
			
		||||
		db.GET("", func(c *gin.Context) {
 | 
			
		||||
			rc := ctx.NewReqCtxWithGin(c)
 | 
			
		||||
@@ -50,6 +53,11 @@ func InitDbRouter(router *gin.RouterGroup) {
 | 
			
		||||
			rc.Handle(d.ExecSql)
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		db.POST(":dbId/exec-sql-file", func(g *gin.Context) {
 | 
			
		||||
			rc := ctx.NewReqCtxWithGin(g).WithLog(ctx.NewLogInfo("执行Sql文件"))
 | 
			
		||||
			rc.Handle(d.ExecSqlFile)
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		db.GET(":dbId/t-metadata", func(c *gin.Context) {
 | 
			
		||||
			ctx.NewReqCtxWithGin(c).Handle(d.TableMA)
 | 
			
		||||
		})
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,10 @@ func InitMachineRouter(router *gin.RouterGroup) {
 | 
			
		||||
			ctx.NewReqCtxWithGin(c).Handle(m.Machines)
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		machines.GET(":machineId/stats", func(c *gin.Context) {
 | 
			
		||||
			ctx.NewReqCtxWithGin(c).Handle(m.MachineStats)
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		saveMachine := ctx.NewLogInfo("保存机器信息")
 | 
			
		||||
		machines.POST("", func(c *gin.Context) {
 | 
			
		||||
			ctx.NewReqCtxWithGin(c).
 | 
			
		||||
 
 | 
			
		||||
@@ -126,7 +126,7 @@ func (a Account) UpdateAccount(rc *ctx.ReqCtx) {
 | 
			
		||||
	a.AccountApp.Update(updateAccount)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取账号接受的消息列表
 | 
			
		||||
// 获取账号接收的消息列表
 | 
			
		||||
func (a Account) GetMsgs(rc *ctx.ReqCtx) {
 | 
			
		||||
	condition := &entity.Msg{
 | 
			
		||||
		RecipientId: int64(rc.LoginAccount.Id),
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user