Sfoglia il codice sorgente

feat(game-score): 增强成绩导出功能支持项目筛选和排名限制

- 在GameEventProjectService中新增queryListByEventIdAndProjectIds方法
- 优化GameScoreController的exportScoresDetail接口支持topN和projectIds参数
- 新增exportProjectScore接口用于导出单个项目成绩详情
- 在成绩详情导出中增加项目类型和项目名称字段显示
- 实现topN排名限制功能,支持导出前N名成绩
- 优化数据查询逻辑,提高导出性能
- 更新相关服务层和控制器的方法签名和实现逻辑
zhou 1 settimana fa
parent
commit
2ee17bcebd

+ 21 - 3
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/GameScoreController.java

@@ -216,12 +216,30 @@ public class GameScoreController extends BaseController {
     }
 
     /**
-     * 导出成绩详情数据(全部)
+     * 导出成绩详情数据(全部项目
      */
     @SaCheckPermission("system:gameScore:export")
     @Log(title = "导出成绩详情", businessType = BusinessType.EXPORT)
     @PostMapping("/exportScoresDetail")
-    public void exportScoresDetail(@RequestParam("eventId") Long eventId, HttpServletResponse response) {
-        gameScoreService.exportScoresDetail(eventId, response);
+    public void exportScoresDetail(
+            @RequestParam("eventId") Long eventId,
+            @RequestParam(value = "topN", required = false) Integer topN,
+            @RequestParam(value = "projectIds", required = false) List<Long> projectIds,
+            HttpServletResponse response) {
+        gameScoreService.exportScoresDetail(eventId, topN, projectIds, response);
+    }
+
+    /**
+     * 导出单个项目成绩详情
+     */
+    @SaCheckPermission("system:gameScore:export")
+    @Log(title = "导出项目成绩", businessType = BusinessType.EXPORT)
+    @PostMapping("/exportProjectScore")
+    public void exportProjectScore(
+            @RequestParam("eventId") Long eventId,
+            @RequestParam("projectId") Long projectId,
+            @RequestParam(value = "topN", required = false) Integer topN,
+            HttpServletResponse response) {
+        gameScoreService.exportProjectScore(eventId, projectId, topN, response);
     }
 }

+ 7 - 3
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/IGameEventProjectService.java

@@ -5,12 +5,10 @@ import org.dromara.system.domain.vo.GameEventProjectVo;
 import org.dromara.system.domain.bo.GameEventProjectBo;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.common.mybatis.core.page.PageQuery;
-import org.dromara.system.service.impl.GameEventProjectServiceImpl;
 
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 
 /**
  * 赛事项目Service接口
@@ -45,7 +43,7 @@ public interface IGameEventProjectService {
      * @return 赛事项目分页列表
      */
     TableDataInfo<GameEventProjectVo> queryAllProjectList(GameEventProjectBo bo, PageQuery pageQuery);
-    
+
     /**
      * 分页查询项目列表并包含实时人数、队数、组数统计
      */
@@ -124,4 +122,10 @@ public interface IGameEventProjectService {
      * @return
      */
     Map<Long, String> queryNameByEventIdAndProjectIds(Long eventId, List<Long> projectIds);
+
+    /**
+     * 根据赛事id和项目id查询项目信息
+     * 项目ID列表为空的话就查询所有
+     */
+    List<GameEventProjectVo> queryListByEventIdAndProjectIds(Long eventId, List<Long> projectIds);
 }

+ 13 - 2
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/IGameScoreService.java

@@ -141,9 +141,20 @@ public interface IGameScoreService {
     void exportBonusExcel(Map<String, Object> data, HttpServletResponse response);
 
     /**
-     * 导出成绩详情数据(全部)
+     * 导出成绩详情数据(全部或筛选项目
      * @param eventId 赛事ID
+     * @param topN 导出前几名(null或0表示全部)
+     * @param projectIds 选中的项目ID列表(为空则导出全部)
      * @param response HTTP响应对象
      */
-    void exportScoresDetail(Long eventId, HttpServletResponse response);
+    void exportScoresDetail(Long eventId, Integer topN, List<Long> projectIds, HttpServletResponse response);
+
+    /**
+     * 导出单个项目的成绩详情
+     * @param eventId 赛事ID
+     * @param projectId 项目ID
+     * @param topN 导出前几名(null或0表示全部)
+     * @param response HTTP响应对象
+     */
+    void exportProjectScore(Long eventId, Long projectId, Integer topN, HttpServletResponse response);
 }

+ 13 - 3
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/GameEventProjectServiceImpl.java

@@ -9,7 +9,6 @@ import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import lombok.extern.slf4j.Slf4j;
 import org.dromara.common.redis.utils.RedisUtils;
@@ -662,10 +661,9 @@ public class GameEventProjectServiceImpl implements IGameEventProjectService {
      */
     @Override
     public List<GameEventProjectVo> queryListByEventId(Long eventId) {
-        List<GameEventProjectVo> projects = baseMapper.selectVoList(
+        return baseMapper.selectVoList(
                 Wrappers.lambdaQuery(GameEventProject.class)
                         .eq(GameEventProject::getEventId, eventId));
-        return projects;
     }
 
     /**
@@ -736,4 +734,16 @@ public class GameEventProjectServiceImpl implements IGameEventProjectService {
 
         return TableDataInfo.build(result);
     }
+
+    /**
+     * 根据赛事id和项目id查询项目信息
+     * 项目ID列表为空的话就查询所有
+     */
+    @Override
+    public List<GameEventProjectVo> queryListByEventIdAndProjectIds(Long eventId, List<Long> projectIds) {
+        return baseMapper.selectVoList(
+                Wrappers.lambdaQuery(GameEventProject.class)
+                        .eq(GameEventProject::getEventId, eventId)
+                        .in(projectIds != null && !projectIds.isEmpty(),GameEventProject::getProjectId, projectIds));
+    }
 }

+ 134 - 31
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/GameScoreServiceImpl.java

@@ -1359,23 +1359,22 @@ public class GameScoreServiceImpl implements IGameScoreService {
         try {
             // 1. 获取所有项目(绕过 Service 层繁重的统计逻辑,直接查持久层)
             List<GameEventProjectVo> projects = gameEventProjectMapper.selectVoList(
-                Wrappers.lambdaQuery(GameEventProject.class)
-                    .eq(GameEventProject::getEventId, eventId)
-                    .eq(GameEventProject::getDelFlag, "0")
-            );
+                    Wrappers.lambdaQuery(GameEventProject.class)
+                            .eq(GameEventProject::getEventId, eventId)
+                            .eq(GameEventProject::getDelFlag, "0"));
 
             // 2. 获取所有队伍(手动实现 ancestors 过滤,彻底避开 Service 层的 N+1 问题)
             LambdaQueryWrapper<GameTeam> teamWrapper = Wrappers.lambdaQuery(GameTeam.class)
-                .eq(GameTeam::getEventId, eventId);
+                    .eq(GameTeam::getEventId, eventId);
 
             List<GameTeamVo> teams;
             if (rgId != null) {
                 // 找出当前分组及其所有子孙分组的 ID
                 List<Long> allRgIds = gameRankGroupMapper.selectList(
-                    Wrappers.lambdaQuery(GameRankGroup.class)
-                        .select(GameRankGroup::getRgId)
-                        .apply("(rg_id = {0} OR FIND_IN_SET({0}, ancestors))", rgId)
-                ).stream().map(GameRankGroup::getRgId).collect(Collectors.toList());
+                        Wrappers.lambdaQuery(GameRankGroup.class)
+                                .select(GameRankGroup::getRgId)
+                                .apply("(rg_id = {0} OR FIND_IN_SET({0}, ancestors))", rgId))
+                        .stream().map(GameRankGroup::getRgId).collect(Collectors.toList());
 
                 if (allRgIds.isEmpty()) {
                     teams = new ArrayList<>();
@@ -1631,14 +1630,14 @@ public class GameScoreServiceImpl implements IGameScoreService {
      * @param response HTTP响应对象
      */
     @Override
-    public void exportScoresDetail(Long eventId, HttpServletResponse response) {
+    public void exportScoresDetail(Long eventId, Integer topN, List<Long> projectIds, HttpServletResponse response) {
         try {
-            log.info("开始导出成绩详情,eventId: {}", eventId);
+            log.info("开始导出成绩详情,eventId: {}, topN: {}, projectIds: {}", eventId, topN, projectIds);
 
-            // 1. 获取所有项目
-            List<GameEventProjectVo> projects = gameEventProjectService.queryListByEventId(eventId);
+            // 1. 获取项目列表并根据 projectIds 过滤
+            List<GameEventProjectVo> projects = gameEventProjectService.queryListByEventIdAndProjectIds(eventId, projectIds);
             if (projects.isEmpty()) {
-                throw new RuntimeException("未找到赛事项目");
+                throw new RuntimeException("未找到待导出的赛事项目");
             }
 
             // 2. 创建Excel工作簿
@@ -1651,7 +1650,7 @@ public class GameScoreServiceImpl implements IGameScoreService {
 
                 // 4. 为每个项目创建Sheet页
                 for (GameEventProjectVo project : projects) {
-                    createProjectSheet(workbook, project, eventId, headerStyle, dataStyleOdd, dataStyleEven);
+                    createProjectSheet(workbook, project, eventId, topN, headerStyle, dataStyleOdd, dataStyleEven);
                 }
 
                 // 5. 设置响应头并输出
@@ -1670,7 +1669,7 @@ public class GameScoreServiceImpl implements IGameScoreService {
     /**
      * 为单个项目创建Sheet页
      */
-    private void createProjectSheet(Workbook workbook, GameEventProjectVo project, Long eventId,
+    private void createProjectSheet(Workbook workbook, GameEventProjectVo project, Long eventId, Integer topN,
             CellStyle headerStyle, CellStyle dataStyleOdd, CellStyle dataStyleEven) {
 
         // 1. 创建Sheet页,名称限制在31个字符内
@@ -1678,18 +1677,37 @@ public class GameScoreServiceImpl implements IGameScoreService {
         Sheet sheet = workbook.createSheet(sheetName);
 
         // 2. 获取项目详细数据
-        List<Map<String, Object>> projectData = getProjectScoreDataAll(
+        List<Map<String, Object>> projectScore = getProjectScoreDataAll(
                 eventId,
                 project.getProjectId(),
                 project.getClassification());
+        if (projectScore == null || projectScore.isEmpty()){
+            throw new RuntimeException("未找到待导出的赛事项目成绩详细数据");
+        }
 
-        // 3. 创建标题行
+        // 3. 如果指定了 topN,则过滤前N名
+        if (topN != null && topN > 0) {
+            projectScore = projectScore.stream()
+                .filter(data -> {
+                    Object rank = data.get("scoreRank");
+                    return rank != null && (Integer)rank > 0;
+                })
+                .sorted((a, b) -> {
+                    Integer rankA = (Integer) a.get("scoreRank");
+                    Integer rankB = (Integer) b.get("scoreRank");
+                    return rankA.compareTo(rankB);
+                })
+                .limit(topN)
+                .collect(Collectors.toList());
+        }
+
+        // 4. 创建标题行
         createProjectHeaderRow(sheet, project, headerStyle);
 
-        // 4. 填充数据行
-        fillProjectDataRows(sheet, projectData, project, dataStyleOdd, dataStyleEven);
+        // 5. 填充数据行
+        fillProjectDataRows(sheet, projectScore, project, dataStyleOdd, dataStyleEven);
 
-        // 5. 自动调整列宽
+        // 6. 自动调整列宽
         autoSizeColumns(sheet);
     }
 
@@ -1830,6 +1848,8 @@ public class GameScoreServiceImpl implements IGameScoreService {
             data.put("teamCode", team.getTeamCode());
             data.put("eventId", eventId);
             data.put("projectId", projectId);
+            data.put("projectType", project.getProjectType());
+            data.put("projectName", project.getProjectName());
 
             // 查询成绩信息
             GameScoreVo score = getScoreByAthleteIdAndProjectId(athlete.getAthleteId(), projectId);
@@ -1872,8 +1892,8 @@ public class GameScoreServiceImpl implements IGameScoreService {
         if (ProjectClassification.SINGLE.getValue().equals(project.getClassification())) {
             // 个人项目列标题
             String[] headers = {
-                    "序号", "队伍编号", "队伍名称", "姓名", "号码",
-                    "个人成绩", "积分", "排名", "更新时间"
+                    "序号", "队伍编号", "队伍名称", "项目类型", "项目", "号码", "姓名",
+                    "性别", "个人成绩", "积分", "排名", "更新时间"
             };
 
             for (String header : headers) {
@@ -1884,7 +1904,7 @@ public class GameScoreServiceImpl implements IGameScoreService {
         } else {
             // 团体项目列标题
             String[] headers = {
-                    "序号", "队伍编号", "队伍名称", "团队成绩",
+                    "序号", "队伍编号", "队伍名称", "项目类型", "项目", "团队成绩",
                     "积分", "排名", "更新时间"
             };
 
@@ -1896,15 +1916,34 @@ public class GameScoreServiceImpl implements IGameScoreService {
         }
     }
 
+    /**
+     * 根据项目类型值获取对应的标签
+     * @param dictInfo 项目类型字典列表
+     * @param dictValue 项目类型值
+     * @return 项目类型标签,如果未找到则返回原值
+     */
+    private String getDictLabel(List<SysDictDataVo> dictInfo, Object dictValue) {
+        if (dictValue == null || dictInfo == null) {
+            return "";
+        }
+
+        return dictInfo.stream()
+            .filter(pt -> Objects.equals(pt.getDictValue(), dictValue.toString()))
+            .map(SysDictDataVo::getDictLabel)
+            .findFirst()
+            .orElse(dictValue.toString());
+    }
+
     /**
      * 填充项目数据行
      */
-    private void fillProjectDataRows(Sheet sheet, List<Map<String, Object>> projectData,
+    private void fillProjectDataRows(Sheet sheet, List<Map<String, Object>> projectScore,
             GameEventProjectVo project, CellStyle dataStyleOdd, CellStyle dataStyleEven) {
 
+        List<SysDictDataVo> projectTypes = dictTypeService.selectDictDataByType("game_project_type");
         int rowIndex = 1;
-        for (int i = 0; i < projectData.size(); i++) {
-            Map<String, Object> data = projectData.get(i);
+        for (int i = 0; i < projectScore.size(); i++) {
+            Map<String, Object> data = projectScore.get(i);
             Row dataRow = sheet.createRow(rowIndex++);
 
             // 选择行样式
@@ -1917,11 +1956,12 @@ public class GameScoreServiceImpl implements IGameScoreService {
                 createCellWithStyle(dataRow, colIndex++, i + 1, currentRowStyle); // 序号
                 createCellWithStyle(dataRow, colIndex++, data.get("teamCode"), currentRowStyle); // 队伍编号
                 createCellWithStyle(dataRow, colIndex++, data.get("teamName"), currentRowStyle); // 队伍名称
+                createCellWithStyle(dataRow, colIndex++, getDictLabel(projectTypes, data.get("projectType")), currentRowStyle); // 项目类型
+                createCellWithStyle(dataRow, colIndex++, data.get("projectName"), currentRowStyle); // 项目名称
                 createCellWithStyle(dataRow, colIndex++, data.get("athleteCode"), currentRowStyle); // 号码
                 createCellWithStyle(dataRow, colIndex++, data.get("name"), currentRowStyle); // 姓名
-                createCellWithStyle(dataRow, colIndex++, data.get("gender") == null ?  "" : data.get("gender").equals("1") ? "男" : "女", currentRowStyle); // 性别
-                createCellWithStyle(dataRow, colIndex++, data.get("projectType"), currentRowStyle); // 项目类型
-                createCellWithStyle(dataRow, colIndex++, data.get("projectName"), currentRowStyle); // 项目名称
+                createCellWithStyle(dataRow, colIndex++,
+                        data.get("gender") == null ? "" : data.get("gender").equals("1") ? "男" : "女", currentRowStyle); // 性别
                 createCellWithStyle(dataRow, colIndex++, data.get("individualPerformance"), currentRowStyle); // 个人成绩
                 createCellWithStyle(dataRow, colIndex++, data.get("scorePoint"), currentRowStyle); // 积分
                 createCellWithStyle(dataRow, colIndex++, data.get("scoreRank"), currentRowStyle); // 排名
@@ -1931,7 +1971,7 @@ public class GameScoreServiceImpl implements IGameScoreService {
                 createCellWithStyle(dataRow, colIndex++, i + 1, currentRowStyle); // 序号
                 createCellWithStyle(dataRow, colIndex++, data.get("teamCode"), currentRowStyle); // 队伍编号
                 createCellWithStyle(dataRow, colIndex++, data.get("teamName"), currentRowStyle); // 队伍名称
-                createCellWithStyle(dataRow, colIndex++, data.get("projectType"), currentRowStyle); // 项目类型
+                createCellWithStyle(dataRow, colIndex++, getDictLabel(projectTypes, data.get("projectType")), currentRowStyle); // 项目类型
                 createCellWithStyle(dataRow, colIndex++, data.get("projectName"), currentRowStyle); // 项目名称
                 createCellWithStyle(dataRow, colIndex++, data.get("teamPerformance"), currentRowStyle); // 团队成绩
                 createCellWithStyle(dataRow, colIndex++, data.get("scorePoint"), currentRowStyle); // 积分
@@ -2092,4 +2132,67 @@ public class GameScoreServiceImpl implements IGameScoreService {
             return null;
         }
     }
+
+    /**
+     * 导出单个项目的成绩详情
+     */
+    @Override
+    public void exportProjectScore(Long eventId, Long projectId, Integer topN, HttpServletResponse response) {
+        try {
+            // 1. 获取项目信息
+            GameEventProjectVo project = gameEventProjectService.queryById(projectId);
+            if (project == null) {
+                throw new RuntimeException("项目不存在");
+            }
+
+            // 2. 获取数据
+            List<Map<String, Object>> dataList = getProjectScoreDataAll(eventId, projectId,
+                    project.getClassification());
+
+            // 3. 如果指定了 topN,则按排名过滤或截取前 N 条
+            if (topN != null && topN > 0 && !dataList.isEmpty()) {
+                // 先按排名排序,确保 topN 准确
+                dataList.sort((a, b) -> {
+                    Integer rankA = a.get("scoreRank") != null ? (Integer) a.get("scoreRank") : Integer.MAX_VALUE;
+                    Integer rankB = b.get("scoreRank") != null ? (Integer) b.get("scoreRank") : Integer.MAX_VALUE;
+                    // 如果排名为0,放到最后
+                    if (rankA == 0)
+                        rankA = Integer.MAX_VALUE;
+                    if (rankB == 0)
+                        rankB = Integer.MAX_VALUE;
+                    return rankA.compareTo(rankB);
+                });
+
+                if (dataList.size() > topN) {
+                    dataList = dataList.subList(0, topN);
+                }
+            }
+
+            // 4. 创建Excel
+            try (Workbook workbook = new XSSFWorkbook()) {
+                CellStyle headerStyle = createHeaderStyle(workbook);
+                CellStyle dataStyleOdd = createDataStyle(workbook, IndexedColors.WHITE);
+                CellStyle dataStyleEven = createDataStyle(workbook, IndexedColors.GREY_25_PERCENT);
+
+                String sheetName = truncateSheetName(project.getProjectName());
+                Sheet sheet = workbook.createSheet(sheetName);
+
+                createProjectHeaderRow(sheet, project, headerStyle);
+                fillProjectDataRows(sheet, dataList, project, dataStyleOdd, dataStyleEven);
+                autoSizeColumns(sheet);
+
+                // 5. 设置文件名并响应
+                String classificationStr = ProjectClassification.SINGLE.getValue().equals(project.getClassification())
+                        ? "个人"
+                        : "团体";
+                String fileName = classificationStr + "_" + project.getProjectName() + "_成绩详情";
+                setResponseHeaders(response, fileName);
+
+                workbook.write(response.getOutputStream());
+            }
+        } catch (Exception e) {
+            log.error("导出项目成绩失败", e);
+            throw new RuntimeException("导出失败:" + e.getMessage());
+        }
+    }
 }