diff --git a/common/common-framework/pom.xml b/common/common-framework/pom.xml
new file mode 100644
index 000000000..2f2a61192
--- /dev/null
+++ b/common/common-framework/pom.xml
@@ -0,0 +1,46 @@
+
+
+
+ common
+ cn.iocoder.mall
+ 1.0-SNAPSHOT
+
+ 4.0.0
+
+ common-framework
+
+
+ org.springframework
+ spring-web
+ 5.1.5.RELEASE
+
+
+ org.springframework
+ spring-web
+ 5.1.5.RELEASE
+ compile
+
+
+ org.springframework
+ spring-webmvc
+ 5.1.5.RELEASE
+
+
+
+ javax.servlet
+ servlet-api
+ 2.5
+ provided
+
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+
+
\ No newline at end of file
diff --git a/common/common-framework/src/main/java/cn/iocoder/common/framework/config/GlobalExceptionHandler.java b/common/common-framework/src/main/java/cn/iocoder/common/framework/config/GlobalExceptionHandler.java
new file mode 100644
index 000000000..fed118e8d
--- /dev/null
+++ b/common/common-framework/src/main/java/cn/iocoder/common/framework/config/GlobalExceptionHandler.java
@@ -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());
+ }
+
+}
\ No newline at end of file
diff --git a/product/product-application/src/main/java/cn/iocoder/mall/product/config/GlobalResponseBodyAdvice.java b/common/common-framework/src/main/java/cn/iocoder/common/framework/config/GlobalResponseBodyAdvice.java
similarity index 83%
rename from product/product-application/src/main/java/cn/iocoder/mall/product/config/GlobalResponseBodyAdvice.java
rename to common/common-framework/src/main/java/cn/iocoder/common/framework/config/GlobalResponseBodyAdvice.java
index 1ce33e816..decfcc14e 100644
--- a/product/product-application/src/main/java/cn/iocoder/mall/product/config/GlobalResponseBodyAdvice.java
+++ b/common/common-framework/src/main/java/cn/iocoder/common/framework/config/GlobalResponseBodyAdvice.java
@@ -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
diff --git a/product/product-application/src/main/java/cn/iocoder/mall/product/constants/ErrorCodeEnum.java b/common/common-framework/src/main/java/cn/iocoder/common/framework/constant/SysErrorCodeEnum.java
similarity index 78%
rename from product/product-application/src/main/java/cn/iocoder/mall/product/constants/ErrorCodeEnum.java
rename to common/common-framework/src/main/java/cn/iocoder/common/framework/constant/SysErrorCodeEnum.java
index ab28001f5..8c79c1579 100644
--- a/product/product-application/src/main/java/cn/iocoder/mall/product/constants/ErrorCodeEnum.java
+++ b/common/common-framework/src/main/java/cn/iocoder/common/framework/constant/SysErrorCodeEnum.java
@@ -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;
}
diff --git a/product/product-application/src/main/java/cn/iocoder/mall/product/exception/ServiceException.java b/common/common-framework/src/main/java/cn/iocoder/common/framework/exception/ServiceException.java
similarity index 95%
rename from product/product-application/src/main/java/cn/iocoder/mall/product/exception/ServiceException.java
rename to common/common-framework/src/main/java/cn/iocoder/common/framework/exception/ServiceException.java
index 695acccab..1bca1cd7c 100644
--- a/product/product-application/src/main/java/cn/iocoder/mall/product/exception/ServiceException.java
+++ b/common/common-framework/src/main/java/cn/iocoder/common/framework/exception/ServiceException.java
@@ -1,4 +1,4 @@
-package cn.iocoder.mall.product.exception;
+package cn.iocoder.common.framework.exception;
/**
* 服务异常
diff --git a/common/common-framework/src/main/java/cn/iocoder/common/framework/util/ServiceExceptionUtil.java b/common/common-framework/src/main/java/cn/iocoder/common/framework/util/ServiceExceptionUtil.java
new file mode 100644
index 000000000..d4db96532
--- /dev/null
+++ b/common/common-framework/src/main/java/cn/iocoder/common/framework/util/ServiceExceptionUtil.java
@@ -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 messages = new ConcurrentHashMap<>();
+
+ public static void putAll(Map 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();
+ }
+
+}
\ No newline at end of file
diff --git a/product/product-application/src/main/java/cn/iocoder/mall/product/vo/RestResult.java b/common/common-framework/src/main/java/cn/iocoder/common/framework/vo/RestResult.java
similarity index 96%
rename from product/product-application/src/main/java/cn/iocoder/mall/product/vo/RestResult.java
rename to common/common-framework/src/main/java/cn/iocoder/common/framework/vo/RestResult.java
index 8f02472b1..5e527b33d 100644
--- a/product/product-application/src/main/java/cn/iocoder/mall/product/vo/RestResult.java
+++ b/common/common-framework/src/main/java/cn/iocoder/common/framework/vo/RestResult.java
@@ -1,4 +1,4 @@
-package cn.iocoder.mall.product.vo;
+package cn.iocoder.common.framework.vo;
public class RestResult {
diff --git a/common/pom.xml b/common/pom.xml
new file mode 100644
index 000000000..400b2a215
--- /dev/null
+++ b/common/pom.xml
@@ -0,0 +1,19 @@
+
+
+
+ mall-parent
+ cn.iocoder.mall
+ 1.0-SNAPSHOT
+
+ 4.0.0
+
+ common
+ pom
+
+ common-framework
+
+
+
+
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index be5dd2ce6..a76b6db58 100644
--- a/pom.xml
+++ b/pom.xml
@@ -16,6 +16,8 @@
product
order
+ user
+ common
pom
diff --git a/product/product-application/pom.xml b/product/product-application/pom.xml
index 4fea6cd86..07bdef822 100644
--- a/product/product-application/pom.xml
+++ b/product/product-application/pom.xml
@@ -21,6 +21,11 @@
product-service-api
1.0-SNAPSHOT
+
+ cn.iocoder.mall
+ common-framework
+ 1.0-SNAPSHOT
+
org.springframework.boot
diff --git a/product/product-application/src/main/java/cn/iocoder/mall/product/config/GlobalExceptionHandler.java b/product/product-application/src/main/java/cn/iocoder/mall/product/config/GlobalExceptionHandler.java
deleted file mode 100644
index 1a8063b30..000000000
--- a/product/product-application/src/main/java/cn/iocoder/mall/product/config/GlobalExceptionHandler.java
+++ /dev/null
@@ -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());
- }
-
-}
\ No newline at end of file
diff --git a/product/product-application/src/main/java/cn/iocoder/mall/product/config/MVCConfiguration.java b/product/product-application/src/main/java/cn/iocoder/mall/product/config/MVCConfiguration.java
index 0015f58cc..c1b865c83 100644
--- a/product/product-application/src/main/java/cn/iocoder/mall/product/config/MVCConfiguration.java
+++ b/product/product-application/src/main/java/cn/iocoder/mall/product/config/MVCConfiguration.java
@@ -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
diff --git a/product/product-application/src/main/resources/application.yaml b/product/product-application/src/main/resources/application.yaml
index a200bc9fe..8c775d3fb 100644
--- a/product/product-application/src/main/resources/application.yaml
+++ b/product/product-application/src/main/resources/application.yaml
@@ -10,7 +10,7 @@ spring:
# server
server:
- port: 8081
+ port: 8080
# mybatis
mybatis:
diff --git a/user/pom.xml b/user/pom.xml
new file mode 100644
index 000000000..351a49a63
--- /dev/null
+++ b/user/pom.xml
@@ -0,0 +1,22 @@
+
+
+
+ mall-parent
+ cn.iocoder.mall
+ 1.0-SNAPSHOT
+
+ 4.0.0
+
+ user
+ pom
+
+ user-application
+ user-service-api
+ user-sdk
+ user-service-impl
+
+
+
+
\ No newline at end of file
diff --git a/user/user-application/pom.xml b/user/user-application/pom.xml
new file mode 100644
index 000000000..8e725f866
--- /dev/null
+++ b/user/user-application/pom.xml
@@ -0,0 +1,119 @@
+
+
+
+ user
+ cn.iocoder.mall
+ 1.0-SNAPSHOT
+
+ 4.0.0
+
+ user-application
+
+
+ 1.3.0.Final
+
+
+
+
+ cn.iocoder.mall
+ user-service-api
+ 1.0-SNAPSHOT
+
+
+ cn.iocoder.mall
+ user-service-impl
+ 1.0-SNAPSHOT
+
+
+
+ cn.iocoder.mall
+ common-framework
+ 1.0-SNAPSHOT
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ com.alibaba
+ dubbo
+ 2.6.5
+
+
+
+ com.alibaba.boot
+ dubbo-spring-boot-starter
+ 0.2.1.RELEASE
+
+
+
+ org.apache.curator
+ curator-framework
+ 2.12.0
+
+
+
+ org.mapstruct
+ mapstruct
+ ${org.mapstruct.version}
+
+
+
+
+ io.springfox
+ springfox-swagger2
+ 2.9.2
+
+
+ io.springfox
+ springfox-swagger-ui
+ 2.9.2
+
+
+ cn.iocoder.mall
+ user-service-api
+ 1.0-SNAPSHOT
+ compile
+
+
+ cn.iocoder.mall
+ user-sdk
+ 1.0-SNAPSHOT
+ compile
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.5.1
+
+
+ 1.8
+
+
+ org.mapstruct
+ mapstruct-processor
+ ${org.mapstruct.version}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/user/user-application/src/main/java/cn/iocoder/mall/user/UserApplication.java b/user/user-application/src/main/java/cn/iocoder/mall/user/UserApplication.java
new file mode 100644
index 000000000..894d44755
--- /dev/null
+++ b/user/user-application/src/main/java/cn/iocoder/mall/user/UserApplication.java
@@ -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);
+ }
+
+}
\ No newline at end of file
diff --git a/user/user-application/src/main/java/cn/iocoder/mall/user/config/MVCConfiguration.java b/user/user-application/src/main/java/cn/iocoder/mall/user/config/MVCConfiguration.java
new file mode 100644
index 000000000..4527c4f9f
--- /dev/null
+++ b/user/user-application/src/main/java/cn/iocoder/mall/user/config/MVCConfiguration.java
@@ -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);
+ }
+
+}
\ No newline at end of file
diff --git a/user/user-application/src/main/java/cn/iocoder/mall/user/config/ServiceExceptionConfiguration.java b/user/user-application/src/main/java/cn/iocoder/mall/user/config/ServiceExceptionConfiguration.java
new file mode 100644
index 000000000..7c5aff2ba
--- /dev/null
+++ b/user/user-application/src/main/java/cn/iocoder/mall/user/config/ServiceExceptionConfiguration.java
@@ -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());
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/user/user-application/src/main/java/cn/iocoder/mall/user/controller/PassportController.java b/user/user-application/src/main/java/cn/iocoder/mall/user/controller/PassportController.java
new file mode 100644
index 000000000..d77e00f1d
--- /dev/null
+++ b/user/user-application/src/main/java/cn/iocoder/mall/user/controller/PassportController.java
@@ -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
+}
\ No newline at end of file
diff --git a/user/user-application/src/main/java/cn/iocoder/mall/user/controller/UserController.java b/user/user-application/src/main/java/cn/iocoder/mall/user/controller/UserController.java
new file mode 100644
index 000000000..9c53ed6a5
--- /dev/null
+++ b/user/user-application/src/main/java/cn/iocoder/mall/user/controller/UserController.java
@@ -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;
+ }
+
+}
\ No newline at end of file
diff --git a/user/user-application/src/main/resources/application.yaml b/user/user-application/src/main/resources/application.yaml
new file mode 100644
index 000000000..bc897a3f1
--- /dev/null
+++ b/user/user-application/src/main/resources/application.yaml
@@ -0,0 +1,7 @@
+spring:
+ application:
+ name: user-application
+
+# server
+server:
+ port: 8082
\ No newline at end of file
diff --git a/user/user-sdk/pom.xml b/user/user-sdk/pom.xml
new file mode 100644
index 000000000..9aa1e6d7f
--- /dev/null
+++ b/user/user-sdk/pom.xml
@@ -0,0 +1,59 @@
+
+
+
+ user
+ cn.iocoder.mall
+ 1.0-SNAPSHOT
+
+ 4.0.0
+
+ user-sdk
+
+
+ org.springframework
+ spring-context
+ 5.1.5.RELEASE
+ compile
+
+
+ org.springframework
+ spring-webmvc
+ 5.1.5.RELEASE
+ compile
+
+
+ org.springframework
+ spring-webmvc
+ 5.1.5.RELEASE
+ compile
+
+
+ com.alibaba
+ dubbo
+ 2.6.5
+ compile
+
+
+ javax.servlet
+ servlet-api
+ 2.5
+ provided
+
+
+ cn.iocoder.mall
+ common-framework
+ 1.0-SNAPSHOT
+ compile
+
+
+ cn.iocoder.mall
+ user-service-api
+ 1.0-SNAPSHOT
+ compile
+
+
+
+
+
\ No newline at end of file
diff --git a/user/user-sdk/src/main/java/cn/iocoder/mall/user/sdk/annotation/PermitAll.java b/user/user-sdk/src/main/java/cn/iocoder/mall/user/sdk/annotation/PermitAll.java
new file mode 100644
index 000000000..06fa8415f
--- /dev/null
+++ b/user/user-sdk/src/main/java/cn/iocoder/mall/user/sdk/annotation/PermitAll.java
@@ -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 {
+}
\ No newline at end of file
diff --git a/user/user-sdk/src/main/java/cn/iocoder/mall/user/sdk/context/SecurityContext.java b/user/user-sdk/src/main/java/cn/iocoder/mall/user/sdk/context/SecurityContext.java
new file mode 100644
index 000000000..12f93bc69
--- /dev/null
+++ b/user/user-sdk/src/main/java/cn/iocoder/mall/user/sdk/context/SecurityContext.java
@@ -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;
+ }
+
+}
\ No newline at end of file
diff --git a/user/user-sdk/src/main/java/cn/iocoder/mall/user/sdk/context/SecurityContextHolder.java b/user/user-sdk/src/main/java/cn/iocoder/mall/user/sdk/context/SecurityContextHolder.java
new file mode 100644
index 000000000..a618c3591
--- /dev/null
+++ b/user/user-sdk/src/main/java/cn/iocoder/mall/user/sdk/context/SecurityContextHolder.java
@@ -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 = new ThreadLocal();
+
+ 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();
+ }
+
+}
\ No newline at end of file
diff --git a/user/user-sdk/src/main/java/cn/iocoder/mall/user/sdk/interceptor/SecurityInterceptor.java b/user/user-sdk/src/main/java/cn/iocoder/mall/user/sdk/interceptor/SecurityInterceptor.java
new file mode 100644
index 000000000..c207a4d41
--- /dev/null
+++ b/user/user-sdk/src/main/java/cn/iocoder/mall/user/sdk/interceptor/SecurityInterceptor.java
@@ -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();
+ }
+
+}
\ No newline at end of file
diff --git a/user/user-sdk/src/main/java/cn/iocoder/mall/user/sdk/package-info.java b/user/user-sdk/src/main/java/cn/iocoder/mall/user/sdk/package-info.java
new file mode 100644
index 000000000..656d91a8c
--- /dev/null
+++ b/user/user-sdk/src/main/java/cn/iocoder/mall/user/sdk/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * 提供 SDK 给其它服务,使用如下功能:
+ *
+ * 1. 通过 {@link } 拦截器,
+ */
+package cn.iocoder.mall.user.sdk;
\ No newline at end of file
diff --git a/user/user-service-api/pom.xml b/user/user-service-api/pom.xml
new file mode 100644
index 000000000..563b020b7
--- /dev/null
+++ b/user/user-service-api/pom.xml
@@ -0,0 +1,22 @@
+
+
+
+ user
+ cn.iocoder.mall
+ 1.0-SNAPSHOT
+
+ 4.0.0
+
+ user-service-api
+
+
+ cn.iocoder.mall
+ common-framework
+ 1.0-SNAPSHOT
+
+
+
+
+
\ No newline at end of file
diff --git a/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/MobileCodeService.java b/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/MobileCodeService.java
new file mode 100644
index 000000000..d7cc99969
--- /dev/null
+++ b/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/MobileCodeService.java
@@ -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;
+
+}
diff --git a/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/OAuth2Service.java b/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/OAuth2Service.java
new file mode 100644
index 000000000..c36aafef5
--- /dev/null
+++ b/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/OAuth2Service.java
@@ -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
+
+}
\ No newline at end of file
diff --git a/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/UserService.java b/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/UserService.java
new file mode 100644
index 000000000..ceeccb715
--- /dev/null
+++ b/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/UserService.java
@@ -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;
+
+}
\ No newline at end of file
diff --git a/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/constant/ThirdPlatformConstant.java b/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/constant/ThirdPlatformConstant.java
new file mode 100644
index 000000000..1e70e7f41
--- /dev/null
+++ b/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/constant/ThirdPlatformConstant.java
@@ -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 可能分成好几个
+
+ // ....
+
+}
diff --git a/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/constant/UserErrorCodeEnum.java b/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/constant/UserErrorCodeEnum.java
new file mode 100644
index 000000000..281852c69
--- /dev/null
+++ b/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/constant/UserErrorCodeEnum.java
@@ -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;
+ }
+
+}
\ No newline at end of file
diff --git a/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/dto/MobileCodeDTO.java b/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/dto/MobileCodeDTO.java
new file mode 100644
index 000000000..0b78623b9
--- /dev/null
+++ b/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/dto/MobileCodeDTO.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/dto/OAuth2AccessTokenBO.java b/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/dto/OAuth2AccessTokenBO.java
new file mode 100644
index 000000000..9723ff6d2
--- /dev/null
+++ b/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/dto/OAuth2AccessTokenBO.java
@@ -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;
+ }
+
+}
\ No newline at end of file
diff --git a/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/dto/OAuth2AuthenticationDTO.java b/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/dto/OAuth2AuthenticationDTO.java
new file mode 100644
index 000000000..ca9990b91
--- /dev/null
+++ b/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/dto/OAuth2AuthenticationDTO.java
@@ -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;
+ }
+
+}
\ No newline at end of file
diff --git a/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/dto/UserDTO.java b/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/dto/UserDTO.java
new file mode 100644
index 000000000..f331fd0ac
--- /dev/null
+++ b/user/user-service-api/src/main/java/cn/iocoder/mall/user/service/api/dto/UserDTO.java
@@ -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;
+ }
+
+}
\ No newline at end of file
diff --git a/user/user-service-impl/pom.xml b/user/user-service-impl/pom.xml
new file mode 100644
index 000000000..a4675d3a2
--- /dev/null
+++ b/user/user-service-impl/pom.xml
@@ -0,0 +1,51 @@
+
+
+
+ user
+ cn.iocoder.mall
+ 1.0-SNAPSHOT
+
+ 4.0.0
+
+ user-service-impl
+
+
+ com.alibaba
+ dubbo
+ 2.6.5
+ compile
+
+
+ cn.iocoder.mall
+ user-service-api
+ 1.0-SNAPSHOT
+ compile
+
+
+
+ mysql
+ mysql-connector-java
+
+
+ org.springframework.boot
+ spring-boot-starter-jdbc
+
+
+
+ org.mybatis.spring.boot
+ mybatis-spring-boot-starter
+ 2.0.0
+
+
+ org.mapstruct
+ mapstruct
+ 1.3.0.Final
+ compile
+
+
+
+
+
+
\ No newline at end of file
diff --git a/user/user-service-impl/src/main/java/cn/iocoder/mall/user/config/DatabaseConfiguration.java b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/config/DatabaseConfiguration.java
new file mode 100644
index 000000000..5c0cbf303
--- /dev/null
+++ b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/config/DatabaseConfiguration.java
@@ -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
+
+}
\ No newline at end of file
diff --git a/user/user-service-impl/src/main/java/cn/iocoder/mall/user/convert/OAuth2Convert.java b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/convert/OAuth2Convert.java
new file mode 100644
index 000000000..5a15dd206
--- /dev/null
+++ b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/convert/OAuth2Convert.java
@@ -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;
+ }
+
+}
\ No newline at end of file
diff --git a/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dao/MobileCodeMapper.java b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dao/MobileCodeMapper.java
new file mode 100644
index 000000000..03b13b420
--- /dev/null
+++ b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dao/MobileCodeMapper.java
@@ -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);
+
+}
\ No newline at end of file
diff --git a/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dao/OAuth2AccessTokenMapper.java b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dao/OAuth2AccessTokenMapper.java
new file mode 100644
index 000000000..81178bdbc
--- /dev/null
+++ b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dao/OAuth2AccessTokenMapper.java
@@ -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);
+
+}
\ No newline at end of file
diff --git a/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dao/OAuth2RefreshTokenMapper.java b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dao/OAuth2RefreshTokenMapper.java
new file mode 100644
index 000000000..dec6e75dd
--- /dev/null
+++ b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dao/OAuth2RefreshTokenMapper.java
@@ -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);
+
+}
\ No newline at end of file
diff --git a/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dao/UserMapper.java b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dao/UserMapper.java
new file mode 100644
index 000000000..e78ec15c8
--- /dev/null
+++ b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dao/UserMapper.java
@@ -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);
+
+}
\ No newline at end of file
diff --git a/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dao/UserRegisterMapper.java b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dao/UserRegisterMapper.java
new file mode 100644
index 000000000..3b586d8d3
--- /dev/null
+++ b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dao/UserRegisterMapper.java
@@ -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);
+
+}
\ No newline at end of file
diff --git a/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dataobject/MobileCodeDO.java b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dataobject/MobileCodeDO.java
new file mode 100644
index 000000000..a152a305a
--- /dev/null
+++ b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dataobject/MobileCodeDO.java
@@ -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;
+ }
+
+}
\ No newline at end of file
diff --git a/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dataobject/OAuth2AccessTokenDO.java b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dataobject/OAuth2AccessTokenDO.java
new file mode 100644
index 000000000..2164fd9c2
--- /dev/null
+++ b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dataobject/OAuth2AccessTokenDO.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dataobject/OAuth2RefreshTokenDO.java b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dataobject/OAuth2RefreshTokenDO.java
new file mode 100644
index 000000000..46724ddd2
--- /dev/null
+++ b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dataobject/OAuth2RefreshTokenDO.java
@@ -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;
+ }
+
+}
diff --git a/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dataobject/UserDO.java b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dataobject/UserDO.java
new file mode 100644
index 000000000..45bcb6e7d
--- /dev/null
+++ b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dataobject/UserDO.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dataobject/UserLoginLogDO.java b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dataobject/UserLoginLogDO.java
new file mode 100644
index 000000000..b716894b7
--- /dev/null
+++ b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dataobject/UserLoginLogDO.java
@@ -0,0 +1,4 @@
+package cn.iocoder.mall.user.dataobject;
+
+public class UserLoginLogDO {
+}
diff --git a/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dataobject/UserRegisterDO.java b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dataobject/UserRegisterDO.java
new file mode 100644
index 000000000..4c5c2c396
--- /dev/null
+++ b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dataobject/UserRegisterDO.java
@@ -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;
+ }
+
+}
\ No newline at end of file
diff --git a/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dataobject/UserThirdAuthDO.java b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dataobject/UserThirdAuthDO.java
new file mode 100644
index 000000000..ef7de1da2
--- /dev/null
+++ b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/dataobject/UserThirdAuthDO.java
@@ -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;
+
+}
\ No newline at end of file
diff --git a/user/user-service-impl/src/main/java/cn/iocoder/mall/user/package-info.java b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/package-info.java
new file mode 100644
index 000000000..85f6bcf32
--- /dev/null
+++ b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/package-info.java
@@ -0,0 +1 @@
+package cn.iocoder.mall.user;
\ No newline at end of file
diff --git a/user/user-service-impl/src/main/java/cn/iocoder/mall/user/service/MobileCodeServiceImpl.java b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/service/MobileCodeServiceImpl.java
new file mode 100644
index 000000000..75f97cd02
--- /dev/null
+++ b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/service/MobileCodeServiceImpl.java
@@ -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 发送验证码短信
+ }
+
+}
\ No newline at end of file
diff --git a/user/user-service-impl/src/main/java/cn/iocoder/mall/user/service/OAuth2ServiceImpl.java b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/service/OAuth2ServiceImpl.java
new file mode 100644
index 000000000..5d8c612d0
--- /dev/null
+++ b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/service/OAuth2ServiceImpl.java
@@ -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("-", "");
+ }
+
+}
diff --git a/user/user-service-impl/src/main/java/cn/iocoder/mall/user/service/UserServiceImpl.java b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/service/UserServiceImpl.java
new file mode 100644
index 000000000..f67d0b787
--- /dev/null
+++ b/user/user-service-impl/src/main/java/cn/iocoder/mall/user/service/UserServiceImpl.java
@@ -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);
+ }
+
+}
\ No newline at end of file
diff --git a/user/user-service-impl/src/main/resources/config/application.properties b/user/user-service-impl/src/main/resources/config/application.properties
new file mode 100644
index 000000000..529623d82
--- /dev/null
+++ b/user/user-service-impl/src/main/resources/config/application.properties
@@ -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
\ No newline at end of file
diff --git a/user/user-service-impl/src/main/resources/config/application.yaml b/user/user-service-impl/src/main/resources/config/application.yaml
new file mode 100644
index 000000000..86d0d17cc
--- /dev/null
+++ b/user/user-service-impl/src/main/resources/config/application.yaml
@@ -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
\ No newline at end of file
diff --git a/user/user-service-impl/src/main/resources/mapper/MobileCodeMapper.xml b/user/user-service-impl/src/main/resources/mapper/MobileCodeMapper.xml
new file mode 100644
index 000000000..f2676639e
--- /dev/null
+++ b/user/user-service-impl/src/main/resources/mapper/MobileCodeMapper.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+ 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}
+ )
+
+
+
+ UPDATE mobile_code
+
+ used = #{used},
+ used_uid = #{usedUid},
+ used_time = #{usedTime},
+
+ WHERE id = #{id}
+
+
+
+
+
\ No newline at end of file
diff --git a/user/user-service-impl/src/main/resources/mapper/OAuth2AccessTokenMapper.xml b/user/user-service-impl/src/main/resources/mapper/OAuth2AccessTokenMapper.xml
new file mode 100644
index 000000000..4ded75083
--- /dev/null
+++ b/user/user-service-impl/src/main/resources/mapper/OAuth2AccessTokenMapper.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+ INSERT INTO oauth2_access_token (
+ token_id, refresh_token, id, valid, expires_time,
+ create_time
+ ) VALUES (
+ #{tokenId}, #{refreshToken}, #{id}, #{valid}, #{expiresTime},
+ #{createTime}
+ )
+
+
+
+
+
\ No newline at end of file
diff --git a/user/user-service-impl/src/main/resources/mapper/OAuth2RefreshTokenMapper.xml b/user/user-service-impl/src/main/resources/mapper/OAuth2RefreshTokenMapper.xml
new file mode 100644
index 000000000..117ee94ba
--- /dev/null
+++ b/user/user-service-impl/src/main/resources/mapper/OAuth2RefreshTokenMapper.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+ INSERT INTO oauth2_refresh_token (
+ token_id, id, valid, expires_time, create_time
+ ) VALUES (
+ #{tokenId}, #{id}, #{valid}, #{expiresTime}, #{createTime}
+ )
+
+
+
\ No newline at end of file
diff --git a/user/user-service-impl/src/main/resources/mapper/UserMapper.xml b/user/user-service-impl/src/main/resources/mapper/UserMapper.xml
new file mode 100644
index 000000000..48897afae
--- /dev/null
+++ b/user/user-service-impl/src/main/resources/mapper/UserMapper.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+ INSERT INTO users (
+ id, mobile, create_time
+ ) VALUES (
+ #{id}, #{mobile}, #{createTime}
+ )
+
+
+
+
+
\ No newline at end of file
diff --git a/user/user-service-impl/src/main/resources/mapper/UserRegisterMapper.xml b/user/user-service-impl/src/main/resources/mapper/UserRegisterMapper.xml
new file mode 100644
index 000000000..0db1ebabd
--- /dev/null
+++ b/user/user-service-impl/src/main/resources/mapper/UserRegisterMapper.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+ INSERT INTO user_register (
+ id, create_time
+ ) VALUES (
+ #{id}, #{createTime}
+ )
+
+
+
\ No newline at end of file
diff --git a/user/user-service-impl/src/main/resources/mybatis-config.xml b/user/user-service-impl/src/main/resources/mybatis-config.xml
new file mode 100644
index 000000000..7f604cc7e
--- /dev/null
+++ b/user/user-service-impl/src/main/resources/mybatis-config.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file