fix(ops): XML 手写 SQL 添加 tenant_id 预编译参数过滤

yudao 官方明确 MyBatis Plus 拦截器不处理 XML 手写 SQL,需手动过滤。
将 ${} OGNL 表达式改为 #{tenantId} 预编译参数,避免 NPE 和
@TenantIgnore 不兼容问题。

- OpsOrderMapper: 8 条统计 SQL 添加 AND tenant_id = #{tenantId}
- OpsTrafficStatisticsMapper: deleteByStatHourBefore 补上 tenant_id
- OpsStatisticsServiceImpl: 10 处调用传入 tenantId 参数
- TrafficStatisticsCleanupJob: executeIgnore → @TenantJob + 显式传参

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
lzh
2026-03-30 11:40:39 +08:00
parent df2d14ce26
commit 74f6207843
6 changed files with 214 additions and 189 deletions

View File

@@ -31,6 +31,7 @@ public interface OpsTrafficStatisticsMapper extends BaseMapperX<OpsTrafficStatis
* @param beforeTime 截止时间
* @return 删除的记录数
*/
int deleteByStatHourBefore(@Param("beforeTime") LocalDateTime beforeTime);
int deleteByStatHourBefore(@Param("beforeTime") LocalDateTime beforeTime,
@Param("tenantId") Long tenantId);
}

View File

@@ -99,33 +99,38 @@ public interface OpsOrderMapper extends BaseMapperX<OpsOrderDO> {
*/
List<DateCountRespVO> selectCreatedCountGroupByDate(@Param("orderType") String orderType,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime);
@Param("endTime") LocalDateTime endTime,
@Param("tenantId") Long tenantId);
/**
* 按日期分组统计工单完成数
*/
List<DateCountRespVO> selectCompletedCountGroupByDate(@Param("orderType") String orderType,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime);
@Param("endTime") LocalDateTime endTime,
@Param("tenantId") Long tenantId);
/**
* 按日期分组统计平均响应时长和完成时长(秒)
*/
List<AvgTimeRespVO> selectAvgTimeGroupByDate(@Param("orderType") String orderType,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime);
@Param("endTime") LocalDateTime endTime,
@Param("tenantId") Long tenantId);
/**
* 按区域分组统计工单数和完成数(非取消的工单)
*/
List<AreaCountRespVO> selectCountGroupByAreaId(@Param("orderType") String orderType);
List<AreaCountRespVO> selectCountGroupByAreaId(@Param("orderType") String orderType,
@Param("tenantId") Long tenantId);
/**
* 按小时分组统计工单创建数
*/
List<HourCountRespVO> selectCountGroupByHour(@Param("orderType") String orderType,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime);
@Param("endTime") LocalDateTime endTime,
@Param("tenantId") Long tenantId);
/**
* 按星期和小时分组统计工单创建数(用于热力图)
@@ -133,18 +138,21 @@ public interface OpsOrderMapper extends BaseMapperX<OpsOrderDO> {
*/
List<HeatmapRespVO> selectCountGroupByDayOfWeekAndHour(@Param("orderType") String orderType,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime);
@Param("endTime") LocalDateTime endTime,
@Param("tenantId") Long tenantId);
/**
* 按保洁类型分组统计平均作业时长(分钟)
* JOIN ops_order_clean_ext使用 arrived_time 和 completed_time 计算
*/
List<CleaningTypeAvgDurationRespVO> selectAvgDurationGroupByCleaningType(@Param("orderType") String orderType);
List<CleaningTypeAvgDurationRespVO> selectAvgDurationGroupByCleaningType(@Param("orderType") String orderType,
@Param("tenantId") Long tenantId);
/**
* 按功能类型分组统计工单数和完成数(非取消的工单)
*/
List<FunctionTypeCountRespVO> selectCountGroupByFunctionType(@Param("orderType") String orderType);
List<FunctionTypeCountRespVO> selectCountGroupByFunctionType(@Param("orderType") String orderType,
@Param("tenantId") Long tenantId);
// 注意分页查询方法需要在Service层实现这里只提供基础查询方法
// 具体分页查询请参考Service实现

View File

@@ -1,53 +1,55 @@
package com.viewsh.module.ops.service.job;
import com.viewsh.framework.tenant.core.util.TenantUtils;
import com.viewsh.module.ops.dal.mysql.statistics.OpsTrafficStatisticsMapper;
import com.xxl.job.core.handler.annotation.XxlJob;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* 客流统计清理任务
* <p>
* 每月 1 日凌晨 2 点执行,删除 30 天前的客流统计记录
*
* @author AI
*/
@Slf4j
@Component
public class TrafficStatisticsCleanupJob {
@Resource
private OpsTrafficStatisticsMapper trafficStatisticsMapper;
/**
* 清理过期的客流统计记录
* <p>
* XxlJob 配置:
* - Cron: 0 0 2 1 * ? (每月 1 日凌晨 2 点)
*
* @return 执行结果
*/
@XxlJob("trafficStatisticsCleanupJob")
public String execute() {
log.info("[TrafficStatisticsCleanupJob] 开始执行客流统计清理任务");
try {
LocalDateTime beforeTime = LocalDateTime.now().minusDays(30);
// 使用 executeIgnore 忽略租户过滤,清理所有租户的过期数据
int deletedCount = TenantUtils.executeIgnore(
() -> trafficStatisticsMapper.deleteByStatHourBefore(beforeTime));
log.info("[TrafficStatisticsCleanupJob] 客流统计清理完成:删除 {} 条记录(截止时间={}",
deletedCount, beforeTime);
return "清理完成:删除 " + deletedCount + " 条记录";
} catch (Exception e) {
log.error("[TrafficStatisticsCleanupJob] 客流统计清理失败", e);
return "清理失败: " + e.getMessage();
}
}
}
package com.viewsh.module.ops.service.job;
import com.viewsh.framework.tenant.core.context.TenantContextHolder;
import com.viewsh.framework.tenant.core.job.TenantJob;
import com.viewsh.module.ops.dal.mysql.statistics.OpsTrafficStatisticsMapper;
import com.xxl.job.core.handler.annotation.XxlJob;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* 客流统计清理任务
* <p>
* 每月 1 日凌晨 2 点执行,删除 30 天前的客流统计记录。
* 使用 @TenantJob 遍历每个租户逐个清理,确保租户隔离。
*
* @author AI
*/
@Slf4j
@Component
public class TrafficStatisticsCleanupJob {
@Resource
private OpsTrafficStatisticsMapper trafficStatisticsMapper;
/**
* 清理过期的客流统计记录
* <p>
* XxlJob 配置:
* - Cron: 0 0 2 1 * ? (每月 1 日凌晨 2 点)
*
* @return 执行结果
*/
@XxlJob("trafficStatisticsCleanupJob")
@TenantJob
public String execute() {
Long tenantId = TenantContextHolder.getRequiredTenantId();
log.info("[TrafficStatisticsCleanupJob] 开始执行客流统计清理任务, tenantId={}", tenantId);
try {
LocalDateTime beforeTime = LocalDateTime.now().minusDays(30);
int deletedCount = trafficStatisticsMapper.deleteByStatHourBefore(beforeTime, tenantId);
log.info("[TrafficStatisticsCleanupJob] 客流统计清理完成tenantId={}, 删除 {} 条记录(截止时间={}",
tenantId, deletedCount, beforeTime);
return "清理完成:删除 " + deletedCount + " 条记录";
} catch (Exception e) {
log.error("[TrafficStatisticsCleanupJob] 客流统计清理失败, tenantId={}", tenantId, e);
return "清理失败: " + e.getMessage();
}
}
}

View File

@@ -2,6 +2,7 @@ package com.viewsh.module.ops.service.statistics;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.viewsh.framework.tenant.core.context.TenantContextHolder;
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.*;
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.DashboardStatsRespVO.*;
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.DeviceTrafficRealtimeRespVO;
@@ -621,8 +622,9 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
LocalDateTime endDateTime = endDate.plusDays(1).atStartOfDay();
// 一次查询获取所有天的创建数
Long tenantId = TenantContextHolder.getRequiredTenantId();
List<DateCountRespVO> createdRows = opsOrderMapper.selectCreatedCountGroupByDate(
orderType, startDateTime, endDateTime);
orderType, startDateTime, endDateTime, tenantId);
Map<String, Integer> createdMap = createdRows.stream()
.collect(Collectors.toMap(
row -> row.getStatDate().toString(),
@@ -632,7 +634,7 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
// 一次查询获取所有天的完成数
List<DateCountRespVO> completedRows = opsOrderMapper.selectCompletedCountGroupByDate(
orderType, startDateTime, endDateTime);
orderType, startDateTime, endDateTime, tenantId);
Map<String, Integer> completedMap = completedRows.stream()
.collect(Collectors.toMap(
row -> row.getStatDate().toString(),
@@ -666,7 +668,7 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
LocalDateTime startDateTime,
LocalDateTime endDateTime) {
List<HourCountRespVO> rows = opsOrderMapper.selectCountGroupByHour(
orderType, startDateTime, endDateTime);
orderType, startDateTime, endDateTime, TenantContextHolder.getRequiredTenantId());
// 构建小时 -> 计数映射
int[] hourBuckets = new int[24];
@@ -696,7 +698,7 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
LocalDateTime endDateTime = endDate.plusDays(1).atStartOfDay();
List<AvgTimeRespVO> rows = opsOrderMapper.selectAvgTimeGroupByDate(
orderType, startDateTime, endDateTime);
orderType, startDateTime, endDateTime, TenantContextHolder.getRequiredTenantId());
// 构建日期 -> 响应/完成时长映射
Map<String, Double> responseMap = new HashMap<>();
@@ -779,7 +781,7 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
LocalDateTime startDateTime,
LocalDateTime endDateTime) {
List<HeatmapRespVO> rows = opsOrderMapper.selectCountGroupByDayOfWeekAndHour(
orderType, startDateTime, endDateTime);
orderType, startDateTime, endDateTime, TenantContextHolder.getRequiredTenantId());
// 生成近7天的日期列表格式MM-dd
LocalDate startDate = startDateTime.toLocalDate();
@@ -843,7 +845,8 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
* 构建功能类型排行(将枚举值转换为中文)
*/
private List<FunctionTypeRankingItem> buildFunctionTypeRanking(String orderType) {
List<FunctionTypeCountRespVO> rows = opsOrderMapper.selectCountGroupByFunctionType(orderType);
List<FunctionTypeCountRespVO> rows = opsOrderMapper.selectCountGroupByFunctionType(
orderType, TenantContextHolder.getRequiredTenantId());
return rows.stream()
.map(row -> {
@@ -959,7 +962,7 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
LocalDateTime todayStart,
LocalDateTime todayEnd) {
List<HourCountRespVO> rows = opsOrderMapper.selectCountGroupByHour(
orderType, todayStart, todayEnd);
orderType, todayStart, todayEnd, TenantContextHolder.getRequiredTenantId());
int[] hourBuckets = new int[24];
for (HourCountRespVO row : rows) {
@@ -1079,7 +1082,7 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
LocalDateTime endDateTime = endDate.plusDays(1).atStartOfDay();
List<DateCountRespVO> rows = opsOrderMapper.selectCreatedCountGroupByDate(
orderType, startDateTime, endDateTime);
orderType, startDateTime, endDateTime, TenantContextHolder.getRequiredTenantId());
Map<String, Integer> countMap = rows.stream()
.collect(Collectors.toMap(
row -> row.getStatDate().toString(),

View File

@@ -14,6 +14,7 @@
<delete id="deleteByStatHourBefore">
DELETE FROM ops_traffic_statistics
WHERE stat_hour &lt; #{beforeTime}
AND tenant_id = #{tenantId}
AND deleted = 0
</delete>

View File

@@ -1,119 +1,129 @@
<?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.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper">
<!-- 按日期分组统计工单创建数 -->
<select id="selectCreatedCountGroupByDate" resultType="com.viewsh.module.ops.controller.admin.workorder.vo.statistics.DateCountRespVO">
SELECT DATE(create_time) AS statDate, COUNT(*) AS cnt
FROM ops_order
WHERE order_type = #{orderType}
AND deleted = 0
AND create_time &gt;= #{startTime}
AND create_time &lt; #{endTime}
GROUP BY DATE(create_time)
ORDER BY statDate
</select>
<!-- 按日期分组统计工单完成数 -->
<select id="selectCompletedCountGroupByDate" resultType="com.viewsh.module.ops.controller.admin.workorder.vo.statistics.DateCountRespVO">
SELECT DATE(end_time) AS statDate, COUNT(*) AS cnt
FROM ops_order
WHERE order_type = #{orderType}
AND status = 'COMPLETED'
AND deleted = 0
AND end_time &gt;= #{startTime}
AND end_time &lt; #{endTime}
GROUP BY DATE(end_time)
ORDER BY statDate
</select>
<!-- 按日期分组统计平均响应时长和完成时长 -->
<select id="selectAvgTimeGroupByDate" resultType="com.viewsh.module.ops.controller.admin.workorder.vo.statistics.AvgTimeRespVO">
SELECT DATE(end_time) AS statDate,
AVG(CASE WHEN response_seconds > 0 THEN response_seconds ELSE NULL END) AS avgResponse,
AVG(CASE WHEN completion_seconds > 0 THEN completion_seconds ELSE NULL END) AS avgCompletion
FROM ops_order
WHERE order_type = #{orderType}
AND status = 'COMPLETED'
AND deleted = 0
AND end_time &gt;= #{startTime}
AND end_time &lt; #{endTime}
GROUP BY DATE(end_time)
ORDER BY statDate
</select>
<!-- 按区域分组统计工单数和完成数 -->
<select id="selectCountGroupByAreaId" resultType="com.viewsh.module.ops.controller.admin.workorder.vo.statistics.AreaCountRespVO">
SELECT area_id AS areaId,
COUNT(*) AS totalCount,
SUM(CASE WHEN status = 'COMPLETED' THEN 1 ELSE 0 END) AS completedCount
FROM ops_order
WHERE order_type = #{orderType}
AND status != 'CANCELLED'
AND deleted = 0
AND area_id IS NOT NULL
GROUP BY area_id
ORDER BY totalCount DESC
LIMIT 10
</select>
<!-- 按小时分组统计工单创建数 -->
<select id="selectCountGroupByHour" resultType="com.viewsh.module.ops.controller.admin.workorder.vo.statistics.HourCountRespVO">
SELECT HOUR(create_time) AS hourVal, COUNT(*) AS cnt
FROM ops_order
WHERE order_type = #{orderType}
AND deleted = 0
AND create_time &gt;= #{startTime}
AND create_time &lt; #{endTime}
GROUP BY HOUR(create_time)
ORDER BY hourVal
</select>
<!-- 按星期和小时分组统计(热力图) -->
<select id="selectCountGroupByDayOfWeekAndHour" resultType="com.viewsh.module.ops.controller.admin.workorder.vo.statistics.HeatmapRespVO">
SELECT DAYOFWEEK(create_time) AS dayOfWeek,
HOUR(create_time) AS hourVal,
COUNT(*) AS cnt
FROM ops_order
WHERE order_type = #{orderType}
AND deleted = 0
AND create_time &gt;= #{startTime}
AND create_time &lt; #{endTime}
GROUP BY DAYOFWEEK(create_time), HOUR(create_time)
ORDER BY dayOfWeek, hourVal
</select>
<!-- 按保洁类型分组统计平均作业时长JOIN clean_ext -->
<select id="selectAvgDurationGroupByCleaningType" resultType="com.viewsh.module.ops.controller.admin.workorder.vo.statistics.CleaningTypeAvgDurationRespVO">
SELECT ext.cleaning_type AS cleaningType,
ROUND(AVG(TIMESTAMPDIFF(MINUTE, ext.arrived_time, ext.completed_time))) AS avgDuration
FROM ops_order_clean_ext ext
INNER JOIN ops_order o ON ext.ops_order_id = o.id
WHERE o.status = 'COMPLETED'
AND o.order_type = #{orderType}
AND o.deleted = 0
AND ext.deleted = 0
AND ext.completed_time IS NOT NULL
AND ext.arrived_time IS NOT NULL
AND ext.cleaning_type IS NOT NULL
GROUP BY ext.cleaning_type
</select>
<!-- 按功能类型分组统计工单数和完成数 -->
<select id="selectCountGroupByFunctionType" resultType="com.viewsh.module.ops.controller.admin.workorder.vo.statistics.FunctionTypeCountRespVO">
SELECT a.function_type AS functionType,
COUNT(o.id) AS totalCount,
SUM(CASE WHEN o.status = 'COMPLETED' THEN 1 ELSE 0 END) AS completedCount
FROM ops_order o
INNER JOIN ops_bus_area a ON o.area_id = a.id
WHERE o.order_type = #{orderType}
AND o.status != 'CANCELLED'
AND o.deleted = 0
AND a.deleted = 0
AND a.function_type IS NOT NULL
GROUP BY a.function_type
ORDER BY totalCount DESC
LIMIT 10
</select>
</mapper>
<?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.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper">
<!-- 按日期分组统计工单创建数 -->
<select id="selectCreatedCountGroupByDate" resultType="com.viewsh.module.ops.controller.admin.workorder.vo.statistics.DateCountRespVO">
SELECT DATE(create_time) AS statDate, COUNT(*) AS cnt
FROM ops_order
WHERE order_type = #{orderType}
AND tenant_id = #{tenantId}
AND deleted = 0
AND create_time &gt;= #{startTime}
AND create_time &lt; #{endTime}
GROUP BY DATE(create_time)
ORDER BY statDate
</select>
<!-- 按日期分组统计工单完成数 -->
<select id="selectCompletedCountGroupByDate" resultType="com.viewsh.module.ops.controller.admin.workorder.vo.statistics.DateCountRespVO">
SELECT DATE(end_time) AS statDate, COUNT(*) AS cnt
FROM ops_order
WHERE order_type = #{orderType}
AND tenant_id = #{tenantId}
AND status = 'COMPLETED'
AND deleted = 0
AND end_time &gt;= #{startTime}
AND end_time &lt; #{endTime}
GROUP BY DATE(end_time)
ORDER BY statDate
</select>
<!-- 按日期分组统计平均响应时长和完成时长 -->
<select id="selectAvgTimeGroupByDate" resultType="com.viewsh.module.ops.controller.admin.workorder.vo.statistics.AvgTimeRespVO">
SELECT DATE(end_time) AS statDate,
AVG(CASE WHEN response_seconds > 0 THEN response_seconds ELSE NULL END) AS avgResponse,
AVG(CASE WHEN completion_seconds > 0 THEN completion_seconds ELSE NULL END) AS avgCompletion
FROM ops_order
WHERE order_type = #{orderType}
AND tenant_id = #{tenantId}
AND status = 'COMPLETED'
AND deleted = 0
AND end_time &gt;= #{startTime}
AND end_time &lt; #{endTime}
GROUP BY DATE(end_time)
ORDER BY statDate
</select>
<!-- 按区域分组统计工单数和完成数 -->
<select id="selectCountGroupByAreaId" resultType="com.viewsh.module.ops.controller.admin.workorder.vo.statistics.AreaCountRespVO">
SELECT area_id AS areaId,
COUNT(*) AS totalCount,
SUM(CASE WHEN status = 'COMPLETED' THEN 1 ELSE 0 END) AS completedCount
FROM ops_order
WHERE order_type = #{orderType}
AND tenant_id = #{tenantId}
AND status != 'CANCELLED'
AND deleted = 0
AND area_id IS NOT NULL
GROUP BY area_id
ORDER BY totalCount DESC
LIMIT 10
</select>
<!-- 按小时分组统计工单创建数 -->
<select id="selectCountGroupByHour" resultType="com.viewsh.module.ops.controller.admin.workorder.vo.statistics.HourCountRespVO">
SELECT HOUR(create_time) AS hourVal, COUNT(*) AS cnt
FROM ops_order
WHERE order_type = #{orderType}
AND tenant_id = #{tenantId}
AND deleted = 0
AND create_time &gt;= #{startTime}
AND create_time &lt; #{endTime}
GROUP BY HOUR(create_time)
ORDER BY hourVal
</select>
<!-- 按星期和小时分组统计(热力图) -->
<select id="selectCountGroupByDayOfWeekAndHour" resultType="com.viewsh.module.ops.controller.admin.workorder.vo.statistics.HeatmapRespVO">
SELECT DAYOFWEEK(create_time) AS dayOfWeek,
HOUR(create_time) AS hourVal,
COUNT(*) AS cnt
FROM ops_order
WHERE order_type = #{orderType}
AND tenant_id = #{tenantId}
AND deleted = 0
AND create_time &gt;= #{startTime}
AND create_time &lt; #{endTime}
GROUP BY DAYOFWEEK(create_time), HOUR(create_time)
ORDER BY dayOfWeek, hourVal
</select>
<!-- 按保洁类型分组统计平均作业时长JOIN clean_ext -->
<select id="selectAvgDurationGroupByCleaningType" resultType="com.viewsh.module.ops.controller.admin.workorder.vo.statistics.CleaningTypeAvgDurationRespVO">
SELECT ext.cleaning_type AS cleaningType,
ROUND(AVG(TIMESTAMPDIFF(MINUTE, ext.arrived_time, ext.completed_time))) AS avgDuration
FROM ops_order_clean_ext ext
INNER JOIN ops_order o ON ext.ops_order_id = o.id
WHERE o.status = 'COMPLETED'
AND o.tenant_id = #{tenantId}
AND o.order_type = #{orderType}
AND o.deleted = 0
AND ext.tenant_id = #{tenantId}
AND ext.deleted = 0
AND ext.completed_time IS NOT NULL
AND ext.arrived_time IS NOT NULL
AND ext.cleaning_type IS NOT NULL
GROUP BY ext.cleaning_type
</select>
<!-- 按功能类型分组统计工单数和完成数 -->
<select id="selectCountGroupByFunctionType" resultType="com.viewsh.module.ops.controller.admin.workorder.vo.statistics.FunctionTypeCountRespVO">
SELECT a.function_type AS functionType,
COUNT(o.id) AS totalCount,
SUM(CASE WHEN o.status = 'COMPLETED' THEN 1 ELSE 0 END) AS completedCount
FROM ops_order o
INNER JOIN ops_bus_area a ON o.area_id = a.id
WHERE o.order_type = #{orderType}
AND o.tenant_id = #{tenantId}
AND o.status != 'CANCELLED'
AND o.deleted = 0
AND a.tenant_id = #{tenantId}
AND a.deleted = 0
AND a.function_type IS NOT NULL
GROUP BY a.function_type
ORDER BY totalCount DESC
LIMIT 10
</select>
</mapper>