java项目开发框架使用

📚SpringBoot

🍎SpringBoot中对MVC的支持

Spring Boot 的 MVC 支持主要来介绍实际项目中最常用的几个注解,包括 @RestController 、 @RequestMapping 、 @PathVariable 、 @RequestParam 以及 @RequestBody 。

🍂@RequestMapping

@RequestMapping 是一个用来处理请求地址映射的注解,它可以用于类上,也可以用于方法上。在类 的级别上的注解会将一个特定请求或者请求模式映射到一个控制器之上,表示类中的所有响应请求的方 法都是以该地址作为父路径;在方法的级别表示进一步指定到处理方法的映射关系。
该注解有6个属性,较常用的有value、method 和 produces

  • value 属性:指定请求的实际地址,value 可以省略不写
  • method 属性:指定请求的类型,主要有 GET、PUT、POST、DELETE,默认为 GET
  • produces属性:指定返回内容类型,如 produces = “application/json; charset=UTF-8”
@RestController
@RequestMapping(value = "/test", produces = "application/json; charset=UTF-8")
public class TestController {
     @RequestMapping(value = "/get", method = RequestMethod.GET)
     public String testGet() {
          return "success";
     }
}

启动项目在浏览器中输入localhost:8080/test/get测试一下即可

针对四种不同的请求方式,是有相应注解的,不用每次在 @RequestMapping 注解中加 method 属性 来指定,上面的 GET 方式请求可以直接使用 @GetMapping("/get") 注解,效果一样。相应地,PUT 方式、POST 方式和 DELETE 方式对应的注解分别为 @PutMapping 、 @PostMapping 和 DeleteMapping 。

🍂@PathVariable

@PathVariable 注解主要是用来获取 url 参数,Spring Boot 支持 restfull 风格的 url,比如一个 GET 请求携带一个参数 id 过来,我们将 id 作为参数接收,可以使用 @PathVariable 注解。如下:

@GetMapping("/user/{id}")
public String testPathVariable(@PathVariable Integer id) {
      System.out.println("获取到的id为:" + id);
      return "success";
}

这里需要注意一个问题,如果想要 url 中占位符中的 id 值直接赋值到参数 id 中,需要保证 url 中的参 数和方法接收参数一致,否则就无法接收。

如果不一致的话,其实也可以解决,需要用 @PathVariable 中的 value 属性来指定对应关系。如下:

@RequestMapping("/user/{idd}/{name}")
public String testPathVariable(@PathVariable(value = "idd") Integer id,
                               @PathVariable String nam) {
       System.out.println("获取到的id为:" + id);
       System.out.println("获取到的name为:" + name);
       return "success";
}

运行项目,在浏览器中请求 localhost:8080/test/user/2/zhangsan 可以看到控制台输出如下信息

获取到的id为:2
获取到的name为:zhangsan

🍂@RequestParam

@RequestParam 注解顾名思义,也是获取请求参数的,上面我们介绍了 @PathValiable 注解也是获 取请求参数的,那么 @RequestParam 和 @PathVariable 有什么不同呢?

主要区别在于: @PathValiable 是从 url 模板中获取参数值, 即这种风格的 url: http://localhost:8080/user/{id} ;而 @RequestParam 是从 request 里面获取参数值,即这种 风格的 url: http://localhost:8080/user?id=1 。我们使用该 url 带上参数 id 来测试一下如下代 码:

@GetMapping("/user")
public String testRequestParam(@RequestParam Integer id) {
     System.out.println("获取到的id为:" + id);
     return "success";
}

同样地,url 上面的参数和方法的参数需要一致,如果不一致,也需 要使用 value 属性来说明,比如 url 为: http://localhost:8080/user?idd=1

@RequestMapping("/user")
public String testRequestParam(@RequestParam(value = "idd", required = false)
Integer id) {
     System.out.println("获取到的id为:" + id);
     return "success";
}

除了 value 属性外,还有个两个属性比较常用:

  • required 属性:true 表示该参数必须要传,否则就会报 404 错误,false 表示可有可无。
  • defaultValue 属性:默认值,表示如果请求中没有同名参数时的默认值。

从 url 中可以看出, @RequestParam 注解用于 GET 请求上时,接收拼接在 url 中的参数。除此之外, 该注解还可以用于 POST 请求,接收前端表单提交的参数,假如前端通过表单提交 username 和 password 两个参数,那我们可以使用 @RequestParam 来接收,用法和上面一样。

@PostMapping("/form1")
public String testForm(@RequestParam String username, 
                       @RequestParam String password){
        System.out.println("获取到的username为:" + username);
        System.out.println("获取到的password为:" + password);
        return "success";
}

我们使用 postman 来模拟一下表单提交,测试一下接口:
在这里插入图片描述
那么问题来了,如果表单数据很多,我们不可能在后台方法中写上很多参数,每个参数还要 @RequestParam 注解。
针对这种情况,我们需要封装一个实体类来接收这些参数,实体中的属性名和 表单中的参数名一致即可。

public class User {
    private String username;
    private String password;
    // set get
}

使用实体接收的话,我们不能在前面加 @RequestParam 注解了直接使用即可。

@PostMapping("/form2")
public String testForm(User user) {
        System.out.println("获取到的username为:" + user.getUsername());
        System.out.println("获取到的password为:" + user.getPassword());
        return "success";
}

🍂@RequestBody

@RequestBody 注解用于接收前端传来的实体,接收参数也是对应的实体

比如前端通过 json 提交传 来两个参数 username 和 password,此时我们需要在后端封装一个实体来接收。在传递的参数比较多 的情况下,使用 @RequestBody 接收会非常方便。

@PostMapping("/user")
public String testRequestBody(@RequestBody User user) {
         System.out.println("获取到的username为:" + user.getUsername());
         System.out.println("获取到的password为:" + user.getPassword());
         return "success";
   
}

请添加图片描述
可以看出, @RequestBody 注解用于 POST 请求上,接收 json 实体参数。它和上面我们介绍的表单提 交有点类似,只不过参数的格式不同,一个是 json 实体,一个是表单提交。

🍎Spring Boot中的项目属性配置

我们知道,在项目中,很多时候需要用到一些配置的信息,这些信息可能在测试环境和生产环境下会有不同的配置,后面根据实际业务情况有可能还会做修改,针对这种情况,我们不能将这些配置在代码中写死,最好就是写到配置文件中。比如可以把这些信息写到 application.yml 文件中。

🍂 读取application配置文件的值

比如我们在application.yml文件中定义自己想用的值

server:
    port: 8001

user.name:张三

然后在业务代码中如何获取到这个配置的订单服务地址呢?我们可以使用 @Value 注解来解决。

@Value("${user.name}")
private String name;

我们再定义一个复杂一点的

user:
    name: 张三
    age: 20
    gender:

也许实际业务中,远远不止这三个字段配置
对于这种情况,我们可以先定义一个类来专门保存字段的值,如下:


/*
@ConfigurationProperties作用:
将配置文件中配置的每一个属性的值,映射到这个组件中;
告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定
参数 prefix = “person” : 将配置文件中的person下面的所有属性一一对应
*/
@Component //注册bean
@ConfigurationProperties(prefix = "user")
public class Person {
   private String name;
   private Integer age;
   private String gender;
   //get set
   }

细心的朋友应该可以看到,使用 @ConfigurationProperties 注解并且使用 prefix 来指定一个前缀,然后该类中的属性名就是配置中去掉前缀后的名字,一一对应即可
该类上面需要加上 @Component 注解,把该类作为组件放到Spring容器中,让Spring 去管理,我们使用的时候直接注入即可。
需要注意的是,使用 @ConfigurationProperties 注解需要导入它的依赖:

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-configuration-processor</artifactId>
      <optional>true</optional>
</dependency>

🍂 读取自定义配置文件的值

以项目当中需要在properties文件中配置统一返回的code和message为例
首先我们需要在resources目录下创建一个result.properties文件

code=200
message=success

然后在我们的代码中指定加载person.properties文件

@Component
@PropertySource(value="classpath:result.properties")
public  class CodeMessage{
     
     public  static Integer code;
     public  static String message;
     
     //由于@value获取的值不能直接赋值给static变量,所以我们需要如下转换
    @Value("${code}")
    public void setCode(Integer code) {
        ResultCode.code = code;
    }
    @Value("${message}")
    public void setMessage(String message) {
        ResultCode.message = message;
    }
    

}

接下来我们创建统一返回类

public class Result {


    private Boolean success;


    private Integer code;


    private String message;

    private Map<String, Object> data = new HashMap<String, Object>();

   private Result(){

   }

   public static Result ok(){
       Result r=new Result();
       r.setCode(CodeMessage.code);
       r.setMessage(CodeMessage.message);
       return r;
   }
    public Result data(String key, Object value){
        this.data.put(key, value);
        return this;
    }

    public Result data(Map<String, Object> map){
        this.setData(map);
        return this;
    }

    public Boolean getSuccess() {
        return success;
    }

    public void setSuccess(Boolean success) {
        this.success = success;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public Map<String, Object> getData() {
        return data;
    }

    public void setData(Map<String, Object> data) {
        this.data = data;
    }
}

此时,我们可以编写一个controller测试是否可以拿到自定义配置类中的属性值,并且是否正常返回

@RestController
public class HelloController {
    @GetMapping("hello")
    public Result hello(){

        List<Object> list = Arrays.asList("Java编程","Springboot项目","读取自定义配置文件");
        return Result.ok().data("list",list);
    }

}

效果如下
在这里插入图片描述
注意
properties配置文件在写中文的时候,会有乱码 , 我们需要去IDEA中设置编码格式为UTF-8;
在这里插入图片描述

通过自定义环境处理类,实现配置文件的加载

public class MyEnvironmentPostProcessor implements EnvironmentPostProcessor {


    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        //自定义配置文件
        String[] profiles = {
                "test.properties",
                "bussiness.properties",
                "blog.yml"
        };

        //循环添加
        for (String profile : profiles) {
            //从classpath路径下面查找文件
            Resource resource = new ClassPathResource(profile);
            //加载成PropertySource对象,并添加到Environment环境中
            environment.getPropertySources().addLast(loadProfiles(resource));
        }
    }

    //加载单个配置文件
    private PropertySource<?> loadProfiles(Resource resource) {
        if (!resource.exists()) {
            throw new IllegalArgumentException("资源" + resource + "不存在");
        }
        if(resource.getFilename().contains(".yml")){
            return loadYaml(resource);
        } else {
            return loadProperty(resource);
        }
    }

    /**
     * 加载properties格式的配置文件
     * @param resource
     * @return
     */
    private PropertySource loadProperty(Resource resource){
        try {
            //从输入流中加载一个Properties对象
            Properties properties = new Properties();
            properties.load(resource.getInputStream());
            return new PropertiesPropertySource(resource.getFilename(), properties);
        }catch (Exception ex) {
            throw new IllegalStateException("加载配置文件失败" + resource, ex);
        }
    }

    /**
     * 加载yml格式的配置文件
     * @param resource
     * @return
     */
    private PropertySource loadYaml(Resource resource){
        try {
            YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
            factory.setResources(resource);
            //从输入流中加载一个Properties对象
            Properties properties = factory.getObject();
            return new PropertiesPropertySource(resource.getFilename(), properties);
        }catch (Exception ex) {
            throw new IllegalStateException("加载配置文件失败" + resource, ex);
        }
    }
}

接着,在resources资源目录下,我们还需要创建一个文件META-INF/spring.factories,通过spi方式,将自定义环境处理类加载到Spring处理器里面,当项目启动时,会自动调用这个类!

#启用我们的自定义环境处理类
org.springframework.boot.env.EnvironmentPostProcessor=com.example.property.env.MyEnvironmentPostProcessor

这种自定义环境处理类方式,相对会更佳灵活,首先编写一个通用的配置文件解析类,支持properties和yml文件的读取,然后将其注入到Spring容器里面,基本上可以做到一劳永逸!

🍎​SpringBoot中的事务

事务的四个特性:

  • 原子性(Atomic)

    表示组成一个事务的多个数据库操作是一个不可分割的原子单元,只有所有操作成功,整个事务才提交,若事务中任何一个操作失败,已经执行的所有操作都必须撤销,让数据恢复到最初的状态。

  • 一致性(Consistency)

    整个事务操作成功之后,数据库所处的状态和业务的规则是抑制的,即数据不会被破坏。比如A转账给B的案例中,无论成功与否,A和B的总金额是不会改变的。

  • 隔离性(Isolation)

    在并发数据库操作时,不同的事务拥有各自的数据空间,他们的操作要做到彼此之间相互不产生干扰,而数据库的不同隔离级别对应着不同的干扰程度,隔离级别越高,数据的一致性越高,但是并发性越弱。

  • 持久性(Durabiliy)

    一旦事务提交成功之后,事务中所有的数据操作都必须被持久化到数据库中,及时提交事务之后,数据库马上回滚,在数据库重启时,页必须保证能通过某种机制恢复数据。

要想搞清楚SpringBoot中的事务怎么处理,我们先要知道Spring中我们是怎么处理的

🍂 Spring对事务的支持

Spring的事务管理主要包括3个接口:

  • TransactionDefinition:封装事务的超时时间,是否为只读事务和事务的隔离级别和传播规则等事务属性,可通过XML配置具体信息。
  • PlatformTransactionManager:根据TransactionDefinition提供的事务属性配置信息,创建事务。
  • TransactionStatus:封装了事务的具体运行状态。比如,是否是新开启事务,是否已经提交事务,设置当前事务为rollback-only等。

Spring支持两种事务管理方式

  • 编程式事务管理:事务和业务代码耦合度太高。上图就是编程式事务
  • 声明式事务管理:侵入性小,把事务从业务代码中抽离出来到配置文件中,提供维护性。
🍑 编程式

1.PlatformTransactionManager:接口统一抽象处理事务操作相关的方法;

  • TransactionStatus getTransaction(TransactionDefinition definition):

    根据事务定义信息从事务环境中返回一个已存在的事务,或者创建一个新的事务,并用TransactionStatus描述该事务的状态。

  • void commit(TransactionStatus status):

    根据事务的状态提交事务,如果事务状态已经标识为rollback-only,该方法执行回滚事务的操作

  • void rollback(TransactionStatus status):

    将事务回滚,当commit方法抛出异常时,rollback会被隐式调用

2.在使用spring管理事务的时候,首先得告诉spring使用哪一个事务管理器

3 常用的事务管理器:

    DataSourceTransactionManager:使用JDBC,MyBatis的事务管理器;

    HibernateTransactionManager  :使用Hibernate的事务管理器

请添加图片描述

🍑​声名式: 以Mybatis为例

请添加图片描述

🍌​事务方法的属性细节

即tx:method中的属性

属性必须默认值描述
name事务要管理的方法,可使用通配符*
propagation×REQUIRED事务传播规则
isolation×DEFAULT事务隔离级别
read-only×false事务是否只读,查询操作设置为只读事务
timeout×-1事务超时时间(秒),若为-1,则由底层事务系统决定
rollback-for×运行期异常需要回滚的异常类型,多个使用逗号隔开
no-rollvack-for×检查类型异常不需要回滚的异常

1,name:匹配到的方法模式name=”save”就表示save方法,name=”save*”,所以以save作为方法名前缀的方法.s;

2,read-only:如果为true,开启一个只读事务,只读事务的性能较高,但是不能在只读事务中,使用DML;

3,isolation:代表数据库事务隔离级别(就使用默认)

DEFAULT:让spring使用数据库默认的事务隔离级别;其他:spring模拟;

4,no-rollback-for: 如果遇到的异常是匹配的异常类型,就不回滚事务;

5,rollback-for:如果遇到的异常是指定匹配的异常类型,才回滚事务;spring默认情况下,spring只会去回滚RuntimeException及其子类,不会回滚Exception和Thowable

6,propagation:事务的传播方式(当一个方法已经在一个开启的事务当中了,应该怎么处理自身的事务)

1,REQUIRED(默认的传播属性):如果当前方法运行在一个没有事务环境的情况下,则开启一个新的事务,如果当前方法运行在一个已经开启了的事务里面,把自己加入到开启的那个事务中

2,REQUIRES_NEW:不管当前方法是否运行在一个事务空间之内,都要开启自己的事务

🍌事务传播规则

Spring在TransactionDefinition接口中定义了七种事务传播规则,规定了事务方法和事务方法发生嵌套调用时事务该如何进行传播

事务传播规则类型描述
PROPAGATION_REQUIRED若当前存在事务,则加入该事务,若不存在事务,则新建一个事务。
PROPAGATION_SUPPORTS支持当前事务,若当前不存在事务,以非事务的方式执行。
PROPAGATION_MANDATORY强制事务执行,若当前不存在事务,则抛出异常
PAOPAGATION_REQUIRE_NEW若当前没有事务,则新建一个事务。若当前存在事务,则新建一个事务,新老事务相互独立。
PROPAGATION_NOT_SUPPORTED以非事务的方式执行,若当前存在事务,则把当前事务挂起。
PROPAGATION_NEVER以非事务的方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED如果当前存在事务,则嵌套在当前事务中执行。如果当前没有事务,则新建一个事务,类似于REQUIRE_NEW。

PROPAGATION_REQUIRED
若当前存在事务,则加入该事务,若不存在事务,则新建一个事务。

class C1(){
    @Transactional(propagation = Propagation.REQUIRED)
    function A(){
        C2.B();
    }
}
 
class C2(){
    @Transactional(propagation = Propagation.REQUIRED)
    function B(){
        do something;
    }
}

若B方法抛出异常,A方法进行捕获,A会抛出异常,因为C2标志回滚,C1标志提交,产生冲突。
若B方法抛出异常,B方法内部捕获,A、B都不会回滚。
若A或B抛出异常,但没有捕获,则A、B都回滚。
A、B可操作同一条记录,因为处于同一个事务中。
PAOPAGATION_REQUIRE_NEW

class C1(){
   @Transactional(propagation = Propagation.REQUIRED)
   function A(){
       C2.B();
   }
}
class C2(){
   @Transactional(propagation = Propagation.REQUIRE_NEW)
   function B(){
       do something;
   }
}

若B方法抛出异常,A方法进行捕获,B方法回滚,A方法不受B异常影响。
若B方法抛出异常,B方法内部捕获,A、B都不会回滚。
若A方法抛出异常,不会影响B正常执行。
若B方法抛出异常,A、B方法都没有处理,则A、B都会回滚。
A、B不可操作同一条记录,因为处于不同事务中,会产生死锁。
PROPAGATION_NESTED

class C1(){
    @Transactional(propagation = Propagation.REQUIRED)
    function A(){
        C2.B();
    }
}
class C2(){
    @Transactional(propagation = Propagation.NESTED)
    function B(){
        do something;
    }
}

若B方法抛出异常,A方法进行捕获,B方法回滚,A方法正常执行。
若A或者B抛出异常,不做任何处理的话,A、B都要回滚。
A、B可操作同一条记录,因为处于同一个事务中。
PROPAGATION_NOT_SUPPORTED

class C1(){
    @Transactional(propagation = Propagation.REQUIRED)
    function A(){
        C2.B();
    }
}
class C2(){
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    function B(){
        do something;
    }
}

A、B不可操作同一条记录,因为A是事务执行,B在A尚未提交前再操作同一条记录,会产生死锁。

🍌事务隔离级别
隔离级别描述
Isolation.DefaultSpring:默认隔离级别,即采用数据库的隔离级别
Isolation.Read_Uncommited事务未提交可读,会出现脏读。
Isolation.Read_Commited不可脏读,但会出现幻读和不可重复读。
Isolation.Repeatable_Read不可脏读,不可重复读,但会出现幻读。 innodb默认隔离级别
Isolation.Searializable事务的最高隔离级别,所有事务串行执行。

脏读:

A事务读取到B事务尚未提交的更改数据,并在这个数据的基础上操作,如果恰巧B事务回滚,那么A事务读到的数据是不被承认的

时间取款事务A转账事务B
T1开始事务
T2开始事务
T3查询账户余额为1000元
T4取出1000元,修改余额为0元
T5查询账户余额为0元(脏读)
T6撤销事务,余额恢复为1000元.
T7存入500元,把余额修改为500元
T8提交事务

幻读: A事务读取B事务提交的新增数据,这时A事务将出现幻读问题

时间取款事务A转账事务B
T1开始事务
T2开始事务
T3查询存款为10000元
取出10000元,修改余额为0元
T4账户存款500元
T5提交事务
T6再次查看发现金额为500元(幻读)

不可重复读:

时间取款事务A转账事务B
T1开始事务
T2开始事务
T3查询账户余额为1000元
T4查询账户余额为1000元
T5取出1000元,修改余额为0元
T6提交事务
T7查询账户余额为0元(和T4不一致)

不同的隔离级别可能会导致不同的数据库并发问题,隔离级别越高,性能越低.

🍌 @Transactional 注解

在关键类中添加 注解 **@EnableTransactionManagement,**开启事务支持

如:主启动类,mybatis的配置类

该注解的属性和Spring 在配置文件中配置的属性是一模一样的

🍌 SpringBoot中使用 @Transactional 注解注意事项

Spring Boot 中使用事务非常简单, @Transactional 注解即可解决问题

在实际项目中,是有很多小坑在等着我们,这些小坑是我们在写代码的时候没有注意 到,而且正常情况下不容易发现这些小坑,等项目写大了,某一天突然出问题了,排查问题非常困难, 到时候肯定是抓瞎,需要费很大的精力去排查问题。

🍐异常没有捕获到

首先要说的,就是异常并没有被 ”捕获“ 到,导致事务并没有回滚。我们在业务层代码中,也许已经考虑 到了异常的存在,或者编辑器已经提示我们需要抛出异常,但是这里面有个需要注意的地方:并不是说 我们把异常抛出来了,有异常了事务就会回滚,我们来看一个例子:

@Service
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
    
@Override
@Transactional
public void isertUser(User user) throws Exception {
    // 插入用户信息
    userMapper.insertUser(user);
    // 手动抛出异常
    throw new SQLException("数据库异常");
  }
}

我们看上面这个代码,其实并没有什么问题,手动抛出一个 SQLException 来模拟实际中操作数据库 发生的异常,在这个方法中,既然抛出了异常,那么事务应该回滚,实际却不如此,读者可以使用我源 码中 controller 的接口,通过 postman 测试一下,就会发现,仍然是可以插入一条用户数据的。
那么问题出在哪呢?因为 Spring Boot 默认的事务规则是遇到运行异常(RuntimeException)和程序 错误(Error)才会回滚。比如上面我们的例子中抛出的 RuntimeException 就没有问题,但是抛出 SQLException 就无法回滚了。针对非运行时异常,如果要进行事务回滚的话,可以在 @Transactional 注解中使用 rollbackFor 属性来指定异常,比如 @Transactional(rollbackFor = Exception.class) ,这样就没有问题了,所以在实际项目中,一定要指定异常。

🍐异常被吞掉

我们在处理异常时,有两种方式, 要么抛出去,让上一层来捕获处理;要么把异常 try catch 掉,在异常出现的地方给处理掉。就因为有 这中 try…catch,所以导致异常被吞掉,事务无法回滚。

@Service
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public void isertUser3(User user) {
   try {
        // 插入用户信息
        userMapper.insertUser(user);
        // 手动抛出异常
       throw new SQLException("数据库异常");
    } catch (Exception e) {
      // 异常处理逻辑
  }
 }
}

这种情况下要么我们将异常再往上一层抛出,或者在catch中加入如下代码,手动回滚事务

 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();```
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值