|
@@ -18,6 +18,7 @@ import com.xuekairui.activity.mapper.ActivityLotteryPrizeMapper;
|
|
|
import com.xuekairui.activity.mapper.ActivityLotteryRecordMapper;
|
|
import com.xuekairui.activity.mapper.ActivityLotteryRecordMapper;
|
|
|
import com.xuekairui.common.BusinessException;
|
|
import com.xuekairui.common.BusinessException;
|
|
|
import com.xuekairui.common.ErrorCode;
|
|
import com.xuekairui.common.ErrorCode;
|
|
|
|
|
+import com.xuekairui.common.activity.ActivityLotteryGate;
|
|
|
import com.xuekairui.common.event.ActivityEventRedisService;
|
|
import com.xuekairui.common.event.ActivityEventRedisService;
|
|
|
import com.xuekairui.user.entity.BusinessLicense;
|
|
import com.xuekairui.user.entity.BusinessLicense;
|
|
|
import com.xuekairui.user.entity.OperationAuditLog;
|
|
import com.xuekairui.user.entity.OperationAuditLog;
|
|
@@ -25,8 +26,8 @@ import com.xuekairui.user.entity.User;
|
|
|
import com.xuekairui.user.enums.AuditResult;
|
|
import com.xuekairui.user.enums.AuditResult;
|
|
|
import com.xuekairui.user.enums.LicenseStatus;
|
|
import com.xuekairui.user.enums.LicenseStatus;
|
|
|
import com.xuekairui.user.enums.MembershipLevel;
|
|
import com.xuekairui.user.enums.MembershipLevel;
|
|
|
-import com.xuekairui.user.enums.OperatorRole;
|
|
|
|
|
import com.xuekairui.user.enums.OperationType;
|
|
import com.xuekairui.user.enums.OperationType;
|
|
|
|
|
+import com.xuekairui.user.enums.OperatorRole;
|
|
|
import com.xuekairui.user.mapper.BusinessLicenseMapper;
|
|
import com.xuekairui.user.mapper.BusinessLicenseMapper;
|
|
|
import com.xuekairui.user.mapper.UserMapper;
|
|
import com.xuekairui.user.mapper.UserMapper;
|
|
|
import com.xuekairui.user.service.MembershipService;
|
|
import com.xuekairui.user.service.MembershipService;
|
|
@@ -57,13 +58,14 @@ import java.util.concurrent.ThreadLocalRandom;
|
|
|
@Slf4j
|
|
@Slf4j
|
|
|
@Service
|
|
@Service
|
|
|
@RequiredArgsConstructor
|
|
@RequiredArgsConstructor
|
|
|
-public class ActivityLotteryService {
|
|
|
|
|
|
|
+public class ActivityLotteryService implements ActivityLotteryGate {
|
|
|
|
|
|
|
|
private static final Long CONFIG_ID = 1L;
|
|
private static final Long CONFIG_ID = 1L;
|
|
|
private static final String SOURCE_COMPARE = "COMPARE";
|
|
private static final String SOURCE_COMPARE = "COMPARE";
|
|
|
private static final String REWARD_TYPE_MEMBERSHIP = "MEMBERSHIP";
|
|
private static final String REWARD_TYPE_MEMBERSHIP = "MEMBERSHIP";
|
|
|
private static final String REWARD_TYPE_EMPTY = "EMPTY";
|
|
private static final String REWARD_TYPE_EMPTY = "EMPTY";
|
|
|
private static final String REWARD_TYPE_PHYSICAL = "PHYSICAL";
|
|
private static final String REWARD_TYPE_PHYSICAL = "PHYSICAL";
|
|
|
|
|
+ private static final String REWARD_TYPE_OTHER = "OTHER";
|
|
|
|
|
|
|
|
private final ActivityLotteryConfigMapper configMapper;
|
|
private final ActivityLotteryConfigMapper configMapper;
|
|
|
private final ActivityLotteryChanceMapper chanceMapper;
|
|
private final ActivityLotteryChanceMapper chanceMapper;
|
|
@@ -117,6 +119,7 @@ public class ActivityLotteryService {
|
|
|
// 事件处理:比价成功后发放抽奖机会
|
|
// 事件处理:比价成功后发放抽奖机会
|
|
|
// ==========================================
|
|
// ==========================================
|
|
|
|
|
|
|
|
|
|
+ @Override
|
|
|
@Transactional
|
|
@Transactional
|
|
|
public void onCrawlerConsumed(Long userId, String usageDateStr) {
|
|
public void onCrawlerConsumed(Long userId, String usageDateStr) {
|
|
|
ActivityLotteryConfig config = getConfig();
|
|
ActivityLotteryConfig config = getConfig();
|
|
@@ -128,6 +131,18 @@ public class ActivityLotteryService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
LocalDate usageDate = usageDateStr != null ? LocalDate.parse(usageDateStr) : LocalDate.now();
|
|
LocalDate usageDate = usageDateStr != null ? LocalDate.parse(usageDateStr) : LocalDate.now();
|
|
|
|
|
+ int maxChance = config.getDailyMaxChance() != null ? config.getDailyMaxChance() : 1;
|
|
|
|
|
+
|
|
|
|
|
+ // 检查当日已发放的机会数量是否已达上限
|
|
|
|
|
+ Long todayChanceCount = chanceMapper.selectCount(
|
|
|
|
|
+ new LambdaQueryWrapper<ActivityLotteryChance>()
|
|
|
|
|
+ .eq(ActivityLotteryChance::getUserId, userId)
|
|
|
|
|
+ .eq(ActivityLotteryChance::getChanceDate, usageDate)
|
|
|
|
|
+ .eq(ActivityLotteryChance::getSource, SOURCE_COMPARE));
|
|
|
|
|
+ if (todayChanceCount != null && todayChanceCount >= maxChance) {
|
|
|
|
|
+ log.info("今日抽奖机会已达上限({}/{}): userId={}, date={}", todayChanceCount, maxChance, userId, usageDate);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
ActivityLotteryChance chance = ActivityLotteryChance.builder()
|
|
ActivityLotteryChance chance = ActivityLotteryChance.builder()
|
|
|
.userId(userId)
|
|
.userId(userId)
|
|
@@ -137,9 +152,10 @@ public class ActivityLotteryService {
|
|
|
.build();
|
|
.build();
|
|
|
try {
|
|
try {
|
|
|
chanceMapper.insert(chance);
|
|
chanceMapper.insert(chance);
|
|
|
- log.info("比价抽奖机会已发放: userId={}, date={}", userId, usageDate);
|
|
|
|
|
|
|
+ log.info("比价抽奖机会已发放: userId={}, date={}, 当日累计={}/{}", userId, usageDate, todayChanceCount != null ? todayChanceCount + 1 : 1, maxChance);
|
|
|
} catch (DuplicateKeyException e) {
|
|
} catch (DuplicateKeyException e) {
|
|
|
- // 今日已发放过机会,忽略
|
|
|
|
|
|
|
+ // 并发插入同一条记录,忽略
|
|
|
|
|
+ log.debug("并发插入抽奖机会,忽略: userId={}, date={}", userId, usageDate);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -164,8 +180,9 @@ public class ActivityLotteryService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
LocalDate today = LocalDate.now();
|
|
LocalDate today = LocalDate.now();
|
|
|
|
|
+ int maxChance = config.getDailyMaxChance() != null ? config.getDailyMaxChance() : 1;
|
|
|
|
|
|
|
|
- // 检查今日是否已抽过(used=1 的机会记录存在即表示已抽奖)
|
|
|
|
|
|
|
+ // 检查今日已抽奖次数是否已达上限
|
|
|
Long usedCount = chanceMapper.selectCount(
|
|
Long usedCount = chanceMapper.selectCount(
|
|
|
new LambdaQueryWrapper<ActivityLotteryChance>()
|
|
new LambdaQueryWrapper<ActivityLotteryChance>()
|
|
|
.eq(ActivityLotteryChance::getUserId, userId)
|
|
.eq(ActivityLotteryChance::getUserId, userId)
|
|
@@ -173,11 +190,12 @@ public class ActivityLotteryService {
|
|
|
.eq(ActivityLotteryChance::getSource, SOURCE_COMPARE)
|
|
.eq(ActivityLotteryChance::getSource, SOURCE_COMPARE)
|
|
|
.eq(ActivityLotteryChance::getUsed, 1));
|
|
.eq(ActivityLotteryChance::getUsed, 1));
|
|
|
|
|
|
|
|
- if (usedCount != null && usedCount > 0) {
|
|
|
|
|
- // 今日已抽过,记录违规审计日志并返回提示
|
|
|
|
|
|
|
+ int used = usedCount != null ? usedCount.intValue() : 0;
|
|
|
|
|
+ if (used >= maxChance) {
|
|
|
|
|
+ // 今日抽奖次数已用完,记录违规审计日志并返回提示
|
|
|
recordViolation(userId, clientIp, today);
|
|
recordViolation(userId, clientIp, today);
|
|
|
- log.warn("重复抽奖违规: userId={}, ip={}, date={}", userId, clientIp, today);
|
|
|
|
|
- throw new BusinessException(ErrorCode.BUSINESS_ERROR, "今日抽奖次数已用完");
|
|
|
|
|
|
|
+ log.warn("重复抽奖违规: userId={}, ip={}, date={}, used={}/{}", userId, clientIp, today, used, maxChance);
|
|
|
|
|
+ throw new BusinessException(ErrorCode.BUSINESS_ERROR, "今日抽奖次数已用完(" + used + "/" + maxChance + ")");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 取一条未使用的机会
|
|
// 取一条未使用的机会
|
|
@@ -284,7 +302,8 @@ public class ActivityLotteryService {
|
|
|
|
|
|
|
|
int avail = available != null ? available.intValue() : 0;
|
|
int avail = available != null ? available.intValue() : 0;
|
|
|
int usedCount = used != null ? used.intValue() : 0;
|
|
int usedCount = used != null ? used.intValue() : 0;
|
|
|
- String reason = computeReason(config, avail, usedCount, eligible, levelEnough, licenseApproved, userLevel, requireLevel);
|
|
|
|
|
|
|
+ int maxChance = config.getDailyMaxChance() != null ? config.getDailyMaxChance() : 1;
|
|
|
|
|
+ String reason = computeReason(config, avail, usedCount, levelEnough, licenseApproved, userLevel, requireLevel);
|
|
|
|
|
|
|
|
return ActivityLotteryStatusResponse.builder()
|
|
return ActivityLotteryStatusResponse.builder()
|
|
|
.enabled(isActive(config))
|
|
.enabled(isActive(config))
|
|
@@ -296,6 +315,7 @@ public class ActivityLotteryService {
|
|
|
.requireMembershipLevel(requireLevel)
|
|
.requireMembershipLevel(requireLevel)
|
|
|
.availableChances(avail)
|
|
.availableChances(avail)
|
|
|
.usedChances(usedCount)
|
|
.usedChances(usedCount)
|
|
|
|
|
+ .dailyMaxChance(maxChance)
|
|
|
.reason(reason)
|
|
.reason(reason)
|
|
|
.build();
|
|
.build();
|
|
|
}
|
|
}
|
|
@@ -463,6 +483,9 @@ public class ActivityLotteryService {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private static final List<String> PRIZE_TIERS = List.of(
|
|
|
|
|
+ "特等奖", "一等奖", "二等奖", "三等奖", "四等奖", "五等奖", "谢谢惠顾");
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 从奖品名称中提取奖项等级(特等奖/一等奖/.../谢谢惠顾)
|
|
* 从奖品名称中提取奖项等级(特等奖/一等奖/.../谢谢惠顾)
|
|
|
*/
|
|
*/
|
|
@@ -470,13 +493,11 @@ public class ActivityLotteryService {
|
|
|
if (prizeName == null) {
|
|
if (prizeName == null) {
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
- if (prizeName.contains("特等奖")) return "特等奖";
|
|
|
|
|
- if (prizeName.contains("一等奖")) return "一等奖";
|
|
|
|
|
- if (prizeName.contains("二等奖")) return "二等奖";
|
|
|
|
|
- if (prizeName.contains("三等奖")) return "三等奖";
|
|
|
|
|
- if (prizeName.contains("四等奖")) return "四等奖";
|
|
|
|
|
- if (prizeName.contains("五等奖")) return "五等奖";
|
|
|
|
|
- if (prizeName.contains("谢谢")) return "谢谢惠顾";
|
|
|
|
|
|
|
+ for (String tier : PRIZE_TIERS) {
|
|
|
|
|
+ if (prizeName.contains(tier)) {
|
|
|
|
|
+ return tier;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -484,8 +505,7 @@ public class ActivityLotteryService {
|
|
|
* 按优先级计算抽奖状态原因。有机会时返回 null。
|
|
* 按优先级计算抽奖状态原因。有机会时返回 null。
|
|
|
*/
|
|
*/
|
|
|
private String computeReason(ActivityLotteryConfig config, int avail, int usedCount,
|
|
private String computeReason(ActivityLotteryConfig config, int avail, int usedCount,
|
|
|
- boolean eligible, boolean levelEnough, boolean licenseApproved,
|
|
|
|
|
- MembershipLevel userLevel, String requireLevel) {
|
|
|
|
|
|
|
+ boolean levelEnough, boolean licenseApproved, MembershipLevel userLevel, String requireLevel) {
|
|
|
if (avail > 0) {
|
|
if (avail > 0) {
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
@@ -504,8 +524,8 @@ public class ActivityLotteryService {
|
|
|
if (!licenseApproved) {
|
|
if (!licenseApproved) {
|
|
|
return "入驻信息未审核通过,无法参与抽奖";
|
|
return "入驻信息未审核通过,无法参与抽奖";
|
|
|
}
|
|
}
|
|
|
- if (usedCount == 0) {
|
|
|
|
|
- return "今日尚未比价,完成首次比价后自动获得抽奖机会";
|
|
|
|
|
|
|
+ if (usedCount == 0 && avail == 0) {
|
|
|
|
|
+ return "今日尚未比价,完成比价后即可获得抽奖机会";
|
|
|
}
|
|
}
|
|
|
return "今日抽奖机会已用完";
|
|
return "今日抽奖机会已用完";
|
|
|
}
|
|
}
|
|
@@ -563,7 +583,7 @@ public class ActivityLotteryService {
|
|
|
if (prizes.isEmpty()) {
|
|
if (prizes.isEmpty()) {
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
- // 过滤掉库存为0的奖品(EMPTY类型不受库存限制,始终参与)
|
|
|
|
|
|
|
+ // 过滤掉库存为0的奖品(EMPTY 类型不受库存限制,始终参与)
|
|
|
List<ActivityLotteryPrize> available = prizes.stream()
|
|
List<ActivityLotteryPrize> available = prizes.stream()
|
|
|
.filter(p -> REWARD_TYPE_EMPTY.equalsIgnoreCase(p.getRewardType())
|
|
.filter(p -> REWARD_TYPE_EMPTY.equalsIgnoreCase(p.getRewardType())
|
|
|
|| (p.getRemainingStock() != null && p.getRemainingStock() > 0))
|
|
|| (p.getRemainingStock() != null && p.getRemainingStock() > 0))
|
|
@@ -593,6 +613,7 @@ public class ActivityLotteryService {
|
|
|
* 根据奖品类型发放奖励
|
|
* 根据奖品类型发放奖励
|
|
|
* - MEMBERSHIP:会员时长即刻到账
|
|
* - MEMBERSHIP:会员时长即刻到账
|
|
|
* - PHYSICAL:实物奖品,记录中奖,需客服后续核实(不自动发放)
|
|
* - PHYSICAL:实物奖品,记录中奖,需客服后续核实(不自动发放)
|
|
|
|
|
+ * - OTHER:其他奖品,记录中奖,需客服后续核实(不自动发放)
|
|
|
* - EMPTY:无奖励
|
|
* - EMPTY:无奖励
|
|
|
*/
|
|
*/
|
|
|
private void applyReward(Long userId, ActivityLotteryPrize prize) {
|
|
private void applyReward(Long userId, ActivityLotteryPrize prize) {
|
|
@@ -604,6 +625,11 @@ public class ActivityLotteryService {
|
|
|
log.info("实物奖品中奖,待客服核实: userId={}, prize={}", userId, prize.getName());
|
|
log.info("实物奖品中奖,待客服核实: userId={}, prize={}", userId, prize.getName());
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
+ if (REWARD_TYPE_OTHER.equalsIgnoreCase(prize.getRewardType())) {
|
|
|
|
|
+ // 其他奖品不自动发放,中奖记录已入库,需客服后续核实
|
|
|
|
|
+ log.info("其他奖品中奖,待客服核实: userId={}, prize={}", userId, prize.getName());
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
if (REWARD_TYPE_MEMBERSHIP.equalsIgnoreCase(prize.getRewardType())) {
|
|
if (REWARD_TYPE_MEMBERSHIP.equalsIgnoreCase(prize.getRewardType())) {
|
|
|
int days = prize.getRewardDays() != null ? prize.getRewardDays() : 0;
|
|
int days = prize.getRewardDays() != null ? prize.getRewardDays() : 0;
|
|
|
if (days <= 0) {
|
|
if (days <= 0) {
|