lrj
5 天以前 7ba080d35812e6db7bd5aa8f88161c02653eb6c1
feat: 优化员工和评委编辑功能的密码重置逻辑

- 优化EmployeeForm.vue中的密码验证和重置功能
- 添加密码重置按钮和相关逻辑
- 完善认证系统和GraphQL API
- 清理测试文件和临时脚本
- 修复登录和用户管理相关功能
11个文件已修改
15个文件已添加
11个文件已删除
2179 ■■■■ 已修改文件
backend/src/main/java/com/rongyichuang/auth/api/AuthGraphqlApi.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/auth/dto/LoginRequest.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/auth/dto/LoginResponse.java 288 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/auth/service/AuthService.java 151 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/auth/util/JwtUtil.java 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/common/api/AppConfigGraphqlApi.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/common/api/MediaFieldResolver.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/common/api/MediaGraphqlApi.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/common/util/UserContextUtil.java 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/employee/repository/EmployeeRepository.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/judge/service/JudgeService.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/resources/application.yml 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/resources/graphql/auth.graphqls 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/test/java/com/rongyichuang/CreateMultiRoleUserTest.java 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/test/java/com/rongyichuang/CreateTestUsersTest.java 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/test/java/com/rongyichuang/VerifyUserDataTest.java 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/test/java/com/rongyichuang/auth/DirectLoginTest.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/test/java/com/rongyichuang/auth/GraphQLLoginTest.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/test/java/com/rongyichuang/employee/DirectEmployeeServiceTest.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
check_table.ps1 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
test-media-query.json 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
test_api.ps1 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
test_complete.ps1 162 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
test_complete_fixed.ps1 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
test_debug.ps1 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
test_employee_api_fix.ps1 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
test_employee_management.ps1 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
test_no_state.ps1 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
test_simple.ps1 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
test_simple_query.ps1 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
test_users.sql 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web/src/layout/index.vue 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web/src/router/index.ts 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web/src/utils/auth.ts 165 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web/src/utils/graphql.ts 143 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web/src/views/employee/EmployeeForm.vue 50 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web/src/views/login/index.vue 41 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/auth/api/AuthGraphqlApi.java
New file
@@ -0,0 +1,27 @@
package com.rongyichuang.auth.api;
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;
    /**
     * Web端用户登录
     */
    @MutationMapping
    public LoginResponse webLogin(@Argument LoginRequest input) {
        return authService.login(input);
    }
}
backend/src/main/java/com/rongyichuang/auth/dto/LoginRequest.java
New file
@@ -0,0 +1,33 @@
package com.rongyichuang.auth.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
/**
 * 登录请求DTO
 */
public class LoginRequest {
    @NotBlank(message = "手机号不能为空")
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    private String phone;
    @NotBlank(message = "密码不能为空")
    private String password;
    public String getPhone() {
        return phone;
    }
    public void setPhone(String phone) {
        this.phone = phone;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
}
backend/src/main/java/com/rongyichuang/auth/dto/LoginResponse.java
New file
@@ -0,0 +1,288 @@
package com.rongyichuang.auth.dto;
/**
 * 登录响应DTO
 */
public class LoginResponse {
    private String token;
    private UserInfo userInfo;
    public LoginResponse() {}
    public LoginResponse(String token, UserInfo userInfo) {
        this.token = token;
        this.userInfo = userInfo;
    }
    public String getToken() {
        return token;
    }
    public void setToken(String token) {
        this.token = token;
    }
    public UserInfo getUserInfo() {
        return userInfo;
    }
    public void setUserInfo(UserInfo userInfo) {
        this.userInfo = userInfo;
    }
    /**
     * 用户信息内部类
     */
    public static class UserInfo {
        private Long userId;
        private String name;
        private String phone;
        private String userType; // 主要角色类型:优先employee,然后judge,最后player
        private EmployeeInfo employee;
        private JudgeInfo judge;
        private PlayerInfo player;
        public UserInfo() {}
        public UserInfo(Long userId, String name, String phone, String userType) {
            this.userId = userId;
            this.name = name;
            this.phone = phone;
            this.userType = userType;
        }
        // Getters and Setters
        public Long getUserId() {
            return userId;
        }
        public void setUserId(Long userId) {
            this.userId = userId;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public String getPhone() {
            return phone;
        }
        public void setPhone(String phone) {
            this.phone = phone;
        }
        public String getUserType() {
            return userType;
        }
        public void setUserType(String userType) {
            this.userType = userType;
        }
        public EmployeeInfo getEmployee() {
            return employee;
        }
        public void setEmployee(EmployeeInfo employee) {
            this.employee = employee;
        }
        public JudgeInfo getJudge() {
            return judge;
        }
        public void setJudge(JudgeInfo judge) {
            this.judge = judge;
        }
        public PlayerInfo getPlayer() {
            return player;
        }
        public void setPlayer(PlayerInfo player) {
            this.player = player;
        }
    }
    /**
     * 员工信息
     */
    public static class EmployeeInfo {
        private Long id;
        private String name;
        private String roleId;
        private String description;
        public EmployeeInfo() {}
        public EmployeeInfo(Long id, String name, String roleId, String description) {
            this.id = id;
            this.name = name;
            this.roleId = roleId;
            this.description = description;
        }
        // Getters and Setters
        public Long getId() {
            return id;
        }
        public void setId(Long id) {
            this.id = id;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public String getRoleId() {
            return roleId;
        }
        public void setRoleId(String roleId) {
            this.roleId = roleId;
        }
        public String getDescription() {
            return description;
        }
        public void setDescription(String description) {
            this.description = description;
        }
    }
    /**
     * 评委信息
     */
    public static class JudgeInfo {
        private Long id;
        private String name;
        private String title;
        private String company;
        private String description;
        public JudgeInfo() {}
        public JudgeInfo(Long id, String name, String title, String company, String description) {
            this.id = id;
            this.name = name;
            this.title = title;
            this.company = company;
            this.description = description;
        }
        // Getters and Setters
        public Long getId() {
            return id;
        }
        public void setId(Long id) {
            this.id = id;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public String getTitle() {
            return title;
        }
        public void setTitle(String title) {
            this.title = title;
        }
        public String getCompany() {
            return company;
        }
        public void setCompany(String company) {
            this.company = company;
        }
        public String getDescription() {
            return description;
        }
        public void setDescription(String description) {
            this.description = description;
        }
    }
    /**
     * 学员信息内部类
     */
    public static class PlayerInfo {
        private Long id;
        private String name;
        private String phone;
        private String description;
        private Integer auditState; // 审核状态:0-等待审核,1-审核通过,2-不通过
        public PlayerInfo() {}
        public PlayerInfo(Long id, String name, String phone, String description, Integer auditState) {
            this.id = id;
            this.name = name;
            this.phone = phone;
            this.description = description;
            this.auditState = auditState;
        }
        // Getters and Setters
        public Long getId() {
            return id;
        }
        public void setId(Long id) {
            this.id = id;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public String getPhone() {
            return phone;
        }
        public void setPhone(String phone) {
            this.phone = phone;
        }
        public String getDescription() {
            return description;
        }
        public void setDescription(String description) {
            this.description = description;
        }
        public Integer getAuditState() {
            return auditState;
        }
        public void setAuditState(Integer auditState) {
            this.auditState = auditState;
        }
    }
}
backend/src/main/java/com/rongyichuang/auth/service/AuthService.java
New file
@@ -0,0 +1,151 @@
package com.rongyichuang.auth.service;
import com.rongyichuang.auth.dto.LoginRequest;
import com.rongyichuang.auth.dto.LoginResponse;
import com.rongyichuang.auth.util.JwtUtil;
import com.rongyichuang.employee.entity.Employee;
import com.rongyichuang.employee.repository.EmployeeRepository;
import com.rongyichuang.judge.entity.Judge;
import com.rongyichuang.judge.repository.JudgeRepository;
import com.rongyichuang.player.entity.Player;
import com.rongyichuang.player.repository.PlayerRepository;
import com.rongyichuang.user.entity.User;
import com.rongyichuang.user.repository.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Optional;
/**
 * 认证服务类
 */
@Service
public class AuthService {
    private static final Logger logger = LoggerFactory.getLogger(AuthService.class);
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private EmployeeRepository employeeRepository;
    @Autowired
    private JudgeRepository judgeRepository;
    @Autowired
    private PlayerRepository playerRepository;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private JwtUtil jwtUtil;
    /**
     * 用户登录
     */
    public LoginResponse login(LoginRequest loginRequest) {
        logger.info("用户登录尝试,手机号: {}", loginRequest.getPhone());
        // 1. 通过手机号查询用户
        Optional<User> userOpt = userRepository.findByPhone(loginRequest.getPhone());
        if (userOpt.isEmpty()) {
            logger.warn("用户不存在,手机号: {}", loginRequest.getPhone());
            throw new BadCredentialsException("用户不存在");
        }
        User user = userOpt.get();
        // 2. 检查密码是否为空
        if (user.getPassword() == null || user.getPassword().trim().isEmpty()) {
            logger.warn("用户密码为空,手机号: {}", loginRequest.getPhone());
            throw new BadCredentialsException("密码不正确");
        }
        // 3. 验证密码
        if (!passwordEncoder.matches(loginRequest.getPassword(), user.getPassword())) {
            logger.warn("密码验证失败,手机号: {}", loginRequest.getPhone());
            throw new BadCredentialsException("密码不正确");
        }
        // 4. 查找关联的员工、评委和学员信息
        Optional<Employee> employeeOpt = employeeRepository.findByUserId(user.getId());
        Optional<Judge> judgeOpt = judgeRepository.findByUserId(user.getId());
        Optional<Player> playerOpt = playerRepository.findByUserId(user.getId());
        // 5. 检查是否有权限(必须关联员工、评委或学员中的至少一个)
        // 注意:Web登录暂时不支持学员角色,只允许员工和评委登录
        if (employeeOpt.isEmpty() && judgeOpt.isEmpty()) {
            logger.warn("用户没有权限,未关联员工或评委,手机号: {}", loginRequest.getPhone());
            throw new BadCredentialsException("没有权限");
        }
        // 6. 生成JWT token
        String token = jwtUtil.generateToken(user.getId(), user.getPhone());
        // 7. 确定主要角色类型(优先级:employee > judge > player)
        String userType;
        if (employeeOpt.isPresent()) {
            userType = "employee";
        } else if (judgeOpt.isPresent()) {
            userType = "judge";
        } else {
            userType = "player";
        }
        // 8. 构建用户信息
        LoginResponse.UserInfo userInfo = new LoginResponse.UserInfo(
                user.getId(),
                user.getName(),
                user.getPhone(),
                userType
        );
        // 9. 设置所有关联的角色信息
        if (employeeOpt.isPresent()) {
            Employee employee = employeeOpt.get();
            userInfo.setEmployee(new LoginResponse.EmployeeInfo(
                    employee.getId(),
                    employee.getName(),
                    employee.getRoleId(),
                    employee.getDescription()
            ));
            logger.info("员工登录成功,ID: {}, 姓名: {}", employee.getId(), employee.getName());
        }
        if (judgeOpt.isPresent()) {
            Judge judge = judgeOpt.get();
            userInfo.setJudge(new LoginResponse.JudgeInfo(
                    judge.getId(),
                    judge.getName(),
                    judge.getTitle(),
                    judge.getCompany(),
                    judge.getDescription()
            ));
            if (employeeOpt.isEmpty()) {
                logger.info("评委登录成功,ID: {}, 姓名: {}", judge.getId(), judge.getName());
            }
        }
        if (playerOpt.isPresent()) {
            Player player = playerOpt.get();
            userInfo.setPlayer(new LoginResponse.PlayerInfo(
                    player.getId(),
                    player.getName(),
                    player.getPhone(),
                    player.getDescription(),
                    player.getAuditState()
            ));
            if (employeeOpt.isEmpty() && judgeOpt.isEmpty()) {
                logger.info("学员登录成功,ID: {}, 姓名: {}", player.getId(), player.getName());
            }
        }
        return new LoginResponse(token, userInfo);
    }
}
backend/src/main/java/com/rongyichuang/auth/util/JwtUtil.java
New file
@@ -0,0 +1,97 @@
package com.rongyichuang.auth.util;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
/**
 * JWT工具类
 */
@Component
public class JwtUtil {
    private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class);
    @Value("${app.jwt.secret}")
    private String jwtSecret;
    @Value("${app.jwt.expiration:86400000}") // 默认24小时
    private long jwtExpiration;
    /**
     * 生成JWT token
     */
    public String generateToken(Long userId, String phone) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + jwtExpiration);
        SecretKey key = Keys.hmacShaKeyFor(jwtSecret.getBytes());
        return Jwts.builder()
                .setSubject(userId.toString())
                .claim("phone", phone)
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();
    }
    /**
     * 从token中获取用户ID
     */
    public Long getUserIdFromToken(String token) {
        Claims claims = getClaimsFromToken(token);
        return Long.parseLong(claims.getSubject());
    }
    /**
     * 从token中获取手机号
     */
    public String getPhoneFromToken(String token) {
        Claims claims = getClaimsFromToken(token);
        return claims.get("phone", String.class);
    }
    /**
     * 验证token是否有效
     */
    public boolean validateToken(String token) {
        try {
            getClaimsFromToken(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            logger.error("JWT token验证失败: {}", e.getMessage());
            return false;
        }
    }
    /**
     * 检查token是否过期
     */
    public boolean isTokenExpired(String token) {
        try {
            Claims claims = getClaimsFromToken(token);
            return claims.getExpiration().before(new Date());
        } catch (JwtException | IllegalArgumentException e) {
            return true;
        }
    }
    /**
     * 从token中解析Claims
     */
    private Claims getClaimsFromToken(String token) {
        SecretKey key = Keys.hmacShaKeyFor(jwtSecret.getBytes());
        return Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
}
backend/src/main/java/com/rongyichuang/common/api/AppConfigGraphqlApi.java
@@ -8,7 +8,7 @@
@Controller
public class AppConfigGraphqlApi {
    @Value("${app.media-url:${app.media.url:}}")
    @Value("${app.media-url}")
    private String mediaBaseUrl;
    @QueryMapping
backend/src/main/java/com/rongyichuang/common/api/MediaFieldResolver.java
@@ -10,7 +10,7 @@
@Controller
public class MediaFieldResolver {
    @Value("${app.media-url:${app.media.url:}}")
    @Value("${app.media-url}")
    private String mediaBaseUrl;
    @SchemaMapping(typeName = "Media", field = "fullUrl")
backend/src/main/java/com/rongyichuang/common/api/MediaGraphqlApi.java
@@ -27,7 +27,7 @@
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Value("${app.media-url:${app.media.url:}}")
    @Value("${app.media-url}")
    private String mediaBaseUrl;
    public MediaGraphqlApi(MediaRepository mediaRepository, MediaService mediaService) {
backend/src/main/java/com/rongyichuang/common/util/UserContextUtil.java
@@ -1,5 +1,8 @@
package com.rongyichuang.common.util;
import com.rongyichuang.auth.util.JwtUtil;
import com.rongyichuang.employee.entity.Employee;
import com.rongyichuang.employee.repository.EmployeeRepository;
import com.rongyichuang.judge.entity.Judge;
import com.rongyichuang.judge.repository.JudgeRepository;
import org.slf4j.Logger;
@@ -8,7 +11,10 @@
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Optional;
/**
@@ -23,30 +29,89 @@
    @Autowired
    private JudgeRepository judgeRepository;
    @Autowired
    private EmployeeRepository employeeRepository;
    @Autowired
    private JwtUtil jwtUtil;
    /**
     * 获取当前登录用户ID
     * 注意:当前系统暂时使用固定用户ID,后续需要根据实际认证机制修改
     * 从JWT token中解析用户ID
     * 
     * @return 用户ID
     */
    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);
                return userId;
            }
            // 如果没有有效的JWT token,尝试从Spring Security上下文获取
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (authentication != null && authentication.isAuthenticated() && 
                !"anonymousUser".equals(authentication.getPrincipal())) {
                // TODO: 从认证信息中获取真实的用户ID
                // 这里需要根据实际的认证机制来实现
                // 例如:从JWT token中解析用户ID,或从UserDetails中获取
                logger.debug("获取到认证用户: {}", authentication.getName());
                return 1L; // 临时返回固定用户ID
                // 如果认证信息中包含用户ID,可以在这里解析
                // 暂时返回固定用户ID用于兼容性
                return 1L;
            }
        } catch (Exception e) {
            logger.warn("获取当前用户ID时发生异常: {}", e.getMessage());
        }
        
        // 如果没有认证信息,返回默认用户ID(开发阶段使用)
        logger.debug("未找到认证信息,使用默认用户ID");
        return 1L;
        // 如果没有认证信息,返回null表示未登录
        logger.debug("未找到有效的认证信息");
        return null;
    }
    /**
     * 从HTTP请求中获取JWT token
     */
    private String getTokenFromRequest() {
        try {
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            if (attributes != null) {
                HttpServletRequest request = attributes.getRequest();
                String authHeader = request.getHeader("Authorization");
                if (authHeader != null && authHeader.startsWith("Bearer ")) {
                    return authHeader.substring(7);
                }
            }
        } catch (Exception e) {
            logger.debug("获取JWT token时发生异常: {}", e.getMessage());
        }
        return null;
    }
    /**
     * 获取当前用户关联的员工信息
     *
     * @return 员工信息,如果当前用户不是员工则返回空
     */
    public Optional<Employee> getCurrentEmployee() {
        Long userId = getCurrentUserId();
        if (userId == null) {
            logger.warn("无法获取当前用户ID");
            return Optional.empty();
        }
        try {
            Optional<Employee> employee = employeeRepository.findByUserId(userId);
            if (employee.isPresent()) {
                logger.debug("找到当前用户关联的员工: {}", employee.get().getName());
            } else {
                logger.debug("当前用户(ID: {})不是员工", userId);
            }
            return employee;
        } catch (Exception e) {
            logger.error("查询员工信息时发生异常: {}", e.getMessage(), e);
            return Optional.empty();
        }
    }
    /**
@@ -76,6 +141,15 @@
    }
    /**
     * 获取当前用户关联的员工ID
     *
     * @return 员工ID,如果当前用户不是员工则返回null
     */
    public Long getCurrentEmployeeId() {
        return getCurrentEmployee().map(Employee::getId).orElse(null);
    }
    /**
     * 获取当前用户关联的评委ID
     * 
     * @return 评委ID,如果当前用户不是评委则返回null
@@ -85,6 +159,15 @@
    }
    /**
     * 检查当前用户是否为员工
     *
     * @return true如果当前用户是员工,否则false
     */
    public boolean isCurrentUserEmployee() {
        return getCurrentEmployee().isPresent();
    }
    /**
     * 检查当前用户是否为评委
     * 
     * @return true如果当前用户是评委,否则false
backend/src/main/java/com/rongyichuang/employee/repository/EmployeeRepository.java
@@ -49,4 +49,9 @@
     * 根据角色ID查询员工列表
     */
    List<Employee> findByRoleId(String roleId);
    /**
     * 根据用户ID查询员工
     */
    Optional<Employee> findByUserId(Long userId);
}
backend/src/main/java/com/rongyichuang/judge/service/JudgeService.java
@@ -40,7 +40,7 @@
    private final MediaService mediaService;
    private final UserService userService;
    
    @Value("${app.media-url:${app.media.url:}}")
    @Value("${app.media-url}")
    private String mediaBaseUrl;
    public JudgeService(JudgeRepository judgeRepository, TagRepository tagRepository, 
backend/src/main/resources/application.yml
@@ -65,11 +65,6 @@
  bucket: ryc-1379367838
  region: ap-chengdu
# JWT配置
jwt:
  secret: ryc-jwt-secret-key-2024
  expiration: 86400000 # 24小时
# 日志配置
logging:
  level:
@@ -81,4 +76,7 @@
# 应用配置
app:
  base-url: https://rych.9village.cn
  jwt:
    secret: ryc-jwt-secret-key-2024-secure-256bit-hmac-sha-algorithm-compatible
    expiration: 86400000 # 24小时
  media-url:  https://ryc-1379367838.cos.ap-chengdu.myqcloud.com
backend/src/main/resources/graphql/auth.graphqls
New file
@@ -0,0 +1,53 @@
extend type Mutation {
    "Web端用户登录"
    webLogin(input: LoginRequest): LoginResponse
}
"登录请求"
input LoginRequest {
    phone: String!
    password: String!
}
"登录响应"
type LoginResponse {
    token: String!
    userInfo: UserInfo!
}
"用户信息"
type UserInfo {
    userId: ID!
    name: String!
    phone: String!
    userType: String!
    employee: EmployeeInfo
    judge: JudgeInfo
    player: PlayerInfo
}
"员工信息"
type EmployeeInfo {
    id: ID!
    name: String!
    roleId: String!
    description: String
}
"评委信息"
type JudgeInfo {
    id: ID!
    name: String!
    title: String
    company: String
    description: String
}
"学员信息"
type PlayerInfo {
    id: ID!
    name: String!
    phone: String
    description: String
    auditState: Int
}
backend/src/test/java/com/rongyichuang/CreateMultiRoleUserTest.java
New file
@@ -0,0 +1,69 @@
package com.rongyichuang;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.ActiveProfiles;
@SpringBootTest
@ActiveProfiles("dev")
public class CreateMultiRoleUserTest {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Test
    public void createMultiRoleUser() {
        try {
            // 检查是否已存在多角色测试用户
            Integer userCount = jdbcTemplate.queryForObject(
                "SELECT COUNT(*) FROM t_user WHERE mobile = '13800000003'",
                Integer.class
            );
            if (userCount > 0) {
                System.out.println("多角色测试用户已存在,跳过创建");
                return;
            }
            // 加密密码 (密码为: 123456)
            String encodedPassword = passwordEncoder.encode("123456");
            // 插入多角色测试用户到 t_user 表
            jdbcTemplate.update(
                "INSERT INTO t_user (name, wx_openid, wx_unionid, state, mobile, password, create_time, update_time, version) VALUES (?, ?, ?, ?, ?, ?, NOW(), NOW(), 0)",
                "多角色测试用户", "test_multi_role_openid", "test_multi_role_unionid", 1, "13800000003", encodedPassword
            );
            // 获取用户ID
            Long userId = jdbcTemplate.queryForObject(
                "SELECT id FROM t_user WHERE mobile = ?", Long.class, "13800000003"
            );
            // 插入为员工
            jdbcTemplate.update(
                "INSERT INTO t_employee (name, phone, role_id, user_id, state, create_time, update_time, version, description) VALUES (?, ?, ?, ?, ?, NOW(), NOW(), 0, ?)",
                "多角色测试用户", "13800000003", "MANAGER", userId, 1, "既是员工又是评委的测试账号"
            );
            // 插入为评委
            jdbcTemplate.update(
                "INSERT INTO t_judge (name, user_id, phone, gender, state, description, create_time, update_time, version, title, company, introduction) VALUES (?, ?, ?, ?, ?, ?, NOW(), NOW(), 0, ?, ?, ?)",
                "多角色测试用户", userId, "13800000003", 1, 1, "既是员工又是评委的测试账号", "技术总监", "测试公司", "拥有管理和技术评审双重职能"
            );
            System.out.println("多角色测试用户创建成功!");
            System.out.println("账号: 13800000003, 密码: 123456");
            System.out.println("该用户同时具有员工和评委角色,用于测试角色优先级");
        } catch (Exception e) {
            System.err.println("创建多角色测试用户失败: " + e.getMessage());
            e.printStackTrace();
        }
    }
}
backend/src/test/java/com/rongyichuang/CreateTestUsersTest.java
New file
@@ -0,0 +1,74 @@
package com.rongyichuang;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.ActiveProfiles;
@SpringBootTest
@ActiveProfiles("dev")
public class CreateTestUsersTest {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Test
    public void createTestUsers() {
        try {
            // 先删除现有的测试用户(如果存在)
            System.out.println("删除现有测试用户...");
            jdbcTemplate.update("DELETE FROM t_employee WHERE phone IN ('13800000001', '13800000002')");
            jdbcTemplate.update("DELETE FROM t_judge WHERE phone IN ('13800000001', '13800000002')");
            jdbcTemplate.update("DELETE FROM t_user WHERE mobile IN ('13800000001', '13800000002') OR phone IN ('13800000001', '13800000002')");
            System.out.println("现有测试用户已删除");
            // 加密密码 (密码为: 123456)
            String encodedPassword = passwordEncoder.encode("123456");
            // 插入测试用户到 t_user 表 (包含密码,同时设置phone和mobile字段)
            jdbcTemplate.update(
                "INSERT INTO t_user (name, wx_openid, wx_unionid, state, mobile, phone, password, create_time, update_time, version) VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), NOW(), 0)",
                "测试员工", "test_employee_openid", "test_employee_unionid", 1, "13800000001", "13800000001", encodedPassword
            );
            jdbcTemplate.update(
                "INSERT INTO t_user (name, wx_openid, wx_unionid, state, mobile, phone, password, create_time, update_time, version) VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), NOW(), 0)",
                "测试评委", "test_judge_openid", "test_judge_unionid", 1, "13800000002", "13800000002", encodedPassword
            );
            // 获取用户ID
            Long employeeUserId = jdbcTemplate.queryForObject(
                "SELECT id FROM t_user WHERE mobile = ?", Long.class, "13800000001"
            );
            Long judgeUserId = jdbcTemplate.queryForObject(
                "SELECT id FROM t_user WHERE mobile = ?", Long.class, "13800000002"
            );
            // 插入测试员工到 t_employee 表 (不包含密码)
            jdbcTemplate.update(
                "INSERT INTO t_employee (name, phone, role_id, user_id, state, create_time, update_time, version, description) VALUES (?, ?, ?, ?, ?, NOW(), NOW(), 0, ?)",
                "测试员工", "13800000001", "ADMIN", employeeUserId, 1, "系统测试员工账号"
            );
            // 插入测试评委到 t_judge 表
            jdbcTemplate.update(
                "INSERT INTO t_judge (name, user_id, phone, gender, state, description, create_time, update_time, version, title, company, introduction) VALUES (?, ?, ?, ?, ?, ?, NOW(), NOW(), 0, ?, ?, ?)",
                "测试评委", judgeUserId, "13800000002", 1, 1, "系统测试评委账号", "高级技术专家", "测试公司", "拥有丰富的技术评审经验"
            );
            System.out.println("测试用户创建成功!");
            System.out.println("员工账号: 13800000001, 密码: 123456");
            System.out.println("评委账号: 13800000002, 密码: 123456");
        } catch (Exception e) {
            System.err.println("创建测试用户失败: " + e.getMessage());
            e.printStackTrace();
        }
    }
}
backend/src/test/java/com/rongyichuang/VerifyUserDataTest.java
New file
@@ -0,0 +1,94 @@
package com.rongyichuang;
import com.rongyichuang.user.entity.User;
import com.rongyichuang.user.repository.UserRepository;
import com.rongyichuang.employee.entity.Employee;
import com.rongyichuang.employee.repository.EmployeeRepository;
import com.rongyichuang.judge.entity.Judge;
import com.rongyichuang.judge.repository.JudgeRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.ActiveProfiles;
import java.util.Optional;
@SpringBootTest
@ActiveProfiles("dev")
public class VerifyUserDataTest {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private EmployeeRepository employeeRepository;
    @Autowired
    private JudgeRepository judgeRepository;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Test
    public void verifyTestUsers() {
        System.out.println("=== 验证测试用户数据 ===");
        // 验证员工用户
        verifyUser("13800000001", "员工");
        // 验证评委用户
        verifyUser("13800000002", "评委");
        // 验证多角色用户
        verifyUser("13800000003", "多角色");
    }
    private void verifyUser(String phone, String userType) {
        System.out.println("\n--- 验证" + userType + "用户: " + phone + " ---");
        // 查找用户
        Optional<User> userOpt = userRepository.findByPhone(phone);
        if (userOpt.isEmpty()) {
            System.out.println("❌ 用户不存在");
            return;
        }
        User user = userOpt.get();
        System.out.println("✅ 用户存在: ID=" + user.getId() + ", 姓名=" + user.getName());
        // 验证密码
        if (user.getPassword() == null || user.getPassword().trim().isEmpty()) {
            System.out.println("❌ 用户密码为空");
        } else {
            boolean passwordMatch = passwordEncoder.matches("123456", user.getPassword());
            System.out.println(passwordMatch ? "✅ 密码验证成功" : "❌ 密码验证失败");
            System.out.println("存储的密码哈希: " + user.getPassword().substring(0, 20) + "...");
        }
        // 查找关联的员工信息
        Optional<Employee> employeeOpt = employeeRepository.findByUserId(user.getId());
        if (employeeOpt.isPresent()) {
            Employee employee = employeeOpt.get();
            System.out.println("✅ 员工信息: ID=" + employee.getId() + ", 姓名=" + employee.getName() + ", 角色=" + employee.getRoleId());
        } else {
            System.out.println("❌ 无员工信息");
        }
        // 查找关联的评委信息
        Optional<Judge> judgeOpt = judgeRepository.findByUserId(user.getId());
        if (judgeOpt.isPresent()) {
            Judge judge = judgeOpt.get();
            System.out.println("✅ 评委信息: ID=" + judge.getId() + ", 姓名=" + judge.getName() + ", 职位=" + judge.getTitle());
        } else {
            System.out.println("❌ 无评委信息");
        }
        // 检查权限
        if (employeeOpt.isEmpty() && judgeOpt.isEmpty()) {
            System.out.println("❌ 用户没有权限(既不是员工也不是评委)");
        } else {
            System.out.println("✅ 用户有权限");
        }
    }
}
backend/src/test/java/com/rongyichuang/auth/DirectLoginTest.java
New file
@@ -0,0 +1,40 @@
package com.rongyichuang.auth;
import com.rongyichuang.auth.dto.LoginRequest;
import com.rongyichuang.auth.dto.LoginResponse;
import com.rongyichuang.auth.service.AuthService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class DirectLoginTest {
    @Autowired
    private AuthService authService;
    @Test
    public void testDirectLogin() {
        try {
            System.out.println("=== 开始直接测试AuthService登录方法 ===");
            LoginRequest request = new LoginRequest();
            request.setPhone("13800000001");
            request.setPassword("123456");
            System.out.println("登录请求: phone=" + request.getPhone() + ", password=" + request.getPassword());
            LoginResponse response = authService.login(request);
            System.out.println("登录成功!");
            System.out.println("Token: " + response.getToken());
            System.out.println("用户ID: " + response.getUserInfo().getUserId());
            System.out.println("用户名: " + response.getUserInfo().getName());
            System.out.println("用户类型: " + response.getUserInfo().getUserType());
        } catch (Exception e) {
            System.err.println("登录失败: " + e.getMessage());
            e.printStackTrace();
        }
    }
}
backend/src/test/java/com/rongyichuang/auth/GraphQLLoginTest.java
New file
@@ -0,0 +1,40 @@
package com.rongyichuang.auth;
import com.rongyichuang.auth.api.AuthGraphqlApi;
import com.rongyichuang.auth.dto.LoginRequest;
import com.rongyichuang.auth.dto.LoginResponse;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class GraphQLLoginTest {
    @Autowired
    private AuthGraphqlApi authGraphqlApi;
    @Test
    public void testGraphQLLogin() {
        try {
            System.out.println("=== 开始测试GraphQL API层登录方法 ===");
            LoginRequest request = new LoginRequest();
            request.setPhone("13800000001");
            request.setPassword("123456");
            System.out.println("登录请求: phone=" + request.getPhone() + ", password=" + request.getPassword());
            LoginResponse response = authGraphqlApi.webLogin(request);
            System.out.println("GraphQL登录成功!");
            System.out.println("Token: " + response.getToken());
            System.out.println("用户ID: " + response.getUserInfo().getUserId());
            System.out.println("用户名: " + response.getUserInfo().getName());
            System.out.println("用户类型: " + response.getUserInfo().getUserType());
        } catch (Exception e) {
            System.err.println("GraphQL登录失败: " + e.getMessage());
            e.printStackTrace();
        }
    }
}
backend/src/test/java/com/rongyichuang/employee/DirectEmployeeServiceTest.java
New file
@@ -0,0 +1,50 @@
package com.rongyichuang.employee;
import com.rongyichuang.employee.dto.request.EmployeeInput;
import com.rongyichuang.employee.dto.response.EmployeeResponse;
import com.rongyichuang.employee.service.EmployeeService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
/**
 * 直接测试EmployeeService的saveEmployee方法
 */
@SpringBootTest
@ActiveProfiles("test")
public class DirectEmployeeServiceTest {
    @Autowired
    private EmployeeService employeeService;
    @Test
    public void testSaveEmployee() {
        try {
            // 创建员工输入
            EmployeeInput input = new EmployeeInput();
            input.setName("测试员工Service");
            input.setPhone("13900000003");
            input.setPassword("test123");
            input.setRoleId("EMPLOYEE");
            input.setDescription("直接测试EmployeeService");
            System.out.println("开始测试EmployeeService.saveEmployee...");
            System.out.println("输入参数: " + input.getName() + ", " + input.getPhone());
            // 调用服务方法
            EmployeeResponse response = employeeService.saveEmployee(input);
            System.out.println("EmployeeService测试成功!");
            System.out.println("员工ID: " + response.getId());
            System.out.println("员工姓名: " + response.getName());
            System.out.println("员工手机: " + response.getPhone());
            System.out.println("员工角色: " + response.getRoleId());
        } catch (Exception e) {
            System.err.println("EmployeeService测试失败: " + e.getMessage());
            e.printStackTrace();
            throw e;
        }
    }
}
check_table.ps1
File was deleted
test-media-query.json
File was deleted
test_api.ps1
File was deleted
test_complete.ps1
File was deleted
test_complete_fixed.ps1
File was deleted
test_debug.ps1
File was deleted
test_employee_api_fix.ps1
File was deleted
test_employee_management.ps1
File was deleted
test_no_state.ps1
File was deleted
test_simple.ps1
File was deleted
test_simple_query.ps1
File was deleted
test_users.sql
New file
@@ -0,0 +1,13 @@
-- 测试用户数据
-- 插入测试用户到 t_user 表
INSERT INTO t_user (name, wx_open_id, wx_union_id, state, mobile, create_time, update_time, version) VALUES
('测试员工', 'test_employee_openid', 'test_employee_unionid', 1, '13800000001', NOW(), NOW(), 0),
('测试评委', 'test_judge_openid', 'test_judge_unionid', 1, '13800000002', NOW(), NOW(), 0);
-- 插入测试员工到 t_employee 表
INSERT INTO t_employee (name, phone, password, role_id, user_id, state, create_time, update_time, version, description) VALUES
('测试员工', '13800000001', '$2a$10$N.zmdr9k7uOCQb96VdOYx.93UGOXOQAaJLVrpJeJEPPTJZaGqQ4oa', 'ADMIN', 1, 1, NOW(), NOW(), 0, '系统测试员工账号');
-- 插入测试评委到 t_judge 表
INSERT INTO t_judge (name, user_id, phone, gender, state, description, create_time, update_time, version, title, company, introduction) VALUES
('测试评委', 2, '13800000002', 1, 1, '系统测试评委账号', NOW(), NOW(), 0, '高级技术专家', '测试公司', '拥有丰富的技术评审经验');
web/src/layout/index.vue
@@ -33,7 +33,8 @@
          <el-dropdown @command="handleCommand">
            <span class="user-info">
              <el-icon><User /></el-icon>
              <span>管理员</span>
              <span>{{ currentUserName }}</span>
              <el-tag size="small" style="margin-left: 8px">{{ currentUserRole }}</el-tag>
              <el-icon><ArrowDown /></el-icon>
            </span>
            <template #dropdown>
@@ -58,9 +59,14 @@
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getCurrentUserDisplayName, getCurrentUserRoleText, clearAuth } from '@/utils/auth'
const route = useRoute()
const router = useRouter()
// 当前用户信息
const currentUserName = computed(() => getCurrentUserDisplayName())
const currentUserRole = computed(() => getCurrentUserRoleText())
// 菜单项
const menuItems = [
@@ -90,7 +96,7 @@
        type: 'warning'
      })
      
      localStorage.removeItem('token')
      clearAuth()
      ElMessage.success('退出登录成功')
      router.push('/login')
    } catch {
web/src/router/index.ts
@@ -1,4 +1,5 @@
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
import { isLoggedIn } from '@/utils/auth'
const routes: RouteRecordRaw[] = [
  {
@@ -105,4 +106,28 @@
  routes
})
// 路由守卫
router.beforeEach((to, from, next) => {
  // 如果是登录页面,直接放行
  if (to.path === '/login') {
    // 如果已经登录,重定向到首页
    if (isLoggedIn()) {
      next('/')
    } else {
      next()
    }
    return
  }
  // 检查是否已登录
  if (!isLoggedIn()) {
    // 未登录,重定向到登录页
    next('/login')
    return
  }
  // 已登录,正常访问
  next()
})
export default router
web/src/utils/auth.ts
New file
@@ -0,0 +1,165 @@
/**
 * 认证相关工具函数
 */
const TOKEN_KEY = 'auth_token'
const USER_INFO_KEY = 'user_info'
// 用户信息接口定义
export interface UserInfo {
  userId: string
  name: string
  phone: string
  userType: string // 主要角色类型:employee > judge > player
  employee?: {
    id: string
    name: string
    roleId: string
    description?: string
  }
  judge?: {
    id: string
    name: string
    title?: string
    company?: string
    description?: string
  }
  player?: {
    id: string
    name: string
    phone?: string
    description?: string
    auditState?: number
  }
}
// Token 管理 - 使用 localStorage 持久化存储
export function setToken(token: string): void {
  localStorage.setItem(TOKEN_KEY, token)
}
export function getToken(): string | null {
  return localStorage.getItem(TOKEN_KEY)
}
export function removeToken(): void {
  localStorage.removeItem(TOKEN_KEY)
}
// 用户信息管理 - 使用 localStorage 持久化存储
export function setUserInfo(userInfo: UserInfo): void {
  localStorage.setItem(USER_INFO_KEY, JSON.stringify(userInfo))
}
export function getUserInfo(): UserInfo | null {
  const userInfoStr = localStorage.getItem(USER_INFO_KEY)
  if (userInfoStr) {
    try {
      return JSON.parse(userInfoStr)
    } catch (error) {
      console.error('解析用户信息失败:', error)
      removeUserInfo()
    }
  }
  return null
}
export function removeUserInfo(): void {
  localStorage.removeItem(USER_INFO_KEY)
}
// 登录状态检查
export function isLoggedIn(): boolean {
  const token = getToken()
  const userInfo = getUserInfo()
  return !!(token && userInfo)
}
// 清除所有认证数据
export function clearAuth(): void {
  removeToken()
  removeUserInfo()
}
// 获取当前用户显示名称(优先级:employee > judge > player)
export function getCurrentUserDisplayName(): string {
  const userInfo = getUserInfo()
  if (!userInfo) return '未知用户'
  // 根据角色优先级返回对应的名称
  if (userInfo.employee) {
    return userInfo.employee.name
  } else if (userInfo.judge) {
    return userInfo.judge.name
  } else if (userInfo.player) {
    return userInfo.player.name
  }
  return userInfo.name || '未知用户'
}
// 获取当前用户角色
export function getCurrentUserRole(): string {
  const userInfo = getUserInfo()
  if (!userInfo) return ''
  if (userInfo.employee) {
    return userInfo.employee.description || '员工'
  }
  if (userInfo.judge) {
    return userInfo.judge.title || '评委'
  }
  return userInfo.userType === 'employee' ? '员工' : '评委'
}
// 获取当前用户角色显示文本
export function getCurrentUserRoleText(): string {
  const userInfo = getUserInfo()
  const role = userInfo?.userType || 'unknown'
  switch (role) {
    case 'employee':
      return '员工'
    case 'judge':
      return '评委'
    case 'player':
      return '学员'
    default:
      return '未知角色'
  }
}
// 检查当前用户是否为员工
export function isEmployee(): boolean {
  const userInfo = getUserInfo()
  return userInfo?.userType === 'employee' && !!userInfo.employee
}
// 检查当前用户是否为评委
export function isJudge(): boolean {
  const userInfo = getUserInfo()
  return userInfo?.userType === 'judge' && !!userInfo.judge
}
// 检查当前用户是否为学员
export function isPlayer(): boolean {
  const userInfo = getUserInfo()
  return userInfo?.userType === 'player' && !!userInfo.player
}
// 获取当前用户的主要角色信息
export function getCurrentRoleInfo(): any {
  const userInfo = getUserInfo()
  if (!userInfo) return null
  if (userInfo.employee) {
    return userInfo.employee
  } else if (userInfo.judge) {
    return userInfo.judge
  } else if (userInfo.player) {
    return userInfo.player
  }
  return null
}
web/src/utils/graphql.ts
New file
@@ -0,0 +1,143 @@
/**
 * GraphQL客户端工具
 */
import { getToken } from './auth'
// GraphQL端点
const GRAPHQL_ENDPOINT = '/api/graphql'
export interface GraphQLResponse<T = any> {
  data?: T
  errors?: Array<{
    message: string
    locations?: Array<{
      line: number
      column: number
    }>
    path?: string[]
  }>
}
/**
 * 发送GraphQL请求
 */
export async function graphqlRequest<T = any>(
  query: string,
  variables?: Record<string, any>
): Promise<T> {
  const token = getToken()
  const headers: Record<string, string> = {
    'Content-Type': 'application/json',
  }
  // 如果有token,添加到请求头
  if (token) {
    headers['Authorization'] = `Bearer ${token}`
  }
  const response = await fetch(GRAPHQL_ENDPOINT, {
    method: 'POST',
    headers,
    body: JSON.stringify({
      query,
      variables
    })
  })
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`)
  }
  const result: GraphQLResponse<T> = await response.json()
  if (result.errors && result.errors.length > 0) {
    throw new Error(result.errors[0].message)
  }
  if (!result.data) {
    throw new Error('No data returned from GraphQL')
  }
  return result.data
}
/**
 * 登录API
 */
export interface LoginRequest {
  phone: string
  password: string
}
export interface LoginResponse {
  webLogin: {
    token: string
    userInfo: {
      userId: string
      name: string
      phone: string
      userType: string
      employee?: {
        id: string
        name: string
        roleId: string
        description?: string
      }
      judge?: {
        id: string
        name: string
        title?: string
        company?: string
        description?: string
      }
      player?: {
        id: string
        name: string
        phone?: string
        description?: string
        auditState?: number
      }
    }
  }
}
export async function loginApi(request: LoginRequest): Promise<LoginResponse['webLogin']> {
  const query = `
    mutation WebLogin($input: LoginRequest!) {
      webLogin(input: $input) {
        token
        userInfo {
          userId
          name
          phone
          userType
          employee {
            id
            name
            roleId
            description
          }
          judge {
            id
            name
            title
            company
            description
          }
          player {
            id
            name
            phone
            description
            auditState
          }
        }
      }
    }
  `
  const response = await graphqlRequest<LoginResponse>(query, { input: request })
  return response.webLogin
}
web/src/views/employee/EmployeeForm.vue
@@ -39,16 +39,27 @@
        />
      </el-form-item>
      <el-form-item label="重置密码" prop="password" v-if="isEdit">
        <el-input
          v-model="form.password"
          type="password"
          placeholder="请输入新密码(6-20位)"
          maxlength="20"
          show-password
          @focus="handlePasswordFocus"
          @input="handlePasswordInput"
        />
      <el-form-item label="密码" prop="password" v-if="isEdit">
        <div style="display: flex; align-items: center; gap: 10px;">
          <el-input
            v-model="form.password"
            type="password"
            :placeholder="isPasswordModified ? '请输入新密码(6-20位,包含字母和数字)' : '点击重置密码按钮来修改密码'"
            maxlength="20"
            show-password
            :disabled="!isPasswordModified"
            @focus="handlePasswordFocus"
            @input="handlePasswordInput"
            style="flex: 1;"
          />
          <el-button
            type="primary"
            size="small"
            @click="handleResetPassword"
          >
            重置密码
          </el-button>
        </div>
      </el-form-item>
      <el-form-item label="员工角色" prop="roleId">
@@ -164,10 +175,16 @@
  password: [
    { 
      validator: (rule, value, callback) => {
        if (!value) {
        // 编辑模式下,如果是占位符密码且未修改,则跳过验证
        if (isEdit.value && value === '••••••••' && !isPasswordModified.value) {
          callback()
          return
        }
        if (!value || value.trim() === '') {
          callback(new Error('请输入登录密码'))
        } else if (value.length < 6 || value.length > 20) {
          callback(new Error('密码长度应在6-20个字符之间'))
        } else if (!/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d@$!%*?&]{6,}$/.test(value)) {
          callback(new Error('密码至少6个字符,必须包含字母和数字'))
        } else {
          callback()
        }
@@ -235,6 +252,13 @@
  }
}
// 处理重置密码
const handleResetPassword = () => {
  form.password = ''
  isPasswordModified.value = true
  ElMessage.success('密码已清空,请输入新密码')
}
// 提交表单
const handleSubmit = async () => {
  if (!formRef.value) return
web/src/views/login/index.vue
@@ -62,6 +62,8 @@
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { loginApi } from '@/utils/graphql'
import { setToken, setUserInfo } from '@/utils/auth'
const router = useRouter()
const loginFormRef = ref<FormInstance>()
@@ -93,17 +95,38 @@
    await loginFormRef.value.validate()
    loading.value = true
    
    // TODO: 调用登录API
    // 模拟登录
    setTimeout(() => {
      localStorage.setItem('token', 'mock-token-' + Date.now())
      ElMessage.success('登录成功')
      router.push('/')
      loading.value = false
    }, 1000)
    // 调用登录API
    const response = await loginApi({
      phone: loginForm.phone,
      password: loginForm.password
    })
    
  } catch (error) {
    // 保存token和用户信息
    setToken(response.token)
    setUserInfo(response.userInfo)
    ElMessage.success('登录成功')
    router.push('/')
  } catch (error: any) {
    console.error('登录失败:', error)
    // 显示错误信息
    let errorMessage = '登录失败'
    if (error.message) {
      if (error.message.includes('用户不存在')) {
        errorMessage = '用户不存在,请检查手机号'
      } else if (error.message.includes('密码不正确')) {
        errorMessage = '密码不正确,请重新输入'
      } else if (error.message.includes('没有权限')) {
        errorMessage = '您没有访问权限,请联系管理员'
      } else {
        errorMessage = error.message
      }
    }
    ElMessage.error(errorMessage)
  } finally {
    loading.value = false
  }
}