十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

就业信息管理系统开发实战:SpringBoot+Vue+MySQL全栈部署

就业信息管理系统开发实战:SpringBoot+Vue+MySQL全栈部署 1. 从零梳理需求就业信息管理系统到底要做哪些事在开始列技术栈、写代码之前把需求彻底理清楚是这类信息管理系统项目最容易被忽略的一步。很多同学拿到“毕业就业信息管理系统”这个题目就急着去建表结果写着写着发现角色不对、字段对不上、页面之间互相矛盾返工成本反而更高。我自己在做这个项目的第一周也经历了“推翻重来”的过程所以这一节先把需求框架讲透后面写代码才有方向。这个系统的核心目标是把学校就业部门、学生、招聘企业、招聘岗位、就业去向这几条线串起来让信息从“学生填写”到“企业查看”再到“管理员统计”形成一个闭环。它本质上不是一个炫技的项目而是一个典型的信息流管理平台因此重点应该放在角色权限边界、数据流转路径、核心业务状态管理这三件事上。从用户视角来看系统分三类角色。学生要能注册登录、完善个人简历、浏览招聘岗位、投递简历、查看就业公告同时还要能填写自己的毕业去向比如签约公司、职位、薪资范围、就业城市。企业端要能发布职位、管理在招岗位、查看投递过来的学生简历、标记简历处理状态。管理员端则负责审核企业资质、审核职位信息、管理公告、查看整体就业数据部分毕设项目还需要支持导出Excel统计表。功能模块拆下来大概是这样的模块学生端企业端管理员端账号体系注册、登录、修改密码注册、登录、资质信息维护管理员账号、用户禁用/启用招聘信息浏览岗位、按条件筛选、投递简历职位发布、编辑、上下架职位审核、违规职位下架简历与投递维护个人信息、查看投递记录查看收到的简历、更新处理状态数据总览按学院/专业统计就业管理填写就业去向、查看记录本企业已录用学生名单就业率统计、导出报表公告通知查看公告详情查看公告详情发布公告、删除公告这个需求表建议你在动手前就画出来因为它决定了整个数据库的表结构。比如“投递记录”这个状态学生端要看到“已投递、被查看、已邀约、已录用、未通过”企业端要能更新这些状态管理员端可能只需要统计录用人数那么这张表的状态字段设计就要统一不能一个是数字一个是字符串。还有“企业审核”这件事容易被漏掉。如果企业注册后直接能发职位系统里会出现大量垃圾招聘信息所以建议在企业注册后加一个“待审核、已通过、已拒绝”的审核状态管理员审核通过后企业才能发布职位。业务流程上核心链路是“企业发职位 - 学生浏览投递 - 企业查看简历并更新状态 - 学生确认就业去向”。我建议把这条链路单独画一张时序图放在项目文档里写代码时按这条链路去倒推每个接口需要哪些参数、返回什么数据。另一个容易被忽略的流程是“就业信息上报”。有些学校要求毕业生离校前填写就业去向那学生端就应该有一个表单页面包含公司名称、职位、薪资、城市、是否已签三方协议等字段提交后管理员端能看到汇总数据。这个功能看起来简单但涉及字段校验和重复提交的问题后面数据库设计时会具体说。需求阶段的最后一步是确定系统的非功能需求比如密码不能明文存储、接口要校验登录状态、列表要分页、文件上传要限制类型。这些不需要做成多么复杂的安全方案但要通过代码体现出来这在答辩或文档评审时是一个加分项。2. 技术选型不等于堆框架为什么是SpringBootVueMyBatisMySQL技术选型是这个项目里面看似最简单、实际上最容易纠结的一个环节。标题里给出的SpringBootVueMyBatisMySQL是当前前后端分离项目非常成熟的一套组合但如果你只是“因为它流行”所以选了后面遇到问题时容易不知道本质原因。我以自己的选择过程为例把这套技术栈背后真正的理由说清楚。先看后端。SpringBoot的核心价值在“约定大于配置”它把Spring MVC、事务管理、Jackson序列化、嵌入式Tomcat这些基础能力整合好了我们只需要在pom.xml里引入依赖写配置类和业务代码不用再像传统SSM项目那样堆一堆XML配置。对于这个系统后端需要的能力无非是REST接口、数据库访问、登录鉴权、文件上传、定时任务这些SpringBoot都能比较优雅地支持。MyBatis作为持久层框架胜在SQL可控。毕业就业系统里有大量多表关联统计比如“按学院统计就业率”、“按企业统计招聘人数”这种统计类SQL直接用MyBatis写SQL更直观比全自动ORM在调优和理解上都要容易可控一些。前端选择Vue核心原因是组件化和生态。Vue的组件化开发方式非常适合把“登录页、职位列表、简历表单、后台管理布局”拆成独立组件多人协作或者自己维护都清晰。Vue Router负责前端路由Vuex或Pinia负责全局状态Axios负责HTTP请求这套组合在前后端分离场景下是标准方案。需要注意Vue 2和Vue 3的差异。如果只是做毕设或学习Vue 2的生态资料更多Element UI组件库也很成熟但如果是新项目我更推荐Vue 3 Vite Element Plus性能和长期维护性更好。本项目的源码最终用的就是Vue 3。MySQL作为数据库不用多解释免费、稳定、资料多毕业设计答辩时老师也熟悉。但有几个细节选型时要想清楚MySQL版本建议用8.0以上因为8.0的字符集默认是utf8mb4支持emoji认证插件是caching_sha2_password连接时JDBC驱动的配置和5.7不同数据库引擎用InnoDB支持事务和外键校园数据量级完全够用。这套技术栈的组合优势在于“每一层都有清晰的替换空间”。比如你现在用MyBatis后面想换MyBatis-Plus改造并不大前端Vue 3不好用可以降级到Vue 2MySQL想换成PostgreSQL后端只要改方言和部分SQL。对学习者来说这种可替换性意味着你掌握的是通用的前后端分离架构能力而不是绑定某个特定工具。我在实际做的时候也验证过换组件库从Element UI到Element Plus大概只改了一天的组件代码。版本约定这里我列一个自己验证过的组合避免大家花时间在版本冲突上组件版本说明JDK1.8或11推荐1.8兼容性最好11也稳定SpringBoot2.7.x不要选3.x3.x要求JDK17部分资料不适用MyBatis3.5.x配合mybatis-spring-boot-starter 2.3.xMySQL8.0.xJDBC驱动用mysql-connector-j 8.0.xVue3.4.x搭配Vite 5.xElement Plus2.xUI组件库Node.js18.x或20.x前端构建环境Maven3.8.x后端构建工具这套组合在Windows、macOS、Linux都跑过只要跟着这个版本表走基本上不会遇到“启动就报错”的尴尬。3. 数据库先行核心表设计与字段取舍我一直认为信息管理系统最重要的不是代码写得花哨而是数据库表设计是否合理。表结构一旦定了业务逻辑和前端页面基本就围绕表结构展开。这个项目我最终设计了9张核心表这里挑最关键的几张说清楚设计思路和字段取舍。第一类是用户与角色表。我设计了一张sys_user表统一存放登录账号字段包括id、username、password、role_type、status、create_time。role_type用整数标识1学生、2企业、3管理员。为什么不设计三张独立的登录表因为登录流程是统一的账号密码校验逻辑只需要写一次登录后的角色权限通过role_type区分。密码字段存的是BCrypt加密后的密文不是明文。status字段用来做禁用账号管理员把某企业账号禁用后该账号无法登录系统。第二类是学生和企业信息表。学生信息表student_profile和sys_user一对一关联字段包括real_name、student_no、college、major、grade、phone、resume_url。企业信息表company则包括company_name、credit_code、industry、scale、address、license_url、status。这里要注意登录需要的账号密码和昵称放在sys_user业务相关的详细资料放在各自的档案表这样避免把用户表塞得太满。第三类是核心业务表职位表和投递表。职位表job_position的字段设计是这个系统的关键点之一CREATE TABLE job_position ( id int NOT NULL AUTO_INCREMENT, company_id int NOT NULL COMMENT 关联企业表, title varchar(100) NOT NULL COMMENT 职位名称, category varchar(50) DEFAULT NULL COMMENT 职位类别, salary_min int DEFAULT NULL COMMENT 最低薪资单位K, salary_max int DEFAULT NULL COMMENT 最高薪资单位K, city varchar(50) DEFAULT NULL COMMENT 工作城市, education varchar(20) DEFAULT NULL COMMENT 学历要求, experience varchar(20) DEFAULT NULL COMMENT 经验要求, description text COMMENT 职位描述, status tinyint DEFAULT 1 COMMENT 0下架 1招聘中 2待审核, view_count int DEFAULT 0 COMMENT 浏览次数, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT招聘岗位表;薪资字段我建议拆成salary_min和salary_max两个整数而不是直接存一个字符串“8K-12K”。原因很简单如果要按薪资范围筛选岗位字符串字段没法高效查询拆成两个整数后直接用BETWEEN查询即可。学历要求、经验要求这类字段用字符串类型来存比如“本科”、“3-5年”因为它们的取值有限且固定不需要过度设计成字典表。投递表job_application是状态流转的核心CREATE TABLE job_application ( id int NOT NULL AUTO_INCREMENT, student_id int NOT NULL COMMENT 关联学生档案表, job_id int NOT NULL COMMENT 关联岗位表, status tinyint DEFAULT 0 COMMENT 0已投递 1被查看 2邀约 3录用 4未通过, resume_url varchar(255) DEFAULT NULL COMMENT 投递时的简历地址, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_student_job (student_id, job_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT投递记录表;这里有一个细节投递时会把学生当前的resume_url冗余一份到投递记录表里。原因是学生后续可能更新简历如果只关联学生表历史投递记录里的简历地址可能已经变了企业查看时的数据就失真了。冗余字段这个操作在业务上叫“快照”在做简历、订单这类有时效性的业务时非常常见。就业信息表employment_info记录毕业去向字段包括student_id、company_name、position、salary、city、is_signed是否签三方协议、entry_date、create_time。这张表是管理员统计就业率的数据来源所以尽量让字段独立清晰方便聚合查询。公告表、企业审核记录表我就不贴完整SQL了设计逻辑比较直白。建表时还有一个容易犯的错误所有时间字段类型统一用datetime不要一半用timestamp一半用datetime所有主键统一用int AUTO_INCREMENT或bigint不要一会儿用id一会儿用uid所有关联字段记得加索引尤其是投递表的student_id和job_id。这个系统数据量不大索引不用太多但联合索引idx_student_job在查询“某个学生的全部投递记录”、“某个职位的收到简历列表”时会有明显帮助。我在设计数据库时还做了另一个建表脚本用于初始化管理员账号。这个脚本在部署阶段会用到INSERT INTO sys_user (username, password, role_type, status, create_time) VALUES (admin, $2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2, 3, 1, NOW());这串$2a$10$开头的是BCrypt加密后的“admin123”。因为Spring Security内置的BCryptPasswordEncoder每次加密同一个密码得到的密文都不同所以直接把加密后的结果写死在SQL里比写一个明文密码然后犹豫要不要在配置里解密更省事。4. 后端实战从Maven工程到核心接口落地数据库设计完成后进入后端编码阶段。这一节我会按实际开发顺序把工程结构、关键配置和几个核心接口的写法走一遍并解释每一步为什么这么做。4.1 项目结构与依赖配置后端工程用Spring Initializr创建包名建议用com.example.employment分模块如下com.example.employment ├── EmploymentApplication.java ├── config │ ├── CorsConfig.java │ ├── WebMvcConfig.java │ └── MybatisPlusConfig.java ├── controller │ ├── AuthController.java │ ├── JobController.java │ ├── ApplicationController.java │ ├── StudentController.java │ └── AdminController.java ├── service │ ├── UserService.java │ ├── JobService.java │ └── ApplicationService.java ├── mapper │ ├── UserMapper.java │ ├── JobMapper.java │ └── ApplicationMapper.java ├── entity │ ├── SysUser.java │ ├── JobPosition.java │ └── JobApplication.java ├── dto │ ├── LoginRequest.java │ ├── JobQueryDTO.java │ └── LoginResponse.java ├── common │ ├── Result.java │ ├── GlobalExceptionHandler.java │ └── JwtUtil.javapom.xml里最核心的依赖就这么几个dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version /dependencyMyBatis不一定要用MyBatis-Plus。这个项目我故意只用原生MyBatis就是希望把SQL写在XML里能看到每一条查询语句。如果你后续想偷懒引入MyBatis-Plus后直接用BaseMapper就能少写大量CRUD但学习阶段先掌握原生SQL更重要。application.yml里有两个地方容易写错我单独说明一下。spring: datasource: url: jdbc:mysql://localhost:3306/employment_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver 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.StdOutImplserverTimezoneAsia/Shanghai必须写否则MySQL 8.0连接时会报时区错误allowPublicKeyRetrievaltrue是配合MySQL 8.0的caching_sha2_password认证插件需要的初期没加的时候本地跑得好好的部署到云上后突然连不上多半就是这个参数的问题。map-underscore-to-camel-case开启后数据库的company_name字段能自动映射到实体的companyName属性省去手写resultMap的麻烦。log-impl配置成StdOutImpl开发阶段控制台会打印SQL语句排查MyBatis问题必备。4.2 统一响应与全局异常处理前后端分离项目接口返回值需要一个统一格式。我的Result类长这样Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }所有Controller的返回值都用Result包装前端拿到的JSON就是这样{ code: 200, message: success, data: { id: 1, title: Java开发工程师 } }GlobalExceptionHandler用RestControllerAdvice捕获业务异常和参数校验异常统一转成Result.error返回。这样做的意义在于前端axios拦截器里只用判断code是否为200其余都走统一的错误提示逻辑代码就变得非常干净。注意业务异常要自己定义不要用它来包未知的Exception否则调试时所有错误都变成了“系统异常”根本定位不到问题。4.3 JWT登录鉴权的实现思路登录鉴权是前后端分离项目里的核心问题。传统Session方案在跨域场景下要处理Cookie的SameSite、跨域携带凭证等一堆问题所以我直接用JWT做无状态登录。流程是用户登录成功后后端生成一个包含用户ID、用户名、角色类型的JWT字符串返回给前端前端把它存在localStorage里每次请求时放在Authorization请求头后端用一个拦截器校验Token并解析出当前用户信息。JWT生成工具我用的是io.jsonwebtoken核心代码不长public String generateToken(Integer userId, String username, Integer roleType) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(username, username) .claim(roleType, roleType) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 7 * 24 * 60 * 60 * 1000L)) .signWith(getSigningKey(), SignatureAlgorithm.HS256) .compact(); }Token有效期我设置成7天。实际项目里有人用2小时但毕设和内部系统7天更省事不至于让学生填个简历填一半登录过期。拦截器我这里用的是Spring MVC的HandlerInterceptor而不是Spring Security的过滤器链原因是为了降低学习曲线。如果你已经会Spring Security直接用当然更好如果只是想先把系统跑通手写拦截器足够。拦截器核心思路是放行登录、注册、公告查询这些不需要鉴权的接口其余接口校验请求头里的Token解析成功后把用户ID放到ThreadLocal或RequestContextHolder里Controller里直接用。遇到Token过期或非法返回401状态码前端路由守卫检测到401后跳回登录页。4.4 职位管理接口与分页查询职位列表是学生端访问频率最高的接口查询条件多写法最能体现MyBatis的功底。我先定义查询DTOData public class JobQueryDTO { private Integer pageNum 1; private Integer pageSize 10; private String keyword; private String city; private String category; private Integer salaryMin; private Integer education; private Integer status 1; // 默认只查招聘中 }Mapper接口public interface JobMapper { ListJobPosition selectJobList(Param(dto) JobQueryDTO dto); Long selectJobCount(Param(dto) JobQueryDTO dto); }XML里这么写select idselectJobList resultTypecom.example.employment.entity.JobPosition SELECT * FROM job_position where if testdto.status ! null AND status #{dto.status} /if if testdto.keyword ! null and dto.keyword ! AND (title LIKE CONCAT(%, #{dto.keyword}, %) OR description LIKE CONCAT(%, #{dto.keyword}, %)) /if if testdto.city ! null and dto.city ! AND city #{dto.city} /if if testdto.salaryMin ! null AND salary_max gt; #{dto.salaryMin} /if /where ORDER BY create_time DESC LIMIT #{dto.pageSize} OFFSET #{dto.pageNum} /select查询逻辑里我用LIMIT #{dto.pageSize} OFFSET #{dto.pageNum}做分页没有引入PageHelper插件。这个量级的系统手写分页完全够用少一个依赖就少一个配置坑。注意where条件里用where标签而不是直接写WHERE 11where会自动去掉多余的AND代码更规范。分页返回体我也封装了一个PageResult包含total、list、pageNum、pageSize四个字段。前端Element Plus的el-pagination组件直接接收这个结构就能正常渲染省去前端做数据转换。接口层面我推荐把所有业务接口按“读取”和“写入”分开命名像/api/job/list是查询/api/application/submit是投递。这样一个接口拉一个表职责清晰后面做权限控制也能直接按前缀处理。5. 前端实战Vue工程搭建与前后端联调细节前端是绝大多数第一次做前后端分离项目的同学最容易卡住的地方因为前端工程化依赖Node生态需要注意的细节比后端还多。我把从创建Vue工程到前后端联调跑通的关键点完整过一遍。5.1 Vue工程初始化与代理配置前端工程我用Vite创建命令很简单npm create vitelatest employment-web -- --template vue cd employment-web npm install npm install vue-router4 pinia axios element-plus npm run dev这里有个常见坑npm install在Windows上如果网络不好会出现ERESOLVE unable to resolve dependency tree的报错一般用npm install --legacy-peer-deps就能绕过。另外Node版本太低会直接报Vite requires Node.js version 18所以开发前先确认node -v的结果。vite.config.js里配置开发环境代理这是解决联调阶段跨域问题的核心import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { host: 0.0.0.0, port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })配置之后前端代码里请求/api/job/list时Vite开发服务器会把它转发到http://localhost:8080/api/job/list浏览器里不存在跨域问题。上线之后代理就交给Nginx处理这部分放到部署章节。5.2 Axios封装与路由守卫Axios实例一定要封装一层。我新建src/utils/request.js设置baseURL为/api然后加请求拦截器和响应拦截器。请求拦截器负责在每次请求前把localStorage里的Token添加到请求头import axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(error.response?.data?.message || 网络异常) return Promise.reject(error) } ) export default request路由守卫放在src/router/index.js里判断用户未登录时不能进入需要鉴权的页面router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path /login || to.path /register) { next() } else if (!token) { next(/login) } else { next() } })这层守卫虽然不能做真正的权限控制但用户体验上能保证未登录用户进不了系统页面。真正的接口权限还是依赖后端的角色校验不要依赖前端路由。5.3 列表页与表单页的核心写法职位列表页我用了Element Plus的el-table加el-pagination模板结构是这样的template div classjob-list el-form :inlinetrue :modelqueryForm el-form-item label关键词 el-input v-modelqueryForm.keyword placeholder职位/岗位 / /el-form-item el-form-item label城市 el-input v-modelqueryForm.city placeholder工作城市 / /el-form-item el-form-item el-button typeprimary clickloadJobs查询/el-button /el-form-item /el-form el-table :datajobList stripe el-table-column proptitle label职位名称 / el-table-column propcompanyName label企业名称 / el-table-column propcity label城市 width100 / el-table-column label薪资范围 width120 template #default{ row } {{ row.salaryMin }}K - {{ row.salaryMax }}K /template /el-table-column el-table-column label操作 width150 template #default{ row } el-button typeprimary link clickhandleApply(row)投递/el-button /template /el-table-column /el-table el-pagination v-model:current-pagequeryForm.pageNum v-model:page-sizequeryForm.pageSize :totaltotal layouttotal, prev, pager, next current-changeloadJobs / /div /template表格里如果返回字段比较多记得把companyName通过联表查询返回而不是让前端拿companyId再去查一次接口那样会多出大量请求页面性能很差。所以我后端职位列表的SQL是直接JOIN company表把企业名称一起查出来的。5.4 联调阶段的常见问题联调阶段最常见的三个问题跨域、字段名对不上、日期格式太丑。跨域通过上面的代理配置能解决字段名对不上多半是因为后端返回的是companyName前端模板里写成了company_name用Vue DevTools查看响应数据照着实际字段名改就行日期格式问题在后端实体类的日期字段上加上JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)返回给前端的就是标准时间字符串。另一个容易踩的坑是前端请求POST数据时没有设置Content-Type。大多数情况下axios会自动处理但如果你用form-data传参后端如果用的是RequestBody接收JSON两边就对不上。建议全部约定使用JSON格式前端post请求时直接传对象axios会默认序列化成JSON后端用RequestBody接收。6. 完整部署流程从本地打包到阿里云上线很多人本地跑得好好的一部署就各种出问题。我自己在阿里云部署这套系统时也踩了不少坑这一节把从零到上线的完整流程和关键操作写清楚。这里以Linux服务器为例系统用CentOS 7或Ubuntu Server 22.04都可以。6.1 本地环境验证部署前先在本地把前端打包一次确认没有编译错误cd employment-web npm run build后端打包前先把application.yml里的数据库地址改成服务器的内网或公网IP然后执行mvn clean package -DskipTests打包成功后target/目录下会生成employment-0.0.1-SNAPSHOT.jar。这个jar包是SpringBoot内置Tomcat的可执行jar所有依赖都整合在里面拷贝到服务器就能跑。前端构建产物在dist/目录是纯静态文件交给Nginx托管即可。6.2 后端打包与Java环境部署服务器上先装JDKsudo apt update sudo apt install openjdk-11-jdk -y java -version然后把jar包上传到服务器比如放在/opt/employment/目录下。前端dist目录压缩后传上去解压到/var/www/employment-web/。启动后端进程我习惯用nohupcd /opt/employment nohup java -jar employment-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod app.log 21 如果你想让它开机自启或崩溃后自动重启用systemd注册一个服务更靠谱。在/etc/systemd/system/employment.service写[Unit] DescriptionEmployment System Afternetwork.target [Service] Userroot WorkingDirectory/opt/employment ExecStart/usr/bin/java -jar /opt/employment/employment-0.0.1-SNAPSHOT.jar Restartalways RestartSec5 SuccessExitStatus143 [Install] WantedBymulti-user.target然后执行systemctl daemon-reload systemctl enable employment systemctl start employment用systemctl status employment查看服务状态用journalctl -u employment -f查看实时日志。这一步非常值得做以后重启服务器后系统能自动恢复不用手动敲java命令。6.3 MySQL安装与远程访问配置服务器上安装MySQL 8.0sudo apt install mysql-server -y sudo mysql_secure_installation注意MySQL 8.0在Ubuntu上用root账户默认是auth_socket插件认证只能本机通过sudo连接。为了让后端服务能连上MySQL我建议创建一个专门的应用账号CREATE USER employmentlocalhost IDENTIFIED BY 你的密码; GRANT ALL PRIVILEGES ON employment_system.* TO employmentlocalhost; FLUSH PRIVILEGES;后端和MySQL在同一台服务器时用localhost连接就行不建议开启MySQL的公网远程访问权限除非你的运维水平足够。把建表SQL脚本上传到服务器执行mysql -u employment -p employment_system /path/to/init.sql导入后执行SHOW TABLES;确认表都建好了。6.4 Nginx部署前端与反向代理安装Nginx后修改配置文件/etc/nginx/sites-available/defaultserver { listen 80; server_name your_domain_or_ip; root /var/www/employment-web; index index.html; location / { 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_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }try_files $uri $uri/ /index.html;这行是Vue Router的history模式必须配置的不写的话刷新页面会404。location /api/把前端发来的/api请求反向代理到SpringBoot的8080端口。配置后重启Nginxsudo nginx -t sudo systemctl reload nginx现在访问服务器IP就能看到前端登录页了。如果你买了域名别忘了在域名解析里加一条A记录指向服务器IP然后把server_name改成你的域名。6.5 部署后的自检清单部署完成后我建议按这个清单从头到尾走一遍避免上线后被小问题卡住浏览器访问首页能正常打开登录页。学生注册一个账号能完成登录。企业注册账号但此时状态是“待审核”不能发布职位。管理员审核通过企业账号企业重新登录后可以发布职位。发布一条职位学生端能看到投递后企业端能收到。管理员端能看到职位投递统计、就业信息汇总。服务器重启后后端服务自动恢复MySQL正常启动Nginx正常代理。如果哪一步不正常先看对应日志。后端用journalctl -u employment -fNginx用tail -f /var/log/nginx/error.logMySQL报错用tail -f /var/log/mysql/error.log。绝大部分部署问题的根因都能从这三类日志里找到。7. 真实踩坑清单与优化方向这个项目做完我把踩过的坑和后面回头看觉得可以优化的点做个梳理给准备拿这套系统当毕设或练手项目的读者一个参考。7.1 跨域与Cookie问题开发阶段用Vite代理后跨域问题基本不出现。但如果你不用代理而是直接在前端请求http://localhost:8080/api/...浏览器就会拦截跨域请求。后端的CorsConfig可以这么解决Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }注意如果使用了JWT放在localStorage其实前端并不需要allowCredentials(true)只有当你使用Cookie时才需要。我这里开着的初衷是兼容后续可能改造成Cookie会话模式但实际使用中这样会引入额外的OPTIONS预检请求所以如果你没用到Cookie建议把allowCredentials设为false减少一次预检开销。7.2 MyBatis映射命名冲突开启map-underscore-to-camel-case后绝大多数字段都能自动映射但有两个坑。一个是实体类里如果有is_signed这种以“is”开头的字段Java属性名会生成isSignedMyBatis在映射时可能会自动把getIsSigned()方法当成getSigned()导致查询返回的字段一直是null。解决办法是数据库字段改成signed_flag或者实体类里明确用TableField或resultMap标注。另一个坑是SQL里用到了带别名的聚合字段比如COUNT(*) AS apply_count如果实体类属性叫applyCount映射规则能对上没问题。但如果你把查出来的结果装进一个没有对应属性的DTO里会自动丢失也不会报错。这种隐含问题最耗时间排查方法就是把后端返回的JSON和SQL结果对比逐字段核对。开发阶段把MyBatis的log-impl打开控制台能看到完整SQL和参数排查效率会高很多。7.3 时区与数据库连接参数这个坑我在本地几乎没遇到部署到云上后突然爆发。现象是MySQL连接时报The server time zone value CST is unrecognized或者Public Key Retrieval is not allowed。解决方案就是application.yml里的JDBC URL加上serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue。另外如果服务器时区不是中国标准时间即使连接成功NOW()返回的时间也可能差8个小时。建议部署时执行一次timedatectl set-timezone Asia/ShanghaiMySQL服务重启后所有时间字段都按北京时间写入。7.4 项目还能怎么扩展如果你想让这个项目在答辩或作品集里更有亮点以下几个方向是我觉得低成本高收益的扩展点。第一加入消息通知功能。学生投递后企业收到新简历通知企业更新投递状态后学生收到通知。通知可以存在数据库里前端轮询或WebSocket推送。这个功能能明显提升系统完整度但注意别一上来就做WebSocket先用简单的“未读消息数列表”即可。第二引入定时任务做数据统计。用SpringBoot的Scheduled注解每天凌晨统计一次各学院就业率、各专业平均薪资存到统计表里。管理员端打开报表页面直接查表即可不用每次实时大表聚合性能会好很多。第三简历文件上传这块目前我用的是本地存储上传的文件直接放在服务器磁盘。生产环境建议改成阿里云OSS或MinIO否则服务器磁盘满了很难清理。切换步骤也不复杂引入OSS SDK把上传接口保存的路径从本地地址改成OSS的URL即可。第四如果只想快速扩展后端开发效率可以把MyBatis升级成MyBatis-Plus牺牲一部分SQL控制力换来自动CRUD和分页插件适合赶工期或后期维护。如果你对SQL本身还想继续打磨那就保持原生MyBatis这两个方向没有绝对优劣。最后分享一个我在部署时反复用到的技巧改完任何配置后先停服务、清缓存、再启动不要直接reload。Nginx改完配置要nginx -t检查语法SpringBoot改完配置要重启进程Vue改完代码要重新npm run build并确认dist目录已更新。很多“为什么改了没生效”的问题最后都是缓存或没重启导致的。部署这层看似简单真正稳定跑起来需要耐心一步一步按依赖关系来先MySQL再后端最后Nginx。按这个顺序排错你的上线之路会顺很多。
返回列表