1.添加邮箱登录,注册接口

This commit is contained in:
2025-08-04 21:58:55 +08:00
parent 715e6a1b70
commit f5cbe5cac2
17 changed files with 530 additions and 22 deletions

Binary file not shown.

44
pom.xml
View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.vv.pk.assistant</groupId>
@@ -102,6 +102,48 @@
<version>5.6.227</version>
</dependency>
<!-- mailgun 邮件服务 -->
<dependency>
<groupId>com.mailgun</groupId>
<artifactId>mailgun-java</artifactId>
<version>2.0.0</version>
</dependency>
<!-- caffeine 本地缓存 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- crypto 加密 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
<version>5.5.1</version> <!-- 请检查最新版本 -->
</dependency>
<!-- Sa-Token 权限认证在线文档https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.44.0</version>
</dependency>
<!-- hu-tool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.38</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>

View File

@@ -0,0 +1,22 @@
package vvpkassistant.Tools;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/*
* @author: ziin
* @date: 2025/8/4 18:19
*/
public class BcryptUtils {
public static String encryptPassword(String password) {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
return passwordEncoder.encode(password);
}
public static Boolean matchPassword(String rawPassWord,String encodePassword){
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
return passwordEncoder.matches(encodePassword, rawPassWord);
}
}

View File

@@ -1,4 +1,5 @@
package vvpkassistant.User.controller;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import vvpkassistant.CoinRecords.CoinRecords;
@@ -7,16 +8,23 @@ import vvpkassistant.Data.ResponseData;
import vvpkassistant.Data.ResponseInfo;
import vvpkassistant.Data.WxChatParam;
import vvpkassistant.User.mapper.UserDao;
import vvpkassistant.User.model.DTO.UserModelDTO;
import vvpkassistant.User.model.UserModel;
import vvpkassistant.User.model.UserModelVO;
import vvpkassistant.User.service.UserService;
import vvpkassistant.common.ErrorCode;
import vvpkassistant.config.FunctionConfigHolder;
import vvpkassistant.Tools.VVRequester;
import vvpkassistant.Tools.VVTools;
import vvpkassistant.exception.BusinessException;
import vvpkassistant.mail.model.MailModel;
import vvpkassistant.mail.service.MailService;
import vvpkassistant.pk.mapper.PkInfoDao;
import vvpkassistant.pk.model.PkInfoModel;
import vvpkassistant.pk.model.PkRecordDetail;
import vvpkassistant.pk.mapper.PkRecordDetailDao;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -43,6 +51,10 @@ public class UserController {
@Autowired
private VVRequester vvRequester;
@Resource
private UserService userService;
// 配置用户信息
@PostMapping("inputUserInfo")
public ResponseData<Object> inputUserInfo(@RequestBody Map<String,Object> param) {
@@ -165,29 +177,12 @@ public class UserController {
}
//todo 修改用户返回结果
// 修改用户信息
@PostMapping("updateUserInfo")
public ResponseData<Object> updateUserInfo(@RequestBody Map<String,Object> map) {
UserModel userModel = new UserModel();
//设置用户id
userModel.setId(Integer.valueOf(map.get("id").toString()));
//设置用户头像
userModel.setHeaderIcon(map.get("headerIcon").toString());
//设置用户昵称
userModel.setNickName(map.get("nickName").toString());
int i = userDao.updateById(userModel);
// 返回结果
Map<String,Object> result = new HashMap<>();
result.put("info", userDao.selectById(map.get("id").toString()));
result.put("newAccount", false);
if (i == 1){
return ResponseData.success(result);
}else {
throw new BusinessException(ErrorCode.SYSTEM_ERROR);
}
public ResponseData<Object> updateUserInfo(@RequestBody UserModelDTO userModelDTO) {
UserModelVO userModelVO = userService.updateUserInfo( userModelDTO);
return ResponseData.success(userModelVO);
}
// 获取用户信息
@@ -367,5 +362,13 @@ public class UserController {
return ResponseData.success(coinRecords);
}
@PostMapping("/loginWithMail")
public ResponseData<Object> loginWithMail(@RequestBody UserModelDTO model) {
return ResponseData.success(userService.loginWithMail(model));
}
@PostMapping("/registerWithMail")
public ResponseData<Object> mailRegister(@RequestBody UserModelDTO model){
return ResponseData.success(userService.addUserWithMail(model));
}
}

View File

@@ -0,0 +1,28 @@
package vvpkassistant.User.model.DTO;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
public class UserModelDTO {
@TableId(type = IdType.AUTO)
private Integer id; // 主键
private String nickName; // 昵称
private String phoneNumber; // 手机号码
private String headerIcon; // 头像
private String openid; // openid
private String sessionKey; // session key
private Integer status; // 用户状态 0 正常 其他业务逻辑待定
private Long createTime; // 创建时间
private String userChatId; // 聊天使用的id使用微信的openid作为标识
private Integer points; // 用户积分
private Integer inviterId; // 邀请人id
private String email;
private String newPassword;
private String oldPassword;
private String password;
}

View File

@@ -20,4 +20,7 @@ public class UserModel {
private String userChatId; // 聊天使用的id使用微信的openid作为标识
private Integer points; // 用户积分
private Integer inviterId; // 邀请人id
private String email;
private String password;
}

View File

@@ -0,0 +1,26 @@
package vvpkassistant.User.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import vvpkassistant.Data.WxChatParam;
@Data
public class UserModelVO {
private Integer id; // 主键
private String nickName; // 昵称
private String phoneNumber; // 手机号码
private String headerIcon; // 头像
private String openid; // openid
private String sessionKey; // session key
private Integer status; // 用户状态 0 正常 其他业务逻辑待定
private Long createTime; // 创建时间
private String userChatId; // 聊天使用的id使用微信的openid作为标识
private Integer points; // 用户积分
private Integer inviterId; // 邀请人id
private String email;
private String token;
private Boolean newAccount;
private WxChatParam chatInfo;
}

View File

@@ -0,0 +1,20 @@
package vvpkassistant.User.service;
import com.baomidou.mybatisplus.extension.service.IService;
import vvpkassistant.User.model.DTO.UserModelDTO;
import vvpkassistant.User.model.UserModel;
import vvpkassistant.User.model.UserModelVO;
/*
* @author: ziin
* @date: 2025/8/4 16:19
*/
public interface UserService extends IService<UserModel> {
UserModelVO loginWithMail(UserModelDTO model);
UserModelVO updateUserInfo(UserModelDTO userModelDTO);
UserModelVO addUserWithMail(UserModelDTO model);
}

View File

@@ -0,0 +1,128 @@
package vvpkassistant.User.service;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import vvpkassistant.Data.WxChatParam;
import vvpkassistant.Tools.BcryptUtils;
import vvpkassistant.Tools.VVTools;
import vvpkassistant.User.mapper.UserDao;
import vvpkassistant.User.model.DTO.UserModelDTO;
import vvpkassistant.User.model.UserModel;
import vvpkassistant.User.model.UserModelVO;
import vvpkassistant.common.ErrorCode;
import vvpkassistant.exception.BusinessException;
import vvpkassistant.mail.model.MailModel;
import javax.annotation.Resource;
/*
* @author: ziin
* @date: 2025/8/4 16:19
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserDao, UserModel> implements UserService {
@Resource
private UserDao userDao;
@Resource
private WxChatParam wxChatParam;
@Override
public UserModelVO loginWithMail(UserModelDTO model) {
LambdaQueryWrapper<UserModel> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(UserModel::getEmail,model.getEmail())
.in(UserModel::getStatus, 0,2);
UserModel userModel = userDao.selectOne(lambdaQueryWrapper);
if (userModel == null) {
throw new BusinessException(ErrorCode.USER_DOES_NOT_EXIST);
}
String password = userModel.getPassword();
UserModelVO userModelVO = BeanUtil.copyProperties(userModel, UserModelVO.class);
if (BcryptUtils.matchPassword(password, model.getPassword())) {
StpUtil.login(userModel.getId());
userModelVO.setToken(StpUtil.getTokenValue());
userModelVO.setChatInfo(wxChatParam);
return userModelVO;
}else {
throw new BusinessException(ErrorCode.PASSWORD_ERROR);
}
}
@Override
public UserModelVO updateUserInfo(UserModelDTO userModelDTO) {
UserModel userInfo = userDao.selectById(userModelDTO.getId());
if (userInfo == null) {
throw new BusinessException(ErrorCode.USER_DOES_NOT_EXIST);
}
// 用户没有密码的情况下设置密码
if (userInfo.getPassword() == null) {
if (!userModelDTO.getNewPassword().isEmpty()){
if (userModelDTO.getNewPassword().length()<6){
throw new BusinessException(ErrorCode.PARAMS_ERROR,"密码长度不能小于 6 位");
}
userModelDTO.setPassword(BcryptUtils.encryptPassword(userModelDTO.getNewPassword()));
}
}
// 用户有密码的情况下重新设置密码
if (userInfo.getPassword() != null && userModelDTO.getOldPassword() != null) {
if (BcryptUtils.matchPassword(userModelDTO.getOldPassword(), userInfo.getPassword())) {
userModelDTO.setPassword(BcryptUtils.encryptPassword(userModelDTO.getNewPassword()));
}else {
throw new BusinessException(ErrorCode.PASSWORD_ERROR,"旧密码不正确");
}
}
UserModel userModel = new UserModel();
BeanUtil.copyProperties(userModelDTO, userModel);
int i = userDao.updateById(userModel);
// 返回结果
userDao.selectById(userModel.getId());
UserModelVO userModelVO = BeanUtil.copyProperties(userModel, UserModelVO.class);
userModelVO.setNewAccount(false);
if (i == 1){
return userModelVO;
}else {
throw new BusinessException(ErrorCode.SYSTEM_ERROR);
}
}
@Override
public UserModelVO addUserWithMail(UserModelDTO model) {
LambdaQueryWrapper<UserModel> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(UserModel::getEmail,model.getEmail());
UserModel userModel = userDao.selectOne(lambdaQueryWrapper);
if (userModel != null) {
throw new BusinessException(ErrorCode.MAIL_ALREADY_EXIST);
}
if (model.getPassword().length() < 6 ){
throw new BusinessException(ErrorCode.PARAMS_ERROR,"密码长度不能小于 6 位");
}
model.setPassword(BcryptUtils.encryptPassword(model.getPassword()));
model.setCreateTime(VVTools.currentTimeStamp());
//设置状态为正常
model.setStatus(2);
//设置积分为0
model.setPoints(0);
userDao.insert(BeanUtil.copyProperties(model, UserModel.class));
// 判断用户是否为邀请用户
if (model.getInviterId() != null) {
UserModel oldUser = userDao.selectById(model.getInviterId());
oldUser.setPoints(oldUser.getPoints() + 10);
userDao.updateById(oldUser);
}
UserModelVO userModelVO = BeanUtil.copyProperties(model, UserModelVO.class);
userModelVO.setNewAccount(true);
userModelVO.setChatInfo(wxChatParam);
return userModelVO;
}
}

View File

@@ -27,7 +27,11 @@ public enum ErrorCode {
WX_SYSTEM_BUSY(-1, "系统繁忙"),
/* =============== 登录/用户信息 =============== */
WX_GET_USER_INFO_FAILED(50001, "获取用户信息失败,请稍后再试");
WX_GET_USER_INFO_FAILED(50001, "获取用户信息失败,请稍后再试"),
USER_DOES_NOT_EXIST(5002,"用户不存在"),
MAIL_ALREADY_EXIST(5003,"邮箱已存在"),
PASSWORD_ERROR(5004, "用户名或密码错误" ),
TOKEN_INVALID(40400, "Token无效请重新登录");
/**
* 状态码

View File

@@ -0,0 +1,69 @@
package vvpkassistant.config;
import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@Slf4j
public class SaTokenConfigure implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册Sa-Token的拦截器
registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
.addPathPatterns("/**")
.excludePathPatterns(getExcludePaths());
}
/**
* 获取需要放行的路径
*/
private String[] getExcludePaths() {
return new String[]{
// Swagger & Knife4j 相关
"/doc.html",
"/webjars/**",
"/swagger-resources/**",
"/v2/api-docs",
"/v3/api-docs",
"/v3/api-docs/**",
"/swagger-ui.html",
"/swagger-ui/**",
"/favicon.ico",
// 你的其他放行路径,例如登录接口
"/user/loginWithPhoneNumber",
"/user/registerWithMail",
"/user/loginWithMail"
};
}
@Bean
public SaCorsHandleFunction corsHandle() {
return (req, res, sto) -> {
res.
// 允许指定域访问跨域资源
setHeader("Access-Control-Allow-Origin", "*")
// 允许所有请求方式
.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE")
// 有效时间
.setHeader("Access-Control-Max-Age", "3600")
// 允许的header参数
.setHeader("Access-Control-Allow-Headers", "*");
// 如果是预检请求,则立即返回到前端
SaRouter.match(SaHttpMethod.OPTIONS)
.back();
};
}
}

View File

@@ -1,5 +1,6 @@
package vvpkassistant.exception;
import cn.dev33.satoken.exception.NotLoginException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@@ -28,6 +29,44 @@ public class GlobalExceptionHandler {
log.error("RuntimeException", e);
return ResponseData.error(ErrorCode.SYSTEM_ERROR);
}
// 全局异常拦截拦截项目中的NotLoginException异常
@ExceptionHandler(NotLoginException.class)
public ResponseData<?> handlerNotLoginException(NotLoginException nle)
throws Exception {
// 打印堆栈,以供调试
nle.printStackTrace();
// 判断场景值,定制化异常信息
String message = "";
if(nle.getType().equals(NotLoginException.NOT_TOKEN)) {
message = "未能读取到有效用户令牌";
}
else if(nle.getType().equals(NotLoginException.INVALID_TOKEN)) {
message = "令牌无效";
}
else if(nle.getType().equals(NotLoginException.TOKEN_TIMEOUT)) {
message = "令牌已过期";
}
else if(nle.getType().equals(NotLoginException.BE_REPLACED)) {
message = "令牌已被顶下线";
}
else if(nle.getType().equals(NotLoginException.KICK_OUT)) {
message = "令牌已被踢下线";
}
else if(nle.getType().equals(NotLoginException.TOKEN_FREEZE)) {
message = "令牌已被冻结";
}
else if(nle.getType().equals(NotLoginException.NO_PREFIX)) {
message = "未按照指定前缀提交令牌";
}
else {
message = "当前会话未登录";
}
// 返回给前端
return ResponseData.error(ErrorCode.TOKEN_INVALID.getCode(),message);
}
}

View File

@@ -0,0 +1,13 @@
package vvpkassistant.mail.model;
import lombok.Data;
/*
* @author: ziin
* @date: 2025/8/4 15:48
*/
@Data
public class MailModel {
private String mailAddress;
private Integer code;
}

View File

@@ -0,0 +1,11 @@
package vvpkassistant.mail.service;
import vvpkassistant.mail.model.MailModel;
/*
* @author: ziin
* @date: 2025/8/4 15:42
*/
public interface MailService {
Boolean sendMail(MailModel model);
}

View File

@@ -0,0 +1,62 @@
package vvpkassistant.mail.service;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.mailgun.api.v3.MailgunMessagesApi;
import com.mailgun.client.MailgunClient;
import com.mailgun.model.message.Message;
import com.mailgun.model.message.MessageResponse;
import feign.AsyncClient;
import feign.Client;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import vvpkassistant.mail.model.MailModel;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/*
* @author: ziin
* @date: 2025/8/4 15:42
*/
@Service
public class MailServiceImpl implements MailService {
@Value("${mailgun}")
private String mailgunKey;
public final Cache<String, String> codeCache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES) // 5 分钟过期
.maximumSize(10_000) // 防止内存暴涨
.build();
@Override
public Boolean sendMail(MailModel model) {
ExecutorService executor = Executors.newFixedThreadPool(2);
AsyncClient.Default<Object> asyncClient = new AsyncClient.Default<>(
new Client.Default(null, null), executor);
MailgunMessagesApi mailgunAsyncMessagesApi = MailgunClient.config(mailgunKey)
.client(asyncClient)
.createAsyncApi(MailgunMessagesApi.class);
Message message = Message.builder()
.from("")
.to(model.getMailAddress())
.subject("your mail address Verification code is : ")
.html("<html>\n" +
"<body>\n" +
"\t<h1>Sending HTML emails with Mailgun</h1>\n" +
"\t<p style=\"color:blue; font-size:30px;\">Hello world</p>\n" +
"\t<p style=\"font-size:30px;\">More examples can be found <a href=\"https://documentation.mailgun.com/en/latest/api-sending.html#examples\">here</a></p>\n" +
"</body>\n" +
"</html>")
.build();
MessageResponse messageResponse = mailgunAsyncMessagesApi.sendMessage("www.baidu.com", message);
return true;
}
}

View File

@@ -7,3 +7,22 @@ spring:
mailgun: b4888f65c7fd452213f3a8aea9d88269-812b35f5-006d7fd6
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: token
# token 有效期(单位:秒) 默认30天-1 代表永久有效
timeout: 2592000
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
active-timeout: -1
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token
is-share: false
# token 风格默认可取值uuid、simple-uuid、random-32、random-64、random-128、tik
token-style: random-128
# 是否输出操作日志
is-log: true

View File

@@ -7,3 +7,22 @@ spring:
mailgun: b4888f65c7fd452213f3a8aea9d88269-812b35f5-006d7fd6
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: token
# token 有效期(单位:秒) 默认30天-1 代表永久有效
timeout: 2592000
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
active-timeout: -1
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token
is-share: false
# token 风格默认可取值uuid、simple-uuid、random-32、random-64、random-128、tik
token-style: random-128
# 是否输出操作日志
is-log: true