☰
图书馆管理系统完整源码解析:从数据模型到并发借还书边界处理
2026/10/17 5:14:40 网站建设 项目流程

简介:图书馆管理系统完整源代码是一套典型B/S架构的Web项目,面向需要快速搭建图书借阅管理场景的开发者、计算机专业学生及课程设计人群,覆盖图书录入、借阅、续借、归还、预约取消、查询与统计分析等核心业务流程。压缩包共175个文件、仅482KB,包含90个gif操作演示图、26个cs后台逻辑文件、23个aspx页面、17个js脚本,另附HTML/CSS前端样式与mdf/ldf数据库文件,可对照演示动图逐步追踪页面请求、后台处理与数据库读写之间的完整链路。目录结构按Controller、Model、View分层设计,配合SQL脚本、配置文件与报表文件,便于理解系统权限控制、模块间调用关系以及借阅数据统计的落地方式,为二次开发提供清晰抓手。已有5713人学习/下载,适合用于课程设计、毕业设计、实训教学或中小型图书馆管理系统的快速搭建,也可作为Web应用开发的学习案例。

1. 图书馆管理系统:人人都说简单,完整源代码的坑全在边界

图书馆管理系统(完整源代码)是软件项目里被低估程度最严重的一个。表面看就是图书增删改查、读者借书还书,可一旦把库存准确性、逾期费用计算、并发还借、角色权限这几件事同时放进一套代码里,翻车概率立刻上去。很多同学拿到的“完整源代码”要么跑不起来,要么能跑但一上线就出数据错乱,原因都在边界没处理。

这个标题的诉求很明确:要一套能直接运行的完整代码,同时知道每段代码为什么这么写。它适合三类人——正在准备课程设计的在校生,想拿一个规范项目练手的初级开发者,以及需要快速搭一个内部图书管理工具的运维同学。读完你能得到的不只是一份代码结构,而是从建表到部署的完整决策链。

下面我从数据模型、借还书链路、权限边界和踩坑排查四个角度拆开说。代码示例以 Java Spring Boot + MySQL 为基准,这个组合在开源社区资料最全,换成 Python Django 或其他 Web 框架时,核心流程和 SQL 思路直接平移。

2. 数据模型先行:四张核心表把业务边界钉死

我在写代码之前会先干一件看起来多余的事:把所有业务名词写出来,再落字段。图书、读者、借阅记录、预约,四张表才是闭环。大多数“完整源代码”喜欢压成两张表——book 里加一个 borrowed_by 字段,或者 reader 里存一个 borrowed_books 列表。这种设计演示时没问题,一旦要查“谁在借什么书”或者“某本书被借过几次”就抓瞎。

2.1 图书表与读者表:字段怎么设计才经得起盘点

先看图书表。我的建议是保留 total_count 和 available_count 两个独立字段,前者是馆藏总量,后者是当前可借数量。很多源码只用一个 count,还书时 count+1、借书时 count-1,看起来没什么问题,可一旦要下架图书或做盘点,就没有参照物。两个字段的差值就是在借数量,哪一笔对不上,可以直接拿差值去 borrow_record 反查。

CREATE TABLE book ( id BIGINT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL COMMENT 'ISBN,同书多副本视为多条记录', title VARCHAR(200) NOT NULL, author VARCHAR(100), publisher VARCHAR(100), category VARCHAR(50), total_count INT NOT NULL DEFAULT 1 COMMENT '馆藏总量', available_count INT NOT NULL DEFAULT 0 COMMENT '当前可借数量', status TINYINT NOT NULL DEFAULT 1 COMMENT '1-在架 0-下架', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_isbn (isbn) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里有几个字段设计是踩过坑换来的。isbn 用 VARCHAR(20) 而不是数值类型,因为 ISBN 里可能带字母 X,而且数值类型会把前导补零吃掉。status 字段不要省,下架图书是真实业务,直接删数据会连着借阅记录一起删干净,历史审计就没了。create_time 和 update_time 用 DATETIME 默认值自动维护,注意 update_time 的 ON UPDATE CURRENT_TIMESTAMP 在旧版 MySQL 上不会自动更新,别把判断逻辑建在这个字段上。

读者表这边,两个字段要求必须写进建表脚本。max_borrow 是最大可借数量,默认 5,写在表里而不是代码里,意味着“教师可借 10 本、学生可借 5 本”就是改一条数据,不是改代码重发版。status 是账号状态,1 正常 0 停用,停用读者不能借书,这是安全边界而不是业务提示。

CREATE TABLE reader ( id BIGINT PRIMARY KEY AUTO_INCREMENT, reader_no VARCHAR(20) NOT NULL COMMENT '学号或工号', name VARCHAR(50) NOT NULL, password VARCHAR(60) NOT NULL COMMENT 'BCrypt哈希', role TINYINT NOT NULL DEFAULT 2 COMMENT '1-管理员 2-读者', max_borrow INT NOT NULL DEFAULT 5, status TINYINT NOT NULL DEFAULT 1 COMMENT '1-正常 0-停用', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_reader_no (reader_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

注意 password 字段长度给 VARCHAR(60),对应 BCrypt 哈希的固定长度。很多项目把密码列设成 CHAR(32),那是为 MD5 准备的,换 BCrypt 后哈希被截断,登录永远失败,这个问题后面避坑章节还会细说。

2.2 借阅表:为什么应还日期必须写死而不是实时计算

借阅表是整系统里业务最重的表,七个字段起步:书 ID、读者 ID、借出时间、应还时间、实际归还时间、状态、续借次数,再带一个罚金字段更稳。这样一建,后续所有关于借阅的查询和统计都有落点。

CREATE TABLE borrow_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, book_id BIGINT NOT NULL, reader_id BIGINT NOT NULL, borrow_time DATETIME NOT NULL, due_time DATETIME NOT NULL COMMENT '应还日期,借出时算好写死', return_time DATETIME NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT '1-借出 2-已还 3-逾期归还 4-续借过', renew_count INT NOT NULL DEFAULT 0, fine DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '逾期罚金,还书时结算', INDEX idx_reader (reader_id), INDEX idx_book (book_id), INDEX idx_due (due_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

fine 字段用 DECIMAL(10,2) 而不是 double,金额计算用浮点是账目大忌,累加多了会出现 0.30000000000000004。due_time 写在表里并且在还书时直接比对,这是借阅规则可演进的前提:今天定的是 30 天,半年后改成 20 天,历史未还记录如果实时计算,应还日期会跟着新规则飘移;写死的话,历史记录永远是借出那一刻的规则,逾期罚金也有据可依。

应还日期的另一种实现是还书时用借出时间加固定时长实时算,省一个字段,但跨天问题会找上门。服务端时区、数据库时区、代码时区任何一个不一致,实时计算的“应还日期”都会比写死的差一天。所以我借书时直接算好:

record.setBorrowTime(LocalDateTime.now()); record.setDueTime(LocalDateTime.now().plusDays(30));

30 这个数字放在配置文件里,不硬编码。想改借阅周期只动配置不动代码。如果规则复杂到要跳过节假日,就把节假日表加进来,在 service 层遍历日期跳过;写死日期不影响这套扩展,反而让扩展有据可依。

2.3 外键、索引和预约表:三个容易想当然的设计点

课程设计和演示项目,物理外键加上去有好处:数据库建模工具能自动生成 ER 图,答辩报告里画出来很加分。但我实际开发时倾向不加物理外键,用逻辑关联——service 层保证引用完整性。原因是分库分表、数据归档、批量导入时,物理外键都会变成阻力;而且外键约束失败时 MySQL 的报错对排查业务问题帮助有限。如果加外键,注意对应列必须建索引,否则删除父表记录时 InnoDB 会全表扫描子表。

索引也不要贪多。借阅记录查询基本围绕三个维度:某读者的在借列表、某本书的借阅历史、到期未还列表。对应 idx_reader、idx_book、idx_due 三个普通索引足够。不要额外建 reader_id + status 这类复合索引,等值加范围的查询用单列索引已经覆盖,多索引只会增加写入放大。

预约表是第四个实体,很多人会忘。预约解决的是“书全被借走但读者还想要”的场景。预约表只存申请人和想借的 book_id,还书时按申请时间顺序分配。

CREATE TABLE reserve ( id BIGINT PRIMARY KEY AUTO_INCREMENT, book_id BIGINT NOT NULL, reader_id BIGINT NOT NULL, reserve_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, status TINYINT NOT NULL DEFAULT 1 COMMENT '1-等待中 2-已通知 3-已取消 4-已借出', expire_time DATETIME COMMENT '通知后的保留截止时间', INDEX idx_book_status (book_id, status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

为什么需要“已通知”状态?因为在真实系统里,预约到书后要给读者保留 48 小时,过期释放给下一位。没有这个状态,通知窗口就协调不了。这个表看起来不起眼,但它是系统区分“玩具项目”还是“可上线项目”的分水岭。

3. 借还书核心链路:并发校验到状态机,一步都不能省

借书不是一句UPDATE book SET available_count = available_count - 1 WHERE id = ?那么简单。这句话单独跑一百遍都不会错,但它没有校验读者有没有达到上限、书有没有下架、读者是不是停用状态、库存扣减和借阅记录写入是否在同一个事务里。四件事缺任何一件,线上数据都会乱。

3.1 借书接口的四个隐藏条件与校验顺序

我一般把校验分成四步,顺序有讲究。先查读者,再查数量,再锁库存行,最后写记录。顺序不能乱,因为先锁库存再查读者,会让无效请求也占着锁,高峰期会拖慢所有借书请求。

@Transactional public BorrowRecord borrowBook(Long bookId, Long readerId) { Reader reader = readerMapper.selectById(readerId); if (reader == null || reader.getStatus() != 1) { throw new BusinessException("读者不存在或已停用"); } int borrowingCount = borrowRecordMapper.countByStatus(readerId, 1); if (borrowingCount >= reader.getMaxBorrow()) { throw new BusinessException("已达最大可借数量: " + reader.getMaxBorrow()); } Book book = bookMapper.selectByIdForUpdate(bookId); if (book == null || book.getAvailableCount() <= 0) { throw new BusinessException("该图书暂无剩余库存"); } bookMapper.decreaseAvailable(bookId); BorrowRecord record = new BorrowRecord(); record.setBookId(bookId); record.setReaderId(readerId); record.setBorrowTime(LocalDateTime.now()); record.setDueTime(LocalDateTime.now().plusDays(30)); record.setStatus(1); borrowRecordMapper.insert(record); return record; }

对应 mapper 里的 selectByIdForUpdate,本质上是一个带排他锁的查询:

@Select("SELECT * FROM book WHERE id = #{id} FOR UPDATE") Book selectByIdForUpdate(Long id);

说一下参数和顺序背后的逻辑。selectByIdForUpdate 是悲观锁,SELECT 时对 book 行加排他锁,另一个事务走到同一行会被阻塞,等前一个事务提交或回滚;库存判断和库存扣减之间不能有间隙,这就是为什么不能先 selectById 再单独 update。plusDays(30) 里的 30 用配置项管理,而不是硬编码在业务代码里。

校验项失败时的提示对应字段
读者存在且正常读者不存在或已停用reader.status
在借数量未达上限已达最大可借数量reader.max_borrow
图书在架且有库存该图书暂无剩余库存book.available_count

3.2 还书与逾期结算:跨天逻辑必须服务端统一算

还书接口的坑集中在“逾期几天”的计算上。不要在 SQL 里用 DATE_FORMAT 掐字符串,也不要在前端用浏览器时间算,统一在服务端用 LocalDateTime 算。代码如下:

@Transactional public void returnBook(Long recordId) { BorrowRecord record = borrowRecordMapper.selectByIdForUpdate(recordId); if (record == null || record.getStatus() != 1) { throw new BusinessException("借阅记录不存在或已归还"); } LocalDateTime now = LocalDateTime.now(); record.setReturnTime(now); if (now.isAfter(record.getDueTime())) { long overdueDays = ChronoUnit.DAYS.between(record.getDueTime(), now); record.setFine(BigDecimal.valueOf(overdueDays).multiply(FINE_PER_DAY)); record.setStatus(3); } else { record.setStatus(2); } borrowRecordMapper.update(record); bookMapper.increaseAvailable(record.getBookId()); }

执行顺序有讲究:先锁借阅记录行,再改状态,最后恢复库存。如果把恢复库存放在改状态之前,中途 update 失败会导致库存多了而记录还停在借出。FINE_PER_DAY 我按每天 0.1 元算,一天是一个自然日,ChronoUnit.DAYS.between 会把两个 LocalDateTime 转成日期再求差,23:59 还和 00:01 还不会出现“同一本书跨了几个小时被算成两天”的怪结果。status 里“逾期归还”和“正常归还”分开存,而不是只根据 return_time 和 due_time 比较,是为了统计报表时可以直接按状态分组。

注意:available_count 恢复和 borrow_record 状态更新必须在同一个事务里。任何一个先提交,另一个失败,就会出现“书还了但库存没加”或者“库存加了但记录还在借出”的数据不一致。

3.3 续借与预约队列:状态机多出来的两个分支

续借不是“把 due_time 加 30 天”这么简单。常见规则是限续借一次、逾期不能续借、续借从原应还日期起算。我用一个分支处理:

public void renewBook(Long recordId) { BorrowRecord record = borrowRecordMapper.selectByIdForUpdate(recordId); if (record.getRenewCount() >= 1) { throw new BusinessException("已达续借次数上限"); } if (LocalDateTime.now().isAfter(record.getDueTime())) { throw new BusinessException("已逾期,请先还书"); } record.setDueTime(record.getDueTime().plusDays(30)); record.setRenewCount(record.getRenewCount() + 1); record.setStatus(4); borrowRecordMapper.update(record); }

这里的 30 天可以独立于首次借阅配置,因为很多馆内规则是首次 30 天、续借 15 天。续借时还要考虑一件事:如果这本书有人预约,是否允许续借?不同机构策略不同,我一般把“是否允许预约中的书续借”做成配置项。预约队列分配时,还书触发的优先级是:等待时间最早、状态正常、未超保留期。不要在分配逻辑里用随机抽取,真实业务要可解释可追溯,谁排在前面必须说得清楚。

4. 角色权限与接口安全:前端的隐藏按钮从来不是安全边界

图书馆管理系统有管理员和读者两类角色。很多“完整源代码”只在页面层做区分——管理员看到“删除图书”按钮,读者看不到——然后后端接口完全没有校验。这个做法放到答辩演示没有问题,放到任何真实环境都会被直接打穿,因为接口是可以不带页面直接调的。

4.1 登录态选 Session 还是 Token:先看部署形态

单体项目我默认用 Session。原因很简单:接入成本最低,登录后 session.setAttribute("currentUser", reader),所有请求都能拿到,不需要额外引入密钥管理,服务端还能主动踢人。前后端完全分离的架构再用 Token,一用 JWT 就要处理秘钥轮换、过期时间、服务端无法主动吊销这几个问题,图书馆系统这种单体场景没有必要。

登录接口的密码校验一定要用 BCrypt 而不是 MD5。MD5 撞库成本极低,彩虹表一查就出来,图书馆系统里虽然核心数据不是隐私,但读者密码泄露出去同样麻烦。BCrypt 天然带盐,每次哈希结果不同,验证用 matches() 而不是重新加密再比对:

@PostMapping("/login") public Result login(@RequestBody LoginRequest req) { Reader reader = readerMapper.selectByUsername(req.getUsername()); if (reader == null || !BCrypt.checkpw(req.getPassword(), reader.getPassword())) { throw new BusinessException("用户名或密码错误"); } request.getSession().setAttribute("currentUser", reader); return Result.ok(); }

注意错误提示要统一用“用户名或密码错误”,不要分开提示“用户不存在”和“密码错误”,否则别人通过提示就能枚举出有效账号。这是很基础但很容易被忽略的细节。

角色可访问范围典型操作
管理员/admin/** 全部接口图书增删改、读者管理、查看全部借阅记录
读者/reader/** 与公开接口查书、借书、还书、查看自己的借阅记录
未登录/login、/books/list浏览图书目录

4.2 拦截器统一校验:管理员接口和读者接口分开控

不要在每个 controller 里复制粘贴“判断是否登录”的代码,用拦截器统一收口。我是一个 AuthInterceptor 处理登录态和角色:

public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { HttpSession session = request.getSession(false); if (session == null || session.getAttribute("currentUser") == null) { response.sendRedirect("/login"); return false; } if (request.getRequestURI().startsWith("/admin/")) { Reader current = (Reader) session.getAttribute("currentUser"); if (current.getRole() != 1) { response.setStatus(403); return false; } } return true; } }

注册时注意放行路径。登录页、静态资源、公开的图书列表都要放行,不然未登录用户连书都查不了。角色判断用 role 字段,管理员 role=1,读者 role=2。不要把“是否是管理员”设计成独立权限表,一个小系统用角色字段就是最简单可靠的方案,等真出现第三类角色再加表,提前加是过度设计。

4.3 两个必查的安全点:SQL 注入与水平越权

SQL 注入是图书馆系统里最容易被忽略的。搜索书名时,如果查询条件直接用字符串拼接,构造一个带单引号的输入就能让整条 SQL 变形。MyBatis 里用 #{} 而不是 ${}:

@Select("SELECT * FROM book WHERE title LIKE CONCAT('%', #{keyword}, '%')") List<Book> search(String keyword);

#{} 会预编译成参数占位符,${} 是直接拼接字符串,keyword 里带个单引号或注释符就会改变 SQL 语义。排序字段、表名这种不能参数化的地方,用白名单映射,不要直接把用户输入拼进去。分页的 pageSize 必须设上限,常见做法是超过 100 强制置 100,防止一次性拖全表。

水平越权的典型场景是读者改 URL 里的记录 ID 查别人的借阅历史。修复方案是不信 URL,从登录态里拿当前用户。在 service 层加一行归属校验:

BorrowRecord record = borrowRecordMapper.selectById(recordId); if (!record.getReaderId().equals(currentReader.getId())) { throw new BusinessException("无权查看该记录"); }

这一行把数据归属和操作者绑死,成本极低,但在课程设计和真实项目里都是最容易丢的一行。很多源代码把“是否能查”交给前端控制,后端拿到 ID 直接查,导致任何登录用户都能遍历 ID 看全馆借阅历史。

5. 完整源代码落地的五个高频坑与排查:从乱码到并发超借

这一章的问题没有玄学,全是我实际帮别人调代码时反复遇到的。每条按现象、原因、解决三步写,遇到类似的问题可以直接对照排查。

5.1 中文乱码:从建库到连接串的字符集链条

现象:页面显示正常,但数据库里存的是问号或者乱码;反过来,数据库里手动插入的中文,页面读出来是问号。

原因:字符集链条有四个环节,任何一个不是 utf8mb4 就乱——建库默认字符集、表字符集、JDBC 连接串、页面响应头。常见的翻车点是建库时用默认 latin1,然后代码里只设置 request.setCharacterEncoding。

解决:统一到 utf8mb4,建库时显式指定:

CREATE DATABASE library_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

连接串里也要给足参数:

jdbc:mysql://localhost:3306/library_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai

排查时先看存进去的原始数据脏没脏:存的时候就脏,改表和连接的字符集并重新插入;存进去干净、读出来乱,多半是响应头或者页面 meta 的字符集问题。用SHOW VARIABLES LIKE 'character%';一条命令看全链路,先定位是哪一环再动手,不要瞎改。

5.2 可借数量对不上:并发借书没锁行

现象:馆藏 1 本的书,两个读者同时点了借阅,都提示成功,最后 available_count 变成 0 或 -1,借阅记录却只有一条,或者两条都插进去了。

原因:典型的 check-then-act 竞态。代码先 SELECT available_count,判断大于 0,再 UPDATE 扣减,两个事务同时通过检查,库存就穿底了。

解决:二选一。悲观锁用前面写的 selectByIdForUpdate,事务里锁住那一行直到提交;乐观锁用 UPDATE 带条件,影响行数为 0 就是被并发抢走:

int rows = bookMapper.decreaseAvailableWithCheck(bookId); if (rows == 0) { throw new BusinessException("该图书暂无剩余库存"); }

对应 SQL:

UPDATE book SET available_count = available_count - 1 WHERE id = #{bookId} AND available_count > 0;

两者参数差异要注意:悲观锁适合冲突频繁的借还高峰期,缺点是持锁期间所有读这行的请求排队;乐观锁适合低并发,代码简单但失败后要重试或提示。图书管理系统借还并发一般不会太高,我用乐观锁居多,扣减语句同时承担了判断职责,少一次查询。

5.3 还书日期差一天:时间类型与时区

现象:借书日期是 2025-06-01,数据库里成了 2025-05-31,或者应还日期比实际晚一天。

原因:混用了 LocalDateTime 和 java.util.Date,Date 在序列化和存储时会带上 JVM 默认时区,MySQL 的 DATETIME 不带时区,两者一翻译就偏了;另一种是 JDBC 连接串缺少 serverTimezone,MySQL 8 的驱动按服务器系统时区解释,本地和服务器时区不一致就偏移。

解决:代码里统一用 java.time.LocalDateTime,实体字段、DTO 都换成这个类型。连接串显式加 serverTimezone=Asia/Shanghai。建表时用 DATETIME 而不是 TIMESTAMP,TIMESTAMP 有 2038 年问题并且隐含时区转换,图书系统没有跨时区需求,用 DATETIME 最不折腾。

5.4 改了密码登不上:哈希没加盐

现象:管理员在数据库里直接 UPDATE 密码为明文 123456,或者用在线工具算了个 MD5 写进去,结果登录永远失败。

原因:代码里密码校验用的是 hash,而且注册和登录不是同一套逻辑。最常见的是注册时 MD5(password + 固定盐),登录时只 MD5(password),盐对不上;另一个是把密码字段设成 CHAR(32),而 BCrypt 哈希长度 60,直接截断导致校验失败。

解决:统一用 BCrypt,注册和登录都走同一个处理函数:

String encoded = BCrypt.hashpw(rawPassword, BCrypt.gensalt());

数据库字段设成 VARCHAR(60),不要再手动改库。排查这类问题,先看注册函数和登录函数用的密码处理函数是不是同一个,再确认字段长度。这是这类项目最隐蔽的翻车点,因为只注册不登录时发现不了。

5.5 换台电脑跑不起来:路径、版本与数据库差异

现象:自己的电脑跑得好好的,代码发给同学,一启动就报错——ClassNotFoundException、端口被占、连接数据库失败、图片加载不出来,五花八门。

原因:绝对路径、JDK 版本不匹配、MySQL 连接驱动与数据库版本不对、root 密码认证插件差异。MySQL 8 默认用 caching_sha2_password,老版本驱动不认识,必须升级驱动或改认证;JDK 编译版本 8、运行环境 11,不是不能跑,但编译和运行不一致时容易出隐蔽问题。

解决:项目里不要出现 C:\Users\xxx\ 这种绝对路径,上传目录用相对路径或 classpath 资源;pom 里把 maven.compiler.source 和 target 显式写死到同一个版本;MySQL 8 配 8.0.x 的驱动,连接串里加 allowPublicKeyRetrieval=true。这些参数都不难,但少了任何一个,换环境必报错。

6. 从能跑到能交付:让源代码有说服力的三个收尾技巧

6.1 带一份可重复初始化的演示数据

代码能跑只是起点,能让别人一键跑起来才算完整。我习惯在 resources 下放 schema.sql 和 data.sql,配合 Spring Boot 的初始化配置:

spring: sql: init: mode: always >log.info("借书成功 readerId={} bookId={} recordId={}", readerId, bookId, record.getId());

别人看你的代码,或者你自己三个月后回来看,都能直接还原现场。日志格式统一成“操作类型|读者ID|图书ID|记录ID|耗时ms”,排查问题的时候 grep 一行就能串起一条完整链路。

6.3 演示项目时先讲这三个数字

第一,并发借书测试:用 CountDownLatch 起 50 个线程同时对同一本书调用借书接口,观察成功数是不是只有 1。这个数字直接证明你的库存逻辑在并发下没穿。第二,核心接口的响应时间,用 curl -w 或者压测工具顺手测一下,报出具体数字。第三,模块行数:controller、service、mapper 各多少行,用行数说明分层清楚、没把业务全塞在 controller 里。

我自己每次收尾项目都会把 README 重写一遍,当成给三个月后的自己看,里面写清楚数据库版本、初始化步骤、默认账号和改密码的方式。这个习惯救过我很多次,也希望帮到你。

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

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

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

立即咨询