尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SpringBoot+微信小程序:高校共享图书系统开发全记录

SpringBoot+微信小程序:高校共享图书系统开发全记录 要说高校里的闲置书我见过太多处理方式了毕业季宿舍楼下论斤卖、楼道里堆成山、二手群靠缘分转让。但真正的问题是——书并没有离开校园只是从一个角落换到另一个角落。我做的这个基于SpringBoot和微信小程序的高校共享图书小程序就是想把这些散落的书重新拉回流通轨道。这篇博文不是产品宣传是我从零把整个系统搭起来的完整记录包括数据库怎么设计、借阅状态怎么流转、微信登录怎么对接、联调时踩了哪些坑适合正在做毕设、课程设计或者想在校园里搞一个轻量共享服务的同学参考。1. 为什么做高校共享图书需求梳理与项目定位1.1 高校图书流通的真实痛点做这类项目前先别急着写代码我建议你花几天时间到真实的校园场景里观察。我当时跑了三个校区发现几个非常具体的痛点第一教材迭代速度快。学校里很多课程教材每隔两三年就换版本旧版书在学生手里成了鸡肋扔掉可惜留着占地方。共享的核心不是免费而是让书在正确的人手里发挥价值。第二信息极度分散。现在校园里流传的书单大多是Excel表、QQ群消息、朋友圈图片找一本书要翻几十条聊天记录而且信息会过期——你看到有货去问可能早就被人拿走了。第三借阅过程不透明。线下借书基本靠口头约定还书时间全凭自觉书借出去之后在谁手里、什么状态完全没有记录。这就是我做这个系统时最确定的切入点把谁有书、书在哪、借给了谁、什么时候还这四件事讲清楚。1.2 第一版功能边界做什么不做什么很多同学做这种项目容易犯一个毛病——功能越加越多最后把自己做崩了。我第一版明确砍掉了一堆东西不做在线支付和押金体系。校园场景下信任成本低先跑通借还流程更重要。不做预约排队。一本只能借给一个人先到先得简单直接。不做实时聊天。读书借阅不是高频社交用模板消息通知就够了。不做积分商城。积分机制留到第二版第一版专注核心链路。第一版核心功能就五个微信登录、图书发布、图书检索、借阅申请、状态流转。别看功能少这五块已经覆盖了完整的闭环而且每一块都能讲清楚技术实现对做毕设来说深度足够了。2. 技术选型与整体架构SpringBoot 微信小程序为什么是这套组合2.1 后端选SpringBoot的理由我见过不少团队用Node.js或者Python Flask做小程序后端但我最终还是选了SpringBoot Java。理由很实际一是生态成熟。SpringBoot对MyBatis、Redis、JWT、文件上传这些基础设施的整合基本是开箱即用我不需要自己造轮子。二是类型安全。共享图书的业务里有很多状态枚举、时间边界判断Java的强类型帮我省掉了大量运行时的低级错误。三是部署方便。打一个jar包丢到服务器上就能跑配合Docker也顺手。我用的核心依赖在这里版本按自己习惯锁定就行dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency2.2 小程序端的页面组织与代码结构微信小程序这边我的页面结构是这样拆的pages/index首页图书列表 搜索框 筛选分类。pages/detail图书详情展示书的基本信息、发布者、当前状态、申请借阅按钮。pages/publish发布图书填写书名、作者、ISBN、分类、图片、备注。pages/mine个人中心我发布的、我借到的、我借出的、历史记录。pages/message消息列表借阅申请、状态变更通知。页面之间通过wx.navigateTo跳转全局状态用一个app.js里的globalData保存用户登录态和用户信息。这里我建议你在开始写页面之前先统一设计好接口返回格式不然前端联调时会非常痛苦。2.3 前后端通信与数据格式约定我统一采用了RESTful风格返回结构全局是{ code: 0, msg: success, data: {} }code为0表示成功非0为业务错误码。我定义了几个常用错误码10001参数错误、10002未登录、10003登录态过期、20001图书已被借出、20002不能借自己的书。前端在wx.request的success回调里统一判断code不再层层嵌套处理。还有一个关键约定所有接口的请求都带tokentoken放在请求头Authorization里。登录态问题是这类项目联调时最容易出幺蛾子的地方后面我会单独讲。3. 数据库设计与核心表结构3.1 用户表与微信登录态设计用户表是最容易被低估的一张表。很多新手直接放一个openid字段就完事然后到了要展示发布者昵称、头像的时候才发现查不出来。我的用户表长这样CREATE TABLE t_user ( id bigint(20) NOT NULL AUTO_INCREMENT, openid varchar(64) NOT NULL COMMENT 微信openid, session_key varchar(64) DEFAULT NULL COMMENT 微信session_key, nickname varchar(64) DEFAULT NULL COMMENT 昵称, avatar varchar(255) DEFAULT NULL COMMENT 头像URL, school varchar(64) DEFAULT NULL COMMENT 所在校区, student_no varchar(32) DEFAULT NULL COMMENT 学号可选绑定, status tinyint(4) DEFAULT 1 COMMENT 1正常 0禁用, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;openid必须加唯一索引因为微信登录的核心就是通过code换取openid这是用户的唯一身份标识。session_key按微信规范不能下发到前端只能存在服务端我这里存起来是为了后续可能需要解密手机号等数据实际上第一版只用来换取登录态存不存都行但留个字段更稳妥。3.2 图书表与共享状态设计图书表是业务核心设计时我特别注意了状态字段。一张书从发布到完全退出平台会经过多个状态我用整数存储便于扩展CREATE TABLE t_book ( id bigint(20) NOT NULL AUTO_INCREMENT, title varchar(128) NOT NULL COMMENT 书名, author varchar(64) DEFAULT NULL, publisher varchar(64) DEFAULT NULL, isbn varchar(32) DEFAULT NULL, category varchar(32) DEFAULT NULL COMMENT 分类教材/文学/计算机/其他, cover varchar(255) DEFAULT NULL COMMENT 封面图URL, description varchar(512) DEFAULT NULL COMMENT 备注, owner_id bigint(20) NOT NULL COMMENT 发布者用户ID, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0可借 1已借出 2已下架, borrow_count int(11) DEFAULT 0 COMMENT 累计借阅次数, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_owner (owner_id), KEY idx_category (category), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里要注意一个设计原则图书状态跟借阅记录是两套数据不要混在一起。图书表只反映当前能不能借借阅历史要单独放借阅记录表。这样既方便首页只查status0的可借图书又能完整追溯一本书的流转史。3.3 借阅记录表与状态机设计借阅记录表是整个系统里最容易写崩的部分因为一次借阅会跨越多个阶段。我设计了这样的状态机0待确认借阅申请已提交等待书主确认1借出中书主确认借出当前书在借阅人手里2归还申请借阅人发起还书3已归还书主确认收到4已拒绝书主拒绝了借阅申请5已取消借阅人主动取消CREATE TABLE t_borrow_record ( id bigint(20) NOT NULL AUTO_INCREMENT, book_id bigint(20) NOT NULL, borrower_id bigint(20) NOT NULL COMMENT 借阅人用户ID, owner_id bigint(20) NOT NULL COMMENT 书主用户ID, status tinyint(4) NOT NULL DEFAULT 0, borrow_time datetime DEFAULT NULL COMMENT 实际借出时间, return_time datetime DEFAULT NULL COMMENT 实际归还时间, apply_msg varchar(255) DEFAULT NULL COMMENT 借阅申请留言, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_borrower (borrower_id), KEY idx_owner (owner_id), KEY idx_book (book_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;状态流转的边界条件很考验细节比如只有status0可借的书才能发起借阅借阅人不能是书主本人前端可以禁止后端必须校验同一个用户不能重复申请同一本可借的书已归还之后书自动回到可借状态并且borrow_count加1。这些校验逻辑我全部写在后端Service层用一个专门的BorrowService管理状态流转绝不在Controller里散着写否则后期维护会崩溃。4. SpringBoot后端核心接口实现4.1 微信登录 code2session 全流程微信小程序登录和传统账号密码登录完全不同它的核心是wx.login()拿一个临时code然后后端拿这个code去微信服务器换openid和session_key。我在AuthService里封装了完整流程public LoginResult wxLogin(String code) { // 1. 向微信接口换取 openid String url https://api.weixin.qq.com/sns/jscode2session?appid appId secret appSecret js_code code grant_typeauthorization_code; RestTemplate restTemplate new RestTemplate(); String resp restTemplate.getForObject(url, String.class); JSONObject json JSONObject.parseObject(resp); String openid json.getString(openid); if (StringUtils.isBlank(openid)) { throw new BusinessException(10001, 微信登录失败: json.getString(errmsg)); } // 2. 查库或新建用户 User user userMapper.selectOne( new LambdaQueryWrapperUser().eq(User::getOpenid, openid)); if (user null) { user new User(); user.setOpenid(openid); user.setNickname(微信用户 openid.substring(openid.length() - 4)); user.setStatus(1); userMapper.insert(user); } // 3. 签发JWT String token Jwts.builder() .setSubject(user.getId().toString()) .setExpiration(new Date(System.currentTimeMillis() 7 * 24 * 3600 * 1000L)) .signWith(SignatureAlgorithm.HS256, jwtSecret) .compact(); return new LoginResult(token, user); }注意几个容易踩的坑第一code只能用一次而且5分钟内有效前端如果代码里多次调wx.login第二次拿到的code很可能已经失效。第二生产环境必须把小程序的appid和appsecret放到配置中心或者环境变量不要硬编码在代码里更不要传到前端。第三登录接口被刷的问题其实不严重因为最终会落到用户表查询/创建真正的风险在接口访问频率后面可以加个简单的synchronized或者Redis锁。4.2 图书发布与图片上传图片上传是共享图书项目里的必备功能。小程序端用wx.chooseMedia选图然后wx.uploadFile把文件传给后端。我在后端单独写了一个FileController用上传目录拼接UUID文件名PostMapping(/upload) public R upload(MultipartFile file) { if (file null || file.isEmpty()) { return R.error(10001, 文件不能为空); } String originalName file.getOriginalFilename(); String ext originalName.substring(originalName.lastIndexOf(.)); String filename UUID.randomUUID().toString().replace(-, ) ext; String dirPath uploadDir / DateUtil.format(new Date(), yyyyMMdd); File dir new File(dirPath); if (!dir.exists()) { dir.mkdirs(); } File dest new File(dirPath, filename); file.transferTo(dest); String url /files/ DateUtil.format(new Date(), yyyyMMdd) / filename; return R.ok(url); }生产环境建议把它换成OSS或者云存储因为服务器重启或扩容时本地文件会丢。但做毕设或者校园内网部署本地目录完全够用。小程序端上传要注意一个经典问题wx.uploadFile的name参数必须跟后端MultipartFile的参数名一致比如后端参数叫file前端就要写name: file。我见过太多人在这里卡半小时因为用wx.request的习惯把files放在了请求体里。4.3 借阅/归还流程接口详解借阅核心接口我定义为三个逻辑各归各POST /api/borrow/apply借阅人提交申请。POST /api/borrow/confirm书主确认借出。POST /api/borrow/reject书主拒绝申请。POST /api/borrow/returnApply借阅人发起还书。POST /api/borrow/returnConfirm书主确认归还。每个接口的第一件事都是从Authorization里解析出当前用户ID然后校验操作权限。以 confirm 为例核心逻辑就是状态校验加更新public void confirm(Long borrowRecordId) { BorrowRecord record borrowRecordMapper.selectById(borrowRecordId); if (record null) { throw new BusinessException(10001, 借阅记录不存在); } // 只有借阅记录中的 owner 才能确认 Long currentUserId UserContext.get(); if (!currentUserId.equals(record.getOwnerId())) { throw new BusinessException(20003, 无权限操作); } if (record.getStatus() ! BorrowStatus.APPLY.getCode()) { throw new BusinessException(20004, 当前状态不可确认借出); } // 图书状态必须还是可借防止并发下被借走 Book book bookMapper.selectById(record.getBookId()); if (book.getStatus() ! BookStatus.AVAILABLE.getCode()) { throw new BusinessException(20001, 图书已被借出); } // 更新借阅记录 更新图书状态这里要加事务 record.setStatus(BorrowStatus.BORROWED.getCode()); record.setBorrowTime(new Date()); borrowRecordMapper.updateById(record); book.setStatus(BookStatus.BORROWED.getCode()); bookMapper.updateById(book); // 发模板消息通知借阅人 messageService.sendApplyResult(record); }这里必须强调的是借阅记录状态和图书状态要放在同一个事务里更新。如果不加事务会出现记录已借出但书还显示可借的情况后患无穷。我在Service方法上加了Transactional(rollbackFor Exception.class)同时提醒自己同一本书可能被两个借阅人同时申请虽然申请阶段不锁定书但 confirm 阶段必须再查一次图书状态这就是乐观锁的思路——用状态字段做版本判断。4.4 统一异常处理与登录拦截器这个项目如果不在后端做一个拦截器会让代码里重复出现几十遍从header取token、解析、查用户。我写了一个AuthInterceptor实现HandlerInterceptorpublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(Authorization); if (token null || token.isEmpty()) { throw new BusinessException(10002, 未登录); } try { Claims claims Jwts.parser() .setSigningKey(jwtSecret) .parseClaimsJws(token) .getBody(); Long userId Long.valueOf(claims.getSubject()); UserContext.set(userId); } catch (ExpiredJwtException e) { throw new BusinessException(10003, 登录态过期); } return true; }配合统一异常处理器把BusinessException转换成标准JSON返回再通过WebMvcConfigurer配置拦截器路径排除/api/auth/**和/files/**。这样每个Controller就能只关心业务逻辑不用反复写鉴权判断。5. 小程序端核心页面与交互实现5.1 首页图书列表与搜索的加载更多首页是整个小程序的流量入口用户体验好不好关键看列表加载是否顺畅。我的实现分三步走页面加载时请求第一页数据page1size10使用onReachBottom监听触底加载下一页加载完显示没有更多了。这里有一个小程序特有的细节onReachBottom触发的频率比你想象的快必须用一个loading变量做防抖否则会出现重复请求相同页码的问题。loadBooks(reset) { if (this.data.loading) return; if (reset) { this.setData({ page: 1, bookList: [], hasMore: true }); } if (!this.data.hasMore) return; this.setData({ loading: true }); wx.request({ url: ${app.globalData.baseUrl}/api/book/list, data: { page: this.data.page, size: this.data.pageSize, keyword: this.data.keyword, category: this.data.category }, header: { Authorization: wx.getStorageSync(token) }, success: (res) { if (res.data.code 0) { const list res.data.data.records || []; this.setData({ bookList: this.data.bookList.concat(list), page: this.data.page 1, hasMore: list.length this.data.pageSize, loading: false }); } } }); }搜索这块我推荐用一个简单粗暴的方案后端SQL做LIKE %keyword%匹配书名和作者前端在输入框bindinput里加300ms的防抖。一开始我想上Elasticsearch后来算了一笔账——校内图书总量撑死几千本一张表走索引扫描完全够用没必要为了看起来高级引入一整套搜索引擎。5.2 图书详情页与借阅申请交互图书详情页的核心是状态感知。同一个页面书主视角和借阅人视角看到的东西完全不同status0可借 当前用户是书主显示下架按钮。status0可借 当前用户不是书主显示申请借阅按钮点击弹出留言框。status1已借出不管是谁都只显示书已被借出不显示任何操作按钮。这里在交互层有一个容易被忽略的点小程序详情页之间传参不要传整个对象最好只传bookId在onLoad里通过接口重新拉详情保证数据是最新的。如果你用wx.navigateTo的url拼接一个超大JSON对象URL会因为编码问题直接报错而且拿到的是旧数据。5.3 个人中心的三个Tab个人中心我用了wx.switchTab做了三个子区块我发布的书、我借到的书、我借出的书。这三个区块的后端接口逻辑是一样的只是查询条件不同我发布的where owner_id ? order by create_time desc我借到的查t_borrow_record中borrower_id ?且status in (1,2,3)我借出的查t_borrow_record中owner_id ?且status in (0,1,2,3)页面上每个借阅记录需要展示书的信息这就要做联表查询。我用MyBatis-Plus的TableField(exist false)加一个bookTitle和bookCover的临时字段在Service里手动补全避免了复杂的多表分页查询。简单业务用简单办法这是我对学生项目的真实建议——不要一上来就用复杂的嵌套查询等性能瓶颈出现再重构不迟。5.4 消息通知与状态提醒的做法消息通知这块我第一版没有做WebSocket也没有做订阅消息而是用了一个站内消息表 小程序角标的方案。核心表就一张CREATE TABLE t_message ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL COMMENT 接收人, content varchar(255) NOT NULL COMMENT 消息内容, type tinyint(4) DEFAULT 0 COMMENT 0系统 1借阅, related_record_id bigint(20) DEFAULT NULL, is_read tinyint(1) DEFAULT 0, create_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_user_read (user_id, is_read) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这样做的原因是订阅消息模板需要申请而且用户如果没点过授权消息根本推不出去。站内信虽然笨但逻辑完全可控。如果你做完这个项目还有精力可以再去申请微信的订阅消息模板把confirm和reject这类事件用订阅消息推送给用户体验会更好。6. 实战联调中的典型坑与排查过程6.1 wx.login 获取 openid 失败的定位联调时我第一次碰到的问题是前端拿code请求登录接口返回invalid code。排查链路是这样的先看是不是code过期了。我在前端打日志发现每次进小程序app.js的onLaunch里调一次wx.login但pages/index的onLoad里又调了一次第二次拿的code是新的没错可前端用了第一次的旧code去请求导致后端换openid失败。解法很直接全小程序只用一次wx.login在app.js里登录成功后把token存到globalData和storage所有页面直接用这个token。另外一个隐蔽坑是wx.login的code在小程序开发者工具里偶尔有问题真机调试反而正常遇到这种情况不要慌先换真机试。6.2 图片上传413错误的背后上传大图时后端报413前端显示uploadFile:fail。我第一反应是改SpringBoot的max-file-sizespring: servlet: multipart: max-file-size: 5MB max-request-size: 10MB改完还是报错最后发现是服务器Nginx的client_max_body_size默认只有1M。真实的排错顺序应该是先看中间件Nginx再看应用配置SpringBoot最后看小程序端wx.compressImage是否生效。我最终是在前端上传前压缩图片后端再限制2MB以内双保险省流量也快。6.3 触底加载重复请求与列表闪烁首页触底加载除了防抖问题外还有一个体验层面的坑如果setData用全量替换列表用户滑到中间突然被拉回顶部那种体验非常糟。正确做法是使用concat追加新数据同时用wx.pageScrollTo不改变位置。另外图片懒加载一定要开在image标签上加lazy-load否则几十本书的封面会同时发起请求在校园弱网环境下会白屏很久。6.4 登录态过期与自动续期JWT过期后用户会突然发现所有接口都报10003体验很差。我采用了一个简单的方案token有效期7天在用户每次打开小程序时如果token存在就静默调一个/api/auth/refresh接口刷新有效期。虽然这不是标准的refresh_token双token方案但对于小程序场景足够了——因为小程序本身有冷启动机制每次打开相当于一次重新登录的时机。7. 部署上线与后续扩展7.1 SpringBoot打包与部署要点开发完打包我建议用Maven的package命令打可执行jar部署到服务器上用nohup java -jar xxx.jar启动。注意几个小点数据库连接串不要写死在application.yml用环境变量覆盖静态资源路径用绝对路径并且映射到Nginx跨域问题因为小程序端不存在浏览器的CORS限制所以后端基本不用配跨域但如果你用浏览器调试管理后台就要加上CORS配置。7.2 小程序发布审核的注意事项小程序上传代码后要过微信审核最容易被打回的原因是类目问题。共享图书如果涉及二手交易类目需要提供相关资质。我当时是用工具-信息查询类目过的但具体的类目选择建议你提交前先看微信公众平台最新的规则。另外开发阶段要在开发者工具里勾选不校验合法域名但上线前必须把后端地址换成HTTPS的合法域名不然所有请求都会被拦截。7.3 第二版扩展方向如果第一版跑通并且有真实用户在用我建议这些方向按优先级扩展第一借阅到期提醒利用订阅消息或定时任务超过约定归还时间自动提醒第二校内排行榜,让借出最多的书主获得共享达人标签制造小范围的传播话题第三Book Walk读书角地图把线下的书柜位置做到小程序里配合扫码取书第四基于用户行为的推荐比如按分类、按历史借阅记录推荐相关图书这时候才值得引入更重的检索方案。这套系统的价值不在于技术多炫而在于把一个极度分散的校园需求收敛到一个入口。SpringBoot负责把业务规则讲清楚微信小程序负责把操作门槛降到最低两者配合起来一个学生团队用一两个月就能落地并且真的会有用户愿意用。我做完整套流程最深的体会是共享类系统的难点从来不在CRUD而是在状态流转的严谨性和边界条件的覆盖上把这些想透了比多写十个接口都管用。
返回列表