C#配置全面详解:从传统方式到现代配置系统

C#配置全面详解:从传统方式到现代配置系统

在软件开发中,配置是指应用程序运行时可调整的参数集合,如数据库连接字符串、API 地址、日志级别等。将这些参数从代码中分离出来,便于在不修改代码的情况下调整应用行为。C# 提供了多种配置管理方式,从传统的 XML 配置文件到现代的多源配置系统,每种方式都有其适用场景。本文将全面介绍 C# 中的配置技术,帮助开发者根据项目需求选择合适的配置方案。

一、配置基础与核心概念

1. 为什么需要配置管理

硬编码配置存在诸多问题:

  • 修改配置需要重新编译代码
  • 不同环境(开发、测试、生产)需要不同配置时难以维护
  • 敏感信息(如密码、密钥)暴露在代码中不安全

良好的配置管理应具备:

  • 易于修改,无需重新编译
  • 支持不同环境的配置隔离
  • 能保护敏感信息
  • 便于扩展和维护

2. C# 配置技术演进

C# 配置技术经历了多个阶段:

  • 传统方式.NET Framework 中的app.configweb.config
  • 现代方式.NET Core引入的新配置系统,支持多源配置、依赖注入等
  • 云原生方式:结合环境变量、服务发现、配置中心等

二、.NET Framework传统配置方式

1. 配置文件结构

.NET Framework应用使用 XML 格式的配置文件:

  • 控制台 / 桌面应用:app.config(编译后生成[程序名].exe.config
  • Web 应用:web.config

典型的app.config结构:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 <!-- 应用设置 -->
 <appSettings>
   <add key="MaxRetries" value="3" />
   <add key="LogLevel" value="Info" />
   <add key="ApiUrl" value="https://api.example.com" />
 </appSettings>

 <!-- 连接字符串 -->
 <connectionStrings>
   <add name="DefaultConnection"
        connectionString="Server=localhost;Database=Test;Integrated Security=True"
        providerName="System.Data.SqlClient" />
 </connectionStrings>

 <!-- 其他配置节 -->
 <system.web>
   <!-- Web相关配置 -->
 </system.web>

</configuration>

2. 读取配置(ConfigurationManager)

使用System.Configuration命名空间下的ConfigurationManager类读取配置,需引用System.Configuration程序集。

using System;
using System.Configuration;

class TraditionalConfigDemo
{
   static void Main()
   {
       // 读取appSettings配置
       string maxRetries = ConfigurationManager.AppSettings["MaxRetries"];
       string logLevel = ConfigurationManager.AppSettings["LogLevel"];
       string apiUrl = ConfigurationManager.AppSettings["ApiUrl"];

       Console.WriteLine($"最大重试次数: {maxRetries}");
       Console.WriteLine($"日志级别: {logLevel}");
       Console.WriteLine($"API地址: {apiUrl}");

       // 读取连接字符串
       ConnectionStringSettings connectionString = ConfigurationManager.ConnectionStrings["DefaultConnection"];
       if (connectionString != null)
       {
           Console.WriteLine($"连接字符串: {connectionString.ConnectionString}");
           Console.WriteLine($"提供程序: {connectionString.ProviderName}");
       }
   }
}

3. 自定义配置节

对于复杂配置,可以定义自定义配置节:

<!-- 配置文件中定义自定义配置节 -->
<configuration>
 <!-- 注册自定义配置节 -->
 <configSections>
   <section name="EmailSettings" type="ConfigDemo.EmailSettingsSection, ConfigDemo" />
 </configSections>

 <!-- 自定义配置内容 -->
 <EmailSettings>
   <SmtpServer address="smtp.example.com" port="587" />
   <Credentials username="user@example.com" password="password" />
   <Options enableSsl="true" timeout="30000" />
 </EmailSettings>
</configuration>

对应的 C# 类:

using System.Configuration;

// 自定义配置节
public class EmailSettingsSection : ConfigurationSection
{
   [ConfigurationProperty("SmtpServer")]
   public SmtpServerElement SmtpServer => (SmtpServerElement)this["SmtpServer"];

   [ConfigurationProperty("Credentials")]
   public CredentialsElement Credentials => (CredentialsElement)this["Credentials"];

   [ConfigurationProperty("Options")]
   public OptionsElement Options => (OptionsElement)this["Options"];
}


// 配置元素
public class SmtpServerElement : ConfigurationElement
{
   [ConfigurationProperty("address", IsRequired = true)]
   public string Address => (string)this["address"];

   [ConfigurationProperty("port", DefaultValue = 25)]
   public int Port => (int)this["port"];
}

public class CredentialsElement : ConfigurationElement
{
   [ConfigurationProperty("username", IsRequired = true)]
   public string Username => (string)this["username"];

   [ConfigurationProperty("password", IsRequired = true)]
   public string Password => (string)this["password"];
}


public class OptionsElement : ConfigurationElement
{
   [ConfigurationProperty("enableSsl", DefaultValue = false)]
   public bool EnableSsl => (bool)this["enableSsl"];

   [ConfigurationProperty("timeout", DefaultValue = 10000)]
   public int Timeout => (int)this["timeout"];
}


// 读取自定义配置节
class CustomConfigDemo
{
   static void Main()
   {
       EmailSettingsSection emailSettings =
           (EmailSettingsSection)ConfigurationManager.GetSection("EmailSettings");

       if (emailSettings != null)
       {
           Console.WriteLine($"SMTP服务器: {emailSettings.SmtpServer.Address}:{emailSettings.SmtpServer.Port}");
           Console.WriteLine($"用户名: {emailSettings.Credentials.Username}");
           Console.WriteLine($"启用SSL: {emailSettings.Options.EnableSsl}");
       }
   }
}

三、.NET Core/.NET 5 + 现代配置系统

.NET Core 引入了全新的配置系统,具有以下特点:

  • 支持多种配置源(JSON、XML、INI、环境变量、命令行等)
  • 配置值可以被后续源覆盖(配置优先级)
  • 支持配置绑定到实体类
  • 集成依赖注入系统
  • 支持配置热重载

1. 配置源与优先级

默认配置源及优先级(从低到高):

  1. appsettings.json
  2. appsettings.[Environment].json(如appsettings.Development.json
  3. 用户 Secrets(仅开发环境)
  4. 环境变量
  5. 命令行参数

2. 控制台应用中的配置

  • 基本使用
  1. 创建appsettings.json文件(设置 “复制到输出目录” 为 “如果较新则复制”):

    {
    "AppSettings": {
    "MaxRetries": 3,
    "LogLevel": "Info",
    "ApiUrl": "https://api.example.com"
    },
    "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=Test;Integrated Security=True"
    },
    "EmailSettings": {
    "SmtpServer": {
        "Address": "smtp.example.com",
        "Port": 587
    },
    "Credentials": {
        "Username": "user@example.com",
        "Password": "password"
    },
    "Options": {
        "EnableSsl": true,
        "Timeout": 30000
    }
    }
    }
    
  2. 安装必要的 NuGet 包:

    Install-Package Microsoft.Extensions.Configuration
    Install-Package Microsoft.Extensions.Configuration.Json
    Install-Package Microsoft.Extensions.Configuration.EnvironmentVariables
    Install-Package Microsoft.Extensions.Configuration.CommandLine
    
  3. 读取配置:

    using System;
    using Microsoft.Extensions.Configuration;
    using System.IO;
    
    class ConsoleConfigDemo
    {
    static void Main(string[] args)
    {
        // 构建配置
        IConfiguration config = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
            .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true)
            .AddEnvironmentVariables()
            .AddCommandLine(args)
            .Build();
    
        // 直接读取配置
        string maxRetries = config["AppSettings:MaxRetries"];
        string apiUrl = config["AppSettings:ApiUrl"];
        string connectionString = config["ConnectionStrings:DefaultConnection"];
    
        Console.WriteLine($"最大重试次数: {maxRetries}");
        Console.WriteLine($"API地址: {apiUrl}");
        Console.WriteLine($"连接字符串: {connectionString}");
    
        // 读取嵌套配置
        string smtpAddress = config["EmailSettings:SmtpServer:Address"];
        int smtpPort = int.Parse(config["EmailSettings:SmtpServer:Port"]);
        Console.WriteLine($"SMTP服务器: {smtpAddress}:{smtpPort}");
    }
    }
    

3. 配置绑定到实体类

将配置绑定到实体类更便于使用:

using Microsoft.Extensions.Configuration;

// 定义实体类
public class AppSettings
{
   public int MaxRetries { get; set; }
   public string LogLevel { get; set; }
   public string ApiUrl { get; set; }
}

public class ConnectionStrings
{
   public string DefaultConnection { get; set; }
}

public class SmtpServerSettings
{
   public string Address { get; set; }
   public int Port { get; set; }
}


public class CredentialsSettings
{
   public string Username { get; set; }
   public string Password { get; set; }
}


public class EmailOptions
{
   public bool EnableSsl { get; set; }
   public int Timeout { get; set; }
}


public class EmailSettings
{
   public SmtpServerSettings SmtpServer { get; set; }
   public CredentialsSettings Credentials { get; set; }
   public EmailOptions Options { get; set; }
}


// 绑定并使用配置
class ConfigBindingDemo
{
   static void Main(string[] args)
   {
       IConfiguration config = new ConfigurationBuilder()
           .SetBasePath(Directory.GetCurrentDirectory())
           .AddJsonFile("appsettings.json")
           .Build();


       // 绑定到实体类
       AppSettings appSettings = new AppSettings();
       config.GetSection("AppSettings").Bind(appSettings);

       ConnectionStrings connectionStrings = new ConnectionStrings();
       config.GetSection("ConnectionStrings").Bind(connectionStrings);

       EmailSettings emailSettings = config.GetSection("EmailSettings").Get<EmailSettings>(); // 另一种绑定方式

       // 使用绑定后的配置
       Console.WriteLine($"最大重试次数: {appSettings.MaxRetries}");
       Console.WriteLine($"连接字符串: {connectionStrings.DefaultConnection}");
       Console.WriteLine($"SMTP服务器: {emailSettings.SmtpServer.Address}:{emailSettings.SmtpServer.Port}");
       Console.WriteLine($"启用SSL: {emailSettings.Options.EnableSsl}");
   }
}

4. ASP.NET Core 中的配置

ASP.NET Core 自动构建配置系统,可直接注入IConfiguration使用:

// 在Program.cs中(ASP.NET Core 6+)
var builder = WebApplication.CreateBuilder(args);

// 配置已自动加载,可在此处添加额外配置源
builder.Configuration.AddIniFile("appsettings.ini", optional: true);

var app = builder.Build();

// 在控制器中使用
app.MapGet("/config", (IConfiguration config) =>
{
   var logLevel = config["AppSettings:LogLevel"];
   var connectionString = config["ConnectionStrings:DefaultConnection"];
   return new
   {
       LogLevel = logLevel,
       ConnectionString = connectionString
   };
});

app.Run();

在控制器中使用:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;

public class ConfigController : Controller
{
   private readonly IConfiguration _config;

   // 注入IConfiguration
   public ConfigController(IConfiguration config)
   {
       _config = config;
   }

   public IActionResult Index()
   {
       string apiUrl = _config["AppSettings:ApiUrl"];

       // 使用配置...

       return View();
   }

}

四、选项模式(Options Pattern)

选项模式是.NET Core推荐的配置使用方式,通过强类型访问配置,提供更好的封装和可测试性。

1. 基本使用

  • 定义选项类:

    using Microsoft.Extensions.Options;
    
    // 选项类
    public class AppSettingsOptions
    {
    public const string AppSettings = "AppSettings";
    public int MaxRetries { get; set; }
    public string LogLevel { get; set; }
    public string ApiUrl { get; set; }
    }
    
    public class EmailSettingsOptions
    {
    public const string EmailSettings = "EmailSettings";
    public SmtpServerSettings SmtpServer { get; set; }
    public CredentialsSettings Credentials { get; set; }
    public EmailOptions Options { get; set; }
    }
    
  • 在依赖注入中配置选项:

    // 控制台应用
    var builder = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json");
    
    IConfiguration config = builder.Build();
    
    // 创建服务集合
    var services = new ServiceCollection();
    
    // 配置选项
    services.Configure<AppSettingsOptions>(config.GetSection(AppSettingsOptions.AppSettings));
    services.Configure<EmailSettingsOptions>(config.GetSection(EmailSettingsOptions.EmailSettings));
    
    // 注册需要使用选项的服务
    services.AddSingleton<MyService>();
    
    // 构建服务提供器
    using (var serviceProvider = services.BuildServiceProvider())
    {
    var myService = serviceProvider.GetRequiredService<MyService>();
    myService.DoWork();
    }
    
  • 使用选项:

    public class MyService
    {
    private readonly AppSettingsOptions _appSettings;
    private readonly IOptions<EmailSettingsOptions> _emailSettings;
    
    // 注入选项
    public MyService(
        IOptions<AppSettingsOptions> appSettings,
        IOptions<EmailSettingsOptions> emailSettings)
    {
        _appSettings = appSettings.Value;
        _emailSettings = emailSettings;
    }
    
    public void DoWork()
    {
        Console.WriteLine($"最大重试次数: {_appSettings.MaxRetries}");
        Console.WriteLine($"SMTP服务器: {_emailSettings.Value.SmtpServer.Address}");
    }
    }
    

2. 三种选项接口

.NET Core提供三种选项接口,适用于不同场景:

  • IOptions

    • 单例模式,应用启动时初始化
    • 不支持配置热重载
    • 适用于启动后不会变化的配置
  • IOptionsSnapshot

    • 作用域模式,每个请求 / 作用域重新计算
    • 支持配置热重载
    • 适用于 Web 应用,每个请求可能需要最新配置
  • IOptionsMonitor

    • 单例模式,但支持配置热重载
    • 可通过OnChange方法监听配置变化
    • 适用于长时间运行的服务,需要实时响应配置变化
// 使用IOptionsMonitor监听配置变化
public class MonitorService
{
   private readonly IOptionsMonitor<AppSettingsOptions> _monitor;
   private IDisposable _changeToken;

   public MonitorService(IOptionsMonitor<AppSettingsOptions> monitor)
   {
       _monitor = monitor;

       // 监听配置变化
       _changeToken = _monitor.OnChange((newValue, name) =>
       {
           Console.WriteLine($"配置已变化: 新的最大重试次数 {newValue.MaxRetries}");
       });
   }

   public void ShowSettings()
   {
       Console.WriteLine($"当前日志级别: {_monitor.CurrentValue.LogLevel}");
   }

   // 清理资源
   public void Dispose()
   {
       _changeToken?.Dispose();
   }
}

五、其他配置源

1. 环境变量

环境变量是容器化部署(如 Docker、Kubernetes)中常用的配置方式:

// 添加环境变量配置源
var config = new ConfigurationBuilder()
   .AddEnvironmentVariables()
   .Build();


// 读取环境变量
// 环境变量名通常使用下划线分隔,如AppSettings__MaxRetries对应配置中的AppSettings:MaxRetries
string maxRetries = config["AppSettings__MaxRetries"];

在 Docker 中设置环境变量:

ENV AppSettings__MaxRetries=5

ENV ConnectionStrings__DefaultConnection="Server=db;Database=Test;User Id=sa;Password=password"

2. 命令行参数

命令行参数可用于临时覆盖配置:

// 添加命令行配置源
var config = new ConfigurationBuilder()
   .AddCommandLine(args)
   .Build();

运行程序时传递参数:

dotnet MyApp.dll --AppSettings:MaxRetries 5 --ConnectionStrings:DefaultConnection "Server=..."

3. INI 文件

INI 文件适合简单的键值对配置:

; appsettings.ini

[AppSettings]
MaxRetries=3
LogLevel=Info
ApiUrl=https://api.example.com

[ConnectionStrings]
DefaultConnection=Server=localhost;Database=Test;Integrated Security=True

读取 INI 文件:

var config = new ConfigurationBuilder()
   .AddIniFile("appsettings.ini", optional: true, reloadOnChange: true)
   .Build();

4. XML 文件

除了传统的app.config,新配置系统也支持读取 XML 文件:

<!-- appsettings.xml -->
<configuration>
 <AppSettings>
   <MaxRetries>3</MaxRetries>
   <LogLevel>Info</LogLevel>
 </AppSettings>
 <ConnectionStrings>
   <DefaultConnection>Server=localhost;Database=Test;</DefaultConnection>
 </ConnectionStrings>
</configuration>

读取 XML 文件:

var config = new ConfigurationBuilder()
   .AddXmlFile("appsettings.xml", optional: true, reloadOnChange: true)
   .Build();

六、配置高级特性

1. 配置热重载

配置热重载允许应用在不重启的情况下读取更新后的配置:

// 启用热重载
var config = new ConfigurationBuilder()
   .AddJsonFile("appsettings.json", reloadOnChange: true) // 启用热重载
   .Build();


// 监控配置变化
var changeToken = config.GetReloadToken();
changeToken.RegisterChangeCallback(state =>
{
   Console.WriteLine("配置已更新!");

   // 重新获取配置
   // ...

}, null);

ASP.NET Core 中使用IOptionsSnapshotIOptionsMonitor自动获取热重载的配置。

2. 开发环境用户密钥

为避免将开发环境的敏感信息提交到代码库,可使用用户密钥(User Secrets):

  • 初始化用户密钥(在项目目录执行):
    dotnet user-secrets init
    
  • 设置密钥:
    dotnet user-secrets set "Credentials:Password" "dev-password"
    
  • 在代码中使用:
    // 自动读取用户密钥(仅在开发环境生效)
    var config = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json")
    .AddUserSecrets<Program>() // 传入任意类型以确定项目
    .Build();
    
    string password = config["Credentials:Password"];
    

3. 敏感配置加密

敏感信息(如密码、API 密钥)不应明文存储,可使用 DPAPI 或 Azure Key Vault 等进行加密。

  • 使用 DataProtection 加密配置

    using Microsoft.AspNetCore.DataProtection;
    using Microsoft.Extensions.DependencyInjection;
    
    // 加密配置
    var serviceCollection = new ServiceCollection();
    serviceCollection.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo(@"C:keys"))
    .SetApplicationName("MyApp");
    
    
    var services = serviceCollection.BuildServiceProvider();
    var protector = services.GetDataProtectionProvider().CreateProtector("ConfigProtection");
    
    
    // 加密
    string plainText = "sensitive-password";
    string encryptedText = protector.Protect(plainText);
    
    
    // 解密
    string decryptedText = protector.Unprotect(encryptedText);
    
  • Azure Key Vault
    对于云部署,推荐使用 Azure Key Vault 存储敏感配置:

// 安装包:Install-Package Azure.Extensions.AspNetCore.Configuration.Secrets
var config = new ConfigurationBuilder()
   .AddAzureKeyVault(new Uri("https://myvault.vault.azure.net/"),
                     new DefaultAzureCredential())
   .Build();

七、多环境配置管理

应用在不同环境(开发、测试、生产)通常需要不同配置,.NET Core 提供了环境区分机制。

1. 环境变量指定环境

通过ASPNETCORE_ENVIRONMENT(Web 应用)或DOTNET_ENVIRONMENT(控制台应用)环境变量指定当前环境:

# 开发环境
set ASPNETCORE_ENVIRONMENT=Development

# 生产环境
set ASPNETCORE_ENVIRONMENT=Production

在 Docker 中设置:

ENV ASPNETCORE_ENVIRONMENT=Production

2. 环境特定配置文件

创建环境特定的配置文件,命名格式为appsettings.[Environment].json

  • appsettings.Development.json:开发环境配置
  • appsettings.Test.json:测试环境配置
  • appsettings.Production.json:生产环境配置

配置文件加载顺序:

  1. appsettings.json(基础配置)
  2. appsettings.[Environment].json(环境特定配置,覆盖基础配置)
// 加载环境特定配置
var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production";
var config = new ConfigurationBuilder()
   .AddJsonFile("appsettings.json", optional: false)
   .AddJsonFile($"appsettings.{env}.json", optional: true)
   .Build();

3. ASP.NET Core 中配置多环境

ASP.NET Core 自动处理多环境配置,可在Program.cs中针对不同环境进行配置:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// 根据环境配置中间件
if (app.Environment.IsDevelopment())
{
   app.UseDeveloperExceptionPage(); // 开发环境显示详细错误页
}
else
{
   app.UseExceptionHandler("/Error"); // 生产环境使用自定义错误页
   app.UseHsts(); // 生产环境启用HSTS
}

// 其他中间件配置
app.UseHttpsRedirection();
app.UseStaticFiles();

// ...
app.Run();

八、最佳实践与常见问题

1. 最佳实践

  • 分离配置与代码:所有可配置参数都应放在配置文件中,避免硬编码
  • 敏感信息保护:密码、密钥等敏感信息应加密存储或使用环境变量、密钥管理服务
  • 使用选项模式:优先使用IOptions<T>而非直接访问IConfiguration,提高可测试性
  • 合理组织配置结构:按功能模块划分配置节点,如DatabaseLoggingExternalServices
  • 配置验证:对配置进行验证,确保应用启动时所有必要配置都已正确设置
// 配置验证示例(使用DataAnnotations)
public class AppSettingsOptions : IValidatableObject
{
   [Required]
   public int MaxRetries { get; set; }

   [Required]
   [Url]
   public string ApiUrl { get; set; }

   public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
   {
       if (MaxRetries < 0 || MaxRetries > 10)
       {
           yield return new ValidationResult(
               "最大重试次数必须在0-10之间",
               new[] { nameof(MaxRetries) });
       }
   }
}


// 在依赖注入中启用验证
services.AddOptions<AppSettingsOptions>()
   .Bind(config.GetSection(AppSettingsOptions.AppSettings))
   .ValidateDataAnnotations() // 启用DataAnnotations验证
   .ValidateOnStart(); // 应用启动时验证

2. 常见问题

  • 配置未更新

    • 检查配置文件的 “复制到输出目录” 属性是否正确
    • 确保使用了支持热重载的选项接口(IOptionsSnapshotIOptionsMonitor
    • 验证配置源的优先级,是否有其他源覆盖了配置
  • 敏感信息泄露

    • 不要将敏感信息提交到代码库,使用用户密钥或环境变量
    • 生产环境配置文件应限制访问权限
    • 考虑使用加密或密钥管理服务
  • 配置绑定失败

    • 检查配置文件中的键名与实体类属性名是否一致(大小写敏感)
    • 确保配置值的类型与实体类属性类型匹配(如字符串不能转换为整数)
    • 使用IOptions<TOptions>.Value时检查是否为null
  • 多环境配置不生效

    • 检查环境变量是否正确设置(ASPNETCORE_ENVIRONMENT
    • 验证环境特定配置文件的名称是否正确
    • 检查配置文件的加载顺序是否正确

九、总结

C# 提供了从传统 XML 配置到现代多源配置系统的完整解决方案:

  • 传统.NET Framework:使用app.config/web.configConfigurationManager,适合维护旧项目
  • .NET Core/.NET 5+:采用新配置系统,支持多种配置源、热重载和依赖注入,是新项目的首选
  • 选项模式:通过IOptions<T>系列接口提供强类型配置访问,提高代码可维护性和可测试性
  • 多环境管理:通过环境变量和环境特定配置文件,轻松实现不同环境的配置隔离

选择合适的配置方式应根据项目类型(传统框架还是现代框架)、部署环境(本地还是云原生)、团队习惯等因素综合考虑。无论采用哪种方式,保持配置的清晰组织、敏感信息的安全保护以及配置的可扩展性都是关键原则。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿蒙Armon

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值