1. 为什么要把阅读行为、借阅、预定、采购四个模块放进同一个平台
先说一个我接手类似项目时遇到的真实场景:校图书馆管理员手里有三张Excel表,一张是学生借书记录,一张是学生填的纸质预订单,一张是各学院交上来的采购申请。每到月底,他要把三张表的数据手工合并,统计"哪些书借得最多""哪个年级的学生最爱借书""哪些书被预定了很多次但始终没有库存"。这套流程最大的问题不是累,而是数据对不上——借阅记录里显示某本书很热门,但采购Excel里完全没有这本书的申请,因为学生想借的书不在馆藏里时,根本没人主动去登记这个需求。
这个平台要解决的,本质上是两件事:一是把"学生和图书之间的每一次交互"变成结构化数据,二是让借阅、预定、采购三个业务动作在同一条数据链路上自动联动。先说第一件,阅读行为分析没那么玄乎,它分析的就是三类数据:学生借了什么书、借了多久、还了之后有没有再借同类的书。只要把借阅记录的时间字段、图书分类字段、学生年级字段存好,这些指标都能算出来。难的是第二件,很多学生想借某本书时发现已经被借走了,系统让他提交一个预定申请,这个申请攒到一定数量,管理员就应该能看到"这本书有N个人在等",从而触发采购流程。这就是预定和采购联动的基本逻辑。
这个项目适合谁来参考?如果你是正在做JavaWeb课程设计或毕业设计的学生,可以直接把它当完整案例来搭;如果你是刚入职的初级开发,想理解一个业务系统从需求分析到表结构设计再到接口实现的全过程,这篇也能给你一条比较完整的思路。我下面写的内容不是教科书式的架构说明,而是按我自己做这类项目时真实走过的流程来讲的,包括哪些地方我一开始想简单了,哪些地方后来返工了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计:先定出八张核心表,再谈业务逻辑
2.1 基础表和业务表的边界划分
这个项目我建议先别急着写代码,花半天时间把表结构定清楚,后面能少改很多。整个系统我最终拆成了八张核心表,分三类:
基础数据表:学生表(student)、图书表(book)、图书分类表(category)、管理员表(admin)。这四张表是业务发生的前提,学生得先登录系统才能借书,图书得先录入馆藏才能被检索。
业务流转表:借阅记录表(borrow_record)、预定单表(reserve_order)、采购单表(purchase_order)。这三张表是整个平台的核心,每条记录都代表一次完整业务的某个状态。
行为分析表:阅读行为日志表(read_behavior_log)。这张表我一开始没设计,后来做统计时才意识到,光靠借阅记录表算不出"学生打开过哪些图书详情页""检索过什么关键词"这类过程数据,而过程数据恰恰是分析阅读兴趣的重要来源。
2.2 关键表和核心字段的取舍说明
每张表的字段设计,我挑几个容易踩坑的详细说说。
学生表除了学号、姓名、学院、年级、专业之外,我建议一定要加一个status字段,区分正常和禁用状态。为什么?因为学生毕业或者转校后,他的借阅历史还需要保留用于统计分析,但账号不能再登录借书了。如果直接删学生记录,借阅记录表的外键就全断了,统计历史数据时会出现大量空值。用状态位做逻辑删除,是这类业务系统最基础也最实用的做法。
图书表有个细节容易忽略:除了书名、作者、ISBN、出版社、馆藏数量这些基础字段外,一定要单独建一个available_stock字段表示当前可借数量,而不是每次查询时用total_stock减去借出中的记录数来现算。原因很简单,借还操作非常频繁,每次查询都聚合计算很伤数据库,在高并发借书时还会出现超借的问题。一本书总共5本,第6个学生来借时,系统要能立刻判断"可借数已为0"。
借阅记录表是整张表里最重要的,字段设计上有个常见争议:要不要存冗余的图书名称和学生姓名?我的建议是存。虽然通过外键join也能查出书名,但借阅记录是查询频次最高的表,管理员要看"某本书被借过多少次",如果每次都要join图书表,数据量大了之后性能会明显下降。把书名、ISBN、学生姓名、学号这几个字段冗余进去,牺牲了一点存储空间,换来了查询的便利,这笔账是划算的。
预定单表要特别注意预留时间的处理。学生提交预定申请后,管理员确认有书了,需要给学生保留一段时间(比如三天),到期没来取就自动释放,把图书状态改回可借。这个"到期自动释放"的逻辑可以用定时任务扫表实现,也可以在下一次有人查询或借阅这本书时懒判断。我实际开发时选了懒判断,因为定时任务要另外引入调度框架,而懒判断只需要在借阅接口里加一个状态检查,成本更低。
2.3 索引怎么建:从查询场景反推
索引设计别照抄网上的模板,要从你实际的查询语句反推。我用阿里的SQL调优经验来梳理这个项目的查询场景:
学生登录时按学号查询,所以student.student_no要建唯一索引。图书列表页支持按书名模糊搜索和按分类筛选,所以book.category_id建普通索引,book.name如果频繁做LIKE '%关键词%'查询,普通索引也帮不上忙,建议用前缀匹配或者考虑全文索引,这个我在后面踩坑部分会详细说。借阅记录表最常见的查询是"某学生借过哪些书"和"某本书被谁借走了",所以borrow_record.student_id和borrow_record.book_id各建一个索引。预定单表要快速统计"哪本书的预定人数最多",所以reserve_order.book_id加索引。
这里有个反直觉的点:不是说索引建得越多越好。我见过有同学给所有外键字段都建了索引,结果插入数据时索引维护成本高,写入变慢。正确的做法是先写完核心查询SQL,再对着WHERE和ORDER BY子句里的字段建索引,一个查询场景一个索引,宁缺毋滥。
3. 借阅、预定、采购三条流程的状态机设计
3.1 借阅流程的状态流转和代码落地
借阅流程看似简单,实际上状态设计不好很容易出逻辑漏洞。我的借阅记录表状态字段取值是:
| 状态值 | 含义 | 触发动作 |
|---|---|---|
| 0 | 借出中 | 学生提交借书申请,库存够则创建记录 |
| 1 | 已归还 | 学生还书时更新 |
| 2 | 逾期未还 | 管理员手动标记或定时任务扫描到期未还 |
| 3 | 已损坏 | 还书时发现图书 |
