|
|
@@ -10,8 +10,13 @@ import com.xuekairui.activity.entity.ActivityInviteRewardRecord;
|
|
|
import com.xuekairui.activity.mapper.ActivityInviteConfigMapper;
|
|
|
import com.xuekairui.activity.mapper.ActivityInviteRewardRecordMapper;
|
|
|
import com.xuekairui.common.activity.ActivityInviteGate;
|
|
|
+import com.xuekairui.invite.entity.InviteRelation;
|
|
|
+import com.xuekairui.invite.mapper.InviteRelationMapper;
|
|
|
+import com.xuekairui.user.entity.BusinessLicense;
|
|
|
import com.xuekairui.user.entity.User;
|
|
|
+import com.xuekairui.user.enums.LicenseStatus;
|
|
|
import com.xuekairui.user.enums.MembershipLevel;
|
|
|
+import com.xuekairui.user.mapper.BusinessLicenseMapper;
|
|
|
import com.xuekairui.user.mapper.UserMapper;
|
|
|
import com.xuekairui.user.service.MembershipService;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
@@ -21,7 +26,10 @@ import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
|
|
import java.time.LocalDateTime;
|
|
|
+import java.util.HashMap;
|
|
|
import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Optional;
|
|
|
|
|
|
/**
|
|
|
* 邀请有礼活动服务(统一管理所有邀请奖励发放)。
|
|
|
@@ -45,8 +53,6 @@ import java.util.List;
|
|
|
@RequiredArgsConstructor
|
|
|
public class ActivityInviteService implements ActivityInviteGate {
|
|
|
|
|
|
- /** 奖励触发模式:注册即发 */
|
|
|
- public static final String TRIGGER_REGISTRATION = "REGISTRATION";
|
|
|
/** 奖励触发模式:入驻审核通过后发 */
|
|
|
public static final String TRIGGER_LICENSE_APPROVED = "LICENSE_APPROVED";
|
|
|
|
|
|
@@ -54,43 +60,132 @@ public class ActivityInviteService implements ActivityInviteGate {
|
|
|
|
|
|
private final ActivityInviteConfigMapper configMapper;
|
|
|
private final ActivityInviteRewardRecordMapper rewardRecordMapper;
|
|
|
+ private final InviteRelationMapper inviteRelationMapper;
|
|
|
private final UserMapper userMapper;
|
|
|
+ private final BusinessLicenseMapper businessLicenseMapper;
|
|
|
private final MembershipService membershipService;
|
|
|
|
|
|
// ==========================================
|
|
|
// ActivityInviteGate 门面实现(供 InviteService 调用)
|
|
|
// ==========================================
|
|
|
|
|
|
+ /**
|
|
|
+ * 发放邀请奖励(统一入口,根据 rewardTrigger 配置决定策略)。
|
|
|
+ * <p>
|
|
|
+ * LICENSE_APPROVED:不发放,等待入驻审核通过后触发。
|
|
|
+ * REGISTRATION:立即发放,受活动时间窗和 30 次上限约束。
|
|
|
+ */
|
|
|
@Override
|
|
|
public void grantInviteReward(Long inviterId, Long inviteeId) {
|
|
|
ActivityInviteConfig config = getConfig();
|
|
|
|
|
|
- // 活动未开启 → 按原逻辑直接发奖,不受活动限制
|
|
|
+ // 活动未开启 → 不发放
|
|
|
if (!isActive(config)) {
|
|
|
- doGrantReward(inviterId, inviteeId, 30);
|
|
|
+ log.info("邀请有礼活动未开启,跳过发奖: inviter={}, invitee={}", inviterId, inviteeId);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // 活动开启 → 校验参与条件:邀请人需为小程序用户
|
|
|
- if (config.getRequireMiniappInviter() != null && config.getRequireMiniappInviter() == 1) {
|
|
|
- User inviter = userMapper.selectById(inviterId);
|
|
|
- if (inviter == null || inviter.getWechatMiniOpenId() == null) {
|
|
|
- log.info("邀请有礼活动:邀请人{}非小程序用户,跳过发奖: invitee={}", inviterId, inviteeId);
|
|
|
- return;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 活动开启 → 根据触发模式决定
|
|
|
+ // LICENSE_APPROVED 模式 → 不发放,等待入驻审核通过
|
|
|
if (TRIGGER_LICENSE_APPROVED.equalsIgnoreCase(config.getRewardTrigger())) {
|
|
|
- // LICENSE_APPROVED 模式:注册环节暂不发奖,等待入驻审核通过后由 onLicenseApproved 触发
|
|
|
log.info("邀请有礼活动(LICENSE_APPROVED模式),注册环节暂不发奖: inviter={}, invitee={}", inviterId, inviteeId);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // REGISTRATION 模式(默认):活动期间内注册即发奖,但受 30 次上限约束
|
|
|
+ // REGISTRATION 模式 → 立即发放
|
|
|
grantWithLimit(inviterId, inviteeId, config);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 已入驻用户补填邀请码时发放奖励(依赖活动配置)。
|
|
|
+ * <p>
|
|
|
+ * 场景:被邀请人已通过入驻审核,后补填邀请码 → 邀请人应获得奖励。
|
|
|
+ * 活动未开启则不发放。幂等由 uk_invitee 唯一索引保障。
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public void grantRewardForApprovedUser(Long inviterId, Long inviteeId) {
|
|
|
+ ActivityInviteConfig config = getConfig();
|
|
|
+ if (!isActive(config)) {
|
|
|
+ log.info("邀请有礼活动未开启,跳过已入驻补填发奖: inviter={}, invitee={}", inviterId, inviteeId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ grantWithLimit(inviterId, inviteeId, config);
|
|
|
+ log.info("已入驻用户补填邀请码,发放邀请奖励: inviter={}, invitee={}", inviterId, inviteeId);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 运营补发:对活动时间窗内的邀请关系批量补发奖励。
|
|
|
+ * <p>
|
|
|
+ * 场景:活动配置晚于实际邀请时间,运营配置活动后对窗口内的邀请进行补发。
|
|
|
+ * 仅对已入驻(审核通过)的被邀请人补发。
|
|
|
+ * 每个被邀请人只发一次(uk_invitee 兜底),邀请人受 30 次上限约束。
|
|
|
+ *
|
|
|
+ * @param startTime 活动开始时间(选填,默认取当前配置)
|
|
|
+ * @param endTime 活动结束时间(选填,默认取当前配置)
|
|
|
+ * @return {granted: 已补发数, skipped: 跳过数, total: 窗口内邀请总数}
|
|
|
+ */
|
|
|
+ @Transactional
|
|
|
+ public Map<String, Integer> retroactiveGrant(LocalDateTime startTime, LocalDateTime endTime) {
|
|
|
+ ActivityInviteConfig config = getConfig();
|
|
|
+ if (startTime == null) {
|
|
|
+ startTime = config.getStartTime();
|
|
|
+ }
|
|
|
+ if (endTime == null) {
|
|
|
+ endTime = config.getEndTime();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查询活动时间窗内的所有邀请关系
|
|
|
+ List<InviteRelation> relations = inviteRelationMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<InviteRelation>()
|
|
|
+ .ge(InviteRelation::getCreateTime, startTime)
|
|
|
+ .le(InviteRelation::getCreateTime, endTime)
|
|
|
+ .eq(InviteRelation::getStatus, 1));
|
|
|
+
|
|
|
+ int total = relations.size();
|
|
|
+ int granted = 0;
|
|
|
+ int skipped = 0;
|
|
|
+
|
|
|
+ for (InviteRelation r : relations) {
|
|
|
+ Long inviteeId = r.getInviteeId();
|
|
|
+ Long inviterId = r.getInviterId();
|
|
|
+
|
|
|
+ // 跳过无效数据
|
|
|
+ if (inviterId == null || inviteeId == null) {
|
|
|
+ skipped++;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否已发过奖(uk_invitee 唯一索引保障,此处提前过滤减少 DB 异常)
|
|
|
+ Long alreadyRewarded = rewardRecordMapper.selectCount(
|
|
|
+ new LambdaQueryWrapper<ActivityInviteRewardRecord>()
|
|
|
+ .eq(ActivityInviteRewardRecord::getInviteeId, inviteeId));
|
|
|
+ if (alreadyRewarded != null && alreadyRewarded > 0) {
|
|
|
+ skipped++;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查被邀请人是否已入驻审核通过
|
|
|
+ Long approvedCount = businessLicenseMapper.selectCount(
|
|
|
+ new LambdaQueryWrapper<BusinessLicense>()
|
|
|
+ .eq(BusinessLicense::getUserId, inviteeId)
|
|
|
+ .eq(BusinessLicense::getReviewStatus, LicenseStatus.APPROVED.name()));
|
|
|
+ if (approvedCount == null || approvedCount == 0) {
|
|
|
+ skipped++;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 补发奖励
|
|
|
+ grantWithLimit(inviterId, inviteeId, config);
|
|
|
+ granted++;
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Integer> result = new HashMap<>();
|
|
|
+ result.put("total", total);
|
|
|
+ result.put("granted", granted);
|
|
|
+ result.put("skipped", skipped);
|
|
|
+ log.info("补发邀请奖励完成: total={}, granted={}, skipped={}", total, granted, skipped);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
// ==========================================
|
|
|
// 配置管理
|
|
|
// ==========================================
|
|
|
@@ -102,8 +197,8 @@ public class ActivityInviteService implements ActivityInviteGate {
|
|
|
.id(CONFIG_ID).enabled(0)
|
|
|
.startTime(LocalDateTime.now())
|
|
|
.endTime(LocalDateTime.now())
|
|
|
- .inviterRewardDays(30).inviteeRewardDays(30).maxInviterReward(30)
|
|
|
- .rewardTrigger(TRIGGER_REGISTRATION).requireMiniappInviter(0)
|
|
|
+ .inviterRewardDays(30).maxInviterReward(30)
|
|
|
+ .rewardTrigger(TRIGGER_LICENSE_APPROVED).requireMiniappInviter(0)
|
|
|
.build();
|
|
|
}
|
|
|
return config;
|
|
|
@@ -115,12 +210,11 @@ public class ActivityInviteService implements ActivityInviteGate {
|
|
|
config.setEnabled(request.getEnabled());
|
|
|
config.setStartTime(request.getStartTime());
|
|
|
config.setEndTime(request.getEndTime());
|
|
|
- if (request.getInviterRewardDays() != null) config.setInviterRewardDays(request.getInviterRewardDays());
|
|
|
- if (request.getInviteeRewardDays() != null) config.setInviteeRewardDays(request.getInviteeRewardDays());
|
|
|
- if (request.getMaxInviterReward() != null) config.setMaxInviterReward(request.getMaxInviterReward());
|
|
|
- if (request.getRewardTrigger() != null) config.setRewardTrigger(request.getRewardTrigger());
|
|
|
- if (request.getRequireMiniappInviter() != null) config.setRequireMiniappInviter(request.getRequireMiniappInviter());
|
|
|
- if (request.getRemark() != null) config.setRemark(request.getRemark());
|
|
|
+ Optional.ofNullable(request.getInviterRewardDays()).ifPresent(config::setInviterRewardDays);
|
|
|
+ Optional.ofNullable(request.getMaxInviterReward()).ifPresent(config::setMaxInviterReward);
|
|
|
+ Optional.ofNullable(request.getRewardTrigger()).ifPresent(config::setRewardTrigger);
|
|
|
+ Optional.ofNullable(request.getRequireMiniappInviter()).ifPresent(config::setRequireMiniappInviter);
|
|
|
+ Optional.ofNullable(request.getRemark()).ifPresent(config::setRemark);
|
|
|
if (config.getId() == null) {
|
|
|
config.setId(CONFIG_ID);
|
|
|
configMapper.insert(config);
|
|
|
@@ -133,30 +227,30 @@ public class ActivityInviteService implements ActivityInviteGate {
|
|
|
}
|
|
|
|
|
|
// ==========================================
|
|
|
- // 事件处理:入驻审核通过后发放邀请人奖励(LICENSE_APPROVED 模式专用)
|
|
|
+ // 事件处理:入驻审核通过后发放邀请人奖励
|
|
|
// ==========================================
|
|
|
|
|
|
/**
|
|
|
- * 入驻审核通过后发放邀请人奖励。
|
|
|
+ * 入驻审核通过后发放邀请人奖励(依赖活动配置,不再区分触发模式)。
|
|
|
* <p>
|
|
|
- * 幂等:t_activity_invite_reward_record 的 uk_invitee 唯一索引保证同一被邀请人仅奖励一次。
|
|
|
+ * 活动未开启则不发放。幂等由 uk_invitee 唯一索引保障。
|
|
|
*/
|
|
|
@Transactional
|
|
|
- public void onLicenseApproved(Long userId, Long licenseId, String licenseType) {
|
|
|
- ActivityInviteConfig config = getConfig();
|
|
|
- if (!isActive(config) || !TRIGGER_LICENSE_APPROVED.equalsIgnoreCase(config.getRewardTrigger())) {
|
|
|
+ public void onLicenseApproved(Long userId) {
|
|
|
+ User invitee = userMapper.selectById(userId);
|
|
|
+ if (invitee == null || invitee.getInviterId() == null) {
|
|
|
+ log.debug("被邀请人无邀请人,跳过邀请奖励: inviteeId={}", userId);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- Long inviteeId = userId;
|
|
|
- User invitee = userMapper.selectById(inviteeId);
|
|
|
- if (invitee == null || invitee.getInviterId() == null) {
|
|
|
- log.debug("被邀请人无邀请人,跳过邀请奖励: inviteeId={}", inviteeId);
|
|
|
+ ActivityInviteConfig config = getConfig();
|
|
|
+ if (!isActive(config)) {
|
|
|
+ log.info("邀请有礼活动未开启,跳过入驻发奖: invitee={}", userId);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
Long inviterId = invitee.getInviterId();
|
|
|
- grantWithLimit(inviterId, inviteeId, config);
|
|
|
+ grantWithLimit(inviterId, userId, config);
|
|
|
}
|
|
|
|
|
|
// ==========================================
|
|
|
@@ -202,17 +296,6 @@ public class ActivityInviteService implements ActivityInviteGate {
|
|
|
return resultPage;
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 用户端:查询我作为邀请人获得的奖励记录。
|
|
|
- */
|
|
|
- public List<ActivityRewardRecordResponse> listMyInviteRewards(Long userId) {
|
|
|
- LambdaQueryWrapper<ActivityInviteRewardRecord> wrapper = new LambdaQueryWrapper<ActivityInviteRewardRecord>()
|
|
|
- .eq(ActivityInviteRewardRecord::getInviterId, userId)
|
|
|
- .orderByDesc(ActivityInviteRewardRecord::getCreateTime);
|
|
|
- List<ActivityInviteRewardRecord> records = rewardRecordMapper.selectList(wrapper);
|
|
|
- return toRewardResponseList(records);
|
|
|
- }
|
|
|
-
|
|
|
private List<ActivityRewardRecordResponse> toRewardResponseList(List<ActivityInviteRewardRecord> records) {
|
|
|
return records.stream()
|
|
|
.map(r -> ActivityRewardRecordResponse.builder()
|