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 + 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