feat(invite): 添加邀请码注册与验证功能

- 新增邀请码实体、Mapper、Service 及 XML 配置
- 注册接口支持填写邀请码并建立绑定关系
- 邀请码校验包含存在性、状态、过期及次数限制
- 补充相关错误码:INVITE_CODE_* 与 RECEIPT_ALREADY_PROCESSED
This commit is contained in:
2025-12-19 14:47:54 +08:00
parent 0ef7a7fd83
commit 6638ff2ccc
6 changed files with 77 additions and 2 deletions

View File

@@ -62,6 +62,8 @@ public enum ErrorCode {
INVITE_CODE_INVALID(50024, "邀请码无效"), INVITE_CODE_INVALID(50024, "邀请码无效"),
INVITE_CODE_EXPIRED(50025, "邀请码已过期"), INVITE_CODE_EXPIRED(50025, "邀请码已过期"),
INVITE_CODE_USED_UP(50026, "邀请码使用次数已达上限"), INVITE_CODE_USED_UP(50026, "邀请码使用次数已达上限"),
INVITE_CODE_ALREADY_BOUND(50028, "您已绑定过邀请码,无法重复绑定"),
INVITE_CODE_CANNOT_BIND_SELF(50029, "不能绑定自己的邀请码"),
RECEIPT_ALREADY_PROCESSED(50027, "收据已处理"); RECEIPT_ALREADY_PROCESSED(50027, "收据已处理");
/** /**

View File

@@ -103,8 +103,8 @@ public class SaTokenConfigure implements WebMvcConfigurer {
"/apple/notification", "/apple/notification",
"/apple/receipt", "/apple/receipt",
"/apple/validate-receipt", "/apple/validate-receipt",
"/user/inviteCode" "/user/inviteCode",
"/user/bindInviteCode"
}; };
} }
@Bean @Bean

View File

@@ -142,4 +142,10 @@ public class UserController {
return ResultUtils.success(respVO); return ResultUtils.success(respVO);
} }
@PostMapping("/bindInviteCode")
@Operation(summary = "绑定邀请码", description = "用户填写邀请码进行绑定")
public BaseResponse<Boolean> bindInviteCode(@RequestBody BindInviteCodeDTO bindInviteCodeDTO) {
return ResultUtils.success(userService.bindInviteCode(bindInviteCodeDTO));
}
} }

View File

@@ -0,0 +1,20 @@
package com.yolo.keyborad.model.dto.user;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 绑定邀请码请求
* @author: ziin
* @date: 2025/12/19
*/
@Data
@Schema(description = "绑定邀请码请求")
public class BindInviteCodeDTO {
/**
* 邀请码
*/
@Schema(description = "邀请码", required = true)
private String inviteCode;
}

View File

@@ -28,4 +28,6 @@ public interface UserService extends IService<KeyboardUser> {
Boolean resetPassWord(ResetPassWordDTO resetPassWordDTO); Boolean resetPassWord(ResetPassWordDTO resetPassWordDTO);
Boolean bindInviteCode(BindInviteCodeDTO bindInviteCodeDTO);
} }

View File

@@ -350,4 +350,49 @@ public class UserServiceImpl extends ServiceImpl<KeyboardUserMapper, KeyboardUse
} }
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean bindInviteCode(BindInviteCodeDTO bindInviteCodeDTO) {
// 获取当前登录用户ID
long userId = StpUtil.getLoginIdAsLong();
// 检查用户是否已经绑定过邀请码
long existingBindCount = userInvitesService.count(
new LambdaQueryWrapper<KeyboardUserInvites>()
.eq(KeyboardUserInvites::getInviteeUserId, userId)
);
if (existingBindCount > 0) {
throw new BusinessException(ErrorCode.INVITE_CODE_ALREADY_BOUND);
}
// 验证邀请码
String inviteCodeStr = bindInviteCodeDTO.getInviteCode().trim();
KeyboardUserInviteCodes inviteCode = inviteCodesService.validateInviteCode(inviteCodeStr);
// 检查是否是绑定自己的邀请码
if (inviteCode.getOwnerUserId().equals(userId)) {
throw new BusinessException(ErrorCode.INVITE_CODE_CANNOT_BIND_SELF);
}
// 创建邀请关系绑定记录
KeyboardUserInvites userInvite = new KeyboardUserInvites();
userInvite.setInviterUserId(inviteCode.getOwnerUserId());
userInvite.setInviteeUserId(userId);
userInvite.setInviteCodeId(inviteCode.getId());
userInvite.setBindType((short) 1); // 1=手动填写邀请码
userInvite.setBoundAt(new Date());
userInvite.setBindIp(request.getRemoteAddr());
userInvite.setBindUserAgent(request.getHeader("User-Agent"));
userInvitesService.save(userInvite);
// 更新邀请码使用次数
inviteCode.setUsedCount(inviteCode.getUsedCount() + 1);
inviteCodesService.updateById(inviteCode);
log.info("User bound invite code, userId={}, inviteCodeId={}, inviterUserId={}",
userId, inviteCode.getId(), inviteCode.getOwnerUserId());
return true;
}
} }