feat(invite): 新增用户邀请码功能

新增实体、Mapper、Service及Controller接口,支持注册时自动生成与用户查询个人邀请码
This commit is contained in:
2025-12-18 19:20:25 +08:00
parent c4d0c60ea8
commit 419878a607
8 changed files with 254 additions and 3 deletions

View File

@@ -8,19 +8,19 @@ import com.yolo.keyborad.model.dto.AppleLoginReq;
import com.yolo.keyborad.model.dto.user.*;
import com.yolo.keyborad.model.entity.KeyboardFeedback;
import com.yolo.keyborad.model.entity.KeyboardUser;
import com.yolo.keyborad.model.entity.KeyboardUserInviteCodes;
import com.yolo.keyborad.model.vo.user.InviteCodeRespVO;
import com.yolo.keyborad.model.vo.user.KeyboardUserInfoRespVO;
import com.yolo.keyborad.model.vo.user.KeyboardUserRespVO;
import com.yolo.keyborad.service.IAppleService;
import com.yolo.keyborad.service.KeyboardUserInviteCodesService;
import com.yolo.keyborad.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.text.SimpleDateFormat;
@@ -46,6 +46,9 @@ public class UserController {
@Resource
private com.yolo.keyborad.service.KeyboardFeedbackService feedbackService;
@Resource
private KeyboardUserInviteCodesService inviteCodesService;
/**
* 苹果登录
*
@@ -130,5 +133,13 @@ public class UserController {
return ResultUtils.success(feedbackService.save(feedback));
}
@GetMapping("/inviteCode")
@Operation(summary = "查询邀请码", description = "查询用户自己的邀请码")
public BaseResponse<InviteCodeRespVO> getInviteCode() {
long userId = StpUtil.getLoginIdAsLong();
KeyboardUserInviteCodes inviteCode = inviteCodesService.getUserInviteCode(userId);
InviteCodeRespVO respVO = BeanUtil.copyProperties(inviteCode, InviteCodeRespVO.class);
return ResultUtils.success(respVO);
}
}

View File

@@ -0,0 +1,12 @@
package com.yolo.keyborad.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yolo.keyborad.model.entity.KeyboardUserInviteCodes;
/*
* @author: ziin
* @date: 2025/12/18 16:26
*/
public interface KeyboardUserInviteCodesMapper extends BaseMapper<KeyboardUserInviteCodes> {
}

View File

@@ -0,0 +1,78 @@
package com.yolo.keyborad.model.entity;
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 io.swagger.v3.oas.annotations.media.Schema;
import java.util.Date;
import lombok.Data;
/*
* @author: ziin
* @date: 2025/12/18 16:26
*/
/**
* 用户生成的邀请码表,用于邀请新用户注册/安装并建立邀请关系
*/
@Schema(description="用户生成的邀请码表,用于邀请新用户注册/安装并建立邀请关系")
@Data
@TableName(value = "keyboard_user_invite_codes")
public class KeyboardUserInviteCodes {
/**
* 邀请码主键ID
*/
@TableId(value = "id", type = IdType.AUTO)
@Schema(description="邀请码主键ID")
private Long id;
/**
* 邀请码字符串,对外展示,唯一
*/
@TableField(value = "code")
@Schema(description="邀请码字符串,对外展示,唯一")
private String code;
/**
* 邀请码所属用户ID邀请人
*/
@TableField(value = "owner_user_id")
@Schema(description="邀请码所属用户ID邀请人")
private Long ownerUserId;
/**
* 邀请码状态1=启用0=停用
*/
@TableField(value = "\"status\"")
@Schema(description="邀请码状态1=启用0=停用")
private Short status;
/**
* 邀请码创建时间
*/
@TableField(value = "created_at")
@Schema(description="邀请码创建时间")
private Date createdAt;
/**
* 邀请码过期时间NULL表示永久有效
*/
@TableField(value = "expires_at")
@Schema(description="邀请码过期时间NULL表示永久有效")
private Date expiresAt;
/**
* 邀请码最大可使用次数NULL表示不限次数
*/
@TableField(value = "max_uses")
@Schema(description="邀请码最大可使用次数NULL表示不限次数")
private Integer maxUses;
/**
* 邀请码已使用次数
*/
@TableField(value = "used_count")
@Schema(description="邀请码已使用次数")
private Integer usedCount;
}

View File

@@ -0,0 +1,29 @@
package com.yolo.keyborad.model.vo.user;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Date;
/**
* 邀请码响应VO
*/
@Data
@Schema(description = "邀请码信息")
public class InviteCodeRespVO {
@Schema(description = "邀请码")
private String code;
@Schema(description = "邀请码状态1=启用0=停用")
private Short status;
@Schema(description = "已使用次数")
private Integer usedCount;
@Schema(description = "最大可使用次数")
private Integer maxUses;
@Schema(description = "过期时间")
private Date expiresAt;
}

View File

@@ -0,0 +1,26 @@
package com.yolo.keyborad.service;
import com.yolo.keyborad.model.entity.KeyboardUserInviteCodes;
import com.baomidou.mybatisplus.extension.service.IService;
/*
* @author: ziin
* @date: 2025/12/18 16:26
*/
public interface KeyboardUserInviteCodesService extends IService<KeyboardUserInviteCodes>{
/**
* 获取用户的邀请码
* @param userId 用户ID
* @return 邀请码实体
*/
KeyboardUserInviteCodes getUserInviteCode(Long userId);
/**
* 为用户创建邀请码
* @param userId 用户ID
* @return 创建的邀请码实体
*/
KeyboardUserInviteCodes createInviteCode(Long userId);
}

View File

@@ -0,0 +1,66 @@
package com.yolo.keyborad.service.impl;
import cn.hutool.core.util.RandomUtil;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Date;
import java.util.List;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.yolo.keyborad.mapper.KeyboardUserInviteCodesMapper;
import com.yolo.keyborad.model.entity.KeyboardUserInviteCodes;
import com.yolo.keyborad.service.KeyboardUserInviteCodesService;
/*
* @author: ziin
* @date: 2025/12/18 16:26
*/
@Service
public class KeyboardUserInviteCodesServiceImpl extends ServiceImpl<KeyboardUserInviteCodesMapper, KeyboardUserInviteCodes> implements KeyboardUserInviteCodesService{
@Override
public KeyboardUserInviteCodes getUserInviteCode(Long userId) {
QueryWrapper<KeyboardUserInviteCodes> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("owner_user_id", userId);
return this.getOne(queryWrapper);
}
@Override
public KeyboardUserInviteCodes createInviteCode(Long userId) {
// 生成唯一的邀请码
String code;
int maxRetries = 10;
int retryCount = 0;
do {
// 生成8位字母数字组合的邀请码
code = RandomUtil.randomString(8).toUpperCase();
// 检查邀请码是否已存在
QueryWrapper<KeyboardUserInviteCodes> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("code", code);
KeyboardUserInviteCodes existingCode = this.getOne(queryWrapper);
if (existingCode == null) {
break;
}
retryCount++;
} while (retryCount < maxRetries);
// 创建邀请码实体
KeyboardUserInviteCodes inviteCode = new KeyboardUserInviteCodes();
inviteCode.setCode(code);
inviteCode.setOwnerUserId(userId);
inviteCode.setStatus((short) 1); // 启用状态
inviteCode.setCreatedAt(new Date());
inviteCode.setExpiresAt(null); // 永久有效
inviteCode.setMaxUses(null); // 不限次数
inviteCode.setUsedCount(0); // 初始使用次数为0
// 保存到数据库
this.save(inviteCode);
return inviteCode;
}
}

View File

@@ -62,6 +62,9 @@ public class UserServiceImpl extends ServiceImpl<KeyboardUserMapper, KeyboardUse
@Resource
private KeyboardUserQuotaTotalService quotaTotalService;
@Resource
private KeyboardUserInviteCodesService inviteCodesService;
private final NacosAppConfigCenter.DynamicAppConfig cfgHolder;
public UserServiceImpl(NacosAppConfigCenter.DynamicAppConfig cfgHolder) {
@@ -107,6 +110,9 @@ public class UserServiceImpl extends ServiceImpl<KeyboardUserMapper, KeyboardUse
quotaTotal.setUpdatedAt(new Date());
quotaTotalService.save(quotaTotal);
// 初始化用户邀请码
inviteCodesService.createInviteCode(keyboardUser.getId());
log.info("User registered with Apple Sign-In, userId={}, freeQuota={}",
keyboardUser.getId(), appConfig.getUserRegisterProperties().getFreeTrialQuota());
@@ -246,6 +252,9 @@ public class UserServiceImpl extends ServiceImpl<KeyboardUserMapper, KeyboardUse
quotaTotal.setUpdatedAt(new Date());
quotaTotalService.save(quotaTotal);
// 初始化用户邀请码
inviteCodesService.createInviteCode(keyboardUser.getId());
log.info("User registered with email, userId={}, email={}, freeQuota={}",
keyboardUser.getId(), keyboardUser.getEmail(), appConfig.getUserRegisterProperties().getFreeTrialQuota());
}

View File

@@ -0,0 +1,20 @@
<?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.yolo.keyborad.mapper.KeyboardUserInviteCodesMapper">
<resultMap id="BaseResultMap" type="com.yolo.keyborad.model.entity.KeyboardUserInviteCodes">
<!--@mbg.generated-->
<!--@Table keyboard_user_invite_codes-->
<id column="id" jdbcType="BIGINT" property="id" />
<result column="code" jdbcType="VARCHAR" property="code" />
<result column="owner_user_id" jdbcType="BIGINT" property="ownerUserId" />
<result column="status" jdbcType="SMALLINT" property="status" />
<result column="created_at" jdbcType="TIMESTAMP" property="createdAt" />
<result column="expires_at" jdbcType="TIMESTAMP" property="expiresAt" />
<result column="max_uses" jdbcType="INTEGER" property="maxUses" />
<result column="used_count" jdbcType="INTEGER" property="usedCount" />
</resultMap>
<sql id="Base_Column_List">
<!--@mbg.generated-->
id, code, owner_user_id, "status", created_at, expires_at, max_uses, used_count
</sql>
</mapper>