feat: 迁入 MiGu.Server、平台前端与车辆列表 reflection 回退
从 Simple-FR 拆出 Platform.Server 并重命名为 MiGu.Server;frontends 源码与构建脚本迁入本仓库。地图监控在 projection/cars 失败或为空时回退 reflection 车辆列表;Simple 仓库已移除旧 Platform.Server。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace MiGu.Server.Configs;
|
||||
|
||||
/// <summary>
|
||||
/// 配置中心存储(内存 + JSON 文件持久化占位)。
|
||||
/// 14 个 section 对应 ARCHITECTURE.md §9 的 13+1 维度。
|
||||
/// 真实落地时由 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"
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
_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(),
|
||||
_ => 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);
|
||||
File.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