简介这份毕业设计资料包围绕基于Springboot、SpringMVC、Mybatis和小程序的图书捐赠管理系统设计与实现展开面向计算机相关专业毕业生或需要快速搭建全栈项目的开发者解决从后端接口、数据库设计到小程序前端联调的一体化需求。压缩包共856个文件、约42.96MB涵盖java/class/jar等后端源码与依赖、jsp/xml/wxss/wxml等小程序与前端页面、sql数据库初始化脚本以及项目配置、文档与毕业设计论文目录结构清晰便于直接导入开发工具运行与学习。已有143人学习浏览适合毕业设计选题、项目实战复现或二次开发参考。通过该系统可掌握Springboot自动配置、SpringMVC请求分发、Mybatis持久层映射和小程序端交互的完整实现思路同时结合捐赠流程、社区模块等业务场景理解从表结构设计到上线部署的工程化流程。1. 图书捐赠管理系统是什么一条链路三个角色这套技术栈为什么够用“SpringbootSpringMVCMybatis小程序的图书捐赠管理系统”是毕业设计和实训项目里出现频率极高的一道题后端用 SpringBoot 搭基础框架SpringMVC 处理路由与拦截MyBatis 映射 SQL前端用微信小程序承载用户端管理端既可以单独做网页也可以复用小程序里的管理员入口。整套系统没有高并发、没有分布式、没有微服务但它能干净地跑通图书从登记、审核、上架、申请领取到统计归档的全流程正好覆盖一个中小型校内场景的真实需求。适合应届生做毕设、Spring 全家桶初学者练手也适合社团或社区快速搭一个轻量捐赠工具。市面上同名源码包不少但多数要么 SpringBoot 版本太旧要么小程序端根本编译不过要么论文和代码对不上。这篇笔记按可复现的方式把方案说透表结构怎么设计、捐赠流程的状态机怎么定、接口怎么定义、小程序怎么对接、部署联调有哪些坑以及怎样从“演示能过”做到“验收能过”。2. 整体架构与服务分层SpringBoot、SpringMVC、MyBatis 怎么各司其职2.1 三个后端组件不是重复轮子职责边界与选型理由很多人拿到这个题目第一反应是“SpringBoot 不就已经包含 SpringMVC 了吗为什么标题还要单独写一个 SpringMVC”这个问题在开题和答辩时几乎必被问到必须能接住。SpringBoot 负责的是自动配置、内嵌容器和启动编排它让项目不用再写一堆 XML 配置SpringMVC 负责 Web 层的请求分发、参数绑定、拦截器和视图解析在这套系统里具体表现为 Controller 层的路由定义和登录拦截MyBatis 负责 DAO 层的 SQL 映射把 Java 方法和 XML 里的 SQL 绑定起来。三者是“地基—楼层—管线”的关系SpringBoot 把所有零件装好SpringMVC 决定请求进来走哪条路MyBatis 决定数据怎么落库、怎么查出来。选型理由在论文里值得单独写一小节为什么不选 JPA图书列表的筛选条件非常动态——按书名模糊、按分类、按状态、按捐赠人组合方式很多MyBatis 的动态 SQL 写起来直观且可控统计报表捐赠排行、分类占比需要手写聚合 SQL用 MyBatis 可以直接贴近数据库方言调优。为什么不选更老的 SSM 手工配置因为 SpringBoot 能省掉大量 bean 注册代码把精力留给业务本身。为什么不选 MyBatis-Plus因为题目明确写的是 MyBatis论文核心概念不能和题目脱节Plus 可以在“扩展与展望”里提但主链路必须用原生 MyBatis 写清楚。前端为什么用小程序而不是 H5因为微信小程序不需要用户安装 App扫码即用分享到班级群或社区群很方便图书捐赠这种低频、强信任、弱社交的场景小程序比独立 App 轻得多也比 H5 更容易触达用户。开发上原生小程序最稳妥微信开发者工具直接预览不需要额外引入 uni-app 或 Taro 的编译链路对毕设来说少一层不确定性。2.2 一次捐赠请求从微信到 MySQL 的完整路径把一次完整捐赠流程画成时序能帮助理解每一层的职责。用户在小程序里登记一本图书提交后前端调用POST /api/book/add请求先进入 SpringMVC 的 DispatcherServlet被拦截器校验 token校验通过后路由到 BookControllerController 做参数校验后调用 BookServiceService 里处理业务规则比如判断用户是否存在、分类是否合法最终由 BookMapper 接口执行 XML 里的 INSERT SQL 把记录写进 MySQL。紧接着是管理端的审核动作。管理员在后台看到待审核列表点击通过后调用POST /api/donation/approve同样走 Controller → Service → Mapper 的链路但这里多了一个事务边界要把 book 表的 status 从 0 改成 1同时生成一条审核记录两步必须同时成功或同时失败。这就是 Service 层加Transactional的原因也是论文里“事务与数据一致性”这一节的核心素材。这条路径里还藏着两个容易被忽略的角色。一个是 SpringMVC 拦截器HandlerInterceptor它在请求进入 Controller 之前检查 Authorization 头token 不存在或过期直接返回 401不需要在每一个 Controller 方法里重复写登录判断。另一个是全局异常处理器RestControllerAdvice它把业务异常统一转成{ code: 500, message: 图书状态已变化 }这样的结构小程序端只需解析一种返回格式。donation-api ├── pom.xml ├── src/main/java/com/example/donation │ ├── DonationApplication.java │ ├── config │ │ ├── WebMvcConfig.java │ │ └── WxConfig.java │ ├── controller │ │ ├── AuthController.java │ │ ├── BookController.java │ │ ├── DonationController.java │ │ └── FileController.java │ ├── service │ │ ├── BookService.java │ │ ├── DonationService.java │ │ └── UserService.java │ ├── mapper │ │ ├── BookMapper.java │ │ ├── DonationMapper.java │ │ └── UserMapper.java │ ├── entity │ ├── dto │ └── common │ ├── Result.java │ └── GlobalExceptionHandler.java └── src/main/resources ├── application.yml └── mapper ├── BookMapper.xml └── DonationMapper.xml这是后端项目结构按 controller / service / mapper / entity / common 分层。controller 里只做参数接收和简单校验业务逻辑全部下沉到 servicemapper 接口和 resources/mapper 下的 XML 一一对应。common 包里放统一返回结构 Result 和全局异常处理。这个结构在论文的系统设计章节可以直接当作模块划分图来写。后端接口的返回体统一使用 Result 包装前端不用再猜字段。这里给出常见写法Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.code 200; r.message success; r.data data; return r; } public static T ResultT fail(String message) { ResultT r new Result(); r.code 500; r.message message; return r; } }code 用 200 表示成功、401 表示未登录、500 表示业务失败message 直接携带给用户看的提示。逻辑上把成功与失败封装成静态方法业务代码里一句return Result.ok(data)就能返回标准结构。参数上如果后续要扩展分页、traceId直接在泛型 T 里加字段即可不用改前端解析逻辑。然后是登录拦截器的核心逻辑public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !TokenStore.isValid(token)) { response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录或登录已过期\}); return false; } return true; } }拦截器里不要做重活只判断 token 是否存在并且有效。TokenStore 可以是一个内存 Map也可以替换成 Redis。注意返回 401 时要把响应体写成 JSON否则前端按 JSON 解析会直接报错。注册这个拦截器时记得 excludePath 掉登录接口/api/auth/login和文件访问路径否则会出现“登录取不到 token”的死循环。2.3 项目骨架与统一返回、拦截器先把地基打好地基部分的坑集中在三点。第一SpringBoot 版本与 JDK 匹配我的习惯是直接用 SpringBoot 2.7.x JDK 8 或 11避开 3.x 的 jakarta 命名空间迁移问题这与 MyBatis-Spring-Boot-Starter 的兼容性最稳。第二统一返回结构要在一开始就定好不要做了一半再改前端封装的所有解析逻辑都依赖这个结构。第三拦截器要区分“接口路径”和“静态资源路径”本地图片上传后要通过资源映射访问如果把静态路径也拦截了前端图片全部裂掉。关于 MyBatis 的配置application.yml 里有几项建议直接写好。map-underscore-to-camel-case: true解决下划线字段到驼峰属性的映射log-impl: org.apache.ibatis.logging.stdout.StdOutImpl在开发期打印 SQL排查问题能直接看到参数值cache-enabled我习惯关掉毕设项目没有高频读场景MyBatis 二级缓存反而容易带来“改完数据查出来还是旧的”的玄学问题不如靠 MySQL 本身。这三项配置在论文里也能作为“系统非功能性设计”的一部分描述。3. 表结构与捐赠流程状态机让发布、审核、领取有据可依3.1 五张核心表字段、状态枚举与逻辑删除图书捐赠系统的业务模型不复杂但表关系要设计得能自圆其说。常见做法是五张核心表用户表 user、图书表 book、分类表 category、捐赠申请/审核表 donation、领取记录表 record。下面这张表概括了每张表的职责表名用途关键字段user微信用户与管理员id, openid, nickname, avatar, role, create_timebook图书信息与状态id, title, author, isbn, cover_url, category_id, owner_id, statuscategory图书分类id, name, sortdonation捐赠登记与审核流id, book_id, applicant_id, reason, statusrecord领取记录id, book_id, user_id, take_timebook 表的 status 是整个系统的核心状态位用 TINYINT 而不是字符串四个取值要写清楚0 待审核1 可领取2 已捐出3 已下架。donation 表的 status 负责审核流0 申领中1 同意2 拒绝。状态用数字的好处是 MyBatis 的if teststatus ! null判断简单SQL 也更短代价是可读性差一点所以建表时每个字段的 COMMENT 必须写清楚论文的数据字典部分直接照抄。再补充两个所有表都该有的字段is_deleted做逻辑删除create_time做记录时间。逻辑删除意味着业务代码里所有查询都要带WHERE is_deleted 0批量删除时执行的是 UPDATE 而不是 DELETE。答辩时老师喜欢问“为什么不用物理删除”回答口径是捐赠记录涉及用户行为留痕删除用户或图书只应隐藏而不应抹除历史否则统计报表的数据会断档。核心建表脚本书如下字段精简到能跑通业务CREATE TABLE book ( id int NOT NULL AUTO_INCREMENT, title varchar(100) NOT NULL COMMENT 书名, author varchar(50) DEFAULT NULL COMMENT 作者, isbn varchar(20) DEFAULT NULL COMMENT ISBN, cover_url varchar(255) DEFAULT NULL COMMENT 封面URL, category_id int DEFAULT NULL COMMENT 分类ID, owner_id int DEFAULT NULL COMMENT 登记人(捐赠人), status tinyint NOT NULL DEFAULT 0 COMMENT 0待审核 1可领取 2已捐出 3下架, is_deleted tinyint NOT NULL DEFAULT 0 COMMENT 逻辑删除, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书表;字段说明title 用 varchar(100) 覆盖绝大多数图书标题status 默认 0即新登记的图书必须经过管理员审核才能被别人看到cover_url 只存相对路径或对象存储 key不存 base64 图片流create_time 用数据库默认值应用层不需要手动 set。索引只建了 status因为主查询路径是“按状态刷列表”等数据量上万后再加复合索引毕设阶段不过度设计。3.2 用 MyBatis 动态 SQL 实现图书多条件筛选图书列表页几乎必然要支持多条件组合查询关键词模糊搜标题、按分类过滤、按状态过滤、按捐赠人过滤。这类需求在 MyBatis 里的标准解法是where加if组合下面这段 XML 是核心查询select idlistBooks resultTypecom.example.donation.entity.Book SELECT id, title, author, isbn, cover_url, category_id, owner_id, status, create_time FROM book where is_deleted 0 if testkeyword ! null and keyword ! AND (title LIKE CONCAT(%, #{keyword}, %) OR author LIKE CONCAT(%, #{keyword}, %)) /if if testcategoryId ! null AND category_id #{categoryId} /if if teststatus ! null AND status #{status} /if if testownerId ! null AND owner_id #{ownerId} /if /where ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} /select逻辑说明where标签会自动去掉第一个多余 AND所以is_deleted 0写在最前面作为恒成立条件时后续 sql 片段直接写AND ...不会出错。keyword 的判断要同时判 null 和空串否则前端传空字符串时会拼出LIKE %%造成全表扫categoryId、status、ownerId 这些包装类型只用判! null即可因为状态 0 是合法值不能按空串逻辑处理。LIMIT 用手写 offset 的方式PageHelper 虽然方便但毕设里手写分页更容易在答辩时把 SQL 讲清楚。这里还要注意一个参数命名问题。Mapper 接口方法如果有多个参数必须加Param注解否则 MyBatis 无法通过#{keyword}取到值。常见写法是ListBook listBooks(Param(keyword) String keyword, Param(categoryId) Integer categoryId, Param(status) Integer status, Param(ownerId) Integer ownerId, Param(offset) Integer offset, Param(pageSize) Integer pageSize);参数说明所有条件都允许为 null通过动态 SQL 决定是否参与过滤offset 和 pageSize 由前端分页组件传入库里只做简单 limit。我第一次写这种多条件查询时翻过车忘了给参数加 Param结果 MyBatis 报Parameter keyword not found这个报错信息很直接看到就知道是注解漏了。3.3 捐赠审核事务用 status 做乐观锁防止重复处理多管理员同时审核一本书或者用户重复点击“申请领取”会产生并发问题。处理手法是拿 status 字段做乐观锁更新时带上“当前状态必须是某个值”的条件影响行数为 0 就说明状态已经被别人改过。Transactional(rollbackFor Exception.class) public void approveDonation(Integer donationId) { DonationApply apply donationMapper.selectById(donationId); if (apply null || apply.getStatus() ! 0) { throw new BizException(该申请已被处理请刷新后重试); } int rows bookMapper.compareAndSetStatus(apply.getBookId(), 1, 2); if (rows 0) { throw new BizException(图书状态已变化请刷新后重试); } donationMapper.updateStatus(donationId, 1); recordMapper.insert(new Record(apply.getBookId(), apply.getApplicantId())); }逻辑说明第一步查申请单并校验状态第二步用UPDATE book SET status 2 WHERE id ? AND status 1这样的原子操作把图书从“可领取”改成“已捐出”受影响行数是 1 才继续往下走。这两步之间即使有两个管理员同时操作数据库的行锁也会保证只有一个请求更新成功另一个影响行数为 0 直接抛异常不会产生“一本书被两个人同时领走”的数据错误。事务注解保证后面两步更新申请单、插入领取记录和第二步一起成功或一起回滚。这里要解释一个概念为什么不用悲观锁毕设数据量下悲观锁SELECT FOR UPDATE没必要还会占用连接乐观锁通过条件更新实现代码简单且并发安全够用。答辩时把这个对比讲清楚比堆一堆理论更能得分。4. 小程序端对接与联调登录态、请求封装与图片上传4.1 wx.login 换自建 tokencode2Session 只能放在后端小程序的登录链路和传统 Web 登录完全不同。前端调用wx.login拿到的 code 是一次性临时凭证不能直接当作身份标识真正的用户身份是后端拿 code 去微信接口换来的 openid。整个流程的正确做法是小程序把 code 发给自己的后端后端再携带 appid 和 secret 请求微信的 jscode2session 接口得到 openid 和 session_key然后为这个用户生成一个自建 token 返回前端。为什么要绕这一圈因为 appid 和 secret 属于后端机密配置一旦写进小程序前端代码等于把密钥公开在每一个用户手机上。session_key 是解密手机号等敏感数据的钥匙也绝对不能下发到前端。这两个原则在论文安全设计部分要单独强调。后端核心代码public String code2Session(String code) { String url https://api.weixin.qq.com/sns/jscode2session ?appid wxConfig.getAppid() secret wxConfig.getSecret() js_code code grant_typeauthorization_code; String body restTemplate.getForObject(url, String.class); JSONObject json JSON.parseObject(body); if (json.getString(openid) null) { throw new BizException(微信登录失败: json.getString(errmsg)); } return json.getString(openid); }逻辑说明RestTemplate 拼参数时要注意 appid 和 secret 需要 URL 编码不过标准的 appid 和 secret 字符集不会触发编码问题返回的 JSON 里只要没有 openid就说明 code 失效或参数错误把 errmsg 原样抛给前端做提示。拿到 openid 后先查 user 表不存在则自动注册存在则直接更新最近登录时间然后生成 token。生成 token 有两种常见做法UUID 随机串或 JWT。如果是毕设我建议用 UUID 加上一个服务端 TokenStore 存储映射关系好处是可控性强——管理员可以随时踢人下线只要删掉 Store 里的记录JWT 的优势是无状态但在小程序场景里 token 本来就要存缓存服务端状态化反而便于管理。if 你要写 JWT 作为加分项注意把过期时间设置在 7 天以内并与小程序的登录态同步失效。这里附带说明一下“获取手机号”。微信要求手机号必须通过button open-typegetPhoneNumber用户主动点击授权不能通过 wx.login 直接拿。拿到的是加密数据需要后端用 session_key 解密。这属于用户画像增强功能应届生毕设不必强做做了就要额外处理密文解密和敏感信息脱敏展示性价比不高。论文里可以作为“系统扩展功能”列出不放进主流程。4.2 封装 request.js401 跳登录统一错误提示小程序端如果每个页面都直接调 wx.request代码会迅速失控。我的做法是先做一层封装把 baseURL、token 注入、错误码处理、加载态统一收口。下面的代码是完整版 request.jsconst BASE_URL http://localhost:8080/api; function request(path, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method: method, data: data, header: { Authorization: wx.getStorageSync(token) || }, success(res) { const body res.data; if (body.code 200) { resolve(body.data); } else if (body.code 401) { wx.removeStorageSync(token); wx.navigateTo({ url: /pages/login/login }); reject(new Error(body.message)); } else { wx.showToast({ title: body.message, icon: none }); reject(new Error(body.message)); } }, fail(err) { wx.showToast({ title: 网络异常请检查后端服务, icon: none }); reject(err); } }); }); } module.exports request;逻辑说明BASE_URL 在开发期填电脑局域网 IP 加后端端口真机预览时手机和电脑必须在同一局域网上线后改成 HTTPS 域名。header 里从本地缓存取 token没有则为空字符串。成功回调先判断业务码200 直接返回 data401 清除本地 token 并跳登录页其他码用 toast 展示后端给出的提示。fail 分支负责网络层错误比如后端没启动、域名不通、证书错误。所有业务代码只需要request(/book/list, GET, { status: 1 })这样一行不用关心底层细节。参数说明method 不传时默认 GETdata 在 GET 请求中会被 wx.request 自动拼到 queryPOST 时放进 body。如果你习惯用 async/await把函数声明改成 async 并在内部 return Promise外层就能直接 await。一个容易被忽视的小坑是 loading 提示我一般不在封装里强制加而是在具体业务里根据场景用 wx.showLoading避免列表页刷新时频繁闪动弹窗。4.3 图片上传与静态资源映射临时路径到持久化 URL图书封面是捐赠系统的刚需功能。小程序端wx.chooseMedia选择图片后拿到的是本地临时路径这个路径只在当前小程序会话内有效必须通过上传接口把它发送到后端保存。前端代码wx.chooseMedia({ count: 1, mediaType: [image], success(res) { const tempPath res.tempFiles[0].tempFilePath; wx.uploadFile({ url: BASE_URL /file/upload, filePath: tempPath, name: file, success(res) { const body JSON.parse(res.data); formData.coverUrl body.data; } }); } });逻辑说明chooseMedia 的 tempFilePath 是本地缓存地址直接用image src展示没问题一旦用户删掉小程序缓存或换设备就失效。上传后后端返回的应该是可持久访问的 URL前端把它存到 formData提交图书信息时一并传给后端保存。name 字段是后端接口接收文件的参数名前后端必须约定一致否则报Required request part file is not present。后端接收上传时最常见的坑是把文件写进项目 resources 目录。SpringBoot 打成 jar 包后resources 目录在容器内部是只读的运行时写入会报 IOException即使开发期成功重启后文件也可能被覆盖或丢失。正确做法是在服务器或本机指定一个外部目录并在 WebMvcConfig 里配置静态资源映射Configuration public class WebMvcConfig implements WebMvcConfigurer { Value(${upload.dir}) private String uploadDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadDir /); } }逻辑说明上传接口把文件保存到 uploadDir 指定的目录接口返回/upload/xxx.jpg这样的相对路径浏览器或小程序访问时请求打到/upload/**SpringMVC 把它映射到磁盘上的绝对路径。这套方案无论开发期还是打包部署后都稳定只要保证 uploadDir 指向一个有写权限的目录。application.yml 里用绝对路径配置 upload.dir部署时通过启动参数覆盖不要硬编码在代码里。这个点我在后面避坑章节还会展开因为它几乎是毕设联调阶段必炸的一环。5. 联调与部署避坑5 条高频翻车记录5.1 真机请求报 url not in domain list现象微信开发者工具里接口通通正常一预览到真机所有请求全部失败报错信息是url not in domain list或request:fail。原因微信小程序对请求地址有安全限制真机环境下只能访问已配置的业务域名且必须 HTTPS开发者在工具里看到的“正常”是因为工具默认打开了“不校验合法域名”选项真机不会沿用这个设置。解决开发调试阶段在微信开发者工具右上角“详情 → 本地设置”勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”。注意这个勾选只对工具当前项目生效并且真机预览时手机端的调试功能要保持开启。如果是体验版或正式版必须在小程序后台配置 request 合法域名和 uploadFile 合法域名域名要有备案和 HTTPS 证书。血泪经验是每次切换电脑或换手机预览前先检查这个勾选是不是还亮着它经常被 IDE 重置。5.2 MyBatis 查询结果全是 null现象SQL 在 Navicat 里跑得好好的通过接口查出来的实体对象所有字段都是 null只有 id 有值或 id 也为 null。原因绝大多数是数据库字段是下划线命名create_time实体属性是驼峰命名createTimeMyBatis 默认不开启自动驼峰转换导致字段映射不上。其次是 resultType 写成了基本类型或错误包名但没有编译期报错只在运行期返回空对象。解决在 application.yml 加一行配置这是最稳的根治手段mybatis: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl配置说明map-underscore-to-camel-case 开启后create_time 会自动映射到 createTime不用在 XML 里写大段 resultMap。log-impl 的作用是在控制台打印完整 SQL 和参数值排查时一眼就能看出是查询条件写错还是映射问题。如果加完配置还出现 null检查实体类字段是否真的叫 createTime 而不是 createtime检查 resultType 是否写的全限定名。还有一个隐藏点查询别名与原字段冲突时以别名优先避免在 SQL 里同时出现带别名的列和不带别名的列混用。5.3 上传图片重启服务就消失现象开发时上传图片当时能显示重启 SpringBoot 或重新打包后图片全部 404目录里也找不到文件。原因上传代码把文件写进了项目相对路径比如new File(src/main/resources/static/upload)。开发工具运行时会解析成工作区目录文件看似存在一旦打成 jar 用java -jar启动相对路径变成 jar 包内部的临时目录重启后临时目录被清空或根本不可写。解决使用外部绝对路径。application.yml 中配置upload.dir: /data/donation-uploadWindows 则写D:/donation-upload上传时用这个配置拼接文件完整路径WebMvcConfig 里把/upload/**映射到该目录。部署到服务器后这个目录要提前创建并确保运行用户有写权限。更进阶的做法是接对象存储但毕设阶段本地磁盘方案完全够用论文里把外部目录的设计讲清楚即可。5.4 Transactional 没生效审核记录莫名丢失现象审核图书时页面提示成功但数据库里 book 表状态变了donation 表和 record 表却没有记录数据对不上。原因事务没有生效。常见情况有三种一是同一个类里this.approveDonation(donationId)调本类另一个Transactional方法Spring 的代理机制拦不住 this 调用事务被跳过二是方法不是 public 导致 AOP 代理不参与三是方法内部 try-catch 把异常吞掉事务感知不到失败就提交了。解决事务方法必须通过注入的 Bean 调用不要用 this 调方法权限用 public不要在事务方法体内 catch 业务异常后正常返回让异常抛给 Spring 触发回滚。审计接口属于后置操作单独拆成一个AuditService.record()并在事务方法里调用比在事务内写一堆 try-catch 干净得多。我自己排查这一类问题时会先看控制台有没有TransactionInterceptor的日志再看方法调用链最后检查异常是不是被吞了按这个顺序基本能定位。5.5 SpringBoot 版本太高MyBatis 启动就报错现象新建项目时选了 Spring Initializr 默认的 SpringBoot 3.x引入mybatis-spring-boot-starter后启动报AbstractMethodError或ClassNotFoundException: javax.servlet.*。原因SpringBoot 3.x 把 JavaEE 迁移到 Jakarta EE包名从javax.servlet变成了jakarta.servlet同时旧版 MyBatis starter 是基于 SpringBoot 2.x 编译的在 3.x 环境下出现二进制不兼容。这个问题在热词里也常见很多人问“springboot版本太高怎么办”。解决毕设项目直接用 SpringBoot 2.7.x不追新。pom 里锁定版本parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent配合 JDK 8 或 11MyBatis 依赖用mybatis-spring-boot-starter:2.3.2三者兼容性稳定网上排错资料也最多。如果导师或题目强制要求 SpringBoot 3.x那就必须换mybatis-spring-boot-starter:3.0.3同时检查所有涉及javax.servlet的代码改成jakarta.servlet。我的习惯是拿到项目先看 pom 和 JDK 版本这个动作能避开后面一大半坑。6. 从“能演示”到“能验收”统计 SQL、接口回归与上线检查清单6.1 给答辩加分的三张统计报表捐赠系统光能增删改查只能算及格论文里如果能展示统计功能评委的印象会明显不一样。第一张是分类捐赠排行按图书分类统计捐出数量SELECT c.name AS category_name, COUNT(*) AS donate_count FROM book b LEFT JOIN category c ON b.category_id c.id WHERE b.status 2 GROUP BY c.name ORDER BY donate_count DESC;第二张是近六个月捐赠趋势用 DATE_FORMAT 按月份聚合SELECT DATE_FORMAT(create_time, %Y-%m) AS month, COUNT(*) AS cnt FROM book WHERE status 2 AND create_time DATE_SUB(CURDATE(), INTERVAL 6 MONTH) GROUP BY DATE_FORMAT(create_time, %Y-%m) ORDER BY month;第三张是用户捐赠贡献榜关联 user 表统计每人捐了多少本。这三张表的数据可以放进论文的“系统测试”章节配合图表展示比纯文字描述有说服力得多。实际开发中这三张 SQL 也正好验证了 MyBatis 处理聚合查询的能力答辩时被问到“为什么不用 JPA”这是现成的论据。6.2 验收前用 curl 把核心链路跑一遍正式提交前我会按“登录 → 登记图书 → 审核 → 领取 → 统计”的顺序用 curl 做一遍接口回归。这个习惯帮我抓住了很多只在界面操作时发现不了的问题比如 token 过期时间设置过短导致页面跳转异常。# 登录获取 token TOKEN$(curl -s -X POST http://localhost:8080/api/auth/login \ -H Content-Type: application/json \ -d {code:test-code-001} | jq -r .data.token) # 登记一本图书 curl -s -X POST http://localhost:8080/api/book/add \ -H Authorization: $TOKEN \ -H Content-Type: application/json \ -d {title:实战Java,categoryId:1,author:张三} # 查询待审核列表 curl -s http://localhost:8080/api/book/list?status0 \ -H Authorization: $TOKEN参数说明code 在开发环境可以用一个每次登录都会变化的测试值后端用假的 code 也能走到后面的逻辑jq 是 Linux 下解析 JSON 的工具没装可以用 Python 或直接肉眼读返回。回归时重点看每一步的返回码和关键业务码比如登记后图书状态应为 0审核通过后状态应变为 1。如果条件允许再用 JMeter 录一个简单的并发脚本模拟两个管理员同时审核同一本书观察乐观锁是否生效。这些验证结论写进论文的测试章节比“系统运行正常”这种空话可信得多。6.3 上线前那几件琐碎但致命的事第一小程序后台的合法域名配置。request、uploadFile、downloadFile 三类域名要分别配置且必须 HTTPS。很多项目部署完接口通图片传不上就是因为 uploadFile 域名没配。第二服务器时区。MySQL 连接串加serverTimezoneAsia/Shanghai否则日期字段差 8 小时统计报表的月份分组会错位。第三数据库字符集统一 utf8mb4图书标题里可能出现生僻字或 emojiutf8mb4 才存得下。第四Spring 的 profile 切换。application-dev.yml 和 application-prod.yml 分开部署时通过--spring.profiles.activeprod指定避免开发期的调试配置泄露到生产环境。我的个人习惯是交付前固定跑一遍“上传图片 → 重启服务 → 刷新页面”的组合拳确保静态资源映射没有失效再跑一遍 curl 回归确认核心链路。这套动作看着简单但每次都能在最后一刻捞出问题。有一次就是这样发现上传目录的权限在重启后变了花了半小时才定位是部署用户和目录属主不一致。提前把这些琐碎环节处理好后面的人接手时会少很多怨气希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?