diff --git a/server/api/v1/system/sys_export_template.go b/server/api/v1/system/sys_export_template.go index 0d61588ee..604309ddc 100644 --- a/server/api/v1/system/sys_export_template.go +++ b/server/api/v1/system/sys_export_template.go @@ -50,6 +50,34 @@ type SysExportTemplateApi struct { var sysExportTemplateService = service.ServiceGroupApp.SystemServiceGroup.SysExportTemplateService +// PreviewSQL 预览最终生成的SQL +// @Tags SysExportTemplate +// @Summary 预览最终生成的SQL(不执行查询,仅返回SQL字符串) +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param templateID query string true "导出模板ID" +// @Param params query string false "查询参数编码字符串,参考 ExportExcel 组件" +// @Success 200 {object} response.Response{data=map[string]string} "获取成功" +// @Router /sysExportTemplate/previewSQL [get] +func (sysExportTemplateApi *SysExportTemplateApi) PreviewSQL(c *gin.Context) { + templateID := c.Query("templateID") + if templateID == "" { + response.FailWithMessage("模板ID不能为空", c) + return + } + + // 直接复用导出接口的参数组织方式:使用 URL Query,其中 params 为内部编码的查询字符串 + queryParams := c.Request.URL.Query() + + if sqlPreview, err := sysExportTemplateService.PreviewSQL(templateID, queryParams); err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + } else { + response.OkWithData(gin.H{"sql": sqlPreview}, c) + } +} + // CreateSysExportTemplate 创建导出模板 // @Tags SysExportTemplate // @Summary 创建导出模板 diff --git a/server/router/system/sys_export_template.go b/server/router/system/sys_export_template.go index e7fd5beef..fb82fb167 100644 --- a/server/router/system/sys_export_template.go +++ b/server/router/system/sys_export_template.go @@ -26,6 +26,7 @@ func (s *SysExportTemplateRouter) InitSysExportTemplateRouter(Router *gin.Router sysExportTemplateRouterWithoutRecord.GET("getSysExportTemplateList", exportTemplateApi.GetSysExportTemplateList) // 获取导出模板列表 sysExportTemplateRouterWithoutRecord.GET("exportExcel", exportTemplateApi.ExportExcel) // 获取导出token sysExportTemplateRouterWithoutRecord.GET("exportTemplate", exportTemplateApi.ExportTemplate) // 导出表格模板 + sysExportTemplateRouterWithoutRecord.GET("previewSQL", exportTemplateApi.PreviewSQL) // 预览SQL } { sysExportTemplateRouterWithoutAuth.GET("exportExcelByToken", exportTemplateApi.ExportExcelByToken) // 通过token导出表格 diff --git a/server/service/system/sys_export_template.go b/server/service/system/sys_export_template.go index 182ce6298..9344291e1 100644 --- a/server/service/system/sys_export_template.go +++ b/server/service/system/sys_export_template.go @@ -213,6 +213,16 @@ func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID sql = fmt.Sprintf("%s %s (?)", condition.Column, condition.Operator) } + if condition.Operator == "BETWEEN" { + sql = fmt.Sprintf("%s BETWEEN ? AND ?", condition.Column) + startValue := paramsValues.Get("start" + condition.From) + endValue := paramsValues.Get("end" + condition.From) + if startValue != "" && endValue != "" { + db = db.Where(sql, startValue, endValue) + } + continue + } + if value != "" { if condition.Operator == "LIKE" { value = "%" + value + "%" @@ -317,14 +327,14 @@ func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID for j, colCell := range row { cell := fmt.Sprintf("%s%d", getColumnName(j+1), i+1) - var sErr error - if v, err := strconv.ParseFloat(colCell, 64); err == nil { - sErr = f.SetCellValue("Sheet1", cell, v) - } else if v, err := strconv.ParseInt(colCell, 10, 64); err == nil { - sErr = f.SetCellValue("Sheet1", cell, v) - } else { - sErr = f.SetCellValue("Sheet1", cell, colCell) - } + var sErr error + if v, err := strconv.ParseFloat(colCell, 64); err == nil { + sErr = f.SetCellValue("Sheet1", cell, v) + } else if v, err := strconv.ParseInt(colCell, 10, 64); err == nil { + sErr = f.SetCellValue("Sheet1", cell, v) + } else { + sErr = f.SetCellValue("Sheet1", cell, colCell) + } if sErr != nil { return nil, "", sErr @@ -340,6 +350,185 @@ func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID return file, template.Name, nil } +// PreviewSQL 预览最终生成的 SQL(不执行查询,仅返回 SQL 字符串) +// Author [piexlmax](https://github.com/piexlmax) & [trae-ai] +func (sysExportTemplateService *SysExportTemplateService) PreviewSQL(templateID string, values url.Values) (sqlPreview string, err error) { + // 解析 params(与导出逻辑保持一致) + var params = values.Get("params") + paramsValues, _ := url.ParseQuery(params) + + // 加载模板 + var template system.SysExportTemplate + err = global.GVA_DB.Preload("Conditions").Preload("JoinTemplate").First(&template, "template_id = ?", templateID).Error + if err != nil { + return "", err + } + + // 解析模板列 + var templateInfoMap = make(map[string]string) + columns, err := utils.GetJSONKeys(template.TemplateInfo) + if err != nil { + return "", err + } + err = json.Unmarshal([]byte(template.TemplateInfo), &templateInfoMap) + if err != nil { + return "", err + } + var selectKeyFmt []string + for _, key := range columns { + selectKeyFmt = append(selectKeyFmt, key) + } + selects := strings.Join(selectKeyFmt, ", ") + + // 生成 FROM 与 JOIN 片段 + var sb strings.Builder + sb.WriteString("SELECT ") + sb.WriteString(selects) + sb.WriteString(" FROM ") + sb.WriteString(template.TableName) + + if len(template.JoinTemplate) > 0 { + for _, join := range template.JoinTemplate { + sb.WriteString(" ") + sb.WriteString(join.JOINS) + sb.WriteString(" ") + sb.WriteString(join.Table) + sb.WriteString(" ON ") + sb.WriteString(join.ON) + } + } + + // WHERE 条件 + var wheres []string + + // 软删除过滤 + filterDeleted := false + if paramsValues != nil { + filterParam := paramsValues.Get("filterDeleted") + if filterParam == "true" { + filterDeleted = true + } + } + if filterDeleted { + wheres = append(wheres, fmt.Sprintf("%s.deleted_at IS NULL", template.TableName)) + if len(template.JoinTemplate) > 0 { + for _, join := range template.JoinTemplate { + if sysExportTemplateService.hasDeletedAtColumn(join.Table) { + wheres = append(wheres, fmt.Sprintf("%s.deleted_at IS NULL", join.Table)) + } + } + } + } + + // 模板条件(保留与 ExportExcel 同步的解析规则) + if len(template.Conditions) > 0 { + for _, condition := range template.Conditions { + op := strings.ToUpper(strings.TrimSpace(condition.Operator)) + col := strings.TrimSpace(condition.Column) + + // 预览优先展示传入值,没有则展示占位符 + val := "" + if paramsValues != nil { + val = paramsValues.Get(condition.From) + } + + switch op { + case "BETWEEN": + startValue := "" + endValue := "" + if paramsValues != nil { + startValue = paramsValues.Get("start" + condition.From) + endValue = paramsValues.Get("end" + condition.From) + } + if startValue != "" && endValue != "" { + wheres = append(wheres, fmt.Sprintf("%s BETWEEN '%s' AND '%s'", col, startValue, endValue)) + } else { + wheres = append(wheres, fmt.Sprintf("%s BETWEEN {start%s} AND {end%s}", col, condition.From, condition.From)) + } + case "IN", "NOT IN": + if val != "" { + // 逗号分隔值做简单展示 + parts := strings.Split(val, ",") + for i := range parts { parts[i] = strings.TrimSpace(parts[i]) } + wheres = append(wheres, fmt.Sprintf("%s %s ('%s')", col, op, strings.Join(parts, "','"))) + } else { + wheres = append(wheres, fmt.Sprintf("%s %s ({%s})", col, op, condition.From)) + } + case "LIKE": + if val != "" { + wheres = append(wheres, fmt.Sprintf("%s LIKE '%%%s%%'", col, val)) + } else { + wheres = append(wheres, fmt.Sprintf("%s LIKE {%%%s%%}", col, condition.From)) + } + default: + if val != "" { + wheres = append(wheres, fmt.Sprintf("%s %s '%s'", col, op, val)) + } else { + wheres = append(wheres, fmt.Sprintf("%s %s {%s}", col, op, condition.From)) + } + } + } + } + + if len(wheres) > 0 { + sb.WriteString(" WHERE ") + sb.WriteString(strings.Join(wheres, " AND ")) + } + + // 排序 + order := "" + if paramsValues != nil { + order = paramsValues.Get("order") + } + if order == "" && template.Order != "" { + order = template.Order + } + if order != "" { + sb.WriteString(" ORDER BY ") + sb.WriteString(order) + } + + // limit/offset(如果传入或默认值为0,则不生成) + limitStr := "" + offsetStr := "" + if paramsValues != nil { + limitStr = paramsValues.Get("limit") + offsetStr = paramsValues.Get("offset") + } + + // 处理模板默认limit(仅当非0时) + if limitStr == "" && template.Limit != nil && *template.Limit != 0 { + limitStr = strconv.Itoa(*template.Limit) + } + + // 解析为数值,用于判断是否生成 + limitInt := 0 + offsetInt := 0 + if limitStr != "" { + if v, e := strconv.Atoi(limitStr); e == nil { limitInt = v } + } + if offsetStr != "" { + if v, e := strconv.Atoi(offsetStr); e == nil { offsetInt = v } + } + + if limitInt > 0 { + sb.WriteString(" LIMIT ") + sb.WriteString(strconv.Itoa(limitInt)) + if offsetInt > 0 { + sb.WriteString(" OFFSET ") + sb.WriteString(strconv.Itoa(offsetInt)) + } + } else { + // 当limit未设置或为0时,仅当offset>0才生成OFFSET + if offsetInt > 0 { + sb.WriteString(" OFFSET ") + sb.WriteString(strconv.Itoa(offsetInt)) + } + } + + return sb.String(), nil +} + // ExportTemplate 导出Excel模板 // Author [piexlmax](https://github.com/piexlmax) func (sysExportTemplateService *SysExportTemplateService) ExportTemplate(templateID string) (file *bytes.Buffer, name string, err error) { diff --git a/server/source/system/api.go b/server/source/system/api.go index 87a47695c..9ed3383c6 100644 --- a/server/source/system/api.go +++ b/server/source/system/api.go @@ -175,6 +175,7 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) { {ApiGroup: "导出模板", Method: "GET", Path: "/sysExportTemplate/getSysExportTemplateList", Description: "获取导出模板列表"}, {ApiGroup: "导出模板", Method: "GET", Path: "/sysExportTemplate/exportExcel", Description: "导出Excel"}, {ApiGroup: "导出模板", Method: "GET", Path: "/sysExportTemplate/exportTemplate", Description: "下载模板"}, + {ApiGroup: "导出模板", Method: "GET", Path: "/sysExportTemplate/previewSQL", Description: "预览SQL"}, {ApiGroup: "导出模板", Method: "POST", Path: "/sysExportTemplate/importExcel", Description: "导入Excel"}, {ApiGroup: "公告", Method: "POST", Path: "/info/createInfo", Description: "新建公告"}, diff --git a/server/source/system/casbin.go b/server/source/system/casbin.go index 6bd3c0499..e6e081c67 100644 --- a/server/source/system/casbin.go +++ b/server/source/system/casbin.go @@ -176,6 +176,7 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error {Ptype: "p", V0: "888", V1: "/sysExportTemplate/getSysExportTemplateList", V2: "GET"}, {Ptype: "p", V0: "888", V1: "/sysExportTemplate/exportExcel", V2: "GET"}, {Ptype: "p", V0: "888", V1: "/sysExportTemplate/exportTemplate", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysExportTemplate/previewSQL", V2: "GET"}, {Ptype: "p", V0: "888", V1: "/sysExportTemplate/importExcel", V2: "POST"}, {Ptype: "p", V0: "888", V1: "/info/createInfo", V2: "POST"}, diff --git a/web/src/api/exportTemplate.js b/web/src/api/exportTemplate.js index 9f8729b01..753547de7 100644 --- a/web/src/api/exportTemplate.js +++ b/web/src/api/exportTemplate.js @@ -126,3 +126,20 @@ export const exportTemplate = (params) => { params }) } + +// PreviewSQL 预览最终生成的SQL +// @Tags SysExportTemplate +// @Summary 预览最终生成的SQL +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Router /sysExportTemplate/previewSQL [get] +// @Param templateID query string true "导出模板ID" +// @Param params query string false "查询参数编码字符串,参考 ExportExcel 组件" +export const previewSQL = (params) => { + return service({ + url: '/sysExportTemplate/previewSQL', + method: 'get', + params + }) +} diff --git a/web/src/pathInfo.json b/web/src/pathInfo.json index 780f9a6e3..318e8113f 100644 --- a/web/src/pathInfo.json +++ b/web/src/pathInfo.json @@ -73,6 +73,7 @@ "/src/view/systemTools/index.vue": "System", "/src/view/systemTools/installPlugin/index.vue": "Index", "/src/view/systemTools/pubPlug/pubPlug.vue": "PubPlug", + "/src/view/systemTools/sysError/sysError.vue": "SysError", "/src/view/systemTools/system/system.vue": "Config", "/src/view/systemTools/version/version.vue": "SysVersion", "/src/plugin/announcement/form/info.vue": "InfoForm", diff --git a/web/src/view/systemTools/exportTemplate/exportTemplate.vue b/web/src/view/systemTools/exportTemplate/exportTemplate.vue index 86449bfa5..458695f8f 100644 --- a/web/src/view/systemTools/exportTemplate/exportTemplate.vue +++ b/web/src/view/systemTools/exportTemplate/exportTemplate.vue @@ -139,7 +139,7 @@ icon="edit-pen" class="table-button" @click="showCode(scope.row)" - >代码代码和SQL预览 + - + + + + + +
+
+ + + + + + + + + + + + + + + 查询条件 +
该模板暂无条件
+ +
+
+
+ +
+
+
+
@@ -436,6 +487,7 @@ findSysExportTemplate, getSysExportTemplateList } from '@/api/exportTemplate.js' + import { previewSQL } from '@/api/exportTemplate.js' // 全量引入格式化工具 请按需保留 import { formatDate } from '@/utils/format' @@ -448,6 +500,7 @@ import 'ace-builds/src-noconflict/mode-vue' import 'ace-builds/src-noconflict/theme-github_dark' + import 'ace-builds/src-noconflict/mode-sql' defineOptions({ name: 'ExportTemplate' @@ -903,7 +956,8 @@ JOINS模式下不支持导入 getTableData() } } - const codeVisible = ref(false) + const drawerVisible = ref(false) + const activeTab = ref('code') // 弹窗控制标记 const dialogFormVisible = ref(false) @@ -911,7 +965,56 @@ JOINS模式下不支持导入 const showCode = (row) => { webCode.value = getCode(row.templateID) - codeVisible.value = true + activeTab.value = 'code' + drawerVisible.value = true + } + + // 预览 SQL + const previewForm = ref({ filterDeleted: true, order: '', limit: 0, offset: 0 }) + const previewSQLCode = ref('') + const previewTemplate = ref(null) + const previewConditions = ref([]) + const aceOptions = { wrap: true, showPrintMargin: false, fontSize: 14 } + + const openPreview = async (row) => { + // 获取模板完整信息以展示条件输入项 + const res = await findSysExportTemplate({ ID: row.ID }) + if (res.code === 0) { + previewTemplate.value = res.data.resysExportTemplate + previewConditions.value = (previewTemplate.value.conditions || []).map((c) => ({ + from: c.from, + column: c.column, + operator: c.operator + })) + // 预填默认的排序与限制 + previewForm.value.order = previewTemplate.value.order || '' + previewForm.value.limit = previewTemplate.value.limit || 0 + previewForm.value.offset = 0 + previewSQLCode.value = '' + activeTab.value = 'sql' + drawerVisible.value = true + } + } + + const runPreview = async () => { + if (!previewTemplate.value) return + // 组装 params,与导出组件保持一致 + const paramsCopy = JSON.parse(JSON.stringify(previewForm.value)) + // 将布尔与数值等按照导出组件规则编码 + if (paramsCopy.filterDeleted) paramsCopy.filterDeleted = 'true' + const entries = Object.entries(paramsCopy).filter(([key, v]) => { + if (v === '' || v === null || v === undefined) return false + if ((key === 'limit' || key === 'offset') && Number(v) === 0) return false + return true + }) + const params = entries + .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) + .join('&') + + const res = await previewSQL({ templateID: previewTemplate.value.templateID, params }) + if (res.code === 0) { + previewSQLCode.value = res.data.sql || '' + } } // 打开弹窗 @@ -922,7 +1025,6 @@ JOINS模式下不支持导入 // 关闭弹窗 const closeDialog = () => { - codeVisible.value = false dialogFormVisible.value = false formData.value = { name: '',