拼团交易平台系统开发-学习日志
拼团交易平台系统
拼团交易平台系统,是我在日常使用拼多多、腾讯、京东等服务平台,交易支付时候,了解到这样的一种营销手段。它可以通过用户自传播方式增强交易量,也是拼多多最开始起家形成巨大规模的一个业务逻辑。因此非常感兴趣这样的系统,所以根据大厂分享的资料、与对应的架构师UP进行交流学习了,设计了这样一套系统。 该系统采用了 DDD 领域驱动设计进行建模,拆分领域模块边界,形成;活动领域、人群领域、交易领域,来构建拼团营销交易流程,达到试算、锁单、结算等步骤流程。这个过程中提炼了通用设计模式,规则树、责任链,可以非常有效的统一的治理流程编排实现。
系统设计
功能流程
- 首先,由运营配置商品拼团活动,增加折扣方式。因为有人群标签的过滤,所以可以控制哪些人可参与拼团。
- 之后,用户可见拼团商品并参与拼团。用户可自主分享拼团或者等待拼团。因为拼团有非常大的折扣刺激用户自主分享,以此可以节省营销推广费用。
- 最后,拼团完成,触达商品发货。这里有两种,一种运营手段是拼团成团稀有性,必须打成拼团才可以。另外一种是虚拟拼团,无论是否打成,到时都完成拼团。
研发设计流程图
项目描述
该项目参考拼多多交易购物拼团场景,调研中大厂相关营销业务场景和技术架构方案,设计实现了本套拼团营销服务系统,支持各类营销优惠(直减、折扣、N元购)。该系统以面向对象开发,运用 DDD 拆分领域边界,使用设计模式设计服务功能。提高系统的扩展性和可维护性。
2-3节:多线程异步数据加载
对通用设计模式树结构扩展出异步数据加载区,这样可以把接口实现中所需的数据前置到异步数据加载区完成加载操作。以此提高接口的响应效率。
之后,串联功能节点,并在 MarketNode 节点,添加数据加载操作。
新增加一个表 sku,也就是商品信息表,通过商品信息表获得当前商品的价格配置,以此来做商品的折扣计算。这块在实际生产中有两种实现方式,一种是每次都调用外部接口获取商品,另外一种是有商品统一同步库可以查询。我们这里通过一个统一的商品库进行处理。那么后续谁要对接这个系统,就调用sku商品库,同步好商品即可。
threadPoolExecutor 线程池配置的是 CallerRunsPolicy 策略。当线程池中的任务队列已满,并且没有空闲线程可以执行新任务时,CallerRunsPolicy 会将任务回退到调用者线程中运行。这种策略适用于不希望丢失任务且可以接受调用者线程被阻塞的场景。
2-4节:策略模式优惠折扣计算
通过策略模式处理多类型折扣方式的逻辑计算,同时定义抽象模板封装计算折扣优惠的执行过程,用于扩展后续人群标签的过滤。
- 动态折扣计算:根据活动配置选择对应的折扣算法(如满减、打折)。折扣是在数据库中配置的,按照类型包括;ZJ - 直减、MJ - 满减、ZK - 折扣、N - n元购。那么这些不同的类型就可以用策略模型进行包装,每个实现类专门负责自己的逻辑计算。
- 上下文传递:通过 DynamicContext 对象在流程中传递活动信息、商品信息和计算结果。
- 异步任务支持:使用线程池执行耗时操作(如查询活动配置、商品信息)。
- 服务路由:将处理结果传递给下一个节点(EndNode)。
2-5节:人群标签数据采集
以轻量化的方式构建人群标签数据,将人群数据写入到 Redis BitMap 用于后续使用。
Redisson 是基于 Redis 的 Java 驻内存数据网格(In-Memory Data Grid),提供了分布式和可扩展的 Java 数据结构(如分布式锁、分布式集合等)。其设计采用了接口与实现类分离的模式。
通过采集人群标签任务获取人群数据,暂时没有这类业务数据,所以先模拟一个用户数据,你也可以调整这里的数据为你需要的。
采集数据后,repository.addCrowdTagsUserId(tagId, userId); 写入到数据库表。注意 addCrowdTagsUserId 方法,写入后还会做 BitMap 存储。
这些操作完成后,会更新统计量。注意,目前的统计量更新是不准的,因为执行 addCrowdTagsUserId 操作,会有主键冲突,主键冲突直接拦截不会抛异常。那么更新人群标签的统计量会继续增加。你课程后续也会继续处理。 思考:1.利用 Redis 的原子性操作,将数据写入和统计更新合并为一个原子操作;2.使用消息队列异步处理统计更新,确保最终一致性。
执行完写库后,开始把数据写入到人群标签。 不过注意人群标签的存储不是字符串,所以要转行为长整型进行存放。
2-6节:拆分库表关联关系
让 MarketNode 节点的查询由原来方式改为先查询SC商品活动配置关联表,获得到活动ID,再查询活动信息。这期间如果有效的活动配置信息无,那么则走到 ErrorNode 节点,返回一个指定的错误码。
根据商品 ID 查询渠道商品活动配置信息 ,把对应的这个商品配置的活动ID查询出来。
2-7节:人群标签节点过滤
在整个首页营销试算流程中,需要添加一个新的人群标签节点 TagNode,来处理人群过滤的操作。
首先,添加一个新的 TagNode 节点,调整营销 MarketNode 节点完成业务功能后,流转到新的 TagNode 节点。
之后,在从个 TagNode 节点流转到 EndNode 结束节点。
group_buy_activity 表中 tag_scope 中配置1,2 代表需要过滤人群,限制可见性和参与性。比如这里的配置表示,一个参加活动的用户,如果不再人群范围内,既不允许看见活动,也不允许参与活动。如果只配置2,那么表示通过人群的用户,能看见,但不能参与。
那么我们这里就要做人群过滤限制,就要拿到这个1,2值,之后判断处理。
可见限制;方法聚合到到类中,判断是否配置了1。如果配置了,那么默认这个对应的值的结果就是 false,之后在判断是否在人群范围内,如果在人群范围内则为 true。
参与限制;方法聚合到到类中,判断是否配置了2。如果配置了,那么默认这个对应的值的结果就是 false,之后在判断是否在人群范围内,如果在人群范围内则为 true。
首先,判断 tagId 是否为空,如果为空则访问的可见性和参与性都为 true 即可。
之后,过滤人群标签,isWithin 为用户是否在人群范围内。visible || isWithin、enable || isWithin 只要有一个 true 则可以通过。
2-8节:动态配置开关操作
基于 Redis 发布/订阅处理动态配置的自研的实现。
步骤1;添加一个自定义注解,用于 Spring 扫描 Bean 对象的时候,可以直接管理这些配置了自定义注解的类的属性。
步骤2;给服务类的属性添加自定义注解。
步骤3;由 app 模块下的 config,添加一个动态配置管理的工厂,会自动的完成属性信息的填充和动态变更操作。原理就是通过Java反射来动态调整属性值。
步骤4;业务使用,会调用步骤2中的属性服务。当有配置操作变动的时候,则可以把配置信息直接刷新到内存属性上。
步骤5;配置的变更来自于这里,当调用 DCCController 时,会触发 Redis 的发布/订阅,动态值的变更,以此把类上的属性的值做变更。testRedisTopicListener 是一个监听 redis 发布/订阅消息的处理,之后动态设置 Redis 值,完事后更新类中 Redis 的属性值。
2-9节:拼团交易营销锁单
首先,团购的商品下单。下单过程分为创建流水单、锁定营销优惠(拼团、积分、券)、创建支付订单、唤起收银台支付、用户扫码支付、支付完成核销优惠等。
那么,这里用户以拼团方式下单,创建流水单完成后,需要与拼团系统交互,锁定营销优惠。更新流水单优惠金额和支付金额。接下来就可以创建支付单了(支付单需要最终的支付金额)。
注意,拼团表 group_buy_order 除了有目标量(target_count)、完成量(complete),还要有一个锁单量(lock_count),当锁单量达到目标量后,用户在此组织下,不能在参与拼团。直至这些用户支付完成达成拼团或者锁单超时回退支付营销,空出可参与锁单量,这样其他用户可以继续参与。
具体流程 首先,需要查询外部交易 outTradeNo 是否存在交易记录。如果存在未完成的订单,直接返回结果即可。这个是幂等的一个防护。如果不查询,最终也是会有数据库唯一索引拦截。
之后,判断拼团锁单是否完成目标量,如果已经完成了目标量则直接直接返回,让用户不能参与当前拼团。一般在并发情况下,如果多人选择一个拼团,那么查询拼团量可以有效拦截。如果没有拦截,最终访问数据,也会有数量判断拦截。注意这里会有数据库表的行级锁,如果每秒事务数(tps)量大,那么则需要加入 redis 操作库存量。
之后,开始做营销优惠试算。判断当前商品在拼团下应该优惠多少。确认完优惠后,开始锁单。最终返回给用户到界面展示,用户确认了支付所用到的营销优惠,那么在点击确认支付跳转收银台扫码支付即可。
2-10节:责任链抽象模板设计
责任链是一种简单的单链路结构,在工程中会有多个这样的单链,为了可以让不同的场景都能创建出自己的链,则需要解耦责任链的链路和执行,再有执行器处理。 单链 一个一个顺序执行,只支持一条链。
多例链路 多例链的设计要解耦链路和执行,把链路当做一个 LinkedList 列表处理,之后执行当做是单独的 for 循环。
2-11节:交易规则责任链过滤
完善拼团交易营销锁单的流程,增加锁单流程中的规则处理。
- 明确试算返回的优惠折扣和支付金额,为了更好的使用拼团营销试算。
- 人群标签过滤,计算折扣范围,限定人群优惠。
- 活动可用性校验:1.校验活动状态;2.校验活动时间;3.写入上下文;4.传递给下一节点
- 用户参与限制校验:1. 获取活动配置;2. 查询用户参与次数;3. 校验参与限制;4. 返回结果
- 责任链的规则创建完成后,就是在工厂类中构建责任链。
- 像是在实际的公司业务开发中,一个责任链会有很多这样的规则,因为要过滤用户的开户状态、授信状态、渠道状态、风控状态、额度状态等。那么这样的设计分层结构就非常好扩展和维护了。
2-12节:拼团组队结算统计
支付完成后,做拼团数量的统计结算。 首先,交易订单的营销结算,核心就是更新拼团队伍的参与人数数量。每完成一笔支付,就有一笔拼团进度数量+1。 之后,这里要知道,更新拼团订单的明细状态(交易完成)、写入回调任务表和更新拼团进度数量要在一个事务下完成。 另外,更新拼团的进度要判断,当前是否为最后一次拼团完结状态。比如计算剩余1个,即可完成拼团目标量,那么这最后一笔更新完成后,既是整个拼团队伍的进度完成了。
重构交易分层,用类的名称,拆分交易中的业务场景。以此完成单一职责的划分。单一职责的特点在于一个类,主要关心一类服务的设计。单一职责的好处是;降低复杂性 、提高可读性和可维护性 、提高复用性 、降低变更风险 。 因此 trade 领域下目前包括2部分服务,一个是营销交易锁单服务,另外一个是营销交易结算服务。
拼团结算实现
- 首先,依照于单一职责,设计出一个交易结算的服务。 之后,定义营销结算的方法。入参为交易支付的成功实体信息,出参为交易结算的实体。
- 查询拼团信息和组团信息.结算的过程分为查询外部的交易单号是否为拼团锁单订单,也就是说,之前这笔交易单号,参与过有效的锁单。 另外,商城类系统调用营销的要过滤是否有营销类信息,如果没有则不调用,这样会减轻对拼团类系统接口的压力。 最后,构建聚合对象,调用仓储层的 settlementMarketPayOrder 完成拼团类数据落库。
- 交易结算的过程分为;更新拼团订单明细状态、更新拼团达成数量,之后要判断当前这笔结算是否为最后的结算,如果是,还需要写入回调任务。 这里的回调任务,是在拼团结束后,回调商城系统,通知拼团完成。之后商城系统要对已经支付完成的订单进行发货。
2-14节:交易结算责任链过滤
本节需要实现一套规则链,来处理以下业务规则:SC渠道管控、有效的外部交易单号、结算实现是否为拼团时效内。
拼团表,group_buy_order 增加 valid_start_time(有效开始时间)、valid_end_time(有效结束时间) 字段。用于每笔交易结算时候,用结算时间判断是否匹配到拼团有效时间范围内。
拼团明细,group_buy_order_list 增加 out_trade_time(交易时间) 字段,记录每笔结算的订单结算的时间。随着状态更新的时候更新。
trade 领域下,lock 锁单。实体对象,修改名称。TradeRuleCommandEntity -> TradeLockRuleCommandEntity,TradeRuleFilterBackEntity -> TradeLockRuleFilterBackEntity 增加了 Lock 标识。便于在添加 TradeSettlementRuleCommandEntity、TradeSettlementRuleFilterBackEntity 时更好理解。
PayActivityEntity 添加 validTime,GroupBuyTeamEntity 添加 validStartTime、validEndTime
trade 领域下,settlement 结算服务中,使用责任链模板,实现营销交易规则的过滤。SCRuleFilter(SC黑名单管控过滤 DCCService 配置新的属性 scBlacklist)、OutTradeNoRuleFilter(外部交易单号有效性过滤)、SettableRuleFilter(交易时间是否在拼团有效时间内过滤)、EndRuleFilter(结束节点封装返回数据)
交易服务,TradePaySettlementEntity 调用 tradeSettlementRuleFilter 责任链方法,并返回相关的数据信息。
settlementMarketPayOrder 结算一个事务下操作,增加 updateOrderStatus2COMPLETE 更新时候添加 outTradeTime 时间。
2-14节:拼团回调通知任务
拼团组队交易结算完结后,实现一个回调通知的任务处理。本节的重点在拼团成团后,实现回调通知流程。回调的过程,需要在用户锁单时需要增加一个回调的地址,并在拼团完结后发起回调。
group_buy_order 在设计的时候有一个 notify_url 回调地址,本节我们修改库表添加上这个字段。并对工程中的 dao&po&mapper 操作,增加 notify_url 字段。
MarketTradeController 营销交易服务,lockMarketPayOrder 锁单接口入参对象,增加 notifyUrl 回调地址。并有 PayDiscountEntity 对象透传到 TradeRepository#lockMarketPayOrder 仓储操作。这样写到 group_buy_order 表就有回调地址了,等做回调操作的时候,就可以把这个地址写入到回调任务表中。
TradeSettlementOrderService#settlementMarketPayOrder 结算服务,需要把锁单记录中的 notify_url 拿到,放到 GroupBuyTeamEntity 中,这样在写入 notify_task 表记录的时候就可以把 notify_url 一起写入进去了。
基于 okhttp 框架,封装对 http 接口的调用。用于处理调用外部其他微服务,实现回调通知的处理。因为外部的接口是随着每个服务调用拼团写入进来的 http 请求地址,所以在封装这部分调用的时候,要允许动态透传请求地址。实现类写到 infrastructure 基础设置层的 gateway 调用外部网关层。实现类;GroupBuyNotifyService 提供方法;groupBuyNotify
在交易结算服务类 ITradeSettlementOrderService,定义执行结算回调通知接口,包括;execSettlementNotifyJob()、execSettlementNotifyJob(String teamId) 一个是有入参的,一个无入参。这样可以指定给某个拼团队伍做结算。结算的过程就是调用 GroupBuyNotifyService#groupBuyNotify 完成回调通知,并根据返回的结果更新 notify_task 表状态记录(成功、失败、重试),并记录回调次数,小于5次的时候都可以继续回调。
回调通知,可以分为两个阶段处理。一个是拼团完成后立即执行,另外一个任务补偿。立即执行是为了提供时效性,但因为远程的 http 调用受网络和服务的影响可能会失败,所以要增加一个任务补偿来做定时检查。其中立即执行在 TradeSettlementOrderService#settlementMarketPayOrder -> settlementMarketPayOrder 处理。另外定时任务在 GroupBuyNotifyJob 处理。
测试接口,trigger/http 下,增加 TestApiClientController 接口实现类,提供回调接口服务。这个是模拟的其他的微服务,将来要提供的接口。
回调服务,在基础设置层的 gateway 中实现。用于动态调用外部的接口,进行回调通知。
拼团UI和接口对接
结合 DeepSeek 等同类型 OpenAI 产品设计拼团 UI。 AI话术: 根据图片内容,编写web h5页面,要求如下;
- 纯 html、js、css 编写,请写在一个文件里。
- 注意页面样式美观,和拼多多电商页面一样效果
- 首页最上面是轮播图展示图,展示3张图,这三张图片,你可以都使用这个图片地址 https://bugstack.cn/images/article/product/book/mybatis-03.png?raw=true
放到 nginx 目录下,是为了后续我们做项目部署的时候,直接就可以交给 Nginx 转发管理了。
api 模块增加 IMarketIndexService 服务接口,封装首页信息查询。 trigger 模块实现 IMarketIndexService 接口的首页数据查询,以及在 MarketTradeController 实现结算接口。
拼团营销配置接口 注意,不要把页面的所有要的属性,都用一个个属性字段平铺到类中,那样以后维护会非常复杂。要思考这些属性的归属问题,定义不同的对象承载。 之后,根据页面诉求,定义三个对象;Goods - 商品、Team - 拼团、TeamStatistic - 统计。
接口的查询组装数据主要分为3部分;营销优惠试算、查询拼团组队、统计拼团数据。 其中,查询拼团组队 分为优先查询一个,个人的一条拼团数据显示在最上面。之后在随机查询2条其他人的拼团数据。查询的过程要先查询个人的明细数据,之后从明细数据获得拼团队伍teamId,在查询拼团队伍的信息,最后组装数据。随机查询可以查询2倍量的数据,之后在随机获取需要的量的数据。
queryInProgressUserGroupBuyOrderDetailListByOwner,组装个人用户数据。 queryInProgressUserGroupBuyOrderDetailListByRandom,随机组装非个人用户数据。 queryTeamStatisticByActivityId,拼团队伍统计。 这些数据的查询,也可以用 Redis 缓存优化以及使用定时任务+异步消息统计好,查询的时候直接使用。
settlementMarketPayOrder 为结算接口,这个接口就是对领域服务 settlementMarketPayOrder 包装即可。
ApiPost + IntelliJ IDEA ApiPost 插件,可以很方便的把接口同步到 ApiPost,这样就减少手动去处理了,而且更加准确。
结合 DeepSeek 等同类型 OpenAI 产品,辅助完成 UI 与服务端接口的对接。
2-16节:引入RabbitMQ分布式多端消费
引入 RabbitMQ 分布式技术框架,实现分布式消息消费和多服务消费的能力。 消息,是一种解耦服务间直接(http/rpc)调用的手段,以发送消息和接收消息的模式,完成业务流程的异步化处理。
增加拼团结算完成 MQ 触达方式,HTTP、MQ 触达,由调用方通过入参类型决定。 MQ 一般用在企业内的微服务系统间通信,因为企业内的微服务,共用了一套的 MQ 注册中心,MQ 可以更加高效的触达和分布式部署。而对于企业外的调用,与我们完全不是一个公司的系统,那么不再同一个微服务环境内,则需要通过 HTTP 方式这样标准的协议调用。如;支付宝支付完成回调、微信公众号发送消息后的回调,都是基于 HTTP 的方式实现。
用户创建营销锁单时,选择MQ、HTTP回调方式。
锁单链路
MarketTradeController#lockMarketPayOrder 营销锁单方法入口,LockMarketPayOrderRequestDTO 入参对象增加 NotifyConfigVO 回调配置。也就是让调用方自己设置入参的回调类型。在 LockMarketPayOrderRequestDTO 类中会内聚一些方法,方便参数设置。
ITradeLockOrderService#lockMarketPayOrder 领域方法,PayDiscountEntity 也需要添加 NotifyConfigVO 对象。只不过这个对象要放在 trade 领域下的值对象里。
数据库操作;GroupBuyOrder 添加回调类型,group_buy_order_mapper.xml 映射回调字段以及插入、查询时,都要增加 notify_type 字段。
TradeRepository#lockMarketPayOrder 锁单仓储存储时,设置字段值 .notifyType(notifyConfigVO.getNotifyType().getCode()) 写入到库里 groupBuyOrderDao.insert(groupBuyOrder); 这样用户在锁单的时候,就把要回调的类型写入进来了。
结算链路
拼团结算的过程,会先过滤 filter,在 SettableRuleFilter 过滤器中,执行拼团对象查询时候,在仓储层组装出拼团回调配置数据。
TradeRepository#settlementMarketPayOrder 结算入库数据,按照不同的回调类型,写入进 notify_task 表中。
根据拼团交易结算是否完成,使用多线程异步执行发送MQ或者HTTP回调。这部分异步操作就好,即时触达。即时失败了,也会有任务兜底操作。
TradePort 执行任务分为HTTP、MQ,来触达回调。
完善小型支付商城对拼团组队结算消息的处理,同时完成小型支付商城中支付交易结算消息的处理。这样我们整个系统就都具备分布式部署的能力了。也就是多个应用实例同时部署,一个MQ在一个应用实例消费宕机,可以被其他应用实例继续拉取消费。
MQ 具有解耦、消峰,最终一致性的特性。所以很多的分布式设计中,都会引入 MQ 来解耦复杂的业务流程,除了数据库事务处理外的流程节点,则由 MQ 进行驱动。 从拼团下单到锁单结算,完成后触达MQ消费。之后由小型支付商城消费 MQ 结算消息,变更订单状态,之后触达下一个支付结算的动作。在接收支付结算完成模拟发货。第二个MQ的发送不用写数据库任务来补偿,如果发MQ失败了,就直接抛异常重试继续发就可以。
在 listener 监听方法内,调用营销结算。这个营销结算其实就是以前 http 回调里的操作。现在迁移过来。
我们要在小型支付系统自己发送支付完成结算的MQ,在消费这个MQ。通过这个方式替换掉原来依赖于 Guava 的发布订阅模型。因为 Guava 没法让不同实例间负载消费(也就是A1应用发送的消息,A2应用不能消费。而分布式架构技术栈 MQ 是可以的)。 生产者,topic_order_pay_success,是订单结算消息。分别在使用拼团和没有使用拼团结算发送消息。 消费者,topic_order_pay_success,消费触达结算动作。另外一个拼团组队成功的消息则是前面已经对接的。
2-17节:独占锁和无锁化场景运用
以独占锁抢占方式,迭代拼团结算通知互备执行任务。再以无锁化设计,处理用户拼团锁单,库存抢占处理,降低对数据库的行锁压力,提高整体吞吐量。 分段锁,颗粒度缩小到库存维度。先加(incr)后锁的操作,是一种无锁化设计。锁的目的只是作为兜底。这类似于我们操作账户,操作完写一条流水。incr 操作是原子的,基本不会产生一样的值。但在实际生产中,遇到过集群的运维配置问题,以及业务运营配置数据问题,导致 incr 得到的值相同。 独占锁,在分布式架构系统设计中,会有多个实例部署。这些实例都会做job任务的执行,为了保障既能让任务互备,同时不要重复执行。这里要加独占锁,谁抢占到谁执行。执行完成后,释放锁,下一轮继续抢占。
独占锁
在执行任务时,要抢占一个锁,谁抢占到谁就执行。 这种独占锁的设计,也是大家最为常用的。但有一些场景是不适合使用的,比如,集中式库存的抢占,这类场景如果使用独占锁,就会出现排队现象,所有的竞争用户都要等待上一个释放锁才能继续执行。这样会大大的降低系统的吞吐量。
无锁化
组队库存规则:首先,如果 teamId 为空,则不需要做库存的抢占。抢占库存是在一个 team 已经创建完成后,再有用户开始参与抢占时候,这个时候要做库存的缓存扣减处理。 之后,从上下文中获取到组队的目标量,有效期时间,以及组队的key和恢复量key。这个恢复量的用途是我们扣了redis中组队的任务缓存,但这个时候,发生异常了。那么我们要记录一个这样的数据。等做库存使用对比量的时候,可以用 target 目标量 + 恢复量一起来比。
占用库存:occupyTeamStock 占用库存操作,先获取恢复量。之后和 target 目标量 + recoveryCount 恢复量做对比。 库存扣减后,添加一个锁,这个锁不会影响整体效率。只是一个兜底操作。如注释说明。