注册接口

This commit is contained in:
dww
2026-01-31 13:07:46 +08:00
parent 06e4e5597f
commit e687cdcc83
9 changed files with 337 additions and 11 deletions

View File

@@ -0,0 +1,60 @@
package com.bicloud.common.utils;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;
/**
* 密码加密工具类
*/
public class PasswordUtils {
private static final SecureRandom RANDOM = new SecureRandom();
/**
* 生成随机盐值
*/
public static String generateSalt() {
byte[] salt = new byte[16];
RANDOM.nextBytes(salt);
return Base64.getEncoder().encodeToString(salt);
}
/**
* 使用MD5加盐加密密码
* @param password 原始密码
* @param salt 盐值
* @return 加密后的密码
*/
public static String encryptPassword(String password, String salt) {
try {
// 将密码和盐值拼接
String combined = password + salt;
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] hashBytes = md.digest(combined.getBytes(StandardCharsets.UTF_8));
// 转换为16进制字符串
StringBuilder sb = new StringBuilder();
for (byte b : hashBytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("密码加密失败", e);
}
}
/**
* 验证密码是否正确
* @param inputPassword 输入的密码
* @param salt 盐值
* @param encryptedPassword 加密后的密码
* @return 是否匹配
*/
public static boolean verifyPassword(String inputPassword, String salt, String encryptedPassword) {
String encrypted = encryptPassword(inputPassword, salt);
return encrypted.equals(encryptedPassword);
}
}

View File

@@ -2,11 +2,13 @@ package com.bicloud.controller;
import com.bicloud.common.result.Result; import com.bicloud.common.result.Result;
import com.bicloud.pojo.dto.RegisterDto;
import com.bicloud.service.UserService; import com.bicloud.service.UserService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession; import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -23,11 +25,20 @@ public class UserController {
/** /**
* 发送手机验证码 * 发送手机验证码
*/ */
@PostMapping("code") @PostMapping("/code")
@Operation(summary = "发送验证码") @Operation(summary = "发送验证码")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session, HttpServletRequest request) { public Result sendCode(@RequestParam("mobile") String mobile, HttpSession session, HttpServletRequest request) {
// 发送短信验证码并保存验证码 // 发送短信验证码并保存验证码
return userService.sendCode(phone, session, request); return userService.sendCode(mobile, session, request);
}
/**
* 用户注册
*/
@PostMapping("/register")
@Operation(summary = "用户注册")
public Result register(@Valid @RequestBody RegisterDto registerDto) {
return userService.register(registerDto);
} }
} }

View File

@@ -0,0 +1,38 @@
package com.bicloud.mapper;
import com.bicloud.pojo.entity.User;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface UserMapper {
/**
* 根据手机号查询用户
*/
User selectByMobile(@Param("mobile") String mobile);
/**
* 根据邮箱查询用户
*/
User selectByEmail(@Param("email") String email);
/**
* 根据用户名查询用户
*/
User selectByUsername(@Param("username") String username);
/**
* 插入用户
*/
int insert(User user);
/**
* 根据ID查询用户
*/
User selectById(@Param("id") Long id);
}

View File

@@ -0,0 +1,55 @@
package com.bicloud.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
@Data
@Schema(description = "用户注册请求DTO")
public class RegisterDto {
@NotBlank(message = "用户名不能为空")
@Schema(description = "用户名", example = "张三")
private String username;
@NotBlank(message = "所属单位不能为空")
@Schema(description = "所属单位", example = "某某大学")
private String company;
@NotBlank(message = "职位不能为空")
@Schema(description = "职位", example = "研究员")
private String job;
@NotBlank(message = "地址不能为空")
@Schema(description = "所在地址", example = "北京市海淀区")
private String address;
@Schema(description = "感兴趣标签", example = "[\"生物信息学\", \"基因组学\"]")
private java.util.List<String> interestTags;
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
@Schema(description = "手机号", example = "13800138000")
private String mobile;
@NotBlank(message = "验证码不能为空")
@Schema(description = "验证码", example = "123123")
private String vcode;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
@Schema(description = "邮箱地址", example = "example@example.com")
private String email;
@NotBlank(message = "密码不能为空")
@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{8,20}$",
message = "密码必须包含大小写字母和数字长度8-20位")
@Schema(description = "密码", example = "Password123")
private String pwd;
@NotBlank(message = "确认密码不能为空")
@Schema(description = "确认密码", example = "Password123")
private String cpwd;
}

View File

@@ -0,0 +1,20 @@
package com.bicloud.pojo.entity;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class User {
private Long id;
private String username;
private String company;
private String job;
private String address;
private String introduce;
private String mobile;
private String email;
private String pwd;
private String salt;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}

View File

@@ -1,9 +1,12 @@
package com.bicloud.service; package com.bicloud.service;
import com.bicloud.common.result.Result; import com.bicloud.common.result.Result;
import com.bicloud.pojo.dto.RegisterDto;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession; import jakarta.servlet.http.HttpSession;
public interface UserService { public interface UserService {
Result sendCode(String phone, HttpSession session, HttpServletRequest request); Result sendCode(String mobile, HttpSession session, HttpServletRequest request);
Result register(RegisterDto registerDto);
} }

View File

@@ -1,7 +1,11 @@
package com.bicloud.service.impl; package com.bicloud.service.impl;
import com.bicloud.common.result.Result; import com.bicloud.common.result.Result;
import com.bicloud.common.utils.PasswordUtils;
import com.bicloud.common.utils.RegexUtils; import com.bicloud.common.utils.RegexUtils;
import com.bicloud.mapper.UserMapper;
import com.bicloud.pojo.dto.RegisterDto;
import com.bicloud.pojo.entity.User;
import com.bicloud.service.UserService; import com.bicloud.service.UserService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
@@ -9,6 +13,7 @@ import jakarta.servlet.http.HttpSession;
import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static com.bicloud.common.utils.RedisConstants.*; import static com.bicloud.common.utils.RedisConstants.*;
@@ -20,10 +25,13 @@ public class UserServiceImpl implements UserService {
@Resource @Resource
private StringRedisTemplate stringRedisTemplate; private StringRedisTemplate stringRedisTemplate;
@Resource
private UserMapper userMapper;
@Override @Override
public Result sendCode(String phone, HttpSession session, HttpServletRequest request) { public Result sendCode(String mobile, HttpSession session, HttpServletRequest request) {
// 1.校验手机号 // 1.校验手机号
if (RegexUtils.isPhoneInvalid(phone)) { if (RegexUtils.isPhoneInvalid(mobile)) {
// 2.如果不符合,返回错误信息 // 2.如果不符合,返回错误信息
return Result.error("手机号格式错误!"); return Result.error("手机号格式错误!");
} }
@@ -32,13 +40,13 @@ public class UserServiceImpl implements UserService {
String clientIpAddr = request.getRemoteAddr(); String clientIpAddr = request.getRemoteAddr();
// 4.如果存在锁,直接返回失败 // 4.如果存在锁,直接返回失败
String phoneGetCodeLock = stringRedisTemplate.opsForValue().get(GET_CODE_LOCK + phone); String phoneGetCodeLock = stringRedisTemplate.opsForValue().get(GET_CODE_LOCK + mobile);
if (phoneGetCodeLock != null) { if (phoneGetCodeLock != null) {
return Result.error("获取验证码过快,请稍后重试!"); return Result.error("获取验证码过快,请稍后重试!");
} }
// 5.检查两个黑名单中的次数 // 5.检查两个黑名单中的次数
String blacklistPhoneCount = stringRedisTemplate.opsForValue().get(GET_CODE_BLACKLIST_PHONE + phone); String blacklistPhoneCount = stringRedisTemplate.opsForValue().get(GET_CODE_BLACKLIST_PHONE + mobile);
int getCodePhoneCount = (blacklistPhoneCount != null) ? Integer.parseInt(blacklistPhoneCount) : 0; int getCodePhoneCount = (blacklistPhoneCount != null) ? Integer.parseInt(blacklistPhoneCount) : 0;
if (getCodePhoneCount >= 400) { if (getCodePhoneCount >= 400) {
return Result.error("获取验证码次数过多,您的手机号已被限制!"); return Result.error("获取验证码次数过多,您的手机号已被限制!");
@@ -51,16 +59,16 @@ public class UserServiceImpl implements UserService {
// } // }
// 6.更新锁和黑名单 // 6.更新锁和黑名单
stringRedisTemplate.opsForValue().set(GET_CODE_BLACKLIST_PHONE + phone, String.valueOf(getCodePhoneCount + 1), REFRESH_BLACKLIST_TTL, TimeUnit.HOURS); stringRedisTemplate.opsForValue().set(GET_CODE_BLACKLIST_PHONE + mobile, String.valueOf(getCodePhoneCount + 1), REFRESH_BLACKLIST_TTL, TimeUnit.HOURS);
// stringRedisTemplate.opsForValue().set(GET_CODE_BLACKLIST_IP_ADDR + clientIpAddr, String.valueOf(getCodeIpAddrCount + 1), REFRESH_BLACKLIST_TTL, TimeUnit.HOURS); // stringRedisTemplate.opsForValue().set(GET_CODE_BLACKLIST_IP_ADDR + clientIpAddr, String.valueOf(getCodeIpAddrCount + 1), REFRESH_BLACKLIST_TTL, TimeUnit.HOURS);
stringRedisTemplate.opsForValue().set(GET_CODE_LOCK + phone, "1", GET_CODE_LOCK_TTL, TimeUnit.MINUTES); stringRedisTemplate.opsForValue().set(GET_CODE_LOCK + mobile, "1", GET_CODE_LOCK_TTL, TimeUnit.MINUTES);
// 7.校验通过,生成验证码 // 7.校验通过,生成验证码
// String code = RandomUtil.randomNumbers(6); // String code = RandomUtil.randomNumbers(6);
String code = "123123"; //为了方便测试,改成固定值 String code = "123123"; //为了方便测试,改成固定值
// 8.保存验证码到 redis并上锁 // 8.保存验证码到 redis并上锁
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES); stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + mobile, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
// 9.发送验证码 // 9.发送验证码
log.debug("发送短信验证码成功,验证码:{}", code); log.debug("发送短信验证码成功,验证码:{}", code);
@@ -68,5 +76,76 @@ public class UserServiceImpl implements UserService {
return Result.success("发送短信验证码成功,验证码:"+code); return Result.success("发送短信验证码成功,验证码:"+code);
} }
@Override
public Result register(RegisterDto registerDto) {
// 1. 校验两次密码是否一致
if (!registerDto.getPwd().equals(registerDto.getCpwd())) {
return Result.error("两次密码输入不一致!");
}
// 2. 校验验证码
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + registerDto.getMobile());
if (cacheCode == null) {
return Result.error("验证码已过期,请重新获取!");
}
if (!cacheCode.equals(registerDto.getVcode())) {
return Result.error("验证码错误!");
}
// 3. 检查手机号是否已注册
User existingUserByPhone = userMapper.selectByMobile(registerDto.getMobile());
if (existingUserByPhone != null) {
return Result.error("该手机号已被注册!");
}
// 4.检查邮箱是否已注册
User existingUserByEmail = userMapper.selectByEmail(registerDto.getEmail());
if (existingUserByEmail != null) {
return Result.error("该邮箱已被注册!");
}
// 检查用户名是否已注册
User existingUserByUsername = userMapper.selectByUsername(registerDto.getUsername());
if (existingUserByUsername != null) {
return Result.error("该用户名已被注册!");
}
// 5. 生成盐值
String salt = PasswordUtils.generateSalt();
// 6. 加密密码
String encryptedPassword = PasswordUtils.encryptPassword(registerDto.getPwd(), salt);
// 7. 将感兴趣标签数组转换为字符串
String introduce = (registerDto.getInterestTags() != null && !registerDto.getInterestTags().isEmpty())
? String.join(",", registerDto.getInterestTags())
: null;
// 8. 创建用户对象
User user = new User();
user.setUsername(registerDto.getUsername());
user.setCompany(registerDto.getCompany());
user.setJob(registerDto.getJob());
user.setAddress(registerDto.getAddress());
user.setIntroduce(introduce);
user.setMobile(registerDto.getMobile());
user.setEmail(registerDto.getEmail());
user.setPwd(encryptedPassword);
user.setSalt(salt);
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
// 9. 保存用户到数据库
int result = userMapper.insert(user);
if (result <= 0) {
return Result.error("注册失败,请稍后重试!");
}
// 10. 删除Redis中的验证码
stringRedisTemplate.delete(LOGIN_CODE_KEY + registerDto.getMobile());
log.info("用户注册成功,手机号:{}", registerDto.getMobile());
return Result.success("注册成功!");
}
} }

View File

@@ -0,0 +1,42 @@
<?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.bicloud.mapper.UserMapper">
<resultMap id="BaseResultMap" type="com.bicloud.pojo.entity.User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="company" property="company"/>
<result column="job" property="job"/>
<result column="address" property="address"/>
<result column="introduce" property="introduce"/>
<result column="mobile" property="mobile"/>
<result column="email" property="email"/>
<result column="pwd" property="pwd"/>
<result column="salt" property="salt"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
</resultMap>
<select id="selectByMobile" resultMap="BaseResultMap">
SELECT * FROM users WHERE mobile = #{mobile}
</select>
<select id="selectByEmail" resultMap="BaseResultMap">
SELECT * FROM users WHERE email = #{email}
</select>
<select id="selectByUsername" resultMap="BaseResultMap">
SELECT * FROM users WHERE username = #{username}
</select>
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM users WHERE id = #{id}
</select>
<insert id="insert" parameterType="com.bicloud.pojo.entity.User" useGeneratedKeys="true" keyProperty="id">
INSERT INTO users (username, company, job, address, introduce, mobile, email, pwd, salt, addtime, updtime)
VALUES (#{username}, #{company}, #{job}, #{address}, #{introduce}, #{mobile}, #{email}, #{pwd}, #{salt}, #{createTime}, #{updateTime})
</insert>
</mapper>

View File

@@ -0,0 +1,18 @@
-- 用户表
CREATE TABLE IF NOT EXISTS `users` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` VARCHAR(50) NOT NULL COMMENT '用户名',
`company` VARCHAR(100) NOT NULL COMMENT '所属单位',
`job` VARCHAR(50) NOT NULL COMMENT '职位',
`address` VARCHAR(200) NOT NULL COMMENT '所在地址',
`introduce` VARCHAR(500) DEFAULT NULL COMMENT '感兴趣标签',
`phone` VARCHAR(11) NOT NULL COMMENT '手机号',
`email` VARCHAR(100) NOT NULL COMMENT '邮箱地址',
`pwd` VARCHAR(64) NOT NULL COMMENT '密码(加密后)',
`salt` VARCHAR(32) NOT NULL COMMENT '盐值',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_phone` (`phone`) COMMENT '手机号唯一索引',
UNIQUE KEY `uk_email` (`email`) COMMENT '邮箱唯一索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';