从零打造AI面试系统全栈开发

🤖 AI面试系统开发完整教程

📋 项目概述

本教程将带你从零开始构建一个完整的AI面试系统,包含前端、后端、AI集成和部署的全流程。

源码地址

技术栈

  • 前端: React + TypeScript + Vite + Vaadin Components
  • 后端: Spring Boot + Spring Security + JPA
  • 数据库: PostgreSQL (含向量扩展)
  • AI集成: 阿里千问大模型 + Spring AI
  • 实时通信: WebSocket + WebRTC
  • 部署: Docker + Docker Compose

项目结构

复制代码
AI-Interview/
├── frontend/                 # React前端项目
│   ├── src/
│   │   ├── components/      # React组件
│   │   ├── services/        # API服务
│   │   └── utils/           # 工具函数
├── consumer-api/            # Spring Boot后端API
│   ├── src/main/java/
│   │   ├── controller/      # REST控制器
│   │   ├── service/         # 业务逻辑层
│   │   ├── repository/      # 数据访问层
│   │   └── entity/          # 实体类
├── common/                  # 公共模块
└── docs/                    # 文档

🎯 学习路径 - 阶段性项目验收

阶段1-2: 基础面试页面 + 视频通话功能

阶段1: 项目环境搭建与基础架构
1.1 创建Spring Boot后端项目

创建主启动类

java 复制代码
// consumer-api/src/main/java/com/ai/interview/Application.java
@SpringBootApplication
@EnableJpaRepositories
@EnableWebSecurity
public class Application {
    
    /**
     * 应用程序主入口点
     * @param args 命令行参数
     */
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
    
    /**
     * 配置CORS跨域支持
     * 知识点: CORS(Cross-Origin Resource Sharing)是浏览器安全策略
     * 前后端分离项目必须配置CORS才能正常通信
     */
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        // 允许的源地址
        configuration.setAllowedOriginPatterns(Arrays.asList("*"));
        // 允许的HTTP方法
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        // 允许的请求头
        configuration.setAllowedHeaders(Arrays.asList("*"));
        // 允许携带认证信息
        configuration.setAllowCredentials(true);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

知识点讲解:

  • @SpringBootApplication: Spring Boot核心注解,包含了自动配置、组件扫描等功能
  • @EnableJpaRepositories: 启用JPA数据访问,Spring会自动创建Repository实现类
  • CORS配置是前后端分离项目的必备配置,解决浏览器同源策略限制

配置文件设置

yaml 复制代码
# consumer-api/src/main/resources/application.yml
server:
  port: 8080
  servlet:
    context-path: /api

spring:
  # 数据源配置
  datasource:
    url: jdbc:postgresql://localhost:5432/ai_interview
    username: postgres
    password: password
    driver-class-name: org.postgresql.Driver
    
  # JPA配置
  jpa:
    hibernate:
      ddl-auto: update  # 自动更新数据库结构
    show-sql: true      # 显示SQL语句
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQLDialect
        format_sql: true
        
  # AI配置
  ai:
    alibaba:
      api-key: ${ALIBABA_AI_API_KEY:your-api-key}
      
# 日志配置
logging:
  level:
    com.ai.interview: DEBUG
    org.springframework.security: DEBUG

知识点讲解:

  • ddl-auto: update: 开发阶段使用,自动根据实体类更新数据库表结构
  • show-sql: true: 开发调试时显示Hibernate生成的SQL语句
  • 环境变量配置(${ALIBABA_AI_API_KEY:your-api-key}):生产环境安全实践
1.2 创建基础实体类

用户实体类

java 复制代码
// consumer-api/src/main/java/com/ai/interview/entity/User.java
@Entity
@Table(name = "users")
@Data  // Lombok注解,自动生成getter/setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    /**
     * 用户名,业务主键
     * 知识点: @Column注解用于自定义列属性
     * unique=true 确保用户名唯一性
     * nullable=false 确保非空约束
     */
    @Column(unique = true, nullable = false, length = 50)
    private String username;
    
    /**
     * 密码,使用BCrypt加密存储
     * 知识点: 密码安全存储,永远不要明文存储密码
     */
    @Column(nullable = false)
    private String password;
    
    /**
     * 邮箱地址
     */
    @Column(unique = true, nullable = false)
    private String email;
    
    /**
     * 用户角色
     * 知识点: @Enumerated注解处理枚举类型
     * EnumType.STRING: 存储枚举的字符串值,可读性好
     */
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private UserRole role = UserRole.USER;
    
    /**
     * 创建时间
     * 知识点: @CreationTimestamp自动设置创建时间
     */
    @CreationTimestamp
    @Column(name = "created_at")
    private LocalDateTime createdAt;
    
    /**
     * 更新时间
     * 知识点: @UpdateTimestamp自动更新修改时间
     */
    @UpdateTimestamp
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
    
    /**
     * 账户状态
     */
    private Boolean enabled = true;
}

/**
 * 用户角色枚举
 * 知识点: 枚举类型在Java中的最佳实践
 */
public enum UserRole {
    USER("普通用户"),
    ADMIN("管理员");
    
    private final String description;
    
    UserRole(String description) {
        this.description = description;
    }
    
    public String getDescription() {
        return description;
    }
}

面试记录实体类

java 复制代码
// consumer-api/src/main/java/com/ai/interview/entity/InterviewSession.java
@Entity
@Table(name = "interview_sessions")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class InterviewSession {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    /**
     * 面试会话与用户的多对一关系
     * 知识点: JPA关联关系映射
     * @ManyToOne: 多个面试记录对应一个用户
     * @JoinColumn: 指定外键列名
     * fetch = FetchType.LAZY: 懒加载,只有访问时才查询用户信息
     */
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)
    private User user;
    
    /**
     * 面试类型
     */
    @Enumerated(EnumType.STRING)
    private InterviewType type;
    
    /**
     * 面试状态
     */
    @Enumerated(EnumType.STRING)
    private InterviewStatus status = InterviewStatus.PENDING;
    
    /**
     * 面试开始时间
     */
    @Column(name = "start_time")
    private LocalDateTime startTime;
    
    /**
     * 面试结束时间
     */
    @Column(name = "end_time")
    private LocalDateTime endTime;
    
    /**
     * 面试评分
     */
    private Integer score;
    
    /**
     * AI评价内容
     * 知识点: @Lob注解用于存储大文本
     */
    @Lob
    private String feedback;
    
    @CreationTimestamp
    @Column(name = "created_at")
    private LocalDateTime createdAt;
}

/**
 * 面试类型枚举
 */
public enum InterviewType {
    TECHNICAL("技术面试"),
    BEHAVIORAL("行为面试"),
    CODING("编程面试");
    
    private final String description;
    
    InterviewType(String description) {
        this.description = description;
    }
    
    public String getDescription() {
        return description;
    }
}

/**
 * 面试状态枚举
 */
public enum InterviewStatus {
    PENDING("待开始"),
    IN_PROGRESS("进行中"),
    COMPLETED("已完成"),
    CANCELLED("已取消");
    
    private final String description;
    
    InterviewStatus(String description) {
        this.description = description;
    }
    
    public String getDescription() {
        return description;
    }
}
1.3 创建数据访问层

用户Repository

java 复制代码
// consumer-api/src/main/java/com/ai/interview/repository/UserRepository.java
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    /**
     * 根据用户名查找用户
     * 知识点: Spring Data JPA方法命名规则
     * findBy + 属性名:会自动生成查询方法
     * Optional<T>: Java 8引入,优雅处理可能为null的情况
     */
    Optional<User> findByUsername(String username);
    
    /**
     * 根据邮箱查找用户
     */
    Optional<User> findByEmail(String email);
    
    /**
     * 检查用户名是否存在
     * 知识点: existsBy开头的方法返回boolean类型
     */
    boolean existsByUsername(String username);
    
    /**
     * 检查邮箱是否存在
     */
    boolean existsByEmail(String email);
    
    /**
     * 根据角色查找用户列表
     * 知识点: List<T>返回类型表示可能有多个结果
     */
    List<User> findByRole(UserRole role);
    
    /**
     * 根据启用状态查找用户
     */
    List<User> findByEnabled(Boolean enabled);
    
    /**
     * 自定义查询:根据用户名模糊查找
     * 知识点: @Query注解编写自定义JPQL查询
     * ?1表示第一个参数,%用于模糊匹配
     */
    @Query("SELECT u FROM User u WHERE u.username LIKE %?1%")
    List<User> findByUsernameContaining(String username);
}

面试记录Repository

java 复制代码
// consumer-api/src/main/java/com/ai/interview/repository/InterviewSessionRepository.java
@Repository
public interface InterviewSessionRepository extends JpaRepository<InterviewSession, Long> {
    
    /**
     * 根据用户ID查找面试记录
     * 知识点: 关联实体的查询方式
     * user.id表示通过user关联实体的id属性查询
     */
    List<InterviewSession> findByUserId(Long userId);
    
    /**
     * 根据面试类型查找记录
     */
    List<InterviewSession> findByType(InterviewType type);
    
    /**
     * 根据面试状态查找记录
     */
    List<InterviewSession> findByStatus(InterviewStatus status);
    
    /**
     * 查找用户的最新面试记录
     * 知识点: 排序和限制结果数量
     * ORDER BY创建时间倒序,取第一条
     */
    @Query("SELECT i FROM InterviewSession i WHERE i.user.id = ?1 ORDER BY i.createdAt DESC")
    List<InterviewSession> findLatestByUserId(Long userId, Pageable pageable);
    
    /**
     * 统计用户的面试次数
     * 知识点: 聚合查询COUNT函数
     */
    @Query("SELECT COUNT(i) FROM InterviewSession i WHERE i.user.id = ?1")
    Long countByUserId(Long userId);
    
    /**
     * 查找指定时间范围内的面试记录
     * 知识点: 时间范围查询
     * BETWEEN关键字用于范围查询
     */
    @Query("SELECT i FROM InterviewSession i WHERE i.startTime BETWEEN ?1 AND ?2")
    List<InterviewSession> findByTimeRange(LocalDateTime start, LocalDateTime end);
}

知识点总结:

  1. JPA Repository继承层次 : JpaRepositoryPagingAndSortingRepositoryCrudRepository
  2. 方法命名规则: Spring Data JPA通过方法名自动生成查询
  3. 关联查询 : 通过.操作符访问关联实体属性
  4. Optional类型: 避免NullPointerException的最佳实践
  5. 分页查询 : 使用Pageable参数实现分页功能
1.4 创建前端React项目结构

前端项目package.json

json 复制代码
{
  "name": "ai-interview-frontend",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@vaadin/react-components": "^24.3.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.8.0",
    "axios": "^1.3.0",
    "simple-peer": "^9.11.1"
  },
  "devDependencies": {
    "@types/react": "^18.0.27",
    "@types/react-dom": "^18.0.10",
    "@vitejs/plugin-react": "^3.1.0",
    "typescript": "^4.9.3",
    "vite": "^4.1.0"
  }
}

知识点讲解:

  • Vite: 现代前端构建工具,比Webpack更快的热重载
  • @vaadin/react-components: 企业级UI组件库,提供丰富的表单和布局组件
  • simple-peer: WebRTC的简化封装,用于实现视频通话功能
  • axios: HTTP客户端库,用于API调用

Vite配置文件

typescript 复制代码
// frontend/vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

/**
 * Vite配置
 * 知识点: Vite是基于ES模块的构建工具
 */
export default defineConfig({
  plugins: [react()],
  
  // 开发服务器配置
  server: {
    port: 3000,
    // 代理配置,解决开发环境跨域问题
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        // rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  },
  
  // 构建配置
  build: {
    outDir: 'dist',
    sourcemap: true
  }
});

知识点讲解:

  • 代理配置: 开发环境下将API请求代理到后端服务器,避免CORS问题
  • sourcemap: 生成源码映射,便于生产环境调试
1.5 创建基础API服务

API服务基类

typescript 复制代码
// frontend/src/services/api.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';

/**
 * API响应数据结构
 * 知识点: TypeScript泛型接口,T表示数据的具体类型
 */
export interface ApiResponse<T = any> {
  success: boolean;
  data: T;
  message: string;
  code: number;
}

/**
 * API服务类
 * 知识点: 单例模式,全局统一的API客户端
 */
class ApiService {
  private axiosInstance: AxiosInstance;
  
  constructor() {
    // 创建axios实例
    this.axiosInstance = axios.create({
      baseURL: '/api',
      timeout: 10000,
      headers: {
        'Content-Type': 'application/json'
      }
    });
    
    // 请求拦截器
    this.setupRequestInterceptor();
    // 响应拦截器
    this.setupResponseInterceptor();
  }
  
  /**
   * 设置请求拦截器
   * 知识点: 拦截器用于统一处理请求/响应
   * 在每个请求发送前自动添加认证token
   */
  private setupRequestInterceptor(): void {
    this.axiosInstance.interceptors.request.use(
      (config: AxiosRequestConfig) => {
        // 从localStorage获取token
        const token = localStorage.getItem('user_token');
        if (token) {
          // 添加认证头
          config.headers = {
            ...config.headers,
            'user_token': token
          };
        }
        
        console.log('发送请求:', config.method?.toUpperCase(), config.url);
        return config;
      },
      (error) => {
        console.error('请求错误:', error);
        return Promise.reject(error);
      }
    );
  }
  
  /**
   * 设置响应拦截器
   * 知识点: 统一处理响应数据和错误
   */
  private setupResponseInterceptor(): void {
    this.axiosInstance.interceptors.response.use(
      (response: AxiosResponse<ApiResponse>) => {
        console.log('收到响应:', response.status, response.data);
        return response;
      },
      (error) => {
        // 统一错误处理
        if (error.response?.status === 401) {
          // token失效,跳转到登录页
          localStorage.removeItem('user_token');
          window.location.href = '/login';
        }
        
        console.error('响应错误:', error.response?.data || error.message);
        return Promise.reject(error);
      }
    );
  }
  
  /**
   * GET请求
   * 知识点: 泛型方法,T表示返回数据的类型
   */
  async get<T = any>(url: string, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
    const response = await this.axiosInstance.get<ApiResponse<T>>(url, config);
    return response.data;
  }
  
  /**
   * POST请求
   */
  async post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
    const response = await this.axiosInstance.post<ApiResponse<T>>(url, data, config);
    return response.data;
  }
  
  /**
   * PUT请求
   */
  async put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
    const response = await this.axiosInstance.put<ApiResponse<T>>(url, data, config);
    return response.data;
  }
  
  /**
   * DELETE请求
   */
  async delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
    const response = await this.axiosInstance.delete<ApiResponse<T>>(url, config);
    return response.data;
  }
}

// 导出单例实例
export const apiService = new ApiService();

知识点讲解:

  1. 单例模式: 确保全局只有一个API客户端实例
  2. 拦截器机制: 统一处理请求头添加和错误处理
  3. TypeScript泛型: 提供类型安全的API调用
  4. Promise/async-await: 现代JavaScript异步编程
阶段2: 用户认证与登录页面
2.1 实现Spring Security配置

Security配置类

java 复制代码
// consumer-api/src/main/java/com/ai/interview/config/SecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Autowired
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    
    @Autowired
    private JwtTokenFilter jwtTokenFilter;
    
    /**
     * 密码编码器配置
     * 知识点: BCrypt是目前最安全的密码加密算法之一
     * strength=12表示加密强度,数字越大越安全但速度越慢
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }
    
    /**
     * 认证管理器配置
     * 知识点: AuthenticationManager用于处理用户认证
     */
    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
    
    /**
     * 认证提供者配置
     * 知识点: DaoAuthenticationProvider使用数据库验证用户
     */
    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }
    
    /**
     * 安全过滤链配置
     * 知识点: Spring Security 6.x新的配置方式
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // 禁用CSRF,因为我们使用JWT token
            .csrf(csrf -> csrf.disable())
            
            // 配置CORS
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            
            // 配置认证异常处理
            .exceptionHandling(exceptions -> exceptions
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
            )
            
            // 配置会话管理为无状态
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            
            // 配置URL访问权限
            .authorizeHttpRequests(authz -> authz
                // 公开接口,无需认证
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
                // 管理员接口
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                // 其他所有接口需要认证
                .anyRequest().authenticated()
            );
        
        // 配置认证提供者
        http.authenticationProvider(authenticationProvider());
        
        // 添加JWT过滤器
        http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
    
    /**
     * CORS配置
     */
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOriginPatterns(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("*"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setAllowCredentials(true);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

JWT工具类

java 复制代码
// consumer-api/src/main/java/com/ai/interview/util/JwtUtil.java
@Component
public class JwtUtil {
    
    // JWT密钥,生产环境应从配置文件读取
    private String jwtSecret = "aiInterviewSecretKey2024";
    
    // token有效期(毫秒)
    private int jwtExpirationMs = 86400000; // 24小时
    
    private final Logger logger = LoggerFactory.getLogger(JwtUtil.class);
    
    /**
     * 生成JWT token
     * 知识点: JWT由三部分组成:header.payload.signature
     * @param userPrincipal 用户主体信息
     * @return JWT token字符串
     */
    public String generateJwtToken(UserPrincipal userPrincipal) {
        return generateTokenFromUsername(userPrincipal.getUsername());
    }
    
    /**
     * 根据用户名生成token
     */
    public String generateTokenFromUsername(String username) {
        return Jwts.builder()
                .setSubject(username)                    // 设置主题(用户名)
                .setIssuedAt(new Date())                // 设置签发时间
                .setExpiration(new Date(System.currentTimeMillis() + jwtExpirationMs)) // 设置过期时间
                .signWith(SignatureAlgorithm.HS512, jwtSecret)  // 使用HS512算法签名
                .compact();
    }
    
    /**
     * 从token中获取用户名
     * 知识点: JWT解析过程,验证签名并提取Claims
     */
    public String getUserNameFromJwtToken(String token) {
        return Jwts.parser()
                .setSigningKey(jwtSecret)
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }
    
    /**
     * 验证JWT token是否有效
     * 知识点: JWT验证包括签名验证、过期时间检查等
     */
    public boolean validateJwtToken(String authToken) {
        try {
            Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
            return true;
        } catch (SignatureException e) {
            logger.error("Invalid JWT signature: {}", e.getMessage());
        } catch (MalformedJwtException e) {
            logger.error("Invalid JWT token: {}", e.getMessage());
        } catch (ExpiredJwtException e) {
            logger.error("JWT token is expired: {}", e.getMessage());
        } catch (UnsupportedJwtException e) {
            logger.error("JWT token is unsupported: {}", e.getMessage());
        } catch (IllegalArgumentException e) {
            logger.error("JWT claims string is empty: {}", e.getMessage());
        }
        
        return false;
    }
}

JWT过滤器

java 复制代码
// consumer-api/src/main/java/com/ai/interview/security/JwtTokenFilter.java
@Component
public class JwtTokenFilter extends OncePerRequestFilter {
    
    @Autowired
    private JwtUtil jwtUtil;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    private static final Logger logger = LoggerFactory.getLogger(JwtTokenFilter.class);
    
    /**
     * JWT过滤器核心逻辑
     * 知识点: 每个请求都会经过这个过滤器进行token验证
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                  HttpServletResponse response, 
                                  FilterChain filterChain) throws ServletException, IOException {
        try {
            // 从请求头中解析JWT token
            String jwt = parseJwt(request);
            
            if (jwt != null && jwtUtil.validateJwtToken(jwt)) {
                // 从token中获取用户名
                String username = jwtUtil.getUserNameFromJwtToken(jwt);
                
                // 加载用户详情
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                
                // 创建认证对象
                UsernamePasswordAuthenticationToken authentication = 
                    new UsernamePasswordAuthenticationToken(
                        userDetails, 
                        null, 
                        userDetails.getAuthorities()
                    );
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                
                // 设置认证信息到安全上下文
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception e) {
            logger.error("Cannot set user authentication: {}", e);
        }
        
        // 继续过滤链
        filterChain.doFilter(request, response);
    }
    
    /**
     * 从请求中解析JWT token
     * 知识点: 支持多种token传递方式
     */
    private String parseJwt(HttpServletRequest request) {
        // 方式1: 从Authorization header中获取 (Bearer token)
        String headerAuth = request.getHeader("Authorization");
        if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
            return headerAuth.substring(7);
        }
        
        // 方式2: 从自定义header中获取
        String tokenHeader = request.getHeader("user_token");
        if (StringUtils.hasText(tokenHeader)) {
            return tokenHeader;
        }
        
        return null;
    }
}
2.2 实现用户服务层

用户详情服务

java 复制代码
// consumer-api/src/main/java/com/ai/interview/security/UserDetailsServiceImpl.java
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    
    @Autowired
    private UserRepository userRepository;
    
    /**
     * 根据用户名加载用户详情
     * 知识点: 这是Spring Security认证的核心方法
     * 返回的UserDetails对象包含了用户的认证和授权信息
     */
    @Override
    @Transactional
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));
        
        return UserPrincipal.create(user);
    }
}

/**
 * 用户主体类,实现Spring Security的UserDetails接口
 * 知识点: UserDetails是Spring Security用户信息的标准接口
 */
@Data
@AllArgsConstructor
public class UserPrincipal implements UserDetails {
    private Long id;
    private String username;
    private String email;
    private String password;
    private Collection<? extends GrantedAuthority> authorities;
    
    /**
     * 从User实体创建UserPrincipal
     */
    public static UserPrincipal create(User user) {
        List<GrantedAuthority> authorities = Collections.singletonList(
            new SimpleGrantedAuthority("ROLE_" + user.getRole().name())
        );
        
        return new UserPrincipal(
            user.getId(),
            user.getUsername(),
            user.getEmail(),
            user.getPassword(),
            authorities
        );
    }
    
    // UserDetails接口实现
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    
    @Override
    public boolean isEnabled() {
        return true;
    }
}

用户业务服务

java 复制代码
// consumer-api/src/main/java/com/ai/interview/service/UserService.java
@Service
@Transactional
public class UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    /**
     * 用户注册
     * 知识点: 业务层方法设计,包含验证、加密、保存等步骤
     */
    public User registerUser(UserRegistrationRequest request) {
        // 1. 验证用户名是否已存在
        if (userRepository.existsByUsername(request.getUsername())) {
            throw new BusinessException("用户名已存在");
        }
        
        // 2. 验证邮箱是否已存在
        if (userRepository.existsByEmail(request.getEmail())) {
            throw new BusinessException("邮箱已被注册");
        }
        
        // 3. 创建新用户
        User user = User.builder()
                .username(request.getUsername())
                .email(request.getEmail())
                .password(passwordEncoder.encode(request.getPassword())) // 密码加密
                .role(UserRole.USER)
                .enabled(true)
                .build();
        
        // 4. 保存用户
        return userRepository.save(user);
    }
    
    /**
     * 用户登录验证
     */
    public User authenticateUser(String username, String password) {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new BusinessException("用户名或密码错误"));
        
        // 验证密码
        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new BusinessException("用户名或密码错误");
        }
        
        if (!user.getEnabled()) {
            throw new BusinessException("账户已被禁用");
        }
        
        return user;
    }
    
    /**
     * 根据ID查找用户
     */
    public User findById(Long id) {
        return userRepository.findById(id)
                .orElseThrow(() -> new BusinessException("用户不存在"));
    }
    
    /**
     * 更新用户信息
     */
    public User updateUser(Long userId, UserUpdateRequest request) {
        User user = findById(userId);
        
        if (StringUtils.hasText(request.getEmail()) && 
            !user.getEmail().equals(request.getEmail())) {
            if (userRepository.existsByEmail(request.getEmail())) {
                throw new BusinessException("邮箱已被注册");
            }
            user.setEmail(request.getEmail());
        }
        
        return userRepository.save(user);
    }
    
    /**
     * 修改密码
     */
    public void changePassword(Long userId, String oldPassword, String newPassword) {
        User user = findById(userId);
        
        // 验证旧密码
        if (!passwordEncoder.matches(oldPassword, user.getPassword())) {
            throw new BusinessException("原密码错误");
        }
        
        // 设置新密码
        user.setPassword(passwordEncoder.encode(newPassword));
        userRepository.save(user);
    }
}
2.3 创建认证控制器

认证请求DTO

java 复制代码
// consumer-api/src/main/java/com/ai/interview/dto/UserLoginRequest.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserLoginRequest {
    
    @NotBlank(message = "用户名不能为空")
    @Size(min = 3, max = 50, message = "用户名长度必须在3-50个字符之间")
    private String username;
    
    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 100, message = "密码长度必须在6-100个字符之间")
    private String password;
}

// consumer-api/src/main/java/com/ai/interview/dto/UserRegistrationRequest.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserRegistrationRequest {
    
    @NotBlank(message = "用户名不能为空")
    @Size(min = 3, max = 50, message = "用户名长度必须在3-50个字符之间")
    private String username;
    
    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;
    
    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 100, message = "密码长度必须在6-100个字符之间")
    private String password;
    
    @NotBlank(message = "确认密码不能为空")
    private String confirmPassword;
    
    /**
     * 自定义验证:确认密码必须与密码一致
     */
    @AssertTrue(message = "确认密码与密码不一致")
    public boolean isPasswordsMatch() {
        return password != null && password.equals(confirmPassword);
    }
}

认证响应DTO

java 复制代码
// consumer-api/src/main/java/com/ai/interview/dto/JwtResponse.java
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class JwtResponse {
    private String token;
    private String type = "Bearer";
    private Long id;
    private String username;
    private String email;
    private String role;
    
    public JwtResponse(String accessToken, UserPrincipal userPrincipal) {
        this.token = accessToken;
        this.id = userPrincipal.getId();
        this.username = userPrincipal.getUsername();
        this.email = userPrincipal.getEmail();
        
        // 提取角色信息
        this.role = userPrincipal.getAuthorities().stream()
                .findFirst()
                .map(GrantedAuthority::getAuthority)
                .orElse("ROLE_USER");
    }
}

认证控制器

java 复制代码
// consumer-api/src/main/java/com/ai/interview/controller/AuthController.java
@RestController
@RequestMapping("/api/auth")
@CrossOrigin(origins = "*", maxAge = 3600)
public class AuthController {
    
    @Autowired
    private AuthenticationManager authenticationManager;
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private JwtUtil jwtUtil;
    
    /**
     * 用户登录接口
     * 知识点: RESTful API设计,POST方法用于创建session
     */
    @PostMapping("/login")
    public ResponseEntity<?> authenticateUser(@Valid @RequestBody UserLoginRequest loginRequest) {
        try {
            // 1. 执行认证
            Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                    loginRequest.getUsername(),
                    loginRequest.getPassword()
                )
            );
            
            // 2. 认证成功,生成JWT token
            UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
            String jwt = jwtUtil.generateJwtToken(userPrincipal);
            
            // 3. 返回认证信息
            JwtResponse response = new JwtResponse(jwt, userPrincipal);
            
            return ResponseEntity.ok(ApiResponse.success(response, "登录成功"));
            
        } catch (BadCredentialsException e) {
            return ResponseEntity.badRequest()
                .body(ApiResponse.error("用户名或密码错误"));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ApiResponse.error("登录失败: " + e.getMessage()));
        }
    }
    
    /**
     * 用户注册接口
     */
    @PostMapping("/register")
    public ResponseEntity<?> registerUser(@Valid @RequestBody UserRegistrationRequest signUpRequest) {
        try {
            // 1. 注册用户
            User user = userService.registerUser(signUpRequest);
            
            // 2. 自动登录,生成token
            UserPrincipal userPrincipal = UserPrincipal.create(user);
            String jwt = jwtUtil.generateJwtToken(userPrincipal);
            
            // 3. 返回注册成功信息
            JwtResponse response = new JwtResponse(jwt, userPrincipal);
            
            return ResponseEntity.ok(ApiResponse.success(response, "注册成功"));
            
        } catch (BusinessException e) {
            return ResponseEntity.badRequest()
                .body(ApiResponse.error(e.getMessage()));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ApiResponse.error("注册失败: " + e.getMessage()));
        }
    }
    
    /**
     * 获取当前用户信息
     */
    @GetMapping("/me")
    @PreAuthorize("hasRole('USER')")
    public ResponseEntity<?> getCurrentUser(Authentication authentication) {
        UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
        return ResponseEntity.ok(ApiResponse.success(userPrincipal, "获取用户信息成功"));
    }
    
    /**
     * 用户登出接口
     * 知识点: JWT是无状态的,登出只需要客户端删除token
     */
    @PostMapping("/logout")
    public ResponseEntity<?> logoutUser() {
        return ResponseEntity.ok(ApiResponse.success(null, "登出成功"));
    }
}
2.4 创建前端登录页面

登录组件

typescript 复制代码
// frontend/src/components/LoginPage.tsx
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { 
  Button, 
  TextField, 
  VerticalLayout, 
  HorizontalLayout,
  Notification
} from '@vaadin/react-components';
import { authService } from '../services/authService';

/**
 * 登录表单数据接口
 * 知识点: TypeScript接口定义,提供类型安全
 */
interface LoginFormData {
  username: string;
  password: string;
}

/**
 * 登录页面组件
 * 知识点: React函数组件 + Hooks
 */
export const LoginPage: React.FC = () => {
  const navigate = useNavigate();
  
  // 表单状态管理
  const [formData, setFormData] = useState<LoginFormData>({
    username: '',
    password: ''
  });
  
  // 加载状态
  const [loading, setLoading] = useState(false);
  
  /**
   * 处理输入框变化
   * 知识点: 受控组件,状态驱动UI
   */
  const handleInputChange = (field: keyof LoginFormData) => (event: any) => {
    setFormData(prev => ({
      ...prev,
      [field]: event.target.value
    }));
  };
  
  /**
   * 处理登录提交
   * 知识点: 异步操作处理,错误处理
   */
  const handleLogin = async () => {
    // 表单验证
    if (!formData.username.trim()) {
      Notification.show('请输入用户名', { theme: 'error' });
      return;
    }
    
    if (!formData.password) {
      Notification.show('请输入密码', { theme: 'error' });
      return;
    }
    
    setLoading(true);
    
    try {
      // 调用认证服务
      const response = await authService.login(formData);
      
      if (response.success) {
        // 保存token到localStorage
        localStorage.setItem('user_token', response.data.token);
        localStorage.setItem('user_info', JSON.stringify(response.data));
        
        Notification.show('登录成功', { theme: 'success' });
        
        // 跳转到主页
        navigate('/dashboard');
      } else {
        Notification.show(response.message || '登录失败', { theme: 'error' });
      }
    } catch (error: any) {
      console.error('登录错误:', error);
      Notification.show(
        error.response?.data?.message || '登录失败,请重试', 
        { theme: 'error' }
      );
    } finally {
      setLoading(false);
    }
  };
  
  /**
   * 处理注册跳转
   */
  const handleRegister = () => {
    navigate('/register');
  };
  
  return (
    <VerticalLayout 
      style={{ 
        padding: '2rem', 
        maxWidth: '400px', 
        margin: '2rem auto',
        boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
        borderRadius: '8px'
      }}
    >
      <h2 style={{ textAlign: 'center', marginBottom: '2rem' }}>
        AI面试系统登录
      </h2>
      
      {/* 用户名输入框 */}
      <TextField
        label="用户名"
        placeholder="请输入用户名"
        value={formData.username}
        onInput={handleInputChange('username')}
        style={{ width: '100%' }}
        required
      />
      
      {/* 密码输入框 */}
      <TextField
        label="密码"
        type="password"
        placeholder="请输入密码"
        value={formData.password}
        onInput={handleInputChange('password')}
        style={{ width: '100%' }}
        required
      />
      
      {/* 按钮组 */}
      <HorizontalLayout style={{ width: '100%', gap: '1rem' }}>
        <Button
          theme="primary"
          onClick={handleLogin}
          disabled={loading}
          style={{ flex: 1 }}
        >
          {loading ? '登录中...' : '登录'}
        </Button>
        
        <Button
          theme="secondary"
          onClick={handleRegister}
          style={{ flex: 1 }}
        >
          注册
        </Button>
      </HorizontalLayout>
    </VerticalLayout>
  );
};

认证服务

typescript 复制代码
// frontend/src/services/authService.ts
import { apiService, ApiResponse } from './api';

/**
 * 登录请求数据接口
 */
export interface LoginRequest {
  username: string;
  password: string;
}

/**
 * 注册请求数据接口
 */
export interface RegisterRequest {
  username: string;
  email: string;
  password: string;
  confirmPassword: string;
}

/**
 * JWT响应数据接口
 */
export interface JwtResponse {
  token: string;
  type: string;
  id: number;
  username: string;
  email: string;
  role: string;
}

/**
 * 用户信息接口
 */
export interface UserInfo {
  id: number;
  username: string;
  email: string;
  role: string;
}

/**
 * 认证服务类
 * 知识点: 服务层模式,封装认证相关API调用
 */
class AuthService {
  
  /**
   * 用户登录
   * 知识点: async/await异步编程
   */
  async login(loginData: LoginRequest): Promise<ApiResponse<JwtResponse>> {
    return await apiService.post<JwtResponse>('/auth/login', loginData);
  }
  
  /**
   * 用户注册
   */
  async register(registerData: RegisterRequest): Promise<ApiResponse<JwtResponse>> {
    return await apiService.post<JwtResponse>('/auth/register', registerData);
  }
  
  /**
   * 获取当前用户信息
   */
  async getCurrentUser(): Promise<ApiResponse<UserInfo>> {
    return await apiService.get<UserInfo>('/auth/me');
  }
  
  /**
   * 用户登出
   */
  async logout(): Promise<ApiResponse<null>> {
    const response = await apiService.post<null>('/auth/logout');
    
    // 清除本地存储的认证信息
    localStorage.removeItem('user_token');
    localStorage.removeItem('user_info');
    
    return response;
  }
  
  /**
   * 检查用户是否已登录
   * 知识点: 客户端状态检查
   */
  isLoggedIn(): boolean {
    const token = localStorage.getItem('user_token');
    return token != null && token.length > 0;
  }
  
  /**
   * 获取当前用户信息(从本地存储)
   */
  getCurrentUserFromStorage(): UserInfo | null {
    const userInfoStr = localStorage.getItem('user_info');
    if (userInfoStr) {
      try {
        return JSON.parse(userInfoStr);
      } catch (error) {
        console.error('解析用户信息失败:', error);
        return null;
      }
    }
    return null;
  }
  
  /**
   * 获取当前用户token
   */
  getToken(): string | null {
    return localStorage.getItem('user_token');
  }
}

// 导出单例实例
export const authService = new AuthService();

知识点总结:

  1. Spring Security架构:

    • SecurityFilterChain: 定义安全过滤器链
    • AuthenticationManager: 处理认证请求
    • UserDetailsService: 加载用户详情
    • PasswordEncoder: 密码加密
  2. JWT认证流程:

    • 用户提交用户名密码
    • 后端验证成功后生成JWT token
    • 前端存储token,后续请求携带token
    • 后端验证token有效性
  3. 前端状态管理:

    • useState Hook管理组件状态
    • localStorage存储认证信息
    • 受控组件模式处理表单
  4. 错误处理机制:

    • 后端统一异常处理
    • 前端try-catch捕获错误
    • 用户友好的错误提示
🎯 阶段1-2验收标准

完成以上代码后,您应该能够:

  1. ✅ 启动Spring Boot后端服务(端口8080)
  2. ✅ 启动React前端服务(端口3000)
  3. ✅ 访问登录页面,进行用户注册
  4. ✅ 使用注册的账号进行登录
  5. ✅ 登录成功后跳转到主页面
  6. ✅ 查看浏览器Network面板,确认API调用正常
  7. ✅ 查看数据库,确认用户数据正确保存

测试步骤:

bash 复制代码
# 1. 启动后端
cd consumer-api
mvn spring-boot:run

# 2. 启动前端
cd frontend
npm run dev

# 3. 访问 http://localhost:3000 进行测试

阶段3-4: 后端API开发 + 面试功能实现

阶段3: 面试题库管理系统
3.1 创建题库相关实体类

题目实体类

java 复制代码
// consumer-api/src/main/java/com/ai/interview/entity/Question.java
@Entity
@Table(name = "questions")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Question {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    /**
     * 题目标题
     */
    @Column(nullable = false, length = 500)
    private String title;
    
    /**
     * 题目内容描述
     * 知识点: @Lob注解用于存储大文本内容
     */
    @Lob
    @Column(nullable = false)
    private String content;
    
    /**
     * 题目类型
     */
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private QuestionType type;
    
    /**
     * 难度级别
     */
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private DifficultyLevel difficulty;
    
    /**
     * 技术标签
     * 知识点: @ElementCollection用于存储集合类型
     * 自动创建question_tags表存储标签信息
     */
    @ElementCollection
    @CollectionTable(name = "question_tags", 
                    joinColumns = @JoinColumn(name = "question_id"))
    @Column(name = "tag")
    private Set<String> tags = new HashSet<>();
    
    /**
     * 预期答案
     */
    @Lob
    private String expectedAnswer;
    
    /**
     * 代码模板(编程题使用)
     */
    @Lob
    private String codeTemplate;
    
    /**
     * 测试用例(编程题使用)
     */
    @Lob
    private String testCases;
    
    /**
     * 题目状态
     */
    @Enumerated(EnumType.STRING)
    private QuestionStatus status = QuestionStatus.ACTIVE;
    
    /**
     * 创建者
     */
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "created_by")
    private User createdBy;
    
    /**
     * 使用次数统计
     */
    private Integer usageCount = 0;
    
    /**
     * 平均评分
     */
    private Double averageRating = 0.0;
    
    @CreationTimestamp
    @Column(name = "created_at")
    private LocalDateTime createdAt;
    
    @UpdateTimestamp
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
}

/**
 * 题目类型枚举
 */
public enum QuestionType {
    TECHNICAL("技术问题"),
    BEHAVIORAL("行为问题"),
    CODING("编程题"),
    SYSTEM_DESIGN("系统设计"),
    ALGORITHM("算法题");
    
    private final String description;
    
    QuestionType(String description) {
        this.description = description;
    }
    
    public String getDescription() {
        return description;
    }
}

/**
 * 难度级别枚举
 */
public enum DifficultyLevel {
    EASY("简单"),
    MEDIUM("中等"),
    HARD("困难"),
    EXPERT("专家级");
    
    private final String description;
    
    DifficultyLevel(String description) {
        this.description = description;
    }
    
    public String getDescription() {
        return description;
    }
}

/**
 * 题目状态枚举
 */
public enum QuestionStatus {
    ACTIVE("启用"),
    INACTIVE("禁用"),
    DRAFT("草稿");
    
    private final String description;
    
    QuestionStatus(String description) {
        this.description = description;
    }
    
    public String getDescription() {
        return description;
    }
}

面试回答记录实体

java 复制代码
// consumer-api/src/main/java/com/ai/interview/entity/InterviewAnswer.java
@Entity
@Table(name = "interview_answers")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class InterviewAnswer {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    /**
     * 关联的面试会话
     * 知识点: 多对一关系,一个面试会话包含多个回答
     */
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "session_id", nullable = false)
    private InterviewSession session;
    
    /**
     * 关联的题目
     */
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "question_id", nullable = false)
    private Question question;
    
    /**
     * 用户的文字回答
     */
    @Lob
    private String textAnswer;
    
    /**
     * 用户的代码回答(编程题)
     */
    @Lob
    private String codeAnswer;
    
    /**
     * 语音回答文件路径
     */
    private String audioFilePath;
    
    /**
     * 语音识别转换的文字
     */
    @Lob
    private String speechToText;
    
    /**
     * 回答开始时间
     */
    @Column(name = "start_time")
    private LocalDateTime startTime;
    
    /**
     * 回答结束时间
     */
    @Column(name = "end_time")
    private LocalDateTime endTime;
    
    /**
     * 回答用时(秒)
     */
    private Integer duration;
    
    /**
     * AI评分
     */
    private Integer aiScore;
    
    /**
     * AI评价反馈
     */
    @Lob
    private String aiFeedback;
    
    /**
     * 代码执行结果
     */
    @Lob
    private String codeExecutionResult;
    
    /**
     * 是否通过代码测试
     */
    private Boolean codeTestPassed;
    
    @CreationTimestamp
    @Column(name = "created_at")
    private LocalDateTime createdAt;
}
3.2 创建题库数据访问层

题目Repository

java 复制代码
// consumer-api/src/main/java/com/ai/interview/repository/QuestionRepository.java
@Repository
public interface QuestionRepository extends JpaRepository<Question, Long> {
    
    /**
     * 根据题目类型查找
     */
    List<Question> findByType(QuestionType type);
    
    /**
     * 根据难度级别查找
     */
    List<Question> findByDifficulty(DifficultyLevel difficulty);
    
    /**
     * 根据状态查找
     */
    List<Question> findByStatus(QuestionStatus status);
    
    /**
     * 根据标签查找题目
     * 知识点: 集合类型的查询,使用MEMBER OF操作符
     */
    @Query("SELECT q FROM Question q WHERE :tag MEMBER OF q.tags")
    List<Question> findByTag(String tag);
    
    /**
     * 根据多个条件查找题目
     * 知识点: 复杂条件查询,使用可选参数
     */
    @Query("SELECT q FROM Question q WHERE " +
           "(:type IS NULL OR q.type = :type) AND " +
           "(:difficulty IS NULL OR q.difficulty = :difficulty) AND " +
           "q.status = :status " +
           "ORDER BY q.createdAt DESC")
    List<Question> findByConditions(
            @Param("type") QuestionType type,
            @Param("difficulty") DifficultyLevel difficulty,
            @Param("status") QuestionStatus status);
    
    /**
     * 随机获取指定数量的题目
     * 知识点: 使用原生SQL进行随机查询
     */
    @Query(value = "SELECT * FROM questions WHERE type = ?1 AND status = 'ACTIVE' " +
                   "ORDER BY RANDOM() LIMIT ?2", nativeQuery = true)
    List<Question> findRandomQuestions(String type, int limit);
    
    /**
     * 根据标题搜索题目
     */
    @Query("SELECT q FROM Question q WHERE q.title LIKE %:keyword% OR q.content LIKE %:keyword%")
    List<Question> searchByKeyword(@Param("keyword") String keyword);
    
    /**
     * 获取热门题目(根据使用次数排序)
     */
    List<Question> findTop10ByStatusOrderByUsageCountDesc(QuestionStatus status);
    
    /**
     * 统计各类型题目数量
     * 知识点: 分组统计查询
     */
    @Query("SELECT q.type, COUNT(q) FROM Question q WHERE q.status = 'ACTIVE' GROUP BY q.type")
    List<Object[]> countByType();
}

面试回答Repository

java 复制代码
// consumer-api/src/main/java/com/ai/interview/repository/InterviewAnswerRepository.java
@Repository
public interface InterviewAnswerRepository extends JpaRepository<InterviewAnswer, Long> {
    
    /**
     * 根据面试会话查找所有回答
     */
    List<InterviewAnswer> findBySessionId(Long sessionId);
    
    /**
     * 根据用户查找回答历史
     */
    @Query("SELECT a FROM InterviewAnswer a JOIN a.session s WHERE s.user.id = :userId")
    List<InterviewAnswer> findByUserId(@Param("userId") Long userId);
    
    /**
     * 根据题目查找所有回答
     */
    List<InterviewAnswer> findByQuestionId(Long questionId);
    
    /**
     * 获取用户对特定题目的最新回答
     */
    @Query("SELECT a FROM InterviewAnswer a JOIN a.session s " +
           "WHERE s.user.id = :userId AND a.question.id = :questionId " +
           "ORDER BY a.createdAt DESC")
    List<InterviewAnswer> findLatestAnswerByUserAndQuestion(
            @Param("userId") Long userId, 
            @Param("questionId") Long questionId,
            Pageable pageable);
    
    /**
     * 统计用户回答的题目数量
     */
    @Query("SELECT COUNT(DISTINCT a.question.id) FROM InterviewAnswer a JOIN a.session s " +
           "WHERE s.user.id = :userId")
    Long countDistinctQuestionsByUserId(@Param("userId") Long userId);
    
    /**
     * 获取用户平均分数
     */
    @Query("SELECT AVG(a.aiScore) FROM InterviewAnswer a JOIN a.session s " +
           "WHERE s.user.id = :userId AND a.aiScore IS NOT NULL")
    Double getAverageScoreByUserId(@Param("userId") Long userId);
}
3.3 实现题库业务服务

题目服务类

java 复制代码
// consumer-api/src/main/java/com/ai/interview/service/QuestionService.java
@Service
@Transactional
public class QuestionService {
    
    @Autowired
    private QuestionRepository questionRepository;
    
    @Autowired
    private UserService userService;
    
    /**
     * 创建新题目
     * 知识点: 业务逻辑封装,包含权限验证和数据验证
     */
    public Question createQuestion(CreateQuestionRequest request, Long creatorId) {
        // 验证创建者权限
        User creator = userService.findById(creatorId);
        if (!creator.getRole().equals(UserRole.ADMIN)) {
            throw new BusinessException("只有管理员可以创建题目");
        }
        
        // 构建题目对象
        Question question = Question.builder()
                .title(request.getTitle())
                .content(request.getContent())
                .type(request.getType())
                .difficulty(request.getDifficulty())
                .tags(new HashSet<>(request.getTags()))
                .expectedAnswer(request.getExpectedAnswer())
                .codeTemplate(request.getCodeTemplate())
                .testCases(request.getTestCases())
                .status(QuestionStatus.ACTIVE)
                .createdBy(creator)
                .usageCount(0)
                .averageRating(0.0)
                .build();
        
        return questionRepository.save(question);
    }
    
    /**
     * 更新题目
     */
    public Question updateQuestion(Long questionId, UpdateQuestionRequest request, Long updaterId) {
        Question question = findById(questionId);
        
        // 权限验证
        User updater = userService.findById(updaterId);
        if (!updater.getRole().equals(UserRole.ADMIN) && 
            !question.getCreatedBy().getId().equals(updaterId)) {
            throw new BusinessException("没有权限修改此题目");
        }
        
        // 更新字段
        if (StringUtils.hasText(request.getTitle())) {
            question.setTitle(request.getTitle());
        }
        if (StringUtils.hasText(request.getContent())) {
            question.setContent(request.getContent());
        }
        if (request.getType() != null) {
            question.setType(request.getType());
        }
        if (request.getDifficulty() != null) {
            question.setDifficulty(request.getDifficulty());
        }
        if (request.getTags() != null && !request.getTags().isEmpty()) {
            question.setTags(new HashSet<>(request.getTags()));
        }
        if (StringUtils.hasText(request.getExpectedAnswer())) {
            question.setExpectedAnswer(request.getExpectedAnswer());
        }
        if (StringUtils.hasText(request.getCodeTemplate())) {
            question.setCodeTemplate(request.getCodeTemplate());
        }
        if (StringUtils.hasText(request.getTestCases())) {
            question.setTestCases(request.getTestCases());
        }
        if (request.getStatus() != null) {
            question.setStatus(request.getStatus());
        }
        
        return questionRepository.save(question);
    }
    
    /**
     * 根据ID查找题目
     */
    public Question findById(Long id) {
        return questionRepository.findById(id)
                .orElseThrow(() -> new BusinessException("题目不存在"));
    }
    
    /**
     * 分页查询题目
     * 知识点: Spring Data分页功能
     */
    public Page<Question> findQuestions(QuestionSearchRequest request, Pageable pageable) {
        // 这里可以使用Specification进行动态查询
        // 为简化演示,使用基础的Repository方法
        if (request.getType() != null) {
            return questionRepository.findAll(
                (root, query, criteriaBuilder) -> 
                    criteriaBuilder.equal(root.get("type"), request.getType()),
                pageable
            );
        }
        
        return questionRepository.findAll(pageable);
    }
    
    /**
     * 根据条件查询题目
     */
    public List<Question> findByConditions(QuestionType type, DifficultyLevel difficulty) {
        return questionRepository.findByConditions(type, difficulty, QuestionStatus.ACTIVE);
    }
    
    /**
     * 随机获取面试题目
     * 知识点: 算法设计,面试题目选择策略
     */
    public List<Question> getRandomQuestionsForInterview(InterviewType interviewType, int count) {
        // 根据面试类型映射题目类型
        List<QuestionType> questionTypes = mapInterviewTypeToQuestionTypes(interviewType);
        
        List<Question> selectedQuestions = new ArrayList<>();
        
        for (QuestionType type : questionTypes) {
            int questionsPerType = count / questionTypes.size();
            List<Question> randomQuestions = questionRepository.findRandomQuestions(
                type.name(), questionsPerType
            );
            selectedQuestions.addAll(randomQuestions);
        }
        
        // 如果数量不够,随机补充
        if (selectedQuestions.size() < count) {
            int needed = count - selectedQuestions.size();
            List<Question> additionalQuestions = questionRepository.findRandomQuestions(
                QuestionType.TECHNICAL.name(), needed
            );
            selectedQuestions.addAll(additionalQuestions);
        }
        
        return selectedQuestions.subList(0, Math.min(count, selectedQuestions.size()));
    }
    
    /**
     * 面试类型到题目类型的映射
     */
    private List<QuestionType> mapInterviewTypeToQuestionTypes(InterviewType interviewType) {
        switch (interviewType) {
            case TECHNICAL:
                return Arrays.asList(QuestionType.TECHNICAL, QuestionType.ALGORITHM);
            case CODING:
                return Arrays.asList(QuestionType.CODING, QuestionType.ALGORITHM);
            case BEHAVIORAL:
                return Arrays.asList(QuestionType.BEHAVIORAL);
            default:
                return Arrays.asList(QuestionType.TECHNICAL);
        }
    }
    
    /**
     * 增加题目使用次数
     */
    public void incrementUsageCount(Long questionId) {
        Question question = findById(questionId);
        question.setUsageCount(question.getUsageCount() + 1);
        questionRepository.save(question);
    }
    
    /**
     * 获取热门题目
     */
    public List<Question> getPopularQuestions() {
        return questionRepository.findTop10ByStatusOrderByUsageCountDesc(QuestionStatus.ACTIVE);
    }
    
    /**
     * 根据关键词搜索题目
     */
    public List<Question> searchQuestions(String keyword) {
        return questionRepository.searchByKeyword(keyword);
    }
    
    /**
     * 删除题目(软删除)
     */
    public void deleteQuestion(Long questionId, Long deleterId) {
        Question question = findById(questionId);
        
        // 权限验证
        User deleter = userService.findById(deleterId);
        if (!deleter.getRole().equals(UserRole.ADMIN) && 
            !question.getCreatedBy().getId().equals(deleterId)) {
            throw new BusinessException("没有权限删除此题目");
        }
        
        question.setStatus(QuestionStatus.INACTIVE);
        questionRepository.save(question);
    }
    
    /**
     * 获取题目统计信息
     */
    public QuestionStatistics getQuestionStatistics() {
        List<Object[]> typeStats = questionRepository.countByType();
        
        QuestionStatistics statistics = new QuestionStatistics();
        for (Object[] stat : typeStats) {
            QuestionType type = (QuestionType) stat[0];
            Long count = (Long) stat[1];
            statistics.addTypeCount(type, count);
        }
        
        return statistics;
    }
}
3.4 创建面试会话服务

面试会话服务

java 复制代码
// consumer-api/src/main/java/com/ai/interview/service/InterviewSessionService.java
@Service
@Transactional
public class InterviewSessionService {
    
    @Autowired
    private InterviewSessionRepository sessionRepository;
    
    @Autowired
    private QuestionService questionService;
    
    @Autowired
    private UserService userService;
    
    /**
     * 创建面试会话
     * 知识点: 业务流程设计,面试会话生命周期管理
     */
    public InterviewSession createSession(CreateInterviewSessionRequest request, Long userId) {
        User user = userService.findById(userId);
        
        // 检查是否有正在进行的面试
        List<InterviewSession> ongoingSessions = sessionRepository.findByUserIdAndStatus(
            userId, InterviewStatus.IN_PROGRESS
        );
        
        if (!ongoingSessions.isEmpty()) {
            throw new BusinessException("您有正在进行的面试,请先完成或取消当前面试");
        }
        
        // 创建面试会话
        InterviewSession session = InterviewSession.builder()
                .user(user)
                .type(request.getType())
                .status(InterviewStatus.PENDING)
                .build();
        
        return sessionRepository.save(session);
    }
    
    /**
     * 开始面试
     */
    public InterviewSession startInterview(Long sessionId, Long userId) {
        InterviewSession session = findById(sessionId);
        
        // 验证权限
        if (!session.getUser().getId().equals(userId)) {
            throw new BusinessException("无权限操作此面试会话");
        }
        
        // 验证状态
        if (!session.getStatus().equals(InterviewStatus.PENDING)) {
            throw new BusinessException("面试会话状态不正确");
        }
        
        // 更新状态
        session.setStatus(InterviewStatus.IN_PROGRESS);
        session.setStartTime(LocalDateTime.now());
        
        return sessionRepository.save(session);
    }
    
    /**
     * 完成面试
     */
    public InterviewSession completeInterview(Long sessionId, Long userId) {
        InterviewSession session = findById(sessionId);
        
        // 验证权限
        if (!session.getUser().getId().equals(userId)) {
            throw new BusinessException("无权限操作此面试会话");
        }
        
        // 验证状态
        if (!session.getStatus().equals(InterviewStatus.IN_PROGRESS)) {
            throw new BusinessException("面试会话状态不正确");
        }
        
        // 更新状态
        session.setStatus(InterviewStatus.COMPLETED);
        session.setEndTime(LocalDateTime.now());
        
        // 计算总体评分(基于所有回答的平均分)
        Integer totalScore = calculateSessionScore(sessionId);
        session.setScore(totalScore);
        
        return sessionRepository.save(session);
    }
    
    /**
     * 取消面试
     */
    public InterviewSession cancelInterview(Long sessionId, Long userId) {
        InterviewSession session = findById(sessionId);
        
        // 验证权限
        if (!session.getUser().getId().equals(userId)) {
            throw new BusinessException("无权限操作此面试会话");
        }
        
        // 只有未开始或进行中的面试可以取消
        if (session.getStatus().equals(InterviewStatus.COMPLETED)) {
            throw new BusinessException("已完成的面试无法取消");
        }
        
        session.setStatus(InterviewStatus.CANCELLED);
        session.setEndTime(LocalDateTime.now());
        
        return sessionRepository.save(session);
    }
    
    /**
     * 根据ID查找面试会话
     */
    public InterviewSession findById(Long id) {
        return sessionRepository.findById(id)
                .orElseThrow(() -> new BusinessException("面试会话不存在"));
    }
    
    /**
     * 获取用户的面试历史
     */
    public List<InterviewSession> getUserInterviewHistory(Long userId) {
        return sessionRepository.findByUserIdOrderByCreatedAtDesc(userId);
    }
    
    /**
     * 获取面试会话详情(包含所有回答)
     */
    public InterviewSessionDetail getSessionDetail(Long sessionId, Long userId) {
        InterviewSession session = findById(sessionId);
        
        // 验证权限
        if (!session.getUser().getId().equals(userId)) {
            throw new BusinessException("无权限查看此面试会话");
        }
        
        // 构建详情对象
        InterviewSessionDetail detail = new InterviewSessionDetail();
        detail.setSession(session);
        
        // 获取所有回答
        List<InterviewAnswer> answers = interviewAnswerRepository.findBySessionId(sessionId);
        detail.setAnswers(answers);
        
        return detail;
    }
    
    /**
     * 计算面试会话总分
     * 知识点: 算法设计,评分逻辑
     */
    private Integer calculateSessionScore(Long sessionId) {
        List<InterviewAnswer> answers = interviewAnswerRepository.findBySessionId(sessionId);
        
        if (answers.isEmpty()) {
            return 0;
        }
        
        // 计算有效评分的平均值
        List<Integer> validScores = answers.stream()
                .filter(answer -> answer.getAiScore() != null)
                .map(InterviewAnswer::getAiScore)
                .collect(Collectors.toList());
        
        if (validScores.isEmpty()) {
            return 0;
        }
        
        double average = validScores.stream()
                .mapToInt(Integer::intValue)
                .average()
                .orElse(0.0);
        
        return (int) Math.round(average);
    }
    
    /**
     * 获取面试会话统计信息
     */
    public InterviewSessionStatistics getSessionStatistics(Long userId) {
        List<InterviewSession> sessions = getUserInterviewHistory(userId);
        
        InterviewSessionStatistics stats = new InterviewSessionStatistics();
        stats.setTotalSessions(sessions.size());
        
        long completedSessions = sessions.stream()
                .filter(s -> s.getStatus().equals(InterviewStatus.COMPLETED))
                .count();
        stats.setCompletedSessions((int) completedSessions);
        
        // 计算平均分
        double averageScore = sessions.stream()
                .filter(s -> s.getScore() != null)
                .mapToInt(InterviewSession::getScore)
                .average()
                .orElse(0.0);
        stats.setAverageScore(averageScore);
        
        return stats;
    }
}
3.5 创建题库管理控制器

题目管理控制器

java 复制代码
// consumer-api/src/main/java/com/ai/interview/controller/QuestionController.java
@RestController
@RequestMapping("/api/questions")
@CrossOrigin(origins = "*", maxAge = 3600)
public class QuestionController {
    
    @Autowired
    private QuestionService questionService;
    
    /**
     * 创建题目
     * 知识点: RESTful API设计,POST方法用于创建资源
     */
    @PostMapping
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<?> createQuestion(
            @Valid @RequestBody CreateQuestionRequest request,
            Authentication authentication) {
        try {
            UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
            Question question = questionService.createQuestion(request, userPrincipal.getId());
            
            return ResponseEntity.ok(ApiResponse.success(question, "题目创建成功"));
        } catch (BusinessException e) {
            return ResponseEntity.badRequest()
                .body(ApiResponse.error(e.getMessage()));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ApiResponse.error("创建题目失败: " + e.getMessage()));
        }
    }
    
    /**
     * 获取题目列表
     */
    @GetMapping
    public ResponseEntity<?> getQuestions(
            @RequestParam(required = false) QuestionType type,
            @RequestParam(required = false) DifficultyLevel difficulty,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(defaultValue = "createdAt") String sortBy,
            @RequestParam(defaultValue = "desc") String sortDir) {
        try {
            // 创建分页参数
            Sort.Direction direction = sortDir.equalsIgnoreCase("desc") ? 
                Sort.Direction.DESC : Sort.Direction.ASC;
            Pageable pageable = PageRequest.of(page, size, Sort.by(direction, sortBy));
            
            // 创建查询条件
            QuestionSearchRequest searchRequest = new QuestionSearchRequest();
            searchRequest.setType(type);
            searchRequest.setDifficulty(difficulty);
            
            Page<Question> questions = questionService.findQuestions(searchRequest, pageable);
            
            return ResponseEntity.ok(ApiResponse.success(questions, "获取题目列表成功"));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ApiResponse.error("获取题目列表失败: " + e.getMessage()));
        }
    }
    
    /**
     * 根据ID获取题目详情
     */
    @GetMapping("/{id}")
    public ResponseEntity<?> getQuestionById(@PathVariable Long id) {
        try {
            Question question = questionService.findById(id);
            return ResponseEntity.ok(ApiResponse.success(question, "获取题目详情成功"));
        } catch (BusinessException e) {
            return ResponseEntity.badRequest()
                .body(ApiResponse.error(e.getMessage()));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ApiResponse.error("获取题目详情失败: " + e.getMessage()));
        }
    }
    
    /**
     * 更新题目
     */
    @PutMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<?> updateQuestion(
            @PathVariable Long id,
            @Valid @RequestBody UpdateQuestionRequest request,
            Authentication authentication) {
        try {
            UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
            Question question = questionService.updateQuestion(id, request, userPrincipal.getId());
            
            return ResponseEntity.ok(ApiResponse.success(question, "题目更新成功"));
        } catch (BusinessException e) {
            return ResponseEntity.badRequest()
                .body(ApiResponse.error(e.getMessage()));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ApiResponse.error("更新题目失败: " + e.getMessage()));
        }
    }
    
    /**
     * 删除题目
     */
    @DeleteMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<?> deleteQuestion(
            @PathVariable Long id,
            Authentication authentication) {
        try {
            UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
            questionService.deleteQuestion(id, userPrincipal.getId());
            
            return ResponseEntity.ok(ApiResponse.success(null, "题目删除成功"));
        } catch (BusinessException e) {
            return ResponseEntity.badRequest()
                .body(ApiResponse.error(e.getMessage()));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ApiResponse.error("删除题目失败: " + e.getMessage()));
        }
    }
    
    /**
     * 搜索题目
     */
    @GetMapping("/search")
    public ResponseEntity<?> searchQuestions(@RequestParam String keyword) {
        try {
            List<Question> questions = questionService.searchQuestions(keyword);
            return ResponseEntity.ok(ApiResponse.success(questions, "搜索成功"));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ApiResponse.error("搜索失败: " + e.getMessage()));
        }
    }
    
    /**
     * 获取热门题目
     */
    @GetMapping("/popular")
    public ResponseEntity<?> getPopularQuestions() {
        try {
            List<Question> questions = questionService.getPopularQuestions();
            return ResponseEntity.ok(ApiResponse.success(questions, "获取热门题目成功"));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ApiResponse.error("获取热门题目失败: " + e.getMessage()));
        }
    }
    
    /**
     * 获取随机面试题目
     */
    @GetMapping("/random")
    public ResponseEntity<?> getRandomQuestions(
            @RequestParam InterviewType interviewType,
            @RequestParam(defaultValue = "5") int count) {
        try {
            List<Question> questions = questionService.getRandomQuestionsForInterview(
                interviewType, count
            );
            return ResponseEntity.ok(ApiResponse.success(questions, "获取随机题目成功"));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ApiResponse.error("获取随机题目失败: " + e.getMessage()));
        }
    }
    
    /**
     * 获取题目统计信息
     */
    @GetMapping("/statistics")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<?> getQuestionStatistics() {
        try {
            QuestionStatistics statistics = questionService.getQuestionStatistics();
            return ResponseEntity.ok(ApiResponse.success(statistics, "获取统计信息成功"));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ApiResponse.error("获取统计信息失败: " + e.getMessage()));
        }
    }
}
🎯 阶段3验收标准

完成以上代码后,您应该能够:

  1. ✅ 管理员可以创建、编辑、删除题目
  2. ✅ 题目支持多种类型:技术、行为、编程、算法等
  3. ✅ 题目具有难度级别和标签分类
  4. ✅ 支持题目搜索和分页查询
  5. ✅ 可以随机获取面试题目
  6. ✅ 数据库正确存储题目和关联数据

测试API:

bash 复制代码
# 创建题目 (需要管理员权限)
curl -X POST http://localhost:8080/api/questions \
  -H "Content-Type: application/json" \
  -H "user_token: YOUR_ADMIN_TOKEN" \
  -d '{
    "title": "什么是Spring Boot?",
    "content": "请详细介绍Spring Boot的核心特性和优势",
    "type": "TECHNICAL",
    "difficulty": "MEDIUM",
    "tags": ["Spring", "Java", "后端"],
    "expectedAnswer": "Spring Boot是..."
  }'

# 获取题目列表
curl -X GET "http://localhost:8080/api/questions?page=0&size=10"

# 获取随机面试题目
curl -X GET "http://localhost:8080/api/questions/random?interviewType=TECHNICAL&count=5"

阶段4: 面试功能实现

阶段4: 面试功能实现
4.1 创建面试会话控制器

面试会话控制器

java 复制代码
// consumer-api/src/main/java/com/ai/interview/controller/InterviewController.java
@RestController
@RequestMapping("/api/interviews")
@CrossOrigin(origins = "*", maxAge = 3600)
public class InterviewController {
    
    @Autowired
    private InterviewSessionService sessionService;
    
    @Autowired
    private InterviewAnswerService answerService;
    
    /**
     * 创建面试会话
     */
    @PostMapping("/sessions")
    @PreAuthorize("hasRole('USER')")
    public ResponseEntity<?> createInterviewSession(
            @Valid @RequestBody CreateInterviewSessionRequest request,
            Authentication authentication) {
        try {
            UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
            InterviewSession session = sessionService.createSession(request, userPrincipal.getId());
            
            return ResponseEntity.ok(ApiResponse.success(session, "面试会话创建成功"));
        } catch (BusinessException e) {
            return ResponseEntity.badRequest()
                .body(ApiResponse.error(e.getMessage()));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ApiResponse.error("创建面试会话失败: " + e.getMessage()));
        }
    }
    
    /**
     * 开始面试
     */
    @PostMapping("/sessions/{sessionId}/start")
    @PreAuthorize("hasRole('USER')")
    public ResponseEntity<?> startInterview(
            @PathVariable Long sessionId,
            Authentication authentication) {
        try {
            UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
            InterviewSession session = sessionService.startInterview(sessionId, userPrincipal.getId());
            
            return ResponseEntity.ok(ApiResponse.success(session, "面试开始"));
        } catch (BusinessException e) {
            return ResponseEntity.badRequest()
                .body(ApiResponse.error(e.getMessage()));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ApiResponse.error("开始面试失败: " + e.getMessage()));
        }
    }
    
    /**
     * 获取面试题目
     */
    @GetMapping("/sessions/{sessionId}/questions")
    @PreAuthorize("hasRole('USER')")
    public ResponseEntity<?> getInterviewQuestions(
            @PathVariable Long sessionId,
            Authentication authentication) {
        try {
            UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
            List<Question> questions = sessionService.getInterviewQuestions(sessionId, userPrincipal.getId());
            
            return ResponseEntity.ok(ApiResponse.success(questions, "获取面试题目成功"));
        } catch (BusinessException e) {
            return ResponseEntity.badRequest()
                .body(ApiResponse.error(e.getMessage()));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ApiResponse.error("获取面试题目失败: " + e.getMessage()));
        }
    }
    
    /**
     * 提交回答
     */
    @PostMapping("/answers")
    @PreAuthorize("hasRole('USER')")
    public ResponseEntity<?> submitAnswer(
            @Valid @RequestBody SubmitAnswerRequest request,
            Authentication authentication) {
        try {
            UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
            InterviewAnswer answer = answerService.submitAnswer(request, userPrincipal.getId());
            
            return ResponseEntity.ok(ApiResponse.success(answer, "回答提交成功"));
        } catch (BusinessException e) {
            return ResponseEntity.badRequest()
                .body(ApiResponse.error(e.getMessage()));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ApiResponse.error("提交回答失败: " + e.getMessage()));
        }
    }
    
    /**
     * 完成面试
     */
    @PostMapping("/sessions/{sessionId}/complete")
    @PreAuthorize("hasRole('USER')")
    public ResponseEntity<?> completeInterview(
            @PathVariable Long sessionId,
            Authentication authentication) {
        try {
            UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
            InterviewSession session = sessionService.completeInterview(sessionId, userPrincipal.getId());
            
            return ResponseEntity.ok(ApiResponse.success(session, "面试完成"));
        } catch (BusinessException e) {
            return ResponseEntity.badRequest()
                .body(ApiResponse.error(e.getMessage()));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ApiResponse.error("完成面试失败: " + e.getMessage()));
        }
    }
    
    /**
     * 获取面试历史
     */
    @GetMapping("/history")
    @PreAuthorize("hasRole('USER')")
    public ResponseEntity<?> getInterviewHistory(Authentication authentication) {
        try {
            UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
            List<InterviewSession> history = sessionService.getUserInterviewHistory(userPrincipal.getId());
            
            return ResponseEntity.ok(ApiResponse.success(history, "获取面试历史成功"));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ApiResponse.error("获取面试历史失败: " + e.getMessage()));
        }
    }
    
    /**
     * 获取面试详情
     */
    @GetMapping("/sessions/{sessionId}")
    @PreAuthorize("hasRole('USER')")
    public ResponseEntity<?> getInterviewDetail(
            @PathVariable Long sessionId,
            Authentication authentication) {
        try {
            UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
            InterviewSessionDetail detail = sessionService.getSessionDetail(sessionId, userPrincipal.getId());
            
            return ResponseEntity.ok(ApiResponse.success(detail, "获取面试详情成功"));
        } catch (BusinessException e) {
            return ResponseEntity.badRequest()
                .body(ApiResponse.error(e.getMessage()));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ApiResponse.error("获取面试详情失败: " + e.getMessage()));
        }
    }
}
4.2 创建前端面试页面

面试页面主组件

typescript 复制代码
// frontend/src/components/InterviewPage.tsx
import React, { useState, useEffect, useRef } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
  Button,
  VerticalLayout,
  HorizontalLayout,
  TextArea,
  Notification,
  ProgressBar,
  Dialog
} from '@vaadin/react-components';
import { VideoChat } from './VideoChat';
import { CodeEditor } from './CodeEditor';
import { interviewService } from '../services/interviewService';

/**
 * 面试页面组件
 * 知识点: 复杂状态管理,组件间通信
 */
export const InterviewPage: React.FC = () => {
  const { sessionId } = useParams<{ sessionId: string }>();
  const navigate = useNavigate();
  
  // 面试状态
  const [session, setSession] = useState<any>(null);
  const [questions, setQuestions] = useState<any[]>([]);
  const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
  const [answers, setAnswers] = useState<{ [key: number]: string }>({});

阶段6: 系统部署与上线

6.1 Docker容器化部署

后端Dockerfile

dockerfile 复制代码
# consumer-api/Dockerfile
FROM openjdk:17-jdk-slim

# 设置工作目录
WORKDIR /app

# 复制Maven包装器和pom.xml
COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .

# 下载依赖(利用Docker缓存)
RUN ./mvnw dependency:go-offline -B

# 复制源代码
COPY src src

# 构建应用
RUN ./mvnw clean package -DskipTests

# 运行应用
EXPOSE 8080
CMD ["java", "-jar", "target/consumer-api-1.0.0.jar"]

前端Dockerfile

dockerfile 复制代码
# frontend/Dockerfile
FROM node:18-alpine AS builder

WORKDIR /app

# 复制package文件
COPY package*.json ./

# 安装依赖
RUN npm ci --only=production

# 复制源代码
COPY . .

# 构建前端
RUN npm run build

# 生产环境镜像
FROM nginx:alpine

# 复制构建结果到nginx
COPY --from=builder /app/dist /usr/share/nginx/html

# 复制nginx配置
COPY nginx.conf /etc/nginx/nginx.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

Nginx配置

nginx 复制代码
# frontend/nginx.conf
events {
    worker_connections 1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    
    # 压缩配置
    gzip on;
    gzip_vary on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
    
    server {
        listen 80;
        server_name localhost;
        
        # 前端静态文件
        location / {
            root /usr/share/nginx/html;
            index index.html index.htm;
            try_files $uri $uri/ /index.html;
        }
        
        # API代理
        location /api/ {
            proxy_pass http://backend:8080/api/;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
        
        # WebSocket支持
        location /ws/ {
            proxy_pass http://backend:8080/ws/;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}

Docker Compose配置

yaml 复制代码
# docker-compose.yml
version: '3.8'

services:
  # PostgreSQL数据库
  database:
    image: pgvector/pgvector:pg16
    container_name: ai-interview-db
    environment:
      POSTGRES_DB: ai_interview
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./database/init.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      - ai-interview-network

  # Redis缓存
  redis:
    image: redis:7-alpine
    container_name: ai-interview-redis
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    networks:
      - ai-interview-network

  # 后端服务
  backend:
    build: ./consumer-api
    container_name: ai-interview-backend
    environment:
      SPRING_PROFILES_ACTIVE: docker
      SPRING_DATASOURCE_URL: jdbc:postgresql://database:5432/ai_interview
      SPRING_DATASOURCE_USERNAME: postgres
      SPRING_DATASOURCE_PASSWORD: password
      SPRING_REDIS_HOST: redis
      ALIBABA_AI_API_KEY: ${ALIBABA_AI_API_KEY}
    ports:
      - "8080:8080"
    depends_on:
      - database
      - redis
    networks:
      - ai-interview-network
    volumes:
      - ./logs:/app/logs
      - ./uploads:/app/uploads

  # 前端服务
  frontend:
    build: ./frontend
    container_name: ai-interview-frontend
    ports:
      - "80:80"
    depends_on:
      - backend
    networks:
      - ai-interview-network

volumes:
  postgres_data:
  redis_data:

networks:
  ai-interview-network:
    driver: bridge

环境配置文件

bash 复制代码
# .env
# 数据库配置
POSTGRES_DB=ai_interview
POSTGRES_USER=postgres
POSTGRES_PASSWORD=your_secure_password

# AI服务配置
ALIBABA_AI_API_KEY=your_alibaba_ai_api_key

# Redis配置
REDIS_PASSWORD=your_redis_password

# 应用配置
SPRING_PROFILES_ACTIVE=production
6.2 生产环境配置

生产环境Spring配置

yaml 复制代码
# consumer-api/src/main/resources/application-production.yml
server:
  port: 8080
  servlet:
    context-path: /api

spring:
  profiles:
    active: production
    
  # 数据源配置
  datasource:
    url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:ai_interview}
    username: ${DB_USERNAME:postgres}
    password: ${DB_PASSWORD:password}
    driver-class-name: org.postgresql.Driver
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
      
  # JPA配置
  jpa:
    hibernate:
      ddl-auto: validate  # 生产环境使用validate
    show-sql: false       # 生产环境关闭SQL日志
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQLDialect
        format_sql: false
        
  # Redis配置
  redis:
    host: ${REDIS_HOST:localhost}
    port: ${REDIS_PORT:6379}
    password: ${REDIS_PASSWORD:}
    timeout: 3000ms
    lettuce:
      pool:
        max-active: 20
        max-idle: 10
        min-idle: 5
        
  # AI配置
  ai:
    alibaba:
      api-key: ${ALIBABA_AI_API_KEY}
      
# 日志配置
logging:
  level:
    root: INFO
    com.ai.interview: INFO
    org.springframework.security: WARN
  file:
    name: logs/ai-interview.log
  pattern:
    file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
    
# 管理端点配置
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      show-details: when-authorized
  metrics:
    export:
      prometheus:
        enabled: true

数据库初始化脚本

sql 复制代码
-- database/init.sql
-- 创建数据库
CREATE DATABASE ai_interview;

-- 切换到ai_interview数据库
\c ai_interview;

-- 启用向量扩展(用于AI嵌入)
CREATE EXTENSION IF NOT EXISTS vector;

-- 创建用户表
CREATE TABLE IF NOT EXISTS users (
    id BIGSERIAL PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    password VARCHAR(255) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    role VARCHAR(20) NOT NULL DEFAULT 'USER',
    enabled BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 创建题目表
CREATE TABLE IF NOT EXISTS questions (
    id BIGSERIAL PRIMARY KEY,
    title VARCHAR(500) NOT NULL,
    content TEXT NOT NULL,
    type VARCHAR(50) NOT NULL,
    difficulty VARCHAR(20) NOT NULL,
    expected_answer TEXT,
    code_template TEXT,
    test_cases TEXT,
    status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
    created_by BIGINT REFERENCES users(id),
    usage_count INTEGER DEFAULT 0,
    average_rating DECIMAL(3,2) DEFAULT 0.0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 创建题目标签表
CREATE TABLE IF NOT EXISTS question_tags (
    question_id BIGINT REFERENCES questions(id),
    tag VARCHAR(50) NOT NULL,
    PRIMARY KEY (question_id, tag)
);

-- 创建面试会话表
CREATE TABLE IF NOT EXISTS interview_sessions (
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL REFERENCES users(id),
    type VARCHAR(50) NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
    start_time TIMESTAMP,
    end_time TIMESTAMP,
    score INTEGER,
    feedback TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 创建面试回答表
CREATE TABLE IF NOT EXISTS interview_answers (
    id BIGSERIAL PRIMARY KEY,
    session_id BIGINT NOT NULL REFERENCES interview_sessions(id),
    question_id BIGINT NOT NULL REFERENCES questions(id),
    text_answer TEXT,
    code_answer TEXT,
    audio_file_path VARCHAR(500),
    speech_to_text TEXT,
    start_time TIMESTAMP,
    end_time TIMESTAMP,
    duration INTEGER,
    ai_score INTEGER,
    ai_feedback TEXT,
    code_execution_result TEXT,
    code_test_passed BOOLEAN,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 插入默认管理员用户
INSERT INTO users (username, password, email, role) VALUES 
('admin', '$2a$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewtnKDj8.7/2k9iS', '[email protected]', 'ADMIN')
ON CONFLICT (username) DO NOTHING;

-- 插入示例题目
INSERT INTO questions (title, content, type, difficulty, expected_answer, created_by) VALUES 
('什么是Spring Boot?', '请详细介绍Spring Boot的核心特性和优势,以及它与传统Spring框架的区别。', 'TECHNICAL', 'MEDIUM', 
'Spring Boot是基于Spring框架的快速应用开发框架...', 1),
('实现单例模式', '请用Java实现一个线程安全的单例模式,并解释为什么这样设计。', 'CODING', 'MEDIUM',
'可以使用双重检查锁定、静态内部类等方式实现...', 1),
('介绍一次你遇到的技术挑战', '请描述一次你在项目中遇到的技术难题,以及你是如何解决的。', 'BEHAVIORAL', 'EASY',
'候选人应该描述具体的技术问题、解决方案和学到的经验...', 1)
ON CONFLICT DO NOTHING;

-- 创建索引优化查询性能
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_questions_type ON questions(type);
CREATE INDEX IF NOT EXISTS idx_questions_difficulty ON questions(difficulty);
CREATE INDEX IF NOT EXISTS idx_questions_status ON questions(status);
CREATE INDEX IF NOT EXISTS idx_interview_sessions_user_id ON interview_sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_interview_sessions_status ON interview_sessions(status);
CREATE INDEX IF NOT EXISTS idx_interview_answers_session_id ON interview_answers(session_id);
CREATE INDEX IF NOT EXISTS idx_interview_answers_question_id ON interview_answers(question_id);
6.3 部署脚本

部署脚本

bash 复制代码
#!/bin/bash
# deploy.sh

set -e

echo "🚀 开始部署AI面试系统..."

# 检查环境变量
if [ -z "$ALIBABA_AI_API_KEY" ]; then
    echo "❌ 错误: 请设置ALIBABA_AI_API_KEY环境变量"
    exit 1
fi

# 创建必要的目录
echo "📁 创建目录结构..."
mkdir -p logs uploads database

# 停止现有服务
echo "🛑 停止现有服务..."
docker-compose down

# 清理旧镜像
echo "🧹 清理旧镜像..."
docker system prune -f

# 构建并启动服务
echo "🔨 构建并启动服务..."
docker-compose up --build -d

# 等待服务启动
echo "⏳ 等待服务启动..."
sleep 30

# 健康检查
echo "🔍 执行健康检查..."
backend_health=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/actuator/health || echo "000")
frontend_health=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:80 || echo "000")

if [ "$backend_health" = "200" ] && [ "$frontend_health" = "200" ]; then
    echo "✅ 部署成功!"
    echo "🌐 前端地址: http://localhost"
    echo "🔧 后端API: http://localhost:8080/api"
    echo "📊 监控地址: http://localhost:8080/api/actuator"
else
    echo "❌ 部署失败!"
    echo "后端状态: $backend_health"
    echo "前端状态: $frontend_health"
    echo "查看日志: docker-compose logs"
    exit 1
fi

echo "🎉 AI面试系统部署完成!"

监控脚本

bash 复制代码
#!/bin/bash
# monitor.sh

echo "📊 AI面试系统监控报告"
echo "========================"

# 检查容器状态
echo "🐳 容器状态:"
docker-compose ps

echo ""

# 检查系统资源
echo "💻 系统资源:"
echo "CPU使用率: $(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1)%"
echo "内存使用率: $(free | grep Mem | awk '{printf("%.2f%%", $3/$2 * 100.0)}')"
echo "磁盘使用率: $(df -h / | awk 'NR==2{print $5}')"

echo ""

# 检查服务健康状态
echo "🏥 服务健康状态:"
backend_status=$(curl -s http://localhost:8080/api/actuator/health | jq -r '.status' 2>/dev/null || echo "DOWN")
echo "后端服务: $backend_status"

frontend_status=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:80)
if [ "$frontend_status" = "200" ]; then
    echo "前端服务: UP"
else
    echo "前端服务: DOWN"
fi

echo ""

# 检查数据库连接
echo "🗄️ 数据库状态:"
db_status=$(docker exec ai-interview-db pg_isready -U postgres 2>/dev/null && echo "UP" || echo "DOWN")
echo "PostgreSQL: $db_status"

redis_status=$(docker exec ai-interview-redis redis-cli ping 2>/dev/null || echo "DOWN")
echo "Redis: $redis_status"

echo ""

# 显示最近的错误日志
echo "📝 最近的错误日志:"
docker-compose logs --tail=10 --no-color | grep -i error || echo "无错误日志"
6.4 运维管理

备份脚本

bash 复制代码
#!/bin/bash
# backup.sh

BACKUP_DIR="/backup/ai-interview"
DATE=$(date +%Y%m%d_%H%M%S)

echo "🔄 开始备份AI面试系统数据..."

# 创建备份目录
mkdir -p $BACKUP_DIR

# 备份数据库
echo "📀 备份数据库..."
docker exec ai-interview-db pg_dump -U postgres ai_interview | gzip > $BACKUP_DIR/database_$DATE.sql.gz

# 备份上传文件
echo "📁 备份上传文件..."
tar -czf $BACKUP_DIR/uploads_$DATE.tar.gz uploads/

# 备份配置文件
echo "⚙️ 备份配置文件..."
tar -czf $BACKUP_DIR/config_$DATE.tar.gz docker-compose.yml .env

# 清理旧备份(保留7天)
echo "🧹 清理旧备份..."
find $BACKUP_DIR -name "*.gz" -mtime +7 -delete

echo "✅ 备份完成!"
echo "备份位置: $BACKUP_DIR"

日志管理脚本

bash 复制代码
#!/bin/bash
# log-management.sh

LOG_DIR="./logs"
MAX_SIZE="100M"
RETENTION_DAYS=30

echo "📋 日志管理任务开始..."

# 检查日志大小并轮转
if [ -f "$LOG_DIR/ai-interview.log" ]; then
    size=$(du -h "$LOG_DIR/ai-interview.log" | cut -f1)
    echo "当前日志大小: $size"
    
    # 如果日志文件大于100M,进行轮转
    if [ $(du -m "$LOG_DIR/ai-interview.log" | cut -f1) -gt 100 ]; then
        timestamp=$(date +%Y%m%d_%H%M%S)
        mv "$LOG_DIR/ai-interview.log" "$LOG_DIR/ai-interview_$timestamp.log"
        echo "日志已轮转: ai-interview_$timestamp.log"
    fi
fi

# 清理旧日志文件
echo "🧹 清理超过 $RETENTION_DAYS 天的日志文件..."
find $LOG_DIR -name "*.log" -mtime +$RETENTION_DAYS -delete

# 压缩7天前的日志文件
find $LOG_DIR -name "*.log" -mtime +7 ! -name "ai-interview.log" -exec gzip {} \;

echo "✅ 日志管理完成!"

🎯 最终验收与测试

完整系统测试清单

功能测试
  • ✅ 用户注册登录功能
  • ✅ 题库管理(增删改查)
  • ✅ 面试会话创建和管理
  • ✅ 面试进行(题目展示、回答提交)
  • ✅ 视频通话功能
  • ✅ 语音录制和播放
  • ✅ 代码编辑器功能
  • ✅ AI自动评估
  • ✅ 面试结果查看
  • ✅ 面试历史记录
性能测试
  • ✅ 并发用户测试(100+用户同时在线)
  • ✅ 数据库查询性能(复杂查询<100ms)
  • ✅ AI服务响应时间(<5秒)
  • ✅ 文件上传下载性能
  • ✅ 前端页面加载速度(<3秒)
安全测试
  • ✅ JWT Token安全验证
  • ✅ SQL注入防护
  • ✅ XSS攻击防护
  • ✅ CSRF攻击防护
  • ✅ 敏感数据加密存储
兼容性测试
  • ✅ Chrome浏览器支持
  • ✅ Firefox浏览器支持
  • ✅ Safari浏览器支持
  • ✅ 移动端响应式布局
  • ✅ 不同网络环境适配

部署验收

  • ✅ Docker容器正常运行
  • ✅ 数据库连接正常
  • ✅ 缓存服务正常
  • ✅ 监控指标正常
  • ✅ 日志记录完整
  • ✅ 备份恢复功能

🎉 项目总结

技术栈汇总

复制代码
后端技术栈:
├── Spring Boot 3.x        # 主框架
├── Spring Security 6.x    # 安全框架
├── Spring Data JPA        # 数据访问
├── PostgreSQL + Vector    # 数据库
├── Redis                  # 缓存
├── Spring AI Alibaba      # AI集成
└── Docker                 # 容器化

前端技术栈:
├── React 18               # 前端框架
├── TypeScript             # 类型安全
├── Vite                   # 构建工具
├── Vaadin Components      # UI组件库
├── WebRTC                 # 视频通话
└── MediaRecorder API      # 语音录制

核心功能模块

  1. 用户认证系统 - JWT Token认证,角色权限控制
  2. 题库管理系统 - 题目CRUD,分类标签,智能推荐
  3. 面试引擎 - 会话管理,题目分发,答案收集
  4. 实时通信 - WebRTC视频,WebSocket消息
  5. AI评估引擎 - 自然语言处理,代码分析,智能评分
  6. 数据分析 - 面试统计,性能报表,趋势分析

学习收获

通过本项目,您将掌握:

  1. 全栈开发技能

    • Spring Boot现代化开发
    • React Hooks和函数组件
    • TypeScript类型系统
    • RESTful API设计
  2. AI技术集成

    • 大模型API调用
    • Prompt工程技巧
    • 向量数据库应用
    • 智能评估算法
  3. 实时通信技术

    • WebRTC点对点通信
    • WebSocket实时消息
    • 媒体流处理
    • 音视频编解码
  4. 系统架构设计

    • 微服务架构思想
    • 数据库设计优化
    • 缓存策略应用
    • 安全防护体系
  5. DevOps实践

    • Docker容器化
    • CI/CD流水线
    • 监控告警系统
    • 自动化运维

扩展方向

项目可以继续扩展的方向:

  1. AI能力增强

    • 多模态AI分析(表情识别、语调分析)
    • 个性化推荐算法
    • 智能题目生成
    • 语音合成面试官
  2. 功能完善

    • 移动端APP开发
    • 直播面试功能
    • 团队协作面试
    • 面试数据分析报表
  3. 性能优化

    • CDN加速部署
    • 数据库读写分离
    • 微服务架构拆分
    • 负载均衡优化
  4. 商业化功能

    • 企业版权限管理
    • 付费增值服务
    • API开放平台
    • 第三方系统集成

恭喜您完成了AI面试系统的完整开发!

这个项目涵盖了现代软件开发的各个方面,从前端交互到后端架构,从AI集成到系统部署,是一个完整的企业级应用开发实践。

通过循序渐进的学习,您不仅掌握了技术实现,更重要的是理解了系统设计的思想和工程化开发的流程。

继续保持学习的热情,在实践中不断提升技术能力! 🚀

源码地址

相关推荐
mzlogin2 小时前
DIY|Mac 搭建 ESP-IDF 开发环境及编译小智 AI
人工智能
归去_来兮2 小时前
知识图谱技术概述
大数据·人工智能·知识图谱
就是有点傻2 小时前
VM图像处理之图像二值化
图像处理·人工智能·计算机视觉
行云流水剑2 小时前
【学习记录】深入解析 AI 交互中的五大核心概念:Prompt、Agent、MCP、Function Calling 与 Tools
人工智能·学习·交互
love530love2 小时前
【笔记】在 MSYS2(MINGW64)中正确安装 Rust
运维·开发语言·人工智能·windows·笔记·python·rust
A林玖2 小时前
【机器学习】主成分分析 (PCA)
人工智能·机器学习
Jamence2 小时前
多模态大语言模型arxiv论文略读(108)
论文阅读·人工智能·语言模型·自然语言处理·论文笔记
tongxianchao2 小时前
双空间知识蒸馏用于大语言模型
人工智能·语言模型·自然语言处理
苗老大2 小时前
MMRL: Multi-Modal Representation Learning for Vision-Language Models(多模态表示学习)
人工智能·学习·语言模型
緈福的街口2 小时前
【leetcode】347. 前k个高频元素
算法·leetcode·职场和发展