179 lines
5.7 KiB
C#
179 lines
5.7 KiB
C#
using System;
|
|||
|
|
using System.Collections.Generic;
|
||
|
|
using System.Diagnostics.CodeAnalysis;
|
||
|
|
using System.IO;
|
||
|
|
using System.Linq;
|
||
|
|
|
||
|
|
namespace PCL.Core.App.Configuration;
|
||
|
|
|
||
|
|
public delegate void ConfigMigrationHandler(string from, string to);
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 配置文件迁移模型与工具。
|
||
|
|
/// </summary>
|
||
|
|
public class ConfigMigration
|
||
|
|
{
|
||
|
|
/// <summary>
|
||
|
|
/// 来源路径。
|
||
|
|
/// </summary>
|
||
|
|
public required string From { get; init; }
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 目标路径。
|
||
|
|
/// </summary>
|
||
|
|
public required string To { get; init; }
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 优先级 (或称"权重"),数字越大越容易被使用。
|
||
|
|
/// </summary>
|
||
|
|
public int Priority { get; init; } = 0;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 迁移实现。
|
||
|
|
/// </summary>
|
||
|
|
public required ConfigMigrationHandler OnMigration { get; init; }
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 执行配置文件迁移。
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="target">最终目标路径</param>
|
||
|
|
/// <param name="migrations">可用的迁移过程</param>
|
||
|
|
/// <returns>若找到最简方案并迁移成功,则为 <c>true</c>,否则为 <c>false</c></returns>
|
||
|
|
public static bool Migrate(string target, IEnumerable<ConfigMigration> migrations)
|
||
|
|
{
|
||
|
|
migrations = migrations as ConfigMigration[] ?? migrations.ToArray();
|
||
|
|
IEnumerable<ConfigMigration>? solution = null;
|
||
|
|
var found = (
|
||
|
|
from migration in migrations.Reverse()
|
||
|
|
let path = migration.From
|
||
|
|
where File.Exists(path) && _TryFindShortestPath(path, target, migrations, out solution)
|
||
|
|
select path
|
||
|
|
).Any();
|
||
|
|
if (!found) return false;
|
||
|
|
foreach (var migration in solution!) migration.OnMigration(migration.From, migration.To);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 寻找最短路径
|
||
|
|
// Partly generated by gpt-5 (20250904)
|
||
|
|
// ReSharper disable InvertIf, ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
|
||
|
|
private static bool _TryFindShortestPath(string start, string end,
|
||
|
|
IEnumerable<ConfigMigration> paths, [NotNullWhen(true)] out IEnumerable<ConfigMigration>? result)
|
||
|
|
{
|
||
|
|
// 起点即终点:最短过程为 0 条边
|
||
|
|
if (start == end)
|
||
|
|
{
|
||
|
|
result = [];
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 构建邻接表(有向图)
|
||
|
|
var adj = new Dictionary<string, List<ConfigMigration>>(StringComparer.Ordinal);
|
||
|
|
foreach (var p in paths)
|
||
|
|
{
|
||
|
|
if (!adj.TryGetValue(p.From, out var list))
|
||
|
|
{
|
||
|
|
list = [];
|
||
|
|
adj[p.From] = list;
|
||
|
|
}
|
||
|
|
list.Add(p);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 第一阶段:BFS 计算从 start 到各点的最短边数 dist
|
||
|
|
var dist = new Dictionary<string, int>(StringComparer.Ordinal);
|
||
|
|
var queue = new Queue<string>();
|
||
|
|
dist[start] = 0;
|
||
|
|
queue.Enqueue(start);
|
||
|
|
|
||
|
|
while (queue.Count > 0)
|
||
|
|
{
|
||
|
|
var u = queue.Dequeue();
|
||
|
|
if (!adj.TryGetValue(u, out var outgoing))
|
||
|
|
continue;
|
||
|
|
|
||
|
|
var du = dist[u];
|
||
|
|
foreach (var e in outgoing)
|
||
|
|
{
|
||
|
|
var v = e.To;
|
||
|
|
if (!dist.ContainsKey(v))
|
||
|
|
{
|
||
|
|
dist[v] = du + 1;
|
||
|
|
queue.Enqueue(v);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 不可达
|
||
|
|
if (!dist.ContainsKey(end))
|
||
|
|
{
|
||
|
|
result = null;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 将节点按深度分层,便于第二阶段的 DP
|
||
|
|
var nodesByDepth = new Dictionary<int, List<string>>();
|
||
|
|
foreach (var kv in dist)
|
||
|
|
{
|
||
|
|
if (!nodesByDepth.TryGetValue(kv.Value, out var list))
|
||
|
|
{
|
||
|
|
list = [];
|
||
|
|
nodesByDepth[kv.Value] = list;
|
||
|
|
}
|
||
|
|
list.Add(kv.Key);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 第二阶段:在所有最短路径子图上,选择累计 Priority 之和最大的路径
|
||
|
|
var bestPriority = new Dictionary<string, long>(StringComparer.Ordinal); // 累计优先级和
|
||
|
|
var prevNode = new Dictionary<string, string>(StringComparer.Ordinal); // 重建路径
|
||
|
|
var prevEdge = new Dictionary<string, ConfigMigration>(StringComparer.Ordinal);
|
||
|
|
|
||
|
|
bestPriority[start] = 0;
|
||
|
|
var maxDepth = dist[end];
|
||
|
|
|
||
|
|
for (var d = 0; d < maxDepth; d++)
|
||
|
|
{
|
||
|
|
if (!nodesByDepth.TryGetValue(d, out var layer))
|
||
|
|
continue;
|
||
|
|
|
||
|
|
foreach (var u in layer)
|
||
|
|
{
|
||
|
|
if (!bestPriority.ContainsKey(u)) continue;
|
||
|
|
|
||
|
|
if (!adj.TryGetValue(u, out var outgoing)) continue;
|
||
|
|
|
||
|
|
foreach (var e in outgoing)
|
||
|
|
{
|
||
|
|
if (!dist.TryGetValue(e.To, out var dv) || dv != d + 1)
|
||
|
|
continue; // 只考虑保持最短性的边
|
||
|
|
|
||
|
|
var candidate = bestPriority[u] + e.Priority;
|
||
|
|
if (!bestPriority.TryGetValue(e.To, out var cur) || candidate > cur)
|
||
|
|
{
|
||
|
|
bestPriority[e.To] = candidate;
|
||
|
|
prevNode[e.To] = u;
|
||
|
|
prevEdge[e.To] = e;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 重建从 start 到 end 的路径(边序列)
|
||
|
|
if (!prevEdge.ContainsKey(end)) { // 理论上不应发生
|
||
|
|
result = null;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
var path = new List<ConfigMigration>();
|
||
|
|
var curr = end;
|
||
|
|
while (curr != start)
|
||
|
|
{
|
||
|
|
var edge = prevEdge[curr];
|
||
|
|
path.Add(edge);
|
||
|
|
curr = prevNode[curr];
|
||
|
|
}
|
||
|
|
path.Reverse();
|
||
|
|
result = path;
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
// ReSharper restore InvertIf, ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
|
||
|
|
}
|