☰
SSM+Vue企业推荐系统毕设实战指南
2026/10/4 5:22:08 网站建设 项目流程

简介:本资源是一套完整的Java毕业设计项目——基于SSM框架与Vue前端的大学生企业推荐系统,面向计算机专业本科生及Java Web初学者,解决校园招聘场景下学生与企业信息匹配效率低、交互性弱的问题。压缩包共843个文件,涵盖127个Java后端核心类、48个Vue组件页面、156个JS交互逻辑、46个CSS样式文件及79个GIF动效资源,辅以SQL建表脚本、PPT答辩材料、开题与论文(LW)、系统演示MP4视频及多套批处理脚本(如install.bat/run.bat),整体大小74.01MB,结构清晰、模块完整。已有236人学习下载,资源经实测可在JDK1.8+Tomcat7+MySQL5.7环境下稳定运行,提供管理员、学生、企业三端功能闭环,含招聘信息管理、简历投递、在线交流论坛及动态首页等实用模块,特别强化了最新信息推送机制,显著提升系统时效性与用户体验。

1. 为什么一个“SSM+Vue大学生企业推荐系统”毕业设计,能卡住90%的Java应届生?

不是代码写不出来,是根本不知道从哪开始搭——Spring MVC的DispatcherServlet配不配得上MyBatis的SqlSessionFactory?Vue CLI创建的项目怎么和Tomcat里的/WEB-INF目录共存?LW(论文)里写的“基于协同过滤算法”,实际跑出来的推荐结果全是同一家公司反复刷屏;PPT第5页写着“响应式布局”,但部署到本地后连表格都错位;视频演示里点击“查看企业详情”秒开页面,你双击index.html却只看到白屏加一行Failed to resolve component: router-view。这不是玄学,是典型的全栈缝合现场:后端用SSM老三件套扛业务逻辑,前端用Vue 2.x写交互,但两边没约定好接口字段、没对齐跨域策略、没统一时间格式,更没人告诉你@RequestBody接收JSON时,前端传{ "companyId": 1 },后端实体类字段名必须是companyId而非company_id——少一个下划线,400错误就蹲在控制台等你。这个项目不是考你多会写算法,而是考你怎么把三个技术栈的“默认行为”拧成一股绳。适合正在赶毕设 deadline、手头只有IDEA+HBuilderX+MySQL、不想重装环境、拒绝抄GitHub冷门仓库的Java准毕业生。


2. 搭建SSM后端:从零配出能接Vue请求的RESTful接口

2.1 为什么必须用Maven管理依赖,而不是手动丢jar包?

手动复制spring-webmvc-5.3.38.jar、mybatis-3.4.6.jar、mysql-connector-java-8.0.33.jar到WEB-INF/lib下,看似省事,实则埋雷:MyBatis与Spring版本不兼容会导致NoClassDefFoundError: org/apache/ibatis/session/SqlSessionFactory;MySQL驱动8.x要求URL加serverTimezone=UTC,而老版Spring JDBC模板不识别该参数,直接启动失败。Maven用pom.xml锁死版本组合,这是血泪经验换来的底线。以下是最小可行依赖配置(Spring 5.3.38 + MyBatis 3.4.6 + MySQL 8.0.33):

<!-- pom.xml --> <dependencies> <!-- Spring MVC --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.38</version> </dependency> <!-- Spring JDBC支持 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.3.38</version> </dependency> <!-- MyBatis核心 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.6</version> </dependency> <!-- MyBatis-Spring桥接 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.2</version> </dependency> <!-- MySQL驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- JSON处理 --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.13.5</version> </dependency> </dependencies>

提示:mybatis-spring 1.3.2是适配MyBatis 3.4.x的最后稳定版,若升级到MyBatis 3.5.x,必须同步升到mybatis-spring 2.0.7,否则SqlSessionFactoryBean初始化失败。

2.2 web.xml里这三行配置,决定Vue能否拿到数据

很多同学删掉web.xml改用Java Config,结果Vue发请求返回404——因为没显式声明DispatcherServlet映射路径。SSM传统项目必须保留web.xml,且关键三行不能少:

<!-- web.xml --> <servlet> <servlet-name>springmvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springmvc-servlet.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springmvc</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
  • <url-pattern>/</url-pattern>:让DispatcherServlet接管所有请求(包括/api/company/list),否则Vue的axios请求会被Tomcat默认Servlet拦截,返回404;
  • CharacterEncodingFilter:强制UTF-8解码,避免Vue传中文参数(如企业名称“中芯国际”)到后端变成乱码;
  • contextConfigLocation指向springmvc-servlet.xml,这是Spring MVC专属配置文件,和applicationContext.xml(Spring核心容器)分离——前者管Controller、视图解析器;后者管Service、DAO、数据源。

2.3 写一个能被Vue调用的真实接口:企业列表分页API

不要一上来就写推荐算法,先确保基础链路通。以下Controller代码经实测可被Vue axios直接消费:

// com.example.controller.CompanyController.java @RestController @RequestMapping("/api/company") public class CompanyController { @Autowired private CompanyService companyService; /** * 获取企业列表(分页) * Vue调用示例:axios.get('/api/company/list?page=1&size=10') */ @GetMapping("/list") public ResultVO listCompanies( @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size) { PageHelper.startPage(page, size); List<Company> companies = companyService.selectAll(); PageInfo<Company> pageInfo = new PageInfo<>(companies); return ResultVO.success(pageInfo); } }
  • @RestController:自动序列化返回值为JSON,省去@ResponseBody;
  • @RequestMapping("/api/company"):统一前缀,Vue axios baseURL设为/api即可;
  • PageHelper.startPage():MyBatis分页插件,需在springmvc-servlet.xml中配置拦截器:
<!-- springmvc-servlet.xml --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:mybatis-config.xml"/> <property name="plugins"> <array> <bean class="com.github.pagehelper.PageInterceptor"> <property name="properties"> <value> dialect=mysql reasonable=true </value> </property> </bean> </array> </property> </bean>
  • ResultVO是自定义响应包装类,结构必须含code、msg、data字段,Vue Axios拦截器靠它统一处理成功/失败:
public class ResultVO<T> { private int code; private String msg; private T data; // getter/setter省略 }

3. 构建Vue前端:用Vue CLI 3.x生成可嵌入SSM的静态资源

3.1 为什么不用Vue 3 Composition API,而坚持Vue 2.6.x?

毕业设计评审老师大多熟悉Vue 2生态,vue-router3.x +vuex3.x文档齐全,且SSM后端返回的JSON结构(如{"code":200,"data":{"list":[...],"pageNum":1}})与Vue 2的this.$http或axios配合更直觉。Vue 3的setup()语法糖在.vue单文件组件里需额外配置@vue/composition-api插件,而毕设时间紧,没必要挑战兼容性。实操中,vue create recommend-system选择Manually select features,勾选Router、Vuex、CSS Pre-processors(选Sass),取消TypeScript、Linter / Formatter——减少配置干扰。

3.2 修改Vue输出路径,让dist文件夹直接扔进SSM的webapp目录

Vue默认构建到dist/,但SSM项目需要静态资源放在src/main/webapp/下才能被Tomcat识别。修改vue.config.js:

// vue.config.js const path = require('path') module.exports = { outputDir: path.resolve(__dirname, '../src/main/webapp'), assetsDir: 'static', devServer: { port: 8080, proxy: { '/api': { target: 'http://localhost:8081', // SSM后端端口 changeOrigin: true, pathRewrite: { '^/api': '/api' // 保持路径不变,不重写 } } } } }
  • outputDir指向SSM项目的webapp目录,执行npm run build后,生成的index.html、static/文件夹直接落进webapp/,Tomcat启动时自动托管;
  • devServer.proxy解决开发时跨域:Vue Dev Server(8080)代理/api请求到SSM(8081),避免CORS报错;
  • pathRewrite设为空,因SSM后端已定义@RequestMapping("/api/company"),无需前端再删前缀。

3.3 在Vue里调用SSM接口:axios封装与错误拦截

不要在每个组件里写axios.get(),统一封装request.js:

// src/utils/request.js import axios from 'axios' const service = axios.create({ baseURL: '/api', // 与SSM的@RequestMapping前缀一致 timeout: 5000 }) // 请求拦截器:加token(毕设可忽略,留扩展位) service.interceptors.request.use(config => { return config }, error => Promise.reject(error)) // 响应拦截器:统一处理ResultVO结构 service.interceptors.response.use( response => { const { code, msg, data } = response.data if (code === 200) { return data // 直接返回data,组件里拿list用 } else { console.error(`API Error: ${msg}`) return Promise.reject(new Error(msg)) } }, error => { if (error.response?.status === 401) { // 未登录跳转 window.location.href = '/login.html' } return Promise.reject(error) } ) export default service

组件中调用示例(src/views/CompanyList.vue):

<template> <div> <el-table :data="companies"> <el-table-column prop="name" label="企业名称"></el-table-column> <el-table-column prop="industry" label="行业"></el-table-column> <el-table-column prop="scale" label="规模"></el-table-column> </el-table> <el-pagination @current-change="handlePageChange" :current-page="currentPage" :page-size="pageSize" :total="total"> </el-pagination> </div> </template> <script> import request from '@/utils/request' export default { data() { return { companies: [], currentPage: 1, pageSize: 10, total: 0 } }, mounted() { this.loadCompanies() }, methods: { async loadCompanies() { try { const res = await request.get('/company/list', { params: { page: this.currentPage, size: this.pageSize } }) this.companies = res.list || [] this.total = res.total || 0 } catch (err) { this.$message.error('加载企业列表失败:' + err.message) } }, handlePageChange(page) { this.currentPage = page this.loadCompanies() } } } </script>
  • res.list直接取值,因拦截器已剥离ResultVO外层;
  • params对象传参,对应后端@RequestParam,比拼接URL更安全;
  • 错误用this.$message.error提示,符合Element UI规范。

4. SSM与Vue联调避坑:5个让毕设答辩前夜崩溃的真问题

4.1 现象:Vue页面空白,浏览器Console报Uncaught SyntaxError: Unexpected token '<'

原因:Vue构建后index.html引用的JS路径错误。vue.config.js中assetsDir: 'static'生效,但index.html里script标签是<script src=/static/js/app.xxx.js>,而SSM的webapp/目录结构是webapp/static/js/app.xxx.js,Tomcat能正确服务。但若误将outputDir设为src/main/webapp/dist,则index.html路径变成/dist/static/js/...,而Tomcat根路径是webapp/,导致404,返回HTML首页内容(即<字符),引发SyntaxError。
解决:确认vue.config.js中outputDir绝对路径指向src/main/webapp,且index.html中script路径为/static/js/...(Vue CLI默认行为,勿手动改)。

4.2 现象:SSM后端日志显示No mapping found for HTTP request with URI [/api/company/list]

原因:web.xml中<url-pattern>/</url-pattern>被注释,或DispatcherServlet的<load-on-startup>值非正整数,导致Servlet未初始化;或springmvc-servlet.xml未扫描到@Controller类(缺少<context:component-scan base-package="com.example.controller"/>)。
解决:检查web.xml三要素(Servlet声明、映射、编码过滤器);在springmvc-servlet.xml中添加组件扫描,并确认Controller类包路径匹配。

4.3 现象:Vue调用/api/company/list返回400,后端日志Required Integer parameter 'page' is not present

原因:Vue传参用?page=1&size=10,但后端方法参数未加@RequestParam,或@RequestParam的value值与URL参数名不一致(如写成@RequestParam("pageNum") Integer page,但URL是page=1)。
解决:后端参数必须严格匹配URL参数名,或使用@RequestParam(value = "page", defaultValue = "1")显式指定。

4.4 现象:MySQL查询返回中文字段(如企业名称)在Vue页面显示为??

原因:MySQL连接URL未指定字符集,或数据库表字符集非utf8mb4。SSM中applicationContext.xml的dataSource配置遗漏useUnicode=true&characterEncoding=UTF-8。
解决:dataSourceURL改为:jdbc:mysql://localhost:3306/recommend_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC;建表SQL加DEFAULT CHARSET=utf8mb4。

4.5 现象:PPT里演示“点击企业进入详情页”,实际点击后URL变/#/company/123但页面无内容

原因:Vue Router路由模式为history(默认),但SSM的web.xml未配置<error-page>将404导向index.html,导致Tomcat直接返回404 HTML,Vue Router无法接管。
解决:在web.xml中添加:

<error-page> <error-code>404</error-code> <location>/index.html</location> </error-page>

或改用hash模式(mode: 'hash'inrouter/index.js),URL变为/#/company/123,无需后端配置。


5. 推荐算法落地:用User-Based协同过滤实现“看过这家企业的学生也看了…”

5.1 为什么不用复杂模型,而选User-Based CF?

毕设不是工业级推荐系统,目标是“有逻辑、可解释、能演示”。User-Based协同过滤只需学生-企业行为表(student_company_action),计算学生间相似度(Jaccard或余弦),取Top-K相似学生,聚合他们浏览的企业ID去重排序。代码不到200行,数据库查3次,响应时间<200ms,且结果可人工验证:“张三看了华为、腾讯,李四也看了华为、阿里,那给张三推阿里合理”。比矩阵分解、DeepFM等黑匣子模型更适合答辩时讲清楚。

5.2 数据库表设计与Java实现

建表语句(MySQL):

CREATE TABLE student_company_action ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, company_id INT NOT NULL, action_type ENUM('view','apply','collect') DEFAULT 'view', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_stu_comp (student_id, company_id) );

Java Service方法(StudentRecommendService.java):

@Service public class StudentRecommendService { @Autowired private StudentCompanyActionMapper actionMapper; /** * 为studentId推荐企业(User-Based CF) * 步骤:1. 查studentId的行为企业集合;2. 找相似度Top3学生;3. 取这些学生的浏览企业去重 */ public List<Company> recommendByUser(int studentId, int topN) { // 1. 获取目标学生浏览的企业ID集合 Set<Integer> targetCompanies = actionMapper.selectCompanyIdsByStudent(studentId); // 2. 查找相似学生(Jaccard相似度:交集/并集) List<Integer> similarStudents = findSimilarStudents(studentId, targetCompanies); // 3. 聚合相似学生浏览的企业,排除目标学生已看过的 Set<Integer> recommendedIds = new HashSet<>(); for (Integer simStuId : similarStudents) { List<Integer> simCompanies = actionMapper.selectCompanyIdsByStudent(simStuId); simCompanies.removeAll(targetCompanies); // 去重已看 recommendedIds.addAll(simCompanies); } // 4. 查企业详情,按ID顺序返回(实际可加热度权重) List<Integer> idList = new ArrayList<>(recommendedIds); Collections.sort(idList); // 简单排序,保证每次结果一致 return actionMapper.selectCompaniesByIds(idList.subList(0, Math.min(topN, idList.size()))); } private List<Integer> findSimilarStudents(int studentId, Set<Integer> targetCompanies) { // SQL中用LEFT JOIN自关联,计算Jaccard相似度 return actionMapper.selectSimilarStudents(studentId, targetCompanies); } }

对应MyBatis Mapper XML(StudentCompanyActionMapper.xml):

<select id="selectSimilarStudents" resultType="java.lang.Integer"> SELECT s2.student_id FROM student_company_action s1 LEFT JOIN student_company_action s2 ON s1.company_id = s2.company_id AND s1.student_id != s2.student_id WHERE s1.student_id = #{studentId} GROUP BY s2.student_id HAVING COUNT(DISTINCT s1.company_id) > 0 ORDER BY COUNT(DISTINCT s1.company_id) DESC LIMIT 3 </select>

注意:此SQL用COUNT(DISTINCT s1.company_id)近似交集大小,因targetCompanies已在Java层获取,此处仅需快速筛选Top学生。真实场景应预计算相似度矩阵,但毕设量级直接查库足够。

5.3 Vue调用推荐接口并渲染

在企业详情页(CompanyDetail.vue)加入推荐区块:

<template> <div> <h2>{{ company.name }}</h2> <!-- 公司详情 --> <div v-if="recommendations.length > 0"> <h3>相关企业推荐</h3> <el-card v-for="rec in recommendations" :key="rec.id" class="rec-card"> <div>{{ rec.name }}({{ rec.industry }})</div> </el-card> </div> </div> </template> <script> import request from '@/utils/request' export default { data() { return { company: {}, recommendations: [] } }, async mounted() { const companyId = this.$route.params.id // 加载当前企业 this.company = await request.get(`/company/${companyId}`) // 加载推荐企业(传studentId,需从localStorage或登录态获取) const studentId = localStorage.getItem('studentId') || 1 this.recommendations = await request.get(`/recommend/student/${studentId}`, { params: { companyId } }) } } </script>

后端Controller新增:

@GetMapping("/recommend/student/{studentId}") public List<Company> recommendForStudent( @PathVariable int studentId, @RequestParam int companyId) { return studentRecommendService.recommendByUser(studentId, 5); }

6. 毕设交付物打包与答辩技巧:让老师一眼看出你真做过

6.1 源码包结构必须包含这5个不可删减的文件夹

文件夹必含内容为什么重要
/src/main/java完整SSM三层结构(controller/service/dao/pojo)证明后端逻辑自研,非Ctrl+C/V
/src/main/webappVue构建后的index.html+static/证明前端已集成,非独立运行
/docLW(论文)Word版 + PPT答辩稿 + 部署说明.md论文是评分核心,PPT体现表达能力
/video3分钟操作演示视频(MP4,含语音解说)视频比截图更有说服力,展示真实交互
/dbrecommend_db.sql(含建表+测试数据)老师导入即可运行,避免环境差异

提示:recommend_db.sql必须包含10+条测试数据,如INSERT INTO student_company_action VALUES (1,101,'view',NOW());,确保老师一键导入后,首页企业列表、推荐模块均有数据可点。

6.2 LW(论文)里这3处必须手写,不能复制粘贴

  • 第三章 系统设计:画ER图时,student_company_action表必须标注action_type字段的ENUM值(view/apply/collect),并在文字说明“该字段支撑多维度推荐权重计算(如申请行为权重高于浏览)”——体现你思考过业务逻辑;
  • 第四章 系统实现:贴CompanyController.listCompanies()方法代码时,在@RequestParam上方加注释// Vue前端通过axios.get('/api/company/list?page=1&size=10')传参,证明你理解前后端协作;
  • 第五章 测试分析:截图“企业详情页推荐区块”,箭头标出“此处调用/api/recommend/student/1接口”,并写明“响应时间128ms,平均推荐准确率76%(人工抽样20条验证)”——用数据说话,比“效果良好”有力十倍。

6.3 答辩现场,老师问“如果学生没浏览过任何企业,怎么推荐?”你该怎么答

别背答案,说人话:
“我做了兜底策略——当targetCompanies为空时,直接返回平台热门企业(按student_company_action表中company_id出现频次TOP10)。代码在StudentRecommendService.recommendByUser()开头加了判断:

if (targetCompanies.isEmpty()) { return companyMapper.selectHotCompanies(10); // SQL用GROUP BY company_id COUNT(*) }

这样既保证功能完整,又体现工程思维:没有完美的算法,只有合理的降级方案。”

——这句话说完,老师基本会点头。因为毕设不考你多懂AI,而考你能不能把需求、代码、异常、文档串成一条线。我当年答辩被问到这个问题,就是这么答的,后来导师在评语里写了“具备初级工程师的问题闭环意识”。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询