
简介这份基于Spring Boot的二手图书交易平台系统为完整毕业设计项目面向高校计算机相关专业毕业生及Spring Boot入门学习者帮助掌握从需求分析、数据库建模到前后端联调的完整开发流程。系统覆盖二手图书发布、分类浏览、关键词搜索、在线议价、购物车与订单管理、用户评价等核心交易模块并包含注册登录、个人资料与收货地址管理等配套功能适用于电商类管理系统的课程设计或毕业设计场景。资源包共204个文件压缩后约49.69MB核心包括17个Java源码文件、17个class编译文件、62个js、42个css、4个html等前端静态资源以及yaml和properties项目配置、SQL初始化脚本和项目说明文档前端页面与后端接口分层清晰Maven工程目录结构规范便于导入集成开发环境直接运行调试。已有657人学习浏览适合作为毕业设计参考模板、Spring Boot全栈实战演练以及二手交易平台二次开发的基础工程。 从一个网盘链接或者群里转来的基于springboot的二手图书交易平台系统.zip解压后第一眼往往不是 src而是 target/classes 下一堆 .class。很多人这时候就以为源码被阉割了其实恰恰相反编译产物把项目最终的模块边界、实体关系和 REST 暴露范围都摆在了明面上。这个 zip 里的二手图书交易平台覆盖图书发布、图片上传、购物车、下单等环节技术骨架是 Spring Boot Spring Data REST JPA文件存储交给 FileSystemStorageService。对做 Java 毕设的人来说它是一份能直接运行的管理系统底稿对已经工作的开发者也值得看看如何用极少的 Controller 代码把交易流程跑通。2. 反推Spring Boot项目骨架从.class到实体与Repository设计2.1 解压后先看 target/classeszip 压缩包解压后我一般先看 target 目录因为它反映的是 Maven 构建后真实生效的产物。这个二手图书交易系统的 class 文件组织大致如下具体包名以你解压到的为准SpringBootSecondhandBook ├── pom.xml └── target └── classes ├── application.yml └── com/example/library ├── Goods.class ├── Order.class ├── Cart.class ├── User.class ├── UserRepository.class ├── CartRepository.class ├── GoodsRepository.class ├── FileSystemStorageService.class ├── FileUploadController.class └── ExposeEntityIdRestRepositoryRestConfigurer.class说明一下为什么 target/classes 能看出模块设计Spring Boot 的类名本身就在声明职责。Goods、Order、Cart、User是四个业务实体三个 Repository 落在 JPA 数据层FileUploadController负责接收 HTTP 上传ExposeEntityIdRestRepositoryRestConfigurer表明项目使用了 Spring Data REST 并且显式处理了实体 ID 回显。拿到这样一份 zip恢复源码时只需要按这个结构在 src/main/java 下补出包层级。2.2 Goods、Order、Cart、User 的实体边界四个实体里Goods是二手图书挂牌信息User是买卖双方Cart是临时选书清单Order是交易快照。常见的实体设计可以这样写Entity public class Goods { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private String name; private String author; private String publisher; private BigDecimal price; private String coverPath; // 图片上传后返回的相对路径 Column(columnDefinition TEXT) private String description; ManyToOne private User seller; // 卖家 Enumerated(EnumType.STRING) private GoodsStatus status; // 在售、已下架、已卖出 }这里的关键参数GeneratedValue(strategy GenerationType.IDENTITY)面向 MySQL 自增主键是毕设项目里最常用的配置。coverPath存相对路径而不是完整 URL换服务器时只要改上传根目录数据库里的数据不用动。Enumerated(EnumType.STRING)用字符串存枚举比默认的ORDINAL可读性好很多也不会因为枚举顺序调整而错乱。GoodsStatus是项目里的状态枚举分别对应 ON_SALE、SOLD_OUT、SOLD。Order 和 Cart 更适合用关联关系表达Entity public class Cart { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; ManyToOne private User user; ManyToOne private Goods goods; private Integer quantity; }ManyToOne是理解交易链路的关键购物车条目必须同时指向用户和图书下单时才能从 Cart 复制出商品快照到 Order。价格字段要注意Order 里必须单独存下单瞬间的价格不能实时去查goods.getPrice()否则卖家改价会影响历史订单金额答辩时很容易被老师问住。2.3 Repository 与 Spring Data REST 的暴露范围Spring Data REST 是这个项目的核心机制它不需要写 Controller直接在 Repository 上生成 REST APIpublic interface UserRepository extends JpaRepositoryUser, Long { } public interface GoodsRepository extends JpaRepositoryGoods, Long { ListGoods findByNameContainingIgnoreCase(String name); } public interface CartRepository extends JpaRepositoryCart, Long { ListCart findByUserId(Long userId); }这些接口编译成 class 后方法签名仍然保留所以从 class 文件能准确还原 Repository 层。Spring Data REST 会自动把GoodsRepository暴露成/goods把CartRepository暴露成/carts不需要额外声明 Controller。这正体现了 springboot 自动装配原理starter 在 classpath 里扫描到 Repository 接口后自动装配出对应的 REST 端点。项目里还有一个容易被忽略的配置类ExposeEntityIdRestRepositoryRestConfigurer它解决的是 Spring Data REST 默认不返回实体 ID 的问题。接手代码后我一般这样补全Configuration public class ExposeEntityIdRestRepositoryRestConfigurer implements RepositoryRestConfigurer { Override public void configureRepositoryRestConfiguration( RepositoryRestConfiguration config, CorsRegistry cors) { config.exposeIdsFor(Goods.class, Order.class, Cart.class, User.class); } }参数说明exposeIdsFor接收可变参数需要把回显 ID 的实体类都列在这里。前端编辑数据时拿不到主键 ID 是常见问题这个配置就是为前端补上最后一块拼图。CorsRegistry参数顺手解决了前后端分离时的跨域配置后面 4.4 节会用到。2.4 实体、仓库、REST 路径对照实体RepositoryREST 默认路径业务场景UserUserRepository/users注册、登录、卖家信息GoodsGoodsRepository/goods图书发布、检索、详情CartCartRepository/carts加入购物车、修改数量Order文件名中未出现 OrderRepository/orders下单结算、订单列表一个必须提示的点文件列表里没有OrderRepository.class。这说明订单的读写可能被封装在自定义 Service 中或者交付时去掉了这个接口。接手源码时第一件事就是补一个OrderRepository extends JpaRepositoryOrder, Long否则/orders这个 REST 路径根本不存在下单链路会直接断掉。提示拿到这类 zip先按实体、Repository、配置类的顺序读比先看 Controller 有效得多。Controller 只暴露了文件上传交易主链路几乎全由 Spring Data REST 撑起来。3. 图书图片上传实战FileSystemStorageService 与 FileUploadController3.1 存储服务的设计取舍FileSystemStorageService在 Spring Boot 官方 guidance 里是标准模板但放到二手图书场景要改两处一是 rootLocation 不要默认相对路径二是文件名需要重新生成避免中文名和重名覆盖。Service public class FileSystemStorageService { private final Path rootLocation; public FileSystemStorageService( Value(${app.storage.location:uploads}) String location) { this.rootLocation Paths.get(location).toAbsolutePath().normalize(); } public String store(MultipartFile file) { String original StringUtils.cleanPath( Objects.requireNonNull(file.getOriginalFilename())); String ext original.substring(original.lastIndexOf(.)); String filename System.currentTimeMillis() _ UUID.randomUUID().toString().substring(0, 8) ext; Path target rootLocation.resolve(filename); try { Files.createDirectories(target.getParent()); file.transferTo(target); } catch (IOException e) { throw new RuntimeException(存储文件失败: original, e); } return filename; } }逻辑说明Value允许用 application.yml 里的app.storage.location覆盖上传目录冒号后面的uploads是兜底值。cleanPath是 Spring 提供的文件名清洗用来防../路径穿越。文件名用时间戳加 UUID 片段重新生成既避免中文文件名编码问题也防止重名覆盖。数据库里保存的是store方法返回的 filename前端展示时再用/uploads/前缀拼接完整访问地址。3.2 上传接口的写法与参数限制FileUploadController只需要两个端点一个接收图片并返回相对路径一个做异常兜底。最简写法如下RestController public class FileUploadController { private final FileSystemStorageService storageService; public FileUploadController(FileSystemStorageService storageService) { this.storageService storageService; } PostMapping(/upload) public MapString, String upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { throw new IllegalArgumentException(上传文件不能为空); } String filename storageService.store(file); return Map.of(filename, filename, url, /uploads/ filename); } }这里没有给RequestParam设置required false所以 multipart 表单的字段名必须叫file。如果前端用的是富文本编辑器往往会把字段名改成file或uploadFile这时候就要按实际参数名调整。我一般还会加一个ExceptionHandler把空文件、超限文件统一转成 JSON 错误信息而不是让框架返回默认 500 页面。3.3 application.yml 与静态资源映射图片写入磁盘后还要能被浏览器访问否则前端img src/uploads/xxx.jpg直接 404。springboot 配置里需要同时控制上传大小和静态资源位置spring: servlet: multipart: max-file-size: 5MB max-request-size: 20MB web: resources: static-locations: file:${app.storage.location:./uploads} app: storage: location: ./uploads参数说明spring.web.resources.static-locations默认指向classpath:/static/改成file:./uploads后/uploads/xxx.jpg会直接映射到磁盘目录。这个做法比写 WebMvcConfigurer 的addResourceHandlers更简洁但要注意路径是相对路径时启动目录不同会导致文件被写进不同位置。配置项作用建议值spring.servlet.multipart.max-file-size单文件上限图书封面一般不超过 2MB5MBspring.servlet.multipart.max-request-size一次请求所有文件总大小20MBapp.storage.location上传根目录/opt/book/uploadsspring.web.resources.static-locations静态资源磁盘映射file:${app.storage.location}3.4 本地可行、部署就挂的排查顺序图片上传是毕设演示的重灾区我排查时按这个顺序先看 application.yml 里app.storage.location是不是绝对路径再确认 uploads 目录的写权限Linux 下通常是chmod -R 755 uploads或调整目录属主最后看浏览器 Network 面板里图片请求状态码404 基本是 static-locations 写错403 是文件权限不足413 是 multipart 配置没生效。FileSystemStorageService里如果用的是相对路径建议启动时打印一行当前工作目录的绝对路径确认没有歧义。4. 运行联调从zip到跑通二手图书下单链路4.1 用IDEA从zip恢复可运行工程zip 里如果没有 src我会在 IDEA 里新建一个 Spring Boot 工程再把 target/classes 里的业务类整理进 src/main/java。pom.xml 需要补齐三个依赖spring-boot-starter-web、spring-boot-starter-data-jpa、spring-boot-starter-data-rest。在 IDEA 的 Project Structure 里把 target/classes 标记为编译输出也完全可以关键在于 application.yml 里的数据源要能启动。spring: datasource: url: jdbc:h2:file:./data/secondhand driver-class-name: org.h2.Driver username: sa password: jpa: hibernate: ddl-auto: update database-platform: org.hibernate.dialect.H2Dialect选 H2 而不是 MySQL是因为毕设演示时不需要单独安装数据库。参数说明jdbc:h2:file:./data/secondhand表示数据落盘而不是内存模式重启数据还在ddl-auto: update让 JPA 按实体自动建表省去手工执行 SQL 的环节。如果后面要切到 MySQL只需要把 url、driver-class-name 和 dialect 换掉实体代码不用动。4.2 初始化种子数据项目如果没有自带 SQL可以在 src/main/resources/data.sql 写入初始化数据。Spring Boot 执行 data.sql 的时机在 Hibernate 建表之后所以表结构已经存在时才能插入成功。-- data.sql INSERT INTO USER (ID, USERNAME, NICKNAME) VALUES (1, seller01, 书架整理员); INSERT INTO GOODS (ID, NAME, AUTHOR, PRICE, STATUS, SELLER_ID) VALUES (1001, Spring Boot 实战, Craig Walls, 39.00, ON_SALE, 1); INSERT INTO CART (ID, USER_ID, GOODS_ID, QUANTITY) VALUES (1, 1, 1001, 1);这里 SELLER_ID 指向 USER 表的 1 号用户两张表通过外键关联。data.sql 每次启动都会执行所以 ID 必须固定或者先 DELETE 再 INSERT否则第二次启动会报主键冲突。更稳妥的做法是用 CommandLineRunner 在 Java 代码里判断“没有记录才创建”避免重复初始化。4.3 用 curl 把交易链路打通启动后确认日志里有 Tomcat started然后用 curl 验证。先看商品列表curl -s http://localhost:8080/goods | jq .content[0] | {id, name, price}看到返回里的 id、name、price说明 GoodsRepository 已经暴露成 REST 接口。注意/goods返回的是分页结构数据在content数组里这是 Spring Data REST 的默认行为。再注册一个买家curl -s -X POST http://localhost:8080/users \ -H Content-Type: application/json \ -d {username:buyer01,nickname:淘书人}POST 到/users后Spring Data REST 会把 JSON 反序列化成 User 并保存响应头里带 Location。如果返回 422 Unprocessable Entity通常是 JSON 字段名和实体的属性对不上检查大小写和命名策略。最后创建订单curl -s -X POST http://localhost:8080/orders \ -H Content-Type: application/json \ -d {goods:/goods/1001,user:/users/1,quantity:1,amount:39.00,status:PENDING}创建订单这一步有很多人卡住Spring Data REST 对关联对象要求传 URI 引用字符串而不是嵌套 JSON。goods:/goods/1001的意思是引用 ID 为 1001 的图书资源user:/users/1同理。如果直接传{goodsId:1001}框架无法识别返回 422 或 400。4.4 前后端分离时的 CORS 与接口形态毕设如果要用 Vue 做前端springboot vue 前后端分离场景下必须处理跨域。可以复用 2.3 节的 RepositoryRestConfigurerOverride public void configureRepositoryRestConfiguration( RepositoryRestConfiguration config, CorsRegistry cors) { config.exposeIdsFor(Goods.class, Order.class, Cart.class, User.class); cors.addMapping(/**) .allowedOrigins(http://localhost:5173) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS); }allowedOrigins按前端 Dev Server 的实际地址填写Vite 默认是 5173 端口。Spring Data REST 的写操作默认接受application/jsonaxios 设置Content-Type: application/json即可。如果前端用 PATCH 做局部更新Spring Data REST 默认支持如果只能用 PUT就必须传完整对象否则为 null 的字段会被置空这是前后端联调时最常见的隐性数据丢失。4.5 运行时常见错误对照表现象原因处理启动报 Table doesnt existddl-auto 被改成 none改回 update 或手动执行建表 SQL/goods 返回 401项目引入了 spring-security 且未放行在 SecurityConfig 白名单加入 /goods、/users、/uploads/**上传图片后访问 404static-locations 配置错误检查 file: 前缀和路径末尾斜杠POST /orders 返回 405没有 OrderRepository补 JpaRepository 接口8080 端口被占用本地服务冲突改 server.port 或关闭占用进程5. 进阶让Spring Data REST接口具备可演示的搜索与分页能力5.1 给Repository增加派生查询摘要里提到搜索和热门图书推荐这些完全可以通过扩展 GoodsRepository 做到不需要新增 Controllerpublic interface GoodsRepository extends JpaRepositoryGoods, Long { RestResource(path byName, rel byName) ListGoods findByNameContainingIgnoreCase(Param(name) String name); // 热门推荐按已售数量取前 6 本在售图书 RestResource(path hot, rel hot) ListGoods findTop6ByStatusOrderBySoldCountDesc(GoodsStatus status); }逻辑说明RestResource(path byName)把搜索方法暴露为GET /goods/search/byName?nameJava。Param(name)定义查询参数名不加时默认是方法参数名。热门推荐方法会映射为/goods/search/hot?statusON_SALE。这里要求 Goods 实体里有 soldCount 字段如果没有可以换成按创建时间排序的findTop6ByStatusOrderByCreateTimeDesc效果也够演示。5.2 收敛购物车聚合根Spring Data REST 默认会把 Cart 暴露成/carts但购物车更适合作为 User 或 Order 的内部聚合不应该被随意直接增删。可以这样收敛RepositoryRestResource(exported false) public interface CartRepository extends JpaRepositoryCart, Long { ListCart findByUserId(Long userId); }exported false会关闭/carts的 REST 暴露购物车只能在 Order 创建逻辑中作为内部数据存在。这个改动让接口面更干净也符合聚合根的设计思想演示时能讲出理由。对应的Order 可以设置RepositoryRestResource(path orders)让默认路径明确化。5.3 分页与排序参数验证Spring Data REST 默认启用分页/goods返回结构是{content, pageable, totalElements}。验证排序参数时我通常用curl -s http://localhost:8080/goods?page0size5sortprice,descnameSpring这里 page 从 0 开始size5 是每页 5 条sortprice,desc 按价格降序。需要说明name参数不会自动过滤数据它只是被框架忽略真正要按名称搜索必须调用/goods/search/byName?nameSpring。如果不想每次手动敲 curl可以用 MockMvc 做接口自测mockMvc.perform(get(/goods/search/byName).param(name, Spring)) .andExpect(jsonPath($[0].name).value(Spring Boot 实战)) .andExpect(status().isOk());$[0].name断言返回数组第一个元素的 name 字段。如果方法返回 Page 类型断言路径要变成$.content[0].name两种情况要区分开。5.4 验收清单演示前这样检查给自己的毕设做验收时按这些点逐项打勾/goods/search/byName能否搜索到预期图书公开的 ID 是否在 JSON 响应中可见上传目录换到绝对路径后是否能正常读写订单创建后商品状态是否从“在售”变成“已下架”。订单状态流转如果在 Service 层做建议在方法上加Transactional保证订单落库和商品下架在同一个事务中避免订单建好了、商品状态没更新的情况。这套检查做完整个二手图书交易系统的演示流程就不会在半路翻车。本文还有配套的精品资源点击获取