JUNIT5+Mockito单元测试

1、前言

之前写过一篇使用testMe自动生成单元测试用例,使用的是junit4来编写的单元测试用例,目前很多新项目都已经使用JDK11+以及SpringBoot3+。本次基于junit5+Mockito来编写单元测试。

2、Maven依赖

2.1 JDK21+SpringBoot版本基于3.1.0

SpringBoot依赖

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.objenesis</groupId>
                    <artifactId>objenesis</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

mockito依赖

<!--junit5单元测试-->
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-junit-jupiter</artifactId>
            <version>5.3.1</version>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>5.3.1</version>
        </dependency>

lombok依赖

<dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>edge-SNAPSHOT</version>
        </dependency>

2.2 JDK17+SpringBoot版本基于2.2.5.RELEASE

SpringBoot依赖

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
    </parent>

Junit依赖

<dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.8.2</version>
        </dependency>

mockito依赖

<dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>5.2.0</version>
            <exclusions>
                <exclusion>
                    <groupId>net.bytebuddy</groupId>
                    <artifactId>byte-buddy</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>net.bytebuddy</groupId>
                    <artifactId>byte-buddy-agent</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-junit-jupiter</artifactId>
            <version>5.2.0</version>
        </dependency>
        <dependency>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy</artifactId>
            <version>1.14.1</version>
        </dependency>
        <dependency>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy-agent</artifactId>
            <version>1.14.1</version>
        </dependency>

lombok依赖

<dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.30</version>
        </dependency>

3、业务代码

UserService代码

@Slf4j
@Service
public class UserServiceImpl implements UserService {

	@Value("${custom.config.value:}")
	private String configValue;

    @Resource
    private UserManager userManager;

    @Override
    public Long createUser(UserDto userDto) {
        log.info("创建用户入参:{}", JSON.toJSONString(userDto));
        String name = userDto.getUsername();
        if (StringUtils.isBlank(name)) {
            log.error("用户名称不能为空");
            throw new BizException("用户名称不能为空");
        }

        Long id = userManager.createUser(userDto);
        log.info("创建用户出参:{}", id);
        return id;
    }

    @Override
    public Boolean updateUser(UserDto userDto) {
        log.info("更新用户入参:{}", JSON.toJSONString(userDto));
        Long id = userDto.getId();
        String name = userDto.getUsername();
        if (Objects.isNull(id)) {
            log.error("用户主键不能为空");
            throw new BizException("用户主键不能为空");
        }
        if (StringUtils.isBlank(name)) {
            log.error("用户名称不能为空");
            throw new BizException("用户名称不能为空");
        }

        UserDto user = userManager.getUser(userDto);
        if (Objects.isNull(user)) {
            log.error("用户不存在");
            throw new BizException("用户不存在");
        }
        Boolean result = userManager.updateUser(userDto);
        log.info("更新用户出参:{}", result);
        return result;
    }

    @Override
    public UserDto getUser(UserDto userDto) {
        log.info("获取用户入参:{}", JSON.toJSONString(userDto));
        Long id = userDto.getId();
        if (Objects.isNull(id)) {
            log.error("用户主键不能为空");
            throw new BizException("用户主键不能为空");
        }
        UserDto user = userManager.getUser(userDto);
        log.info("获取用户出参:{}", user);
        return user;
    }

    @Override
    public Boolean batchCreateUser(List<UserDto> list) {
        log.info("批量创建用户入参:{}", JSON.toJSONString(list));
        if (CollectionUtils.isEmpty(list)) {
            log.error("入参集合不能为空");
            throw new BizException("入参集合不能为空");
        }

        int size = 10;
        long keepAliveTime = 60;
        long start = System.currentTimeMillis();
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(10000);
        ThreadFactory threadFactory = new DefaultThreadFactory("executor");
        ExecutorService executorService
                = new ThreadPoolExecutor(size, size, keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory);

        List<CompletableFuture<Boolean>> futureList = new ArrayList<>();
        for (UserDto userDto : list) {
            CompletableFuture<Boolean> future = CompletableFuture.supplyAsync(() -> {
                log.info("当前线程名称:{}", Thread.currentThread());
                try {
                    Long id = userManager.createUser(userDto);
                    TimeUnit.SECONDS.sleep(3L);
                    log.info("线程:{} id={} done", Thread.currentThread(), id);
                    return Boolean.TRUE;
                } catch (InterruptedException e) {
                    log.error("创建用户异常:{}", e.getMessage(), e);
                    return Boolean.FALSE;
                }
            }, executorService);
            futureList.add(future);
        }

        Boolean result = Boolean.TRUE;
        try {
            CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])).get(10, TimeUnit.SECONDS);
            for (CompletableFuture<Boolean> future : futureList) {
                Boolean back = future.get();
                if (Boolean.FALSE.equals(back)) {
                    result = Boolean.FALSE;
                }
            }
        } catch (Exception e) {
            log.error("创建用户异常:{}", e.getMessage(), e);
            result = Boolean.FALSE;
        }

        long end = System.currentTimeMillis();
        log.info("批量创建用户耗时:{}", (end - start));

        log.info("批量创建用户出参:{}", result);
        return result;
    }

}

UserManager 代码

@Slf4j
@Component
public class UserManagerImpl implements UserManager {

    @Resource
    private UserRepository userRepository;

    @Override
    public Long createUser(UserDto userDto) {
        Long result = userRepository.createUser(userDto);
        log.info("UserManagerImpl.createUser方法执行结果:{}", result);
        return result;
    }

    @Override
    public Boolean updateUser(UserDto userDto) {
        return userRepository.updateUser(userDto);
    }

    @Override
    public UserDto getUser(UserDto userDto) {
        return userRepository.getUser(userDto);
    }

}

UserRepository代码

@Slf4j
@Component
public class UserRepositoryImpl implements UserRepository {

    @Override
    public Long createUser(UserDto userDto) {
        log.info("com.summer.toolkit.mock.UserRepositoryImpl.createUser方法执行");
        return 1L;
    }

    @Override
    public Boolean updateUser(UserDto userDto) {
        log.info("com.summer.toolkit.mock.UserRepositoryImpl.updateUser方法执行");
        return Boolean.TRUE;
    }

    @Override
    public UserDto getUser(UserDto userDto) {
        log.info("com.summer.toolkit.mock.UserRepositoryImpl.getUser方法执行");
        return userDto;
    }

4、单元测试

package com.summer.toolkit.service;

import com.alibaba.fastjson.JSON;
import com.summer.toolkit.dto.UserDto;
import com.summer.toolkit.exception.BizException;
import com.summer.toolkit.mock.UserManagerImpl;
import com.summer.toolkit.mock.UserRepositoryImpl;
import com.summer.toolkit.mock.UserServiceImpl;
import com.summer.toolkit.util.FileUtils;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.stubbing.Answer;
import org.springframework.test.util.ReflectionTestUtils;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;


@Slf4j
@ExtendWith(MockitoExtension.class)
@EnabledIfEnvironmentVariable(named = "DEBUG", matches = "true")
public class UserServiceTest {

    @Mock
    private UserRepositoryImpl userRepository;

    @Spy
    private UserManagerImpl userManager;

    @InjectMocks
    private UserServiceImpl userService;

    @Captor
    private ArgumentCaptor<UserDto> userDtoArgumentCaptor;

    @BeforeEach
    public void before() {
        // 通过ReflectionTestUtils注入需要的非public字段数据
        ReflectionTestUtils.setField(userManager, "userRepository", userRepository);
        // 通过反射给@Value从配置文件注入的属性赋值
        ReflectionTestUtils.setField(userService, "configValue", "definedValue");
    }

    @Test
    public void testCreateUser() {
        // 模拟依赖方法
        //Mockito.when(userManager.createUser(any())).thenReturn(Long.valueOf(1));
        Mockito.when(userRepository.createUser(any())).thenReturn(Long.valueOf(2));

        // 调用被测方法
        UserDto userDto = this.buildUserDto();
        Long result = userService.createUser(userDto);

        // 验证方法结果
        Long expect = 2L;
        Assertions.assertEquals(expect, result);
        // 验证方法是否被调用,并带有超时时间
        Mockito.verify(userManager, Mockito.timeout(100).times(1)).createUser(userDto);
        // 自定义异常描述
        Mockito.verify(userManager, Mockito.times(1).description("someMethod should be called twice")).createUser(any());
    }

	@Test
	public void testCreateUserException() {
		// 模拟依赖方法
		Mockito.when(userRepository.createUser(any())).thenThrow(new BizException("defined message"));
	
		// 验证方法是否抛出指定异常
		BizException exception = Assertions.assertThrows(BizException.class, () -> {
			// 调用被测方法
			UserDto userDto = this.buildUserDto();
			userService.createUser(userDto);
		});
		Assertions.assertEquals("defined message", exception.getMessage());
	}

    @Test
    public void testCreateUserContinuity() {
        // 模拟依赖方法多次调用返回值
        Mockito.when(userRepository.createUser(any())).thenReturn(1L, 2L, 3L);

        // 多次调用被测方法并验证每次返回值是否符合预期
        UserDto userDto = this.buildUserDto();

        Long result = userService.createUser(userDto);
        Assertions.assertEquals(1L, result);

        result = userService.createUser(userDto);
        Assertions.assertEquals(2L, result);

        result = userService.createUser(userDto);
        Assertions.assertEquals(3L, result);

        // 验证方法是否被调用
        Mockito.verify(userManager, Mockito.timeout(100).times(3)).createUser(userDto);
        // 自定义异常描述
        Mockito.verify(userManager, Mockito.times(3).description("someMethod should be called twice")).createUser(any());
    }

    @Test
    public void testUpdateUser() {
        // 模拟依赖方法
        Mockito.when(userManager.updateUser(any())).thenReturn(Boolean.TRUE);
        Mockito.when(userManager.getUser(any())).thenReturn(new UserDto());

        // 调用被测方法
        UserDto userDto = this.buildUserDto();
        userDto.setId(1L);
        Boolean result = userService.updateUser(userDto);

        // 验证方法结果
        Assertions.assertEquals(Boolean.TRUE, result);
        // 验证方法是否被调用
        Mockito.verify(userManager).getUser(any());
        Mockito.verify(userManager).updateUser(any());
    }

    @Test
    public void testGetUser() {
        // 模拟依赖方法
        Mockito.when(userManager.getUser(any())).thenReturn(new UserDto());

        // 调用被测方法
        UserDto userDto = this.buildUserDto();
        userDto.setId(1L);
        UserDto result = userService.getUser(userDto);

        // 验证方法结果
        Assertions.assertNotNull(result);
        // 验证方法是否被调用
        Mockito.verify(userManager).getUser(userDto);
    }

    @Test
    public void testBatchCreateUserException() {
        // 模拟依赖方法,指定单个异常类型
        Mockito.when(userManager.createUser(any())).thenThrow(BizException.class);

        // 调用被测方法
        List<UserDto> param = new ArrayList<>();
        UserDto userDto = this.buildUserDto();
        param.add(userDto);
        Boolean result = userService.batchCreateUser(param);

        // 验证方法结果
        Assertions.assertEquals(Boolean.FALSE, result);
        // 验证方法是否被调用,默认一次
        Mockito.verify(userManager).createUser(userDto);
        // 验证方法是否被调用了1次
        Mockito.verify(userManager, Mockito.times(1)).createUser(any());
    }

    @Test
    public void testBatchCreateUserTimes() {
        // 模拟依赖方法
        Mockito.when(userManager.createUser(any())).thenReturn(1L);

        // 调用被测方法
        List<UserDto> param = new ArrayList<>();
        UserDto userDto = this.buildUserDto();
        param.add(userDto);
        param.add(userDto);
        param.add(userDto);
        Boolean result = userService.batchCreateUser(param);

        // 验证方法结果
        Assertions.assertEquals(Boolean.TRUE, result);
        // 验证方法是否被调用了3次
        Mockito.verify(userManager, Mockito.times(3)).createUser(any());
        Mockito.verify(userManager, Mockito.atLeast(3)).createUser(any());
        Mockito.verify(userManager, Mockito.atMost(3)).createUser(any());
    }

    @Test
    public void testBatchCreateUserCallBack() {
        // 模拟依赖方法,指定回调函数,回调函数返回值会覆盖原有逻辑的真实返回值
        Mockito.when(userManager.createUser(any())).thenAnswer(new Answer<Long>() {
            @Override
            public Long answer(InvocationOnMock invocationOnMock) throws Throwable {
                log.info("执行回调方法");
                Object[] arguments = invocationOnMock.getArguments();
                log.info("回调参数为:{}", JSON.toJSONString(arguments));
                Method method = invocationOnMock.getMethod();
                log.info("方法名称为:{}", method.getName());
                return 2L;
            }
        });

        // 调用被测方法
        UserDto userDto = this.buildUserDto();
        Long result = userService.createUser(userDto);

        // 验证方法结果,此结果为回调函数的返回值
        Long expect = 2L;
        Assertions.assertEquals(expect, result);
    }

    @Test
    public void testCaptor() {
        // 模拟依赖方法
        Mockito.when(userManager.createUser(any())).thenReturn(1L);

        // 调用被测方法
        UserDto userDto = this.buildUserDto();
        Long result = userService.createUser(userDto);

        // 验证方法结果
        Assertions.assertEquals(1L, result);

        // 捕获调用方法参数,验证保存用户时数据主键是否为空
        Mockito.verify(userManager).createUser(userDtoArgumentCaptor.capture());
        Assertions.assertNull(userDtoArgumentCaptor.getValue().getId());
    }

    @Test
    @DisplayName("静态方法测试")
    public void testFileUtils() {
        // 构建对象
        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");

        // 模拟对应的静态类
        // JDK11及以上版本中,try块中的变量可以在外部声明
        MockedStatic<FileUtils> mocked = Mockito.mockStatic(FileUtils.class);
        try (mocked) {
            // 模拟依赖静态方法
            mocked.when(() -> FileUtils.readFileAllLines(anyString())).thenReturn(list);

            // 调用被测方法
            List<String> lines = FileUtils.readFileAllLines(anyString());

            // 验证方法结果
            Assertions.assertEquals(list.size(), lines.size());
        } catch (Exception e) {
            log.error("模拟静态方法异常:{}", e.getMessage(), e);
        }
    }

    /**
     * 构建用户数据传输对象
     *
     * @return UserDto 返回构建好的用户数据传输对象
     */
    private UserDto buildUserDto() {
        UserDto userDto = new UserDto();
        userDto.setUsername("小明");
        userDto.setBirthday(new Date());
        userDto.setAddress("北京市大兴区亦庄经济开发区");
        userDto.setComment("加麻加辣");
        userDto.setGender(1);
        return userDto;
    }

}

  • 9
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
第1章:对Spring框架进行宏观性的概述,力图使读者建立起对Spring整体性的认识。   第2章:通过一个简单的例子展现开发Spring Web应用的整体过程,通过这个实例,读者可以快速跨入Spring Web应用的世界。   第3章:讲解Spring IoC容器的知识,通过具体的实例详细地讲解IoC概念。同时,对Spring框架的三个最重要的框架级接口进行了剖析,并对Bean的生命周期进行讲解。   第4章:讲解如何在Spring配置文件中使用Spring 3.0的Schema格式配置Bean的内容,并对各个配置项的意义进行了深入的说明。   第5章:对Spring容器进行解构,从内部探究Spring容器的体系结构和运行流程。此外,我们还将对Spring容器一些高级主题进行深入的阐述。   第6章:我们从Spring AOP的底层实现技术入手,一步步深入到Spring AOP的内核中,分析它的底层结构和具体实现。   第7章:对如何使用基于AspectJ配置AOP的知识进行了深入的分析,这包括使用XML Schema配置文件、使用注解进行配置等内容。   第8章:介绍了Spring所提供的DAO封装层,这包括Spring DAO的异常体系、数据访问模板等内容。   第9章:介绍了Spring事务管理的工作机制,通过XML、注解等方式进行事务管理配置,同时还讲解了JTA事务配置知识。   第10章:对实际应用中Spring事务管理各种疑难问题进行透彻的剖析,让读者对Spring事务管理不再有云遮雾罩的感觉。   第11章:讲解了如何使用Spring JDBC进行数据访问操作,我们还重点讲述了LOB字段处理、主键产生和获取等难点知识。   第12章:讲解了如何在Spring中集成Hibernate、myBatis等数据访问框架,同时,读者还将学习到ORM框架的混用和DAO层设计的知识。   第13章:本章重点对在Spring中如何使用Quartz进行任务调度进行了讲解,同时还涉及了使用JDK Timer和JDK 5.0执行器的知识。   第14章:介绍Spring 3.0新增的OXM模块,同时对XML技术进行了整体的了解。   第15章:对Spring MVC框架进行详细介绍,对REST风格编程方式进行重点讲解,同时还对Spring 3.0的校验和格式化框架如果和Spring MVC整合进行讲解。   第16章:有别于一般书籍的单元测试内容,本书以当前最具实战的JUnit4+Unitils+ Mockito复合测试框架对如何测试数据库、Web的应用进行了深入的讲解。   第17章:以一个实际的项目为蓝本,带领读者从项目需求分析、项目设计、代码开发、单元测试直到应用部署经历整个实际项目的整体开发过程。
陈开雄 Spring+3.x企业应用开发实战光盘源码 !!!!压缩包的jar包太多,太大无法上传,请谅解,需要的可以联系我 QQ:349721489 第1章:对Spring框架进行宏观性的概述,力图使读者建立起对Spring整体性的认识。   第2章:通过一个简单的例子展现开发Spring Web应用的整体过程,通过这个实例,读者可以快速跨入Spring Web应用的世界。   第3章:讲解Spring IoC容器的知识,通过具体的实例详细地讲解IoC概念。同时,对Spring框架的三个最重要的框架级接口进行了剖析,并对Bean的生命周期进行讲解。   第4章:讲解如何在Spring配置文件中使用Spring 3.0的Schema格式配置Bean的内容,并对各个配置项的意义进行了深入的说明。   第5章:对Spring容器进行解构,从内部探究Spring容器的体系结构和运行流程。此外,我们还将对Spring容器一些高级主题进行深入的阐述。   第6章:我们从Spring AOP的底层实现技术入手,一步步深入到Spring AOP的内核中,分析它的底层结构和具体实现。   第7章:对如何使用基于AspectJ配置AOP的知识进行了深入的分析,这包括使用XML Schema配置文件、使用注解进行配置等内容。   第8章:介绍了Spring所提供的DAO封装层,这包括Spring DAO的异常体系、数据访问模板等内容。   第9章:介绍了Spring事务管理的工作机制,通过XML、注解等方式进行事务管理配置,同时还讲解了JTA事务配置知识。   第10章:对实际应用中Spring事务管理各种疑难问题进行透彻的剖析,让读者对Spring事务管理不再有云遮雾罩的感觉。   第11章:讲解了如何使用Spring JDBC进行数据访问操作,我们还重点讲述了LOB字段处理、主键产生和获取等难点知识。   第12章:讲解了如何在Spring中集成Hibernate、myBatis等数据访问框架,同时,读者还将学习到ORM框架的混用和DAO层设计的知识。   第13章:本章重点对在Spring中如何使用Quartz进行任务调度进行了讲解,同时还涉及了使用JDK Timer和JDK 5.0执行器的知识。   第14章:介绍Spring 3.0新增的OXM模块,同时对XML技术进行了整体的了解。   第15章:对Spring MVC框架进行详细介绍,对REST风格编程方式进行重点讲解,同时还对Spring 3.0的校验和格式化框架如果和Spring MVC整合进行讲解。   第16章:有别于一般书籍的单元测试内容,本书以当前最具实战的JUnit4+Unitils+ Mockito复合测试框架对如何测试数据库、Web的应用进行了深入的讲解。   第17章:以一个实际的项目为蓝本,带领读者从项目需求分析、项目设计、代码开发、单元测试直到应用部署经历整个实际项目的整体开发过程。
Android Studio提供了强大的单元测试功能,可以帮助开发人员验证代码的正确性和稳定性。以下是关于Android Studio单元测试的一些常见问题和答案: 1. 如何创建一个单元测试类? 在Android Studio中,可以在测试文件夹中创建一个Java类,并添加@Test注解来标记测试方法。例如: ```java import org.junit.Test; public class MyUnitTest { @Test public void addition_isCorrect() { assertEquals(4, 2 + 2); } } ``` 运行该测试类时,可以通过右键点击类名或方法名并选择"Run 'MyUnitTest'"来执行测试。 2. 如何运行单元测试? 可以通过多种方式运行单元测试: - 在测试类或测试方法上右键点击,并选择"Run 'ClassName'"或"Run 'MethodName'"。 - 在项目视图中,找到测试类或测试方法,并点击绿色的三角形图标来运行。 - 使用快捷键Shift + F10(Windows)或Control + R(Mac)来运行最后一次测试。 3. 如何检查单元测试的结果? 在运行单元测试后,可以在"Run"窗口中查看测试结果。绿色表示通过的测试,红色表示失败的测试。如果有失败的测试,可以点击失败的测试方法来查看详细的错误信息。 4. 如何使用断言来验证结果? 在单元测试中,可以使用断言来验证代码的预期行为。Android Studio使用JUnit框架来提供断言方法。常用的断言方法包括: - assertEquals(expected, actual):验证两个值是否相等。 - assertTrue(condition):验证条件是否为真。 - assertFalse(condition):验证条件是否为假。 - assertNull(object):验证对象是否为null。 - assertNotNull(object):验证对象是否不为null。 等等。 5. 如何模拟依赖进行单元测试? 在某些情况下,我们可能需要模拟依赖对象来进行单元测试。Android Studio可以使用Mockito等框

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值