新增 user 模块

调整新的项目结构,将 service 和 dao 模块,从 application 拆除,独立成 service-impl 模块
增加 sdk 模块,用于提供一个封装过的功能。例如说,认证和授权~
This commit is contained in:
YunaiV 2019-02-25 18:20:30 +08:00
parent 2523f8b616
commit 4ae211dbc2
64 changed files with 2063 additions and 45 deletions

View File

@ -0,0 +1,46 @@
<?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">
<parent>
<artifactId>common</artifactId>
<groupId>cn.iocoder.mall</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>common-framework</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.1.5.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,55 @@
package cn.iocoder.common.framework.config;
import cn.iocoder.common.framework.constant.SysErrorCodeEnum;
import cn.iocoder.common.framework.exception.ServiceException;
import cn.iocoder.common.framework.vo.RestResult;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.UndeclaredThrowableException;
@ControllerAdvice
public class GlobalExceptionHandler {
@ResponseBody
@ExceptionHandler(value = ServiceException.class)
public RestResult serviceExceptionHandler(HttpServletRequest req, ServiceException ex) {
return RestResult.error(ex.getCode(), ex.getMessage());
}
// 处理 Spring 动态代理调用时发生 UndeclaredThrowableException 的情况
// 不了解的胖友可以看看 https://segmentfault.com/a/1190000012262244 文章
@ResponseBody
@ExceptionHandler(value = UndeclaredThrowableException.class)
public RestResult undeclaredThrowableExceptionHandler(HttpServletRequest req, UndeclaredThrowableException e) {
// 尝试获得 ServiceException 异常如果是则使用 serviceExceptionHandler 方法处理
Throwable undeclaredThrowable = e.getUndeclaredThrowable();
if (undeclaredThrowable instanceof InvocationTargetException) {
InvocationTargetException invocationTargetException = (InvocationTargetException) undeclaredThrowable;
Throwable targetException = invocationTargetException.getTargetException();
if (targetException != null & targetException instanceof ServiceException) {
return serviceExceptionHandler(req, (ServiceException) targetException);
}
}
// 获得不到使用 异常日志 方法处理
return resultExceptionHandler(req, e);
}
@ResponseBody
@ExceptionHandler(value = Exception.class)
public RestResult resultExceptionHandler(HttpServletRequest req, Exception e) {
// TODO 异常日志
e.printStackTrace();
// TODO 翻译不同的异常
if (e instanceof MissingServletRequestParameterException) {
return RestResult.error(SysErrorCodeEnum.MISSING_REQUEST_PARAM_ERROR.getCode(), SysErrorCodeEnum.MISSING_REQUEST_PARAM_ERROR.getMessage());
}
// 返回
return RestResult.error(SysErrorCodeEnum.SYS_ERROR.getCode(), SysErrorCodeEnum.SYS_ERROR.getMessage());
}
}

View File

@ -1,13 +1,14 @@
package cn.iocoder.mall.product.config;
package cn.iocoder.common.framework.config;
import cn.iocoder.mall.product.vo.RestResult;
import cn.iocoder.common.framework.vo.RestResult;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
//@ControllerAdvice
@ControllerAdvice
public class GlobalResponseBodyAdvice implements ResponseBodyAdvice {
@Override

View File

@ -1,11 +1,11 @@
package cn.iocoder.mall.product.constants;
package cn.iocoder.common.framework.constant;
/**
* 错误码枚举类
*
* 系统级异常使用 2-001-000-000
*/
public enum ErrorCodeEnum {
public enum SysErrorCodeEnum {
SYS_ERROR(2001001000, "服务端发生异常"),
MISSING_REQUEST_PARAM_ERROR(2001001001, "参数缺失"),
@ -15,7 +15,7 @@ public enum ErrorCodeEnum {
private final int code;
private final String message;
ErrorCodeEnum(int code, String message) {
SysErrorCodeEnum(int code, String message) {
this.code = code;
this.message = message;
}

View File

@ -1,4 +1,4 @@
package cn.iocoder.mall.product.exception;
package cn.iocoder.common.framework.exception;
/**
* 服务异常

View File

@ -0,0 +1,99 @@
package cn.iocoder.common.framework.util;
import cn.iocoder.common.framework.exception.ServiceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* {@link ServiceException} 工具类
*
* 目的在于格式化异常信息提示
* 考虑到 String.format 在参数不正确时会报错因此使用 {} 作为占位符并使用 {@link #doFormat(int, String, Object...)} 方法来格式化
*
* 因为 {@link #messages} 里面默认是没有异常信息提示的模板的所以需要使用方自己初始化进去目前想到的有几种方式
*
* 1. 异常提示信息写在枚举类中例如说cn.iocoder.oceans.user.api.constants.ErrorCodeEnum + ServiceExceptionConfiguration
* 2. 异常提示信息写在 .properties 等等配置文件
* 3. 异常提示信息写在 Apollo 等等配置中心中从而实现可动态刷新
* 4. 异常提示信息存储在 db 等等数据库中从而实现可动态刷新
*/
public class ServiceExceptionUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(ServiceExceptionUtil.class);
/**
* 错误码提示模板
*/
private static ConcurrentMap<Integer, String> messages = new ConcurrentHashMap<>();
public static void putAll(Map<Integer, String> messages) {
ServiceExceptionUtil.messages.putAll(messages);
}
public static void put(Integer code, String message) {
ServiceExceptionUtil.messages.put(code, message);
}
/**
* 创建指定编号的 ServiceException 的异常
*
* @param code 编号
* @return 异常
*/
public static ServiceException exception(Integer code) {
return new ServiceException(code, messages.get(code));
}
/**
* 创建指定编号的 ServiceException 的异常
*
* @param code 编号
* @param params 消息提示的占位符对应的参数
* @return 异常
*/
public static ServiceException exception(Integer code, Object... params) {
String message = doFormat(code, messages.get(code), params);
return new ServiceException(code, message);
}
/**
* 将错误编号对应的消息使用 params 进行格式化
*
* @param code 错误编号
* @param messagePattern 消息模版
* @param params 参数
* @return 格式化后的提示
*/
private static String doFormat(int code, String messagePattern, Object... params) {
StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50);
int i = 0;
int j;
int l;
for (l = 0; l < params.length; l++) {
j = messagePattern.indexOf("{}", i);
if (j == -1) {
LOGGER.error("[doFormat][参数过多:错误码({})|错误内容({})|参数({})", code, messagePattern, params);
if (i == 0) {
return messagePattern;
} else {
sbuf.append(messagePattern.substring(i, messagePattern.length()));
return sbuf.toString();
}
} else {
sbuf.append(messagePattern.substring(i, j));
sbuf.append(params[l]);
i = j + 2;
}
}
if (messagePattern.indexOf("{}", i) != -1) {
LOGGER.error("[doFormat][参数过少:错误码({})|错误内容({})|参数({})", code, messagePattern, params);
}
sbuf.append(messagePattern.substring(i, messagePattern.length()));
return sbuf.toString();
}
}

View File

@ -1,4 +1,4 @@
package cn.iocoder.mall.product.vo;
package cn.iocoder.common.framework.vo;
public class RestResult {

19
common/pom.xml Normal file
View File

@ -0,0 +1,19 @@
<?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">
<parent>
<artifactId>mall-parent</artifactId>
<groupId>cn.iocoder.mall</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>common</artifactId>
<packaging>pom</packaging>
<modules>
<module>common-framework</module>
</modules>
</project>

View File

@ -16,6 +16,8 @@
<modules>
<module>product</module>
<module>order</module>
<module>user</module>
<module>common</module>
</modules>
<packaging>pom</packaging>

View File

@ -21,6 +21,11 @@
<artifactId>product-service-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>cn.iocoder.mall</groupId>
<artifactId>common-framework</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@ -1,36 +0,0 @@
package cn.iocoder.mall.product.config;
import cn.iocoder.mall.product.constants.ErrorCodeEnum;
import cn.iocoder.mall.product.exception.ServiceException;
import cn.iocoder.mall.product.vo.RestResult;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
@ControllerAdvice
public class GlobalExceptionHandler {
@ResponseBody
@ExceptionHandler(value = ServiceException.class)
public RestResult serviceExceptionHandler(HttpServletRequest req, Exception e) {
ServiceException ex = (ServiceException) e;
return RestResult.error(ex.getCode(), ex.getMessage());
}
@ResponseBody
@ExceptionHandler(value = Exception.class)
public RestResult resultExceptionHandler(HttpServletRequest req, Exception e) {
// TODO 异常日志
e.printStackTrace();
// TODO 翻译不同的异常
if (e instanceof MissingServletRequestParameterException) {
return RestResult.error(ErrorCodeEnum.MISSING_REQUEST_PARAM_ERROR.getCode(), ErrorCodeEnum.MISSING_REQUEST_PARAM_ERROR.getMessage());
}
// 返回
return RestResult.error(ErrorCodeEnum.SYS_ERROR.getCode(), ErrorCodeEnum.SYS_ERROR.getMessage());
}
}

View File

@ -1,12 +1,16 @@
package cn.iocoder.mall.product.config;
import cn.iocoder.common.framework.config.GlobalExceptionHandler;
import cn.iocoder.common.framework.config.GlobalResponseBodyAdvice;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@EnableWebMvc
@Configuration
@Import(value = {GlobalResponseBodyAdvice.class, GlobalExceptionHandler.class}) // 统一全局返回
public class MVCConfiguration implements WebMvcConfigurer {
// @Autowired

View File

@ -10,7 +10,7 @@ spring:
# server
server:
port: 8081
port: 8080
# mybatis
mybatis:

22
user/pom.xml Normal file
View File

@ -0,0 +1,22 @@
<?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">
<parent>
<artifactId>mall-parent</artifactId>
<groupId>cn.iocoder.mall</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>user</artifactId>
<packaging>pom</packaging>
<modules>
<module>user-application</module>
<module>user-service-api</module>
<module>user-sdk</module>
<module>user-service-impl</module>
</modules>
</project>

View File

@ -0,0 +1,119 @@
<?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">
<parent>
<artifactId>user</artifactId>
<groupId>cn.iocoder.mall</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>user-application</artifactId>
<properties>
<org.mapstruct.version>1.3.0.Final</org.mapstruct.version>
</properties>
<dependencies>
<dependency>
<groupId>cn.iocoder.mall</groupId>
<artifactId>user-service-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>cn.iocoder.mall</groupId>
<artifactId>user-service-impl</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>cn.iocoder.mall</groupId>
<artifactId>common-framework</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.6.5</version>
</dependency>
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>0.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId> <!-- use mapstruct-jdk8 for Java 8 or higher -->
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>cn.iocoder.mall</groupId>
<artifactId>user-service-api</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>cn.iocoder.mall</groupId>
<artifactId>user-sdk</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 提供给 mapstruct 使用 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source> <!-- or higher, depending on your project -->
<target>1.8</target> <!-- or higher, depending on your project -->
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,13 @@
package cn.iocoder.mall.user;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = "cn.iocoder.mall.user")
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}

View File

@ -0,0 +1,27 @@
package cn.iocoder.mall.user.config;
import cn.iocoder.common.framework.config.GlobalExceptionHandler;
import cn.iocoder.common.framework.config.GlobalResponseBodyAdvice;
import cn.iocoder.mall.user.sdk.interceptor.SecurityInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@EnableWebMvc
@Configuration
@Import(value = {GlobalResponseBodyAdvice.class, GlobalExceptionHandler.class, // 统一全局返回
SecurityInterceptor.class}) // 安全拦截器实现认证和授权功能
public class MVCConfiguration implements WebMvcConfigurer {
@Autowired
private SecurityInterceptor securityInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(securityInterceptor);
}
}

View File

@ -0,0 +1,26 @@
package cn.iocoder.mall.user.config;
import cn.iocoder.common.framework.util.ServiceExceptionUtil;
import cn.iocoder.mall.user.service.api.constant.UserErrorCodeEnum;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
@Configuration
public class ServiceExceptionConfiguration {
@EventListener(ApplicationReadyEvent.class) // 可参考 https://www.cnblogs.com/ssslinppp/p/7607509.html
public void initMessages() {
// service_exception_message.properties 加载错误码的方案
// Properties properties;
// try {
// properties = PropertiesLoaderUtils.loadAllProperties("classpath:service_exception_message.properties");
// } catch (IOException e) {
// throw new RuntimeException(e);
// }
for (UserErrorCodeEnum item : UserErrorCodeEnum.values()) {
ServiceExceptionUtil.put(item.getCode(), item.getMessage());
}
}
}

View File

@ -0,0 +1,95 @@
package cn.iocoder.mall.user.controller;
import cn.iocoder.common.framework.exception.ServiceException;
import cn.iocoder.mall.user.sdk.annotation.PermitAll;
import cn.iocoder.mall.user.service.api.MobileCodeService;
import cn.iocoder.mall.user.service.api.OAuth2Service;
import cn.iocoder.mall.user.service.api.UserService;
import cn.iocoder.mall.user.service.api.constant.UserErrorCodeEnum;
import cn.iocoder.mall.user.service.api.dto.OAuth2AccessTokenBO;
import com.alibaba.dubbo.config.annotation.Reference;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/passport")
public class PassportController {
@Reference
private OAuth2Service oauth2Service;
@Reference
private UserService userService;
@Reference
private MobileCodeService mobileCodeService;
// TODO 功能手机密码登陆
// @PostMapping("/mobile/pwd/login")
// public OAuth2AccessToken mobileLogin(@RequestParam("mobile") String mobile,
// @RequestParam("password") String password) {
// return oauth2Service.getAccessToken(clientId, clientSecret, mobile, password);
// }
/**
* 手机号 + 验证码登陆
*
* @param mobile 手机号
* @param code 验证码
* @return 授权信息
*/
@PermitAll
@PostMapping("/mobile/login")
public OAuth2AccessTokenBO mobileRegister(@RequestParam("mobile") String mobile,
@RequestParam("code") String code) {
// 尝试直接授权
OAuth2AccessTokenBO accessTokenDTO;
try {
accessTokenDTO = oauth2Service.getAccessToken(mobile, code);
return accessTokenDTO;
} catch (ServiceException serviceException) {
if (!serviceException.getCode().equals(UserErrorCodeEnum.USER_MOBILE_NOT_REGISTERED.getCode())) { // 如果是未注册异常忽略下面发起自动注册逻辑
throw serviceException;
}
}
// 上面尝试授权失败说明用户未注册发起自动注册
try {
userService.createUser(mobile, code);
} catch (ServiceException serviceException) {
if (!serviceException.getCode().equals(UserErrorCodeEnum.USER_MOBILE_ALREADY_REGISTERED.getCode())) { // 如果是已注册异常忽略下面再次发起授权
throw serviceException;
}
}
// 再次发起授权
accessTokenDTO = oauth2Service.getAccessToken(mobile, code);
return accessTokenDTO;
}
/**
* 发送手机验证码
*
* @param mobile 手机号
*/
@PostMapping("mobile/send")
public void mobileSend(@RequestParam("mobile") String mobile) {
mobileCodeService.send(mobile);
}
// TODO 功能qq 登陆
@PermitAll
@PostMapping("/qq/login")
public String qqLogin() {
return null;
}
// TODO 功能qq 绑定
@PermitAll
@PostMapping("/qq/bind")
public String qqBind() {
return null;
}
// TODO 功能刷新 token
// TODO 功能退出销毁 token
}

View File

@ -0,0 +1,16 @@
package cn.iocoder.mall.user.controller;
import org.springframework.web.bind.annotation.GetMapping;
//@RestController
//@RequestMapping("/user")
public class UserController {
@GetMapping("/info")
public Long info() {
// TODO 芋艿正在实现中
// return SecurityContextHolder.getContext().getUid();
return null;
}
}

View File

@ -0,0 +1,7 @@
spring:
application:
name: user-application
# server
server:
port: 8082

59
user/user-sdk/pom.xml Normal file
View File

@ -0,0 +1,59 @@
<?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">
<parent>
<artifactId>user</artifactId>
<groupId>cn.iocoder.mall</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>user-sdk</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.5.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.5.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.5.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.6.5</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>cn.iocoder.mall</groupId>
<artifactId>common-framework</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>cn.iocoder.mall</groupId>
<artifactId>user-service-api</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,14 @@
package cn.iocoder.mall.user.sdk.annotation;
import java.lang.annotation.*;
/**
* URL 是否允许所有都可访问即用户不登陆就可以访问指定 URL
*
* 例如说注册接口用户是不需要登陆就可以访问的
*/
@Documented
@Target({ElementType.METHOD}) // ElementType.TYPE 暂时不支持类级别为了减少判断略微提升性能
@Retention(RetentionPolicy.RUNTIME)
public @interface PermitAll {
}

View File

@ -0,0 +1,18 @@
package cn.iocoder.mall.user.sdk.context;
/**
* Security 上下文
*/
public class SecurityContext {
private final Long uid;
public SecurityContext(Long uid) {
this.uid = uid;
}
public Long getUid() {
return uid;
}
}

View File

@ -0,0 +1,30 @@
package cn.iocoder.mall.user.sdk.context;
/**
* {@link SecurityContext} Holder
*
* 参考 spring security ThreadLocalSecurityContextHolderStrategy 简单实现
*/
public class SecurityContextHolder {
private static final ThreadLocal<SecurityContext> securityContext = new ThreadLocal<SecurityContext>();
public static void setContext(SecurityContext context) {
securityContext.set(context);
}
public static SecurityContext getContext() {
SecurityContext ctx = securityContext.get();
// 为空时设置一个空的进去
if (ctx == null) {
ctx = new SecurityContext(null);
securityContext.set(ctx);
}
return ctx;
}
public static void clear() {
securityContext.remove();
}
}

View File

@ -0,0 +1,66 @@
package cn.iocoder.mall.user.sdk.interceptor;
import cn.iocoder.common.framework.exception.ServiceException;
import cn.iocoder.mall.user.sdk.annotation.PermitAll;
import cn.iocoder.mall.user.sdk.context.SecurityContext;
import cn.iocoder.mall.user.sdk.context.SecurityContextHolder;
import cn.iocoder.mall.user.service.api.OAuth2Service;
import cn.iocoder.mall.user.service.api.dto.OAuth2AuthenticationDTO;
import com.alibaba.dubbo.config.annotation.Reference;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 安全拦截器
*/
@Component
public class SecurityInterceptor extends HandlerInterceptorAdapter {
@Reference
private OAuth2Service oauth2Service;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 校验访问令牌是否正确若正确返回授权信息
String accessToken = obtainAccess(request);
OAuth2AuthenticationDTO authentication = null;
if (accessToken != null) {
authentication = oauth2Service.checkToken(accessToken);
// 添加到 SecurityContext
SecurityContext context = new SecurityContext(authentication.getUid());
SecurityContextHolder.setContext(context);
}
// 校验是否需要已授权
HandlerMethod method = (HandlerMethod) handler;
boolean isPermitAll = method.hasMethodAnnotation(PermitAll.class);
if (!isPermitAll && authentication == null) {
throw new ServiceException(-1, "未授权"); // TODO 这里要改下
}
return super.preHandle(request, response, handler);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 清空 SecurityContext
SecurityContextHolder.clear();
}
private String obtainAccess(HttpServletRequest request) {
String authorization = request.getHeader("Authorization");
if (!StringUtils.hasText(authorization)) {
return null;
}
int index = authorization.indexOf("Bearer ");
if (index == -1) { // 未找到
return null;
}
return authorization.substring(index + 7).trim();
}
}

View File

@ -0,0 +1,6 @@
/**
* 提供 SDK 给其它服务使用如下功能
*
* 1. 通过 {@link } 拦截器
*/
package cn.iocoder.mall.user.sdk;

View File

@ -0,0 +1,22 @@
<?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">
<parent>
<artifactId>user</artifactId>
<groupId>cn.iocoder.mall</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>user-service-api</artifactId>
<dependencies>
<dependency>
<groupId>cn.iocoder.mall</groupId>
<artifactId>common-framework</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,14 @@
package cn.iocoder.mall.user.service.api;
import cn.iocoder.common.framework.exception.ServiceException;
public interface MobileCodeService {
/**
* 发送验证码
*
* @param mobile 手机号
*/
void send(String mobile) throws ServiceException;
}

View File

@ -0,0 +1,35 @@
package cn.iocoder.mall.user.service.api;
import cn.iocoder.common.framework.exception.ServiceException;
import cn.iocoder.mall.user.service.api.dto.OAuth2AccessTokenBO;
import cn.iocoder.mall.user.service.api.dto.OAuth2AuthenticationDTO;
public interface OAuth2Service {
/**
* 使用手机号 + 验证码获取访问令牌等信息
*
* 如果手机未注册并且验证码正确进行自动注册
*
* @param mobile 手机号
* @param code 验证码
* @return 授权信息
*/
OAuth2AccessTokenBO getAccessToken(String mobile, String code)
throws ServiceException;
/**
* 校验访问令牌获取身份信息( 不包括 accessToken 等等 )
*
* @param accessToken 访问令牌
* @return 授权信息
*/
OAuth2AuthenticationDTO checkToken(String accessToken)
throws ServiceException;
// @see 刷新 token
// @see 移除 token
}

View File

@ -0,0 +1,17 @@
package cn.iocoder.mall.user.service.api;
import cn.iocoder.common.framework.exception.ServiceException;
import cn.iocoder.mall.user.service.api.dto.UserDTO;
public interface UserService {
/**
* 创建用户一般在用户注册时调用该方法
*
* @param mobile 手机号
* @param code 手机验证码
* @return 用户
*/
UserDTO createUser(String mobile, String code) throws ServiceException;
}

View File

@ -0,0 +1,13 @@
package cn.iocoder.mall.user.service.api.constant;
public class ThirdPlatformConstant {
public static final int QQ = 1;
public static final int WEIBO = 2;
// WECHAT 可能分成好几个
// ....
}

View File

@ -0,0 +1,49 @@
package cn.iocoder.mall.user.service.api.constant;
/**
* 错误码枚举类
*
* 用户中心使用 1-001-000-000
*/
public enum UserErrorCodeEnum {
// ========== OAUTH2 模块 ==========
OAUTH2_UNKNOWN(1001001000, "未知错误"), // 预留
OAUTH2_INVALID_GRANT_BAD_CREDENTIALS(1001001001, "密码不正确"), // 暂时没用到
OAUTH2_INVALID_GRANT_USERNAME_NOT_FOUND(1001001002, "账号不存在"), // 暂时没用到
OAUTH2_INVALID_GRANT(1001001010, ""), // 预留
OAUTH_INVALID_TOKEN_NOT_FOUND(1001001011, "访问令牌不存在"),
OAUTH_INVALID_TOKEN_EXPIRED(1001001012, "访问令牌已过期"),
OAUTH_INVALID_TOKEN_INVALID(1001001013, "访问令牌已失效"),
OAUTH_INVALID_TOKEN(1001001020, ""), // 预留
// ========== 用户模块 ==========
USER_MOBILE_NOT_REGISTERED(1001002000, "手机号未注册用户"),
USER_MOBILE_ALREADY_REGISTERED(1001002001, "手机号已经注册用户"),
// ========== 手机验证码模块 ==========
MOBILE_CODE_NOT_FOUND(1001003000, "验证码不存在"),
MOBILE_CODE_EXPIRED(1001003001, "验证码已过期"),
MOBILE_CODE_USED(1001003002, "验证码已使用"),
MOBILE_CODE_NOT_CORRECT(1001003003, "验证码不正确"),
MOBILE_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY(1001003004, "超过每日短信发送数量"),
MOBILE_CODE_SEND_TOO_FAST(1001003005, "短信发送过于频率")
;
private final int code;
private final String message;
UserErrorCodeEnum(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}

View File

@ -0,0 +1,15 @@
package cn.iocoder.mall.user.service.api.dto;
public class MobileCodeDTO {
private String code;
public String getCode() {
return code;
}
public MobileCodeDTO setCode(String code) {
this.code = code;
return this;
}
}

View File

@ -0,0 +1,47 @@
package cn.iocoder.mall.user.service.api.dto;
import java.io.Serializable;
public class OAuth2AccessTokenBO implements Serializable {
/**
* 访问令牌
*/
private String accessToken;
/**
* 刷新令牌
*/
private String refreshToken;
/**
* 过期时间单位
*/
private Integer expiresIn;
public String getAccessToken() {
return accessToken;
}
public OAuth2AccessTokenBO setAccessToken(String accessToken) {
this.accessToken = accessToken;
return this;
}
public String getRefreshToken() {
return refreshToken;
}
public OAuth2AccessTokenBO setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
return this;
}
public Integer getExpiresIn() {
return expiresIn;
}
public OAuth2AccessTokenBO setExpiresIn(Integer expiresIn) {
this.expiresIn = expiresIn;
return this;
}
}

View File

@ -0,0 +1,21 @@
package cn.iocoder.mall.user.service.api.dto;
import java.io.Serializable;
public class OAuth2AuthenticationDTO implements Serializable {
/**
* 用户编号
*/
private Long uid;
public Long getUid() {
return uid;
}
public OAuth2AuthenticationDTO setUid(Long uid) {
this.uid = uid;
return this;
}
}

View File

@ -0,0 +1,32 @@
package cn.iocoder.mall.user.service.api.dto;
public class UserDTO {
/**
* 用户编号
*/
private Long uid;
/**
* 手机号
*/
private String mobile;
public Long getUid() {
return uid;
}
public UserDTO setUid(Long uid) {
this.uid = uid;
return this;
}
public String getMobile() {
return mobile;
}
public UserDTO setMobile(String mobile) {
this.mobile = mobile;
return this;
}
}

View File

@ -0,0 +1,51 @@
<?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">
<parent>
<artifactId>user</artifactId>
<groupId>cn.iocoder.mall</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>user-service-impl</artifactId>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.6.5</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>cn.iocoder.mall</groupId>
<artifactId>user-service-api</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.3.0.Final</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,14 @@
package cn.iocoder.mall.user.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@MapperScan("cn.iocoder.mall.user.dao") // 扫描对应的 Mapper 接口
@EnableTransactionManagement(proxyTargetClass = true) // 启动事务管理为什么使用 proxyTargetClass 参数参见 https://blog.csdn.net/huang_550/article/details/76492600
public class DatabaseConfiguration {
// 数据源使用 HikariCP
}

View File

@ -0,0 +1,21 @@
package cn.iocoder.mall.user.convert;
import cn.iocoder.mall.user.dataobject.OAuth2AccessTokenDO;
import cn.iocoder.mall.user.service.api.dto.OAuth2AccessTokenBO;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
public interface OAuth2Convert {
OAuth2Convert INSTANCE = Mappers.getMapper(OAuth2Convert.class);
@Mappings({})
OAuth2AccessTokenBO convert(OAuth2AccessTokenDO oauth2AccessTokenDO);
default OAuth2AccessTokenBO convertWithExpiresIn(OAuth2AccessTokenDO oauth2AccessTokenDO) {
OAuth2AccessTokenBO bo = this.convert(oauth2AccessTokenDO);
bo.setExpiresIn(Math.max((int) ((oauth2AccessTokenDO.getExpiresTime().getTime() - System.currentTimeMillis()) / 1000), 0));
return bo;
}
}

View File

@ -0,0 +1,26 @@
package cn.iocoder.mall.user.dao;
import cn.iocoder.mall.user.dataobject.MobileCodeDO;
import org.springframework.stereotype.Repository;
@Repository // 实际不加也没问entity就是不想 IDEA 那看到有个报错
public interface MobileCodeMapper {
void insert(MobileCodeDO entity);
/**
* 更新手机验证码
*
* @param entity 更新信息
*/
void update(MobileCodeDO entity);
/**
* 获得手机号的最后一个手机验证码
*
* @param mobile 手机号
* @return 手机验证码
*/
MobileCodeDO selectLast1ByMobile(String mobile);
}

View File

@ -0,0 +1,13 @@
package cn.iocoder.mall.user.dao;
import cn.iocoder.mall.user.dataobject.OAuth2AccessTokenDO;
import org.springframework.stereotype.Repository;
@Repository
public interface OAuth2AccessTokenMapper {
void insert(OAuth2AccessTokenDO entity);
OAuth2AccessTokenDO selectByTokenId(String tokenId);
}

View File

@ -0,0 +1,11 @@
package cn.iocoder.mall.user.dao;
import cn.iocoder.mall.user.dataobject.OAuth2RefreshTokenDO;
import org.springframework.stereotype.Repository;
@Repository
public interface OAuth2RefreshTokenMapper {
void insert(OAuth2RefreshTokenDO entity);
}

View File

@ -0,0 +1,13 @@
package cn.iocoder.mall.user.dao;
import cn.iocoder.mall.user.dataobject.UserDO;
import org.springframework.stereotype.Repository;
@Repository
public interface UserMapper {
void insert(UserDO entity);
UserDO selectByMobile(String mobile);
}

View File

@ -0,0 +1,11 @@
package cn.iocoder.mall.user.dao;
import cn.iocoder.mall.user.dataobject.UserRegisterDO;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRegisterMapper {
void insert(UserRegisterDO entity);
}

View File

@ -0,0 +1,113 @@
package cn.iocoder.mall.user.dataobject;
import java.util.Date;
// TODO 优化IP
public class MobileCodeDO {
/**
* 编号
*/
private Long id;
/**
* 手机号
*/
private String mobile;
/**
* 验证码
*/
private String code;
/**
* 今日发送的第几条
*/
private Integer todayIndex;
/**
* 是否使用
*/
private Boolean used;
/**
* 注册的用户编号
*/
private Long usedUid;
/**
* 创建时间
*/
private Date createTime;
/**
* 使用时间
*/
private Date usedTime;
public Long getId() {
return id;
}
public MobileCodeDO setId(Long id) {
this.id = id;
return this;
}
public String getMobile() {
return mobile;
}
public MobileCodeDO setMobile(String mobile) {
this.mobile = mobile;
return this;
}
public String getCode() {
return code;
}
public MobileCodeDO setCode(String code) {
this.code = code;
return this;
}
public Integer getTodayIndex() {
return todayIndex;
}
public MobileCodeDO setTodayIndex(Integer todayIndex) {
this.todayIndex = todayIndex;
return this;
}
public Boolean getUsed() {
return used;
}
public MobileCodeDO setUsed(Boolean used) {
this.used = used;
return this;
}
public Long getUsedUid() {
return usedUid;
}
public MobileCodeDO setUsedUid(Long usedUid) {
this.usedUid = usedUid;
return this;
}
public Date getCreateTime() {
return createTime;
}
public MobileCodeDO setCreateTime(Date createTime) {
this.createTime = createTime;
return this;
}
public Date getUsedTime() {
return usedTime;
}
public MobileCodeDO setUsedTime(Date usedTime) {
this.usedTime = usedTime;
return this;
}
}

View File

@ -0,0 +1,85 @@
package cn.iocoder.mall.user.dataobject;
import java.util.Date;
public class OAuth2AccessTokenDO {
/**
* 访问令牌
*/
private String tokenId;
/**
* 刷新令牌
*/
private String refreshToken;
/**
* 用户编号
*/
private Long uid;
/**
* 过期时间
*/
private Date expiresTime;
/**
* 是否有效
*/
private Boolean valid;
/**
* 创建时间
*/
private Date createTime;
public String getTokenId() {
return tokenId;
}
public OAuth2AccessTokenDO setTokenId(String tokenId) {
this.tokenId = tokenId;
return this;
}
public String getRefreshToken() {
return refreshToken;
}
public OAuth2AccessTokenDO setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
return this;
}
public Long getUid() {
return uid;
}
public OAuth2AccessTokenDO setUid(Long uid) {
this.uid = uid;
return this;
}
public Date getExpiresTime() {
return expiresTime;
}
public OAuth2AccessTokenDO setExpiresTime(Date expiresTime) {
this.expiresTime = expiresTime;
return this;
}
public Boolean getValid() {
return valid;
}
public OAuth2AccessTokenDO setValid(Boolean valid) {
this.valid = valid;
return this;
}
public Date getCreateTime() {
return createTime;
}
public OAuth2AccessTokenDO setCreateTime(Date createTime) {
this.createTime = createTime;
return this;
}
}

View File

@ -0,0 +1,78 @@
package cn.iocoder.mall.user.dataobject;
import java.util.Date;
/**
* 刷新令牌
*
* idx_uid
*/
public class OAuth2RefreshTokenDO {
/**
* 刷新令牌
*/
private String tokenId;
/**
* 用户编号
*/
private Long uid;
/**
* 是否有效
*/
private Boolean valid;
/**
* 过期时间
*/
private Date expiresTime;
/**
* 创建时间
*/
private Date createTime;
public String getTokenId() {
return tokenId;
}
public OAuth2RefreshTokenDO setTokenId(String tokenId) {
this.tokenId = tokenId;
return this;
}
public Long getUid() {
return uid;
}
public OAuth2RefreshTokenDO setUid(Long uid) {
this.uid = uid;
return this;
}
public Boolean getValid() {
return valid;
}
public OAuth2RefreshTokenDO setValid(Boolean valid) {
this.valid = valid;
return this;
}
public Date getExpiresTime() {
return expiresTime;
}
public OAuth2RefreshTokenDO setExpiresTime(Date expiresTime) {
this.expiresTime = expiresTime;
return this;
}
public Date getCreateTime() {
return createTime;
}
public OAuth2RefreshTokenDO setCreateTime(Date createTime) {
this.createTime = createTime;
return this;
}
}

View File

@ -0,0 +1,51 @@
package cn.iocoder.mall.user.dataobject;
import java.util.Date;
/**
* 用户实体存储用户基本数据
*
* idx_mobile 唯一索引
*/
public class UserDO {
/**
* 用户编号
*/
private Long id;
/**
* 手机号
*/
private String mobile;
/**
* 创建时间
*/
private Date createTime;
public Long getId() {
return id;
}
public UserDO setId(Long id) {
this.id = id;
return this;
}
public String getMobile() {
return mobile;
}
public UserDO setMobile(String mobile) {
this.mobile = mobile;
return this;
}
public Date getCreateTime() {
return createTime;
}
public UserDO setCreateTime(Date createTime) {
this.createTime = createTime;
return this;
}
}

View File

@ -0,0 +1,4 @@
package cn.iocoder.mall.user.dataobject;
public class UserLoginLogDO {
}

View File

@ -0,0 +1,42 @@
package cn.iocoder.mall.user.dataobject;
import java.util.Date;
/**
* 用户注册信息
*/
public class UserRegisterDO {
/**
* 用户编号
*/
private Long id;
/**
* 创建时间
*/
private Date createTime;
// TODO 芋艿 ip
// TODO 芋艿 ua
// TODO 芋艿 方式手机注册qq 等等
public Long getId() {
return id;
}
public UserRegisterDO setId(Long id) {
this.id = id;
return this;
}
public Date getCreateTime() {
return createTime;
}
public UserRegisterDO setCreateTime(Date createTime) {
this.createTime = createTime;
return this;
}
}

View File

@ -0,0 +1,66 @@
package cn.iocoder.mall.user.dataobject;
import java.util.Date;
/**
* 用户三方开放平台授权例如QQ / 微博 / 微信等等
*/
public class UserThirdAuthDO {
/**
* 用户编号
*
* 外键 {@link UserDO#uid}
*/
private Long uid;
// ========== 授权相关字段
/**
* 用户的唯一标识
*/
private String openid;
/**
* 开放平台
*
* @see cn.iocoder.mall.user.constant.ThirdPlatformConstant
*/
private Integer platform;
/**
* 访问令牌
*/
private Date accessToken;
/**
* 过期时间
*/
private Date expireTime;
/**
* 刷新令牌
*/
private Date refreshToken;
/**
* 授权范围一般情况下使用逗号分隔
*/
private String scopes;
// ========== 基础信息
/**
* 用户昵称
*/
private String nickname;
/**
* 性别
*
* TODO 芋艿找地方统一枚举0-未知1-2-
*/
private Integer gender;
// TODO https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842
// TODO 芋艿其他字段国家/省份/城市/地区等
// TODO 芋艿头像
// TODO 芋艿微信独有 unionid
/**
* 统一存储基础信息使用 JSON 格式化避免未有效解析的情况
*/
private String extras;
}

View File

@ -0,0 +1 @@
package cn.iocoder.mall.user;

View File

@ -0,0 +1,104 @@
package cn.iocoder.mall.user.service;
import cn.iocoder.common.framework.util.ServiceExceptionUtil;
import cn.iocoder.mall.user.dao.MobileCodeMapper;
import cn.iocoder.mall.user.dataobject.MobileCodeDO;
import cn.iocoder.mall.user.service.api.MobileCodeService;
import cn.iocoder.mall.user.service.api.constant.UserErrorCodeEnum;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.Date;
/**
* MobileCodeService 实现用户登陆时需要的验证码
*/
@Service
@com.alibaba.dubbo.config.annotation.Service
public class MobileCodeServiceImpl implements MobileCodeService {
/**
* 每条验证码的过期时间单位毫秒
*/
@Value("${modules.mobile-code-service.code-expire-time-millis}")
private int codeExpireTimes;
/**
* 每日发送最大数量
*/
@Value("${modules.mobile-code-service.send-maximum-quantity-per-day}")
private int sendMaximumQuantityPerDay;
/**
* 短信发送频率单位毫秒
*/
@Value("${modules.mobile-code-service.send-frequency}")
private int sendFrequency;
@Autowired
private MobileCodeMapper mobileCodeMapper;
@Autowired
private UserServiceImpl userService;
/**
* 校验手机号的最后一个手机验证码是否有效
*
* @param mobile 手机号
* @param code 验证码
* @return 手机验证码信息
*/
public MobileCodeDO validLastMobileCode(String mobile, String code) {
MobileCodeDO mobileCodePO = mobileCodeMapper.selectLast1ByMobile(mobile);
if (mobileCodePO == null) { // 若验证码不存在抛出异常
throw ServiceExceptionUtil.exception(UserErrorCodeEnum.MOBILE_CODE_NOT_FOUND.getCode());
}
if (System.currentTimeMillis() - mobileCodePO.getCreateTime().getTime() >= codeExpireTimes) { // 验证码已过期
throw ServiceExceptionUtil.exception(UserErrorCodeEnum.MOBILE_CODE_EXPIRED.getCode());
}
if (mobileCodePO.getUsed()) { // 验证码已使用
throw ServiceExceptionUtil.exception(UserErrorCodeEnum.MOBILE_CODE_USED.getCode());
}
if (!mobileCodePO.getCode().equals(code)) {
throw ServiceExceptionUtil.exception(UserErrorCodeEnum.MOBILE_CODE_NOT_CORRECT.getCode());
}
return mobileCodePO;
}
/**
* 更新手机验证码已使用
*
* @param id 验证码编号
* @param uid 用户编号
*/
public void useMobileCode(Long id, Long uid) {
MobileCodeDO update = new MobileCodeDO().setId(id).setUsed(true).setUsedUid(uid).setUsedTime(new Date());
mobileCodeMapper.update(update);
}
public void send(String mobile) {
// TODO 芋艿校验手机格式
// 校验手机号码是否已经注册
if (userService.getUser(mobile) != null) {
throw ServiceExceptionUtil.exception(UserErrorCodeEnum.USER_MOBILE_ALREADY_REGISTERED.getCode());
}
// 校验是否可以发送验证码
MobileCodeDO lastMobileCodePO = mobileCodeMapper.selectLast1ByMobile(mobile);
if (lastMobileCodePO != null) {
if (lastMobileCodePO.getTodayIndex() >= sendMaximumQuantityPerDay) { // 超过当天发送的上限
throw ServiceExceptionUtil.exception(UserErrorCodeEnum.MOBILE_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY.getCode());
}
if (System.currentTimeMillis() - lastMobileCodePO.getCreateTime().getTime() < sendFrequency) { // 发送过于频繁
throw ServiceExceptionUtil.exception(UserErrorCodeEnum.MOBILE_CODE_SEND_TOO_FAST.getCode());
}
// TODO 提升每个 IP 每天可发送数量
// TODO 提升每个 IP 每小时可发送数量
}
// 创建验证码记录
MobileCodeDO newMobileCodePO = new MobileCodeDO().setMobile(mobile)
.setCode("9999") // TODO 芋艿随机 4 位验证码 or 6 位验证码
.setTodayIndex(lastMobileCodePO != null ? lastMobileCodePO.getTodayIndex() : 1)
.setUsed(false).setCreateTime(new Date());
mobileCodeMapper.insert(newMobileCodePO);
// TODO 发送验证码短信
}
}

View File

@ -0,0 +1,114 @@
package cn.iocoder.mall.user.service;
import cn.iocoder.common.framework.exception.ServiceException;
import cn.iocoder.common.framework.util.ServiceExceptionUtil;
import cn.iocoder.mall.user.convert.OAuth2Convert;
import cn.iocoder.mall.user.dao.OAuth2AccessTokenMapper;
import cn.iocoder.mall.user.dao.OAuth2RefreshTokenMapper;
import cn.iocoder.mall.user.dataobject.MobileCodeDO;
import cn.iocoder.mall.user.dataobject.OAuth2AccessTokenDO;
import cn.iocoder.mall.user.dataobject.OAuth2RefreshTokenDO;
import cn.iocoder.mall.user.dataobject.UserDO;
import cn.iocoder.mall.user.service.api.OAuth2Service;
import cn.iocoder.mall.user.service.api.constant.UserErrorCodeEnum;
import cn.iocoder.mall.user.service.api.dto.OAuth2AccessTokenBO;
import cn.iocoder.mall.user.service.api.dto.OAuth2AuthenticationDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.UUID;
/**
* OAuth2Service 实现用户授权相关的逻辑
*/
@Service
@com.alibaba.dubbo.config.annotation.Service
public class OAuth2ServiceImpl implements OAuth2Service {
/**
* 访问令牌过期时间单位毫秒
*/
@Value("${modules.oauth2-code-service.access-token-expire-time-millis}")
private int accessTokenExpireTimeMillis;
/**
* 刷新令牌过期时间单位毫秒
*/
@Value("${modules.oauth2-code-service.refresh-token-expire-time-millis}")
private int refreshTokenExpireTimeMillis;
@Autowired
private UserServiceImpl userService;
@Autowired
private MobileCodeServiceImpl mobileCodeService;
@Autowired
private OAuth2AccessTokenMapper oauth2AccessTokenMapper;
@Autowired
private OAuth2RefreshTokenMapper oauth2RefreshTokenMapper;
@Override
@Transactional
public OAuth2AccessTokenBO getAccessToken(String mobile, String code) {
// 校验手机号的最后一个手机验证码是否有效
MobileCodeDO mobileCodeDO = mobileCodeService.validLastMobileCode(mobile, code);
// 获取用户
UserDO userDO = userService.getUser(mobile);
if (userDO == null) { // 用户不存在
throw ServiceExceptionUtil.exception(UserErrorCodeEnum.USER_MOBILE_NOT_REGISTERED.getCode());
}
// 创建刷新令牌
OAuth2RefreshTokenDO oauth2RefreshTokenDO = createOAuth2RefreshToken(userDO.getId());
// 创建访问令牌
OAuth2AccessTokenDO oauth2AccessTokenDO = createOAuth2AccessToken(userDO.getId(), oauth2RefreshTokenDO.getTokenId());
// 标记已使用
mobileCodeService.useMobileCode(mobileCodeDO.getId(), userDO.getId());
// 转换返回
return OAuth2Convert.INSTANCE.convertWithExpiresIn(oauth2AccessTokenDO);
}
@Override
public OAuth2AuthenticationDTO checkToken(String accessToken) throws ServiceException {
OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenMapper.selectByTokenId(accessToken);
if (accessTokenDO == null) { // 不存在
throw ServiceExceptionUtil.exception(UserErrorCodeEnum.OAUTH_INVALID_TOKEN_NOT_FOUND.getCode());
}
if (accessTokenDO.getExpiresTime().getTime() < System.currentTimeMillis()) { // 已过期
throw ServiceExceptionUtil.exception(UserErrorCodeEnum.OAUTH_INVALID_TOKEN_EXPIRED.getCode());
}
if (!accessTokenDO.getValid()) { // 无效
throw ServiceExceptionUtil.exception(UserErrorCodeEnum.OAUTH_INVALID_TOKEN_INVALID.getCode());
}
// 转换返回
return new OAuth2AuthenticationDTO().setUid(accessTokenDO.getUid());
}
private OAuth2AccessTokenDO createOAuth2AccessToken(Long uid, String refreshToken) {
OAuth2AccessTokenDO accessToken = new OAuth2AccessTokenDO().setTokenId(generateAccessToken())
.setRefreshToken(refreshToken)
.setUid(uid)
.setExpiresTime(new Date(System.currentTimeMillis() + accessTokenExpireTimeMillis))
.setValid(true);
oauth2AccessTokenMapper.insert(accessToken);
return accessToken;
}
private OAuth2RefreshTokenDO createOAuth2RefreshToken(Long uid) {
OAuth2RefreshTokenDO refreshToken = new OAuth2RefreshTokenDO().setTokenId(generateRefreshToken())
.setUid(uid)
.setExpiresTime(new Date(System.currentTimeMillis() + refreshTokenExpireTimeMillis))
.setValid(true);
oauth2RefreshTokenMapper.insert(refreshToken);
return refreshToken;
}
private String generateAccessToken() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
private String generateRefreshToken() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
}

View File

@ -0,0 +1,60 @@
package cn.iocoder.mall.user.service;
import cn.iocoder.common.framework.util.ServiceExceptionUtil;
import cn.iocoder.mall.user.dao.UserMapper;
import cn.iocoder.mall.user.dao.UserRegisterMapper;
import cn.iocoder.mall.user.dataobject.UserDO;
import cn.iocoder.mall.user.dataobject.UserRegisterDO;
import cn.iocoder.mall.user.service.api.UserService;
import cn.iocoder.mall.user.service.api.constant.UserErrorCodeEnum;
import cn.iocoder.mall.user.service.api.dto.UserDTO;
import com.alibaba.dubbo.config.annotation.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
/**
* UserService 实现和用户信息相关的逻辑
*/
@org.springframework.stereotype.Service
@com.alibaba.dubbo.config.annotation.Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private UserRegisterMapper userRegisterMapper;
@Autowired
private MobileCodeServiceImpl mobileCodeService;
public UserDO getUser(String mobile) {
return userMapper.selectByMobile(mobile);
}
@Override
@Transactional
public UserDTO createUser(String mobile, String code) {
// TODO 芋艿校验手机格式
// 校验手机号的最后一个手机验证码是否有效
mobileCodeService.validLastMobileCode(mobile, code);
// 校验用户是否已经存在
if (getUser(mobile) != null) {
throw ServiceExceptionUtil.exception(UserErrorCodeEnum.USER_MOBILE_ALREADY_REGISTERED.getCode());
}
// 创建用户
UserDO userDO = new UserDO().setMobile(mobile).setCreateTime(new Date());
userMapper.insert(userDO);
// 插入注册信息
createUserRegister(userDO);
// 转换返回
return new UserDTO().setUid(userDO.getId());
}
private void createUserRegister(UserDO userDO) {
UserRegisterDO userRegisterDO = new UserRegisterDO().setId(userDO.getId())
.setCreateTime(new Date());
userRegisterMapper.insert(userRegisterDO);
}
}

View File

@ -0,0 +1,8 @@
##################### 业务模块 #####################
## MobileCodeService
modules.mobile-code-service.code-expire-time-millis = 600000
modules.mobile-code-service.send-maximum-quantity-per-day = 10
modules.mobile-code-service.send-frequency = 60000
## OAuth2CodeService
modules.oauth2-code-service.access-token-expire-time-millis = 2880000
modules.oauth2-code-service.refresh-token-expire-time-millis = 43200000

View File

@ -0,0 +1,32 @@
spring:
# datasource
datasource:
url: jdbc:mysql://127.0.0.1:33061/mall_user?useSSL=false
driver-class-name: com.mysql.jdbc.Driver
username: root
password: 123456
# server
server:
port: 8082
# mybatis
mybatis:
config-location: classpath:mybatis-config.xml
mapper-locations: classpath:mapper/*.xml
type-aliases-package: cn.iocoder.mall.user.dataobject
# dubbo
dubbo:
application:
name: user-service
registry:
address: zookeeper://127.0.0.1:2181
protocol:
port: -1
name: dubbo
scan:
base-packages: cn.iocoder.mall.user.service
demo:
service:
version: 1.0.0

View File

@ -0,0 +1,35 @@
<?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="cn.iocoder.mall.user.dao.MobileCodeMapper">
<insert id="insert" parameterType="MobileCodeDO">
INSERT INTO mobile_code (
id, mobile, code, today_index, used,
used_uid, used_time, create_time
) VALUES (
#{id}, #{mobile}, #{code}, #{todayIndex}, #{used},
#{usedUid}, #{usedTime}, #{createTime}
)
</insert>
<update id="update" parameterType="MobileCodeDO">
UPDATE mobile_code
<set>
<if test="used != null"> used = #{used}, </if>
<if test="usedUid != null"> used_uid = #{usedUid}, </if>
<if test="usedTime != null"> used_time = #{usedTime}, </if>
</set>
WHERE id = #{id}
</update>
<select id="selectLast1ByMobile" parameterType="String" resultType="MobileCodeDO">
SELECT
id, mobile, code, today_index, used,
used_uid, used_time, create_time
FROM mobile_code
WHERE mobile = #{mobile}
ORDER BY id DESC
LIMIT 1
</select>
</mapper>

View File

@ -0,0 +1,22 @@
<?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="cn.iocoder.mall.user.dao.OAuth2AccessTokenMapper">
<insert id="insert" parameterType="OAuth2AccessTokenDO">
INSERT INTO oauth2_access_token (
token_id, refresh_token, id, valid, expires_time,
create_time
) VALUES (
#{tokenId}, #{refreshToken}, #{id}, #{valid}, #{expiresTime},
#{createTime}
)
</insert>
<select id="selectByTokenId" parameterType="String" resultType="OAuth2AccessTokenDO">
SELECT
id, valid, expires_time
FROM oauth2_access_token
WHERE token_id = #{tokenId}
</select>
</mapper>

View File

@ -0,0 +1,13 @@
<?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="cn.iocoder.mall.user.dao.OAuth2RefreshTokenMapper">
<insert id="insert" parameterType="OAuth2RefreshTokenDO">
INSERT INTO oauth2_refresh_token (
token_id, id, valid, expires_time, create_time
) VALUES (
#{tokenId}, #{id}, #{valid}, #{expiresTime}, #{createTime}
)
</insert>
</mapper>

View File

@ -0,0 +1,20 @@
<?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="cn.iocoder.mall.user.dao.UserMapper">
<insert id="insert" parameterType="UserDO" useGeneratedKeys="true" keyProperty="id">
INSERT INTO users (
id, mobile, create_time
) VALUES (
#{id}, #{mobile}, #{createTime}
)
</insert>
<select id="selectByMobile" parameterType="String" resultType="UserDO">
SELECT
id, mobile
FROM users
WHERE mobile = #{mobile}
</select>
</mapper>

View File

@ -0,0 +1,13 @@
<?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="cn.iocoder.mall.user.dao.UserRegisterMapper">
<insert id="insert" parameterType="UserRegisterDO">
INSERT INTO user_register (
id, create_time
) VALUES (
#{id}, #{createTime}
)
</insert>
</mapper>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 使用驼峰命名法转换字段。 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<typeAliases>
<typeAlias alias="Integer" type="java.lang.Integer"/>
<typeAlias alias="Long" type="java.lang.Long"/>
<typeAlias alias="HashMap" type="java.util.HashMap"/>
<typeAlias alias="LinkedHashMap" type="java.util.LinkedHashMap"/>
<typeAlias alias="ArrayList" type="java.util.ArrayList"/>
<typeAlias alias="LinkedList" type="java.util.LinkedList"/>
</typeAliases>
</configuration>