邀请码功能完善

This commit is contained in:
tianfeng 2025-10-21 21:25:22 +08:00
parent f63bbc8c65
commit 88ef879b22
8 changed files with 340 additions and 3 deletions

View File

@ -1,5 +1,6 @@
package com.red.circle.other.app.command.activity;
import com.red.circle.other.app.command.user.InviteCodeService;
import com.red.circle.other.app.dto.cmd.activity.BindInviteCodeCmd;
import com.red.circle.other.domain.gateway.user.UserProfileGateway;
import com.red.circle.other.domain.model.user.UserProfile;
@ -28,6 +29,7 @@ public class BindInviteCodeExe {
private final UserProfileGateway userProfileGateway;
private final UserInviteUserService userInviteUserService;
private final InviteCodeService inviteCodeService;
/**
* 执行绑定邀请码
@ -54,8 +56,8 @@ public class BindInviteCodeExe {
throw new RuntimeException("The inviter has been bound before and cannot be bound again");
}
// 3. 根据邀请码查找邀请人邀请码即为用户账号
UserProfile inviterProfile = userProfileGateway.getByAccount(sysOrigin, inviteCode);
// 3. 根据邀请码查找邀请人
UserProfile inviterProfile = inviteCodeService.getUserProfileByInviteCode(inviteCode);
if (inviterProfile == null) {
throw new RuntimeException("The invitation code is invalid and the corresponding user was not found");
}

View File

@ -1,6 +1,7 @@
package com.red.circle.other.app.command.activity;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import com.red.circle.other.app.command.user.InviteCodeService;
import com.red.circle.other.app.dto.clientobject.activity.InviteCodeCO;
import com.red.circle.other.domain.gateway.user.UserProfileGateway;
import com.red.circle.other.domain.model.user.UserProfile;
@ -26,6 +27,7 @@ public class GetMyInviteCodeExe {
private final UserProfileGateway userProfileGateway;
private final UserInviteUserService userInviteUserService;
private final InviteCodeService inviteCodeService;
/**
* 执行获取我的邀请码
@ -43,9 +45,17 @@ public class GetMyInviteCodeExe {
return null;
}
// 调用邀请码服务获取或创建邀请码
String inviteCode = inviteCodeService.generateInviteCode(userId);
if (inviteCode == null) {
log.error("未能生成邀请码, userId={}", userId);
return null;
}
// 构造返回对象
InviteCodeCO inviteCodeCO = new InviteCodeCO();
inviteCodeCO.setUserId(userId);
inviteCodeCO.setInviteCode(userProfile.getAccount()); // 使用用户账号作为邀请码
inviteCodeCO.setInviteCode(inviteCode);
// 查询是否已绑定邀请人
List<UserInviteUser> inviteUserList = userInviteUserService.query().eq(UserInviteUser::getUserId, userId).list();

View File

@ -0,0 +1,80 @@
package com.red.circle.other.app.command.user;
import com.red.circle.other.app.util.Base36InviteCodeGenerator;
import com.red.circle.other.domain.gateway.user.UserProfileGateway;
import com.red.circle.other.domain.model.user.UserProfile;
import com.red.circle.other.infra.database.rds.dao.user.user.InviteCodeMappingDAO;
import com.red.circle.other.infra.database.rds.entity.user.user.InviteCodeMapping;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Slf4j
public class InviteCodeService {
private final InviteCodeMappingDAO inviteCodeMappingDAO;
private final UserProfileGateway userProfileGateway;
public String generateInviteCode(Long userId) {
UserProfile profile = userProfileGateway.getByUserId(userId);
if (profile == null) {
log.warn("用户不存在, userId={}", userId);
return null;
}
// 先查是否已有邀请码
InviteCodeMapping existing = inviteCodeMappingDAO.selectOneByUserId(userId);
if (existing != null) {
return existing.getInviteCode();
}
// 生成邀请码基于 userId 编码
String code;
try {
code = Base36InviteCodeGenerator.encode(Long.parseLong(profile.getAccount()));
} catch (IllegalArgumentException e) {
log.error("用户ID过大无法生成邀请码: {}", userId);
return null;
}
// 插入映射关系注意唯一性约束防止重复
InviteCodeMapping mapping = new InviteCodeMapping();
mapping.setInviteCode(code);
mapping.setUserId(userId);
mapping.setAccount(profile.getAccount());
try {
inviteCodeMappingDAO.insert(mapping);
return code;
} catch (DuplicateKeyException e) {
// 并发情况下可能冲突重试一次
log.info("邀请码冲突,重试生成: {}", code);
InviteCodeMapping conflict = inviteCodeMappingDAO.selectByInviteCode(code);
if (conflict != null) {
return conflict.getInviteCode();
}
throw e;
}
}
public UserProfile getUserProfileByInviteCode(String inviteCode) {
if (inviteCode == null || inviteCode.isEmpty()) {
return null;
}
inviteCode = inviteCode.trim().toUpperCase();
// 查询映射表
InviteCodeMapping mapping = inviteCodeMappingDAO.selectByInviteCode(inviteCode);
if (mapping == null) {
log.warn("未找到对应的邀请码记录: {}", inviteCode);
return null;
}
// 返回用户信息
return userProfileGateway.getByUserId(mapping.getUserId());
}
}

View File

@ -0,0 +1,77 @@
package com.red.circle.other.app.util;
import java.security.SecureRandom;
public class Base36InviteCodeGenerator {
private static final String CHAR_SET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; // 36字符
private static final int CODE_LENGTH = 8; // 固定8位
private static final SecureRandom RANDOM = new SecureRandom();
/**
* 将用户ID编码为8位随机邀请码Base36
* 使用用户ID作为随机种子确保同一ID生成相同邀请码
*/
public static String encode(long userId) {
if (userId < 0) {
throw new IllegalArgumentException("用户ID不能为负数: " + userId);
}
// 使用用户ID作为种子初始化随机数生成器确保同一ID生成相同邀请码
SecureRandom random = new SecureRandom();
random.setSeed(userId);
StringBuilder sb = new StringBuilder(CODE_LENGTH);
// 第一位不使用0避免看起来像前导零
sb.append(CHAR_SET.charAt(1 + random.nextInt(35))); // 1-35 1-Z
// 后续7位随机生成
for (int i = 1; i < CODE_LENGTH; i++) {
sb.append(CHAR_SET.charAt(random.nextInt(36))); // 0-35 0-Z
}
return sb.toString();
}
/**
* 生成完全随机的8位邀请码不基于用户ID
*/
public static String generateRandom() {
StringBuilder sb = new StringBuilder(CODE_LENGTH);
// 第一位不使用0
sb.append(CHAR_SET.charAt(1 + RANDOM.nextInt(35))); // 1-Z
// 后续7位随机生成
for (int i = 1; i < CODE_LENGTH; i++) {
sb.append(CHAR_SET.charAt(RANDOM.nextInt(36))); // 0-Z
}
return sb.toString();
}
// 测试
public static void main(String[] args) {
System.out.println("=== 基于用户ID的邀请码生成同一ID生成相同邀请码===");
long[] testIds = {1L, 100L, 8826602L, 1000000L, 99999999L};
for (long userId : testIds) {
String code1 = encode(userId);
String code2 = encode(userId); // 再次生成验证是否相同
System.out.println("ID: " + userId + " -> 邀请码: " + code1 + " (再次生成: " + code2 + ", 相同: " + code1.equals(code2) + ")");
}
System.out.println("\n=== 特定ID测试 ===");
long userId = 8826602L;
String code = encode(userId);
System.out.println("ID " + userId + " -> 邀请码: " + code);
System.out.println("无前导零: " + !code.startsWith("0"));
System.out.println("长度: " + code.length());
System.out.println("\n=== 完全随机邀请码生成 ===");
for (int i = 0; i < 5; i++) {
System.out.println("随机邀请码 " + (i + 1) + ": " + generateRandom());
}
}
}

View File

@ -0,0 +1,42 @@
package com.red.circle.other.infra.database.rds.dao.user.user;
import com.red.circle.framework.mybatis.dao.BaseDAO;
import com.red.circle.other.infra.database.rds.entity.user.user.InviteCodeMapping;
import org.apache.ibatis.annotations.Param;
/**
* <p>
* 邀请码映射关系 Mapper 接口.
* </p>
*
* @author generated
* @since 2025-10-21
*/
public interface InviteCodeMappingDAO extends BaseDAO<InviteCodeMapping> {
/**
* 根据邀请码查询映射记录.
*
* @param inviteCode 邀请码
* @return 邀请码映射实体
*/
InviteCodeMapping selectByInviteCode(@Param("inviteCode") String inviteCode);
/**
* 根据用户ID查询映射记录.
*
* @param userId 用户ID
* @return 邀请码映射实体
*/
InviteCodeMapping selectOneByUserId(@Param("userId") Long userId);
/**
* 根据账号查询映射记录.
*
* @param account 用户账号
* @return 邀请码映射实体
*/
InviteCodeMapping selectByAccount(@Param("account") String account);
}

View File

@ -0,0 +1,61 @@
package com.red.circle.other.infra.database.rds.entity.user.user;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.red.circle.framework.mybatis.entity.TimestampBaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.sql.Timestamp;
/**
* <p>
* 邀请码映射关系表.
* </p>
*
* @author generated
* @since 2025-10-21
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("invite_code_mapping")
public class InviteCodeMapping implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键标识.
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
/**
* 邀请码8位数字+大写字母.
*/
@TableField("invite_code")
private String inviteCode;
/**
* 用户ID.
*/
@TableField("user_id")
private Long userId;
/**
* 用户账号冗余存储便于查询.
*/
@TableField("account")
private String account;
/**
* 创建时间
*/
@TableField("create_time")
private Timestamp createTime;
}

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.red.circle.other.infra.database.rds.dao.user.user.InviteCodeMappingDAO">
<!-- Result Map -->
<resultMap id="InviteCodeMappingResult" type="com.red.circle.other.infra.database.rds.entity.user.user.InviteCodeMapping">
<id property="id" column="id"/>
<result property="inviteCode" column="invite_code"/>
<result property="userId" column="user_id"/>
<result property="account" column="account"/>
<result property="createTime" column="create_time" />
</resultMap>
<!-- 根据邀请码查询 -->
<select id="selectByInviteCode" resultMap="InviteCodeMappingResult">
SELECT
id,
invite_code,
user_id,
account,
create_time
FROM invite_code_mapping
WHERE invite_code = #{inviteCode}
</select>
<!-- 根据用户ID查询 -->
<select id="selectOneByUserId" resultMap="InviteCodeMappingResult">
SELECT
id,
invite_code,
user_id,
account,
create_time
FROM invite_code_mapping
WHERE user_id = #{userId}
</select>
<!-- 根据账号查询 -->
<select id="selectByAccount" resultMap="InviteCodeMappingResult">
SELECT
id,
invite_code,
user_id,
account,
create_time
FROM invite_code_mapping
WHERE account = #{account}
</select>
</mapper>

View File

@ -0,0 +1,14 @@
import com.red.circle.OtherServiceApplication;
import com.red.circle.other.app.scheduler.TeamBillTask;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@SpringBootTest(classes = OtherServiceApplication.class)
@RunWith(SpringRunner.class)
public class SpringTest {
}