从0搭建SpringCloud集群与OAuth2.1前后分离框架(2/12)

一、摘要

        实战文章,主要分为两部分。 一是搭建SpringCloud微服务与集群;二是基于OAuth2.1(Spring Authorization Server)搭建前后分离安全框架。

        后端主要采用的技术栈:SpringBoot3.0,Security6.0,OAuth2.1。

        前端主要采用的技术栈:Vue3.0+Vite,ElementPlus。

二、环境

        系统:MAC Apple M2 13.3

        开发工具:IntelliJ IDEA 2022.3.3 (Ultimate Edition)

        JDK版本:OpenJDK 17.0.8

三、文章内容

        本章内容,搭建用户信息服(cloud-user)、测试服(cloud-test)、公共服(cloud-common),并实现负载均衡。

5、父工程依赖

本章中,父工程相对于上一章,需要添加的依赖如下:

  • Mysql驱动
  • MybatisPlus(Mybatis加强版)
<properties>
		<java.version>17</java.version>
        ……
		<!-- mysql驱动版本 -->
		<mysql-j.version>8.0.33</mysql-j.version>
		<!-- mybatisPlus版本 -->
		<mybatis-plus.version>3.5.3.1</mybatis-plus.version>
	</properties>

<dependencyManagement>
		<dependencies>
		    ……
			<!--  mysql驱动  -->
			<dependency>
				<groupId>com.mysql</groupId>
				<artifactId>mysql-connector-j</artifactId>
				<version>${mysql-j.version}</version>
			</dependency>
			<!--  MybatisPlus  -->
			<dependency>
				<groupId>com.baomidou</groupId>
				<artifactId>mybatis-plus-boot-starter</artifactId>
				<version>${mybatis-plus.version}</version>
			</dependency>
			<dependency>
				<groupId>com.baomidou</groupId>
				<artifactId>mybatis-plus</artifactId>
				<version>${mybatis-plus.version}</version>
			</dependency>
            ……
		</dependencies>
	</dependencyManagement>

完整的依赖配置为:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.0.8</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<groupId>com.example</groupId>
	<artifactId>cloud</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>pom</packaging>

	<name>cloud</name>
	<description>cloud</description>
	<modules>
		<module>cloud-test</module>
		<module>cloud-user</module>
		<module>cloud-common</module>
		<module>cloud-eureka</module>
	</modules>

	<properties>
		<java.version>17</java.version>
		<!-- snakeyaml版本 -->
		<snakeyaml.version>2.0</snakeyaml.version>
		<!-- lombok注解版本 -->
		<lombok.version>1.18.28</lombok.version>
		<!-- SpringCloud版本 -->
		<spring-cloud.version>2022.0.1</spring-cloud.version>
		<!-- mysql驱动版本 -->
		<mysql-j.version>8.0.33</mysql-j.version>
		<!-- mybatisPlus版本 -->
		<mybatis-plus.version>3.5.3.1</mybatis-plus.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>

		<!--  指定snakeyaml,修复1.33版本漏洞  -->
		<dependency>
			<groupId>org.yaml</groupId>
			<artifactId>snakeyaml</artifactId>
			<version>${snakeyaml.version}</version>
		</dependency>

		<!--  lombok注解开发  -->
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>${lombok.version}</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<dependencyManagement>
		<dependencies>
			<!--  spring-cloud  -->
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring-cloud.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
			<!--  mysql驱动  -->
			<dependency>
				<groupId>com.mysql</groupId>
				<artifactId>mysql-connector-j</artifactId>
				<version>${mysql-j.version}</version>
			</dependency>
			<!--  MybatisPlus  -->
			<dependency>
				<groupId>com.baomidou</groupId>
				<artifactId>mybatis-plus-boot-starter</artifactId>
				<version>${mybatis-plus.version}</version>
			</dependency>
			<dependency>
				<groupId>com.baomidou</groupId>
				<artifactId>mybatis-plus</artifactId>
				<version>${mybatis-plus.version}</version>
			</dependency>
		</dependencies>
	</dependencyManagement>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

6、搭建用户信息服

(1)添加依赖

添加用户信息服需要的依赖:

  • SpringWeb依赖
  • Eureka客户端
  • JDBC数据库驱动
  • mysql驱动
  • MybatisPlus依赖
<?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>
    <parent>
        <groupId>com.example</groupId>
        <artifactId>cloud</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>cloud-user</artifactId>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!--  SpringWeb依赖  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--  Eureka客户端依赖  -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <!--  JDBC驱动  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <!--  mysql驱动  -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>

        <!--  MybatisPlus  -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus</artifactId>
        </dependency>
    </dependencies>

</project>

(2)配置参数

在src.main.resources包中,创建application.yml配置文件(其中注释部分可根据需求自行调整)

主要设置:

  • service.port,分配端口。
  • spring.application.name,修改服务名称,后续方便在注册中心查看。
  • spring.application.datasource,mysql参数配置,本服务使用的数据库(不懂可参考第7节)
  • eureka.client.service-url.defaultZone,填写两个Eureka服务地址
  • mybatis-plus,mybatis-plus的配置,具体注释上面说明。

server:
  port: 8001

spring:
  application:
    name: CLOUD-USER
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/cloud?useUnicode=true&characterEncoding=utf-8
    username: cloud
    password: c123456

eureka:
  client:
    service-url:
      defaultZone: http://eureka01:7101/eureka, http://eureka02:7102/eureka

mybatis-plus:
  global-config:
    db-config:
      # 表名前缀
      table-prefix: sys_
      # 自增ID策略
      id-type: auto

      # 逻辑删除
      # 全局逻辑删除的实体字段名(3.3.0后,可以不配置后面的默认值)
      logic-delete-field: delFlag
      # 逻辑删除值(默认为1)
      logic-delete-value: 1
      # 逻辑未删除值(默认为0)
      logic-not-delete-value: 0

    #configuration:
    # 配置日志输出
    #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  # 指定SQL语句xml的目录
  mapper-locations: classpath:/mybatis-plus/mapper/**/*.xml
  # 项目启动会检查xml配置存在
  check-config-location: true
  # 指定本地额外配置文件(不能与configuration共存)
  #config-location: classpath:/mybatis-plus/mybatis-config.xml
  # 实体类package包加载位置(可以直接用类名)
  type-aliases-package: com.example.domain.entity

(3)启动类修改

在src.main.java.com.example包中,将Main启动类修改为

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class UserApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserApplication.class, args);
    }
}

(4)实体表

在src.main.java.com.example.domain.entity包中,编写一个sys_user表的实体表User。

(其中@mock与@since,为SmartDoc的内容,不影响,可忽略或删除。)

package com.example.domain.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    /**
     * 用户账号ID
     * @mock 1000
     * @since v1.0
     */
    @TableId(type= IdType.AUTO)
    private Long id;

    /**
     * 用户名称(用户账号)
     * @mock user
     * @since v1.0
     */
    private String username;

    /**
     * 用户昵称
     * @mock 赵四
     * @since v1.0
     */
    private String nickname;

    /**
     * 用户密码
     * @mock 123456
     * @since v1.0
     */
    private String password;

    /**
     * 绑定邮箱
     * @mock 123456@qq.com
     * @since v1.0
     */
    private String email;

    /**
     * 账号状态,0激活,1停用
     * @mock 0
     * @since v1.0
     */
    private Byte isActive;

    /**
     * 性别(默认0未知,1男,2女)
     * @mock 0
     * @since v1.0
     */
    private Byte sex;

    /**
     * 用户类型,0普通用户,1超级管理员,2管理员
     * @mock 0
     * @since v1.0
     */
    private Byte userType;

    /**
     * 创建时间
     * @mock 2023-05-05 17:00:11
     * @since v1.0
     */
    @TableField(fill = FieldFill.INSERT)
    private String createTime;

    /**
     * 更新时间
     * @mock 2023-05-05 17:00:11
     * @since v1.0
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private String updateTime;

    /**
     * 登录时间
     * @mock 2023-05-05 17:00:11
     * @since v1.0
     */
    private String loginTime;

    /**
     * 登出时间
     * @mock 2023-05-05 17:00:11
     * @since v1.0
     */
    private String logoutTime;

    /**
     * 上次登录时间
     * @mock 2023-05-05 17:00:11
     * @since v1.0
     */
    private String lastLoginTime;

    /**
     * IP地址(登录)
     * @mock 175.178.15.253
     * @since v1.0
     */
    private String loginIp;

    /**
     * 删除标志,0正常使用,1已删除
     * @mock 0
     * @since v1.0
     */
    private Byte delFlag;
    /**
     * 乐观锁版本
     * @mock 1
     * @since v1.0
     */
    @Version
    private int version;
}

(5)Mapper类

在src.main.java.com.example.mapper包中编写一个Mapper接口类UserMapper,用于获取数据库内容。

其中,这里可以继承BaseMapper方法,可比较方便使用MybatisPlus已编写好的一些方法,也可自行编写xml,或者使用注解类如@Select来编写sql查询代码。

package com.example.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.domain.entity.User;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper extends BaseMapper<User> {

}

(6)控制类

在src.main.java.com.example.controller包中编写一个控制类UserController,用于请求返回用户信息。

这里的接口地址,邀请附带一个username参数,方便后续测试用。

package com.example.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.domain.entity.User;
import com.example.mapper.UserMapper;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class UserController {

    @Resource
    UserMapper userMapper;

    @GetMapping("/user/{username}")
    public User getUser(@PathVariable String username) {
        System.out.println("我被访问了");
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUsername, username);
        User user = userMapper.selectOne(wrapper);
        return user;
    }
}

(7)配置启动工具

与上一篇文章编写Eureka的方式同理,这里同样方式处理,启动两个User服务。

  • UserApplication02启动时,分配8002的端口。(UserApplication01为8001)

7、搭建数据库

(1)安装数据库,可自行下载安装mysql8.0.33,Mac推荐使用brew下载,这里不展开细说。

(2)配置帐号及数据库:

  • 打开终端,使用命令mysql -u root -p,然后输入密码登录mysql。
  • 创建cloud数据库

    create database cloud;

  • 创建cloud用户,密码为c123456,专项专用。

    create user 'cloud'@'%' identified by 'c123456';

  • 分配数据库cloud的权限给用户cloud

    grant all privileges on cloud.* to 'cloud'@'%' with grant option;

  • 最后刷新一下权限,以防止没有生效

    flush privileges;

(2)创建sys_user表

CREATE TABLE `sys_user` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `username` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '' COMMENT '用户名',
  `nickname` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '用户昵称',
  `password` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '' COMMENT '用户密码',
  `email` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '邮箱',
  `phone` varchar(16) DEFAULT NULL COMMENT '手机',
  `is_active` tinyint NOT NULL DEFAULT '0' COMMENT '账号状态(0激活,1停用)',
  `source_from` varchar(16) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '' COMMENT '用户来源',
  `sex` tinyint DEFAULT '0' COMMENT '性别(默认0未知,1男,2女)',
  `user_type` tinyint NOT NULL DEFAULT '0' COMMENT '用户类型(0普通用户,1管理员)',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `login_time` datetime DEFAULT NULL COMMENT '登录时间',
  `logout_time` datetime DEFAULT NULL COMMENT '登出时间',
  `last_login_time` datetime DEFAULT NULL COMMENT '上一次登录时间',
  `login_ip` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT 'IP地址',
  `del_flag` tinyint DEFAULT '0' COMMENT '删除标志(0未删除,1已删除)',
  `version` int DEFAULT '0' COMMENT '乐观锁版本(默认0)',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb3;

(3)创建两条基础数据,如id=1,username=user;id=2,username=test。

8、启动用户信息服

到这里,可以启动UserApplication的两个服务了。

服务启动后,打开浏览器,访问http://127.0.0.1:8001/api/user/user可拿到user用户的数据 

尝试一下访问另一个服务http://127.0.0.1:8002/api/user/test也是可以拿到test用户的数据

另外打开http://eureka01:7101可以发现,两个用户信息服也是已经被Eureka发现并注册。

 9、创建测试服

现在我们来搭建测试服,实现通过集群内部访问用户信息服,并实现负载均衡。

(1)添加依赖

添加测试服需要的依赖:

  • 导入内部用户信息服的模块
  • SpringWeb依赖
  • Eureka客户端
  • openfeign依赖(用于内部调用,类似RestTemplate)
<?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>
    <parent>
        <groupId>com.example</groupId>
        <artifactId>cloud</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>cloud-test</artifactId>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!--	导入公共模块	-->
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>cloud-user</artifactId>
            <version>${project.version}</version>
        </dependency>

        <!--  SpringWeb依赖  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--  Eureka客户端依赖  -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <!--  openfeign依赖(内部服调用)  -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <!--  JDBC驱动  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <!--  mysql驱动  -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>

    </dependencies>

</project>

(2)配置参数

在src.main.resources包中,创建application.yml配置文件(其中注释部分可根据需求自行调整)

主要设置:

  • service.port,分配端口。
  • spring.application.name,修改服务名称,后续方便在注册中心查看。
  • spring.application.datasource,mysql参数配置。
  • spring.cloud.openfeign,openfeign客户端配置。
  • eureka.client.service-url.defaultZone,填写两个Eureka服务地址
server:
  port: 8101

spring:
  application:
    name: CLOUD-TEST
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/cloud?useUnicode=true&characterEncoding=utf-8
    username: cloud
    password: c123456
  cloud:
    openfeign:
      client:
        config:
          default:
            # 连接超时时间
            connectTimeout: 5000
            # 读取超时时间
            readTimeout: 5000

eureka:
  client:
    service-url:
      defaultZone: http://eureka01:7101/eureka, http://eureka02:7102/eureka

(3)启动类修改

在src.main.java.com.example包中,将Main启动类修改为

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
public class TestApplication {
    public static void main(String[] args) {
        SpringApplication.run(TestApplication.class, args);
    }
}

(注意,这里需要添加@EnableFeignClients注解,注册openfeign。)

(4)client类

在src.main.java.com.example.service.client包中编写一个openfeign接口类UserClient,用于内部调用接口。

  • name,填写需要访问的服务名称spring.application.name(集群内)
  • path,可填写统一接口,作用类似@RequestMapping。
  • getUser方法,有点类似Mapper的用法,直接用接口实现访问。这里接口填写需要请求的内部接口,如第6节中用户信息服中提供的接口。
package com.example.service.client;

import com.example.domain.entity.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(name = "CLOUD-USER", path = "/api")
public interface UserClient {
    @GetMapping("/user/{username}")
    User getUser(@PathVariable String username);
}

(这里解释下,为什么这个接口要放在service包下,正常来说,从Controller接收的请求,一般都会转到service服务下处理,这里为了方便,会直接在Controller中实现逻辑。)

(5)控制类

在src.main.java.com.example.controller包中编写一个控制类TestController,主要实现通过测试服,访问用户信息服,并实现负载均衡。

package com.example.controller;

import com.example.domain.entity.User;
import com.example.service.client.UserClient;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
public class TestController {
    
    @Resource
    UserClient userClient;
    
    @GetMapping("/info/{username}")
    public String testOne(@PathVariable String username) {
        User user = userClient.getUser(username);
        return user.toString();
    }

    @GetMapping("/my")
    public String testOne() {
        return "访问成功";
    }
}

最后同理,可以实现下启动配置,再分配一个端口出来,实现两个测试服。

写到这里,已经实现通过测试服,可以访问内部接口,并且实现负载均衡(默认为轮训策略)。

启动测试服后,请求测试服自己内容的接口http://127.0.0.1:8101/test/my,访问成功。

接着访问用户信息的接口http://127.0.0.1:8101/test/info/user,也是没有问题的。

另外,我们可以修改用户信息服的UserController,添加一些日志

package com.example.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.domain.entity.User;
import com.example.mapper.UserMapper;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class UserController {

    @Resource
    UserMapper userMapper;

    @Value("${spring.application.name}")
    String serviceName;

    @Value("${server.port}")
    String serverPort;

    @GetMapping("/user/{username}")
    public User getUser(@PathVariable String username) {
        System.out.println(serviceName + ":" + serverPort + ",被访问了");
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUsername, username);
        User user = userMapper.selectOne(wrapper);
        return user;
    }
}

然后多次访问接口http://127.0.0.1:8101/test/info/user,发现是实现了负载均衡。

 四、后续

 下一章:编写通用服,并实现统一响应类。 基于OAuth2.1,实现安全框架。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值