关于Maven项目在tomcat服务器中运行输出的System.getProperty(“user.dir“)值不一致的问题

user.dir属性根据不同的运行环境有不同的返回值:在Eclipse或IDEA中为项目目录,Windows下Tomcat中为Tomcat的bin目录,Linux下Tomcat中为Tomcat根目录,启动脚本执行位置不同,该值也相应变化。

user.dir字面解释是用户工作目录,实际上因项目的不同运行环境和启动位置有关。

1. 项目运行在EclipseIEAD开发工具中,System.getProperty("user.dir")的值就是项目目录所在路径。

2. 项目运行在Windows下的tomcat中,System.getProperty("user.dir")的值就是tomcatbin目录所在路径。

3. 项目运行在Linux下的tomcat中,System.getProperty("user.dir")的值就是tomcat的根目录所在路径。

4.startup.batstartup.sh启动脚本的执行位置不一样,System.getProperty("user.dir")的值就是启动脚本的执行位置所在路径。

package com.ruoyi.kettle.config; import com.ruoyi.kettle.tools.KettleUtil; import lombok.extern.slf4j.Slf4j; import org.pentaho.di.core.Const; import org.pentaho.di.core.KettleEnvironment; import org.pentaho.di.core.exception.KettleException; import org.pentaho.di.core.plugins.PluginFolder; import org.pentaho.di.core.plugins.StepPluginType; import org.pentaho.di.core.util.EnvUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.URISyntaxException; import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.Enumeration; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.jar.JarEntry; import java.util.jar.JarFile; @Slf4j public class KettleEnv { private static final String PLUGIN_DIR = "kettlePlugins"; private static final String JNDI_DIR = "simple-jndi"; private static volatile Path extractedPluginPath; private static volatile Path extractedJndiPath; private static String pluginBasePath; // 读写锁确保线程安全 private static final ReentrantReadWriteLock jndiLock = new ReentrantReadWriteLock(); public static synchronized void init() { try { String pluginPath = getConfiguredPluginPath(); if (pluginPath == null) { // 自动检测并处理插件目录 pluginPath = resolvePluginPath(); } log.info("使用的Kettle插件路径: {}", pluginPath); // 添加插件路径(避免重复添加) String finalPluginPath = pluginPath; if (StepPluginType.getInstance().getPluginFolders().stream() .noneMatch(a -> a.getFolder().equals(finalPluginPath))) { StepPluginType.getInstance().getPluginFolders().add( new PluginFolder(pluginPath, false, true) ); } // 初始化Kettle环境 KettleEnvironment.init(); EnvUtil.environmentInit(); log.info("Kettle环境初始化成功"); pluginBasePath = pluginPath; } catch (Exception e) { log.error("Kettle环境初始化失败", e); } } /** * 设置JNDI环境(任务运行前调用) */ public static void setupJndiEnvironment() { jndiLock.writeLock().lock(); try { Path jndiPath = getDefaultJndiPath(); // 确保目录存在 if (!Files.exists(jndiPath)) { Files.createDirectories(jndiPath); log.info("创建JNDI目录: {}", jndiPath); } // 设置JNDI系统属性 System.setProperty("org.osjava.sj.root", jndiPath.toString()); System.setProperty("org.osjava.sj.delimiter", "/"); System.setProperty("org.osjava.sj.reload", "true"); Const.JNDI_DIRECTORY = jndiPath.toString(); log.info("JNDI路径已设置: {}", jndiPath); } catch (Exception e) { log.error("JNDI环境设置失败", e); throw new RuntimeException(e); } finally { jndiLock.writeLock().unlock(); } } /** * 获取默认JNDI路径 */ public static Path getDefaultJndiPath() { // 优先使用已提取的JNDI路径 if (extractedJndiPath != null) { return extractedJndiPath; } // 检查是否有配置的外部JNDI路径 String externalJndiPath = System.getProperty("kettle.jndi.path"); if (externalJndiPath != null && Files.exists(Paths.get(externalJndiPath))) { return Paths.get(externalJndiPath); } // 默认使用项目根目录下的config/simple-jndi return Paths.get(System.getProperty("user.dir"), "config/simple-jndi"); } /** * 读取jdbc.properties文件内容为字符串(保留原始格式) */ public static String readJdbcPropertiesAsString() throws IOException { return readJdbcPropertiesAsString(getDefaultJndiPath()); } public static String readJdbcPropertiesAsString(Path jndiPath) throws IOException { jndiLock.readLock().lock(); try { Path jdbcFile = jndiPath.resolve("jdbc.properties"); if (!Files.exists(jdbcFile)) { log.warn("jdbc.properties文件存在: {}", jdbcFile); return ""; } return new String(Files.readAllBytes(jdbcFile), StandardCharsets.UTF_8); } finally { jndiLock.readLock().unlock(); } } /** * 将字符串内容写入jdbc.properties文件(保留原始格式) */ public static void writeJdbcPropertiesFromString(String content) throws IOException { writeJdbcPropertiesFromString(getDefaultJndiPath(), content); } public static void writeJdbcPropertiesFromString(Path jndiPath, String content) throws IOException { jndiLock.writeLock().lock(); try { Path jdbcFile = jndiPath.resolve("jdbc.properties"); // 确保目录存在 if (!Files.exists(jndiPath)) { Files.createDirectories(jndiPath); } Files.write(jdbcFile, content.getBytes(StandardCharsets.UTF_8)); log.info("jdbc.properties更新成功, 路径: {}", jdbcFile); } finally { jndiLock.writeLock().unlock(); } } public static String getPluginBasePath() { return pluginBasePath; } /** * 获取配置的插件路径(外部目录优先) */ public static String getConfiguredPluginPath() { // 1. 检查系统属性 String sysPropPath = System.getProperty("kettle.plugin.path"); if (sysPropPath != null && Files.exists(Paths.get(sysPropPath))) { return sysPropPath; } // 2. 检查当前工作目录下的插件目录 Path workingDirPath = Paths.get(System.getProperty("user.dir"), PLUGIN_DIR); if (Files.exists(workingDirPath)) { return workingDirPath.toString(); } // 3. 检查JAR文件同级的插件目录 Path jarDirPath = null; try { jarDirPath = Paths.get(KettleUtil.class.getProtectionDomain() .getCodeSource().getLocation().toURI()) .getParent() .resolve(PLUGIN_DIR); } catch (URISyntaxException e) { throw new RuntimeException(e); } if (Files.exists(jarDirPath)) { return jarDirPath.toString(); } return null; } /** * 解析插件路径(支持JAR包和IDE环境) */ private static String resolvePluginPath() throws Exception { // 优先返回已提取的路径(避免重复提取) if (extractedPluginPath != null) { return extractedPluginPath.toString(); } URL resourceUrl = KettleUtil.class.getClassLoader().getResource(PLUGIN_DIR); if (resourceUrl == null) { throw new FileNotFoundException("未找到Kettle插件资源目录"); } // 开发环境直接使用路径 if (!"jar".equals(resourceUrl.getProtocol())) { // 同时提取JNDI目录 extractJndiFromResources(); return Paths.get(resourceUrl.toURI()).toString(); } // 生产环境提取到临时目录 synchronized (KettleEnv.class) { if (extractedPluginPath == null) { extractedPluginPath = extractPluginsFromJar(resourceUrl); // 同时提取JNDI目录 extractJndiFromJar(); } return extractedPluginPath.toString(); } } /** * 从JAR包提取插件到临时目录(优化版) */ private static Path extractPluginsFromJar(URL jarResourceUrl) throws Exception { // 创建临时目录(带随机后缀) Path tempDir = Files.createTempDirectory("kettle-plugins-"); tempDir.toFile().deleteOnExit(); log.info("创建临时插件目录: {}", tempDir); // 解析JAR文件路径 String jarPath = jarResourceUrl.getPath().split("!")[0].replace("file:", ""); File jarFile = new File(jarPath); try (JarFile jar = new JarFile(jarFile)) { Enumeration<JarEntry> entries = jar.entries(); while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); String entryName = entry.getName(); // 只处理插件目录下的文件 if (entryName.startsWith(PLUGIN_DIR + "/") && !entry.isDirectory()) { // 计算目标路径(去掉插件目录前缀) String relativePath = entryName.substring(PLUGIN_DIR.length() + 1); Path targetPath = tempDir.resolve(relativePath); // 创建父目录 Files.createDirectories(targetPath.getParent()); // 复制文件内容 try (InputStream in = jar.getInputStream(entry)) { Files.copy(in, targetPath, StandardCopyOption.REPLACE_EXISTING); log.debug("提取插件文件: {} -> {}", entryName, targetPath); } } } } log.info("成功提取 {} 个插件文件到临时目录", Files.list(tempDir).count()); return tempDir; } /** * 从JAR包提取JNDI配置到临时目录 */ private static void extractJndiFromJar() throws Exception { URL jndiResourceUrl = KettleUtil.class.getClassLoader().getResource(JNDI_DIR); if (jndiResourceUrl == null) { log.warn("未找到JNDI资源目录"); return; } // 创建临时目录(带随机后缀) Path tempDir = Files.createTempDirectory("kettle-jndi-"); tempDir.toFile().deleteOnExit(); log.info("创建临时JNDI目录: {}", tempDir); // 解析JAR文件路径 String jarPath = jndiResourceUrl.getPath().split("!")[0].replace("file:", ""); File jarFile = new File(jarPath); try (JarFile jar = new JarFile(jarFile)) { Enumeration<JarEntry> entries = jar.entries(); while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); String entryName = entry.getName(); // 只处理JNDI目录下的文件 if (entryName.startsWith(JNDI_DIR + "/") && !entry.isDirectory()) { // 计算目标路径(去掉JNDI目录前缀) String relativePath = entryName.substring(JNDI_DIR.length() + 1); Path targetPath = tempDir.resolve(relativePath); // 创建父目录 Files.createDirectories(targetPath.getParent()); // 复制文件内容 try (InputStream in = jar.getInputStream(entry)) { Files.copy(in, targetPath, StandardCopyOption.REPLACE_EXISTING); log.debug("提取JNDI文件: {} -> {}", entryName, targetPath); } } } } extractedJndiPath = tempDir; log.info("成功提取JNDI配置到临时目录: {}", tempDir); } /** * 在开发环境中提取JNDI配置 */ private static void extractJndiFromResources() throws Exception { URL jndiResourceUrl = KettleUtil.class.getClassLoader().getResource(JNDI_DIR); if (jndiResourceUrl == null) { log.warn("未找到JNDI资源目录"); return; } // 开发环境直接使用资源路径 extractedJndiPath = Paths.get(jndiResourceUrl.toURI()); log.info("使用开发环境JNDI目录: {}", extractedJndiPath); } /** * 重新加载Kettle环境(包括JNDI配置) */ public static synchronized void reload() { try { // 停止所有正在运行的作业 KettleJobManager.stopAllJobs(); // 1. 关闭当前Kettle环境 KettleEnvironment.shutdown(); log.info("Kettle环境已关闭"); // 3. 重新初始化 init(); log.info("Kettle环境重新初始化完成"); } catch (Exception e) { log.error("Kettle环境重新加载失败", e); throw new RuntimeException(e); } } } // 保存配置内容(保留原始格式) @PostMapping("/saveConfig") @ResponseBody public AjaxResult saveConfig(@RequestParam String content) { try { // 直接保存原始内容,解析为Properties KettleEnv.writeJdbcPropertiesFromString(content); // 重新加载Kettle环境 KettleEnv.reload(); return success("配置保存成功"); } catch (Exception e) { return error("保存配置失败: " + e.getMessage()); } } 保存配置时把原文件也修改 ruoyi-admin/src/main/resources/simple-jndi/jdbc.properties ruoyi-admin/target/classes/simple-jndi/jdbc.properties
最新发布
09-17
<think> 我们有两个文件路径需要修改: 1. ruoyi-admin/src/main/resources/simple-jndi/jdbc.properties (源文件) 2. ruoyi-admin/target/classes/simple-jndi/jdbc.properties (编译后的文件) 在保存配置时,我们需要同时更新这两个文件。由于项目运行中,target/classes下的文件可能是从src/main/resources复制过去的(在编译或运行时),所以我们需要注意: - 在开发环境下,我们可能直接修改src/main/resources下的文件,然后项目重新编译或重启时会复制到target/classes。 - 但是,如果我们在运行时需要动态更新配置,并且希望立即生效,那么就需要同时修改这两个文件(因为运行时程序读取的可能是target/classes下的文件)。 因此,在保存配置的方法中,我们应该: 1. 将新的配置内容写入到源文件(src/main/resources/simple-jndi/jdbc.properties)中,这样下次编译时也会包含这个更新。 2. 同时,将同样的内容写入到target/classes下的对应文件(ruoyi-admin/target/classes/simple-jndi/jdbc.properties)中,使得当前运行的应用能够立即使用新的配置。 注意:由于路径中的“ruoyi-admin”是模块名,实际在项目中,我们可能需要根据项目的实际路径来定位。在代码中,我们可以通过获取类路径或者资源路径的方式来定位到target/classes下的文件,而源文件的位置则需要根据项目结构来确定(因为源文件在src/main/resources,在类路径下?实际上,在开发时,src/main/resources下的文件在编译后会被复制到target/classes,所以我们在代码中可能只能通过文件系统的绝对路径来访问src/main/resources下的文件)。 假设我们的项目结构是标准的Maven项目,并且当前工作目录是项目的根目录(比如ruoyi项目的根目录),那么: 源文件路径:ruoyi-admin/src/main/resources/simple-jndi/jdbc.properties target文件路径:ruoyi-admin/target/classes/simple-jndi/jdbc.properties 但是,在代码中,我们能硬编码这样的路径,因为项目部署的位置可能同。因此,我们可以考虑以下方法: 对于target/classes下的文件,我们可以通过类加载器来获取资源,然后得到其真实路径(但是注意,如果是在jar包中运行,则能修改,因为jar包是只读的。但是这里我们是在开发环境,所以通常是解压的目录,所以可以直接修改文件)。然而,这里我们明确知道文件在文件系统中的位置(target/classes),所以我们可以根据当前运行环境的类路径来推断。 但是,更简单且可靠的做法是:在配置文件(比如application.yml)中指定这两个文件的路径,然后在代码中读取配置的路径。或者,我们可以通过系统属性(如user.dir)来获取项目根目录,然后拼接相对路径。 然而,在Ruoyi框架中,KettleEnv类可能已经有一些关于路径的配置。我们需要参考Ruoyi的原有设计。 另一种思路:在保存配置时,我们只更新源文件(src/main/resources/simple-jndi/jdbc.properties),然后通过触发重新加载类路径(比如重启应用)来使得target/classes下的文件更新。但是,这样能立即生效,而且重启应用可能是用户期望的。 因此,我们选择同时写入两个文件。 具体步骤: 1. 获取项目根路径。可以通过System.getProperty("user.dir")获取当前工作目录(通常是项目根目录)。 2. 拼接源文件路径:rootPath + "/ruoyi-admin/src/main/resources/simple-jndi/jdbc.properties" 3. 拼接target文件路径:rootPath + "/ruoyi-admin/target/classes/simple-jndi/jdbc.properties" 4. 将新的配置内容同时写入这两个文件。 注意:写入文件时,如果目录存在,需要创建目录;如果文件存在,则创建文件。 但是,在打包部署的环境下,可能存在src/main/resources目录(因为部署时只有jar包和配置文件的外部目录),所以我们需要判断文件是否存在,如果存在则跳过写入源文件,只写入target/classes下的文件?或者,在部署环境下,我们可能只希望更新外部配置文件(在src/main/resources,而是在外部配置目录)。因此,我们需要设计得更灵活。 但是,根据问题描述,用户明确要求修改这两个文件(ruoyi-admin/src/main/resources和ruoyi-admin/target/classes下的文件),所以我们假设这是在开发环境中,并且两个文件都存在。 在KettleEnv类中,我们可以这样实现保存配置的方法(伪代码): ```java public void saveJdbcProperties(String content) { // 获取项目根路径 String rootPath = System.getProperty("user.dir"); // 定义两个文件路径 String srcFilePath = rootPath + "/ruoyi-admin/src/main/resources/simple-jndi/jdbc.properties"; String targetFilePath = rootPath + "/ruoyi-admin/target/classes/simple-jndi/jdbc.properties"; // 写入源文件 writeFile(srcFilePath, content); // 写入target文件 writeFile(targetFilePath, content); } private void writeFile(String filePath, String content) { File file = new File(filePath); // 如果目录存在,创建目录 File parent = file.getParentFile(); if (parent != null && !parent.exists()) { parent.mkdirs(); } try (FileOutputStream fos = new FileOutputStream(file); OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8); BufferedWriter writer = new BufferedWriter(osw)) { writer.write(content); } catch (IOException e) { e.printStackTrace(); // 处理异常 } } ``` 但是,这里有几个需要注意的点: 1. 项目根路径的获取:在IDE中运行user.dir通常是项目根目录(即包含ruoyi-admin、ruoyi-common等模块的目录)。但是,如果是在Tomcat等容器中运行user.dir可能是Tomcat的bin目录。因此,这种方法在开发环境可以,但在生产环境可能适用。 2. 用户的需求是修改这两个文件,所以我们应该确保这两个文件都存在。如果存在,则创建(如果是生产环境,可能没有src目录,那么我们就只写入target/classes下的文件?但是问题要求同时修改两个文件,所以我们假设在开发环境)。 3. 在Ruoyi框架中,可能已经提供了获取配置路径的方法,我们可以参考已有的配置。 考虑到Ruoyi框架的实际情况,它使用Spring Boot,而Spring Boot的配置文件通常放在classpath下(即target/classes)。但是,在开发时,我们修改src/main/resources下的文件,然后重新编译就会覆盖target/classes。所以,在开发环境,我们同时修改两个文件;在生产环境,我们可能只修改外部配置文件(因为src/main/resources在生产环境存在)。 但是,问题中明确要求修改这两个路径,所以我们假设开发环境。另外,如果生产环境存在src/main/resources,那么我们可以跳过该文件的写入(如果写入失败,记录日志)。 因此,我们修改上面的方法,加入文件存在性的检查,如果文件存在,则跳过(或者只写入存在的目标?但是用户要求同时修改两个,所以我们要写入,即使文件存在,我们也创建。注意:在生产环境,我们可能没有权限在项目根目录下创建src/main/resources目录,所以需要谨慎)。 另一种更安全的方式:我们只修改存在的文件。对于源文件,如果路径存在就修改,存在就忽略;对于target文件,总是修改(因为运行时需要)。 所以,我们可以这样: ```java public void saveJdbcProperties(String content) { String rootPath = System.getProperty("user.dir"); String srcFilePath = rootPath + "/ruoyi-admin/src/main/resources/simple-jndi/jdbc.properties"; String targetFilePath = rootPath + "/ruoyi-admin/target/classes/simple-jndi/jdbc.properties"; // 写入源文件(如果该文件所在的目录存在,或者我们可以尝试创建目录,但创建目录可能失败) writeFile(srcFilePath, content); // 写入target文件(通常这个文件在运行中是存在的,因为项目启动时已经复制了资源文件) writeFile(targetFilePath, content); } ``` 然后,在writeFile方法中,我们尝试创建文件(包括目录)。 但是,由于生产环境可能没有src目录,所以我们需要在写入之前判断父目录是否存在?如果存在,则跳过该文件?但是用户要求两个文件都要修改,所以如果有一个文件存在,我们也要创建它吗?问题中要求修改原文件,如果原文件存在,那么创建它就是修改?还是说应该先存在? 根据用户描述,这两个文件是原文件(应该是已经存在的),所以我们假设它们都存在。如果存在,可能是首次初始化,那么我们就创建。 因此,我们按照创建文件的方式写入即可。 但是,考虑到生产环境部署方式(比如使用jar包运行),target/classes下的文件实际上是在jar包内,我们无法直接修改。所以,这种方法只适用于开发环境。 所以,我们需要明确:这个功能只在开发环境下使用,还是生产环境下也需要?根据Ruoyi系统的使用场景,保存配置应该是管理员在后台操作,所以应该同时支持开发和生产。 在生产环境,我们通常会把src/main/resources放在部署目录下,而是将配置文件放在外部目录(比如通过spring.config.location指定)。同时,target/classes下的文件被打包在jar内,是可写的。 因此,我们需要调整策略: - 对于开发环境:修改两个文件(src/main/resources和target/classes)。 - 对于生产环境:修改外部配置文件(比如application.yml中指定的配置文件路径)或者只修改外部化的jdbc.properties(如果simple-jndi目录在外部)。 所以,更合理的做法是:在配置文件中指定需要更新的jdbc.properties的路径(多个),然后在保存时更新这些路径。 但是,由于问题中明确要求修改那两个路径,并且用户可能是在开发环境使用,我们可以先按照开发环境的方式实现,同时预留扩展性。 在KettleEnv类中,我们可以注入一个配置项,用来指定需要更新的jdbc.properties的路径列表。如果没有配置,则使用默认的两个路径(即开发环境的两个路径)。 过,由于问题中没有提及生产环境,我们就按照开发环境的要求实现。 另外,在Ruoyi框架中,KettleEnv类可能是一个Spring管理的Bean,我们可以通过@Value来获取配置文件中的路径列表,但是这里为了简单,我们直接使用固定路径(开发环境)。 因此,我们按照之前的方式实现,但需要注意在生产环境可能无法写入,所以需要捕获异常并记录日志。 总结:在KettleEnv类的保存配置方法中,我们将给定的内容同时写入到两个文件中(路径固定为项目根目录下的两个路径),并确保目录存在,文件被创建或覆盖。 最后,注意文件编码(UTF-8)。 代码示例(在KettleEnv类中添加方法): </think> 在 RuoYi 框架中同时修改源文件和编译后文件,需要在保存配置方法中实现双重写入逻辑。以下是具体实现方案: ### 实现步骤 1. **获取文件路径**:确定两个文件的绝对路径 2. **双重写入**:将配置内容同时写入两个目标文件 3. **异常处理**:确保文件操作安全性 ```java import org.springframework.util.FileCopyUtils; import java.io.File; import java.nio.charset.StandardCharsets; public class KettleEnv { public void saveConfig(String configContent) { // 1. 定义两个目标文件路径 String srcPath = "ruoyi-admin/src/main/resources/simple-jndi/jdbc.properties"; String targetPath = "ruoyi-admin/target/classes/simple-jndi/jdbc.properties"; try { // 2. 同时写入源文件和编译文件 writeFile(srcPath, configContent); writeFile(targetPath, configContent); // 3. 日志记录 System.out.println("配置文件更新成功:" + srcPath); System.out.println("配置文件更新成功:" + targetPath); } catch (Exception e) { // 4. 异常处理 System.err.println("配置更新失败: " + e.getMessage()); } } private void writeFile(String path, String content) throws IOException { File file = new File(path); // 确保目录存在 file.getParentFile().mkdirs(); // 使用UTF-8编码写入 FileCopyUtils.copy(content.getBytes(StandardCharsets.UTF_8), file); } } ``` ### 关键说明 1. **路径处理**: - 源文件路径:`src/main/resources` 存放原始配置文件 - 编译文件路径:`target/classes` 是 Maven 编译后的资源目录 - 使用 `mkdirs()` 确保目录树存在[^2] 2. **编码保障**: - 明确指定 UTF-8 编码避免乱码问题 - 使用 `FileCopyUtils` 简化文件操作 3. **实时生效**: - 同时修改两个文件确保: - 源码版本控制包含最新配置 - 运行时应用立即加载新配置[^1] ### 注意事项 1. 生产环境部署时需确认 `target/classes` 路径是否可写 2. 如需动态加载配置,可结合 `@RefreshScope` 实现热更新 3. 文件操作需添加适当的权限检查 ---
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值