Browse Source

feat(app): 新增体验版赛事功能模块

- 新增 ExGameScheduleVo、ExperienceEnrollInfoVo、ExperienceEnrollSubmitBo、ExperienceMenuVo、ExperienceProjectVo 等数据传输对象
- 在 ExperienceVersionController 中新增菜单查询、线上报名信息获取与提交、竞赛日程查询等接口
- 扩展 GameAthlete 实体类,添加紧急联系人姓名和电话字段
- 在 GameEventProjectMapper 中实现赛事日程信息查询功能
- 在 GameUserMapper 中添加体验端赛事菜单查询方法
- 在 UserEventService 中实现体验端线上报名相关业务逻辑
- 优化微信登录流程,完善异常处理机制
zhou 2 days ago
parent
commit
8cc6c62798

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

@@ -8,15 +8,14 @@ import org.dromara.common.core.domain.R;
 import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.tenant.helper.TenantHelper;
 import org.dromara.system.domain.GameEvent;
-import org.dromara.system.domain.vo.app.ExperienceGameEventVo;
-import org.dromara.system.domain.vo.app.UserEventInfoVo;
-import org.dromara.system.domain.vo.app.ExperienceMyInfoVo;
-import org.dromara.system.domain.vo.app.ExperienceMyRecordVo;
-import org.dromara.system.domain.vo.app.UserLoginVo;
+import org.dromara.system.domain.vo.app.*;
+import org.dromara.system.domain.bo.ExperienceEnrollSubmitBo;
 import org.dromara.system.mapper.GameEventConfigMapper;
 import org.dromara.system.mapper.GameEventMapper;
+import org.dromara.system.mapper.GameEventProjectMapper;
 import org.dromara.system.service.ISysOssService;
 import org.dromara.system.service.app.IUserEventService;
+import org.dromara.system.mapper.app.GameUserMapper;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
@@ -36,8 +35,9 @@ public class ExperienceVersionController {
 
     private final GameEventMapper gameEventMapper;
     private final GameEventConfigMapper gameEventConfigMapper;
-    private final ISysOssService sysOssService;
     private final IUserEventService userEventService;
+    private final GameUserMapper gameUserMapper;
+    private final GameEventProjectMapper projectMapper;
 
     /**
      * 1、登录接口(微信小程序授权登录)
@@ -109,5 +109,41 @@ public class ExperienceVersionController {
     /**
      * 7、赛事菜单查询接口
      */
+    @GetMapping("/menu")
+    public R<List<ExperienceMenuVo>> getEventMenu(@RequestParam("eventId") Long eventId) {
+        if (eventId == null) {
+            return R.fail("赛事Id不能为空");
+        }
+        return R.ok(TenantHelper.ignore(() -> gameUserMapper.getExperienceEventMenu(eventId)));
+    }
+
+    /**
+     * 8、线上报名接口--信息获取
+     */
+    @GetMapping("/enrollInfo")
+    public R<ExperienceEnrollInfoVo> getEnrollInfo(@RequestParam("phone") String phone, @RequestParam("eventId") Long eventId) {
+        if (StringUtils.isAnyBlank(phone)) {
+            return R.fail("手机号不能为空");
+        }
+        if (eventId == null) {
+            return R.fail("赛事Id不能为空");
+        }
+        return R.ok(TenantHelper.ignore(() -> userEventService.getExperienceEnrollInfo(phone.trim(), eventId)));
+    }
+
+    /**
+     * 9、线上报名接口--信息提交
+     */
+    @PostMapping("/enrollSubmit")
+    public R<Boolean> enrollSubmit(@RequestBody @Validated ExperienceEnrollSubmitBo submitBo) {
+        return R.ok(TenantHelper.ignore(() -> userEventService.submitExperienceEnroll(submitBo)));
+    }
 
+    /**
+     * 10、竞赛日程信息获取接口
+     */
+    @GetMapping("/schedule")
+    public R<List<ExGameScheduleVo>> getSchedule(@RequestParam("eventId") Long eventId) {
+        return R.ok(TenantHelper.ignore(() -> projectMapper.selectSchedule(eventId)));
+    }
 }

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

@@ -118,6 +118,16 @@ public class GameAthlete extends TenantEntity {
     @TableLogic
     private String delFlag;
 
+    /**
+     * 紧急联系人姓名
+     */
+    private String emergencyContactName;
+
+    /**
+     * 紧急联系人电话
+     */
+    private String emergencyContactPhone;
+
     /**
      * 备注
      */

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

@@ -0,0 +1,79 @@
+package org.dromara.system.domain.bo;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 体验端线上报名提交参数BO
+ *
+ * @author system
+ */
+@Data
+public class ExperienceEnrollSubmitBo implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 赛事ID
+     */
+    @NotNull(message = "赛事ID不能为空")
+    private Long eventId;
+
+    /**
+     * 项目ID
+     */
+    @NotNull(message = "项目ID不能为空")
+    private Long projectId;
+    /**
+     * 类别(0:单人项目 1:团队项目)
+     */
+    @NotBlank(message = "项目类别不能为空")
+    private String classification;
+
+    /**
+     * 运动员ID
+     */
+    private Long athleteId;
+    /**
+     * 真实姓名
+     */
+    @NotBlank(message = "姓名不能为空")
+    private String name;
+
+    /**
+     * 证件号码
+     */
+    private String idCard;
+
+    /**
+     * 性别 (1-男  2-女)
+     */
+    @NotBlank(message = "性别不能为空")
+    private String gender;
+
+    /**
+     * 手机号码
+     */
+    @NotBlank(message = "手机号码不能为空")
+    private String phone;
+
+    /**
+     * 服装尺码
+     */
+    private String tshirtSize;
+
+    /**
+     * 紧急联系人姓名
+     */
+    private String emergencyContactName;
+
+    /**
+     * 紧急联系人电话
+     */
+    private String emergencyContactPhone;
+
+}

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

@@ -140,6 +140,16 @@ public class GameAthleteBo extends BaseEntity {
     // EditGroup.class })
     private String status;
 
+    /**
+     * 紧急联系人姓名
+     */
+    private String emergencyContactName;
+
+    /**
+     * 紧急联系人电话
+     */
+    private String emergencyContactPhone;
+
     /**
      * 备注
      */

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

@@ -160,6 +160,18 @@ public class GameAthleteVo implements Serializable {
     @ExcelDictFormat(dictType = "game_event_status")
     private String status;
 
+    /**
+     * 紧急联系人姓名
+     */
+    @ExcelProperty(value = "紧急联系人姓名")
+    private String emergencyContactName;
+
+    /**
+     * 紧急联系人电话
+     */
+    @ExcelProperty(value = "紧急联系人电话")
+    private String emergencyContactPhone;
+
     /**
      * 备注
      */

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

@@ -0,0 +1,32 @@
+package org.dromara.system.domain.vo.app;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+public class ExGameScheduleVo implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 日期 (yyyy-MM-dd)
+     */
+    private String date;
+
+    /**
+     * 时间 (HH:mm - HH:mm)
+     */
+    private String time;
+
+    /**
+     * 项目名称
+     */
+    private String projectName;
+
+    /**
+     * 比赛场地
+     */
+    private String location;
+}

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

@@ -0,0 +1,62 @@
+package org.dromara.system.domain.vo.app;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 体验端线上报名获取的全部信息VO
+ *
+ * @author system
+ */
+@Data
+public class ExperienceEnrollInfoVo implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 用户ID
+     */
+    private Long athleteId;
+    /**
+     * 姓名
+     */
+    private String name;
+
+    /**
+     * 证件号码
+     */
+    private String idCard;
+
+    /**
+     * 性别 (1-男  2-女)
+     */
+    private String gender;
+
+    /**
+     * 手机号码
+     */
+    private String phone;
+
+    /**
+     * 服装尺码
+     */
+    private String tshirtSize;
+
+    /**
+     * 紧急联系人姓名
+     */
+    private String emergencyContactName;
+
+    /**
+     * 紧急联系人电话
+     */
+    private String emergencyContactPhone;
+
+    /**
+     * 赛事项目列表
+     */
+    private List<ExperienceProjectVo> projects;
+}

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

@@ -0,0 +1,32 @@
+package org.dromara.system.domain.vo.app;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 体验版赛事关联菜单精简VO
+ *
+ * @author system
+ */
+@Data
+public class ExperienceMenuVo implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 菜单的导航名称
+     */
+    private String name;
+
+    /**
+     * 菜单的导航图标
+     */
+    private String pic;
+
+    /**
+     * 菜单的图标背景颜色
+     */
+    private String color;
+}

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

@@ -0,0 +1,32 @@
+package org.dromara.system.domain.vo.app;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 体验端线上报名项目VO
+ *
+ * @author system
+ */
+@Data
+public class ExperienceProjectVo implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 项目ID
+     */
+    private Long projectId;
+
+    /**
+     * 项目名称
+     */
+    private String projectName;
+
+    /**
+     * 类别(0:单人项目 1:团队项目)
+     */
+    private String classification;
+}

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

@@ -10,6 +10,12 @@ import org.dromara.system.domain.GameEventProject;
 import org.dromara.system.domain.bo.GameEventProjectBo;
 import org.dromara.system.domain.vo.GameEventProjectVo;
 import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.system.domain.vo.app.ExGameScheduleVo;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
 
 import java.util.List;
 
@@ -21,43 +27,100 @@ import java.util.List;
  */
 @Mapper
 public interface GameEventProjectMapper extends BaseMapperPlus<GameEventProject, GameEventProjectVo> {
-    /**
-     * 分页查询项目列表,并进行数据权限控制
-     *
-     * @param page         分页参数
-     * @param queryWrapper 查询条件
-     * @return 分页的赛事信息
-     */
-    @DataPermission({
-            @DataColumn(key = "deptName", value = "create_dept"),
-            @DataColumn(key = "userName", value = "create_by")
-    })
-    default Page<GameEventProjectVo> selectPageEventProjectList(Page<GameEventProject> page,
-            Wrapper<GameEventProject> queryWrapper) {
-        return this.selectVoPage(page, queryWrapper);
-    }
-
-    /**
-     * 获取项目列表,并进行数据权限控制
-     *
-     * @param queryWrapper 筛选条件
-     * @return 项目列表
-     */
-    @DataPermission({
-            @DataColumn(key = "deptName", value = "create_dept"),
-            @DataColumn(key = "userName", value = "create_by")
-    })
-    default List<GameEventProjectVo> selectEventProjectList(Wrapper<GameEventProject> queryWrapper) {
-        return this.selectVoList(queryWrapper);
-    }
-
-    /**
-     * 分页查询项目列表并包含实时统计(XML 实现)
-     */
-    @DataPermission({
-            @DataColumn(key = "deptName", value = "create_dept"),
-            @DataColumn(key = "userName", value = "create_by")
-    })
-    Page<GameEventProjectVo> selectPageWithStats(@Param("page") Page<GameEventProjectVo> page,
-            @Param("query") GameEventProjectBo bo);
+        /**
+         * 分页查询项目列表,并进行数据权限控制
+         *
+         * @param page         分页参数
+         * @param queryWrapper 查询条件
+         * @return 分页的赛事信息
+         */
+        @DataPermission({
+                        @DataColumn(key = "deptName", value = "create_dept"),
+                        @DataColumn(key = "userName", value = "create_by")
+        })
+        default Page<GameEventProjectVo> selectPageEventProjectList(Page<GameEventProject> page,
+                        Wrapper<GameEventProject> queryWrapper) {
+                return this.selectVoPage(page, queryWrapper);
+        }
+
+        /**
+         * 获取项目列表,并进行数据权限控制
+         *
+         * @param queryWrapper 筛选条件
+         * @return 项目列表
+         */
+        @DataPermission({
+                        @DataColumn(key = "deptName", value = "create_dept"),
+                        @DataColumn(key = "userName", value = "create_by")
+        })
+        default List<GameEventProjectVo> selectEventProjectList(Wrapper<GameEventProject> queryWrapper) {
+                return this.selectVoList(queryWrapper);
+        }
+
+        /**
+         * 分页查询项目列表并包含实时统计(XML 实现)
+         */
+        @DataPermission({
+                        @DataColumn(key = "deptName", value = "create_dept"),
+                        @DataColumn(key = "userName", value = "create_by")
+        })
+        Page<GameEventProjectVo> selectPageWithStats(@Param("page") Page<GameEventProjectVo> page,
+                        @Param("query") GameEventProjectBo bo);
+
+        /**
+         * 查询赛事日程信息(按时间排序)
+         */
+        default List<ExGameScheduleVo> selectSchedule(@Param("eventId") Long eventId) {
+            List<GameEventProject> projects = this.selectList(
+                Wrappers.lambdaQuery(GameEventProject.class)
+                    .eq(GameEventProject::getEventId, eventId)
+                    .orderByAsc(GameEventProject::getStartTime)
+                    .select(GameEventProject::getStartTime, GameEventProject::getEndTime, GameEventProject::getProjectName, GameEventProject::getLocation)
+            );
+
+            List<ExGameScheduleVo> scheduleList = new ArrayList<>();
+            SimpleDateFormat dateFmt = new SimpleDateFormat("yyyy-MM-dd");
+            SimpleDateFormat timeFmt = new SimpleDateFormat("HH:mm");
+            for (GameEventProject project : projects) {
+                ExGameScheduleVo vo = new ExGameScheduleVo();
+                // 格式化日期 (yyyy-MM-dd)
+                Date startTime = project.getStartTime();
+                Date endTime = project.getEndTime();
+                String dateStr = "";
+                if (startTime != null) {
+                        dateStr = dateFmt.format(startTime);
+                }
+                vo.setDate(dateStr);
+                // 格式化时间 (如果跨天,显示 (dd) 后缀)
+                String timeStr = "00:00 - 00:00";
+                if (startTime != null && endTime != null) {
+                    String startFmt = timeFmt.format(startTime);
+                    String endFmt = timeFmt.format(endTime);
+
+                    Calendar startCal = Calendar.getInstance();
+                    startCal.setTime(startTime);
+                    Calendar endCal = Calendar.getInstance();
+                    endCal.setTime(endTime);
+
+                    // 跨天判断:年、月、日中有任意一个不同即为跨天
+                    if (startCal.get(Calendar.YEAR) != endCal.get(Calendar.YEAR) ||
+                                    startCal.get(Calendar.MONTH) != endCal.get(Calendar.MONTH) ||
+                                    startCal.get(Calendar.DAY_OF_MONTH) != endCal.get(Calendar.DAY_OF_MONTH)) {
+                        int endDay = endCal.get(Calendar.DAY_OF_MONTH);
+                        timeStr = startFmt + " - " + endFmt + " (" + String.format("%02d", endDay) + ")";
+                    } else {
+                        timeStr = startFmt + " - " + endFmt;
+                    }
+                } else if (startTime != null) {
+                    timeStr = timeFmt.format(startTime) + " - 00:00";
+                }
+                vo.setTime(timeStr);
+                // 项目名称
+                vo.setProjectName(project.getProjectName());
+                // 比赛场地
+                vo.setLocation(project.getLocation());
+                scheduleList.add(vo);
+            }
+            return scheduleList;
+        }
 }

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

@@ -6,6 +6,7 @@ import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
 import org.dromara.system.domain.app.GameUser;
 import org.dromara.system.domain.vo.app.ExperienceMyInfoVo;
 import org.dromara.system.domain.vo.app.ExperienceMyRecordVo;
+import org.dromara.system.domain.vo.app.ExperienceMenuVo;
 import org.dromara.system.domain.vo.app.GameUserVo;
 import java.util.List;
 
@@ -91,8 +92,25 @@ public interface GameUserMapper extends BaseMapperPlus<GameUser, GameUserVo> {
             "  gep.project_id, " +
             "  REPLACE(REPLACE(REPLACE(REPLACE(IFNULL(ga.project_value, ''), '[', ''), ']', ''), '\"', ''), ' ', '') " +
             ") > 0 AND gep.del_flag = '0' " +
-            "LEFT JOIN game_score gs ON ga.athlete_id = gs.athlete_id AND gep.project_id = gs.project_id AND gs.del_flag = '0' " +
+            "LEFT JOIN game_score gs ON ga.athlete_id = gs.athlete_id AND gep.project_id = gs.project_id AND gs.del_flag = '0' "
+            +
             "WHERE ga.phone = #{phone} AND ga.del_flag = '0' " +
             "ORDER BY gep.start_time DESC")
     List<ExperienceMyRecordVo> getExperienceMyRecord(String phone);
+
+    // 体验端获取赛事菜单列表
+    @Select("SELECT " +
+            "  gn.name, " +
+            "  gn.pic, " +
+            "  gn.color " +
+            "FROM event_menu em " +
+            "INNER JOIN common_navigator gn ON FIND_IN_SET( " +
+            "  gn.nav_id, " +
+            "  REPLACE(REPLACE(REPLACE(REPLACE(IFNULL(em.menu_list, ''), '[', ''), ']', ''), '\"', ''), ' ', '') " +
+            ") > 0 " +
+            "WHERE em.event_id = #{eventId} " +
+            "  AND gn.status = 0 " +
+            "  AND gn.del_flag = '0' " +
+            "ORDER BY gn.sort_num ASC")
+    List<ExperienceMenuVo> getExperienceEventMenu(Long eventId);
 }

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

@@ -2,6 +2,8 @@ package org.dromara.system.service.app;
 
 import org.dromara.system.domain.vo.app.ExperienceMyInfoVo;
 import org.dromara.system.domain.vo.app.ExperienceMyRecordVo;
+import org.dromara.system.domain.vo.app.ExperienceEnrollInfoVo;
+import org.dromara.system.domain.bo.ExperienceEnrollSubmitBo;
 import org.dromara.system.domain.vo.app.UserEventInfoVo;
 import org.dromara.system.domain.vo.app.UserLoginVo;
 
@@ -19,4 +21,10 @@ public interface IUserEventService {
 
     // 体验端获取“我的”参赛记录
     List<ExperienceMyRecordVo> getExperienceMyRecord(String phone);
+
+    // 体验端线上报名--信息获取
+    ExperienceEnrollInfoVo getExperienceEnrollInfo(String phone, Long eventId);
+
+    // 体验端线上报名--信息提交
+    Boolean submitExperienceEnroll(ExperienceEnrollSubmitBo submitBo);
 }

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

@@ -2,6 +2,7 @@ package org.dromara.system.service.impl.app;
 
 import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.utils.MapstructUtils;
 import org.dromara.common.core.utils.StringUtils;
 import org.dromara.system.domain.GameAthlete;
@@ -17,16 +18,20 @@ import org.dromara.system.domain.vo.app.UserEventInfoVo;
 import org.dromara.system.domain.vo.app.UserLoginVo;
 import org.dromara.system.domain.vo.app.ExperienceMyInfoVo;
 import org.dromara.system.domain.vo.app.ExperienceMyRecordVo;
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 
 import java.math.BigDecimal;
-import java.util.stream.Collectors;
+import org.dromara.system.domain.vo.app.ExperienceEnrollInfoVo;
+import org.dromara.system.domain.vo.app.ExperienceProjectVo;
+import org.dromara.system.domain.bo.ExperienceEnrollSubmitBo;
+import cn.hutool.json.JSONUtil;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 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.transaction.annotation.Transactional;
 import org.springframework.web.reactive.function.client.WebClient;
 import reactor.core.publisher.Mono;
 
@@ -68,7 +73,7 @@ public class UserEventServiceImpl implements IUserEventService {
         try {
             // 步骤1:通过code获取微信openid和session_key
             WxLoginResult wxResult = getWxLoginResult(loginVo.getCode());
-            if (wxResult == null || wxResult.getOpenid() == null || wxResult.getOpenid().isEmpty()) {
+            if (wxResult.getOpenid() == null || wxResult.getOpenid().isEmpty()) {
                 throw new RuntimeException("微信登录失败,无法获取用户信息");
             }
 
@@ -93,49 +98,42 @@ public class UserEventServiceImpl implements IUserEventService {
      * 通过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("微信接口返回空结果");
-            }
+        if (appId == null || appId.isEmpty() || secret == null || secret.isEmpty()) {
+            throw new RuntimeException("微信小程序配置未设置");
+        }
 
-            // 手动解析JSON响应
-            WxLoginResult result = parseWxResponse(responseBody);
+        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("微信接口返回空结果");
+        }
 
-            if (result.getErrcode() != null && result.getErrcode() != 0) {
-                throw new RuntimeException("微信接口返回错误:" + result.getErrmsg() + " (错误码: " + result.getErrcode() + ")");
-            }
+        // 手动解析JSON响应
+        WxLoginResult result = parseWxResponse(responseBody);
 
-            if (result.getOpenid() == null || result.getOpenid().isEmpty()) {
-                throw new RuntimeException("微信接口未返回openid");
-            }
+        if (result.getErrcode() != null && result.getErrcode() != 0) {
+            throw new RuntimeException("微信接口返回错误:" + result.getErrmsg() + " (错误码: " + result.getErrcode() + ")");
+        }
 
-            return result;
-        } catch (Exception e) {
-            if (e instanceof RuntimeException) {
-                throw e;
-            }
-            throw new RuntimeException("调用微信接口失败:" + e.getMessage(), e);
+        if (result.getOpenid() == null || result.getOpenid().isEmpty()) {
+            throw new RuntimeException("微信接口未返回openid");
         }
+
+        return result;
     }
 
     /**
@@ -144,8 +142,7 @@ public class UserEventServiceImpl implements IUserEventService {
     private WxLoginResult parseWxResponse(String responseBody) {
         try {
             // 使用Jackson解析JSON
-            WxLoginResult result = objectMapper.readValue(responseBody, WxLoginResult.class);
-            return result;
+            return objectMapper.readValue(responseBody, WxLoginResult.class);
         } catch (Exception e) {
             throw new RuntimeException("解析微信响应失败:" + e.getMessage() + ", 响应内容: " + responseBody);
         }
@@ -362,4 +359,86 @@ public class UserEventServiceImpl implements IUserEventService {
             return rawScore.stripTrailingZeros().toPlainString();
         }
     }
+
+    @Override
+    public ExperienceEnrollInfoVo getExperienceEnrollInfo(String phone, Long eventId) {
+        ExperienceEnrollInfoVo infoVo = new ExperienceEnrollInfoVo();
+        // 1. 获取赛事下的项目列表
+        List<GameEventProject> projects = gameEventProjectMapper.selectList(
+                Wrappers.lambdaQuery(GameEventProject.class)
+                    .select(GameEventProject::getProjectId, GameEventProject::getProjectName, GameEventProject::getClassification)
+                        .eq(GameEventProject::getEventId, eventId)
+                        .eq(GameEventProject::getDelFlag, "0")
+                        .orderByAsc(GameEventProject::getProjectId)
+        );
+        List<ExperienceProjectVo> projectVos = new ArrayList<>();
+        if (CollectionUtils.isNotEmpty(projects)) {
+            for (GameEventProject project : projects) {
+                ExperienceProjectVo pVo = new ExperienceProjectVo();
+                pVo.setProjectId(project.getProjectId());
+                pVo.setProjectName(project.getProjectName());
+                pVo.setClassification(project.getClassification());
+                projectVos.add(pVo);
+            }
+        }
+        infoVo.setProjects(projectVos);
+
+        // 2. 根据手机号和赛事ID查询是否已经有运动员报名信息
+        GameAthlete athlete = gameAthleteMapper.selectOne(
+                Wrappers.lambdaQuery(GameAthlete.class)
+                    .select(GameAthlete::getAthleteId, GameAthlete::getName, GameAthlete::getPhone,
+                        GameAthlete::getIdCard, GameAthlete::getGender, GameAthlete::getTshirtSize,
+                        GameAthlete::getEmergencyContactName, GameAthlete::getEmergencyContactPhone)
+                        .eq(GameAthlete::getPhone, phone.trim())
+                        .eq(GameAthlete::getEventId, eventId)
+                        .eq(GameAthlete::getDelFlag, "0")
+        );
+
+        if (athlete != null) {
+            infoVo.setAthleteId(athlete.getAthleteId());
+            infoVo.setName(athlete.getName());
+            infoVo.setGender(athlete.getGender());
+            infoVo.setIdCard(athlete.getIdCard());
+            infoVo.setTshirtSize(athlete.getTshirtSize());
+            infoVo.setEmergencyContactName(athlete.getEmergencyContactName());
+            infoVo.setEmergencyContactPhone(athlete.getEmergencyContactPhone());
+        }
+
+        return infoVo;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean submitExperienceEnroll(ExperienceEnrollSubmitBo submitBo) {
+        // 1. 验证项目是否存在于该赛事
+        boolean project = gameEventProjectMapper.exists(
+                Wrappers.lambdaQuery(GameEventProject.class)
+                        .eq(GameEventProject::getProjectId, submitBo.getProjectId())
+                        .eq(GameEventProject::getEventId, submitBo.getEventId())
+                        .eq(GameEventProject::getDelFlag, "0")
+        );
+        if (!project) {
+            throw new ServiceException("所选项目在当前赛事中不存在或已被删除");
+        }
+        GameAthlete athlete = gameAthleteMapper.selectOne(Wrappers.lambdaQuery(GameAthlete.class)
+            .eq(GameAthlete::getAthleteId, submitBo.getAthleteId())
+            .eq(GameAthlete::getDelFlag, "0")
+            .select(GameAthlete::getProjectValue)
+        );
+        if (athlete == null){
+            throw new ServiceException("运动员信息不存在或已被删除");
+        }
+        List<Long> list = JSONUtil.toList(athlete.getProjectValue(), Long.class);
+        if (list == null) {
+            list = new ArrayList<>();
+        }
+        if (list.contains(submitBo.getProjectId())){
+            return true;
+        }
+        list.add(submitBo.getProjectId());
+        athlete.setProjectValue(JSONUtil.toJsonStr(list));
+        return gameAthleteMapper.update(athlete, Wrappers.lambdaUpdate(GameAthlete.class)
+            .eq(GameAthlete::getAthleteId, submitBo.getAthleteId())
+        ) > 0;
+    }
 }