Forráskód Böngészése

perf(app): 通过批量查询优化首页及场地评论数据加载

- 首页接口批量获取所有教练好评率,避免N+1查询问题
- 场地列表批量获取评论统计数据,减少单条查询次数
- AppSite数据批量预加载,优化processPlaceVO内查询效率
- 修改价格规则SQL,左连接教学时间表动态获取is_teaching字段
- 订单预览接口根据教学日配置动态调整时间段及库存计算
- 增加评价Mapper批量查询多个教练好评率及多个场地评论统计方法
- 在评估服务实现类中新增批量获取教练好评率的方法
- 增加日志打印,便于追踪教学日和非教学日时间段的加载流程及数据准确性
SheepHy 2 hete
szülő
commit
b0dfed6677

+ 8 - 5
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/app/controller/AppHomeController.java

@@ -8,21 +8,20 @@ import lombok.extern.slf4j.Slf4j;
 import org.jeecg.common.api.vo.Result;
 import org.jeecg.modules.app.dto.GetPlaceListDTO;
 import org.jeecg.modules.app.dto.SearchDTO;
-import org.jeecg.modules.app.dto.evaluate.FindEvaluateDTO;
-import org.jeecg.modules.app.dto.evaluate.FindEvaluatePage;
 import org.jeecg.modules.app.service.IAppHomeService;
 import org.jeecg.modules.app.service.IUserService;
 import org.jeecg.modules.app.vo.HomeVO;
 import org.jeecg.modules.app.vo.MsgInfoVO;
 import org.jeecg.modules.app.vo.MsgVO;
 import org.jeecg.modules.app.vo.PlaceVO;
-import org.jeecg.modules.system.app.dto.evaluate.FindEvaluateResponseDTO;
 import org.jeecg.modules.system.app.entity.AppSearchHot;
 import org.jeecg.modules.system.app.service.IEvaluateService;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
 import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
 
 @Slf4j
 @Tag(name = "App首页相关接口")
@@ -40,9 +39,13 @@ public class AppHomeController {
     @Operation(summary = "首页基础数据查询")
     public Result<HomeVO> homeInfo() {
         HomeVO homeVO = appHomeService.homeInfo();
+        // 优化:批量查询所有教练的好评率,避免 N+1 查询问题
+        List<String> instructorIds = homeVO.getInstructorList().stream()
+                .map(instructorVO -> instructorVO.getId())
+                .collect(Collectors.toList());
+        Map<String, Long> goodRateMap = evaluateService.batchGetGoodRateByInstructorIds(instructorIds);
         homeVO.getInstructorList().forEach(instructorVO -> {
-            FindEvaluatePage<FindEvaluateResponseDTO> byOrderPage = evaluateService.findByOrderPage(new FindEvaluateDTO().setInstructorId(instructorVO.getId()));
-            instructorVO.setGoodRate(byOrderPage.getApplauseRate());
+            instructorVO.setGoodRate(goodRateMap.getOrDefault(instructorVO.getId(), 0L));
         });
         return Result.ok(homeVO);
     }

+ 32 - 6
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/app/service/impl/AppHomeServiceImpl.java

@@ -223,9 +223,24 @@ public class AppHomeServiceImpl implements IAppHomeService {
                 Map<String, Long> scoreNumMap = new HashMap<>();
                 Map<String, Long> scoreSumMap = new HashMap<>();
                 if (!placeIds.isEmpty()) {
-                    for (String placeId : placeIds) {
-                        scoreNumMap.put(placeId, evaluateMapper.findByScoreNum(placeId));
-                        scoreSumMap.put(placeId, evaluateMapper.findByAverageScore(placeId));
+                    // 优化:批量查询场地评论统计,避免 N+1 查询问题
+                    List<Map<String, Object>> evaluateStats = evaluateMapper.batchFindEvaluateStatsBySiteIds(placeIds);
+                    if (evaluateStats != null) {
+                        for (Map<String, Object> stat : evaluateStats) {
+                            String siteId = (String) stat.get("siteId");
+                            Long scoreNum = stat.get("scoreNum") != null ? ((Number) stat.get("scoreNum")).longValue() : 0L;
+                            Long scoreSum = stat.get("scoreSum") != null ? ((Number) stat.get("scoreSum")).longValue() : 0L;
+                            scoreNumMap.put(siteId, scoreNum);
+                            scoreSumMap.put(siteId, scoreSum);
+                        }
+                    }
+                }
+                // 优化:批量查询所有需要的 AppSite 数据,避免 processPlaceVO 中循环查询
+                Map<String, AppSite> appSiteMap = new HashMap<>();
+                if (!placeIds.isEmpty()) {
+                    List<AppSite> allSites = appSiteMapper.selectBatchIds(placeIds);
+                    for (AppSite site : allSites) {
+                        appSiteMap.put(site.getId(), site);
                     }
                 }
                 Map<String, String> categoryNameCache = new HashMap<>();
@@ -253,7 +268,7 @@ public class AppHomeServiceImpl implements IAppHomeService {
                         }
                     }
 
-                    processPlaceVO(placeVO, getPlaceListDTO, sitePlacesMap, priceRulesMap, scoreNum, scoreSum, categoryNames);
+                    processPlaceVO(placeVO, getPlaceListDTO, sitePlacesMap, priceRulesMap, scoreNum, scoreSum, categoryNames, appSiteMap);
                 }
                 sortPlacesIfNecessary(records, getPlaceListDTO.getVenueType());
             }
@@ -268,7 +283,8 @@ public class AppHomeServiceImpl implements IAppHomeService {
     private void processPlaceVO(PlaceVO placeVO, GetPlaceListDTO getPlaceListDTO,
                                 Map<String, List<AppSitePlace>> sitePlacesMap,
                                 Map<String, List<AppSitePriceRules>> priceRulesMap,
-                                Long scoreNum, Long scoreSum, List<String> categoryNames) {
+                                Long scoreNum, Long scoreSum, List<String> categoryNames,
+                                Map<String, AppSite> appSiteMap) {
         // 检查是否有票价规则
         boolean ticketWhether = false;
         List<AppSitePlace> sitePlaces = sitePlacesMap.getOrDefault(placeVO.getId(), Collections.emptyList());
@@ -276,14 +292,24 @@ public class AppHomeServiceImpl implements IAppHomeService {
             for (AppSitePlace sitePlace : sitePlaces) { // 使用增强for循环替代索引循环
                 List<AppSitePriceRules> priceRules = priceRulesMap.getOrDefault(
                         sitePlace.getId(), Collections.emptyList());
-                AppSite appSite = appSiteMapper.selectById(sitePlace.getSiteId());
+                // 优化:使用预加载的 Map 而不是单独查询
+                AppSite appSite = appSiteMap.get(sitePlace.getSiteId());
+                if (appSite == null) {
+                    continue;
+                }
                 // 解析教学日数据
                 String teachingDayJson = appSite.getTeachingDay();
+                if (teachingDayJson == null) {
+                    continue;
+                }
                 JsonObject teachingDayObj = JsonParser.parseString(teachingDayJson).getAsJsonObject();
                 JsonArray teachingDayData = teachingDayObj.getAsJsonArray("data");
                 int teachingDayTicketNum = teachingDayData.get(0).getAsJsonObject().get("ticketNum").getAsInt();
                 // 解析非教学日数据
                 String noTeachingDayJson = appSite.getNoTeachingDay();
+                if (noTeachingDayJson == null) {
+                    continue;
+                }
                 JsonObject noTeachingDayObj = JsonParser.parseString(noTeachingDayJson).getAsJsonObject();
                 JsonArray noTeachingDayData = noTeachingDayObj.getAsJsonArray("data");
                 int noTeachingDayTicketNum = noTeachingDayData.get(0).getAsJsonObject().get("ticketNum").getAsInt();

+ 72 - 22
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/app/service/impl/OrderServiceImpl.java

@@ -209,30 +209,43 @@ public class OrderServiceImpl extends ServiceImpl<AppOrderMapper, AppOrder> impl
         // 解析教学日和非教学日数据
         List<OrderVO.TimeSlotData> teachingList = parseTimeSlotData(appSite.getTeachingDay());
         List<OrderVO.TimeSlotData> nonTeachingList = parseTimeSlotData(appSite.getNoTeachingDay());
+        
+        log.info("[previewOrderPlaceSchool] placeId={}, startTime={}, teachingList={}, nonTeachingList={}", 
+                placeId, DateUtil.format(startTime, "yyyy-MM-dd"), appSite.getTeachingDay(), appSite.getNoTeachingDay());
 
         timeSlot.forEach(a -> {
-            LocalTime slotStart = LocalTime.parse(DateUtil.format(a.getStartTime(), "HH:mm"));
-            LocalTime slotEnd = LocalTime.parse(DateUtil.format(a.getEndTime(), "HH:mm"));
+            // is_teaching: 0=是(教学日), 1=否(非教学日)
             boolean isTeaching = a.getIsTeaching() == 0;
             List<OrderVO.TimeSlotData> targetList = isTeaching ? teachingList : nonTeachingList;
-            Optional<OrderVO.TimeSlotData> matchedSlot = targetList.stream()
-                    .filter(t -> {
-                        LocalTime invStart = LocalTime.parse(t.getStartTime());
-                        LocalTime invEnd = LocalTime.parse(t.getEndTime());
-                        // 严格匹配有交集的时间段(不包含端点相接)
-                        return invStart.isBefore(invEnd) && slotStart.isBefore(invEnd) && slotEnd.isAfter(invStart);
-                    })
-                    .findFirst();
-            matchedSlot.ifPresent(timeSlotData -> {
-                int totalInventory = Integer.parseInt(timeSlotData.getTicketNum());
-                int bookedCount = appOrderMapper.queryBookedCount(
-                        placeId,
-                        DateUtil.format(startTime, "yyyy-MM-dd"),
-                        timeSlotData.getStartTime(),
-                        timeSlotData.getEndTime()
-                );
-                a.setInventory(totalInventory - bookedCount);
-            });
+            log.info("[previewOrderPlaceSchool] 原始timeSlot: is_teaching={}, 使用配置={}, startTime={}, endTime={}", 
+                    a.getIsTeaching(), isTeaching ? "教学日" : "非教学日", a.getStartTime(), a.getEndTime());
+            
+            // 根据 is_teaching 动态替换时间段配置
+            if (!targetList.isEmpty()) {
+                OrderVO.TimeSlotData timeSlotData = targetList.get(0);
+                try {
+                    // 更新时间段
+                    LocalTime newStartTime = LocalTime.parse(timeSlotData.getStartTime(), DateTimeFormatter.ofPattern("HH:mm"));
+                    LocalTime newEndTime = LocalTime.parse(timeSlotData.getEndTime(), DateTimeFormatter.ofPattern("HH:mm"));
+                    a.setStartTime(java.sql.Time.valueOf(newStartTime));
+                    a.setEndTime(java.sql.Time.valueOf(newEndTime));
+                    a.setName(timeSlotData.getStartTime() + "-" + timeSlotData.getEndTime());
+                    
+                    // 计算库存
+                    int totalInventory = Integer.parseInt(timeSlotData.getTicketNum());
+                    int bookedCount = appOrderMapper.queryBookedCount(
+                            placeId,
+                            DateUtil.format(startTime, "yyyy-MM-dd"),
+                            timeSlotData.getStartTime(),
+                            timeSlotData.getEndTime()
+                    );
+                    a.setInventory(totalInventory - bookedCount);
+                    log.info("[previewOrderPlaceSchool] 更新后timeSlot: startTime={}, endTime={}, inventory={}", 
+                            a.getStartTime(), a.getEndTime(), a.getInventory());
+                } catch (Exception e) {
+                    log.error("[previewOrderPlaceSchool] 解析时间段失败: {}", e.getMessage());
+                }
+            }
         });
         AppSitePlace appSitePlace = appSitePlaceMapper.selectById(placeId);
         previewOrderPlaceSchool.setName(appSite.getName())
@@ -360,13 +373,50 @@ public class OrderServiceImpl extends ServiceImpl<AppOrderMapper, AppOrder> impl
 
     @Override
     public List<OrderVO.PreviewOrderPlaceSchoolTime> previewOrderPlaceSchoolTime(String placeId) {
-        List<OrderVO.PreviewOrderPlaceSchoolTime> previewOrderPlaceSchoolTimes = appTeachingTimeMapper.previewOrderPlaceSchoolTime(appSitePlaceMapper.selectById(placeId).getOrgCode());
-        AppSite appSite = appSiteMapper.selectOne(Wrappers.<AppSite>lambdaQuery().eq(AppSite::getId, appSitePlaceMapper.selectById(placeId).getSiteId()));
+        AppSitePlace sitePlace = appSitePlaceMapper.selectById(placeId);
+        String orgCode = sitePlace.getOrgCode();
+        log.info("[previewOrderPlaceSchoolTime] placeId={}, orgCode={}, siteId={}", placeId, orgCode, sitePlace.getSiteId());
+        
+        // 直接查询 nm_teaching_time 表的原始数据,确认数据库中的配置
+        List<AppTeachingTime> rawTeachingTimes = appTeachingTimeMapper.selectList(
+                Wrappers.<AppTeachingTime>lambdaQuery()
+                        .eq(AppTeachingTime::getOrgCode, orgCode)
+                        .ge(AppTeachingTime::getDay, cn.hutool.core.date.DateUtil.beginOfDay(new java.util.Date()))
+                        .le(AppTeachingTime::getDay, cn.hutool.core.date.DateUtil.offsetDay(new java.util.Date(), 6))
+                        .eq(AppTeachingTime::getDelFlag, 0)
+        );
+        log.info("[previewOrderPlaceSchoolTime] 原始数据库查询 nm_teaching_time 表,找到{}条记录:", rawTeachingTimes.size());
+        rawTeachingTimes.forEach(t -> log.info("  -> day={}, is_teaching={}", t.getDay(), t.getIsTeaching()));
+        
+        List<OrderVO.PreviewOrderPlaceSchoolTime> previewOrderPlaceSchoolTimes = appTeachingTimeMapper.previewOrderPlaceSchoolTime(orgCode);
+        AppSite appSite = appSiteMapper.selectOne(Wrappers.<AppSite>lambdaQuery().eq(AppSite::getId, sitePlace.getSiteId()));
+        
+        log.info("[previewOrderPlaceSchoolTime] Mapper查询到{}条教学时间记录", previewOrderPlaceSchoolTimes.size());
+        previewOrderPlaceSchoolTimes.forEach(t -> log.info("[previewOrderPlaceSchoolTime] day={}, is_teaching={}, namedDay={}", t.getDay(), t.getIsTeaching(), t.getNamedDay()));
+        
         // 解析教学日和非教学日数据
         List<OrderVO.TimeSlotData> teachingList = parseTimeSlotData(appSite.getTeachingDay());
         List<OrderVO.TimeSlotData> nonTeachingList = parseTimeSlotData(appSite.getNoTeachingDay());
+        
+        log.info("[previewOrderPlaceSchoolTime] teachingList size={}, nonTeachingList size={}", teachingList.size(), nonTeachingList.size());
+        log.info("[previewOrderPlaceSchoolTime] 教学日原始配置 teachingDay={}", appSite.getTeachingDay());
+        log.info("[previewOrderPlaceSchoolTime] 非教学日原始配置 noTeachingDay={}", appSite.getNoTeachingDay());
+        if (!teachingList.isEmpty()) {
+            log.info("[previewOrderPlaceSchoolTime] teachingList[0]: startTime={}, endTime={}, ticketNum={}", 
+                    teachingList.get(0).getStartTime(), teachingList.get(0).getEndTime(), teachingList.get(0).getTicketNum());
+        }
+        if (!nonTeachingList.isEmpty()) {
+            log.info("[previewOrderPlaceSchoolTime] nonTeachingList[0]: startTime={}, endTime={}, ticketNum={}", 
+                    nonTeachingList.get(0).getStartTime(), nonTeachingList.get(0).getEndTime(), nonTeachingList.get(0).getTicketNum());
+        }
+        
         previewOrderPlaceSchoolTimes.forEach(previewOrderPlaceSchoolTime -> {
+            // is_teaching: 0=是(教学日), 1=否(非教学日)
             boolean isTeaching = previewOrderPlaceSchoolTime.getIsTeaching() == 0;
+            log.info("[previewOrderPlaceSchoolTime] day={}, is_teaching={}, 使用配置={}", 
+                    previewOrderPlaceSchoolTime.getDay(), 
+                    previewOrderPlaceSchoolTime.getIsTeaching(), 
+                    isTeaching ? "教学日(teachingList)" : "非教学日(nonTeachingList)");
             List<OrderVO.TimeSlotData> targetList = isTeaching ? teachingList : nonTeachingList;
             if (!targetList.isEmpty()) {
                 // 找到最早开始时间

+ 15 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/mapper/EvaluateMapper.java

@@ -9,6 +9,7 @@ import org.jeecg.modules.system.app.dto.evaluate.FindEvaluateResponseDTO;
 import org.jeecg.modules.system.app.entity.Evaluate;
 
 import java.util.List;
+import java.util.Map;
 
 /**
  * @Description: 评价管理
@@ -39,4 +40,18 @@ public interface EvaluateMapper extends BaseMapper<Evaluate> {
     Long findByGoodScoreNumByCoursesId(@Param("coursesIds")List<String> coursesIds);
 
     String findPositiveRating(@Param("id") String id);
+
+    /**
+     * 批量查询多个教练的好评率
+     * @param instructorIds 教练ID列表
+     * @return 教练ID与好评率的映射列表
+     */
+    List<Map<String, Object>> batchFindGoodRateByInstructorIds(@Param("instructorIds") List<String> instructorIds);
+
+    /**
+     * 批量查询多个场地的评论统计(评论数和评分总和)
+     * @param siteIds 场地ID列表
+     * @return 场地ID与评论统计的映射列表
+     */
+    List<Map<String, Object>> batchFindEvaluateStatsBySiteIds(@Param("siteIds") List<String> siteIds);
 }

+ 13 - 11
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/mapper/xml/AppSitePriceRulesMapper.xml

@@ -137,19 +137,21 @@
     </select>
     <select id="timeSlot" resultType="org.jeecg.modules.app.vo.OrderVO$PreviewOrderPlaceSchoolChild">
         SELECT
-            id,
-            inventory,
-            selling_price AS price,
-            is_teaching,
-            start_time,
-            end_time,
-            CONCAT( DATE_FORMAT( start_time, '%H:%i' ), '-', DATE_FORMAT( end_time, '%H:%i' ) ) AS NAME
+            r.id,
+            r.inventory,
+            r.selling_price AS price,
+            COALESCE(t.is_teaching, r.is_teaching) AS is_teaching,
+            r.start_time,
+            r.end_time,
+            CONCAT( DATE_FORMAT( r.start_time, '%H:%i' ), '-', DATE_FORMAT( r.end_time, '%H:%i' ) ) AS NAME
         FROM
-            `nm_site_price_rules`
+            `nm_site_price_rules` r
+        LEFT JOIN `nm_site_place` p ON r.site_place_id = p.id
+        LEFT JOIN `nm_teaching_time` t ON t.org_code = p.org_code AND t.day = r.date_of_sale AND t.del_flag = 0
         WHERE
-            del_flag = 0
-           AND date_of_sale = #{startTime}
-          AND site_place_id = #{placeId};
+            r.del_flag = 0
+           AND r.date_of_sale = #{startTime}
+          AND r.site_place_id = #{placeId};
     </select>
     <select id="previewOrderPlaceGymnasiumChartered" resultType="org.jeecg.modules.app.vo.OrderVO$PreviewOrderTimePeriod">
 

+ 34 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/mapper/xml/EvaluateMapper.xml

@@ -160,4 +160,38 @@
         AND o.product_ids  = #{id}
 
     </select>
+
+    <!-- 批量查询多个教练的好评率 -->
+    <select id="batchFindGoodRateByInstructorIds" resultType="java.util.Map">
+        SELECT
+            ni.user_id AS instructorId,
+            COALESCE(
+                ROUND(
+                    COUNT(CASE WHEN ne.score >= 4 THEN 1 END) * 100.0 / NULLIF(COUNT(ne.id), 0)
+                , 0),
+            0) AS goodRate
+        FROM nm_instructor ni
+        LEFT JOIN nm_courses nc ON ni.user_id = nc.user_id AND nc.del_flag = 0
+        LEFT JOIN nm_order o ON FIND_IN_SET(nc.id, o.product_ids)
+        LEFT JOIN nm_evaluate ne ON ne.order_id = o.id AND ne.check_status = 1
+        WHERE ni.del_flag = 0 AND ni.user_id IN
+        <foreach collection="instructorIds" item="instructorId" open="(" separator="," close=")">
+            #{instructorId}
+        </foreach>
+        GROUP BY ni.user_id
+    </select>
+
+    <!-- 批量查询多个场地的评论统计 -->
+    <select id="batchFindEvaluateStatsBySiteIds" resultType="java.util.Map">
+        SELECT
+            ne.site_id AS siteId,
+            COUNT(1) AS scoreNum,
+            COALESCE(SUM(ne.score), 0) AS scoreSum
+        FROM nm_evaluate ne
+        WHERE ne.check_status = 1 AND ne.site_id IN
+        <foreach collection="siteIds" item="siteId" open="(" separator="," close=")">
+            #{siteId}
+        </foreach>
+        GROUP BY ne.site_id
+    </select>
 </mapper>

+ 10 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/service/IEvaluateService.java

@@ -9,6 +9,9 @@ import org.jeecg.modules.system.app.dto.evaluate.FindEvaluateRequestDTO;
 import org.jeecg.modules.system.app.dto.evaluate.FindEvaluateResponseDTO;
 import org.jeecg.modules.system.app.entity.Evaluate;
 
+import java.util.List;
+import java.util.Map;
+
 /**
  * @Description: 评价管理
  * @Author: jeecg-boot
@@ -22,5 +25,12 @@ public interface IEvaluateService extends IService<Evaluate> {
 
     FindEvaluatePage<FindEvaluateResponseDTO> findByOrderPage(FindEvaluateDTO findEvaluateDTO);
 
+    /**
+     * 批量查询多个教练的好评率
+     * @param instructorIds 教练ID列表
+     * @return 教练ID与好评率的映射
+     */
+    Map<String, Long> batchGetGoodRateByInstructorIds(List<String> instructorIds);
+
     Evaluate findByOrder(String orderId);
 }

+ 25 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/service/impl/EvaluateServiceImpl.java

@@ -20,7 +20,9 @@ import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * @Description: 评价管理
@@ -110,6 +112,29 @@ public class EvaluateServiceImpl extends ServiceImpl<EvaluateMapper, Evaluate> i
         return evaluateMapper.findByOrderId(orderId);
     }
 
+    @Override
+    public Map<String, Long> batchGetGoodRateByInstructorIds(List<String> instructorIds) {
+        Map<String, Long> resultMap = new HashMap<>();
+        if (instructorIds == null || instructorIds.isEmpty()) {
+            return resultMap;
+        }
+        List<Map<String, Object>> results = evaluateMapper.batchFindGoodRateByInstructorIds(instructorIds);
+        if (results != null) {
+            for (Map<String, Object> result : results) {
+                String instructorId = (String) result.get("instructorId");
+                Object goodRateObj = result.get("goodRate");
+                Long goodRate = 0L;
+                if (goodRateObj != null) {
+                    if (goodRateObj instanceof Number) {
+                        goodRate = ((Number) goodRateObj).longValue();
+                    }
+                }
+                resultMap.put(instructorId, goodRate);
+            }
+        }
+        return resultMap;
+    }
+
     public static BigDecimal calculateAverage(Long scoreSum, Long scoreNum) {
         if (scoreNum == 0L) {
           return  BigDecimal.valueOf(0).setScale(1 , RoundingMode.DOWN);