☰
SpringBoot+MyBatis+JSP图书管理系统:库存扣减与借还事务
2026/10/27 13:40:48 网站建设 项目流程

简介:一份基于 SpringBoot、MyBatis 与 JSP 构建的图书管理系统源码包,面向 Java Web 初学者、课程设计或毕业设计开发者,帮助其完成从框架整合到业务落地的完整实践。系统围绕图书增删改查、分类管理、借阅归还与分页展示展开,采用 MVC 分层组织代码,并涉及 Spring Security 权限控制、MyBatis Mapper 映射及可执行 jar 部署等常见环节。压缩包共 1192 个文件、约 95.3MB,包含 648 个 class、173 个 jsp、148 个 xml、65 个 jar 与 51 个 java 等,分别对应编译产物、页面视图、配置与依赖、业务源码,便于直接运行和对照学习。已有 2208 人学习下载。读者可据此梳理 SpringBoot 与 MyBatis 的整合流程、理解数据库交互与分页实现,并参考现有页面和配置快速搭建自己的图书管理项目,适合作为入门实战与课程作业的参照。

1. 需求先摆正:库存数字在任何时刻都要对得上

图书管理系统的需求从来不复杂,复杂的是怎么让一本书的可借数量在借出、归还、超期、下架这些动作之后依然准确。用 SpringBoot + MyBatis + JSP 做图书管理系统,前后端不分离、页面服务端渲染,看着像老派做法,实际是课设与毕设里最稳的一条路:调用链短、报错位置明确、一个 war 包丢上去就能跑。这套方案适合两类人,一类是要交一份能演示、能讲清设计的学生,另一类是需要快速交付一套内部借阅工具的开发。下面按需求、架构、实现、排查、部署、验收的顺序拆开讲。

2. 总体架构与数据模型:三层结构、六个模块和六张核心表

很多人一上来就写 Controller,写到第三天发现 Service 里塞满了参数校验、事务注解和 HTTP 相关的对象,改一处牵动三处。架构这件事在图书管理系统里不玄学,先把职责切干净,后面所有代码都有地方放。

2.1 分层与模块边界:Controller 只管参数,事务一律放 Service

整体是经典四层:controller接请求、service承载业务与事务、mapper只写 SQL、entity/dto/vo负责数据搬运。六个业务模块按这条线各走一遍:图书管理、读者管理、借阅管理、分类管理、系统用户与权限、统计报表。

职责划分我一般这么定:

  • Controller 做三件事——接参数、调 Service、决定跳哪个 JSP 或返回什么 JSON。不写业务判断,不碰SqlSession。
  • Service 是事务边界,一个业务动作一个方法,方法内部要么全成功要么全回滚。借阅、归还、续借都在这层。
  • Mapper 只放 SQL,不在 XML 里写if (status == 1)这种 Java 逻辑,判断条件用<if>传参解决。
  • 实体分三种:entity对应表、dto接前端表单、vo给页面渲染(比如图书列表要带分类名称,就该是BookVO而不是Book)。

这样切的好处很直接:答辩时评委问「库存扣减在哪」,你能一句话答出「在 BorrowServiceImpl.borrow 里,和借阅记录插入在同一个事务」。翻车场景也少,改页面不用动 SQL,改 SQL 不用动页面。

统计报表模块单独说明一下。它不新建表,全部从借阅记录聚合:近 30 天借阅量、分类占比、超期未还清单。查询写在ReportMapper.xml里,用GROUP BY直接出结果,页面用 JSTL 循环渲染表格,不引图表库也能交差。

2.2 依赖清单:pom 里必须钉死的四项

JSP 放在 Spring Boot 里最容易踩的坑就是依赖没引全,页面被当成静态文件直接下载。下面这段是 pom 的关键部分,注释标了每一项的作用:

<!-- 打包方式必须是 war,jar 包下内嵌 Tomcat 默认不解析 JSP --> <packaging>war</packaging> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- JSP 解析器,缺了它页面会以源码形式被下载 --> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> </dependency> <!-- JSTL 标签库,页面里的 c:forEach、fmt:formatDate 靠它 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <!-- MyBatis 整合包,含 SqlSessionFactory 自动装配与事务桥接 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- 分页插件,后台所有列表页共用 --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.6</version> </dependency> </dependencies>

版本选择上有个绕不开的决定:Spring Boot 2.7.x 维持在javax.servlet命名空间,JSTL 用 1.2 即可;如果换成 3.x,整个 servlet 体系迁到jakarta,JSTL 依赖坐标和页面里的标签 uri 都要跟着换,一处没改就是 500 报错。做课设或毕设,建议直接锁 2.7.x,把精力放在业务上而不是迁移上。这一点属于血泪经验:迁移带来的收益在这类项目里几乎为零,风险却是连环的。

配置文件对应写清楚视图前缀、数据源和 MyBatis 扫描路径:

server: port: 8080 servlet: context-path: /library # 有上下文路径,页面里所有链接都要带上前缀 encoding: charset: UTF-8 force: true # 强制请求与响应都用 UTF-8,挡住大部分乱码 spring: mvc: view: prefix: /WEB-INF/jsp/ # 故意放在 WEB-INF 下,浏览器无法直接访问 suffix: .jsp datasource: url: jdbc:mysql://127.0.0.1:3306/library_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: library password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.library.entity configuration: map-underscore-to-camel-case: true # book_id 自动映射到 bookId,省掉大量 resultMap log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 调试期把 SQL 打到控制台 pagehelper: helper-dialect: mysql reasonable: true # 页码越界时回到合理范围,不返回空列表 support-methods-arguments: true

context-path这一项要特别注意,一旦设了/library,JSP 里所有href和src都要用${pageContext.request.contextPath}拼,否则上线后全站 404。这是新手最常翻的一次车,本地不加 context-path 测得好好的,部署到外部 Tomcat 就崩。

2.3 数据模型:六张表怎么切,库存字段要不要冗余

六张核心表:sys_user(系统用户)、reader(读者)、book(图书)、category(分类)、borrow_record(借阅记录)、fine_rule(超期规则,可并为配置表)。外键在业务系统里通常不建物理约束,靠应用层保证,避免删改时被数据库卡住,这一点在演示环境里尤其重要。

表名作用关键字段索引
book图书主数据与库存isbn、title、total_copies、available_copiesuk_isbn、idx_title、idx_category
reader读者证与借阅额度card_no、name、max_borrow、loan_days、statusuk_card_no
borrow_record借阅流水与状态机book_id、reader_id、due_date、return_date、statusidx_reader_status、idx_book
category分类树name、parent_id、sortidx_parent
sys_user管理员与操作员username、password、roleuk_username
fine_rule超期罚金规则reader_type、free_days、fee_per_dayidx_type

库存字段是这张设计里唯一需要争论的地方。我倾向于冗余:book表同时存total_copies和available_copies,列表页直接读,不用每次COUNT借阅记录。代价是写入时必须同步维护,所以扣减一律走一条带条件的UPDATE:

-- 图书主表:库存双字段,列表页零聚合查询 CREATE TABLE book ( id BIGINT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL, title VARCHAR(120) NOT NULL, author VARCHAR(60) NOT NULL, category_id BIGINT NOT NULL, total_copies INT NOT NULL DEFAULT 1, available_copies INT NOT NULL DEFAULT 1, location VARCHAR(30) DEFAULT NULL COMMENT '馆藏位置,如 A区-3排', status TINYINT NOT NULL DEFAULT 1 COMMENT '1 在架 0 下架', UNIQUE KEY uk_isbn (isbn), KEY idx_title (title), KEY idx_category (category_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 借阅记录:status 是有限状态机,超期由 due_date 与 return_date 派生 CREATE TABLE borrow_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, book_id BIGINT NOT NULL, reader_id BIGINT NOT NULL, borrow_date DATE NOT NULL, due_date DATE NOT NULL, return_date DATE DEFAULT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT '1 借出 2 已还 3 超期未还', renew_count TINYINT NOT NULL DEFAULT 0 COMMENT '续借次数,上限 1 次', operator_id BIGINT DEFAULT NULL, KEY idx_reader_status (reader_id, status), KEY idx_book (book_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

有个常见的错误设计想提醒一句:有人用UNIQUE KEY (reader_id, book_id, status)来防止同一读者重复借同一本书。这条约束只在「一辈子只能借一次」时成立,读者还了再借就会直接插入失败。正确做法是查询时加status IN (1,3)判断是否已有未还记录,把并发兜底交给库存的条件更新。

3. 核心业务落地:检索分页、借还事务、权限拦截三条主线

功能列表看着长,真正有技术含量、也最容易被追问的只有三条线:查得准、借得对、拦得住。把这三条写扎实,剩下的增删改都是体力活。

3.1 图书检索与分页:动态 SQL 拼条件,PageHelper 管翻页

列表页的需求通常是:关键词(书名/作者/ISBN 模糊)、分类筛选、只看有货、分页。这类查询最忌讳写四条 SQL 分支,用 MyBatis 的<where>加<if>一条搞定:

<!-- BookMapper.xml:条件全部可选,前端传空就不参与拼接 --> <select id="selectByCondition" resultType="com.example.library.vo.BookVO"> SELECT b.id, b.isbn, b.title, b.author, b.total_copies, b.available_copies, b.location, b.status, c.name AS categoryName FROM book b LEFT JOIN category c ON c.id = b.category_id <where> <if test="keyword != null and keyword != ''"> AND (b.title LIKE CONCAT('%', #{keyword}, '%') OR b.author LIKE CONCAT('%', #{keyword}, '%') OR b.isbn = #{keyword}) </if> <if test="categoryId != null"> AND b.category_id = #{categoryId} </if> <if test="onlyAvailable != null and onlyAvailable"> AND b.available_copies &gt; 0 </if> <if test="status != null"> AND b.status = #{status} </if> </where> ORDER BY b.id DESC </select>

两个细节值得单独讲。第一,XML 里的大于号要写成&gt;,直接写>会让整个 mapper 解析失败,报的错还指向文件头,看着莫名其妙。第二,resultType用BookVO而不是Book,靠categoryName这个别名完成关联字段填充,省掉一个resultMap的配置量。

Service 层的分页调用有一个硬性顺序要求:

@Override public PageInfo<BookVO> page(BookQuery query, int pageNum, int pageSize) { // startPage 把分页参数存进 ThreadLocal,必须紧贴下面的查询 PageHelper.startPage(pageNum, pageSize); List<BookVO> list = bookMapper.selectByCondition(query); // PageInfo 会顺带算出总条数、总页数、是否有上一页下一页 return new PageInfo<>(list); }

startPage和查询之间不能插入任何其它数据库操作,否则分页参数会被前一次查询消耗掉,表现是「总数对、列表错」或者「永远只返回第一页」。这是分页插件最经典的失效方式,代码看上去完全正常。

3.2 借阅与归还:一次事务里的库存扣减与记录写入

借阅是整个系统里唯一必须保证原子性的动作:库存减一和流水插入要么一起成功,要么一起失败。我把校验放在最前,扣减用条件更新,最后写流水:

@Override @Transactional(rollbackFor = Exception.class) // 必须带上,否则受检异常不回滚 public void borrow(Long bookId, Long readerId, Long operatorId) { Reader reader = readerMapper.selectById(readerId); if (reader == null || reader.getStatus() != 1) { throw new BizException("读者证状态异常,无法借阅"); } int borrowing = recordMapper.countActive(readerId); // 已借未还数量 if (borrowing >= reader.getMaxBorrow()) { throw new BizException("已达可借上限:" + reader.getMaxBorrow() + " 册"); } // 条件更新:库存为 0 时影响行数是 0,天然挡住并发超借 int rows = bookMapper.decreaseStock(bookId); if (rows == 0) { throw new BizException("该书暂无可借副本"); } BorrowRecord record = new BorrowRecord(); record.setBookId(bookId); record.setReaderId(readerId); record.setBorrowDate(LocalDate.now()); record.setDueDate(LocalDate.now().plusDays(reader.getLoanDays())); record.setStatus(1); record.setOperatorId(operatorId); recordMapper.insert(record); }

对应的扣减 SQL 只有一句,但它是整个并发安全的支点:

<update id="decreaseStock"> UPDATE book SET available_copies = available_copies - 1 WHERE id = #{bookId} AND available_copies &gt; 0 AND status = 1 </update>

为什么不用「先SELECT查库存,再UPDATE减一」?因为两步之间可能被另一个请求插进来,两个人同时看到库存 1,都判断可借,最后库存变成 -1。把判断条件塞进WHERE里,由数据库的行锁保证同一行的更新串行执行,影响行数为 0 就代表抢失败了,直接抛异常回滚。这套写法在单机和多实例下都成立,比在 Java 里加synchronized靠谱得多。

归还的逻辑反过来,先校验记录状态,再回补库存,最后更新流水:

@Override @Transactional(rollbackFor = Exception.class) public void returnBook(Long recordId) { BorrowRecord record = recordMapper.selectById(recordId); if (record == null || record.getStatus() == 2) { throw new BizException("该借阅记录不存在或已归还"); } LocalDate today = LocalDate.now(); record.setReturnDate(today); // 超过应还日期记为超期,页面据此计算罚金 record.setStatus(today.isAfter(record.getDueDate()) ? 3 : 2); recordMapper.updateReturn(record); bookMapper.increaseStock(record.getBookId()); }

status = 3在这里表示「超期后归还」,逾期天数由return_date - due_date现算,不落库。这样规则改了(比如免罚天数从 0 天改成 3 天)只需要改计算逻辑,不用刷数据。

3.3 登录与角色权限:拦截器加白名单,够用且好讲

图书管理系统不需要 Spring Security 那一套,一个拦截器加会话就够,还能在答辩时讲清楚原理。核心是登录拦截:

public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(false); Object user = session == null ? null : session.getAttribute("loginUser"); if (user == null) { // Ajax 请求返回 401 让页面自己跳,普通请求直接重定向到登录页 if ("XMLHttpRequest".equals(request.getHeader("X-Requested-With"))) { response.setStatus(401); return false; } response.sendRedirect(request.getContextPath() + "/login"); return false; } return true; } }

注册时把静态资源和登录接口放进白名单,否则 CSS 也会被拦下来导致页面裸奔:

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/login", "/doLogin", "/css/**", "/js/**", "/images/**", "/error"); } }

角色区分用路径前缀做粗粒度控制:/admin/**只允许role=1的管理员访问,在拦截器里再读一次 session 中的角色判断即可。读者端和管理端共用一个登录入口,登录成功后按角色决定跳哪个首页。密码存储不要明文,用BCryptPasswordEncoder或至少MD5 + 盐,这一条几乎是答辩必问项。

3.4 视图层写法:JSTL 渲染 + 表单回填

JSP 页面只做渲染,不写<% %>脚本片段。列表页的典型写法:

<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <table class="table"> <thead> <tr><th>书名</th><th>作者</th><th>可借/总数</th><th>操作</th></tr> </thead> <tbody> <c:forEach items="${pageInfo.list}" var="book"> <tr> <td>${book.title}</td> <td>${book.author}</td> <td>${book.availableCopies} / ${book.totalCopies}</td> <td> <c:choose> <c:when test="${book.availableCopies > 0}"> <a href="${pageContext.request.contextPath}/borrow?bookId=${book.id}">借阅</a> </c:when> <c:otherwise><span class="disabled">暂无库存</span></c:otherwise> </c:choose> </td> </tr> </c:forEach> </tbody> </table>

pageInfo.list是后端model.addAttribute("pageInfo", pageInfo)传进来的,翻页条直接读pageInfo.pages和pageInfo.pageNum循环生成页码。所有链接统一用${pageContext.request.contextPath}打头,是保证本地和部署环境都能跳转的关键。

提示:JSP 里${}默认不过滤 HTML,图书简介这类用户可填字段要加<c:out value="${book.remark}"/>或fn:escapeXml,不然就是现成的 XSS 演示。

4. 常见问题排查:JSP 404、事务不回滚、乱码这几类翻车点

这套技术组合的问题高度集中,翻来覆去就那几类。下面按现象、原因、解决三步写,遇到时照着对一遍,比漫无目的搜要快。

4.1 视图层:页面打不开与静态资源丢失

现象一:访问接口后浏览器直接下载了一个.jsp文件,或者报 404。原因通常有三个:项目打成 jar 包,内嵌 Tomcat 只把 JSP 当静态资源返回;@RestController用在了本该返回页面的 Controller 上;视图前缀没以/开头,解析出来的路径不对。 解决:pom.xml改<packaging>war</packaging>并引入tomcat-embed-jasper;页面跳转的类改用@Controller;spring.mvc.view.prefix写成/WEB-INF/jsp/,注意首尾的斜杠。另外,用外部 Tomcat 部署时,启动类要继承SpringBootServletInitializer并重写configure,否则 war 起来后找不到任何映射。

现象二:页面能打开,但样式全丢,控制台一堆 404。原因是配了context-path之后,页面里仍写着href="/css/app.css"这种绝对路径,或者拦截器把/css/**也拦了。 解决:链接统一用${pageContext.request.contextPath}/css/app.css;拦截器白名单里补上/css/**、/js/**、/images/**、/favicon.ico。

4.2 数据层:事务失效与分页错乱

现象三:借阅失败抛了异常,库存却已经被扣掉了。原因有三种常见形态:@Transactional加在同类内部被this调用的方法上,绕过了 Spring 代理;方法不是public;异常在方法里被try-catch吞掉,或者抛的是受检异常但没写rollbackFor。 解决:把事务方法定义在 Service 接口实现里并由外部调用;显式写@Transactional(rollbackFor = Exception.class);不要在事务方法内部吞异常,需要记录日志就记完再抛出去。这一条我见过太多次,报错信息干净得很,问题全在调用路径上。

现象四:分页查出来的总数是对的,列表却是全量数据。原因是PageHelper.startPage()之后、真正查询之前插入了别的数据库调用,比如先查了一次读者信息,分页参数被那次查询消耗掉了。 解决:startPage紧贴目标查询,中间不插任何 mapper 调用;返回时用new PageInfo<>(list)包装,不要自己算总数。

4.3 运行环境:乱码、时区与打包

现象五:中文显示成问号,或者借阅时间比实际早了 8 小时。原因是链路三处编码不统一:JDBC url 没带characterEncoding=utf8;server.servlet.encoding.force没开;JSP 的pageEncoding与contentType不一致。时区问题则是 MySQL 连接没指定serverTimezone。 解决:数据库、表、连接三处统一utf8mb4;url 补上characterEncoding=utf8&serverTimezone=Asia/Shanghai;JSP 页头写全contentType="text/html;charset=UTF-8" pageEncoding="UTF-8"。这三处对齐后,乱码问题基本绝迹。

注意:外部 Tomcat 部署时访问路径由 war 包名决定。想让系统跑在根路径,把 war 改名成ROOT.war,同时确认context-path不要和它冲突。

5. 环境搭建与部署:从本地跑通到 war 包上线的一份清单

代码写完之后,能不能在另一台机器上从零跑起来,是检验这套方案完整度的硬指标。这一章给一份可照着执行的清单。

5.1 本地环境与建库脚本的执行顺序

本地需要 JDK 8 或 11、Maven 3.6+、MySQL 8。脚本分成两个文件:schema.sql建表建索引,data.sql灌基础数据(分类、管理员账号、超期规则),演示用的图书和借阅记录单独一份demo.sql,便于答辩前重置。

# 建库:字符集必须是 utf8mb4,否则书名里的生僻字和 emoji 会插入失败 mysql -u root -p -e "CREATE DATABASE library_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" # 顺序不能反:先结构,再基础数据,最后演示数据 mysql -u root -p library_db < sql/schema.sql mysql -u root -p library_db < sql/data.sql mysql -u root -p library_db < sql/demo.sql # 启动开发环境 mvn spring-boot:run -Dspring-boot.run.profiles=dev

data.sql里至少要有:一个管理员账号、一个操作员账号、三个读者、五个分类、二十本图书(其中至少两本available_copies设为 1,专门用来演示「最后一本被借走」)。演示数据的质量直接影响验收观感,别拿一堆「测试1」「测试2」凑数。

5.2 打包与两种部署方式的选择

# 打包,产物是 war 不是 jar mvn clean package -DskipTests # 方式一:丢进外部 Tomcat,路径由 war 包名决定 cp target/library.war /opt/tomcat/webapps/ # 方式二:直接运行,内嵌 Tomcat 配合 jasper 同样能解析 JSP java -jar target/library.war --spring.profiles.active=prod

两种方式都可行,差别在于:外部 Tomcat 需要把spring-boot-starter-tomcat的 scope 设为provided,并让启动类继承SpringBootServletInitializer;直接java -jar则不需要这些,但要求 JSP 相关依赖都在 war 内。答辩现场建议用java -jar起,一条命令、不依赖外部容器,出问题的概率最低。

5.3 配置分环境与答辩演示数据

application.yml只放公共配置,application-dev.yml和application-prod.yml各自覆盖数据源,用spring.profiles.active切换。这样本地连自己的库,演示环境连演示库,互不干扰。

演示前建议执行一次重置脚本,把借阅记录清空、库存恢复、把某条记录的due_date改到三天前用于展示超期。这类准备看着琐碎,但它决定了演示时能不能一句话把超期规则讲明白,而不是现场改数据库。

6. 验收与答辩:用一条借还链路把整套设计讲圆

这套系统功能不少,但答辩只有十几分钟,什么都讲等于什么都没讲。我的建议是只演示一条完整链路:登录 → 检索 → 借出 → 库存变化 → 超期展示 → 归还,每一步都对应一个设计决策。这条链路串起来,架构、事务、权限、页面渲染全部覆盖到了,评委追问也有落点。

准备一张验收表,自己先走一遍,每一条都过:

验收项演示动作预期结果
登录与拦截未登录直接访问图书列表地址跳转登录页,登录后回到原页面
检索分页输入关键词并翻到第二页结果只含匹配项,总条数与总页数正确
借阅扣减借出一本库存为 1 的书可借数减 1,借阅记录新增一条
并发兜底对同一本书连续快速点两次借阅第二次提示无可借副本,无脏记录
归还回补归还该书可借数恢复原值,状态改为已还
超期判定把应还日期改到昨天后刷新列表标红并显示逾期天数

讲解顺序上,先给一张架构图讲清楚六层模块的职责,再挑借阅这个动作往下钻:为什么库存要冗余、为什么扣减用条件更新而不是先查后改、为什么事务放在 Service 而不是 Controller。把这三问答完整,基本就没人再纠结你用的是 JSP 还是模板引擎了。反而如果一上来就讲页面多漂亮,最后往往被问「两个人同时借最后一本会怎样」而答不上来。

还有一个细节:所有校验失败、库存不足、超期归还的提示语要提前设计好,别出现NullPointerException的堆栈直接弹到页面上。演示环境里崩一次,后面讲得再好也救不回来。

我第一次带这类项目验收时,就是因为演示前没跑一遍重置脚本,讲到归还环节发现库存已经对不上,当场花了五分钟查数据。从那以后我每次演示前都强制走一遍完整链路,从登录到归还一条不落,跑通了才敢上台。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询