chore: sync local latest state and repository cleanup

This commit is contained in:
Your Name
2026-03-23 13:02:36 +08:00
parent f1ff3d629f
commit 2ef0f17961
493 changed files with 46912 additions and 7977 deletions

View File

@@ -0,0 +1,374 @@
package com.mosquito.project.job;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mosquito.project.domain.Activity;
import com.mosquito.project.persistence.entity.ActivityEntity;
import com.mosquito.project.persistence.entity.RewardJobEntity;
import com.mosquito.project.persistence.entity.ShortLinkEntity;
import com.mosquito.project.persistence.entity.UserRewardEntity;
import com.mosquito.project.persistence.repository.ActivityRepository;
import com.mosquito.project.persistence.repository.RewardJobRepository;
import com.mosquito.project.persistence.repository.ShortLinkRepository;
import com.mosquito.project.persistence.repository.UserRewardRepository;
import com.mosquito.project.service.CouponRewardService;
import com.mosquito.project.service.RewardService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Map;
/**
* 奖励任务消费处理器
* 定时处理奖励队列中的待发放任务
* 使用tracking_id进行精确归因
* 按活动规则计算奖励值
*/
@Component
public class RewardJobProcessor {
private static final Logger log = LoggerFactory.getLogger(RewardJobProcessor.class);
private static final int MAX_RETRY_COUNT = 3;
private final RewardJobRepository rewardJobRepository;
private final ShortLinkRepository shortLinkRepository;
private final UserRewardRepository userRewardRepository;
private final ActivityRepository activityRepository;
private final ObjectMapper objectMapper;
private final RewardDistributor rewardDistributor;
private final CouponRewardService couponRewardService;
public RewardJobProcessor(RewardJobRepository rewardJobRepository,
ShortLinkRepository shortLinkRepository,
UserRewardRepository userRewardRepository,
ActivityRepository activityRepository,
ObjectMapper objectMapper,
RewardDistributor rewardDistributor,
CouponRewardService couponRewardService) {
this.rewardJobRepository = rewardJobRepository;
this.shortLinkRepository = shortLinkRepository;
this.userRewardRepository = userRewardRepository;
this.activityRepository = activityRepository;
this.objectMapper = objectMapper;
this.rewardDistributor = rewardDistributor;
this.couponRewardService = couponRewardService;
}
@Scheduled(fixedDelay = 5000) // 每5秒执行一次
@Transactional
public void processRewardJobs() {
OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC);
List<RewardJobEntity> pendingJobs = rewardJobRepository
.findTop10ByStatusAndNextRunAtLessThanEqualOrderByCreatedAtAsc("pending", now);
if (pendingJobs.isEmpty()) {
return;
}
log.info("开始处理 {} 个奖励任务", pendingJobs.size());
for (RewardJobEntity job : pendingJobs) {
try {
processRewardJob(job);
} catch (Exception e) {
log.error("处理奖励任务 {} 失败: {}", job.getId(), e.getMessage());
handleJobFailure(job);
}
}
log.info("奖励任务处理完成");
}
/**
* 处理单个奖励任务
* 通过tracking_id精确定位邀请关系并发奖
* PRD要求去掉"按externalUserId查最近邀请记录"的隐式归因
*/
private void processRewardJob(RewardJobEntity job) {
String trackingId = job.getTrackingId();
if (trackingId == null || trackingId.isEmpty()) {
log.warn("奖励任务 {} 缺少tracking_id", job.getId());
job.setStatus("failed");
job.setUpdatedAt(OffsetDateTime.now(ZoneOffset.UTC));
rewardJobRepository.save(job);
return;
}
// 通过tracking_id精确查找对应的短链接记录包含邀请关系
var shortLinkOpt = shortLinkRepository.findByTrackingId(trackingId);
if (shortLinkOpt.isEmpty()) {
log.warn("找不到tracking_id {} 对应的邀请记录", trackingId);
job.setStatus("failed");
job.setUpdatedAt(OffsetDateTime.now(ZoneOffset.UTC));
rewardJobRepository.save(job);
return;
}
ShortLinkEntity shortLink = shortLinkOpt.get();
Long inviterUserId = shortLink.getInviterUserId();
Long activityId = shortLink.getActivityId();
if (inviterUserId == null) {
log.warn("tracking_id {} 对应的邀请记录没有邀请人", trackingId);
job.setStatus("failed");
job.setUpdatedAt(OffsetDateTime.now(ZoneOffset.UTC));
rewardJobRepository.save(job);
return;
}
// 根据活动配置计算奖励值和奖励类型
int points = calculateRewardPoints(activityId);
String rewardType = calculateRewardType(activityId); // 从活动配置获取奖励类型
// 处理优惠券奖励
if ("COUPON".equals(rewardType)) {
// 优惠券奖励处理先创建积分记录状态为APPROVED再调用优惠券服务发放
processCouponReward(job, shortLink, inviterUserId, activityId, trackingId, points);
return;
}
// 调用外部发放适配器进行奖励发放
try {
boolean success = rewardDistributor.distribute(inviterUserId, activityId, trackingId, points, rewardType);
if (!success) {
log.warn("奖励发放失败: userId={}, activityId={}, trackingId={}", inviterUserId, activityId, trackingId);
handleJobFailure(job);
return;
}
} catch (Exception e) {
log.error("奖励发放异常: {}", e.getMessage());
handleJobFailure(job);
return;
}
// 创建用户奖励记录
UserRewardEntity reward = new UserRewardEntity();
reward.setUserId(inviterUserId);
reward.setActivityId(activityId);
reward.setPoints(points);
reward.setType(rewardType);
reward.setStatus(RewardService.STATUS_GRANTED);
reward.setCreatedAt(OffsetDateTime.now(ZoneOffset.UTC));
reward.setTrackingId(trackingId); // 记录归因的tracking_id
// 从活动获取部门ID用于数据权限过滤
var activityOpt = activityRepository.findById(activityId);
if (activityOpt.isPresent()) {
reward.setDepartmentId(activityOpt.get().getDepartmentId());
log.debug("设置奖励departmentId={} from activityId={}", activityOpt.get().getDepartmentId(), activityId);
}
userRewardRepository.save(reward);
// 更新任务状态为已完成
job.setStatus("completed");
job.setUpdatedAt(OffsetDateTime.now(ZoneOffset.UTC));
rewardJobRepository.save(job);
log.info("成功处理奖励任务 {}通过tracking_id {} 为用户 {} 发放 {} 积分", job.getId(), trackingId, inviterUserId, points);
}
/**
* 根据活动配置计算奖励积分
*/
private int calculateRewardPoints(Long activityId) {
// 默认奖励值
int defaultPoints = 10;
try {
var activityOpt = activityRepository.findById(activityId);
if (activityOpt.isEmpty()) {
log.warn("找不到活动 {} 的配置,使用默认奖励", activityId);
return defaultPoints;
}
ActivityEntity activity = activityOpt.get();
String calculationMode = activity.getRewardCalculationMode();
// 如果有阶梯奖励配置,按阶梯计算
if (activity.getRewardTiersConfig() != null && !activity.getRewardTiersConfig().isEmpty()) {
return calculateTieredReward(activity.getRewardTiersConfig());
}
// 根据计算模式返回奖励
if ("FIXED".equals(calculationMode)) {
return defaultPoints;
}
// 默认返回固定奖励
return defaultPoints;
} catch (Exception e) {
log.error("计算奖励失败: {}", e.getMessage());
return defaultPoints;
}
}
/**
* 计算阶梯奖励
*/
private int calculateTieredReward(String tiersConfig) {
try {
@SuppressWarnings("unchecked")
List<Map<String, Object>> tiers = objectMapper.readValue(tiersConfig, List.class);
if (tiers == null || tiers.isEmpty()) {
return 10;
}
// 取第一个阶梯的奖励值
Map<String, Object> firstTier = tiers.get(0);
Object points = firstTier.get("points");
if (points instanceof Number) {
return ((Number) points).intValue();
}
return 10;
} catch (JsonProcessingException e) {
log.error("解析阶梯奖励配置失败: {}", e.getMessage());
return 10;
}
}
/**
* 从活动配置中获取奖励类型
*/
private String calculateRewardType(Long activityId) {
try {
var activityOpt = activityRepository.findById(activityId);
if (activityOpt.isEmpty()) {
return "POINTS"; // 默认积分奖励
}
ActivityEntity activity = activityOpt.get();
// 从活动配置的 rewardTiersConfig 中获取奖励类型
if (activity.getRewardTiersConfig() != null && !activity.getRewardTiersConfig().isEmpty()) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> tiers = objectMapper.readValue(activity.getRewardTiersConfig(), List.class);
if (tiers != null && !tiers.isEmpty()) {
Map<String, Object> firstTier = tiers.get(0);
Object type = firstTier.get("type");
if (type != null) {
return type.toString();
}
}
}
return "POINTS"; // 默认积分奖励
} catch (Exception e) {
log.error("获取奖励类型失败: {}", e.getMessage());
return "POINTS";
}
}
/**
* 处理优惠券奖励发放
*/
private void processCouponReward(RewardJobEntity job, ShortLinkEntity shortLink,
Long inviterUserId, Long activityId, String trackingId, int points) {
try {
// 1. 先创建用户奖励记录状态为APPROVED待发放
UserRewardEntity reward = new UserRewardEntity();
reward.setUserId(inviterUserId);
reward.setActivityId(activityId);
reward.setPoints(points);
reward.setType("COUPON");
reward.setStatus("APPROVED"); // 待发放状态
reward.setCreatedAt(OffsetDateTime.now(ZoneOffset.UTC));
reward.setTrackingId(trackingId);
// 从活动获取部门ID和优惠券批次ID
var activityOpt = activityRepository.findById(activityId);
if (activityOpt.isPresent()) {
ActivityEntity activity = activityOpt.get();
reward.setDepartmentId(activity.getDepartmentId());
// 从活动配置中获取优惠券批次ID
if (activity.getRewardTiersConfig() != null) {
try {
@SuppressWarnings("unchecked")
List<Map<String, Object>> tiers = objectMapper.readValue(activity.getRewardTiersConfig(), List.class);
if (tiers != null && !tiers.isEmpty()) {
Object couponBatchId = tiers.get(0).get("couponBatchId");
if (couponBatchId != null) {
reward.setCouponBatchId(couponBatchId.toString());
}
}
} catch (Exception e) {
log.warn("解析优惠券批次ID失败: {}", e.getMessage());
}
}
}
UserRewardEntity savedReward = userRewardRepository.save(reward);
// 2. 调用优惠券服务发放优惠券
if (savedReward.getCouponBatchId() != null) {
CouponRewardService.CouponGrantResult result = couponRewardService.grantCoupon(
savedReward.getId(),
savedReward.getCouponBatchId(),
inviterUserId
);
if (result.isSuccess()) {
log.info("优惠券发放成功: rewardId={}, couponCode={}", savedReward.getId(), result.getCouponCode());
} else {
log.warn("优惠券发放失败: rewardId={}, reason={}", savedReward.getId(), result.getMessage());
// 优惠券发放失败不算任务失败,因为奖励记录已创建
}
}
// 3. 更新任务状态为已完成
job.setStatus("completed");
job.setUpdatedAt(OffsetDateTime.now(ZoneOffset.UTC));
rewardJobRepository.save(job);
log.info("成功处理优惠券奖励任务 {}通过tracking_id {} 为用户 {} 发放优惠券", job.getId(), trackingId, inviterUserId);
} catch (Exception e) {
log.error("处理优惠券奖励任务 {} 失败: {}", job.getId(), e.getMessage());
handleJobFailure(job);
}
}
/**
* 处理任务失败,重试或标记为失败
*/
private void handleJobFailure(RewardJobEntity job) {
int retryCount = job.getRetryCount() != null ? job.getRetryCount() : 0;
if (retryCount >= MAX_RETRY_COUNT) {
// 超过最大重试次数,标记为失败
job.setStatus("failed");
log.warn("奖励任务 {} 超过最大重试次数,标记为失败", job.getId());
} else {
// 增加重试次数,安排下次执行
job.setRetryCount(retryCount + 1);
job.setNextRunAt(OffsetDateTime.now(ZoneOffset.UTC).plusMinutes((long) Math.pow(2, retryCount)));
job.setStatus("pending");
log.info("奖励任务 {} 重试次数 {}, 下次执行时间: {}", job.getId(), retryCount + 1, job.getNextRunAt());
}
job.setUpdatedAt(OffsetDateTime.now(ZoneOffset.UTC));
rewardJobRepository.save(job);
}
/**
* 奖励发放接口(适配器模式)
*/
public interface RewardDistributor {
/**
* 发放奖励
* @param userId 用户ID
* @param activityId 活动ID
* @param trackingId 追踪ID
* @param points 积分数量
* @param rewardType 奖励类型
* @return 是否发放成功
*/
boolean distribute(Long userId, Long activityId, String trackingId, int points, String rewardType);
}
}