7.1 责任链校验活动是否有效、用户拼团是否已达上限

This commit is contained in:
zhangsan 2025-07-01 17:40:44 +08:00
parent 16433ffdc2
commit ac00d61503
17 changed files with 359 additions and 19 deletions

View File

@ -24,8 +24,10 @@ public class TrialBalanceEntity {
private String goodsName;
/** 原始价格 */
private BigDecimal originalPrice;
/** 折扣价格 */
/** 折扣金额 */
private BigDecimal deductionPrice;
/** 支付金额 */
private BigDecimal payPrice;
/** 拼团目标数量 */
private Integer targetCount;
/** 拼团开始时间 */

View File

@ -32,8 +32,10 @@ public class DefaultActivityStrategyFactory {
private GroupBuyActivityDiscountVO groupBuyActivityDiscountVO;
// 商品信息
private SkuVO skuVO;
// 折扣价格
// 折扣金额
private BigDecimal deductionPrice;
// 支付金额
private BigDecimal payPrice;
// 活动可见性限制
private boolean visible;
// 活动

View File

@ -27,6 +27,9 @@ public class EndNode extends AbstractGroupBuyMarketSupport<MarketProductEntity,
// 商品信息
SkuVO skuVO = dynamicContext.getSkuVO();
// 支付金额
BigDecimal payPrice = dynamicContext.getPayPrice();
// 折扣价格
BigDecimal deductionPrice = dynamicContext.getDeductionPrice();
// 返回空结果
@ -35,6 +38,7 @@ public class EndNode extends AbstractGroupBuyMarketSupport<MarketProductEntity,
.goodsName(skuVO.getGoodsName())
.originalPrice(skuVO.getOriginalPrice())
.deductionPrice(deductionPrice)
.payPrice(payPrice)
.targetCount(groupBuyActivityDiscountVO.getTarget())
.startTime(groupBuyActivityDiscountVO.getStartTime())
.endTime(groupBuyActivityDiscountVO.getEndTime())

View File

@ -1,6 +1,7 @@
package edu.whut.domain.trade.adapter.repository;
import edu.whut.domain.trade.model.aggregate.GroupBuyOrderAggregate;
import edu.whut.domain.trade.model.entity.GroupBuyActivityEntity;
import edu.whut.domain.trade.model.entity.MarketPayOrderEntity;
import edu.whut.domain.trade.model.valobj.GroupBuyProgressVO;
@ -15,4 +16,8 @@ public interface ITradeRepository {
GroupBuyProgressVO queryGroupBuyProgress(String teamId);
GroupBuyActivityEntity queryGroupBuyActivityEntityByActivityId(Long activityId);
Integer queryOrderCountByActivityId(Long activityId, String userId);
}

View File

@ -0,0 +1,44 @@
package edu.whut.domain.trade.model.entity;
import edu.whut.types.enums.ActivityStatusEnumVO;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* 拼团活动实体对象
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class GroupBuyActivityEntity {
/** 活动ID */
private Long activityId;
/** 活动名称 */
private String activityName;
/** 折扣ID */
private String discountId;
/** 拼团方式0自动成团、1达成目标拼团 */
private Integer groupType;
/** 拼团次数限制 */
private Integer takeLimitCount;
/** 拼团目标 */
private Integer target;
/** 拼团时长(分钟) */
private Integer validTime;
/** 活动状态0创建、1生效、2过期、3废弃 */
private ActivityStatusEnumVO status;
/** 活动开始时间 */
private Date startTime;
/** 活动结束时间 */
private Date endTime;
/** 人群标签规则标识 */
private String tagId;
/** 人群标签规则范围 */
private String tagScope;
}

View File

@ -28,6 +28,8 @@ public class PayDiscountEntity {
private BigDecimal originalPrice;
/** 折扣金额 */
private BigDecimal deductionPrice;
/** 支付金额 */
private BigDecimal payPrice;
/** 外部交易单号-确保外部调用唯一幂等 */
private String outTradeNo;

View File

@ -0,0 +1,22 @@
package edu.whut.domain.trade.model.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 交易规则命令实体 进入链路的请求
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class TradeRuleCommandEntity {
/** 用户ID */
private String userId;
/** 活动ID */
private Long activityId;
}

View File

@ -0,0 +1,20 @@
package edu.whut.domain.trade.model.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 交易规则过滤反馈实体 链路返回
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class TradeRuleFilterBackEntity {
// 用户参与活动的订单量
private Integer userTakeOrderCount;
}

View File

@ -0,0 +1,50 @@
package edu.whut.domain.trade.service.factory;
import edu.whut.domain.trade.model.entity.GroupBuyActivityEntity;
import edu.whut.domain.trade.model.entity.TradeRuleCommandEntity;
import edu.whut.domain.trade.model.entity.TradeRuleFilterBackEntity;
import edu.whut.domain.trade.service.filter.ActivityUsabilityRuleFilter;
import edu.whut.domain.trade.service.filter.UserTakeLimitRuleFilter;
import edu.whut.types.design.framework.link.model2.LinkArmory;
import edu.whut.types.design.framework.link.model2.chain.BusinessLinkedList;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Service;
/**
* 交易规则过滤链工厂把各个规则节点按顺序组装成责任链
*/
@Slf4j
@Service
public class TradeRuleFilterFactory {
/**
* 通过 Spring @Bean 暴露外部只需注入 BusinessLinkedList 即可调用 apply
*/
@Bean("tradeRuleFilter")
public BusinessLinkedList<TradeRuleCommandEntity, DynamicContext, TradeRuleFilterBackEntity> tradeRuleFilter(ActivityUsabilityRuleFilter activityUsabilityRuleFilter, UserTakeLimitRuleFilter userTakeLimitRuleFilter) {
// 1. 组装链
LinkArmory<TradeRuleCommandEntity, DynamicContext, TradeRuleFilterBackEntity> linkArmory =
new LinkArmory<>("交易规则过滤链", activityUsabilityRuleFilter, userTakeLimitRuleFilter);
// 2. 返回链容器即可作为责任链使用
return linkArmory.getLogicLink();
}
/**
* 动态上下文在链路中共享数据避免重复查询
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class DynamicContext {
/** 拼团活动信息,供后续节点复用 */
private GroupBuyActivityEntity groupBuyActivity;
}
}

View File

@ -0,0 +1,55 @@
package edu.whut.domain.trade.service.filter;
import edu.whut.domain.trade.adapter.repository.ITradeRepository;
import edu.whut.domain.trade.model.entity.GroupBuyActivityEntity;
import edu.whut.domain.trade.model.entity.TradeRuleCommandEntity;
import edu.whut.domain.trade.model.entity.TradeRuleFilterBackEntity;
import edu.whut.domain.trade.service.factory.TradeRuleFilterFactory;
import edu.whut.types.design.framework.link.model2.handler.ILogicHandler;
import edu.whut.types.enums.ActivityStatusEnumVO;
import edu.whut.types.enums.ResponseCode;
import edu.whut.types.exception.AppException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Date;
/**
* 活动的可用性规则过滤状态有效期
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ActivityUsabilityRuleFilter implements ILogicHandler<TradeRuleCommandEntity, TradeRuleFilterFactory.DynamicContext, TradeRuleFilterBackEntity> {
private final ITradeRepository repository;
@Override
public TradeRuleFilterBackEntity apply(TradeRuleCommandEntity requestParameter, TradeRuleFilterFactory.DynamicContext dynamicContext) throws Exception {
log.info("交易规则过滤-活动的可用性校验{} activityId:{}", requestParameter.getUserId(), requestParameter.getActivityId());
// 查询拼团活动
GroupBuyActivityEntity groupBuyActivity = repository.queryGroupBuyActivityEntityByActivityId(requestParameter.getActivityId());
// 校验活动状态 - 可以抛业务异常code或者把code写入到动态上下文dynamicContext中最后获取
if (!ActivityStatusEnumVO.EFFECTIVE.equals(groupBuyActivity.getStatus())) {
log.info("活动的可用性校验,非生效状态 activityId:{}", requestParameter.getActivityId());
throw new AppException(ResponseCode.E0101);
}
// 校验活动时间
Date currentTime = new Date();
if (currentTime.before(groupBuyActivity.getStartTime()) || currentTime.after(groupBuyActivity.getEndTime())) {
log.info("活动的可用性校验,非可参与时间范围 activityId:{}", requestParameter.getActivityId());
throw new AppException(ResponseCode.E0102);
}
// 写入动态上下文
dynamicContext.setGroupBuyActivity(groupBuyActivity);
// 走到下一个责任链节点
return null;
}
}

View File

@ -0,0 +1,44 @@
package edu.whut.domain.trade.service.filter;
import edu.whut.domain.trade.adapter.repository.ITradeRepository;
import edu.whut.domain.trade.model.entity.GroupBuyActivityEntity;
import edu.whut.domain.trade.model.entity.TradeRuleCommandEntity;
import edu.whut.domain.trade.model.entity.TradeRuleFilterBackEntity;
import edu.whut.domain.trade.service.factory.TradeRuleFilterFactory;
import edu.whut.types.design.framework.link.model2.handler.ILogicHandler;
import edu.whut.types.enums.ResponseCode;
import edu.whut.types.exception.AppException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* 用户参与限制规则过滤
*/
@Slf4j
@Service
public class UserTakeLimitRuleFilter implements ILogicHandler<TradeRuleCommandEntity, TradeRuleFilterFactory.DynamicContext, TradeRuleFilterBackEntity> {
@Resource
private ITradeRepository repository;
@Override
public TradeRuleFilterBackEntity apply(TradeRuleCommandEntity requestParameter, TradeRuleFilterFactory.DynamicContext dynamicContext) throws Exception {
log.info("交易规则过滤-用户参与次数校验{} activityId:{}", requestParameter.getUserId(), requestParameter.getActivityId());
GroupBuyActivityEntity groupBuyActivity = dynamicContext.getGroupBuyActivity();
// 查询用户在一个拼团活动上参与的次数
Integer count = repository.queryOrderCountByActivityId(requestParameter.getActivityId(), requestParameter.getUserId());
if (null != groupBuyActivity.getTakeLimitCount() && count >= groupBuyActivity.getTakeLimitCount()) {
log.info("用户参与次数校验,已达可参与上限 activityId:{}", requestParameter.getActivityId());
throw new AppException(ResponseCode.E0103);
}
return TradeRuleFilterBackEntity.builder()
.userTakeOrderCount(count)
.build();
}
}

View File

@ -2,16 +2,17 @@ package edu.whut.infrastructure.adapter.repository;
import edu.whut.domain.trade.adapter.repository.ITradeRepository;
import edu.whut.domain.trade.model.aggregate.GroupBuyOrderAggregate;
import edu.whut.domain.trade.model.entity.MarketPayOrderEntity;
import edu.whut.domain.trade.model.entity.PayActivityEntity;
import edu.whut.domain.trade.model.entity.PayDiscountEntity;
import edu.whut.domain.trade.model.entity.UserEntity;
import edu.whut.domain.trade.model.entity.*;
import edu.whut.domain.trade.model.valobj.GroupBuyProgressVO;
import edu.whut.domain.trade.model.valobj.TradeOrderStatusEnumVO;
import edu.whut.infrastructure.dao.IGroupBuyActivityDao;
import edu.whut.infrastructure.dao.IGroupBuyOrderDao;
import edu.whut.infrastructure.dao.IGroupBuyOrderListDao;
import edu.whut.infrastructure.dao.po.GroupBuyActivity;
import edu.whut.infrastructure.dao.po.GroupBuyOrder;
import edu.whut.infrastructure.dao.po.GroupBuyOrderList;
import edu.whut.types.common.Constants;
import edu.whut.types.enums.ActivityStatusEnumVO;
import edu.whut.types.enums.ResponseCode;
import edu.whut.types.exception.AppException;
import lombok.RequiredArgsConstructor;
@ -22,7 +23,6 @@ import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
/**
* 仓储实现负责把领域对象 <-> 数据库 PO 的转换与持久化
@ -37,6 +37,8 @@ public class TradeRepository implements ITradeRepository {
private final IGroupBuyOrderListDao groupBuyOrderListDao;
private final IGroupBuyActivityDao groupBuyActivityDao;
/**
* 根据外部交易号 & 用户id 查询未支付的锁单记录用于幂等
*/
@ -74,13 +76,17 @@ public class TradeRepository implements ITradeRepository {
UserEntity user = agg.getUserEntity();
PayActivityEntity activity = agg.getPayActivityEntity();
PayDiscountEntity discount = agg.getPayDiscountEntity();
Integer userTakeOrderCount = agg.getUserTakeOrderCount();
/* ---------- 1. 处理 group_buy_order团单主表 ---------- */
String teamId = activity.getTeamId();
//自己发起拼团
if (StringUtils.isBlank(teamId)) {
// 新建团队随机 8 位数字作 teamId示例中用 RandomStringUtils线上可换雪花算法等
teamId = RandomStringUtils.randomNumeric(8);
// 构建拼团订单
GroupBuyOrder orderPo = GroupBuyOrder.builder()
.teamId(teamId)
.activityId(activity.getActivityId())
@ -88,7 +94,7 @@ public class TradeRepository implements ITradeRepository {
.channel(discount.getChannel())
.originalPrice(discount.getOriginalPrice())
.deductionPrice(discount.getDeductionPrice())
.payPrice(discount.getDeductionPrice()) // 直减后应付价格
.payPrice(discount.getOriginalPrice().subtract(discount.getDeductionPrice()))
.targetCount(activity.getTargetCount())
.completeCount(0)
.lockCount(1) // 首单已锁定
@ -121,6 +127,8 @@ public class TradeRepository implements ITradeRepository {
.deductionPrice(discount.getDeductionPrice())
.status(TradeOrderStatusEnumVO.CREATE.getCode()) // 0 = 初始锁定
.outTradeNo(discount.getOutTradeNo())
// 构建 bizId 唯一值活动id_用户id_参与次数累加
.bizId(activity.getActivityId() + Constants.UNDERLINE + user.getUserId() + Constants.UNDERLINE + (userTakeOrderCount + 1))
.build();
try {
@ -153,4 +161,31 @@ public class TradeRepository implements ITradeRepository {
.lockCount(po.getLockCount())
.build();
}
@Override
public GroupBuyActivityEntity queryGroupBuyActivityEntityByActivityId(Long activityId) {
GroupBuyActivity groupBuyActivity = groupBuyActivityDao.queryGroupBuyActivityByActivityId(activityId);
return GroupBuyActivityEntity.builder()
.activityId(groupBuyActivity.getActivityId())
.activityName(groupBuyActivity.getActivityName())
.discountId(groupBuyActivity.getDiscountId())
.groupType(groupBuyActivity.getGroupType())
.takeLimitCount(groupBuyActivity.getTakeLimitCount())
.target(groupBuyActivity.getTarget())
.validTime(groupBuyActivity.getValidTime())
.status(ActivityStatusEnumVO.valueOf(groupBuyActivity.getStatus()))
.startTime(groupBuyActivity.getStartTime())
.endTime(groupBuyActivity.getEndTime())
.tagId(groupBuyActivity.getTagId())
.tagScope(groupBuyActivity.getTagScope())
.build();
}
@Override
public Integer queryOrderCountByActivityId(Long activityId, String userId) {
GroupBuyOrderList groupBuyOrderListReq = new GroupBuyOrderList();
groupBuyOrderListReq.setActivityId(activityId);
groupBuyOrderListReq.setUserId(userId);
return groupBuyOrderListDao.queryOrderCountByActivityId(groupBuyOrderListReq);
}
}

View File

@ -3,5 +3,6 @@ package edu.whut.types.common;
public class Constants {
public final static String SPLIT = ",";
public final static String UNDERLINE = "_";
}

View File

@ -4,12 +4,16 @@ import edu.whut.types.design.framework.link.model2.chain.BusinessLinkedList;
import edu.whut.types.design.framework.link.model2.handler.ILogicHandler;
/**
* 链路装配
* 链路装配工厂 负责把一组 ILogicHandler 顺序注册到 BusinessLinkedList
*/
public class LinkArmory<T, D, R> {
private final BusinessLinkedList<T, D, R> logicLink;
/**
* @param linkName 链路名称便于日志排查
* @param logicHandlers 节点列表按传入顺序链接
*/
@SafeVarargs
public LinkArmory(String linkName, ILogicHandler<T, D, R>... logicHandlers) {
logicLink = new BusinessLinkedList<>(linkName);
@ -18,6 +22,7 @@ public class LinkArmory<T, D, R> {
}
}
/** 返回组装完成的链路 */
public BusinessLinkedList<T, D, R> getLogicLink() {
return logicLink;
}

View File

@ -4,7 +4,11 @@ package edu.whut.types.design.framework.link.model2.chain;
import edu.whut.types.design.framework.link.model2.handler.ILogicHandler;
/**
* 业务链路
* 业务链路容器 双向链表实现同时实现 ILogicHandler从而可以被当作单个节点使用
* 责任链特点
* 链表插入删除复杂度 O(1)运行期可动态调整节点
* 循环遍历不使用递归避免深链路导致的栈溢出风险
* 当某个节点返回非 null 时立即短路结束
*/
public class BusinessLinkedList<T, D, R> extends LinkedList<ILogicHandler<T, D, R>> implements ILogicHandler<T, D, R>{
@ -12,18 +16,25 @@ public class BusinessLinkedList<T, D, R> extends LinkedList<ILogicHandler<T, D,
super(name);
}
/**
* BusinessLinkedList是头节点它的apply方法就是循环调用后面的节点直至返回
* 遍历并执行链路
*/
@Override
public R apply(T requestParameter, D dynamicContext) throws Exception {
Node<ILogicHandler<T, D, R>> current = this.first;
do {
ILogicHandler<T, D, R> item = current.item;
R apply = item.apply(requestParameter, dynamicContext);
if (null != apply) return apply;
// 顺序执行直到链尾或返回结果
while (current != null) {
ILogicHandler<T, D, R> handler = current.item;
R result = handler.apply(requestParameter, dynamicContext);
if (result != null) {
// 节点命中立即返回
return result;
}
//result==null则交给那一节点继续处理
current = current.next;
} while (null != current);
}
// 全链未命中
return null;
}
}

View File

@ -0,0 +1,38 @@
package edu.whut.types.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* 拼团活动状态枚举
*/
@Getter
@AllArgsConstructor
@NoArgsConstructor
public enum ActivityStatusEnumVO {
CREATE(0, "创建"),
EFFECTIVE(1, "生效"),
OVERDUE(2, "过期"),
ABANDONED(3, "废弃"),
;
private Integer code;
private String info;
public static ActivityStatusEnumVO valueOf(Integer code) {
switch (code) {
case 0:
return CREATE;
case 1:
return EFFECTIVE;
case 2:
return OVERDUE;
case 3:
return ABANDONED;
}
throw new RuntimeException("err code not exist!");
}
}

View File

@ -43,7 +43,7 @@ public class AppException extends RuntimeException {
@Override
public String toString() {
return "edu.whut.x.api.types.exception.XApiException{" +
return "edu.whut.types.exception.AppException{" +
"code='" + code + '\'' +
", info='" + info + '\'' +
'}';