Bladeren bron

feat(app): 为移动端应用增加赛事和项目管理功能

- 在GameEventProject实体类中完善项目类型的详细注释说明
- 为IToClientService接口添加获取赛事列表、删除赛事和获取项目列表的方法定义
- 在ToClientController控制器中实现赛事列表查询、赛事删除和项目列表查询的REST API端点
- 在ToClientServiceImpl服务实现类中添加异步删除赛事数据的功能,支持删除关联的组别、配置、成绩和项目数据
- 实现赛事列表查询功能,按创建时间倒序返回赛事基本信息
- 实现项目列表查询功能,根据赛事ID返回对应的项目信息
- 使用@Async注解实现异步删除操作,提高大容量数据删除的性能
- 添加事务管理和异常处理机制,确保数据删除操作的一致性和可靠性
zhou 2 weken geleden
bovenliggende
commit
f2c7bc6dea
17 gewijzigde bestanden met toevoegingen van 747 en 70 verwijderingen
  1. 21 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/app_client/ToClientController.java
  2. 6 2
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/GameAthlete.java
  3. 23 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/bo/ClientProjectSaveBo.java
  4. 19 13
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/bo/GameAthleteBo.java
  5. 29 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/bo/ScoreSubmitBo.java
  6. 23 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/bo/ScoreSubmitDetailBo.java
  7. 40 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/bo/ScoreSubmitItemBo.java
  8. 16 12
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/GameAthleteVo.java
  9. 33 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/ScoreSheetDetailVo.java
  10. 40 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/ScoreSheetItemVo.java
  11. 47 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/ScoreSheetVo.java
  12. 10 8
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/mapper/GameEventProjectMapper.java
  13. 23 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/mapper/app/ToClientMapper.java
  14. 12 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/app/IToClientService.java
  15. 5 3
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/GameScoreServiceImpl.java
  16. 333 32
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/app/ToClientServiceImpl.java
  17. 67 0
      ruoyi-modules/ruoyi-game-event/src/main/resources/mapper/system/app/ToClientMapper.xml

+ 21 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/app_client/ToClientController.java

@@ -8,6 +8,8 @@ import org.dromara.common.log.enums.BusinessType;
 import org.dromara.system.domain.GameEvent;
 import org.dromara.system.domain.GameEventProject;
 import org.dromara.system.domain.bo.ClientProjectSaveBo;
+import org.dromara.system.domain.bo.ScoreSubmitBo;
+import org.dromara.system.domain.vo.ScoreSheetVo;
 import org.dromara.system.service.app.IToClientService;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
@@ -61,4 +63,23 @@ public class ToClientController {
     public R<List<GameEventProject>> projectList(@RequestParam Long eventId) {
         return R.ok(toClientService.getProjectList(eventId));
     }
+
+    /**
+     * 成绩记录单展示 (接口6 展示)
+     */
+    @GetMapping("/scoreSheet")
+    public R<ScoreSheetVo> scoreSheet(@RequestParam Long eventId, @RequestParam Long projectId) {
+        return R.ok(toClientService.getScoreSheet(eventId, projectId));
+    }
+
+    /**
+     * 成绩记录单提交 (接口6 提交)
+     */
+    @Log(title = "app客户端提交成绩单", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PostMapping("/scoreSubmit")
+    public R<Void> scoreSubmit(@Validated @RequestBody ScoreSubmitBo bo) {
+        toClientService.submitScoreSheet(bo);
+        return R.ok();
+    }
 }

+ 6 - 2
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/GameAthlete.java

@@ -24,7 +24,7 @@ public class GameAthlete extends TenantEntity {
     /**
      * 主键
      */
-    @TableId(value = "athlete_id",type = IdType.AUTO)
+    @TableId(value = "athlete_id", type = IdType.AUTO)
     private Long athleteId;
 
     /**
@@ -102,6 +102,11 @@ public class GameAthlete extends TenantEntity {
      */
     private String projectValue;
 
+    /**
+     * 参赛序号/道次(app端录入成绩时需要存储的字段)
+     */
+    private Long trackIndex;
+
     /**
      * 状态(0正常 1停用)
      */
@@ -118,5 +123,4 @@ public class GameAthlete extends TenantEntity {
      */
     private String remark;
 
-
 }

+ 23 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/bo/ClientProjectSaveBo.java

@@ -56,6 +56,29 @@ public class ClientProjectSaveBo {
      * 1-计时类;2-距离类;3-单次计数类;4-多次计数类;5-排名类;6-远度距离类;7-高度距离类
      */
     private String scoreRule;
+    /**
+     * 计时格式 0---00:00:00.000, 1---00:00.00
+     */
+    // @ExcelProperty(value = "计时格式")
+    private String timingFormat;
+
+    /**
+     * 距离模式(0:单轮最高, 1:双轮最高)
+     */
+    // @ExcelProperty(value = "距离模式")
+    private String distanceMode;
+
+    /**
+     * 计数单位
+     */
+    // @ExcelProperty(value = "计数单位")
+    private String countUnit;
+
+    /**
+     * 成绩数量
+     */
+    // @ExcelProperty(value = "成绩数量")
+    private Integer scoreCount;
 
     /**
      * 性别---字典sys_group_sex

+ 19 - 13
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/bo/GameAthleteBo.java

@@ -30,7 +30,7 @@ public class GameAthleteBo extends BaseEntity {
     /**
      * 用户ID
      */
-//    @NotNull(message = "用户ID不能为空", groups = { AddGroup.class, EditGroup.class })
+    // @NotNull(message = "用户ID不能为空", groups = { AddGroup.class, EditGroup.class })
     private Long userId;
 
     /**
@@ -46,7 +46,7 @@ public class GameAthleteBo extends BaseEntity {
     /**
      * 队伍ID
      */
-//    @NotNull(message = "队伍ID不能为空", groups = { AddGroup.class, EditGroup.class })
+    // @NotNull(message = "队伍ID不能为空", groups = { AddGroup.class, EditGroup.class })
     private Long teamId;
 
     /**
@@ -63,7 +63,7 @@ public class GameAthleteBo extends BaseEntity {
     /**
      * 姓名
      */
-//    @NotBlank(message = "姓名不能为空", groups = { AddGroup.class, EditGroup.class })
+    // @NotBlank(message = "姓名不能为空", groups = { AddGroup.class, EditGroup.class })
     private String name;
 
     /**
@@ -75,7 +75,7 @@ public class GameAthleteBo extends BaseEntity {
     /**
      * 年龄
      */
-//    @NotNull(message = "年龄不能为空", groups = { AddGroup.class, EditGroup.class })
+    // @NotNull(message = "年龄不能为空", groups = { AddGroup.class, EditGroup.class })
     private Long age;
 
     /**
@@ -86,51 +86,58 @@ public class GameAthleteBo extends BaseEntity {
     /**
      * 证件号
      */
-//    @NotBlank(message = "证件号不能为空", groups = { AddGroup.class, EditGroup.class })
+    // @NotBlank(message = "证件号不能为空", groups = { AddGroup.class, EditGroup.class })
     private String idCard;
 
     /**
      * 芯片号
      */
-//    @NotBlank(message = "芯片号不能为空", groups = { AddGroup.class, EditGroup.class })
+    // @NotBlank(message = "芯片号不能为空", groups = { AddGroup.class, EditGroup.class })
     private String chipCode;
 
     /**
      * 手机号
      */
-//    @NotBlank(message = "手机号不能为空", groups = { AddGroup.class, EditGroup.class })
+    // @NotBlank(message = "手机号不能为空", groups = { AddGroup.class, EditGroup.class })
     private String phone;
 
     /**
      * 居住地址
      */
-//    @NotBlank(message = "居住地址不能为空", groups = { AddGroup.class, EditGroup.class })
+    // @NotBlank(message = "居住地址不能为空", groups = { AddGroup.class, EditGroup.class })
     private String location;
 
     /**
      * T恤尺码
      */
-//    @NotBlank(message = "T恤尺码不能为空", groups = { AddGroup.class, EditGroup.class })
+    // @NotBlank(message = "T恤尺码不能为空", groups = { AddGroup.class, EditGroup.class })
     private String tshirtSize;
 
     /**
      * 组别
      */
-//    @NotBlank(message = "组别不能为空", groups = { AddGroup.class, EditGroup.class })
+    // @NotBlank(message = "组别不能为空", groups = { AddGroup.class, EditGroup.class })
     private String groupType;
 
     /**
      * 参与项目列表
      */
-//    @NotBlank(message = "参与项目列表不能为空", groups = { AddGroup.class, EditGroup.class })
+    // @NotBlank(message = "参与项目列表不能为空", groups = { AddGroup.class, EditGroup.class
+    // })
     private Long projectId;
     private String projectValue;
     private List<Long> projectList;
 
+    /**
+     * 参赛序号/道次(app端录入成绩时需要存储的字段)
+     */
+    private Long trackIndex;
+
     /**
      * 状态(0正常 1停用)
      */
-//    @NotBlank(message = "状态(0正常 1停用)不能为空", groups = { AddGroup.class, EditGroup.class })
+    // @NotBlank(message = "状态(0正常 1停用)不能为空", groups = { AddGroup.class,
+    // EditGroup.class })
     private String status;
 
     /**
@@ -138,5 +145,4 @@ public class GameAthleteBo extends BaseEntity {
      */
     private String remark;
 
-
 }

+ 29 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/bo/ScoreSubmitBo.java

@@ -0,0 +1,29 @@
+package org.dromara.system.domain.bo;
+
+import lombok.Data;
+import jakarta.validation.constraints.NotNull;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 成绩记录单提交业务对象
+ */
+@Data
+public class ScoreSubmitBo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /** 赛事ID */
+    @NotNull(message = "赛事ID不能为空")
+    private Long eventId;
+
+    /** 项目ID */
+    @NotNull(message = "项目ID不能为空")
+    private Long projectId;
+
+    /** 提交项列表 */
+    private List<ScoreSubmitItemBo> items;
+}

+ 23 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/bo/ScoreSubmitDetailBo.java

@@ -0,0 +1,23 @@
+package org.dromara.system.domain.bo;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 成绩记录单提交明细业务对象
+ */
+@Data
+public class ScoreSubmitDetailBo implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /** 明细ID */
+    private Long detailId;
+    /** 尝试序号 */
+    private Integer attemptIndex;
+    /** 成绩值 */
+    private BigDecimal performanceValue;
+}

+ 40 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/bo/ScoreSubmitItemBo.java

@@ -0,0 +1,40 @@
+package org.dromara.system.domain.bo;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 成绩记录单提交项业务对象
+ */
+@Data
+public class ScoreSubmitItemBo implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /** 运动员ID */
+    private Long athleteId;
+    /** 队伍ID */
+    private Long teamId;
+    /** 队员号码 */
+    private String athleteCode;
+    /** 队员姓名 */
+    private String name;
+    /** 队伍名称 */
+    private String teamName;
+    /** 参赛序号 / 道次 */
+    private Long trackIndex;
+    /** 成绩ID (APP传回已有ID以避免重复创建) */
+    private Long scoreId;
+    /** 成绩 (支持复杂格式字符串) */
+    private String score;
+    /** 失误次数A */
+    private Integer faultA;
+    /** 失误次数B */
+    private Integer faultB;
+
+    /** 成绩明细列表 (APP传回已有明细ID以避免冗余) */
+    private List<ScoreSubmitDetailBo> details;
+}

+ 16 - 12
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/GameAthleteVo.java

@@ -12,7 +12,6 @@ import java.io.Serial;
 import java.io.Serializable;
 import java.util.List;
 
-
 /**
  * 参赛队员视图对象 game_athlete
  *
@@ -30,7 +29,7 @@ public class GameAthleteVo implements Serializable {
     /**
      * 主键
      */
-//     @ExcelProperty(value = "Id")
+    // @ExcelProperty(value = "Id")
     private Long athleteId;
 
     /**
@@ -54,7 +53,7 @@ public class GameAthleteVo implements Serializable {
     /**
      * 队伍ID
      */
-//    @ExcelProperty(value = "队伍ID")
+    // @ExcelProperty(value = "队伍ID")
     private Long teamId;
 
     /**
@@ -91,19 +90,19 @@ public class GameAthleteVo implements Serializable {
     /**
      * 单位
      */
-//    @ExcelProperty(value = "单位")
+    // @ExcelProperty(value = "单位")
     private String unit;
 
     /**
      * 证件号
      */
-//    @ExcelProperty(value = "证件号")
+    // @ExcelProperty(value = "证件号")
     private String idCard;
 
     /**
      * 芯片号
      */
-//    @ExcelProperty(value = "芯片号")
+    // @ExcelProperty(value = "芯片号")
     private String chipCode;
 
     /**
@@ -115,25 +114,25 @@ public class GameAthleteVo implements Serializable {
     /**
      * 居住地址
      */
-//    @ExcelProperty(value = "居住地址")
+    // @ExcelProperty(value = "居住地址")
     private String location;
 
     /**
      * T恤尺码
      */
-//    @ExcelProperty(value = "T恤尺码")
+    // @ExcelProperty(value = "T恤尺码")
     private String tshirtSize;
 
     /**
      * 组别
      */
-//    @ExcelProperty(value = "组别")
+    // @ExcelProperty(value = "组别")
     private String groupType;
 
     /**
      * 参与项目列表
      */
-//    @ExcelProperty(value = "参与项目列表")
+    // @ExcelProperty(value = "参与项目列表")
     private String projectValue;
     private List<Long> projectList;
     /**
@@ -148,10 +147,16 @@ public class GameAthleteVo implements Serializable {
     @ExcelProperty(value = "队伍")
     private String teamName;
 
+    /**
+     * 参赛序号/道次(app端录入成绩时需要存储的字段)
+     */
+    // @ExcelProperty(value = "参赛序号/道次")
+    private Long trackIndex;
+
     /**
      * 状态(0正常 1停用)
      */
-//    @ExcelProperty(value = "状态", converter = ExcelDictConvert.class)
+    // @ExcelProperty(value = "状态", converter = ExcelDictConvert.class)
     @ExcelDictFormat(dictType = "game_event_status")
     private String status;
 
@@ -161,5 +166,4 @@ public class GameAthleteVo implements Serializable {
     @ExcelProperty(value = "备注")
     private String remark;
 
-
 }

+ 33 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/ScoreSheetDetailVo.java

@@ -0,0 +1,33 @@
+package org.dromara.system.domain.vo;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 成绩记录明细视图对象
+ */
+@Data
+public class ScoreSheetDetailVo implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /** 明细ID */
+    private Long detailId;
+    /** 成绩ID */
+    private Long scoreId;
+    /** 第attemptIndex轮的成绩 */
+    private Integer attemptIndex;
+    /** 成绩值 */
+    private String performanceValue;
+    /** 失误A */
+    private Integer faultA;
+    /** 失误B */
+    private Integer faultB;
+    /** 运动员ID (逻辑关联使用) */
+    private Long athleteId;
+    /** 队伍ID (逻辑关联使用) */
+    private Long teamId;
+}

+ 40 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/ScoreSheetItemVo.java

@@ -0,0 +1,40 @@
+package org.dromara.system.domain.vo;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 成绩记录单项视图对象
+ */
+@Data
+public class ScoreSheetItemVo implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /** 运动员ID */
+    private Long athleteId;
+    /** 队伍ID */
+    private Long teamId;
+    /** 队员号码 */
+    private String athleteCode;
+    /** 队员姓名 */
+    private String name;
+    /** 队伍名称 */
+    private String teamName;
+    /** 参赛序号 / 道次 */
+    private Long trackIndex;
+    /** 成绩ID */
+    private Long scoreId;
+    /** 成绩 */
+    private String score;
+    /** 失误次数A */
+    private Integer faultA;
+    /** 失误次数B */
+    private Integer faultB;
+
+    /** 成绩明细列表 */
+    private List<ScoreSheetDetailVo> details;
+}

+ 47 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/ScoreSheetVo.java

@@ -0,0 +1,47 @@
+package org.dromara.system.domain.vo;
+
+import lombok.Data;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 成绩记录单展示视图对象
+ */
+@Data
+public class ScoreSheetVo implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 赛事名称 */
+    private String eventName;
+    /** 计算规则 */
+    private String scoreRule;
+    /** 项目名称 */
+    private String projectName;
+    /** 项目类型 */
+    private String projectType;
+    /** 项目分类 (0个人 1团体) */
+    private String classification;
+    /** 参赛性别 */
+    private String gender;
+    /** 参赛组别 */
+    private String rgName;
+    /** 比赛阶段 */
+    private String gameStage;
+    /** 比赛轮次 */
+    private String gameRound;
+    /** 完赛人数/对数 */
+    private Integer finishedParticipants;
+    /** 报名人数/对数 */
+    private Integer totalParticipants;
+    /** 比赛时间 */
+    private String startTime;
+    /** 裁判员 */
+    private String refereeName;
+    /** 计时格式 */
+    private String timingFormat;
+    /** 赛事提示 */
+    private String eventTip;
+
+    /** 队员列表 */
+    private List<ScoreSheetItemVo> list;
+}

+ 10 - 8
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/mapper/GameEventProjectMapper.java

@@ -29,10 +29,11 @@ public interface GameEventProjectMapper extends BaseMapperPlus<GameEventProject,
      * @return 分页的赛事信息
      */
     @DataPermission({
-        @DataColumn(key = "deptName", value = "create_dept"),
-        @DataColumn(key = "userName", value = "create_by")
+            @DataColumn(key = "deptName", value = "create_dept"),
+            @DataColumn(key = "userName", value = "create_by")
     })
-    default Page<GameEventProjectVo> selectPageEventProjectList(Page<GameEventProject> page, Wrapper<GameEventProject> queryWrapper) {
+    default Page<GameEventProjectVo> selectPageEventProjectList(Page<GameEventProject> page,
+            Wrapper<GameEventProject> queryWrapper) {
         return this.selectVoPage(page, queryWrapper);
     }
 
@@ -43,8 +44,8 @@ public interface GameEventProjectMapper extends BaseMapperPlus<GameEventProject,
      * @return 项目列表
      */
     @DataPermission({
-        @DataColumn(key = "deptName", value = "create_dept"),
-        @DataColumn(key = "userName", value = "create_by")
+            @DataColumn(key = "deptName", value = "create_dept"),
+            @DataColumn(key = "userName", value = "create_by")
     })
     default List<GameEventProjectVo> selectEventProjectList(Wrapper<GameEventProject> queryWrapper) {
         return this.selectVoList(queryWrapper);
@@ -54,8 +55,9 @@ public interface GameEventProjectMapper extends BaseMapperPlus<GameEventProject,
      * 分页查询项目列表并包含实时统计(XML 实现)
      */
     @DataPermission({
-        @DataColumn(key = "deptName", value = "create_dept"),
-        @DataColumn(key = "userName", value = "create_by")
+            @DataColumn(key = "deptName", value = "create_dept"),
+            @DataColumn(key = "userName", value = "create_by")
     })
-    Page<GameEventProjectVo> selectPageWithStats(@Param("page") Page<GameEventProjectVo> page, @Param("query") GameEventProjectBo bo);
+    Page<GameEventProjectVo> selectPageWithStats(@Param("page") Page<GameEventProjectVo> page,
+            @Param("query") GameEventProjectBo bo);
 }

+ 23 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/mapper/app/ToClientMapper.java

@@ -0,0 +1,23 @@
+package org.dromara.system.mapper.app;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.dromara.system.domain.vo.ScoreSheetItemVo;
+import org.dromara.system.domain.vo.ScoreSheetVo;
+
+import java.util.List;
+
+@Mapper
+public interface ToClientMapper {
+
+
+    /**
+     * 获取成绩记录单元数据 (接口6)
+     */
+    ScoreSheetVo selectScoreSheetMetadata(@Param("eventId") Long eventId, @Param("projectId") Long projectId);
+
+    /**
+     * 获取成绩记录单项列表
+     */
+    List<ScoreSheetItemVo> selectScoreSheetItems(@Param("eventId") Long eventId, @Param("projectId") Long projectId);
+}

+ 12 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/app/IToClientService.java

@@ -3,6 +3,8 @@ package org.dromara.system.service.app;
 import org.dromara.system.domain.GameEvent;
 import org.dromara.system.domain.GameEventProject;
 import org.dromara.system.domain.bo.ClientProjectSaveBo;
+import org.dromara.system.domain.bo.ScoreSubmitBo;
+import org.dromara.system.domain.vo.ScoreSheetVo;
 
 import java.util.List;
 
@@ -29,4 +31,14 @@ public interface IToClientService {
      * 获取项目列表 (接口5)
      */
     List<GameEventProject> getProjectList(Long eventId);
+
+    /**
+     * 获取成绩记录单 (接口6 展示)
+     */
+    ScoreSheetVo getScoreSheet(Long eventId, Long projectId);
+
+    /**
+     * 提交成绩记录单 (接口6 提交)
+     */
+    void submitScoreSheet(ScoreSubmitBo bo);
 }

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

@@ -54,6 +54,8 @@ import org.apache.poi.ss.usermodel.*;
 import org.apache.poi.xssf.usermodel.XSSFWorkbook;
 import org.springframework.transaction.annotation.Transactional;
 import org.dromara.common.excel.utils.ExcelUtil;
+import org.springframework.web.multipart.MultipartFile;
+
 import java.io.IOException;
 
 /**
@@ -748,7 +750,7 @@ public class GameScoreServiceImpl implements IGameScoreService {
                         ProjectClassification.SINGLE.getValue()) != 0) {
                     currentRank = i + 1;
                 }
-                
+
                 Integer points = (currentRank <= pointConfig.size()) ? pointConfig.get(currentRank - 1) : 0;
                 Integer finalRank = currentRank;
 
@@ -2340,7 +2342,7 @@ public class GameScoreServiceImpl implements IGameScoreService {
     @Override
     @Transactional(rollbackFor = Exception.class)
     public String importScore(Long eventId, Long projectId, String classification, Boolean updateSupport,
-            org.springframework.web.multipart.MultipartFile file) {
+            MultipartFile file) {
         GameEventProjectVo project = gameEventProjectService.queryById(projectId);
         int scoreCount = project.getScoreCount() != null ? project.getScoreCount() : 1;
 
@@ -2599,7 +2601,7 @@ public class GameScoreServiceImpl implements IGameScoreService {
                 } else {
                     throw new IllegalArgumentException("格式非法");
                 }
-                return BigDecimal.valueOf(totalSeconds).setScale(3, java.math.RoundingMode.HALF_UP);
+                return BigDecimal.valueOf(totalSeconds).setScale(3, RoundingMode.HALF_UP);
             } catch (Exception e) {
                 failureMsg.append("<br/>第").append(rowIndex + 1).append("行第").append(colIndex + 1)
                         .append("列时间格式不正确:[").append(scoreStr).append("]");

+ 333 - 32
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/app/ToClientServiceImpl.java

@@ -7,20 +7,25 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.dromara.common.core.utils.StringUtils;
 import org.dromara.system.domain.*;
-import org.dromara.system.domain.bo.ClientProjectSaveBo;
-import org.dromara.system.domain.bo.GameRankGroupBo;
+import org.dromara.system.domain.bo.*;
+import org.dromara.system.domain.constant.ProjectClassification;
+import org.dromara.system.domain.vo.ScoreSheetDetailVo;
+import org.dromara.system.domain.vo.ScoreSheetItemVo;
+import org.dromara.system.domain.vo.ScoreSheetVo;
+import org.dromara.system.mapper.GameAthleteMapper;
 import org.dromara.system.mapper.GameEventMapper;
 import org.dromara.system.mapper.GameEventProjectMapper;
+import org.dromara.system.mapper.app.ToClientMapper;
+import org.dromara.system.service.IGameScoreService;
 import org.dromara.system.service.IGameRankGroupService;
 import org.dromara.system.service.app.IToClientService;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
-import java.util.HashMap;
-import java.util.List;
-import java.util.ArrayList;
-import java.util.Map;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.*;
 import java.util.stream.Collectors;
 
 @Slf4j
@@ -31,6 +36,9 @@ public class ToClientServiceImpl implements IToClientService {
     private final GameEventProjectMapper projectMapper;
     private final GameEventMapper gameEventMapper;
     private final IGameRankGroupService gameRankGroupService;
+    private final ToClientMapper baseMapper;
+    private final IGameScoreService gameScoreService;
+    private final GameAthleteMapper gameAthleteMapper;
 
     /**
      * 客户端同步赛事及项目信息 (接口4)
@@ -106,6 +114,10 @@ public class ToClientServiceImpl implements IToClientService {
                 project.setProjectType(bo.getProjectType());
                 project.setClassification(bo.getClassification());
                 project.setScoreRule(bo.getScoreRule());
+                project.setTimingFormat(bo.getTimingFormat());
+                project.setDistanceMode(bo.getDistanceMode());
+                project.setCountUnit(bo.getCountUnit());
+                project.setScoreCount(bo.getScoreCount());
                 project.setGender(bo.getGender());
                 project.setRgName(rgName);
                 project.setRgId(rgId);
@@ -140,28 +152,25 @@ public class ToClientServiceImpl implements IToClientService {
     public void asyncRemoveEventData(Long eventId) {
         log.info("开始异步删除赛事数据,eventId: {}", eventId);
         long startTime = System.currentTimeMillis();
-        try{
+        try {
             // 删除关联的组别
             Db.remove(Wrappers.lambdaQuery(GameRankGroup.class)
-                .eq(GameRankGroup::getEventId, eventId)
-            );
+                    .eq(GameRankGroup::getEventId, eventId));
             // 删除关联的配置信息
             Db.remove(Wrappers.lambdaQuery(GameEventConfig.class)
-                .eq(GameEventConfig::getEventId, eventId)
-            );
+                    .eq(GameEventConfig::getEventId, eventId));
             // 删除关联的成绩信息
             Db.remove(Wrappers.lambdaQuery(GameScore.class)
-                .eq(GameScore::getEventId, eventId)
-            );
+                    .eq(GameScore::getEventId, eventId));
             // 删除关联的项目
             int projectCount = projectMapper.delete(Wrappers.lambdaQuery(GameEventProject.class)
-                .eq(GameEventProject::getEventId, eventId));
+                    .eq(GameEventProject::getEventId, eventId));
             log.info("删除项目完成,eventId: {}, 删除数量: {}", eventId, projectCount);
             // 删除赛事
             gameEventMapper.deleteById(eventId);
             long endTime = System.currentTimeMillis();
             log.info("赛事数据删除完成,eventId: {}, 耗时: {}ms", eventId, (endTime - startTime));
-        }catch (Exception e){
+        } catch (Exception e) {
             log.error("异步删除赛事数据失败,eventId: {}", eventId, e);
             throw new RuntimeException("删除赛事数据失败: " + e.getMessage());
         }
@@ -170,17 +179,269 @@ public class ToClientServiceImpl implements IToClientService {
     @Override
     public List<GameEvent> getEventList() {
         return gameEventMapper.selectList(Wrappers.lambdaQuery(GameEvent.class)
-            .orderByDesc(GameEvent::getCreateTime)
-            .select(GameEvent::getEventId, GameEvent::getEventCode, GameEvent::getEventName, GameEvent::getCreateTime)
-        );
+                .orderByDesc(GameEvent::getCreateTime)
+                .select(GameEvent::getEventId, GameEvent::getEventCode, GameEvent::getEventName,
+                        GameEvent::getCreateTime));
     }
 
     @Override
     public List<GameEventProject> getProjectList(Long eventId) {
         return projectMapper.selectList(Wrappers.lambdaQuery(GameEventProject.class)
-            .eq(GameEventProject::getEventId, eventId)
-            .select(GameEventProject::getProjectId, GameEventProject::getProjectType, GameEventProject::getClassification, GameEventProject::getProjectName)
-        );
+                .eq(GameEventProject::getEventId, eventId)
+                .select(GameEventProject::getProjectId, GameEventProject::getProjectType,
+                        GameEventProject::getClassification, GameEventProject::getProjectName));
+    }
+
+    @Override
+    public ScoreSheetVo getScoreSheet(Long eventId, Long projectId) {
+        // 1. 查询元数据和统计信息
+        ScoreSheetVo vo = baseMapper.selectScoreSheetMetadata(eventId, projectId);
+        if (vo == null) {
+            throw new RuntimeException("该赛事下不存在此项目");
+        }
+        // 2. 查询该项目下所有参赛人员和队伍信息 (包括已录入成绩和未录入的)
+        List<ScoreSheetItemVo> items = baseMapper.selectScoreSheetItems(eventId, projectId);
+
+        // 3. 批量查询该项目下所有成绩明细 (适配个人/团体关联差异)
+        List<GameScoreDetail> allDetails = Db.list(Wrappers.lambdaQuery(GameScoreDetail.class)
+                .eq(GameScoreDetail::getProjectId, projectId)
+                .eq(GameScoreDetail::getDelFlag, "0"));
+
+        if (!allDetails.isEmpty()) {
+            Map<Long, List<ScoreSheetDetailVo>> detailMap;
+            if (ProjectClassification.TEAM.getValue().equals(vo.getClassification())) {
+                // 团体项目:按 teamId 分组
+                detailMap = allDetails.stream()
+                        .filter(d -> d.getTeamId() != null)
+                        .map(this::convertToDetailVo)
+                        .collect(Collectors.groupingBy(ScoreSheetDetailVo::getTeamId));
+
+                items.forEach(item -> item.setDetails(detailMap.get(item.getTeamId())));
+            } else {
+                // 个人项目:优先按 athleteId 分组 (如果存储时没存 athleteId 则按 scoreId)
+                detailMap = allDetails.stream()
+                        .filter(d -> d.getAthleteId() != null)
+                        .map(this::convertToDetailVo)
+                        .collect(Collectors.groupingBy(ScoreSheetDetailVo::getAthleteId));
+
+                items.forEach(item -> item.setDetails(detailMap.get(item.getAthleteId())));
+
+                // 兜底:处理部分仅关联 scoreId 的旧数据
+                items.stream().filter(i -> i.getDetails() == null && i.getScoreId() != null).forEach(item -> {
+                    List<ScoreSheetDetailVo> scoreDetails = allDetails.stream()
+                            .filter(d -> Objects.equals(d.getScoreId(), item.getScoreId()))
+                            .map(this::convertToDetailVo)
+                            .toList();
+                    item.setDetails(scoreDetails);
+                });
+            }
+        }
+
+        // 4. 处理计时格式转换
+        if (vo.getTimingFormat() != null && StringUtils.isNotBlank(vo.getTimingFormat())) {
+            items.forEach(item -> {
+                // 格式化主成绩
+                if (StringUtils.isNotBlank(item.getScore())) {
+                    item.setScore(convertDecimalToTimeScore(new BigDecimal(item.getScore()), vo.getTimingFormat()));
+                }
+                // 格式化明细成绩
+                if (item.getDetails() != null) {
+                    item.getDetails().forEach(detail -> {
+                        if (StringUtils.isNotBlank(detail.getPerformanceValue())) {
+                            detail.setPerformanceValue(convertDecimalToTimeScore(
+                                    new BigDecimal(detail.getPerformanceValue()),
+                                    vo.getTimingFormat()));
+                        }
+                    });
+                }
+            });
+        }
+
+        vo.setList(items);
+        return vo;
+    }
+
+    private ScoreSheetDetailVo convertToDetailVo(GameScoreDetail d) {
+        ScoreSheetDetailVo dvo = new ScoreSheetDetailVo();
+        dvo.setDetailId(d.getDetailId());
+        dvo.setScoreId(d.getScoreId());
+        dvo.setAttemptIndex(d.getAttemptIndex());
+        dvo.setPerformanceValue(d.getPerformanceValue() != null ? d.getPerformanceValue().toString() : null);
+        dvo.setFaultA(d.getFaultA());
+        dvo.setFaultB(d.getFaultB());
+        // 传递关联 ID 供分组使用
+        dvo.setAthleteId(d.getAthleteId());
+        dvo.setTeamId(d.getTeamId());
+        return dvo;
+    }
+
+    /**
+     * 将小数格式的成绩转换成时间格式显示
+     *
+     * @param decimalScore 以秒为单位的小数值
+     * @param format 格式 (HH:mm:ss.SSS 或 mm:ss.SSS)
+     * @return 时间格式字符串
+     */
+    private String convertDecimalToTimeScore(BigDecimal decimalScore, String format) {
+        if (decimalScore == null || decimalScore.compareTo(BigDecimal.ZERO) < 0) {
+            return "0".equals(format) ? "00:00:00.000" : "00:00.000";
+        }
+
+        try {
+            long totalMs = decimalScore.multiply(new BigDecimal(1000)).setScale(0, RoundingMode.HALF_UP).longValue();
+            long hours = totalMs / 3600000;
+            long minutes = (totalMs % 3600000) / 60000;
+            long seconds = (totalMs % 60000) / 1000;
+            long milliseconds = totalMs % 1000;
+
+            if ("1".equals(format)) {
+                // 如果是 mm:ss.SSS,把小时累加到分钟
+                long totalMinutes = hours * 60 + minutes;
+                return String.format("%02d:%02d.%03d", totalMinutes, seconds, milliseconds);
+            } else {
+                // 默认 HH:mm:ss.SSS
+                return String.format("%02d:%02d:%02d.%03d", hours, minutes, seconds, milliseconds);
+            }
+        } catch (Exception e) {
+            log.warn("转换成绩格式失败: score={}, format={}", decimalScore, format, e);
+            return decimalScore.toString();
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void submitScoreSheet(ScoreSubmitBo bo) {
+        Long projectId = bo.getProjectId();
+        Long eventId = bo.getEventId();
+        GameEventProject project = projectMapper.selectOne(Wrappers.lambdaQuery(GameEventProject.class)
+                .eq(GameEventProject::getEventId, eventId)
+                .eq(GameEventProject::getProjectId, projectId));
+        if (project == null) {
+            throw new RuntimeException("该赛事下不存在此项目");
+        }
+
+        List<ScoreSubmitItemBo> items = bo.getItems();
+        if (CollectionUtils.isEmpty(items)) {
+            return;
+        }
+
+        // 1. 批量处理队伍 (缺失则新增)
+        Map<String, Long> teamIdMap = new HashMap<>();
+        Set<String> missingTeamNames = items.stream()
+                .filter(item -> item.getTeamId() == null && StringUtils.isNotBlank(item.getTeamName()))
+                .map(ScoreSubmitItemBo::getTeamName)
+                .collect(Collectors.toSet());
+
+        if (!missingTeamNames.isEmpty()) {
+            List<GameTeam> existingTeams = Db.list(Wrappers.lambdaQuery(GameTeam.class)
+                    .eq(GameTeam::getEventId, eventId)
+                    .in(GameTeam::getTeamName, missingTeamNames)
+                    .select(GameTeam::getTeamId, GameTeam::getTeamName));
+            existingTeams.forEach(t -> teamIdMap.put(t.getTeamName(), t.getTeamId()));
+
+            List<GameTeam> newTeams = missingTeamNames.stream()
+                    .filter(name -> !teamIdMap.containsKey(name))
+                    .map(name -> {
+                        GameTeam t = new GameTeam();
+                        t.setEventId(eventId);
+                        t.setTeamName(name);
+                        return t;
+                    }).toList();
+            if (!newTeams.isEmpty()) {
+                Db.saveBatch(newTeams);
+                newTeams.forEach(t -> teamIdMap.put(t.getTeamName(), t.getTeamId()));
+            }
+        }
+
+        // 2. 批量处理运动员 (缺失则新增,其余准备批量更新)
+        Map<String, Long> athleteIdMap = new HashMap<>();
+        List<ScoreSubmitItemBo> missingAthleteItems = items.stream()
+                .filter(item -> item.getAthleteId() == null && StringUtils.isNotBlank(item.getAthleteCode()))
+                .toList();
+
+        if (!missingAthleteItems.isEmpty()) {
+            // 预查是否存在,防止重复
+            Set<String> codes = missingAthleteItems.stream().map(ScoreSubmitItemBo::getAthleteCode).collect(Collectors.toSet());
+            List<GameAthlete> existAthletes = gameAthleteMapper.selectList(Wrappers.lambdaQuery(GameAthlete.class)
+                    .eq(GameAthlete::getEventId, eventId)
+                    .in(GameAthlete::getAthleteCode, codes)
+                    .select(GameAthlete::getAthleteId, GameAthlete::getAthleteCode));
+            existAthletes.forEach(a -> athleteIdMap.put(a.getAthleteCode(), a.getAthleteId()));
+
+            List<GameAthlete> newAthletes = missingAthleteItems.stream()
+                    .filter(item -> !athleteIdMap.containsKey(item.getAthleteCode()))
+                    .map(item -> {
+                        GameAthlete a = new GameAthlete();
+                        a.setEventId(eventId);
+                        a.setAthleteCode(item.getAthleteCode());
+                        a.setName(item.getName());
+                        a.setTeamId(item.getTeamId() != null ? item.getTeamId() : teamIdMap.get(item.getTeamName()));
+                        a.setProjectValue("[\"" + projectId + "\"]");
+                        a.setTrackIndex(item.getTrackIndex());
+                        return a;
+                    }).collect(Collectors.toMap(GameAthlete::getAthleteCode, a -> a, (a1, a2) -> a1))
+                    .values().stream().toList();
+
+            if (!newAthletes.isEmpty()) {
+                Db.saveBatch(newAthletes);
+                newAthletes.forEach(a -> athleteIdMap.put(a.getAthleteCode(), a.getAthleteId()));
+            }
+        }
+
+        // 3. 直接批量更新所有上传项的运动员信息 (不再预查对比,直接全量同步)
+        List<GameAthlete> updateBatch = items.stream()
+                .map(item -> {
+                    Long aid = item.getAthleteId() != null ? item.getAthleteId() : athleteIdMap.get(item.getAthleteCode());
+                    if (aid == null) return null;
+                    GameAthlete a = new GameAthlete();
+                    a.setAthleteId(aid);
+                    a.setTrackIndex(item.getTrackIndex());
+                    // 如果有姓名或其它字段变更也可以在此同步
+                    return a;
+                }).filter(Objects::nonNull).toList();
+
+        if (!updateBatch.isEmpty()) {
+            Db.updateBatchById(updateBatch);
+        }
+
+        // 4. 循环处理成绩录入 (统一使用 details 明细列表)
+        for (ScoreSubmitItemBo item : items) {
+            Long athleteId = item.getAthleteId() != null ? item.getAthleteId() : athleteIdMap.get(item.getAthleteCode());
+            if (athleteId == null) continue;
+
+            Long teamId = item.getTeamId() != null ? item.getTeamId() : teamIdMap.get(item.getTeamName());
+
+            GameScoreBo scoreBo = new GameScoreBo();
+            scoreBo.setScoreId(item.getScoreId());
+            scoreBo.setEventId(eventId);
+            scoreBo.setProjectId(projectId);
+            scoreBo.setAthleteId(athleteId);
+            scoreBo.setTeamId(teamId);
+            scoreBo.setFaultA(item.getFaultA());
+            scoreBo.setFaultB(item.getFaultB());
+
+            // 统一模式:优先处理明细列表
+            if (CollectionUtils.isNotEmpty(item.getDetails())) {
+                List<GameScoreDetailBo> details = item.getDetails().stream().map(d -> {
+                    GameScoreDetailBo dbo = new GameScoreDetailBo();
+                    dbo.setDetailId(d.getDetailId());
+                    dbo.setAttemptIndex(d.getAttemptIndex());
+                    dbo.setPerformanceValue(d.getPerformanceValue());
+                    return dbo;
+                }).toList();
+                scoreBo.setDetails(details);
+            } else if (StringUtils.isNotBlank(item.getScore())) {
+                // 如果没有明细,说明只有一个成绩,解析赋值给对应字段
+                BigDecimal performance = parsePerformanceValue(item.getScore());
+                if (ProjectClassification.TEAM.getValue().equals(project.getClassification())) {
+                    scoreBo.setTeamPerformance(performance);
+                } else {
+                    scoreBo.setIndividualPerformance(performance);
+                }
+            }
+
+            gameScoreService.updateScoreAndRecalculate(scoreBo);
+        }
     }
 
     /**
@@ -188,25 +449,28 @@ public class ToClientServiceImpl implements IToClientService {
      */
     private void batchSaveConfigs(Long eventId, ClientProjectSaveBo bo) {
         Map<String, String> configValues = new HashMap<>();
-        if (StringUtils.isNotBlank(bo.getMachineCode())) configValues.put("machine_code", bo.getMachineCode());
-        if (StringUtils.isNotBlank(bo.getEventTip())) configValues.put("event_tip", bo.getEventTip());
-        if (StringUtils.isNotBlank(bo.getUploadPath())) configValues.put("upload_path", bo.getUploadPath());
+        if (StringUtils.isNotBlank(bo.getMachineCode()))
+            configValues.put("machine_code", bo.getMachineCode());
+        if (StringUtils.isNotBlank(bo.getEventTip()))
+            configValues.put("event_tip", bo.getEventTip());
+        if (StringUtils.isNotBlank(bo.getUploadPath()))
+            configValues.put("upload_path", bo.getUploadPath());
 
-        if (configValues.isEmpty()) return;
+        if (configValues.isEmpty())
+            return;
 
         Map<String, String> configDescs = Map.of(
-            "machine_code", "机器编号",
-            "event_tip", "赛事提示",
-            "upload_path", "上传成绩路径"
-        );
+                "machine_code", "机器编号",
+                "event_tip", "赛事提示",
+                "upload_path", "上传成绩路径");
 
         // 批量查询现有配置
         List<GameEventConfig> existingConfigs = Db.list(Wrappers.lambdaQuery(GameEventConfig.class)
-            .eq(GameEventConfig::getEventId, eventId)
-            .in(GameEventConfig::getConfigKey, configValues.keySet()));
+                .eq(GameEventConfig::getEventId, eventId)
+                .in(GameEventConfig::getConfigKey, configValues.keySet()));
 
         Map<String, GameEventConfig> configMap = existingConfigs.stream()
-            .collect(Collectors.toMap(GameEventConfig::getConfigKey, c -> c));
+                .collect(Collectors.toMap(GameEventConfig::getConfigKey, c -> c));
 
         List<GameEventConfig> toSave = new ArrayList<>();
         for (Map.Entry<String, String> entry : configValues.entrySet()) {
@@ -227,4 +491,41 @@ public class ToClientServiceImpl implements IToClientService {
         }
         Db.saveOrUpdateBatch(toSave);
     }
+
+    /**
+     * 解析成绩字符串为 BigDecimal
+     * 支持普通数字和时间格式 (HH:mm:ss.SSS 或 mm:ss.SSS)
+     */
+    private BigDecimal parsePerformanceValue(String scoreStr) {
+        if (StringUtils.isBlank(scoreStr)) return null;
+        scoreStr = scoreStr.trim();
+        // 兼容时间格式
+        if (scoreStr.contains(":")) {
+            try {
+                String[] parts = scoreStr.split(":");
+                double totalSeconds = 0;
+                if (parts.length == 3) {
+                    // HH:mm:ss.SSS
+                    totalSeconds = Double.parseDouble(parts[0]) * 3600
+                            + Double.parseDouble(parts[1]) * 60
+                            + Double.parseDouble(parts[2]);
+                } else if (parts.length == 2) {
+                    // mm:ss.SSS
+                    totalSeconds = Double.parseDouble(parts[0]) * 60
+                            + Double.parseDouble(parts[1]);
+                }
+                return BigDecimal.valueOf(totalSeconds).setScale(3, RoundingMode.HALF_UP);
+            } catch (Exception e) {
+                log.warn("成绩时间格式解析失败: {}", scoreStr);
+                return null;
+            }
+        }
+        try {
+            return new BigDecimal(scoreStr);
+        } catch (Exception e) {
+            log.warn("成绩数字解析失败: {}", scoreStr);
+            return null;
+        }
+    }
 }
+

+ 67 - 0
ruoyi-modules/ruoyi-game-event/src/main/resources/mapper/system/app/ToClientMapper.xml

@@ -0,0 +1,67 @@
+<?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="org.dromara.system.mapper.app.ToClientMapper">
+    <select id="selectScoreSheetMetadata" resultType="org.dromara.system.domain.vo.ScoreSheetVo">
+        SELECT
+            e.event_name as eventName,
+            p.score_rule as scoreRule,
+            p.project_name as projectName,
+            p.project_type as projectType,
+            p.classification as classification,
+            p.gender as gender,
+            p.rg_name as rgName,
+            p.game_stage as gameStage,
+            p.game_round as gameRound,
+            DATE_FORMAT(p.start_time, '%Y-%m-%d %H:%i:%s') as startTime,
+            (SELECT GROUP_CONCAT(name SEPARATOR ',') FROM game_referee WHERE del_flag = '0'
+                    AND event_id = p.event_id
+                    AND (JSON_CONTAINS(p.referee_group, JSON_ARRAY(referee_id))
+                             OR JSON_CONTAINS(p.referee_group, JSON_ARRAY(CAST(referee_id AS CHAR))))
+            ) as refereeName,
+            p.timing_format as timingFormat,
+            (SELECT config_value FROM game_event_config WHERE event_id = p.event_id
+                                                          AND config_key = 'event_tip' AND del_flag = '0' LIMIT 1) as eventTip,
+            IFNULL((SELECT COUNT(*) FROM game_athlete a WHERE a.event_id = p.event_id AND a.del_flag = '0'
+                AND (JSON_CONTAINS(a.project_value, JSON_ARRAY(p.project_id))
+                OR JSON_CONTAINS(a.project_value, JSON_ARRAY(CAST(p.project_id AS CHAR)))))
+                , 0) as totalParticipants,
+            IFNULL((SELECT COUNT(*)
+                    FROM game_score s
+                    JOIN game_athlete a ON s.athlete_id = a.athlete_id AND a.del_flag = '0'
+                    WHERE s.project_id = p.project_id AND s.del_flag = '0'
+                    AND ((p.classification = '0' AND s.individual_performance > 0) OR (p.classification = '1' AND s.team_performance > 0))
+                    )
+            , 0) as finishedParticipants
+        FROM game_event_project p
+        JOIN game_event e ON p.event_id = e.event_id
+        WHERE p.event_id = #{eventId} AND p.project_id = #{projectId} AND p.del_flag = '0'
+    </select>
+
+    <select id="selectScoreSheetItems" resultType="org.dromara.system.domain.vo.ScoreSheetItemVo">
+        SELECT
+            a.athlete_id as athleteId,
+            a.team_id as teamId,
+            a.athlete_code as athleteCode,
+            a.name as name,
+            t.team_name as teamName,
+            a.track_index as trackIndex,
+            s.score_id as scoreId,
+            CASE
+                WHEN p.classification = '0' THEN s.individual_performance
+                ELSE s.team_performance
+            END as score,
+            s.fault_a as faultA,
+            s.fault_b as faultB
+        FROM game_athlete a
+        LEFT JOIN game_team t ON a.team_id = t.team_id AND t.del_flag = '0'
+        CROSS JOIN game_event_project p ON p.project_id = #{projectId}
+        LEFT JOIN game_score s ON a.athlete_id = s.athlete_id AND s.project_id = #{projectId} AND s.del_flag = '0'
+        WHERE a.event_id = #{eventId}
+          AND a.del_flag = '0'
+          AND (JSON_CONTAINS(a.project_value, JSON_ARRAY(#{projectId}))
+               OR JSON_CONTAINS(a.project_value, JSON_ARRAY(CAST(#{projectId} AS CHAR))))
+        ORDER BY a.track_index ASC, a.athlete_code ASC
+    </select>
+</mapper>