diff --git a/client-api/src/main/java/org/jiayunet/controller/LoginController.java b/client-api/src/main/java/org/jiayunet/controller/LoginController.java index 78f8031..d943ca1 100644 --- a/client-api/src/main/java/org/jiayunet/controller/LoginController.java +++ b/client-api/src/main/java/org/jiayunet/controller/LoginController.java @@ -33,6 +33,6 @@ public class LoginController { public LoginVo smsLogin(@Validated @RequestBody SmsLoginDto dto, HttpServletRequest request, HttpServletResponse response) { - return loginServer.smsLogin(dto.getMobileNumber(), dto.getCode(), request, response); + return loginServer.smsLogin(dto.getMobileNumber(), dto.getCode(), dto.getInviteCode(), request, response); } } diff --git a/client-api/src/main/java/org/jiayunet/pojo/dto/SmsLoginDto.java b/client-api/src/main/java/org/jiayunet/pojo/dto/SmsLoginDto.java index 6449165..f43fe27 100644 --- a/client-api/src/main/java/org/jiayunet/pojo/dto/SmsLoginDto.java +++ b/client-api/src/main/java/org/jiayunet/pojo/dto/SmsLoginDto.java @@ -17,4 +17,9 @@ public class SmsLoginDto { @NotBlank(message = "验证码不能为空") private String code; + + /** + * 邀请码(可选) + */ + private String inviteCode; } diff --git a/client-api/src/main/java/org/jiayunet/server/LoginServer.java b/client-api/src/main/java/org/jiayunet/server/LoginServer.java index 40fd755..6ac13f9 100644 --- a/client-api/src/main/java/org/jiayunet/server/LoginServer.java +++ b/client-api/src/main/java/org/jiayunet/server/LoginServer.java @@ -59,6 +59,9 @@ public class LoginServer { @Autowired private RedisServerTool redisServerTool; + @Autowired + private UserRegisterServer userRegisterServer; + /** * 发送短信验证码 */ @@ -76,7 +79,7 @@ public class LoginServer { * 短信验证码登录 */ @Transactional(rollbackFor = Exception.class) - public LoginVo smsLogin(String mobileNumber, String code, HttpServletRequest request, HttpServletResponse response) { + public LoginVo smsLogin(String mobileNumber, String code, String inviteCode, HttpServletRequest request, HttpServletResponse response) { Assert.hasText(mobileNumber, "手机号不能为空"); Assert.hasText(code, "验证码不能为空"); @@ -91,13 +94,7 @@ public class LoginServer { // 用户不存在则自动注册 if (user == null) { - user = new User(); - user.setMobileNumber(mobileNumber); - user.setNick("用户" + mobileNumber.substring(mobileNumber.length() - 4)); - user.setStatus(0); - user.setIsDelete(0L); - user.setCreateTime(Instant.now()); - userMapper.insert(user); + user = userRegisterServer.register(mobileNumber, inviteCode); } // 检查用户状态 diff --git a/client-api/src/main/java/org/jiayunet/server/UserRegisterServer.java b/client-api/src/main/java/org/jiayunet/server/UserRegisterServer.java new file mode 100644 index 0000000..3e28797 --- /dev/null +++ b/client-api/src/main/java/org/jiayunet/server/UserRegisterServer.java @@ -0,0 +1,126 @@ +package org.jiayunet.server; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.extern.slf4j.Slf4j; +import org.jiayunet.exception.BusinessException; +import org.jiayunet.exception.BusinessExpCodeEnum; +import org.jiayunet.mapper.UserInviteMapper; +import org.jiayunet.mapper.UserMapper; +import org.jiayunet.pojo.po.User; +import org.jiayunet.pojo.po.UserInvite; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.security.SecureRandom; +import java.time.Instant; + +/** + * 用户注册服务 + * + * @author zk + */ +@Service +@Slf4j +public class UserRegisterServer { + + /** + * 邀请码字符集(大写字母+数字) + */ + private static final String INVITE_CODE_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + /** + * 邀请码长度 + */ + private static final int INVITE_CODE_LENGTH = 10; + + /** + * 安全随机数生成器 + */ + private static final SecureRandom RANDOM = new SecureRandom(); + + @Autowired + private UserMapper userMapper; + + @Autowired + private UserInviteMapper userInviteMapper; + + /** + * 注册新用户 + * + * @param mobileNumber 手机号 + * @param inviteCode 邀请码(可选) + * @return 新注册的用户 + */ + @Transactional(rollbackFor = Exception.class) + public User register(String mobileNumber, String inviteCode) { + // 创建用户 + User user = new User(); + user.setMobileNumber(mobileNumber); + user.setNick("用户" + mobileNumber.substring(mobileNumber.length() - 4)); + user.setInviteCode(generateInviteCode()); + user.setStatus(0); + user.setIsDelete(0L); + user.setCreateTime(Instant.now()); + userMapper.insert(user); + + // 绑定邀请关系 + if (StringUtils.hasText(inviteCode)) { + bindInvite(user, inviteCode); + } + + return user; + } + + /** + * 绑定邀请关系 + */ + private void bindInvite(User user, String inviteCode) { + // 查找邀请人 + User inviter = userMapper.selectOne( + new LambdaQueryWrapper() + .eq(User::getInviteCode, inviteCode) + ); + + if (inviter == null) { + log.warn("邀请码无效 inviteCode:{} userId:{}", inviteCode, user.getId()); + return; + } + + // 不能自己邀请自己 + if (inviter.getId().equals(user.getId())) { + return; + } + + // 记录邀请关系 + UserInvite invite = new UserInvite(); + invite.setUserId(user.getId()); + invite.setInviterId(inviter.getId()); + invite.setInviteCode(inviteCode); + invite.setCreateTime(Instant.now()); + userInviteMapper.insert(invite); + } + + /** + * 生成10位邀请码(大写字母+数字),确保唯一 + */ + private String generateInviteCode() { + for (int i = 0; i < 3; i++) { + StringBuilder sb = new StringBuilder(INVITE_CODE_LENGTH); + for (int j = 0; j < INVITE_CODE_LENGTH; j++) { + sb.append(INVITE_CODE_CHARS.charAt(RANDOM.nextInt(INVITE_CODE_CHARS.length()))); + } + String code = sb.toString(); + // 检查是否已存在 + Long count = userMapper.selectCount( + new LambdaQueryWrapper().eq(User::getInviteCode, code) + ); + if (count == 0) { + return code; + } + log.warn("邀请码碰撞 code:{} 第{}次重试", code, i + 1); + } + throw new BusinessException(BusinessExpCodeEnum.UNKNOWN_ERROR, "邀请码生成失败,请重试"); + } +} diff --git a/manager/src/main/java/org/jiayunet/mapper/UserInviteMapper.java b/manager/src/main/java/org/jiayunet/mapper/UserInviteMapper.java new file mode 100644 index 0000000..f3e0c62 --- /dev/null +++ b/manager/src/main/java/org/jiayunet/mapper/UserInviteMapper.java @@ -0,0 +1,14 @@ +package org.jiayunet.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.jiayunet.pojo.po.UserInvite; + +/** + * 用户邀请记录Mapper + * + * @author zk + */ +@Mapper +public interface UserInviteMapper extends CommonMapper { + +} diff --git a/manager/src/main/java/org/jiayunet/pojo/po/User.java b/manager/src/main/java/org/jiayunet/pojo/po/User.java index f8a1221..381df89 100644 --- a/manager/src/main/java/org/jiayunet/pojo/po/User.java +++ b/manager/src/main/java/org/jiayunet/pojo/po/User.java @@ -79,6 +79,11 @@ public class User { */ private String wechatUnionId; + /** + * 邀请码 + */ + private String inviteCode; + /** * 状态 0 正常 1 禁用 */ diff --git a/manager/src/main/java/org/jiayunet/pojo/po/UserInvite.java b/manager/src/main/java/org/jiayunet/pojo/po/UserInvite.java new file mode 100644 index 0000000..99f7e12 --- /dev/null +++ b/manager/src/main/java/org/jiayunet/pojo/po/UserInvite.java @@ -0,0 +1,41 @@ +package org.jiayunet.pojo.po; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.Instant; + +/** + * 用户邀请记录表 + * + * @author zk + */ +@Data +@TableName(value = "bg_user_invite") +public class UserInvite { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 被邀请人ID + */ + private Long userId; + + /** + * 邀请人ID + */ + private Long inviterId; + + /** + * 使用的邀请码 + */ + private String inviteCode; + + /** + * 邀请时间 + */ + private Instant createTime; +}