☰
前后端分离就业管理系统开发实战:SpringBoot+Vue全栈项目详解
2026/9/28 14:11:08 网站建设 项目流程

1. 项目背景与整体设计思路

1.1 为什么选择前后端分离架构

说实话,你要是还在用传统的JSP+Servlet那套做Web管理系统,可能已经被时代的车轮碾过去了。前后端分离早就不是可选方案,而是当前Web开发的默认姿势。这次我做的就业管理系统,前端用Vue全家桶,后端用SpringBoot,中间通过RESTful API通信,整个项目从零搭建到部署上线,踩了不少坑,也积累了不少经验,今天一次性捋清楚。

所谓前后端分离,核心就一句话:前端只负责渲染页面和交互,后端只负责提供数据和业务逻辑。两者之间通过JSON格式的HTTP请求进行通信,互不干扰。这样做的好处非常明显——前端团队和后端团队可以并行开发,谁都不用等谁;而且同一套后端API可以同时支撑Web端、移动端甚至小程序端,复用性极强。

用生活化的方式理解,前后端分离就像餐厅里的前台服务员和后台厨师。服务员(前端)负责把顾客的话(用户操作)记下来,用标准格式传达给厨房;厨师(后端)只管做好菜(处理业务逻辑),再把菜端到出餐口(API接口),服务员端走送给顾客。两边各干各的,效率自然高。

1.2 就业管理系统到底要解决什么问题

在动手写代码之前,我先把需求捋了一遍。这个就业管理系统面向的角色主要有四类:学生、企业、教师、系统管理员。

学生的核心诉求是:查看招聘信息、投递简历、查看投递状态、管理个人简历。企业的核心诉求是:发布职位、筛选简历、管理招聘流程。教师角色可以理解为辅导员或就业指导老师,需要能查看本班学生的就业情况、统计就业率。系统管理员则负责整个系统的后台管理,比如用户管理、数据字典、权限分配、系统配置等。

所以整个系统的核心就是职位发布与简历投递这条主线,围绕主线再展开企业注册审核、学生简历管理、投递状态流转、就业数据统计这些支线功能。搞清楚业务流程,数据库表和接口设计才能落到实处。

技术选型上,后端我用的是SpringBoot 2.7 + MyBatis 3.5 + MySQL 8.0,前端用Vue 2 + Element UI + Axios。有人可能会问,为什么不用Vue 3和Element Plus?我在实际开发中的体会是,Vue 2的生态更成熟,社区的坑基本都被踩平了,对新手特别友好,而且现在很多老项目依然是Vue 2维护,学一遍Vue 2再过渡到Vue 3并不难。当然如果你是从零开始的新项目,直接用Vue 3也没毛病,就看团队的技术储备了。

2. 项目搭建与核心配置

2.1 后端工程初始化

后端我用的是Spring Initializr创建的工程,这里有几个要点需要注意。

首先,Java版本选的1.8,别看Java 17、21都出来了,企业里跑得最多的还是Java 8,不是不想升级,是历史包袱太重。SpringBoot版本选2.7.x,这是2.x系列的最后一个稳定版本,坑最少。

依赖这块,核心就四个:spring-boot-starter-web(提供Web能力和内嵌Tomcat)、mybatis-spring-boot-starter(MyBatis官方提供的SpringBoot整合包)、mysql-connector-java(MySQL驱动)、lombok(简化实体类代码)。

在application.yml里配置数据源的时候,很多人容易踩坑。我贴一份可用的配置:

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/employment?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 你的数据库密码 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.employment.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

有几个参数我必须专门解释一下。serverTimezone=Asia/Shanghai,这个不配的话,时间字段会差8个小时,因为MySQL驱动默认用的是UTC时区。useSSL=false,本地开发没必要开SSL,开了反而会报警告。allowPublicKeyRetrieval=true,MySQL 8.0的驱动要求这个参数,不然会报Public Key Retrieval is not allowed。

MyBatis的map-underscore-to-camel-case这个配置特别重要,数据库字段是user_name这种下划线风格,Java属性是userName驼峰风格,打开这个开关后,MyBatis会自动帮你做映射,省去一堆ResultMap。

2.2 前端工程初始化

前端我用Vue CLI创建,命令是vue create employment-web。这里有几个选项需要手动选择:Babel、Router、Vuex、Less(或者Sass,看你习惯)。

创建完基础工程后,接下来是装UI组件库和HTTP工具库:

npm install element-ui@2.15.6 npm install axios npm install js-cookie # 用来管理token的存取的

Element UI的引入建议用按需引入的方式,虽然全量引入更方便,但打包体积能差出一倍。我在main.js里这样配:

import Vue from 'vue' import ElementUI from 'element-ui' import 'element-ui/lib/theme-chalk/index.css' import App from './App.vue' import router from './router' import store from './store' Vue.use(ElementUI) Vue.config.productionTip = false new Vue({ router, store, render: h => h(App) }).$mount('#app')

顺便说一下,axios需要封装一个请求拦截器,统一处理token注入和响应错误码。这个结构我要重点讲一下,因为前后端联调时大部分诡异问题都出在这个环节。

前端在src下建立这样几个目录:api(放所有接口调用)、router(路由配置)、store(Vuex状态管理)、views(页面组件)、components(公共组件)、utils(工具函数,比如axios封装就放这里)。目录清晰了,后续开发效率能翻一倍。

3. 数据库设计与核心表结构

3.1 数据库表设计思路

就业管理系统的核心表我设计了这些:user(用户表)、student(学生信息表)、company(企业信息表)、job(职位表)、resume(简历表)、application(投递记录表)、message(站内消息表)。

表设计上我一直坚持一个原则:尽量单表查询,不要动不动就join。MyBatis虽然支持多表关联,但复杂join的SQL维护成本太高,而且性能很难优化。我的做法是冗余字段,比如职位表里直接把企业名称、企业logo地址存进去,省得每次查询都要关联企业表。

下面是用户表的设计,其他表类似,我就不全贴了:

CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录用户名', `password` varchar(100) NOT NULL COMMENT 'MD5加密后的密码', `role` varchar(20) NOT NULL DEFAULT 'student' COMMENT '角色:student/company/teacher/admin', `status` int NOT NULL DEFAULT '1' COMMENT '状态:0禁用 1正常', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

这里有两个细节值得说道:一是字符集必须用utf8mb4而不是utf8,因为utf8在MySQL里最多存3个字节,遇到生僻字和emoji会报错,而utf8mb4完全兼容utf8且支持4字节字符。二是密码我用的MD5加盐存储,虽然现在更推荐BCrypt,但MD5+固定盐在管理系统里也够用了,安全性取决于你的防护意识,不要明文存密码就行。

3.2 职位表与投递记录表的设计要点

职位表(job)的核心字段包括:企业ID、职位名称、职位类型、薪资范围(最低/最高月薪)、工作城市、学历要求、工作经验要求、职位描述、招聘人数、截止日期、状态(上架/下架)、浏览数。

投递记录表(application)的设计需要仔细想一下,这是整个系统最核心的业务表:

CREATE TABLE `application` ( `id` int NOT NULL AUTO_INCREMENT, `student_id` int NOT NULL COMMENT '学生ID', `job_id` int NOT NULL COMMENT '职位ID', `resume_id` int DEFAULT NULL COMMENT '投递的简历ID', `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态:0已投递 1已查看 2面试邀约 3已通过 4已拒绝 5已撤回', `apply_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '投递时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_student_id` (`student_id`), KEY `idx_job_id` (`job_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='投递记录表';

状态流转的逻辑是:学生投递简历成功 → 状态变成已投递;企业查看简历 → 状态变成已查看;企业发出面试邀请 → 状态变成面试邀约;企业点击通过/拒绝 → 最终状态确定;学生可以在企业未处理前撤回投递。

为什么status用tinyint而不是varchar?因为你永远不知道业务方后面会不会改状态名称。用数字存状态,前端通过数据字典映射显示文字,改起来只动配置不动数据库。

3.3 数据初始化的经验之谈

系统开发阶段,我强烈建议准备一套完整的初始化SQL脚本,里面包含管理员账号、测试教师账号、几家企业账号、一批学生账号、若干条职位数据和投递记录。这套脚本的价值在开发期就能体现——你联调接口的时候不用一条条手动造数据,而且可以覆盖各种状态的数据,测试更全面。

初始化SQL脚本我放在项目根目录的sql/employment.sql文件里,需要说明的是,脚本里插了密码时会写死MD5值,不要直接写明文。比如admin/123456的MD5值是e10adc3949ba59abbe56e057f20f883e,我在脚本里就直接存这个值。

4. 后端核心业务模块实现

4.1 登录鉴权与拦截器设计

登录模块看起来简单,但它的设计会影响整个系统的安全性。我的方案是基于Token做登录状态管理,流程是:用户提交用户名密码 → 后端校验通过后生成一个Token(采用UUID) → 把Token存到Redis(实际用的是ConcurrentHashMap模拟)并设置过期时间 → 返回给前端。

前端拿到Token后存在cookie里(我用的js-cookie),每次请求时在拦截器里把Token塞到请求头中。后端写了一个LoginInterceptor拦截器,在WebMvcConfigurer里注册,对所有需要登录的接口进行Token校验。

拦截器核心代码如下:

@Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("token"); if (token == null || token.isEmpty()) { response.setStatus(401); return false; } // 从TokenStore中验证token并把用户信息放入request属性 User user = TokenStore.getUser(token); if (user == null) { response.setStatus(401); return false; } request.setAttribute("userId", user.getId()); request.setAttribute("role", user.getRole()); return true; } }

这里有一个角色权限的细节:不同角色的用户能访问的接口不一样,比如学生端接口和企业端接口必须分开。我定义了三个权限注解@StudentAuth、@CompanyAuth、@AdminAuth,在拦截器里通过角色字段进行判断,这样后端接口的权限控制在代码层面就能看清楚,不会出现漏配的情况。

4.2 职位模块接口实现

职位模块是后端最典型的CRUD场景,但里面的列表查询有点讲究。学生端的职位列表需要支持关键词搜索、类型筛选、城市筛选、排序(按发布时间/薪资),还要做分页。

MyBatis分页我用的PageHelper插件,用法很简单:

public PageResult<JobVO> getJobList(JobQuery query) { PageHelper.startPage(query.getPageNum(), query.getPageSize()); List<JobVO> list = jobMapper.selectJobList(query); PageInfo<JobVO> pageInfo = new PageInfo<>(list); return PageResult.success(pageInfo.getList(), pageInfo.getTotal(), query.getPageNum(), query.getPageSize()); }

在Service层写PageHelper.startPage后,紧接着的Mapper查询会被自动拦截并生成带LIMIT的SQL,非常方便。不过要特别注意一个坑:startPage只会对紧接着的下一条查询生效,如果中间有其他查询语句,分页就会失效。所以PageHelper.startPage的调用必须紧挨着Mapper查询语句。

职位列表的SQL是动态SQL,用MyBatis的 标签拼接查询条件:

<select id="selectJobList" resultType="com.example.employment.vo.JobVO"> SELECT j.*, c.company_name, c.logo FROM job j LEFT JOIN company c ON j.company_id = c.id <where> <if test="keyword != null and keyword != ''"> AND (j.job_name LIKE CONCAT('%', #{keyword}, '%') OR c.company_name LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="jobType != null and jobType != ''"> AND j.job_type = #{jobType} </if> <if test="city != null and city != ''"> AND j.city = #{city} </if> <if test="status != null"> AND j.status = #{status} </if> </where> ORDER BY <choose> <when test="sortBy == 'salary'">j.max_salary DESC</when> <otherwise>j.create_time DESC</otherwise> </choose> </select>

动态SQL是MyBatis的灵魂,一个标签解决了条件拼接的繁琐问题。记住 标签里判断空串必须用!= ''而不是isEmpty()这种写法,因为MyBatis OG NL表达式和Java方法不是一回事。

4.3 简历上传与文件存储

简历模块我做的功能包括:简历信息维护(基本信息、教育经历、工作经历、专业技能)、文件上传(支持PDF、Word格式)、简历预览。在这里我遇到了一个比较典型的问题:文件上传到哪里?

本地磁盘存储是最简单的方案,但生产环境中应用服务器可能有多台,文件存到本地磁盘会导致负载均衡时文件找不到。我当时项目规模不大,就选择了本地存储方案:上传到服务器指定目录(比如/data/employment/resume/),数据库里存文件访问路径。如果以后要做集群部署,可以换成MinIO或阿里云OSS。

文件上传的Controller如下,有个细节是要限制文件大小:

@PostMapping("/upload") public Result<String> upload(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { return Result.error("上传文件不能为空"); } // 检查文件类型,防止上传恶意文件 String originalFilename = file.getOriginalFilename(); String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); if (!Arrays.asList(".pdf", ".doc", ".docx").contains(suffix)) { return Result.error("不支持的文件类型"); } String fileName = UUID.randomUUID().toString().replace("-", "") + suffix; try { file.transferTo(new File(UPLOAD_DIR + fileName)); } catch (IOException e) { e.printStackTrace(); return Result.error("上传失败"); } return Result.success("/files/resume/" + fileName); }

文件重命名这里我用了UUID字符串,这样保证文件名不会重复。但为方便用户下载时知道这是谁的简历,我设计了另一个方案:生成文件名时把用户ID带进去,比如202401_张三_1234567890.pdf,这样文件管理后台从文件名就能看出来是哪份简历。两种方式各有利弊,看你更看重可读性还是随机性。

4.4 就业统计模块的数据查询

数据统计模块是教师端最核心的功能,班主任最关心的就是“我们班有多少人就业了、就业率是多少”。这个模块的SQL写得有些技巧,需要从application表里统计学生投递状态,然后和学生表、班级表关联。

就业率统计的SQL大概是这样:

SELECT COUNT(DISTINCT s.id) AS total_student, COUNT(DISTINCT CASE WHEN a.status = 3 THEN s.id END) AS employed_student, ROUND(COUNT(DISTINCT CASE WHEN a.status = 3 THEN s.id END) / COUNT(DISTINCT s.id) * 100, 2) AS employment_rate FROM student s LEFT JOIN `user` u ON u.id = s.user_id LEFT JOIN application a ON a.student_id = s.id WHERE s.teacher_id = #{teacherId}

这里用了CASE WHEN配合COUNT(DISTINCT),避免了子查询嵌套,性能更好。如果数据量非常大了,再考虑用临时表或定时任务预聚合,但就业管理系统这个量级,单条SQL就能扛住。

数据统计这里我一开始的思路是先查出所有学生,然后在Java代码里逐个统计,结果发现这样做N+1查询问题严重。后来才改成一条SQL搞定。这个教训挺深刻的:能用SQL解决的就别在Java里循环。

5. 前端Vue项目开发实战

5.1 路由设计与登录守卫

前端路由设计我分了三级:首屏是登录页(/login),登录成功后进入主布局(/layout),主布局内部是各个模块页面。

路由守卫是前端鉴权的第一层防线,我用的是Vue Router的全局前置守卫:

router.beforeEach((to, from, next) => { const token = Cookie.get('token') if (to.path === '/login') { next() } else if (!token) { next('/login') } else { // 根据角色判断访问权限 if (to.meta.roles && to.meta.roles.indexOf(store.state.user.role) === -1) { next('/403') } else { next() } } })

这里我遇到过一个问题:刷新页面后Vuex状态会丢失。解决方法是把用户信息同时存到sessionStorage里,刷新后重新加载。或者也可以直接用token去后端重新获取用户信息,两种都行。

5.2 使用Vuex管理用户状态

Vuex里我设计了两个module,一个是user(用户信息、token、角色),一个是app(侧边栏折叠状态、全局loading)。user module的actions部分有个很常见的异步处理问题。

登录时,前端调用登录接口,拿到token存到cookie,然后调用获取用户详情接口拿到用户角色信息存到Vuex。但这个流程里有两个异步请求,必须保证先后顺序,我用了async/await:

async login({ commit }, loginForm) { const res = await loginApi(loginForm) Cookie.set('token', res.data.token) const userRes = await getUserInfo() commit('SET_USER_INFO', userRes.data) return res }

await保证了login接口调用完之后,再调getUserInfo接口。如果忘了await,你会遇到典型的竞态问题——用户信息还没返回,页面已经跳转了。

5.3 职位列表页面的核心组件实现

职位列表页面我用了Element UI的el-table展示职位信息,配合el-pagination做分页。这个页面有几个交互细节值得一说:

搜索区域使用el-form的inline模式,表单项有职位名称、职位类型、城市、薪资范围、发布日期排序,用el-select和el-date-picker配合。需要注意的是,el-date-picker返回的是数组结构(开始日期/结束日期),提交给后端时要转换成两个独立的参数。

职位发布的弹窗表单里,我用el-form自带的rules做校验,比如职位名称必填、薪资范围起薪不能高于终薪、职位描述不能超过200字。这些前端校验能挡掉大部分低级错误,减少后端接口的无效调用。

投递操作做了一个二次确认组件,点击投递按钮后弹出确认提示,防止学生手滑误操作。

5.4 Axios封装与拦截器

Axios封装这块我认为是整个前端工程质量的关键。很多新手项目里,每个页面直接import axios然后调接口,出错了在页面里写一堆catch,这种代码维护起来特别痛苦。

我封装了一个request.js,把baseURL统一配置、token注入、响应码统一处理、错误提示统一弹出做了整合:

import axios from 'axios' import { Message } from 'element-ui' import Cookie from 'js-cookie' const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 15000 }) service.interceptors.request.use(config => { const token = Cookie.get('token') if (token) { config.headers['token'] = token } return config }, error => { return Promise.reject(error) }) service.interceptors.response.use(response => { const res = response.data if (res.code !== 200) { Message.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res }, error => { Message.error(error.message || '网络异常') return Promise.reject(error) }) export default service

然后每个API模块都基于这个封装创建,比如job.js文件里定义所有职位相关的接口方法:

import request from '@/utils/request' export function getJobList(params) { return request({ url: '/api/job/list', method: 'get', params }) } export function applyJob(data) { return request({ url: '/api/application/apply', method: 'post', data }) }

这样在页面里只需要import对应的API方法然后调用,代码非常干净。统一封装的另一个好处是,后端接口地址变了,你只需要改一个文件里的URL。

6. 部署流程与前后端联调

6.1 后端打包与本地运行

后端打包用的是Maven的package命令,打包前要确认application.yml里配置的是生产环境的数据库连接。

mvn clean package -DskipTests

打包完会在target目录下生成employment-server.jar,这个包内置了Tomcat,可以直接运行:

java -jar employment-server.jar

生产环境我建议用一个启动脚本release.sh,里面设置JVM内存参数和日志输出:

#!/bin/bash nohup java -Xms256m -Xmx512m -jar employment-server.jar > /data/employment/logs/app.log 2>&1 & echo "Application started with PID: $!"

Xms和Xmx设置为相同的值可以避免JVM运行时动态扩容内存带来的性能损耗。设置日志输出到指定文件,方便排查问题。

6.2 前端构建与Nginx部署

前端打包命令是npm run build,打包产物在dist目录下。这个dist目录包含的是静态文件(HTML、CSS、JS),需要放到Nginx里托管。

这里有一个非常关键的坑:前端的接口请求地址和静态资源地址必须在同一域名下,否则会引发跨域问题。我的做法是在Nginx里配置反向代理,把/api开头的请求转发到后端的8080端口:

server { listen 80; server_name your-domain.com; location / { root /data/employment/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_connect_timeout 60s; proxy_read_timeout 60s; } }

try_files配置是Vue Router的history模式必须的。如果不加这个配置,你在浏览器里刷新某个子页面路由(比如/admin/job/list),Nginx会去找这个路径对应的物理文件,找不到就返回404。try_files $uri $uri/ /index.html的作用就是:如果找不到请求的路径,就统一返回index.html,让Vue Router接管路由。

6.3 前后端联调时的跨域问题

开发环境下,前端跑在8080端口(npm run serve),后端跑在8081端口,必然存在跨域问题。解决办法有两种:

第一种,前端开发服务器配置代理。在vue.config.js里配置devServer.proxy:

module.exports = { devServer: { port: 8080, proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true } } } }

第二种,后端加CORS全局配置。我当时两种都加了,开发用前端的proxy,生产用Nginx的反向代理,后端CORS配置只作为兜底方案。

跨域这个问题我建议从根源理解:浏览器出于安全考虑,会阻止从一个域名下的页面请求另一个域名的资源。但Nginx反向代理后,浏览器看到的是同域请求,所以不存在跨域问题。这就是为什么生产环境几乎不会遇到跨域报错,而开发环境经常遇到的根因。

6.4 数据库迁移到生产环境

本地开发用的数据库是本地MySQL,生产环境的数据库地址可能不一样。部署时要先在服务器上建好数据库:

mysql -u root -p CREATE DATABASE employment DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE employment; SOURCE /data/employment/sql/employment.sql;

导入SQL脚本的时候,注意脚本里如果有CREATE DATABASE或USE语句,需要先确认目标数据库存在并且为空,避免重复导入导致数据异常。

7. 常见问题与排查技巧

7.1 启动报错汇总

我在开发和测试过程中遇到的各种报错,整理成了一张问题速查表,贴给各位参考:

报错信息原因分析解决方案
Access denied for user 'root'@'localhost'数据库密码错误或用户权限不足检查application.yml里的密码;用Navicat工具测试连接
Unknown database 'employment'数据库不存在执行CREATE DATABASE employment;
Public Key Retrieval is not allowedMySQL 8.0驱动安全限制在JDBC URL加上allowPublicKeyRetrieval=true
java.sql.SQLException: Connection refusedMySQL服务未启动service mysql start
Invalid bound statement (not found)Mapper接口找不到对应的XML检查mapper-locations路径是否正确
Field 'stu_name' doesn't have a default value插入数据时非空字段没赋值检查实体类字段与数据库字段的映射关系

这里我要说一个排查的思路:看到报错先别慌,把异常堆栈信息从下往上读,真正的根因一般在最底层。比如看到Caused by: java.net.ConnectException时,就知道是网络连不上,自然去查MySQL服务有没有启动。

7.2 MyBatis常用问题

MyBatis的项目里经常出现“明明Mapper接口和XML文件都写了,却报Invalid bound statement”的问题。我以前排查这类问题时一般从三个方向查:

第一,检查Mapper接口的package路径和XML文件的namespace是否一致,哪怕差一个字母都不行。 第二,检查application.yml里mybatis.mapper-locations路径是否正确,classpath:mapper/*.xml表示的是resources目录下的mapper文件夹。 第三,检查Maven构建时,resources目录下的XML文件是否被打进jar包。有时候POM文件里的build配置把XML文件排除掉了,导致打包后jar里没有Mapper XML。

关于MyBatis缓存,我在这个项目里用到了二级缓存。一级缓存是SqlSession级别的,二级缓存是Mapper级别的。配置二级缓存很简单,在Mapper XML里加一个cache标签:

<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>

但是要小心,如果表的数据更新频繁,二级缓存可能会查询到旧数据。我做了个折中:只有不经常变更的数据(比如数据字典表、城市列表)开启二级缓存,业务数据表就不开。

MyBatis分页插件PageHelper还有一个坑是:PageHelper.startPage之后如果查询返回的不是List类型,分页会失效。另外,count查询用的是自动生成的SQL,如果你写的SQL里有GROUP BY或者DISTINCT,PageHelper生成的count语句可能会出错,需要手写count查询。

7.3 Vue前端常见问题

掉进过的坑比较多,挑几个最典型的讲讲。

第一个是Element UI按需引入后图标不显示。MyIcons是通过全局注册的,全量引入element-ui时可以看到完整组件,按需引入的时候,如果只引入了Button、Table等组件,一些组件的子组件或者内置图标没注册的话,页面显示的图标就是个空白。在按需引入时,需要在main.js中额外注册ElIcon:

import { Button, Table, TableColumn, Pagination, Icon } from 'element-ui' Vue.use(Icon)

第二个是路由懒加载导致首屏加载慢。我用的是路由懒加载(component: () => import('@/views/job/JobList.vue')),这在首次访问某个路由时,会异步加载对应组件,首屏渲染更快。但懒加载有个副作用:因为组件是异步加载的,在切换路由时会有一段白屏时间。我的优化方案是,核心页面(职位列表、简历详情)提前预加载,后台管理页面保持懒加载。

第三个是el-table的数据刷新问题。修改完某行数据后,表格不能实时更新数据。原因是Vue 2的响应式系统对数组索引的修改无法触发视图更新。我通过重新赋值整个数组解决,或者用this.$set来触发更新:

// 错误方式:直接修改数组项不会触发更新 this.tableData[0].status = 3 // 正确方式:用 $set 方法触发更新 this.$set(this.tableData[0], 'status', 3)

7.4 Nginx部署后的避坑指南

关于Nginx部署,有两点经验值得单独强调。

第一是权限问题,如果Nginx进程运行在nginx用户下,而dist目录属于root用户,静态资源可能无法访问。我遇到过的情况是页面能加载但CSS/JS文件404,排查半天最后发现是权限问题。解决方案是把dist目录的属主改为nginx用户,或者给dist目录设置o+r权限。

第二是端口冲突,服务器上可能已经跑了Nginx或者别的服务占用80端口。部署前先检查端口占用情况:netstat -tlnp | grep 80。如果80端口被占用,可以换其他端口,或者通过server_name区分不同站点。

7.5 前后端联调时的参数格式问题

联调阶段最容易出的问题是前端传的参数格式和后端接收的不一致。比如前端用JSON报文提交数据,后端用@RequestBody接收,这时候如果前端传的日期格式是“2024-03-15T10:30:00”,后端Date类型可能解析不了。

我的处理方案是在全局配置Jackson的日期格式并统一格式:前端在传参前先把日期格式化成yyyy-MM-dd HH:mm:ss,后端在实体类字段上用@JsonFormat注解指定反序列化格式。这样两端约定好,就不会出现参数格式不匹配的问题。

另外一个常见的坑是Long类型精度丢失。后端数据库主键id是BIGINT类型,Java实体类是Long,但前端JavaScript的Number类型能精确表示的整数范围是2^53 - 1(约9007199254740991),超过这个范围会丢失精度。如果你的主键用的是雪花算法生成的长ID,前端在传给后端时可能会被四舍五入。我的解决方案是主键ID用数据库自增,如果必须用雪花ID,就在后端返回时把ID转成字符串(用toString方法)传给前端。

8. 实操心得与扩展思路

8.1 这套源码怎么复用

如果你打算用这套源码做课程设计或者毕业设计,我建议先梳理需求再改代码。不要一上来就换数据库账号改页面,而是画清楚你得业务流程图,再定位对应的后端Controller和前端路由,然后逐步改造。

比如你想加一个“企业评价”功能,核心就是:新增表、新增后端接口、新增前端页面、配置路由。这四个步骤依次做下来,比你拿着源码东改一下西改一下高效得多。

8.2 还能往哪些方向扩展

这个系统可以扩展的方向还挺多的。比如:集成消息推送(钉钉通知企业有新简历)、Excel批量导入学生信息、对接第三方登录(微信扫码登录)、加入WebSocket实现站内即时通讯、引入Redis做会话管理和热点数据缓存、把文件存储切换到MinIO做分布式存储。

我在实际使用中发现,给系统做一个数据大屏面板是投入产出比很高的扩展。教师端首页放一个可视化看板,展示各专业就业率趋势图、企业需求Top10排行榜、未就业学生预警名单,这种功能对教师来说是最实用的。

8.3 最后分享一个小技巧

开发阶段把SQL日志打开(我在application.yml里配了StdOutImpl),能实时看到MyBatis执行的SQL语句和参数值。排查问题时这个日志比任何调试器都好用,能直接看到实际传给数据库的SQL是什么,一眼就能定位是不是参数拼错了。

另一个是Git的使用习惯,每次完成一个功能模块,打一个tag或者在一份commit message里写清楚改动内容。我最近被拉去维护一个别人写的项目,commit message全是“update”“fix”,翻Git日志都不知道改了什么,真的痛苦。好的仓库习惯能省下大量排查时间。

这个项目我在本地跑了大概两周,从前端页面搭建到后端接口开发再到部署上线,全部流程走通后才整理出这篇文章。如果你照着这篇文章做完了一遍,你会发现前后端分离项目并没有想象中那么难,真正的难点在于理解每一层为什么要这么设计。等你把业务逻辑和技术方案打通了,这套架构就能灵活套用到任何管理系统开发里。

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

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

立即咨询