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
核心功能
-
用户管理
- 学生注册/登录
- 企业注册/登录
- 管理员后台管理
-
兼职信息管理
- 发布兼职
- 职位搜索
- 职位分类展示
- 职位详情查看
-
简历管理
- 在线制作简历
- 投递简历
- 简历状态跟踪
-
消息通知
- 简历投递通知
- 面试邀请通知
- 系统公告
数据库设计
主要表结构
-- 用户表
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>
项目亮点
- 采用前后端分离架构,提高开发效率和系统可维护性
- 使用Redis缓存热门职位信息,提升系统性能
- 实现基于JWT的无状态认证
- 引入ElasticSearch实现职位全文检索
- 使用WebSocket实现即时消息通知
项目部署
-
环境要求
- JDK 1.8+
- Maven 3.6+
- Node.js 14+
- MySQL 8.0
- Redis
-
部署步骤
- 后端打包:
mvn clean package
- 前端打包:
npm run build
- 使用Nginx部署前端静态资源
- 使用Docker部署后端服务
- 后端打包:
总结与展望
本项目实现了校园兼职信息平台的基本功能,后续计划添加以下功能:
- 智能职位推荐
- 在线面试系统
- 移动端适配
- 数据分析功能
通过本项目的开发,不仅提升了全栈开发能力,也对项目架构设计有了更深的理解。
校园兼职信息平台功能详细设计
一、用户管理模块
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. 消息表设计
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. 前端消息展示
以上是简历管理和消息通知模块的详细设计。主要包括:
- 完整的简历信息存储结构
- 简历投递和跟踪功能
- 基于WebSocket的实时消息推送
- 消息分类管理和展示
系统通过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>
四、技术要点
-
实时通信
- 使用WebRTC实现音视频通话
- 使用WebSocket实现代码同步和白板同步
- 支持屏幕共享功能
-
代码编辑器
- 使用Monaco Editor实现代码编辑
- 支持多种编程语言
- 实现代码运行功能
- 代码实时同步
-
在线白板
- Canvas实现绘画功能
- 支持多种绘画工具
- 实时同步绘画数据
- 支持撤销/重做
-
面试评价
- 多维度评分系统
- 支持文字评价
- 自动更新简历投递状态
-
数据安全
- 房间访问权限控制
- 面试记录加密存储
- 视频通话数据加密传输
这个在线面试系统实现了远程面试的核心功能,包括视频通话、代码编辑、在线白板等,为远程技术面试提供了完整的解决方案。系统采用前后端分离架构,使用WebRTC和WebSocket实现实时通信,确保了面试过程的流畅性和可靠性。