├── .gitignore ├── README.md ├── database.sql ├── my-project-backend ├── .gitignore ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── example │ │ │ ├── MyProjectBackendApplication.java │ │ │ ├── config │ │ │ ├── RabbitConfiguration.java │ │ │ ├── SecurityConfiguration.java │ │ │ ├── SwaggerConfiguration.java │ │ │ └── WebConfiguration.java │ │ │ ├── controller │ │ │ ├── AuthorizeController.java │ │ │ └── exception │ │ │ │ ├── ErrorPageController.java │ │ │ │ └── ValidationController.java │ │ │ ├── entity │ │ │ ├── BaseData.java │ │ │ ├── RestBean.java │ │ │ ├── dto │ │ │ │ └── Account.java │ │ │ └── vo │ │ │ │ ├── request │ │ │ │ ├── ConfirmResetVO.java │ │ │ │ ├── EmailRegisterVO.java │ │ │ │ └── EmailResetVO.java │ │ │ │ └── response │ │ │ │ └── AuthorizeVO.java │ │ │ ├── filter │ │ │ ├── CorsFilter.java │ │ │ ├── FlowLimitingFilter.java │ │ │ ├── JwtAuthenticationFilter.java │ │ │ └── RequestLogFilter.java │ │ │ ├── listener │ │ │ └── MailQueueListener.java │ │ │ ├── mapper │ │ │ └── AccountMapper.java │ │ │ ├── service │ │ │ ├── AccountService.java │ │ │ └── impl │ │ │ │ └── AccountServiceImpl.java │ │ │ └── utils │ │ │ ├── Const.java │ │ │ ├── FlowUtils.java │ │ │ ├── JwtUtils.java │ │ │ └── SnowflakeIdGenerator.java │ └── resources │ │ ├── application-dev.yml │ │ ├── application-prod.yml │ │ ├── application.yml │ │ └── logback-spring.xml │ └── test │ └── java │ └── com │ └── example │ └── MyProjectBackendApplicationTests.java └── my-project-frontend ├── .gitignore ├── .vscode └── extensions.json ├── README.md ├── index.html ├── my-project-frontend.iml ├── package-lock.json ├── package.json ├── public └── favicon.ico ├── src ├── App.vue ├── main.js ├── net │ └── index.js ├── router │ └── index.js └── views │ ├── IndexView.vue │ ├── WelcomeView.vue │ └── welcome │ ├── ForgetPage.vue │ ├── LoginPage.vue │ └── RegisterPage.vue └── vite.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | log/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 前后端分离项目(JWT方案) 2 | 采用SpringBoot3 + Vue3编写的前后端分离模版项目,集成多种技术栈,使用JWT校验方案。 3 | *** 4 | ### 后端功能与技术点 5 | 用户注册、用户登录、重置密码等基础功能以及对应接口 6 | * 采用Mybatis-Plus作为持久层框架,使用更便捷 7 | * 采用Redis存储注册/重置操作验证码,带过期时间控制 8 | * 采用RabbitMQ积压短信发送任务,再由监听器统一处理 9 | * 采用SpringSecurity作为权限校验框架,手动整合Jwt校验方案 10 | * 采用Redis进行IP地址限流处理,防刷接口 11 | * 视图层对象和数据层对象分离,编写工具方法利用反射快速互相转换 12 | * 错误和异常页面统一采用JSON格式返回,前端处理响应更统一 13 | * 手动处理跨域,采用过滤器实现 14 | * 使用Swagger作为接口文档自动生成,已自动配置登录相关接口 15 | * 采用过滤器实现对所有请求自动生成雪花ID方便线上定位问题 16 | * 针对于多环境进行处理,开发环境和生产环境采用不同的配置 17 | * 日志中包含单次请求完整信息以及对应的雪花ID,支持文件记录 18 | * 项目整体结构清晰,职责明确,注释全面,开箱即用 19 | 20 | ### 前端功能与技术点 21 | 用户注册、用户登录、重置密码等界面,以及一个简易的主页 22 | * 采用Vue-Router作为路由 23 | * 采用Axios作为异步请求框架 24 | * 采用Element-Plus作为UI组件库 25 | * 使用VueUse适配深色模式切换 26 | * 使用unplugin-auto-import按需引入,减少打包后体积 27 | -------------------------------------------------------------------------------- /database.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat MySQL Data Transfer 3 | 4 | Source Server : 本地测试环境 5 | Source Server Type : MySQL 6 | Source Server Version : 80034 (8.0.34) 7 | Source Host : localhost:3306 8 | Source Schema : test 9 | 10 | Target Server Type : MySQL 11 | Target Server Version : 80034 (8.0.34) 12 | File Encoding : 65001 13 | 14 | Date: 07/08/2023 00:03:19 15 | */ 16 | 17 | SET NAMES utf8mb4; 18 | SET FOREIGN_KEY_CHECKS = 0; 19 | 20 | -- ---------------------------- 21 | -- Table structure for db_account 22 | -- ---------------------------- 23 | DROP TABLE IF EXISTS `db_account`; 24 | CREATE TABLE `db_account` ( 25 | `id` int NOT NULL AUTO_INCREMENT, 26 | `username` varchar(255) DEFAULT NULL, 27 | `email` varchar(255) DEFAULT NULL, 28 | `password` varchar(255) DEFAULT NULL, 29 | `role` varchar(255) DEFAULT NULL, 30 | `register_time` datetime DEFAULT NULL, 31 | PRIMARY KEY (`id`), 32 | UNIQUE KEY `unique_email` (`email`), 33 | UNIQUE KEY `unique_username` (`username`) 34 | ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 35 | 36 | -- ---------------------------- 37 | -- Records of db_account 38 | -- ---------------------------- 39 | BEGIN; 40 | COMMIT; 41 | 42 | SET FOREIGN_KEY_CHECKS = 1; 43 | -------------------------------------------------------------------------------- /my-project-backend/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /my-project-backend/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.1.2 9 | 10 | 11 | com.example 12 | my-project-backend 13 | 0.0.1-SNAPSHOT 14 | my-project-backend 15 | my-project-backend 16 | 17 | 17 18 | 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-mail 24 | 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-validation 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-security 34 | 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-web 39 | 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-starter-data-redis 44 | 45 | 46 | 47 | com.baomidou 48 | mybatis-plus-boot-starter 49 | 3.5.3.1 50 | 51 | 52 | 53 | com.mysql 54 | mysql-connector-j 55 | runtime 56 | 57 | 58 | 59 | org.projectlombok 60 | lombok 61 | true 62 | 63 | 64 | 65 | org.springframework.boot 66 | spring-boot-starter-test 67 | test 68 | 69 | 70 | org.springframework.security 71 | spring-security-test 72 | test 73 | 74 | 75 | 76 | org.springframework.boot 77 | spring-boot-starter-amqp 78 | 79 | 80 | 81 | com.alibaba.fastjson2 82 | fastjson2 83 | 2.0.25 84 | 85 | 86 | 87 | com.auth0 88 | java-jwt 89 | 4.3.0 90 | 91 | 92 | 93 | org.springdoc 94 | springdoc-openapi-starter-webmvc-ui 95 | 2.1.0 96 | 97 | 98 | 99 | 100 | 101 | 102 | dev 103 | 104 | true 105 | 106 | 107 | dev 108 | 109 | 110 | 111 | 112 | prod 113 | 114 | false 115 | 116 | 117 | prod 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | org.graalvm.buildtools 126 | native-maven-plugin 127 | 128 | 129 | org.springframework.boot 130 | spring-boot-maven-plugin 131 | 132 | 133 | 134 | org.projectlombok 135 | lombok 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | src/main/resources 144 | 145 | application*.yml 146 | 147 | 148 | 149 | src/main/resources 150 | true 151 | 152 | application.yml 153 | application-${environment}.yml 154 | 155 | 156 | 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /my-project-backend/src/main/java/com/example/MyProjectBackendApplication.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class MyProjectBackendApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(MyProjectBackendApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /my-project-backend/src/main/java/com/example/config/RabbitConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.example.config; 2 | 3 | import org.springframework.amqp.core.Queue; 4 | import org.springframework.amqp.core.QueueBuilder; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | /** 9 | * RabbitMQ消息队列配置 10 | */ 11 | @Configuration 12 | public class RabbitConfiguration { 13 | @Bean("mailQueue") 14 | public Queue queue(){ 15 | return QueueBuilder 16 | .durable("mail") 17 | .build(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /my-project-backend/src/main/java/com/example/config/SecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.example.config; 2 | 3 | import com.example.entity.RestBean; 4 | import com.example.entity.dto.Account; 5 | import com.example.entity.vo.response.AuthorizeVO; 6 | import com.example.filter.JwtAuthenticationFilter; 7 | import com.example.filter.RequestLogFilter; 8 | import com.example.service.AccountService; 9 | import com.example.utils.Const; 10 | import com.example.utils.JwtUtils; 11 | import jakarta.annotation.Resource; 12 | import jakarta.servlet.http.HttpServletRequest; 13 | import jakarta.servlet.http.HttpServletResponse; 14 | import org.springframework.context.annotation.Bean; 15 | import org.springframework.context.annotation.Configuration; 16 | import org.springframework.security.access.AccessDeniedException; 17 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 18 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; 19 | import org.springframework.security.config.http.SessionCreationPolicy; 20 | import org.springframework.security.core.Authentication; 21 | import org.springframework.security.core.userdetails.User; 22 | import org.springframework.security.web.SecurityFilterChain; 23 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 24 | 25 | import java.io.IOException; 26 | import java.io.PrintWriter; 27 | 28 | /** 29 | * SpringSecurity相关配置 30 | */ 31 | @Configuration 32 | public class SecurityConfiguration { 33 | 34 | @Resource 35 | JwtAuthenticationFilter jwtAuthenticationFilter; 36 | 37 | @Resource 38 | RequestLogFilter requestLogFilter; 39 | 40 | @Resource 41 | JwtUtils utils; 42 | 43 | @Resource 44 | AccountService service; 45 | 46 | /** 47 | * 针对于 SpringSecurity 6 的新版配置方法 48 | * @param http 配置器 49 | * @return 自动构建的内置过滤器链 50 | * @throws Exception 可能的异常 51 | */ 52 | @Bean 53 | public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { 54 | return http 55 | .authorizeHttpRequests(conf -> conf 56 | .requestMatchers("/api/auth/**", "/error").permitAll() 57 | .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() 58 | .anyRequest().hasAnyRole(Const.ROLE_DEFAULT) 59 | ) 60 | .formLogin(conf -> conf 61 | .loginProcessingUrl("/api/auth/login") 62 | .failureHandler(this::handleProcess) 63 | .successHandler(this::handleProcess) 64 | .permitAll() 65 | ) 66 | .logout(conf -> conf 67 | .logoutUrl("/api/auth/logout") 68 | .logoutSuccessHandler(this::onLogoutSuccess) 69 | ) 70 | .exceptionHandling(conf -> conf 71 | .accessDeniedHandler(this::handleProcess) 72 | .authenticationEntryPoint(this::handleProcess) 73 | ) 74 | .csrf(AbstractHttpConfigurer::disable) 75 | .sessionManagement(conf -> conf 76 | .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) 77 | .addFilterBefore(requestLogFilter, UsernamePasswordAuthenticationFilter.class) 78 | .addFilterBefore(jwtAuthenticationFilter, RequestLogFilter.class) 79 | .build(); 80 | } 81 | 82 | /** 83 | * 将多种类型的Handler整合到同一个方法中,包含: 84 | * - 登录成功 85 | * - 登录失败 86 | * - 未登录拦截/无权限拦截 87 | * @param request 请求 88 | * @param response 响应 89 | * @param exceptionOrAuthentication 异常或是验证实体 90 | * @throws IOException 可能的异常 91 | */ 92 | private void handleProcess(HttpServletRequest request, 93 | HttpServletResponse response, 94 | Object exceptionOrAuthentication) throws IOException { 95 | response.setContentType("application/json;charset=utf-8"); 96 | PrintWriter writer = response.getWriter(); 97 | if(exceptionOrAuthentication instanceof AccessDeniedException exception) { 98 | writer.write(RestBean 99 | .forbidden(exception.getMessage()).asJsonString()); 100 | } else if(exceptionOrAuthentication instanceof Exception exception) { 101 | writer.write(RestBean 102 | .unauthorized(exception.getMessage()).asJsonString()); 103 | } else if(exceptionOrAuthentication instanceof Authentication authentication){ 104 | User user = (User) authentication.getPrincipal(); 105 | Account account = service.findAccountByNameOrEmail(user.getUsername()); 106 | String jwt = utils.createJwt(user, account.getUsername(), account.getId()); 107 | if(jwt == null) { 108 | writer.write(RestBean.forbidden("登录验证频繁,请稍后再试").asJsonString()); 109 | } else { 110 | AuthorizeVO vo = account.asViewObject(AuthorizeVO.class, o -> o.setToken(jwt)); 111 | vo.setExpire(utils.expireTime()); 112 | writer.write(RestBean.success(vo).asJsonString()); 113 | } 114 | } 115 | } 116 | 117 | /** 118 | * 退出登录处理,将对应的Jwt令牌列入黑名单不再使用 119 | * @param request 请求 120 | * @param response 响应 121 | * @param authentication 验证实体 122 | * @throws IOException 可能的异常 123 | */ 124 | private void onLogoutSuccess(HttpServletRequest request, 125 | HttpServletResponse response, 126 | Authentication authentication) throws IOException { 127 | response.setContentType("application/json;charset=utf-8"); 128 | PrintWriter writer = response.getWriter(); 129 | String authorization = request.getHeader("Authorization"); 130 | if(utils.invalidateJwt(authorization)) { 131 | writer.write(RestBean.success("退出登录成功").asJsonString()); 132 | return; 133 | } 134 | writer.write(RestBean.failure(400, "退出登录失败").asJsonString()); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /my-project-backend/src/main/java/com/example/config/SwaggerConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.example.config; 2 | 3 | import com.example.entity.RestBean; 4 | import com.example.entity.vo.response.AuthorizeVO; 5 | import io.swagger.v3.oas.annotations.OpenAPIDefinition; 6 | import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; 7 | import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; 8 | import io.swagger.v3.oas.annotations.security.SecurityRequirement; 9 | import io.swagger.v3.oas.annotations.security.SecurityScheme; 10 | import io.swagger.v3.oas.models.ExternalDocumentation; 11 | import io.swagger.v3.oas.models.OpenAPI; 12 | import io.swagger.v3.oas.models.Operation; 13 | import io.swagger.v3.oas.models.PathItem; 14 | import io.swagger.v3.oas.models.info.Info; 15 | import io.swagger.v3.oas.models.info.License; 16 | import io.swagger.v3.oas.models.media.Content; 17 | import io.swagger.v3.oas.models.media.MediaType; 18 | import io.swagger.v3.oas.models.parameters.QueryParameter; 19 | import io.swagger.v3.oas.models.responses.ApiResponse; 20 | import io.swagger.v3.oas.models.responses.ApiResponses; 21 | import org.springdoc.core.customizers.OpenApiCustomizer; 22 | import org.springframework.context.annotation.Bean; 23 | import org.springframework.context.annotation.Configuration; 24 | 25 | import java.util.HashMap; 26 | import java.util.List; 27 | import java.util.Map; 28 | 29 | /** 30 | * Swagger开发文档相关配置 31 | */ 32 | @Configuration 33 | @SecurityScheme(type = SecuritySchemeType.HTTP, scheme = "Bearer", 34 | name = "Authorization", in = SecuritySchemeIn.HEADER) 35 | @OpenAPIDefinition(security = { @SecurityRequirement(name = "Authorization") }) 36 | public class SwaggerConfiguration { 37 | 38 | /** 39 | * 配置文档介绍以及详细信息 40 | * @return OpenAPI 41 | */ 42 | @Bean 43 | public OpenAPI springShopOpenAPI() { 44 | return new OpenAPI() 45 | .info(new Info().title("示例项目 API 文档") 46 | .description("欢迎来到本示例项目API测试文档,在这里可以快速进行接口调试") 47 | .version("1.0") 48 | .license(new License() 49 | .name("项目开源地址") 50 | .url("https://github.com/Ketuer/SpringBoot-Vue-Template-Jwt") 51 | ) 52 | ) 53 | .externalDocs(new ExternalDocumentation() 54 | .description("我们的官方网站") 55 | .url("https://itbaima.net") 56 | ); 57 | } 58 | 59 | /** 60 | * 配置自定义的OpenApi相关信息 61 | * @return OpenApiCustomizer 62 | */ 63 | @Bean 64 | public OpenApiCustomizer customerGlobalHeaderOpenApiCustomizer() { 65 | return api -> this.authorizePathItems().forEach(api.getPaths()::addPathItem); 66 | } 67 | 68 | /** 69 | * 登录接口和退出登录接口手动添加一下 70 | * @return PathItems 71 | */ 72 | private Map authorizePathItems(){ 73 | Map map = new HashMap<>(); 74 | map.put("/api/auth/login", new PathItem() 75 | .post(new Operation() 76 | .tags(List.of("登录校验相关")) 77 | .summary("登录验证接口") 78 | .addParametersItem(new QueryParameter() 79 | .name("username") 80 | .required(true) 81 | ) 82 | .addParametersItem(new QueryParameter() 83 | .name("password") 84 | .required(true) 85 | ) 86 | .responses(new ApiResponses() 87 | .addApiResponse("200", new ApiResponse() 88 | .description("OK") 89 | .content(new Content().addMediaType("*/*", new MediaType() 90 | .example(RestBean.success(new AuthorizeVO()).asJsonString()) 91 | )) 92 | ) 93 | ) 94 | ) 95 | ); 96 | map.put("/api/auth/logout", new PathItem() 97 | .get(new Operation() 98 | .tags(List.of("登录校验相关")) 99 | .summary("退出登录接口") 100 | .responses(new ApiResponses() 101 | .addApiResponse("200", new ApiResponse() 102 | .description("OK") 103 | .content(new Content().addMediaType("*/*", new MediaType() 104 | .example(RestBean.success()) 105 | )) 106 | ) 107 | ) 108 | ) 109 | 110 | ); 111 | return map; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /my-project-backend/src/main/java/com/example/config/WebConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.example.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 6 | import org.springframework.security.crypto.password.PasswordEncoder; 7 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 8 | 9 | /** 10 | * 一般Web服务相关配置 11 | */ 12 | @Configuration 13 | public class WebConfiguration implements WebMvcConfigurer { 14 | 15 | @Bean 16 | public PasswordEncoder passwordEncoder(){ 17 | return new BCryptPasswordEncoder(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /my-project-backend/src/main/java/com/example/controller/AuthorizeController.java: -------------------------------------------------------------------------------- 1 | package com.example.controller; 2 | 3 | import com.example.entity.RestBean; 4 | import com.example.entity.vo.request.ConfirmResetVO; 5 | import com.example.entity.vo.request.EmailRegisterVO; 6 | import com.example.entity.vo.request.EmailResetVO; 7 | import com.example.service.AccountService; 8 | import io.swagger.v3.oas.annotations.Operation; 9 | import io.swagger.v3.oas.annotations.tags.Tag; 10 | import jakarta.annotation.Resource; 11 | import jakarta.servlet.http.HttpServletRequest; 12 | import jakarta.validation.Valid; 13 | import jakarta.validation.constraints.Email; 14 | import jakarta.validation.constraints.Pattern; 15 | import org.springframework.validation.annotation.Validated; 16 | import org.springframework.web.bind.annotation.*; 17 | 18 | import java.util.function.Supplier; 19 | 20 | /** 21 | * 用于验证相关Controller包含用户的注册、重置密码等操作 22 | */ 23 | @Validated 24 | @RestController 25 | @RequestMapping("/api/auth") 26 | @Tag(name = "登录校验相关", description = "包括用户登录、注册、验证码请求等操作。") 27 | public class AuthorizeController { 28 | 29 | @Resource 30 | AccountService accountService; 31 | 32 | /** 33 | * 请求邮件验证码 34 | * @param email 请求邮件 35 | * @param type 类型 36 | * @param request 请求 37 | * @return 是否请求成功 38 | */ 39 | @GetMapping("/ask-code") 40 | @Operation(summary = "请求邮件验证码") 41 | public RestBean askVerifyCode(@RequestParam @Email String email, 42 | @RequestParam @Pattern(regexp = "(register|reset)") String type, 43 | HttpServletRequest request){ 44 | return this.messageHandle(() -> 45 | accountService.registerEmailVerifyCode(type, String.valueOf(email), request.getRemoteAddr())); 46 | } 47 | 48 | /** 49 | * 进行用户注册操作,需要先请求邮件验证码 50 | * @param vo 注册信息 51 | * @return 是否注册成功 52 | */ 53 | @PostMapping("/register") 54 | @Operation(summary = "用户注册操作") 55 | public RestBean register(@RequestBody @Valid EmailRegisterVO vo){ 56 | return this.messageHandle(() -> 57 | accountService.registerEmailAccount(vo)); 58 | } 59 | 60 | /** 61 | * 执行密码重置确认,检查验证码是否正确 62 | * @param vo 密码重置信息 63 | * @return 是否操作成功 64 | */ 65 | @PostMapping("/reset-confirm") 66 | @Operation(summary = "密码重置确认") 67 | public RestBean resetConfirm(@RequestBody @Valid ConfirmResetVO vo){ 68 | return this.messageHandle(() -> accountService.resetConfirm(vo)); 69 | } 70 | 71 | /** 72 | * 执行密码重置操作 73 | * @param vo 密码重置信息 74 | * @return 是否操作成功 75 | */ 76 | @PostMapping("/reset-password") 77 | @Operation(summary = "密码重置操作") 78 | public RestBean resetPassword(@RequestBody @Valid EmailResetVO vo){ 79 | return this.messageHandle(() -> 80 | accountService.resetEmailAccountPassword(vo)); 81 | } 82 | 83 | /** 84 | * 针对于返回值为String作为错误信息的方法进行统一处理 85 | * @param action 具体操作 86 | * @return 响应结果 87 | * @param 响应结果类型 88 | */ 89 | private RestBean messageHandle(Supplier action){ 90 | String message = action.get(); 91 | if(message == null) 92 | return RestBean.success(); 93 | else 94 | return RestBean.failure(400, message); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /my-project-backend/src/main/java/com/example/controller/exception/ErrorPageController.java: -------------------------------------------------------------------------------- 1 | package com.example.controller.exception; 2 | 3 | import com.example.entity.RestBean; 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import org.springframework.boot.autoconfigure.web.servlet.error.AbstractErrorController; 6 | import org.springframework.boot.web.error.ErrorAttributeOptions; 7 | import org.springframework.boot.web.servlet.error.ErrorAttributes; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | import java.util.Map; 13 | import java.util.Optional; 14 | 15 | /** 16 | * 专用用于处理错误页面的Controller 17 | */ 18 | @RestController 19 | @RequestMapping({"${server.error.path:${error.path:/error}}"}) 20 | public class ErrorPageController extends AbstractErrorController { 21 | 22 | public ErrorPageController(ErrorAttributes errorAttributes) { 23 | super(errorAttributes); 24 | } 25 | 26 | /** 27 | * 所有错误在这里统一处理,自动解析状态码和原因 28 | * @param request 请求 29 | * @return 失败响应 30 | */ 31 | @RequestMapping 32 | public RestBean error(HttpServletRequest request) { 33 | HttpStatus status = this.getStatus(request); 34 | Map errorAttributes = this.getErrorAttributes(request, this.getAttributeOptions()); 35 | String message = this.convertErrorMessage(status) 36 | .orElse(errorAttributes.get("message").toString()); 37 | return RestBean.failure(status.value(), message); 38 | } 39 | 40 | /** 41 | * 对于一些特殊的状态码,错误信息转换 42 | * @param status 状态码 43 | * @return 错误信息 44 | */ 45 | private Optional convertErrorMessage(HttpStatus status){ 46 | String value = switch (status.value()) { 47 | case 400 -> "请求参数有误"; 48 | case 404 -> "请求的接口不存在"; 49 | case 405 -> "请求方法错误"; 50 | case 500 -> "内部错误,请联系管理员"; 51 | default -> null; 52 | }; 53 | return Optional.ofNullable(value); 54 | } 55 | 56 | /** 57 | * 错误属性获取选项,这里额外添加了错误消息和异常类型 58 | * @return 选项 59 | */ 60 | private ErrorAttributeOptions getAttributeOptions(){ 61 | return ErrorAttributeOptions 62 | .defaults() 63 | .including(ErrorAttributeOptions.Include.MESSAGE, 64 | ErrorAttributeOptions.Include.EXCEPTION); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /my-project-backend/src/main/java/com/example/controller/exception/ValidationController.java: -------------------------------------------------------------------------------- 1 | package com.example.controller.exception; 2 | 3 | import com.example.entity.RestBean; 4 | import jakarta.validation.ValidationException; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.web.bind.annotation.ExceptionHandler; 7 | import org.springframework.web.bind.annotation.RestControllerAdvice; 8 | 9 | /** 10 | * 用于接口参数校验处理的控制器 11 | */ 12 | @Slf4j 13 | @RestControllerAdvice 14 | public class ValidationController { 15 | 16 | /** 17 | * 与SpringBoot保持一致,校验不通过打印警告信息,而不是直接抛出异常 18 | * @param exception 验证异常 19 | * @return 校验结果 20 | */ 21 | @ExceptionHandler(ValidationException.class) 22 | public RestBean validateError(ValidationException exception) { 23 | log.warn("Resolved [{}: {}]", exception.getClass().getName(), exception.getMessage()); 24 | return RestBean.failure(400, "请求参数有误"); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /my-project-backend/src/main/java/com/example/entity/BaseData.java: -------------------------------------------------------------------------------- 1 | package com.example.entity; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.lang.reflect.Constructor; 7 | import java.lang.reflect.Field; 8 | import java.util.Arrays; 9 | import java.util.function.Consumer; 10 | 11 | /** 12 | * 用于DTO快速转换VO实现,只需将DTO类继承此类即可使用 13 | */ 14 | public interface BaseData { 15 | /** 16 | * 创建指定的VO类并将当前DTO对象中的所有成员变量值直接复制到VO对象中 17 | * @param clazz 指定VO类型 18 | * @param consumer 返回VO对象之前可以使用Lambda进行额外处理 19 | * @return 指定VO对象 20 | * @param 指定VO类型 21 | */ 22 | default V asViewObject(Class clazz, Consumer consumer) { 23 | V v = this.asViewObject(clazz); 24 | consumer.accept(v); 25 | return v; 26 | } 27 | 28 | /** 29 | * 创建指定的VO类并将当前DTO对象中的所有成员变量值直接复制到VO对象中 30 | * @param clazz 指定VO类型 31 | * @return 指定VO对象 32 | * @param 指定VO类型 33 | */ 34 | default V asViewObject(Class clazz) { 35 | try { 36 | Field[] fields = clazz.getDeclaredFields(); 37 | Constructor constructor = clazz.getConstructor(); 38 | V v = constructor.newInstance(); 39 | Arrays.asList(fields).forEach(field -> convert(field, v)); 40 | return v; 41 | } catch (ReflectiveOperationException exception) { 42 | Logger logger = LoggerFactory.getLogger(BaseData.class); 43 | logger.error("在VO与DTO转换时出现了一些错误", exception); 44 | throw new RuntimeException(exception.getMessage()); 45 | } 46 | } 47 | 48 | /** 49 | * 内部使用,快速将当前类中目标对象字段同名字段的值复制到目标对象字段上 50 | * @param field 目标对象字段 51 | * @param target 目标对象 52 | */ 53 | private void convert(Field field, Object target){ 54 | try { 55 | Field source = this.getClass().getDeclaredField(field.getName()); 56 | field.setAccessible(true); 57 | source.setAccessible(true); 58 | field.set(target, source.get(this)); 59 | } catch (IllegalAccessException | NoSuchFieldException ignored) {} 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /my-project-backend/src/main/java/com/example/entity/RestBean.java: -------------------------------------------------------------------------------- 1 | package com.example.entity; 2 | 3 | import com.alibaba.fastjson2.JSONObject; 4 | import com.alibaba.fastjson2.JSONWriter; 5 | import org.slf4j.MDC; 6 | 7 | import java.util.Optional; 8 | 9 | /** 10 | * 响应实体类封装,Rest风格 11 | * @param code 状态码 12 | * @param data 响应数据 13 | * @param message 其他消息 14 | * @param 响应数据类型 15 | */ 16 | public record RestBean (long id, int code, T data, String message) { 17 | public static RestBean success(T data){ 18 | return new RestBean<>(requestId(), 200, data, "请求成功"); 19 | } 20 | 21 | public static RestBean success(){ 22 | return success(null); 23 | } 24 | 25 | public static RestBean forbidden(String message){ 26 | return failure(403, message); 27 | } 28 | 29 | public static RestBean unauthorized(String message){ 30 | return failure(401, message); 31 | } 32 | 33 | public static RestBean failure(int code, String message){ 34 | return new RestBean<>(requestId(), code, null, message); 35 | } 36 | 37 | /** 38 | * 快速将当前实体转换为JSON字符串格式 39 | * @return JSON字符串 40 | */ 41 | public String asJsonString() { 42 | return JSONObject.toJSONString(this, JSONWriter.Feature.WriteNulls); 43 | } 44 | 45 | /** 46 | * 获取当前请求ID方便快速定位错误 47 | * @return ID 48 | */ 49 | private static long requestId(){ 50 | String requestId = Optional.ofNullable(MDC.get("reqId")).orElse("0"); 51 | return Long.parseLong(requestId); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /my-project-backend/src/main/java/com/example/entity/dto/Account.java: -------------------------------------------------------------------------------- 1 | package com.example.entity.dto; 2 | 3 | import com.baomidou.mybatisplus.annotation.IdType; 4 | import com.baomidou.mybatisplus.annotation.TableId; 5 | import com.baomidou.mybatisplus.annotation.TableName; 6 | import com.example.entity.BaseData; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Data; 9 | 10 | import java.util.Date; 11 | 12 | /** 13 | * 数据库中的用户信息 14 | */ 15 | @Data 16 | @TableName("db_account") 17 | @AllArgsConstructor 18 | public class Account implements BaseData { 19 | @TableId(type = IdType.AUTO) 20 | Integer id; 21 | String username; 22 | String password; 23 | String email; 24 | String role; 25 | Date registerTime; 26 | } 27 | -------------------------------------------------------------------------------- /my-project-backend/src/main/java/com/example/entity/vo/request/ConfirmResetVO.java: -------------------------------------------------------------------------------- 1 | package com.example.entity.vo.request; 2 | 3 | import jakarta.validation.constraints.Email; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import org.hibernate.validator.constraints.Length; 7 | 8 | @Data 9 | @AllArgsConstructor 10 | public class ConfirmResetVO { 11 | @Email 12 | String email; 13 | @Length(max = 6, min = 6) 14 | String code; 15 | } 16 | -------------------------------------------------------------------------------- /my-project-backend/src/main/java/com/example/entity/vo/request/EmailRegisterVO.java: -------------------------------------------------------------------------------- 1 | package com.example.entity.vo.request; 2 | 3 | import jakarta.validation.constraints.Email; 4 | import jakarta.validation.constraints.Pattern; 5 | import lombok.Data; 6 | import org.hibernate.validator.constraints.Length; 7 | 8 | /** 9 | * 用户注册表单信息 10 | */ 11 | @Data 12 | public class EmailRegisterVO { 13 | @Email 14 | String email; 15 | @Length(max = 6, min = 6) 16 | String code; 17 | @Pattern(regexp = "^[a-zA-Z0-9\\u4e00-\\u9fa5]+$") 18 | @Length(min = 1, max = 10) 19 | String username; 20 | @Length(min = 6, max = 20) 21 | String password; 22 | } 23 | -------------------------------------------------------------------------------- /my-project-backend/src/main/java/com/example/entity/vo/request/EmailResetVO.java: -------------------------------------------------------------------------------- 1 | package com.example.entity.vo.request; 2 | 3 | import jakarta.validation.constraints.Email; 4 | import jakarta.validation.constraints.Pattern; 5 | import lombok.Data; 6 | import org.hibernate.validator.constraints.Length; 7 | 8 | /** 9 | * 密码重置表单实体 10 | */ 11 | @Data 12 | public class EmailResetVO { 13 | @Email 14 | String email; 15 | @Length(max = 6, min = 6) 16 | String code; 17 | @Length(min = 6, max = 20) 18 | String password; 19 | } 20 | -------------------------------------------------------------------------------- /my-project-backend/src/main/java/com/example/entity/vo/response/AuthorizeVO.java: -------------------------------------------------------------------------------- 1 | package com.example.entity.vo.response; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.Date; 6 | 7 | /** 8 | * 登录验证成功的用户信息响应 9 | */ 10 | @Data 11 | public class AuthorizeVO { 12 | String username; 13 | String role; 14 | String token; 15 | Date expire; 16 | } 17 | -------------------------------------------------------------------------------- /my-project-backend/src/main/java/com/example/filter/CorsFilter.java: -------------------------------------------------------------------------------- 1 | package com.example.filter; 2 | 3 | import com.example.utils.Const; 4 | import jakarta.servlet.FilterChain; 5 | import jakarta.servlet.ServletException; 6 | import jakarta.servlet.http.HttpFilter; 7 | import jakarta.servlet.http.HttpServletRequest; 8 | import jakarta.servlet.http.HttpServletResponse; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.core.annotation.Order; 11 | import org.springframework.stereotype.Component; 12 | 13 | import java.io.IOException; 14 | 15 | /** 16 | * 跨域配置过滤器,仅处理跨域,添加跨域响应头 17 | */ 18 | @Component 19 | @Order(Const.ORDER_CORS) 20 | public class CorsFilter extends HttpFilter { 21 | 22 | @Value("${spring.web.cors.origin}") 23 | String origin; 24 | 25 | @Value("${spring.web.cors.credentials}") 26 | boolean credentials; 27 | 28 | @Value("${spring.web.cors.methods}") 29 | String methods; 30 | 31 | @Override 32 | protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { 33 | this.addCorsHeader(request, response); 34 | chain.doFilter(request, response); 35 | } 36 | 37 | /** 38 | * 添加所有跨域相关响应头 39 | * @param request 请求 40 | * @param response 响应 41 | */ 42 | private void addCorsHeader(HttpServletRequest request, HttpServletResponse response) { 43 | response.addHeader("Access-Control-Allow-Origin", this.resolveOrigin(request)); 44 | response.addHeader("Access-Control-Allow-Methods", this.resolveMethod()); 45 | response.addHeader("Access-Control-Allow-Headers", "Authorization, Content-Type"); 46 | if(credentials) { 47 | response.addHeader("Access-Control-Allow-Credentials", "true"); 48 | } 49 | } 50 | 51 | /** 52 | * 解析配置文件中的请求方法 53 | * @return 解析得到的请求头值 54 | */ 55 | private String resolveMethod(){ 56 | return methods.equals("*") ? "GET, HEAD, POST, PUT, DELETE, OPTIONS, TRACE, PATCH" : methods; 57 | } 58 | 59 | /** 60 | * 解析配置文件中的请求原始站点 61 | * @param request 请求 62 | * @return 解析得到的请求头值 63 | */ 64 | private String resolveOrigin(HttpServletRequest request){ 65 | return origin.equals("*") ? request.getHeader("Origin") : origin; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /my-project-backend/src/main/java/com/example/filter/FlowLimitingFilter.java: -------------------------------------------------------------------------------- 1 | package com.example.filter; 2 | 3 | import com.example.entity.RestBean; 4 | import com.example.utils.Const; 5 | import com.example.utils.FlowUtils; 6 | import jakarta.annotation.Resource; 7 | import jakarta.servlet.FilterChain; 8 | import jakarta.servlet.ServletException; 9 | import jakarta.servlet.http.HttpFilter; 10 | import jakarta.servlet.http.HttpServletRequest; 11 | import jakarta.servlet.http.HttpServletResponse; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.springframework.beans.factory.annotation.Value; 14 | import org.springframework.core.annotation.Order; 15 | import org.springframework.data.redis.core.StringRedisTemplate; 16 | import org.springframework.stereotype.Component; 17 | 18 | import java.io.IOException; 19 | import java.io.PrintWriter; 20 | 21 | /** 22 | * 限流控制过滤器 23 | * 防止用户高频请求接口,借助Redis进行限流 24 | */ 25 | @Slf4j 26 | @Component 27 | @Order(Const.ORDER_FLOW_LIMIT) 28 | public class FlowLimitingFilter extends HttpFilter { 29 | 30 | @Resource 31 | StringRedisTemplate template; 32 | //指定时间内最大请求次数限制 33 | @Value("${spring.web.flow.limit}") 34 | int limit; 35 | //计数时间周期 36 | @Value("${spring.web.flow.period}") 37 | int period; 38 | //超出请求限制封禁时间 39 | @Value("${spring.web.flow.block}") 40 | int block; 41 | 42 | @Resource 43 | FlowUtils utils; 44 | 45 | @Override 46 | protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { 47 | String address = request.getRemoteAddr(); 48 | if ("OPTIONS".equals(request.getMethod()) && !tryCount(address)) 49 | this.writeBlockMessage(response); 50 | else 51 | chain.doFilter(request, response); 52 | } 53 | 54 | /** 55 | * 尝试对指定IP地址请求计数,如果被限制则无法继续访问 56 | * @param address 请求IP地址 57 | * @return 是否操作成功 58 | */ 59 | private boolean tryCount(String address) { 60 | synchronized (address.intern()) { 61 | if(Boolean.TRUE.equals(template.hasKey(Const.FLOW_LIMIT_BLOCK + address))) 62 | return false; 63 | String counterKey = Const.FLOW_LIMIT_COUNTER + address; 64 | String blockKey = Const.FLOW_LIMIT_BLOCK + address; 65 | return utils.limitPeriodCheck(counterKey, blockKey, block, limit, period); 66 | } 67 | } 68 | 69 | /** 70 | * 为响应编写拦截内容,提示用户操作频繁 71 | * @param response 响应 72 | * @throws IOException 可能的异常 73 | */ 74 | private void writeBlockMessage(HttpServletResponse response) throws IOException { 75 | response.setStatus(429); 76 | response.setContentType("application/json;charset=utf-8"); 77 | PrintWriter writer = response.getWriter(); 78 | writer.write(RestBean.failure(429, "请求频率过快,请稍后再试").asJsonString()); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /my-project-backend/src/main/java/com/example/filter/JwtAuthenticationFilter.java: -------------------------------------------------------------------------------- 1 | package com.example.filter; 2 | 3 | import com.auth0.jwt.interfaces.DecodedJWT; 4 | import com.example.utils.Const; 5 | import com.example.utils.JwtUtils; 6 | import jakarta.annotation.Resource; 7 | import jakarta.servlet.FilterChain; 8 | import jakarta.servlet.ServletException; 9 | import jakarta.servlet.http.HttpServletRequest; 10 | import jakarta.servlet.http.HttpServletResponse; 11 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 12 | import org.springframework.security.core.context.SecurityContextHolder; 13 | import org.springframework.security.core.userdetails.UserDetails; 14 | import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; 15 | import org.springframework.stereotype.Component; 16 | import org.springframework.web.filter.OncePerRequestFilter; 17 | 18 | import java.io.IOException; 19 | 20 | /** 21 | * 用于对请求头中Jwt令牌进行校验的工具,为当前请求添加用户验证信息 22 | * 并将用户的ID存放在请求对象属性中,方便后续使用 23 | */ 24 | @Component 25 | public class JwtAuthenticationFilter extends OncePerRequestFilter { 26 | 27 | @Resource 28 | JwtUtils utils; 29 | 30 | @Override 31 | protected void doFilterInternal(HttpServletRequest request, 32 | HttpServletResponse response, 33 | FilterChain filterChain) throws ServletException, IOException { 34 | String authorization = request.getHeader("Authorization"); 35 | DecodedJWT jwt = utils.resolveJwt(authorization); 36 | if(jwt != null) { 37 | UserDetails user = utils.toUser(jwt); 38 | UsernamePasswordAuthenticationToken authentication = 39 | new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); 40 | authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); 41 | SecurityContextHolder.getContext().setAuthentication(authentication); 42 | request.setAttribute(Const.ATTR_USER_ID, utils.toId(jwt)); 43 | } 44 | filterChain.doFilter(request, response); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /my-project-backend/src/main/java/com/example/filter/RequestLogFilter.java: -------------------------------------------------------------------------------- 1 | package com.example.filter; 2 | 3 | import com.alibaba.fastjson2.JSONObject; 4 | import com.example.utils.Const; 5 | import com.example.utils.SnowflakeIdGenerator; 6 | import jakarta.annotation.Resource; 7 | import jakarta.servlet.FilterChain; 8 | import jakarta.servlet.ServletException; 9 | import jakarta.servlet.http.HttpServletRequest; 10 | import jakarta.servlet.http.HttpServletResponse; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.slf4j.MDC; 13 | import org.springframework.security.core.context.SecurityContextHolder; 14 | import org.springframework.security.core.userdetails.User; 15 | import org.springframework.stereotype.Component; 16 | import org.springframework.web.filter.OncePerRequestFilter; 17 | import org.springframework.web.util.ContentCachingResponseWrapper; 18 | 19 | import java.io.IOException; 20 | import java.util.Set; 21 | 22 | /** 23 | * 请求日志过滤器,用于记录所有用户请求信息 24 | */ 25 | @Slf4j 26 | @Component 27 | public class RequestLogFilter extends OncePerRequestFilter { 28 | 29 | @Resource 30 | SnowflakeIdGenerator generator; 31 | 32 | private final Set ignores = Set.of("/swagger-ui", "/v3/api-docs"); 33 | 34 | @Override 35 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { 36 | if(this.isIgnoreUrl(request.getServletPath())) { 37 | filterChain.doFilter(request, response); 38 | } else { 39 | long startTime = System.currentTimeMillis(); 40 | this.logRequestStart(request); 41 | ContentCachingResponseWrapper wrapper = new ContentCachingResponseWrapper(response); 42 | filterChain.doFilter(request, wrapper); 43 | this.logRequestEnd(wrapper, startTime); 44 | wrapper.copyBodyToResponse(); 45 | } 46 | } 47 | 48 | /** 49 | * 判定当前请求url是否不需要日志打印 50 | * @param url 路径 51 | * @return 是否忽略 52 | */ 53 | private boolean isIgnoreUrl(String url){ 54 | for (String ignore : ignores) { 55 | if(url.startsWith(ignore)) return true; 56 | } 57 | return false; 58 | } 59 | 60 | /** 61 | * 请求结束时的日志打印,包含处理耗时以及响应结果 62 | * @param wrapper 用于读取响应结果的包装类 63 | * @param startTime 起始时间 64 | */ 65 | public void logRequestEnd(ContentCachingResponseWrapper wrapper, long startTime){ 66 | long time = System.currentTimeMillis() - startTime; 67 | int status = wrapper.getStatus(); 68 | String content = status != 200 ? 69 | status + " 错误" : new String(wrapper.getContentAsByteArray()); 70 | log.info("请求处理耗时: {}ms | 响应结果: {}", time, content); 71 | } 72 | 73 | /** 74 | * 请求开始时的日志打印,包含请求全部信息,以及对应用户角色 75 | * @param request 请求 76 | */ 77 | public void logRequestStart(HttpServletRequest request){ 78 | long reqId = generator.nextId(); 79 | MDC.put("reqId", String.valueOf(reqId)); 80 | JSONObject object = new JSONObject(); 81 | request.getParameterMap().forEach((k, v) -> object.put(k, v.length > 0 ? v[0] : null)); 82 | Object id = request.getAttribute(Const.ATTR_USER_ID); 83 | if(id != null) { 84 | User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); 85 | log.info("请求URL: \"{}\" ({}) | 远程IP地址: {} │ 身份: {} (UID: {}) | 角色: {} | 请求参数列表: {}", 86 | request.getServletPath(), request.getMethod(), request.getRemoteAddr(), 87 | user.getUsername(), id, user.getAuthorities(), object); 88 | } else { 89 | log.info("请求URL: \"{}\" ({}) | 远程IP地址: {} │ 身份: 未验证 | 请求参数列表: {}", 90 | request.getServletPath(), request.getMethod(), request.getRemoteAddr(), object); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /my-project-backend/src/main/java/com/example/listener/MailQueueListener.java: -------------------------------------------------------------------------------- 1 | package com.example.listener; 2 | 3 | import jakarta.annotation.Resource; 4 | import org.springframework.amqp.rabbit.annotation.RabbitHandler; 5 | import org.springframework.amqp.rabbit.annotation.RabbitListener; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.mail.SimpleMailMessage; 8 | import org.springframework.mail.javamail.JavaMailSender; 9 | import org.springframework.stereotype.Component; 10 | 11 | import java.util.Map; 12 | 13 | /** 14 | * 用于处理邮件发送的消息队列监听器 15 | */ 16 | @Component 17 | @RabbitListener(queues = "mail") 18 | public class MailQueueListener { 19 | 20 | @Resource 21 | JavaMailSender sender; 22 | 23 | @Value("${spring.mail.username}") 24 | String username; 25 | 26 | /** 27 | * 处理邮件发送 28 | * @param data 邮件信息 29 | */ 30 | @RabbitHandler 31 | public void sendMailMessage(Map data) { 32 | String email = data.get("email").toString(); 33 | Integer code = (Integer) data.get("code"); 34 | SimpleMailMessage message = switch (data.get("type").toString()) { 35 | case "register" -> 36 | createMessage("欢迎注册我们的网站", 37 | "您的邮件注册验证码为: "+code+",有效时间3分钟,为了保障您的账户安全,请勿向他人泄露验证码信息。", 38 | email); 39 | case "reset" -> 40 | createMessage("您的密码重置邮件", 41 | "你好,您正在执行重置密码操作,验证码: "+code+",有效时间3分钟,如非本人操作,请无视。", 42 | email); 43 | default -> null; 44 | }; 45 | if(message == null) return; 46 | sender.send(message); 47 | } 48 | 49 | /** 50 | * 快速封装简单邮件消息实体 51 | * @param title 标题 52 | * @param content 内容 53 | * @param email 收件人 54 | * @return 邮件实体 55 | */ 56 | private SimpleMailMessage createMessage(String title, String content, String email){ 57 | SimpleMailMessage message = new SimpleMailMessage(); 58 | message.setSubject(title); 59 | message.setText(content); 60 | message.setTo(email); 61 | message.setFrom(username); 62 | return message; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /my-project-backend/src/main/java/com/example/mapper/AccountMapper.java: -------------------------------------------------------------------------------- 1 | package com.example.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.example.entity.dto.Account; 5 | import org.apache.ibatis.annotations.Mapper; 6 | 7 | @Mapper 8 | public interface AccountMapper extends BaseMapper { 9 | } 10 | -------------------------------------------------------------------------------- /my-project-backend/src/main/java/com/example/service/AccountService.java: -------------------------------------------------------------------------------- 1 | package com.example.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import com.example.entity.dto.Account; 5 | import com.example.entity.vo.request.ConfirmResetVO; 6 | import com.example.entity.vo.request.EmailRegisterVO; 7 | import com.example.entity.vo.request.EmailResetVO; 8 | import org.springframework.security.core.userdetails.UserDetailsService; 9 | 10 | public interface AccountService extends IService, UserDetailsService { 11 | Account findAccountByNameOrEmail(String text); 12 | String registerEmailVerifyCode(String type, String email, String address); 13 | String registerEmailAccount(EmailRegisterVO info); 14 | String resetEmailAccountPassword(EmailResetVO info); 15 | String resetConfirm(ConfirmResetVO info); 16 | } 17 | -------------------------------------------------------------------------------- /my-project-backend/src/main/java/com/example/service/impl/AccountServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.example.service.impl; 2 | 3 | import com.baomidou.mybatisplus.core.toolkit.Wrappers; 4 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 5 | import com.example.entity.dto.Account; 6 | import com.example.entity.vo.request.ConfirmResetVO; 7 | import com.example.entity.vo.request.EmailRegisterVO; 8 | import com.example.entity.vo.request.EmailResetVO; 9 | import com.example.mapper.AccountMapper; 10 | import com.example.service.AccountService; 11 | import com.example.utils.Const; 12 | import com.example.utils.FlowUtils; 13 | import jakarta.annotation.Resource; 14 | import jakarta.servlet.http.HttpServletRequest; 15 | import org.springframework.amqp.core.AmqpTemplate; 16 | import org.springframework.beans.factory.annotation.Value; 17 | import org.springframework.data.redis.core.StringRedisTemplate; 18 | import org.springframework.security.core.userdetails.User; 19 | import org.springframework.security.core.userdetails.UserDetails; 20 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 21 | import org.springframework.security.crypto.password.PasswordEncoder; 22 | import org.springframework.stereotype.Service; 23 | 24 | import java.util.Date; 25 | import java.util.Map; 26 | import java.util.Random; 27 | import java.util.concurrent.TimeUnit; 28 | 29 | /** 30 | * 账户信息处理相关服务 31 | */ 32 | @Service 33 | public class AccountServiceImpl extends ServiceImpl implements AccountService { 34 | 35 | //验证邮件发送冷却时间限制,秒为单位 36 | @Value("${spring.web.verify.mail-limit}") 37 | int verifyLimit; 38 | 39 | @Resource 40 | AmqpTemplate rabbitTemplate; 41 | 42 | @Resource 43 | StringRedisTemplate stringRedisTemplate; 44 | 45 | @Resource 46 | PasswordEncoder passwordEncoder; 47 | 48 | @Resource 49 | FlowUtils flow; 50 | 51 | /** 52 | * 从数据库中通过用户名或邮箱查找用户详细信息 53 | * @param username 用户名 54 | * @return 用户详细信息 55 | * @throws UsernameNotFoundException 如果用户未找到则抛出此异常 56 | */ 57 | @Override 58 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 59 | Account account = this.findAccountByNameOrEmail(username); 60 | if(account == null) 61 | throw new UsernameNotFoundException("用户名或密码错误"); 62 | return User 63 | .withUsername(username) 64 | .password(account.getPassword()) 65 | .roles(account.getRole()) 66 | .build(); 67 | } 68 | 69 | /** 70 | * 生成注册验证码存入Redis中,并将邮件发送请求提交到消息队列等待发送 71 | * @param type 类型 72 | * @param email 邮件地址 73 | * @param address 请求IP地址 74 | * @return 操作结果,null表示正常,否则为错误原因 75 | */ 76 | public String registerEmailVerifyCode(String type, String email, String address){ 77 | synchronized (address.intern()) { 78 | if(!this.verifyLimit(address)) 79 | return "请求频繁,请稍后再试"; 80 | Random random = new Random(); 81 | int code = random.nextInt(899999) + 100000; 82 | Map data = Map.of("type",type,"email", email, "code", code); 83 | rabbitTemplate.convertAndSend(Const.MQ_MAIL, data); 84 | stringRedisTemplate.opsForValue() 85 | .set(Const.VERIFY_EMAIL_DATA + email, String.valueOf(code), 3, TimeUnit.MINUTES); 86 | return null; 87 | } 88 | } 89 | 90 | /** 91 | * 邮件验证码注册账号操作,需要检查验证码是否正确以及邮箱、用户名是否存在重名 92 | * @param info 注册基本信息 93 | * @return 操作结果,null表示正常,否则为错误原因 94 | */ 95 | public String registerEmailAccount(EmailRegisterVO info){ 96 | String email = info.getEmail(); 97 | String code = this.getEmailVerifyCode(email); 98 | if(code == null) return "请先获取验证码"; 99 | if(!code.equals(info.getCode())) return "验证码错误,请重新输入"; 100 | if(this.existsAccountByEmail(email)) return "该邮件地址已被注册"; 101 | String username = info.getUsername(); 102 | if(this.existsAccountByUsername(username)) return "该用户名已被他人使用,请重新更换"; 103 | String password = passwordEncoder.encode(info.getPassword()); 104 | Account account = new Account(null, info.getUsername(), 105 | password, email, Const.ROLE_DEFAULT, new Date()); 106 | if(!this.save(account)) { 107 | return "内部错误,注册失败"; 108 | } else { 109 | this.deleteEmailVerifyCode(email); 110 | return null; 111 | } 112 | } 113 | 114 | /** 115 | * 邮件验证码重置密码操作,需要检查验证码是否正确 116 | * @param info 重置基本信息 117 | * @return 操作结果,null表示正常,否则为错误原因 118 | */ 119 | @Override 120 | public String resetEmailAccountPassword(EmailResetVO info) { 121 | String verify = resetConfirm(new ConfirmResetVO(info.getEmail(), info.getCode())); 122 | if(verify != null) return verify; 123 | String email = info.getEmail(); 124 | String password = passwordEncoder.encode(info.getPassword()); 125 | boolean update = this.update().eq("email", email).set("password", password).update(); 126 | if(update) { 127 | this.deleteEmailVerifyCode(email); 128 | } 129 | return update ? null : "更新失败,请联系管理员"; 130 | } 131 | 132 | /** 133 | * 重置密码确认操作,验证验证码是否正确 134 | * @param info 验证基本信息 135 | * @return 操作结果,null表示正常,否则为错误原因 136 | */ 137 | @Override 138 | public String resetConfirm(ConfirmResetVO info) { 139 | String email = info.getEmail(); 140 | String code = this.getEmailVerifyCode(email); 141 | if(code == null) return "请先获取验证码"; 142 | if(!code.equals(info.getCode())) return "验证码错误,请重新输入"; 143 | return null; 144 | } 145 | 146 | /** 147 | * 移除Redis中存储的邮件验证码 148 | * @param email 电邮 149 | */ 150 | private void deleteEmailVerifyCode(String email){ 151 | String key = Const.VERIFY_EMAIL_DATA + email; 152 | stringRedisTemplate.delete(key); 153 | } 154 | 155 | /** 156 | * 获取Redis中存储的邮件验证码 157 | * @param email 电邮 158 | * @return 验证码 159 | */ 160 | private String getEmailVerifyCode(String email){ 161 | String key = Const.VERIFY_EMAIL_DATA + email; 162 | return stringRedisTemplate.opsForValue().get(key); 163 | } 164 | 165 | /** 166 | * 针对IP地址进行邮件验证码获取限流 167 | * @param address 地址 168 | * @return 是否通过验证 169 | */ 170 | private boolean verifyLimit(String address) { 171 | String key = Const.VERIFY_EMAIL_LIMIT + address; 172 | return flow.limitOnceCheck(key, verifyLimit); 173 | } 174 | 175 | /** 176 | * 通过用户名或邮件地址查找用户 177 | * @param text 用户名或邮件 178 | * @return 账户实体 179 | */ 180 | public Account findAccountByNameOrEmail(String text){ 181 | return this.query() 182 | .eq("username", text).or() 183 | .eq("email", text) 184 | .one(); 185 | } 186 | 187 | /** 188 | * 查询指定邮箱的用户是否已经存在 189 | * @param email 邮箱 190 | * @return 是否存在 191 | */ 192 | private boolean existsAccountByEmail(String email){ 193 | return this.baseMapper.exists(Wrappers.query().eq("email", email)); 194 | } 195 | 196 | /** 197 | * 查询指定用户名的用户是否已经存在 198 | * @param username 用户名 199 | * @return 是否存在 200 | */ 201 | private boolean existsAccountByUsername(String username){ 202 | return this.baseMapper.exists(Wrappers.query().eq("username", username)); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /my-project-backend/src/main/java/com/example/utils/Const.java: -------------------------------------------------------------------------------- 1 | package com.example.utils; 2 | 3 | /** 4 | * 一些常量字符串整合 5 | */ 6 | public final class Const { 7 | //JWT令牌 8 | public final static String JWT_BLACK_LIST = "jwt:blacklist:"; 9 | public final static String JWT_FREQUENCY = "jwt:frequency:"; 10 | //请求频率限制 11 | public final static String FLOW_LIMIT_COUNTER = "flow:counter:"; 12 | public final static String FLOW_LIMIT_BLOCK = "flow:block:"; 13 | //邮件验证码 14 | public final static String VERIFY_EMAIL_LIMIT = "verify:email:limit:"; 15 | public final static String VERIFY_EMAIL_DATA = "verify:email:data:"; 16 | //过滤器优先级 17 | public final static int ORDER_FLOW_LIMIT = -101; 18 | public final static int ORDER_CORS = -102; 19 | //请求自定义属性 20 | public final static String ATTR_USER_ID = "userId"; 21 | //消息队列 22 | public final static String MQ_MAIL = "mail"; 23 | //用户角色 24 | public final static String ROLE_DEFAULT = "user"; 25 | 26 | } 27 | -------------------------------------------------------------------------------- /my-project-backend/src/main/java/com/example/utils/FlowUtils.java: -------------------------------------------------------------------------------- 1 | package com.example.utils; 2 | 3 | import jakarta.annotation.Resource; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.data.redis.core.StringRedisTemplate; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.util.Optional; 9 | import java.util.concurrent.TimeUnit; 10 | 11 | /** 12 | * 限流通用工具 13 | * 针对于不同的情况进行限流操作,支持限流升级 14 | */ 15 | @Slf4j 16 | @Component 17 | public class FlowUtils { 18 | 19 | @Resource 20 | StringRedisTemplate template; 21 | 22 | /** 23 | * 针对于单次频率限制,请求成功后,在冷却时间内不得再次进行请求,如3秒内不能再次发起请求 24 | * @param key 键 25 | * @param blockTime 限制时间 26 | * @return 是否通过限流检查 27 | */ 28 | public boolean limitOnceCheck(String key, int blockTime){ 29 | return this.internalCheck(key, 1, blockTime, (overclock) -> false); 30 | } 31 | 32 | /** 33 | * 针对于单次频率限制,请求成功后,在冷却时间内不得再次进行请求 34 | * 如3秒内不能再次发起请求,如果不听劝阻继续发起请求,将限制更长时间 35 | * @param key 键 36 | * @param frequency 请求频率 37 | * @param baseTime 基础限制时间 38 | * @param upgradeTime 升级限制时间 39 | * @return 是否通过限流检查 40 | */ 41 | public boolean limitOnceUpgradeCheck(String key, int frequency, int baseTime, int upgradeTime){ 42 | return this.internalCheck(key, frequency, baseTime, (overclock) -> { 43 | if (overclock) 44 | template.opsForValue().set(key, "1", upgradeTime, TimeUnit.SECONDS); 45 | return false; 46 | }); 47 | } 48 | 49 | /** 50 | * 针对于在时间段内多次请求限制,如3秒内限制请求20次,超出频率则封禁一段时间 51 | * @param counterKey 计数键 52 | * @param blockKey 封禁键 53 | * @param blockTime 封禁时间 54 | * @param frequency 请求频率 55 | * @param period 计数周期 56 | * @return 是否通过限流检查 57 | */ 58 | public boolean limitPeriodCheck(String counterKey, String blockKey, int blockTime, int frequency, int period){ 59 | return this.internalCheck(counterKey, frequency, period, (overclock) -> { 60 | if (overclock) 61 | template.opsForValue().set(blockKey, "", blockTime, TimeUnit.SECONDS); 62 | return !overclock; 63 | }); 64 | } 65 | 66 | /** 67 | * 内部使用请求限制主要逻辑 68 | * @param key 计数键 69 | * @param frequency 请求频率 70 | * @param period 计数周期 71 | * @param action 限制行为与策略 72 | * @return 是否通过限流检查 73 | */ 74 | private boolean internalCheck(String key, int frequency, int period, LimitAction action){ 75 | String count = template.opsForValue().get(key); 76 | if (count != null) { 77 | long value = Optional.ofNullable(template.opsForValue().increment(key)).orElse(0L); 78 | int c = Integer.parseInt(count); 79 | if(value != c + 1) 80 | template.expire(key, period, TimeUnit.SECONDS); 81 | return action.run(value > frequency); 82 | } else { 83 | template.opsForValue().set(key, "1", period, TimeUnit.SECONDS); 84 | return true; 85 | } 86 | } 87 | 88 | /** 89 | * 内部使用,限制行为与策略 90 | */ 91 | private interface LimitAction { 92 | boolean run(boolean overclock); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /my-project-backend/src/main/java/com/example/utils/JwtUtils.java: -------------------------------------------------------------------------------- 1 | package com.example.utils; 2 | 3 | import com.auth0.jwt.JWT; 4 | import com.auth0.jwt.JWTVerifier; 5 | import com.auth0.jwt.algorithms.Algorithm; 6 | import com.auth0.jwt.exceptions.JWTVerificationException; 7 | import com.auth0.jwt.interfaces.Claim; 8 | import com.auth0.jwt.interfaces.DecodedJWT; 9 | import jakarta.annotation.Resource; 10 | import org.springframework.beans.factory.annotation.Value; 11 | import org.springframework.data.redis.core.StringRedisTemplate; 12 | import org.springframework.security.core.GrantedAuthority; 13 | import org.springframework.security.core.userdetails.User; 14 | import org.springframework.security.core.userdetails.UserDetails; 15 | import org.springframework.stereotype.Component; 16 | 17 | import java.util.*; 18 | import java.util.concurrent.TimeUnit; 19 | 20 | /** 21 | * 用于处理Jwt令牌的工具类 22 | */ 23 | @Component 24 | public class JwtUtils { 25 | 26 | //用于给Jwt令牌签名校验的秘钥 27 | @Value("${spring.security.jwt.key}") 28 | private String key; 29 | //令牌的过期时间,以小时为单位 30 | @Value("${spring.security.jwt.expire}") 31 | private int expire; 32 | //为用户生成Jwt令牌的冷却时间,防止刷接口频繁登录生成令牌,以秒为单位 33 | @Value("${spring.security.jwt.limit.base}") 34 | private int limit_base; 35 | //用户如果继续恶意刷令牌,更严厉的封禁时间 36 | @Value("${spring.security.jwt.limit.upgrade}") 37 | private int limit_upgrade; 38 | //判定用户在冷却时间内,继续恶意刷令牌的次数 39 | @Value("${spring.security.jwt.limit.frequency}") 40 | private int limit_frequency; 41 | 42 | @Resource 43 | StringRedisTemplate template; 44 | 45 | @Resource 46 | FlowUtils utils; 47 | 48 | /** 49 | * 让指定Jwt令牌失效 50 | * @param headerToken 请求头中携带的令牌 51 | * @return 是否操作成功 52 | */ 53 | public boolean invalidateJwt(String headerToken){ 54 | String token = this.convertToken(headerToken); 55 | Algorithm algorithm = Algorithm.HMAC256(key); 56 | JWTVerifier jwtVerifier = JWT.require(algorithm).build(); 57 | try { 58 | DecodedJWT verify = jwtVerifier.verify(token); 59 | return deleteToken(verify.getId(), verify.getExpiresAt()); 60 | } catch (JWTVerificationException e) { 61 | return false; 62 | } 63 | } 64 | 65 | /** 66 | * 根据配置快速计算过期时间 67 | * @return 过期时间 68 | */ 69 | public Date expireTime() { 70 | Calendar calendar = Calendar.getInstance(); 71 | calendar.add(Calendar.HOUR, expire); 72 | return calendar.getTime(); 73 | } 74 | 75 | /** 76 | * 根据UserDetails生成对应的Jwt令牌 77 | * @param user 用户信息 78 | * @return 令牌 79 | */ 80 | public String createJwt(UserDetails user, String username, int userId) { 81 | if(this.frequencyCheck(userId)) { 82 | Algorithm algorithm = Algorithm.HMAC256(key); 83 | Date expire = this.expireTime(); 84 | return JWT.create() 85 | .withJWTId(UUID.randomUUID().toString()) 86 | .withClaim("id", userId) 87 | .withClaim("name", username) 88 | .withClaim("authorities", user.getAuthorities() 89 | .stream() 90 | .map(GrantedAuthority::getAuthority).toList()) 91 | .withExpiresAt(expire) 92 | .withIssuedAt(new Date()) 93 | .sign(algorithm); 94 | } else { 95 | return null; 96 | } 97 | } 98 | 99 | /** 100 | * 解析Jwt令牌 101 | * @param headerToken 请求头中携带的令牌 102 | * @return DecodedJWT 103 | */ 104 | public DecodedJWT resolveJwt(String headerToken){ 105 | String token = this.convertToken(headerToken); 106 | if(token == null) return null; 107 | Algorithm algorithm = Algorithm.HMAC256(key); 108 | JWTVerifier jwtVerifier = JWT.require(algorithm).build(); 109 | try { 110 | DecodedJWT verify = jwtVerifier.verify(token); 111 | if(this.isInvalidToken(verify.getId())) return null; 112 | Map claims = verify.getClaims(); 113 | return new Date().after(claims.get("exp").asDate()) ? null : verify; 114 | } catch (JWTVerificationException e) { 115 | return null; 116 | } 117 | } 118 | 119 | /** 120 | * 将jwt对象中的内容封装为UserDetails 121 | * @param jwt 已解析的Jwt对象 122 | * @return UserDetails 123 | */ 124 | public UserDetails toUser(DecodedJWT jwt) { 125 | Map claims = jwt.getClaims(); 126 | return User 127 | .withUsername(claims.get("name").asString()) 128 | .password("******") 129 | .authorities(claims.get("authorities").asArray(String.class)) 130 | .build(); 131 | } 132 | 133 | /** 134 | * 将jwt对象中的用户ID提取出来 135 | * @param jwt 已解析的Jwt对象 136 | * @return 用户ID 137 | */ 138 | public Integer toId(DecodedJWT jwt) { 139 | Map claims = jwt.getClaims(); 140 | return claims.get("id").asInt(); 141 | } 142 | 143 | /** 144 | * 频率检测,防止用户高频申请Jwt令牌,并且采用阶段封禁机制 145 | * 如果已经提示无法登录的情况下用户还在刷,那么就封禁更长时间 146 | * @param userId 用户ID 147 | * @return 是否通过频率检测 148 | */ 149 | private boolean frequencyCheck(int userId){ 150 | String key = Const.JWT_FREQUENCY + userId; 151 | return utils.limitOnceUpgradeCheck(key, limit_frequency, limit_base, limit_upgrade); 152 | } 153 | 154 | /** 155 | * 校验并转换请求头中的Token令牌 156 | * @param headerToken 请求头中的Token 157 | * @return 转换后的令牌 158 | */ 159 | private String convertToken(String headerToken){ 160 | if(headerToken == null || !headerToken.startsWith("Bearer ")) 161 | return null; 162 | return headerToken.substring(7); 163 | } 164 | 165 | /** 166 | * 将Token列入Redis黑名单中 167 | * @param uuid 令牌ID 168 | * @param time 过期时间 169 | * @return 是否操作成功 170 | */ 171 | private boolean deleteToken(String uuid, Date time){ 172 | if(this.isInvalidToken(uuid)) 173 | return false; 174 | Date now = new Date(); 175 | long expire = Math.max(time.getTime() - now.getTime(), 0); 176 | template.opsForValue().set(Const.JWT_BLACK_LIST + uuid, "", expire, TimeUnit.MILLISECONDS); 177 | return true; 178 | } 179 | 180 | /** 181 | * 验证Token是否被列入Redis黑名单 182 | * @param uuid 令牌ID 183 | * @return 是否操作成功 184 | */ 185 | private boolean isInvalidToken(String uuid){ 186 | return Boolean.TRUE.equals(template.hasKey(Const.JWT_BLACK_LIST + uuid)); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /my-project-backend/src/main/java/com/example/utils/SnowflakeIdGenerator.java: -------------------------------------------------------------------------------- 1 | package com.example.utils; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | /** 6 | * 雪花算法ID生成器 7 | */ 8 | @Component 9 | public class SnowflakeIdGenerator { 10 | private static final long START_TIMESTAMP = 1691087910202L; 11 | 12 | private static final long DATA_CENTER_ID_BITS = 5L; 13 | private static final long WORKER_ID_BITS = 5L; 14 | private static final long SEQUENCE_BITS = 12L; 15 | 16 | private static final long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS); 17 | private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS); 18 | private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS); 19 | 20 | private static final long WORKER_ID_SHIFT = SEQUENCE_BITS; 21 | private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS; 22 | private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS; 23 | 24 | private final long dataCenterId; 25 | private final long workerId; 26 | private long lastTimestamp = -1L; 27 | private long sequence = 0L; 28 | 29 | public SnowflakeIdGenerator(){ 30 | this(1, 1); 31 | } 32 | 33 | private SnowflakeIdGenerator(long dataCenterId, long workerId) { 34 | if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) { 35 | throw new IllegalArgumentException("Data center ID can't be greater than " + MAX_DATA_CENTER_ID + " or less than 0"); 36 | } 37 | if (workerId > MAX_WORKER_ID || workerId < 0) { 38 | throw new IllegalArgumentException("Worker ID can't be greater than " + MAX_WORKER_ID + " or less than 0"); 39 | } 40 | this.dataCenterId = dataCenterId; 41 | this.workerId = workerId; 42 | } 43 | 44 | /** 45 | * 生成一个新的雪花算法ID加锁 46 | * @return 雪花ID 47 | */ 48 | public synchronized long nextId() { 49 | long timestamp = getCurrentTimestamp(); 50 | if (timestamp < lastTimestamp) { 51 | throw new IllegalStateException("Clock moved backwards. Refusing to generate ID."); 52 | } 53 | if (timestamp == lastTimestamp) { 54 | sequence = (sequence + 1) & MAX_SEQUENCE; 55 | if (sequence == 0) { 56 | timestamp = getNextTimestamp(lastTimestamp); 57 | } 58 | } else { 59 | sequence = 0L; 60 | } 61 | lastTimestamp = timestamp; 62 | return ((timestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT) | 63 | (dataCenterId << DATA_CENTER_ID_SHIFT) | 64 | (workerId << WORKER_ID_SHIFT) | 65 | sequence; 66 | } 67 | 68 | private long getCurrentTimestamp() { 69 | return System.currentTimeMillis(); 70 | } 71 | 72 | private long getNextTimestamp(long lastTimestamp) { 73 | long timestamp = getCurrentTimestamp(); 74 | while (timestamp <= lastTimestamp) { 75 | timestamp = getCurrentTimestamp(); 76 | } 77 | return timestamp; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /my-project-backend/src/main/resources/application-dev.yml: -------------------------------------------------------------------------------- 1 | # 开发环境配置 2 | springdoc: 3 | paths-to-match: /api/** 4 | swagger-ui: 5 | operations-sorter: alpha 6 | spring: 7 | mail: 8 | host: smtp.163.com 9 | username: javastudy111@163.com 10 | password: VKQFYZMUSUZGSGEG 11 | rabbitmq: 12 | addresses: localhost 13 | username: admin 14 | password: admin 15 | virtual-host: / 16 | datasource: 17 | url: jdbc:mysql://localhost:3306/test 18 | username: root 19 | password: 123456 20 | driver-class-name: com.mysql.cj.jdbc.Driver 21 | security: 22 | jwt: 23 | key: 'abcdefghijklmn' 24 | expire: 72 25 | limit: 26 | base: 10 27 | upgrade: 300 28 | frequency: 30 29 | filter: 30 | order: -100 31 | web: 32 | verify: 33 | mail-limit: 60 34 | flow: 35 | period: 3 36 | limit: 50 37 | block: 30 38 | cors: 39 | origin: '*' 40 | credentials: false 41 | methods: '*' 42 | -------------------------------------------------------------------------------- /my-project-backend/src/main/resources/application-prod.yml: -------------------------------------------------------------------------------- 1 | #生产环境配置 2 | server: 3 | port: 80 4 | springdoc: 5 | api-docs: 6 | enabled: false 7 | mybatis-plus: 8 | configuration: 9 | log-impl: org.apache.ibatis.logging.stdout.StdOutImpl 10 | spring: 11 | mail: 12 | host: smtp.163.com 13 | username: javastudy111@163.com 14 | password: VKQFYZMUSUZGSGEG 15 | rabbitmq: 16 | addresses: localhost 17 | username: admin 18 | password: admin 19 | virtual-host: / 20 | datasource: 21 | url: jdbc:mysql://localhost:3306/test 22 | username: root 23 | password: 123456 24 | driver-class-name: com.mysql.cj.jdbc.Driver 25 | security: 26 | jwt: 27 | key: 'abcdefghijklmn' 28 | expire: 72 29 | limit: 30 | base: 10 31 | upgrade: 300 32 | frequency: 30 33 | filter: 34 | order: -100 35 | web: 36 | verify: 37 | mail-limit: 60 38 | flow: 39 | period: 3 40 | limit: 10 41 | block: 30 42 | cors: 43 | origin: '*' 44 | credentials: false 45 | methods: '*' 46 | -------------------------------------------------------------------------------- /my-project-backend/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | profiles: 3 | active: '@environment@' 4 | -------------------------------------------------------------------------------- /my-project-backend/src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 9 | 10 | 11 | 12 | ${CONSOLE_LOG_PATTERN} 13 | ${CONSOLE_LOG_CHARSET} 14 | 15 | 16 | 17 | 18 | 19 | ${FILE_LOG_PATTERN} 20 | ${FILE_LOG_CHARSET} 21 | 22 | 23 | log/%d{yyyy-MM-dd}-spring-%i.log 24 | true 25 | 7 26 | 10MB 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /my-project-backend/src/test/java/com/example/MyProjectBackendApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class MyProjectBackendApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /my-project-frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | -------------------------------------------------------------------------------- /my-project-frontend/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /my-project-frontend/README.md: -------------------------------------------------------------------------------- 1 | # my-project-frontend 2 | 3 | This template should help get you started developing with Vue 3 in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). 8 | 9 | ## Customize configuration 10 | 11 | See [Vite Configuration Reference](https://vitejs.dev/config/). 12 | 13 | ## Project Setup 14 | 15 | ```sh 16 | npm install 17 | ``` 18 | 19 | ### Compile and Hot-Reload for Development 20 | 21 | ```sh 22 | npm run dev 23 | ``` 24 | 25 | ### Compile and Minify for Production 26 | 27 | ```sh 28 | npm run build 29 | ``` 30 | -------------------------------------------------------------------------------- /my-project-frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Vite App 9 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /my-project-frontend/my-project-frontend.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /my-project-frontend/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-project-frontend", 3 | "version": "0.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "my-project-frontend", 9 | "version": "0.0.0", 10 | "dependencies": { 11 | "@element-plus/icons-vue": "^2.1.0", 12 | "@vueuse/core": "^10.3.0", 13 | "axios": "^1.4.0", 14 | "element-plus": "^2.3.9", 15 | "vue": "^3.3.4", 16 | "vue-router": "^4.2.4" 17 | }, 18 | "devDependencies": { 19 | "@vitejs/plugin-vue": "^4.2.3", 20 | "unplugin-auto-import": "^0.15.2", 21 | "unplugin-vue-components": "^0.24.1", 22 | "vite": "^4.4.6" 23 | } 24 | }, 25 | "node_modules/@antfu/utils": { 26 | "version": "0.7.5", 27 | "resolved": "https://registry.npmmirror.com/@antfu/utils/-/utils-0.7.5.tgz", 28 | "integrity": "sha512-dlR6LdS+0SzOAPx/TPRhnoi7hE251OVeT2Snw0RguNbBSbjUHdWr0l3vcUUDg26rEysT89kCbtw1lVorBXLLCg==", 29 | "dev": true 30 | }, 31 | "node_modules/@babel/parser": { 32 | "version": "7.22.7", 33 | "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.22.7.tgz", 34 | "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", 35 | "bin": { 36 | "parser": "bin/babel-parser.js" 37 | }, 38 | "engines": { 39 | "node": ">=6.0.0" 40 | } 41 | }, 42 | "node_modules/@ctrl/tinycolor": { 43 | "version": "3.6.0", 44 | "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.0.tgz", 45 | "integrity": "sha512-/Z3l6pXthq0JvMYdUFyX9j0MaCltlIn6mfh9jLyQwg5aPKxkyNa0PTHtU1AlFXLNk55ZuAeJRcpvq+tmLfKmaQ==", 46 | "engines": { 47 | "node": ">=10" 48 | } 49 | }, 50 | "node_modules/@element-plus/icons-vue": { 51 | "version": "2.1.0", 52 | "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.1.0.tgz", 53 | "integrity": "sha512-PSBn3elNoanENc1vnCfh+3WA9fimRC7n+fWkf3rE5jvv+aBohNHABC/KAR5KWPecxWxDTVT1ERpRbOMRcOV/vA==", 54 | "peerDependencies": { 55 | "vue": "^3.2.0" 56 | } 57 | }, 58 | "node_modules/@esbuild/android-arm": { 59 | "version": "0.18.17", 60 | "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.18.17.tgz", 61 | "integrity": "sha512-wHsmJG/dnL3OkpAcwbgoBTTMHVi4Uyou3F5mf58ZtmUyIKfcdA7TROav/6tCzET4A3QW2Q2FC+eFneMU+iyOxg==", 62 | "cpu": [ 63 | "arm" 64 | ], 65 | "dev": true, 66 | "optional": true, 67 | "os": [ 68 | "android" 69 | ], 70 | "engines": { 71 | "node": ">=12" 72 | } 73 | }, 74 | "node_modules/@esbuild/android-arm64": { 75 | "version": "0.18.17", 76 | "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.18.17.tgz", 77 | "integrity": "sha512-9np+YYdNDed5+Jgr1TdWBsozZ85U1Oa3xW0c7TWqH0y2aGghXtZsuT8nYRbzOMcl0bXZXjOGbksoTtVOlWrRZg==", 78 | "cpu": [ 79 | "arm64" 80 | ], 81 | "dev": true, 82 | "optional": true, 83 | "os": [ 84 | "android" 85 | ], 86 | "engines": { 87 | "node": ">=12" 88 | } 89 | }, 90 | "node_modules/@esbuild/android-x64": { 91 | "version": "0.18.17", 92 | "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.18.17.tgz", 93 | "integrity": "sha512-O+FeWB/+xya0aLg23hHEM2E3hbfwZzjqumKMSIqcHbNvDa+dza2D0yLuymRBQQnC34CWrsJUXyH2MG5VnLd6uw==", 94 | "cpu": [ 95 | "x64" 96 | ], 97 | "dev": true, 98 | "optional": true, 99 | "os": [ 100 | "android" 101 | ], 102 | "engines": { 103 | "node": ">=12" 104 | } 105 | }, 106 | "node_modules/@esbuild/darwin-arm64": { 107 | "version": "0.18.17", 108 | "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.17.tgz", 109 | "integrity": "sha512-M9uJ9VSB1oli2BE/dJs3zVr9kcCBBsE883prage1NWz6pBS++1oNn/7soPNS3+1DGj0FrkSvnED4Bmlu1VAE9g==", 110 | "cpu": [ 111 | "arm64" 112 | ], 113 | "dev": true, 114 | "optional": true, 115 | "os": [ 116 | "darwin" 117 | ], 118 | "engines": { 119 | "node": ">=12" 120 | } 121 | }, 122 | "node_modules/@esbuild/darwin-x64": { 123 | "version": "0.18.17", 124 | "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.18.17.tgz", 125 | "integrity": "sha512-XDre+J5YeIJDMfp3n0279DFNrGCXlxOuGsWIkRb1NThMZ0BsrWXoTg23Jer7fEXQ9Ye5QjrvXpxnhzl3bHtk0g==", 126 | "cpu": [ 127 | "x64" 128 | ], 129 | "dev": true, 130 | "optional": true, 131 | "os": [ 132 | "darwin" 133 | ], 134 | "engines": { 135 | "node": ">=12" 136 | } 137 | }, 138 | "node_modules/@esbuild/freebsd-arm64": { 139 | "version": "0.18.17", 140 | "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.17.tgz", 141 | "integrity": "sha512-cjTzGa3QlNfERa0+ptykyxs5A6FEUQQF0MuilYXYBGdBxD3vxJcKnzDlhDCa1VAJCmAxed6mYhA2KaJIbtiNuQ==", 142 | "cpu": [ 143 | "arm64" 144 | ], 145 | "dev": true, 146 | "optional": true, 147 | "os": [ 148 | "freebsd" 149 | ], 150 | "engines": { 151 | "node": ">=12" 152 | } 153 | }, 154 | "node_modules/@esbuild/freebsd-x64": { 155 | "version": "0.18.17", 156 | "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.17.tgz", 157 | "integrity": "sha512-sOxEvR8d7V7Kw8QqzxWc7bFfnWnGdaFBut1dRUYtu+EIRXefBc/eIsiUiShnW0hM3FmQ5Zf27suDuHsKgZ5QrA==", 158 | "cpu": [ 159 | "x64" 160 | ], 161 | "dev": true, 162 | "optional": true, 163 | "os": [ 164 | "freebsd" 165 | ], 166 | "engines": { 167 | "node": ">=12" 168 | } 169 | }, 170 | "node_modules/@esbuild/linux-arm": { 171 | "version": "0.18.17", 172 | "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.18.17.tgz", 173 | "integrity": "sha512-2d3Lw6wkwgSLC2fIvXKoMNGVaeY8qdN0IC3rfuVxJp89CRfA3e3VqWifGDfuakPmp90+ZirmTfye1n4ncjv2lg==", 174 | "cpu": [ 175 | "arm" 176 | ], 177 | "dev": true, 178 | "optional": true, 179 | "os": [ 180 | "linux" 181 | ], 182 | "engines": { 183 | "node": ">=12" 184 | } 185 | }, 186 | "node_modules/@esbuild/linux-arm64": { 187 | "version": "0.18.17", 188 | "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.18.17.tgz", 189 | "integrity": "sha512-c9w3tE7qA3CYWjT+M3BMbwMt+0JYOp3vCMKgVBrCl1nwjAlOMYzEo+gG7QaZ9AtqZFj5MbUc885wuBBmu6aADQ==", 190 | "cpu": [ 191 | "arm64" 192 | ], 193 | "dev": true, 194 | "optional": true, 195 | "os": [ 196 | "linux" 197 | ], 198 | "engines": { 199 | "node": ">=12" 200 | } 201 | }, 202 | "node_modules/@esbuild/linux-ia32": { 203 | "version": "0.18.17", 204 | "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.18.17.tgz", 205 | "integrity": "sha512-1DS9F966pn5pPnqXYz16dQqWIB0dmDfAQZd6jSSpiT9eX1NzKh07J6VKR3AoXXXEk6CqZMojiVDSZi1SlmKVdg==", 206 | "cpu": [ 207 | "ia32" 208 | ], 209 | "dev": true, 210 | "optional": true, 211 | "os": [ 212 | "linux" 213 | ], 214 | "engines": { 215 | "node": ">=12" 216 | } 217 | }, 218 | "node_modules/@esbuild/linux-loong64": { 219 | "version": "0.18.17", 220 | "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.18.17.tgz", 221 | "integrity": "sha512-EvLsxCk6ZF0fpCB6w6eOI2Fc8KW5N6sHlIovNe8uOFObL2O+Mr0bflPHyHwLT6rwMg9r77WOAWb2FqCQrVnwFg==", 222 | "cpu": [ 223 | "loong64" 224 | ], 225 | "dev": true, 226 | "optional": true, 227 | "os": [ 228 | "linux" 229 | ], 230 | "engines": { 231 | "node": ">=12" 232 | } 233 | }, 234 | "node_modules/@esbuild/linux-mips64el": { 235 | "version": "0.18.17", 236 | "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.17.tgz", 237 | "integrity": "sha512-e0bIdHA5p6l+lwqTE36NAW5hHtw2tNRmHlGBygZC14QObsA3bD4C6sXLJjvnDIjSKhW1/0S3eDy+QmX/uZWEYQ==", 238 | "cpu": [ 239 | "mips64el" 240 | ], 241 | "dev": true, 242 | "optional": true, 243 | "os": [ 244 | "linux" 245 | ], 246 | "engines": { 247 | "node": ">=12" 248 | } 249 | }, 250 | "node_modules/@esbuild/linux-ppc64": { 251 | "version": "0.18.17", 252 | "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.17.tgz", 253 | "integrity": "sha512-BAAilJ0M5O2uMxHYGjFKn4nJKF6fNCdP1E0o5t5fvMYYzeIqy2JdAP88Az5LHt9qBoUa4tDaRpfWt21ep5/WqQ==", 254 | "cpu": [ 255 | "ppc64" 256 | ], 257 | "dev": true, 258 | "optional": true, 259 | "os": [ 260 | "linux" 261 | ], 262 | "engines": { 263 | "node": ">=12" 264 | } 265 | }, 266 | "node_modules/@esbuild/linux-riscv64": { 267 | "version": "0.18.17", 268 | "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.17.tgz", 269 | "integrity": "sha512-Wh/HW2MPnC3b8BqRSIme/9Zhab36PPH+3zam5pqGRH4pE+4xTrVLx2+XdGp6fVS3L2x+DrsIcsbMleex8fbE6g==", 270 | "cpu": [ 271 | "riscv64" 272 | ], 273 | "dev": true, 274 | "optional": true, 275 | "os": [ 276 | "linux" 277 | ], 278 | "engines": { 279 | "node": ">=12" 280 | } 281 | }, 282 | "node_modules/@esbuild/linux-s390x": { 283 | "version": "0.18.17", 284 | "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.18.17.tgz", 285 | "integrity": "sha512-j/34jAl3ul3PNcK3pfI0NSlBANduT2UO5kZ7FCaK33XFv3chDhICLY8wJJWIhiQ+YNdQ9dxqQctRg2bvrMlYgg==", 286 | "cpu": [ 287 | "s390x" 288 | ], 289 | "dev": true, 290 | "optional": true, 291 | "os": [ 292 | "linux" 293 | ], 294 | "engines": { 295 | "node": ">=12" 296 | } 297 | }, 298 | "node_modules/@esbuild/linux-x64": { 299 | "version": "0.18.17", 300 | "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.18.17.tgz", 301 | "integrity": "sha512-QM50vJ/y+8I60qEmFxMoxIx4de03pGo2HwxdBeFd4nMh364X6TIBZ6VQ5UQmPbQWUVWHWws5MmJXlHAXvJEmpQ==", 302 | "cpu": [ 303 | "x64" 304 | ], 305 | "dev": true, 306 | "optional": true, 307 | "os": [ 308 | "linux" 309 | ], 310 | "engines": { 311 | "node": ">=12" 312 | } 313 | }, 314 | "node_modules/@esbuild/netbsd-x64": { 315 | "version": "0.18.17", 316 | "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.17.tgz", 317 | "integrity": "sha512-/jGlhWR7Sj9JPZHzXyyMZ1RFMkNPjC6QIAan0sDOtIo2TYk3tZn5UDrkE0XgsTQCxWTTOcMPf9p6Rh2hXtl5TQ==", 318 | "cpu": [ 319 | "x64" 320 | ], 321 | "dev": true, 322 | "optional": true, 323 | "os": [ 324 | "netbsd" 325 | ], 326 | "engines": { 327 | "node": ">=12" 328 | } 329 | }, 330 | "node_modules/@esbuild/openbsd-x64": { 331 | "version": "0.18.17", 332 | "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.17.tgz", 333 | "integrity": "sha512-rSEeYaGgyGGf4qZM2NonMhMOP/5EHp4u9ehFiBrg7stH6BYEEjlkVREuDEcQ0LfIl53OXLxNbfuIj7mr5m29TA==", 334 | "cpu": [ 335 | "x64" 336 | ], 337 | "dev": true, 338 | "optional": true, 339 | "os": [ 340 | "openbsd" 341 | ], 342 | "engines": { 343 | "node": ">=12" 344 | } 345 | }, 346 | "node_modules/@esbuild/sunos-x64": { 347 | "version": "0.18.17", 348 | "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.18.17.tgz", 349 | "integrity": "sha512-Y7ZBbkLqlSgn4+zot4KUNYst0bFoO68tRgI6mY2FIM+b7ZbyNVtNbDP5y8qlu4/knZZ73fgJDlXID+ohY5zt5g==", 350 | "cpu": [ 351 | "x64" 352 | ], 353 | "dev": true, 354 | "optional": true, 355 | "os": [ 356 | "sunos" 357 | ], 358 | "engines": { 359 | "node": ">=12" 360 | } 361 | }, 362 | "node_modules/@esbuild/win32-arm64": { 363 | "version": "0.18.17", 364 | "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.18.17.tgz", 365 | "integrity": "sha512-bwPmTJsEQcbZk26oYpc4c/8PvTY3J5/QK8jM19DVlEsAB41M39aWovWoHtNm78sd6ip6prilxeHosPADXtEJFw==", 366 | "cpu": [ 367 | "arm64" 368 | ], 369 | "dev": true, 370 | "optional": true, 371 | "os": [ 372 | "win32" 373 | ], 374 | "engines": { 375 | "node": ">=12" 376 | } 377 | }, 378 | "node_modules/@esbuild/win32-ia32": { 379 | "version": "0.18.17", 380 | "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.18.17.tgz", 381 | "integrity": "sha512-H/XaPtPKli2MhW+3CQueo6Ni3Avggi6hP/YvgkEe1aSaxw+AeO8MFjq8DlgfTd9Iz4Yih3QCZI6YLMoyccnPRg==", 382 | "cpu": [ 383 | "ia32" 384 | ], 385 | "dev": true, 386 | "optional": true, 387 | "os": [ 388 | "win32" 389 | ], 390 | "engines": { 391 | "node": ">=12" 392 | } 393 | }, 394 | "node_modules/@esbuild/win32-x64": { 395 | "version": "0.18.17", 396 | "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.18.17.tgz", 397 | "integrity": "sha512-fGEb8f2BSA3CW7riJVurug65ACLuQAzKq0SSqkY2b2yHHH0MzDfbLyKIGzHwOI/gkHcxM/leuSW6D5w/LMNitA==", 398 | "cpu": [ 399 | "x64" 400 | ], 401 | "dev": true, 402 | "optional": true, 403 | "os": [ 404 | "win32" 405 | ], 406 | "engines": { 407 | "node": ">=12" 408 | } 409 | }, 410 | "node_modules/@floating-ui/core": { 411 | "version": "1.4.1", 412 | "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.4.1.tgz", 413 | "integrity": "sha512-jk3WqquEJRlcyu7997NtR5PibI+y5bi+LS3hPmguVClypenMsCY3CBa3LAQnozRCtCrYWSEtAdiskpamuJRFOQ==", 414 | "dependencies": { 415 | "@floating-ui/utils": "^0.1.1" 416 | } 417 | }, 418 | "node_modules/@floating-ui/dom": { 419 | "version": "1.5.1", 420 | "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.5.1.tgz", 421 | "integrity": "sha512-KwvVcPSXg6mQygvA1TjbN/gh///36kKtllIF8SUm0qpFj8+rvYrpvlYdL1JoA71SHpDqgSSdGOSoQ0Mp3uY5aw==", 422 | "dependencies": { 423 | "@floating-ui/core": "^1.4.1", 424 | "@floating-ui/utils": "^0.1.1" 425 | } 426 | }, 427 | "node_modules/@floating-ui/utils": { 428 | "version": "0.1.1", 429 | "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.1.1.tgz", 430 | "integrity": "sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==" 431 | }, 432 | "node_modules/@jridgewell/sourcemap-codec": { 433 | "version": "1.4.15", 434 | "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", 435 | "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" 436 | }, 437 | "node_modules/@nodelib/fs.scandir": { 438 | "version": "2.1.5", 439 | "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", 440 | "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", 441 | "dev": true, 442 | "dependencies": { 443 | "@nodelib/fs.stat": "2.0.5", 444 | "run-parallel": "^1.1.9" 445 | }, 446 | "engines": { 447 | "node": ">= 8" 448 | } 449 | }, 450 | "node_modules/@nodelib/fs.stat": { 451 | "version": "2.0.5", 452 | "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", 453 | "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", 454 | "dev": true, 455 | "engines": { 456 | "node": ">= 8" 457 | } 458 | }, 459 | "node_modules/@nodelib/fs.walk": { 460 | "version": "1.2.8", 461 | "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", 462 | "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", 463 | "dev": true, 464 | "dependencies": { 465 | "@nodelib/fs.scandir": "2.1.5", 466 | "fastq": "^1.6.0" 467 | }, 468 | "engines": { 469 | "node": ">= 8" 470 | } 471 | }, 472 | "node_modules/@popperjs/core": { 473 | "name": "@sxzz/popperjs-es", 474 | "version": "2.11.7", 475 | "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz", 476 | "integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==" 477 | }, 478 | "node_modules/@rollup/pluginutils": { 479 | "version": "5.0.2", 480 | "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", 481 | "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", 482 | "dev": true, 483 | "dependencies": { 484 | "@types/estree": "^1.0.0", 485 | "estree-walker": "^2.0.2", 486 | "picomatch": "^2.3.1" 487 | }, 488 | "engines": { 489 | "node": ">=14.0.0" 490 | }, 491 | "peerDependencies": { 492 | "rollup": "^1.20.0||^2.0.0||^3.0.0" 493 | }, 494 | "peerDependenciesMeta": { 495 | "rollup": { 496 | "optional": true 497 | } 498 | } 499 | }, 500 | "node_modules/@types/estree": { 501 | "version": "1.0.1", 502 | "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.1.tgz", 503 | "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", 504 | "dev": true 505 | }, 506 | "node_modules/@types/lodash": { 507 | "version": "4.14.196", 508 | "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.14.196.tgz", 509 | "integrity": "sha512-22y3o88f4a94mKljsZcanlNWPzO0uBsBdzLAngf2tp533LzZcQzb6+eZPJ+vCTt+bqF2XnvT9gejTLsAcJAJyQ==" 510 | }, 511 | "node_modules/@types/lodash-es": { 512 | "version": "4.17.8", 513 | "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.8.tgz", 514 | "integrity": "sha512-euY3XQcZmIzSy7YH5+Unb3b2X12Wtk54YWINBvvGQ5SmMvwb11JQskGsfkH/5HXK77Kr8GF0wkVDIxzAisWtog==", 515 | "dependencies": { 516 | "@types/lodash": "*" 517 | } 518 | }, 519 | "node_modules/@types/web-bluetooth": { 520 | "version": "0.0.17", 521 | "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.17.tgz", 522 | "integrity": "sha512-4p9vcSmxAayx72yn70joFoL44c9MO/0+iVEBIQXe3v2h2SiAsEIo/G5v6ObFWvNKRFjbrVadNf9LqEEZeQPzdA==" 523 | }, 524 | "node_modules/@vitejs/plugin-vue": { 525 | "version": "4.2.3", 526 | "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz", 527 | "integrity": "sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==", 528 | "dev": true, 529 | "engines": { 530 | "node": "^14.18.0 || >=16.0.0" 531 | }, 532 | "peerDependencies": { 533 | "vite": "^4.0.0", 534 | "vue": "^3.2.25" 535 | } 536 | }, 537 | "node_modules/@vue/compiler-core": { 538 | "version": "3.3.4", 539 | "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.3.4.tgz", 540 | "integrity": "sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==", 541 | "dependencies": { 542 | "@babel/parser": "^7.21.3", 543 | "@vue/shared": "3.3.4", 544 | "estree-walker": "^2.0.2", 545 | "source-map-js": "^1.0.2" 546 | } 547 | }, 548 | "node_modules/@vue/compiler-dom": { 549 | "version": "3.3.4", 550 | "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz", 551 | "integrity": "sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==", 552 | "dependencies": { 553 | "@vue/compiler-core": "3.3.4", 554 | "@vue/shared": "3.3.4" 555 | } 556 | }, 557 | "node_modules/@vue/compiler-sfc": { 558 | "version": "3.3.4", 559 | "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz", 560 | "integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==", 561 | "dependencies": { 562 | "@babel/parser": "^7.20.15", 563 | "@vue/compiler-core": "3.3.4", 564 | "@vue/compiler-dom": "3.3.4", 565 | "@vue/compiler-ssr": "3.3.4", 566 | "@vue/reactivity-transform": "3.3.4", 567 | "@vue/shared": "3.3.4", 568 | "estree-walker": "^2.0.2", 569 | "magic-string": "^0.30.0", 570 | "postcss": "^8.1.10", 571 | "source-map-js": "^1.0.2" 572 | } 573 | }, 574 | "node_modules/@vue/compiler-ssr": { 575 | "version": "3.3.4", 576 | "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz", 577 | "integrity": "sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==", 578 | "dependencies": { 579 | "@vue/compiler-dom": "3.3.4", 580 | "@vue/shared": "3.3.4" 581 | } 582 | }, 583 | "node_modules/@vue/devtools-api": { 584 | "version": "6.5.0", 585 | "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.5.0.tgz", 586 | "integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==" 587 | }, 588 | "node_modules/@vue/reactivity": { 589 | "version": "3.3.4", 590 | "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.3.4.tgz", 591 | "integrity": "sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==", 592 | "dependencies": { 593 | "@vue/shared": "3.3.4" 594 | } 595 | }, 596 | "node_modules/@vue/reactivity-transform": { 597 | "version": "3.3.4", 598 | "resolved": "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz", 599 | "integrity": "sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==", 600 | "dependencies": { 601 | "@babel/parser": "^7.20.15", 602 | "@vue/compiler-core": "3.3.4", 603 | "@vue/shared": "3.3.4", 604 | "estree-walker": "^2.0.2", 605 | "magic-string": "^0.30.0" 606 | } 607 | }, 608 | "node_modules/@vue/runtime-core": { 609 | "version": "3.3.4", 610 | "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.3.4.tgz", 611 | "integrity": "sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==", 612 | "dependencies": { 613 | "@vue/reactivity": "3.3.4", 614 | "@vue/shared": "3.3.4" 615 | } 616 | }, 617 | "node_modules/@vue/runtime-dom": { 618 | "version": "3.3.4", 619 | "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz", 620 | "integrity": "sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==", 621 | "dependencies": { 622 | "@vue/runtime-core": "3.3.4", 623 | "@vue/shared": "3.3.4", 624 | "csstype": "^3.1.1" 625 | } 626 | }, 627 | "node_modules/@vue/server-renderer": { 628 | "version": "3.3.4", 629 | "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.3.4.tgz", 630 | "integrity": "sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==", 631 | "dependencies": { 632 | "@vue/compiler-ssr": "3.3.4", 633 | "@vue/shared": "3.3.4" 634 | }, 635 | "peerDependencies": { 636 | "vue": "3.3.4" 637 | } 638 | }, 639 | "node_modules/@vue/shared": { 640 | "version": "3.3.4", 641 | "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.3.4.tgz", 642 | "integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==" 643 | }, 644 | "node_modules/@vueuse/core": { 645 | "version": "10.3.0", 646 | "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-10.3.0.tgz", 647 | "integrity": "sha512-BEM5yxcFKb5btFjTSAFjTu5jmwoW66fyV9uJIP4wUXXU8aR5Hl44gndaaXp7dC5HSObmgbnR2RN+Un1p68Mf5Q==", 648 | "dependencies": { 649 | "@types/web-bluetooth": "^0.0.17", 650 | "@vueuse/metadata": "10.3.0", 651 | "@vueuse/shared": "10.3.0", 652 | "vue-demi": ">=0.14.5" 653 | } 654 | }, 655 | "node_modules/@vueuse/core/node_modules/vue-demi": { 656 | "version": "0.14.5", 657 | "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.5.tgz", 658 | "integrity": "sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==", 659 | "hasInstallScript": true, 660 | "bin": { 661 | "vue-demi-fix": "bin/vue-demi-fix.js", 662 | "vue-demi-switch": "bin/vue-demi-switch.js" 663 | }, 664 | "engines": { 665 | "node": ">=12" 666 | }, 667 | "peerDependencies": { 668 | "@vue/composition-api": "^1.0.0-rc.1", 669 | "vue": "^3.0.0-0 || ^2.6.0" 670 | }, 671 | "peerDependenciesMeta": { 672 | "@vue/composition-api": { 673 | "optional": true 674 | } 675 | } 676 | }, 677 | "node_modules/@vueuse/metadata": { 678 | "version": "10.3.0", 679 | "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-10.3.0.tgz", 680 | "integrity": "sha512-Ema3YhNOa4swDsV0V7CEY5JXvK19JI/o1szFO1iWxdFg3vhdFtCtSTP26PCvbUpnUtNHBY2wx5y3WDXND5Pvnw==" 681 | }, 682 | "node_modules/@vueuse/shared": { 683 | "version": "10.3.0", 684 | "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-10.3.0.tgz", 685 | "integrity": "sha512-kGqCTEuFPMK4+fNWy6dUOiYmxGcUbtznMwBZLC1PubidF4VZY05B+Oht7Jh7/6x4VOWGpvu3R37WHi81cKpiqg==", 686 | "dependencies": { 687 | "vue-demi": ">=0.14.5" 688 | } 689 | }, 690 | "node_modules/@vueuse/shared/node_modules/vue-demi": { 691 | "version": "0.14.5", 692 | "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.5.tgz", 693 | "integrity": "sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==", 694 | "hasInstallScript": true, 695 | "bin": { 696 | "vue-demi-fix": "bin/vue-demi-fix.js", 697 | "vue-demi-switch": "bin/vue-demi-switch.js" 698 | }, 699 | "engines": { 700 | "node": ">=12" 701 | }, 702 | "peerDependencies": { 703 | "@vue/composition-api": "^1.0.0-rc.1", 704 | "vue": "^3.0.0-0 || ^2.6.0" 705 | }, 706 | "peerDependenciesMeta": { 707 | "@vue/composition-api": { 708 | "optional": true 709 | } 710 | } 711 | }, 712 | "node_modules/acorn": { 713 | "version": "8.10.0", 714 | "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.10.0.tgz", 715 | "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", 716 | "dev": true, 717 | "bin": { 718 | "acorn": "bin/acorn" 719 | }, 720 | "engines": { 721 | "node": ">=0.4.0" 722 | } 723 | }, 724 | "node_modules/anymatch": { 725 | "version": "3.1.3", 726 | "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", 727 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", 728 | "dev": true, 729 | "dependencies": { 730 | "normalize-path": "^3.0.0", 731 | "picomatch": "^2.0.4" 732 | }, 733 | "engines": { 734 | "node": ">= 8" 735 | } 736 | }, 737 | "node_modules/async-validator": { 738 | "version": "4.2.5", 739 | "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", 740 | "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==" 741 | }, 742 | "node_modules/asynckit": { 743 | "version": "0.4.0", 744 | "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", 745 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 746 | }, 747 | "node_modules/axios": { 748 | "version": "1.4.0", 749 | "resolved": "https://registry.npmmirror.com/axios/-/axios-1.4.0.tgz", 750 | "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", 751 | "dependencies": { 752 | "follow-redirects": "^1.15.0", 753 | "form-data": "^4.0.0", 754 | "proxy-from-env": "^1.1.0" 755 | } 756 | }, 757 | "node_modules/balanced-match": { 758 | "version": "1.0.2", 759 | "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", 760 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 761 | "dev": true 762 | }, 763 | "node_modules/binary-extensions": { 764 | "version": "2.2.0", 765 | "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.2.0.tgz", 766 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 767 | "dev": true, 768 | "engines": { 769 | "node": ">=8" 770 | } 771 | }, 772 | "node_modules/brace-expansion": { 773 | "version": "2.0.1", 774 | "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz", 775 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 776 | "dev": true, 777 | "dependencies": { 778 | "balanced-match": "^1.0.0" 779 | } 780 | }, 781 | "node_modules/braces": { 782 | "version": "3.0.2", 783 | "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.2.tgz", 784 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 785 | "dev": true, 786 | "dependencies": { 787 | "fill-range": "^7.0.1" 788 | }, 789 | "engines": { 790 | "node": ">=8" 791 | } 792 | }, 793 | "node_modules/chokidar": { 794 | "version": "3.5.3", 795 | "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.5.3.tgz", 796 | "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", 797 | "dev": true, 798 | "dependencies": { 799 | "anymatch": "~3.1.2", 800 | "braces": "~3.0.2", 801 | "glob-parent": "~5.1.2", 802 | "is-binary-path": "~2.1.0", 803 | "is-glob": "~4.0.1", 804 | "normalize-path": "~3.0.0", 805 | "readdirp": "~3.6.0" 806 | }, 807 | "engines": { 808 | "node": ">= 8.10.0" 809 | }, 810 | "optionalDependencies": { 811 | "fsevents": "~2.3.2" 812 | } 813 | }, 814 | "node_modules/combined-stream": { 815 | "version": "1.0.8", 816 | "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", 817 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 818 | "dependencies": { 819 | "delayed-stream": "~1.0.0" 820 | }, 821 | "engines": { 822 | "node": ">= 0.8" 823 | } 824 | }, 825 | "node_modules/csstype": { 826 | "version": "3.1.2", 827 | "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.2.tgz", 828 | "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" 829 | }, 830 | "node_modules/dayjs": { 831 | "version": "1.11.9", 832 | "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.9.tgz", 833 | "integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==" 834 | }, 835 | "node_modules/debug": { 836 | "version": "4.3.4", 837 | "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz", 838 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 839 | "dev": true, 840 | "dependencies": { 841 | "ms": "2.1.2" 842 | }, 843 | "engines": { 844 | "node": ">=6.0" 845 | }, 846 | "peerDependenciesMeta": { 847 | "supports-color": { 848 | "optional": true 849 | } 850 | } 851 | }, 852 | "node_modules/delayed-stream": { 853 | "version": "1.0.0", 854 | "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", 855 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 856 | "engines": { 857 | "node": ">=0.4.0" 858 | } 859 | }, 860 | "node_modules/element-plus": { 861 | "version": "2.3.9", 862 | "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.3.9.tgz", 863 | "integrity": "sha512-TIOLnPl4cnoCPXqK3QYh+jpkthUBQnAM21O7o3Lhbse8v9pfrRXRTaBJtoEKnYNa8GZ4lZptUfH0PeZgDCNLUg==", 864 | "dependencies": { 865 | "@ctrl/tinycolor": "^3.4.1", 866 | "@element-plus/icons-vue": "^2.0.6", 867 | "@floating-ui/dom": "^1.0.1", 868 | "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", 869 | "@types/lodash": "^4.14.182", 870 | "@types/lodash-es": "^4.17.6", 871 | "@vueuse/core": "^9.1.0", 872 | "async-validator": "^4.2.5", 873 | "dayjs": "^1.11.3", 874 | "escape-html": "^1.0.3", 875 | "lodash": "^4.17.21", 876 | "lodash-es": "^4.17.21", 877 | "lodash-unified": "^1.0.2", 878 | "memoize-one": "^6.0.0", 879 | "normalize-wheel-es": "^1.2.0" 880 | }, 881 | "peerDependencies": { 882 | "vue": "^3.2.0" 883 | } 884 | }, 885 | "node_modules/element-plus/node_modules/@types/web-bluetooth": { 886 | "version": "0.0.16", 887 | "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz", 888 | "integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==" 889 | }, 890 | "node_modules/element-plus/node_modules/@vueuse/core": { 891 | "version": "9.13.0", 892 | "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz", 893 | "integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==", 894 | "dependencies": { 895 | "@types/web-bluetooth": "^0.0.16", 896 | "@vueuse/metadata": "9.13.0", 897 | "@vueuse/shared": "9.13.0", 898 | "vue-demi": "*" 899 | } 900 | }, 901 | "node_modules/element-plus/node_modules/@vueuse/core/node_modules/vue-demi": { 902 | "version": "0.14.5", 903 | "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.5.tgz", 904 | "integrity": "sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==", 905 | "hasInstallScript": true, 906 | "bin": { 907 | "vue-demi-fix": "bin/vue-demi-fix.js", 908 | "vue-demi-switch": "bin/vue-demi-switch.js" 909 | }, 910 | "engines": { 911 | "node": ">=12" 912 | }, 913 | "peerDependencies": { 914 | "@vue/composition-api": "^1.0.0-rc.1", 915 | "vue": "^3.0.0-0 || ^2.6.0" 916 | }, 917 | "peerDependenciesMeta": { 918 | "@vue/composition-api": { 919 | "optional": true 920 | } 921 | } 922 | }, 923 | "node_modules/element-plus/node_modules/@vueuse/metadata": { 924 | "version": "9.13.0", 925 | "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz", 926 | "integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==" 927 | }, 928 | "node_modules/element-plus/node_modules/@vueuse/shared": { 929 | "version": "9.13.0", 930 | "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz", 931 | "integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==", 932 | "dependencies": { 933 | "vue-demi": "*" 934 | } 935 | }, 936 | "node_modules/element-plus/node_modules/@vueuse/shared/node_modules/vue-demi": { 937 | "version": "0.14.5", 938 | "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.5.tgz", 939 | "integrity": "sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==", 940 | "hasInstallScript": true, 941 | "bin": { 942 | "vue-demi-fix": "bin/vue-demi-fix.js", 943 | "vue-demi-switch": "bin/vue-demi-switch.js" 944 | }, 945 | "engines": { 946 | "node": ">=12" 947 | }, 948 | "peerDependencies": { 949 | "@vue/composition-api": "^1.0.0-rc.1", 950 | "vue": "^3.0.0-0 || ^2.6.0" 951 | }, 952 | "peerDependenciesMeta": { 953 | "@vue/composition-api": { 954 | "optional": true 955 | } 956 | } 957 | }, 958 | "node_modules/esbuild": { 959 | "version": "0.18.17", 960 | "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.18.17.tgz", 961 | "integrity": "sha512-1GJtYnUxsJreHYA0Y+iQz2UEykonY66HNWOb0yXYZi9/kNrORUEHVg87eQsCtqh59PEJ5YVZJO98JHznMJSWjg==", 962 | "dev": true, 963 | "hasInstallScript": true, 964 | "bin": { 965 | "esbuild": "bin/esbuild" 966 | }, 967 | "engines": { 968 | "node": ">=12" 969 | }, 970 | "optionalDependencies": { 971 | "@esbuild/android-arm": "0.18.17", 972 | "@esbuild/android-arm64": "0.18.17", 973 | "@esbuild/android-x64": "0.18.17", 974 | "@esbuild/darwin-arm64": "0.18.17", 975 | "@esbuild/darwin-x64": "0.18.17", 976 | "@esbuild/freebsd-arm64": "0.18.17", 977 | "@esbuild/freebsd-x64": "0.18.17", 978 | "@esbuild/linux-arm": "0.18.17", 979 | "@esbuild/linux-arm64": "0.18.17", 980 | "@esbuild/linux-ia32": "0.18.17", 981 | "@esbuild/linux-loong64": "0.18.17", 982 | "@esbuild/linux-mips64el": "0.18.17", 983 | "@esbuild/linux-ppc64": "0.18.17", 984 | "@esbuild/linux-riscv64": "0.18.17", 985 | "@esbuild/linux-s390x": "0.18.17", 986 | "@esbuild/linux-x64": "0.18.17", 987 | "@esbuild/netbsd-x64": "0.18.17", 988 | "@esbuild/openbsd-x64": "0.18.17", 989 | "@esbuild/sunos-x64": "0.18.17", 990 | "@esbuild/win32-arm64": "0.18.17", 991 | "@esbuild/win32-ia32": "0.18.17", 992 | "@esbuild/win32-x64": "0.18.17" 993 | } 994 | }, 995 | "node_modules/escape-html": { 996 | "version": "1.0.3", 997 | "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", 998 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 999 | }, 1000 | "node_modules/escape-string-regexp": { 1001 | "version": "5.0.0", 1002 | "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", 1003 | "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", 1004 | "dev": true, 1005 | "engines": { 1006 | "node": ">=12" 1007 | } 1008 | }, 1009 | "node_modules/estree-walker": { 1010 | "version": "2.0.2", 1011 | "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", 1012 | "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" 1013 | }, 1014 | "node_modules/fast-glob": { 1015 | "version": "3.3.1", 1016 | "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.1.tgz", 1017 | "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", 1018 | "dev": true, 1019 | "dependencies": { 1020 | "@nodelib/fs.stat": "^2.0.2", 1021 | "@nodelib/fs.walk": "^1.2.3", 1022 | "glob-parent": "^5.1.2", 1023 | "merge2": "^1.3.0", 1024 | "micromatch": "^4.0.4" 1025 | }, 1026 | "engines": { 1027 | "node": ">=8.6.0" 1028 | } 1029 | }, 1030 | "node_modules/fastq": { 1031 | "version": "1.15.0", 1032 | "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.15.0.tgz", 1033 | "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", 1034 | "dev": true, 1035 | "dependencies": { 1036 | "reusify": "^1.0.4" 1037 | } 1038 | }, 1039 | "node_modules/fill-range": { 1040 | "version": "7.0.1", 1041 | "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.0.1.tgz", 1042 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 1043 | "dev": true, 1044 | "dependencies": { 1045 | "to-regex-range": "^5.0.1" 1046 | }, 1047 | "engines": { 1048 | "node": ">=8" 1049 | } 1050 | }, 1051 | "node_modules/follow-redirects": { 1052 | "version": "1.15.2", 1053 | "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.2.tgz", 1054 | "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", 1055 | "engines": { 1056 | "node": ">=4.0" 1057 | }, 1058 | "peerDependenciesMeta": { 1059 | "debug": { 1060 | "optional": true 1061 | } 1062 | } 1063 | }, 1064 | "node_modules/form-data": { 1065 | "version": "4.0.0", 1066 | "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.0.tgz", 1067 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 1068 | "dependencies": { 1069 | "asynckit": "^0.4.0", 1070 | "combined-stream": "^1.0.8", 1071 | "mime-types": "^2.1.12" 1072 | }, 1073 | "engines": { 1074 | "node": ">= 6" 1075 | } 1076 | }, 1077 | "node_modules/fsevents": { 1078 | "version": "2.3.2", 1079 | "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz", 1080 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 1081 | "dev": true, 1082 | "hasInstallScript": true, 1083 | "optional": true, 1084 | "os": [ 1085 | "darwin" 1086 | ], 1087 | "engines": { 1088 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 1089 | } 1090 | }, 1091 | "node_modules/function-bind": { 1092 | "version": "1.1.1", 1093 | "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.1.tgz", 1094 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", 1095 | "dev": true 1096 | }, 1097 | "node_modules/glob-parent": { 1098 | "version": "5.1.2", 1099 | "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", 1100 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 1101 | "dev": true, 1102 | "dependencies": { 1103 | "is-glob": "^4.0.1" 1104 | }, 1105 | "engines": { 1106 | "node": ">= 6" 1107 | } 1108 | }, 1109 | "node_modules/has": { 1110 | "version": "1.0.3", 1111 | "resolved": "https://registry.npmmirror.com/has/-/has-1.0.3.tgz", 1112 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 1113 | "dev": true, 1114 | "dependencies": { 1115 | "function-bind": "^1.1.1" 1116 | }, 1117 | "engines": { 1118 | "node": ">= 0.4.0" 1119 | } 1120 | }, 1121 | "node_modules/is-binary-path": { 1122 | "version": "2.1.0", 1123 | "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", 1124 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 1125 | "dev": true, 1126 | "dependencies": { 1127 | "binary-extensions": "^2.0.0" 1128 | }, 1129 | "engines": { 1130 | "node": ">=8" 1131 | } 1132 | }, 1133 | "node_modules/is-core-module": { 1134 | "version": "2.13.0", 1135 | "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.13.0.tgz", 1136 | "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", 1137 | "dev": true, 1138 | "dependencies": { 1139 | "has": "^1.0.3" 1140 | } 1141 | }, 1142 | "node_modules/is-extglob": { 1143 | "version": "2.1.1", 1144 | "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", 1145 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 1146 | "dev": true, 1147 | "engines": { 1148 | "node": ">=0.10.0" 1149 | } 1150 | }, 1151 | "node_modules/is-glob": { 1152 | "version": "4.0.3", 1153 | "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", 1154 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 1155 | "dev": true, 1156 | "dependencies": { 1157 | "is-extglob": "^2.1.1" 1158 | }, 1159 | "engines": { 1160 | "node": ">=0.10.0" 1161 | } 1162 | }, 1163 | "node_modules/is-number": { 1164 | "version": "7.0.0", 1165 | "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", 1166 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 1167 | "dev": true, 1168 | "engines": { 1169 | "node": ">=0.12.0" 1170 | } 1171 | }, 1172 | "node_modules/jsonc-parser": { 1173 | "version": "3.2.0", 1174 | "resolved": "https://registry.npmmirror.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz", 1175 | "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", 1176 | "dev": true 1177 | }, 1178 | "node_modules/local-pkg": { 1179 | "version": "0.4.3", 1180 | "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-0.4.3.tgz", 1181 | "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", 1182 | "dev": true, 1183 | "engines": { 1184 | "node": ">=14" 1185 | } 1186 | }, 1187 | "node_modules/lodash": { 1188 | "version": "4.17.21", 1189 | "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", 1190 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 1191 | }, 1192 | "node_modules/lodash-es": { 1193 | "version": "4.17.21", 1194 | "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz", 1195 | "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" 1196 | }, 1197 | "node_modules/lodash-unified": { 1198 | "version": "1.0.3", 1199 | "resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz", 1200 | "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", 1201 | "peerDependencies": { 1202 | "@types/lodash-es": "*", 1203 | "lodash": "*", 1204 | "lodash-es": "*" 1205 | } 1206 | }, 1207 | "node_modules/magic-string": { 1208 | "version": "0.30.2", 1209 | "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.2.tgz", 1210 | "integrity": "sha512-lNZdu7pewtq/ZvWUp9Wpf/x7WzMTsR26TWV03BRZrXFsv+BI6dy8RAiKgm1uM/kyR0rCfUcqvOlXKG66KhIGug==", 1211 | "dependencies": { 1212 | "@jridgewell/sourcemap-codec": "^1.4.15" 1213 | }, 1214 | "engines": { 1215 | "node": ">=12" 1216 | } 1217 | }, 1218 | "node_modules/memoize-one": { 1219 | "version": "6.0.0", 1220 | "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz", 1221 | "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" 1222 | }, 1223 | "node_modules/merge2": { 1224 | "version": "1.4.1", 1225 | "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", 1226 | "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", 1227 | "dev": true, 1228 | "engines": { 1229 | "node": ">= 8" 1230 | } 1231 | }, 1232 | "node_modules/micromatch": { 1233 | "version": "4.0.5", 1234 | "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.5.tgz", 1235 | "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", 1236 | "dev": true, 1237 | "dependencies": { 1238 | "braces": "^3.0.2", 1239 | "picomatch": "^2.3.1" 1240 | }, 1241 | "engines": { 1242 | "node": ">=8.6" 1243 | } 1244 | }, 1245 | "node_modules/mime-db": { 1246 | "version": "1.52.0", 1247 | "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", 1248 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 1249 | "engines": { 1250 | "node": ">= 0.6" 1251 | } 1252 | }, 1253 | "node_modules/mime-types": { 1254 | "version": "2.1.35", 1255 | "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", 1256 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 1257 | "dependencies": { 1258 | "mime-db": "1.52.0" 1259 | }, 1260 | "engines": { 1261 | "node": ">= 0.6" 1262 | } 1263 | }, 1264 | "node_modules/minimatch": { 1265 | "version": "9.0.3", 1266 | "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.3.tgz", 1267 | "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", 1268 | "dev": true, 1269 | "dependencies": { 1270 | "brace-expansion": "^2.0.1" 1271 | }, 1272 | "engines": { 1273 | "node": ">=16 || 14 >=14.17" 1274 | } 1275 | }, 1276 | "node_modules/mlly": { 1277 | "version": "1.4.0", 1278 | "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.4.0.tgz", 1279 | "integrity": "sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg==", 1280 | "dev": true, 1281 | "dependencies": { 1282 | "acorn": "^8.9.0", 1283 | "pathe": "^1.1.1", 1284 | "pkg-types": "^1.0.3", 1285 | "ufo": "^1.1.2" 1286 | } 1287 | }, 1288 | "node_modules/ms": { 1289 | "version": "2.1.2", 1290 | "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz", 1291 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 1292 | "dev": true 1293 | }, 1294 | "node_modules/nanoid": { 1295 | "version": "3.3.6", 1296 | "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.6.tgz", 1297 | "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", 1298 | "bin": { 1299 | "nanoid": "bin/nanoid.cjs" 1300 | }, 1301 | "engines": { 1302 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 1303 | } 1304 | }, 1305 | "node_modules/normalize-path": { 1306 | "version": "3.0.0", 1307 | "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", 1308 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 1309 | "dev": true, 1310 | "engines": { 1311 | "node": ">=0.10.0" 1312 | } 1313 | }, 1314 | "node_modules/normalize-wheel-es": { 1315 | "version": "1.2.0", 1316 | "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", 1317 | "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==" 1318 | }, 1319 | "node_modules/path-parse": { 1320 | "version": "1.0.7", 1321 | "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", 1322 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", 1323 | "dev": true 1324 | }, 1325 | "node_modules/pathe": { 1326 | "version": "1.1.1", 1327 | "resolved": "https://registry.npmmirror.com/pathe/-/pathe-1.1.1.tgz", 1328 | "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", 1329 | "dev": true 1330 | }, 1331 | "node_modules/picocolors": { 1332 | "version": "1.0.0", 1333 | "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz", 1334 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" 1335 | }, 1336 | "node_modules/picomatch": { 1337 | "version": "2.3.1", 1338 | "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", 1339 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 1340 | "dev": true, 1341 | "engines": { 1342 | "node": ">=8.6" 1343 | } 1344 | }, 1345 | "node_modules/pkg-types": { 1346 | "version": "1.0.3", 1347 | "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.0.3.tgz", 1348 | "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", 1349 | "dev": true, 1350 | "dependencies": { 1351 | "jsonc-parser": "^3.2.0", 1352 | "mlly": "^1.2.0", 1353 | "pathe": "^1.1.0" 1354 | } 1355 | }, 1356 | "node_modules/postcss": { 1357 | "version": "8.4.27", 1358 | "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.27.tgz", 1359 | "integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", 1360 | "dependencies": { 1361 | "nanoid": "^3.3.6", 1362 | "picocolors": "^1.0.0", 1363 | "source-map-js": "^1.0.2" 1364 | }, 1365 | "engines": { 1366 | "node": "^10 || ^12 || >=14" 1367 | } 1368 | }, 1369 | "node_modules/proxy-from-env": { 1370 | "version": "1.1.0", 1371 | "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 1372 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" 1373 | }, 1374 | "node_modules/queue-microtask": { 1375 | "version": "1.2.3", 1376 | "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", 1377 | "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", 1378 | "dev": true 1379 | }, 1380 | "node_modules/readdirp": { 1381 | "version": "3.6.0", 1382 | "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", 1383 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 1384 | "dev": true, 1385 | "dependencies": { 1386 | "picomatch": "^2.2.1" 1387 | }, 1388 | "engines": { 1389 | "node": ">=8.10.0" 1390 | } 1391 | }, 1392 | "node_modules/resolve": { 1393 | "version": "1.22.4", 1394 | "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.4.tgz", 1395 | "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", 1396 | "dev": true, 1397 | "dependencies": { 1398 | "is-core-module": "^2.13.0", 1399 | "path-parse": "^1.0.7", 1400 | "supports-preserve-symlinks-flag": "^1.0.0" 1401 | }, 1402 | "bin": { 1403 | "resolve": "bin/resolve" 1404 | } 1405 | }, 1406 | "node_modules/reusify": { 1407 | "version": "1.0.4", 1408 | "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.0.4.tgz", 1409 | "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", 1410 | "dev": true, 1411 | "engines": { 1412 | "iojs": ">=1.0.0", 1413 | "node": ">=0.10.0" 1414 | } 1415 | }, 1416 | "node_modules/rollup": { 1417 | "version": "3.27.0", 1418 | "resolved": "https://registry.npmmirror.com/rollup/-/rollup-3.27.0.tgz", 1419 | "integrity": "sha512-aOltLCrYZ0FhJDm7fCqwTjIUEVjWjcydKBV/Zeid6Mn8BWgDCUBBWT5beM5ieForYNo/1ZHuGJdka26kvQ3Gzg==", 1420 | "dev": true, 1421 | "bin": { 1422 | "rollup": "dist/bin/rollup" 1423 | }, 1424 | "engines": { 1425 | "node": ">=14.18.0", 1426 | "npm": ">=8.0.0" 1427 | }, 1428 | "optionalDependencies": { 1429 | "fsevents": "~2.3.2" 1430 | } 1431 | }, 1432 | "node_modules/run-parallel": { 1433 | "version": "1.2.0", 1434 | "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", 1435 | "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", 1436 | "dev": true, 1437 | "dependencies": { 1438 | "queue-microtask": "^1.2.2" 1439 | } 1440 | }, 1441 | "node_modules/scule": { 1442 | "version": "1.0.0", 1443 | "resolved": "https://registry.npmmirror.com/scule/-/scule-1.0.0.tgz", 1444 | "integrity": "sha512-4AsO/FrViE/iDNEPaAQlb77tf0csuq27EsVpy6ett584EcRTp6pTDLoGWVxCD77y5iU5FauOvhsI4o1APwPoSQ==", 1445 | "dev": true 1446 | }, 1447 | "node_modules/source-map-js": { 1448 | "version": "1.0.2", 1449 | "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.0.2.tgz", 1450 | "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", 1451 | "engines": { 1452 | "node": ">=0.10.0" 1453 | } 1454 | }, 1455 | "node_modules/strip-literal": { 1456 | "version": "1.3.0", 1457 | "resolved": "https://registry.npmmirror.com/strip-literal/-/strip-literal-1.3.0.tgz", 1458 | "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", 1459 | "dev": true, 1460 | "dependencies": { 1461 | "acorn": "^8.10.0" 1462 | } 1463 | }, 1464 | "node_modules/supports-preserve-symlinks-flag": { 1465 | "version": "1.0.0", 1466 | "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", 1467 | "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", 1468 | "dev": true, 1469 | "engines": { 1470 | "node": ">= 0.4" 1471 | } 1472 | }, 1473 | "node_modules/to-regex-range": { 1474 | "version": "5.0.1", 1475 | "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", 1476 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1477 | "dev": true, 1478 | "dependencies": { 1479 | "is-number": "^7.0.0" 1480 | }, 1481 | "engines": { 1482 | "node": ">=8.0" 1483 | } 1484 | }, 1485 | "node_modules/ufo": { 1486 | "version": "1.2.0", 1487 | "resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.2.0.tgz", 1488 | "integrity": "sha512-RsPyTbqORDNDxqAdQPQBpgqhWle1VcTSou/FraClYlHf6TZnQcGslpLcAphNR+sQW4q5lLWLbOsRlh9j24baQg==", 1489 | "dev": true 1490 | }, 1491 | "node_modules/unimport": { 1492 | "version": "3.1.3", 1493 | "resolved": "https://registry.npmmirror.com/unimport/-/unimport-3.1.3.tgz", 1494 | "integrity": "sha512-up4TE2yA+nMyyErGTjbYGVw95MriGa2hVRXQ3/JRp7984cwwqULcnBjHaovVpsO8tZc2j0fvgGu9yiBKOyxvYw==", 1495 | "dev": true, 1496 | "dependencies": { 1497 | "@rollup/pluginutils": "^5.0.2", 1498 | "escape-string-regexp": "^5.0.0", 1499 | "fast-glob": "^3.3.1", 1500 | "local-pkg": "^0.4.3", 1501 | "magic-string": "^0.30.2", 1502 | "mlly": "^1.4.0", 1503 | "pathe": "^1.1.1", 1504 | "pkg-types": "^1.0.3", 1505 | "scule": "^1.0.0", 1506 | "strip-literal": "^1.3.0", 1507 | "unplugin": "^1.4.0" 1508 | } 1509 | }, 1510 | "node_modules/unplugin": { 1511 | "version": "1.4.0", 1512 | "resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-1.4.0.tgz", 1513 | "integrity": "sha512-5x4eIEL6WgbzqGtF9UV8VEC/ehKptPXDS6L2b0mv4FRMkJxRtjaJfOWDd6a8+kYbqsjklix7yWP0N3SUepjXcg==", 1514 | "dev": true, 1515 | "dependencies": { 1516 | "acorn": "^8.9.0", 1517 | "chokidar": "^3.5.3", 1518 | "webpack-sources": "^3.2.3", 1519 | "webpack-virtual-modules": "^0.5.0" 1520 | } 1521 | }, 1522 | "node_modules/unplugin-auto-import": { 1523 | "version": "0.15.3", 1524 | "resolved": "https://registry.npmmirror.com/unplugin-auto-import/-/unplugin-auto-import-0.15.3.tgz", 1525 | "integrity": "sha512-RLT8SqbPn4bT7yBshZId0uPSofKWnwr66RyDaxWaFb/+f7OTDOWAsVNz+hOQLBWSjvbekr2xZY9ccS8TDHJbCQ==", 1526 | "dev": true, 1527 | "dependencies": { 1528 | "@antfu/utils": "^0.7.2", 1529 | "@rollup/pluginutils": "^5.0.2", 1530 | "local-pkg": "^0.4.3", 1531 | "magic-string": "^0.30.0", 1532 | "minimatch": "^9.0.0", 1533 | "unimport": "^3.0.6", 1534 | "unplugin": "^1.3.1" 1535 | }, 1536 | "engines": { 1537 | "node": ">=14" 1538 | }, 1539 | "peerDependencies": { 1540 | "@nuxt/kit": "^3.2.2", 1541 | "@vueuse/core": "*" 1542 | }, 1543 | "peerDependenciesMeta": { 1544 | "@nuxt/kit": { 1545 | "optional": true 1546 | }, 1547 | "@vueuse/core": { 1548 | "optional": true 1549 | } 1550 | } 1551 | }, 1552 | "node_modules/unplugin-vue-components": { 1553 | "version": "0.24.1", 1554 | "resolved": "https://registry.npmmirror.com/unplugin-vue-components/-/unplugin-vue-components-0.24.1.tgz", 1555 | "integrity": "sha512-T3A8HkZoIE1Cja95xNqolwza0yD5IVlgZZ1PVAGvVCx8xthmjsv38xWRCtHtwl+rvZyL9uif42SRkDGw9aCfMA==", 1556 | "dev": true, 1557 | "dependencies": { 1558 | "@antfu/utils": "^0.7.2", 1559 | "@rollup/pluginutils": "^5.0.2", 1560 | "chokidar": "^3.5.3", 1561 | "debug": "^4.3.4", 1562 | "fast-glob": "^3.2.12", 1563 | "local-pkg": "^0.4.3", 1564 | "magic-string": "^0.30.0", 1565 | "minimatch": "^7.4.2", 1566 | "resolve": "^1.22.1", 1567 | "unplugin": "^1.1.0" 1568 | }, 1569 | "engines": { 1570 | "node": ">=14" 1571 | }, 1572 | "peerDependencies": { 1573 | "@babel/parser": "^7.15.8", 1574 | "@nuxt/kit": "^3.2.2", 1575 | "vue": "2 || 3" 1576 | }, 1577 | "peerDependenciesMeta": { 1578 | "@babel/parser": { 1579 | "optional": true 1580 | }, 1581 | "@nuxt/kit": { 1582 | "optional": true 1583 | } 1584 | } 1585 | }, 1586 | "node_modules/unplugin-vue-components/node_modules/minimatch": { 1587 | "version": "7.4.6", 1588 | "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-7.4.6.tgz", 1589 | "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", 1590 | "dev": true, 1591 | "dependencies": { 1592 | "brace-expansion": "^2.0.1" 1593 | }, 1594 | "engines": { 1595 | "node": ">=10" 1596 | } 1597 | }, 1598 | "node_modules/vite": { 1599 | "version": "4.4.8", 1600 | "resolved": "https://registry.npmmirror.com/vite/-/vite-4.4.8.tgz", 1601 | "integrity": "sha512-LONawOUUjxQridNWGQlNizfKH89qPigK36XhMI7COMGztz8KNY0JHim7/xDd71CZwGT4HtSRgI7Hy+RlhG0Gvg==", 1602 | "dev": true, 1603 | "dependencies": { 1604 | "esbuild": "^0.18.10", 1605 | "postcss": "^8.4.26", 1606 | "rollup": "^3.25.2" 1607 | }, 1608 | "bin": { 1609 | "vite": "bin/vite.js" 1610 | }, 1611 | "engines": { 1612 | "node": "^14.18.0 || >=16.0.0" 1613 | }, 1614 | "optionalDependencies": { 1615 | "fsevents": "~2.3.2" 1616 | }, 1617 | "peerDependencies": { 1618 | "@types/node": ">= 14", 1619 | "less": "*", 1620 | "lightningcss": "^1.21.0", 1621 | "sass": "*", 1622 | "stylus": "*", 1623 | "sugarss": "*", 1624 | "terser": "^5.4.0" 1625 | }, 1626 | "peerDependenciesMeta": { 1627 | "@types/node": { 1628 | "optional": true 1629 | }, 1630 | "less": { 1631 | "optional": true 1632 | }, 1633 | "lightningcss": { 1634 | "optional": true 1635 | }, 1636 | "sass": { 1637 | "optional": true 1638 | }, 1639 | "stylus": { 1640 | "optional": true 1641 | }, 1642 | "sugarss": { 1643 | "optional": true 1644 | }, 1645 | "terser": { 1646 | "optional": true 1647 | } 1648 | } 1649 | }, 1650 | "node_modules/vue": { 1651 | "version": "3.3.4", 1652 | "resolved": "https://registry.npmmirror.com/vue/-/vue-3.3.4.tgz", 1653 | "integrity": "sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==", 1654 | "dependencies": { 1655 | "@vue/compiler-dom": "3.3.4", 1656 | "@vue/compiler-sfc": "3.3.4", 1657 | "@vue/runtime-dom": "3.3.4", 1658 | "@vue/server-renderer": "3.3.4", 1659 | "@vue/shared": "3.3.4" 1660 | } 1661 | }, 1662 | "node_modules/vue-router": { 1663 | "version": "4.2.4", 1664 | "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.2.4.tgz", 1665 | "integrity": "sha512-9PISkmaCO02OzPVOMq2w82ilty6+xJmQrarYZDkjZBfl4RvYAlt4PKnEX21oW4KTtWfa9OuO/b3qk1Od3AEdCQ==", 1666 | "dependencies": { 1667 | "@vue/devtools-api": "^6.5.0" 1668 | }, 1669 | "peerDependencies": { 1670 | "vue": "^3.2.0" 1671 | } 1672 | }, 1673 | "node_modules/webpack-sources": { 1674 | "version": "3.2.3", 1675 | "resolved": "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.2.3.tgz", 1676 | "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", 1677 | "dev": true, 1678 | "engines": { 1679 | "node": ">=10.13.0" 1680 | } 1681 | }, 1682 | "node_modules/webpack-virtual-modules": { 1683 | "version": "0.5.0", 1684 | "resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz", 1685 | "integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==", 1686 | "dev": true 1687 | } 1688 | } 1689 | } 1690 | -------------------------------------------------------------------------------- /my-project-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-project-frontend", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@element-plus/icons-vue": "^2.1.0", 12 | "@vueuse/core": "^10.3.0", 13 | "axios": "^1.4.0", 14 | "element-plus": "^2.3.9", 15 | "vue": "^3.3.4", 16 | "vue-router": "^4.2.4" 17 | }, 18 | "devDependencies": { 19 | "@vitejs/plugin-vue": "^4.2.3", 20 | "unplugin-auto-import": "^0.15.2", 21 | "unplugin-vue-components": "^0.24.1", 22 | "vite": "^4.4.6" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /my-project-frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itbaima-study/SpringBoot-Vue-Template-Jwt/0bb072e6b99825cf9a0911a79175366f105fd8d4/my-project-frontend/public/favicon.ico -------------------------------------------------------------------------------- /my-project-frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 24 | 25 | 30 | -------------------------------------------------------------------------------- /my-project-frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import axios from "axios"; 5 | 6 | import 'element-plus/theme-chalk/dark/css-vars.css' 7 | 8 | axios.defaults.baseURL = 'http://localhost:8080' 9 | 10 | const app = createApp(App) 11 | 12 | app.use(router) 13 | 14 | app.mount('#app') 15 | -------------------------------------------------------------------------------- /my-project-frontend/src/net/index.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import {ElMessage} from "element-plus"; 3 | import router from "@/router"; 4 | 5 | const authItemName = "authorize" 6 | 7 | const accessHeader = () => { 8 | return { 9 | 'Authorization': `Bearer ${takeAccessToken()}` 10 | } 11 | } 12 | 13 | const defaultError = (error) => { 14 | console.error(error) 15 | const status = error.response.status 16 | if (status === 429) { 17 | ElMessage.error(error.response.data.message) 18 | } else { 19 | ElMessage.error('发生了一些错误,请联系管理员') 20 | } 21 | } 22 | 23 | const defaultFailure = (message, status, url) => { 24 | console.warn(`请求地址: ${url}, 状态码: ${status}, 错误信息: ${message}`) 25 | ElMessage.warning(message) 26 | } 27 | 28 | function takeAccessToken() { 29 | const str = localStorage.getItem(authItemName) || sessionStorage.getItem(authItemName); 30 | if(!str) return null 31 | const authObj = JSON.parse(str) 32 | if(new Date(authObj.expire) <= new Date()) { 33 | deleteAccessToken() 34 | ElMessage.warning("登录状态已过期,请重新登录!") 35 | return null 36 | } 37 | return authObj.token 38 | } 39 | 40 | function storeAccessToken(remember, token, expire){ 41 | const authObj = { 42 | token: token, 43 | expire: expire 44 | } 45 | const str = JSON.stringify(authObj) 46 | if(remember) 47 | localStorage.setItem(authItemName, str) 48 | else 49 | sessionStorage.setItem(authItemName, str) 50 | } 51 | 52 | function deleteAccessToken(redirect = false) { 53 | localStorage.removeItem(authItemName) 54 | sessionStorage.removeItem(authItemName) 55 | if(redirect) { 56 | router.push({ name: 'welcome-login' }) 57 | } 58 | } 59 | 60 | function internalPost(url, data, headers, success, failure, error = defaultError){ 61 | axios.post(url, data, { headers: headers }).then(({data}) => { 62 | if(data.code === 200) { 63 | success(data.data) 64 | } else if(data.code === 401) { 65 | failure('登录状态已过期,请重新登录!') 66 | deleteAccessToken(true) 67 | } else { 68 | failure(data.message, data.code, url) 69 | } 70 | }).catch(err => error(err)) 71 | } 72 | 73 | function internalGet(url, headers, success, failure, error = defaultError){ 74 | axios.get(url, { headers: headers }).then(({data}) => { 75 | if(data.code === 200) { 76 | success(data.data) 77 | } else if(data.code === 401) { 78 | failure('登录状态已过期,请重新登录!') 79 | deleteAccessToken(true) 80 | } else { 81 | failure(data.message, data.code, url) 82 | } 83 | }).catch(err => error(err)) 84 | } 85 | 86 | function login(username, password, remember, success, failure = defaultFailure){ 87 | internalPost('/api/auth/login', { 88 | username: username, 89 | password: password 90 | }, { 91 | 'Content-Type': 'application/x-www-form-urlencoded' 92 | }, (data) => { 93 | storeAccessToken(remember, data.token, data.expire) 94 | ElMessage.success(`登录成功,欢迎 ${data.username} 来到我们的系统`) 95 | success(data) 96 | }, failure) 97 | } 98 | 99 | function post(url, data, success, failure = defaultFailure) { 100 | internalPost(url, data, accessHeader() , success, failure) 101 | } 102 | 103 | function logout(success, failure = defaultFailure){ 104 | get('/api/auth/logout', () => { 105 | deleteAccessToken() 106 | ElMessage.success(`退出登录成功,欢迎您再次使用`) 107 | success() 108 | }, failure) 109 | } 110 | 111 | function get(url, success, failure = defaultFailure) { 112 | internalGet(url, accessHeader(), success, failure) 113 | } 114 | 115 | function unauthorized() { 116 | return !takeAccessToken() 117 | } 118 | 119 | export { post, get, login, logout, unauthorized } 120 | -------------------------------------------------------------------------------- /my-project-frontend/src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import { unauthorized } from "@/net"; 3 | 4 | const router = createRouter({ 5 | history: createWebHistory(import.meta.env.BASE_URL), 6 | routes: [ 7 | { 8 | path: '/', 9 | name: 'welcome', 10 | component: () => import('@/views/WelcomeView.vue'), 11 | children: [ 12 | { 13 | path: '', 14 | name: 'welcome-login', 15 | component: () => import('@/views/welcome/LoginPage.vue') 16 | }, { 17 | path: 'register', 18 | name: 'welcome-register', 19 | component: () => import('@/views/welcome/RegisterPage.vue') 20 | }, { 21 | path: 'forget', 22 | name: 'welcome-forget', 23 | component: () => import('@/views/welcome/ForgetPage.vue') 24 | } 25 | ] 26 | }, { 27 | path: '/index', 28 | name: 'index', 29 | component: () => import('@/views/IndexView.vue'), 30 | } 31 | ] 32 | }) 33 | 34 | router.beforeEach((to, from, next) => { 35 | const isUnauthorized = unauthorized() 36 | if(to.name.startsWith('welcome') && !isUnauthorized) { 37 | next('/index') 38 | } else if(to.fullPath.startsWith('/index') && isUnauthorized) { 39 | next('/') 40 | } else { 41 | next() 42 | } 43 | }) 44 | 45 | export default router 46 | -------------------------------------------------------------------------------- /my-project-frontend/src/views/IndexView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 19 | -------------------------------------------------------------------------------- /my-project-frontend/src/views/WelcomeView.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 25 | 26 | 41 | -------------------------------------------------------------------------------- /my-project-frontend/src/views/welcome/ForgetPage.vue: -------------------------------------------------------------------------------- 1 | 79 | 80 | 175 | 176 | 179 | -------------------------------------------------------------------------------- /my-project-frontend/src/views/welcome/LoginPage.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 81 | 82 | 85 | -------------------------------------------------------------------------------- /my-project-frontend/src/views/welcome/RegisterPage.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 165 | 166 | 169 | -------------------------------------------------------------------------------- /my-project-frontend/vite.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | import AutoImport from 'unplugin-auto-import/vite' 6 | import Components from 'unplugin-vue-components/vite' 7 | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' 8 | 9 | // https://vitejs.dev/config/ 10 | export default defineConfig({ 11 | plugins: [ 12 | vue(), 13 | AutoImport({ 14 | resolvers: [ElementPlusResolver()], 15 | }), 16 | Components({ 17 | resolvers: [ElementPlusResolver()], 18 | }), 19 | ], 20 | resolve: { 21 | alias: { 22 | '@': fileURLToPath(new URL('./src', import.meta.url)) 23 | } 24 | } 25 | }) 26 | --------------------------------------------------------------------------------