6.28 人群标签节点过滤,活动是否对xx用户可见、可参与

This commit is contained in:
zhangsan 2025-06-28 11:33:26 +08:00
parent b09e3bae76
commit 8ff266ff41
17 changed files with 179 additions and 18 deletions

View File

@ -95,7 +95,7 @@ CREATE TABLE `group_buy_activity` (
-- ----------------------------
-- Records of group_buy_activity
-- ----------------------------
INSERT INTO `group_buy_activity` VALUES (1, 100123, '测试活动', '25120207', 0, 1, 1, 15, 1, '2025-06-19 10:19:40', '2025-06-19 10:19:40', '1', '1', '2025-06-19 10:19:40', '2025-06-26 15:27:48');
INSERT INTO `group_buy_activity` VALUES (1, 100123, '测试活动', '25120207', 0, 1, 1, 15, 1, '2025-06-19 10:19:40', '2025-06-19 10:19:40', 'RQ_KJHKL98UU78H66554GFDV', '1', '2025-06-19 10:19:40', '2025-06-26 15:27:48');
-- ----------------------------
-- Table structure for group_buy_discount

View File

@ -23,6 +23,9 @@ public class IIndexGroupBuyMarketServiceTest {
@Resource
private IIndexGroupBuyMarketService indexGroupBuyMarketService;
/**
* 测试人群标签功能的时候可以进入 ITagServiceTest#test_tag_job 执行人群写入
*/
@Test
public void test_indexMarketTrial() throws Exception {
MarketProductEntity marketProductEntity = new MarketProductEntity();
@ -36,6 +39,19 @@ public class IIndexGroupBuyMarketServiceTest {
log.info("返回结果:{}", JSON.toJSONString(trialBalanceEntity));
}
@Test
public void test_indexMarketTrial_no_tag() throws Exception {
MarketProductEntity marketProductEntity = new MarketProductEntity();
marketProductEntity.setUserId("user01");
marketProductEntity.setSource("s01");
marketProductEntity.setChannel("c01");
marketProductEntity.setGoodsId("9890001");
TrialBalanceEntity trialBalanceEntity = indexGroupBuyMarketService.indexMarketTrial(marketProductEntity);
log.info("请求参数:{}", JSON.toJSONString(marketProductEntity));
log.info("返回结果:{}", JSON.toJSONString(trialBalanceEntity));
}
@Test
public void test_indexMarketTrial_error() throws Exception {
MarketProductEntity marketProductEntity = new MarketProductEntity();

View File

@ -36,4 +36,10 @@ public class ITagServiceTest {
log.info("gudebai 不存在,预期结果为 false测试结果:{}", bitSet.get(redisService.getIndexFromUserId("gudebai")));
}
@Test
public void test_null_tag_bitmap() {
RBitSet bitSet = redisService.getBitSet("null");
log.info("测试结果:{}", bitSet.isExists());
}
}

View File

@ -14,4 +14,6 @@ public interface IActivityRepository {
SCSkuActivityVO querySCSkuActivityBySCGoodsId(String source, String channel, String goodsId);
boolean isTagCrowdRange(String tagId, String userId);
}

View File

@ -1,11 +1,14 @@
package edu.whut.domain.activity.model.valobj;
import edu.whut.types.common.Constants;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import java.util.Date;
import java.util.Objects;
/**
* 拼团活动营销配置值对象
@ -73,10 +76,36 @@ public class GroupBuyActivityDiscountVO {
*/
private String tagId;
/**
* 人群标签规则范围
* 人群标签规则范围如果有值说明有限制
*/
private String tagScope;
/**
* 可见限制可以看见不等于能参加拼团
* 只要存在这样一个值那么首次获得的默认值就是 falsefalse代表有限制
*/
public boolean isVisible() {
if(StringUtils.isBlank(this.tagScope)) return TagScopeEnumVO.VISIBLE.getAllow(); //等价于return true,放行
String[] split = this.tagScope.split(Constants.SPLIT);
if (split.length > 0 && Objects.equals(split[0], "1") && StringUtils.isNotBlank(split[0])) {
return TagScopeEnumVO.VISIBLE.getRefuse(); //等价于return false待后续校验
}
return TagScopeEnumVO.VISIBLE.getAllow();
}
/**
* 参与限制
* 只要存在这样一个值那么首次获得的默认值就是 false
*/
public boolean isEnable() {
if(StringUtils.isBlank(this.tagScope)) return TagScopeEnumVO.VISIBLE.getAllow();
String[] split = this.tagScope.split(Constants.SPLIT);
if (split.length == 2 && Objects.equals(split[1], "2") && StringUtils.isNotBlank(split[1])) {
return TagScopeEnumVO.ENABLE.getRefuse();
}
return TagScopeEnumVO.ENABLE.getAllow();
}
@Getter
@Builder
@AllArgsConstructor

View File

@ -0,0 +1,23 @@
package edu.whut.domain.activity.model.valobj;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* 活动人群标签作用域范围枚举
*/
@Getter
@AllArgsConstructor
@NoArgsConstructor
public enum TagScopeEnumVO {
VISIBLE(true,false,"是否可看见拼团"),
ENABLE(true, false,"是否可参与拼团"),
;
private Boolean allow;
private Boolean refuse;
private String desc;
}

View File

@ -17,7 +17,7 @@ public class MJCalculateService extends AbstractDiscountCalculateService {
@Override
public BigDecimal doCalculate(BigDecimal originalPrice, GroupBuyActivityDiscountVO.GroupBuyDiscount groupBuyDiscount) {
log.info("优惠策略折扣计算:{}", groupBuyDiscount.getDiscountType().getCode());
log.info("满减优惠策略折扣计算规则0代表人人可参与1代表指定标签人群参与:{}", groupBuyDiscount.getDiscountType().getCode());
// 获取折扣表达式 - 100,10 满100减10元
String marketExpr = groupBuyDiscount.getMarketExpr();

View File

@ -15,7 +15,7 @@ public class NCalculateService extends AbstractDiscountCalculateService {
@Override
public BigDecimal doCalculate(BigDecimal originalPrice, GroupBuyActivityDiscountVO.GroupBuyDiscount groupBuyDiscount) {
log.info("优惠策略折扣计算:{}", groupBuyDiscount.getDiscountType().getCode());
log.info("N元购优惠策略折扣计算规则0代表人人可参与1代表指定标签人群参与:{}", groupBuyDiscount.getDiscountType().getCode());
// 折扣表达式 - 直接为优惠后的金额
String marketExpr = groupBuyDiscount.getMarketExpr();

View File

@ -15,12 +15,12 @@ public class ZJCalculateService extends AbstractDiscountCalculateService {
@Override
public BigDecimal doCalculate(BigDecimal originalPrice, GroupBuyActivityDiscountVO.GroupBuyDiscount groupBuyDiscount) {
log.info("优惠策略折扣计算:{}", groupBuyDiscount.getDiscountType().getCode());
log.info("直减优惠策略折扣计算规则0代表人人可参与1代表指定标签人群参与:{}", groupBuyDiscount.getDiscountType().getCode());
// 折扣表达式 - 直减为扣减金额
String marketExpr = groupBuyDiscount.getMarketExpr();
// 折扣价格
// 折扣后的实际支付金额
BigDecimal deductionPrice = originalPrice.subtract(new BigDecimal(marketExpr));
// 判断折扣后金额最低支付1分钱

View File

@ -15,7 +15,7 @@ public class ZKCalculateService extends AbstractDiscountCalculateService {
@Override
public BigDecimal doCalculate(BigDecimal originalPrice, GroupBuyActivityDiscountVO.GroupBuyDiscount groupBuyDiscount) {
log.info("优惠策略折扣计算:{}", groupBuyDiscount.getDiscountType().getCode());
log.info("折扣优惠策略折扣计算规则0代表人人可参与1代表指定标签人群参与:{}", groupBuyDiscount.getDiscountType().getCode());
// 折扣表达式 - 折扣百分比
String marketExpr = groupBuyDiscount.getMarketExpr();

View File

@ -34,6 +34,10 @@ public class DefaultActivityStrategyFactory {
private SkuVO skuVO;
// 折扣价格
private BigDecimal deductionPrice;
// 活动可见性限制
private boolean visible;
// 活动
private boolean enable;
}
}

View File

@ -7,8 +7,6 @@ import edu.whut.domain.activity.model.valobj.SkuVO;
import edu.whut.domain.activity.service.trial.AbstractGroupBuyMarketSupport;
import edu.whut.domain.activity.service.trial.factory.DefaultActivityStrategyFactory;
import edu.whut.types.design.framework.tree.StrategyHandler;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@ -24,8 +22,9 @@ public class EndNode extends AbstractGroupBuyMarketSupport<MarketProductEntity,
@Override
public TrialBalanceEntity doApply(MarketProductEntity requestParameter, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws Exception {
log.info("拼团商品查询试算服务-EndNode userId:{} requestParameter:{}", requestParameter.getUserId(), JSON.toJSONString(requestParameter));
// 拼团活动配置
GroupBuyActivityDiscountVO groupBuyActivityDiscountVO = dynamicContext.getGroupBuyActivityDiscountVO();
// 商品信息
SkuVO skuVO = dynamicContext.getSkuVO();
// 折扣价格
@ -35,12 +34,12 @@ public class EndNode extends AbstractGroupBuyMarketSupport<MarketProductEntity,
.goodsId(skuVO.getGoodsId())
.goodsName(skuVO.getGoodsName())
.originalPrice(skuVO.getOriginalPrice())
.deductionPrice(new BigDecimal("0.00"))
.deductionPrice(deductionPrice)
.targetCount(groupBuyActivityDiscountVO.getTarget())
.startTime(groupBuyActivityDiscountVO.getStartTime())
.endTime(groupBuyActivityDiscountVO.getEndTime())
.isVisible(false)
.isEnable(false)
.isVisible(dynamicContext.isVisible())
.isEnable(dynamicContext.isEnable())
.build();
}

View File

@ -37,6 +37,8 @@ public class MarketNode extends AbstractGroupBuyMarketSupport<MarketProductEntit
private final Map<String, IDiscountCalculateService> discountCalculateServiceMap;
private final TagNode tagNode;
/**
* 异步加载数据
* @param requestParameter
@ -57,8 +59,10 @@ public class MarketNode extends AbstractGroupBuyMarketSupport<MarketProductEntit
FutureTask<SkuVO> skuVOFutureTask = new FutureTask<>(querySkuVOFromDBThreadTask);
threadPoolExecutor.execute(skuVOFutureTask);
// 写入上下文 - 对于一些复杂场景获取数据的操作有时候会在下N个节点获取这样前置查询数据可以提高接口响应效率
// 写入上下文- 对于一些复杂场景获取数据的操作有时候会在下N个节点获取这样前置查询数据可以提高接口响应效率
// 写入活动配置信息
dynamicContext.setGroupBuyActivityDiscountVO(groupBuyActivityDiscountVOFutureTask.get(timeout, TimeUnit.MINUTES));
// 写入商品信息
dynamicContext.setSkuVO(skuVOFutureTask.get(timeout, TimeUnit.MINUTES));
log.info("拼团商品查询试算服务-MarketNode userId:{} 异步线程加载数据「GroupBuyActivityDiscountVO、SkuVO」完成", requestParameter.getUserId());
@ -75,13 +79,14 @@ public class MarketNode extends AbstractGroupBuyMarketSupport<MarketProductEntit
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);
@ -96,6 +101,7 @@ public class MarketNode extends AbstractGroupBuyMarketSupport<MarketProductEntit
// 折扣价格
BigDecimal deductionPrice = discountCalculateService.calculate(requestParameter.getUserId(), skuVO.getOriginalPrice(), groupBuyDiscount);
//设置折扣价格到上下文中
dynamicContext.setDeductionPrice(deductionPrice);
return router(requestParameter, dynamicContext);
@ -114,7 +120,7 @@ public class MarketNode extends AbstractGroupBuyMarketSupport<MarketProductEntit
if (null == dynamicContext.getGroupBuyActivityDiscountVO() || null == dynamicContext.getSkuVO() || null == dynamicContext.getDeductionPrice()) {
return errorNode;
}
return endNode;
return tagNode;
}
}

View File

@ -0,0 +1,52 @@
package edu.whut.domain.activity.service.trial.node;
import edu.whut.domain.activity.model.entity.MarketProductEntity;
import edu.whut.domain.activity.model.entity.TrialBalanceEntity;
import edu.whut.domain.activity.model.valobj.GroupBuyActivityDiscountVO;
import edu.whut.domain.activity.service.trial.AbstractGroupBuyMarketSupport;
import edu.whut.domain.activity.service.trial.factory.DefaultActivityStrategyFactory;
import edu.whut.types.design.framework.tree.StrategyHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* 人群标签判断
*/
@Slf4j
@Service
public class TagNode extends AbstractGroupBuyMarketSupport<MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, TrialBalanceEntity> {
@Resource
private EndNode endNode;
@Override
protected TrialBalanceEntity doApply(MarketProductEntity requestParameter, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws Exception {
// 获取拼团活动配置
GroupBuyActivityDiscountVO groupBuyActivityDiscountVO = dynamicContext.getGroupBuyActivityDiscountVO();
String tagId = groupBuyActivityDiscountVO.getTagId();
boolean visible = groupBuyActivityDiscountVO.isVisible();
boolean enable = groupBuyActivityDiscountVO.isEnable();
// 人群标签配置为空说明该活动不限定人群参与
if (StringUtils.isBlank(tagId)) {
dynamicContext.setVisible(true);
dynamicContext.setEnable(true);
return router(requestParameter, dynamicContext);
}
// 是否在人群范围内visibleenable 如果值为 ture 则表示没有配置拼团限制那么就直接保证为 true 即可
boolean isWithin = repository.isTagCrowdRange(tagId, requestParameter.getUserId());
dynamicContext.setVisible(visible || isWithin);
dynamicContext.setEnable(enable || isWithin);
return router(requestParameter, dynamicContext);
}
@Override
public StrategyHandler<MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, TrialBalanceEntity> get(MarketProductEntity requestParameter, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws Exception {
return endNode;
}
}

View File

@ -13,7 +13,10 @@ import edu.whut.infrastructure.dao.po.GroupBuyActivity;
import edu.whut.infrastructure.dao.po.GroupBuyDiscount;
import edu.whut.infrastructure.dao.po.SCSkuActivity;
import edu.whut.infrastructure.dao.po.Sku;
import edu.whut.infrastructure.redis.IRedisService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBitSet;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
@ -23,6 +26,7 @@ import javax.annotation.Resource;
*/
@Repository
@RequiredArgsConstructor
@Slf4j
public class ActivityRepository implements IActivityRepository {
private final IGroupBuyActivityDao groupBuyActivityDao;
@ -33,6 +37,8 @@ public class ActivityRepository implements IActivityRepository {
private final ISCSkuActivityDao skuActivityDao;
private final IRedisService redisService;
@Override
public GroupBuyActivityDiscountVO queryGroupBuyActivityDiscountVO(Long activityId) {
GroupBuyActivity groupBuyActivityRes = groupBuyActivityDao.queryValidGroupBuyActivityId(activityId);
@ -97,4 +103,15 @@ public class ActivityRepository implements IActivityRepository {
.build();
}
@Override
public boolean isTagCrowdRange(String tagId, String userId) {
//根据标签id获取对应位图
RBitSet bitSet = redisService.getBitSet(tagId);
if (!bitSet.isExists()) {
return true;
}
// 判断用户是否存在人群中
return bitSet.get(redisService.getIndexFromUserId(userId));
}
}

View File

@ -10,6 +10,7 @@ import edu.whut.infrastructure.dao.po.CrowdTagsDetail;
import edu.whut.infrastructure.dao.po.CrowdTagsJob;
import edu.whut.infrastructure.redis.IRedisService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBitSet;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Repository;
@ -21,6 +22,7 @@ import javax.annotation.Resource;
*/
@Repository
@RequiredArgsConstructor
@Slf4j
public class TagRepository implements ITagRepository {
private final ICrowdTagsDao crowdTagsDao;
@ -73,11 +75,11 @@ public class TagRepository implements ITagRepository {
try {
crowdTagsDetailDao.addCrowdTagsUserId(crowdTagsDetailReq);
// 获取BitSet
RBitSet bitSet = redisService.getBitSet(tagId);
bitSet.set(redisService.getIndexFromUserId(userId), true);
bitSet.set(redisService.getIndexFromUserId(userId));
} catch (DuplicateKeyException ignore) {
log.info("用户id{}已在人群标签{}中",userId,tagId);
// 忽略唯一索引冲突
}
}

View File

@ -283,6 +283,11 @@ public interface IRedisService {
//位图
RBitSet getBitSet(String key);
/**
* 将userId 映射到一个哈希值指定需存入位图的位置
* @param userId
* @return
*/
default int getIndexFromUserId(String userId) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");