简介:基于Java与MySQL开发的完整图书管理系统资源包,面向Java初学者、课程设计及毕业设计人群。系统按角色划分权限,普通读者可查询图书与个人借阅情况,管理员可完成用户管理、新书入库等操作;主体包含系统管理、进书管理、图书入库管理和查询四大模块,覆盖馆藏登记、编目、检索、借阅等日常核心流程。包内共270个文件,压缩后约1.59MB。含46个Java源文件与200个class文件,既可直接运行项目,也可导入开发环境进行二次修改;另有library.sql完整数据库脚本、工程配置文件及若干界面图片,方便快速搭建调试。资源附有管理员登录账号和数据库连接配置说明,降低上手门槛。目前已有6306人学习下载,适合需要一套能跑、能改、能演示的图书管理系统参考答案。通过它可掌握用户权限控制、图书信息登记与编目、多条件查询等典型功能的实现思路,并能在其基础上扩展预约、续借、统计等功能。
1. 一个 Java 图书管理系统,最值钱的其实是那份“完整数据库脚本”
很多人看到“java实现的完整图书管理系统+mysql+完整数据库脚本”这个标题,第一反应是课程设计,第二反应是“又是 CRUD 练手项目”。但你要是真去做一遍就会发现:借书、还书、图书检索这些功能本身并不难,难的是把 MySQL 表结构设计得能撑住业务、把数据库脚本写得能一次跑通、把 Java 和 SQL 之间的数据类型与事务边界理顺。我在带新人时经常拿它当第一份实战任务,一个人从空库开始,建表、连库、写借还逻辑,跑完基本就能覆盖后端开发日常最常用的那组技能。适合谁:想补 Java 后端基本功的新人、需要做课程设计的学生、以及准备面试前想复习 JDBC 事务和索引的工程师。
2. 先把“完整数据库脚本”做扎实:6 张核心表加一套可重复执行的 SQL
2.1 先从实体关系说起:为什么图书管理系统至少需要 6 张表
图书管理系统的业务模型并不复杂,但很多人直接从一张 books 表开干,结果做到借阅记录时被迫返工。先想清楚实体关系:管理员需要登录,读者要办借阅证,图书要有分类,一次借阅行为要记录“谁、哪本书、什么时候借、什么时候还、是否续借”。这里最核心的关系是读者与图书之间的多对多:一个读者可以借多本书,一本书在不同时间可以被多个读者借出,所以必须拆出一张独立的借阅记录表,用来承接这层关系。
我一般会把表拆成这样:管理员表、图书分类表、图书表、读者表、借阅记录表,再加上一个可选的逾期费用表或操作日志表。标题里说的“完整数据库脚本”,指的不仅是创建这几张表,还应该包含外键约束、索引、初始管理员账号、测试图书数据和部分样例借阅记录。否则一套空表业务跑不起来,测验也不方便。
建表顺序有讲究:必须先建父表,再建子表。比如图书表依赖分类表,借阅记录表依赖图书表和读者表。如果你把脚本一股脑写在一个文件里,MySQL 执行到外键时会检查引用的表是否存在,顺序错了直接报Cannot add foreign key constraint。为了避免这种问题,我会在文件里把依赖关系从前往后排好,并且把建库语句放在最前面。
2.2 一份能直接落地的建库建表脚本:字段、索引与外键一次到位
下面这份 SQL 是一个最小但完整的数据库脚本骨架。你可以直接复制到本机 MySQL 执行,也可以在此基础上扩充。我用的是 MySQL 5.7 和 8.0 都兼容的写法。
-- 建库:用 utf8mb4 而不是 utf8,否则生僻字和 emoji 会丢 CREATE DATABASE IF NOT EXISTS library_system DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE library_system; -- 管理员表 CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', username VARCHAR(32) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(64) NOT NULL COMMENT '建议存 MD5 或加盐后的密文', real_name VARCHAR(32) DEFAULT NULL COMMENT '真实姓名', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='管理员表'; -- 图书分类表 CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '分类ID', name VARCHAR(50) NOT NULL UNIQUE COMMENT '分类名', sort_order INT DEFAULT 0 COMMENT '排序,越小越靠前' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图书分类'; -- 图书表 CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '图书ID', category_id INT NOT NULL COMMENT '所属分类', isbn VARCHAR(20) DEFAULT NULL COMMENT 'ISBN 编码', title VARCHAR(128) NOT NULL COMMENT '书名', author VARCHAR(64) DEFAULT NULL COMMENT '作者', publisher VARCHAR(128) DEFAULT NULL COMMENT '出版社', total_stock INT NOT NULL DEFAULT 0 COMMENT '总库存', available_stock INT NOT NULL DEFAULT 0 COMMENT '可借库存', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_category (category_id), INDEX idx_title (title), CONSTRAINT fk_book_category FOREIGN KEY (category_id) REFERENCES category(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图书表'; -- 读者表 CREATE TABLE reader ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '读者ID', card_no VARCHAR(32) NOT NULL UNIQUE COMMENT '借阅证号', name VARCHAR(64) NOT NULL COMMENT '读者姓名', phone VARCHAR(20) DEFAULT NULL COMMENT '联系电话', status TINYINT DEFAULT 1 COMMENT '1正常 0挂失', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='读者表'; -- 借阅记录表:承接读者和图书的多对多关系 CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '记录ID', book_id INT NOT NULL COMMENT '图书ID', reader_id INT NOT NULL COMMENT '读者ID', borrow_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '借出时间', due_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '应还时间', return_time DATETIME DEFAULT NULL COMMENT '实际归还时间', renew_count TINYINT DEFAULT 0 COMMENT '续借次数', fine_amount DECIMAL(10,2) DEFAULT 0.00 COMMENT '罚款金额', status TINYINT DEFAULT 0 COMMENT '状态:0借出 1已还 2逾期', INDEX idx_reader (reader_id), INDEX idx_book (book_id), INDEX idx_status (status), CONSTRAINT fk_br_book FOREIGN KEY (book_id) REFERENCES book(id), CONSTRAINT fk_br_reader FOREIGN KEY (reader_id) REFERENCES reader(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='借阅记录表'; -- 初始数据:密码为 123456 的 MD5 值,仅用于演示 INSERT INTO admin(username, password, real_name) VALUES ('admin', 'e10adc3949ba59abbe56e057f20f883e', '系统管理员'); INSERT INTO category(name, sort_order) VALUES ('文学', 1), ('计算机', 2), ('历史', 3); INSERT INTO book(category_id, isbn, title, author, publisher, total_stock, available_stock) VALUES (1, '9787020002207', '红楼梦', '曹雪芹', '人民文学出版社', 5, 5), (2, '9787111213826', 'Java编程思想', 'Bruce Eckel', '机械工业出版社', 3, 3), (3, '9787101000124', '史记', '司马迁', '中华书局', 2, 2);这段脚本的逻辑很直白:建库、指定字符集、建表、加索引和外键、灌初始数据。几个关键参数说明一下。ENGINE=InnoDB必须写,因为 InnoDB 才支持外键和事务,MyISAM 不支持,后期做借书还书时会非常被动。utf8mb4_general_ci是排序规则,检索时不区分大小写,适合中文场景。外键命名前缀fk_便于后面删除和排查,索引命名前缀idx_是为了在有多个索引时能一眼看出来是哪个字段。
你可能会问available_stock为什么不加CHECK (available_stock >= 0)。因为在 MySQL 5.7 里 CHECK 约束会被解析但不会真正生效,8.0.16 之后才严格执行。所以我在后面的 Java 代码里用条件更新来兜底,这是更可靠的做法。
2.3 外键、索引与字符集:脚本里最容易翻车的三个参数
先说外键。外键必须要求字段类型完全一致,比如book.category_id是 INT,父表category.id也必须是 INT,不能是 BIGINT 或 INT UNSIGNED,否则导入脚本时 MySQL 会报一个让你摸不着头脑的Cannot add foreign key constraint。另外外键列最好都建索引,MySQL 在创建外键时会自动为父表列建索引,但子表列不一定会,所以我在脚本里手动加了idx_category、idx_reader、idx_book。
再说字符集。很多老旧脚本还在用utf8,但 MySQL 的utf8实际最多只支持 3 字节,遇到“𠮷”这类生僻字或 emoji 就会报错。现在的共识是直接上utf8mb4,字符集和排序规则在建库时定好,建表时也顺手带上DEFAULT CHARSET=utf8mb4。这样能避免一大半的中文乱码问题。
最后是导入方式。这份脚本要整体执行,不是复制一句话跑一句话。如果你在命令行手工复制粘贴大段 SQL,一旦中间有语法错误,MySQL 客户端会中断,你也不知道哪些表建好了、哪些没建好。所以要么用source命令导入整个文件,要么用数据库工具一次性执行整个脚本。
2.4 导入脚本的两条路径:命令行和可视化工具,至少要会一条
在 Windows 或 Linux 上,最直接的导入方式是利用source命令:
mysql -uroot -p -e "source /path/to/library_schema.sql"如果你已经建好库,也可以用重定向方式:
mysql -uroot -p library_system < library_schema.sql这两条命令有一个区别:source是 MySQL 客户端的命令,它会逐条读取文件里的 SQL 并执行,报错时你能看到具体在哪一行;重定向方式是 shell 把文件内容喂给 mysql 客户端,遇到错误时也能看到 SQL 错误编号,但排查行号不那么直观。新手我更推荐source。
如果你用的是 IDEA 自带的 Database 工具或者 Navicat,导入逻辑一样:先创建连接,选中library_system库,再执行整个脚本文件。唯一要注意的是,可视化工具里“脚本是否包含建库语句”决定了你怎么导入。如果脚本里有CREATE DATABASE,你直接对着连接的根节点执行即可;如果没有,你就必须先手动建好库,再在目标库上跑表结构。标题里强调“完整数据库脚本”,我个人的标准是脚本里必须带上建库语句和初始数据,这样换个新环境删库重建,一条命令全搞定,这才是真正的“后悔药”。
导入完成后,用一条 SQL 验证一下表是否齐全:
SHOW TABLES;正常会看到admin、category、book、reader、borrow_record这五张表。如果表都在,初始数据也插进去了,数据库这一半就算完成了。
3. Java 端借书还书事务链路:从 JDBC 连接到 Service 层事务边界
3.1 选型先说清楚:Servlet + JSP + JDBC,还是纯 JDBC 控制台
标题没写用的什么前端框架,这很正常。常见的做法是两类:一是纯 Java 控制台或 Swing 界面配合 JDBC,适合强调 Java SE 基础;二是 Servlet + JSP + JDBC 的 Web 版本,这也是大多数课设和面试项目的标准形态。我更推荐 Web 版本,因为你能顺带把请求参数、会话、异常处理都过一遍,面试时讲起来素材更多。
不管哪种形态,核心都不在页面,而在 JDBC 连接和事务。所以我先写一个通用的数据库连接工具类,后面所有的 DAO 和 Service 都基于它:
import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DbUtil { private static final String URL = "jdbc:mysql://localhost:3306/library_system" + "?useUnicode=true&characterEncoding=utf8" + "&useSSL=false&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "你的密码"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new RuntimeException("MySQL 驱动未找到,请检查依赖", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }这段代码里有三个参数需要格外注意。characterEncoding=utf8对应数据库的utf8mb4,保证中文在 JDBC 传输过程中不乱码。serverTimezone=Asia/Shanghai解决 MySQL 8.0 下的时区报错,不写的话连接时可能出现 “The server time zone value” 异常。useSSL=false是在本地开发时省去 SSL 握手,生产环境不能照抄这个参数。
关于驱动类名,MySQL 5.7 及以前常用com.mysql.jdbc.Driver,MySQL 8.0 之后必须用com.mysql.cj.jdbc.Driver。如果你用的是 mysql-connector-java 8.x,继续写老驱动类名也能兼容,但控制台会打警告,建议一步到位写新的。
3.2 借书业务的事务代码:为什么不能把连接关在 try-with-resources 里
借书、还书、续借这三个动作是典型的跨表事务。以借书为例,业务上要做两件事:往borrow_record插一条记录,同时把book.available_stock减一。这两步必须在一个事务里完成,否则会出现“记录有了但库存没减”或者“库存减了但记录丢了”的数据不一致。
我在 Service 层的实现如下:
public void borrowBook(int bookId, int readerId) throws Exception { String checkSql = "SELECT available_stock FROM book WHERE id = ? FOR UPDATE"; String insertSql = "INSERT INTO borrow_record(book_id, reader_id, due_time) " + "VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 30 DAY))"; String updateSql = "UPDATE book SET available_stock = available_stock - 1 " + "WHERE id = ? AND available_stock > 0"; Connection conn = null; try { conn = DbUtil.getConnection(); conn.setAutoCommit(false); PreparedStatement checkPs = conn.prepareStatement(checkSql); checkPs.setInt(1, bookId); ResultSet rs = checkPs.executeQuery(); if (!rs.next() || rs.getInt("available_stock") <= 0) { throw new RuntimeException("库存不足,无法借出"); } PreparedStatement insertPs = conn.prepareStatement(insertSql); insertPs.setInt(1, bookId); insertPs.setInt(2, readerId); insertPs.executeUpdate(); PreparedStatement updatePs = conn.prepareStatement(updateSql); updatePs.setInt(1, bookId); int rows = updatePs.executeUpdate(); if (rows != 1) { throw new RuntimeException("库存扣减失败,可能已被并发借出"); } conn.commit(); } catch (Exception e) { if (conn != null) { conn.rollback(); } throw e; } finally { if (conn != null) { conn.setAutoCommit(true); conn.close(); } } }这段代码有三个设计点值得展开。第一,我手动把autoCommit关掉,让两条 SQL 同生共死,任何一个失败都能回滚。第二,SELECT ... FOR UPDATE是对图书行加锁,阻止两个请求同时读到同一个库存数值,这是解决“超借”的关键。第三,UPDATE语句里带了available_stock > 0条件,就算并发穿透了第一层检查,数据库层面也会挡住负数库存。
有同学会问:为什么不用 try-with-resources 自动关连接?因为 try-with-resources 会在代码块结束时自动关闭连接,而我们的commit/rollback必须在连接存活时执行。如果按 try-with-resources 写,事务控制就失控了。正确姿势就是手动管理Connection的声明周期,把它放在最外层,finally 里再关。
实际项目中,这类事务操作还可以用@Transactional注解(Spring 容器环境),但注解不改变底层的逻辑,面试时被问到事务传播行为,你仍然要能说出“连接、事务边界、回滚时机”这三件事。
3.3 图书检索的 DAO 写法:LEFT JOIN 和 LIMIT 是标配
借书之前总得先查到哪些书能借,所以 DAO 层有一个高频方法叫searchBooks。下面这段代码是一个完整的 JDBC 查询链路,包含了 PreparedStatement、参数绑定、ResultSet 到实体对象的映射:
public List<Book> searchBooks(String keyword) throws SQLException { String sql = "SELECT b.id, b.title, b.author, b.available_stock, " + "c.name AS category_name " + "FROM book b " + "LEFT JOIN category c ON b.category_id = c.id " + "WHERE b.title LIKE ? OR b.author LIKE ? " + "ORDER BY b.id DESC " + "LIMIT 20"; List<Book> list = new ArrayList<>(); try (Connection conn = DbUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { String like = "%" + keyword + "%"; ps.setString(1, like); ps.setString(2, like); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Book book = new Book(); book.setId(rs.getInt("id")); book.setTitle(rs.getString("title")); book.setAuthor(rs.getString("author")); book.setAvailableStock(rs.getInt("available_stock")); book.setCategoryName(rs.getString("category_name")); list.add(book); } } } return list; }这个查询用到了LEFT JOIN,因为有的图书可能没有分类,或者分类被删除了,这时候我们仍然想让它显示在结果集里。LIKE '%关键字%'会导致索引失效,但如果数据库里只有几百本测试数据,全表扫描也没问题,不用为了几万行数据纠结。真正要注意的是LIMIT 20,它防止一次查询把内存打爆,尤其是你在做 Web 接口时,别人手动构造一个空字符串,你可能把整张表都捞出来了。
PreparedStatement 的另一个作用是预防 SQL 注入。这里的参数全部是通过setString绑定的,而不是直接拼字符串。哪怕是图书系统这种内部系统,也千万不要用字符串拼接 SQL,这是红线。
4. 数据库脚本落地时的 5 个避坑记录:从乱码到外键约束失败
4.1 整库导入报错:Unknown database 'library_system'
现象:在 MySQL 里执行完整脚本,前面的建表语句跑完了,导入借阅记录表时报Unknown database 'library_system'。原因很可能是:你拿到的脚本里只有CREATE TABLE没有CREATE DATABASE,而当前连接默认的库是另一个名字,或者压根没选中库。解决:先手动建库,或者找一个真正完整的脚本文件;如果脚本里已经带了CREATE DATABASE和USE语句,导入时不要把命令后面的库名写错。
我自己的习惯是:脚本第一行永远是CREATE DATABASE IF NOT EXISTS,IF NOT EXISTS很关键,它允许你反复执行脚本而不报错。如果用可视化工具,导入前也要留意光标选中的连接位置,别把一个带USE的脚本对着服务器根节点执行,这样容易把库建在错误位置。
4.2 中文乱码:为什么表里全是问号
现象:用命令行source导入脚本后,SELECT * FROM book查出来中文全是???,但用 Navicat 看又是正常的。原因有两个层面:表结构字符集如果设成了latin1,那神仙也救不回来;还有一种可能是命令行客户端连接时的字符集和表字符集不一致,导入时数据被错误转码。解决:建表时统一用utf8mb4,导入命令加上字符集参数:
mysql --default-character-set=utf8mb4 -uroot -p < library_schema.sql如果是在 IDEA 里执行,可以在 Database 控制台的连接属性里加上characterEncoding=utf8。事前定好字符集比事后转换容易得多,这是我从乱的表里捞中文数据的血泪经验。
4.3 外键约束失败:Cannot add or update a child row
现象:往borrow_record插入一条记录,报Cannot add or update a child row: a foreign key constraint fails。原因:插入的book_id在book表里不存在,或者reader_id在reader表里不存在。这种情况在手工测试时特别常见,比如你先填了一张书 ID=10,但图书表最大 ID 才到 9。解决:插入前先查这两张主表,确认 ID 真实存在。也可以用一条 SQL 批量检查无效数据:
SELECT br.* FROM borrow_record br LEFT JOIN book b ON br.book_id = b.id LEFT JOIN reader r ON br.reader_id = r.id WHERE b.id IS NULL OR r.id IS NULL;如果业务上允许临时跳过外键检查,可以使用SET FOREIGN_KEY_CHECKS=0,但这是一个非常危险的开关。我建议只在初始化测试数据时用,正常业务不要开。
4.4 脚本导入后少了一堆对象:没有存储过程、触发器或测试数据
现象:从同事那拷来一份full.sql,导完之后表结构齐全,但翻遍整个库找不到任何存储过程、触发器,连初始数据都只有半截。原因:他导出时用的是只导出结构的选项,或者使用了mysqldump -d这样的参数,-d代表 only data? 这里容易混淆,真正的只导出结构参数是--no-data,而只导出数据不导出结构的是--no-create-info。很多导出工具默认只导结构,所以要拿到动作齐全的脚本,导出时要把存储过程、触发器、事件、数据都勾上。
在命令行下,完整导出一个库的推荐写法是:
mysqldump -uroot -p --databases library_system \ --routines --triggers --events \ --set-gtid-purged=OFF \ > library_full.sql其中--routines导出存储过程和函数,--triggers导出触发器,--events导出事件。这个参数组合是项目交接时我最常用的一套。--set-gtid-purged=OFF是为了在本地环境导入时避免 GTID 相关报错。
4.5 Java 连接 MySQL 失败:Communications link failure 和 Access denied
现象:Java 代码启动时报Communications link failure,或者直接抛Access denied for user 'root'@'localhost'。原因有两类。链接失败通常是端口不对、MySQL 没启动、连接串里多写了空格;Access denied 是密码错误或用户权限问题。在 MySQL 8.0 下还有一个隐蔽坑:默认认证插件是caching_sha2_password,如果你的 JDBC 驱动版本太老,会报Public Key Retrieval is not allowed。
解决:先检查 MySQL 是否启动并监听 3306 端口,Linux 用netstat -tunlp | grep 3306,Windows 用任务管理器看服务。然后确认驱动版本,mysql-connector-java 至少 8.0 以上。URL 里再加一个参数allowPublicKeyRetrieval=true,这能解决本地开发时除名认证插件的连接问题。密码和用户权限,我的排查顺序是:先命令行连,再 JDBC 连;命令能连 JDBC 不能连,就检查驱动和 URL;命令都不能连,就去查服务状态和密码。
5. 把项目从“能跑”升级到“能讲”:3 个验证数据正确性的技巧
项目跑起来不难,但面试或答辩时,你总不能只说“我点了借书按钮,库存减了”。我从自己带人的经验里总结了三个验证技巧,按顺序做一遍,你对这套系统的掌控力会明显不一样。
第一个技巧是写一条统计 SQL,验证借阅数据的完整性。比如统计每个未还书读者的数量:
SELECT r.card_no, r.name, COUNT(br.id) AS unreturn_count FROM reader r LEFT JOIN borrow_record br ON r.id = br.reader_id AND br.return_time IS NULL GROUP BY r.id, r.card_no, r.name HAVING unreturn_count > 0 ORDER BY unreturn_count DESC;这条 SQL 能帮你发现脏数据:如果一个读者的未还数量比他在borrow_record里借出的记录还多,说明数据逻辑有问题。我一般会把它作为一个固定脚本放到项目里,每次手动压测之前跑一遍。
第二个技巧是手工模拟并发超借。开两个命令行窗口,同时执行同一个“借书”请求,或者用简单的 JMeter 脚本对同一个bookId发并发请求。跑完之后立刻查看available_stock是否为负数。正常情况下,因为有SELECT ... FOR UPDATE和available_stock > 0双重保护,库存永远不会变成负数。如果变成负数,说明你的事务边界写错了,大概率是UPDATE没加条件,或者把connection在事务中途关掉了。
第三个技巧是面试讲解的准备。Java 面试题里关于这块的经典问法是这样的:你说说借书这功能,怎么防止两个人同时借走同一本书?标准答法是:先在可重复读隔离级别下用行锁FOR UPDATE锁定图书行,然后插入借阅记录,再执行带条件的库存扣减,整个过程在一个事务里,任何一个失败都会回滚。如果你还能顺口说出available_stock > 0是兜底方案、为什么 MySQL 5.7 不信任 CHECK 约束,面试官基本不会再追问下去。
我做完这个项目之后的最大感受是:所谓“完整”不是功能多,而是你得知道自己每一步在干什么。数据库脚本为什么这么写、事务边界为什么画在这里、外键为什么会被阻塞,这些问题比表面跑通更能检验功夫。项目迭代多轮之后,我反而会每隔一段时间把一个新环境从建库到跑通完整来一遍,用最笨的办法确认脚本没有腐化。希望帮到你,让这份“图书管理系统”从一份能跑的项目,变成你真正能讲清楚的技术底气。
本文还有配套的精品资源,点击获取