反射调用service对象,@Autowired注入为空值

场景需求为:前台传来的对象种类不确定,根据不同的对象的对象种类id的传入,在同一个接口中,会调用不同的与对象相对应的service层及该service层内的方法。
举例两个种类分别为User和Student。
以枚举形势创建常量,保证了controller层调用不同service层的可扩展性和service层内方法的可扩展性:

public enum BeanEnum {

    User(1,"userServiceImpl","findUser"),
    Student(2,"studentServiceImpl","findStudent");

    private Integer beanType;
    private String className;
    private String methodName;

    BeanEnum(Integer beanType, String className, String methodName) {
        this.beanType = beanType;
        this.className = className;
        this.methodName = methodName;
    }

    public static BeanConfig getBeanConfig(Integer beanType){
        BeanConfig beanConfig = new BeanConfig();
        for (BeanEnum singleBE : BeanEnum.values()) {
            if (singleBE.beanType.equals(beanType)) {
                beanConfig.setClassName(singleBE.className);
                beanConfig.setMethodName(singleBE.methodName);
            }
        }
        return beanConfig;
    }
}

下面注意:在获取service层实例对象时,如果利用className直接反射获取字节码对象,然后newInstance获取实例对象,这个实例对象是与springIOC容器无关的,因此无法在该对象中,也就是userServiceImpl/studentServiceImpl中执行@Autowired注入dao层数据,所以需要直接在springIOC容器中获取实例对象,并反向获取其字节码对象,用于反射执行该实例对象内的方法。
代码如下:

@Service
public class BeanServiceImpl implements BeanService {

    /**
     * 获取对象实例
     * @param beanType  对象种类
     * @param beanId    对象id
     * @return
     */
    public Object findObj(Integer beanType,Integer beanId){
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath*:spring/applicationContext-*.xml");

        BeanConfig beanConfig = BeanEnum.getBeanConfig(beanType);
        String className = beanConfig.getClassName();
        String methodName = beanConfig.getMethodName();
        try {
            Object o = applicationContext.getBean(className);
            Class<?> clazz = o.getClass();
            Method m = clazz.getDeclaredMethod(methodName, Integer.class);
            m.setAccessible(true);
            Object obj = m.invoke(o, beanId);
            return obj;
        }  catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}
<think>好的,我需要解决用户的问题,即如何用Spring Boot和MyBatis实现后端查询折线图数据的需求。首先,我得理清用户的需求。 用户提到前端会传不同的时间段:本月、本年、自定义。当选择本月时,需要输出截止到当天的每天数据,并且如果传了地区参数,每个天的总数要包括所选地区的总和,同时每个地区单独的数量也要输出。时间跨度大时,后端需要调整查询粒度,比如按周或月分组。 首先,我需要设计一个数据表结构。假设有一个记录表,包含时间、地区和数量。可能的结构是:记录表(record)有字段record_date(日期)、region(地区)、count(数量)。但用户没有给出具体表结构,可能需要根据常规设计假设。 接下来考虑接口设计。用户需要接收时间类型(type)、开始和结束时间(自定义时使用)、地区列表。返回的数据结构应该包含日期、总数量和各个地区的数量。例如,一个每日的数据对象包含日期、总数量,以及一个地区数量的映射。 然后,处理不同的时间类型。例如,当类型是本月时,自动设置开始时间为本月1号,结束时间为今天。对于本年,可能从年初到当前日期。自定义则直接使用传入的开始和结束时间。 对于SQL部分,需要根据时间跨度和地区参数动态生成查询。如果时间跨度大,比如超过一个月,可能需要按周或月分组,而不是每天。这里需要根据时间范围动态选择分组方式,比如使用CASE语句或动态SQL。 在MyBatis的XML中,需要构建一个查询,按时间分组(可能是天、周、月),并统计总数以及各地区的数量。如果传入了地区参数,需要在WHERE条件中过滤这些地区,并且使用SUM和CASE WHEN来分别统计每个地区的数量。 例如,SQL可能像这样: SELECT DATE_FORMAT(record_date, '%Y-%m-%d') AS date, SUM(count) AS total, SUM(CASE WHEN region = '安庆' THEN count ELSE 0 END) AS anqing, SUM(CASE WHEN region = '合肥' THEN count ELSE 0 END) AS hefei FROM record WHERE record_date BETWEEN #{start} AND #{end} AND region IN ('安庆', '合肥') GROUP BY date; 但这里的问题是地区参数是动态的,用户可能传多个地区,所以需要动态生成CASE WHEN部分。这可能需要使用MyBatis的动态SQL功能,比如用foreach循环生成每个地区的统计列。 不过,MyBatis在处理动态列名时比较麻烦,可能需要使用注解或拼接SQL,但这样可能存在SQL注入风险。或者,可以在Java代码中构建这些CASE语句,作为字符串传递给XML。但需要确保参数安全。 另一个方法是,先查询所有记录,然后在Java中进行分组和统计。但这样如果数据量大,性能可能不佳。所以最好在SQL中处理。 因此,可能需要使用动态SQL来构建每个地区的SUM CASE语句。例如,在MyBatis的XML中,遍历地区列表,为每个地区生成一个SUM(CASE WHEN region = #{region} THEN count ELSE 0 END) AS #{region}。 但MyBatis的XML中,列别名不能直接使用参数,需要用${},但这会有注入风险。如果地区参数是用户输入的,可能会有问题。但这里地区参数是前端传的可选值,可能从固定列表中选择,比如下拉框,所以可以认为是安全的。 因此,在XML中,可以这样处理: <foreach collection="regions" item="region" separator=","> SUM(CASE WHEN region = #{region} THEN count ELSE 0 END) AS ${region} </foreach> 然后,在查询结果中,每个地区的别名就是地区名,例如“合肥”、“安庆”。这样,在Java的DTO中,可以用一个Map<String, Integer>来存储这些动态的地区数量。 但MyBatis的结果映射如何处理动态列呢?通常,结果映射需要明确的字段名,但这里列名是动态的。可能需要使用@ResultMap或者Map类型来接收结果,然后在Service层转换为所需的DTO结构。 或者,可以将查询结果映射为一个包含日期、总数量和地区数量Map的对象。例如,每个返回的DailyData对象包含日期、总数量和一个Map,其中键是地区名,值是对应的数量。 但MyBatis默认不支持直接映射到Map中的动态键。可能的解决方案是,在查询时返回一个包含所有地区的列,然后在Java中将这些列转换为Map。例如,DTO对象有属性date、total,以及各个地区的字段,如String hefei, String anqing等,但这需要预先知道所有可能的地区,不灵活。 所以,更好的方法是在查询时,将地区作为行的一部分,然后使用Java进行分组。例如,查询结果按日期和地区分组,然后在Service层按日期合并,计算总和和各地区的数量。但这样可能需要更复杂的SQL和Java处理。 或者,可以使用MyBatis的ResultHandler来自定义结果处理,将动态的列存入Map中。但这样可能比较复杂。 权衡之下,可能还是需要在SQL中动态生成每个地区的列,然后在DTO中使用一个Map来存储这些动态的列。例如,DTO如下: public class DailyData { private String date; private Integer total; private Map<String, Integer> regionCounts; } 然后,在MyBatis的结果映射中,可以使用@Result注解来映射固定字段date和total,而动态的地区列则需要特殊处理。可能需要使用@Results和@Result来手动映射,或者使用拦截器处理。 这可能比较复杂。另一种方法是,在SQL查询中使用JSON对象来聚合地区数据。例如,在MySQL中可以使用JSON_OBJECTAGG函数: SELECT DATE_FORMAT(record_date, '%Y-%m-%d') AS date, SUM(count) AS total, JSON_OBJECTAGG(region, SUM(count)) AS region_counts FROM record WHERE record_date BETWEEN #{start} AND #{end} AND region IN <foreach ...> GROUP BY date; 但这样需要数据库支持JSON函数,并且处理JSON结果。在Java中解析JSON字符串到Map中。 这可能是一个可行的方法,但需要数据库支持,例如MySQL 5.7以上。 假设数据库支持,那么可以在SQL中生成一个JSON对象,包含每个地区的数量,然后在Java中将这个JSON字符串转换为Map。 这样,MyBatis的ResultMap可以这样写: <result property="regionCounts" column="region_counts" typeHandler="com.example.JsonToMapTypeHandler"/> 然后自定义一个TypeHandler,将JSON字符串转换为Map<String, Integer>。 这可能是一个可行的方法,但需要处理TypeHandler和JSON转换。 不过,用户的问题中提到使用MyBatis,所以需要确保这种方法的可行性。 另一种方法是,在Service层处理,先查询所有满足条件的记录,然后按日期和地区进行分组统计。例如,查询所有在时间范围内且地区在参数列表中的记录,然后在内存中按日期分组,计算总和和各地区的数量。这可能适用于数据量不大的情况。 例如,SQL查询为: SELECT record_date, region, SUM(count) AS count FROM record WHERE record_date BETWEEN #{start} AND #{end} AND region IN <foreach collection="regions" item="region" open="(" separator="," close=")">#{region}</foreach> GROUP BY record_date, region 然后,在Java中,遍历结果,按日期分组,累积总和和各地区的数量。 这种方法可能更简单,不需要动态SQL生成多个列,但缺点是如果数据量大,查询结果的行数会很多,可能影响性能。 用户提到时间跨度大时,后端调整查询的时间跨度,比如按周或月分组。所以,可能需要根据时间范围的长短,动态决定按天、周或月分组。 例如,如果时间跨度超过30天,按周分组;超过3个月,按月分组。这需要在SQL中动态调整日期格式函数,比如按天:DATE_FORMAT(record_date, '%Y-%m-%d'),按周:CONCAT(YEAR(record_date), '-', WEEK(record_date)),按月:DATE_FORMAT(record_date, '%Y-%m')。 因此,在Java代码中,需要根据时间范围计算天数差,决定分组方式。然后在SQL中使用相应的日期格式。 综上,后端处理步骤可能如下: 1. 接收前端参数:时间类型(type)、开始结束时间(自定义时需要)、地区列表。 2. 根据时间类型计算实际的开始和结束时间。 3. 根据时间范围的长度决定分组粒度(天、周、月)。 4. 构建SQL查询,按分组粒度聚合数据,统计总数量和各个地区的数量。 5. 执行查询,处理结果,返回给前端。 现在考虑代码实现: 首先,定义请求参数DTO: public class ChartQueryParam { private String type; // month, year, custom private LocalDate startDate; private LocalDate endDate; private List<String> regions; } 在Controller中,处理请求: @PostMapping("/chart/data") public Result getChartData(@RequestBody ChartQueryParam param) { // 处理时间范围 LocalDate startDate; LocalDate endDate = LocalDate.now(); if ("month".equals(param.getType())) { startDate = LocalDate.now().withDayOfMonth(1); } else if ("year".equals(param.getType())) { startDate = LocalDate.now().withDayOfYear(1); } else if ("custom".equals(param.getType())) { startDate = param.getStartDate(); endDate = param.getEndDate(); } else { throw new IllegalArgumentException("Invalid type"); } // 调整结束时间为今天,如果结束时间超过今天 endDate = endDate.isAfter(LocalDate.now()) ? LocalDate.now() : endDate; // 计算时间跨度,决定分组粒度 long days = ChronoUnit.DAYS.between(startDate, endDate); String groupBy; if (days <= 30) { groupBy = "day"; } else if (days <= 90) { groupBy = "week"; } else { groupBy = "month"; } // 查询数据 List<DailyData> data = recordService.getChartData(startDate, endDate, param.getRegions(), groupBy); return Result.success(data); } 在Service层: public List<DailyData> getChartData(LocalDate start, LocalDate end, List<String> regions, String groupBy) { return recordMapper.selectChartData(start, end, regions, groupBy); } 在MyBatis的Mapper接口: List<DailyData> selectChartData(@Param("start") LocalDate start, @Param("end") LocalDate end, @Param("regions") List<String> regions, @Param("groupBy") String groupBy); 在XML映射文件中,需要动态构建分组表达式和地区统计: <select id="selectChartData" resultType="com.example.DailyData"> SELECT <choose> <when test="groupBy == 'day'"> DATE_FORMAT(record_date, '%Y-%m-%d') AS date, </when> <when test="groupBy == 'week'"> CONCAT(YEAR(record_date), '-', WEEK(record_date)) AS date, </when> <when test="groupBy == 'month'"> DATE_FORMAT(record_date, '%Y-%m') AS date, </when> </choose> SUM(count) AS total <if test="regions != null and regions.size() > 0"> <foreach collection="regions" item="region"> , SUM(CASE WHEN region = #{region} THEN count ELSE 0 END) AS ${region} </foreach> </if> FROM record WHERE record_date BETWEEN #{start} AND #{end} <if test="regions != null and regions.size() > 0"> AND region IN <foreach collection="regions" item="region" open="(" separator="," close=")"> #{region} </foreach> </if> GROUP BY date ORDER BY date </select> 这里使用了动态SQL来根据groupBy参数选择不同的日期格式,同时遍历地区列表生成每个地区的SUM CASE语句。结果列的名称是地区名,例如“合肥”、“安庆”。 然后,DailyData类需要能够接收这些动态列。一种方法是使用Map<String, Object>来接收额外的地区数量,但MyBatis默认会将查询结果映射到对象的属性中。因此,DailyData类应该有date、total属性,以及一个Map<String, Integer>来保存地区数量。但MyBatis无法直接映射动态列到Map中,除非使用@MapKey注解或其他方式。 另一种方法是,让DailyData类包含所有可能的地区字段,但这不现实。因此,可能需要使用MyBatis的结果处理器(ResultHandler)或者反射来动态设置字段。 不过,这可能比较复杂。另一种解决方法是让DailyData类包含一个泛型的属性,例如: public class DailyData { private String date; private Integer total; private Map<String, Integer> regionCounts; } 然后在MyBatis中,将每个地区的列值映射到regionCounts中。这需要自定义结果映射,可能不可行。 或者,可以使用一个通用的对象,如LinkedHashMap,作为结果类型,然后在Service层转换为DailyData对象。例如,Mapper接口返回List<Map<String, Object>>,每个Map代表一行数据,包含date、total以及各个地区的列名和值。然后,在Service层遍历这些Map,构建DailyData对象: public List<DailyData> getChartData(...) { List<Map<String, Object>> rows = recordMapper.selectChartData(...); List<DailyData> result = new ArrayList<>(); for (Map<String, Object> row : rows) { DailyData data = new DailyData(); data.setDate((String) row.get("date")); data.setTotal((Integer) row.get("total")); Map<String, Integer> regionCounts = new HashMap<>(); for (String region : regions) { regionCounts.put(region, (Integer) row.get(region)); } data.setRegionCounts(regionCounts); result.add(data); } return result; } 这样,Mapper接口返回的是List<Map<String, Object>>,然后在Service层进行转换。这种方法可能更灵活,但需要确保地区列表在Service层可用。 但此时,在Mapper的XML中,动态生成的列名必须与region参数一致,这样在Map中才能正确获取。 例如,当regions是["合肥", "安庆"]时,SQL生成的列名为合肥、安庆,对应的Map的键就是“合肥”、“安庆”,然后Service层根据传入的regions列表遍历这些键,从Map中获取值。 这样,即使某些日期没有某个地区的数量,也会显示0,因为SQL中的CASE WHEN已经处理了。 因此,调整Mapper接口返回类型为List<Map<String, Object>>,然后在Service中处理: 在Mapper接口中: List<Map<String, Object>> selectChartData(...); 在Service中: public List<DailyData> getChartData(LocalDate start, LocalDate end, List<String> regions, String groupBy) { List<Map<String, Object>> rows = recordMapper.selectChartData(start, end, regions, groupBy); List<DailyData> result = new ArrayList<>(); for (Map<String, Object> row : rows) { DailyData data = new DailyData(); data.setDate((String) row.get("date")); data.setTotal(((Number) row.get("total")).intValue()); Map<String, Integer> regionCounts = new HashMap<>(); if (regions != null) { for (String region : regions) { Object value = row.get(region); regionCounts.put(region, value != null ? ((Number) value).intValue() : 0); } } data.setRegionCounts(regionCounts); result.add(data); } return result; } 这样,即使某些地区在某个时间段没有数据,SUM会返回null,但CASE WHEN中ELSE 0,所以应该为0。不过需要确认数据库的SUM行为,当没有匹配时是否返回0还是null。例如,如果所有行的region都不匹配,SUM(CASE...)会是0吗?在MySQL中,如果没有匹配的行,SUM返回NULL。因此,可能需要使用COALESCE函数将NULL转换为0: SUM(COALESCE(CASE WHEN region = #{region} THEN count ELSE 0 END, 0)) AS ${region} 或者在Java代码中处理null值,将其视为0。 综上,调整SQL中的每个地区列为: SUM(COALESCE(CASE WHEN region = #{region} THEN count ELSE 0 END, 0)) AS ${region} 或者,在MyBatis的XML中: <foreach collection="regions" item="region"> , COALESCE(SUM(CASE WHEN region = #{region} THEN count ELSE 0 END), 0) AS ${region} </foreach> 这样可以确保即使没有数据,也会返回0。 现在,关于SQL注入的问题,因为地区名是用户输入的,但这里使用${region}作为列别名,存在风险。但假设地区参数是前端从固定选项中选择的,例如下拉框,而不是自由输入,那么可以认为是安全的。否则,需要验证地区参数是否在允许的列表中,防止SQL注入。 因此,在Service层,调用Mapper之前,应该验证regions参数是否都是有效的地区,例如查询数据库中的有效地区列表,或者有一个预定义的地区列表。例如: // 在Service方法中 if (regions != null) { List<String> validRegions = regionService.getValidRegions(); for (String region : regions) { if (!validRegions.contains(region)) { throw new IllegalArgumentException("Invalid region: " + region); } } } 这样可以确保传入的region参数都是合法的,避免SQL注入。 最后,关于时间跨度的调整,用户在问题中提到“时间跨度大的时间,后端适量调整查询的时间跨度”。例如,当时间跨度较大时,按周或月分组。前面的代码已经根据天数判断groupBy的类型,可能还需要进一步调整,比如当groupBy是周或月时,返回的date字段格式需要是周或月的标识,前端需要根据这个格式显示。 总结: - 控制器处理参数,计算时间范围和分组粒度。 - Service调用Mapper查询数据,处理结果转换。 - Mapper使用动态SQL生成按不同时间粒度分组的查询,并统计总数量和各个地区的数量。 - 结果以Map形式返回,Service层转换为结构化的DTO。 现在给出示例代码: 1. 请求参数DTO: public class ChartQueryParam { private String type; private LocalDate startDate; private LocalDate endDate; private List<String> regions; // getters and setters } 2. 返回结果DTO: public class DailyData { private String date; private Integer total; private Map<String, Integer> regionCounts; // getters and setters } 3. Controller: @RestController @RequestMapping("/api/chart") public class ChartController { @Autowired private RecordService recordService; @PostMapping("/data") public ResponseEntity<Result> getChartData(@RequestBody ChartQueryParam param) { List<DailyData> data = recordService.getChartData(param); return ResponseEntity.ok(Result.success(data)); } } 4. Service: @Service public class RecordService { @Autowired private RecordMapper recordMapper; public List<DailyData> getChartData(ChartQueryParam param) { // 处理时间范围 LocalDate startDate; LocalDate endDate = LocalDate.now(); switch (param.getType()) { case "month": startDate = LocalDate.now().withDayOfMonth(1); break; case "year": startDate = LocalDate.now().withDayOfYear(1); break; case "custom": startDate = param.getStartDate(); endDate = param.getEndDate(); break; default: throw new IllegalArgumentException("Invalid type"); } // 确保结束日期不超过今天 endDate = endDate.isAfter(LocalDate.now()) ? LocalDate.now() : endDate; // 计算时间跨度,决定分组粒度 long days = ChronoUnit.DAYS.between(startDate, endDate); String groupBy = "day"; if (days > 90) { groupBy = "month"; } else if (days > 30) { groupBy = "week"; } // 查询数据 List<Map<String, Object>> rows = recordMapper.selectChartData(startDate, endDate, param.getRegions(), groupBy); // 转换为DailyData列表 return convertToDailyData(rows, param.getRegions()); } private List<DailyData> convertToDailyData(List<Map<String, Object>> rows, List<String> regions) { List<DailyData> result = new ArrayList<>(); for (Map<String, Object> row : rows) { DailyData data = new DailyData(); data.setDate((String) row.get("date")); data.setTotal(((Number) row.get("total")).intValue()); Map<String, Integer> regionCounts = new HashMap<>(); if (regions != null) { for (String region : regions) { Object value = row.get(region); regionCounts.put(region, value != null ? ((Number) value).intValue() : 0); } } data.setRegionCounts(regionCounts); result.add(data); } return result; } } 5. Mapper接口: public interface RecordMapper { List<Map<String, Object>> selectChartData(@Param("start") LocalDate start, @Param("end") LocalDate end, @Param("regions") List<String> regions, @Param("groupBy") String groupBy); } 6. MyBatis XML映射: <select id="selectChartData" resultType="map"> SELECT <choose> <when test="groupBy == 'day'"> DATE_FORMAT(record_date, '%Y-%m-%d') AS date, </when> <when test="groupBy == 'week'"> CONCAT(YEAR(record_date), '-', WEEK(record_date, 3)) AS date, </when> <when test="groupBy == 'month'"> DATE_FORMAT(record_date, '%Y-%m') AS date, </when> <otherwise> DATE_FORMAT(record_date, '%Y-%m-%d') AS date, </otherwise> </choose> COALESCE(SUM(count), 0) AS total <if test="regions != null and regions.size() > 0"> <foreach collection="regions" item="region"> , COALESCE(SUM(CASE WHEN region = #{region} THEN count ELSE 0 END), 0) AS `${region}` </foreach> </if> FROM record WHERE record_date BETWEEN #{start} AND #{end} <if test="regions != null and regions.size() > 0"> AND region IN <foreach collection="regions" item="region" open="(" separator="," close=")"> #{region} </foreach> </if> GROUP BY date ORDER BY date </select> 注意:在MyBatis的XML中,使用`${region}`作为列别名时,需要用反引号包裹,防止地区名中有特殊字符,如空格等。但假设地区名是简单的汉字,可能不需要,但安全起见可以加上反引号。 此外,WEEK函数的第二个参数用于指定周的开始,这里使用3表示ISO周(周一作为开始)。 最后,确保数据库中的record表有record_date(日期类型)、region(字符串)、count(整数)字段。 测试用例: 假设查询本月数据,地区参数是合肥和安庆。 SQL会生成类似: SELECT DATE_FORMAT(record_date, '%Y-%m-%d') AS date, COALESCE(SUM(count), 0) AS total, COALESCE(SUM(CASE WHEN region = '合肥' THEN count ELSE 0 END), 0) AS `合肥`, COALESCE(SUM(CASE WHEN region = '安庆' THEN count ELSE 0 END), 0) AS `安庆` FROM record WHERE record_date BETWEEN '2023-10-01' AND '2023-10-20' AND region IN ('合肥', '安庆') GROUP BY date ORDER BY date 返回结果中,每个日期有总数量、合肥和安庆的数量。 在Java中,Service层将这些结果转换为DailyData对象,包含日期、总数量和地区数量映射。 这样,前端收到的数据结构大致是: [ { "date": "2023-10-01", "total": 100, "regionCounts": { "合肥": 60, "安庆": 40 } }, ... ] 总结,这个方案通过动态SQL处理不同的时间粒度和地区参数,确保查询效率和数据的准确性。同时,Service层进行结果转换,保证返回结构清晰。需要注意地区参数的验证和SQL注入的防范,确保安全性。</think>### 分步解决方案及代码示例 #### 1. 数据库表结构设计 假设有记录表 `record`,包含以下字段: - `record_date` DATE COMMENT '记录日期', - `region` VARCHAR(20) COMMENT '地区', - `count` INT COMMENT '数量' ```sql CREATE TABLE record ( record_date DATE, region VARCHAR(20), count INT ); ``` #### 2. DTO类定义 - **请求参数类** `ChartQueryParam.java` ```java import lombok.Data; import java.time.LocalDate; import java.util.List; @Data public class ChartQueryParam { private String type; // month, year, custom private LocalDate startDate; private LocalDate endDate; private List<String> regions; } ``` - **返回数据类** `DailyData.java` ```java import lombok.Data; import java.util.Map; @Data public class DailyData { private String date; // 日期 private Integer total; // 总数 private Map<String, Integer> regionCounts; // 地区对应数量 } ``` #### 3. Controller层 处理请求参数,调用Service层 ```java import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/chart") public class ChartController { @Autowired private RecordService recordService; @PostMapping("/data") public ResponseEntity<Result> getChartData(@RequestBody ChartQueryParam param) { List<DailyData> data = recordService.getChartData(param); return ResponseEntity.ok(Result.success(data)); } } ``` #### 4. Service层 处理业务逻辑,调用Mapper查询数据并转换结果 ```java import org.springframework.stereotype.Service; import java.time.LocalDate; import java.time.temporal.ChronoUnit; import java.util.*; @Service public class RecordService { @Autowired private RecordMapper recordMapper; public List<DailyData> getChartData(ChartQueryParam param) { // 处理时间范围 LocalDate startDate = calculateStartDate(param.getType()); LocalDate endDate = validateEndDate(param.getEndDate()); // 计算分组粒度 String groupBy = calculateGroupBy(startDate, endDate); // 查询数据 List<Map<String, Object>> rows = recordMapper.selectChartData( startDate, endDate, param.getRegions(), groupBy ); // 转换为DailyData列表 return convertToDailyData(rows, param.getRegions()); } private LocalDate calculateStartDate(String type) { switch (type) { case "month": return LocalDate.now().withDayOfMonth(1); case "year": return LocalDate.now().withDayOfYear(1); default: throw new IllegalArgumentException("Invalid type"); } } private LocalDate validateEndDate(LocalDate endDate) { return endDate.isAfter(LocalDate.now()) ? LocalDate.now() : endDate; } private String calculateGroupBy(LocalDate start, LocalDate end) { long days = ChronoUnit.DAYS.between(start, end); if (days > 90) return "month"; else if (days > 30) return "week"; else return "day"; } private List<DailyData> convertToDailyData(List<Map<String, Object>> rows, List<String> regions) { List<DailyData> result = new ArrayList<>(); for (Map<String, Object> row : rows) { DailyData data = new DailyData(); data.setDate((String) row.get("date")); data.setTotal(((Number) row.get("total")).intValue()); data.setRegionCounts(extractRegionCounts(row, regions)); result.add(data); } return result; } private Map<String, Integer> extractRegionCounts(Map<String, Object> row, List<String> regions) { Map<String, Integer> counts = new HashMap<>(); if (regions != null) { regions.forEach(region -> { Object value = row.get(region); counts.put(region, value != null ? ((Number) value).intValue() : 0); }); } return counts; } } ``` #### 5. Mapper接口及XML映射 - **Mapper接口** `RecordMapper.java` ```java import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.time.LocalDate; import java.util.List; import java.util.Map; @Mapper public interface RecordMapper { List<Map<String, Object>> selectChartData( @Param("start") LocalDate start, @Param("end") LocalDate end, @Param("regions") List<String> regions, @Param("groupBy") String groupBy ); } ``` - **XML映射** `RecordMapper.xml` ```xml <?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.example.mapper.RecordMapper"> <select id="selectChartData" resultType="map"> SELECT <choose> <when test="groupBy == 'day'"> DATE_FORMAT(record_date, '%Y-%m-%d') AS date, </when> <when test="groupBy == 'week'"> CONCAT(YEAR(record_date), '-', WEEK(record_date, 3)) AS date, </when> <when test="groupBy == 'month'"> DATE_FORMAT(record_date, '%Y-%m') AS date, </when> </choose> COALESCE(SUM(count), 0) AS total <if test="regions != null and regions.size() > 0"> <foreach collection="regions" item="region"> , COALESCE(SUM(CASE WHEN region = #{region} THEN count ELSE 0 END), 0) AS `${region}` </foreach> </if> FROM record WHERE record_date BETWEEN #{start} AND #{end} <if test="regions != null and regions.size() > 0"> AND region IN <foreach collection="regions" item="region" open="(" separator="," close=")"> #{region} </foreach> </if> GROUP BY date ORDER BY date </select> </mapper> ``` ### 关键点说明 1. **动态时间处理**:根据前端传入的`type`自动计算时间范围,支持本月、本年、自定义 2. **智能分组策略**:根据时间跨度动态调整分组粒度(天/周/月) 3. **地区过滤统计**:使用动态SQL生成多个地区的统计列,保证查询效率 4. **结果集处理**:通过Map接收动态列结果,在Service层转换为结构化数据 5. **空值处理**:使用`COALESCE`确保空值返回0,保持数据一致性 ### 使用示例 **请求示例(本月+地区过滤)**: ```json { "type": "month", "regions": ["合肥", "安庆"] } ``` **响应示例**: ```json [ { "date": "2023-10-01", "total": 150, "regionCounts": { "合肥": 100, "安庆": 50 } }, { "date": "2023-10-02", "total": 200, "regionCounts": { "合肥": 120, "安庆": 80 } } ] ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值