news 2026/10/3 9:10:44

SpringBoot+Vue打造图书阅读与商城一体化系统实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+Vue打造图书阅读与商城一体化系统实战

我自己做线上图书项目已经不是第一次了,SpringBoot加Vue这个技术组合更是用了很多年。之前帮人做过一个纯商城版的图书销售系统,也见过不少同学把阅读器和商城分开做,结果系统上线后发现,用户买完书要去另一个地方看,体验非常割裂。

这个"阳光好书系统",从一开始就不是单纯的图书展示页,而是把在线图书阅读和图书销售放在同一个平台里——用户在书城看到一本感兴趣的电子书,可以试读几章,满意就下单购买,买完直接在阅读器里继续读,管理员在后台维护图书上架、章节发布和订单发货。整个业务闭环在一个系统里跑通。技术选型上,后端用的SpringBoot,前端用的Vue,这两个在目前JavaWeb项目里是出现频率最高的组合。SpringBoot负责提供稳定的接口服务、数据持久化和业务逻辑处理,Vue负责页面的交互和用户体验。

这个项目的定位很明确:面向做Java方向毕设或者个人作品的同学,你需要的是一个能讲清楚、能演示、能扩展的完整系统,而不是一个只会增删改查的演示工程。接下来我会从业务建模、数据库设计、阅读器实现、商城交易、前后端联调几个部分,把做这个系统时踩过的坑和值得注意的细节都梳理一遍。

1. 先把这个系统的业务逻辑理顺

很多人在动手写代码之前,根本说不清楚"阳光好书系统"到底要解决什么问题。这是最致命的一点。如果一个系统的边界都模糊,后面做出来的东西大概率是一个什么都想做、什么都做不透的缝合怪。

1.1 阅读和销售合并,解决的是体验割裂问题

单独做一个图书阅读器,技术上并不难。难的是阅读器里只有免费内容,没有商业闭环,用户读完了就走,平台没有收入、没有留存,作者和出版社也没有动力持续提供内容。

单独做一个图书商城,也不难。但传统商城卖的是实体书,你需要考虑库存、物流、快递单号、退货退款,业务链特别长。而且用户买完实体书之后,和平台的互动基本就断了,下一次回访只能靠促销活动。

阳光好书系统把这两者合并,业务逻辑是这样的:平台维护一批电子书,每本书有简介、封面、价格和试读章节。用户浏览书城,先试读,觉得内容值这个价,就下单购买。订单支付成功后,系统给用户开通这本书的阅读权限。用户打开阅读器,按章节阅读,阅读进度自动保存,下次打开还能接着上次的位置看。

这个模式在国内的知识付费和在线阅读平台里已经跑通了。核心价值不是"既做了阅读器又做了商城",而是"发现、试读、付费、阅读、回访"这个完整用户旅程被打通了。用户不需要跳出平台去其他工具里阅读,运营方也不需要维护两套数据、两套账号体系。

1.2 参与这个系统的三种角色

权限模型在动手建表之前就必须定下来,不然后面加角色字段会让人很痛苦。

阳光好书系统按最小可用原则,我建议只保留两种角色:普通用户和管理员。作者、运营这些角色可以在后期扩展,初期没必要给自己加戏。

普通用户的用例:

  • 注册、登录、修改个人信息
  • 浏览图书列表,按分类筛选,用关键词搜索图书
  • 查看图书详情和目录,试读免费章节
  • 将图书加入购物车,下单购买
  • 在阅读器内阅读已购图书,自动保存阅读进度
  • 查看个人书架和订单列表

管理员的用例:

  • 图书管理:上架、下架、修改价格、维护库存
  • 章节管理:新增章节、设置试读章数、编辑章节内容
  • 订单管理:查看订单列表、处理模拟支付订单、发货
  • 用户管理:禁用账号、重置密码

两种角色用一张user表加一个role字段就能区分,不需要单独建两张用户表。0表示普通用户,1表示管理员。权限控制上,后端拦截器校验接口时判断role,管理端接口统一加/admin前缀,前端再配合Vue Router的路由守卫做页面级隔离。

1.3 为什么是SpringBoot + Vue,而不是其他组合

这个选择不是因为它最先进,而是因为它最适合这个场景。

SpringBoot的优势在于自动配置和生态成熟。图书系统涉及的MyBatis-Plus、JWT、文件上传、分页插件,都有非常成熟的开源方案,起步成本极低。内嵌Tomcat也让打包部署变得简单,一个jar包扔到服务器上就能跑。

Vue的优势在于组件化和响应式。图书商城这类SPA页面需要频繁切换列表、详情、购物车、阅读器,组件化开发可以把这些页面拆成独立模块,阅读状态用Vuex或Pinia管理非常自然。相比传统的JSP模板渲染,Vue的前后端分离开发模式让接口调试清晰很多,前端页面改了不用重启后端。

当然,React也是一套优秀的方案,但如果你是Java方向,团队里大家最熟的前端框架大概率是Vue。中文资料多、上手快、出了坑能搜到答案,这就是它在毕设和中小型项目里胜出的原因。技术选型要选团队能驾驭的,不是选论坛上吹得最火的。

2. 后端骨架:SpringBoot模块划分与数据库设计

2.1 项目目录怎么组织才能不越到后面越乱

很多同学写SpringBoot项目,前期很爽,一旦模块增多,接口、实体、配置全堆在默认包下,后面每加一个功能都想重构。我的建议是:一开始就按下面的结构组织,后面哪怕扩展到二十张表也不会乱。

com.sunnybooks ├── controller // 接口层:UserController、BookController、OrderController ├── service // 业务层:BookService、OrderService、ReadingProgressService ├── mapper // 数据访问层:BookMapper、OrderMapper ├── entity // 数据库实体:User、Book、Chapter、Order ├── dto // 传输对象:BookDTO、OrderDTO、LoginDTO ├── vo // 视图对象:BookDetailVO、ChapterVO ├── config // 配置:WebConfig、MinioConfig、MybatisPlusConfig ├── common // 通用:Result、PageResult、GlobalExceptionHandler └── utils // 工具:JwtUtil、BeanCopyUtils

这里有一个非常关键的实践:前端请求和后端接收数据时,不要直接把数据库实体丢出去。数据库实体包含的字段往往比前端需要的多,比如用户表的密码hash、图书表的状态字段等。用一个独立的DTO层做数据封装,接口只暴露需要的内容。这个习惯看着简单,后期改接口、加权限、做接口文档时你会庆幸当初留了这一层。

2.2 五张核心表的设计细节

阳光好书系统最小可用版本只需要五张表:用户表、图书表、章节表、订单表、阅读进度表。购物车表可以加,也可以放后面再说。我强烈建议第一版就把阅读进度表做进去,因为这是"阅读系统"区别于"普通商城"的核心差异点,也是你答辩时最值得讲的功能之一。

CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, nickname VARCHAR(50), avatar VARCHAR(255), role TINYINT DEFAULT 0 COMMENT '0普通用户 1管理员', status TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE book ( id BIGINT PRIMARY KEY AUTO_INCREMENT, book_name VARCHAR(100) NOT NULL, author VARCHAR(50), cover_url VARCHAR(255), description TEXT, category_id BIGINT, price DECIMAL(10,2) DEFAULT 0.00, stock INT DEFAULT 0, sales_count INT DEFAULT 0, trial_chapters INT DEFAULT 3 COMMENT '免费试读章数', status TINYINT DEFAULT 1 COMMENT '1上架 0下架', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); CREATE TABLE book_chapter ( id BIGINT PRIMARY KEY AUTO_INCREMENT, book_id BIGINT NOT NULL, chapter_name VARCHAR(100), chapter_no INT, content LONGTEXT, is_trial TINYINT DEFAULT 0 COMMENT '1试读章节 0付费章节', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL, book_id BIGINT NOT NULL, book_name VARCHAR(100), total_amount DECIMAL(10,2), status TINYINT DEFAULT 0 COMMENT '0待支付 1已支付 2已完成 3已取消', pay_type VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME, UNIQUE KEY uk_order_no (order_no) ); CREATE TABLE reading_progress ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT, book_id BIGINT, last_chapter_id BIGINT, last_chapter_no INT, read_percent DECIMAL(5,2), update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_user_book (user_id, book_id) );

说一下几个设计细节。

第一,book表和book_chapter表必须分开。章节内容用LONGTEXT 存储,如果把章节直接塞进book表,每次查询图书列表都会把大文本字段带出来,接口响应速度会肉眼可见地变慢。分开之后,列表页只查book表,阅读器才按需查章节内容。

第二,book表里存了trial_chapters,这是为了"试读"功能。试读章节的判定可以直接用chapter_no <= trial_chapters来做,不一定非要在chapter表里维护is_trial字段。但如果你的系统里试读规则比较灵活(比如第一章免费、第五章也免费),那就用is_trial字段更合理。

第三,orders表里冗余了book_name字段。这是刻意为之的。订单产生之后图书可能下架、改价,如果不冗余书名,后期订单列表还要关联查询book表,一旦图书被物理删除,订单历史就查不出买的是什么了。电商系统里订单表冗余商品快照是常规操作。

第四,reading_progress表用了(user_id, book_id)联合唯一索引。为什么不用主键自增id做唯一?因为业务上同一个用户对同一本书只可能有一条进度记录,如果用代码判断先查再更新,并发情况下可能出现两条重复数据。有了唯一索引,直接insert ... on duplicate key update或先delete再insert,数据库层面就保证了唯一性。

2.3 JWT认证与接口权限控制

前后端分离的项目,Session方案不好用,因为前端和后端可能不在同一个域名下,CORS跨域时Cookie的处理比较麻烦。JWT是目前最常见的方案:用户登录成功后,后端签发一个token,前端把token存在localStorage里,每次请求在Header中携带,后端拦截器校验签名和过期时间。

JWT工具类核心逻辑如下:

@Component public class JwtUtil { // 生产环境务必放到配置中心或配置文件中,用@Value注入 private String secret = "sunny-books-secret"; public String createToken(Integer userId, String role) { return Jwts.builder() .claim("userId", userId) .claim("role", role) .setExpiration(new Date(System.currentTimeMillis() + 7 * 24 * 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } }

拦截器里做两件事:一是解析token,二是根据接口前缀判断是否需要管理员权限。公开接口如登录、注册、图书列表、图书详情不需要token,这类接口可以在拦截器里直接放行。需要登录的接口如果没有token,返回401。需要管理员的接口用/admin前缀统一管理,拦截器里校验role是不是1。

这里有个非常容易踩的坑:JWT的密钥不能硬编码。说出来你可能觉得是废话,但很多项目就是这么写的,包括一些生产项目。一旦代码泄露,任何人都可以用这个密钥伪造token。建议把secret放到application.yml里,用@Value注入,JDK8以上也可以用record或者ConfigurationProperties管理。

另一个坑是token的过期时间。图书阅读场景下,用户可能连续读两个小时书,如果token有效期只有30分钟,读到一半突然接口全部401,体验极差。我建议普通用户token给7天甚至30天有效期,管理端可以短一些,比如2小时,降低安全风险。这算是阅读类系统的特殊考量。

3. Vue端阅读体验的实现:从书城到阅读器

3.1 前端工程结构与路由守卫

前端我用的Vue 3 + Vite + Pinia + Vue Router + Element Plus。Vite比Webpack启动快太多,开发体验不是一个量级的,如果你的SpringBoot项目前端准备单独开发,Vite是首选。

目录结构如下:

src ├── api // 接口封装:user.js、book.js、order.js ├── assets // 静态资源 ├── components // 通用组件:BookCard、Pagination、Header ├── router // 路由配置 ├── store // Pinia状态管理:user.js、cart.js ├── views │ ├── home // 首页 │ ├── book // 图书列表、详情 │ ├── reader // 阅读器 │ ├── order // 购物车、订单列表 │ └── admin // 管理端 └── main.js

路由守卫是Vue项目里必须用心写的一段逻辑。阳光好书系统的路由分三种:公开路由(首页、图书列表、图书详情)、需要登录的路由(购物车、订单、阅读器)、管理员路由(图书管理、订单管理、用户管理)。逻辑很简单:每次路由跳转前,判断目标路由的meta字段,比如meta.requiresAuth为true时,检查Pinia里的token,没有就跳登录页。管理员路由加meta.requiresAdmin,再检查user info里的role。

很多同学在这里直接判断"有没有token",这有个隐患:token过期了但localStorage里还残留着旧值,前端以为登录了,后端接口却不断返回401。更稳妥的做法是后端提供一个接口校验token有效性,或者在前端axios响应拦截器里统一处理401并跳登录。

3.2 图书详情页的权限判断逻辑

图书详情页是阳光好书系统里业务逻辑最重的页面。它要同时展示图书信息、目录、价格、购买状态和阅读入口。

页面数据来源是两个接口:图书详情接口返回book信息和目录列表;另一个是"用户是否可读"的判断接口。为什么单独做一个判断接口,而不是每次查订单表?因为订单表只记录支付状态,而"用户可读"这个状态会随着订单支付成功、订单退款、管理员手动开通权限等操作变化。与其每次查询时做复杂的状态推导,不如单独建一张user_book_permission表,用户支付成功后立即往这张表里插一条记录,阅读时直接查这张表。

CREATE TABLE user_book_permission ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, book_id BIGINT NOT NULL, source VARCHAR(20) DEFAULT 'buy' COMMENT 'buy购买 admin管理员赠送', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_book (user_id, book_id) );

前端拿到permission字段后,详情页按钮显示逻辑就非常清晰:

  • 未登录:展示"登录后购买"
  • 已登录但没购买:展示价格和"购买"按钮,同时可以点"试读"打开阅读器只看前几章
  • 已购买:展示"开始阅读"按钮,直接进入完整阅读器

这个"试读"和"完整阅读"在同一个阅读器组件里实现,区别就是进入时传入的参数不同。试读模式传bookId和试读章节数,完整模式传bookId和上次阅读进度。

3.3 章节阅读器与阅读进度同步

阅读器是整个阳光好书系统里最像"阅读系统"的部分。别人看你的项目,能不能一眼认出这是图书阅读系统,就看阅读器的体验。

我的实现方案是用一个独立的Reader.vue组件,通过路由参数接收bookId。组件内部主要做几件事:

第一,加载目录。目录数据从图书详情接口拿,按chapter_no排序。阅读器左侧或顶部做一个抽屉,点击章节名切换内容。

第二,加载章节内容。切换章节时调用章节详情接口,把content字段渲染到页面主体。内容以文本段落为主,用v-html渲染需要后端返回富文本,但要注意XSS风险。安全做法是后端对内容做过滤,或者前端用DOMPurify清洗后再渲染。

第三,上一章、下一章。这个交互逻辑要处理边界:第一章时上一章按钮禁用,最后一章时下一章按钮禁用。实现上就是维护一个currentChapterNo,切换时带上chapter_no参数重新请求接口。

第四,字体大小调节。阅读类应用这个功能几乎是标配。我直接在阅读器顶部放一个字号加减控件,拿到字号值后设置给页面主体容器的font-size。再配合localStorage记录偏好,下次打开还保持用户习惯的字号。

第五,阅读进度保存。这是最需要用心做的功能。我的方案是:阅读器加载时会先请求reading_progress接口,获取last_chapter_no,如果有记录就自动跳转到上次章节。阅读过程中,滚动条滚动时监听scroll事件,计算当前阅读位置百分比,同时用一个防抖函数每3秒上报一次进度到后端。离开阅读器时,在onBeforeUnmount钩子里再强制上报一次,确保最后的位置不丢。

进度上报的防抖代码很简单,但非常管用:

let timer = null function reportProgress() { if (timer) clearTimeout(timer) timer = setTimeout(async () => { await saveProgress({ bookId: route.params.bookId, lastChapterNo: currentChapterNo.value, readPercent: calcPercent() }) }, 3000) }

为什么要防抖?因为用户翻页或者滚动时,scroll事件一秒触发几十次,如果每次都发请求,后端压力大,前端也会因为频繁的网络请求变得卡顿。3秒一次的频率足够保存进度,又不会浪费请求资源。

4. 商城交易链路:购物车、订单和支付

4.1 购物车用一张表就够了

电子书商城和实体书商城的购物车设计差异很大。实体书要考虑库存、运费、套餐组合,但电子书没有这些麻烦。一份图书就是一个SKU,价格固定,无实物配送,所以购物车表非常简单:

CREATE TABLE cart ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, book_id BIGINT NOT NULL, quantity TINYINT DEFAULT 1, checked TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_book (user_id, book_id) );

虽然有quantity字段,但电子书购买数永远只能是1。保留这个字段是为了兼容以后可能出现的"赠送"和"多买多送"玩法。

有人问过我一件事:购物车为什么不存前端localStorage,省一张表?我说可以,但对于阳光好书系统这种偏教学和毕设的项目,购物车存数据库的好处是更完整地演示了"一个业务功能从表设计到接口到页面的完整链路",答辩时也更有话讲。而且如果用户换浏览器登录,购物车数据还在,体验一致。

购物车接口我建议这样设计:

  • GET /cart/list 查询用户购物车
  • POST /cart/add 加入购物车
  • PUT /cart/update 修改数量或勾选状态
  • DELETE /cart/delete/{cartId} 删除条目
  • POST /cart/checkout 结算

加入购物车时要做一次校验:这本书是否还在售、购物车里是否已经有这本书。已经有的情况直接把quantity置1就行,不需要真的累加。

4.2 订单状态机不能拍脑袋设计

订单模块是商城系统的灵魂,也是最容易被做成"就是一个insert"的地方。很多毕设项目,用户点下单,后端插一条order记录,前端弹个"支付成功",就完了。这样不是不行,但一旦你想把支付做得真实一点,就会发现状态管理一塌糊涂。

阳光好书系统的订单状态我建议这样设计:

状态码含义触发条件可流转到
0待支付用户下单成功1、3
1已支付支付回调成功2、3
2已完成用户确认或超时自动完成无
3已取消用户主动取消或超时未支付无

下单接口的逻辑顺序很重要:先生成订单,再扣减库存,再清空购物车。这里有个并发问题,如果两个人同时购买同一本书,库存从1变成负数怎么办?最简单也最实用的办法是下单时用乐观锁,UPDATE语句里加条件:UPDATE book SET stock = stock - 1 WHERE id = ? AND stock > 0,返回受影响行数为0就说明库存不足,回滚订单。

订单号生成建议用"时间戳 + 随机数"或者雪花算法。如果用自增主键直接当订单号,有几个问题:可能被别人猜到订单数量、订单号过短且无业务含义。我用的方案是:

String orderNo = "BOOK" + System.currentTimeMillis() + String.format("%04d", new Random().nextInt(10000));

这个格式简单直观,保证基本唯一。真要上生产,用雪花算法或者数据库发号器。

4.3 支付对接与模拟支付的取舍

真实对接支付宝或微信支付是很多同学卡住的环节,因为需要商户资质,个人开发者很难拿到正式商户号。这里有两种务实的方案。

第一种方案:用支付宝沙箱环境。支付宝开放平台提供一个沙箱环境,有测试账号、测试密钥,完全可以模拟真实支付流程。后端集成流程是:配置appId、应用私钥、支付宝公钥、回调地址,调用支付宝sdk的支付接口生成支付表单,用户支付成功后支付宝异步回调通知后端,后端验签成功后更新订单状态。这个方案能让你的项目看起来非常完整,答辩时也很有说服力。唯一要注意的是,沙箱环境需要自己在支付宝开放平台申请,并下载支付宝客户端配合沙箱钱包使用。

第二种方案:模拟支付,也叫"假支付"。后端提供一个模拟支付接口,前端弹出一个支付确认框,用户点"确认支付"后调用后端接口,后端直接把这个订单标记为已支付,同时写好user_book_permission记录。这个方案零外部依赖,适合纯学习和演示场景。

我个人建议:如果有时间,优先做支付宝沙箱,因为"异步回调验签"这个技术点在面试时是加分项。没时间就做模拟支付,但一定要在代码里预留支付参数和回调接口的封装,后期接真实支付就是换个实现的问题。

支付成功后要做两件事,一个都不能忘:更新订单状态为已支付、往user_book_permission表插入一条记录。如果忘了插权限记录,用户钱付了书却读不了,这是整个系统最严重的逻辑漏洞。

5. 前后端联调时最容易翻车的五个地方

SpringBoot项目单测通过不代表项目能跑,前后端联调才是真正暴露问题的地方。我把自己做阳光好书系统时反复踩的坑集中说一下,这些在文档里经常看不到,但每个都会消耗你几个小时。

5.1 跨域配置和JWT请求头

前端跑在5173端口,后端跑在8080端口,浏览器会拦截跨域请求。很多同学在后端写了CorsRegistry配置,但还是报错,往往是因为配置了allowedOrigins("*")的同时又启用了allowCredentials(true)。这两个配置是冲突的:允许所有来源的情况下不能携带凭证。JWT认证模式里,token通常是放在Authorization头里的,这属于自定义请求头,所以必须显式允许。

正确的配置是:

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("http://localhost:5173") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true); } }

生产环境上线后,把allowedOrigins改成你的线上域名即可。前端发起请求时,一定记得在axios里设置withCredentials: true,同时带上Authorization头。

5.2 本地文件上传与静态资源映射

图书封面需要上传,章节内容也经常需要插图。如果你用本地上传方案,文件会被保存到服务器磁盘的某个目录,但SpringBoot默认静态资源只映射classpath下的static目录。你访问http://localhost:8080/upload/cover.jpg得到404,就是这个原因。

解决方法是自定义资源映射:

registry.addResourceHandler("/upload/**") .addResourceLocations("file:D:/sunnybooks/upload/");

如果项目要部署到云服务器或Docker环境,我建议直接上MinIO。MinIO是开源的分布式对象存储服务,兼容S3协议,在SpringBoot里集成也就几个依赖和一段配置的事。它比本地文件方案强的地方在于:文件和代码解耦、支持海量图片、Docker一键部署、可以做成独立存储服务供多个系统复用。你甚至可以把它当工具用,微博上很多教程都是"SpringBoot整合MinIO实现文件上传"。

5.3 token过期与axios统一处理

前端每个发起请求的地方都手动带token,这是最原始的写法。麻烦不说,token过期时的处理会变成噩梦。建议封装一个统一的axios实例,在请求拦截器里带上token,在响应拦截器里统一处理401。

这里给出一个精简的封装思路:

request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = 'Bearer ' + token } return config }) request.interceptors.response.use( response => response.data, error => { if (error.response?.status === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(error) } )

统一处理的好处是:所有接口的鉴权逻辑都在一个地方,不会出现"登录接口也带token"这种怪问题,也不会出现某个页面漏传token导致401后弹出难看的报错框。

5.4 分页查询参数两边不一致

图书列表必然要分页。前端Element Plus的分页组件页码从1开始,后端MyBatis-Plus的page对象页码也是从1开始,但如果前端用了某些组件,或者后端代码里写了PageHelper,页码起始值可能变成0。最常见的现象是:前端点第2页,传pageNum=2,后端却理解成"跳过0条取第2条记录",结果一直在加载重复数据。

我的建议是:接口参数统一定义为pageNum和pageSize,前端和后端都从1开始编号,后端代码里不要自己减一。同时在接口文档里明确写清参数含义。前后端联调第一件事就是确认分页参数对不对,这能省很多无意义的排查时间。

5.5 前端history路由和打包部署

Vue项目默认的hash模式部署简单,但URL里带个#号不太好看。很多人切到history模式,结果部署到服务器后,刷新页面就404。原因是history模式下,前端路由是浏览器端的history API管理的,服务器并没有对应的文件,刷新时服务器返回404。

如果你是前后端分离部署,Nginx配置如下:

location / { try_files $uri $uri/ /index.html; }

如果你图省事,把前端打包后的dist目录直接丢进SpringBoot项目的resources/static下,那么你需要让SpringBoot将所有未知路由转发到index.html。但这只是权宜之计,我还是建议用Nginx单独部署前端,SpringBoot只做后端接口服务。这样两个项目职责清晰,扩展时彼此不受影响。

6. 上线前要补的功课和功能演进方向

6.1 阅读器体验打磨

阅读器做完基础功能之后,我强烈建议再补三个体验优化点:夜间模式、字号设置持久化、目录预览。

夜间模式实现不难,在阅读器页面加一个isDark变量,切换时给内容区加一个class,背景色换成深色,文字颜色换成浅色。设置后的状态存localStorage,下次打开阅读器直接读取。

字号设置持久化是我一开始忽略的。用户体验的角度,用户每次都要重新调字号是很烦的,存localStorage成本极低,用户体验提升明显。

目录预览方面,如果章节很多,比如几百章,一次性渲染所有章节会让滚动列表卡顿。可以用虚拟滚动或者分页加载目录,Vue生态里有很多现成方案。

6.2 搜索从like到全文检索

图书列表的搜索功能第一版用MySQL like查询完全够用,比如按书名模糊搜索。但书名、作者、简介这几个字段的搜索会越用越觉得吃力,尤其简介字段是text类型,like '%关键词%' 无法走索引,数据量大了以后全表扫描,性能会很难看。

数据量在几千条以内,MySQL like方案完全没问题。数据量到几十万上百万,就要考虑Elasticsearch或者更轻量级的全文检索方案。有人提到在SpringBoot里集成HanLP做分词,这在项目里是可以的,中文分词后建立索引,再配合Elasticsearch做搜索,搜索体验会好很多。但这是中后期优化,不用第一版就上。把搜索接口的设计做得合理一点,比如参数用keyword参数名,不要写死成bookName,这样后面换搜索引擎时前端不用改。

6.3 缓存与性能优化

阅读系统里,图书详情和热门榜单是典型的热点数据,几乎所有用户进入首页都会查。这些数据量不大但访问频率高,非常适合加缓存。用Redis的话,key可以设计成book:detail:{id},value存JSON序列化后的图书详情。后台修改图书时主动删除对应的缓存,下次请求重新回源数据库。

章节内容要不要缓存?我的建议是不要直接把LONGTEXT全文塞进Redis。章节内容动辄几十KB,如果一本书几百章,缓存所有章节会占用大量内存。更合理的做法是缓存章节的列表信息,章节正文按需查库。阅读频繁的章节可以用短期缓存,比如设置10分钟过期,但收益有限,早期版本不做也没问题。

另外,首页热门榜可以定时计算:每天凌晨跑一次统计,把销售数据排个列表存Redis,当天所有访问都走缓存。这样比每次请求都实时统计的体验好得多。

6.4 演示数据和项目展示

最后说一个很多人忽略的事:把一个项目从"代码写完"变成"能拿得出手,让人看了眼前一亮",演示数据占一半功劳。

阳光好书系统至少要准备10本以上不同风格的图书,每本书至少3个章节,封面图风格统一,简介写得像模像样。为什么要这样?因为评审老师或面试官看项目时,第一眼看到的是界面,不是代码。如果书城里只有两三本书,封面还是默认占位图,哪怕你代码写得再好,第一印象就输了。

演示数据本身可以用代码初始化,写一个CommandLineRunner,项目启动时检查数据库是否为空,为空就自动插入种子数据。这样无论谁clone你的项目,一启动就能看到完整效果,不需要手动造数,这个体验是很加分的。

答辩或面试时,我建议重点讲这三个点:一是阅读权限设计,说明试读和已购用户的权限如何判断;二是订单状态机,说明支付回调后系统如何更新状态并发开通阅读权限;三是阅读进度同步,说明防抖上报和断点续读的实现。这三个功能分别对应内容展示、交易闭环、用户粘性,正好覆盖了阳光好书系统"阅读+销售"双核心的价值。

我个人在实际操作中的体会是,这类图书系统最容易翻车的地方不在具体某个技术点,而在于"打通"。书店和阅读器必须是一体的,订单和权限必须是联动的,进度和章节必须是匹配的。只要这些关键链路都通了,剩下的就是打磨细节。如果你正在做类似的项目,可以按我上面的思路先搭骨架,再逐个模块填充。遇到问题的时候,记住一句话:先把业务逻辑画清楚,再写代码。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 9:09:36

SSM+Flask混合架构实战:社区流浪动物救助领养系统开发全记录

做社区流浪动物救助领养系统这类题目&#xff0c;每年都能在各大毕业设计选题清单里看到。很多同学看到“Java SSM Flask”三个词叠在一起就懵了&#xff0c;下意识觉得这是不是把两套后端硬塞到一个项目里&#xff0c;技术栈太乱。但恰恰相反&#xff0c;我在用这套组合完整…

作者头像 李华
网站建设 2026/10/3 9:09:24

Servlet高校图书管理系统源码解析:从环境配置到核心模块实战

很多同学拿着这套“servlet高校图书管理信息系统”的源码找到我&#xff0c;第一句话都是&#xff1a;学长&#xff0c;代码我打开看了&#xff0c;下一步该干什么&#xff1f;还有同学直接双击运行&#xff0c;发现打不开&#xff0c;跑来问我是不是源码有问题。其实问题不在源…

作者头像 李华
网站建设 2026/10/3 9:07:18

Visual Studio二月更新解析:升级避坑与高频问题排查指南

每年二月的 Visual Studio 更新&#xff0c;在微软的发布节奏里通常是个承前启后的版本&#xff1a;既要把年初预览阶段定下来的功能做一轮收口&#xff0c;又要为三四月的重头戏铺路。今年的二月更新我看完之后&#xff0c;第一感觉是“稳”&#xff0c;第二感觉是“某些坑终于…

作者头像 李华
网站建设 2026/10/3 9:06:02

Lombok插件失效不报错?IDEA与Maven编译链路深度排查指南

早上到工位&#xff0c;同事跟我说了一句让人头皮发麻的话&#xff1a;“我代码写着写着&#xff0c;所有实体类里的 getter 和 setter 突然全红了&#xff0c;但 Maven 编译又一点错都没有&#xff0c;连 warning 都不带一条。” 我过去看了一眼&#xff0c;确实诡异&#xff…

作者头像 李华
网站建设 2026/10/3 9:03:58

AI辅助安卓开发实战:提效场景、踩坑记录与工具推荐

安卓开发这个圈子&#xff0c;最近一年绕不开的话题就是AI辅助编程。从Android Studio内置的Gemini&#xff0c;到各类AI编程助手&#xff0c;再到直接用通用大模型对话生成代码&#xff0c;我是实实在在把这些工具用在了日常业务开发里。今天这篇就把我近半年来用AI辅助安卓应…

作者头像 李华
网站建设 2026/10/3 9:03:43

Pandas数据分析全流程:从数据清洗到可视化的完整实战

最近在带几个朋友入门数据分析&#xff0c;发现大家拿到一份数据之后最常见的状态就是愣住&#xff0c;不知道从哪下手。清洗数据嫌麻烦&#xff0c;画图又画不明白&#xff0c;最后折腾半天还在 print(df.head()) 打转。其实这个流程完全不神秘&#xff0c; Pandas 就是那…

作者头像 李华