27、Spring开发最佳实践与Kotlin应用

Spring开发最佳实践与Kotlin应用

1. 依赖注入最佳实践

Bean有两种依赖类型:
- 强制依赖 :是Bean必需的依赖,如果依赖不可用,上下文加载会失败。
- 可选依赖 :是可选的依赖,即使不可用,上下文也能正常加载。

建议使用构造函数注入来处理强制依赖,而不是setter注入。这样可以确保在缺少强制依赖时,上下文无法加载。示例代码如下:

public class SomeClass {
    private MandatoryDependency mandatoryDependency;
    private OptionalDependency optionalDependency;
    public SomeClass(MandatoryDependency mandatoryDependency) {
        this.mandatoryDependency = mandatoryDependency;
    }
    public void setOptionalDependency(OptionalDependency optionalDependency) {
        this.optionalDependency = optionalDependency;
    }
    //All other logic
}

Spring团队通常提倡构造函数注入,因为它能将应用组件实现为不可变对象,并确保所需依赖不为空。此外,构造函数注入的组件始终以完全初始化的状态返回给客户端代码。不过,大量的构造函数参数是一个不好的代码信号,意味着该类可能承担了过多的职责,应该进行重构以更好地实现关注点分离。Setter注入主要用于可以在类中分配合理默认值的可选依赖。否则,在代码使用依赖的地方必须进行非空检查。Setter注入的一个好处是,setter方法使该类的对象便于后续重新配置或重新注入。通过JMX MBeans进行管理就是Setter注入的一个有说服力的用例。

2. 管理Spring项目的依赖版本
  • 使用Spring Boot :最简单的管理依赖版本的方法是使用 spring-boot-starter-parent 作为父POM。示例如下:
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>${spring-boot.version}</version>
    <relativePath /> <!-- lookup parent from repository -->
</parent>

spring-boot-starter-parent 管理着200多个依赖的版本。在Spring Boot发布之前,会确保这些依赖的所有版本能够很好地协同工作。部分管理的依赖版本如下表所示:
| 依赖 | 版本 |
| ---- | ---- |
| activemq | 5.14.3 |
| ehcache | 2.10.3 |
| elasticsearch | 2.4.4 |
| h2 | 1.4.193 |
| jackson | 2.8.7 |
| jersey | 2.25.1 |
| junit | 4.12 |
| mockito | 1.10.19 |
| mongodb | 3.4.2 |
| mysql | 5.1.41 |
| reactor | 2.0.8.RELEASE |
| reactor-spring | 2.0.7.RELEASE |
| selenium | 2.53.1 |
| spring | 4.3.7.RELEASE |
| spring-amqp | 1.7.1.RELEASE |
| spring-cloud-connectors | 1.2.3.RELEASE |
| spring-batch | 3.0.7.RELEASE |
| spring-hateoas | 0.23.0.RELEASE |
| spring-kafka | 1.1.3.RELEASE |
| spring-restdocs | 1.1.2.RELEASE |
| spring-security | 4.2.2.RELEASE |
| thymeleaf | 2.1.5.RELEASE |

建议不要在项目POM文件中覆盖这些管理的依赖版本。这样可以确保在升级Spring Boot版本时,能获得所有依赖的最新版本升级。
- 使用自定义企业POM :如果必须使用自定义企业POM作为父POM,可以使用以下配置来管理依赖版本:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring-boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
  • 不使用Spring Boot :可以使用Spring BOM来管理所有基本的Spring依赖:
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-framework-bom</artifactId>
            <version>${org.springframework-version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
3. 单元测试

单元测试的基本目的是发现缺陷,但不同层的单元测试编写方法不同。
- 业务层 :编写业务层测试时,建议避免在单元测试中使用Spring Framework,这样可以确保测试独立于框架,并且运行速度更快。示例代码如下:

@RunWith(MockitoJUnitRunner.class)
public class BusinessServiceMockitoTest {
    private static final User DUMMY_USER = new User("dummy");
    @Mock
    private DataService dataService;
    @InjectMocks
    private BusinessService service = new BusinessServiceImpl();
    @Test
    public void testCalculateSum() {
        BDDMockito.given(dataService.retrieveData(Matchers.any(User.class)))
           .willReturn(Arrays.asList(new Data(10), new Data(15), new Data(25)));
        long sum = service.calculateSum(DUMMY_USER);
        assertEquals(10 + 15 + 25, sum);
    }
}

Spring Framework用于在运行的应用程序中连接依赖。但在单元测试中,结合使用 @InjectMocks @Mock Mockito注解是最佳选择。
- Web层 :Web层的单元测试涉及测试控制器(REST和其他类型)。建议如下:
- 对于基于Spring MVC构建的Web层,使用Mock MVC。
- 对于使用Jersey和JAX - RS构建的REST服务,Jersey Test Framework是一个不错的选择。
以下是设置Mock MVC框架的示例:

@RunWith(SpringRunner.class)
@WebMvcTest(TodoController.class)
public class TodoControllerTest {
    @Autowired
    private MockMvc mvc;
    @MockBean
    private TodoService service;
    //Tests
}

使用 @WebMvcTest 可以自动注入 MockMvc 并执行Web请求。 @WebMvcTest 的一个重要特性是它只实例化控制器组件,其他Spring组件需要进行模拟并使用 @MockBean 进行自动注入。
- 数据层 :Spring Boot为数据层单元测试提供了简单的注解 @DataJpaTest 。示例如下:

@DataJpaTest
@RunWith(SpringRunner.class)
public class UserRepositoryTest {
    @Autowired
    UserRepository userRepository;
    @Autowired
    TestEntityManager entityManager;
    //Test Methods
}

@DataJpaTest 还可以注入 TestEntityManager bean,它是专门为测试设计的标准JPA entityManager 的替代方案。如果想在 @DataJpaTest 之外使用 TestEntityManager ,可以使用 @AutoConfigureTestEntityManager 注解。数据JPA测试默认在嵌入式数据库上运行,这确保了测试可以多次运行而不影响数据库。

4. 其他单元测试最佳实践
  • 单元测试应该具有可读性,其他开发人员应该能够在不到15秒的时间内理解测试。目标是使测试能够作为代码的文档。
  • 单元测试应该仅在生产代码存在缺陷时失败。如果单元测试使用外部数据,当外部数据发生变化时,测试可能会失败,随着时间的推移,开发人员会对单元测试失去信心。
  • 单元测试应该运行快速。缓慢的测试很少被运行,会失去单元测试的所有好处。
  • 单元测试应该作为持续集成的一部分运行。一旦在版本控制中进行了提交,构建(包括单元测试)就应该运行,并在失败时通知开发人员。
5. 集成测试

单元测试用于测试特定的层,而集成测试用于测试多层代码。为了使测试具有可重复性,建议在集成测试中使用嵌入式数据库而不是真实数据库。建议为集成测试创建一个使用嵌入式数据库的单独配置文件,这样每个开发人员都可以有自己的数据库来运行测试。
以下是相关配置文件示例:
- application.properties

app.profiles.active: production
  • application-production.properties
app.jpa.database: MYSQL
app.datasource.url: <<VALUE>>
app.datasource.username: <<VALUE>>
app.datasource.password: <<VALUE>>
  • application-integration-test.properties
app.jpa.database: H2
app.datasource.url=jdbc:h2:mem:mydb
app.datasource.username=sa
app.datasource.pool-size=30

需要在测试范围中包含H2驱动依赖,示例如下:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>test</scope>
</dependency>

以下是一个使用 @ActiveProfiles("integration-test") 的集成测试示例:

@ActiveProfiles("integration-test")
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class TodoControllerIT {
    @LocalServerPort
    private int port;
    private TestRestTemplate template = new TestRestTemplate();
    //Tests
}

集成测试对于持续交付可用的软件至关重要。Spring Boot提供的功能使实现集成测试变得容易。

6. Spring Session

管理会话状态是分布式和扩展Web应用程序的重要挑战之一。HTTP是无状态协议,用户与Web应用程序交互的状态通常在 HttpSession 中管理。重要的是在会话中存储尽可能少的数据,专注于识别和删除会话中不需要的数据。

考虑一个具有三个实例的分布式应用程序,每个实例都有自己的本地会话副本。如果用户当前由应用实例1提供服务,而实例1出现故障,负载均衡器将用户发送到应用实例2,实例2不知道实例1中可用的会话状态,用户必须重新登录并重新开始,这不是一个好的用户体验。

Spring Session提供了将会话存储外部化的功能。它不是使用本地 HttpSession ,而是提供了将会话状态存储到不同数据存储的替代方案。Spring Session还提供了清晰的关注点分离,无论使用哪种会话数据存储,应用程序代码都保持不变。可以通过配置在不同的会话数据存储之间进行切换。

以下是连接Spring Session使用Redis会话存储的示例:
- 添加Spring Session的依赖

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
    <type>pom</type>
</dependency>
<dependency>
    <groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
</dependency>
  • 配置过滤器以用Spring Session替换 HttpSession
@EnableRedisHttpSession
public class ApplicationConfiguration {
    @Bean
    public LettuceConnectionFactory connectionFactory() {
        return new LettuceConnectionFactory();
    }
}
  • 通过扩展 AbstractHttpSessionApplicationInitializer 为Tomcat启用过滤
public class Initializer extends AbstractHttpSessionApplicationInitializer {
    public Initializer() {
        super(ApplicationConfiguration.class);
    }
}

这样配置后,应用程序代码与 HttpSession 交互的部分无需更改,仍然可以使用 HttpSession 接口,但在后台,Spring Session会确保会话数据存储到外部数据存储(在这个例子中是Redis):

req.getSession().setAttribute(name, value);

Spring Session提供了简单的选项来连接外部会话存储,将会话备份到外部会话存储可以确保即使应用程序的某个实例出现故障,用户也可以继续使用。

7. 缓存

缓存对于构建高性能应用程序至关重要。不希望每次都访问外部服务或数据库,可以缓存不经常更改的数据。Spring提供了透明的机制来连接和使用缓存。启用应用程序缓存的步骤如下:
- 添加Spring Boot Starter Cache依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
  • 添加缓存注解
@Component
public class ExampleRepository implements Repository {
    @Override
    @Cacheable("something-cache-key")
    public Something getSomething(String id) {
        //Other code
    }
}

支持的一些注解如下:
| 注解 | 说明 |
| ---- | ---- |
| @Cacheable | 用于缓存方法调用的结果。默认实现根据传递给方法的参数构造键。如果在缓存中找到值,则不会调用该方法。 |
| @CachePut | 与 @Cacheable 类似,但不同的是,该方法总是被调用,并且结果会被放入缓存中。 |
| @CacheEvict | 触发从缓存中移除特定元素。通常在元素被删除或更新时执行。 |

其他需要注意的事项:
- 默认使用的缓存是 ConcurrentHashMap
- Spring缓存抽象符合JSR - 107标准。
- 其他可以自动配置的缓存包括EhCache、Redis和Hazelcast。

8. 日志

Spring和Spring Boot依赖于Commons Logging API,不依赖于其他日志框架。Spring Boot提供了启动器来简化特定日志框架的配置。
- Logback :使用 spring-boot-starter-logging 启动器即可使用Logback框架。该依赖是大多数启动器(包括 spring-boot-starter-web )中包含的默认日志依赖。示例如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-logging</artifactId>
</dependency>

spring-boot-starter-logging 包含的Logback及相关依赖如下:

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl-over-slf4j</artifactId>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jul-to-slf4j</artifactId>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>log4j-over-slf4j</artifactId>
</dependency>
  • Log4j2 :要使用Log4j2,需要使用 spring-boot-starter-log4j2 启动器。使用 spring-boot-starter-web 等启动器时,需要确保排除 spring-boot-starter-logging 依赖。示例如下:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

spring-boot-starter-log4j2 启动器使用的依赖如下:

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jul-to-slf4j</artifactId>
</dependency>
  • 框架独立配置 :无论使用哪种日志框架,Spring Boot都允许在应用程序属性中进行一些基本的配置。示例如下:
logging.level.org.springframework.web=DEBUG
logging.level.org.hibernate=ERROR
logging.file=<<PATH_TO_LOG_FILE>>

在微服务时代,无论使用哪种日志框架,建议将日志记录到控制台(而不是文件),并使用集中式日志存储工具来捕获所有微服务实例的日志。

9. Kotlin简介

Kotlin是一种静态类型的JVM语言,使代码具有表现力、简洁性和可读性。Spring Framework 5.0对Kotlin有很好的支持。Kotlin是一种开源的静态类型语言,可用于构建在JVM、Android和JavaScript平台上运行的应用程序。它由JetBrains根据Apache 2.0许可证开发,源代码可在GitHub(https://github.com/jetbrains/kotlin)上获取。后续将探讨Kotlin的一些重要特性,并学习如何使用Kotlin和Spring Boot创建基本的REST服务。

Spring开发最佳实践与Kotlin应用

10. 使用Kotlin创建Spring Boot项目

在了解了Spring开发的各项最佳实践后,接下来看看如何使用Kotlin创建Spring Boot项目。以下为具体步骤:

  • 创建项目骨架 :可以使用Spring Initializr(https://start.spring.io/ )来创建一个新的Spring Boot项目。在该网站上,选择Kotlin作为语言,添加所需的依赖,如Spring Web等,然后生成项目压缩包并解压到本地。

  • 配置构建文件 :如果使用Maven,项目的 pom.xml 文件会包含Kotlin相关的配置和依赖。以下是一个简单的示例:

<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.example</groupId>
    <artifactId>kotlin-spring-boot</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <kotlin.version>1.7.20</kotlin.version>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib-jdk8</artifactId>
        </dependency>
    </dependencies>

    <build>
        <sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
        <testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.jetbrains.kotlin</groupId>
                <artifactId>kotlin-maven-plugin</artifactId>
                <configuration>
                    <args>
                        <arg>-Xjsr305=strict</arg>
                    </args>
                    <compilerPlugins>
                        <plugin>spring</plugin>
                    </compilerPlugins>
                </configuration>
                <dependencies>
                    <dependency>
                        <groupId>org.jetbrains.kotlin</groupId>
                        <artifactId>kotlin-maven-allopen</artifactId>
                        <version>${kotlin.version}</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>
</project>
  • 创建主应用类 :在 src/main/kotlin 目录下创建主应用类,示例如下:
package com.example.kotlinspringboot

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class KotlinSpringBootApplication

fun main(args: Array<String>) {
    runApplication<KotlinSpringBootApplication>(*args)
}
11. 实现简单的Spring Boot REST服务

创建好项目后,我们来实现一个简单的REST服务。

  • 定义数据模型 :创建一个简单的数据类来表示资源,例如:
package com.example.kotlinspringboot.model

data class Greeting(val id: Long, val content: String)
  • 创建控制器 :创建一个控制器类来处理HTTP请求,示例如下:
package com.example.kotlinspringboot.controller

import com.example.kotlinspringboot.model.Greeting
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import java.util.concurrent.atomic.AtomicLong

@RestController
class GreetingController {
    private val counter = AtomicLong()

    @GetMapping("/greeting")
    fun greeting(@RequestParam(value = "name", defaultValue = "World") name: String): Greeting {
        return Greeting(counter.incrementAndGet(), "Hello, $name!")
    }
}
12. Kotlin实现的单元测试

对于使用Kotlin实现的REST服务,同样需要进行单元测试。以下是一个使用MockMvc进行单元测试的示例:

package com.example.kotlinspringboot.controller

import com.example.kotlinspringboot.model.Greeting
import com.fasterxml.jackson.databind.ObjectMapper
import org.junit.jupiter.api.Test
import org.mockito.Mockito.`when`
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*

@WebMvcTest(GreetingController::class)
class GreetingControllerTest {

    @Autowired
    private lateinit var mockMvc: MockMvc

    @MockBean
    private lateinit var controller: GreetingController

    @Test
    fun testGreeting() {
        val id = 1L
        val content = "Hello, World!"
        val greeting = Greeting(id, content)

        `when`(controller.greeting("World")).thenReturn(greeting)

        mockMvc.perform(get("/greeting").param("name", "World"))
           .andExpect(status().isOk)
           .andExpect(content().contentType(MediaType.APPLICATION_JSON))
           .andExpect(jsonPath("$.id").value(id))
           .andExpect(jsonPath("$.content").value(content))
    }
}
13. 总结

本文涵盖了Spring开发的多个最佳实践,包括依赖注入、依赖版本管理、单元测试、集成测试、会话管理、缓存、日志等方面,同时介绍了Kotlin的基本概念以及如何使用Kotlin创建Spring Boot项目和实现REST服务。

以下是Spring开发最佳实践的总结表格:
| 实践领域 | 最佳实践要点 |
| ---- | ---- |
| 依赖注入 | 强制依赖使用构造函数注入,可选依赖使用Setter注入 |
| 依赖版本管理 | Spring Boot使用 spring-boot-starter-parent ,自定义POM使用 spring-boot-dependencies ,非Spring Boot使用Spring BOM |
| 单元测试 | 业务层避免使用Spring Framework,Web层根据情况选择Mock MVC或Jersey Test Framework,数据层使用 @DataJpaTest |
| 集成测试 | 使用嵌入式数据库,创建单独的配置文件 |
| 会话管理 | 使用Spring Session将会话存储外部化 |
| 缓存 | 添加 spring-boot-starter-cache 依赖并使用缓存注解 |
| 日志 | 选择合适的日志框架,建议记录到控制台并使用集中式存储 |
| Kotlin应用 | 使用Spring Initializr创建项目,使用Kotlin实现REST服务并进行单元测试 |

mermaid格式流程图展示Spring开发项目的整体流程:

graph LR
    A[项目初始化] --> B[依赖注入配置]
    B --> C[依赖版本管理]
    C --> D[单元测试编写]
    D --> E[集成测试编写]
    E --> F[会话管理配置]
    F --> G[缓存配置]
    G --> H[日志配置]
    H --> I[Kotlin应用开发]

通过遵循这些最佳实践,可以提高Spring项目的开发效率、可维护性和性能,同时利用Kotlin的优势,使代码更加简洁和易读。在实际开发中,应根据项目的具体需求和场景,灵活运用这些技术和方法。

内容概要:本文详细介绍了一个基于Java和Vue的联邦学习隐私保护推荐系统的设计实现。系统采用联邦学习架构,使用户数据在本地完成模型训练,仅上传加密后的模型参数或梯度,通过中心服务器进行联邦平均聚合,从而实现数据隐私保护协同建模的双重目标。项目涵盖完整的系统架构设计,包括本地模型训练、中心参数聚合、安全通信、前后端解耦、推荐算法插件化等模块,并结合差分隐私同态加密等技术强化安全性。同时,系统通过Vue前端实现用户行为采集个性化推荐展示,Java后端支撑高并发服务日志处理,形成“本地训练—参数上传—全局聚合—模型下发—个性化微调”的完整闭环。文中还提供了关键模块的代码示例,如特征提取、模型聚合、加密上传等,增强了项目的可实施性工程参考价值。 适合人群:具备一定Java和Vue开发基础,熟悉Spring Boot、RESTful API、分布式系统或机器学习相关技术,从事推荐系统、隐私计算或全栈开发方向的研发人员。 使用场景及目标:①学习联邦学习在推荐系统中的工程落地方法;②掌握隐私保护机制(如加密传输、差分隐私)模型聚合技术的集成;③构建高安全、可扩展的分布式推荐系统原型;④实现前后端协同的个性化推荐闭环系统。 阅读建议:建议结合代码示例深入理解联邦学习流程,重点关注本地训练全局聚合的协同逻辑,同时可基于项目架构进行算法替换功能扩展,适用于科研验证工业级系统原型开发
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值