ソースを参照

feat(app): 添加体验端个人中心功能

- 新增ExperienceMyInfoVo用于展示个人基本信息
- 新增ExperienceMyRecordVo用于展示个人参赛记录
- 在ExperienceVersionController中添加/myInfo和/myRecord接口
- 在GameUserMapper中实现个人中心相关查询SQL
- 在UserEventService中添加个人中心数据获取方法
- 实现成绩格式化功能,支持计时类成绩转换为时分秒格式
- 完善参赛车数和证书数统计逻辑
zhou 3 日 前
コミット
8af9ba8abb

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

@@ -10,6 +10,8 @@ import org.dromara.common.tenant.helper.TenantHelper;
 import org.dromara.system.domain.GameEvent;
 import org.dromara.system.domain.vo.app.ExperienceGameEventVo;
 import org.dromara.system.domain.vo.app.UserEventInfoVo;
+import org.dromara.system.domain.vo.app.ExperienceMyInfoVo;
+import org.dromara.system.domain.vo.app.ExperienceMyRecordVo;
 import org.dromara.system.domain.vo.app.UserLoginVo;
 import org.dromara.system.mapper.GameEventConfigMapper;
 import org.dromara.system.mapper.GameEventMapper;
@@ -60,13 +62,13 @@ public class ExperienceVersionController {
      */
     @GetMapping("/search")
     public R<List<ExperienceGameEventVo>> search(@RequestParam(value = "keyword", required = false) String keyword) {
-        if (StringUtils.isBlank(keyword)){
+        if (StringUtils.isBlank(keyword)) {
             return R.fail("关键词不能为空");
         }
         LambdaQueryWrapper<GameEvent> queryWrapper = Wrappers.lambdaQuery(GameEvent.class)
-            .and(wrapper -> wrapper.like(GameEvent::getEventName, keyword.trim())
-                    .or()
-                    .like(GameEvent::getLocation, keyword.trim()));
+                .and(wrapper -> wrapper.like(GameEvent::getEventName, keyword.trim())
+                        .or()
+                        .like(GameEvent::getLocation, keyword.trim()));
         return R.ok(TenantHelper.ignore(() -> gameEventMapper.selectInfoWithConfig(queryWrapper)));
     }
 
@@ -79,11 +81,33 @@ public class ExperienceVersionController {
     }
 
     /**
-     * 4、赛事详情页---赛事成绩排行榜查询接口
+     * 4、我的---个人信息
+     */
+    @GetMapping("/myInfo")
+    public R<ExperienceMyInfoVo> getMyInfo(@RequestParam("userId") String userId) {
+        if (StringUtils.isBlank(userId)) {
+            return R.fail("用户Id不能为空");
+        }
+        return R.ok(TenantHelper.ignore(() -> userEventService.getExperienceMyInfo(userId.trim())));
+    }
+
+    /**
+     * 5、我的---个人参赛记录
+     */
+    @GetMapping("/myRecord")
+    public R<List<ExperienceMyRecordVo>> getMyRecord(@RequestParam("phone") String phone) {
+        if (StringUtils.isBlank(phone)) {
+            return R.fail("手机号不能为空");
+        }
+        return R.ok(TenantHelper.ignore(() -> userEventService.getExperienceMyRecord(phone.trim())));
+    }
+
+    /**
+     * 6、赛事详情页---赛事成绩排行榜查询接口
      */
 
     /**
-     * 5、赛事菜单查询接口
+     * 7、赛事菜单查询接口
      */
 
 }

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

@@ -0,0 +1,49 @@
+package org.dromara.system.domain.vo.app;
+
+import lombok.Data;
+import java.io.Serializable;
+
+/**
+ * 体验端“我的”个人信息展示VO
+ *
+ * @author system
+ */
+@Data
+public class ExperienceMyInfoVo implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 用户ID
+     */
+    private Long userId;
+
+    /**
+     * 微信昵称
+     */
+    private String nickName;
+
+    /**
+     * 头像
+     */
+    private String avatar;
+
+    /**
+     * 手机号
+     */
+    private String phone;
+
+    /**
+     * 总积分
+     */
+    private Integer totalPoints;
+
+    /**
+     * 参与赛事数
+     */
+    private Integer eventCount;
+
+    /**
+     * 获得证书数
+     */
+    private Integer certificateCount;
+}

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

@@ -0,0 +1,87 @@
+package org.dromara.system.domain.vo.app;
+
+import lombok.Data;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 体验端“我的”参赛记录展示VO
+ *
+ * @author system
+ */
+@Data
+public class ExperienceMyRecordVo implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 赛事ID
+     */
+    private Long eventId;
+
+    /**
+     * 赛事名称
+     */
+    private String eventName;
+
+    /**
+     * 项目ID
+     */
+    private Long projectId;
+
+    /**
+     * 项目名称
+     */
+    private String projectName;
+
+    /**
+     * 归类(0个人项目/1团体项目)
+     */
+    private String classification;
+
+    /**
+     * 开始时间
+     */
+    private Date startTime;
+
+    /**
+     * 结束时间
+     */
+    private Date endTime;
+
+    /**
+     * 状态(已完赛/进行中)
+     */
+    private String status;
+
+    /**
+     * 成绩
+     */
+    private String score;
+
+    /**
+     * 临时原始成绩
+     */
+    @JsonIgnore
+    private BigDecimal rawScore;
+
+    /**
+     * 成绩类型
+     */
+    @JsonIgnore
+    private String scoreRule;
+
+    /**
+     * 获得积分
+     */
+    private Integer points;
+
+    /**
+     * 电子证书 url 地址
+     */
+    private String certificateUrl;
+}

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

@@ -1,15 +1,20 @@
 package org.dromara.system.mapper.app;
 
+import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Select;
 import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
 import org.dromara.system.domain.app.GameUser;
+import org.dromara.system.domain.vo.app.ExperienceMyInfoVo;
+import org.dromara.system.domain.vo.app.ExperienceMyRecordVo;
 import org.dromara.system.domain.vo.app.GameUserVo;
+import java.util.List;
 
 /**
  * 赛事用户Mapper接口
  *
  * @author zlt
  */
+@Mapper
 public interface GameUserMapper extends BaseMapperPlus<GameUser, GameUserVo> {
 
     /**
@@ -48,4 +53,46 @@ public interface GameUserMapper extends BaseMapperPlus<GameUser, GameUserVo> {
      */
     @Select("select * from game_user where openid = #{openid} and del_flag = '0'")
     GameUser selectByOpenid(String openid);
+
+    // 体验端获取“我的”个人信息
+    @Select("SELECT " +
+            "  MAX(gs.user_id) AS userId, " +
+            "  MAX(IFNULL(gs.nickname, gs.username)) AS nickName, " +
+            "  MAX(gs.avatar) AS avatar, " +
+            "  MAX(gs.phone) AS phone, " +
+            "  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_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);
+
+    // 体验端获取“我的”参赛记录
+    @Select("SELECT " +
+            "  ga.event_id AS eventId, " +
+            "  ge.event_name AS eventName, " +
+            "  gep.project_id AS projectId, " +
+            "  gep.project_name AS projectName, " +
+            "  gep.classification AS classification, " +
+            "  gep.start_time AS startTime, " +
+            "  gep.end_time AS endTime, " +
+            "  (CASE WHEN gs.score_id IS NULL THEN '进行中' ELSE '已完赛' END) AS status, " +
+            "  gep.score_rule AS scoreRule, " +
+            "  (CASE WHEN gep.classification = '0' THEN gs.individual_performance " +
+            "        WHEN gep.classification = '1' THEN gs.team_performance " +
+            "        ELSE COALESCE(gs.individual_performance, gs.team_performance) END) AS rawScore, " +
+            "  IFNULL(gs.score_point, 0) AS points, " +
+            "  gs.award AS certificateUrl " +
+            "FROM game_athlete ga " +
+            "LEFT JOIN game_event ge ON ga.event_id = ge.event_id AND ge.del_flag = '0' " +
+            "INNER JOIN game_event_project gep ON FIND_IN_SET( " +
+            "  gep.project_id, " +
+            "  REPLACE(REPLACE(REPLACE(REPLACE(IFNULL(ga.project_value, ''), '[', ''), ']', ''), '\"', ''), ' ', '') " +
+            ") > 0 AND gep.del_flag = '0' " +
+            "LEFT JOIN game_score gs ON ga.athlete_id = gs.athlete_id AND gep.project_id = gs.project_id AND gs.del_flag = '0' " +
+            "WHERE ga.phone = #{phone} AND ga.del_flag = '0' " +
+            "ORDER BY gep.start_time DESC")
+    List<ExperienceMyRecordVo> getExperienceMyRecord(String phone);
 }

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

@@ -1,12 +1,22 @@
 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.UserEventInfoVo;
 import org.dromara.system.domain.vo.app.UserLoginVo;
 
+import java.util.List;
+
 public interface IUserEventService {
     // 用户登录
     UserEventInfoVo login(UserLoginVo loginVo);
 
     // 获取用户赛事信息
     UserEventInfoVo getUserEventInfo(Long userId, String phone);
+
+    // 体验端获取“我的”个人信息
+    ExperienceMyInfoVo getExperienceMyInfo(String userId);
+
+    // 体验端获取“我的”参赛记录
+    List<ExperienceMyRecordVo> getExperienceMyRecord(String phone);
 }

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

@@ -3,6 +3,7 @@ package org.dromara.system.service.impl.app;
 import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.dromara.common.core.utils.MapstructUtils;
+import org.dromara.common.core.utils.StringUtils;
 import org.dromara.system.domain.GameAthlete;
 import org.dromara.system.domain.GameEvent;
 import org.dromara.system.domain.GameEventProject;
@@ -14,6 +15,12 @@ import org.dromara.system.domain.vo.GameEventProjectVo;
 import org.dromara.system.domain.vo.GameEventVo;
 import org.dromara.system.domain.vo.app.UserEventInfoVo;
 import org.dromara.system.domain.vo.app.UserLoginVo;
+import org.dromara.system.domain.vo.app.ExperienceMyInfoVo;
+import org.dromara.system.domain.vo.app.ExperienceMyRecordVo;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+
+import java.math.BigDecimal;
+import java.util.stream.Collectors;
 import org.dromara.system.mapper.*;
 import org.dromara.system.mapper.app.GameUserMapper;
 import org.dromara.system.service.app.IUserEventService;
@@ -92,20 +99,20 @@ public class UserEventServiceImpl implements IUserEventService {
             }
 
             String url = String.format(
-                "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
-                appId, secret, code
-            );
+                    "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
+                    appId, secret, code);
 
             // 先获取字符串响应,避免Content-Type不匹配问题
             String responseBody = webClient.get()
-                .uri(url)
-                .retrieve()
-                .onStatus(status -> status.is4xxClientError() || status.is5xxServerError(),
-                    response -> response.bodyToMono(String.class)
-                        .flatMap(body -> Mono.error(new RuntimeException("HTTP错误: " + response.statusCode() + ", 响应: " + body))))
-                .bodyToMono(String.class)
-                .timeout(Duration.ofSeconds(30)) // 30秒超时
-                .block(); // 同步等待结果
+                    .uri(url)
+                    .retrieve()
+                    .onStatus(status -> status.is4xxClientError() || status.is5xxServerError(),
+                            response -> response.bodyToMono(String.class)
+                                    .flatMap(body -> Mono.error(new RuntimeException(
+                                            "HTTP错误: " + response.statusCode() + ", 响应: " + body))))
+                    .bodyToMono(String.class)
+                    .timeout(Duration.ofSeconds(30)) // 30秒超时
+                    .block(); // 同步等待结果
 
             if (responseBody == null || responseBody.trim().isEmpty()) {
                 throw new RuntimeException("微信接口返回空结果");
@@ -243,8 +250,8 @@ public class UserEventServiceImpl implements IUserEventService {
     }
 
     private UserEventInfoVo assembleUserEventInfo(GameUser user, GameAthlete athlete,
-                                                  GameEvent event, List<GameEventProject> projects,
-                                                  List<GameScore> scores) {
+            GameEvent event, List<GameEventProject> projects,
+            List<GameScore> scores) {
         UserEventInfoVo result = new UserEventInfoVo();
         result.setUserId(user.getUserId());
         result.setUsername(user.getUsername());
@@ -271,11 +278,11 @@ public class UserEventServiceImpl implements IUserEventService {
                 // 将成绩信息添加到项目信息中,可以通过扩展字段或备注字段存储
                 // 这里我们使用备注字段来存储额外的成绩信息
                 String scoreInfo = String.format("个人成绩:%s, 团队成绩:%s, 积分:%d, 排名:%d, 奖项:%s",
-                    score.getIndividualPerformance() != null ? score.getIndividualPerformance().toString() : "无",
-                    score.getTeamPerformance() != null ? score.getTeamPerformance().toString() : "无",
-                    score.getScorePoint() != null ? score.getScorePoint() : 0,
-                    score.getScoreRank() != null ? score.getScoreRank() : 0,
-                    calculateAward(score.getScoreRank()));
+                        score.getIndividualPerformance() != null ? score.getIndividualPerformance().toString() : "无",
+                        score.getTeamPerformance() != null ? score.getTeamPerformance().toString() : "无",
+                        score.getScorePoint() != null ? score.getScorePoint() : 0,
+                        score.getScoreRank() != null ? score.getScoreRank() : 0,
+                        calculateAward(score.getScoreRank()));
                 projectInfo.setRemark(scoreInfo);
             }
 
@@ -283,7 +290,8 @@ public class UserEventServiceImpl implements IUserEventService {
         }
 
         // 按项目开始时间排序(处理null值)
-        projectList.sort(Comparator.comparing(GameEventProjectVo::getStartTime, Comparator.nullsLast(Comparator.naturalOrder())));
+        projectList.sort(Comparator.comparing(GameEventProjectVo::getStartTime,
+                Comparator.nullsLast(Comparator.naturalOrder())));
         result.setProjectList(projectList);
 
         return result;
@@ -291,16 +299,67 @@ public class UserEventServiceImpl implements IUserEventService {
 
     private GameScore findScoreByProjectId(List<GameScore> scores, Long projectId) {
         return scores.stream()
-            .filter(score -> score.getProjectId().equals(projectId))
-            .findFirst()
-            .orElse(null);
+                .filter(score -> score.getProjectId().equals(projectId))
+                .findFirst()
+                .orElse(null);
     }
 
     private String calculateAward(Integer rank) {
-        if (rank == null) return "无";
-        if (rank == 1) return "金牌";
-        if (rank == 2) return "银牌";
-        if (rank == 3) return "铜牌";
+        if (rank == null)
+            return "无";
+        if (rank == 1)
+            return "金牌";
+        if (rank == 2)
+            return "银牌";
+        if (rank == 3)
+            return "铜牌";
         return "无";
     }
+
+    @Override
+    public ExperienceMyInfoVo getExperienceMyInfo(String userId) {
+        return gameUserMapper.getExperienceMyInfo(userId);
+    }
+
+    @Override
+    public List<ExperienceMyRecordVo> getExperienceMyRecord(String phone) {
+        if (StringUtils.isBlank(phone)) {
+            return new ArrayList<>();
+        }
+        List<ExperienceMyRecordVo> records = gameUserMapper.getExperienceMyRecord(phone.trim());
+        if (CollectionUtils.isNotEmpty(records)) {
+            for (ExperienceMyRecordVo vo : records) {
+                if (vo.getRawScore() != null) {
+                    vo.setScore(formatScore(vo.getRawScore(), vo.getScoreRule()));
+                }
+            }
+        }
+        return records;
+    }
+
+    /**
+     * 格式化成绩
+     * 计时类(scoreRule = "1")转换为 "HH:mm:ss.SSS" 格式,其它类型保留原始数字字符串
+     */
+    private String formatScore(BigDecimal rawScore, String scoreRule) {
+        if (rawScore == null) {
+            return "";
+        }
+        // "1" 代表计时类
+        if ("1".equals(scoreRule)) {
+            long totalSeconds = rawScore.longValue();
+            BigDecimal fraction = rawScore.subtract(BigDecimal.valueOf(totalSeconds));
+            long millis = fraction.multiply(BigDecimal.valueOf(1000))
+                    .setScale(0, java.math.RoundingMode.HALF_UP)
+                    .longValue();
+            long hours = totalSeconds / 3600;
+            long minutes = (totalSeconds % 3600) / 60;
+            long seconds = totalSeconds % 60;
+
+            return String.format("%02d:%02d:%02d.%03d", hours, minutes, seconds, millis);
+        } else {
+            // 其它类型,去除尾随零后转为普通字符串
+            return rawScore.stripTrailingZeros().toPlainString();
+        }
+    }
 }