Browse Source

feat(game-event): 实现微信小程序用户登录和赛事信息获取功能- 新增微信小程序配置项- 实现用户登录接口,支持微信小程序登录
- 实现获取用户赛事信息接口
- 新增 GameUser 相关实体和 mapper
- 优化运动员和赛事信息查询逻辑

zhou 16 hours ago
parent
commit
8817c085e1
21 changed files with 787 additions and 7 deletions
  1. 6 0
      ruoyi-admin/src/main/resources/application.yml
  2. 6 0
      ruoyi-modules/ruoyi-game-event/pom.xml
  3. 50 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/config/WebClientConfig.java
  4. 1 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/GameEventController.java
  5. 1 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/GameTeamController.java
  6. 53 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/app/UserEventController.java
  7. 86 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/app/GameUser.java
  8. 15 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/app/WxLoginResult.java
  9. 3 3
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/bo/GameEventBo.java
  10. 3 3
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/GameTeamVo.java
  11. 96 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/app/GameUserVo.java
  12. 34 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/app/UserEventInfoVo.java
  13. 52 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/app/UserLoginVo.java
  14. 3 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/mapper/GameAthleteMapper.java
  15. 12 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/mapper/GameScoreMapper.java
  16. 41 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/mapper/app/GameUserMapper.java
  17. 12 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/app/IUserEventService.java
  18. 3 1
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/AdviceServiceImpl.java
  19. 293 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/app/UserEventServiceImpl.java
  20. 10 0
      ruoyi-modules/ruoyi-game-event/src/main/resources/mapper/system/GameScoreMapper.xml
  21. 7 0
      ruoyi-modules/ruoyi-game-event/src/main/resources/mapper/system/app/GameUserMapper.xml

+ 6 - 0
ruoyi-admin/src/main/resources/application.yml

@@ -282,3 +282,9 @@ warm-flow:
     - 255,205,23
     ## 已办理
     - 157,255,0
+
+# 微信小程序配置
+wechat:
+  miniapp:
+    appid: wx017241c84de43b7a
+    secret: 91ee2725605ba0ae73829cf4538395ac

+ 6 - 0
ruoyi-modules/ruoyi-game-event/pom.xml

@@ -79,6 +79,12 @@
             <artifactId>ruoyi-common-web</artifactId>
         </dependency>
 
+        <!-- WebFlux依赖,用于WebClient -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-webflux</artifactId>
+        </dependency>
+
         <dependency>
             <groupId>org.dromara</groupId>
             <artifactId>ruoyi-common-idempotent</artifactId>

+ 50 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/config/WebClientConfig.java

@@ -0,0 +1,50 @@
+package org.dromara.system.config;
+
+import io.netty.channel.ChannelOption;
+import io.netty.handler.timeout.ReadTimeoutHandler;
+import io.netty.handler.timeout.WriteTimeoutHandler;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.client.reactive.ReactorClientHttpConnector;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.netty.http.client.HttpClient;
+import reactor.netty.resources.ConnectionProvider;
+
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * WebClient配置类
+ * 用于微信API调用,替代已弃用的RestTemplate
+ *
+ * @author zlt
+ */
+@Configuration
+public class WebClientConfig {
+
+    @Bean
+    public WebClient webClient() {
+        // 连接池配置
+        ConnectionProvider connectionProvider = ConnectionProvider.builder("custom")
+            .maxConnections(100)
+            .maxIdleTime(Duration.ofSeconds(20))
+            .maxLifeTime(Duration.ofSeconds(60))
+            .pendingAcquireTimeout(Duration.ofSeconds(60))
+            .evictInBackground(Duration.ofSeconds(120))
+            .build();
+
+        // HTTP客户端配置
+        HttpClient httpClient = HttpClient.create(connectionProvider)
+            .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000) // 连接超时10秒
+            .responseTimeout(Duration.ofSeconds(30)) // 响应超时30秒
+            .doOnConnected(conn -> conn
+                .addHandlerLast(new ReadTimeoutHandler(30, TimeUnit.SECONDS)) // 读取超时30秒
+                .addHandlerLast(new WriteTimeoutHandler(30, TimeUnit.SECONDS)) // 写入超时30秒
+            );
+
+        return WebClient.builder()
+            .clientConnector(new ReactorClientHttpConnector(httpClient))
+            .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024)) // 2MB
+            .build();
+    }
+} 

+ 1 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/GameEventController.java

@@ -131,6 +131,7 @@ public class GameEventController extends BaseController {
      */
     @SaCheckPermission("system:gameEvent:edit")
     @Log(title = "赛事默认状态修改", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
     @PutMapping("/changeEventDefault")
     public R<Void> changeEventDefault(@RequestBody GameEventBo bo) {
         // 如果修改的对象 原本是默认并且准备取消则禁止

+ 1 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/GameTeamController.java

@@ -156,6 +156,7 @@ public class GameTeamController extends BaseController {
      */
     @SaCheckPermission("system:gameTeam:edit")
     @Log(title = "参赛队伍", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
     @PutMapping("/updateAthletes")
     public R<Void> updateTeamAthletes(@RequestBody UpdateTeamAthletesRequest request) {
         return toAjax(gameTeamService.updateTeamAthletes(request.getTeamId(), request.getAthleteIds()));

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

@@ -0,0 +1,53 @@
+package org.dromara.system.controller.app;
+
+
+import cn.dev33.satoken.annotation.SaIgnore;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.system.domain.vo.app.UserEventInfoVo;
+import org.dromara.system.domain.vo.app.UserLoginVo;
+import org.dromara.system.service.app.IUserEventService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * app-我的赛事
+ */
+@SaIgnore
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/app/user")
+public class UserEventController {
+    @Autowired
+    private IUserEventService userEventService;
+
+    @Log(title = "我的赛事-微信小程序登录", businessType = BusinessType.OTHER)
+    @PostMapping("/login")
+    public R<UserEventInfoVo> login(@RequestBody UserLoginVo loginVo) {
+        try {
+            // 验证必要参数
+            if (loginVo.getCode() == null || loginVo.getCode().trim().isEmpty()) {
+                return R.fail("微信登录凭证不能为空");
+            }
+
+            UserEventInfoVo result = userEventService.login(loginVo);
+            return R.ok(result);
+        } catch (Exception e) {
+            return R.fail("登录失败:" + e.getMessage());
+        }
+    }
+
+    @GetMapping("/eventInfo/{userId}")
+    public R<UserEventInfoVo> getUserEventInfo(@PathVariable Long userId) {
+        try {
+            UserEventInfoVo result = userEventService.getUserEventInfo(userId);
+            return R.ok(result);
+        } catch (Exception e) {
+            return R.fail("获取用户赛事信息失败:" + e.getMessage());
+        }
+    }
+}

+ 86 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/app/GameUser.java

@@ -0,0 +1,86 @@
+package org.dromara.system.domain.app;
+
+import org.dromara.common.tenant.core.TenantEntity;
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serial;
+
+/**
+ * 赛事用户对象 game_user
+ *
+ * @author Lion Li
+ * @date 2025-08-27
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("game_user")
+public class GameUser extends TenantEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 用户ID
+     */
+    @TableId(value = "user_id")
+    private Long userId;
+
+    /**
+     * 用户名/账号
+     */
+    private String username;
+
+    /**
+     * 密码
+     */
+    private String password;
+
+    /**
+     * 邮箱
+     */
+    private String email;
+
+    /**
+     * 手机号
+     */
+    private String phone;
+
+    /**
+     * 头像
+     */
+    private String avatar;
+
+    /**
+     * 状态(0正常 1停用)
+     */
+    private String status;
+
+    /**
+     * 删除标志(0代表存在 1代表删除)
+     */
+    @TableLogic
+    private String delFlag;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 微信openid
+     */
+    private String openid;
+
+    /**
+     * 微信unionid
+     */
+    private String unionid;
+
+    /**
+     * 微信昵称
+     */
+    private String nickname;
+
+}

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

@@ -0,0 +1,15 @@
+package org.dromara.system.domain.app;
+
+import lombok.Data;
+
+/**
+ * 微信登录结果类
+ */
+@Data
+public class WxLoginResult {
+    private String openid;
+    private String session_key;
+    private String unionid;
+    private Integer errcode;
+    private String errmsg;
+}

+ 3 - 3
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/bo/GameEventBo.java

@@ -54,7 +54,7 @@ public class GameEventBo extends BaseEntity {
     /**
      * 用途
      */
-    @NotBlank(message = "用途不能为空", groups = { AddGroup.class, EditGroup.class })
+//    @NotBlank(message = "用途不能为空", groups = { AddGroup.class, EditGroup.class })
     private String purpose;
 
     /**
@@ -72,13 +72,13 @@ public class GameEventBo extends BaseEntity {
     /**
      * 赛事链接
      */
-    @NotBlank(message = "赛事链接不能为空", groups = { AddGroup.class, EditGroup.class })
+//    @NotBlank(message = "赛事链接不能为空", groups = { AddGroup.class, EditGroup.class })
     private String eventUrl;
 
     /**
      * 裁判码
      */
-    @NotBlank(message = "裁判码不能为空", groups = { AddGroup.class, EditGroup.class })
+//    @NotBlank(message = "裁判码不能为空", groups = { AddGroup.class, EditGroup.class })
     private String refereeUrl;
 
     /**

+ 3 - 3
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/GameTeamVo.java

@@ -38,9 +38,9 @@ public class GameTeamVo implements Serializable {
     private Long eventId;
 
     /**
-     * 赛事ID
+     * 赛事名称
      */
-//    @ExcelProperty(value = "赛事名称")
+    @ExcelProperty(value = "赛事名称")
     private String eventName;
 
     /**
@@ -83,7 +83,7 @@ public class GameTeamVo implements Serializable {
     /**
      * 号码段
      */
-//    @ExcelProperty(value = "号码段")
+    @ExcelProperty(value = "号码段")
     private String numberRange;
 
     /**

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

@@ -0,0 +1,96 @@
+package org.dromara.system.domain.vo.app;
+import org.dromara.system.domain.app.GameUser;
+import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
+import cn.idev.excel.annotation.ExcelProperty;
+import org.dromara.common.excel.annotation.ExcelDictFormat;
+import org.dromara.common.excel.convert.ExcelDictConvert;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+
+
+/**
+ * 赛事用户视图对象 game_user
+ *
+ * @author Lion Li
+ * @date 2025-08-27
+ */
+@Data
+@ExcelIgnoreUnannotated
+@AutoMapper(target = GameUser.class)
+public class GameUserVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 用户ID
+     */
+    @ExcelProperty(value = "用户ID")
+    private Long userId;
+
+    /**
+     * 用户名/账号
+     */
+    @ExcelProperty(value = "用户名/账号")
+    private String username;
+
+    /**
+     * 密码
+     */
+    @ExcelProperty(value = "密码")
+    private String password;
+
+    /**
+     * 邮箱
+     */
+    @ExcelProperty(value = "邮箱")
+    private String email;
+
+    /**
+     * 手机号
+     */
+    @ExcelProperty(value = "手机号")
+    private String phone;
+
+    /**
+     * 头像
+     */
+    @ExcelProperty(value = "头像")
+    private String avatar;
+
+    /**
+     * 状态(0正常 1停用)
+     */
+    @ExcelProperty(value = "状态", converter = ExcelDictConvert.class)
+    @ExcelDictFormat(readConverterExp = "0=正常,1=停用")
+    private String status;
+
+    /**
+     * 备注
+     */
+    @ExcelProperty(value = "备注")
+    private String remark;
+
+    /**
+     * 微信openid
+     */
+    @ExcelProperty(value = "微信openid")
+    private String openid;
+
+    /**
+     * 微信unionid
+     */
+    @ExcelProperty(value = "微信unionid")
+    private String unionid;
+
+    /**
+     * 微信昵称
+     */
+    @ExcelProperty(value = "微信昵称")
+    private String nickname;
+
+}

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

@@ -0,0 +1,34 @@
+package org.dromara.system.domain.vo.app;
+
+import lombok.Data;
+import org.dromara.system.domain.vo.GameAthleteVo;
+import org.dromara.system.domain.vo.GameEventVo;
+
+import java.io.Serial;
+import java.io.Serializable;
+import org.dromara.system.domain.vo.GameEventProjectVo;
+import java.util.List;
+
+/**
+ * 用户赛事信息返回VO
+ *
+ * @author zlt
+ */
+@Data
+public class UserEventInfoVo implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    // 用户基本信息
+    private Long userId;
+    private String username;
+
+    // 运动员信息
+    private GameAthleteVo athleteInfo;
+
+    // 赛事信息
+    private GameEventVo eventInfo;
+
+    // 项目列表(包含成绩信息)
+    private List<GameEventProjectVo> projectList;
+}

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

@@ -0,0 +1,52 @@
+package org.dromara.system.domain.vo.app;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 微信小程序用户登录VO
+ *
+ * @author zlt
+ */
+@Data
+public class UserLoginVo implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 微信小程序登录凭证
+     */
+    private String code;
+
+    /**
+     * 用户昵称
+     */
+    private String nickName;
+
+    /**
+     * 用户头像
+     */
+    private String avatarUrl;
+
+    /**
+     * 性别 0-未知 1-男 2-女
+     */
+    private Integer gender;
+
+    /**
+     * 国家
+     */
+    private String country;
+
+    /**
+     * 省份
+     */
+    private String province;
+
+    /**
+     * 城市
+     */
+    private String city;
+}

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

@@ -1,5 +1,6 @@
 package org.dromara.system.mapper;
 
+import org.apache.ibatis.annotations.Select;
 import org.dromara.system.domain.GameAthlete;
 import org.dromara.system.domain.vo.GameAthleteVo;
 import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
@@ -12,4 +13,6 @@ import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
  */
 public interface GameAthleteMapper extends BaseMapperPlus<GameAthlete, GameAthleteVo> {
 
+    @Select("select * from game_athlete where user_id = #{userId} AND del_flag = '0'")
+    GameAthlete selectByUserId(Long userId);
 }

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

@@ -1,5 +1,6 @@
 package org.dromara.system.mapper;
 
+import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 import org.dromara.system.domain.GameScore;
 import org.dromara.system.domain.vo.GameScoreVo;
@@ -33,4 +34,15 @@ public interface GameScoreMapper extends BaseMapperPlus<GameScore, GameScoreVo>
     GameScoreVo selectVoByAthleteIdAndProjectId(Long athleteId, Long projectId);
 
 
+    /**
+     * 获取运动员项目成绩
+     * @param athleteId
+     * @return
+     */
+    @Select("select project_id from game_score where athlete_id = #{athleteId} AND del_flag = '0'")
+    List<Long> selectProjectIdsByAthleteId(Long athleteId);
+
+    // 查询运动员在指定项目中的成绩
+    List<GameScore> selectByAthleteAndProjects(@Param("athleteId") Long athleteId,
+                                               @Param("projectIds") List<Long> projectIds);
 }

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

@@ -0,0 +1,41 @@
+package org.dromara.system.mapper.app;
+
+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.GameUserVo;
+
+/**
+ * 赛事用户Mapper接口
+ *
+ * @author zlt
+ */
+public interface GameUserMapper extends BaseMapperPlus<GameUser, GameUserVo> {
+
+    /**
+     * 根据用户名查询用户
+     *
+     * @param username 用户名
+     * @return 用户
+     */
+    @Select("select * from game_user where username = #{username}")
+    GameUser selectUserByUserName(String username);
+
+    /**
+     * 根据用户ID查询用户
+     *
+     * @param userId 用户ID
+     * @return 用户
+     */
+    @Select("select * from game_user where user_id = #{userId}")
+    GameUser selectUserById(Long userId);
+
+    /**
+     * 根据微信openid查询用户
+     *
+     * @param openid 微信openid
+     * @return 用户
+     */
+    @Select("select * from game_user where openid = #{openid} and del_flag = '0'")
+    GameUser selectByOpenid(String openid);
+}

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

@@ -0,0 +1,12 @@
+package org.dromara.system.service.app;
+
+import org.dromara.system.domain.vo.app.UserEventInfoVo;
+import org.dromara.system.domain.vo.app.UserLoginVo;
+
+public interface IUserEventService {
+    // 用户登录
+    UserEventInfoVo login(UserLoginVo loginVo);
+
+    // 获取用户赛事信息
+    UserEventInfoVo getUserEventInfo(Long userId);
+}

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

@@ -70,7 +70,9 @@ public class AdviceServiceImpl implements IAdviceService {
         result.getRecords().stream()
             .map(vo -> {
                 GameTeamVo gameTeamVo = gameTeamService.queryById(vo.getTeamId());
-                vo.setTeamName(gameTeamVo.getTeamName());
+                if (gameTeamVo != null){
+                    vo.setTeamName(gameTeamVo.getTeamName());
+                }
                 return vo;
             })
             .collect(Collectors.toList());

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

@@ -0,0 +1,293 @@
+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.system.domain.GameAthlete;
+import org.dromara.system.domain.GameEvent;
+import org.dromara.system.domain.GameEventProject;
+import org.dromara.system.domain.GameScore;
+import org.dromara.system.domain.app.GameUser;
+import org.dromara.system.domain.app.WxLoginResult;
+import org.dromara.system.domain.vo.GameAthleteVo;
+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.mapper.*;
+import org.dromara.system.mapper.app.GameUserMapper;
+import org.dromara.system.service.app.IUserEventService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Mono;
+
+import java.time.Duration;
+import java.util.*;
+
+@Service
+public class UserEventServiceImpl implements IUserEventService {
+    @Autowired
+    private GameUserMapper gameUserMapper;
+
+    @Autowired
+    private GameAthleteMapper gameAthleteMapper;
+
+    @Autowired
+    private GameScoreMapper gameScoreMapper;
+
+    @Autowired
+    private GameEventProjectMapper gameEventProjectMapper;
+
+    @Autowired
+    private GameEventMapper gameEventMapper;
+
+    @Autowired
+    private WebClient webClient;
+
+    // Jackson ObjectMapper用于JSON解析
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    // 微信小程序配置(需要在配置文件中设置)
+    @Value("${wechat.miniapp.appid:}")
+    private String appId;
+
+    @Value("${wechat.miniapp.secret:}")
+    private String secret;
+
+    @Override
+    public UserEventInfoVo login(UserLoginVo loginVo) {
+        try {
+            // 步骤1:通过code获取微信openid和session_key
+            WxLoginResult wxResult = getWxLoginResult(loginVo.getCode());
+            if (wxResult == null || wxResult.getOpenid() == null || wxResult.getOpenid().isEmpty()) {
+                throw new RuntimeException("微信登录失败,无法获取用户信息");
+            }
+
+            // 步骤2:查找或创建用户
+            GameUser user = findOrCreateUser(wxResult.getOpenid(), loginVo);
+
+            // 步骤3:返回用户基本信息
+            UserEventInfoVo result = new UserEventInfoVo();
+            result.setUserId(user.getUserId());
+            result.setUsername(user.getUsername());
+
+            return result;
+        } catch (Exception e) {
+            throw new RuntimeException("微信登录失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 通过code获取微信登录结果
+     */
+    private WxLoginResult getWxLoginResult(String code) {
+        try {
+            if (appId == null || appId.isEmpty() || secret == null || secret.isEmpty()) {
+                throw new RuntimeException("微信小程序配置未设置");
+            }
+
+            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
+            );
+
+            // 先获取字符串响应,避免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(); // 同步等待结果
+
+            if (responseBody == null || responseBody.trim().isEmpty()) {
+                throw new RuntimeException("微信接口返回空结果");
+            }
+
+            // 手动解析JSON响应
+            WxLoginResult result = parseWxResponse(responseBody);
+            
+            if (result.getErrcode() != null && result.getErrcode() != 0) {
+                throw new RuntimeException("微信接口返回错误:" + result.getErrmsg() + " (错误码: " + result.getErrcode() + ")");
+            }
+            
+            if (result.getOpenid() == null || result.getOpenid().isEmpty()) {
+                throw new RuntimeException("微信接口未返回openid");
+            }
+            
+            return result;
+        } catch (Exception e) {
+            if (e instanceof RuntimeException) {
+                throw e;
+            }
+            throw new RuntimeException("调用微信接口失败:" + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 解析微信API响应
+     */
+    private WxLoginResult parseWxResponse(String responseBody) {
+        try {
+            // 使用Jackson解析JSON
+            WxLoginResult result = objectMapper.readValue(responseBody, WxLoginResult.class);
+            return result;
+        } catch (Exception e) {
+            throw new RuntimeException("解析微信响应失败:" + e.getMessage() + ", 响应内容: " + responseBody);
+        }
+    }
+
+    /**
+     * 查找或创建用户
+     */
+    private GameUser findOrCreateUser(String openid, UserLoginVo loginVo) {
+        // 先通过openid查找用户
+        GameUser user = gameUserMapper.selectByOpenid(openid);
+
+        if (user == null) {
+            // 用户不存在,创建新用户
+            user = new GameUser();
+            user.setOpenid(openid);
+            user.setUsername("wx_" + openid.substring(0, Math.min(8, openid.length()))); // 生成用户名
+            user.setNickname(loginVo.getNickName() != null ? loginVo.getNickName() : "微信用户");
+            user.setAvatar(loginVo.getAvatarUrl());
+            // 注意:如果数据库password字段允许为NULL,则不需要设置密码
+            // 如果数据库password字段不允许为NULL,请取消注释下面这行代码
+            // user.setPassword("WX_LOGIN_" + System.currentTimeMillis());
+            user.setCreateTime(new Date());
+            user.setUpdateTime(new Date());
+            user.setStatus("0"); // 正常状态
+            user.setDelFlag("0");
+
+            // 插入用户
+            gameUserMapper.insert(user);
+        } else {
+            // 用户存在,更新信息
+            if (loginVo.getNickName() != null && !loginVo.getNickName().isEmpty()) {
+                user.setNickname(loginVo.getNickName());
+            }
+            if (loginVo.getAvatarUrl() != null && !loginVo.getAvatarUrl().isEmpty()) {
+                user.setAvatar(loginVo.getAvatarUrl());
+            }
+            user.setUpdateTime(new Date());
+
+            // 更新用户
+            gameUserMapper.updateById(user);
+        }
+
+        return user;
+    }
+
+    @Override
+    public UserEventInfoVo getUserEventInfo(Long userId) {
+        // 步骤1:查询用户基本信息
+        GameUser user = gameUserMapper.selectUserById(userId);
+        if (user == null) {
+            throw new RuntimeException("用户不存在");
+        }
+
+        // 步骤2:查询用户关联的运动员信息
+        GameAthlete athlete = gameAthleteMapper.selectByUserId(userId);
+        if (athlete == null) {
+            throw new RuntimeException("用户未关联运动员信息");
+        }
+
+        // 步骤3:查询运动员参与的项目ID列表
+        List<Long> projectIds = gameScoreMapper.selectProjectIdsByAthleteId(athlete.getAthleteId());
+        if (CollectionUtils.isEmpty(projectIds)) {
+            // 用户没有参与任何项目
+            return buildEmptyUserEventInfo(user, athlete);
+        }
+
+        // 步骤4:查询项目详细信息
+        List<GameEventProject> projects = gameEventProjectMapper.selectBatchIds(projectIds);
+
+        // 步骤5:查询赛事信息
+        GameEvent event = gameEventMapper.selectById(projects.get(0).getEventId());
+
+        // 步骤6:查询运动员在各项目中的成绩
+        List<GameScore> scores = gameScoreMapper.selectByAthleteAndProjects(athlete.getAthleteId(), projectIds);
+
+        // 步骤7:组装数据返回
+        return assembleUserEventInfo(user, athlete, event, projects, scores);
+    }
+
+    private UserEventInfoVo buildEmptyUserEventInfo(GameUser user, GameAthlete athlete) {
+        UserEventInfoVo result = new UserEventInfoVo();
+        result.setUserId(user.getUserId());
+        result.setUsername(user.getUsername());
+
+        // 设置运动员信息
+        GameAthleteVo athleteInfo = MapstructUtils.convert(athlete, GameAthleteVo.class);
+        result.setAthleteInfo(athleteInfo);
+
+        // 设置空的赛事和项目信息
+        result.setEventInfo(null);
+        result.setProjectList(new ArrayList<>());
+
+        return result;
+    }
+
+    private UserEventInfoVo assembleUserEventInfo(GameUser user, GameAthlete athlete,
+                                                  GameEvent event, List<GameEventProject> projects,
+                                                  List<GameScore> scores) {
+        UserEventInfoVo result = new UserEventInfoVo();
+        result.setUserId(user.getUserId());
+        result.setUsername(user.getUsername());
+
+        // 组装运动员信息
+        GameAthleteVo athleteInfo = MapstructUtils.convert(athlete, GameAthleteVo.class);
+        result.setAthleteInfo(athleteInfo);
+
+        // 组装赛事信息
+        GameEventVo eventInfo = MapstructUtils.convert(event, GameEventVo.class);
+        result.setEventInfo(eventInfo);
+
+        // 组装项目信息
+        List<GameEventProjectVo> projectList = new ArrayList<>();
+        for (GameEventProject project : projects) {
+            GameEventProjectVo projectInfo = MapstructUtils.convert(project, GameEventProjectVo.class);
+
+            // 查找该项目的成绩信息
+            GameScore score = findScoreByProjectId(scores, project.getProjectId());
+            if (score != null && projectInfo != null) {
+                // 将成绩信息添加到项目信息中,可以通过扩展字段或备注字段存储
+                // 这里我们使用备注字段来存储额外的成绩信息
+                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()));
+                projectInfo.setRemark(scoreInfo);
+            }
+
+            projectList.add(projectInfo);
+        }
+
+        // 按项目开始时间排序
+        projectList.sort(Comparator.comparing(GameEventProjectVo::getStartTime));
+        result.setProjectList(projectList);
+
+        return result;
+    }
+
+    private GameScore findScoreByProjectId(List<GameScore> scores, Long projectId) {
+        return scores.stream()
+            .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 "铜牌";
+        return "无";
+    }
+}

+ 10 - 0
ruoyi-modules/ruoyi-game-event/src/main/resources/mapper/system/GameScoreMapper.xml

@@ -18,4 +18,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         FROM game_score
         WHERE athlete_id = #{athleteId} AND project_id = #{projectId}
     </select>
+
+    <select id="selectByAthleteAndProjects" resultType="GameScore">
+        SELECT *
+        FROM game_score
+        WHERE athlete_id = #{athleteId} AND project_id IN
+        <foreach item="projectId" collection="projectIds" separator="," open="(" close=")">
+            #{projectId}
+        </foreach>
+        AND del_flag = '0'
+    </select>
 </mapper>

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

@@ -0,0 +1,7 @@
+<?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.GameUserMapper">
+
+</mapper>