1. 汽车资讯网站的定位与功能拆解
做Java后端开发这几年,我接手过不少类似的项目源码,说实话,"汽车资讯网站"听起来简单,真正做起来依然有不少值得琢磨的地方。身边总有学弟学妹或者刚转行的朋友问我:拿到一套SpringBoot的源码,怎么快速看懂、跑起来,甚至在上面二次开发?这篇文章就围绕一套基于SpringBoot的汽车资讯网站系统来讲讲,从源码结构、核心代码到部署文档,一条线过完。
这套系统本质上是典型的资讯类内容管理系统(CMS),只不过内容主题聚焦在汽车领域。对比通用博客系统,它的差异化体现在两个地方:一是资讯内容有明确的分类维度,比如品牌、车型、资讯类型;二是有车型库或者说车系库的概念,让用户看资讯的同时能关联到具体车型的参数和图片。这两点决定了数据库表的设计逻辑——不能只做一张文章表草草了事。
适合看这篇文章的人,主要有三类:
- 准备做Java课程设计或毕业设计的学生,需要一套结构完整、注释清晰、能跑通的参考项目
- 刚入行一到两年的后端开发,想看看一个真实项目的分层结构、接口设计、部署流程
- 想快速给客户或学校交付"汽车资讯类网站"的开发者,需要一套能直接改改用的基础系统
功能层面,这套系统分前台和后台两块。前台解决"看"的问题:首页资讯推荐、资讯列表按分类筛选、关键词搜索、资讯详情、车型库浏览、车型详情、用户评论。后台解决"管"的问题:管理员维护资讯、管理车型品牌、审核评论、查看基础统计。有些版本会加用户注册登录、收藏资讯的功能,核心业务流程其实都一样。
拿到源码之后,我建议你按这个顺序去读,效率最高:
- 先看SQL文件,搞清楚有哪些表、表之间什么关系
- 再看application.yml配置文件,确认端口、数据库连接、上传路径等基础配置
- 接着按 "Controller → Service → Mapper" 的顺序读一条完整业务链
- 最后跑前端,用管理员账号登录后台,对照界面把逻辑串起来
这套顺序基本能覆盖市面上绝大多数SpringBoot单体项目的阅读路径。很多新手上来就点开实体类一个个看,看到后面就迷失方向了,因为实体类只是数据的映射,业务逻辑在Service层,接口入口在Controller层,光看实体类很难拼出完整的业务图景。
2. 技术栈选型对比与数据库设计思路
2.1 为什么是SpringBoot + MyBatis-Plus这套组合
汽车资讯系统这种项目,技术选型不需要追求新和炫,而是要稳、资料多、上手快。源码里采用SpringBoot作为基础框架,这是目前Java后端开发事实上的标准选择。它的自动配置机制省掉了大量XML配置,内嵌Tomcat让部署直接变"打jar包 → 扔服务器 → java -jar"三步曲。对于课设、毕设和中小型资讯网站,这个组合的性价比极高。
持久层方面,很多早期项目会原生使用MyBatis,需要手写每一条SQL。而这套系统选择了MyBatis-Plus,相当于在MyBatis基础之上叠加了一层通用Mapper。单表CRUD不用写SQL,直接继承BaseMapper 就能得到增删改查方法。这对开发效率的提升非常明显——同样的功能,代码量能减少三分之一以上。
至于前端部分,如果版本是前后端不分离的,通常就是Thymeleaf模板引擎加Bootstrap;如果版本是前后端分离的,常见组合是Vue + Element UI,部署时把前端打包成静态文件后放进Nginx。两种版本的差异主要在于接口形态:不分离版返回的是页面,分离版返回的是JSON数据。你在看源码的时候,先确认这个项目的具体形态再往下走。
2.2 数据库表设计的关键逻辑
汽车资讯网站的数据库设计,最核心的是要回答一个问题:资讯和车的品牌车型之间的关系是什么?
常规设计会包含这几张核心表,我按重要性给你排一下:
| 表名 | 作用 | 关键字段 |
|---|---|---|
| user | 用户表 | username, password, role, avatar |
| car_brand | 汽车品牌表 | brand_name, brand_logo, sort_order |
| car_series | 车系列表 | series_name, brand_id, price_range, image |
| car_news | 资讯文章表 | title, content, cover_image, category_id, brand_id, series_id |
| news_category | 资讯分类表 | category_name, parent_id, sort_order |
| comment | 评论表 | news_id, user_id, content, create_time |
| admin_user | 后台管理员表 | username, password, role |
其中比较关键的一处设计在于car_news表同时携带brand_id和series_id。如果一个资讯内容是"比亚迪汉EV上市",那么它会关联到比亚迪品牌和汉车系。这样前台在展示车型详情页时,可以直接拉出"本车系相关资讯"列表。这种设计看似多存了两个字段,但查询性能比三张表来回join要好很多,尤其当资讯量到十万级之后,冗余字段换性能是划算的。
评论表的设计也值得留意。资讯和评论是典型的一对多关系,所以comment表只需要记录news_id和user_id即可。如果要做评论回复功能,常见做法是增加一个parent_id字段,父评论为空表示顶级评论,不为空表示回复某条评论。这套系统如果带了回复功能,逻辑上就是这么设计的。
3. 项目目录结构与核心代码讲解
3.1 分层架构的目录规划
拿到源码解压后,你会看到这样的目录结构:
car-news-system/ ├── src/main/java/com/example/carnews/ │ ├── controller/ # 控制层,接收请求,返回数据 │ │ ├── NewsController.java │ │ ├── BrandController.java │ │ ├── SeriesController.java │ │ ├── UserController.java │ │ └── AdminController.java │ ├── service/ # 业务逻辑层 │ │ ├── NewsService.java │ │ ├── BrandService.java │ │ └── UserService.java │ ├── mapper/ # 数据访问层,MyBatis-Plus的Mapper接口 │ ├── entity/ # 实体类,对应数据库表 │ ├── config/ # 配置类,比如跨域、拦截器 │ ├── common/ # 公共类,比如统一返回结果Result │ └── utils/ # 工具类,比如JWT工具、密码加密工具 ├── src/main/resources/ │ ├── mapper/ # XML文件(如果需要复杂SQL时使用) │ ├── static/ # 静态资源 │ ├── templates/ # 前端模板(Thymeleaf版) │ └── application.yml # 项目配置文件 └── sql/ └── car_news.sql # 数据库初始化脚本这个分层结构非常标准,也是面试时最容易被问到的。Controller负责接收HTTP请求、参数校验、调用Service;Service负责业务逻辑组装,比如"查询资讯详情时需要同时增加浏览量",就应该放在Service层而不是Controller层;Mapper只负责数据库交互。三层各司其职,互相之间通过接口调用,后面维护的时候就很好扩展。
3.2 统一返回结果的设计
一个值得仔细看的类是common/Result.java,它几乎是所有SpringBoot项目的标配。它的作用是把接口返回数据包一层统一格式,前端拿到之后不用逐个判断字段是否存在。源码里的Result类一般包含三个字段:
public class Result<T> { private Integer code; // 状态码,200成功,500失败 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; } }这套设计在开发阶段的收益不明显,但当前端页面多、接口多的时候非常有用。前端只要判断code是不是200就能决定走成功逻辑还是失败逻辑,不需要每个接口单独处理异常分支。如果你打算在源码基础上加功能,新建的接口务必保持这个统一返回格式,否则前后端联调的时候会很难受。
3.3 核心接口讲解:资讯分页列表
资讯页是网站流量最大的入口,所以分页查询接口的性能表现直接影响用户体验。看这部分代码时,重点关注MyBatis-Plus的分页插件的使用方式。
在NewsController里,分页查询的代码通常长这样:
@GetMapping("/list") public Result<IPage<NewsVO>> list( @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) Integer brandId, @RequestParam(required = false) Integer categoryId) { Page<News> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<News> wrapper = new LambdaQueryWrapper<>(); // 按品牌筛选 if (brandId != null) { wrapper.eq(News::getBrandId, brandId); } // 按分类筛选 if (categoryId != null) { wrapper.eq(News::getCategoryId, categoryId); } // 按发布时间倒序 wrapper.orderByDesc(News::getCreateTime); IPage<News> newsPage = newsService.page(page, wrapper); return Result.success(newsPage); }这段代码的核心价值在于用LambdaQueryWrapper替代了手写SQL的条件拼接。传统做法里面,品牌筛选和分类筛选需要写动态SQL,用<if>标签判断条件是否为空。而现在一行wrapper.eq()就搞定了,代码可读性高了一大截。
不过要注意一个问题:分页插件需要在配置类里注册,否则Page参数不生效。这也是很多新手跑项目时遇到"明明设置了pageSize但查出来还是全部数据"的原因。看源码时,找到MybatisPlusConfig配置类,里面应该有类似这样的代码:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }如果没有这段配置,Page对象只是作为一个普通参数传进去,不会真正作用于SQL语句。
3.4 核心接口讲解:登录认证与拦截器
资讯网站系统的后台肯定需要管理员登录保护。看源码时,重点看两个地方:登录时密码怎么校验的,以及未登录请求是怎么被拦截的。
常见的实现方案有两种。一种是基于Session的传统方案,登录成功后把用户信息放进HttpSession,拦截器里检查Session是否存在。另一种是基于JWT(JSON Web Token)的方案,登录成功后签发一个token返回给前端,前端把token存在本地,后续请求放进Authorization头里,后端用拦截器校验token合法性。
这套系统如果用的是JWT方案,代码执行流程是这样的:
@PostMapping("/login") public Result<String> login(@RequestBody AdminUser user) { AdminUser admin = adminUserService.checkLogin(user.getUsername(), user.getPassword()); if (admin == null) { return Result.error("用户名或密码错误"); } String token = JwtUtil.generateToken(admin.getId(), admin.getUsername()); return Result.success(token); }而拦截器这边,需要实现HandlerInterceptor接口,在preHandle方法里取token、解析token、放行或拒绝:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || !JwtUtil.verify(token)) { response.setStatus(401); return false; } return true; } }同时还要在WebMvcConfigurer配置类里注册这个拦截器,并排除掉登录接口和前台公开接口:
@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/admin/**") .excludePathPatterns("/admin/login"); }用配置类统一管理拦截路径,比在每个Controller方法里手动判断优雅得多。以后要新增受保护的接口,只要保证路径前缀是/admin/,拦截器就会自动生效。
3.5 密码加密不能只做MD5
看到用户表里密码字段的时候,建议你验证一下源码里是怎么处理的。如果直接明文存储,或者只用MD5加密而不加盐,这个项目的安全性是不过关的。
MD5在现在的算力条件下,用彩虹表碰撞简直轻而易举。虽然它的确是不可逆的,但市面上免费的MD5反查网站一大把,把常见密码的MD5值提前算好存起来,你存一个密文,拿去一查就还原了。
比较稳妥的做法是使用BCrypt这类自带盐值的加密算法。它的特点是同一个密码每次加密结果都不同,因为每次会随机生成一个盐值混进去。验证的时候用专门的matches方法:
// 注册时加密存储 String encodedPassword = BCrypt.hashpw(rawPassword, BCrypt.gensalt()); // 登录时校验 boolean isMatch = BCrypt.checkpw(rawPassword, encodedPassword);如果你拿到手的源码用的是MD5,动手改造也不复杂,关键就是把原来存的密文换掉重新生成一遍。今后的新增用户直接用BCrypt,老用户的密码统一重置一次。这项工作量不大,但价值很高,属于典型的"半小时改动、安心一整年"。
4. 从零到一的本地启动步骤
4.1 环境准备清单
跑SpringBoot项目的本地环境,常见的坑不在版本高,而在版本不匹配。我的建议是,先看源码的pom.xml文件确认SpringBoot版本,再决定本地JDK版本。比如SpringBoot 2.x对应JDK 8到JDK 11,SpringBoot 3.x则要求JDK 17及以上。
环境准备清单如下:
- JDK:版本由项目决定,下载后配置好
JAVA_HOME环境变量 - Maven:3.6以上版本,配置好
settings.xml里的本地仓库路径和镜像源,国内建议配阿里云镜像加快依赖下载 - MySQL:5.7或8.0都行,8.0需要注意驱动类和时区配置问题
- IDEA:社区版完全够用,因为SpringBoot项目不依赖专门的IDE插件
- Navicat或MySQL命令行:用于导入SQL、查看表结构
4.2 导入源码的完整操作
第一步是在IDEA里用File → New → Project from Existing Sources,选中源码目录,选择Maven模式导入。导入过程Maven会自动下载依赖,第一次可能需要几分钟到十几分钟,取决于网络状况。
依赖下载期间,打开application.yml把数据库连接信息改成本地环境。这一段经常有人栽跟头,我贴一个典型的配置:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/car_news?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 servlet: multipart: max-file-size: 10MB max-request-size: 10MBserverTimezone=Asia/Shanghai这一项尤其重要。MySQL 8.0默认用的UTC时区,如果URL里不指定时区,用中国时间插入的数据会比实际时间少8个小时,排查起来特别容易懵。
然后是数据库初始化。打开Navicat,新建一个名为car_news的数据库,字符集选择utf8mb4,排序规则选utf8mb4_general_ci或utf8mb4_unicode_ci都行。然后右键数据库选择"运行SQL文件",指向sql/car_news.sql。执行完成之后刷新一下,十几个表都出现了,说明SQL没问题。
导入SQL的时候,如果文件路径里带中文,某些版本的Navicat会报编码问题,最保险的做法是把SQL文件放到纯英文路径下再导入。
4.3 启动时的常见报错与处理
第一次启动SpringBoot项目,报错基本上集中在下面几个地方:
第一是端口被占用。IDEA控制台会直接提示Port 8080 was already in use,处理方式很简单——找到占用进程结束掉,或者在application.yml里换一个端口。命令行查占用可以这样操作:
# 查看8080端口的PID netstat -ano | findstr 8080 # 结束进程 taskkill /PID 进程号 /F第二是数据库连接失败。报错信息通常是Access denied for user 'root'@'localhost'或Communications link failure。前者是密码和application.yml里对不上,后者是MySQL没启动或端口不对。按顺序检查MySQL服务状态、用户名密码、URL里的端口。
第三是登录后台后CSS样式丢失。这个多半发生在前后端不分离的版本里,原因是静态资源被拦截了。排查方式就是看拦截器配置里的排除路径是否包含了/static/**,如果不包含,专门加一行排除路径即可。
启动成功后,控制台会看到SpringBoot的启动日志,最后一行是Started CarNewsApplication in x.xxx seconds。这时候浏览器访问http://localhost:8080,前台首页就能出来了。后台管理入口一般是http://localhost:8080/admin,初始账号密码在SQL文件里有预设,查一下admin_user表就知道。
5. 服务器部署方案
5.1 传统方式:jar包部署到Linux服务器
本地跑通了,部署上线是最后一步。最简单可靠的方式是把项目打成jar包,直接用java -jar运行,这也是SpringBoot单体应用最大的优势。
打包之前,确认application.yml里的数据库地址需要改成服务器的实际地址。如果数据库也装在服务器上,通常写localhost或者内网IP就行。
打包命令在项目根目录下执行:
mvn clean package -DskipTests执行完成后,target目录下会生成一个car-news-system-0.0.1-SNAPSHOT.jar,一般几十MB。这个jar包内置了Tomcat,传到哪里都能跑。
把jar包上传到服务器后,用nohup方式启动:
nohup java -jar car-news-system-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod > app.log 2>&1 &这里解释一下--spring.profiles.active=prod的含义,SpringBoot支持多环境配置,比如application-dev.yml对应开发环境,application-prod.yml对应生产环境。启动时通过参数指定激活哪个配置文件。如果你的源码没有拆分多环境配置文件,那此参数不需要带,直接使用默认配置。
由于是后台启动,日志会写到app.log文件里。如果启动报错,直接tail -200 app.log就能看到完整堆栈。
5.2 Docker方式部署
如果服务器的环境比较乱,或者你想统一开发环境和服务器环境,用Docker部署更干净。结合热词里提到的高频问题,这里简单列一下Docker的部署要点。
首先在项目根目录创建一个Dockerfile:
FROM openjdk:8-jdk-alpine LABEL maintainer="yourname" COPY target/car-news-system-0.0.1-SNAPSHOT.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "/app.jar"]然后执行构建和启动:
# 构建镜像 docker build -t car-news:1.0 . # 启动容器,映射端口 docker run -d -p 8080:8080 --name car-news car-news:1.0这里需要注意一点,如果项目里配置的数据库地址是localhost,在Docker容器内指的是容器自身而不是宿主机。正确做法是配置成宿主机IP,或者让容器按host模式运行:
docker run -d --network host --name car-news car-news:1.0host模式的好处是网络性能好、端口配置简单,缺点是会有端口管理上的小混乱。对于这台服务器只跑一个项目的场景来说,host模式反而是最省心的选择。
5.3 前后端分离版本的部署与Nginx配置
如果你拿到的是前后端分离的版本,那部署流程会多一步。前端Vue项目需要先构建静态文件,然后交给Nginx托管,后端接口继续用上面的方式跑。
前端构建:
npm install npm run build构建完成后,dist目录里的文件就是纯静态资源。把它传到服务器的/var/www/car-news目录,然后在Nginx配置里做反向代理:
server { listen 80; server_name yourdomain.com; # 静态文件 location / { root /var/www/car-news; index index.html; try_files $uri $uri/ /index.html; } # API接口反向代理到后端 location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }关键配置就是location /api/这一段。前端请求/api/list,Nginx把它转发到http://127.0.0.1:8080/list,后端感觉不到代理的存在。如果根路径的try_files不写,Vue路由刷新页面时会报404,这个细节很容易被忽略。
6. 部署与开发中最容易踩的五个坑
6.1 SpringBoot版本太高的兼容性问题
热词里有"springboot版本太高"这一条,这个确实太常见了。很多人从网上下载新项目,默认用的SpringBoot版本是2.7甚至3.x,却沿用旧博客的教程去配置,很容易卡住。
典型场景是SpringBoot 3.x要求JDK 17,但你本机装的是JDK 8。IDEA一启动就报java.lang.UnsupportedClassVersionError,提示编译版本不够。处理方案有两条路:要么升级JDK到17,要么把项目的SpringBoot版本降回2.x系列。对学习为目的的项目来说,我建议直接换JDK 17,毕竟SpringBoot 3是大势所趋,新学的东西没必要一开始就用老版本。
另外,SpringBoot 3.x里javax包全部换成了jakarta,如果源码里用了import javax.servlet.*,升级之后就得全局替换成jakarta.servlet.*,这种小改动很容易被忽略。
6.2 文件上传路径不存在导致的图片不显示
资讯网站肯定要传图片。源码里通常会配置一个上传路径:
file: upload-dir: /data/car-news/upload/这个路径如果不存在,图片上传会报FileNotFoundException。而最常见的坑是,本地开发时Windows上用D盘路径没问题,到服务器Linux上忘了创建目录,或者权限不对,导致上传失败。
我的习惯是在代码里做目录自动创建:
String uploadDir = fileProperties.getUploadDir(); File dir = new File(uploadDir); if (!dir.exists()) { dir.mkdirs(); }然后在上传接口里保存文件:
@PostMapping("/upload") public Result<String> upload(@RequestParam("file") MultipartFile file) { String originalFilename = file.getOriginalFilename(); String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); String newFilename = UUID.randomUUID().toString().replace("-", "") + suffix; File dest = new File(uploadDir + newFilename); file.transferTo(dest); return Result.success("/upload/" + newFilename); }为什么文件名要用UUID重造?因为用户上传的文件名可能是中文、可能带空格,直接当文件名保存到服务器上,分享链接时URL编码问题会让人头大。用UUID既避免了重名覆盖,也避免了特殊字符问题。
6.3 跨域问题
前后端分离开发时,前端跑在http://localhost:5173,后端跑在http://localhost:8080,二者端口不同。浏览器的同源策略会拒绝前端发请求,报的错是Access-Control-Allow-Origin相关。
解决方式有两种。第一种是后端配置跨域:
@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); } }第二种是生产环境走Nginx反向代理,由于前后端同域,跨域问题自然消失。我的经验是本地开发用第一种,部署上线用第二种,分工明确。
6.4 MyBatis-Plus条件构造器的坑
LambdaQueryWrapper用起来方便,但也容易出问题。最常见的坑是条件字段拼错,导致查出来的数据不对。比如:
wrapper.eq(News::getStatus, "1");写成分页查询加条件时,如果status字段在数据库中是int类型,而传入的是字符串类型,MySQL有隐式类型转换,索引会失效,全表扫描一次。数据量小的时候问题不大,等数据量到了几十万条,接口响应就会明显变慢。
另一个坑是like查询。wrapper.like()默认是%keyword%,两边都带百分号,这意味着数据库索引会失效。对于资讯标题搜索还好,因为数据量通常不至于大到不可接受,但如果你要优化搜索性能,就得考虑全文索引或者接入Elasticsearch了。
6.5 时区问题
时区问题在很多系统里都出现过,不只是汽车资讯系统。MySQL的时区、JDBC连接的时区、Jackson序列化时的时区,三处不一致,就会出现"数据库存的时间是对的,接口返回的时间差了8小时"。
我在配置上遵循这样的习惯:
- MySQL服务器使用中国标准时间
- JDBC URL里明确指定
serverTimezone=Asia/Shanghai - SpringBoot的
spring.jackson.time-zone=GMT+8
三处对齐之后,时间就不会出问题了。这个问题在本地部署时往往不暴露,因为很多开发机默认时区就是+8;但云服务器默认时区可能是UTC,一键部署上去就会中招。
7. 在源码基础上做二次开发的扩展思路
7.1 给资讯加上浏览量统计
我在实际使用中还发现,这类资讯网站一个高频需求是统计每篇文章的浏览量,并在列表页展示热门资讯。实现起来不复杂,在car_news表加一个view_count字段,然后在资讯详情接口里做自增:
@GetMapping("/detail/{id}") public Result<News> detail(@PathVariable Integer id) { News news = newsService.getById(id); // 浏览量+1 news.setViewCount(news.getViewCount() + 1); newsService.updateById(news); return Result.success(news); }注意这种写法在并发高的时候会有性能问题,因为每次请求都触发一次UPDATE数据库的操作。数据量小无所谓,但如果是真实上线的场景,建议用Redis做计数器,定时批量写回数据库。不过对课设、毕设和中小规模资讯站来说,直接UPDATE就够了,不必过度设计。
7.2 用户收藏功能
收藏功能的本质是维护一张关联表,记录用户和资讯的对应关系。
CREATE TABLE `user_favorite` ( `id` int NOT NULL AUTO_INCREMENT, `user_id` int NOT NULL, `news_id` int NOT NULL, `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;查询"当前用户收藏了哪些资讯"时,就是根据user_id查这张表,再关联car_news表拿资讯详情。判断"这篇资讯是否已被该用户收藏",就用user_id + news_id联合查询,命中则说明已收藏,再点一次就是取消收藏,逻辑上就是DELETE操作。
这个表属于典型的多对多关联表。它的设计要点是给user_id和news_id加上联合唯一索引,可以防止同一用户重复收藏同一资讯:
ALTER TABLE user_favorite ADD UNIQUE KEY uk_user_news (user_id, news_id);7.3 接入Redis做缓存
如果你想把项目做得更有技术含量,把热门资讯改成走Redis缓存是一个很不错的切入点。核心思路是:不查数据库先查Redis,缓存没有再去查数据库并回填Redis,然后设置合理的过期时间。
@GetMapping("/hot") public Result<List<News>> hotNews() { String key = "hot:news"; // 先从缓存查 String cached = redisTemplate.opsForValue().get(key); if (cached != null) { List<News> list = JSON.parseArray(cached, News.class); return Result.success(list); } // 缓存没有,查数据库 List<News> list = newsService.list( new LambdaQueryWrapper<News>() .orderByDesc(News::getViewCount) .last("limit 10") ); // 回填缓存,10分钟过期 redisTemplate.opsForValue().set(key, JSON.toJSONString(list), 10, TimeUnit.MINUTES); return Result.success(list); }这段代码虽然简单,但它代表了一个非常核心的思路:缓存和数据库的一致性如何权衡。10分钟过期时间意味着数据最多滞后10分钟,对于热门资讯榜单这种场景完全够用。这个改动如果写进毕设或项目文档里,技术亮点会很突出。
8. 从拿到源码到内化成自己的项目,我的几点体会
最后说点我在实际反复折腾这类项目过程中的体会。第一点是,源码这东西,跑起来永远只是第一步,真正的价值在于你能讲清楚"它为什么这么设计"。面试时或者答辩时,别人问"为什么选MyBatis-Plus而不是JPA"、"为什么这里用拦截器不用过滤器",如果你能回答出背后的取舍逻辑,比你把代码背下来强十倍。
第二点是,拿到项目之后,我强烈建议你先做一次"破坏性实验"。改一改配置、删一段代码、加一个字段,看看项目会不会崩、会怎么崩。通过这种方式,你才能真正理解每个组件的边界和依赖关系。我见过太多人把项目跑起来后就扔在那边,问起来哪块代码是干什么的,全然不知。这样即便最后交付了,心里也是虚的。
第三点是,部署文档这件事,看起来是给用户看的,其实最受益的是你自己。每次部署踩坑,把解决办法记下来,下次遇到同样问题,十分钟就能定位。我每次换新服务器部署这个项目,都会顺手更新一遍文档,把不同环境下的特殊问题补充进去。这套系统的部署文档在Linux服务器上完整跑一遍流程之后,你对SpringBoot项目的认识会和看教程完全不一样——纸上得来终觉浅,绝知此事要躬行,这句话放在部署上同样适用。
如果你手头也有一套类似的项目源码,不妨按照这篇文章的结构,先去跑通它,再读透它,最后去改好它,你收获的东西会比源码本身多得多。