简介:高校社区生鲜配送系统是一套面向大学生与周边社区居民的在线生鲜购物平台源码,适合计算机专业学生、Java Web初学者及需要课程设计或毕业设计参考的开发者,用于理解电商类系统的完整业务闭环。资源包共909个文件,约1.61MB,以html页面、class编译文件、css样式、js脚本、png图片和java源码为主,另含json配置、sql建表脚本与yml部署文件,覆盖前端展示、后端控制与数据持久化各层。系统围绕用户管理、商品管理、订单处理、库存控制、配送调度、支付接口、数据分析、客户服务及移动端适配等模块展开,可帮助读者梳理从注册登录、下单支付到库存扣减与配送签收的完整链路,并参考后台管理端与前台商城的目录组织方式。目前已有138人学习下载,适合作为生鲜电商项目实战与二次开发的起步模板。
1. 高校社区生鲜配送系统:从一份课程设计到能跑通的最小闭环
高校社区生鲜配送系统.zip 这个标题,大概率来自一份课程设计或毕业设计交付物。它要解决的核心问题很具体:把「学生下单—仓库分拣—配送员接单—宿舍楼下交付」这条链路用一套 Web 系统串起来。和面向社会的美团买菜、多多买菜不同,高校场景有几个硬约束:收货地址高度集中(就那几栋宿舍楼)、下单时间集中在午晚饭前两小时、配送距离短但订单密度极高、用户对价格极度敏感。这些约束决定了系统设计不能照搬电商那一套,库存模型、配送调度、并发策略都得重新想。适合谁看?正在做类似课设的在校生、想快速搭一套社区团购原型的独立开发者、以及需要理解「高密度短距配送」这类业务的技术负责人。下面我按自己实际搭过的一套方案,把选型、建表、下单、分拣、配送、压测这条线讲透。
2. 技术选型与数据模型:为什么不用微服务,以及订单表怎么设计
2.1 单体 Spring Boot + MySQL 是高校场景的最优解
很多人一上来就想上 Spring Cloud,觉得微服务显得高级。但高校社区生鲜配送系统的真实并发量是多少?一个校区撑死两万学生,午高峰同时下单的峰值大概在 300~500 QPS,这个量级一台 4 核 8G 的云服务器加 MySQL 单实例完全扛得住。拆成微服务反而带来分布式事务、服务发现、链路追踪一堆额外复杂度,课设周期内根本调不完。
我一般会选的技术栈是:后端 Spring Boot 2.7 + MyBatis-Plus + MySQL 8.0 + Redis,前端 Vue 3 + Element Plus,配送端用微信小程序或简单的 H5。Redis 只做两件事:缓存商品列表和做下单限流。这套组合的好处是资料多、踩坑少、部署简单,一台服务器跑 Docker Compose 就能全起来。
选型理由说到底就一条:在业务量没到瓶颈之前,架构复杂度就是纯粹的负债。高校生鲜配送的瓶颈从来不在服务拆分,而在库存扣减的准确性和配送调度的合理性。
2.2 核心表结构:商品、库存、订单、配送四张表怎么落地
数据模型是整个系统的地基,这里翻车后面全得返工。下面是我实际用的建表语句,重点看库存表和订单表的设计。
-- 商品表:生鲜商品有规格和称重两种计价方式 CREATE TABLE `product` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(64) NOT NULL COMMENT '商品名,如 山东红富士苹果', `category` VARCHAR(32) NOT NULL COMMENT '分类:蔬菜/水果/肉禽/蛋奶', `price_type` TINYINT NOT NULL DEFAULT 1 COMMENT '1=按份计价 2=按重量计价', `price` DECIMAL(10,2) NOT NULL COMMENT '单价,按重量时单位为元/500g', `stock` INT NOT NULL DEFAULT 0 COMMENT '可售库存', `daily_limit` INT DEFAULT NULL COMMENT '每日限购量,NULL 表示不限', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1=上架 0=下架', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单表:关键是把配送批次和自提点冗余进来 CREATE TABLE `order` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL UNIQUE COMMENT '业务单号', `user_id` BIGINT NOT NULL, `building_id` INT NOT NULL COMMENT '宿舍楼 ID,用于聚合配送', `pickup_point` VARCHAR(64) NOT NULL COMMENT '自提点,如 3号楼架空层', `total_amount` DECIMAL(10,2) NOT NULL, `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2分拣中 3配送中 4已完成 5已取消', `batch_no` VARCHAR(16) DEFAULT NULL COMMENT '配送批次号,按楼栋+时段生成', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX `idx_building_batch` (`building_id`, `batch_no`), INDEX `idx_user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:price_type区分按份和按重,是因为生鲜里苹果按份卖、排骨按斤卖,混在一起会导致金额计算逻辑分叉。building_id和pickup_point冗余在订单表里,是为了分拣和配送时不用再关联用户地址表,直接按楼栋聚合。batch_no是配送调度的核心字段,同一栋楼同一时段的订单会被打上同一个批次号,配送员一次拿一批。
参数说明:daily_limit用于限购,比如特价鸡蛋每人每天限 2 份,防止黄牛囤货。status的状态机要严格,尤其是「已支付」到「分拣中」这一步,必须由支付回调触发,不能靠前端传。
2.3 库存扣减:为什么不能用stock = stock - 1
生鲜配送最怕超卖。学生下单时显示有货,付款后告诉你没货了,体验极差。常见的错误写法是:
-- 错误示范:先查后扣,并发下必然超卖 SELECT stock FROM product WHERE id = 1; -- 应用层判断 stock > 0 UPDATE product SET stock = stock - 1 WHERE id = 1;正确做法是用数据库的原子操作加乐观锁:
-- 正确写法:扣减时带上库存条件,影响行数为 0 说明库存不足 UPDATE product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity} AND status = 1;在 MyBatis 里拿到返回值affectedRows,如果等于 0 就抛「库存不足」异常,回滚整个下单事务。这个方案在 500 QPS 下实测没有超卖。如果并发再高,可以上 Redis 预扣减,但高校场景没必要,数据库行锁足够。
注意:按重量计价的商品(如排骨)库存单位要统一。我见过有人库存存「斤」,下单传「克」,结果扣减时差了 500 倍,直接扣成负数。建表时就要在注释里写死单位。
3. 下单与分拣链路:从购物车到拣货单的完整实现
3.1 下单接口:事务边界和幂等怎么处理
下单是整个系统最核心的写操作,必须保证「扣库存、建订单、建订单明细」三件事在同一个事务里。下面是我用的 Service 层代码骨架:
@Transactional(rollbackFor = Exception.class) public OrderVO createOrder(Long userId, OrderCreateDTO dto) { // 1. 幂等校验:同一用户 5 秒内相同商品组合只允许下一单 String idempotentKey = "order:lock:" + userId + ":" + dto.getCartHash(); Boolean locked = redisTemplate.opsForValue() .setIfAbsent(idempotentKey, "1", 5, TimeUnit.SECONDS); if (Boolean.FALSE.equals(locked)) { throw new BizException("请勿重复提交"); } // 2. 逐商品扣减库存,任一失败则整体回滚 BigDecimal total = BigDecimal.ZERO; for (OrderItemDTO item : dto.getItems()) { int affected = productMapper.deductStock(item.getProductId(), item.getQuantity()); if (affected == 0) { throw new BizException("商品[" + item.getName() + "]库存不足"); } Product p = productMapper.selectById(item.getProductId()); total = total.add(p.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } // 3. 生成订单号和配送批次 String orderNo = generateOrderNo(userId); String batchNo = buildBatchNo(dto.getBuildingId(), new Date()); Order order = new Order(); order.setOrderNo(orderNo); order.setUserId(userId); order.setBuildingId(dto.getBuildingId()); order.setPickupPoint(dto.getPickupPoint()); order.setTotalAmount(total); order.setBatchNo(batchNo); order.setStatus(0); orderMapper.insert(order); // 4. 批量插入订单明细 orderItemMapper.batchInsert(order.getId(), dto.getItems()); return buildOrderVO(order); }逻辑说明:第 1 步用 Redis 的setIfAbsent做幂等锁,防止用户手抖连点两次。第 2 步循环扣库存,任何一件商品扣失败就抛异常,@Transactional保证前面已扣的库存回滚。第 3 步的buildBatchNo是关键,它决定了后续分拣和配送的聚合粒度。
参数说明:幂等锁的 5 秒是经验值,太短防不住连点,太长会误伤正常复购。batchNo的生成规则我一般用「楼栋ID + 日期 + 时段码」,比如B03-20241216-L表示 3 号楼 12 月 16 日午餐时段。
3.2 分拣单生成:按商品聚合还是按订单聚合
分拣环节的效率直接决定履约成本。假设午高峰有 200 单,每单 3~5 件商品,分拣员如果按订单逐单拣,要在货架间来回跑 200 趟。正确做法是按商品聚合,生成拣货总单。
-- 按商品聚合当日待分拣数量 SELECT oi.product_id, p.name AS product_name, p.category, SUM(oi.quantity) AS total_qty, COUNT(DISTINCT o.id) AS order_count FROM order_item oi JOIN `order` o ON oi.order_id = o.id JOIN product p ON oi.product_id = p.id WHERE o.status = 1 AND o.batch_no = #{batchNo} GROUP BY oi.product_id, p.name, p.category ORDER BY p.category, total_qty DESC;逻辑说明:这条 SQL 输出的是「这个批次里,苹果总共要拣 80 份,涉及 45 个订单」。分拣员按分类顺序(先蔬菜后水果再肉禽)一次性把每个商品的总量拣出来,再回到分拣台按订单拆分。实测这套流程比逐单拣货效率提升 3 倍以上。
参数说明:batch_no是必传参数,对应 3.1 里生成的批次号。ORDER BY p.category让同一分类的商品排在一起,减少货架间移动。
3.3 配送批次与自提点:怎么把 200 单压缩成 8 趟配送
高校配送的终极优化点是「聚合」。200 单如果一单一送,配送员跑断腿。按楼栋聚合后,3 号楼的所有订单打成一个批次,配送员用小推车一次拉到 3 号楼架空层,学生凭取货码自取。
// 按楼栋+时段生成配送任务 public List<DeliveryTask> buildDeliveryTasks(String batchNo) { List<Order> orders = orderMapper.selectByBatch(batchNo); Map<Integer, List<Order>> byBuilding = orders.stream() .collect(Collectors.groupingBy(Order::getBuildingId)); List<DeliveryTask> tasks = new ArrayList<>(); for (Map.Entry<Integer, List<Order>> entry : byBuilding.entrySet()) { DeliveryTask task = new DeliveryTask(); task.setBuildingId(entry.getKey()); task.setOrderCount(entry.getValue().size()); task.setPickupPoint(entry.getValue().get(0).getPickupPoint()); // 预估重量:生鲜平均每单 2.5kg task.setEstWeight(entry.getValue().size() * 2.5); task.setStatus(0); // 待接单 tasks.add(task); } return tasks; }逻辑说明:groupingBy按楼栋分组,每个楼栋生成一个配送任务。estWeight用于判断是否需要多个配送员,超过 30kg 建议拆成两个任务。pickupPoint取该楼栋第一个订单的自提点,实际项目中同一楼栋自提点应该统一。
参数说明:2.5kg 是生鲜订单的经验均值,蔬菜水果偏轻,米面粮油偏重,可以按分类加权计算。配送任务状态0待接单 1已接单 2配送中 3已送达,配送员在小程序里抢单。
4. 避坑与排查:那些让我熬夜改代码的坑
4.1 库存扣成负数:单位不统一是元凶
现象:上线第二天发现某个商品库存显示 -47,但订单量远没这么多。原因:商品表库存单位是「份」,但下单接口传的是「克」,扣减时stock - 500直接把库存干穿。解决:在product表加unit字段,下单 DTO 里强制带单位,Service 层做单位换算校验,不一致直接抛异常。所有涉及数量的字段都在注释里写死单位。
4.2 支付回调重复执行导致重复发货
现象:同一个订单被分拣了两次,学生收到两份货。原因:支付平台回调可能重试,回调接口没有做幂等,每次回调都把订单状态从「已支付」推到「分拣中」。解决:回调入口先查订单当前状态,只有status=0才处理,处理时用UPDATE order SET status=2 WHERE id=? AND status=1这种带状态条件的更新,影响行数为 0 说明已被处理过,直接返回成功。
4.3 午高峰 Redis 连接池被打满
现象:12:00~12:30 接口大量超时,日志报Could not get a resource from the pool。原因:商品列表缓存和幂等锁共用同一个 Redis 连接池,默认最大连接数 8,高峰时不够用。解决:把连接池max-active调到 50,max-wait设为 200ms 快速失败。更重要的是把商品列表缓存改成 Caffeine 本地缓存,只有幂等锁走 Redis,压力直接降一个数量级。
4.4 分拣单和实际订单对不上
现象:分拣员反馈拣货总单显示苹果 80 份,但按订单拆分时只有 75 份。原因:分拣单生成和订单状态更新之间有并发,生成分拣单时有些订单还没支付成功,但状态已经是「已支付」。解决:分拣单生成加一个「支付后延迟 30 秒」的缓冲,或者用定时任务每 5 分钟扫一次status=1的订单批量生成,避免实时生成时的状态不一致。
4.5 配送批次号在跨天时冲突
现象:昨天晚餐批次的订单和今天午餐批次的订单混在一起分拣。原因:batchNo生成规则只用了「楼栋+时段码」,没有日期,跨天后B03-L重复。解决:批次号加上日期,格式改为20241216-B03-L。同时给batch_no加唯一索引,从数据库层面兜底。
5. 压测与调优:用 JMeter 验证 500 并发下单,以及一个我常用的排查习惯
系统能不能扛住午高峰,不能靠猜。我一般用 JMeter 做一轮下单压测,目标是在 500 并发下 P99 响应时间低于 800ms,且无超卖。
压测脚本的核心配置:线程数 500,Ramp-up 10 秒,循环 5 次,也就是 2500 个下单请求。请求体里商品 ID 和数量随机,但库存总量设为 3000,确保有 500 个请求会因库存不足失败——这是故意的,用来验证库存扣减的原子性。
压测后重点看三个指标:
| 指标 | 合格线 | 不合格时的排查方向 |
|---|---|---|
| P99 响应时间 | < 800ms | 看慢 SQL 日志,多半是订单表缺索引 |
| 超卖数量 | 0 | 检查扣库存 SQL 是否带stock >= quantity条件 |
| 错误率 | < 1% | 看 Redis 连接池和数据库连接池配置 |
压测中我遇到过一次 P99 飙到 3 秒,排查发现是order_item表的batchInsert没加事务,每条 insert 都单独提交。改成rewriteBatchedStatements=true加批量提交后,降到 400ms。
最后分享一个我自己的习惯:每次改完库存相关的代码,先手动把库存改成 1,然后用两个浏览器同时下单同一商品,看是不是只有一个成功。这个土办法比任何单元测试都直观,帮我拦下过至少三次超卖 bug。生鲜配送系统看着简单,但库存和状态机这两块,稍不留神就是线上事故。希望帮到你。
本文还有配套的精品资源,点击获取