SpringMock测试总结

1.常用注解

@Before:初始化方法,在任何一个测试方法执行之前,必须执行的代码。

@After: 释放资源,在任何一个测试方法执行之后,需要进行的收尾工作。

@Ignore:忽略的测试方法,标注的含义就是“某些方法尚未完成,咱不参与此次测试”;这样的话测试结果就会提示你有几个测试被忽略,而不是失败

@BeforeClass:针对所有测试,也就是整个测试类中,在所有测试方法执行前,都会先执行由它注解的方法,而且只执行一次。当然,需要注意的是,修饰符必须是public static void xxxx ;此 Annotation 是 JUnit 4 新增的功能。

@AfterClass:针对所有测试,也就是整个测试类中,在所有测试方法都执行完之后,才会执行由它注解的方法,而且只执行一次。需要注意的是,修饰符也必须是 public static void xxxx ; JUnit 4 新增的功能,与 @BeforeClass 是一对。

执行顺序:
在这里插入图片描述

2.示例

2.1 Controller层

import org.springframework.web.context.WebApplicationContext;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class ArticleControllerTest extends AbstractTransactionalJUnit4SpringContextTests {

    private MockMvc mockMvc;

    @Value("${api.secret-token}")
    private String secretToken;

    @Autowired
    private WebApplicationContext webApplicationContext;

    @Before
    public void setUp() {
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).alwaysDo(print()).build();
    }
    
    // 1.模拟上传Excel
	@Test
    @Sql("/db/test/test_import_loan_data.sql")
    public void testImportLoanData() throws Exception {
        URL resource = this.getClass().getResource("/file/upload_excel.xls");
        Path path = Paths.get(resource.toURI());
        byte[] data = Files.readAllBytes(path);
        MockMultipartFile file = new MockMultipartFile("excel", "upload_excel.xls",
                MediaType.IMAGE_JPEG_VALUE, data);
        ResultActions perform = mockMvc.perform(
                MockMvcRequestBuilders.fileUpload("/admin/import_loan").file(file)
        );
        perform.andExpect(status().isOk());
        perform.andExpect(jsonPath("status").value("200"));
    }
    
    /**
     *  2. 测试翻页
     * @throws Exception 异常
     */
    @Test
    @Sql(scripts = {"/db/test/admin_init.sql", "/db/test/exchange_rest_controller_test.sql", "/db/180411_dictionary_init.sql"})
    public void testGetExchangeList1() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/admin/exchange/get_exchange_list")   // 构建请求
                .param("page_size", "10")    // 构建传参
                .param("page_num", "1")
                .param("status", "2")
                .accept(MediaType.APPLICATION_JSON_UTF8))   // 设置请求类型
                .andExpect(status().isOk())   // 期望借口调用状态断言
                .andExpect(jsonPath("status").value(200))   // 期望借口返回值断言
                .andExpect(jsonPath("data.total_record").value(261))
                .andExpect(jsonPath("data.data").isNotEmpty())
                .andReturn();
    }
}

2.2 Service层

import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.rule.OutputCapture;
import org.springframework.test.annotation.Commit;
import org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.transaction.AfterTransaction;
import static org.hamcrest.core.StringContains.containsString;
import static org.junit.Assert.assertThat;

@RunWith(MockitoJUnitRunner.class)
public class IfcertAccessCoreServiceImplTest {

	 @Mock
    private AccountCheckConfig config;

    @Mock
    private IfcertAccessDataService accessDataService;
    @Mock
    private IfcertAccessClientService accessClientService;
    @Mock
    private QueryTaskService queryTaskService;
    @Mock
    private PushTaskService pushTaskService;
    @Mock
    private IfcertDailyCheckHistoryMapper dailyCheckHistoryMapper;
    @Mock
    private IfcertDailyFailRepushHistoryMapper failRepushHistoryMapper;
    @Mock
    private MailService mailService;

    private IfcertAccessCoreServiceImpl service;
	
	// 此处必须要求service实现类要有构造方法
    @Before
    public void setup() {
        service = new IfcertAccessCoreServiceImpl(config, accessDataService, accessClientService, queryTaskService,
                pushTaskService, dailyCheckHistoryMapper, failRepushHistoryMapper, mailService);
    }

    /**
     * 对账接口1:对账入库流程测试
     */
    @Test
    public void getBatchMessage() {

        String[] params = new String[]{"123", "456"};
        AccountCheckToDbRequest request = new AccountCheckToDbRequest();
        request.setApiKey("apikey");
        request.setEndPoint("endpoint");
        request.setSourceCode("sourcecode");
        request.setVersion("version");
        request.setDataType("0");
        request.setInfType("81");
        request.setBatchNum("323223");
        when(accessDataService.fillCheckRequestData(AccountCheckTypeEnum.DATA_TO_DB.getCode(), params))
                .thenReturn(request);

        BatchMessageResponseWrapper response = new BatchMessageResponseWrapper();
        List<BatchMessageResponse> result = Lists.newArrayList();
        BatchMessageResponse batchResponse = new BatchMessageResponse();
        batchResponse.setBatchNum("23232332");
        batchResponse.setDataType("0");
        batchResponse.setErrorMsg("errormsg");
        result.add(batchResponse);
        response.setResult(result);
        response.setCode("0000");
        response.setMessage("查询成功");
        when(accessClientService.getBatchMessage(request)).thenReturn(response);

        service.getBatchMessage("123", "456");

        verify(accessDataService, times(1))
                .fillCheckRequestData(AccountCheckTypeEnum.DATA_TO_DB.getCode(), params);
        verify(accessClientService, times(1)).getBatchMessage(request);

    }
    
	// 解决方法反复时void的处理
	@Test 
    public void updateReading() throws Exception { 
        doNothing().when(taskModificationService).changeReadingStatus(anyList(),anyByte());
    }

}
/**
 * @author lei.liu 
 * @version 1.0.0
 * @date 2017年12月29日 下午3:45:23
 */
@Service
public class IfcertAccessCoreServiceImpl implements IfcertAccessCoreService {
    private static final Logger LOGGER = LoggerFactory.getLogger(IfcertAccessCoreServiceImpl.class);

    /**
     * 对账接口2接口返回的数据分页显示数
     */
    private static final int PAGE_SIZE = 3000;

    private AccountCheckConfig config;
    private IfcertAccessDataService accessDataService;
    private IfcertAccessClientService ifcertAccessClientService;
    private QueryTaskService queryTaskService;
    private PushTaskService pushTaskService;
    private IfcertDailyCheckHistoryMapper dailyCheckHistoryMapper;
    private IfcertDailyFailRepushHistoryMapper failRepushHistoryMapper;
    private MailService mailService;

    /**
     * 构造注入,避免测试案例mock调用关联bean为null
     *
     * @param config
     * @param accessDataService
     * @param ifcertAccessClientService
     * @param queryTaskService
     * @param pushTaskService
     * @param dailyCheckHistoryMapper
     * @param failRepushHistoryMapper
     * @param mailService
     */
    @Autowired
    public IfcertAccessCoreServiceImpl(AccountCheckConfig config, IfcertAccessDataService accessDataService,
                                       IfcertAccessClientService ifcertAccessClientService, QueryTaskService queryTaskService,
                                       PushTaskService pushTaskService, IfcertDailyCheckHistoryMapper dailyCheckHistoryMapper,
                                       IfcertDailyFailRepushHistoryMapper failRepushHistoryMapper, MailService mailService) {
        this.config = config;
        this.accessDataService = accessDataService;
        this.ifcertAccessClientService = ifcertAccessClientService;
        this.queryTaskService = queryTaskService;
        this.pushTaskService = pushTaskService;
        this.dailyCheckHistoryMapper = dailyCheckHistoryMapper;
        this.failRepushHistoryMapper = failRepushHistoryMapper;
        this.mailService = mailService;
    }
    ...
    ...
}

2.3 Mapper层

import com.htouhui.bonus.coupon.entity.Account;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests;
import org.springframework.test.context.junit4.SpringRunner;
import java.math.BigDecimal;

@RunWith(SpringRunner.class)
@SpringBootTest
public class AccountMapperTest extends AbstractTransactionalJUnit4SpringContextTests {

    @Autowired
    private AccountMapper accountMapper;

    @Test
    @Sql("/db/test/account_mapper_test.sql") // 调用方法前执行sql脚本
    public void testGetByUserId() {
        Account account = accountMapper.getByUserId("h5602154779");
        Assert.assertNotNull(account);
        Assert.assertEquals(new BigDecimal("888.00"), account.getGrantMoney());
    }

    @Test
    @Sql("/db/test/account_mapper_test.sql")
    public void testUpdate() {
        Account account = Account.builder()
                .id(370941)
                .grantMoney(new BigDecimal("444.00"))
                .build();

        int num = accountMapper.update(account);

        Assert.assertEquals(1, num);

        Account byId = accountMapper.findById(370941L);

        Assert.assertNotNull(byId);
        Assert.assertEquals(new BigDecimal("444.00"), byId.getGrantMoney());
    }

}

3.对特殊框架的测试mock

3.1 针对shiro模拟用户登录进行mock

在某些场景下,对于shiro用户登录后产生的用户信息,需要用到

shiro官方文档提供了一个测试工具类AbstractShiroTest,需要用到

public abstract class AbstractShiroTest {

    private static ThreadState subjectThreadState;

    public AbstractShiroTest() {
    }

    /**
     * Allows subclasses to set the currently executing {@link Subject} instance.
     *
     * @param subject the Subject instance
     */
    protected void setSubject(Subject subject) {
        clearSubject();
        subjectThreadState = createThreadState(subject);
        subjectThreadState.bind();
    }

    protected Subject getSubject() {
        return SecurityUtils.getSubject();
    }

    protected ThreadState createThreadState(Subject subject) {
        return new SubjectThreadState(subject);
    }

    /**
     * Clears Shiro's thread state, ensuring the thread remains clean for future test execution.
     */
    protected void clearSubject() {
        doClearSubject();
    }

    private static void doClearSubject() {
        if (subjectThreadState != null) {
            subjectThreadState.clear();
            subjectThreadState = null;
        }
    }

    protected static void setSecurityManager(SecurityManager securityManager) {
        SecurityUtils.setSecurityManager(securityManager);
    }

    protected static SecurityManager getSecurityManager() {
        return SecurityUtils.getSecurityManager();
    }

    @AfterClass
    public static void tearDownShiro() {
        doClearSubject();
        try {
            SecurityManager securityManager = getSecurityManager();
            LifecycleUtils.destroy(securityManager);
        } catch (UnavailableSecurityManagerException e) {
            //we don't care about this when cleaning up the test environment
            //(for example, maybe the subclass is a unit test and it didn't
            // need a SecurityManager instance because it was using only
            // mock Subject instances)
        }
        setSecurityManager(null);
    }
}

模拟shiro测试

package com.micecs.erp.group.service;

import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.google.common.collect.Lists;
import com.micecs.erp.module.admin.Admin;
import com.micecs.erp.module.approval.entity.GroupAudit;
import com.micecs.erp.module.approval.exception.GroupAuditException;
import com.micecs.erp.module.approval.mapper.GroupAuditMapper;
import com.micecs.erp.module.common.CommonException;
import com.micecs.erp.module.enums.*;
import com.micecs.erp.module.meeting.dto.ReserveFundDto;
import com.micecs.erp.module.meeting.entity.AuditRequire;
import com.micecs.erp.module.meeting.entity.GroupBaseInfo;
import com.micecs.erp.module.meeting.entity.ReserveFund;
import com.micecs.erp.module.meeting.exception.DataInitException;
import com.micecs.erp.module.meeting.exception.RfpException;
import com.micecs.erp.module.meeting.mapper.AuditRequireMapper;
import com.micecs.erp.module.meeting.mapper.GroupBaseInfoMapper;
import com.micecs.erp.module.meeting.mapper.ReserveFundMapper;
import com.micecs.erp.module.meeting.service.ReserveManagerService;
import com.micecs.erp.module.meeting.service.impl.ReserveManagerServiceImpl;
import org.apache.shiro.UnavailableSecurityManagerException;
import org.apache.shiro.subject.Subject;
import org.junit.*;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import static com.micecs.erp.admin.ShiroTestUtils.setSubject;
import static org.mockito.Mockito.*;

/**
 * @author liulei, lei.liu@htouhui.com
 * @version 1.0
 */
@RunWith(MockitoJUnitRunner.class)
public class ReserveManagerServiceTest {

    @Mock
    private ReserveFundMapper reserveFundMapper;
    @Mock
    private GroupBaseInfoMapper groupBaseInfoMapper;
    @Mock
    private GroupAuditMapper groupAuditMapper;
    @Mock
    private AuditRequireMapper auditRequireMapper;

    private ReserveManagerService service;

    @Before
    public void init() {
        // 模拟shiro登录
        Subject subjectUnderTest = mock(Subject.class);
        setSubject(subjectUnderTest);
        Admin admin = new Admin();
        admin.setId(1L);
        admin.setAdministrator(true);
        when(subjectUnderTest.getPrincipal()).thenReturn(admin);
        service = new ReserveManagerServiceImpl(reserveFundMapper, groupBaseInfoMapper, groupAuditMapper, auditRequireMapper);
    }
	
    //该测试案例,有用到shiro登录后的一些用户信息取值静态方法
    @Test
    public void testGetReserveFundListByGroupId() {
        Long groupId = 1L;
        List<ReserveFund> list = Lists.newArrayList();
        ReserveFund fund = new ReserveFund();
        fund.setId(1L);
        fund.setGroupId(groupId);
        fund.setMoney(new BigDecimal(1000));
        fund.setFundType(CostDetailEnum.TRAVEL);
        fund.setDataType(DataTypeEnum.BUDGET.getValue());
        fund.setCreateTime(LocalDateTime.now());
        ReserveFund fund2 = new ReserveFund();
        fund2.setId(1L);
        fund2.setGroupId(groupId);
        fund2.setMoney(new BigDecimal(1000));
        fund2.setFundType(CostDetailEnum.TRAVEL);
        fund2.setDataType(DataTypeEnum.BUDGET.getValue());
        fund2.setCreateTime(LocalDateTime.now());
        list.add(fund);
        list.add(fund2);
        when(reserveFundMapper.selectListByGroupId(isA(LambdaQueryWrapper.class))).thenReturn(list);

        try {
            service.getReserveFundListByGroupId(groupId, DataTypeEnum.BUDGET);
        } catch (UnavailableSecurityManagerException e) {
            System.out.println("用户未登录");
        }

        verify(reserveFundMapper, times(1)).selectListByGroupId(isA(LambdaQueryWrapper.class));
    }
}

3.2 针对redisTemplate进行mock

package com.micecs.erp.group.service;

import com.google.common.collect.Lists;
import com.micecs.erp.common.Settings;
import com.micecs.erp.module.approval.entity.AuditArchive;
import com.micecs.erp.module.enums.*;
import com.micecs.erp.module.meeting.entity.GroupBaseInfo;
import com.micecs.erp.module.meeting.entity.InvoiceInfo;
import com.micecs.erp.module.meeting.entity.RequireDetail;
import com.micecs.erp.module.meeting.exception.CustomerBaseException;
import com.micecs.erp.module.meeting.mapper.GroupBaseInfoMapper;
import com.micecs.erp.module.meeting.mapper.InvoiceInfoMapper;
import com.micecs.erp.module.meeting.service.InvoiceInfoService;
import com.micecs.erp.module.meeting.service.impl.InvoiceInfoServiceImpl;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

import java.math.BigDecimal;
import java.util.List;
import java.util.concurrent.TimeUnit;

import static org.mockito.Mockito.*;

/**
 * @author liulei, lei.liu@htouhui.com
 * @version 1.0
 */
@RunWith(MockitoJUnitRunner.class)
public class InvoiceInfoServiceTest {

    @Mock
    private InvoiceInfoMapper invoiceInfoMapper;
    @Mock
    private GroupBaseInfoMapper groupBaseInfoMapper;
    @Mock
    private RedisTemplate<String, Object> collectionRedisTemplate;
    @Mock
    private ValueOperations valueOperations;

    private InvoiceInfoService service;

    @Before
    public void init() {
        when(collectionRedisTemplate.opsForValue()).thenReturn(valueOperations);
        service = new InvoiceInfoServiceImpl(invoiceInfoMapper, groupBaseInfoMapper, collectionRedisTemplate);
    }

    @Test
    public void testGetInvoiceOfDefined() {

        when(invoiceInfoMapper.getInvoiceByTypes(anyList())).thenReturn(Lists.newArrayList());

        service.getInvoiceOfDefined(InvoiceInfoEnum.MEETING);

        verify(invoiceInfoMapper, times(1)).getInvoiceByTypes(anyList());
    }

    @Test
    public void testGetInvoiceInfoByType() {
        InvoiceInfoEnum type = InvoiceInfoEnum.TRAVEL;
        List<InvoiceInfo> itemLists = Lists.newArrayList();
        InvoiceInfo item1 = new InvoiceInfo();
        item1.setName("款项1");
        item1.setValue(1);
        InvoiceInfo item2 = new InvoiceInfo();
        item2.setName("款项2");
        item2.setValue(1);
        itemLists.add(item1);
        itemLists.add(item2);
        List<InvoiceInfo> taxLists = Lists.newArrayList();
        InvoiceInfo iv1 = new InvoiceInfo();
        iv1.setName("发票1");
        iv1.setValue(1);
        InvoiceInfo iv2 = new InvoiceInfo();
        iv2.setName("发票2");
        iv2.setValue(2);
        InvoiceInfo iv3 = new InvoiceInfo();
        iv3.setName("发票3");
        iv3.setValue(3);
        InvoiceInfo iv4 = new InvoiceInfo();
        iv4.setName("发票4");
        iv4.setValue(4);
        taxLists.add(iv1);
        taxLists.add(iv2);
        taxLists.add(iv3);
        taxLists.add(iv4);
        TeamNatureEnum teamType = TeamNatureEnum.TRAVEL;
        String redisKey = Settings.ENV + ":INVOICE:DEFINE:" + type.getValue() + "-" + teamType.getValue() + "-";

        when(valueOperations.get(redisKey)).thenReturn(null);
        when(invoiceInfoMapper.getInvoiceByTypes(anyList())).thenReturn(itemLists).thenReturn(taxLists);
        doNothing().when(valueOperations).set(isA(String.class), anyList(), anyLong(), isA(TimeUnit.class));

        List<InvoiceInfo> info = service.getInvoiceInfoByType(type, teamType, null);

        verify(valueOperations, times(1)).get(argThat(argument -> {
            Assert.assertEquals(redisKey, argument);
            return true;
        }));

        verify(invoiceInfoMapper, times(2)).getInvoiceByTypes(anyList());
        info.forEach(vo -> {
            // 断言发票税率
            Assert.assertTrue(new BigDecimal("0").compareTo(vo.getChildren().get(0).getTaxRate()) == 0);
            Assert.assertEquals(4, vo.getChildren().size());
        });
    }
}

Classes contained in spring-mock.jar: org.springframework.mock.jndi.ExpectedLookupTemplate.class org.springframework.mock.jndi.SimpleNamingContext.class org.springframework.mock.jndi.SimpleNamingContextBuilder.class org.springframework.mock.web.DelegatingServletInputStream.class org.springframework.mock.web.DelegatingServletOutputStream.class org.springframework.mock.web.HeaderValueHolder.class org.springframework.mock.web.MockExpressionEvaluator.class org.springframework.mock.web.MockFilterChain.class org.springframework.mock.web.MockFilterConfig.class org.springframework.mock.web.MockHttpServletRequest.class org.springframework.mock.web.MockHttpServletResponse.class org.springframework.mock.web.MockHttpSession.class org.springframework.mock.web.MockMultipartFile.class org.springframework.mock.web.MockMultipartHttpServletRequest.class org.springframework.mock.web.MockPageContext.class org.springframework.mock.web.MockRequestDispatcher.class org.springframework.mock.web.MockServletConfig.class org.springframework.mock.web.MockServletContext.class org.springframework.mock.web.PassThroughFilterChain.class org.springframework.mock.web.portlet.MockActionRequest.class org.springframework.mock.web.portlet.MockActionResponse.class org.springframework.mock.web.portlet.MockMultipartActionRequest.class org.springframework.mock.web.portlet.MockPortalContext.class org.springframework.mock.web.portlet.MockPortletConfig.class org.springframework.mock.web.portlet.MockPortletContext.class org.springframework.mock.web.portlet.MockPortletPreferences.class org.springframework.mock.web.portlet.MockPortletRequest.class org.springframework.mock.web.portlet.MockPortletRequestDispatcher.class org.springframework.mock.web.portlet.MockPortletResponse.class org.springframework.mock.web.portlet.MockPortletSession.class org.springframework.mock.web.portlet.MockPortletURL.class org.springframework.mock.web.portlet.MockRenderRequest.class org.springframework.mock.web.portlet.MockRenderResponse.class org.springframework.test.AbstractDependencyInjectionSpringContextTests.class org.springframework.test.AbstractSingleSpringContextTests.class org.springframework.test.AbstractSpringContextTests.class org.springframework.test.AbstractTransactionalDataSourceSpringContextTests.class org.springframework.test.AbstractTransactionalSpringContextTests.class org.springframework.test.AssertThrows.class org.springframework.test.ConditionalTestCase.class org.springframework.test.annotation.AbstractAnnotationAwareTransactionalTests.class org.springframework.test.annotation.DirtiesContext.class org.springframework.test.annotation.ExpectedException.class org.springframework.test.annotation.IfProfileValue.class org.springframework.test.annotation.NotTransactional.class org.springframework.test.annotation.ProfileValueSource.class org.springframework.test.annotation.Repeat.class org.springframework.test.annotation.SystemProfileValueSource.class org.springframework.test.annotation.Timed.class org.springframework.test.jpa.AbstractAspectjJpaTests.class org.springframework.test.jpa.AbstractJpaTests.class org.springframework.test.jpa.OrmXmlOverridingShadowingClassLoader.class org.springframework.test.web.AbstractModelAndViewTests.class
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值