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:
zhaowei.huang
2026-05-29 18:16:34 +08:00
co-authored by Cursor
parent 804aa68ade
commit 42978930ca
280 changed files with 30046 additions and 8 deletions
+18
View File
@@ -0,0 +1,18 @@
namespace MiGu.Server.Configs;
public record ChargePriorityRule(string Id, string Condition, double Weight);
public record ChargePolicy(
bool AllowMidTaskCharge,
int IdleChargeAfterSec,
List<ChargePriorityRule> Priority)
{
public static ChargePolicy Default() => new(
AllowMidTaskCharge: false,
IdleChargeAfterSec: 300,
Priority: new List<ChargePriorityRule>
{
new("CP1", "soc<0.2", 100),
new("CP2", "idle>5min", 30)
});
}
+190
View File
@@ -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");
}
+17
View File
@@ -0,0 +1,17 @@
namespace MiGu.Server.Configs;
public record CustomWidget(
string Id, string Name, string SchemaJson, string LayoutJson,
List<string> BindToScopes);
public record CustomWidgetConfig(List<CustomWidget> Items)
{
public static CustomWidgetConfig Default() => new(
Items: new List<CustomWidget>
{
new("widget-call-button", "呼叫按钮",
"{\"fields\":[{\"name\":\"siteId\"}]}",
"{\"x\":0,\"y\":0,\"w\":2,\"h\":1}",
new List<string> { "RCSMonitor" })
});
}
@@ -0,0 +1,38 @@
namespace MiGu.Server.Configs;
public record DeviceDriverBinding(string Id, string DeviceType, string DriverName, string Version);
public record DeviceInstance(
string Id, string Name, string DeviceType, string Protocol, string Address,
string DriverId, bool Enabled);
public record DeviceHealthPolicy(int HeartbeatSec, int OfflineSec);
public record AlarmRule(string Level, string Condition);
public record AlarmPolicy(bool Enabled, List<AlarmRule> Rules);
public record DeviceManagementConfig(
List<DeviceDriverBinding> Drivers,
List<DeviceInstance> Devices,
DeviceHealthPolicy HealthPolicy,
AlarmPolicy AlarmPolicy)
{
public static DeviceManagementConfig Default() => new(
Drivers: new List<DeviceDriverBinding>
{
new("drv-elev", "电梯", "OpcUaElevatorDriver", "1.2.0"),
new("drv-chrg", "充电桩", "ModbusChargerDriver", "1.0.5"),
new("drv-cam", "摄像头", "OnvifCameraDriver", "2.1.0")
},
Devices: new List<DeviceInstance>
{
new("dev-elev-1", "#1 电梯", "电梯", "opc-ua", "opc.tcp://10.0.2.20:4840", "drv-elev", true),
new("dev-chrg-1", "充电桩-A1", "充电桩", "modbus-tcp", "10.0.2.30:502", "drv-chrg", true)
},
HealthPolicy: new DeviceHealthPolicy(5, 30),
AlarmPolicy: new AlarmPolicy(true, new List<AlarmRule>
{
new("warn", "offline>30s"),
new("error", "driverException")
}));
}
@@ -0,0 +1,44 @@
namespace MiGu.Server.Configs;
public record WidgetGrantDto(string WidgetId, string Visibility);
public record AuthRole(
string Id, string Name, string Scope,
List<string> Permissions,
List<WidgetGrantDto> WidgetGrants);
public record AuthUser(string Id, string Username, List<string> Roles, bool Enabled);
public record AuthRoleConfig(List<AuthRole> Roles, List<AuthUser> Users)
{
public static AuthRoleConfig Default() => new(
Roles: new List<AuthRole>
{
new("role-admin", "管理员", "Platform",
new List<string> { "*" },
new List<WidgetGrantDto>()),
new("role-ops", "运营", "RCSMonitor",
new List<string>
{
"ops.car.pause", "ops.car.resume", "ops.car.gohome",
"ops.task.pause", "ops.task.cancel", "ops.task.reassign",
"ops.task.boostPriority", "monitor.note.write"
},
new List<WidgetGrantDto>
{
new("MapEditor", "readonly"),
new("CadToolbar", "hidden")
})
},
Users: new List<AuthUser>
{
new("u-admin", "admin", new List<string> { "role-admin" }, true),
new("u-ops", "ops", new List<string> { "role-ops" }, true)
});
}
public record EffectivePermissions(
string UserId,
int Version,
List<string> AllowedOps,
List<WidgetGrantDto> VisibleWidgets);
@@ -0,0 +1,14 @@
namespace MiGu.Server.Configs;
public record EndpointDescriptor(string Id, string Name, string Url, bool Enabled);
public record ExternalIntegrations(
List<EndpointDescriptor> Mes,
List<EndpointDescriptor> Wms,
List<EndpointDescriptor> Rcs)
{
public static ExternalIntegrations Default() => new(
Mes: new List<EndpointDescriptor> { new("mes-1", "MES 主线", "http://mes.lan/api", true) },
Wms: new List<EndpointDescriptor> { new("wms-1", "WMS 仓储", "http://wms.lan/api", true) },
Rcs: new List<EndpointDescriptor>());
}
@@ -0,0 +1,23 @@
namespace MiGu.Server.Configs;
public record FleetGroup(string Id, string Name, string Floor, string Region, List<string> CarIds);
public record OtaPolicy(bool Enabled, int BatchSize, bool RollbackOnFail);
public record BatchOpsPolicy(bool ConfirmationRequired, int MaxBatch);
public record NetworkDiagPolicy(int RttThresholdMs, double PacketLossThreshold);
public record FleetLifecycleConfig(
List<FleetGroup> Groups,
OtaPolicy Ota,
BatchOpsPolicy BatchOps,
NetworkDiagPolicy NetworkDiag)
{
public static FleetLifecycleConfig Default() => new(
Groups: new List<FleetGroup>
{
new("G-A", "A 区车队", "F1", "A", new List<string> { "C01", "C02", "C03" }),
new("G-B", "B 区车队", "F1", "B", new List<string> { "C04", "C05" })
},
Ota: new OtaPolicy(true, 2, true),
BatchOps: new BatchOpsPolicy(true, 10),
NetworkDiag: new NetworkDiagPolicy(80, 0.02));
}
+21
View File
@@ -0,0 +1,21 @@
namespace MiGu.Server.Configs;
public record Location(string Id, string Code, string Name, string SiteId, int Capacity, int Occupied);
public record InventoryRule(string Id, string ItemType, int MinQty, int MaxQty);
public record LocationManagement(
List<Location> Locations,
List<InventoryRule> InventoryRules)
{
public static LocationManagement Default() => new(
Locations: new List<Location>
{
new("L01", "A-01", "A 区货架 1", "S001", 20, 12),
new("L02", "A-02", "A 区货架 2", "S002", 20, 7),
new("L03", "B-01", "B 区缓存", "S003", 30, 25)
},
InventoryRules: new List<InventoryRule>
{
new("IR1", "PalletA", 5, 30)
});
}
+28
View File
@@ -0,0 +1,28 @@
namespace MiGu.Server.Configs;
public record PlaybackPolicy(int RetentionDays, int SamplingHz);
public record LogRetention(int HotDays, int ColdDays);
public record VersionPolicy(int KeepReleases);
public record MonitorPanelPolicy(string[] PropertyKeys, string[] StatusKeys, string[] ActionKeys);
public record MonitorOpsPolicy(
MonitorPanelPolicy Car,
MonitorPanelPolicy Site,
MonitorPanelPolicy Track,
Dictionary<string, string[]> CarActionByType);
public record OpsConfig(
PlaybackPolicy Playback,
LogRetention LogRetention,
VersionPolicy Version,
MonitorOpsPolicy Monitor)
{
public static OpsConfig Default() => new(
Playback: new PlaybackPolicy(30, 5),
LogRetention: new LogRetention(7, 180),
Version: new VersionPolicy(5),
Monitor: new MonitorOpsPolicy(
Car: new MonitorPanelPolicy(Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>()),
Site: new MonitorPanelPolicy(Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>()),
Track: new MonitorPanelPolicy(Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>()),
CarActionByType: new Dictionary<string, string[]>()));
}
+22
View File
@@ -0,0 +1,22 @@
namespace MiGu.Server.Configs;
public record AvoidanceRule(string Id, string ZoneId, string Rule);
public record ZoneSpeedLimit(string ZoneId, double MaxSpeedMps);
public record RoutingPolicy(
string Algorithm,
Dictionary<string, double> Weights,
List<AvoidanceRule> Avoidance,
List<ZoneSpeedLimit> ZoneSpeedLimits)
{
public static RoutingPolicy Default() => new(
Algorithm: "astar",
Weights: new Dictionary<string, double>
{
["distance"] = 1.0,
["congestion"] = 0.5,
["turnPenalty"] = 0.2
},
Avoidance: new List<AvoidanceRule> { new("AV1", "Z-NORTH", "no-entry-while-loading") },
ZoneSpeedLimits: new List<ZoneSpeedLimit> { new("Z-NARROW", 0.5) });
}
@@ -0,0 +1,25 @@
namespace MiGu.Server.Configs;
public record ScenarioTemplate(string Id, string Name, string Category, string Version, string BaselineJson);
public record TemplateDslPolicy(bool Enabled, string SchemaVersion);
public record LowCodePolicy(bool Enabled, string Editor);
public record TemplateVersionPolicy(int KeepVersions, bool AllowRollback);
public record ScenarioTemplateConfig(
List<ScenarioTemplate> Templates,
TemplateDslPolicy DslPolicy,
LowCodePolicy LowCode,
TemplateVersionPolicy VersionPolicy)
{
public static ScenarioTemplateConfig Default() => new(
Templates: new List<ScenarioTemplate>
{
new("tpl-sps", "SPS 物料配送", "SPS", "1.0.0", "{}"),
new("tpl-pack", "电池 Pack 自动化产线", "BatteryPack", "1.0.0", "{}"),
new("tpl-loop", "环线运行", "Loop", "1.0.0", "{}"),
new("tpl-p2p", "点对点柔性搬运", "P2P", "1.0.0", "{}")
},
DslPolicy: new TemplateDslPolicy(true, "1"),
LowCode: new LowCodePolicy(false, "json"),
VersionPolicy: new TemplateVersionPolicy(10, true));
}
+12
View File
@@ -0,0 +1,12 @@
namespace MiGu.Server.Configs;
public record LogPolicy(string Level, int RollDays, int MaxSizeMB);
public record SecurityPolicy(int JwtExpireMin, bool EnableSwagger, List<string> CorsWhitelist);
public record SystemConfig(int DispatchLoopHz, LogPolicy Log, SecurityPolicy Security)
{
public static SystemConfig Default() => new(
DispatchLoopHz: 50,
Log: new LogPolicy("info", 7, 256),
Security: new SecurityPolicy(1440, false, new List<string> { "http://localhost:5173" }));
}
@@ -0,0 +1,12 @@
namespace MiGu.Server.Configs;
public record TaskAllocationPolicy(
string Mode,
bool LoadBalance,
int MaxQueuePerCar)
{
public static TaskAllocationPolicy Default() => new(
Mode: "leastLoad",
LoadBalance: true,
MaxQueuePerCar: 3);
}
+16
View File
@@ -0,0 +1,16 @@
namespace MiGu.Server.Configs;
public record IntersectionPolicy(string Id, List<string> SiteIds, string Mode);
public record ZoneMutex(string Id, List<string> ZoneIds);
public record DynamicYield(string Id, string From, string To, string Condition);
public record TrafficRule(
List<IntersectionPolicy> Intersections,
List<ZoneMutex> Mutex,
List<DynamicYield> Yields)
{
public static TrafficRule Default() => new(
Intersections: new List<IntersectionPolicy> { new("IX1", new List<string> { "S006", "S007" }, "mutex") },
Mutex: new List<ZoneMutex> { new("MZ1", new List<string> { "Z-CROSS" }) },
Yields: new List<DynamicYield> { new("YD1", "A 区", "B 区", "priority<peer") });
}
@@ -0,0 +1,17 @@
namespace MiGu.Server.Configs;
public record FaultReportPolicy(bool Enabled, List<string> EmailTo);
public record AutoRepairPolicy(bool Enabled, int CooldownSec);
public record VehicleMaintenancePolicy(
double LowBatteryThreshold,
double CriticalBatteryThreshold,
FaultReportPolicy FaultReport,
AutoRepairPolicy AutoRepair)
{
public static VehicleMaintenancePolicy Default() => new(
LowBatteryThreshold: 0.3,
CriticalBatteryThreshold: 0.15,
FaultReport: new FaultReportPolicy(true, new List<string> { "ops@example.com" }),
AutoRepair: new AutoRepairPolicy(false, 600));
}