319 lines
12 KiB
Java
319 lines
12 KiB
Java
|
|
package com.dora.manager;
|
|||
|
|
|
|||
|
|
import com.dora.entity.RevenueConfig;
|
|||
|
|
import com.dora.entity.User;
|
|||
|
|
import com.dora.event.PromotionLevelChangedEvent;
|
|||
|
|
import com.dora.mapper.RevenueConfigMapper;
|
|||
|
|
import com.dora.mapper.UserMapper;
|
|||
|
|
import lombok.Builder;
|
|||
|
|
import lombok.Getter;
|
|||
|
|
import lombok.RequiredArgsConstructor;
|
|||
|
|
import lombok.Singular;
|
|||
|
|
import lombok.extern.slf4j.Slf4j;
|
|||
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|||
|
|
import org.springframework.cache.annotation.Cacheable;
|
|||
|
|
import org.springframework.context.ApplicationEventPublisher;
|
|||
|
|
import org.springframework.context.annotation.Lazy;
|
|||
|
|
import org.springframework.stereotype.Component;
|
|||
|
|
import org.springframework.transaction.annotation.Transactional;
|
|||
|
|
|
|||
|
|
import java.time.LocalDateTime;
|
|||
|
|
import java.util.ArrayList;
|
|||
|
|
import java.util.HashMap;
|
|||
|
|
import java.util.List;
|
|||
|
|
import java.util.Map;
|
|||
|
|
import java.util.Objects;
|
|||
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|||
|
|
import java.util.concurrent.locks.Lock;
|
|||
|
|
import java.util.concurrent.locks.ReentrantLock;
|
|||
|
|
import java.util.stream.Collectors;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 推广等级统一管理器
|
|||
|
|
* 解决多个地方更新推广等级的问题
|
|||
|
|
*/
|
|||
|
|
@Component
|
|||
|
|
@RequiredArgsConstructor
|
|||
|
|
@Slf4j
|
|||
|
|
public class PromotionLevelManager {
|
|||
|
|
|
|||
|
|
private final UserMapper userMapper;
|
|||
|
|
private final RevenueConfigMapper revenueConfigMapper;
|
|||
|
|
private final ApplicationEventPublisher eventPublisher;
|
|||
|
|
|
|||
|
|
@Autowired
|
|||
|
|
@Lazy
|
|||
|
|
private PromotionLevelManager self;
|
|||
|
|
|
|||
|
|
// 用户级别的锁,防止并发更新同一用户
|
|||
|
|
private final ConcurrentHashMap<Long, Lock> userLocks = new ConcurrentHashMap<>();
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 推广等级计算策略枚举
|
|||
|
|
*/
|
|||
|
|
public enum CalculationStrategy {
|
|||
|
|
PAID_FANS_ONLY("paid_fans", "仅付费粉丝数"),
|
|||
|
|
TOTAL_FANS_ONLY("total_fans", "仅总粉丝数"),
|
|||
|
|
WEIGHTED_AVERAGE("weighted", "加权平均");
|
|||
|
|
|
|||
|
|
@Getter
|
|||
|
|
private final String code;
|
|||
|
|
@Getter
|
|||
|
|
private final String description;
|
|||
|
|
|
|||
|
|
CalculationStrategy(String code, String description) {
|
|||
|
|
this.code = code;
|
|||
|
|
this.description = description;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 推广等级更新结果
|
|||
|
|
*/
|
|||
|
|
@Getter
|
|||
|
|
@Builder
|
|||
|
|
public static class PromotionLevelUpdateResult {
|
|||
|
|
private final boolean updated;
|
|||
|
|
private final Integer oldLevel;
|
|||
|
|
private final Integer newLevel;
|
|||
|
|
private final String triggerSource;
|
|||
|
|
private final LocalDateTime updateTime;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 统一的推广等级更新方法
|
|||
|
|
*
|
|||
|
|
* @param userId 用户ID
|
|||
|
|
* @param strategy 计算策略
|
|||
|
|
* @param triggerSource 触发源
|
|||
|
|
* @return 更新结果
|
|||
|
|
*/
|
|||
|
|
@Transactional
|
|||
|
|
public PromotionLevelUpdateResult updatePromotionLevel(
|
|||
|
|
Long userId,
|
|||
|
|
CalculationStrategy strategy,
|
|||
|
|
String triggerSource) {
|
|||
|
|
|
|||
|
|
Lock userLock = userLocks.computeIfAbsent(userId, k -> new ReentrantLock());
|
|||
|
|
userLock.lock();
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
log.info("开始更新用户推广等级 - userId: {}, strategy: {}, source: {}",
|
|||
|
|
userId, strategy.getCode(), triggerSource);
|
|||
|
|
|
|||
|
|
// 1. 获取用户当前信息
|
|||
|
|
User user = userMapper.selectById(userId);
|
|||
|
|
if (user == null) {
|
|||
|
|
log.warn("用户不存在 - userId: {}", userId);
|
|||
|
|
return createFailedResult(triggerSource);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Integer currentLevel = user.getPromotionLevel();
|
|||
|
|
|
|||
|
|
// 2. 根据策略计算新等级
|
|||
|
|
int newLevel = calculateLevelByStrategy(user, strategy);
|
|||
|
|
|
|||
|
|
// 3. 检查等级是否发生变化
|
|||
|
|
if (Objects.equals(currentLevel, newLevel)) {
|
|||
|
|
log.debug("用户推广等级未发生变化 - userId: {}, level: {}", userId, currentLevel);
|
|||
|
|
return createUnchangedResult(currentLevel, triggerSource);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 4. 更新数据库
|
|||
|
|
int updatedRows = userMapper.updatePromotionLevel(userId, newLevel);
|
|||
|
|
if (updatedRows == 0) {
|
|||
|
|
log.warn("更新用户推广等级失败,可能存在并发更新 - userId: {}", userId);
|
|||
|
|
//可以选择返回失败或者重新尝试
|
|||
|
|
return createFailedResult(triggerSource);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 5. 记录审计日志
|
|||
|
|
recordAuditLog(userId, currentLevel, newLevel, strategy, triggerSource);
|
|||
|
|
|
|||
|
|
// 6. 发布等级变化事件
|
|||
|
|
publishLevelChangedEvent(userId, currentLevel, newLevel, triggerSource);
|
|||
|
|
|
|||
|
|
log.info("用户推广等级更新成功 - userId: {}, level: {} -> {}, strategy: {}, source: {}",
|
|||
|
|
userId, currentLevel, newLevel, strategy.getCode(), triggerSource);
|
|||
|
|
|
|||
|
|
return createSuccessResult(currentLevel, newLevel, triggerSource);
|
|||
|
|
|
|||
|
|
} finally {
|
|||
|
|
userLock.unlock();
|
|||
|
|
// 当锁没有其他等待线程时,从map中移除,避免内存泄漏
|
|||
|
|
userLocks.compute(userId, (k, v) -> (v != null && v.hasQueuedThreads()) ? v : null);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 根据策略计算推广等级
|
|||
|
|
*/
|
|||
|
|
private int calculateLevelByStrategy(User user, CalculationStrategy strategy) {
|
|||
|
|
int fansCount;
|
|||
|
|
Long userId = user.getId();
|
|||
|
|
|
|||
|
|
switch (strategy) {
|
|||
|
|
case TOTAL_FANS_ONLY:
|
|||
|
|
fansCount = userMapper.countFansByInviterId(userId);
|
|||
|
|
break;
|
|||
|
|
case WEIGHTED_AVERAGE:
|
|||
|
|
// 加权策略:付费粉丝权重 * 2 + 总粉丝权重 * 1
|
|||
|
|
// TODO: 可以优化为一次数据库查询返回两个字段
|
|||
|
|
int paidFans = userMapper.countPaidFansByInviterId(userId);
|
|||
|
|
int totalFans = userMapper.countFansByInviterId(userId);
|
|||
|
|
fansCount = (paidFans * 2) + totalFans;
|
|||
|
|
break;
|
|||
|
|
case PAID_FANS_ONLY:
|
|||
|
|
default:
|
|||
|
|
fansCount = userMapper.countPaidFansByInviterId(userId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return calculatePromotionLevel(fansCount);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 根据粉丝数计算推广等级
|
|||
|
|
*/
|
|||
|
|
private int calculatePromotionLevel(int fansCount) {
|
|||
|
|
try {
|
|||
|
|
List<RevenueConfig> configs = self.getPromotionRevenueConfigs();
|
|||
|
|
|
|||
|
|
return configs.stream()
|
|||
|
|
.filter(config -> config.getMinFans() != null && fansCount >= config.getMinFans())
|
|||
|
|
.mapToInt(RevenueConfig::getLevel)
|
|||
|
|
.max()
|
|||
|
|
.orElse(1);
|
|||
|
|
|
|||
|
|
} catch (Exception e) {
|
|||
|
|
log.error("计算推广等级失败 - fansCount: {}", fansCount, e);
|
|||
|
|
return 1; // 默认返回1级
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取并缓存推广收益配置
|
|||
|
|
*/
|
|||
|
|
@Cacheable("revenueConfigs")
|
|||
|
|
public List<RevenueConfig> getPromotionRevenueConfigs() {
|
|||
|
|
return revenueConfigMapper.selectByConfigType("promotion");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 记录审计日志
|
|||
|
|
*/
|
|||
|
|
private void recordAuditLog(Long userId, Integer oldLevel, Integer newLevel,
|
|||
|
|
CalculationStrategy strategy, String triggerSource) {
|
|||
|
|
// 这里可以记录到专门的审计日志表
|
|||
|
|
String strategyCode = (strategy != null) ? strategy.getCode() : "admin_override";
|
|||
|
|
log.info("推广等级变化审计 - userId: {}, level: {} -> {}, strategy: {}, source: {}, time: {}",
|
|||
|
|
userId, oldLevel, newLevel, strategyCode, triggerSource, LocalDateTime.now());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 发布等级变化事件
|
|||
|
|
*/
|
|||
|
|
private void publishLevelChangedEvent(Long userId, Integer oldLevel, Integer newLevel, String triggerSource) {
|
|||
|
|
try {
|
|||
|
|
PromotionLevelChangedEvent event = new PromotionLevelChangedEvent(
|
|||
|
|
this, userId, oldLevel, newLevel, triggerSource);
|
|||
|
|
eventPublisher.publishEvent(event);
|
|||
|
|
} catch (Exception e) {
|
|||
|
|
log.error("发布推广等级变化事件失败 - userId: {}", userId, e);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 批量更新推广等级(管理员操作)
|
|||
|
|
*/
|
|||
|
|
@Transactional
|
|||
|
|
public BatchUpdateResult batchUpdatePromotionLevel(List<Long> userIds, Integer targetLevel, String adminId) {
|
|||
|
|
log.info("管理员批量更新推广等级 - adminId: {}, userCount: {}, targetLevel: {}",
|
|||
|
|
adminId, userIds.size(), targetLevel);
|
|||
|
|
|
|||
|
|
BatchUpdateResult.BatchUpdateResultBuilder resultBuilder = BatchUpdateResult.builder();
|
|||
|
|
|
|||
|
|
if (userIds == null || userIds.isEmpty()) {
|
|||
|
|
return resultBuilder.build();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
List<User> users = userMapper.selectBatchIds(userIds);
|
|||
|
|
Map<Long, User> userMap = users.stream().collect(Collectors.toMap(User::getId, user -> user));
|
|||
|
|
|
|||
|
|
for (Long userId : userIds) {
|
|||
|
|
User user = userMap.get(userId);
|
|||
|
|
if (user == null) {
|
|||
|
|
resultBuilder.failedUser(userId, "用户不存在");
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Integer oldLevel = user.getPromotionLevel();
|
|||
|
|
if (Objects.equals(oldLevel, targetLevel)) {
|
|||
|
|
resultBuilder.skippedUser(userId, "等级未变化");
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
int updatedRows = userMapper.updatePromotionLevel(userId, targetLevel);
|
|||
|
|
if (updatedRows > 0) {
|
|||
|
|
resultBuilder.successUser(userId);
|
|||
|
|
recordAuditLog(userId, oldLevel, targetLevel, null, "admin_batch_update:" + adminId);
|
|||
|
|
publishLevelChangedEvent(userId, oldLevel, targetLevel, "admin_batch_update");
|
|||
|
|
} else {
|
|||
|
|
resultBuilder.failedUser(userId, "数据库更新失败");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
BatchUpdateResult result = resultBuilder.build();
|
|||
|
|
log.info("批量更新推广等级完成 - 成功: {}, 失败: {}, 跳过: {}",
|
|||
|
|
result.getSuccessCount(), result.getFailedCount(), result.getSkippedCount());
|
|||
|
|
|
|||
|
|
return result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 辅助方法创建结果对象
|
|||
|
|
private PromotionLevelUpdateResult createSuccessResult(Integer oldLevel, Integer newLevel, String triggerSource) {
|
|||
|
|
return PromotionLevelUpdateResult.builder()
|
|||
|
|
.updated(true)
|
|||
|
|
.oldLevel(oldLevel)
|
|||
|
|
.newLevel(newLevel)
|
|||
|
|
.triggerSource(triggerSource)
|
|||
|
|
.updateTime(LocalDateTime.now())
|
|||
|
|
.build();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private PromotionLevelUpdateResult createUnchangedResult(Integer level, String triggerSource) {
|
|||
|
|
return PromotionLevelUpdateResult.builder()
|
|||
|
|
.updated(false)
|
|||
|
|
.oldLevel(level)
|
|||
|
|
.newLevel(level)
|
|||
|
|
.triggerSource(triggerSource)
|
|||
|
|
.updateTime(LocalDateTime.now())
|
|||
|
|
.build();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private PromotionLevelUpdateResult createFailedResult(String triggerSource) {
|
|||
|
|
return PromotionLevelUpdateResult.builder()
|
|||
|
|
.updated(false)
|
|||
|
|
.triggerSource(triggerSource)
|
|||
|
|
.updateTime(LocalDateTime.now())
|
|||
|
|
.build();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 批量更新结果
|
|||
|
|
*/
|
|||
|
|
@Getter
|
|||
|
|
@Builder
|
|||
|
|
public static class BatchUpdateResult {
|
|||
|
|
@Singular
|
|||
|
|
private final List<Long> successUsers;
|
|||
|
|
@Singular
|
|||
|
|
private final Map<Long, String> failedUsers;
|
|||
|
|
@Singular
|
|||
|
|
private final Map<Long, String> skippedUsers;
|
|||
|
|
|
|||
|
|
public int getSuccessCount() { return successUsers.size(); }
|
|||
|
|
public int getFailedCount() { return failedUsers.size(); }
|
|||
|
|
public int getSkippedCount() { return skippedUsers.size(); }
|
|||
|
|
}
|
|||
|
|
}
|