|
@@ -179,7 +179,9 @@ public class InviteService {
|
|
|
|
|
|
|
|
// 5. 创建新邀请码
|
|
// 5. 创建新邀请码
|
|
|
InviteConfig config = inviteConfigService.getActiveConfig();
|
|
InviteConfig config = inviteConfigService.getActiveConfig();
|
|
|
- LocalDateTime expireTime = LocalDateTime.now().plusDays(config.getInviteCodeExpireDays());
|
|
|
|
|
|
|
+ int expireDays = config.getInviteCodeExpireDays() != null ? config.getInviteCodeExpireDays() : 0;
|
|
|
|
|
+ LocalDateTime expireTime = expireDays > 0
|
|
|
|
|
+ ? LocalDateTime.now().plusDays(expireDays) : null;
|
|
|
String code = generateUniqueCode();
|
|
String code = generateUniqueCode();
|
|
|
InviteCode newCode = InviteCode.builder()
|
|
InviteCode newCode = InviteCode.builder()
|
|
|
.userId(userId)
|
|
.userId(userId)
|
|
@@ -320,7 +322,7 @@ public class InviteService {
|
|
|
.wechatRedirectUrl(config.getWechatRedirectUrl())
|
|
.wechatRedirectUrl(config.getWechatRedirectUrl())
|
|
|
.dingtalkAppId(config.getDingtalkAppId())
|
|
.dingtalkAppId(config.getDingtalkAppId())
|
|
|
.feishuAppId(config.getFeishuAppId())
|
|
.feishuAppId(config.getFeishuAppId())
|
|
|
- .rewardPerRegister(config.getRewardMonths() != null ? config.getRewardMonths() : 1)
|
|
|
|
|
|
|
+ .rewardPerRegister(getInviterRewardMonths())
|
|
|
// 新增:自动填入支持
|
|
// 新增:自动填入支持
|
|
|
.autoFillSupported(true)
|
|
.autoFillSupported(true)
|
|
|
.urlScheme(urlScheme)
|
|
.urlScheme(urlScheme)
|
|
@@ -651,7 +653,17 @@ public class InviteService {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 5.1 奖励统一由活动模块处理(ActivityInviteGate),此处不再做上限校验
|
|
|
|
|
|
|
+ // 5.1 每日邀请上限校验(邀请配置,防滥用;总人数不限,奖励由活动配置 maxInviterReward 控制)
|
|
|
|
|
+ int maxPerDay = config.getMaxInvitePerDay() != null ? config.getMaxInvitePerDay() : 10;
|
|
|
|
|
+ LocalDateTime todayStart = LocalDate.now().atStartOfDay();
|
|
|
|
|
+ Long todayInvites = inviteRelationMapper.selectCount(
|
|
|
|
|
+ new LambdaQueryWrapper<InviteRelation>()
|
|
|
|
|
+ .eq(InviteRelation::getInviterId, inviterId)
|
|
|
|
|
+ .ge(InviteRelation::getCreateTime, todayStart));
|
|
|
|
|
+ if (todayInvites != null && todayInvites >= maxPerDay) {
|
|
|
|
|
+ log.warn("邀请人{}已达每日邀请上限{},拒绝创建邀请: invitee={}", inviterId, maxPerDay, inviteeId);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
// 6. 创建邀请关系(rewardGranted 初始为 0,由活动模块实际发奖后回写)
|
|
// 6. 创建邀请关系(rewardGranted 初始为 0,由活动模块实际发奖后回写)
|
|
|
InviteRelation relation = InviteRelation.builder()
|
|
InviteRelation relation = InviteRelation.builder()
|
|
|
.inviterId(inviterId)
|
|
.inviterId(inviterId)
|
|
@@ -770,8 +782,17 @@ public class InviteService {
|
|
|
throw new BusinessException(ErrorCode.BUSINESS_ERROR, "邀请功能已停用");
|
|
throw new BusinessException(ErrorCode.BUSINESS_ERROR, "邀请功能已停用");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 7.1 奖励统一由活动模块处理,此处不再做上限校验
|
|
|
|
|
-
|
|
|
|
|
|
|
+ // 7.1 每日邀请上限校验(邀请配置,防滥用;总人数不限)
|
|
|
|
|
+ int maxPerDay = config.getMaxInvitePerDay() != null ? config.getMaxInvitePerDay() : 10;
|
|
|
|
|
+ LocalDateTime todayStart = LocalDate.now().atStartOfDay();
|
|
|
|
|
+ Long todayInvites = inviteRelationMapper.selectCount(
|
|
|
|
|
+ new LambdaQueryWrapper<InviteRelation>()
|
|
|
|
|
+ .eq(InviteRelation::getInviterId, inviterId)
|
|
|
|
|
+ .ge(InviteRelation::getCreateTime, todayStart));
|
|
|
|
|
+ if (todayInvites != null && todayInvites >= maxPerDay) {
|
|
|
|
|
+ throw new BusinessException(ErrorCode.BUSINESS_ERROR,
|
|
|
|
|
+ "今日邀请已达上限(" + maxPerDay + "人),请明天再试");
|
|
|
|
|
+ }
|
|
|
// 8. 创建邀请关系(rewardGranted 初始为 0,由活动模块实际发奖后回写)
|
|
// 8. 创建邀请关系(rewardGranted 初始为 0,由活动模块实际发奖后回写)
|
|
|
InviteRelation relation = InviteRelation.builder()
|
|
InviteRelation relation = InviteRelation.builder()
|
|
|
.inviterId(inviterId)
|
|
.inviterId(inviterId)
|
|
@@ -919,24 +940,34 @@ public class InviteService {
|
|
|
.summaryText(summaryText)
|
|
.summaryText(summaryText)
|
|
|
.todayInvited(todayInvited.intValue())
|
|
.todayInvited(todayInvited.intValue())
|
|
|
.todayRemaining(todayRemaining)
|
|
.todayRemaining(todayRemaining)
|
|
|
- .rewardPerInvite(config.getRewardMonths() != null ? config.getRewardMonths() : 1)
|
|
|
|
|
|
|
+ .rewardPerInvite(getInviterRewardMonths())
|
|
|
.crawlerTodayRemaining(crawlerTodayRemaining)
|
|
.crawlerTodayRemaining(crawlerTodayRemaining)
|
|
|
.crawlerQuotaFromInvites(crawlerQuotaFromInvites)
|
|
.crawlerQuotaFromInvites(crawlerQuotaFromInvites)
|
|
|
.build();
|
|
.build();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public List<InviteRewardResponse> getInviteRewards(Long userId) {
|
|
public List<InviteRewardResponse> getInviteRewards(Long userId) {
|
|
|
- return getInviteRewards(userId, 1, Integer.MAX_VALUE).getRecords();
|
|
|
|
|
|
|
+ return getInviteRewards(userId, null, 1, Integer.MAX_VALUE).getRecords();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 分页查询邀请记录(批量查询被邀请人,避免 N+1)
|
|
* 分页查询邀请记录(批量查询被邀请人,避免 N+1)
|
|
|
*/
|
|
*/
|
|
|
- public Page<InviteRewardResponse> getInviteRewards(Long userId, int pageNum, int pageSize) {
|
|
|
|
|
|
|
+ public Page<InviteRewardResponse> getInviteRewards(Long userId, Integer rewardGranted,
|
|
|
|
|
+ int pageNum, int pageSize) {
|
|
|
Page<InviteRelation> page = new Page<>(pageNum, pageSize);
|
|
Page<InviteRelation> page = new Page<>(pageNum, pageSize);
|
|
|
- LambdaQueryWrapper<InviteRelation> wrapper = new LambdaQueryWrapper<InviteRelation>()
|
|
|
|
|
- .eq(InviteRelation::getInviterId, userId)
|
|
|
|
|
- .orderByDesc(InviteRelation::getCreateTime);
|
|
|
|
|
|
|
+ LambdaQueryWrapper<InviteRelation> wrapper = new LambdaQueryWrapper<>();
|
|
|
|
|
+ if (userId != null) {
|
|
|
|
|
+ wrapper.eq(InviteRelation::getInviterId, userId);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (rewardGranted != null) {
|
|
|
|
|
+ // 改用奖励记录表判定(与发奖在同一事务,无延迟)
|
|
|
|
|
+ String sql = (rewardGranted == 1)
|
|
|
|
|
+ ? "invitee_id IN (SELECT invitee_id FROM t_activity_invite_reward_record)"
|
|
|
|
|
+ : "invitee_id NOT IN (SELECT invitee_id FROM t_activity_invite_reward_record)";
|
|
|
|
|
+ wrapper.apply(sql);
|
|
|
|
|
+ }
|
|
|
|
|
+ wrapper.orderByDesc(InviteRelation::getCreateTime);
|
|
|
Page<InviteRelation> result = inviteRelationMapper.selectPage(page, wrapper);
|
|
Page<InviteRelation> result = inviteRelationMapper.selectPage(page, wrapper);
|
|
|
|
|
|
|
|
List<InviteRelation> relations = result.getRecords();
|
|
List<InviteRelation> relations = result.getRecords();
|
|
@@ -960,8 +991,16 @@ public class InviteService {
|
|
|
Map<Long, String> licenseStatusMap = businessLicenseService.getLicenseStatusByUserIds(inviteeIds);
|
|
Map<Long, String> licenseStatusMap = businessLicenseService.getLicenseStatusByUserIds(inviteeIds);
|
|
|
Map<Long, String> storeNameMap = businessLicenseService.getStoreNameByUserIds(inviteeIds);
|
|
Map<Long, String> storeNameMap = businessLicenseService.getStoreNameByUserIds(inviteeIds);
|
|
|
|
|
|
|
|
- // 邀请人信息只查一次
|
|
|
|
|
- String inviterNickname = getUserNickname(userId);
|
|
|
|
|
|
|
+ // 批量查邀请人信息(查全部时邀请人不同,需按记录取对应昵称)
|
|
|
|
|
+ List<Long> inviterIds = relations.stream()
|
|
|
|
|
+ .map(InviteRelation::getInviterId).distinct().toList();
|
|
|
|
|
+ Map<Long, String> inviterNicknameMap = new HashMap<>();
|
|
|
|
|
+ if (!inviterIds.isEmpty()) {
|
|
|
|
|
+ List<User> inviters = userMapper.selectByIds(inviterIds);
|
|
|
|
|
+ for (User inv : inviters) {
|
|
|
|
|
+ inviterNicknameMap.put(inv.getId(), getDisplayName(inv));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
List<InviteRewardResponse> list = relations.stream().map(r -> {
|
|
List<InviteRewardResponse> list = relations.stream().map(r -> {
|
|
|
boolean isRegistered = r.getRegistered() != null && r.getRegistered() == 1;
|
|
boolean isRegistered = r.getRegistered() != null && r.getRegistered() == 1;
|
|
@@ -975,7 +1014,7 @@ public class InviteService {
|
|
|
|
|
|
|
|
return InviteRewardResponse.builder()
|
|
return InviteRewardResponse.builder()
|
|
|
.inviterId(r.getInviterId())
|
|
.inviterId(r.getInviterId())
|
|
|
- .inviterNickname(inviterNickname)
|
|
|
|
|
|
|
+ .inviterNickname(inviterNicknameMap.getOrDefault(r.getInviterId(), "一位药店用户"))
|
|
|
.inviteeId(r.getInviteeId())
|
|
.inviteeId(r.getInviteeId())
|
|
|
.inviteeNickname(inviteeNickname)
|
|
.inviteeNickname(inviteeNickname)
|
|
|
.inviteePhone(inviteePhone)
|
|
.inviteePhone(inviteePhone)
|
|
@@ -1109,10 +1148,18 @@ public class InviteService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 根据用户ID获取展示名
|
|
|
|
|
|
|
+ * 获取活动配置的邀请人奖励天数(通过 ActivityInviteGate,默认30天=1个月)
|
|
|
*/
|
|
*/
|
|
|
- private String getUserNickname(Long userId) {
|
|
|
|
|
- return getDisplayName(userMapper.selectById(userId));
|
|
|
|
|
|
|
+ private int getInviterRewardDays() {
|
|
|
|
|
+ if (activityInviteGate != null) {
|
|
|
|
|
+ return activityInviteGate.getInviterRewardDays();
|
|
|
|
|
+ }
|
|
|
|
|
+ return 30;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 用户端展示:奖励天数转为月数(30天=1个月) */
|
|
|
|
|
+ private int getInviterRewardMonths() {
|
|
|
|
|
+ return Math.max(1, getInviterRewardDays() / 30);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -1140,7 +1187,7 @@ public class InviteService {
|
|
|
User inviter = userMapper.selectById(code.getUserId());
|
|
User inviter = userMapper.selectById(code.getUserId());
|
|
|
String inviterNickname = getDisplayName(inviter);
|
|
String inviterNickname = getDisplayName(inviter);
|
|
|
String appName = config.getAppName() != null ? config.getAppName() : "智价云(药店版)";
|
|
String appName = config.getAppName() != null ? config.getAppName() : "智价云(药店版)";
|
|
|
- int rewardMonths = config.getRewardMonths() != null ? config.getRewardMonths() : 1;
|
|
|
|
|
|
|
+ int rewardMonths = getInviterRewardMonths();
|
|
|
|
|
|
|
|
// 根据登录来源生成不同的邀请链接
|
|
// 根据登录来源生成不同的邀请链接
|
|
|
String inviteLink;
|
|
String inviteLink;
|
|
@@ -1311,7 +1358,80 @@ public class InviteService {
|
|
|
// ==========================================
|
|
// ==========================================
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 获取用户邀请转化统计(点击-注册转化率)
|
|
|
|
|
|
|
+ * 全员邀请转化统计(按邀请人分组,分页)
|
|
|
|
|
+ */
|
|
|
|
|
+ public Page<InviteConversionStatsResponse> getAllConversionStats(int pageNum, int pageSize,
|
|
|
|
|
+ Long userId, String phone) {
|
|
|
|
|
+ // 解析手机号为 userId
|
|
|
|
|
+ Long filterUserId = userId;
|
|
|
|
|
+ if (filterUserId == null && phone != null && !phone.isBlank()) {
|
|
|
|
|
+ User u = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, phone));
|
|
|
|
|
+ if (u != null) filterUserId = u.getId();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ long total;
|
|
|
|
|
+ List<Map<String, Object>> inviterStats;
|
|
|
|
|
+ if (filterUserId != null) {
|
|
|
|
|
+ // 查单个邀请人
|
|
|
|
|
+ Map<String, Object> stat = inviteRelationMapper.statsByInviter(filterUserId);
|
|
|
|
|
+ inviterStats = stat != null ? List.of(stat) : List.of();
|
|
|
|
|
+ total = inviterStats.size();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ int offset = (pageNum - 1) * pageSize;
|
|
|
|
|
+ inviterStats = inviteRelationMapper.groupByInviter(offset, pageSize);
|
|
|
|
|
+ total = inviteRelationMapper.countDistinctInviters();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ List<Long> inviterIds = inviterStats.stream()
|
|
|
|
|
+ .map(m -> (Long) m.get("inviter_id")).distinct().toList();
|
|
|
|
|
+ Map<Long, User> userMap = inviterIds.isEmpty() ? Map.of()
|
|
|
|
|
+ : userMapper.selectByIds(inviterIds).stream()
|
|
|
|
|
+ .collect(Collectors.toMap(User::getId, u -> u));
|
|
|
|
|
+ Map<Long, String> codeMap = new HashMap<>();
|
|
|
|
|
+ Map<Long, Integer> clickMap = new HashMap<>();
|
|
|
|
|
+ if (!inviterIds.isEmpty()) {
|
|
|
|
|
+ List<InviteCode> codes = inviteCodeMapper.selectList(
|
|
|
|
|
+ new LambdaQueryWrapper<InviteCode>().in(InviteCode::getUserId, inviterIds));
|
|
|
|
|
+ for (InviteCode c : codes) {
|
|
|
|
|
+ codeMap.put(c.getUserId(), c.getCode());
|
|
|
|
|
+ clickMap.put(c.getUserId(), c.getClickCount());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ List<InviteConversionStatsResponse> list = inviterStats.stream().map(m -> {
|
|
|
|
|
+ Long inviterId = (Long) m.get("inviter_id");
|
|
|
|
|
+ int totalInvited = ((Number) m.get("total_invited")).intValue();
|
|
|
|
|
+ int registered = ((Number) m.get("registered_count")).intValue();
|
|
|
|
|
+ int approved = ((Number) m.get("approved_count")).intValue();
|
|
|
|
|
+ int totalReward = ((Number) m.get("total_reward")).intValue();
|
|
|
|
|
+
|
|
|
|
|
+ User inviter = userMap.get(inviterId);
|
|
|
|
|
+ String nickname = inviter != null ? getDisplayName(inviter) : String.valueOf(inviterId);
|
|
|
|
|
+ int clicks = clickMap.getOrDefault(inviterId, 0);
|
|
|
|
|
+ double rate = totalInvited > 0
|
|
|
|
|
+ ? Math.round((double) registered / totalInvited * 10000.0) / 100.0 : 0.0;
|
|
|
|
|
+
|
|
|
|
|
+ return InviteConversionStatsResponse.builder()
|
|
|
|
|
+ .inviterId(inviterId)
|
|
|
|
|
+ .inviterNickname(nickname)
|
|
|
|
|
+ .inviteCode(codeMap.get(inviterId))
|
|
|
|
|
+ .clickedCount(clicks)
|
|
|
|
|
+ .totalInvited(totalInvited)
|
|
|
|
|
+ .registeredCount(registered)
|
|
|
|
|
+ .pendingCount(totalInvited - registered)
|
|
|
|
|
+ .approvedCount(approved)
|
|
|
|
|
+ .conversionRate(rate)
|
|
|
|
|
+ .totalReward(totalReward)
|
|
|
|
|
+ .build();
|
|
|
|
|
+ }).toList();
|
|
|
|
|
+
|
|
|
|
|
+ Page<InviteConversionStatsResponse> result = new Page<>(pageNum, pageSize, total);
|
|
|
|
|
+ result.setRecords(list);
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 单用户邀请转化统计(点击-注册转化率)
|
|
|
*/
|
|
*/
|
|
|
public InviteConversionStatsResponse getInviteConversionStats(Long userId, String phone, String loginSource) {
|
|
public InviteConversionStatsResponse getInviteConversionStats(Long userId, String phone, String loginSource) {
|
|
|
if (userId == null && phone != null && !phone.isBlank()) {
|
|
if (userId == null && phone != null && !phone.isBlank()) {
|
|
@@ -1347,6 +1467,14 @@ public class InviteService {
|
|
|
.count();
|
|
.count();
|
|
|
int pendingCount = totalInvited - registeredCount;
|
|
int pendingCount = totalInvited - registeredCount;
|
|
|
|
|
|
|
|
|
|
+ // 已入驻人数
|
|
|
|
|
+ int approvedCount = 0;
|
|
|
|
|
+ if (!relations.isEmpty()) {
|
|
|
|
|
+ List<Long> inviteeIds = relations.stream().map(InviteRelation::getInviteeId).toList();
|
|
|
|
|
+ approvedCount = (int) businessLicenseService.getLicenseStatusByUserIds(inviteeIds)
|
|
|
|
|
+ .values().stream().filter("APPROVED"::equals).count();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 计算邀请→注册转化率(注册人数/总邀请人数,不含点击数噪声)
|
|
// 计算邀请→注册转化率(注册人数/总邀请人数,不含点击数噪声)
|
|
|
double conversionRate = 0.0;
|
|
double conversionRate = 0.0;
|
|
|
if (totalInvited > 0) {
|
|
if (totalInvited > 0) {
|
|
@@ -1358,12 +1486,16 @@ public class InviteService {
|
|
|
.mapToInt(InviteRelation::getRewardGranted)
|
|
.mapToInt(InviteRelation::getRewardGranted)
|
|
|
.sum();
|
|
.sum();
|
|
|
|
|
|
|
|
|
|
+ User inviter = userMapper.selectById(userId);
|
|
|
return InviteConversionStatsResponse.builder()
|
|
return InviteConversionStatsResponse.builder()
|
|
|
|
|
+ .inviterId(userId)
|
|
|
|
|
+ .inviterNickname(inviter != null ? getDisplayName(inviter) : String.valueOf(userId))
|
|
|
.inviteCode(codeResponse.getCode())
|
|
.inviteCode(codeResponse.getCode())
|
|
|
.clickedCount(clickedCount)
|
|
.clickedCount(clickedCount)
|
|
|
.totalInvited(totalInvited)
|
|
.totalInvited(totalInvited)
|
|
|
.registeredCount(registeredCount)
|
|
.registeredCount(registeredCount)
|
|
|
.pendingCount(pendingCount)
|
|
.pendingCount(pendingCount)
|
|
|
|
|
+ .approvedCount(approvedCount)
|
|
|
.conversionRate(conversionRate)
|
|
.conversionRate(conversionRate)
|
|
|
.totalReward(totalReward)
|
|
.totalReward(totalReward)
|
|
|
.build();
|
|
.build();
|