拼团交易系统设计

功能流程、策略模式、锁单机制、营销结算、系统架构

Posted by PyroHao on 2025-10-25
Estimated Reading Time 49 Minutes
Words 11.1k In Total
Viewed Times

一、功能分析

拼团交易系统是一个支持多人组团购买商品以获取优惠价格的营销系统。系统核心功能围绕活动配置用户参团拼团达成三个主要阶段展开。

1.1 功能流程概述

根据功能流程图,拼团交易系统包含以下核心功能模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌─────────────────────────────────────────────────────────────────┐
│ 拼团交易系统功能架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 运营配置端 │ │ 用户参与端 │ │ 订单履约端 │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │• 活动配置 │ │• 浏览商品 │ │• 支付回调 │ │
│ │• 商品管理 │ │• 发起拼团 │ │• 成团判断 │ │
│ │• 折扣策略 │───▶│• 参与拼团 │───▶│• 订单发货 │ │
│ │• 人群标签 │ │• 分享邀请 │ │• 退款处理 │ │
│ │• 成团规则 │ │• 支付订单 │ │• 虚拟成团 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘

1.2 运营配置阶段

首先,由运营配置商品拼团活动,增加折扣方式。因为有人群标签的过滤,所以可以控制哪些人可参与拼团。

核心配置项:

配置项 说明 示例
活动基本信息 活动名称、时间、状态 “双十一拼团大促”
参与商品 指定参与拼团的 SKU iPhone 15 Pro
拼团价格 成团后的优惠价格 ¥6999(原价¥7999)
成团人数 达到此人数即成团 3人团、5人团
成团时限 从开团到必须成团的时间 24小时
人群标签 可参与用户的筛选条件 新用户专享、VIP用户
成团类型 真实成团或虚拟成团 见下方说明
切量配置 灰度发布比例 10%用户可见
降级策略 系统过载时的降级方案 关闭拼团入口

成团类型说明:

  • 真实成团:必须达到指定人数才能成团,未成团则自动退款
  • 虚拟成团:无论是否达到人数,到期自动成团(系统模拟虚拟用户凑数)

规则校验机制:

根据功能流程图,系统需要进行多层规则校验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
┌─────────────────────────────────────────────────────────┐
│ 规则校验层 │
├─────────────────────────────────────────────────────────┤
│ │
1. 人群校验 │
│ • 用户标签匹配(新用户、VIP等) │
│ • 黑名单过滤 │
│ │
2. 有效期校验 │
│ • 活动是否开始/结束 │
│ • 用户参团次数限制 │
│ │
3. 切量控制(灰度发布) │
│ • 按用户ID哈希取模 │
│ • 逐步放量比例控制 │
│ │
4. 降级策略 │
│ • 系统负载过高时关闭拼团 │
│ • 库存不足时隐藏拼团入口 │
│ • 支付通道异常时暂停活动 │
│ │
5. 风控校验 │
│ • 防止恶意刷单 │
│ • 同一设备/IP限制 │
│ │
└─────────────────────────────────────────────────────────┘

1.3 用户参与阶段

之后,用户可见拼团商品并参与拼团。用户可自主分享拼团或者等待拼团。因为拼团有非常大的折扣刺激用户自主分享,以此可以节省营销推广费用。

用户参与流程(根据功能流程图):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
用户浏览商品 ──▶ 查看拼团活动 ──▶ 优惠试算 ──▶ 展示拼团信息


参与拼团/发起拼团


商品支付(折扣价)


展示拼团+分享

┌──────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
拼团系统记录 拼团超时失败 免拼下单
多人拼团 发起退单 直接成单
│ │ │
▼ ▼ ▼
团购回调处理 退款回调处理 直接发货
成团后发货 商品退单

优惠试算功能:

用户在查看拼团时,系统提供实时的优惠试算能力:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Service
public class GroupBuyTrialService {

/**
* 拼团优惠试算
*/
public GroupBuyTrialResult trial(GroupBuyTrialRequest request) {
// 1. 获取商品原价
Money originalPrice = productService.getPrice(request.getSkuId());

// 2. 获取拼团优惠
GroupBuyActivity activity = groupBuyActivityService
.getActivity(request.getActivityId());
Money groupBuyPrice = activity.getGroupBuyPrice();

// 3. 计算团长优惠(如有)
Money leaderDiscount = Money.ZERO;
if (request.isLeader()) {
leaderDiscount = activity.getLeaderDiscount();
}

// 4. 计算其他优惠(优惠券、积分)
Money otherDiscount = calculateOtherDiscounts(request);

// 5. 计算最终价格
Money finalPrice = groupBuyPrice
.subtract(leaderDiscount)
.subtract(otherDiscount);

return GroupBuyTrialResult.builder()
.originalPrice(originalPrice)
.groupBuyPrice(groupBuyPrice)
.leaderDiscount(leaderDiscount)
.otherDiscount(otherDiscount)
.finalPrice(finalPrice)
.savedAmount(originalPrice.subtract(finalPrice))
.build();
}
}

用户角色定义:

角色 说明 权限
团长 发起拼团的用户 可查看团进度、分享邀请、取消订单、享受团长优惠
团员 参与他人拼团的用户 可查看团进度、邀请好友
待参团用户 浏览中未下单的用户 可发起新团或参与现有团、查看优惠试算

免拼下单功能:

根据功能流程图,系统支持免拼下单模式:

  • 定义:用户可以选择不等待成团,直接以拼团价购买,立即成单发货
  • 适用场景
    • 库存充足且用户愿意立即购买
    • 运营配置的特定商品支持免拼
    • VIP用户特权
  • 实现方式:支付时跳过拼团等待阶段,直接标记为"已成团"并触发发货
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Service
public class GroupBuyService {

/**
* 免拼下单
*/
public OrderResult createInstantOrder(CreateOrderRequest request) {
// 1. 校验是否支持免拼
GroupBuyActivity activity = getActivity(request.getActivityId());
if (!activity.isAllowInstantBuy()) {
throw new BizException("该商品不支持免拼下单");
}

// 2. 创建订单(直接成单,跳过拼团等待)
GroupBuyOrder groupBuyOrder = GroupBuyOrder.builder()
.activityId(activity.getId())
.targetCount(1) // 目标1人(自己)
.completeCount(1)
.status(GroupBuyStatus.SUCCESS) // 直接标记成功
.successTime(LocalDateTime.now())
.build();

// 3. 创建交易订单并支付
TradeOrder tradeOrder = createTradeOrder(request, groupBuyOrder);

// 4. 立即触发发货流程(无需等待)
triggerDelivery(tradeOrder);

return OrderResult.success(tradeOrder.getOrderNo());
}
}

1.4 订单履约阶段

最后,拼团完成,触达商品发货。这里有两种,一种运营手段是拼团成团稀有性,必须打成拼团才可以。另外一种是虚拟拼团,无论是否打成,到时都完成拼团。

拼团结果处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌─────────────────────────────────────────────────────────┐
│ 拼团结果处理 │
├─────────────────────────────────────────────────────────┤
│ │
│ 拼团到期 │
│ │ │
│ ├──▶ 达到目标人数 ──▶ 拼团成功 ──▶ 触发发货 │
│ │ │ │
│ │ └──▶ 团购回调通知 │
│ │ │
│ └──▶ 未达到人数 │
│ │ │
│ ├──▶ 真实成团模式 ──▶ 自动退款 │
│ │ │ │
│ │ └──▶ 退款回调│
│ │ │
│ └──▶ 虚拟成团模式 ──▶ 系统补单 ──▶ 发货 │
│ │
└─────────────────────────────────────────────────────────┘

团购回调机制:

根据功能流程图,拼团系统需要对外提供回调机制,通知业务方拼团结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@Service
public class GroupBuyCallbackService {

@Autowired
private RestTemplate restTemplate;

/**
* 拼团成功回调
*/
public void onGroupBuySuccess(GroupBuyOrder groupBuyOrder) {
CallbackRequest callback = CallbackRequest.builder()
.eventType("GROUP_BUY_SUCCESS")
.groupBuyOrderId(groupBuyOrder.getId())
.activityId(groupBuyOrder.getActivityId())
.userIds(getUserIds(groupBuyOrder))
.successTime(groupBuyOrder.getSuccessTime())
.build();

// 异步发送回调通知
sendCallback(groupBuyOrder.getCallbackUrl(), callback);
}

/**
* 拼团失败/退款回调
*/
public void onGroupBuyFailed(GroupBuyOrder groupBuyOrder) {
CallbackRequest callback = CallbackRequest.builder()
.eventType("GROUP_BUY_FAILED")
.groupBuyOrderId(groupBuyOrder.getId())
.activityId(groupBuyOrder.getActivityId())
.reason("TIMEOUT") // 或 "REFUND"
.build();

sendCallback(groupBuyOrder.getCallbackUrl(), callback);
}

/**
* 发送回调(带重试机制)
*/
private void sendCallback(String callbackUrl, CallbackRequest request) {
int maxRetry = 3;
for (int i = 0; i < maxRetry; i++) {
try {
ResponseEntity<String> response = restTemplate.postForEntity(
callbackUrl, request, String.class
);
if (response.getStatusCode().is2xxSuccessful()) {
log.info("回调发送成功,url={}", callbackUrl);
return;
}
} catch (Exception e) {
log.error("回调发送失败,重试次数={},url={}", i + 1, callbackUrl, e);
if (i < maxRetry - 1) {
Thread.sleep(1000 * (i + 1)); // 指数退避
}
}
}
// 重试耗尽,记录到补偿表
saveFailedCallback(callbackUrl, request);
}
}

直接购买/放弃拼团:

用户在支付前可以选择:

  1. 直接购买:放弃拼团优惠,以原价直接购买并立即发货
  2. 放弃拼团:取消参团意向,返回商品详情页
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌─────────────────────────────────────────────────────────┐
│ 用户购买选择 │
├─────────────────────────────────────────────────────────┤
│ │
│ 查看拼团 ──▶ 优惠试算 │
│ │ │
│ ┌───────────┼───────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ 参与拼团 直接购买 放弃购买 │
│ (折扣价) (原价) (返回) │
│ │ │ │
│ │ └──▶ 立即发货 │
│ │ │
│ └──▶ 支付后等待成团 │
│ │
└─────────────────────────────────────────────────────────┘

alt text

二、业务流程设计

2.1 策略模式关系图

拼团交易系统采用**策略模式(Strategy Pattern)**来应对不同的拼团业务场景。通过定义一系列算法,将它们封装起来,并且使它们可以互相替换,从而让业务逻辑与具体算法解耦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
┌─────────────────────────────────────────────────────────────────┐
│ 拼团策略模式架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 策略接口 (GroupBuyStrategy) │ │
│ │ + validate(GroupBuyContext): boolean │ │
│ │ + calculatePrice(OrderContext): Money │ │
│ │ + canJoin(GroupBuyOrder, User): boolean │ │
│ │ + onSuccess(GroupBuyOrder): void │ │
│ │ + onFailure(GroupBuyOrder): void │ │
│ └─────────────────────────────────────────────────────────┘ │
│ △ │
│ ┌───────────────┼───────────────┐ │
│ │ │ │ │
│ ┌───────────┴───┐ ┌────────┴────┐ ┌──────┴──────┐ │
│ │ 新用户专享团 │ │ 普通拼团 │ │ 老带新团 │ │
│ │ (NewUser) │ │ (Normal) │ │ (Referral) │ │
│ └───────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ └───────────────┼───────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 策略上下文 (StrategyContext) │ │
│ │ - 持有当前策略实例 │ │
│ │ - 根据活动配置动态选择策略 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 策略工厂 (StrategyFactory) │ │
│ │ + getStrategy(StrategyType): GroupBuyStrategy │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘

策略模式在拼团系统中的应用:

策略类型 适用场景 核心逻辑
普通拼团策略 常规拼团活动 任意用户可参与,按人数成团
新用户专享策略 拉新活动 仅限新用户参与,老用户不可见/不可参团
老带新策略 裂变拉新 团长必须是老用户,团员必须包含新用户
VIP专享策略 会员权益 仅限VIP等级以上用户参与
阶梯拼团策略 人数越多折扣越大 根据实际成团人数计算最终折扣

代码实现示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// 策略接口
public interface GroupBuyStrategy {

/**
* 验证用户是否有资格参与
*/
boolean canJoin(GroupBuyOrder groupBuyOrder, User user);

/**
* 计算拼团价格
*/
Money calculatePrice(Product product, GroupBuyActivity activity);

/**
* 成团成功处理
*/
void onSuccess(GroupBuyOrder groupBuyOrder);

/**
* 成团失败处理
*/
void onFailure(GroupBuyOrder groupBuyOrder);

/**
* 获取策略类型
*/
StrategyType getType();
}

// 新用户专享策略实现
@Component
public class NewUserGroupBuyStrategy implements GroupBuyStrategy {

@Autowired
private UserService userService;

@Override
public boolean canJoin(GroupBuyOrder groupBuyOrder, User user) {
// 仅新用户可参与
return userService.isNewUser(user.getId());
}

@Override
public Money calculatePrice(Product product, GroupBuyActivity activity) {
// 新用户专享价 = 原价 * 新用户折扣
return product.getOriginalPrice()
.multiply(activity.getNewUserDiscount());
}

@Override
public void onSuccess(GroupBuyOrder groupBuyOrder) {
// 发送新用户专享优惠券
sendNewUserCoupon(groupBuyOrder);
}

@Override
public void onFailure(GroupBuyOrder groupBuyOrder) {
// 发送挽留优惠券
sendRetentionCoupon(groupBuyOrder);
}

@Override
public StrategyType getType() {
return StrategyType.NEW_USER;
}
}

// 策略工厂
@Component
public class GroupBuyStrategyFactory {

@Autowired
private List<GroupBuyStrategy> strategies;

public GroupBuyStrategy getStrategy(StrategyType type) {
return strategies.stream()
.filter(s -> s.getType() == type)
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Unknown strategy: " + type));
}
}

// 策略上下文
@Service
public class GroupBuyService {

@Autowired
private GroupBuyStrategyFactory strategyFactory;

public GroupBuyResult joinGroupBuy(GroupBuyRequest request) {
GroupBuyActivity activity = getActivity(request.getActivityId());
GroupBuyStrategy strategy = strategyFactory.getStrategy(activity.getStrategyType());

// 验证参与资格
if (!strategy.canJoin(request.getGroupBuyOrder(), request.getUser())) {
return GroupBuyResult.fail("不符合参与条件");
}

// 执行参团逻辑...
}
}

策略模式的优势:

  1. 开闭原则:新增拼团类型只需添加新策略类,无需修改原有代码
  2. 消除条件判断:避免大量 if-else 或 switch 代码
  3. 算法复用:相同逻辑可以抽象到父类或工具类
  4. 易于测试:每个策略可以独立单元测试

alt text

2.2 拼团交易订单泳道图

拼团交易订单泳道图展示了从用户下单到订单完成的完整业务流程,根据泳道图包含前端支付系统拼团系统DB支付宝五个参与方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
┌─────────────────────────────────────────────────────────────────────────────┐
│ 拼团交易订单业务流程泳道图 │
├──────────┬────────────┬──────────┬──────────┬──────────┬────────────────────┤
│ 前端 │ 支付系统 │ 拼团系统 │ DB │ 支付宝 │ 说明 │
├──────────┼────────────┼──────────┼──────────┼──────────┼────────────────────┤
│ │ │ │ │ │ │ │
│ 创建订单 │ │ │ │ │ 用户点击购买 │
│ │ │ │ │ │ │ │
│ └────┤▶ create_ │ │ │ │ 创建支付订单 │
│ │ pay_order│ │ │ │ │
│ │ │ │ │ │ │ │
│ │ ▼ │ │ │ │ │
│ │ ┌────────┐│ │ │ │ 检查是否存在 │
│ │ │存在掉单 ││ │ │ │ 掉单或未支付订单 │
│ │ │或未支付 │├────是────┤▶ 支付页面 │ │ 防止重复创建 │
│ │ │ 订单? ││ │ │ │ │
│ │ └────────┘│ │ │ │ │
│ │ │否 │ │ │ │ │
│ │ ▼ │ │ │ │ │
│ │ 创建订单 │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ └──────┼─────────▶│▶ save │▶ 持久化 │ 保存订单数据 │
│ │ │ │ │ │ │ │
│ │ 营销锁单 │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ └──────┤▶ lockMarket│▶ 锁单服务 │ │ 锁定拼团名额 │
│ │ │ PayOrder │ │ │ │ │
│ │ │◀───────────┤ │ │ │ │
│ │ ◀─────────┤ 返回折扣 │ │ │ │ 返回拼团优惠金额 │
│ │ │ │ │ │ │
│ │ 创建支付单 │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ └──────┼──────────┼──────────┤▶ alipay │ 调用支付宝 │
│ │ │ │ │ Client │ 订单结算服务 │
│ │ │ │ │ │ │ │
│ │ │ │ │ ▼ │ │
│ │ │ │ │ 支付成功 │ │
│ │ │ │ │ │ │ │
│ │ 更新订单状态│ │ │ │ │ │
│ │ (PAY_WAIT) │ │ │ │ │ │
│ │ │ │ │ │ │ │ │
│ │ └──────┼─────────▶│▶ update │▶ 持久化 │ 更新待支付状态 │
│ │ │ │ │ │ │
│ ◀───────┤ notify_url│ │ │ │ 返回支付页面 │
│ 支付页面 │ return_url│ │ │ │ (同步/异步回调) │
│ │ │ │ │ │ │ │
│ 完成支付 │ │ │ │ │ 用户扫码支付 │
│ │ │ │ │ │ │ │
│ │ ◀─────────┤◀─────────┤◀─────────┤ 回调服务 │ 支付宝回调通知 │
│ │ 回调服务 │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ ▼ │ │ │ │ │
│ │ 更新订单状态│ │ │ │ │
│ │(PAY_SUCCESS)│ │ │ │ │
│ │ │ │ │ │ │ │
│ │ └──────┼─────────▶│▶ update │▶ 持久化 │ 更新支付成功状态 │
│ │ │ │ │ │ │
│ │ │▶ settlement│ │ │ 拼团结算服务 │
│ │ │◀─────────┤ Market │ │ (异步处理) │
│ │ │ │ PayOrder│ │ │
│ │ │ ┌────┤ │ │ │ │
│ │ │ │ │ ▼ │ │ │
│ │ │ │ │ MQ │ │ 发送MQ消息 │
│ │ │ │ │ │ │ │ │
│ │ │ │ │ ▼ │ │ │
│ │ │ └────┤ 消费 │ │ 消费消息完成后续 │
│ │ │ │ │ │ │
│ ◀───────┤◀───────────┤◀─────────┤ │ │ 返回结果给前端 │
│ 返回结果 │ │ │ │ │ │
│ │ │ │ │ │ │
└──────────┴────────────┴──────────┴──────────┴──────────┴────────────────────┘

泳道图各阶段详解:

阶段 1:订单创建(前端 + 支付系统)

  • 前端调用 create_pay_order 创建支付订单
  • 支付系统首先检查是否存在掉单未支付订单
    • 若存在,直接返回已有的支付页面,防止重复创建
    • 若不存在,继续创建新订单
  • 创建订单后持久化到数据库

阶段 2:营销锁单(支付系统 + 拼团系统)

  • 支付系统调用拼团系统的 lockMarketPayOrder 接口
  • 拼团系统执行锁单服务,锁定拼团名额
  • 返回拼团折扣金额给支付系统
  • 关键设计:锁单在创建支付单之前,确保优惠金额准确

阶段 3:创建支付单(支付系统 + 支付宝)

  • 支付系统根据折扣后的金额创建支付单
  • 通过 alipayClient 调用支付宝订单结算服务
  • 获取支付页面 URL(notify_urlreturn_url
  • 更新订单状态为 PAY_WAIT(待支付)并持久化
  • 返回支付页面给前端

阶段 4:支付回调(支付宝 + 支付系统 + 拼团系统)

  • 用户在支付宝完成支付
  • 支付宝异步回调通知支付系统
  • 支付系统更新订单状态为 PAY_SUCCESS
  • 调用拼团系统的 settlementMarketPayOrder 进行拼团结算
  • 拼团系统发送 MQ 消息,异步处理成团逻辑

阶段 5:异步消费(拼团系统)

  • 消费 MQ 消息完成后续处理
    • 更新拼团人数
    • 判断是否成团
    • 触发发货流程

关键状态流转:

1
2
3
4
5
6
7
8
9
10
11
┌──────────┐    ┌────────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
│ INIT │───▶│ PAY_WAIT │───▶│PAY_SUCCESS│───▶│ SUCCESS │───▶│ FINISHED │
│ 初始化 │ │ 待支付 │ │ 支付成功 │ │ 已成团 │ │ 已完成 │
└──────────┘ └────────────┘ └──────────┘ └──────────┘ └──────────┘
│ │
│ │
▼ ▼
┌──────────┐ ┌──────────┐
CANCELLED│ │ FAILED │
│ 已取消 │ │ 拼团失败 │
└──────────┘ └──────────┘

防掉单机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Service
public class OrderCreateService {

/**
* 创建订单前检查掉单
* 防止用户重复点击或网络超时导致的重复订单
*/
public OrderCreateResult createOrder(CreateOrderRequest request) {
String userId = request.getUserId();
String activityId = request.getActivityId();

// 1. 检查是否存在未支付订单(掉单检查)
PayOrder existingOrder = payOrderRepository
.findUnpaidOrder(userId, activityId);

if (existingOrder != null) {
// 存在未支付订单,返回已有订单
log.info("存在未支付订单,直接返回,orderNo={}", existingOrder.getOrderNo());
return OrderCreateResult.success(existingOrder);
}

// 2. 检查是否存在处理中的订单(幂等性)
PayOrder processingOrder = payOrderRepository
.findProcessingOrder(userId, activityId);

if (processingOrder != null) {
// 存在处理中订单,返回等待
log.info("存在处理中订单,请稍后再试,orderNo={}", processingOrder.getOrderNo());
return OrderCreateResult.waiting(processingOrder);
}

// 3. 创建新订单
return createNewOrder(request);
}
}

alt text

2.3 营销交易结算流程

拼团交易的营销结算流程是整个系统的核心,根据营销交易结算图,分为结算规则过滤拼团状态更新回调执行三个主要部分。

2.3.1 结算流程概述(根据结算图)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
┌─────────────────────────────────────────────────────────────────────────────┐
│ 营销交易结算流程图(根据结算图) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────┐ ┌─────────────────────────────────────────┐ │
│ │ 结算规则过滤(左侧) │ │ 拼团状态更新(中间) │ │
│ ├─────────────────────────┤ ├─────────────────────────────────────────┤ │
│ │ │ │ │ │
│ │ 交易结算 │ │ 构建聚合对象(@Transactional) │ │
│ │ │ │ │ │ │ │
│ │ ▼ │ │ ▼ │ │
│ │ ┌──────────┐ │ │ 查询组团信息 │ │
│ │ │ 参数验证 │ │ │ │ │ │
│ │ └────┬─────┘ │ │ ▼ │ │
│ │ │ │ │ ┌─────────────────────────────────┐ │ │
│ │ ├──▶ 否 ──▶ 返回 │ │ │ 更新订单完成状态 │ │ │
│ │ │ │ │ └────┬────────────────────────────┘ │ │
│ │ ▼ 是 │ │ │ │ │
│ │ 结算规则过滤 │ │ ├──▶ 否 ──▶ 业务异常 │ │
│ │ │ │ │ │ │ │
│ │ ▼ │ │ ▼ 是 │ │
│ │ ┌──────────────┐ │ │ ┌─────────────────────────────────┐ │ │
│ │ │SCRuleFilter │ │ │ │ 更新拼团达成数量 │ │ │
│ │ │渠道黑名单拦截 │ │ │ └────┬────────────────────────────┘ │ │
│ │ └──────┬───────┘ │ │ │ │ │
│ │ │ │ │ ├──▶ 否 ──▶ null │ │
│ │ ▼ │ │ │ │ │
│ │ ┌──────────────────┐ │ │ ▼ 是 │ │
│ │ │OutTradeNoRuleFilter│ │ │ ┌─────────────────────────────────┐ │ │
│ │ │ 外部单号校验 │ │ │ │ 更新拼团完成状态 │ │ │
│ │ └──────┬───────────┘ │ │ └────┬────────────────────────────┘ │ │
│ │ │ │ │ │ │ │
│ │ ▼ │ │ ├──▶ 否 ──▶ null │ │
│ │ ┌──────────────────┐ │ │ │ │ │
│ │ │SettableRuleFilter│ │ │ ▼ 是 │ │
│ │ │ 有效时间校验 │ │ │ 查询拼团交易完成外部单号列表 │ │
│ │ └──────┬───────────┘ │ │ │ │ │
│ │ │ │ │ ▼ │ │
│ │ ▼ │ │ 写入回调任务 │ │
│ │ ┌──────────────┐ │ │ │ │ │
│ │ │EndRuleFilter │ │ │ └────────────────────────────────┐ │ │
│ │ └──────────────┘ │ │ │ │ │
│ │ │ │ ▼ │ │
│ └─────────────────────────┘ │ ┌─────────────────────────────────────────┐│
│ │ │ 回调执行(右侧) ││
│ │ ├─────────────────────────────────────────┤│
│ │ │ ││
│ │ │ 执行回调 ││
│ │ │ │ ││
│ │ │ ▼ ││
│ │ │ 获取分布式锁 ││
│ │ │ │ ││
│ │ │ ▼ ││
│ │ │ ┌──────────┐ ││
│ │ │ │ 回调方式 │ ││
│ │ │ └────┬─────┘ ││
│ │ │ │ ││
│ │ │ ┌───┴───┐ ││
│ │ │ ▼ ▼ ││
│ │ │ http mq ││
│ │ │ │ │ ││
│ │ │ ▼ ▼ ││
│ │ │ 执行Url 发送消息 ││
│ │ │ │ │ ││
│ │ │ └───┬───┘ ││
│ │ │ │ ││
│ │ │ ▼ ││
│ │ │ 解锁 ││
│ │ │ │ ││
│ │ │ ▼ ││
│ │ │ 返回结算信息 ──▶ 返回结果 ││
│ │ │ ││
│ │ └─────────────────────────────────────────┘│
│ │ │
│ └─────────────────────────────────────────────┘

流程说明:

根据营销交易结算图,整个结算流程分为三个部分:

  1. 结算规则过滤(左侧):对交易进行多维度校验,包括渠道黑名单、外部单号、有效时间等
  2. 拼团状态更新(中间):更新订单和拼团的各种状态,记录完成的交易
  3. 回调执行(右侧):通过HTTP或MQ方式通知业务方结算结果

2.3.2 拼团锁单机制详解

拼团表 group_buy_order 除了有目标量(target_count)、完成量(complete),还要有一个锁单量(lock_count),当锁单量达到目标量后,用户在此组织下,不能在参与拼团。直至这些用户支付完成达成拼团或者锁单超时回退支付营销,空出可参与锁单量,这样其他用户可以继续参与。

拼团订单状态字段设计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class GroupBuyOrder {

/** 拼团订单ID */
private String groupBuyOrderId;

/** 活动ID */
private String activityId;

/** 目标成团人数 */
private Integer targetCount;

/** 已完成人数(已支付) */
private Integer completeCount;

/** 锁单人数(已下单未支付) */
private Integer lockCount;

/** 团长用户ID */
private String leaderUserId;

/** 拼团状态 */
private GroupBuyStatus status;

/** 成团截止时间 */
private LocalDateTime expireTime;

/**
* 获取剩余可参与人数
*/
public int getRemainingCount() {
return targetCount - completeCount - lockCount;
}

/**
* 判断是否可参与
*/
public boolean canJoin() {
return status == GroupBuyStatus.IN_PROGRESS
&& getRemainingCount() > 0
&& expireTime.isAfter(LocalDateTime.now());
}
}

public enum GroupBuyStatus {
IN_PROGRESS, // 拼团中
SUCCESS, // 已成团
FAILED, // 拼团失败
VIRTUAL_SUCCESS // 虚拟成团
}

锁单量控制逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
┌─────────────────────────────────────────────────────────────────┐
│ 锁单量状态流转 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 初始状态:target_count = 5, complete = 0, lock_count = 0
│ │
│ 用户A下单 ──▶ lock_count = 1 (剩余可参与:4) │
│ │ │
│ ├──▶ 支付成功 ──▶ complete = 1, lock_count = 0
│ │ (剩余可参与:4) │
│ │ │
│ └──▶ 支付超时 ──▶ lock_count = 0
│ (剩余可参与:5) │
│ │
│ 用户B、C、D、E 同时下单 │
│ │ │
│ ▼ │
│ lock_count = 4 (剩余可参与:1) │
│ │ │
│ ├──▶ 其中3人支付成功 ──▶ complete = 3, lock_count = 1
│ │ │
│ └──▶ 1人支付超时 ──▶ lock_count = 0
│ │
│ 用户F 看到剩余1个名额,下单 │
│ │ │
│ ▼ │
│ lock_count = 1, complete = 3
│ │ │
│ ├──▶ 支付成功 ──▶ complete = 4
│ │ │ │
│ │ └──▶ 还差1人,继续等待 │
│ │ │
│ └──▶ 支付超时 ──▶ lock_count = 0
│ │ │
│ └──▶ 名额释放,其他用户可参与 │
│ │
└─────────────────────────────────────────────────────────────────┘

锁定的原子性保证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
@Service
public class GroupBuyLockService {

@Autowired
private StringRedisTemplate redisTemplate;

private static final String LOCK_KEY_PREFIX = "group_buy:lock:";
private static final long LOCK_TIMEOUT = 30; // 30分钟

/**
* 尝试锁定拼团名额
*/
public LockResult tryLock(String groupBuyOrderId, String userId) {
String lockKey = LOCK_KEY_PREFIX + groupBuyOrderId;

// 使用Redis Lua脚本保证原子性
String luaScript =
"local lockCount = redis.call('GET', KEYS[1] .. ':lock_count') " +
"local completeCount = redis.call('GET', KEYS[1] .. ':complete_count') " +
"local targetCount = redis.call('GET', KEYS[1] .. ':target_count') " +
"if tonumber(lockCount) + tonumber(completeCount) < tonumber(targetCount) then " +
" redis.call('INCR', KEYS[1] .. ':lock_count') " +
" redis.call('EXPIRE', KEYS[1] .. ':lock_count', ARGV[1]) " +
" return 1 " +
"else " +
" return 0 " +
"end";

Long result = redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(lockKey),
String.valueOf(LOCK_TIMEOUT * 60)
);

if (result != null && result == 1) {
// 锁定成功,记录用户锁单信息
recordUserLock(groupBuyOrderId, userId);
return LockResult.success();
} else {
return LockResult.fail("拼团名额已满");
}
}

/**
* 释放锁定的名额(支付超时或取消)
*/
public void releaseLock(String groupBuyOrderId, String userId) {
String lockKey = LOCK_KEY_PREFIX + groupBuyOrderId;

// 使用Redis Lua脚本保证原子性
String luaScript =
"local currentLock = redis.call('GET', KEYS[1] .. ':lock_count') " +
"if tonumber(currentLock) > 0 then " +
" redis.call('DECR', KEYS[1] .. ':lock_count') " +
" return 1 " +
"else " +
" return 0 " +
"end";

redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(lockKey)
);

// 清除用户锁单记录
clearUserLock(groupBuyOrderId, userId);
}

/**
* 锁定转完成(支付成功)
*/
public void lockToComplete(String groupBuyOrderId, String userId) {
String lockKey = LOCK_KEY_PREFIX + groupBuyOrderId;

String luaScript =
"redis.call('DECR', KEYS[1] .. ':lock_count') " +
"redis.call('INCR', KEYS[1] .. ':complete_count') " +
"return 1";

redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(lockKey)
);

// 更新用户状态为已完成
updateUserStatus(groupBuyOrderId, userId, UserGroupBuyStatus.COMPLETED);
}
}

2.3.3 结算规则过滤器实现

根据营销交易结算图,结算规则过滤包含多个过滤器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
/**
* 结算规则过滤器链
* 根据结算图:SCRuleFilter -> OutTradeNoRuleFilter -> SettableRuleFilter -> EndRuleFilter
*/
@Component
public class SettlementRuleFilterChain {

@Autowired
private List<SettlementRuleFilter> filters;

public FilterResult execute(SettlementContext context) {
for (SettlementRuleFilter filter : filters) {
FilterResult result = filter.doFilter(context);
if (!result.isSuccess()) {
return result;
}
}
return FilterResult.success();
}
}

/**
* 结算规则过滤器接口
*/
public interface SettlementRuleFilter {
FilterResult doFilter(SettlementContext context);
int getOrder();
}

/**
* SCRuleFilter:渠道黑名单拦截
*/
@Component
@Order(1)
public class SCRuleFilter implements SettlementRuleFilter {

@Autowired
private BlacklistService blacklistService;

@Override
public FilterResult doFilter(SettlementContext context) {
String channel = context.getChannel();
String userId = context.getUserId();

// 1. 检查渠道是否在黑名单
if (blacklistService.isChannelBlocked(channel)) {
return FilterResult.fail("当前渠道暂不支持该活动");
}

// 2. 检查用户是否在黑名单
if (blacklistService.isUserBlocked(userId)) {
return FilterResult.fail("用户无法参与该活动");
}

return FilterResult.success();
}

@Override
public int getOrder() {
return 1;
}
}

/**
* OutTradeNoRuleFilter:外部单号校验
*/
@Component
@Order(2)
public class OutTradeNoRuleFilter implements SettlementRuleFilter {

@Autowired
private TradeOrderRepository tradeOrderRepository;

@Override
public FilterResult doFilter(SettlementContext context) {
String outTradeNo = context.getOutTradeNo();

if (StringUtils.isBlank(outTradeNo)) {
return FilterResult.fail("外部单号不能为空");
}

// 检查外部单号是否已存在(幂等性)
TradeOrder existingOrder = tradeOrderRepository
.findByOutTradeNo(outTradeNo);

if (existingOrder != null) {
// 已存在,返回已有订单信息
context.setExistingOrder(existingOrder);
return FilterResult.skip("订单已存在");
}

return FilterResult.success();
}

@Override
public int getOrder() {
return 2;
}
}

/**
* SettableRuleFilter:有效时间校验
*/
@Component
@Order(3)
public class SettableRuleFilter implements SettlementRuleFilter {

@Override
public FilterResult doFilter(SettlementContext context) {
GroupBuyActivity activity = context.getActivity();
LocalDateTime now = LocalDateTime.now();

// 1. 检查活动是否开始
if (now.isBefore(activity.getStartTime())) {
return FilterResult.fail("活动尚未开始");
}

// 2. 检查活动是否已结束
if (now.isAfter(activity.getEndTime())) {
return FilterResult.fail("活动已结束");
}

// 3. 检查订单是否在有效时间内创建
if (context.getOrderCreateTime() != null) {
Duration duration = Duration.between(context.getOrderCreateTime(), now);
if (duration.toMinutes() > 30) {
return FilterResult.fail("订单已过期,请重新下单");
}
}

return FilterResult.success();
}

@Override
public int getOrder() {
return 3;
}
}

/**
* EndRuleFilter:结束过滤器(最后执行)
*/
@Component
@Order(4)
public class EndRuleFilter implements SettlementRuleFilter {

@Override
public FilterResult doFilter(SettlementContext context) {
// 可以在这里进行最终的校验或日志记录
log.info("结算规则过滤完成,context={}", context);
return FilterResult.success();
}

@Override
public int getOrder() {
return 4;
}
}

2.3.4 拼团状态更新与回调执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
/**
* 营销交易结算服务
*/
@Service
@Slf4j
public class MarketingSettlementService {

@Autowired
private SettlementRuleFilterChain filterChain;

@Autowired
private GroupBuyOrderRepository groupBuyOrderRepository;

@Autowired
private UserGroupBuyOrderRepository userOrderRepository;

@Autowired
private RedissonClient redissonClient;

@Autowired
private RocketMQTemplate rocketMQTemplate;

@Autowired
private RestTemplate restTemplate;

/**
* 交易结算(根据结算图)
*/
@Transactional
public SettlementResult settle(SettlementRequest request) {
// ========== 1. 结算规则过滤(左侧)==========

// 1.1 参数验证
ValidationResult validation = validateParams(request);
if (!validation.isSuccess()) {
return SettlementResult.fail(validation.getErrorMsg());
}

// 1.2 执行结算规则过滤链
SettlementContext context = buildSettlementContext(request);
FilterResult filterResult = filterChain.execute(context);

if (!filterResult.isSuccess()) {
if (filterResult.isSkip()) {
// 订单已存在,返回已有结果
return SettlementResult.success(context.getExistingOrder());
}
return SettlementResult.fail(filterResult.getErrorMsg());
}

// 1.3 查询组团信息,构建聚合对象
GroupBuyOrder groupBuyOrder = groupBuyOrderRepository
.findById(request.getGroupBuyOrderId());

// ========== 2. 拼团状态更新(中间)==========

// 2.1 更新订单完成状态
boolean orderUpdated = updateOrderStatus(request.getOrderId(), OrderStatus.COMPLETED);
if (!orderUpdated) {
throw new BizException("更新订单状态失败");
}

// 2.2 更新拼团达成数量
boolean countUpdated = incrementCompleteCount(groupBuyOrder.getId());
if (!countUpdated) {
throw new BizException("更新拼团数量失败");
}

// 2.3 检查是否成团,更新拼团完成状态
boolean isGroupComplete = checkAndUpdateGroupStatus(groupBuyOrder);

// 2.4 查询拼团交易完成外部单号列表
List<String> completedOutTradeNos = userOrderRepository
.findCompletedOutTradeNos(groupBuyOrder.getId());

// 2.5 写入回调任务
CallbackTask callbackTask = CallbackTask.builder()
.groupBuyOrderId(groupBuyOrder.getId())
.callbackUrl(groupBuyOrder.getCallbackUrl())
.callbackType(groupBuyOrder.getCallbackType())
.completedOutTradeNos(completedOutTradeNos)
.build();

saveCallbackTask(callbackTask);

// ========== 3. 回调执行(右侧)==========

executeCallback(callbackTask);

return SettlementResult.success();
}

/**
* 执行回调(支持HTTP和MQ两种方式)
*/
private void executeCallback(CallbackTask task) {
String lockKey = "callback:lock:" + task.getGroupBuyOrderId();
RLock lock = redissonClient.getLock(lockKey);

try {
// 获取分布式锁,防止重复回调
boolean acquired = lock.tryLock(5, 30, TimeUnit.SECONDS);
if (!acquired) {
log.warn("获取回调锁失败,taskId={}", task.getId());
return;
}

try {
CallbackType callbackType = task.getCallbackType();

if (callbackType == CallbackType.HTTP) {
// HTTP回调方式
executeHttpCallback(task);
} else if (callbackType == CallbackType.MQ) {
// MQ回调方式
executeMqCallback(task);
}

// 更新回调任务状态
updateCallbackStatus(task.getId(), CallbackStatus.SUCCESS);

} finally {
lock.unlock();
}

} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("回调执行被中断", e);
}
}

/**
* 执行HTTP回调
*/
private void executeHttpCallback(CallbackTask task) {
CallbackRequest request = CallbackRequest.builder()
.groupBuyOrderId(task.getGroupBuyOrderId())
.completedOutTradeNos(task.getCompletedOutTradeNos())
.timestamp(System.currentTimeMillis())
.build();

// 带重试的HTTP调用
int maxRetry = 3;
for (int i = 0; i < maxRetry; i++) {
try {
ResponseEntity<String> response = restTemplate.postForEntity(
task.getCallbackUrl(),
request,
String.class
);

if (response.getStatusCode().is2xxSuccessful()) {
log.info("HTTP回调成功,url={}", task.getCallbackUrl());
return;
}
} catch (Exception e) {
log.error("HTTP回调失败,重试次数={},url={}", i + 1, task.getCallbackUrl(), e);
if (i < maxRetry - 1) {
Thread.sleep(1000 * (i + 1));
}
}
}

// 重试耗尽,标记为失败
throw new CallbackException("HTTP回调失败,已重试" + maxRetry + "次");
}

/**
* 执行MQ回调
*/
private void executeMqCallback(CallbackTask task) {
CallbackMessage message = CallbackMessage.builder()
.groupBuyOrderId(task.getGroupBuyOrderId())
.completedOutTradeNos(task.getCompletedOutTradeNos())
.build();

Message<CallbackMessage> mqMessage = MessageBuilder
.withPayload(message)
.setHeader("KEYS", task.getGroupBuyOrderId())
.build();

SendResult sendResult = rocketMQTemplate.syncSend(
"group-buy-callback-topic",
mqMessage
);

if (!sendResult.getSendStatus().equals(SendStatus.SEND_OK)) {
throw new CallbackException("MQ回调发送失败");
}

log.info("MQ回调发送成功,groupBuyOrderId={}", task.getGroupBuyOrderId());
}
}

alt text

2.4 拼团锁单业务流程

拼团锁单业务是拼团系统的核心机制,根据锁单业务流程图,包含主流程营销优惠试算营销锁单三个主要部分。

2.4.1 锁单业务流程图(根据流程图)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
┌─────────────────────────────────────────────────────────────────────────────┐
│ 拼团锁单业务流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 主流程(左侧) │ │
│ ├─────────────────────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ 支付系统发起 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────┐ │ │
│ │ │ 参数验证 │ │ │
│ │ └────┬─────┘ │ │
│ │ │ │ │
│ │ ├──▶ 否 ──▶ Response(返回错误) │ │
│ │ │ │ │
│ │ ▼ 是 │ │
│ │ ┌──────────┐ │ │
│ │ │是否存在 │ │ │
│ │ │外部单号 │ │ │
│ │ └────┬─────┘ │ │
│ │ │ │ │
│ │ ├──▶ 是 ──▶ Response(返回已有订单) │ │
│ │ │ │ │
│ │ ▼ 否 │ │
│ │ ┌──────────┐ │ │
│ │ │ 锁单拦截 │ │ │
│ │ └────┬─────┘ │ │
│ │ │ │ │
│ │ ├──▶ 未达到 ──▶ 营销优惠试算 + 营销锁单 │ │
│ │ │ │ │
│ │ ▼ 达到拼团数量 │ │
│ │ ┌──────────┐ │ │
│ │ │ Response │ │ │
│ │ │ 返回结果 │ │ │
│ │ └──────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────┐ ┌─────────────────────────────────────────┐ │
│ │ 营销优惠试算(中间) │ │ 营销锁单(右侧) │ │
│ ├─────────────────────────┤ ├─────────────────────────────────────────┤ │
│ │ │ │ │ │
│ │ 活动策略工厂 │ │ 交易规则过滤工厂 │ │
│ │ │ │ │ │ │ │
│ │ ▼ │ │ ▼ │ │
│ │ RootNode │ │ ┌─────────────────────────────────┐ │ │
│ │ │ │ │ │ • ActivityUsabilityRuleFilter │ │ │
│ │ ▼ │ │ │ 活动可用性规则过滤器 │ │ │
│ │ SwitchNode │ │ │ │ │ │
│ │ │ │ │ │ • UserTakeLimitRuleFilter │ │ │
│ │ ▼ │ │ │ 用户参与次数限制过滤器 │ │ │
│ │ MarketNode │ │ │ │ │ │
│ │ │ │ │ │ • TeamStockOccupyRuleFilter │ │ │
│ │ ▼ │ │ │ 团队库存占用过滤器 │ │ │
│ │ TagNode │ │ └─────────────────────────────────┘ │ │
│ │ │ │ │ │ │ │
│ │ ▼ │ │ ▼ │ │
│ │ 活动是否存在? ──▶ 否 ──▶ │ │ 抢占Redis库存 │ │
│ │ │ │ │ │ │ │
│ │ ▼ 是 │ │ ├──▶ 否 ──▶ 响应库存不足 │ │
│ │ 是否可参与? ──▶ 否 ───▶ │ │ │ │ │
│ │ │ │ │ ▼ 是 │ │
│ │ ▼ 是 │ │ ┌──────────┐ │ │
│ │ 返回折扣信息 │ │ │ 库存加锁 │ │ │
│ │ │ │ │ └────┬─────┘ │ │
│ │ │ │ │ │ │ │
│ │ └─────────────────┼──┼───────┘ │ │
│ │ │ │ ▼ │ │
│ │ │ │ 生成TeamId/OrderId/bizId │ │
│ │ │ │ │ │ │
│ │ │ │ ▼ │ │
│ │ │ │ @Transactional │ │
│ │ │ │ 写入拼团记录 │ │
│ │ │ │ │ │ │
│ │ │ │ ▼ │ │
│ │ │ │ 返回结果 │ │
│ │ │ │ │ │
│ └─────────────────────────┘ └─────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

流程说明:

根据拼团锁单业务流程图,整个锁单流程分为三个部分:

  1. 主流程(左侧):处理支付系统发起的锁单请求,进行参数验证、外部单号检查、锁单拦截判断
  2. 营销优惠试算(中间):通过活动策略工厂计算优惠价格,验证活动可用性和用户参与资格
  3. 营销锁单(右侧):通过交易规则过滤工厂进行多维度校验,最终抢占Redis库存并写入拼团记录

2.4.2 锁单超时处理流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
┌─────────────────────────────────────────────────────────────────────────────┐
│ 锁单超时处理流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 延迟任务触发(30分钟到期) │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. 查询订单状态 │ │
│ │ • 检查订单是否已支付 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ├──▶ 已支付 ──▶ 忽略(正常流程已处理) │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 2. 获取分布式锁 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 3. 释放Redis锁单名额 │ │
│ │ • lock_count - 1 │ │
│ │ • 清除用户锁单记录 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 4. 更新订单状态 │ │
│ │ • 拼团订单:LOCKED → CANCELLED │ │
│ │ • 交易订单:待支付 → 已取消 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 5. 释放营销优惠 │ │
│ │ • 释放优惠券(如有) │ │
│ │ • 释放积分(如有) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 6. 发送通知 │ │
│ │ • 通知用户:拼团订单已超时取消 │ │
│ │ • 可附带挽留优惠券 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 7. 释放分布式锁 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

2.4.3 支付成功处理流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
┌─────────────────────────────────────────────────────────────────────────────┐
│ 支付成功处理流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 支付回调通知 │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. 幂等性校验 │ │
│ │ • 检查该支付流水是否已处理 │ │
│ │ • 防止重复处理 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ├──▶ 已处理 ──▶ 直接返回成功 │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 2. 获取分布式锁 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 3. 更新Redis计数 │ │
│ │ • lock_count - 1 │ │
│ │ • complete_count + 1 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 4. 更新订单状态 │ │
│ │ • 交易订单:待支付 → 已支付 │ │
│ │ • 拼团用户单:LOCKED → PAID │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 5. 核销营销优惠 │ │
│ │ • 核销优惠券(标记已使用) │ │
│ │ • 扣减积分 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 6. 判断是否成团 │ │
│ │ • 检查 complete_count >= target_count │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ├──▶ 未成团 ──▶ 取消延迟任务 ──▶ 释放分布式锁 ──▶ 结束 │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 7. 成团处理(达到目标人数) │ │
│ │ • 更新拼团订单状态:IN_PROGRESS → SUCCESS │ │
│ │ • 取消其他用户的延迟任务(已支付用户) │ │
│ │ • 触发库存锁定 │ │
│ │ • 发送成团通知(站内信、短信、微信) │ │
│ │ • 触发发货流程 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 8. 释放分布式锁 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

2.4.4 核心代码实现

根据拼团锁单业务流程图,代码实现分为三个主要部分:

1. 活动策略工厂(营销优惠试算)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
/**
* 活动策略工厂
* 负责计算拼团优惠价格
*/
@Component
public class ActivityStrategyFactory {

@Autowired
private GroupBuyActivityRepository activityRepository;

/**
* 构建策略节点树并执行优惠试算
* RootNode -> SwitchNode -> MarketNode -> TagNode
*/
public MarketResult calculateDiscount(GroupBuyContext context) {
// 1. RootNode:根节点,初始化上下文
RootNode rootNode = new RootNode();
StrategyContext strategyContext = rootNode.execute(context);

// 2. SwitchNode:根据活动类型选择策略分支
SwitchNode switchNode = new SwitchNode();
strategyContext = switchNode.execute(strategyContext);

// 3. MarketNode:计算市场优惠价格
MarketNode marketNode = new MarketNode();
MarketResult marketResult = marketNode.calculate(strategyContext);

// 4. TagNode:验证用户标签和参与资格
TagNode tagNode = new TagNode();
boolean canJoin = tagNode.validate(strategyContext);

if (!canJoin) {
return MarketResult.fail("用户不符合参与条件");
}

return marketResult;
}
}

/**
* 策略节点抽象类
*/
public abstract class StrategyNode {
protected StrategyNode nextNode;

public void setNext(StrategyNode nextNode) {
this.nextNode = nextNode;
}

public abstract StrategyContext execute(StrategyContext context);
}

/**
* 根节点:初始化策略上下文
*/
@Component
public class RootNode extends StrategyNode {
@Override
public StrategyContext execute(StrategyContext context) {
// 加载活动配置
GroupBuyActivity activity = activityRepository
.findById(context.getActivityId());
context.setActivity(activity);

// 加载用户信息
UserInfo userInfo = userService.getUserInfo(context.getUserId());
context.setUserInfo(userInfo);

return context;
}
}

/**
* 开关节点:根据活动类型选择策略
*/
@Component
public class SwitchNode extends StrategyNode {
@Override
public StrategyContext execute(StrategyContext context) {
StrategyType strategyType = context.getActivity().getStrategyType();

// 根据策略类型设置不同的处理逻辑
switch (strategyType) {
case NEW_USER:
context.setStrategy(new NewUserStrategy());
break;
case REFERRAL:
context.setStrategy(new ReferralStrategy());
break;
default:
context.setStrategy(new NormalStrategy());
}

return context;
}
}

/**
* 市场节点:计算优惠价格
*/
@Component
public class MarketNode {

public MarketResult calculate(StrategyContext context) {
GroupBuyActivity activity = context.getActivity();

// 计算拼团价
Money groupBuyPrice = activity.getGroupBuyPrice();

// 计算团长优惠
Money leaderDiscount = Money.ZERO;
if (context.isLeader()) {
leaderDiscount = activity.getLeaderDiscount();
}

// 应用策略特定的优惠
Money strategyDiscount = context.getStrategy()
.calculateDiscount(context);

Money finalPrice = groupBuyPrice
.subtract(leaderDiscount)
.subtract(strategyDiscount);

return MarketResult.builder()
.originalPrice(activity.getOriginalPrice())
.groupBuyPrice(groupBuyPrice)
.leaderDiscount(leaderDiscount)
.strategyDiscount(strategyDiscount)
.finalPrice(finalPrice)
.build();
}
}

/**
* 标签节点:验证用户参与资格
*/
@Component
public class TagNode {

public boolean validate(StrategyContext context) {
GroupBuyActivity activity = context.getActivity();
UserInfo userInfo = context.getUserInfo();

// 1. 验证活动是否存在且有效
if (activity == null || !activity.isValid()) {
return false;
}

// 2. 验证用户标签
List<String> requiredTags = activity.getUserTags();
if (!CollectionUtils.isEmpty(requiredTags)) {
boolean hasTag = requiredTags.stream()
.anyMatch(tag -> userInfo.hasTag(tag));
if (!hasTag) {
return false;
}
}

// 3. 验证用户是否已参与
if (activity.isLimitOnePerUser() &&
hasUserJoined(activity.getId(), userInfo.getUserId())) {
return false;
}

return true;
}
}
2. 交易规则过滤工厂(营销锁单)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
/**
* 交易规则过滤工厂
* 根据流程图,包含多个规则过滤器
*/
@Component
public class TransactionRuleFilterFactory {

@Autowired
private List<RuleFilter> ruleFilters;

/**
* 执行规则过滤链
*/
public FilterResult executeFilters(FilterContext context) {
for (RuleFilter filter : ruleFilters) {
FilterResult result = filter.doFilter(context);
if (!result.isSuccess()) {
return result;
}
}
return FilterResult.success();
}
}

/**
* 规则过滤器接口
*/
public interface RuleFilter {
FilterResult doFilter(FilterContext context);
int getOrder(); // 过滤顺序
}

/**
* 活动可用性规则过滤器
*/
@Component
@Order(1)
public class ActivityUsabilityRuleFilter implements RuleFilter {

@Autowired
private GroupBuyActivityRepository activityRepository;

@Override
public FilterResult doFilter(FilterContext context) {
GroupBuyActivity activity = activityRepository
.findById(context.getActivityId());

// 1. 检查活动是否存在
if (activity == null) {
return FilterResult.fail("活动不存在");
}

// 2. 检查活动状态
if (activity.getStatus() != ActivityStatus.IN_PROGRESS) {
return FilterResult.fail("活动未开始或已结束");
}

// 3. 检查活动时间
LocalDateTime now = LocalDateTime.now();
if (now.isBefore(activity.getStartTime()) ||
now.isAfter(activity.getEndTime())) {
return FilterResult.fail("不在活动有效期内");
}

return FilterResult.success();
}

@Override
public int getOrder() {
return 1;
}
}

/**
* 用户参与次数限制过滤器
*/
@Component
@Order(2)
public class UserTakeLimitRuleFilter implements RuleFilter {

@Autowired
private UserGroupBuyOrderRepository userOrderRepository;

@Override
public FilterResult doFilter(FilterContext context) {
String userId = context.getUserId();
String activityId = context.getActivityId();

// 1. 检查用户在该活动下的参与次数
int joinCount = userOrderRepository
.countUserJoinTimes(userId, activityId);

if (joinCount >= context.getMaxJoinTimes()) {
return FilterResult.fail("您已达到该活动的参与次数上限");
}

// 2. 检查用户是否已在当前团中
if (context.getGroupBuyOrderId() != null) {
boolean alreadyInGroup = userOrderRepository
.existsByGroupBuyOrderIdAndUserId(
context.getGroupBuyOrderId(), userId);
if (alreadyInGroup) {
return FilterResult.fail("您已参与该拼团");
}
}

return FilterResult.success();
}

@Override
public int getOrder() {
return 2;
}
}

/**
* 团队库存占用过滤器
*/
@Component
@Order(3)
public class TeamStockOccupyRuleFilter implements RuleFilter {

@Autowired
private StringRedisTemplate redisTemplate;

private static final String STOCK_KEY_PREFIX = "group_buy:stock:";

@Override
public FilterResult doFilter(FilterContext context) {
String activityId = context.getActivityId();
String stockKey = STOCK_KEY_PREFIX + activityId;

// 1. 检查Redis库存
Long stock = redisTemplate.opsForValue().get(stockKey);
if (stock == null || stock <= 0) {
return FilterResult.fail("库存不足");
}

// 2. 预占库存(使用Lua脚本保证原子性)
String luaScript =
"local stock = tonumber(redis.call('GET', KEYS[1])) " +
"if stock > 0 then " +
" redis.call('DECR', KEYS[1]) " +
" return 1 " +
"else " +
" return 0 " +
"end";

Long result = redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(stockKey)
);

if (result == null || result == 0) {
return FilterResult.fail("库存不足");
}

// 记录预占的库存,用于回滚
context.setStockPreoccupied(true);

return FilterResult.success();
}

@Override
public int getOrder() {
return 3;
}
}
3. 锁单服务(整合上述组件)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
@Service
@Slf4j
public class GroupBuyLockOrderService {

@Autowired
private StringRedisTemplate redisTemplate;

@Autowired
private RedissonClient redissonClient;

@Autowired
private GroupBuyOrderRepository groupBuyOrderRepository;

@Autowired
private UserGroupBuyOrderRepository userGroupBuyOrderRepository;

@Autowired
private ActivityStrategyFactory strategyFactory;

@Autowired
private TransactionRuleFilterFactory filterFactory;

@Autowired
private RocketMQTemplate rocketMQTemplate;

private static final String LOCK_KEY_PREFIX = "group_buy:lock:";
private static final String DISTRIBUTED_LOCK_PREFIX = "distributed:lock:group_buy:";
private static final long LOCK_TIMEOUT_MINUTES = 30;

/**
* 锁单(核心方法)
* 根据流程图:主流程 -> 营销优惠试算 -> 营销锁单
*/
@Transactional
public LockOrderResult lockOrder(LockOrderRequest request) {
String activityId = request.getActivityId();
String userId = request.getUserId();
String distributedLockKey = DISTRIBUTED_LOCK_PREFIX + activityId;

// ========== 1. 主流程:基础校验 ==========

// 1.1 参数验证
ValidationResult validation = validateParams(request);
if (!validation.isSuccess()) {
return LockOrderResult.fail(validation.getErrorMsg());
}

// 1.2 检查是否存在外部单号(防掉单)
if (request.getExternalOrderNo() != null) {
GroupBuyOrder existingOrder = groupBuyOrderRepository
.findByExternalOrderNo(request.getExternalOrderNo());
if (existingOrder != null) {
return LockOrderResult.success(existingOrder.getId(), LOCK_TIMEOUT_MINUTES);
}
}

// 1.3 锁单拦截检查(如达到拼团数量)
if (isGroupFull(activityId)) {
return LockOrderResult.fail("该拼团已满员");
}

// ========== 2. 营销优惠试算 ==========

GroupBuyContext context = GroupBuyContext.builder()
.activityId(activityId)
.userId(userId)
.skuId(request.getSkuId())
.isLeader(request.isLeader())
.build();

// 2.1 调用活动策略工厂计算优惠
MarketResult marketResult = strategyFactory.calculateDiscount(context);
if (!marketResult.isSuccess()) {
return LockOrderResult.fail(marketResult.getErrorMsg());
}

// ========== 3. 营销锁单 ==========

// 3.1 获取分布式锁(防止并发超卖)
RLock distributedLock = redissonClient.getLock(distributedLockKey);
try {
boolean locked = distributedLock.tryLock(5, 10, TimeUnit.SECONDS);
if (!locked) {
log.warn("获取分布式锁失败,activityId={}", activityId);
return LockOrderResult.fail("系统繁忙,请稍后重试");
}

try {
// 3.2 执行交易规则过滤链
FilterContext filterContext = FilterContext.builder()
.activityId(activityId)
.userId(userId)
.groupBuyOrderId(request.getGroupBuyOrderId())
.maxJoinTimes(request.getMaxJoinTimes())
.build();

FilterResult filterResult = filterFactory.executeFilters(filterContext);
if (!filterResult.isSuccess()) {
return LockOrderResult.fail(filterResult.getErrorMsg());
}

// 3.3 抢占Redis库存并加锁
String lockKey = LOCK_KEY_PREFIX + activityId;
Long remainingCount = getRemainingCount(lockKey);

if (remainingCount <= 0) {
return LockOrderResult.fail("拼团名额已满");
}

boolean lockSuccess = lockQuota(lockKey, userId);
if (!lockSuccess) {
return LockOrderResult.fail("锁定名额失败,请稍后重试");
}

// 3.4 生成TeamId/OrderId/bizId并写入拼团记录
String groupBuyOrderId = createGroupBuyOrder(request, marketResult);

// 3.5 发送延迟消息(超时释放)
sendDelayReleaseMessage(groupBuyOrderId, userId, LOCK_TIMEOUT_MINUTES);

return LockOrderResult.success(groupBuyOrderId, LOCK_TIMEOUT_MINUTES);

} finally {
distributedLock.unlock();
}

} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("获取分布式锁被中断", e);
return LockOrderResult.fail("系统异常,请稍后重试");
}
}

/**
* 获取剩余名额
*/
private Long getRemainingCount(String lockKey) {
String luaScript =
"local target = tonumber(redis.call('GET', KEYS[1] .. ':target_count') or '0') " +
"local complete = tonumber(redis.call('GET', KEYS[1] .. ':complete_count') or '0') " +
"local lock = tonumber(redis.call('GET', KEYS[1] .. ':lock_count') or '0') " +
"return target - complete - lock";

return redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(lockKey)
);
}

/**
* 锁定名额
*/
private boolean lockQuota(String lockKey, String userId) {
String luaScript =
"local target = tonumber(redis.call('GET', KEYS[1] .. ':target_count') or '0') " +
"local complete = tonumber(redis.call('GET', KEYS[1] .. ':complete_count') or '0') " +
"local lock = tonumber(redis.call('GET', KEYS[1] .. ':lock_count') or '0') " +
"if target - complete - lock > 0 then " +
" redis.call('INCR', KEYS[1] .. ':lock_count') " +
" redis.call('HSET', KEYS[1] .. ':user_locks', ARGV[1], ARGV[2]) " +
" return 1 " +
"else " +
" return 0 " +
"end";

Long result = redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(lockKey),
userId,
String.valueOf(System.currentTimeMillis())
);

return result != null && result == 1;
}

/**
* 支付成功回调
*/
@Transactional
public void onPaymentSuccess(String groupBuyOrderId, String userId, String paymentId) {
String distributedLockKey = DISTRIBUTED_LOCK_PREFIX + groupBuyOrderId;
RLock distributedLock = redissonClient.getLock(distributedLockKey);

try {
if (!distributedLock.tryLock(5, 10, TimeUnit.SECONDS)) {
throw new RuntimeException("获取分布式锁失败");
}

try {
// 1. 更新Redis计数
String lockKey = LOCK_KEY_PREFIX + groupBuyOrderId;
String luaScript =
"redis.call('DECR', KEYS[1] .. ':lock_count') " +
"redis.call('INCR', KEYS[1] .. ':complete_count') " +
"redis.call('HDEL', KEYS[1] .. ':user_locks', ARGV[1]) " +
"return redis.call('GET', KEYS[1] .. ':complete_count')";

Long completeCount = redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(lockKey),
userId
);

// 2. 更新订单状态
userGroupBuyOrderRepository.updateStatus(
groupBuyOrderId, userId, UserGroupBuyStatus.PAID
);

// 3. 检查是否成团
GroupBuyOrder groupBuyOrder = groupBuyOrderRepository.findById(groupBuyOrderId);
if (completeCount != null && completeCount >= groupBuyOrder.getTargetCount()) {
onGroupBuySuccess(groupBuyOrder);
}

} finally {
distributedLock.unlock();
}

} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("处理支付回调被中断", e);
}
}

/**
* 超时释放(由延迟队列触发)
*/
@Transactional
public void onLockTimeout(String groupBuyOrderId, String userId) {
// 1. 检查订单状态
UserGroupBuyOrder userOrder = userGroupBuyOrderRepository
.findByGroupBuyOrderIdAndUserId(groupBuyOrderId, userId);

if (userOrder == null || userOrder.getStatus() != UserGroupBuyStatus.LOCKED) {
log.info("订单状态已变更,无需处理,groupBuyOrderId={}, userId={}",
groupBuyOrderId, userId);
return;
}

// 2. 释放名额
String lockKey = LOCK_KEY_PREFIX + groupBuyOrderId;
String luaScript =
"redis.call('DECR', KEYS[1] .. ':lock_count') " +
"redis.call('HDEL', KEYS[1] .. ':user_locks', ARGV[1]) " +
"return 1";

redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(lockKey),
userId
);

// 3. 更新订单状态
userGroupBuyOrderRepository.updateStatus(
groupBuyOrderId, userId, UserGroupBuyStatus.CANCELLED
);

// 4. 释放营销优惠
releaseMarketingBenefits(groupBuyOrderId, userId);

// 5. 发送通知
sendTimeoutNotification(userId, groupBuyOrderId);

log.info("锁单超时释放完成,groupBuyOrderId={}, userId={}", groupBuyOrderId, userId);
}

/**
* 成团处理
*/
private void onGroupBuySuccess(GroupBuyOrder groupBuyOrder) {
// 1. 更新拼团订单状态
groupBuyOrderRepository.updateStatus(
groupBuyOrder.getId(), GroupBuyStatus.SUCCESS
);

// 2. 锁定库存
lockInventory(groupBuyOrder);

// 3. 发送成团通知
sendGroupSuccessNotification(groupBuyOrder);

// 4. 触发发货流程
triggerDeliveryProcess(groupBuyOrder);

log.info("拼团成功,groupBuyOrderId={}", groupBuyOrder.getId());
}
}

alt text


三、系统架构设计

3.1 整体架构

拼团交易系统采用微服务架构,各服务之间通过消息队列和 RPC 进行通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
┌─────────────────────────────────────────────────────────────────────────────┐
│ 拼团交易系统架构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 接入层(Gateway) │ │
│ │ • 统一入口 • 限流熔断 • 鉴权认证 • 路由转发 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 应用服务层 │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ 拼团服务 │ │ 交易服务 │ │ 支付服务 │ │ 商品服务 │ │ │
│ │ │ GroupBuy │ │ Trade │ │ Payment │ │ Product │ │ │
│ │ │ - 活动管理 │ │ - 订单管理 │ │ - 支付处理 │ │ - 商品信息 │ │ │
│ │ │ - 锁单逻辑 │ │ - 流水单 │ │ - 回调处理 │ │ - 库存管理 │ │ │
│ │ │ - 成团判断 │ │ - 状态机 │ │ - 对账 │ │ - 价格计算 │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ 营销服务 │ │ 用户服务 │ │ 库存服务 │ │ 通知服务 │ │ │
│ │ │ Marketing │ │ User │ │ Inventory │ │ Notification│ │ │
│ │ │ - 优惠券 │ │ - 用户信息 │ │ - 库存锁定 │ │ - 短信 │ │ │
│ │ │ - 积分 │ │ - 人群标签 │ │ - 库存扣减 │ │ - 推送 │ │ │
│ │ │ - 活动规则 │ │ - 会员等级 │ │ - 库存回滚 │ │ - 站内信 │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 消息队列(MQ) │ │
│ │ • 延迟消息:锁单超时释放 │ │
│ │ • 异步通知:成团通知、发货通知 │ │
│ │ • 事件驱动:订单状态变更事件 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 数据存储层 │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ MySQL │ │ Redis │ │ Elasticsearch│ │ OSS │ │ │
│ │ │ 主数据库 │ │ 缓存/计数 │ │ 搜索引擎 │ │ 文件存储 │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

3.2 核心数据模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
-- 拼团活动表
CREATE TABLE group_buy_activity (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
activity_no VARCHAR(64) NOT NULL COMMENT '活动编号',
activity_name VARCHAR(128) NOT NULL COMMENT '活动名称',
product_id BIGINT NOT NULL COMMENT '商品ID',
sku_id BIGINT NOT NULL COMMENT 'SKU ID',
original_price DECIMAL(10,2) NOT NULL COMMENT '原价',
group_buy_price DECIMAL(10,2) NOT NULL COMMENT '拼团价',
target_count INT NOT NULL COMMENT '成团人数',
strategy_type VARCHAR(32) NOT NULL COMMENT '策略类型:NORMAL/NEW_USER/REFERRAL',
user_tags VARCHAR(256) COMMENT '可参与人群标签,JSON格式',
start_time DATETIME NOT NULL COMMENT '活动开始时间',
end_time DATETIME NOT NULL COMMENT '活动结束时间',
group_timeout INT NOT NULL DEFAULT 24 COMMENT '成团时限(小时)',
group_type TINYINT NOT NULL DEFAULT 1 COMMENT '成团类型:1-真实成团 2-虚拟成团',
status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-未开始 1-进行中 2-已结束',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_product (product_id),
INDEX idx_time (start_time, end_time),
INDEX idx_status (status)
) COMMENT='拼团活动表';

-- 拼团订单表(团单)
CREATE TABLE group_buy_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
group_buy_order_no VARCHAR(64) NOT NULL COMMENT '拼团订单号',
activity_id BIGINT NOT NULL COMMENT '活动ID',
leader_user_id BIGINT NOT NULL COMMENT '团长用户ID',
target_count INT NOT NULL COMMENT '目标人数',
complete_count INT NOT NULL DEFAULT 0 COMMENT '已完成人数',
lock_count INT NOT NULL DEFAULT 0 COMMENT '锁单人数',
status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-拼团中 1-已成团 2-拼团失败 3-虚拟成团',
expire_time DATETIME NOT NULL COMMENT '成团截止时间',
success_time DATETIME COMMENT '成团成功时间',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_order_no (group_buy_order_no),
INDEX idx_activity (activity_id),
INDEX idx_status (status),
INDEX idx_expire (expire_time)
) COMMENT='拼团订单表';

-- 用户拼团订单表
CREATE TABLE user_group_buy_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
group_buy_order_id BIGINT NOT NULL COMMENT '拼团订单ID',
user_id BIGINT NOT NULL COMMENT '用户ID',
trade_order_no VARCHAR(64) COMMENT '关联交易订单号',
status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-待锁定 1-已锁定 2-已支付 3-已取消',
lock_time DATETIME COMMENT '锁定时间',
pay_time DATETIME COMMENT '支付时间',
cancel_time DATETIME COMMENT '取消时间',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_user_group (group_buy_order_id, user_id),
INDEX idx_user (user_id),
INDEX idx_status (status),
INDEX idx_trade (trade_order_no)
) COMMENT='用户拼团订单表';

-- 拼团活动库存表(用于高并发场景,与Redis配合使用)
CREATE TABLE group_buy_inventory (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
activity_id BIGINT NOT NULL COMMENT '活动ID',
total_count INT NOT NULL COMMENT '总库存',
sold_count INT NOT NULL DEFAULT 0 COMMENT '已售数量',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_activity (activity_id)
) COMMENT='拼团活动库存表';

3.3 关键技术点

3.3.1 高并发锁单方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
┌─────────────────────────────────────────────────────────────────┐
│ 高并发锁单技术方案 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 问题:万人抢团,如何保证不超卖? │
│ │
│ 方案:Redis + Lua 原子操作 + 分布式锁 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. 热点数据预热 │ │
│ │ • 活动开始前将库存加载到 Redis │ │
│ │ • Key: group_buy:lock:{activityId}:target_count │ │
│ │ • Key: group_buy:lock:{activityId}:complete_count │ │
│ │ • Key: group_buy:lock:{activityId}:lock_count │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 2. 请求削峰 │ │
│ │ • 网关层限流(Sentinel) │ │
│ │ • 队列缓冲(用户进入等待队列) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 3. 原子扣减 │ │
│ │ • 使用 Lua 脚本保证原子性 │ │
│ │ • 避免并发导致的数据不一致 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 4. 异步落库 │ │
│ │ • 锁单成功后异步写入数据库 │ │
│ │ • 使用消息队列保证最终一致性 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘

3.3.2 延迟任务实现方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
/**
* 锁单超时延迟任务实现
*
* 方案对比:
*
* 1. JDK DelayQueue
* 优点:实现简单,精度高
* 缺点:单机、内存限制、重启丢失
* 适用:单机版、任务量小
*
* 2. Redis 过期监听
* 优点:实现简单,分布式
* 缺点:过期时间精度有限(默认1秒),可能丢失
* 适用:对精度要求不高的场景
*
* 3. RocketMQ 延迟消息(推荐)
* 优点:分布式、高可靠、支持大量消息
* 缺点:延迟级别固定(18个级别)
* 适用:大规模分布式系统
*
* 4. 时间轮算法(如 Netty HashedWheelTimer)
* 优点:精度高,性能好
* 缺点:单机、内存限制
* 适用:单机高性能场景
*/

// RocketMQ 延迟消息实现
@Service
public class DelayMessageService {

@Autowired
private RocketMQTemplate rocketMQTemplate;

private static final String DELAY_TOPIC = "group-buy-delay-topic";

/**
* 发送锁单超时延迟消息
*
* RocketMQ 延迟级别:
* 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
* level: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
*/
public void sendLockTimeoutMessage(String groupBuyOrderId, String userId, int delayMinutes) {
// 将分钟映射到 RocketMQ 延迟级别
int delayLevel = mapMinutesToDelayLevel(delayMinutes);

LockTimeoutMessage message = new LockTimeoutMessage();
message.setGroupBuyOrderId(groupBuyOrderId);
message.setUserId(userId);
message.setLockTime(LocalDateTime.now());

Message<LockTimeoutMessage> mqMessage = MessageBuilder
.withPayload(message)
.setHeader("KEYS", groupBuyOrderId + ":" + userId)
.build();

SendResult sendResult = rocketMQTemplate.syncSendDelayTimeSeconds(
DELAY_TOPIC,
mqMessage,
delayLevel
);

if (!sendResult.getSendStatus().equals(SendStatus.SEND_OK)) {
throw new RuntimeException("发送延迟消息失败");
}
}

/**
* 延迟消息消费者
*/
@Service
@RocketMQMessageListener(
topic = DELAY_TOPIC,
consumerGroup = "group-buy-delay-consumer"
)
public class LockTimeoutConsumer implements RocketMQListener<LockTimeoutMessage> {

@Autowired
private GroupBuyLockOrderService lockOrderService;

@Override
public void onMessage(LockTimeoutMessage message) {
log.info("收到锁单超时消息,groupBuyOrderId={}, userId={}",
message.getGroupBuyOrderId(), message.getUserId());

// 处理超时释放
lockOrderService.onLockTimeout(
message.getGroupBuyOrderId(),
message.getUserId()
);
}
}
}

3.3.3 分布式事务处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
/**
* 拼团下单涉及多个服务的分布式事务
*
* 场景:
* 1. 锁定拼团名额(拼团服务)
* 2. 锁定优惠券(营销服务)
* 3. 创建交易订单(交易服务)
*
* 方案:TCC 事务(Try-Confirm-Cancel)
*/

// TCC 接口定义
public interface GroupBuyTccAction {

/**
* 尝试锁定资源
*/
@TwoPhaseBusinessAction(name = "groupBuyLockTcc",
useTCCFence = true)
boolean tryLock(@BusinessActionContextParameter(paramName = "groupBuyOrderId") String groupBuyOrderId,
@BusinessActionContextParameter(paramName = "userId") String userId);

/**
* 确认(提交)
*/
boolean commit(BusinessActionContext context);

/**
* 取消(回滚)
*/
boolean rollback(BusinessActionContext context);
}

// TCC 实现
@Component
public class GroupBuyTccActionImpl implements GroupBuyTccAction {

@Autowired
private GroupBuyLockService lockService;

@Override
public boolean tryLock(String groupBuyOrderId, String userId) {
// 尝试锁定名额,设置预锁定状态
return lockService.tryPreLock(groupBuyOrderId, userId);
}

@Override
public boolean commit(BusinessActionContext context) {
String groupBuyOrderId = context.getActionContext("groupBuyOrderId");
String userId = context.getActionContext("userId");

// 确认锁定,将预锁定转为正式锁定
return lockService.confirmLock(groupBuyOrderId, userId);
}

@Override
public boolean rollback(BusinessActionContext context) {
String groupBuyOrderId = context.getActionContext("groupBuyOrderId");
String userId = context.getActionContext("userId");

// 取消锁定,释放资源
return lockService.releaseLock(groupBuyOrderId, userId);
}
}

// 业务服务中调用 TCC
@Service
public class GroupBuyBizService {

@Autowired
private GroupBuyTccAction groupBuyTccAction;

@Autowired
private CouponTccAction couponTccAction;

@Autowired
private TradeOrderTccAction tradeOrderTccAction;

@GlobalTransactional(name = "group-buy-order", rollbackFor = Exception.class)
public OrderResult createGroupBuyOrder(CreateOrderRequest request) {
// 1. 尝试锁定拼团名额
boolean lockSuccess = groupBuyTccAction.tryLock(
request.getGroupBuyOrderId(),
request.getUserId()
);
if (!lockSuccess) {
throw new BizException("锁定拼团名额失败");
}

// 2. 尝试锁定优惠券
if (request.getCouponId() != null) {
boolean couponSuccess = couponTccAction.tryLock(
request.getCouponId(),
request.getUserId()
);
if (!couponSuccess) {
throw new BizException("锁定优惠券失败");
}
}

// 3. 尝试创建交易订单
boolean orderSuccess = tradeOrderTccAction.tryCreate(
request.getTradeOrderNo(),
request
);
if (!orderSuccess) {
throw new BizException("创建订单失败");
}

// 所有 Try 成功,Seata 会自动执行各服务的 Commit
return OrderResult.success(request.getTradeOrderNo());
}
}

四、总结

拼团交易系统是一个典型的高并发营销系统,核心挑战在于:

  1. 并发控制:通过 Redis + Lua + 分布式锁保证不超卖
  2. 状态管理:清晰的状态流转和幂等性保证
  3. 一致性:TCC 分布式事务保证跨服务数据一致
  4. 性能优化:多级缓存 + 异步处理 + 削峰填谷

系统设计要点:

  • 策略模式支持多种拼团玩法
  • 锁单机制平衡用户体验和库存准确
  • 延迟任务处理超时释放
  • 微服务架构保证可扩展性

参考资料:

  • 《大型网站技术架构》
  • 《Redis 设计与实现》
  • 《分布式系统:概念与设计》
  • Seata 官方文档
  • RocketMQ 官方文档

如果您喜欢此博客或发现它对您有用,欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !