learshaw 2 месяцев назад
Родитель
Сommit
f7ffc63e85

+ 443 - 0
ems/ems-cloud/ems-server/src/main/java/com/ruoyi/ems/controller/CustomReportController.java

@@ -0,0 +1,443 @@
+/*
+ * 文 件 名:  CustomReportController
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2026/1/30
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.controller;
+
+import com.huashe.common.domain.AjaxResult;
+import com.ruoyi.common.core.web.controller.BaseController;
+import com.ruoyi.common.core.web.page.TableDataInfo;
+import com.ruoyi.common.log.annotation.Log;
+import com.ruoyi.common.log.enums.BusinessType;
+import com.ruoyi.common.security.annotation.RequiresPermissions;
+import com.ruoyi.common.security.utils.SecurityUtils;
+import com.ruoyi.ems.domain.ReportDatasource;
+import com.ruoyi.ems.domain.ReportField;
+import com.ruoyi.ems.domain.ReportFieldCondition;
+import com.ruoyi.ems.domain.ReportQueryConfig;
+import com.ruoyi.ems.domain.ReportRelation;
+import com.ruoyi.ems.domain.ReportTemplate;
+import com.ruoyi.ems.model.ReportQueryResult;
+import com.ruoyi.ems.service.ICustomReportService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletResponse;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 自定义报表Controller
+ *
+ * 提供动态SQL查询能力,支持:
+ * 1. 数据源管理(配置可查询的表)
+ * 2. 字段配置(配置可选字段和条件)
+ * 3. 报表模板管理(保存/加载用户配置)
+ * 4. 动态查询执行(根据配置生成SQL并执行)
+ * 5. 数据导出(Excel导出)
+ *
+ * @author ruoyi
+ * @date 2026-01-30
+ */
+@RestController
+@RequestMapping("/report/custom")
+@Api(value = "CustomReportController", description = "自定义报表接口")
+public class CustomReportController extends BaseController {
+
+    @Autowired
+    private ICustomReportService customReportService;
+
+    // ==================== 数据源管理 ====================
+
+    /**
+     * 查询所有可用的数据源
+     */
+    @RequiresPermissions("report:custom:list")
+    @GetMapping("/datasource/list")
+    @ApiOperation("查询数据源列表")
+    public AjaxResult listDatasources(
+        @ApiParam("分类筛选") @RequestParam(required = false) String category) {
+        List<ReportDatasource> list = customReportService.selectDatasourceList(category);
+        return success(list);
+    }
+
+    /**
+     * 获取数据源详情(包含字段、条件、关联配置)
+     */
+    @RequiresPermissions("report:custom:list")
+    @GetMapping("/datasource/{dsCode}")
+    @ApiOperation("获取数据源详情")
+    public AjaxResult getDatasourceDetail(
+        @ApiParam("数据源编码") @PathVariable String dsCode) {
+        ReportDatasource datasource = customReportService.selectDatasourceDetail(dsCode);
+        if (datasource == null) {
+            return error("数据源不存在");
+        }
+        return success(datasource);
+    }
+
+    /**
+     * 获取数据源的字段列表
+     */
+    @RequiresPermissions("report:custom:list")
+    @GetMapping("/datasource/{dsCode}/fields")
+    @ApiOperation("获取数据源字段列表")
+    public AjaxResult getDatasourceFields(
+        @ApiParam("数据源编码") @PathVariable String dsCode) {
+        List<ReportField> fields = customReportService.selectFieldsByDsCode(dsCode);
+        return success(fields);
+    }
+
+    /**
+     * 获取字段的可用条件
+     */
+    @RequiresPermissions("report:custom:list")
+    @GetMapping("/datasource/{dsCode}/field/{fieldCode}/conditions")
+    @ApiOperation("获取字段条件列表")
+    public AjaxResult getFieldConditions(
+        @ApiParam("数据源编码") @PathVariable String dsCode,
+        @ApiParam("字段编码") @PathVariable String fieldCode) {
+        List<ReportFieldCondition> conditions = customReportService.selectFieldConditions(dsCode, fieldCode);
+        return success(conditions);
+    }
+
+    // ==================== 报表模板管理 ====================
+
+    /**
+     * 查询模板列表
+     */
+    @RequiresPermissions("report:custom:list")
+    @GetMapping("/template/list")
+    @ApiOperation("查询报表模板列表")
+    public TableDataInfo listTemplates(ReportTemplate query) {
+        startPage();
+        // 设置当前用户ID,用于查询私有模板
+        Long userId = SecurityUtils.getUserId();
+        query.setUserId(userId);
+        List<ReportTemplate> list = customReportService.selectTemplateList(query);
+        return getDataTable(list);
+    }
+
+    /**
+     * 获取模板详情
+     */
+    @RequiresPermissions("report:custom:list")
+    @GetMapping("/template/{templateCode}")
+    @ApiOperation("获取报表模板详情")
+    public AjaxResult getTemplate(
+        @ApiParam("模板编码") @PathVariable String templateCode) {
+        ReportTemplate template = customReportService.selectTemplateByCode(templateCode);
+        if (template == null) {
+            return error("模板不存在");
+        }
+        return success(template);
+    }
+
+    /**
+     * 保存模板
+     */
+    @RequiresPermissions("report:custom:add")
+    @Log(title = "自定义报表模板", businessType = BusinessType.INSERT)
+    @PostMapping("/template")
+    @ApiOperation("保存报表模板")
+    public AjaxResult saveTemplate(@RequestBody ReportTemplate template) {
+        // 设置当前用户信息
+        Long userId = SecurityUtils.getUserId();
+        String userName = SecurityUtils.getUsername();
+        template.setUserId(userId);
+        template.setUserName(userName);
+        template.setCreateBy(userName);
+
+        int result = customReportService.saveTemplate(template);
+        return result > 0 ? success("保存成功") : error("保存失败");
+    }
+
+    /**
+     * 更新模板
+     */
+    @RequiresPermissions("report:custom:edit")
+    @Log(title = "自定义报表模板", businessType = BusinessType.UPDATE)
+    @PutMapping("/template")
+    @ApiOperation("更新报表模板")
+    public AjaxResult updateTemplate(@RequestBody ReportTemplate template) {
+        // 检查权限(只能修改自己的模板或系统管理员)
+        ReportTemplate existing = customReportService.selectTemplateByCode(template.getTemplateCode());
+        if (existing == null) {
+            return error("模板不存在");
+        }
+        Long userId = SecurityUtils.getUserId();
+        if (existing.getIsSystem() != null && existing.getIsSystem() == 1) {
+            return error("系统模板不可修改");
+        }
+        if (!userId.equals(existing.getUserId())) {
+            return error("无权修改此模板");
+        }
+
+        template.setUpdateBy(SecurityUtils.getUsername());
+        int result = customReportService.updateTemplate(template);
+        return result > 0 ? success("更新成功") : error("更新失败");
+    }
+
+    /**
+     * 删除模板
+     */
+    @RequiresPermissions("report:custom:remove")
+    @Log(title = "自定义报表模板", businessType = BusinessType.DELETE)
+    @DeleteMapping("/template/{templateCode}")
+    @ApiOperation("删除报表模板")
+    public AjaxResult deleteTemplate(
+        @ApiParam("模板编码") @PathVariable String templateCode) {
+        // 检查权限
+        ReportTemplate existing = customReportService.selectTemplateByCode(templateCode);
+        if (existing == null) {
+            return error("模板不存在");
+        }
+        if (existing.getIsSystem() != null && existing.getIsSystem() == 1) {
+            return error("系统模板不可删除");
+        }
+        Long userId = SecurityUtils.getUserId();
+        if (!userId.equals(existing.getUserId())) {
+            return error("无权删除此模板");
+        }
+
+        int result = customReportService.deleteTemplate(templateCode);
+        return result > 0 ? success("删除成功") : error("删除失败");
+    }
+
+    // ==================== 动态查询执行 ====================
+
+    /**
+     * 执行报表查询
+     */
+    @RequiresPermissions("report:custom:query")
+    @PostMapping("/query")
+    @ApiOperation("执行报表查询")
+    public AjaxResult executeQuery(@RequestBody ReportQueryConfig queryConfig) {
+        try {
+            // 参数校验
+            if (queryConfig.getDsCode() == null && queryConfig.getTemplateCode() == null) {
+                return error("请指定数据源或模板");
+            }
+
+            queryConfig.initDefaults();
+
+            ReportQueryResult result = customReportService.executeQuery(queryConfig);
+            return success(result);
+        } catch (Exception e) {
+            logger.error("执行报表查询异常", e);
+            return error("查询失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 执行报表查询(基于模板)
+     */
+    @RequiresPermissions("report:custom:query")
+    @PostMapping("/query/template/{templateCode}")
+    @ApiOperation("基于模板执行查询")
+    public AjaxResult executeQueryByTemplate(
+        @ApiParam("模板编码") @PathVariable String templateCode,
+        @ApiParam("参数值") @RequestBody(required = false) Map<String, Object> params) {
+        try {
+            ReportQueryConfig queryConfig = new ReportQueryConfig();
+            queryConfig.setTemplateCode(templateCode);
+            queryConfig.setParams(params);
+            queryConfig.initDefaults();
+
+            // 更新模板使用次数
+            customReportService.incrementTemplateUseCount(templateCode);
+
+            ReportQueryResult result = customReportService.executeQuery(queryConfig);
+            return success(result);
+        } catch (Exception e) {
+            logger.error("执行模板查询异常", e);
+            return error("查询失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 获取报表汇总数据
+     */
+    @RequiresPermissions("report:custom:query")
+    @PostMapping("/summary")
+    @ApiOperation("获取报表汇总数据")
+    public AjaxResult getSummary(@RequestBody ReportQueryConfig queryConfig) {
+        try {
+            if (queryConfig.getDsCode() == null && queryConfig.getTemplateCode() == null) {
+                return error("请指定数据源或模板");
+            }
+
+            Map<String, Object> summary = customReportService.executeSummaryQuery(queryConfig);
+            return success(summary);
+        } catch (Exception e) {
+            logger.error("获取报表汇总异常", e);
+            return error("查询失败: " + e.getMessage());
+        }
+    }
+
+    // ==================== 数据导出 ====================
+
+    /**
+     * 导出报表数据
+     */
+    @RequiresPermissions("report:custom:export")
+    @Log(title = "自定义报表", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    @ApiOperation("导出报表数据")
+    public void exportReport(HttpServletResponse response, @RequestBody ReportQueryConfig queryConfig) {
+        try {
+            queryConfig.setExportAll(true);
+            queryConfig.initDefaults();
+
+            // 执行查询获取全部数据
+            ReportQueryResult result = customReportService.executeQuery(queryConfig);
+
+            if (result.getCode() != 200) {
+                response.setContentType("application/json;charset=UTF-8");
+                response.getWriter().write("{\"code\":500,\"msg\":\"" + result.getMsg() + "\"}");
+                return;
+            }
+
+            // 获取数据源信息用于文件名
+            String dsCode = queryConfig.getDsCode();
+            if (dsCode == null && queryConfig.getTemplateCode() != null) {
+                ReportTemplate template = customReportService.selectTemplateByCode(queryConfig.getTemplateCode());
+                if (template != null) {
+                    dsCode = template.getDsCode();
+                }
+            }
+            ReportDatasource datasource = customReportService.selectDatasourceByCode(dsCode);
+            String dsName = datasource != null ? datasource.getDsName() : "报表";
+
+            // 导出Excel
+            customReportService.exportToExcel(response, result, dsName);
+        } catch (Exception e) {
+            logger.error("导出报表异常", e);
+            try {
+                response.setContentType("application/json;charset=UTF-8");
+                response.getWriter().write("{\"code\":500,\"msg\":\"导出失败: " + e.getMessage() + "\"}");
+            } catch (Exception ex) {
+                logger.error("响应异常信息失败", ex);
+            }
+        }
+    }
+
+    // ==================== 配置管理(管理员) ====================
+
+    /**
+     * 新增数据源配置
+     */
+    @RequiresPermissions("report:config:add")
+    @Log(title = "数据源配置", businessType = BusinessType.INSERT)
+    @PostMapping("/datasource")
+    @ApiOperation("新增数据源配置")
+    public AjaxResult addDatasource(@RequestBody ReportDatasource datasource) {
+        datasource.setCreateBy(SecurityUtils.getUsername());
+        int result = customReportService.insertDatasource(datasource);
+        return result > 0 ? success("新增成功") : error("新增失败");
+    }
+
+    /**
+     * 修改数据源配置
+     */
+    @RequiresPermissions("report:config:edit")
+    @Log(title = "数据源配置", businessType = BusinessType.UPDATE)
+    @PutMapping("/datasource")
+    @ApiOperation("修改数据源配置")
+    public AjaxResult updateDatasource(@RequestBody ReportDatasource datasource) {
+        datasource.setUpdateBy(SecurityUtils.getUsername());
+        int result = customReportService.updateDatasource(datasource);
+        return result > 0 ? success("修改成功") : error("修改失败");
+    }
+
+    /**
+     * 新增字段配置
+     */
+    @RequiresPermissions("report:config:add")
+    @Log(title = "字段配置", businessType = BusinessType.INSERT)
+    @PostMapping("/field")
+    @ApiOperation("新增字段配置")
+    public AjaxResult addField(@RequestBody ReportField field) {
+        int result = customReportService.insertField(field);
+        return result > 0 ? success("新增成功") : error("新增失败");
+    }
+
+    /**
+     * 批量新增字段配置
+     */
+    @RequiresPermissions("report:config:add")
+    @Log(title = "字段配置", businessType = BusinessType.INSERT)
+    @PostMapping("/field/batch")
+    @ApiOperation("批量新增字段配置")
+    public AjaxResult addFieldBatch(@RequestBody List<ReportField> fields) {
+        int result = customReportService.insertFieldBatch(fields);
+        return result > 0 ? success("新增成功") : error("新增失败");
+    }
+
+    /**
+     * 修改字段配置
+     */
+    @RequiresPermissions("report:config:edit")
+    @Log(title = "字段配置", businessType = BusinessType.UPDATE)
+    @PutMapping("/field")
+    @ApiOperation("修改字段配置")
+    public AjaxResult updateField(@RequestBody ReportField field) {
+        int result = customReportService.updateField(field);
+        return result > 0 ? success("修改成功") : error("修改失败");
+    }
+
+    /**
+     * 删除字段配置
+     */
+    @RequiresPermissions("report:config:remove")
+    @Log(title = "字段配置", businessType = BusinessType.DELETE)
+    @DeleteMapping("/field/{dsCode}/{fieldCode}")
+    @ApiOperation("删除字段配置")
+    public AjaxResult deleteField(
+        @PathVariable String dsCode,
+        @PathVariable String fieldCode) {
+        int result = customReportService.deleteField(dsCode, fieldCode);
+        return result > 0 ? success("删除成功") : error("删除失败");
+    }
+
+    /**
+     * 新增关联配置
+     */
+    @RequiresPermissions("report:config:add")
+    @Log(title = "关联配置", businessType = BusinessType.INSERT)
+    @PostMapping("/relation")
+    @ApiOperation("新增关联配置")
+    public AjaxResult addRelation(@RequestBody ReportRelation relation) {
+        int result = customReportService.insertRelation(relation);
+        return result > 0 ? success("新增成功") : error("新增失败");
+    }
+
+    /**
+     * 修改关联配置
+     */
+    @RequiresPermissions("report:config:edit")
+    @Log(title = "关联配置", businessType = BusinessType.UPDATE)
+    @PutMapping("/relation")
+    @ApiOperation("修改关联配置")
+    public AjaxResult updateRelation(@RequestBody ReportRelation relation) {
+        int result = customReportService.updateRelation(relation);
+        return result > 0 ? success("修改成功") : error("修改失败");
+    }
+}

+ 95 - 0
ems/ems-core/src/main/java/com/ruoyi/ems/domain/ReportDatasource.java

@@ -0,0 +1,95 @@
+package com.ruoyi.ems.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.huashe.common.annotation.Excel;
+import com.huashe.common.domain.BaseEntity;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 自定义报表-数据源配置实体
+ *
+ * @author ruoyi
+ * @date 2026-01-30
+ */
+@Data
+@ApiModel("数据源配置")
+public class ReportDatasource extends BaseEntity {
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 数据源编码 */
+    @ApiModelProperty("数据源编码")
+    @Excel(name = "数据源编码")
+    private String dsCode;
+
+    /** 数据源名称 */
+    @ApiModelProperty("数据源名称")
+    @Excel(name = "数据源名称")
+    private String dsName;
+
+    /** 数据源描述 */
+    @ApiModelProperty("数据源描述")
+    private String dsDesc;
+
+    /** 主表名 */
+    @ApiModelProperty("主表名")
+    private String mainTable;
+
+    /** 主表别名 */
+    @ApiModelProperty("主表别名")
+    private String mainAlias;
+
+    /** 基础查询条件 */
+    @ApiModelProperty("基础查询条件")
+    private String baseWhere;
+
+    /** 默认排序 */
+    @ApiModelProperty("默认排序")
+    private String defaultOrder;
+
+    /** 分类 */
+    @ApiModelProperty("分类(prod-产能,elec-用电,water-用水,store-储能)")
+    @Excel(name = "分类")
+    private String category;
+
+    /** 图标 */
+    @ApiModelProperty("图标")
+    private String icon;
+
+    /** 排序号 */
+    @ApiModelProperty("排序号")
+    private Integer sortOrder;
+
+    /** 状态 */
+    @ApiModelProperty("状态(0-禁用,1-启用)")
+    private Integer status;
+
+    /** 创建者 */
+    private String createBy;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 更新者 */
+    private String updateBy;
+
+    /** 更新时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+
+    // ==================== 扩展属性 ====================
+
+    /** 字段列表(非数据库字段) */
+    private transient List<ReportField> fields;
+
+    /** 关联列表(非数据库字段) */
+    private transient List<ReportRelation> relations;
+}

+ 134 - 0
ems/ems-core/src/main/java/com/ruoyi/ems/domain/ReportField.java

@@ -0,0 +1,134 @@
+package com.ruoyi.ems.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.huashe.common.annotation.Excel;
+import com.huashe.common.domain.BaseEntity;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 自定义报表-字段配置实体
+ *
+ * @author ruoyi
+ * @date 2026-01-30
+ */
+@Data
+@ApiModel("字段配置")
+public class ReportField extends BaseEntity {
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 数据源编码 */
+    @ApiModelProperty("数据源编码")
+    private String dsCode;
+
+    /** 字段编码 */
+    @ApiModelProperty("字段编码")
+    @Excel(name = "字段编码")
+    private String fieldCode;
+
+    /** 字段显示名称 */
+    @ApiModelProperty("字段显示名称")
+    @Excel(name = "字段名称")
+    private String fieldName;
+
+    /** 字段别名 */
+    @ApiModelProperty("字段别名")
+    private String fieldAlias;
+
+    /** 字段表达式 */
+    @ApiModelProperty("字段表达式")
+    private String fieldExpr;
+
+    /** 字段类型 */
+    @ApiModelProperty("字段类型(string/number/date/datetime/time)")
+    @Excel(name = "字段类型")
+    private String fieldType;
+
+    /** 显示格式 */
+    @ApiModelProperty("显示格式")
+    private String fieldFormat;
+
+    /** 小数位数 */
+    @ApiModelProperty("小数位数")
+    private Integer decimals;
+
+    /** 单位 */
+    @ApiModelProperty("单位")
+    @Excel(name = "单位")
+    private String unit;
+
+    /** 字典类型 */
+    @ApiModelProperty("字典类型")
+    private String dictType;
+
+    /** 是否默认显示 */
+    @ApiModelProperty("是否默认显示")
+    private Integer isDefault;
+
+    /** 是否必选 */
+    @ApiModelProperty("是否必选")
+    private Integer isRequired;
+
+    /** 是否可筛选 */
+    @ApiModelProperty("是否可筛选")
+    private Integer isFilterable;
+
+    /** 是否可排序 */
+    @ApiModelProperty("是否可排序")
+    private Integer isSortable;
+
+    /** 是否可聚合 */
+    @ApiModelProperty("是否可聚合")
+    private Integer isAggregatable;
+
+    /** 默认聚合函数 */
+    @ApiModelProperty("默认聚合函数")
+    private String aggregateFunc;
+
+    /** 字段分组名称 */
+    @ApiModelProperty("字段分组名称")
+    @Excel(name = "分组")
+    private String groupName;
+
+    /** 列宽度 */
+    @ApiModelProperty("列宽度")
+    private Integer width;
+
+    /** 最小列宽度 */
+    @ApiModelProperty("最小列宽度")
+    private Integer minWidth;
+
+    /** 排序号 */
+    @ApiModelProperty("排序号")
+    private Integer sortOrder;
+
+    /** 状态 */
+    @ApiModelProperty("状态")
+    private Integer status;
+
+    /** 备注 */
+    private String remark;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 更新时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+
+    // ==================== 扩展属性 ====================
+
+    /** 支持的条件列表(非数据库字段) */
+    private transient List<ReportFieldCondition> conditions;
+
+    /** 需要的关联列表(非数据库字段) */
+    private transient List<String> relationCodes;
+}

+ 60 - 0
ems/ems-core/src/main/java/com/ruoyi/ems/domain/ReportFieldCondition.java

@@ -0,0 +1,60 @@
+package com.ruoyi.ems.domain;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 自定义报表-字段条件配置实体
+ *
+ * @author ruoyi
+ * @date 2026-01-30
+ */
+@Data
+@ApiModel("字段条件配置")
+public class ReportFieldCondition {
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 数据源编码 */
+    @ApiModelProperty("数据源编码")
+    private String dsCode;
+
+    /** 字段编码 */
+    @ApiModelProperty("字段编码")
+    private String fieldCode;
+
+    /** 条件类型 */
+    @ApiModelProperty("条件类型(eq/ne/gt/gte/lt/lte/like/in/between/isNull/isNotNull)")
+    private String conditionType;
+
+    /** 条件显示名称 */
+    @ApiModelProperty("条件显示名称")
+    private String conditionName;
+
+    /** 条件符号 */
+    @ApiModelProperty("条件符号")
+    private String conditionSymbol;
+
+    /** 需要的值数量 */
+    @ApiModelProperty("需要的值数量(1-单值,2-双值如between)")
+    private Integer valueCount;
+
+    /** 默认值 */
+    @ApiModelProperty("默认值")
+    private String defaultValue;
+
+    /** 是否默认条件 */
+    @ApiModelProperty("是否默认条件")
+    private Integer isDefault;
+
+    /** 排序号 */
+    @ApiModelProperty("排序号")
+    private Integer sortOrder;
+
+    /** 状态 */
+    @ApiModelProperty("状态")
+    private Integer status;
+}

+ 136 - 0
ems/ems-core/src/main/java/com/ruoyi/ems/domain/ReportQueryConfig.java

@@ -0,0 +1,136 @@
+package com.ruoyi.ems.domain;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 报表查询配置对象
+ * 用于前端传参和模板配置解析
+ *
+ * @author ruoyi
+ * @date 2026-01-30
+ */
+@Data
+@ApiModel("报表查询配置")
+public class ReportQueryConfig {
+    private static final long serialVersionUID = 1L;
+
+    /** 数据源编码 */
+    @ApiModelProperty("数据源编码")
+    private String dsCode;
+
+    /** 模板编码(可选,如果指定则使用模板配置) */
+    @ApiModelProperty("模板编码")
+    private String templateCode;
+
+    /** 选中的字段列表 */
+    @ApiModelProperty("选中的字段列表")
+    private List<String> selectedFields;
+
+    /** 查询条件列表 */
+    @ApiModelProperty("查询条件列表")
+    private List<QueryCondition> conditions;
+
+    /** 分组字段列表 */
+    @ApiModelProperty("分组字段列表")
+    private List<String> groupBy;
+
+    /** 排序配置列表 */
+    @ApiModelProperty("排序配置列表")
+    private List<OrderByConfig> orderBy;
+
+    /** 聚合字段配置 */
+    @ApiModelProperty("聚合字段配置 fieldCode -> 聚合函数(SUM/AVG/MAX/MIN/COUNT)")
+    private Map<String, String> aggregateFields;
+
+    /** 是否去重 */
+    @ApiModelProperty("是否去重")
+    private Boolean distinct;
+
+    /** 分页-页码 */
+    @ApiModelProperty("页码")
+    private Integer pageNum;
+
+    /** 分页-每页数量 */
+    @ApiModelProperty("每页数量")
+    private Integer pageSize;
+
+    /** 是否导出全部(不分页) */
+    @ApiModelProperty("是否导出全部")
+    private Boolean exportAll;
+
+    /** 条件参数值(用于模板变量替换) */
+    @ApiModelProperty("条件参数值")
+    private Map<String, Object> params;
+
+    /**
+     * 查询条件
+     */
+    @Data
+    @ApiModel("查询条件")
+    public static class QueryCondition {
+        /** 字段编码 */
+        @ApiModelProperty("字段编码")
+        private String field;
+
+        /** 操作符 */
+        @ApiModelProperty("操作符(eq/ne/gt/gte/lt/lte/like/in/between/isNull/isNotNull)")
+        private String operator;
+
+        /** 值(单值条件) */
+        @ApiModelProperty("值")
+        private Object value;
+
+        /** 第二个值(between条件) */
+        @ApiModelProperty("第二个值(between条件)")
+        private Object value2;
+
+        /** 是否可选(为空时跳过) */
+        @ApiModelProperty("是否可选")
+        private Boolean optional;
+
+        /** 逻辑关系(与下一个条件的关系) */
+        @ApiModelProperty("逻辑关系(AND/OR)")
+        private String logic;
+    }
+
+    /**
+     * 排序配置
+     */
+    @Data
+    @ApiModel("排序配置")
+    public static class OrderByConfig {
+        /** 字段编码 */
+        @ApiModelProperty("字段编码")
+        private String field;
+
+        /** 排序方向 */
+        @ApiModelProperty("排序方向(ASC/DESC)")
+        private String direction;
+    }
+
+    /**
+     * 初始化默认值
+     */
+    public void initDefaults() {
+        if (this.pageNum == null || this.pageNum < 1) {
+            this.pageNum = 1;
+        }
+        if (this.pageSize == null || this.pageSize < 1) {
+            this.pageSize = 20;
+        }
+        if (this.pageSize > 10000) {
+            this.pageSize = 10000;
+        }
+        if (this.distinct == null) {
+            this.distinct = false;
+        }
+        if (this.exportAll == null) {
+            this.exportAll = false;
+        }
+    }
+}

+ 63 - 0
ems/ems-core/src/main/java/com/ruoyi/ems/domain/ReportRelation.java

@@ -0,0 +1,63 @@
+package com.ruoyi.ems.domain;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 自定义报表-关联关系配置实体
+ *
+ * @author ruoyi
+ * @date 2026-01-30
+ */
+@Data
+@ApiModel("关联关系配置")
+public class ReportRelation {
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 数据源编码 */
+    @ApiModelProperty("数据源编码")
+    private String dsCode;
+
+    /** 关联编码 */
+    @ApiModelProperty("关联编码")
+    private String relationCode;
+
+    /** 关联名称 */
+    @ApiModelProperty("关联名称")
+    private String relationName;
+
+    /** 关联类型 */
+    @ApiModelProperty("关联类型(LEFT/INNER/RIGHT)")
+    private String joinType;
+
+    /** 关联表名 */
+    @ApiModelProperty("关联表名")
+    private String joinTable;
+
+    /** 关联表别名 */
+    @ApiModelProperty("关联表别名")
+    private String joinAlias;
+
+    /** 关联条件 */
+    @ApiModelProperty("关联条件")
+    private String joinCondition;
+
+    /** 是否自动关联 */
+    @ApiModelProperty("是否自动关联")
+    private Integer isAutoJoin;
+
+    /** 排序号 */
+    @ApiModelProperty("排序号")
+    private Integer sortOrder;
+
+    /** 状态 */
+    @ApiModelProperty("状态")
+    private Integer status;
+
+    /** 备注 */
+    private String remark;
+}

+ 100 - 0
ems/ems-core/src/main/java/com/ruoyi/ems/domain/ReportTemplate.java

@@ -0,0 +1,100 @@
+package com.ruoyi.ems.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.huashe.common.annotation.Excel;
+import com.huashe.common.domain.BaseEntity;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 自定义报表-用户模板实体
+ *
+ * @author ruoyi
+ * @date 2026-01-30
+ */
+@Data
+@ApiModel("用户报表模板")
+public class ReportTemplate extends BaseEntity {
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 模板编码 */
+    @ApiModelProperty("模板编码")
+    @Excel(name = "模板编码")
+    private String templateCode;
+
+    /** 模板名称 */
+    @ApiModelProperty("模板名称")
+    @Excel(name = "模板名称")
+    private String templateName;
+
+    /** 模板描述 */
+    @ApiModelProperty("模板描述")
+    private String templateDesc;
+
+    /** 数据源编码 */
+    @ApiModelProperty("数据源编码")
+    @Excel(name = "数据源")
+    private String dsCode;
+
+    /** 配置JSON */
+    @ApiModelProperty("配置JSON")
+    private String configJson;
+
+    /** 是否公开 */
+    @ApiModelProperty("是否公开(0-私有,1-公开)")
+    private Integer isPublic;
+
+    /** 是否系统模板 */
+    @ApiModelProperty("是否系统模板(0-否,1-是)")
+    private Integer isSystem;
+
+    /** 使用次数 */
+    @ApiModelProperty("使用次数")
+    private Integer useCount;
+
+    /** 用户ID */
+    @ApiModelProperty("用户ID")
+    private Long userId;
+
+    /** 用户名 */
+    @ApiModelProperty("用户名")
+    @Excel(name = "创建人")
+    private String userName;
+
+    /** 部门ID */
+    @ApiModelProperty("部门ID")
+    private Long deptId;
+
+    /** 状态 */
+    @ApiModelProperty("状态(0-禁用,1-启用)")
+    private Integer status;
+
+    /** 创建者 */
+    private String createBy;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "创建时间", dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 更新者 */
+    private String updateBy;
+
+    /** 更新时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+
+    // ==================== 扩展属性 ====================
+
+    /** 数据源名称(非数据库字段) */
+    private transient String dsName;
+
+    /** 解析后的配置对象(非数据库字段) */
+    private transient ReportQueryConfig queryConfig;
+}

+ 353 - 0
ems/ems-core/src/main/java/com/ruoyi/ems/mapper/CustomReportMapper.java

@@ -0,0 +1,353 @@
+/*
+ * 文 件 名:  CustomReportMapper
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2026/1/30
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.mapper;
+
+import com.ruoyi.ems.domain.ReportDatasource;
+import com.ruoyi.ems.domain.ReportField;
+import com.ruoyi.ems.domain.ReportFieldCondition;
+import com.ruoyi.ems.domain.ReportRelation;
+import com.ruoyi.ems.domain.ReportTemplate;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 自定义报表Mapper接口
+ *
+ * @author ruoyi
+ * @date 2026-01-30
+ */
+public interface CustomReportMapper {
+
+    // ==================== 数据源管理 ====================
+
+    /**
+     * 查询数据源列表
+     *
+     * @param category 分类(可选)
+     * @return 数据源列表
+     */
+    List<ReportDatasource> selectDatasourceList(@Param("category") String category);
+
+    /**
+     * 根据编码查询数据源
+     *
+     * @param dsCode 数据源编码
+     * @return 数据源
+     */
+    ReportDatasource selectDatasourceByCode(@Param("dsCode") String dsCode);
+
+    /**
+     * 新增数据源
+     *
+     * @param datasource 数据源
+     * @return 结果
+     */
+    int insertDatasource(ReportDatasource datasource);
+
+    /**
+     * 修改数据源
+     *
+     * @param datasource 数据源
+     * @return 结果
+     */
+    int updateDatasource(ReportDatasource datasource);
+
+    /**
+     * 删除数据源
+     *
+     * @param dsCode 数据源编码
+     * @return 结果
+     */
+    int deleteDatasource(@Param("dsCode") String dsCode);
+
+    // ==================== 字段管理 ====================
+
+    /**
+     * 查询数据源的字段列表
+     *
+     * @param dsCode 数据源编码
+     * @return 字段列表
+     */
+    List<ReportField> selectFieldsByDsCode(@Param("dsCode") String dsCode);
+
+    /**
+     * 根据编码查询字段
+     *
+     * @param dsCode    数据源编码
+     * @param fieldCode 字段编码
+     * @return 字段
+     */
+    ReportField selectField(@Param("dsCode") String dsCode, @Param("fieldCode") String fieldCode);
+
+    /**
+     * 新增字段配置
+     *
+     * @param field 字段
+     * @return 结果
+     */
+    int insertField(ReportField field);
+
+    /**
+     * 批量新增字段配置
+     *
+     * @param fields 字段列表
+     * @return 结果
+     */
+    int insertFieldBatch(@Param("list") List<ReportField> fields);
+
+    /**
+     * 修改字段配置
+     *
+     * @param field 字段
+     * @return 结果
+     */
+    int updateField(ReportField field);
+
+    /**
+     * 删除字段配置
+     *
+     * @param dsCode    数据源编码
+     * @param fieldCode 字段编码
+     * @return 结果
+     */
+    int deleteField(@Param("dsCode") String dsCode, @Param("fieldCode") String fieldCode);
+
+    /**
+     * 删除数据源下所有字段
+     *
+     * @param dsCode 数据源编码
+     * @return 结果
+     */
+    int deleteFieldsByDsCode(@Param("dsCode") String dsCode);
+
+    // ==================== 字段条件管理 ====================
+
+    /**
+     * 查询字段的条件列表
+     *
+     * @param dsCode    数据源编码
+     * @param fieldCode 字段编码
+     * @return 条件列表
+     */
+    List<ReportFieldCondition> selectFieldConditions(@Param("dsCode") String dsCode, @Param("fieldCode") String fieldCode);
+
+    /**
+     * 查询数据源所有字段条件
+     *
+     * @param dsCode 数据源编码
+     * @return 条件列表
+     */
+    List<ReportFieldCondition> selectAllFieldConditions(@Param("dsCode") String dsCode);
+
+    /**
+     * 新增字段条件
+     *
+     * @param condition 条件
+     * @return 结果
+     */
+    int insertFieldCondition(ReportFieldCondition condition);
+
+    /**
+     * 批量新增字段条件
+     *
+     * @param conditions 条件列表
+     * @return 结果
+     */
+    int insertFieldConditionBatch(@Param("list") List<ReportFieldCondition> conditions);
+
+    /**
+     * 删除字段条件
+     *
+     * @param dsCode    数据源编码
+     * @param fieldCode 字段编码
+     * @return 结果
+     */
+    int deleteFieldConditions(@Param("dsCode") String dsCode, @Param("fieldCode") String fieldCode);
+
+    // ==================== 关联管理 ====================
+
+    /**
+     * 查询数据源的关联配置
+     *
+     * @param dsCode 数据源编码
+     * @return 关联列表
+     */
+    List<ReportRelation> selectRelationsByDsCode(@Param("dsCode") String dsCode);
+
+    /**
+     * 根据编码查询关联
+     *
+     * @param dsCode       数据源编码
+     * @param relationCode 关联编码
+     * @return 关联
+     */
+    ReportRelation selectRelation(@Param("dsCode") String dsCode, @Param("relationCode") String relationCode);
+
+    /**
+     * 新增关联配置
+     *
+     * @param relation 关联
+     * @return 结果
+     */
+    int insertRelation(ReportRelation relation);
+
+    /**
+     * 修改关联配置
+     *
+     * @param relation 关联
+     * @return 结果
+     */
+    int updateRelation(ReportRelation relation);
+
+    /**
+     * 删除关联配置
+     *
+     * @param dsCode       数据源编码
+     * @param relationCode 关联编码
+     * @return 结果
+     */
+    int deleteRelation(@Param("dsCode") String dsCode, @Param("relationCode") String relationCode);
+
+    /**
+     * 删除数据源下所有关联
+     *
+     * @param dsCode 数据源编码
+     * @return 结果
+     */
+    int deleteRelationsByDsCode(@Param("dsCode") String dsCode);
+
+    // ==================== 字段关联映射管理 ====================
+
+    /**
+     * 查询字段的关联编码列表
+     *
+     * @param dsCode    数据源编码
+     * @param fieldCode 字段编码
+     * @return 关联编码列表
+     */
+    List<String> selectFieldRelationCodes(@Param("dsCode") String dsCode, @Param("fieldCode") String fieldCode);
+
+    /**
+     * 查询数据源所有字段关联映射
+     *
+     * @param dsCode 数据源编码
+     * @return 字段关联映射列表
+     */
+    List<Map<String, String>> selectAllFieldRelations(@Param("dsCode") String dsCode);
+
+    /**
+     * 新增字段关联映射
+     *
+     * @param dsCode       数据源编码
+     * @param fieldCode    字段编码
+     * @param relationCode 关联编码
+     * @return 结果
+     */
+    int insertFieldRelation(@Param("dsCode") String dsCode, @Param("fieldCode") String fieldCode, @Param("relationCode") String relationCode);
+
+    /**
+     * 删除字段关联映射
+     *
+     * @param dsCode    数据源编码
+     * @param fieldCode 字段编码
+     * @return 结果
+     */
+    int deleteFieldRelations(@Param("dsCode") String dsCode, @Param("fieldCode") String fieldCode);
+
+    // ==================== 模板管理 ====================
+
+    /**
+     * 查询模板列表
+     *
+     * @param query 查询条件
+     * @return 模板列表
+     */
+    List<ReportTemplate> selectTemplateList(ReportTemplate query);
+
+    /**
+     * 根据编码查询模板
+     *
+     * @param templateCode 模板编码
+     * @return 模板
+     */
+    ReportTemplate selectTemplateByCode(@Param("templateCode") String templateCode);
+
+    /**
+     * 检查模板编码是否存在
+     *
+     * @param templateCode 模板编码
+     * @return 数量
+     */
+    int checkTemplateCodeExists(@Param("templateCode") String templateCode);
+
+    /**
+     * 新增模板
+     *
+     * @param template 模板
+     * @return 结果
+     */
+    int insertTemplate(ReportTemplate template);
+
+    /**
+     * 修改模板
+     *
+     * @param template 模板
+     * @return 结果
+     */
+    int updateTemplate(ReportTemplate template);
+
+    /**
+     * 删除模板
+     *
+     * @param templateCode 模板编码
+     * @return 结果
+     */
+    int deleteTemplate(@Param("templateCode") String templateCode);
+
+    /**
+     * 增加模板使用次数
+     *
+     * @param templateCode 模板编码
+     * @return 结果
+     */
+    int incrementTemplateUseCount(@Param("templateCode") String templateCode);
+
+    // ==================== 动态查询执行 ====================
+
+    /**
+     * 执行动态查询
+     *
+     * @param sql    SQL语句
+     * @param params 参数
+     * @return 结果列表
+     */
+    List<Map<String, Object>> executeDynamicQuery(@Param("sql") String sql, @Param("params") Map<String, Object> params);
+
+    /**
+     * 执行动态计数查询
+     *
+     * @param sql    SQL语句
+     * @param params 参数
+     * @return 计数结果
+     */
+    Long executeDynamicCount(@Param("sql") String sql, @Param("params") Map<String, Object> params);
+
+    /**
+     * 执行动态汇总查询
+     *
+     * @param sql    SQL语句
+     * @param params 参数
+     * @return 汇总结果
+     */
+    Map<String, Object> executeDynamicSummary(@Param("sql") String sql, @Param("params") Map<String, Object> params);
+}

+ 144 - 0
ems/ems-core/src/main/java/com/ruoyi/ems/model/ReportQueryResult.java

@@ -0,0 +1,144 @@
+/*
+ * 文 件 名:  ReportQueryResult
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2026/1/30
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.model;
+
+import com.ruoyi.ems.domain.ReportField;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 报表查询结果对象
+ *
+ * @author ruoyi
+ * @date 2026-01-30
+ */
+@Data
+@ApiModel("报表查询结果")
+public class ReportQueryResult {
+    private static final long serialVersionUID = 1L;
+
+    /** 返回码 */
+    @ApiModelProperty("返回码")
+    private Integer code;
+
+    /** 提示消息 */
+    @ApiModelProperty("提示消息")
+    private String msg;
+
+    /** 总记录数 */
+    @ApiModelProperty("总记录数")
+    private Long total;
+
+    /** 数据列表 */
+    @ApiModelProperty("数据列表")
+    private List<Map<String, Object>> rows;
+
+    /** 列定义(用于前端表格渲染) */
+    @ApiModelProperty("列定义")
+    private List<ColumnDefine> columns;
+
+    /** 汇总数据(聚合查询时返回) */
+    @ApiModelProperty("汇总数据")
+    private Map<String, Object> summary;
+
+    /** 执行的SQL(仅开发调试时返回) */
+    @ApiModelProperty("执行的SQL")
+    private String sql;
+
+    /**
+     * 列定义
+     */
+    @Data
+    @ApiModel("列定义")
+    public static class ColumnDefine {
+        /** 字段编码 */
+        @ApiModelProperty("字段编码")
+        private String prop;
+
+        /** 显示名称 */
+        @ApiModelProperty("显示名称")
+        private String label;
+
+        /** 字段类型 */
+        @ApiModelProperty("字段类型")
+        private String type;
+
+        /** 宽度 */
+        @ApiModelProperty("宽度")
+        private Integer width;
+
+        /** 最小宽度 */
+        @ApiModelProperty("最小宽度")
+        private Integer minWidth;
+
+        /** 小数位数 */
+        @ApiModelProperty("小数位数")
+        private Integer decimals;
+
+        /** 单位 */
+        @ApiModelProperty("单位")
+        private String unit;
+
+        /** 显示格式 */
+        @ApiModelProperty("显示格式")
+        private String format;
+
+        /** 是否可排序 */
+        @ApiModelProperty("是否可排序")
+        private Boolean sortable;
+
+        /** 是否显示千分位 */
+        @ApiModelProperty("是否显示千分位")
+        private Boolean showThousands;
+
+        public ColumnDefine() {}
+
+        public ColumnDefine(ReportField field) {
+            this.prop = field.getFieldAlias() != null ? field.getFieldAlias() : field.getFieldCode();
+            this.label = field.getFieldName();
+            this.type = field.getFieldType();
+            this.width = field.getWidth();
+            this.minWidth = field.getMinWidth();
+            this.decimals = field.getDecimals();
+            this.unit = field.getUnit();
+            this.format = field.getFieldFormat();
+            this.sortable = field.getIsSortable() != null && field.getIsSortable() == 1;
+            this.showThousands = "number".equals(field.getFieldType());
+        }
+    }
+
+    /**
+     * 成功
+     */
+    public static ReportQueryResult success(List<Map<String, Object>> rows, Long total, List<ColumnDefine> columns) {
+        ReportQueryResult result = new ReportQueryResult();
+        result.setCode(200);
+        result.setMsg("查询成功");
+        result.setRows(rows);
+        result.setTotal(total);
+        result.setColumns(columns);
+        return result;
+    }
+
+    /**
+     * 失败
+     */
+    public static ReportQueryResult error(String msg) {
+        ReportQueryResult result = new ReportQueryResult();
+        result.setCode(500);
+        result.setMsg(msg);
+        return result;
+    }
+}

+ 246 - 0
ems/ems-core/src/main/java/com/ruoyi/ems/service/ICustomReportService.java

@@ -0,0 +1,246 @@
+/*
+ * 文 件 名:  ICustomReportService
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2026/1/30
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.service;
+
+import com.ruoyi.ems.domain.ReportDatasource;
+import com.ruoyi.ems.domain.ReportField;
+import com.ruoyi.ems.domain.ReportFieldCondition;
+import com.ruoyi.ems.domain.ReportQueryConfig;
+import com.ruoyi.ems.domain.ReportRelation;
+import com.ruoyi.ems.domain.ReportTemplate;
+import com.ruoyi.ems.model.ReportQueryResult;
+
+import javax.servlet.http.HttpServletResponse;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 自定义报表Service接口
+ *
+ * @author ruoyi
+ * @date 2026-01-30
+ */
+public interface ICustomReportService {
+
+    // ==================== 数据源管理 ====================
+
+    /**
+     * 查询数据源列表
+     *
+     * @param category 分类筛选(可选)
+     * @return 数据源列表
+     */
+    List<ReportDatasource> selectDatasourceList(String category);
+
+    /**
+     * 根据编码查询数据源
+     *
+     * @param dsCode 数据源编码
+     * @return 数据源
+     */
+    ReportDatasource selectDatasourceByCode(String dsCode);
+
+    /**
+     * 获取数据源详情(包含字段、条件、关联配置)
+     *
+     * @param dsCode 数据源编码
+     * @return 数据源详情
+     */
+    ReportDatasource selectDatasourceDetail(String dsCode);
+
+    /**
+     * 新增数据源
+     *
+     * @param datasource 数据源
+     * @return 结果
+     */
+    int insertDatasource(ReportDatasource datasource);
+
+    /**
+     * 修改数据源
+     *
+     * @param datasource 数据源
+     * @return 结果
+     */
+    int updateDatasource(ReportDatasource datasource);
+
+    /**
+     * 删除数据源
+     *
+     * @param dsCode 数据源编码
+     * @return 结果
+     */
+    int deleteDatasource(String dsCode);
+
+    // ==================== 字段管理 ====================
+
+    /**
+     * 查询数据源的字段列表
+     *
+     * @param dsCode 数据源编码
+     * @return 字段列表
+     */
+    List<ReportField> selectFieldsByDsCode(String dsCode);
+
+    /**
+     * 查询字段的可用条件
+     *
+     * @param dsCode    数据源编码
+     * @param fieldCode 字段编码
+     * @return 条件列表
+     */
+    List<ReportFieldCondition> selectFieldConditions(String dsCode, String fieldCode);
+
+    /**
+     * 新增字段配置
+     *
+     * @param field 字段
+     * @return 结果
+     */
+    int insertField(ReportField field);
+
+    /**
+     * 批量新增字段配置
+     *
+     * @param fields 字段列表
+     * @return 结果
+     */
+    int insertFieldBatch(List<ReportField> fields);
+
+    /**
+     * 修改字段配置
+     *
+     * @param field 字段
+     * @return 结果
+     */
+    int updateField(ReportField field);
+
+    /**
+     * 删除字段配置
+     *
+     * @param dsCode    数据源编码
+     * @param fieldCode 字段编码
+     * @return 结果
+     */
+    int deleteField(String dsCode, String fieldCode);
+
+    // ==================== 关联管理 ====================
+
+    /**
+     * 查询数据源的关联配置
+     *
+     * @param dsCode 数据源编码
+     * @return 关联列表
+     */
+    List<ReportRelation> selectRelationsByDsCode(String dsCode);
+
+    /**
+     * 新增关联配置
+     *
+     * @param relation 关联
+     * @return 结果
+     */
+    int insertRelation(ReportRelation relation);
+
+    /**
+     * 修改关联配置
+     *
+     * @param relation 关联
+     * @return 结果
+     */
+    int updateRelation(ReportRelation relation);
+
+    /**
+     * 删除关联配置
+     *
+     * @param dsCode       数据源编码
+     * @param relationCode 关联编码
+     * @return 结果
+     */
+    int deleteRelation(String dsCode, String relationCode);
+
+    // ==================== 模板管理 ====================
+
+    /**
+     * 查询模板列表
+     *
+     * @param query 查询条件
+     * @return 模板列表
+     */
+    List<ReportTemplate> selectTemplateList(ReportTemplate query);
+
+    /**
+     * 根据编码查询模板
+     *
+     * @param templateCode 模板编码
+     * @return 模板
+     */
+    ReportTemplate selectTemplateByCode(String templateCode);
+
+    /**
+     * 保存模板
+     *
+     * @param template 模板
+     * @return 结果
+     */
+    int saveTemplate(ReportTemplate template);
+
+    /**
+     * 更新模板
+     *
+     * @param template 模板
+     * @return 结果
+     */
+    int updateTemplate(ReportTemplate template);
+
+    /**
+     * 删除模板
+     *
+     * @param templateCode 模板编码
+     * @return 结果
+     */
+    int deleteTemplate(String templateCode);
+
+    /**
+     * 增加模板使用次数
+     *
+     * @param templateCode 模板编码
+     * @return 结果
+     */
+    int incrementTemplateUseCount(String templateCode);
+
+    // ==================== 动态查询执行 ====================
+
+    /**
+     * 执行报表查询
+     *
+     * @param queryConfig 查询配置
+     * @return 查询结果
+     */
+    ReportQueryResult executeQuery(ReportQueryConfig queryConfig);
+
+    /**
+     * 执行汇总查询
+     *
+     * @param queryConfig 查询配置
+     * @return 汇总数据
+     */
+    Map<String, Object> executeSummaryQuery(ReportQueryConfig queryConfig);
+
+    /**
+     * 导出到Excel
+     *
+     * @param response 响应对象
+     * @param result   查询结果
+     * @param fileName 文件名
+     */
+    void exportToExcel(HttpServletResponse response, ReportQueryResult result, String fileName);
+}

+ 742 - 0
ems/ems-core/src/main/java/com/ruoyi/ems/service/impl/CustomReportServiceImpl.java

@@ -0,0 +1,742 @@
+package com.ruoyi.ems.service.impl;
+
+import com.alibaba.fastjson2.JSON;
+import com.ruoyi.ems.domain.ReportDatasource;
+import com.ruoyi.ems.domain.ReportField;
+import com.ruoyi.ems.domain.ReportFieldCondition;
+import com.ruoyi.ems.domain.ReportQueryConfig;
+import com.ruoyi.ems.domain.ReportRelation;
+import com.ruoyi.ems.domain.ReportTemplate;
+import com.ruoyi.ems.mapper.CustomReportMapper;
+import com.ruoyi.ems.model.ReportQueryResult;
+import com.ruoyi.ems.service.ICustomReportService;
+import com.ruoyi.ems.util.DynamicSqlBuilder;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.poi.ss.usermodel.BorderStyle;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.CellStyle;
+import org.apache.poi.ss.usermodel.FillPatternType;
+import org.apache.poi.ss.usermodel.Font;
+import org.apache.poi.ss.usermodel.HorizontalAlignment;
+import org.apache.poi.ss.usermodel.IndexedColors;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.OutputStream;
+import java.net.URLEncoder;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 自定义报表Service实现
+ *
+ * @author ruoyi
+ * @date 2026-01-30
+ */
+@Service
+public class CustomReportServiceImpl implements ICustomReportService {
+
+    private static final Logger logger = LoggerFactory.getLogger(CustomReportServiceImpl.class);
+
+    @Autowired
+    private CustomReportMapper reportMapper;
+
+    // ==================== 数据源管理 ====================
+
+    @Override
+    public List<ReportDatasource> selectDatasourceList(String category) {
+        return reportMapper.selectDatasourceList(category);
+    }
+
+    @Override
+    public ReportDatasource selectDatasourceByCode(String dsCode) {
+        if (StringUtils.isBlank(dsCode)) {
+            return null;
+        }
+        return reportMapper.selectDatasourceByCode(dsCode);
+    }
+
+    @Override
+    public ReportDatasource selectDatasourceDetail(String dsCode) {
+        if (StringUtils.isBlank(dsCode)) {
+            return null;
+        }
+
+        ReportDatasource datasource = reportMapper.selectDatasourceByCode(dsCode);
+        if (datasource == null) {
+            return null;
+        }
+
+        // 加载字段列表
+        List<ReportField> fields = reportMapper.selectFieldsByDsCode(dsCode);
+        datasource.setFields(fields);
+
+        // 加载字段条件
+        if (CollectionUtils.isNotEmpty(fields)) {
+            List<ReportFieldCondition> allConditions = reportMapper.selectAllFieldConditions(dsCode);
+            Map<String, List<ReportFieldCondition>> conditionMap = allConditions.stream()
+                .collect(Collectors.groupingBy(ReportFieldCondition::getFieldCode));
+
+            // 加载字段关联
+            List<Map<String, String>> allRelations = reportMapper.selectAllFieldRelations(dsCode);
+            Map<String, List<String>> fieldRelationMap = new HashMap<>();
+            for (Map<String, String> rel : allRelations) {
+                String fieldCode = rel.get("fieldCode");
+                String relationCode = rel.get("relationCode");
+                fieldRelationMap.computeIfAbsent(fieldCode, k -> new ArrayList<>()).add(relationCode);
+            }
+
+            for (ReportField field : fields) {
+                field.setConditions(conditionMap.getOrDefault(field.getFieldCode(), Collections.emptyList()));
+                field.setRelationCodes(fieldRelationMap.getOrDefault(field.getFieldCode(), Collections.emptyList()));
+            }
+        }
+
+        // 加载关联配置
+        List<ReportRelation> relations = reportMapper.selectRelationsByDsCode(dsCode);
+        datasource.setRelations(relations);
+
+        return datasource;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int insertDatasource(ReportDatasource datasource) {
+        if (datasource == null || StringUtils.isBlank(datasource.getDsCode())) {
+            return 0;
+        }
+
+        // 检查编码是否存在
+        ReportDatasource existing = reportMapper.selectDatasourceByCode(datasource.getDsCode());
+        if (existing != null) {
+            throw new RuntimeException("数据源编码已存在: " + datasource.getDsCode());
+        }
+
+        // 设置默认值
+        if (datasource.getMainAlias() == null) {
+            datasource.setMainAlias("t");
+        }
+        if (datasource.getStatus() == null) {
+            datasource.setStatus(1);
+        }
+        if (datasource.getSortOrder() == null) {
+            datasource.setSortOrder(0);
+        }
+
+        return reportMapper.insertDatasource(datasource);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int updateDatasource(ReportDatasource datasource) {
+        if (datasource == null || StringUtils.isBlank(datasource.getDsCode())) {
+            return 0;
+        }
+        return reportMapper.updateDatasource(datasource);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int deleteDatasource(String dsCode) {
+        if (StringUtils.isBlank(dsCode)) {
+            return 0;
+        }
+
+        // 删除关联数据
+        reportMapper.deleteFieldConditions(dsCode, null);
+        reportMapper.deleteFieldRelations(dsCode, null);
+        reportMapper.deleteFieldsByDsCode(dsCode);
+        reportMapper.deleteRelationsByDsCode(dsCode);
+
+        return reportMapper.deleteDatasource(dsCode);
+    }
+
+    // ==================== 字段管理 ====================
+
+    @Override
+    public List<ReportField> selectFieldsByDsCode(String dsCode) {
+        if (StringUtils.isBlank(dsCode)) {
+            return Collections.emptyList();
+        }
+        return reportMapper.selectFieldsByDsCode(dsCode);
+    }
+
+    @Override
+    public List<ReportFieldCondition> selectFieldConditions(String dsCode, String fieldCode) {
+        if (StringUtils.isBlank(dsCode) || StringUtils.isBlank(fieldCode)) {
+            return Collections.emptyList();
+        }
+        return reportMapper.selectFieldConditions(dsCode, fieldCode);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int insertField(ReportField field) {
+        if (field == null || StringUtils.isBlank(field.getDsCode()) || StringUtils.isBlank(field.getFieldCode())) {
+            return 0;
+        }
+
+        // 设置默认值
+        if (field.getStatus() == null) {
+            field.setStatus(1);
+        }
+        if (field.getIsDefault() == null) {
+            field.setIsDefault(0);
+        }
+        if (field.getIsFilterable() == null) {
+            field.setIsFilterable(1);
+        }
+        if (field.getIsSortable() == null) {
+            field.setIsSortable(1);
+        }
+        if (field.getIsAggregatable() == null) {
+            field.setIsAggregatable(0);
+        }
+
+        return reportMapper.insertField(field);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int insertFieldBatch(List<ReportField> fields) {
+        if (CollectionUtils.isEmpty(fields)) {
+            return 0;
+        }
+
+        for (ReportField field : fields) {
+            if (field.getStatus() == null) {
+                field.setStatus(1);
+            }
+            if (field.getIsDefault() == null) {
+                field.setIsDefault(0);
+            }
+        }
+
+        return reportMapper.insertFieldBatch(fields);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int updateField(ReportField field) {
+        if (field == null || StringUtils.isBlank(field.getDsCode()) || StringUtils.isBlank(field.getFieldCode())) {
+            return 0;
+        }
+        return reportMapper.updateField(field);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int deleteField(String dsCode, String fieldCode) {
+        if (StringUtils.isBlank(dsCode) || StringUtils.isBlank(fieldCode)) {
+            return 0;
+        }
+
+        // 删除字段条件
+        reportMapper.deleteFieldConditions(dsCode, fieldCode);
+        // 删除字段关联映射
+        reportMapper.deleteFieldRelations(dsCode, fieldCode);
+        // 删除字段
+        return reportMapper.deleteField(dsCode, fieldCode);
+    }
+
+    // ==================== 关联管理 ====================
+
+    @Override
+    public List<ReportRelation> selectRelationsByDsCode(String dsCode) {
+        if (StringUtils.isBlank(dsCode)) {
+            return Collections.emptyList();
+        }
+        return reportMapper.selectRelationsByDsCode(dsCode);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int insertRelation(ReportRelation relation) {
+        if (relation == null || StringUtils.isBlank(relation.getDsCode()) || StringUtils.isBlank(relation.getRelationCode())) {
+            return 0;
+        }
+
+        if (relation.getJoinType() == null) {
+            relation.setJoinType("LEFT");
+        }
+        if (relation.getIsAutoJoin() == null) {
+            relation.setIsAutoJoin(1);
+        }
+        if (relation.getStatus() == null) {
+            relation.setStatus(1);
+        }
+
+        return reportMapper.insertRelation(relation);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int updateRelation(ReportRelation relation) {
+        if (relation == null || StringUtils.isBlank(relation.getDsCode()) || StringUtils.isBlank(relation.getRelationCode())) {
+            return 0;
+        }
+        return reportMapper.updateRelation(relation);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int deleteRelation(String dsCode, String relationCode) {
+        if (StringUtils.isBlank(dsCode) || StringUtils.isBlank(relationCode)) {
+            return 0;
+        }
+        return reportMapper.deleteRelation(dsCode, relationCode);
+    }
+
+    // ==================== 模板管理 ====================
+
+    @Override
+    public List<ReportTemplate> selectTemplateList(ReportTemplate query) {
+        return reportMapper.selectTemplateList(query);
+    }
+
+    @Override
+    public ReportTemplate selectTemplateByCode(String templateCode) {
+        if (StringUtils.isBlank(templateCode)) {
+            return null;
+        }
+
+        ReportTemplate template = reportMapper.selectTemplateByCode(templateCode);
+        if (template != null && StringUtils.isNotBlank(template.getConfigJson())) {
+            try {
+                ReportQueryConfig config = JSON.parseObject(template.getConfigJson(), ReportQueryConfig.class);
+                template.setQueryConfig(config);
+            } catch (Exception e) {
+                logger.warn("解析模板配置JSON失败: {}", e.getMessage());
+            }
+        }
+        return template;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int saveTemplate(ReportTemplate template) {
+        if (template == null || StringUtils.isBlank(template.getDsCode())) {
+            return 0;
+        }
+
+        // 生成模板编码
+        if (StringUtils.isBlank(template.getTemplateCode())) {
+            template.setTemplateCode("tpl_" + System.currentTimeMillis());
+        }
+
+        // 检查编码是否存在
+        int count = reportMapper.checkTemplateCodeExists(template.getTemplateCode());
+        if (count > 0) {
+            throw new RuntimeException("模板编码已存在: " + template.getTemplateCode());
+        }
+
+        // 设置默认值
+        if (template.getStatus() == null) {
+            template.setStatus(1);
+        }
+        if (template.getIsPublic() == null) {
+            template.setIsPublic(0);
+        }
+        if (template.getIsSystem() == null) {
+            template.setIsSystem(0);
+        }
+
+        return reportMapper.insertTemplate(template);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int updateTemplate(ReportTemplate template) {
+        if (template == null || StringUtils.isBlank(template.getTemplateCode())) {
+            return 0;
+        }
+        return reportMapper.updateTemplate(template);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int deleteTemplate(String templateCode) {
+        if (StringUtils.isBlank(templateCode)) {
+            return 0;
+        }
+        return reportMapper.deleteTemplate(templateCode);
+    }
+
+    @Override
+    public int incrementTemplateUseCount(String templateCode) {
+        if (StringUtils.isBlank(templateCode)) {
+            return 0;
+        }
+        return reportMapper.incrementTemplateUseCount(templateCode);
+    }
+
+    // ==================== 动态查询执行 ====================
+
+    @Override
+    public ReportQueryResult executeQuery(ReportQueryConfig queryConfig) {
+        try {
+            // 1. 解析配置
+            ReportQueryConfig effectiveConfig = resolveQueryConfig(queryConfig);
+            if (effectiveConfig == null) {
+                return ReportQueryResult.error("无法解析查询配置");
+            }
+
+            String dsCode = effectiveConfig.getDsCode();
+            if (StringUtils.isBlank(dsCode)) {
+                return ReportQueryResult.error("数据源编码不能为空");
+            }
+
+            // 2. 加载数据源配置
+            ReportDatasource datasource = selectDatasourceDetail(dsCode);
+            if (datasource == null) {
+                return ReportQueryResult.error("数据源不存在: " + dsCode);
+            }
+
+            // 3. 构建字段Map和关联Map
+            Map<String, ReportField> fieldMap = datasource.getFields().stream()
+                .collect(Collectors.toMap(ReportField::getFieldCode, Function.identity()));
+
+            Map<String, ReportRelation> relationMap = datasource.getRelations().stream()
+                .collect(Collectors.toMap(ReportRelation::getRelationCode, Function.identity()));
+
+            // 构建字段关联映射
+            Map<String, List<String>> fieldRelationMap = new HashMap<>();
+            for (ReportField field : datasource.getFields()) {
+                if (CollectionUtils.isNotEmpty(field.getRelationCodes())) {
+                    fieldRelationMap.put(field.getFieldCode(), field.getRelationCodes());
+                }
+            }
+
+            // 4. 构建SQL
+            DynamicSqlBuilder.BuildResult buildResult = DynamicSqlBuilder.buildQuerySql(
+                datasource, fieldMap, relationMap, fieldRelationMap, effectiveConfig);
+
+            if (!buildResult.isSuccess()) {
+                return ReportQueryResult.error(buildResult.getErrorMsg());
+            }
+
+            // 5. 执行查询
+            Map<String, Object> execParams = new HashMap<>(buildResult.getParams());
+
+            // 处理分页
+            if (!Boolean.TRUE.equals(effectiveConfig.getExportAll())) {
+                int pageNum = effectiveConfig.getPageNum() != null ? effectiveConfig.getPageNum() : 1;
+                int pageSize = effectiveConfig.getPageSize() != null ? effectiveConfig.getPageSize() : 20;
+                execParams.put("pageNum", pageNum);
+                execParams.put("pageSize", pageSize);
+                execParams.put("offset", (pageNum - 1) * pageSize);
+            }
+
+            logger.info("执行报表查询SQL: {}", buildResult.getSql());
+            logger.info("查询参数: {}", execParams);
+
+            // 执行数据查询
+            List<Map<String, Object>> rows = reportMapper.executeDynamicQuery(buildResult.getSql(), execParams);
+
+            // 执行计数查询
+            Long total = 0L;
+            if (!Boolean.TRUE.equals(effectiveConfig.getExportAll())) {
+                total = reportMapper.executeDynamicCount(buildResult.getCountSql(), execParams);
+            } else {
+                total = rows != null ? (long) rows.size() : 0L;
+            }
+
+            // 6. 构建结果
+            ReportQueryResult result = ReportQueryResult.success(
+                rows != null ? rows : Collections.emptyList(),
+                total,
+                buildResult.getColumns()
+            );
+
+            // 开发环境返回SQL(生产环境应该移除)
+            result.setSql(buildResult.getSql());
+
+            return result;
+
+        } catch (Exception e) {
+            logger.error("执行报表查询异常", e);
+            return ReportQueryResult.error("查询异常: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public Map<String, Object> executeSummaryQuery(ReportQueryConfig queryConfig) {
+        try {
+            // 1. 解析配置
+            ReportQueryConfig effectiveConfig = resolveQueryConfig(queryConfig);
+            if (effectiveConfig == null) {
+                logger.warn("无法解析查询配置");
+                return Collections.emptyMap();
+            }
+
+            String dsCode = effectiveConfig.getDsCode();
+            if (StringUtils.isBlank(dsCode)) {
+                logger.warn("数据源编码为空");
+                return Collections.emptyMap();
+            }
+
+            // 2. 加载数据源配置
+            ReportDatasource datasource = selectDatasourceDetail(dsCode);
+            if (datasource == null) {
+                logger.warn("数据源不存在: {}", dsCode);
+                return Collections.emptyMap();
+            }
+
+            // 3. 构建字段Map和关联Map
+            Map<String, ReportField> fieldMap = datasource.getFields().stream()
+                .collect(Collectors.toMap(ReportField::getFieldCode, Function.identity()));
+
+            Map<String, ReportRelation> relationMap = datasource.getRelations().stream()
+                .collect(Collectors.toMap(ReportRelation::getRelationCode, Function.identity()));
+
+            Map<String, List<String>> fieldRelationMap = new HashMap<>();
+            for (ReportField field : datasource.getFields()) {
+                if (CollectionUtils.isNotEmpty(field.getRelationCodes())) {
+                    fieldRelationMap.put(field.getFieldCode(), field.getRelationCodes());
+                }
+            }
+
+            // 4. 构建汇总SQL
+            DynamicSqlBuilder.BuildResult buildResult = DynamicSqlBuilder.buildSummarySql(
+                datasource, fieldMap, relationMap, fieldRelationMap, effectiveConfig);
+
+            if (!buildResult.isSuccess()) {
+                logger.warn("构建汇总SQL失败: {}", buildResult.getErrorMsg());
+                return Collections.emptyMap();
+            }
+
+            // 5. 执行查询
+            logger.info("执行汇总查询SQL: {}", buildResult.getSql());
+
+            Map<String, Object> summary = reportMapper.executeDynamicSummary(buildResult.getSql(), buildResult.getParams());
+            return summary != null ? summary : Collections.emptyMap();
+
+        } catch (Exception e) {
+            logger.error("执行汇总查询异常", e);
+            return Collections.emptyMap();
+        }
+    }
+
+    @Override
+    public void exportToExcel(HttpServletResponse response, ReportQueryResult result, String fileName) {
+        if (result == null || CollectionUtils.isEmpty(result.getRows())) {
+            logger.warn("导出数据为空");
+            return;
+        }
+
+        try (Workbook workbook = new XSSFWorkbook()) {
+            Sheet sheet = workbook.createSheet("报表数据");
+
+            // 创建表头样式
+            CellStyle headerStyle = workbook.createCellStyle();
+            headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
+            headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+            headerStyle.setAlignment(HorizontalAlignment.CENTER);
+            headerStyle.setBorderBottom(BorderStyle.THIN);
+            headerStyle.setBorderTop(BorderStyle.THIN);
+            headerStyle.setBorderLeft(BorderStyle.THIN);
+            headerStyle.setBorderRight(BorderStyle.THIN);
+
+            Font headerFont = workbook.createFont();
+            headerFont.setBold(true);
+            headerStyle.setFont(headerFont);
+
+            // 创建数据样式
+            CellStyle dataStyle = workbook.createCellStyle();
+            dataStyle.setAlignment(HorizontalAlignment.CENTER);
+            dataStyle.setBorderBottom(BorderStyle.THIN);
+            dataStyle.setBorderTop(BorderStyle.THIN);
+            dataStyle.setBorderLeft(BorderStyle.THIN);
+            dataStyle.setBorderRight(BorderStyle.THIN);
+
+            // 数值样式
+            CellStyle numberStyle = workbook.createCellStyle();
+            numberStyle.cloneStyleFrom(dataStyle);
+            numberStyle.setAlignment(HorizontalAlignment.RIGHT);
+
+            List<ReportQueryResult.ColumnDefine> columns = result.getColumns();
+            List<Map<String, Object>> rows = result.getRows();
+
+            // 创建表头
+            Row headerRow = sheet.createRow(0);
+            for (int i = 0; i < columns.size(); i++) {
+                Cell cell = headerRow.createCell(i);
+                ReportQueryResult.ColumnDefine col = columns.get(i);
+                String label = col.getLabel();
+                if (StringUtils.isNotBlank(col.getUnit())) {
+                    label += "(" + col.getUnit() + ")";
+                }
+                cell.setCellValue(label);
+                cell.setCellStyle(headerStyle);
+
+                // 设置列宽
+                int width = col.getMinWidth() != null ? col.getMinWidth() : 100;
+                sheet.setColumnWidth(i, width * 40);
+            }
+
+            // 填充数据
+            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
+            SimpleDateFormat datetimeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+
+            for (int rowIdx = 0; rowIdx < rows.size(); rowIdx++) {
+                Row dataRow = sheet.createRow(rowIdx + 1);
+                Map<String, Object> rowData = rows.get(rowIdx);
+
+                for (int colIdx = 0; colIdx < columns.size(); colIdx++) {
+                    Cell cell = dataRow.createCell(colIdx);
+                    ReportQueryResult.ColumnDefine col = columns.get(colIdx);
+                    Object value = rowData.get(col.getProp());
+
+                    if (value == null) {
+                        cell.setCellValue("");
+                        cell.setCellStyle(dataStyle);
+                    } else if ("number".equals(col.getType())) {
+                        if (value instanceof Number) {
+                            cell.setCellValue(((Number) value).doubleValue());
+                        } else {
+                            cell.setCellValue(value.toString());
+                        }
+                        cell.setCellStyle(numberStyle);
+                    } else if ("date".equals(col.getType()) && value instanceof Date) {
+                        cell.setCellValue(dateFormat.format((Date) value));
+                        cell.setCellStyle(dataStyle);
+                    } else if ("datetime".equals(col.getType()) && value instanceof Date) {
+                        cell.setCellValue(datetimeFormat.format((Date) value));
+                        cell.setCellStyle(dataStyle);
+                    } else {
+                        cell.setCellValue(value.toString());
+                        cell.setCellStyle(dataStyle);
+                    }
+                }
+            }
+
+            // 输出文件
+            String exportFileName = StringUtils.isNotBlank(fileName) ? fileName : "报表";
+            exportFileName = exportFileName + "_" + System.currentTimeMillis() + ".xlsx";
+
+            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+            response.setCharacterEncoding("utf-8");
+            response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(exportFileName, "UTF-8"));
+
+            OutputStream outputStream = response.getOutputStream();
+            workbook.write(outputStream);
+            outputStream.flush();
+
+            logger.info("导出Excel成功: {}, 行数: {}", exportFileName, rows.size());
+
+        } catch (Exception e) {
+            logger.error("导出Excel异常", e);
+            throw new RuntimeException("导出失败: " + e.getMessage());
+        }
+    }
+
+    // ==================== 私有辅助方法 ====================
+
+    /**
+     * 解析查询配置(处理模板和变量替换)
+     */
+    private ReportQueryConfig resolveQueryConfig(ReportQueryConfig queryConfig) {
+        if (queryConfig == null) {
+            return null;
+        }
+
+        // 如果指定了模板,加载模板配置
+        if (StringUtils.isNotBlank(queryConfig.getTemplateCode())) {
+            ReportTemplate template = selectTemplateByCode(queryConfig.getTemplateCode());
+            if (template == null) {
+                logger.warn("模板不存在: {}", queryConfig.getTemplateCode());
+                return null;
+            }
+
+            ReportQueryConfig templateConfig = template.getQueryConfig();
+            if (templateConfig == null) {
+                logger.warn("模板配置解析失败: {}", queryConfig.getTemplateCode());
+                return null;
+            }
+
+            // 合并配置:用户传入的参数覆盖模板默认值
+            mergeQueryConfig(templateConfig, queryConfig);
+            return templateConfig;
+        }
+
+        return queryConfig;
+    }
+
+    /**
+     * 合并查询配置
+     */
+    private void mergeQueryConfig(ReportQueryConfig target, ReportQueryConfig source) {
+        // 合并数据源
+        if (StringUtils.isNotBlank(source.getDsCode())) {
+            target.setDsCode(source.getDsCode());
+        }
+
+        // 合并字段选择
+        if (CollectionUtils.isNotEmpty(source.getSelectedFields())) {
+            target.setSelectedFields(source.getSelectedFields());
+        }
+
+        // 合并分页
+        if (source.getPageNum() != null) {
+            target.setPageNum(source.getPageNum());
+        }
+        if (source.getPageSize() != null) {
+            target.setPageSize(source.getPageSize());
+        }
+
+        // 合并导出标志
+        if (source.getExportAll() != null) {
+            target.setExportAll(source.getExportAll());
+        }
+
+        // 合并参数值(用于模板变量替换)
+        if (source.getParams() != null) {
+            if (target.getParams() == null) {
+                target.setParams(new HashMap<>());
+            }
+            target.getParams().putAll(source.getParams());
+        }
+
+        // 处理条件中的模板变量替换
+        if (CollectionUtils.isNotEmpty(target.getConditions()) && source.getParams() != null) {
+            for (ReportQueryConfig.QueryCondition condition : target.getConditions()) {
+                if (condition.getValue() instanceof String) {
+                    String strValue = (String) condition.getValue();
+                    if (strValue.startsWith("${") && strValue.endsWith("}")) {
+                        String varName = strValue.substring(2, strValue.length() - 1);
+                        Object replacement = source.getParams().get(varName);
+                        if (replacement != null) {
+                            condition.setValue(replacement);
+                        }
+                    }
+                }
+                if (condition.getValue2() instanceof String) {
+                    String strValue = (String) condition.getValue2();
+                    if (strValue.startsWith("${") && strValue.endsWith("}")) {
+                        String varName = strValue.substring(2, strValue.length() - 1);
+                        Object replacement = source.getParams().get(varName);
+                        if (replacement != null) {
+                            condition.setValue2(replacement);
+                        }
+                    }
+                }
+            }
+        }
+    }
+}

+ 714 - 0
ems/ems-core/src/main/java/com/ruoyi/ems/util/DynamicSqlBuilder.java

@@ -0,0 +1,714 @@
+/*
+ * 文 件 名:  DynamicSqlBuilder
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2026/1/30
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.util;
+
+import com.ruoyi.ems.domain.ReportDatasource;
+import com.ruoyi.ems.domain.ReportField;
+import com.ruoyi.ems.domain.ReportQueryConfig;
+import com.ruoyi.ems.domain.ReportRelation;
+import com.ruoyi.ems.model.ReportQueryResult;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/**
+ * 动态SQL构建器
+ * 根据报表配置动态生成安全的SQL语句
+ *
+ * @author ruoyi
+ * @date 2026-01-30
+ */
+public class DynamicSqlBuilder {
+
+    private static final Logger logger = LoggerFactory.getLogger(DynamicSqlBuilder.class);
+
+    /** SQL注入检测正则 */
+    private static final Pattern SQL_INJECTION_PATTERN = Pattern.compile(
+        "(?i)(\\b(select|insert|update|delete|drop|truncate|alter|create|exec|execute|xp_|sp_|0x)\\b)|" +
+            "(--|#|/\\*|\\*/|;|'|\")"
+    );
+
+    /** 变量占位符正则 ${xxx} */
+    private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\$\\{(\\w+)}");
+
+    /** 支持的操作符 */
+    private static final Set<String> VALID_OPERATORS = new HashSet<>(Arrays.asList(
+        "eq", "ne", "gt", "gte", "lt", "lte", "like", "notLike", "in", "notIn", "between", "isNull", "isNotNull"
+    ));
+
+    /** 支持的聚合函数 */
+    private static final Set<String> VALID_AGGREGATE_FUNCS = new HashSet<>(Arrays.asList(
+        "SUM", "AVG", "MAX", "MIN", "COUNT"
+    ));
+
+    /** 支持的排序方向 */
+    private static final Set<String> VALID_ORDER_DIRECTIONS = new HashSet<>(Arrays.asList(
+        "ASC", "DESC"
+    ));
+
+    /**
+     * 构建结果对象
+     */
+    public static class BuildResult {
+        private String sql;
+        private String countSql;
+        private Map<String, Object> params;
+        private List<ReportQueryResult.ColumnDefine> columns;
+        private boolean success;
+        private String errorMsg;
+
+        public BuildResult() {
+            this.params = new HashMap<>();
+            this.columns = new ArrayList<>();
+            this.success = true;
+        }
+
+        public static BuildResult error(String msg) {
+            BuildResult result = new BuildResult();
+            result.success = false;
+            result.errorMsg = msg;
+            return result;
+        }
+
+        // Getters and Setters
+        public String getSql() { return sql; }
+        public void setSql(String sql) { this.sql = sql; }
+        public String getCountSql() { return countSql; }
+        public void setCountSql(String countSql) { this.countSql = countSql; }
+        public Map<String, Object> getParams() { return params; }
+        public void setParams(Map<String, Object> params) { this.params = params; }
+        public List<ReportQueryResult.ColumnDefine> getColumns() { return columns; }
+        public void setColumns(List<ReportQueryResult.ColumnDefine> columns) { this.columns = columns; }
+        public boolean isSuccess() { return success; }
+        public void setSuccess(boolean success) { this.success = success; }
+        public String getErrorMsg() { return errorMsg; }
+        public void setErrorMsg(String errorMsg) { this.errorMsg = errorMsg; }
+    }
+
+    /**
+     * 构建查询SQL
+     *
+     * @param datasource  数据源配置
+     * @param fields      字段配置Map(key: fieldCode)
+     * @param relations   关联配置Map(key: relationCode)
+     * @param fieldRelations 字段关联映射Map(key: fieldCode, value: relationCodes)
+     * @param queryConfig 查询配置
+     * @return 构建结果
+     */
+    public static BuildResult buildQuerySql(
+        ReportDatasource datasource,
+        Map<String, ReportField> fields,
+        Map<String, ReportRelation> relations,
+        Map<String, List<String>> fieldRelations,
+        ReportQueryConfig queryConfig) {
+
+        BuildResult result = new BuildResult();
+
+        try {
+            // 1. 验证查询配置
+            String validateError = validateQueryConfig(queryConfig, fields);
+            if (validateError != null) {
+                return BuildResult.error(validateError);
+            }
+
+            // 2. 确定需要查询的字段
+            List<String> selectedFields = queryConfig.getSelectedFields();
+            if (CollectionUtils.isEmpty(selectedFields)) {
+                // 使用默认字段
+                selectedFields = fields.values().stream()
+                    .filter(f -> f.getIsDefault() != null && f.getIsDefault() == 1)
+                    .map(ReportField::getFieldCode)
+                    .collect(Collectors.toList());
+            }
+
+            if (CollectionUtils.isEmpty(selectedFields)) {
+                return BuildResult.error("请至少选择一个字段");
+            }
+
+            // 3. 确定需要的关联
+            Set<String> requiredRelations = determineRequiredRelations(selectedFields, fieldRelations, queryConfig);
+
+            // 4. 构建SELECT子句
+            StringBuilder selectBuilder = new StringBuilder();
+            boolean hasAggregate = queryConfig.getAggregateFields() != null && !queryConfig.getAggregateFields().isEmpty();
+            List<String> groupByFields = queryConfig.getGroupBy();
+            boolean hasGroupBy = CollectionUtils.isNotEmpty(groupByFields);
+
+            for (int i = 0; i < selectedFields.size(); i++) {
+                if (i > 0) selectBuilder.append(", ");
+                String fieldCode = selectedFields.get(i);
+                ReportField field = fields.get(fieldCode);
+                if (field == null) {
+                    return BuildResult.error("字段不存在: " + fieldCode);
+                }
+
+                String selectExpr = buildSelectExpr(field, queryConfig, hasGroupBy);
+                selectBuilder.append(selectExpr);
+
+                // 构建列定义
+                result.getColumns().add(new ReportQueryResult.ColumnDefine(field));
+            }
+
+            // 5. 构建FROM子句
+            StringBuilder fromBuilder = new StringBuilder();
+            fromBuilder.append(datasource.getMainTable()).append(" ").append(datasource.getMainAlias());
+
+            // 6. 构建JOIN子句
+            StringBuilder joinBuilder = new StringBuilder();
+            for (String relationCode : requiredRelations) {
+                ReportRelation relation = relations.get(relationCode);
+                if (relation != null && relation.getStatus() != null && relation.getStatus() == 1) {
+                    joinBuilder.append(" ")
+                        .append(relation.getJoinType()).append(" JOIN ")
+                        .append(relation.getJoinTable()).append(" ").append(relation.getJoinAlias())
+                        .append(" ON ").append(relation.getJoinCondition());
+                }
+            }
+
+            // 7. 构建WHERE子句
+            StringBuilder whereBuilder = new StringBuilder();
+            int paramIndex = 0;
+
+            // 基础条件
+            if (StringUtils.isNotBlank(datasource.getBaseWhere())) {
+                whereBuilder.append(datasource.getBaseWhere());
+            }
+
+            // 用户条件
+            if (CollectionUtils.isNotEmpty(queryConfig.getConditions())) {
+                for (ReportQueryConfig.QueryCondition condition : queryConfig.getConditions()) {
+                    // 跳过可选的空条件
+                    if (Boolean.TRUE.equals(condition.getOptional()) && isConditionValueEmpty(condition)) {
+                        continue;
+                    }
+
+                    ReportField field = fields.get(condition.getField());
+                    if (field == null) {
+                        logger.warn("条件字段不存在: {}", condition.getField());
+                        continue;
+                    }
+
+                    String conditionSql = buildConditionSql(field, condition, result.getParams(), paramIndex++);
+                    if (StringUtils.isNotBlank(conditionSql)) {
+                        if (whereBuilder.length() > 0) {
+                            String logic = StringUtils.isNotBlank(condition.getLogic()) ? condition.getLogic() : "AND";
+                            whereBuilder.append(" ").append(logic.toUpperCase()).append(" ");
+                        }
+                        whereBuilder.append(conditionSql);
+                    }
+                }
+            }
+
+            // 8. 构建GROUP BY子句
+            StringBuilder groupByBuilder = new StringBuilder();
+            if (hasGroupBy) {
+                for (int i = 0; i < groupByFields.size(); i++) {
+                    if (i > 0) groupByBuilder.append(", ");
+                    String fieldCode = groupByFields.get(i);
+                    ReportField field = fields.get(fieldCode);
+                    if (field != null) {
+                        groupByBuilder.append(field.getFieldExpr());
+                    }
+                }
+            }
+
+            // 9. 构建ORDER BY子句
+            StringBuilder orderByBuilder = new StringBuilder();
+            if (CollectionUtils.isNotEmpty(queryConfig.getOrderBy())) {
+                for (int i = 0; i < queryConfig.getOrderBy().size(); i++) {
+                    if (i > 0) orderByBuilder.append(", ");
+                    ReportQueryConfig.OrderByConfig orderBy = queryConfig.getOrderBy().get(i);
+                    ReportField field = fields.get(orderBy.getField());
+                    if (field != null) {
+                        String direction = orderBy.getDirection();
+                        if (!VALID_ORDER_DIRECTIONS.contains(direction.toUpperCase())) {
+                            direction = "ASC";
+                        }
+                        // 使用别名或字段表达式
+                        String orderExpr = field.getFieldAlias() != null ? field.getFieldAlias() : field.getFieldExpr();
+                        orderByBuilder.append(orderExpr).append(" ").append(direction.toUpperCase());
+                    }
+                }
+            } else if (StringUtils.isNotBlank(datasource.getDefaultOrder())) {
+                orderByBuilder.append(datasource.getDefaultOrder());
+            }
+
+            // 10. 组装完整SQL
+            StringBuilder sqlBuilder = new StringBuilder();
+            sqlBuilder.append("SELECT ");
+            if (Boolean.TRUE.equals(queryConfig.getDistinct())) {
+                sqlBuilder.append("DISTINCT ");
+            }
+            sqlBuilder.append(selectBuilder);
+            sqlBuilder.append(" FROM ").append(fromBuilder);
+            sqlBuilder.append(joinBuilder);
+
+            if (whereBuilder.length() > 0) {
+                sqlBuilder.append(" WHERE ").append(whereBuilder);
+            }
+
+            if (groupByBuilder.length() > 0) {
+                sqlBuilder.append(" GROUP BY ").append(groupByBuilder);
+            }
+
+            if (orderByBuilder.length() > 0) {
+                sqlBuilder.append(" ORDER BY ").append(orderByBuilder);
+            }
+
+            // 11. 构建COUNT SQL
+            StringBuilder countSqlBuilder = new StringBuilder();
+            countSqlBuilder.append("SELECT COUNT(");
+            if (hasGroupBy) {
+                countSqlBuilder.append("DISTINCT ").append(groupByBuilder);
+            } else if (Boolean.TRUE.equals(queryConfig.getDistinct())) {
+                countSqlBuilder.append("DISTINCT ").append(datasource.getMainAlias()).append(".*");
+            } else {
+                countSqlBuilder.append("1");
+            }
+            countSqlBuilder.append(") AS total");
+            countSqlBuilder.append(" FROM ").append(fromBuilder);
+            countSqlBuilder.append(joinBuilder);
+            if (whereBuilder.length() > 0) {
+                countSqlBuilder.append(" WHERE ").append(whereBuilder);
+            }
+
+            result.setSql(sqlBuilder.toString());
+            result.setCountSql(countSqlBuilder.toString());
+
+            logger.debug("Generated SQL: {}", result.getSql());
+            logger.debug("Generated Count SQL: {}", result.getCountSql());
+            logger.debug("SQL Parameters: {}", result.getParams());
+
+            return result;
+
+        } catch (Exception e) {
+            logger.error("构建SQL异常", e);
+            return BuildResult.error("构建SQL失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 构建汇总查询SQL
+     */
+    public static BuildResult buildSummarySql(
+        ReportDatasource datasource,
+        Map<String, ReportField> fields,
+        Map<String, ReportRelation> relations,
+        Map<String, List<String>> fieldRelations,
+        ReportQueryConfig queryConfig) {
+
+        BuildResult result = new BuildResult();
+
+        try {
+            // 1. 确定需要的关联
+            Set<String> requiredRelations = new HashSet<>();
+            if (CollectionUtils.isNotEmpty(queryConfig.getConditions())) {
+                for (ReportQueryConfig.QueryCondition condition : queryConfig.getConditions()) {
+                    List<String> rels = fieldRelations.get(condition.getField());
+                    if (rels != null) requiredRelations.addAll(rels);
+                }
+            }
+
+            // 2. 构建SELECT子句(只包含聚合字段)
+            StringBuilder selectBuilder = new StringBuilder();
+            Map<String, String> aggregateFields = queryConfig.getAggregateFields();
+            if (aggregateFields == null || aggregateFields.isEmpty()) {
+                // 使用可聚合字段的默认聚合函数
+                aggregateFields = new HashMap<>();
+                for (ReportField field : fields.values()) {
+                    if (field.getIsAggregatable() != null && field.getIsAggregatable() == 1
+                        && StringUtils.isNotBlank(field.getAggregateFunc())) {
+                        aggregateFields.put(field.getFieldCode(), field.getAggregateFunc());
+                    }
+                }
+            }
+
+            if (aggregateFields.isEmpty()) {
+                return BuildResult.error("没有可聚合的字段");
+            }
+
+            int idx = 0;
+            for (Map.Entry<String, String> entry : aggregateFields.entrySet()) {
+                if (idx > 0) selectBuilder.append(", ");
+                String fieldCode = entry.getKey();
+                String aggFunc = entry.getValue().toUpperCase();
+
+                if (!VALID_AGGREGATE_FUNCS.contains(aggFunc)) {
+                    logger.warn("无效的聚合函数: {}", aggFunc);
+                    continue;
+                }
+
+                ReportField field = fields.get(fieldCode);
+                if (field == null) {
+                    logger.warn("聚合字段不存在: {}", fieldCode);
+                    continue;
+                }
+
+                String alias = "sum_" + fieldCode;
+                selectBuilder.append("ROUND(").append(aggFunc).append("(COALESCE(")
+                    .append(field.getFieldExpr()).append(", 0)), 2) AS ").append(alias);
+
+                // 列定义
+                ReportQueryResult.ColumnDefine col = new ReportQueryResult.ColumnDefine(field);
+                col.setProp(alias);
+                col.setLabel(field.getFieldName() + "(" + aggFunc + ")");
+                result.getColumns().add(col);
+                idx++;
+            }
+
+            // 3. 构建FROM子句
+            StringBuilder fromBuilder = new StringBuilder();
+            fromBuilder.append(datasource.getMainTable()).append(" ").append(datasource.getMainAlias());
+
+            // 4. 构建JOIN子句
+            StringBuilder joinBuilder = new StringBuilder();
+            for (String relationCode : requiredRelations) {
+                ReportRelation relation = relations.get(relationCode);
+                if (relation != null && relation.getStatus() != null && relation.getStatus() == 1) {
+                    joinBuilder.append(" ")
+                        .append(relation.getJoinType()).append(" JOIN ")
+                        .append(relation.getJoinTable()).append(" ").append(relation.getJoinAlias())
+                        .append(" ON ").append(relation.getJoinCondition());
+                }
+            }
+
+            // 5. 构建WHERE子句
+            StringBuilder whereBuilder = new StringBuilder();
+            int paramIndex = 0;
+
+            if (StringUtils.isNotBlank(datasource.getBaseWhere())) {
+                whereBuilder.append(datasource.getBaseWhere());
+            }
+
+            if (CollectionUtils.isNotEmpty(queryConfig.getConditions())) {
+                for (ReportQueryConfig.QueryCondition condition : queryConfig.getConditions()) {
+                    if (Boolean.TRUE.equals(condition.getOptional()) && isConditionValueEmpty(condition)) {
+                        continue;
+                    }
+
+                    ReportField field = fields.get(condition.getField());
+                    if (field == null) continue;
+
+                    String conditionSql = buildConditionSql(field, condition, result.getParams(), paramIndex++);
+                    if (StringUtils.isNotBlank(conditionSql)) {
+                        if (whereBuilder.length() > 0) {
+                            String logic = StringUtils.isNotBlank(condition.getLogic()) ? condition.getLogic() : "AND";
+                            whereBuilder.append(" ").append(logic.toUpperCase()).append(" ");
+                        }
+                        whereBuilder.append(conditionSql);
+                    }
+                }
+            }
+
+            // 6. 组装SQL
+            StringBuilder sqlBuilder = new StringBuilder();
+            sqlBuilder.append("SELECT ").append(selectBuilder);
+            sqlBuilder.append(" FROM ").append(fromBuilder);
+            sqlBuilder.append(joinBuilder);
+            if (whereBuilder.length() > 0) {
+                sqlBuilder.append(" WHERE ").append(whereBuilder);
+            }
+
+            result.setSql(sqlBuilder.toString());
+            logger.debug("Generated Summary SQL: {}", result.getSql());
+
+            return result;
+
+        } catch (Exception e) {
+            logger.error("构建汇总SQL异常", e);
+            return BuildResult.error("构建汇总SQL失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 验证查询配置
+     */
+    private static String validateQueryConfig(ReportQueryConfig config, Map<String, ReportField> fields) {
+        if (config == null) {
+            return "查询配置不能为空";
+        }
+
+        // 验证字段
+        if (CollectionUtils.isNotEmpty(config.getSelectedFields())) {
+            for (String fieldCode : config.getSelectedFields()) {
+                if (hasSqlInjection(fieldCode)) {
+                    return "字段编码包含非法字符: " + fieldCode;
+                }
+                if (!fields.containsKey(fieldCode)) {
+                    return "字段不存在: " + fieldCode;
+                }
+            }
+        }
+
+        // 验证条件
+        if (CollectionUtils.isNotEmpty(config.getConditions())) {
+            for (ReportQueryConfig.QueryCondition condition : config.getConditions()) {
+                if (StringUtils.isNotBlank(condition.getOperator()) && !VALID_OPERATORS.contains(condition.getOperator())) {
+                    return "不支持的操作符: " + condition.getOperator();
+                }
+            }
+        }
+
+        // 验证排序
+        if (CollectionUtils.isNotEmpty(config.getOrderBy())) {
+            for (ReportQueryConfig.OrderByConfig orderBy : config.getOrderBy()) {
+                if (hasSqlInjection(orderBy.getField())) {
+                    return "排序字段包含非法字符: " + orderBy.getField();
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * 确定需要的关联
+     */
+    private static Set<String> determineRequiredRelations(
+        List<String> selectedFields,
+        Map<String, List<String>> fieldRelations,
+        ReportQueryConfig queryConfig) {
+
+        Set<String> required = new HashSet<>();
+
+        // 选中字段需要的关联
+        for (String fieldCode : selectedFields) {
+            List<String> rels = fieldRelations.get(fieldCode);
+            if (rels != null) required.addAll(rels);
+        }
+
+        // 条件字段需要的关联
+        if (CollectionUtils.isNotEmpty(queryConfig.getConditions())) {
+            for (ReportQueryConfig.QueryCondition condition : queryConfig.getConditions()) {
+                List<String> rels = fieldRelations.get(condition.getField());
+                if (rels != null) required.addAll(rels);
+            }
+        }
+
+        // 分组字段需要的关联
+        if (CollectionUtils.isNotEmpty(queryConfig.getGroupBy())) {
+            for (String fieldCode : queryConfig.getGroupBy()) {
+                List<String> rels = fieldRelations.get(fieldCode);
+                if (rels != null) required.addAll(rels);
+            }
+        }
+
+        // 排序字段需要的关联
+        if (CollectionUtils.isNotEmpty(queryConfig.getOrderBy())) {
+            for (ReportQueryConfig.OrderByConfig orderBy : queryConfig.getOrderBy()) {
+                List<String> rels = fieldRelations.get(orderBy.getField());
+                if (rels != null) required.addAll(rels);
+            }
+        }
+
+        return required;
+    }
+
+    /**
+     * 构建SELECT表达式
+     */
+    private static String buildSelectExpr(ReportField field, ReportQueryConfig queryConfig, boolean hasGroupBy) {
+        StringBuilder expr = new StringBuilder();
+
+        String fieldExpr = field.getFieldExpr();
+        String alias = field.getFieldAlias() != null ? field.getFieldAlias() : field.getFieldCode();
+
+        // 检查是否需要聚合
+        boolean needAggregate = false;
+        String aggFunc = null;
+
+        if (hasGroupBy && queryConfig.getAggregateFields() != null) {
+            aggFunc = queryConfig.getAggregateFields().get(field.getFieldCode());
+            if (StringUtils.isNotBlank(aggFunc) && VALID_AGGREGATE_FUNCS.contains(aggFunc.toUpperCase())) {
+                needAggregate = true;
+            }
+        }
+
+        if (needAggregate) {
+            // 数值类型聚合需要处理NULL值和小数位数
+            if ("number".equals(field.getFieldType())) {
+                int decimals = field.getDecimals() != null ? field.getDecimals() : 2;
+                expr.append("ROUND(").append(aggFunc.toUpperCase())
+                    .append("(COALESCE(").append(fieldExpr).append(", 0)), ")
+                    .append(decimals).append(")");
+            } else {
+                expr.append(aggFunc.toUpperCase()).append("(").append(fieldExpr).append(")");
+            }
+        } else {
+            // 非聚合字段
+            if ("number".equals(field.getFieldType()) && field.getDecimals() != null) {
+                expr.append("ROUND(COALESCE(").append(fieldExpr).append(", 0), ")
+                    .append(field.getDecimals()).append(")");
+            } else {
+                expr.append(fieldExpr);
+            }
+        }
+
+        expr.append(" AS ").append(alias);
+        return expr.toString();
+    }
+
+    /**
+     * 构建条件SQL
+     */
+    private static String buildConditionSql(
+        ReportField field,
+        ReportQueryConfig.QueryCondition condition,
+        Map<String, Object> params,
+        int paramIndex) {
+
+        String fieldExpr = field.getFieldExpr();
+        String operator = condition.getOperator();
+        Object value = condition.getValue();
+        Object value2 = condition.getValue2();
+
+        String paramName = "p" + paramIndex;
+
+        switch (operator.toLowerCase()) {
+            case "eq":
+                params.put(paramName, value);
+                return fieldExpr + " = #{params." + paramName + "}";
+
+            case "ne":
+                params.put(paramName, value);
+                return fieldExpr + " != #{params." + paramName + "}";
+
+            case "gt":
+                params.put(paramName, value);
+                return fieldExpr + " > #{params." + paramName + "}";
+
+            case "gte":
+                params.put(paramName, value);
+                return fieldExpr + " >= #{params." + paramName + "}";
+
+            case "lt":
+                params.put(paramName, value);
+                return fieldExpr + " < #{params." + paramName + "}";
+
+            case "lte":
+                params.put(paramName, value);
+                return fieldExpr + " <= #{params." + paramName + "}";
+
+            case "like":
+                params.put(paramName, "%" + value + "%");
+                return fieldExpr + " LIKE #{params." + paramName + "}";
+
+            case "notlike":
+                params.put(paramName, "%" + value + "%");
+                return fieldExpr + " NOT LIKE #{params." + paramName + "}";
+
+            case "in":
+                if (value instanceof Collection) {
+                    params.put(paramName, value);
+                    return fieldExpr + " IN <foreach collection='params." + paramName + "' item='item' open='(' separator=',' close=')'>#{item}</foreach>";
+                } else if (value instanceof String) {
+                    // 逗号分隔的字符串
+                    List<String> list = Arrays.asList(((String) value).split(","));
+                    params.put(paramName, list);
+                    return fieldExpr + " IN <foreach collection='params." + paramName + "' item='item' open='(' separator=',' close=')'>#{item}</foreach>";
+                }
+                return null;
+
+            case "notin":
+                if (value instanceof Collection) {
+                    params.put(paramName, value);
+                    return fieldExpr + " NOT IN <foreach collection='params." + paramName + "' item='item' open='(' separator=',' close=')'>#{item}</foreach>";
+                }
+                return null;
+
+            case "between":
+                params.put(paramName + "_1", value);
+                params.put(paramName + "_2", value2);
+                return fieldExpr + " BETWEEN #{params." + paramName + "_1} AND #{params." + paramName + "_2}";
+
+            case "isnull":
+                return fieldExpr + " IS NULL";
+
+            case "isnotnull":
+                return fieldExpr + " IS NOT NULL";
+
+            default:
+                logger.warn("未知操作符: {}", operator);
+                return null;
+        }
+    }
+
+    /**
+     * 判断条件值是否为空
+     */
+    private static boolean isConditionValueEmpty(ReportQueryConfig.QueryCondition condition) {
+        String operator = condition.getOperator();
+        if ("isNull".equalsIgnoreCase(operator) || "isNotNull".equalsIgnoreCase(operator)) {
+            return false;
+        }
+        Object value = condition.getValue();
+        if (value == null) return true;
+        if (value instanceof String && StringUtils.isBlank((String) value)) return true;
+        if (value instanceof Collection && ((Collection<?>) value).isEmpty()) return true;
+        return false;
+    }
+
+    /**
+     * 替换模板变量
+     */
+    public static String replaceVariables(String template, Map<String, Object> params) {
+        if (StringUtils.isBlank(template) || params == null) {
+            return template;
+        }
+
+        Matcher matcher = VARIABLE_PATTERN.matcher(template);
+        StringBuffer sb = new StringBuffer();
+        while (matcher.find()) {
+            String varName = matcher.group(1);
+            Object value = params.get(varName);
+            String replacement = value != null ? value.toString() : "";
+            // 防止SQL注入
+            if (hasSqlInjection(replacement)) {
+                logger.warn("变量值包含SQL注入风险: {} = {}", varName, replacement);
+                replacement = "";
+            }
+            matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
+        }
+        matcher.appendTail(sb);
+        return sb.toString();
+    }
+
+    /**
+     * SQL注入检测
+     */
+    public static boolean hasSqlInjection(String value) {
+        if (StringUtils.isBlank(value)) {
+            return false;
+        }
+        return SQL_INJECTION_PATTERN.matcher(value).find();
+    }
+
+    /**
+     * 安全转义
+     */
+    public static String escapeValue(String value) {
+        if (value == null) return null;
+        return value.replace("'", "''").replace("\\", "\\\\");
+    }
+}

+ 473 - 0
ems/ems-core/src/main/resources/mapper/ems/CustomReportMapper.xml

@@ -0,0 +1,473 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.ems.mapper.CustomReportMapper">
+
+    <!-- ==================== ResultMap定义 ==================== -->
+
+    <resultMap type="com.ruoyi.ems.domain.ReportDatasource" id="DatasourceResult">
+        <id property="id" column="id"/>
+        <result property="dsCode" column="ds_code"/>
+        <result property="dsName" column="ds_name"/>
+        <result property="dsDesc" column="ds_desc"/>
+        <result property="mainTable" column="main_table"/>
+        <result property="mainAlias" column="main_alias"/>
+        <result property="baseWhere" column="base_where"/>
+        <result property="defaultOrder" column="default_order"/>
+        <result property="category" column="category"/>
+        <result property="icon" column="icon"/>
+        <result property="sortOrder" column="sort_order"/>
+        <result property="status" column="status"/>
+        <result property="remark" column="remark"/>
+        <result property="createBy" column="create_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateTime" column="update_time"/>
+    </resultMap>
+
+    <resultMap type="com.ruoyi.ems.domain.ReportField" id="FieldResult">
+        <id property="id" column="id"/>
+        <result property="dsCode" column="ds_code"/>
+        <result property="fieldCode" column="field_code"/>
+        <result property="fieldName" column="field_name"/>
+        <result property="fieldAlias" column="field_alias"/>
+        <result property="fieldExpr" column="field_expr"/>
+        <result property="fieldType" column="field_type"/>
+        <result property="fieldFormat" column="field_format"/>
+        <result property="decimals" column="decimals"/>
+        <result property="unit" column="unit"/>
+        <result property="dictType" column="dict_type"/>
+        <result property="isDefault" column="is_default"/>
+        <result property="isRequired" column="is_required"/>
+        <result property="isFilterable" column="is_filterable"/>
+        <result property="isSortable" column="is_sortable"/>
+        <result property="isAggregatable" column="is_aggregatable"/>
+        <result property="aggregateFunc" column="aggregate_func"/>
+        <result property="groupName" column="group_name"/>
+        <result property="width" column="width"/>
+        <result property="minWidth" column="min_width"/>
+        <result property="sortOrder" column="sort_order"/>
+        <result property="status" column="status"/>
+        <result property="remark" column="remark"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateTime" column="update_time"/>
+    </resultMap>
+
+    <resultMap type="com.ruoyi.ems.domain.ReportFieldCondition" id="FieldConditionResult">
+        <id property="id" column="id"/>
+        <result property="dsCode" column="ds_code"/>
+        <result property="fieldCode" column="field_code"/>
+        <result property="conditionType" column="condition_type"/>
+        <result property="conditionName" column="condition_name"/>
+        <result property="conditionSymbol" column="condition_symbol"/>
+        <result property="valueCount" column="value_count"/>
+        <result property="defaultValue" column="default_value"/>
+        <result property="isDefault" column="is_default"/>
+        <result property="sortOrder" column="sort_order"/>
+        <result property="status" column="status"/>
+    </resultMap>
+
+    <resultMap type="com.ruoyi.ems.domain.ReportRelation" id="RelationResult">
+        <id property="id" column="id"/>
+        <result property="dsCode" column="ds_code"/>
+        <result property="relationCode" column="relation_code"/>
+        <result property="relationName" column="relation_name"/>
+        <result property="joinType" column="join_type"/>
+        <result property="joinTable" column="join_table"/>
+        <result property="joinAlias" column="join_alias"/>
+        <result property="joinCondition" column="join_condition"/>
+        <result property="isAutoJoin" column="is_auto_join"/>
+        <result property="sortOrder" column="sort_order"/>
+        <result property="status" column="status"/>
+        <result property="remark" column="remark"/>
+    </resultMap>
+
+    <resultMap type="com.ruoyi.ems.domain.ReportTemplate" id="TemplateResult">
+        <id property="id" column="id"/>
+        <result property="templateCode" column="template_code"/>
+        <result property="templateName" column="template_name"/>
+        <result property="templateDesc" column="template_desc"/>
+        <result property="dsCode" column="ds_code"/>
+        <result property="configJson" column="config_json"/>
+        <result property="isPublic" column="is_public"/>
+        <result property="isSystem" column="is_system"/>
+        <result property="useCount" column="use_count"/>
+        <result property="userId" column="user_id"/>
+        <result property="userName" column="user_name"/>
+        <result property="deptId" column="dept_id"/>
+        <result property="status" column="status"/>
+        <result property="createBy" column="create_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateTime" column="update_time"/>
+        <result property="dsName" column="ds_name"/>
+    </resultMap>
+
+    <!-- ==================== 数据源管理SQL ==================== -->
+
+    <sql id="selectDatasourceSql">
+        SELECT id, ds_code, ds_name, ds_desc, main_table, main_alias, base_where,
+               default_order, category, icon, sort_order, status, remark,
+               create_by, create_time, update_by, update_time
+        FROM adm_report_datasource
+    </sql>
+
+    <select id="selectDatasourceList" resultMap="DatasourceResult">
+        <include refid="selectDatasourceSql"/>
+        <where>
+            status = 1
+            <if test="category != null and category != ''">
+                AND category = #{category}
+            </if>
+        </where>
+        ORDER BY sort_order ASC, ds_code ASC
+    </select>
+
+    <select id="selectDatasourceByCode" resultMap="DatasourceResult">
+        <include refid="selectDatasourceSql"/>
+        WHERE ds_code = #{dsCode}
+    </select>
+
+    <insert id="insertDatasource" parameterType="com.ruoyi.ems.domain.ReportDatasource" useGeneratedKeys="true" keyProperty="id">
+        INSERT INTO adm_report_datasource (
+            ds_code, ds_name, ds_desc, main_table, main_alias, base_where,
+            default_order, category, icon, sort_order, status, remark, create_by, create_time
+        ) VALUES (
+                     #{dsCode}, #{dsName}, #{dsDesc}, #{mainTable}, #{mainAlias}, #{baseWhere},
+                     #{defaultOrder}, #{category}, #{icon}, #{sortOrder}, #{status}, #{remark}, #{createBy}, NOW()
+                 )
+    </insert>
+
+    <update id="updateDatasource" parameterType="com.ruoyi.ems.domain.ReportDatasource">
+        UPDATE adm_report_datasource
+        <set>
+            <if test="dsName != null and dsName != ''">ds_name = #{dsName},</if>
+            <if test="dsDesc != null">ds_desc = #{dsDesc},</if>
+            <if test="mainTable != null and mainTable != ''">main_table = #{mainTable},</if>
+            <if test="mainAlias != null">main_alias = #{mainAlias},</if>
+            <if test="baseWhere != null">base_where = #{baseWhere},</if>
+            <if test="defaultOrder != null">default_order = #{defaultOrder},</if>
+            <if test="category != null">category = #{category},</if>
+            <if test="icon != null">icon = #{icon},</if>
+            <if test="sortOrder != null">sort_order = #{sortOrder},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="remark != null">remark = #{remark},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            update_time = NOW()
+        </set>
+        WHERE ds_code = #{dsCode}
+    </update>
+
+    <delete id="deleteDatasource">
+        DELETE FROM adm_report_datasource WHERE ds_code = #{dsCode}
+    </delete>
+
+    <!-- ==================== 字段管理SQL ==================== -->
+
+    <sql id="selectFieldSql">
+        SELECT id, ds_code, field_code, field_name, field_alias, field_expr, field_type,
+               field_format, decimals, unit, dict_type, is_default, is_required,
+               is_filterable, is_sortable, is_aggregatable, aggregate_func, group_name,
+               width, min_width, sort_order, status, remark, create_time, update_time
+        FROM adm_report_field
+    </sql>
+
+    <select id="selectFieldsByDsCode" resultMap="FieldResult">
+        <include refid="selectFieldSql"/>
+        WHERE ds_code = #{dsCode} AND status = 1
+        ORDER BY sort_order ASC, field_code ASC
+    </select>
+
+    <select id="selectField" resultMap="FieldResult">
+        <include refid="selectFieldSql"/>
+        WHERE ds_code = #{dsCode} AND field_code = #{fieldCode}
+    </select>
+
+    <insert id="insertField" parameterType="com.ruoyi.ems.domain.ReportField" useGeneratedKeys="true" keyProperty="id">
+        INSERT INTO adm_report_field (
+            ds_code, field_code, field_name, field_alias, field_expr, field_type,
+            field_format, decimals, unit, dict_type, is_default, is_required,
+            is_filterable, is_sortable, is_aggregatable, aggregate_func, group_name,
+            width, min_width, sort_order, status, remark, create_time
+        ) VALUES (
+                     #{dsCode}, #{fieldCode}, #{fieldName}, #{fieldAlias}, #{fieldExpr}, #{fieldType},
+                     #{fieldFormat}, #{decimals}, #{unit}, #{dictType}, #{isDefault}, #{isRequired},
+                     #{isFilterable}, #{isSortable}, #{isAggregatable}, #{aggregateFunc}, #{groupName},
+                     #{width}, #{minWidth}, #{sortOrder}, #{status}, #{remark}, NOW()
+                 )
+    </insert>
+
+    <insert id="insertFieldBatch" parameterType="java.util.List">
+        INSERT INTO adm_report_field (
+        ds_code, field_code, field_name, field_alias, field_expr, field_type,
+        field_format, decimals, unit, dict_type, is_default, is_required,
+        is_filterable, is_sortable, is_aggregatable, aggregate_func, group_name,
+        width, min_width, sort_order, status, remark, create_time
+        ) VALUES
+        <foreach collection="list" item="item" separator=",">
+            (
+            #{item.dsCode}, #{item.fieldCode}, #{item.fieldName}, #{item.fieldAlias}, #{item.fieldExpr}, #{item.fieldType},
+            #{item.fieldFormat}, #{item.decimals}, #{item.unit}, #{item.dictType}, #{item.isDefault}, #{item.isRequired},
+            #{item.isFilterable}, #{item.isSortable}, #{item.isAggregatable}, #{item.aggregateFunc}, #{item.groupName},
+            #{item.width}, #{item.minWidth}, #{item.sortOrder}, #{item.status}, #{item.remark}, NOW()
+            )
+        </foreach>
+    </insert>
+
+    <update id="updateField" parameterType="com.ruoyi.ems.domain.ReportField">
+        UPDATE adm_report_field
+        <set>
+            <if test="fieldName != null and fieldName != ''">field_name = #{fieldName},</if>
+            <if test="fieldAlias != null">field_alias = #{fieldAlias},</if>
+            <if test="fieldExpr != null">field_expr = #{fieldExpr},</if>
+            <if test="fieldType != null">field_type = #{fieldType},</if>
+            <if test="fieldFormat != null">field_format = #{fieldFormat},</if>
+            <if test="decimals != null">decimals = #{decimals},</if>
+            <if test="unit != null">unit = #{unit},</if>
+            <if test="dictType != null">dict_type = #{dictType},</if>
+            <if test="isDefault != null">is_default = #{isDefault},</if>
+            <if test="isRequired != null">is_required = #{isRequired},</if>
+            <if test="isFilterable != null">is_filterable = #{isFilterable},</if>
+            <if test="isSortable != null">is_sortable = #{isSortable},</if>
+            <if test="isAggregatable != null">is_aggregatable = #{isAggregatable},</if>
+            <if test="aggregateFunc != null">aggregate_func = #{aggregateFunc},</if>
+            <if test="groupName != null">group_name = #{groupName},</if>
+            <if test="width != null">width = #{width},</if>
+            <if test="minWidth != null">min_width = #{minWidth},</if>
+            <if test="sortOrder != null">sort_order = #{sortOrder},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="remark != null">remark = #{remark},</if>
+            update_time = NOW()
+        </set>
+        WHERE ds_code = #{dsCode} AND field_code = #{fieldCode}
+    </update>
+
+    <delete id="deleteField">
+        DELETE FROM adm_report_field WHERE ds_code = #{dsCode} AND field_code = #{fieldCode}
+    </delete>
+
+    <delete id="deleteFieldsByDsCode">
+        DELETE FROM adm_report_field WHERE ds_code = #{dsCode}
+    </delete>
+
+    <!-- ==================== 字段条件管理SQL ==================== -->
+
+    <sql id="selectFieldConditionSql">
+        SELECT id, ds_code, field_code, condition_type, condition_name, condition_symbol,
+               value_count, default_value, is_default, sort_order, status
+        FROM adm_report_field_condition
+    </sql>
+
+    <select id="selectFieldConditions" resultMap="FieldConditionResult">
+        <include refid="selectFieldConditionSql"/>
+        WHERE ds_code = #{dsCode} AND field_code = #{fieldCode} AND status = 1
+        ORDER BY sort_order ASC
+    </select>
+
+    <select id="selectAllFieldConditions" resultMap="FieldConditionResult">
+        <include refid="selectFieldConditionSql"/>
+        WHERE ds_code = #{dsCode} AND status = 1
+        ORDER BY field_code ASC, sort_order ASC
+    </select>
+
+    <insert id="insertFieldCondition" parameterType="com.ruoyi.ems.domain.ReportFieldCondition" useGeneratedKeys="true" keyProperty="id">
+        INSERT INTO adm_report_field_condition (
+            ds_code, field_code, condition_type, condition_name, condition_symbol,
+            value_count, default_value, is_default, sort_order, status
+        ) VALUES (
+                     #{dsCode}, #{fieldCode}, #{conditionType}, #{conditionName}, #{conditionSymbol},
+                     #{valueCount}, #{defaultValue}, #{isDefault}, #{sortOrder}, #{status}
+                 )
+    </insert>
+
+    <insert id="insertFieldConditionBatch" parameterType="java.util.List">
+        INSERT INTO adm_report_field_condition (
+        ds_code, field_code, condition_type, condition_name, condition_symbol,
+        value_count, default_value, is_default, sort_order, status
+        ) VALUES
+        <foreach collection="list" item="item" separator=",">
+            (
+            #{item.dsCode}, #{item.fieldCode}, #{item.conditionType}, #{item.conditionName}, #{item.conditionSymbol},
+            #{item.valueCount}, #{item.defaultValue}, #{item.isDefault}, #{item.sortOrder}, #{item.status}
+            )
+        </foreach>
+    </insert>
+
+    <delete id="deleteFieldConditions">
+        DELETE FROM adm_report_field_condition WHERE ds_code = #{dsCode}
+        <if test="fieldCode != null and fieldCode != ''">
+            AND field_code = #{fieldCode}
+        </if>
+    </delete>
+
+    <!-- ==================== 关联管理SQL ==================== -->
+
+    <sql id="selectRelationSql">
+        SELECT id, ds_code, relation_code, relation_name, join_type, join_table,
+               join_alias, join_condition, is_auto_join, sort_order, status, remark
+        FROM adm_report_relation
+    </sql>
+
+    <select id="selectRelationsByDsCode" resultMap="RelationResult">
+        <include refid="selectRelationSql"/>
+        WHERE ds_code = #{dsCode} AND status = 1
+        ORDER BY sort_order ASC
+    </select>
+
+    <select id="selectRelation" resultMap="RelationResult">
+        <include refid="selectRelationSql"/>
+        WHERE ds_code = #{dsCode} AND relation_code = #{relationCode}
+    </select>
+
+    <insert id="insertRelation" parameterType="com.ruoyi.ems.domain.ReportRelation" useGeneratedKeys="true" keyProperty="id">
+        INSERT INTO adm_report_relation (
+            ds_code, relation_code, relation_name, join_type, join_table,
+            join_alias, join_condition, is_auto_join, sort_order, status, remark
+        ) VALUES (
+                     #{dsCode}, #{relationCode}, #{relationName}, #{joinType}, #{joinTable},
+                     #{joinAlias}, #{joinCondition}, #{isAutoJoin}, #{sortOrder}, #{status}, #{remark}
+                 )
+    </insert>
+
+    <update id="updateRelation" parameterType="com.ruoyi.ems.domain.ReportRelation">
+        UPDATE adm_report_relation
+        <set>
+            <if test="relationName != null">relation_name = #{relationName},</if>
+            <if test="joinType != null">join_type = #{joinType},</if>
+            <if test="joinTable != null">join_table = #{joinTable},</if>
+            <if test="joinAlias != null">join_alias = #{joinAlias},</if>
+            <if test="joinCondition != null">join_condition = #{joinCondition},</if>
+            <if test="isAutoJoin != null">is_auto_join = #{isAutoJoin},</if>
+            <if test="sortOrder != null">sort_order = #{sortOrder},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="remark != null">remark = #{remark},</if>
+        </set>
+        WHERE ds_code = #{dsCode} AND relation_code = #{relationCode}
+    </update>
+
+    <delete id="deleteRelation">
+        DELETE FROM adm_report_relation WHERE ds_code = #{dsCode} AND relation_code = #{relationCode}
+    </delete>
+
+    <delete id="deleteRelationsByDsCode">
+        DELETE FROM adm_report_relation WHERE ds_code = #{dsCode}
+    </delete>
+
+    <!-- ==================== 字段关联映射SQL ==================== -->
+
+    <select id="selectFieldRelationCodes" resultType="java.lang.String">
+        SELECT relation_code FROM adm_report_field_relation
+        WHERE ds_code = #{dsCode} AND field_code = #{fieldCode}
+    </select>
+
+    <select id="selectAllFieldRelations" resultType="java.util.Map">
+        SELECT field_code AS fieldCode, relation_code AS relationCode
+        FROM adm_report_field_relation
+        WHERE ds_code = #{dsCode}
+    </select>
+
+    <insert id="insertFieldRelation">
+        INSERT INTO adm_report_field_relation (ds_code, field_code, relation_code)
+        VALUES (#{dsCode}, #{fieldCode}, #{relationCode})
+    </insert>
+
+    <delete id="deleteFieldRelations">
+        DELETE FROM adm_report_field_relation WHERE ds_code = #{dsCode}
+        <if test="fieldCode != null and fieldCode != ''">
+            AND field_code = #{fieldCode}
+        </if>
+    </delete>
+
+    <!-- ==================== 模板管理SQL ==================== -->
+
+    <sql id="selectTemplateSql">
+        SELECT t.id, t.template_code, t.template_name, t.template_desc, t.ds_code,
+               t.config_json, t.is_public, t.is_system, t.use_count, t.user_id, t.user_name,
+               t.dept_id, t.status, t.create_by, t.create_time, t.update_by, t.update_time,
+               d.ds_name
+        FROM adm_report_template t
+                 LEFT JOIN adm_report_datasource d ON t.ds_code = d.ds_code
+    </sql>
+
+    <select id="selectTemplateList" parameterType="com.ruoyi.ems.domain.ReportTemplate" resultMap="TemplateResult">
+        <include refid="selectTemplateSql"/>
+        <where>
+            t.status = 1
+            <if test="dsCode != null and dsCode != ''">
+                AND t.ds_code = #{dsCode}
+            </if>
+            <if test="templateName != null and templateName != ''">
+                AND t.template_name LIKE CONCAT('%', #{templateName}, '%')
+            </if>
+            <!-- 查询公开模板或自己的私有模板 -->
+            <if test="userId != null">
+                AND (t.is_public = 1 OR t.user_id = #{userId})
+            </if>
+            <if test="isSystem != null">
+                AND t.is_system = #{isSystem}
+            </if>
+        </where>
+        ORDER BY t.is_system DESC, t.use_count DESC, t.create_time DESC
+    </select>
+
+    <select id="selectTemplateByCode" resultMap="TemplateResult">
+        <include refid="selectTemplateSql"/>
+        WHERE t.template_code = #{templateCode}
+    </select>
+
+    <select id="checkTemplateCodeExists" resultType="int">
+        SELECT COUNT(1) FROM adm_report_template WHERE template_code = #{templateCode}
+    </select>
+
+    <insert id="insertTemplate" parameterType="com.ruoyi.ems.domain.ReportTemplate" useGeneratedKeys="true" keyProperty="id">
+        INSERT INTO adm_report_template (
+            template_code, template_name, template_desc, ds_code, config_json,
+            is_public, is_system, use_count, user_id, user_name, dept_id, status, create_by, create_time
+        ) VALUES (
+                     #{templateCode}, #{templateName}, #{templateDesc}, #{dsCode}, #{configJson},
+                     #{isPublic}, #{isSystem}, 0, #{userId}, #{userName}, #{deptId}, #{status}, #{createBy}, NOW()
+                 )
+    </insert>
+
+    <update id="updateTemplate" parameterType="com.ruoyi.ems.domain.ReportTemplate">
+        UPDATE adm_report_template
+        <set>
+            <if test="templateName != null and templateName != ''">template_name = #{templateName},</if>
+            <if test="templateDesc != null">template_desc = #{templateDesc},</if>
+            <if test="configJson != null">config_json = #{configJson},</if>
+            <if test="isPublic != null">is_public = #{isPublic},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            update_time = NOW()
+        </set>
+        WHERE template_code = #{templateCode}
+    </update>
+
+    <delete id="deleteTemplate">
+        DELETE FROM adm_report_template WHERE template_code = #{templateCode}
+    </delete>
+
+    <update id="incrementTemplateUseCount">
+        UPDATE adm_report_template SET use_count = use_count + 1 WHERE template_code = #{templateCode}
+    </update>
+
+    <!-- ==================== 动态查询执行SQL ==================== -->
+
+    <!-- 注意:动态SQL在Service层构建,这里使用${}直接拼接 -->
+    <select id="executeDynamicQuery" resultType="java.util.LinkedHashMap">
+        ${sql}
+        <if test="params != null and params.pageNum != null and params.pageSize != null">
+            LIMIT #{params.offset}, #{params.pageSize}
+        </if>
+    </select>
+
+    <select id="executeDynamicCount" resultType="java.lang.Long">
+        SELECT COUNT(*) FROM (${sql}) AS count_table
+    </select>
+
+    <select id="executeDynamicSummary" resultType="java.util.LinkedHashMap">
+        ${sql}
+    </select>
+
+</mapper>