SpringCloud一代组件+nginx实现简单的登录注册


使用简单的登录注册功能串联起 验证码的生成与发送、邮件发送、IP防爆刷、用户统一认证等功能,实现所涉及到的技术全部基于 SpringCloud微服务架构: Nginx、Eureka、Feign(Ribbon、Hystrix)、Gateway、Config+bus

软件依赖版本

依赖软件版本号
JDK11
SpringCloudGreenwich.RELEASE
SpringBoot2.1.6.RELEASE
Nginxwindows版1.20.2
rabbitMqwindows版rabbitmq_server-3.10.0
configServerGitgitee

SpringCloud一代组件对应的实现步骤:

1.要求

实现需求

2.实现步骤

1. 首先将项目骨架创建出来

lagou-liuyu-login-parent 项目(父工程,pom文件中打包方式为pom),然后添加SpringCloud相关组件的依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.lagou.liuyu</groupId>
    <artifactId>lagou-liuyu-login-parent</artifactId>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>lagou-service-user</module>
        <module>lagou-service-code</module>
        <module>lagou-service-email</module>
        <module>lagou-cloud-configserver</module>
        <module>lagou-cloud-eureka-server-8761</module>
        <module>lagou-cloud-eureka-server-8762</module>
        <module>lagou-service-common</module>
        <module>lagou-cloud-business</module>
    </modules>

    <!--⽗⼯程打包⽅式为pom-->
    <packaging>pom</packaging>

    <!--spring boot 父启动器依赖-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
    </parent>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!--spring cloud依赖管理,引入了Spring Cloud的版本-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!--web依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--日志依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
        <!--测试依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--lombok工具-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.4</version>
            <scope>provided</scope>
        </dependency>
        <!-- Actuator可以帮助你监控和管理Spring Boot应用-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--热部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

        <!--eureka server 需要引入Jaxb,开始-->
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-core</artifactId>
            <version>2.2.11</version>
        </dependency>
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
        </dependency>
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-impl</artifactId>
            <version>2.2.11</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jaxb</groupId>
            <artifactId>jaxb-runtime</artifactId>
            <version>2.2.10-b140310.1920</version>
        </dependency>
        <dependency>
            <groupId>javax.activation</groupId>
            <artifactId>activation</artifactId>
            <version>1.1.1</version>
        </dependency>
        <!--引入Jaxb,结束-->
    </dependencies>

    <build>
        <plugins>
            <!--编译插件-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>11</source>
                    <target>11</target>
                    <encoding>utf-8</encoding>
                </configuration>
            </plugin>
            <!--打包插件-->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

2.然后将每个功能对应的子项目创建出来

其中包括:

​ a. 注册中心(集群)项目 lagou-cloud-eureka-server-8761、lagou-cloud-eureka-server-8762

​ b. 全局配置中心项目 lagou-cloud-configserver (9006端口)

​ c. 全局网关项目 lagou-cloud-gateway (网关项目为单独的工程,不做为子项目引入)(9002端口)

​ d. 验证码项目 lagou-service-code (8081端口)

​ e. 邮件项目 lagou-service-email (8082端口)

​ f. 用户项目 lagou-service-user (8080端口)

​ g. 公共服务项目 lagou-service-common

​ h. 统一业务层项目 lagou-cloud-business (8070端口)

3.导入对应的数据库和配置全局配置

  1. 添加项目中所需要的数据库表
    在这里插入图片描述

  2. gitee中添加仓库用于config的全局配置文件

在这里插入图片描述

4.配置每个项目基本依赖和配置文件

1.注册中心项目

添加eureka-server的依赖

<dependencies>
    <!--Eureka server依赖-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
</dependencies>

添加端口号和eureka注册地址相关的配置

server:
  port: 8761
spring:
  application:
    name: lagou-cloud-eureka-server

eureka:
  instance:
    # 当前eureka实例的主机名称
    hostname: LagouCloudEurekaServerA
  client:
    service-url:
      # 客户端与Eureka server交互的地址,EurekaServer相对于其他server来说也是client
      # 集群模式下,需要写其他server的地址,多个使用逗号拼接即可
      defaultZone: http://LagouCloudEurekaServerB:8762/eureka/
      # 是否注册到eureka中, 自己就是serve不需要注册自己,集群模式下设置为true
    register-with-eureka: true
    # 自己就是服务不需要从Eureka Server获取服务信息,默认为true, 这里设置为false
    fetch-registry: true

启动类上添加开启eurekaserver的注解

/**
 * @author LiuYu
 * @date 2022/5/14 18:15
 */
@SpringBootApplication
@EnableEurekaServer
public class LagouCloudEurekaServer8761 {
    public static void main(String[] args) {
        SpringApplication.run(LagouCloudEurekaServer8761.class, args);
    }
}

2.网关项目

添加相应的SpringCloud依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!--spring boot 父启动器依赖-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
    </parent>

    <artifactId>lagou-cloud-gateway</artifactId>
    <groupId>com.lagou.liuyu</groupId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-commons</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!--GateWay 网关-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!--引入webflux-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <!--日志依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
        <!--测试依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--lombok工具-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.4</version>
            <scope>provided</scope>
        </dependency>

        <!--引入Jaxb,开始-->
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-core</artifactId>
            <version>2.2.11</version>
        </dependency>
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
        </dependency>
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-impl</artifactId>
            <version>2.2.11</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jaxb</groupId>
            <artifactId>jaxb-runtime</artifactId>
            <version>2.2.10-b140310.1920</version>
        </dependency>
        <dependency>
            <groupId>javax.activation</groupId>
            <artifactId>activation</artifactId>
            <version>1.1.1</version>
        </dependency>
        <!--引入Jaxb,结束-->

        <!-- Actuator可以帮助你监控和管理Spring Boot应用-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--热部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

        <!--链路追踪-->
        <!--<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-sleuth</artifactId>
        </dependency>-->
    </dependencies>

    <dependencyManagement>
        <!--spring cloud依赖版本管理-->
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <!--编译插件-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>11</source>
                    <target>11</target>
                    <encoding>utf-8</encoding>
                </configuration>
            </plugin>
            <!--打包插件-->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>

添加相关的配置信息

eureka配置信息和网关gateway配置信息

server:
  port: 9002
eureka:
  client:
    serviceUrl: # eureka server的路径
      defaultZone: http://lagoucloudeurekaservera:8761/eureka/,http://lagoucloudeurekaserverb:8762/eureka/ #把 eureka 集群中的所有 url 都填写了进来,也可以只写一台,因为各个 eureka server 可以同步注册表
  instance:
    #使用ip注册,否则会使用主机名注册了(此处考虑到对老版本的兼容,新版本经过实验都是ip)
    prefer-ip-address: true
    #自定义实例显示格式,加上版本号,便于多版本管理,注意是ip-address,早期版本是ipAddress
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
spring:
  application:
    name: lagou-cloud-gateway
  cloud:
    gateway:
      routes: # 路由可以有多个
        # 我们自定义的路由 ID,保持唯一
        - id: lagou-service-code
          #uri: http://127.0.0.1:8091  # 目标服务地址  自动投递微服务(部署多实例)  动态路由:uri配置的应该是一个服务名称,而不应该是一个具体的服务实例的地址
          # gateway网关从服务注册中心获取实例信息然后负载后路由
          uri: lb://lagou-service-code
          # 断言:路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默 认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。
          predicates:
            - Path=/api/code/**
        # 我们自定义的路由 ID,保持唯一
        - id: lagou-service-mail
          #uri: http://127.0.0.1:8080       # 目标服务地址
          uri: lb://lagou-service-mail
          # 断言:路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默 认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。
          predicates:
        - id: lagou-service-user
          #uri: http://127.0.0.1:8080       # 目标服务地址
          uri: lb://lagou-service-user
          # 断言:路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默 认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。
          predicates:
            - Path=/api/user/**

启动类中添加eureka注解

/**
 * @author LiuYu
 * @date 2022/5/14 20:31
 */
@SpringBootApplication
@EnableDiscoveryClient
public class GateWayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GateWayApplication.class, args);
    }
}

3.全局配置中心项目

添加eureka、config、bus相关的依赖信息

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-config-server</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-bus-amqp</artifactId>
    </dependency>
</dependencies>

添加eureka相关的配置信息和config相关的配置信息和开启springboot的健康检查接口

server:
  port: 9006
#注册到Eureka服务中心
eureka:
  client:
    service-url:
      # 注册到集群,就把多个Eurekaserver地址使用逗号连接起来即可;注册到单实例(非集群模式),那就写一个就ok
      defaultZone: http://LagouCloudEurekaServerA:8761/eureka,http://LagouCloudEurekaServerB:8762/eureka
  instance:
    prefer-ip-address: true  #服务实例中显示ip,而不是显示主机名(兼容老的eureka版本)
    # 实例名称: 192.168.1.103:lagou-service-resume:8080,我们可以自定义它
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@

spring:
  application:
    name: lagou-cloud-configserver
  cloud:
    config:
      server:
        git:
          uri: https://gitee.com/struggle_ly/spring-cloud-config-repo.git #配置git服务地址
          username: ****** #配置git用户名
          password: ****** #配置git密码
          search-paths:
            - spring-cloud-config-repo
      # 读取分支
      label: lagou-login-config
#针对的被调用方微服务名称,不加就是全局生效
#lagou-service-resume:
#  ribbon:
#    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule #负载策略调整

启动类添加eureka和configServer的注解

/**
 * @author LiuYu
 * @date 2022/5/14 20:42
 */
@SpringBootApplication
@EnableDiscoveryClient
@EnableConfigServer
public class ConfigServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }
}

4.公共服务common项目

添加对mysql和jpa的依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

添加对应的配置文件和启动类

设置pojo包,用于放置对应数据库表的实体类

在这里插入图片描述

5.用户服务项目

6.邮件服务项目

7.验证码服务项目

添加对应的依赖信息:service-common、eureka、config、bus、mail(邮件服务需要该依赖)

<dependencies>
    <dependency>
        <groupId>com.lagou.liuyu</groupId>
        <artifactId>lagou-service-common</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-config-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-bus-amqp</artifactId>
    </dependency>
    <!-- 发邮件的依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-mail</artifactId>
    </dependency>
            <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
</dependencies>

添加对应的配置文件信息:config信息、eureka信息、邮件信息(邮件服务需要)

server:
  port: 8082
spring:
  application:
    name: lagou-service-email
  cloud:
    # config客户端配置和ConfigServer进行通信,并告知ConfigServer希望获取的配置信息在哪个文件中(lable分支下的name-profile.yml)
    config:
      # 获取的文件名称
      name: lagou-config-common
      # 获取的文件名称后缀
      profile:
      # 获取的仓库分支
      label: lagou-login-config
      # configServer的访问地址 这里不能设置eureka server的名称
      uri: http://localhost:9006
  # 邮件发送配置
  mail:
    # 邮箱服务器地址
    host: smtp.189.cn
    # 邮箱账号
    username: ******@**.com
    # 邮箱密码,如果POP3需要授权码,此处应填授权码
    password: ******
    default-encoding: UTF-8

# 注册到eureka server中
eureka:
  client:
    service-url:
      # 注册到集群,就把多个eurekaServer地址使用逗号进行连接起来即可,注册到单实例(非集群模式), 那就写一个
      defaultZone: http://LagouCloudEurekaServerA:8761/eureka/,http://LagouCloudEurekaServerB:8762/eureka/
  instance:
    #  服务实例中显示ip,而不是显示主机名(兼容老的eureka版本)
    prefer-ip-address: true
    # 实例名称  172.16.172.24:lagou-service-resume:8080, 这里使用instance-id进行自定义
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@

#针对的被调用方微服务名称,不加就是全局生效
ribbon:
  #请求连接超时时间
  ConnectTimeout: 2000
  #请求处理超时时间
  ##########################################Feign超时时长设置
  ReadTimeout: 15000
  #对所有操作都进行重试
  OkToRetryOnAllOperations: true
  ####根据如上配置,当访问到故障请求的时候,它会再尝试访问一次当前实例(次数由MaxAutoRetries配置),
  ####如果不行,就换一个实例进行访问,如果还不行,再换一次实例访问(更换次数由MaxAutoRetriesNextServer配置),
  ####如果依然不行,返回失败信息。
  MaxAutoRetries: 0 #对当前选中实例重试次数,不包括第一次调用
  MaxAutoRetriesNextServer: 0 #切换实例的重试次数
  NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule #负载策略调整
logging:
  level:
    # Feign 日志只会对日志级别为debug的做出响应
    com.lagou.liuyu.edu.service.ResumeServiceFeignClient: debug

# 开启Feign的熔断功能
feign:
  hystrix:
    enabled: true
  ## Feign 支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗
  compression:
    request:
      # 开启请求压缩
      enabled: true
      # 设置压缩的数据类型,此处也是默认的数据
      mime-types: text/html,application/xml,application/json
      # 设置触发压缩的大小下限,此处也是默认的数据
      min-request-size: 2048
    response:
      # 开启响应压缩
      enabled: true
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            ##########################################Hystrix的超时时长设置
            timeoutInMilliseconds: 5000

在对应的启动类上添加相应的注解:eureka、扫描实体的路径

/**
 * @author LiuYu
 * @date 2022/5/14 22:02
 */
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EntityScan("com.lagou.liuyu")
public class EmailApplication {
    public static void main(String[] args) {
        SpringApplication.run(EmailApplication.class, args);
    }
}

8.统一对外提供服务业务层

添加相应的依赖eureka、openFeign、config、bus

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-amqp</artifactId>
        </dependency>
    </dependencies>

添加相应的配置信息eureka、config、ribbon、feign.hystrix

server:
  port: 8070

spring:
  application:
    name: lagou-cloud-business
  cloud:
    # config客户端配置和ConfigServer进行通信,并告知ConfigServer希望获取的配置信息在哪个文件中(lable分支下的name-profile.yml)
    config:
      # 获取的文件名称
      name: lagou-config-common
      # 获取的文件名称后缀
      profile:
      # 获取的仓库分支
      label: lagou-login-config
      # configServer的访问地址 这里不能设置eureka server的名称
      uri: http://localhost:9006

# 注册到eureka server中
eureka:
  client:
    service-url:
      # 注册到集群,就把多个eurekaServer地址使用逗号进行连接起来即可,注册到单实例(非集群模式), 那就写一个
      defaultZone: http://LagouCloudEurekaServerA:8761/eureka/,http://LagouCloudEurekaServerB:8762/eureka/
  instance:
    #  服务实例中显示ip,而不是显示主机名(兼容老的eureka版本)
    prefer-ip-address: true
    # 实例名称  172.16.172.24:lagou-service-resume:8080, 这里使用instance-id进行自定义
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@

# ribbon负载均衡策略调整, 指定项目使用的负载均衡策略,不写项目名默认所有项目
#lagou-service-resume:
#  ribbon:
#    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule # 负载均衡策略的调整

#针对的被调用方微服务名称,不加就是全局生效
ribbon:
  #请求连接超时时间
  ConnectTimeout: 2000
  #请求处理超时时间
  ##########################################Feign超时时长设置
  ReadTimeout: 15000
  #对所有操作都进行重试
  OkToRetryOnAllOperations: true
  ####根据如上配置,当访问到故障请求的时候,它会再尝试访问一次当前实例(次数由MaxAutoRetries配置),
  ####如果不行,就换一个实例进行访问,如果还不行,再换一次实例访问(更换次数由MaxAutoRetriesNextServer配置),
  ####如果依然不行,返回失败信息。
  MaxAutoRetries: 0 #对当前选中实例重试次数,不包括第一次调用
  MaxAutoRetriesNextServer: 0 #切换实例的重试次数
  NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule #负载策略调整
logging:
  level:
    # Feign 日志只会对日志级别为debug的做出响应
    com.lagou.liuyu.edu.service.ResumeServiceFeignClient: debug

# 开启Feign的熔断功能
feign:
  hystrix:
    enabled: true
  ## Feign 支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗
  compression:
    request:
      # 开启请求压缩
      enabled: true
      # 设置压缩的数据类型,此处也是默认的数据
      mime-types: text/html,application/xml,application/json
      # 设置触发压缩的大小下限,此处也是默认的数据
      min-request-size: 2048
    response:
      # 开启响应压缩
      enabled: true
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            ##########################################Hystrix的超时时长设置
            timeoutInMilliseconds: 5000

启动类添加相应的注解

/**
 * @author LiuYu
 * @date 2022/5/15 10:05
 */
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class BusinessApplication {
    public static void main(String[] args) {
        SpringApplication.run(BusinessApplication.class, args);
    }
}

5.具体业务实现

1.邮件服务

邮件服务实现一个发送邮件的工具类

/**
 * 发送邮件工具类
 *
 * @author LiuYu
 * @date 2022/5/14 14:27
 */
@Component
public class SendMailUtil{

    @Autowired
    private JavaMailSender javaMailSender;

    @Value("${spring.mail.username}")
    private String fromUserName;

    /**
     * 发送邮件工具类(普通的文本文件)
     *
     * @param toEmail 收件人邮箱(1********23@qq.com)
     * @param title 邮件的标题
     * @param msg 发送的内容
     */
    public void sendSimpleMail(String toEmail,String title, String msg){
        SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
        // 发件人
        simpleMailMessage.setFrom(fromUserName);
        // 收件人
        simpleMailMessage.setTo(toEmail);
        // 邮件标题
        simpleMailMessage.setSubject(title);
        // 邮件内容
        simpleMailMessage.setText(msg);

        javaMailSender.send(simpleMailMessage);
    }

    /**
     * 发送邮件工具类(富文本邮件---带图片的邮件,支持多张图片)
     * 发送富文本邮件需要使用MimeMessageHelper类,MimeMessageHelper支持发送复杂邮件模板,支持文本、附件、HTML、图片等。
     * 如果需要发送多张图片,可以改变传参方式,使用集合添加多个<img src='cid:rscId'>和 helper.addInline(rscId, res);即可实现'
     *
     * @param toEmail 收件人
     * @param title 邮件标题
     * @param msg 邮件内容
     * @param recPathMap 图片信息 : 图片路径
     */
    public void sendInlineResourceMail(String toEmail, String title, String msg, Map<String, String> recPathMap) throws MessagingException {
        MimeMessage mimeMessage = javaMailSender.createMimeMessage();

        MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true);

        mimeMessageHelper.setFrom(fromUserName);
        mimeMessageHelper.setTo(toEmail);
        mimeMessageHelper.setSubject(title);
        mimeMessageHelper.setText(msg, true);

        if (!recPathMap.isEmpty()){
            recPathMap.keySet().forEach(rsc -> {
                FileSystemResource res = new FileSystemResource(new File(recPathMap.get(rsc)));
                try {
                    mimeMessageHelper.addInline(rsc, res);
                } catch (MessagingException e) {
                    e.printStackTrace();
                }
            });
        }

        javaMailSender.send(mimeMessage);
    }

    /**
     * 发送邮件工具类(发送HTML邮件)
     *
     * @param toEmail 收件人
     * @param title 邮件标题
     * @param msg 邮件内容
     */
    public void sendHtmlMail(String toEmail,String title, String msg) throws MessagingException {
        MimeMessage mimeMessage = javaMailSender.createMimeMessage();

        MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true);

        mimeMessageHelper.setFrom(fromUserName);
        mimeMessageHelper.setTo(toEmail);
        mimeMessageHelper.setSubject(title);
        mimeMessageHelper.setText(msg, true);

        javaMailSender.send(mimeMessage);
    }

    /**
     * 发送邮件工具类(发送带附件的邮件)
     * 如果有多个附件,同样可以改变传参方式,使用集合多次调用helper.addAttachment(fileName, file);如多个图片的实现方式
     *
     * @param toEmail 收件人
     * @param title 标题
     * @param msg 内容
     * @param filePath 附件路径
     */
    public void sendAttachmentsMail(String toEmail,String title, String msg, String filePath) throws MessagingException {
        MimeMessage mimeMessage = javaMailSender.createMimeMessage();

        MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true);

        mimeMessageHelper.setFrom(fromUserName);
        mimeMessageHelper.setTo(toEmail);
        mimeMessageHelper.setSubject(title);
        mimeMessageHelper.setText(msg, true);

        FileSystemResource file = new FileSystemResource(new File(filePath));
        mimeMessageHelper.addAttachment(file.getFilename(), file);

        javaMailSender.send(mimeMessage);
    }
}

然后提供相关的发送邮件的接口

/**
 * 发送邮件服务
 *
 * @author LiuYu
 * @date 2022/5/15 10:38
 */
@RestController
@RequestMapping("/email")
public class EmailController {

    @Autowired
    private IEmailService emailService;

    @PostMapping("/{toEmail}/{code}")
    public ResultDTO<String> sendEmailByCode(@PathVariable("toEmail") String toEmail, @PathVariable("code") String code){
        emailService.sendEmailByCode(toEmail, code);
        return ResultUtils.SUCCESS();
    }

}

/**
 * @author LiuYu
 * @date 2022/5/15 11:12
 */
@Service
public class EmailServiceImpl implements IEmailService {

    @Autowired
    private SendMailUtil sendMailUtil;

    @Override
    public void sendEmailByCode(String toEmail, String code) {
        String title = "注册用户验证码";
        String msg = "当前注册的验证码为:【" + code + "】,10分钟内有效,请不要泄露给其他人!";
        sendMailUtil.sendSimpleMail(toEmail, title, msg);
    }
}

测试邮件发送功能

/**
 * @author LiuYu
 * @date 2022/5/15 11:27
 */
@SpringBootTest(classes = {EmailApplication.class})
@RunWith(SpringJUnit4ClassRunner.class)
public class EmailServiceImplTest {

    @Autowired
    private IEmailService emailService;

    @Test
    public void sendEmailByCode() {
        emailService.sendEmailByCode("****@qq.com", "123456");
    }
}

正常接收到邮件

在这里插入图片描述

2.验证码服务

在这里插入图片描述

定义对应的controller层

/**
 * @author LiuYu
 * @date 2022/5/15 11:37
 */
@RestController
@RequestMapping("/code")
public class CodeController {

    @Autowired
    private ICodeService codeService;

    @PostMapping("/create/{toEmail}")
    public ResultDTO createCode(@PathVariable("toEmail") String toEmail){
        try {
            codeService.createCode(toEmail);
        } catch (Exception e) {
            e.printStackTrace();
            return ResultUtils.ERROR(BaseEnum.CREATE_CODE_ERROR);
        }
        return ResultUtils.SUCCESS();
    }

    @PostMapping("/validate/{toEmail}/{code}")
    public ResultDTO validateCode(@PathVariable("toEmail")String toEmail, @PathVariable("code")String code){
        final int result;
        try {
            result = codeService.validateCode(toEmail, code);
        } catch (Exception e) {
            e.printStackTrace();
            return ResultUtils.ERROR(BaseEnum.VALIDATE_CODE_ERROR);
        }
        return ResultUtils.SUCCESS(result);
    }

}

具体的service层实现逻辑

@Service
public class CodeServiceImpl implements ICodeService {

    @Autowired
    private LagouAuthCodeDao lagouAuthCodeDao;
    @Autowired
    private IEmailService emailService;

    @Override
    public void createCode(String toEmail) {
        // 随机生成一个6位数的验证码,10分钟内有效
        Random random = new Random();
        final int randomCode = random.nextInt(1000000);

        LagouAuthCode lagouAuthCode = new LagouAuthCode();
        lagouAuthCode.setEmail(toEmail);
        lagouAuthCode.setCode(String.valueOf(randomCode));
        lagouAuthCode.setCreateTime(new Date());
        lagouAuthCode.setExpireTime(DateUtils.addMinutes(new Date(), 10));
        lagouAuthCodeDao.save(lagouAuthCode);

        // 发送验证码至邮件
        emailService.sendEmailByCode(toEmail, String.valueOf(randomCode));
    }

    @Override
    public int validateCode(String toEmail, String code) {
        LagouAuthCode lagouAuthCode = new LagouAuthCode();
        lagouAuthCode.setEmail(toEmail);
        lagouAuthCode.setCode(code);
        Example<LagouAuthCode> example = Example.of(lagouAuthCode);
        Sort sort = Sort.by(Sort.Direction.DESC, "expireTime");

        final List<LagouAuthCode> all = lagouAuthCodeDao.findAll(example, sort);
        if(CollectionUtils.isEmpty(all)){
            // 当前email没有查询到对应的code,则为验证码错误
            return 1;
        }

        final LagouAuthCode lac = all.get(0);
        final Date expireTime = lac.getExpireTime();
        if(new Date().after(expireTime)){
            // 验证码超时,超过了10分钟
            return 2;
        }

        return 0;
    }
}

service内部使用的dao层和email服务的接口

/**
 * @author LiuYu
 * @date 2022/5/15 11:51
 */
public interface LagouAuthCodeDao extends JpaRepository<LagouAuthCode, Integer> {
}

通过feign请求的方式

/**
 * @author LiuYu
 * @date 2022/5/15 15:52
 */
@FeignClient(value = "lagou-service-email", fallback = EmailServiceFallBack.class, path = "/email")
public interface IEmailService {

    @PostMapping("/{toEmail}/{code}")
    ResultDTO sendEmailByCode(@PathVariable("toEmail") String toEmail, @PathVariable("code") String code);

}

对应的EmailServiceFallBack(服务降级)方法

/**
 * @author LiuYu
 * @date 2022/5/15 15:54
 */
@Component
public class EmailServiceFallBack implements IEmailService {

    @Override
    public ResultDTO sendEmailByCode(String toEmail, String code) {
        return ResultUtils.SUCCESS();
    }

}

测试生成验证码和校验验证码的功能

/**
 * @author LiuYu
 * @date 2022/5/15 16:27
 */
@SpringBootTest(classes = {CodeApplication.class})
@RunWith(SpringJUnit4ClassRunner.class)
public class CodeServiceImplTest {

    @Autowired
    private ICodeService codeService;

    @Test
    public void createCode() {
        codeService.createCode("******@qq.com");
    }

    @Test
    public void validateCode() {
        final int result = codeService.validateCode("******@qq.com", "804959");
        System.out.println("result = " + result);
    }
}

正常收到邮件

在这里插入图片描述

验证码校验通过,正常返回数据

在这里插入图片描述

3.用户服务

在这里插入图片描述

定义对应的controller层

/**
 * @author LiuYu
 * @date 2022/5/15 09:50
 */
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private IUserService userService;

    @PostMapping("/register/{email}/{passWord}/{code}")
    public ResultDTO register(@PathVariable("email") String email,
                              @PathVariable("passWord") String passWord,
                              @PathVariable("code") String code,
                              HttpServletResponse response){
        return userService.register(email, passWord, code, response);
    }

    @GetMapping("/isRegistered/{email}")
    public ResultDTO isRegistered(@PathVariable("email") String email){
        return ResultUtils.SUCCESS(userService.isRegistered(email));
    }

    @PostMapping("/user/login/{email}/{passWord}")
    public ResultDTO login(@PathVariable("email") String email, @PathVariable("passWord") String passWord,
                           HttpServletResponse response){
        return ResultUtils.SUCCESS(userService.login(email, passWord, response));
    }

    @GetMapping("/user/info/{token}")
    public ResultDTO userInfo(@PathVariable("token") String token){
        return ResultUtils.SUCCESS(userService.userInfo(token));
    }

}

定义对应的service层

/**
 * @author LiuYu
 * @date 2022/5/15 22:04
 */
@Service
public class UserServiceImpl implements IUserService {

    @Autowired
    private ICodeService codeService;
    @Autowired
    private LagouTokenDao lagouTokenDao;

    /**
     * 注册接⼝,true成功,false失败
     *
     * @param email
     * @param passWord
     * @param code
     */
    @Override
    public ResultDTO register(String email, String passWord, String code, HttpServletResponse response) {
        // 验证验证码是否正确
        final ResultDTO resultDTO = codeService.validateCode(email, code);
        // 验证码服务异常
        if(!Objects.equals(BaseEnum.SUCCESS.code(), resultDTO.getCode())){
            return ResultUtils.ERROR(BaseEnum.REGISTER_ERROR);
        }
        // 验证码错误
        if(Objects.equals(resultDTO.getData(), 1)){
            return ResultUtils.ERROR(BaseEnum.VALIDATE_CODE_ERROR);
        }
        // 验证码超时,超过10分钟
        if(Objects.equals(resultDTO.getData(), 2)){
            return ResultUtils.ERROR(BaseEnum.VALIDATE_CODE_TIMEOUT);
        }

        // 验证码正确,开始注册账户,生成token令牌
        final String token = DigestUtils.md5Hex(email+passWord);
        LagouToken lagouToken = new LagouToken();
        lagouToken.setToken(token);
        lagouToken.setEmail(email);
        lagouTokenDao.save(lagouToken);

        Cookie cookie = new Cookie("user_token", token);
        response.addCookie(cookie);

        return ResultUtils.SUCCESS();
    }

    /**
     * 是否已注册,根据邮箱判断,true代表已经注册过,
     * false代表尚未注册
     *
     * @param email
     * @return
     */
    @Override
    public boolean isRegistered(String email) {
        LagouToken lagouToken = new LagouToken();
        lagouToken.setEmail(email);

        final Optional<LagouToken> one = lagouTokenDao.findOne(Example.of(lagouToken));
        return one.isPresent();
    }

    /**
     * 登录接⼝,验证⽤户名密码合法性,根据⽤户名和
     * 密码⽣成token,token存⼊数据库,并写⼊cookie
     * 中,登录成功返回邮箱地址,重定向到欢迎⻚
     *
     * @param email
     * @param passWord
     * @return
     */
    @Override
    public String login(String email, String passWord, HttpServletResponse response) {

        final String token = DigestUtils.md5Hex(email+passWord);
        LagouToken lagouToken = new LagouToken();
        lagouToken.setToken(token);
        lagouToken.setEmail(email);

        final Optional<LagouToken> one = lagouTokenDao.findOne(Example.of(lagouToken));
        if(one.isPresent()){
            Cookie cookie = new Cookie("user_token", token);
            response.addCookie(cookie);
            return email;
        }

        return null;
    }

    /**
     * 根据token查询⽤户登录邮箱接⼝
     *
     * @param token
     * @return
     */
    @Override
    public String userInfo(String token) {
        LagouToken lagouToken = new LagouToken();
        lagouToken.setToken(token);

        final Optional<LagouToken> one = lagouTokenDao.findOne(Example.of(lagouToken));
        return one.orElse(new LagouToken()).getEmail();
    }
}

对应的dao层

/**
 * @author LiuYu
 * @date 2022/5/16 09:27
 */
public interface LagouTokenDao extends JpaRepository<LagouToken, Integer> {
}

通过feign请求的方式请求code服务接口

/**
 * @author LiuYu
 * @date 2022/5/15 22:15
 */
@FeignClient(value = "lagou-service-code", fallback = CodeServiceFallBack.class, path = "/code")
public interface ICodeService {

    @PostMapping("/validate/{toEmail}/{code}")
    ResultDTO validateCode(@PathVariable("toEmail")String toEmail, @PathVariable("code")String code);

}

对应的服务降级方法为

/**
 * @author LiuYu
 * @date 2022/5/15 22:17
 */
@Component
public class CodeServiceFallBack implements ICodeService {

    @Override
    public ResultDTO validateCode(String toEmail, String code) {
        return ResultUtils.SUCCESS(0);
    }
}

测试对应的接口实现

/**
 * @author LiuYu
 * @date 2022/5/16 21:25
 */
@SpringBootTest(classes = {UserApplication.class})
@RunWith(SpringJUnit4ClassRunner.class)
public class UserServiceImplTest {

    @Autowired
    private IUserService userService;
    @Autowired
    private HttpServletResponse response;

    private static final String EMAIL = "****@qq.com";
    private static final String PASS_WORD = "123456";

    @Test
    public void register() {
        final ResultDTO register = userService.register(EMAIL, PASS_WORD, "804959", response);
        System.out.println("register = " + register);
    }

    @Test
    public void isRegistered() {
        final boolean registered = userService.isRegistered(EMAIL);
        System.out.println("registered = " + registered);
    }

    @Test
    public void login() {
        final String login = userService.login(EMAIL, PASS_WORD, response);
        System.out.println("login = " + login);
    }

    @Test
    public void userInfo() {
        final String userInfo = userService.userInfo("e5f2a8136ece06ad8fddc2760443d666");
        System.out.println("userInfo = " + userInfo);
    }
}
  1. 注册接口,成功返回对应的数据并插入数据库

在这里插入图片描述

  1. 是否注册过接口, 使用已经注册过的email进行测试,正常返回结果

在这里插入图片描述

  1. 登录接口,使用注册的email和密码进行登录测试,正常登录成功返回email信息
    在这里插入图片描述

  2. 用户信息接口,使用注册生成token进行用户信息的查询,结果正常返回email

在这里插入图片描述

6.gateway网关相关逻辑实现

  1. gateway实现ip防爆刷限制

    /**
     * @author LiuYu
     * @date 2022/5/16 22:27
     */
    @Slf4j
    @Component
    public class IpFilter implements GlobalFilter, Ordered {
    
        @Value("${minute:10}")
        private Integer minute;
        @Value("${number:3}")
        private Integer number;
    
        private final Map<String, List<LocalDateTime>> IP_MAP = new HashMap<>();
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            // 获取对应的客户端IP地址
            final ServerHttpRequest request = exchange.getRequest();
            final ServerHttpResponse response = exchange.getResponse();
            final String hostString = request.getRemoteAddress().getHostString();
    
            final String path = request.getURI().getPath();
            if(path.contains("register")){
                final List<LocalDateTime> dates = IP_MAP.get(hostString);
                // 如果当前ip第一次请求,则进行记录然后放行
                if(CollectionUtils.isEmpty(dates)){
                    log.error("当前ip:【{}】第一次请求注册接口允许放行.", hostString);
                    List<LocalDateTime> nowDates = new ArrayList<>();
                    nowDates.add(LocalDateTime.now());
                    IP_MAP.put(hostString, nowDates);
                    return chain.filter(exchange);
                }
                // 如果这里注册的次数小于配置的次数,则直接放行
                if(dates.size() < number){
                    log.error("当前ip:【{}】,请求第【{}】次注册接口,小于限制【{}】次,允许放行.", hostString, dates.size(), number);
                    return chain.filter(exchange);
                }
    
                final LocalDateTime localDateTime = dates.get(number);
                if(LocalDateTime.now().minusMinutes(minute).isBefore(localDateTime)){
                    // 如果当前ip在minute分钟内注册超过number次,则不允许当前注册请求
                    response.setStatusCode(HttpStatus.SEE_OTHER);
                    log.error("当前ip:【{}】在【{}】分钟内注册超过【{}】次,不允许放行.", hostString, minute, number);
                    String data = "您频繁进行注册,请求已被拒绝";
                    final DataBuffer wrap = response.bufferFactory().wrap(data.getBytes());
    
                    return response.writeWith(Mono.just(wrap));
                }
                dates.add(LocalDateTime.now());
            }
    
            return chain.filter(exchange);
        }
    
        @Override
        public int getOrder() {
            return 0;
        }
    
    }
    

7. 配置nginx反向代理服务器

  1. 由于都是基于windows进行测试和开发,所以这里下载了windows版的nginx进行配置使用

    下载链接:http://nginx.org/en/download.html, 选择对应的版本进行下载即可

  2. 下载解压之后打开conf文件夹中nginx.conf文件进行本次需要的配置

  3. 首先配置server_name为www.test.com, 设置允许代理的时候携带header信息(cookie等) proxy_set_header Host $host;

  4. 设置拦截/api开头的请求转发到 gateway网关项目

  5. 设置拦截/static/开头的静态资源请求到本地的目录

    	upstream login-gateway{
    		server 127.0.0.1:9002;
    	}    
    
    	server {
            listen       80;
            server_name  www.test.com;
    
            #charset koi8-r;
    
            #access_log  logs/host.access.log  main;
    		proxy_set_header Host $host;
    		
            #location / {
            #    root   html;
            #    index  index.html index.htm;
            #}
    
            # 拦截动态请求
            location /api {
                proxy_pass http://login-gateway;
            }
    
            #拦截静态请求
            location /static/ {
                root C:/Users/liu.yu/Desktop/html;
            }
    	}
    

3. 项目部署和启动

  1. 部署nginx服务

  2. 由于使用到了springcloud config bus 需要部署rabbitmq

  3. 启动服务需要按照顺序启动

    1. 启动 eureka server
    2. 启动config server
    3. 启动 gateway
    4. 启动 email、code、user服务
  4. 最终的项目结构是如下这个样子的
    在这里插入图片描述
    在这里插入图片描述

4. 具体的演示和使用

  1. 登录页面
    在这里插入图片描述在这里插入图片描述
  2. 注册页面
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  3. 验证码过期:手动将验证码的过期时间调整为已过期
    在这里插入图片描述
  4. 验证码错误:
    在这里插入图片描述
  5. 用户已注册:
    在这里插入图片描述
  6. Ip防爆刷:由于当前在configServer中配置的是1分钟内最多允许注册15次,一旦超过就会在网关gateWay进行请求拦截
    在这里插入图片描述
  7. 欢迎页面: 登录成功显示登录邮箱和token
    在这里插入图片描述
  8. 演示视频

https://gitee.com/struggle_ly/lagou-cloud-first-generation/blob/master/%E6%A8%A1%E5%9D%9710springCloud%E7%AC%AC%E4%B8%80%E4%BB%A3%E7%BB%84%E4%BB%B6.mp4

5. 踩坑

  1. 配置nginx时,指定的server_name为www.test.com域名,在使用chrome进行测试的时候打不开http://www.test.com/static/login.html 静态资源页面

    解决: 尝试使用postman或者更换浏览器进行尝试,或者重启nginx,其次就是配置的nginx路径是否能找到,/static/对应的路径为root C:/Users/liu.yu/Desktop/html, 其进行查找的时候会去C:/Users/liu.yu/Desktop/html/static/login.html对应的路径进行查找,验证路径是否正确,

    最后还有一点,如果本机使用了代理/VPN,也会导致nginx不能正常访问

  2. 登录成功token写入cookie失败

    解决:首先在nginx中添加proxy_set_header Host $host; 允许nginx在转发的时候携带cookie等信息,其次是在后端进行添加cookie的时候设置对应cookie的path为/

    Cookie cookie = new Cookie("token", token);
    cookie.setPath("/");
    cookie.setMaxAge(36000);
    response.addCookie(cookie);
    
  3. springcloud config对应的client无法正常获取server端的配置信息

    解决:client端的配置文件中server的uri不能设置eureka server的名称,需要设置对应的ip+port, 对应的使用配置的类上添加@RefreshScope注解

6. 程序下载地址

项目包下载点我

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 13
    评论
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

代码拯救不了世界

心情好的话,可以打赏一下

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值