资讯详情

SSM+Layui图书管理系统实战:从架构选型到整合避坑

📅 2026/10/11 13:09:52 | 华诺云谱 👁 阅读
SSM+Layui图书管理系统实战:从架构选型到整合避坑
简介基于SSMLayui的图书管理系统完整项目资源面向Java Web初学者、毕业设计及课程设计人群可解决图书增删改查、借阅归还、用户与权限管理等常见业务模块的开发难题。压缩包内含8个文件共61.06MB主要由Java源码压缩包、MySQL数据库sql脚本、部署文档、表结构说明及7z附加资料组成覆盖代码、脚本与文档三类。系统采用SpringSpringMVCMyBatis三层架构前端用Layui组件库构建交互界面源码包含用户管理、图书检索、借阅归还及权限控制等完整功能模块适合直接部署运行和二次开发。配套的数据库表结构文档和部署文档能帮助快速搭建环境txt说明文件补充了使用细节对理解SSM核心注解、MyBatis动态SQL及事务控制均有直接参考价值。目前已有872人学习下载是理论结合实战、快速掌握企业级Web项目流程的不错选择。1. 基于SSMLayui的图书管理系统它到底解决什么问题图书管理大概是Java Web里出现频率最高的项目类型而“基于SSMLayui的图书管理系统”又是其中最经典的组合。管理一个内部图书角、一间小型图书馆或者完成一份课程设计核心诉求无非三件事图书库存能查、借还记录能对、逾期情况能提醒。用Excel靠手记借还日期月底盘点永远对不上账这套系统把流程固化下来借书靠按钮还书靠记录台账永远比人工靠谱。SSM负责后端Spring管对象、SpringMVC管请求、MyBatis管SQLLayui负责前端页面表格、弹窗、表单、日期选择器都开箱即用。后端暴露JSON接口前端渲染页面各司其职。这个组合适合三类人想走通“数据库到页面”全链路的新手需要快速交付一个可演示系统的开发者以及要给公司内部图书角做管理工具的同事。它不追求高并发分布式而是把“几十个人能用、结构清晰、能改能跑”这件事做到位。理解这套系统不用把它想得多复杂。三张表、一组接口、一个前端框架就能覆盖借书、还书、查书、管读者的大部分日常操作。真正花时间的往往不是业务代码而是SSM整合配置、数据库字段取舍和那几处隐蔽的坑。2. 为什么这套组合在图书管理系统里长盛不衰选型逻辑2.1 三个框架各管一摊Spring管对象、SpringMVC管请求、MyBatis管SQLSSM在图书管理系统里能站稳脚跟不是因为名字响亮而是三个框架的职责边界刚好贴合这类项目的结构。Spring的IoC容器负责管理对象的创建和依赖关系。借阅服务需要调用图书DAO、借阅DAO用Autowired注入就好不需要自己new。这带来的直接好处是替换实现不需要改业务代码换数据源、换DAO实现、给Service写单元测试都不动上层逻辑。图书管理系统虽然不大但“替换数据源”这件事在实际交付里几乎一定会遇到某公司内部部署环境和开发环境数据库版本不一致的情况太常见了。SpringMVC负责把HTTP请求路由到Controller方法。一个GetMapping(/borrow/list)对应一个查询方法参数绑定、JSON序列化都交给框架。图书管理系统的接口风格很适合做成RESTful查书、借书、还书、读者管理每个模块就是一组URL映射结构一目了然。MyBatis则守住了数据的最后一环。SQL写在Mapper.xml里DAO接口方法签名与SQL语句一一对应。图书管理这类系统有大量列表查询和统计查询——借阅记录联查书名和读者名、逾期列表筛选、分类统计库存这些操作写SQL比用JPA/Hibernate的实体关系映射更直观可控。尤其是后期调优一条SQL是否走了索引能不能加条件直接看文件就清楚。这个三层结构最朴素的美德是“每层都看得懂”。一个新人接手时顺着Controller → Service → Mapper的路径就能定位问题。Spring Boot当然更好用但SSM的显式配置让过程对你完全透明这恰恰是入门阶段最值钱的东西。2.2 Layui在SSM项目中的角色不需要前端工程化的UI方案Layui不是MVVM框架它是基于jQuery的UI组件库。table表格、layer弹层、form表单、laydate日期选择器这几个组件差不多覆盖了图书管理系统90%的页面需求。它最大的价值是零工程化不需要Node、不需要Webpack、不需要npm install下载dist目录放进webapp浏览器直接可用。这跟SSM的部署方式天然同频——打包成war丢到Tomcat就能跑。对比一下VueElement那套方案组件能力强但引入后要处理node构建、跨域代理、token传递对一个图书管理系统来说是杀鸡用牛刀。Layui的table组件与服务端的交互模式尤其简单后端返回约定好的一套JSON表格自动渲染数据和分页。这让后端工程师可以独立完成整个项目不需要等前端资源。需要注意Layui的定位决定了它的交互上限。它适合“表格表单弹窗”这类管理型页面不适合做复杂联动、拖拽、实时编辑。图书管理系统恰好就落在它最舒服的区间里。表格展示书目列表弹窗做借书还书操作日期选择器选借阅时间——每一样都是Layui的主场。2.3 这套组合的容量边界几十人用的系统才是它的主场SSM是同步阻塞模型Tomcat默认线程池有限每个请求占用一个线程直到响应返回扛几百并发会很吃力。但图书管理系统的真实场景是一个内部图书角几十个员工一个实验室几百本专业书一天借还操作不超过百笔。这个量级下SSMLayui根本不是性能问题而是性能过剩。做选型决策前可以先看这张对比表方案前端上手成本后端配置方式适合量级典型场景SSM Layui低HTMLJS直接改显式XML配置每层可见并发100以内课程设计、内部图书角、小图书室Spring Boot Vue高需要Node构建链约定优于配置并发中等以上前后端分离、多端复用接口JSP Servlet极低几乎无框架配置并发很低老系统维护这个边界决定了你不用为图书管理系统引入复杂的微服务、消息队列、分布式缓存。真到需要小程序端复用接口、需要WebSocket推送逾期通知的那一天再考虑迁移到Spring Boot也不迟。图书管理系统的业务模型足够简单SSM这套骨架完全兜得住。3. 跑通SSMLayui的整合骨架Maven依赖与三份核心配置3.1 目录结构与pom.xml先把包名和依赖定下来SSM整合的第一步不是写业务代码而是把Maven依赖和目录结构定好。目录乱是后续所有排查困难的根源。我习惯的分层是controller放接口层service放业务和事务dao放MyBatis接口webapp/static放Layui静态文件resources/mapper放Mapper XML。pom.xml的依赖组合直接决定后面整合顺不顺利。给一份我常用的核心依赖properties spring.version5.2.x.RELEASE/spring.version mybatis.version3.5.x/mybatis.version /properties dependencies !-- Spring MVC 与事务 -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency !-- MyBatis 本身与 Spring 整合包 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.x/version /dependency !-- Druid 数据源 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.1.x/version /dependency !-- JSON 序列化 -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.9.x/version /dependency !-- MySQL 驱动版本与数据库对应 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.x/version /dependency !-- Servlet API打包时由容器提供 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency /dependencies依赖里最容易踩坑的是版本错位Spring 5.x要求Java 8MyBatis 3.5.x对应mybatis-spring 2.0.x这两个必须配套否则运行时会出现NoSuchMethodError。MySQL驱动版本要看数据库5.x的MySQL用5.1.x驱动8.0的MySQL要用8.0.x驱动并且驱动类名换成com.mysql.cj.jdbc.Driver。jackson-databind如果版本太旧可能不支持LocalDate序列化后面日期问题会来敲门。3.2 web.xml与SpringMVC配置静态资源放行决定了Layui能不能显示web.xml是SSM项目的入口它决定了两件事请求怎么进入Spring容器中文编码怎么保证。先看完整配置web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 !-- 编码过滤器必须放在所有过滤器最前面 -- filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping !-- Spring 根容器加载数据源、MyBatis、Service -- context-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-dao.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener !-- SpringMVC 子容器只扫描 Controller -- servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mapping /web-app根容器和子容器的划分是SSM的一个经典考点根容器ContextLoaderListener加载的管数据源、MyBatis、Service子容器DispatcherServlet加载的只管Controller。子容器可以引用根容器里的Bean反向不行所以Controller注入Service没问题但别把Service放到spring-mvc.xml里扫描。url-pattern配成/意味着所有请求都进DispatcherServlet包括CSS、JS、字体文件。如果不放行静态资源Layui的样式表、图标字体全都会被SpringMVC拦截页面打开是一片裸HTML。接下来在spring-mvc.xml里解决这个问题!-- 只扫描 ControllerService 交给根容器 -- context:component-scan base-packagecom.demo.controller/ !-- 开启注解驱动ResponseBody、RequestBody 全靠它 -- mvc:annotation-driven/ !-- 静态资源放行Layui 的 css/js/font 都在这里 -- mvc:resources mapping/static/** location/static// !-- 兜底仍找不到的静态资源交给容器默认处理器 -- mvc:default-servlet-handler/mvc:resources的mapping/static/**表示URL以/static/开头的请求直接去webapp/static/目录下找文件不再经过Controller。这是Layui能显示的前提。default-servlet-handler是保险锁当某些资源路径没被上面的规则匹配到时交给Tomcat默认的静态资源处理器。这两行加上前端资源的问题就解决了大半。3.3 数据源与MyBatis配置SqlSessionFactory和Mapper接口交给Spring托管根容器的spring-dao.xml承载了数据源、SQL会话工厂、Mapper扫描和事务管理四件事。这是一份能直接用的模板context:component-scan base-packagecom.demo.service/ context:property-placeholder locationclasspath:jdbc.properties/ bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nameconfigLocation valueclasspath:mybatis-config.xml/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.demo.dao/ /bean bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertransactionManager/SqlSessionFactoryBean把MyBatis的核心工厂对象交给Spring管理mapperLocations指定Mapper XML的路径classpath:mapper/*.xml会自动加载所有Mapper文件。MapperScannerConfigurer按包扫描DAO接口给每个接口生成代理对象放进Spring容器这样Service里Autowired一个接口就能直接调用SQL。tx:annotation-driven开启Transactional注解的事务能力。借书操作需要先扣库存再插入借阅记录两步必须在一个事务里没有这行配置任何一个步骤失败都可能留下脏数据。mybatis-config.xml里还有一处关键配置它决定下划线字段能不能正确映射到驼峰属性settings setting namemapUnderscoreToCamelCase valuetrue/ setting namelogImpl valueSTDOUT_LOGGING/ /settingsmapUnderscoreToCamelCase开启后数据库的book_name能自动映射到实体类的bookName属性。不开启的话查询出来的实体类字段全是null这是后面第5章要展开讲的高频问题。4. 数据库设计与借阅核心SQL三张表撑起一个图书室4.1 表结构设计book、reader、borrow的字段取舍图书管理系统的数据库设计有两条路一条把每本书作为一行记录适合单册管理另一条把同一ISBN的书聚合为一行用库存数字表示复本量。课程设计和内部图书角场景下后一种更实用——书目列表不重复库存总量和可借数量一目了然。三张表的关系很直接book表管书的信息和库存reader表管读者borrow表记录每一次借阅行为。建表语句如下CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL COMMENT ISBN号, book_name VARCHAR(100) NOT NULL COMMENT 书名, author VARCHAR(50) DEFAULT , publisher VARCHAR(100) DEFAULT , category VARCHAR(30) DEFAULT , location VARCHAR(50) DEFAULT COMMENT 书架位置, total_stock INT NOT NULL DEFAULT 1 COMMENT 总库存, available_stock INT NOT NULL DEFAULT 1 COMMENT 可借数量, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_book_name (book_name), INDEX idx_book_category (category) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书表; CREATE TABLE reader ( id INT PRIMARY KEY AUTO_INCREMENT, reader_no VARCHAR(20) NOT NULL UNIQUE COMMENT 读者证号, reader_name VARCHAR(50) NOT NULL, phone VARCHAR(20) DEFAULT , max_borrow INT NOT NULL DEFAULT 5 COMMENT 最大可借册数, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0停用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT读者表; CREATE TABLE borrow ( id INT PRIMARY KEY AUTO_INCREMENT, book_id INT NOT NULL, reader_id INT NOT NULL, borrow_date DATETIME NOT NULL COMMENT 借出时间, due_date DATETIME NOT NULL COMMENT 应还时间, return_date DATETIME DEFAULT NULL COMMENT 实际归还时间, NULL表示未还, status TINYINT NOT NULL DEFAULT 0 COMMENT 0借出中 1已归还, CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES book(id), CONSTRAINT fk_borrow_reader FOREIGN KEY (reader_id) REFERENCES reader(id), INDEX idx_borrow_reader (reader_id), INDEX idx_borrow_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT借阅记录表;几个字段取舍值得说清楚。available_stock是个冗余字段它本可以通过“总库存减未还数量”实时算出来但每次查询都做聚合计算浪费性能而且图书室场景下借还频繁用UPDATE原子维护成本更低。due_date必须落库不能在还书时用“借出时间加30天”现算——如果中途调整过借阅规则历史记录的应还时间就全错了。return_date允许NULL这是一个有意的设计NULL天然表示“未还”不需要额外标志位。borrow.status只放两个状态借出中、已归还。逾期是派生状态由due_date和return_date计算得出不落库避免状态更新延迟导致判断错误。utf8mb4字符集是必须的utf8在MySQL里支持不到完整的Unicode字符书名或读者名里出现生僻字、特殊符号时写入直接报错。4.2 借阅闭环的三个关键SQL原子扣库存、联查列表、逾期判断借阅流程里最核心的是“库存扣减”。很多人写业务时会先查库存判断大于0再UPDATE这在单线程下没问题但两个请求同时借同一册书时就会超借。正确的写法是用一条UPDATE语句同时完成检查和扣减!-- 借书库存足够才扣减影响行数为0表示库存不足 -- update iddecreaseStock parameterTypeint UPDATE book SET available_stock available_stock - 1 WHERE id #{bookId} AND available_stock 0 /updateMyBatis执行UPDATE返回的是受影响行数。服务层里拿到返回值判断rows 0说明库存没了直接终止借阅流程。这样就不需要SELECT、UPDATE、加锁这一套复杂流程一条SQL解决并发问题。列表查询是借阅管理页面的主力接口需要联查书名和读者名。借阅表里只存了外键ID不联查的话前端拿到一串数字毫无意义select idlistBorrow resultTypemap SELECT b.id, bk.book_name, bk.isbn, r.reader_name, b.borrow_date, b.due_date, b.return_date, b.status FROM borrow b JOIN book bk ON b.book_id bk.id JOIN reader r ON b.reader_id r.id ORDER BY b.borrow_date DESC LIMIT #{offset}, #{pageSize} /selectORDER BY b.borrow_date DESC保证最新的借阅记录在最上面这是管理页面的基本预期。LIMIT分页的两个参数由Layui table传入的page和limit换算得到注意返回值里不要忘了总条数查询否则表格无法分页。逾期判断有个隐蔽的坑不要在SQL里直接用NOW()比较。数据库服务器和应用服务器的时间可能不一致尤其在容器化部署下这个问题几乎必然出现。可靠做法是把当前时间作为参数从Java层传进来select idlistBorrowWithStatus resultTypemap SELECT b.id, bk.book_name, r.reader_name, b.borrow_date, b.due_date, b.return_date, CASE WHEN b.return_date IS NOT NULL THEN 已归还 WHEN b.due_date lt; #{now} THEN 逾期 ELSE 借出中 END AS status_text FROM borrow b JOIN book bk ON b.book_id bk.id JOIN reader r ON b.reader_id r.id WHERE b.status 0 /select#{now}在Java层传入new Date()保证了时间基准来自应用服务器。注意XML里小于号要写成lt;否则XML解析直接报错这也是个容易翻车的细节。4.3 服务端返回格式Layui table只认这一套JSONLayui的table组件对后端返回格式有硬性约定后端必须返回一个包含code、msg、count、data四个字段的JSON对象。code为0表示成功count是总记录数不是当前页条数data是当前页数据列表。后端Controller的返回结构长这样public class Result { private int code; private String msg; private long count; private Object data; public static Result ok(long count, Object data) { Result r new Result(); r.code 0; r.msg ; r.count count; r.data data; return r; } public static Result fail(String msg) { Result r new Result(); r.code 1; r.msg msg; return r; } // getter/setter 省略 }Controller里返回Result对象借助ResponseBody自动序列化成JSONController public class BorrowController { Autowired private BorrowService borrowService; GetMapping(/borrow/list) ResponseBody public Result list(RequestParam int page, RequestParam int limit) { int offset (page - 1) * limit; ListMapString, Object rows borrowService.listBorrow(offset, limit); long total borrowService.countBorrow(); return Result.ok(total, rows); } }page和limit是Layui table默认的请求参数名分页计算公式是offset (page - 1) * limit。count必须返回数据库的总记录数Layui才能算出总页数和页码按钮。如果这里错用成rows.size()表格永远只有第一页。5. SSMLayui高频踩坑记录现象、原因、解决路径5.1 Layui图标变成方块字体文件没放行现象页面HTML结构完全正常按钮文字也在但所有图标显示成方格或空白。原因Layui的图标不是图片是字体文件iconfont.woff等。Tomcat对woff、woff2的MIME类型识别不全或者SpringMVC的静态资源放行路径没覆盖到字体目录都会导致字体加载失败。解决先在浏览器F12的Network面板看字体文件请求状态。如果404检查mvc:resources的mapping是否覆盖了/static/**全部路径以及页面里Layui的引用路径是否和实际文件一致。如果文件返回200但图标仍是方块在web.xml里补上MIME映射mime-mapping extensionwoff/extension mime-typefont/woff/mime-type /mime-mapping mime-mapping extensionwoff2/extension mime-typefont/woff2/mime-type /mime-mappingTomcat 9以上一般内置了这些类型老版本容器或嵌入式容器常需要手动补。这个坑十个人里八个人会踩排查路径固定不算难但很浪费时间。5.2 Controller明明写了方法却返回404包扫描和注解驱动的问题现象DAO、Service、Controller的代码都写了启动也不报错但访问任何接口都返回404只有静态页面能打开。原因通常是两个配置没配对。一是spring-mvc.xml的context:component-scan扫描包路径没覆盖Controller所在的包二是mvc:annotation-driven/缺失导致ResponseBody、RequestMapping注解不生效。解决按顺序排查三处。第一确认spring-mvc.xml里扫描的是Controller所在的包或其父包第二确认文件里有mvc:annotation-driven/这行配置第三访问接口时看Tomcat日志有没有“Mapped to”开头的打印。如果日志里没有这个映射信息说明请求压根没进Controller。另外注意打war包后检查target/classes里Controller类的class文件是否存在有时候代码改了没重新编译纯属白忙活。5.3 查询结果全是nullMyBatis驼峰映射没打开现象数据库里book_name有值SQL也执行了但返回给前端的JSON里对应字段是null。原因数据库字段是下划线命名book_nameJava属性是驼峰命名bookNameMyBatis默认不做自动映射。用实体类接收结果时列名对不上属性名字段就全是null。解决在mybatis-config.xml里开启全局设置settings setting namemapUnderscoreToCamelCase valuetrue/ /settings这是最省事的方案。如果不方便改全局配置可以在SQL里写别名SELECT book_name AS bookName但几十个字段都写别名太啰嗦维护成本高。还有一个容易混淆的点这个设置只对JavaBean映射生效如果resultType用的是map返回的Map里键名仍然是下划线命名前端取值时要跟着数据库字段名走不是跟着驼峰走。5.4 并发借阅后库存变成负数先查后改是根因现象两个人同时借同一本只剩一册的书两个请求都通过了“库存大于0”的校验最终库存变成-1。原因代码逻辑是“先SELECT检查available_stock再UPDATE减1”。两个请求并发进来同时读到库存为1同时通过检查又同时执行UPDATE各自减一次就变成了-1。这是典型的读改写并发问题不是偶发是并发必现。解决用一条带条件的UPDATE把检查和扣减合并成原子操作int rows bookDao.decreaseStock(bookId); if (rows 0) { return Result.fail(库存不足借阅失败); }decreaseStock里的SQL自带AND available_stock 0条件库存不足时影响行数为0业务层直接终止流程。这个方法不用锁、不用事务嵌套一条SQL解决是图书管理系统这个量级下最匹配的方案。5.5 日期变成一串数字Jackson序列化的默认行为现象前端借阅列表里的时间列显示成“1735000000000”这种长数字。原因Java的Date对象被Jackson默认序列化为时间戳毫秒数。表格展示场景需要的是“2025-03-01 10:20:30”这种可读格式。解决在实体类的日期字段上加注解这是最直观的做法JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private Date dueDate;timezone不写GMT8的话Jackson默认用UTC时区序列化时间会比北京时间少8小时这是同一类问题的进阶版隐蔽坑。如果项目里字段多也可以在spring-mvc.xml里全局配置ObjectMapper的dateFormat但我更推荐注解方式字段级控制更灵活不会误伤不需要格式化的字段。6. 进阶把“能演示”的系统做成“能一直用”的系统借书和还书流程里事务配置是底线。借书包含扣库存和插入借阅记录两步还书包含加库存和更新借阅记录两步任何一个步骤失败都必须全部回滚。Transactional注解放在Service层的方法上写在Controller层无效因为Spring的AOP代理拦截的是Service的调用链。注解里建议写明rollbackFor Exception.class——默认配置下只有运行时异常才触发回滚受检异常不会不写明就会出现“扣了库存但记录没插上”的脏数据。逾期计算不要在SQL里用DATEDIFF改用Java时间对象更可控。用ChronoUnit.DAYS.between()能正确跨越自然月也更方便后续扩展按小时计算或分段计费LocalDate due borrow.getDueDate().toLocalDate(); LocalDate today LocalDate.now(); long overdueDays ChronoUnit.DAYS.between(due, today);前端也值得做一个小动作。Layui table的done回调会在数据渲染完成后触发利用这个时机给逾期行加背景色管理页面的易用性立刻上一个台阶table.render({ elem: #borrowTable, url: /borrow/list, cols: [[ {field: bookName, title: 书名}, {field: readerName, title: 读者}, {field: dueDate, title: 应还日期}, {field: statusText, title: 状态} ]], done: function(res) { this.elem.next().find(tr).each(function(i, tr) { var status $(tr).find(td[data-fieldstatusText]).text(); if (status 逾期) { $(tr).css(background-color, #fff3e6); } }); } });这三个习惯——原子扣减、时间不靠数据库、事务配齐——是我在模拟项目X里被现实教育出来的。那次还书规则从30天改成15天因为due_date落库而不用现算历史记录没乱库存因为带条件的UPDATE从没出现过负数日期因为统一用Java传入没出现过时区偏差。把这些做进去这套系统就能从“能演示”变成“能用一年”借阅台账再也不用人肉核对。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑