尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

小区团购系统源码实战:Spring Boot+MySQL+Redis全流程

小区团购系统源码实战:Spring Boot+MySQL+Redis全流程 简介社区团购是典型的私域电商场景其系统开发涉及预售集单、团长分销、自提核销等业务模型。理解订单状态机、库存防超卖、支付回调幂等基础原理是构建稳定系统的前提。基于Spring Boot、MySQL、Redis的技术栈通过合理的表结构设计与分布式锁应用可有效应对集中下单的并发压力。这类架构广泛应用于小区买菜、生鲜配送等场景尤其适合需要二次开发与定制化改造的源码项目。内容涵盖需求拆解、核心接口实现与上线避坑提供一套可落地的小区团购系统源码设计参考。 做小区团购这个项目是我去年接过的一个实战需求。当时客户给了两句需求“让我们小区的人能在一个页面上下单买菜团长能收钱平台能管供货”。听起来不算复杂但真正动手拆解“小区团购系统源码”要包含哪些东西的时候才发现这里面既有常规电商的订单、支付、库存又有社区场景特有的团长分销、自提核销、佣金结算甚至还要考虑一个几百户的小区在晚高峰同一秒下单的场景。市面上所谓的“小区团购管理系统”很多但要么是前后端耦合在一起的老旧PHP要么是SaaS化改造不彻底、没法二次开发的闭源产品。基于Spring Boot的Java实现才是我真正顺手、也最适合拿去改造成定制化项目的技术路线。这篇文章不打算只讲“怎么跑起来”我会从需求拆解、技术选型、表结构设计到核心接口实现、上线后的坑完整走一遍。如果你正准备自己开发一套小区团购系统或者你手上有一套源码但不知道怎么改、怎么部署这篇应该能帮你省掉不少瞎折腾的时间。适合想拿Spring Boot做实操项目的Java工程师、准备毕业设计的在校生以及想给小区做私域电商的技术合伙人参考。1. 项目整体设计与需求拆解刚开始做这类系统很多人第一反应是照着淘宝京东的架构往上套用户中心、商品中心、订单中心、支付中心再弄个微服务网关。但实际上小区团购的业务模型跟传统电商有本质区别它是“预售集单自提”的模式不是即买即发。需求拆不透后面写得再花哨也是白搭。1.1 小区团购的业务模型小区团购的核心链路可以压缩成一句话平台组织一个团购场次团长把场次分享到小区群居民在小程序或H5里下单付款平台按场次集中采购商品配送到小区提货点居民去提货团长拿佣金。这个链路里最关键的是“场次”概念。传统电商没有场次商品挂上去就能买小区团购必须有场次比如“今晚8点截单明天下午3点自提”。没有场次约束就没法确定一次采购的订单总量更没法跟供应商谈运力和价格。所以我在设计时就确定了核心数据模型必须围绕activity团购活动场次展开而不是简单的商品SPU。除场次外小区团购还有一个特殊角色是团长。团长既是销售员负责拉群、分享、答疑又是提货点管理员负责收货、分拣、核销。平台、团长、居民三方的关系决定了权限模型必须拆开设计不能像普通商城那样只分管理员和普通用户。1.2 核心角色与用户链路一套小区团购管理系统至少需要四类角色平台管理员负责商品上下架、场次创建、供货价维护、团长审核、佣金结算、售后处理。供货商可选在更复杂的版本里供货商可以自己登录后台维护商品库存看到自己商品的销售数据和结算单。团长查看自己小区的订单、提醒居民提货、在后台或小程序里核销自提码、查看累计佣金。居民端用户浏览场次商品、下单支付、查看订单状态、到点提货、申请售后。用户链路里有一个很重要的交互节点是分享溯源。居民是通过哪个团长的分享链接下的单佣金就得算给哪个团长。这个在数据库里必须记录在订单上而且要保证稳定不能因为用户后续从别的入口进就改变归属。常见的做法是用户第一次点击分享链接时把captainId写入用户表的推广绑定字段同时把这一次下单的归属关系冗余到订单表里。1.3 功能模块与优先级划分跟所有项目管理一样做小区团购源码不能一上来就追求全功能。我按业务必要性把功能分成了三个优先级第一优先级没有就跑不了微信登录、小区选择、场次商品浏览、购物车下单、微信支付、后台商品管理、场次管理、订单管理、自提核销。这一轮做完系统已经能真实运营。第二优先级提升运营效率团长佣金结算、优惠券、限购与秒杀、提货通知公众号模板消息、售后流程、数据看板。第三优先级锦上添花多供应商分账、积分商城、配送路线规划、团长业绩排行榜、多小区拼团裂变。我建议任何做这套系统的人都按这个顺序迭代。很多时候客户会提一堆花里胡哨的需求比如“要做直播带货”但你得先把基础的集单、支付、核销跑通直播只是引流工具不能建立在还没走通的订单主链路上。2. 技术选型与核心依赖解析技术选型这部分我直接说结论用Spring Boot 2.7.x MySQL 8 Redis MyBatis-Plus就够用了。为什么不是Spring Boot 3先说清楚Spring Boot 3.x 基线是JDK 17很多云服务器上的JDK还是1.8如果你不打算升级就别硬选3.x。另外很多二次开发的老依赖比如某些支付SDK、Excel导入导出组件在Jakarta命名空间迁移后需要额外适配运维成本会高不少。当然如果是全新项目且团队Java基础好Spring Boot 3.2其实体验也不错只是你得提前验证依赖兼容性。2.1 为什么是 Spring Boot这个项目用Spring Boot而不是其他框架理由非常实在第一社区团购是典型的CRUD密集型业务Spring Boot的自动配置和Starter机制能把项目搭建时间从几天压缩到几小时第二Java生态里做微信支付、微信登录、消息推送都有成熟SDK遇到问题网上资料多第三Spring Boot项目对后期接运营商平台、接财务对账、做数据分析都有天然优势团队招人也容易。还有一个很实际的原因客户大概率会要求源码交付不管他是要做二次开发还是自己养团队维护。Java Spring Boot是市场上最容易找到维护人员的技术栈这决定了交付后的责任边界。2.2 关键依赖与版本选择我在项目里用到的核心依赖大概是这样组合的依赖版本用途Spring Boot2.7.18基础框架最后一个兼容JDK 8的高版本MyBatis-Plus3.5.3ORM简化单表CRUD和分页MySQL8.0.34主数据库存储事务性核心数据Redis6.2缓存、库存预扣、分布式锁、验证码存储Redisson3.20分布式锁和信号量封装Hutool5.8.x工具类免去手写各种工具方法Knife4j4.1.0接口文档跟Swagger无缝整合Sa-Token1.36登录鉴权比Shiro轻比Spring Security易用JustAuth1.16.6微信网页/小程序授权登录微信支付SDKv3 Java版微信支付、退款、账单下载XXL-Job2.4.0分布式定时任务调度后期引入这里特别想提醒一下尽量不要手动引入一堆互相依赖的第三方Jar。有的同学图省事从网上随便下载一个“mybatis-plus-boot-starter”版本跟Spring Boot版本不兼容启动直接报错。统一用一个父POM管理版本号能用官方BOM的就用BOM能少写一个版本号就少写一个。我后来遇到过好几次“本地好好的服务器上启动Bean冲突”的问题都是依赖版本漂移惹的祸。2.3 单体架构足够用别急着上微服务做小区团购系统绝大多数场景连微服务的边都不用沾。微服务解决的是团队规模、独立部署、资源隔离的问题而不是业务复杂度的问题。我一个同事之前给客户开发了“社区电商中台”强行拆了订单、用户、支付、商品四个服务结果客户小区总共才三百户一台2核4G的服务器跑四个Spring Boot实例动辄内存不足还引入了分布式事务的麻烦。我在这套系统里的结构是一个spring-boot-admin后台管理端服务 一个面向居民H5/小程序的API服务两个服务复用同一个数据库。有人会问为什么不分一个服务就好因为后台管理端的鉴权模型和接口风格跟C端差异很大塞在一起会让代码越来越乱。两个Spring Boot应用部署在同一台机器共用MySQL和Redis既能保证维护清晰又不会把运维复杂度拉上去。3. 数据库设计与核心表结构实现数据库设计是整个小区团购系统的地基这一块设计得不好后面写业务代码会不断被返工。我重点讲订单、活动、库存这几张牵扯并发和钱的核心表其他比如用户表、小区表、地址表相对常规一笔带过。3.1 业务表全景先给一个总览我实际建了大概18张表核心是下面这些表名用途关键字段community小区表名称、区域、提货点地址、经纬度user用户表openid、手机号、昵称、绑定团长ID、当前小区IDcaptain团长表用户ID、所属小区ID、审核状态、佣金比例product商品表(SPU)名称、图片、详情、单位、状态product_sku商品规格表(SKU)规格名、原价、供货价、总库存activity团购场次表场次名、开始时间、截单时间、自提时间、状态activity_product场次商品关联表场次ID、SKU ID、场次价、场次限购数、场次库存cart购物车表用户ID、SKU ID、数量、选中状态orders订单表订单号、用户ID、团长ID、商品总额、实付金额、状态、自提码order_item订单明细表订单ID、SKU ID、商品快照、单价、数量payment支付流水表订单ID、微信支付单号、支付金额、状态、支付时间refund_order退款单表原订单ID、退款金额、原因、状态、平台退款单号commission佣金结算表团长ID、订单号、佣金金额、结算状态pickup_record自提核销记录订单ID、核销人、核销时间coupon优惠券模板表面额、门槛、有效期、发放总量user_coupon用户优惠券表用户ID、券模板ID、状态、领取时间、使用时间这里有一个设计细节值得注意activity_product表独立于product_sku。为什么不让商品直接写死一个价格因为同一个商品在不同场次的促销价、供货价、限购数量都可能不同。比如黄瓜这个商品第一场卖3块9第二场供货价变了可能卖4块5把价格放在关联表里才能灵活支持每场上调下调。3.2 订单相关的核心约束订单表是整个系统最核心的表它的设计有几个我踩过坑后沉淀下来的经验第一订单状态必须离散且可流转。我的订单状态用整数表示0待支付、1已支付待配货、2配货中、3待自提、4已完成、5已取消、6退款中、7已退款。每个状态之间的流转必须由特定事件触发比如只有支付成功回调才能把0改成1。第二订单号不能依赖数据库自增ID。使用自增ID很容易被别人通过订单号摸清你的销量而且多表关联时不够优雅。我采用的是yyyyMMddHHmmss 雪花算法后6位 随机3位生成一个20位左右的订单号。第三金额字段必须用整数类型存分不要用Double/Float。用Decimal(10,2)虽然看着直观但Java里 BigDecimal 比较大小、求和处处麻烦。存分然后展示时除以100这是支付行业的标准做法。微信支付接口传入的金额单位也是分这样接口对接时连转换都省了。第四订单明细中的商品名称、规格、价格必须做快照。因为商品可能改价、改名甚至下架但历史订单的金额不能跟着商品表变动。这个问题可以用order_item里的冗余字段解决很多人设计数据库时觉得冗余不好但在订单这种需要留痕、不可变的场景里冗余就是必要的。3.3 索引设计与查询优化小区团购系统的查询场景相对固定我整理了一下最频繁的SQL基本是这些-- 用户首页看不到场的商品列表 SELECT a.name, ap.price FROM activity a JOIN activity_product ap ON ap.activity_id a.id AND ap.deleted 0 WHERE a.status 1 AND a.start_time NOW() AND a.end_time NOW() ORDER BY ap.stock DESC; -- 用户查看“我的订单” SELECT * FROM orders WHERE user_id ? AND deleted 0 ORDER BY create_time DESC LIMIT 20; -- 团长端查看本小区今日待提货订单 SELECT * FROM orders WHERE captain_id ? AND status 3 AND pickup_date CURDATE() ORDER BY create_time;这些SQL都不会太复杂但数据量上来之后缺索引就会肉眼可见地变慢。我给几个高频查询字段都加了组合索引orders(user_id, create_time)、orders(captain_id, status, pickup_date)、order_item(order_id)、activity_product(activity_id, sku_id)。一个常见问题是在orders表里给各种状态的组合都建索引导致索引过多。我遇到过一个极端案例一个开发小哥为了查询快给订单表建了7个索引结果每次插入都慢一倍。索引不是越多越好你的核心是user_id和captain_id两个维度围绕它们建两到三个组合索引就够了。4. 核心功能实现与关键代码解析这一部分我挑四个最常见的核心功能点来详细说下单与库存控制、支付回调、佣金结算、自提核销。这四个功能踩的坑最多写好了系统就稳了一大半。4.1 下单链路与库存防超卖小区团购虽然峰值远不如双十一但是在截单前最后一小时几百个人同时下单抢一个库存不多的爆款商品也完全可能发生超卖。防超卖有两种主流方案我实际用的是“数据库乐观锁 前置Redis兜底”组合。先看数据库层的乐观锁扣库存UPDATE product_sku SET stock stock - #{count}, version version 1 WHERE id #{skuId} AND stock #{count} AND deleted 0这条SQL的关键是stock #{count}条件。如果受影响行数为0说明库存不足直接抛出库存不足异常。这里不用先查再判因为“先查再判”在并发下必然有竞态窗口——两个请求同时查到了库存是5都判断可以买都去扣最终只能保证第一个成功第二个就会因为条件不满足而失败。数据库行锁天然帮我们做了并发控制。但如果你只依赖数据库扣库存在秒杀场景下数据库压力会很大。所以我前面加了Redis前置校验-- 扣减前检查与预扣 local stock redis.call(get, KEYS[1]) if not stock then return -1 -- 缓存不存在 end if tonumber(stock) tonumber(ARGV[1]) then redis.call(decrby, KEYS[1], ARGV[1]) return 1 -- 扣减成功 end return 0 -- 库存不足下单成功后通过消息队列异步把Redis里的预扣库存同步回数据库。这个方案的细节在于如果订单取消或者支付超时必须异步把Redis库存加回来否则会出现“缓存显示没货数据库其实有货”的线上事故。关于限购还有一个很重要的小细节一个用户在同一个活动里最多只能买N件同一个商品。这个判断不能只在前端做后端接口也要做。我在activity_product表里加了limit_count字段在下单时用一条SQL统计当天该用户在该活动下已购买的总量超过就直接拒绝。下单接口的整体流程可以简化成下面这个伪代码Transactional public OrderResult createOrder(CreateOrderRequest req) { // 1. 校验活动场次是否处于可下单时间段 Activity activity activityService.getById(req.getActivityId()); if (activity null || activity.getStatus() ! 1) { throw new BusinessException(该场次不存在或已结束); } // 2. 校验商品库存先走Redis再扣数据库 Sku stockResult stockService.deductStock(req.getSkuId(), req.getCount()); if (!stockResult.isSuccess()) { throw new BusinessException(库存不足); } // 3. 生成订单主表和明细表数据 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(req.getUserId()); order.setCaptainId(req.getCaptainId()); // 归属团长 // ... 其他字段 // 4. 计算优惠与实付金额 BigDecimal totalAmount calculateAmount(req); // 5. 创建支付单调用微信支付统一下单 WechatPayResult payResult wechatPayService.createOrder(order.getOrderNo(), totalAmount); return OrderResult.build(order, payResult); }值得注意的是这个createOrder方法用了Transactional但事务内部不要直接调用远程微信支付接口。远程调用如果耗时过长数据库连接会被长时间占用锁也会一直保持。我的做法是先生成订单数据并提交事务再调用支付接口收到微信返回的prepay_id之后更新订单的支付参数。如果调用支付接口失败就把订单状态标记为已取消给用户一个明显的提示。4.2 支付回调幂等支付回调是电商项目里最容易出问题的环节。微信支付会调用你配置的notify_url通知支付结果这个回调可能因为网络问题被发送多次也可能你处理完第一次之后第二次又来了。如果你不做幂等订单就可能从“已支付”被覆盖成“退款中”或者给用户发两次“支付成功”通知。我的做法分三步第一步在payment流水表里给transaction_id微信支付单号建唯一索引。这是天然的去重键因为同一个微信支付单号只会对应一笔真实支付。第二步回调处理逻辑整体包在分布式锁里锁的key用支付单号RLock lock redissonClient.getLock(PAY_NOTIFY: transactionId); boolean locked lock.tryLock(3, 30, TimeUnit.SECONDS); if (!locked) { return 正在处理中; } try { // 这里判断 payment 表是否已存在该 transaction_id // 存在且状态是 SUCCESS直接返回成功不再处理 // 不存在则插入流水并更新订单状态 } finally { lock.unlock(); }第三步更新订单状态时用“条件更新”而不是“set状态”。比如当前订单状态是0待支付才允许改成1已支付。如果SQL里只写set status 1不管原状态重复回调就一定会错乱。还有个小技巧微信支付回调处理完后返回值要写{code: SUCCESS, message: }不要只返回一个200。微信会根据响应体判断是否要重发如果你只返回200但没带正确响应体它依然会重试。4.3 团长佣金结算团长佣金这块如果只在订单完成时更新一个commission金额那大概率会在退款的场景里翻车。我的做法是把佣金计算做成一个独立的异步任务订单状态变为“已完成”后再触发。佣金计算规则是这样平台预先给每个团长设置佣金比例比如4%。佣金金额 订单实付金额扣除优惠券后 x 佣金比例。这里要注意如果用户用了平台发放的优惠券平台不能把券的优惠部分也算进团长佣金基数里。所以我会在结算时先减去优惠分摊额再乘以比例。退款场景的佣金处理如果订单已结算给团长用户申请退款那就从团长待结算或者已结算的佣金中扣除对应金额。最简单粗暴的方案是“退款时先把该订单已产生的佣金记录标记为无效”然后从待结算余额里扣掉。如果团长已经提现过那就会产生负余额下一笔佣金先抵扣负数。这块逻辑我建议在上线前用Excel把几十种组合提前算一遍比如用户使用优惠券下单订单完成团长佣金怎么算用户下单后未支付订单取消佣金记录会不会生成订单支付成功后用户部分退款佣金按比例扣还是全额扣团长A分享用户下单用户退款后再次购买佣金算给A还是不算这些边界逻辑不梳理清楚上线后的对账会成为运营每天晚上最头疼的事。4.4 提货核销与消息提醒自提核销是小区团购有别于一般电商的核心体验环节。我设计的流程是订单进入“待自提”状态后系统生成一个6位数的自提码发送到用户手机和订单详情页。居民到提货点后把自提码给团长团长在团长端输入号码或者扫描二维码系统校验通过后订单变为“已完成”。自提码的设计有隐藏坑不能直接用订单号后6位因为连续订单的订单号有规律可能被猜出别人的码。我生成的是一个纯随机6位数字同时存到orders表的pickup_code字段里并加了pickup_code唯一索引。核销接口也要做幂等UPDATE orders SET status 4, pickup_time NOW() WHERE id #{orderId} AND status 3如果更新行数为0说明订单状态已经不是待自提直接提示“该订单已被核销或状态异常”避免团长重复核销同一个订单。消息提醒这块我一开始用的是普通短信成本太高后来切到微信服务号的模板消息订阅。用户在支付成功后小程序端通过订阅消息接口申请“订单提货通知”后台在截单时间或者商品到货后统一推送。这个体验其实比短信好因为模板消息里可以直接带上提货地址和自提码。5. 常见问题与排查技巧实录最后这部分我整理一下整个项目上线前后最常遇到的几个问题基本都是我在实际开发中踩过的坑有些问题的排查思路值得提前写在你的技术方案里。5.1 并发下单导致库存变负数症状用户反馈明明看到商品还有库存付款后订单却显示“支付失败”或“库存不足”。后台看数据库stock字段变负数。原因扣库存的SQL没有stock count条件或者代码里先查库存再手动减库存查出来的库存已经被别的请求改了。排查方式SELECT id, stock, version FROM product_sku WHERE id 1001;如果stock是负数基本可以断定扣减SQL没有加stock #{count}判断。修复就是改成前面4.1里那条带条件的UPDATE。另外如果扣减走的是多个事务还需要检查事务隔离级别和锁超时时间。5.2 回调重复导致订单状态被覆盖症状用户支付成功后订单状态偶尔会从“待自提”回到“待支付”有时候订单被错误地标记为“已取消”。原因支付回调处理没有做幂等payment表没有唯一索引导致两条同样的回调同时进去后一条把前一条的状态覆盖了。排查方式查一下payment表里同一个transaction_id是否有两条记录有就是去重没生效。修复方案参考4.2节三步缺一不可。5.3 定时任务多实例重复执行症状部署两个后端实例后用户每天收到两条“提货提醒”消息截单后库存被两次同步数据错乱。原因Spring Boot自带的Scheduled注解是进程内调度多个实例会各自执行同一份任务。排查方式看应用日志里同一时刻是否有两个实例都在打印“执行提货提醒任务”。解决办法有两个一是引入XXL-Job用它的分片广播和阻塞策略管理任务二是给任务加Redis分布式锁锁的key用任务名过期时间设置比任务最长执行时间长。对于初期体量后者完全够用。5.4 接口被脚本刷与鉴权加固症状上线第一天就发现用户数涨得诡异后台看到一批头像相同、昵称全是数字的用户订单全部集中在同一个活动。原因接口没有做频率限制登录接口和下单接口被脚本批量调用。这里要注意小区团购一旦开始在小区群里传播一定会有人拿脚本抢低价商品、刷优惠券。排查方式查用户表看创建时间是否集中在数秒内看接口日志里同一IP的请求频率。修复措施分三层网关层Nginx对单个IP做QPS限制limit_req_zone。应用层用拦截器或AOP给核心接口加每分钟调用次数限制。我用的是RedissonRateLimiter一个注解就搞定。业务层用户单日下单次数、单日优惠券领取次数必须做数据库层校验。另外管理后台登录一定要开验证码不能用弱密码。我见过一个项目直接用admin/123456被人扫到后台后改了一堆价格团购直接做成了“慈善”。管理后台的操作日志至少保留6个月方便追溯。5.5 Spring Boot本地跑不起来和版本冲突这个我放到最后说因为绝大多数新手第一个卡点不是业务逻辑而是环境。最常见的报错是启动时ClassNotFoundException或者BeanCreationException基本都是依赖版本冲突。尤其是热词里提到的“Spring Boot版本太高”和“Spring Boot配置”这两个点我见过很多同学在pom里写了spring-boot-starter-parent 3.x但本地JDK还是8直接编译失败。处理这类问题我建议按下面的顺序排查# 1. 检查JDK版本 java -version # 2. 检查Maven是否用了正确的JDK mvn -version # 3. 查看依赖树定位版本冲突 mvn dependency:tree -Dverbose # 4. 如果依赖冲突在pom里对问题依赖显式指定版本号Spring Boot 2.7项目里如果同时引入了spring-boot-starter-data-redis和某个旧版本的jedis也很容易导致连接池配置失效。我的惯例是除了业务必须的第三方SDK比如微信支付其他的都用Spring Boot官方Starter统一管理不要自己手动引入多个容易冲突的底层客户端。写在后面的一些体会这套系统从设计到上线前后大概用了六周。回过头看小区团购不是一个技术壁垒很高的项目它更考验的是对业务细节的理解和对边界情况的处理。真正拉开项目质量差距的往往不是用了多新的技术而是有没有把订单状态机、库存扣减、佣金结算这些看似简单的基础逻辑想透彻。最后分享一个我自己的小习惯在开发阶段我会用Docker Compose把MySQL、Redis、应用服务一次性拉起来每次改动完代码跑一遍核心链路浏览商品、加购、下单、模拟支付回调、核销确认全链路没断再提交代码。这个动作虽然多花几分钟但帮我省掉的“凌晨上线翻车”次数至少是两位数。小区团购系统的源码本身不难找能真正根据自己业务灵活改造才是这套代码值钱的地方。本文还有配套的精品资源点击获取
返回列表