做了两年多的Java后端,大大小小的管理系统写过不少,但真正让我把一个项目从零开始完整梳理、把源码整理到可以直接交付给别人跑起来的,还是最近这套Spring Boot美食评价管理系统。这个项目本身不算复杂,但它覆盖了一个典型业务系统从建模、开发到交付的所有关键环节,源码包编号82480,拿到手就能跑,这一点在交付的时候帮了我大忙。这篇博文就把这个项目的核心设计思路、开发过程中踩过的坑、以及源码里几个值得留意的细节一次性说清楚,希望能给正在做Spring Boot毕设、课程设计或者想练手完整项目的朋友一些参考。
1. 为什么做美食评价系统:需求拆解与业务边界
先说清楚这个项目解决的到底是什么问题。市面上已经有美团、大众点评这类成熟的评价平台,功能强大、维度复杂,但对于一个学习型项目或者中小型私域场景来说,那些系统的业务模型太冗余了——什么会员等级、消费返券、商家入驻审核、UGC内容审核,一上来就是几十张表,根本不适合用来理解"一个评价系统最核心的骨架是什么"。
1.1 轻量级美食评价的核心业务闭环
我做的这个系统,业务边界非常清晰,就围绕三个角色和一条主链路展开。
三个角色分别是:游客、注册用户、管理员。游客可以浏览美食信息和查看评价;注册用户除了浏览,还能发布评价、修改自己的评价、收藏喜欢的美食;管理员则负责维护美食信息、管理用户状态、查看系统统计数据。
一条主链路就是:用户登录 → 浏览美食列表 → 查看美食详情 → 发表评价(打分+文字) → 系统更新综合评分 → 榜单排序变化。
这个闭环几乎涵盖了所有评价类系统的通用逻辑,而且每一步都有清晰的CRUD操作和业务规则,特别适合用来展示Spring Boot的开发流程。相比之下,如果一上来就搞商家端、骑手端、运营后台,项目规模翻了三四倍,核心的评价逻辑反而被稀释了。
1.2 源码交付时的模块划分
源码包82480里,我按标准的Maven多模块思路拆分了工程,但考虑到很多朋友拿到的Spring Boot项目都是单模块结构,这个项目最后采用了单模块+清晰分包的方式,比多模块更容易上手。核心分包如下:
config:全局配置类,包括跨域配置、拦截器注册、静态资源映射controller:接口层,接收前端请求、参数校验、返回统一响应service:业务逻辑层,事务控制、评分计算、业务规则校验都在这层mapper(DAO层):数据访问层,本项目用的是Spring Data JPAentity:实体类,与数据库表结构一一对应dto:数据传输对象,避免直接暴露实体给前端,防止循环引用和多余字段泄露common:统一返回结果封装、异常处理、常量定义
这种结构的优势在于,每一层的职责非常单一,排错的时候能快速定位。比如评价发布后评分没更新,那就先看service层的计算逻辑,再看事务边界是否正确,而不会在controller里堆业务代码。
2. 技术选型与工程搭建:Spring Boot版本到底怎么选
技术选型是很多项目一开始就卡住的地方,尤其是Spring Boot的版本问题。这方面我确实踩过教训,得详细说说。
2.1 Spring Boot版本选择的经验教训:不是越高越好
项目初始阶段,我直接选了当时最新的Spring Boot 3.x版本。结果一开工就遇到一连串问题:javax.servlet包名改成了jakarta.servlet,Springfox Swagger 2.x完全不兼容,MyBatis Plus的某些旧版本分页插件直接起飞,连连接池的配置项都变了。
这些问题的本质原因是:Spring Boot 3.x是一次重大版本更新,底层从Java EE规范转向了Jakarta EE 9+规范,大量第三方库必须同步升级到适配版本,否则就会出现类找不到、方法签名对不上、注解不生效等问题。
如果你是学习项目、毕业设计,或者需要快速交付源码给别人跑起来,我强烈建议选择Spring Boot 2.7.x系列。原因很简单:
- 生态兼容性最好。市面上能找到的教程、第三方文档、CSDN博客,绝大多数是基于2.x版本写的。
- 稳定且成熟。2.7.x是Spring Boot 2.x的最后一个版本,官方维护周期长,bug修复充分。
- Java版本要求适中。2.7.x支持Java 8到Java 17,很多人的本机环境就是JDK 8,而Spring Boot 3.x强制要求JDK 17及以上,光这一点就够折腾半天了。
提示:如果你非要上3.x,请确认三件事——JDK环境是17+、所有第三方依赖都已适配Jakarta规范、不再使用Springfox Swagger而是用springdoc-openapi。否则,你可能会花一个下午去解决"为什么注解不生效"这种跟业务无关的问题。
2.2 快速搭建工程骨架的细节
我用了Spring Initializr来生成基础工程,这里有几个细节值得注意:
第一,打包方式选jar而不是war。现在的部署方式基本不依赖外部Tomcat了,Spring Boot内嵌Tomcat直接跑jar包最省事。
第二,依赖别一次加太多。我见过很多人生成项目时勾了一堆依赖,什么Actuator、Web Services、JMS,最后根本用不上,还拖慢启动速度。这个项目只需要:Spring Web、Spring Data JPA、MySQL Driver、Lombok、Validation。安全认证方面也没用Spring Security——因为项目体量小,用拦截器加自定义注解实现简单的登录校验完全够用,而且更容易让初学者理解登录态控制的原理。
第三,Lombok的引入虽然方便,但也埋了一个小坑。eclipse编译器在处理Lombok注解时偶尔会抽风,导致getter/setter找不到。所以我在源码包里的pom.xml中把Lombok的版本显式固定了,避免因为IDE的annotation processing配置问题导致编译失败。
2.3 工程里的几个招牌配置
application.yml是整个项目的核心配置,我写了详细的注释,这里挑几个关键的说说:
server: port: 8080 servlet: context-path: /food spring: datasource: url: jdbc:mysql://localhost:3306/food_rating?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: "123456" driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update show-sql: true open-in-view: false这里有个很重要的坑:spring.jpa.open-in-view默认是true。它会开启OSIV模式,也就是在Controller层也保持EntityManager打开,这会导致事务边界失控、数据库连接长时间占用、在高并发下直接把连接池打满。我在开发过程中遇到过连接池耗尽导致接口全部超时的问题,就是这个设置引起的。所以生产环境一定要设置为false。
ddl-auto: update适合开发阶段,表结构变了自动更新,但生产环境建议改成validate或者none,通过Flyway管理表结构。我在项目中用了一个技巧:开发期用update,部署前手动执行一份schema.sql,避免update误改数据表结构。
3. 数据库建模:评价系统的核心是"评分体系"设计
数据库设计直接决定了一个系统的上限。美食评价系统表面看是增删改查,但真正的核心在"评分数据"上——怎么存、怎么更新、怎么统计,每一步都影响系统的性能和准确性。
3.1 四张核心表的设计思路
我用四张表覆盖整个业务:user(用户表)、food(美食信息表)、evaluation(评价表)、category(美食分类表)。
先看用户表:
CREATE TABLE `user` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE, `password` VARCHAR(100) NOT NULL, `nickname` VARCHAR(50), `avatar` VARCHAR(255), `role` TINYINT NOT NULL DEFAULT 1 COMMENT '1-普通用户 2-管理员', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1-正常 0-封禁', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;密码字段我设置了100个字符的长度,因为存储的是BCrypt加密后的哈希串,不是明文密码。这一点很多初学项目都踩坑——密码字段设置成varchar(20),加密后直接数据库报错。
美食信息表我设计了几个关键字段:name(名称)、image(封面图URL)、category_id(分类ID)、price(人均价格)、address(地址)、description(描述)、avg_score(综合评分)、evaluation_count(评价数量)。
注意这个avg_score字段,它不是用户直接传入的,而是系统根据所有评价动态计算后写入的冗余字段。为什么不用实时聚合查询?因为首页的美食列表需要按评分排序,如果每次查询都去evaluation表做AVG()聚合,随着数据量增长,数据库的压力会指数级上升。用冗余字段存储计算结果,通过事务保证一致性,是典型的空间换时间思路。
3.2 评价表设计:为什么拆成"总分"和"分维度"
评价表的设计我纠结了一段时间,最终采用了"总分+多个维度分"的组合方式。表结构如下:
CREATE TABLE `evaluation` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `food_id` BIGINT NOT NULL, `taste_score` TINYINT NOT NULL COMMENT '口味评分1-5', `environment_score` TINYINT NOT NULL COMMENT '环境评分1-5', `service_score` TINYINT NOT NULL COMMENT '服务评分1-5', `content` VARCHAR(1000) COMMENT '评价内容', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_food_id` (`food_id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;综合评分由三个维度分按权重计算得出,权重比是口味5:环境3:服务2。这个权重不是随便定的,而是参考了餐饮行业用户评价的核心关注点——口味永远是第一位的,环境和服务次之。
可能你会有疑问:为什么不在表里直接存一个总分的冗余字段呢?原因是这样做的维护成本太高。用户只能修改评价内容,三个维度分如果变了,总分就要同步更新,一旦在应用层漏了一处就会造成数据不一致。与其存一个可能出错的总分,不如存三个原始维度分,在service层计算总分再写入food.avg_score里。
评分更新的完整逻辑
每次新增或修改评价,service层执行一个原子操作:
- 根据
food_id查询该美食的所有评价记录 - 累加所有评价的加权总分
- 除以评价总数,得到新的平均分
- 更新
food表的avg_score和evaluation_count字段
如果你觉得每次都全量聚合太慢,可以考虑用增量更新的方式:记录修改前后的分数差异,一次UPDATE完成。但考虑到这个项目的数据量级(单店评价几千条封顶),全量聚合的准确性和可维护性更好,性能损耗可以忽略不计。
4. 核心业务逻辑实现:登录拦截、评价发布与榜单统计
业务逻辑是系统的心脏,这里我挑三个最有代表性的模块详细展开。
4.1 登录拦截器的实现原理解析
我没有引入Spring Security,用的是HandlerInterceptor加自定义注解的方案。自定义了一个@RequireLogin注解,凡是标注了这个注解的Controller方法,在进入业务逻辑之前都会先经过登录校验。
拦截器的核心逻辑如下:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; RequireLogin requireLogin = handlerMethod.getMethodAnnotation(RequireLogin.class); // 从Redis或Session中取登录态 String token = request.getHeader("Authorization"); if (token == null || token.isEmpty()) { throw new BizException(401, "未登录,请先登录"); } User user = userService.getUserByToken(token); if (user == null || user.getStatus() != 1) { throw new BizException(401, "登录状态失效或账号已被禁用"); } // 将用户信息放入ThreadLocal,供后续业务使用 UserContext.set(user); } return true; } }为什么不用Spring Security?因为这个项目的权限模型极其简单——只有"登录/未登录"和"管理员/普通用户"两种维度。引入Spring Security意味着要配置SecurityFilterChain、UserDetailsService、密码编码器、异常处理等一系列东西,对于学习型项目来说反而造成了认知负担。拦截器只需要十几行代码就解决了核心问题,而且所有逻辑一目了然。
注意:用户信息放入ThreadLocal后,一定要在拦截器的
afterCompletion方法里调用UserContext.clear(),否则线程池复用会导致用户信息串到下一个请求上。这个Bug我在测试并发的时候抓过,前端用户A的请求居然拿到了用户B的信息,问题根源就是ThreadLocal没有清理。
4.2 评价发布与校验逻辑
评价发布接口是评价系统的核心链路,涉及多个业务规则。我在设计接口时使用了DTO对象,把前端传入的参数包装起来,并用JSR-303注解做参数校验:
public class EvaluationCreateDTO { @NotNull(message = "美食ID不能为空") private Long foodId; @NotNull(message = "口味评分不能为空") @Min(value = 1, message = "评分最低为1") @Max(value = 5, message = "评分最高为5") private Integer tasteScore; @NotNull(message = "环境评分不能为空") @Min(value = 1, message = "评分最低为1") @Max(value = 5, message = "评分最高为5") private Integer environmentScore; @NotNull(message = "服务评分不能为空") @Min(value = 1, message = "评分最低为1") @Max(value = 5, message = "评分最高为5") private Integer serviceScore; @Length(max = 1000, message = "评价内容最多1000字") private String content; }统一参数校验的@Validated注解配合@RestControllerAdvice全局异常处理,可以有效避免在业务代码里写一堆if-else判断。
这里有一个业务规则需要重点说明:同一个用户对同一家美食只能评价一次。如果是重复评价,就更新原有评价。这个规则在service层实现,具体是先在数据库里查询是否存在user_id + food_id两条维度的记录,如果存在就执行更新,不存在就插入。
有个并发场景我一开始没考虑到:用户快速点击两次"提交评价",产生了两个并发请求,两个请求同时查出"不存在历史评价",结果插入了两条记录。这属于典型的并发幂等问题。解决方式有两种:
第一种是数据库层面加唯一索引:
ALTER TABLE `evaluation` ADD UNIQUE INDEX `uk_user_food` (`user_id`, `food_id`);第二种是应用层加分布式锁或同步锁。考虑到单体应用的场景,加唯一索引是最简单有效的方案,一次到位,从数据库层面杜绝了重复数据。我在源码中保留了唯一索引,并在service层做了DuplicateKeyException的捕获处理,返回友好提示"您已经评价过该美食,重复提交会更新原评价"。
4.3 美食榜单的SQL设计与查询优化
榜单功能是评价系统最有"感觉"的部分:按综合评分排序展示美食列表,支持按分类筛选,评分相同时按评价数量倒序。对应的核心查询:
SELECT f.id, f.name, f.image, f.price, f.address, f.avg_score, f.evaluation_count FROM food f WHERE f.category_id = ? AND f.status = 1 ORDER BY f.avg_score DESC, f.evaluation_count DESC LIMIT 10;这个SQL看起来简单,但在数据量上来之后有一个隐藏的排序性能问题:ORDER BY f.avg_score DESC, f.evaluation_count DESC无法使用索引(因为avg_score是动态更新的冗余字段,值会频繁变化,不适合建索引),MySQL需要做文件排序(filesort)。
我的解决方案分成两步:
第一步,给evaluation_count和status建联合索引,因为status = 1是一个高选择性的过滤条件,可以快速过滤掉下架的美食。
第二步,对于访问量最大的首页榜单,增加一层Redis缓存。key是"rank:category:1",value是榜单JSON数据,缓存时间设置为5分钟。这样即使排序慢一点,也不会直接影响前端响应速度——因为大部分请求都命中缓存。
关于缓存一致性,因为美食评价系统对实时性要求没那么苛刻,5分钟的延迟用户完全感知不到,所以采用简单的定时过期策略就足够了,没有必要引入Canal监听数据库binlog这种重方案。
5. 数据访问层的那些坑:N+1问题与事务失效排查
这一部分是最能体现项目"实战含量"的地方,因为这些问题不是靠看教程就能发现的,必须在真实开发中踩过才会知道。我把整个排查过程记录下来,供大家参考。
5.1 Spring Data JPA还是MyBatis?
先说选型。这个项目用的是Spring Data JPA,没有用MyBatis。原因很简单:
项目涉及的表数量少、关联关系并不复杂,JPA的自动建表、findByXxx方法派发查询、乐观锁@Version支持,可以极大提升开发效率。而MyBatis的优势在于复杂SQL的灵活控制,适合报表、多表联查极其复杂的业务场景。本项目里的SQL基本都围绕单表和简单两表联查,JPA完全能Hold住。
但这里有个典型的JPA坑必须要说:Entity之间的关联关系不要滥用@OneToMany和@ManyToOne。
我第一次设计的时候,给Food实体加了@OneToMany(mappedBy = "food")来映射评价列表,结果在查询美食列表时,JPA自动帮我把每个美食的评价记录也查出来了——这就是教科书级别的N+1问题。
完整排查链路是这样的:
我通过foodRepository.findAll()查询10条美食记录,打开show-sql日志后,发现数据库执行了1条查询+10条查询,一共11条SQL。前1条查的是food表,后10条是JPA懒加载触发的查询,分别查询每个food对应的评价。
数据量小的时候毫无感觉,但当美食数量达到200条时,一个列表接口就会产生201条SQL,响应时间从50ms暴增到2秒以上。这个性能问题极其典型。
解决方案有两种:
第一种方案是使用@EntityGraph,通过注解方式强制JPA使用LEFT JOIN FETCH一次性把关联数据查出来:
@EntityGraph(attributePaths = {"evaluations"}) @Query("select f from Food f where f.categoryId = :categoryId") List<Food> findByCategoryWithEvaluations(Long categoryId);第二种方案是彻底放弃对象关系的懒加载,把DTO查询作为主要手段,通过@Query写法直接联表投影到DTO对象,而不是返回Entity。我在项目中最终采用了第二种方案,因为它应对复杂查询更可控,不会出现"莫名其妙多查了几张表"的问题。
5.2 事务注解@Transactional失效的真实排查场景
Spring Boot评价系统里,评价发布涉及两步写操作:插入评价记录 + 更新美食表的评分。这两个操作必须在同一个事务里,否则会出现"评价记录有了但评分没更新"的数据不一致问题。
我一开始在EvaluationService.evaluate()方法上加@Transactional。结果测试时发现,评价插入成功了,但美食评分没有回滚——模拟第二次写入时报错时,第一次的插入操作已经提交了。
这个问题的排查过程让我记忆深刻。我检查了以下三个位置:
第一步,检查类是不是被Spring管理。@Transactional要生效,类必须被Spring容器托管,而且方法必须通过代理调用。如果你在同一个类的内部调用evaluate()方法(比如this.evaluate()),事务注解就是无效的。这是最常见的失效原因。
@Service public class EvaluationService { public void evaluate(EvaluationCreateDTO dto) { // 这里会触发事务 createEvaluation(dto); updateFoodScore(dto.getFoodId()); } @Transactional public void createEvaluation(EvaluationCreateDTO dto) { // 数据库插入 } }上面的写法,createEvaluation()的事务是失效的,因为外部调用是从evaluate()进入的,Spring的AOP代理只对"从外部进入被代理对象的方法"生效,内部方法调用不会走代理。
第二步,检查事务是否真的回滚了。默认情况下,@Transactional只在抛出RuntimeException时回滚,受检异常(checked exception)不会触发回滚。
@Transactional public void updateFoodScore(Long foodId) throws Exception { // 抛出受检异常,不会回滚 }第三步,检查数据库引擎。MySQL的MyISAM引擎不支持事务,InnoDB才支持。如果你的表是MyISAM,任何事务配置都不会生效。这个项目里所有表我都指定了ENGINE=InnoDB。
最终的解决方案是:把两步写操作调整到一个方法里,在类外部调用,确保走Spring代理,同时使用@Transactional(rollbackFor = Exception.class)显式指定所有异常都回滚。
5.3 数据库连接池耗尽问题的定位
前面提过open-in-view: false的问题,这里展开说说是怎么发现的。
项目开发初期,接口偶尔出现"连接超时"的报错。我检查了数据库连接池的状态,发现HikariCP的活跃连接数一直居高不下,而且趋势图显示连接在某个时间点直线上升。
排查思路是这样的:
先看代码里有没有未关闭的资源。用户的Service里使用JdbcTemplate手动查询了一些统计功能,个别方法没有在finally块中释放连接。JPA的用法也值得怀疑——如果实体在视图渲染过程中被懒加载,连接就不会被释放。
我用Arthas在线诊断工具做了线程栈分析,发现大量线程卡在com.mysql.cj.jdbc.ConnectionImpl的读操作上,进一步看调用栈,发现是视图层在遍历美食对象时触发了评价集合的懒加载。
这就印证了:open-in-view: true导致Hibernate在Controller层甚至视图渲染层都保持着数据库会话,一个请求从进入到响应完成,连接始终被占用——在并发量上来之后,连接池就原地爆炸了。
设置open-in-view: false之后,连接占用时间立刻大幅下降。同时,我把视角层需要的关联数据在Service层就通过DTO一次性查询出来,彻底断绝了在视图层触发懒加载的可能。
6. 源码包完整解读与实际部署指南
最后这部分,聊聊源码包82480的内部结构和部署细节。对拿到源码的人来说,这是最关心的事情——怎么让项目顺利跑起来。
6.1 源码目录结构说明
压缩包解压后,核心目录如下:
springboot-food-rating/ ├── src/main/java/com/example/foodrating/ │ ├── FoodRatingApplication.java // 启动类 │ ├── config/ // 配置类 │ │ ├── WebConfig.java // 拦截器注册、跨域配置 │ │ └── UserContext.java // 用户上下文ThreadLocal │ ├── controller/ // 接口层 │ │ ├── AuthController.java // 登录注册接口 │ │ ├── FoodController.java // 美食信息接口 │ │ └── EvaluationController.java // 评价接口 │ ├── service/ // 业务逻辑层 │ ├── repository/ // JPA数据访问层 │ ├── entity/ // 实体类 │ ├── dto/ // 请求响应对象 │ ├── common/ // 统一响应、异常处理、工具类 │ └── interceptor/ // 登录拦截器 ├── src/main/resources/ │ ├── application.yml // 核心配置 │ ├── schema.sql // 数据库初始化脚本 │ └── data.sql // 测试数据 └── pom.xml有几个细节我特意在代码里加了注释,分别是:
WebConfig.java里的跨域配置,如果不配,前端页面在8081端口调用8080接口会被浏览器拦截。GlobalExceptionHandler.java里对不同异常类型的处理顺序,顺序错了会导致Detail消息被吞掉。UserContext.java的ThreadLocal清理逻辑。
6.2 从零启动到跑通全流程的完整步骤
如果你拿到源码想在自己电脑上跑起来,按下面的顺序一步步来不会出错:
第一步,准备环境。JDK 8或11、Maven 3.6+、MySQL 5.7+。这个项目不依赖Redis(拦截器用的是本地Session机制简化了部署),所以即使你完全没有Redis环境也能跑通。
第二步,创建数据库。登录MySQL后执行:
CREATE DATABASE IF NOT EXISTS food_rating DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;然后直接执行源码包里的schema.sql和data.sql,里面已经包含了表结构初始化、默认管理员账号(admin/admin123)和几组测试美食数据。
第三步,修改配置文件。打开application.yml,把spring.datasource.username和password改成你本机的数据库账号密码。
第四步,启动项目。在项目根目录执行:
mvn spring-boot:run或者先打包再运行:
mvn clean package -DskipTests java -jar target/food-rating-0.0.1-SNAPSHOT.jar启动成功后会看到Spring Boot的Logo和"Tomcat started on port(s): 8080"的日志。浏览器访问http://localhost:8080/food/api/food/list,能看到测试美食列表数据,就说明项目已经正常工作了。
6.3 实际部署时容易忽略的几个细节
这里分享几个部署中的隐性坑,可能帮大家避免一些不必要的折腾。
第一,MySQL时区问题。如果你在application.yml里配置的数据库连接串没有加serverTimezone=Asia/Shanghai,高版本的MySQL驱动会报The server time zone value错误。我在源码里已经加上了,但如果你自己新建项目,这个非常容易忘记。
第二,文件上传的目录问题。美食图片上传后保存到服务器本地磁盘,我用了一个绝对路径配置项来指定上传目录。如果你把项目部署到Linux服务器上,一定要先创建这个目录并且设置好读写权限,否则上传接口会直接报FileNotFoundException。Windows开发环境默认路径和Linux不一致,接手源码的人在这个地方踩坑的频率最高。
第三,Maven依赖的镜像配置。国内网络环境下,从Maven中央仓库拉取依赖经常超时。我建议在~/.m2/settings.xml中配置阿里云镜像,这个项目我实测过,配置镜像后依赖下载速度能提升5倍以上。
第四,默认端口和context-path。项目的服务端口是8080,context-path是/food。所以接口的完整访问路径是/food/api/...。如果你修改了端口或者删除了context-path,前端页面的请求地址也要对应调整,否则登录接口404了不要惊讶。
第五,前后端联调时的Cookie问题。这个项目的登录态是通过请求头Authorization传递的,不是Cookie。如果你用Postman测试登录接口,登录成功后会返回一个token,需要在后续请求中手动加到请求头里。如果不用Header而是依赖Cookie,你会遇到"登录成功但访问用户信息接口还是401"的诡异问题。
6.4 源码交付后的测试建议
最后聊聊测试。源码包里我放了一个简单的test/目录,里面有几个基础的单元测试,但覆盖度有限。如果你想把这个项目当成毕设或者进一步扩展,建议补齐以下三类测试:
第一类,Service层测试。重点覆盖评价发布流程,包括重复评价、评分越界、美食不存在等异常场景。
第二类,并发测试。模拟同一用户同时提交多次评价,验证唯一索引是否兜住了并发问题。
第三类,接口联调测试。用Postman或ApiPost把核心接口串起来跑通全流程。这是交付验收环节最常遇到的问题,我自己在交付前会跑一遍完整的"注册→登录→浏览美食→发布评价→修改评价→查看榜单"流程,确认没有任何一环断裂。
在测试评分更新一致性的时候,我自己写过一个小的JUnit测试:先插入5条评价,断言美食的avg_score正确;然后更新其中一条评价的分值,再断言评分跟着变了。这类测试看起来简单,但它能防止最基础的数据一致性Bug被带到生产环境。
7. 写在最后:一个项目的价值在于"可复现"
说句实在话,美食评价管理系统在技术上并没有用到什么高深的东西,Spring Boot全家桶、JPA、MySQL、拦截器,每一样都是Java开发者的日常工具。但这个项目的价值在于,它把一个完整的业务场景从头到尾串了起来——从数据库设计到接口开发,从性能问题排查到部署上线。
如果你正在学Spring Boot,我的建议是不要只看教程,动手把这类项目完整做一遍。重点体会几个容易忽略的地方:第一,事务注解为什么有时候不生效;第二,JPA的对象关系映射为什么会引发出乎意料的SQL;第三,一个简单的评分字段为什么会牵扯到数据一致性设计。如果能把这几个关键点真正想透,你对Spring Boot的掌握程度会超出大多数只看视频的初学者。
这套源码82480我已经整理了完整的运行说明文档,数据库脚本、测试数据、部署指引都在包里。希望这篇博文的详细拆解,能帮你更快地把它跑起来,然后按照自己的需求去改造、扩展,让这个项目真正成为你自己的作品。