ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于SpringBoot的小区团购系统实战:从开团到自提全链路

基于SpringBoot的小区团购系统实战:从开团到自提全链路 简介这是一套基于SpringBoot的小区团购管理系统完整源码面向Java Web初学者、课程设计学生及需要团购类项目练手的开发者可帮助快速理解并搭建一套前后端分离的社区团购平台。项目采用Java语言与SpringBoot框架前端使用Vue、ElementUI与Ajax后端整合MyBatisPlus操作MySQL 5.7数据库开发环境兼容Eclipse、MyEclipse与IDEAMaven管理依赖浏览器端以谷歌浏览器调试为主。压缩包共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/11MyBatis-Plus 用 3.5.xMySQL 8.0 驱动。下面是最小 pom 依赖片段!-- pom.xml 关键依赖版本按团队 JDK 实际情况锁 -- parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version !-- JDK8 友好别乱升 3.x -- /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency /dependencies逻辑说明parent 统一管理版本避免各 starter 版本打架MyBatis-Plus 的 starter 会自动装配 SqlSessionFactory不用手写配置类。参数说明spring-boot-starter-parent的版本决定了整个依赖树改它等于改全局动之前先确认 JDK。数据库连接写在 application.ymlspring: datasource: url: jdbc:mysql://127.0.0.1:3306/community_group?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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: 0serverTimezone不写会报时区错误这是新手最常见的启动失败原因之一。逻辑删除一定要开团购订单涉及对账物理删除等于把后悔药扔了。2.2 核心表结构团批次、商品、订单三张主表怎么设计小区团购和普通商城的核心差异在「团批次」这个概念。同一个商品这周开团和下周开团是两个批次价格、截单时间、自提点都可能不同。所以表设计上要单独抽一张group_batch表订单必须挂在批次上而不是直接挂商品。表名作用关键字段group_batch团批次id, title, start_time, end_time, pickup_point, statusproduct商品id, batch_id, name, price, stock, unitorder订单id, batch_id, user_id, total_amount, status, pickup_codeorder_item订单明细id, order_id, product_id, quantity, pricestatus字段是状态机的核心。批次状态建议用枚举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) ENGINEInnoDB DEFAULT CHARSETutf8mb4;); return sb.toString(); } // camelToUnderline / mapType 略按 String-VARCHAR(255)、Long-BIGINT 映射 }逻辑说明遍历实体字段读TableId判断主键读TableField(existfalse)跳过 VO 字段。参数说明mapType的映射规则要自己定LocalDateTime映射DATETIMEBigDecimal映射DECIMAL(10,2)——金额千万别用 double。这个工具只在开发期跑一次生成后手工补索引和外键不要放进生产代码。3. 团购核心链路落地开团、下单、截单、自提四步怎么串3.1 开团与商品上架的接口实现开团是团长动作接口要校验权限和批次时间。核心逻辑创建批次时状态为「待开团」上架商品后状态转「进行中」。这里有个容易忽略的点——同一小区同一时间段不应该有两个进行中的批次指向同一自提点否则业主会下错单。PostMapping(/batch/open) public ResultLong 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() { ListGroupBatch expired batchMapper.listExpired(LocalDateTime.now()); for (GroupBatch b : expired) { // 1. 取消未付款订单并回滚库存 ListOrder 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 status2 WHERE id? AND status1的条件更新保证只成功一次。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 有用得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表