|
@@ -18,6 +18,7 @@ import com.xuekairui.user.mapper.UserMapper;
|
|
|
import com.xuekairui.user.service.CrawlerQuotaGrantService;
|
|
import com.xuekairui.user.service.CrawlerQuotaGrantService;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.RequiredArgsConstructor;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.jetbrains.annotations.NotNull;
|
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
@@ -88,24 +89,33 @@ public class InviteService {
|
|
|
return toCodeResponse(existingCode);
|
|
return toCodeResponse(existingCode);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 创建新邀请码
|
|
|
|
|
|
|
+ // 创建新邀请码(uk_code 唯一索引兜底防止并发碰撞)
|
|
|
InviteConfig config = inviteConfigService.getActiveConfig();
|
|
InviteConfig config = inviteConfigService.getActiveConfig();
|
|
|
- String code = generateUniqueCode();
|
|
|
|
|
LocalDateTime expireTime = LocalDateTime.now().plusDays(config.getInviteCodeExpireDays());
|
|
LocalDateTime expireTime = LocalDateTime.now().plusDays(config.getInviteCodeExpireDays());
|
|
|
-
|
|
|
|
|
- InviteCode newCode = InviteCode.builder()
|
|
|
|
|
- .userId(userId)
|
|
|
|
|
- .code(code)
|
|
|
|
|
- .maxUses(-1)
|
|
|
|
|
- .usedCount(0)
|
|
|
|
|
- .clickCount(0)
|
|
|
|
|
- .expireTime(expireTime)
|
|
|
|
|
- .status(1)
|
|
|
|
|
- .build();
|
|
|
|
|
- inviteCodeMapper.insert(newCode);
|
|
|
|
|
-
|
|
|
|
|
- log.info("用户 {} 创建邀请码: {}", userId, code);
|
|
|
|
|
- return toCodeResponse(newCode);
|
|
|
|
|
|
|
+ int maxInsertRetries = 3;
|
|
|
|
|
+ for (int attempt = 0; attempt < maxInsertRetries; attempt++) {
|
|
|
|
|
+ String code = generateUniqueCode();
|
|
|
|
|
+ InviteCode newCode = InviteCode.builder()
|
|
|
|
|
+ .userId(userId)
|
|
|
|
|
+ .code(code)
|
|
|
|
|
+ .maxUses(-1)
|
|
|
|
|
+ .usedCount(0)
|
|
|
|
|
+ .clickCount(0)
|
|
|
|
|
+ .expireTime(expireTime)
|
|
|
|
|
+ .status(1)
|
|
|
|
|
+ .build();
|
|
|
|
|
+ try {
|
|
|
|
|
+ inviteCodeMapper.insert(newCode);
|
|
|
|
|
+ log.info("用户 {} 创建邀请码: {}", userId, code);
|
|
|
|
|
+ return toCodeResponse(newCode);
|
|
|
|
|
+ } catch (org.springframework.dao.DuplicateKeyException e) {
|
|
|
|
|
+ log.warn("邀请码并发碰撞(attempt={}): {}", attempt + 1, code);
|
|
|
|
|
+ if (attempt == maxInsertRetries - 1) {
|
|
|
|
|
+ throw new BusinessException(ErrorCode.BUSINESS_ERROR, "邀请码生成失败,请重试");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ throw new BusinessException(ErrorCode.BUSINESS_ERROR, "邀请码生成失败,请重试");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ==========================================
|
|
// ==========================================
|
|
@@ -119,9 +129,8 @@ public class InviteService {
|
|
|
*
|
|
*
|
|
|
* @param code 邀请码
|
|
* @param code 邀请码
|
|
|
* @param channel 渠道(可选,默认app)
|
|
* @param channel 渠道(可选,默认app)
|
|
|
- * @param userAgent 浏览器 User-Agent,用于判断桌面客户端下载包
|
|
|
|
|
*/
|
|
*/
|
|
|
- public InvitePageResponse getInvitePage(String code, String channel, String userAgent) {
|
|
|
|
|
|
|
+ public InvitePageResponse getInvitePage(String code, String channel) {
|
|
|
InviteCode inviteCode = inviteCodeMapper.selectOne(
|
|
InviteCode inviteCode = inviteCodeMapper.selectOne(
|
|
|
new LambdaQueryWrapper<InviteCode>()
|
|
new LambdaQueryWrapper<InviteCode>()
|
|
|
.eq(InviteCode::getCode, code));
|
|
.eq(InviteCode::getCode, code));
|
|
@@ -149,7 +158,7 @@ public class InviteService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 根据渠道确定打开方式
|
|
// 根据渠道确定打开方式
|
|
|
- String openType = determineOpenType(channel, config);
|
|
|
|
|
|
|
+ String openType = determineOpenType(channel);
|
|
|
|
|
|
|
|
// 构建小程序路径(含邀请码参数)
|
|
// 构建小程序路径(含邀请码参数)
|
|
|
String miniappPath = config.getMiniappPath();
|
|
String miniappPath = config.getMiniappPath();
|
|
@@ -242,14 +251,13 @@ public class InviteService {
|
|
|
/**
|
|
/**
|
|
|
* 根据渠道确定打开方式
|
|
* 根据渠道确定打开方式
|
|
|
*/
|
|
*/
|
|
|
- private String determineOpenType(String channel, InviteConfig config) {
|
|
|
|
|
|
|
+ private String determineOpenType(String channel) {
|
|
|
return switch (channel) {
|
|
return switch (channel) {
|
|
|
case "windows" -> "download"; // 下载Windows桌面客户端
|
|
case "windows" -> "download"; // 下载Windows桌面客户端
|
|
|
case "app" -> "download"; // 下载客户端(移动端通用)
|
|
case "app" -> "download"; // 下载客户端(移动端通用)
|
|
|
case "miniapp" -> "miniapp"; // 打开小程序
|
|
case "miniapp" -> "miniapp"; // 打开小程序
|
|
|
case "wechat" -> "redirect"; // 跳转公众号/H5
|
|
case "wechat" -> "redirect"; // 跳转公众号/H5
|
|
|
- case "dingtalk" -> "deeplink"; // 应用内深度链接
|
|
|
|
|
- case "feishu" -> "deeplink"; // 应用内深度链接
|
|
|
|
|
|
|
+ case "dingtalk", "feishu" -> "deeplink"; // 应用内深度链接
|
|
|
default -> "download";
|
|
default -> "download";
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
@@ -344,7 +352,7 @@ public class InviteService {
|
|
|
* 解析任意邀请链接或邀请码,返回邀请人信息
|
|
* 解析任意邀请链接或邀请码,返回邀请人信息
|
|
|
* 支持:
|
|
* 支持:
|
|
|
* 1. 纯邀请码:A3K7M9P2
|
|
* 1. 纯邀请码:A3K7M9P2
|
|
|
- * 2. 完整邀请链接:https://app.zhijiayun.com/invite/A3K7M9P2?channel=wechat
|
|
|
|
|
|
|
+ * 2. 完整邀请链接:{@code https://app.zhijiayun.com/invite/A3K7M9P2?channel=wechat}
|
|
|
* 3. 带查询参数的链接:...?inviteCode=A3K7M9P2 或 ?code=A3K7M9P2
|
|
* 3. 带查询参数的链接:...?inviteCode=A3K7M9P2 或 ?code=A3K7M9P2
|
|
|
*/
|
|
*/
|
|
|
public InviteLinkResolveResponse resolveInviteLink(String linkOrCode) {
|
|
public InviteLinkResolveResponse resolveInviteLink(String linkOrCode) {
|
|
@@ -375,6 +383,21 @@ public class InviteService {
|
|
|
String inviterAvatar = inviter != null ? inviter.getAvatar() : null;
|
|
String inviterAvatar = inviter != null ? inviter.getAvatar() : null;
|
|
|
|
|
|
|
|
InviteConfig config = inviteConfigService.getActiveConfig();
|
|
InviteConfig config = inviteConfigService.getActiveConfig();
|
|
|
|
|
+ String landingTitle = getString(config, inviter, inviterNickname);
|
|
|
|
|
+
|
|
|
|
|
+ return InviteLinkResolveResponse.builder()
|
|
|
|
|
+ .inviteCode(code)
|
|
|
|
|
+ .inviteLink(inviteBaseUrl + "/api/invite/" + code)
|
|
|
|
|
+ .inviterNickname(inviterNickname)
|
|
|
|
|
+ .inviterAvatar(inviterAvatar)
|
|
|
|
|
+ .appName(config.getAppName())
|
|
|
|
|
+ .landingTitle(landingTitle)
|
|
|
|
|
+ .landingDesc(config.getLandingDesc())
|
|
|
|
|
+ .build();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @NotNull
|
|
|
|
|
+ private static String getString(InviteConfig config, User inviter, String inviterNickname) {
|
|
|
String landingTitle = config.getLandingTitle();
|
|
String landingTitle = config.getLandingTitle();
|
|
|
if (landingTitle == null || landingTitle.isBlank()) {
|
|
if (landingTitle == null || landingTitle.isBlank()) {
|
|
|
String pharmacyName = inviter != null ? inviter.getPharmacyName() : null;
|
|
String pharmacyName = inviter != null ? inviter.getPharmacyName() : null;
|
|
@@ -392,16 +415,7 @@ public class InviteService {
|
|
|
landingTitle = landingTitle.replace("{pharmacy}", inviter.getPharmacyName());
|
|
landingTitle = landingTitle.replace("{pharmacy}", inviter.getPharmacyName());
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- return InviteLinkResolveResponse.builder()
|
|
|
|
|
- .inviteCode(code)
|
|
|
|
|
- .inviteLink(inviteBaseUrl + "/api/invite/" + code)
|
|
|
|
|
- .inviterNickname(inviterNickname)
|
|
|
|
|
- .inviterAvatar(inviterAvatar)
|
|
|
|
|
- .appName(config.getAppName())
|
|
|
|
|
- .landingTitle(landingTitle)
|
|
|
|
|
- .landingDesc(config.getLandingDesc())
|
|
|
|
|
- .build();
|
|
|
|
|
|
|
+ return landingTitle;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -409,16 +423,14 @@ public class InviteService {
|
|
|
* 支持:纯邀请码、完整URL路径、query参数、以及被截断/编码的URL
|
|
* 支持:纯邀请码、完整URL路径、query参数、以及被截断/编码的URL
|
|
|
*/
|
|
*/
|
|
|
private String extractInviteCode(String linkOrCode) {
|
|
private String extractInviteCode(String linkOrCode) {
|
|
|
- // 1. 纯邀请码:8位(去掉了易混淆的 I,O,0,1)
|
|
|
|
|
- if (linkOrCode.matches("^[A-HJ-NP-Z2-9]{8}$")) {
|
|
|
|
|
|
|
+ // 1. 纯邀请码精确匹配
|
|
|
|
|
+ if (CODE_PURE.matcher(linkOrCode).matches()) {
|
|
|
log.debug("提取邀请码[纯码匹配]: {}", linkOrCode);
|
|
log.debug("提取邀请码[纯码匹配]: {}", linkOrCode);
|
|
|
return linkOrCode;
|
|
return linkOrCode;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 2. 从路径 /invite/{code} 提取(兼容各种端口、域名)
|
|
|
|
|
- java.util.regex.Pattern pathPattern = java.util.regex.Pattern
|
|
|
|
|
- .compile("/invite/([A-HJ-NP-Z2-9]{8})(?:[?/#]|$)");
|
|
|
|
|
- java.util.regex.Matcher pathMatcher = pathPattern.matcher(linkOrCode);
|
|
|
|
|
|
|
+ // 2. 从路径 /invite/{code} 提取
|
|
|
|
|
+ java.util.regex.Matcher pathMatcher = CODE_PATH.matcher(linkOrCode);
|
|
|
if (pathMatcher.find()) {
|
|
if (pathMatcher.find()) {
|
|
|
String code = pathMatcher.group(1);
|
|
String code = pathMatcher.group(1);
|
|
|
log.debug("提取邀请码[路径匹配]: {} from {}", code, linkOrCode);
|
|
log.debug("提取邀请码[路径匹配]: {} from {}", code, linkOrCode);
|
|
@@ -426,17 +438,13 @@ public class InviteService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 3. 从查询参数 ?inviteCode=XXX 或 ?code=XXX 提取
|
|
// 3. 从查询参数 ?inviteCode=XXX 或 ?code=XXX 提取
|
|
|
- java.util.regex.Pattern queryPattern = java.util.regex.Pattern
|
|
|
|
|
- .compile("[?&](?:inviteCode|code)=([A-HJ-NP-Z2-9]{8})(?:&|$|#)");
|
|
|
|
|
- java.util.regex.Matcher queryMatcher = queryPattern.matcher(linkOrCode);
|
|
|
|
|
|
|
+ java.util.regex.Matcher queryMatcher = CODE_QUERY.matcher(linkOrCode);
|
|
|
if (queryMatcher.find()) {
|
|
if (queryMatcher.find()) {
|
|
|
return queryMatcher.group(1);
|
|
return queryMatcher.group(1);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 4. 兜底:从任意位置提取符合格式的8位邀请码(处理URL被截断、编码等异常情况)
|
|
|
|
|
- java.util.regex.Pattern fallbackPattern = java.util.regex.Pattern
|
|
|
|
|
- .compile("([A-HJ-NP-Z2-9]{8})");
|
|
|
|
|
- java.util.regex.Matcher fallbackMatcher = fallbackPattern.matcher(linkOrCode);
|
|
|
|
|
|
|
+ // 4. 兜底:从任意位置提取
|
|
|
|
|
+ java.util.regex.Matcher fallbackMatcher = CODE_FALLBACK.matcher(linkOrCode);
|
|
|
if (fallbackMatcher.find()) {
|
|
if (fallbackMatcher.find()) {
|
|
|
return fallbackMatcher.group(1);
|
|
return fallbackMatcher.group(1);
|
|
|
}
|
|
}
|
|
@@ -496,9 +504,9 @@ public class InviteService {
|
|
|
log.warn("用户 {} 已被邀请过, inviterId={}", inviteeId, invitee.getInviterId());
|
|
log.warn("用户 {} 已被邀请过, inviterId={}", inviteeId, invitee.getInviterId());
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
- Long existingCount = inviteRelationMapper.selectCount(
|
|
|
|
|
|
|
+ long existingCount = Optional.ofNullable(inviteRelationMapper.selectCount(
|
|
|
new LambdaQueryWrapper<InviteRelation>()
|
|
new LambdaQueryWrapper<InviteRelation>()
|
|
|
- .eq(InviteRelation::getInviteeId, inviteeId));
|
|
|
|
|
|
|
+ .eq(InviteRelation::getInviteeId, inviteeId))).orElse(0L);
|
|
|
if (existingCount > 0) {
|
|
if (existingCount > 0) {
|
|
|
log.warn("用户 {} 已有邀请关系(异常数据)", inviteeId);
|
|
log.warn("用户 {} 已有邀请关系(异常数据)", inviteeId);
|
|
|
return;
|
|
return;
|
|
@@ -512,10 +520,10 @@ public class InviteService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 5.1 检查邀请人总邀请奖励上限(仍可继续邀请,但超过上限后不再发放奖励)
|
|
// 5.1 检查邀请人总邀请奖励上限(仍可继续邀请,但超过上限后不再发放奖励)
|
|
|
- Long totalInvited = inviteRelationMapper.selectCount(
|
|
|
|
|
|
|
+ long totalInvited = Optional.ofNullable(inviteRelationMapper.selectCount(
|
|
|
new LambdaQueryWrapper<InviteRelation>()
|
|
new LambdaQueryWrapper<InviteRelation>()
|
|
|
.eq(InviteRelation::getInviterId, inviterId)
|
|
.eq(InviteRelation::getInviterId, inviterId)
|
|
|
- .eq(InviteRelation::getRegistered, 1));
|
|
|
|
|
|
|
+ .eq(InviteRelation::getRegistered, 1))).orElse(0L);
|
|
|
int maxTotal = config.getMaxTotalInvites() != null ? config.getMaxTotalInvites() : 30;
|
|
int maxTotal = config.getMaxTotalInvites() != null ? config.getMaxTotalInvites() : 30;
|
|
|
boolean rewardAllowed = totalInvited < maxTotal;
|
|
boolean rewardAllowed = totalInvited < maxTotal;
|
|
|
if (!rewardAllowed) {
|
|
if (!rewardAllowed) {
|
|
@@ -523,7 +531,7 @@ public class InviteService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 6. 创建邀请关系(直接标记为已注册,奖励类型为赠送会员时长)
|
|
// 6. 创建邀请关系(直接标记为已注册,奖励类型为赠送会员时长)
|
|
|
- Integer rewardMonths = config.getRewardMonths() != null ? config.getRewardMonths() : 1;
|
|
|
|
|
|
|
+ int rewardMonths = config.getRewardMonths() != null ? config.getRewardMonths() : 1;
|
|
|
InviteRelation relation = InviteRelation.builder()
|
|
InviteRelation relation = InviteRelation.builder()
|
|
|
.inviterId(inviterId)
|
|
.inviterId(inviterId)
|
|
|
.inviteeId(inviteeId)
|
|
.inviteeId(inviteeId)
|
|
@@ -607,7 +615,7 @@ public class InviteService {
|
|
|
throw new BusinessException(ErrorCode.INVITE_RELATION_EXISTS,
|
|
throw new BusinessException(ErrorCode.INVITE_RELATION_EXISTS,
|
|
|
"您已绑定过邀请码,您是由「" + inviterName + "」邀请的,无法重复绑定");
|
|
"您已绑定过邀请码,您是由「" + inviterName + "」邀请的,无法重复绑定");
|
|
|
}
|
|
}
|
|
|
- Long existingCount = inviteRelationMapper.selectCount(
|
|
|
|
|
|
|
+ long existingCount = inviteRelationMapper.selectCount(
|
|
|
new LambdaQueryWrapper<InviteRelation>()
|
|
new LambdaQueryWrapper<InviteRelation>()
|
|
|
.eq(InviteRelation::getInviteeId, userId));
|
|
.eq(InviteRelation::getInviteeId, userId));
|
|
|
if (existingCount > 0) {
|
|
if (existingCount > 0) {
|
|
@@ -640,7 +648,7 @@ public class InviteService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 7.1 检查邀请人总邀请奖励上限(仍可继续绑定,但超过上限后不再发放奖励)
|
|
// 7.1 检查邀请人总邀请奖励上限(仍可继续绑定,但超过上限后不再发放奖励)
|
|
|
- Long totalInvited = inviteRelationMapper.selectCount(
|
|
|
|
|
|
|
+ long totalInvited = inviteRelationMapper.selectCount(
|
|
|
new LambdaQueryWrapper<InviteRelation>()
|
|
new LambdaQueryWrapper<InviteRelation>()
|
|
|
.eq(InviteRelation::getInviterId, inviterId)
|
|
.eq(InviteRelation::getInviterId, inviterId)
|
|
|
.eq(InviteRelation::getRegistered, 1));
|
|
.eq(InviteRelation::getRegistered, 1));
|
|
@@ -648,7 +656,7 @@ public class InviteService {
|
|
|
boolean rewardAllowed = totalInvited < maxTotal;
|
|
boolean rewardAllowed = totalInvited < maxTotal;
|
|
|
|
|
|
|
|
// 8. 创建邀请关系
|
|
// 8. 创建邀请关系
|
|
|
- Integer rewardMonths = config.getRewardMonths() != null ? config.getRewardMonths() : 1;
|
|
|
|
|
|
|
+ int rewardMonths = config.getRewardMonths() != null ? config.getRewardMonths() : 1;
|
|
|
InviteRelation relation = InviteRelation.builder()
|
|
InviteRelation relation = InviteRelation.builder()
|
|
|
.inviterId(inviterId)
|
|
.inviterId(inviterId)
|
|
|
.inviteeId(userId)
|
|
.inviteeId(userId)
|
|
@@ -880,35 +888,56 @@ public class InviteService {
|
|
|
return crawlerQuotaGrantService.listGrants(userId, grantType);
|
|
return crawlerQuotaGrantService.listGrants(userId, grantType);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // ==========================================
|
|
|
|
|
+ // 邀请码常量
|
|
|
|
|
+ // ==========================================
|
|
|
|
|
+
|
|
|
|
|
+ /** 邀请码字符集(32个,去掉易混淆的 I/O/0/1) */
|
|
|
|
|
+ private static final String CODE_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
|
|
|
+
|
|
|
|
|
+ /** 邀请码正则片段:8位不含易混淆字符 */
|
|
|
|
|
+ private static final String CODE_RE = "[A-HJ-NP-Z2-9]{8}";
|
|
|
|
|
+
|
|
|
|
|
+ /** 纯邀请码精确匹配 */
|
|
|
|
|
+ private static final java.util.regex.Pattern CODE_PURE = java.util.regex.Pattern.compile("^" + CODE_RE + "$");
|
|
|
|
|
+ /** 从 URL 路径 /invite/{code} 提取 */
|
|
|
|
|
+ private static final java.util.regex.Pattern CODE_PATH =
|
|
|
|
|
+ java.util.regex.Pattern.compile("/invite/(" + CODE_RE + ")(?:[?/#]|$)");
|
|
|
|
|
+ /** 从 query 参数提取 */
|
|
|
|
|
+ private static final java.util.regex.Pattern CODE_QUERY =
|
|
|
|
|
+ java.util.regex.Pattern.compile("[?&](?:inviteCode|code)=(" + CODE_RE + ")(?:&|$|#)");
|
|
|
|
|
+ /** 从任意位置提取(兜底) */
|
|
|
|
|
+ private static final java.util.regex.Pattern CODE_FALLBACK =
|
|
|
|
|
+ java.util.regex.Pattern.compile("(" + CODE_RE + ")");
|
|
|
|
|
+
|
|
|
// ==========================================
|
|
// ==========================================
|
|
|
// 私有工具方法
|
|
// 私有工具方法
|
|
|
// ==========================================
|
|
// ==========================================
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 生成唯一邀请码(8位大写字母+数字)
|
|
* 生成唯一邀请码(8位大写字母+数字)
|
|
|
- * 字符集:ABCDEFGHJKLMNPQRSTUVWXYZ23456789(去掉易混淆字符 I,O,0,1)
|
|
|
|
|
- * 示例:A3K7M9P2、X5B8N4Q6
|
|
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 防重复:先查 DB(快速路径),再用 uk_code 唯一索引兜底。
|
|
|
|
|
+ * 32^8 ≈ 1.1万亿种组合,正常不会碰撞,此处为防御性设计。
|
|
|
*/
|
|
*/
|
|
|
private String generateUniqueCode() {
|
|
private String generateUniqueCode() {
|
|
|
- String chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // 32个字符,去掉易混淆的
|
|
|
|
|
java.security.SecureRandom random = new java.security.SecureRandom();
|
|
java.security.SecureRandom random = new java.security.SecureRandom();
|
|
|
- String code;
|
|
|
|
|
-
|
|
|
|
|
- do {
|
|
|
|
|
|
|
+ int maxRetries = 5;
|
|
|
|
|
+ for (int attempt = 0; attempt < maxRetries; attempt++) {
|
|
|
StringBuilder sb = new StringBuilder(8);
|
|
StringBuilder sb = new StringBuilder(8);
|
|
|
for (int i = 0; i < 8; i++) {
|
|
for (int i = 0; i < 8; i++) {
|
|
|
- sb.append(chars.charAt(random.nextInt(chars.length())));
|
|
|
|
|
|
|
+ sb.append(CODE_CHARS.charAt(random.nextInt(CODE_CHARS.length())));
|
|
|
}
|
|
}
|
|
|
- code = sb.toString();
|
|
|
|
|
- } while (inviteCodeMapper.selectCount(
|
|
|
|
|
- new LambdaQueryWrapper<InviteCode>().eq(InviteCode::getCode, code)) > 0);
|
|
|
|
|
-
|
|
|
|
|
- return code;
|
|
|
|
|
|
|
+ String code = sb.toString();
|
|
|
|
|
+ if (inviteCodeMapper.selectCount(
|
|
|
|
|
+ new LambdaQueryWrapper<InviteCode>().eq(InviteCode::getCode, code)) == 0) {
|
|
|
|
|
+ return code;
|
|
|
|
|
+ }
|
|
|
|
|
+ log.warn("邀请码碰撞(attempt={}): {}", attempt + 1, code);
|
|
|
|
|
+ }
|
|
|
|
|
+ throw new BusinessException(ErrorCode.BUSINESS_ERROR, "邀请码生成失败,请重试");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 获取用户昵称
|
|
|
|
|
- */
|
|
|
|
|
/**
|
|
/**
|
|
|
* 获取用户对外展示名:
|
|
* 获取用户对外展示名:
|
|
|
* 优先顺序:微信昵称 > 用户自定义昵称 > 手机尾号 > 兜底文案
|
|
* 优先顺序:微信昵称 > 用户自定义昵称 > 手机尾号 > 兜底文案
|
|
@@ -955,15 +984,12 @@ public class InviteService {
|
|
|
? config.getLandingDesc() : "注册登录" + config.getAppName() + "平台,聚合比价查低价";
|
|
? config.getLandingDesc() : "注册登录" + config.getAppName() + "平台,聚合比价查低价";
|
|
|
String rewardDesc = "邀请人每成功邀请1家药店注册,赠送1个月高级会员时长";
|
|
String rewardDesc = "邀请人每成功邀请1家药店注册,赠送1个月高级会员时长";
|
|
|
|
|
|
|
|
- // 邀请链接即下载入口,访问后根据对方设备自动重定向到对应安装包
|
|
|
|
|
- String downloadLink = inviteLink;
|
|
|
|
|
-
|
|
|
|
|
// 复制文本:与示例文案保持一致,便于用户一键转发
|
|
// 复制文本:与示例文案保持一致,便于用户一键转发
|
|
|
String copyText = String.format(
|
|
String copyText = String.format(
|
|
|
"我是%s,在这里发现了一个药店采购神器——%s!它聚合比价功能特别方便,能快速查到最低价,帮你节省采购成本。下载链接%s填我的邀请码 %s 完成注册,你也会获得会员权益!",
|
|
"我是%s,在这里发现了一个药店采购神器——%s!它聚合比价功能特别方便,能快速查到最低价,帮你节省采购成本。下载链接%s填我的邀请码 %s 完成注册,你也会获得会员权益!",
|
|
|
inviterNickname,
|
|
inviterNickname,
|
|
|
config.getAppName(),
|
|
config.getAppName(),
|
|
|
- downloadLink,
|
|
|
|
|
|
|
+ inviteLink,
|
|
|
code.getCode()
|
|
code.getCode()
|
|
|
);
|
|
);
|
|
|
|
|
|