前几天帮一个朋友梳理他手头的毕设项目,题目是《基于Spring Boot河南特色美食分享系统》。第一眼看到这个题目,我其实挺有好感的——相比千篇一律的“XX管理系统”,这个题目既有明确的地域文化属性,又有真实的内容社区逻辑,做成一个能演示、能答辩、能扩展的系统,空间很大。但这个项目真正做下来,踩的坑比想象中多,从Spring Boot版本选型到Vue打包部署,每一环都有细节,很多坑都不是教程里会提前告诉你的。这篇就结合我做这个项目的完整过程,把从需求拆解、技术选型、数据库设计到接口开发、前后端集成、上线路障排查的经验一次性讲清楚。
不管你是准备拿它当毕业设计,还是想练手Spring Boot + MyBatis + Vue这套主流组合,或者纯粹对“内容分享类系统”的后端设计感兴趣,这篇都能给你一份可以直接照着走的路线图。我会尽量说人话,多讲“为什么”,少念“操作手册”,项目里的关键代码我也会贴出来,方便你直接抄作业。
1. 从毕设选题到系统拆解:河南美食分享系统的本质是内容社区
1.1 河南美食的领域特色决定了系统的内容属性
在做系统之前,先别急着写代码,得想清楚这个“美食分享系统”到底在解决什么问题。河南特色美食最典型的代表有胡辣汤、烩面、开封灌汤包、道口烧鸡、桶子鸡、鲤鱼焙面、洛阳水席等,这些美食的地域属性极强,每一样背后都有历史故事和制作门道。所以这个系统的核心资产不是“商品”,而是“内容”——一道菜的文化背景、用料做法、推荐店铺/家庭做法、食客的评价心得,这些内容才是用户浏览和分享的动力。
这和做一套“图书借阅管理系统”或“商品管理系统”有本质区别。图书借阅核心是“借还状态”,商品系统核心是“库存和订单”,而美食分享系统核心是“内容的展示、沉淀和互动”。想清楚这一点,后面的数据表设计、接口设计才不会跑偏。我用一句话概括这个项目的本质:一个垂直领域的轻量内容社区,挂上Spring Boot的后端壳。
1.2 功能模块收敛:用户端、管理端、数据基础层三块
很多第一次做毕设的人容易犯一个毛病——把功能列得又多又碎,最后砍不完做不完。我的建议是收敛成三个层面:
- 用户端(面向普通访客/注册用户):用户注册登录、首页轮播推荐、美食分类浏览、关键词搜索、美食详情查看、收藏、点赞、评论、个人中心(查看我的收藏/我的评论/我的资料)。
- 管理端(面向系统管理员):管理员登录、美食信息发布与管理(增删改查、上下架)、美食分类管理、用户管理(禁用/启用)、评论审核与删除、系统公告发布。
- 数据基础层:MySQL数据库存储业务数据,图片文件本地存储(或OSS,但毕设项目本地存储足够了),一个定时任务用来刷新每日推荐。
这里要特别说一句:管理端不要做太重。像用户管理、评论审核这类功能,能实现基础的列表展示、状态修改就够了,重点精力要放在“用户端的美食分享闭环”上,因为答辩时演示的核心场景都在用户端。我当时把管理端压缩到只做了美食管理、分类管理和用户管理三块,节省了大量时间。
1.3 为什么选 Spring Boot + MyBatis,而不是其他组合
技术选型这块,直接说结论:Spring Boot 2.7.x + MyBatis + MySQL + Vue 2/3 + Element UI是当前中等难度毕设和应用型项目最稳妥的组合。
- Spring Boot:自动装配把SSM时代的繁琐XML配置基本干掉了,一个应用类直接启动内嵌Tomcat,部署时不再需要单独装Tomcat再打war包。这对学生党或快速交付场景极其友好。
- MyBatis:灵活写SQL,尤其是美食列表这种带多条件动态查询的场景,用XML里拼动态SQL特别顺手,比JPA那种“把你封装得太好、想写个多表联查反而别扭”的方式更直白。
- Vue + Element UI:前端组件现成,表格、表单、分页组件拖过来就能用,能极大压缩前端开发时间。
- MySQL:中小型内容系统的最稳选择,资料多,遇到中文乱码、字符集问题也好查。
这套组合放在简历里、放在答辩PPT里都说得过去。后端逻辑清晰、前端界面大气,演示效果也好。
2. Maven工程搭建与Spring Boot版本选择的实战考量
2.1 Spring Boot版本别盲目追新:版本选型是第一步坑
打开Spring Initializr或者IDEA自带的Spring Boot创建向导,默认给的往往是当前最新稳定版,比如Spring Boot 3.x。但这里面有个很现实的坑:Spring Boot 3.x要求JDK 17+,而大量学校教学环境和老机器的JDK还停在Java 8。如果你机器上装的是JDK 8,却选了个3.x版本,项目启动就会直接报错,而且很多老版本的依赖也会出现兼容问题。
结合我这次项目的实测,最终选了Spring Boot 2.7.18 + JDK 8 + MyBatis Spring Boot Starter 2.3.x,理由有三个:
- 毕设/中小型系统用不到Spring Boot 3的AOT、GraalVM等新特性,选新版本纯属给自己加戏。
- 2.7.x还是javax命名空间,网上大量教程、博客都是javax的写法,遇到问题搜出来基本能直接套用,而3.x把javax换成了jakarta,很多老代码需要改import。
- 兼容性稳,Java 8 + 2.7.x这个组合bug少、报错少,能让你把精力花在业务上而非环境上。
如果你的JDK本来就是17+,也可以选Spring Boot 3.x,但下文涉及的代码请把javax.servlet替换为jakarta.servlet,大致逻辑不变。
2.2 Maven项目结构与依赖配置
对应的maven项目构建方法非常简单:在IDEA里直接新建Spring Initializr项目,或者用Maven命令mvn archetype:generate拉一个web骨架,然后手动往pom.xml里加依赖。我最终的pom.xml核心依赖如下,你可以直接参考:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <!-- Web支持:内嵌Tomcat、Spring MVC、Jackson --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis整合 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <!-- MySQL驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> <scope>runtime</scope> </dependency> <!-- Lombok减少样板代码 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- PageHelper分页插件 --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.7</version> </dependency> </dependencies>项目目录采用标准的三层结构,我习惯配上一个config包和common包,分别放配置类和通用返回结果/工具类:
com.example.foodshare ├── FoodShareApplication.java // Spring Boot 启动类 ├── config │ ├── WebMvcConfig.java // 静态资源映射、拦截器注册 │ └── MybatisPlusConfig.java // 分页插件配置(如果用PageHelper则不需要) ├── controller // 控制层 ├── service // 业务层 ├── mapper // 持久层接口 ├── entity // 数据库实体 ├── vo // 视图对象,给前端用 └── common ├── Result.java // 统一返回结构 └── GlobalExceptionHandler.java // 全局异常处理这里有个非常容易被忽略的点:实体类、VO、DTO别混为一谈。直接用实体类返回给前端,会把密码、冗余字段全部暴露出去,很危险。我后期会专门说这个。
2.3 Spring Boot自动装配在这个工程里的具体体现
说实话,大多数学生做完项目,面试或答辩被问到“Spring Boot自动装配原理”就卡住了。其实放在实战里理解特别方便。
@SpringBootApplication是个组合注解,里面包含了@SpringBootConfiguration、@EnableAutoConfiguration和@ComponentScan。其中关键的是@EnableAutoConfiguration,它的作用是通过spring.factories文件(2.7.x及以前)里配置的AutoConfiguration.imports,自动加载一堆XxxAutoConfiguration类。
拿我们项目里的MyBatis自动装配来说:mybatis-spring-boot-starter里带了一个MybatisAutoConfiguration,它会在项目启动时自动检测到你的DataSource(因为Spring Boot已经把数据库连接池自动配好了),然后自动创建SqlSessionFactory和SqlSessionTemplate。这就是为什么你不需要写一堆XML配置,直接在application.yml里配置spring.datasource.url就能连上数据库,并且直接在Mapper接口上使用@Mapper注解即可完成扫描注册。
现实中遇到“Mapper接口无法注入”的报错,十有八九就是自动装配的扫描路径没对上:要么启动类位置不在包外层,导致组件扫描扫不到;要么忘了加@MapperScan或@Mapper注解。这个坑我第6部分会再提。
3. 数据库设计:用一张美食主表看清前后端需求的边界
3.1 从“内容分享”倒推出来的六张表
数据库设计是这类项目最关键也最容易被轻视的一步。我见过太多人一上来就建一张巨大的“美食表”,把所有字段堆在一起,后期扩展时痛不欲生。合理的表结构应当至少包含六张表:用户表、分类表、美食表、收藏表、评论表、点赞表。
我最终的建表设计选用了如下关键字段(简化版),你可以对照自己的需求调整:
| 表名 | 核心字段 | 设计说明 |
|---|---|---|
user | id, username, password, nickname, avatar, role, status, create_time | role区分管理员/普通用户;status用于禁用 |
category | id, name, sort, status | 美食分类,如“汤羹类”“面食类”“肉食类” |
dish | id, category_id, name, cover_image, description, history, practices, recommended_level, view_count, status, create_time | 美食主表,history存文化背景,practices存做法步骤 |
favorite | id, user_id, dish_id, create_time | 收藏关联表,UNIQUE KEY(user_id, dish_id)防重复收藏 |
comment | id, user_id, dish_id, content, parent_id, create_time | 支持一级回复逻辑,parent_id默认为0表示顶级评论 |
like_record | id, user_id, dish_id, create_time | 点赞记录表,同样加唯一索引 |
这样做的好处是:用户行为数据与内容数据解耦。美食表不需要存“谁收藏了它”,只需要一个like_count字段做冗余计数;收藏表本身是一张明细流水,既方便查询“我收藏了什么”,也能统计“这道菜被多少人收藏”。反过来,如果非要在美食表里塞一个“收藏用户列表”的JSON字段,查询时就会非常难受,还会破坏数据规范性。
3.2 河南美食分类怎么建模
分类这块是体现项目“河南特色”的小细节。我一开始很随意地建了个“热菜、凉菜、主食、汤”,后来发现完全不对——河南美食的文化逻辑是“地域 + 形态”结合的。我最终落地的分类是:
- 汤羹类:胡辣汤、洛阳水席的代表菜、酸辣汤等;
- 面食类:烩面、饸饹面、开封灌汤包、蒸饺等;
- 肉食类:道口烧鸡、桶子鸡、卤肉、羊肉等;
- 小吃类:焖子、炒凉粉、芝麻烧饼等;
- 宴席类:水席整桌、传统宴席中的经典菜。
分类建的合理,前端分类导航、首页“分类推荐”模块、后台分类管理都变得顺理成章。这也是答辩时能拿出来的设计亮点:你思考了领域,而不是抄了一个通用电商模板。
3.3 收藏、评论、点赞三张关联表的取舍
有朋友问“收藏、点赞不都是记录用户对美食的行为吗,合并成一张行为表行不行?”理论上可以用一张user_dish_action表,加上action_type字段区分收藏和点赞。但实际开发中我建议拆开,理由很实在:
- 两者的业务语义、查询频率不一样。收藏一般在“个人中心”列表页高频展示,点赞通常只做一个计数的累加。
- 分表后SQL简单,索引命中也直观。收藏表查
user_id + create_time,点赞表主要查dish_id + user_id校验今日是否已赞。 - 后续如果扩展“踩”“分享”“浏览量”等行为,可以理解为“每张行为表都是垂直独立的”,不会互相污染。
在数据量只有几千上万条时,拆表和合表性能上几乎没有差异,但代码可读性和后期维护差别很大。用最简单直接的方式建模,永远是第一原则。
4. 后端核心接口实战:注册登录、分页查询、收藏点赞与图片上传
4.1 注册登录流程:密码加盐与前后端会话保持
用户模块是几乎所有系统的起点。这里我会讲两种落地方案,并说明为什么最终选择其中一个。
密码存储:禁止明文存密码,至少要做MD5加盐。所谓加盐就是在用户密码后面拼接一段随机字符串,再做MD5散列,这样即使用户A和用户B密码相同,数据库里的密文也不同,能有效防止撞库。不过MD5本身已不被推荐做密码哈希,毕设/中小项目里我会直接建议用BCryptPasswordEncoder,Spring Security里自带,但单独用Spring Security会加大项目复杂度。我的做法是直接用spring-security-crypto里的BCrypt类,只当密码工具用,不引入完整的安全框架,代价极小效果很好。
// 注册:密码加密 String rawPassword = user.getPassword(); String encodedPassword = BCrypt.hashpw(rawPassword, BCrypt.gensalt()); user.setPassword(encodedPassword); // 登录:密码校验 if (BCrypt.checkpw(rawPassword, dbUser.getPassword())) { // 校验通过,记录session }会话保持:毕设项目优先用Session方案。登录成功后直接存session.setAttribute("loginUser", user),配合一个拦截器判断是否登录;这种方案代码量小、原理清晰,面试被问也能答得清楚。如果你开发的是前后端分离且要上线的项目,可以用JWT,但JWT引入密钥管理、过期刷新等问题,对我来说属于“后期优化项”,不会在毕设阶段增加负担。
// 拦截器验证登录 public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getSession().getAttribute("loginUser") == null) { // 判断是否AJAX请求,返回JSON提示 response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"未登录\"}"); return false; } return true; } }4.2 美食列表分页查询:PageHelper 与手写LIMIT的抉择
美食列表是最核心的查询接口,需要支持分页、分类筛选、关键词搜索。放在MyBatis里,如果全部手写LIMIT,最大的问题是需要先count一次再select一次,还要手动计算页码偏移量,代码会很啰嗦。用PageHelper则能大幅简化:
@GetMapping("/list") public Result list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "8") Integer size, @RequestParam(required = false) Integer categoryId, @RequestParam(required = false) String keyword) { PageHelper.startPage(page, size); List<DishVO> dishList = dishMapper.selectDishList(categoryId, keyword); PageInfo<DishVO> pageInfo = new PageInfo<>(dishList); return Result.success(pageInfo); }对应的Mapper XML里,动态查询就这样写:
<select id="selectDishList" resultType="com.example.foodshare.vo.DishVO"> SELECT d.id, d.name, d.cover_image, d.description, d.recommended_level, d.view_count, d.like_count, c.name AS category_name FROM dish d LEFT JOIN category c ON d.category_id = c.id <where> <if test="categoryId != null"> AND d.category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> AND d.name LIKE CONCAT('%', #{keyword}, '%') </if> AND d.status = 1 </where> ORDER BY d.create_time DESC </select>注意两个细节:PageHelper.startPage之后必须紧跟第一条查询方法,别中间插其他查询;返回给前端的一定是DishVO而不是Dish实体,因为VO里需要展示分类名称,而实体里只有category_id。如果把实体直接返回,前端还得再查一遍分类表,没必要。
4.3 收藏与点赞:幂等设计与统计字段的一致性
收藏和点赞看似简单,但很容易翻车的地方是幂等性和统计值一致性。
收藏:前端“收藏/取消收藏”是一个toggle动作。后端接口设计成POST /favorite/add和POST /favorite/cancel,每个接口都做校验。比如添加收藏前,先查favorite表是否已有记录,若已有直接返回“已收藏”,不要重复插入。最稳妥的兜底方案是在表上建唯一索引(user_id, dish_id),即使接口并发调用,数据库层面也会拒绝重复数据,同时捕获DuplicateKeyException友好提示。
@PostMapping("/favorite/add") public Result addFavorite(@RequestParam Integer dishId, HttpSession session) { User user = (User) session.getAttribute("loginUser"); if (user == null) { return Result.error(401, "未登录"); } try { favoriteMapper.insert(user.getId(), dishId); // 冗余更新dish表收藏数 dishMapper.increaseFavoriteCount(dishId); return Result.success(null); } catch (DuplicateKeyException e) { return Result.error("请勿重复收藏"); } }点赞:类似处理,但还要考虑“同一用户是否点赞过”的判断。如果你不需要做“点赞列表”,甚至可以只存一个冗余计数like_count,在like_record表里记录数据即可。统计字段favorite_count、like_count、view_count都是冗余字段,更新时要通过SQL原子自增UPDATE dish SET favorite_count = favorite_count + 1 WHERE id = #{dishId},而不是先查出来再改回去,否则并发时一定会丢数据。
4.4 美食图片上传:MultipartFile 处理细节
系统中的美食图片上传涉及后端接收、存储、回显三条链路。用Spring MVC的MultipartFile接收文件,核心处理流程如下:
@PostMapping("/admin/dish/upload") public Result upload(@RequestParam("file") MultipartFile file, @RequestParam("dishId") Integer dishId) { // 1. 校验文件类型和后缀 String originalFilename = file.getOriginalFilename(); String ext = originalFilename.substring(originalFilename.lastIndexOf(".")); if (!Arrays.asList(".jpg", ".jpeg", ".png", ".webp").contains(ext.toLowerCase())) { return Result.error("图片格式不支持"); } // 2. 生成不重复文件名 String newFilename = UUID.randomUUID().toString().replace("-", "") + ext; // 3. 保存到本地upload文件目录 String uploadPath = System.getProperty("user.dir") + "/upload/"; File dir = new File(uploadPath); if (!dir.exists()) dir.mkdirs(); file.transferTo(new File(uploadPath + newFilename)); // 4. 构造可供外部访问的URL String url = "/upload/" + newFilename; dishMapper.updateCoverImage(dishId, url); return Result.success(url); }这里有两个坑必须提醒:
- 本地磁盘路径和访问URL是两回事。你把文件保存到了D:\upload,用户浏览器无法访问,必须在
WebMvcConfig里添加静态资源映射,把/upload/**映射到磁盘目录:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + System.getProperty("user.dir") + "/upload/"); } }- 上传大小默认只有1MB。不修改配置的话,稍微大一点的美食图片会直接报错。在
application.yml里加上:
spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB5. 前端Vue打包放进Spring Boot:从开发联调到单机部署
5.1 前后端分离开发,统一打包部署
前端我用Vue + Element UI开发,开发时由Vue CLI启动的dev server监听localhost:8081,接口通过代理转发到后端localhost:8080,这样在开发阶段完全不用处理跨域问题。开发完成后执行npm run build,生成dist目录,这里就是前端所有静态资源的集合。
部署方式有两种,我推荐第二种:
- 分开部署:前端dist丢到Nginx,后端jar单独跑,Nginx里配置反向代理转发
/api。这种方式适合正式上线,但要额外装Nginx。 - 统一打进Spring Boot:直接把dist目录拷贝到Spring Boot项目下的
src/main/resources/static,重新打包成jar。浏览器访问http://localhost:8080时,Tomcat直接把静态资源吐出来,接口走同一个端口,无需跨域,是最省心、最适合答辩演示的部署方式。
把这套前端静态资源纳入Spring Boot的classpath后,spring-boot-starter-web默认会把static目录映射为根路径。实测下来,图片、JS、CSS都能正常加载,接口也能正常访问,部署成本近乎为零。
5.2 路由history模式的坑:刷新404是绕不开的问题
Vue Router如果用默认的hash模式,URL里会带#,不够美观;但切到history模式后,直接访问或刷新http://localhost:8080/dish/12这样的路径,后端找不到对应的Controller,只会返回404。这个问题的根因是:Tomcat在处理非静态资源路径时不会去找前端路由,而是去找后端接口。
解法是在Spring Boot里配置一个转发规则:所有非API、非静态资源的请求统一转发到index.html,让前端路由接管。可以写一个Controller或者实现WebMvcConfigurer的addViewControllers:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { // 前端路由history模式刷新拦截兜底 registry.addViewController("/{path:^(?!api|upload|.*\\..*$).*$}") .setViewName("forward:/index.html"); } }这个配置的意思是把所有不以api开头、不是带点的静态资源文件请求的路径,全部转发回index.html。实测下来必须排除掉upload开头的业务图片路径,避免图片也走前端路由。
5.3 跨域与Session Cookie:开发环境与生产环境的不同处理
开发阶段我通过Vue的devServer.proxy把/api代理到8080,所有请求都是同源的,session cookie自动携带,完全不需要处理跨域。生产阶段前端dist打进Spring Boot后同端口部署,也不存在跨域。所以只要别把前后端分开部署到不同域名,跨域问题基本不存在。
如果你非要前后端分开部署,那就得在后端加CORS配置,并且要额外处理一个容易踩的坑:跨域请求时Session丢失。浏览器跨域请求不会自动带上同源Cookie,需要后端在CORS配置中允许credentials,前端发起请求时也要设置withCredentials: true。这个联动配置很容易漏掉。
@Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOriginPatterns("http://localhost:8081") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowCredentials(true) .maxAge(3600); }6. 实测中的问题排查与系统上线经验复盘
6.1 启动失败与Mapper注入问题:一个链路排查实例
Spring Boot项目启动报错的排查链路基本固定。我做这个项目时遇到过两次启动直接失败,每次都能总结出一个典型原因。
第一次是数据源连接失败。排查链路如下:项目启动时MybatisAutoConfiguration需要拿DataSource,但我的application.yml里spring.datasource.url写错了端口,且没配置驱动类名。报错信息会明确提示Failed to configure a DataSource。遇到这种问题,先检查数据库是否启动、地址端口、账号密码,再检查pom.xml是否引入了MySQL驱动。新版MySQL驱动8.0.x可以省略driver-class-name,但如果遇到版本不匹配,显式加上更稳。我当时把useUnicode=true&characterEncoding=utf8加在JDBC URL里,同时解决了后面要说的中文乱码问题。
第二次是Mapper接口无法自动注入。启动正常,但启动类里注入DishMapper时报Field dishMapper in ... required a bean of type 'DishMapper' that could not be found。根因是Mapper接口没被扫描到。我的包结构是com.example.foodshare.controller、com.example.foodshare.mapper,启动类在com.example.foodshare,理论上能扫到。但Spring Boot默认只会扫描启动类所在包的子包,如果你把Mapper接口放到了包结构之外,就扫不到。解法是在启动类上加@MapperScan("com.example.foodshare.mapper"),或者在每个Mapper接口上加@Mapper注解。这里建议直接上@MapperScan,一劳永逸。
6.2 中文乱码、数据库字符集与MyBatis驼峰映射
这个项目里大量内容涉及中文(美食名称、描述、做法),乱码问题不解决整个系统没法看。乱码可能出现在三个环节:
- 数据库:建库时指定
DEFAULT CHARSET=utf8mb4,注意不是utf8,utf8在MySQL里最多存3字节,遇到特殊字符会报错。 - JDBC连接:URL后面加参数
?useUnicode=true&characterEncoding=utf8。 - 接口返回:在Spring Boot中,
@RequestMapping默认会通过Jackson输出JSON,本身是UTF-8。但如果用HttpServletResponse.getWriter()直接输出字符串,就需要显式response.setCharacterEncoding("UTF-8")。
另一个容易忽略的是MyBatis驼峰映射。数据库字段cover_image对应实体属性coverImage,如果不在application.yml里开启驼峰映射,前端会拿到nullcoverImage但能拿到cover_image,看着就诡异。配置如下:
mybatis: configuration: map-underscore-to-camel-case: true这项配置强烈建议一上来就写上,不然后面每个实体都得靠@Results手动映射,麻烦死。
6.3 定时任务:给系统加一点“每日推荐”的活力
这个系统还可以加一个定时任务来提升“动态感”:每天早上8点自动刷新“今日推荐”美食。这块用Spring Boot自带的@Scheduled就能实现,不引入额外框架。
启动类上加@EnableScheduling,然后在任意Service类上写:
@Component public class DishRecommendTask { @Scheduled(cron = "0 0 8 * * ?") // 每天早上8点执行 public void refreshDailyRecommend() { // 根据浏览量、收藏量、点赞量计算综合分 // 更新dish表中的一个recommended字段,标记今日推荐的几道菜 System.out.println("每日推荐刷新完成:" + new Date()); } }定时任务看起来“小”,但放在答辩演示里是很好的加分项,因为它证明了你不只会写增删改查,还考虑了系统的运营功能。不过要注意一个细节:本地开发时定时任务默认开启,如果不想每次启动都跑,可以在application.yml里加一个自定义开关,用@ConditionalOnProperty控制,这个属于进阶玩法,感兴趣可以后边研究。
6.4 安全问题的最小实现:SQL注入、XSS与日志脱敏
虽然是毕设级项目,但安全和规范意识要到位,我在项目中做了三个最低成本的防御:
SQL注入:MyBatis的#{}预编译能防住绝大多数注入攻击。但如果你用了${}去拼接表名、排序字段,就存在注入风险。我的做法是:所有用户输入字段一律用#{};排序字段这种需要动态拼接的,先做白名单校验,比如只允许传入create_time、view_count等指定字段,传别的直接拒绝。
XSS防御:评论内容是富文本注入的高发区。最直接的方式是后端接口统一处理,对用户提交的content字段做HTML转义或过滤<script>标签。简单的写法是引入commons-text工具包,用StringEscapeUtils.escapeHtml4()转义前后端展示。
日志脱敏:系统打印日志时,用户密码、手机号这些敏感字段一定不要原样输出。我在登录日志里统一是日志:用户username登录,结果success,绝不记录密码。这套习惯放到工作中也是一样的,日志里多留一份明文密码就是多一个泄露渠道。
这个项目做下来之后,我最大的体会是:毕设/实战项目的价值不在于用了多少新技术,而在于你能不能把一个题目拆成清晰的数据模型和接口边界,并且把每个环节的坑填平。从数据库六张表到分页查询,从图片上传到Vue打包部署,每一环都是Spring Boot应用开发中最高频的实战场景,把这些跑通一遍,比你背十遍Spring Boot面试题都管用。最后再送你一个小技巧:上线前记得把日志级别从info调到warn或error,不然跑一天生成的日志文件能占几个G,别问我怎么知道的。