修复评审功能和用户认证问题
- 修复微信小程序评审页面switch语句数字字符串转换问题
- 修复GraphQL查询参数为空字符串的问题
- 添加用户认证API和相关功能
- 改进用户信息保存和验证逻辑
- 优化前端用户界面和交互体验
New file |
| | |
| | | package com.rongyichuang.auth.api; |
| | | |
| | | import com.rongyichuang.auth.dto.PhoneDecryptResponse; |
| | | import com.rongyichuang.auth.dto.WxLoginRequest; |
| | | import com.rongyichuang.auth.dto.WxLoginResponse; |
| | | import com.rongyichuang.auth.dto.LoginRequest; |
| | | import com.rongyichuang.auth.dto.LoginResponse; |
| | | import com.rongyichuang.auth.service.AuthService; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.graphql.data.method.annotation.Argument; |
| | | import org.springframework.graphql.data.method.annotation.MutationMapping; |
| | | import org.springframework.stereotype.Controller; |
| | | |
| | | /** |
| | | * 认证GraphQL API控制器 |
| | | */ |
| | | @Controller |
| | | public class AuthGraphqlApi { |
| | | |
| | | @Autowired |
| | | private AuthService authService; |
| | | |
| | | /** |
| | | * 微信登录 |
| | | */ |
| | | @MutationMapping |
| | | public WxLoginResponse wxLogin(@Argument WxLoginRequest input) { |
| | | try { |
| | | return authService.wxLogin(input); |
| | | } catch (Exception e) { |
| | | throw new RuntimeException("微信登录失败: " + e.getMessage(), e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Web端登录 |
| | | */ |
| | | @MutationMapping |
| | | public LoginResponse webLogin(@Argument LoginRequest input) { |
| | | try { |
| | | return authService.login(input); |
| | | } catch (Exception e) { |
| | | throw new RuntimeException("登录失败: " + e.getMessage(), e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 解密微信手机号(旧版API) |
| | | */ |
| | | @MutationMapping |
| | | public PhoneDecryptResponse decryptPhoneNumber( |
| | | @Argument String encryptedData, |
| | | @Argument String iv, |
| | | @Argument String sessionKey) { |
| | | try { |
| | | return authService.decryptPhoneNumber(encryptedData, iv, sessionKey); |
| | | } catch (Exception e) { |
| | | throw new RuntimeException("手机号解密失败: " + e.getMessage(), e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 获取微信手机号(新版API) |
| | | */ |
| | | @MutationMapping |
| | | public PhoneDecryptResponse getPhoneNumberByCode(@Argument String code) { |
| | | try { |
| | | return authService.getPhoneNumberByCode(code); |
| | | } catch (Exception e) { |
| | | throw new RuntimeException("获取手机号失败: " + e.getMessage(), e); |
| | | } |
| | | } |
| | | } |
| | |
| | | * 返回权限错误响应 |
| | | */ |
| | | private void sendUnauthorizedResponse(HttpServletResponse response) throws IOException { |
| | | response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); |
| | | response.setStatus(HttpServletResponse.SC_FORBIDDEN); |
| | | response.setContentType("application/json;charset=UTF-8"); |
| | | response.getWriter().write("{\"errors\":[{\"message\":\"没有权限访问,请先登录\",\"extensions\":{\"code\":\"UNAUTHORIZED\"}}]}"); |
| | | } |
| | |
| | | return; |
| | | } |
| | | |
| | | // 查找用户信息并设置认证 |
| | | Optional<User> userOpt = userRepository.findById(userId); |
| | | if (userOpt.isPresent()) { |
| | | User user = userOpt.get(); |
| | | // 检查是否为匿名用户(负数用户ID) |
| | | if (userId < 0) { |
| | | // 匿名用户,设置特殊的认证信息 |
| | | UsernamePasswordAuthenticationToken authToken = |
| | | new UsernamePasswordAuthenticationToken( |
| | | user.getId().toString(), |
| | | "anonymous_" + userId, |
| | | null, |
| | | new ArrayList<>() |
| | | Arrays.asList(new SimpleGrantedAuthority("ROLE_ANONYMOUS")) |
| | | ); |
| | | authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); |
| | | SecurityContextHolder.getContext().setAuthentication(authToken); |
| | | logger.debug("GraphQL请求认证成功: userId={}", user.getId()); |
| | | logger.debug("GraphQL请求匿名用户认证成功: userId={}", userId); |
| | | } else { |
| | | logger.warn("GraphQL请求的用户不存在: userId={}", userId); |
| | | sendUnauthorizedResponse(response); |
| | | return; |
| | | // 正常用户,查找用户信息并设置认证 |
| | | Optional<User> userOpt = userRepository.findById(userId); |
| | | if (userOpt.isPresent()) { |
| | | User user = userOpt.get(); |
| | | UsernamePasswordAuthenticationToken authToken = |
| | | new UsernamePasswordAuthenticationToken( |
| | | user.getId().toString(), |
| | | null, |
| | | new ArrayList<>() |
| | | ); |
| | | authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); |
| | | SecurityContextHolder.getContext().setAuthentication(authToken); |
| | | logger.debug("GraphQL请求认证成功: userId={}", user.getId()); |
| | | } else { |
| | | logger.warn("GraphQL请求的用户不存在: userId={}", userId); |
| | | sendUnauthorizedResponse(response); |
| | | return; |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | logger.error("GraphQL请求JWT验证失败: {}", e.getMessage()); |
| | |
| | | if (jwtUtil.validateToken(token)) { |
| | | logger.debug("Token验证成功,查找用户信息"); |
| | | |
| | | // 查找用户信息 |
| | | Optional<User> userOpt = userRepository.findById(userId); |
| | | if (userOpt.isPresent()) { |
| | | User user = userOpt.get(); |
| | | logger.debug("找到用户: userId={}, phone={}", user.getId(), user.getPhone()); |
| | | |
| | | // 创建认证对象 |
| | | // 检查是否为匿名用户(负数用户ID) |
| | | if (userId < 0) { |
| | | // 匿名用户,设置特殊的认证信息 |
| | | UsernamePasswordAuthenticationToken authToken = |
| | | new UsernamePasswordAuthenticationToken( |
| | | user.getId().toString(), |
| | | "anonymous_" + userId, |
| | | null, |
| | | new ArrayList<>() // 暂时不设置权限,后续可以根据角色设置 |
| | | Arrays.asList(new SimpleGrantedAuthority("ROLE_ANONYMOUS")) |
| | | ); |
| | | |
| | | authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); |
| | | SecurityContextHolder.getContext().setAuthentication(authToken); |
| | | |
| | | logger.info("用户认证成功: userId={}, phone={}", user.getId(), user.getPhone()); |
| | | logger.info("匿名用户认证成功: userId={}", userId); |
| | | } else { |
| | | logger.warn("用户不存在: userId={}", userId); |
| | | // 正常用户,查找用户信息 |
| | | Optional<User> userOpt = userRepository.findById(userId); |
| | | if (userOpt.isPresent()) { |
| | | User user = userOpt.get(); |
| | | logger.debug("找到用户: userId={}, phone={}", user.getId(), user.getPhone()); |
| | | |
| | | // 创建认证对象 |
| | | UsernamePasswordAuthenticationToken authToken = |
| | | new UsernamePasswordAuthenticationToken( |
| | | user.getId().toString(), |
| | | null, |
| | | new ArrayList<>() // 暂时不设置权限,后续可以根据角色设置 |
| | | ); |
| | | |
| | | authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); |
| | | SecurityContextHolder.getContext().setAuthentication(authToken); |
| | | |
| | | logger.info("用户认证成功: userId={}, phone={}", user.getId(), user.getPhone()); |
| | | } else { |
| | | logger.warn("用户不存在: userId={}", userId); |
| | | } |
| | | } |
| | | } else { |
| | | logger.warn("Token验证失败"); |
| | |
| | | logger.warn("⚠️ unionid为空或用户已找到,跳过unionid查找"); |
| | | } |
| | | |
| | | // 4. 如果都没找到用户,不创建新用户,只记录登录信息 |
| | | // 4. 如果都没找到用户,为小程序访问创建匿名用户对象(不保存到数据库) |
| | | if (user == null) { |
| | | logger.info("未找到现有用户,普通访问用户不创建user记录,只记录登录信息"); |
| | | logger.info("未找到现有用户,为小程序访问创建匿名用户对象"); |
| | | logger.info("访问用户信息:"); |
| | | logger.info("- openid: {}", wxLoginRequest.getWxOpenid()); |
| | | logger.info("- unionid: {}", wxLoginRequest.getWxUnionid()); |
| | |
| | | throw new RuntimeException("创建登录记录失败: " + e.getMessage(), e); |
| | | } |
| | | |
| | | // 返回访问用户的响应(无用户信息,无token) |
| | | // 6. 为匿名用户生成特殊的JWT token(使用专门的wxopenid字段) |
| | | logger.info("步骤4: 为匿名用户生成JWT token"); |
| | | String token = null; |
| | | try { |
| | | // 使用特殊的用户ID(负数)来标识匿名用户,避免与真实用户ID冲突 |
| | | Long anonymousUserId = -Math.abs(wxLoginRequest.getWxOpenid().hashCode()) % 1000000L; |
| | | // 使用新的三参数方法:userId, phone(null), wxopenid |
| | | token = jwtUtil.generateToken(anonymousUserId, null, wxLoginRequest.getWxOpenid()); |
| | | logger.info("✅ 成功生成匿名用户JWT token,wxopenid: {}", wxLoginRequest.getWxOpenid()); |
| | | } catch (Exception e) { |
| | | logger.error("❌ 生成匿名用户JWT token失败: {}", e.getMessage()); |
| | | logger.error("异常堆栈:", e); |
| | | throw new RuntimeException("生成JWT token失败: " + e.getMessage(), e); |
| | | } |
| | | |
| | | // 7. 构建匿名用户信息 |
| | | logger.info("步骤5: 构建匿名用户信息响应"); |
| | | LoginResponse.UserInfo userInfo = new LoginResponse.UserInfo( |
| | | null, // userId为null,表示匿名用户 |
| | | "微信用户", // 默认名称 |
| | | null, // 没有电话号码 |
| | | "anonymous" // 用户类型为匿名 |
| | | ); |
| | | |
| | | // 返回匿名用户的响应(包含用户信息和token) |
| | | WxLoginResponse response = new WxLoginResponse(); |
| | | response.setSuccess(true); |
| | | response.setMessage("访问成功"); |
| | | response.setMessage("匿名访问成功"); |
| | | response.setToken(token); |
| | | response.setUserInfo(userInfo); |
| | | response.setSessionKey(wxLoginRequest.getSessionKey()); |
| | | response.setIsNewUser(false); |
| | | response.setHasEmployee(false); |
| | | response.setHasJudge(false); |
| | | response.setHasPlayer(false); |
| | | response.setLoginRecordId(loginRecord.getId()); |
| | | |
| | | logger.info("=== 微信访问流程完成(无用户创建) ==="); |
| | | logger.info("=== 微信匿名访问流程完成 ==="); |
| | | return response; |
| | | } |
| | | |
| | |
| | | |
| | | // 6. 生成JWT token |
| | | logger.info("步骤5: 生成JWT token"); |
| | | String tokenIdentifier = user.getPhone() != null ? user.getPhone() : user.getWxOpenid(); |
| | | logger.info("Token标识符: {}", tokenIdentifier); |
| | | logger.info("用户手机号: {}", user.getPhone()); |
| | | logger.info("用户wxopenid: {}", user.getWxOpenid()); |
| | | |
| | | String token = null; |
| | | try { |
| | | token = jwtUtil.generateToken(user.getId(), tokenIdentifier); |
| | | // 使用新的三参数方法:userId, phone, wxopenid |
| | | token = jwtUtil.generateToken(user.getId(), user.getPhone(), user.getWxOpenid()); |
| | | logger.info("✅ 成功生成JWT token,长度: {}", token != null ? token.length() : 0); |
| | | } catch (Exception e) { |
| | | logger.error("❌ 生成JWT token失败: {}", e.getMessage()); |
| | |
| | | private long jwtExpiration; |
| | | |
| | | /** |
| | | * 生成JWT token |
| | | * 生成JWT token(旧版本,保持兼容性) |
| | | */ |
| | | public String generateToken(Long userId, String phone) { |
| | | return generateToken(userId, phone, null); |
| | | } |
| | | |
| | | /** |
| | | * 生成JWT token(新版本,支持wxopenid) |
| | | */ |
| | | public String generateToken(Long userId, String phone, String wxopenid) { |
| | | Date now = new Date(); |
| | | Date expiryDate = new Date(now.getTime() + jwtExpiration); |
| | | |
| | | SecretKey key = Keys.hmacShaKeyFor(jwtSecret.getBytes()); |
| | | |
| | | return Jwts.builder() |
| | | JwtBuilder builder = Jwts.builder() |
| | | .setSubject(userId.toString()) |
| | | .claim("phone", phone) |
| | | .setIssuedAt(now) |
| | | .setExpiration(expiryDate) |
| | | .signWith(key, SignatureAlgorithm.HS256) |
| | | .compact(); |
| | | .setExpiration(expiryDate); |
| | | |
| | | // 只有当phone不为null时才添加phone claim |
| | | if (phone != null) { |
| | | builder.claim("phone", phone); |
| | | } |
| | | |
| | | // 只有当wxopenid不为null时才添加wxopenid claim |
| | | if (wxopenid != null) { |
| | | builder.claim("wxopenid", wxopenid); |
| | | } |
| | | |
| | | return builder.signWith(key, SignatureAlgorithm.HS256).compact(); |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | /** |
| | | * 从token中获取微信openid |
| | | */ |
| | | public String getWxOpenidFromToken(String token) { |
| | | Claims claims = getClaimsFromToken(token); |
| | | return claims.get("wxopenid", String.class); |
| | | } |
| | | |
| | | /** |
| | | * 验证token是否有效 |
| | | */ |
| | | public boolean validateToken(String token) { |
| | |
| | | private JwtUtil jwtUtil; |
| | | |
| | | /** |
| | | * 获取当前登录用户ID |
| | | * 从JWT token中解析用户ID |
| | | * 获取当前登录用户ID(包括匿名用户) |
| | | * 从JWT token中解析用户ID,包括负数的匿名用户ID |
| | | * |
| | | * @return 用户ID |
| | | * @throws SecurityException 当没有有效认证时抛出 |
| | | * @return 用户ID,包括匿名用户的负数ID |
| | | */ |
| | | public Long getCurrentUserId() { |
| | | public Long getCurrentUserIdIncludingAnonymous() { |
| | | try { |
| | | // 首先尝试从HTTP请求头中获取JWT token |
| | | String token = getTokenFromRequest(); |
| | | if (token != null && jwtUtil.validateToken(token)) { |
| | | Long userId = jwtUtil.getUserIdFromToken(token); |
| | | logger.debug("从JWT token中获取到用户ID: {}", userId); |
| | | logger.debug("从JWT token中获取到用户ID(包括匿名用户): {}", userId); |
| | | return userId; |
| | | } |
| | | |
| | |
| | | |
| | | // 如果没有有效的JWT token,尝试从Spring Security上下文获取 |
| | | Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); |
| | | if (authentication != null && authentication.isAuthenticated() && |
| | | !"anonymousUser".equals(authentication.getPrincipal())) { |
| | | logger.debug("获取到认证用户: {}", authentication.getName()); |
| | | if (authentication != null && authentication.isAuthenticated()) { |
| | | String principal = authentication.getName(); |
| | | logger.debug("获取到认证用户: {}", principal); |
| | | |
| | | // 检查是否为匿名用户 |
| | | if ("anonymousUser".equals(principal)) { |
| | | logger.debug("检测到Spring默认匿名用户,返回null"); |
| | | return null; |
| | | } else if (principal.startsWith("anonymous_")) { |
| | | // 从 "anonymous_-833488" 中提取用户ID |
| | | try { |
| | | String userIdStr = principal.substring("anonymous_".length()); |
| | | Long userId = Long.parseLong(userIdStr); |
| | | logger.debug("从匿名认证中解析到用户ID: {}", userId); |
| | | return userId; |
| | | } catch (NumberFormatException e) { |
| | | logger.warn("无法从匿名认证信息中解析用户ID: {}", principal); |
| | | } |
| | | } |
| | | |
| | | // 从Spring Security上下文中获取用户ID |
| | | try { |
| | | return Long.parseLong(authentication.getName()); |
| | | return Long.parseLong(principal); |
| | | } catch (NumberFormatException e) { |
| | | logger.warn("无法从认证信息中解析用户ID: {}", authentication.getName()); |
| | | logger.warn("无法从认证信息中解析用户ID: {}", principal); |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | logger.warn("获取当前用户ID时发生异常: {}", e.getMessage()); |
| | | } |
| | | |
| | | // 如果没有有效的认证信息,抛出权限异常 |
| | | logger.warn("没有有效的认证信息,拒绝访问"); |
| | | throw new SecurityException("没有权限"); |
| | | // 如果没有有效的认证信息,返回null |
| | | logger.debug("没有有效的认证信息,返回null"); |
| | | return null; |
| | | } |
| | | |
| | | /** |
| | | * 获取当前登录用户ID |
| | | * 从JWT token中解析用户ID |
| | | * |
| | | * @return 用户ID,如果是匿名用户则返回null |
| | | */ |
| | | public Long getCurrentUserId() { |
| | | try { |
| | | // 首先尝试从HTTP请求头中获取JWT token |
| | | String token = getTokenFromRequest(); |
| | | if (token != null && jwtUtil.validateToken(token)) { |
| | | Long userId = jwtUtil.getUserIdFromToken(token); |
| | | logger.debug("从JWT token中获取到用户ID: {}", userId); |
| | | |
| | | // 检查是否为匿名用户(负数用户ID) |
| | | if (userId != null && userId < 0) { |
| | | logger.debug("检测到匿名用户,返回null"); |
| | | return null; |
| | | } |
| | | |
| | | return userId; |
| | | } |
| | | |
| | | if (token == null) { |
| | | logger.debug("未能从请求头获取到JWT token"); |
| | | } else { |
| | | logger.debug("从请求头获取到token但校验失败"); |
| | | } |
| | | |
| | | // 如果没有有效的JWT token,尝试从Spring Security上下文获取 |
| | | Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); |
| | | if (authentication != null && authentication.isAuthenticated()) { |
| | | String principal = authentication.getName(); |
| | | logger.debug("获取到认证用户: {}", principal); |
| | | |
| | | // 检查是否为匿名用户 |
| | | if ("anonymousUser".equals(principal) || principal.startsWith("anonymous_")) { |
| | | logger.debug("检测到匿名用户认证,返回null"); |
| | | return null; |
| | | } |
| | | |
| | | // 从Spring Security上下文中获取用户ID |
| | | try { |
| | | return Long.parseLong(principal); |
| | | } catch (NumberFormatException e) { |
| | | logger.warn("无法从认证信息中解析用户ID: {}", principal); |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | logger.warn("获取当前用户ID时发生异常: {}", e.getMessage()); |
| | | } |
| | | |
| | | // 如果没有有效的认证信息,返回null(支持匿名访问) |
| | | logger.debug("没有有效的认证信息,返回null(匿名用户)"); |
| | | return null; |
| | | } |
| | | |
| | | /** |
| | | * 从HTTP请求中获取JWT token |
| | | */ |
| | | private String getTokenFromRequest() { |
| | | public String getTokenFromRequest() { |
| | | try { |
| | | ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); |
| | | if (attributes == null) { |
| | |
| | | package com.rongyichuang.employee.dto.response; |
| | | |
| | | import com.rongyichuang.employee.entity.Employee; |
| | | import com.rongyichuang.user.entity.User; |
| | | |
| | | /** |
| | | * 员工响应DTO |
| | |
| | | private String phone; |
| | | private String roleId; |
| | | private String description; |
| | | private Integer state; |
| | | private String createTime; |
| | | private String updateTime; |
| | | |
| | | // 构造函数 |
| | | public EmployeeResponse() {} |
| | | |
| | | /** |
| | | * @deprecated 此构造函数已废弃,请使用 EmployeeResponse(Employee, User) 构造函数 |
| | | */ |
| | | @Deprecated |
| | | public EmployeeResponse(Employee employee) { |
| | | this.id = employee.getId(); |
| | | this.name = employee.getName(); |
| | | this.phone = employee.getPhone(); |
| | | this.phone = employee.getPhone(); // 这里会返回null,因为phone字段已废弃 |
| | | this.roleId = employee.getRoleId(); |
| | | this.description = employee.getDescription(); |
| | | this.state = employee.getState(); |
| | | this.createTime = employee.getCreateTime() != null ? employee.getCreateTime().toString() : null; |
| | | this.updateTime = employee.getUpdateTime() != null ? employee.getUpdateTime().toString() : null; |
| | | } |
| | | |
| | | /** |
| | | * 推荐使用的构造函数,从User对象获取phone信息 |
| | | */ |
| | | public EmployeeResponse(Employee employee, User user) { |
| | | this.id = employee.getId(); |
| | | this.name = employee.getName(); |
| | | this.phone = user != null ? user.getPhone() : null; // 从User对象获取phone |
| | | this.roleId = employee.getRoleId(); |
| | | this.description = employee.getDescription(); |
| | | this.state = employee.getState(); |
| | | this.createTime = employee.getCreateTime() != null ? employee.getCreateTime().toString() : null; |
| | | this.updateTime = employee.getUpdateTime() != null ? employee.getUpdateTime().toString() : null; |
| | | } |
| | |
| | | this.description = description; |
| | | } |
| | | |
| | | public Integer getState() { |
| | | return state; |
| | | } |
| | | |
| | | public void setState(Integer state) { |
| | | this.state = state; |
| | | } |
| | | |
| | | public String getCreateTime() { |
| | | return createTime; |
| | | } |
| | |
| | | |
| | | /** |
| | | * 手机号码 |
| | | * @deprecated 此字段已废弃,电话号码统一保存在t_user表中,此字段将设置为null |
| | | */ |
| | | @Column(name = "phone", length = 32, nullable = false) |
| | | @Deprecated |
| | | @Column(name = "phone", length = 32, nullable = true) |
| | | private String phone; |
| | | |
| | | |
| | |
| | | this.name = name; |
| | | } |
| | | |
| | | /** |
| | | * @deprecated 此方法已废弃,请通过关联的User对象获取电话号码 |
| | | */ |
| | | @Deprecated |
| | | public String getPhone() { |
| | | return phone; |
| | | } |
| | | |
| | | /** |
| | | * @deprecated 此方法已废弃,电话号码统一保存在User表中 |
| | | */ |
| | | @Deprecated |
| | | public void setPhone(String phone) { |
| | | this.phone = phone; |
| | | } |
| | |
| | | private static final Pattern PASSWORD_PATTERN = Pattern.compile("^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d@$!%*?&]{6,}$"); |
| | | |
| | | /** |
| | | * 辅助方法:将Employee转换为EmployeeResponse |
| | | */ |
| | | private EmployeeResponse convertToResponse(Employee employee) { |
| | | Optional<User> userOpt = userService.findById(employee.getUserId()); |
| | | if (userOpt.isPresent()) { |
| | | User user = userOpt.get(); |
| | | return new EmployeeResponse(employee, user); |
| | | } else { |
| | | throw new BusinessException("USER_NOT_FOUND", "员工对应的用户不存在,员工ID: " + employee.getId()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 获取所有员工列表 |
| | | */ |
| | | public List<EmployeeResponse> findAllEmployees() { |
| | | List<Employee> employees = employeeRepository.findAll(); |
| | | return employees.stream() |
| | | .map(EmployeeResponse::new) |
| | | .map(this::convertToResponse) |
| | | .collect(Collectors.toList()); |
| | | } |
| | | |
| | |
| | | public Page<EmployeeResponse> findEmployees(String name, int page, int size) { |
| | | Pageable pageable = PageRequest.of(page, size); |
| | | Page<Employee> employeePage = employeeRepository.findByNameContainingOrderByCreateTimeDesc(name, pageable); |
| | | return employeePage.map(EmployeeResponse::new); |
| | | return employeePage.map(this::convertToResponse); |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | List<Employee> employees = employeeRepository.findByNameContaining(name.trim()); |
| | | return employees.stream() |
| | | .map(EmployeeResponse::new) |
| | | .map(this::convertToResponse) |
| | | .collect(Collectors.toList()); |
| | | } |
| | | |
| | |
| | | public EmployeeResponse findById(Long id) { |
| | | Optional<Employee> employee = employeeRepository.findById(id); |
| | | if (employee.isPresent()) { |
| | | return new EmployeeResponse(employee.get()); |
| | | Employee emp = employee.get(); |
| | | Optional<User> userOpt = userService.findById(emp.getUserId()); |
| | | if (userOpt.isPresent()) { |
| | | User user = userOpt.get(); |
| | | return new EmployeeResponse(emp, user); |
| | | } else { |
| | | throw new BusinessException("USER_NOT_FOUND", "员工对应的用户不存在,员工ID: " + id); |
| | | } |
| | | } |
| | | throw new BusinessException("EMPLOYEE_NOT_FOUND", "员工不存在"); |
| | | } |
| | |
| | | employee = employeeRepository.findById(input.getId()) |
| | | .orElseThrow(() -> new BusinessException("EMPLOYEE_NOT_FOUND", "员工不存在")); |
| | | |
| | | // 检查手机号是否被其他员工使用 |
| | | if (employeeRepository.existsByPhoneAndIdNot(input.getPhone(), input.getId())) { |
| | | throw new BusinessException("PHONE_ALREADY_EXISTS", "手机号已被其他员工使用"); |
| | | // 检查用户ID是否被其他员工使用(排除当前员工) |
| | | Optional<Employee> existingEmployee = employeeRepository.findByUserId(user.getId()); |
| | | if (existingEmployee.isPresent() && !existingEmployee.get().getId().equals(input.getId())) { |
| | | throw new BusinessException("PHONE_ALREADY_EXISTS", "该手机号已被其他员工使用"); |
| | | } |
| | | } else { |
| | | // 新增员工 |
| | | if (employeeRepository.existsByPhone(input.getPhone())) { |
| | | throw new BusinessException("PHONE_ALREADY_EXISTS", "手机号已存在"); |
| | | // 新增员工 - 检查该用户是否已经是员工 |
| | | Optional<Employee> existingEmployee = employeeRepository.findByUserId(user.getId()); |
| | | if (existingEmployee.isPresent()) { |
| | | throw new BusinessException("PHONE_ALREADY_EXISTS", "该手机号已被其他员工使用"); |
| | | } |
| | | employee = new Employee(); |
| | | } |
| | | |
| | | // 设置基本信息 |
| | | employee.setName(input.getName()); |
| | | employee.setPhone(input.getPhone()); |
| | | // 不再设置phone字段,保持为null |
| | | employee.setPhone(null); |
| | | employee.setRoleId(input.getRoleId()); |
| | | employee.setDescription(input.getDescription()); |
| | | employee.setUserId(user.getId()); // 设置关联的用户ID |
| | |
| | | Employee savedEmployee = employeeRepository.save(employee); |
| | | logger.info("员工保存成功: {}", savedEmployee.getName()); |
| | | |
| | | return new EmployeeResponse(savedEmployee); |
| | | return new EmployeeResponse(savedEmployee, user); |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | if (input.getId() != null) { |
| | | // 更新现有评委 |
| | | judge = judgeRepository.findById(input.getId()) |
| | | .orElseThrow(() -> new BusinessException("评委不存在")); |
| | | |
| | | // 检查用户ID是否被其他评委使用 |
| | | Optional<Judge> existingJudgeByUserId = judgeRepository.findByUserId(user.getId()); |
| | | if (existingJudgeByUserId.isPresent() && !existingJudgeByUserId.get().getId().equals(input.getId())) { |
| | | throw new BusinessException("该手机号已被其他评委使用"); |
| | | } |
| | | } else { |
| | | // 新增评委 |
| | | judge = new Judge(); |
| | | // 新增评委时检查手机号是否已存在 |
| | | if (judgeRepository.existsByPhone(input.getPhone())) { |
| | | throw new BusinessException("PHONE_EXISTS", "手机号码已存在,请使用其他手机号码"); |
| | | |
| | | // 检查该用户是否已是评委 |
| | | Optional<Judge> existingJudge = judgeRepository.findByUserId(user.getId()); |
| | | if (existingJudge.isPresent()) { |
| | | throw new BusinessException("该手机号已被其他评委使用"); |
| | | } |
| | | } |
| | | |
| | | judge.setName(input.getName()); |
| | | judge.setPhone(input.getPhone()); |
| | | judge.setPhone(null); // 废弃judge.phone字段,设置为null |
| | | judge.setGender(input.getGender()); |
| | | judge.setDescription(input.getDescription()); |
| | | judge.setTitle(input.getTitle()); |
| | |
| | | JudgeResponse response = new JudgeResponse(); |
| | | response.setId(judge.getId()); |
| | | response.setName(judge.getName()); |
| | | response.setPhone(judge.getPhone()); |
| | | |
| | | // 通过关联的user获取phone |
| | | if (judge.getUserId() != null) { |
| | | Optional<User> userOpt = userService.findById(judge.getUserId()); |
| | | if (userOpt.isPresent()) { |
| | | response.setPhone(userOpt.get().getPhone()); |
| | | } else { |
| | | response.setPhone(null); |
| | | } |
| | | } else { |
| | | response.setPhone(null); |
| | | } |
| | | |
| | | response.setGender(judge.getGender()); |
| | | response.setDescription(judge.getDescription()); |
| | | response.setTitle(judge.getTitle()); |
| | |
| | | */ |
| | | @QueryMapping |
| | | public ReviewProjectPageResponse unReviewedProjects( |
| | | @Argument String searchKeyword, |
| | | @Argument int page, |
| | | @Argument int pageSize, |
| | | @Argument String searchKeyword) { |
| | | log.info("查询我未评审的项目列表,page: {}, pageSize: {}, searchKeyword: {}", page, pageSize, searchKeyword); |
| | | @Argument int pageSize) { |
| | | log.info("查询我未评审的项目列表,searchKeyword: {}, page: {}, pageSize: {}", searchKeyword, page, pageSize); |
| | | |
| | | Long currentJudgeId = userContextUtil.getCurrentJudgeId(); |
| | | if (currentJudgeId == null) { |
| | |
| | | */ |
| | | @QueryMapping |
| | | public ReviewProjectPageResponse reviewedProjects( |
| | | @Argument String searchKeyword, |
| | | @Argument int page, |
| | | @Argument int pageSize, |
| | | @Argument String searchKeyword) { |
| | | log.info("查询我已评审的项目列表,page: {}, pageSize: {}, searchKeyword: {}", page, pageSize, searchKeyword); |
| | | @Argument int pageSize) { |
| | | log.info("查询我已评审的项目列表,searchKeyword: {}, page: {}, pageSize: {}", searchKeyword, page, pageSize); |
| | | |
| | | Long currentJudgeId = userContextUtil.getCurrentJudgeId(); |
| | | if (currentJudgeId == null) { |
| | |
| | | */ |
| | | @QueryMapping |
| | | public ReviewProjectPageResponse studentUnReviewedProjects( |
| | | @Argument String searchKeyword, |
| | | @Argument int page, |
| | | @Argument int pageSize, |
| | | @Argument String searchKeyword) { |
| | | log.info("查询学员未评审的项目列表,page: {}, pageSize: {}, searchKeyword: {}", page, pageSize, searchKeyword); |
| | | @Argument int pageSize) { |
| | | log.info("查询学员未评审的项目列表,searchKeyword: {}, page: {}, pageSize: {}", searchKeyword, page, pageSize); |
| | | |
| | | Long currentJudgeId = userContextUtil.getCurrentJudgeId(); |
| | | if (currentJudgeId == null) { |
| | |
| | | private String gender; |
| | | private String birthday; |
| | | private List<String> roles; |
| | | private String userType; |
| | | private String createdAt; |
| | | private EmployeeInfo employee; |
| | | private JudgeInfo judge; |
| | |
| | | this.roles = roles; |
| | | } |
| | | |
| | | public String getUserType() { |
| | | return userType; |
| | | } |
| | | |
| | | public void setUserType(String userType) { |
| | | this.userType = userType; |
| | | } |
| | | |
| | | public String getCreatedAt() { |
| | | return createdAt; |
| | | } |
| | |
| | | import com.rongyichuang.auth.dto.LoginResponse.JudgeInfo; |
| | | import com.rongyichuang.auth.dto.LoginResponse.PlayerInfo; |
| | | import com.rongyichuang.common.util.UserContextUtil; |
| | | import com.rongyichuang.auth.util.JwtUtil; |
| | | import com.rongyichuang.employee.entity.Employee; |
| | | import com.rongyichuang.employee.service.EmployeeService; |
| | | import com.rongyichuang.judge.entity.Judge; |
| | |
| | | import com.rongyichuang.common.repository.MediaRepository; |
| | | import com.rongyichuang.common.entity.Media; |
| | | import com.rongyichuang.common.enums.MediaTargetType; |
| | | import com.rongyichuang.media.service.MediaV2Service; |
| | | import com.rongyichuang.media.dto.MediaSaveInput; |
| | | import com.rongyichuang.media.dto.MediaSaveResponse; |
| | | import com.rongyichuang.user.dto.response.UserProfile; |
| | | import com.rongyichuang.user.dto.response.UserStats; |
| | | import com.rongyichuang.user.dto.response.UserRegistration; |
| | |
| | | @Autowired |
| | | private UserContextUtil userContextUtil; |
| | | |
| | | @Autowired |
| | | private JwtUtil jwtUtil; |
| | | |
| | | @Autowired |
| | | private MediaV2Service mediaV2Service; |
| | | |
| | | @Value("${app.media-url}") |
| | | private String mediaBaseUrl; |
| | | |
| | |
| | | try { |
| | | Long userId = userContextUtil.getCurrentUserId(); |
| | | logger.debug("进入userProfile,解析到用户ID: {}", userId); |
| | | |
| | | // 如果是匿名用户,返回基本的用户档案 |
| | | if (userId == null) { |
| | | throw new RuntimeException("用户未登录"); |
| | | logger.debug("匿名用户访问userProfile,返回基本档案"); |
| | | UserProfile profile = new UserProfile(); |
| | | profile.setId("0"); // 匿名用户ID设为0 |
| | | profile.setName("匿名用户"); |
| | | profile.setRoles(new ArrayList<>()); |
| | | profile.setUserType("user"); // 匿名用户类型为普通用户 |
| | | profile.setCreatedAt(java.time.LocalDateTime.now().toString()); |
| | | return profile; |
| | | } |
| | | |
| | | UserProfile profile = new UserProfile(); |
| | |
| | | User user = null; |
| | | if (userOpt.isPresent()) { |
| | | user = userOpt.get(); |
| | | // 设置基本信息(姓名和电话号码) |
| | | profile.setName(user.getName()); |
| | | profile.setPhone(user.getPhone()); |
| | | // 设置性别 |
| | | if (user.getGender() != null) { |
| | | profile.setGender(user.getGender() == 1 ? "MALE" : "FEMALE"); |
| | |
| | | Employee employee = employeeService.findByUserId(userId); |
| | | logger.debug("员工查询结果: {}", employee != null ? ("id=" + employee.getId() + ", name=" + employee.getName()) : "无"); |
| | | if (employee != null) { |
| | | profile.setName(employee.getName()); |
| | | profile.setPhone(employee.getPhone()); |
| | | // 如果员工信息存在,则覆盖基本信息 |
| | | if (employee.getName() != null) { |
| | | profile.setName(employee.getName()); |
| | | } |
| | | if (employee.getPhone() != null) { |
| | | profile.setPhone(employee.getPhone()); |
| | | } |
| | | roles.add("EMPLOYEE"); |
| | | |
| | | EmployeeInfo employeeInfo = new EmployeeInfo( |
| | |
| | | Judge judge = judgeService.findByUserId(userId); |
| | | logger.debug("评委查询结果: {}", judge != null ? ("id=" + judge.getId() + ", name=" + judge.getName()) : "无"); |
| | | if (judge != null) { |
| | | if (profile.getName() == null) { |
| | | // 如果评委信息存在且基本信息为空,则设置评委信息 |
| | | if (profile.getName() == null && judge.getName() != null) { |
| | | profile.setName(judge.getName()); |
| | | } |
| | | if (profile.getPhone() == null && judge.getPhone() != null) { |
| | | profile.setPhone(judge.getPhone()); |
| | | } |
| | | roles.add("JUDGE"); |
| | |
| | | Player player = playerService.findByUserId(userId); |
| | | logger.debug("学员查询结果: {}", player != null ? ("id=" + player.getId() + ", name=" + player.getName()) : "无"); |
| | | if (player != null) { |
| | | if (profile.getName() == null) { |
| | | // 如果学员信息存在且基本信息为空,则设置学员信息 |
| | | if (profile.getName() == null && player.getName() != null) { |
| | | profile.setName(player.getName()); |
| | | } |
| | | if (profile.getPhone() == null && player.getPhone() != null) { |
| | | profile.setPhone(player.getPhone()); |
| | | } |
| | | roles.add("PLAYER"); |
| | |
| | | } |
| | | |
| | | profile.setRoles(roles); |
| | | |
| | | // 确定主要角色类型(优先级:employee > judge > player) |
| | | String userType; |
| | | if (employee != null) { |
| | | userType = "employee"; |
| | | } else if (judge != null) { |
| | | userType = "judge"; |
| | | } else if (player != null) { |
| | | userType = "player"; |
| | | } else { |
| | | userType = "user"; // 普通用户 |
| | | } |
| | | profile.setUserType(userType); |
| | | logger.debug("设置用户类型: {}", userType); |
| | | |
| | | // 获取用户头像 |
| | | try { |
| | |
| | | @MutationMapping |
| | | public UserProfileInfo saveUserInfo(@Argument UserInput input) { |
| | | try { |
| | | Long userId = userContextUtil.getCurrentUserId(); |
| | | logger.debug("进入saveUserInfo,解析到用户ID: {}", userId); |
| | | Long userId = userContextUtil.getCurrentUserIdIncludingAnonymous(); |
| | | logger.debug("进入saveUserInfo,解析到用户ID(包括匿名用户): {}", userId); |
| | | if (userId == null) { |
| | | throw new RuntimeException("用户未登录"); |
| | | } |
| | | |
| | | // 查找现有用户 |
| | | Optional<User> userOpt = userRepository.findById(userId); |
| | | User user; |
| | | boolean isNewUser = false; |
| | | |
| | | if (userOpt.isPresent()) { |
| | | // 用户存在,更新信息 |
| | | user = userOpt.get(); |
| | | logger.debug("更新现有用户信息,用户ID: {}", userId); |
| | | // 检查手机号是否存在 |
| | | if (StringUtils.hasText(input.getPhone())) { |
| | | Optional<User> existingUserByPhone = userRepository.findByPhone(input.getPhone()); |
| | | if (existingUserByPhone.isPresent()) { |
| | | // 手机号已存在,更新现有用户 |
| | | user = existingUserByPhone.get(); |
| | | logger.debug("手机号{}已存在,更新现有用户,用户ID: {}", input.getPhone(), user.getId()); |
| | | |
| | | // 如果当前用户ID与手机号对应的用户ID不同,需要处理匿名用户转正的情况 |
| | | if (!user.getId().equals(userId)) { |
| | | logger.debug("匿名用户{}转正为正式用户{}", userId, user.getId()); |
| | | |
| | | // 检查匿名用户是否有需要合并的信息 |
| | | if (userId < 0) { |
| | | // 这是匿名用户转正,检查是否有临时信息需要合并 |
| | | Optional<User> anonymousUserOpt = userRepository.findById(userId); |
| | | if (anonymousUserOpt.isPresent()) { |
| | | User anonymousUser = anonymousUserOpt.get(); |
| | | logger.debug("发现匿名用户记录,准备合并信息"); |
| | | |
| | | // 合并匿名用户的信息到正式用户(如果正式用户没有这些信息) |
| | | if (!StringUtils.hasText(user.getName()) && StringUtils.hasText(anonymousUser.getName())) { |
| | | user.setName(anonymousUser.getName()); |
| | | logger.debug("合并匿名用户的姓名: {}", anonymousUser.getName()); |
| | | } |
| | | |
| | | if (user.getGender() == null && anonymousUser.getGender() != null) { |
| | | user.setGender(anonymousUser.getGender()); |
| | | logger.debug("合并匿名用户的性别: {}", anonymousUser.getGender()); |
| | | } |
| | | |
| | | if (user.getBirthday() == null && anonymousUser.getBirthday() != null) { |
| | | user.setBirthday(anonymousUser.getBirthday()); |
| | | logger.debug("合并匿名用户的生日: {}", anonymousUser.getBirthday()); |
| | | } |
| | | |
| | | // 删除匿名用户记录(可选,避免数据冗余) |
| | | try { |
| | | userRepository.delete(anonymousUser); |
| | | logger.debug("删除匿名用户记录: {}", userId); |
| | | } catch (Exception e) { |
| | | logger.warn("删除匿名用户记录失败: {}", e.getMessage()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | userId = user.getId(); // 使用正式用户ID |
| | | } |
| | | } else { |
| | | // 手机号不存在,检查当前用户ID是否存在 |
| | | Optional<User> userOpt = userRepository.findById(userId); |
| | | if (userOpt.isPresent()) { |
| | | // 用户ID存在,更新信息 |
| | | user = userOpt.get(); |
| | | logger.debug("更新现有用户信息,用户ID: {}", userId); |
| | | } else { |
| | | // 用户ID不存在,创建新用户 |
| | | user = new User(); |
| | | user.setId(userId); |
| | | isNewUser = true; |
| | | logger.debug("创建新用户,用户ID: {}", userId); |
| | | } |
| | | } |
| | | } else { |
| | | // 用户不存在,创建新用户 |
| | | user = new User(); |
| | | user.setId(userId); |
| | | logger.debug("创建新用户,用户ID: {}", userId); |
| | | // 没有提供手机号,直接根据用户ID查找或创建 |
| | | Optional<User> userOpt = userRepository.findById(userId); |
| | | if (userOpt.isPresent()) { |
| | | user = userOpt.get(); |
| | | logger.debug("更新现有用户信息,用户ID: {}", userId); |
| | | } else { |
| | | user = new User(); |
| | | user.setId(userId); |
| | | isNewUser = true; |
| | | logger.debug("创建新用户,用户ID: {}", userId); |
| | | } |
| | | } |
| | | |
| | | // 更新用户基本信息(不包含头像) |
| | | user.setName(input.getName()); |
| | | user.setPhone(input.getPhone()); |
| | | if (StringUtils.hasText(input.getName())) { |
| | | user.setName(input.getName()); |
| | | } |
| | | if (StringUtils.hasText(input.getPhone())) { |
| | | user.setPhone(input.getPhone()); |
| | | } |
| | | |
| | | // 处理性别转换:MALE -> 1, FEMALE -> 0 |
| | | if ("MALE".equals(input.getGender())) { |
| | |
| | | logger.warn("生日格式解析失败: {}", input.getBirthday(), e); |
| | | } |
| | | } |
| | | |
| | | // 处理wxopenid更新:从JWT token中获取当前用户的wxopenid |
| | | try { |
| | | String token = userContextUtil.getTokenFromRequest(); |
| | | if (token != null && jwtUtil.validateToken(token)) { |
| | | Long currentUserId = jwtUtil.getUserIdFromToken(token); |
| | | String wxopenidFromToken = jwtUtil.getWxOpenidFromToken(token); |
| | | |
| | | // 如果token中包含wxopenid,则更新用户的wxopenid |
| | | if (StringUtils.hasText(wxopenidFromToken)) { |
| | | logger.debug("从token中获取到wxopenid: {}", wxopenidFromToken); |
| | | |
| | | // 检查这个openid是否已经被其他用户使用 |
| | | Optional<User> existingUserWithOpenid = userRepository.findByWxOpenid(wxopenidFromToken); |
| | | if (existingUserWithOpenid.isEmpty() || existingUserWithOpenid.get().getId().equals(user.getId())) { |
| | | user.setWxOpenid(wxopenidFromToken); |
| | | logger.debug("设置用户wxopenid: {}", wxopenidFromToken); |
| | | } else { |
| | | logger.warn("wxopenid {} 已被其他用户使用,用户ID: {}", wxopenidFromToken, existingUserWithOpenid.get().getId()); |
| | | } |
| | | } else { |
| | | logger.debug("token中未包含wxopenid信息"); |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | logger.warn("处理wxopenid更新时发生异常: {}", e.getMessage(), e); |
| | | } |
| | | |
| | | // 保存用户基本信息 |
| | | user = userRepository.save(user); |
| | | logger.debug("用户信息保存成功,用户ID: {}", user.getId()); |
| | | logger.debug("用户信息保存成功,用户ID: {}, 是否新用户: {}", user.getId(), isNewUser); |
| | | |
| | | // 处理头像保存 |
| | | if (StringUtils.hasText(input.getAvatar())) { |
| | | try { |
| | | logger.debug("开始保存用户头像,路径: {}", input.getAvatar()); |
| | | |
| | | // 构建MediaSaveInput |
| | | MediaSaveInput mediaSaveInput = new MediaSaveInput(); |
| | | mediaSaveInput.setTargetType("player"); // 使用"player"作为目标类型 |
| | | mediaSaveInput.setTargetId(user.getId()); |
| | | mediaSaveInput.setPath(input.getAvatar()); |
| | | mediaSaveInput.setMediaType(1); // 1表示图片 |
| | | mediaSaveInput.setFileSize(0L); // 设置默认文件大小为0,避免数据库约束错误 |
| | | |
| | | // 从路径中提取文件名和扩展名 |
| | | String fileName = input.getAvatar(); |
| | | if (fileName.contains("/")) { |
| | | fileName = fileName.substring(fileName.lastIndexOf("/") + 1); |
| | | } |
| | | mediaSaveInput.setFileName(fileName); |
| | | |
| | | if (fileName.contains(".")) { |
| | | String fileExt = fileName.substring(fileName.lastIndexOf(".") + 1); |
| | | mediaSaveInput.setFileExt(fileExt); |
| | | } |
| | | |
| | | // 保存头像媒体记录 |
| | | MediaSaveResponse saveResponse = mediaV2Service.saveMedia(mediaSaveInput); |
| | | if (saveResponse.getSuccess()) { |
| | | logger.debug("头像保存成功,媒体ID: {}", saveResponse.getMediaId()); |
| | | } else { |
| | | logger.warn("头像保存失败: {}", saveResponse.getMessage()); |
| | | } |
| | | } catch (Exception e) { |
| | | logger.error("保存头像时发生错误", e); |
| | | // 头像保存失败不影响用户信息保存 |
| | | } |
| | | } |
| | | |
| | | // 构建返回结果 |
| | | UserProfileInfo result = new UserProfileInfo(); |
| | |
| | | try { |
| | | List<Media> avatarMedias = mediaRepository.findByTargetTypeAndTargetIdAndState( |
| | | MediaTargetType.USER_AVATAR.getValue(), |
| | | userId, |
| | | user.getId(), |
| | | 1 |
| | | ); |
| | | if (!avatarMedias.isEmpty()) { |
| | | Media avatarMedia = avatarMedias.get(0); |
| | | result.setAvatar(buildFullMediaUrl(avatarMedia.getPath())); |
| | | logger.debug("设置头像URL: {}", result.getAvatar()); |
| | | } |
| | | } catch (Exception e) { |
| | | logger.warn("获取头像失败: {}", e.getMessage(), e); |
| | |
| | | boolean needUpdateWx = false; |
| | | if (currentWxOpenid != null && !currentWxOpenid.trim().isEmpty()) { |
| | | if (user.getWxOpenid() == null || !currentWxOpenid.equals(user.getWxOpenid())) { |
| | | user.setWxOpenid(currentWxOpenid); |
| | | needUpdateWx = true; |
| | | // 检查这个openid是否已经被其他用户使用 |
| | | Optional<User> existingUserWithOpenid = userRepository.findByWxOpenid(currentWxOpenid); |
| | | if (existingUserWithOpenid.isEmpty() || existingUserWithOpenid.get().getId().equals(user.getId())) { |
| | | user.setWxOpenid(currentWxOpenid); |
| | | needUpdateWx = true; |
| | | } |
| | | } |
| | | } |
| | | if (currentWxUnionid != null && !currentWxUnionid.trim().isEmpty()) { |
| | | if (user.getWxUnionid() == null || !currentWxUnionid.equals(user.getWxUnionid())) { |
| | | user.setWxUnionid(currentWxUnionid); |
| | | needUpdateWx = true; |
| | | // 检查这个unionid是否已经被其他用户使用 |
| | | Optional<User> existingUserWithUnionid = userRepository.findByWxUnionid(currentWxUnionid); |
| | | if (existingUserWithUnionid.isEmpty() || existingUserWithUnionid.get().getId().equals(user.getId())) { |
| | | user.setWxUnionid(currentWxUnionid); |
| | | needUpdateWx = true; |
| | | } |
| | | } |
| | | } |
| | | if (needUpdateWx) { |
| | |
| | | base-url: https://rych.9village.cn |
| | | jwt: |
| | | secret: ryc-jwt-secret-key-2024-secure-256bit-hmac-sha-algorithm-compatible |
| | | expiration: 7200000 # 2小时 |
| | | expiration: 86400000 # 24小时 |
| | | media-url: https://ryc-1379367838.cos.ap-chengdu.myqcloud.com |
| | | |
| | | # 微信小程序配置 |
| | |
| | | } |
| | | |
| | | extend type Mutation { |
| | | # 认证相关接口已迁移到RESTful API (/auth/*) |
| | | _: Boolean |
| | | # 微信登录 |
| | | wxLogin(input: WxLoginRequest!): WxLoginResponse |
| | | |
| | | # Web端登录 |
| | | webLogin(input: LoginRequest!): LoginResponse |
| | | |
| | | # 解密微信手机号(旧版API) |
| | | decryptPhoneNumber(encryptedData: String!, iv: String!, sessionKey: String!): PhoneDecryptResponse |
| | | |
| | | # 获取微信手机号(新版API) |
| | | getPhoneNumberByCode(code: String!): PhoneDecryptResponse |
| | | } |
| | | |
| | | input LoginRequest { |
| | |
| | | isNewUser: Boolean |
| | | loginRecordId: Long |
| | | sessionKey: String |
| | | success: Boolean |
| | | message: String |
| | | hasEmployee: Boolean |
| | | hasJudge: Boolean |
| | | hasPlayer: Boolean |
| | | } |
| | | |
| | | type UserInfo { |
| | |
| | | gender: String |
| | | birthday: String |
| | | roles: [String!]! |
| | | userType: String |
| | | createdAt: String |
| | | # 角色相关信息 |
| | | employee: EmployeeInfo |
| | |
| | | <el-button |
| | | type="primary" |
| | | size="small" |
| | | @click="previewAttachment(attachment)" |
| | | > |
| | | 预览 |
| | | </el-button> |
| | | <el-button |
| | | type="primary" |
| | | size="small" |
| | | @click="downloadAttachment(attachment)" |
| | | > |
| | | 下载 |
| | |
| | | <div class="card-header"> |
| | | <span>审核管理</span> |
| | | </div> |
| | | </template> |
| | | <!-- 附件预览对话框 --> |
| | | <el-dialog v-model="previewVisible" title="文件预览" width="80%" center> |
| | | <div class="preview-content"> |
| | | <!-- 图片预览 --> |
| | | <img v-if="previewType === 'image' && previewUrl" :src="previewUrl" style="max-width: 100%; max-height: 70vh; object-fit: contain;" /> |
| | | <!-- 视频预览 --> |
| | | <video v-else-if="previewType === 'video' && previewUrl" :src="previewUrl" controls style="width: 100%; max-height: 70vh;"></video> |
| | | <!-- PDF 预览 --> |
| | | <iframe v-else-if="previewType === 'pdf' && previewUrl" :src="previewUrl" style="width: 100%; height: 70vh; border: none;"></iframe> |
| | | <!-- DOCX 预览 --> |
| | | <div v-else-if="previewType === 'docx'" ref="docxContainer" class="docx-preview"></div> |
| | | <!-- 其它不支持 --> |
| | | <div v-else class="preview-error"> |
| | | <el-empty description="无法预览此文件类型,请下载查看" /> |
| | | </div> |
| | | </div> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <div class="review-section"> |
| | | <div class="review-status"> |
| | |
| | | const playerData = ref<any>(null) |
| | | const activityPlayerData = ref<any>(null) |
| | | const attachments = ref<any[]>([]) |
| | | |
| | | // 预览相关 |
| | | const previewVisible = ref(false) |
| | | const previewUrl = ref('') |
| | | const previewType = ref<'' | 'image' | 'video' | 'pdf' | 'docx' | 'unknown'>('') |
| | | const docxContainer = ref<HTMLElement | null>(null) |
| | | |
| | | // 审核相关数据 |
| | | const feedbackText = ref('') |
| | |
| | | const downloadAttachment = (attachment: any) => { |
| | | // TODO: 实现附件下载功能 |
| | | window.open(attachment.url, '_blank') |
| | | } |
| | | |
| | | /** |
| | | * 预览附件:按扩展名分发 图片/视频/PDF/DOCX |
| | | */ |
| | | const previewAttachment = async (attachment: any) => { |
| | | const name: string = attachment.originalName || attachment.name || '' |
| | | const url: string = attachment.url |
| | | const ext = (name.split('.').pop() || '').toLowerCase() |
| | | |
| | | previewVisible.value = true |
| | | previewUrl.value = '' |
| | | previewType.value = '' |
| | | |
| | | const imageExts = ['jpg','jpeg','png','gif','bmp','webp'] |
| | | const videoExts = ['mp4','webm','ogg','avi','mov','wmv','flv','mkv'] |
| | | |
| | | if (imageExts.includes(ext)) { |
| | | previewType.value = 'image' |
| | | previewUrl.value = url |
| | | return |
| | | } |
| | | if (videoExts.includes(ext)) { |
| | | previewType.value = 'video' |
| | | previewUrl.value = url |
| | | return |
| | | } |
| | | if (ext === 'pdf') { |
| | | previewType.value = 'pdf' |
| | | previewUrl.value = url |
| | | return |
| | | } |
| | | if (ext === 'docx') { |
| | | previewType.value = 'docx' |
| | | try { |
| | | await renderDocx(url) |
| | | } catch (e: any) { |
| | | console.error('DOCX 预览失败:', e) |
| | | ElMessage.warning('DOCX 预览失败,建议下载查看') |
| | | previewType.value = 'unknown' |
| | | } |
| | | return |
| | | } |
| | | if (ext === 'doc') { |
| | | ElMessage.info('暂不支持 .doc 预览,请下载查看') |
| | | previewType.value = 'unknown' |
| | | return |
| | | } |
| | | |
| | | ElMessage.warning('此文件类型不支持预览,请下载查看') |
| | | previewType.value = 'unknown' |
| | | } |
| | | |
| | | /** |
| | | * 动态加载 docx-preview 并渲染 DOCX |
| | | */ |
| | | const renderDocx = async (url: string) => { |
| | | // 动态加载 docx-preview |
| | | const ensureScript = () => new Promise((resolve, reject) => { |
| | | if ((window as any).docx && (window as any).docx.renderAsync) return resolve(true) |
| | | const existed = document.querySelector('script[data-docx-preview]') |
| | | if (existed) { |
| | | existed.addEventListener('load', () => resolve(true)) |
| | | existed.addEventListener('error', reject) |
| | | return |
| | | } |
| | | const s = document.createElement('script') |
| | | s.src = 'https://unpkg.com/docx-preview/dist/docx-preview.min.js' |
| | | s.async = true |
| | | s.setAttribute('data-docx-preview', '1') |
| | | s.onload = () => resolve(true) |
| | | s.onerror = reject |
| | | document.head.appendChild(s) |
| | | }) |
| | | |
| | | // 规范化 URL(支持相对路径) |
| | | const buildUrl = (u: string) => { |
| | | if (!u) return u |
| | | if (u.startsWith('http://') || u.startsWith('https://')) return u |
| | | if (u.startsWith('/')) return location.origin + u |
| | | return u |
| | | } |
| | | |
| | | // 携带鉴权头访问附件(很多文件服务不走 cookie,而是 JWT Header) |
| | | const { getToken } = await import('@/utils/auth') |
| | | const token = getToken ? getToken() : '' |
| | | const requestUrl = buildUrl(url) |
| | | |
| | | let res: Response |
| | | try { |
| | | res = await fetch(requestUrl, { |
| | | credentials: 'include', |
| | | headers: token ? { Authorization: `Bearer ${token}` } : undefined, |
| | | mode: 'cors' |
| | | } as RequestInit) |
| | | } catch (e) { |
| | | throw new Error('获取 DOCX 失败: 网络不可达或被拦截') |
| | | } |
| | | if (!res.ok) throw new Error('获取 DOCX 失败: ' + res.status) |
| | | |
| | | const blob = await res.blob() |
| | | |
| | | await ensureScript() |
| | | if (docxContainer.value) { |
| | | docxContainer.value.innerHTML = '' |
| | | await (window as any).docx.renderAsync(blob, docxContainer.value, null, { inWrapper: true }) |
| | | } |
| | | } |
| | | |
| | | // 审核通过 |
| | |
| | | grid-template-columns: 1fr; |
| | | } |
| | | } |
| | | .preview-content { |
| | | text-align: center; |
| | | } |
| | | |
| | | .preview-error { |
| | | padding: 40px 0; |
| | | } |
| | | |
| | | .docx-preview { |
| | | text-align: left; |
| | | max-height: 70vh; |
| | | overflow: auto; |
| | | background: #fff; |
| | | padding: 12px; |
| | | } |
| | | </style> |
| | |
| | | <!-- 文件预览对话框 --> |
| | | <el-dialog v-model="previewVisible" title="文件预览" width="80%" center> |
| | | <div class="preview-content"> |
| | | <iframe |
| | | v-if="previewUrl" |
| | | :src="previewUrl" |
| | | style="width: 100%; height: 500px; border: none;" |
| | | ></iframe> |
| | | <!-- 图片预览 --> |
| | | <img v-if="previewType === 'image' && previewUrl" :src="previewUrl" style="max-width: 100%; max-height: 70vh; object-fit: contain;" /> |
| | | <!-- 视频预览 --> |
| | | <video v-else-if="previewType === 'video' && previewUrl" :src="previewUrl" controls style="width: 100%; max-height: 70vh;"></video> |
| | | <!-- PDF 预览 --> |
| | | <iframe v-else-if="previewType === 'pdf' && previewUrl" :src="previewUrl" style="width: 100%; height: 70vh; border: none;"></iframe> |
| | | <!-- DOCX 预览 --> |
| | | <div v-else-if="previewType === 'docx'" ref="docxContainer" class="docx-preview"></div> |
| | | <!-- 其它不支持 --> |
| | | <div v-else class="preview-error"> |
| | | <el-empty description="无法预览此文件类型" /> |
| | | <el-empty description="无法预览此文件类型,请下载查看" /> |
| | | </div> |
| | | </div> |
| | | </el-dialog> |
| | |
| | | const ratingComment = ref('') |
| | | const previewVisible = ref(false) |
| | | const previewUrl = ref('') |
| | | const previewType = ref('') // image | video | pdf | docx | unknown |
| | | const docxContainer = ref(null) |
| | | |
| | | // 权限验证相关 |
| | | const currentJudge = ref(null) |
| | |
| | | } |
| | | } |
| | | |
| | | // 文件预览 |
| | | const previewFile = (file) => { |
| | | // 根据文件类型决定预览方式 |
| | | const fileExtension = file.name.split('.').pop().toLowerCase() |
| | | const previewableTypes = ['pdf', 'txt', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'mp4', 'avi', 'mov', 'wmv', 'flv', 'webm'] |
| | | |
| | | if (previewableTypes.includes(fileExtension)) { |
| | | // 在新窗口中打开预览 |
| | | window.open(file.url, '_blank') |
| | | } else { |
| | | ElMessage.warning('此文件类型不支持预览,请下载查看') |
| | | /** |
| | | * 文件预览:按扩展名分发到图片/视频/PDF/DOCX |
| | | */ |
| | | const previewFile = async (file) => { |
| | | const ext = (file.name?.split('.').pop() || '').toLowerCase() |
| | | previewVisible.value = true |
| | | previewUrl.value = '' |
| | | previewType.value = '' |
| | | // 统一取可用的完整 URL |
| | | const url = file.url || file.fullUrl || file.full_path || file.path |
| | | |
| | | const imageExts = ['jpg','jpeg','png','gif','bmp','webp'] |
| | | const videoExts = ['mp4','webm','ogg','avi','mov','wmv','flv','mkv'] |
| | | |
| | | if (imageExts.includes(ext)) { |
| | | previewType.value = 'image' |
| | | previewUrl.value = url |
| | | return |
| | | } |
| | | if (videoExts.includes(ext)) { |
| | | previewType.value = 'video' |
| | | previewUrl.value = url |
| | | return |
| | | } |
| | | if (ext === 'pdf') { |
| | | previewType.value = 'pdf' |
| | | previewUrl.value = url |
| | | return |
| | | } |
| | | if (ext === 'docx') { |
| | | previewType.value = 'docx' |
| | | try { |
| | | await renderDocx(url) |
| | | } catch (e) { |
| | | console.error('DOCX 预览失败:', e) |
| | | ElMessage.warning('DOCX 预览失败,建议下载查看') |
| | | previewType.value = 'unknown' |
| | | } |
| | | return |
| | | } |
| | | if (ext === 'doc') { |
| | | ElMessage.info('暂不支持 .doc 预览,请下载查看') |
| | | previewType.value = 'unknown' |
| | | return |
| | | } |
| | | |
| | | ElMessage.warning('此文件类型不支持预览,请下载查看') |
| | | previewType.value = 'unknown' |
| | | } |
| | | |
| | | /** |
| | | * 动态加载 docx-preview 并渲染 DOCX |
| | | */ |
| | | const renderDocx = async (url) => { |
| | | const ensureScript = () => new Promise((resolve, reject) => { |
| | | if (window.docx && window.docx.renderAsync) return resolve(true) |
| | | const existed = document.querySelector('script[data-docx-preview]') |
| | | if (existed) { |
| | | existed.addEventListener('load', () => resolve(true)) |
| | | existed.addEventListener('error', reject) |
| | | return |
| | | } |
| | | const s = document.createElement('script') |
| | | s.src = 'https://unpkg.com/docx-preview/dist/docx-preview.min.js' |
| | | s.async = true |
| | | s.setAttribute('data-docx-preview', '1') |
| | | s.onload = () => resolve(true) |
| | | s.onerror = reject |
| | | document.head.appendChild(s) |
| | | }) |
| | | |
| | | await ensureScript() |
| | | const res = await fetch(url, { credentials: 'include' }) |
| | | if (!res.ok) throw new Error('获取 DOCX 失败: ' + res.status) |
| | | const blob = await res.blob() |
| | | if (docxContainer.value) { |
| | | docxContainer.value.innerHTML = '' |
| | | await window.docx.renderAsync(blob, docxContainer.value, null, { inWrapper: true }) |
| | | } |
| | | } |
| | | |
| | |
| | | .preview-content { |
| | | text-align: center; |
| | | } |
| | | .docx-preview { |
| | | text-align: left; |
| | | max-height: 70vh; |
| | | overflow: auto; |
| | | background: #fff; |
| | | padding: 12px; |
| | | } |
| | | |
| | | .preview-error { |
| | | padding: 40px 0; |
| | |
| | | } |
| | | |
| | | // 检查是否有错误信息(适配不同的错误响应格式) |
| | | if (res.data.error || res.data.message || res.data.success === false) { |
| | | if (res.data.error || res.data.success === false) { |
| | | const errorMsg = res.data.error || res.data.message || '登录失败' |
| | | console.error('❌ 登录失败:', errorMsg) |
| | | wx.showToast({ |
| | |
| | | // GraphQL请求封装 |
| | | graphqlRequest(query, variables = {}) { |
| | | return new Promise((resolve, reject) => { |
| | | this._makeGraphQLRequest(query, variables, resolve, reject, false) |
| | | }) |
| | | }, |
| | | |
| | | // 内部GraphQL请求方法,支持重试机制 |
| | | _makeGraphQLRequest(query, variables, resolve, reject, isRetry = false) { |
| | | // 确保token的一致性:优先使用globalData中的token,如果没有则从storage获取 |
| | | let token = this.globalData.token |
| | | if (!token) { |
| | | token = wx.getStorageSync('token') |
| | | if (token) { |
| | | this.globalData.token = token // 同步到globalData |
| | | // 确保token的一致性:优先使用globalData中的token,如果没有则从storage获取 |
| | | let token = this.globalData.token |
| | | if (!token) { |
| | | token = wx.getStorageSync('token') |
| | | if (token) { |
| | | this.globalData.token = token // 同步到globalData |
| | | } |
| | | } |
| | | } |
| | | |
| | | wx.request({ |
| | | url: this.globalData.baseUrl, |
| | | method: 'POST', |
| | | header: { |
| | | 'Content-Type': 'application/json', |
| | | 'Authorization': token ? `Bearer ${token}` : '' |
| | | }, |
| | | data: { |
| | | query: query, |
| | | variables: variables |
| | | }, |
| | | success: (res) => { |
| | | console.log('GraphQL响应:', res.data) |
| | | |
| | | // 检查HTTP状态码 |
| | | if (res.statusCode !== 200) { |
| | | // 对于401状态码,可能是认证错误,需要检查响应内容 |
| | | if (res.statusCode === 401 && res.data && res.data.errors) { |
| | | console.log('收到401状态码,检查是否为认证错误') |
| | | // 继续处理,让下面的GraphQL错误检查逻辑处理认证错误 |
| | | } else { |
| | | wx.request({ |
| | | url: this.globalData.baseUrl, |
| | | method: 'POST', |
| | | header: { |
| | | 'Content-Type': 'application/json', |
| | | 'Authorization': token ? `Bearer ${token}` : '' |
| | | }, |
| | | data: { |
| | | query: query, |
| | | variables: variables |
| | | }, |
| | | success: (res) => { |
| | | console.log('GraphQL响应:', res.data) |
| | | |
| | | // 检查HTTP状态码 |
| | | if (res.statusCode !== 200) { |
| | | console.error('GraphQL HTTP错误:', res.statusCode) |
| | | reject(new Error(`HTTP错误: ${res.statusCode}`)) |
| | | return |
| | | } |
| | | } |
| | | |
| | | // 检查GraphQL错误 |
| | | if (res.data && res.data.errors) { |
| | | console.error('GraphQL错误:', res.data.errors) |
| | | |
| | | // 检查是否是认证错误(token过期或无效) |
| | | const authErrors = res.data.errors.filter(error => |
| | | error.message && ( |
| | | error.message.includes('没有权限访问') || |
| | | error.message.includes('请先登录') || |
| | | error.message.includes('UNAUTHORIZED') || |
| | | error.extensions?.code === 'UNAUTHORIZED' |
| | | ) |
| | | ) |
| | | |
| | | if (authErrors.length > 0 && !isRetry) { |
| | | console.log('🔄 检测到认证错误,尝试重新登录...') |
| | | // 清除过期的认证信息 |
| | | this.globalData.token = null |
| | | this.globalData.userInfo = null |
| | | this.globalData.sessionKey = null |
| | | wx.removeStorageSync('token') |
| | | wx.removeStorageSync('userInfo') |
| | | wx.removeStorageSync('sessionKey') |
| | | |
| | | // 重新登录 |
| | | wx.login({ |
| | | success: (loginRes) => { |
| | | if (loginRes.code) { |
| | | console.log('🔄 重新获取微信code成功,调用后端登录...') |
| | | this._retryAfterLogin(loginRes.code, query, variables, resolve, reject) |
| | | } else { |
| | | console.error('❌ 重新获取微信code失败') |
| | | reject(new Error('重新登录失败')) |
| | | } |
| | | }, |
| | | fail: (err) => { |
| | | console.error('❌ 重新登录失败:', err) |
| | | reject(new Error('重新登录失败')) |
| | | } |
| | | }) |
| | | // 检查GraphQL错误 |
| | | if (res.data.errors) { |
| | | console.error('GraphQL错误:', res.data.errors) |
| | | reject(new Error(res.data.errors[0]?.message || 'GraphQL请求错误')) |
| | | return |
| | | } |
| | | |
| | | reject(new Error(res.data.errors[0]?.message || 'GraphQL请求错误')) |
| | | return |
| | | } |
| | | |
| | | // 检查数据 |
| | | if (res.data.data !== undefined) { |
| | | resolve(res.data.data) |
| | | } else { |
| | | console.error('GraphQL响应异常:', res.data) |
| | | reject(new Error('GraphQL响应数据异常')) |
| | | } |
| | | }, |
| | | fail: (err) => { |
| | | console.error('GraphQL网络请求失败:', err) |
| | | reject(new Error('网络请求失败')) |
| | | } |
| | | }) |
| | | }, |
| | | |
| | | // 重新登录后重试GraphQL请求 |
| | | _retryAfterLogin(code, query, variables, resolve, reject) { |
| | | const that = this |
| | | const deviceInfo = this.getDeviceInfo() |
| | | const requestData = { |
| | | code: code, |
| | | loginIp: '127.0.0.1', // 小程序无法获取真实IP,使用默认值 |
| | | deviceInfo: deviceInfo |
| | | } |
| | | |
| | | wx.request({ |
| | | url: 'http://localhost:8080/api/auth/wx-login', |
| | | method: 'POST', |
| | | header: { |
| | | 'Content-Type': 'application/json' |
| | | }, |
| | | data: requestData, |
| | | success: (res) => { |
| | | console.log('🔄 重新登录响应:', res.data) |
| | | |
| | | if (res.statusCode !== 200 || res.data.error) { |
| | | console.error('❌ 重新登录失败:', res.data.error || res.data.message) |
| | | reject(new Error('重新登录失败')) |
| | | return |
| | | } |
| | | |
| | | // 检查响应数据格式 |
| | | let loginResult = null |
| | | if (res.data.token && res.data.userInfo) { |
| | | loginResult = res.data |
| | | } else if (res.data.success && res.data.data) { |
| | | loginResult = res.data.data |
| | | } |
| | | |
| | | if (loginResult && loginResult.token) { |
| | | console.log('✅ 重新登录成功,更新token') |
| | | |
| | | // 保存新的登录信息 |
| | | try { |
| | | wx.setStorageSync('token', loginResult.token) |
| | | wx.setStorageSync('userInfo', loginResult.userInfo) |
| | | if (loginResult.sessionKey) { |
| | | wx.setStorageSync('sessionKey', loginResult.sessionKey) |
| | | } |
| | | } catch (storageErr) { |
| | | console.error('❌ 保存重新登录信息失败:', storageErr) |
| | | // 检查数据 |
| | | if (res.data.data !== undefined) { |
| | | resolve(res.data.data) |
| | | } else { |
| | | console.error('GraphQL响应异常:', res.data) |
| | | reject(new Error('GraphQL响应数据异常')) |
| | | } |
| | | |
| | | that.globalData.token = loginResult.token |
| | | that.globalData.userInfo = loginResult.userInfo |
| | | that.globalData.sessionKey = loginResult.sessionKey |
| | | |
| | | // 使用新token重试原始请求 |
| | | console.log('🔄 使用新token重试GraphQL请求...') |
| | | that._makeGraphQLRequest(query, variables, resolve, reject, true) |
| | | } else { |
| | | console.error('❌ 重新登录响应格式错误') |
| | | reject(new Error('重新登录响应格式错误')) |
| | | }, |
| | | fail: (err) => { |
| | | console.error('GraphQL网络请求失败:', err) |
| | | reject(new Error('网络请求失败')) |
| | | } |
| | | }, |
| | | fail: (err) => { |
| | | console.error('❌ 重新登录网络请求失败:', err) |
| | | reject(new Error('重新登录网络请求失败')) |
| | | } |
| | | }) |
| | | }) |
| | | } |
| | | }) |
| | |
| | | // pages/judge/review.js |
| | | const app = getApp() |
| | | const { graphqlRequest, formatDate: formatDateUtil } = require('../../lib/utils') |
| | | const { graphqlRequest, formatDate } = require('../../lib/utils') |
| | | |
| | | Page({ |
| | | data: { |
| | |
| | | // 提交作品信息 |
| | | submission: null, |
| | | activityPlayerId: '', |
| | | stageId: null, |
| | | submissionId: null, |
| | | |
| | | |
| | | // 活动信息 |
| | | activity: null, |
| | | |
| | | |
| | | // 评审标准 |
| | | criteria: [], |
| | | |
| | | |
| | | // 评分数据 |
| | | scores: {}, |
| | | |
| | |
| | | reviewStatus: 'PENDING', // PENDING, COMPLETED |
| | | |
| | | // 已有评审记录 |
| | | existingReview: null |
| | | existingReview: null, |
| | | |
| | | // 媒体预览 |
| | | showMediaPreview: false, |
| | | currentMedia: null, |
| | | mediaType: 'image', |
| | | |
| | | // 文件下载 |
| | | downloadingFiles: [], |
| | | |
| | | // 评分等级 |
| | | scoreOptions: [ |
| | | { value: 1, label: '1分 - 很差' }, |
| | | { value: 2, label: '2分 - 较差' }, |
| | | { value: 3, label: '3分 - 一般' }, |
| | | { value: 4, label: '4分 - 良好' }, |
| | | { value: 5, label: '5分 - 优秀' } |
| | | ] |
| | | }, |
| | | |
| | | onLoad(options) { |
| | |
| | | // 页面显示时检查评审状态 |
| | | if (this.data.submissionId) { |
| | | this.checkReviewStatus() |
| | | } |
| | | }, |
| | | |
| | | transformMediaFile(file) { |
| | | const url = file.fullUrl || file.url |
| | | const thumbUrl = file.fullThumbUrl || url |
| | | const ext = (file.fileExt || '').toLowerCase() |
| | | let mediaType = 'file' |
| | | |
| | | if (file.mediaType === 1 || ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'heic'].includes(ext)) { |
| | | mediaType = 'image' |
| | | } else if (file.mediaType === 2 || ['mp4', 'mov', 'avi', 'wmv', 'mkv', 'webm', 'flv'].includes(ext)) { |
| | | mediaType = 'video' |
| | | } else if (ext === 'pdf') { |
| | | mediaType = 'pdf' |
| | | } else if (['doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx', 'wps', 'txt', 'rtf'].includes(ext)) { |
| | | mediaType = 'word' |
| | | } |
| | | |
| | | return { |
| | | id: file.id, |
| | | name: file.name, |
| | | url, |
| | | thumbUrl, |
| | | mediaType, |
| | | size: file.fileSize || 0 |
| | | } |
| | | }, |
| | | |
| | |
| | | submissionFiles { |
| | | id |
| | | name |
| | | url |
| | | fullUrl |
| | | fullThumbUrl |
| | | fileExt |
| | | fileSize |
| | | mediaType |
| | | thumbUrl |
| | | fullThumbUrl |
| | | } |
| | | ratingForm { |
| | | schemeId |
| | |
| | | items { |
| | | id |
| | | name |
| | | description |
| | | maxScore |
| | | orderNo |
| | | weight |
| | | sortOrder |
| | | } |
| | | } |
| | | } |
| | |
| | | id: detail.id, |
| | | title: detail.projectName, |
| | | description: detail.description, |
| | | submittedAt: detail.submitTime || null, |
| | | team: detail.team || null, |
| | | files: detail.submissionFiles ? detail.submissionFiles.map(file => ({ |
| | | id: file.id, |
| | | name: file.name, |
| | | url: file.fullUrl || file.url, |
| | | type: file.fileExt, |
| | | size: file.fileSize, |
| | | isDownloading: this.data.downloadingFiles.indexOf(file.id) > -1 |
| | | })) : [], |
| | | images: detail.submissionFiles ? detail.submissionFiles |
| | | .filter(file => file.mediaType === 1) |
| | | .map(file => file.fullUrl || file.url) : [], |
| | | videos: detail.submissionFiles ? detail.submissionFiles |
| | | .filter(file => file.mediaType === 2) |
| | | .map(file => file.fullUrl || file.url) : [], |
| | | participant: { |
| | | id: detail.playerInfo?.id, |
| | | name: detail.playerInfo?.name, |
| | | phone: detail.playerInfo?.phone || detail.playerInfo?.userInfo?.phone || '', |
| | | avatar: detail.playerInfo?.userInfo?.avatarUrl || '/images/default-avatar.svg', |
| | | gender: this.getGenderLabel(detail.playerInfo?.gender), |
| | | birthday: this.getBirthdayText(detail.playerInfo?.birthday), |
| | | region: detail.regionInfo?.fullPath || detail.regionInfo?.name || '', |
| | | education: detail.playerInfo?.education || '', |
| | | id: detail.playerInfo.id, |
| | | name: detail.playerInfo.name, |
| | | school: detail.regionInfo ? detail.regionInfo.name : '', |
| | | major: detail.playerInfo?.education || '' |
| | | major: detail.playerInfo.education || '', |
| | | avatar: detail.playerInfo.userInfo?.avatarUrl || '/images/default-avatar.svg' |
| | | }, |
| | | status: detail.state === 1 ? 'APPROVED' : detail.state === 2 ? 'REJECTED' : 'PENDING', |
| | | mediaList: (detail.submissionFiles || []).map(file => this.transformMediaFile(file)) |
| | | status: detail.state === 1 ? 'APPROVED' : detail.state === 2 ? 'REJECTED' : 'PENDING' |
| | | } |
| | | |
| | | const criteria = (detail.ratingForm?.items || []).map(item => { |
| | | const maxScore = item.maxScore || 0 |
| | | return { |
| | | id: item.id, |
| | | name: item.name, |
| | | maxScore, |
| | | description: item.description || '暂无描述', |
| | | step: maxScore > 20 ? 1 : 0.5, |
| | | currentScore: 0 |
| | | } |
| | | }) |
| | | |
| | | |
| | | // 构建activity对象 |
| | | const activity = { |
| | | id: detail.stageId, |
| | | title: detail.activityName, |
| | | description: detail.description, |
| | | judgeCriteria: detail.ratingForm ? detail.ratingForm.items || [] : [] |
| | | } |
| | | |
| | | // 初始化评分数据 |
| | | const scores = {} |
| | | criteria.forEach(criterion => { |
| | | scores[criterion.id] = 0 |
| | | }) |
| | | |
| | | const maxScore = detail.ratingForm?.totalMaxScore || criteria.reduce((sum, item) => sum + (item.maxScore || 0), 0) |
| | | |
| | | let maxScore = 0 |
| | | |
| | | if (activity.judgeCriteria) { |
| | | activity.judgeCriteria.forEach(criterion => { |
| | | scores[criterion.id] = 0 // 暂时设为0,后续需要查询已有评分 |
| | | maxScore += criterion.maxScore |
| | | }) |
| | | } |
| | | |
| | | this.setData({ |
| | | submission, |
| | | activity: { |
| | | id: detail.id, |
| | | stageId: detail.stageId, |
| | | ratingSchemeId: detail.ratingForm?.schemeId || null, |
| | | totalMaxScore: maxScore |
| | | }, |
| | | stageId: detail.stageId || null, |
| | | submissionId: detail.id, |
| | | criteria, |
| | | activity, |
| | | criteria: activity.judgeCriteria || [], |
| | | scores, |
| | | maxScore, |
| | | totalScore: 0, |
| | | existingReview: null, |
| | | existingReview: null, // 暂时设为null,后续需要查询已有评分 |
| | | reviewStatus: 'PENDING', |
| | | comment: '' |
| | | }) |
| | | |
| | | |
| | | this.calculateTotalScore() |
| | | |
| | | |
| | | // 检查是否已有评分 |
| | | this.checkReviewStatus() |
| | | } |
| | |
| | | currentJudgeRating(activityPlayerId: $activityPlayerId) { |
| | | id |
| | | totalScore |
| | | remark |
| | | comment |
| | | status |
| | | ratedAt |
| | | items { |
| | |
| | | |
| | | if (rating.items) { |
| | | rating.items.forEach(item => { |
| | | const numericScore = item.score !== undefined && item.score !== null ? Number(item.score) : 0 |
| | | scores[item.ratingItemId] = numericScore |
| | | totalScore += numericScore |
| | | scores[item.ratingItemId] = item.score |
| | | totalScore += item.score |
| | | }) |
| | | } |
| | | |
| | | const updatedCriteria = this.data.criteria.map(criterion => { |
| | | const value = scores[criterion.id] !== undefined ? scores[criterion.id] : 0 |
| | | return { |
| | | ...criterion, |
| | | currentScore: value |
| | | } |
| | | }) |
| | | |
| | | const normalizedTotal = Number(totalScore.toFixed(2)) |
| | | |
| | | |
| | | this.setData({ |
| | | scores, |
| | | criteria: updatedCriteria, |
| | | totalScore: normalizedTotal, |
| | | comment: rating.remark || rating.comment || '', |
| | | existingReview: { |
| | | ...rating, |
| | | totalScore: rating.totalScore ? Number(rating.totalScore) : normalizedTotal, |
| | | reviewedAt: rating.ratedAt || rating.reviewedAt || rating.updateTime || null |
| | | }, |
| | | reviewStatus: 'COMPLETED' |
| | | totalScore, |
| | | comment: rating.comment || '', |
| | | existingReview: rating, |
| | | reviewStatus: rating.status || 'COMPLETED' |
| | | }) |
| | | |
| | | console.log('已加载现有评分:', rating) |
| | | } else { |
| | | console.log('当前评委尚未评分') |
| | | } |
| | | |
| | | this.calculateTotalScore() |
| | | } catch (error) { |
| | | console.error('检查评审状态失败:', error) |
| | | } |
| | | }, |
| | | |
| | | normalizeScore(value, criterion) { |
| | | const maxScore = Number(criterion.maxScore || 0) |
| | | const step = Number(criterion.step || (maxScore > 20 ? 1 : 0.5)) |
| | | if (Number.isNaN(value)) { |
| | | value = 0 |
| | | } |
| | | let normalized = Math.round(value / step) * step |
| | | if (normalized < 0) normalized = 0 |
| | | if (normalized > maxScore) normalized = maxScore |
| | | return Number(normalized.toFixed(2)) |
| | | }, |
| | | |
| | | updateCriterionScore(criterionId, index, value) { |
| | | const criterion = this.data.criteria[index] |
| | | if (!criterion) return |
| | | const normalized = this.normalizeScore(value, criterion) |
| | | |
| | | this.setData({ |
| | | [`scores.${criterionId}`]: normalized, |
| | | [`criteria[${index}].currentScore`]: normalized |
| | | }) |
| | | |
| | | this.calculateTotalScore() |
| | | }, |
| | | |
| | | // 评分改变 |
| | | onScoreChange(e) { |
| | | const { criterionId, index } = e.currentTarget.dataset |
| | | const criterion = this.data.criteria[index] |
| | | if (!criterion) return |
| | | |
| | | const inputValue = Number(e.detail.value) |
| | | const newScore = this.normalizeScore(inputValue, criterion) |
| | | this.updateCriterionScore(criterionId, index, newScore) |
| | | }, |
| | | |
| | | increaseScore(e) { |
| | | const { criterionId, index } = e.currentTarget.dataset |
| | | const criterion = this.data.criteria[index] |
| | | if (!criterion) return |
| | | const current = Number(this.data.scores[criterionId] || criterion.currentScore || 0) |
| | | const step = criterion.step || (criterion.maxScore > 20 ? 1 : 0.5) |
| | | this.updateCriterionScore(criterionId, index, current + step) |
| | | }, |
| | | |
| | | decreaseScore(e) { |
| | | const { criterionId, index } = e.currentTarget.dataset |
| | | const criterion = this.data.criteria[index] |
| | | if (!criterion) return |
| | | const current = Number(this.data.scores[criterionId] || criterion.currentScore || 0) |
| | | const step = criterion.step || (criterion.maxScore > 20 ? 1 : 0.5) |
| | | this.updateCriterionScore(criterionId, index, current - step) |
| | | const { criterionId } = e.currentTarget.dataset |
| | | const { value } = e.detail |
| | | |
| | | this.setData({ |
| | | [`scores.${criterionId}`]: parseInt(value) |
| | | }) |
| | | |
| | | this.calculateTotalScore() |
| | | }, |
| | | |
| | | // 计算总分 |
| | |
| | | let totalScore = 0 |
| | | |
| | | criteria.forEach(criterion => { |
| | | const score = Number(scores[criterion.id] || 0) |
| | | totalScore += score |
| | | const score = scores[criterion.id] || 0 |
| | | totalScore += score * (criterion.weight || 1) |
| | | }) |
| | | |
| | | this.setData({ totalScore: Number(totalScore.toFixed(2)) }) |
| | | this.setData({ totalScore }) |
| | | }, |
| | | |
| | | // 评审意见输入 |
| | |
| | | |
| | | // 媒体点击 |
| | | onMediaTap(e) { |
| | | const index = Number(e.currentTarget.dataset.index) |
| | | const mediaList = this.data.submission?.mediaList || [] |
| | | const media = mediaList[index] |
| | | if (!media) return |
| | | |
| | | if (media.mediaType === 'image') { |
| | | const imageUrls = mediaList |
| | | .filter(item => item.mediaType === 'image') |
| | | .map(item => item.url) |
| | | const { url, type } = e.currentTarget.dataset |
| | | |
| | | if (type === 'image') { |
| | | wx.previewImage({ |
| | | current: media.url, |
| | | urls: imageUrls |
| | | current: url, |
| | | urls: this.data.submission.images || [] |
| | | }) |
| | | } else if (media.mediaType === 'video') { |
| | | wx.navigateTo({ |
| | | url: `/pages/video/video?url=${encodeURIComponent(media.url)}&title=${encodeURIComponent(media.name)}` |
| | | } else if (type === 'video') { |
| | | this.setData({ |
| | | showMediaPreview: true, |
| | | currentMedia: url, |
| | | mediaType: 'video' |
| | | }) |
| | | } else { |
| | | this.openDocumentMedia(media) |
| | | } |
| | | }, |
| | | |
| | | async openDocumentMedia(media) { |
| | | // 关闭媒体预览 |
| | | onCloseMediaPreview() { |
| | | this.setData({ |
| | | showMediaPreview: false, |
| | | currentMedia: null |
| | | }) |
| | | }, |
| | | |
| | | // 下载文件 |
| | | async onDownloadFile(e) { |
| | | const { fileId, fileName, fileUrl } = e.currentTarget.dataset |
| | | |
| | | try { |
| | | wx.showLoading({ title: '打开中...' }) |
| | | const downloadRes = await new Promise((resolve, reject) => { |
| | | wx.downloadFile({ |
| | | url: media.url, |
| | | success: resolve, |
| | | fail: reject |
| | | }) |
| | | }) |
| | | |
| | | if (downloadRes.statusCode !== 200) { |
| | | throw new Error('文件下载失败') |
| | | // 添加到下载中列表 |
| | | const downloadingFiles = [...this.data.downloadingFiles, fileId] |
| | | |
| | | // 同时更新文件的isDownloading字段 |
| | | const submission = { ...this.data.submission } |
| | | if (submission.files) { |
| | | submission.files = submission.files.map(file => ({ |
| | | ...file, |
| | | isDownloading: file.id === fileId ? true : file.isDownloading |
| | | })) |
| | | } |
| | | |
| | | await new Promise((resolve, reject) => { |
| | | wx.openDocument({ |
| | | filePath: downloadRes.tempFilePath, |
| | | showMenu: true, |
| | | success: resolve, |
| | | fail: reject |
| | | }) |
| | | |
| | | this.setData({ |
| | | downloadingFiles, |
| | | submission |
| | | }) |
| | | |
| | | wx.showLoading({ title: '下载中...' }) |
| | | |
| | | const result = await wx.downloadFile({ |
| | | url: fileUrl, |
| | | success: (res) => { |
| | | if (res.statusCode === 200) { |
| | | // 保存到相册或文件 |
| | | wx.saveFile({ |
| | | tempFilePath: res.tempFilePath, |
| | | success: () => { |
| | | wx.showToast({ |
| | | title: '下载成功', |
| | | icon: 'success' |
| | | }) |
| | | }, |
| | | fail: () => { |
| | | wx.showToast({ |
| | | title: '保存失败', |
| | | icon: 'error' |
| | | }) |
| | | } |
| | | }) |
| | | } |
| | | }, |
| | | fail: () => { |
| | | wx.showToast({ |
| | | title: '下载失败', |
| | | icon: 'error' |
| | | }) |
| | | } |
| | | }) |
| | | } catch (error) { |
| | | console.error('打开文件失败:', error) |
| | | console.error('下载文件失败:', error) |
| | | wx.showToast({ |
| | | title: '无法打开文件', |
| | | title: '下载失败', |
| | | icon: 'error' |
| | | }) |
| | | } finally { |
| | | // 从下载中列表移除 |
| | | const downloadingFiles = this.data.downloadingFiles.filter(id => id !== fileId) |
| | | |
| | | // 同时更新文件的isDownloading字段 |
| | | const submission = { ...this.data.submission } |
| | | if (submission.files) { |
| | | submission.files = submission.files.map(file => ({ |
| | | ...file, |
| | | isDownloading: file.id === fileId ? false : file.isDownloading |
| | | })) |
| | | } |
| | | |
| | | this.setData({ |
| | | downloadingFiles, |
| | | submission |
| | | }) |
| | | wx.hideLoading() |
| | | } |
| | | }, |
| | |
| | | // 验证评审数据 |
| | | validateReview() { |
| | | const { scores, criteria, comment } = this.data |
| | | const commentText = (comment || '').trim() |
| | | |
| | | // 检查是否所有标准都已评分 |
| | | for (let criterion of criteria) { |
| | |
| | | } |
| | | |
| | | // 检查评审意见 |
| | | if (!commentText) { |
| | | if (!comment.trim()) { |
| | | wx.showToast({ |
| | | title: '请填写评审意见', |
| | | icon: 'error' |
| | |
| | | return false |
| | | } |
| | | |
| | | if (commentText.length < 10) { |
| | | if (comment.trim().length < 10) { |
| | | wx.showToast({ |
| | | title: '评审意见至少10个字符', |
| | | icon: 'error' |
| | |
| | | } |
| | | |
| | | return true |
| | | }, |
| | | |
| | | // 保存草稿 |
| | | async onSaveDraft() { |
| | | try { |
| | | wx.showLoading({ title: '保存中...' }) |
| | | |
| | | const { activityPlayerId, scores, comment, criteria, activity } = this.data |
| | | |
| | | // 构建评分项数组 |
| | | const ratings = criteria.map(criterion => ({ |
| | | itemId: criterion.id, |
| | | score: scores[criterion.id] || 0 |
| | | })) |
| | | |
| | | const mutation = ` |
| | | mutation SaveActivityPlayerRating($input: ActivityPlayerRatingInput!) { |
| | | saveActivityPlayerRating(input: $input) |
| | | } |
| | | ` |
| | | |
| | | const input = { |
| | | activityPlayerId, |
| | | stageId: activity.stageId, |
| | | ratings, |
| | | comment: comment.trim() |
| | | } |
| | | |
| | | const result = await graphqlRequest(mutation, { input }) |
| | | |
| | | if (result && result.saveActivityPlayerRating) { |
| | | wx.showToast({ |
| | | title: '草稿已保存', |
| | | icon: 'success' |
| | | }) |
| | | |
| | | // 重新加载评分状态 |
| | | await this.checkReviewStatus() |
| | | } |
| | | } catch (error) { |
| | | console.error('保存草稿失败:', error) |
| | | wx.showToast({ |
| | | title: '保存失败', |
| | | icon: 'error' |
| | | }) |
| | | } finally { |
| | | wx.hideLoading() |
| | | } |
| | | }, |
| | | |
| | | // 提交评审 |
| | |
| | | this.setData({ submitting: true }) |
| | | wx.showLoading({ title: '提交中...' }) |
| | | |
| | | const { activityPlayerId, scores, comment, criteria, stageId } = this.data |
| | | const commentText = (comment || '').trim() |
| | | |
| | | if (!stageId) { |
| | | wx.showToast({ |
| | | title: '缺少阶段信息,无法提交', |
| | | icon: 'none' |
| | | }) |
| | | this.setData({ submitting: false }) |
| | | wx.hideLoading() |
| | | return |
| | | } |
| | | const { activityPlayerId, scores, comment, criteria, activity } = this.data |
| | | |
| | | // 构建评分项数组 |
| | | const ratings = criteria.map(criterion => ({ |
| | | itemId: criterion.id, |
| | | score: Number(scores[criterion.id] || 0) |
| | | score: scores[criterion.id] || 0 |
| | | })) |
| | | |
| | | const mutation = ` |
| | |
| | | |
| | | const input = { |
| | | activityPlayerId, |
| | | stageId, |
| | | stageId: activity.stageId, |
| | | ratings, |
| | | comment: commentText |
| | | comment: comment.trim() |
| | | } |
| | | |
| | | const result = await graphqlRequest(mutation, { input }) |
| | |
| | | // 联系参赛者 |
| | | onContactParticipant() { |
| | | const { submission } = this.data |
| | | const phone = submission?.participant?.phone |
| | | if (phone) { |
| | | wx.makePhoneCall({ |
| | | phoneNumber: phone |
| | | }) |
| | | } else { |
| | | wx.showToast({ |
| | | title: '暂无联系方式', |
| | | icon: 'none' |
| | | |
| | | 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 |
| | | } |
| | | } |
| | | }) |
| | | } |
| | | }, |
| | | |
| | | // 获取评分等级文本 |
| | | getScoreLabel(score) { |
| | | const option = this.data.scoreOptions.find(opt => opt.value === score) |
| | | return option ? option.label : `${score}分` |
| | | }, |
| | | |
| | | // 获取文件大小文本 |
| | |
| | | } |
| | | }, |
| | | |
| | | // 统一处理性别显示文本 |
| | | getGenderLabel(gender) { |
| | | if (gender === null || gender === undefined || gender === '') { |
| | | return '未填写' |
| | | } |
| | | |
| | | const normalized = String(gender).trim().toLowerCase() |
| | | |
| | | if (normalized === '') { |
| | | return '未填写' |
| | | } |
| | | |
| | | if (normalized === 'male' || normalized === 'm') { |
| | | return '男' |
| | | } |
| | | if (normalized === 'female' || normalized === 'f') { |
| | | return '女' |
| | | } |
| | | |
| | | if (/^-?\d+$/.test(normalized)) { |
| | | const numeric = Number(normalized) |
| | | if (numeric === 1) return '男' |
| | | if (numeric === 0) return '女' |
| | | if (numeric === 2) return '女' |
| | | } |
| | | |
| | | return gender === undefined || gender === null ? '未填写' : String(gender) |
| | | }, |
| | | |
| | | // 统一处理出生日期显示文本 |
| | | getBirthdayText(dateString) { |
| | | if (!dateString) { |
| | | return '未填写' |
| | | } |
| | | const formatted = formatDateUtil(dateString, 'YYYY-MM-DD') |
| | | return formatted || '未填写' |
| | | }, |
| | | |
| | | // 格式化日期 |
| | | formatDate(dateString) { |
| | | return formatDateUtil(dateString, 'YYYY-MM-DD HH:mm') |
| | | return formatDate(dateString, 'YYYY-MM-DD HH:mm') |
| | | }, |
| | | |
| | | // 分享页面 |
| | |
| | | |
| | | // 加载消息列表 |
| | | loadMessages() { |
| | | // 检查用户是否已登录,如果globalData中没有,尝试从本地存储恢复 |
| | | let userInfo = app.globalData.userInfo |
| | | if (!userInfo || !userInfo.userId) { |
| | | console.log('globalData中没有userInfo,尝试从本地存储恢复') |
| | | try { |
| | | const storedUserInfo = wx.getStorageSync('userInfo') |
| | | if (storedUserInfo && storedUserInfo.userId) { |
| | | console.log('从本地存储恢复userInfo成功') |
| | | app.globalData.userInfo = storedUserInfo |
| | | userInfo = storedUserInfo |
| | | } |
| | | } catch (error) { |
| | | console.error('从本地存储恢复userInfo失败:', error) |
| | | } |
| | | } |
| | | |
| | | // 检查用户是否已登录 |
| | | const userInfo = app.globalData.userInfo |
| | | if (!userInfo || !userInfo.userId) { |
| | | console.error('用户未登录或userId不存在') |
| | | wx.showToast({ |
| | |
| | | title: '获取中...' |
| | | }) |
| | | |
| | | // TODO: 调用后端API解密手机号 |
| | | // 调用后端API解密手机号 |
| | | const app = getApp() |
| | | const sessionKey = app.globalData.sessionKey |
| | | |
| | | if (!sessionKey) { |
| | | throw new Error('SessionKey不存在,请重新登录') |
| | | } |
| | | |
| | | const mutation = ` |
| | | mutation DecryptPhoneNumber($encryptedData: String!, $iv: String!) { |
| | | decryptPhoneNumber(encryptedData: $encryptedData, iv: $iv) { |
| | | mutation DecryptPhoneNumber($encryptedData: String!, $iv: String!, $sessionKey: String!) { |
| | | decryptPhoneNumber(encryptedData: $encryptedData, iv: $iv, sessionKey: $sessionKey) { |
| | | phoneNumber |
| | | } |
| | | } |
| | |
| | | |
| | | const variables = { |
| | | encryptedData: e.detail.encryptedData, |
| | | iv: e.detail.iv |
| | | iv: e.detail.iv, |
| | | sessionKey: sessionKey |
| | | } |
| | | |
| | | const result = await graphqlRequest(mutation, variables) |
| | |
| | | return |
| | | } |
| | | |
| | | if (!userInfo.phone) { |
| | | wx.showToast({ |
| | | title: '请获取手机号', |
| | | icon: 'none' |
| | | // 强制要求授权电话号码 |
| | | if (!userInfo.phone || userInfo.phone.trim() === '') { |
| | | wx.showModal({ |
| | | title: '需要授权手机号', |
| | | content: '根据平台规定,必须授权手机号码才能保存用户信息。请先获取手机号码授权。', |
| | | showCancel: false, |
| | | confirmText: '我知道了' |
| | | }) |
| | | return |
| | | } |
| | |
| | | // 保存到本地存储 |
| | | wx.setStorageSync('userInfo', app.globalData.userInfo) |
| | | |
| | | wx.showToast({ |
| | | // 显示保存成功提示,并提醒需要重新登录 |
| | | wx.showModal({ |
| | | title: '保存成功', |
| | | icon: 'success' |
| | | content: '用户信息已保存成功。为了获取最新的关联信息(如参赛者、评委、员工身份),系统将重新登录。', |
| | | showCancel: false, |
| | | confirmText: '确定', |
| | | success: () => { |
| | | // 强制重新登录以获取最新的用户关联信息 |
| | | this.forceRelogin() |
| | | } |
| | | }) |
| | | |
| | | // 延迟返回上一页 |
| | | setTimeout(() => { |
| | | wx.navigateBack() |
| | | }, 1500) |
| | | } else { |
| | | throw new Error('保存失败') |
| | | } |
| | |
| | | // 获取文件扩展名 |
| | | getFileExtension(fileName) { |
| | | return fileName.split('.').pop().toLowerCase() |
| | | }, |
| | | |
| | | // 强制重新登录 |
| | | forceRelogin() { |
| | | wx.showLoading({ |
| | | title: '重新登录中...' |
| | | }) |
| | | |
| | | try { |
| | | // 清除本地存储的登录信息 |
| | | wx.removeStorageSync('token') |
| | | wx.removeStorageSync('userInfo') |
| | | wx.removeStorageSync('sessionKey') |
| | | |
| | | // 清除全局数据 |
| | | app.globalData.token = null |
| | | app.globalData.userInfo = null |
| | | app.globalData.sessionKey = null |
| | | |
| | | console.log('已清除本地登录信息,准备重新登录') |
| | | |
| | | // 重新调用登录流程 |
| | | app.login() |
| | | |
| | | // 延迟一段时间后隐藏loading并返回上一页 |
| | | setTimeout(() => { |
| | | wx.hideLoading() |
| | | wx.showToast({ |
| | | title: '重新登录完成', |
| | | icon: 'success' |
| | | }) |
| | | |
| | | // 返回上一页 |
| | | setTimeout(() => { |
| | | wx.navigateBack() |
| | | }, 1000) |
| | | }, 2000) |
| | | |
| | | } catch (error) { |
| | | console.error('强制重新登录失败:', error) |
| | | wx.hideLoading() |
| | | wx.showToast({ |
| | | title: '重新登录失败', |
| | | icon: 'none' |
| | | }) |
| | | |
| | | // 即使失败也返回上一页 |
| | | setTimeout(() => { |
| | | wx.navigateBack() |
| | | }, 1500) |
| | | } |
| | | } |
| | | }) |
| | |
| | | if (projectDetail.submissionFiles) { |
| | | projectDetail.submissionFiles.forEach(file => { |
| | | file.fileSizeText = this.formatFileSize(file.fileSize) |
| | | // 字段已经是正确的名称,无需映射 |
| | | // fullUrl, fullThumbUrl, fileSize, fileExt 都是正确的字段名 |
| | | }) |
| | | } |
| | | |
| | |
| | | query GetProjectDetail($id: ID!) { |
| | | activityPlayerDetail(id: $id) { |
| | | id |
| | | playerInfo { |
| | | id |
| | | name |
| | | phone |
| | | gender |
| | | birthday |
| | | education |
| | | introduction |
| | | description |
| | | avatarUrl |
| | | avatar { |
| | | id |
| | | fullUrl |
| | | fullThumbUrl |
| | | name |
| | | fileSize |
| | | fileExt |
| | | } |
| | | userInfo { |
| | | userId |
| | | name |
| | | phone |
| | | avatarUrl |
| | | } |
| | | } |
| | | regionInfo { |
| | | id |
| | | name |
| | | fullPath |
| | | } |
| | | activityName |
| | | activityId |
| | | playerId |
| | | playerName |
| | | playerGender |
| | | playerPhone |
| | | playerEducation |
| | | playerBirthDate |
| | | playerIdCard |
| | | playerAddress |
| | | projectName |
| | | description |
| | | feedback |
| | | state |
| | | stageId |
| | | submissionFiles { |
| | | projectDescription |
| | | projectCategory |
| | | projectTags |
| | | projectFiles { |
| | | id |
| | | fullUrl |
| | | fullThumbUrl |
| | | name |
| | | fileName |
| | | fileUrl |
| | | fileSize |
| | | fileExt |
| | | mediaType |
| | | fileType |
| | | uploadTime |
| | | } |
| | | ratingForm { |
| | | schemeId |
| | | schemeName |
| | | items { |
| | | id |
| | | name |
| | | maxScore |
| | | orderNo |
| | | } |
| | | totalMaxScore |
| | | submitTime |
| | | reviewTime |
| | | reviewerId |
| | | reviewerName |
| | | score |
| | | rating { |
| | | id |
| | | judgeId |
| | | judgeName |
| | | score |
| | | feedback |
| | | ratingTime |
| | | } |
| | | state |
| | | feedback |
| | | } |
| | | } |
| | | ` |
| | |
| | | |
| | | // 获取评分统计 |
| | | async getRatingStatsFromAPI(projectId) { |
| | | // 暂时返回空的评分数据,避免GraphQL查询错误 |
| | | // TODO: 需要后端提供合适的评分统计查询接口 |
| | | try { |
| | | return { |
| | | averageScore: null, |
| | | ratingCount: 0, |
| | | judgeRatings: [] |
| | | const query = ` |
| | | query GetRatingStats($activityPlayerId: ID!) { |
| | | ratingStats(activityPlayerId: $activityPlayerId) { |
| | | averageScore |
| | | totalRatings |
| | | scoreDistribution { |
| | | score |
| | | count |
| | | } |
| | | } |
| | | } |
| | | ` |
| | | |
| | | try { |
| | | const result = await app.graphqlRequest(query, { activityPlayerId: projectId }) |
| | | return result.ratingStats |
| | | } catch (error) { |
| | | throw error |
| | | } |
| | |
| | | errors.name = '请输入姓名'; |
| | | } |
| | | |
| | | if (!formData.phone.trim()) { |
| | | errors.phone = '请输入手机号'; |
| | | if (!formData.phone || !formData.phone.trim()) { |
| | | errors.phone = '请先授权获取手机号'; |
| | | } else if (!/^1[3-9]\d{9}$/.test(formData.phone)) { |
| | | errors.phone = '请输入正确的手机号'; |
| | | } |
| | |
| | | }) |
| | | return |
| | | } |
| | | |
| | | // 额外检查:确保必须授权电话号码 |
| | | if (!this.data.formData.phone || !this.data.formData.phone.trim()) { |
| | | wx.showModal({ |
| | | title: '需要授权手机号', |
| | | content: '根据平台规定,必须授权手机号码才能报名参赛。请先获取手机号码授权。', |
| | | showCancel: false, |
| | | confirmText: '我知道了' |
| | | }) |
| | | return |
| | | } |
| | | |
| | | this.setData({ isSubmitting: true }) |
| | | |
| | |
| | | const app = getApp() |
| | | const { graphqlRequest, formatDate } = require('../../lib/utils') |
| | | |
| | | const GET_RATING_STATS_QUERY = ` |
| | | query GetRatingStats($activityPlayerId: ID!) { |
| | | judgeRatingsForPlayer(activityPlayerId: $activityPlayerId) { |
| | | hasRated |
| | | } |
| | | } |
| | | ` |
| | | |
| | | Page({ |
| | | data: { |
| | | loading: false, |
| | |
| | | |
| | | // 切换选项卡 |
| | | switchTab(e) { |
| | | const index = parseInt(e.currentTarget.dataset.index) || 0 |
| | | const index = parseInt(e.currentTarget.dataset.index) // 将字符串转换为数字 |
| | | if (index === this.data.currentTab) return |
| | | |
| | | this.setData({ |
| | |
| | | ]) |
| | | } catch (error) { |
| | | console.error('加载数据失败:', error) |
| | | |
| | | // 检查是否是认证相关错误 |
| | | if (error.message && ( |
| | | error.message.includes('没有权限访问') || |
| | | error.message.includes('请先登录') || |
| | | error.message.includes('重新登录') |
| | | )) { |
| | | wx.showToast({ |
| | | title: '正在重新登录...', |
| | | icon: 'loading', |
| | | duration: 2000 |
| | | }) |
| | | |
| | | // 等待一段时间后重试 |
| | | setTimeout(() => { |
| | | this.loadData() |
| | | }, 2000) |
| | | } else { |
| | | wx.showToast({ |
| | | title: '加载失败,请重试', |
| | | icon: 'none' |
| | | }) |
| | | } |
| | | wx.showToast({ |
| | | title: '加载失败', |
| | | icon: 'none' |
| | | }) |
| | | } finally { |
| | | this.setData({ loading: false }) |
| | | wx.stopPullDownRefresh() |
| | |
| | | } |
| | | |
| | | // 根据当前选项卡构建不同的查询 |
| | | switch (currentTab) { |
| | | switch (parseInt(currentTab)) { // 确保currentTab是数字 |
| | | case 0: // 我未评审 |
| | | query = ` |
| | | query GetUnReviewedProjects($page: Int!, $pageSize: Int!, $searchKeyword: String) { |
| | |
| | | } |
| | | ` |
| | | break |
| | | default: |
| | | console.error('无效的选项卡索引:', currentTab) |
| | | query = ` |
| | | query GetUnReviewedProjects($page: Int!, $pageSize: Int!, $searchKeyword: String) { |
| | | unReviewedProjects(page: $page, pageSize: $pageSize, searchKeyword: $searchKeyword) { |
| | | items { |
| | | id |
| | | projectName |
| | | activityName |
| | | stageName |
| | | studentName |
| | | submitTime |
| | | status |
| | | } |
| | | total |
| | | hasMore |
| | | } |
| | | } |
| | | ` |
| | | break |
| | | } |
| | | |
| | | // 检查query是否为空 |
| | | if (!query || query.trim() === '') { |
| | | console.error('GraphQL查询为空,无法执行请求') |
| | | return |
| | | } |
| | | |
| | | const result = await graphqlRequest(query, variables) |
| | | |
| | | if (result) { |
| | | const dataKey = currentTab === 0 ? 'unReviewedProjects' : |
| | | currentTab === 1 ? 'reviewedProjects' : |
| | | const dataKey = parseInt(currentTab) === 0 ? 'unReviewedProjects' : |
| | | parseInt(currentTab) === 1 ? 'reviewedProjects' : |
| | | 'studentUnReviewedProjects' |
| | | |
| | | const data = result[dataKey] |
| | |
| | | const projects = data.items.map(item => ({ |
| | | ...item, |
| | | submitTime: item.submitTime ? formatDate(item.submitTime) : '', |
| | | reviewTime: item.reviewTime ? formatDate(item.reviewTime) : '' |
| | | reviewTime: item.reviewTime ? formatDate(item.reviewTime) : '', |
| | | statusText: this.getStatusText(item.status), |
| | | statusType: this.getStatusType(item.status) |
| | | })) |
| | | |
| | | const projectsWithRatingCount = await this.enrichProjectsWithRatingCounts(projects) |
| | | |
| | | this.setData({ |
| | | projectList: isLoadMore ? [...this.data.projectList, ...projectsWithRatingCount] : projectsWithRatingCount, |
| | | projectList: isLoadMore ? [...this.data.projectList, ...projects] : projects, |
| | | hasMore: data.hasMore || false, |
| | | currentPage: variables.page |
| | | }) |
| | |
| | | } |
| | | }, |
| | | |
| | | async enrichProjectsWithRatingCounts(projects) { |
| | | if (!Array.isArray(projects) || projects.length === 0) { |
| | | return projects || [] |
| | | // 获取状态文本 |
| | | getStatusText(status) { |
| | | const statusMap = { |
| | | 'SUBMITTED': '已提交', |
| | | 'UNDER_REVIEW': '评审中', |
| | | 'REVIEWED': '已评审', |
| | | 'REJECTED': '已拒绝' |
| | | } |
| | | |
| | | try { |
| | | const counts = await Promise.all(projects.map(project => this.getProjectRatingCount(project.id))) |
| | | return projects.map((project, index) => ({ |
| | | ...project, |
| | | ratingCount: counts[index] |
| | | })) |
| | | } catch (error) { |
| | | console.error('批量获取评审次数失败:', error) |
| | | return projects.map(project => ({ |
| | | ...project, |
| | | ratingCount: typeof project.ratingCount === 'number' ? project.ratingCount : 0 |
| | | })) |
| | | } |
| | | return statusMap[status] || status |
| | | }, |
| | | |
| | | async getProjectRatingCount(activityPlayerId) { |
| | | if (!activityPlayerId) { |
| | | return 0 |
| | | // 获取状态类型 |
| | | getStatusType(status) { |
| | | const typeMap = { |
| | | 'SUBMITTED': 'info', |
| | | 'UNDER_REVIEW': 'warning', |
| | | 'REVIEWED': 'success', |
| | | 'REJECTED': 'danger' |
| | | } |
| | | |
| | | try { |
| | | const result = await graphqlRequest(GET_RATING_STATS_QUERY, { activityPlayerId }) |
| | | const ratings = result?.judgeRatingsForPlayer || [] |
| | | return ratings.filter(item => item?.hasRated).length |
| | | } catch (error) { |
| | | console.error(`获取项目 ${activityPlayerId} 的评审次数失败:`, error) |
| | | return 0 |
| | | } |
| | | return typeMap[status] || 'info' |
| | | }, |
| | | |
| | | // 获取空状态文本 |