spring框架理解


一.题目

1:PO、DTO、VO、BO与使用场景

为什么有了PO,还要添加一个DTO?
举个例子,一张数据表中有100列,则对应的PO同样有100个属性。但是,有一个服务调用只需要其中的10个属性,此时我们可以创建一个只有10个属性的DTO返回给调用者。同时也避免了数据表结构给客户端,使用数据表结构和调用结果解偶。

当使用持久对象(PO)时,有时候会发现直接将 PO 传递给服务调用者并不是最佳选择。这时引入数据传输对象(DTO)可以带来一些好处,让我来详细解释一下。

为什么使用 DTO?
数据精简:正如你所提到的,有些服务调用只需要部分属性,而不需要整个持久对象。使用 DTO 可以只选择需要的属性,从而减少数据传输量和提升效率。
解耦合:PO 反映了数据库表的结构和业务对象的状态,而 DTO 则更贴近服务调用的需求。通过引入 DTO,可以避免将数据库表结构直接暴露给客户端,也可以更灵活地控制返回给客户端的数据。
安全性:通过 DTO,可以控制向客户端暴露的信息,避免过多敏感数据泄露。

通过引入 DTO,可以解耦系统的不同层次或模块,具体来说主要体现在以下几个方面:

  1. 解耦数据库模型和业务逻辑
    持久对象(PO) 通常直接映射数据库表结构,它包含了数据库中的所有字段,反映了数据库的设计。这些 PO 对象在应用程序中用于数据库操作,如增删改查。

数据传输对象(DTO) 则是专门为数据传输设计的对象,它只包含服务调用者需要的字段。DTO 与 PO 的解耦可以避免将数据库表的内部结构暴露给外部系统或客户端。

示例
假设有一个 User 数据表和相应的 UserPO 对象:

java
public class UserPO {
private int id;
private String username;
private String password;
private String email;
private String address;
// 其他属性…

// getters and setters

}
如果某个服务调用只需要用户名和电子邮件,而不需要其他属性,可以创建一个 UserDTO:

java
public class UserDTO {
private String username;
private String email;

// getters and setters

}
通过这种方式,服务调用者只会接触到 UserDTO,而不会知道 UserPO 的全部细节,从而实现了解耦。

  1. 解耦不同层次之间的依赖
    在分层架构中(如控制层、服务层、数据访问层),每一层都有其特定的职责。DTO 在各层之间传递数据,减少了各层之间的直接依赖。

示例
假设有一个用户管理系统,包括以下层次:

控制层(Controller):处理 HTTP 请求。
服务层(Service):包含业务逻辑。
数据访问层(DAO):负责与数据库交互。
在控制层中,我们可以使用 DTO 来接收和返回数据,而不直接依赖持久对象:

java
@RestController
public class UserController {
@Autowired
private UserService userService;

@GetMapping("/user/{id}")
public ResponseEntity<UserDTO> getUser(@PathVariable int id) {
    UserDTO userDTO = userService.getUserById(id);
    return ResponseEntity.ok(userDTO);
}

}
在服务层中,可以将持久对象转换为 DTO 以便传递给控制层:

java
@Service
public class UserService {
@Autowired
private UserRepository userRepository;

public UserDTO getUserById(int id) {
    UserPO userPO = userRepository.findById(id).orElseThrow();
    return convertToDTO(userPO);
}

private UserDTO convertToDTO(UserPO userPO) {
    UserDTO userDTO = new UserDTO();
    userDTO.setUsername(userPO.getUsername());
    userDTO.setEmail(userPO.getEmail());
    return userDTO;
}

}
这样,控制层只需要关心 DTO,而不需要了解持久对象的细节,从而实现了各层之间的解耦。

  1. 提高灵活性和可维护性
    通过引入 DTO,可以更灵活地调整不同层次之间的数据传输格式,而不需要修改底层数据库结构。例如,如果某个 API 的需求发生变化,只需修改相应的 DTO 和转换逻辑,而无需改变数据库表结构。

示例
假设服务调用现在需要增加一个新的字段 nickname,只需修改 UserDTO 和相应的转换逻辑:

java
public class UserDTO {
private String username;
private String email;
private String nickname; // 新增字段

// getters and setters

}

@Service
public class UserService {
@Autowired
private UserRepository userRepository;

public UserDTO getUserById(int id) {
    UserPO userPO = userRepository.findById(id).orElseThrow();
    return convertToDTO(userPO);
}

private UserDTO convertToDTO(UserPO userPO) {
    UserDTO userDTO = new UserDTO();
    userDTO.setUsername(userPO.getUsername());
    userDTO.setEmail(userPO.getEmail());
    userDTO.setNickname(userPO.getNickname()); // 设置新字段
    return userDTO;
}

}
通过这种方式,DTO 的引入使得系统更加灵活易维护,同时保证了各层之间的解耦。

总结
通过使用 DTO,可以有效地解耦数据库模型和业务逻辑、解耦不同层次之间的依赖,并提高系统的灵活性和可维护性。这种解耦不仅有助于保护数据的安全性,还能使代码更容易维护和扩展。

2 使用的是service层的接口

在这里插入图片描述

编写一个接口及其实现类的时候,遇到一个现象,一个后台管理里面的查询功能,在controller层对接口的方法进行实现的时候,我采用的是接口的引用对象来实现。为什么不采用实现类创建引用对象来调用方法实现呢?
按照正常的逻辑,接口里面只定义了方法名称。主要的实现还是在继承接口的实现类里面,而我看了别人的代码,他们也大部分都是创建接口对象来实现,重写的方法体是在实现类里面,接口对象为什么就可以使用。
例如:

List list = new ArrayList();
//List是接口,ArrayList是它的实现类

原因:这就是JAVA多态的特点。父类引用指向子类对象,父类(接口)的引用就能够直接调用子类(实现类)的方法。

3.JWT

admin-secret-key中的内容和载荷一起被某种算法转换为jwt,理解的对嘛

是的,你的理解基本正确。JWT(JSON Web Token)的生成过程确实涉及将载荷(Payload)和秘钥(如admin-secret-key)结合起来,通过某种算法进行签名。具体流程如下:

  1. 准备头部(Header):头部通常包含两个部分,令牌的类型(JWT)和使用的签名算法(例如,HS256)。这部分内容会被Base64 URL编码。

  2. 准备载荷(Payload):载荷包含实际需要传输的数据,例如用户信息、权限等。该部分也会被Base64 URL编码。

  3. 生成签名(Signature):签名是确保JWT未被篡改的重要部分。签名的生成涉及以下步骤:

    • 将头部和载荷的编码结果拼接在一起,中间用一个点(.)分隔。
    • 使用指定的签名算法(例如HMAC SHA-256)对拼接的字符串进行加密,秘钥就是你提到的admin-secret-key

最终形成的JWT是由头部、载荷和签名三部分组成的字符串,中间用点(.)分隔。下面是一个更详细的示例代码,以Python为例:

import jwt  # PyJWT库

# 定义头部
header = {
    "alg": "HS256",
    "typ": "JWT"
}

# 定义载荷
payload = {
    "user_id": 123,
    "username": "example_user",
    "role": "admin"
}

# 从配置文件中获取admin-secret-key
admin_secret_key = "itcast"

# 使用PyJWT生成JWT
# 注意,这里PyJWT库会自动处理头部的Base64编码
jwt_token = jwt.encode(payload, admin_secret_key, algorithm='HS256', headers=header)

print("Generated JWT token:", jwt_token)

在这个示例中,PyJWT库自动处理了JWT生成的各个步骤,包括头部和载荷的Base64编码以及签名的生成。

签名生成的具体细节

  1. 头部和载荷的编码

    • 头部:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
    • 载荷:eyJ1c2VyX2lkIjoxMjMsInVzZXJuYW1lIjoiZXhhbXBsZV91c2VyIiwicm9sZSI6ImFkbWluIn0
  2. 拼接

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjMsInVzZXJuYW1lIjoiZXhhbXBsZV91c2VyIiwicm9sZSI6ImFkbWluIn0
    
  3. 签名生成

    • 使用HMAC SHA-256算法和admin-secret-key对上面的拼接字符串进行加密,生成签名字串(假设签名为signature_content
    • 例如:Pq9e7kF1nP1bP3sdfsdf...(这是一个示例,实际签名是二进制数据的Base64 URL编码)
  4. 最终JWT

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjMsInVzZXJuYW1lIjoiZXhhbXBsZV91c2VyIiwicm9sZSI6ImFkbWluIn0.Pq9e7kF1nP1bP3sdfsdf...
    

这样,JWT就完整地生成了,包含了头部、载荷和签名部分,可以用于安全的身份验证。

4.spring springMVC springboot 区别

在这里插入图片描述

在这里插入图片描述

Spring Framework 是一个非常广泛和流行的 Java 应用程序开发框架,它提供了一个全面的编程和配置模型。Spring Framework 本身提供了核心功能,如依赖注入、事务管理和模块化服务。基于这个核心框架,开发了许多其他的 Spring 项目,用于支持现代企业应用程序的各种需求。

Spring Framework
Spring Framework 是其他所有 Spring 项目的基础,它提供了基本的编程和配置模型。核心特征包括:
IoC 容器:控制反转容器用于依赖注入。
事件处理:应用程序可以使用标准的观察者模式进行事件发布和响应。
资源管理:提供从多种源(如本地文件系统、Java classpath等)访问资源的抽象。
数据访问:通过JDBC和ORM框架整合提供了一致的数据访问技术。

Spring MVC
Spring MVC 是基于 Spring Framework 的 web 框架,用于构建灵活和可测试的 web 应用。使用 Spring MVC,开发者可以通过控制器类来处理请求,并返回响应。
模型视图控制器:MVC 模式允许分离应用的输入逻辑、业务逻辑和 UI 逻辑。
强大的配置:支持 REST APIs 的构建、数据绑定、类型转换等。

Spring Boot
Spring Boot 是为了简化 Spring 应用的初始搭建及开发过程。它使用了“约定优于配置”的原则,帮助开发者快速启动和运行新项目。
自动配置:自动配置 Spring 和第三方库几乎不需要任何配置。
独立运行:可以创建独立的 Spring 应用程序,可直接运行或作为 jar 包部署。

Spring Cloud
Spring Cloud 为开发者提供了在分布式系统(如配置管理、服务发现、断路器)中常见模式的工具。
分布式配置管理:集中管理所有环境的配置。
服务发现:自动处理服务间的网络通信。

Spring Data
Spring Data 简化了基于 Spring 框架的数据访问技术,支持 NoSQL 和关系数据库的数据访问和管理。
数据仓库抽象:提供统一的数据访问模式减少重复代码。
查询派生机制:允许从方法名自动生成查询。

Spring Security
Spring Security 是一个强大的身份验证和访问控制框架,非常适合为企业应用提供安全性。
全面的安全性:支持身份验证、授权、防止 CSRF 等。
自定义扩展:可通过扩展点自定义安全行为。

5:controller

HTTP 协议中的 POSTPUT 是两种不同的请求方法,它们在使用时有一些显著的区别和适用场景:

POST 请求方法

  • 用途

    • POST 方法用于向服务器提交数据,创建资源或执行一些可能导致服务器状态变化的操作。
    • 典型的应用场景包括用户注册、提交表单数据、上传文件等需要在服务器上创建新资源或执行数据处理操作的情况。
  • 幂等性

    • POST 方法不具备幂等性,即对同一请求的多次调用可能会导致多次资源的创建或状态的变化。
  • 安全性

    • POST 方法不是安全的,因为它可能会引起服务器状态的变化或创建新的资源,而不仅仅是数据的读取或查询。
  • 使用示例

    @PostMapping("/users")
    public ResponseEntity<User> createUser(@RequestBody User user) {
        // 处理用户创建逻辑
        userService.createUser(user);
        return ResponseEntity.status(HttpStatus.CREATED).body(user);
    }
    

    上述示例中的 @PostMapping("/users") 表示这个方法处理 POST 请求,用于创建新的用户资源。

PUT 请求方法

  • 用途

    • PUT 方法用于更新服务器上的资源,或者在服务器上创建或替换指定资源。
  • 幂等性

    • PUT 方法具有幂等性,即多次相同的请求不会对资源状态产生额外的影响,即使多次执行,资源状态也不会改变。
  • 安全性

    • PUT 方法不是安全的,因为它通常会导致服务器上资源的更新或创建。
  • 使用示例

    @PutMapping("/users/{id}")
    public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User user) {
        // 根据id更新用户信息
        User updatedUser = userService.updateUser(id, user);
        if (updatedUser != null) {
            return ResponseEntity.ok(updatedUser);
        } else {
            return ResponseEntity.notFound().build();
        }
    }
    

    上述示例中的 @PutMapping("/users/{id}") 表示这个方法处理 PUT 请求,用于更新特定用户的信息。

总结

  • POSTPUT 方法都用于向服务器提交数据,但它们的主要区别在于语义和用途。
  • POST 用于创建新资源或执行操作,不具备幂等性和安全性。
  • PUT 用于更新或创建资源,具有幂等性但不是安全的。
  • 选择使用 POST 还是 PUT 取决于要执行的操作类型和语义的正确性。

6:事务

1.详细解释为什么加@transaction

 /**
     * 新增菜品和对应的口味
     *
     * @param dishDTO
     */
    @Transactional
    public void saveWithFlavor(DishDTO dishDTO) {

        Dish dish = new Dish();
        BeanUtils.copyProperties(dishDTO, dish);

        //向菜品表插入1条数据
        dishMapper.insert(dish);//后绪步骤实现

        //获取insert语句生成的主键值
        Long dishId = dish.getId();

        List<DishFlavor> flavors = dishDTO.getFlavors();
        if (flavors != null && flavors.size() > 0) {
            flavors.forEach(dishFlavor -> {
                dishFlavor.setDishId(dishId);
            });
            //向口味表插入n条数据
            dishFlavorMapper.insertBatch(flavors);//后绪步骤实现
        }
    }

在上面的代码中,为 saveWithFlavor() 方法添加 @Transactional 注解的原因如下:

  1. 原子性操作保证

    • saveWithFlavor() 方法涉及到两个数据库操作:插入菜品信息和插入菜品口味信息。
    • 使用事务可以确保这两个操作要么都成功执行并提交,要么都失败回滚。如果任一操作失败(比如数据库异常或唯一键冲突),事务会回滚到最初的状态,避免数据库数据不一致的情况。
  2. 异常处理

    • 事务可以有效地捕获和处理运行时异常。例如,如果插入菜品成功而插入口味失败,事务将会回滚,保证数据库不会有部分成功的操作留下来。
  3. 数据完整性

    • 确保在同一事务中的所有数据库操作要么全部成功,要么全部失败,从而保持数据的完整性。这在复杂的业务逻辑中尤为重要,例如插入一组相关联的数据(如菜品和口味)。
  4. 并发控制

    • 如果多个线程或请求同时调用 saveWithFlavor() 方法,事务可以确保在同一时间内只有一个事务能够修改数据。这防止了多个请求之间的数据竞争和不一致状态。
  5. 性能优化

    • 虽然事务会带来一些额外的开销,但通过将多个操作作为一个逻辑单元进行批处理,事务可以减少数据库锁的时间和通信开销,从而提高整体性能。

综上所述,使用 @Transactional 注解确保了 saveWithFlavor() 方法内部的数据库操作要么全部成功提交,要么全部失败回滚,从而保证了数据的一致性和完整性,是良好的开发实践。

2.这个为什么不用加@Transaction:

/**
     * 根据id修改菜品基本信息和对应的口味信息
     *
     * @param dishDTO
     */
    public void updateWithFlavor(DishDTO dishDTO) {
        Dish dish = new Dish();
        BeanUtils.copyProperties(dishDTO, dish);

        //修改菜品表基本信息
        dishMapper.update(dish);

        //删除原有的口味数据
        dishFlavorMapper.deleteByDishId(dishDTO.getId());

        //重新插入口味数据
        List<DishFlavor> flavors = dishDTO.getFlavors();
        if (flavors != null && flavors.size() > 0) {
            flavors.forEach(dishFlavor -> {
                dishFlavor.setDishId(dishDTO.getId());
            });
            //向口味表插入n条数据
            dishFlavorMapper.insertBatch(flavors);
        }
    }

updateWithFlavor() 方法中,为什么不需要加 @Transactional 注解呢?让我们分析一下:

  1. 单一数据库操作

    • updateWithFlavor() 方法中虽然涉及到了多个数据库操作(更新菜品信息、删除口味信息、重新插入口味信息),但这些操作并非复合操作,每个操作都可以独立执行并且是原子性的。即使其中的某个操作失败,也不会影响其他操作的执行。
  2. 不涉及多个数据表的事务一致性

    • 事务的主要作用是确保多个操作的一致性,尤其是涉及到多张数据表的操作。在 updateWithFlavor() 中,虽然有多个数据库操作,但这些操作分别对应着不同的表(菜品表和口味表),它们之间的操作并不依赖于彼此的一致性保证。
  3. 异常处理与回滚

    • 如果在更新菜品信息或者重新插入口味信息时发生异常,这些操作可以单独捕获并处理异常情况,而不需要整体回滚。例如,如果口味信息插入失败,可以采取适当的处理措施而不影响菜品信息的更新。
  4. 业务逻辑的简单性

    • updateWithFlavor() 方法的逻辑相对简单,并没有复杂的业务流程或需要多个操作在同一个事务中执行。因此,不需要引入事务管理的开销和复杂性。

综上所述,对于 updateWithFlavor() 方法来说,不需要添加 @Transactional 注解是合理的。这样做不仅简化了代码的实现,而且避免了不必要的事务管理开销,保持了方法的清晰和高效执行。

7:swagger

Knife4j 和 Swagger 是两个常用的接口文档生成工具,它们基本上是同一个项目的不同版本,都是由Swagger项目发展而来。

Swagger

Swagger 是一个开源框架,它用于设计、构建、文档化和消费 RESTful Web 服务。Swagger 提供了一套工具集,可以根据 API 的注解生成文档,并提供交互式 API 文档界面。主要功能包括:

  • 自动生成 API 文档:通过注解在代码中描述 API,然后生成对应的 API 文档。
  • 提供交互式界面:可以在浏览器中直接浏览和测试 API。
  • 支持多种语言和框架:可以与多种后端语言和框架集成,如 Java、Python、Node.js 等。

Swagger 在使用上比较成熟和稳定,有广泛的社区支持和大量的使用案例。

Knife4j

Knife4j 是 Swagger-Bootstrap UI 的增强版本,主要为了更好地展示和定制 Swagger 生成的 API 文档。Knife4j 在 Swagger 基础上提供了一些额外的功能和样式优化,例如:

  • 增强的界面展示:提供更美观、更易用的 API 文档展示界面,支持主题切换和自定义样式。
  • 文档的动态更新:支持文档的实时更新,不需要重新生成静态文件。
  • 便捷的配置:提供了更多的配置选项和插件,例如接口排序、响应示例展示等。
区别和选择
  1. 界面风格和交互体验

    • Swagger 的界面比较简洁,功能基本齐全,适合一般的 API 文档展示和测试需求。
    • Knife4j 则在 Swagger 的基础上做了更多的界面优化和功能增强,提供更美观、更用户友好的文档展示。
  2. 定制能力

    • Knife4j 提供了更多的自定义配置选项和插件,可以根据项目需要进行更精细的定制。
    • Swagger 虽然也支持一定程度的自定义,但相比之下选项较为有限。
  3. 社区和支持

    • Swagger 有着更广泛的社区支持和更多的使用案例,更新迭代也比较稳定。
    • Knife4j 虽然基于 Swagger,但相对来说社区规模和使用案例可能较 Swagger 小。
结论

选择使用 Swagger 还是 Knife4j 可根据具体的需求和偏好来决定:

  • 如果需要简单而直接的接口文档展示和测试功能,Swagger 是一个不错的选择。
  • 如果希望文档界面更加美观、功能更加丰富,并且愿意接受可能的配置复杂性,可以考虑使用 Knife4j。

无论选择哪个工具,它们都能帮助开发团队更好地管理和展示 API 文档,提升接口开发和维护的效率和可视化程度。

7:@RequestMapping

@RequestMapping("/aliyun/oss") 是一个注解,用于在Spring MVC控制器类中定义处理HTTP请求的基本路径。让我们详细解释它的作用和使用方式:

  1. 作用

    • 定义基本路径:该注解指定了控制器中所有处理方法的公共URL前缀。在这个例子中,"/aliyun/oss" 意味着所有映射到这个控制器的请求路径都应该以 /aliyun/oss 开头。
  2. 使用方式

    • 在类级别使用:可以将 @RequestMapping("/aliyun/oss") 放置在控制器类的开头,如下所示:

      @Controller
      @RequestMapping("/aliyun/oss")
      public class OssController {
          // 控制器方法
      }
      

      这样,所有在该类中声明的处理方法,如果没有明确指定完整的路径,都会以 /aliyun/oss 开头。

    • 在方法级别使用:除了类级别的路径前缀外,还可以在方法级别使用 @RequestMapping 注解来进一步定义特定方法的路径。例如:

      @ApiOperation(value = "Oss上传签名生成")
      @RequestMapping(value = "/policy", method = RequestMethod.GET)
      @ResponseBody
      public CommonResult<OssPolicyResult> policy() {
          // 方法逻辑
      }
      

      在这个例子中,@RequestMapping(value = "/policy", method = RequestMethod.GET) 指定了处理GET请求 /aliyun/oss/policy 的方法。此路径由类级别的 @RequestMapping 和方法级别的 @RequestMapping 组合而成。

  3. 组合路径

    • 当类级别和方法级别的路径组合时,例如类级别为 /aliyun/oss,方法级别为 /policy,最终的请求路径就是 /aliyun/oss/policy
  4. 其他用途

    • 请求方法限定@RequestMapping 还可以用来指定处理请求的方法类型(GET、POST、PUT等)和其他请求属性(如参数、头部信息等)。
    • 参数映射:可以使用路径变量(例如 /aliyun/oss/{id})或者参数映射(如 @RequestParam)来进一步处理请求。

总之,@RequestMapping("/aliyun/oss") 是在Spring MVC中用于定义控制器类处理路径的一种方式,它帮助开发者组织和管理应用程序中的请求映射,使得代码更加清晰和易于维护。

二.代码


三.总结

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring框架是一个开源的Java应用程序开发框架,它提供了一系列的解决方案和工具,用于简化企业级应用程序的开发。以下是对Spring框架的一些理解: 1. 轻量级:Spring框架的设计目标之一是保持轻量级,它不会强制引入过多的依赖或复杂的配置。使用Spring,你可以选择性地使用各个模块和功能,以满足你的需求。 2. 控制反转(IoC):Spring框架通过控制反转(IoC)容器来管理对象的生命周期和依赖关系。你可以通过配置文件或注解来描述对象之间的依赖关系,而不需要显式地在代码中进行硬编码。这样可以提高代码的可维护性和可测试性。 3. 依赖注入(DI):依赖注入是控制反转的一种实现方式,它使得对象不需要自己创建或管理它们所依赖的对象。Spring框架通过依赖注入将所需的依赖关系注入到对象中,使得对象之间解耦,并且方便进行单元测试和模块化开发。 4. 面向切面编程(AOP):Spring框架提供了面向切面编程的支持,使得你可以将横切关注点(例如日志记录、事务管理等)从核心业务逻辑中分离出来,并通过配置文件或注解的方式进行统一管理。 5. 企业级开发支持:Spring框架提供了许多企业级开发的功能和扩展,如集成事务管理、安全性、远程访问、缓存管理等。这些功能可以帮助开发者快速构建高可靠性、可扩展性和安全性的企业级应用程序。 总结来说,Spring框架是一个强大而灵活的Java开发框架,它通过控制反转、依赖注入和面向切面编程等特性,提供了一种优雅的方式来构建和管理Java应用程序。它的设计目标是简化开发过程,提高代码的可维护性和可测试性,同时提供了丰富的企业级功能和扩展。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值