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

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

在(1)的基础上做一些优化和补充,一方面是满足日期范围搜索条件,一方面是对命名的优化。

后端 - MPJAggregateUtil
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());
            } else {
                String snakeCaseColumnName = camelToSnakeCase(columnName);
                sql = buildSql(aggregationFunction, "t." + snakeCaseColumnName, snakeCaseColumnName);
            }

            mpjLW.select(sql);
        });
    }

    /**
     * 构建 SQL 聚合表达式
     *
     * @param aggregationFunction 聚合函数
     * @param columnExpression    列表达式
     * @param aliasPrefix         别名前缀
     * @return 完整的 SQL 表达式
     */
    private static String buildSql(AggregationFunction aggregationFunction, String columnExpression, String aliasPrefix) {
        String 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, aliasSuffix);
            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
    }
}
后端 - DTO

@Data
public class AggregationColumn {
    private String columnName;
    private AggregationFunction func;
}

@Data
public  class AggregationData<T> {
    private T entity;
    private List<AggregationColumn> params;

}

后端 - 枚举
package com.bytz.modules.erp.enums;

public enum AggregationFunction {
    COUNT("COUNT"),
    COUNT_DISTINCT("COUNT_DISTINCT"),
    SUM("SUM"),
    AVG("AVG"),
    MIN("MIN"),
    MAX("MAX");

    private final String functionName;

    AggregationFunction(String functionName) {
        this.functionName = functionName;
    }

    public String getFunctionName() {
        return functionName;
    }

    @Override
    public String toString() {
        return functionName;
    }
}
后端 - Controller

    @ApiOperation("数据统计")
    @RequestMapping(value =  "/aggregate", method = RequestMethod.POST)
    public Result<List<Map<String, Object>>> statTimeCard(
            @RequestBody AggregationData<TimeCardRes> aggregationData,
            HttpServletRequest req) {
        return Result.ok(tsmService.statWithJoin(aggregationData.getEntity(), req.getParameterMap(), aggregationData.getParams()));
    }
}
后端 - Service
 public List<Map<String, Object>> statWithJoin(TimeCardRes conditionEntity, Map<String, String[]> parameters,List<AggregationColumn> columns) {
        MPJLambdaWrapper<TimeCard> mpjLW = new MPJLambdaWrapper<>(TimeCard.class);
        // 联表
        mpjLW.leftJoin(ActivityInfo.class, ActivityInfo::getId, TimeCard::getActivityId);
        mpjLW.leftJoin(SysUser.class, SysUser::getId, TimeCard::getUserId);
        // 条件
        MPJQueryGenerator.installMPJ(mpjLW, conditionEntity, parameters);
        MPJAggregateUtil.buildAggregation(mpjLW, TimeCardRes.class ,columns);
        return this.selectJoinMaps(mpjLW);
    }
前端 - 请求接口

const aggregateData = (url, queryParam, aggregationParams) => postAction(url, {
  entity: queryParam,
  params: aggregationParams
});

前端 - 请求方法
 aggregate() {
      // 说明有需要统计的内容
      aggregateData(this.aggregationUrl + "/aggregate" + this.urlParam, this.getQueryParams(), this.aggregationParams)
        .then(res => {
          if (res.code === 200) {
            // 如果返回的结果不为空,则更新 aggregationResult
            if (res.result[0] !== null) {
              this.aggregationResult = Object.fromEntries(
                Object.entries(res.result[0]).map(([key, value]) => [key, value ?? 0])
              );
            } else {
              // 如果返回的结果为空,则将所有字段设置为 0
              this.aggregationResult = Object.fromEntries(
                Object.keys(this.aggregationResult).map(key => [key, 0])
              );
            }
          }
        });

关于URLParam的问题,这个变量包含的参数会被后端的req接收到

思考一个问题,加载List的时候,用的是get请求,因此 HttpServletRequest req能正确收到数据
但Post请求的时候,请求体都在Body,就会出现req根本收不到任何数据
可是我们必须把某些数据放在req里面(主要是为了和body的参数分离),以便复用原有的解析方法
解决办法也很简单,就是使用URL拼接的方式
定义一个 urlParam: "?",然后把要放在URL里面的参数拼接上去

比如

   getQueryParams(startIndex = 1) {
      this.urlParam = "?"; // 重置请求路径上的参数
      const queryParam = { ...this.queryParam, ...this.queryParamOther };
      // 增加对 startPage的支持 2025-1-16
      queryParam.startPage = this.ipagination.current - startIndex;
      Object.keys(queryParam).forEach(key => {
        // 起始时间
        if (key.endsWith("_flag") && queryParam[key].length > 0) {
          queryParam[key.slice(0, -5) + "_begin"] = moment(queryParam[key][0]).startOf("day").format("YYYY-MM-DD HH:mm:ss");
          queryParam[key.slice(0, -5) + "_end"] = moment(queryParam[key][1]).endOf("day").format("YYYY-MM-DD HH:mm:ss");
          delete queryParam[key];
          this.urlParam += key.slice(0, -5) + "_begin=" + queryParam[key.slice(0, -5) + "_begin"] + "&" + key.slice(0, -5) + "_end=" + queryParam[key.slice(0, -5) + "_end"];
        }
        ……其他代码

当然,你后端要有对应的处理逻辑哈,没这个需求可以不要的

前端 - 参数
  aggregationUrl: "/erp/issuedInvoice",
  aggregationParams: [
    {
      "columnName": "invoiceAmount",
      "func": "SUM"
    }
  ],
  aggregationResult: {
    invoice_amount__sum: 0
  }
前端 - 显示
      <div class="statistics-info">
        <span> 开票总金额:¥ {{ aggregationResult.invoice_amount__sum | ThousandSeparate(2) }} </span>
        <a-button type="link" @click="aggregate">
          刷新统计
        </a-button>
      </div>
前端 - 样式
.list-action {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.statistics-info {
  margin-left: 20px;
  padding: 4px 12px;
  box-sizing: border-box;
  span {
    margin-right: 10px;
    &:not(:first-child) {
      border-left: 1px solid #ccc;
      padding-left: 10px;
    }
  }
}
posted @ 2025-04-22 16:59  萌狼蓝天  阅读(48)  评论(0)    收藏  举报