1. 项目概述
接到这个标题的时候,我第一反应是:这不就是Java Web学习路线里的“集大成者”吗。Spring Boot + SSM + MySQL + Maven + Thymeleaf + Ajax,这一套组合下来,几乎把Java后端开发从入门到实战的核心技能点全部覆盖了。说实话,这类兼职平台系统在课程设计、毕业设计、甚至初级开发者的练手项目里都非常常见,但它绝不是那种随便拼凑的“增删改查demo”。一个真正合格的兼职平台,需要做到用户角色的权限区分、兼职信息的发布与审核、报名流程的状态流转、企业端和个人端的功能隔离,这些业务逻辑放在一起,才是这个项目的真正价值所在。
我见过太多人把这类系统做成“四不像”——前端套一个免费模板,后端就是几个CRUD接口,用户表、兼职表、报名表各搞一张,登录一调通就算完事。这样做完,项目交上去确实能过,但代码里全是隐患,数据库设计经不起推敲,更别说面试的时候被问到“你为什么要用这种设计方式”时,根本答不上来。这篇文章我打算从实际开发的角度,把这套系统的设计思路、技术选型背后的考量、数据库建模、核心功能的落地步骤,以及我踩过的坑、排查过的问题,全部摊开来讲。
适合什么样的人看?如果你正在做类似的课程设计或者毕业设计,想把这个项目做得能展示、经得起盘问;或者你刚学完Java SE和MySQL,想通过一个完整项目把Spring Boot、MyBatis、Maven这些工具串起来;再或者你准备面试,需要一个真实业务场景来梳理自己的项目经验——这篇文章都能给你实打实的参考。我会尽量不讲废话,直接上干货。
2. 技术选型拆解:为什么是这套组合
2.1 Spring Boot + SSM:看似重复,实则互补
先理清一个概念:SSM是Spring + Spring MVC + MyBatis的缩写,Spring Boot并不是要“取代”SSM,而是用一种更高效的方式来组织这三者。Spring Boot的自动配置机制把Spring MVC的DispatcherServlet、MyBatis的SqlSessionFactory、数据源连接池全部按默认规则装配好了,你只需要在application.yml里写几行配置,项目就能跑起来。这就是为什么大家常说“Spring Boot是SSM的脚手架”。
但这个项目里,SSM的价值在于它的“显式配置”能让你搞清楚底层原理。举个例子,Spring Boot把文件上传的MultipartResolver自动配置好了,而纯SSM项目里,你要在spring-mvc.xml里手动声明CommonsMultipartResolver。我做这个项目的时候,特意保留了部分SSM风格的配置和代码注释,比如手动配置了MyBatis的Mapper扫描路径、在WebConfig里注册拦截器,目的就是为了让自己(也包括看我代码的人)明白每一个配置项对应的是框架的哪个环节。
面试官问“Spring Boot和Spring MVC是什么关系”,如果你能从这个角度回答——Spring Boot通过自动配置简化了Spring MVC和MyBatis的集成,但底层还是那套IoC容器、DispatcherServlet和Mapper代理机制——这比背概念强太多了。
2.2 Thymeleaf + Ajax:不前后端分离,但也要异步交互
现在很多新项目一上来就是Vue + Spring Boot前后端分离,但这类Java Web课程项目用Thymeleaf服务端渲染其实更合适。原因很简单:第一,Thymeleaf的语法和HTML天然兼容,你用浏览器直接打开静态页面也能看到效果,调试起来方便;第二,它能在服务端直接渲染数据,配合th:each、th:if这些指令,处理表格、列表、详情页的效率非常高,不需要额外定义API文档;第三,也是最重要的,如果你的项目要求是“JavaWeb”,那服务端渲染才是这个范畴里的原汁原味方案。
但是纯服务端渲染有一个痛点:局部刷新不方便。比如兼职详情页里,用户点击“立即报名”按钮,如果用表单提交加redirect,页面会整体刷新,体验很生硬。这个项目里我用Ajax来处理这类高频但轻量的交互:点击报名、切换收藏、加载评论区、异步校验用户名是否占用,全部走$.ajax或$.post,后端返回JSON数据,前端用jQuery操作DOM更新。这样一来,既保留了服务端渲染的优点,又在关键交互点做了异步体验,技术栈听起来也不单调。
2.3 Maven:统一构建与依赖管理
之所以单独提Maven,是因为它是整个项目能“一键跑起来”的基石。兼职平台系统涉及的依赖不少:Spring Boot Starter Web、MyBatis Starter、MySQL驱动、Thymeleaf Starter、Druid连接池、Lombok、Jackson等等。如果没有Maven,手动下载jar包再一个个往项目里塞,光是版本冲突就能把人搞崩溃。
Maven在这里解决了两件事:一是依赖的“传递性依赖管理”,比如你引入spring-boot-starter-web,它自动帮你拉取Spring MVC、Tomcat、Jackson等一系列配套依赖;二是统一的构建生命周期,mvn clean package一条命令就能把项目打成可执行的jar包。后面我还会专门讲一下Maven的阿里云镜像配置,这个在国内几乎是必做的,不然下载依赖的速度会让你怀疑人生。
2.4 MySQL:数据持久化的核心
这套系统的数据量级对于MySQL来说完全是小菜一碟,但表结构怎么设计才是重点。兼职平台涉及的实体有用户(分为学生端和企业端)、兼职信息、报名记录、收藏记录、消息通知等,表之间的关联关系需要用外键逻辑(不推荐物理外键)来维护。后面我会给出详细的建表SQL,这里先埋一个关键点:表的字段命名、类型选择、索引设计,会直接影响你写SQL的复杂度和查询效率,千万别随手建表。
3. 数据库设计与核心表结构
3.1 需求分析先行:兼职系统到底有哪些“人”和“事”
很多初学者拿到这个题目直接开建表,这是最大的忌讳。先坐下来想清楚角色和业务流:系统里有普通用户(学生,想找兼职)、企业用户(发布兼职的人),还有管理员(负责审核和信息管理)。对应的业务就有:用户注册登录、企业发布兼职、用户浏览搜索兼职、用户报名兼职、企业查看报名者、管理员审核发布内容。
我把核心表设计成5张:
user:用户表,含角色字段(0普通用户,1企业用户,2管理员)job_info:兼职信息表,记录标题、描述、薪资、地址、发布者等apply_record:报名记录表,关联用户和兼职favorite_record:收藏记录表,用户收藏的兼职admin_notice:通知/公告表,管理员发布的系统公告
这张表的设计顺序也有讲究:先定“用户”和“角色”,再定“兼职信息”,因为用户是系统的绝对主体,兼职信息围绕用户产生;报名和收藏属于关联表,依赖前两者;管理员通知是辅助扩展,不影响核心流程。按照这个顺序建表,代码写起来也顺手。
3.2 user表设计:角色字段和状态字段的关键取舍
用户表是最基础但最容易设计出问题的表。我见过有人把手机号设为主键,也有人把用户名设为主键,其实这两种都有隐患。主键用自增的id最稳妥,业务字段用唯一索引去约束就可以。下面是建表SQL:
CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码(BCrypt加密)', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `phone` varchar(11) DEFAULT NULL COMMENT '手机号', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `role` tinyint(4) NOT NULL DEFAULT '0' COMMENT '角色:0普通用户 1企业用户 2管理员', `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:0禁用 1正常', `company_name` varchar(100) DEFAULT NULL COMMENT '企业名称(企业用户填写)', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';几个设计点说明一下:
password一定不能明文存储。我在这套项目里用的是BCrypt加密,每次校验密码时用BCrypt.matches()方法比对,这比MD5加盐还省事且更安全。role字段设计成tinyint而不是字符串,这样查询效率高,代码里用常量类统一管理,避免魔法值散落各处。- 企业用户多出的
company_name字段,用DEFAULT NULL处理,普通用户就是NULL,不影响整体查询。
3.3 job_info表设计:薪资用字符串还是数字
兼职信息的字段比用户表复杂,有个容易被忽略的坑:薪资字段。有人用salary字符串,存“200元/天”,这样展示确实方便,但用户一旦想按薪资排序或筛选,字符串就无能为力了。我的建议是拆成三个字段:salary_min(最低,单位元)、salary_max(最高)、salary_unit(单位:天/周/月)。展示时在页面拼成“200-300/天”,筛选时直接BETWEEN salary_min AND salary_max,灵活且规范。
CREATE TABLE `job_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL COMMENT '发布者ID(企业用户)', `title` varchar(100) NOT NULL COMMENT '兼职标题', `description` text COMMENT '兼职描述', `category` varchar(50) DEFAULT NULL COMMENT '兼职分类', `salary_min` decimal(10,2) DEFAULT NULL COMMENT '最低薪资(元)', `salary_max` decimal(10,2) DEFAULT NULL COMMENT '最高薪资(元)', `salary_unit` varchar(10) DEFAULT NULL COMMENT '薪资单位:day周 month', `address` varchar(200) DEFAULT NULL COMMENT '工作地点', `headcount` int(11) DEFAULT '1' COMMENT '招聘人数', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0待审核 1已发布 2已下架 3审核未通过', `view_count` int(11) NOT NULL DEFAULT '0' COMMENT '浏览量', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_status_category` (`status`, `category`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='兼职信息表';这里status字段用数字状态机管理:0待审核、1已发布、2已下架、3审核未通过。为什么要单独一个“待审核”状态?因为管理员审核是这个系统的核心功能之一,企业发布的信息不能直接上架,必须管理员后台确认。这个设计在答辩里是一定会被问到的,提前想好“审核机制保障了兼职信息的真实性和安全性”这个说法。
3.4 关联表设计:报名、收藏的防重设计
报名记录表apply_record是典型的业务关联表,它的设计难点是“防止重复报名”。数据库层面怎么做?加联合唯一索引:
CREATE TABLE `apply_record` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL COMMENT '报名用户ID', `job_id` bigint(20) NOT NULL COMMENT '兼职信息ID', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0待处理 1已录用 2已拒绝 3已取消', `apply_time` datetime DEFAULT CURRENT_TIMESTAMP, `remark` varchar(500) DEFAULT NULL COMMENT '报名备注', PRIMARY KEY (`id`), UNIQUE KEY `uk_user_job` (`user_id`, `job_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报名记录表';UNIQUE KEY uk_user_job就是用来防重的。就算代码逻辑里忘了判断,数据库这一层也会拦截重复插入。这一点在面试时可以主动讲,体现你有“数据库约束兜底”的意识。
收藏表favorite_record结构类似,也是user_id+job_id联合唯一。值得注意的是,收藏和报名是两张表,因为业务状态不同:报名有状态流转(待处理、已录用、已拒绝),收藏就是个简单的“存没存”关系。字段设计要贴合业务复杂度,不要为未来根本不会出现的需求过度设计。
4. 环境搭建与项目初始化
4.1 开发环境版本匹配:JDK、MySQL、Maven、Spring Boot
这个项目踩过最大的坑就是版本匹配问题。热词里也有springboot版本太高、maven下载安装、mysql安装配置教程这些搜索词,可见这批坑几乎人人都会遇到。我建议的版本组合如下,实测兼容性很好:
- JDK:1.8或11(不要贪新用JDK 17+,除非你清楚Spring Boot版本对它的支持情况)
- Spring Boot:2.5.12(稳定且资料多,支持JDK 8)
- MySQL:8.0.x(注意5.7和8.0的驱动配置差异)
- Maven:3.6.3或3.8.x
- IDEA:2021.3及以上(自带Maven插件,但要注意内置Maven版本和设置)
用Spring Boot 2.5.12配合JDK 8是最保险的组合,网上几乎所有的教程、博客、AI回答都基于这套版本,遇到问题搜得到解决方案。你要是看了热词“springboot版本太高”就知道,很多人的问题全是因为盲选最新版,结果依赖拉不下来、启动报错、语法又不兼容,纯粹自找麻烦。
4.2 Maven阿里云镜像配置:国内开发的必修课
Maven中央仓库在国外,没有镜像加速的话,下载依赖的网速会让人崩溃。修改maven安装目录/conf/settings.xml,在<mirrors>标签里加入阿里云镜像:
<mirror> <id>aliyunmaven</id> <mirrorOf>central</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public</url> </mirror>注意mirrorOf不能写*,否则本地仓库和私服都会被强制走镜像。另外,如果项目里还有其他镜像源,优先级是按settings.xml里的声明顺序来的。配好之后,清理一下本地仓库的lastUpdated文件再重新reimport,基本就能解决依赖下载失败的问题。
4.3 项目初始化:Spring Initializr创建工程
创建Spring Boot工程最快捷的方式是利用IDEA内置的Spring Initializr(File → New → Project → Spring Initializr),选择Maven类型,填入Group和Artifact。需要选的依赖只有四个:Spring Web、Thymeleaf、MyBatis Framework(选MyBatis Spring Boot Starter,注意别选错成mybatis-plus)、MySQL Driver。另外手动在pom.xml里加一下Druid连接池和Lombok:
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.9</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>Druid的好处是自带监控页面和SQL拦截,开发调试时能看到每一条SQL的执行时间和参数,定位问题非常省事。Lombok则能节省大量getter/setter代码,让实体类干净不少。但注意,Lombok在JDK版本不匹配时会出现编译失败的迷之问题,比如java: cannot find symbol,这时候先排查Lombok版本,再用mvn compile看详细日志。
4.4 配置文件:pom.xml和application.yml的关键细节
application.yml的配置看起来简单,但有几个细节决定成败。首先是数据库连接,务必加上useSSL=false和serverTimezone=Asia/Shanghai,不然MySQL 8.0会给你报一堆SSL和时区警告,热词里的mysql ssl连接错误就是这个原因。下面是完整配置:
server: port: 8080 servlet: context-path: / spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/parttime?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 druid: initial-size: 5 min-idle: 5 max-active: 20 thymeleaf: cache: false prefix: classpath:/templates/ suffix: .html mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.parttime.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl特别注意map-underscore-to-camel-case: true这行。没有它,MyBatis把数据库的create_time映射到实体类的createTime字段会失败,全是NULL。这也是MyBatis和JPA的一个明显差异:JPA默认遵循驼峰映射,MyBatis得自己开这个开关。
5. 核心业务实现与实操过程
5.1 后端分层:Controller层要做减法
Spring Boot项目传统的分层是Controller → Service → Mapper。我见过很多新手把业务逻辑直接堆在Controller里,看起来代码少,但排查问题、单元测试都很难写。这套项目的分层规则我在编码前定下来:
- Controller层只用接收参数、调用Service、返回结果(JSON或视图名),不写任何SQL相关逻辑
- Service层负责业务逻辑组合,比如报名兼职需要“校验用户是否存在 + 校验兼职状态 + 插入记录 + 更新兼职报名数”
- Mapper层只做SQL数据访问,XML里写动态SQL
Controller层的代码风格应该长这样:
@Controller @RequestMapping("/job") public class JobController { @Resource private JobService jobService; @GetMapping("/list") public String list(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, Model model) { PageResult<JobInfo> page = jobService.getJobPage(pageNum, pageSize, null, null); model.addAttribute("page", page); return "job/list"; } @PostMapping("/apply") @ResponseBody public Result apply(@RequestParam Long jobId) { // 从Session获取当前登录用户 User loginUser = (User) session.getAttribute("loginUser"); return jobService.applyJob(loginUser.getId(), jobId); } }/job/list返回视图名,走Thymeleaf渲染;/job/apply返回Result对象,由Jackson自动转成JSON。一个Controller里同时包含“页面跳转”和“接口返回”两种方法,这是服务端渲染 + Ajax混合架构的最直观体现。
5.2 登录功能:Session管理与拦截器
登录功能做得好不好,直接决定整个系统的安全感。我用的方式是:用户登录成功后将User对象存入Session,然后自定义一个LoginInterceptor拦截器,校验所有需要登录才能访问的URL。在WebConfig中注册拦截器的代码:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") .excludePathPatterns( "/login", "/register", "/logout", "/job/list", "/job/detail/**", "/static/**", "/css/**", "/js/**", "/images/**" ); } }excludePathPatterns的意思很明确:不需要登录就能访问的公开URL(首页、兼职列表、详情页、静态资源)放行,其余全部拦截。在LoginInterceptor里重写preHandle方法,判断Session里有没有用户:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { User user = (User) request.getSession().getAttribute("loginUser"); if (user == null) { // 如果是Ajax请求 String requestedWith = request.getHeader("X-Requested-With"); if ("XMLHttpRequest".equals(requestedWith)) { response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"请先登录\"}"); } else { response.sendRedirect("/login"); } return false; } return true; }这里区分Ajax请求和普通页面请求是必须的。如果不区分,Ajax请求被拦截后直接302跳转到登录页,前端拿到的是整个登录页的HTML而不是JSON,解析必然报错。这个小细节是我实际调了很久才发现的,写在这里希望你能少走弯路。
5.3 兼职信息发布与审核流程
企业用户发布兼职后,数据插入job_info表时status默认是0(待审核)。管理员登录后台,在待审核列表里查看兼职详情,点击“通过”调updateJobStatus接口,把状态改为1;点击“驳回”则改成3,同时最好填一个驳回理由,方便前端展示。审核通过以后,企业用户还能自己对已发布的兼职做下架操作,把状态改成2。
这些状态流转全部在Service层用一个方法收敛:
public Result changeJobStatus(Long jobId, Integer targetStatus, Long operatorId) { JobInfo job = jobMapper.selectById(jobId); if (job == null) { return Result.error("兼职信息不存在"); } // 管理员可以执行所有状态修改 // 企业用户只能下架自己发布的兼职 if (operatorId != null && job.getUserId().equals(operatorId) && targetStatus == 2) { jobMapper.updateStatus(jobId, 2); return Result.success(); } return Result.error("无权执行该操作"); }5.4 报名功能与状态回显
报名流程的逻辑在校验和防重上下功夫。一次报名请求的Service方法执行顺序:
- 根据
jobId查兼职是否存在,状态是否为“已发布” - 根据
userId查当前用户身份,普通用户才能报名(企业用户不能报名兼职) - 用
apply_record表的联合唯一索引兜底,捕获DuplicateKeyException返回“您已报名该兼职” - 插入报名记录,状态为0(待处理)
- 更新兼职表的报名人数(如果设计了该字段)
在用户端页面,已报名、已收藏的兼职状态要回显,做法是查详情时同时查询当前用户对该兼职的报名记录和收藏记录:
public JobDetailVO getJobDetail(Long jobId, Long loginUserId) { JobInfo job = jobMapper.selectDetailById(jobId); JobDetailVO vo = new JobDetailVO(); BeanUtils.copyProperties(job, vo); if (loginUserId != null) { vo.setApplied(applyMapper.countByUserIdAndJobId(loginUserId, jobId) > 0); vo.setFavorited(favoriteMapper.countByUserIdAndJobId(loginUserId, jobId) > 0); } return vo; }前端拿到applied和favorited两个布尔字段,就能决定按钮的文案和disabled状态,刷新之后状态不丢,这个体验很重要。
5.5 搜索与分页:MyBatis动态SQL的实战用法
兼职列表页的搜索功能是一个典型的多条件动态查询。用户可能按标题关键词、分类、薪资范围、地区筛选。用MyBatis的if标签拼接条件最灵活:
<select id="searchJobList" resultType="com.example.parttime.entity.JobInfo"> SELECT * FROM job_info <where> <if test="keyword != null and keyword != ''"> AND title LIKE CONCAT('%', #{keyword}, '%') </if> <if test="category != null and category != ''"> AND category = #{category} </if> <if test="minSalary != null"> AND salary_max >= #{minSalary} </if> <if test="maxSalary != null"> AND salary_min <= #{maxSalary} </if> AND status = 1 </where> ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} </select>这里有两个注意点:一是用<where>标签而不是手写WHERE 1=1,MyBatis会自动处理开头的AND;二是>和<在XML里要转义成>和<,不然XML解析直接报错。分页我建议直接手写LIMIT #{offset}, #{pageSize},因为这个项目的数据量用不着引入PageHelper插件,手写反而更清晰。offset由前端传过来的pageNum和pageSize计算:offset = (pageNum - 1) * pageSize。
5.6 Thymeleaf页面渲染:列表、详情、后台管理
Thymeleaf写页面的时候,最容易忘的是th:各种指令的语义。我举两个实际页面来说明:
兼职列表页的关键片段:
<div class="job-card" th:each="job : ${page.records}"> <h3><a th:href="@{/job/detail/{id}(id=${job.id})}" th:text="${job.title}">兼职标题</a></h3> <p th:text="${job.description}">兼职描述</p> <span th:text="${#numbers.formatDecimal(job.salaryMin, 1, 0)} + '-' + ${#numbers.formatDecimal(job.salaryMax, 1, 0)} + '/' + ${job.salaryUnit}">薪资范围</span> </div>分页导航怎么处理?简单做法是Controller返回pageNum、pageSize、total、pages,前端用Thymeleaf的th:href拼接页码参数,加上上一页/下一页的按钮。
后台管理页面临时表比较多,建议用Bootstrap或AdminLTE这类现成的CSS框架,别自己写样式,耗时且效果不好。这个地方没人管你用的是不是手写CSS,项目核心是后端和业务逻辑,前端用成熟组件库是合理取舍。
6. 常见问题排查与实战避坑
6.1 启动失败:端口被占用和数据库连接失败
Spring Boot启动报错五花八门,最高频的是Port 8080 was already in use。排查方式:
- Windows:
netstat -ano | findstr 8080找到PID,然后用taskkill /F /PID 对应PID杀掉 - Linux/Mac:
lsof -i:8080查看占用进程,kill -9 PID处理
数据库连接失败需要分清是网络层面还是权限层面。先ping localhost确认MySQL已启动,再用mysql -uroot -p测试账号密码。常见报错Access denied for user 'root'@'localhost'基本是密码错误或账号授权问题。别忘了MySQL 8.0默认使用caching_sha2_password认证,如果驱动版本太老会报Public Key Retrieval is not allowed,这时候在连接URL加上allowPublicKeyRetrieval=true可以绕过去。
6.2 MyBatis映射错误:结果集全为NULL
如果你查出的数据全是NULL,十有八九是驼峰映射没开,或者是实体类属性名和数据库列名对不上。在application.yml里已经开了map-underscore-to-camel-case就不怕了。另外检查一下Mapper接口的@Param注解:
List<JobInfo> searchJobList(@Param("keyword") String keyword, @Param("category") String category, @Param("minSalary") BigDecimal minSalary, @Param("maxSalary") BigDecimal maxSalary, @Param("offset") Integer offset, @Param("pageSize") Integer pageSize);XML里用#{keyword}引用时,参数名必须和@Param的值一致,否则会报Parameter 'keyword' not found。
6.3 静态资源404:CSS、JS加载不出来
这个问题通常是拦截器搞的鬼。如果静态资源被登录拦截器拦截,就会出现登录页能打开但样式全丢的情况。解决办法:确保WebConfig的excludePathPatterns里正确放行了静态资源路径:
.excludePathPatterns("/static/**", "/templates/**", "/css/**", "/js/**", "/images/**", "/favicon.ico")同时,把静态文件放在src/main/resources/static目录下,不要放在templates里。templates目录下的文件默认是受Thymeleaf模板解析器管理的,直接放CSS会解析报错。
6.4 Thymeleaf语法错误:表达式解析失败
Thymeleaf对表达式的书写非常敏感。常见错误:th:each写成了th:foreach(这俩不一样),表达式里的字符串没有加单引号,比如th:text="${job.status == 1}"这里的1是数字没问题,但如果你想比较字符串,必须写成${job.role == 'admin'}(注意引号)。另外,th:if里不能用逻辑运算符&&和||,要用and和or,或者用括号加#strings工具类:
<th:if="${job.status == 1 and job.salaryMin >= 200}">符合条件</th:if>6.5 MySQL连接8小时超时和SSL警告
这属于运行期问题。MySQL的wait_timeout默认是8小时,如果项目长期闲置数据库连接池里的连接会被服务端断开,下次请求时报Communications link failure。Druid连接池里配置testWhileIdle=true和timeBetweenEvictionRunsMillis=60000,让连接池空闲时自动检测并丢弃死连接:
druid: test-while-idle: true time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 3000006.6 打包部署注意点
项目最终要能mvn clean package打成jar包。这里有几个容易踩的坑:
- 打包前确认
pom.xml中包含spring-boot-maven-plugin,否则jar包无法直接java -jar运行 - 运行jar包时数据库、端口等配置可以用
--server.port=8081命令行参数覆盖,方便不同环境部署 - 如果打包后的jar包体积特别小(只有几百KB),多半是依赖没打进去,检查插件配置里有没有
<executions>节点
7. 项目扩展思路
这套基础版本做完之后,建议你有余力就往这几个方向扩展,每一个都够写进简历里当加分项:
- 消息通知模块:企业处理报名后给用户发站内信,用
admin_notice表加一个notice_type字段区分系统通知和业务通知 - 图片上传:企业发布兼职时上传工作环境照片,用Spring Boot的
MultipartFile接收文件,存本地目录或MinIO。如果想接MinIO,可以参考当前热词里“minio加入到springboot”的玩法,用spring-boot-starter-data-redis集成做缓存也行 - 数据可视化报表:管理员后台用ECharts展示兼职发布量趋势、报名最多的兼职Top10,数据源就是
job_info表和apply_record表 - Redis缓存:兼职列表页的搜索是高频查询,可以把热门关键词和首页推荐职位缓存到Redis,减少MySQL压力
但说句心里话,扩展功能的优先级要排在“把现有功能做好”的后面。很多人上来就想着加这个加那个,结果连最基础的登录和CRUD都做得不完善。先把稳定性和代码质量兜住,再去谈扩展,面试的时候才不会一问三不知。
8. 实操总结与补充建议
8.1 编码前先画业务流程图
我说一个很多教程里不强调但我实际做项目时体验很深的点:编码前花半小时把业务流程图和数据流图画出来,比直接开工写代码节省的时间多得多。比如报名兼职的流程,画清楚“用户点按钮 → Controller接收 → Service处理 → Mapper操作 → 前端响应”每一步的输入输出,后面调试的时候心里有数,遇到问题不会手忙脚乱。
8.2 代码提交规范与Git使用
这个项目如果要用Git管理,建议每个功能模块完成后提交一次:feat: 用户注册登录、feat: 企业发布兼职、feat: 管理员审核、fix: 修复报名防重。习惯了规范提交以后,回滚和查看改动历史会非常方便。我见过太多人项目做完才想起来git init一次提交,最后想回退某个功能只能靠记忆,苦不堪言。
8.3 “会做”和“讲得出”是两回事
最后多说一句,这类JavaWeb项目的核心价值不只是跑起来,而是你能不能把设计思路、技术选型、关键代码的取舍讲给面试官听。比如“为什么用联合唯一索引防重”、“拦截器里为什么区分Ajax请求”、“Thymeleaf和前后端分离各自的优劣”,这些不是背诵题,而是基于你做项目时的真实思考。你把这个项目老老实实做下来,这些问题的答案自然就在你脑子里,不需要背。
我今天把环境配置、表结构设计、核心业务流程、常见问题排查从头到尾捋了一遍,就是希望你能少走一些弯路。如果过程中遇到具体报错,把错误信息复制到搜索引擎,加上“Spring Boot”和版本号,基本都能找到答案。毕竟这个技术栈的用户基数太大了,你踩过的坑,大概率早有人踩过。