毕业设计年年都有,但“图书捐赠系统”这个题目每隔几届就会换张皮重新出现一次,后台技术从最早的JSP到SSH再到SSM,现在终于稳定在了SpringBoot+Vue这套组合上。我前阵子刚带完一个学弟做完这个题目,从选题开题到中期检查再到最终答辩全程走了一遍,踩了不少坑,也总结了一套相对稳妥的实施方案。这篇就把整个项目的设计思路、核心代码实现、前后端联调以及部署上线过程中真正有价值的东西梳理出来,给准备做类似题目的同学一个参考。
如果你拿到的是“基于Web的高校书籍图书捐赠系统的设计与实现”这种题,先别急着写代码,这个题目核心要解决的问题并不复杂:让捐赠者有地方登记要捐的书、让想要书的人能检索和申请、让管理员能审核和统计,就这么三件事。难点在于怎么把这套流程做得完整、靠谱,同时技术上保证一个毕设该有的工作量和技术深度。
1. 系统整体设计与技术选型思路
1.1 需求痛点与角色分析
高校图书捐赠这个场景比你想象中更刚需。每年毕业季宿舍楼下都会堆满被当成废纸卖的书,专业教材几十块钱一本收破烂只给几毛钱,而大一新生又要花原价去买。与此同时,学校的图书馆压根不会收教材类图书,辅导员办公室堆满了自愿捐赠的旧书却没人管理,最后只能清仓处理。
这个系统要解决的痛点就是信息不透明和流程不可控。我来梳理一下实际使用场景中的角色和操作,规划系统功能时基本上是围绕这三个角色展开的。
- 普通学生用户:可以注册登录、浏览图书列表、搜索图书、发起捐书申请、查看自己的捐赠记录和领取记录、收藏想要的书。
- 图书管理员:负责审核用户提交的图书信息,管理图书上下架,处理用户领取申请,维护图书分类和公告。
- 系统管理员:管理用户账号和权限,查看平台数据统计,处理异常反馈,管理整个系统的运行状态。
其中普通用户又天然分成两类:捐书者和受赠者,大多数时候是同一个人在不同场景下扮演的两种身份。我设计系统时强调了一点,就是不要把捐赠和领取设计成两个割裂的模块,而是围绕“图书”这条主线串起来:一本书从录入、审核、上架,到被申请领取、完成流转,生命周期完整可追踪。
1.2 为什么不是SSM也不是JSP,而是SpringBoot+Vue
这个选择其实没什么悬念,但是值得聊一下背后的逻辑,因为答辩时老师很喜欢问“为什么采用这个技术栈”。
SpringBoot相比传统的SSM框架,最大的价值在于大幅降低了配置成本。SSM那一套需要手动配置大量的XML文件:web.xml、spring-mvc.xml、spring-mybatis.xml,每个配置文件里还有一堆bean声明。SpringBoot把这些全部压缩成了自动配置,一个启动类加一个application.yml就能搞定,省下来的时间可以去做真正有价值的业务逻辑。而且SpringBoot内嵌了Tomcat,不需要再单独部署WAR包,开发调试体验比SSM好了不止一个级别。
前端选Vue的原因更简单,Vue的渐进式框架设计决定了它上手门槛低,模板语法直观,核心API说得上来的就那么几个。配合Element-UI组件库,后台管理页面基本靠“拼积木”就能搭出来,不需要自己手写复杂的CSS。而且Vue生态里的Vue Router和Vuex/Pinia对应路由和状态管理,恰好覆盖了这种中后台系统的所有需求。前后端分离以后,后端只提供JSON接口,前端只负责渲染数据,两边可以并行开发互不阻塞。
1.3 项目整体架构规划
整个系统按前后端分离的模式来设计,后端服务只处理业务逻辑和数据库交互,前端项目纯静态资源,开发阶段通过Vite/Webpack Dev Server代理请求到后端,生产环境用Nginx统一托管前端打包产物并反向代理API接口。这套架构目前是国内中小型团队做Web应用的绝对主流,用在学校毕业设计项目上绰绰有余。
后端项目结构我按经典的三层架构来划分:Controller层负责接收HTTP请求和参数校验,Service层负责具体业务逻辑处理,Mapper层操作数据库。再加一个common包放统一返回结果、全局异常处理、工具类,一个config包放跨域配置、拦截器配置、Mybatis-Plus配置。这样分层的核心价值是每一层的职责足够清晰,出了问题知道去哪找,写起代码来心里也有底。
前端按页面功能分成视图层、路由层、状态管理层,再用Axios统一封装HTTP请求。视图层就是一个个.vue文件,路由层负责URL到组件的映射,状态管理层用Pinia存用户登录信息和全局状态。页面之间不直接互相引用数据,所有数据流都经过接口,保证数据来源单一、可追踪。
2. 数据库设计与核心模块拆解
2.1 核心数据表结构设计
数据库设计直接决定系统能走多远。我见过很多毕设项目把一堆字段塞进一两张表里,答辩时被老师一问数据冗余就得愣半天。图书捐赠系统虽然不算复杂的业务,但该拆的表一张都不能少。
我个人常用的设计方案是五张核心表加两张辅助表。核心表分别是用户表、图书表、捐赠记录表、领取记录表、图书分类表;辅助表是公告表和收藏表。
用户表字段基本是标配:id、username、password、nickname、phone、email、avatar、role、status、create_time。注意role字段我用的是字符串类型的标识符,而不是数字枚举,比如admin代表管理员,user代表普通用户,这样代码里判断角色时语义更清晰。status字段标记账号是否被禁用,一个简单的0/1标记就能搞定拉黑功能。
图书表设计是重头戏,我的建议是字段宁可多设也不要漏。核心字段包含id、book_name、author、publisher、isbn、category_id、cover_url、description、donor_id、status、create_time、audit_time。其中status字段是整个系统的关键状态位,我用数字枚举来定义状态机:0代表待审核,1代表已上架,2代表已被申请领取,3代表已领取完成,4代表已下架。用一句话概括,图书表记录了“一本书从进系统到离开系统的完整生命轨迹”。
捐赠记录表和领取记录表分别记录两条业务线的操作流水。虽然这两个表字段有相似性(都涉及user_id、book_id、操作时间),但我坚持分表设计。原因很实际,毕业设计阶段做数据统计是加分项,分表后统计捐赠量和领取量只需要分别count两张表,逻辑一目了然。答辩时老师问“系统怎么统计某位同学的捐书数量”这种问题,你回答起来会非常顺畅。
2.2 为什么坚持用逻辑删除而不是物理删除
不少同学做毕设时喜欢用DELETE语句彻底删掉记录,这在小项目里不会立刻暴露问题,但一旦涉及用户行为和业务数据,物理删除的后果很严重。图书捐赠这种场景里,用户捐书、审核、领取这些操作是要留痕的,如果管理员把一本违规图书直接DELETE了,相关的捐赠记录就查不到了,数据统计也会对不上。
我的方案是给核心业务表都加上deleted字段,默认值为0,删除操作只是把deleted置为1,查询时默认过滤掉deleted=1的数据。使用Mybatis-Plus时,只需要在实体类字段上标注@TableLogic注解,框架就会自动帮你拼接过滤条件,删除时自动转成UPDATE操作,代码层面完全无感,但数据却保住了。这个细节可以作为答辩时的亮点来讲——你在系统设计时考虑了数据审计和可追溯性。
2.3 图书状态机的设计与流转
整个系统里最有逻辑含量的部分是图书状态的管理。我把状态流转画成了一条单向链:待审核 -> 已上架 -> 已被申请 -> 已领取完成,中间任何一个节点都可以被管理员操作转为已下架。
这个状态机设计有两个关键约束要特别注意。第一,状态只能向后流转,不允许回退变回上一个状态。比如一本书已经被某个用户申请领取了,就不能再改成待审核,只能取消申请回到已上架。第二,用户侧只能看到已上架状态的图书,未审核的图书只有提交者本人和管理员能看见。这两个约束保证了系统的数据一致性和用户体验的合理性。
实现状态流转时,后端Service层是唯一可以修改图书状态的地方,Controller层只负责接收前端传过来的操作指令。比如用户提交领书申请,Controller收到请求后调用Service层的applyBook方法,方法内部先校验图书状态是否为1(已上架),再校验申请者是否黑名单用户,全部通过后才把状态改成2并写入领取记录表。每一步校验都不能省,这是防止脏数据的最后一道闸门。
3. SpringBoot后端核心实现要点
3.1 项目初始化与依赖版本选型
SpringBoot项目初始化方式我推荐直接用IDEA的Spring Initializr,或者去start.spring.io生成压缩包导入。选版本时有一条血泪教训:别一上来就选最新的SpringBoot 3.x版本。如果你用的是JDK 8的环境,3.x根本跑不起来,它强制要求JDK 17起步。即便你机器上装了JDK 17,后续引入Mybatis-Plus、某些依赖时也可能遇到版本不兼容的坑,而网上搜到的教程大部分还停留在SpringBoot 2.x时代,报错信息对不上,排查起来极其痛苦。
我推荐的组合是SpringBoot 2.7.x + JDK 8 + Mybatis-Plus 3.5.x + MySQL 8.0 + Redis 2.x(如果用到缓存)。这套组合历经了大量生产项目验证,稳定性极高,遇到的任何问题都能在搜索引擎里找到现成的解决方案。用SpringBoot版本太高导致的各种奇怪问题,我在文末的排查表里会详细列出来。
核心依赖需要在pom.xml中配置的如下几项:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.18</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>你在写pom.xml时记得加一下这些依赖,缺哪个点哪个的引入原则会让你的代码越写越顺。Hutool工具箱强烈建议引入,无论是生成验证码、日期处理还是加密工具,它都封装好了现成的方法,能省非常多的重复代码。
3.2 统一返回结果与全局异常处理
前后端分离架构下,接口返回格式的统一是基本素养。我定义了一个统一的Result类,所有Controller方法的返回值都是这个类型。核心字段有三个:code(状态码)、message(提示信息)、data(业务数据)。业务正常时code为200,业务异常时code为500,未登录时code为401,权限不足时code为403。
@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注解的类,里面用@ExceptionHandler分别处理参数校验异常、业务异常、兜底的Exception异常。好处是Service层只管抛出异常,不用在每个方法里写try-catch去包装返回结果,代码整体会干净很多。这个设计在答辩时也可以展开说,它体现的是你对SpringAOP和异常处理机制的理解。
3.3 JWT登录认证与拦截器配置
用户登录模块是几乎每个系统都绕不开的标配。图书捐赠系统我选择了JWT(JSON Web Token)做无状态认证,而不是传统的Session方案。区别在于:Session方案把登录状态存在服务端内存里,每次请求都要查一次缓存,而且后端扩容时Session同步问题会很麻烦;JWT把用户信息加密后发给前端存着,后端每次请求只要验签解析就能拿到用户身份,天然适合前后端分离和横向扩展。
具体实现分三步。第一步在用户登录成功时生成JWT字符串返回给前端,我用的算法是HS256,秘钥直接写在application.yml配置文件中,payload里放userId和username两个关键字段,过期时间设为24小时。第二步在前端每次请求时把JWT放在Authorization请求头里携带。第三步在后端配置一个拦截器,拦截所有需要登录才能访问的接口,从请求头中取出JWT并解析验证,验证通过就把用户ID放进Request域中供后续业务使用。
有一个容易踩的坑是跨域请求时的预检请求OPTIONS,如果不放行会导致前端浏览器拿不到正确的响应头。所以拦截器里要先判断请求方法是不是OPTIONS,是的话直接放行,否则才做JWT的校验。
3.4 图书模块的完整实现流程
图书模块是整个系统的业务核心,包含了图书录入、图片上传、审核、检索、领取申请这一整套流程。我刚做完这套功能时有个体会,就是真正复杂的不在CRUD,而在流程中各个状态之间的协同。
先说图片上传。图书封面图片我直接用本地存储方案,在配置文件中指定上传目录,通过配置静态资源映射路径让外部通过URL访问,比如配置/upload/**映射到file:D:/upload/。这种方式部署时简单直接,不需要额外引入OSS之类的对象存储服务,对毕设项目来说已经够了。但要注意两点:文件名一定要重命名,否则不同用户上传同名文件会互相覆盖;上传接口要做文件类型和大小校验,我限制的是jpg/png格式、最大5MB。
图书录入的接口设计,我把它分成两个:提交图书信息和提交封面图片,先用图片接口拿到URL,再随图书信息一起提交。这样设计的好处是上传进度可控、失败重试成本低。前端展示时如果封面加载失败,需要备一张默认图书图片兜底,避免页面出现破图。
图书检索部分用的Mybatis-Plus的LambdaQueryWrapper做条件拼接。因为检索条件可能是空字符串,拼接SQL时要用StringUtils.isNotBlank做判断,条件满足才追加,这样能避免生成冗余的WHERE条件。分页功能直接用Mybatis-Plus的分页插件,只需要配置一个MybatisPlusInterceptor的Bean,设置PaginationInnerInterceptor就能自动完成分页。分页参数pageNum和pageSize由前端传入,后端返回包含总条数和当前页数据的分页对象。
审核这个环节很值得展开讲。用户提交的图书信息不会直接进入书库,而是先落入待审核状态,图书管理员登录后台后可以对每一本书进行审核。审核操作的接口设计成一个通用的方法:接收bookId和审核结果(通过/驳回),Service方法内部根据审核结果分别把状态改成1(已上架)或4(已下架),同时把审核时间写入audit_time字段。驳回时管理员还要填写驳回原因,用户在前端”我的捐赠“里能看到自己哪本书没通过、为什么没通过。这个闭环是系统体验感的关键。
3.5 Redis缓存的应用场景
虽然图书捐赠系统的并发量不大,但适当引入Redis会让你在答辩时多一个技术亮点。我的建议是在两个场景用Redis。
第一个场景是图书浏览量统计。图书详情页每次被打开就执行一次INCR操作,不直接更新MySQL,而是等缓存里的值累积到一定程度再异步回写数据库。这个方案在高并发场景下能有效减少数据库写入压力,作为毕设展示正好合适。
第二个场景是热点图书Top10榜。首页展示借阅量最高的10本书,这个数据不需要实时性,可以用Redis的ZSet(有序集合)来维护,每次图书被领取时score加1,前端查询Top10时直接从Redis取,性能极好。如果Redis里没有数据,就查一次数据库兜底并回填缓存。
记得给Redis的key加统一的业务前缀,比如book:detail:1代表id为1的图书的详情缓存,这样后面排查问题时候你能一眼看出这个key是干嘛用的。
4. Vue前端页面与交互实现
4.1 前端项目搭建与环境配置
前端项目用npm创建Vue项目时,我建议先确认好Node.js版本,Vue CLI创建的项目建议Node 14以上,但如果用的是Vite创建项目,Node版本要求会更高一些,最好16以上。创建命令如下:
# 使用Vue CLI创建项目(适用于Vue 2/3) npm install -g @vue/cli vue create book-donation-frontend # 或者使用Vite(Vue 3推荐) npm create vite@latest book-donation-frontend -- --template vue如果你之前没有配过npm镜像源,建议先执行一次npm config set registry https://registry.npmmirror.com,否则安装依赖的时候卡在进度条上半天不动是常态。vue安装依赖这个步骤看着简单,但网速不稳时重试好几次的人比比皆是,先改镜像源能省掉很大一块时间成本。
前端项目结构我按功能模块来划分,主要包含以下目录:
- src/api:按业务模块拆分的接口请求文件
- src/assets:静态资源
- src/components:公共组件
- src/router:路由配置文件
- src/store:Pinia状态管理(Vue 3)/ Vuex(Vue 2)
- src/views:页面级组件
- src/utils:工具函数
4.2 路由设计与登录守卫
路由配置是整个前端框架的骨架。图书捐赠系统的页面可以分成前台和后台两块:前台面向普通用户,包括首页图书列表、图书详情、个人中心、捐赠申请等页面;后台面向管理员,包括图书审核、用户管理、数据统计等页面。
const routes = [ { path: '/', component: () => import('@/views/Home.vue') }, { path: '/book/detail/:id', component: () => import('@/views/book/BookDetail.vue') }, { path: '/donate', component: () => import('@/views/donate/DonateForm.vue'), meta: { requiresAuth: true } }, { path: '/user/profile', component: () => import('@/views/user/UserProfile.vue'), meta: { requiresAuth: true } }, { path: '/admin', component: () => import('@/views/admin/AdminLayout.vue'), meta: { requiresAuth: true, requiresAdmin: true } }, ]路由守卫Code写起来也不复杂,在router/index.js里注册一个全局前置守卫:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next('/login') return } if (to.meta.requiresAdmin) { const role = localStorage.getItem('role') if (role !== 'admin') { next('/') return } } next() })这里有一个细节想提醒你:路由守卫只是前端层面的拦截,从安全角度来说后端接口必须再次做权限校验。前端守卫是为了用户体验,后端校验才是真正的安全防线,两者不能互相替代。
4.3 Axios封装与请求拦截器
前端和后端的接口对接是联调阶段最容易出问题的地方。我习惯把Axios单独封装成一个模块,统一配置baseURL和超时时间,然后在请求拦截器和响应拦截器里做统一处理。
import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const service = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:统一携带token service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = token } return config }) // 响应拦截器:统一处理错误码 service.interceptors.response.use( response => { const res = response.data if (res.code === 200) { return res } ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) }, error => { if (error.response?.status === 401) { localStorage.removeItem('token') router.push('/login') } ElMessage.error(error.message || '网络异常') return Promise.reject(error) } )把Axios封装好以后,每个页面里调用接口就非常清爽,比如图书列表页只需要写:
import { getBookList } from '@/api/book' const res = await getBookList({ pageNum: 1, pageSize: 10, keyword: 'Java' })开发模式下前端通过Vue CLI的devServer配置代理到后端,生产和联调之间完全无感迁移:
devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }这些配置里最值得注意的点是:baseURL统一是/api开头,和后端接口的RequestMapping前缀保持一致。生产环境Nginx上只需要写一条匹配/api的反向代理规则就能全部打通,省去大量的重复配置。
4.4 核心页面与组件的交互实现
图书列表页是整个系统用户访问量最大的页面,我采用卡片式布局展示图书封面、书名、作者、分类和状态标签。分页用的是Element-UI的Pagination组件,搜索区提供关键词模糊搜索和分类下拉筛选。这里有个小细节:搜索时用watch监听筛选条件变化,但要做防抖处理,否则用户每敲一个字母就会触发一次接口请求,既浪费资源又容易产生竞态问题。
图书详情页展示完整的图书信息和捐赠人信息,页面底部是操作区。关键交互点在于按钮的状态控制:图书状态为1(已上架)时显示“申请领取”按钮;如果当前登录用户就是捐赠者本人,按钮隐藏并显示“这是你捐赠的图书”;如果图书处于其他状态,按钮禁用并展示当前状态。这个细节能极大提升用户对系统状态机的感知。
捐赠申请页我设计成表单页,包含图书名称、作者、出版社、ISBN、分类、封面图上传、新旧程度、图书简介等字段。表单校验用Element-UI自带的rules规则,必填项用required标记,ISBN格式用正则表达式校验,图片上传使用el-upload组件并限制文件类型和大小。
管理员端的图书审核页是最能体现后台价值的地方。页面用表格展示所有待审核图书,操作列提供“通过”和“驳回”两个按钮,驳回时弹出对话框让管理员填写驳回原因。表格配合分页和状态筛选标签,管理员可以按待审核/已上架/已下架等状态快速过滤数据。这里我用了一个很实用的组件设计:把审核操作封装成独立的el-dialog组件,通过props传入当前行数据,通过事件通知父组件刷新列表,做到组件间低耦合。
5. 系统联调、部署与常见问题排查
5.1 前后端联调中的跨域问题
联调阶段遇到最多的就是跨域问题。浏览器出于安全策略限制,前端项目跑在8081端口(Vue CLI默认),后端跑在8080端口,两个端口不同,浏览器就会判定为跨域请求并直接拦截。
解决跨域有三种常见方式。第一种是后端加CORS配置类,实现WebMvcConfigurer接口,重写addCorsMappings方法,允许指定前端的源访问。第二种是前端配置devServer代理转发,实际开发中我更推荐这种,因为生产环境也是用Nginx反向代理,开发环境模拟生产环境的行为,两个环境表现一致,排查问题更容易。第三种是后端加@CrossOrigin注解在每个Controller上,虽然最省事,但每个类都要加注解,后续维护很烦人,而且对生产环境也不适用。
我给个最稳妥的方案:开发时用代理,生产时用Nginx。后端代码里不需要写任何跨域配置,所有跨域处理都交给代理层完成。
server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html/book-donation; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这个配置解决了两个问题:静态资源托管和后端接口反向代理。try_files $uri $uri/ /index.html是SPA应用的精髓,所有前端路由都指向index.html,由Vue Router接管页面渲染,刷新任何路径都不会404。
5.2 常见问题排查速查表
项目做到后期,你大概率会被这些坑绊倒。我整理了一张速查表,都是实测踩过的真问题。
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 前端请求接口报CORS错误 | 跨域配置未生效或代理配置错误 | 后端检查CORS过滤链顺序;前端检查devServer.proxy配置 |
| SpringBoot启动报端口被占用 | 8080端口被其他进程占用 | Windows下netstat -ano找PID并kill,Linux下lsof -i:8080 |
| 数据库连接报Public Key Retrieval错误 | MySQL 8的缓存SHA-2密码认证问题 | JDBC连接串添加allowPublicKeyRetrieval=true&useSSL=false |
| 数据库时间差了8小时 | MySQL默认时区和系统时区不一致 | JDBC连接串添加serverTimezone=Asia/Shanghai |
| 上传图片后通过URL访问404 | 静态资源映射未配置 | 配置类实现addResourceHandlers,映射/upload/** |
| Vue打包后页面白屏 | 静态资源路径相对路径问题 | vue.config.js设置publicPath: './' |
| 前端路由刷新后404 | Nginx未配置try_files规则 | 添加try_files $uri $uri/ /index.html |
| Mybatis-Plus分页失效 | 分页插件未注册 | 配置MybatisPlusInterceptor并添加PaginationInnerInterceptor |
| Vue打包后布局异常 | 部署路径和开发路径不一致 | 检查publicPath配置和图片/接口的绝对路径引用 |
5.3 安全防护措施与答辩加分项
安全方面容易被忽视,但恰恰是答辩时最能体现你系统思维的地方。图书捐赠系统虽然用户数据量不大,但基础的防护手段一定要做足。
第一是密码加密。用户密码绝对不能明文存储,我用的方案是MD5加盐再哈希。Spring Security的BCryptPasswordEncoder也可以,但引入Spring Security对毕设项目来说有点重,用Hutool的DigestUtil加盐值就行,实现简单效果好。
第二是接口鉴权。普通用户不能调用管理员接口,管理员也不能操作其他用户的数据,需要在后端接口层面做双重校验——除了JWT里解析的用户角色,还要校验资源归属。比如用户只能查看和编辑自己的捐赠记录,修改图书信息的接口必须校验图书的donor_id和当前登录用户id是否一致,防止水平越权。
第三是防止SQL注入。严格禁止使用字符串拼接SQL的方式,Mybatis-Plus的LambdaQueryWrapper在构造SQL时使用的是参数预编译机制,基本杜绝了注入风险。如果自己写XML里的SQL,一定要用${}拼接的地方加倍小心,能避免就避免,用#{}参数占位。
第四是XSS攻击防护。在全局异常处理和请求入口处对用户输入做HTML标签过滤,前端在展示用户输入的图书简介时用插值表达式{{}}而不是v-html指令,可以避免恶意脚本执行。
我觉得最值得加到答辩PPT里的点有两个:一是JWT无状态认证机制的原理图,面试官/老师听到你能把Token认证和Session认证的优缺点讲清楚,基本默认你是真的理解了而不是背代码;二是图书状态机的完整流转图,这体现了你对业务闭环的思考深度。
5.4 部署上线的完整流程
项目做完到上线这一步,其实就是把两条线合并成一条线。后端打包用Maven命令mvn clean package -DskipTests,生成target目录下的jar包,上传服务器后用java -jar book-donation-0.0.1-SNAPSHOT.jar直接跑。如果服务器内存有限,可以用nohup java -jar xxx.jar > app.log 2>&1 &让它在后台运行并把日志输出到文件里。
前端打包用npm run build,生成dist目录,把里面的内容上传到Nginx的html/book-donation目录下。生产环境里,Mybatis-Plus默认打印的SQL日志会拖慢一定的性能,记得在application.yml里把日志级别调成info,只在排查问题的时候临时打开debug。
数据库初始化方面,我建议直接用SQL脚本建库建表,不要把数据操作写在代码里。数据库服务器单独准备一份完整的数据库设计文档,包括ER图、表结构说明、字段含义注释,这些材料答辩时要装订成册的。
6. 写在最后的实践经验总结
我带队做过几次类似的项目,有一点体会特别深:系统本身的复杂度其实不高,真正拉开差距的是细节的完成度和思考的深度。同样是图书捐赠系统,有人做到图书上传、审核、领取、统计一条龙闭环,管理员后台做得有模有样;有人做成一个简单的增删改查,效果天差地别。
要是照着做,有几个点我要单独拎出来提醒大家。第一,数据库建表时把外键关系理清楚,但真正建表时不要物理外键,所有关联都在Service层通过逻辑关联处理,这样做数据迁移和分页查询时性能更好。第二,表字段不要用关键字和MySQL内置函数名,比如description这种,虽然目前没出问题,但还是要养成好习惯。第三,前端表单校验至少要覆盖必填项和格式校验,一个允许用户提交空表单的系统在答辩时会显得很业余。
最后再分享一个实用的小技巧:整个项目写完后,把核心的业务流程走一遍,从注册登录、捐赠图书、管理员审核、用户领取、数据统计,每个环节截一张图,整理成项目功能演示文档。答辩的时候放PPT,老师看着直观,你也省得临时操作翻车。我把这个习惯保持到今天,每次做新项目都受益匪浅。