ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

高校社区生鲜配送系统源码解析:Spring Boot+Vue3实现订单状态机与库存扣减

高校社区生鲜配送系统源码解析:Spring Boot+Vue3实现订单状态机与库存扣减

简介:高校社区生鲜配送系统是一套面向大学生及周边社区居民的在线生鲜购物平台源码,适合计算机专业学生、Java Web开发者用于课程设计、毕业设计或企业级项目练手。系统围绕用户管理、商品管理、订单处理、库存控制、配送调度、支付接口、数据分析、客户服务与移动端适配等模块展开,完整呈现从下单、支付到配送签收的业务闭环,并涉及库存预警、保鲜期管理与个性化推荐等实用逻辑。资源包共909个文件,包含184个html页面、152个class与120个java源文件、134个css与122个js脚本,另有121张png及38张gif界面素材、18个json配置、1个sql建表脚本和2个yml部署文件,压缩后约1.61MB,前后端与数据库结构齐全。已有139人学习下载,可帮助读者快速理解生鲜电商系统的分层设计与模块协作方式,并在此基础上进行二次开发与功能扩展。

1. 从一份高校社区生鲜配送系统源码说起:它到底能跑出什么

有次帮一个做校园创业的团队看后端,他们想做一个“宿舍楼下自提+次日达”的生鲜小程序,预算不多,问我有没有能直接改的底子。我翻到这份《高校社区生鲜配送系统.zip》,解压后第一反应是:这不是那种只有登录页和空壳首页的“演示级”项目,它把生鲜配送里最麻烦的几件事——商品分拣、按楼栋合并订单、配送时段切分、自提点核销——都落成了可运行的模块。它适合两类人:一类是想拿现成骨架快速搭一个校园生鲜平台的开发者,另一类是想学“订单状态机+库存扣减”怎么在真实业务里落地的人。技术栈上,后端是 Spring Boot + MyBatis-Plus,前端是 Vue3 + Vite,数据库 MySQL,缓存 Redis,整体是前后端分离的单体架构,部署门槛不高,一台 2 核 4G 的云主机就能跑起来。下面我按“能干什么 → 怎么跑 → 怎么改”的顺序,把这份源码拆开讲。

2. 架构与目录结构:先看清模块边界再动手

2.1 技术选型与分层设计

这个项目没有上微服务,是典型的单体分层:Controller 层只做参数校验和路由,Service 层承载业务规则,Mapper 层用 MyBatis-Plus 做单表 CRUD,复杂查询走 XML。选 MyBatis-Plus 而不是 JPA,常见做法是因为生鲜订单的查询条件太碎——按楼栋、按时段、按自提点、按状态组合筛选,用 Wrapper 拼条件比写 JPQL 顺手。Redis 主要用在两个地方:一是商品库存的预扣减,二是自提点核销码的短期缓存。前端 Vue3 用 Pinia 管状态,请求统一走 axios 封装,拦截器里处理 token 和错误提示。

分层的好处是改需求时不用满项目找逻辑。比如“满 30 元免配送费”这种规则,只动 Service 层的一个方法,Controller 和 Mapper 都不用碰。

2.2 目录结构逐层说明

解压后根目录大致是这样:

src/main/java/com/campus/fresh/ ├── controller/ # 接口入口,按业务域分:OrderController、ProductController ├── service/ │ ├── impl/ # 业务实现,订单状态流转、库存扣减都在这里 │ └── OrderService.java ├── mapper/ # MyBatis-Plus Mapper 接口 ├── entity/ # 数据库实体,和表一一对应 ├── dto/ # 入参出参对象,避免实体直接暴露给前端 ├── config/ # Redis、跨域、拦截器配置 └── common/ # 统一返回体、异常处理、常量 src/main/resources/ ├── mapper/ # 复杂查询的 XML ├── application.yml # 数据源、Redis、端口配置 └── sql/init.sql # 建表 + 初始数据 frontend/ ├── src/views/ # 页面:商品列表、购物车、订单、自提核销 ├── src/api/ # 接口封装 └── src/store/ # Pinia 状态

这个结构里最值得先看的是service/impl/OrderServiceImpl.java和sql/init.sql。前者是业务核心,后者决定了你初始化后能不能直接看到数据。很多源码包跑不起来,问题就出在 init.sql 没执行或者字段对不上。

2.3 数据库表关系速览

核心表有六张:user(用户)、product(商品)、category(分类)、cart(购物车)、orders(订单主表)、order_item(订单明细)。订单和明细是一对多,商品和分类是多对一。自提点信息直接冗余在 orders 表里,用pickup_point字段存,没有单独建表——这是单体项目里常见的简化,改多自提点时再抽表也不迟。

提示:init.sql 里默认插了一个管理员账号和几个测试商品,先跑通再改数据,别一上来就清库。

3. 本地跑起来:从建库到前后端联调

3.1 环境准备与依赖版本

需要 JDK 8 或 11、Maven 3.6+、MySQL 5.7/8.0、Redis 5+、Node 16+。版本别乱升,Spring Boot 2.7 配 JDK 17 容易在启动时卡在反射相关的地方。我一般先用java -version和mvn -v确认,再动配置文件。

3.2 数据库初始化与配置修改

先建库再导数据:

mysql -u root -p -e "CREATE DATABASE campus_fresh DEFAULT CHARSET utf8mb4;" mysql -u root -p campus_fresh < src/main/resources/sql/init.sql

然后改application.yml里的三处:

spring: datasource: url: jdbc:mysql://localhost:3306/campus_fresh?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的密码 redis: host: localhost port: 6379 server: port: 8080

serverTimezone必须写,否则订单创建时间会差 8 小时,这个坑我踩过不止一次。Redis 如果没设密码,password留空即可。

3.3 后端启动与接口验证

mvn clean package -DskipTests java -jar target/campus-fresh-0.0.1-SNAPSHOT.jar

启动后看日志有没有Started Application。然后用 curl 验证商品列表接口:

curl http://localhost:8080/api/product/list?page=1&size=10

返回体里code为 200 且data.records有数据,说明后端和数据库通了。如果返回 401,检查拦截器是否把该接口放行——config/WebConfig.java里配了白名单。

3.4 前端启动与跨域处理

cd frontend npm install npm run dev

Vite 默认跑在 5173。前端vite.config.js里配了 proxy,把/api转发到 8080,所以本地开发不用改后端跨域配置。如果 npm install 卡住,换淘宝源:npm config set registry https://registry.npmmirror.com。

注意:前后端都起来后,先走一遍“加购物车 → 下单 → 查看订单”的完整链路,别只刷首页。链路通了,后面改代码才有底。

4. 核心模块拆解:订单状态机与库存扣减怎么写的

4.1 订单状态流转的实现

生鲜订单的状态比普通电商多一层:待支付 → 已支付 → 分拣中 → 待自提 → 已完成,外加一个已取消。状态流转集中在OrderServiceImpl的updateStatus方法里,用枚举约束合法迁移,防止前端乱传状态把订单改飞。

public void updateStatus(Long orderId, OrderStatus target) { Orders order = orderMapper.selectById(orderId); // 校验当前状态能否迁移到目标状态 if (!order.getStatus().canTransferTo(target)) { throw new BizException("订单状态不允许此操作"); } order.setStatus(target); order.setUpdateTime(LocalDateTime.now()); orderMapper.updateById(order); // 进入待自提时生成核销码,写入 Redis 并设 24 小时过期 if (target == OrderStatus.WAIT_PICKUP) { String code = RandomUtil.randomNumbers(6); redisTemplate.opsForValue().set("pickup:" + orderId, code, 24, TimeUnit.HOURS); } }

canTransferTo是枚举里的方法,用 switch 判断合法路径。这样改需求时只动枚举,不用在 Service 里堆 if-else。核销码放 Redis 而不是数据库,是因为它有时效性,过期自动清,省得写定时任务。

4.2 库存预扣减与回滚

生鲜最怕超卖。这个项目没有用分布式锁,而是在下单时用 Redis 的decrement做原子预扣,扣到负数就回滚并提示库存不足。

public void deductStock(Long productId, Integer count) { String key = "stock:" + productId; Long remain = redisTemplate.opsForValue().decrement(key, count); if (remain == null || remain < 0) { // 扣成负数说明超卖,加回去并抛异常 redisTemplate.opsForValue().increment(key, count); throw new BizException("库存不足"); } // 异步落库,减少数据库压力 stockMapper.reduceStock(productId, count); }

decrement是原子操作,并发下不会出现两个请求同时读到 10 再各扣 1 的情况。异步落库用@Async注解,方法上加线程池配置。注意 Redis 里的库存要在项目启动时从数据库预热,config/RedisPreheat.java里做了这件事。

4.3 按楼栋合并订单的查询

校园场景里,同一个自提点、同一个时段、同一栋楼的订单可以合并分拣。这个查询用 XML 写,因为要 group by 加聚合:

<select id="listMergeOrders" resultType="OrderMergeVO"> SELECT pickup_point, time_slot, building, COUNT(*) AS order_count, SUM(total_amount) AS total FROM orders WHERE status = 'PAID' AND pickup_point = #{point} GROUP BY pickup_point, time_slot, building ORDER BY building </select>

time_slot是下单时选的配送时段,比如“17:00-18:00”。分拣员按这个结果去备货,一栋楼一个批次。改多自提点时,把pickup_point换成关联表查询即可。

4.4 自提核销的接口设计

核销接口接收订单号和核销码,比对 Redis 里的值:

public void verifyPickup(Long orderId, String inputCode) { String key = "pickup:" + orderId; String realCode = redisTemplate.opsForValue().get(key); if (realCode == null) { throw new BizException("核销码已过期"); } if (!realCode.equals(inputCode)) { throw new BizException("核销码错误"); } updateStatus(orderId, OrderStatus.COMPLETED); redisTemplate.delete(key); }

核销成功后删 key,防止重复核销。这里没做防重放,如果要严谨,可以在删 key 前用delete的返回值判断是否已被删过。

5. 避坑与常见问题:跑不起来时先查这几处

5.1 启动报数据源连接失败

现象:日志里Communications link failure。原因通常是 MySQL 没启动,或者application.yml里的端口、密码不对。解决:先用mysql -u root -p手动连一次,确认能进再启动项目。如果 MySQL 是 8.0,驱动类要写com.mysql.cj.jdbc.Driver,老版本驱动会报时区错误。

5.2 前端请求 404 或跨域

现象:浏览器控制台报 CORS 或 404。原因:Vite proxy 没生效,或者后端拦截器把请求拦了。解决:检查vite.config.js里 proxy 的 target 是不是http://localhost:8080,再检查WebConfig白名单有没有放行/api/**。本地开发用 proxy 最省事,别去后端加@CrossOrigin,容易和拦截器打架。

5.3 库存扣减后数据库没变

现象:Redis 里库存减了,但product表的stock字段没动。原因:@Async方法没生效,常见于同类内部调用——在同一个类里直接调this.asyncMethod()不会走代理。解决:把异步方法抽到单独的StockService里,或者用AopContext.currentProxy()拿代理对象。我一般直接抽类,干净。

5.4 订单时间差 8 小时

现象:下单后订单时间比实际早 8 小时。原因:JDBC URL 没加serverTimezone=Asia/Shanghai,或者实体里用了java.util.Date而不是LocalDateTime。解决:URL 补上时区参数,实体统一用LocalDateTime,MyBatis-Plus 会自动映射。

5.5 核销码在 Redis 里查不到

现象:核销时提示“核销码已过期”,但刚下单不久。原因:Redis 没配持久化,重启后 key 丢了;或者下单时状态没流转到WAIT_PICKUP,压根没写 key。解决:先确认订单状态,再检查 Redis 是否正常运行。生产环境给 Redis 开 AOF,别用默认的 RDB -only。

6. 二次开发怎么改:从单自提点到多校区配送

这份源码的默认场景是单校区、单自提点。要改成多校区,核心改动在三处:订单表加campus_id字段、商品表加校区关联、分拣查询按校区过滤。我一般先加字段再改查询,最后动前端。

ALTER TABLE orders ADD COLUMN campus_id BIGINT DEFAULT 1; ALTER TABLE product ADD COLUMN campus_id BIGINT DEFAULT 1; CREATE INDEX idx_campus_status ON orders(campus_id, status);

加完字段后,OrderServiceImpl里创建订单时从用户信息里带出campus_id,分拣查询的 XML 加一个AND campus_id = #{campusId}。前端在商品列表页加一个校区切换的下拉,选中后存 Pinia,请求时带上。

验证方法:用两个不同校区的账号各下一单,看分拣列表是否只显示各自校区的订单。如果混在一起,检查 XML 里的条件有没有拼上。

另一个常见改造是接支付。项目里支付状态是手动改的,接真实支付时在updateStatus前加一个支付回调接口,校验签名后再流转状态。别在回调里直接改数据库,先写日志表,再异步处理,防止回调重复导致状态错乱。

还有个技巧:把application.yml里的配置抽成环境变量,部署时用-Dspring.profiles.active=prod切生产配置。我见过有人把数据库密码提交到 Git,后来改起来满仓库找,血泪经验。

从那以后我每次拿到新源码,都强制先跑一遍 init.sql 再启动,确认基础链路通了才动代码。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进