bug
Codex Assistant
2025-11-05 3714621173c606c4c58439ed8941100ce9ddea14
bug
29个文件已修改
1364 ■■■■ 已修改文件
backend/src/main/java/com/rongyichuang/activity/repository/ActivityRepository.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/activity/service/ActivityService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/auth/util/JwtUtil.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/config/GraphQLConfig.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/config/SecurityConfig.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/player/api/PlayerGraphqlApi.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/player/service/ActivityPlayerDetailService.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/player/service/PromotionService.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/resources/graphql/player.graphqls 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web/src/api/media.js 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web/src/api/promotion.js 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web/src/utils/appConfig.js 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web/src/utils/cos-simple.ts 192 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web/src/utils/cos.ts 484 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web/src/views/check-detail.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web/src/views/next-list.vue 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web/vite.config.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
wx/app.js 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
wx/app.json 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
wx/pages/index/index.js 179 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
wx/pages/index/index.json 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
wx/pages/index/index.wxml 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
wx/pages/index/index.wxss 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
wx/pages/judge/review.js 124 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
wx/pages/judge/review.wxml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
wx/pages/judge/review.wxss 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
wx/pages/message/message.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
wx/pages/profile/profile.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
wx/pages/registration/registration.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/activity/repository/ActivityRepository.java
@@ -19,8 +19,14 @@
    Page<Activity> findByPidAndStateOrderByCreateTimeDesc(Long pid, int state, Pageable pageable);
    Page<Activity> findByPidAndStateNotOrderByCreateTimeDesc(Long pid, int state, Pageable pageable);
    Page<Activity> findByPidAndStateNotAndNameContainingOrderByCreateTimeDesc(Long pid, int state, String name, Pageable pageable);
    List<Activity> findByPidAndStateOrderByCreateTimeAsc(Long pid, int state);
    List<Activity> findByPidAndStateOrderByCreateTimeDesc(Long pid, int state);
    List<Activity> findByStateOrderByPidAscNameAsc(int state);
    @Query("SELECT a FROM Activity a WHERE a.pid = 0 ORDER BY a.createTime DESC")
backend/src/main/java/com/rongyichuang/activity/service/ActivityService.java
@@ -72,10 +72,11 @@
                page = activityRepository.findByPidAndStateOrderByCreateTimeDesc(0L, state, pageable);
            }
        } else if (hasName) {
            page = activityRepository.findByPidAndNameContainingOrderByCreateTimeDesc(0L, name, pageable);
            // 当state为null但有名称搜索时,需要过滤掉已删除的比赛(state != 0)
            page = activityRepository.findByPidAndStateNotAndNameContainingOrderByCreateTimeDesc(0L, 0, name, pageable);
        } else {
            // 查询所有主活动(pid = 0)
            page = activityRepository.findByPidOrderByCreateTimeDesc(0L, pageable);
            // 当state为null时,查询所有未删除的主活动(pid = 0 且 state != 0)
            page = activityRepository.findByPidAndStateNotOrderByCreateTimeDesc(0L, 0, pageable);
        }
        List<ActivityResponse> content = page.getContent().stream()
backend/src/main/java/com/rongyichuang/auth/util/JwtUtil.java
@@ -8,6 +8,9 @@
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Date;
/**
@@ -38,7 +41,7 @@
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + jwtExpiration);
        SecretKey key = Keys.hmacShaKeyFor(jwtSecret.getBytes());
        SecretKey key = getSigningKey();
        JwtBuilder builder = Jwts.builder()
                .setSubject(userId.toString())
@@ -56,6 +59,29 @@
        }
        return builder.signWith(key, SignatureAlgorithm.HS256).compact();
    }
    /**
     * 根据配置的密钥生成满足 HMAC-SHA 要求的签名密钥:
     * - 若明文密钥长度不足 256 bit,使用 SHA-256 衍生为 256-bit
     * - 保持对现有 app.jwt.secret 的兼容,不修改配置键名或其它逻辑
     */
    private SecretKey getSigningKey() {
        try {
            byte[] keyBytes = jwtSecret.getBytes(StandardCharsets.UTF_8);
            if (keyBytes.length < 32) {
                MessageDigest digest = MessageDigest.getInstance("SHA-256");
                keyBytes = digest.digest(keyBytes);
            }
            if (keyBytes.length < 32) {
                byte[] padded = new byte[32];
                System.arraycopy(keyBytes, 0, padded, 0, Math.min(keyBytes.length, 32));
                keyBytes = padded;
            }
            return new SecretKeySpec(keyBytes, "HmacSHA256");
        } catch (Exception e) {
            throw new RuntimeException("初始化JWT签名密钥失败", e);
        }
    }
    /**
@@ -111,7 +137,7 @@
     * 从token中解析Claims
     */
    private Claims getClaimsFromToken(String token) {
        SecretKey key = Keys.hmacShaKeyFor(jwtSecret.getBytes());
        SecretKey key = getSigningKey();
        return Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
backend/src/main/java/com/rongyichuang/config/GraphQLConfig.java
@@ -12,7 +12,6 @@
    @Bean
    public RuntimeWiringConfigurer runtimeWiringConfigurer() {
        return wiringBuilder -> wiringBuilder
                .scalar(ExtendedScalars.GraphQLLong)
                .scalar(longScalar());
    }
backend/src/main/java/com/rongyichuang/config/SecurityConfig.java
@@ -49,6 +49,7 @@
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/**", "/actuator/**", "/test/**", "/cleanup/**").permitAll()
                .requestMatchers("/api/health/**").permitAll() // 允许健康检查端点访问
                .requestMatchers("/upload/**").permitAll()
                .requestMatchers("/graphiql/**", "/graphql/**", "/api/graphql/**", "/api/graphiql/**").permitAll() // 允许GraphQL和GraphiQL访问
                .requestMatchers("/**/graphql", "/**/graphiql").permitAll() // 更宽泛的GraphQL路径匹配
backend/src/main/java/com/rongyichuang/player/api/PlayerGraphqlApi.java
@@ -15,6 +15,7 @@
import com.rongyichuang.player.dto.response.PlayerRegistrationResponse;
import com.rongyichuang.player.dto.response.StageJudgeRatingDetailResponse;
import com.rongyichuang.player.dto.PromotionCompetitionResponse;
import com.rongyichuang.player.dto.response.PromotionCompetitionPageResponse;
import com.rongyichuang.player.dto.CompetitionParticipantResponse;
import com.rongyichuang.player.dto.PromotionInput;
import com.rongyichuang.player.dto.PromotionResult;
@@ -285,7 +286,7 @@
     * 获取比赛晋级列表
     */
    @QueryMapping
    public List<PromotionCompetitionResponse> promotionCompetitions(
    public PromotionCompetitionPageResponse promotionCompetitions(
            @Argument String name,
            @Argument Integer page,
            @Argument Integer size) {
backend/src/main/java/com/rongyichuang/player/service/ActivityPlayerDetailService.java
@@ -158,9 +158,11 @@
        }
        
        Object birthdayObj = row.get("birthday");
        playerInfo.setBirthday(birthdayObj != null ?
        playerInfo.setBirthday(birthdayObj != null ?
            (birthdayObj instanceof java.sql.Date ? ((java.sql.Date) birthdayObj).toString() : birthdayObj.toString()) : null);
        playerInfo.setEducation(row.get("education") != null ? row.get("education").toString() : "");
        Object educationObj = row.get("education");
        log.info("调试:从数据库查询到的education值: {}", educationObj);
        playerInfo.setEducation(educationObj != null ? educationObj.toString() : "");
        playerInfo.setIntroduction(row.get("introduction") != null ? row.get("introduction").toString() : "");
        // 构建区域信息
backend/src/main/java/com/rongyichuang/player/service/PromotionService.java
@@ -8,6 +8,7 @@
import com.rongyichuang.common.repository.MediaRepository;
import com.rongyichuang.message.service.MessageService;
import com.rongyichuang.player.dto.*;
import com.rongyichuang.player.dto.response.PromotionCompetitionPageResponse;
import com.rongyichuang.player.entity.ActivityPlayer;
import com.rongyichuang.player.repository.ActivityPlayerRepository;
import org.springframework.beans.factory.annotation.Autowired;
@@ -45,11 +46,11 @@
    /**
     * 获取比赛晋级列表
     */
    public List<PromotionCompetitionResponse> getPromotionCompetitions(String name, Integer page, Integer size) {
        List<PromotionCompetitionResponse> result = new ArrayList<>();
    public PromotionCompetitionPageResponse getPromotionCompetitions(String name, Integer page, Integer size) {
        List<PromotionCompetitionResponse> allResults = new ArrayList<>();
        
        // 查询所有有效的主比赛(pid = 0)
        List<Activity> competitions = activityRepository.findByPidAndStateOrderByCreateTimeAsc(0L, 1);
        List<Activity> competitions = activityRepository.findByPidAndStateOrderByCreateTimeDesc(0L, 1);
        
        // 如果有名称过滤条件,进行过滤
        if (name != null && !name.trim().isEmpty()) {
@@ -60,7 +61,7 @@
        
        // 为每个比赛查询其阶段
        for (Activity competition : competitions) {
            List<Activity> stages = activityRepository.findByPidAndStateOrderByCreateTimeAsc(competition.getId(), 1);
            List<Activity> stages = activityRepository.findByPidAndStateOrderByCreateTimeDesc(competition.getId(), 1);
            
            for (Activity stage : stages) {
                // 统计当前阶段的参赛人数
@@ -68,22 +69,27 @@
                Integer currentCount = playerCountLong != null ? playerCountLong.intValue() : 0;
                
                PromotionCompetitionResponse response = new PromotionCompetitionResponse(competition, stage, currentCount);
                result.add(response);
                allResults.add(response);
            }
        }
        
        // 简单分页处理(实际项目中建议使用数据库分页)
        // 计算总数
        long totalElements = allResults.size();
        // 分页处理
        List<PromotionCompetitionResponse> pagedResults = allResults;
        if (page != null && size != null && page > 0 && size > 0) {
            int start = (page - 1) * size;
            int end = Math.min(start + size, result.size());
            if (start < result.size()) {
                result = result.subList(start, end);
            int end = Math.min(start + size, allResults.size());
            if (start < allResults.size()) {
                pagedResults = allResults.subList(start, end);
            } else {
                result = new ArrayList<>();
                pagedResults = new ArrayList<>();
            }
        }
        
        return result;
        // 返回分页响应对象
        return new PromotionCompetitionPageResponse(pagedResults, totalElements, page, size);
    }
    
    /**
backend/src/main/resources/graphql/player.graphqls
@@ -24,7 +24,7 @@
    # 微信端获取选手报名状态
    getPlayerRegistrationState(activityId: ID!): PlayerRegistrationResponse
    # 获取比赛晋级列表
    promotionCompetitions(name: String, page: Int, size: Int): [PromotionCompetitionResponse!]!
    promotionCompetitions(name: String, page: Int, size: Int): PromotionCompetitionPageResponse!
    # 获取可晋级参赛者列表
    promotableParticipants(currentStageId: ID!): PromotableParticipantsResponse
}
@@ -310,6 +310,15 @@
    state: Int
}
# 比赛晋级列表分页响应类型
type PromotionCompetitionPageResponse {
    content: [PromotionCompetitionResponse!]!
    totalElements: Long!
    page: Int!
    size: Int!
    totalPages: Int!
}
# 可晋级参赛者列表响应类型
type PromotableParticipantsResponse {
    participants: [PromotableParticipantResponse!]!
web/src/api/media.js
@@ -1,5 +1,6 @@
// 媒体查询 API
import { graphqlRequest, API_CONFIG } from '../config/api.ts';
import { serverUrl } from '../utils/appConfig.js';
const GRAPHQL_ENDPOINT = API_CONFIG.GRAPHQL_ENDPOINT;
@@ -80,7 +81,7 @@
  
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch('http://localhost:8080/api/upload/image', {
      const response = await fetch(`${serverUrl}/api/upload/image`, {
        method: 'POST',
        headers: headers,
        body: formData,
web/src/api/promotion.js
@@ -4,17 +4,23 @@
const GET_PROMOTION_COMPETITIONS = `
  query GetPromotionCompetitions($name: String, $page: Int, $size: Int) {
    promotionCompetitions(name: $name, page: $page, size: $size) {
      id
      competitionId
      competitionName
      stageName
      maxParticipants
      currentCount
      status
      startTime
      endTime
      sortOrder
      state
      content {
        id
        competitionId
        competitionName
        stageName
        maxParticipants
        currentCount
        status
        startTime
        endTime
        sortOrder
        state
      }
      totalElements
      page
      size
      totalPages
    }
  }
`
web/src/utils/appConfig.js
@@ -1,25 +1,54 @@
import { graphqlRequest } from '../config/api.ts';
const GET_APP_CONFIG = `
  query AppConfig {
    appConfig {
      mediaBaseUrl
    }
  }
`;
export async function loadAppConfig() {
  try {
    const result = await graphqlRequest(GET_APP_CONFIG);
    const mediaBaseUrl = result.data?.appConfig?.mediaBaseUrl || '';
    // 作为全局变量暴露
    window.__APP_MEDIA_BASE_URL__ = mediaBaseUrl;
    return mediaBaseUrl;
  } catch (e) {
    // 如果GraphQL查询失败,使用默认配置
    console.warn('loadAppConfig failed, using default config:', e?.message || e);
    const defaultMediaBaseUrl = 'http://localhost:8080';
    window.__APP_MEDIA_BASE_URL__ = defaultMediaBaseUrl;
    return defaultMediaBaseUrl;
  }
import { graphqlRequest } from '../config/api.ts';
export const serverUrl =
  (typeof window !== 'undefined' && window.__APP_SERVER_URL__) ||
  (typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.VITE_SERVER_URL) ||
  'http://139.155.104.10:8080';
const GET_APP_CONFIG = `
  query AppConfig {
    appConfig {
      mediaBaseUrl
    }
  }
`;
export async function loadAppConfig() {
  try {
    const result = await graphqlRequest(GET_APP_CONFIG);
    const mediaBaseUrl = result.data?.appConfig?.mediaBaseUrl || '';
    // 作为全局变量暴露
    window.__APP_MEDIA_BASE_URL__ = mediaBaseUrl;
    return mediaBaseUrl;
  } catch (e) {
    // 如果GraphQL查询失败,使用默认配置
    console.warn('loadAppConfig failed, using default config:', e?.message || e);
    const defaultMediaBaseUrl = serverUrl;
    window.__APP_MEDIA_BASE_URL__ = defaultMediaBaseUrl;
    return defaultMediaBaseUrl;
  }
}
web/src/utils/cos-simple.ts
@@ -1,65 +1,129 @@
import axios from 'axios'
// GraphQL查询获取上传凭证
const GET_UPLOAD_CREDENTIALS = `
  query GetUploadCredentials {
    getUploadCredentials {
      bucket
      region
      key
      presignedUrl
      expiration
    }
  }
`
// 从后端GraphQL获取上传凭证
const getUploadCredentials = async () => {
  try {
    const response = await axios.post('http://localhost:8080/graphql', {
      query: GET_UPLOAD_CREDENTIALS
    })
    if (response.data.errors) {
      throw new Error(response.data.errors[0].message)
    }
    return response.data.data.getUploadCredentials
  } catch (error) {
    console.error('获取上传凭证失败:', error)
    throw error
  }
}
/**
 * 使用预签名URL上传文件到腾讯云COS
 * @param file 要上传的文件
 * @returns Promise<string> 返回文件的访问URL
 */
export const uploadToCOS = async (file: File): Promise<string> => {
  try {
    // 获取上传凭证
    const credentials = await getUploadCredentials()
    // 使用预签名URL
    const uploadUrl = credentials.presignedUrl
    // 使用预签名URL上传文件
    const uploadResponse = await axios.put(uploadUrl, file, {
      headers: {
        'Content-Type': file.type,
      }
    })
    if (uploadResponse.status === 200) {
      // 返回文件的访问URL(去掉查询参数)
      const fileUrl = `https://${credentials.bucket}.cos.${credentials.region}.myqcloud.com/${credentials.key}`
      return fileUrl
    } else {
      throw new Error(`上传失败,状态码: ${uploadResponse.status}`)
    }
  } catch (error) {
    throw error
  }
import axios from 'axios'
import { serverUrl } from './appConfig.js'
// GraphQL查询获取上传凭证
const GET_UPLOAD_CREDENTIALS = `
  query GetUploadCredentials {
    getUploadCredentials {
      bucket
      region
      key
      presignedUrl
      expiration
    }
  }
`
// 从后端GraphQL获取上传凭证
const getUploadCredentials = async () => {
  try {
    const response = await axios.post(`${serverUrl}/graphql`, {
      query: GET_UPLOAD_CREDENTIALS
    })
    if (response.data.errors) {
      throw new Error(response.data.errors[0].message)
    }
    return response.data.data.getUploadCredentials
  } catch (error) {
    console.error('获取上传凭证失败:', error)
    throw error
  }
}
/**
 * 使用预签名URL上传文件到腾讯云COS
 * @param file 要上传的文件
 * @returns Promise<string> 返回文件的访问URL
 */
export const uploadToCOS = async (file: File): Promise<string> => {
  try {
    // 获取上传凭证
    const credentials = await getUploadCredentials()
    // 使用预签名URL
    const uploadUrl = credentials.presignedUrl
    // 使用预签名URL上传文件
    const uploadResponse = await axios.put(uploadUrl, file, {
      headers: {
        'Content-Type': file.type,
      }
    })
    if (uploadResponse.status === 200) {
      // 返回文件的访问URL(去掉查询参数)
      const fileUrl = `https://${credentials.bucket}.cos.${credentials.region}.myqcloud.com/${credentials.key}`
      return fileUrl
    } else {
      throw new Error(`上传失败,状态码: ${uploadResponse.status}`)
    }
  } catch (error) {
    throw error
  }
}
web/src/utils/cos.ts
@@ -1,162 +1,324 @@
import COS from 'cos-js-sdk-v5'
import axios from 'axios'
// 从后端获取临时密钥
const getCredentialsFromBackend = async () => {
  try {
    const response = await axios.get('http://localhost:8080/api/cos/credentials')
    return response.data
  } catch (error) {
    throw error
  }
}
// 创建COS实例
const cos = new COS({
  getAuthorization: async function (options: any, callback: any) {
    try {
      console.log('正在从后端获取COS临时密钥...')
      // 从后端获取临时密钥
      const credentials = await getCredentialsFromBackend()
      console.log('成功获取临时密钥:', {
        TmpSecretId: credentials.TmpSecretId?.substring(0, 10) + '...',
        bucket: credentials.config?.bucket,
        region: credentials.config?.region
      })
      callback({
        TmpSecretId: credentials.TmpSecretId,
        TmpSecretKey: credentials.TmpSecretKey,
        SecurityToken: credentials.SecurityToken,
        StartTime: credentials.StartTime,
        ExpiredTime: credentials.ExpiredTime,
      })
    } catch (error) {
      console.error('获取临时密钥失败:', error)
      callback(error)
    }
  }
})
// 获取COS配置信息
export const getCOSConfig = async () => {
  try {
    const response = await axios.get('http://localhost:8080/api/cos/config')
    return response.data
  } catch (error) {
    console.error('获取COS配置失败:', error)
    throw error
  }
}
/**
 * 上传文件到腾讯云COS
 * @param file 要上传的文件
 * @param folder 存储文件夹路径,如 'avatars/', 'documents/'
 * @returns Promise<string> 返回文件的访问URL
 */
export const uploadToCOS = async (file: File, folder: string = ''): Promise<string> => {
  try {
    // 获取COS配置
    const config = await getCOSConfig()
    // 生成唯一文件名
    const timestamp = Date.now()
    const randomStr = Math.random().toString(36).substring(2, 8)
    const fileExt = file.name.split('.').pop()
    const fileName = `${folder}${timestamp}_${randomStr}.${fileExt}`
    console.log('开始上传文件:', fileName, '到存储桶:', config.bucket)
    return new Promise((resolve, reject) => {
      cos.uploadFile({
        Bucket: config.bucket,
        Region: config.region,
        Key: fileName,
        Body: file,
        SliceSize: 1024 * 1024 * 5, // 大于5MB的文件使用分块上传
        onProgress: (progressData) => {
          console.log('上传进度:', Math.round(progressData.percent * 100) + '%')
        }
      }, (err, data) => {
        if (err) {
          console.error('上传失败:', err)
          reject(err)
        } else {
          console.log('上传成功:', data)
          // 返回文件的访问URL
          const fileUrl = `https://${data.Location}`
          resolve(fileUrl)
        }
      })
    })
  } catch (error) {
    console.error('上传文件失败:', error)
    throw error
  }
}
/**
 * 删除COS中的文件
 * @param key 文件的Key(路径)
 * @returns Promise<boolean>
 */
export const deleteFromCOS = async (key: string): Promise<boolean> => {
  try {
    const config = await getCOSConfig()
    return new Promise((resolve, reject) => {
      cos.deleteObject({
        Bucket: config.bucket,
        Region: config.region,
        Key: key
      }, (err, data) => {
        if (err) {
          console.error('删除失败:', err)
          reject(err)
        } else {
          console.log('删除成功:', data)
          resolve(true)
        }
      })
    })
  } catch (error) {
    console.error('删除文件失败:', error)
    throw error
  }
}
/**
 * 获取文件的临时访问URL(用于私有读取的文件)
 * @param key 文件的Key(路径)
 * @param expires 过期时间(秒),默认1小时
 * @returns Promise<string>
 */
export const getObjectUrl = async (key: string, expires: number = 3600): Promise<string> => {
  try {
    const config = await getCOSConfig()
    return new Promise((resolve, reject) => {
      cos.getObjectUrl({
        Bucket: config.bucket,
        Region: config.region,
        Key: key,
        Expires: expires,
        Sign: true
      }, (err, data) => {
        if (err) {
          console.error('获取URL失败:', err)
          reject(err)
        } else {
          resolve(data.Url)
        }
      })
    })
  } catch (error) {
    console.error('获取文件URL失败:', error)
    throw error
  }
}
import COS from 'cos-js-sdk-v5'
import axios from 'axios'
import { serverUrl } from './appConfig.js'
// 从后端获取临时密钥
const getCredentialsFromBackend = async () => {
  try {
    const response = await axios.get(`${serverUrl}/api/cos/credentials`)
    return response.data
  } catch (error) {
    throw error
  }
}
// 创建COS实例
const cos = new COS({
  getAuthorization: async function (options: any, callback: any) {
    try {
      console.log('正在从后端获取COS临时密钥...')
      // 从后端获取临时密钥
      const credentials = await getCredentialsFromBackend()
      console.log('成功获取临时密钥:', {
        TmpSecretId: credentials.TmpSecretId?.substring(0, 10) + '...',
        bucket: credentials.config?.bucket,
        region: credentials.config?.region
      })
      callback({
        TmpSecretId: credentials.TmpSecretId,
        TmpSecretKey: credentials.TmpSecretKey,
        SecurityToken: credentials.SecurityToken,
        StartTime: credentials.StartTime,
        ExpiredTime: credentials.ExpiredTime,
      })
    } catch (error) {
      console.error('获取临时密钥失败:', error)
      callback(error)
    }
  }
})
// 获取COS配置信息
export const getCOSConfig = async () => {
  try {
    const response = await axios.get(`${serverUrl}/api/cos/config`)
    return response.data
  } catch (error) {
    console.error('获取COS配置失败:', error)
    throw error
  }
}
/**
 * 上传文件到腾讯云COS
 * @param file 要上传的文件
 * @param folder 存储文件夹路径,如 'avatars/', 'documents/'
 * @returns Promise<string> 返回文件的访问URL
 */
export const uploadToCOS = async (file: File, folder: string = ''): Promise<string> => {
  try {
    // 获取COS配置
    const config = await getCOSConfig()
    // 生成唯一文件名
    const timestamp = Date.now()
    const randomStr = Math.random().toString(36).substring(2, 8)
    const fileExt = file.name.split('.').pop()
    const fileName = `${folder}${timestamp}_${randomStr}.${fileExt}`
    console.log('开始上传文件:', fileName, '到存储桶:', config.bucket)
    return new Promise((resolve, reject) => {
      cos.uploadFile({
        Bucket: config.bucket,
        Region: config.region,
        Key: fileName,
        Body: file,
        SliceSize: 1024 * 1024 * 5, // 大于5MB的文件使用分块上传
        onProgress: (progressData) => {
          console.log('上传进度:', Math.round(progressData.percent * 100) + '%')
        }
      }, (err, data) => {
        if (err) {
          console.error('上传失败:', err)
          reject(err)
        } else {
          console.log('上传成功:', data)
          // 返回文件的访问URL
          const fileUrl = `https://${data.Location}`
          resolve(fileUrl)
        }
      })
    })
  } catch (error) {
    console.error('上传文件失败:', error)
    throw error
  }
}
/**
 * 删除COS中的文件
 * @param key 文件的Key(路径)
 * @returns Promise<boolean>
 */
export const deleteFromCOS = async (key: string): Promise<boolean> => {
  try {
    const config = await getCOSConfig()
    return new Promise((resolve, reject) => {
      cos.deleteObject({
        Bucket: config.bucket,
        Region: config.region,
        Key: key
      }, (err, data) => {
        if (err) {
          console.error('删除失败:', err)
          reject(err)
        } else {
          console.log('删除成功:', data)
          resolve(true)
        }
      })
    })
  } catch (error) {
    console.error('删除文件失败:', error)
    throw error
  }
}
/**
 * 获取文件的临时访问URL(用于私有读取的文件)
 * @param key 文件的Key(路径)
 * @param expires 过期时间(秒),默认1小时
 * @returns Promise<string>
 */
export const getObjectUrl = async (key: string, expires: number = 3600): Promise<string> => {
  try {
    const config = await getCOSConfig()
    return new Promise((resolve, reject) => {
      cos.getObjectUrl({
        Bucket: config.bucket,
        Region: config.region,
        Key: key,
        Expires: expires,
        Sign: true
      }, (err, data) => {
        if (err) {
          console.error('获取URL失败:', err)
          reject(err)
        } else {
          resolve(data.Url)
        }
      })
    })
  } catch (error) {
    console.error('获取文件URL失败:', error)
    throw error
  }
}
export default cos
web/src/views/check-detail.vue
@@ -417,7 +417,7 @@
}
// 获取学历文本
const getEducationText = (education: number) => {
const getEducationText = (education: number | string) => {
  const educationMap: Record<number, string> = {
    1: '高中',
    2: '大专',
@@ -425,7 +425,8 @@
    4: '硕士',
    5: '博士'
  }
  return educationMap[education] || '-'
  const numEducation = typeof education === 'string' ? parseInt(education) : education
  return educationMap[numEducation] || '-'
}
// 获取状态文本
web/src/views/next-list.vue
@@ -276,8 +276,15 @@
      size: pagination.size
    })
    
    competitions.value = data || []
    pagination.total = data ? data.length : 0
    // 处理分页响应对象
    if (data && data.content) {
      competitions.value = data.content
      pagination.total = parseInt(data.totalElements) || 0
    } else {
      // 兼容旧的返回格式(如果后端还没更新)
      competitions.value = data || []
      pagination.total = data ? data.length : 0
    }
  } catch (error) {
    console.error('获取比赛晋级列表失败:', error)
    ElMessage.error('获取比赛数据失败: ' + (error.message || '未知错误'))
web/vite.config.ts
@@ -10,12 +10,25 @@
    }
  },
  server: {
    host: '0.0.0.0',
    port: 3000,
    open: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        target: 'http://127.0.0.1:8080',
        changeOrigin: true,
        secure: false,
        configure: (proxy, options) => {
          proxy.on('error', (err, req, res) => {
            console.log('proxy error', err);
          });
          proxy.on('proxyReq', (proxyReq, req, res) => {
            console.log('Sending Request to the Target:', req.method, req.url);
          });
          proxy.on('proxyRes', (proxyRes, req, res) => {
            console.log('Received Response from the Target:', proxyRes.statusCode, req.url);
          });
        },
        // 不需要重写路径,因为后端的context-path就是/api
        // rewrite: (path) => path.replace(/^\/api/, '/api')
      }
wx/app.js
@@ -4,7 +4,10 @@
    userInfo: null,
    token: null,
    sessionKey: null, // 微信会话密钥,用于解密手机号等敏感数据
    baseUrl: 'http://localhost:8080/api/graphql', // 后台GraphQL接口地址
    baseUrl: 'https://ryc.9village.cn/api/graphql', // 后台GraphQL接口地址
    loginUrl:'https://ryc.9village.cn',
    // baseUrl: 'http://localhost:8080/api/graphql', // 后台GraphQL接口地址
    // loginUrl:'http://localhost:8080',
    hasPhoneAuth: false, // 是否已授权手机号
    rejectPhone: false, // 是否拒绝过手机号授权
    cos: {
@@ -101,10 +104,6 @@
    wx.login({
      success: (res) => {
        if (res.code) {
          console.log('✅ 获取微信登录code成功')
          console.log('登录code:', res.code)
          console.log('code长度:', res.code.length)
          console.log('准备调用后端wxLogin接口...')
          this.wxLogin(res.code)
        } else {
          console.error('❌ 获取微信登录code失败')
@@ -130,29 +129,21 @@
    const deviceInfo = this.getDeviceInfo()
    const requestData = {
      code: code,
      loginIp: '127.0.0.1', // 小程序无法获取真实IP,使用默认值
      // loginIp: '127.0.0.1', // 小程序无法获取真实IP,使用默认值
      deviceInfo: deviceInfo
    }
    
    console.log('=== 准备调用后端wxLogin接口 ===')
    console.log('请求URL:', 'http://localhost:8080/api/auth/wx-login')
    console.log('设备信息:', deviceInfo)
    console.log('请求参数:', requestData)
    console.log('请求开始时间:', new Date().toISOString())
    
    wx.request({
      url: 'http://localhost:8080/api/auth/wx-login',
      url: this.globalData.loginUrl + '/api/auth/wx-login',
      method: 'POST',
      header: {
        'Content-Type': 'application/json'
      },
      data: requestData,
      success: (res) => {
        console.log('=== 后端wxLogin接口响应 ===')
        console.log('响应时间:', new Date().toISOString())
        console.log('HTTP状态码:', res.statusCode)
        console.log('响应头:', res.header)
        console.log('响应数据:', JSON.stringify(res.data, null, 2))
        
        if (res.statusCode !== 200) {
          console.error('❌ HTTP请求失败,状态码:', res.statusCode)
wx/app.json
@@ -17,7 +17,7 @@
  "window": {
    "backgroundTextStyle": "light",
    "navigationBarBackgroundColor": "#ffffff",
    "navigationBarTitleText": "蓉易创",
    "navigationBarTitleText": "蓉e创",
    "navigationBarTextStyle": "black",
    "backgroundColor": "#f5f5f5"
  },
wx/pages/index/index.js
@@ -21,19 +21,20 @@
    // 筛选条件
    filterStatus: 'all', // all, upcoming, ongoing, ended
    // 轮播图当前索引
    currentBannerIndex: 0
    currentBannerIndex: 0,
    // 分享相关数据
    shareActivityId: null,
    shareActivityName: null
  },
  onLoad(options) {
    console.log('首页加载')
    this.loadBanners()
    this.loadActivities()
  },
  onShow() {
    console.log('首页显示')
    // 统一系统导航栏标题
    try { wx.setNavigationBarTitle({ title: '蓉易创' }) } catch (e) {}
    try { wx.setNavigationBarTitle({ title: '蓉e创' }) } catch (e) {}
    // 检查登录状态
    if (!app.globalData.token) {
      app.login()
@@ -42,6 +43,9 @@
    if (typeof this.getTabBar === 'function' && this.getTabBar()) {
      this.getTabBar().init();
    }
    // 加载数据
    this.loadBanners()
    this.loadActivities()
  },
  onPullDownRefresh() {
@@ -175,9 +179,9 @@
    if (filterStatus !== 'all') {
      // 根据filterStatus映射到对应的state值
      const stateMapping = {
        'upcoming': 1,    // 即将开始
        'ongoing': 2,     // 进行中
        'ended': 3        // 已结束
        'upcoming': 1,    // 即将开始 -> 发布状态
        'ongoing': 1,     // 进行中 -> 发布状态
        'ended': 2        // 已结束 -> 关闭状态
      }
      stateFilter = stateMapping[filterStatus]
    }
@@ -214,7 +218,7 @@
      page: currentPage,
      size: pageSize,
      name: nameFilter,
      state: stateFilter
      state: 1
    }).then(data => {
      if (data.activities) {
        let newActivities = data.activities.content
@@ -434,5 +438,164 @@
    return now <= signupDeadline && 
           activity.state === 'SIGNUP' &&
           activity.playerCount < activity.playerMax
  },
  // 分享单个比赛
  onShareActivity(e) {
    const { id, name } = e.currentTarget.dataset
    // 显示分享选项
    wx.showActionSheet({
      itemList: ['分享给朋友', '生成分享海报'],
      success: (res) => {
        if (res.tapIndex === 0) {
          // 分享给朋友
          this.shareToFriend(id, name)
        } else if (res.tapIndex === 1) {
          // 生成分享海报
          this.generateSharePoster(id, name)
        }
      },
      fail: (res) => {
        console.log('用户取消分享')
      }
    })
  },
  // 分享给朋友
  shareToFriend(activityId, activityName) {
    wx.showShareMenu({
      withShareTicket: true,
      menus: ['shareAppMessage', 'shareTimeline']
    })
    // 设置当前要分享的活动信息
    this.setData({
      shareActivityId: activityId,
      shareActivityName: activityName
    })
    // 触发分享
    wx.updateShareMenu({
      withShareTicket: true,
      isUpdatableMessage: true,
      activityId: 'share_activity_' + activityId,
      templateInfo: {
        parameterList: [{
          name: 'activity_name',
          value: activityName
        }]
      }
    })
    wx.showToast({
      title: '请点击右上角分享',
      icon: 'none',
      duration: 2000
    })
  },
  // 生成分享海报
  generateSharePoster(activityId, activityName) {
    wx.showLoading({
      title: '生成海报中...'
    })
    // 这里可以调用后端API生成分享海报
    // 或者使用canvas在前端生成
    setTimeout(() => {
      wx.hideLoading()
      wx.showToast({
        title: '海报生成功能开发中',
        icon: 'none',
        duration: 2000
      })
    }, 1500)
  },
  // 页面分享功能 - 分享给朋友
  onShareAppMessage(res) {
    console.log('分享给朋友', res)
    // 如果是从比赛卡片分享
    if (this.data.shareActivityId && this.data.shareActivityName) {
      const shareData = {
        title: `${this.data.shareActivityName} - 蓉e创比赛平台`,
        path: `/pages/activity/detail?id=${this.data.shareActivityId}`,
        imageUrl: '', // 可以设置分享图片
        success: (res) => {
          console.log('分享成功', res)
          wx.showToast({
            title: '分享成功',
            icon: 'success',
            duration: 2000
          })
          // 清除分享状态
          this.setData({
            shareActivityId: null,
            shareActivityName: null
          })
        },
        fail: (res) => {
          console.log('分享失败', res)
          wx.showToast({
            title: '分享失败',
            icon: 'none',
            duration: 2000
          })
        }
      }
      return shareData
    }
    // 默认分享整个首页
    return {
      title: '蓉e创比赛平台 - 发现精彩比赛',
      path: '/pages/index/index',
      imageUrl: '', // 可以设置默认分享图片
      success: (res) => {
        console.log('分享成功', res)
        wx.showToast({
          title: '分享成功',
          icon: 'success',
          duration: 2000
        })
      },
      fail: (res) => {
        console.log('分享失败', res)
        wx.showToast({
          title: '分享失败',
          icon: 'none',
          duration: 2000
        })
      }
    }
  },
  // 分享到朋友圈
  onShareTimeline() {
    console.log('分享到朋友圈')
    return {
      title: '蓉e创比赛平台 - 发现精彩比赛',
      query: '',
      imageUrl: '', // 可以设置分享图片
      success: (res) => {
        console.log('分享到朋友圈成功', res)
        wx.showToast({
          title: '分享成功',
          icon: 'success',
          duration: 2000
        })
      },
      fail: (res) => {
        console.log('分享到朋友圈失败', res)
        wx.showToast({
          title: '分享失败',
          icon: 'none',
          duration: 2000
        })
      }
    }
  }
})
wx/pages/index/index.json
@@ -1,3 +1,3 @@
{
  "navigationBarTitleText": "蓉易创"
  "navigationBarTitleText": "蓉e创"
}
wx/pages/index/index.wxml
@@ -1,6 +1,17 @@
<wxs src="./filters.wxs" module="filters" />
<!--pages/index/index.wxml-->
<view class="container">
  <!-- 页面头部分享区域 -->
  <view class="header-section">
    <view class="page-title">蓉e创比赛平台</view>
    <view class="share-section">
      <!-- <button class="share-btn" open-type="share">
        <text class="share-icon">📤</text>
        <text class="share-text">分享</text>
      </button> -->
    </view>
  </view>
  <!-- 搜索栏 - 暂时隐藏 -->
  <view class="search-bar" style="display: none;">
    <view class="search-input-wrapper">
@@ -93,6 +104,9 @@
          <view class="registered">已报名:{{item.playerCount}}人</view>
          <view class="btn-row">
            <button class="ghost-btn" catchtap="onActivityDetailTap" data-idx="{{index}}"  data-id="{{item.id}}" data-xid="{{item.name}}"  >查看详情</button>
            <!-- <button class="share-activity-btn" catchtap="onShareActivity" data-idx="{{index}}" data-id="{{item.id}}" data-name="{{item.name}}">
              <text class="share-icon-small">📤</text>
            </button> -->
          </view>
        </view>
      </view>
wx/pages/index/index.wxss
@@ -1,5 +1,83 @@
/* pages/index/index.wxss */
/* 页面头部分享区域样式 */
.header-section {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20rpx;
  background: #ffffff;
  border-bottom: 1rpx solid #f0f0f0;
}
.page-title {
  font-size: 36rpx;
  font-weight: 700;
  color: #0f172a;
}
.share-section {
  display: flex;
  align-items: center;
}
.share-btn {
  display: flex;
  align-items: center;
  gap: 8rpx;
  background: #007aff;
  color: #ffffff;
  border: none;
  border-radius: 50rpx;
  padding: 16rpx 24rpx;
  font-size: 26rpx;
  line-height: 1;
}
.share-btn::after {
  border: none;
}
.share-btn:active {
  background: #0056cc;
}
.share-icon {
  font-size: 28rpx;
}
.share-text {
  font-size: 26rpx;
}
/* 比赛卡片分享按钮样式 */
.share-activity-btn {
  width: 80rpx;
  height: 60rpx;
  background: #f8fafc;
  border: 1rpx solid #e2e8f0;
  border-radius: 12rpx;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-left: 16rpx;
  padding: 0;
  line-height: 1;
}
.share-activity-btn::after {
  border: none;
}
.share-activity-btn:active {
  background: #e2e8f0;
}
.share-icon-small {
  font-size: 24rpx;
  color: #64748b;
}
/* 搜索栏样式 */
.search-bar {
  padding: 20rpx;
wx/pages/judge/review.js
@@ -149,9 +149,27 @@
          videos: detail.submissionFiles ? detail.submissionFiles
            .filter(file => file.mediaType === 2)
            .map(file => file.fullUrl || file.url) : [],
          mediaList: (detail.submissionFiles || []).map(file => {
            const mt = file.mediaType
            let typeStr = ''
            if (mt === 1 || mt === 'image' || (typeof mt === 'string' && mt.startsWith('image'))) typeStr = 'image'
            else if (mt === 2 || mt === 'video' || (typeof mt === 'string' && mt.startsWith('video'))) typeStr = 'video'
            else if ((file.fileExt || '').toLowerCase().includes('pdf')) typeStr = 'pdf'
            else if ((file.fileExt || '').toLowerCase().includes('doc')) typeStr = 'word'
            else typeStr = 'file'
            return {
              id: file.id,
              name: file.name,
              size: file.fileSize,
              mediaType: typeStr,
              thumbUrl: file.fullThumbUrl || file.thumbUrl || file.fullUrl || file.url,
              url: file.fullUrl || file.url
            }
          }),
          participant: {
            id: detail.playerInfo.id,
            name: detail.playerInfo.name,
            phone: detail.playerInfo.phone || '',
            gender: this.getGenderText(detail.playerInfo.gender),
            birthday: detail.playerInfo.birthday || '',
            region: detail.regionInfo ? detail.regionInfo.name : '',
@@ -293,20 +311,39 @@
    })
  },
  // 媒体点击
  // 媒体点击(通过 index 定位 mediaList 项)
  onMediaTap(e) {
    const { url, type } = e.currentTarget.dataset
    if (type === 'image') {
    const { index } = e.currentTarget.dataset
    const item = this.data.submission?.mediaList?.[index]
    if (!item) return
    if (item.mediaType === 'image') {
      const imgs = (this.data.submission.mediaList || [])
        .filter(it => it.mediaType === 'image')
        .map(it => it.url)
      wx.previewImage({
        current: url,
        urls: this.data.submission.images || []
        current: item.url,
        urls: imgs.length ? imgs : [item.url]
      })
    } else if (type === 'video') {
    } else if (item.mediaType === 'video') {
      this.setData({
        showMediaPreview: true,
        currentMedia: url,
        currentMedia: item.url,
        mediaType: 'video'
      })
    } else {
      wx.downloadFile({
        url: item.url,
        success: (res) => {
          if (res.statusCode === 200) {
            wx.openDocument({
              filePath: res.tempFilePath,
              showMenu: true
            })
          } else {
            wx.showToast({ title: '预览失败', icon: 'none' })
          }
        },
        fail: () => wx.showToast({ title: '下载失败', icon: 'none' })
      })
    }
  },
@@ -317,6 +354,44 @@
      showMediaPreview: false,
      currentMedia: null
    })
  },
  // 点击预览按钮:图片/视频用 wx.previewMedia,文档用 openDocument
  onPreviewTap(e) {
    const { index } = e.currentTarget.dataset
    const list = this.data.submission?.mediaList || []
    const item = list[index]
    if (!item) return
    if (item.mediaType === 'image' || item.mediaType === 'video') {
      const mediaList = list
        .filter(m => m.mediaType === 'image' || m.mediaType === 'video')
        .map(m => ({
          url: m.url,
          type: m.mediaType === 'video' ? 'video' : 'image',
          poster: m.thumbUrl || m.url
        }))
      const current = Math.max(0, mediaList.findIndex(m => m.url === item.url))
      wx.previewMedia({
        sources: mediaList,
        current
      })
    } else {
      wx.downloadFile({
        url: item.url,
        success: (res) => {
          if (res.statusCode === 200) {
            wx.openDocument({
              filePath: res.tempFilePath,
              showMenu: true
            })
          } else {
            wx.showToast({ title: '预览失败', icon: 'none' })
          }
        },
        fail: () => wx.showToast({ title: '下载失败', icon: 'none' })
      })
    }
  },
  // 下载文件
@@ -560,30 +635,15 @@
  },
  */
  // 联系参赛者
  // 联系参赛者:直接拨打电话
  onContactParticipant() {
    const { submission } = this.data
    if (submission.participant) {
      wx.showActionSheet({
        itemList: ['发送消息', '查看详情'],
        success: (res) => {
          switch (res.tapIndex) {
            case 0:
              // 发送消息功能
              wx.navigateTo({
                url: `/pages/chat/chat?userId=${submission.participant.id}`
              })
              break
            case 1:
              // 查看用户详情
              wx.navigateTo({
                url: `/pages/user/profile?userId=${submission.participant.id}`
              })
              break
          }
        }
      })
    const phone =
      this.data.submission?.participant?.phone ||
      this.data.submission?.participant?.userInfo?.phone
    if (phone) {
      wx.makePhoneCall({ phoneNumber: String(phone) })
    } else {
      wx.showToast({ title: '无联系电话', icon: 'none' })
    }
  },
@@ -619,7 +679,7 @@
  // 分享页面
  onShareAppMessage() {
    return {
      title: '蓉易创 - 评审作品',
      title: '蓉e创 - 评审作品',
      path: '/pages/index/index'
    }
  }
wx/pages/judge/review.wxml
@@ -74,6 +74,9 @@
          <view class="media-info">
            <text class="media-name">{{item.name}}</text>
            <text class="media-size">{{getFileSizeText(item.size)}}</text>
            <view class="preview-btn" catchtap="onPreviewTap" data-index="{{index}}">
              <text>预览</text>
            </view>
          </view>
        </view>
      </view>
wx/pages/judge/review.wxss
@@ -454,6 +454,26 @@
  font-size: 24rpx;
}
/* 预览按钮样式(使用 view 呈现为按钮外观) */
.preview-btn {
  align-self: flex-start;
  margin-top: 8rpx;
  padding: 10rpx 24rpx;
  border: 2rpx solid #007aff;
  color: #007aff;
  border-radius: 999rpx;
  font-size: 24rpx;
  line-height: 1;
  background-color: #ffffff;
}
.preview-btn:active {
  background-color: #e6f0ff;
}
/* 保持媒体信息区的紧凑性 */
.media-info .media-name {
  margin-bottom: 4rpx;
}
@media (max-width: 375px) {
  .container {
    padding-bottom: 100rpx;
wx/pages/message/message.js
@@ -24,11 +24,6 @@
    // 检查用户是否已登录
    const userInfo = app.globalData.userInfo
    if (!userInfo || !userInfo.userId) {
      console.error('用户未登录或userId不存在')
      wx.showToast({
        title: '请先登录',
        icon: 'error'
      })
      return
    }
wx/pages/profile/profile.js
@@ -690,7 +690,7 @@
  // 分享页面
  onShareAppMessage() {
    return {
      title: '蓉易创 - 我的个人中心',
      title: '蓉e创 - 我的个人中心',
      path: '/pages/index/index'
    }
  }
wx/pages/registration/registration.js
@@ -1292,7 +1292,7 @@
        // 第三步:报名成功后强制调用wxlogin获取新的JWT token
        console.log('📱 报名成功,开始强制调用wxlogin获取新的JWT token')
        try {
          await app.wxLogin()
          await app.login()
          console.log('✅ 报名成功后wxlogin调用成功,已获取新的JWT token')
        } catch (wxLoginError) {
          console.error('❌ 报名成功后wxlogin调用失败:', wxLoginError)