做了几年 Spring Boot 项目,其实我有个很深的体会:“小区蔬菜水果商城系统”这种题目,看起来就是一套普通电商的增删改查,但真动手做的时候,你会发现“小区”和“蔬菜水果”这两个词,才是决定整个系统设计走向的关键。
先说结论:这是一个非常适合 Spring Boot 入门到进阶的实战项目。它覆盖了用户端、管理端、订单交易、库存、支付、文件存储、缓存、部署这些完整链路,技术栈又不会难到劝退。但同时,它和那些卖标品、数码产品的电商系统有很多细节差异——生鲜有损耗、有配送时效、有库存实时性要求,小区场景又有典型的“小范围、高频次、强时效”特征。如果只是照搬一套通用商城,做完会发现很多地方是拧巴的。
这篇博客我会从实际开发角度,把这个系统的功能边界、表结构、核心交易链路、文件存储、部署兼容性,一步步拆开讲,顺便把我在这个项目里踩过的坑、最后怎么解决的,也一起交代了。适合正在做毕设、想拿一个完整项目练手、或者准备面试时需要一个电商类项目经验的开发者参考。
1. 这个小区的商城系统,和普通电商到底差在哪
先把业务想清楚再写代码,这是我做项目一贯的顺序。不然代码写了一半,需求一变,返工成本很高。
1.1 两个关键词带来的设计约束
“小区”意味着什么?意味着配送范围极小、用户群体集中、复购率高。用户可能今天买几个西红柿,明天买一把青菜,下单频率比淘宝那种“想起来了买一次”高得多。所以这个系统对操作便捷度的要求很高——商品浏览要快、下单要快、购物车要好用,你总不能让人家每次买个菜还要像逛淘宝一样翻半天。
“蔬菜水果”意味着什么?意味着SKU多但库存浅、保质期短、价格波动频繁。白菜不会因为你在系统里设置了1000件库存就真的放1000件在货架上,它今天进多少货、卖完就下架,剩下的晚上还要打折清掉。所以库存字段的设计、上下架的逻辑,比起标品电商要更灵活。
这两个约束落到系统设计上,就是几件事:
- 用户的默认收货地址基本就是小区内部,甚至可以是自提点,配送逻辑不需要复杂的物流系统。
- 商品的库存数量要小,但上下架状态要优先级很高,库存为0自动下架是刚需。
- 需要支持配送时段,比如“17:00-18:30送达”,因为社区团购和生鲜配送有这个典型交互。
- 订单状态不能照搬淘宝那套“待发货/已发货/已签收”,应该更贴合“待支付/备货中/配送中/已完成”。
1.2 功能边界:先做核心闭环,别一上来就造大车
很多初学者拿到题目后,第一反应是我要做会员积分、要做拼团砍价、要做秒杀、要做多商家入驻。我劝你冷静。一个毕设或者练手项目,最忌讳的就是功能清单拉得很长,结果每个模块都是半成品。
我当时给自己定的范围是这样的:
用户端:注册登录、商品分类浏览、商品搜索、购物车、下单、支付(支付宝沙箱)、订单列表与详情、收货地址管理。
管理端:商品管理(含上下架、库存调整)、分类管理、订单管理(发货/配送状态流转)、会员列表、基础数据统计(销量、销售额、热销商品)。
这个范围跑通之后,核心交易链路是完整的:上架商品 → 用户浏览 → 加购物车 → 提交订单 → 支付 → 管理员备货/配送 → 用户确认收货。至于拼团、秒杀、优惠券这些营销玩法,属于第二期的事,先把地基打好。
提示:对毕设来说,完整跑通的核心闭环,远比一堆“有界面但没逻辑”的功能值钱。面试官或者答辩老师问起来,你能把一个闭环讲清楚,每个表字段、每个状态都说得明明白白,这是加分项。
2. 表结构设计:库存、订单和商品分类怎么建模
表结构是这个系统的地基,设计得好,后面写业务代码就是顺畅的流水线;设计得不好,后面每写一个查询都在跟表结构较劲。
2.1 核心四大表:用户、分类、商品、购物车
先看不那么复杂的三张表。
用户表没太多花样,字段基本就是用户名、密码(BCrypt加密)、手机号、角色(USER/ADMIN)、状态、创建时间。注意角色字段,因为管理端和用户端共用一张用户表,后面做权限拦截时就是靠这个字段区分的。
分类表要注意的是层级问题。蔬菜水果这个场景,分类往往是两级的:一级是“蔬菜”“水果”“肉禽蛋”“粮油调味”,二级是“叶菜类”“茄果类”“根茎类”这种。我用的是经典的 parent_id 方案,父分类的 parent_id 为0,查询时先用一级分类加载二级分类列表。
商品表是整个系统里字段最多的表之一。我当时的字段大致是:商品名称、分类ID、主图、轮播图(多张用逗号分隔或JSON数组)、详情描述、单位(斤/份/个)、原价、现价、库存、销量、上架状态、创建时间、更新时间。
这里有几个细节值得敲黑板:
- 价格用 DECIMAL(10,2),绝对不要用 DOUBLE。浮点数做加法减法会出现 0.1+0.2=0.30000000000000004 这种问题,钱这种东西一分钱都不能差。
- 主图和轮播图分开存。主图用于列表页小图,轮播图用于详情页大图。我见过不少项目把所有图塞进一个字段,最后前端展示很难受。
- 库存和销量分开。有些人图省事,把库存做成“总库存-销量”算出来,然后直接用销量去减。这样一旦有退货、上下架操作记录,数据就乱了。
购物车表的设计有不同方案,有人说用 Redis,有人说用数据库表。我最终选了数据库表,理由很实在:购物车是强数据,用户加了几件商品,不能因为 Redis 缓存过期就没了。而且这个系统的购物车查询量不大,数据库完全扛得住,还能直接联表查商品名称、图片、价格。
2.2 订单与订单明细:一主多从的经典结构
订单相关的设计,“订单主表 + 订单明细表”是标配,一主多从。
订单主表关注的是这笔交易的整体信息:订单号、用户ID、总金额、实付金额、支付方式、支付时间、订单状态、收货人、收货地址、备注、创建时间。
订单明细表关注的是商品维度的信息:订单ID、商品ID、商品名称(下单时快照)、商品图片(下单时快照)、单价、数量、小计金额。
这里最重要的一个设计理念:订单明细里的商品名称、价格、图片,必须是在用户下单那一刻存下来的快照,下单之后不要再SQL去商品表联查。
为什么?因为商品表是时刻在变的。今天白菜卖3块,你下单时是3块,明天涨到5块了,你的订单明细如果去联查商品表,显示出来的单价就变5块了,这订单还怎么对账?这种问题开发阶段不会暴露,上线运营了必然出事。所以下单时把商品关键信息冗余到订单明细表,这是电商系统里人尽皆知的规则,但也恰恰是第一次做商城最容易忽略的地方。
订单号我用了时间戳+随机数的方案,比如20250112103045加几位随机数。别直接用数据库自增主键当订单号,会暴露你的订单量,而且生成规则太容易被猜。雪花算法当然更好,但毕设项目没必要引入额外依赖,时间戳加随机数够用。
2.3 库存扣减:谁减库存,什么时候减
这是一个在系统设计阶段就要定下来的关键决策,它的影响贯穿整个订单流程。
我把规则定成了支付成功才扣减库存,而不是下单时预占库存。原因也简单:
- 下单但不支付,如果预占库存,那么这批货就被“冻住”了,用户不支付、定时任务又没及时释放,真实库存是够的但系统里显示没货。
- 生鲜类商品库存浅,预占机制带来的一致性处理成本偏高,对毕设项目不友好。
- 支付时扣库存,配合“超时未支付订单自动关闭”的定时任务,逻辑上更直观。
当然这个方案有自己的代价——存在超卖窗口。两个用户同时为一个商品提交订单并支付,理论上可能出现库存只剩1件但两单都支付成功的情况。解决这个问题的经典方案,后面第四章我会详细讲,这里先说结论:用数据库的乐观锁做库存扣减判断,是最适合这个项目量级的方案。
3. 工程搭建与基础配置:从一个干净的Spring Boot工程说起
业务设计完了,开始搭工程。这一章讲的是我从零到能跑起来的完整过程,包括目录怎么分、依赖怎么选、配置怎么写。
3.1 分层结构与包命名
项目用的是经典的三层架构 plus 扩展:controller 层(接收请求)、service 层(业务逻辑)、mapper 层(数据库操作)、entity 包(实体类)、dto 包(数据传输对象)、vo 包(视图对象)、config 包(配置类)、common 包(统一返回结果、异常处理、工具类)。
com.example.freshshop ├── controller // 接口层 ├── service // 业务逻辑层 ├── mapper // 数据库访问层 ├── entity // 实体类 ├── dto // 入参对象 ├── vo // 出参对象 ├── config // 配置类 └── common // 统一返回、异常、工具类实体类和 DTO/VO 一定要区分开,这是我见过太多项目做烂的地方。实体类里的字段对应数据库字段,一个萝卜一个坑;DTO 是接收前端传参用的;VO 是返回给前端用的。有些人不区分,直接拿实体类去接收前端参数,一旦前端传了一个你数据库里没有的字段,MyBatis-Plus 做插入时可能直接报错或者产生脏数据。
3.2 依赖选型:Maven 管理,别什么都塞进来
Maven 项目的构建就是维护好 pom.xml,这块值得花点心思。
我的核心依赖不算多:Spring Boot 基础包、Spring Web、Spring Validation(参数校验)、MyBatis-Plus(数据库操作)、MySQL 驱动、Redis 客户端(Spring Data Redis)、Lombok、JWT 相关库(jjwt)、Hutool(工具类)、MinIO SDK(文件存储)。
提示:用 MyBatis-Plus 而不是原版 MyBatis,不是因为原版不好,而是它的内置 CRUD、分页插件、条件构造器能让开发效率提升很多。商城系统的数据库操作大部分是单表操作,MyBatis-Plus 写起来非常顺手,而且它对新手极其友好,几乎不需要手写 SQL。
另外建议加上 spring-boot-starter-validation,参数校验这个事千万别小看。比如下单接口,前端传的商品 ID 可能是不存在的、购买数量可能是负数、收货地址可能是空字符串。不校验,脏数据就进了数据库,后期排查成本非常高。
3.3 application.yml 核心配置与常见坑
配置文件是整个服务能不能跑起来的关键。我当时的配置要点大致如下:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/fresh_shop?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 database: 0 servlet: multipart: max-file-size: 10MB max-request-size: 50MB mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0几个容易踩的坑,逐一说明:
MySQL 连接串必须有 serverTimezone 参数。不然后面连接数据库时会报时区错误,那个报错信息对新手特别不友好,字面意思完全看不出来是这个参数的问题。
map-underscore-to-camel-case 一定打开。数据库字段比如create_time,Java 属性是createTime,这个配置打开后 MyBatis-Plus 才能自动做映射。不然你查出来一个字段是一个字段,赋值全为 null。
multipart 上传限制要主动设置。Spring Boot 默认文件上传大小是 1MB,商品图片随便一张手机拍的图就超了,一开始不设置后面传图必报MaxUploadSizeExceededException。
3.4 统一返回结果与全局异常处理
前后端联调最怕的就是每个接口返回格式都不一样,前端解析要写一堆 if else。所以我在 common 包里做了一个统一的返回结构:
public class Result<T> { private Integer code; private String message; private T data; }正常返回时 code 是 200,业务异常时 code 按错误类型定义,比如 401 未登录、403 无权限、500 系统错误。前端拿到 code 非 200 的情况直接弹 message 就行,非常简单。
全局异常处理用@RestControllerAdvice实现。业务异常、参数校验异常、兜底异常分别写处理方法,避免异常信息直接暴露给前端——对毕设项目来说,给用户看 “NullPointerException: null” 这样的堆栈信息,既不专业也更危险。
4. 核心交易链路:认证、缓存、下单与库存扣减
前面的都是底座,真正有含金量的业务逻辑在这个部分。
4.1 登录认证:JWT + 拦截器
登录方案我用了 JWT,这也是现在 Spring Boot 项目的主流选择。
流程是这样:用户登录成功后,后端生成一个 JWT 令牌返回给前端;前端后续请求在 Header 里带上Authorization: Bearer <token>;后端注册一个拦截器,每次请求先解析 token,解析失败直接返回 401 未登录,解析成功把用户信息放进 ThreadLocal 里供后续业务取用。
拦截器里必须区分管理员和普通用户。比如/api/admin/**开头的接口,先验证 token,再验证角色是否是 ADMIN,不是就返回 403。这个思路对保护管理端接口非常关键——商品上下架、订单状态流转这些操作如果被普通用户调用,整个系统就失控了。
我在 token 里放的是用户 ID 和角色,有效期设为 24 小时,管理端的 token 有效期短一些,这是安全上的小讲究。
4.2 商品列表与 Redis 缓存
商品列表是这个系统访问量最大的接口,首页、分类页、搜索页都要查。数据库本身扛得住,但把热数据缓存到 Redis 里,响应速度能从几十毫秒降到个位数毫秒,这种优化做起来不难,性价比很高。
我的缓存策略是分类维度缓存。比如访问“蔬菜”分类下的商品列表,缓存 key 是goods:list:category:1,第一次请求从数据库查,然后写入 Redis 并设置 10 分钟的过期时间,后续请求直接读缓存,直到过期或者管理员主动更新商品信息时删除对应缓存。
这里有一个核心问题:缓存和数据库的一致性怎么做?
我的处理方式是删缓存而不是更新缓存。管理员把白菜的价格从3块改成5块时,我先更新数据库,然后删掉goods:list:category:1这个缓存 key,用户下一次请求发现缓存 miss,就会从数据库拉最新数据再把缓存建好。这是业界经典的 Cache Aside 模式,简单可靠,适合这个项目。
复杂的一致性方案像延迟双删、读写锁、消息队列串行化,在这个项目里属于杀鸡用了牛刀,理解更深的同学可以用,但毕设阶段没有太大必要。
4.3 下单流程:多个步骤如何保证数据安全
创建订单的核心逻辑,我拆成了下面几步:
- 校验用户登录状态
- 从购物车或“立即购买”传入的商品列表里,查出商品最新数据
- 校验商品是否存在、是否上架、库存是否充足
- 按实时价格计算总金额
- 生成订单号和订单明细快照
- 扣减库存
- 清空购物车中对应的商品
这个链路里最要小心的就是库存扣减。
第一种写法是经典的超卖写法:
Goods goods = goodsMapper.selectById(goodsId); if (goods.getStock() >= quantity) { goods.setStock(goods.getStock() - quantity); goodsMapper.updateById(goods); }两个用户同时查到库存是1,都判断能买,然后都去更新,最后数据库里库存变成0,但产生了两个订单。这就是超卖。
这个场景我没用悲观锁(SELECT FOR UPDATE),而是用了数据库乐观锁,给商品表加一个 version 字段,更新时带着旧的 version 去更新,更新的 SQL 里带上 version 条件:
int count = goodsMapper.updateStock(goodsId, quantity, version); if (count == 0) { throw new BusinessException("库存不足或商品已变更,请重新下单"); }对应的 SQL 思路是:
UPDATE goods SET stock = stock - #{quantity}, version = version + 1 WHERE id = #{goodsId} AND version = #{version} AND stock >= #{quantity}affected rows 为0说明要么版本号变了,要么库存不够,此时直接让用户重新下单。这个方案在并发量不大的小区生鲜场景里非常稳,而且代码量极少。
4.4 超时未支付订单的关闭机制
用户下单后如果一直不支付,订单会占着系统资源。我当时的方案是 Spring 定时任务 + 状态判断,每 30 秒扫描一次“已创建但超过 15 分钟未支付”的订单,把它们状态改成已关闭,同时把当时扣减的库存加回来。
@Scheduled(fixedDelay = 30000) public void closeExpiredOrders() { ... }这里要提醒一下:**这个方案靠轮询扫描数据库,在大规模场景下效率不高,但在这个项目量级下完全够用,而且实现简单、可解释性强。**如果在毕设答辩中老师问“为什么不用延迟消息队列”,你可以说这个项目的订单量级使轮询不会带来性能瓶颈,同时轮询方案更稳定,不引入额外中间件也降低了系统复杂度。如果你学有余力,引入 RabbitMQ 的延迟队列或者 Redis 过期监听来做,确实是更优雅的方案,但绝不是必选项。
5. 商品图片与文件上传:MinIO接入Spring Boot的完整方案
商品图片的管理,看着不起眼,做不好很影响体验。图片不能存数据库,BLOB 很蠢;也不能就直接丢到项目本地目录,因为项目一旦打包部署,新上传的图片只要服务重启或者容器重建就丢了。
所以我用了 MinIO——一个开源的对象存储服务,兼容 S3 接口,非常轻量。
5.1 为什么是 MinIO 而不是 FastDFS
很多教程喜欢用 FastDFS,但我个人更推荐 MinIO。原因有三点:
第一,MinIO 部署极其简单,就是一个 Docker 命令,而 FastDFS 要启动 tracker 和 storage 两个服务,配置相对麻烦。
第二,MinIO 自带了一个 Web 管理界面,上传的文件可以直接在浏览器里看,排查问题很方便。
第三,MinIO 官方提供了 Java SDK,接入 Spring Boot 非常顺滑,代码写起来很直观。
5.2 MinIO 的部署与集成
部署用 Docker 一条命令就能完成:
docker run -d -p 9000:9000 -p 9001:9001 \ --name minio \ -e "MINIO_ROOT_USER=minioadmin" \ -e "MINIO_ROOT_PASSWORD=minioadmin" \ -v /opt/minio/data:/data \ minio/minio server /data --console-address ":9001"9000 是 API 端口,9001 是管理控制台端口。启动后先到控制台创建一个 bucket,比如叫fresh-shop,并且把访问策略设置为 public。因为商品图是要被用户直接访问的,如果没有 public 权限,图片 URL 即使生成出来也无法在浏览器里加载。
Spring Boot 集成时,我建了一个配置类:
@Configuration public class MinioConfig { @Bean public MinioClient minioClient() { return MinioClient.builder() .endpoint("http://localhost:9000") .credentials("minioadmin", "minioadmin") .build(); } }上传接口的核心逻辑如下:
public String upload(MultipartFile file) { String extension = FilenameUtils.getExtension(file.getOriginalFilename()); String objectName = UUID.randomUUID() + "." + extension; minioClient.putObject(PutObjectArgs.builder() .bucket("fresh-shop") .object(objectName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); return objectName; }文件名用 UUID 生成,不要用用户上传的原始文件名。两个原因:一个是防乱码,中文文件名在 URL 里会出现编码问题;另一个是防止覆盖,不同用户上传两张都叫“葡萄.jpg”的图片,不能互相覆盖。
5.3 图片访问的坑
这里有两个坑,是我实际踩到的:
第一,对象名和URL的拼接。如果桶策略是 public,那么图片URL就是http://localhost:9000/fresh-shop/图片对象名。但对象名里如果包含目录前缀,比如2025/01/12/uuid.jpg,URL需要完整拼接目录前缀。我最终选择不使用目录前缀,直接拉平存,以当前量级完全够用,省去一堆URL拼接的麻烦。
第二,内网地址与外网地址的问题。本机测试时上传URL是http://localhost:9000,但部署到服务器后,这个地址必须改成服务器的公网地址,不然后端上传接口返回的图片URL用户无法访问。我先把这个配置做成了常量,也建议改成配置项,部署时改一处就行。
6. 打包部署与版本兼容:从本机到Docker
本地跑通不是终点,部署到服务器上稳定运行才是。这个环节看似枯燥,实际坑比写业务代码还多。
6.1 版本选择:Spring Boot 2.7.x 还是 3.x
这是个现实问题。如果你用的是 JDK 8,老老实实选 Spring Boot 2.7.x;如果你用 JDK 17,可以上 3.x。
热搜里经常看到“springboot版本太高”的抱怨,本质上是这样:Spring Boot 3.x 是建立在 JDK 17 和 Jakarta EE 9 基础上的,很多老项目的第三方库和写法在 3.x 下跑不了。比如 javax.servlet 变成 jakarta.servlet,这个改动导致大量基于传统 Servlet 的第三方组件要大改才能兼容。
我当时选的是 Spring Boot 2.7.x + JDK 8。原因很实在:MyBatis-Plus、MinIO SDK、各种工具类对 2.7.x 的兼容性经过大量验证,资料最多,踩坑后能搜到答案的概率最高。这个项目是毕设或练手项目,没必要为了新版而新版。
6.2 经典流程:Vue前端打包放进Spring Boot
这个系统的前端我用的是 Vue,开发阶段前后端分离,前端跑 8081 端口,后端跑 8080 端口,通过代理解决跨域。但部署阶段不可能搞两个服务,最省事的方案是让 Spring Boot 同时提供静态页面和 API。
具体做法非常简单:
- 前端执行
npm run build,产出 dist 目录 - 把 dist 目录下的所有文件复制到 Spring Boot 的
src/main/resources/static目录下 - 重新打包 Spring Boot,一个 jar 搞定所有内容
这种做法部署简单,一个进程托管整个应用,特别适合个人项目。但要确认前端里的 API 请求地址必须是相对路径,比如/api/goods/list,而不是http://localhost:8080/api/goods/list。不然前端页面虽然打包进了后端,但请求还是发到 localhost,在服务器上必然跑不通。
6.3 Docker Compose 编排整个环境
我最终用 Docker Compose 把 MySQL、Redis、MinIO、应用服务四个组件编排在一起。这一步对毕设来说属于加分项,但对理解现代部署方式帮助很大。
version: '3.8' services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: 123456 MYSQL_DATABASE: fresh_shop ports: - "3306:3306" volumes: - mysql-data:/var/lib/mysql redis: image: redis:7-alpine ports: - "6379:6379" minio: image: minio/minio command: server /data --console-address ":9001" ports: - "9000:9000" - "9001:9001" app: build: . ports: - "8080:8080" depends_on: - mysql - redis - minio应用服务的 Dockerfile 也很常规:
FROM openjdk:8-jre COPY target/fresh-shop.jar /app.jar ENTRYPOINT ["java", "-jar", "/app.jar"]这里有一个细节我踩过坑:启动顺序问题。Spring Boot 应用启动时就要连数据库和 Redis,如果 MySQL 容器还没完全就绪,应用会反复报连接失败然后启动失败。我最后的处理方式是给应用加一个重启策略,配合 Docker 默认的延时机制,基本能解决并发启动的问题。你甚至可以写一个简单的 wait-for-it 脚本,但多数情况下重启策略已经够用。
6.4 springdoc 的坑:不需要的接口文档插件直接关掉
搜索热词里提到的 “springboot怎么关闭springdoc” ,我正好遇到过。有时候你引入了一个全链路追踪的框架或者网关,它会自动带进去 springdoc,然后每次请求都多出一些文档相关的处理逻辑,还会绑定一些意外端口。如果项目中确实用不到这些接口文档能力,关闭方式很简单:
springdoc: api-docs: enabled: false或者直接用依赖排除。这个坑提醒我们:引入依赖的时候一定要想清楚它到底带了多少附加组件,建议每次引入新依赖后启动一次服务,看控制台里多出的自动配置,心里有数。
7. 复盘:如果重新做一遍,我会在哪些地方重点投入
最后一部分,聊点项目做完后的回头思考。这些也是我在答辩和工作面试里最常被问到的问题。
7.1 肯定要好好准备的三个细节
第一,事务要加对地方。创建订单操作涉及写订单表、写订单明细表、扣库存、清购物车,这四个操作必须放在同一个数据库事务里。我用的是@Transactional(rollbackFor = Exception.class),注意带上 rollbackFor 参数,这样遇到 RuntimeException 之外的异常时也会回滚。
第二,参数校验要全面。下单接口,前端传过来的商品数量如果是一个负数或者一个天文数字,后端必须拦下来。我见过很多项目在入参时完全不做校验,导致库存变成负数或者金额对不上,排障排到怀疑人生。
第三,订单状态的流转要有完整记录。至少要在管理端能看出“这个订单当前在哪个环节、什么时候创建的、什么时候支付的、什么时候配送的”。如果后面加上状态变更历史表,就是一个加分项。
7.2 一定要避开的三个设计坑
一是不要把图片存数据库。BLOB字段存图片,数据库越来越庞大,备份慢、查询慢,毫无好处。MinIO方案只需要存一个对象名。
二是不要忽视逻辑删除与唯一索引的冲突。用了 MyBatis-Plus 的逻辑删除后,同一个商品如果删了又新增,数据库里会存在两条记录,一个 deleted=1 一个 deleted=0。这种情况下,商品名称无法建唯一索引,因为你会存两条同名记录。这个问题的处理方案是自己维护一张“已删除记录归档表”或者允许重名,二选一,根据业务需求取舍。
三是不要为了功能数量牺牲完成度。一个能跑通全流程的、所有按钮真实可用的、数据能对上的系统,在答辩或者面试时的说服力,远大于十个只做了页面没做逻辑的模块。
7.3 下一步还能怎么扩展
如果做完上面这些还有余力,我有几个扩展方向建议,按性价比排序:
第一个是引入 RabbitMQ 或者 ActiveMQ 做异步通知。比如用户支付成功后,发短信通知管理员备货,或者订单状态变更事件驱动缓存更新。搜索热词里“springboot整合activemq”“springboot远程调用”指的就是这类能力。第二个是加一个 JVM 本地缓存(比如 Caffeine)配合 Redis 做二级缓存,再往上引一个 Spring Cache 抽象,对读多写少的首页能有更明显的性能提升。
第三个是数据统计模块做得更丰富一点,比如近7日销量走势、分类销售占比这种,用 ECharts 画图表展示,对答辩很加分。
我个人做过几个类似的商城项目之后,最大的体会是:写一个能跑的项目不难,难的是每一个细节都经得起追问。库存为什么这么扣、订单为什么要存快照、缓存为什么删而不更新、事务加在哪里,这些问题的答案,决定了你做完这个项目之后是“会写代码”还是“理解系统设计”。这套小区蔬菜水果商城系统,恰好是能把这一整条链路都串联起来的好项目。做一遍,收获会很大。