在Maven2中运行单个测试用例并添加JVM参数<转>

本文介绍了如何在Maven2中单独运行一个JUnit测试用例,以及如何添加JVM参数。通过设置环境变量MAVEN_OPTS可全局调整Maven的JVM内存分配。如果需要针对特定测试用例 fork 新JVM并添加参数,可以在pom.xml中配置maven-surefire-plugin,并使用-D选项在命令行传递测试用例名和JVM参数。

 

 

版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明
http://ralf0131.blogbus.com/logs/75672327.html

参考:

http://blog.tfd.co.uk/2007/09/05/surefire-unit-test-arguments-in-maven-2/

http://maven.apache.org/plugins/maven-surefire-plugin/howto.html

http://mavenize.blogspot.com/2007/07/setting-command-line-arguments-for.html

都说Maven2是Ant的替代品,今天稍微使用了下Maven,记录备忘。

通过Maven单独运行一个Junit测试用例(无需配置surefire):

mvn -Dtest=TestXXX test

为Maven运行添加JVM参数,比如想给运行Maven的JVM分配更多内存,或者进行profiling等。有两种方法,一种是全局方法,即设置一个全局的环境变量MAVEN_OPTS。

linux下可修改.profile或者.bash_profile文件:export MAVEN_OPTS=-Xmx1024m

windows下可以添加环境变量MAVEN_OPTS

这样对于所有的maven进程都会启用这个JVM参数,所以是一个全局变量,具体可在bin\mvn.bat或者mvn.sh文件中找到如下内容:(%MAVEN_OPTS%即为全局JVM参数)

@REM Start MAVEN2
:runm2
%MAVEN_JAVA_EXE% %MAVEN_OPTS% -classpath %CLASSWORLDS_JAR% "-Dclassworlds.conf=%M2_HOME%\bin\m2.conf" "-Dmaven.home=%M2_HOME%" org.codehaus.classworlds.Launcher %MAVEN_CMD_LINE_ARGS%

如果有更加specific的需求,比如要单独运行一个JUnit Testcase,并且要fork出一个新的JVM来运行,还要为这个JVM加上特定的参数,那就需要更改项目的pom.xml文件了。具体方法是,修改项目的pom.xml在<build>-><plugins>,添加一个plugin,目的是配置surefire,使得每运行一个testcase,都单独fork出一个新的JVM来运行,若还要添加JVM参数,则可通过maven.test.jvmargs来进行传递:

 

<plugin>

        <groupId>org.apache.maven.plugins</groupId>

        <artifactId>maven-surefire-plugin</artifactId>

        <configuration>

            <forkMode>pertest</forkMode>

            <argLine>${maven.test.jvmargs}</argLine>

        </configuration>

</plugin>

然后在<properties>标签下加入,这样maven能够知道maven.test.jvmargs这个参数存在,默认值为空,通过运行时命令行传入:

 

<deploy.target/>

<maven.test.jvmargs></maven.test.jvmargs>

最后通过如下命令来运行,其中-Dtest是需要运行的testcase的名称,-Dmaven.test.jvmargs指需要传入的JVM参数,maven将这个参数传给新的fork出来的JVM运行。

mvn -Dtest=TestXXX -Dmaven.test.jvmargs='-agentlib:xxxagent -Xmx128m' test

 

 

 

 

【创新型】硬件控制器内存优化--java堆外缓存调研及落地 当前硬件控制器上,部分内存用于缓存TOPO、client等数据,该部分数据放在heap中,GC压力大。 通过调研OHC(off heap cache),采用堆外+native方式管理缓存,减少内存占用,同时减轻GC压力。 1.调研OHC,梳理其特性、压缩策略、性能压测 2.在硬件上,将现有内存缓存替换为OHC,验证优化硬件控制器内存使用 3.结合硬件控制器内存问题,优化硬件控制器内存 1.调研业内成熟的OHC组件,按照调研规范输出调研报告 2.设计和评审调研报告,完成技术选型和POC验证 3.梳理controller上缓存使用,分析缓存替换可行性和方案 4.在硬件控制器上完成缓存优化,通过graalvm编译,分析和验证效果 5、严格按照团队项目流程规范开展工作,组织需求评审会议、概要设计评审会议、设计&代码评审会议; 6、代码满足编码规范,通过SonarQube检查,单元测试覆盖率达标。 这是我的课题 “ 1. 前言 1.1. 项目简要说明 1.2. 任务概述 1.3. 可用资源 1.4. 术语定义 1.5. 参考资料 2. 需求分析(v2.0有调整) 3. 原理概述(v2.0有删减) 3.1OHCache内存分配原理 3.2 LZ4压缩算法原理 1. 滑动窗口与查找匹配 2. 编码匹配结果 3. 处理字面量 4. 压缩数据格式 3.3 堆外缓存显式更新数据 4. 系统架构描述(v2.0有调整) 4.1. 概述 4.2. 模块结构 4.2.1 序列化压缩实现 4.2.2 淘汰策略实现 4.2.3 CacheService的API实现 4.2.4 cache实例的管理: 4.3. 流程设计 5. 任务(或进程/线程)设计(v2.0有调整) 5.1. 原因 5.2. 任务(或进程/线程)内部流程 5.3. 任务(或进程/线程)间通信 6. 数据库及中间件设计(v2.0有调整) 6.1. 数据库设计 6.2. 缓存设计 7. 开发环境、测试环境及部署环境 7.1. 测试环境 7.2 使用方法 1. 前言 1.1. 项目简要说明 1.GC开销大: 随着缓存数据量增大,GC(尤其是Full GC)频率和停顿时间(STW)显著增加。 硬件控制器对实时性要求高,长GC停顿可能导致控制指令延迟、丢包甚至服务不可用,直接影响设备稳定性和用户体验。 2.Heap空间受限: JVM堆大小不能无限增长(受限于控制器物理内存和JVM配置),大量缓存占用宝贵的堆空间,挤压了业务逻辑运行所需内存, 限制了控制器处理更大规模网络或更复杂业务的能力。 在上述背景下硬件控制器的性能受到影响,本项目旨在修改原有的堆内缓存组件实现,将TOPO、Client等数据存放到堆外内存中,从而降低GC压力。为此本项目需要实现缓存条目级别的TTL淘汰实现,序列化效率、压缩效率高。 1.2. 任务概述 本任务是项目的全部,需要新设计模块(solution-components-cache-ohc),该模块是对(olution-components-cache-api)的堆外缓存实现。 1.3. 可用资源 1.可以复用(solution-components-lock-mem)锁模块来对缓存上读写锁,锁的粒度是锁到key-value级别 2.可以参考(solution-components-cache-mem)的代码设计 3.利用OHCache来操作堆外内存,利用其API来实现key-value级别的TTL定时淘汰和LZ4压缩策略 1.4. 术语定义 GC Garbage Colletion(垃圾回收) OHC off-heap cache(堆外缓存) TOPO 网络拓扑数据 Graalvm 支持AOT编译的高性能JDK LZ4 一种压缩算法 Kryo 一种序列化组件 1.5. 参考资料 Java堆外缓存OHC在马蜂窝推荐引擎的应用-优快云博客: ohcahe应用 堆外缓存OHCache使用总结-优快云博客:OHCache使用方法 Kryo序列化详解-优快云博客:序列化 LZ4压缩算法详解:原理、实现与Java应用-优快云博客;LZ4压缩算法原理及应用 在maven环境中使用GraalVM来构建本地原生应用程序(一)构建本地可执行文件_graalvm maven-优快云博客:graalvm编译 01 - GraalVM - CRD-CP-Cloud-Infra - Confluence:graalvm编译 Outline Design for Cache and Lock Component - EBG-Internal-Shared - Confluence:cache组件、lock组件的设计 2. 需求分析(v2.0有调整) 硬件控制器内存优化--java堆外缓存调研及落地需求分析文档-彭小刚 - CRD_EP_Network_Service_Backend_Project - Confluence:需求文档链接 3. 原理概述(v2.0有删减) 由于拓扑数据、用户数据等数据在cache-mem中使用了caffine组件,将数据存在JVM heap内,这部分数据不需要被GC扫描,是通过自己的过期时间定时清除的。随着数据量不断变大,GC扫描的时间会不断延长,从而降低硬件控制器的性能。使用chroniclemap组件来将数据存入堆外,一方面可以降低GC的压力,另一方面这些数据存入堆外后不需要额外的对象头,字段填充引用指针等数据外的空间,还可以集成LZ4等压缩算法进一步减少序列化后的内存占用。 3.1OHCache内存分配原理 OHCache的实现类OHCacheLinkedImpl的put方法点进去能看到oldValueAdr = Uns.allocate(oldValueLen, throwOOME);,再往下可以看到long address = allocator.allocate(bytes);这个allocator是一个接口,有UnsafeAllocator和JNANativeAllocator两个实现类,第一种就是经典的通过反射拿到unsafe类调用unsafe.allocateMemory来分配内存。第二种点进去Native.malloc(size);,这个是用JNI方法来分配内存。OHCache默认使用的效率更高的JNA方法。 3.2 LZ4压缩算法原理 1. 滑动窗口与查找匹配 原理:LZ4 使用一个滑动窗口来查找输入数据中的重复字符串。滑动窗口是一个固定大小的缓冲区,它存储了最近处理过的数据。当处理新的数据时,算法会在滑动窗口中查找与当前数据匹配的最长字符串。 2. 编码匹配结果 原理:一旦找到匹配,LZ4 会将匹配信息编码为一个标记(token)和一个偏移量 - 长度对。标记用于指示匹配的长度和是否有额外的字面量(未匹配的字符),偏移量表示匹配字符串在滑动窗口中的起始位置,长度表示匹配的长度。 3. 处理字面量 原理:如果在滑动窗口中没有找到匹配,或者匹配长度较短,LZ4 会将未匹配的字符(字面量)直接复制到压缩数据中。 4. 压缩数据格式 原理:LZ4 压缩数据由一系列的标记和数据块组成。每个标记后面跟着一个或多个数据块,数据块可以是字面量或偏移量 - 长度对。 例如给出一个字符串: abcde_fghabcde_ghxxahcde,描述出此字符串的压缩过程 压缩得到的压缩串 a.当lz4解压从0开始遍历时,先判断token值(-110),-110换为计算机二进制为10010010,高四位1001代表字面量长度为9,低四位0010代表重复项匹配的长度2+4(minimum repeated bytes) b.向后遍历9位,得到长度为9的字符串(abcde_fgh),偏移量为9,从当前位置向前移动9位则是重复位起始位置,低四位说明重复项长度为6字节,则继续生成长度为6的字符串(abcde_) c.此时生成(abcde_fghabcde_),接着开始判断下一sequence token起始位,最终生成abcde_fghabcde_ghxxahcde(压缩前的字符串) 3.3 堆外缓存显式更新数据 在堆内缓存中,直接修改从缓存获取的对象会导致缓存中的对象同步被修改。这是因为: 引用传递机制 堆内缓存存储的是对象的引用而非副本。当您通过 cache.get(key) 获取对象时,实际得到的是缓存中原始对象的直接引用。任何对此对象的修改都会直接影响缓存中的值。 堆外缓存(如 Ehcache off-heap)存储的是对象的序列化字节数据而非内存引用。获取对象时需经过 内存隔离性 堆外缓存与堆内存完全隔离,两者数据独立存在: 修改堆内对象 = 修改本地副本 原始缓存数据 = 未改变的序列化字节流 因此在对数据进行更新操作的时候需要显示的将更新同步到堆外缓存。 以下是统计需要写操作需要显式更新数据的方法,堆内实现里这些更新方法都只从缓存里取出对象,对对象进行修改,没有显式更新到缓存里。 boolean replace(String cacheName, String key, Object oldValue, Object newValue) ; 替换缓存值,缓存值=oldValue才进行替换,发操作安全 redis cache根据序列化后的结果进行比较,mem cache根据equals方法进行比较 boolean expireKey(String cacheName, String key, long timeToLive, TimeUnit timeUnit) ; 设置缓存key的过期时间hash、timed hash、SortedSet等结构均可使用该接口进行缓存超时设置 hash结构 T putHashValue(String cacheName, String key, String hashKey, Object value) ; 向hashKey中放入缓存值,没有过期时间 T deleteHashValue(String cacheName, String key, String hashKey) ; 删除key对应的键值对 T putTimedHashValue(String cacheName, @NonNull String key, @NonNull String hashKey, @NonNull T value)向hashkey放入缓存值,存入延迟任务中 T deleteTimedHashValue(String cacheName, @NonNull String key, @NonNull String hashKey)删除TimedHashValue SortedSet结构 boolean addSortedSetValue(String cacheName, String key, Object value, double score) ; 向有序集合中添加 value 及其 score,如果存在则更新其 score int addSortedSetValueReturnSize(String cacheName, String key, Object value, double score) ; 向有序集合中添加 value 及其 score,如果存在则更新其 score,返回添加后的集合大小 Collection<T> rangeSortedByScore(String cacheName, String key, double min, double max, Class<T> clazz) ; 取出有序集合中 score 在 [min, max] 区间内的 value 集合 boolean removeSortedSetValue(String cacheName, String key, Object value) ; 删除有序集合中的指定 value Collection<T> pollSortedByScore(String cacheName, String key, double min, double max, Class<T> clazz);取出删除有序集合中 score 在 [min, max] 区间内的 value 集合 pollSortedByScore(String cacheName, @NonNull String key, double min, double max, int count, @NonNull Class<T> clazz);取出删除前 count 个有序集合中 score 在 [min, max] 区间内的 value 集合 Collection<T> pollFirst(String cacheName, String key, int count, Class<T> clazz);取出删除有序集合前count个元素 Collection<T> pollFirst(String cacheName, String key, int count, Double filterMin, Double filterMax, Class<T> clazz);对于有序集合前count个元素,仅删除和返回score在[filterMin,filterMax]之间的元素 4. 系统架构描述(v2.0有调整) 4.1. 概述 耦合度:本项目只需在原本使用堆内存的地方将cache组件依赖更换成solution-components-cache-ohc即可,耦合度低。 内存溢出:如果不断向缓存中添加数据导致物理内存不足以容纳这些数据时会发生内存溢出。需要通过设置过期时间来删除这些数据,释放内存;使用压缩技术降低内存空间的使用强度 发冲突:OHCache本身是发安全的,但实现sortedset类的实现需要在一个方法里存两个set缓存,这就不能保证发安全了,需要额外加锁。 4.2. 模块结构 4.2.1 序列化压缩实现 序列化选择kryo,由于kryo非线程安全的,需要用treadlocal去管理; 且output实现closeable接口,需要手动关闭和flush,操作比较麻烦; 其次为了提高序列化的空间使用效率,需要对需要序列化的类进行统一注册。 因此需要开发一个kryoUtil类来简化操作,该工具类内部保有一个treadlocal<kryo>,使用者无需管理kryo实例,创建output缓存区,flush数据,直接调用工具类的静态方法存储或读取数据即可。 初始化负责kryo的一些配置 @Override protected Kryo initialValue() { Kryo kryo = new Kryo(); /** * 不要轻易改变这里的配置!更改之后,序列化的格式就会发生变化, * 上线的同时就必须清除 Redis 里的所有缓存, * 否则那些缓存再回来反序列化的时候,就会报错 */ //支持对象循环引用(否则会栈溢出) kryo.setReferences(true); //默认值就是 true,添加此行的目的是为了提醒维护者,不要改变这个配置 //不强制要求注册类(注册行为无法保证多个 JVM 内同一个类的注册编号相同;而且业务系统中大量的 Class 也难以一一注册) kryo.setRegistrationRequired(true);//默认值就是 false,添加此行的目的是为了提醒维护者,不要改变这个配置 /** * 注册需要序列化的类 */ kryo.register(List.class); kryo.register(ArrayList.class); kryo.register(Map.class); kryo.register(HashMap.class); kryo.register(FdbEntry.class); kryo.register(FdbMacEntry.class); kryo.register(InformFdb.class); kryo.register(OswFdbInfo.class); kryo.register(Wrapper.class); kryo.register(HashSet.class); //Fix the NPE bug when deserializing Collections ((Kryo.DefaultInstantiatorStrategy) kryo.getInstantiatorStrategy()) .setFallbackInstantiatorStrategy(new StdInstantiatorStrategy()); //返回Kryo实例 return kryo; } 一共九个方法,第一个方法是获取当前线程的kryo实例的;剩下八种分为前后两类,一类是序列化携带类信息,一类是不带序列化信息,可以看到入参需要引入class。 另外,AtomicLong由于内部限制了字段访问,kryo序列器无法对其进行序列化,需要手动实现其序列化。 public class AtomicLongSerializer extends Serializer<AtomicLong> { @Override public void write(Kryo kryo, Output output, AtomicLong object) { output.writeLong(object.get()); // 序列化当前值 } @Override public AtomicLong read(Kryo kryo, Input input, Class<AtomicLong> type) { return new AtomicLong(input.readLong()); // 通过 long 值构造新对象 } } kryo.register(AtomicLong.class,new AtomicLongSericalizer<>()); 关于这个AtomicLong存在一个问题,CacheService里只提供了getAtomicLong方法,对于堆内缓存获取AtomicLong再对其进行修改,修改会自动同步到缓存里。对于堆外缓存,拿到这个AtomicLong的值对其进行修改,还需要显式的更新到缓存里才能保证一致性。而CacheService接口没有提供相应的set接口。 //本地实现 @Override public IAtomicLong getAtomicLong(String cacheName, @NonNull String key) { Wrapper wrapper = cacheManager.getCache(cacheName).get(key, k -> Wrapper.of(new AtomicLong())); assert wrapper != null; if (TimeUtil.isExpire(wrapper.getExpireTime())) { // 缓存已失效,放入新缓存 wrapper = Wrapper.of(new AtomicLong()); cacheManager.getCache(cacheName).put(key, wrapper); } return AtomicLongImpl.of((AtomicLong) wrapper.getCacheObj()); } //redis实现 @Override public IAtomicLong getAtomicLong(String cacheName, @NonNull String key) { key = buildKey(cacheName, key); RAtomicLong atomicLong = redissonClient.getAtomicLong(key); return AtomicLongImpl.of(atomicLong); } redis用了代理模式设计,RAtomicLong 不是简单的 POJO,而是通过动态代理封装的操作接口: // 获取代理对象 RAtomicLong atomicLong = redisson.getAtomicLong("myAtomicLong"); 每次调用方法时(如 incrementAndGet()),代理对象会: 自动生成 Redis 命令 通过 Redis 连接器发送到服务端 在 Redis 单线程环境下保证原子性 根据redis的设计思路,提出以下解决方案 @AllArgsConstructor public class AtomicLongImpl implements IAtomicLong { private final OHCache cache; private final AtomicLong oAtomicLong; private final String cacheName; private final String key; public static IAtomicLong of( OHCache cache,AtomicLong oAtomicLong, String cacheName, String key) { return new AtomicLongImpl(cache, oAtomicLong,cacheName,key); } @Override public void set(long newValue) { 先set 再存缓存 } @Override public long get() { } @Override public long incrementAndGet() { } @Override public long getAndIncrement() { } 类似的,还有getHash、getSet、getList等方法,都是获取一个可操作的对象,无需手动更新,修改会自动同步到缓存里。 压缩方式通过CacheConfig提供是否启用压缩算法,以及选用哪种压缩算法。 public class CompressdKryoSerializer<T> implements CacheSerializer<Wrapper<T>> { @Override public void serialize(Wrapper<T> value, ByteBuffer buf) { } @Override public Wrapper<T> deserialize(ByteBuffer buf) { } @Override public int serializedSize(Wrapper<T> value) { } } 4.2.2 淘汰策略实现 过期时间在Wrapper和DelayTask之间是通过set或expireKey方法调用DelayedTaskManager.addTask创建DelayQueue存入DelayTask类实现同步的 线程一:清除超时缓存的逻辑图 线程二:清除无效的延迟任务 4.2.3 CacheService的API实现 1.<T> T get(String cacheName, @NonNull String key, Class<T> clazz) 2.<T> T get(String cacheName, @NonNull String key, @NonNull BiFunction<String, String, T> loadHandle, Class<T> clazz) 3.<T> void set(String cacheName, @NonNull String key, @NonNull T value, long timeToLive, @NonNull TimeUnit timeUnit) 4.long deleteKeyByPrefix(String cacheName, @NonNull String prefix) 5.boolean cas(String cacheName, @NonNull String key, @NonNull Version value) 6.<T> int addSortedSetValueReturnSize(String cacheName, @NonNull String key, @NonNull T value, double score) 4.2.4 cache实例的管理: Wrapper是存储value的统一类,里面有过期时间和cacheobj(存储数据的对象),由cachemanager管理;cachemanager还要管理cacheconfig。 cacheconfig与堆内缓存不同,需要设置segmentcount和capacity,来为堆外内存分配空间。还提供了配置项压缩策略,为0时,不启用压缩算法;为1时提供LZ4压缩算法,以此类推。 4.3. 流程设计 需要详细说明系统运行的相关流程。通常使用时序图进行说明。 绘制时注意包含所有的内部模块和外部资源,外部资源包括但不限于用户(APP或Web)、基础云,数据库,中间件等。 5. 任务(或进程/线程)设计(v2.0有调整) 涉及用户表现的定时任务(如给用户定时发送报表任务),有考虑夏令时的影响。夏令时开始时,会跳过某些时间段;夏令时结束时,某些时间段会重复。 如果不清楚有何影响,请向方嘉豪确认 设计时充分考虑线程安全,尤其涉及到状态机修改、配置修改等场景的地方(加锁规范 2.1新增)。 Click to add a new task... 5.1. 原因 1.由于chroniclemap不能设置TTL过期时间,需要为一个键值对添加过期时间是属性。当过期时间达到以后,如果使用get等方法查询时发现其已经到期就会直接将其清除缓存;但如果一直没有读取这个缓存,这个缓存就会一直存在内存里,需要一个线程来定时清除过期的键值对。 2.上述线程清理过期键值对是通过一个延迟队列来实现的,在清除过期键值对的同时也会将延迟队列的对应的延迟任务清除掉;但如果这个键值对在过期后,被定期清理之前被get方法查询删除的话,就会出现键值对已经删除,但延迟队列任务没有删除,因此还需要一个线程来清理这个延迟队列 因此本项目需要额外两个线程。 5.2. 任务(或进程/线程)内部流程 线程一:CleanTimeOutCacheThread,用于清除TTL到期的缓存条目 线程二:CleanQueueThread,用于清除TTL到期的延迟队列任务,线程一中清除过期缓存条目时会同时清理过期延迟任务,但会出现缓存条目已经清除但延迟任务还未清除的情况,需要定时清理。 5.3. 任务(或进程/线程)间通信 6. 数据库及中间件设计(v2.0有调整) 6.1. 数据库设计 6.2. 缓存设计 cacheName作为缓存实例的名字,每一个cacheName表示一个 Chroniclemap<String, Object> 缓存实例。每一个缓存实例支持设置单独的初始容量、最大容量、缓存失效时间等参数,也可使用全局默认参数设置。 当cacheName参数为null时,在memCache中使用全局 Chroniclemap<String, Object> globalCache 缓存实例进行数据缓存。 如上图, 使用 CacheManager 管理 chroniclemap<String, Object> 实例,包含一个全局的 globalCache,以及由 Map<String, chroniclemap<String, Object>> cacheMap 保存的多个 <cacheName, chroniclemap<String, Object>> 键值对。 cacheMap 大小可以动态扩容,也就是cacheName个数不用事先指定,由用户获取的时候进行判断,没有该cacheName则动态创建实例加入cacheMap,配置文件中的cache-name用作对单个cacheName的参数进行特殊配置。 基本checklist: restore时是否有删除缓存; 如果是site相关缓存,site删除时是否有联动删除缓存; 如果是设备相关缓存,设备forget、adopt、move site等事件时是否会联动删除缓存。 Click to add a new task... 7. 开发环境、测试环境及部署环境 7.1. 测试环境 需要在OC或者一体机上连接一定数量的设备进行测试。 7.2 使用方法 1.引入cache-api模块,使用接口进行编码 <dependency> <groupId>com.tplink.smb</groupId> <artifactId>solution-components-cache-api</artifactId> <version>1.2.9</version> </dependency> 2.引入cache-ohc模块 <dependency> <groupId>com.tplink.smb</groupId> <artifactId>solution-components-cache-ohc</artifactId> <version>1.2.9</version> </dependency> 3.注入cacheService: @AutuWired private CacheService cacheService; ” 这是我的课题设计概要文档;请你据此设计一些测试用例
最新发布
11-11
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值