| | |
| | | |
| | | 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") |
| | |
| | | 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() |
| | |
| | | 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; |
| | | |
| | | /** |
| | |
| | | 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()) |
| | |
| | | } |
| | | |
| | | 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); |
| | | } |
| | | } |
| | | |
| | | /** |
| | |
| | | * 从token中解析Claims |
| | | */ |
| | | private Claims getClaimsFromToken(String token) { |
| | | SecretKey key = Keys.hmacShaKeyFor(jwtSecret.getBytes()); |
| | | SecretKey key = getSigningKey(); |
| | | return Jwts.parserBuilder() |
| | | .setSigningKey(key) |
| | | .build() |
| | |
| | | @Bean |
| | | public RuntimeWiringConfigurer runtimeWiringConfigurer() { |
| | | return wiringBuilder -> wiringBuilder |
| | | .scalar(ExtendedScalars.GraphQLLong) |
| | | .scalar(longScalar()); |
| | | } |
| | | |
| | |
| | | .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路径匹配 |
| | |
| | | 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; |
| | |
| | | * 获取比赛晋级列表 |
| | | */ |
| | | @QueryMapping |
| | | public List<PromotionCompetitionResponse> promotionCompetitions( |
| | | public PromotionCompetitionPageResponse promotionCompetitions( |
| | | @Argument String name, |
| | | @Argument Integer page, |
| | | @Argument Integer size) { |
| | |
| | | Object birthdayObj = row.get("birthday"); |
| | | 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() : ""); |
| | | |
| | | // 构建区域信息 |
| | |
| | | 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; |
| | |
| | | /** |
| | | * 获取比赛晋级列表 |
| | | */ |
| | | 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()) { |
| | |
| | | |
| | | // 为每个比赛查询其阶段 |
| | | for (Activity competition : competitions) { |
| | | List<Activity> stages = activityRepository.findByPidAndStateOrderByCreateTimeAsc(competition.getId(), 1); |
| | | List<Activity> stages = activityRepository.findByPidAndStateOrderByCreateTimeDesc(competition.getId(), 1); |
| | | |
| | | for (Activity stage : stages) { |
| | | // 统计当前阶段的参赛人数 |
| | |
| | | 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); |
| | | } |
| | | |
| | | /** |
| | |
| | | # 微信端获取选手报名状态 |
| | | getPlayerRegistrationState(activityId: ID!): PlayerRegistrationResponse |
| | | # 获取比赛晋级列表 |
| | | promotionCompetitions(name: String, page: Int, size: Int): [PromotionCompetitionResponse!]! |
| | | promotionCompetitions(name: String, page: Int, size: Int): PromotionCompetitionPageResponse! |
| | | # 获取可晋级参赛者列表 |
| | | promotableParticipants(currentStageId: ID!): PromotableParticipantsResponse |
| | | } |
| | |
| | | state: Int |
| | | } |
| | | |
| | | # 比赛晋级列表分页响应类型 |
| | | type PromotionCompetitionPageResponse { |
| | | content: [PromotionCompetitionResponse!]! |
| | | totalElements: Long! |
| | | page: Int! |
| | | size: Int! |
| | | totalPages: Int! |
| | | } |
| | | |
| | | # 可晋级参赛者列表响应类型 |
| | | type PromotableParticipantsResponse { |
| | | participants: [PromotableParticipantResponse!]! |
| | |
| | | // 媒体查询 API |
| | | import { graphqlRequest, API_CONFIG } from '../config/api.ts'; |
| | | import { serverUrl } from '../utils/appConfig.js'; |
| | | |
| | | const GRAPHQL_ENDPOINT = API_CONFIG.GRAPHQL_ENDPOINT; |
| | | |
| | |
| | | |
| | | 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, |
| | |
| | | const GET_PROMOTION_COMPETITIONS = ` |
| | | query GetPromotionCompetitions($name: String, $page: Int, $size: Int) { |
| | | promotionCompetitions(name: $name, page: $page, size: $size) { |
| | | content { |
| | | id |
| | | competitionId |
| | | competitionName |
| | |
| | | sortOrder |
| | | state |
| | | } |
| | | totalElements |
| | | page |
| | | size |
| | | totalPages |
| | | } |
| | | } |
| | | ` |
| | | |
| | |
| | | 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 = 'http://localhost:8080';
|
| | | |
| | | const defaultMediaBaseUrl = serverUrl; |
| | | |
| | | window.__APP_MEDIA_BASE_URL__ = defaultMediaBaseUrl;
|
| | | |
| | | return defaultMediaBaseUrl;
|
| | | |
| | | }
|
| | | |
| | | } |
| | |
| | | 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('http://localhost:8080/graphql', {
|
| | | |
| | | 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
|
| | | |
| | | }
|
| | | |
| | | } |
| | |
| | | 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('http://localhost:8080/api/cos/credentials')
|
| | | |
| | | 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('http://localhost:8080/api/cos/config')
|
| | | |
| | | 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 |
| | |
| | | } |
| | | |
| | | // 获取学历文本 |
| | | const getEducationText = (education: number) => { |
| | | const getEducationText = (education: number | string) => { |
| | | const educationMap: Record<number, string> = { |
| | | 1: '高中', |
| | | 2: '大专', |
| | |
| | | 4: '硕士', |
| | | 5: '博士' |
| | | } |
| | | return educationMap[education] || '-' |
| | | const numEducation = typeof education === 'string' ? parseInt(education) : education |
| | | return educationMap[numEducation] || '-' |
| | | } |
| | | |
| | | // 获取状态文本 |
| | |
| | | size: pagination.size |
| | | }) |
| | | |
| | | // 处理分页响应对象 |
| | | 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 || '未知错误')) |
| | |
| | | } |
| | | }, |
| | | 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') |
| | | } |
| | |
| | | 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: { |
| | |
| | | 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失败') |
| | |
| | | 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) |
| | |
| | | "window": { |
| | | "backgroundTextStyle": "light", |
| | | "navigationBarBackgroundColor": "#ffffff", |
| | | "navigationBarTitleText": "蓉易创", |
| | | "navigationBarTitleText": "蓉e创", |
| | | "navigationBarTextStyle": "black", |
| | | "backgroundColor": "#f5f5f5" |
| | | }, |
| | |
| | | // 筛选条件 |
| | | 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() |
| | |
| | | if (typeof this.getTabBar === 'function' && this.getTabBar()) { |
| | | this.getTabBar().init(); |
| | | } |
| | | // 加载数据 |
| | | this.loadBanners() |
| | | this.loadActivities() |
| | | }, |
| | | |
| | | onPullDownRefresh() { |
| | |
| | | if (filterStatus !== 'all') { |
| | | // 根据filterStatus映射到对应的state值 |
| | | const stateMapping = { |
| | | 'upcoming': 1, // 即将开始 |
| | | 'ongoing': 2, // 进行中 |
| | | 'ended': 3 // 已结束 |
| | | 'upcoming': 1, // 即将开始 -> 发布状态 |
| | | 'ongoing': 1, // 进行中 -> 发布状态 |
| | | 'ended': 2 // 已结束 -> 关闭状态 |
| | | } |
| | | stateFilter = stateMapping[filterStatus] |
| | | } |
| | |
| | | page: currentPage, |
| | | size: pageSize, |
| | | name: nameFilter, |
| | | state: stateFilter |
| | | state: 1 |
| | | }).then(data => { |
| | | if (data.activities) { |
| | | let newActivities = data.activities.content |
| | |
| | | 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 |
| | | }) |
| | | } |
| | | } |
| | | } |
| | | }) |
| | |
| | | { |
| | | "navigationBarTitleText": "蓉易创" |
| | | "navigationBarTitleText": "蓉e创" |
| | | } |
| | |
| | | <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"> |
| | |
| | | <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> |
| | |
| | | /* 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; |
| | |
| | | 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 : '', |
| | |
| | | }) |
| | | }, |
| | | |
| | | // 媒体点击 |
| | | // 媒体点击(通过 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' }) |
| | | }) |
| | | } |
| | | }, |
| | |
| | | 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' }) |
| | | }) |
| | | } |
| | | }, |
| | | |
| | | // 下载文件 |
| | |
| | | }, |
| | | */ |
| | | |
| | | // 联系参赛者 |
| | | // 联系参赛者:直接拨打电话 |
| | | 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' }) |
| | | } |
| | | }, |
| | | |
| | |
| | | // 分享页面 |
| | | onShareAppMessage() { |
| | | return { |
| | | title: '蓉易创 - 评审作品', |
| | | title: '蓉e创 - 评审作品', |
| | | path: '/pages/index/index' |
| | | } |
| | | } |
| | |
| | | <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> |
| | |
| | | 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; |
| | |
| | | // 检查用户是否已登录 |
| | | const userInfo = app.globalData.userInfo |
| | | if (!userInfo || !userInfo.userId) { |
| | | console.error('用户未登录或userId不存在') |
| | | wx.showToast({ |
| | | title: '请先登录', |
| | | icon: 'error' |
| | | }) |
| | | return |
| | | } |
| | | |
| | |
| | | // 分享页面 |
| | | onShareAppMessage() { |
| | | return { |
| | | title: '蓉易创 - 我的个人中心', |
| | | title: '蓉e创 - 我的个人中心', |
| | | path: '/pages/index/index' |
| | | } |
| | | } |
| | |
| | | // 第三步:报名成功后强制调用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) |