当前位置: 首页 > article >正文

Java全栈项目-校园兼职信息平台

项目简介

本项目是一个基于Spring Boot + Vue.js的校园兼职信息平台,旨在为大学生提供便捷的兼职信息获取渠道。平台支持学生查看兼职信息、投递简历,企业发布职位、管理应聘等功能。

技术栈

后端技术

  • Spring Boot 2.7.x
  • Spring Security
  • MyBatis Plus
  • MySQL 8.0
  • Redis
  • JWT认证

前端技术

  • Vue.js 3
  • Element Plus
  • Axios
  • Vuex
  • Vue Router

核心功能

  1. 用户管理

    • 学生注册/登录
    • 企业注册/登录
    • 管理员后台管理
  2. 兼职信息管理

    • 发布兼职
    • 职位搜索
    • 职位分类展示
    • 职位详情查看
  3. 简历管理

    • 在线制作简历
    • 投递简历
    • 简历状态跟踪
  4. 消息通知

    • 简历投递通知
    • 面试邀请通知
    • 系统公告

数据库设计

主要表结构

-- 用户表
CREATE TABLE `user` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL,
  `password` varchar(100) NOT NULL,
  `role` tinyint NOT NULL COMMENT '角色:1-学生 2-企业 3-管理员',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
);

-- 职位表
CREATE TABLE `job` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `title` varchar(100) NOT NULL,
  `company_id` bigint NOT NULL,
  `salary` decimal(10,2) NOT NULL,
  `description` text,
  `status` tinyint DEFAULT '1' COMMENT '状态:0-下架 1-招聘中',
  PRIMARY KEY (`id`)
);

核心代码展示

后端接口示例

@RestController
@RequestMapping("/api/job")
public class JobController {
    @Autowired
    private JobService jobService;
    
    @PostMapping("/publish")
    public Result publishJob(@RequestBody JobDTO jobDTO) {
        return jobService.publishJob(jobDTO);
    }
    
    @GetMapping("/list")
    public Result getJobList(@RequestParam(required = false) String keyword,
                           @RequestParam(defaultValue = "1") Integer page,
                           @RequestParam(defaultValue = "10") Integer size) {
        return jobService.getJobList(keyword, page, size);
    }
}

前端页面示例

<template>
  <div class="job-list">
    <el-card>
      <el-input
        v-model="searchKeyword"
        placeholder="请输入职位关键词"
        @keyup.enter="handleSearch"
      />
      <el-table :data="jobList">
        <el-table-column prop="title" label="职位名称" />
        <el-table-column prop="salary" label="薪资" />
        <el-table-column prop="companyName" label="公司名称" />
        <el-table-column label="操作">
          <template #default="scope">
            <el-button @click="viewDetail(scope.row)">查看详情</el-button>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
  </div>
</template>

项目亮点

  1. 采用前后端分离架构,提高开发效率和系统可维护性
  2. 使用Redis缓存热门职位信息,提升系统性能
  3. 实现基于JWT的无状态认证
  4. 引入ElasticSearch实现职位全文检索
  5. 使用WebSocket实现即时消息通知

项目部署

  1. 环境要求

    • JDK 1.8+
    • Maven 3.6+
    • Node.js 14+
    • MySQL 8.0
    • Redis
  2. 部署步骤

    • 后端打包:mvn clean package
    • 前端打包:npm run build
    • 使用Nginx部署前端静态资源
    • 使用Docker部署后端服务

总结与展望

本项目实现了校园兼职信息平台的基本功能,后续计划添加以下功能:

  1. 智能职位推荐
  2. 在线面试系统
  3. 移动端适配
  4. 数据分析功能

通过本项目的开发,不仅提升了全栈开发能力,也对项目架构设计有了更深的理解。

校园兼职信息平台功能详细设计

一、用户管理模块

1. 学生用户功能

1.1 注册功能
  • 必填信息:
    • 用户名(学号)
    • 密码
    • 确认密码
    • 真实姓名
    • 手机号
    • 邮箱
  • 选填信息:
    • 所属院系
    • 年级
    • 专业
CREATE TABLE `student` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `student_no` varchar(20) NOT NULL COMMENT '学号',
  `username` varchar(50) NOT NULL,
  `password` varchar(100) NOT NULL,
  `real_name` varchar(20) NOT NULL,
  `phone` varchar(11) NOT NULL,
  `email` varchar(50) NOT NULL,
  `department` varchar(50),
  `grade` varchar(20),
  `major` varchar(50),
  `status` tinyint DEFAULT '1' COMMENT '状态:0-禁用 1-正常',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_student_no` (`student_no`)
);
1.2 登录功能
@RestController
@RequestMapping("/api/auth")
public class AuthController {
    
    @PostMapping("/student/login")
    public Result studentLogin(@RequestBody LoginDTO loginDTO) {
        // 验证用户名密码
        // 生成JWT token
        // 返回用户信息和token
    }
}

2. 企业用户功能

2.1 注册功能
  • 必填信息:
    • 企业名称
    • 统一社会信用代码
    • 企业联系人
    • 联系电话
    • 邮箱
    • 密码
  • 选填信息:
    • 企业简介
    • 企业地址
    • 企业规模
    • 企业logo
CREATE TABLE `company` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `company_name` varchar(100) NOT NULL,
  `credit_code` varchar(50) NOT NULL,
  `contact_person` varchar(20) NOT NULL,
  `contact_phone` varchar(11) NOT NULL,
  `email` varchar(50) NOT NULL,
  `password` varchar(100) NOT NULL,
  `description` text,
  `address` varchar(200),
  `scale` varchar(50),
  `logo_url` varchar(200),
  `status` tinyint DEFAULT '0' COMMENT '状态:0-待审核 1-正常 2-禁用',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_credit_code` (`credit_code`)
);

3. 管理员功能

3.1 用户管理
@RestController
@RequestMapping("/api/admin/user")
public class UserManageController {
    
    @GetMapping("/student/list")
    public Result getStudentList(PageQuery query) {
        // 分页查询学生列表
    }
    
    @GetMapping("/company/list")
    public Result getCompanyList(PageQuery query) {
        // 分页查询企业列表
    }
    
    @PostMapping("/company/audit/{id}")
    public Result auditCompany(@PathVariable Long id, @RequestBody AuditDTO auditDTO) {
        // 企业审核
    }
}

二、兼职信息管理模块

1. 发布兼职

1.1 职位信息表设计
CREATE TABLE `job` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `company_id` bigint NOT NULL,
  `title` varchar(100) NOT NULL COMMENT '职位标题',
  `category_id` bigint NOT NULL COMMENT '职位分类ID',
  `salary_type` tinyint NOT NULL COMMENT '薪资类型:1-时薪 2-日结 3-月结',
  `salary_min` decimal(10,2) NOT NULL,
  `salary_max` decimal(10,2) NOT NULL,
  `work_time` varchar(200) NOT NULL COMMENT '工作时间',
  `work_address` varchar(200) NOT NULL,
  `required_num` int NOT NULL COMMENT '招聘人数',
  `description` text NOT NULL COMMENT '职位描述',
  `requirement` text COMMENT '岗位要求',
  `status` tinyint DEFAULT '1' COMMENT '状态:0-下架 1-招聘中',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_company_id` (`company_id`),
  KEY `idx_category_id` (`category_id`)
);
1.2 发布接口
@PostMapping("/publish")
public Result publishJob(@RequestBody JobDTO jobDTO) {
    // 验证企业状态
    // 保存职位信息
    // 发送消息通知关注该分类的学生
}

2. 职位搜索

@Service
public class JobSearchService {
    
    public PageResult<JobVO> searchJobs(JobSearchDTO searchDTO) {
        // 构建查询条件
        QueryWrapper<Job> queryWrapper = new QueryWrapper<>();
        
        // 关键词搜索
        if (StringUtils.hasText(searchDTO.getKeyword())) {
            queryWrapper.like("title", searchDTO.getKeyword())
                       .or()
                       .like("description", searchDTO.getKeyword());
        }
        
        // 分类过滤
        if (searchDTO.getCategoryId() != null) {
            queryWrapper.eq("category_id", searchDTO.getCategoryId());
        }
        
        // 薪资范围过滤
        if (searchDTO.getSalaryMin() != null) {
            queryWrapper.ge("salary_min", searchDTO.getSalaryMin());
        }
        
        // 排序
        queryWrapper.orderByDesc("create_time");
        
        // 返回结果
        return jobMapper.selectPage(searchDTO.getPage(), queryWrapper);
    }
}

3. 职位分类展示

CREATE TABLE `job_category` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL COMMENT '分类名称',
  `parent_id` bigint DEFAULT '0' COMMENT '父分类ID',
  `sort` int DEFAULT '0' COMMENT '排序',
  `status` tinyint DEFAULT '1' COMMENT '状态:0-禁用 1-正常',
  PRIMARY KEY (`id`)
);

4. 职位详情页面

<template>
  <div class="job-detail">
    <el-card>
      <!-- 职位基本信息 -->
      <div class="job-header">
        <h2>{{ job.title }}</h2>
        <div class="salary">{{ formatSalary(job) }}</div>
      </div>
      
      <!-- 工作信息 -->
      <div class="job-info">
        <el-descriptions :column="3">
          <el-descriptions-item label="工作地点">
            {{ job.workAddress }}
          </el-descriptions-item>
          <el-descriptions-item label="工作时间">
            {{ job.workTime }}
          </el-descriptions-item>
          <el-descriptions-item label="招聘人数">
            {{ job.requiredNum }}人
          </el-descriptions-item>
        </el-descriptions>
      </div>
      
      <!-- 职位描述 -->
      <div class="job-content">
        <h3>职位描述</h3>
        <div v-html="job.description"></div>
        
        <h3>岗位要求</h3>
        <div v-html="job.requirement"></div>
      </div>
      
      <!-- 企业信息 -->
      <div class="company-info">
        <h3>企业信息</h3>
        <el-descriptions>
          <el-descriptions-item label="企业名称">
            {{ job.companyName }}
          </el-descriptions-item>
          <el-descriptions-item label="企业规模">
            {{ job.companyScale }}
          </el-descriptions-item>
        </el-descriptions>
      </div>
      
      <!-- 投递按钮 -->
      <div class="apply-btn">
        <el-button type="primary" @click="handleApply">
          立即投递
        </el-button>
      </div>
    </el-card>
  </div>
</template>

以上是校园兼职信息平台用户管理和兼职信息管理模块的详细设计。每个功能模块都包含了数据库设计、后端接口和前端页面的核心代码。系统采用分层架构,遵循RESTful API设计规范,确保了代码的可维护性和扩展性。

校园兼职信息平台简历管理与消息通知模块设计

一、简历管理模块

1. 简历信息表设计

-- 简历基本信息表
CREATE TABLE `resume` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `student_id` bigint NOT NULL,
  `name` varchar(20) NOT NULL,
  `gender` tinyint NOT NULL COMMENT '性别:1-男 2-女',
  `birth_date` date,
  `phone` varchar(11) NOT NULL,
  `email` varchar(50) NOT NULL,
  `education` varchar(20) NOT NULL COMMENT '学历',
  `school` varchar(50) NOT NULL,
  `major` varchar(50) NOT NULL,
  `grade` varchar(20) NOT NULL,
  `self_evaluation` text COMMENT '自我评价',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_student_id` (`student_id`)
);

-- 工作经验表
CREATE TABLE `resume_experience` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `resume_id` bigint NOT NULL,
  `company_name` varchar(100) NOT NULL,
  `position` varchar(50) NOT NULL,
  `start_date` date NOT NULL,
  `end_date` date,
  `description` text,
  PRIMARY KEY (`id`),
  KEY `idx_resume_id` (`resume_id`)
);

-- 技能特长表
CREATE TABLE `resume_skill` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `resume_id` bigint NOT NULL,
  `skill_name` varchar(50) NOT NULL,
  `skill_level` tinyint NOT NULL COMMENT '熟练度:1-了解 2-熟悉 3-精通',
  PRIMARY KEY (`id`),
  KEY `idx_resume_id` (`resume_id`)
);

2. 在线制作简历

@RestController
@RequestMapping("/api/resume")
public class ResumeController {
    
    @PostMapping("/save")
    public Result saveResume(@RequestBody ResumeDTO resumeDTO) {
        return resumeService.saveResume(resumeDTO);
    }
    
    @GetMapping("/{id}")
    public Result getResume(@PathVariable Long id) {
        return resumeService.getResumeDetail(id);
    }
    
    @PostMapping("/experience/add")
    public Result addExperience(@RequestBody ResumeExperienceDTO experienceDTO) {
        return resumeService.addExperience(experienceDTO);
    }
    
    @PostMapping("/skill/add")
    public Result addSkill(@RequestBody ResumeSkillDTO skillDTO) {
        return resumeService.addSkill(skillDTO);
    }
}

3. 简历投递

CREATE TABLE `resume_delivery` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `student_id` bigint NOT NULL,
  `resume_id` bigint NOT NULL,
  `job_id` bigint NOT NULL,
  `company_id` bigint NOT NULL,
  `status` tinyint DEFAULT '1' COMMENT '状态:1-待查看 2-已查看 3-邀请面试 4-不合适',
  `delivery_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `view_time` datetime COMMENT '查看时间',
  `interview_time` datetime COMMENT '面试时间',
  `interview_address` varchar(200) COMMENT '面试地点',
  `interview_note` text COMMENT '面试备注',
  `reject_reason` varchar(200) COMMENT '拒绝原因',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_student_job` (`student_id`, `job_id`),
  KEY `idx_company_id` (`company_id`)
);

4. 简历状态跟踪

@Service
public class ResumeDeliveryService {
    
    public PageResult<DeliveryVO> getDeliveryList(DeliveryQueryDTO queryDTO) {
        QueryWrapper<ResumeDelivery> wrapper = new QueryWrapper<>();
        wrapper.eq("student_id", queryDTO.getStudentId());
        
        if (queryDTO.getStatus() != null) {
            wrapper.eq("status", queryDTO.getStatus());
        }
        
        wrapper.orderByDesc("delivery_time");
        return deliveryMapper.selectDeliveryList(queryDTO.getPage(), wrapper);
    }
    
    public Result updateDeliveryStatus(Long deliveryId, DeliveryStatusDTO statusDTO) {
        ResumeDelivery delivery = deliveryMapper.selectById(deliveryId);
        if (delivery == null) {
            return Result.error("投递记录不存在");
        }
        
        // 更新状态
        delivery.setStatus(statusDTO.getStatus());
        
        // 根据不同状态设置相应字段
        if (statusDTO.getStatus() == DeliveryStatusEnum.INTERVIEW.getCode()) {
            delivery.setInterviewTime(statusDTO.getInterviewTime());
            delivery.setInterviewAddress(statusDTO.getInterviewAddress());
            delivery.setInterviewNote(statusDTO.getInterviewNote());
            
            // 发送面试通知
            messageService.sendInterviewNotification(delivery);
        }
        
        deliveryMapper.updateById(delivery);
        return Result.success();
    }
}

二、消息通知模块

1. 消息表设计

CREATE TABLE `message` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` bigint NOT NULL COMMENT '接收用户ID',
  `type` tinyint NOT NULL COMMENT '消息类型:1-系统公告 2-简历投递 3-面试通知',
  `title` varchar(100) NOT NULL,
  `content` text NOT NULL,
  `is_read` tinyint DEFAULT '0' COMMENT '是否已读:0-未读 1-已读',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_user_id` (`user_id`)
);

2. 消息服务实现

@Service
public class MessageService {
    
    @Autowired
    private MessageMapper messageMapper;
    
    @Autowired
    private WebSocketService webSocketService;
    
    // 发送简历投递通知
    public void sendDeliveryNotification(ResumeDelivery delivery) {
        Message message = new Message();
        message.setUserId(delivery.getCompanyId());
        message.setType(MessageTypeEnum.DELIVERY.getCode());
        message.setTitle("新简历投递通知");
        message.setContent(String.format("学生 %s 投递了 %s 职位的简历", 
            delivery.getStudentName(), delivery.getJobTitle()));
        
        messageMapper.insert(message);
        
        // WebSocket推送消息
        webSocketService.sendMessage(delivery.getCompanyId(), message);
    }
    
    // 发送面试通知
    public void sendInterviewNotification(ResumeDelivery delivery) {
        Message message = new Message();
        message.setUserId(delivery.getStudentId());
        message.setType(MessageTypeEnum.INTERVIEW.getCode());
        message.setTitle("面试通知");
        message.setContent(String.format("您投递的 %s 职位已通过初筛,请准时参加面试。\n面试时间:%s\n面试地点:%s", 
            delivery.getJobTitle(),
            DateUtil.formatDateTime(delivery.getInterviewTime()),
            delivery.getInterviewAddress()));
        
        messageMapper.insert(message);
        
        // WebSocket推送消息
        webSocketService.sendMessage(delivery.getStudentId(), message);
    }
    
    // 发布系统公告
    public void publishAnnouncement(AnnouncementDTO announcementDTO) {
        Message message = new Message();
        message.setType(MessageTypeEnum.ANNOUNCEMENT.getCode());
        message.setTitle(announcementDTO.getTitle());
        message.setContent(announcementDTO.getContent());
        
        // 群发消息
        webSocketService.broadcastMessage(message);
    }
}

3. WebSocket消息推送

@ServerEndpoint("/websocket/{userId}")
@Component
public class WebSocketServer {
    
    private static Map<Long, Session> sessionMap = new ConcurrentHashMap<>();
    
    @OnOpen
    public void onOpen(Session session, @PathParam("userId") Long userId) {
        sessionMap.put(userId, session);
    }
    
    @OnClose
    public void onClose(@PathParam("userId") Long userId) {
        sessionMap.remove(userId);
    }
    
    @OnMessage
    public void onMessage(String message, Session session) {
        // 处理客户端消息
    }
    
    // 发送消息给指定用户
    public void sendMessage(Long userId, Message message) {
        Session session = sessionMap.get(userId);
        if (session != null) {
            session.getAsyncRemote().sendText(JSON.toJSONString(message));
        }
    }
    
    // 广播消息
    public void broadcastMessage(Message message) {
        sessionMap.forEach((userId, session) -> {
            session.getAsyncRemote().sendText(JSON.toJSONString(message));
        });
    }
}

4. 前端消息展示

<template>
  <div class="message-list">
    <el-tabs v-model="activeTab">
      <el-tab-pane label="全部消息" name="all">
        <message-table :messages="messages" />
      </el-tab-pane>
      <el-tab-pane label="未读消息" name="unread">
        <message-table :messages="unreadMessages" />
      </el-tab-pane>
    </el-tabs>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { useWebSocket } from '@/hooks/useWebSocket'

const activeTab = ref('all')
const messages = ref([])
const unreadMessages = ref([])

// WebSocket连接
const { connect, onMessage } = useWebSocket()

onMounted(() => {
  // 建立WebSocket连接
  connect()
  
  // 监听新消息
  onMessage((message) => {
    messages.value.unshift(message)
    if (!message.isRead) {
      unreadMessages.value.unshift(message)
    }
  })
  
  // 加载历史消息
  loadMessages()
})

const loadMessages = async () => {
  const res = await messageApi.getMessageList()
  messages.value = res.data
  unreadMessages.value = res.data.filter(msg => !msg.isRead)
}
</script>

以上是简历管理和消息通知模块的详细设计。主要包括:

  1. 完整的简历信息存储结构
  2. 简历投递和跟踪功能
  3. 基于WebSocket的实时消息推送
  4. 消息分类管理和展示

系统通过WebSocket实现了实时消息推送,确保用户可以及时收到重要通知。同时,消息持久化到数据库,保证了消息的可靠性和可追溯性。

在线面试系统详细设计

一、数据库设计

1. 面试房间表

CREATE TABLE `interview_room` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `room_no` varchar(32) NOT NULL COMMENT '房间号',
  `delivery_id` bigint NOT NULL COMMENT '简历投递ID',
  `company_id` bigint NOT NULL,
  `student_id` bigint NOT NULL,
  `start_time` datetime NOT NULL COMMENT '开始时间',
  `end_time` datetime COMMENT '结束时间',
  `status` tinyint DEFAULT '0' COMMENT '状态:0-待开始 1-进行中 2-已结束 3-已取消',
  `cancel_reason` varchar(200) COMMENT '取消原因',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_room_no` (`room_no`),
  KEY `idx_delivery_id` (`delivery_id`)
);

-- 面试记录表
CREATE TABLE `interview_record` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `room_id` bigint NOT NULL,
  `type` tinyint NOT NULL COMMENT '记录类型:1-聊天 2-代码 3-白板',
  `content` text NOT NULL,
  `create_by` bigint NOT NULL,
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_room_id` (`room_id`)
);

-- 面试评价表
CREATE TABLE `interview_evaluation` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `room_id` bigint NOT NULL,
  `score` int NOT NULL COMMENT '综合评分:1-5分',
  `professional_ability` int NOT NULL COMMENT '专业能力',
  `communication_ability` int NOT NULL COMMENT '沟通能力',
  `comprehensive_quality` int NOT NULL COMMENT '综合素质',
  `evaluation` text NOT NULL COMMENT '评价内容',
  `result` tinyint NOT NULL COMMENT '面试结果:1-通过 2-待定 3-不通过',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_room_id` (`room_id`)
);

二、核心功能实现

1. 面试房间管理

@Service
public class InterviewRoomService {
    
    @Autowired
    private InterviewRoomMapper roomMapper;
    
    // 创建面试房间
    public Result createRoom(CreateRoomDTO createDTO) {
        InterviewRoom room = new InterviewRoom();
        room.setRoomNo(generateRoomNo());
        room.setDeliveryId(createDTO.getDeliveryId());
        room.setCompanyId(createDTO.getCompanyId());
        room.setStudentId(createDTO.getStudentId());
        room.setStartTime(createDTO.getStartTime());
        
        roomMapper.insert(room);
        
        // 发送面试通知
        messageService.sendInterviewNotification(room);
        
        return Result.success(room);
    }
    
    // 进入面试房间
    public Result enterRoom(String roomNo) {
        InterviewRoom room = roomMapper.selectByRoomNo(roomNo);
        if (room == null) {
            return Result.error("面试房间不存在");
        }
        
        // 验证面试时间
        if (LocalDateTime.now().isBefore(room.getStartTime())) {
            return Result.error("面试还未开始");
        }
        
        // 更新房间状态
        if (room.getStatus() == 0) {
            room.setStatus(1);
            roomMapper.updateById(room);
        }
        
        return Result.success(room);
    }
}

2. WebRTC视频通话

export class RTCClient {
    constructor(roomId, userId) {
        this.pc = new RTCPeerConnection(configuration);
        this.roomId = roomId;
        this.userId = userId;
        this.localStream = null;
        this.remoteStream = null;
        
        this.initPeerConnection();
    }
    
    // 初始化连接
    async initPeerConnection() {
        // 获取本地媒体流
        this.localStream = await navigator.mediaDevices.getUserMedia({
            audio: true,
            video: true
        });
        
        // 添加本地流
        this.localStream.getTracks().forEach(track => {
            this.pc.addTrack(track, this.localStream);
        });
        
        // 监听远程流
        this.pc.ontrack = event => {
            this.remoteStream = event.streams[0];
            this.onRemoteStream(this.remoteStream);
        };
        
        // ICE候选处理
        this.pc.onicecandidate = event => {
            if (event.candidate) {
                this.sendIceCandidate(event.candidate);
            }
        };
    }
    
    // 创建offer
    async createOffer() {
        const offer = await this.pc.createOffer();
        await this.pc.setLocalDescription(offer);
        this.sendOffer(offer);
    }
    
    // 处理answer
    async handleAnswer(answer) {
        await this.pc.setRemoteDescription(new RTCSessionDescription(answer));
    }
    
    // 处理ICE候选
    async handleIceCandidate(candidate) {
        await this.pc.addIceCandidate(new RTCIceCandidate(candidate));
    }
}

3. 在线代码编辑器

<template>
  <div class="code-editor">
    <div class="editor-header">
      <el-select v-model="language" placeholder="选择语言">
        <el-option label="Java" value="java" />
        <el-option label="Python" value="python" />
        <el-option label="JavaScript" value="javascript" />
      </el-select>
      <el-button type="primary" @click="runCode">运行代码</el-button>
    </div>
    
    <monaco-editor
      v-model:value="code"
      :language="language"
      :options="editorOptions"
      @change="handleCodeChange"
    />
    
    <div class="output-panel">
      <div class="output-header">
        <span>输出结果</span>
        <el-button size="small" @click="clearOutput">清空</el-button>
      </div>
      <pre class="output-content">{{ output }}</pre>
    </div>
  </div>
</template>

<script setup>
import { ref, watch } from 'vue'
import { MonacoEditor } from 'monaco-editor-vue3'

const language = ref('java')
const code = ref('')
const output = ref('')

const editorOptions = {
  theme: 'vs-dark',
  automaticLayout: true,
  minimap: { enabled: false }
}

// 代码变更同步
const handleCodeChange = (value) => {
  socket.emit('code-change', {
    roomId,
    code: value
  })
}

// 运行代码
const runCode = async () => {
  try {
    const res = await codeRunnerApi.execute({
      language: language.value,
      code: code.value
    })
    output.value = res.data.output
  } catch (error) {
    output.value = error.message
  }
}
</script>

4. 在线白板

<template>
  <div class="whiteboard">
    <canvas ref="canvas" @mousedown="startDrawing" @mousemove="draw" @mouseup="stopDrawing" />
    
    <div class="toolbar">
      <el-color-picker v-model="strokeColor" size="small" />
      <el-slider v-model="strokeWidth" :min="1" :max="10" />
      <el-button-group>
        <el-button @click="setTool('pen')">画笔</el-button>
        <el-button @click="setTool('eraser')">橡皮擦</el-button>
        <el-button @click="clearCanvas">清空</el-button>
      </el-button-group>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'

interface Point {
  x: number
  y: number
}

const canvas = ref<HTMLCanvasElement | null>(null)
const ctx = ref<CanvasRenderingContext2D | null>(null)
const isDrawing = ref(false)
const lastPoint = ref<Point | null>(null)

const strokeColor = ref('#000000')
const strokeWidth = ref(2)
const currentTool = ref('pen')

onMounted(() => {
  if (canvas.value) {
    ctx.value = canvas.value.getContext('2d')
    initCanvas()
  }
})

const initCanvas = () => {
  if (!canvas.value || !ctx.value) return
  
  // 设置画布大小
  canvas.value.width = canvas.value.offsetWidth
  canvas.value.height = canvas.value.offsetHeight
  
  // 初始化画笔样式
  ctx.value.lineCap = 'round'
  ctx.value.lineJoin = 'round'
}

const startDrawing = (e: MouseEvent) => {
  isDrawing.value = true
  lastPoint.value = getCanvasPoint(e)
}

const draw = (e: MouseEvent) => {
  if (!isDrawing.value || !ctx.value || !lastPoint.value) return
  
  const currentPoint = getCanvasPoint(e)
  
  ctx.value.beginPath()
  ctx.value.strokeStyle = currentTool.value === 'eraser' ? '#ffffff' : strokeColor.value
  ctx.value.lineWidth = currentTool.value === 'eraser' ? 20 : strokeWidth.value
  
  ctx.value.moveTo(lastPoint.value.x, lastPoint.value.y)
  ctx.value.lineTo(currentPoint.x, currentPoint.y)
  ctx.value.stroke()
  
  // 同步绘画数据
  socket.emit('draw', {
    roomId,
    from: lastPoint.value,
    to: currentPoint,
    tool: currentTool.value,
    color: strokeColor.value,
    width: strokeWidth.value
  })
  
  lastPoint.value = currentPoint
}

const stopDrawing = () => {
  isDrawing.value = false
  lastPoint.value = null
}

const getCanvasPoint = (e: MouseEvent): Point => {
  if (!canvas.value) return { x: 0, y: 0 }
  
  const rect = canvas.value.getBoundingClientRect()
  return {
    x: e.clientX - rect.left,
    y: e.clientY - rect.top
  }
}
</script>

5. 面试评价

@RestController
@RequestMapping("/api/interview/evaluation")
public class InterviewEvaluationController {
    
    @PostMapping("/submit")
    public Result submitEvaluation(@RequestBody EvaluationDTO evaluationDTO) {
        InterviewEvaluation evaluation = new InterviewEvaluation();
        evaluation.setRoomId(evaluationDTO.getRoomId());
        evaluation.setScore(evaluationDTO.getScore());
        evaluation.setProfessionalAbility(evaluationDTO.getProfessionalAbility());
        evaluation.setCommunicationAbility(evaluationDTO.getCommunicationAbility());
        evaluation.setComprehensiveQuality(evaluationDTO.getComprehensiveQuality());
        evaluation.setEvaluation(evaluationDTO.getEvaluation());
        evaluation.setResult(evaluationDTO.getResult());
        
        evaluationService.save(evaluation);
        
        // 更新简历投递状态
        deliveryService.updateStatus(evaluationDTO.getDeliveryId(), 
            evaluationDTO.getResult() == 1 ? DeliveryStatus.PASS : DeliveryStatus.REJECT);
        
        return Result.success();
    }
}

三、面试房间页面

<template>
  <div class="interview-room">
    <!-- 视频区域 -->
    <div class="video-area">
      <div class="video-container">
        <video ref="localVideo" autoplay muted />
        <video ref="remoteVideo" autoplay />
      </div>
      <div class="video-controls">
        <el-button-group>
          <el-button @click="toggleAudio">
            <i-icon :name="isAudioEnabled ? 'mic' : 'mic-off'" />
          </el-button>
          <el-button @click="toggleVideo">
            <i-icon :name="isVideoEnabled ? 'video' : 'video-off'" />
          </el-button>
          <el-button @click="shareScreen">
            <i-icon name="desktop" />
          </el-button>
        </el-button-group>
      </div>
    </div>
    
    <!-- 功能区域 -->
    <div class="function-area">
      <el-tabs v-model="activeTab">
        <el-tab-pane label="代码编辑器" name="code">
          <code-editor />
        </el-tab-pane>
        <el-tab-pane label="在线白板" name="whiteboard">
          <whiteboard />
        </el-tab-pane>
        <el-tab-pane label="聊天" name="chat">
          <chat-panel />
        </el-tab-pane>
      </el-tabs>
    </div>
    
    <!-- 面试评价弹窗 -->
    <el-dialog v-model="showEvaluation" title="面试评价" width="600px">
      <evaluation-form @submit="submitEvaluation" />
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { RTCClient } from '@/utils/rtc'
import CodeEditor from '@/components/CodeEditor.vue'
import Whiteboard from '@/components/Whiteboard.vue'
import ChatPanel from '@/components/ChatPanel.vue'
import EvaluationForm from '@/components/EvaluationForm.vue'

const rtcClient = ref(null)
const activeTab = ref('code')
const showEvaluation = ref(false)

onMounted(() => {
  initRoom()
})

const initRoom = async () => {
  rtcClient.value = new RTCClient(roomId, userId)
  await rtcClient.value.initPeerConnection()
}

onBeforeUnmount(() => {
  rtcClient.value?.destroy()
})
</script>

四、技术要点

  1. 实时通信

    • 使用WebRTC实现音视频通话
    • 使用WebSocket实现代码同步和白板同步
    • 支持屏幕共享功能
  2. 代码编辑器

    • 使用Monaco Editor实现代码编辑
    • 支持多种编程语言
    • 实现代码运行功能
    • 代码实时同步
  3. 在线白板

    • Canvas实现绘画功能
    • 支持多种绘画工具
    • 实时同步绘画数据
    • 支持撤销/重做
  4. 面试评价

    • 多维度评分系统
    • 支持文字评价
    • 自动更新简历投递状态
  5. 数据安全

    • 房间访问权限控制
    • 面试记录加密存储
    • 视频通话数据加密传输

这个在线面试系统实现了远程面试的核心功能,包括视频通话、代码编辑、在线白板等,为远程技术面试提供了完整的解决方案。系统采用前后端分离架构,使用WebRTC和WebSocket实现实时通信,确保了面试过程的流畅性和可靠性。


http://www.kler.cn/a/463325.html

相关文章:

  • Rockect基于Dledger的Broker主从同步原理
  • 如何利用Logo设计免费生成器创建专业级Logo
  • Flutter-插件 scroll-to-index 实现 listView 滚动到指定索引位置
  • 【51单片机零基础-chapter6:LCD1602调试工具】
  • RS485方向自动控制电路分享
  • C++模板相关概念汇总
  • C++ Lambda 表达式: 简洁与高效的完美结合
  • 【 解决国内Github.com打不开的方法】
  • Tailwind CSS 实战:电商产品展示页面开发
  • leetcode 1345. 跳跃游戏 IV
  • 常见中间件漏洞复现
  • 【一款超好用的开源笔记Logseq本地Docker部署与远程使用指南】
  • 如何实现el-select多选下拉框中嵌套复选框并加校验不为空功能呢?
  • 核心业务从SQLServer迁移到金仓KingbaseES V9实录
  • perl:多线程 简单示例
  • 第七讲 比特币的法律地位与监管
  • 接雨水-力扣热题100
  • 使用 Jenkins 和 Spring Cloud 自动化微服务部署
  • 可编辑31页PPT | 大数据湖仓一体解决方案
  • 华为研发工程师编程题——明明的随机数
  • Win32汇编学习笔记01.环境配置
  • Apache Hive常见问题
  • 【网络】HTTP/1.0、HTTP/1.1、HTTP/2、HTTP/3比对
  • Keepalived + LVS 搭建高可用负载均衡及支持 Websocket 长连接
  • 【深度学习】Java DL4J基于 CNN 构建车辆识别与跟踪模型
  • Vue3 中的计算属性和监听属性