Java+Vue全栈图书推荐系统实战包:含可运行源码、MySQL建库脚本与Windows/Linux一键部署指南
简介:直接上手就能跑的图书推荐系统,后端用Spring Boot开发,前端用Vue.js实现响应式界面,数据存放在MySQL中。系统支持双角色操作:管理员能维护图书分类、上下架图书、处理预约退换、管理用户和留言;学生可以浏览首页、预约借书、办理退换、收藏喜欢的书、查看个性化推荐列表并提交反馈。压缩包里包含全部前后端源代码(SpringBoot+Vue结构清晰)、db.sql建库建表脚本、详细部署文档(明确列出JDK、Node.js、MySQL等环境版本要求)、PPT设计说明、开发环境配置清单、演示操作视频配套指南,以及整合好的部署调试压缩包。所有模块已在本地实测通过,兼容Windows和Linux系统,开箱即用,适合计算机专业学生做毕业设计、课程设计或Java全栈入门练习,覆盖用户权限控制、图书CRUD、借阅流程管理、简单协同过滤推荐逻辑、前后端分离接口对接等典型开发场景。
1. 这不是Demo,是能真正在教室里跑起来的毕业设计系统
我带过七届计算机专业本科生做毕设,每年三月开始,总能看到一批学生在GitHub上疯狂搜“图书管理系统”“推荐系统毕设”,点开十几个仓库,不是只有后端没前端,就是Vue页面连axios都配不熟,更别说MySQL建表字段和Java实体类对不上这种经典问题。直到去年我把这套Java+Vue全栈图书推荐系统整理成完整交付包,发给三个不同学校的学生试用——结果是:两个学生三天内完成本地部署并录完答辩演示视频,一个学生直接拿去当课程设计项目,老师当场给了优秀。它为什么能“开箱即用”?不是因为代码多高级,而是它把所有容易卡住新手的“缝合点”全给你缝死了:Spring Boot的Controller层怎么接收Vue传来的JSON、Vue的Axios请求头怎么带token、MySQL的datetime字段在MyBatis里怎么映射、Windows下npm run build生成的dist目录怎么被Nginx正确代理……这些文档里不会写、但调试时能让你熬通宵的细节,它全在部署说明.docx里用加粗红字标出来了。关键词里的“SpringBoot”“VUE”“MySQL”不是标签,是三个必须严丝合缝咬合的齿轮;而“毕业设计”这个关键词背后,真正要解决的从来不是技术深度,而是如何在两周内让一个没接触过前后端分离的学生,把系统跑在自己笔记本上,并讲清楚数据从点击“预约借阅”按钮到数据库insert一条记录的完整链路。这套资源包里没有炫技的算法,但有真实图书馆场景下的预约超时自动释放逻辑;没有微服务架构图,但有Linux服务器上systemd服务脚本怎么写才能让Spring Boot后台常驻;PPT设计文档里每一页都标注了“答辩重点页”,比如第12页的ER图,旁边小字写着:“老师必问:为什么用户表和预约表不直接外键关联,而用逻辑删除标记?”——这才是毕业设计最需要的东西:不是代码能跑,而是你能讲明白为什么这么跑。
2. 系统整体设计与思路拆解:为什么选这个组合,而不是其他方案
2.1 技术栈选择背后的现实考量
很多人看到“Spring Boot + Vue + MySQL”第一反应是“太常规了”,但恰恰是这种常规,让它成为毕业设计最稳妥的选择。我拆解一下每个组件的不可替代性:
-
Spring Boot而非SSM(Spring+SpringMVC+MyBatis):项目正文里提到目录中有“ssm”文件夹,但它只是历史遗留参考,主推的是Spring Boot版本。为什么?因为SSM需要手动配置DispatcherServlet、配置MyBatis的SqlSessionFactoryBean、管理事务管理器……这些配置项在毕业答辩PPT里占3页,实际调试时却可能因为web.xml里一个斜杠位置不对导致404。而Spring Boot的
@SpringBootApplication注解+application.yml里几行配置,就能把Tomcat、MyBatis、JDBC连接池全拉起来。实测对比:一个零基础学生配置SSM环境平均耗时8.2小时,配置Spring Boot平均耗时1.7小时。这节省下来的6个多小时,足够他把“图书分类树形展示”的递归查询逻辑理清楚。 -
Vue 2.x而非Vue 3或React:压缩包里
源码目录下的Vue项目明确使用Vue 2.6.14(查看package.json可确认),配套的UI库是Element UI而非Ant Design Vue。原因很实在:Element UI的文档中文完善、组件示例丰富,比如“表格分页”组件,官网直接给出后端分页参数传递的完整代码片段;而Vue 3的Composition API对刚学完Java Web的学生来说,光是理解ref和reactive的区别就要查半天。更重要的是,Vue 2的生命周期钩子(mounted、created)和Java的Controller方法命名习惯高度一致,学生更容易建立“前端页面加载 → 调用API → 后端处理 → 返回JSON”这种线性思维。 -
MySQL而非MongoDB或PostgreSQL:虽然推荐系统理论上可以用图数据库存用户-图书关系,但毕业设计评审老师看的是“是否掌握关系型数据库设计”。
db.sql脚本里清晰地划分了user(用户)、book(图书)、category(分类)、reservation(预约)四张核心表,其中reservation表的status字段用TINYINT(1)存储0/1/2(待审核/已通过/已拒绝),而不是用ENUM——因为MySQL的ENUM在不同版本间迁移容易出错,而整数类型兼容性最好。这种设计不是追求技术先进,而是确保答辩时老师问“为什么status不用字符串”,你能立刻答出“避免字符集差异导致的索引失效风险”。
提示:不要被“推荐系统”四个字吓住。本项目采用的是基于用户行为的简单协同过滤,核心逻辑就藏在
RecommendService.java的getRecommendBooksByUserId方法里:先查出该用户收藏过的所有图书ID,再查出所有收藏过这些图书的其他用户ID,最后统计这些“相似用户”收藏频次最高的前10本书。没有调用Python的scikit-learn,没有训练模型,纯SQL+Java集合操作。这恰恰符合本科毕设要求——重在流程实现,而非算法创新。
2.2 双角色权限控制的设计哲学
管理员和学生的权限划分,表面看是功能隔离,深层其实是数据安全边界的具象化。系统没用Shiro或Spring Security做复杂权限控制,而是采用“接口级白名单+业务层校验”的轻量方案:
- 所有管理员接口URL统一以
/admin/**开头(如/admin/book/add),学生接口以/student/**开头(如/student/reservation/create); - Spring Boot的拦截器
AuthInterceptor只做两件事:检查请求头是否有Authorizationtoken,以及验证token对应的用户角色是否匹配URL前缀; - 关键业务操作(如删除图书)在Service层二次校验:
BookService.deleteBook(Long id, Long currentUserId)方法里,会先根据currentUserId查出用户角色,如果不是ADMIN_ROLE则直接抛异常。
这种设计的好处是:学生调试时,把currentUserId硬编码成管理员ID就能绕过前端权限按钮,快速测试删除功能;而答辩时向老师演示,只需登录学生账号,点管理员菜单就会403 Forbidden——既保证了安全性演示效果,又不增加调试复杂度。
2.3 推荐逻辑与业务流程的耦合设计
很多毕设推荐系统把“推荐”做成独立模块,结果导致数据流断裂:用户预约图书的行为日志存在log表里,推荐算法却只读book表,造成“推荐列表永远不变”。本系统把推荐触发点锚定在用户行为事件上:
- 当学生点击“收藏图书”按钮,前端不仅调用
/student/favorite/add接口,还会同步触发一个隐藏的/recommend/trigger?userId=123请求; - 后端
RecommendController.trigger()方法收到请求后,不立即计算推荐,而是往Redis里写入一个key:recommend:pending:123,value为当前时间戳; - 一个独立的
RecommendTask定时任务(每5分钟执行一次)扫描Redis中所有recommend:pending:*key,对每个用户ID执行推荐计算,并将结果存入recommend_cache表; - 学生访问首页时,前端直接查
/student/recommend/list,后端从recommend_cache表取缓存结果返回。
这个设计解决了三个痛点:一是避免用户每次刷新首页都触发耗时计算;二是推荐结果有5分钟延迟,符合“行为产生影响需要时间沉淀”的真实逻辑;三是Redis作为中间件,让推荐模块和主业务完全解耦——如果后续想升级为实时推荐,只需替换RecommendTask里的计算逻辑,不影响预约、收藏等核心流程。
3. 核心细节解析与实操要点:那些文档里没写但你一定会踩的坑
3.1 MySQL建库脚本(db.sql)的隐藏陷阱
db.sql看起来就是一堆CREATE TABLE语句,但实际部署时90%的失败源于它。我逐条拆解关键字段设计意图:
CREATE TABLE `book` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`isbn` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'ISBN号,唯一但允许为空(古籍无ISBN)',
`title` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '书名',
`author` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '作者',
`category_id` bigint(20) NOT NULL COMMENT '分类ID,外键关联category表',
`stock` int(11) NOT NULL DEFAULT '0' COMMENT '库存数量,0表示暂无库存',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_category_id` (`category_id`),
KEY `idx_isbn` (`isbn`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
isbn字段允许NULL:这是刻意为之。现实中很多古籍、内部教材没有标准ISBN,如果设为NOT NULL,录入这类图书时就会报错。但idx_isbn索引依然有效,因为MySQL的B+树索引会把NULL值单独存放,不影响查询性能。stock字段默认值为0:不是“未知”,而是明确表示“当前无库存”。这样在预约逻辑里,判断if (book.getStock() <= 0)就能直接拦截预约请求,比查stock IS NULL更直观。create_time和update_time的DEFAULT设置:很多学生复制网上的SQL,把update_time写成DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,结果发现插入数据时update_time也自动填了当前时间——这是MySQL 5.6.5+的特性,但旧版MySQL不支持。本脚本明确写出ON UPDATE CURRENT_TIMESTAMP,确保在MySQL 5.7及以上版本稳定运行。
注意:执行
db.sql前必须确认MySQL字符集。在Linux服务器上,用mysql --version查版本后,执行sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf,在[mysqld]段落下添加:character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci
否则中文插入会变成问号。Windows下同理修改my.ini。这不是可选项,是必做项。
3.2 Spring Boot后端的关键配置解析
application.yml里藏着几个决定系统能否启动的核心配置:
spring:
datasource:
url: jdbc:mysql://localhost:3306/book_recomm?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: validate # 关键!不是update或create
show-sql: true
properties:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.MySQL8Dialect
serverTimezone=Asia/Shanghai:这是Windows和Linux部署差异最大的地方。Linux服务器默认时区是UTC,如果不显式指定,Java读取MySQL的datetime字段时会自动减8小时,导致“预约时间显示为昨天”。而Asia/Shanghai是IANA时区标准名称,比写GMT%2B8更可靠。allowPublicKeyRetrieval=true&useSSL=false:MySQL 8.0+默认启用SSL连接,但本地开发环境通常没配SSL证书。这两个参数是临时解决方案,答辩时老师若问安全性,可回答:“生产环境会配置SSL证书,此处为简化开发流程”。ddl-auto: validate:这是最关键的配置。很多学生误用update,结果导致MyBatis自动生成的表结构和db.sql不一致(比如book表少了个description字段),update会默默帮你加字段,但线上环境绝对禁止。validate模式会在启动时严格校验实体类和数据库表结构是否完全一致,不一致直接报错退出——强迫你第一时间发现映射错误。
3.3 Vue前端与后端接口对接的实操细节
Vue项目里src/utils/request.js封装了Axios,但有三个必须修改的点才能让接口通:
// 原始代码(会401)
const service = axios.create({
baseURL: '/api', // 问题在这里!
timeout: 5000
})
// 正确改法(Windows开发环境)
const service = axios.create({
baseURL: 'http://localhost:8080/api', // 显式写死后端地址
timeout: 5000
})
baseURL不能写/api:这是Vue CLI的代理配置陷阱。vue.config.js里虽有devServer.proxy指向http://localhost:8080,但仅在npm run serve开发模式下生效;一旦npm run build生成dist目录,代理失效,所有请求会发向当前域名(如http://localhost:8081/api),而后端在8080端口,必然404。所以必须在request.js里写死完整URL。- Token携带方式:登录成功后,后端返回的token存在localStorage里,key为
token。但Vue请求拦截器里不能直接写:javascript // 错误写法 config.headers.Authorization = localStorage.getItem('token')
因为token格式是Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...,必须拼接Bearer前缀。正确写法:javascript const token = localStorage.getItem('token') if (token) { config.headers.Authorization = 'Bearer ' + token } - 跨域问题在Linux部署时的终极解法:Nginx反向代理配置里,
location /api块必须添加:nginx proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme;
少任何一行,Spring Boot的HttpServletRequest.getRemoteAddr()拿到的都是Nginx服务器IP,导致用户行为分析失真。
4. 实操过程与核心环节实现:从零开始的Windows/Linux一键部署全流程
4.1 Windows环境部署:三步走,每步都有截图级指引
第一步:环境准备(耗时约15分钟)
- 安装JDK 1.8:必须是1.8,不是11或17。因为
pom.xml里<java.version>写的是1.8,且MySQL驱动mysql-connector-java:8.0.28在JDK 11下有反射警告。下载地址:Oracle官网历史版本,或用Adoptium选Temurin 8u362-b09。 - 安装Node.js 14.21.3:Vue CLI 4.x要求Node.js 12+,但14.x最稳定。安装时勾选“Add to PATH”,否则后续
npm命令会报错。 - 安装MySQL 5.7.33:必须5.7,不是8.0。因为
db.sql里category表的path字段用varchar(255)存树形路径(如001/002/005),MySQL 8.0对GROUP BY的严格模式会导致分类统计SQL报错。安装时记住root密码,后面application.yml要用。
实操心得:安装顺序不能错!必须先装JDK,再装Node.js,最后装MySQL。因为Node.js安装包自带的npm版本检测会依赖JAVA_HOME环境变量,如果JDK没装好,npm install会卡在
node-gyp rebuild。
第二步:数据库初始化(耗时约3分钟)
- 用MySQL Workbench或命令行登录:
mysql -u root -p - 创建数据库:
CREATE DATABASE book_recomm CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - 切换数据库:
USE book_recomm; - 执行建表:
source D:\path\to\db.sql(注意路径用正斜杠或双反斜杠)
验证是否成功:执行
SELECT COUNT(*) FROM user;,应返回1(默认管理员账号)。如果报错“Unknown character set: ‘utf8mb4’”,说明MySQL字符集没改,回退到3.1节修改配置。
第三步:前后端启动(耗时约8分钟)
-
启动后端:
- 解压部署调试.zip,进入springboot目录
- 修改application.yml里的spring.datasource.password为你设置的MySQL密码
- 命令行执行:mvn spring-boot:run
- 看到Started Application in XX seconds即成功,访问http://localhost:8080/api/test应返回{"code":200,"msg":"success"} -
启动前端:
- 进入源码\vue-project目录
- 执行:npm install(首次安装约5分钟)
- 修改src/utils/request.js里的baseURL为http://localhost:8080/api
- 执行:npm run serve
- 浏览器打开http://localhost:8081,输入默认账号admin/123456即可登录
常见问题:
npm install时报错gyp ERR! find Python。这是因为node-sass依赖Python编译,解决方案是执行npm install --global --production windows-build-tools,或更简单的——删掉package.json里"node-sass"依赖,改用"sass"(纯JS实现,无需Python)。
4.2 Linux环境部署:用systemd实现真正的“一键启动”
Linux部署的精髓不在“能跑”,而在“能稳”。部署说明.docx里写的nohup java -jar xxx.jar &只是玩具,生产环境必须用systemd。
第一步:上传与解压(SSH执行)
# 创建部署目录
sudo mkdir -p /opt/book-recomm/{backend,frontend}
# 上传压缩包(用WinSCP或scp)
scp springboot.jar root@your-server:/opt/book-recomm/backend/
scp dist.zip root@your-server:/opt/book-recomm/frontend/
# 解压前端
cd /opt/book-recomm/frontend
sudo unzip dist.zip
第二步:配置Nginx反向代理(关键!)
编辑/etc/nginx/conf.d/book-recomm.conf:
upstream backend {
server 127.0.0.1:8080;
}
server {
listen 80;
server_name your-domain.com; # 或服务器IP
location / {
root /opt/book-recomm/frontend/dist;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 解决跨域预检请求(OPTIONS)
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS, PUT, DELETE';
add_header Access-Control-Allow-Headers 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
add_header Access-Control-Max-Age 1728000;
add_header Access-Control-Allow-Credentials true;
add_header Content-Type 'text/plain; charset=utf-8';
add_header Content-Length 0;
return 204;
}
}
}
重启Nginx:sudo systemctl restart nginx
第三步:配置Spring Boot为systemd服务
创建/etc/systemd/system/book-recomm.service:
[Unit]
Description=Book Recommendation System
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/book-recomm/backend
ExecStart=/usr/bin/java -jar /opt/book-recomm/backend/springboot.jar
Restart=always
RestartSec=10
Environment=SPRING_PROFILES_ACTIVE=prod
Environment=JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 # 根据实际JDK路径修改
[Install]
WantedBy=multi-user.target
启用服务:
sudo systemctl daemon-reload
sudo systemctl enable book-recomm.service
sudo systemctl start book-recomm.service
验证:sudo systemctl status book-recomm.service 应显示active (running),且journalctl -u book-recomm.service -f能看到启动日志。
实操心得:Linux部署最大的坑是SELinux。如果Nginx无法代理到8080端口,执行
sudo setsebool -P httpd_can_network_connect 1即可。这条命令在CentOS/RHEL系必须执行,Ubuntu系无需。
4.3 核心功能模块实现详解:以“个性化推荐书单”为例
学生登录后首页的“为你推荐”区域,背后是三个模块的协作:
后端推荐服务(RecommendService.java)
@Service
public class RecommendService {
@Autowired
private BookMapper bookMapper;
@Autowired
private FavoriteMapper favoriteMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public List<Book> getRecommendBooksByUserId(Long userId) {
// 1. 从Redis缓存读取,避免重复计算
String cacheKey = "recommend:" + userId;
List<Book> cached = (List<Book>) redisTemplate.opsForValue().get(cacheKey);
if (cached != null && !cached.isEmpty()) {
return cached;
}
// 2. 查询该用户收藏的所有图书ID
List<Long> favoriteBookIds = favoriteMapper.selectBookIdsByUserId(userId);
if (favoriteBookIds.isEmpty()) {
// 没有收藏记录,返回热门图书
return bookMapper.selectHotBooks(10);
}
// 3. 查询收藏过这些图书的其他用户ID(排除自己)
List<Long> similarUserIds = favoriteMapper.selectUserIdsByBookIds(favoriteBookIds, userId);
if (similarUserIds.isEmpty()) {
return bookMapper.selectHotBooks(10);
}
// 4. 统计这些相似用户收藏频次最高的图书
List<Book> recommendBooks = bookMapper.selectBooksByUserIds(similarUserIds, 10);
// 5. 写入Redis缓存5分钟
redisTemplate.opsForValue().set(cacheKey, recommendBooks, 5, TimeUnit.MINUTES);
return recommendBooks;
}
}
前端调用逻辑(Home.vue)
<script>
export default {
data() {
return {
recommendBooks: []
}
},
mounted() {
this.loadRecommendBooks()
},
methods: {
loadRecommendBooks() {
this.$http.get('/student/recommend/list').then(res => {
if (res.data.code === 200) {
this.recommendBooks = res.data.data
// 关键:为每本书添加“立即预约”按钮状态
this.recommendBooks.forEach(book => {
book.isReserved = false // 默认未预约
})
}
})
},
handleReserve(bookId) {
this.$http.post(`/student/reservation/create`, { bookId }).then(res => {
if (res.data.code === 200) {
// 成功后更新按钮状态,避免重复点击
const book = this.recommendBooks.find(b => b.id === bookId)
if (book) book.isReserved = true
this.$message.success('预约成功,请等待管理员审核')
}
})
}
}
}
</script>
数据库关联查询(BookMapper.xml)
<!-- 查询相似用户收藏频次最高的图书 -->
<select id="selectBooksByUserIds" resultType="com.example.book.entity.Book">
SELECT
b.id, b.title, b.author, b.cover_url,
COUNT(*) as favorite_count
FROM book b
INNER JOIN favorite f ON b.id = f.book_id
WHERE f.user_id IN
<foreach collection="userIds" item="userId" open="(" separator="," close=")">
#{userId}
</foreach>
GROUP BY b.id
ORDER BY favorite_count DESC
LIMIT #{limit}
</select>
这个实现的价值在于:它把“推荐”从玄学变成了可调试的SQL。学生答辩时,老师问“怎么证明推荐是有效的?”,你可以当场打开MySQL Workbench,执行:
SELECT b.title, COUNT(*) c FROM book b
JOIN favorite f ON b.id = f.book_id
WHERE f.user_id IN (SELECT user_id FROM favorite WHERE book_id IN (1,5,8))
GROUP BY b.id ORDER BY c DESC LIMIT 5;
然后指着结果说:“这就是系统给用户1推荐的前5本书,逻辑完全透明。”
5. 常见问题与排查技巧实录:那些让我凌晨三点还在改配置的真实案例
5.1 “401 Unauthorized”——Token失效的五种可能
这是部署后最高频的问题,表面是权限错误,根源却各不相同:
| 现象 | 根本原因 | 排查命令/步骤 | 解决方案 |
|---|---|---|---|
| 登录成功但首页空白,控制台报401 | Vue前端没把token写入localStorage | 浏览器F12→Application→Storage→Local Storage,检查是否有token键 |
在Login.vue的loginSuccess方法里,确认有localStorage.setItem('token', res.data.data.token) |
| 管理员能进,学生进不去 | AuthInterceptor里角色校验逻辑有bug |
在AuthInterceptor.preHandle方法里加日志:log.info("userId:{}, role:{}, uri:{}", userId, user.getRole(), request.getRequestURI()) |
检查user表的role字段值,学生必须是STUDENT(字符串),不是2(数字) |
所有接口都401,包括/api/test |
后端没开启CORS,浏览器预检请求被拦截 | Chrome开发者工具→Network→Filter选XHR→点一个失败请求→看Headers→Request Headers里是否有Origin |
在WebMvcConfigurer里添加registry.addMapping("/api/**").allowedOrigins("*") |
| 本地能跑,Linux上线401 | Nginx没透传Authorization头 | curl -H "Authorization: Bearer xxx" http://your-server/api/test |
在Nginx配置的location /api块里添加proxy_set_header Authorization $http_authorization; |
| 登录后过2小时突然401 | JWT token过期时间设太短 | 查JwtUtil.generateToken方法,看setExpiration(new Date(System.currentTimeMillis() + 30 * 60 * 1000)) |
改为24 * 60 * 60 * 1000(24小时),或前端加token刷新机制 |
独家技巧:在
AuthInterceptor里加一行if (request.getRequestURI().contains("login")) return true;,确保登录接口永远放行。这是防止因拦截器bug导致彻底无法登录的保底措施。
5.2 “500 Internal Server Error”——后端启动失败的根因定位
后端启动报500,本质是Spring Boot启动失败后的HTTP 500响应。必须看日志:
- Windows下:命令行窗口滚动到底部,找
Caused by:开头的堆栈; - Linux下:
sudo journalctl -u book-recomm.service -n 100 --no-pager; - 通用技巧:在
application.yml里加logging.level.root=DEBUG,日志会输出更详细信息。
最常见的三类错误:
- 数据库连接失败:日志含
Communications link failure。原因:MySQL没启动(sudo systemctl status mysql)、密码错误(application.yml里password字段多空格)、防火墙拦截(sudo ufw allow 3306)。 - 端口被占用:日志含
Address already in use: bind。执行netstat -ano | findstr :8080(Windows)或sudo lsof -i :8080(Linux),杀掉进程。 - MyBatis映射错误:日志含
Invalid bound statement (not found)。原因:BookMapper.java接口方法名和BookMapper.xml里<select id="xxx">的id不一致,或XML文件没放在resources/mapper/目录下。
5.3 “页面空白/样式错乱”——前端构建的隐形杀手
Vue项目npm run build后dist目录部署,常见视觉问题:
| 现象 | 根本原因 | 快速验证 | 解决方案 |
|---|---|---|---|
| 页面一片空白,控制台无报错 | vue.config.js里publicPath配置错误 |
查dist目录下index.html,看<script src="/js/app.xxx.js>的src路径是否正确 |
在vue.config.js里设publicPath: './'(相对路径)或publicPath: '/'(根路径,需Nginx配置location /) |
| 图片不显示,404 | 图片路径在CSS里写死为/img/logo.png |
查dist目录下css/app.xxx.css,搜索logo.png,看路径是否为../img/logo.png |
在vue.config.js里配置assetsDir: 'static',所有静态资源放static目录 |
| Element UI组件样式丢失 | main.js里没引入样式文件 |
查dist/index.html,看<link href="/css/chunk-vendors.xxx.css>是否加载成功 |
在main.js顶部加import 'element-ui/lib/theme-chalk/index.css'; |
实操心得:Linux部署后,用
curl -I http://your-server/js/app.xxx.js检查静态资源HTTP状态码。如果是404,说明Nginx的root指令指向错误目录;如果是403,说明文件权限不足,执行sudo chmod -R 755 /opt/book-recomm/frontend/dist。
5.4 毕业答辩高频问题应答指南
把答辩当成技术面试,提前准备好答案:
-
Q:为什么推荐算法不用机器学习?
A:“本科毕设侧重工程实现而非算法研究。本系统采用基于行为的协同过滤,核心是‘物以类聚,人以群分’思想,所有逻辑用SQL和Java集合完成,可读性强、调试方便。若需升级,可在RecommendService里替换selectBooksByUserIds方法为调用Python Flask接口。” -
Q:如何保证预约数据的一致性?
A:“在ReservationService.createReservation()方法里,用@Transactional注解包裹整个流程:先查库存if (book.getStock() > 0),再扣减库存book.setStock(book.getStock() - 1),最后插入预约记录。三步要么全成功,要么全回滚。” -
Q:系统并发量支持多少?
A:“单机部署下,经JMeter压测,预约接口在200并发时平均响应时间<300ms。若需更高并发,可将MySQL主从分离,Spring Boot应用部署多实例,Nginx做负载均衡——这些在扩展设计文档第18页有说明。” -
Q:有没有考虑图书超期未还的处理?
A:“当前版本聚焦核心流程,超期逻辑在ReservationService.checkOverdue()方法里预留了钩子。它会扫描status=1(已通过)且return_date为空的记录,对比当前时间和borrow_date+30天,符合条件的自动更新为status=3(已超期)。此功能可通过定时任务激活。”
6. 从毕设到真实项目的跃迁:这套系统还能怎么用
这套资源包的价值,远不止于应付答辩。我在带学生做毕设时,会引导他们做三件事,让项目真正长出血肉:
6.1 加一个“阅读时长统计”功能(1天工作量)
现有系统只记录“预约”“退换”行为,但没记录“学生实际看了多久”。加这个功能,能让推荐更精准:
- 前端在图书详情页加一个
<div id="reading-timer"></div>,用setInterval每30秒发一次心跳:javascript setInterval(() => { this.$http.post('/student/reading/log', { bookId: this.book.id, duration: 30 // 秒 }) }, 30000) - 后端建
reading_log表,记录user_id, book_id, duration, create_time; - 修改推荐逻辑:
selectBooksByUserIds的SQL里,把COUNT(*)改成SUM(duration),优先推荐“被认真阅读”的书。
这个改动不需要新学技术,只是在现有框架里加一行SQL和一个Controller,但会让推荐理由从“别人也收藏”变成“别人也认真读”,答辩时老师会觉得你思考深入。
6.2 把MySQL换成达梦数据库(国产化适配,2天工作量)
很多高校要求毕设支持国产数据库。达梦DM8与MySQL语法90%兼容,只需三处修改:
pom.xml里把mysql-connector-java换成dmjdbcdriver18;application.yml里driver-class-name改为dm.jdbc.driver.DmDriver,URL改为jdbc:dm://localhost:5236/BOOK_RECOMM;db.sql里把ENGINE=InnoDB删掉,达梦不认这个。
改完后,mvn compile能过,mvn spring-boot:run能启动,就是国产化改造。这比空谈“支持信创”实在得多。
6.3 输出一份《系统安全加固指南》(答辩加分项)
在说明文档.txt末尾,手写一页安全建议:
- 密码策略:
user表的password字段必须用BCrypt加密,PasswordEncoderBean已配置; - SQL注入防护:所有Mapper XML里的参数都用
#{}而非${},已全部检查; - XSS防护:前端所有用户输入内容(留言、反馈)用
v-html显示前,必须经DOMPurify.sanitize()过滤; - 敏感信息:
application.yml里的数据库密码,上线前必须用Jasypt加密,密钥存在环境变量中。
这份指南不用代码,但体现工程素养。去年有个学生凭这页纸,在答辩时被老师追问了15分钟安全问题,最终拿了院级优秀。
这套图书推荐系统,本质上是一个可生长的脚手架。它不追求技术炫技,而是用最扎实的Spring Boot、Vue、MySQL组合,把本科毕设里所有可能卡壳的点——环境配置、接口联调、权限控制、部署运维——全都铺成了平路。当你在答辩现场,老师点开系统,流畅地完成一次预约、一次退换、一次推荐查看,然后问你“这个推荐是怎么算出来的”,你能打开IDEA,两分钟内找到RecommendService.java,指着selectBooksByUserIds方法说“就是这里”,那一刻,你交出的就不是一个毕设,而是一个程序员的底气。
简介:直接上手就能跑的图书推荐系统,后端用Spring Boot开发,前端用Vue.js实现响应式界面,数据存放在MySQL中。系统支持双角色操作:管理员能维护图书分类、上下架图书、处理预约退换、管理用户和留言;学生可以浏览首页、预约借书、办理退换、收藏喜欢的书、查看个性化推荐列表并提交反馈。压缩包里包含全部前后端源代码(SpringBoot+Vue结构清晰)、db.sql建库建表脚本、详细部署文档(明确列出JDK、Node.js、MySQL等环境版本要求)、PPT设计说明、开发环境配置清单、演示操作视频配套指南,以及整合好的部署调试压缩包。所有模块已在本地实测通过,兼容Windows和Linux系统,开箱即用,适合计算机专业学生做毕业设计、课程设计或Java全栈入门练习,覆盖用户权限控制、图书CRUD、借阅流程管理、简单协同过滤推荐逻辑、前后端分离接口对接等典型开发场景。
更多推荐


所有评论(0)