做毕设选了个图书借阅系统的题目,乍一看挺普通,但真正动手之后才发现,这类系统的坑全藏在细节里。尤其是“社区公益”这个前缀,它决定了整个系统不能照搬学校图书馆那一套业务模型。我把自己从选题、设计到编码、排错的全过程梳理了一遍,把思路、表结构、核心接口和踩过的坑都放在这篇里,给准备做类似项目的同学一个参考。
整个题目围绕三条线展开:社区公益图书借阅系统、基于Java的共享图书流通平台、智慧社区图书互助服务。拆开看,核心本质是一样的——面向社区场景的图书共享与流通管理系统。它要解决的痛点很实际:居民家里有大量闲置书,放着吃灰,扔掉可惜;社区里想搞图书角,但靠手工登记Excel管理,图书去向不明、借了不还没人管。落到系统上,就是要把“谁有书、书在哪儿、谁借走了、什么时候还、逾期了怎么办”这几件事管清楚。
这个内容适合三类人看:准备拿Java方向做毕设的学生,想快速上手Spring Boot全栈开发的初学者,以及真的想给社区或者单位做图书共享小工具的人。下面我会按照从设计到落地的顺序,把关键环节逐个说透。
1. 先说清楚:这个项目到底在做什么
1.1 从标题拆出真实需求
拿到项目标题,第一件事不是写代码,而是拆标题。“毕设程序java社区公益图书借阅系统设计”这句话里藏着三层信息:技术栈是Java,场景是社区,业务是图书借阅。再往后看“基于Java的社区共享图书流通平台开发”和“智慧社区图书互助服务系统的设计与实现”,又多了两个关键词:共享和互助。
这三者不是简单的同义反复。“共享图书流通平台”强调的是图书在不同用户之间流动,对应的是状态管理、借阅记录、预约续借这一整套机制;“智慧社区图书互助服务”强调的则是社区属性和激励机制——大家愿意把书拿出来共享,系统得让这个共享过程可信任、可持续。所以需求拆到最后就三件事:图书要能管起来、借还要有完整链路、用户要有参与动力。
我见过不少同学拿到类似题目就立刻开始建表写接口,结果写到一半发现业务边界很模糊。比如一本社区图书到底是谁的?用户捐的书是归社区所有还是仍归个人?借书需不需要管理员审批?这些问题不提前定清楚,后面全是返工。我的建议是,在动手前先画一张简单的业务流程图,不需要多专业,哪怕是在草稿纸上画圈都行,把借书、还书、预约、捐赠这条主线走通再写代码。
1.2 社区场景和普通图书馆系统的差别
社区图书借阅系统和传统的图书馆管理系统,表面看都是管书、管借还,但业务模型差别非常大。高校图书馆是中心化模式:有专门的馆员、严格的编目体系、庞大的馆藏、复杂的分类法。社区图书角是什么情况?没有专职管理员,书的数量少则几百多则几千,用户就是小区居民,书的来源大多是邻里捐赠。
这个场景差异直接决定了系统设计不能照搬图书馆那一套。比如图书馆需要MARC编目、索书号定位、跨馆借还这些专业功能,社区系统完全用不上;反过来社区场景有独特的玩法:图书漂流、积分奖励、邻里互助、线下取书点管理,这些是普通图书馆系统没有的。
说得直接一点,社区图书系统不是图书馆系统的“缩小版”,而是“邻里互助版”的图书流通工具。设计的核心逻辑是低门槛、易操作、强信任。低门槛意味着居民捐书、借书的操作必须简单到“三步以内”,易操作意味着管理员上手就要能管,强信任意味着图书去向、借还记录、逾期情况都要清晰可查。想通了这一点,整个技术方案和功能设计就有了主线,后面做的所有决策都可以拿这条主线来校验。
2. 技术选型:没有花架子,全是务实选择
2.1 Spring Boot为什么是首选
在这个项目里选择Spring Boot,最大的理由不是它“新”或者“流行”,而是它的开发效率和生态成熟度正好卡在毕设项目的需求点上。Spring Boot通过自动配置解决了传统Spring项目里大量繁琐的XML配置问题,一个内嵌的Tomcat加一个spring-boot-starter-web依赖,就能把一个Web项目跑起来,这对时间紧凑的毕设来说非常友好。
我见过有些同学在这个题目前纠结要不要上Spring Cloud、要不要拆微服务。我的态度很明确:单机单体加合理的模块分层就够了。图书借阅业务的并发量放在社区场景下极低,峰值也就是几十个人同时用,微服务带来的服务发现、配置中心、分布式事务这些复杂度在这个体量下完全是负资产。用单体架构把业务代码写清楚、分层写标准,在答辩的时候反而更清晰,因为每一层都能讲出让人信服的设计理由。
另外Spring Boot的starter机制让集成变得非常省事。引入mybatis-plus-boot-starter就能用MyBatis Plus实现大部分CRUD;引入spring-boot-starter-validation就能做参数校验;引入spring-boot-starter-aop就能把操作日志这类横切逻辑统一处理。这些不是花架子,是实打实提升编码效率的东西。
2.2 前端、模板引擎与数据存储的搭配
前端方案上常见的路线有两种。一种是使用Thymeleaf模板引擎做传统的服务端渲染,页面直接由后端Controller返回视图,这种方式的优点是结构简单、不需要额外部署前端工程,答辩时讲起来顺,调试也容易;另一种是前后端分离,Vue加Axios调后端REST接口,这种更贴近真实企业项目,但多了一个前端构建步骤,开发周期会长一些。
我给大部分人的建议是:如果你对前端不熟,时间又紧,Thymeleaf加上Bootstrap完全够用,社区管理后台和用户借书页面都能做得像模像样;如果你希望简历上能写“前后端分离项目”,那就上Vue加Element UI,但要想清楚前端路由、状态管理、跨域问题这些额外的坑你有时间去填。两种方案我都会在后面的核心实现里基于后端接口来讲,前端怎么选都不影响后端设计。
数据库没有悬念,选MySQL。理由不是性能,而是资料多、排错方便、面试被问的概率高。存储引擎选InnoDB,因为这项目里的借书、还书、预约操作都涉及事务,InnoDB对事务和行级锁的支持是MyISAM不具备的。字符集选utf8mb4,有备无患,特别是图书书名、读者留言里出现冷门生僻字时不会乱码。
2.3 认证与权限怎么处理
很多同学在认证方案上纠结,是JWT还是Session?我的建议是毕设优先考虑Session加拦截器。Spring Boot内置了对Session的支持,登录成功后把用户对象放进Session,拦截器里校验Session是否存在,逻辑清晰,调试方便,答辩的时候也能完整说清楚整个链路。
角色权限我通常设计成三种:USER是普通用户,可以浏览图书、借阅、预约、查看个人借阅记录;ADMIN是系统管理员,负责图书上架审核、用户管理、借阅异常处理;如果需要更贴合“社区”的概念,还可以扩展一个COMMUNITY_ADMIN,也就是社区级管理员,只能管理本社区的图书。
权限控制在接口层面的实现也不复杂,一个自定义拦截器加上@RequireRole注解,拦截器里从Session里拿到当前用户角色,再判断该角色是否能访问当前接口。这样写的好处是权限逻辑集中,不用在每个Controller里重复写if判断,代码看起来干净,也方便扩展新的角色。
3. 核心功能拆解与设计细节
3.1 图书管理:录入、上架、状态流转
图书的来源主要有三种:居民主动捐赠、管理员统一采购、社区共建活动收集。对应到系统里,就得设计一个“图书录入”的入口以及一套“待上架到在架”的审核流程。居民捐赠的书不能直接进入可借列表,需要管理员确认图书品相和信息无误后才上架,这是唯一需要人工介入的业务环节。
图书的状态是整个系统最核心的字段,我建议用整数枚举来管理,而不是存字符串。常见的状态有:0-在架(AVAILABLE)、1-借出(BORROWED)、2-预约占用(RESERVED)、3-下架(OFF_SHELF)、4-损坏(DAMAGED)、5-丢失(LOST)。为什么状态要设计得这么细?因为图书在流通过程中会经过多个角色的手,任何一个状态分叉处理不当,都会出现“书上显示在架但实际被借走了”的脏数据。
状态流转要严格遵守状态机。比如在架的书被借出,只能由AVAILABLE变成BORROWED;归还时只能由BORROWED变回AVAILABLE,如果存在预约用户,则流转至RESERVED等待通知。不得出现像从DAMAGED直接跳到BORROWED这种非法流转。这块最稳妥的做法是在Service层定义统一的状态流转方法,所有入口都通过这个方法去改状态,禁止在Controller里直接操作状态字段。
3.2 借阅流程:从借到还的全链路设计
借阅流程是系统的心脏,完整链路是这样的:用户搜索图书发现状态为在架,提交借阅请求,系统校验用户资质和图书状态,将图书标记为借出,同时生成一条借阅记录,记录借出时间和应还时间。还书时用户找到管理员,管理员确认图书无损,系统登记归还时间,更新图书状态,完成一次闭环。
借阅周期我建议默认设为30天,到期前三天系统自动发站内信提醒。这里有一个容易被忽略的设计点:应还时间不是简单的当前时间加30天,而是要在生成借阅记录那一刻就计算好并落库,后续所有的逾期判断都基于这个字段,而不是实时计算。这样定时扫描、列表展示、逾期检测都有统一的基准。
还书环节我建议做成“线上登记加线下确认”的模式,也就是说用户点击“申请还书”,这只是发起了一个动作,真正把状态改掉需要管理员确认。这么做虽然在流程上多了一步,但极大避免了用户随手乱点导致的图书状态错误。社区本来就缺专职人员,管理员哪怕每周集中确认一次,也比系统里状态满天飞要好得多。
3.3 预约与续借的设计取舍
预约和续借做不做?我的答案是都做,但要控制好边界,避免做成巨无霸。
预约解决了“书被借走了、我排个队”的需求。设计要点是:只有图书状态为BORROWED时才能发起预约,同一本书同时只能有一个有效预约,还书时如果存在有效预约,图书状态不回到AVAILABLE而是进入RESERVED,系统通知预约用户在一定时间内来取书,比如24小时。超时未取则自动释放预约,图书恢复为在架。
续借则要加上限制条件:一本书最多续借一次,续借天数建议15天;续借前要检查这本书是否有其他用户预约,如果有,续借请求直接拒绝。这个限制条件经常被忽略,但它是保证公平性的关键——书都有人排队等了,你还续借,等于变相插队。
预约和续借虽然逻辑不难,但代码里的判断分支会非常多,条件组合有十几种。我建议把这两个功能的业务判断单独抽成两个方法,每个方法只做一件事,输入是图书和用户的状态,输出是允许或不允许加原因码。这样后续写单元测试都方便,比堆在Controller里强太多。
3.4 积分激励与社区互助
做社区公益系统,冷启动是最大的难题。书从哪里来?人为什么愿意来借?如果系统对用户没有正向反馈,居民把书捐出来之后就没了下文,这个平台很快会沉寂。所以积分激励不是锦上添花,而是系统持久运转的引擎。
积分规则我设计得比较轻量:捐赠一本审核通过的书加50分,按时归还一册加10分,逾期一天扣1分,成功借阅他人图书加5分,完成一次社区图书互助活动加30分。积分的用途可以是兑换社区提供的公益小礼品,也可以用来获得优先预约权。
技术实现上有个容易忽略的细节:积分变更必须走独立的流水表记录,而不是直接改用户积分字段了事。每次加分扣分插入一条积分流水,记录变更值、原因、关联业务ID和时间。为什么要这样?因为积分是用户参与度的核心数据,答辩时能拿出“某月捐书数、借阅数、逾期率”这些统计数据,都依赖流水表。只留一个总数字的话,想溯源就傻了。
4. 数据库设计与关键表结构
4.1 表设计的原则
数据库设计我习惯先找实体,再定义关系,最后补充字段。这个项目的核心实体有:用户、图书、借阅记录、预约记录、分类、社区、积分流水、站内消息。这些实体之间的关系也比较清晰:一个用户可以在多个社区参与活动或绑定一个常驻社区,一本图书属于一个分类,一条借阅记录关联一个用户和一本图书。
关于表字段,有三条原则要遵守。第一,状态类字段全部用int枚举,不存字符串,比varchar存什么“已借出”“可借”这种文本相比,既节省空间又避免脏数据,后期加枚举值成本也低。第二,所有业务表都要有create_time和update_time两个时间字段,这是排查数据问题的救命字段。第三,金额或者积分类字段不要用浮点数,积分用int就行。
外键要不要建?我建议在借用环节保留物理外键,特别是借阅记录表的book_id和user_id。虽然很多互联网项目禁用物理外键,但毕设项目里物理外键能在数据库层面挡掉一些逻辑漏掉的非法数据,也方便答辩时展示表关系。真正到了高并发场景再说不迟。
4.2 核心表结构
用户表,字段如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| username | varchar(50) | 登录账号 |
| password | varchar(100) | 加密后的密码 |
| real_name | varchar(50) | 真实姓名 |
| phone | varchar(20) | 联系电话 |
| community_id | bigint | 所属社区 |
| points | int | 积分余额 |
| status | tinyint | 0-正常 1-封禁 |
| create_time | datetime | 注册时间 |
图书表是核心中的核心:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| isbn | varchar(20) | ISBN号 |
| title | varchar(200) | 书名 |
| author | varchar(100) | 作者 |
| publisher | varchar(100) | 出版社 |
| category_id | bigint | 分类ID |
| owner_user_id | bigint | 捐赠人ID |
| cover_url | varchar(255) | 封面图 |
| description | text | 书籍简介 |
| location | varchar(100) | 存放位置 |
| status | tinyint | 0-在架 1-借出 2-预约 3-下架 4-损坏 5-丢失 |
| version | int | 乐观锁版本号 |
| community_id | bigint | 所属社区 |
| create_time | datetime | 上架时间 |
借阅记录表:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| book_id | bigint | 图书ID |
| user_id | bigint | 借阅人ID |
| borrow_time | datetime | 借出时间 |
| due_time | datetime | 应还时间 |
| return_time | datetime | 实际归还时间 |
| status | tinyint | 0-借出中 1-已归还 2-逾期 |
| renew_count | int | 续借次数 |
| operator_id | bigint | 经办管理员ID |
预约表则重点关注预约状态:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| book_id | bigint | 图书ID |
| user_id | bigint | 预约人ID |
| status | tinyint | 0-等待中 1-已通知取书 2-已取消 3-已兑现 |
| create_time | datetime | 预约时间 |
这套表结构基本能覆盖整个系统的核心业务。要注意的是book表里有version字段,这个是用来做乐观锁的,后面并发控制那节会详细讲。
4.3 事务与并发控制
借书这个动作看起来简单,实际上涉及多个写操作:更新图书状态为借出、插入借阅记录、更新用户积分。这三个操作要么全部成功,要么全部失败,必须包在同一个事务里。Spring的@Transactional注解可以搞定这件事,但要注意事务的边界放在Service层,而不是Controller层,否则可能出现Controller里调了多个Service方法,事务被切成两段的问题。
并发控制是图书系统最容易出bug的地方。试想这个场景:一本在架的书,两个用户同时点了借阅,如果代码是“先查询图书状态,判断为在架,再更新为借出”,那么在高并发下会出现两个请求都读到在架状态,都执行了更新,最后书被借了两次,产生两条借阅记录。这就是典型的丢失更新问题。
解决方式有两种。悲观锁用SELECT FOR UPDATE,对数据行为加锁,简单粗暴但锁持有时间长;乐观锁则在book表加version字段,执行更新时带上version条件,语句是UPDATE book SET status = 1, version = version + 1 WHERE id = #{bookId} AND status = 0 AND version = #{oldVersion}。当返回的影响行数为0时,说明条件不满足,要么书已经被借走,要么版本变了,就让用户重试或者提示“手慢了,书被借走了”。我在项目里推荐用乐观锁,原因很简单:社区系统的并发量根本到不了悲观锁的瓶颈,而乐观锁的实现和讲解都很清晰,答辩时能讲出东西来。
5. 实操过程与核心环节实现
5.1 借书接口实现
借书接口是系统的核心入口,我直接贴一个简化版的实现,把关键逻辑放在这里。
@Transactional(rollbackFor = Exception.class) public BorrowResult borrowBook(BorrowRequest request) { // 1. 校验用户和图书 User user = userMapper.selectById(request.getUserId()); if (user == null || user.getStatus() != 0) { return BorrowResult.fail("用户不存在或已被禁用"); } // 2. 乐观锁更新图书状态,条件必须是状态为在架 Book book = new Book(); book.setId(request.getBookId()); book.setStatus(BookStatus.BORROWED.getCode()); int rows = bookMapper.updateStatusWithVersion(book); if (rows == 0) { return BorrowResult.fail("图书已借出或状态异常"); } // 3. 生成借阅记录,应还日期 = 当前时间 + 30天 BorrowRecord record = new BorrowRecord(); record.setBookId(request.getBookId()); record.setUserId(request.getUserId()); record.setBorrowTime(new Date()); record.setDueTime(DateUtil.offsetDay(new Date(), 30)); record.setStatus(BorrowStatus.BORROWING.getCode()); record.setRenewCount(0); borrowRecordMapper.insert(record); // 4. 积分变动记录 pointsMapper.insert(PointsRecord.buildBorrowPoints(request.getUserId(), record.getId())); return BorrowResult.success(record.getId()); }关键点有两个。第一,乐观锁的更新语句用专门的mapper方法,这个方法的SQL是UPDATE book SET status=#{status}, version=version+1 WHERE id=#{id} AND status=0,这里的status=0是硬编码在SQL里的业务条件,这样该行记录就不会被并发重复借出。第二,借阅记录的生成和图书状态的更新必须在一个事务里,任何一个失败都会回滚,不会出现“书状态改了但没借阅记录”的不一致。
5.2 还书接口实现
还书逻辑比借书更繁琐,因为要动到的数据更多。核心代码如下。
@Transactional(rollbackFor = Exception.class) public ReturnResult returnBook(Long recordId) { // 1. 找到借阅记录,校验状态 BorrowRecord record = borrowRecordMapper.selectById(recordId); if (record == null || record.getStatus() != BorrowStatus.BORROWING.getCode()) { return ReturnResult.fail("借阅记录不存在或已归还"); } // 2. 计算是否逾期 Date now = new Date(); boolean overdue = now.after(record.getDueTime()); int status = overdue ? BorrowStatus.OVERDUE.getCode() : BorrowStatus.RETURNED.getCode(); // 3. 更新记录 record.setReturnTime(now); record.setStatus(status); borrowRecordMapper.updateById(record); // 4. 检查这本书是否有有效的预约 Reservation activeReservation = reservationMapper.selectActiveByBookId(record.getBookId()); int bookStatus = activeReservation != null ? BookStatus.RESERVED.getCode() : BookStatus.AVAILABLE.getCode(); bookMapper.updateStatus(record.getBookId(), bookStatus); // 5. 积分处理,逾期扣分,正常还加分 if (overdue) { pointsMapper.insert(PointsRecord.buildOverduePenalty(record.getUserId(), record.getId())); } else { pointsMapper.insert(PointsRecord.buildReturnPoints(record.getUserId(), record.getId())); } return ReturnResult.success(); }这段里值得注意的点是第4步——还书时不能想当然把书状态改回在架,必须先去查一下这本书有没有人预约。如果没有预约,回在架;如果有,回预约占用状态并通知预约人。这个逻辑是图书流通公平性的关键一环。第5步的积分处理也要注意,逾期和正常归还是两个不同的积分流水类型,这样后期统计逾期率、用户信用分才拿得到数据。
5.3 接口设计注意点
项目里所有的业务接口我都建议统一封装返回体,不要直接返回一个Map或者裸的实体类。统一返回体长这样:ok字段表示操作是否成功,code是业务错误码,message是给用户看的提示信息,data是业务数据。这样做的好处是前端调用时只看code就能判断结果,不用自己解析异常结构,也方便写全局异常处理器统一兜底。
参数校验不要太随意,能用注解校验就用注解校验。比如借书接口的userId和bookId,用@NotNull配合@Positive就能挡掉大部分无效请求。同时要配一个全局异常处理器,用@RestControllerAdvice捕获业务异常、参数校验异常和兜底异常,转换成统一返回体返回给前端。这个处理器能省下大量Controller里try-catch的代码,也让错误信息真正对用户友好,而不是把堆栈直接甩在页面上。
日志这块建议在Service层关键入口打上操作日志,包括借书、还书、预约、续借、上架审核等动作,记录操作人、操作对象和操作结果。日志文件用logback按天滚动,不要全塞在一个文件里。排查问题的时候,日志和时间线对齐能帮你快速还原用户操作过程。
6. 常见问题与排查技巧
6.1 并发借阅同一本书的处理
这是整个系统里我遇到过最典型的问题,也是面试官最爱问的。表象是两个用户几乎同时点了借阅,结果两个人都显示借阅成功,一条书的借阅记录却有两条。原因就是前面的分析了,典型的读改写(read-modify-write)操作没有加并发保护。
排查思路是先看SQL,再看事务。如果你的代码是bookMapper.selectById先查再判断再bookMapper.updateById,那就是典型的覆盖写。解决方式前面提过了,用乐观锁的UPDATE条件更新,并检查影响行数。这里想再提一个细节:MyBatis Plus的updateById不会帮你带上业务状态条件,它会按主键更新所有字段,等于把状态直接覆盖掉。所以必须写一条自定义SQL,把业务条件放进WHERE里,这是很多从CRUD框架入手的新手容易忽略的地方。
6.2 超期图书的处理
逾期是图书共享系统无法避免的问题。系统层面要做的不是避免逾期,而是发现逾期并产生提醒。最直接的方式是写一个定时任务,每天凌晨两点钟跑一次,扫描所有状态为借出中、应还时间小于当前时间的记录,把状态更新为已逾期,同时给用户发一条站内消息提醒还书。
这个任务我用了Spring的@Scheduled注解,cron表达式为0 0 2 * * ?,配合事务处理,一次把当天所有到期数据都更新完。这个方法虽然简单,但要考虑连续几天不运行的情况——定时任务服务重启了怎么办?所以状态更新时用到的判断条件是due_time < now,而不是等于某一天,这样即使漏跑几天,第一次恢复运行时也能把所有过期记录一次性纠正过来。
完整链路里还有个环节是管理员在后台可以看到所有逾期记录,支持按逾期天数排序。这种书要多借多久、谁最爱超期,一目了然。顺着这个数据能扩展出用户信用分的功能,不过毕设做到逾期提醒、逾期列表展示已经够用了。
6.3 图书损坏和丢书处理
社区图书流动过程中,书被弄脏、折页、甚至丢了,都是现实存在的问题。在系统里要给管理员提供两条处理路径。损坏的书,管理员可以先把状态改为损坏,然后和借阅人沟通赔偿或修复方式,还原后再把状态改回在架。丢书的情况,直接把状态改为丢失,同时这条借阅记录要标记为异常关闭,不能再通过正常还书接口操作。
这个模块要小心的地方在于:状态改为损坏或丢失后,图书不能再被借阅,也不能再被预约。所以图书详情页的查询条件要把status为4或5的情况排除在可借列表之外。我在实现时把所有跟“可借”相关的判断都收敛到了一个工具方法里,isBorrowable(book)方法内部统一判断status是否等于AVAILABLE,避免在多个controller里分散判断,到时候改一处漏一处的尴尬局面不可能出现。
6.4 扫码借书的小细节
很多社区系统会加一个扫码借书的功能,用手机扫贴在书上的二维码直接进入图书详情页。这个功能看上去简单,但设计二维码内容时要留个心眼。二维码里应该存的是图书的业务ID,比如bookId,而不是一本书的详情URL。如果直接存URL,一旦部署域名变了,所有印出来的二维码全废掉了。存业务ID则可以在扫码后动态拼接当前域名拼接图书详情路径,灵活得多。我这边实测下来,二维码用QRCode库生成,内容存bookId,扫码后跳到一个中间过渡页,再把用户带到图书详情和借阅确认页,这个链路最稳妥。
还有一个小坑是二维码的容错率。社区里的书被很多人摸过,贴的二维码容易污损,生成二维码时建议把容错级别设置为较高,比如H级,这样就算二维码被划了几道,还是能扫出来。这是个很小的细节,但在实际使用中影响非常大。
走到这里,整个系统的设计从需求拆解到数据库落地,再到几个核心接口的实现和常见问题的排查,算是完整过了一遍。这个题目的难点从来不在单个技术点上,而在于把社区场景下的业务逻辑想周全,把借阅链条上的状态、并发、提醒、积分这些要素串成一条闭合的线。我自己的体会是,编码前把业务规则逐条写清楚,比如“有预约就不能续借”“还书时先查预约再看状态”,这些规则比任何框架技巧都重要。最后再分享一个小建议:做完这个系统,可以试着把管理员端的数据统计做出来,图书借阅排行榜、社区活跃用户榜、逾期率这些页面会让整个项目在答辩时上一个档次,也让这个公益平台真正有“智慧”的感觉。