简介:这份软件工程文档围绕图书管理系统,完整展示了一个典型信息管理系统的开发全过程,适合软件工程课程设计、毕业设计以及需要了解系统开发规范的学习者阅读。内容从需求分析切入,先明确系统目标,再描述图书管理员、读者、系统管理员三类角色的功能需求,并补充大量图书数据存储、多用户并发操作和快速检索等性能要求;设计部分采用三层体系结构,分别处理界面交互、业务逻辑和数据存储,数据库基于关系模型建立,同时结合建模工具绘制用例图、时序图和类图,使系统结构一目了然;实现部分说明编码思路和测试环节,覆盖单元测试、集成测试与系统测试。资源包共1个doc文件,约535KB,内容紧凑完整,方便整体学习。目前已有四百三十九人学习下载。这份文档不仅给出了图书管理系统的工程化拆分方法,还提供了一份规范、完整的软件工程文档范本,读者可借此掌握需求到设计的落地要点,为撰写课程报告或毕业设计说明书提供直接参考。
1. 图书管理系统看着像增删改查,为什么软件工程实践里翻车最多
图书管理系统是软件工程课程里最常见的练手题目,但也是我见过翻车率最高的一个小型系统。很多人把它当成一张图书表加一张借阅记录表,代码写完才发现借书、还书、预约、续借这几个动作相互纠缠:一本书能不能借、超期罚多少钱、预约顺序怎么释放,任何一个环节没想清楚,线上数据就对不上。这篇笔记围绕图书管理系统从需求分析、数据库设计、核心编码到测试部署的完整落地过程,讲清楚每一步该做什么、参数怎么设、坑在哪里。适合正在做课程设计、毕业设计,或者准备接手一个小型管理系统的开发者。
2. 需求分析先把业务规则钉死:用例建模与需求规格说明书
需求分析最容易被跳过。我见过不少团队直接建表写接口,结果做到一半发现“预约的书被别人借走了”“超期罚款对不上”,回头改表结构。核心原因是没有先把业务规则写成任何人都能读懂的条目,直接把模糊的业务描述交到了代码层。图书管理系统的业务规则并不复杂,但数量不少,而且彼此关联,需求阶段漏掉一条,后面就要用一堆补丁代码来偿还。
2.1 用用例建模圈定系统边界:图书借阅的核心用例与扩展用例
用例图不必画得花哨,但要画出边界。图书管理系统常见用例包括:读者注册与登录、按书名/作者/ISBN 检索图书、借书、还书、续借、预约借阅、管理员维护书目与副本、超期罚款处理、借阅统计与排行。这些用例里,借书、还书、续借、预约是核心闭环,其余是支撑功能。
边界这一步的意义在于控制范围蔓延。很多团队在需求阶段把“图书采购”“馆藏盘点”也纳入进来,导致开发范围失控。软件工程里这叫范围蔓延,一个课程设计级别或中小型业务系统,通常不建议纳入采购流程,把借阅闭环做扎实更重要。判断标准很简单:这个功能是不是读者借书必须经过的环节?不是,就先砍掉或标记为二期。
每个用例要写完整描述,而不是一句话。以“借书”为例,我会在需求文档里这样描述:
- 主流程:读者在馆内找到在馆副本,管理员扫描读者证与图书条码,系统校验读者身份与可借数量,校验副本状态为可借,生成借阅记录,副本状态变为已借出。
- 备选流程:读者已超过最大可借数量,拒绝借出并提示;副本状态不是可借,拒绝借出并提示;读者有未缴罚款且超过冻结阈值,冻结借阅权限并提示。
把这些流程写进需求文档后,后续测试用例直接从这些流程派生,不用凭空想。预约用例也要写完整:副本全部借出时读者可提交预约,预约队列按时间排序,有副本归还时按顺序分配给最早预约的读者,预约保留 3 天,逾期未取自动释放并顺延给下一位。这些细节如果不提前写清楚,开发时就会凭感觉实现,测试时又说不清对错。
2.2 需求规格说明书里需要钉死的 6 条业务规则
业务规则是不随界面变化的核心约束,必须在需求阶段明确。我列一份常用清单,可以直接抄进需求规格说明书:
- 借阅数量上限:每位读者最大同时借阅 5 本,此数值需可配置。
- 借期与续借:默认借期 30 天,每本书可续借 1 次,续借后借期从原到期日延后 30 天。
- 超期罚款:按自然日计算,每册每天 0.1 元,不足一天按一天计;还书时自动生成罚款单。
- 预约规则:当某副本全部借出时开放预约,预约成功后保留 3 天,逾期未取自动释放。
- 状态合法性:副本状态只能在“可借—已借出—预约中—下架”之间按合法路径迁移,不允许跳变。
- 数据约束:同一读者不能同时预约同一本书的多个副本;同一副本同一时间只能有一条有效借阅记录。
这 6 条规则写清楚后,代码实现只是翻译工作。重点在于第 5 条状态合法性,它决定了数据库字段设计,也决定了后面状态机怎么写。第 6 条数据约束则直接转换成数据库的唯一索引,是并发场景下的最后一道防线。
这里特别强调超期罚款的取整规则。“不足一天按一天计”意味着超期 1 小时要罚 1 天,超期 2 天 1 小时要罚 3 天。很多团队在这个细节上没有提前约定,开发用“按整天截断”实现,用户则按“不足一天按一天”理解,上线后对账必然出问题。
2.3 把需求转成验收标准:从用例派生出可执行清单
需求规格说明书最后要附验收标准,否则开发结束不知道“做完”的定义。我习惯给关键功能点配上验收条件,做成一张表,直接贴在文档末尾:
| 功能点 | 验收标准 |
|---|---|
| 借书 | 合法读者借阅在馆副本,生成借阅记录,副本状态变为已借出 |
| 还书 | 归还超期副本时自动生成罚款单,副本状态变为可借 |
| 续借 | 每本书仅可续借一次,到期日顺延 30 天 |
| 预约 | 副本全部借出时允许预约,预约保留 3 天后自动释放 |
| 检索 | 按书名模糊检索,10 万条数据量内响应时间小于 500ms |
非功能需求里,并发支撑是图书管理系统最容易被忽视的指标。高校场景下,开学季还书借书集中,接口至少要能承受 30 并发,超过就排队。这个数字不需要拍脑袋,观察图书馆高峰时段一个服务台的办理量就能估出来。把并发指标写进验收标准,开发时才会主动考虑锁和事务,而不是等到上线后被用户投诉“一借书就卡住”。
需求阶段最后还要约定变更流程。图书管理系统需求变更是常态,比如“最大可借数量从 5 改成 9”“罚款从每天 0.1 元涨到 0.2 元”。这些变更如果直接改代码而不回写需求文档,一周后团队内部就会对规则产生分歧。我的做法是:需求文档用版本号管理,任何规则变更必须同步更新文档并通知测试人员,避免测试用例和实际需求脱节。
3. 架构与数据库设计:分层选型、8 张核心表与索引参数
3.1 单体分层架构选型:为什么图书管理系统不需要微服务
图书管理系统的用户规模和业务复杂度决定了它不需要微服务,一个前后端分离的单体应用就是最常见且可靠的方案。前端用 Vue 或 React 都可以,后端主流的组合是 Spring Boot 加 MyBatis Plus,数据库用 MySQL,缓存按需引入 Redis。选型原则是团队最熟的技术栈,而不是最新最热门的技术栈。微服务在这个规模下只会引入服务发现、分布式事务这些额外复杂度,得不偿失。
后端分层依然是经典的三层架构:Controller 负责参数接收与校验,Service 层承载业务规则与事务边界,Mapper 层负责数据访问。领域逻辑不应该写在 Controller 里,也不应该写在 SQL 里。判断标准很简单:一条业务规则如果换一个接口就失效,说明它放错了层。比如“读者超量不能借书”这条规则,必须沉淀在 Service 层,而不是分别写在两个 Controller 方法里。
需要注意的是,事务边界就在 Service 层。图书管理系统的核心操作几乎都是“先校验、再写记录、再改状态”的多步操作,任何一步失败都应该回滚全部操作。三层架构下,事务注解加在 Service 方法上是最自然的做法,Controller 层不处理事务,Mapper 层只负责单表操作。
3.2 数据库设计:从 ER 图到 8 张核心表的落地清单
数据库设计是图书管理系统最见功底的部分。这里有一个常见误区:只建 book 表和 borrow 表,书目信息与副本信息混在一起。真实图书馆里,同一本书会有多个副本,每个副本有独立的借阅状态。因此正确的做法是拆成“书目”和“馆藏副本”两张表。
核心表我一般按下面 8 张来建:
| 表名 | 用途 |
|---|---|
| reader | 读者信息,包含可借数量、罚款状态 |
| admin | 管理员账号 |
| book_info | 书目信息,ISBN、书名、作者、分类 |
| book_copy | 馆藏副本,唯一标识一本书的实体及当前状态 |
| borrow_record | 借阅记录,每次借还一条记录 |
| reservation | 预约记录 |
| fine | 罚款单 |
| category | 图书分类 |
关键的建表语句如下,这里包含了借阅状态和并发控制的核心字段:
CREATE TABLE book_copy ( id BIGINT PRIMARY KEY AUTO_INCREMENT, book_info_id BIGINT NOT NULL COMMENT '所属书目 ID', copy_code VARCHAR(32) NOT NULL COMMENT '馆藏条码,每本书唯一', status TINYINT NOT NULL DEFAULT 0 COMMENT '0-可借 1-已借出 2-预约中 3-下架', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_copy_code (copy_code), KEY idx_book_info (book_info_id), KEY idx_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='馆藏副本表';status 用 TINYINT 而不是 VARCHAR,是因为状态是枚举值,用数字存储可以节省空间,同时配合代码里的枚举类做映射。copy_code 加唯一索引,保证每一本实体书在系统里只有一条记录,也方便扫码时精确定位。借阅状态只挂在 book_copy 表上,book_info 表不存状态,否则一本书有 3 个副本时,书目表的状态字段不知道该表示哪一个副本。
借阅记录表是另一张关键表:
CREATE TABLE borrow_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, reader_id BIGINT NOT NULL COMMENT '读者 ID', book_copy_id BIGINT NOT NULL COMMENT '副本 ID', borrow_date DATE NOT NULL COMMENT '借出日期', due_date DATE NOT NULL COMMENT '应还日期', return_date DATE DEFAULT NULL COMMENT '实际归还日期', status TINYINT NOT NULL DEFAULT 0 COMMENT '0-借阅中 1-已归还 2-续借过', is_active TINYINT NOT NULL DEFAULT 1 COMMENT '1-有效记录 0-历史记录', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_copy_active (book_copy_id, is_active), KEY idx_reader_active (reader_id, is_active), KEY idx_due (due_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='借阅记录表';这里多设计了一个 is_active 字段,配合唯一索引 (book_copy_id, is_active),保证同一副本只能有一条有效借阅记录。这个字段是并发场景下的兜底约束,业务层锁即使被绕过,数据库层也会拒绝第二条有效借阅记录。索引 (reader_id, is_active) 服务“我的借阅”列表查询,(due_date) 服务超期任务扫描。
3.3 索引和字段约束的参数设置:让借阅查询不慢的 3 个关键点
图书管理系统数据量不大,但索引设计仍要规范。我总结 3 个必调参数:
第一,借阅记录表必须建 (reader_id, is_active) 联合索引。读者中心页要查“我借了哪些书”,条件是 reader_id 加 is_active=1,联合索引可以让这个查询走索引而不是全表扫描。
第二,due_date 单独建索引。超期提醒和罚款计算都要按应还日期扫描,比如每天定时任务查 due_date < 今天的有效记录,这个字段没有索引会导致慢查询。
第三,所有时间字段统一用 DATETIME,罚款金额用 DECIMAL(10,2),不要用 FLOAT。FLOAT 在金额累加时会出现精度误差,做罚款合计时特别容易翻车。字符集统一 utf8mb4,排序规则用 utf8mb4_unicode_ci,避免中文检索乱码。
还有一个容易被忽略的参数是数据库连接池大小。默认配置通常够用,但高并发场景下连接池太小会导致请求排队,连接池太大会拖垮数据库。一般小型系统设置在 20 到 50 之间,配合压测结果调整即可。
4. 核心模块编码:借阅状态机、超期计算与并发控制
4.1 借书流程的状态机实现:三段式代码骨架
先把状态机模型定下来。副本状态有四种:可借、已借出、预约中、下架。合法的状态迁移路径是:
- 可借 → 已借出(借书)
- 可借 → 预约中(预约)
- 预约中 → 已借出(预约取书)
- 预约中 → 可借(预约过期释放)
- 已借出 → 可借(还书)
- 已借出 → 下架、可借 → 下架(管理员操作,需先处理未还记录)
代码实现上,我习惯在 Service 方法里做状态校验,而不是在每个接口里散落 if 判断。借书的最小代码骨架如下:
@Transactional public void borrowBook(Long readerId, Long bookCopyId) { // 1. 校验读者:存在、未冻结、未超量 Reader reader = readerMapper.selectById(readerId); if (reader == null || reader.getStatus() != 0) { throw new BusinessException("读者不存在或已被冻结"); } int activeCount = borrowRecordMapper.countActiveByReader(readerId); if (activeCount >= MAX_BORROW_COUNT) { throw new BusinessException("已达到最大借阅数量"); } // 2. 锁定副本行并校验状态 BookCopy copy = bookCopyMapper.selectByIdForUpdate(bookCopyId); if (copy.getStatus() != COPY_STATUS_AVAILABLE) { throw new BusinessException("该副本当前不可借"); } // 3. 生成借阅记录,修改副本状态 LocalDate now = LocalDate.now(); borrowRecordMapper.insert(new BorrowRecord(readerId, bookCopyId, now, now.plusDays(30))); bookCopyMapper.updateStatus(bookCopyId, COPY_STATUS_BORROWED); }这个方法有三个关键参数和顺序不能乱:selectByIdForUpdate 是悲观锁,锁住 book_copy 这一行,防止两个请求同时读到“可借”状态;@Transactional 保证第三步的两步操作要么都成功,要么都失败;顺序上必须先写借阅记录再改副本状态,方便排查问题时追踪数据变化。读者校验放在事务内,是因为读者被冻结的状态可能在事务执行期间发生变化,事务外读到的旧值不可信。
为什么这里用悲观锁而不是乐观锁?借书操作冲突概率高,同一副本在高峰期几乎同时被多个管理员扫描,乐观锁会导致大量请求失败重试,用户体验差。悲观锁的代价是持有锁期间其他事务等待,但图书管理系统的单次借书事务执行时间在毫秒级,锁竞争几乎可以忽略。
4.2 还书与续借:超期罚款的原子计算
还书比借书多一个超期罚款计算。核心逻辑是:根据应还日期与当前日期计算超期天数,超期则生成罚款单,同时更新副本状态和借阅记录。最容易踩的坑是“先更新状态后算罚款”,一旦罚款计算报错,书已经还掉了,状态和数据就对不上。正确做法是把罚款计算放到更新状态之前:
@Transactional public void returnBook(Long borrowRecordId) { // 1. 查询借阅记录并锁定,防止重复归还 BorrowRecord record = borrowRecordMapper.selectByIdForUpdate(borrowRecordId); if (record == null || record.getStatus() != BORROW_STATUS_ACTIVE) { throw new BusinessException("借阅记录不存在或已归还"); } // 2. 计算超期天数并生成罚款单 LocalDate today = LocalDate.now(); long overdueDays = ChronoUnit.DAYS.between(record.getDueDate(), today); if (overdueDays > 0) { BigDecimal fineAmount = BigDecimal.valueOf(overdueDays) .multiply(FINE_PER_DAY); fineMapper.insert(new Fine(record.getReaderId(), record.getId(), fineAmount, FineStatus.UNPAID)); } // 3. 更新借阅记录与副本状态 record.setReturnDate(today); record.setStatus(BORROW_STATUS_RETURNED); record.setActive(Boolean.FALSE); borrowRecordMapper.updateById(record); bookCopyMapper.updateStatus(record.getBookCopyId(), COPY_STATUS_AVAILABLE); }超期天数计算用 ChronoUnit.DAYS.between,这是按自然日计算,跨月、跨年都不会出错。罚款金额用 BigDecimal 乘以每天罚款金额,避免浮点误差。罚款单生成后必须在同一次事务里提交,否则还书成功但罚款丢失,账目对不上。罚款单本身有 UNPAID 和 PAID 两种状态,用户缴纳后更新状态,逾期未缴的读者在 2.2 节第 6 条规则里处理。
续借的实现相对简单,要点是只在到期前允许续借,每本书只能续借一次。实现时给 borrow_record 加一个 renew_count 字段,每次续借把 due_date 往后推 30 天,renew_count 加 1。续借后借阅记录保持“借阅中”状态,只是日期变了。不要在续借时新建一条记录,否则统计借阅次数会虚高,状态机也会乱。
4.3 并发控制:同一本书被同时借出时不超卖
并发问题是图书管理系统最容易被测试忽略、上线后被投诉的环节。两个管理员同时扫描同一个副本条码,如果代码是先查状态再判断,两个请求都可能看到“可借”,然后都写借阅记录,书就超卖了。解决方案在 4.1 已经埋下:selectByIdForUpdate 行级锁。只要所有写操作都走同一把锁,并发就不会超卖。
悲观锁和乐观锁在这个场景的取舍可以看这张对比:
| 方案 | 实现方式 | 适用场景 | 风险 |
|---|---|---|---|
| 悲观锁 | select ... for update | 冲突概率高 | 锁等待,需要控制事务时间 |
| 乐观锁 | version 字段或状态条件更新 | 冲突概率低 | 冲突时需重试或直接失败 |
图书管理系统的借书、还书是高频操作,冲突概率不比秒杀低多少,所以我选择悲观锁。但要注意两个边界:第一,锁必须在事务内才有效,事务提交后锁才释放;第二,必须先锁后读,即直接按主键 selectByIdForUpdate,如果先普通查询再锁,两个请求可能都读到旧状态。借阅记录表上的唯一索引 (book_copy_id, is_active) 是兜底方案,即使业务层锁被绕过,数据库层也会拒绝第二条有效借阅记录,双保险缺一不可。
5. 避坑排查:图书管理系统最容易翻车的 5 个问题点
这一章把我在多个项目里常见的线上问题整理成清单,每条按现象、原因、解决三个部分来写。
5.1 现象:一本书显示“在馆”,却始终借不出去
原因:书目表 book_info 和副本表 book_copy 的状态混用了。常见做法是在 book_info 上放一个 status 字段表示“在馆/借出”,但一本书有多个副本时,这个字段不知道该表示哪个副本的状态。结果就是一本明明有在馆副本的书,因为书目状态显示“已借出”而被拒绝借阅。
解决:借阅状态只允许出现在 book_copy 表,book_info 表不设计状态字段。前端展示“可借/馆藏”时,用 count(book_copy where status=0) 实时计算。排查时可以执行这条 SQL,找出状态异常的副本:
-- 找出状态为已借出,但没有对应有效借阅记录的副本 SELECT bc.id, bc.copy_code, bc.status FROM book_copy bc LEFT JOIN borrow_record br ON br.book_copy_id = bc.id AND br.is_active = 1 WHERE bc.status = 1 AND br.id IS NULL;这条 SQL 是检查数据一致性的利器,上线前跑一遍,能发现大量历史数据问题。
5.2 现象:并发借阅时同一副本被借出两次
原因:典型的 check-then-act 问题。代码先查副本状态,判断可借后再插入借阅记录,两步之间没有加锁。两个请求同时查到可借状态,同时插入记录,数据库层也没有唯一索引兜底。
解决:借书方法加上行级锁 selectByIdForUpdate,锁在事务内生效。同时在借阅记录表加唯一索引 (book_copy_id, is_active),保证同一副本只能有一条有效借阅记录。验证并发是否生效,可以用脚本对同一副本发起 20 个并发借书请求,成功数必须为 1,其余 19 个必须被拒绝。
5.3 现象:事务注解不生效,方法报错后数据只提交了一半
原因:常见的三个原因——@Transactional 加在 private 方法上不生效;同类内部方法自调用时绕过代理;方法内部把异常 catch 掉没有往外抛。Spring 的事务是通过代理实现的,自调用走的是 this 方法而不是代理对象,注解形同虚设。
解决:事务注解加在 public Service 方法上;自调用场景拆成两个类或注入自身代理;业务异常一律抛出去,让事务感知并回滚。排查时可以开启 SQL 日志,观察异常发生后是否有 rollback 日志:
# application.yml 中开启 MyBatis 日志 logging.level.com.example.mapper=DEBUG借书方法里如果 catch 了异常并打印日志后返回成功,事务就会提交,数据必然半成品。记住一个原则:事务方法里不要自己吞异常,让 Spring 统一处理回滚。
5.4 现象:超期天数计算错误,跨月跨年对不上账
原因:用 java.util.Date 相减再除以 86400000 毫秒,闰年和夏令时会引入误差;或者用 SQL 的 DATEDIFF 但传入参数时精度丢失。更隐蔽的坑是需求里“超期不足一天按一天算”没有落实,导致 1.5 天被算成 1 天。
解决:统一用 LocalDate 和 ChronoUnit.DAYS.between 计算自然日,罚款金额用 BigDecimal 计算。“按天取整”规则建议统一向上取整,超期 1 天整算 1 天,超期 1 天 1 小时算 2 天。排查超期数据时,直接对比借阅记录里的 due_date 和 return_date,用 SQL 验证:
SELECT id, due_date, return_date, DATEDIFF(return_date, due_date) AS overdue_days FROM borrow_record WHERE return_date IS NOT NULL AND return_date > due_date;把这条 SQL 的结果和 fine 表里的金额逐一核对,能快速定位是计算逻辑问题还是取整规则问题。
5.5 现象:预约功能出现死锁,事务超时回滚
原因:多表更新的顺序不一致。一个事务先更新 reservation 再更新 book_copy,另一个事务先更新 book_copy 再更新 reservation,两个事务互相等对方释放锁,数据库检测到死锁后回滚一个事务。预约取书和预约释放这两个流程最容易踩到这个坑。
解决:约定所有事务按同一个顺序更新表,比如全部先更新 book_copy 再更新 reservation,避免交叉持锁。同时设置合理的事务超时时间,让异常事务快速失败而不是无限等待:
// 设置事务超时时间为 5 秒,防止异常场景长时间占用连接 @Transactional(timeout = 5) public void takeReservedBook(Long reservationId) { ... }死锁发生时数据库会报 Deadlock found 错误,应用日志里能看到。排查方法是把所有写操作的表顺序列出来,凡是顺序不一致的事务,统一调整。
6. 测试与上线验证:最小可用的自动化用例与冒烟清单
6.1 核心业务流程的单元测试用例
图书管理系统的核心逻辑是借书、还书、续借、预约,测试用例要覆盖主流程和关键异常分支。JUnit 就能完成大部分工作,我一般会给借书接口写这样一组用例:正常借书成功、副本已借出时失败、读者超量时失败、读者被冻结时失败。示例:
@Test @Transactional public void borrow_whenCopyAvailable_shouldSucceed() { // 准备:一个可借副本、一个有效读者 Long readerId = insertReader("test-reader"); Long copyId = insertAvailableCopy(1L); // 执行 borrowService.borrowBook(readerId, copyId); // 断言:副本状态变为已借出,借阅记录存在 assertEquals(1, copyMapper.selectById(copyId).getStatus()); assertEquals(1, borrowRecordMapper.countActiveByReader(readerId)); }写测试用例时注意两个原则:每个用例用 @Transactional 注解,测试结束自动回滚,不污染数据库;测试数据用构造方法插入而不是依赖数据库初始数据,否则换环境就跑不起来。核心用例不需要多,覆盖主流程加 3 个关键异常分支,就已经能拦住大部分回归问题。
6.2 上线的冒烟验证清单
部署完成后,冒烟测试直接用真实操作走一遍核心链路。我常用的清单是:读者登录 → 检索图书 → 借书 → 再次检索发现库存减一 → 还书 → 查询罚款为 0 → 重复借同一本书被拒绝。这七个动作全部通过,系统才可以上线。在这个基础上,再补一个并发模拟,用脚本对同一副本发起 20 个并发借书请求,成功数必须为 1,这是验证锁是否生效的最直接手段。
图书管理系统这类项目,最容易在需求阶段偷懒、在上线前慌张。我自己的习惯是:动手写代码前先把业务规则逐条过一遍,画出状态迁移表,再进数据库设计。规则没理清,后面返工的成本是编码本身的好几倍。希望这篇笔记能帮你在做图书管理系统时少走一段弯路。
本文还有配套的精品资源,点击获取