Bladeren bron

Merge branch 'dev_zlt' into dev

# Conflicts:
#	ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/AdviceServiceImpl.java
zhou 1 maand geleden
bovenliggende
commit
919243da45
36 gewijzigde bestanden met toevoegingen van 2173 en 27 verwijderingen
  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. 25 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/GameEventGroupController.java
  6. 1 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/GameTeamController.java
  7. 93 1
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/app/ScoreController.java
  8. 53 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/app/UserEventController.java
  9. 91 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/GameAthleteCompetitionGroup.java
  10. 86 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/app/GameUser.java
  11. 15 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/app/WxLoginResult.java
  12. 90 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/bo/AppScoreUploadBo.java
  13. 96 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/bo/GameAthleteCompetitionGroupBo.java
  14. 3 3
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/bo/GameEventBo.java
  15. 2 2
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/bo/GameScoreBo.java
  16. 90 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/GameAthleteCompetitionGroupVo.java
  17. 3 3
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/GameTeamVo.java
  18. 96 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/app/GameUserVo.java
  19. 34 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/app/UserEventInfoVo.java
  20. 52 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/app/UserLoginVo.java
  21. 15 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/mapper/GameAthleteCompetitionGroupMapper.java
  22. 3 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/mapper/GameAthleteMapper.java
  23. 12 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/mapper/GameScoreMapper.java
  24. 41 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/mapper/app/GameUserMapper.java
  25. 95 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/IGameAthleteCompetitionGroupService.java
  26. 17 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/IGameEventGroupService.java
  27. 211 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/app/AppScoreValidationService.java
  28. 12 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/app/IUserEventService.java
  29. 1 1
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/AdviceServiceImpl.java
  30. 295 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/GameAthleteCompetitionGroupServiceImpl.java
  31. 233 8
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/GameEventGroupServiceImpl.java
  32. 5 9
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/GameScoreServiceImpl.java
  33. 293 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/app/UserEventServiceImpl.java
  34. 30 0
      ruoyi-modules/ruoyi-game-event/src/main/resources/mapper/system/GameAthleteCompetitionGroupMapper.xml
  35. 10 0
      ruoyi-modules/ruoyi-game-event/src/main/resources/mapper/system/GameScoreMapper.xml
  36. 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) {
         // 如果修改的对象 原本是默认并且准备取消则禁止

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

@@ -1,6 +1,7 @@
 package org.dromara.system.controller;
 
 import java.util.List;
+import java.util.Map;
 
 import lombok.RequiredArgsConstructor;
 import jakarta.servlet.http.HttpServletResponse;
@@ -102,4 +103,28 @@ public class GameEventGroupController extends BaseController {
                           @PathVariable Long[] groupIds) {
         return toAjax(gameEventGroupService.deleteWithValidByIds(List.of(groupIds), true));
     }
+
+    /**
+     * 生成分组结果
+     *
+     * @param groupId 分组ID
+     */
+    @SaCheckPermission("system:gameEventGroup:query")
+    @GetMapping("/generateGroups/{groupId}")
+    public R<Map<String, Object>> generateGroups(@NotNull(message = "分组ID不能为空")
+                                               @PathVariable Long groupId) {
+        return R.ok(gameEventGroupService.generateGroups(groupId));
+    }
+
+    /**
+     * 从数据库获取分组结果
+     *
+     * @param groupId 分组ID
+     */
+    @SaCheckPermission("system:gameEventGroup:query")
+    @GetMapping("/getGroupResultFromDB/{groupId}")
+    public R<Map<String, Object>> getGroupResultFromDB(@NotNull(message = "分组ID不能为空")
+                                                     @PathVariable Long groupId) {
+        return R.ok(gameEventGroupService.getGroupResultFromDB(groupId));
+    }
 }

+ 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()));

+ 93 - 1
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/app/ScoreController.java

@@ -3,10 +3,16 @@ 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.idempotent.annotation.RepeatSubmit;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
 import org.dromara.system.domain.vo.AppScoreVo;
 import org.dromara.system.domain.vo.GameScoreVo;
-import org.dromara.system.domain.vo.GameTeamVo;
 import org.dromara.system.service.IGameScoreService;
+import org.dromara.system.service.app.AppScoreValidationService;
+import org.dromara.system.domain.bo.AppScoreUploadBo;
+import org.dromara.system.domain.bo.GameScoreBo;
+import org.dromara.common.core.validate.AddGroup;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
@@ -24,6 +30,7 @@ import java.util.Map;
 public class ScoreController {
 
     private final IGameScoreService gameScoreService;
+    private final AppScoreValidationService appScoreValidationService;
 
     /**
      * 查看获奖名单
@@ -45,4 +52,89 @@ public class ScoreController {
         List<AppScoreVo> result = gameScoreService.listAppScore(eventId, projectId);
         return R.ok(result);
     }
+
+    /**
+     * 小程序端批量上传成绩数据
+     * 专门为小程序端设计的批量成绩上传接口,包含完整的校验逻辑
+     */
+    @RepeatSubmit()
+    @PostMapping("/upload")
+    @Log(title = "小程序端批量上传成绩数据", businessType = BusinessType.INSERT)
+    public R<String> uploadScore(@Validated(AddGroup.class) @RequestBody List<AppScoreUploadBo> scoreList) {
+        try {
+            if (scoreList == null || scoreList.isEmpty()) {
+                return R.fail("成绩数据列表不能为空");
+            }
+
+            if (scoreList.size() > 100) {
+                return R.fail("单次最多上传100条成绩数据");
+            }
+
+            int successCount = 0;
+            int failCount = 0;
+            StringBuilder errorMsg = new StringBuilder();
+
+            // 批量处理成绩数据
+            for (int i = 0; i < scoreList.size(); i++) {
+                AppScoreUploadBo bo = scoreList.get(i);
+                try {
+                    // 保存前校验
+                    appScoreValidationService.validateAppScoreUpload(bo);
+
+                    // 校验通过后,转换为GameScoreBo并调用服务层保存
+                    GameScoreBo gameScoreBo = convertToGameScoreBo(bo);
+
+                    // 调用成绩服务保存数据
+                    boolean success = gameScoreService.insertByBo(gameScoreBo);
+
+                    if (success) {
+                        successCount++;
+                    } else {
+                        failCount++;
+                        errorMsg.append("第").append(i + 1).append("条数据保存失败; ");
+                    }
+                } catch (Exception e) {
+                    failCount++;
+                    errorMsg.append("第").append(i + 1).append("条数据校验失败: ").append(e.getMessage()).append("; ");
+                }
+            }
+
+            // 返回处理结果
+            if (failCount == 0) {
+                return R.ok("批量成绩上传成功,共上传" + successCount + "条数据");
+            } else if (successCount > 0) {
+                return R.ok("部分成绩上传成功,成功" + successCount + "条,失败" + failCount + "条。失败原因:" + errorMsg.toString());
+            } else {
+                return R.fail("批量成绩上传失败,失败原因:" + errorMsg.toString());
+            }
+        } catch (Exception e) {
+            return R.fail("批量成绩上传失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 将AppScoreUploadBo转换为GameScoreBo
+     */
+    private GameScoreBo convertToGameScoreBo(AppScoreUploadBo bo) {
+        GameScoreBo gameScoreBo = new GameScoreBo();
+        gameScoreBo.setEventId(bo.getEventId());
+        gameScoreBo.setProjectId(bo.getProjectId());
+        gameScoreBo.setAthleteId(bo.getAthleteId());
+        gameScoreBo.setTeamId(bo.getTeamId());
+        gameScoreBo.setIndividualPerformance(bo.getIndividualPerformance());
+        gameScoreBo.setTeamPerformance(bo.getTeamPerformance());
+        gameScoreBo.setScoreType(bo.getScoreType());
+        gameScoreBo.setScoreRank(bo.getScoreRank());
+        gameScoreBo.setScorePoint(bo.getScorePoint());
+        gameScoreBo.setAward(bo.getAward());
+        gameScoreBo.setExtraScore1(bo.getExtraScore1());
+        gameScoreBo.setExtraScore2(bo.getExtraScore2());
+        gameScoreBo.setRemark(bo.getRemark());
+
+        // 设置默认值
+        gameScoreBo.setStatusFlag("0"); // 有效
+        gameScoreBo.setStatus("0");     // 正常
+
+        return gameScoreBo;
+    }
 }

+ 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());
+        }
+    }
+}

+ 91 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/GameAthleteCompetitionGroup.java

@@ -0,0 +1,91 @@
+package org.dromara.system.domain;
+
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serial;
+
+/**
+ * 运动员比赛场次分组关联对象 game_athlete_competition_group
+ *
+ * @author zlt
+ * @date 2025-08-28
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("game_athlete_competition_group")
+public class GameAthleteCompetitionGroup extends BaseEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(value = "id")
+    private Long id;
+
+    /**
+     * 分组ID
+     */
+    private Long groupId;
+
+    /**
+     * 赛事ID
+     */
+    private Long eventId;
+
+    /**
+     * 项目ID
+     */
+    private Long projectId;
+
+    /**
+     * 运动员ID
+     */
+    private Long athleteId;
+
+    /**
+     * 队伍ID
+     */
+    private Long teamId;
+
+    /**
+     * 组次序号
+     */
+    private Long groupIndex;
+
+    /**
+     * 道次序号
+     */
+    private Long trackIndex;
+
+    /**
+     * 运动员编号
+     */
+    private String athleteCode;
+
+    /**
+     * 运动员姓名
+     */
+    private String athleteName;
+
+    /**
+     * 队伍名称
+     */
+    private String teamName;
+
+    /**
+     * 状态(0正常 1停用)
+     */
+    private String status;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+
+}

+ 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;
+}

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

@@ -0,0 +1,90 @@
+package org.dromara.system.domain.bo;
+
+import lombok.Data;
+import org.dromara.common.core.validate.AddGroup;
+import jakarta.validation.constraints.*;
+
+import java.math.BigDecimal;
+
+/**
+ * 小程序端成绩上传业务对象
+ *
+ * @author zlt
+ * @date 2025-07-30
+ */
+@Data
+public class AppScoreUploadBo {
+
+    /**
+     * 赛事ID
+     */
+    @NotNull(message = "赛事ID不能为空", groups = { AddGroup.class })
+    private Long eventId;
+
+    /**
+     * 项目ID
+     */
+    @NotNull(message = "项目ID不能为空", groups = { AddGroup.class })
+    private Long projectId;
+
+    /**
+     * 运动员ID
+     */
+    @NotNull(message = "运动员ID不能为空", groups = { AddGroup.class })
+    private Long athleteId;
+
+    /**
+     * 队伍ID
+     */
+    @NotNull(message = "队伍ID不能为空", groups = { AddGroup.class })
+    private Long teamId;
+
+    /**
+     * 个人成绩
+     */
+    @DecimalMin(value = "0.0", message = "个人成绩不能为负数", groups = { AddGroup.class })
+    private BigDecimal individualPerformance;
+
+    /**
+     * 团队成绩
+     */
+    @DecimalMin(value = "0.0", message = "团队成绩不能为负数", groups = { AddGroup.class })
+    private BigDecimal teamPerformance;
+
+    /**
+     * 成绩类型
+     */
+    private String scoreType;
+
+    /**
+     * 名次
+     */
+    @Min(value = 1, message = "名次必须大于0", groups = { AddGroup.class })
+    private Integer scoreRank;
+
+    /**
+     * 积分
+     */
+    @Min(value = 0, message = "积分不能为负数", groups = { AddGroup.class })
+    private Integer scorePoint;
+
+    /**
+     * 奖项
+     */
+    private String award;
+
+    /**
+     * 附加成绩1
+     */
+    private String extraScore1;
+
+    /**
+     * 附加成绩2
+     */
+    private String extraScore2;
+
+    /**
+     * 备注
+     */
+    private String remark;
+}

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

@@ -0,0 +1,96 @@
+package org.dromara.system.domain.bo;
+
+import io.github.linpeilie.annotations.AutoMapper;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import jakarta.validation.constraints.NotNull;
+import org.dromara.system.domain.GameAthleteCompetitionGroup;
+
+import java.io.Serial;
+
+/**
+ * 运动员比赛场次分组关联业务对象 game_athlete_competition_group
+ *
+ * @author zlt
+ * @date 2025-08-28
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AutoMapper(target = GameAthleteCompetitionGroup.class, reverseConvertGenerate = false)
+public class GameAthleteCompetitionGroupBo extends BaseEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    private Long id;
+
+    /**
+     * 分组ID
+     */
+    @NotNull(message = "分组ID不能为空")
+    private Long groupId;
+
+    /**
+     * 赛事ID
+     */
+    private Long eventId;
+
+    /**
+     * 项目ID
+     */
+    private Long projectId;
+
+    /**
+     * 运动员ID
+     */
+    @NotNull(message = "运动员ID不能为空")
+    private Long athleteId;
+
+    /**
+     * 队伍ID
+     */
+    private Long teamId;
+
+    /**
+     * 组次序号
+     */
+    @NotNull(message = "组次序号不能为空")
+    private Long groupIndex;
+
+    /**
+     * 道次序号
+     */
+    @NotNull(message = "道次序号不能为空")
+    private Long trackIndex;
+
+    /**
+     * 运动员编号
+     */
+    private String athleteCode;
+
+    /**
+     * 运动员姓名
+     */
+    private String athleteName;
+
+    /**
+     * 队伍名称
+     */
+    private String teamName;
+
+    /**
+     * 状态(0正常 1停用)
+     */
+    private String status;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+}

+ 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;
 
     /**

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

@@ -31,13 +31,13 @@ public class GameScoreBo extends BaseEntity {
     /**
      * 赛事ID
      */
-//    @NotNull(message = "赛事ID不能为空", groups = { AddGroup.class, EditGroup.class })
+    @NotNull(message = "赛事ID不能为空", groups = { AddGroup.class, EditGroup.class })
     private Long eventId;
 
     /**
      * 项目ID
      */
-//    @NotNull(message = "项目ID不能为空", groups = { AddGroup.class, EditGroup.class })
+    @NotNull(message = "项目ID不能为空", groups = { AddGroup.class, EditGroup.class })
     private Long projectId;
 
     /**

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

@@ -0,0 +1,90 @@
+package org.dromara.system.domain.vo;
+
+import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import org.dromara.system.domain.GameAthleteCompetitionGroup;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 运动员比赛场次分组关联视图对象 game_athlete_competition_group
+ *
+ * @author zlt
+ * @date 2025-08-28
+ */
+@Data
+@ExcelIgnoreUnannotated
+@AutoMapper(target = GameAthleteCompetitionGroup.class)
+public class GameAthleteCompetitionGroupVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    private Long id;
+
+    /**
+     * 分组ID
+     */
+    private Long groupId;
+
+    /**
+     * 赛事ID
+     */
+    private Long eventId;
+
+    /**
+     * 项目ID
+     */
+    private Long projectId;
+
+    /**
+     * 运动员ID
+     */
+    private Long athleteId;
+
+    /**
+     * 队伍ID
+     */
+    private Long teamId;
+
+    /**
+     * 组次序号
+     */
+    private Long groupIndex;
+
+    /**
+     * 道次序号
+     */
+    private Long trackIndex;
+
+    /**
+     * 运动员编号
+     */
+    private String athleteCode;
+
+    /**
+     * 运动员姓名
+     */
+    private String athleteName;
+
+    /**
+     * 队伍名称
+     */
+    private String teamName;
+
+    /**
+     * 状态(0正常 1停用)
+     */
+    private String status;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+}

+ 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;
+}

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

@@ -0,0 +1,15 @@
+package org.dromara.system.mapper;
+
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.system.domain.GameAthleteCompetitionGroup;
+import org.dromara.system.domain.vo.GameAthleteCompetitionGroupVo;
+
+/**
+ * 运动员比赛场次分组关联Mapper接口
+ *
+ * @author zlt
+ * @date 2025-08-28
+ */
+public interface GameAthleteCompetitionGroupMapper extends BaseMapperPlus<GameAthleteCompetitionGroup, GameAthleteCompetitionGroupVo> {
+
+} 

+ 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);
+}

+ 95 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/IGameAthleteCompetitionGroupService.java

@@ -0,0 +1,95 @@
+package org.dromara.system.service;
+
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.system.domain.bo.GameAthleteCompetitionGroupBo;
+import org.dromara.system.domain.vo.GameAthleteCompetitionGroupVo;
+import org.dromara.system.domain.vo.GameAthleteVo;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 运动员比赛场次分组关联Service接口
+ *
+ * @author zlt
+ * @date 2025-08-28
+ */
+public interface IGameAthleteCompetitionGroupService {
+
+    /**
+     * 查询运动员比赛场次分组关联
+     *
+     * @param id 主键
+     * @return 运动员比赛场次分组关联
+     */
+    GameAthleteCompetitionGroupVo queryById(Long id);
+
+    /**
+     * 查询运动员比赛场次分组关联列表
+     *
+     * @param bo 查询条件
+     * @return 运动员比赛场次分组关联集合
+     */
+    TableDataInfo<GameAthleteCompetitionGroupVo> queryPageList(GameAthleteCompetitionGroupBo bo, PageQuery pageQuery);
+
+    /**
+     * 查询运动员比赛场次分组关联列表
+     *
+     * @param bo 查询条件
+     * @return 运动员比赛场次分组关联集合
+     */
+    List<GameAthleteCompetitionGroupVo> queryList(GameAthleteCompetitionGroupBo bo);
+
+    /**
+     * 新增运动员比赛场次分组关联
+     *
+     * @param bo 运动员比赛场次分组关联新增业务对象
+     * @return 结果
+     */
+    Boolean insertByBo(GameAthleteCompetitionGroupBo bo);
+
+    /**
+     * 修改运动员比赛场次分组关联
+     *
+     * @param bo 运动员比赛场次分组关联编辑业务对象
+     * @return 结果
+     */
+    Boolean updateByBo(GameAthleteCompetitionGroupBo bo);
+
+    /**
+     * 校验并批量删除运动员比赛场次分组关联信息
+     *
+     * @param ids     主键集合
+     * @param isValid 是否检验,true-删除前校验,false-不校验
+     * @return 结果
+     */
+    Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
+
+    /**
+     * 根据分组ID查询分组结果
+     *
+     * @param groupId 分组ID
+     * @return 分组结果
+     */
+    Map<String, Object> getGroupResultFromDB(Long groupId);
+
+    /**
+     * 保存分组结果到数据库
+     *
+     * @param groupId 分组ID
+     * @param groupResult 分组结果
+     * @param groupInfo 分组信息
+     * @return 是否保存成功
+     */
+    Boolean saveGroupResult(Long groupId, Map<String, GameAthleteVo> groupResult, Object groupInfo);
+
+    /**
+     * 根据分组ID删除分组结果
+     *
+     * @param groupId 分组ID
+     * @return 是否删除成功
+     */
+    Boolean deleteGroupResultByGroupId(Long groupId);
+}

+ 17 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/IGameEventGroupService.java

@@ -8,6 +8,7 @@ import org.dromara.common.mybatis.core.page.PageQuery;
 
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 
 /**
  * 赛事分组Service接口
@@ -73,4 +74,20 @@ public interface IGameEventGroupService {
      * @return
      */
     GameEventGroup queryByEventId(Long defaultEventId);
+
+    /**
+     * 生成分组结果
+     *
+     * @param groupId 分组ID
+     * @return 分组结果
+     */
+    Map<String, Object> generateGroups(Long groupId);
+
+    /**
+     * 从数据库获取分组结果
+     *
+     * @param groupId 分组ID
+     * @return 分组结果
+     */
+    Map<String, Object> getGroupResultFromDB(Long groupId);
 }

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

@@ -0,0 +1,211 @@
+package org.dromara.system.service.app;
+
+import org.dromara.system.domain.bo.AppScoreUploadBo;
+import org.dromara.system.domain.vo.GameEventVo;
+import org.dromara.system.domain.vo.GameEventProjectVo;
+import org.dromara.system.domain.vo.GameAthleteVo;
+import org.dromara.system.domain.vo.GameTeamVo;
+import org.dromara.system.service.IGameAthleteService;
+import org.dromara.system.service.IGameEventProjectService;
+import org.dromara.system.service.IGameEventService;
+import org.dromara.system.service.IGameTeamService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.dromara.common.core.exception.ServiceException;
+
+import java.math.BigDecimal;
+
+/**
+ * 小程序端成绩校验服务
+ * 专门用于校验小程序端成绩上传的业务逻辑
+ *
+ * @author zlt
+ * @date 2025-07-30
+ */
+@Service
+public class AppScoreValidationService {
+
+    @Autowired
+    private IGameEventService gameEventService;
+
+    @Autowired
+    private IGameEventProjectService gameEventProjectService;
+
+    @Autowired
+    private IGameAthleteService gameAthleteService;
+
+    @Autowired
+    private IGameTeamService gameTeamService;
+
+    /**
+     * 校验小程序端成绩上传数据
+     * @param bo 成绩上传业务对象
+     */
+    public void validateAppScoreUpload(AppScoreUploadBo bo) {
+        // 基础数据校验
+        validateBasicData(bo);
+
+        // 关联数据校验
+        validateRelatedData(bo);
+
+        // 业务逻辑校验
+        validateBusinessLogic(bo);
+
+        // 成绩数据校验
+        validateScoreData(bo);
+    }
+
+    /**
+     * 基础数据校验
+     */
+    private void validateBasicData(AppScoreUploadBo bo) {
+        if (bo.getEventId() == null) {
+            throw new ServiceException("赛事ID不能为空");
+        }
+
+        if (bo.getProjectId() == null) {
+            throw new ServiceException("项目ID不能为空");
+        }
+
+        if (bo.getAthleteId() == null) {
+            throw new ServiceException("运动员ID不能为空");
+        }
+
+        if (bo.getTeamId() == null) {
+            throw new ServiceException("队伍ID不能为空");
+        }
+    }
+
+    /**
+     * 关联数据校验
+     */
+    private void validateRelatedData(AppScoreUploadBo bo) {
+        // 校验赛事ID
+        try {
+            GameEventVo event = gameEventService.queryById(bo.getEventId());
+            if (event == null) {
+                throw new ServiceException("赛事不存在: " + bo.getEventId());
+            }
+            // 校验赛事状态是否正常
+            if (!"0".equals(event.getStatus())) {
+                throw new ServiceException("赛事状态异常,无法上传成绩");
+            }
+        } catch (Exception e) {
+            if (e instanceof ServiceException) {
+                throw e;
+            }
+            throw new ServiceException("校验赛事ID失败: " + e.getMessage());
+        }
+
+        // 校验项目ID
+        try {
+            GameEventProjectVo project = gameEventProjectService.queryById(bo.getProjectId());
+            if (project == null) {
+                throw new ServiceException("项目不存在: " + bo.getProjectId());
+            }
+
+            // 校验项目是否属于指定赛事
+            if (!bo.getEventId().equals(project.getEventId())) {
+                throw new ServiceException("项目不属于指定赛事");
+            }
+
+            // 校验项目状态是否正常
+            if (!"0".equals(project.getStatus())) {
+                throw new ServiceException("项目状态异常,无法上传成绩");
+            }
+        } catch (Exception e) {
+            if (e instanceof ServiceException) {
+                throw e;
+            }
+            throw new ServiceException("校验项目ID失败: " + e.getMessage());
+        }
+
+        // 校验运动员ID
+        try {
+            GameAthleteVo athlete = gameAthleteService.queryById(bo.getAthleteId());
+            if (athlete == null) {
+                throw new ServiceException("运动员不存在: " + bo.getAthleteId());
+            }
+
+            // 校验运动员状态是否正常
+            if (!"0".equals(athlete.getStatus())) {
+                throw new ServiceException("运动员状态异常,无法上传成绩");
+            }
+        } catch (Exception e) {
+            if (e instanceof ServiceException) {
+                throw e;
+            }
+            throw new ServiceException("校验运动员ID失败: " + e.getMessage());
+        }
+
+        // 校验队伍ID
+        try {
+            GameTeamVo team = gameTeamService.queryById(bo.getTeamId());
+            if (team == null) {
+                throw new ServiceException("队伍不存在: " + bo.getTeamId());
+            }
+
+            // 校验队伍状态是否正常
+            if (!"0".equals(team.getStatus())) {
+                throw new ServiceException("队伍状态异常,无法上传成绩");
+            }
+        } catch (Exception e) {
+            if (e instanceof ServiceException) {
+                throw e;
+            }
+            throw new ServiceException("校验队伍ID失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 业务逻辑校验
+     */
+    private void validateBusinessLogic(AppScoreUploadBo bo) {
+        // 校验是否已经存在该运动员在该项目的成绩
+        // 这里可以添加防重复提交的逻辑
+
+        // 校验成绩上传时间是否在合理范围内
+        // 可以添加时间窗口校验
+
+        // 校验运动员是否参加了该项目
+        // 可以添加参赛资格校验
+    }
+
+    /**
+     * 成绩数据校验
+     */
+    private void validateScoreData(AppScoreUploadBo bo) {
+        // 校验个人成绩
+        if (bo.getIndividualPerformance() != null) {
+            if (bo.getIndividualPerformance().compareTo(BigDecimal.ZERO) < 0) {
+                throw new ServiceException("个人成绩不能为负数");
+            }
+        }
+
+        // 校验团队成绩
+        if (bo.getTeamPerformance() != null) {
+            if (bo.getTeamPerformance().compareTo(BigDecimal.ZERO) < 0) {
+                throw new ServiceException("团队成绩不能为负数");
+            }
+        }
+
+        // 至少需要有一个成绩值
+        if (bo.getIndividualPerformance() == null && bo.getTeamPerformance() == null) {
+            throw new ServiceException("至少需要提供个人成绩或团队成绩");
+        }
+
+        // 校验名次
+        if (bo.getScoreRank() != null) {
+            if (bo.getScoreRank() < 1) {
+                throw new ServiceException("名次必须大于0");
+            }
+        }
+
+        // 校验积分
+        if (bo.getScorePoint() != null) {
+            if (bo.getScorePoint() < 0) {
+                throw new ServiceException("积分不能为负数");
+            }
+        }
+    }
+}

+ 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);
+}

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

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

+ 295 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/GameAthleteCompetitionGroupServiceImpl.java

@@ -0,0 +1,295 @@
+package org.dromara.system.service.impl;
+
+import org.dromara.common.core.utils.MapstructUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.dromara.system.domain.bo.GameAthleteCompetitionGroupBo;
+import org.dromara.system.domain.vo.GameAthleteCompetitionGroupVo;
+import org.dromara.system.domain.GameAthleteCompetitionGroup;
+import org.dromara.system.mapper.GameAthleteCompetitionGroupMapper;
+import org.dromara.system.service.IGameAthleteCompetitionGroupService;
+import org.dromara.system.domain.vo.GameEventGroupVo;
+import org.dromara.system.domain.vo.GameAthleteVo;
+import org.dromara.system.domain.vo.GameTeamVo;
+import org.dromara.system.service.IGameTeamService;
+import org.dromara.common.core.utils.SpringUtils;
+
+import java.util.*;
+
+/**
+ * 运动员比赛场次分组关联Service业务层处理
+ *
+ * @author zlt
+ * @date 2025-08-28
+ */
+@Slf4j
+@Service
+public class GameAthleteCompetitionGroupServiceImpl implements IGameAthleteCompetitionGroupService {
+
+    @Autowired
+    private GameAthleteCompetitionGroupMapper baseMapper;
+
+    private IGameTeamService gameTeamService;
+
+    // 获取队伍服务的辅助方法
+    private IGameTeamService getGameTeamService() {
+        if (gameTeamService == null) {
+            gameTeamService = SpringUtils.getBean(IGameTeamService.class);
+        }
+        return gameTeamService;
+    }
+
+    /**
+     * 查询运动员比赛场次分组关联
+     *
+     * @param id 主键
+     * @return 运动员比赛场次分组关联
+     */
+    @Override
+    public GameAthleteCompetitionGroupVo queryById(Long id) {
+        return baseMapper.selectVoById(id);
+    }
+
+    /**
+     * 分页查询运动员比赛场次分组关联列表
+     *
+     * @param bo        查询条件
+     * @param pageQuery 分页参数
+     * @return 运动员比赛场次分组关联分页列表
+     */
+    @Override
+    public TableDataInfo<GameAthleteCompetitionGroupVo> queryPageList(GameAthleteCompetitionGroupBo bo, PageQuery pageQuery) {
+        LambdaQueryWrapper<GameAthleteCompetitionGroup> lqw = buildQueryWrapper(bo);
+        Page<GameAthleteCompetitionGroupVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
+        return TableDataInfo.build(result);
+    }
+
+    /**
+     * 查询符合条件的运动员比赛场次分组关联列表
+     *
+     * @param bo 查询条件
+     * @return 运动员比赛场次分组关联列表
+     */
+    @Override
+    public List<GameAthleteCompetitionGroupVo> queryList(GameAthleteCompetitionGroupBo bo) {
+        LambdaQueryWrapper<GameAthleteCompetitionGroup> lqw = buildQueryWrapper(bo);
+        return baseMapper.selectVoList(lqw);
+    }
+
+    private LambdaQueryWrapper<GameAthleteCompetitionGroup> buildQueryWrapper(GameAthleteCompetitionGroupBo bo) {
+        Map<String, Object> params = bo.getParams();
+        LambdaQueryWrapper<GameAthleteCompetitionGroup> lqw = Wrappers.lambdaQuery();
+        lqw.eq(bo.getGroupId() != null, GameAthleteCompetitionGroup::getGroupId, bo.getGroupId());
+        lqw.eq(bo.getEventId() != null, GameAthleteCompetitionGroup::getEventId, bo.getEventId());
+        lqw.eq(bo.getProjectId() != null, GameAthleteCompetitionGroup::getProjectId, bo.getProjectId());
+        lqw.eq(bo.getAthleteId() != null, GameAthleteCompetitionGroup::getAthleteId, bo.getAthleteId());
+        lqw.eq(bo.getTeamId() != null, GameAthleteCompetitionGroup::getTeamId, bo.getTeamId());
+        lqw.eq(bo.getGroupIndex() != null, GameAthleteCompetitionGroup::getGroupIndex, bo.getGroupIndex());
+        lqw.eq(bo.getTrackIndex() != null, GameAthleteCompetitionGroup::getTrackIndex, bo.getTrackIndex());
+        lqw.eq(StringUtils.isNotBlank(bo.getStatus()), GameAthleteCompetitionGroup::getStatus, bo.getStatus());
+        lqw.orderByAsc(GameAthleteCompetitionGroup::getGroupIndex)
+            .orderByAsc(GameAthleteCompetitionGroup::getTrackIndex);
+        return lqw;
+    }
+
+    /**
+     * 新增运动员比赛场次分组关联
+     *
+     * @param bo 运动员比赛场次分组关联
+     * @return 是否新增成功
+     */
+    @Override
+    public Boolean insertByBo(GameAthleteCompetitionGroupBo bo) {
+        GameAthleteCompetitionGroup add = MapstructUtils.convert(bo, GameAthleteCompetitionGroup.class);
+        validEntityBeforeSave(add);
+        boolean flag = baseMapper.insert(add) > 0;
+        if (flag) {
+            bo.setId(add.getId());
+        }
+        return flag;
+    }
+
+    /**
+     * 修改运动员比赛场次分组关联
+     *
+     * @param bo 运动员比赛场次分组关联
+     * @return 是否修改成功
+     */
+    @Override
+    public Boolean updateByBo(GameAthleteCompetitionGroupBo bo) {
+        GameAthleteCompetitionGroup update = MapstructUtils.convert(bo, GameAthleteCompetitionGroup.class);
+        validEntityBeforeSave(update);
+        return baseMapper.updateById(update) > 0;
+    }
+
+    /**
+     * 保存前的数据校验
+     */
+    private void validEntityBeforeSave(GameAthleteCompetitionGroup entity) {
+        //TODO 做一些数据校验,如唯一约束
+    }
+
+    /**
+     * 校验并批量删除运动员比赛场次分组关联信息
+     *
+     * @param ids     待删除的主键集合
+     * @param isValid 是否进行有效性校验
+     * @return 是否删除成功
+     */
+    @Override
+    public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
+        if (isValid) {
+            //TODO 做一些业务上的校验,判断是否需要校验
+        }
+        return baseMapper.deleteByIds(ids) > 0;
+    }
+
+    /**
+     * 根据分组ID查询分组结果
+     *
+     * @param groupId 分组ID
+     * @return 分组结果
+     */
+    @Override
+    public Map<String, Object> getGroupResultFromDB(Long groupId) {
+        Map<String, Object> result = new HashMap<>();
+
+        try {
+            // 查询数据库中的分组结果
+            GameAthleteCompetitionGroupBo queryBo = new GameAthleteCompetitionGroupBo();
+            queryBo.setGroupId(groupId);
+            List<GameAthleteCompetitionGroupVo> groupData = queryList(queryBo);
+
+            if (groupData.isEmpty()) {
+                result.put("success", false);
+                result.put("message", "数据库中没有找到分组数据");
+                return result;
+            }
+
+            // 构建分组结果
+            Map<String, Object> groupResult = new HashMap<>();
+            for (GameAthleteCompetitionGroupVo item : groupData) {
+                String key = item.getGroupIndex() + "-" + item.getTrackIndex();
+                Map<String, Object> athlete = new HashMap<>();
+                athlete.put("athleteId", item.getAthleteId());
+                athlete.put("athleteCode", item.getAthleteCode());
+                athlete.put("name", item.getAthleteName());
+                athlete.put("teamId", item.getTeamId());
+                athlete.put("teamName", item.getTeamName());
+                groupResult.put(key, athlete);
+            }
+
+            result.put("success", true);
+            result.put("groupResult", groupResult);
+            result.put("totalAthletes", groupData.size());
+
+        } catch (Exception e) {
+            log.error("从数据库查询分组结果失败", e);
+            result.put("success", false);
+            result.put("message", "查询分组结果失败: " + e.getMessage());
+        }
+
+        return result;
+    }
+
+    /**
+     * 保存分组结果到数据库
+     *
+     * @param groupId 分组ID
+     * @param groupResult 分组结果
+     * @param groupInfo 分组信息
+     * @return 是否保存成功
+     */
+    @Override
+    public Boolean saveGroupResult(Long groupId, Map<String, GameAthleteVo> groupResult, Object groupInfo) {
+        try {
+            // 先删除原有的分组数据
+            deleteGroupResultByGroupId(groupId);
+
+            // 保存新的分组数据
+            List<GameAthleteCompetitionGroupBo> boList = new ArrayList<>();
+
+            for (Map.Entry<String, GameAthleteVo> entry : groupResult.entrySet()) {
+                String key = entry.getKey();
+                GameAthleteVo value = entry.getValue();
+
+                String[] keyParts = key.split("-");
+                if (keyParts.length >= 2) {
+                    GameAthleteCompetitionGroupBo bo = new GameAthleteCompetitionGroupBo();
+                    bo.setGroupId(groupId);
+
+                    // 设置组别和道次
+                    bo.setGroupIndex(Long.valueOf(keyParts[0]));
+                    bo.setTrackIndex(Long.valueOf(keyParts[1]));
+
+                    // 设置运动员信息 - 直接从GameAthleteVo对象获取
+                    bo.setAthleteId(value.getAthleteId());
+                    bo.setAthleteCode(value.getAthleteCode());
+                    bo.setAthleteName(value.getName());
+                    bo.setTeamId(value.getTeamId());
+                    
+                    // 通过队伍ID查询队伍名称
+                    String teamName = "";
+                    if (value.getTeamId() != null) {
+                        try {
+                            GameTeamVo team = getGameTeamService().queryById(value.getTeamId());
+                            if (team != null) {
+                                teamName = team.getTeamName();
+                            }
+                        } catch (Exception e) {
+                            log.warn("查询队伍名称失败,队伍ID: {}, 错误: {}", value.getTeamId(), e.getMessage());
+                        }
+                    }
+                    bo.setTeamName(teamName != null ? teamName : "");
+
+                    // 设置其他信息
+                    if (groupInfo instanceof GameEventGroupVo) {
+                        GameEventGroupVo group = (GameEventGroupVo) groupInfo;
+                        bo.setEventId(group.getEventId());
+                        bo.setProjectId(group.getProjectId());
+                    }
+
+                    bo.setStatus("0"); // 正常状态
+                    boList.add(bo);
+                }
+            }
+
+                        // 批量插入
+            log.info("准备插入 {} 条分组数据", boList.size());
+            for (GameAthleteCompetitionGroupBo bo : boList) {
+                boolean insertResult = insertByBo(bo);
+                if (!insertResult) {
+                    log.error("插入分组数据失败: groupId={}, athleteId={}, groupIndex={}, trackIndex={}", 
+                             bo.getGroupId(), bo.getAthleteId(), bo.getGroupIndex(), bo.getTrackIndex());
+                }
+            }
+            
+            log.info("分组数据保存完成,共插入 {} 条记录", boList.size());
+            return true;
+
+        } catch (Exception e) {
+            log.error("保存分组结果到数据库失败", e);
+            return false;
+        }
+    }
+
+    /**
+     * 根据分组ID删除分组结果
+     *
+     * @param groupId 分组ID
+     * @return 是否删除成功
+     */
+    @Override
+    public Boolean deleteGroupResultByGroupId(Long groupId) {
+        LambdaQueryWrapper<GameAthleteCompetitionGroup> wrapper = Wrappers.lambdaQuery();
+        wrapper.eq(GameAthleteCompetitionGroup::getGroupId, groupId);
+        return baseMapper.delete(wrapper) > 0;
+    }
+}

+ 233 - 8
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/GameEventGroupServiceImpl.java

@@ -9,22 +9,30 @@ import org.dromara.common.mybatis.core.page.PageQuery;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
-import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.dromara.system.domain.bo.GameTeamBo;
 import org.dromara.system.domain.vo.GameEventProjectVo;
 import org.dromara.system.service.IGameEventProjectService;
+import org.dromara.system.service.IGameAthleteService;
+import org.dromara.system.service.IGameTeamService;
+import org.dromara.system.domain.vo.GameAthleteVo;
+import org.dromara.system.domain.vo.GameTeamVo;
 import org.springframework.stereotype.Service;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.InitializingBean;
 import org.dromara.system.domain.bo.GameEventGroupBo;
 import org.dromara.system.domain.constant.GameEventConstant;
 import org.dromara.system.domain.vo.GameEventGroupVo;
 import org.dromara.system.domain.GameEventGroup;
 import org.dromara.system.mapper.GameEventGroupMapper;
 import org.dromara.system.service.IGameEventGroupService;
+import org.dromara.system.service.IGameAthleteCompetitionGroupService;
+import jakarta.annotation.PostConstruct;
+import org.dromara.system.domain.bo.GameAthleteBo;
+import org.dromara.common.core.utils.SpringUtils;
 
-import java.util.List;
-import java.util.Map;
-import java.util.Collection;
-import java.util.Optional;
+import java.util.*;
 import java.util.stream.Collectors;
 
 /**
@@ -34,13 +42,49 @@ import java.util.stream.Collectors;
  * @date 2025-07-30
  */
 @Slf4j
-@RequiredArgsConstructor
 @Service
 public class GameEventGroupServiceImpl implements IGameEventGroupService {
 
-    private final GameEventGroupMapper baseMapper;
+    @Autowired
+    private GameEventGroupMapper baseMapper;
 
-    private final IGameEventProjectService gameEventProjectService;
+    @Autowired
+    private IGameEventProjectService gameEventProjectService;
+
+    private IGameAthleteService gameAthleteService;
+
+    private IGameTeamService gameTeamService;
+
+    private IGameAthleteCompetitionGroupService gameAthleteCompetitionGroupService;
+
+    @PostConstruct
+    public void init() {
+        // 延迟初始化依赖,避免循环依赖
+    }
+
+    // 获取运动员服务的辅助方法
+    private IGameAthleteService getGameAthleteService() {
+        if (gameAthleteService == null) {
+            gameAthleteService = SpringUtils.getBean(IGameAthleteService.class);
+        }
+        return gameAthleteService;
+    }
+
+    // 获取队伍服务的辅助方法
+    private IGameTeamService getGameTeamService() {
+        if (gameTeamService == null) {
+            gameTeamService = SpringUtils.getBean(IGameTeamService.class);
+        }
+        return gameTeamService;
+    }
+
+    // 获取运动员比赛分组服务的辅助方法
+    private IGameAthleteCompetitionGroupService getGameAthleteCompetitionGroupService() {
+        if (gameAthleteCompetitionGroupService == null) {
+            gameAthleteCompetitionGroupService = SpringUtils.getBean(IGameAthleteCompetitionGroupService.class);
+        }
+        return gameAthleteCompetitionGroupService;
+    }
 
     /**
      * 查询赛事分组
@@ -216,4 +260,185 @@ public class GameEventGroupServiceImpl implements IGameEventGroupService {
                 .eq(GameEventGroup::getEventId, defaultEventId)
         ).get(0);
     }
+
+    /**
+     * 从数据库获取分组结果
+     *
+     * @param groupId 分组ID
+     * @return 分组结果
+     */
+    @Override
+    public Map<String, Object> getGroupResultFromDB(Long groupId) {
+        return getGameAthleteCompetitionGroupService().getGroupResultFromDB(groupId);
+    }
+
+    /**
+     * 生成分组结果
+     *
+     * @param groupId 分组ID
+     * @return 分组结果
+     */
+    @Override
+    public Map<String, Object> generateGroups(Long groupId) {
+        Map<String, Object> result = new HashMap<>();
+
+        try {
+            // 获取分组信息
+            GameEventGroupVo groupInfo = queryById(groupId);
+            if (groupInfo == null) {
+                result.put("success", false);
+                result.put("message", "分组信息不存在");
+                return result;
+            }
+
+            // 获取运动员列表
+            GameAthleteBo athleteBo = new GameAthleteBo();
+            athleteBo.setEventId(groupInfo.getEventId());
+            List<GameAthleteVo> athletes = getGameAthleteService().queryList(athleteBo);
+
+            // 获取队伍列表
+            GameTeamBo teamBo = new GameTeamBo();
+            teamBo.setEventId(groupInfo.getEventId());
+            List<GameTeamVo> teams = getGameTeamService().queryList(teamBo);
+
+            // 获取项目信息
+            GameEventProjectVo project = null;
+            if (groupInfo.getProjectId() != null) {
+                project = gameEventProjectService.queryById(groupInfo.getProjectId());
+            }
+
+            // 筛选符合条件的运动员
+            List<GameAthleteVo> eligibleAthletes = athletes.stream()
+                .filter(athlete -> {
+                    // 检查是否参与该项目
+                    if (athlete.getProjectList() == null) {
+                        return false;
+                    }
+
+                    // 处理项目列表
+                    List<String> projectIds = new ArrayList<>();
+                    if (athlete.getProjectList() != null) {
+                        projectIds = ((List<?>) athlete.getProjectList()).stream()
+                            .map(Object::toString)
+                            .toList();
+                    }
+//                    else if (athlete.getProjectList() instanceof String) {
+//                        String projectListStr = (String) athlete.getProjectList();
+//                        try {
+//                            projectIds = JSONUtil.toList(projectListStr, String.class);
+//                        } catch (Exception e) {
+//                            // 如果不是JSON格式,可能是逗号分隔的字符串
+//                            projectIds = List.of(projectListStr.split(","));
+//                        }
+//                    }
+
+                    String targetProjectId = groupInfo.getProjectId() != null ?
+                        groupInfo.getProjectId().toString() : null;
+                    if (targetProjectId == null || !projectIds.contains(targetProjectId)) {
+                        return false;
+                    }
+
+                    // 检查性别是否匹配
+                    if (StringUtils.isNotBlank(groupInfo.getMemberGender()) &&
+                        !"0".equals(groupInfo.getMemberGender())) {
+                        if (!groupInfo.getMemberGender().equals(
+                            athlete.getGender() != null ? athlete.getGender().toString() : null)) {
+                            return false;
+                        }
+                    }
+
+                    return true;
+                })
+                .toList();
+
+            if (eligibleAthletes.isEmpty()) {
+                result.put("success", false);
+                result.put("message", "没有找到符合条件的运动员");
+                return result;
+            }
+
+            // 随机打乱运动员顺序
+            List<GameAthleteVo> shuffledAthletes = new ArrayList<>(eligibleAthletes);
+            Collections.shuffle(shuffledAthletes);
+
+            // 记录已分配的运动员ID,避免重复分配
+            Set<Long> assignedAthleteIds = new HashSet<>();
+
+            // 分组结果
+            Map<String, GameAthleteVo> groupResult = new HashMap<>();
+
+            // 按组别和道次分配运动员
+            for (int groupIndex = 1; groupIndex <= groupInfo.getIncludeGroupNum(); groupIndex++) {
+                final int currentGroupIndex = groupIndex; // 创建final变量用于lambda表达式
+                for (int track = 1; track <= groupInfo.getTrackNum(); track++) {
+                    // 寻找可用的运动员
+                    GameAthleteVo selectedAthlete = null;
+                    int athleteIndex = 0;
+
+                    while (athleteIndex < shuffledAthletes.size() && selectedAthlete == null) {
+                        GameAthleteVo candidateAthlete = shuffledAthletes.get(athleteIndex);
+
+                        // 检查运动员是否已经被分配
+                        if (assignedAthleteIds.contains(candidateAthlete.getAthleteId())) {
+                            athleteIndex++;
+                            continue;
+                        }
+
+                        // 检查同一组中是否已有同一队伍的运动员
+                        boolean hasSameTeamInGroup = groupResult.entrySet().stream()
+                            .anyMatch(entry -> {
+                                String[] keyParts = entry.getKey().split("-");
+                                if (keyParts.length >= 1) {
+                                    String existingGroup = keyParts[0];
+                                    return existingGroup.equals(String.valueOf(currentGroupIndex)) &&
+                                           entry.getValue().getTeamId().equals(candidateAthlete.getTeamId());
+                                }
+                                return false;
+                            });
+
+                        if (!hasSameTeamInGroup) {
+                            selectedAthlete = candidateAthlete;
+                            // 标记运动员为已分配
+                            assignedAthleteIds.add(candidateAthlete.getAthleteId());
+                        }
+
+                        athleteIndex++;
+                    }
+
+                    // 如果找到了合适的运动员,分配到当前组和道次
+                    if (selectedAthlete != null) {
+                        String key = currentGroupIndex + "-" + track;
+                        groupResult.put(key, selectedAthlete);
+                    }
+                }
+            }
+
+            // 保存分组结果到数据库
+            try {
+                boolean saveSuccess = getGameAthleteCompetitionGroupService().saveGroupResult(groupId, groupResult, groupInfo);
+                if (saveSuccess) {
+                    log.info("分组结果已保存到数据库,分组ID: {}", groupId);
+                } else {
+                    log.warn("分组结果保存到数据库失败,分组ID: {}", groupId);
+                }
+            } catch (Exception e) {
+                log.error("保存分组结果到数据库时发生错误", e);
+            }
+
+            // 构建返回结果
+            result.put("success", true);
+            result.put("groupResult", groupResult);
+            result.put("totalAthletes", eligibleAthletes.size());
+            result.put("groupInfo", groupInfo);
+            result.put("project", project);
+            result.put("teams", teams);
+
+        } catch (Exception e) {
+            log.error("生成分组失败", e);
+            result.put("success", false);
+            result.put("message", "生成分组失败: " + e.getMessage());
+        }
+
+        return result;
+    }
 }

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

@@ -1,10 +1,8 @@
 package org.dromara.system.service.impl;
 
 import cn.hutool.core.bean.BeanUtil;
-import com.baomidou.mybatisplus.core.toolkit.reflect.GenericTypeUtils;
 import org.dromara.common.core.utils.MapstructUtils;
 import org.dromara.common.core.utils.StringUtils;
-import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -13,8 +11,6 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.dromara.common.redis.utils.RedisUtils;
-import org.dromara.system.domain.GameAthlete;
-import org.dromara.system.domain.GameEventProject;
 import org.dromara.system.domain.constant.GameEventConstant;
 import org.dromara.system.domain.constant.ProjectClassification;
 import org.dromara.system.domain.constant.SortType;
@@ -34,7 +30,6 @@ import java.util.*;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 import jakarta.servlet.http.HttpServletResponse;
-import org.dromara.common.excel.utils.ExcelUtil;
 import org.dromara.system.domain.bo.GameEventProjectBo;
 import org.dromara.system.domain.bo.GameTeamBo;
 
@@ -62,6 +57,7 @@ public class GameScoreServiceImpl implements IGameScoreService {
     private final GameEventProjectMapper projectMapper;
     private final IGameEventProjectService gameEventProjectService;
 
+
     /**
      * 查询成绩
      *
@@ -178,7 +174,7 @@ public class GameScoreServiceImpl implements IGameScoreService {
      * 保存前的数据校验
      */
     private void validEntityBeforeSave(GameScore entity) {
-        //TODO 做一些数据校验,如唯一约束
+
     }
 
     /**
@@ -697,10 +693,10 @@ public class GameScoreServiceImpl implements IGameScoreService {
         for (int i = 0; i < sortedByPoints.size(); i++) {
             GameScoreVo score = sortedByPoints.get(i);
             int currentPoints = score.getScorePoint() != null ? score.getScorePoint() : 0;
-            
+
             // 如果不是第一个,检查是否与前一个积分相同
             if (i > 0) {
-                int previousPoints = sortedByPoints.get(i - 1).getScorePoint() != null ? 
+                int previousPoints = sortedByPoints.get(i - 1).getScorePoint() != null ?
                     sortedByPoints.get(i - 1).getScorePoint() : 0;
                 if (currentPoints != previousPoints) {
                     currentRank = i + 1;
@@ -779,7 +775,7 @@ public class GameScoreServiceImpl implements IGameScoreService {
             Long teamId = teamEntry.getKey();
             BigDecimal currentPerformance = teamEntry.getValue();
             int points = i < pointValues.size() ? pointValues.get(i) : 0;
-            
+
             // 如果不是第一个,检查是否与前一个成绩相同
             if (i > 0) {
                 BigDecimal previousPerformance = sortedTeamsByPerformance.get(i - 1).getValue();

+ 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 "无";
+    }
+}

+ 30 - 0
ruoyi-modules/ruoyi-game-event/src/main/resources/mapper/system/GameAthleteCompetitionGroupMapper.xml

@@ -0,0 +1,30 @@
+<?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.GameAthleteCompetitionGroupMapper">
+
+    <resultMap type="org.dromara.system.domain.GameAthleteCompetitionGroup" id="GameAthleteCompetitionGroupResult">
+        <result property="id" column="id"/>
+        <result property="groupId" column="group_id"/>
+        <result property="eventId" column="event_id"/>
+        <result property="projectId" column="project_id"/>
+        <result property="athleteId" column="athlete_id"/>
+        <result property="teamId" column="team_id"/>
+        <result property="groupIndex" column="group_index"/>
+        <result property="trackIndex" column="track_index"/>
+        <result property="athleteCode" column="athlete_code"/>
+        <result property="athleteName" column="athlete_name"/>
+        <result property="teamName" column="team_name"/>
+        <result property="status" column="status"/>
+        <result property="remark" column="remark"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateTime" column="update_time"/>
+        <result property="createBy" column="create_by"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="tenantId" column="tenant_id"/>
+        <result property="createDept" column="create_dept"/>
+    </resultMap>
+
+</mapper>

+ 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>