距离毕业设计答辩还有两周,系统功能倒是写得差不多了,但论文配图缺一张数据库E-R图,管理员统计报表的饼图样式和导师要求不一致,前端同事催接口文档催到群里@我三次。这些场景,做过Java Web毕设的同学应该都不陌生。如果你正在做或准备做“基于Java+SpringBoot的旅游信息平台”这类选题,尤其是把“源码+讲解视频+LW”作为交付物的话,这篇文章把我从项目结构搭建到最终上线部署踩过的坑、总结的经验全部拆开讲清楚,从需求分析到数据库设计,从接口联调到部署上线,每一段都有可直接复用的代码片段和配置,希望能帮你省下几个通宵。
这个项目表面上看是一个典型的SpringBoot单体应用,但真正做完你会发现,它其实是一条完整的Web开发实践链路:Java基础、SpringBoot自动装配原理、MyBatis操作MySQL、JWT无状态认证、Redis缓存、前端Vue渲染,再到Docker部署。整个项目做下来,SpringBoot的核心知识点基本都能覆盖。对于准备校招面试的同学来说,这些内容也刚好是面试八股文里反复出现的高频考点,比如SpringBoot自动装配怎么实现的、JDK版本升级对框架有没有影响、单元测试怎么编写等等,后面我会结合项目实际一一说明。
1. 项目整体设计与思路拆解
1.1 核心需求解析:这个旅游平台到底要做什么
云南的旅游资源丰富,大理、丽江、香格里拉、西双版纳、腾冲、普者黑这些热门目的地,每年吸引大量自由行和跟团游客。我选这个选题的出发点很简单:找一个“业务场景足够真实、功能边界足够清晰、Table结构不会复杂到失控”的领域。旅游信息平台恰好符合这三个条件。
它的核心用户有两类:普通游客和后台管理员。游客端需要的是什么?查景点、看路线、找攻略、收藏目的地、发表评论,最好还能有一个行程规划的小工具。管理员端则是管理景点信息、审核评论、统计数据、发布公告。这里要注意,我刻意没有做在线支付和订单交易,原因很简单:一旦接入支付,你就要处理退款、对账、商户号申请等一系列复杂度,这对一个本科毕设来说,很容易让项目烂尾。做信息展示和预订意向收集,既体现了业务完整性,又把复杂度控制在了可控范围内。
所以在跟导师确认需求时,我梳理了四个核心功能模块:
- 景点信息管理:景点的CRUD(增删改查),包括图文介绍、开放时间、门票价格、地理位置标记。
- 旅游线路推荐:系统根据热门程度和用户访问量,在首页展示推荐线路,支持关键词模糊查询和多条件筛选。
- 用户互动体系:用户注册登录后可以收藏景点、发表评论、给景点评分,还能在个人中心查看自己的收藏列表和评论历史。
- 数据统计看板:管理员端通过ECharts图表展示景点访问量趋势、热门景点TOP5、用户增长曲线,这些可视化图表在论文的“系统实现”章节里是非常加分的配图。
提示:项目命名不要叫demo或者test,我用的是
yunnan-travel-platform。论文里写系统名称时也保持一致,答辩时导师第一眼看到的就是你的项目名称和代码结构,规范命名能省掉很多解释成本。
1.2 技术方案选型:为什么是SpringBoot而不是SSH或SSM
选题确定后,最纠结的就是技术栈。学校课程里教的还是Servlet + JSP那一套,但我直接放弃了这个选项。原因很现实:第一,Servlet手写Web层代码,配置web.xml、写过滤器、手动封装JSON,光这些重复劳动就要耗费大量时间;第二,现代企业开发中SpringBoot已经基本成为Java后端的事实标准,毕设用SpringBoot不仅代码量更少,也能向答辩老师证明你跟上了技术发展。
SpringBoot最核心的价值在于自动装配和约定优于配置。比如你要引入Web模块,以前用SSM要配置DispatcherServlet、CharacterEncodingFilter、ViewResolver等一堆Bean,SpringBoot只需要加一个 spring-boot-starter-web 依赖,内嵌的Tomcat会自动启动,所有常规配置都是默认值。这个“自动装配”的实现机理其实很值得深挖,也是面试官特别喜欢问的一个点。
SpringBoot的自动装配核心是 @EnableAutoConfiguration 注解,它通过 @Import(AutoConfigurationImportSelector.class) 导入所有候选配置类。这背后依赖SpringFactoriesLoader机制,SpringBoot在运行时读取META-INF目录下的spring.factories文件,把其中列出的所有AutoConfiguration类加载进来,然后通过 @ConditionalOnClass、@ConditionalOnMissingBean 等条件注解判断是否需要生效。比如 DataSourceAutoConfiguration 里面有一个 @ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class}),也就是说只有当classpath下存在数据库驱动依赖时,数据源自动配置才会生效。
我当时为了弄清楚这段原理,专门去翻了SpringBoot的源码(在spring-boot-autoconfigure包下面),对照着IDE里的ConditionEvaluator日志把启动过程过了一遍。做完这个项目以后,面试官再问“SpringBoot自动装配原理”这类问题,你就能从spring.factories、条件注解、Bean的实例化顺序三个层面去回答,而不是只会背结论。
1.3 项目结构规划与目录设计
项目采用了经典的分层架构:Controller(接口层)、Service(业务层)、Mapper(数据访问层)、Entity(实体类)、DTO(数据传输对象)、Config(配置类)。我按照这个结构组织目录,既没有过度设计,又保证了代码清晰度。
txt复制yunnan-travel-platform/
├── src/main/java/com/example/yunnantravel/
│ ├── controller/ # 控制层,接收请求,返回统一响应结果
│ ├── service/ # 业务层,处理核心业务逻辑
│ │ └── impl/ # 业务实现类
│ ├── mapper/ # MyBatis的Mapper接口,对应数据库操作
│ ├── entity/ # 实体类,与数据库表字段一一对应
│ ├── dto/ # 数据传输对象,避免直接暴露实体
│ ├── config/ # 配置类(跨域、拦截器、Redis序列化等)
│ ├── common/ # 统一返回结果、异常处理、工具类
│ └── YunnanTravelApplication.java # 启动类
├── src/main/resources/
│ ├── mapper/ # MyBatis XML映射文件
│ ├── static/ # 静态资源(前端打包后的文件)
│ ├── application.yml # 主配置
│ └── application-dev.yml # 开发环境配置
└── src/test/java/ # 单元测试目录
很多同学做项目喜欢把全部代码堆在Controller里,一个类几百行,虽然省略了设计步骤,但写论文架构图的时候就很尴尬,因为图里画不出层次。我建议至少分出Controller、Service、Mapper三层,每一层只负责自己的事。这不仅是毕设的礼仪,也是公司Code Review的底线。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计与核心表结构实现
2.1 数据表设计与关系梳理
数据库我用的是MySQL 8.0,图形化管理工具用Navicat。设计表结构前,我先把所有业务对象罗列出来,理清它们之间的关系,这个步骤直接决定了论文里E-R图的完整性。
整个系统一共设计了8张表:
| 表名 | 说明 | 关键字段 |
|---|---|---|
user |
用户表 | id, username, password, nickname, avatar, phone, create_time |
scenic |
景点表 | id, name, description, address, open_time, ticket_price, image, city, status |
route |
线路推荐表 | id, title, summary, cover, day_count, scenic_ids, create_time |
comment |
评论表 | id, user_id, scenic_id, content, score, create_time |
favorite |
收藏表 | id, user_id, scenic_id, create_time |
category |
景点分类表 | id, name, sort |
notice |
公告表 | id, title, content, create_time |
admin_user |
管理员表 | id, username, password, last_login_time |
表结构设计有几个细节需要特别说明。
首先是用户表和评论表的关系,一对多。一个用户可以发表多条评论,一条评论只属于一个用户。在 comment 表里通过 user_id 外键关联用户表。其次是景点表和多张表的关联,收藏表和评论表都承载了与景点表的关联。第三是 route 表的 scenic_ids 字段,我用了VARCHAR类型存储逗号拼接的ID串(如"1,3,5"),而不是单独建一张线路景点关联表。这是斟酌后的取舍:线路和景点的关联基本是固定组合,查询时一条SQL拿到ID串再拆解,效率够用,同时少一张表可以简化E-R图,对于毕设来说逻辑更直观。
密码存储必须强调:不能明文存储。我用的是Spring Security Crypto模块的BCrypt加密,比如原始密码是123456,存到数据库里是长这样的哈希值:
txt复制$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2
BCrypt算法内置随机盐,每次加密结果都不同,但从数据库验证时只需要用 BCryptPasswordEncoder.matches(rawPassword, encodedPassword) 就能校验。这个细节写进论文和答辩演示时非常加分,能体现安全意识。
2.2 数据库连接的配置与优化
在application.yml里,我配置了开发环境的数据库连接。这里有点要提醒:MySQL 8.0的JDBC驱动类名和5.x版本不同,驱动是 com.mysql.cj.jdbc.Driver,并且URL中必须带上 useSSL=false&serverTimezone=Asia/Shanghai 参数,否则控制台会报时区错误。
yaml复制spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/yunnan_travel?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: "你的数据库密码"
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: Asia/Shanghai
字符集必须显式指定为utf8,否则景点介绍里的中文和表情符号(emoji)容易出现乱码。如果你要在景点描述里放emoji,数据库表字段要设置成utf8mb4字符集,因为utf8在MySQL里最多支持3字节,存不了4字节的emoji。
还有连接池的问题。SpringBoot 2.x默认使用HikariCP连接池,它非常快,基本不需要额外调优。但有一个参数我推荐设置一下,就是 maximum-pool-size,默认是10。毕设项目并发量不高,10个连接足够用;如果你的项目要在云服务器上跑,建议把 connection-timeout 设置为30000,防止网络抖动导致连接获取超时。
2.3 MyBatis还是MyBatis-Plus
这是很多同学纠结的问题。我最终的方案是:用MyBatis-Plus,但核心业务SQL还是手写在XML里。
为什么?MyBatis-Plus内置了通用的CRUD方法,单表操作根本不用写SQL,比如保存一个实体类,直接 scenicMapper.insert(scenic) 就完成了,极大提高开发效率。但复杂查询(比如列表分页+多条件筛选+按城市分组统计)手写SQL更可控。这两种方式在项目里共存没有任何冲突。
举一个例子,热门景点TOP5这个统计需求,用MyBatis-Plus的LambdaQueryWrapper很难优雅实现,因为你得先查评论表,按景点ID分组,统计数量后再排序,还要关联景点表拿详情信息。但写一条SQL就很简单:
sql复制SELECT s.id, s.name, s.city, s.ticket_price, COUNT(c.id) AS comment_count
FROM scenic s
LEFT JOIN comment c ON c.scenic_id = s.id
GROUP BY s.id
ORDER BY comment_count DESC
LIMIT 5;
这条SQL放到resource/mapper/ScenicMapper.xml里,Mapper接口对应写一个方法 selectHotScenics(),调用时直接返回结果列表。
3. 核心功能实现与实操过程
3.1 统一返回结果与全局异常处理
开发接口时,前端同学(或者自己写的前端页面)最怕遇到这种情况:后端接口返回的数据结构不统一,有时返回一个对象,有时返回一个布尔值,有时直接抛异常一堆报错信息,解析无从下手。所以项目中最先搭建的是统一返回结果类和全局异常处理器。
统一返回结果类,我命名为 Result<T>,由三部分组成:code编码、message提示信息、data业务数据。成功时code为200,失败时code为500,未登录时code为401。所有接口都返回这个结构,前端解析逻辑就能统一。
java复制@Data
public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("操作成功");
result.setData(data);
return result;
}
public static <T> Result<T> error(String message) {
Result<T> result = new Result<>();
result.setCode(500);
result.setMessage(message);
return result;
}
}
全局异常处理使用 @RestControllerAdvice 注解。这个注解的作用是拦截所有Controller中抛出的异常,统一转化成Result结构返回。如果没做这个处理,数据库查询报错时直接就返回500的大堆异常栈给前端,既不安全也不友好。
3.2 JWT认证机制实现
用户登录认证我选择了JWT(JSON Web Token),而不是传统的Session方案。原因是SpringBoot内嵌Tomcat默认对Session和Cookie的支持虽然好用,但前端是Vue项目时,前后端分离后会产生跨域问题,操作Cookie(尤其是携带认证信息时)还要处理跨域证书等问题,很麻烦。JWT本身是无状态的,后端不需要存储Session,用户登录成功后服务器签发一个Token给前端,前端在请求头里带着Token访问需要认证的接口,后端校验Token有效即可。
JWT的结构是三段Base64编码的字符串,用点号分隔:Header.Payload.Signature。
Header指定签名算法(默认HS256)和Token类型,Payload存放自定义声明,比如userId、username、过期时间等,Signature则用服务器保存的密钥对header和payload签名。
我用的JWT库是 io.jsonwebtoken:jjwt,引入依赖后,封装一个JwtUtil工具类,包含两个核心方法:生成Token和解析Token。
java复制public class JwtUtil {
// 密钥,实际项目应该放在配置文件中,不要硬编码在代码里
private static final String SECRET_KEY = "your-secret-key-please-change-in-production";
// 过期时间:24小时
private static final long EXPIRE_TIME = 24 * 60 * 60 * 1000;
public static String generateToken(Long userId, String username) {
Date now = new Date();
Date expireDate = new Date(now.getTime() + EXPIRE_TIME);
return Jwts.builder()
.setSubject(username)
.claim("userId", userId)
.setIssuedAt(now)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
public static Claims parseToken(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
}
}
生成Token之后,需要写一个拦截器来统一校验。SpringBoot提供了 HandlerInterceptor 接口,我们实现 preHandle 方法,在方法里从请求头 Authorization 字段获取Token,然后解析。解析成功就把userId放到request的attribute中,方便Controller取用;失败则直接返回401状态码。
注意:JWT密钥不要硬编码,我上面写的是演示,实际项目要放到application.yml里通过
@Value注入,否则代码上传到公开仓库之后,任何人都能用这个密钥伪造Token。
3.3 景点管理的完整功能闭环
景点管理是系统的核心业务,包含两部分:前台用户浏览和后台管理员维护。
前台用户可以按城市筛选景点。我实现了一个 GET /api/scenic/list 接口,接收三个参数:城市city(可选)、关键词keyword(可选)、页码pageNum和每页条数pageSize。查询逻辑在Service层组装:如果city不为空,就拼上城市的等值条件;如果keyword不为空,就对name和description做模糊查询;最后按创建时间倒序排序,进行分页。
后台管理员登录后,可以对景点执行增删改操作。新增景点时,景点图片我先上传到本地服务器某个目录(如 /upload/scenic/),再把访问路径存到数据库的image字段。这里要特别注意:SpringBoot默认静态资源路径不包含外部目录,所以要么配置 spring.web.resources.static-locations,要么写一个自定义的 WebMvcConfigurer 把 /upload/** 映射到本地磁盘路径。我当时是改的配置文件,一行搞定:
yaml复制spring:
web:
resources:
static-locations: classpath:/static/,file:${upload.path}
这样图片上传后,访问地址就是 http://localhost:8080/upload/scenic/xxx.jpg,不占用后端代码生成权限,也没有跨域问题。
3.4 旅游线路推荐的实现逻辑
旅游线路推荐这个功能,最开始我想得复杂了,觉得应该上协同过滤算法或基于内容的推荐。后来冷静下来分析:毕设项目没有用户历史行为大数据,冷启动问题根本解决不了,硬上推荐算法只会让代码变得臃肿,而且没有足够数据根本看不出效果。最后选择的方案是:“人工运营+规则排序”。
管理员在后台发布线路时,可以给线路设置“是否推荐”的标记字段 is_recommend。前台首页展示推荐线路时,SQL查询条件是 is_recommend = 1,然后按浏览量降序排列。同时我对每条线路记录一个浏览量字段,每次用户点击详情,浏览量加一。这个方案简单有效,还在答辩时可以跟导师解释“将关键的业务决策保留给运营人员,技术负责高效展示”,很合理。
如果你想让这个模块显得更有技术含量,可以增加一个“相似线路推荐”功能:根据当前线路的 city 字段,查出同一个城市或邻近城市(也就是城市名称相同)的其他线路,排除当前线路,取4条作为“猜你喜欢”。SQL就是一条简单的等值查询加limit,非常容易实现,但在演示时可以营造一种“系统具有智能推荐能力”的感觉。
3.5 评论、收藏与个人中心
评论功能是游客互动的重要手段。我设计的表结构允许用户对景点进行打分(1-5分,其中5分是好评),所有评论在个人中心可以直接看到。
发布评论接口的调用链是:前端提交 scenicId 和 content、score,后端从拦截器设置好的request attribute中获取当前登录用户的userId,然后组装评论实体类,校验分数在1到5之间,再执行插入。为了防刷,我在插入前会先查询该用户是否已经评论过当前景点,如果评论过就提示“您已评论过该景点”。这个业务规则在论文的“系统详细设计”章节里是很好的文字素材。
收藏功能的逻辑稍微复杂一点:用户点击收藏按钮时,前端先判断用户是否登录(通常用是否存在Token来判断),然后调用 POST /api/favorite/toggle,后端先判断是否已经收藏,如果没有就插入,有就删除,这就是“收藏/取消收藏”的切换效果。个人中心展示收藏列表时,我用了JOIN查询一次性把scenic表的名称、封面图、价格带出来,避免前端拿到一堆scenicId后再逐个查询,减少网络请求次数。
3.6 管理端数据统计看板
数据统计看板是我论文里最满意的一个部分,因为视觉效果最好。我用ECharts实现图表,后端提供两个统计接口。
第一个接口是“近7天用户访问量趋势”。这里我建了一张 visit_log 表,每次用户访问首页时,异步记录一条访问日志,包含访问时间和IP。统计时按天分组,用MySQL的 DATE_FORMAT(create_time, '%Y-%m-%d') 格式化日期,再COUNT出每天的访问量,返回连续7天的数组。前端用ECharts的折线图展示。
第二个接口是“热门景点TOP5排行榜”,SQL就是前面2.3节写的那条评论计数查询。前端用ECharts的饼图展示。
实操心得:ECharts图表初始化时,
<div>容器必须有明确的高度和宽度。我在写前端时把图表容器的高度设成了40vh,结果在部分笔记本上显示很矮,数据饼图挤成一团。后来统一改成固定像素高320px,所有屏幕显示一致,不再出问题。
4. 项目部署、常见问题与排查实录
4.1 SpringBoot版本和JDK版本怎么选
这一步很多同学在项目一开始就会踩坑。当前最新的SpringBoot 3.x要求JDK17,而很多学校教学和大部分企业的线上环境还在用JDK8。如果你用的是JDK8,只能选择SpringBoot 2.7.x版本。这两个版本之间的差异非常大:SpringBoot 3.x基于Jakarta EE规范(原先的javax前缀改成了jakarta),整合第三方组件时也容易出现版本兼容问题。比如你用SpringBoot 3.x + MyBatis-Plus,就要用MyBatis-Plus的3.5.3以上版本,否则启动会直接报ClassNotFoundException。
我在做这个项目时用的是SpringBoot 2.7.6 + JDK8 + MyBatis-Plus 3.5.3。原因很简单:JDK8在企业中还非常常见,很多面试题都在问JDK8的HashMap和ConcurrentHashMap实现原理,用这个组合写毕设,兼容性最好,部署到服务器上也最省心。如果你刚从网上下载了最新的SpringBoot 3.x代码,在本机启动莫名报错,大概率就是JDK版本和框架版本不匹配导致的。
4.2 启动类报错排除实录
项目启动时报错,最常见的是以下几种:
场景一:端口被占用
启动日志最后几行会出现类似 Web server failed to start. Port 8080 was already in use. 的报错。排查方法:Windows上执行 netstat -ano | findstr 8080,找到占用端口的PID,然后 taskkill /F /PID 进程号。Linux/Mac上执行 lsof -i:8080。
场景二:MyBatis-Plus多数据源或Mapper扫描不到
报错信息通常是 Invalid bound statement (not found): com.example.mapper.UserMapper.selectById。这个报错的原因大多是Mapper接口和XML文件没有对应上。我的排查思路是:先确认Mapper接口上有 @Mapper 注解,或者在启动类上加了 @MapperScan("com.example.mapper");再确认XML文件在resources/mapper目录下,并且XML文件中的namespace属性写的是接口的全限定名;最后检查配置文件里是否写了 mapper-locations: classpath*:/mapper/**/*.xml。这三个地方任何一处不对,都会报这个错。
场景三:MySQL版本不一致导致驱动报错
如果是 java.sql.SQLException: Unknown database,说明连接地址配错,检查一下URL里的数据库名有没有创建。如果是 Communications link failure,多半是数据库服务没启动。前一段时间就遇到一个同学,他本机装的是MySQL 5.7,但是从网上下载的项目代码里用的驱动类是MySQL 8.0的 com.mysql.cj.jdbc.Driver,5.7也能识别这个驱动,但真正导致连接失败的是url里没有加 serverTimezone 参数,加上就好。
4.3 前后端联调时典型的跨域问题
同学们用Vue开发前端时,经常遇到跨域报错:Access to XMLHttpRequest at 'http://localhost:8080/api/...' from origin 'http://localhost:8081' has been blocked by CORS policy。
这个问题的原因在于:前端项目运行在8081端口,后端在8080端口,两个端口不同便构成了跨域。后端的解决办法是写一个CORS配置类,允许来自前端的请求访问任意接口。
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
还有个很容易踩的坑:当你使用了自定义拦截器之后,每次前端请求都会先发一个OPTIONS预检请求,如果拦截器没有放行OPTIONS请求,前端就一直报跨域错误。解决方法是拦截器判断请求方法:
java复制if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return true;
}
4.4 部署到云服务器的完整流程
毕设答辩前,我把项目部署到了一台轻量云服务器上。流程不算复杂,但涉及不少细节,这里分享一个可以直接照搬的步骤:
第一步:打包SpringBoot项目。 在项目根目录执行 mvn clean package -DskipTests,打包成功后在target目录下生成 yunnan-travel-platform-0.0.1-SNAPSHOT.jar。
第二步:安装JDK。 服务器上没有JDK8的话,用命令安装。注意在Linux服务器上查看已安装的JDK版本要用 java -version,根据之前的选择装JDK8。
第三步:安装MySQL并把本地数据导入。 服务器上装好MySQL之后,创建同名数据库,然后把你本地导出的SQL文件上传到服务器,用 mysql -u root -p yunnan_travel < yunnan_travel.sql 导入数据。导出SQL时,建议勾选“包含建库语句”,这样服务器端都不用手动建库。
第四步:启动应用。 nohup java -jar yunnan-travel-platform-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod > app.log 2>&1 &,不挂断地启动项目,并将日志输出到app.log,方便排查问题。
第五步:开放端口。 云服务器控制台安全组放行8080端口,同时检查系统防火墙,否则外部访问不到。
注意:第一次部署时,项目配置连接数据库的密码要改成服务器上的实际密码,否则启动时数据源初始化就报错,日志里能直接看到密码认证失败的异常信息。
4.5 单元测试怎么写才不敷衍
SpringBoot对单元测试的支持非常完善。很多同学交项目时没写测试,或者只写了一个默认的 contextLoads() 空方法,这在检查代码时不太有说服力。我建议至少写一个针对核心业务Service的测试类,比如测试评论业务的插入和校验逻辑。
java复制@SpringBootTest
@Transactional
class CommentServiceTest {
@Resource
private CommentService commentService;
@Test
void testAddCommentSuccess() {
CommentDTO dto = new CommentDTO();
dto.setScenicId(1L);
dto.setContent("风景很美,值得推荐!");
dto.setScore(5);
boolean result = commentService.addComment(dto, 1L);
assertTrue(result);
}
@Test
void testAddCommentDuplicate() {
CommentDTO dto = new CommentDTO();
dto.setScenicId(1L);
dto.setContent("再来一条");
dto.setScore(4);
commentService.addComment(dto, 1L);
// 第二次评论同一景点,应该抛出异常或返回false
assertThrows(BusinessException.class, () -> commentService.addComment(dto, 1L));
}
}
@Transactional 注解确保了测试方法执行完毕后事务回滚,不会污染数据库的测试数据。你能写出来这两个测试方法,答辩的时候就可以说“我为评论的核心业务逻辑编写了单元测试,覆盖了成功场景和重复评论的异常场景”,在项目质量方面这是很好的加分项。
5. 这篇项目后续可以怎么扩展
项目主体做完之后,其实还有很多扩展空间。如果你还有精力,或者想在简历上让这个项目更有竞争力,我推荐两个方向。
第一个方向是引入Redis做缓存。景点详情接口是高频访问接口,可以用 @Cacheable 注解把查询结果缓存到Redis里,当管理员修改景点信息时,通过 @CacheEvict 注解清理缓存。这个改动在两小时内就能完成,但在面试时你可以讲清楚缓存穿透、缓存雪崩、缓存一致性这些概念,项目含金量立刻就不一样了。
第二个方向是把访问日志接入定时任务,在每天凌晨统计前一天的数据。SpringBoot的 @Scheduled 注解可以非常方便地实现定时任务,在启动类上加上 @EnableScheduling 后,写一个定时任务方法,用 @Scheduled(cron = "0 0 2 * * ?") 设定每天凌晨2点执行。数据统计看板里的“昨日访问量”就完全自动化了。
我个人的经验是,毕设项目不要追求大而全,而是要把一个核心业务做深做透。这个云南旅游信息平台,覆盖了JavaWeb开发的全链路:数据库设计、后端业务开发、前端页面联调、服务器部署,做完之后你去面试Java开发岗,提到这个项目,面试官通常不会觉得太菜,聊到SpringBoot细节时自己也更有底气。
如果你在跟着做的时候遇到某个具体的报错,可以把报错信息完整贴出来,我抽空看到了会尽量帮忙建议排查方向。祝你答辩顺利。
