liuzeyu #1

Merged
pyuv9atf8 merged 42 commits from LiiuZeYu_branch into develop 9 months ago

@ -0,0 +1 @@
undefined

@ -0,0 +1,34 @@
/*
* Copyright (c) 2018-2999 广 All rights reserved.
*
* https://www.mall4j.com/
*
*
*
*
*/
package com.yami.shop.common.bean;
// 导入lombok的Data注解使用该注解可以自动生成类的一些常规方法比如getter、setter、toString等方法简化代码编写
import lombok.Data;
/**
*
* 访ID访
* 便
* @author LGH
*/
@Data
// 定义名为AliDaYu的公共类用于封装阿里大鱼配置相关的属性
public class AliDaYu {
// 以下是类的私有属性,用于存储具体的配置信息
// 用于存储阿里大鱼的访问密钥ID通过该ID来标识访问阿里大鱼服务的身份凭证之一
private String accessKeyId;
// 用于存储阿里大鱼的访问密钥密码与访问密钥ID配合使用用于进行安全验证等操作确保对阿里大鱼服务的合法访问
private String accessKeySecret;
// 用于存储阿里大鱼的签名名称,在涉及到一些需要签名验证的业务场景中会用到该名称
private String signName;
}

@ -0,0 +1,44 @@
/*
* Copyright (c) 2018-2999 广 All rights reserved.
*
* https://www.mall4j.com/
*
*
*
*
*/
package com.yami.shop.common.bean;
// 引入Lombok的Data注解通过该注解编译器会自动帮我们生成类的常用方法
// 比如各属性的Getter、Setter方法以及toString、equals、hashCode方法等简化代码编写
import lombok.Data;
/**
* ImgUpload
* 便使
*
* @author lgh
*/
@Data
public class ImgUpload {
/**
*
* 便访
*/
private String imagePath;
/**
*
* 1imagePath
* 2使
*/
private Integer uploadType;
/**
* 访URL访
* resourceUrl访URL访
*/
private String resourceUrl;
}

@ -0,0 +1,133 @@
/*
* Copyright (c) 2018-2999 广 All rights reserved.
*
* https://www.mall4j.com/
*
*
*
*
*/
package com.yami.shop.common.config;
// 导入相关的自定义异常类、响应相关的枚举、响应实体类等
import com.yami.shop.common.exception.YamiShopBindException;
import com.yami.shop.common.response.ResponseEnum;
import com.yami.shop.common.response.ServerResponseEntity;
// 导入日志相关的Lombok注解用于简化日志记录代码
import lombok.extern.slf4j.Slf4j;
// 导入Spring的HTTP状态码相关枚举类
import org.springframework.http.HttpStatus;
// 导入Spring用于构建HTTP响应实体的类
import org.springframework.http.ResponseEntity;
// 导入Spring用于标记控制器类的注解此处结合异常处理相关特性使用
import org.springframework.stereotype.Controller;
// 导入Spring在数据校验场景下的异常类以及表示字段错误的类
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
// 导入Spring用于定义异常处理方法的注解以及标记全局异常处理类的注解
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
// 导入Spring用于处理资源未找到异常的类
import org.springframework.web.servlet.resource.NoResourceFoundException;
import java.util.ArrayList;
import java.util.List;
/**
*
*
* @author LGH
*/
@Slf4j
@Controller
@RestControllerAdvice
public class DefaultExceptionHandlerConfig {
/**
* MethodArgumentNotValidExceptionBindException
* Spring
*
*
* @param e MethodArgumentNotValidExceptionBindException
* @return ResponseEntity<ServerResponseEntity<List<String>>> HTTP
*
*/
@ExceptionHandler({ MethodArgumentNotValidException.class, BindException.class })
public ResponseEntity<ServerResponseEntity<List<String>>> methodArgumentNotValidExceptionHandler(Exception e) {
// 记录异常信息,方便后续查看出现异常的具体情况,用于调试和问题排查
log.error("methodArgumentNotValidExceptionHandler", e);
List<FieldError> fieldErrors = null;
// 判断异常类型是否是MethodArgumentNotValidException如果是则获取对应的字段错误列表
if (e instanceof MethodArgumentNotValidException) {
fieldErrors = ((MethodArgumentNotValidException) e).getBindingResult().getFieldErrors();
}
// 判断异常类型是否是BindException如果是则获取对应的字段错误列表
if (e instanceof BindException) {
fieldErrors = ((BindException) e).getBindingResult().getFieldErrors();
}
// 如果没有获取到字段错误列表(可能出现不符合预期的异常情况等)
if (fieldErrors == null) {
// 返回一个包含特定失败响应枚举(表示参数校验不通过)的响应实体,没有具体字段错误信息
return ResponseEntity.status(HttpStatus.OK)
.body(ServerResponseEntity.fail(ResponseEnum.METHOD_ARGUMENT_NOT_VALID));
}
// 用于存放每个字段的错误信息字符串(格式为:字段名:默认错误消息)
List<String> defaultMessages = new ArrayList<>(fieldErrors.size());
// 遍历字段错误列表拼接每个字段的错误信息字符串并添加到defaultMessages列表中
for (FieldError fieldError : fieldErrors) {
defaultMessages.add(fieldError.getField() + ":" + fieldError.getDefaultMessage());
}
// 返回一个包含具体字段错误信息列表的失败响应实体,使用参数校验不通过的响应枚举标识
return ResponseEntity.status(HttpStatus.OK)
.body(ServerResponseEntity.fail(ResponseEnum.METHOD_ARGUMENT_NOT_VALID, defaultMessages));
}
/**
* YamiShopBindException
* YamiShopBindException
*
*
* @param e YamiShopBindException
* @return ResponseEntity<ServerResponseEntity<?>> HTTP
*
*/
@ExceptionHandler(YamiShopBindException.class)
public ResponseEntity<ServerResponseEntity<?>> unauthorizedExceptionHandler(YamiShopBindException e){
// 记录异常信息,方便后续排查问题,了解异常出现的具体情况
log.error("mall4jExceptionHandler", e);
ServerResponseEntity<?> serverResponseEntity = e.getServerResponseEntity();
// 如果异常中已经封装好了响应实体(在抛出异常时可能已经构建好了特定的响应内容)
if (serverResponseEntity!=null) {
// 直接将其作为响应内容返回给客户端设置状态码为OK
return ResponseEntity.status(HttpStatus.OK).body(serverResponseEntity);
}
// 失败返回消息,状态码固定为直接显示消息的状态码,根据异常的错误码和错误消息构建失败响应实体并返回
return ResponseEntity.status(HttpStatus.OK).body(ServerResponseEntity.fail(e.getCode(),e.getMessage()));
}
/**
* Exception
*
*
*
* @param e Exception
* @return ResponseEntity<ServerResponseEntity<Object>> HTTP
*
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<ServerResponseEntity<Object>> exceptionHandler(Exception e){
// 判断异常是否是资源未找到异常类型
if (e instanceof NoResourceFoundException) {
// 如果是则返回一个包含异常消息的失败响应实体告知客户端资源未找到相关错误信息状态码设为OK
return ResponseEntity.status(HttpStatus.OK).body(ServerResponseEntity.showFailMsg(e.getMessage()));
}
// 记录异常信息,方便后续排查问题,知道出现异常的具体情况
log.error("exceptionHandler", e);
// 返回一个使用默认的异常响应枚举构建的失败响应实体,告知客户端出现了未预期的通用异常情况
return ResponseEntity.status(HttpStatus.OK).body(ServerResponseEntity.fail(ResponseEnum.EXCEPTION));
}
}

@ -0,0 +1,112 @@
/*
* Copyright (c) 2018-2999 广 All rights reserved.
*
* https://www.mall4j.com/
*
*
*
*
*/
package com.yami.shop.common.config;
// 导入相关的自定义类,可能用于存储图片上传相关的配置、枚举等信息
import com.yami.shop.common.bean.ImgUpload;
import com.yami.shop.common.enums.QiniuZone;
// 导入Spring的注解相关类用于实现依赖注入和配置相关功能
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// 导入七牛云相关的类,用于与七牛云存储服务进行交互,涉及区域、上传管理、认证等功能
import com.qiniu.common.Zone;
import com.qiniu.storage.BucketManager;
import com.qiniu.storage.UploadManager;
import com.qiniu.util.Auth;
import com.yami.shop.common.bean.Qiniu;
import java.util.Objects;
/**
*
*
* 便
* @author lgh
*/
@Configuration
public class FileUploadConfig {
// 通过Spring的依赖注入获取Qiniu类型的配置对象该对象应该包含了七牛云相关的配置信息如密钥、机房区域等
@Autowired
private Qiniu qiniu;
/**
* com.qiniu.storage.Configuration
* QiniuQiniuZoneZone
* 使
*
* @return com.qiniu.storage.Configuration
*/
@Bean
public com.qiniu.storage.Configuration qiniuConfig() {
Zone zone = null;
// 判断配置中的机房区域是否为华北区如果是则设置对应的七牛云机房区域为华北区Zone.huabei()
if (Objects.equals(qiniu.getZone(), QiniuZone.HUA_BEI)) {
zone = Zone.huabei();
}
// 判断是否为华东区若是则设置对应的七牛云机房区域为华东区Zone.huadong()
else if (Objects.equals(qiniu.getZone(), QiniuZone.HUA_DONG)) {
zone = Zone.huadong();
}
// 判断是否为华南区若是则设置对应的七牛云机房区域为华南区Zone.huanan()
else if (Objects.equals(qiniu.getZone(), QiniuZone.HUA_NAN)) {
zone = Zone.huanan();
}
// 判断是否为北美区若是则设置对应的七牛云机房区域为北美区Zone.beimei()
else if (Objects.equals(qiniu.getZone(), QiniuZone.BEI_MEI)) {
zone = Zone.beimei();
}
// 判断是否为新加坡区若是则设置对应的七牛云机房区域为新加坡区Zone.xinjiapo()
else if (Objects.equals(qiniu.getZone(), QiniuZone.XIN_JIA_PO)) {
zone = Zone.xinjiapo();
}
// 使用确定好的机房区域zone构建七牛云存储配置实例并返回
return new com.qiniu.storage.Configuration(zone);
}
/**
* UploadManager
* qiniuConfigcom.qiniu.storage.Configuration
*
*
* @return UploadManager
*/
@Bean
public UploadManager uploadManager() {
return new UploadManager(qiniuConfig());
}
/**
* Auth
* Qiniu访accessKeysecretKey
*
*
* @return Auth 访
*/
@Bean
public Auth auth() {
return Auth.create(qiniu.getAccessKey(), qiniu.getSecretKey());
}
/**
* BucketManager
* authAuthqiniuConfig
*
*
* @return BucketManager
*/
@Bean
public BucketManager bucketManager() {
return new BucketManager(auth(), qiniuConfig());
}
}

@ -0,0 +1,26 @@
package com.yami.shop.common.constants;
/**
* Constant
*
* 便
*
*
* @author TRACK
*/
public class Constant {
/**
* PERIOD
* 使
* 使
*/
public static final String PERIOD = ".";
/**
* COMMA
* CSV
* 便使
*/
public static final String COMMA = ",";
}

@ -0,0 +1,131 @@
package com.yami.shop.common.handler;
// 导入 Hutool 工具库中处理字符集相关的工具类,用于设置响应的字符编码
import cn.hutool.core.util.CharsetUtil;
// 导入 Jackson 库中用于将对象转换为 JSON 字符串以及反序列化等操作的核心类
import com.fasterxml.jackson.databind.ObjectMapper;
// 导入自定义的业务异常类,可能在项目中用于处理特定业务逻辑出错的情况
import com.yami.shop.common.exception.YamiShopBindException;
// 导入自定义的用于封装服务器响应信息的实体类,包含响应状态码、消息、数据等内容
import com.yami.shop.common.response.ServerResponseEntity;
// 导入 Slf4j 框架的日志记录相关类,用于创建日志记录器来记录不同情况的日志信息
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// 导入 Spring 框架用于实现依赖注入的注解,表明某个属性需要由 Spring 容器进行注入
import org.springframework.beans.factory.annotation.Autowired;
// 导入 Spring 框架中定义媒体类型的枚举类,用于设置响应的内容类型为 JSON 格式
import org.springframework.http.MediaType;
// 导入 Spring 框架用于将类标记为组件的注解,表明该类是一个 Spring 管理的组件,可被自动扫描并注入到其他需要的地方
import org.springframework.stereotype.Component;
// 导入 Spring 框架中用于获取请求上下文相关信息的类和接口
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
// 导入 Servlet 相关的用于操作 HTTP 响应的类,用于设置响应的各种属性以及向客户端输出内容
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Objects;
/**
* HttpHandler Web
* JSON HTTP
*
*
* @author
* @date 2022/3/28 14:15
*/
@Component
public class HttpHandler {
// 创建一个日志记录器,用于记录该类中不同操作阶段的日志信息,方便后续进行问题排查和调试
private static final Logger logger = LoggerFactory.getLogger(HttpHandler.class);
// 通过 Spring 的依赖注入机制,注入一个 ObjectMapper 对象,用于将对象转换为 JSON 字符串以便输出到客户端
@Autowired
private ObjectMapper objectMapper;
/**
* ServerResponseEntity JSON Web HTTP
* null
* HTTP
* I/O YamiShopBindException
*
* @param serverResponseEntity
* @param <T>
*/
public <T> void printServerResponseToWeb(ServerResponseEntity<T> serverResponseEntity) {
// 如果传入的服务器响应实体为 null记录日志提示信息并直接返回不进行后续操作
if (serverResponseEntity == null) {
logger.info("print obj is null");
return;
}
// 从 Spring 的请求上下文中获取 ServletRequestAttributes 对象,它包含了与当前请求相关的信息,如请求和响应对象等
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
// 如果获取到的 ServletRequestAttributes 对象为 null说明无法获取到请求上下文相关信息记录错误日志并返回无法进行响应输出操作
if (requestAttributes == null) {
logger.error("requestAttributes is null, can not print to web");
return;
}
// 从 ServletRequestAttributes 对象中获取 HttpServletResponse 对象,用于后续设置响应属性和向客户端输出内容
HttpServletResponse response = requestAttributes.getResponse();
// 如果获取到的 HttpServletResponse 对象为 null说明无法获取到有效的 HTTP 响应对象,记录错误日志并返回,无法进行响应输出操作
if (response == null) {
logger.error("httpServletResponse is null, can not print to web");
return;
}
// 记录响应的错误消息(这里假设 getMsg 方法获取的是错误相关信息,实际情况可能根据 ServerResponseEntity 的具体实现而定)到日志中,方便排查问题
logger.error("response error: " + serverResponseEntity.getMsg());
// 设置 HTTP 响应的字符编码为 UTF-8确保输出的内容能够正确地被客户端解析尤其是包含中文等多字节字符的情况
response.setCharacterEncoding(CharsetUtil.UTF_8);
// 设置 HTTP 响应的内容类型为 application/json表明响应的内容是 JSON 格式的数据,让客户端能够正确识别并解析
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
// 用于向 HTTP 响应中写入字符数据的对象,初始化为 null后续通过获取响应的输出流来实例化
PrintWriter printWriter = null;
try {
// 获取 HttpServletResponse 的输出流对象,用于向客户端写入响应内容(这里是将服务器响应实体转换后的 JSON 字符串写入)
printWriter = response.getWriter();
// 使用注入的 ObjectMapper 对象将服务器响应实体转换为 JSON 字符串,并写入到 HTTP 响应的输出流中,从而输出到客户端
printWriter.write(objectMapper.writeValueAsString(serverResponseEntity));
} catch (IOException e) {
// 如果在写入过程中出现 I/O 异常抛出自定义的业务异常YamiShopBindException并将原始的 I/O 异常作为原因传递,方便上层进行统一的异常处理和日志记录
throw new YamiShopBindException("io 异常", e);
}
}
/**
* YamiShopBindException JSON Web HTTP
* null
* ServerResponseEntity printServerResponseToWeb
* printServerResponseToWeb
*
* @param yamiShopBindException YamiShopBindException
* @param <T>
*/
public <T> void printServerResponseToWeb(YamiShopBindException yamiShopBindException) {
// 如果传入的 YamiShopBindException 异常对象为 null记录日志提示信息并直接返回不进行后续操作
if (yamiShopBindException == null) {
logger.info("print obj is null");
return;
}
// 判断异常对象中是否包含了服务器响应实体ServerResponseEntity如果包含则直接调用 printServerResponseToWeb 方法输出该实体内容到客户端
if (Objects.nonNull(yamiShopBindException.getServerResponseEntity())) {
printServerResponseToWeb(yamiShopBindException.getServerResponseEntity());
return;
}
// 如果异常对象中没有包含服务器响应实体,则创建一个新的 ServerResponseEntity 对象,用于封装异常中的错误码和错误消息等信息
ServerResponseEntity<T> serverResponseEntity = new ServerResponseEntity<>();
serverResponseEntity.setCode(yamiShopBindException.getCode());
serverResponseEntity.setMsg(yamiShopBindException.getMessage());
// 调用 printServerResponseToWeb 方法将构建好的服务器响应实体输出到客户端,完成响应信息的输出操作
printServerResponseToWeb(serverResponseEntity);
}
}

@ -0,0 +1,104 @@
/*
* Copyright (c) 2018-2999 广 All rights reserved.
*
* https://www.mall4j.com/
*
*
*
*
*/
package com.yami.shop.common.serializer.json;
// 导入Hutool工具库中用于字符串操作的工具类例如判断字符串是否为空、处理字符串拼接等操作
import cn.hutool.core.util.StrUtil;
// 导入Jackson库中用于自定义JSON序列化相关的核心类用于定义如何将Java对象序列化为JSON格式
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
// 导入自定义的类Qiniu类可能包含了七牛云相关的配置信息如资源访问地址等
import com.yami.shop.common.bean.Qiniu;
// 导入自定义的图片上传工具类,可能用于获取图片上传相关的配置信息,比如上传类型、资源地址等
import com.yami.shop.common.util.ImgUploadUtil;
// 导入Spring框架用于实现依赖注入的注解以及将类标记为组件的注解表明该类是受Spring管理的组件可被自动注入到需要的地方
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* ImgJsonSerializerJSONJSON
* httphttps访
* JSONJSON
*
* @author lanhai
*/
@Component
public class ImgJsonSerializer extends JsonSerializer<String> {
// 通过Spring的依赖注入机制注入一个Qiniu对象该对象可能包含了七牛云存储相关的配置信息比如七牛云资源的访问地址等
@Autowired
private Qiniu qiniu;
// 注入一个ImgUploadUtil对象用于获取图片上传相关的配置信息例如图片上传类型等辅助对图片路径进行处理
@Autowired
private ImgUploadUtil imgUploadUtil;
/**
* JacksonserializeJSON
*
* @param value
* @param gen JacksonJsonGeneratorJSONJSON
* @param serializers JacksonSerializerProvider
* @throws IOException JsonGeneratorI/O
*/
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
// 判断传入的要序列化的字符串是否为空空白字符串包括null、空字符串以及只包含空格等不可见字符的字符串
if (StrUtil.isBlank(value)) {
// 如果为空则向JsonGenerator写入一个空字符串保持JSON格式的一致性然后直接返回不再进行后续处理
gen.writeString(StrUtil.EMPTY);
return;
}
// 将传入的字符串按照逗号进行分割,得到一个字符串数组,假设这里的字符串表示多个图片路径,以逗号分隔开,每个元素就是一个单独的图片路径
String[] imgs = value.split(StrUtil.COMMA);
// 创建一个可变的字符串构建器,用于拼接处理后的图片路径,方便后续构建最终的序列化字符串
StringBuilder sb = new StringBuilder();
// 用于存储资源访问地址,根据不同的上传类型来确定具体的值,后续会将其添加到图片路径前面(如果图片路径本身不符合要求的话)
String resourceUrl = "";
// 定义一个正则表达式字符串,用于匹配以"http"或"https"开头的字符串,目的是判断图片路径是否已经是完整的网络访问地址形式
String rule = "^((http[s]{0,1})://)";
// 使用定义好的正则表达式创建一个Pattern对象用于后续进行正则匹配操作
Pattern pattern = Pattern.compile(rule);
// 根据ImgUploadUtil对象获取的上传类型来确定资源访问地址resourceUrl的值
if (Objects.equals(imgUploadUtil.getUploadType(), 2)) {
// 如果上传类型为2从注入的Qiniu对象中获取资源访问地址可能是七牛云存储资源的访问地址用于构建完整的图片访问路径
resourceUrl = qiniu.getResourcesUrl();
} else if (Objects.equals(imgUploadUtil.getUploadType(), 1)) {
// 如果上传类型为1从ImgUploadUtil对象中获取资源访问地址可能是其他存储方式对应的资源访问地址
resourceUrl = imgUploadUtil.getResourceUrl();
}
// 遍历分割后的每个图片路径字符串
for (String img : imgs) {
// 使用创建的Pattern对象对当前图片路径字符串进行正则匹配得到一个Matcher对象用于查看是否匹配成功
Matcher matcher = pattern.matcher(img);
// 如果匹配成功,说明图片路径已经是以"http"或"https"开头的完整网络访问地址形式,直接将其添加到字符串构建器中,并添加逗号(保持与传入格式的一致性,后续会处理末尾多余的逗号)
if (matcher.find()) {
sb.append(img).append(StrUtil.COMMA);
} else {
// 如果图片路径不是完整的网络访问地址形式则将前面确定的资源访问地址resourceUrl、当前图片路径以及逗号依次添加到字符串构建器中构建完整的图片访问路径格式
sb.append(resourceUrl).append(img).append(StrUtil.COMMA);
}
}
// 删除字符串构建器中最后一个字符(末尾多余的逗号),得到最终处理好的图片路径字符串
sb.deleteCharAt(sb.length() - 1);
// 将处理好的图片路径字符串通过JsonGenerator写入到最终的JSON输出中完成序列化操作
gen.writeString(sb.toString());
}
}

@ -0,0 +1,168 @@
/*
* Copyright (c) 2018-2999 广 All rights reserved.
*
* https://www.mall4j.com/
*
*
*
*
*/
package com.yami.shop.common.util;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
*
* doubleBigDecimal
* @author lanhai
*/
public class Arith {
/**
* div2
* HALF_EVEN
*/
private static final int DEF_DIV_SCALE = 2;
/**
*
*
*/
private Arith() {
}
/**
*
* doubleBigDecimal
* double使double
*
* @param v1
* @param v2
* @return double
*/
public static double add(double v1, double v2) {
// 必须转换成String因为BigDecimal的构造函数建议使用基于字符串的构造方式来避免精度问题
// 如果直接使用double类型传入构造函数在某些情况下可能会出现精度丢失。
String s1 = Double.toString(v1);
String s2 = Double.toString(v2);
BigDecimal b1 = new BigDecimal(s1);
BigDecimal b2 = new BigDecimal(s2);
return b1.add(b2).doubleValue();
}
/**
*
* doubleBigDecimal
* double
*
* @param v1
* @param v2
* @return double
*/
public static double sub(double v1, double v2) {
String s1 = Double.toString(v1);
String s2 = Double.toString(v2);
BigDecimal b1 = new BigDecimal(s1);
BigDecimal b2 = new BigDecimal(s2);
return b1.subtract(b2).doubleValue();
}
/**
*
* doubleBigDecimal
* double
*
* @param v1
* @param v2
* @return double
*/
public static double mul(double v1, double v2) {
String s1 = Double.toString(v1);
String s2 = Double.toString(v2);
BigDecimal b1 = new BigDecimal(s1);
BigDecimal b2 = new BigDecimal(s2);
return b1.multiply(b2).doubleValue();
}
/**
* 10
* divDEF_DIV_SCALE
*
*
* @param v1
* @param v2
* @return double
*/
public static double div(double v1, double v2) {
return div(v1, v2, DEF_DIV_SCALE);
}
/**
* scale
* scale0
* 0doubleBigDecimal
* 使HALF_EVENdouble
*
* @param v1
* @param v2
* @param scale
* @return double
*/
public static double div(double v1, double v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException("The scale must be a positive integer or zero");
}
String s1 = Double.toString(v1);
String s2 = Double.toString(v2);
BigDecimal b1 = new BigDecimal(s1);
BigDecimal b2 = new BigDecimal(s2);
return b1.divide(b2, scale, RoundingMode.HALF_EVEN).doubleValue();
}
/**
*
* scale0
* BigDecimal1BigDecimal
* scaleHALF_EVENdouble
*
* @param v double
* @param scale
* @return double
*/
public static double round(double v, int scale) {
if (scale < 0) {
throw new IllegalArgumentException("The scale must be a positive integer or zero");
}
String s = Double.toString(v);
BigDecimal b = new BigDecimal(s);
BigDecimal one = new BigDecimal("1");
return b.divide(one, scale, RoundingMode.HALF_EVEN).doubleValue();
}
/**
* BigDecimal
* double便BigDecimal使
*
* @param bigDecimal BigDecimal
* @param bigDecimal2 BigDecimal
* @param bigDecimal3 BigDecimal
* @return double
*/
public static double add(BigDecimal bigDecimal, BigDecimal bigDecimal2, BigDecimal bigDecimal3) {
return bigDecimal.add(bigDecimal2).add(bigDecimal3).doubleValue();
}
/**
* BigDecimal
* doubleBigDecimal
*
* @param preDepositPrice BigDecimal
* @param finalPrice BigDecimal
* @return double
*/
public static double add(BigDecimal preDepositPrice, BigDecimal finalPrice) {
return preDepositPrice.add(finalPrice).doubleValue();
}
}

@ -0,0 +1,87 @@
/*
* Copyright (c) 2018-2999 广 All rights reserved.
*
* https://www.mall4j.com/
*
*
*
*
*/
package com.yami.shop.common.util;
import lombok.AllArgsConstructor;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Component;
/**
* CacheManagerUtil便Spring
* SpringCacheManager
* 便
* Spring@Component使
*
* @author lanhai
*/
@Component
@AllArgsConstructor
public class CacheManagerUtil {
// Spring的缓存管理器用于获取具体的缓存对象等操作通过构造注入的方式获取
private CacheManager cacheManager;
/**
*
*
* @param <T>
* @param cacheName CacheManager
* @param key
* @return Tnull
*/
@SuppressWarnings({"unchecked"})
public <T> T getCache(String cacheName, String key) {
// 通过缓存管理器获取指定名称的缓存对象
Cache cache = cacheManager.getCache(cacheName);
if (cache == null) {
return null;
}
// 从缓存对象中尝试获取对应键的值的包装对象
Cache.ValueWrapper valueWrapper = cache.get(key);
if (valueWrapper == null) {
return null;
}
// 从包装对象中获取实际的值并转换为指定的泛型类型T返回
return (T) valueWrapper.get();
}
/**
*
*
* @param cacheName CacheManager
* @param key
* @param value Java
*/
public void putCache(String cacheName, String key, Object value) {
// 通过缓存管理器获取指定名称的缓存对象
Cache cache = cacheManager.getCache(cacheName);
if (cache!= null) {
// 如果缓存对象存在,则将键值对存入该缓存对象中
cache.put(key, value);
}
}
/**
*
*
* @param cacheName CacheManager
* @param key
*/
public void evictCache(String cacheName, String key) {
// 通过缓存管理器获取指定名称的缓存对象
Cache cache = cacheManager.getCache(cacheName);
if (cache!= null) {
// 如果缓存对象存在,则清除对应键的缓存数据
cache.evict(key);
}
}
}

@ -0,0 +1,66 @@
/*
* Copyright (c) 2018-2999 广 All rights reserved.
*
* https://www.mall4j.com/
*
*
*
*
*/
package com.yami.shop.common.util;
// 导入Spring相关的类用于获取请求相关的上下文信息基于请求上下文来获取HttpServletRequest对象
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
// 导入Servlet相关的类用于操作HTTP请求这里主要是HttpServletRequest它包含了请求相关的各种信息
import jakarta.servlet.http.HttpServletRequest;
/**
* HttpContextUtilsHTTP便
* SpringHttpServletRequest
* 便使
*
* @author lanhai
*/
public class HttpContextUtils {
/**
* 线HttpServletRequest
* SpringRequestContextHolderServletRequestAttributes
* HttpServletRequest
*
* @return HttpServletRequest HttpServletRequest
*
*/
public static HttpServletRequest getHttpServletRequest() {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
}
/**
*
* getHttpServletRequestHttpServletRequest
* getRequestURLURL
* getRequestURIURL
*
* @return String http://example.com:8080具体取决于实际请求情况
*/
public static String getDomain() {
HttpServletRequest request = getHttpServletRequest();
StringBuffer url = request.getRequestURL();
return url.delete(url.length() - request.getRequestURI().length(), url.length()).toString();
}
/**
* Origin
* HttpServletRequestgetHeader"Origin"
*
*
* @return String Originhttp://example.com如果有Origin请求头的话否则返回null等情况
*/
public static String getOrigin() {
HttpServletRequest request = getHttpServletRequest();
return request.getHeader("Origin");
}
}

@ -0,0 +1,127 @@
/*
* Copyright (c) 2018-2999 广 All rights reserved.
*
* https://www.mall4j.com/
*
*
*
*
*/
package com.yami.shop.common.util;
// 导入 Hutool 库中用于生成分布式唯一ID的 Snowflake 类,通常基于雪花算法实现
import cn.hutool.core.lang.Snowflake;
// 导入 Spring 框架用于实现依赖注入的注解以及将类标记为组件的注解,表明该类是受 Spring 管理的组件
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* IdUtil
* 1. DICT DICT.length()
* 2. ID便使 ID
* 3. ID ID
* 4. Snowflake ID ID
*
* @author xuliugen
* @date 2018/04/23
*/
@Component
public class IdUtil {
// 通过 Spring 的依赖注入机制,注入一个 Snowflake 实例,用于生成分布式唯一 ID可能用于生成短 ID 的基础)
@Autowired
private Snowflake snowflake;
// 定义了一个包含数字和大小写字母(去除了容易混淆的部分字母)的字符串,作为自定义进制的字符集,用于进制转换操作
private static final String DICT = "0123456789abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ";
// 计算出基于 DICT 字符集的进制数,即字符集的长度,用于进制转换过程中的计算
private static final int SEED = DICT.length();
// 定义了短网址编码的最小长度,用于在生成短网址编码时,如果长度不足则进行补位操作,保证编码长度符合一定要求
private static final int ID_MIN_LENGTH = 6;
/**
* DICT 便
*/
private static final char[] CHARS = DICT.toCharArray();
/**
* Map
*/
private static final Map<Character, Integer> NUMBERS = new HashMap<>();
// 静态代码块,用于初始化 NUMBERS 这个字符到数字的映射 Map遍历 CHARS 字符数组,将每个字符与其对应的索引(在自定义进制下的数字)存入 Map 中
static {
int len = CHARS.length;
for (int i = 0; i < len; i++) {
NUMBERS.put(CHARS[i], i);
}
}
/**
* ID DICT
* SEED DICT
* ID_MIN_LENGTH DICT
*
* @param id ID (1 - 56.8 billion)
* @return "RwTji8""GijT7Y"
*/
public static String encode(long id) {
// 创建一个可变的字符串构建器,用于逐步构建短网址编码字符串
StringBuilder shortUrl = new StringBuilder();
// 当传入的十进制数字大于 0 时,进行进制转换操作,采用除基取余法
while (id > 0) {
// 计算当前十进制数除以自定义进制数SEED的余数将其转换为整数类型该余数将作为在 DICT 中查找对应字符的索引
int r = (int) (id % SEED);
// 将根据余数获取到的对应字符插入到短网址编码字符串的开头(逆序构建编码字符串)
shortUrl.insert(0, CHARS[r]);
// 更新十进制数,将其除以自定义进制数,得到下一轮循环要处理的数字
id = id / SEED;
}
// 获取当前已经构建好的短网址编码字符串的长度
int len = shortUrl.length();
// 如果长度小于最小长度要求ID_MIN_LENGTH进行补位操作
while (len < ID_MIN_LENGTH) {
// 在短网址编码字符串的开头插入 DICT 中的第一个字符(通常是 '0')进行补位
shortUrl.insert(0, CHARS[0]);
// 更新长度
len++;
}
// 返回最终构建好的短网址编码字符串
return shortUrl.toString();
}
/**
* ID
*
*
* @param key "RwTji8""GijT7Y"
* @return ID
*/
public static long decode(String key) {
// 将传入的短网址编码字符串转换为字符数组,方便逐个字符进行处理
char[] shorts = key.toCharArray();
// 获取字符数组的长度,即短网址编码的长度
int len = shorts.length;
// 初始化用于累加计算的十进制数字为 0
long id = 0L;
// 遍历短网址编码的每个字符,从左到右(按照权重从高到低)进行解析计算
for (int i = 0; i < len; i++) {
// 根据当前字符在 NUMBERS 映射 Map 中获取其对应的数字在自定义进制下的数字表示并乘以当前位置对应的权重SEED 的幂次方),然后累加到结果中
id = id + (long) (NUMBERS.get(shorts[i]) * Math.pow(SEED, len - i - 1));
}
// 返回解析得到的十进制数字(数据库记录 ID
return id;
}
/**
* Snowflake ID ID
* ID
*
* @return ID ID
*/
public String nextShortId() {
return encode(snowflake.nextId());
}
}

@ -0,0 +1,127 @@
package com.yami.shop.common.util;
// 导入Hutool工具库中用于字符串操作的工具类可用于判断字符串是否为空等操作
import cn.hutool.core.util.StrUtil;
// 导入自定义的用于封装图片上传相关配置信息的类包含存储路径、上传类型、资源访问URL等属性
import com.yami.shop.common.bean.ImgUpload;
// 导入自定义的业务异常类,用于在特定业务逻辑出现问题时抛出相应的异常信息,方便统一处理
import com.yami.shop.common.exception.YamiShopBindException;
// 导入Spring框架用于实现依赖注入的注解以及将类标记为组件的注解表明该类是受Spring管理的组件可在项目中被自动注入和使用
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
// 导入Spring用于处理文件上传的核心类代表上传的文件对象包含文件内容、文件名等信息
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
/**
* ImgUploadUtil
* 访URL
* 便
*
* @author TRACK
*/
@Component
public class ImgUploadUtil {
// 通过Spring的依赖注入机制注入一个ImgUpload对象该对象包含了本地文件上传相关的配置信息如存储路径、上传方式等
@Autowired
private ImgUpload imgUpload;
/**
* ImgUpload
* nullYamiShopBindException
*
*
* @return Integer 12使
*/
public Integer getUploadType() {
Integer uploadType = imgUpload.getUploadType();
if (Objects.isNull(uploadType)) {
throw new YamiShopBindException("请配置图片存储方式");
}
return uploadType;
}
/**
* ImgUploadimagePath
* nullYamiShopBindException
*
*
* @return String
*/
public String getUploadPath() {
String imagePath = imgUpload.getImagePath();
if (Objects.isNull(imagePath) || StrUtil.isBlank(imagePath)) {
throw new YamiShopBindException("请配置图片存储路径");
}
return imagePath;
}
/**
* 访URLImgUpload访URLresourceUrl
* null访URLYamiShopBindException
* 访URLURL访
*
* @return String 访URL访
*/
public String getResourceUrl() {
String resourceUrl = imgUpload.getResourceUrl();
if (Objects.isNull(resourceUrl) || StrUtil.isBlank(resourceUrl)) {
throw new YamiShopBindException("请配置图片路径");
}
return resourceUrl;
}
/**
* MultipartFile
*
* I/O
*
*
* @param img Spring
* @param fileName
* @return String 访
*/
public String upload(MultipartFile img, String fileName) {
// 获取配置的本地文件上传路径(存储文件夹路径)
String filePath = imgUpload.getImagePath();
// 根据文件上传路径和传入的文件名构建一个File对象代表要保存的目标文件
File file = new File(filePath + fileName);
// 判断目标文件所在的目录是否存在,如果不存在则尝试创建目录
if (!file.exists()) {
boolean result = file.mkdirs();
// 如果目录创建失败返回false抛出自定义的业务异常提示创建目录失败的具体路径信息
if (!result) {
throw new YamiShopBindException("创建目录:" + filePath + "失败");
}
}
try {
// 将上传的文件内容转移到目标文件中即将MultipartFile中的文件数据写入到本地创建好的目标文件里
img.transferTo(file);
} catch (IOException e) {
// 如果在文件转移写入过程中出现I/O异常抛出自定义的业务异常提示图片上传失败
throw new YamiShopBindException("图片上传失败");
}
// 文件上传成功后,返回上传后的文件名,方便后续业务使用
return fileName;
}
/**
*
* deleteOnExitJVM退
* JVM退
*
* @param fileName
*/
public void delete(String fileName) {
// 获取配置的本地文件上传路径(存储文件夹路径)
String filePath = imgUpload.getImagePath();
// 根据文件上传路径和传入的文件名构建一个File对象代表要删除的目标文件
File file = new File(filePath + fileName);
// 标记文件在JVM退出时删除注意这并不一定会立即删除文件而是在JVM正常退出时执行删除操作如果文件存在且可删除的话
file.deleteOnExit();
}
}

@ -0,0 +1,67 @@
/*
* Copyright (c) 2018-2999 广 All rights reserved.
*
* https://www.mall4j.com/
*
*
*
*
*/
package com.yami.shop.common.util;
// 导入Servlet相关的用于操作HTTP请求的类后续用于获取请求头以及客户端IP地址等信息
import jakarta.servlet.http.HttpServletRequest;
/**
* IpHelperIP
* IP
* IP
* IPIP
*
* @author lanhai
*/
public class IpHelper {
// 定义一个表示未知IP地址的常量字符串用于后续判断请求头中获取到的IP地址是否有效
private static final String UNKNOWN = "unknown";
/**
* IPIP
* "x-forwarded-for"IPIP
* IP0"unknown""Proxy-Client-IP"
* "WL-Proxy-Client-IP"
* IPRemoteAddrIP
* IPIPIPIP
*
* @return String IPnull
*/
public static String getIpAddr() {
// 通过HttpContextUtils工具类获取当前线程绑定的HttpServletRequest对象该对象包含了请求相关的各种信息
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
// 如果获取到的HttpServletRequest对象为null说明无法获取到请求上下文相关信息直接返回null无法获取IP地址
if (request == null) {
return null;
}
// 首先尝试从"x-forwarded-for"请求头获取IP地址该请求头在经过代理服务器转发时可能包含客户端真实IP
String ip = request.getHeader("x-forwarded-for");
// 如果获取到的IP地址为空、长度为0或者等于表示未知的字符串不区分大小写比较则继续尝试从其他请求头获取
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
// 如果从"Proxy-Client-IP"请求头获取到的IP地址仍不符合要求为空、长度为0或者是未知字符串则再尝试从"WL-Proxy-Client-IP"请求头获取
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
// 如果经过前面的尝试还是没有获取到有效IP地址则直接获取请求的远程地址RemoteAddr作为IP地址这是最基本的获取客户端IP的方式
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 将获取到的IP地址字符串按照逗号进行分割因为可能存在经过多层代理IP地址有多个的情况以逗号分隔
String[] ips = ip.split(",");
// 返回分割后的第一个IP地址去除两端的空白字符作为客户端的真实IP地址如果只有一个IP则就是该IP本身
return ips[0].trim();
}
}

@ -0,0 +1,148 @@
/*
* Copyright (c) 2018-2999 广 All rights reserved.
*
* https://www.mall4j.com/
*
*
*
*
*/
package com.yami.shop.common.util;
// 导入Jackson相关的注解和类用于配置JSON序列化和反序列化过程中的一些行为比如包含哪些属性、如何处理未知属性等
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.json.JsonWriteFeature;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
// 导入Lombok的日志记录相关注解用于简化日志记录代码自动生成名为log的日志记录对象
import lombok.extern.slf4j.Slf4j;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* JsonJackson便JSON
* JavaJSONJSONJavaJSON
* JacksonObjectMapperJSON
*
* @author lanhai
*/
@Slf4j
public class Json {
// 创建一个静态的ObjectMapper实例ObjectMapper是Jackson库中用于进行JSON序列化和反序列化的核心类后续所有的JSON操作都基于它来实现
private static ObjectMapper objectMapper = new ObjectMapper();
// 静态代码块用于对ObjectMapper实例进行一系列的配置这些配置会影响JSON序列化和反序列化的行为。
static {
// 设置序列化时的包含规则这里配置为JsonInclude.Include.NON_EMPTY表示如果属性值为空比如null、空字符串、空集合等则不输出该属性到JSON字符串中减少不必要的JSON数据量。
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
// 配置在序列化时对于空的Java对象没有任何属性值的对象转JSON的时候不抛出错误而是正常返回一个空的JSON对象如 {}),增强程序的健壮性,避免因空对象序列化失败导致整个操作中断。
objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
// 禁用将日期类型属性序列化为时间戳的功能,这样在处理日期类型数据时可以按照更符合业务需求的日期格式(比如特定的字符串格式)进行序列化,而不是默认的时间戳形式。
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
// 禁用在反序列化时遇到未知属性即JSON字符串中的属性在对应的Java类中不存在定义抛出异常的功能这样即使JSON数据有额外的属性也能尽量解析出已知的属性值避免因未知属性导致整个反序列化失败。
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
// 取消对非ASCII字符的转码操作使得JSON字符串中可以直接包含如中文等非ASCII字符而不需要进行转义编码方便查看和处理更符合实际业务场景中对中文等字符的使用需求。
objectMapper.configure(JsonWriteFeature.ESCAPE_NON_ASCII.mappedFeature(), false);
}
/**
* JavaJSON
* ObjectMapperwriteValueAsStringJSON
* null
*
* @param object JSONJavaJacksonPOJO
* @return String JSONnull
*/
public static String toJsonString(Object object) {
try {
return objectMapper.writeValueAsString(object);
} catch (JsonProcessingException e) {
// 记录对象转JSON时出现的错误日志方便后续排查问题这里记录了异常的详细信息通过传入e作为参数
log.error("对象转json错误", e);
}
return null;
}
/**
* JSONJava
* ObjectMapperreadValueclazzJSONJava
* JSONnull
*
*
* @param json JSONJavaJackson
* @param clazz JavaObjectMapperJSON
* @return <T> Javaclazznull
*/
public static <T> T parseObject(String json, Class<T> clazz) {
T result = null;
try {
result = objectMapper.readValue(json, clazz);
} catch (Exception e) {
// 记录JSON转对象时出现的错误日志方便后续排查问题这里记录了异常的详细信息通过传入e作为参数
log.error("对象转json错误", e);
}
return result;
}
/**
* ObjectMapperJSON
* JSON使JSON
*
* @return ObjectMapper ObjectMapperJSON
*/
public static ObjectMapper getObjectMapper() {
return objectMapper;
}
/**
* JSONJavaList
* ObjectMapperreadValueJSONclazz
* ListnullList
* 便
* 使TypeReference10
*
* @param json JSONJavaJSON
* @param clazz JavaObjectMapperJSONMyClass[].class
* @return <T> JavaclazzList
*/
public static <T> List<T> parseArray(String json, Class<T[]> clazz) {
T[] result = null;
try {
result = objectMapper.readValue(json, clazz);
} catch (Exception e) {
// 记录JSON转换时出现的错误日志方便后续排查问题这里记录了异常的详细信息通过传入e作为参数
log.error("Json转换错误", e);
}
if (result == null) {
return Collections.emptyList();
}
return Arrays.asList(result);
}
/**
* JSONJsonNodeJsonNodeJSON便JSON
* JSONJSONMap
* null
*
* @param jsonStr JsonNodeJSONJSON
* @return JsonNode JsonNodenullJSON
*/
public static JsonNode parseJson(String jsonStr) {
JsonNode jsonNode = null;
try {
jsonNode = objectMapper.readTree(jsonStr);
} catch (Exception e) {
// 记录JSON转换时出现的错误日志方便后续排查问题这里记录了异常的详细信息通过传入e作为参数
log.error("Json转换错误", e);
}
return jsonNode;
}
}

@ -0,0 +1,51 @@
/*
* Copyright (c) 2018-2999 广 All rights reserved.
*
* https://www.mall4j.com/
*
*
*
*
*/
package com.yami.shop.common.util;
// 导入Hutool工具库中用于分页相关操作的工具类这里主要用于将页码、每页数量等信息转换为数据库查询中起始位置和结束位置的相关操作
import cn.hutool.core.util.PageUtil;
// 导入MyBatis Plus框架中用于表示分页信息的核心类包含了当前页码、每页显示数量等分页相关的属性和方法
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
// 引入Lombok的Data注解通过该注解编译器会自动帮我们生成类的常用方法比如各属性的Getter、Setter方法以及toString、equals、hashCode方法等简化代码编写
import lombok.Data;
/**
* PageAdapterMyBatis PlusPage
*
* beginsize便使
* Hutool
*
* @author lh
*/
@Data
public class PageAdapter {
// 用于表示分页查询时的起始位置(通常对应数据库查询中的偏移量,从第几条记录开始查询)
private int begin;
// 用于表示分页查询时每页的记录数量,即每页显示多少条数据
private int size;
/**
* MyBatis PlusPage
* HutoolPageUtiltransToStartEnd
* Pagebeginsize
*
* @param page MyBatis PlusPagebeginsize
*/
public PageAdapter(Page page) {
// 调用Hutool的PageUtil工具类的transToStartEnd方法将当前页码需要减1因为数据库查询中页码通常从0开始计数和每页数量转换为起始位置和结束位置的数组这里取数组的第一个元素作为起始位置begin
int[] startEnd = PageUtil.transToStartEnd((int) page.getCurrent() - 1, (int) page.getSize());
this.begin = startEnd[0];
// 将传入的Page对象中的每页数量赋值给size属性作为每页的记录数量
this.size = (int) page.getSize();
}
}

@ -0,0 +1,213 @@
/*
* Copyright (c) 2018-2999 广 All rights reserved.
*
* https://www.mall4j.com/
*
*
*
*
*/
package com.yami.shop.common.util;
// 导入MyBatis Plus框架中用于表示分页信息的核心类包含了分页相关的属性如当前页、每页大小等以及操作方法如获取记录列表、设置总数等
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
// 导入Jackson相关的注解用于在JSON序列化和反序列化过程中忽略指定的属性这里用于标记某些属性在转换为JSON时不进行处理
import com.fasterxml.jackson.annotation.JsonIgnore;
// 导入Swagger相关的注解用于在生成API文档时隐藏某些属性使得这些属性不在文档中展示出来一般用于内部使用或不需要对外暴露的属性
import io.swagger.v3.oas.annotations.Hidden;
// 导入Swagger相关的注解用于在生成API文档时为类、属性等添加描述信息方便前端开发人员等查看其含义和作用
import io.swagger.v3.oas.annotations.media.Schema;
// 导入SpringDoc相关的注解用于标记该类作为参数对象在生成API文档以及进行参数校验等场景下可以被识别和处理
import org.springdoc.core.annotations.ParameterObject;
import java.util.List;
/**
* PageParamMyBatis PlusPage
* PageJSON
* Page
* count
*
* @author lanhai
*/
@Schema
@ParameterObject
public class PageParam<T> extends Page<T> {
/**
* 10
* @SchemaAPI便使
*/
@Schema(description = "每页大小默认10")
private long size = 10;
/**
* 1@Schema便API
*/
@Schema(description = "当前页默认1")
private long current = 1;
/**
* @HiddenAPI使
*/
@Hidden
private List<T> records;
/**
* @HiddenAPI
*/
@Hidden
private long total = 0;
/**
* counttruecount
* @JsonIgnoreJSON
*/
@JsonIgnore
private boolean isSearchCount = true;
/**
* @JsonIgnoreJSON使
*/
@JsonIgnore
private String countId;
/**
* 100@JsonIgnoreJSON
*/
@JsonIgnore
private Long maxLimit;
/**
* countSQL@JsonIgnoreJSON使
*/
@JsonIgnore
private boolean optimizeCountSql;
/**
* Pagerecords
*
* @return List<T> T
*/
@Override
public List<T> getRecords() {
return this.records;
}
/**
* Pagerecordsthis便
* 便
*
* @param records T
* @return Page<T>
*/
@Override
public Page<T> setRecords(List<T> records) {
this.records = records;
return this;
}
/**
* Pagetotal使
*
* @return long
*/
@Override
public long getTotal() {
return this.total;
}
/**
* Pagetotalthis便
* count
*
* @param total
* @return Page<T>
*/
@Override
public Page<T> setTotal(long total) {
this.total = total;
return this;
}
/**
* countisSearchCount
* total0falsecount
* isSearchCountcount
*
* @return boolean counttruecountfalse
*/
@JsonIgnore
public boolean getSearchCount() {
if (total < 0) {
return false;
}
return isSearchCount;
}
/**
* PagecountisSearchCountthis便
* count
*
* @param isSearchCount counttruefalse
* @return Page<T>
*/
@Override
public Page<T> setSearchCount(boolean isSearchCount) {
this.isSearchCount = isSearchCount;
return this;
}
/**
* Pagesize使
*
* @return long
*/
@Override
public long getSize() {
return this.size;
}
/**
* Page
* size100size100
* sizesizethis便
*
* @param size
* @return Page<T>
*/
@Override
public Page<T> setSize(long size) {
int maxSize = 100;
if (size > maxSize) {
this.size = maxSize;
} else {
this.size = size;
}
return this;
}
/**
* Pagecurrent使
*
* @return long
*/
@Override
public long getCurrent() {
return this.current;
}
/**
* Pagecurrentthis便
*
*
* @param current
* @return Page<T>
*/
@Override
public Page<T> setCurrent(long current) {
this.current = current;
return this;
}
}

@ -0,0 +1,27 @@
package com.yami.shop.security.admin.dto;
import com.yami.shop.security.common.dto.AuthenticationDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* CaptchaAuthenticationDTODTO
* AuthenticationDTO
*
*
*
* @author
* @date 2022/3/28 14:57
*/
@Data
public class CaptchaAuthenticationDTO extends AuthenticationDTO {
/**
* captchaVerification
* @SchemaSwagger
* required = true
* 便
*/
@Schema(description = "验证码", required = true)
private String captchaVerification;
}

@ -0,0 +1,136 @@
package com.yami.shop.security.api.controller;
// 导入MyBatis Plus框架中用于构建条件查询的核心类方便编写数据库查询条件此处用于查询用户信息
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
// 导入项目中自定义的用户实体类,代表数据库中存储的用户相关信息,包含各种用户属性,如用户名、手机号、密码等
import com.yami.shop.bean.model.User;
// 导入项目自定义的业务异常类,用于在业务逻辑出现特定错误情况时抛出异常,方便统一处理和向客户端返回错误信息
import com.yami.shop.common.exception.YamiShopBindException;
// 导入项目自定义的工具类,可能包含一些通用的业务逻辑处理方法,此处具体功能需看其内部实现(从名字推测和主体逻辑相关)
import com.yami.shop.common.util.PrincipalUtil;
// 导入项目中定义的用于操作数据库中用户表的Mapper接口通过它可以调用数据库相关的查询、插入等操作方法由MyBatis框架生成实现类来具体执行SQL操作
import com.yami.shop.dao.UserMapper;
// 导入与安全相关的业务对象类用于封装在Token中存储的用户信息方便在不同模块间传递和使用用户相关数据
import com.yami.shop.security.common.bo.UserInfoInTokenBO;
// 导入与安全相关的用于接收登录认证信息的数据传输对象DTO包含用户名、密码以及用户所属系统类型等信息用于接收前端传入的登录数据
import com.yami.shop.security.common.dto.AuthenticationDTO;
// 导入与安全相关的枚举类用于区分不同的系统类型此处有个普通用户类型ORDINARY可能还有其他类型用于不同业务场景下的用户分类
import com.yami.shop.security.common.enums.SysTypeEnum;
// 导入与安全相关的用于密码检查的管理类,可能包含验证密码是否符合规则、检查密码错误次数等逻辑,保障登录密码的安全性
import com.yami.shop.security.common.manager.PasswordCheckManager;
// 导入与安全相关的用于密码管理的类,可能包含密码加密、解密等相关操作方法,确保密码在存储和传输过程中的安全性
import com.yami.shop.security.common.manager.PasswordManager;
// 导入与安全相关的用于管理Token存储和操作的类负责生成、存储Token以及基于用户信息获取相应的Token相关信息等功能
import com.yami.shop.security.common.manager.TokenStore;
// 导入与安全相关的用于返回给前端包含Token信息的视图对象VO将后端处理后的Token相关数据以合适的格式返回给前端展示和后续使用
import com.yami.shop.security.common.vo.TokenInfoVO;
// 导入Swagger相关的注解用于生成API文档@Tag注解用于给接口分类添加标签方便文档中进行分组展示和说明
import io.swagger.v3.oas.annotations.tags.Tag;
// 导入Swagger相关的注解用于在API文档中描述接口的功能摘要和详细信息方便前端开发人员等查看接口的作用和使用方式
import io.swagger.v3.oas.annotations.Operation;
// 导入Spring框架用于实现依赖注入的注解表明后续的属性需要由Spring容器进行注入实例化
import org.springframework.beans.factory.annotation.Autowired;
// 导入项目自定义的用于封装服务器响应信息的实体类,包含响应状态码、消息、数据等内容,用于统一向客户端返回响应结果
import com.yami.shop.common.response.ServerResponseEntity;
// 导入Spring框架用于定义处理POST请求的注解将下面的方法映射为一个接收POST请求的接口处理对应的业务逻辑
import org.springframework.web.bind.annotation.PostMapping;
// 导入Spring框架用于接收请求体数据并将其绑定到方法参数的注解确保前端传入的JSON等格式的数据能正确映射到对应的参数对象上
import org.springframework.web.bind.annotation.RequestBody;
// 导入Spring框架用于将类标记为RESTful风格的控制器的注解表明该类中的方法主要用于处理HTTP请求并返回JSON等格式的响应数据
import org.springframework.web.bind.annotation.RestController;
// 导入Jakarta验证相关的注解用于对传入的参数对象进行数据校验确保参数符合一定的规则比如非空等要求
import jakarta.validation.Valid;
/**
* LoginControllerSpring RESTful
*
* Token
* 便使SwaggerAPI便使
*
* @author
* @date 2022/3/28 15:20
*/
@RestController
@Tag(name = "登录")
public class LoginController {
// 通过Spring的依赖注入机制注入一个TokenStore实例用于管理Token的存储、生成以及获取相关信息等操作
@Autowired
private TokenStore tokenStore;
// 注入一个UserMapper实例用于调用数据库中用户表相关的查询、操作方法以便获取用户信息进行登录验证等操作
@Autowired
private UserMapper userMapper;
// 注入一个PasswordCheckManager实例用于对用户输入的密码进行各种检查如密码错误次数限制等安全相关的验证操作
@Autowired
private PasswordCheckManager passwordCheckManager;
// 注入一个PasswordManager实例用于对密码进行加密、解密等管理操作此处主要用于解密前端传入的密码进行后续验证
@Autowired
private PasswordManager passwordManager;
/**
* @Valid
*
* Token
* TokenStoreTokenTokenTokenInfoVOToken
*
* @param authenticationDTO @RequestBody
* @return ServerResponseEntity<TokenInfoVO> TokenTokenInfoVOToken
*/
@PostMapping("/login")
@Operation(summary = "账号密码(用于前端登录)", description = "通过账号/手机号/用户名密码登录,还要携带用户的类型,也就是用户所在的系统")
public ServerResponseEntity<TokenInfoVO> login(
@Valid @RequestBody AuthenticationDTO authenticationDTO) {
// 从登录认证信息对象中获取用户名(可能是手机号或者普通用户名形式),用于后续查找用户信息
String mobileOrUserName = authenticationDTO.getUserName();
// 根据获取到的用户名查找对应的用户信息,若找不到则会抛出异常告知账号或密码不正确
User user = getUser(mobileOrUserName);
// 通过PasswordManager对前端传入的密码进行解密操作获取解密后的密码以便后续和数据库中存储的密码进行比对验证
String decryptPassword = passwordManager.decryptPassword(authenticationDTO.getPassWord());
// 通过PasswordCheckManager检查密码是否正确以及是否达到密码错误次数限制半小时内密码输入错误十次已限制登录30分钟若不符合要求会抛出相应异常进行处理
passwordCheckManager.checkPassword(SysTypeEnum.ORDINARY, authenticationDTO.getUserName(), decryptPassword, user.getLoginPassword());
// 创建一个用于在Token中存储用户信息的业务对象将用户的ID、所属系统类型以及是否启用等信息设置进去用于后续生成Token以及存储相关用户标识信息
UserInfoInTokenBO userInfoInToken = new UserInfoInTokenBO();
userInfoInToken.setUserId(user.getUserId());
userInfoInToken.setSysType(SysTypeEnum.ORDINARY.value());
userInfoInToken.setEnabled(user.getStatus() == 1);
// 通过TokenStore存储用户信息并生成Token同时获取包含Token相关信息的视图对象TokenInfoVO用于返回给前端告知登录成功以及提供后续鉴权等所需的Token信息
TokenInfoVO tokenInfoVO = tokenStore.storeAndGetVo(userInfoInToken);
return ServerResponseEntity.success(tokenInfoVO);
}
/**
*
*
*
* YamiShopBindException
*
* @param mobileOrUserName
* @return User null
*/
private User getUser(String mobileOrUserName) {
User user = null;
// 通过PrincipalUtil工具类判断传入的用户名是否符合手机号格式如果是则进行下一步操作通过手机号查找用户
if (PrincipalUtil.isMobile(mobileOrUserName)) {
// 使用MyBatis Plus构建的条件查询对象LambdaQueryWrapper根据手机号在数据库中查找对应的用户信息通过UserMapper调用数据库查询方法
user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUserMobile, mobileOrUserName));
}
// 如果通过手机号没找到用户即user为null则尝试通过用户名在数据库中查找用户
if (user == null) {
user = userMapper.selectOneByUserName(mobileOrUserName);
}
// 如果最终还是没找到对应的用户,则抛出自定义的业务异常,告知账号或密码不正确
if (user == null) {
throw new YamiShopBindException("账号或密码不正确");
}
return user;
}
}

@ -0,0 +1,37 @@
package com.yami.shop.security.common.adapter;
import java.util.List;
/**
* 便
* 使
*
* @author
* @date 2022/3/25 17:31
*/
public interface AuthConfigAdapter {
/**
* URL
* "/**/ma/**" "ma" 访
*
*/
String MAYBE_AUTH_URI = "/**/ma/**";
/**
*
* 访
*
*
* @return
*/
List<String> pathPatterns();
/**
*
* 访
*
* @return
*/
List<String> excludePathPatterns();
}

@ -0,0 +1,79 @@
package com.yami.shop.security.common.adapter;
import com.anji.captcha.service.CaptchaCacheService;
import com.yami.shop.common.util.RedisUtil;
/**
* CaptchaCacheServiceRedisImplRedis
* CaptchaCacheService
* Redis
* 使Redis
*
* @author
* @date 2022/3/25 17:33
*/
public class CaptchaCacheServiceRedisImpl implements CaptchaCacheService {
/**
* Redis
* RedisUtilsetRedis
* keyvalueexpiresInSecondsRedisUtil
*
* @param key Redis
* @param value Redis
* @param expiresInSeconds RedisRedis
*/
@Override
public void set(String key, String value, long expiresInSeconds) {
RedisUtil.set(key, value, expiresInSeconds);
}
/**
* Redis
* RedisUtilhasKey
* Redistruefalse
*
* @param key Redis
* @return Redistruefalse
*/
@Override
public boolean exists(String key) {
return RedisUtil.hasKey(key);
}
/**
* Redis
* RedisUtildelRedis
* 使Redis
*
* @param key RedisRedis
*/
@Override
public void delete(String key) {
RedisUtil.del(key);
}
/**
* Redis
* RedisUtilgetRedis
* RedisUtil
*
* @param key RedisRedis
* @return RedisRedisUtil
*/
@Override
public String get(String key) {
return RedisUtil.get(key);
}
/**
* 使redis
* Redis便
*
* @return redisRedis
*/
@Override
public String type() {
return "redis";
}
}

@ -0,0 +1,53 @@
package com.yami.shop.security.common.adapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.List;
/**
* DefaultAuthConfigAdapterAuthConfigAdapter
* pathPatternsexcludePathPatterns
*
*
* @author
* @date 2022/3/25 17:33
*/
public class DefaultAuthConfigAdapter implements AuthConfigAdapter {
// 创建一个日志记录器用于记录与该类相关的日志信息方便在运行时排查问题、了解类的执行情况等这里记录器的名称为DefaultAuthConfigAdapter类的全限定名
private static final Logger logger = LoggerFactory.getLogger(DefaultAuthConfigAdapter.class);
/**
* DefaultAuthConfigAdapter
* AuthConfigAdapter使DefaultAuthConfigAdapter
* URL
*/
public DefaultAuthConfigAdapter() {
logger.info("not implement other AuthConfigAdapter, use DefaultAuthConfigAdapter... all url need auth...");
}
/**
* pathPatternsAuthConfigAdapter
* /*
*
*
* @return List<String>/*
*/
@Override
public List<String> pathPatterns() {
return Collections.singletonList("/*");
}
/**
* excludePathPatternsAuthConfigAdapter
* pathPatterns
*
*
* @return List<String>
*/
@Override
public List<String> excludePathPatterns() {
return Collections.emptyList();
}
}

@ -0,0 +1,59 @@
package com.yami.shop.security.common.adapter;
// 导入Spring框架中用于将方法标记为创建Bean的注解通过该注解定义的方法返回的对象会被Spring容器管理可在其他地方进行注入使用
import org.springframework.context.annotation.Bean;
// 导入Spring Security框架中用于配置基于Web的安全相关设置的类通过链式调用的方式可以配置如认证、授权、跨域、会话管理等多个方面的安全策略
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
// 导入Spring Security框架中用于启用Web安全功能的注解表明在当前应用中要开启Spring Security相关的Web安全配置机制
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
// 导入Spring Security框架中用于定义会话创建策略的枚举类此处配置为无状态STATELESS会话策略适用于基于Token的认证方式
import org.springframework.security.config.http.SessionCreationPolicy;
// 导入Spring Security框架中用于构建安全过滤链的核心接口配置好的安全规则最终会形成这样一个过滤链来处理进入应用的请求进行安全相关的检查和过滤操作
import org.springframework.security.web.SecurityFilterChain;
// 导入Spring框架中用于将类标记为组件的注解表明该类是一个Spring管理的组件会被自动扫描并实例化可在其他地方进行依赖注入使用
import org.springframework.stereotype.Component;
// 导入Spring框架中用于处理跨域相关的工具类此处用于判断请求是否是跨域预检请求OPTIONS请求以便对不同类型请求做不同的授权处理
import org.springframework.web.cors.CorsUtils;
/**
* MallWebSecurityConfigurerAdapterSpring SecurityWeb
* Spring Security使Spring Security
* Token
* SecurityFilterChain使
*
* @author
* @date 2022/3/25 17:33
*/
@Component
@EnableWebSecurity
public class MallWebSecurityConfigurerAdapter {
/**
* filterChain@BeanSecurityFilterChainSpring使Bean
*
*
* @param http HttpSecurityHTTP
* @return SecurityFilterChain Web
* @throws Exception HttpSecurity
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 关闭跨站请求伪造CSRF防护功能因为基于Token的认证方式下通常不需要CSRF防护Token本身就用于身份验证可避免此类安全风险
return http.csrf().disable().cors()
// 配置跨域相关设置,启用跨域支持(后续可能还需要配置更具体的跨域规则等,这里只是开启了基本的支持),
// 接着配置会话管理相关内容将会话创建策略设置为无状态STATELESS意味着服务器不会为客户端创建和维护会话状态
// 适用于基于Token的认证场景每次请求都通过携带Token来验证身份服务器不依赖会话来识别客户端
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 配置请求授权相关规则对于跨域预检请求OPTIONS请求通过CorsUtils::isPreFlightRequest判断允许所有的此类请求通过
// 因为跨域预检请求主要是浏览器在正式发起跨域请求前进行的一种询问服务器是否允许跨域的请求,通常需要放行
.and().authorizeRequests().requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
// 继续配置请求授权规则,对于所有其他请求(通过"/**"表示任意路径的请求也都允许通过这意味着在这个配置下没有进行严格的基于Spring Security自带认证授权的访问限制
// 可能后续会通过其他自定义的中间件或者业务逻辑来对请求进行更细致的权限判断等操作,此处只是简单放开了所有请求的访问权限
.and().authorizeRequests().requestMatchers(
"/**").permitAll().and().build();
}
}

@ -0,0 +1,84 @@
/*
* Copyright (c) 2018-2999 广 All rights reserved.
*
* https://www.mall4j.com/
*
*
*
*
*/
package com.yami.shop.security.common.config;
import cn.hutool.core.util.ArrayUtil;
// 导入自定义的授权配置适配器类,用于适配不同的授权配置相关逻辑,可根据具体业务需求进行扩展或定制
import com.yami.shop.security.common.adapter.AuthConfigAdapter;
// 导入默认的授权配置适配器实现类,当没有自定义的授权配置适配器时,会使用这个默认的实现类来处理相关逻辑
import com.yami.shop.security.common.adapter.DefaultAuthConfigAdapter;
// 导入自定义的授权过滤器类,用于对请求进行授权相关的过滤操作,比如验证用户权限等
import com.yami.shop.security.common.filter.AuthFilter;
import jakarta.servlet.DispatcherType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
/**
* Spring
*
*
* @author
* @date 2022/3/25 17:33
*/
@Configuration
// 启用方法级别的安全配置,允许在方法上使用诸如 @PreAuthorize、@PostAuthorize 等注解来进行权限控制
@EnableMethodSecurity
public class AuthConfig {
// 通过自动注入获取授权过滤器实例该过滤器将在后续的配置中被注册到Servlet容器中用于对请求进行过滤处理
@Autowired
private AuthFilter authFilter;
/**
* Bean
* SpringAuthConfigAdapterBean
* DefaultAuthConfigAdapter
* 便
*
* @return AuthConfigAdapter
*/
@Bean
@ConditionalOnMissingBean
public AuthConfigAdapter authConfigAdapter() {
return new DefaultAuthConfigAdapter();
}
/**
* FilterRegistrationBean
* FilterRegistrationBeanAuthFilterServlet
*
* 使 @Lazy Bean使
*
* @param authConfigAdapter
* @return FilterRegistrationBean<AuthFilter>Servlet
*/
@Bean
@Lazy
public FilterRegistrationBean<AuthFilter> filterRegistration(AuthConfigAdapter authConfigAdapter) {
FilterRegistrationBean<AuthFilter> registration = new FilterRegistrationBean<>();
// 将之前注入的授权过滤器添加到FilterRegistrationBean中这样Servlet容器就能识别并使用该过滤器来处理请求了
registration.setFilter(authFilter);
// 设置过滤路径,通过授权配置适配器获取需要过滤的路径列表,并转换为字符串数组格式。
// 这里使用ArrayUtil工具类将获取到的路径列表转换为String数组以便设置给FilterRegistrationBean
// 表示该过滤器将会对这些指定的路径下的请求进行过滤处理,此处的 /* 表示所有路径,具体路径配置通常由授权配置适配器决定
registration.addUrlPatterns(ArrayUtil.toArray(authConfigAdapter.pathPatterns(), String.class));
registration.setName("authFilter");
// 设置过滤器的优先级数值越小优先级越高这里设置为0表示较高的优先级在多个过滤器存在的情况下会先执行该过滤器
registration.setOrder(0);
registration.setDispatcherTypes(DispatcherType.REQUEST);
return registration;
}
}

@ -0,0 +1,116 @@
/*
* Copyright (c) 2018-2999 广 All rights reserved.
*
* https://www.mall4j.com/
*
*
*
*
*/
package com.yami.shop.security.common.config;
import com.anji.captcha.model.common.CaptchaTypeEnum;
import com.anji.captcha.model.common.Const;
import com.anji.captcha.service.CaptchaService;
import com.anji.captcha.service.impl.CaptchaServiceFactory;
import com.anji.captcha.util.ImageUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.util.Base64Utils;
import org.springframework.util.FileCopyUtils;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* CaptchaConfigSpring@Configuration
* Redis
* Redis
* Redis
*
* @author
* @date 2022/3/25 17:33
*/
@Configuration
public class CaptchaConfig {
/**
* captchaServiceSpringBean@Bean
* CaptchaService
* PropertiesCaptchaService
* 使CaptchaServiceFactoryCaptchaService
*
* @return CaptchaServiceSpring使
*/
@Bean
public CaptchaService captchaService() {
Properties config = new Properties();
// 设置验证码的缓存类型为Redis意味着验证码相关的数据如底图等将存储在Redis中
config.put(Const.CAPTCHA_CACHETYPE, "redis");
// 设置验证码的水印为空字符串,即不添加水印(具体根据验证码实现逻辑来定是否生效)
config.put(Const.CAPTCHA_WATER_MARK, "");
// 设置验证码的类型为滑动验证这里使用了CaptchaTypeEnum枚举中BLOCKPUZZLE对应的代码值来指定
config.put(Const.CAPTCHA_TYPE, CaptchaTypeEnum.BLOCKPUZZLE.getCodeValue());
// 设置初始化原始图片相关的标志为true具体含义取决于验证码服务内部对该参数的使用逻辑
config.put(Const.CAPTCHA_INIT_ORIGINAL, "true");
// 调用initializeBaseMap方法进行一些初始化操作可能与验证码底图相关比如预加载等
initializeBaseMap();
return CaptchaServiceFactory.getInstance(config);
}
/**
* initializeBaseMap
* ImageUtilscacheBootImage
* Map
* ImageUtils
* 便便使
*/
private static void initializeBaseMap() {
ImageUtils.cacheBootImage(getResourcesImagesFile("classpath:captcha" + "/original/*.png"),
getResourcesImagesFile("classpath:captcha" + "/slidingBlock/*.png"),
Collections.emptyMap());
}
/**
* getResourcesImagesFileBase64Map
* MapBase64便使
*
*
* @param path "classpath:captcha" + "/original/*.png"png
* @return MapBase64
*/
public static Map<String, String> getResourcesImagesFile(String path) {
Map<String, String> imgMap = new HashMap<>(16);
// 创建一个PathMatchingResourcePatternResolver实例用于解析类路径下的资源模式能够匹配符合指定模式的多个资源文件
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
try {
// 根据传入的路径表达式获取对应的资源数组,即匹配该模式的所有图片资源文件
Resource[] resources = resolver.getResources(path);
// 遍历获取到的所有资源文件
Resource[] var4 = resources;
int var5 = resources.length;
for (int var6 = 0; var6 < var5; ++var6) {
Resource resource = var4[var6];
// 将资源文件的内容读取为字节数组以便后续进行Base64编码操作
byte[] bytes = FileCopyUtils.copyToByteArray(resource.getInputStream());
// 将字节数组转换为Base64编码的字符串使得图片数据可以以文本形式方便地在内存中存储和传输等
String string = Base64Utils.encodeToString(bytes);
// 获取资源文件的文件名用于作为Map中的键来标识对应的Base64编码后的图片数据
String filename = resource.getFilename();
imgMap.put(filename, string);
}
} catch (Exception var11) {
// 如果在获取资源文件或处理过程中出现异常,打印异常堆栈信息,方便排查问题
var11.printStackTrace();
}
return imgMap;
}
}

@ -0,0 +1,61 @@
package com.yami.shop.security.common.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
/**
* CorsConfigSpring@Configuration
* CORS便
* 使访
*
* @author yami
*/
@Configuration
public class CorsConfig {
/**
* corsConfigurationSourceSpringBean@Bean
* CorsConfigurationSourceSpring WebCORS
* origins访HTTP
*
*
* - *访
* 便访
* - addAllowedOrigin访
* configuration.addAllowedOrigin("http://localhost:8080");
* configuration.addAllowedOrigin("http://192.168.1.6:8080");
*
* @return CorsConfigurationSourceSpring Web使
*/
@Bean
public CorsConfigurationSource corsConfigurationSource() {
// 创建一个CorsConfiguration对象用于配置跨域相关的各种规则
CorsConfiguration configuration = new CorsConfiguration();
// 使用addAllowedOriginPattern方法添加允许跨域访问的来源模式这里使用通配符*)表示允许所有来源访问,
// 但需注意在生产环境应按实际情况修改为具体域名,可参考上面的注释说明。
configuration.addAllowedOriginPattern("*");
// 修改为添加add而不是设置set允许的HTTP方法使用通配符*表示允许所有的HTTP方法如GET、POST、PUT等
// 这样前端可以使用各种类型的HTTP请求来访问后端接口。
configuration.addAllowedMethod("*");
// 这里配置允许的请求头,同样使用通配符(*)表示允许所有的请求头信息,
// 特别重要的是起码需要允许 Access-Control-Allow-Origin这个请求头它与跨域访问的合法性验证密切相关。
configuration.addAllowedHeader("*");
// 设置是否允许发送Cookie等凭证信息设置为true表示允许
// 若前后端交互需要携带用户认证等相关凭证如Cookie中的登录信息则需要开启此项。
configuration.setAllowCredentials(true);
// 设置预检请求OPTIONS请求的缓存时间单位为秒这里设置为一天3600 * 24秒
// 在缓存有效期内,对于相同来源、方法和请求头的请求,浏览器不会再次发送预检请求,提高性能。
configuration.setMaxAge(3600 * 24L);
// 创建一个基于URL的CorsConfigurationSource对象它可以根据不同的URL路径应用不同的CorsConfiguration配置。
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// 将上面配置好的CorsConfiguration应用到所有路径/**表示匹配所有的请求路径),
// 意味着所有的接口请求都会按照这个配置来处理跨域相关的规则。
source.registerCorsConfiguration("/**", configuration);
return source;
}
}

@ -0,0 +1,79 @@
/*
* Copyright (c) 2018-2999 广 All rights reserved.
*
* https://www.mall4j.com/
*
*
*
*
*/
package com.yami.shop.security.common.controller;
import com.anji.captcha.model.common.RepCodeEnum;
import com.anji.captcha.model.common.ResponseModel;
import com.anji.captcha.model.vo.CaptchaVO;
import com.anji.captcha.service.CaptchaService;
import io.swagger.v3.oas.annotations.tags.Tag;
import com.yami.shop.common.response.ServerResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* CaptchaControllerSpring RESTful@RestController
* HTTP
* "/captcha"@RequestMappingSwagger"验证码"@Tag便
*
* @author
* @date 2022/3/25 17:33
*/
@RestController
@RequestMapping("/captcha")
@Tag(name = "验证码")
public class CaptchaController {
// 通过构造函数注入的方式引入CaptchaService用于后续调用验证码相关的业务逻辑方法比如生成验证码、验证验证码有效性等操作
private final CaptchaService captchaService;
public CaptchaController(CaptchaService captchaService) {
this.captchaService = captchaService;
}
/**
* getHTTP POST
* CaptchaVO@RequestBodyCaptchaVO
* captchaServicegetServerResponseEntity
*
*
* @param captchaVO
* @return ServerResponseEntityResponseModel
*/
@PostMapping({ "/get" })
public ServerResponseEntity<ResponseModel> get(@RequestBody CaptchaVO captchaVO) {
return ServerResponseEntity.success(captchaService.get(captchaVO));
}
/**
* checkHTTP POST
* CaptchaVOCaptchaVO
* captchaServicecheckServerResponseEntity
*
* ResponseModel.errorMsgResponseModel
* ServerResponseEntity
*
* @param captchaVO
* @return ServerResponseEntityResponseModel
*/
@PostMapping({ "/check" })
public ServerResponseEntity<ResponseModel> check(@RequestBody CaptchaVO captchaVO) {
ResponseModel responseModel;
try {
responseModel = captchaService.check(captchaVO);
} catch (Exception e) {
return ServerResponseEntity.success(ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_COORDINATE_ERROR));
}
return ServerResponseEntity.success(responseModel);
}
}

@ -0,0 +1,71 @@
/*
* Copyright (c) 2018-2999 广 All rights reserved.
*
* https://www.mall4j.com/
*
*
*
*
*/
package com.yami.shop.security.common.controller;
// 导入Hutool工具库中用于字符串操作的工具类此处用于判断字符串是否为空空白字符串方便进行逻辑判断
import cn.hutool.core.util.StrUtil;
// 导入项目自定义的用于封装服务器响应信息的实体类包含响应状态码、消息、数据等内容用于统一向客户端返回响应结果此处响应数据为Void表示无具体数据返回
import com.yami.shop.common.response.ServerResponseEntity;
// 导入与安全相关的用于管理Token存储和操作的类负责对Token进行删除等相关管理操作此处用于删除用户的登录Token
import com.yami.shop.security.common.manager.TokenStore;
// 导入Swagger相关的注解用于生成API文档@Operation注解用于描述接口的功能摘要和详细信息方便前端开发人员等查看接口的作用和使用方式
import io.swagger.v3.oas.annotations.Operation;
// 导入Swagger相关的注解用于给接口分类添加标签方便文档中进行分组展示和说明此处标记为"注销"类别
import io.swagger.v3.oas.annotations.tags.Tag;
// 导入Spring框架用于实现依赖注入的注解表明后续的属性需要由Spring容器进行注入实例化此处用于注入TokenStore实例
import org.springframework.beans.factory.annotation.Autowired;
// 导入Spring框架用于定义处理POST请求的注解将下面的方法映射为一个接收POST请求的接口处理对应的业务逻辑即处理用户退出登录的请求
import org.springframework.web.bind.annotation.PostMapping;
// 导入Spring框架用于将类标记为RESTful风格的控制器的注解表明该类中的方法主要用于处理HTTP请求并返回JSON等格式的响应数据
import org.springframework.web.bind.annotation.RestController;
// 导入Servlet相关的用于操作HTTP请求的类用于获取请求头中的信息此处主要用于获取包含Token的"Authorization"请求头内容
import jakarta.servlet.http.HttpServletRequest;
/**
* LogoutControllerSpring RESTful退
* 退TokenTokenTokenStoreToken
* 使SwaggerAPI便使
*
* @author
* @date 2022/3/25 17:33
*/
@RestController
@Tag(name = "注销")
public class LogoutController {
// 通过Spring的依赖注入机制注入一个TokenStore实例用于管理Token的删除等相关操作以实现清除用户登录Token的功能
@Autowired
private TokenStore tokenStore;
/**
* 退HTTPHttpServletRequest
* "Authorization"TokenToken
* 退
* TokenTokenStoreToken退
*
* @param request 退HttpServletRequestToken
* @return ServerResponseEntity<Void> Void退
*/
@PostMapping("/logOut")
@Operation(summary = "退出登陆", description = "点击退出登陆清除token清除菜单缓存")
public ServerResponseEntity<Void> logOut(HttpServletRequest request) {
// 从请求头中获取名为"Authorization"的Token信息通常在登录成功后客户端会在后续请求的这个请求头中携带Token用于身份验证等操作
String accessToken = request.getHeader("Authorization");
// 通过Hutool工具类判断获取到的Token信息是否为空空白字符串如果为空则直接返回成功的服务器响应实体表示可视为已退出登录可能本身就没登录
if (StrUtil.isBlank(accessToken)) {
return ServerResponseEntity.success();
}
// 如果获取到了有效的Token信息则调用注入的TokenStore实例的方法删除该用户在当前系统对应的Token实现清除登录状态的功能
tokenStore.deleteCurrentToken(accessToken);
return ServerResponseEntity.success();
}
}

@ -0,0 +1,47 @@
/*
* Copyright (c) 2018-2999 广 All rights reserved.
*
* https://www.mall4j.com/
*
*
*
*
*/
package com.yami.shop.security.common.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
/**
* DTO
* 便
*
* @author
* @date 2022/3/25 17:33
*/
@Data
public class AuthenticationDTO {
/**
*
*
* @NotBlank "userName不能为空"
* 使 @Schema API Swagger API //
*/
@NotBlank(message = "userName不能为空")
@Schema(description = "用户名/邮箱/手机号", required = true)
protected String userName;
/**
*
*
* @NotBlank "passWord不能为空"
* @Schema API
*/
@NotBlank(message = "passWord不能为空")
@Schema(description = "一般用作密码", required = true)
protected String passWord;
}

@ -0,0 +1,163 @@
/*
* Copyright (c) 2018-2999 广 All rights reserved.
*
* https://www.mall4j.com/
*
*
*
*
*/
package com.yami.shop.security.common.filter;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.yami.shop.common.exception.YamiShopBindException;
import com.yami.shop.common.handler.HttpHandler;
import com.yami.shop.common.response.ResponseEnum;
import com.yami.shop.common.response.ServerResponseEntity;
import com.yami.shop.security.common.adapter.AuthConfigAdapter;
import com.yami.shop.security.common.bo.UserInfoInTokenBO;
import com.yami.shop.security.common.manager.TokenStore;
import com.yami.shop.security.common.util.AuthUserContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
/**
*
* AuthConfigAdapter访
* 访线
*
* @author
* @date 2022/3/25 17:33
*/
@Component
public class AuthFilter implements Filter {
// 用于记录日志,方便在运行过程中输出相关的调试、错误等信息,便于排查问题
private static final Logger logger = LoggerFactory.getLogger(AuthFilter.class);
// 注入AuthConfigAdapter接口的实现类通过它可以获取到配置的需要授权和不需要授权的路径信息
@Autowired
private AuthConfigAdapter authConfigAdapter;
// 注入HttpHandler用于处理向Web端输出响应相关的操作比如将服务器响应信息正确地返回给前端页面
@Autowired
private HttpHandler httpHandler;
// 注入TokenStore主要用于管理和操作与令牌Token相关的存储及查询等功能例如从存储中获取用户信息等
@Autowired
private TokenStore tokenStore;
// 通过配置文件注入令牌名称,用于后续从请求头中获取对应的令牌信息
@Value("${sa-token.token-name}")
private String tokenName;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
/**
*
*
* @param request ServletHttpServletRequestHTTP
* @param response ServletHttpServletResponse
* @param chain ServletJSP
* @throws IOException
* @throws ServletException Servlet
*/
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 将ServletRequest转换为HttpServletRequest方便获取HTTP请求相关的属性和信息比如请求路径、请求头信息等
HttpServletRequest req = (HttpServletRequest) request;
// 将ServletResponse转换为HttpServletResponse便于后续进行HTTP响应相关的操作比如设置响应状态码、响应头、输出响应内容等
HttpServletResponse resp = (HttpServletResponse) response;
// 获取当前请求的URI统一资源标识符用于后续与配置的授权路径进行匹配判断
String requestUri = req.getRequestURI();
// 通过AuthConfigAdapter获取不需要授权的路径列表用于判断当前请求是否命中这些无需授权的路径
List<String> excludePathPatterns = authConfigAdapter.excludePathPatterns();
// 创建AntPathMatcher对象用于进行路径的匹配操作它支持通配符等方式来灵活匹配路径
AntPathMatcher pathMatcher = new AntPathMatcher();
// 如果不需要授权的路径列表不为空就遍历这些路径检查当前请求的URI是否匹配其中某个无需授权的路径模式
if (CollectionUtil.isNotEmpty(excludePathPatterns)) {
for (String excludePathPattern : excludePathPatterns) {
// 使用AntPathMatcher进行路径匹配如果匹配成功说明当前请求是不需要授权的直接将请求传递给下一个过滤器或目标资源
if (pathMatcher.match(excludePathPattern, requestUri)) {
chain.doFilter(req, resp);
return;
}
}
}
// 从请求头中获取名为tokenName的令牌信息这个令牌通常用于验证用户的登录状态和权限等
String accessToken = req.getHeader(tokenName);
// 判断当前请求的URI是否匹配可能需要登录但不登录也能用的路径模式通过AuthConfigAdapter中定义的常量路径进行匹配
boolean mayAuth = pathMatcher.match(AuthConfigAdapter.MAYBE_AUTH_URI, requestUri);
// 用于存储从令牌中解析出来的用户信息,如果后续成功获取到用户信息则会赋值
UserInfoInTokenBO userInfoInToken = null;
try {
// 如果获取到的令牌信息不为空(即存在令牌),说明用户可能已经登录,需要进一步验证并获取用户信息
if (StrUtil.isNotBlank(accessToken)) {
// 使用StpUtil工具类来校验用户是否已经登录若登录验证失败会抛出异常在这里捕获异常并进行相应处理
try {
StpUtil.checkLogin();
} catch (Exception e) {
// 如果登录校验失败通过HttpHandler将表示未授权的服务器响应信息输出到Web端通常是返回给前端页面显示相应的错误提示
httpHandler.printServerResponseToWeb(ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED));
return;
}
// 如果登录校验通过从TokenStore中根据令牌获取对应的用户信息从缓存等存储中查询并解析用户信息
userInfoInToken = tokenStore.getUserInfoByAccessToken(accessToken, true);
} else if (!mayAuth) {
// 如果没有令牌且当前请求也不属于可能不需要登录就能访问的路径,那么说明该请求需要授权但未提供有效令牌,返回表示未授权的响应给前端
httpHandler.printServerResponseToWeb(ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED));
return;
}
// 将获取到的用户信息保存到AuthUserContext中方便在后续的请求处理过程中比如在其他地方需要获取当前登录用户信息时可以直接获取
AuthUserContext.set(userInfoInToken);
// 如果前面的授权校验等操作都通过了将请求传递给下一个过滤器或者最终的目标资源如业务处理的Servlet等继续处理
chain.doFilter(req, resp);
} catch (Exception e) {
// 手动捕获非controller层抛出的异常进行统一处理
if (e instanceof YamiShopBindException) {
// 如果是YamiShopBindException类型的异常通过HttpHandler将该异常对应的响应信息输出到Web端
httpHandler.printServerResponseToWeb((YamiShopBindException) e);
} else {
// 如果是其他类型的异常,直接抛出,让上层的异常处理机制(比如容器的异常处理)去进一步处理
throw e;
}
} finally {
// 无论请求处理过程是否出现异常最终都要清理AuthUserContext中的用户信息避免数据残留影响下一次请求处理
AuthUserContext.clean();
}
}
@Override
public void destroy() {
Filter.super.destroy();
}
}

@ -0,0 +1,52 @@
/*
* Copyright (c) 2018-2999 广 All rights reserved.
*
* https://www.mall4j.com/
*
*
*
*
*/
package com.yami.shop.security.common.manager;
import cn.hutool.crypto.symmetric.AES;
import com.yami.shop.common.exception.YamiShopBindException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
/**
* @author
* @date 2022/1/19 16:02
*/
@Component
public class PasswordManager {
private static final Logger logger = LoggerFactory.getLogger(PasswordManager.class);
/**
* aeskey16
*/
@Value("${auth.password.signKey:-mall4j-password}")
public String passwordSignKey;
public String decryptPassword(String data) {
// 在使用oracle的JDK时JAR包必须签署特殊的证书才能使用。
// 解决方案 1.使用openJDK或者非oracle的JDK建议 2.添加证书
// hutool的aes报错可以打开下面那段代码
// SecureUtil.disableBouncyCastle();
AES aes = new AES(passwordSignKey.getBytes(StandardCharsets.UTF_8));
String decryptStr;
String decryptPassword;
try {
decryptStr = aes.decryptStr(data);
decryptPassword = decryptStr.substring(13);
} catch (Exception e) {
logger.error("Exception:", e);
throw new YamiShopBindException("AES解密错误", e);
}
return decryptPassword;
}
}

@ -0,0 +1,63 @@
/*
* Copyright (c) 2018-2999 广 All rights reserved.
*
* https://www.mall4j.com/
*
*
*
*
*/
package com.yami.shop.security.common.util;
import com.alibaba.ttl.TransmittableThreadLocal;
import com.yami.shop.security.common.bo.UserInfoInTokenBO;
/**
* 线
* `TransmittableThreadLocal` 线便
*
*
* @author FrozenWatermelon
* @date 2020/7/16
*/
public class AuthUserContext {
// 使用 `TransmittableThreadLocal` 来创建一个线程本地变量,用于存储 `UserInfoInTokenBO` 类型的用户信息对象。
// `TransmittableThreadLocal` 相较于普通的 `ThreadLocal` 具有能在线程间传递数据的优势,
// 适合在多线程环境下(比如异步调用、线程池场景等)保证用户信息的正确传递和共享。
private static final ThreadLocal<UserInfoInTokenBO> USER_INFO_IN_TOKEN_HOLDER = new TransmittableThreadLocal<>();
/**
* 线
* `USER_INFO_IN_TOKEN_HOLDER` `get` `UserInfoInTokenBO`
* 线 `null`
*
* @return 线 `UserInfoInTokenBO` `null`
*/
public static UserInfoInTokenBO get() {
return USER_INFO_IN_TOKEN_HOLDER.get();
}
/**
* 线线
* `USER_INFO_IN_TOKEN_HOLDER` `set` `UserInfoInTokenBO`
* 线 `get`
*
* @param userInfoInTokenBo `UserInfoInTokenBO`
*/
public static void set(UserInfoInTokenBO userInfoInTokenBo) {
USER_INFO_IN_TOKEN_HOLDER.set(userInfoInTokenBo);
}
/**
* 线线
* 线 `USER_INFO_IN_TOKEN_HOLDER.get()` `null`
* `null` `USER_INFO_IN_TOKEN_HOLDER` `remove`
* 线
*/
public static void clean() {
if (USER_INFO_IN_TOKEN_HOLDER.get()!= null) {
USER_INFO_IN_TOKEN_HOLDER.remove();
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Loading…
Cancel
Save