ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot+MySQL图书馆管理系统实战:从架构设计到事务处理详解

SpringBoot+MySQL图书馆管理系统实战:从架构设计到事务处理详解 简介这是一套面向计算机专业本科生的毕业设计与课程设计实践资源基于Spring Boot框架与MySQL数据库实现完整的图书馆管理系统覆盖前后端分离开发、权限控制与业务闭环管理等典型Web应用开发能力训练。资源包含385个文件以Java源码30个、HTML页面42个、CSS样式30个、JavaScript脚本56个及SQL建表语句为核心辅以GIF操作示意、SVG图标与配置文件整体压缩包仅6.24MB轻量易部署。已有1587人学习下载适用于Java Web开发入门到进阶的学习者可直接运行验证登录认证、读者借阅查询、后台图书与用户管理等全部功能模块并参考完整文档理解MVC分层设计、Service层事务处理及Controller响应逻辑代码结构清晰、命名规范具备良好的教学示范性与工程参考价值。1. 项目概述与核心价值最近在整理过往项目时翻到了一个几年前做的图书馆管理系统用的是Springboot和MySQL这套经典组合。当时是为了解决一个社区图书馆手工登记效率低、图书盘点混乱的实际问题而开发的。虽然现在微服务、云原生概念满天飞但这种单体架构、技术栈清晰的项目对于初学者理解企业级应用开发的全流程或者对于需要快速交付一个稳定可靠的管理系统的团队来说依然具有极高的参考价值。这个项目麻雀虽小五脏俱全涵盖了用户认证、图书信息管理、借阅归还、数据统计等核心业务并且附带了完整的源码和设计文档。今天我就把这个项目的设计思路、关键技术实现细节以及我踩过的一些“坑”系统地梳理出来希望能给正在学习Springboot或需要开发类似管理系统的朋友一些实实在在的启发和可以直接“抄作业”的代码。2. 系统整体架构与设计思路拆解2.1 业务需求分析与模块划分做任何系统之前理清业务需求是第一步。图书馆管理系统的核心业务其实非常典型主要围绕“书”和“人”展开。经过与当时图书馆管理员的多次沟通我们梳理出了以下几个核心模块用户管理模块区分管理员与普通读者。管理员拥有全部权限负责图书入库、信息修改、用户管理等读者则只能进行图书查询、个人借阅记录查看、借书/还书操作。图书信息管理模块这是系统的基石。需要记录图书的ISBN、书名、作者、出版社、出版日期、价格、库存数量、分类、简介等详细信息。支持增删改查特别是要处理“库存”这个关键字段借出一本库存减一归还一本库存加一。借阅与归还模块系统的核心业务流程。涉及借阅记录的产生谁、何时、借了哪本书、应何时归还、归还处理实际归还时间、是否超期、续借功能等。数据统计与报表模块管理员需要直观的数据支持如图书借阅排行榜、读者借阅活跃度、超期未归还图书清单等以便进行采购决策和运营管理。系统基础模块包括用户登录/登出、权限校验、操作日志记录等保证系统的安全性和可追溯性。基于以上模块我们采用了经典的三层架构表现层Web Controller、业务逻辑层Service、数据访问层Dao/Mapper通过Springboot进行无缝集成。这种结构清晰职责分离非常适合此类业务逻辑明确的管理系统。2.2 技术栈选型背后的考量为什么选择Springboot MySQL这背后有非常实际的考虑。Springboot它极大地简化了Spring应用的初始搭建和开发过程。不用再被繁琐的XML配置困扰内嵌了Tomcat服务器真正做到“开箱即用”。对于这个项目而言我们需要快速构建RESTful API供前端调用Springboot的RestController、RequestMapping等注解让这一切变得异常简单。同时其强大的自动配置和起步依赖Starter能轻松集成MyBatis数据访问、Spring Security安全、SwaggerAPI文档等我们需要的组件。MySQL作为最流行的开源关系型数据库之一其稳定性、成熟度和社区支持毋庸置疑。图书馆管理系统的数据关系明确用户、图书、借阅记录之间存在清晰的关系且对事务一致性有要求比如借书操作必须同时扣减库存和生成借阅记录这两个操作必须在一个事务里成功或失败关系型数据库是天然的选择。MySQL在中小型数据量下性能表现优秀完全能满足此类系统的需求。其他辅助技术MyBatis-Plus在MyBatis的基础上只做增强不做改变内置了通用的CRUD方法我们不用再写简单的insert、select语句极大地提高了开发效率。它的条件构造器Wrapper对于构建复杂的动态查询条件也非常友好。Swagger2用于自动生成、描述和调用RESTful风格的Web服务接口文档。前后端分离开发时这是前后端沟通的桥梁后端写完接口前端就可以通过Swagger UI界面直接查看和测试省去了手动维护文档的麻烦。Lombok通过注解自动生成Java Bean的getter、setter、toString、构造函数等方法让代码更简洁。前端技术当时为了快速交付前端选用了Thymeleaf模板引擎配合Bootstrap和jQuery。这样后端可以直接渲染页面适合全栈开发。当然现在更流行的做法是前后端彻底分离后端只提供API前端使用Vue、React等框架项目源码中也提供了这种模式的示例分支。注意技术选型没有绝对的好坏只有适合与否。对于初学者或追求快速迭代的项目SpringbootMySQL的组合是风险最低、学习曲线最平缓的选择。如果预估未来数据量极大或业务极其复杂可以在初期就考虑分库分表、引入缓存如Redis等但切忌过度设计。3. 数据库设计与核心表结构解析数据库设计是系统的灵魂设计得好后续开发事半功倍设计得不好则可能处处掣肘。3.1 E-R图与核心表关系我们抽象出三个核心实体User用户、Book图书、BorrowRecord借阅记录。它们之间的关系是一个用户可以有多条借阅记录一本书也可以被多次借阅在不同时间因此User和Book与BorrowRecord都是一对多的关系。基于此我们设计了以下几张核心表用户表 (sys_user)存储系统所有用户信息。CREATE TABLE sys_user ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, username varchar(50) NOT NULL COMMENT 用户名登录账号, password varchar(100) NOT NULL COMMENT 加密后的密码, real_name varchar(50) DEFAULT NULL COMMENT 真实姓名, phone varchar(20) DEFAULT NULL COMMENT 手机号, user_type tinyint(4) NOT NULL DEFAULT 1 COMMENT 用户类型0-管理员1-普通读者, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态0-禁用1-正常, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT系统用户表;关键字段解析user_type用于权限控制。password字段长度建议设长一些因为我们会使用Spring Security的BCryptPasswordEncoder进行加密密文较长。status字段用于软删除或禁用账户。图书信息表 (book_info)存储所有图书的详细信息。CREATE TABLE book_info ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, isbn varchar(20) NOT NULL COMMENT 国际标准书号, book_name varchar(200) NOT NULL COMMENT 图书名称, author varchar(100) NOT NULL COMMENT 作者, publisher varchar(100) DEFAULT NULL COMMENT 出版社, publish_date date DEFAULT NULL COMMENT 出版日期, price decimal(10,2) DEFAULT NULL COMMENT 定价, stock int(11) NOT NULL DEFAULT 0 COMMENT 库存数量可借阅数, total int(11) NOT NULL DEFAULT 0 COMMENT 总数量入库数, category_id bigint(20) DEFAULT NULL COMMENT 分类ID, cover_image varchar(500) DEFAULT NULL COMMENT 封面图片URL, description text COMMENT 图书简介, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_isbn (isbn), KEY idx_book_name (book_name), KEY idx_author (author) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书信息表;关键字段解析isbn具有唯一性是图书的唯一标识。这里设计了stock当前库存和total总藏书量两个字段stock total - 已借出未归还的数量。这种设计避免了每次查询可借数量时都要关联borrow_record表进行聚合计算用空间换取了查询性能。category_id关联到一个独立的图书分类表book_category这里为了简化未展开。借阅记录表 (borrow_record)记录每一次借阅行为。CREATE TABLE borrow_record ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, user_id bigint(20) NOT NULL COMMENT 借阅用户ID, book_id bigint(20) NOT NULL COMMENT 借阅图书ID, borrow_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 借阅时间, due_time datetime NOT NULL COMMENT 应归还时间, return_time datetime DEFAULT NULL COMMENT 实际归还时间, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态0-借阅中1-已归还2-超期归还3-超期未归还, renew_count tinyint(4) NOT NULL DEFAULT 0 COMMENT 续借次数, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_book_id (book_id), KEY idx_due_time (due_time), CONSTRAINT fk_borrow_user FOREIGN KEY (user_id) REFERENCES sys_user (id), CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES book_info (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书借阅记录表;关键字段解析这是系统的核心事务表。due_time应还时间根据借阅规则如30天在借书时自动计算。return_time为空表示未归还。status字段是一个状态机根据due_time和return_time动态更新可通过定时任务或查询时计算便于快速筛选“超期未归还”的图书。renew_count限制了续借次数如最多续借1次。外键约束保证了数据的一致性。3.2 设计中的经验与避坑点关于库存字段的设计为什么不用总数量 - COUNT(状态为借阅中的记录)来计算实时库存在高并发借阅场景下频繁的聚合查询会对数据库造成压力。而使用独立的stock字段通过事务保证其与借阅记录的原子性更新查询效率极高。这是一个典型的用冗余字段换取查询性能的实践。关于时间字段create_time和update_time是审计字段建议每个表都加上。borrow_time和due_time使用了datetime类型精确到秒。应还时间due_time的计算要考虑到节假日等规则本项目简化处理直接在借阅时间上增加固定天数。关于索引在主键和外键上建立索引是基础。此外在book_name和author上建立了普通索引因为这是高频查询条件。在due_time上建索引可以高效地查询即将到期或已超期的借阅记录。关于外键在应用层代码足够健壮的情况下有些团队会选择不在数据库层面设置外键约束以提升写入性能和解耦。但对于这类强一致性要求的业务并且数据量不是特别巨大的情况下我建议保留外键。它能从根本上防止产生“脏数据”是数据完整性的最后一道防线。4. Springboot后端核心功能实现详解4.1 项目结构与依赖配置使用Spring Initializr或IDE直接创建初始化项目主要依赖如下dependencies !-- Web支持 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- 模板引擎如果做全栈 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency !-- 安全框架 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency !-- 数据访问 -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.2.2/version /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency !-- MySQL驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency !-- 简化代码 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency !-- API文档 -- dependency groupIdio.springfox/groupId artifactIdspringfox-swagger2/artifactId version2.9.2/version /dependency dependency groupIdio.springfox/groupId artifactIdspringfox-swagger-ui/artifactId version2.9.2/version /dependency !-- 测试 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependenciesapplication.yml配置数据库、MyBatis-Plus、文件上传等spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/library_db?useUnicodetruecharacterEncodingutf-8serverTimezoneAsia/Shanghai username: root password: yourpassword servlet: multipart: max-file-size: 10MB max-request-size: 100MB mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印SQL生产环境关闭 global-config: db-config: logic-delete-field: deleted # 全局逻辑删除字段名如果使用 logic-delete-value: 1 logic-not-delete-value: 0 # 自定义配置如图书封面存储路径 library: upload-path: /upload/book-cover/4.2 用户认证与权限控制实现我们使用Spring Security进行安全管理。核心是自定义一个UserDetailsService来从我们的sys_user表中加载用户信息。实体与DTO创建User实体类使用Lombok注解。创建LoginDTO接收前端传来的用户名密码。自定义UserDetailsServiceService public class LibraryUserDetailsService implements UserDetailsService { Autowired private UserMapper userMapper; Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user userMapper.selectOne(new LambdaQueryWrapperUser().eq(User::getUsername, username)); if (user null) { throw new UsernameNotFoundException(用户不存在); } if (user.getStatus() 0) { throw new DisabledException(用户已被禁用); } // 构建权限列表这里简单根据userType判断 ListGrantedAuthority authorities new ArrayList(); if (user.getUserType() 0) { authorities.add(new SimpleGrantedAuthority(ROLE_ADMIN)); } else { authorities.add(new SimpleGrantedAuthority(ROLE_USER)); } return new org.springframework.security.core.userdetails.User( user.getUsername(), user.getPassword(), // 数据库存储的是BCrypt加密后的密文 authorities ); } }安全配置类继承WebSecurityConfigurerAdapterSpring Security 5.7以下或使用新式组件配置。主要配置密码编码器、放行静态资源和登录接口、设置权限规则。Configuration EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { Autowired private LibraryUserDetailsService userDetailsService; Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers(/, /login, /api/login, /css/**, /js/**, /images/**).permitAll() // 放行资源 .antMatchers(/admin/**).hasRole(ADMIN) // 管理员路径 .anyRequest().authenticated() // 其他所有请求都需要认证 .and() .formLogin() .loginPage(/login) // 自定义登录页 .loginProcessingUrl(/api/login) // 登录处理URL .defaultSuccessUrl(/index, true) .failureUrl(/login?errortrue) .permitAll() .and() .logout() .logoutUrl(/logout) .logoutSuccessUrl(/login) .permitAll() .and() .csrf().disable(); // 开发阶段可禁用CSRF生产环境建议开启并妥善处理 } }实操心得在前后端分离项目中通常不会使用formLogin而是编写一个/api/login的Controller接收JSON格式的账号密码调用AuthenticationManager进行认证成功后生成一个JWT令牌返回给前端。前端后续在请求头中携带此令牌进行访问。项目源码中也包含了这种JWT方案的实现分支。4.3 图书借阅业务的核心事务处理这是整个系统最需要关注事务一致性的地方。借书操作不是一个简单的插入记录它需要检查图书库存是否大于0。检查用户是否已有超期未归还的图书可配置的规则。插入一条借阅记录。将图书库存减1。这些操作必须在一个数据库事务中完成要么全部成功要么全部回滚。我们使用Spring的Transactional注解来声明事务。Service public class BorrowService { Autowired private BookInfoMapper bookInfoMapper; Autowired private BorrowRecordMapper borrowRecordMapper; Transactional(rollbackFor Exception.class) // 声明式事务 public Result borrowBook(Long userId, Long bookId) { // 1. 查询图书并锁定行悲观锁防止超借 BookInfo book bookInfoMapper.selectBookForUpdate(bookId); if (book null) { return Result.error(图书不存在); } if (book.getStock() 0) { return Result.error(该图书已无库存); } // 2. 检查用户借阅资格例如是否有超期未还记录 Long overdueCount borrowRecordMapper.countOverdueByUserId(userId); if (overdueCount 0) { return Result.error(您有超期未归还的图书请先归还); } // 3. 创建借阅记录 BorrowRecord record new BorrowRecord(); record.setUserId(userId); record.setBookId(bookId); record.setBorrowTime(new Date()); // 计算应还时间假设借期30天 Calendar calendar Calendar.getInstance(); calendar.add(Calendar.DAY_OF_YEAR, 30); record.setDueTime(calendar.getTime()); record.setStatus(0); // 借阅中 borrowRecordMapper.insert(record); // 4. 更新图书库存原子操作 int updateCount bookInfoMapper.decrementStock(bookId); if (updateCount ! 1) { // 理论上不会发生因为前面已经锁行并判断了stock0 throw new RuntimeException(更新库存失败可能并发冲突); } // 5. 记录日志等后续操作... return Result.success(借阅成功); } }关键点1selectBookForUpdate这里使用了SELECT ... FOR UPDATE行级锁。在高并发场景下多个用户同时借同一本仅剩1库存的书时这个锁能确保只有一个事务能成功执行后续的库存扣减其他事务会等待锁释放从而避免“超借”问题。这是悲观锁的典型应用。关键点2Transactional该方法被此注解修饰Spring会代理这个方法在方法开始前开启事务方法执行成功则提交事务如果抛出RuntimeException或Error则回滚事务。rollbackFor Exception.class指定了所有Exception也触发回滚。关键点3库存更新使用decrementStock这样的原子更新操作UPDATE book_info SET stock stock - 1 WHERE id ? AND stock 0比“先查询再计算再更新”更安全高效。4.4 数据统计查询的SQL优化管理员后台需要一些统计报表例如“本月最热图书借阅榜”。这类查询往往涉及多表关联和聚合容易成为性能瓶颈。示例查询借阅量前十的图书本月Mapper public interface BorrowRecordMapper extends BaseMapperBorrowRecord { Select(SELECT b.id, b.book_name, b.author, COUNT(br.id) as borrow_count FROM borrow_record br JOIN book_info b ON br.book_id b.id WHERE br.borrow_time DATE_SUB(CURDATE(), INTERVAL DAY(CURDATE())-1 DAY) // 本月第一天 GROUP BY br.book_id ORDER BY borrow_count DESC LIMIT 10) ListMapString, Object selectHotBooksThisMonth(); }优化建议索引是王道确保borrow_record表在book_id和borrow_time上有联合索引(book_id, borrow_time)这样GROUP BY和WHERE的效率会大幅提升。考虑物化视图或定时统计如果数据量极大百万级借阅记录实时跑这个SQL可能很慢。可以创建一个“图书借阅统计日表”每天凌晨通过定时任务如Spring Scheduler将前一天的统计结果计算好存入此表。查询时直接查这个聚合表速度极快。这是用空间换时间和预计算的思路。分页查询LIMIT在大数据量下深度分页LIMIT 100000, 10性能很差可以使用基于游标的分页记录上一次查询的最后一条记录的ID或时间。5. 前端界面与交互实现要点考虑到项目的完整性这里简要提一下使用Thymeleaf Bootstrap的前端实现要点。5.1 模板布局与公共片段使用Thymeleaf的th:fragment和th:replace/th:insert来实现页面的公共布局如头部导航栏、侧边菜单、底部信息避免代码重复。!-- common/layout.html -- !DOCTYPE html html langzh xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8 title th:text${title} - 图书馆管理系统图书馆管理系统/title link relstylesheet href/css/bootstrap.min.css /head body div th:replace~{common/header :: header}/div div classcontainer-fluid div classrow div th:replace~{common/sidebar :: sidebar}/div main rolemain classcol-md-9 ml-sm-auto col-lg-10 px-4 div th:insert~{common/alert :: alert}/div !-- 页面主要内容块 -- div th:replace${content}/div /main /div /div div th:replace~{common/footer :: footer}/div script src/js/jquery-3.6.0.min.js/script script src/js/bootstrap.bundle.min.js/script /body /html5.2 图书列表页与借阅操作列表页使用Bootstrap Table组件展示通过Ajax从后端/api/book/list接口获取JSON数据。关键点在于“借阅”按钮的状态控制如果库存为0或用户未登录按钮应为禁用状态。借阅操作通过Ajax POST到/api/borrow接口后端返回统一格式的Result对象包含code, msg, data。前端根据返回结果使用SweetAlert等插件给出成功或失败提示并刷新列表数据。// 借阅函数示例 function borrowBook(bookId) { if (!confirm(确定要借阅这本书吗)) return; $.ajax({ url: /api/borrow, type: POST, contentType: application/json, data: JSON.stringify({bookId: bookId}), success: function(result) { if (result.code 200) { swal(成功, result.msg, success).then(() location.reload()); } else { swal(失败, result.msg, error); } }, error: function() { swal(错误, 网络请求失败, error); } }); }5.3 文件上传图书封面使用input typefile配合FormData进行文件上传。后端使用Spring MVC的RequestParam(file) MultipartFile file接收。form iduploadForm enctypemultipart/form-data input typefile namefile idcoverFile acceptimage/* button typebutton onclickuploadCover()上传封面/button /form script function uploadCover() { var formData new FormData($(#uploadForm)[0]); $.ajax({ url: /api/upload/cover, type: POST, data: formData, processData: false, // 必须 contentType: false, // 必须 success: function(result) { // 返回图片访问路径更新到表单隐藏域或预览 $(#coverImageUrl).val(result.data); $(#coverPreview).attr(src, result.data).show(); } }); } /script后端需要将文件保存到配置的library.upload-path目录下并生成一个唯一的文件名如UUID最后返回文件的相对访问URL。6. 项目部署与运维注意事项6.1 多环境配置使用Springboot的Profile功能管理开发、测试、生产环境的配置。 在application.yml中定义通用配置然后创建application-dev.yml,application-prod.yml。 通过启动参数--spring.profiles.activeprod来激活生产环境配置。生产环境的配置通常包括数据库连接池参数如HikariCP调优。关闭Swagger等开发工具。配置正确的日志级别和输出路径使用Logback或Log4j2。设置服务器端口、上下文路径等。6.2 数据库连接与性能调优连接池Springboot默认使用HikariCP性能很好。在生产环境中需要根据实际并发量调整maximum-pool-size最大连接数、connection-timeout连接超时等参数。spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 10 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000SQL监控可以使用p6spy等组件在开发环境打印完整的、可执行的SQL日志方便调试。生产环境务必关闭或仅记录慢查询。MyBatis-Plus分页插件务必配置分页插件否则分页功能不生效。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }6.3 定时任务与业务状态维护系统中有一些状态需要定期维护例如更新借阅记录的超期状态可以每天凌晨1点执行一个定时任务扫描due_time NOW() AND return_time IS NULL的记录将其status更新为“超期未归还”。发送超期提醒邮件/短信同样是定时任务查询超期未还的记录关联用户信息调用消息服务发送提醒。使用Spring的Scheduled注解可以轻松实现Component public class OverdueTask { Autowired private BorrowRecordMapper borrowRecordMapper; Autowired private EmailService emailService; // 每天凌晨1点执行 Scheduled(cron 0 0 1 * * ?) public void updateOverdueStatusAndNotify() { // 1. 更新状态 borrowRecordMapper.updateOverdueStatus(); // 2. 查询超期用户并发送提醒 ListOverdueUserInfo overdueList borrowRecordMapper.selectOverdueUserList(); for (OverdueUserInfo user : overdueList) { emailService.sendOverdueReminder(user); } } }需要在启动类上添加EnableScheduling注解来启用定时任务。7. 常见问题排查与实战技巧在实际开发和部署中你肯定会遇到各种各样的问题。这里分享几个我印象深刻的“坑”和解决技巧。7.1 中文乱码问题这是一个老生常谈但新手必踩的坑。确保以下三处编码统一为UTF-8数据库创建数据库时指定字符集CREATE DATABASE library_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;。表字段也建议用utf8mb4支持emoji。Springboot应用在application.yml中配置数据库连接URL时加上参数jdbc:mysql://...?useUnicodetruecharacterEncodingutf-8serverTimezoneAsia/Shanghai。Tomcat容器如果打WAR包部署在server.xml的Connector配置中加上URIEncodingUTF-8。7.2 事务失效的几种常见场景Transactional注解用起来简单但失效场景不少方法非public修饰Spring AOP代理要求被代理的方法必须是public。自调用问题在同一个类中一个没有Transactional注解的方法A调用了有Transactional注解的方法B事务不会生效。因为代理对象调用方法B时走的是代理逻辑而自调用是this.B()绕过了代理。解决方案将方法B放到另一个Service中通过注入调用或者使用AopContext.currentProxy()获取当前代理对象再调用。异常被捕获如果在方法中捕获了异常并且没有重新抛出事务管理器就感知不到异常不会回滚。解决方案在catch块中抛出RuntimeException或者在注解中指定rollbackFor Exception.class并在catch块中抛出具体异常。数据库引擎不支持事务如MySQL的MyISAM引擎不支持事务必须使用InnoDB引擎。7.3 并发借书导致的超借问题这是我们之前用SELECT ... FOR UPDATE悲观锁解决的问题。还有一种乐观锁的方案在book_info表中增加一个version版本号字段。 借书时UPDATE book_info SET stock stock - 1, version version 1 WHERE id ? AND version ? AND stock 0在Service中先查询出图书信息和当前的version然后执行上面的更新语句。如果返回的影响行数为0说明版本号不对或库存不足可能是其他事务已经修改则借书失败提示用户重试。乐观锁在高并发冲突少的场景下性能更好冲突多时重试逻辑会复杂一些。7.4 前端页面缓存问题在开发时修改了CSS/JS文件但浏览器因为缓存没有加载最新版本。可以通过以下方式解决在引用资源时加上版本号或时间戳script src/js/app.js?v1.0.1/script。使用Springboot的spring.resources.chain.strategy.content.enabledtrue配置它会根据文件内容生成哈希值作为版本号自动添加到资源URL中。7.5 项目源码与文档的使用建议提供的“源码文档”压缩包建议按以下步骤学习或复用导入IDE使用IntelliJ IDEA或Eclipse作为Maven或Gradle项目导入。阅读文档先看README.md或设计文档了解项目结构、技术栈和数据库脚本位置。初始化数据库运行sql/init.sql脚本创建数据库和表结构。修改配置根据你的本地环境修改application.yml中的数据库连接信息、文件上传路径等。运行项目找到主启动类通常叫LibraryApplication直接运行。访问http://localhost:8080。接口测试如果项目集成了Swagger访问http://localhost:8080/swagger-ui.html可以查看和测试所有API。按需修改你可以在此基础上增加新功能如邮件通知、扫码借书接口、修改前端界面、或者将其改造成纯后端API项目供Vue/React前端调用。这个项目作为一个教学或原型模板已经具备了核心功能。但在投入真实生产环境前还需要考虑更多因素比如更完善的异常处理、API接口签名验证、更细粒度的权限控制如使用Spring Security的PreAuthorize注解、接口限流、以及部署到Linux服务器后的日志收集和监控等。希望这份详细的拆解能帮助你不仅“跑起来”这个系统更能理解其背后的设计思想和实现细节从而有能力去定制和优化它应对更复杂的场景。本文还有配套的精品资源点击获取
返回列表