毕业季对于高校和学生来说都是一个信息爆炸的时期。每年这个时候,就业指导中心需要处理海量的企业招聘信息、学生简历和面试安排,而学生则要在众多机会中寻找适合自己的岗位。传统的人工管理方式不仅效率低下,还容易出现信息错漏。这就是为什么我们需要一个专门的毕业就业信息管理系统。
这个系统采用前后端分离架构,前端使用Vue.js构建响应式界面,后端基于SpringBoot提供RESTful API,数据存储选用MySQL关系型数据库,通过MyBatis实现对象关系映射。这种技术组合在当前企业级应用开发中非常主流,既保证了系统性能,又便于后期维护扩展。
提示:选择SpringBoot+Vue的技术栈不仅因为它们的流行度,更重要的是它们拥有完善的生态系统和丰富的学习资源,这对于毕业设计项目来说至关重要。
后端选择SpringBoot框架主要考虑以下几点:
前端选择Vue.js的原因包括:
数据库选用MySQL的考量:
整个系统可以分为以下几个核心模块:
用户管理模块
就业信息管理模块
简历管理模块
数据统计模块
消息通知模块
sql复制-- 用户表
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(100) NOT NULL,
`real_name` varchar(50) DEFAULT NULL,
`role` enum('admin','company','student') NOT NULL,
`email` varchar(100) DEFAULT NULL,
`phone` varchar(20) DEFAULT NULL,
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 企业信息表
CREATE TABLE `company` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`company_name` varchar(100) NOT NULL,
`industry` varchar(50) DEFAULT NULL,
`address` varchar(200) DEFAULT NULL,
`description` text,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
CONSTRAINT `company_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 职位表
CREATE TABLE `job` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL,
`title` varchar(100) NOT NULL,
`category` varchar(50) DEFAULT NULL,
`salary_range` varchar(50) DEFAULT NULL,
`location` varchar(100) DEFAULT NULL,
`description` text,
`requirements` text,
`publish_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`status` enum('published','closed') NOT NULL DEFAULT 'published',
PRIMARY KEY (`id`),
KEY `company_id` (`company_id`),
CONSTRAINT `job_ibfk_1` FOREIGN KEY (`company_id`) REFERENCES `company` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 学生简历表
CREATE TABLE `resume` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`education` varchar(100) DEFAULT NULL,
`experience` text,
`skills` text,
`projects` text,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
CONSTRAINT `resume_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 职位申请记录表
CREATE TABLE `application` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`job_id` int(11) NOT NULL,
`resume_id` int(11) NOT NULL,
`status` enum('pending','reviewed','rejected','hired') NOT NULL DEFAULT 'pending',
`apply_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`feedback` text,
PRIMARY KEY (`id`),
KEY `job_id` (`job_id`),
KEY `resume_id` (`resume_id`),
CONSTRAINT `application_ibfk_1` FOREIGN KEY (`job_id`) REFERENCES `job` (`id`),
CONSTRAINT `application_ibfk_2` FOREIGN KEY (`resume_id`) REFERENCES `resume` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
xml复制<!-- JobMapper.xml -->
<mapper namespace="com.example.employment.mapper.JobMapper">
<resultMap id="BaseResultMap" type="com.example.employment.entity.Job">
<id column="id" property="id" jdbcType="INTEGER"/>
<result column="company_id" property="companyId" jdbcType="INTEGER"/>
<result column="title" property="title" jdbcType="VARCHAR"/>
<result column="category" property="category" jdbcType="VARCHAR"/>
<result column="salary_range" property="salaryRange" jdbcType="VARCHAR"/>
<result column="location" property="location" jdbcType="VARCHAR"/>
<result column="description" property="description" jdbcType="LONGVARCHAR"/>
<result column="requirements" property="requirements" jdbcType="LONGVARCHAR"/>
<result column="publish_time" property="publishTime" jdbcType="TIMESTAMP"/>
<result column="status" property="status" jdbcType="VARCHAR"/>
</resultMap>
<select id="selectByCompanyId" resultMap="BaseResultMap">
SELECT * FROM job WHERE company_id = #{companyId}
</select>
<insert id="insert" parameterType="com.example.employment.entity.Job" useGeneratedKeys="true" keyProperty="id">
INSERT INTO job (company_id, title, category, salary_range, location, description, requirements, status)
VALUES (#{companyId}, #{title}, #{category}, #{salaryRange}, #{location}, #{description}, #{requirements}, #{status})
</insert>
</mapper>
java复制// 主应用类
@SpringBootApplication
@MapperScan("com.example.employment.mapper")
public class EmploymentApplication {
public static void main(String[] args) {
SpringApplication.run(EmploymentApplication.class, args);
}
}
// 安全配置类
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.antMatchers("/api/company/**").hasRole("COMPANY")
.antMatchers("/api/student/**").hasRole("STUDENT")
.anyRequest().authenticated()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
}
java复制// 职位控制器
@RestController
@RequestMapping("/api/jobs")
public class JobController {
@Autowired
private JobService jobService;
@GetMapping
public ResponseEntity<List<JobDTO>> getAllJobs(
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String category,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
Pageable pageable = PageRequest.of(page, size);
Page<JobDTO> jobs = jobService.findJobs(keyword, category, pageable);
return ResponseEntity.ok()
.header("X-Total-Count", String.valueOf(jobs.getTotalElements()))
.body(jobs.getContent());
}
@GetMapping("/{id}")
public ResponseEntity<JobDetailDTO> getJobById(@PathVariable Long id) {
JobDetailDTO job = jobService.getJobDetail(id);
return ResponseEntity.ok(job);
}
@PostMapping
@PreAuthorize("hasRole('COMPANY')")
public ResponseEntity<JobDTO> createJob(@RequestBody JobCreateRequest request,
@CurrentUser UserPrincipal currentUser) {
JobDTO createdJob = jobService.createJob(request, currentUser.getId());
return ResponseEntity.status(HttpStatus.CREATED).body(createdJob);
}
@PutMapping("/{id}")
@PreAuthorize("hasRole('COMPANY')")
public ResponseEntity<JobDTO> updateJob(@PathVariable Long id,
@RequestBody JobUpdateRequest request,
@CurrentUser UserPrincipal currentUser) {
JobDTO updatedJob = jobService.updateJob(id, request, currentUser.getId());
return ResponseEntity.ok(updatedJob);
}
}
java复制@Service
@RequiredArgsConstructor
public class JobServiceImpl implements JobService {
private final JobMapper jobMapper;
private final CompanyMapper companyMapper;
private final ModelMapper modelMapper;
@Override
public Page<JobDTO> findJobs(String keyword, String category, Pageable pageable) {
Page<Job> jobs = jobMapper.findByKeywordAndCategory(
keyword, category, pageable);
return jobs.map(job -> {
JobDTO dto = modelMapper.map(job, JobDTO.class);
Company company = companyMapper.selectByPrimaryKey(job.getCompanyId());
dto.setCompanyName(company.getCompanyName());
return dto;
});
}
@Override
public JobDetailDTO getJobDetail(Long id) {
Job job = jobMapper.selectByPrimaryKey(id);
if (job == null) {
throw new ResourceNotFoundException("Job not found with id: " + id);
}
Company company = companyMapper.selectByPrimaryKey(job.getCompanyId());
JobDetailDTO detailDTO = modelMapper.map(job, JobDetailDTO.class);
detailDTO.setCompanyInfo(modelMapper.map(company, CompanyInfoDTO.class));
return detailDTO;
}
@Override
@Transactional
public JobDTO createJob(JobCreateRequest request, Long userId) {
Company company = companyMapper.findByUserId(userId)
.orElseThrow(() -> new BusinessException("Company profile not found"));
Job job = modelMapper.map(request, Job.class);
job.setCompanyId(company.getId());
job.setStatus("published");
job.setPublishTime(new Date());
jobMapper.insert(job);
return modelMapper.map(job, JobDTO.class);
}
}
code复制src/
├── api/ # API请求封装
│ ├── auth.js # 认证相关API
│ ├── jobs.js # 职位相关API
│ └── ...
├── assets/ # 静态资源
├── components/ # 公共组件
│ ├── JobCard.vue # 职位卡片组件
│ ├── Pagination.vue # 分页组件
│ └── ...
├── router/ # 路由配置
├── store/ # Vuex状态管理
│ ├── modules/ # 模块化store
│ └── index.js # 主store文件
├── utils/ # 工具函数
├── views/ # 页面组件
│ ├── auth/ # 认证相关页面
│ ├── jobs/ # 职位相关页面
│ └── ...
└── App.vue # 根组件
vue复制<template>
<div class="job-list-container">
<div class="search-filters">
<el-input
v-model="searchQuery.keyword"
placeholder="搜索职位..."
class="search-input"
@keyup.enter="fetchJobs"
>
<el-button slot="append" icon="el-icon-search" @click="fetchJobs" />
</el-input>
<el-select
v-model="searchQuery.category"
placeholder="选择职位类别"
clearable
@change="fetchJobs"
>
<el-option
v-for="category in jobCategories"
:key="category.value"
:label="category.label"
:value="category.value"
/>
</el-select>
</div>
<div class="job-list">
<job-card
v-for="job in jobs"
:key="job.id"
:job="job"
@click.native="viewJobDetail(job.id)"
/>
<el-pagination
:current-page="pagination.currentPage"
:page-size="pagination.pageSize"
:total="pagination.total"
layout="total, prev, pager, next, jumper"
@current-change="handlePageChange"
/>
</div>
</div>
</template>
<script>
import JobCard from '@/components/JobCard.vue'
import { fetchJobs } from '@/api/jobs'
export default {
name: 'JobList',
components: { JobCard },
data() {
return {
jobs: [],
jobCategories: [
{ value: 'technology', label: '技术类' },
{ value: 'product', label: '产品类' },
{ value: 'design', label: '设计类' },
// 其他类别...
],
searchQuery: {
keyword: '',
category: ''
},
pagination: {
currentPage: 1,
pageSize: 10,
total: 0
}
}
},
created() {
this.fetchJobs()
},
methods: {
async fetchJobs() {
try {
const params = {
page: this.pagination.currentPage - 1,
size: this.pagination.pageSize,
...this.searchQuery
}
const response = await fetchJobs(params)
this.jobs = response.data.content
this.pagination.total = parseInt(response.headers['x-total-count']) || 0
} catch (error) {
console.error('获取职位列表失败:', error)
this.$message.error('获取职位列表失败')
}
},
handlePageChange(page) {
this.pagination.currentPage = page
this.fetchJobs()
},
viewJobDetail(jobId) {
this.$router.push(`/jobs/${jobId}`)
}
}
}
</script>
<style scoped>
.job-list-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.search-filters {
display: flex;
gap: 15px;
margin-bottom: 20px;
}
.search-input {
width: 300px;
}
.job-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.el-pagination {
margin-top: 20px;
justify-content: center;
}
</style>
javascript复制// store/modules/job.js
const state = {
jobs: [],
currentJob: null,
pagination: {
page: 0,
size: 10,
total: 0
},
searchQuery: {
keyword: '',
category: ''
}
}
const mutations = {
SET_JOBS(state, payload) {
state.jobs = payload.jobs
state.pagination.total = payload.total
},
SET_CURRENT_JOB(state, job) {
state.currentJob = job
},
UPDATE_SEARCH_QUERY(state, query) {
state.searchQuery = { ...state.searchQuery, ...query }
},
UPDATE_PAGINATION(state, pagination) {
state.pagination = { ...state.pagination, ...pagination }
}
}
const actions = {
async fetchJobs({ commit, state }) {
try {
const params = {
page: state.pagination.page,
size: state.pagination.size,
...state.searchQuery
}
const response = await jobApi.fetchJobs(params)
commit('SET_JOBS', {
jobs: response.data.content,
total: parseInt(response.headers['x-total-count']) || 0
})
} catch (error) {
console.error('获取职位列表失败:', error)
throw error
}
},
async fetchJobDetail({ commit }, jobId) {
try {
const response = await jobApi.fetchJobDetail(jobId)
commit('SET_CURRENT_JOB', response.data)
} catch (error) {
console.error('获取职位详情失败:', error)
throw error
}
}
}
const getters = {
filteredJobs: state => {
return state.jobs.filter(job => {
const matchesKeyword = job.title.includes(state.searchQuery.keyword) ||
job.description.includes(state.searchQuery.keyword)
const matchesCategory = !state.searchQuery.category ||
job.category === state.searchQuery.category
return matchesKeyword && matchesCategory
})
}
}
export default {
namespaced: true,
state,
mutations,
actions,
getters
}
bash复制# 使用Maven打包
mvn clean package -DskipTests
# 生成的jar文件位于target目录下
# employment-system-0.0.1-SNAPSHOT.jar
dockerfile复制# 使用OpenJDK 11作为基础镜像
FROM openjdk:11-jre-slim
# 设置工作目录
WORKDIR /app
# 复制打包好的jar文件
COPY target/employment-system-0.0.1-SNAPSHOT.jar app.jar
# 暴露端口
EXPOSE 8080
# 启动命令
ENTRYPOINT ["java", "-jar", "app.jar"]
yaml复制spring:
datasource:
url: jdbc:mysql://mysql-server:3306/employment_db?useSSL=false&serverTimezone=UTC
username: employment_user
password: strong_password
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: validate
show-sql: true
server:
port: 8080
bash复制npm run build
# 生成的静态文件位于dist目录
nginx复制server {
listen 80;
server_name employment.example.com;
root /var/www/employment-system/dist;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, no-transform";
}
}
yaml复制version: '3.8'
services:
backend:
build: ./backend
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
depends_on:
- mysql
networks:
- employment-network
frontend:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./frontend/dist:/usr/share/nginx/html
- ./frontend/nginx.conf:/etc/nginx/conf.d/default.conf
networks:
- employment-network
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root_password
MYSQL_DATABASE: employment_db
MYSQL_USER: employment_user
MYSQL_PASSWORD: strong_password
volumes:
- mysql-data:/var/lib/mysql
ports:
- "3306:3306"
networks:
- employment-network
volumes:
mysql-data:
networks:
employment-network:
driver: bridge
在开发阶段,前后端分离架构经常会遇到跨域问题。以下是几种解决方案:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.exposedHeaders("Authorization")
.allowCredentials(false)
.maxAge(3600);
}
}
javascript复制module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
}
}
数据库优化
后端优化
前端优化
MyBatis查询结果为空
Vue页面数据不更新
SpringBoot启动失败
跨域问题即使配置后仍然存在
注意:在开发过程中,保持前后端开发人员对API接口的及时沟通非常重要。可以使用Swagger或YApi等工具维护API文档,确保前后端对接口的理解一致。