news 2026/10/4 14:01:33

Spring Boot美食分享系统开发实战:从毕设选题到部署上线全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot美食分享系统开发实战:从毕设选题到部署上线全流程

前几天帮一个朋友梳理他手头的毕设项目,题目是《基于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,理由有三个:

  1. 毕设/中小型系统用不到Spring Boot 3的AOT、GraalVM等新特性,选新版本纯属给自己加戏。
  2. 2.7.x还是javax命名空间,网上大量教程、博客都是javax的写法,遇到问题搜出来基本能直接套用,而3.x把javax换成了jakarta,很多老代码需要改import。
  3. 兼容性稳,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 从“内容分享”倒推出来的六张表

数据库设计是这类项目最关键也最容易被轻视的一步。我见过太多人一上来就建一张巨大的“美食表”,把所有字段堆在一起,后期扩展时痛不欲生。合理的表结构应当至少包含六张表:用户表、分类表、美食表、收藏表、评论表、点赞表。

我最终的建表设计选用了如下关键字段(简化版),你可以对照自己的需求调整:

表名核心字段设计说明
userid, username, password, nickname, avatar, role, status, create_timerole区分管理员/普通用户;status用于禁用
categoryid, name, sort, status美食分类,如“汤羹类”“面食类”“肉食类”
dishid, category_id, name, cover_image, description, history, practices, recommended_level, view_count, status, create_time美食主表,history存文化背景,practices存做法步骤
favoriteid, user_id, dish_id, create_time收藏关联表,UNIQUE KEY(user_id, dish_id)防重复收藏
commentid, user_id, dish_id, content, parent_id, create_time支持一级回复逻辑,parent_id默认为0表示顶级评论
like_recordid, user_id, dish_id, create_time点赞记录表,同样加唯一索引

这样做的好处是:用户行为数据与内容数据解耦。美食表不需要存“谁收藏了它”,只需要一个like_count字段做冗余计数;收藏表本身是一张明细流水,既方便查询“我收藏了什么”,也能统计“这道菜被多少人收藏”。反过来,如果非要在美食表里塞一个“收藏用户列表”的JSON字段,查询时就会非常难受,还会破坏数据规范性。

3.2 河南美食分类怎么建模

分类这块是体现项目“河南特色”的小细节。我一开始很随意地建了个“热菜、凉菜、主食、汤”,后来发现完全不对——河南美食的文化逻辑是“地域 + 形态”结合的。我最终落地的分类是:

  • 汤羹类:胡辣汤、洛阳水席的代表菜、酸辣汤等;
  • 面食类:烩面、饸饹面、开封灌汤包、蒸饺等;
  • 肉食类:道口烧鸡、桶子鸡、卤肉、羊肉等;
  • 小吃类:焖子、炒凉粉、芝麻烧饼等;
  • 宴席类:水席整桌、传统宴席中的经典菜。

分类建的合理,前端分类导航、首页“分类推荐”模块、后台分类管理都变得顺理成章。这也是答辩时能拿出来的设计亮点:你思考了领域,而不是抄了一个通用电商模板。

3.3 收藏、评论、点赞三张关联表的取舍

有朋友问“收藏、点赞不都是记录用户对美食的行为吗,合并成一张行为表行不行?”理论上可以用一张user_dish_action表,加上action_type字段区分收藏和点赞。但实际开发中我建议拆开,理由很实在:

  1. 两者的业务语义、查询频率不一样。收藏一般在“个人中心”列表页高频展示,点赞通常只做一个计数的累加。
  2. 分表后SQL简单,索引命中也直观。收藏表查user_id + create_time,点赞表主要查dish_id + user_id校验今日是否已赞。
  3. 后续如果扩展“踩”“分享”“浏览量”等行为,可以理解为“每张行为表都是垂直独立的”,不会互相污染。

在数据量只有几千上万条时,拆表和合表性能上几乎没有差异,但代码可读性和后期维护差别很大。用最简单直接的方式建模,永远是第一原则。

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); }

这里有两个坑必须提醒:

  1. 本地磁盘路径和访问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/"); } }
  1. 上传大小默认只有1MB。不修改配置的话,稍微大一点的美食图片会直接报错。在application.yml里加上:
spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB

5. 前端Vue打包放进Spring Boot:从开发联调到单机部署

5.1 前后端分离开发,统一打包部署

前端我用Vue + Element UI开发,开发时由Vue CLI启动的dev server监听localhost:8081,接口通过代理转发到后端localhost:8080,这样在开发阶段完全不用处理跨域问题。开发完成后执行npm run build,生成dist目录,这里就是前端所有静态资源的集合。

部署方式有两种,我推荐第二种:

  1. 分开部署:前端dist丢到Nginx,后端jar单独跑,Nginx里配置反向代理转发/api。这种方式适合正式上线,但要额外装Nginx。
  2. 统一打进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驼峰映射

这个项目里大量内容涉及中文(美食名称、描述、做法),乱码问题不解决整个系统没法看。乱码可能出现在三个环节:

  1. 数据库:建库时指定DEFAULT CHARSET=utf8mb4,注意不是utf8,utf8在MySQL里最多存3字节,遇到特殊字符会报错。
  2. JDBC连接:URL后面加参数?useUnicode=true&characterEncoding=utf8。
  3. 接口返回:在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,别问我怎么知道的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/4 13:53:12

问卷设计新手避坑指南:90%的人都栽在这五个细节上

第一次做问卷调研的人&#xff0c;几乎都会犯同样的错误&#xff1a;题目写得像聊天、选项重叠或者遗漏、题量长到让人想弃答、引导性问题不自觉带偏、收回来的数据发现根本没法分析。这些坑不是因为你不够聪明&#xff0c;而是因为问卷设计本身就是一门需要训练的技术活&#…

作者头像 李华
网站建设 2026/10/4 13:51:50

Windows Server 2019安装教程:UEFI/GPT分区与驱动排错全指南

简介&#xff1a;Windows Server 2019系统安装教程以图文详解形式呈现&#xff0c;面向需要独立完成服务器部署的运维新手、企业IT人员及培训机构学员&#xff0c;重点解决安装流程不熟悉、分区规划与版本选择易出错等问题。压缩包内仅包含1个PDF文件&#xff0c;大小177KB&…

作者头像 李华
网站建设 2026/10/4 13:51:44

多孔介质生物堵塞的COMSOL PDE数值模拟:从耦合机理到参数标定

做地下水原位修复那阵子&#xff0c;我被一个“越算越堵”的问题折腾了小一个月。说的是生物堵塞&#xff0c;英文常叫 bioclogging——往含水层里注营养液&#xff0c;让土著细菌在砂孔隙里繁殖&#xff0c;形成的生物膜逐渐把孔道填实&#xff0c;渗透率肉眼可见地往下掉。在…

作者头像 李华
网站建设 2026/10/4 13:50:04

RISC-V 入门必读:base ISA 与 ABI 寄存器约定详解

1. 从零上手 RISC-V&#xff1a;为什么 base ISA 和 ABI 寄存器约定是绕不开的第一道坎刚接触 RISC-V 的人&#xff0c;十有八九会卡在同一个地方&#xff1a;指令集手册翻了几十页&#xff0c;每个字母都认识&#xff0c;但连起来就是不知道在说什么。尤其是看到x0到x31这 32 …

作者头像 李华
网站建设 2026/10/4 13:49:10

OpenShell教程:Windows 11下还原经典开始菜单的安装、配置与避坑指南

如果你升级到Windows 11之后看着屏幕左下角那个居中的开始菜单&#xff0c;或者找遍整个系统都找不到“控制面板”入口的时候&#xff0c;心里还惦记着Windows 7那种干净利落的开始菜单&#xff0c;那OpenShell这个名字你应该早就听过了。OpenShell是经典开源项目Classic Shell…

作者头像 李华