DDD拼团系统常用设计模式

首先是产品需求分拼团模式是线性
创建活动领域工厂,返回对应责任策略
下面是表设计
根据你提供的数据库结构,我将每个表的字段信息整理成表格形式:

1. crowd_tags(人群标签表)

字段名 数据类型 是否主键 是否必填 默认值 注释
id int unsigned AUTO_INCREMENT 自增ID
tag_id varchar(32) 人群ID
tag_name varchar(64) 人群名称
tag_desc varchar(256) 人群描述
statistics int 人群标签统计量
create_time datetime CURRENT_TIMESTAMP 创建时间
update_time datetime CURRENT_TIMESTAMP ON UPDATE 更新时间

2. crowd_tags_detail(人群标签明细表)

字段名 数据类型 是否主键 是否必填 默认值 注释
id int unsigned AUTO_INCREMENT 自增ID
tag_id varchar(32) 人群ID
user_id varchar(16) 用户ID
create_time datetime CURRENT_TIMESTAMP 创建时间
update_time datetime CURRENT_TIMESTAMP ON UPDATE 更新时间

3. crowd_tags_job(人群标签任务表)

字段名 数据类型 是否主键 是否必填 默认值 注释
id int unsigned AUTO_INCREMENT 自增ID
tag_id varchar(32) 标签ID
batch_id varchar(8) 批次ID
tag_type tinyint(1) 1 标签类型(参与量、消费金额)
tag_rule varchar(8) 标签规则(限定类型 N次)
stat_start_time datetime CURRENT_TIMESTAMP 统计数据,开始时间
stat_end_time datetime CURRENT_TIMESTAMP 统计数据,结束时间
status tinyint(1) 0 状态;0初始、1计划、2重置、3完成
create_time datetime CURRENT_TIMESTAMP 创建时间
update_time datetime CURRENT_TIMESTAMP ON UPDATE 更新时间

4. group_buy_activity(拼团活动表)

字段名 数据类型 是否主键 是否必填 默认值 注释
id bigint unsigned AUTO_INCREMENT 自增
activity_id bigint 活动ID
activity_name varchar(128) 活动名称
discount_id varchar(8) 折扣ID
group_type tinyint(1) 0 拼团方式(0自动成团、1达成目标拼团)
take_limit_count int 1 拼团次数限制
target int 1 拼团目标
valid_time int 15 拼团时长(分钟)
status tinyint(1) 0 活动状态(0创建、1生效、2过期、3废弃)
start_time datetime CURRENT_TIMESTAMP 活动开始时间
end_time datetime CURRENT_TIMESTAMP 活动结束时间
tag_id varchar(8) NULL 人群标签规则标识
tag_scope varchar(4) NULL 人群标签规则范围(多选;1可见限制、2参与限制)
create_time datetime CURRENT_TIMESTAMP 创建时间
update_time datetime CURRENT_TIMESTAMP ON UPDATE 更新时间

5. group_buy_discount(拼团折扣表)

字段名 数据类型 是否主键 是否必填 默认值 注释
id bigint unsigned AUTO_INCREMENT 自增ID
discount_id varchar(8) 折扣ID
discount_name varchar(64) 折扣标题
discount_desc varchar(256) 折扣描述
discount_type tinyint(1) 0 折扣类型(0:base、1:tag)
market_plan varchar(4) ZJ 营销优惠计划(ZJ:直减、MJ:满减、N元购)
market_expr varchar(32) 营销优惠表达式
tag_id varchar(8) NULL 人群标签,特定优惠限定
create_time datetime CURRENT_TIMESTAMP 创建时间
update_time datetime CURRENT_TIMESTAMP ON UPDATE 更新时间

6. group_buy_order(拼团订单表)

字段名 数据类型 是否主键 是否必填 默认值 注释
id int unsigned AUTO_INCREMENT 自增ID
team_id varchar(8) 拼单组队ID
activity_id bigint 活动ID
source varchar(8) 渠道
channel varchar(8) 来源
original_price decimal(8,2) 原始价格
deduction_price decimal(8,2) 折扣金额
pay_price decimal(8,2) 支付价格
target_count int 目标数量
complete_count int 完成数量
lock_count int 锁单数量
status tinyint(1) 0 状态(0-拼单中、1-完成、2-失败、3-完成-含退单)
valid_start_time datetime 拼团开始时间
valid_end_time datetime 拼团结束时间
notify_type varchar(8) HTTP 回调类型(HTTP、MQ)
notify_url varchar(512) NULL 回调地址(HTTP 回调不可为空)
create_time datetime CURRENT_TIMESTAMP 创建时间
update_time datetime CURRENT_TIMESTAMP ON UPDATE 更新时间

7. group_buy_order_list(拼团订单列表表)

字段名 数据类型 是否主键 是否必填 默认值 注释
id int unsigned AUTO_INCREMENT 自增ID
user_id varchar(64) 用户ID
team_id varchar(8) 拼单组队ID
order_id varchar(12) 订单ID
activity_id bigint 活动ID
start_time datetime 活动开始时间
end_time datetime 活动结束时间
goods_id varchar(16) 商品ID
source varchar(8) 渠道
channel varchar(8) 来源
original_price decimal(8,2) 原始价格
deduction_price decimal(8,2) 折扣金额
pay_price decimal(8,2) 支付金额
status tinyint(1) 0 状态;0初始锁定、1消费完成、2用户退单
out_trade_no varchar(12) 外部交易单号-确保外部调用唯一幂等
out_trade_time datetime NULL 外部交易时间
biz_id varchar(64) 业务唯一ID
create_time datetime CURRENT_TIMESTAMP 创建时间
update_time datetime CURRENT_TIMESTAMP ON UPDATE 更新时间

8. notify_task(通知任务表)

字段名 数据类型 是否主键 是否必填 默认值 注释
id int unsigned AUTO_INCREMENT 自增ID
activity_id bigint 活动ID
team_id varchar(8) 拼单组队ID
notify_category varchar(64) NULL 回调种类
notify_type varchar(8) HTTP 回调类型(HTTP、MQ)
notify_mq varchar(32) NULL 回调消息
notify_url varchar(128) NULL 回调接口
notify_count int 回调次数
notify_status tinyint(1) 回调状态【0初始、1完成、2重试、3失败】
parameter_json varchar(256) 参数对象
uuid varchar(128) 唯一标识
create_time datetime CURRENT_TIMESTAMP 创建时间
update_time datetime CURRENT_TIMESTAMP ON UPDATE 更新时间

9. sc_sku_activity(渠道商品活动配置关联表)

字段名 数据类型 是否主键 是否必填 默认值 注释
id int unsigned AUTO_INCREMENT 自增ID
source varchar(8) 渠道
channel varchar(8) 来源
activity_id bigint 活动ID
goods_id varchar(16) 商品ID
create_time datetime CURRENT_TIMESTAMP 创建时间
update_time datetime CURRENT_TIMESTAMP ON UPDATE 更新时间

10. sku(商品信息表)

字段名 数据类型 是否主键 是否必填 默认值 注释
id int unsigned AUTO_INCREMENT 自增ID
source varchar(8) 渠道
channel varchar(8) 来源
goods_id varchar(16) 商品ID
goods_name varchar(128) 商品名称
original_price decimal(10,2) 商品价格
create_time datetime CURRENT_TIMESTAMP 创建时间
update_time datetime CURRENT_TIMESTAMP ON UPDATE 更新时间

索引信息:

  • 每个表都有主键索引
  • 多个表有唯一索引(uq_*)
  • 部分表有复合索引和普通索引

结合之前领域模式,我们可以创建一个接口StrategyMapper<T,D,R>策略下一个节点,StrategyHandler<T,D,R>策略处理器
T入参,D领域封装对象,R返回

/**
 * @author Yu.Pan
 * @description 策略映射器
 * T 入参类型
 * D 上下文参数
 * R 返参类型
 * @create 2024-12-14 12:05
 */
public interface StrategyMapper<T, D, R> {

    /**
     * 获取待执行策略
     *
     * @param requestParameter 入参
     * @param dynamicContext   上下文
     * @return 返参
     * @throws Exception 异常
     */
    StrategyHandler<T, D, R> get(T requestParameter, D dynamicContext) throws Exception;

}
/**
 * @author Yu.Pan
 * @description 受理策略处理
 * T 入参类型
 * D 上下文参数
 * R 返参类型
 * @create 2024-12-14 12:06
 */
public interface StrategyHandler<T, D, R> {

    StrategyHandler DEFAULT = (T, D) -> null;

    R apply(T requestParameter, D dynamicContext) throws Exception;

}

public abstract class AbstractMultiThreadStrategyRouter<T, D, R> implements StrategyMapper<T, D, R>, StrategyHandler<T, D, R> {


    protected StrategyHandler<T, D, R> defaultStrategyHandler = StrategyHandler.DEFAULT;


    public R router(T requestParameter, D dynamicContext) throws Exception {
        //获取下一个节点,然后执行apply方法
        StrategyHandler<T, D, R> next = get(requestParameter, dynamicContext);
        if(null != next) return next.apply(requestParameter, dynamicContext);
        //如果下一个节点不存在,默认返回
        return defaultStrategyHandler.apply(requestParameter, dynamicContext);
    }

//    public R chain(T requestParameter, D dynamicContext) throws Exception {
//        R apply = apply(requestParameter, dynamicContext);
//        if(apply != null) {
//            return apply;
//        }else{
//            StrategyHandler<T, D, R> next = get(requestParameter, dynamicContext);
            //可以在StrategyHandler增加chain方法可以地柜调用了
//            return next.chain(requestParameter,dynamicContext);
//        }
//    }

    @Override
    public R apply(T requestParameter, D dynamicContext) throws Exception {
        // 异步加载数据
        multiThread(requestParameter, dynamicContext);
        // 业务流程受理
        return doApply(requestParameter, dynamicContext);
    }

    /**
     * 异步加载数据
     */
    protected abstract void multiThread(T requestParameter, D dynamicContext) throws ExecutionException, InterruptedException, TimeoutException;

    /**
     * 业务流程受理
     */
    protected abstract R doApply(T requestParameter, D dynamicContext) throws Exception;

}

构建拼团工厂默认返回RootNode,其中包含整个流程的传输对象


@Service
public class DefaultActivityStrategyFactory {

    private final RootNode rootNode;

    public DefaultActivityStrategyFactory(RootNode rootNode) {
        this.rootNode = rootNode;
    }

    public StrategyHandler<MarketProductEntity, DynamicContext, TrialBalanceEntity> strategyHandler() {
        return rootNode;
    }

    @Data
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    public static class DynamicContext {
        // 拼团活动营销配置值对象
        private GroupBuyActivityDiscountVO groupBuyActivityDiscountVO;
        // 商品信息
        private SkuVO skuVO;
        // 折扣金额
        private BigDecimal deductionPrice;
        // 支付金额
        private BigDecimal payPrice;
        // 活动可见性限制
        private boolean visible;
        // 活动
        private boolean enable;
    }

}

整个服务降级,限流,黑名单可以使用demo版本动态配置


@Service
public class DCCService {

    /**
     * 降级开关 0关闭、1开启
     */
    @DCCValue("downgradeSwitch:0")
    private String downgradeSwitch;

    @DCCValue("cutRange:100")
    private String cutRange;

    @DCCValue("scBlacklist:s02c02")
    private String scBlacklist;

    @DCCValue("cacheSwitch:0")
    private String cacheOpenSwitch;

    public boolean isDowngradeSwitch() {
        return "1".equals(downgradeSwitch);
    }

    public boolean isCutRange(String userId) {
        // 计算哈希码的绝对值
        int hashCode = Math.abs(userId.hashCode());

        // 获取最后两位
        int lastTwoDigits = hashCode % 100;

        // 判断是否在切量范围内
        if (lastTwoDigits <= Integer.parseInt(cutRange)) {
            return true;
        }

        return false;
    }

    /**
     * 判断黑名单拦截渠道,true 拦截、false 放行
     */
    public boolean isSCBlackIntercept(String source, String channel) {
        List<String> list = Arrays.asList(scBlacklist.split(Constants.SPLIT));
        return list.contains(source + channel);
    }

    /**
     * 缓存开启开关,true为开启,1为关闭
     */
    public boolean isCacheOpenSwitch(){
        return "0".equals(cacheOpenSwitch);
    }

}

缓存配置开关用于封装了Repository

public abstract class AbstractRepository {

    private final Logger logger = LoggerFactory.getLogger(AbstractRepository.class);

    @Resource
    protected IRedisService redisService;
    
    @Resource
    protected DCCService dccService;

    /**
     * 通用缓存处理方法
     * 优先从缓存获取,缓存不存在则从数据库获取并写入缓存
     *
     * @param cacheKey      缓存键
     * @param dbFallback    数据库查询函数
     * @param <T>           返回类型
     * @return              查询结果
     */
    protected <T> T getFromCacheOrDb(String cacheKey, Supplier<T> dbFallback) {
        // 判断是否开启缓存
        if (dccService.isCacheOpenSwitch()) {
            // 从缓存获取
            T cacheResult = redisService.getValue(cacheKey);
            // 缓存存在则直接返回
            if (null != cacheResult) {
                return cacheResult;
            }
            // 缓存不存在则从数据库获取
            T dbResult = dbFallback.get();
            // 数据库查询结果为空则直接返回
            if (null == dbResult) {
                return null;
            }
            // 写入缓存
            redisService.setValue(cacheKey, dbResult);
            return dbResult;
        } else {
            // 缓存未开启,直接从数据库获取
            logger.warn("缓存降级 {}", cacheKey);
            return dbFallback.get();
        }
    }

RootNode做参数校验,用户 商品 来源 渠道
SwitchNode做降级 用户切量拦截
MarketNode使用线程池填充DynamicContext,拼团活动()对象,SKU商品对象,计算优惠结果封装到dynamicContext
TagNode对用户标签管理,是否可见,可参与
EndNode返回封装对象结果
主要包含

public class TrialBalanceEntity {

    /** 商品ID */
    private String goodsId;
    /** 商品名称 */
    private String goodsName;
    /** 原始价格 */
    private BigDecimal originalPrice;
    // 折扣金额
    private BigDecimal deductionPrice;
    // 支付金额
    private BigDecimal payPrice;
    /** 拼团目标数量 */
    private Integer targetCount;
    /** 拼团开始时间 */
    private Date startTime;
    /** 拼团结束时间 */
    private Date endTime;
    /** 是否可见拼团 */
    private Boolean isVisible;
    /** 是否可参与进团 */
    private Boolean isEnable;

    /** 活动配置信息 */
    private GroupBuyActivityDiscountVO groupBuyActivityDiscountVO;

}

@Override
    protected void multiThread(MarketProductEntity requestParameter, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws ExecutionException, InterruptedException, TimeoutException {
        // 异步查询活动配置
        QueryGroupBuyActivityDiscountVOThreadTask queryGroupBuyActivityDiscountVOThreadTask = new QueryGroupBuyActivityDiscountVOThreadTask(requestParameter.getActivityId(), requestParameter.getSource(), requestParameter.getChannel(), requestParameter.getGoodsId(), repository);
        FutureTask<GroupBuyActivityDiscountVO> groupBuyActivityDiscountVOFutureTask = new FutureTask<>(queryGroupBuyActivityDiscountVOThreadTask);
        threadPoolExecutor.execute(groupBuyActivityDiscountVOFutureTask);

        // 异步查询商品信息 - 在实际生产中,商品有同步库或者调用接口查询。这里暂时使用DB方式查询。
        QuerySkuVOFromDBThreadTask querySkuVOFromDBThreadTask = new QuerySkuVOFromDBThreadTask(requestParameter.getGoodsId(), repository);
        FutureTask<SkuVO> skuVOFutureTask = new FutureTask<>(querySkuVOFromDBThreadTask);
        threadPoolExecutor.execute(skuVOFutureTask);

        // 写入上下文 - 对于一些复杂场景,获取数据的操作,有时候会在下N个节点获取,这样前置查询数据,可以提高接口响应效率
        dynamicContext.setGroupBuyActivityDiscountVO(groupBuyActivityDiscountVOFutureTask.get(timeout, TimeUnit.MILLISECONDS));
        dynamicContext.setSkuVO(skuVOFutureTask.get(timeout, TimeUnit.MILLISECONDS));

        log.info("拼团商品查询试算服务-MarketNode userId:{} 异步线程加载数据「GroupBuyActivityDiscountVO、SkuVO」完成", requestParameter.getUserId());
    }

    @Override
    public TrialBalanceEntity doApply(MarketProductEntity requestParameter, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws Exception {
        log.info("拼团商品查询试算服务-MarketNode userId:{} requestParameter:{}", requestParameter.getUserId(), JSON.toJSONString(requestParameter));

        // 获取上下文数据
        GroupBuyActivityDiscountVO groupBuyActivityDiscountVO = dynamicContext.getGroupBuyActivityDiscountVO();
        if (null == groupBuyActivityDiscountVO) {
            return router(requestParameter, dynamicContext);
        }

        GroupBuyActivityDiscountVO.GroupBuyDiscount groupBuyDiscount = groupBuyActivityDiscountVO.getGroupBuyDiscount();
        SkuVO skuVO = dynamicContext.getSkuVO();
        if (null == groupBuyDiscount || null == skuVO) {
            return router(requestParameter, dynamicContext);
        }

        // 优惠试算
        IDiscountCalculateService discountCalculateService = discountCalculateServiceMap.get(groupBuyDiscount.getMarketPlan());
        if (null == discountCalculateService) {
            log.info("不存在{}类型的折扣计算服务,支持类型为:{}", groupBuyDiscount.getMarketPlan(), JSON.toJSONString(discountCalculateServiceMap.keySet()));
            throw new AppException(ResponseCode.E0001.getCode(), ResponseCode.E0001.getInfo());
        }

        // 折扣价格
        BigDecimal payPrice = discountCalculateService.calculate(requestParameter.getUserId(), skuVO.getOriginalPrice(), groupBuyDiscount);
        dynamicContext.setDeductionPrice(skuVO.getOriginalPrice().subtract(payPrice));
        dynamicContext.setPayPrice(payPrice);

        return router(requestParameter, dynamicContext);
    }

试算金额完成之后

查询用户参与的进行中的拼团队伍信息,支持两种查询模式:
个人模式:查询用户自己参与的拼团队伍
随机模式:查询其他用户参与的进行中的拼团队伍(用于推荐用户加入)

  1. 统计拼团数据
    多少个团,多少个成团,多少个锁定用户

4.返回1个个人团,2个组队团数据

拼团营销锁单
1.根据外部订单号和用户id查询 子拼团表 是否存在创建状态订单
2.查询拼团进度,如果已经完成返回拦截,已完成
3.营销优惠试算
4.人群限定过滤
5.锁定,营销预支付订单;商品下单前,预购锁定。
活动状态校验,用户最大参与活动,团队库存限制(使用Redis参与人数>目标数量+失败人数)
6.插入/更新 组团表 新增 订单列表
7.返回结果


@Slf4j
@Service
public class TradeSettlementRuleFilterFactory {

    @Bean("tradeSettlementRuleFilter")
    public BusinessLinkedList<TradeSettlementRuleCommandEntity,
            DynamicContext, TradeSettlementRuleFilterBackEntity> tradeSettlementRuleFilter(
            SCRuleFilter scRuleFilter,
            OutTradeNoRuleFilter outTradeNoRuleFilter,
            SettableRuleFilter settableRuleFilter,
            EndRuleFilter endRuleFilter) {

        // 组装链
        LinkArmory<TradeSettlementRuleCommandEntity, DynamicContext, TradeSettlementRuleFilterBackEntity> linkArmory =
                new LinkArmory<>("交易结算规则过滤链", scRuleFilter, outTradeNoRuleFilter, settableRuleFilter, endRuleFilter);

        // 链对象
        return linkArmory.getLogicLink();
    }

    @Data
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    public static class DynamicContext {
        // 订单营销实体对象
        private MarketPayOrderEntity marketPayOrderEntity;
        // 拼团组队实体对象
        private GroupBuyTeamEntity groupBuyTeamEntity;
    }

}

拼团营销支付
1.经过责任链--->黑名单过滤,外部单号关闭状态校验,订单交易时间,最后封装对象
TradeSettlementRuleFilterFactory.DynamicContext是过程中链式调用的对象,TradeSettlementRuleFilterBackEntity才是最后返回对象

@Slf4j
@Service
public class EndRuleFilter implements ILogicHandler<TradeSettlementRuleCommandEntity, TradeSettlementRuleFilterFactory.DynamicContext, TradeSettlementRuleFilterBackEntity> {

    @Override
    public TradeSettlementRuleFilterBackEntity apply(TradeSettlementRuleCommandEntity requestParameter, TradeSettlementRuleFilterFactory.DynamicContext dynamicContext) throws Exception {
        log.info("结算规则过滤-结束节点{} outTradeNo:{}", requestParameter.getUserId(), requestParameter.getOutTradeNo());

        // 获取上下文对象
        GroupBuyTeamEntity groupBuyTeamEntity = dynamicContext.getGroupBuyTeamEntity();

        // 返回封装数据
        return TradeSettlementRuleFilterBackEntity.builder()
                .teamId(groupBuyTeamEntity.getTeamId())
                .activityId(groupBuyTeamEntity.getActivityId())
                .targetCount(groupBuyTeamEntity.getTargetCount())
                .completeCount(groupBuyTeamEntity.getCompleteCount())
                .lockCount(groupBuyTeamEntity.getLockCount())
                .status(groupBuyTeamEntity.getStatus())
                .validStartTime(groupBuyTeamEntity.getValidStartTime())
                .validEndTime(groupBuyTeamEntity.getValidEndTime())
                .notifyConfigVO(groupBuyTeamEntity.getNotifyConfigVO())
                .build();
    }

}

2.更新拼团子订单状态已完成,更新完成订单数量target是拼团总人数,complete是支付人数,lock是锁单人数

     update group_buy_order
        set complete_count = complete_count + 1, update_time= now()
        where team_id = #{teamId} and complete_count < target_count

3.校验如果我是最后一个拼团就完成的人,更新主子订单为完成状态,创建回调任务发送MQ消息,第三方服务接收拼团成功回调

退单逆向流程,同样适用Filter封装并传递信息

    @Bean("tradeRefundRuleFilter")
    public BusinessLinkedList<TradeRefundCommandEntity, TradeRefundRuleFilterFactory.DynamicContext, TradeRefundBehaviorEntity> tradeRefundRuleFilter(
            DataNodeFilter dataNodeFilter,
            UniqueRefundNodeFilter uniqueRefundNodeFilter,
            RefundOrderNodeFilter refundOrderNodeFilter) {

        // 组装链
        LinkArmory<TradeRefundCommandEntity, TradeRefundRuleFilterFactory.DynamicContext, TradeRefundBehaviorEntity> linkArmory =
                new LinkArmory<>("退单规则过滤链",
                        dataNodeFilter,
                        uniqueRefundNodeFilter,
                        refundOrderNodeFilter);

        // 链对象
        return linkArmory.getLogicLink();
    }

1.DataNodeFilter 获取拼团数据信息
2.UniqueRefundNodeFilter 进行幂等操作,补充幂等实现有下列

1.通过外部唯一订单号,比如**新创建订单**,如果存在则不会创建,可以引申主键唯一标识符,或者唯一标识符,
2.通过状态值绑定,重复调用,查询最新的状态,如果不是更改前状态,比如锁单->关闭.如果不是锁单状态也不会操作,这个可以引申乐观锁,比如时间戳,版本号等等
 时间戳指的是,读取最新更新时间,再次执行的时候where update_time = 上次读取的值 版本号同理
3.高性能,引入redis 多久过期存入订单号,结合1,2使用,提高性能

退单分为,未支付未成团,已支付未成团,已支付已成团
1未支付未成团,主订单需要减少lock数量并改成失败状态,子订单需要改成失败状态,
发送task任务的mq消息,增加recoveryTeamStockKey,注意使用分布式锁,避免重复消费问题

2支付未成团,额外主订单需要改完成数量-1 增加recoveryTeamStockKey
3支付已成团,退款主订单改成[完成已退单],无需操作

   update group_buy_order
        set lock_count = lock_count + #{lockCount},
        complete_count = complete_count + #{completeCount},
        update_time= now()
        where
        team_id = #{teamId} and
        status = 0

发送mq消息,异步使用线程池发送消息,额外封装了task记录调用次数,还有状态
执行器用到了外部订单号+分布式锁,mq外加持久化消息配置

    protected void sendRefundNotifyMessage(NotifyTaskEntity notifyTaskEntity, String refundType) {
        if (null != notifyTaskEntity) {
            threadPoolExecutor.execute(() -> {
                Map<String, Integer> notifyResultMap = null;
                try {
                    notifyResultMap = tradeTaskService.execNotifyJob(notifyTaskEntity);
                    log.info("回调通知交易退单({}) result:{}", refundType, JSON.toJSONString(notifyResultMap));
                } catch (Exception e) {
                    log.error("回调通知交易退单失败({}) result:{}", refundType, JSON.toJSONString(notifyResultMap), e);
                    throw new AppException(e.getMessage());
                }
            });
        }
    }
	
	 private Map<String, Integer> execNotifyJob(List<NotifyTaskEntity> notifyTaskEntityList) throws Exception {
        int successCount = 0, errorCount = 0, retryCount = 0;
        for (NotifyTaskEntity notifyTask : notifyTaskEntityList) {
            // 回调处理 success 成功,error 失败
            String response = port.groupBuyNotify(notifyTask);

            // 更新状态判断&变更数据库表回调任务状态
            if (NotifyTaskHTTPEnumVO.SUCCESS.getCode().equals(response)) {
                int updateCount = repository.updateNotifyTaskStatusSuccess(notifyTask);
                if (1 == updateCount) {
                    successCount += 1;
                }
            } else if (NotifyTaskHTTPEnumVO.ERROR.getCode().equals(response)) {
                if (notifyTask.getNotifyCount() > 4) {
                    int updateCount = repository.updateNotifyTaskStatusError(notifyTask);
                    if (1 == updateCount) {
                        errorCount += 1;
                    }
                } else {
                    int updateCount = repository.updateNotifyTaskStatusRetry(notifyTask);
                    if (1 == updateCount) {
                        retryCount += 1;
                    }
                }
            }
        }

        Map<String, Integer> resultMap = new HashMap<>();
        resultMap.put("waitCount", notifyTaskEntityList.size());
        resultMap.put("successCount", successCount);
        resultMap.put("errorCount", errorCount);
        resultMap.put("retryCount", retryCount);

        return resultMap;
    }
	
	    @Override
    public String groupBuyNotify(NotifyTaskEntity notifyTask) throws Exception {
        RLock lock = redisService.getLock(notifyTask.lockKey());
        try {
            // group-buy-market 拼团服务端会被部署到多台应用服务器上,那么就会有很多任务一起执行。这个时候要进行抢占,避免被多次执行
            if (lock.tryLock(3, 0, TimeUnit.SECONDS)) {
                try {
                    // 回调方式 HTTP
                    if (NotifyTypeEnumVO.HTTP.getCode().equals(notifyTask.getNotifyType())) {
                        // 无效的 notifyUrl 则直接返回成功
                        if (StringUtils.isBlank(notifyTask.getNotifyUrl()) || "暂无".equals(notifyTask.getNotifyUrl())) {
                            return NotifyTaskHTTPEnumVO.SUCCESS.getCode();
                        }
                        return groupBuyNotifyService.groupBuyNotify(notifyTask.getNotifyUrl(), notifyTask.getParameterJson());
                    }

                    // 回调方式 MQ
                    if (NotifyTypeEnumVO.MQ.getCode().equals(notifyTask.getNotifyType())) {
                        publisher.publish(notifyTask.getNotifyMQ(), notifyTask.getParameterJson());
                        return NotifyTaskHTTPEnumVO.SUCCESS.getCode();
                    }
                } finally {
                    if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                        lock.unlock();
                    }
                }
            }
            return NotifyTaskHTTPEnumVO.NULL.getCode();
        } catch (Exception e) {
            Thread.currentThread().interrupt();
            return NotifyTaskHTTPEnumVO.NULL.getCode();
        }
    }

分布式调用注意使用redission锁,锁释放一定要注意是否当前线程持有资源

    @Scheduled(cron = "0 0 0 * * ?")
    public void exec() {
        // 为什么加锁?分布式应用N台机器部署互备(一个应用实例挂了,还有另外可用的),任务调度会有N个同时执行,那么这里需要增加抢占机制,谁抢占到谁就执行。完毕后,下一轮继续抢占。
        RLock lock = redissonClient.getLock("group_buy_market_notify_job_exec");
        try {
            // waitTime:等待获取锁的最长时间
            // leaseTime:租约时间,如果当前线程成功获取到锁,那么锁将被持有的时间长度。这个时间过后,锁会自动释放。续租时间可按照执行方法时间的耗时max来设置。如 50毫秒
            boolean isLocked = lock.tryLock(3, 0, TimeUnit.SECONDS);
            if (!isLocked) return;

            Map<String, Integer> result = tradeTaskService.execNotifyJob();
            log.info("定时任务,回调通知完成 result:{}", JSON.toJSONString(result));
        } catch (Exception e) {
            log.error("定时任务,回调通知完成失败", e);
        } finally {
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

超时订单实现是记录endtime,然后数据库扫描now>end_time,执行退单操作,好处是可以重复执行,也不用全表扫描

 select user_id, team_id, order_id, activity_id, start_time,
           end_time, goods_id, source, channel, original_price, deduction_price, 
           pay_price, status, out_trade_no, out_trade_time, source, channel
    from group_buy_order_list
    where status = 0 
      and out_trade_time is null 
      and now() > end_time
    limit 10
posted @ 2025-11-29 17:18  8023渡劫  阅读(9)  评论(0)    收藏  举报