调整 .gitignore 以适配 backends 目录结构
将 MiGu.Server 相关忽略规则迁移至 backends/MiGu.Server,并新增对 .tmp-build* 和 /.cursor/rules 的忽略,优化敏感数据与临时文件的管理。
This commit is contained in:
@@ -1,236 +0,0 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
using MiGu.Server.Infra;
|
||||
|
||||
namespace MiGu.Server.Configs;
|
||||
|
||||
/// <summary>
|
||||
/// 配置中心存储(内存 + JSON 文件持久化占位)。
|
||||
/// 前 14 个 section 对应 ARCHITECTURE.md §9 的 13+1 维度;外加 deployment —— 登录后「配置向导」的部署画像。
|
||||
/// 真实落地时由 SimpleShared.Persistence 接入 EF Core,并配合 YARP 下发至 SimpleLite。
|
||||
/// </summary>
|
||||
public sealed class ConfigStore
|
||||
{
|
||||
public static readonly string[] AllSections =
|
||||
{
|
||||
"system", "integrations", "routing", "vehicle", "charge", "task",
|
||||
"traffic", "auth", "device", "fleet", "scenario", "location", "ops", "widget",
|
||||
"deployment"
|
||||
};
|
||||
|
||||
public sealed record Envelope(string Section, int Version, DateTimeOffset UpdatedAt, object Payload);
|
||||
|
||||
private readonly ConcurrentDictionary<string, Envelope> _mem = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly string _dataDir;
|
||||
private readonly ILogger<ConfigStore> _logger;
|
||||
private readonly JsonSerializerOptions _jsonOpts = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
public ConfigStore(IWebHostEnvironment env, ILogger<ConfigStore> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_dataDir = Path.Combine(env.ContentRootPath, "data");
|
||||
Directory.CreateDirectory(_dataDir);
|
||||
SeedAndLoad();
|
||||
}
|
||||
|
||||
public Envelope Get(string section)
|
||||
{
|
||||
section = section.ToLowerInvariant();
|
||||
if (_mem.TryGetValue(section, out var env))
|
||||
{
|
||||
if (section == "ops") return MergeOpsEnvelope(env);
|
||||
return env;
|
||||
}
|
||||
var def = NewDefault(section);
|
||||
_mem[section] = def;
|
||||
Persist(def);
|
||||
return def;
|
||||
}
|
||||
|
||||
public Envelope Put(string section, JsonElement payload)
|
||||
{
|
||||
section = section.ToLowerInvariant();
|
||||
if (!AllSections.Contains(section))
|
||||
throw new ArgumentException($"未知 section: {section}");
|
||||
|
||||
var prev = _mem.TryGetValue(section, out var p) ? p : null;
|
||||
var version = (prev?.Version ?? 0) + 1;
|
||||
// payload 保留 JsonElement 原样;序列化器会按 camelCase 输出
|
||||
var env = new Envelope(section, version, DateTimeOffset.UtcNow, JsonElementToObject(payload));
|
||||
if (section == "ops") env = MergeOpsEnvelope(env);
|
||||
_mem[section] = env;
|
||||
Persist(env);
|
||||
return env;
|
||||
}
|
||||
|
||||
public IEnumerable<Envelope> List() => AllSections.Select(Get);
|
||||
|
||||
/// <summary>
|
||||
/// 强类型读取部署画像(<c>deployment</c> section)。兼容 Payload 为 <see cref="DeploymentProfile"/>(默认值场景)
|
||||
/// 或 <see cref="JsonElement"/>(已持久化场景)两种形态,并把 null 列表/空字符串规整为安全默认值,
|
||||
/// 供 AuthController(NeedsWizard)/ WizardController / Launcher 直接使用。
|
||||
/// </summary>
|
||||
public DeploymentProfile GetDeployment()
|
||||
{
|
||||
var env = Get("deployment");
|
||||
var dp = env.Payload switch
|
||||
{
|
||||
DeploymentProfile d => d,
|
||||
JsonElement el => SafeDeserializeDeployment(el),
|
||||
_ => DeploymentProfile.Default()
|
||||
};
|
||||
return new DeploymentProfile(
|
||||
dp.Configured,
|
||||
string.IsNullOrWhiteSpace(dp.PlatformType) ? "standard" : dp.PlatformType,
|
||||
dp.Modules ?? new List<string>(),
|
||||
dp.NavigationKinds ?? new List<string>(),
|
||||
dp.Scenarios ?? new List<string>(),
|
||||
dp.UpdatedBy ?? "");
|
||||
}
|
||||
|
||||
/// <summary>以强类型保存部署画像(统一经 <see cref="Put"/> 走版本/持久化/camelCase 序列化)。</summary>
|
||||
public Envelope PutDeployment(DeploymentProfile profile)
|
||||
{
|
||||
var el = JsonSerializer.SerializeToElement(profile, _jsonOpts);
|
||||
return Put("deployment", el);
|
||||
}
|
||||
|
||||
private DeploymentProfile SafeDeserializeDeployment(JsonElement el)
|
||||
{
|
||||
try { return el.Deserialize<DeploymentProfile>(_jsonOpts) ?? DeploymentProfile.Default(); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "反序列化 deployment 失败,回退默认值");
|
||||
return DeploymentProfile.Default();
|
||||
}
|
||||
}
|
||||
|
||||
private static object JsonElementToObject(JsonElement el)
|
||||
{
|
||||
// 简化:直接保留 JsonElement,让 STJ 在响应时再原样写回
|
||||
return el.Clone();
|
||||
}
|
||||
|
||||
private void SeedAndLoad()
|
||||
{
|
||||
foreach (var s in AllSections)
|
||||
{
|
||||
var file = FilePath(s);
|
||||
if (File.Exists(file))
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(file);
|
||||
var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
var version = root.TryGetProperty("version", out var v) && v.ValueKind == JsonValueKind.Number
|
||||
? v.GetInt32() : 1;
|
||||
var updated = root.TryGetProperty("updatedAt", out var u) && u.ValueKind == JsonValueKind.String
|
||||
? DateTimeOffset.Parse(u.GetString()!) : DateTimeOffset.UtcNow;
|
||||
var payload = root.TryGetProperty("payload", out var pl)
|
||||
? (object)pl.Clone()
|
||||
: DefaultPayload(s);
|
||||
_mem[s] = new Envelope(s, version, updated, payload);
|
||||
continue;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "加载 {Section} 失败,回退到默认值", s);
|
||||
// S2:疑似损坏的配置先备份,避免随后写入的默认值把用户配置永久冲掉。
|
||||
var bak = AtomicFile.BackupCorrupt(file);
|
||||
if (bak != null) _logger.LogWarning("已备份疑似损坏的 {Section} 配置到 {Backup}", s, bak);
|
||||
}
|
||||
}
|
||||
_mem[s] = NewDefault(s);
|
||||
Persist(_mem[s]);
|
||||
}
|
||||
}
|
||||
|
||||
private Envelope NewDefault(string section)
|
||||
{
|
||||
return new Envelope(section, 1, DateTimeOffset.UtcNow, DefaultPayload(section));
|
||||
}
|
||||
|
||||
private static object DefaultPayload(string section) => section switch
|
||||
{
|
||||
"system" => SystemConfig.Default(),
|
||||
"integrations" => ExternalIntegrations.Default(),
|
||||
"routing" => RoutingPolicy.Default(),
|
||||
"vehicle" => VehicleMaintenancePolicy.Default(),
|
||||
"charge" => ChargePolicy.Default(),
|
||||
"task" => TaskAllocationPolicy.Default(),
|
||||
"traffic" => TrafficRule.Default(),
|
||||
"auth" => AuthRoleConfig.Default(),
|
||||
"device" => DeviceManagementConfig.Default(),
|
||||
"fleet" => FleetLifecycleConfig.Default(),
|
||||
"scenario" => ScenarioTemplateConfig.Default(),
|
||||
"location" => LocationManagement.Default(),
|
||||
"ops" => OpsConfig.Default(),
|
||||
"widget" => CustomWidgetConfig.Default(),
|
||||
"deployment" => DeploymentProfile.Default(),
|
||||
_ => new { }
|
||||
};
|
||||
|
||||
private void Persist(Envelope env)
|
||||
{
|
||||
try
|
||||
{
|
||||
var toWrite = env.Section == "ops" ? MergeOpsEnvelope(env) : env;
|
||||
var root = new Dictionary<string, object?>
|
||||
{
|
||||
["section"] = toWrite.Section,
|
||||
["version"] = toWrite.Version,
|
||||
["updatedAt"] = toWrite.UpdatedAt,
|
||||
["payload"] = toWrite.Payload
|
||||
};
|
||||
var json = JsonSerializer.Serialize(root, _jsonOpts);
|
||||
AtomicFile.WriteAllText(FilePath(env.Section), json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "持久化 {Section} 失败", env.Section);
|
||||
}
|
||||
}
|
||||
|
||||
private static Envelope MergeOpsEnvelope(Envelope env)
|
||||
{
|
||||
if (env.Payload is not JsonElement el || el.ValueKind != JsonValueKind.Object)
|
||||
return env;
|
||||
|
||||
var def = OpsConfig.Default();
|
||||
var merged = JsonSerializer.SerializeToElement(new
|
||||
{
|
||||
playback = ReadSection(el, "playback") ?? def.Playback,
|
||||
logRetention = ReadSection(el, "logRetention") ?? def.LogRetention,
|
||||
version = ReadSection(el, "version") ?? def.Version,
|
||||
monitor = MergeOpsMonitor(el, def.Monitor)
|
||||
}, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
|
||||
|
||||
return env with { Payload = merged.Clone() };
|
||||
}
|
||||
|
||||
private static object? ReadSection(JsonElement root, string name) =>
|
||||
root.TryGetProperty(name, out var p) ? p.Clone() : null;
|
||||
|
||||
private static JsonElement MergeOpsMonitor(JsonElement root, MonitorOpsPolicy def)
|
||||
{
|
||||
var opts = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
||||
if (!root.TryGetProperty("monitor", out var m) || m.ValueKind != JsonValueKind.Object)
|
||||
return JsonSerializer.SerializeToElement(def, opts);
|
||||
|
||||
var car = m.TryGetProperty("car", out var c) ? c.Clone() : JsonSerializer.SerializeToElement(def.Car, opts);
|
||||
var site = m.TryGetProperty("site", out var s) ? s.Clone() : JsonSerializer.SerializeToElement(def.Site, opts);
|
||||
var track = m.TryGetProperty("track", out var t) ? t.Clone() : JsonSerializer.SerializeToElement(def.Track, opts);
|
||||
var carActionByType = m.TryGetProperty("carActionByType", out var cat)
|
||||
? cat.Clone()
|
||||
: JsonSerializer.SerializeToElement(def.CarActionByType, opts);
|
||||
|
||||
return JsonSerializer.SerializeToElement(new { car, site, track, carActionByType }, opts);
|
||||
}
|
||||
|
||||
private string FilePath(string section) => Path.Combine(_dataDir, $"config-{section}.json");
|
||||
}
|
||||
Reference in New Issue
Block a user