
前几天帮一个朋友梳理他手头的毕设项目题目是《基于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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies !-- Web支持内嵌Tomcat、Spring MVC、Jackson -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis整合 -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency !-- MySQL驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version scoperuntime/scope /dependency !-- Lombok减少样板代码 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency !-- PageHelper分页插件 -- dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.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字段区分收藏和点赞。但实际开发中我建议拆开理由很实在两者的业务语义、查询频率不一样。收藏一般在“个人中心”列表页高频展示点赞通常只做一个计数的累加。分表后SQL简单索引命中也直观。收藏表查user_id create_time点赞表主要查dish_id user_id校验今日是否已赞。后续如果扩展“踩”“分享”“浏览量”等行为可以理解为“每张行为表都是垂直独立的”不会互相污染。在数据量只有几千上万条时拆表和合表性能上几乎没有差异但代码可读性和后期维护差别很大。用最简单直接的方式建模永远是第一原则。4. 后端核心接口实战注册登录、分页查询、收藏点赞与图片上传4.1 注册登录流程密码加盐与前后端会话保持用户模块是几乎所有系统的起点。这里我会讲两种落地方案并说明为什么最终选择其中一个。密码存储禁止明文存密码至少要做MD5加盐。所谓加盐就是在用户密码后面拼接一段随机字符串再做MD5散列这样即使用户A和用户B密码相同数据库里的密文也不同能有效防止撞库。不过MD5本身已不被推荐做密码哈希毕设/中小项目里我会直接建议用BCryptPasswordEncoderSpring 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;charsetUTF-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); ListDishVO dishList dishMapper.selectDishList(categoryId, keyword); PageInfoDishVO pageInfo new PageInfo(dishList); return Result.success(pageInfo); }对应的Mapper XML里动态查询就这样写select idselectDishList resultTypecom.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 testcategoryId ! null AND d.category_id #{categoryId} /if if testkeyword ! 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的addViewControllersConfiguration 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但如果遇到版本不匹配显式加上更稳。我当时把useUnicodetruecharacterEncodingutf8加在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 CHARSETutf8mb4注意不是utf8utf8在MySQL里最多存3字节遇到特殊字符会报错。JDBC连接URL后面加参数?useUnicodetruecharacterEncodingutf8。接口返回在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别问我怎么知道的。