- Model层新增AdminConfig实体和AdminConfigReadDbContext(只读连接Admin库) - API项目新增AdminConnection连接字符串,注册AdminConfigReadDbContext - Core层ConfigService按key路由:运营配置走Admin库,业务配置走业务库 - WechatPayConfigService改为从Admin库读取支付/小程序配置 - WechatService新增AdminConfigReadDbContext注入,配置读取改为Admin库 - Autofac注册同步更新三个服务的依赖注入 - Admin.Business的AdminConfigService改用AdminConfigDbContext连接Admin库
378 lines
11 KiB
C#
378 lines
11 KiB
C#
using System.Text.Json;
|
||
using MiAssessment.Admin.Business.Data;
|
||
using MiAssessment.Admin.Business.Entities;
|
||
using MiAssessment.Admin.Business.Models;
|
||
using MiAssessment.Admin.Business.Models.Config;
|
||
using MiAssessment.Admin.Business.Services.Interfaces;
|
||
using MiAssessment.Core.Interfaces;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using Microsoft.Extensions.Logging;
|
||
|
||
namespace MiAssessment.Admin.Business.Services;
|
||
|
||
/// <summary>
|
||
/// 后台配置服务实现
|
||
/// 从 Admin 数据库读取配置
|
||
/// </summary>
|
||
public class AdminConfigService : IAdminConfigService
|
||
{
|
||
private readonly AdminConfigDbContext _adminDbContext;
|
||
private readonly IRedisService _redisService;
|
||
private readonly ILogger<AdminConfigService> _logger;
|
||
|
||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||
{
|
||
PropertyNameCaseInsensitive = true,
|
||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||
WriteIndented = false
|
||
};
|
||
|
||
public AdminConfigService(
|
||
AdminConfigDbContext adminDbContext,
|
||
IRedisService redisService,
|
||
ILogger<AdminConfigService> logger)
|
||
{
|
||
_adminDbContext = adminDbContext;
|
||
_redisService = redisService;
|
||
_logger = logger;
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<T?> GetConfigAsync<T>(string key) where T : class
|
||
{
|
||
var jsonValue = await GetConfigRawAsync(key);
|
||
if (string.IsNullOrEmpty(jsonValue))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
try
|
||
{
|
||
return JsonSerializer.Deserialize<T>(jsonValue, JsonOptions);
|
||
}
|
||
catch (JsonException ex)
|
||
{
|
||
_logger.LogWarning(ex, "Failed to deserialize config {Key}", key);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<string?> GetConfigRawAsync(string key)
|
||
{
|
||
// 尝试从缓存获取
|
||
var cacheKey = GetCacheKey(key);
|
||
try
|
||
{
|
||
var cachedValue = await _redisService.GetStringAsync(cacheKey);
|
||
if (!string.IsNullOrEmpty(cachedValue))
|
||
{
|
||
return cachedValue;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogWarning(ex, "Failed to get config from cache: {Key}", key);
|
||
}
|
||
|
||
// 从数据库获取
|
||
var config = await _adminDbContext.AdminConfigs
|
||
.Where(c => c.ConfigKey == key)
|
||
.Select(c => c.ConfigValue)
|
||
.FirstOrDefaultAsync();
|
||
|
||
// 存入缓存(10分钟过期)
|
||
if (!string.IsNullOrEmpty(config))
|
||
{
|
||
try
|
||
{
|
||
await _redisService.SetStringAsync(cacheKey, config, TimeSpan.FromMinutes(10));
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogWarning(ex, "Failed to cache config: {Key}", key);
|
||
}
|
||
}
|
||
|
||
return config;
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<bool> UpdateConfigAsync<T>(string key, T config) where T : class
|
||
{
|
||
var jsonValue = JsonSerializer.Serialize(config, JsonOptions);
|
||
return await UpdateConfigRawAsync(key, jsonValue);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<bool> UpdateConfigRawAsync(string key, string jsonValue)
|
||
{
|
||
// 验证配置
|
||
var validationError = await ValidateConfigAsync(key, jsonValue);
|
||
if (!string.IsNullOrEmpty(validationError))
|
||
{
|
||
throw new BusinessException(BusinessErrorCodes.ValidationFailed, validationError);
|
||
}
|
||
|
||
// 查找现有配置
|
||
var existingConfig = await _adminDbContext.AdminConfigs
|
||
.FirstOrDefaultAsync(c => c.ConfigKey == key);
|
||
|
||
if (existingConfig != null)
|
||
{
|
||
// 更新现有配置
|
||
existingConfig.ConfigValue = jsonValue;
|
||
existingConfig.UpdatedAt = DateTime.Now;
|
||
}
|
||
else
|
||
{
|
||
// 创建新配置
|
||
var newConfig = new AdminConfig
|
||
{
|
||
ConfigKey = key,
|
||
ConfigValue = jsonValue,
|
||
CreatedAt = DateTime.Now
|
||
};
|
||
_adminDbContext.AdminConfigs.Add(newConfig);
|
||
}
|
||
|
||
var result = await _adminDbContext.SaveChangesAsync() > 0;
|
||
|
||
// 清理缓存
|
||
if (result)
|
||
{
|
||
await ClearConfigCacheAsync(key);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
|
||
/// <inheritdoc />
|
||
public Task<string?> ValidateConfigAsync(string key, string jsonValue)
|
||
{
|
||
try
|
||
{
|
||
// 验证JSON格式
|
||
using var doc = JsonDocument.Parse(jsonValue);
|
||
|
||
// 根据配置键进行特定验证
|
||
return key switch
|
||
{
|
||
ConfigKeys.WeixinPaySetting => Task.FromResult(ValidateWeixinPaySetting(jsonValue)),
|
||
ConfigKeys.AlipayPaySetting => Task.FromResult(ValidateAlipaySetting(jsonValue)),
|
||
ConfigKeys.MiniprogramSetting => Task.FromResult(ValidateMiniprogramSetting(jsonValue)),
|
||
ConfigKeys.H5Setting => Task.FromResult(ValidateH5Setting(jsonValue)),
|
||
_ => Task.FromResult<string?>(null)
|
||
};
|
||
}
|
||
catch (JsonException)
|
||
{
|
||
return Task.FromResult<string?>("配置格式无效,请提供有效的JSON数据");
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task ClearConfigCacheAsync(string key)
|
||
{
|
||
var cacheKey = GetCacheKey(key);
|
||
try
|
||
{
|
||
await _redisService.DeleteAsync(cacheKey);
|
||
_logger.LogInformation("Cleared config cache: {Key}", key);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogWarning(ex, "Failed to clear config cache: {Key}", key);
|
||
}
|
||
}
|
||
|
||
#region Private Validation Methods
|
||
|
||
/// <summary>
|
||
/// 验证支付宝配置
|
||
/// </summary>
|
||
private string? ValidateAlipaySetting(string jsonValue)
|
||
{
|
||
try
|
||
{
|
||
var setting = JsonSerializer.Deserialize<AlipaySetting>(jsonValue, JsonOptions);
|
||
if (setting?.Merchants == null || setting.Merchants.Count == 0)
|
||
{
|
||
return null; // 空配置是允许的
|
||
}
|
||
|
||
foreach (var merchant in setting.Merchants)
|
||
{
|
||
// 验证AppId必填
|
||
if (string.IsNullOrWhiteSpace(merchant.AppId))
|
||
{
|
||
return $"商户\"{merchant.Name}\"的AppId不能为空";
|
||
}
|
||
|
||
// 验证商户名称必填
|
||
if (string.IsNullOrWhiteSpace(merchant.Name))
|
||
{
|
||
return "商户名称不能为空";
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
catch (JsonException)
|
||
{
|
||
return "支付宝配置格式无效";
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证微信支付配置
|
||
/// </summary>
|
||
private string? ValidateWeixinPaySetting(string jsonValue)
|
||
{
|
||
try
|
||
{
|
||
var setting = JsonSerializer.Deserialize<WeixinPaySetting>(jsonValue, JsonOptions);
|
||
if (setting?.Merchants == null || setting.Merchants.Count == 0)
|
||
{
|
||
return null; // 空配置是允许的
|
||
}
|
||
|
||
var prefixes = new HashSet<string>();
|
||
foreach (var merchant in setting.Merchants)
|
||
{
|
||
// 验证前缀长度
|
||
if (string.IsNullOrEmpty(merchant.OrderPrefix) || merchant.OrderPrefix.Length != 3)
|
||
{
|
||
return $"商户\"{merchant.Name}\"的前缀必须是3位字符";
|
||
}
|
||
|
||
// 验证前缀唯一性
|
||
if (!prefixes.Add(merchant.OrderPrefix))
|
||
{
|
||
return $"商户前缀\"{merchant.OrderPrefix}\"重复,每个商户的前缀必须唯一";
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
catch (JsonException)
|
||
{
|
||
return "微信支付配置格式无效";
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 验证小程序配置
|
||
/// </summary>
|
||
private string? ValidateMiniprogramSetting(string jsonValue)
|
||
{
|
||
try
|
||
{
|
||
var setting = JsonSerializer.Deserialize<MiniprogramSetting>(jsonValue, JsonOptions);
|
||
if (setting?.Miniprograms == null || setting.Miniprograms.Count == 0)
|
||
{
|
||
return null; // 空配置是允许的
|
||
}
|
||
|
||
var hasDefault = false;
|
||
var prefixes = new HashSet<string>();
|
||
|
||
foreach (var miniprogram in setting.Miniprograms)
|
||
{
|
||
// 检查是否有默认小程序
|
||
if (miniprogram.IsDefault == 1)
|
||
{
|
||
hasDefault = true;
|
||
}
|
||
|
||
// 验证订单前缀(如果有)
|
||
if (!string.IsNullOrEmpty(miniprogram.OrderPrefix))
|
||
{
|
||
if (miniprogram.OrderPrefix.Length != 2)
|
||
{
|
||
return $"小程序\"{miniprogram.Name}\"的订单前缀必须是2位字符";
|
||
}
|
||
|
||
if (!prefixes.Add(miniprogram.OrderPrefix))
|
||
{
|
||
return $"订单前缀\"{miniprogram.OrderPrefix}\"重复,每个小程序的前缀必须唯一";
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!hasDefault)
|
||
{
|
||
return "请至少设置一个默认小程序";
|
||
}
|
||
|
||
return null;
|
||
}
|
||
catch (JsonException)
|
||
{
|
||
return "小程序配置格式无效";
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证H5配置
|
||
/// </summary>
|
||
private string? ValidateH5Setting(string jsonValue)
|
||
{
|
||
try
|
||
{
|
||
var setting = JsonSerializer.Deserialize<H5Setting>(jsonValue, JsonOptions);
|
||
if (setting?.H5Apps == null || setting.H5Apps.Count == 0)
|
||
{
|
||
return null; // 空配置是允许的
|
||
}
|
||
|
||
var hasDefault = false;
|
||
var prefixes = new HashSet<string>();
|
||
|
||
foreach (var h5app in setting.H5Apps)
|
||
{
|
||
// 检查是否有默认H5应用
|
||
if (h5app.IsDefault == 1)
|
||
{
|
||
hasDefault = true;
|
||
}
|
||
|
||
// 验证订单前缀(如果有)
|
||
if (!string.IsNullOrEmpty(h5app.OrderPrefix))
|
||
{
|
||
if (h5app.OrderPrefix.Length != 2)
|
||
{
|
||
return $"H5应用\"{h5app.Name}\"的订单前缀必须是2位字符";
|
||
}
|
||
|
||
if (!prefixes.Add(h5app.OrderPrefix))
|
||
{
|
||
return $"订单前缀\"{h5app.OrderPrefix}\"重复,每个H5应用的前缀必须唯一";
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!hasDefault)
|
||
{
|
||
return "请至少设置一个默认H5应用";
|
||
}
|
||
|
||
return null;
|
||
}
|
||
catch (JsonException)
|
||
{
|
||
return "H5配置格式无效";
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Private Helper Methods
|
||
|
||
private static string GetCacheKey(string key) => $"config:{key}";
|
||
|
||
#endregion
|
||
}
|