├── .idea ├── .gitignore ├── JavaSceneConfigState.xml ├── compiler.xml ├── dataSources.xml ├── encodings.xml ├── jarRepositories.xml ├── misc.xml ├── mybatisx │ └── templates.xml ├── uiDesigner.xml └── vcs.xml ├── LICENSE ├── pom.xml ├── shortlink-admin ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── enndfp │ │ │ └── shortlink │ │ │ └── admin │ │ │ ├── ShortLinkAdminApplication.java │ │ │ ├── common │ │ │ ├── constant │ │ │ │ └── RedisCacheConstant.java │ │ │ ├── convention │ │ │ │ ├── errorcode │ │ │ │ │ ├── ErrorCode.java │ │ │ │ │ └── IErrorCode.java │ │ │ │ ├── exception │ │ │ │ │ ├── AbstractException.java │ │ │ │ │ ├── ClientException.java │ │ │ │ │ ├── GlobalExceptionHandler.java │ │ │ │ │ ├── RemoteException.java │ │ │ │ │ └── ServerException.java │ │ │ │ └── result │ │ │ │ │ └── Result.java │ │ │ └── serialize │ │ │ │ └── PhoneDesensitizationSerializer.java │ │ │ ├── config │ │ │ ├── InterceptorConfig.java │ │ │ ├── MyBatisPlusConfig.java │ │ │ └── RbloomFilterConfig.java │ │ │ ├── context │ │ │ ├── UserContext.java │ │ │ └── UserContextDTO.java │ │ │ ├── controller │ │ │ ├── GroupController.java │ │ │ ├── LinkController.java │ │ │ └── UserController.java │ │ │ ├── dao │ │ │ ├── entity │ │ │ │ ├── GroupDO.java │ │ │ │ └── UserDO.java │ │ │ └── mapper │ │ │ │ ├── GroupMapper.java │ │ │ │ └── UserMapper.java │ │ │ ├── dto │ │ │ ├── req │ │ │ │ ├── group │ │ │ │ │ ├── GroupAddReqDTO.java │ │ │ │ │ ├── GroupSortReqDTO.java │ │ │ │ │ └── GroupUpdateReqDTO.java │ │ │ │ └── user │ │ │ │ │ ├── UserLoginReqDTO.java │ │ │ │ │ ├── UserRegisterReqDTO.java │ │ │ │ │ └── UserUpdateReqDTO.java │ │ │ └── resp │ │ │ │ ├── group │ │ │ │ └── GroupRespDTO.java │ │ │ │ └── user │ │ │ │ ├── UserActualRespDTO.java │ │ │ │ ├── UserLoginRespDTO.java │ │ │ │ └── UserRespDTO.java │ │ │ ├── interceptor │ │ │ └── UserContextInterceptor.java │ │ │ ├── remote │ │ │ └── dto │ │ │ │ ├── LinkRemoteService.java │ │ │ │ ├── req │ │ │ │ └── link │ │ │ │ │ ├── LinkCreateReqDTO.java │ │ │ │ │ └── LinkPageReqDTO.java │ │ │ │ └── resp │ │ │ │ └── link │ │ │ │ ├── LinkCountRespDTO.java │ │ │ │ ├── LinkCreateRespDTO.java │ │ │ │ └── LinkPageRespDTO.java │ │ │ ├── service │ │ │ ├── GroupService.java │ │ │ ├── UserService.java │ │ │ └── impl │ │ │ │ ├── GroupServiceImpl.java │ │ │ │ └── UserServiceImpl.java │ │ │ └── utils │ │ │ ├── RandomStringUtil.java │ │ │ ├── ResultUtil.java │ │ │ └── ThrowUtil.java │ └── resources │ │ ├── application.yml │ │ ├── shardingsphere-config-dev.yml │ │ └── shardingsphere-config-prod.yml │ └── test │ └── java │ └── com │ └── enndfp │ └── shortlink │ └── admin │ └── UserTableShardingTest.java ├── shortlink-gateway ├── pom.xml └── src │ └── main │ └── resources │ └── application.yml └── shortlink-project ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── enndfp │ │ └── shortlink │ │ └── project │ │ ├── ShortLinkApplication.java │ │ ├── common │ │ └── convention │ │ │ ├── errorcode │ │ │ ├── ErrorCode.java │ │ │ └── IErrorCode.java │ │ │ ├── exception │ │ │ ├── AbstractException.java │ │ │ ├── ClientException.java │ │ │ ├── GlobalExceptionHandler.java │ │ │ ├── RemoteException.java │ │ │ └── ServerException.java │ │ │ └── result │ │ │ └── Result.java │ │ ├── config │ │ ├── MyBatisPlusConfig.java │ │ └── RbloomFilterConfig.java │ │ ├── controller │ │ └── LinkController.java │ │ ├── dao │ │ ├── entity │ │ │ └── LinkDO.java │ │ └── mapper │ │ │ └── LinkMapper.java │ │ ├── dto │ │ ├── req │ │ │ └── link │ │ │ │ ├── LinkCreateReqDTO.java │ │ │ │ └── LinkPageReqDTO.java │ │ └── resp │ │ │ └── link │ │ │ ├── LinkCountRespDTO.java │ │ │ ├── LinkCreateRespDTO.java │ │ │ └── LinkPageRespDTO.java │ │ ├── service │ │ ├── LinkService.java │ │ └── impl │ │ │ └── LinkServiceImpl.java │ │ └── utils │ │ ├── HashUtil.java │ │ ├── ResultUtil.java │ │ └── ThrowUtil.java └── resources │ ├── application.yml │ ├── shardingsphere-config-dev.yml │ └── shardingsphere-config-prod.yml └── test └── java └── com └── enndfp └── shortlink └── project └── LinkTableShardingTest.java /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/JavaSceneConfigState.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.idea/dataSources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mysql.8 6 | true 7 | com.mysql.cj.jdbc.Driver 8 | jdbc:mysql://localhost:3306 9 | $ProjectFileDir$ 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/mybatisx/templates.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 85 | 86 | -------------------------------------------------------------------------------- /.idea/uiDesigner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Enndfp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.enndfp.shortlink 8 | shortlink 9 | pom 10 | 1.0-SNAPSHOT 11 | 12 | 13 | shortlink-admin 14 | shortlink-gateway 15 | shortlink-project 16 | 17 | 18 | 19 | 17 20 | 3.0.7 21 | 2022.0.3 22 | 2022.0.0.0-RC2 23 | 3.0.2 24 | 5.3.2 25 | 0.9.1 26 | 2.0.36 27 | 3.5.3.1 28 | 6.5.2 29 | 5.8.20 30 | 3.21.3 31 | 30.0-jre 32 | 33 | 34 | 35 | 36 | org.projectlombok 37 | lombok 38 | 39 | 40 | 41 | 42 | 43 | 44 | org.springframework.boot 45 | spring-boot-dependencies 46 | ${spring-boot.version} 47 | pom 48 | import 49 | 50 | 51 | 52 | org.springframework.cloud 53 | spring-cloud-dependencies 54 | ${spring-cloud.version} 55 | pom 56 | import 57 | 58 | 59 | 60 | com.alibaba.cloud 61 | spring-cloud-alibaba-dependencies 62 | ${spring-cloud-alibaba.version} 63 | pom 64 | import 65 | 66 | 67 | 68 | com.baomidou 69 | mybatis-plus-boot-starter 70 | ${mybatis-plus.version} 71 | 72 | 73 | 74 | org.apache.shardingsphere 75 | shardingsphere-jdbc-core 76 | ${shardingsphere.version} 77 | 78 | 79 | 80 | io.jsonwebtoken 81 | jjwt 82 | ${jjwt.version} 83 | 84 | 85 | 86 | com.alibaba.fastjson2 87 | fastjson2 88 | ${fastjson2.version} 89 | 90 | 91 | 92 | com.github.dozermapper 93 | dozer-core 94 | ${dozer-core.version} 95 | 96 | 97 | 98 | cn.hutool 99 | hutool-all 100 | ${hutool-all.version} 101 | 102 | 103 | 104 | org.redisson 105 | redisson-spring-boot-starter 106 | ${redisson.version} 107 | 108 | 109 | 110 | com.google.guava 111 | guava 112 | ${guava.version} 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | org.apache.maven.plugins 121 | maven-compiler-plugin 122 | 3.6.1 123 | 124 | 17 125 | 17 126 | 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /shortlink-admin/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | shortlink 7 | com.enndfp.shortlink 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | shortlink-admin 13 | 14 | 15 | 16 | org.springframework.boot 17 | spring-boot-starter-web 18 | 19 | 20 | 21 | com.baomidou 22 | mybatis-plus-boot-starter 23 | 24 | 25 | 26 | com.mysql 27 | mysql-connector-j 28 | runtime 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-jdbc 34 | 35 | 36 | 37 | cn.hutool 38 | hutool-all 39 | 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-starter-data-redis 44 | 45 | 46 | 47 | org.redisson 48 | redisson-spring-boot-starter 49 | 50 | 51 | 52 | org.apache.shardingsphere 53 | shardingsphere-jdbc-core 54 | 55 | 56 | 57 | com.alibaba.fastjson2 58 | fastjson2 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/ShortLinkAdminApplication.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin; 2 | 3 | import org.mybatis.spring.annotation.MapperScan; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | 7 | /** 8 | * @author Enndfp 9 | */ 10 | @SpringBootApplication 11 | @MapperScan("com.enndfp.shortlink.admin.dao.mapper") 12 | public class ShortLinkAdminApplication { 13 | 14 | public static void main(String[] args) { 15 | SpringApplication.run(ShortLinkAdminApplication.class,args); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/common/constant/RedisCacheConstant.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.common.constant; 2 | 3 | /** 4 | * Redis缓存常量 5 | * 6 | * @author Enndfp 7 | */ 8 | public class RedisCacheConstant { 9 | 10 | /** 11 | * 用户注册锁 12 | */ 13 | public static final String LOCK_USER_REGISTER_KEY = "short-link:lock:user-register:"; 14 | 15 | /** 16 | * 用户登录token 17 | */ 18 | public static final String USER_LOGIN_KEY = "user:login:"; 19 | 20 | /** 21 | * 用户登录token过期时间 22 | */ 23 | public static final Long USER_LOGIN_TTL = 30L; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/common/convention/errorcode/ErrorCode.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.common.convention.errorcode; 2 | 3 | /** 4 | * 错误码定义 5 | * 6 | * @author Enndfp 7 | */ 8 | public enum ErrorCode implements IErrorCode { 9 | 10 | // ========== 一级宏观错误码 客户端错误 ========== 11 | CLIENT_ERROR("A000001", "客户端请求错误"), 12 | 13 | // ========== 二级宏观错误码 ========== 14 | USER_NAME_NULL("A000100", "用户名为空"), 15 | PASSWORD_NULL("A000101", "密码为空"), 16 | GROUP_NAME_NULL("A000201", "分组名称为空"), 17 | 18 | 19 | // ========== 一级宏观错误码 系统执行出错 ========== 20 | SERVICE_ERROR("B000001", "服务端错误"), 21 | 22 | // ========== 二级宏观错误码 ========== 23 | USER_NOT_EXIST("B000100", "用户不存在"), 24 | USER_NAME_EXIST("B000101", "用户名已存在"), 25 | USER_REGISTER_ERROR("B000102", "用户保存失败"), 26 | USER_INFO_UPDATE_ERROR("B000103", "用户信息更新失败"), 27 | USER_HAS_LOGIN("B000104", "用户已登录"), 28 | USER_OR_TOKEN_NULL("B000105", "用户不存在或token已过期"), 29 | USER_NOT_LOGIN("B000106", "用户未登录"), 30 | GROUP_SAVE_ERROR("B000201", "分组保存失败"), 31 | GROUP_UPDATE_ERROR("B000202", "分组修改失败"), 32 | GROUP_DELETE_ERROR("B000203", "分组删除失败"), 33 | GROUP_SORT_ERROR("B000203", "分组排序失败"), 34 | 35 | 36 | // ========== 一级宏观错误码 调用第三方服务出错 ========== 37 | REMOTE_ERROR("C000001", "调用第三方服务出错"); 38 | 39 | private final String code; 40 | 41 | private final String message; 42 | 43 | ErrorCode(String code, String message) { 44 | this.code = code; 45 | this.message = message; 46 | } 47 | 48 | @Override 49 | public String code() { 50 | return code; 51 | } 52 | 53 | @Override 54 | public String message() { 55 | return message; 56 | } 57 | } -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/common/convention/errorcode/IErrorCode.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.common.convention.errorcode; 2 | 3 | /** 4 | * 平台错误码 5 | * 6 | * @author Enndfp 7 | */ 8 | public interface IErrorCode { 9 | 10 | /** 11 | * 错误码 12 | * 13 | * @return 错误码 14 | */ 15 | String code(); 16 | 17 | /** 18 | * 错误信息 19 | * 20 | * @return 错误信息 21 | */ 22 | String message(); 23 | } -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/common/convention/exception/AbstractException.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.common.convention.exception; 2 | 3 | import com.enndfp.shortlink.admin.common.convention.errorcode.IErrorCode; 4 | import lombok.Getter; 5 | import org.springframework.util.StringUtils; 6 | 7 | import java.util.Optional; 8 | 9 | /** 10 | * 抽象项目中三类异常体系,客户端异常、服务端异常以及远程服务调用异常 11 | * 12 | * @author Enndfp 13 | * @see ClientException 14 | * @see ServerException 15 | * @see RemoteException 16 | */ 17 | @Getter 18 | public abstract class AbstractException extends RuntimeException { 19 | 20 | public final String errorCode; 21 | 22 | public final String errorMessage; 23 | 24 | public AbstractException(String message, Throwable throwable, IErrorCode errorCode) { 25 | super(message, throwable); 26 | this.errorCode = errorCode.code(); 27 | this.errorMessage = Optional.ofNullable(StringUtils.hasLength(message) ? message : null).orElse(errorCode.message()); 28 | } 29 | } -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/common/convention/exception/ClientException.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.common.convention.exception; 2 | 3 | import com.enndfp.shortlink.admin.common.convention.errorcode.ErrorCode; 4 | import com.enndfp.shortlink.admin.common.convention.errorcode.IErrorCode; 5 | 6 | /** 7 | * 客户端异常 8 | * 9 | * @author Enndfp 10 | */ 11 | public class ClientException extends AbstractException { 12 | 13 | public ClientException(IErrorCode errorCode) { 14 | this(null, null, errorCode); 15 | } 16 | 17 | public ClientException(IErrorCode errorCode,String message) { 18 | this(message, null, errorCode); 19 | } 20 | 21 | public ClientException(String message, Throwable throwable, IErrorCode errorCode) { 22 | super(message, throwable, errorCode); 23 | } 24 | 25 | @Override 26 | public String toString() { 27 | return "ClientException{" + 28 | "code='" + errorCode + "'," + 29 | "message='" + errorMessage + "'" + 30 | '}'; 31 | } 32 | } -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/common/convention/exception/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.common.convention.exception; 2 | 3 | import com.enndfp.shortlink.admin.common.convention.errorcode.ErrorCode; 4 | import com.enndfp.shortlink.admin.common.convention.result.Result; 5 | import com.enndfp.shortlink.admin.utils.ResultUtil; 6 | import lombok.SneakyThrows; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.validation.BindingResult; 9 | import org.springframework.validation.FieldError; 10 | import org.springframework.web.bind.MethodArgumentNotValidException; 11 | import org.springframework.web.bind.annotation.ExceptionHandler; 12 | import org.springframework.web.bind.annotation.RestControllerAdvice; 13 | 14 | import java.util.HashMap; 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | /** 19 | * 全局异常处理器 20 | * 21 | * @author Enndfp 22 | */ 23 | @Slf4j 24 | @RestControllerAdvice 25 | public class GlobalExceptionHandler { 26 | 27 | /** 28 | * 拦截参数验证异常 29 | */ 30 | @SneakyThrows 31 | @ExceptionHandler(MethodArgumentNotValidException.class) 32 | public Result methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException ex) { 33 | log.error("methodArgumentNotValidExceptionHandler: ", ex); 34 | BindingResult bindingResult = ex.getBindingResult(); 35 | Map errorMap = getErrors(bindingResult); 36 | return ResultUtil.failure(ErrorCode.SERVICE_ERROR, errorMap); 37 | } 38 | 39 | /** 40 | * 拦截自定义抛出的异常 41 | */ 42 | @ExceptionHandler(AbstractException.class) 43 | public Result abstractException(AbstractException ex) { 44 | log.error("abstractException: " + ex.getMessage(), ex); 45 | return ResultUtil.failure(ex.getErrorCode(), ex.getMessage()); 46 | } 47 | 48 | /** 49 | * 运行时异常捕获 50 | */ 51 | @ExceptionHandler(RuntimeException.class) 52 | public Result runtimeExceptionHandler(RuntimeException ex) { 53 | log.error("runtimeException: ", ex); 54 | return ResultUtil.failure(ErrorCode.SERVICE_ERROR, ex.getMessage()); 55 | } 56 | 57 | /** 58 | * 从方法参数校验异常中获取详细异常信息 59 | * 60 | * @param bindingResult 61 | * @return 62 | */ 63 | private Map getErrors(BindingResult bindingResult) { 64 | 65 | Map errorMap = new HashMap<>(16); 66 | List errorList = bindingResult.getFieldErrors(); 67 | 68 | for (FieldError error : errorList) { 69 | // 错误所对应的属性字段名 70 | String field = error.getField(); 71 | // 错误所对应的信息 72 | String message = error.getDefaultMessage(); 73 | errorMap.put(field, message); 74 | } 75 | return errorMap; 76 | } 77 | } -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/common/convention/exception/RemoteException.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.common.convention.exception; 2 | 3 | import com.enndfp.shortlink.admin.common.convention.errorcode.ErrorCode; 4 | import com.enndfp.shortlink.admin.common.convention.errorcode.IErrorCode; 5 | 6 | /** 7 | * 远程服务调用异常 8 | * 9 | * @author Enndfp 10 | */ 11 | public class RemoteException extends AbstractException { 12 | 13 | public RemoteException(IErrorCode errorCode) { 14 | this(null, null, errorCode); 15 | } 16 | 17 | public RemoteException(IErrorCode errorCode,String message) { 18 | this(message, null, errorCode); 19 | } 20 | 21 | public RemoteException(String message, Throwable throwable, IErrorCode errorCode) { 22 | super(message, throwable, errorCode); 23 | } 24 | 25 | @Override 26 | public String toString() { 27 | return "RemoteException{" + 28 | "code='" + errorCode + "'," + 29 | "message='" + errorMessage + "'" + 30 | '}'; 31 | } 32 | } -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/common/convention/exception/ServerException.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.common.convention.exception; 2 | 3 | import com.enndfp.shortlink.admin.common.convention.errorcode.ErrorCode; 4 | import com.enndfp.shortlink.admin.common.convention.errorcode.IErrorCode; 5 | 6 | import java.util.Optional; 7 | 8 | /** 9 | * 服务端异常 10 | * 11 | * @author Enndfp 12 | */ 13 | public class ServerException extends AbstractException { 14 | 15 | public ServerException(IErrorCode errorCode,String message) { 16 | this(message, null, errorCode); 17 | } 18 | 19 | public ServerException(IErrorCode errorCode) { 20 | this(null, errorCode); 21 | } 22 | 23 | public ServerException(String message, IErrorCode errorCode) { 24 | this(message, null, errorCode); 25 | } 26 | 27 | public ServerException(String message, Throwable throwable, IErrorCode errorCode) { 28 | super(Optional.ofNullable(message).orElse(errorCode.message()), throwable, errorCode); 29 | } 30 | 31 | @Override 32 | public String toString() { 33 | return "ServerException{" + 34 | "code='" + errorCode + "'," + 35 | "message='" + errorMessage + "'" + 36 | '}'; 37 | } 38 | } -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/common/convention/result/Result.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.common.convention.result; 2 | 3 | import com.enndfp.shortlink.admin.common.convention.errorcode.IErrorCode; 4 | import lombok.Data; 5 | import lombok.experimental.Accessors; 6 | 7 | import java.io.Serial; 8 | import java.io.Serializable; 9 | 10 | /** 11 | * 全局返回对象 12 | * 13 | * @author Enndfp 14 | */ 15 | @Data 16 | @Accessors(chain = true) 17 | public class Result implements Serializable { 18 | 19 | @Serial 20 | private static final long serialVersionUID = 5679018624309023727L; 21 | 22 | /** 23 | * 返回码 24 | */ 25 | private String code; 26 | 27 | /** 28 | * 返回消息 29 | */ 30 | private String message; 31 | 32 | /** 33 | * 响应数据 34 | */ 35 | private T data; 36 | 37 | /** 38 | * 请求ID 39 | */ 40 | private String requestId; 41 | 42 | public Result(String code, String message, T data, String requestId) { 43 | this.code = code; 44 | this.message = message; 45 | this.data = data; 46 | this.requestId = requestId; 47 | } 48 | 49 | public Result(IErrorCode iErrorCode) { 50 | this(iErrorCode.code(), iErrorCode.message(), null, null); 51 | } 52 | } -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/common/serialize/PhoneDesensitizationSerializer.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.common.serialize; 2 | 3 | import cn.hutool.core.util.DesensitizedUtil; 4 | import com.fasterxml.jackson.core.JsonGenerator; 5 | import com.fasterxml.jackson.databind.JsonSerializer; 6 | import com.fasterxml.jackson.databind.SerializerProvider; 7 | 8 | import java.io.IOException; 9 | 10 | /** 11 | * 手机号脱敏反序列化 12 | * 13 | * @author Enndfp 14 | */ 15 | public class PhoneDesensitizationSerializer extends JsonSerializer { 16 | 17 | @Override 18 | public void serialize(String phone, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { 19 | String phoneDesensitization = DesensitizedUtil.mobilePhone(phone); 20 | jsonGenerator.writeString(phoneDesensitization); 21 | } 22 | } -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/config/InterceptorConfig.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.config; 2 | 3 | import com.enndfp.shortlink.admin.interceptor.UserContextInterceptor; 4 | import jakarta.annotation.Resource; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.data.redis.core.StringRedisTemplate; 7 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 8 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 9 | 10 | import java.util.List; 11 | 12 | /** 13 | * 拦截器配置类 14 | * 15 | * @author Enndfp 16 | */ 17 | @Configuration 18 | public class InterceptorConfig implements WebMvcConfigurer { 19 | 20 | @Resource 21 | private StringRedisTemplate stringRedisTemplate; 22 | 23 | private static final List EXCLUDE_PATHS = List.of( 24 | "/user/login", 25 | "/user/register", 26 | "/user/check-username" 27 | ); 28 | 29 | @Override 30 | public void addInterceptors(InterceptorRegistry registry) { 31 | 32 | registry.addInterceptor(new UserContextInterceptor(stringRedisTemplate)) 33 | .excludePathPatterns(EXCLUDE_PATHS) 34 | .order(0); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/config/MyBatisPlusConfig.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.config; 2 | 3 | import com.baomidou.mybatisplus.annotation.DbType; 4 | import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; 5 | import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | /** 10 | * MyBatis Plus 配置 11 | * 12 | * @author Enndfp 13 | */ 14 | @Configuration 15 | public class MyBatisPlusConfig { 16 | 17 | /** 18 | * 拦截器配置 19 | */ 20 | @Bean 21 | public MybatisPlusInterceptor mybatisPlusInterceptor() { 22 | MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); 23 | // 分页插件 24 | interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); 25 | return interceptor; 26 | } 27 | } -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/config/RbloomFilterConfig.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.config; 2 | 3 | import org.redisson.api.RBloomFilter; 4 | import org.redisson.api.RedissonClient; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | /** 9 | * 布隆过滤器配置 10 | * 11 | * @author Enndfp 12 | */ 13 | @Configuration 14 | public class RbloomFilterConfig { 15 | 16 | /** 17 | * 防止用户注册查询数据库的布隆过滤器 18 | */ 19 | @Bean 20 | public RBloomFilter userRegisterCachePenetrationBloomFilter(RedissonClient redissonClient) { 21 | // 1. 创建布隆过滤器对象 22 | RBloomFilter cachePenetrationBloomFilter = redissonClient.getBloomFilter("userRegisterCachePenetrationBloomFilter"); 23 | // 2. 初始化布隆过滤器(预计元素数量为100000000L,误差率为0.001) 24 | cachePenetrationBloomFilter.tryInit(100000000L, 0.001); 25 | 26 | return cachePenetrationBloomFilter; 27 | } 28 | 29 | /** 30 | * 防止Gid生成重复查询数据库的布隆过滤器 31 | */ 32 | @Bean 33 | public RBloomFilter gidGenerateCachePenetrationBloomFilter(RedissonClient redissonClient) { 34 | // 1. 创建布隆过滤器对象 35 | RBloomFilter cachePenetrationBloomFilter = redissonClient.getBloomFilter("gidGenerateCachePenetrationBloomFilter"); 36 | // 2. 初始化布隆过滤器(预计元素数量为100000000L,误差率为0.001) 37 | cachePenetrationBloomFilter.tryInit(100000000L, 0.001); 38 | 39 | return cachePenetrationBloomFilter; 40 | } 41 | } -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/context/UserContext.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.context; 2 | 3 | import com.alibaba.ttl.TransmittableThreadLocal; 4 | 5 | import java.util.Optional; 6 | 7 | /** 8 | * 用户上下文 9 | * 10 | * @author Enndfp 11 | */ 12 | public final class UserContext { 13 | 14 | private static final ThreadLocal USER_THREAD_LOCAL = new TransmittableThreadLocal<>(); 15 | 16 | /** 17 | * 设置用户至上下文 18 | * 19 | * @param user 用户详情信息 20 | */ 21 | public static void setUser(UserContextDTO user) { 22 | USER_THREAD_LOCAL.set(user); 23 | } 24 | 25 | /** 26 | * 获取上下文中用户 ID 27 | * 28 | * @return 用户 ID 29 | */ 30 | public static String getUserId() { 31 | UserContextDTO userContextDTO = USER_THREAD_LOCAL.get(); 32 | return Optional.ofNullable(userContextDTO).map(UserContextDTO::getId).orElse(null); 33 | } 34 | 35 | /** 36 | * 获取上下文中用户名称 37 | * 38 | * @return 用户名称 39 | */ 40 | public static String getUsername() { 41 | UserContextDTO userContextDTO = USER_THREAD_LOCAL.get(); 42 | return Optional.ofNullable(userContextDTO).map(UserContextDTO::getUsername).orElse(null); 43 | } 44 | 45 | /** 46 | * 获取上下文中用户真实姓名 47 | * 48 | * @return 用户真实姓名 49 | */ 50 | public static String getRealName() { 51 | UserContextDTO userContextDTO = USER_THREAD_LOCAL.get(); 52 | return Optional.ofNullable(userContextDTO).map(UserContextDTO::getRealName).orElse(null); 53 | } 54 | 55 | /** 56 | * 清理用户上下文 57 | */ 58 | public static void removeUser() { 59 | USER_THREAD_LOCAL.remove(); 60 | } 61 | } -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/context/UserContextDTO.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.context; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * 用户上下文信息传输实体 7 | * 8 | * @author Enndfp 9 | */ 10 | @Data 11 | public class UserContextDTO { 12 | 13 | /** 14 | * 用户 ID 15 | */ 16 | private String id; 17 | 18 | /** 19 | * 用户名 20 | */ 21 | private String username; 22 | 23 | /** 24 | * 真实姓名 25 | */ 26 | private String realName; 27 | } -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/controller/GroupController.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.controller; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import com.enndfp.shortlink.admin.common.convention.errorcode.ErrorCode; 5 | import com.enndfp.shortlink.admin.common.convention.result.Result; 6 | import com.enndfp.shortlink.admin.dto.req.group.GroupAddReqDTO; 7 | import com.enndfp.shortlink.admin.dto.req.group.GroupSortReqDTO; 8 | import com.enndfp.shortlink.admin.dto.req.group.GroupUpdateReqDTO; 9 | import com.enndfp.shortlink.admin.dto.resp.group.GroupRespDTO; 10 | import com.enndfp.shortlink.admin.service.GroupService; 11 | import com.enndfp.shortlink.admin.utils.ResultUtil; 12 | import com.enndfp.shortlink.admin.utils.ThrowUtil; 13 | import jakarta.annotation.Resource; 14 | import org.springframework.web.bind.annotation.*; 15 | 16 | import java.util.List; 17 | 18 | /** 19 | * 短链接分组控制层 20 | * 21 | * @author Enndfp 22 | */ 23 | @RestController 24 | @RequestMapping("/group") 25 | public class GroupController { 26 | 27 | @Resource 28 | private GroupService groupService; 29 | 30 | /** 31 | * 添加分组 32 | * 33 | * @param groupAddReqDTO 分组添加请求数据传输对象 34 | * @return 添加结果 35 | */ 36 | @PostMapping("/add") 37 | public Result add(@RequestBody GroupAddReqDTO groupAddReqDTO) { 38 | // 1. 校验请求参数 39 | ThrowUtil.throwClientIf(groupAddReqDTO == null, ErrorCode.CLIENT_ERROR); 40 | // 2. 添加分组 41 | groupService.add(groupAddReqDTO); 42 | 43 | return ResultUtil.success(); 44 | } 45 | 46 | /** 47 | * 查询分组列表 48 | * 49 | * @return 分组列表 50 | */ 51 | @GetMapping("/list") 52 | public Result> list() { 53 | // 1. 查询分组列表 54 | List groupRespDTOList = groupService.listGroup(); 55 | 56 | return ResultUtil.success(groupRespDTOList); 57 | } 58 | 59 | /** 60 | * 修改分组 61 | * 62 | * @param groupUpdateReqDTO 分组修改请求数据传输对象 63 | * @return 修改结果 64 | */ 65 | @PostMapping("/update") 66 | public Result update(@RequestBody GroupUpdateReqDTO groupUpdateReqDTO) { 67 | // 1. 校验请求参数 68 | ThrowUtil.throwClientIf(groupUpdateReqDTO == null, ErrorCode.CLIENT_ERROR); 69 | // 2. 修改分组 70 | groupService.update(groupUpdateReqDTO); 71 | 72 | return ResultUtil.success(); 73 | } 74 | 75 | /** 76 | * 删除分组 77 | * 78 | * @param gid 分组标识 79 | * @return 删除结果 80 | */ 81 | @PostMapping("/delete") 82 | public Result delete(@RequestParam String gid) { 83 | // 1. 校验请求参数 84 | ThrowUtil.throwClientIf(StrUtil.isBlank(gid), ErrorCode.CLIENT_ERROR); 85 | // 2. 删除分组 86 | groupService.delete(gid); 87 | 88 | return ResultUtil.success(); 89 | } 90 | 91 | /** 92 | * 排序分组 93 | * 94 | * @param groupSortReqDTOList 分组排序请求数据传输对象列表 95 | * @return 排序结果 96 | */ 97 | @PostMapping("sort") 98 | public Result sort(@RequestBody List groupSortReqDTOList) { 99 | // 1. 校验请求参数 100 | ThrowUtil.throwClientIf(groupSortReqDTOList == null, ErrorCode.CLIENT_ERROR); 101 | // 2. 排序分组 102 | groupService.sort(groupSortReqDTOList); 103 | 104 | return ResultUtil.success(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/controller/LinkController.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.controller; 2 | 3 | import com.baomidou.mybatisplus.core.metadata.IPage; 4 | import com.enndfp.shortlink.admin.common.convention.errorcode.ErrorCode; 5 | import com.enndfp.shortlink.admin.common.convention.result.Result; 6 | import com.enndfp.shortlink.admin.remote.dto.LinkRemoteService; 7 | import com.enndfp.shortlink.admin.remote.dto.req.link.LinkPageReqDTO; 8 | import com.enndfp.shortlink.admin.remote.dto.resp.link.LinkPageRespDTO; 9 | import com.enndfp.shortlink.admin.remote.dto.req.link.LinkCreateReqDTO; 10 | import com.enndfp.shortlink.admin.remote.dto.resp.link.LinkCreateRespDTO; 11 | import com.enndfp.shortlink.admin.utils.ThrowUtil; 12 | import org.springframework.web.bind.annotation.*; 13 | 14 | /** 15 | * 短链接控制层 16 | * 17 | * @author Enndfp 18 | */ 19 | @RestController 20 | @RequestMapping("/link") 21 | public class LinkController { 22 | 23 | /** 24 | * TODO: 后续重构为 SpringCloud Feign 调用 25 | */ 26 | LinkRemoteService linkRemoteService = new LinkRemoteService() { 27 | }; 28 | 29 | /** 30 | * 创建短链接 31 | * 32 | * @param linkCreateReqDTO 短链接创建请求传输对象 33 | * @return 短链接创建响应传输对象 34 | */ 35 | @PostMapping("/create") 36 | public Result create(@RequestBody LinkCreateReqDTO linkCreateReqDTO) { 37 | // 1. 校验请求参数 38 | ThrowUtil.throwClientIf(linkCreateReqDTO == null, ErrorCode.CLIENT_ERROR); 39 | // 2. 执行创建短链接逻辑 40 | return linkRemoteService.create(linkCreateReqDTO); 41 | } 42 | 43 | /** 44 | * 分页查询短链接 45 | * 46 | * @param linkPageReqDTO 短链接分页请求传输对象 47 | * @return 短链接分页响应传输对象 48 | */ 49 | @GetMapping("/page") 50 | public Result> page(@ModelAttribute LinkPageReqDTO linkPageReqDTO) { 51 | // 1. 校验请求参数 52 | ThrowUtil.throwClientIf(linkPageReqDTO == null, ErrorCode.CLIENT_ERROR); 53 | // 2. 执行分页查询短链接逻辑 54 | return linkRemoteService.pageLink(linkPageReqDTO); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.controller; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import com.enndfp.shortlink.admin.common.convention.errorcode.ErrorCode; 5 | import com.enndfp.shortlink.admin.common.convention.result.Result; 6 | import com.enndfp.shortlink.admin.dto.req.user.UserLoginReqDTO; 7 | import com.enndfp.shortlink.admin.dto.req.user.UserRegisterReqDTO; 8 | import com.enndfp.shortlink.admin.dto.req.user.UserUpdateReqDTO; 9 | import com.enndfp.shortlink.admin.dto.resp.user.UserActualRespDTO; 10 | import com.enndfp.shortlink.admin.dto.resp.user.UserLoginRespDTO; 11 | import com.enndfp.shortlink.admin.dto.resp.user.UserRespDTO; 12 | import com.enndfp.shortlink.admin.service.UserService; 13 | import com.enndfp.shortlink.admin.utils.ResultUtil; 14 | import com.enndfp.shortlink.admin.utils.ThrowUtil; 15 | import jakarta.annotation.Resource; 16 | import org.springframework.web.bind.annotation.*; 17 | 18 | /** 19 | * 用户管理控制层 20 | * 21 | * @author Enndfp 22 | */ 23 | @RestController 24 | @RequestMapping("/user") 25 | public class UserController { 26 | 27 | @Resource 28 | private UserService userService; 29 | 30 | /** 31 | * 根据用户名查询用户信息 32 | * 33 | * @param username 用户名 34 | * @return 用户信息 35 | */ 36 | @GetMapping("/query") 37 | public Result getUserByUsername(@RequestParam("username") String username) { 38 | // 1. 校验请求参数 39 | ThrowUtil.throwClientIf(username == null, ErrorCode.USER_NAME_NULL); 40 | // 2. 查询用户信息 41 | UserRespDTO userRespDTO = userService.getUserByUsername(username); 42 | 43 | return ResultUtil.success(userRespDTO); 44 | } 45 | 46 | /** 47 | * 根据用户名查询无脱敏用户信息 48 | * 49 | * @param username 用户名 50 | * @return 用户信息 51 | */ 52 | @GetMapping("/query-actual") 53 | public Result getActualUserByUsername(@RequestParam("username") String username) { 54 | // 1. 校验请求参数 55 | ThrowUtil.throwClientIf(username == null, ErrorCode.USER_NAME_NULL); 56 | // 2. 查询用户信息 57 | UserActualRespDTO userActualRespDTO = userService.getActualUserByUsername(username); 58 | 59 | return ResultUtil.success(userActualRespDTO); 60 | } 61 | 62 | /** 63 | * 根据用户名判断用户是否存在 64 | * 65 | * @param username 用户名 66 | * @return 是否存在 67 | */ 68 | @GetMapping("/check-username") 69 | public Result checkUserExistByUsername(@RequestParam("username") String username) { 70 | // 1. 校验请求参数 71 | ThrowUtil.throwClientIf(username == null, ErrorCode.USER_NAME_NULL); 72 | // 2. 执行检查逻辑 73 | return ResultUtil.success(userService.checkUsername(username)); 74 | } 75 | 76 | /** 77 | * 用户注册 78 | * 79 | * @param userRegisterReqDTO 用户注册请求实体 80 | * @return 注册结果 81 | */ 82 | @PostMapping("/register") 83 | public Result register(@RequestBody UserRegisterReqDTO userRegisterReqDTO) { 84 | // 1. 校验请求参数 85 | ThrowUtil.throwClientIf(userRegisterReqDTO == null, ErrorCode.CLIENT_ERROR); 86 | // 2. 处理注册逻辑 87 | userService.register(userRegisterReqDTO); 88 | 89 | return ResultUtil.success(); 90 | } 91 | 92 | /** 93 | * 用户修改 94 | * 95 | * @param userUpdateReqDTO 用户修改请求实体 96 | * @return 修改结果 97 | */ 98 | @PostMapping("/update") 99 | public Result update(@RequestBody UserUpdateReqDTO userUpdateReqDTO) { 100 | // 1. 校验请求参数 101 | ThrowUtil.throwClientIf(userUpdateReqDTO == null, ErrorCode.CLIENT_ERROR); 102 | // 2. 处理修改逻辑 103 | userService.update(userUpdateReqDTO); 104 | 105 | return ResultUtil.success(); 106 | } 107 | 108 | /** 109 | * 用户登录 110 | * 111 | * @param userLoginReqDTO 用户登录请求实体 112 | * @return 登录结果 113 | */ 114 | @PostMapping("/login") 115 | public Result login(@RequestBody UserLoginReqDTO userLoginReqDTO) { 116 | // 1. 校验请求参数 117 | ThrowUtil.throwClientIf(userLoginReqDTO == null, ErrorCode.CLIENT_ERROR); 118 | // 2. 处理登录逻辑 119 | UserLoginRespDTO userLoginRespDTO = userService.login(userLoginReqDTO); 120 | 121 | return ResultUtil.success(userLoginRespDTO); 122 | } 123 | 124 | /** 125 | * 检查用户是否登录 126 | * 127 | * @param username 用户名 128 | * @param token token 129 | * @return 是否登录 130 | */ 131 | @GetMapping("/check-login") 132 | public Result checkLogin(@RequestParam("username") String username, @RequestParam("token") String token) { 133 | // 1. 校验请求参数 134 | ThrowUtil.throwClientIf(StrUtil.hasBlank(username, token), ErrorCode.USER_OR_TOKEN_NULL); 135 | // 2. 执行检查逻辑 136 | Boolean checkLogin = userService.checkLogin(username, token); 137 | 138 | return ResultUtil.success(checkLogin); 139 | } 140 | 141 | /** 142 | * 用户登出 143 | * 144 | * @param username 用户名 145 | * @param token token 146 | * @return 登出结果 147 | */ 148 | @PostMapping("/logout") 149 | public Result logout(@RequestParam("username") String username, @RequestParam("token") String token) { 150 | // 1. 校验请求参数 151 | ThrowUtil.throwClientIf(StrUtil.hasBlank(username, token), ErrorCode.USER_OR_TOKEN_NULL); 152 | // 2. 处理登出逻辑 153 | userService.logout(username, token); 154 | 155 | return ResultUtil.success(); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/dao/entity/GroupDO.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.dao.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.*; 4 | import lombok.Data; 5 | 6 | import java.io.Serial; 7 | import java.io.Serializable; 8 | import java.util.Date; 9 | 10 | /** 11 | * 短链接分组持久层实体类 12 | * 13 | * @author Enndfp 14 | */ 15 | @Data 16 | @TableName(value = "t_group") 17 | public class GroupDO implements Serializable { 18 | /** 19 | * ID 20 | */ 21 | @TableId(type = IdType.ASSIGN_ID) 22 | private Long id; 23 | 24 | /** 25 | * 分组标识 26 | */ 27 | private String gid; 28 | 29 | /** 30 | * 分组名称 31 | */ 32 | private String groupName; 33 | 34 | /** 35 | * 创建分组用户名 36 | */ 37 | private String username; 38 | 39 | /** 40 | * 分组排序 41 | */ 42 | private Integer sortOrder; 43 | 44 | /** 45 | * 创建时间 46 | */ 47 | private Date createdTime; 48 | 49 | /** 50 | * 修改时间 51 | */ 52 | private Date updatedTime; 53 | 54 | /** 55 | * 删除标识 0:未删除 1:已删除 56 | */ 57 | @TableLogic 58 | private Integer isDeleted; 59 | 60 | @Serial 61 | @TableField(exist = false) 62 | private static final long serialVersionUID = 1L; 63 | } -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/dao/entity/UserDO.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.dao.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.*; 4 | import com.enndfp.shortlink.admin.common.serialize.PhoneDesensitizationSerializer; 5 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 6 | import lombok.Data; 7 | 8 | import java.io.Serial; 9 | import java.io.Serializable; 10 | import java.util.Date; 11 | 12 | /** 13 | * 用户持久层实体类 14 | * 15 | * @author Enndfp 16 | */ 17 | @Data 18 | @TableName("t_user") 19 | public class UserDO implements Serializable { 20 | 21 | /** 22 | * ID 23 | */ 24 | @TableId(type = IdType.ASSIGN_ID) 25 | private Long id; 26 | 27 | /** 28 | * 用户名 29 | */ 30 | private String username; 31 | 32 | /** 33 | * 密码 34 | */ 35 | private String password; 36 | 37 | /** 38 | * 真实姓名 39 | */ 40 | private String realName; 41 | 42 | /** 43 | * 手机号 44 | */ 45 | @JsonSerialize(using = PhoneDesensitizationSerializer.class) 46 | private String phone; 47 | 48 | /** 49 | * 邮箱 50 | */ 51 | private String mail; 52 | 53 | /** 54 | * 注销时间戳 55 | */ 56 | private Date deletedTime; 57 | 58 | /** 59 | * 创建时间 60 | */ 61 | private Date createdTime; 62 | 63 | /** 64 | * 修改时间 65 | */ 66 | private Date updatedTime; 67 | 68 | /** 69 | * 删除标识 0:未删除 1:已删除 70 | */ 71 | @TableLogic(value = "0", delval = "1,deleted_time = now()") 72 | private Integer isDeleted; 73 | 74 | @Serial 75 | @TableField(exist = false) 76 | private static final long serialVersionUID = 1L; 77 | 78 | } -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/dao/mapper/GroupMapper.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.dao.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.enndfp.shortlink.admin.dao.entity.GroupDO; 5 | 6 | /** 7 | * 短链接分组持久层接口 8 | * 9 | * @author Enndfp 10 | */ 11 | public interface GroupMapper extends BaseMapper { 12 | 13 | } 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/dao/mapper/UserMapper.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.dao.mapper; 2 | 3 | 4 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 5 | import com.enndfp.shortlink.admin.dao.entity.UserDO; 6 | 7 | /** 8 | * 用户持久层接口 9 | * 10 | * @author Enndfp 11 | */ 12 | public interface UserMapper extends BaseMapper { 13 | } 14 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/dto/req/group/GroupAddReqDTO.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.dto.req.group; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * 短链接分组添加请求数据传输对象 7 | * 8 | * @author Enndfp 9 | */ 10 | @Data 11 | public class GroupAddReqDTO { 12 | 13 | /** 14 | * 分组名称 15 | */ 16 | private String groupName; 17 | } 18 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/dto/req/group/GroupSortReqDTO.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.dto.req.group; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * 短链接分组排序请求数据传输对象 7 | * 8 | * @author Enndfp 9 | */ 10 | @Data 11 | public class GroupSortReqDTO { 12 | 13 | /** 14 | * 分组标识 15 | */ 16 | private String gid; 17 | 18 | /** 19 | * 排序 20 | */ 21 | private Integer sortOrder; 22 | } 23 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/dto/req/group/GroupUpdateReqDTO.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.dto.req.group; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * 短链接分组修改请求数据传输对象 7 | * 8 | * @author Enndfp 9 | */ 10 | @Data 11 | public class GroupUpdateReqDTO { 12 | 13 | /** 14 | * 分组标识 15 | */ 16 | private String gid; 17 | 18 | /** 19 | * 分组名称 20 | */ 21 | private String groupName; 22 | } 23 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/dto/req/user/UserLoginReqDTO.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.dto.req.user; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * 用户登录请求实体 7 | * 8 | * @author Enndfp 9 | */ 10 | @Data 11 | public class UserLoginReqDTO { 12 | 13 | /** 14 | * 用户名 15 | */ 16 | private String username; 17 | 18 | /** 19 | * 密码 20 | */ 21 | private String password; 22 | } 23 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/dto/req/user/UserRegisterReqDTO.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.dto.req.user; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * 用户注册请求实体 7 | * 8 | * @author Enndfp 9 | */ 10 | @Data 11 | public class UserRegisterReqDTO { 12 | 13 | /** 14 | * 用户名 15 | */ 16 | private String username; 17 | 18 | /** 19 | * 密码 20 | */ 21 | private String password; 22 | 23 | /** 24 | * 真实姓名 25 | */ 26 | private String realName; 27 | 28 | /** 29 | * 手机号 30 | */ 31 | private String phone; 32 | 33 | /** 34 | * 邮箱 35 | */ 36 | private String mail; 37 | } 38 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/dto/req/user/UserUpdateReqDTO.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.dto.req.user; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * 用户修改请求实体 7 | * 8 | * @author Enndfp 9 | */ 10 | @Data 11 | public class UserUpdateReqDTO { 12 | 13 | /** 14 | * 用户名 15 | */ 16 | private String username; 17 | 18 | /** 19 | * 密码 20 | */ 21 | private String password; 22 | 23 | /** 24 | * 真实姓名 25 | */ 26 | private String realName; 27 | 28 | /** 29 | * 手机号 30 | */ 31 | private String phone; 32 | 33 | /** 34 | * 邮箱 35 | */ 36 | private String mail; 37 | } 38 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/dto/resp/group/GroupRespDTO.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.dto.resp.group; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * 分组信息返回实体 7 | * 8 | * @author Enndfp 9 | */ 10 | @Data 11 | public class GroupRespDTO { 12 | 13 | /** 14 | * 分组标识 15 | */ 16 | private String gid; 17 | 18 | /** 19 | * 分组名称 20 | */ 21 | private String groupName; 22 | 23 | /** 24 | * 分组排序 25 | */ 26 | private Integer sortOrder; 27 | 28 | /** 29 | * 分组下的链接数量 30 | */ 31 | private Integer linkCount; 32 | } 33 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/dto/resp/user/UserActualRespDTO.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.dto.resp.user; 2 | 3 | import com.enndfp.shortlink.admin.common.serialize.PhoneDesensitizationSerializer; 4 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 5 | import lombok.Data; 6 | 7 | /** 8 | * 用户信息返回实体 9 | * 10 | * @author Enndfp 11 | */ 12 | @Data 13 | public class UserActualRespDTO { 14 | 15 | private static final long serialVersionUID = 1L; 16 | 17 | /** 18 | * id 19 | */ 20 | private Long id; 21 | 22 | /** 23 | * 用户名 24 | */ 25 | private String username; 26 | 27 | /** 28 | * 真实姓名 29 | */ 30 | private String realName; 31 | 32 | /** 33 | * 手机号 34 | */ 35 | private String phone; 36 | 37 | /** 38 | * 邮箱 39 | */ 40 | private String mail; 41 | } 42 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/dto/resp/user/UserLoginRespDTO.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.dto.resp.user; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | /** 8 | * 用户登录返回实体 9 | * 10 | * @author Enndfp 11 | */ 12 | @Data 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | public class UserLoginRespDTO { 16 | 17 | private static final long serialVersionUID = 1L; 18 | 19 | /** 20 | * 用户token 21 | */ 22 | private String token; 23 | } 24 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/dto/resp/user/UserRespDTO.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.dto.resp.user; 2 | 3 | import com.enndfp.shortlink.admin.common.serialize.PhoneDesensitizationSerializer; 4 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 5 | import lombok.Data; 6 | 7 | /** 8 | * 用户信息返回实体 9 | * 10 | * @author Enndfp 11 | */ 12 | @Data 13 | public class UserRespDTO { 14 | 15 | private static final long serialVersionUID = 1L; 16 | 17 | /** 18 | * id 19 | */ 20 | private Long id; 21 | 22 | /** 23 | * 用户名 24 | */ 25 | private String username; 26 | 27 | /** 28 | * 真实姓名 29 | */ 30 | private String realName; 31 | 32 | /** 33 | * 手机号 34 | */ 35 | @JsonSerialize(using = PhoneDesensitizationSerializer.class) 36 | private String phone; 37 | 38 | /** 39 | * 邮箱 40 | */ 41 | private String mail; 42 | } 43 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/interceptor/UserContextInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.interceptor; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import cn.hutool.json.JSONUtil; 5 | import com.enndfp.shortlink.admin.common.convention.errorcode.ErrorCode; 6 | import com.enndfp.shortlink.admin.context.UserContext; 7 | import com.enndfp.shortlink.admin.context.UserContextDTO; 8 | import com.enndfp.shortlink.admin.utils.ThrowUtil; 9 | import jakarta.servlet.http.HttpServletRequest; 10 | import jakarta.servlet.http.HttpServletResponse; 11 | import lombok.RequiredArgsConstructor; 12 | import org.springframework.data.redis.core.StringRedisTemplate; 13 | import org.springframework.web.servlet.HandlerInterceptor; 14 | 15 | import static com.enndfp.shortlink.admin.common.constant.RedisCacheConstant.USER_LOGIN_KEY; 16 | 17 | /** 18 | * 用户上下文信息传递拦截器 19 | * 20 | * @author Enndfp 21 | */ 22 | @RequiredArgsConstructor 23 | public class UserContextInterceptor implements HandlerInterceptor { 24 | 25 | private final StringRedisTemplate stringRedisTemplate; 26 | 27 | @Override 28 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { 29 | // 1. 从请求头中获取用户名和token 30 | String username = request.getHeader("username"); 31 | String token = request.getHeader("token"); 32 | 33 | // 2. 校验用户是否登录 34 | String userInfoJsonStr = (String) stringRedisTemplate.opsForHash().get(USER_LOGIN_KEY + username, token); 35 | ThrowUtil.throwServerIf(StrUtil.isEmpty(userInfoJsonStr), ErrorCode.USER_NOT_LOGIN); 36 | 37 | // 3. 将用户信息放入上下文 38 | UserContextDTO userContextDTO = JSONUtil.toBean(userInfoJsonStr, UserContextDTO.class); 39 | UserContext.setUser(userContextDTO); 40 | 41 | return true; 42 | } 43 | 44 | @Override 45 | public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { 46 | // 1. 清除用户上下文信息 47 | UserContext.removeUser(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/remote/dto/LinkRemoteService.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.remote.dto; 2 | 3 | import cn.hutool.http.HttpUtil; 4 | import cn.hutool.json.JSONUtil; 5 | import com.alibaba.fastjson2.JSON; 6 | import com.alibaba.fastjson2.TypeReference; 7 | import com.baomidou.mybatisplus.core.metadata.IPage; 8 | import com.enndfp.shortlink.admin.common.convention.result.Result; 9 | import com.enndfp.shortlink.admin.remote.dto.req.link.LinkPageReqDTO; 10 | import com.enndfp.shortlink.admin.remote.dto.resp.link.LinkPageRespDTO; 11 | import com.enndfp.shortlink.admin.remote.dto.resp.link.LinkCountRespDTO; 12 | import com.enndfp.shortlink.admin.remote.dto.req.link.LinkCreateReqDTO; 13 | import com.enndfp.shortlink.admin.remote.dto.resp.link.LinkCreateRespDTO; 14 | import org.springframework.web.bind.annotation.ModelAttribute; 15 | import org.springframework.web.bind.annotation.RequestParam; 16 | 17 | import java.util.HashMap; 18 | import java.util.List; 19 | import java.util.Map; 20 | 21 | /** 22 | * 短链接中台远程调用服务 23 | * 24 | * @author Enndfp 25 | */ 26 | public interface LinkRemoteService { 27 | 28 | /** 29 | * 创建短链接 30 | * 31 | * @param linkCreateReqDTO 短链接创建请求传输对象 32 | * @return 短链接创建响应传输对象 33 | */ 34 | default Result create(LinkCreateReqDTO linkCreateReqDTO) { 35 | // 1. 执行远程调用 36 | String resultBody = HttpUtil.post("http://127.0.0.1:8001/api/short-link/v1/link/create", JSONUtil.toJsonStr(linkCreateReqDTO)); 37 | 38 | // 2. 解析响应结果 39 | return JSON.parseObject(resultBody, new TypeReference>() { 40 | }); 41 | } 42 | 43 | /** 44 | * 分页查询短链接 45 | * 46 | * @param linkPageReqDTO 短链接分页请求传输对象 47 | * @return 短链接分页响应传输对象 48 | */ 49 | default Result> pageLink(@ModelAttribute LinkPageReqDTO linkPageReqDTO) { 50 | // 1. 获取请求参数 51 | Map requestParamMap = new HashMap<>(); 52 | requestParamMap.put("gid", linkPageReqDTO.getGid()); 53 | requestParamMap.put("current", linkPageReqDTO.getCurrent()); 54 | requestParamMap.put("size", linkPageReqDTO.getSize()); 55 | 56 | // 2. 执行远程调用 57 | String resultPageStr = HttpUtil.get("http://127.0.0.1:8001/api/short-link/v1/link/page", requestParamMap); 58 | 59 | // 3. 解析响应结果 60 | return JSON.parseObject(resultPageStr, new TypeReference<>() { 61 | }); 62 | } 63 | 64 | /** 65 | * 根据分组标识统计链接数量 66 | * 67 | * @param gids 分组标识列表 68 | * @return 链接数量返回数据传输对象列表 69 | */ 70 | default Result> count(@RequestParam List gids) { 71 | // 1. 获取请求参数 72 | Map requestParamMap = new HashMap<>(); 73 | requestParamMap.put("gids", gids); 74 | 75 | // 2. 执行远程调用 76 | String resultPageStr = HttpUtil.get("http://127.0.0.1:8001/api/short-link/v1/link/count", requestParamMap); 77 | 78 | // 3. 解析响应结果 79 | return JSON.parseObject(resultPageStr, new TypeReference<>() { 80 | }); 81 | } 82 | 83 | 84 | } 85 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/remote/dto/req/link/LinkCreateReqDTO.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.remote.dto.req.link; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import lombok.Data; 5 | 6 | import java.util.Date; 7 | 8 | /** 9 | * 短链接创建请求传输对象 10 | * 11 | * @author Enndfp 12 | */ 13 | @Data 14 | public class LinkCreateReqDTO { 15 | 16 | /** 17 | * 域名 18 | */ 19 | private String domain; 20 | 21 | /** 22 | * 原始链接 23 | */ 24 | private String originUrl; 25 | 26 | /** 27 | * 分组标识 28 | */ 29 | private String gid; 30 | 31 | /** 32 | * 创建类型 0:控制台 1:接口 33 | */ 34 | private Integer createdType; 35 | 36 | /** 37 | * 有效期类型 0:永久有效 1:用户自定义 38 | */ 39 | private Integer validDateType; 40 | 41 | /** 42 | * 有效期 43 | */ 44 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") 45 | private Date validDate; 46 | 47 | /** 48 | * 描述 49 | */ 50 | private String description; 51 | } 52 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/remote/dto/req/link/LinkPageReqDTO.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.remote.dto.req.link; 2 | 3 | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 4 | import lombok.Data; 5 | 6 | /** 7 | * 短链接分页请求传输对象 8 | * 9 | * @author Enndfp 10 | */ 11 | @Data 12 | public class LinkPageReqDTO extends Page { 13 | 14 | /** 15 | * 分组标识 16 | */ 17 | private String gid; 18 | } 19 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/remote/dto/resp/link/LinkCountRespDTO.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.remote.dto.resp.link; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * 链接数量返回数据传输对象 7 | * 8 | * @author Enndfp 9 | */ 10 | @Data 11 | public class LinkCountRespDTO { 12 | 13 | /** 14 | * 分组标识 15 | */ 16 | private String gid; 17 | 18 | /** 19 | * 分组下的链接数量 20 | */ 21 | private Integer linkCount; 22 | } 23 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/remote/dto/resp/link/LinkCreateRespDTO.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.remote.dto.resp.link; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * 短链接创建响应传输对象 7 | * 8 | * @author Enndfp 9 | */ 10 | @Data 11 | public class LinkCreateRespDTO { 12 | 13 | /** 14 | * 分组标识 15 | */ 16 | private String gid; 17 | 18 | /** 19 | * 完整短链接 20 | */ 21 | private String fullShortUrl; 22 | 23 | /** 24 | * 原始链接 25 | */ 26 | private String originUrl; 27 | } 28 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/remote/dto/resp/link/LinkPageRespDTO.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.remote.dto.resp.link; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import lombok.Data; 5 | 6 | import java.util.Date; 7 | 8 | /** 9 | * 短链接分页响应传输对象 10 | * 11 | * @author Enndfp 12 | */ 13 | @Data 14 | public class LinkPageRespDTO { 15 | 16 | /** 17 | * ID 18 | */ 19 | private Long id; 20 | 21 | /** 22 | * 域名 23 | */ 24 | private String domain; 25 | 26 | /** 27 | * 短链接 28 | */ 29 | private String shortUri; 30 | 31 | /** 32 | * 完整短链接 33 | */ 34 | private String fullShortUrl; 35 | 36 | /** 37 | * 原始链接 38 | */ 39 | private String originUrl; 40 | 41 | /** 42 | * 分组标识 43 | */ 44 | private String gid; 45 | 46 | /** 47 | * 网站图标 48 | */ 49 | private String favicon; 50 | 51 | /** 52 | * 有效期类型 0:永久有效 1:用户自定义 53 | */ 54 | private Integer validDateType; 55 | 56 | /** 57 | * 有效期 58 | */ 59 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") 60 | private Date validDate; 61 | 62 | /** 63 | * 描述 64 | */ 65 | private String description; 66 | 67 | /** 68 | * 创建时间 69 | */ 70 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") 71 | private Date createdTime; 72 | } 73 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/service/GroupService.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import com.enndfp.shortlink.admin.dao.entity.GroupDO; 5 | import com.enndfp.shortlink.admin.dto.req.group.GroupAddReqDTO; 6 | import com.enndfp.shortlink.admin.dto.req.group.GroupSortReqDTO; 7 | import com.enndfp.shortlink.admin.dto.req.group.GroupUpdateReqDTO; 8 | import com.enndfp.shortlink.admin.dto.resp.group.GroupRespDTO; 9 | 10 | import java.util.List; 11 | 12 | /** 13 | * 短链接分组业务层接口 14 | * 15 | * @author Enndfp 16 | */ 17 | public interface GroupService extends IService { 18 | 19 | /** 20 | * 添加分组 21 | * 22 | * @param groupAddReqDTO 分组添加请求数据传输对象 23 | */ 24 | void add(GroupAddReqDTO groupAddReqDTO); 25 | 26 | /** 27 | * 查询分组列表 28 | * 29 | * @return 分组列表 30 | */ 31 | List listGroup(); 32 | 33 | /** 34 | * 修改分组 35 | * 36 | * @param groupUpdateReqDTO 分组修改请求数据传输对象 37 | */ 38 | void update(GroupUpdateReqDTO groupUpdateReqDTO); 39 | 40 | /** 41 | * 删除分组 42 | * 43 | * @param gid 分组标识 44 | */ 45 | void delete(String gid); 46 | 47 | /** 48 | * 分组排序 49 | * 50 | * @param groupSortReqDTOList 分组排序请求数据传输对象列表 51 | */ 52 | void sort(List groupSortReqDTOList); 53 | 54 | /** 55 | * 添加默认分组 56 | * 57 | * @param username 用户名 58 | */ 59 | void addDefaultGroup(String username); 60 | } 61 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/service/UserService.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import com.enndfp.shortlink.admin.dao.entity.UserDO; 5 | import com.enndfp.shortlink.admin.dto.req.user.UserLoginReqDTO; 6 | import com.enndfp.shortlink.admin.dto.req.user.UserRegisterReqDTO; 7 | import com.enndfp.shortlink.admin.dto.req.user.UserUpdateReqDTO; 8 | import com.enndfp.shortlink.admin.dto.resp.user.UserActualRespDTO; 9 | import com.enndfp.shortlink.admin.dto.resp.user.UserLoginRespDTO; 10 | import com.enndfp.shortlink.admin.dto.resp.user.UserRespDTO; 11 | 12 | /** 13 | * 用户业务层接口 14 | * 15 | * @author Enndfp 16 | */ 17 | public interface UserService extends IService { 18 | 19 | /** 20 | * 根据用户名获取用户信息 21 | * 22 | * @param username 用户名 23 | * @return 用户信息 24 | */ 25 | UserRespDTO getUserByUsername(String username); 26 | 27 | /** 28 | * 根据用户名获取无脱敏用户信息 29 | * 30 | * @param username 用户名 31 | * @return 无脱敏用户信息 32 | */ 33 | UserActualRespDTO getActualUserByUsername(String username); 34 | 35 | /** 36 | * 根据用户名判断用户是否存在 37 | * 38 | * @param username 用户名 39 | * @return 是否存在 40 | */ 41 | Boolean checkUsername(String username); 42 | 43 | /** 44 | * 用户注册 45 | * 46 | * @param userRegisterReqDTO 用户注册请求实体 47 | */ 48 | void register(UserRegisterReqDTO userRegisterReqDTO); 49 | 50 | /** 51 | * 用户修改 52 | * 53 | * @param userUpdateReqDTO 用户修改请求实体 54 | */ 55 | void update(UserUpdateReqDTO userUpdateReqDTO); 56 | 57 | /** 58 | * 用户登录 59 | * 60 | * @param userLoginReqDTO 用户登录请求实体 61 | * @return 登录结果 62 | */ 63 | UserLoginRespDTO login(UserLoginReqDTO userLoginReqDTO); 64 | 65 | /** 66 | * 检查用户是否登录 67 | * 68 | * @param username 用户名 69 | * @param token token 70 | * @return 是否登录 71 | */ 72 | Boolean checkLogin(String username, String token); 73 | 74 | /** 75 | * 用户登出 76 | * 77 | * @param username 用户名 78 | * @param token token 79 | */ 80 | void logout(String username, String token); 81 | } 82 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/service/impl/GroupServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.service.impl; 2 | 3 | import cn.hutool.core.bean.BeanUtil; 4 | import cn.hutool.core.util.StrUtil; 5 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 6 | import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; 7 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 8 | import com.enndfp.shortlink.admin.common.convention.errorcode.ErrorCode; 9 | import com.enndfp.shortlink.admin.context.UserContext; 10 | import com.enndfp.shortlink.admin.dao.entity.GroupDO; 11 | import com.enndfp.shortlink.admin.dao.mapper.GroupMapper; 12 | import com.enndfp.shortlink.admin.dto.req.group.GroupAddReqDTO; 13 | import com.enndfp.shortlink.admin.dto.req.group.GroupSortReqDTO; 14 | import com.enndfp.shortlink.admin.dto.req.group.GroupUpdateReqDTO; 15 | import com.enndfp.shortlink.admin.dto.resp.group.GroupRespDTO; 16 | import com.enndfp.shortlink.admin.remote.dto.LinkRemoteService; 17 | import com.enndfp.shortlink.admin.remote.dto.resp.link.LinkCountRespDTO; 18 | import com.enndfp.shortlink.admin.service.GroupService; 19 | import com.enndfp.shortlink.admin.utils.RandomStringUtil; 20 | import com.enndfp.shortlink.admin.utils.ThrowUtil; 21 | import jakarta.annotation.Resource; 22 | import org.redisson.api.RBloomFilter; 23 | import org.springframework.stereotype.Service; 24 | 25 | import java.util.List; 26 | 27 | /** 28 | * 短链接分组业务层实现类 29 | * 30 | * @author Enndfp 31 | */ 32 | @Service 33 | public class GroupServiceImpl extends ServiceImpl implements GroupService { 34 | 35 | @Resource 36 | private GroupMapper groupMapper; 37 | 38 | @Resource 39 | private RBloomFilter gidGenerateCachePenetrationBloomFilter; 40 | 41 | /** 42 | * TODO: 后续重构为 SpringCloud Feign 调用 43 | */ 44 | LinkRemoteService linkRemoteService = new LinkRemoteService() { 45 | }; 46 | 47 | @Override 48 | public void add(GroupAddReqDTO groupAddReqDTO) { 49 | // 1. 校验请求参数 50 | String groupName = groupAddReqDTO.getGroupName(); 51 | ThrowUtil.throwClientIf(StrUtil.isBlank(groupName), ErrorCode.GROUP_NAME_NULL); 52 | 53 | // 2. 生成的分组id不能重复 54 | String gid; 55 | do { 56 | gid = RandomStringUtil.generateRandomString(6); 57 | } while (checkGidExist(gid)); 58 | 59 | // 3. 添加分组 60 | GroupDO groupDO = new GroupDO(); 61 | groupDO.setGid(gid); 62 | groupDO.setUsername(UserContext.getUsername()); 63 | groupDO.setGroupName(groupName); 64 | int insert = groupMapper.insert(groupDO); 65 | ThrowUtil.throwServerIf(insert != 1, ErrorCode.GROUP_SAVE_ERROR); 66 | 67 | // 4. 将gid添加到布隆过滤器中 68 | gidGenerateCachePenetrationBloomFilter.add(gid); 69 | } 70 | 71 | @Override 72 | public List listGroup() { 73 | // 1. 从上下文中获取用户名 74 | String username = UserContext.getUsername(); 75 | 76 | // 2. 构造查询条件 77 | LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); 78 | queryWrapper.eq(GroupDO::getUsername, username); 79 | queryWrapper.orderByDesc(GroupDO::getSortOrder); 80 | queryWrapper.orderByDesc(GroupDO::getUpdatedTime); 81 | 82 | // 3. 查询分组列表 83 | List groupDOList = groupMapper.selectList(queryWrapper); 84 | // 3.1 查询分组下的链接数量 85 | List linkCountRespDTOList = linkRemoteService.count(groupDOList.stream().map(GroupDO::getGid).toList()).getData(); 86 | // 3.2 拷贝分组列表 87 | List groupRespDTOList = BeanUtil.copyToList(groupDOList, GroupRespDTO.class); 88 | // 3.3 设置分组下的链接数量 89 | groupRespDTOList.forEach(groupRespDTO -> { 90 | for (LinkCountRespDTO linkCountRespDTO : linkCountRespDTOList) { 91 | if (groupRespDTO.getGid().equals(linkCountRespDTO.getGid())) { 92 | groupRespDTO.setLinkCount(linkCountRespDTO.getLinkCount()); 93 | break; 94 | } 95 | } 96 | }); 97 | 98 | return groupRespDTOList; 99 | } 100 | 101 | @Override 102 | public void update(GroupUpdateReqDTO groupUpdateReqDTO) { 103 | // 1. 校验请求参数 104 | String gid = groupUpdateReqDTO.getGid(); 105 | String groupName = groupUpdateReqDTO.getGroupName(); 106 | ThrowUtil.throwClientIf(StrUtil.hasBlank(gid, groupName), ErrorCode.CLIENT_ERROR); 107 | 108 | // 2. 构造修改条件 109 | LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); 110 | updateWrapper.eq(GroupDO::getGid, gid); 111 | updateWrapper.eq(GroupDO::getUsername, UserContext.getUsername()); 112 | 113 | // 3. 修改分组 114 | GroupDO groupDO = new GroupDO(); 115 | groupDO.setGroupName(groupName); 116 | int update = groupMapper.update(groupDO, updateWrapper); 117 | ThrowUtil.throwServerIf(update != 1, ErrorCode.GROUP_UPDATE_ERROR); 118 | } 119 | 120 | @Override 121 | public void delete(String gid) { 122 | // 1. 构造删除条件 123 | LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); 124 | updateWrapper.eq(GroupDO::getGid, gid); 125 | updateWrapper.eq(GroupDO::getUsername, UserContext.getUsername()); 126 | 127 | // 3. 删除分组 128 | int delete = groupMapper.delete(updateWrapper); 129 | ThrowUtil.throwServerIf(delete != 1, ErrorCode.GROUP_DELETE_ERROR); 130 | } 131 | 132 | @Override 133 | public void sort(List groupSortReqDTOList) { 134 | for (GroupSortReqDTO groupSortReqDTO : groupSortReqDTOList) { 135 | // 1. 校验请求参数 136 | String gid = groupSortReqDTO.getGid(); 137 | Integer sortOrder = groupSortReqDTO.getSortOrder(); 138 | ThrowUtil.throwClientIf(StrUtil.hasBlank(gid) || sortOrder == null, ErrorCode.CLIENT_ERROR); 139 | 140 | // 2. 构造排序修改条件 141 | LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); 142 | updateWrapper.eq(GroupDO::getGid, gid); 143 | updateWrapper.eq(GroupDO::getUsername, UserContext.getUsername()); 144 | 145 | // 3. 修改排序分组 146 | GroupDO groupDO = new GroupDO(); 147 | groupDO.setSortOrder(sortOrder); 148 | int update = groupMapper.update(groupDO, updateWrapper); 149 | ThrowUtil.throwServerIf(update != 1, ErrorCode.GROUP_SORT_ERROR); 150 | } 151 | } 152 | 153 | @Override 154 | public void addDefaultGroup(String username) { 155 | // 1. 生成的分组id不能重复 156 | String gid; 157 | do { 158 | gid = RandomStringUtil.generateRandomString(6); 159 | } while (checkGidExist(gid)); 160 | 161 | // 2. 添加默认分组 162 | GroupDO groupDO = new GroupDO(); 163 | groupDO.setGid(gid); 164 | groupDO.setUsername(username); 165 | groupDO.setGroupName("默认分组"); 166 | int insert = groupMapper.insert(groupDO); 167 | ThrowUtil.throwServerIf(insert != 1, ErrorCode.GROUP_SAVE_ERROR); 168 | 169 | // 3. 将gid添加到布隆过滤器中 170 | gidGenerateCachePenetrationBloomFilter.add(gid); 171 | } 172 | 173 | /** 174 | * 校验Gid是否存在 175 | * 176 | * @param gid 分组ID 177 | * @return 是否存在 178 | */ 179 | private boolean checkGidExist(String gid) { 180 | // 判断布隆过滤器是否存在此gid 181 | return gidGenerateCachePenetrationBloomFilter.contains(gid); 182 | } 183 | } 184 | 185 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/service/impl/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.service.impl; 2 | 3 | import cn.hutool.core.bean.BeanUtil; 4 | import cn.hutool.core.lang.UUID; 5 | import cn.hutool.json.JSONUtil; 6 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 7 | import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; 8 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 9 | import com.enndfp.shortlink.admin.common.convention.errorcode.ErrorCode; 10 | import com.enndfp.shortlink.admin.dao.entity.UserDO; 11 | import com.enndfp.shortlink.admin.dao.mapper.UserMapper; 12 | import com.enndfp.shortlink.admin.dto.req.user.UserLoginReqDTO; 13 | import com.enndfp.shortlink.admin.dto.req.user.UserRegisterReqDTO; 14 | import com.enndfp.shortlink.admin.dto.req.user.UserUpdateReqDTO; 15 | import com.enndfp.shortlink.admin.dto.resp.user.UserActualRespDTO; 16 | import com.enndfp.shortlink.admin.dto.resp.user.UserLoginRespDTO; 17 | import com.enndfp.shortlink.admin.dto.resp.user.UserRespDTO; 18 | import com.enndfp.shortlink.admin.service.GroupService; 19 | import com.enndfp.shortlink.admin.service.UserService; 20 | import com.enndfp.shortlink.admin.utils.ThrowUtil; 21 | import jakarta.annotation.Resource; 22 | import org.redisson.api.RBloomFilter; 23 | import org.redisson.api.RLock; 24 | import org.redisson.api.RedissonClient; 25 | import org.springframework.data.redis.core.StringRedisTemplate; 26 | import org.springframework.stereotype.Service; 27 | 28 | import java.util.concurrent.TimeUnit; 29 | 30 | import static com.enndfp.shortlink.admin.common.constant.RedisCacheConstant.*; 31 | 32 | /** 33 | * 用户业务层实现类 34 | * 35 | * @author Enndfp 36 | */ 37 | @Service 38 | public class UserServiceImpl extends ServiceImpl implements UserService { 39 | 40 | @Resource 41 | private UserMapper userMapper; 42 | @Resource 43 | private GroupService groupService; 44 | @Resource 45 | private RedissonClient redissonClient; 46 | @Resource 47 | private StringRedisTemplate stringRedisTemplate; 48 | @Resource 49 | private RBloomFilter userRegisterCachePenetrationBloomFilter; 50 | 51 | @Override 52 | public UserRespDTO getUserByUsername(String username) { 53 | // 1. 构造查询条件并查询 54 | LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); 55 | queryWrapper.eq(UserDO::getUsername, username); 56 | UserDO userDO = userMapper.selectOne(queryWrapper); 57 | 58 | // 2. 校验用户是否存在 59 | ThrowUtil.throwServerIf(userDO == null, ErrorCode.USER_NOT_EXIST); 60 | 61 | return BeanUtil.toBean(userDO, UserRespDTO.class); 62 | } 63 | 64 | @Override 65 | public UserActualRespDTO getActualUserByUsername(String username) { 66 | // 1. 构造查询条件并查询 67 | LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); 68 | queryWrapper.eq(UserDO::getUsername, username); 69 | UserDO userDO = userMapper.selectOne(queryWrapper); 70 | 71 | // 2. 校验用户是否存在 72 | ThrowUtil.throwServerIf(userDO == null, ErrorCode.USER_NOT_EXIST); 73 | 74 | return BeanUtil.toBean(userDO, UserActualRespDTO.class); 75 | } 76 | 77 | @Override 78 | public Boolean checkUsername(String username) { 79 | // 判断布隆过滤器是否存在此用户名 80 | return userRegisterCachePenetrationBloomFilter.contains(username); 81 | } 82 | 83 | @Override 84 | public void register(UserRegisterReqDTO userRegisterReqDTO) { 85 | // 1. 校验用户名是否存在 86 | String username = userRegisterReqDTO.getUsername(); 87 | ThrowUtil.throwClientIf(username == null, ErrorCode.USER_NAME_NULL); 88 | ThrowUtil.throwClientIf(checkUsername(username), ErrorCode.USER_NAME_EXIST); 89 | 90 | // 2. 给用户名上锁,防止并发注册 91 | RLock lock = redissonClient.getLock(LOCK_USER_REGISTER_KEY + username); 92 | try { 93 | // 2.1 尝试获取锁 94 | ThrowUtil.throwClientIf(!lock.tryLock(), ErrorCode.USER_NAME_EXIST); 95 | 96 | // 3. 保存用户信息 97 | UserDO userDO = BeanUtil.toBean(userRegisterReqDTO, UserDO.class); 98 | int insert = userMapper.insert(userDO); 99 | ThrowUtil.throwServerIf(insert != 1, ErrorCode.USER_REGISTER_ERROR); 100 | 101 | // 4. 将用户名存入布隆过滤器 102 | userRegisterCachePenetrationBloomFilter.add(username); 103 | 104 | // 5. 创建默认分组 105 | groupService.addDefaultGroup(username); 106 | } finally { 107 | // 5. 释放锁 108 | lock.unlock(); 109 | } 110 | } 111 | 112 | @Override 113 | public void update(UserUpdateReqDTO userUpdateReqDTO) { 114 | // TODO 验证当前用户是否为登录用户 115 | // 1. 校验用户名是否存在 116 | String username = userUpdateReqDTO.getUsername(); 117 | ThrowUtil.throwClientIf(username == null, ErrorCode.USER_NAME_NULL); 118 | 119 | // 2. 构造更新条件 120 | LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); 121 | updateWrapper.eq(UserDO::getUsername, username); 122 | UserDO userDO = BeanUtil.toBean(userUpdateReqDTO, UserDO.class); 123 | 124 | // 3. 执行更新 125 | int update = userMapper.update(userDO, updateWrapper); 126 | ThrowUtil.throwServerIf(update != 1, ErrorCode.USER_INFO_UPDATE_ERROR); 127 | } 128 | 129 | @Override 130 | public UserLoginRespDTO login(UserLoginReqDTO userLoginReqDTO) { 131 | // 1. 校验用户名和密码 132 | String username = userLoginReqDTO.getUsername(); 133 | String password = userLoginReqDTO.getPassword(); 134 | ThrowUtil.throwClientIf(username == null, ErrorCode.USER_NAME_NULL); 135 | ThrowUtil.throwClientIf(password == null, ErrorCode.PASSWORD_NULL); 136 | 137 | // 2. 校验用户是否存在 138 | LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); 139 | queryWrapper.eq(UserDO::getUsername, username); 140 | queryWrapper.eq(UserDO::getPassword, password); 141 | UserDO userDO = userMapper.selectOne(queryWrapper); 142 | ThrowUtil.throwServerIf(userDO == null, ErrorCode.USER_NOT_EXIST); 143 | 144 | // 3. 校验用户是否已登录 145 | Boolean hasLogin = stringRedisTemplate.hasKey(USER_LOGIN_KEY + username); 146 | ThrowUtil.throwServerIf(Boolean.TRUE.equals(hasLogin), ErrorCode.USER_HAS_LOGIN); 147 | 148 | // 4. 生成token并存入redis 149 | String token = UUID.randomUUID().toString(true); 150 | stringRedisTemplate.opsForHash().put(USER_LOGIN_KEY + username, token, JSONUtil.toJsonStr(userDO)); 151 | stringRedisTemplate.expire(USER_LOGIN_KEY + username, USER_LOGIN_TTL, TimeUnit.DAYS); 152 | 153 | return new UserLoginRespDTO(token); 154 | } 155 | 156 | @Override 157 | public Boolean checkLogin(String username, String token) { 158 | // 检查redis中是否存在token 159 | return stringRedisTemplate.opsForHash().get(USER_LOGIN_KEY + username, token) != null; 160 | } 161 | 162 | @Override 163 | public void logout(String username, String token) { 164 | // 1. 判断用户是否登录 165 | ThrowUtil.throwServerIf(!checkLogin(username, token), ErrorCode.USER_HAS_LOGIN); 166 | 167 | // 2. 删除redis中的token 168 | stringRedisTemplate.opsForHash().delete(USER_LOGIN_KEY + username, token); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/utils/RandomStringUtil.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.utils; 2 | 3 | import java.security.SecureRandom; 4 | 5 | /** 6 | * 生成随机字符串工具类 7 | * 8 | * @author Enndfp 9 | */ 10 | public class RandomStringUtil { 11 | 12 | /** 13 | * 定义小写字母字符串 14 | */ 15 | private static final String CHAR_LOWER = "abcdefghijklmnopqrstuvwxyz"; 16 | /** 17 | * 将小写字母字符串转换为大写 18 | */ 19 | private static final String CHAR_UPPER = CHAR_LOWER.toUpperCase(); 20 | /** 21 | * 定义数字字符串 22 | */ 23 | private static final String NUMBER = "0123456789"; 24 | 25 | /** 26 | * 将小写字母、大写字母和数字组合成一个用于生成随机字符串的数据源 27 | */ 28 | private static final String DATA_FOR_RANDOM_STRING = CHAR_LOWER + CHAR_UPPER + NUMBER; 29 | /** 30 | * 使用SecureRandom来确保生成的随机数更安全 31 | */ 32 | private static final SecureRandom RANDOM = new SecureRandom(); 33 | 34 | /** 35 | * 生成指定长度的随机字符串 36 | * 37 | * @param length 随机字符串长度 38 | * @return 随机字符串 39 | */ 40 | public static String generateRandomString(int length) { 41 | if (length < 1) { 42 | throw new IllegalArgumentException(); 43 | } 44 | 45 | StringBuilder sb = new StringBuilder(length); 46 | for (int i = 0; i < length; i++) { 47 | // 生成一个随机索引,用于从数据源字符串中选择字符 48 | int rndCharAt = RANDOM.nextInt(DATA_FOR_RANDOM_STRING.length()); 49 | char rndChar = DATA_FOR_RANDOM_STRING.charAt(rndCharAt); 50 | 51 | // 将选择的字符添加到字符串构建器中 52 | sb.append(rndChar); 53 | } 54 | 55 | return sb.toString(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/utils/ResultUtil.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.utils; 2 | 3 | import com.enndfp.shortlink.admin.common.convention.errorcode.ErrorCode; 4 | import com.enndfp.shortlink.admin.common.convention.exception.AbstractException; 5 | import com.enndfp.shortlink.admin.common.convention.result.Result; 6 | 7 | import java.util.Map; 8 | import java.util.Optional; 9 | 10 | /** 11 | * 全局返回对象构造器 12 | * 13 | * @author Enndfp 14 | */ 15 | public class ResultUtil { 16 | 17 | /** 18 | * 构造无返回数据成功响应 19 | */ 20 | public static Result success() { 21 | return new Result<>("200", "操作成功!", null, null); 22 | } 23 | 24 | /** 25 | * 构造带返回数据的成功响应 26 | */ 27 | public static Result success(T data) { 28 | return new Result("200", "操作成功!", data, null); 29 | } 30 | 31 | /** 32 | * 构建服务端失败响应 33 | */ 34 | public static Result failure() { 35 | return new Result<>(ErrorCode.SERVICE_ERROR.code(), ErrorCode.SERVICE_ERROR.message(), null, null); 36 | } 37 | 38 | /** 39 | * 通过 {@link AbstractException} 构建失败响应 40 | */ 41 | public static Result failure(AbstractException abstractException) { 42 | String errorCode = Optional.ofNullable(abstractException.getErrorCode()) 43 | .orElse(ErrorCode.SERVICE_ERROR.code()); 44 | String errorMessage = Optional.ofNullable(abstractException.getErrorMessage()) 45 | .orElse(ErrorCode.SERVICE_ERROR.message()); 46 | return new Result<>(errorCode, errorMessage, null, null); 47 | } 48 | 49 | /** 50 | * 通过 errorCode、errorMessage 构建失败响应 51 | */ 52 | public static Result failure(String errorCode, String errorMessage) { 53 | return new Result<>(errorCode, errorMessage, null, null); 54 | } 55 | 56 | /** 57 | * 通过 errorCode、errorMessage、data 构建失败响应 58 | */ 59 | public static Result failure(ErrorCode errorCode, Map map) { 60 | return new Result<>(errorCode.code(), errorCode.message(), map, null); 61 | } 62 | 63 | /** 64 | * 通过 errorCode、errorMessage、data 构建失败响应 65 | */ 66 | public static Result failure(ErrorCode errorCode, String message) { 67 | return new Result<>(errorCode.code(), message, null, null); 68 | } 69 | } -------------------------------------------------------------------------------- /shortlink-admin/src/main/java/com/enndfp/shortlink/admin/utils/ThrowUtil.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin.utils; 2 | 3 | 4 | import com.enndfp.shortlink.admin.common.convention.errorcode.ErrorCode; 5 | import com.enndfp.shortlink.admin.common.convention.exception.AbstractException; 6 | import com.enndfp.shortlink.admin.common.convention.exception.ClientException; 7 | import com.enndfp.shortlink.admin.common.convention.exception.RemoteException; 8 | import com.enndfp.shortlink.admin.common.convention.exception.ServerException; 9 | 10 | /** 11 | * 抛异常工具类 12 | * 13 | * @author Enndfp 14 | */ 15 | public class ThrowUtil { 16 | 17 | /** 18 | * 无条件抛出ClientException 19 | * 20 | * @param errorCode 业务错误码 21 | */ 22 | public static void throwClientException(ErrorCode errorCode) { 23 | throw new ClientException(errorCode); 24 | } 25 | 26 | /** 27 | * 无条件抛出RemoteException 28 | * 29 | * @param errorCode 业务错误码 30 | */ 31 | public static void throwRemoteException(ErrorCode errorCode) { 32 | throw new RemoteException(errorCode); 33 | } 34 | 35 | /** 36 | * 无条件抛出ServerException 37 | * 38 | * @param errorCode 业务错误码 39 | */ 40 | public static void throwServerException(ErrorCode errorCode) { 41 | throw new ServerException(errorCode); 42 | } 43 | 44 | /** 45 | * 条件成立则抛出指定的异常 46 | * 47 | * @param condition 条件 48 | * @param exception 要抛出的异常 49 | */ 50 | public static void throwIf(boolean condition, AbstractException exception) { 51 | if (condition) { 52 | throw exception; 53 | } 54 | } 55 | 56 | /** 57 | * 条件成立则抛出ClientException 58 | * 59 | * @param condition 条件 60 | * @param errorCode 业务错误码 61 | */ 62 | public static void throwClientIf(boolean condition, ErrorCode errorCode) { 63 | throwIf(condition, new ClientException(errorCode, errorCode.message())); 64 | } 65 | 66 | /** 67 | * 条件成立则抛出RemoteException 68 | * 69 | * @param condition 条件 70 | * @param errorCode 业务错误码 71 | */ 72 | public static void throwRemoteIf(boolean condition, ErrorCode errorCode) { 73 | throwIf(condition, new RemoteException(errorCode, errorCode.message())); 74 | } 75 | 76 | /** 77 | * 条件成立则抛出ServerException 78 | * 79 | * @param condition 条件 80 | * @param errorCode 业务错误码 81 | */ 82 | public static void throwServerIf(boolean condition, ErrorCode errorCode) { 83 | throwIf(condition, new ServerException(errorCode, errorCode.message())); 84 | } 85 | } 86 | 87 | -------------------------------------------------------------------------------- /shortlink-admin/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8002 3 | servlet: 4 | context-path: /api/short-link/admin/v1 5 | 6 | spring: 7 | datasource: 8 | # ShardingSphere 对 Driver 自定义,实现分库分表等隐藏逻辑 9 | driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver 10 | # ShardingSphere 配置文件路径 11 | url: jdbc:shardingsphere:classpath:shardingsphere-config-${database.env:dev}.yml 12 | data: 13 | redis: 14 | host: 127.0.0.1 15 | port: 6379 16 | password: 123321 17 | 18 | mybatis-plus: 19 | configuration: 20 | map-underscore-to-camel-case: true 21 | log-impl: org.apache.ibatis.logging.stdout.StdOutImpl 22 | global-config: 23 | db-config: 24 | logic-delete-field: isDeleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2) 25 | logic-delete-value: 1 # 逻辑已删除值(默认为 1) 26 | logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) 27 | 28 | # 日志级别 29 | logging: 30 | level: 31 | root: info -------------------------------------------------------------------------------- /shortlink-admin/src/main/resources/shardingsphere-config-dev.yml: -------------------------------------------------------------------------------- 1 | # 数据源集合 2 | dataSources: 3 | ds_0: 4 | dataSourceClassName: com.zaxxer.hikari.HikariDataSource 5 | driverClassName: com.mysql.cj.jdbc.Driver 6 | jdbcUrl: jdbc:mysql://127.0.0.1:3306/link?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true 7 | username: root 8 | password: 123456 9 | 10 | rules: 11 | - !SHARDING 12 | tables: 13 | t_user: 14 | # 真实数据节点,比如数据库源以及数据库在数据库中真实存在的 15 | actualDataNodes: ds_0.t_user_${0..15} 16 | # 分表策略 17 | tableStrategy: 18 | # 用于单分片键的标准分片场景 19 | standard: 20 | # 分片键 21 | shardingColumn: username 22 | # 分片算法,对应 rules[0].shardingAlgorithms 23 | shardingAlgorithmName: user_table_hash_mod 24 | t_group: 25 | # 真实数据节点,比如数据库源以及数据库在数据库中真实存在的 26 | actualDataNodes: ds_0.t_group_${0..15} 27 | # 分表策略 28 | tableStrategy: 29 | # 用于单分片键的标准分片场景 30 | standard: 31 | # 分片键 32 | shardingColumn: username 33 | # 分片算法,对应 rules[0].shardingAlgorithms 34 | shardingAlgorithmName: group_table_hash_mod 35 | # 分片算法 36 | shardingAlgorithms: 37 | # 数据表分片算法 38 | user_table_hash_mod: 39 | # 根据分片键 Hash 分片 40 | type: HASH_MOD 41 | # 分片数量 42 | props: 43 | sharding-count: 16 44 | # 数据表分片算法 45 | group_table_hash_mod: 46 | # 根据分片键 Hash 分片 47 | type: HASH_MOD 48 | # 分片数量 49 | props: 50 | sharding-count: 16 51 | # 数据加密存储规则 52 | - !ENCRYPT 53 | # 需要加密的表集合 54 | tables: 55 | # 用户表 56 | t_user: 57 | # 用户表中哪些字段需要进行加密 58 | columns: 59 | # 手机号字段,逻辑字段,不一定是在数据库中真实存在 60 | phone: 61 | # 手机号字段存储的密文字段,这个是数据库中真实存在的字段 62 | cipherColumn: phone 63 | # 手机号字段加密算法 64 | encryptorName: common_encryptor 65 | mail: 66 | cipherColumn: mail 67 | encryptorName: common_encryptor 68 | password: 69 | cipherColumn: password 70 | encryptorName: common_encryptor 71 | # 是否按照密文字段查询 72 | queryWithCipherColumn: true 73 | # 加密算法 74 | encryptors: 75 | # 自定义加密算法名称 76 | common_encryptor: 77 | # 加密算法类型 78 | type: AES 79 | props: 80 | # AES 加密密钥 81 | aes-key-value: d6oadClrrb9A3GWo 82 | # 展现逻辑 SQL & 真实 SQL 83 | props: 84 | sql-show: true -------------------------------------------------------------------------------- /shortlink-admin/src/main/resources/shardingsphere-config-prod.yml: -------------------------------------------------------------------------------- 1 | # 数据源集合 2 | dataSources: 3 | ds_0: 4 | dataSourceClassName: com.zaxxer.hikari.HikariDataSource 5 | driverClassName: com.mysql.cj.jdbc.Driver 6 | jdbcUrl: jdbc:mysql://127.0.0.1:3306/link?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true 7 | username: root 8 | password: 123456 9 | 10 | rules: 11 | - !SHARDING 12 | tables: 13 | t_user: 14 | # 真实数据节点,比如数据库源以及数据库在数据库中真实存在的 15 | actualDataNodes: ds_0.t_user_${0..15} 16 | # 分表策略 17 | tableStrategy: 18 | # 用于单分片键的标准分片场景 19 | standard: 20 | # 分片键 21 | shardingColumn: username 22 | # 分片算法,对应 rules[0].shardingAlgorithms 23 | shardingAlgorithmName: user_table_hash_mod 24 | # 分片算法 25 | shardingAlgorithms: 26 | # 数据表分片算法 27 | user_table_hash_mod: 28 | # 根据分片键 Hash 分片 29 | type: HASH_MOD 30 | # 分片数量 31 | props: 32 | sharding-count: 16 33 | # 数据加密存储规则 34 | - !ENCRYPT 35 | # 需要加密的表集合 36 | tables: 37 | # 用户表 38 | t_user: 39 | # 用户表中哪些字段需要进行加密 40 | columns: 41 | # 手机号字段,逻辑字段,不一定是在数据库中真实存在 42 | phone: 43 | # 手机号字段存储的密文字段,这个是数据库中真实存在的字段 44 | cipherColumn: phone 45 | # 手机号字段加密算法 46 | encryptorName: common_encryptor 47 | mail: 48 | cipherColumn: mail 49 | encryptorName: common_encryptor 50 | password: 51 | cipherColumn: password 52 | encryptorName: common_encryptor 53 | # 是否按照密文字段查询 54 | queryWithCipherColumn: true 55 | # 加密算法 56 | encryptors: 57 | # 自定义加密算法名称 58 | common_encryptor: 59 | # 加密算法类型 60 | type: AES 61 | props: 62 | # AES 加密密钥 63 | aes-key-value: d6oadClrrb9A3GWo 64 | # 展现逻辑 SQL & 真实 SQL 65 | props: 66 | sql-show: true -------------------------------------------------------------------------------- /shortlink-admin/src/test/java/com/enndfp/shortlink/admin/UserTableShardingTest.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.admin; 2 | 3 | /** 4 | * @author Enndfp 5 | */ 6 | public class UserTableShardingTest { 7 | 8 | public static final String SQL = "CREATE TABLE `t_group_%d` (\n" + 9 | "\t`id` BIGINT ( 20 ) NOT NULL AUTO_INCREMENT COMMENT 'ID',\n" + 10 | "\t`gid` VARCHAR ( 32 ) NOT NULL COMMENT '分组标识',\n" + 11 | "\t`group_name` VARCHAR ( 64 ) NOT NULL COMMENT '分组名称',\n" + 12 | "\t`username` VARCHAR ( 256 ) NOT NULL COMMENT '创建分组用户名',\n" + 13 | "\t`sort_order` INT ( 3 ) DEFAULT 0 NOT NULL COMMENT '分组排序',\n" + 14 | "\t`created_time` datetime DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '创建时间',\n" + 15 | "\t`updated_time` datetime DEFAULT CURRENT_TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',\n" + 16 | "\t`is_deleted` TINYINT ( 1 ) DEFAULT 0 NOT NULL COMMENT '删除标识 0:未删除 1:已删除',\n" + 17 | "\tPRIMARY KEY ( `id` ),\n" + 18 | "\tUNIQUE KEY `idx_unique_gid` ( `gid` ) USING BTREE \n" + 19 | ") ENGINE = INNODB DEFAULT CHARSET = utf8mb4;"; 20 | 21 | public static void main(String[] args) { 22 | for (int i = 0; i < 16; i++) { 23 | System.out.printf((SQL) + "%n", i); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /shortlink-gateway/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | shortlink 7 | com.enndfp.shortlink 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | shortlink-gateway 13 | 14 | -------------------------------------------------------------------------------- /shortlink-gateway/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8000 -------------------------------------------------------------------------------- /shortlink-project/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | shortlink 7 | com.enndfp.shortlink 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | shortlink-project 13 | 14 | 15 | 16 | org.springframework.boot 17 | spring-boot-starter-web 18 | 19 | 20 | 21 | com.baomidou 22 | mybatis-plus-boot-starter 23 | 24 | 25 | 26 | com.mysql 27 | mysql-connector-j 28 | runtime 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-jdbc 34 | 35 | 36 | 37 | cn.hutool 38 | hutool-all 39 | 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-starter-data-redis 44 | 45 | 46 | 47 | org.redisson 48 | redisson-spring-boot-starter 49 | 50 | 51 | 52 | org.apache.shardingsphere 53 | shardingsphere-jdbc-core 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /shortlink-project/src/main/java/com/enndfp/shortlink/project/ShortLinkApplication.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.project; 2 | 3 | import org.mybatis.spring.annotation.MapperScan; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | 7 | /** 8 | * @author Enndfp 9 | */ 10 | @SpringBootApplication 11 | @MapperScan("com.enndfp.shortlink.project.dao.mapper") 12 | public class ShortLinkApplication { 13 | 14 | public static void main(String[] args) { 15 | SpringApplication.run(ShortLinkApplication.class,args); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /shortlink-project/src/main/java/com/enndfp/shortlink/project/common/convention/errorcode/ErrorCode.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.project.common.convention.errorcode; 2 | 3 | /** 4 | * 错误码定义 5 | * 6 | * @author Enndfp 7 | */ 8 | public enum ErrorCode implements IErrorCode { 9 | 10 | // ========== 一级宏观错误码 客户端错误 ========== 11 | CLIENT_ERROR("A000001", "客户端请求错误"), 12 | 13 | // ========== 二级宏观错误码 ========== 14 | ORIGIN_URL_NULL("A000300", "原始链接为空"), 15 | GROUP_ID_NULL("A000301", "分组标识为空"), 16 | 17 | 18 | 19 | // ========== 一级宏观错误码 系统执行出错 ========== 20 | SERVICE_ERROR("B000001", "服务端错误"), 21 | 22 | // ========== 二级宏观错误码 ========== 23 | LINK_CREATE_ERROR("B000300", "短链接创建失败"), 24 | LINK_FREQUENT_CREATE("B000301", "短链接频繁生成,请稍后再试"), 25 | 26 | 27 | 28 | // ========== 一级宏观错误码 调用第三方服务出错 ========== 29 | REMOTE_ERROR("C000001", "调用第三方服务出错"); 30 | 31 | private final String code; 32 | 33 | private final String message; 34 | 35 | ErrorCode(String code, String message) { 36 | this.code = code; 37 | this.message = message; 38 | } 39 | 40 | @Override 41 | public String code() { 42 | return code; 43 | } 44 | 45 | @Override 46 | public String message() { 47 | return message; 48 | } 49 | } -------------------------------------------------------------------------------- /shortlink-project/src/main/java/com/enndfp/shortlink/project/common/convention/errorcode/IErrorCode.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.project.common.convention.errorcode; 2 | 3 | /** 4 | * 平台错误码 5 | * 6 | * @author Enndfp 7 | */ 8 | public interface IErrorCode { 9 | 10 | /** 11 | * 错误码 12 | * 13 | * @return 错误码 14 | */ 15 | String code(); 16 | 17 | /** 18 | * 错误信息 19 | * 20 | * @return 错误信息 21 | */ 22 | String message(); 23 | } -------------------------------------------------------------------------------- /shortlink-project/src/main/java/com/enndfp/shortlink/project/common/convention/exception/AbstractException.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.project.common.convention.exception; 2 | 3 | import com.enndfp.shortlink.project.common.convention.errorcode.IErrorCode; 4 | import lombok.Getter; 5 | import org.springframework.util.StringUtils; 6 | 7 | import java.util.Optional; 8 | 9 | /** 10 | * 抽象项目中三类异常体系,客户端异常、服务端异常以及远程服务调用异常 11 | * 12 | * @author Enndfp 13 | * @see ClientException 14 | * @see ServerException 15 | * @see RemoteException 16 | */ 17 | @Getter 18 | public abstract class AbstractException extends RuntimeException { 19 | 20 | public final String errorCode; 21 | 22 | public final String errorMessage; 23 | 24 | public AbstractException(String message, Throwable throwable, IErrorCode errorCode) { 25 | super(message, throwable); 26 | this.errorCode = errorCode.code(); 27 | this.errorMessage = Optional.ofNullable(StringUtils.hasLength(message) ? message : null).orElse(errorCode.message()); 28 | } 29 | } -------------------------------------------------------------------------------- /shortlink-project/src/main/java/com/enndfp/shortlink/project/common/convention/exception/ClientException.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.project.common.convention.exception; 2 | 3 | import com.enndfp.shortlink.project.common.convention.errorcode.ErrorCode; 4 | import com.enndfp.shortlink.project.common.convention.errorcode.IErrorCode; 5 | 6 | /** 7 | * 客户端异常 8 | * 9 | * @author Enndfp 10 | */ 11 | public class ClientException extends AbstractException { 12 | 13 | public ClientException(IErrorCode errorCode) { 14 | this(null, null, errorCode); 15 | } 16 | 17 | public ClientException(IErrorCode errorCode,String message) { 18 | this(message, null, errorCode); 19 | } 20 | 21 | public ClientException(String message) { 22 | this(message, null, ErrorCode.CLIENT_ERROR); 23 | } 24 | 25 | public ClientException(String message, IErrorCode errorCode) { 26 | this(message, null, errorCode); 27 | } 28 | 29 | public ClientException(String message, Throwable throwable, IErrorCode errorCode) { 30 | super(message, throwable, errorCode); 31 | } 32 | 33 | @Override 34 | public String toString() { 35 | return "ClientException{" + 36 | "code='" + errorCode + "'," + 37 | "message='" + errorMessage + "'" + 38 | '}'; 39 | } 40 | } -------------------------------------------------------------------------------- /shortlink-project/src/main/java/com/enndfp/shortlink/project/common/convention/exception/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.project.common.convention.exception; 2 | 3 | import com.enndfp.shortlink.project.common.convention.errorcode.ErrorCode; 4 | import com.enndfp.shortlink.project.common.convention.result.Result; 5 | import com.enndfp.shortlink.project.utils.ResultUtil; 6 | import lombok.SneakyThrows; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.validation.BindingResult; 9 | import org.springframework.validation.FieldError; 10 | import org.springframework.web.bind.MethodArgumentNotValidException; 11 | import org.springframework.web.bind.annotation.ExceptionHandler; 12 | import org.springframework.web.bind.annotation.RestControllerAdvice; 13 | 14 | import java.util.HashMap; 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | /** 19 | * 全局异常处理器 20 | * 21 | * @author Enndfp 22 | */ 23 | @Slf4j 24 | @RestControllerAdvice 25 | public class GlobalExceptionHandler { 26 | 27 | /** 28 | * 拦截参数验证异常 29 | */ 30 | @SneakyThrows 31 | @ExceptionHandler(MethodArgumentNotValidException.class) 32 | public Result methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException ex) { 33 | BindingResult bindingResult = ex.getBindingResult(); 34 | Map errorMap = getErrors(bindingResult); 35 | log.error("methodArgumentNotValidExceptionHandler: " + ex.getMessage() + errorMap, ex); 36 | return ResultUtil.failure(); 37 | } 38 | 39 | /** 40 | * 拦截自定义抛出的异常 41 | */ 42 | @ExceptionHandler(AbstractException.class) 43 | public Result abstractException(AbstractException ex) { 44 | log.error("abstractException: " + ex.getMessage(), ex); 45 | return ResultUtil.failure(ex.getErrorCode(), ex.getMessage()); 46 | } 47 | 48 | /** 49 | * 运行时异常捕获 50 | */ 51 | @ExceptionHandler(RuntimeException.class) 52 | public Result runtimeExceptionHandler(RuntimeException ex) { 53 | log.error("runtimeException: " + ex.getMessage(), ex); 54 | return ResultUtil.failure(); 55 | } 56 | 57 | /** 58 | * 从方法参数校验异常中获取详细异常信息 59 | * 60 | * @param bindingResult 61 | * @return 62 | */ 63 | private Map getErrors(BindingResult bindingResult) { 64 | 65 | Map errorMap = new HashMap<>(16); 66 | List errorList = bindingResult.getFieldErrors(); 67 | 68 | for (FieldError error : errorList) { 69 | // 错误所对应的属性字段名 70 | String field = error.getField(); 71 | // 错误所对应的信息 72 | String message = error.getDefaultMessage(); 73 | errorMap.put(field, message); 74 | } 75 | return errorMap; 76 | } 77 | } -------------------------------------------------------------------------------- /shortlink-project/src/main/java/com/enndfp/shortlink/project/common/convention/exception/RemoteException.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.project.common.convention.exception; 2 | 3 | import com.enndfp.shortlink.project.common.convention.errorcode.ErrorCode; 4 | import com.enndfp.shortlink.project.common.convention.errorcode.IErrorCode; 5 | 6 | /** 7 | * 远程服务调用异常 8 | * 9 | * @author Enndfp 10 | */ 11 | public class RemoteException extends AbstractException { 12 | 13 | public RemoteException(IErrorCode errorCode) { 14 | this(null, null, errorCode); 15 | } 16 | 17 | public RemoteException(IErrorCode errorCode,String message) { 18 | this(message, null, errorCode); 19 | } 20 | public RemoteException(String message) { 21 | this(message, null, ErrorCode.REMOTE_ERROR); 22 | } 23 | 24 | public RemoteException(String message, IErrorCode errorCode) { 25 | this(message, null, errorCode); 26 | } 27 | 28 | public RemoteException(String message, Throwable throwable, IErrorCode errorCode) { 29 | super(message, throwable, errorCode); 30 | } 31 | 32 | @Override 33 | public String toString() { 34 | return "RemoteException{" + 35 | "code='" + errorCode + "'," + 36 | "message='" + errorMessage + "'" + 37 | '}'; 38 | } 39 | } -------------------------------------------------------------------------------- /shortlink-project/src/main/java/com/enndfp/shortlink/project/common/convention/exception/ServerException.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.project.common.convention.exception; 2 | 3 | import com.enndfp.shortlink.project.common.convention.errorcode.ErrorCode; 4 | import com.enndfp.shortlink.project.common.convention.errorcode.IErrorCode; 5 | 6 | import java.util.Optional; 7 | 8 | /** 9 | * 服务端异常 10 | * 11 | * @author Enndfp 12 | */ 13 | public class ServerException extends AbstractException { 14 | 15 | public ServerException(String message) { 16 | this(message, null, ErrorCode.SERVICE_ERROR); 17 | } 18 | 19 | public ServerException(IErrorCode errorCode,String message) { 20 | this(message, null, errorCode); 21 | } 22 | 23 | public ServerException(IErrorCode errorCode) { 24 | this(null, errorCode); 25 | } 26 | 27 | public ServerException(String message, IErrorCode errorCode) { 28 | this(message, null, errorCode); 29 | } 30 | 31 | public ServerException(String message, Throwable throwable, IErrorCode errorCode) { 32 | super(Optional.ofNullable(message).orElse(errorCode.message()), throwable, errorCode); 33 | } 34 | 35 | @Override 36 | public String toString() { 37 | return "ServerException{" + 38 | "code='" + errorCode + "'," + 39 | "message='" + errorMessage + "'" + 40 | '}'; 41 | } 42 | } -------------------------------------------------------------------------------- /shortlink-project/src/main/java/com/enndfp/shortlink/project/common/convention/result/Result.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.project.common.convention.result; 2 | 3 | import com.enndfp.shortlink.project.common.convention.errorcode.IErrorCode; 4 | import lombok.Data; 5 | import lombok.experimental.Accessors; 6 | 7 | import java.io.Serial; 8 | import java.io.Serializable; 9 | 10 | /** 11 | * 全局返回对象 12 | * 13 | * @author Enndfp 14 | */ 15 | @Data 16 | @Accessors(chain = true) 17 | public class Result implements Serializable { 18 | 19 | @Serial 20 | private static final long serialVersionUID = 5679018624309023727L; 21 | 22 | /** 23 | * 返回码 24 | */ 25 | private String code; 26 | 27 | /** 28 | * 返回消息 29 | */ 30 | private String message; 31 | 32 | /** 33 | * 响应数据 34 | */ 35 | private T data; 36 | 37 | /** 38 | * 请求ID 39 | */ 40 | private String requestId; 41 | 42 | public Result(String code, String message, T data, String requestId) { 43 | this.code = code; 44 | this.message = message; 45 | this.data = data; 46 | this.requestId = requestId; 47 | } 48 | 49 | public Result(IErrorCode iErrorCode) { 50 | this(iErrorCode.code(), iErrorCode.message(), null, null); 51 | } 52 | } -------------------------------------------------------------------------------- /shortlink-project/src/main/java/com/enndfp/shortlink/project/config/MyBatisPlusConfig.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.project.config; 2 | 3 | import com.baomidou.mybatisplus.annotation.DbType; 4 | import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; 5 | import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; 6 | import org.mybatis.spring.annotation.MapperScan; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | /** 11 | * MyBatis Plus 配置 12 | * 13 | * @author Enndfp 14 | */ 15 | @Configuration 16 | public class MyBatisPlusConfig { 17 | 18 | /** 19 | * 拦截器配置 20 | */ 21 | @Bean 22 | public MybatisPlusInterceptor mybatisPlusInterceptor() { 23 | MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); 24 | // 分页插件 25 | interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); 26 | return interceptor; 27 | } 28 | } -------------------------------------------------------------------------------- /shortlink-project/src/main/java/com/enndfp/shortlink/project/config/RbloomFilterConfig.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.project.config; 2 | 3 | import org.redisson.api.RBloomFilter; 4 | import org.redisson.api.RedissonClient; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | /** 9 | * 布隆过滤器配置 10 | * 11 | * @author Enndfp 12 | */ 13 | @Configuration 14 | public class RbloomFilterConfig { 15 | 16 | /** 17 | * 防止短链接生成查询数据库的布隆过滤器 18 | */ 19 | @Bean 20 | public RBloomFilter shortUriCreateCachePenetrationBloomFilter(RedissonClient redissonClient) { 21 | // 1. 创建布隆过滤器对象 22 | RBloomFilter cachePenetrationBloomFilter = redissonClient.getBloomFilter("shortUriCreateCachePenetrationBloomFilter"); 23 | // 2. 初始化布隆过滤器(预计元素数量为100000000L,误差率为0.001) 24 | cachePenetrationBloomFilter.tryInit(100000000L, 0.001); 25 | 26 | return cachePenetrationBloomFilter; 27 | } 28 | } -------------------------------------------------------------------------------- /shortlink-project/src/main/java/com/enndfp/shortlink/project/controller/LinkController.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.project.controller; 2 | 3 | import com.baomidou.mybatisplus.core.metadata.IPage; 4 | import com.enndfp.shortlink.project.common.convention.errorcode.ErrorCode; 5 | import com.enndfp.shortlink.project.common.convention.result.Result; 6 | import com.enndfp.shortlink.project.dto.req.link.LinkCreateReqDTO; 7 | import com.enndfp.shortlink.project.dto.req.link.LinkPageReqDTO; 8 | import com.enndfp.shortlink.project.dto.resp.link.LinkCountRespDTO; 9 | import com.enndfp.shortlink.project.dto.resp.link.LinkCreateRespDTO; 10 | import com.enndfp.shortlink.project.dto.resp.link.LinkPageRespDTO; 11 | import com.enndfp.shortlink.project.service.LinkService; 12 | import com.enndfp.shortlink.project.utils.ResultUtil; 13 | import com.enndfp.shortlink.project.utils.ThrowUtil; 14 | import jakarta.annotation.Resource; 15 | import org.springframework.web.bind.annotation.*; 16 | 17 | import java.util.List; 18 | 19 | /** 20 | * 短链接控制层 21 | * 22 | * @author Enndfp 23 | */ 24 | @RestController 25 | @RequestMapping("/link") 26 | public class LinkController { 27 | 28 | @Resource 29 | private LinkService linkService; 30 | 31 | /** 32 | * 创建短链接 33 | * 34 | * @param linkCreateReqDTO 短链接创建请求传输对象 35 | * @return 短链接创建响应传输对象 36 | */ 37 | @PostMapping("/create") 38 | public Result create(@RequestBody LinkCreateReqDTO linkCreateReqDTO) { 39 | // 1. 校验请求参数 40 | ThrowUtil.throwClientIf(linkCreateReqDTO == null, ErrorCode.CLIENT_ERROR); 41 | // 2. 执行创建短链接逻辑 42 | LinkCreateRespDTO linkCreateRespDTO = linkService.create(linkCreateReqDTO); 43 | 44 | return ResultUtil.success(linkCreateRespDTO); 45 | } 46 | 47 | /** 48 | * 分页查询短链接 49 | * 50 | * @param linkPageReqDTO 短链接分页请求传输对象 51 | * @return 短链接分页响应传输对象 52 | */ 53 | @GetMapping("/page") 54 | public Result> page(@ModelAttribute LinkPageReqDTO linkPageReqDTO) { 55 | // 1. 校验请求参数 56 | ThrowUtil.throwClientIf(linkPageReqDTO == null, ErrorCode.CLIENT_ERROR); 57 | // 2. 执行分页查询短链接逻辑 58 | IPage linkPageRespDTO = linkService.pageLink(linkPageReqDTO); 59 | 60 | return ResultUtil.success(linkPageRespDTO); 61 | } 62 | 63 | /** 64 | * 统计分组下短链接数量 65 | * 66 | * @param gids 分组标识列表 67 | * @return 分组下的链接数量 68 | */ 69 | @GetMapping("/count") 70 | public Result> count(@RequestParam List gids) { 71 | // 1. 校验请求参数 72 | ThrowUtil.throwClientIf(gids == null || gids.isEmpty(), ErrorCode.CLIENT_ERROR); 73 | // 2. 统计分组下短链接数量 74 | List linkCountRespDTOList = linkService.countByGids(gids); 75 | 76 | return ResultUtil.success(linkCountRespDTOList); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /shortlink-project/src/main/java/com/enndfp/shortlink/project/dao/entity/LinkDO.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.project.dao.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.*; 4 | 5 | import java.io.Serial; 6 | import java.io.Serializable; 7 | import java.util.Date; 8 | 9 | import lombok.Data; 10 | 11 | /** 12 | * 短链接持久层实体类 13 | * 14 | * @author Enndfp 15 | */ 16 | @Data 17 | @TableName(value = "t_link") 18 | public class LinkDO implements Serializable { 19 | 20 | /** 21 | * ID 22 | */ 23 | @TableId(type = IdType.ASSIGN_ID) 24 | private Long id; 25 | 26 | /** 27 | * 域名 28 | */ 29 | private String domain; 30 | 31 | /** 32 | * 短链接 33 | */ 34 | private String shortUri; 35 | 36 | /** 37 | * 完整短链接 38 | */ 39 | private String fullShortUrl; 40 | 41 | /** 42 | * 原始链接 43 | */ 44 | private String originUrl; 45 | 46 | /** 47 | * 点击量 48 | */ 49 | private Integer clickNum; 50 | 51 | /** 52 | * 分组标识 53 | */ 54 | private String gid; 55 | 56 | /** 57 | * 网站图标 58 | */ 59 | private String favicon; 60 | 61 | /** 62 | * 启用标识 0:未启用 1:已启用 63 | */ 64 | private Integer enableStatus; 65 | 66 | /** 67 | * 创建类型 0:控制台 1:接口 68 | */ 69 | private Integer createdType; 70 | 71 | /** 72 | * 有效期类型 0:永久有效 1:用户自定义 73 | */ 74 | private Integer validDateType; 75 | 76 | /** 77 | * 有效期 78 | */ 79 | private Date validDate; 80 | 81 | /** 82 | * 描述 83 | */ 84 | private String description; 85 | 86 | /** 87 | * 创建时间 88 | */ 89 | private Date createdTime; 90 | 91 | /** 92 | * 修改时间 93 | */ 94 | private Date updatedTime; 95 | 96 | /** 97 | * 删除标识 0:未删除 1:已删除 98 | */ 99 | @TableLogic 100 | private Integer isDeleted; 101 | 102 | @Serial 103 | @TableField(exist = false) 104 | private static final long serialVersionUID = 1L; 105 | } -------------------------------------------------------------------------------- /shortlink-project/src/main/java/com/enndfp/shortlink/project/dao/mapper/LinkMapper.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.project.dao.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.enndfp.shortlink.project.dao.entity.LinkDO; 5 | 6 | /** 7 | * 短链接持久层接口 8 | * 9 | * @author Enndfp 10 | */ 11 | public interface LinkMapper extends BaseMapper { 12 | 13 | } 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /shortlink-project/src/main/java/com/enndfp/shortlink/project/dto/req/link/LinkCreateReqDTO.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.project.dto.req.link; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import lombok.Data; 5 | 6 | import java.util.Date; 7 | 8 | /** 9 | * 短链接创建请求传输对象 10 | * 11 | * @author Enndfp 12 | */ 13 | @Data 14 | public class LinkCreateReqDTO { 15 | 16 | /** 17 | * 域名 18 | */ 19 | private String domain; 20 | 21 | /** 22 | * 原始链接 23 | */ 24 | private String originUrl; 25 | 26 | /** 27 | * 分组标识 28 | */ 29 | private String gid; 30 | 31 | /** 32 | * 创建类型 0:控制台 1:接口 33 | */ 34 | private Integer createdType; 35 | 36 | /** 37 | * 有效期类型 0:永久有效 1:用户自定义 38 | */ 39 | private Integer validDateType; 40 | 41 | /** 42 | * 有效期 43 | */ 44 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") 45 | private Date validDate; 46 | 47 | /** 48 | * 描述 49 | */ 50 | private String description; 51 | } 52 | -------------------------------------------------------------------------------- /shortlink-project/src/main/java/com/enndfp/shortlink/project/dto/req/link/LinkPageReqDTO.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.project.dto.req.link; 2 | 3 | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 4 | import com.enndfp.shortlink.project.dao.entity.LinkDO; 5 | import lombok.Data; 6 | 7 | /** 8 | * 短链接分页请求传输对象 9 | * 10 | * @author Enndfp 11 | */ 12 | @Data 13 | public class LinkPageReqDTO extends Page { 14 | 15 | /** 16 | * 分组标识 17 | */ 18 | private String gid; 19 | } 20 | -------------------------------------------------------------------------------- /shortlink-project/src/main/java/com/enndfp/shortlink/project/dto/resp/link/LinkCountRespDTO.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.project.dto.resp.link; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * 链接数量返回数据传输对象 7 | * 8 | * @author Enndfp 9 | */ 10 | @Data 11 | public class LinkCountRespDTO { 12 | 13 | /** 14 | * 分组标识 15 | */ 16 | private String gid; 17 | 18 | /** 19 | * 分组下的链接数量 20 | */ 21 | private String linkCount; 22 | } 23 | -------------------------------------------------------------------------------- /shortlink-project/src/main/java/com/enndfp/shortlink/project/dto/resp/link/LinkCreateRespDTO.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.project.dto.resp.link; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.Date; 6 | 7 | /** 8 | * 短链接创建响应传输对象 9 | * 10 | * @author Enndfp 11 | */ 12 | @Data 13 | public class LinkCreateRespDTO { 14 | 15 | /** 16 | * 分组标识 17 | */ 18 | private String gid; 19 | 20 | /** 21 | * 完整短链接 22 | */ 23 | private String fullShortUrl; 24 | 25 | /** 26 | * 原始链接 27 | */ 28 | private String originUrl; 29 | } 30 | -------------------------------------------------------------------------------- /shortlink-project/src/main/java/com/enndfp/shortlink/project/dto/resp/link/LinkPageRespDTO.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.project.dto.resp.link; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import lombok.Data; 5 | 6 | import java.util.Date; 7 | 8 | /** 9 | * 短链接分页响应传输对象 10 | * 11 | * @author Enndfp 12 | */ 13 | @Data 14 | public class LinkPageRespDTO { 15 | 16 | /** 17 | * ID 18 | */ 19 | private Long id; 20 | 21 | /** 22 | * 域名 23 | */ 24 | private String domain; 25 | 26 | /** 27 | * 短链接 28 | */ 29 | private String shortUri; 30 | 31 | /** 32 | * 完整短链接 33 | */ 34 | private String fullShortUrl; 35 | 36 | /** 37 | * 原始链接 38 | */ 39 | private String originUrl; 40 | 41 | /** 42 | * 分组标识 43 | */ 44 | private String gid; 45 | 46 | /** 47 | * 网站图标 48 | */ 49 | private String favicon; 50 | 51 | /** 52 | * 有效期类型 0:永久有效 1:用户自定义 53 | */ 54 | private Integer validDateType; 55 | 56 | /** 57 | * 有效期 58 | */ 59 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") 60 | private Date validDate; 61 | 62 | /** 63 | * 描述 64 | */ 65 | private String description; 66 | 67 | /** 68 | * 创建时间 69 | */ 70 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") 71 | private Date createdTime; 72 | } 73 | -------------------------------------------------------------------------------- /shortlink-project/src/main/java/com/enndfp/shortlink/project/service/LinkService.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.project.service; 2 | 3 | import com.baomidou.mybatisplus.core.metadata.IPage; 4 | import com.baomidou.mybatisplus.extension.service.IService; 5 | import com.enndfp.shortlink.project.dao.entity.LinkDO; 6 | import com.enndfp.shortlink.project.dto.req.link.LinkCreateReqDTO; 7 | import com.enndfp.shortlink.project.dto.req.link.LinkPageReqDTO; 8 | import com.enndfp.shortlink.project.dto.resp.link.LinkCountRespDTO; 9 | import com.enndfp.shortlink.project.dto.resp.link.LinkCreateRespDTO; 10 | import com.enndfp.shortlink.project.dto.resp.link.LinkPageRespDTO; 11 | 12 | import java.util.List; 13 | 14 | /** 15 | * 短链接业务层接口 16 | * 17 | * @author Enndfp 18 | */ 19 | public interface LinkService extends IService { 20 | 21 | /** 22 | * 创建短链接 23 | * 24 | * @param linkCreateReqDTO 短链接创建请求传输对象 25 | * @return 短链接创建响应传输对象 26 | */ 27 | LinkCreateRespDTO create(LinkCreateReqDTO linkCreateReqDTO); 28 | 29 | /** 30 | * 分页查询短链接 31 | * 32 | * @param linkPageReqDTO 短链接分页请求传输对象 33 | * @return 短链接分页响应传输对象 34 | */ 35 | IPage pageLink(LinkPageReqDTO linkPageReqDTO); 36 | 37 | /** 38 | * 根据分组标识统计链接数量 39 | * 40 | * @param gids 分组标识列表 41 | * @return 链接数量返回数据传输对象列表 42 | */ 43 | List countByGids(List gids); 44 | } 45 | -------------------------------------------------------------------------------- /shortlink-project/src/main/java/com/enndfp/shortlink/project/service/impl/LinkServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.project.service.impl; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | 6 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 7 | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; 8 | import com.baomidou.mybatisplus.core.metadata.OrderItem; 9 | 10 | import cn.hutool.core.bean.BeanUtil; 11 | import cn.hutool.core.util.StrUtil; 12 | import com.baomidou.mybatisplus.core.metadata.IPage; 13 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 14 | import com.enndfp.shortlink.project.common.convention.errorcode.ErrorCode; 15 | import com.enndfp.shortlink.project.common.convention.exception.ServerException; 16 | import com.enndfp.shortlink.project.dao.entity.LinkDO; 17 | import com.enndfp.shortlink.project.dao.mapper.LinkMapper; 18 | import com.enndfp.shortlink.project.dto.req.link.LinkCreateReqDTO; 19 | import com.enndfp.shortlink.project.dto.req.link.LinkPageReqDTO; 20 | import com.enndfp.shortlink.project.dto.resp.link.LinkCountRespDTO; 21 | import com.enndfp.shortlink.project.dto.resp.link.LinkCreateRespDTO; 22 | import com.enndfp.shortlink.project.dto.resp.link.LinkPageRespDTO; 23 | import com.enndfp.shortlink.project.service.LinkService; 24 | import com.enndfp.shortlink.project.utils.HashUtil; 25 | import com.enndfp.shortlink.project.utils.ThrowUtil; 26 | import jakarta.annotation.Resource; 27 | import lombok.extern.slf4j.Slf4j; 28 | import org.redisson.api.RBloomFilter; 29 | import org.springframework.dao.DuplicateKeyException; 30 | import org.springframework.stereotype.Service; 31 | 32 | /** 33 | * 短链接业务层实现类 34 | * 35 | * @author Enndfp 36 | */ 37 | @Slf4j 38 | @Service 39 | public class LinkServiceImpl extends ServiceImpl implements LinkService { 40 | 41 | @Resource 42 | private LinkMapper linkMapper; 43 | @Resource 44 | private RBloomFilter shortUriCreateCachePenetrationBloomFilter; 45 | 46 | @Override 47 | public LinkCreateRespDTO create(LinkCreateReqDTO linkCreateReqDTO) { 48 | // 1. 拷贝请求参数到持久层实体类 49 | LinkDO linkDO = BeanUtil.toBean(linkCreateReqDTO, LinkDO.class); 50 | 51 | // 2. 生成短链接后缀 52 | String linkSuffix = generateSuffix(linkCreateReqDTO); 53 | 54 | // 3. 设置持久层实体类属性 55 | String fullShortUrl = linkCreateReqDTO.getDomain() + "/" + linkSuffix; 56 | linkDO.setFullShortUrl(fullShortUrl); 57 | linkDO.setShortUri(linkSuffix); 58 | 59 | // 4. 创建短链接 60 | try { 61 | linkMapper.insert(linkDO); 62 | } catch (DuplicateKeyException ex) { 63 | log.warn("短链接已存在,短链接:{}", fullShortUrl); 64 | throw new ServerException(ErrorCode.LINK_CREATE_ERROR); 65 | } finally { 66 | // 抛不抛异常都要加到布隆过滤器,抛异常说明并发访问布隆过滤器不存在则加入进去,不抛异常说明短链接加入数据库,也要加入进去 67 | shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl); 68 | } 69 | 70 | return BeanUtil.toBean(linkDO, LinkCreateRespDTO.class); 71 | } 72 | 73 | @Override 74 | public IPage pageLink(LinkPageReqDTO linkPageReqDTO) { 75 | // 1. 校验请求参数 76 | String gid = linkPageReqDTO.getGid(); 77 | ThrowUtil.throwClientIf(StrUtil.isBlank(gid), ErrorCode.GROUP_ID_NULL); 78 | 79 | // 2. 构造分页查询参数 80 | LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); 81 | queryWrapper.eq(LinkDO::getGid, gid); 82 | queryWrapper.eq(LinkDO::getEnableStatus, 1); 83 | queryWrapper.orderByDesc(LinkDO::getCreatedTime); 84 | 85 | // 3. 执行分页查询 86 | IPage linkPage = linkMapper.selectPage(linkPageReqDTO, queryWrapper); 87 | return linkPage.convert(linkDO -> BeanUtil.toBean(linkDO, LinkPageRespDTO.class)); 88 | } 89 | 90 | @Override 91 | public List countByGids(List gids) { 92 | // 1. 构造查询条件 93 | QueryWrapper queryWrapper = new QueryWrapper<>(); 94 | queryWrapper.select("gid, count(1) as linkCount") 95 | .eq("enable_status", 1) 96 | .in("gid", gids) 97 | .groupBy("gid"); 98 | 99 | // 2. 查询分组下的链接数量 100 | List> linkCountList = linkMapper.selectMaps(queryWrapper); 101 | 102 | return BeanUtil.copyToList(linkCountList, LinkCountRespDTO.class); 103 | } 104 | 105 | 106 | /** 107 | * 生成短链接后缀 108 | * 109 | * @param linkCreateReqDTO 短链接创建请求传输对象 110 | * @return 短链接后缀 111 | */ 112 | private String generateSuffix(LinkCreateReqDTO linkCreateReqDTO) { 113 | // 1. 校验请求参数 114 | String originUrl = linkCreateReqDTO.getOriginUrl(); 115 | String domain = linkCreateReqDTO.getDomain(); 116 | ThrowUtil.throwClientIf(StrUtil.hasBlank(originUrl, domain), ErrorCode.ORIGIN_URL_NULL); 117 | 118 | // 2. 自定义生成次数,防止生成短链接时,短链接已存在,导致死循环 119 | int customGenerateCount = 0; 120 | String shortUri; 121 | while (true) { 122 | // 如果生成次数超过10次,则抛出服务器频繁创建链接的错误 123 | ThrowUtil.throwServerIf(customGenerateCount > 10, ErrorCode.LINK_FREQUENT_CREATE); 124 | 125 | // 3. 加上时间戳,防止短链接重复 126 | originUrl += System.currentTimeMillis(); 127 | shortUri = HashUtil.hashToBase62(originUrl); 128 | String fullShortUrl = domain + "/" + shortUri; 129 | 130 | // 4. 如果短链接不存在,则跳出循环 131 | if (!shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl)) { 132 | break; 133 | } 134 | customGenerateCount++; 135 | } 136 | return shortUri; 137 | } 138 | } 139 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /shortlink-project/src/main/java/com/enndfp/shortlink/project/utils/HashUtil.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.project.utils; 2 | 3 | import cn.hutool.core.lang.hash.MurmurHash; 4 | 5 | /** 6 | * HASH 工具类 7 | * 8 | * @author Enndfp 9 | */ 10 | public class HashUtil { 11 | 12 | private static final char[] CHARS = new char[]{ 13 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 14 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 15 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' 16 | }; 17 | 18 | private static final int SIZE = CHARS.length; 19 | 20 | private static String convertDecToBase62(long num) { 21 | StringBuilder sb = new StringBuilder(); 22 | while (num > 0) { 23 | int i = (int) (num % SIZE); 24 | sb.append(CHARS[i]); 25 | num /= SIZE; 26 | } 27 | return sb.reverse().toString(); 28 | } 29 | 30 | public static String hashToBase62(String str) { 31 | int i = MurmurHash.hash32(str); 32 | long num = i < 0 ? Integer.MAX_VALUE - (long) i : i; 33 | return convertDecToBase62(num); 34 | } 35 | } -------------------------------------------------------------------------------- /shortlink-project/src/main/java/com/enndfp/shortlink/project/utils/ResultUtil.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.project.utils; 2 | 3 | import com.enndfp.shortlink.project.common.convention.errorcode.ErrorCode; 4 | import com.enndfp.shortlink.project.common.convention.exception.AbstractException; 5 | import com.enndfp.shortlink.project.common.convention.result.Result; 6 | 7 | import java.util.Map; 8 | import java.util.Optional; 9 | 10 | /** 11 | * 全局返回对象构造器 12 | * 13 | * @author Enndfp 14 | */ 15 | public class ResultUtil { 16 | 17 | /** 18 | * 构造无返回数据成功响应 19 | */ 20 | public static Result success() { 21 | return new Result<>("200", "操作成功!", null, null); 22 | } 23 | 24 | /** 25 | * 构造带返回数据的成功响应 26 | */ 27 | public static Result success(T data) { 28 | return new Result("200", "操作成功!", data, null); 29 | } 30 | 31 | /** 32 | * 构建服务端失败响应 33 | */ 34 | public static Result failure() { 35 | return new Result<>(ErrorCode.SERVICE_ERROR.code(), ErrorCode.SERVICE_ERROR.message(), null, null); 36 | } 37 | 38 | /** 39 | * 通过 {@link AbstractException} 构建失败响应 40 | */ 41 | public static Result failure(AbstractException abstractException) { 42 | String errorCode = Optional.ofNullable(abstractException.getErrorCode()) 43 | .orElse(ErrorCode.SERVICE_ERROR.code()); 44 | String errorMessage = Optional.ofNullable(abstractException.getErrorMessage()) 45 | .orElse(ErrorCode.SERVICE_ERROR.message()); 46 | return new Result<>(errorCode, errorMessage, null, null); 47 | } 48 | 49 | /** 50 | * 通过 errorCode、errorMessage 构建失败响应 51 | */ 52 | public static Result failure(String errorCode, String errorMessage) { 53 | return new Result<>(errorCode, errorMessage, null, null); 54 | } 55 | 56 | /** 57 | * 通过 errorCode、errorMessage、data 构建失败响应 58 | */ 59 | public static Result failure(ErrorCode errorCode, Map map) { 60 | return new Result<>(errorCode.code(), errorCode.message(), map, null); 61 | } 62 | 63 | /** 64 | * 通过 errorCode、errorMessage、data 构建失败响应 65 | */ 66 | public static Result failure(ErrorCode errorCode, String message) { 67 | return new Result<>(errorCode.code(), message, null, null); 68 | } 69 | } -------------------------------------------------------------------------------- /shortlink-project/src/main/java/com/enndfp/shortlink/project/utils/ThrowUtil.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.project.utils; 2 | 3 | 4 | import com.enndfp.shortlink.project.common.convention.errorcode.ErrorCode; 5 | import com.enndfp.shortlink.project.common.convention.exception.AbstractException; 6 | import com.enndfp.shortlink.project.common.convention.exception.ClientException; 7 | import com.enndfp.shortlink.project.common.convention.exception.RemoteException; 8 | import com.enndfp.shortlink.project.common.convention.exception.ServerException; 9 | 10 | /** 11 | * 抛异常工具类 12 | * 13 | * @author Enndfp 14 | */ 15 | public class ThrowUtil { 16 | 17 | /** 18 | * 无条件抛出ClientException 19 | * 20 | * @param errorCode 业务错误码 21 | */ 22 | public static void throwClientException(ErrorCode errorCode) { 23 | throw new ClientException(errorCode); 24 | } 25 | 26 | /** 27 | * 无条件抛出RemoteException 28 | * 29 | * @param errorCode 业务错误码 30 | */ 31 | public static void throwRemoteException(ErrorCode errorCode) { 32 | throw new RemoteException(errorCode); 33 | } 34 | 35 | /** 36 | * 无条件抛出ServerException 37 | * 38 | * @param errorCode 业务错误码 39 | */ 40 | public static void throwServerException(ErrorCode errorCode) { 41 | throw new ServerException(errorCode); 42 | } 43 | 44 | /** 45 | * 条件成立则抛出指定的异常 46 | * 47 | * @param condition 条件 48 | * @param exception 要抛出的异常 49 | */ 50 | public static void throwIf(boolean condition, AbstractException exception) { 51 | if (condition) { 52 | throw exception; 53 | } 54 | } 55 | 56 | /** 57 | * 条件成立则抛出ClientException 58 | * 59 | * @param condition 条件 60 | * @param errorCode 业务错误码 61 | */ 62 | public static void throwClientIf(boolean condition, ErrorCode errorCode) { 63 | throwIf(condition, new ClientException(errorCode)); 64 | } 65 | 66 | /** 67 | * 条件成立则抛出RemoteException 68 | * 69 | * @param condition 条件 70 | * @param errorCode 业务错误码 71 | */ 72 | public static void throwRemoteIf(boolean condition, ErrorCode errorCode) { 73 | throwIf(condition, new RemoteException(errorCode)); 74 | } 75 | 76 | /** 77 | * 条件成立则抛出ServerException 78 | * 79 | * @param condition 条件 80 | * @param errorCode 业务错误码 81 | */ 82 | public static void throwServerIf(boolean condition, ErrorCode errorCode) { 83 | throwIf(condition, new ServerException(errorCode)); 84 | } 85 | 86 | /** 87 | * 条件成立则抛出ClientException,包含自定义消息 88 | * 89 | * @param condition 条件 90 | * @param errorCode 业务错误码 91 | * @param message 自定义消息 92 | */ 93 | public static void throwClientIf(boolean condition, ErrorCode errorCode, String message) { 94 | if (condition) { 95 | throw new ClientException(errorCode, message); 96 | } 97 | } 98 | 99 | /** 100 | * 条件成立则抛出RemoteException,包含自定义消息 101 | * 102 | * @param condition 条件 103 | * @param errorCode 业务错误码 104 | * @param message 自定义消息 105 | */ 106 | public static void throwRemoteIf(boolean condition, ErrorCode errorCode, String message) { 107 | if (condition) { 108 | throw new RemoteException(errorCode, message); 109 | } 110 | } 111 | 112 | /** 113 | * 条件成立则抛出ServerException,包含自定义消息 114 | * 115 | * @param condition 条件 116 | * @param errorCode 业务错误码 117 | * @param message 自定义消息 118 | */ 119 | public static void throwServerIf(boolean condition, ErrorCode errorCode, String message) { 120 | if (condition) { 121 | throw new ServerException(errorCode, message); 122 | } 123 | } 124 | 125 | } 126 | 127 | -------------------------------------------------------------------------------- /shortlink-project/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8001 3 | servlet: 4 | context-path: /api/short-link/v1 5 | 6 | spring: 7 | datasource: 8 | # ShardingSphere 对 Driver 自定义,实现分库分表等隐藏逻辑 9 | driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver 10 | # ShardingSphere 配置文件路径 11 | url: jdbc:shardingsphere:classpath:shardingsphere-config-${database.env:dev}.yml 12 | data: 13 | redis: 14 | host: 127.0.0.1 15 | port: 6379 16 | password: 123321 17 | 18 | mybatis-plus: 19 | configuration: 20 | map-underscore-to-camel-case: true 21 | log-impl: org.apache.ibatis.logging.stdout.StdOutImpl 22 | global-config: 23 | db-config: 24 | logic-delete-field: isDeleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2) 25 | logic-delete-value: 1 # 逻辑已删除值(默认为 1) 26 | logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) 27 | 28 | # 日志级别 29 | logging: 30 | level: 31 | root: info -------------------------------------------------------------------------------- /shortlink-project/src/main/resources/shardingsphere-config-dev.yml: -------------------------------------------------------------------------------- 1 | # 数据源集合 2 | dataSources: 3 | ds_0: 4 | dataSourceClassName: com.zaxxer.hikari.HikariDataSource 5 | driverClassName: com.mysql.cj.jdbc.Driver 6 | jdbcUrl: jdbc:mysql://127.0.0.1:3306/link?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true 7 | username: root 8 | password: 123456 9 | 10 | rules: 11 | - !SHARDING 12 | tables: 13 | t_link: 14 | # 真实数据节点,比如数据库源以及数据库在数据库中真实存在的 15 | actualDataNodes: ds_0.t_link_${0..15} 16 | # 分表策略 17 | tableStrategy: 18 | # 用于单分片键的标准分片场景 19 | standard: 20 | # 分片键 21 | shardingColumn: gid 22 | # 分片算法,对应 rules[0].shardingAlgorithms 23 | shardingAlgorithmName: link_table_hash_mod 24 | # 分片算法 25 | shardingAlgorithms: 26 | # 数据表分片算法 27 | link_table_hash_mod: 28 | # 根据分片键 Hash 分片 29 | type: HASH_MOD 30 | # 分片数量 31 | props: 32 | sharding-count: 16 33 | # 展现逻辑 SQL & 真实 SQL 34 | props: 35 | sql-show: true -------------------------------------------------------------------------------- /shortlink-project/src/main/resources/shardingsphere-config-prod.yml: -------------------------------------------------------------------------------- 1 | # 数据源集合 2 | dataSources: 3 | ds_0: 4 | dataSourceClassName: com.zaxxer.hikari.HikariDataSource 5 | driverClassName: com.mysql.cj.jdbc.Driver 6 | jdbcUrl: jdbc:mysql://127.0.0.1:3306/link?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true 7 | username: root 8 | password: 123456 9 | 10 | rules: 11 | - !SHARDING 12 | tables: 13 | t_link: 14 | # 真实数据节点,比如数据库源以及数据库在数据库中真实存在的 15 | actualDataNodes: ds_0.t_link_${0..15} 16 | # 分表策略 17 | tableStrategy: 18 | # 用于单分片键的标准分片场景 19 | standard: 20 | # 分片键 21 | shardingColumn: gid 22 | # 分片算法,对应 rules[0].shardingAlgorithms 23 | shardingAlgorithmName: link_table_hash_mod 24 | # 分片算法 25 | shardingAlgorithms: 26 | # 数据表分片算法 27 | link_table_hash_mod: 28 | # 根据分片键 Hash 分片 29 | type: HASH_MOD 30 | # 分片数量 31 | props: 32 | sharding-count: 16 33 | # 展现逻辑 SQL & 真实 SQL 34 | props: 35 | sql-show: true -------------------------------------------------------------------------------- /shortlink-project/src/test/java/com/enndfp/shortlink/project/LinkTableShardingTest.java: -------------------------------------------------------------------------------- 1 | package com.enndfp.shortlink.project; 2 | 3 | /** 4 | * @author Enndfp 5 | */ 6 | public class LinkTableShardingTest { 7 | 8 | public static final String SQL = "CREATE TABLE `t_link_%d` (\n" + 9 | " `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',\n" + 10 | " `domain` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '域名',\n" + 11 | " `short_uri` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '短链接',\n" + 12 | " `full_short_url` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '完整短链接',\n" + 13 | " `origin_url` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '原始链接',\n" + 14 | " `click_num` int NULL DEFAULT 0 COMMENT '点击量',\n" + 15 | " `gid` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '分组标识',\n" + 16 | " `favicon` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '网站图标',\n" + 17 | " `enable_status` tinyint(1) NULL DEFAULT 1 COMMENT '启用标识 0:未启用 1:已启用',\n" + 18 | " `created_type` tinyint(1) NULL DEFAULT NULL COMMENT '创建类型 0:控制台 1:接口',\n" + 19 | " `valid_date_type` tinyint(1) NULL DEFAULT NULL COMMENT '有效期类型 0:永久有效 1:用户自定义',\n" + 20 | " `valid_date` datetime NULL DEFAULT NULL COMMENT '有效期',\n" + 21 | " `description` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '描述',\n" + 22 | " `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',\n" + 23 | " `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',\n" + 24 | " `is_deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '删除标识 0:未删除 1:已删除',\n" + 25 | " PRIMARY KEY (`id`) USING BTREE,\n" + 26 | " UNIQUE INDEX `idx_unique_full_short_url`(`full_short_url` ASC) USING BTREE\n" + 27 | ") ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;"; 28 | 29 | public static void main(String[] args) { 30 | for (int i = 0; i < 16; i++) { 31 | System.out.printf((SQL) + "%n", i); 32 | } 33 | } 34 | } --------------------------------------------------------------------------------