基于Vue和Spring Boot的二手图书交易系统设计与实现
简介这是一份基于 Spring Boot Vue 的二手图书交易系统 Java 毕业论文内容定位于计算机相关专业学生的毕业设计论文指导和系统设计参考。全文围绕课题背景、研究意义、系统设计、技术架构、模块划分、系统实现与系统优点展开以 B/S 架构为基础采用 Java 语言与 MySQL 数据库重点介绍用户、图书信息、留言板、系统和订单等模块同时覆盖 RESTful API 设计风格与 Spring Security 身份验证授权机制体现高内聚低耦合的设计特点能够帮助读者理解从需求分析、系统设计到实际部署的完整流程尤其对需要实现二手书交易、在线留言、订单管理等典型功能的学生有直接参考价值。资源包共 1 个文件为 docx 格式大小 5.33MB内含中英文摘要、目录、正文、结论与参考文献论文结构完整格式规范可方便地用于排版参照、开题报告撰写或毕设答辩准备。已有 151 人学习该资源适合正在筹备同类 Java Web 毕设选题的学生作为重要参考资料。1. 二手图书交易系统为什么 Vue Spring Boot 能撑起一篇毕业论文每年毕业季都能看到大量类似选题基于 Vue 和 Spring Boot 的二手图书交易系统。这个题目看起来是“图书 CRUD”真正写起来才会发现工作量集中在交易状态流转、图片存储、权限控制和前后端联调上。下面的内容按一套完整可交付的方案来组织从表结构设计、JWT 鉴权、图书发布接口到 Vue 3 路由和 Axios 拦截器最后落到订单状态机和全链路验证。这些内容适合正在做毕设、或者想把手里的“管理系统”升级成带真实交易闭环的人。2. 项目初始化与表设计版本怎么选、字段怎么定2.1 选型边界Vue 3、Spring Boot 2.7 和 MyBatis-Plus做二手图书交易系统前后端分离是默认姿势。前端用 Vue后端用 Spring Boot中间走 JSON 接口。这个组合之所以成为毕设和简历项目的常客不是因为技术新而是因为每一层都有足够成熟的生态出了问题能在一小时内找到答案。选型时先把版本钉死这是大家最容易反复折腾的地方。Spring Boot 3.x 确实更“新”但它强制要求 JDK 17很多学校机器、旧教程和网上的依赖配置都是基于 JDK 8 的。Spring Boot 2.7.x 是 2.x 系列的最终维护版本兼容 JDK 8跑在实验室老机器上毫无压力答辩演示时不会因为环境问题翻车。技术栈组件版本选择说明Spring Boot2.7.xJDK 8 兼容2.x 最终维护版MyBatis-Plus3.5.x单表 CRUD 不写 SQLVue3.x Vite组合式 API 写起来更紧凑UI 组件库Element Plus表单、表格、上传开箱即用数据库MySQL 8.x如果本机装不上用 H2 替代演示Vue 3 和 React 的对比在面试里被反复问做毕设时不需要纠结这个。选 Vue 3 的理由很实际Element Plus 的表单校验、分页表格、图片上传组件能直接省掉大量样式工作让项目的“完成度”看起来比实际投入高。2.2 四张核心表用户、图书、订单、购物车表设计是论文里最容易被评委盯着看的部分。二手图书交易和普通电商的核心区别在于图书有“成色”属性订单流程里买卖双方都是普通用户没有后台管理员一刀切的审核环节。CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录名, password varchar(100) NOT NULL COMMENT BCrypt 加密后的密码, nickname varchar(50) DEFAULT NULL, phone varchar(20) DEFAULT NULL, avatar varchar(255) DEFAULT NULL, credit int DEFAULT 100 COMMENT 信用分扣到 0 禁止交易, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE book ( id bigint NOT NULL AUTO_INCREMENT, seller_id bigint NOT NULL COMMENT 卖家用户 ID, title varchar(100) NOT NULL, author varchar(50) DEFAULT NULL, isbn varchar(20) DEFAULT NULL, original_price decimal(10,2) DEFAULT NULL, price decimal(10,2) NOT NULL COMMENT 实际售价, condition_level tinyint DEFAULT 4 COMMENT 成色5全新 4九成新 3七成新 2有笔记 1破损, description text, cover_url varchar(255) DEFAULT NULL, status tinyint DEFAULT 1 COMMENT 1在售 2已预订 3已售出 0下架, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_seller (seller_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单表和购物车表篇幅原因不展开但设计时有三个点需要注意。第一订单里要冗余book_title、cover_url、price这三个快照字段一旦图书被删除或改价订单历史仍然完整第二订单表的status字段用tinyint加注释不要用字符串后面写状态机时映射更清晰第三购物车表用user_id book_id做唯一索引防止同一个用户把同一本书加两遍。图书表的condition_level是二手场景的差异化字段论文的“系统分析”章节可以拿它做文章比如按成色筛选、按成色设置价格区间。status字段从 1 到 3 的流转配合订单状态一起构成系统的业务闭环。2.3 项目目录结构与跨域放行前后端分离最常见的第一个坑是跨域。Spring Boot 后端跑在 8080Vite 前端跑在 5173浏览器会把这两个端口视为不同来源。Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:5173) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .maxAge(3600); } }这段配置只放行了/api/**路径前端页面本身不走跨域逻辑。allowedOrigins写死为本地开发地址部署时改成服务器域名不要图省事写*否则携带 Cookie 的请求会被浏览器拦截。后端目录结构用最常见的分层controller、service、mapper、entity、config五个包。论文里的“系统设计”章节画包图时这个结构可以直接照抄成 UML 包图。3. 后端实现JWT 登录与图书发布接口3.1 JWT 工具类与登录接口登录方案现在很少用 Session常见做法是 JWT。无状态意味着后端不需要维护会话表后续扩展成小程序端或移动端时同一套 Token 机制可以直接复用。Component public class JwtUtil { Value(${jwt.secret}) private String secret; Value(${jwt.expire}) private Long expire; public String generateToken(Long userId, String username) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(username, username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() expire)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } }登录接口写一个AuthController接收用户名和密码密码用BCryptPasswordEncoder校验。生成 Token 后返回给前端前端存到localStorage之后的请求在 Header 里带Authorization: Bearer token。jwt.secret和jwt.expire配置在application.yml里secret 至少 32 位expire 设 24 小时太长有安全风险太短用户要频繁重新登录。3.2 图书发布文件上传与参数校验图书发布接口是系统的核心写操作涉及表单数据和图片文件混合提交。前端用multipart/form-data同时传 JSON 字段和文件后端用MultipartFile接收。PostMapping(/api/books) public Result publish(RequestParam(file) MultipartFile file, RequestParam(title) String title, RequestParam(price) BigDecimal price, RequestParam(conditionLevel) Integer conditionLevel, RequestParam(description) String description) { // 校验文件类型和大小只允许 jpg/png/webp不超过 2MB if (file.isEmpty() || !allowedTypes.contains(file.getContentType())) { return Result.error(图片格式不支持仅限 jpg/png/webp); } String coverUrl fileService.saveToLocal(file); Book book new Book(); book.setSellerId(currentUserId()); book.setTitle(title); book.setPrice(price); book.setConditionLevel(conditionLevel); book.setDescription(description); book.setCoverUrl(coverUrl); bookService.save(book); return Result.success(book); }参数校验没写在实体类的注解上而是在接口入口处手动判断文件类型。原因很直接NotBlank这类注解对普通字段好用对MultipartFile不能直接起效与其混用两套校验机制不如在 controller 里统一处理文件校验其余字段用Validated注解。文件存储用本地目录upload/路径配在配置文件中。答辩时如果被问“生产环境怎么做”标准回答是换 OSS 或 MinIO但本地存储更适合演示环境。3.3 线上最常见三个报错日期格式、跨域、空指针日期格式是联调时第一个报错点。前端传2026-03-15 10:30:00后端默认解析不了带空格的时间格式。spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 servlet: multipart: max-file-size: 2MB max-request-size: 10MBapplication.yml里这段配置把 JSON 日期的序列化和反序列化统一成带时间的格式time-zone设成 GMT8否则数据库存的时间会比实际少 8 小时。max-file-size是单文件上限max-request-size是整个请求体的上限客户端一般不会超过 10MB设太大会占用服务器带宽。空指针问题集中在currentUserId()这个从 Token 解析用户的方法上。如果前端没带 Token解析结果为 null后续调用userService.getById(userId)会直接空指针。处理方式是写一个全局异常处理器拦截NullPointerException和自定义的UnauthorizedException统一返回 401 状态码前端收到 401 就跳转登录页。4. 前端实现Vue3 路由、Axios 拦截器与图书广场4.1 Vite 初始化与依赖安装npm create vitelatest book-front -- --template vue cd book-front npm install npm install axios vue-router4 pinia element-plusbook-front是前端项目名模板选vue而不是vue-ts毕设项目去掉 TypeScript 能省掉类型定义的时间。依赖安装卡住是常见问题npm install超过 5 分钟基本可以断定网络问题换成国内镜像源npm config set registry https://registry.npmmirror.com后重新执行即可。element-plus按需引入还是全量引入值得说一句。全量引入在main.js里app.use(ElementPlus)一行搞定打包体积大一点但对毕设无所谓。按需引入要用unplugin-vue-components插件省体积但增加配置复杂度二选一就行。4.2 路由与导航守卫详情页参数怎么传图书列表跳详情页是路由传参的典型场景两种方式在面试里会被追问区别。// 方式一路径参数适合详情页 { path: /books/:id, name: BookDetail, component: () import(../views/BookDetail.vue) } // 方式二query 参数适合列表筛选条件 router.push({ path: /books, query: { condition: 4, page: 2 } })详情页用路径参数因为图书 ID 是唯一标识URL 形如/books/12刷新页面后参数仍然在地址栏里刷新不丢。筛选条件用 query 参数因为条件是可选的、非唯一的/books?condition4这种 URL 还能被收藏和分享。两种方式在组件里分别用route.params.id和route.query.condition读取。导航守卫拦截未登录访问发布页面router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } })redirect参数记录用户原本想去的页面登录成功后调用router.push(route.query.redirect)跳转回去这个细节写进论文的“系统实现”章节算一个亮点。4.3 Axios 封装与图书列表渲染Axios 拦截器统一处理 Token 注入和错误提示避免每个页面重复写一遍状态判断。const request axios.create({ baseURL: http://localhost:8080/api, timeout: 10000 }) 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) } )图书广场页面用一个el-row布局渲染图书卡片每个卡片包含封面图、书名、价格和成色标签。数据请求放在onMounted里分页参数变化时重新请求const loadBooks async () { const res await request.get(/books, { params: { page: currentPage.value, size: 12, keyword: keyword.value } }) bookList.value res.data.records total.value res.data.total }后端配合MyBatis-Plus的Page分页插件返回records total结构前端el-pagination组件直接对接这两项。搜索框的v-model绑定keyword触发搜索时把currentPage重置为 1否则会出现搜完在第 5 页没数据的体验问题。图书详情的el-carousel轮播图可以展示多张实物图但表里只有一个cover_url字段时不要强行设计多图。二手交易的真实诉求是“看得清成色”图片清晰度比数量重要。5. 订单状态机交易闭环验证与论文答辩的加分项5.1 五个状态的迁移规则订单状态是整个系统里最有技术含量的部分也是论文“核心功能设计”章节最有得写的一块。定义五个状态待付款、已付款、已发货、已完成、已取消。只有四种合法迁移待付款到已付款、已付款到已发货、已发货到已完成以及待付款到已取消。public enum OrderStatus { PENDING(0, 待付款), PAID(1, 已付款), SHIPPED(2, 已发货), COMPLETED(3, 已完成), CANCELLED(4, 已取消); public boolean canTransitTo(OrderStatus target) { return switch (this) { case PENDING - target PAID || target CANCELLED; case PAID - target SHIPPED; case SHIPPED - target COMPLETED; default - false; }; } }不允许从已取消或已完成状态回头不允许跨状态跳转。这个枚举集中管理所有迁移规则比在 Service 里写if (status 1 target 2)清晰得多答辩时被追问“状态流转怎么保证安全”就有了明确答案。5.2 用 curl 走一遍完整交易链路TOKEN$(curl -s -X POST http://localhost:8080/api/auth/login \ -H Content-Type: application/json \ -d {username:buyer,password:123456} \ | python3 -c import sys,json; print(json.load(sys.stdin)[token])) # 买家下单 curl -s -X POST http://localhost:8080/api/orders \ -H Authorization: Bearer $TOKEN \ -H Content-Type: application/json \ -d {bookId:1} # 模拟卖家发货替换为卖家 token curl -s -X PUT http://localhost:8080/api/orders/1/status \ -H Authorization: Bearer $SELLER_TOKEN \ -d {action:ship}用Bearer Token串起登录、下单、发货三个动作后图书状态从在售变成已售出订单状态从待付款一路到已发货。这个验证过程截图放进论文就是完整的“系统测试”证据。5.3 答辩前能落地的两点准备第一把状态图和时序图画清晰。用 draw.io 画好之后导出成 PDF 再转 PNG插入 Word 里不会糊。图中只保留状态迁移箭头和接口调用路径不要堆砌数据库字段。第二把 JWT 无状态认证的原理准备好一句话版本服务端不存登录态通过签名验证 Token 可信适合前后端分离和多端复用。这两个点通常是评委在高分和及格分之间的分水岭。本文还有配套的精品资源点击获取