大学生就业问题一直是社会关注的焦点。每年数百万毕业生涌入就业市场,传统线下招聘模式存在信息不对称、匹配效率低下等问题。我在参与多个高校就业系统建设项目时发现,学生往往需要同时关注十几个招聘网站,而企业HR也疲于应付各种渠道的简历投递。这种低效的对接方式造成了大量资源浪费。
基于SpringBoot+Vue的大学生就业服务平台正是为了解决这些痛点而设计。系统采用前后端分离架构,整合了学生档案管理、企业招聘发布、智能岗位匹配等核心功能模块。相比传统系统,我们的平台具有三个显著优势:
提示:系统设计时特别考虑了高校IT基础设施现状,所有组件都支持国产化替代方案,数据库同时兼容MySQL和达梦等国产数据库。
系统采用经典的三层架构设计,前后端完全分离:
code复制前端层:Vue.js + ElementUI + Axios
↑
API网关:Spring Cloud Gateway
↑
业务层:SpringBoot + MyBatis-Plus
↑
数据层:MySQL + Redis
这种架构选择基于以下考量:
SpringBoot 2.7.x:
Vue 3.x:
MySQL 8.0:
岗位推荐算法是本系统的核心创新点,采用多维度加权评分模型:
java复制// 伪代码示例
public List<JobPosition> recommendJobs(Student student) {
// 基础权重
double majorWeight = 0.4; // 专业相关度
double skillWeight = 0.3; // 技能匹配度
double salaryWeight = 0.2; // 薪资期望匹配
double locationWeight = 0.1; // 地域偏好
return jobPositionList.stream()
.map(job -> {
double score =
calculateMajorMatch(student.getMajor(), job.getRequiredMajor()) * majorWeight +
calculateSkillMatch(student.getSkillTags(), job.getRequiredSkills()) * skillWeight +
calculateSalaryMatch(student.getExpectedSalary(), job.getSalaryRange()) * salaryWeight +
calculateLocationPreference(student.getPreferredCities(), job.getWorkCity()) * locationWeight;
job.setRecommendScore(score);
return job;
})
.sorted(Comparator.comparingDouble(JobPosition::getRecommendScore).reversed())
.limit(20)
.collect(Collectors.toList());
}
实际项目中还加入了以下优化:
系统采用RBAC模型结合JWT认证,关键实现要点:
sql复制CREATE TABLE `sys_permission` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(50) COLLATE utf8mb4_general_ci NOT NULL COMMENT '权限名称',
`code` varchar(50) COLLATE utf8mb4_general_ci NOT NULL COMMENT '权限代码',
`type` tinyint NOT NULL COMMENT '1菜单 2按钮 3API',
`parent_id` bigint DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/student/**").hasAnyRole("STUDENT", "ADMIN")
.antMatchers("/api/company/**").hasAnyRole("COMPANY", "ADMIN")
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager()));
}
}
javascript复制router.beforeEach((to, from, next) => {
const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
const userRole = store.getters.role
if (requiresAuth && !store.getters.isAuthenticated) {
next('/login')
} else if (to.meta.roles && !to.meta.roles.includes(userRole)) {
next('/403')
} else {
next()
}
})
学生表优化方案:
sql复制CREATE TABLE `student_profile` (
`id` bigint NOT NULL AUTO_INCREMENT,
`student_no` varchar(20) NOT NULL COMMENT '学号',
`name` varchar(50) NOT NULL,
`college_id` int NOT NULL COMMENT '学院ID',
`major_id` int NOT NULL COMMENT '专业ID',
`education` enum('本科','硕士','博士') NOT NULL,
`skills` json DEFAULT NULL COMMENT '技能标签',
`internship_exp` json DEFAULT NULL COMMENT '实习经历',
`expected_salary` varchar(20) DEFAULT NULL,
`preferred_cities` json DEFAULT NULL COMMENT '意向城市',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_student_no` (`student_no`),
KEY `idx_college_major` (`college_id`,`major_id`),
KEY `idx_education` (`education`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
职位表特殊设计:
sql复制CREATE TABLE `job_position` (
`id` bigint NOT NULL AUTO_INCREMENT,
`company_id` bigint NOT NULL,
`title` varchar(100) NOT NULL,
`job_type` enum('全职','兼职','实习') NOT NULL,
`salary_range` varchar(30) NOT NULL,
`work_cities` json NOT NULL COMMENT '工作城市(支持多选)',
`requirements` json NOT NULL COMMENT '任职要求',
`description` text NOT NULL,
`is_active` tinyint(1) NOT NULL DEFAULT '1',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FULLTEXT KEY `ft_title_desc` (`title`,`description`),
KEY `idx_company` (`company_id`),
KEY `idx_job_type` (`job_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
sql复制-- 低效写法
SELECT * FROM job_position
WHERE JSON_CONTAINS(work_cities, '"北京"')
AND salary_range LIKE '10k%';
-- 优化后写法
SELECT * FROM job_position
WHERE work_cities LIKE '%北京%' -- 利用前缀索引
AND salary_min >= 10000
AND salary_max <= 15000; -- 拆分salary_range为两个字段
sql复制ALTER TABLE student_profile
ADD COLUMN skill_php tinyint GENERATED ALWAYS AS (JSON_CONTAINS(skills, '"PHP"')) STORED,
ADD INDEX idx_skill_php (skill_php);
前后端分离部署时的完整解决方案:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.exposedHeaders("Authorization")
.allowCredentials(false)
.maxAge(3600);
}
}
javascript复制const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 10000,
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
// 请求拦截器
service.interceptors.request.use(config => {
if (store.getters.token) {
config.headers['Authorization'] = 'Bearer ' + getToken()
}
return config
})
简历文件上传的完整实现:
java复制@PostMapping("/upload/resume")
public R uploadResume(@RequestParam("file") MultipartFile file,
@RequestHeader("X-User-ID") Long userId) {
// 校验文件类型
String originalFilename = file.getOriginalFilename();
String fileExt = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);
if (!Arrays.asList("pdf", "doc", "docx").contains(fileExt.toLowerCase())) {
return R.error("仅支持PDF/DOC/DOCX格式");
}
// 存储文件
String newFilename = UUID.randomUUID() + "." + fileExt;
Path destPath = Paths.get(uploadDir, newFilename);
try {
Files.copy(file.getInputStream(), destPath, StandardCopyOption.REPLACE_EXISTING);
// 更新数据库记录
Resume resume = new Resume();
resume.setUserId(userId);
resume.setOriginalName(originalFilename);
resume.setStorageName(newFilename);
resume.setFileType(fileExt);
resumeService.save(resume);
return R.ok().put("url", "/download/" + newFilename);
} catch (IOException e) {
log.error("文件上传失败", e);
return R.error("上传失败");
}
}
vue复制<template>
<el-upload
action="/api/upload/resume"
:headers="headers"
:before-upload="beforeUpload"
:on-success="handleSuccess"
>
<el-button type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">
仅支持PDF/DOC/DOCX格式,大小不超过10MB
</div>
</el-upload>
</template>
<script>
export default {
data() {
return {
headers: {
'X-User-ID': this.$store.state.user.id
}
}
},
methods: {
beforeUpload(file) {
const isValidType = ['pdf', 'doc', 'docx'].includes(
file.name.split('.').pop().toLowerCase()
)
const isLt10M = file.size / 1024 / 1024 < 10
if (!isValidType) {
this.$message.error('文件格式不正确')
}
if (!isLt10M) {
this.$message.error('文件大小不能超过10MB')
}
return isValidType && isLt10M
},
handleSuccess(response) {
if (response.code === 200) {
this.$emit('upload-success', response.data.url)
}
}
}
}
</script>
推荐采用Docker Compose部署:
yaml复制version: '3.8'
services:
mysql:
image: mysql:8.0
container_name: employment-mysql
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS}
MYSQL_DATABASE: employment
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASS}
volumes:
- mysql_data:/var/lib/mysql
ports:
- "3306:3306"
restart: always
redis:
image: redis:6.2
container_name: employment-redis
ports:
- "6379:6379"
restart: always
backend:
build: ./backend
container_name: employment-api
ports:
- "8080:8080"
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/employment
SPRING_DATASOURCE_USERNAME: ${DB_USER}
SPRING_DATASOURCE_PASSWORD: ${DB_PASS}
SPRING_REDIS_HOST: redis
depends_on:
- mysql
- redis
restart: always
frontend:
build: ./frontend
container_name: employment-web
ports:
- "80:80"
restart: always
volumes:
mysql_data:
SpringBoot Actuator监控端点配置:
properties复制# application-prod.properties
management.endpoints.web.exposure.include=health,info,metrics,prometheus
management.metrics.export.prometheus.enabled=true
management.endpoint.health.show-details=always
配合Grafana监控面板,关键指标包括:
在实际使用中,我们收集到以下改进需求:
技术债清理计划:
这个项目从最初版本到现在已经迭代了3个主要版本,最大的体会是:高校信息化项目必须平衡技术先进性与运维简便性。我们采用的技术栈都是高校IT部门能够自主维护的,这是系统能够长期稳定运行的关键。