拼团系统设计与实现

拼团系统设计与实现

前言

拼团是电商平台常见的营销玩法,通过社交裂变的方式实现用户增长和销量提升。美团、拼多多等平台的拼团功能每天处理数百万订单,涉及复杂的业务规则和高并发场景。本文将从系统架构、核心业务、代码实现等方面,详细讲解如何设计一个完整的拼团系统。

一、拼团业务分析

1.1 拼团模式

diff 复制代码
+------------------+------------------+------------------+------------------+
|    普通拼团      |    阶梯拼团       |    老带新拼团     |    抽奖拼团       |
+------------------+------------------+------------------+------------------+
| 固定人数成团     | 人数越多价格越低   | 新用户参团优惠    | 成团后抽奖        |
| 2人团/3人团/5人团 | 2人95折/5人8折   | 老用户开团        | 中奖获得商品      |
+------------------+------------------+------------------+------------------+

1.2 拼团核心流程

lua 复制代码
+----------+     +----------+     +----------+     +----------+     +----------+
|  开团    | --> |  支付    | --> |  等待    | --> |  成团    | --> |  发货    |
+----------+     +----------+     +----------+     +----------+     +----------+
     |               |               |               |               |
     v               v               v               v               v
  创建团单       团长支付       分享邀请       满足人数       订单履约
  生成分享链接   锁定库存       等待参团       触发成团       通知发货
                                    |
                                    v
                              +----------+
                              |  超时    |
                              |  退款    |
                              +----------+

1.3 参团流程

lua 复制代码
                         +------------------+
                         |    分享链接       |
                         +--------+---------+
                                  |
                         +--------v---------+
                         |    查看团详情     |
                         +--------+---------+
                                  |
                    +-------------+-------------+
                    |                           |
           +--------v--------+         +--------v--------+
           |   新开一个团     |         |   参加现有团     |
           +--------+--------+         +--------+--------+
                    |                           |
                    v                           v
           +------------------+        +------------------+
           |    选择商品规格   |        |    确认参团      |
           +--------+---------+        +--------+---------+
                    |                           |
                    +-------------+-------------+
                                  |
                         +--------v---------+
                         |    提交订单       |
                         +--------+---------+
                                  |
                         +--------v---------+
                         |    支付          |
                         +------------------+

1.4 核心业务指标

指标 说明 目标值
成团率 开团后成功成团的比例 >60%
参团转化率 点击分享链接后参团的比例 >30%
人均拉新 每个团长平均带来的新用户 >1.5
开团QPS 高峰期开团请求量 1万+

二、系统架构设计

2.1 整体架构

sql 复制代码
                              +------------------+
                              |     用户端       |
                              |  APP/小程序/H5  |
                              +---------+--------+
                                        |
                              +---------v--------+
                              |   API Gateway    |
                              |  认证/限流/路由   |
                              +---------+--------+
                                        |
         +------------------------------+------------------------------+
         |                              |                              |
+--------v--------+           +---------v--------+           +---------v--------+
|   拼团服务       |           |    订单服务      |           |    商品服务      |
| GroupBuy-Service|           |  Order-Service   |           | Product-Service  |
+--------+--------+           +---------+--------+           +---------+--------+
         |                              |                              |
         +------------------------------+------------------------------+
                                        |
         +------------------------------+------------------------------+
         |                              |                              |
+--------v--------+           +---------v--------+           +---------v--------+
|   Redis集群     |           |    RocketMQ     |           |    MySQL集群     |
|  缓存/分布式锁   |           |    异步处理      |           |    数据持久化    |
+-----------------+           +------------------+           +------------------+
                                        |
                              +---------v--------+
                              |   定时任务服务    |
                              |   XXL-JOB        |
                              +------------------+

2.2 拼团服务核心模块

lua 复制代码
+------------------------------------------------------------------+
|                        拼团服务                                   |
+------------------------------------------------------------------+
|  +-------------+  +-------------+  +-------------+  +-----------+ |
|  | 活动管理    |  | 团单管理    |  | 参团服务    |  | 成团处理  | |
|  | Activity    |  | GroupOrder  |  | JoinGroup   |  | Success   | |
|  +-------------+  +-------------+  +-------------+  +-----------+ |
|                                                                   |
|  +-------------+  +-------------+  +-------------+  +-----------+ |
|  | 超时处理    |  | 退款服务    |  | 分享服务    |  | 数据统计  | |
|  | Timeout     |  | Refund      |  | Share       |  | Statistics| |
|  +-------------+  +-------------+  +-------------+  +-----------+ |
+------------------------------------------------------------------+

三、数据库设计

3.1 核心表结构

sql 复制代码
-- 拼团活动表
CREATE TABLE `group_activity` (
    `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '活动ID',
    `name` VARCHAR(64) NOT NULL COMMENT '活动名称',
    `product_id` BIGINT(20) NOT NULL COMMENT '商品ID',
    `product_name` VARCHAR(128) NOT NULL COMMENT '商品名称',
    `product_img` VARCHAR(256) DEFAULT NULL COMMENT '商品图片',
    `original_price` DECIMAL(10,2) NOT NULL COMMENT '原价',
    `group_price` DECIMAL(10,2) NOT NULL COMMENT '拼团价',
    `group_size` INT(11) NOT NULL DEFAULT 2 COMMENT '成团人数',
    `limit_per_user` INT(11) NOT NULL DEFAULT 1 COMMENT '每人限购数量',
    `total_stock` INT(11) NOT NULL COMMENT '活动总库存',
    `remain_stock` INT(11) NOT NULL COMMENT '剩余库存',
    `virtual_count` INT(11) DEFAULT 0 COMMENT '虚拟成团数(展示用)',
    `expire_minutes` INT(11) NOT NULL DEFAULT 1440 COMMENT '成团有效期(分钟)',
    `start_time` DATETIME NOT NULL COMMENT '活动开始时间',
    `end_time` DATETIME NOT NULL COMMENT '活动结束时间',
    `status` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '状态:1-待开始,2-进行中,3-已结束,4-已停止',
    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    KEY `idx_product_id` (`product_id`),
    KEY `idx_status_time` (`status`, `start_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='拼团活动表';

-- 团单表(每个拼团实例)
CREATE TABLE `group_order` (
    `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '团单ID',
    `group_no` VARCHAR(32) NOT NULL COMMENT '团单号',
    `activity_id` BIGINT(20) NOT NULL COMMENT '活动ID',
    `leader_user_id` BIGINT(20) NOT NULL COMMENT '团长用户ID',
    `leader_order_no` VARCHAR(32) NOT NULL COMMENT '团长订单号',
    `group_size` INT(11) NOT NULL COMMENT '成团人数',
    `current_size` INT(11) NOT NULL DEFAULT 1 COMMENT '当前人数',
    `status` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '状态:1-拼团中,2-已成团,3-已失败',
    `expire_time` DATETIME NOT NULL COMMENT '过期时间',
    `success_time` DATETIME DEFAULT NULL COMMENT '成团时间',
    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_group_no` (`group_no`),
    KEY `idx_activity_id` (`activity_id`),
    KEY `idx_leader_user_id` (`leader_user_id`),
    KEY `idx_status_expire` (`status`, `expire_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='团单表';

-- 参团记录表
CREATE TABLE `group_member` (
    `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
    `group_no` VARCHAR(32) NOT NULL COMMENT '团单号',
    `activity_id` BIGINT(20) NOT NULL COMMENT '活动ID',
    `user_id` BIGINT(20) NOT NULL COMMENT '用户ID',
    `order_no` VARCHAR(32) NOT NULL COMMENT '订单号',
    `is_leader` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否团长:0-否,1-是',
    `status` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '状态:1-待支付,2-已支付,3-已退款',
    `pay_time` DATETIME DEFAULT NULL COMMENT '支付时间',
    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_group_user` (`group_no`, `user_id`),
    KEY `idx_user_id` (`user_id`),
    KEY `idx_order_no` (`order_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='参团记录表';

-- 拼团订单表(关联业务订单)
CREATE TABLE `group_buy_order` (
    `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
    `order_no` VARCHAR(32) NOT NULL COMMENT '订单号',
    `group_no` VARCHAR(32) NOT NULL COMMENT '团单号',
    `activity_id` BIGINT(20) NOT NULL COMMENT '活动ID',
    `user_id` BIGINT(20) NOT NULL COMMENT '用户ID',
    `product_id` BIGINT(20) NOT NULL COMMENT '商品ID',
    `product_name` VARCHAR(128) NOT NULL COMMENT '商品名称',
    `buy_count` INT(11) NOT NULL DEFAULT 1 COMMENT '购买数量',
    `original_price` DECIMAL(10,2) NOT NULL COMMENT '原价',
    `group_price` DECIMAL(10,2) NOT NULL COMMENT '拼团价',
    `pay_amount` DECIMAL(10,2) NOT NULL COMMENT '实付金额',
    `order_status` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '订单状态:1-待支付,2-已支付待成团,3-已成团,4-已取消,5-已退款',
    `pay_time` DATETIME DEFAULT NULL COMMENT '支付时间',
    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_order_no` (`order_no`),
    KEY `idx_group_no` (`group_no`),
    KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='拼团订单表';

3.2 ER关系图

lua 复制代码
+------------------+       +------------------+       +------------------+
| group_activity   |       |   group_order    |       |  group_member    |
+------------------+       +------------------+       +------------------+
| id (PK)          |<------| activity_id (FK) |       | id (PK)          |
| name             |       | id (PK)          |<------| group_no (FK)    |
| product_id       |       | group_no (UK)    |       | user_id          |
| group_price      |       | leader_user_id   |       | order_no         |
| group_size       |       | group_size       |       | is_leader        |
| remain_stock     |       | current_size     |       | status           |
| expire_minutes   |       | status           |       +------------------+
| status           |       | expire_time      |
+------------------+       +------------------+
                                   |
                                   v
                          +------------------+
                          | group_buy_order  |
                          +------------------+
                          | order_no (PK)    |
                          | group_no (FK)    |
                          | activity_id      |
                          | user_id          |
                          | pay_amount       |
                          | order_status     |
                          +------------------+

四、核心实体类

4.1 实体定义

java 复制代码
package com.groupbuy.service.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * 拼团活动实体
 */
@Data
@TableName("group_activity")
public class GroupActivity {

    @TableId(type = IdType.AUTO)
    private Long id;

    private String name;

    private Long productId;

    private String productName;

    private String productImg;

    private BigDecimal originalPrice;

    private BigDecimal groupPrice;

    /**
     * 成团人数
     */
    private Integer groupSize;

    /**
     * 每人限购
     */
    private Integer limitPerUser;

    private Integer totalStock;

    private Integer remainStock;

    /**
     * 虚拟成团数
     */
    private Integer virtualCount;

    /**
     * 成团有效期(分钟)
     */
    private Integer expireMinutes;

    private LocalDateTime startTime;

    private LocalDateTime endTime;

    /**
     * 状态:1-待开始,2-进行中,3-已结束,4-已停止
     */
    private Integer status;

    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
}
java 复制代码
package com.groupbuy.service.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;

/**
 * 团单实体
 */
@Data
@TableName("group_order")
public class GroupOrder {

    @TableId(type = IdType.AUTO)
    private Long id;

    /**
     * 团单号
     */
    private String groupNo;

    private Long activityId;

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

    /**
     * 团长订单号
     */
    private String leaderOrderNo;

    /**
     * 成团人数
     */
    private Integer groupSize;

    /**
     * 当前人数
     */
    private Integer currentSize;

    /**
     * 状态:1-拼团中,2-已成团,3-已失败
     */
    private Integer status;

    /**
     * 过期时间
     */
    private LocalDateTime expireTime;

    /**
     * 成团时间
     */
    private LocalDateTime successTime;

    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
}
java 复制代码
package com.groupbuy.service.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * 拼团订单实体
 */
@Data
@TableName("group_buy_order")
public class GroupBuyOrder {

    @TableId(type = IdType.AUTO)
    private Long id;

    private String orderNo;

    private String groupNo;

    private Long activityId;

    private Long userId;

    private Long productId;

    private String productName;

    private Integer buyCount;

    private BigDecimal originalPrice;

    private BigDecimal groupPrice;

    private BigDecimal payAmount;

    /**
     * 订单状态:1-待支付,2-已支付待成团,3-已成团,4-已取消,5-已退款
     */
    private Integer orderStatus;

    private LocalDateTime payTime;

    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
}

4.2 枚举定义

java 复制代码
package com.groupbuy.service.enums;

import lombok.Getter;

/**
 * 团单状态枚举
 */
@Getter
public enum GroupOrderStatusEnum {

    GROUPING(1, "拼团中"),
    SUCCESS(2, "已成团"),
    FAILED(3, "已失败");

    private final Integer code;
    private final String desc;

    GroupOrderStatusEnum(Integer code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public static GroupOrderStatusEnum of(Integer code) {
        for (GroupOrderStatusEnum value : values()) {
            if (value.getCode().equals(code)) {
                return value;
            }
        }
        return null;
    }
}
java 复制代码
package com.groupbuy.service.enums;

import lombok.Getter;

/**
 * 拼团订单状态枚举
 */
@Getter
public enum GroupBuyOrderStatusEnum {

    UNPAID(1, "待支付"),
    PAID_GROUPING(2, "已支付待成团"),
    GROUP_SUCCESS(3, "已成团"),
    CANCELLED(4, "已取消"),
    REFUNDED(5, "已退款");

    private final Integer code;
    private final String desc;

    GroupBuyOrderStatusEnum(Integer code, String desc) {
        this.code = code;
        this.desc = desc;
    }
}

4.3 DTO定义

java 复制代码
package com.groupbuy.service.dto;

import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;

/**
 * 团单详情DTO
 */
@Data
public class GroupOrderDetailDTO {

    /**
     * 团单号
     */
    private String groupNo;

    /**
     * 活动信息
     */
    private Long activityId;
    private String activityName;
    private String productName;
    private String productImg;
    private BigDecimal originalPrice;
    private BigDecimal groupPrice;

    /**
     * 团单信息
     */
    private Integer groupSize;
    private Integer currentSize;
    private Integer remainSize;
    private Integer status;
    private String statusDesc;

    /**
     * 团长信息
     */
    private Long leaderUserId;
    private String leaderNickname;
    private String leaderAvatar;

    /**
     * 参团成员
     */
    private List<GroupMemberDTO> members;

    /**
     * 时间信息
     */
    private LocalDateTime expireTime;
    private Long remainSeconds;
    private LocalDateTime createTime;
}
java 复制代码
package com.groupbuy.service.dto;

import lombok.Data;
import java.time.LocalDateTime;

/**
 * 参团成员DTO
 */
@Data
public class GroupMemberDTO {

    private Long userId;

    private String nickname;

    private String avatar;

    private Boolean isLeader;

    private LocalDateTime joinTime;
}
java 复制代码
package com.groupbuy.service.dto;

import lombok.Data;
import javax.validation.constraints.NotNull;

/**
 * 开团请求
 */
@Data
public class CreateGroupRequest {

    @NotNull(message = "活动ID不能为空")
    private Long activityId;

    /**
     * 购买数量
     */
    private Integer buyCount = 1;

    /**
     * 收货地址ID
     */
    @NotNull(message = "收货地址不能为空")
    private Long addressId;
}
java 复制代码
package com.groupbuy.service.dto;

import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

/**
 * 参团请求
 */
@Data
public class JoinGroupRequest {

    @NotBlank(message = "团单号不能为空")
    private String groupNo;

    /**
     * 购买数量
     */
    private Integer buyCount = 1;

    /**
     * 收货地址ID
     */
    @NotNull(message = "收货地址不能为空")
    private Long addressId;
}

五、开团服务实现

5.1 开团流程

rust 复制代码
+----------+     +----------+     +----------+     +----------+     +----------+
| 参数校验  | --> | 活动校验  | --> | 库存校验  | --> | 创建团单  | --> | 创建订单  |
+----------+     +----------+     +----------+     +----------+     +----------+
     |               |               |               |               |
     v               v               v               v               v
  请求参数        时间/状态       库存是否充足    生成团单号       生成订单号
  用户校验        限购校验        Redis预扣减    设置过期时间     待支付状态

5.2 开团服务实现

java 复制代码
package com.groupbuy.service.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.groupbuy.common.exception.GroupBuyException;
import com.groupbuy.common.result.ResultCode;
import com.groupbuy.service.dto.CreateGroupRequest;
import com.groupbuy.service.dto.CreateGroupResponse;
import com.groupbuy.service.entity.*;
import com.groupbuy.service.enums.GroupBuyOrderStatusEnum;
import com.groupbuy.service.enums.GroupOrderStatusEnum;
import com.groupbuy.service.mapper.*;
import com.groupbuy.service.utils.OrderNoGenerator;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Collections;

/**
 * 开团服务
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class CreateGroupService {

    private final GroupActivityMapper activityMapper;
    private final GroupOrderMapper groupOrderMapper;
    private final GroupMemberMapper memberMapper;
    private final GroupBuyOrderMapper buyOrderMapper;
    private final RedisTemplate<String, Object> redisTemplate;
    private final RedisScript<Long> stockDeductScript;

    private static final String STOCK_KEY_PREFIX = "groupbuy:stock:";
    private static final String USER_BUY_KEY_PREFIX = "groupbuy:user:buy:";

    /**
     * 开团
     */
    @Transactional(rollbackFor = Exception.class)
    public CreateGroupResponse createGroup(Long userId, CreateGroupRequest request) {
        Long activityId = request.getActivityId();
        Integer buyCount = request.getBuyCount();

        // 1. 查询活动信息
        GroupActivity activity = activityMapper.selectById(activityId);
        if (activity == null) {
            throw new GroupBuyException(ResultCode.ACTIVITY_NOT_EXIST);
        }

        // 2. 校验活动状态
        validateActivity(activity);

        // 3. 校验限购
        validateUserLimit(userId, activityId, activity.getLimitPerUser(), buyCount);

        // 4. 预扣减库存
        boolean stockResult = preDeductStock(activityId, buyCount);
        if (!stockResult) {
            throw new GroupBuyException(ResultCode.STOCK_NOT_ENOUGH);
        }

        try {
            // 5. 生成团单号和订单号
            String groupNo = OrderNoGenerator.generateGroupNo();
            String orderNo = OrderNoGenerator.generateOrderNo();

            // 6. 创建团单
            GroupOrder groupOrder = createGroupOrder(groupNo, activity, userId, orderNo);
            groupOrderMapper.insert(groupOrder);

            // 7. 创建参团记录(团长)
            GroupMember member = createGroupMember(groupNo, activityId, userId, orderNo, true);
            memberMapper.insert(member);

            // 8. 创建拼团订单
            GroupBuyOrder buyOrder = createBuyOrder(orderNo, groupNo, activity, userId, buyCount);
            buyOrderMapper.insert(buyOrder);

            // 9. 记录用户购买数量
            recordUserBuy(userId, activityId, buyCount);

            log.info("开团成功: userId={}, activityId={}, groupNo={}, orderNo={}",
                userId, activityId, groupNo, orderNo);

            // 10. 返回结果
            CreateGroupResponse response = new CreateGroupResponse();
            response.setGroupNo(groupNo);
            response.setOrderNo(orderNo);
            response.setPayAmount(buyOrder.getPayAmount());
            response.setExpireTime(groupOrder.getExpireTime());

            return response;

        } catch (Exception e) {
            // 回滚库存
            rollbackStock(activityId, buyCount);
            throw e;
        }
    }

    /**
     * 校验活动状态
     */
    private void validateActivity(GroupActivity activity) {
        LocalDateTime now = LocalDateTime.now();

        if (activity.getStatus() == 4) {
            throw new GroupBuyException(ResultCode.ACTIVITY_STOPPED);
        }

        if (now.isBefore(activity.getStartTime())) {
            throw new GroupBuyException(ResultCode.ACTIVITY_NOT_START);
        }

        if (now.isAfter(activity.getEndTime())) {
            throw new GroupBuyException(ResultCode.ACTIVITY_ENDED);
        }
    }

    /**
     * 校验用户限购
     */
    private void validateUserLimit(Long userId, Long activityId, Integer limit, Integer buyCount) {
        String key = USER_BUY_KEY_PREFIX + activityId + ":" + userId;
        Object bought = redisTemplate.opsForValue().get(key);
        int boughtCount = bought != null ? Integer.parseInt(bought.toString()) : 0;

        if (boughtCount + buyCount > limit) {
            throw new GroupBuyException(ResultCode.EXCEED_LIMIT);
        }
    }

    /**
     * 预扣减库存
     */
    private boolean preDeductStock(Long activityId, Integer count) {
        String stockKey = STOCK_KEY_PREFIX + activityId;

        Long result = redisTemplate.execute(
            stockDeductScript,
            Collections.singletonList(stockKey),
            count
        );

        return result != null && result >= 0;
    }

    /**
     * 回滚库存
     */
    private void rollbackStock(Long activityId, Integer count) {
        String stockKey = STOCK_KEY_PREFIX + activityId;
        redisTemplate.opsForValue().increment(stockKey, count);
    }

    /**
     * 创建团单
     */
    private GroupOrder createGroupOrder(String groupNo, GroupActivity activity,
                                         Long userId, String orderNo) {
        GroupOrder groupOrder = new GroupOrder();
        groupOrder.setGroupNo(groupNo);
        groupOrder.setActivityId(activity.getId());
        groupOrder.setLeaderUserId(userId);
        groupOrder.setLeaderOrderNo(orderNo);
        groupOrder.setGroupSize(activity.getGroupSize());
        groupOrder.setCurrentSize(1);
        groupOrder.setStatus(GroupOrderStatusEnum.GROUPING.getCode());
        groupOrder.setExpireTime(LocalDateTime.now().plusMinutes(activity.getExpireMinutes()));

        return groupOrder;
    }

    /**
     * 创建参团记录
     */
    private GroupMember createGroupMember(String groupNo, Long activityId,
                                           Long userId, String orderNo, boolean isLeader) {
        GroupMember member = new GroupMember();
        member.setGroupNo(groupNo);
        member.setActivityId(activityId);
        member.setUserId(userId);
        member.setOrderNo(orderNo);
        member.setIsLeader(isLeader ? 1 : 0);
        member.setStatus(1);  // 待支付

        return member;
    }

    /**
     * 创建拼团订单
     */
    private GroupBuyOrder createBuyOrder(String orderNo, String groupNo,
                                          GroupActivity activity, Long userId, Integer buyCount) {
        GroupBuyOrder order = new GroupBuyOrder();
        order.setOrderNo(orderNo);
        order.setGroupNo(groupNo);
        order.setActivityId(activity.getId());
        order.setUserId(userId);
        order.setProductId(activity.getProductId());
        order.setProductName(activity.getProductName());
        order.setBuyCount(buyCount);
        order.setOriginalPrice(activity.getOriginalPrice());
        order.setGroupPrice(activity.getGroupPrice());
        order.setPayAmount(activity.getGroupPrice().multiply(BigDecimal.valueOf(buyCount)));
        order.setOrderStatus(GroupBuyOrderStatusEnum.UNPAID.getCode());

        return order;
    }

    /**
     * 记录用户购买数量
     */
    private void recordUserBuy(Long userId, Long activityId, Integer count) {
        String key = USER_BUY_KEY_PREFIX + activityId + ":" + userId;
        redisTemplate.opsForValue().increment(key, count);
    }
}

5.3 库存扣减Lua脚本

lua 复制代码
-- 库存扣减脚本
-- KEYS[1]: 库存key
-- ARGV[1]: 扣减数量

local stockKey = KEYS[1]
local deductCount = tonumber(ARGV[1])

-- 获取当前库存
local stock = tonumber(redis.call('GET', stockKey) or 0)

if stock < deductCount then
    return -1  -- 库存不足
end

-- 扣减库存
local newStock = redis.call('DECRBY', stockKey, deductCount)

return newStock

六、参团服务实现

6.1 参团流程

rust 复制代码
+----------+     +----------+     +----------+     +----------+     +----------+
| 团单校验  | --> | 活动校验  | --> | 重复参团  | --> | 库存校验  | --> | 加入团单  |
+----------+     +----------+     +----------+     +----------+     +----------+
     |               |               |               |               |
     v               v               v               v               v
  团单状态        活动有效性       用户是否已参团   库存是否充足    更新当前人数
  是否已满        时间校验        同团不可重复     Redis扣减      创建订单

6.2 参团服务实现

java 复制代码
package com.groupbuy.service.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.groupbuy.common.exception.GroupBuyException;
import com.groupbuy.common.result.ResultCode;
import com.groupbuy.service.dto.JoinGroupRequest;
import com.groupbuy.service.dto.JoinGroupResponse;
import com.groupbuy.service.entity.*;
import com.groupbuy.service.enums.GroupBuyOrderStatusEnum;
import com.groupbuy.service.enums.GroupOrderStatusEnum;
import com.groupbuy.service.mapper.*;
import com.groupbuy.service.utils.OrderNoGenerator;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;

/**
 * 参团服务
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class JoinGroupService {

    private final GroupActivityMapper activityMapper;
    private final GroupOrderMapper groupOrderMapper;
    private final GroupMemberMapper memberMapper;
    private final GroupBuyOrderMapper buyOrderMapper;
    private final RedisTemplate<String, Object> redisTemplate;
    private final RedissonClient redissonClient;
    private final GroupSuccessService groupSuccessService;

    private static final String STOCK_KEY_PREFIX = "groupbuy:stock:";
    private static final String GROUP_LOCK_PREFIX = "groupbuy:lock:group:";
    private static final String USER_BUY_KEY_PREFIX = "groupbuy:user:buy:";

    /**
     * 参团
     */
    @Transactional(rollbackFor = Exception.class)
    public JoinGroupResponse joinGroup(Long userId, JoinGroupRequest request) {
        String groupNo = request.getGroupNo();
        Integer buyCount = request.getBuyCount();

        // 1. 加分布式锁(防止并发参团超员)
        String lockKey = GROUP_LOCK_PREFIX + groupNo;
        RLock lock = redissonClient.getLock(lockKey);

        try {
            boolean acquired = lock.tryLock(3, 10, TimeUnit.SECONDS);
            if (!acquired) {
                throw new GroupBuyException(ResultCode.SYSTEM_BUSY);
            }

            // 2. 查询团单
            GroupOrder groupOrder = groupOrderMapper.selectByGroupNo(groupNo);
            if (groupOrder == null) {
                throw new GroupBuyException(ResultCode.GROUP_NOT_EXIST);
            }

            // 3. 校验团单状态
            validateGroupOrder(groupOrder);

            // 4. 查询活动
            GroupActivity activity = activityMapper.selectById(groupOrder.getActivityId());
            if (activity == null) {
                throw new GroupBuyException(ResultCode.ACTIVITY_NOT_EXIST);
            }

            // 5. 校验活动状态
            validateActivity(activity);

            // 6. 校验是否重复参团
            validateNotJoined(groupNo, userId);

            // 7. 校验限购
            validateUserLimit(userId, activity.getId(), activity.getLimitPerUser(), buyCount);

            // 8. 预扣减库存
            boolean stockResult = preDeductStock(activity.getId(), buyCount);
            if (!stockResult) {
                throw new GroupBuyException(ResultCode.STOCK_NOT_ENOUGH);
            }

            try {
                // 9. 生成订单号
                String orderNo = OrderNoGenerator.generateOrderNo();

                // 10. 创建参团记录
                GroupMember member = createGroupMember(groupNo, activity.getId(), userId, orderNo);
                memberMapper.insert(member);

                // 11. 创建拼团订单
                GroupBuyOrder buyOrder = createBuyOrder(orderNo, groupNo, activity, userId, buyCount);
                buyOrderMapper.insert(buyOrder);

                // 12. 更新团单当前人数
                groupOrderMapper.incrementCurrentSize(groupNo);

                // 13. 记录用户购买数量
                recordUserBuy(userId, activity.getId(), buyCount);

                log.info("参团成功: userId={}, groupNo={}, orderNo={}",
                    userId, groupNo, orderNo);

                // 14. 返回结果
                JoinGroupResponse response = new JoinGroupResponse();
                response.setGroupNo(groupNo);
                response.setOrderNo(orderNo);
                response.setPayAmount(buyOrder.getPayAmount());
                response.setCurrentSize(groupOrder.getCurrentSize() + 1);
                response.setGroupSize(groupOrder.getGroupSize());

                return response;

            } catch (Exception e) {
                // 回滚库存
                rollbackStock(activity.getId(), buyCount);
                throw e;
            }

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new GroupBuyException(ResultCode.SYSTEM_ERROR);
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    /**
     * 校验团单状态
     */
    private void validateGroupOrder(GroupOrder groupOrder) {
        // 检查状态
        if (!GroupOrderStatusEnum.GROUPING.getCode().equals(groupOrder.getStatus())) {
            throw new GroupBuyException(ResultCode.GROUP_NOT_GROUPING);
        }

        // 检查是否已满
        if (groupOrder.getCurrentSize() >= groupOrder.getGroupSize()) {
            throw new GroupBuyException(ResultCode.GROUP_FULL);
        }

        // 检查是否过期
        if (LocalDateTime.now().isAfter(groupOrder.getExpireTime())) {
            throw new GroupBuyException(ResultCode.GROUP_EXPIRED);
        }
    }

    /**
     * 校验活动状态
     */
    private void validateActivity(GroupActivity activity) {
        LocalDateTime now = LocalDateTime.now();

        if (activity.getStatus() == 4) {
            throw new GroupBuyException(ResultCode.ACTIVITY_STOPPED);
        }

        if (now.isAfter(activity.getEndTime())) {
            throw new GroupBuyException(ResultCode.ACTIVITY_ENDED);
        }
    }

    /**
     * 校验是否已参团
     */
    private void validateNotJoined(String groupNo, Long userId) {
        GroupMember existMember = memberMapper.selectOne(
            new LambdaQueryWrapper<GroupMember>()
                .eq(GroupMember::getGroupNo, groupNo)
                .eq(GroupMember::getUserId, userId)
        );

        if (existMember != null) {
            throw new GroupBuyException(ResultCode.ALREADY_JOINED);
        }
    }

    /**
     * 校验用户限购
     */
    private void validateUserLimit(Long userId, Long activityId, Integer limit, Integer buyCount) {
        String key = USER_BUY_KEY_PREFIX + activityId + ":" + userId;
        Object bought = redisTemplate.opsForValue().get(key);
        int boughtCount = bought != null ? Integer.parseInt(bought.toString()) : 0;

        if (boughtCount + buyCount > limit) {
            throw new GroupBuyException(ResultCode.EXCEED_LIMIT);
        }
    }

    /**
     * 预扣减库存
     */
    private boolean preDeductStock(Long activityId, Integer count) {
        String stockKey = STOCK_KEY_PREFIX + activityId;
        Long newStock = redisTemplate.opsForValue().decrement(stockKey, count);
        return newStock != null && newStock >= 0;
    }

    /**
     * 回滚库存
     */
    private void rollbackStock(Long activityId, Integer count) {
        String stockKey = STOCK_KEY_PREFIX + activityId;
        redisTemplate.opsForValue().increment(stockKey, count);
    }

    /**
     * 创建参团记录
     */
    private GroupMember createGroupMember(String groupNo, Long activityId,
                                           Long userId, String orderNo) {
        GroupMember member = new GroupMember();
        member.setGroupNo(groupNo);
        member.setActivityId(activityId);
        member.setUserId(userId);
        member.setOrderNo(orderNo);
        member.setIsLeader(0);
        member.setStatus(1);  // 待支付

        return member;
    }

    /**
     * 创建拼团订单
     */
    private GroupBuyOrder createBuyOrder(String orderNo, String groupNo,
                                          GroupActivity activity, Long userId, Integer buyCount) {
        GroupBuyOrder order = new GroupBuyOrder();
        order.setOrderNo(orderNo);
        order.setGroupNo(groupNo);
        order.setActivityId(activity.getId());
        order.setUserId(userId);
        order.setProductId(activity.getProductId());
        order.setProductName(activity.getProductName());
        order.setBuyCount(buyCount);
        order.setOriginalPrice(activity.getOriginalPrice());
        order.setGroupPrice(activity.getGroupPrice());
        order.setPayAmount(activity.getGroupPrice().multiply(BigDecimal.valueOf(buyCount)));
        order.setOrderStatus(GroupBuyOrderStatusEnum.UNPAID.getCode());

        return order;
    }

    /**
     * 记录用户购买数量
     */
    private void recordUserBuy(Long userId, Long activityId, Integer count) {
        String key = USER_BUY_KEY_PREFIX + activityId + ":" + userId;
        redisTemplate.opsForValue().increment(key, count);
    }
}

七、支付回调与成团处理

7.1 支付回调流程

rust 复制代码
+----------+     +----------+     +----------+     +----------+     +----------+
| 支付回调  | --> | 订单校验  | --> | 更新状态  | --> | 检查成团  | --> | 触发成团  |
+----------+     +----------+     +----------+     +----------+     +----------+
     |               |               |               |               |
     v               v               v               v               v
  验签验证        订单存在性       订单->已支付    人数是否达标    更新团单状态
  金额校验        状态校验        参团记录更新    自动成团判断    通知所有成员

7.2 支付回调处理

java 复制代码
package com.groupbuy.service.service;

import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.groupbuy.common.exception.GroupBuyException;
import com.groupbuy.common.result.ResultCode;
import com.groupbuy.service.dto.PayCallbackRequest;
import com.groupbuy.service.entity.*;
import com.groupbuy.service.enums.GroupBuyOrderStatusEnum;
import com.groupbuy.service.enums.GroupOrderStatusEnum;
import com.groupbuy.service.mapper.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;

/**
 * 支付回调服务
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class PayCallbackService {

    private final GroupBuyOrderMapper buyOrderMapper;
    private final GroupOrderMapper groupOrderMapper;
    private final GroupMemberMapper memberMapper;
    private final RedissonClient redissonClient;
    private final GroupSuccessService groupSuccessService;

    private static final String GROUP_LOCK_PREFIX = "groupbuy:lock:group:";

    /**
     * 处理支付回调
     */
    @Transactional(rollbackFor = Exception.class)
    public void handlePayCallback(PayCallbackRequest request) {
        String orderNo = request.getOrderNo();

        // 1. 查询订单
        GroupBuyOrder buyOrder = buyOrderMapper.selectByOrderNo(orderNo);
        if (buyOrder == null) {
            log.warn("订单不存在: orderNo={}", orderNo);
            return;
        }

        // 2. 校验订单状态(幂等)
        if (!GroupBuyOrderStatusEnum.UNPAID.getCode().equals(buyOrder.getOrderStatus())) {
            log.info("订单已处理,跳过: orderNo={}, status={}", orderNo, buyOrder.getOrderStatus());
            return;
        }

        // 3. 校验支付金额
        if (buyOrder.getPayAmount().compareTo(request.getPayAmount()) != 0) {
            log.error("支付金额不匹配: orderNo={}, expected={}, actual={}",
                orderNo, buyOrder.getPayAmount(), request.getPayAmount());
            throw new GroupBuyException(ResultCode.PAY_AMOUNT_ERROR);
        }

        String groupNo = buyOrder.getGroupNo();

        // 4. 加锁处理成团逻辑
        String lockKey = GROUP_LOCK_PREFIX + groupNo;
        RLock lock = redissonClient.getLock(lockKey);

        try {
            lock.lock(30, TimeUnit.SECONDS);

            // 5. 更新订单状态
            LocalDateTime now = LocalDateTime.now();
            buyOrderMapper.update(null,
                new LambdaUpdateWrapper<GroupBuyOrder>()
                    .eq(GroupBuyOrder::getOrderNo, orderNo)
                    .eq(GroupBuyOrder::getOrderStatus, GroupBuyOrderStatusEnum.UNPAID.getCode())
                    .set(GroupBuyOrder::getOrderStatus, GroupBuyOrderStatusEnum.PAID_GROUPING.getCode())
                    .set(GroupBuyOrder::getPayTime, now)
            );

            // 6. 更新参团记录状态
            memberMapper.update(null,
                new LambdaUpdateWrapper<GroupMember>()
                    .eq(GroupMember::getOrderNo, orderNo)
                    .set(GroupMember::getStatus, 2)  // 已支付
                    .set(GroupMember::getPayTime, now)
            );

            // 7. 查询团单
            GroupOrder groupOrder = groupOrderMapper.selectByGroupNo(groupNo);
            if (groupOrder == null) {
                log.error("团单不存在: groupNo={}", groupNo);
                return;
            }

            // 8. 检查是否成团
            int paidCount = memberMapper.countPaidMembers(groupNo);

            log.info("支付回调处理完成: orderNo={}, groupNo={}, paidCount={}, groupSize={}",
                orderNo, groupNo, paidCount, groupOrder.getGroupSize());

            if (paidCount >= groupOrder.getGroupSize()) {
                // 触发成团
                groupSuccessService.handleGroupSuccess(groupNo);
            }

        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

7.3 成团处理服务

java 复制代码
package com.groupbuy.service.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.groupbuy.service.entity.*;
import com.groupbuy.service.enums.GroupBuyOrderStatusEnum;
import com.groupbuy.service.enums.GroupOrderStatusEnum;
import com.groupbuy.service.mapper.*;
import com.groupbuy.service.mq.GroupSuccessProducer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;

/**
 * 成团处理服务
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class GroupSuccessService {

    private final GroupOrderMapper groupOrderMapper;
    private final GroupBuyOrderMapper buyOrderMapper;
    private final GroupMemberMapper memberMapper;
    private final GroupActivityMapper activityMapper;
    private final GroupSuccessProducer successProducer;

    /**
     * 处理成团
     */
    @Transactional(rollbackFor = Exception.class)
    public void handleGroupSuccess(String groupNo) {
        log.info("开始处理成团: groupNo={}", groupNo);

        // 1. 查询团单
        GroupOrder groupOrder = groupOrderMapper.selectByGroupNo(groupNo);
        if (groupOrder == null) {
            log.warn("团单不存在: groupNo={}", groupNo);
            return;
        }

        // 2. 校验状态(幂等)
        if (!GroupOrderStatusEnum.GROUPING.getCode().equals(groupOrder.getStatus())) {
            log.info("团单已处理,跳过: groupNo={}, status={}", groupNo, groupOrder.getStatus());
            return;
        }

        LocalDateTime now = LocalDateTime.now();

        // 3. 更新团单状态
        groupOrderMapper.update(null,
            new LambdaUpdateWrapper<GroupOrder>()
                .eq(GroupOrder::getGroupNo, groupNo)
                .eq(GroupOrder::getStatus, GroupOrderStatusEnum.GROUPING.getCode())
                .set(GroupOrder::getStatus, GroupOrderStatusEnum.SUCCESS.getCode())
                .set(GroupOrder::getSuccessTime, now)
        );

        // 4. 更新所有已支付订单状态为已成团
        buyOrderMapper.update(null,
            new LambdaUpdateWrapper<GroupBuyOrder>()
                .eq(GroupBuyOrder::getGroupNo, groupNo)
                .eq(GroupBuyOrder::getOrderStatus, GroupBuyOrderStatusEnum.PAID_GROUPING.getCode())
                .set(GroupBuyOrder::getOrderStatus, GroupBuyOrderStatusEnum.GROUP_SUCCESS.getCode())
        );

        // 5. 更新活动虚拟成团数
        activityMapper.incrementVirtualCount(groupOrder.getActivityId());

        // 6. 发送成团通知消息
        List<GroupMember> members = memberMapper.selectList(
            new LambdaQueryWrapper<GroupMember>()
                .eq(GroupMember::getGroupNo, groupNo)
                .eq(GroupMember::getStatus, 2)  // 已支付
        );

        for (GroupMember member : members) {
            successProducer.sendGroupSuccessNotice(member.getUserId(), groupNo);
        }

        log.info("成团处理完成: groupNo={}, memberCount={}", groupNo, members.size());
    }
}

八、超时处理服务

8.1 超时处理流程

lua 复制代码
+------------------+     +------------------+     +------------------+
|   定时扫描        |     |   查询超时团单    |     |   批量处理        |
+------------------+     +------------------+     +------------------+
         |                        |                        |
         v                        v                        v
    每分钟执行              status=1且过期           更新团单状态
                            expire_time < now        处理退款
                                                     释放库存

+------------------+     +------------------+     +------------------+
|   延迟消息        |     |   消费延迟消息    |     |   处理超时        |
+------------------+     +------------------+     +------------------+
         |                        |                        |
         v                        v                        v
    开团时发送              到期时消费               单个团单处理
    设置延迟时间            检查是否成团             退款+释放库存

8.2 超时处理服务

java 复制代码
package com.groupbuy.service.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.groupbuy.service.entity.*;
import com.groupbuy.service.enums.GroupBuyOrderStatusEnum;
import com.groupbuy.service.enums.GroupOrderStatusEnum;
import com.groupbuy.service.mapper.*;
import com.groupbuy.service.mq.GroupFailProducer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 超时处理服务
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class GroupTimeoutService {

    private final GroupOrderMapper groupOrderMapper;
    private final GroupBuyOrderMapper buyOrderMapper;
    private final GroupMemberMapper memberMapper;
    private final RedisTemplate<String, Object> redisTemplate;
    private final RedissonClient redissonClient;
    private final GroupFailProducer failProducer;
    private final RefundService refundService;

    private static final String STOCK_KEY_PREFIX = "groupbuy:stock:";
    private static final String GROUP_LOCK_PREFIX = "groupbuy:lock:group:";
    private static final String USER_BUY_KEY_PREFIX = "groupbuy:user:buy:";

    /**
     * 定时扫描超时团单
     */
    @Scheduled(cron = "0 * * * * ?")  // 每分钟执行
    public void scanTimeoutGroups() {
        log.info("开始扫描超时团单...");

        LocalDateTime now = LocalDateTime.now();
        int batchSize = 100;

        while (true) {
            // 查询超时的拼团中团单
            List<GroupOrder> timeoutGroups = groupOrderMapper.selectList(
                new LambdaQueryWrapper<GroupOrder>()
                    .eq(GroupOrder::getStatus, GroupOrderStatusEnum.GROUPING.getCode())
                    .lt(GroupOrder::getExpireTime, now)
                    .last("LIMIT " + batchSize)
            );

            if (timeoutGroups.isEmpty()) {
                break;
            }

            for (GroupOrder groupOrder : timeoutGroups) {
                try {
                    handleGroupTimeout(groupOrder.getGroupNo());
                } catch (Exception e) {
                    log.error("处理超时团单失败: groupNo={}", groupOrder.getGroupNo(), e);
                }
            }

            if (timeoutGroups.size() < batchSize) {
                break;
            }
        }

        log.info("超时团单扫描完成");
    }

    /**
     * 处理单个团单超时
     */
    @Transactional(rollbackFor = Exception.class)
    public void handleGroupTimeout(String groupNo) {
        String lockKey = GROUP_LOCK_PREFIX + groupNo;
        RLock lock = redissonClient.getLock(lockKey);

        try {
            boolean acquired = lock.tryLock(3, 30, TimeUnit.SECONDS);
            if (!acquired) {
                log.warn("获取锁失败,跳过处理: groupNo={}", groupNo);
                return;
            }

            // 1. 查询团单
            GroupOrder groupOrder = groupOrderMapper.selectByGroupNo(groupNo);
            if (groupOrder == null) {
                return;
            }

            // 2. 校验状态
            if (!GroupOrderStatusEnum.GROUPING.getCode().equals(groupOrder.getStatus())) {
                return;
            }

            log.info("开始处理超时团单: groupNo={}", groupNo);

            // 3. 更新团单状态为失败
            groupOrderMapper.update(null,
                new LambdaUpdateWrapper<GroupOrder>()
                    .eq(GroupOrder::getGroupNo, groupNo)
                    .eq(GroupOrder::getStatus, GroupOrderStatusEnum.GROUPING.getCode())
                    .set(GroupOrder::getStatus, GroupOrderStatusEnum.FAILED.getCode())
            );

            // 4. 查询所有已支付的订单
            List<GroupBuyOrder> paidOrders = buyOrderMapper.selectList(
                new LambdaQueryWrapper<GroupBuyOrder>()
                    .eq(GroupBuyOrder::getGroupNo, groupNo)
                    .eq(GroupBuyOrder::getOrderStatus, GroupBuyOrderStatusEnum.PAID_GROUPING.getCode())
            );

            // 5. 处理退款
            for (GroupBuyOrder order : paidOrders) {
                try {
                    // 更新订单状态
                    buyOrderMapper.update(null,
                        new LambdaUpdateWrapper<GroupBuyOrder>()
                            .eq(GroupBuyOrder::getOrderNo, order.getOrderNo())
                            .set(GroupBuyOrder::getOrderStatus, GroupBuyOrderStatusEnum.REFUNDED.getCode())
                    );

                    // 更新参团记录
                    memberMapper.update(null,
                        new LambdaUpdateWrapper<GroupMember>()
                            .eq(GroupMember::getOrderNo, order.getOrderNo())
                            .set(GroupMember::getStatus, 3)  // 已退款
                    );

                    // 发起退款
                    refundService.refund(order.getOrderNo(), order.getPayAmount(), "拼团失败自动退款");

                    // 恢复库存
                    rollbackStock(order.getActivityId(), order.getBuyCount());

                    // 恢复用户购买数量
                    rollbackUserBuy(order.getUserId(), order.getActivityId(), order.getBuyCount());

                    // 发送拼团失败通知
                    failProducer.sendGroupFailNotice(order.getUserId(), groupNo);

                    log.info("订单退款处理完成: orderNo={}", order.getOrderNo());

                } catch (Exception e) {
                    log.error("订单退款处理失败: orderNo={}", order.getOrderNo(), e);
                }
            }

            // 6. 处理未支付的订单
            buyOrderMapper.update(null,
                new LambdaUpdateWrapper<GroupBuyOrder>()
                    .eq(GroupBuyOrder::getGroupNo, groupNo)
                    .eq(GroupBuyOrder::getOrderStatus, GroupBuyOrderStatusEnum.UNPAID.getCode())
                    .set(GroupBuyOrder::getOrderStatus, GroupBuyOrderStatusEnum.CANCELLED.getCode())
            );

            log.info("超时团单处理完成: groupNo={}, refundCount={}", groupNo, paidOrders.size());

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.error("处理超时团单被中断: groupNo={}", groupNo);
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    /**
     * 回滚库存
     */
    private void rollbackStock(Long activityId, Integer count) {
        String stockKey = STOCK_KEY_PREFIX + activityId;
        redisTemplate.opsForValue().increment(stockKey, count);
    }

    /**
     * 回滚用户购买数量
     */
    private void rollbackUserBuy(Long userId, Long activityId, Integer count) {
        String key = USER_BUY_KEY_PREFIX + activityId + ":" + userId;
        redisTemplate.opsForValue().decrement(key, count);
    }
}

8.3 延迟消息方案

java 复制代码
package com.groupbuy.service.mq;

import com.alibaba.fastjson.JSON;
import com.groupbuy.service.dto.GroupTimeoutMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;

/**
 * 团单超时消息生产者
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class GroupTimeoutProducer {

    private final RocketMQTemplate rocketMQTemplate;

    private static final String TOPIC = "group-timeout-topic";

    /**
     * 发送超时消息
     * @param groupNo 团单号
     * @param delayMinutes 延迟分钟数
     */
    public void sendTimeoutMessage(String groupNo, int delayMinutes) {
        GroupTimeoutMessage message = new GroupTimeoutMessage();
        message.setGroupNo(groupNo);
        message.setCreateTime(System.currentTimeMillis());

        String jsonMessage = JSON.toJSONString(message);
        Message<String> msg = MessageBuilder.withPayload(jsonMessage).build();

        // 计算延迟级别
        int delayLevel = calculateDelayLevel(delayMinutes);

        rocketMQTemplate.syncSend(TOPIC, msg, 3000, delayLevel);

        log.info("团单超时消息已发送: groupNo={}, delayMinutes={}", groupNo, delayMinutes);
    }

    /**
     * 计算延迟级别
     * RocketMQ延迟级别:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
     */
    private int calculateDelayLevel(int minutes) {
        if (minutes <= 1) return 5;   // 1m
        if (minutes <= 2) return 6;   // 2m
        if (minutes <= 3) return 7;   // 3m
        if (minutes <= 5) return 9;   // 5m
        if (minutes <= 10) return 14; // 10m
        if (minutes <= 20) return 15; // 20m
        if (minutes <= 30) return 16; // 30m
        if (minutes <= 60) return 17; // 1h
        return 18; // 2h
    }
}
java 复制代码
package com.groupbuy.service.mq;

import com.alibaba.fastjson.JSON;
import com.groupbuy.service.dto.GroupTimeoutMessage;
import com.groupbuy.service.service.GroupTimeoutService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;

/**
 * 团单超时消息消费者
 */
@Slf4j
@Component
@RequiredArgsConstructor
@RocketMQMessageListener(
    topic = "group-timeout-topic",
    consumerGroup = "group-timeout-consumer-group"
)
public class GroupTimeoutConsumer implements RocketMQListener<String> {

    private final GroupTimeoutService timeoutService;

    @Override
    public void onMessage(String message) {
        log.info("收到团单超时消息: {}", message);

        try {
            GroupTimeoutMessage timeoutMessage = JSON.parseObject(message, GroupTimeoutMessage.class);
            timeoutService.handleGroupTimeout(timeoutMessage.getGroupNo());
        } catch (Exception e) {
            log.error("处理团单超时消息失败: {}", message, e);
        }
    }
}

九、接口层实现

9.1 Controller

java 复制代码
package com.groupbuy.service.controller;

import com.groupbuy.common.result.Result;
import com.groupbuy.service.dto.*;
import com.groupbuy.service.service.*;
import com.groupbuy.service.utils.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.List;

/**
 * 拼团接口
 */
@Slf4j
@RestController
@RequestMapping("/group")
@RequiredArgsConstructor
public class GroupBuyController {

    private final CreateGroupService createGroupService;
    private final JoinGroupService joinGroupService;
    private final GroupQueryService queryService;

    /**
     * 获取拼团活动列表
     */
    @GetMapping("/activity/list")
    public Result<List<GroupActivityDTO>> getActivityList() {
        List<GroupActivityDTO> activities = queryService.getActivityList();
        return Result.success(activities);
    }

    /**
     * 获取拼团活动详情
     */
    @GetMapping("/activity/{activityId}")
    public Result<GroupActivityDetailDTO> getActivityDetail(@PathVariable Long activityId) {
        GroupActivityDetailDTO detail = queryService.getActivityDetail(activityId);
        return Result.success(detail);
    }

    /**
     * 开团
     */
    @PostMapping("/create")
    public Result<CreateGroupResponse> createGroup(@Valid @RequestBody CreateGroupRequest request) {
        Long userId = UserContext.getCurrentUserId();
        CreateGroupResponse response = createGroupService.createGroup(userId, request);
        return Result.success(response);
    }

    /**
     * 参团
     */
    @PostMapping("/join")
    public Result<JoinGroupResponse> joinGroup(@Valid @RequestBody JoinGroupRequest request) {
        Long userId = UserContext.getCurrentUserId();
        JoinGroupResponse response = joinGroupService.joinGroup(userId, request);
        return Result.success(response);
    }

    /**
     * 获取团单详情
     */
    @GetMapping("/order/{groupNo}")
    public Result<GroupOrderDetailDTO> getGroupOrderDetail(@PathVariable String groupNo) {
        GroupOrderDetailDTO detail = queryService.getGroupOrderDetail(groupNo);
        return Result.success(detail);
    }

    /**
     * 获取可参与的团单列表
     */
    @GetMapping("/activity/{activityId}/groups")
    public Result<List<GroupOrderDTO>> getJoinableGroups(@PathVariable Long activityId) {
        List<GroupOrderDTO> groups = queryService.getJoinableGroups(activityId);
        return Result.success(groups);
    }

    /**
     * 获取我的拼团列表
     */
    @GetMapping("/my/list")
    public Result<List<MyGroupOrderDTO>> getMyGroupOrders(
            @RequestParam(required = false) Integer status) {
        Long userId = UserContext.getCurrentUserId();
        List<MyGroupOrderDTO> orders = queryService.getMyGroupOrders(userId, status);
        return Result.success(orders);
    }

    /**
     * 生成分享链接
     */
    @GetMapping("/share/{groupNo}")
    public Result<ShareLinkDTO> generateShareLink(@PathVariable String groupNo) {
        Long userId = UserContext.getCurrentUserId();
        ShareLinkDTO shareLink = queryService.generateShareLink(userId, groupNo);
        return Result.success(shareLink);
    }
}

9.2 查询服务

java 复制代码
package com.groupbuy.service.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.groupbuy.service.dto.*;
import com.groupbuy.service.entity.*;
import com.groupbuy.service.enums.GroupOrderStatusEnum;
import com.groupbuy.service.mapper.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 拼团查询服务
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class GroupQueryService {

    private final GroupActivityMapper activityMapper;
    private final GroupOrderMapper groupOrderMapper;
    private final GroupMemberMapper memberMapper;
    private final GroupBuyOrderMapper buyOrderMapper;

    /**
     * 获取活动列表
     */
    public List<GroupActivityDTO> getActivityList() {
        LocalDateTime now = LocalDateTime.now();

        List<GroupActivity> activities = activityMapper.selectList(
            new LambdaQueryWrapper<GroupActivity>()
                .eq(GroupActivity::getStatus, 2)  // 进行中
                .le(GroupActivity::getStartTime, now)
                .ge(GroupActivity::getEndTime, now)
                .orderByDesc(GroupActivity::getCreateTime)
        );

        return activities.stream().map(this::convertToDTO).collect(Collectors.toList());
    }

    /**
     * 获取活动详情
     */
    public GroupActivityDetailDTO getActivityDetail(Long activityId) {
        GroupActivity activity = activityMapper.selectById(activityId);
        if (activity == null) {
            return null;
        }

        GroupActivityDetailDTO dto = new GroupActivityDetailDTO();
        BeanUtils.copyProperties(activity, dto);

        // 计算已成团数
        int successCount = groupOrderMapper.countByActivityAndStatus(
            activityId, GroupOrderStatusEnum.SUCCESS.getCode());
        dto.setSuccessGroupCount(successCount + activity.getVirtualCount());

        // 获取正在拼团的团单
        List<GroupOrder> groupingOrders = groupOrderMapper.selectList(
            new LambdaQueryWrapper<GroupOrder>()
                .eq(GroupOrder::getActivityId, activityId)
                .eq(GroupOrder::getStatus, GroupOrderStatusEnum.GROUPING.getCode())
                .gt(GroupOrder::getExpireTime, LocalDateTime.now())
                .orderByDesc(GroupOrder::getCurrentSize)
                .last("LIMIT 10")
        );

        dto.setGroupingOrders(groupingOrders.stream()
            .map(this::convertToGroupOrderDTO)
            .collect(Collectors.toList()));

        return dto;
    }

    /**
     * 获取团单详情
     */
    public GroupOrderDetailDTO getGroupOrderDetail(String groupNo) {
        GroupOrder groupOrder = groupOrderMapper.selectByGroupNo(groupNo);
        if (groupOrder == null) {
            return null;
        }

        GroupActivity activity = activityMapper.selectById(groupOrder.getActivityId());

        GroupOrderDetailDTO dto = new GroupOrderDetailDTO();
        dto.setGroupNo(groupNo);
        dto.setActivityId(activity.getId());
        dto.setActivityName(activity.getName());
        dto.setProductName(activity.getProductName());
        dto.setProductImg(activity.getProductImg());
        dto.setOriginalPrice(activity.getOriginalPrice());
        dto.setGroupPrice(activity.getGroupPrice());
        dto.setGroupSize(groupOrder.getGroupSize());
        dto.setCurrentSize(groupOrder.getCurrentSize());
        dto.setRemainSize(groupOrder.getGroupSize() - groupOrder.getCurrentSize());
        dto.setStatus(groupOrder.getStatus());
        dto.setStatusDesc(GroupOrderStatusEnum.of(groupOrder.getStatus()).getDesc());
        dto.setExpireTime(groupOrder.getExpireTime());
        dto.setCreateTime(groupOrder.getCreateTime());

        // 计算剩余时间
        if (GroupOrderStatusEnum.GROUPING.getCode().equals(groupOrder.getStatus())) {
            Duration duration = Duration.between(LocalDateTime.now(), groupOrder.getExpireTime());
            dto.setRemainSeconds(Math.max(0, duration.getSeconds()));
        }

        // 获取参团成员
        List<GroupMember> members = memberMapper.selectList(
            new LambdaQueryWrapper<GroupMember>()
                .eq(GroupMember::getGroupNo, groupNo)
                .eq(GroupMember::getStatus, 2)  // 已支付
                .orderByAsc(GroupMember::getCreateTime)
        );

        dto.setMembers(members.stream().map(this::convertToMemberDTO).collect(Collectors.toList()));

        // 设置团长信息
        dto.setLeaderUserId(groupOrder.getLeaderUserId());
        // TODO: 调用用户服务获取团长昵称头像

        return dto;
    }

    /**
     * 获取可参与的团单列表
     */
    public List<GroupOrderDTO> getJoinableGroups(Long activityId) {
        LocalDateTime now = LocalDateTime.now();

        List<GroupOrder> groups = groupOrderMapper.selectList(
            new LambdaQueryWrapper<GroupOrder>()
                .eq(GroupOrder::getActivityId, activityId)
                .eq(GroupOrder::getStatus, GroupOrderStatusEnum.GROUPING.getCode())
                .gt(GroupOrder::getExpireTime, now)
                .orderByDesc(GroupOrder::getCurrentSize)
                .last("LIMIT 20")
        );

        return groups.stream().map(this::convertToGroupOrderDTO).collect(Collectors.toList());
    }

    /**
     * 获取我的拼团列表
     */
    public List<MyGroupOrderDTO> getMyGroupOrders(Long userId, Integer status) {
        LambdaQueryWrapper<GroupBuyOrder> wrapper = new LambdaQueryWrapper<GroupBuyOrder>()
            .eq(GroupBuyOrder::getUserId, userId)
            .orderByDesc(GroupBuyOrder::getCreateTime);

        if (status != null) {
            wrapper.eq(GroupBuyOrder::getOrderStatus, status);
        }

        List<GroupBuyOrder> orders = buyOrderMapper.selectList(wrapper);

        return orders.stream().map(order -> {
            MyGroupOrderDTO dto = new MyGroupOrderDTO();
            BeanUtils.copyProperties(order, dto);

            // 获取团单信息
            GroupOrder groupOrder = groupOrderMapper.selectByGroupNo(order.getGroupNo());
            if (groupOrder != null) {
                dto.setGroupStatus(groupOrder.getStatus());
                dto.setGroupSize(groupOrder.getGroupSize());
                dto.setCurrentSize(groupOrder.getCurrentSize());
                dto.setExpireTime(groupOrder.getExpireTime());
            }

            return dto;
        }).collect(Collectors.toList());
    }

    /**
     * 生成分享链接
     */
    public ShareLinkDTO generateShareLink(Long userId, String groupNo) {
        GroupOrder groupOrder = groupOrderMapper.selectByGroupNo(groupNo);
        if (groupOrder == null) {
            return null;
        }

        GroupActivity activity = activityMapper.selectById(groupOrder.getActivityId());

        ShareLinkDTO dto = new ShareLinkDTO();
        dto.setGroupNo(groupNo);
        dto.setShareUrl(String.format("https://m.example.com/group/join?groupNo=%s&inviter=%d",
            groupNo, userId));
        dto.setTitle(String.format("我正在拼【%s】,还差%d人成团,快来帮我砍一刀!",
            activity.getProductName(),
            groupOrder.getGroupSize() - groupOrder.getCurrentSize()));
        dto.setDescription(String.format("拼团价仅需%.2f元,原价%.2f元",
            activity.getGroupPrice(), activity.getOriginalPrice()));
        dto.setImageUrl(activity.getProductImg());

        return dto;
    }

    private GroupActivityDTO convertToDTO(GroupActivity activity) {
        GroupActivityDTO dto = new GroupActivityDTO();
        BeanUtils.copyProperties(activity, dto);
        return dto;
    }

    private GroupOrderDTO convertToGroupOrderDTO(GroupOrder groupOrder) {
        GroupOrderDTO dto = new GroupOrderDTO();
        dto.setGroupNo(groupOrder.getGroupNo());
        dto.setGroupSize(groupOrder.getGroupSize());
        dto.setCurrentSize(groupOrder.getCurrentSize());
        dto.setRemainSize(groupOrder.getGroupSize() - groupOrder.getCurrentSize());
        dto.setExpireTime(groupOrder.getExpireTime());

        // 计算剩余时间
        Duration duration = Duration.between(LocalDateTime.now(), groupOrder.getExpireTime());
        dto.setRemainSeconds(Math.max(0, duration.getSeconds()));

        return dto;
    }

    private GroupMemberDTO convertToMemberDTO(GroupMember member) {
        GroupMemberDTO dto = new GroupMemberDTO();
        dto.setUserId(member.getUserId());
        dto.setIsLeader(member.getIsLeader() == 1);
        dto.setJoinTime(member.getPayTime());
        // TODO: 调用用户服务获取昵称头像
        return dto;
    }
}

十、库存预热服务

java 复制代码
package com.groupbuy.service.service;

import com.groupbuy.service.entity.GroupActivity;
import com.groupbuy.service.mapper.GroupActivityMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 库存预热服务
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class StockWarmUpService {

    private final RedisTemplate<String, Object> redisTemplate;
    private final GroupActivityMapper activityMapper;

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

    /**
     * 启动时预热
     */
    @PostConstruct
    public void warmUpOnStartup() {
        log.info("系统启动,开始预热拼团库存...");
        warmUpStock();
    }

    /**
     * 定时预热(每5分钟)
     */
    @Scheduled(cron = "0 */5 * * * ?")
    public void scheduledWarmUp() {
        warmUpStock();
    }

    /**
     * 预热库存
     */
    public void warmUpStock() {
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime future = now.plusHours(1);

        // 查询进行中或即将开始的活动
        List<GroupActivity> activities = activityMapper.selectActiveActivities(now, future);

        for (GroupActivity activity : activities) {
            String stockKey = STOCK_KEY_PREFIX + activity.getId();

            // 检查是否已预热
            if (Boolean.TRUE.equals(redisTemplate.hasKey(stockKey))) {
                continue;
            }

            // 预热库存
            redisTemplate.opsForValue().set(stockKey, activity.getRemainStock());

            // 设置过期时间
            long expireSeconds = java.time.Duration.between(
                now, activity.getEndTime().plusHours(1)).getSeconds();
            if (expireSeconds > 0) {
                redisTemplate.expire(stockKey, expireSeconds, TimeUnit.SECONDS);
            }

            log.info("活动库存预热完成: activityId={}, stock={}",
                activity.getId(), activity.getRemainStock());
        }
    }

    /**
     * 同步库存到数据库
     */
    @Scheduled(cron = "0 */10 * * * ?")
    public void syncStockToDb() {
        log.info("开始同步拼团库存到数据库...");

        LocalDateTime now = LocalDateTime.now();
        List<GroupActivity> activities = activityMapper.selectActiveActivities(
            now, now.plusDays(7));

        for (GroupActivity activity : activities) {
            String stockKey = STOCK_KEY_PREFIX + activity.getId();
            Object redisStock = redisTemplate.opsForValue().get(stockKey);

            if (redisStock != null) {
                int stock = Integer.parseInt(redisStock.toString());
                activityMapper.updateRemainStock(activity.getId(), stock);
            }
        }

        log.info("拼团库存同步完成");
    }
}

十一、统计分析

11.1 统计服务

java 复制代码
package com.groupbuy.service.service;

import com.groupbuy.service.dto.GroupStatisticsDTO;
import com.groupbuy.service.mapper.GroupStatisticsMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;

/**
 * 拼团统计服务
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class GroupStatisticsService {

    private final GroupStatisticsMapper statisticsMapper;

    /**
     * 获取活动统计
     */
    public GroupStatisticsDTO getActivityStatistics(Long activityId) {
        GroupStatisticsDTO stats = new GroupStatisticsDTO();

        // 开团数
        stats.setTotalGroups(statisticsMapper.countGroupsByActivity(activityId));

        // 成团数
        stats.setSuccessGroups(statisticsMapper.countSuccessGroupsByActivity(activityId));

        // 失败数
        stats.setFailedGroups(statisticsMapper.countFailedGroupsByActivity(activityId));

        // 参团人数
        stats.setTotalParticipants(statisticsMapper.countParticipantsByActivity(activityId));

        // 订单金额
        stats.setTotalAmount(statisticsMapper.sumAmountByActivity(activityId));

        // 成团率
        if (stats.getTotalGroups() > 0) {
            BigDecimal successRate = BigDecimal.valueOf(stats.getSuccessGroups())
                .divide(BigDecimal.valueOf(stats.getTotalGroups()), 4, RoundingMode.HALF_UP)
                .multiply(BigDecimal.valueOf(100));
            stats.setSuccessRate(successRate);
        }

        return stats;
    }

    /**
     * 获取日统计
     */
    public GroupStatisticsDTO getDailyStatistics(LocalDate date) {
        GroupStatisticsDTO stats = new GroupStatisticsDTO();

        stats.setTotalGroups(statisticsMapper.countGroupsByDate(date));
        stats.setSuccessGroups(statisticsMapper.countSuccessGroupsByDate(date));
        stats.setTotalParticipants(statisticsMapper.countParticipantsByDate(date));
        stats.setTotalAmount(statisticsMapper.sumAmountByDate(date));

        return stats;
    }
}
java 复制代码
package com.groupbuy.service.dto;

import lombok.Data;
import java.math.BigDecimal;

/**
 * 拼团统计DTO
 */
@Data
public class GroupStatisticsDTO {

    /**
     * 开团总数
     */
    private Integer totalGroups;

    /**
     * 成团数
     */
    private Integer successGroups;

    /**
     * 失败数
     */
    private Integer failedGroups;

    /**
     * 参团人数
     */
    private Integer totalParticipants;

    /**
     * 订单总金额
     */
    private BigDecimal totalAmount;

    /**
     * 成团率(%)
     */
    private BigDecimal successRate;

    /**
     * 平均成团时间(分钟)
     */
    private Integer avgSuccessMinutes;

    /**
     * 新用户参团数
     */
    private Integer newUserCount;
}

十二、配置文件

12.1 application.yml

yaml 复制代码
server:
  port: 8082

spring:
  application:
    name: groupbuy-service

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/groupbuy?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: root
    hikari:
      minimum-idle: 10
      maximum-pool-size: 50
      connection-timeout: 30000

  redis:
    host: localhost
    port: 6379
    database: 0
    lettuce:
      pool:
        max-active: 100
        max-idle: 50
        min-idle: 10

rocketmq:
  name-server: localhost:9876
  producer:
    group: groupbuy-producer-group
    send-message-timeout: 3000

mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  global-config:
    db-config:
      id-type: auto
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

logging:
  level:
    com.groupbuy: debug
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"

# 拼团配置
groupbuy:
  # 默认成团有效期(分钟)
  default-expire-minutes: 1440
  # 库存预热提前时间(分钟)
  warmup-advance-minutes: 60
  # 超时扫描间隔(秒)
  timeout-scan-interval: 60

十三、总结

13.1 核心技术点

diff 复制代码
+------------------+------------------+----------------------------------+
|      模块        |      技术        |             说明                 |
+------------------+------------------+----------------------------------+
|     开团服务     |  Redis库存       |   预扣减库存,防止超卖            |
+------------------+------------------+----------------------------------+
|     参团服务     |  分布式锁        |   防止并发参团超员                |
+------------------+------------------+----------------------------------+
|     成团处理     |  事务+MQ        |   状态更新+异步通知               |
+------------------+------------------+----------------------------------+
|     超时处理     |  定时+延迟队列   |   双重保障超时处理                |
+------------------+------------------+----------------------------------+
|     库存管理     |  预热+同步      |   Redis预热+定时同步DB            |
+------------------+------------------+----------------------------------+

13.2 关键设计要点

  1. 分布式锁:参团时加锁,防止并发超员
  2. 状态机:团单状态流转清晰可控
  3. 库存预扣:下单即扣库存,支付失败回滚
  4. 双重超时:定时扫描 + 延迟消息双保险
  5. 幂等设计:支付回调、成团处理均幂等
  6. 异步通知:成团/失败通过MQ异步通知用户

13.3 扩展方向

  1. 阶梯拼团:人数越多,价格越低
  2. 老带新:团长邀请新用户有额外奖励
  3. 虚拟成团:时间到自动补齐人数成团
  4. 机器人凑团:系统自动凑团提高成团率
  5. 拼团排行:展示拼团达人排行榜

相关推荐
青云交1 小时前
Java 大视界 -- Java 大数据在智能医疗影像数据标注与疾病辅助诊断模型训练中的应用
java·大数据·多模态融合·医疗影像标注·辅助诊断·临床 ai·dicom 处理
雨中飘荡的记忆1 小时前
Step Builder模式实战
java·设计模式
悦来客栈的老板1 小时前
AST反混淆实战|reese84_jsvmp反编译前的优化处理
java·前端·javascript·数据库·算法
悟空码字1 小时前
SpringBoot实现日志系统,Bug现形记
java·spring boot·后端
iナナ1 小时前
Java自定义协议的发布订阅式消息队列(二)
java·开发语言·jvm·学习·spring·消息队列
狂奔小菜鸡1 小时前
Day24 | Java泛型通配符与边界解析
java·后端·java ee
天天摸鱼的java工程师1 小时前
🐇RabbitMQ 从入门到业务实战:一个 Java 程序员的实战手记
java·后端
ZHang......1 小时前
JDBC 笔记
java·笔记
uup1 小时前
多线程下线程安全的单例模式实现缺陷
java