新增登录注册
This commit is contained in:
86
src/main/java/com/corewing/app/common/Result.java
Normal file
86
src/main/java/com/corewing/app/common/Result.java
Normal file
@@ -0,0 +1,86 @@
|
||||
package com.corewing.app.common;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 统一返回结果类
|
||||
*/
|
||||
@Data
|
||||
public class Result<T> implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 状态码
|
||||
*/
|
||||
private Integer code;
|
||||
|
||||
/**
|
||||
* 返回消息
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 返回数据
|
||||
*/
|
||||
private T data;
|
||||
|
||||
/**
|
||||
* 成功标识
|
||||
*/
|
||||
private Boolean success;
|
||||
|
||||
public Result() {
|
||||
}
|
||||
|
||||
public Result(Integer code, String message, T data, Boolean success) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
this.data = data;
|
||||
this.success = success;
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功返回(无数据)
|
||||
*/
|
||||
public static <T> Result<T> success() {
|
||||
return new Result<>(200, "操作成功", null, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功返回(带数据)
|
||||
*/
|
||||
public static <T> Result<T> success(T data) {
|
||||
return new Result<>(200, "操作成功", data, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功返回(自定义消息和数据)
|
||||
*/
|
||||
public static <T> Result<T> success(String message, T data) {
|
||||
return new Result<>(200, message, data, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败返回
|
||||
*/
|
||||
public static <T> Result<T> error() {
|
||||
return new Result<>(500, "操作失败", null, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败返回(自定义消息)
|
||||
*/
|
||||
public static <T> Result<T> error(String message) {
|
||||
return new Result<>(500, message, null, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败返回(自定义状态码和消息)
|
||||
*/
|
||||
public static <T> Result<T> error(Integer code, String message) {
|
||||
return new Result<>(code, message, null, false);
|
||||
}
|
||||
}
|
||||
62
src/main/java/com/corewing/app/config/DruidConfig.java
Normal file
62
src/main/java/com/corewing/app/config/DruidConfig.java
Normal file
@@ -0,0 +1,62 @@
|
||||
package com.corewing.app.config;
|
||||
|
||||
import com.alibaba.druid.support.http.StatViewServlet;
|
||||
import com.alibaba.druid.support.http.WebStatFilter;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.boot.web.servlet.ServletRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Druid 监控配置类
|
||||
*/
|
||||
@Configuration
|
||||
public class DruidConfig {
|
||||
|
||||
/**
|
||||
* 配置 Druid 监控后台 Servlet
|
||||
*/
|
||||
@Bean
|
||||
public ServletRegistrationBean<StatViewServlet> statViewServlet() {
|
||||
ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(
|
||||
new StatViewServlet(), "/druid/*"
|
||||
);
|
||||
|
||||
Map<String, String> initParams = new HashMap<>();
|
||||
// 监控页面登录用户名
|
||||
initParams.put("loginUsername", "admin");
|
||||
// 监控页面登录密码
|
||||
initParams.put("loginPassword", "admin123");
|
||||
// 允许访问的IP,默认允许所有
|
||||
initParams.put("allow", "");
|
||||
// 禁止访问的IP
|
||||
initParams.put("deny", "");
|
||||
// 禁用HTML页面上的"Reset All"功能
|
||||
initParams.put("resetEnable", "false");
|
||||
|
||||
bean.setInitParameters(initParams);
|
||||
return bean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置 Druid Web 监控 Filter
|
||||
*/
|
||||
@Bean
|
||||
public FilterRegistrationBean<WebStatFilter> webStatFilter() {
|
||||
FilterRegistrationBean<WebStatFilter> bean = new FilterRegistrationBean<>();
|
||||
bean.setFilter(new WebStatFilter());
|
||||
|
||||
Map<String, String> initParams = new HashMap<>();
|
||||
// 排除不需要监控的资源
|
||||
initParams.put("exclusions", "*.js,*.css,*.gif,*.jpg,*.png,*.ico,/druid/*");
|
||||
|
||||
bean.setInitParameters(initParams);
|
||||
// 拦截所有请求
|
||||
bean.addUrlPatterns("/*");
|
||||
|
||||
return bean;
|
||||
}
|
||||
}
|
||||
37
src/main/java/com/corewing/app/config/MybatisPlusConfig.java
Normal file
37
src/main/java/com/corewing/app/config/MybatisPlusConfig.java
Normal file
@@ -0,0 +1,37 @@
|
||||
package com.corewing.app.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* MyBatis-Plus 配置类
|
||||
*/
|
||||
@Configuration
|
||||
@MapperScan("com.corewing.app.mapper")
|
||||
public class MybatisPlusConfig {
|
||||
|
||||
/**
|
||||
* MyBatis-Plus 插件配置
|
||||
*/
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
|
||||
// 分页插件
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
|
||||
|
||||
// 乐观锁插件
|
||||
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
|
||||
|
||||
// 防止全表更新与删除插件
|
||||
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
|
||||
|
||||
return interceptor;
|
||||
}
|
||||
}
|
||||
53
src/main/java/com/corewing/app/config/RedisConfig.java
Normal file
53
src/main/java/com/corewing/app/config/RedisConfig.java
Normal file
@@ -0,0 +1,53 @@
|
||||
package com.corewing.app.config;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
/**
|
||||
* Redis 配置类
|
||||
*/
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
public class RedisConfig {
|
||||
|
||||
/**
|
||||
* RedisTemplate 配置
|
||||
*/
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
|
||||
// 使用 Jackson2JsonRedisSerializer 来序列化和反序列化 redis 的 value 值
|
||||
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
|
||||
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
|
||||
serializer.setObjectMapper(mapper);
|
||||
|
||||
// 使用 StringRedisSerializer 来序列化和反序列化 redis 的 key
|
||||
StringRedisSerializer stringSerializer = new StringRedisSerializer();
|
||||
|
||||
// key 采用 String 的序列化方式
|
||||
template.setKeySerializer(stringSerializer);
|
||||
// hash 的 key 也采用 String 的序列化方式
|
||||
template.setHashKeySerializer(stringSerializer);
|
||||
// value 序列化方式采用 jackson
|
||||
template.setValueSerializer(serializer);
|
||||
// hash 的 value 序列化方式采用 jackson
|
||||
template.setHashValueSerializer(serializer);
|
||||
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
}
|
||||
37
src/main/java/com/corewing/app/config/SaTokenConfig.java
Normal file
37
src/main/java/com/corewing/app/config/SaTokenConfig.java
Normal file
@@ -0,0 +1,37 @@
|
||||
package com.corewing.app.config;
|
||||
|
||||
import cn.dev33.satoken.interceptor.SaInterceptor;
|
||||
import cn.dev33.satoken.router.SaRouter;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* Sa-Token 配置类
|
||||
*/
|
||||
@Configuration
|
||||
public class SaTokenConfig implements WebMvcConfigurer {
|
||||
|
||||
/**
|
||||
* 注册 Sa-Token 拦截器
|
||||
*/
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验。
|
||||
registry.addInterceptor(new SaInterceptor(handle -> {
|
||||
// 指定一条 match 规则
|
||||
SaRouter
|
||||
// 拦截所有路由
|
||||
.match("/**")
|
||||
// 排除登录、注册、发送验证码接口
|
||||
.notMatch("/user/login", "/user/register", "/user/sendCode")
|
||||
// 排除静态资源
|
||||
.notMatch("/", "/index.html", "/*.html", "/*.css", "/*.js", "/*.ico", "/static/**")
|
||||
// 排除 Druid 监控
|
||||
.notMatch("/druid/**")
|
||||
// 执行认证校验
|
||||
.check(r -> StpUtil.checkLogin());
|
||||
})).addPathPatterns("/**");
|
||||
}
|
||||
}
|
||||
173
src/main/java/com/corewing/app/controller/AppUserController.java
Normal file
173
src/main/java/com/corewing/app/controller/AppUserController.java
Normal file
@@ -0,0 +1,173 @@
|
||||
package com.corewing.app.controller;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import com.corewing.app.common.Result;
|
||||
import com.corewing.app.dto.LoginRequest;
|
||||
import com.corewing.app.dto.RegisterRequest;
|
||||
import com.corewing.app.dto.SendCodeRequest;
|
||||
import com.corewing.app.dto.UpdatePasswordRequest;
|
||||
import com.corewing.app.entity.AppUser;
|
||||
import com.corewing.app.service.AppUserService;
|
||||
import com.corewing.app.service.VerifyCodeService;
|
||||
import com.corewing.app.util.IpUtil;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 应用用户 Controller
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/user")
|
||||
public class AppUserController {
|
||||
|
||||
private final AppUserService userService;
|
||||
private final VerifyCodeService verifyCodeService;
|
||||
|
||||
public AppUserController(AppUserService userService, VerifyCodeService verifyCodeService) {
|
||||
this.userService = userService;
|
||||
this.verifyCodeService = verifyCodeService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送验证码
|
||||
*/
|
||||
@PostMapping("/sendCode")
|
||||
public Result<String> sendCode(@RequestBody SendCodeRequest request) {
|
||||
try {
|
||||
boolean success = verifyCodeService.sendCode(request.getAccount(), request.getType());
|
||||
if (success) {
|
||||
return Result.success("验证码发送成功");
|
||||
}
|
||||
return Result.error("验证码发送失败");
|
||||
} catch (Exception e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录(支持用户名/邮箱/手机号)
|
||||
*/
|
||||
@PostMapping("/login")
|
||||
public Result<Map<String, Object>> login(@RequestBody LoginRequest request, HttpServletRequest httpRequest) {
|
||||
try {
|
||||
String token = userService.login(request.getAccount(), request.getPassword());
|
||||
|
||||
// 更新登录IP
|
||||
AppUser user = userService.getByAccount(request.getAccount());
|
||||
String loginIp = IpUtil.getClientIp(httpRequest);
|
||||
userService.updateLoginIp(user.getId(), loginIp);
|
||||
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("token", token);
|
||||
data.put("userId", user.getId());
|
||||
data.put("username", user.getUsername());
|
||||
|
||||
return Result.success("登录成功", data);
|
||||
} catch (Exception e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册(需要验证码)
|
||||
*/
|
||||
@PostMapping("/register")
|
||||
public Result<String> register(@RequestBody RegisterRequest request) {
|
||||
try {
|
||||
AppUser user = new AppUser();
|
||||
user.setUsername(request.getUsername());
|
||||
user.setPassword(request.getPassword());
|
||||
user.setEmail(request.getEmail());
|
||||
user.setTelephone(request.getTelephone());
|
||||
user.setAvatar(request.getAvatar());
|
||||
|
||||
userService.register(user, request.getCode());
|
||||
return Result.success("注册成功");
|
||||
} catch (Exception e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
public Result<String> logout() {
|
||||
StpUtil.logout();
|
||||
return Result.success("登出成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户信息
|
||||
*/
|
||||
@GetMapping("/info")
|
||||
public Result<AppUser> getUserInfo() {
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
AppUser user = userService.getById(userId);
|
||||
// 隐藏密码
|
||||
user.setPassword(null);
|
||||
return Result.success(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询用户
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public Result<AppUser> getById(@PathVariable Long id) {
|
||||
AppUser user = userService.getById(id);
|
||||
if (user != null) {
|
||||
// 隐藏密码
|
||||
user.setPassword(null);
|
||||
return Result.success(user);
|
||||
}
|
||||
return Result.error("用户不存在");
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
*/
|
||||
@PutMapping
|
||||
public Result<String> update(@RequestBody AppUser user) {
|
||||
// 不允许通过此接口修改密码
|
||||
user.setPassword(null);
|
||||
|
||||
boolean success = userService.updateById(user);
|
||||
if (success) {
|
||||
return Result.success("更新成功");
|
||||
}
|
||||
return Result.error("更新失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*/
|
||||
@PutMapping("/password")
|
||||
public Result<String> updatePassword(@RequestBody UpdatePasswordRequest request) {
|
||||
try {
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
AppUser user = userService.getById(userId);
|
||||
|
||||
// 验证旧密码
|
||||
String oldPasswordMd5 = org.springframework.util.DigestUtils.md5DigestAsHex(
|
||||
request.getOldPassword().getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
|
||||
if (!oldPasswordMd5.equals(user.getPassword())) {
|
||||
return Result.error("原密码错误");
|
||||
}
|
||||
|
||||
// 更新新密码
|
||||
String newPasswordMd5 = org.springframework.util.DigestUtils.md5DigestAsHex(
|
||||
request.getNewPassword().getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
|
||||
user.setPassword(newPasswordMd5);
|
||||
userService.updateById(user);
|
||||
|
||||
return Result.success("密码修改成功");
|
||||
} catch (Exception e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/main/java/com/corewing/app/dto/LoginRequest.java
Normal file
20
src/main/java/com/corewing/app/dto/LoginRequest.java
Normal file
@@ -0,0 +1,20 @@
|
||||
package com.corewing.app.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 登录请求参数
|
||||
*/
|
||||
@Data
|
||||
public class LoginRequest {
|
||||
|
||||
/**
|
||||
* 账号(用户名/邮箱/手机号)
|
||||
*/
|
||||
private String account;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
private String password;
|
||||
}
|
||||
45
src/main/java/com/corewing/app/dto/RegisterRequest.java
Normal file
45
src/main/java/com/corewing/app/dto/RegisterRequest.java
Normal file
@@ -0,0 +1,45 @@
|
||||
package com.corewing.app.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 用户注册请求
|
||||
*/
|
||||
@Data
|
||||
public class RegisterRequest {
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 邮箱(邮箱和手机号至少填一个)
|
||||
*/
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 手机号(邮箱和手机号至少填一个)
|
||||
*/
|
||||
private String telephone;
|
||||
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
@NotBlank(message = "验证码不能为空")
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 头像URL
|
||||
*/
|
||||
private String avatar;
|
||||
}
|
||||
24
src/main/java/com/corewing/app/dto/SendCodeRequest.java
Normal file
24
src/main/java/com/corewing/app/dto/SendCodeRequest.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package com.corewing.app.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 发送验证码请求
|
||||
*/
|
||||
@Data
|
||||
public class SendCodeRequest {
|
||||
|
||||
/**
|
||||
* 手机号或邮箱
|
||||
*/
|
||||
@NotBlank(message = "手机号或邮箱不能为空")
|
||||
private String account;
|
||||
|
||||
/**
|
||||
* 验证码类型:register-注册, login-登录, reset-重置密码
|
||||
*/
|
||||
@NotBlank(message = "验证码类型不能为空")
|
||||
private String type;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.corewing.app.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 修改密码请求参数
|
||||
*/
|
||||
@Data
|
||||
public class UpdatePasswordRequest {
|
||||
|
||||
/**
|
||||
* 旧密码
|
||||
*/
|
||||
private String oldPassword;
|
||||
|
||||
/**
|
||||
* 新密码
|
||||
*/
|
||||
private String newPassword;
|
||||
}
|
||||
74
src/main/java/com/corewing/app/entity/AppUser.java
Normal file
74
src/main/java/com/corewing/app/entity/AppUser.java
Normal file
@@ -0,0 +1,74 @@
|
||||
package com.corewing.app.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
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 lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 应用用户实体类
|
||||
*/
|
||||
@Data
|
||||
@TableName("app_user")
|
||||
public class AppUser implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
private String telephone;
|
||||
|
||||
/**
|
||||
* 头像URL
|
||||
*/
|
||||
private String avatar;
|
||||
|
||||
/**
|
||||
* 最后登录IP
|
||||
*/
|
||||
private String loginIp;
|
||||
|
||||
/**
|
||||
* 状态:0-禁用 1-启用
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.corewing.app.handler;
|
||||
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* MyBatis-Plus 自动填充处理器
|
||||
* 用于自动填充创建时间和更新时间
|
||||
*/
|
||||
@Component
|
||||
public class MyMetaObjectHandler implements MetaObjectHandler {
|
||||
|
||||
/**
|
||||
* 插入时的填充策略
|
||||
*/
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
// 自动填充创建时间
|
||||
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
|
||||
// 自动填充更新时间
|
||||
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新时的填充策略
|
||||
*/
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
// 自动填充更新时间
|
||||
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
|
||||
}
|
||||
}
|
||||
13
src/main/java/com/corewing/app/mapper/AppUserMapper.java
Normal file
13
src/main/java/com/corewing/app/mapper/AppUserMapper.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.corewing.app.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.corewing.app.entity.AppUser;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 应用用户 Mapper 接口
|
||||
*/
|
||||
@Mapper
|
||||
public interface AppUserMapper extends BaseMapper<AppUser> {
|
||||
|
||||
}
|
||||
68
src/main/java/com/corewing/app/service/AppUserService.java
Normal file
68
src/main/java/com/corewing/app/service/AppUserService.java
Normal file
@@ -0,0 +1,68 @@
|
||||
package com.corewing.app.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.corewing.app.entity.AppUser;
|
||||
|
||||
/**
|
||||
* 应用用户 Service 接口
|
||||
*/
|
||||
public interface AppUserService extends IService<AppUser> {
|
||||
|
||||
/**
|
||||
* 根据用户名查询用户
|
||||
*
|
||||
* @param username 用户名
|
||||
* @return 用户信息
|
||||
*/
|
||||
AppUser getByUsername(String username);
|
||||
|
||||
/**
|
||||
* 根据账号查询用户(支持用户名/邮箱/手机号)
|
||||
*
|
||||
* @param account 账号
|
||||
* @return 用户信息
|
||||
*/
|
||||
AppUser getByAccount(String account);
|
||||
|
||||
/**
|
||||
* 根据邮箱查询用户
|
||||
*
|
||||
* @param email 邮箱
|
||||
* @return 用户信息
|
||||
*/
|
||||
AppUser getByEmail(String email);
|
||||
|
||||
/**
|
||||
* 根据手机号查询用户
|
||||
*
|
||||
* @param telephone 手机号
|
||||
* @return 用户信息
|
||||
*/
|
||||
AppUser getByTelephone(String telephone);
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*
|
||||
* @param account 账号(用户名/邮箱/手机号)
|
||||
* @param password 密码
|
||||
* @return token
|
||||
*/
|
||||
String login(String account, String password);
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*
|
||||
* @param user 用户信息
|
||||
* @param code 验证码
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean register(AppUser user, String code);
|
||||
|
||||
/**
|
||||
* 更新登录IP
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param loginIp 登录IP
|
||||
*/
|
||||
void updateLoginIp(Long userId, String loginIp);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.corewing.app.service;
|
||||
|
||||
/**
|
||||
* 验证码服务接口
|
||||
*/
|
||||
public interface VerifyCodeService {
|
||||
|
||||
/**
|
||||
* 发送验证码
|
||||
*
|
||||
* @param account 账号(手机号或邮箱)
|
||||
* @param type 验证码类型(register-注册, login-登录, reset-重置密码)
|
||||
* @return 是否发送成功
|
||||
*/
|
||||
boolean sendCode(String account, String type);
|
||||
|
||||
/**
|
||||
* 验证验证码
|
||||
*
|
||||
* @param account 账号(手机号或邮箱)
|
||||
* @param code 验证码
|
||||
* @param type 验证码类型
|
||||
* @return 是否验证成功
|
||||
*/
|
||||
boolean verifyCode(String account, String code, String type);
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package com.corewing.app.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.corewing.app.entity.AppUser;
|
||||
import com.corewing.app.mapper.AppUserMapper;
|
||||
import com.corewing.app.service.AppUserService;
|
||||
import com.corewing.app.service.VerifyCodeService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.DigestUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* 应用用户 Service 实现类
|
||||
*/
|
||||
@Service
|
||||
public class AppUserServiceImpl extends ServiceImpl<AppUserMapper, AppUser> implements AppUserService {
|
||||
|
||||
private final VerifyCodeService verifyCodeService;
|
||||
|
||||
public AppUserServiceImpl(VerifyCodeService verifyCodeService) {
|
||||
this.verifyCodeService = verifyCodeService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AppUser getByUsername(String username) {
|
||||
LambdaQueryWrapper<AppUser> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(AppUser::getUsername, username);
|
||||
return this.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AppUser getByAccount(String account) {
|
||||
if (!StringUtils.hasText(account)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<AppUser> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(AppUser::getUsername, account)
|
||||
.or()
|
||||
.eq(AppUser::getEmail, account)
|
||||
.or()
|
||||
.eq(AppUser::getTelephone, account);
|
||||
return this.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AppUser getByEmail(String email) {
|
||||
if (!StringUtils.hasText(email)) {
|
||||
return null;
|
||||
}
|
||||
LambdaQueryWrapper<AppUser> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(AppUser::getEmail, email);
|
||||
return this.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AppUser getByTelephone(String telephone) {
|
||||
if (!StringUtils.hasText(telephone)) {
|
||||
return null;
|
||||
}
|
||||
LambdaQueryWrapper<AppUser> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(AppUser::getTelephone, telephone);
|
||||
return this.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String login(String account, String password) {
|
||||
// 查询用户(支持用户名/邮箱/手机号)
|
||||
AppUser user = getByAccount(account);
|
||||
if (user == null) {
|
||||
throw new RuntimeException("用户不存在");
|
||||
}
|
||||
|
||||
// 验证密码(MD5加密)
|
||||
String encryptPassword = DigestUtils.md5DigestAsHex(password.getBytes(StandardCharsets.UTF_8));
|
||||
if (!encryptPassword.equals(user.getPassword())) {
|
||||
throw new RuntimeException("密码错误");
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
if (user.getStatus() == 0) {
|
||||
throw new RuntimeException("账号已被禁用");
|
||||
}
|
||||
|
||||
// 登录成功,使用 Sa-Token 生成 token
|
||||
StpUtil.login(user.getId());
|
||||
return StpUtil.getTokenValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean register(AppUser user, String code) {
|
||||
// 检查用户名是否已存在
|
||||
AppUser existUser = getByUsername(user.getUsername());
|
||||
if (existUser != null) {
|
||||
throw new RuntimeException("用户名已存在");
|
||||
}
|
||||
|
||||
// 邮箱和手机号至少要有一个
|
||||
if (!StringUtils.hasText(user.getEmail()) && !StringUtils.hasText(user.getTelephone())) {
|
||||
throw new RuntimeException("邮箱和手机号至少填写一个");
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
if (StringUtils.hasText(user.getEmail())) {
|
||||
existUser = getByEmail(user.getEmail());
|
||||
if (existUser != null) {
|
||||
throw new RuntimeException("邮箱已被使用");
|
||||
}
|
||||
}
|
||||
|
||||
// 检查手机号是否已存在
|
||||
if (StringUtils.hasText(user.getTelephone())) {
|
||||
existUser = getByTelephone(user.getTelephone());
|
||||
if (existUser != null) {
|
||||
throw new RuntimeException("手机号已被使用");
|
||||
}
|
||||
}
|
||||
|
||||
// 验证验证码
|
||||
String account = StringUtils.hasText(user.getTelephone()) ? user.getTelephone() : user.getEmail();
|
||||
boolean codeValid = verifyCodeService.verifyCode(account, code, "register");
|
||||
if (!codeValid) {
|
||||
throw new RuntimeException("验证码错误或已过期");
|
||||
}
|
||||
|
||||
// 密码加密(MD5)
|
||||
String encryptPassword = DigestUtils.md5DigestAsHex(user.getPassword().getBytes(StandardCharsets.UTF_8));
|
||||
user.setPassword(encryptPassword);
|
||||
|
||||
// 设置默认状态为启用
|
||||
if (user.getStatus() == null) {
|
||||
user.setStatus(1);
|
||||
}
|
||||
|
||||
// 保存用户
|
||||
return this.save(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateLoginIp(Long userId, String loginIp) {
|
||||
AppUser user = new AppUser();
|
||||
user.setId(userId);
|
||||
user.setLoginIp(loginIp);
|
||||
this.updateById(user);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package com.corewing.app.service.impl;
|
||||
|
||||
import com.corewing.app.service.VerifyCodeService;
|
||||
import com.corewing.app.util.EmailUtil;
|
||||
import com.corewing.app.util.RedisUtil;
|
||||
import com.corewing.app.util.SmsBaoUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 验证码服务实现类
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class VerifyCodeServiceImpl implements VerifyCodeService {
|
||||
|
||||
private final RedisUtil redisUtil;
|
||||
private final SmsBaoUtil smsBaoUtil;
|
||||
private final EmailUtil emailUtil;
|
||||
|
||||
/**
|
||||
* 验证码有效期(分钟)
|
||||
*/
|
||||
private static final int CODE_EXPIRE_MINUTES = 5;
|
||||
|
||||
/**
|
||||
* 验证码长度
|
||||
*/
|
||||
private static final int CODE_LENGTH = 6;
|
||||
|
||||
/**
|
||||
* 手机号正则
|
||||
*/
|
||||
private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
|
||||
|
||||
/**
|
||||
* 邮箱正则
|
||||
*/
|
||||
private static final Pattern EMAIL_PATTERN = Pattern.compile("^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$");
|
||||
|
||||
public VerifyCodeServiceImpl(RedisUtil redisUtil, SmsBaoUtil smsBaoUtil, EmailUtil emailUtil) {
|
||||
this.redisUtil = redisUtil;
|
||||
this.smsBaoUtil = smsBaoUtil;
|
||||
this.emailUtil = emailUtil;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean sendCode(String account, String type) {
|
||||
if (!StringUtils.hasText(account)) {
|
||||
throw new RuntimeException("账号不能为空");
|
||||
}
|
||||
|
||||
if (!StringUtils.hasText(type)) {
|
||||
throw new RuntimeException("验证码类型不能为空");
|
||||
}
|
||||
|
||||
// 判断是手机号还是邮箱
|
||||
boolean isPhone = PHONE_PATTERN.matcher(account).matches();
|
||||
boolean isEmail = EMAIL_PATTERN.matcher(account).matches();
|
||||
|
||||
if (!isPhone && !isEmail) {
|
||||
throw new RuntimeException("请输入正确的手机号或邮箱");
|
||||
}
|
||||
|
||||
// 生成验证码
|
||||
String code = generateCode();
|
||||
|
||||
// 保存到 Redis,key格式:verify_code:{type}:{account}
|
||||
String redisKey = String.format("verify_code:%s:%s", type, account);
|
||||
redisUtil.set(redisKey, code, CODE_EXPIRE_MINUTES * 60);
|
||||
|
||||
log.info("验证码已生成: account={}, type={}, code={}", account, type, code);
|
||||
|
||||
// 发送验证码
|
||||
if (isPhone) {
|
||||
// 发送短信验证码
|
||||
boolean success = smsBaoUtil.sendVerifyCode(account, code);
|
||||
if (!success) {
|
||||
throw new RuntimeException("短信发送失败,请稍后重试");
|
||||
}
|
||||
} else {
|
||||
// 发送邮件验证码
|
||||
boolean success = emailUtil.sendVerifyCode(account, code);
|
||||
if (!success) {
|
||||
throw new RuntimeException("邮件发送失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verifyCode(String account, String code, String type) {
|
||||
if (!StringUtils.hasText(account) || !StringUtils.hasText(code) || !StringUtils.hasText(type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 从 Redis 获取验证码
|
||||
String redisKey = String.format("verify_code:%s:%s", type, account);
|
||||
Object savedCode = redisUtil.get(redisKey);
|
||||
|
||||
if (savedCode == null) {
|
||||
log.warn("验证码不存在或已过期: account={}, type={}", account, type);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证码比对
|
||||
boolean matched = code.equals(savedCode.toString());
|
||||
|
||||
if (matched) {
|
||||
// 验证成功后删除验证码
|
||||
redisUtil.del(redisKey);
|
||||
log.info("验证码验证成功: account={}, type={}", account, type);
|
||||
} else {
|
||||
log.warn("验证码错误: account={}, type={}, inputCode={}, savedCode={}",
|
||||
account, type, code, savedCode);
|
||||
}
|
||||
|
||||
return matched;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机验证码
|
||||
*
|
||||
* @return 验证码
|
||||
*/
|
||||
private String generateCode() {
|
||||
Random random = new Random();
|
||||
StringBuilder code = new StringBuilder();
|
||||
for (int i = 0; i < CODE_LENGTH; i++) {
|
||||
code.append(random.nextInt(10));
|
||||
}
|
||||
return code.toString();
|
||||
}
|
||||
}
|
||||
141
src/main/java/com/corewing/app/util/EmailUtil.java
Normal file
141
src/main/java/com/corewing/app/util/EmailUtil.java
Normal file
@@ -0,0 +1,141 @@
|
||||
package com.corewing.app.util;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.mail.SimpleMailMessage;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.mail.MessagingException;
|
||||
import javax.mail.internet.MimeMessage;
|
||||
|
||||
/**
|
||||
* 邮件发送工具类
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class EmailUtil {
|
||||
|
||||
private final JavaMailSender mailSender;
|
||||
|
||||
@Value("${spring.mail.username}")
|
||||
private String from;
|
||||
|
||||
public EmailUtil(JavaMailSender mailSender) {
|
||||
this.mailSender = mailSender;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送简单文本邮件
|
||||
*
|
||||
* @param to 收件人邮箱
|
||||
* @param subject 邮件主题
|
||||
* @param content 邮件内容
|
||||
* @return 是否发送成功
|
||||
*/
|
||||
public boolean sendSimpleMail(String to, String subject, String content) {
|
||||
try {
|
||||
SimpleMailMessage message = new SimpleMailMessage();
|
||||
message.setFrom(from);
|
||||
message.setTo(to);
|
||||
message.setSubject(subject);
|
||||
message.setText(content);
|
||||
|
||||
mailSender.send(message);
|
||||
log.info("简单文本邮件发送成功, 收件人: {}, 主题: {}", to, subject);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("简单文本邮件发送失败, 收件人: {}, 主题: {}, 异常信息: {}", to, subject, e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送HTML邮件
|
||||
*
|
||||
* @param to 收件人邮箱
|
||||
* @param subject 邮件主题
|
||||
* @param content HTML内容
|
||||
* @return 是否发送成功
|
||||
*/
|
||||
public boolean sendHtmlMail(String to, String subject, String content) {
|
||||
try {
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
||||
|
||||
helper.setFrom(from);
|
||||
helper.setTo(to);
|
||||
helper.setSubject(subject);
|
||||
helper.setText(content, true);
|
||||
|
||||
mailSender.send(message);
|
||||
log.info("HTML邮件发送成功, 收件人: {}, 主题: {}", to, subject);
|
||||
return true;
|
||||
} catch (MessagingException e) {
|
||||
log.error("HTML邮件发送失败, 收件人: {}, 主题: {}, 异常信息: {}", to, subject, e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送验证码邮件
|
||||
*
|
||||
* @param to 收件人邮箱
|
||||
* @param code 验证码
|
||||
* @return 是否发送成功
|
||||
*/
|
||||
public boolean sendVerifyCode(String to, String code) {
|
||||
String subject = "【CoreWing】验证码";
|
||||
String content = buildVerifyCodeHtml(code);
|
||||
return sendHtmlMail(to, subject, content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建验证码邮件HTML内容
|
||||
*
|
||||
* @param code 验证码
|
||||
* @return HTML内容
|
||||
*/
|
||||
private String buildVerifyCodeHtml(String code) {
|
||||
return "<!DOCTYPE html>" +
|
||||
"<html>" +
|
||||
"<head>" +
|
||||
"<meta charset=\"UTF-8\">" +
|
||||
"<style>" +
|
||||
"body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }" +
|
||||
".container { max-width: 600px; margin: 0 auto; padding: 20px; }" +
|
||||
".header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; text-align: center; border-radius: 5px 5px 0 0; }" +
|
||||
".content { background: #f9f9f9; padding: 30px; border-radius: 0 0 5px 5px; }" +
|
||||
".code-box { background: white; border: 2px dashed #667eea; padding: 20px; margin: 20px 0; text-align: center; border-radius: 5px; }" +
|
||||
".code { font-size: 32px; font-weight: bold; color: #667eea; letter-spacing: 5px; }" +
|
||||
".tips { color: #666; font-size: 14px; margin-top: 20px; }" +
|
||||
".footer { text-align: center; margin-top: 20px; color: #999; font-size: 12px; }" +
|
||||
"</style>" +
|
||||
"</head>" +
|
||||
"<body>" +
|
||||
"<div class=\"container\">" +
|
||||
"<div class=\"header\">" +
|
||||
"<h2>CoreWing 验证码</h2>" +
|
||||
"</div>" +
|
||||
"<div class=\"content\">" +
|
||||
"<p>您好!</p>" +
|
||||
"<p>您正在进行身份验证,您的验证码是:</p>" +
|
||||
"<div class=\"code-box\">" +
|
||||
"<div class=\"code\">" + code + "</div>" +
|
||||
"</div>" +
|
||||
"<div class=\"tips\">" +
|
||||
"<p>⏰ 验证码有效期为 <strong>5分钟</strong>,请尽快使用。</p>" +
|
||||
"<p>🔒 为了您的账户安全,请勿将验证码告知他人。</p>" +
|
||||
"<p>❓ 如果这不是您本人的操作,请忽略此邮件。</p>" +
|
||||
"</div>" +
|
||||
"</div>" +
|
||||
"<div class=\"footer\">" +
|
||||
"<p>此邮件由系统自动发送,请勿回复。</p>" +
|
||||
"<p>© 2025 CoreWing. All rights reserved.</p>" +
|
||||
"</div>" +
|
||||
"</div>" +
|
||||
"</body>" +
|
||||
"</html>";
|
||||
}
|
||||
}
|
||||
44
src/main/java/com/corewing/app/util/IpUtil.java
Normal file
44
src/main/java/com/corewing/app/util/IpUtil.java
Normal file
@@ -0,0 +1,44 @@
|
||||
package com.corewing.app.util;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* IP 工具类
|
||||
*/
|
||||
public class IpUtil {
|
||||
|
||||
/**
|
||||
* 获取客户端真实IP
|
||||
*
|
||||
* @param request HttpServletRequest
|
||||
* @return 客户端IP地址
|
||||
*/
|
||||
public static String getClientIp(HttpServletRequest request) {
|
||||
String ip = request.getHeader("X-Forwarded-For");
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("X-Real-IP");
|
||||
}
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("Proxy-Client-IP");
|
||||
}
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("WL-Proxy-Client-IP");
|
||||
}
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("HTTP_CLIENT_IP");
|
||||
}
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
|
||||
}
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getRemoteAddr();
|
||||
}
|
||||
|
||||
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
|
||||
if (ip != null && ip.length() > 15 && ip.contains(",")) {
|
||||
ip = ip.substring(0, ip.indexOf(","));
|
||||
}
|
||||
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
564
src/main/java/com/corewing/app/util/RedisUtil.java
Normal file
564
src/main/java/com/corewing/app/util/RedisUtil.java
Normal file
@@ -0,0 +1,564 @@
|
||||
package com.corewing.app.util;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Redis 工具类
|
||||
*/
|
||||
@SuppressWarnings("CallToPrintStackTrace")
|
||||
@Component
|
||||
public class RedisUtil {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
public RedisUtil(RedisTemplate<String, Object> redisTemplate) {
|
||||
this.redisTemplate = redisTemplate;
|
||||
}
|
||||
|
||||
// =============================common============================
|
||||
|
||||
/**
|
||||
* 指定缓存失效时间
|
||||
*
|
||||
* @param key 键
|
||||
* @param time 时间(秒)
|
||||
*/
|
||||
public boolean expire(String key, long time) {
|
||||
try {
|
||||
if (time > 0) {
|
||||
redisTemplate.expire(key, time, TimeUnit.SECONDS);
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据key 获取过期时间
|
||||
*
|
||||
* @param key 键 不能为null
|
||||
* @return 时间(秒) 返回0代表为永久有效
|
||||
*/
|
||||
public long getExpire(String key) {
|
||||
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断key是否存在
|
||||
*
|
||||
* @param key 键
|
||||
* @return true 存在 false不存在
|
||||
*/
|
||||
public boolean hasKey(String key) {
|
||||
try {
|
||||
return redisTemplate.hasKey(key);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除缓存
|
||||
*
|
||||
* @param key 可以传一个值 或多个
|
||||
*/
|
||||
public void del(String... key) {
|
||||
if (key != null && key.length > 0) {
|
||||
if (key.length == 1) {
|
||||
redisTemplate.delete(key[0]);
|
||||
} else {
|
||||
redisTemplate.delete((Collection<String>) List.of(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================String=============================
|
||||
|
||||
/**
|
||||
* 普通缓存获取
|
||||
*
|
||||
* @param key 键
|
||||
* @return 值
|
||||
*/
|
||||
public Object get(String key) {
|
||||
return key == null ? null : redisTemplate.opsForValue().get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 普通缓存放入
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @return true成功 false失败
|
||||
*/
|
||||
public boolean set(String key, Object value) {
|
||||
try {
|
||||
redisTemplate.opsForValue().set(key, value);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 普通缓存放入并设置时间
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
|
||||
* @return true成功 false 失败
|
||||
*/
|
||||
public boolean set(String key, Object value, long time) {
|
||||
try {
|
||||
if (time > 0) {
|
||||
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
|
||||
} else {
|
||||
set(key, value);
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递增
|
||||
*
|
||||
* @param key 键
|
||||
* @param delta 要增加几(大于0)
|
||||
*/
|
||||
public long incr(String key, long delta) {
|
||||
if (delta < 0) {
|
||||
throw new RuntimeException("递增因子必须大于0");
|
||||
}
|
||||
Long result = redisTemplate.opsForValue().increment(key, delta);
|
||||
return result != null ? result : 0L;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递减
|
||||
*
|
||||
* @param key 键
|
||||
* @param delta 要减少几(小于0)
|
||||
*/
|
||||
public long decr(String key, long delta) {
|
||||
if (delta < 0) {
|
||||
throw new RuntimeException("递减因子必须大于0");
|
||||
}
|
||||
Long result = redisTemplate.opsForValue().increment(key, -delta);
|
||||
return result != null ? result : 0L;
|
||||
}
|
||||
|
||||
// ================================Hash=================================
|
||||
|
||||
/**
|
||||
* HashGet
|
||||
*
|
||||
* @param key 键 不能为null
|
||||
* @param item 项 不能为null
|
||||
*/
|
||||
public Object hget(String key, String item) {
|
||||
return redisTemplate.opsForHash().get(key, item);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取hashKey对应的所有键值
|
||||
*
|
||||
* @param key 键
|
||||
* @return 对应的多个键值
|
||||
*/
|
||||
public Map<Object, Object> hmget(String key) {
|
||||
return redisTemplate.opsForHash().entries(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* HashSet
|
||||
*
|
||||
* @param key 键
|
||||
* @param map 对应多个键值
|
||||
*/
|
||||
public boolean hmset(String key, Map<String, Object> map) {
|
||||
try {
|
||||
redisTemplate.opsForHash().putAll(key, map);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HashSet 并设置时间
|
||||
*
|
||||
* @param key 键
|
||||
* @param map 对应多个键值
|
||||
* @param time 时间(秒)
|
||||
* @return true成功 false失败
|
||||
*/
|
||||
public boolean hmset(String key, Map<String, Object> map, long time) {
|
||||
try {
|
||||
redisTemplate.opsForHash().putAll(key, map);
|
||||
if (time > 0) {
|
||||
expire(key, time);
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向一张hash表中放入数据,如果不存在将创建
|
||||
*
|
||||
* @param key 键
|
||||
* @param item 项
|
||||
* @param value 值
|
||||
* @return true 成功 false失败
|
||||
*/
|
||||
public boolean hset(String key, String item, Object value) {
|
||||
try {
|
||||
redisTemplate.opsForHash().put(key, item, value);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向一张hash表中放入数据,如果不存在将创建
|
||||
*
|
||||
* @param key 键
|
||||
* @param item 项
|
||||
* @param value 值
|
||||
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
|
||||
* @return true 成功 false失败
|
||||
*/
|
||||
public boolean hset(String key, String item, Object value, long time) {
|
||||
try {
|
||||
redisTemplate.opsForHash().put(key, item, value);
|
||||
if (time > 0) {
|
||||
expire(key, time);
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除hash表中的值
|
||||
*
|
||||
* @param key 键 不能为null
|
||||
* @param item 项 可以使多个 不能为null
|
||||
*/
|
||||
public void hdel(String key, Object... item) {
|
||||
redisTemplate.opsForHash().delete(key, item);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断hash表中是否有该项的值
|
||||
*
|
||||
* @param key 键 不能为null
|
||||
* @param item 项 不能为null
|
||||
* @return true 存在 false不存在
|
||||
*/
|
||||
public boolean hHasKey(String key, String item) {
|
||||
return redisTemplate.opsForHash().hasKey(key, item);
|
||||
}
|
||||
|
||||
/**
|
||||
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
|
||||
*
|
||||
* @param key 键
|
||||
* @param item 项
|
||||
* @param by 要增加几(大于0)
|
||||
*/
|
||||
public double hincr(String key, String item, double by) {
|
||||
return redisTemplate.opsForHash().increment(key, item, by);
|
||||
}
|
||||
|
||||
/**
|
||||
* hash递减
|
||||
*
|
||||
* @param key 键
|
||||
* @param item 项
|
||||
* @param by 要减少记(小于0)
|
||||
*/
|
||||
public double hdecr(String key, String item, double by) {
|
||||
return redisTemplate.opsForHash().increment(key, item, -by);
|
||||
}
|
||||
|
||||
// ============================Set=============================
|
||||
|
||||
/**
|
||||
* 根据key获取Set中的所有值
|
||||
*
|
||||
* @param key 键
|
||||
*/
|
||||
public Set<Object> sGet(String key) {
|
||||
try {
|
||||
return redisTemplate.opsForSet().members(key);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据value从一个set中查询,是否存在
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @return true 存在 false不存在
|
||||
*/
|
||||
public boolean sHasKey(String key, Object value) {
|
||||
try {
|
||||
Boolean result = redisTemplate.opsForSet().isMember(key, value);
|
||||
return result != null && result;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将数据放入set缓存
|
||||
*
|
||||
* @param key 键
|
||||
* @param values 值 可以是多个
|
||||
* @return 成功个数
|
||||
*/
|
||||
public long sSet(String key, Object... values) {
|
||||
try {
|
||||
Long result = redisTemplate.opsForSet().add(key, values);
|
||||
return result != null ? result : 0L;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将set数据放入缓存
|
||||
*
|
||||
* @param key 键
|
||||
* @param time 时间(秒)
|
||||
* @param values 值 可以是多个
|
||||
* @return 成功个数
|
||||
*/
|
||||
public long sSetAndTime(String key, long time, Object... values) {
|
||||
try {
|
||||
Long count = redisTemplate.opsForSet().add(key, values);
|
||||
if (time > 0) {
|
||||
expire(key, time);
|
||||
}
|
||||
return count != null ? count : 0L;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取set缓存的长度
|
||||
*
|
||||
* @param key 键
|
||||
*/
|
||||
public long sGetSetSize(String key) {
|
||||
try {
|
||||
Long result = redisTemplate.opsForSet().size(key);
|
||||
return result != null ? result : 0L;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除值为value的
|
||||
*
|
||||
* @param key 键
|
||||
* @param values 值 可以是多个
|
||||
* @return 移除的个数
|
||||
*/
|
||||
public long setRemove(String key, Object... values) {
|
||||
try {
|
||||
Long count = redisTemplate.opsForSet().remove(key, values);
|
||||
return count != null ? count : 0L;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================List=================================
|
||||
|
||||
/**
|
||||
* 获取list缓存的内容
|
||||
*
|
||||
* @param key 键
|
||||
* @param start 开始
|
||||
* @param end 结束 0 到 -1代表所有值
|
||||
*/
|
||||
public List<Object> lGet(String key, long start, long end) {
|
||||
try {
|
||||
return redisTemplate.opsForList().range(key, start, end);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取list缓存的长度
|
||||
*
|
||||
* @param key 键
|
||||
*/
|
||||
public long lGetListSize(String key) {
|
||||
try {
|
||||
Long result = redisTemplate.opsForList().size(key);
|
||||
return result != null ? result : 0L;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过索引 获取list中的值
|
||||
*
|
||||
* @param key 键
|
||||
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
|
||||
*/
|
||||
public Object lGetIndex(String key, long index) {
|
||||
try {
|
||||
return redisTemplate.opsForList().index(key, index);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将list放入缓存
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
*/
|
||||
public boolean lSet(String key, Object value) {
|
||||
try {
|
||||
redisTemplate.opsForList().rightPush(key, value);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将list放入缓存
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @param time 时间(秒)
|
||||
*/
|
||||
public boolean lSet(String key, Object value, long time) {
|
||||
try {
|
||||
redisTemplate.opsForList().rightPush(key, value);
|
||||
if (time > 0) {
|
||||
expire(key, time);
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将list放入缓存
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
*/
|
||||
public boolean lSet(String key, List<Object> value) {
|
||||
try {
|
||||
redisTemplate.opsForList().rightPushAll(key, value);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将list放入缓存
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @param time 时间(秒)
|
||||
*/
|
||||
public boolean lSet(String key, List<Object> value, long time) {
|
||||
try {
|
||||
redisTemplate.opsForList().rightPushAll(key, value);
|
||||
if (time > 0) {
|
||||
expire(key, time);
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据索引修改list中的某条数据
|
||||
*
|
||||
* @param key 键
|
||||
* @param index 索引
|
||||
* @param value 值
|
||||
*/
|
||||
public boolean lUpdateIndex(String key, long index, Object value) {
|
||||
try {
|
||||
redisTemplate.opsForList().set(key, index, value);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除N个值为value
|
||||
*
|
||||
* @param key 键
|
||||
* @param count 移除多少个
|
||||
* @param value 值
|
||||
* @return 移除的个数
|
||||
*/
|
||||
public long lRemove(String key, long count, Object value) {
|
||||
try {
|
||||
Long remove = redisTemplate.opsForList().remove(key, count, value);
|
||||
return remove != null ? remove : 0L;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
118
src/main/java/com/corewing/app/util/SmsBaoUtil.java
Normal file
118
src/main/java/com/corewing/app/util/SmsBaoUtil.java
Normal file
@@ -0,0 +1,118 @@
|
||||
package com.corewing.app.util;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* 短信宝工具类
|
||||
* 官方文档: https://www.smsbao.com/
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class SmsBaoUtil {
|
||||
|
||||
@Value("${smsbao.username:}")
|
||||
private String username;
|
||||
|
||||
@Value("${smsbao.password:}")
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 短信宝API地址
|
||||
*/
|
||||
private static final String SMS_API_URL = "http://api.smsbao.com/sms";
|
||||
|
||||
/**
|
||||
* 发送短信
|
||||
*
|
||||
* @param phone 手机号
|
||||
* @param content 短信内容
|
||||
* @return 是否发送成功
|
||||
*/
|
||||
public boolean sendSms(String phone, String content) {
|
||||
try {
|
||||
// URL编码
|
||||
String encodedContent = URLEncoder.encode(content, StandardCharsets.UTF_8.name());
|
||||
|
||||
// 构建请求URL
|
||||
String requestUrl = String.format("%s?u=%s&p=%s&m=%s&c=%s",
|
||||
SMS_API_URL, username, password, phone, encodedContent);
|
||||
|
||||
// 发送HTTP请求
|
||||
URL url = new URL(requestUrl);
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setRequestMethod("GET");
|
||||
connection.setConnectTimeout(5000);
|
||||
connection.setReadTimeout(5000);
|
||||
|
||||
// 读取响应
|
||||
BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));
|
||||
String response = reader.readLine();
|
||||
reader.close();
|
||||
|
||||
// 解析响应码
|
||||
int code = Integer.parseInt(response);
|
||||
if (code == 0) {
|
||||
log.info("短信发送成功, 手机号: {}", phone);
|
||||
return true;
|
||||
} else {
|
||||
log.error("短信发送失败, 手机号: {}, 错误码: {}, 错误信息: {}",
|
||||
phone, code, getErrorMessage(code));
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("短信发送异常, 手机号: {}, 异常信息: {}", phone, e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送验证码短信
|
||||
*
|
||||
* @param phone 手机号
|
||||
* @param code 验证码
|
||||
* @return 是否发送成功
|
||||
*/
|
||||
public boolean sendVerifyCode(String phone, String code) {
|
||||
String content = String.format("【CoreWing】您的验证码是%s,5分钟内有效。请勿泄露给他人!", code);
|
||||
return sendSms(phone, content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误信息
|
||||
*
|
||||
* @param code 错误码
|
||||
* @return 错误信息
|
||||
*/
|
||||
private String getErrorMessage(int code) {
|
||||
switch (code) {
|
||||
case 0:
|
||||
return "发送成功";
|
||||
case 30:
|
||||
return "密码错误";
|
||||
case 40:
|
||||
return "账号不存在";
|
||||
case 41:
|
||||
return "余额不足";
|
||||
case 42:
|
||||
return "账户已过期";
|
||||
case 43:
|
||||
return "IP地址限制";
|
||||
case 50:
|
||||
return "内容含有敏感词";
|
||||
case 51:
|
||||
return "手机号码不正确";
|
||||
default:
|
||||
return "未知错误";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,77 @@
|
||||
# 应用服务 WEB 访问端口
|
||||
server.port=8080
|
||||
|
||||
# 数据源配置
|
||||
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
|
||||
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
|
||||
spring.datasource.url=jdbc:mysql://120.24.204.180:3306/app?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
|
||||
spring.datasource.username=app
|
||||
spring.datasource.password=AB636NGhhH2cC5A5
|
||||
|
||||
# Druid 连接池配置
|
||||
spring.datasource.druid.initial-size=5
|
||||
spring.datasource.druid.min-idle=5
|
||||
spring.datasource.druid.max-active=20
|
||||
spring.datasource.druid.max-wait=60000
|
||||
spring.datasource.druid.test-while-idle=true
|
||||
spring.datasource.druid.test-on-borrow=false
|
||||
spring.datasource.druid.test-on-return=false
|
||||
spring.datasource.druid.time-between-eviction-runs-millis=60000
|
||||
spring.datasource.druid.min-evictable-idle-time-millis=300000
|
||||
spring.datasource.druid.validation-query=SELECT 1
|
||||
spring.datasource.druid.pool-prepared-statements=true
|
||||
spring.datasource.druid.max-pool-prepared-statement-per-connection-size=20
|
||||
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
|
||||
spring.datasource.druid.filters=stat,wall
|
||||
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
|
||||
spring.datasource.druid.connection-properties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
|
||||
|
||||
# MyBatis-Plus 配置
|
||||
mybatis-plus.mapper-locations=classpath*:/mapper/**/*.xml
|
||||
mybatis-plus.type-aliases-package=com.corewing.app.entity
|
||||
mybatis-plus.configuration.map-underscore-to-camel-case=true
|
||||
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
mybatis-plus.global-config.db-config.id-type=AUTO
|
||||
mybatis-plus.global-config.db-config.table-prefix=
|
||||
|
||||
# Redis 配置
|
||||
spring.redis.host=localhost
|
||||
spring.redis.port=6379
|
||||
spring.redis.password=
|
||||
spring.redis.database=0
|
||||
spring.redis.timeout=3000
|
||||
spring.redis.lettuce.pool.max-active=8
|
||||
spring.redis.lettuce.pool.max-idle=8
|
||||
spring.redis.lettuce.pool.min-idle=0
|
||||
spring.redis.lettuce.pool.max-wait=-1ms
|
||||
|
||||
# Sa-Token 配置
|
||||
sa-token.token-name=Authorization
|
||||
sa-token.timeout=2592000
|
||||
sa-token.active-timeout=-1
|
||||
sa-token.is-concurrent=true
|
||||
sa-token.is-share=true
|
||||
sa-token.token-style=uuid
|
||||
sa-token.is-log=false
|
||||
|
||||
# 短信宝配置
|
||||
# 请前往 https://www.smsbao.com/ 注册账号并获取用户名和密码
|
||||
smsbao.username=your_username
|
||||
smsbao.password=your_password
|
||||
|
||||
# 邮件配置
|
||||
# SMTP 服务器地址
|
||||
spring.mail.host=smtp.chengmail.cn
|
||||
# SMTP 服务器端口
|
||||
spring.mail.port=465
|
||||
# 发件人邮箱
|
||||
spring.mail.username=dev@corewing.com
|
||||
# 邮箱授权码(不是邮箱密码!需要在邮箱设置中开启 SMTP 服务并获取授权码)
|
||||
spring.mail.password=HRTmmNrBRjSxfwAk
|
||||
# 编码格式
|
||||
spring.mail.default-encoding=UTF-8
|
||||
# 其他配置
|
||||
spring.mail.properties.mail.smtp.auth=true
|
||||
spring.mail.properties.mail.smtp.starttls.enable=true
|
||||
spring.mail.properties.mail.smtp.starttls.required=true
|
||||
spring.mail.properties.mail.smtp.ssl.enable=false
|
||||
|
||||
26
src/main/resources/db/user.sql
Normal file
26
src/main/resources/db/user.sql
Normal file
@@ -0,0 +1,26 @@
|
||||
-- 用户表
|
||||
DROP TABLE IF EXISTS `app_user`;
|
||||
|
||||
CREATE TABLE `app_user` (
|
||||
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
|
||||
`username` VARCHAR(50) NOT NULL COMMENT '用户名',
|
||||
`password` VARCHAR(100) NOT NULL COMMENT '密码',
|
||||
`email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
|
||||
`telephone` VARCHAR(20) DEFAULT NULL COMMENT '手机号',
|
||||
`avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像URL',
|
||||
`login_ip` VARCHAR(50) DEFAULT NULL COMMENT '最后登录IP',
|
||||
`status` TINYINT(1) DEFAULT 1 COMMENT '状态:0-禁用 1-启用',
|
||||
`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_username` (`username`),
|
||||
UNIQUE KEY `uk_email` (`email`),
|
||||
UNIQUE KEY `uk_telephone` (`telephone`),
|
||||
KEY `idx_create_time` (`create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='应用用户表';
|
||||
|
||||
-- 插入测试用户(密码为:123456,实际使用时应该加密)
|
||||
INSERT INTO `app_user` (`username`, `password`, `email`, `telephone`, `avatar`, `status`)
|
||||
VALUES
|
||||
('admin', '123456', 'admin@corewing.com', '13800138000', NULL, 1),
|
||||
('test', '123456', 'test@corewing.com', '13800138001', NULL, 1);
|
||||
356
src/main/resources/docs/API接口说明.md
Normal file
356
src/main/resources/docs/API接口说明.md
Normal file
@@ -0,0 +1,356 @@
|
||||
# API 接口说明
|
||||
|
||||
## 用户相关接
|
||||
|
||||
### 1. 发送验证码
|
||||
|
||||
**接口地址:** `POST /user/sendCode`
|
||||
|
||||
**请求参数:**
|
||||
```json
|
||||
{
|
||||
"account": "13800138000", // 手机号或邮箱
|
||||
"type": "register" // 验证码类型: register-注册, login-登录, reset-重置密码
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "验证码发送成功",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
**说明:**
|
||||
- 手机号格式: 1开头的11位数字
|
||||
- 邮箱格式: 标准邮箱格式
|
||||
- 验证码有效期: 5分钟
|
||||
- 验证码长度: 6位数字
|
||||
|
||||
---
|
||||
|
||||
### 2. 用户注册
|
||||
|
||||
**接口地址:** `POST /user/register`
|
||||
|
||||
**请求参数:**
|
||||
```json
|
||||
{
|
||||
"username": "testuser", // 用户名(必填)
|
||||
"password": "123456", // 密码(必填)
|
||||
"email": "test@example.com", // 邮箱(邮箱和手机号至少填一个)
|
||||
"telephone": "13800138000", // 手机号(邮箱和手机号至少填一个)
|
||||
"code": "123456", // 验证码(必填)
|
||||
"avatar": "http://..." // 头像URL(可选)
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "注册成功",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
**说明:**
|
||||
- 用户名不能重复
|
||||
- 邮箱和手机号至少填写一个
|
||||
- 邮箱和手机号不能重复
|
||||
- 需要先调用发送验证码接口
|
||||
- 密码会自动进行 MD5 加密
|
||||
|
||||
---
|
||||
|
||||
### 3. 用户登录
|
||||
|
||||
**接口地址:** `POST /user/login`
|
||||
|
||||
**请求参数:**
|
||||
```json
|
||||
{
|
||||
"account": "testuser", // 账号(用户名/邮箱/手机号)
|
||||
"password": "123456" // 密码
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "登录成功",
|
||||
"data": {
|
||||
"token": "uuid-token-string",
|
||||
"userId": 1,
|
||||
"username": "testuser"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**说明:**
|
||||
- account 支持用户名、邮箱、手机号三种方式登录
|
||||
- 登录成功后返回 token,后续请求需要在 Header 中携带: `Authorization: token值`
|
||||
|
||||
---
|
||||
|
||||
### 4. 用户登出
|
||||
|
||||
**接口地址:** `POST /user/logout`
|
||||
|
||||
**请求头:**
|
||||
```
|
||||
Authorization: your-token
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "登出成功",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. 获取当前用户信息
|
||||
|
||||
**接口地址:** `GET /user/info`
|
||||
|
||||
**请求头:**
|
||||
```
|
||||
Authorization: your-token
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"username": "testuser",
|
||||
"password": null,
|
||||
"email": "test@example.com",
|
||||
"telephone": "13800138000",
|
||||
"avatar": null,
|
||||
"loginIp": "127.0.0.1",
|
||||
"status": 1,
|
||||
"createTime": "2025-01-01T12:00:00",
|
||||
"updateTime": "2025-01-01T12:00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. 根据ID查询用户
|
||||
|
||||
**接口地址:** `GET /user/{id}`
|
||||
|
||||
**请求头:**
|
||||
```
|
||||
Authorization: your-token
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"username": "testuser",
|
||||
"password": null,
|
||||
"email": "test@example.com",
|
||||
"telephone": "13800138000",
|
||||
"avatar": null,
|
||||
"loginIp": "127.0.0.1",
|
||||
"status": 1,
|
||||
"createTime": "2025-01-01T12:00:00",
|
||||
"updateTime": "2025-01-01T12:00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. 更新用户信息
|
||||
|
||||
**接口地址:** `PUT /user`
|
||||
|
||||
**请求头:**
|
||||
```
|
||||
Authorization: your-token
|
||||
```
|
||||
|
||||
**请求参数:**
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"username": "newusername",
|
||||
"email": "newemail@example.com",
|
||||
"telephone": "13900139000",
|
||||
"avatar": "http://..."
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "更新成功",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
**说明:**
|
||||
- 不能通过此接口修改密码
|
||||
- 需要修改密码请使用专门的修改密码接口
|
||||
|
||||
---
|
||||
|
||||
### 8. 修改密码
|
||||
|
||||
**接口地址:** `PUT /user/password`
|
||||
|
||||
**请求头:**
|
||||
```
|
||||
Authorization: your-token
|
||||
```
|
||||
|
||||
**请求参数:**
|
||||
```json
|
||||
{
|
||||
"oldPassword": "123456",
|
||||
"newPassword": "654321"
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "密码修改成功",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 短信宝配置
|
||||
|
||||
在 `application.properties` 中配置短信宝账号信息:
|
||||
|
||||
```properties
|
||||
# 短信宝配置
|
||||
# 请前往 https://www.smsbao.com/ 注册账号并获取用户名和密码
|
||||
smsbao.username=your_username
|
||||
smsbao.password=your_password
|
||||
```
|
||||
|
||||
**注册短信宝账号:**
|
||||
1. 访问 https://www.smsbao.com/
|
||||
2. 注册账号并充值
|
||||
3. 获取用户名和密码(注意:密码是 MD5 加密后的值)
|
||||
4. 配置到 application.properties 中
|
||||
|
||||
---
|
||||
|
||||
### 邮件配置
|
||||
|
||||
在 `application.properties` 中配置邮箱信息:
|
||||
|
||||
```properties
|
||||
# 邮件配置(以 QQ 邮箱为例)
|
||||
spring.mail.host=smtp.qq.com
|
||||
spring.mail.port=587
|
||||
spring.mail.username=your_email@qq.com
|
||||
spring.mail.password=your_authorization_code
|
||||
spring.mail.default-encoding=UTF-8
|
||||
spring.mail.properties.mail.smtp.auth=true
|
||||
spring.mail.properties.mail.smtp.starttls.enable=true
|
||||
spring.mail.properties.mail.smtp.starttls.required=true
|
||||
```
|
||||
|
||||
**配置 QQ 邮箱:**
|
||||
1. 登录 QQ 邮箱网页版
|
||||
2. 进入【设置】->【账户】
|
||||
3. 找到【POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务】
|
||||
4. 开启 SMTP 服务
|
||||
5. 生成授权码(注意:不是 QQ 密码!)
|
||||
6. 将邮箱地址和授权码配置到 application.properties 中
|
||||
|
||||
**其他邮箱配置:**
|
||||
- **163 邮箱**: `smtp.163.com`,端口 `465` 或 `25`
|
||||
- **Gmail**: `smtp.gmail.com`,端口 `587`
|
||||
- **企业邮箱**: 联系管理员获取 SMTP 服务器地址
|
||||
|
||||
---
|
||||
|
||||
## 数据库说明
|
||||
|
||||
### 用户表结构
|
||||
|
||||
```sql
|
||||
CREATE TABLE `app_user` (
|
||||
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
|
||||
`username` VARCHAR(50) NOT NULL COMMENT '用户名',
|
||||
`password` VARCHAR(100) NOT NULL COMMENT '密码',
|
||||
`email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
|
||||
`telephone` VARCHAR(20) DEFAULT NULL COMMENT '手机号',
|
||||
`avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像URL',
|
||||
`login_ip` VARCHAR(50) DEFAULT NULL COMMENT '最后登录IP',
|
||||
`status` TINYINT(1) DEFAULT 1 COMMENT '状态:0-禁用 1-启用',
|
||||
`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_username` (`username`),
|
||||
UNIQUE KEY `uk_email` (`email`),
|
||||
UNIQUE KEY `uk_telephone` (`telephone`),
|
||||
KEY `idx_create_time` (`create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='应用用户表';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误码说明
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|------|
|
||||
| 200 | 成功 |
|
||||
| 500 | 失败 |
|
||||
|
||||
---
|
||||
|
||||
## 开发说明
|
||||
|
||||
### 验证码存储
|
||||
|
||||
- 验证码存储在 Redis 中
|
||||
- Key 格式: `verify_code:{type}:{account}`
|
||||
- 有效期: 5分钟
|
||||
- 验证成功后自动删除
|
||||
|
||||
### 密码加密
|
||||
|
||||
- 使用 MD5 加密
|
||||
- 前端传输明文密码,后端自动加密存储
|
||||
|
||||
### 登录认证
|
||||
|
||||
- 使用 Sa-Token 进行身份认证
|
||||
- Token 有效期: 30天
|
||||
- 除登录、注册、发送验证码接口外,其他接口都需要认证
|
||||
|
||||
### 验证码发送
|
||||
|
||||
- **手机验证码**: 通过短信宝自动发送短信
|
||||
- **邮件验证码**: 通过 Spring Mail 自动发送 HTML 格式的精美邮件
|
||||
- 验证码为 6 位随机数字
|
||||
- 邮件模板包含品牌样式,提升用户体验
|
||||
150
src/test/java/com/corewing/app/util/EmailUtilTest.java
Normal file
150
src/test/java/com/corewing/app/util/EmailUtilTest.java
Normal file
@@ -0,0 +1,150 @@
|
||||
package com.corewing.app.util;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
/**
|
||||
* 邮件发送测试类
|
||||
*
|
||||
* 使用说明:
|
||||
* 1. 确保 application.properties 中已正确配置邮件服务器信息
|
||||
* 2. 将下面测试方法中的收件人邮箱改为你自己的邮箱
|
||||
* 3. 运行测试前确保网络连接正常
|
||||
*/
|
||||
@SpringBootTest
|
||||
public class EmailUtilTest {
|
||||
|
||||
@Autowired
|
||||
private EmailUtil emailUtil;
|
||||
|
||||
/**
|
||||
* 测试发送简单文本邮件
|
||||
*/
|
||||
@Test
|
||||
public void testSendSimpleMail() {
|
||||
String to = "zmissu@163.com"; // 修改为实际收件人邮箱
|
||||
String subject = "【CoreWing】简单文本邮件测试";
|
||||
String content = "这是一封测试邮件,如果您收到这封邮件,说明邮件发送功能正常。\n\n发送时间:" +
|
||||
java.time.LocalDateTime.now();
|
||||
|
||||
boolean success = emailUtil.sendSimpleMail(to, subject, content);
|
||||
|
||||
if (success) {
|
||||
System.out.println("✅ 简单文本邮件发送成功!请检查收件箱: " + to);
|
||||
} else {
|
||||
System.err.println("❌ 简单文本邮件发送失败!请检查邮件配置和网络连接。");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试发送HTML邮件
|
||||
*/
|
||||
@Test
|
||||
public void testSendHtmlMail() {
|
||||
String to = "test@example.com"; // 修改为实际收件人邮箱
|
||||
String subject = "【CoreWing】HTML邮件测试";
|
||||
String content = "<html>" +
|
||||
"<body style='font-family: Arial, sans-serif;'>" +
|
||||
"<div style='background-color: #f0f0f0; padding: 20px;'>" +
|
||||
"<h2 style='color: #333;'>CoreWing 系统通知</h2>" +
|
||||
"<p>这是一封 <strong>HTML格式</strong> 的测试邮件。</p>" +
|
||||
"<p>如果您能看到这封邮件的样式,说明HTML邮件发送功能正常。</p>" +
|
||||
"<hr>" +
|
||||
"<p style='color: #666; font-size: 12px;'>发送时间:" +
|
||||
java.time.LocalDateTime.now() + "</p>" +
|
||||
"</div>" +
|
||||
"</body>" +
|
||||
"</html>";
|
||||
|
||||
boolean success = emailUtil.sendHtmlMail(to, subject, content);
|
||||
|
||||
if (success) {
|
||||
System.out.println("✅ HTML邮件发送成功!请检查收件箱: " + to);
|
||||
} else {
|
||||
System.err.println("❌ HTML邮件发送失败!请检查邮件配置和网络连接。");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试发送验证码邮件
|
||||
*/
|
||||
@Test
|
||||
public void testSendVerifyCode() {
|
||||
String to = "test@example.com"; // 修改为实际收件人邮箱
|
||||
String code = "123456"; // 测试验证码
|
||||
|
||||
boolean success = emailUtil.sendVerifyCode(to, code);
|
||||
|
||||
if (success) {
|
||||
System.out.println("✅ 验证码邮件发送成功!");
|
||||
System.out.println(" 收件人: " + to);
|
||||
System.out.println(" 验证码: " + code);
|
||||
System.out.println(" 请检查收件箱(可能在垃圾邮件中)");
|
||||
} else {
|
||||
System.err.println("❌ 验证码邮件发送失败!");
|
||||
System.err.println(" 可能的原因:");
|
||||
System.err.println(" 1. 邮件服务器配置错误");
|
||||
System.err.println(" 2. 邮箱授权码不正确");
|
||||
System.err.println(" 3. 网络连接问题");
|
||||
System.err.println(" 4. 邮件服务器防火墙限制");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试发送多个验证码邮件(模拟实际场景)
|
||||
*/
|
||||
@Test
|
||||
public void testSendMultipleVerifyCode() {
|
||||
String[] emails = {
|
||||
"test1@example.com", // 修改为实际收件人邮箱
|
||||
"test2@example.com" // 可以添加更多测试邮箱
|
||||
};
|
||||
|
||||
System.out.println("🚀 开始批量发送验证码邮件测试...\n");
|
||||
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
|
||||
for (int i = 0; i < emails.length; i++) {
|
||||
String email = emails[i];
|
||||
String code = generateRandomCode();
|
||||
|
||||
System.out.println("📧 正在发送第 " + (i + 1) + " 封邮件...");
|
||||
System.out.println(" 收件人: " + email);
|
||||
System.out.println(" 验证码: " + code);
|
||||
|
||||
boolean success = emailUtil.sendVerifyCode(email, code);
|
||||
|
||||
if (success) {
|
||||
System.out.println(" ✅ 发送成功\n");
|
||||
successCount++;
|
||||
} else {
|
||||
System.err.println(" ❌ 发送失败\n");
|
||||
failCount++;
|
||||
}
|
||||
|
||||
// 避免发送过快被限流,间隔1秒
|
||||
if (i < emails.length - 1) {
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("📊 测试完成统计:");
|
||||
System.out.println(" 总计: " + emails.length + " 封");
|
||||
System.out.println(" 成功: " + successCount + " 封");
|
||||
System.out.println(" 失败: " + failCount + " 封");
|
||||
System.out.println(" 成功率: " + (successCount * 100 / emails.length) + "%");
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机6位验证码
|
||||
*/
|
||||
private String generateRandomCode() {
|
||||
return String.format("%06d", (int)(Math.random() * 1000000));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user