基于SpringBoot的校园二手交易平台实战:从架构到避坑全解析 简介基于SpringBoot的校园二手交易平台Java项目源码及配套数据库文件面向计算机专业高年级学生与实战开发者适用于课程设计、毕业设计等实践环节可帮助从零搭建包含用户管理、商品展示、交易撮合、订单处理等核心模块的完整系统。压缩包共计262个文件内含34个Java源代码文件、1个MySQL数据库脚本、前端页面所需的HTML/CSS/JS资源、界面演示图片及项目说明文档整体仅3.88MB目录层次分明便于按模块检索。当前已有81人学习下载。资源在导师指导下完成并获评98分代码经多轮编译调试稳定可用技术栈采用SpringBoot 2.x、MyBatis Plus、Redis缓存与Shiro权限控制前后端分离数据库设计遵循第三范式前端还实现响应式布局以适配多终端。附加文档涵盖系统设计说明书、数据库设计文档、部署指南及API接口文档代码注释完整适合学习者参考架构、理解关键业务流转并在此基础上进行二次开发与功能扩展。1. 基于SpringBoot的校园二手交易平台真正花时间的不是增删改查而是这些课设里没教的事随便搜“基于SpringBoot的校园二手交易平台”能翻出一大堆Java项目源码但绝大多数人把压缩包解压后卡在三个地方数据库脚本导入报错、图片上传后页面显示不出来、一改代码就碰到SpringBoot版本兼容问题。这个项目的核心价值不在Controller里那几个CRUD方法——那些随便一个培训班都能教——真正的难点在数据库表怎么设计、订单状态怎么流转、上传的图片存哪。这篇文章就按我实际做这类项目的顺序从数据库设计讲到SpringBoot业务落地再把最容易翻车的几个坑按“现象→原因→解决”给你捋一遍。适合正在做毕业设计、课设或者想练手SpringBootMyBatis-Plus的Java工程师照着做能把一个能跑的二手交易平台源代码真正变成你能改、敢改、跑不崩的东西。2. 项目结构与技术选型先搞懂SpringBoot项目骨架再谈改源码2.1 典型的SpringBoot分层Controller、Service、Mapper到底怎么分才能不打架拿到一份SpringBoot二手交易平台的源码别急着启动先把包结构看一遍。正常这类项目会按“controller / service / mapper / entity / common / config”分层。Controller只负责收参数、调Service、返回统一结果Service里写业务逻辑Mapper对接数据库。很多课设源码的问题恰恰是Controller里直接写了业务代码一个方法几百行看着能跑但想加个“商品下架时自动通知买家”的功能就无从下手。Controller层我习惯用一个统一的返回类包装比如Result对象里面放code、message和data前端拿到code为200就知道请求成功了。这样做的直接好处是项目里所有接口的返回结构都一样前端Axios拦截器只需处理一种格式。管理端、用户端、小程序端都能复用同一套后端接口。改源码的人最怕什么最怕每个接口返回结构都不一样改一个功能要动三条链路。统一返回结构是在动手改任何业务之前必须做的一步。Service层要守住一个边界只处理业务不碰HttpServletRequest和MultipartFile。文件上传这种涉及Servlet对象的事放在Controller层处理完把存储路径传给Service。这样写的好处是Service能被单元测试直接调用不用Mock Servlet环境。比如发布商品时Controller接收图片文件先把文件存到本地磁盘再把图片的URL路径传给Service去落库Service根本不知道文件这回事。2.2 核心依赖与pom.xml版本选不对跑起来全是泪这类校园二手交易平台的pom.xml里通常有这几个关键依赖spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-java、lombok。用MyBatis-Plus而不是原生MyBatis的原因很简单——单表CRUD不用写XMLBaseMapper直接提供insert、deleteById、selectById、selectPage这些方法一个商品表、用户表、订单表的基础操作能省掉几十行XML。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies用SpringBoot 2.7.18而不是3.x这个选择是有讲究的。SpringBoot 3.x要求JDK 17起步很多课设环境还在JDK 8直接用3.x的脚手架启动就报“UnsupportedClassVersionError”。另外MyBatis-Plus的3.5.x对SpringBoot 2.x支持最稳到SpringBoot 3.x还得换mybatis-plus-spring-boot3-starter名称都不一样。初次接触这类项目的人看到“springboot版本太高”相关的报错九成是这个问题。数据库连接那一行mysql-connector-java的groupId在老版本里是mysql:mysql-connector-java8.0.33虽然也能用但新写法是com.mysql:mysql-connector-j我建议你直接写com.mysql省得Maven拉包时踩到老坐标的坑。2.3 application.yml配置端口、数据源、上传路径三个必调项拿到源码第一件事是改application.yml里的数据库连接。数据源配置看起来简单但校园二手交易平台最常见的一个NoteMySQL 8.x的驱动类是com.mysql.cj.jdbc.DriverMySQL 5.x是com.mysql.jdbc.Driver很多老源码写的是后者你用MySQL 8跑就会出现“Loading class com.mysql.jdbc.Driver is deprecated”的警告严重时直接启动失败。server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/campus_trade?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0multipart的max-file-size这一段特别容易被忽略。校园二手平台发布商品时通常要传多张图片手机拍的照片动不动就3-5MB默认的1MB上限会让你“上传成功”的提示还没出现后台就报MaxUploadSizeExceededException。max-file-size是单文件上限max-request-size是整个请求的总上限传4张图就按4倍去估。数据库连接串里的serverTimezoneAsia/Shanghai是给MySQL 8用的不写的话你本地和数据库时区不一致查询时间字段会差8个小时商品发布时间的显示就诡异了。MyBatis-Plus的log-impl配成StdOutImpl好处是在控制台直接看到每条SQL的完整日志。排查“查询条件没生效”这类问题时看控制台打印的Preparing和Parameters两行能直接确认MyBatis-Plus有没有把你的条件拼对。看清了再动手改代码比盲猜快得多。3. 数据库设计校园二手交易平台的表结构是源码能不能改得动的关键3.1 从需求反推核心表九张表的字段设计与关系先把需求捋一遍用户能注册登录、发布闲置商品、浏览搜索商品、下单购买、收藏商品、给商品留言。这个规模的项目表数量一般在8到10张。我按最常见的方案给你拆用户表、商品表、分类表、订单表、订单明细表、收藏表、留言表、轮播图表外加一张地址表用于收货。不要羡慕那些三张表就“跑通”的源码——商品信息、订单详情全塞在一张表里是能演示但改起来会痛不欲生。用户表和商品表之间是一对多关系一个用户能发布多个商品所以商品表里会有一个user_id外键指向用户表。商品表和分类表是多多对一一个分类下面有多个商品所以商品表里有一个category_id。订单表和商品表的关联要拆分订单表只存订单级的字段、总价、状态、下单时间订单明细表单独存每一件商品的快照信息。这里有个细节——订单明细表里要冗余一份商品的标题和图片路径因为商品可能被卖家下架甚至删除如果只在订单明细里存一个商品ID等商品删了订单记录的历史信息就成了一张白纸。3.2 商品表的设计状态字段、图片存储和索引取舍商品表是整个平台的核心字段设计要一次到位。id用BIGINT自增title是VARCHAR(80)存标题description用TEXT存详细描述price用DECIMAL(10,2)存价格原价和售价可以分开两个字段。图片字段我用VARCHAR(1024)存JSON数组比如[/images/goods/20240601/uuid1.jpg, /images/goods/20240601/uuid2.jpg]这样既不用为了“一个商品多张图”单独建一张图片表读出来又方便前端直接循环渲染。商品状态status字段用TINYINT0表示下架1表示在售2表示已被购买。注意这里有个细节被购买不等于下架订单没完成之前卖家的商品应该处于“已被拍下但未交易完成”的状态防止别的买家重复下单。等订单取消了或者完成了再回写商品状态。状态字段的值是什么含义一定在源码里写成常量比如GoodsStatusEnum别在Controller里写魔法数字1、2、3不然代码一长没人记得2是“已被购买”还是“审核中”。索引设计上商品表的category_id和status需要建普通索引因为首页和搜索页最常见的查询是“某个分类下所有在售的商品”SQL的WHERE条件往往长这样WHERE category_id ? AND status 1 ORDER BY create_time DESC。title字段上可以考虑全文索引但对课设规模的项目来说用LIKE %keyword%做模糊搜索已经够了数量级在万级以下性能压力可以接受不必上Elasticsearch。CREATE TABLE goods ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 商品ID, user_id BIGINT NOT NULL COMMENT 发布者用户ID, category_id BIGINT NOT NULL COMMENT 分类ID, title VARCHAR(80) NOT NULL COMMENT 商品标题, description TEXT COMMENT 商品描述, price DECIMAL(10,2) NOT NULL COMMENT 售价, images VARCHAR(1024) COMMENT 图片路径JSON数组, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态0下架 1在售 2已被拍下, view_count INT NOT NULL DEFAULT 0 COMMENT 浏览次数, deleted TINYINT NOT NULL DEFAULT 0 COMMENT 逻辑删除0未删 1已删, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category_status (category_id, status), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT二手商品表;deleted字段和good_status是两个完全不同的概念。deleted是逻辑删除标记用来防止用户删除商品后订单明细里的关联数据因为外键约束直接崩掉。逻辑删除的好处是数据不真删只是查询时被过滤掉。MyBatis-Plus里配了logic-delete-field后调用deleteById其实是执行UPDATE goods SET deleted 1 WHERE id ?查询时自动追加AND deleted 0。这是新手的重灾区——只见deleted字段以为商品被删了订单就查不到其实根本没动。3.3 订单与交易流程状态流转要在表结构里埋好线订单表的核心是status字段校园二手交易平台的订单状态比电商平台的简单一般就5个值0待付款、1已付款待发货、2已发货、3已收货、4已完成。要是做了取消功能再加一个5已取消。状态流转要单向推进不能跳比如订单不可能从“待付款”直接跳到“已完成”必须在代码里做状态校验。这也直接在源码的Service层体现上一章讲业务代码时会专门写。订单表本身要带一个order_no字段格式可以用时间戳加随机数比如202406011530001234。为什么不用自增ID直接当订单号因为订单号会暴露平台的真实订单量而且自增ID在联调时容易被人遍历。order_no可以设置唯一索引用UUID去重也可以但UUID做主键会导致索引碎片订单表的主键还是用BIGINT自增order_no单独做唯一索引就够了。地址表单独建还有一个好处——一个用户可以有多个常见收货地址。地址表里存user_id、收货人姓名、手机号、省市区、详细地址以及一个is_default字段。下单时用户从地址列表里选一个订单表里冗余存一份收货人快照而不是用地址表的外键关联。因为地址可能会在用户编辑后被覆盖甚至删除订单的收货信息一旦绑定地址表的实时数据历史订单的收货信息随时会丢失。4. SpringBoot业务代码落地商品发布、搜索分页与订单状态流转4.1 商品发布MultipartFile接收图片与存储路径设计先看Controller怎么收商品信息和图片。常见做法是用一个商品DTO同时接JSON字段和MultipartFile数组SpringBoot用RequestPart就能处理。注意商品信息的JSON字段当前端用FormData发请求时后端不能只写RequestBody必须用RequestPart(goods) GoodsDTO goods和RequestPart(files) MultipartFile[] files配合否则一接一个空对象。PostMapping(/goods) public ResultLong publishGoods(RequestPart(goods) GoodsDTO goods, RequestPart(files) MultipartFile[] files) { if (goods.getPrice() null || goods.getPrice().compareTo(BigDecimal.ZERO) 0) { return Result.error(价格不能为空或负数); } if (files null || files.length 0) { return Result.error(至少上传一张商品图片); } Long goodsId goodsService.publishGoods(goods, files); return Result.success(goodsId); }Controller层里做了两个校验价格不能是负数、图片至少一张。这些基础校验放Controller是合理的因为它们是请求入口第一道关Service层做的是更重的业务校验比如“分类是否存在”“用户是否有权限操作这个商品”。这样分工的好处是异常能早发现不让无效请求进到数据库层面同时Service层保持纯净不依赖Servlet的MultipartFile对象。存储这一层我一般会在配置里指定一个本地上传路径比如upload.dir生产环境改成OSS或者系统绝对路径。文件名用UUID重命名拼上原文件的后缀防止文件名重复覆盖。实际代码逻辑是读原始文件名→截取扩展名→拼接UUID扩展名→按日期分目录存储。public String saveImage(MultipartFile file) { String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String newFileName UUID.randomUUID().toString().replace(-, ) ext; String dateDir new SimpleDateFormat(yyyyMMdd).format(new Date()); String absoluteDir uploadDir File.separator dateDir; File dir new File(absoluteDir); if (!dir.exists()) { dir.mkdirs(); } File dest new File(absoluteDir, newFileName); file.transferTo(dest); return /uploads/ dateDir / newFileName; }按天分目录是因为校园二手平台上线一段时间后图片数量会冲到几千张全堆在一个目录里文件系统查找效率会下降。transferTo方法是Spring封装好的底层会把MultipartFile临时文件移动到目标位置。这里有个小问题如果目标文件已存在transferTo会直接覆盖UUID重命名后基本不会撞车但依然建议在调用前加File.exists()判断防一手。返回的路径要设计成URL能直接访问的映射SpringBoot默认的静态资源目录是classpath:/static/你自己加的本地磁盘路径不在里面所以必须加配置类做映射这一步在下一节避坑里单独展开。4.2 商品搜索与分页MyBatis-Plus的LambdaQueryWrapper组合查询校园二手交易平台首页和搜索页的查询条件通常是关键字、分类、价格区间、排序方式。用MyBatis-Plus的LambdaQueryWrapper能把这些组合条件串起来代码比写XML的动态SQL更直观。分页用Page对象直接selectPage。public PageGoodsVO searchGoods(String keyword, Long categoryId, BigDecimal minPrice, BigDecimal maxPrice, Integer sortType, int pageNum, int pageSize) { PageGoods page new Page(pageNum, pageSize); LambdaQueryWrapperGoods wrapper new LambdaQueryWrapper(); wrapper.eq(Goods::getStatus, 1) .eq(Goods::getDeleted, 0) .eq(categoryId ! null, Goods::getCategoryId, categoryId) .like(StringUtils.hasText(keyword), Goods::getTitle, keyword) .ge(minPrice ! null, Goods::getPrice, minPrice) .le(maxPrice ! null, Goods::getPrice, maxPrice); if (sortType ! null sortType 1) { wrapper.orderByDesc(Goods::getCreateTime); } else if (sortType ! null sortType 2) { wrapper.orderByDesc(Goods::getViewCount); } else { wrapper.orderByDesc(Goods::getId); } return goodsMapper.selectPage(page, wrapper); }这种写法的基础是LambdaQueryWrapper的条件构造。注意eq方法后面那个布尔参数——categoryId ! null时才会拼接这个等值条件keyword为空时like条件自动跳过。这个写法能省掉一大坨if判断而且是MyBatis-Plus最推荐的写法。orderByDesc是倒序按最新发布或浏览量排序。数据量到十万级以后为了排序性能可能得在create_time和view_count上建索引课设阶段不用管。4.3 订单状态流转Transactional与重复下单的兜底订单创建涉及两张表的写操作订单表插一条主单订单明细表插N条子单。这两步必须在一个事务里任何一个失败都要回滚。方法上加Transactional默认就对运行时异常生效遇到Exception的子类会自动回滚。但有一个细节如果代码里自己catch了异常没往外抛事务不会感知到数据就出现半截情况——订单主单有了明细没了。Transactional(rollbackFor Exception.class) public Long createOrder(Long goodsId, Long buyerId, Long addressId) { Goods goods goodsMapper.selectById(goodsId); if (goods null || goods.getStatus() ! 1) { throw new BusinessException(商品不存在或已下架); } int updated goodsMapper.updateStatusWithCondition(goodsId, 1, 2); if (updated 0) { throw new BusinessException(手慢了商品已被拍下); } Order order new Order(); order.setOrderNo(generateOrderNo()); order.setBuyerId(buyerId); order.setSellerId(goods.getUserId()); order.setTotalAmount(goods.getPrice()); order.setStatus(0); orderMapper.insert(order); OrderItem item new OrderItem(); item.setOrderId(order.getId()); item.setGoodsId(goodsId); item.setGoodsTitle(goods.getTitle()); item.setGoodsImage(getFirstImage(goods.getImages())); item.setPrice(goods.getPrice()); orderItemMapper.insert(item); return order.getId(); }updateStatusWithCondition是解决并发重复下单的关键。在商品表上执行“UPDATE goods SET status 2 WHERE id ? AND status 1”记录影响行数。如果返回0说明商品已经不是待售状态直接被其他买家抢先了。这个方案比先SELECT再UPDATE安全得多因为两个请求同时查到status1都往下走如果只用selectById判断就会有两张订单插进库。像这种“用UPDATE自带的行锁特性做并发控制”的办法是课设源码里很少见的写法但它是真实项目里处理超卖的标准姿势。5. 校园二手交易平台源码避坑5个跑不起来的典型问题与排查方法5.1 现象数据库连接失败启动报Communications link failure数据库连接串或驱动问题是最常见的启动报错源头。SpringBoot启动到一半突然抛“Communications link failure”或者“Public Key Retrieval is not allowed”别急着怀疑代码。拿你本地自带的MySQL命令行工具去连一下确认用户名密码是不是对的。我见过太多人拿着别人的源码改却把自己本机的密码写进application.yml密码对不上就开始折腾依赖版本方向完全错了。如果命令行能连上但程序连不上你要检查两件事。第一MySQL驱动和你的MySQL版本是不是匹配——MySQL 5.x用8.0的驱动能连但MySQL 8用5.x的驱动连不上或者报加密相关错误。第二连接串里是否加了useSSLfalse和allowPublicKeyRetrievaltrueMySQL 8默认的认证插件是caching_sha2_password驱动首次连接要拿公钥不放开这个参数就会报Public Key Retrieval is not allowed。建议直接用第四节里那串配置别删参数别省参数。5.2 现象MyBatis-Plus查得到数据但加了deleted1过滤后查不到逻辑删除碰上“手写SQL”是一个典型翻车点。你表格里没配logic-delete-field时手写的XML里又自己写了AND deleted 0这两个叠加看似没问题。但配置了逻辑删除后MyBatis-Plus会自动改写你的SQL把WHERE id ?这种条件追加成WHERE id ? AND deleted 0。如果你手写SQL里还带了deleted 0没问题如果手写SQL把表名写错了那可就是另一回事了不过这里先说最常见的——你以为自己“查不到数据”是因为逻辑删除生效了真正的排查法是看控制台打印出来的完整SQL。比如你在mapper XML里写了一条SELECT * FROM goods WHERE id #{id}MyBatis-Plus全局逻辑删除配置会让它变成SELECT * FROM goods WHERE id ? AND deleted 0这个效果是正确的。问题是你如果想连已删除的商品都查出来比如管理员后台要恢复商品就必须在XML里用InterceptorIgnore注解或者单独写一条不走逻辑删除的SQL。解决方式是在查询前调用QueryWrapper的last(and deleted 1)或者干脆在XML里写一条不经过MyBatis-Plus拦截器的原生SQL。5.3 现象图片上传成功浏览器直接访问却404这不是上传代码的问题是静态资源映射没配。SpringBoot对classpath:/static/目录下的静态资源会自动映射但你在配置里指定的本地磁盘路径upload.dir是系统路径不在SpringBoot的默认静态资源扫描范围内。这里的排查顺序是先看文件落盘在哪再确认URL路径和物理路径的对应关系。解决方案最常见的是写一个WebMvcConfigurer把/uploads/**映射到本地磁盘目录。Configuration public class WebMvcConfig implements WebMvcConfigurer { Value(${upload.dir}) private String uploadDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/uploads/**) .addResourceLocations(file: uploadDir /); } }addResourceHandler里的/uploads/**是URL访问路径addResourceLocations里的file:是磁盘映射路径。必须写file:前缀不能直接写路径不然SpringBoot会当成classpath资源去处理。配置好重启访问http://localhost:8080/uploads/20240601/uuid.jpg就能看到图片了。还有一个坑Windows下uploadDir如果是C:/upload末尾没有/你要手工拼一个斜杠不然映射的目录层级会错乱。如果这个也配了还是404去target/classes目录看看你的配置文件是不是没被重新编译进去IDEA里经常有这种旧配置残留。5.4 现象订单状态乱跳前端页面展示和数据库不一致订单状态乱。说白了就是代码里到处都在直接setStatus没有任何统一的状态机校验。用户在前端点“取消订单”时后端接口没有检查当前状态是不是“待付款”结果把“已完成”的订单也改成“已取消”了。这属于典型的业务状态失控。解决办法是写一个状态校验方法在每个改状态的地方先校验当前状态比如从“待付款”到“已取消”允许从“已发货”到“已取消”不允许。同时把所有状态流转的判断集中在一个枚举类里禁止在业务代码里散落魔法数字。课程设计里最容易看到的状态机实现是if-else硬编码那个方案在真实项目中是不可维护的采购方一眼就会打回。5.5 现象SpringBoot版本太高项目跑起来老是报错现在的脚手架默认给你拉SpringBoot 3.x很多老源码的代码是基于2.x的。SpringBoot 3.x除了要求JDK 17还有几处源码层面的改动。javax.servlet变成jakarta.servletspringfox的Swagger没法直接用得换springdocMyBatis-Plus得换mybatis-plus-spring-boot3-starter。这三处改动对于一个课设项目来说足够让你在环境配置上耗掉一个通宵了。如果你拿到一份源码先看pom.xml里SpringBoot的parent版本要是2.x就按住别升用JDK 8跑这是最省事的路子。要升到3.x那就一次性把javax改成jakarta把MyBatis-Plus换成新坐标把SpringFox换成springdoc-openapi。升版本这事用一句话总结就是项目能跑就别动版本换环境的成本远高于换代码的成本。6. 从能跑到好用给平台加一个定时下架与销售统计的进阶技巧项目能跑之后真正让它从“课设demo”变成“能给人演示的东西”我建议做一个小功能定时检查超过30天没有成交的闲置商品自动下架。这个功能用到SpringBoot的定时任务和订单表的数据查询十五分钟就能加上但很能体现项目完整度。先在启动类上加EnableScheduling然后新建一个TaskService用Scheduled(cron 0 0 2 * * ?)每天凌晨两点跑一次。查询条件用LambdaQueryWrapper查出所有status1但create_time小于30天前的时间点再把状态批量改成0。为什么用cron不用fixedRate因为这类任务每天执行一次就够了fixedRate是从上次开始时间算间隔cron是标准表达式适合精确控制。Component public class GoodsScheduleTask { Autowired private GoodsMapper goodsMapper; Scheduled(cron 0 0 2 * * ?) public void autoOffShelves() { LocalDateTime deadline LocalDateTime.now().minusDays(30); LambdaUpdateWrapperGoods wrapper new LambdaUpdateWrapper(); wrapper.eq(Goods::getStatus, 1) .lt(Goods::getCreateTime, deadline); Goods update new Goods(); update.setStatus(0); goodsMapper.update(update, wrapper); } }批量更新时用LambdaUpdateWrapper直接对符合条件的记录UPDATE成下架状态不用先selectPage再循环update。这个方案在数据量几百条时感觉不到差别到几千条时就有明显优势了。定时任务的日志要加上不然跑没跑你都不知道。线上环境还得担心任务重复执行的问题可以用分布式锁或者把任务放在独立的job模块里。课设阶段这一行cron加上去已经能拿出来说了。再补一个销售统计的思路按周统计成交量可以写成一条原生SQL。用DATE_FORMAT(order.create_time, %Y-W%u)做周分组统计每单的成交金额和数量商品维度可以join商品表按seller_id过滤。这个统计查询对订单表的索引有要求order.create_time上要建索引否则数据量一大这个统计接口会拖垮整个服务。我给类似项目统计时一般是再建一张daily_sales_summary表定时任务每天跑一次汇总查询就直接读汇总表不碰原始订单表。这些优化做完后还有一个容易被忽视的验证动作——把项目打包成jar放到一个没有安装IDE的目录里去启动。很多人的源码在IDEA里点着跑没问题命令行java -jar就崩原因不外乎target目录下不缺配置、日志路径写死成绝对路径、tomcat端口被占用。早点把这个流程跑通遇到问题早处理比最后演示前一小时手忙脚乱强太多。我自己的习惯是把启动命令写到一份README里jar包名和启动参数怎么写清楚自己过一个月再看这个项目也不用重新摸索。希望这些经验能帮你在改校园二手交易平台这类SpringBoot项目时少走一段弯路。本文还有配套的精品资源点击获取