ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SpringBoot+Vue+小程序三端协同图书系统设计

SpringBoot+Vue+小程序三端协同图书系统设计 简介这是一套基于SpringBootVue微信小程序的全栈式在线阅读与创作社区系统面向Java后端、Vue前端及小程序开发者解决图书管理、多端阅读、创作者孵化与社区互动的一体化需求适用于毕业设计、课程实训或中小型数字出版平台原型开发。资源包共583个文件含247个Java后端业务逻辑与控制器代码、94个Vue组件与页面源码、66个JS工具与API调用脚本、86个SVG图标资源以及SQL建表语句、YML配置、SCSS样式等配套文件整体压缩包仅1.53MB轻量易部署。已有1248人学习下载资源结构清晰含run.bat、package.bat等一键启停脚本.env.development环境配置及.gitignore等工程规范文件开箱即用便于快速理解三层架构协同逻辑、掌握图书CRUD、用户权限分级、社区动态发布等核心功能实现路径。1. 三端协同的图书系统不是简单拼凑而是数据流与权限边界的重新定义你见过一个图书平台后台管理员删掉一本书5秒内小程序端阅读页自动灰显、Vue前台页面弹出“该书已下架”提示、读者收藏夹里对应条目同步消失吗这不是理想状态而是这个 SpringBootVue小程序一体化项目默认达成的实时一致性。它没用 WebSocket 做长连接轮询也没靠 Redis Pub/Sub 强推消息而是通过统一的 RESTful 接口契约 JWT 权限透传 模块化状态管理在三个独立部署的前端中实现了「单点操作、多端响应」。核心不在技术堆砌而在把「图书生命周期」上架→阅读→评论→下架→归档拆解成可复用的领域事件让 SpringBoot 后端成为唯一事实源Vue 和小程序只做状态消费与交互渲染。适合正在做毕业设计、中小出版社数字出版系统升级、或想深入理解跨端权限同步机制的 Java/前端开发者——尤其当你发现 Vue 路由守卫和小程序onShow生命周期里反复校验 token 权限时会意识到这个项目真正解决的是「身份上下文如何穿透三端」这个被多数教程忽略的硬伤。2. 后端架构SpringBoot 多模块分层与图书领域事件驱动设计2.1 模块划分逻辑与pom.xml关键依赖解析项目采用 Maven 多模块结构根目录下可见book-admin后台管理、book-api核心业务 API、book-common工具类与实体、book-web前台 Web四个子模块。这种划分不是为炫技而是为隔离变更风险当社区模块需接入第三方评论 SDK 时只需修改book-api的community子包不影响book-admin的权限校验逻辑。关键依赖如下!-- book-api/pom.xml 片段 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId !-- 注意版本锁定在 2.7.18避免与 Vue 打包后静态资源路径冲突 -- /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdcom.h2database/groupId artifactIdh2/artifactId scoperuntime/scope !-- 开发阶段用 H2 内存库避免 MySQL 配置阻塞启动 -- /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency提示jjwt-api未引入jjwt-impl和jjwt-jackson需手动补全否则Jwts.builder()会抛NoClassDefFoundError。这是package.bat构建失败的常见原因。2.2 图书状态机与领域事件实现图书状态流转草稿→审核中→已上架→已下架→已归档不靠数据库字段硬编码而是通过BookStatusChangeEvent事件驱动。以「下架」操作为例// book-api/src/main/java/com/book/event/BookStatusChangeEvent.java public class BookStatusChangeEvent { private Long bookId; private BookStatus fromStatus; private BookStatus toStatus; private String operator; // 操作人ID用于审计日志 }// book-api/src/main/java/com/book/service/BookService.java Transactional public void changeBookStatus(Long bookId, BookStatus targetStatus, String operator) { Book book bookRepository.findById(bookId).orElseThrow(); BookStatus originalStatus book.getStatus(); // 状态校验禁止从「已归档」直接跳转到「已上架」 if (originalStatus BookStatus.ARCHIVED targetStatus BookStatus.ONLINE) { throw new BusinessException(归档图书不可直接上架请先恢复为草稿); } book.setStatus(targetStatus); bookRepository.save(book); // 发布事件非阻塞 applicationEventPublisher.publishEvent( new BookStatusChangeEvent(bookId, originalStatus, targetStatus, operator) ); }2.2.1 事件监听器的三端适配策略事件监听器不直接调用前端接口而是写入book_event_log表并触发定时任务扫描// 监听器仅记录日志 Component public class BookStatusChangeEventListener implements ApplicationListenerBookStatusChangeEvent { Override public void onApplicationEvent(BookStatusChangeEvent event) { eventLogRepository.save(EventLog.builder() .eventType(BOOK_STATUS_CHANGE) .payload(JsonUtil.toJson(event)) // 序列化为 JSON 字符串 .createdAt(LocalDateTime.now()) .build()); } }Vue 前台和小程序通过/api/v1/events/latest?sincexxx接口拉取增量事件自行解析并更新本地状态。这种设计规避了服务端主动推送的复杂性也避免了小程序因网络限制无法接收回调的问题。2.3 权限体系JWT Payload 中嵌套角色与资源权限映射权限控制粒度精确到「模块操作」如BOOK:DELETE、COMMUNITY:COMMENT。JWT Token 的 Payload 不仅含userId和role还预置了permissions数组{ userId: 1001, role: AUTHOR, permissions: [BOOK:UPLOAD, BOOK:EDIT, COMMUNITY:POST], exp: 1735689600 }Spring Security 配置中PreAuthorize注解直接读取该数组RestController RequestMapping(/api/v1/books) public class BookController { DeleteMapping(/{id}) PreAuthorize(permissionService.hasPermission(authentication, BOOK:DELETE)) public Result deleteBook(PathVariable Long id) { bookService.delete(id); return Result.success(); } }// permissionService 实现 Service public class PermissionService { public boolean hasPermission(Authentication auth, String requiredPermission) { Object permissionsObj auth.getPrincipal(); // 实际为 UserDetails 实现类 if (permissionsObj instanceof JwtUserDetails) { return ((JwtUserDetails) permissionsObj).getPermissions() .contains(requiredPermission); } return false; } }注意JwtUserDetails类必须在book-common模块中定义确保三端共享同一权限模型。若 Vue 前端路由守卫需校验权限直接解析 JWT payload 中的permissions字段即可无需额外请求后端。3. Vue 前台基于 Composition API 的阅读状态管理与 M3U8 视频加载优化3.1 阅读页状态管理useBookReader组合式函数封装Vue 前台阅读模块未使用 Vuex 或 Pinia 全局状态而是为每个阅读页实例创建独立的useBookReaderHook解决多标签页同时阅读时的状态污染问题!-- src/views/reader/BookReader.vue -- script setup import { useBookReader } from /composables/useBookReader import { ref, onMounted } from vue const props defineProps({ bookId: { type: Number, required: true } }) const { currentChapter, currentPage, isLoading, loadChapter, nextPage, prevPage } useBookReader(props.bookId) onMounted(() { loadChapter(1) // 加载第一章 }) /script// src/composables/useBookReader.js import { ref, reactive, onUnmounted } from vue import { getChapterContent } from /api/book export function useBookReader(bookId) { const state reactive({ currentChapter: 1, currentPage: 1, content: , isLoading: false, totalPage: 0 }) const loadChapter async (chapterIndex) { state.isLoading true try { const res await getChapterContent(bookId, chapterIndex) state.content res.data.content state.totalPage Math.ceil(res.data.content.length / 1200) // 按每页1200字符分页 state.currentChapter chapterIndex state.currentPage 1 } finally { state.isLoading false } } const nextPage () { if (state.currentPage state.totalPage) { state.currentPage } } const prevPage () { if (state.currentPage 1) { state.currentPage-- } } // 页面卸载时清理内存 onUnmounted(() { state.content }) return { ...state, loadChapter, nextPage, prevPage } }3.2 M3U8 视频播放vue-video-player与自定义加载提示集成项目支持图书配套音视频如有声书前端采用vue-video-player基于 video.js播放 M3U8 流。关键在于处理 HLS 加载延迟导致的白屏问题!-- src/components/VideoPlayer.vue -- template div classvideo-container video-player refvideoPlayer :optionsplayerOptions readyonPlayerReady erroronPlayerError / div v-ifisLoading classloading-overlay div classspinner/div p正在加载章节音轨.../p /div /div /template script setup import { ref, onMounted, watch } from vue import VideoPlayer from videojs-player/vue const props defineProps({ m3u8Url: { type: String, required: true } }) const videoPlayer ref(null) const isLoading ref(true) const playerOptions { playbackRates: [0.7, 1.0, 1.2, 1.5], sources: [{ withCredentials: false, // 避免跨域预检失败 type: application/x-mpegURL, src: props.m3u8Url }], language: zh-CN, fluid: true, aspectRatio: 16:9, // 关键配置启用 HLS 插件并设置超时 html5: { hls: { overrideNative: true, bandwidth: 0, capLevelToPlayerSize: false, maxBufferLength: 30, maxMaxBufferLength: 60 } } } const onPlayerReady () { isLoading.value false // 监听播放开始事件避免用户误触播放按钮 videoPlayer.value.player.on(play, () { isLoading.value false }) } const onPlayerError (e) { console.error(Video player error:, e) isLoading.value false } // 当 m3u8Url 变更时重置播放器 watch(() props.m3u8Url, (newUrl) { if (videoPlayer.value newUrl) { videoPlayer.value.player.src({ src: newUrl, type: application/x-mpegURL }) videoPlayer.value.player.load() isLoading.value true } }) /script3.2.1 M3U8 跨域与鉴权方案M3U8 文件本身不包含鉴权信息但其内部.ts分片 URL 需携带签名参数。后端生成分片 URL 时注入token和expireshttps://cdn.example.com/chunks/123456789.ts?tokenabc123expires1735689600Vue 前端通过videojs-contrib-hls的beforeRequest钩子动态添加请求头// 在 playerOptions 中追加 html5: { hls: { // ...其他配置 beforeRequest: (options) { // 从 localStorage 读取用户 token const token localStorage.getItem(user_token) if (token options.uri.includes(.ts)) { options.headers { Authorization: Bearer ${token} } } return options } } }提示此方案要求 CDN 支持Authorization请求头透传若使用 Nginx 代理需显式配置proxy_pass_request_headers on;。4. 小程序端微信原生组件与跨端状态同步的实践陷阱4.1 登录态与 JWT 同步wx.logincode2Session的二次封装小程序未直接使用wx.request调用后端/login接口而是通过code2Session获取openid后由后端生成 JWT 并返回给小程序存储// utils/auth.js export const login async () { try { const { code } await wx.login() const res await wx.request({ url: https://api.example.com/auth/wx-login, method: POST, data: { code }, header: { Content-Type: application/json } }) if (res.data.code 200) { // 将 JWT 存入 storage后续请求自动携带 wx.setStorageSync(auth_token, res.data.data.token) return res.data.data } } catch (err) { console.error(Login failed:, err) } }后端WxLoginController实现PostMapping(/wx-login) public Result wxLogin(RequestBody WxLoginRequest request) { // 调用微信接口获取 openid String url https://api.weixin.qq.com/sns/jscode2session? appid wechatConfig.getAppId() secret wechatConfig.getAppSecret() js_code request.getCode() grant_typeauthorization_code; String response restTemplate.getForObject(url, String.class); JSONObject json JSONObject.parseObject(response); if (json.containsKey(openid)) { String openid json.getString(openid); // 根据 openid 查询或创建用户 User user userService.findByOpenid(openid); if (user null) { user userService.createByOpenid(openid); } // 生成 JWT含 openid 和权限 String token jwtUtil.generateToken(user.getId(), user.getPermissions()); return Result.success(token); } return Result.fail(微信登录失败); }4.2 小程序路由与 Vue 前台的标题同步小程序页面标题需动态设置且要与 Vue 前台保持一致如阅读页显示「《三体》第5章」。项目采用「中心化标题管理」策略后端提供/api/v1/books/{id}/title接口返回结构化标题数据小程序在onLoad中调用该接口并调用wx.setNavigationBarTitleVue 前台在mounted钩子中同样调用使用document.title设置。// pages/reader/reader.js Page({ data: { bookTitle: }, onLoad(options) { const { bookId } options this.loadBookTitle(bookId) }, loadBookTitle(bookId) { wx.request({ url: https://api.example.com/api/v1/books/${bookId}/title, success: (res) { if (res.data.code 200) { const titleData res.data.data this.setData({ bookTitle: titleData.chapterName }) wx.setNavigationBarTitle({ title: titleData.fullTitle }) // 如「《三体》第5章红岸基地」 } } }) } })4.2.1 小程序onShow中的权限校验时机小程序onShow生命周期常被误用于权限校验但实际应放在onLoad或onReady中——因为onShow在用户切回小程序时触发此时页面可能已渲染完成校验失败会导致白屏。正确做法是onLoad中发起登录态校验若 token 过期跳转至登录页并清空 storage校验通过后才执行setData渲染。onLoad() { const token wx.getStorageSync(auth_token) if (!token) { wx.navigateTo({ url: /pages/login/login }) return } // 校验 token 有效性调用 /auth/verify 接口 wx.request({ url: https://api.example.com/auth/verify, header: { Authorization: Bearer ${token} }, success: (res) { if (res.data.code ! 200) { wx.removeStorageSync(auth_token) wx.navigateTo({ url: /pages/login/login }) } } }) }5. 构建与部署run.bat与package.bat的环境变量隔离策略5.1 Windows 批处理脚本的环境变量注入机制项目提供的run.bat和package.bat并非简单执行mvn spring-boot:run而是通过.env.development文件注入环境变量实现开发/测试/生产配置分离:: run.bat echo off setlocal enabledelayedexpansion :: 读取 .env.development 并设置环境变量 for /f delims tokens1,* %%a in (type .env.development 2^nul) do ( if not %%b set %%a%%b ) :: 启动 SpringBoot指定 profile mvn spring-boot:run -Dspring-boot.run.profilesdev ^ -Dspring-boot.run.jvmArguments-Dfile.encodingUTF-8 endlocal.env.development文件内容示例SPRING_PROFILES_ACTIVEdev SERVER_PORT8080 DB_URLjdbc:h2:mem:testdb DB_USERNAMEsa DB_PASSWORD REDIS_HOSTlocalhost REDIS_PORT6379 WECHAT_APPIDwx1234567890 WECHAT_APPSECRETabcdefg1234567890注意package.bat中的mvn clean package -Pprod会激活prodprofile此时读取的是.env.production若存在否则 fallback 到application-prod.yml。务必确认src/main/resources下存在对应 profile 配置文件。5.2 Vue 前台构建的路径适配技巧Vue 项目vue.config.js中的关键配置解决打包后静态资源 404 问题// vue.config.js module.exports { // 部署到子路径如 http://example.com/book-web/ publicPath: process.env.NODE_ENV production ? /book-web/ : /, // 代理开发服务器请求到 SpringBoot devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: /api // 保持后端接口前缀一致 } } } }, // 关键配置 outputDir 与 SpringBoot 静态资源路径对齐 outputDir: ../book-web/src/main/resources/static }此配置使npm run build输出的dist目录内容直接覆盖book-web/src/main/resources/staticSpringBoot 启动时即可通过http://localhost:8080/访问 Vue 前台无需额外 Nginx 配置。5.3 小程序构建产物的自动化上传流程项目未提供小程序上传脚本但给出了可复用的upload-wx.miniapp.js位于scripts/目录// scripts/upload-wx.miniapp.js const { execSync } require(child_process) const fs require(fs) // 读取小程序 project.config.json 获取 appid const config JSON.parse(fs.readFileSync(miniprogram/project.config.json)) const appid config.appid // 执行微信开发者工具命令行上传 try { execSync(cli --upload --projectminiprogram --appid${appid} --version1.0.0 --desc自动构建-${new Date().toISOString().slice(0,10)}, { stdio: inherit }) console.log(✅ 小程序上传成功) } catch (error) { console.error(❌ 小程序上传失败:, error.message) }运行前需安装微信开发者工具 CLI 工具并配置环境变量WX_DEVTOOLS_PATH指向安装目录。此脚本可集成进 CI/CD 流程在package.bat执行完毕后自动触发。6. 三端联调验证用curl和weinre定位跨端状态不一致问题6.1 使用curl快速验证 JWT 权限链路当 Vue 前台提示「无权限访问」而小程序正常时优先验证后端权限接口是否一致# 1. 获取 Vue 前台使用的 token从浏览器 Application → Storage → Local Storage TOKENeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... # 2. 直接调用权限校验接口 curl -X GET \ http://localhost:8080/api/v1/auth/verify \ -H Authorization: Bearer $TOKEN \ -H Content-Type: application/json # 3. 检查响应中的 permissions 字段是否包含所需权限 # 若返回 401说明 token 解析失败若返回 200 但 permissions 缺失则检查 JwtUserDetails 构造逻辑6.2 小程序真机调试weinre远程调试配置微信开发者工具的「调试」功能无法查看真机网络请求需借助weinre# 安装 weinre npm install -g weinre # 启动服务监听 8080 端口 weinre --boundHost 0.0.0.0 --httpPort 8080 # 在小程序 app.js 的 onLaunch 中注入调试脚本 App({ onLaunch() { // 真机调试开关仅开发环境 if (process.env.NODE_ENV development) { const script document.createElement(script) script.src http://YOUR_SERVER_IP:8080/target/target-script-min.js#anonymous document.head.appendChild(script) } } })启动后访问http://YOUR_SERVER_IP:8080/client/#anonymous即可远程 inspect 小程序 DOM 和 Console。6.3 三端日志关联通过traceId追踪一次阅读请求项目在book-api的WebMvcConfigurer中注入TraceFilter为每次请求生成唯一traceId并写入日志Component public class TraceFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String traceId UUID.randomUUID().toString().replace(-, ) MDC.put(traceId, traceId) try { chain.doFilter(request, response) } finally { MDC.clear() } } }日志格式配置logback-spring.xmlencoder pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{50} - %msg%n/pattern /encoder当 Vue 前台请求/api/v1/books/123时日志中会出现[traceId]字段在小程序端抓包获取相同请求的traceId再在 SpringBoot 日志中搜索该 ID即可定位三端请求的完整链路快速识别是前端传参错误、网关拦截还是后端逻辑异常。提示若traceId在日志中为空检查MDC.clear()是否过早执行——应在finally块中调用且确保 Filter 链完整。本文还有配套的精品资源点击获取
返回列表