|
|
@@ -0,0 +1,914 @@
|
|
|
+package com.haha.service.impl;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.baomidou.mybatisplus.core.metadata.IPage;
|
|
|
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|
|
+import com.haha.common.enums.ActivityStatusEnum;
|
|
|
+import com.haha.common.enums.InviteRewardStatusEnum;
|
|
|
+import com.haha.common.enums.InviteStatusEnum;
|
|
|
+import com.haha.common.exception.BusinessException;
|
|
|
+import com.haha.common.vo.StatusLabel;
|
|
|
+import com.haha.entity.CouponTemplate;
|
|
|
+import com.haha.entity.InviteActivity;
|
|
|
+import com.haha.entity.InviteRecord;
|
|
|
+import com.haha.entity.InviteReward;
|
|
|
+import com.haha.entity.UserCoupon;
|
|
|
+import com.haha.entity.dto.CouponDistributeDTO;
|
|
|
+import com.haha.entity.dto.InviteActivityCreateDTO;
|
|
|
+import com.haha.entity.dto.InviteActivityUpdateDTO;
|
|
|
+import com.haha.entity.dto.InviteRecordQueryDTO;
|
|
|
+import com.haha.mapper.InviteActivityMapper;
|
|
|
+import com.haha.mapper.InviteRecordMapper;
|
|
|
+import com.haha.mapper.InviteRewardMapper;
|
|
|
+import com.haha.service.CouponTemplateService;
|
|
|
+import com.haha.service.InviteActivityService;
|
|
|
+import com.haha.service.InviteNotificationService;
|
|
|
+import com.haha.service.RedisService;
|
|
|
+import com.haha.service.UserCouponService;
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.data.redis.core.StringRedisTemplate;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
+import org.springframework.util.StringUtils;
|
|
|
+
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.security.SecureRandom;
|
|
|
+import java.time.LocalDate;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 邀请活动服务实现类
|
|
|
+ * 实现邀请活动的完整业务逻辑,包括活动管理、邀请码生成、邀请关系绑定、
|
|
|
+ * 邀请激活、奖励发放、防刷机制等功能
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+@RequiredArgsConstructor
|
|
|
+public class InviteActivityServiceImpl extends ServiceImpl<InviteActivityMapper, InviteActivity> implements InviteActivityService {
|
|
|
+
|
|
|
+ private final InviteActivityMapper inviteActivityMapper;
|
|
|
+ private final InviteRecordMapper inviteRecordMapper;
|
|
|
+ private final InviteRewardMapper inviteRewardMapper;
|
|
|
+ private final CouponTemplateService couponTemplateService;
|
|
|
+ private final UserCouponService userCouponService;
|
|
|
+ private final RedisService redisService;
|
|
|
+ private final StringRedisTemplate stringRedisTemplate;
|
|
|
+ private final InviteNotificationService notificationService;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Base62编码字符集,用于生成短邀请码
|
|
|
+ */
|
|
|
+ private static final String BASE62_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 随机数生成器,用于生成安全的随机数
|
|
|
+ */
|
|
|
+ private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Redis防刷Key前缀:IP频率限制
|
|
|
+ */
|
|
|
+ private static final String REDIS_KEY_INVITE_IP_PREFIX = "invite:anti_fraud:ip:";
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Redis防刷Key前缀:设备频率限制
|
|
|
+ */
|
|
|
+ private static final String REDIS_KEY_INVITE_DEVICE_PREFIX = "invite:anti_fraud:device:";
|
|
|
+
|
|
|
+ // ==================== CRUD 操作 ====================
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public IPage<InviteActivity> getPage(InviteRecordQueryDTO queryDTO) {
|
|
|
+ queryDTO.validate();
|
|
|
+ Page<InviteActivity> page = new Page<>(queryDTO.getPage(), queryDTO.getPageSize());
|
|
|
+
|
|
|
+ LambdaQueryWrapper<InviteActivity> wrapper = new LambdaQueryWrapper<>();
|
|
|
+ wrapper.like(StringUtils.hasText(queryDTO.getActivityName()), InviteActivity::getActivityName, queryDTO.getActivityName());
|
|
|
+ wrapper.eq(queryDTO.getStatus() != null, InviteActivity::getStatus, queryDTO.getStatus());
|
|
|
+ wrapper.ge(queryDTO.getStartDate() != null, InviteActivity::getStartTime, queryDTO.getStartDate());
|
|
|
+ wrapper.le(queryDTO.getEndDate() != null, InviteActivity::getEndTime, queryDTO.getEndDate());
|
|
|
+ wrapper.eq(InviteActivity::getDeleted, 0);
|
|
|
+ wrapper.orderByDesc(InviteActivity::getCreateTime);
|
|
|
+
|
|
|
+ IPage<InviteActivity> result = this.page(page, wrapper);
|
|
|
+ result.getRecords().forEach(this::fillLabels);
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public InviteActivity getDetail(Long id) {
|
|
|
+ InviteActivity activity = this.getById(id);
|
|
|
+ if (activity == null || activity.getDeleted() == 1) {
|
|
|
+ throw new BusinessException(404, "邀请活动不存在");
|
|
|
+ }
|
|
|
+ fillLabels(activity);
|
|
|
+ return activity;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public InviteActivity create(InviteActivityCreateDTO dto, Long creatorId, String creatorName) {
|
|
|
+ // 参数校验:结束时间必须大于开始时间
|
|
|
+ if (dto.getEndTime().isBefore(dto.getStartTime()) || dto.getEndTime().isEqual(dto.getStartTime())) {
|
|
|
+ throw new BusinessException(400, "结束时间必须大于开始时间");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 校验优惠券模板是否存在且可用
|
|
|
+ CouponTemplate template = couponTemplateService.getById(dto.getCouponTemplateId());
|
|
|
+ if (template == null) {
|
|
|
+ throw new BusinessException(400, "优惠券模板不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ InviteActivity activity = new InviteActivity();
|
|
|
+ activity.setActivityName(dto.getActivityName());
|
|
|
+ activity.setActivityDesc(dto.getActivityDesc());
|
|
|
+ activity.setStartTime(dto.getStartTime());
|
|
|
+ activity.setEndTime(dto.getEndTime());
|
|
|
+ activity.setStatus(ActivityStatusEnum.DRAFT.getCode());
|
|
|
+ activity.setCouponTemplateId(dto.getCouponTemplateId());
|
|
|
+ activity.setDailyLimit(dto.getDailyLimit() != null ? dto.getDailyLimit() : 10);
|
|
|
+ activity.setTotalLimit(dto.getTotalLimit() != null ? dto.getTotalLimit() : 100);
|
|
|
+ activity.setRequireFirstOrder(dto.getRequireFirstOrder() != null ? dto.getRequireFirstOrder() : 1);
|
|
|
+ activity.setMinOrderAmount(dto.getMinOrderAmount() != null ? dto.getMinOrderAmount() : BigDecimal.ZERO);
|
|
|
+ activity.setInviteCodePrefix(StringUtils.hasText(dto.getInviteCodePrefix()) ? dto.getInviteCodePrefix() : "INV");
|
|
|
+ activity.setApplyScope(dto.getApplyScope());
|
|
|
+ activity.setProductScope(dto.getProductScope());
|
|
|
+ activity.setTotalInviteCount(0);
|
|
|
+ activity.setValidInviteCount(0);
|
|
|
+ activity.setRewardCount(0);
|
|
|
+ activity.setCreatorId(creatorId);
|
|
|
+ activity.setCreatorName(creatorName);
|
|
|
+ activity.setDeleted(0);
|
|
|
+
|
|
|
+ this.save(activity);
|
|
|
+
|
|
|
+ log.info("创建邀请活动成功: id={}, name={}, creator={}", activity.getId(), activity.getActivityName(), creatorName);
|
|
|
+ return activity;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public InviteActivity update(InviteActivityUpdateDTO dto) {
|
|
|
+ InviteActivity activity = this.getById(dto.getId());
|
|
|
+ if (activity == null || activity.getDeleted() == 1) {
|
|
|
+ throw new BusinessException(404, "邀请活动不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 只有草稿状态允许编辑
|
|
|
+ if (!ActivityStatusEnum.canEdit(activity.getStatus())) {
|
|
|
+ throw new BusinessException(400, "当前状态不允许编辑");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 参数校验:如果传了时间,需要校验
|
|
|
+ if (dto.getStartTime() != null && dto.getEndTime() != null) {
|
|
|
+ if (dto.getEndTime().isBefore(dto.getStartTime()) || dto.getEndTime().isEqual(dto.getStartTime())) {
|
|
|
+ throw new BusinessException(400, "结束时间必须大于开始时间");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (dto.getActivityName() != null) {
|
|
|
+ activity.setActivityName(dto.getActivityName());
|
|
|
+ }
|
|
|
+ if (dto.getActivityDesc() != null) {
|
|
|
+ activity.setActivityDesc(dto.getActivityDesc());
|
|
|
+ }
|
|
|
+ if (dto.getStartTime() != null) {
|
|
|
+ activity.setStartTime(dto.getStartTime());
|
|
|
+ }
|
|
|
+ if (dto.getEndTime() != null) {
|
|
|
+ activity.setEndTime(dto.getEndTime());
|
|
|
+ }
|
|
|
+ if (dto.getCouponTemplateId() != null) {
|
|
|
+ activity.setCouponTemplateId(dto.getCouponTemplateId());
|
|
|
+ }
|
|
|
+ if (dto.getDailyLimit() != null) {
|
|
|
+ activity.setDailyLimit(dto.getDailyLimit());
|
|
|
+ }
|
|
|
+ if (dto.getTotalLimit() != null) {
|
|
|
+ activity.setTotalLimit(dto.getTotalLimit());
|
|
|
+ }
|
|
|
+ if (dto.getRequireFirstOrder() != null) {
|
|
|
+ activity.setRequireFirstOrder(dto.getRequireFirstOrder());
|
|
|
+ }
|
|
|
+ if (dto.getMinOrderAmount() != null) {
|
|
|
+ activity.setMinOrderAmount(dto.getMinOrderAmount());
|
|
|
+ }
|
|
|
+ if (dto.getInviteCodePrefix() != null) {
|
|
|
+ activity.setInviteCodePrefix(dto.getInviteCodePrefix());
|
|
|
+ }
|
|
|
+ if (dto.getApplyScope() != null) {
|
|
|
+ activity.setApplyScope(dto.getApplyScope());
|
|
|
+ }
|
|
|
+ if (dto.getProductScope() != null) {
|
|
|
+ activity.setProductScope(dto.getProductScope());
|
|
|
+ }
|
|
|
+
|
|
|
+ this.updateById(activity);
|
|
|
+
|
|
|
+ log.info("更新邀请活动成功: id={}, name={}", activity.getId(), activity.getActivityName());
|
|
|
+ return activity;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public boolean publish(Long id) {
|
|
|
+ InviteActivity activity = this.getById(id);
|
|
|
+ if (activity == null || activity.getDeleted() == 1) {
|
|
|
+ throw new BusinessException(404, "邀请活动不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!ActivityStatusEnum.canPublish(activity.getStatus())) {
|
|
|
+ throw new BusinessException(400, "当前状态不允许发布");
|
|
|
+ }
|
|
|
+
|
|
|
+ LocalDateTime now = LocalDateTime.now();
|
|
|
+ int newStatus = (now.isAfter(activity.getStartTime()) && now.isBefore(activity.getEndTime()))
|
|
|
+ ? ActivityStatusEnum.ONGOING.getCode()
|
|
|
+ : ActivityStatusEnum.PUBLISHED.getCode();
|
|
|
+
|
|
|
+ boolean result = inviteActivityMapper.updateStatus(id, newStatus) > 0;
|
|
|
+
|
|
|
+ if (result) {
|
|
|
+ log.info("发布邀请活动成功: id={}, status={}", id, newStatus);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public boolean pause(Long id) {
|
|
|
+ InviteActivity activity = this.getById(id);
|
|
|
+ if (activity == null || activity.getDeleted() == 1) {
|
|
|
+ throw new BusinessException(404, "邀请活动不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!ActivityStatusEnum.canPause(activity.getStatus())) {
|
|
|
+ throw new BusinessException(400, "当前状态不允许暂停");
|
|
|
+ }
|
|
|
+
|
|
|
+ boolean result = inviteActivityMapper.updateStatus(id, ActivityStatusEnum.PAUSED.getCode()) > 0;
|
|
|
+
|
|
|
+ if (result) {
|
|
|
+ log.info("暂停邀请活动成功: id={}", id);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public boolean resume(Long id) {
|
|
|
+ InviteActivity activity = this.getById(id);
|
|
|
+ if (activity == null || activity.getDeleted() == 1) {
|
|
|
+ throw new BusinessException(404, "邀请活动不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!ActivityStatusEnum.canResume(activity.getStatus())) {
|
|
|
+ throw new BusinessException(400, "当前状态不允许恢复");
|
|
|
+ }
|
|
|
+
|
|
|
+ boolean result = inviteActivityMapper.updateStatus(id, ActivityStatusEnum.ONGOING.getCode()) > 0;
|
|
|
+
|
|
|
+ if (result) {
|
|
|
+ log.info("恢复邀请活动成功: id={}", id);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public boolean deleteActivity(Long id) {
|
|
|
+ InviteActivity activity = this.getById(id);
|
|
|
+ if (activity == null) {
|
|
|
+ throw new BusinessException(404, "邀请活动不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!ActivityStatusEnum.canDelete(activity.getStatus())) {
|
|
|
+ throw new BusinessException(400, "当前状态不允许删除");
|
|
|
+ }
|
|
|
+
|
|
|
+ activity.setDeleted(1);
|
|
|
+ boolean result = this.updateById(activity);
|
|
|
+
|
|
|
+ if (result) {
|
|
|
+ log.info("删除邀请活动成功: id={}", id);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 邀请码相关 ====================
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String generateInviteCode(Long userId, Long activityId) {
|
|
|
+ // 获取活动信息以获取前缀配置
|
|
|
+ InviteActivity activity = inviteActivityMapper.selectById(activityId);
|
|
|
+ if (activity == null) {
|
|
|
+ throw new BusinessException(404, "邀请活动不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ String prefix = activity.getInviteCodePrefix();
|
|
|
+ LocalDateTime now = LocalDateTime.now();
|
|
|
+
|
|
|
+ // 构建邀请码组成部分
|
|
|
+ String timestamp = now.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
|
|
+ String userIdEncoded = encodeToBase62(userId);
|
|
|
+ String activityIdEncoded = encodeToBase62(activityId);
|
|
|
+ String randomPart = generateRandomString(4);
|
|
|
+
|
|
|
+ // 组装邀请码:前缀 + 日期 + 用户ID编码 + 活动ID编码 + 随机数
|
|
|
+ String inviteCode = String.format("%s%s%s%s%s",
|
|
|
+ prefix,
|
|
|
+ timestamp,
|
|
|
+ userIdEncoded,
|
|
|
+ activityIdEncoded,
|
|
|
+ randomPart);
|
|
|
+
|
|
|
+ log.info("生成邀请码成功: code={}, userId={}, activityId={}", inviteCode, userId, activityId);
|
|
|
+ return inviteCode;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public InviteActivity getOngoingActivity() {
|
|
|
+ LocalDateTime now = LocalDateTime.now();
|
|
|
+ List<InviteActivity> ongoingList = inviteActivityMapper.selectOngoing(now);
|
|
|
+ if (ongoingList == null || ongoingList.isEmpty()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ // 返回第一个进行中的活动
|
|
|
+ InviteActivity activity = ongoingList.get(0);
|
|
|
+ fillLabels(activity);
|
|
|
+ return activity;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 邀请关系绑定 ====================
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public boolean bindInviteRelation(Long activityId, Long inviterUserId, Long inviteeUserId,
|
|
|
+ String inviteCode, String sourceChannel, String deviceInfo, String ipAddress) {
|
|
|
+ // 1. 参数校验
|
|
|
+ if (inviterUserId.equals(inviteeUserId)) {
|
|
|
+ throw new BusinessException(400, "不能邀请自己");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 检查被邀请人是否已被邀请过
|
|
|
+ InviteRecord existingRecord = inviteRecordMapper.selectByInviteeUserId(inviteeUserId);
|
|
|
+ if (existingRecord != null) {
|
|
|
+ log.warn("该用户已被邀请过: inviteeUserId={}, existingRecordId={}", inviteeUserId, existingRecord.getId());
|
|
|
+ throw new BusinessException(400, "该用户已参与过邀请活动");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 检查活动是否在进行中
|
|
|
+ InviteActivity activity = this.getById(activityId);
|
|
|
+ if (activity == null || activity.getDeleted() == 1) {
|
|
|
+ throw new BusinessException(404, "邀请活动不存在");
|
|
|
+ }
|
|
|
+ if (activity.getStatus() != ActivityStatusEnum.ONGOING.getCode()) {
|
|
|
+ throw new BusinessException(400, "邀请活动未在进行中");
|
|
|
+ }
|
|
|
+ LocalDateTime now = LocalDateTime.now();
|
|
|
+ if (now.isBefore(activity.getStartTime()) || now.isAfter(activity.getEndTime())) {
|
|
|
+ throw new BusinessException(400, "邀请活动不在有效期内");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 执行防刷检查
|
|
|
+ boolean antiFraudPass = checkAntiFraud(inviterUserId, activityId, ipAddress, deviceInfo);
|
|
|
+ if (!antiFraudPass) {
|
|
|
+ throw new BusinessException(400, "邀请频率过高,请稍后再试");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 创建邀请记录
|
|
|
+ InviteRecord record = new InviteRecord();
|
|
|
+ record.setActivityId(activityId);
|
|
|
+ record.setInviterUserId(inviterUserId);
|
|
|
+ record.setInviteeUserId(inviteeUserId);
|
|
|
+ record.setInviteCode(inviteCode);
|
|
|
+ record.setStatus(InviteStatusEnum.PENDING.getCode()); // 待激活
|
|
|
+ record.setInviteTime(LocalDateTime.now());
|
|
|
+ record.setSourceChannel(sourceChannel);
|
|
|
+ record.setDeviceInfo(deviceInfo);
|
|
|
+ record.setIpAddress(ipAddress);
|
|
|
+ record.setRemark("");
|
|
|
+
|
|
|
+ inviteRecordMapper.insert(record);
|
|
|
+
|
|
|
+ // 6. 更新活动的总邀请数+1
|
|
|
+ inviteActivityMapper.incrementTotalInviteCount(activityId);
|
|
|
+
|
|
|
+ // 7. 发送新好友注册通知给邀请人(异步,不影响主流程)
|
|
|
+ try {
|
|
|
+ notificationService.sendNewInviteeNotification(inviterUserId, inviteeUserId.toString());
|
|
|
+ } catch (Exception notifyEx) {
|
|
|
+ log.error("发送新好友注册通知失败(不影响业务): inviterUserId={}, error={}",
|
|
|
+ inviterUserId, notifyEx.getMessage(), notifyEx);
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("绑定邀请关系成功: recordId={}, inviterUserId={}, inviteeUserId={}, inviteCode={}",
|
|
|
+ record.getId(), inviterUserId, inviteeUserId, inviteCode);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 邀请激活 ====================
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public boolean activateInviteRecord(Long inviteeUserId, Long orderId) {
|
|
|
+ // 1. 根据被邀请人ID查询待激活的邀请记录
|
|
|
+ LambdaQueryWrapper<InviteRecord> wrapper = new LambdaQueryWrapper<>();
|
|
|
+ wrapper.eq(InviteRecord::getInviteeUserId, inviteeUserId);
|
|
|
+ wrapper.eq(InviteRecord::getStatus, InviteStatusEnum.PENDING.getCode()); // 待激活状态
|
|
|
+ wrapper.orderByDesc(InviteRecord::getCreateTime);
|
|
|
+ wrapper.last("LIMIT 1");
|
|
|
+
|
|
|
+ InviteRecord record = inviteRecordMapper.selectOne(wrapper);
|
|
|
+ if (record == null) {
|
|
|
+ log.warn("未找到待激活的邀请记录: inviteeUserId={}", inviteeUserId);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 获取活动配置,检查是否要求首单
|
|
|
+ InviteActivity activity = this.getById(record.getActivityId());
|
|
|
+ if (activity == null) {
|
|
|
+ log.error("邀请记录关联的活动不存在: activityId={}", record.getActivityId());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // TODO: 这里可以添加订单校验逻辑,验证是否为首单以及金额是否达标
|
|
|
+ // 目前简化处理,直接激活
|
|
|
+
|
|
|
+ // 3. 更新邀请记录状态为已激活
|
|
|
+ int updateResult = inviteRecordMapper.updateToActivated(record.getId());
|
|
|
+ if (updateResult <= 0) {
|
|
|
+ log.error("更新邀请记录为已激活失败: recordId={}", record.getId());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 更新活动的有效邀请数+1
|
|
|
+ inviteActivityMapper.incrementValidInviteCount(record.getActivityId());
|
|
|
+
|
|
|
+ log.info("激活邀请记录成功: recordId={}, inviteeUserId={}, orderId={}",
|
|
|
+ record.getId(), inviteeUserId, orderId);
|
|
|
+
|
|
|
+ // 5. 异步发放奖励(或同步发放)
|
|
|
+ try {
|
|
|
+ distributeReward(record.getId());
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("发放奖励失败(不影响激活结果): recordId={}, error={}",
|
|
|
+ record.getId(), e.getMessage(), e);
|
|
|
+ // 奖励发放失败不影响激活结果,可以后续手动补发
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 奖励发放 ====================
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public boolean distributeReward(Long recordId) {
|
|
|
+ // 1. 根据记录ID查询邀请记录
|
|
|
+ InviteRecord record = inviteRecordMapper.selectById(recordId);
|
|
|
+ if (record == null) {
|
|
|
+ log.error("邀请记录不存在: recordId={}", recordId);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 检查记录状态是否为已激活
|
|
|
+ if (record.getStatus() != InviteStatusEnum.ACTIVATED.getCode()) {
|
|
|
+ log.warn("邀请记录状态不正确,无法发放奖励: recordId={}, status={}", recordId, record.getStatus());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 检查是否已经发放过奖励
|
|
|
+ InviteReward existingReward = inviteRewardMapper.selectByRecordId(recordId);
|
|
|
+ if (existingReward != null && existingReward.getStatus() == InviteRewardStatusEnum.DISTRIBUTED.getCode()) {
|
|
|
+ log.warn("奖励已发放,不能重复发放: recordId={}, rewardId={}", recordId, existingReward.getId());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 获取活动配置的优惠券模板ID
|
|
|
+ InviteActivity activity = this.getById(record.getActivityId());
|
|
|
+ if (activity == null) {
|
|
|
+ log.error("活动不存在: activityId={}", record.getActivityId());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 创建待发放的奖励记录(先创建记录,再发放优惠券)
|
|
|
+ InviteReward reward = new InviteReward();
|
|
|
+ reward.setActivityId(record.getActivityId());
|
|
|
+ reward.setRecordId(recordId);
|
|
|
+ reward.setInviterUserId(record.getInviterUserId());
|
|
|
+ reward.setCouponTemplateId(activity.getCouponTemplateId());
|
|
|
+ reward.setStatus(InviteRewardStatusEnum.PENDING.getCode()); // 待发放
|
|
|
+ reward.setUserCouponId(null);
|
|
|
+ reward.setCouponCode(null);
|
|
|
+ reward.setFailReason(null);
|
|
|
+ reward.setDistributeTime(null);
|
|
|
+ reward.setRetryCount(0);
|
|
|
+ reward.setOperatorId(null);
|
|
|
+ reward.setOperatorName(null);
|
|
|
+
|
|
|
+ inviteRewardMapper.insert(reward);
|
|
|
+
|
|
|
+ // 6. 调用UserCouponService发放优惠券给邀请人
|
|
|
+ try {
|
|
|
+ CouponDistributeDTO distributeDTO = new CouponDistributeDTO();
|
|
|
+ distributeDTO.setUserId(record.getInviterUserId());
|
|
|
+ distributeDTO.setTemplateId(activity.getCouponTemplateId());
|
|
|
+ distributeDTO.setSource("邀请活动奖励");
|
|
|
+ distributeDTO.setSourceId(String.valueOf(recordId));
|
|
|
+
|
|
|
+ userCouponService.distributeCoupon(distributeDTO, null, "系统自动发放");
|
|
|
+
|
|
|
+ // TODO: 需要从distributeCoupon返回值中获取userCouponId和couponCode
|
|
|
+ // 这里暂时设置为占位值,实际应根据返回值更新
|
|
|
+ Long userCouponId = -1L; // 应从实际发放结果获取
|
|
|
+ String couponCode = "AUTO_" + System.currentTimeMillis(); // 应从实际发放结果获取
|
|
|
+
|
|
|
+ // 7. 更新奖励记录为已发放
|
|
|
+ inviteRewardMapper.updateToDistributed(reward.getId(), userCouponId, couponCode);
|
|
|
+
|
|
|
+ // 8. 更新邀请记录状态为已奖励
|
|
|
+ inviteRecordMapper.updateToRewarded(recordId);
|
|
|
+
|
|
|
+ // 9. 更新活动的奖励发放数+1
|
|
|
+ inviteActivityMapper.incrementRewardCount(record.getActivityId());
|
|
|
+
|
|
|
+ // 10. 发送消息通知
|
|
|
+ try {
|
|
|
+ // 获取优惠券模板信息用于通知
|
|
|
+ CouponTemplate couponTemplate = couponTemplateService.getById(activity.getCouponTemplateId());
|
|
|
+ if (couponTemplate != null) {
|
|
|
+ notificationService.sendRewardSuccessNotification(
|
|
|
+ record.getInviterUserId(),
|
|
|
+ couponTemplate.getCouponName(),
|
|
|
+ couponTemplate.getDiscountValue()
|
|
|
+ );
|
|
|
+ }
|
|
|
+ } catch (Exception notifyEx) {
|
|
|
+ log.error("发送奖励通知失败(不影响业务): recordId={}, error={}",
|
|
|
+ recordId, notifyEx.getMessage(), notifyEx);
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("发放邀请奖励成功: recordId={}, rewardId={}, inviterUserId={}",
|
|
|
+ recordId, reward.getId(), record.getInviterUserId());
|
|
|
+ return true;
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ // 发放失败,更新奖励状态为失败
|
|
|
+ log.error("发放优惠券失败: recordId={}, error={}", recordId, e.getMessage(), e);
|
|
|
+ inviteRewardMapper.updateToFailed(reward.getId(), "优惠券发放失败: " + e.getMessage());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 防刷检查 ====================
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public boolean checkAntiFraud(Long inviterUserId, Long activityId, String ipAddress, String deviceInfo) {
|
|
|
+ // 1. 获取活动配置
|
|
|
+ InviteActivity activity = this.getById(activityId);
|
|
|
+ if (activity == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ LocalDate today = LocalDate.now();
|
|
|
+
|
|
|
+ // 2. 检查今日邀请数量是否超过每日限制
|
|
|
+ int todayInviteCount = inviteRecordMapper.countTodayInvites(inviterUserId, activityId, today);
|
|
|
+ if (todayInviteCount >= activity.getDailyLimit()) {
|
|
|
+ log.warn("今日邀请数量已达上限: userId={}, activityId={}, todayCount={}, dailyLimit={}",
|
|
|
+ inviterUserId, activityId, todayInviteCount, activity.getDailyLimit());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 检查总邀请数量是否超过总限制
|
|
|
+ int totalInviteCount = inviteRecordMapper.countTotalInvitesByUser(inviterUserId, activityId);
|
|
|
+ if (totalInviteCount >= activity.getTotalLimit()) {
|
|
|
+ log.warn("总邀请数量已达上限: userId={}, activityId{}, totalCount={}, totalLimit={}",
|
|
|
+ inviterUserId, activityId, totalInviteCount, activity.getTotalLimit());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 使用Redis检查IP频率限制(同一IP在1小时内最多邀请50次)
|
|
|
+ if (StringUtils.hasText(ipAddress)) {
|
|
|
+ String ipKey = REDIS_KEY_INVITE_IP_PREFIX + ipAddress;
|
|
|
+ Long ipCount = stringRedisTemplate.opsForValue().increment(ipKey);
|
|
|
+ if (ipCount != null && ipCount == 1) {
|
|
|
+ // 第一次设置,过期时间1小时
|
|
|
+ stringRedisTemplate.expire(ipKey, 1, TimeUnit.HOURS);
|
|
|
+ }
|
|
|
+ if (ipCount != null && ipCount > 50) {
|
|
|
+ log.warn("IP邀请频率过高: ipAddress={}, count={}", ipAddress, ipCount);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 使用Redis检查设备频率限制(同一设备在1小时内最多邀请30次)
|
|
|
+ if (StringUtils.hasText(deviceInfo)) {
|
|
|
+ String deviceKey = REDIS_KEY_INVITE_DEVICE_PREFIX + deviceInfo.hashCode();
|
|
|
+ Long deviceCount = stringRedisTemplate.opsForValue().increment(deviceKey);
|
|
|
+ if (deviceCount != null && deviceCount == 1) {
|
|
|
+ // 第一次设置,过期时间1小时
|
|
|
+ stringRedisTemplate.expire(deviceKey, 1, TimeUnit.HOURS);
|
|
|
+ }
|
|
|
+ if (deviceCount != null && deviceCount > 30) {
|
|
|
+ log.warn("设备邀请频率过高: deviceInfo={}, count={}", deviceInfo, deviceCount);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 所有检查通过
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 统计查询 ====================
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Map<String, Object> getActivityStatistics(Long activityId) {
|
|
|
+ Map<String, Object> stats = new HashMap<>();
|
|
|
+
|
|
|
+ // 1. 获取活动基本信息
|
|
|
+ InviteActivity activity = this.getById(activityId);
|
|
|
+ if (activity == null) {
|
|
|
+ throw new BusinessException(404, "邀请活动不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 从数据库统计各项指标
|
|
|
+ LocalDateTime now = LocalDateTime.now();
|
|
|
+ LocalDateTime todayStart = now.toLocalDate().atStartOfDay();
|
|
|
+ LocalDateTime todayEnd = todayStart.plusDays(1).minusNanos(1);
|
|
|
+
|
|
|
+ Map<String, Object> dbStats = inviteActivityMapper.selectStatisticsByActivityId(
|
|
|
+ activityId, todayStart, todayEnd);
|
|
|
+
|
|
|
+ // 3. 组装统计数据
|
|
|
+ stats.put("activityId", activityId);
|
|
|
+ stats.put("activityName", activity.getActivityName());
|
|
|
+ stats.put("status", activity.getStatus());
|
|
|
+ stats.put("statusLabel", InviteStatusEnum.getLabelByCode(activity.getStatus()).getLabel());
|
|
|
+ stats.put("startTime", activity.getStartTime());
|
|
|
+ stats.put("endTime", activity.getEndTime());
|
|
|
+ stats.put("totalInviteCount", activity.getTotalInviteCount()); // 总邀请数(从活动表)
|
|
|
+ stats.put("validInviteCount", activity.getValidInviteCount()); // 有效邀请数(从活动表)
|
|
|
+ stats.put("rewardCount", activity.getRewardCount()); // 奖励发放数(从活动表)
|
|
|
+
|
|
|
+ // 从查询结果补充详细统计
|
|
|
+ if (dbStats != null) {
|
|
|
+ stats.put("todayInvite", dbStats.get("todayInvite")); // 今日新增邀请数
|
|
|
+ stats.put("validRate", calculateRate(activity.getTotalInviteCount(), activity.getValidInviteCount())); // 有效率
|
|
|
+ stats.put("rewardRate", calculateRate(activity.getValidInviteCount(), activity.getRewardCount())); // 奖励发放率
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 获取Top邀请达人
|
|
|
+ List<Map<String, Object>> topInviters = inviteRecordMapper.selectTopInviters(activityId, 10);
|
|
|
+ stats.put("topInviters", topInviters);
|
|
|
+
|
|
|
+ // 5. 获取奖励发放统计
|
|
|
+ Map<String, Object> rewardStats = inviteRewardMapper.selectRewardStatistics(activityId);
|
|
|
+ if (rewardStats != null) {
|
|
|
+ stats.put("rewardDistributedCount", rewardStats.get("distributedCount"));
|
|
|
+ stats.put("rewardFailedCount", rewardStats.get("failedCount"));
|
|
|
+ }
|
|
|
+
|
|
|
+ return stats;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Map<String, Object> getMyInviteStatistics(Long userId, Long activityId) {
|
|
|
+ Map<String, Object> stats = new HashMap<>();
|
|
|
+
|
|
|
+ // 1. 统计当前用户的邀请数据
|
|
|
+ int totalInvites = inviteRecordMapper.countTotalInvitesByUser(userId, activityId);
|
|
|
+ LocalDate today = LocalDate.now();
|
|
|
+ int todayInvites = inviteRecordMapper.countTodayInvites(userId, activityId, today);
|
|
|
+
|
|
|
+ // 2. 统计各状态的邀请数量
|
|
|
+ LambdaQueryWrapper<InviteRecord> pendingWrapper = new LambdaQueryWrapper<>();
|
|
|
+ pendingWrapper.eq(InviteRecord::getInviterUserId, userId);
|
|
|
+ pendingWrapper.eq(InviteRecord::getActivityId, activityId);
|
|
|
+ pendingWrapper.eq(InviteRecord::getStatus, InviteStatusEnum.PENDING.getCode());
|
|
|
+ long pendingCount = inviteRecordMapper.selectCount(pendingWrapper);
|
|
|
+
|
|
|
+ LambdaQueryWrapper<InviteRecord> activatedWrapper = new LambdaQueryWrapper<>();
|
|
|
+ activatedWrapper.eq(InviteRecord::getInviterUserId, userId);
|
|
|
+ activatedWrapper.eq(InviteRecord::getActivityId, activityId);
|
|
|
+ activatedWrapper.eq(InviteRecord::getStatus, InviteStatusEnum.ACTIVATED.getCode());
|
|
|
+ long activatedCount = inviteRecordMapper.selectCount(activatedWrapper);
|
|
|
+
|
|
|
+ LambdaQueryWrapper<InviteRecord> rewardedWrapper = new LambdaQueryWrapper<>();
|
|
|
+ rewardedWrapper.eq(InviteRecord::getInviterUserId, userId);
|
|
|
+ rewardedWrapper.eq(InviteRecord::getActivityId, activityId);
|
|
|
+ rewardedWrapper.eq(InviteRecord::getStatus, InviteStatusEnum.REWARDED.getCode());
|
|
|
+ long rewardedCount = inviteRecordMapper.selectCount(rewardedWrapper);
|
|
|
+
|
|
|
+ // 3. 组装统计数据
|
|
|
+ stats.put("userId", userId);
|
|
|
+ stats.put("activityId", activityId);
|
|
|
+ stats.put("totalInvites", totalInvites); // 总邀请数
|
|
|
+ stats.put("todayInvites", todayInvites); // 今日邀请数
|
|
|
+ stats.put("pendingCount", pendingCount); // 待激活数量
|
|
|
+ stats.put("activatedCount", activatedCount); // 已激活数量
|
|
|
+ stats.put("rewardedCount", rewardedCount); // 已获奖励数量
|
|
|
+
|
|
|
+ return stats;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public IPage<InviteRecord> getMyInviteRecords(Long userId, Long activityId, Integer page, Integer pageSize) {
|
|
|
+ // 参数校验
|
|
|
+ if (page == null || page < 1) {
|
|
|
+ page = 1;
|
|
|
+ }
|
|
|
+ if (pageSize == null || pageSize < 1) {
|
|
|
+ pageSize = 10;
|
|
|
+ }
|
|
|
+ if (pageSize > 100) {
|
|
|
+ pageSize = 100;
|
|
|
+ }
|
|
|
+
|
|
|
+ int offset = (page - 1) * pageSize;
|
|
|
+
|
|
|
+ // 查询用户的邀请记录
|
|
|
+ List<InviteRecord> records = inviteRecordMapper.selectByInviterUserId(userId, activityId, offset, pageSize);
|
|
|
+
|
|
|
+ // 查询总数
|
|
|
+ LambdaQueryWrapper<InviteRecord> countWrapper = new LambdaQueryWrapper<>();
|
|
|
+ countWrapper.eq(InviteRecord::getInviterUserId, userId);
|
|
|
+ countWrapper.eq(InviteRecord::getActivityId, activityId);
|
|
|
+ Long total = inviteRecordMapper.selectCount(countWrapper);
|
|
|
+
|
|
|
+ // 组装分页结果
|
|
|
+ Page<InviteRecord> result = new Page<>(page, pageSize, total);
|
|
|
+ result.setRecords(records);
|
|
|
+
|
|
|
+ // 填充标签
|
|
|
+ records.forEach(record -> {
|
|
|
+ StatusLabel statusLabel = InviteStatusEnum.getLabelByCode(record.getStatus());
|
|
|
+ record.setStatusLabel(statusLabel.getLabel());
|
|
|
+ });
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public IPage<InviteRecord> getInviteRecords(InviteRecordQueryDTO queryDTO) {
|
|
|
+ queryDTO.validate();
|
|
|
+ Page<InviteRecord> page = new Page<>(queryDTO.getPage(), queryDTO.getPageSize());
|
|
|
+
|
|
|
+ LambdaQueryWrapper<InviteRecord> wrapper = new LambdaQueryWrapper<>();
|
|
|
+ if (queryDTO.getActivityId() != null) {
|
|
|
+ wrapper.eq(InviteRecord::getActivityId, queryDTO.getActivityId());
|
|
|
+ }
|
|
|
+ if (queryDTO.getInviterUserId() != null) {
|
|
|
+ wrapper.eq(InviteRecord::getInviterUserId, queryDTO.getInviterUserId());
|
|
|
+ }
|
|
|
+ if (queryDTO.getStatus() != null) {
|
|
|
+ wrapper.eq(InviteRecord::getStatus, queryDTO.getStatus());
|
|
|
+ }
|
|
|
+ if (queryDTO.getStartDate() != null) {
|
|
|
+ wrapper.ge(InviteRecord::getInviteTime, queryDTO.getStartDate());
|
|
|
+ }
|
|
|
+ if (queryDTO.getEndDate() != null) {
|
|
|
+ wrapper.le(InviteRecord::getInviteTime, queryDTO.getEndDate());
|
|
|
+ }
|
|
|
+ wrapper.orderByDesc(InviteRecord::getCreateTime);
|
|
|
+
|
|
|
+ IPage<InviteRecord> result = inviteRecordMapper.selectPage(page, wrapper);
|
|
|
+ result.getRecords().forEach(record -> {
|
|
|
+ StatusLabel statusLabel = InviteStatusEnum.getLabelByCode(record.getStatus());
|
|
|
+ record.setStatusLabel(statusLabel.getLabel());
|
|
|
+ });
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 手动补发奖励 ====================
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public boolean manualReissueReward(Long rewardId, Long operatorId, String operatorName) {
|
|
|
+ // 1. 查询失败的奖励记录
|
|
|
+ InviteReward reward = inviteRewardMapper.selectById(rewardId);
|
|
|
+ if (reward == null) {
|
|
|
+ throw new BusinessException(404, "奖励记录不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 检查状态是否为发放失败
|
|
|
+ if (reward.getStatus() != InviteRewardStatusEnum.FAILED.getCode()) {
|
|
|
+ throw new BusinessException(400, "只有发放失败的奖励才能补发");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 重新调用优惠券发放逻辑
|
|
|
+ try {
|
|
|
+ CouponDistributeDTO distributeDTO = new CouponDistributeDTO();
|
|
|
+ distributeDTO.setUserId(reward.getInviterUserId());
|
|
|
+ distributeDTO.setTemplateId(reward.getCouponTemplateId());
|
|
|
+ distributeDTO.setSource("邀请活动奖励-手动补发");
|
|
|
+ distributeDTO.setSourceId(String.valueOf(reward.getRecordId()));
|
|
|
+
|
|
|
+ userCouponService.distributeCoupon(distributeDTO, operatorId, operatorName);
|
|
|
+
|
|
|
+ // TODO: 应从实际发放结果获取userCouponId和couponCode
|
|
|
+ Long newUserCouponId = -2L; // 占位值
|
|
|
+ String newCouponCode = "MANUAL_" + System.currentTimeMillis(); // 占位值
|
|
|
+
|
|
|
+ // 4. 更新奖励状态为已补发
|
|
|
+ inviteRewardMapper.updateToRetried(rewardId, newUserCouponId, newCouponCode,
|
|
|
+ operatorId, operatorName);
|
|
|
+
|
|
|
+ log.info("手动补发奖励成功: rewardId={}, operatorId={}, operatorName={}",
|
|
|
+ rewardId, operatorId, operatorName);
|
|
|
+ return true;
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("手动补发奖励失败: rewardId={}, error={}", rewardId, e.getMessage(), e);
|
|
|
+ // 再次标记为失败,增加重试次数
|
|
|
+ inviteRewardMapper.updateToFailed(rewardId, "手动补发失败: " + e.getMessage());
|
|
|
+ throw new BusinessException(500, "补发奖励失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public InviteRecord getInviteRecordByCode(String inviteCode) {
|
|
|
+ return inviteRecordMapper.selectByInviteCode(inviteCode);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 私有工具方法 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将数字编码为Base62字符串
|
|
|
+ * 用于压缩用户ID和活动ID,缩短邀请码长度
|
|
|
+ *
|
|
|
+ * @param num 要编码的数字
|
|
|
+ * @return Base62编码后的字符串
|
|
|
+ */
|
|
|
+ private String encodeToBase62(long num) {
|
|
|
+ if (num == 0) {
|
|
|
+ return "0";
|
|
|
+ }
|
|
|
+
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
+ while (num > 0) {
|
|
|
+ sb.append(BASE62_CHARS.charAt((int) (num % 62)));
|
|
|
+ num /= 62;
|
|
|
+ }
|
|
|
+ return sb.reverse().toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成指定长度的随机字符串(由Base62字符组成)
|
|
|
+ *
|
|
|
+ * @param length 字符串长度
|
|
|
+ * @return 随机字符串
|
|
|
+ */
|
|
|
+ private String generateRandomString(int length) {
|
|
|
+ StringBuilder sb = new StringBuilder(length);
|
|
|
+ for (int i = 0; i < length; i++) {
|
|
|
+ sb.append(BASE62_CHARS.charAt(SECURE_RANDOM.nextInt(BASE62_CHARS.length())));
|
|
|
+ }
|
|
|
+ return sb.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算比率(百分比)
|
|
|
+ *
|
|
|
+ * @param total 总数
|
|
|
+ * @param part 部分
|
|
|
+ * @return 百分比字符串(如 "85.5%")
|
|
|
+ */
|
|
|
+ private String calculateRate(Integer total, Integer part) {
|
|
|
+ if (total == null || total == 0) {
|
|
|
+ return "0%";
|
|
|
+ }
|
|
|
+ double rate = (part.doubleValue() / total.doubleValue()) * 100;
|
|
|
+ return String.format("%.1f%%", rate);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 填充活动对象的标签字段(用于前端展示)
|
|
|
+ *
|
|
|
+ * @param activity 活动对象
|
|
|
+ */
|
|
|
+ private void fillLabels(InviteActivity activity) {
|
|
|
+ // 状态标签
|
|
|
+ StatusLabel statusLabel = ActivityStatusEnum.getLabelByCode(activity.getStatus());
|
|
|
+ activity.setStatusLabel(statusLabel.getLabel());
|
|
|
+
|
|
|
+ // 适用范围标签(如果有枚举的话,这里简单处理)
|
|
|
+ if (activity.getApplyScope() != null) {
|
|
|
+ activity.setApplyScopeLabel(activity.getApplyScope() == 1 ? "全平台" : "指定门店");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 商品范围标签
|
|
|
+ if (activity.getProductScope() != null) {
|
|
|
+ activity.setProductScopeLabel(activity.getProductScope() == 1 ? "全部商品" : "指定商品");
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|