当前位置: 首页 > news >正文

拼单功能的设计实战

今天我们说一下拼单功能的设计实现。支付模型采用发起人统一支付,支付完成后通过群收款向参与者收取各自的费用。

拼单

可以简单理解为是多人协作下一笔订单。多个人选各自的商品,汇总成一笔订单统一履约(比如统一配送到同一个地址),由发起人统一支付。拼单改变的不是订单结构,而是订单的生成过程:多了一个「协作选品」的前置阶段。

业务流程

完整流程分三个阶段:

选品阶段:

  1. 发起人创建拼单组,选择门店和配送方式
  2. 系统生成一个10位唯一标识(uniqueId),发起人把拼单组链接分享给朋友
  3. 朋友通过链接加入拼单组,各自选商品
  4. 选完的人点「确认」,等待其他人选完
  5. 发起人确认所有人选完后,点「去下单」,系统锁定拼单组

下单支付阶段:

  1. 拼单组锁定后,发起人进入正常下单流程,提交订单时携带拼单组的uniqueId
  2. 系统把各人的选品合并成一笔订单,发起人支付
  3. 支付和普通订单完全一致,一个人付,一笔钱

费用分摊阶段:

  1. 支付完成后,系统计算每个参与者应付的金额
  2. 发起人通过群收款向参与者收取各自应付的费用

几个关键约束:

  • 一人一组:同一用户在同一时间只能加入一个进行中的拼单组。创建新的之前必须退出已有的
  • 发起人不能退出:只能取消整个拼单组。取消时系统会记录取消原因(比如「选的人太少」「门店太远」等),用于后续运营分析
  • 锁定与解锁:发起人锁定拼单组后进入下单流程,如果想修改可以解锁回到选品状态。但一旦订单已经生成,就不能再解锁了
  • 拼单组过期:拼单组创建后48小时内没有提交订单,自动过期。用消息队列的延时任务处理

订单域和支付域的改造

这是最容易被过度设计的地方。拼单系统对订单域和支付域的改动极小,小到可能出乎你的预期。

订单域:加一个类型值,加一张关系表

orders表的改动只有一处:在已有的order_type字段里新增一个枚举值(比如4=拼单订单)。不新增字段,不改表结构。

为什么需要这个类型值?因为订单列表查询、客服后台筛选、运营数据统计,都需要快速知道一笔订单是什么来源。如果每次都要JOIN关系表才能判断是不是拼单订单,查询成本不合理。一个tinyint字段就能解决的事,没必要搞复杂。

拼单组和订单的详细关联用一张独立的关系表:

CREATETABLEorder_group_order(idbigintunsignedNOTNULLAUTO_INCREMENT,group_idbigintNOTNULLCOMMENT'拼单组ID',order_idbigintNOTNULLCOMMENT'订单ID',created_atdatetimeNOTNULLDEFAULTCURRENT_TIMESTAMP,PRIMARYKEY(id),KEYidx_group_id(group_id),KEYidx_order_id(order_id))COMMENT='拼单组与订单关系表';

order_type告诉你这是拼单订单,order_group_order告诉你它属于哪个拼单组。两者各司其职。

关系表的设计理由是:拼单是一个可插拔的功能模块。它可能上线、可能下线、可能做灰度发布。拼单组的详细数据(成员、选品、费用分摊)都在独立表中管理,不侵入订单核心表。哪天拼单功能下线,这些表直接废弃即可。

订单提交时,前端在提交参数中直接标明订单类型为拼单,同时携带拼单组的uniqueId。后端根据order_type写入订单表,根据uniqueId创建关系记录并把拼单组状态改为「已提交」。

支付域:回调里加一行

支付域唯一的改动是在支付成功回调里多调一个方法:

支付成功回调: → 正常标记订单已支付 → 更新拼单组状态为「已完成」 // 新增这一行 → 触发费用分摊计算

不改支付链路、不改支付接口、不改退款逻辑。一行状态同步,完事。

这就是拼单系统的一个反直觉的点:看起来「多人一起下单」应该对订单和支付产生很大影响,但实际上这两个核心域几乎不需要改。所有复杂度都收敛在订单生成之前的协作阶段,和支付完成之后的费用分摊。

拼单组表设计

拼单系统真正需要独立设计的数据模型在这里。

拼单组表 order_group

字段类型说明
idbigint主键
unique_idvarchar(10)唯一标识,用于分享链接和缓存Key
creator_idbigint发起人用户ID
shop_idbigint门店ID
address_idbigint配送地址ID,自取为0
statustinyint0选品中 1已提交 2已完成 3已取消 4已过期
create_channelvarchar(20)创建渠道(weapp/alipay/app)
share_channelvarchar(20)分享渠道
expire_atdatetime过期时间(创建时间+48小时)

拼单组成员表 order_group_member

字段类型说明
idbigint主键
group_idbigint拼单组ID
user_idbigint用户ID
join_channelvarchar(20)加入渠道
statustinyint0未选品 1选品中 2已确认 3已完成 4已取消 5已过期 6已退出

用户选品明细表 order_group_item

字段类型说明
idbigint主键
user_idbigint选购人
order_idbigint订单生成后回填
order_item_idbigint对应订单明细ID
quantityint数量
pricedecimal(10,2)结算价(含折扣后)
origin_pricedecimal(10,2)原价
discount_feedecimal(10,2)分摊优惠金额
promo_codevarchar(50)命中的活动编码
has_box_feetinyint是否需要包装费
add_item_channelvarchar(20)选品渠道

费用分摊表 order_group_fee_split

字段类型说明
idbigint主键
group_idbigint拼单组ID
order_idbigint订单ID
user_idbigint应付人
goods_amountdecimal(10,2)商品金额
delivery_feedecimal(10,2)分摊配送费
box_feedecimal(10,2)分摊包装费
discount_amountdecimal(10,2)分摊优惠金额
total_amountdecimal(10,2)应付总额

表设计速查

核心职责数据写入时机
order_group拼单组生命周期管理发起人创建时
order_group_member参与者管理和状态追踪用户加入时
order_group_item记录每人选了什么(含最终价格)订单提交时从Redis读取并持久化
order_group_order关联拼单组和订单订单提交时
order_group_fee_split记录每人应付多少支付成功后计算并写入
ordersl订单类型枚举值加多一个拼单的类型正常订单流程

选品阶段的协作设计

选品阶段的数据全部走Redis缓存,不落库。原因是选品阶段的数据变动非常频繁(加商品、改数量、删商品),而且有大量废弃数据(用户退出、拼单取消),如果每次操作都写数据库会产生大量无意义的IO。

Redis的Key结构按天分片:

order:group:{日期}:members:{uniqueId} → Hash,存储成员信息 order:group:{日期}:goods:{uniqueId} → Hash,存储各人选品 order:group:{日期}:status:{uniqueId} → String,拼单组状态快照

选品数据只在一个时刻持久化到数据库:发起人提交订单的那一刻。从Redis读取所有成员的选品数据,合并成订单明细写入orders相关表,同时写入order_group_item表记录「谁选了什么」。

这个设计带来两个好处:

  • 选品阶段的读写性能极高,纯内存操作
  • 拼单取消或过期时不需要清理数据库,Redis的TTL自动过期即可

Redis缓存TTL设为1天,配合拼单组48小时过期的业务规则,缓存一定不会比业务状态先失效。

费用分摊的计算

费用分摊在支付成功后触发计算。每个人应付多少钱,涉及四项费用的拆分:

商品费:每个人自己选的商品结算总价,已经在下单时确定。

配送费分摊:整单配送费按人头均分。配送费 ÷ 参与人数,保留两位小数,用HALF_UP舍入。

包装费分摊:按各人需要包装的商品件数占比分摊。如果A选了2杯需要包装的、B选了1杯需要包装的,总包装费6元,A承担4元,B承担2元。有些商品不需要独立包装(比如加料),通过has_box_fee字段标记。

优惠金额分摊:满减、优惠券等整单优惠,按各人商品原价占整单商品原价的比例分摊。公式:用户优惠 = 用户商品原价 ÷ 全单商品原价 × 总优惠金额

精度处理:分摊计算用BigDecimal,保留两位小数,HALF_UP模式。计算完所有人后,检查分摊优惠总和是否等于实际总优惠。如果有差额(通常是一分钱),把差额补到发起人的优惠里。如果发起人自己没选品(只帮别人下单),差额补给第一个参与者。

每人最终应付商品原价 - 分摊优惠 + 分摊配送费 + 分摊包装费

有一个边界校验:如果某个参与者计算出来的应付金额为0或负数(极端折扣场景),群收款时会报错,需要在前端提示发起人。

分摊项分摊规则精度处理
商品费各自商品结算总价,无需分摊下单时已确定
配送费人头均分HALF_UP保留两位小数
包装费按需包装的商品件数占比HALF_UP保留两位小数
优惠金额按商品原价占比差额归发起人

群收款

群收款不是微信的个人社交转账功能,而是微信官方提供给小程序的拼单群收款API

POST https://api.weixin.qq.com/wxa/business/groupBuy/createOrder

这是一个正式的微信小程序接口,需要小程序具备相应的权限。调用流程:

  1. 支付成功后,计算每个参与者应付金额
  2. 获取参与者的微信openId
  3. 构造请求体(包含每人的openId和金额),调用微信API
  4. 微信返回群收款页面,发起人可以分享到群聊
  5. 参与者在微信中看到收款通知,自行付款

群收款按钮的显示条件很严格:

  • 当前用户必须是发起人
  • 客户端必须是微信小程序
  • 拼单组的创建渠道必须是微信小程序
  • 拼单组的分享渠道也必须是微信小程序

只要有一个环节走了支付宝或原生App,群收款按钮就不展示。这是因为微信群收款API只能在微信生态内闭环。对于非微信渠道的拼单,发起人只能看到分摊明细,自行和参与者结算。

群收款的最大参与人数是100人。这是微信API的限制,超过100人调用会报错。这个限制更多是防御性校验。

群收款和订单履约是解耦的。发起人付完款,订单就开始制作配送。参与者给不给钱是发起人和参与者之间的社交问题,不影响订单流程。拼单天然依赖熟人关系做信任担保,陌生人之间不会拼单。

取消和退款

取消流程

取消有两个入口,对应两种场景:

手动取消(支付前):用户在待支付状态取消订单,触发拼单组状态变为「已取消」,所有成员状态变为「已取消」,清除Redis缓存。

超时取消(支付超时):订单超时未支付,由消息队列的延时消费者处理。拼单组状态变为「已过期」,所有成员状态变为「已过期」。

两种取消用不同的状态码区分(取消=3,过期=4),方便运营统计哪些拼单是主动放弃、哪些是忘了支付。

退款处理

退款没有额外的拼单逻辑。退款走正常订单退款流程,钱退到发起人账户。

为什么不把退款直接退给对应的参与者?因为参与者不是付款人。微信支付的退款只能退到原支付账户。参与者通过群收款给的钱是微信社交转账,不在订单系统的支付链路里,平台无法操作。

退款后费用分摊记录不删除,保留作为历史数据。发起人如果要把钱退给参与者,自行在微信里处理。

状态机

拼单组状态

状态值含义触发条件
0选品中创建拼单组后的初始状态
1已提交发起人点击下单,订单生成
2已完成关联订单支付成功
3已取消发起人主动取消 / 支付前取消订单
4已过期48小时未提交 / 订单支付超时

成员状态

状态值含义触发条件
0未选品刚加入拼单组
1选品中开始浏览菜单选商品
2已确认选完点确认
3已完成订单提交成功
4已取消拼单组被取消
5已过期拼单组超时
6已退出主动退出拼单组

两层状态之间只有一个联动点:订单支付成功时,拼单组从「已提交」变为「已完成」,同时触发费用分摊计算。

生产环境约束速查

约束项规则原因
拼单组过期时间创建后48小时防止僵尸拼单占用缓存和门店资源
群收款人数上限100人微信API限制
加入人数上限不限制加入时不校验,群收款时才校验100人
一人一组同一用户同一渠道同时只能在一个进行中的拼单组防止数据混乱
发起人退出不允许退出,只能取消发起人退出后无人能操作拼单组
锁定后修改未生成订单可以解锁,已生成订单不可逆防止订单和选品数据不一致
群收款渠道约束全链路微信小程序才显示群收款按钮微信API只在微信生态内可用
门店/地址修改发起人在提交前可修改灵活应对临时变更
选品数据存储全部在Redis,提交时才持久化高频读写+大量废弃数据不适合直接落库
取消原因记录独立表存储取消标签运营分析拼单取消原因

小结

拼单系统给人的第一印象是「多人协作下单」,直觉上会觉得订单模型和支付流程需要大改。但看过生产级别的实现后会发现,订单域和支付域的改造加起来不超过20行代码。一张关系表、一个支付回调hook,就把两个核心域打通了。

真正的工程量集中在两个地方:选品阶段的实时协作体验(Redis缓存方案、状态同步、多渠道支持),和费用分摊的准确计算(BigDecimal精度、差额处理、边界校验)。这两块做好了,产品体验就到位了。


最近在知乎出了

  • 「应付6000万会员的秒杀系统专栏」和
  • 「几亿用户,百万并发的C端商品系统实战」
  • 「技术团队DDD领域驱动设计三年落地实战」

专栏,感兴趣的可以订阅一下。至于知识星球的,可以搜:

  • 老码头的技术浮生录

它是一个能实际帮你解决难题的星球。有问题的,找知心的Sam哥,支持无限次语音一对一解决你遇到的难题。「另外后续我新写的所有对外的付费专栏,在星球内都是免费的,且可以拿到所有源代码。」

当前星球里免费看的专栏有:

  • 「几亿用户,百万并发的C端商品系统实战」
  • 「技术团队DDD领域驱动设计三年落地实战」

知识星球内后续将推出20+个付费专栏,覆盖电商全链路:

选购线用户会员营销线中后台
购物车服务营销系统订单系统
商品服务用户系统支付系统
菜单服务结算服务

从前台选购到中后台结算,星球成员全部免费,后续新增也不额外收费。

我的知乎账号:

  • SamDeepThinking
http://www.jsqmd.com/news/816414/

相关文章:

  • open_prj20_MPSOC概述
  • WebSocket安全审计:构建OpenClaw弱令牌检测工具BruteClaw
  • 为现有 OpenAI 兼容应用快速切换至 Taotoken 端点
  • 现场服务管理数字化转型的关键路径
  • OpenClaw仪表盘:基于Next.js的自托管自动化任务控制中心
  • 从零构建主权身份系统:DID与可验证凭证技术实践
  • 谷歌正式宣布Gemini Intelligence:AI不再是“对话机器人”,而是你真正的“数字员工”
  • 掌握多模态RAG:图文并茂的知识库构建与检索,小白程序员必备收藏指南
  • GitHub AI副驾驶实战:用run-gemini-cli自动化代码审查与Issue管理
  • 量化基石:深入解析盈利因子(RMW)和投资因子(CMA)
  • 抖音批量下载器终极指南:5步实现高效无水印视频下载
  • OpenClaw AI助手集成Rocket.Chat:实时通信与多账户配置详解
  • 【YOLO目标检测全栈实战】26 模型剪枝与量化:把YOLO塞进边缘设备的“瘦身”秘籍
  • Flutter+开源鸿蒙实战:企业级工具类APP开发教程(含第三方库适配)
  • 2026届学术党必备的AI写作工具实测分析
  • 2026年少儿编程哪家不踩雷?品牌资质、课程与教学模式全解析 - 科技焦点
  • 2026届最火的六大AI论文平台实际效果
  • Blender化学插件:3分钟创建专业级分子可视化模型
  • 终极简单指南:如何使用 Gofile 下载器轻松获取文件
  • 从 NIST 到 OpenID:AI Agent 身份与授权正在成为企业级 AI 落地的基础议题
  • 离线优先AI助手实战:本地部署PersonalTaskAgent,打造私有自动化工作流
  • 开源交易副驾驶OpenClaw:模块化架构与AI驱动的市场监控实践
  • Cursor Pro 免费使用终极指南:如何绕过限制实现AI编程助手永久激活
  • 超导量子计算中的弱耦合多模玻色存储器技术
  • 同一个故障为什么每个月都要出一次?谈谈 IT 问题管理
  • 从安装到精通:Beyond Compare 4在Linux下的那些隐藏技巧与高级配置
  • 告别硬编码:使用EasyPOI模板引擎动态生成复杂Excel报表
  • 基于华为海思与Openharmony开发一款爆品潮玩BubblePal巴波泡
  • 宝可梦跨世代存档管理终极指南:PKSM工具全面解析
  • 政企级无人机管理系统实战|从0到1的项目落地与私有化部署经验分享