refactor(config): 统一配置读取架构,运营配置从Admin库读取

- Model层新增AdminConfig实体和AdminConfigReadDbContext(只读连接Admin库)
- API项目新增AdminConnection连接字符串,注册AdminConfigReadDbContext
- Core层ConfigService按key路由:运营配置走Admin库,业务配置走业务库
- WechatPayConfigService改为从Admin库读取支付/小程序配置
- WechatService新增AdminConfigReadDbContext注入,配置读取改为Admin库
- Autofac注册同步更新三个服务的依赖注入
- Admin.Business的AdminConfigService改用AdminConfigDbContext连接Admin库
This commit is contained in:
zpc 2026-02-20 15:48:16 +08:00
parent 60c018b08a
commit 8489b4300c
15 changed files with 205 additions and 56 deletions

View File

@ -13,15 +13,6 @@ public class AdminBusinessDbContext : DbContext
{
}
#region
/// <summary>
/// 后台配置表
/// </summary>
public DbSet<AdminConfig> AdminConfigs { get; set; } = null!;
#endregion
#region
/// <summary>
@ -153,21 +144,6 @@ public class AdminBusinessDbContext : DbContext
{
base.OnModelCreating(modelBuilder);
// =============================================
// AdminConfig 配置
// =============================================
modelBuilder.Entity<AdminConfig>(entity =>
{
// ConfigKey 唯一索引
entity.HasIndex(e => e.ConfigKey)
.IsUnique()
.HasDatabaseName("IX_admin_configs_config_key");
// ConfigValue 使用 nvarchar(max)
entity.Property(e => e.ConfigValue)
.HasColumnType("nvarchar(max)");
});
// =============================================
// Config 配置
// =============================================

View File

@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore;
using MiAssessment.Admin.Business.Entities;
namespace MiAssessment.Admin.Business.Data;
/// <summary>
/// Admin 配置数据库上下文
/// 连接后台管理库,专门用于读取 admin_configs 配置表
/// </summary>
public class AdminConfigDbContext : DbContext
{
public AdminConfigDbContext(DbContextOptions<AdminConfigDbContext> options) : base(options)
{
}
/// <summary>
/// 后台配置表
/// </summary>
public DbSet<AdminConfig> AdminConfigs { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<AdminConfig>(entity =>
{
entity.HasIndex(e => e.ConfigKey)
.IsUnique()
.HasDatabaseName("IX_admin_configs_config_key");
entity.Property(e => e.ConfigValue)
.HasColumnType("nvarchar(max)");
});
}
}

View File

@ -34,6 +34,12 @@ public static class ServiceCollectionExtensions
{
options.UseSqlServer(configuration.GetConnectionString("BusinessConnection"));
});
// 注册 AdminConfigDbContext连接后台管理库读取配置
services.AddDbContext<AdminConfigDbContext>(options =>
{
options.UseSqlServer(configuration.GetConnectionString("DefaultConnection"));
});
}
// 自动注册业务服务

View File

@ -16,7 +16,7 @@ namespace MiAssessment.Admin.Business.Services;
/// </summary>
public class AdminConfigService : IAdminConfigService
{
private readonly AdminBusinessDbContext _adminDbContext;
private readonly AdminConfigDbContext _adminDbContext;
private readonly IRedisService _redisService;
private readonly ILogger<AdminConfigService> _logger;
@ -28,7 +28,7 @@ public class AdminConfigService : IAdminConfigService
};
public AdminConfigService(
AdminBusinessDbContext adminDbContext,
AdminConfigDbContext adminDbContext,
IRedisService redisService,
ILogger<AdminConfigService> logger)
{

View File

@ -26,7 +26,7 @@ public class ConfigController : ControllerBase
/// </summary>
private static class ConfigKeys
{
public const string Upload = "upload_setting";
public const string Upload = "uploads";
}
public ConfigController(IAdminConfigService configService, ILogger<ConfigController> logger)

View File

@ -46,12 +46,18 @@ try
containerBuilder.RegisterModule<ServiceModule>();
});
// 配置 DbContext
// 配置 DbContext(业务库)
builder.Services.AddDbContext<MiAssessmentDbContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
});
// 配置 AdminConfigReadDbContextAdmin库只读用于读取运营配置
builder.Services.AddDbContext<AdminConfigReadDbContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("AdminConnection"));
});
// 配置 JWT 设置
var jwtSettings = new JwtSettings();
builder.Configuration.GetSection("JwtSettings").Bind(jwtSettings);

View File

@ -1,6 +1,7 @@
{
"ConnectionStrings": {
"DefaultConnection": "Server=192.168.195.15,1433;uid=sa;pwd=Dbt@com@123;Database=MiAssessment_Business;MultipleActiveResultSets=true;pooling=true;min pool size=5;max pool size=32767;connect timeout=20;Encrypt=True;TrustServerCertificate=True;",
"AdminConnection": "Server=192.168.195.15,1433;uid=sa;pwd=Dbt@com@123;Database=MiAssessment_Admin;MultipleActiveResultSets=true;pooling=true;min pool size=5;max pool size=32767;connect timeout=20;Encrypt=True;TrustServerCertificate=True;",
"Redis": "192.168.195.15:6379,defaultDatabase=2,abortConnect=false,connectTimeout=5000"
},
"AppSettings": {

View File

@ -9,22 +9,38 @@ namespace MiAssessment.Core.Services;
/// <summary>
/// 配置服务实现
/// 运营配置app_setting、base等从 Admin 库读取
/// 业务配置从业务库读取
/// </summary>
public class ConfigService : IConfigService
{
private readonly MiAssessmentDbContext _dbContext;
private readonly AdminConfigReadDbContext _adminConfigDbContext;
private readonly ILogger<ConfigService> _logger;
private readonly IRedisService _redisService;
// 当前版本号
private const string CurrentVersion = "116";
/// <summary>
/// 需要从 Admin 库读取的运营配置键
/// </summary>
private static readonly HashSet<string> AdminConfigKeys = new()
{
"app_setting", "base", "weixinpay_setting", "miniprogram_setting",
"weixinpay", "alipay_setting", "h5_setting", "uploads",
"systemconfig", "sign", "user_config", "infinite_multiple",
"rank_setting", "system_test", "tencent_sms_config"
};
public ConfigService(
MiAssessmentDbContext dbContext,
AdminConfigReadDbContext adminConfigDbContext,
ILogger<ConfigService> logger,
IRedisService redisService)
{
_dbContext = dbContext;
_adminConfigDbContext = adminConfigDbContext;
_logger = logger;
_redisService = redisService;
}
@ -116,11 +132,24 @@ public class ConfigService : IConfigService
_logger.LogWarning(ex, "Failed to get config from cache: {Key}", key);
}
// 从数据库获取
var config = await _dbContext.Configs
.Where(c => c.ConfigKey == key)
.Select(c => c.ConfigValue)
.FirstOrDefaultAsync();
// 根据配置键决定从哪个库读取
string? config;
if (AdminConfigKeys.Contains(key))
{
// 运营配置从 Admin 库读取
config = await _adminConfigDbContext.AdminConfigs
.Where(c => c.ConfigKey == key)
.Select(c => c.ConfigValue)
.FirstOrDefaultAsync();
}
else
{
// 业务配置从业务库读取
config = await _dbContext.Configs
.Where(c => c.ConfigKey == key)
.Select(c => c.ConfigValue)
.FirstOrDefaultAsync();
}
// 存入缓存10分钟过期
if (!string.IsNullOrEmpty(config))

View File

@ -14,7 +14,7 @@ namespace MiAssessment.Core.Services;
/// </summary>
public class WechatPayConfigService : IWechatPayConfigService
{
private readonly MiAssessmentDbContext _dbContext;
private readonly AdminConfigReadDbContext _adminConfigDbContext;
private readonly IRedisService _redisService;
private readonly ILogger<WechatPayConfigService> _logger;
private readonly Random _random = new();
@ -24,11 +24,11 @@ public class WechatPayConfigService : IWechatPayConfigService
private static readonly TimeSpan CACHE_DURATION = TimeSpan.FromMinutes(5);
public WechatPayConfigService(
MiAssessmentDbContext dbContext,
AdminConfigReadDbContext adminConfigDbContext,
IRedisService redisService,
ILogger<WechatPayConfigService> logger)
{
_dbContext = dbContext;
_adminConfigDbContext = adminConfigDbContext;
_redisService = redisService;
_logger = logger;
}
@ -49,7 +49,7 @@ public class WechatPayConfigService : IWechatPayConfigService
var merchants = new List<WechatPayMerchantConfig>();
try
{
var settingConfig = await _dbContext.Configs
var settingConfig = await _adminConfigDbContext.AdminConfigs
.Where(c => c.ConfigKey == "weixinpay_setting")
.Select(c => c.ConfigValue)
.FirstOrDefaultAsync();
@ -79,7 +79,7 @@ public class WechatPayConfigService : IWechatPayConfigService
}
}
var weixinpayConfig = await _dbContext.Configs
var weixinpayConfig = await _adminConfigDbContext.AdminConfigs
.Where(c => c.ConfigKey == "weixinpay")
.Select(c => c.ConfigValue)
.FirstOrDefaultAsync();
@ -132,7 +132,7 @@ public class WechatPayConfigService : IWechatPayConfigService
var miniprograms = new List<MiniprogramConfig>();
try
{
var settingConfig = await _dbContext.Configs
var settingConfig = await _adminConfigDbContext.AdminConfigs
.Where(c => c.ConfigKey == "miniprogram_setting")
.Select(c => c.ConfigValue)
.FirstOrDefaultAsync();

View File

@ -20,6 +20,7 @@ public class WechatService : IWechatService
private readonly WechatPaySettings _wechatPaySettings;
private readonly IRedisService _redisService;
private readonly MiAssessmentDbContext _dbContext;
private readonly AdminConfigReadDbContext _adminConfigDbContext;
// 微信API端点
private const string WechatCodeToSessionUrl = "https://api.weixin.qq.com/sns/jscode2session";
@ -34,13 +35,15 @@ public class WechatService : IWechatService
ILogger<WechatService> logger,
IOptions<WechatPaySettings> wechatPaySettings,
IRedisService redisService,
MiAssessmentDbContext dbContext)
MiAssessmentDbContext dbContext,
AdminConfigReadDbContext adminConfigDbContext)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_wechatPaySettings = wechatPaySettings?.Value ?? throw new ArgumentNullException(nameof(wechatPaySettings));
_redisService = redisService ?? throw new ArgumentNullException(nameof(redisService));
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
_adminConfigDbContext = adminConfigDbContext ?? throw new ArgumentNullException(nameof(adminConfigDbContext));
}
/// <summary>
@ -620,8 +623,8 @@ public class WechatService : IWechatService
{
try
{
// 从 miniprogram_setting 读取小程序配置
var miniprogramConfig = await _dbContext.Configs
// 从 miniprogram_setting 读取小程序配置运营配置从Admin库读取
var miniprogramConfig = await _adminConfigDbContext.AdminConfigs
.Where(c => c.ConfigKey == "miniprogram_setting")
.Select(c => c.ConfigValue)
.FirstOrDefaultAsync();
@ -700,8 +703,8 @@ public class WechatService : IWechatService
{
try
{
// 从数据库读取 weixinpay 配置
var weixinpayConfig = await _dbContext.Configs
// 从数据库读取 weixinpay 配置运营配置从Admin库读取
var weixinpayConfig = await _adminConfigDbContext.AdminConfigs
.Where(c => c.ConfigKey == "weixinpay")
.Select(c => c.ConfigValue)
.FirstOrDefaultAsync();

View File

@ -31,7 +31,8 @@ public class ServiceModule : Module
var wechatPaySettings = c.Resolve<Microsoft.Extensions.Options.IOptions<WechatPaySettings>>();
var redisService = c.Resolve<IRedisService>();
var dbContext = c.Resolve<MiAssessmentDbContext>();
return new WechatService(httpClientFactory.CreateClient(), logger, wechatPaySettings, redisService, dbContext);
var adminConfigDbContext = c.Resolve<AdminConfigReadDbContext>();
return new WechatService(httpClientFactory.CreateClient(), logger, wechatPaySettings, redisService, dbContext, adminConfigDbContext);
}).As<IWechatService>().InstancePerLifetimeScope();
// 注册 IP 地理位置服务
@ -69,13 +70,13 @@ public class ServiceModule : Module
// ========== 支付系统服务注册 ==========
// 注册微信支付配置服务(从数据库读取配置)
// 注册微信支付配置服务(从Admin库读取配置)
builder.Register(c =>
{
var dbContext = c.Resolve<MiAssessmentDbContext>();
var adminConfigDbContext = c.Resolve<AdminConfigReadDbContext>();
var redisService = c.Resolve<IRedisService>();
var logger = c.Resolve<ILogger<WechatPayConfigService>>();
return new WechatPayConfigService(dbContext, redisService, logger);
return new WechatPayConfigService(adminConfigDbContext, redisService, logger);
}).As<IWechatPayConfigService>().InstancePerLifetimeScope();
// 注册微信支付 V3 服务
@ -156,9 +157,10 @@ public class ServiceModule : Module
builder.Register(c =>
{
var dbContext = c.Resolve<MiAssessmentDbContext>();
var adminConfigDbContext = c.Resolve<AdminConfigReadDbContext>();
var logger = c.Resolve<ILogger<ConfigService>>();
var redisService = c.Resolve<IRedisService>();
return new ConfigService(dbContext, logger, redisService);
return new ConfigService(dbContext, adminConfigDbContext, logger, redisService);
}).As<IConfigService>().InstancePerLifetimeScope();
// ========== 小程序首页模块服务注册 ==========

View File

@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore;
using MiAssessment.Model.Entities;
namespace MiAssessment.Model.Data;
/// <summary>
/// Admin 配置只读数据库上下文
/// 连接 Admin 库,用于读取 admin_configs 表中的运营配置
/// 如:支付配置、小程序配置、上传配置等
/// 注意:此上下文仅用于只读操作,写操作通过 Admin 项目进行
/// </summary>
public class AdminConfigReadDbContext : DbContext
{
public AdminConfigReadDbContext(DbContextOptions<AdminConfigReadDbContext> options) : base(options)
{
}
/// <summary>
/// 后台配置表
/// </summary>
public DbSet<AdminConfig> AdminConfigs { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<AdminConfig>(entity =>
{
// ConfigKey 唯一索引
entity.HasIndex(e => e.ConfigKey)
.IsUnique()
.HasDatabaseName("IX_admin_configs_config_key");
// ConfigValue 使用 nvarchar(max)
entity.Property(e => e.ConfigValue)
.HasColumnType("nvarchar(max)");
});
}
}

View File

@ -0,0 +1,52 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MiAssessment.Model.Entities;
/// <summary>
/// 后台配置表Admin库存储运营级配置信息
/// 如:支付配置、小程序配置、上传配置等
/// </summary>
[Table("admin_configs")]
public class AdminConfig
{
/// <summary>
/// 主键ID
/// </summary>
[Key]
public int Id { get; set; }
/// <summary>
/// 配置键名
/// </summary>
[Required]
[MaxLength(100)]
[Column("config_key")]
public string ConfigKey { get; set; } = null!;
/// <summary>
/// 配置值JSON格式或普通文本
/// </summary>
[Column("config_value", TypeName = "nvarchar(max)")]
public string? ConfigValue { get; set; }
/// <summary>
/// 配置描述
/// </summary>
[MaxLength(200)]
[Column("description")]
public string? Description { get; set; }
/// <summary>
/// 创建时间
/// </summary>
[Column("created_at")]
public DateTime CreatedAt { get; set; } = DateTime.Now;
/// <summary>
/// 更新时间
/// </summary>
[Column("updated_at")]
public DateTime? UpdatedAt { get; set; }
}

View File

@ -46,11 +46,11 @@ public class AdminConfigServicePropertyTests
};
// Create a fresh service instance for each test
var options = new DbContextOptionsBuilder<AdminBusinessDbContext>()
var options = new DbContextOptionsBuilder<AdminConfigDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
using var dbContext = new AdminBusinessDbContext(options);
using var dbContext = new AdminConfigDbContext(options);
var mockRedis = new Mock<IRedisService>();
mockRedis.Setup(x => x.GetStringAsync(It.IsAny<string>())).ReturnsAsync((string?)null);
@ -313,11 +313,11 @@ public class AdminConfigServicePropertyTests
private AdminConfigService CreateService()
{
var options = new DbContextOptionsBuilder<AdminBusinessDbContext>()
var options = new DbContextOptionsBuilder<AdminConfigDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
var dbContext = new AdminBusinessDbContext(options);
var dbContext = new AdminConfigDbContext(options);
var mockRedis = new Mock<IRedisService>();
mockRedis.Setup(x => x.GetStringAsync(It.IsAny<string>())).ReturnsAsync((string?)null);

View File

@ -17,7 +17,7 @@ namespace MiAssessment.Tests.Services;
/// </summary>
public class AdminConfigServiceTests : IDisposable
{
private readonly AdminBusinessDbContext _adminDbContext;
private readonly AdminConfigDbContext _adminDbContext;
private readonly Mock<IRedisService> _mockRedisService;
private readonly Mock<ILogger<AdminConfigService>> _mockLogger;
private readonly AdminConfigService _configService;
@ -25,11 +25,11 @@ public class AdminConfigServiceTests : IDisposable
public AdminConfigServiceTests()
{
// 使用 InMemory 数据库
var options = new DbContextOptionsBuilder<AdminBusinessDbContext>()
var options = new DbContextOptionsBuilder<AdminConfigDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
_adminDbContext = new AdminBusinessDbContext(options);
_adminDbContext = new AdminConfigDbContext(options);
_mockRedisService = new Mock<IRedisService>();
_mockLogger = new Mock<ILogger<AdminConfigService>>();