<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
}
}
]
```