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);
}
试算金额完成之后
查询用户参与的进行中的拼团队伍信息,支持两种查询模式:
个人模式:查询用户自己参与的拼团队伍
随机模式:查询其他用户参与的进行中的拼团队伍(用于推荐用户加入)
- 统计拼团数据
多少个团,多少个成团,多少个锁定用户
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

浙公网安备 33010602011771号