Bläddra i källkod

feat(game): 新增运动员编号生成工具并扩展赛事功能

- 新增 AthleteCodeUtils 工具类,实现基于 Redis 原子递增的唯一运动员编号生成
- 在 ExperienceMyInfoVo 中新增性别、年龄、单位字段,并添加序列化支持
- 为 ExperienceMenuVo 添加菜单跳转路径字段
- 新增 getEventInfoById 接口用于根据赛事ID查询赛事信息
- 新增 projectList 接口实现赛事项目分页列表查询
- 扩展 GameUserMapper 查询逻辑,关联运动员表获取更多信息
- 修改运动员报名逻辑,在插入新运动员时自动生成唯一编号
- 优化数据库查询,修复 join 条件和查询性能问题
zhou 4 dagar sedan
förälder
incheckning
e0263d3cb9

+ 20 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/app/ExperienceVersionController.java

@@ -12,6 +12,8 @@ import org.dromara.system.domain.vo.app.*;
 import org.dromara.system.domain.bo.ExperienceEnrollSubmitBo;
 import org.dromara.system.domain.bo.ExperienceUpdateInfoBo;
 import org.dromara.system.mapper.*;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.system.service.app.IUserEventService;
 import org.dromara.system.service.IGameScoreService;
 import org.dromara.system.mapper.app.GameUserMapper;
@@ -204,4 +206,22 @@ public class ExperienceVersionController {
         return R.ok("操作成功", limit);
     }
 
+    /**
+     * 16、根据赛事Id查询赛事信息接口
+     */
+    @GetMapping("/getEventInfoById")
+    public R<ExperienceGameEventVo> getEventInfoById(@RequestParam(value = "eventId") String eventId) {
+        if (StringUtils.isBlank(eventId)) {
+            return R.fail("赛事ID不能为空");
+        }
+        return R.ok(TenantHelper.ignore(() -> gameEventMapper.selectInfoWithConfigById(eventId)));
+    }
+
+    /**
+     * 17、赛事项目分页列表查询接口
+     */
+    @GetMapping("/projectList")
+    public TableDataInfo<ExperienceProjectVo> getProjectList(@RequestParam("eventId") Long eventId, PageQuery pageQuery) {
+        return TenantHelper.ignore(() -> userEventService.getExperienceProjectPageList(eventId, pageQuery));
+    }
 }

+ 5 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/app/ExperienceMenuVo.java

@@ -29,4 +29,9 @@ public class ExperienceMenuVo implements Serializable {
      * 菜单的图标背景颜色
      */
     private String color;
+
+    /**
+     * 菜单的跳转路径
+     */
+    private String jumpPath;
 }

+ 15 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/app/ExperienceMyInfoVo.java

@@ -1,6 +1,8 @@
 package org.dromara.system.domain.vo.app;
 
 import lombok.Data;
+
+import java.io.Serial;
 import java.io.Serializable;
 
 /**
@@ -10,6 +12,7 @@ import java.io.Serializable;
  */
 @Data
 public class ExperienceMyInfoVo implements Serializable {
+    @Serial
     private static final long serialVersionUID = 1L;
 
     /**
@@ -31,6 +34,18 @@ public class ExperienceMyInfoVo implements Serializable {
      * 手机号
      */
     private String phone;
+    /**
+     * 性别
+     */
+    private String gender;
+    /**
+     * 年龄
+     */
+    private Integer age;
+    /**
+     * 单位
+     */
+    private String unit;
 
     /**
      * 总积分

+ 13 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/mapper/GameEventMapper.java

@@ -78,4 +78,17 @@ public interface GameEventMapper extends BaseMapperPlus<GameEvent, GameEventVo>
             "order by e.start_time desc " +
             "</script>")
     List<ExperienceGameEventVo> selectInfoWithConfig(@Param("ew") Wrapper<GameEvent> queryWrapper);
+
+    /**
+     * 小程序体验端获取赛事列表,并关联查询配置项
+     */
+    @Select("<script>" +
+        "SELECT e.event_id, e.location, e.event_name, e.start_time, e.end_time, ec.config_value as hallImage, e.limit_application as projectNum " +
+        "from game_event e " +
+        "left join game_event_config ec on e.event_id = ec.event_id and ec.config_key = 'hall_image' " +
+        "where e.del_flag = '0' and e.status = '0' " +
+        "  and e.event_id = #{eventId} " +
+        "order by e.start_time desc " +
+        "</script>")
+    ExperienceGameEventVo selectInfoWithConfigById(String eventId);
 }

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

@@ -15,6 +15,7 @@ import org.dromara.system.domain.vo.app.ExEnrollLimitVo;
 import org.dromara.system.domain.vo.app.ExGameScheduleVo;
 import org.dromara.system.domain.vo.app.ExGameGroupDetailVo;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import org.dromara.system.domain.vo.app.ExperienceProjectVo;
 
 import java.math.BigDecimal;
 import java.text.SimpleDateFormat;
@@ -186,4 +187,13 @@ public interface GameEventProjectMapper extends BaseMapperPlus<GameEventProject,
         "from game_event_project " +
         "where project_id = #{projectId}")
     ExEnrollLimitVo selectLimit(Long projectId);
+
+    /**
+     * 体验端分页获取赛事下的项目列表(ID、名称、分类)
+     */
+    @Select("select project_id, project_name, classification " +
+            "from game_event_project " +
+            "where event_id = #{eventId} and del_flag = '0' " +
+            "order by project_id asc")
+    Page<ExperienceProjectVo> selectExperienceProjectPage(@Param("page") Page<ExperienceProjectVo> page, @Param("eventId") Long eventId);
 }

+ 5 - 1
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/mapper/app/GameUserMapper.java

@@ -61,11 +61,14 @@ public interface GameUserMapper extends BaseMapperPlus<GameUser, GameUserVo> {
             "  MAX(IFNULL(gs.nickname, gs.username)) AS nickName, " +
             "  MAX(gs.avatar) AS avatar, " +
             "  MAX(gs.phone) AS phone, " +
+            "  MAX(ga.gender) AS gender, " +
+            "  MAX(ga.age) AS age, " +
+            "  MAX(ga.unit) AS unit, " +
             "  IFNULL(SUM(gsc.score_point), 0) AS totalPoints, " +
             "  COUNT(DISTINCT ga.event_id) AS eventCount, " +
             "  IFNULL(SUM(CASE WHEN gsc.award IS NULL OR gsc.award = '' THEN 0 ELSE 1 END), 0) AS certificateCount " +
             "FROM game_user gs " +
-            "LEFT JOIN game_athlete ga ON ga.user_id = gs.user_id AND ga.del_flag = '0' " +
+            "LEFT JOIN game_athlete ga ON ga.phone = gs.phone AND ga.del_flag = '0' " +
             "LEFT JOIN game_score gsc ON ga.athlete_id = gsc.athlete_id AND gsc.del_flag = '0' " +
             "WHERE gs.user_id = #{userId} AND gs.del_flag = '0'")
     ExperienceMyInfoVo getExperienceMyInfo(String userId);
@@ -103,6 +106,7 @@ public interface GameUserMapper extends BaseMapperPlus<GameUser, GameUserVo> {
             "  gn.name, " +
             "  gn.pic, " +
             "  gn.color " +
+            "  gn.jump_path " +
             "FROM event_menu em " +
             "INNER JOIN common_navigator gn ON FIND_IN_SET( " +
             "  gn.nav_id, " +

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

@@ -3,10 +3,13 @@ package org.dromara.system.service.app;
 import org.dromara.system.domain.vo.app.ExperienceMyInfoVo;
 import org.dromara.system.domain.vo.app.ExperienceMyRecordVo;
 import org.dromara.system.domain.vo.app.ExperienceEnrollInfoVo;
+import org.dromara.system.domain.vo.app.ExperienceProjectVo;
 import org.dromara.system.domain.bo.ExperienceEnrollSubmitBo;
 import org.dromara.system.domain.bo.ExperienceUpdateInfoBo;
 import org.dromara.system.domain.vo.app.UserEventInfoVo;
 import org.dromara.system.domain.vo.app.UserLoginVo;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
 
 import java.util.List;
 
@@ -31,4 +34,7 @@ public interface IUserEventService {
 
     // 体验端修改“我的”个人信息
     Boolean updateExperienceMyInfo(ExperienceUpdateInfoBo updateInfoBo);
+
+    // 体验端获取赛事项目分页列表
+    TableDataInfo<ExperienceProjectVo> getExperienceProjectPageList(Long eventId, PageQuery pageQuery);
 }

+ 26 - 30
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/app/UserEventServiceImpl.java

@@ -24,8 +24,12 @@ import org.dromara.system.domain.bo.ExperienceUpdateInfoBo;
 import cn.hutool.json.JSONUtil;
 import com.baomidou.lock.annotation.Lock4j;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import org.dromara.system.utils.AthleteCodeUtils;
 import org.dromara.system.mapper.*;
 import org.dromara.system.mapper.app.GameUserMapper;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import org.dromara.system.service.app.IUserEventService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
@@ -365,27 +369,7 @@ public class UserEventServiceImpl implements IUserEventService {
     @Override
     public ExperienceEnrollInfoVo getExperienceEnrollInfo(String phone, Long eventId) {
         ExperienceEnrollInfoVo infoVo = new ExperienceEnrollInfoVo();
-        // 1. 获取赛事下的项目列表
-        List<GameEventProject> projects = gameEventProjectMapper.selectList(
-                Wrappers.lambdaQuery(GameEventProject.class)
-                        .select(GameEventProject::getProjectId, GameEventProject::getProjectName,
-                                GameEventProject::getClassification)
-                        .eq(GameEventProject::getEventId, eventId)
-                        .eq(GameEventProject::getDelFlag, "0")
-                        .orderByAsc(GameEventProject::getProjectId));
-        List<ExperienceProjectVo> projectVos = new ArrayList<>();
-        if (CollectionUtils.isNotEmpty(projects)) {
-            for (GameEventProject project : projects) {
-                ExperienceProjectVo pVo = new ExperienceProjectVo();
-                pVo.setProjectId(project.getProjectId());
-                pVo.setProjectName(project.getProjectName());
-                pVo.setClassification(project.getClassification());
-                projectVos.add(pVo);
-            }
-        }
-        infoVo.setProjects(projectVos);
-
-        // 2. 根据手机号和赛事ID查询是否已经有运动员报名信息
+        // 根据手机号和赛事ID查询是否已经有运动员报名信息
         GameAthlete athlete = gameAthleteMapper.selectOne(
                 Wrappers.lambdaQuery(GameAthlete.class)
                         .select(GameAthlete::getAthleteId, GameAthlete::getName, GameAthlete::getPhone,
@@ -432,18 +416,18 @@ public class UserEventServiceImpl implements IUserEventService {
         // 优先根据传入的 athleteId 查询
         if (submitBo.getAthleteId() != null) {
             athlete = gameAthleteMapper.selectOne(Wrappers.lambdaQuery(GameAthlete.class)
-                .select(GameAthlete::getAthleteId, GameAthlete::getName, GameAthlete::getPhone,
-                    GameAthlete::getIdCard, GameAthlete::getGender, GameAthlete::getTshirtSize,
-                    GameAthlete::getEmergencyContactName, GameAthlete::getEmergencyContactPhone)
+                    .select(GameAthlete::getAthleteId, GameAthlete::getName, GameAthlete::getPhone,
+                            GameAthlete::getIdCard, GameAthlete::getGender, GameAthlete::getTshirtSize,
+                            GameAthlete::getEmergencyContactName, GameAthlete::getEmergencyContactPhone)
                     .eq(GameAthlete::getAthleteId, submitBo.getAthleteId())
                     .eq(GameAthlete::getDelFlag, "0"));
         }
         // 如果没有传入 athleteId 或查不到,则根据手机号和赛事ID查询
         if (athlete == null) {
             athlete = gameAthleteMapper.selectOne(Wrappers.lambdaQuery(GameAthlete.class)
-                .select(GameAthlete::getAthleteId, GameAthlete::getName, GameAthlete::getPhone,
-                        GameAthlete::getIdCard, GameAthlete::getGender, GameAthlete::getTshirtSize,
-                        GameAthlete::getEmergencyContactName, GameAthlete::getEmergencyContactPhone)
+                    .select(GameAthlete::getAthleteId, GameAthlete::getName, GameAthlete::getPhone,
+                            GameAthlete::getIdCard, GameAthlete::getGender, GameAthlete::getTshirtSize,
+                            GameAthlete::getEmergencyContactName, GameAthlete::getEmergencyContactPhone)
                     .eq(GameAthlete::getPhone, submitBo.getPhone().trim())
                     .eq(GameAthlete::getEventId, submitBo.getEventId())
                     .eq(GameAthlete::getDelFlag, "0"));
@@ -497,7 +481,8 @@ public class UserEventServiceImpl implements IUserEventService {
                     .select(GameTeam::getTeamId)
                     .eq(GameTeam::getEventId, submitBo.getEventId())
                     .eq(GameTeam::getTeamName, submitBo.getTeamName().trim())
-                    .eq(GameTeam::getDelFlag, "0"));
+                    .eq(GameTeam::getDelFlag, "0")
+                .last("limit 1"));
 
             // 找不到匹配的队伍则新建
             if (team == null) {
@@ -530,8 +515,8 @@ public class UserEventServiceImpl implements IUserEventService {
 
         // 5. 校验该赛事每人的项目报名限制数
         GameEvent event = gameEventMapper.selectOne(Wrappers.lambdaQuery(GameEvent.class)
-            .select(GameEvent::getEventId, GameEvent::getLimitApplication)
-            .eq(GameEvent::getEventId, submitBo.getEventId()));
+                .select(GameEvent::getEventId, GameEvent::getLimitApplication)
+                .eq(GameEvent::getEventId, submitBo.getEventId()));
         if (event == null) {
             throw new ServiceException("该赛事不存在或已被删除");
         }
@@ -668,6 +653,8 @@ public class UserEventServiceImpl implements IUserEventService {
         athlete.setProjectValue(JSONUtil.toJsonStr(list));
         boolean success;
         if (isNewAthlete) {
+            // 在插入新运动员前,为其生成唯一的运动员编号
+            athlete.setAthleteCode(AthleteCodeUtils.generateAthleteCode(athlete.getEventId()));
             success = gameAthleteMapper.insert(athlete) > 0;
         } else {
             success = gameAthleteMapper.updateById(athlete) > 0;
@@ -720,4 +707,13 @@ public class UserEventServiceImpl implements IUserEventService {
         }
         return flag;
     }
+
+    @Override
+    public TableDataInfo<ExperienceProjectVo> getExperienceProjectPageList(Long eventId, PageQuery pageQuery) {
+        if (eventId == null) {
+            throw new ServiceException("赛事Id不能为空");
+        }
+        Page<ExperienceProjectVo> page = gameEventProjectMapper.selectExperienceProjectPage(pageQuery.build(), eventId);
+        return TableDataInfo.build(page);
+    }
 }

+ 79 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/utils/AthleteCodeUtils.java

@@ -0,0 +1,79 @@
+package org.dromara.system.utils;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.system.domain.GameAthlete;
+import org.dromara.system.mapper.GameAthleteMapper;
+
+/**
+ * 运动员编号生成工具类
+ *
+ * @author system
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class AthleteCodeUtils {
+
+    /**
+     * 在当前赛事已有的最大运动员编号基础上,通过 Redis 原子递增分配新编号,
+     * 并结合数据库防重校验,防止因员工后台手动修改、导入导致编号冲突。
+     *
+     * @param eventId 赛事ID
+     * @return 唯一的运动员编号
+     */
+    public static String generateAthleteCode(Long eventId) {
+        if (eventId == null) {
+            return null;
+        }
+        String redisKey = "game_event:athlete_code:global:" + eventId;
+        GameAthleteMapper gameAthleteMapper = SpringUtils.getBean(GameAthleteMapper.class);
+
+        // 1. 初始化 Redis 计数器(若不存在)
+        if (!RedisUtils.hasKey(redisKey)) {
+            // 查询该赛事下已有的最大运动员编号
+            GameAthlete maxAthlete = gameAthleteMapper.selectOne(
+                Wrappers.lambdaQuery(GameAthlete.class)
+                    .eq(GameAthlete::getEventId, eventId)
+                    .isNotNull(GameAthlete::getAthleteCode)
+                    .orderByDesc(GameAthlete::getAthleteCode)
+                    .select(GameAthlete::getAthleteCode)
+                    .last("limit 1")
+            );
+            long initVal = 0;
+            if (maxAthlete != null && StringUtils.isNotBlank(maxAthlete.getAthleteCode())) {
+                try {
+                    initVal = Long.parseLong(maxAthlete.getAthleteCode());
+                } catch (NumberFormatException e) {
+                    // 容错:若最大编号包含非数字字符,从 0 开始
+                }
+            }
+            RedisUtils.setAtomicValue(redisKey, initVal);
+        }
+
+        // 2. 循环递增获取,直至该编号在数据库中确实不存在(防止与人工编辑或导入的编号冲突)
+        String athleteCode;
+        int loopCount = 0; // 设置安全上限,避免极端错误配置下引起死循环
+        do {
+            long nextCode = RedisUtils.incrAtomicValue(redisKey);
+            athleteCode = String.valueOf(nextCode);
+            loopCount++;
+        } while (loopCount < 100 && checkExists(gameAthleteMapper, eventId, athleteCode));
+
+        return athleteCode;
+    }
+
+    /**
+     * 检查编号是否在当前赛事中已被占用
+     */
+    private static boolean checkExists(GameAthleteMapper gameAthleteMapper, Long eventId, String athleteCode) {
+        Long count = gameAthleteMapper.selectCount(
+            Wrappers.lambdaQuery(GameAthlete.class)
+                .eq(GameAthlete::getEventId, eventId)
+                .eq(GameAthlete::getAthleteCode, athleteCode)
+        );
+        return count > 0;
+    }
+}