Commit f6a04991 authored by zhouxudong's avatar zhouxudong

提交代码

parent 09461a47
Pipeline #105 failed with stages
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.15</version>
</parent>
<groupId>com.postcard</groupId>
<artifactId>postcard-service</artifactId>
<version>1.0-SNAPSHOT</version>
<description>舒城明信片服务</description>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--mysql连接驱动-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- mybatis增强器依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<!-- hutool工具包 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.20</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
<!--常用工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- jwt-->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.23</version>
</dependency>
<!--启用Spring Boot的配置处理器。-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!--swagger 增强-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15to18</artifactId>
<version>1.68</version>
</dependency>
<!-- ## 类型转换-->
<dependency>
<groupId>ma.glasnost.orika</groupId>
<artifactId>orika-core</artifactId>
<version>1.5.4</version>
</dependency>
</dependencies>
</project>
package com.postcard.service;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @Author:zhouxudong
* @version: 1.0
* @Date: 2023/11/17 15:41
* @Description:
*/
@SpringBootApplication
/*@EnableOpenApi
@EnableSwagger2*/
public class PostcardApplication {
public static void main(String[] args) {
SpringApplication.run(PostcardApplication.class, args);
}
}
package com.postcard.service.config.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Author:zhouxudong
* @version: 1.0
* @Date: 2023/11/17 16:45
* @Description: 自定义注解,添加注解后不包装返回对象
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface ResponseAnnotation {
}
package com.postcard.service.config.auth;
import com.postcard.service.domain.JwtInfo;
/**
* @author: zhouxudong
* @version: 1.0
* @createTime: 2023/11/17 17:16
* @description: 当前登录人信息
*/
public class BaseContextHandler {
private static final ThreadLocal<JwtInfo> currentUserInfoThreadLocal = new ThreadLocal<>();
public static void setCurrentUserInfo(JwtInfo currentUserInfo) throws Exception {
if (currentUserInfo != null) {
try {
currentUserInfoThreadLocal.set(currentUserInfo);
} catch (Exception e) {
throw new Exception("失效");
}
}
}
public static JwtInfo getCurrentUserInfo() {
JwtInfo currentUserInfo = currentUserInfoThreadLocal.get();
if (currentUserInfo == null) {
currentUserInfo = new JwtInfo();
currentUserInfoThreadLocal.set(currentUserInfo);
}
return currentUserInfo;
}
public static void removeCurrentUserInfo() {
currentUserInfoThreadLocal.remove();
}
}
package com.postcard.service.config.auth;
import com.postcard.service.config.exception.AuthException;
import com.postcard.service.constant.Constants;
import com.postcard.service.domain.JwtInfo;
import com.postcard.service.util.jwt.JwtTokenUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.AsyncHandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/** 服务拦截器,根据配置的请求路径,进行请求的拦截,执行preHandle方法 */
@Configuration
public class CurrentUserInterceptor implements AsyncHandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// 从header中获取权限请求头
String authToken = request.getHeader(Constants.TOKEN_HEADER);
if (StringUtils.isBlank(authToken)) {
throw new AuthException("未授权,请重新登录");
}
String token = authToken.substring("Bearer".length() + 1).trim();
JwtInfo jwtUser = JwtTokenUtil.getJwtUser(token);
// 根据token信息,生成当前登录用户信息的设置,存放容器threadlocal
BaseContextHandler.setCurrentUserInfo(jwtUser);
return true;
}
@Override
public void afterCompletion(
HttpServletRequest request, HttpServletResponse response, Object handler, Exception e) {
BaseContextHandler.removeCurrentUserInfo();
}
}
package com.postcard.service.config.auth;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/** Web层配置类 */
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Autowired private CurrentUserInterceptor currentUserInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 添加需要拦截的路径,以及处理拦截的拦截器
registry
// 拦截器
.addInterceptor(currentUserInterceptor)
// 拦截路径
.addPathPatterns(getIncludePathPatterns())
// 白名单
.excludePathPatterns(getExcludePathPatterns());
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
registry
.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
}
// Spring Boot使用UTF-8编码来对字符串进行处理
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new StringHttpMessageConverter(StandardCharsets.UTF_8));
}
/** 需要拦截的信息 */
private ArrayList<String> getIncludePathPatterns() {
ArrayList<String> list = new ArrayList<>();
String[] urls = {"/**"};
Collections.addAll(list, urls);
return list;
}
private String[] swagger() {
return new String[] {
"/swagger-resources/**",
"/swagger-ui.html",
"/webjars/**",
"/v3/**",
"/v2/**",
"/error",
"/swagger**/**",
"/swagger-ui/index.html",
"/swagger-ui/index.html/**",
"/doc.html",
"doc.html/**",
"/doc.html#/**",
"/**/login",
"/favicon.ico",
"/favicon.ico/**",
"/swagger-ui.html/**"
};
}
/** 不需要任何用户信息 白名单 */
private ArrayList<String> getExcludePathPatterns() {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, swagger());
return list;
}
}
package com.postcard.service.config.exception;
/**
* @description: 授权异常
* @date: 2023/11/17 16:31
* @param:
* @return:
**/
public class AuthException extends BaseException {
public AuthException(String message) {
super(message);
}
}
package com.postcard.service.config.exception;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* @description:
* @date: 2023/11/17 16:30
* @param:
* @return:
**/
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class BaseException extends RuntimeException {
private Integer code;
private String message;
public BaseException(String message){
super(message);
this.message = message;
}
}
package com.postcard.service.config.exception;
import com.postcard.service.domain.AjaxResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ValidationException;
import java.util.stream.Collectors;
/**
* @author: zhouxudong
* @version: 1.0
* @createTime: 2023/11/17 16:32
* @description: 统一异常处理
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(value = BaseException.class)
public AjaxResult handlerException(BaseException e){
log.error("发生异常BaseException:",e);
return AjaxResult.error(e.getMessage());
}
@ExceptionHandler(value = ValidationException.class)
public AjaxResult handlerException(ValidationException e){
log.error("发生参数校验异常Exception:",e);
return AjaxResult.error(e.getMessage());
}
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public AjaxResult handlerException(MethodArgumentNotValidException e){
log.error("发生参数校验异常:",e);
String error = e.getBindingResult().getAllErrors().stream().map(ObjectError::getDefaultMessage).distinct().collect(Collectors.joining(";"));
return AjaxResult.error(error);
// return AjaxResult.error(e.getMessage());
}
@ExceptionHandler(value = AuthException.class)
public AjaxResult authException(AuthException e){
log.error("发生权限异常:",e);
return AjaxResult.auth(e.getMessage());
}
@ExceptionHandler(value = ServiceException.class)
public AjaxResult serviceException(ServiceException e){
log.error("发生自定义异常Exception:",e);
return AjaxResult.error(e.getMessage());
}
@ExceptionHandler(value = Exception.class)
public AjaxResult handlerException(Exception e){
log.error("发生异常Exception:",e);
return AjaxResult.error(e.getMessage());
}
}
package com.postcard.service.config.exception;
import cn.hutool.json.JSONUtil;
import com.postcard.service.config.annotation.ResponseAnnotation;
import com.postcard.service.domain.AjaxResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
/**
* @author: zhouxudong
* @version: 1.0
* @createTime: 2023/12/1 16:56
* @description:
*/
@RestControllerAdvice(basePackages = "com.lyy.user.moudle")
@Slf4j
public class GlobalResponseBodyAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter methodParameter, Class converterType) {
boolean isIntercept = true;
Method method = methodParameter.getMethod();
AnnotatedElement annotatedElement = methodParameter.getAnnotatedElement();
ResponseAnnotation annotation =
AnnotationUtils.findAnnotation(annotatedElement, ResponseAnnotation.class);
if (!ObjectUtils.isEmpty(annotation)) {
isIntercept = false;
}
// methodParameter.getMethodAnnotations();
// if(AntPathMatcher)
return isIntercept;
}
@Override
public Object beforeBodyWrite(
Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
AjaxResult value = AjaxResult.success(body);
boolean isStringResult = returnType.getParameterType().equals(String.class);
if (isStringResult) {
return JSONUtil.toJsonStr(value);
}
return value;
}
}
package com.postcard.service.config.exception;
/**
* @description: 业务异常
* @date: 2023/11/17 16:31
* @param:
* @return:
**/
public class ServiceException extends BaseException {
public ServiceException(String message) {
super(message);
}
}
package com.postcard.service.config.filter;
import cn.hutool.json.JSONUtil;
import com.postcard.service.config.weixin.WeixinRequestUtils;
import com.postcard.service.domain.AjaxResult;
import com.postcard.service.domain.JwtInfo;
import com.postcard.service.moudle.user.entity.UserAppletEntity;
import com.postcard.service.moudle.user.service.UserService;
import com.postcard.service.util.jwt.JwtTokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;
/**
* @Author:zhouxudong
*
* @version: 1.0 @Date: 2023/12/14 18:35 @Description: 微信登录授权过滤器
*/
@Component
@Slf4j
public class WxLoginFilter implements Filter {
@Autowired private WeixinRequestUtils weixinRequestUtils;
@Autowired private UserService userService;
@Override
public void doFilter(
ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String requestURI = request.getRequestURI();
if (!requestURI.contains("/user/login")) {
filterChain.doFilter(request, response);
return;
}
// 获取小程序传递的code码
String code = request.getParameter("code");
if (StringUtils.isBlank(code)) {
this.loginFailHandler(response, "code不能为空");
}
Map userIdetify = this.weixinRequestUtils.getUserIdetify(code);
log.info("微信登录授权结果:{}", userIdetify);
if (ObjectUtils.isNotEmpty(userIdetify)) {
String openid = String.valueOf(userIdetify.get("openid"));
if (StringUtils.isNotBlank(openid)) {
JwtInfo jwtInfo = new JwtInfo();
jwtInfo.setOpenId(openid);
jwtInfo.setSessionKey(
ObjectUtils.isEmpty(userIdetify.get("session_key"))
? null
: userIdetify.get("session_key").toString());
UserAppletEntity userApplet = userService.getUserInfo(openid);
jwtInfo.setUserId(userApplet.getId());
jwtInfo.setNickName(userApplet.getNickName());
jwtInfo.setPhone(userApplet.getPhone());
log.info("jwt信息:{}",jwtInfo);
String token;
try {
token = JwtTokenUtil.createToken(jwtInfo, null);
log.info("创建token:{}",token);
this.loginSuccessHandler(response, token);
return;
} catch (Exception e) {
this.loginFailHandler(response, "获取用户openid失败");
}
}
}
this.loginFailHandler(response, "获取用户openid失败");
}
private void loginSuccessHandler(HttpServletResponse response, Object data) throws IOException {
response.setStatus(HttpServletResponse.SC_OK);
AjaxResult result = new AjaxResult(HttpServletResponse.SC_OK, null, data);
this.printWriter(response, result);
}
private void loginFailHandler(HttpServletResponse response, String exception) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
AjaxResult result = new AjaxResult(HttpServletResponse.SC_UNAUTHORIZED, exception, null);
this.printWriter(response, result);
}
private void printWriter(HttpServletResponse response, AjaxResult ajaxResult) throws IOException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write(JSONUtil.toJsonStr(ajaxResult));
out.flush();
out.close();
}
}
package com.postcard.service.config.mybatis;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableTransactionManagement
@Slf4j
public class MyBatisPlusConfig {
/**
* @description:
* @date: 2023/11/17 16:22
* @param: []
* @return: com.baomidou.mybatisplus.extension.plugins.OptimisticLockerInterceptor
**/
@Bean
public MybatisPlusInterceptor optimisticLockerInterceptor() {
//1 创建MybatisPlusInterceptor拦截器对象
MybatisPlusInterceptor mpInterceptor=new MybatisPlusInterceptor();
//2 乐观锁
mpInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
//3.分页
mpInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mpInterceptor;
}
}
package com.postcard.service.config.mybatis;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.postcard.service.config.auth.BaseContextHandler;
import com.postcard.service.domain.JwtInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Arrays;
/**
* @author: zhouxudong
* @version: 1.0
* @createTime: 2023/11/17 16:46
* @description:
*/
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
private final String[] createNeedToFill = {
"createBy", "createTime", "updateBy", "updateTime", "version"
};
private final String[] updateNeedToFill = {"updateBy", "updateTime"};
private final Class[] ignoreAutoFillEntity = {};
@Override
public void insertFill(MetaObject metaObject) {
if (!ignoreAutoFill(metaObject)) {
autofill(metaObject, createNeedToFill);
}
}
@Override
public void updateFill(MetaObject metaObject) {
if (!ignoreAutoFill(metaObject)) {
autofill(metaObject, updateNeedToFill);
}
}
private void autofill(MetaObject metaObject, String[] arrayNeedToFill) {
JwtInfo userInfo = BaseContextHandler.getCurrentUserInfo();
Arrays.stream(arrayNeedToFill)
.forEach(
property -> {
if (metaObject.hasSetter(property)) {
if (property.endsWith("By")) {
this.setFieldValByName(property, userInfo.getUserName(), metaObject);
}
if (property.endsWith("Time")) {
this.setFieldValByName(property, LocalDateTime.now(), metaObject);
}
if (property.equals("version")) {
this.setFieldValByName(property, 1, metaObject);
}
}
});
}
private Boolean ignoreAutoFill(MetaObject metaObject) {
return Arrays.stream(ignoreAutoFillEntity)
.anyMatch(entity -> metaObject.getOriginalObject().getClass() == entity);
}
}
package com.postcard.service.config.other;
import lombok.extern.slf4j.Slf4j;
import ma.glasnost.orika.CustomConverter;
import ma.glasnost.orika.MappingContext;
import ma.glasnost.orika.metadata.Type;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
@Configuration
@Slf4j
@Order(30)
public class CustomerConvertConfig {
private static final DateTimeFormatter df_datetime =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter df_date = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final DateTimeFormatter df_time = DateTimeFormatter.ofPattern("HH:mm:ss");
/**
* String -> LocalDateTime 转换器
*
* @return
*/
@Bean("localDateTimeConvert")
public CustomConverter localDateTimeConvert() {
return new CustomConverter<String, LocalDateTime>() {
@Override
public boolean canConvert(Type sourceType, Type destinationType) {
return ((String.class).isAssignableFrom(sourceType.getClass()))
&& ((LocalDateTime.class).isAssignableFrom(destinationType.getClass()));
}
@Override
public LocalDateTime convert(
String o, Type<? extends LocalDateTime> type, MappingContext mappingContext) {
return LocalDateTime.parse(o, df_datetime);
}
};
}
/** String -> LocalDate 转换器 */
@Bean("localDateConvert")
public CustomConverter localDateConvert() {
return new CustomConverter<String, LocalDate>() {
@Override
public boolean canConvert(Type sourceType, Type destinationType) {
return ((String.class).isAssignableFrom(sourceType.getClass()))
&& ((LocalDate.class).isAssignableFrom(destinationType.getClass()));
}
@Override
public LocalDate convert(
String o, Type<? extends LocalDate> type, MappingContext mappingContext) {
return LocalDate.parse(o, df_date);
}
};
}
/**
* String -> LocalTime 转换器
*
* @return
*/
@Bean("localTimeConvert")
public CustomConverter localTimeConvert() {
return new CustomConverter<String, LocalTime>() {
@Override
public boolean canConvert(Type sourceType, Type destinationType) {
return ((String.class).isAssignableFrom(sourceType.getClass()))
&& ((LocalTime.class).isAssignableFrom(destinationType.getClass()));
}
@Override
public LocalTime convert(
String o, Type<? extends LocalTime> type, MappingContext mappingContext) {
return LocalTime.parse(o, df_time);
}
};
}
}
package com.postcard.service.config.other;
import lombok.extern.slf4j.Slf4j;
import ma.glasnost.orika.CustomConverter;
import ma.glasnost.orika.MapperFactory;
import ma.glasnost.orika.converter.ConverterFactory;
import ma.glasnost.orika.impl.DefaultMapperFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
/**
* @author: zhouxudong
* @version: 1.0
* @createTime: 2023/11/17 16:53
* @description: 类型转换
*/
@Configuration
@Slf4j
@Order(31)
public class OrikaConfig {
@Autowired
@Qualifier("localDateTimeConvert")
private CustomConverter localDateTimeConvert;
@Autowired
@Qualifier("localDateConvert")
private CustomConverter localDateConvert;
@Autowired
@Qualifier("localTimeConvert")
private CustomConverter localTimeConvert;
@Bean
public MapperFactory mapperFactory() {
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
// 注册自定义转换器
ConverterFactory converterFactory = mapperFactory.getConverterFactory();
converterFactory.registerConverter(localDateTimeConvert);
converterFactory.registerConverter(localDateConvert);
converterFactory.registerConverter(localTimeConvert);
return mapperFactory;
}
}
package com.postcard.service.config.redis;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
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配置
*
* @author garden
*/
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings(value = {"unchecked", "rawtypes"})
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
template.setKeySerializer(stringRedisSerializer); // key的序列化类型
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer =
new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
template.setValueSerializer(jackson2JsonRedisSerializer); // value的序列化类型
template.setHashKeySerializer(stringRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
package com.postcard.service.config.swagger;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
/**
* @Author:zhouxudong
*
* @version: 1.0 @Date: 2023/11/22 18:31 @Description:
*/
@Slf4j
@Configuration
public class SwaggerConfig {
@Bean // 相当于Spring 配置中的<bean>
public Docket createRestApi() {
return new Docket(DocumentationType.OAS_30)
// 是否启用Swagger
// 分组设置
.groupName("区域大纲基本接口")
// 用来创建该API的基本信息,展示在文档的页面中(自定义展示的信息)
.apiInfo(apiInfo())
// 设置哪些接口暴露给Swagger展示
.select()
// 扫描所有有注解的api,用这种方式更灵活
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
// 用ApiInfoBuilder进行定制
return new ApiInfoBuilder()
// 设置标题
.title("标题:明信片模块_接口文档")
// 描述
.description("描述:明信片模块_接口文档")
// 作者信息
.contact(new Contact("postcard", null, null))
// 版本
.version("版本号:v1.0")
.build();
}
}
package com.postcard.service.config.webflux;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
@Slf4j
public class WebClientConfig {
@Bean
public WebClient webClient() {
return WebClient.builder().build();
}
}
package com.postcard.service.config.weixin;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.Map;
@Component
@Slf4j
public class WeixinRequestUtils {
@Value("${spring.wxconfig.code2Session-url}")
private String code2SessionUrl;
@Autowired private WebClient webClient;
/**
* 根据code从微信处获取用户唯一标识
*
* @param code
* @return {@link Map}
* @since 2021/9/18 8:25
*/
public Map getUserIdetify(String code) {
Map result =
this.webClient
.get()
.uri(this.code2SessionUrl + code)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.retrieve()
.bodyToMono(String.class)
.timeout(Duration.ofSeconds(4))
.onErrorResume(
e -> {
log.error("调用微信 auth.code2Session 接口换取用户唯一标识失败。", e);
return Mono.empty();
})
.map(str -> JSONUtil.toBean(str, Map.class))
.block();
return result;
}
}
package com.postcard.service.constant;
/**
* @Author:zhouxudong
*
* @version: 1.0 @Date: 2023/12/14 17:01 @Description:
*/
public class Constants {
public static final String TOKEN_HEADER = "Authorization";
}
package com.postcard.service.constant;
/**
* @Author:zhouxudong
* @version: 1.0
* @Date: 2023/12/14 17:02
* @Description:
*/
public class MsgConstants {
}
package com.postcard.service.constant;
/**
* @Author:zhouxudong
* @version: 1.0
* @Date: 2023/12/14 17:02
* @Description:
*/
public class RedisConstants {
}
package com.postcard.service.constant;
/**
* @author: zhouxudong
* @version: 1.0
* @createTime: 2023/11/17 17:14
* @description:
*/
public class TokenConstants {
public static final byte[] SECRET = "03jh6Uf6#4z9^c6!gGA|BYc!@q3Sl4IDR".getBytes();
public static final String DETAILS = "details";
public static final String AUTHORITIES = "authorities";
public static final String EXPIRE_TIME = "expire_time";
public static final String CREATE_TIME = "create_time";
public static final String RESULT = "message";
public static final String SUCCESS = "success";
public static final class TOKEN_RESULT {
public static final String TOKEN_PARSE_SUCCESS = "token解析成功";
public static final String TOKEN_EXPIRED = "token失效";
}
public static final String DATA = "data";
}
package com.postcard.service.domain;
import lombok.Data;
import org.apache.commons.lang3.ObjectUtils;
import java.util.HashMap;
import java.util.Objects;
/**
* @author: zhouxudong
* @version: 1.0
* @createTime: 2023/11/24 17:21
* @description: 请求用户服务返回数据格式
*/
@Data
public class AjaxResult extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
/** 状态码 */
public static final String CODE_TAG = "code";
/** 返回内容 */
public static final String MSG_TAG = "msg";
/** 数据对象 */
public static final String DATA_TAG = "data";
/** 状态类型 */
public enum ErrorCodeEum {
/** 成功 */
SUCCESS(200, "成功"),
/** 警告 */
WARN(300, "警告"),
AUTHENTICATION(401, "认证"),
/** 错误 */
ERROR(500, "错误");
private final int code;
private final String message;
ErrorCodeEum(int code, String message) {
this.code = code;
this.message = message;
}
public int code() {
return this.code;
}
public String message() {
return this.message;
}
}
/** 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。 */
public AjaxResult() {}
/**
* 初始化一个新创建的 AjaxResult 对象
*
* @param errorCodeEum 状态类型
* @param msg 返回内容
*/
public AjaxResult(ErrorCodeEum errorCodeEum, String msg) {
super.put(CODE_TAG, errorCodeEum.code());
super.put(MSG_TAG, msg);
}
/**
* 初始化一个新创建的 AjaxResult 对象
*
* @param code 状态类型
* @param msg 返回内容
* @param data 数据对象
*/
public AjaxResult(int code, String msg, Object data) {
super.put(CODE_TAG, code);
super.put(MSG_TAG, msg);
if (ObjectUtils.isNotEmpty(data)) {
super.put(DATA_TAG, data);
}
}
/**
* 返回成功消息
*
* @return 成功消息
*/
public static AjaxResult success() {
return AjaxResult.success("操作成功");
}
/**
* 返回成功数据
*
* @return 成功消息
*/
public static AjaxResult success(Object data) {
return AjaxResult.success("操作成功", data);
}
/**
* 返回成功消息
*
* @param msg 返回内容
* @return 成功消息
*/
public static AjaxResult success(String msg) {
return AjaxResult.success(msg, null);
}
/**
* 返回成功消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 成功消息
*/
public static AjaxResult success(String msg, Object data) {
return new AjaxResult(ErrorCodeEum.SUCCESS.code(), msg, data);
}
/**
* 返回消息
*
* @param msg 返回内容
* @return 警告消息
*/
public static AjaxResult warn(String msg) {
return AjaxResult.warn(msg, null);
}
/**
* 返回警告消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 警告消息
*/
public static AjaxResult warn(String msg, Object data) {
return new AjaxResult(ErrorCodeEum.WARN.code(), msg, data);
}
/**
* 返回授权消息
*
* @param msg 返回内容
* @return 返回授权消息
*/
public static AjaxResult auth(String msg) {
return AjaxResult.auth(msg, null);
}
/**
* 返回授权消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 返回授权消息
*/
public static AjaxResult auth(String msg, Object data) {
return new AjaxResult(ErrorCodeEum.AUTHENTICATION.code(), msg, data);
}
/**
* 返回错误消息
*
* @return
*/
public static AjaxResult error() {
return AjaxResult.error("操作失败");
}
/**
* 返回错误消息
*
* @param msg 返回内容
* @return 警告消息
*/
public static AjaxResult error(String msg) {
return AjaxResult.error(msg, null);
}
public static AjaxResult error(int code, String msg) {
return new AjaxResult(ErrorCodeEum.ERROR.code(), msg, null);
}
/**
* 返回错误消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 警告消息
*/
public static AjaxResult error(String msg, Object data) {
return new AjaxResult(ErrorCodeEum.ERROR.code(), msg, data);
}
/**
* 是否为成功消息
*
* @return 结果
*/
public boolean isSuccess() {
return Objects.equals(ErrorCodeEum.SUCCESS.code(), this.get(CODE_TAG));
}
/**
* 是否为警告消息
*
* @return 结果
*/
public boolean isWarn() {
return Objects.equals(ErrorCodeEum.WARN.code(), this.get(CODE_TAG));
}
/**
* 是否为错误消息
*
* @return 结果
*/
public boolean isError() {
return Objects.equals(ErrorCodeEum.ERROR.code(), this.get(CODE_TAG));
}
/**
* 方便链式调用
*
* @param key 键
* @param value 值
* @return 数据对象
*/
@Override
public AjaxResult put(String key, Object value) {
super.put(key, value);
return this;
}
}
package com.postcard.service.domain;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.Version;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.*;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* @Author:zhouxudong
* @version: 1.0
* @Date: 2023/11/23 16:57
* @Description:
*/
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public abstract class BaseEntity extends Model implements Serializable {
@TableId
protected Long id;
@TableField(
value = "create_by",
fill = FieldFill.INSERT
)
protected String createBy;
@TableField(
value = "create_time",
fill = FieldFill.INSERT
)
protected LocalDateTime createTime;
@TableField(
value = "update_by",
fill = FieldFill.INSERT_UPDATE
)
protected String updateBy;
@TableField(
value = "update_time",
fill = FieldFill.INSERT_UPDATE
)
protected LocalDateTime updateTime;
@Version
@TableField(
value = "version",
fill = FieldFill.INSERT
)
protected Integer version;
public Serializable pkVal() {
return this.id;
}
}
package com.postcard.service.domain;
import lombok.*;
import lombok.experimental.Accessors;
/**
* @author: zhouxudong
* @version: 1.0
* @createTime: 2023/11/17 17:13
* @description: jwt 信息
*/
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Accessors(chain = true)
public class JwtInfo {
/** 人员id */
private Long userId;
/** 人员名称 */
private String nickName;
/** 手机号 */
private String phone;
private String openId;
private String sessionKey;
}
package com.postcard.service.moudle.carrier.controller;
/**
* @Author:zhouxudong
* @version: 1.0
* @Date: 2023/12/14 19:29
* @Description:
*/
public class test {
}
package com.postcard.service.moudle.carrier.dto;
/**
* @Author:zhouxudong
* @version: 1.0
* @Date: 2023/12/14 19:29
* @Description:
*/
public class test {
}
package com.postcard.service.moudle.carrier.entity;
/**
* @Author:zhouxudong
* @version: 1.0
* @Date: 2023/12/14 19:29
* @Description:
*/
public class test {
}
package com.postcard.service.moudle.carrier.mapper;
/**
* @Author:zhouxudong
* @version: 1.0
* @Date: 2023/12/14 19:29
* @Description:
*/
public class test {
}
package com.postcard.service.moudle.carrier.service;
/**
* @Author:zhouxudong
* @version: 1.0
* @Date: 2023/12/14 19:29
* @Description:
*/
public class test {
}
package com.postcard.service.moudle.carrier.vo;
/**
* @Author:zhouxudong
* @version: 1.0
* @Date: 2023/12/14 19:29
* @Description:
*/
public class test {
}
package com.postcard.service.moudle.park.controller;
/**
* @Author:zhouxudong
* @version: 1.0
* @Date: 2023/12/14 19:29
* @Description:
*/
public class test {
}
package com.postcard.service.moudle.park.dto;
/**
* @Author:zhouxudong
* @version: 1.0
* @Date: 2023/12/14 19:29
* @Description:
*/
public class test {
}
package com.postcard.service.moudle.park.entity;
/**
* @Author:zhouxudong
* @version: 1.0
* @Date: 2023/12/14 19:29
* @Description:
*/
public class test {
}
package com.postcard.service.moudle.park.mapper;
/**
* @Author:zhouxudong
* @version: 1.0
* @Date: 2023/12/14 19:29
* @Description:
*/
public class test {
}
package com.postcard.service.moudle.park.service;
/**
* @Author:zhouxudong
* @version: 1.0
* @Date: 2023/12/14 19:29
* @Description:
*/
public class test {
}
package com.postcard.service.moudle.park.vo;
/**
* @Author:zhouxudong
* @version: 1.0
* @Date: 2023/12/14 19:29
* @Description:
*/
public class test {
}
package com.postcard.service.moudle.user.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.postcard.service.domain.BaseEntity;
import lombok.*;
import lombok.experimental.Accessors;
/**
* @author: zhouxudong
* @version: 1.0
* @createTime: 2023/12/14 18:57
* @description: 用户信息
*/
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@TableName("user_applet_info")
@ToString
public class UserAppletEntity extends BaseEntity {
// 昵称
private String nickName;
// 性别 0:男 1:女
private Integer gender;
// 头像
private String avatarUrl;
// openId
private String openId;
// 国家
private String country;
// 省份
private String province;
// 城市
private String city;
// 手机号
private String phone;
}
package com.postcard.service.moudle.user.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.postcard.service.moudle.user.entity.UserAppletEntity;
import org.apache.ibatis.annotations.Mapper;
/**
* @author: zhouxudong
* @version: 1.0
* @createTime: 2023/12/14 19:01
* @description: 用户服务
*/
@Mapper
public interface UserMapper extends BaseMapper<UserAppletEntity> {
}
package com.postcard.service.moudle.user.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.postcard.service.moudle.user.entity.UserAppletEntity;
/**
* @author: zhouxudong
* @version: 1.0
* @createTime: 2023/12/14 18:58
* @description: 用户服务
*/
public interface UserService extends IService<UserAppletEntity> {
/**
* @description: 通过openId 查询用户信息
* @date: 2023/12/14 19:04
* @param: [openid]
* @return: com.postcard.service.moudle.user.entity.UserAppletEntity
**/
UserAppletEntity getUserInfo(String openid);
}
package com.postcard.service.moudle.user.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.postcard.service.moudle.user.entity.UserAppletEntity;
import com.postcard.service.moudle.user.mapper.UserMapper;
import com.postcard.service.moudle.user.service.UserService;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.stereotype.Service;
/**
* @Author:zhouxudong
* @version: 1.0
* @Date: 2023/12/14 18:59
* @Description: 用户信息
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, UserAppletEntity>
implements UserService {
@Override
public UserAppletEntity getUserInfo(String openid) {
LambdaQueryWrapper<UserAppletEntity> lambdaQueryWrapper=new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(UserAppletEntity::getOpenId,openid);
UserAppletEntity userApplet = this.getOne(lambdaQueryWrapper, false);
if(ObjectUtils.isEmpty(userApplet)){
UserAppletEntity user=new UserAppletEntity();
user.setOpenId(openid);
this.save(user);
return user;
}
return userApplet;
}
}
package com.postcard.service.util;
import org.apache.commons.lang3.ObjectUtils;
import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* 获取IP方法
*
* @author ruoyi
*/
public class IpUtils {
public static String getIpAddr(HttpServletRequest request) {
if (request == null) {
return null;
}
String ip = null;
// X-Forwarded-For:Squid 服务代理
String ipAddresses = request.getHeader("X-Forwarded-For");
if (ipAddresses == null
|| ipAddresses.length() == 0
|| "unknown".equalsIgnoreCase(ipAddresses)) {
// Proxy-Client-IP:apache 服务代理
ipAddresses = request.getHeader("Proxy-Client-IP");
}
if (ipAddresses == null
|| ipAddresses.length() == 0
|| "unknown".equalsIgnoreCase(ipAddresses)) {
// WL-Proxy-Client-IP:weblogic 服务代理
ipAddresses = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddresses == null
|| ipAddresses.length() == 0
|| "unknown".equalsIgnoreCase(ipAddresses)) {
// HTTP_CLIENT_IP:有些代理服务器
ipAddresses = request.getHeader("HTTP_CLIENT_IP");
}
if (ipAddresses == null
|| ipAddresses.length() == 0
|| "unknown".equalsIgnoreCase(ipAddresses)) {
// X-Real-IP:nginx服务代理
ipAddresses = request.getHeader("X-Real-IP");
}
// 有些网络通过多层代理,那么获取到的ip就会有多个,一般都是通过逗号(,)分割开来,并且第一个ip为客户端的真实IP
if (ipAddresses != null && ipAddresses.length() != 0) {
ip = ipAddresses.split(",")[0];
}
// 还是不能获取到,最后再通过request.getRemoteAddr();获取
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) {
ip = request.getRemoteAddr();
}
return ip.equals("0:0:0:0:0:0:0:1") ? "127.0.0.1" : ip;
}
public static boolean internalIp(String ip) {
byte[] addr = textToNumericFormatV4(ip);
return internalIp(addr) || "127.0.0.1".equals(ip);
}
private static boolean internalIp(byte[] addr) {
if (ObjectUtils.isEmpty(addr) || addr.length < 2) {
return true;
}
final byte b0 = addr[0];
final byte b1 = addr[1];
// 10.x.x.x/8
final byte SECTION_1 = 0x0A;
// 172.16.x.x/12
final byte SECTION_2 = (byte) 0xAC;
final byte SECTION_3 = (byte) 0x10;
final byte SECTION_4 = (byte) 0x1F;
// 192.168.x.x/16
final byte SECTION_5 = (byte) 0xC0;
final byte SECTION_6 = (byte) 0xA8;
switch (b0) {
case SECTION_1:
return true;
case SECTION_2:
if (b1 >= SECTION_3 && b1 <= SECTION_4) {
return true;
}
case SECTION_5:
switch (b1) {
case SECTION_6:
return true;
}
default:
return false;
}
}
/**
* 将IPv4地址转换成字节
*
* @param text IPv4地址
* @return byte 字节
*/
public static byte[] textToNumericFormatV4(String text) {
if (text.length() == 0) {
return null;
}
byte[] bytes = new byte[4];
String[] elements = text.split("\\.", -1);
try {
long l;
int i;
switch (elements.length) {
case 1:
l = Long.parseLong(elements[0]);
if ((l < 0L) || (l > 4294967295L)) {
return null;
}
bytes[0] = (byte) (int) (l >> 24 & 0xFF);
bytes[1] = (byte) (int) ((l & 0xFFFFFF) >> 16 & 0xFF);
bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF);
bytes[3] = (byte) (int) (l & 0xFF);
break;
case 2:
l = Integer.parseInt(elements[0]);
if ((l < 0L) || (l > 255L)) {
return null;
}
bytes[0] = (byte) (int) (l & 0xFF);
l = Integer.parseInt(elements[1]);
if ((l < 0L) || (l > 16777215L)) {
return null;
}
bytes[1] = (byte) (int) (l >> 16 & 0xFF);
bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF);
bytes[3] = (byte) (int) (l & 0xFF);
break;
case 3:
for (i = 0; i < 2; ++i) {
l = Integer.parseInt(elements[i]);
if ((l < 0L) || (l > 255L)) {
return null;
}
bytes[i] = (byte) (int) (l & 0xFF);
}
l = Integer.parseInt(elements[2]);
if ((l < 0L) || (l > 65535L)) {
return null;
}
bytes[2] = (byte) (int) (l >> 8 & 0xFF);
bytes[3] = (byte) (int) (l & 0xFF);
break;
case 4:
for (i = 0; i < 4; ++i) {
l = Integer.parseInt(elements[i]);
if ((l < 0L) || (l > 255L)) {
return null;
}
bytes[i] = (byte) (int) (l & 0xFF);
}
break;
default:
return null;
}
} catch (NumberFormatException e) {
return null;
}
return bytes;
}
public static String getHostIp() {
try {
return InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
}
return "127.0.0.1";
}
public static String getHostName() {
try {
return InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e) {
}
return "未知";
}
}
package com.postcard.service.util.jwt;
import cn.hutool.core.bean.BeanUtil;
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.crypto.MACVerifier;
import com.nimbusds.jose.shaded.json.JSONObject;
import com.postcard.service.config.exception.AuthException;
import com.postcard.service.constant.TokenConstants;
import com.postcard.service.domain.JwtInfo;
import lombok.extern.slf4j.Slf4j;
import java.text.ParseException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author: zhouxudong
* @version: 1.0
* @createTime: 2023/11/17 17:11
* @description:
*/
@Slf4j
public class JwtTokenUtil {
private static final Long expireTime = 28800000L;
/**
* 采用HS256算法生成token
*
* @param
* @return
* @throws JOSEException
*/
public static String createToken(JwtInfo jwtUser, List authorities) {
JWSHeader jwsHeader = new JWSHeader(JWSAlgorithm.HS256);
JSONObject jsonObject = new JSONObject();
jsonObject.appendField(TokenConstants.DETAILS, jwtUser);
jsonObject.appendField(TokenConstants.AUTHORITIES, authorities);
jsonObject.appendField(TokenConstants.CREATE_TIME, System.currentTimeMillis());
Long current_time = System.currentTimeMillis();
Long expire_time = current_time + expireTime;
jsonObject.appendField(TokenConstants.EXPIRE_TIME, expire_time);
Payload payload = new Payload(jsonObject);
JWSObject jwsObject = new JWSObject(jwsHeader, payload);
JWSSigner jwsSigner;
try {
jwsSigner = new MACSigner(TokenConstants.SECRET);
try {
jwsObject.sign(jwsSigner);
} catch (JOSEException e) {
log.error("生成token异常", e);
}
} catch (KeyLengthException e) {
log.error("生成token,密钥长度异常", e);
}
return jwsObject.serialize();
}
/**
* 解析token
*
* @param token
* @return
* @throws ParseException
* @throws JOSEException
*/
public static Map<String, Object> parseToken(String token) {
JWSObject jwsObject;
JWSVerifier jwsVerifier;
try {
jwsObject = JWSObject.parse(token);
jwsVerifier = new MACVerifier(TokenConstants.SECRET);
} catch (ParseException | JOSEException e) {
log.error("解析token报错:", e);
throw new AuthException("认证失败,请重新登录");
}
return verify(jwsObject, jwsVerifier);
}
/**
* 验证token
*
* @param jwsObject
* @param jwsVerifier
* @return
* @throws JOSEException
*/
private static Map<String, Object> verify(JWSObject jwsObject, JWSVerifier jwsVerifier) {
Map<String, Object> resultMap = new HashMap<>();
Payload payload = jwsObject.getPayload();
boolean flag = Boolean.TRUE;
try {
if (jwsObject.verify(jwsVerifier)) {
resultMap.put(TokenConstants.RESULT, TokenConstants.TOKEN_RESULT.TOKEN_PARSE_SUCCESS);
Map<String, Object> jsonObject = payload.toJSONObject();
resultMap.put(TokenConstants.DATA, jsonObject);
if (jsonObject.containsKey(TokenConstants.EXPIRE_TIME)) {
Long expireTime = Long.valueOf(jsonObject.get(TokenConstants.EXPIRE_TIME).toString());
Long nowTime = System.currentTimeMillis();
log.debug("nowTime : " + nowTime);
if (nowTime > expireTime) {
resultMap.clear();
flag = false;
resultMap.put(TokenConstants.RESULT, TokenConstants.TOKEN_RESULT.TOKEN_EXPIRED);
}
}
} else {
throw new AuthException("认证失败");
// resultMap.put(TokenConstants.RESULT, TokenConstants.TOKEN_RESULT.TOKEN_PARSE_FAILED);
}
} catch (JOSEException e) {
log.error("解析token报错", e);
throw new AuthException("认证失败");
}
resultMap.put(TokenConstants.SUCCESS, flag);
return resultMap;
}
/**
* 返回jwtUser
*
* @param token
* @return
*/
public static JwtInfo getJwtUser(String token) {
Map<String, Object> objectMap = parseToken(token);
Map<String, Object> data = (Map<String, Object>) objectMap.get(TokenConstants.DATA);
JwtInfo jwtUser = BeanUtil.toBean(data.get(TokenConstants.DETAILS), JwtInfo.class);
return jwtUser;
}
}
package com.postcard.service.util.redis;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.extra.spring.SpringUtil;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @Author:zhouxudong
*
* @version: 1.0 @Date: 2023/11/20 9:27 @Description: redis 工具类
*/
public class RedisUtil {
private RedisUtil() {}
@SuppressWarnings("unchecked")
private static RedisTemplate<String, Object> redisTemplate =
SpringUtil.getBean("redisTemplate", RedisTemplate.class);
/**
* 设置有效时间 单位默认秒
*
* @param key Redis键
* @param timeout 超时时间
* @return true=设置成功;false=设置失败
*/
public static boolean expire(final String key, final long timeout) {
return expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @param unit 时间单位
* @return true=设置成功;false=设置失败
*/
public static boolean expire(final String key, final long timeout, final TimeUnit unit) {
Boolean ret = redisTemplate.expire(key, timeout, unit);
return ret != null && ret;
}
/**
* 删除单个key
*
* @param key 键
* @return true=删除成功;false=删除失败
*/
public static boolean del(final String key) {
Boolean ret = redisTemplate.delete(key);
return ret != null && ret;
}
/**
* 删除多个key
*
* @param keys 键集合
* @return 成功删除的个数
*/
public static long del(final Collection<String> keys) {
Long ret = redisTemplate.delete(keys);
return ret == null ? 0 : ret;
}
/**
* 存入普通对象
*
* @param key Redis键
* @param value 值
*/
public static void set(final String key, final Object value) {
redisTemplate.opsForValue().set(key, value);
}
// 存储普通对象操作
/**
* 存入普通对象
*
* @param key 键
* @param value 值
* @param timeout 有效期,单位秒
*/
public static void set(final String key, final Object value, final long timeout) {
redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
}
public static void set(final String key, final Object value, final long timeout,TimeUnit timeUnit) {
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
/**
* 获取普通对象
*
* @param key 键
* @return 对象
*/
public static Object get(final String key) {
return redisTemplate.opsForValue().get(key);
}
/**
* @description: 获取key的剩余过期时间 如果key不存在 或者没有设置过期时间 返回 -1 单位 为秒
* @date: 2023/11/23 9:53
* @param: [key]
* @return: java.lang.Long
**/
public static Long getExpire(final String key){
return redisTemplate.getExpire(key);
}
// 存储Hash操作
/**
* 往Hash中存入数据
*
* @param key Redis键
* @param filed Hash filed键
* @param value 值
*/
public static void hPut(final String key, final String filed, final Object value) {
redisTemplate.opsForHash().put(key, filed, value);
}
/**
* 往Hash中存入多个数据
*
* @param key Redis键
* @param filedMap Hash键值对
*/
public static void hPutAll(final String key, final Map<String, Object> filedMap) {
redisTemplate.opsForHash().putAll(key, filedMap);
}
/**
* 获取Hash中的数据
*
* @param key Redis键
* @param filed Hash filed键
* @return Hash中的对象
*/
public static Object hGet(final String key, final String filed) {
return redisTemplate.opsForHash().get(key, filed);
}
/**
* 获取多个Hash中的数据
*
* @param key Redis键
* @param fileds Hash filed键集合
* @return Hash对象集合
*/
public static List<Object> hMultiGet(final String key, final Collection<Object> fileds) {
return redisTemplate.opsForHash().multiGet(key, fileds);
}
// 存储Set相关操作
/**
* 往Set中存入数据
*
* @param key Redis键
* @param values 值
* @return 存入的个数
*/
public static long sSet(final String key, final Object... values) {
Long count = redisTemplate.opsForSet().add(key, values);
return count == null ? 0 : count;
}
/**
* 删除Set中的数据
*
* @param key Redis键
* @param values 值
* @return 移除的个数
*/
public static long sDel(final String key, final Object... values) {
Long count = redisTemplate.opsForSet().remove(key, values);
return count == null ? 0 : count;
}
// 存储List相关操作
/**
* 往List左侧中存入数据
*
* @param key Redis键
* @param value 数据
* @return 存入的个数
*/
public static long lPush(final String key, final Object value) {
Long count = redisTemplate.opsForList().leftPush(key, value);
return count == null ? 0 : count;
}
/**
* 往List右侧中存入数据
*
* @param key Redis键
* @param value 数据
* @return 存入的个数
*/
public static long rPush(final String key, final Object value) {
Long count = redisTemplate.opsForList().rightPush(key, value);
return count == null ? 0 : count;
}
/**
* 往List中左侧存入多个数据
*
* @param key Redis键
* @param values 多个数据
* @return 存入的个数
*/
public static long lPushAll(final String key, final Collection<Object> values) {
Long count = redisTemplate.opsForList().leftPushAll(key, values);
return count == null ? 0 : count;
}
/**
* 往List中左侧存入多个数据
*
* @param key Redis键
* @param values 多个数据
* @return 存入的个数
*/
public static long lPushAll(final String key, final Object... values) {
Long count = redisTemplate.opsForList().leftPushAll(key, values);
return count == null ? 0 : count;
}
/**
* 往List中右侧存入多个数据
*
* @param key Redis键
* @param values 多个数据
* @return 存入的个数
*/
public static long rPushAll(final String key, final Collection<Object> values) {
Long count = redisTemplate.opsForList().rightPushAll(key, values);
return count == null ? 0 : count;
}
/**
* 往List中右侧存入多个数据
*
* @param key Redis键
* @param values 多个数据
* @return 存入的个数
*/
public static long rPushAll(final String key, final Object... values) {
Long count = redisTemplate.opsForList().rightPushAll(key, values);
return count == null ? 0 : count;
}
/**
* 从List中获取begin到end之间的元素
*
* @param key Redis键
* @param start 开始位置
* @param end 结束位置(start=0,end=-1表示获取全部元素)
* @return List对象
*/
public static List<Object> listGetRange(final String key, final int start, final int end) {
return redisTemplate.opsForList().range(key, start, end);
}
/**
* 从List左侧弹出数据
*
* @param key Redis键
* @return 对象
*/
public static Object listGetL(final String key) {
return redisTemplate.opsForList().leftPop(key);
}
/**
* 从List右侧弹出数据
*
* @param key Redis键
* @return 对象
*/
public static Object listGetR(final String key) {
return redisTemplate.opsForList().rightPop(key);
}
/**
* @description: 移除有序集合中给定的分数区间的所有成员
* @date: 2023/11/27 15:19
* @param: [key, start, end]
* @return: Long 移除的成员数
**/
public static Long delZset(String key,double start,double end){
return redisTemplate.opsForZSet().removeRangeByScore(key, start, end);
}
/**
* @description: 计算在有序集合中指定区间分数的成员数
* @date: 2023/11/27 15:21
* @param: [key, start, end]
* @return: java.lang.Long
**/
public static Long countZset(String key,double start,double end){
return redisTemplate.opsForZSet().count(key, start, end);
}
/**
* @description: 向有序集合添加一个或多个成员,或者更新已存在成员的分数
* @date: 2023/11/27 15:26
* @param: [key, fileds, end]
* @return: boolean
**/
public static boolean addZset(String key,Object fileds,double end){
return Boolean.TRUE.equals(redisTemplate.opsForZSet().add(key, fileds, end));
}
//-------------流量控制-----------------
/**
* @description: 访问控制
* @date: 2023/12/14 14:30
* @param: [key, intervalTime 时间间隔(秒), max 最大访问次数]
* @return: void
**/
public static boolean rateLimit(String key,int intervalTime,int max) {
DateTime now = DateUtil.date();
DateTime startTime = DateUtil.offsetSecond(now, intervalTime * -1);
delZset(key, 0, startTime.getTime());
Long count = countZset(key, startTime.getTime(), now.getTime());
if (count == null || count <= max) {
return addZset(key, now.getTime(), now.getTime());
} else {
return false;
}
}
}
spring:
config:
activate:
on-profile: dev
#spring 默认数据库连接池
datasource:
type: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://rm-2ze10ohzb1898j5qdfo.mysql.rds.aliyuncs.com:3306/liyeyun?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8&useSSL=false
username: liyeyun
password: CF**ldcn
hikari:
maximum-pool-size: 10
minimum-idle: 5
connection-timeout: 10000
redis:
host: r-2zekq6swp5wr808a3lpd.redis.rds.aliyuncs.com
port: 6379
password: techbook4redis#&20190909
timeout: 20000
database: 2
lettuce:
pool:
# 连接池中最大连接数,负数表示没有限制
max-active: 12
# 连接池中的最大空闲连接
max-idle: 8
# 连接池中的最小空闲连接
min-idle: 0
# 最大阻塞等待时间,负数表示没有限制
max-wait: 5000
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印日志
knife4j:
# 开启增强配置
enable: true
logging:
level:
com.postcard.service: debug
root: info
spring:
config:
activate:
on-profile: pro
#spring 默认数据库连接池
datasource:
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:mysql://1.92.66.73:3306/liyeyun?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true
username: liyeyun
password: CF**ldcn
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 10
minimum-idle: 5
connection-timeout: 10000
redis:
host: r-2zekq6swp5wr808a3lpd.redis.rds.aliyuncs.com
port: 6379
password: techbook4redis#&20190909
timeout: 3000
database: 4
lettuce:
pool:
max-active: 12
max-idle: 8
min-idle: 0
max-wait: 5000
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl # 打印日志
knife4j:
# 开启增强配置
enable: false
logging:
level:
com.postcard.service: debug
root: info
spring:
config:
activate:
on-profile: test
#spring 默认数据库连接池
datasource:
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:mysql://rm-2ze10ohzb1898j5qdfo.mysql.rds.aliyuncs.com:3306/liyeyun?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true
username: liyeyun
password: CF**ldcn
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 10
minimum-idle: 5
connection-timeout: 10000
redis:
host: r-2zekq6swp5wr808a3lpd.redis.rds.aliyuncs.com
port: 6379
password: techbook4redis#&20190909
timeout: 3000
database: 3
lettuce:
pool:
max-active: 12
max-idle: 8
min-idle: 0
max-wait: 5000
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl # 打印日志
knife4j:
# 开启增强配置
enable: true
business:
url:
thread: http://test.user.liyeyun.com/lyy/api/threadInfo/v1.0/save
logging:
level:
com.postcard.service: debug
root: info
spring:
profiles:
active: dev
application:
name: postcard-service
main:
#允许定义相同的bean对象 去覆盖原有的
allow-bean-definition-overriding: true
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
wxconfig:
appid: 123456
secret: 123456
code2Session-url: https://api.weixin.qq.com/sns/jscode2session?appid=${spring.wxconfig.appid}&secret=${spring.wxconfig.secret}&grant_type=authorization_code&js_code=
server:
port: 8090
servlet:
# 应用的访问路径
context-path: /postcard
tomcat:
uri-encoding: UTF-8
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
cache-enabled: true
call-setters-on-nulls: true
return-instance-for-empty-row: true
jdbc-type-for-null: varchar
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
#注解自增
id-type: ASSIGN_ID
#数据库大写下划线转换
capital-mode: true
#表名是否使用驼峰转下划线命名
table-underline: true
#逻辑已删除值
logic-delete-value: 0
logic-not-delete-value: 1
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="projectName" value="postcard-service"/>
<springProperty scope="context" name="moduleName" source="spring.application.name"
defaultValue="postcard-service"/>
<springProperty scope="context" name="logNum" source="log.num" defaultValue="01"/>
<springProfile name="test">
<property name="logPathPrefix" value="/app/logs"/>
</springProfile>
<springProfile name="pro">
<property name="logPathPrefix" value="/app/logs"/>
</springProfile>
<property name="logger.path" value="${logPathPrefix}/${projectName}"/>
<property name="maxHistory" value="30"/>
<property name="maxFileSize" value="50MB"/>
<property name="CONSOLE_LOG_PATTERN"
value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
<conversionRule conversionWord="wex"
converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
<conversionRule conversionWord="wEx"
converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>utf8</charset>
</encoder>
</appender>
<appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${logger.path}/debug/${moduleName}-${logNum}-debug.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${logger.path}/debug/${moduleName}-${logNum}-debug-%d{yyyy-MM-dd}.%i.zip</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>${maxFileSize}</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>${maxHistory}</maxHistory>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>debug</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${logger.path}/info/${moduleName}-${logNum}-info.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${logger.path}/info/${moduleName}-${logNum}-info-%d{yyyy-MM-dd}.%i.zip</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>${maxFileSize}</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>${maxHistory}</maxHistory>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>info</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${logger.path}/warn/${moduleName}-${logNum}-warn.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${logger.path}/warn/${moduleName}-${logNum}-warn-%d{yyyy-MM-dd}.%i.zip</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>${maxFileSize}</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>${maxHistory}</maxHistory>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>warn</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${logger.path}/error/${moduleName}-${logNum}-error.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${logger.path}/error/${moduleName}-${logNum}-error-%d{yyyy-MM-dd}.%i.zip</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>${maxFileSize}</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>${maxHistory}</maxHistory>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!--dev环境 info界别 输出到控制台-->
<springProfile name="dev">
<root level="info">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
<!--test环境 info界别 输出到三个日志文件中-->
<springProfile name="test">
<root level="info">
<appender-ref ref="INFO_FILE"/>
<appender-ref ref="DEBUG_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</springProfile>
<springProfile name="pro">
<root level="info">
<appender-ref ref="INFO_FILE"/>
<appender-ref ref="DEBUG_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</springProfile>
</configuration>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.postcard.service.moudle.user.mapper.UserMapper">
</mapper>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment