说实话,“悦读圈”这个图书共享项目刚拿到手时,我第一反应是“不就是个借还书管理系统吗”。真正把需求捋完、代码写起来才发现,图书共享比图书买卖要复杂得多——用户把书挂上平台,不是挂商品,而是要约定借期、书况、押金、逾期责任,甚至要处理“书在A手里,B也想读”的排队预约逻辑。这个项目最终做下来,涉及完整的用户端小程序与后台管理、核心的借阅状态机、预约队列和消息提醒机制,技术栈集中在Java与SpringBoot,配套的还有系统设计文档(LW)、调试文档和讲解材料。整个过程既考察了SpringBoot接口开发的熟练度,也强迫我重新思考共享场景下的数据一致性与业务边界。
如果你正准备做同类项目,或者说你想了解一个“看起来简单、做起来有门道”的图书共享小程序是如何从设计落到代码的,这篇文章应该能帮你省下不少弯路。我会从整体方案选型、数据库设计、后端接口实现、小程序联调、部署debug五个方面完整拆解,最后一并列出我在实际操作中踩过的坑和对应的排查方法。
1. 项目整体设计与方案选型
1.1 核心需求拆解:共享场景下的特殊约束
我在动手前先把业务场景完整过了一遍。悦读圈定位的是校园/社区内的图书共享平台,和普通商城最大的区别在于:图书所有权不发生转移,只转移“一段时间内的阅读权”。这就带来了几个很现实的业务问题:
- 一本书可以被多人轮流借阅,但同一时刻只能被一个人持有。
- 书主愿意把书共享出来,但希望知道书在谁手里、什么时候归还、归还时书况如何。
- 热门好书可能有多人想要,必须有一种“排队预约”的机制,避免用户反复刷新碰运气。
- 借阅需要约定归还时间,逾期要有提醒或信用约束,否则共享的信任体系会崩掉。
基于这些约束,我把整个系统定义为三层:用户层(微信小程序,负责登录、浏览、上传图书、发起借阅、归还、评价)、服务层(SpringBoot后端,负责鉴权、业务规则、预约调度、消息通知)、数据层(MySQL保存业务数据,Redis处理热点缓存与借阅并发控制)。
1.2 技术选型的取舍逻辑
技术栈整体选的是SpringBoot 2.7.x + MyBatis-Plus + MySQL 8.0 + Redis + 微信小程序原生框架。这套组合在同类项目中非常主流,但它不是唯一选择,我简单说说我为什么这样定:
- SpringBoot做后端基础框架:自动装配省掉大量配置,社区资料多、问题排查方便,对新手也友好。对于这个体量的项目,SpringMVC那套手动配置反而浪费时间。
- MyBatis-Plus做ORM:单表CRUD基本不用写SQL,分页插件用起来顺手,代码量能缩减三分之一左右。
- MySQL + Redis组合:MySQL存储强一致性的核心业务数据,Redis主要用来做图书热度的缓存,以及在后端解决“同一本书同时被多人借阅”的并发请求——纯靠数据库行锁也能扛,但Redis的分布式锁写起来更优雅,也更适合讲解。
- 小程序端用原生WXML/WXSS/JS:没有引入uni-app或第三方UI库。原因很简单,原生环境稳定,不用处理编译链问题,页面结构清晰,评审讲解时也好展开。如果后续要扩展到支付宝小程序,再考虑迁移到跨端框架也不迟。
还有人问为什么不用Vue或React做后台管理,因为这套项目已经包含了一个基于Vue Element UI的Web管理后台,用于管理员审核图书、管理用户、处理举报。前后端分离,通过接口交互,这也是当前主流项目的标准形态。
1.3 模块边界划分
后台管理端、小程序端是两类客户端,两者调用的是同一套后端API,但权限不同。小程序的用户只是普通用户,后台管理员具备更高的操作权限。所以我额外设计了一套基于JWT的用户身份校验和基于拦截器的权限控制,这保证了同一个接口服务既能服务C端也能服务管理端,同时在设计文档里也有一个清晰的技术亮点可以展开讲解。
2. 数据库设计与核心模型
2.1 核心表结构设计思路
这套项目的表划分我遵循了一个原则:用户、图书是静态基础数据,借阅记录、预约记录是动态业务数据,通知消息属于辅助数据。因此我设计了七张主表,外加两张辅助关联表。
| 表名 | 用途 | 关键字段 |
|---|---|---|
| t_user | 用户信息 | id, openid, nickname, avatar, credit_score, status |
| t_book | 图书信息 | id, owner_id, isbn, title, author, publisher, category_id, cover, description, condition_level, deposit, status |
| t_category | 图书分类 | id, name, sort |
| t_borrow_record | 借阅记录 | id, book_id, borrower_id, owner_id, borrow_time, due_time, return_time, status |
| t_reserve_record | 预约记录 | id, book_id, user_id, create_time, status |
| t_notification | 消息通知 | id, user_id, type, content, is_read, create_time |
| t_feedback / t_report | 反馈与举报 | id, user_id, target_id, content, status |
简单解释一下几个关键决策:
- 用户表不直接存密码,而是存openid,这是微信小程序的唯一标识。密码登录在C端小程序里被我砍掉了,因为微信授权登录的体验更顺畅,也更符合校园共享场景的使用习惯。
- 图书表里冗余存储了借阅人id和当前状态,这一点看起来破坏了范式,但实际查询“书架上哪些书可借”时能极大简化SQL逻辑。我和owner_id一起维护一致性,而不是每次join借阅记录表去算当前状态。
- credit_score字段是信用分。用户逾期还书会扣分,信用过低就不能再借书,这是图书共享平台区别于普通管理系统的关键所在。
2.2 图书状态机与借阅流程设计
图书状态这块我反复推敲过。最终确定的枚举是四个状态码:0可借、1已借出、2预约锁定、3已下架。
这里有个容易被忽略的细节:为什么需要“预约锁定”状态?假设书在用户A手里,B提交了预约。等A归还时,这本书应该被锁定给B,而不是回到可借状态,否则B可能永远抢不过手速更快的C。我用一个定时任务扫描归还记录,到期的书状态自动从“已借出”变为“预约锁定”,并给B发送预约成功通知。B需要在规定时间内确认借阅,否则锁定释放,书重新变为可借。
借阅流程大致如下:
- 用户浏览书架,发起借阅申请。
- 后端校验图书状态为可借,同时校验用户信用分不低于阈值。
- 锁定图书状态,创建借阅记录,记录借出时间和应还时间(默认借期30天)。
- 每天凌晨定时任务扫描借阅记录,把到期未还的记录标记为逾期,给用户推送提醒,并扣减信用分。
- 用户归还时,书主确认书况后,借阅记录状态改为已完成,图书状态回落到可借,若存在预约队列则自动进入预约锁定。
拿一个具体的并发场景来举例:如果同时有10个用户请求借阅同一本书,后端通过分布式锁保证只有一个请求能成功更新图书状态,其余9个返回“手慢了,图书已被借走”。这在代码层面实际上就是一句话的事,但能明显体现设计者对并发安全的理解。
2.3 预约排队机制
预约表的设计稍微特殊一点。除了基本的“哪本书、预约人、创建时间”之外,我加了一个status状态位和queue_rank序号。queue_rank是用来支持“多人预约同一本书”的排队顺序。印象比较深的一次测试,某本专业书有5个用户同时预约,归还后系统按预约先后顺序依次通知,第一个人确认借阅后其余人排名自动前移。为了不写死复杂SQL,预约排名我直接用Redis的ZSet存储,预约人id作为value,预约时间戳作为score,取队列时按score正序取出。
这样设计的直接好处是:预约队列本身是内存数据,读取速度快,插入和排名都不需要反复更新MySQL中的现有记录,数据库的压力小很多。缺点就是Redis一旦重启未持久化会有丢失风险,所以我在Redis里也会定期把当前预约队列快照同步到MySQL表,保证极端情况下可恢复。
3. 后端核心接口实现详解
3.1 登录鉴权链路:JWT与微信身份绑定
小程序端登录是整套系统的第一道门。用户在小程序里点击“微信登录”,前端通过wx.login拿到临时code,传给后端的 /api/auth/login 接口。后端拿这个code去微信的接口换session_key和openid,然后根据openid查询用户表;如果用户不存在,就自动注册一个新账号,而这一步全在接口内部完成,前端无感知。
拿到用户身份后,后端签发一个JWT令牌返回给前端。JWT里我存了几个非敏感字段:uid、role、nickname,过期时间设为7天。小程序端每次请求都在header里带上Authorization: Bearer ,后端通过拦截器统一解析鉴权。
拦截器的实现思路:
public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (StringUtils.hasText(token) && token.startsWith("Bearer ")) { token = token.substring(7); Claims claims = JwtUtil.parseToken(token); // 将用户id放入请求上下文,供后续controller直接使用 request.setAttribute("uid", claims.get("uid")); return true; } // 校验失败,返回401 response.setStatus(401); return false; } }这里有个经验值得分享:拦截器只做身份校验,不做业务校验。比如管理员权限判断,我是通过自定义注解@RequireAdmin配合另一个AOP切面去实现,这样权限逻辑清晰,也方便对不同接口做差异化控制。
3.2 图书上架与借阅核心接口
图书上架接口 /api/book/add 实际是个信息组合:封面图片先上传到七牛云或本地上传目录,接口接收的是图片URL,图书表单字段走JSON。我把“图片上传”和“图书信息提交”拆成了两个独立接口,好处在于用户图片上传失败不必重复填写整个表单,这种细粒度拆解在移动端弱网环境下体验差异非常明显。
借阅接口的并发安全是我重点展示的一块。核心代码逻辑:
@Override @Transactional(rollbackFor = Exception.class) public Result borrowBook(Long uid, Long bookId) { // 先查用户信用分,不合格直接返回 User user = userMapper.selectById(uid); if (user.getCreditScore() < 60) { return Result.error("信用分过低,无法借阅"); } Book book = bookMapper.selectById(bookId); if (book.getStatus() != BookStatus.AVAILABLE) { return Result.error("该图书已被借出或锁定"); } // 乐观锁扣减,防止并发请求同时借到一本书 int rows = bookMapper.updateStatus(bookId, BookStatus.AVAILABLE, BookStatus.BORROWED); if (rows == 0) { return Result.error("手慢了,图书已被借走"); } // 生成借阅记录 BorrowRecord record = new BorrowRecord(); record.setBookId(bookId); record.setBorrowerId(uid); record.setOwnerId(book.getOwnerId()); record.setBorrowTime(new Date()); record.setDueTime(DateUtil.offsetDay(new Date(), 30)); record.setStatus(BorrowStatus.BORROWING); borrowRecordMapper.insert(record); return Result.ok(); }updateStatus这个SQL是带条件的更新:UPDATE t_book SET status = 1 WHERE id = ? AND status = 0。这本质上就是乐观锁思想。实际测试中我拿模拟并发工具开了50个线程同时请求,最终只有1个成功,其余全部返回错误提示,数据也没有出现两份冲突的借阅记录。
3.3 归还流程与书况确认
归还的流程比借阅要更复杂,因为它涉及“借阅人发起归还”和“书主确认归还”两个阶段。为避免扯皮,我设计了归还码机制:借阅人点击“申请归还”,后端生成一个一次性随机码,借阅人把图书当面交给书主时出示这个码,书主在系统里输入码完成确认。
这个设计我非常满意。你在设计文档里可以把它写成一个业务创新点,实际使用中也确实能解决“书到底还没还”的纠纷——借阅人申请归还不代表流程结束,只有书主输入归还码,借阅记录才真正进入已完成状态。
确认归还后的后续动作也自动衔接:图书状态从已借出变成可借(或预约锁定),同时系统检查该书的预约队列,如果有排队的用户,自动发送“预约图书已归还,请在24小时内确认借阅”的通知。
3.4 定时任务与订阅消息推送
到期提醒是图书共享场景下绝不能少的功能。系统采用SpringBoot自带的@Scheduled定时任务,每天凌晨执行两类任务:
- 扫描已借出图书中超过due_time未归还的记录,状态改为逾期,扣减用户信用分5分。
- 扫描预约锁定状态超过24小时未确认的用户,自动解除锁定并将图书重新放回可借状态。
消息推送给用户的方式,微信小程序里叫“订阅消息”。这里有个坑必须提:小程序端需要用户主动授权“允许发送借阅提醒”,所以我在用户首次借阅时就主动引导授权,否则后端发的订阅消息会被微信拦截。我在讲解文档里也会特意强调这一点,因为很多人在联调时发现消息发不出去,多半是忽略了这个前置条件。
4. 小程序端与联调要点
4.1 页面结构与导航设计
小程序端一共有7个主页面:首页书架、图书详情、发布图书、借阅记录、预约记录、我的(个人中心)、消息通知。首页底部用tabBar分成三栏:书架、借阅、我的。发布入口放在首页右上角浮动按钮,一目了然。
书架页面用分类筛选加关键词搜索,顶部有轮播图区域展示平台推荐书目。这里我采用了“分页加载”模式:每页10条,下拉触底自动加载下一页。图书卡片里展示封面缩略图、书名、借阅次数和归属校区,点击后进入详情页。详情页包含图书简介、书主评价、借阅规则和“立即借阅”按钮。
发布图书页相对简单,但需要处理图片上传的预览和提交状态。我建议表单提交加一个disabed状态,防止用户频繁点击产生重复提交。这一点比较基础,但能体现开发的细节意识。
4.2 接口联调与Token处理细节
小程序端没有cookie,所以Token存到了storage里。我在封装request工具时统一做了三件事:自动带上Authorization头、返回401时自动清理本地Token并跳转登录页、所有错误信息统一toast提示。这样业务代码里基本不用写繁琐的错误拦截处理,各页面能保持干净。
字段命名上有个教训:后端Java属性是驼峰命名(borrowTime),如果直接返回给前端就是borrowTime,小程序端直接用没问题。但后台管理端用的Vue项目里普遍会配下划线转驼峰,两边不一致容易导致联调时找不到字段。我最后统一在后端JSON序列化时给所有API返回字段设置了驼峰格式,这个问题才彻底消失。
4.3 小程序端状态管理与刷新机制
小程序端的难点在于页面间数据同步。比如用户在小程序详情页点“立即借阅”,成功之后,返回书架列表,那本书应该显示为“已借出”状态。如果书架页不做任何处理,用户看到的还是“可借”,就会造成误导。
我的方案是:利用小程序页面的onShow钩子,每次页面重新展示时重新请求一次列表数据。这样虽然会增加接口调用次数,但对于图书共享场景,数据流的实时性远比性能重要。实测下来小程序接口响应在200ms以内,用户无感知,而数据永远是最新的。
5. 部署、调试与上线避坑记录
5.1 本地快速启动步骤
这套项目的启动顺序很重要。先启动MySQL和Redis,再启动SpringBoot后端服务,最后启动小程序前端,缺一不可。数据库脚本里包含了建库建表和初始数据,导入到MySQL时注意选择utf8mb4字符集,否则中文会出现乱码。Redis默认端口6379,密码为空,如果本地环境改了配置,记得同步修改application.yml里的redis配置。
启动后端直接用IDE运行主类即可。第一次启动时MyBatis-Plus会自动建表吗?不会。你需要手动执行doc目录下的sql脚本,或者开启spring.sql.init.mode=always让它自动执行。
小程序端用微信开发者工具导入项目目录,修改app.js里的baseUrl为你的后端地址。注意一个问题:本地联调时,小程序开发者工具必须勾选“不校验合法域名”,因为本地后端是http且没有备案域名。
5.2 服务器部署的常规配置
真正上线部署,我采用的是单台云服务器方案:Nginx做反向代理,后端服务以jar包方式后台运行,HTTPS证书绑定域名,用户访问域名时Nginx把请求转发到本机的8080端口。
后端jar包的启动命令:
nohup java -jar yueduquan-server.jar --server.port=8080 --spring.profiles.active=prod > server.log 2>&1 &需要注意application-prod.yml里数据库地址改成云服务器内网地址或公网地址,同时把MySQL的时区显式配置成Asia/Shanghai。这个不配置好,时间字段会差8小时,所有借阅记录和提醒时间都会错位。
微信小程序要求所有请求域名必须是HTTPS,且域名不能带端口号。我一开始图省事直接用IP加端口测试,微信开发者工具里还算顺利,一键上传体验版后完全无法请求,折腾了半天才反应过来是域名白名单的问题。这个坑写出来希望大家绕过去。
5.3 常见问题排查速查表
我从调试阶段里整理出几类出现频率极高的问题,基本都是联调时的经典场景,直接列成表方便对照。
| 现象 | 可能原因 | 处理方案 |
|---|---|---|
| 前端请求后端500,后端日志显示空指针 | 用户实体没查到,返回了null | 登录后先确认数据库用户表有对应openid记录 |
| 图书图片上传成功但列表里不显示 | 静态资源映射没配好 | 检查SpringBoot对upload目录的静态资源映射配置 |
| 小程序请求提示“url not in domain list” | 没配合法域名或域名没备案 | 在微信公众平台配置request合法域名,且域名必须HTTPS |
| 时间字段相差8小时 | JDBC连接串没设置时区 | 在数据库连接URL加serverTimezone=Asia/Shanghai |
| Redis缓存数据总是旧的 | 没有设置过期时间或缓存未更新 | 图书状态变化时主动删除相关缓存,而不是被动等待过期 |
| 订阅消息发送失败 | 用户未授权一次性订阅 | 在用户借阅前弹窗引导订阅消息授权 |
| 借阅并发时出现两条相同记录的借阅单 | 调用借阅接口未走乐观锁更新 | 确认updateStatus的SQL条件里包含原状态字段 |
5.4 讲解与文档编写的重点策略
这个项目除了代码本身,还要配合LW(系统设计文档)和调试文档做评审讲解。文档我建议按这个顺序组织:需求分析、系统设计、数据库设计、接口设计、核心代码说明、系统测试、部署运行。其中“数据库设计”和“核心代码说明”这两章是评审关注最多的部分。
写文档时有个小技巧:每张核心表都配上E-R图和字段说明表,核心接口配上“输入参数、输出参数”表格,这样讲解时可以直接对着PPT讲,逻辑非常顺。调试文档则记录实际联调过程中遇到的问题清单,对应上述排查表,方便答辩时回答老师提问。
我个人特别建议在文档里的“系统测试”部分放一张功能测试用例表,每个用例包含“前置条件、操作步骤、预期结果、实际结果、是否通过”五列,这能让整体项目完整度提升一个档次,看起来更像一个规范的全流程交付项目。
6. 实操过程与核心环节实现
6.1 开发节奏与代码结构组织
如果你是第一次从0开始做这类项目,我建议按“基础骨架 → 用户登录 → 图书CRUD → 借阅归还 → 预约排队 → 消息通知 → 管理后台”的顺序推进。不要一上来就写管理后台,也不要先做界面美化,核心交易链路才是这个项目的地基。
后端代码包结构我保持了比较标准的mvc分层:controller、service、mapper、entity、config、common。common里放统一响应体Result、自定义异常、工具类。config里放拦截器配置、Cors跨域配置、Redis配置。entity对应每张表。
这个过程里最容易忽略的是接口统一返回结构。我做了一个Result 类,包含code、msg、data三个字段,所有接口统一返回它。前端解析就只需要判断code是否等于200,不需要为每个接口单独做异常处理,联调效率翻倍。
6.2 前端页面的代码组织示例
举一个核心页面的代码示例,图书详情页的“立即借阅”按钮逻辑:
async handleBorrow() { // token校验 const token = wx.getStorageSync('token'); if (!token) { wx.navigateTo({ url: '/pages/login/login' }); return; } const res = await request({ url: '/api/borrow/add', method: 'POST', data: { bookId: this.data.bookInfo.id } }); if (res.code === 200) { wx.showToast({ title: '借阅成功', icon: 'success' }); setTimeout(() => { wx.switchTab({ url: '/pages/borrow/borrow' }); }, 1500); } else { wx.showToast({ title: res.msg, icon: 'none' }); } }这里有个细节:请求失败时提示用的是res.msg而非固定文案,这样后端返回“该图书已被借出”,前端就直接展示给用户,不需要再维护一套错误文案映射。
6.3 接口联调的核心环节实录
联调阶段是所有前后端撕裂感最明显的地方。我在做借阅接口对接时就发现,后端返回的dueTime是一个时间戳,前端需要转成“2025-07-15”这样直观的格式,而这个转换逻辑如果放在每个页面单独处理,非常容易出错。后来我在小程序里封装了一个formatTime过滤器,在页面模板里统一调用,所有时间字段显示问题一个函数解决。
还有分类筛选。书架页面的分类筛选是按categoryId查询,但后端接口最初只支持全量列表。后来我调整接口增加了categoryId参数,利用MyBatis-Plus的LambdaQueryWrapper动态拼接条件,选择“全部”时传空值,就不做条件过滤,这样一处改动同时满足筛选和搜索两种需求。
6.4 联调期间值得固化的好习惯
调试这个项目的过程里,我养成了用一个接口调试工具做自动化请求的习惯,把所有核心接口的测试用例保存下来。每次后端改动后,我批量跑一遍,看哪些接口挂了,比在页面里手动点来点去效率高得多。另一个习惯是给重要的业务操作都加上日志输出,包括用户id、图书id、操作类型和结果,排查问题时这些日志能准确告诉你用户走到哪一步卡住了。
还有一点强调一下:前后端字段名一定要提前对齐。项目中出现过前端传的bookId后端接收不到,排查半天发现前端写的属性名是book_id。这种问题纯属沟通成本,建议大家在开发前把接口文档定义清楚,字段名大小写、命名风格统一用驼峰,减少联调期反复修改。
7. 项目扩展方向与个人经验总结
项目做完之后,我复盘了整个开发过程,觉得如果时间充裕,还可以做三处扩展。第一是引入更多维度信用评价,例如用户借阅频率、主动分享图书数量、书主好评率,综合计算动态信用分,让共享机制更公平;第二是增加基于LBS的附近书架功能,用户可以根据地理位置查找周边可借图书,增强同城共享效率;第三是考虑把管理后台的统计报表做成可视化仪表盘,直观展示图书借阅排行、用户活跃度、逾期率等指标,这些数据对运营决策非常有帮助。
最后再分享一个小技巧:如果你要在评审或面试时讲解这个项目,不要从头到尾把模块都讲一遍,时间不够也显得没有重点。建议准备两个深挖点,比如“如何借助Redis实现热门图书借阅的并发控制”和“如何通过状态机设计保证图书状态流转的安全性”。把这两个问题讲透,比泛泛而谈十个功能都要加分。
我在实际开发中还深刻体会到一点:图书共享平台的核心不是“图书”也不是“平台”,而是“共享信任机制”。谁有权限借书、如何保证归还不失信、图书损坏如何定责,这些业务规则最终都会落到一次次状态变更和数据记录上。把规则用代码表达清楚,程序才能承载真实的社区场景。这个项目真正教会我的,不只是SpringBoot和小程序的编码能力,而是如何把一个现实生活中的共享协作诉求,平稳翻译成一个可以落地运行的系统。