【企业级项目实操指南3】结合已有代码和MPJ实现统一的数据统计接口(3)

【企业级项目实操指南2】结合已有代码和MPJ实现统一的数据统计接口(1)
https://www.cnblogs.com/zwj/p/18841146/bnp-doit-001

【企业级项目实操指南2】结合已有代码和MPJ实现统一的数据统计接口(2)
https://www.cnblogs.com/zwj/p/18841146/bnp-doit-002

在1、2的基础上做一些补充,主要是应对 某个字段有自己的统计条件限制 问题
比如有一个金额列,但是我要把支出金额和收入金额分开统计,而不是所有金额单纯的做一个累加。

于是

@Data
public class AggregationColumn {
    /**
     * 字段名和统计方式 是前端提供的
     * 如果有需要特殊处理的字段,可以在后端针对性补充 条件表达式condition 和自定义别名后缀,可参考 com/bytz/modules/erp/service/ContractService.java
     */
    private String columnName;
    private String condition; // 条件表达式,如 "字段 = '内容'"
    private String aliasSuffix; // 自定义别名后缀
    private AggregationFunction func; // 统计方式
}

工具类

import com.github.yulichang.wrapper.MPJLambdaWrapper;
import org.jeecg.common.system.query.MPJQueryGenerator;
import org.jeecg.common.system.query.ReferenceColumn;

import java.lang.reflect.Field;
import java.util.List;

public class MPJAggregateUtil {
    /**
     * 构建聚合查询条件
     *
     * @param mpjLW       查询包装器
     * @param entityClass 实体类
     * @param columns     统计参数列表
     * @param <T>         泛型实体类型
     */
    public static <T> void buildAggregation(MPJLambdaWrapper<T> mpjLW, Class<?> entityClass, List<AggregationColumn> columns) {
        // 遍历统计列配置
        columns.forEach(column -> {
            if (column == null) {
                throw new IllegalArgumentException("请输入要统计的列");
            }
            String columnName = column.getColumnName();
            AggregationFunction aggregationFunction = column.getFunc();

            if (columnName == null || aggregationFunction == null) {
                throw new IllegalArgumentException("请输入要统计的方式");
            }

            Field field = getFieldFromHierarchy(entityClass, columnName);
            String sql = "";

            if (field != null && field.isAnnotationPresent(ReferenceColumn.class)) {
                ReferenceColumn annotation = field.getAnnotation(ReferenceColumn.class);
                String tColumn = MPJQueryGenerator.buildReferenceColumn(annotation, mpjLW);

                sql = buildSql(aggregationFunction, tColumn, field.getName(),
                        column.getCondition()!=null, column.getCondition(), column.getAliasSuffix());
            } else {
                String snakeCaseColumnName = camelToSnakeCase(columnName);
                sql = buildSql(aggregationFunction, "t." + snakeCaseColumnName, snakeCaseColumnName,
                        column.getCondition()!=null, column.getCondition(), column.getAliasSuffix());
            }

            mpjLW.select(sql);
        });
    }

    /**
     * 构建 SQL 聚合表达式
     *
     * @param aggregationFunction 聚合函数,如 SUM, COUNT, AVG 等
     * @param columnExpression    列表达式,通常是列名或带有表前缀的列名(例如 "t.money")
     * @param aliasPrefix         别名前缀,通常与列名或实体类中的字段名相关,用于生成结果集中的别名
     * @param conditional         是否使用条件聚合。如果为 true,则会根据提供的条件构造 CASE WHEN 语句
     * @param condition           条件表达式,当 conditional 为 true 时使用,定义了聚合计算时应满足的条件
     * @param aliasSuffix         自定义别名后缀,用于区分不同聚合结果或指定特定的别名后缀(例如 income 或 expense)
     * @return 完整的 SQL 表达式,包含聚合函数、可能的条件逻辑以及对应的别名
     */
    private static String buildSql(AggregationFunction aggregationFunction,
                                   String columnExpression,
                                   String aliasPrefix,
                                   boolean conditional,
                                   String condition,
                                   String aliasSuffix) {
        if (conditional && condition != null && !condition.isEmpty()) {
            return String.format("SUM(CASE WHEN %s THEN %s ELSE 0 END) AS %s__%s",
                    condition, columnExpression, aliasPrefix, aliasSuffix);
        }

        String aliasName = aliasSuffix != null ? aliasSuffix : aggregationFunction.name().toLowerCase();

        switch (aggregationFunction) {
            case COUNT_DISTINCT:
                return String.format("COUNT(DISTINCT %s) AS %s__count", columnExpression, aliasPrefix);
            case COUNT:
            case SUM:
            case AVG:
            case MIN:
            case MAX:
                return String.format("%s(%s) AS %s__%s", aggregationFunction.getFunctionName(), columnExpression, aliasPrefix, aliasName);
            default:
                throw new IllegalArgumentException("Unsupported aggregation function: " + aggregationFunction);
        }
    }

    /**
     * 小驼峰转蛇形命名(snake_case)
     *
     * @param str 输入的小驼峰格式字符串
     * @return 转换后的蛇形命名字符串(snake_case)
     */
    public static String camelToSnakeCase(String str) {
        if (str == null || str.isEmpty()) {
            return str;
        }
        StringBuilder result = new StringBuilder();
        for (char c : str.toCharArray()) {
            if (Character.isUpperCase(c)) {
                result.append("_").append(Character.toLowerCase(c));
            } else {
                result.append(c);
            }
        }
        return result.toString();
    }

    /**
     * 从类及其父类中递归查找字段
     *
     * @param clazz     类
     * @param fieldName 字段名
     * @return 找到的字段,如果未找到则返回 null
     */
    private static Field getFieldFromHierarchy(Class<?> clazz, String fieldName) {
        while (clazz != null && !clazz.equals(Object.class)) {
            try {
                return clazz.getDeclaredField(fieldName);
            } catch (NoSuchFieldException e) {
                clazz = clazz.getSuperclass(); // 递归查找父类
            }
        }
        return null; // 如果未找到字段,返回 null
    }
}

使用

    public List<Map<String, Object>> statWithJoin(ContractForListRes entity, Map<String, String[]> queryParams, List<AggregationColumn> columns) {
        MPJLambdaWrapper<Contract> mpjLW = new MPJLambdaWrapper<>(Contract.class)
                .leftJoin(Project.class, Project::getId, Contract::getProjectId)
                .leftJoin(Org.class, Org::getId, Contract::getTheirOrgId)
                .leftJoin(SysUser.class, SysUser::getId, Contract::getCreateBy);

        MPJQueryGenerator.installMPJ(mpjLW, entity,  queryParams);

        // 检查是否有需要特殊处理的字段
        // "columnName": "amount"
        // 使用 Stream API 查找 amount 字段并处理
        boolean containsAmount = columns.stream()
                .anyMatch(column -> "amount".equals(column.getColumnName()));

        if (containsAmount) {
            // 移除原始的 amount 列
            columns.removeIf(column -> "amount".equals(column.getColumnName()));
            // 添加需要特殊处理的列
            AggregationColumn incomeColumn = new AggregationColumn();
            incomeColumn.setColumnName("amount");
            incomeColumn.setFunc(AggregationFunction.SUM);
            incomeColumn.setCondition("contract_category = '1'");
            incomeColumn.setAliasSuffix("sales"); // 销售
            AggregationColumn expenseColumn = new AggregationColumn();
            expenseColumn.setColumnName("amount");
            expenseColumn.setFunc(AggregationFunction.SUM);
            expenseColumn.setCondition("contract_category = '2'");
            expenseColumn.setAliasSuffix("purchase"); // 采购
            columns.add(incomeColumn);
            columns.add(expenseColumn);
        }

        MPJAggregateUtil.buildAggregation(mpjLW, ContractForListRes.class ,columns);

        return this.selectJoinMaps(mpjLW);
    }

这种方式,就能够实现对某个字段增加专属它的查询条件(虽然有很大局限性)

那么,接下来就谈问题:

AggregationColumn作为框架,应该提供通用功能,并为特殊需求提供可能性,这就是所谓的OCP (Open Close Principle)。注意理解“可能性”,如果可能性存在局限,那就说明设计不够完善,或者需求还不清晰,宁可不做。而且在Service层中加入数据表字段本来就不是好的实践,相当于领域层就必须知道数据库(Adapter)才可以做查询。

再说回需求,用户给的条件是基础,某个列的条件是额外附加的。所以,强烈建议,可不可以把用户给的条件作为基础,再附加某个特定列的条件,而且确保用MPJ而不是直接写数据表字段。这里我可以接受多次请求,宁可请求多次,也不要留下坏味道,给以后留下问题。

所以

总体不建议在AggregationColumn中增加condition,因为目前的condition支持的条件并不完备,一旦添加了condition你就会无止境的去扩张它,最终造成不可控。

建议暂时先只在Service类中处理这种特殊的聚合。

posted @ 2025-04-29 14:15  萌狼蓝天  阅读(31)  评论(0)    收藏  举报