☰
基于SpringBoot的小区团购系统实战:从开团到自提全链路
2026/10/8 4:15:04 网站建设 项目流程

简介:这是一套基于SpringBoot的小区团购管理系统完整源码,面向Java Web初学者、课程设计学生及需要团购类项目练手的开发者,可帮助快速理解并搭建一套前后端分离的社区团购平台。项目采用Java语言与SpringBoot框架,前端使用Vue、ElementUI与Ajax,后端整合MyBatisPlus操作MySQL 5.7数据库,开发环境兼容Eclipse、MyEclipse与IDEA,Maven管理依赖,浏览器端以谷歌浏览器调试为主。压缩包共827个文件,约25.6MB,其中122个java文件承载核心业务逻辑,62个vue与160个js文件构成前端交互页面,另有43个html、52个css及大量svg、png、jpg等图片素材,并附带docx说明文档、pdf资料与mp4演示视频,目录结构清晰,便于按模块查阅。系统涵盖用户信息、图片素材、视频素材等管理功能,配套论文摘要与章节目录,从绪论、技术介绍到系统分析均有覆盖。目前已有146人学习下载,适合作为毕业设计、课程作业或自学SpringBoot全栈开发的参考案例。

1. 小区团购系统到底解决什么问题:从一张 Excel 接龙表说起

一个 300 户的小区做团购,最开始往往就是一张 Excel 接龙表加一个微信群。团长在群里发商品图,业主跟帖报名,团长手动统计、手动收款、手动对账,一趟下来两三个小时。做到第三周,问题就集中爆发了:接龙顺序错乱、有人重复报名、有人报了不付款、有人付款没记上、截单时间到了还有人往里加。这不是团长不细心,而是「群聊 + 表格」这套组合本身没有事务、没有库存、没有状态机。

小区团购系统要干的事,就是把这套流程搬到一个有状态、有权限、有对账能力的后台里。基于 SpringBoot 的小区团购管理系统,本质是一个「多角色 + 商品 + 订单 + 自提点」的轻量电商后台:业主端下单、团长端开团、平台端管商品和结算。它和通用商城最大的区别在于——履约方式是「小区自提」而不是快递,所以库存按团批次锁、订单按自提点聚合、退款按整团处理。这套源码适合谁?适合想拿一个真实业务练 SpringBoot 全栈的 Java 工程师,也适合物业或社区运营方想自建一套不被平台抽成的工具。下面我按「能跑起来 → 能改得动 → 不踩坑」的顺序讲。

2. 技术选型与工程骨架:为什么是 SpringBoot + MyBatis-Plus 这套组合

2.1 分层结构与依赖版本怎么定

小区团购系统的业务复杂度不高,但角色和状态多,所以骨架要清晰。常见做法是标准四层:controller / service / mapper / entity,再加一个 vo 层做前端出参。SpringBoot 负责容器和自动配置,MyBatis-Plus 负责单表 CRUD 和分页,省掉大量 XML。前端如果是 Vue3 后台管理系统,打包后直接放进 SpringBoot 的 static 目录,一个 jar 就能部署,这对小区这种没有专业运维的场景很实用。

版本上有个血泪经验:不要盲目追最新。SpringBoot 3.x 默认要求 JDK 17,很多老服务器还停在 JDK 8,硬上会直接启动失败。我一般锁 SpringBoot 2.7.x + JDK 8/11,MyBatis-Plus 用 3.5.x,MySQL 8.0 驱动。下面是最小 pom 依赖片段:

<!-- pom.xml 关键依赖,版本按团队 JDK 实际情况锁 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <!-- JDK8 友好,别乱升 3.x --> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> </dependencies>

逻辑说明:parent 统一管理版本,避免各 starter 版本打架;MyBatis-Plus 的 starter 会自动装配 SqlSessionFactory,不用手写配置类。参数说明:spring-boot-starter-parent的版本决定了整个依赖树,改它等于改全局,动之前先确认 JDK。数据库连接写在 application.yml:

spring: datasource: url: jdbc:mysql://127.0.0.1:3306/community_group?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true # 下划线转驼峰,实体不用写 @TableField global-config: db-config: logic-delete-field: deleted # 逻辑删除字段,订单不能物理删 logic-delete-value: 1 logic-not-delete-value: 0

serverTimezone不写会报时区错误,这是新手最常见的启动失败原因之一。逻辑删除一定要开,团购订单涉及对账,物理删除等于把后悔药扔了。

2.2 核心表结构:团批次、商品、订单三张主表怎么设计

小区团购和普通商城的核心差异在「团批次」这个概念。同一个商品,这周开团和下周开团是两个批次,价格、截单时间、自提点都可能不同。所以表设计上要单独抽一张group_batch表,订单必须挂在批次上,而不是直接挂商品。

表名作用关键字段
group_batch团批次id, title, start_time, end_time, pickup_point, status
product商品id, batch_id, name, price, stock, unit
order订单id, batch_id, user_id, total_amount, status, pickup_code
order_item订单明细id, order_id, product_id, quantity, price

status字段是状态机的核心。批次状态建议用枚举:0 待开团、1 进行中、2 已截单、3 已完结。订单状态:0 待付款、1 已付款、2 已自提、3 已退款。状态流转必须写在 service 层用条件更新,不能靠前端传。

-- 下单时扣库存,用条件更新防止超卖 UPDATE product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity};

这条 SQL 的AND stock >= #{quantity}是关键,返回影响行数为 0 就说明库存不足,直接抛业务异常。很多人先查再扣,中间有并发窗口,团购秒杀场景必翻车。

2.3 用 MyBatis-Plus 生成实体和建表 SQL 的实操

热词里提到「mybatisplus 根据 java 实体类生成创建表的 sql 语句」,这在快速搭骨架时确实省事。MyBatis-Plus 本身不直接生成 DDL,但可以借助它的元数据 + 一段反射代码输出建表语句。我一般写个一次性工具类:

// 根据实体类反射生成 MySQL 建表语句,仅用于开发期搭骨架 public class DdlGenerator { public static String generate(Class<?> clazz) { TableName tn = clazz.getAnnotation(TableName.class); String table = tn != null ? tn.value() : clazz.getSimpleName().toLowerCase(); StringBuilder sb = new StringBuilder("CREATE TABLE " + table + " (\n"); for (Field f : clazz.getDeclaredFields()) { TableId id = f.getAnnotation(TableId.class); TableField tf = f.getAnnotation(TableField.class); if (tf != null && !tf.exist()) continue; // 忽略非数据库字段 String col = camelToUnderline(f.getName()); String type = mapType(f.getType()); sb.append(" ").append(col).append(" ").append(type); if (id != null) sb.append(" PRIMARY KEY"); sb.append(",\n"); } sb.append(" deleted TINYINT DEFAULT 0\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"); return sb.toString(); } // camelToUnderline / mapType 略,按 String->VARCHAR(255)、Long->BIGINT 映射 }

逻辑说明:遍历实体字段,读@TableId判断主键,读@TableField(exist=false)跳过 VO 字段。参数说明:mapType的映射规则要自己定,LocalDateTime映射DATETIME,BigDecimal映射DECIMAL(10,2)——金额千万别用 double。这个工具只在开发期跑一次,生成后手工补索引和外键,不要放进生产代码。

3. 团购核心链路落地:开团、下单、截单、自提四步怎么串

3.1 开团与商品上架的接口实现

开团是团长动作,接口要校验权限和批次时间。核心逻辑:创建批次时状态为「待开团」,上架商品后状态转「进行中」。这里有个容易忽略的点——同一小区同一时间段不应该有两个进行中的批次指向同一自提点,否则业主会下错单。

@PostMapping("/batch/open") public Result<Long> openBatch(@RequestBody BatchOpenDTO dto) { // 1. 校验该自提点是否已有进行中的批次 Long running = batchMapper.countRunning(dto.getPickupPoint()); if (running > 0) { throw new BizException("该自提点已有进行中的团,请先截单"); } // 2. 落库,状态置为进行中 GroupBatch batch = new GroupBatch(); batch.setTitle(dto.getTitle()); batch.setPickupPoint(dto.getPickupPoint()); batch.setStartTime(LocalDateTime.now()); batch.setEndTime(dto.getEndTime()); batch.setStatus(BatchStatus.RUNNING.getCode()); batchMapper.insert(batch); return Result.ok(batch.getId()); }

逻辑说明:先查后插在并发下仍有极小概率重复,生产环境建议在pickup_point + status上加唯一索引兜底。参数说明:endTime由前端传,服务端要校验必须大于当前时间,否则批次一创建就是过期状态。

3.2 下单接口的库存与金额校验

下单是整个系统最容易出问题的地方。要同时做四件事:校验批次是否进行中、校验商品属于该批次、扣库存、算金额。金额必须服务端重算,绝不能信前端传的 total。

@Transactional(rollbackFor = Exception.class) public Long createOrder(Long userId, OrderCreateDTO dto) { GroupBatch batch = batchMapper.selectById(dto.getBatchId()); if (batch.getStatus() != BatchStatus.RUNNING.getCode()) { throw new BizException("团已截单"); } BigDecimal total = BigDecimal.ZERO; for (OrderItemDTO item : dto.getItems()) { Product p = productMapper.selectById(item.getProductId()); if (!p.getBatchId().equals(dto.getBatchId())) { throw new BizException("商品不属于当前团"); } int affected = productMapper.deductStock(p.getId(), item.getQuantity()); if (affected == 0) { throw new BizException("库存不足:" + p.getName()); } total = total.add(p.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } // 落订单 + 明细,生成自提码 Order order = new Order(); order.setUserId(userId); order.setBatchId(dto.getBatchId()); order.setTotalAmount(total); order.setStatus(OrderStatus.UNPAID.getCode()); order.setPickupCode(generatePickupCode()); orderMapper.insert(order); return order.getId(); }

逻辑说明:@Transactional保证扣库存和落订单同生共死,任何一步抛异常全部回滚。参数说明:deductStock就是前面那条条件更新 SQL,返回 0 即库存不足。generatePickupCode建议用「批次号后 4 位 + 随机 4 位」,自提时人工核对方便。

3.3 截单与自提核销的状态流转

截单不是简单改个状态,它要触发两件事:把未付款订单标记为超时取消并回滚库存,把已付款订单聚合到自提清单。这一步建议用定时任务扫批次,而不是等团长手动点。

@Scheduled(cron = "0 */5 * * * ?") // 每 5 分钟扫一次 public void autoCloseBatch() { List<GroupBatch> expired = batchMapper.listExpired(LocalDateTime.now()); for (GroupBatch b : expired) { // 1. 取消未付款订单并回滚库存 List<Order> unpaid = orderMapper.listByBatchAndStatus(b.getId(), OrderStatus.UNPAID.getCode()); for (Order o : unpaid) { orderMapper.updateStatus(o.getId(), OrderStatus.CANCELED.getCode()); rollbackStock(o); // 按明细把库存加回去 } // 2. 批次置为已截单 batchMapper.updateStatus(b.getId(), BatchStatus.CLOSED.getCode()); } }

逻辑说明:定时任务要加分布式锁或数据库乐观锁,多实例部署时防止重复执行。参数说明:cron 表达式按业务调整,团购截单精度到分钟即可,没必要每秒扫。自提核销就是扫自提码把订单状态从「已付款」改成「已自提」,这个动作要记录操作人和时间,方便事后对账。

3.4 对账与退款:整团维度的金额核对

团购退款和普通电商不同,往往是整团取消或某个商品缺货导致批量退款。对账的核心是「批次应收 = 已付款订单金额之和」,退款要按订单维度逐笔退,不能直接改批次金额。建议单独建一张refund_record表,记录退款订单、金额、原因、操作人。对账时用一条聚合 SQL 就能看出差异:

-- 批次对账:应收、已收、已退、净收 SELECT b.id, SUM(o.total_amount) AS should_receive, SUM(CASE WHEN o.status IN (1,2) THEN o.total_amount ELSE 0 END) AS received, IFNULL((SELECT SUM(r.amount) FROM refund_record r WHERE r.batch_id = b.id), 0) AS refunded FROM group_batch b LEFT JOIN `order` o ON o.batch_id = b.id WHERE b.id = #{batchId} GROUP BY b.id;

逻辑说明:received只统计已付款和已自提的订单,未付款和已取消不计入。参数说明:refund_record的batch_id要冗余存一份,避免每次退款都去 join 订单表,团购数据量不大但查询频繁。

4. 避坑与排查:小区团购系统上线后最容易翻车的 5 个点

4.1 库存扣成负数

现象:截单后盘点发现某商品库存是 -3。原因:扣库存用了「先查再扣」两步操作,两个业主同时下单,都查到库存为 1,都扣成功。解决:改成UPDATE ... WHERE stock >= #{qty}的条件更新,用影响行数判断成败,这是唯一可靠的做法。

4.2 订单金额和前端显示不一致

现象:业主截图显示 29.9,后台订单是 30.0。原因:前端用 JS 浮点数算金额,0.1 + 0.2这类精度问题导致。解决:所有金额计算在服务端用BigDecimal,前端只负责展示,下单接口不接收 total 字段,服务端重算。

4.3 定时截单任务重复执行

现象:同一批次被截单两次,库存回滚了两遍,凭空多出库存。原因:服务部署了两个实例,定时任务各跑各的。解决:给截单任务加 Redis 分布式锁,或者用数据库UPDATE batch SET status=2 WHERE id=? AND status=1的条件更新保证只成功一次。

4.4 自提码重复或猜得到

现象:两个业主拿到同一个自提码,或者有人用规律码冒领。原因:自提码用了自增 ID 或简单随机。解决:自提码用「批次号 + 用户 ID 后 4 位 + 随机 4 位」组合,核销时必须同时校验批次和订单状态,不能只认码。

4.5 SpringBoot 版本升级导致启动失败

现象:本地跑得好好的,换台机器或升了版本就报NoClassDefFoundError或Unsupported class file major version。原因:SpringBoot 3.x 强制 JDK 17,而服务器是 JDK 8。解决:先确认服务器 JDK 版本再定 SpringBoot 版本,2.7.x 是 JDK 8 的稳妥选择。升级前先在测试环境跑一遍全链路,别直接动生产。

5. 进阶技巧:把团购系统做成可复用的多小区版本

单小区跑通之后,下一步通常是复制到多个小区。这时候最大的改动是「数据隔离」——每个小区只能看到自己的批次、商品、订单。最省事的做法是在所有业务表加一个community_id字段,用 MyBatis-Plus 的租户插件自动拼接条件,业务代码几乎不用改。

@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); TenantLineInnerInterceptor tenant = new TenantLineInnerInterceptor(); tenant.setTenantLineHandler(new TenantLineHandler() { @Override public Expression getTenantId() { // 从当前登录用户上下文取小区 ID return new LongValue(UserContext.getCommunityId()); } @Override public String getTenantIdColumn() { return "community_id"; } @Override public boolean ignoreTable(String tableName) { // 平台级表不隔离,比如小区信息表本身 return "community".equals(tableName); } }); interceptor.addInnerInterceptor(tenant); return interceptor; }

逻辑说明:租户插件会在每条 SQL 的 where 后自动追加community_id = ?,查询、更新、删除都生效。参数说明:ignoreTable要列出所有平台级共享表,漏一个就会导致跨小区数据串。UserContext用 ThreadLocal 存当前请求的小区 ID,在拦截器里从 token 解析后塞进去。

验证隔离是否生效,最直接的办法是开两个账号分别属于不同小区,交叉访问对方的订单 ID,看是否返回空。我一般会写一个集成测试专门跑这个用例,因为租户隔离一旦漏了,是数据事故级别的坑。

另一个值得做的进阶是「拼团进度可视化」。业主最关心「还差几份成团」,可以在批次表加min_quantity和current_quantity,每次下单后更新,前端轮询展示进度条。这个功能对成团率的提升很明显,实现成本却很低。

最后说个我自己的习惯:每次改完订单状态相关的代码,我都会手动跑一遍「下单 → 付款 → 截单 → 自提 → 退款」全链路,而不是只跑单元测试。因为状态机的 bug 往往藏在跨方法的流转里,单测覆盖不到。这套小区团购系统源码的价值不在于代码多复杂,而在于它把「团批次 + 自提 + 对账」这条真实业务链路走通了,拿它练手比写 CRUD demo 有用得多。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询