refactor: enforce one top-level type per file
This commit is contained in:
parent
6ee0568e02
commit
86af47ad09
@ -400,11 +400,11 @@ git commit -m "refactor: classify backend business errors"
|
|||||||
- Create `tests/BuildFlow.Workflow.Tests/Form/FormTestFixtureClassFixture.cs` from `FormTestFixture.cs`.
|
- Create `tests/BuildFlow.Workflow.Tests/Form/FormTestFixtureClassFixture.cs` from `FormTestFixture.cs`.
|
||||||
- Create `tests/BuildFlow.Workflow.Tests/Notification/NoOpPlatformNotificationPublisher.cs` from `NotificationServiceTests.cs`.
|
- Create `tests/BuildFlow.Workflow.Tests/Notification/NoOpPlatformNotificationPublisher.cs` from `NotificationServiceTests.cs`.
|
||||||
|
|
||||||
- [ ] **Step 1: Move declarations without changing behavior**
|
- [x] **Step 1: Move declarations without changing behavior**
|
||||||
|
|
||||||
Preserve namespaces, modifiers, XML comments, JSON attributes, extension methods, and member order. Each original file retains its namesake type; each moved declaration gets a same-named file.
|
Preserve namespaces, modifiers, XML comments, JSON attributes, extension methods, and member order. Each original file retains its namesake type; each moved declaration gets a same-named file.
|
||||||
|
|
||||||
- [ ] **Step 2: Run the file organization guard**
|
- [x] **Step 2: Run the file organization guard**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet test tests/BuildFlow.Architecture.Tests/BuildFlow.Architecture.Tests.csproj --no-restore --filter FullyQualifiedName~SourceFilesShouldContainAtMostOneTopLevelClassRecordOrEnum
|
dotnet test tests/BuildFlow.Architecture.Tests/BuildFlow.Architecture.Tests.csproj --no-restore --filter FullyQualifiedName~SourceFilesShouldContainAtMostOneTopLevelClassRecordOrEnum
|
||||||
@ -412,7 +412,7 @@ dotnet test tests/BuildFlow.Architecture.Tests/BuildFlow.Architecture.Tests.cspr
|
|||||||
|
|
||||||
Expected: PASS with zero violations under `src` and `tests`.
|
Expected: PASS with zero violations under `src` and `tests`.
|
||||||
|
|
||||||
- [ ] **Step 3: Run affected projects**
|
- [x] **Step 3: Run affected projects**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet test tests/BuildFlow.Platform.Tests/BuildFlow.Platform.Tests.csproj --no-restore
|
dotnet test tests/BuildFlow.Platform.Tests/BuildFlow.Platform.Tests.csproj --no-restore
|
||||||
@ -423,7 +423,7 @@ dotnet build src/BuildFlow.Host/BuildFlow.Host.csproj --no-restore
|
|||||||
|
|
||||||
Expected: PASS, 0 warning, 0 error.
|
Expected: PASS, 0 warning, 0 error.
|
||||||
|
|
||||||
- [ ] **Step 4: Commit**
|
- [x] **Step 4: Commit**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add src/BuildFlow.Host/Configuration src/modules/workflow/Domain/Engine tests
|
git add src/BuildFlow.Host/Configuration src/modules/workflow/Domain/Engine tests
|
||||||
|
|||||||
@ -2,36 +2,6 @@ using System.Net.Http.Headers;
|
|||||||
|
|
||||||
namespace BuildFlow.Host.Configuration;
|
namespace BuildFlow.Host.Configuration;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 轻量 Consul KV 配置 Provider(自实现,替代 Winton.Extensions.Configuration.Consul)。
|
|
||||||
/// 启动时用 Consul HTTP API 拉取 prefix 下所有 KV,扁平化注入 IConfiguration。
|
|
||||||
/// 支持 ReloadOnChange(后台 long-poll watch)。
|
|
||||||
///
|
|
||||||
/// 与 Winton 的区别:完全可控、有日志、不黑盒;用 HttpClient 直接调 /v1/kv/?recurse=true。
|
|
||||||
///
|
|
||||||
/// KV key 映射:Consul key 去掉 root 前缀后,剩余部分直接作为 IConfiguration key。
|
|
||||||
/// 如 Consul key "buildflow/Jwt:SigningKey",root="buildflow/",剩余 "Jwt:SigningKey" → IConfiguration key "Jwt:SigningKey"。
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class ConsulConfigurationSource : IConfigurationSource
|
|
||||||
{
|
|
||||||
private readonly ConsulOptions _options;
|
|
||||||
|
|
||||||
public ConsulConfigurationSource(ConsulOptions options) => _options = options;
|
|
||||||
|
|
||||||
public IConfigurationProvider Build(IConfigurationBuilder builder) =>
|
|
||||||
new ConsulConfigurationProvider(_options);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class ConsulOptions
|
|
||||||
{
|
|
||||||
public required string Host { get; init; } // 如 http://43.134.235.58:8520
|
|
||||||
public required string Token { get; init; } // ACL token
|
|
||||||
public required string KeyPrefix { get; init; } // 如 buildflow/
|
|
||||||
public bool Optional { get; init; } = true;
|
|
||||||
public bool ReloadOnChange { get; init; } = true;
|
|
||||||
public TimeSpan PollInterval { get; init; } = TimeSpan.FromSeconds(30);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class ConsulConfigurationProvider : ConfigurationProvider, IDisposable
|
internal sealed class ConsulConfigurationProvider : ConfigurationProvider, IDisposable
|
||||||
{
|
{
|
||||||
private readonly ConsulOptions _options;
|
private readonly ConsulOptions _options;
|
||||||
@ -160,14 +130,3 @@ internal sealed class ConsulConfigurationProvider : ConfigurationProvider, IDisp
|
|||||||
_loggerFactory?.Dispose();
|
_loggerFactory?.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consul KV API 响应项(recurse 接口)
|
|
||||||
internal sealed class ConsulKvItem
|
|
||||||
{
|
|
||||||
public int CreateIndex { get; set; }
|
|
||||||
public int ModifyIndex { get; set; }
|
|
||||||
public string Key { get; set; } = "";
|
|
||||||
public int LockIndex { get; set; }
|
|
||||||
public string? Value { get; set; }
|
|
||||||
public int Flags { get; set; }
|
|
||||||
}
|
|
||||||
|
|||||||
@ -0,0 +1,21 @@
|
|||||||
|
namespace BuildFlow.Host.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 轻量 Consul KV 配置 Provider(自实现,替代 Winton.Extensions.Configuration.Consul)。
|
||||||
|
/// 启动时用 Consul HTTP API 拉取 prefix 下所有 KV,扁平化注入 IConfiguration。
|
||||||
|
/// 支持 ReloadOnChange(后台 long-poll watch)。
|
||||||
|
///
|
||||||
|
/// 与 Winton 的区别:完全可控、有日志、不黑盒;用 HttpClient 直接调 /v1/kv/?recurse=true。
|
||||||
|
///
|
||||||
|
/// KV key 映射:Consul key 去掉 root 前缀后,剩余部分直接作为 IConfiguration key。
|
||||||
|
/// 如 Consul key "buildflow/Jwt:SigningKey",root="buildflow/",剩余 "Jwt:SigningKey" → IConfiguration key "Jwt:SigningKey"。
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class ConsulConfigurationSource : IConfigurationSource
|
||||||
|
{
|
||||||
|
private readonly ConsulOptions _options;
|
||||||
|
|
||||||
|
public ConsulConfigurationSource(ConsulOptions options) => _options = options;
|
||||||
|
|
||||||
|
public IConfigurationProvider Build(IConfigurationBuilder builder) =>
|
||||||
|
new ConsulConfigurationProvider(_options);
|
||||||
|
}
|
||||||
12
src/BuildFlow.Host/Configuration/ConsulKvItem.cs
Normal file
12
src/BuildFlow.Host/Configuration/ConsulKvItem.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
namespace BuildFlow.Host.Configuration;
|
||||||
|
|
||||||
|
// Consul KV API 响应项(recurse 接口)
|
||||||
|
internal sealed class ConsulKvItem
|
||||||
|
{
|
||||||
|
public int CreateIndex { get; set; }
|
||||||
|
public int ModifyIndex { get; set; }
|
||||||
|
public string Key { get; set; } = "";
|
||||||
|
public int LockIndex { get; set; }
|
||||||
|
public string? Value { get; set; }
|
||||||
|
public int Flags { get; set; }
|
||||||
|
}
|
||||||
11
src/BuildFlow.Host/Configuration/ConsulOptions.cs
Normal file
11
src/BuildFlow.Host/Configuration/ConsulOptions.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
namespace BuildFlow.Host.Configuration;
|
||||||
|
|
||||||
|
internal sealed class ConsulOptions
|
||||||
|
{
|
||||||
|
public required string Host { get; init; } // 如 http://43.134.235.58:8520
|
||||||
|
public required string Token { get; init; } // ACL token
|
||||||
|
public required string KeyPrefix { get; init; } // 如 buildflow/
|
||||||
|
public bool Optional { get; init; } = true;
|
||||||
|
public bool ReloadOnChange { get; init; } = true;
|
||||||
|
public TimeSpan PollInterval { get; init; } = TimeSpan.FromSeconds(30);
|
||||||
|
}
|
||||||
49
src/modules/workflow/Domain/Engine/GraphEdge.cs
Normal file
49
src/modules/workflow/Domain/Engine/GraphEdge.cs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Workflow.Domain.Enums;
|
||||||
|
|
||||||
|
namespace Workflow.Domain.Engine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// X6 边。source/target 用 { cell, port } 结构;几何信息(vertices/router/connector)放顶层;
|
||||||
|
/// 业务字段(edgeType/condition/order)放 data。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GraphEdge
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("shape")]
|
||||||
|
public string? Shape { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("source")]
|
||||||
|
public GraphEndpoint Source { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonPropertyName("target")]
|
||||||
|
public GraphEndpoint Target { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>业务数据:edgeType / condition / order。</summary>
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public JsonElement? Data { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("label")]
|
||||||
|
public JsonElement? Label { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("vertices")]
|
||||||
|
public List<GraphPoint>? Vertices { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("router")]
|
||||||
|
public JsonElement? Router { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("connector")]
|
||||||
|
public JsonElement? Connector { get; set; }
|
||||||
|
|
||||||
|
/// <summary>边类型(Normal=0/Approved=1/Rejected=2)。缺省为 Normal。</summary>
|
||||||
|
public EdgeType GetEdgeType() => Data.TryGetInt32("edgeType") is { } v ? (EdgeType)v : EdgeType.Normal;
|
||||||
|
|
||||||
|
/// <summary>条件表达式(Condition/ExclusiveGateway 出边)。</summary>
|
||||||
|
public string? GetCondition() => Data.TryGetString("condition");
|
||||||
|
|
||||||
|
/// <summary>分支评估顺序(值小优先)。</summary>
|
||||||
|
public int GetOrder() => Data.TryGetInt32("order") ?? 0;
|
||||||
|
}
|
||||||
14
src/modules/workflow/Domain/Engine/GraphEndpoint.cs
Normal file
14
src/modules/workflow/Domain/Engine/GraphEndpoint.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Workflow.Domain.Engine;
|
||||||
|
|
||||||
|
public sealed class GraphEndpoint
|
||||||
|
{
|
||||||
|
/// <summary>节点 id(对应 GraphNode.Id)。</summary>
|
||||||
|
[JsonPropertyName("cell")]
|
||||||
|
public string? Cell { get; set; }
|
||||||
|
|
||||||
|
/// <summary>端口 id(top/right/bottom/left)。</summary>
|
||||||
|
[JsonPropertyName("port")]
|
||||||
|
public string? Port { get; set; }
|
||||||
|
}
|
||||||
25
src/modules/workflow/Domain/Engine/GraphJsonExtensions.cs
Normal file
25
src/modules/workflow/Domain/Engine/GraphJsonExtensions.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Workflow.Domain.Engine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JsonElement? 安全取值扩展:统一处理 Nullable 解构与 ValueKind 校验,
|
||||||
|
/// 避免 WorkflowGraph 各 getter 重复 null 检查。
|
||||||
|
/// </summary>
|
||||||
|
internal static class GraphJsonExtensions
|
||||||
|
{
|
||||||
|
public static string? TryGetString(this JsonElement? element, string name) =>
|
||||||
|
element is { ValueKind: JsonValueKind.Object } el
|
||||||
|
&& el.TryGetProperty(name, out var p)
|
||||||
|
&& p.ValueKind == JsonValueKind.String
|
||||||
|
? p.GetString()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
public static int? TryGetInt32(this JsonElement? element, string name) =>
|
||||||
|
element is { ValueKind: JsonValueKind.Object } el
|
||||||
|
&& el.TryGetProperty(name, out var p)
|
||||||
|
&& (p.ValueKind == JsonValueKind.Number || p.ValueKind == JsonValueKind.String)
|
||||||
|
&& p.TryGetInt32(out var n)
|
||||||
|
? n
|
||||||
|
: null;
|
||||||
|
}
|
||||||
45
src/modules/workflow/Domain/Engine/GraphNode.cs
Normal file
45
src/modules/workflow/Domain/Engine/GraphNode.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Workflow.Domain.Engine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// X6 节点。shape 决定渲染器并反推 NodeType(见 <see cref="WorkflowShapeMap"/>);
|
||||||
|
/// 业务配置统一进 data;x/y/width/height/ports 为几何与渲染信息。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GraphNode
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("shape")]
|
||||||
|
public string Shape { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("x")]
|
||||||
|
public double X { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("y")]
|
||||||
|
public double Y { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("width")]
|
||||||
|
public double? Width { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("height")]
|
||||||
|
public double? Height { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("angle")]
|
||||||
|
public double? Angle { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("visible")]
|
||||||
|
public bool? Visible { get; set; }
|
||||||
|
|
||||||
|
/// <summary>业务数据(节点配置)。替代旧 WorkflowNode.Config 字符串字段。</summary>
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public JsonElement? Data { get; set; }
|
||||||
|
|
||||||
|
/// <summary>显示名(对应旧 WorkflowNode.Name)。</summary>
|
||||||
|
public string? GetTitle() => Data.TryGetString("title");
|
||||||
|
|
||||||
|
[JsonPropertyName("ports")]
|
||||||
|
public JsonElement? Ports { get; set; }
|
||||||
|
}
|
||||||
12
src/modules/workflow/Domain/Engine/GraphPoint.cs
Normal file
12
src/modules/workflow/Domain/Engine/GraphPoint.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Workflow.Domain.Engine;
|
||||||
|
|
||||||
|
public sealed class GraphPoint
|
||||||
|
{
|
||||||
|
[JsonPropertyName("x")]
|
||||||
|
public double X { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("y")]
|
||||||
|
public double Y { get; set; }
|
||||||
|
}
|
||||||
26
src/modules/workflow/Domain/Engine/TreeBranch.cs
Normal file
26
src/modules/workflow/Domain/Engine/TreeBranch.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Workflow.Domain.Engine;
|
||||||
|
|
||||||
|
/// <summary>条件/并行分支容器中的一条分支。</summary>
|
||||||
|
public sealed class TreeBranch
|
||||||
|
{
|
||||||
|
/// <summary>分支 id,纯 Guid 字符串。</summary>
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>分支显示名(如"条件 1"/"默认分支"/"分支 A")。</summary>
|
||||||
|
[JsonPropertyName("label")]
|
||||||
|
public string? Label { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 条件表达式(仅 ConditionBranch 使用;复用 ConditionEvaluator 语法,如 <c>金额 > 5000</c>)。
|
||||||
|
/// 默认分支(else)置空,编译时 order 最大,引擎兜底命中。
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("condition")]
|
||||||
|
public string? Condition { get; set; }
|
||||||
|
|
||||||
|
/// <summary>该分支内的首节点(分支内又是顺序链;null 表示空分支,编译时直通收口)。</summary>
|
||||||
|
[JsonPropertyName("child")]
|
||||||
|
public TreeNode? Child { get; set; }
|
||||||
|
}
|
||||||
41
src/modules/workflow/Domain/Engine/TreeNode.cs
Normal file
41
src/modules/workflow/Domain/Engine/TreeNode.cs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Workflow.Domain.Enums;
|
||||||
|
|
||||||
|
namespace Workflow.Domain.Engine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 树节点。一条顺序链上的节点通过 <see cref="Child"/> 链接;
|
||||||
|
/// 容器型节点(条件/并行)通过 <see cref="Branches"/> 挂多条分支。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TreeNode
|
||||||
|
{
|
||||||
|
/// <summary>节点 id,必须是纯 Guid 字符串(引擎 ParseGuid 硬约束)。</summary>
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>节点类型。决定渲染卡片与配置面板。</summary>
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
|
public TreeNodeTypeEnum Type { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 节点业务配置(自由对象)。沿用引擎 node.data 的字段约定:
|
||||||
|
/// 审批节点:approvalMode/assigneeRules/timeoutMinutes/fieldPermissions 等;
|
||||||
|
/// 抄送节点:recipients;分支内首节点的 condition 在 <see cref="TreeBranch"/> 上。
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("config")]
|
||||||
|
public JsonElement? Config { get; set; }
|
||||||
|
|
||||||
|
/// <summary>显示标题(冗余于 config.title,便于编辑器直接读取)。</summary>
|
||||||
|
[JsonPropertyName("title")]
|
||||||
|
public string? Title { get; set; }
|
||||||
|
|
||||||
|
/// <summary>顺序链上的下一个节点(null 表示链尾,编译器自动接 End)。</summary>
|
||||||
|
[JsonPropertyName("child")]
|
||||||
|
public TreeNode? Child { get; set; }
|
||||||
|
|
||||||
|
/// <summary>分支集合(仅 ConditionBranch / ParallelBranch 容器节点使用)。</summary>
|
||||||
|
[JsonPropertyName("branches")]
|
||||||
|
public List<TreeBranch>? Branches { get; set; }
|
||||||
|
}
|
||||||
@ -20,130 +20,3 @@ public sealed class WorkflowGraph
|
|||||||
/// <summary>空图工厂(容错用)。</summary>
|
/// <summary>空图工厂(容错用)。</summary>
|
||||||
public static WorkflowGraph Empty() => new();
|
public static WorkflowGraph Empty() => new();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// X6 节点。shape 决定渲染器并反推 NodeType(见 <see cref="WorkflowShapeMap"/>);
|
|
||||||
/// 业务配置统一进 data;x/y/width/height/ports 为几何与渲染信息。
|
|
||||||
/// </summary>
|
|
||||||
public sealed class GraphNode
|
|
||||||
{
|
|
||||||
[JsonPropertyName("id")]
|
|
||||||
public string Id { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
[JsonPropertyName("shape")]
|
|
||||||
public string Shape { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
[JsonPropertyName("x")]
|
|
||||||
public double X { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("y")]
|
|
||||||
public double Y { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("width")]
|
|
||||||
public double? Width { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("height")]
|
|
||||||
public double? Height { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("angle")]
|
|
||||||
public double? Angle { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("visible")]
|
|
||||||
public bool? Visible { get; set; }
|
|
||||||
|
|
||||||
/// <summary>业务数据(节点配置)。替代旧 WorkflowNode.Config 字符串字段。</summary>
|
|
||||||
[JsonPropertyName("data")]
|
|
||||||
public JsonElement? Data { get; set; }
|
|
||||||
|
|
||||||
/// <summary>显示名(对应旧 WorkflowNode.Name)。</summary>
|
|
||||||
public string? GetTitle() => Data.TryGetString("title");
|
|
||||||
|
|
||||||
[JsonPropertyName("ports")]
|
|
||||||
public JsonElement? Ports { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// X6 边。source/target 用 { cell, port } 结构;几何信息(vertices/router/connector)放顶层;
|
|
||||||
/// 业务字段(edgeType/condition/order)放 data。
|
|
||||||
/// </summary>
|
|
||||||
public sealed class GraphEdge
|
|
||||||
{
|
|
||||||
[JsonPropertyName("id")]
|
|
||||||
public string Id { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
[JsonPropertyName("shape")]
|
|
||||||
public string? Shape { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("source")]
|
|
||||||
public GraphEndpoint Source { get; set; } = new();
|
|
||||||
|
|
||||||
[JsonPropertyName("target")]
|
|
||||||
public GraphEndpoint Target { get; set; } = new();
|
|
||||||
|
|
||||||
/// <summary>业务数据:edgeType / condition / order。</summary>
|
|
||||||
[JsonPropertyName("data")]
|
|
||||||
public JsonElement? Data { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("label")]
|
|
||||||
public JsonElement? Label { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("vertices")]
|
|
||||||
public List<GraphPoint>? Vertices { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("router")]
|
|
||||||
public JsonElement? Router { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("connector")]
|
|
||||||
public JsonElement? Connector { get; set; }
|
|
||||||
|
|
||||||
/// <summary>边类型(Normal=0/Approved=1/Rejected=2)。缺省为 Normal。</summary>
|
|
||||||
public EdgeType GetEdgeType() => Data.TryGetInt32("edgeType") is { } v ? (EdgeType)v : EdgeType.Normal;
|
|
||||||
|
|
||||||
/// <summary>条件表达式(Condition/ExclusiveGateway 出边)。</summary>
|
|
||||||
public string? GetCondition() => Data.TryGetString("condition");
|
|
||||||
|
|
||||||
/// <summary>分支评估顺序(值小优先)。</summary>
|
|
||||||
public int GetOrder() => Data.TryGetInt32("order") ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class GraphEndpoint
|
|
||||||
{
|
|
||||||
/// <summary>节点 id(对应 GraphNode.Id)。</summary>
|
|
||||||
[JsonPropertyName("cell")]
|
|
||||||
public string? Cell { get; set; }
|
|
||||||
|
|
||||||
/// <summary>端口 id(top/right/bottom/left)。</summary>
|
|
||||||
[JsonPropertyName("port")]
|
|
||||||
public string? Port { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class GraphPoint
|
|
||||||
{
|
|
||||||
[JsonPropertyName("x")]
|
|
||||||
public double X { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("y")]
|
|
||||||
public double Y { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// JsonElement? 安全取值扩展:统一处理 Nullable 解构与 ValueKind 校验,
|
|
||||||
/// 避免 WorkflowGraph 各 getter 重复 null 检查。
|
|
||||||
/// </summary>
|
|
||||||
internal static class GraphJsonExtensions
|
|
||||||
{
|
|
||||||
public static string? TryGetString(this JsonElement? element, string name) =>
|
|
||||||
element is { ValueKind: JsonValueKind.Object } el
|
|
||||||
&& el.TryGetProperty(name, out var p)
|
|
||||||
&& p.ValueKind == JsonValueKind.String
|
|
||||||
? p.GetString()
|
|
||||||
: null;
|
|
||||||
|
|
||||||
public static int? TryGetInt32(this JsonElement? element, string name) =>
|
|
||||||
element is { ValueKind: JsonValueKind.Object } el
|
|
||||||
&& el.TryGetProperty(name, out var p)
|
|
||||||
&& (p.ValueKind == JsonValueKind.Number || p.ValueKind == JsonValueKind.String)
|
|
||||||
&& p.TryGetInt32(out var n)
|
|
||||||
? n
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -27,62 +27,3 @@ public sealed class WorkflowTree
|
|||||||
/// <summary>空树工厂:用于缺省/容错。</summary>
|
/// <summary>空树工厂:用于缺省/容错。</summary>
|
||||||
public static WorkflowTree Empty() => new();
|
public static WorkflowTree Empty() => new();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 树节点。一条顺序链上的节点通过 <see cref="Child"/> 链接;
|
|
||||||
/// 容器型节点(条件/并行)通过 <see cref="Branches"/> 挂多条分支。
|
|
||||||
/// </summary>
|
|
||||||
public sealed class TreeNode
|
|
||||||
{
|
|
||||||
/// <summary>节点 id,必须是纯 Guid 字符串(引擎 ParseGuid 硬约束)。</summary>
|
|
||||||
[JsonPropertyName("id")]
|
|
||||||
public string Id { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>节点类型。决定渲染卡片与配置面板。</summary>
|
|
||||||
[JsonPropertyName("type")]
|
|
||||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
|
||||||
public TreeNodeTypeEnum Type { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 节点业务配置(自由对象)。沿用引擎 node.data 的字段约定:
|
|
||||||
/// 审批节点:approvalMode/assigneeRules/timeoutMinutes/fieldPermissions 等;
|
|
||||||
/// 抄送节点:recipients;分支内首节点的 condition 在 <see cref="TreeBranch"/> 上。
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("config")]
|
|
||||||
public JsonElement? Config { get; set; }
|
|
||||||
|
|
||||||
/// <summary>显示标题(冗余于 config.title,便于编辑器直接读取)。</summary>
|
|
||||||
[JsonPropertyName("title")]
|
|
||||||
public string? Title { get; set; }
|
|
||||||
|
|
||||||
/// <summary>顺序链上的下一个节点(null 表示链尾,编译器自动接 End)。</summary>
|
|
||||||
[JsonPropertyName("child")]
|
|
||||||
public TreeNode? Child { get; set; }
|
|
||||||
|
|
||||||
/// <summary>分支集合(仅 ConditionBranch / ParallelBranch 容器节点使用)。</summary>
|
|
||||||
[JsonPropertyName("branches")]
|
|
||||||
public List<TreeBranch>? Branches { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>条件/并行分支容器中的一条分支。</summary>
|
|
||||||
public sealed class TreeBranch
|
|
||||||
{
|
|
||||||
/// <summary>分支 id,纯 Guid 字符串。</summary>
|
|
||||||
[JsonPropertyName("id")]
|
|
||||||
public string Id { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>分支显示名(如"条件 1"/"默认分支"/"分支 A")。</summary>
|
|
||||||
[JsonPropertyName("label")]
|
|
||||||
public string? Label { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 条件表达式(仅 ConditionBranch 使用;复用 ConditionEvaluator 语法,如 <c>金额 > 5000</c>)。
|
|
||||||
/// 默认分支(else)置空,编译时 order 最大,引擎兜底命中。
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("condition")]
|
|
||||||
public string? Condition { get; set; }
|
|
||||||
|
|
||||||
/// <summary>该分支内的首节点(分支内又是顺序链;null 表示空分支,编译时直通收口)。</summary>
|
|
||||||
[JsonPropertyName("child")]
|
|
||||||
public TreeNode? Child { get; set; }
|
|
||||||
}
|
|
||||||
|
|||||||
64
tests/BuildFlow.Platform.Tests/DictionaryConfiguration.cs
Normal file
64
tests/BuildFlow.Platform.Tests/DictionaryConfiguration.cs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
|
namespace BuildFlow.Platform.Tests;
|
||||||
|
|
||||||
|
/// 极简字典实现的 IConfiguration,仅供测试使用。
|
||||||
|
/// 完整实现 GetSection/GetChildren,让 GetValue<T> 扩展方法(走 ConfigurationBinder)能正确工作。
|
||||||
|
/// 注意:ConfigurationBinder.GetValue 内部调用 GetSection(key).Value,故必须返回有效 section。
|
||||||
|
internal sealed class DictionaryConfiguration : IConfiguration
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, string?> _values = [];
|
||||||
|
|
||||||
|
public string? this[string key]
|
||||||
|
{
|
||||||
|
get => _values.TryGetValue(key, out var v) ? v : null;
|
||||||
|
set => _values[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支持集合初始化器语法:new DictionaryConfiguration { ["k"] = "v" }
|
||||||
|
public void Add(string key, string? value) => _values[key] = value;
|
||||||
|
|
||||||
|
IEnumerable<IConfigurationSection> IConfiguration.GetChildren() =>
|
||||||
|
_values.Select(kv => (IConfigurationSection)new LeafSection(kv.Key, kv.Value)).ToList();
|
||||||
|
|
||||||
|
IChangeToken IConfiguration.GetReloadToken() => EmptyChangeToken.Instance;
|
||||||
|
|
||||||
|
IConfigurationSection IConfiguration.GetSection(string key) =>
|
||||||
|
new LeafSection(key, _values.TryGetValue(key, out var v) ? v : null);
|
||||||
|
|
||||||
|
private sealed class LeafSection(string key, string? value) : IConfigurationSection
|
||||||
|
{
|
||||||
|
public string Key => key;
|
||||||
|
public string Path => key;
|
||||||
|
public string? Value { get; set; } = value;
|
||||||
|
|
||||||
|
string? IConfiguration.this[string childKey]
|
||||||
|
{
|
||||||
|
get => null;
|
||||||
|
set { }
|
||||||
|
}
|
||||||
|
|
||||||
|
IConfigurationSection IConfiguration.GetSection(string childKey) =>
|
||||||
|
new LeafSection($"{key}:{childKey}", null);
|
||||||
|
|
||||||
|
IEnumerable<IConfigurationSection> IConfiguration.GetChildren() => [];
|
||||||
|
|
||||||
|
IChangeToken IConfiguration.GetReloadToken() => EmptyChangeToken.Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>永不变更的占位 IChangeToken,避免引入额外包依赖。</summary>
|
||||||
|
private sealed class EmptyChangeToken : IChangeToken
|
||||||
|
{
|
||||||
|
public static readonly EmptyChangeToken Instance = new();
|
||||||
|
public bool ActiveChangeCallbacks => false;
|
||||||
|
public bool HasChanged => false;
|
||||||
|
public IDisposable RegisterChangeCallback(Action<object> callback, object? state) =>
|
||||||
|
new NoopDisposable();
|
||||||
|
|
||||||
|
private sealed class NoopDisposable : IDisposable
|
||||||
|
{
|
||||||
|
public void Dispose() { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,7 +3,6 @@ using BuildFlow.Platform.Infrastructure.Persistence;
|
|||||||
using BuildFlow.Shared.Domain.Common;
|
using BuildFlow.Shared.Domain.Common;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Primitives;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace BuildFlow.Platform.Tests;
|
namespace BuildFlow.Platform.Tests;
|
||||||
@ -106,63 +105,3 @@ internal static class PlatformTestDb
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 极简字典实现的 IConfiguration,仅供测试使用。
|
|
||||||
/// 完整实现 GetSection/GetChildren,让 GetValue<T> 扩展方法(走 ConfigurationBinder)能正确工作。
|
|
||||||
/// 注意:ConfigurationBinder.GetValue 内部调用 GetSection(key).Value,故必须返回有效 section。
|
|
||||||
internal sealed class DictionaryConfiguration : IConfiguration
|
|
||||||
{
|
|
||||||
private readonly Dictionary<string, string?> _values = [];
|
|
||||||
|
|
||||||
public string? this[string key]
|
|
||||||
{
|
|
||||||
get => _values.TryGetValue(key, out var v) ? v : null;
|
|
||||||
set => _values[key] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 支持集合初始化器语法:new DictionaryConfiguration { ["k"] = "v" }
|
|
||||||
public void Add(string key, string? value) => _values[key] = value;
|
|
||||||
|
|
||||||
IEnumerable<IConfigurationSection> IConfiguration.GetChildren() =>
|
|
||||||
_values.Select(kv => (IConfigurationSection)new LeafSection(kv.Key, kv.Value)).ToList();
|
|
||||||
|
|
||||||
IChangeToken IConfiguration.GetReloadToken() => EmptyChangeToken.Instance;
|
|
||||||
|
|
||||||
IConfigurationSection IConfiguration.GetSection(string key) =>
|
|
||||||
new LeafSection(key, _values.TryGetValue(key, out var v) ? v : null);
|
|
||||||
|
|
||||||
private sealed class LeafSection(string key, string? value) : IConfigurationSection
|
|
||||||
{
|
|
||||||
public string Key => key;
|
|
||||||
public string Path => key;
|
|
||||||
public string? Value { get; set; } = value;
|
|
||||||
|
|
||||||
string? IConfiguration.this[string childKey]
|
|
||||||
{
|
|
||||||
get => null;
|
|
||||||
set { }
|
|
||||||
}
|
|
||||||
|
|
||||||
IConfigurationSection IConfiguration.GetSection(string childKey) =>
|
|
||||||
new LeafSection($"{key}:{childKey}", null);
|
|
||||||
|
|
||||||
IEnumerable<IConfigurationSection> IConfiguration.GetChildren() => [];
|
|
||||||
|
|
||||||
IChangeToken IConfiguration.GetReloadToken() => EmptyChangeToken.Instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>永不变更的占位 IChangeToken,避免引入额外包依赖。</summary>
|
|
||||||
private sealed class EmptyChangeToken : IChangeToken
|
|
||||||
{
|
|
||||||
public static readonly EmptyChangeToken Instance = new();
|
|
||||||
public bool ActiveChangeCallbacks => false;
|
|
||||||
public bool HasChanged => false;
|
|
||||||
public IDisposable RegisterChangeCallback(Action<object> callback, object? state) =>
|
|
||||||
new NoopDisposable();
|
|
||||||
|
|
||||||
private sealed class NoopDisposable : IDisposable
|
|
||||||
{
|
|
||||||
public void Dispose() { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -24,13 +24,3 @@ public sealed class StubDocumentExtractor : IDocumentExtractor
|
|||||||
SourcePath = filePath
|
SourcePath = filePath
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 工厂:统一返回 StubDocumentExtractor。
|
|
||||||
/// </summary>
|
|
||||||
public sealed class StubDocumentExtractorFactory : IDocumentExtractorFactory
|
|
||||||
{
|
|
||||||
public StubDocumentExtractor InnerExtractor { get; } = new();
|
|
||||||
|
|
||||||
public IDocumentExtractor GetExtractor(string fileNameOrExtension) => InnerExtractor;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -0,0 +1,13 @@
|
|||||||
|
using BuildFlow.Domain.Interfaces;
|
||||||
|
|
||||||
|
namespace BuildFlow.Rag.Tests.Stub;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 工厂:统一返回 StubDocumentExtractor。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StubDocumentExtractorFactory : IDocumentExtractorFactory
|
||||||
|
{
|
||||||
|
public StubDocumentExtractor InnerExtractor { get; } = new();
|
||||||
|
|
||||||
|
public IDocumentExtractor GetExtractor(string fileNameOrExtension) => InnerExtractor;
|
||||||
|
}
|
||||||
@ -43,8 +43,3 @@ public class FormTestFixture : IDisposable
|
|||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// xUnit ClassFixture,用于在测试类级别共享 Fixture 实例。
|
|
||||||
/// </summary>
|
|
||||||
public class FormTestFixtureClassFixture : FormTestFixture;
|
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
namespace BuildFlow.Workflow.Tests.Form;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// xUnit ClassFixture,用于在测试类级别共享 Fixture 实例。
|
||||||
|
/// </summary>
|
||||||
|
public class FormTestFixtureClassFixture : FormTestFixture;
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
using Workflow.Domain.Common;
|
||||||
|
|
||||||
|
namespace BuildFlow.Workflow.Tests.Notification;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试用 no-op 平台通知发布器:转发逻辑由 Host 层实现,单元测试无需真实推送,
|
||||||
|
/// 此实现空跑返回 0,避免 NotificationService 构造依赖阻断。
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class NoOpPlatformNotificationPublisher : IPlatformNotificationPublisher
|
||||||
|
{
|
||||||
|
public Task<int> PublishAsync(
|
||||||
|
string type, string title, string content, string source,
|
||||||
|
string? relatedId, string? relatedType,
|
||||||
|
IEnumerable<Guid> recipientUserIds, CancellationToken ct = default)
|
||||||
|
=> Task.FromResult(0);
|
||||||
|
}
|
||||||
@ -255,16 +255,3 @@ public class NotificationServiceTests
|
|||||||
(await db.WebhookDeliveries.CountAsync()).Should().Be(0);
|
(await db.WebhookDeliveries.CountAsync()).Should().Be(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试用 no-op 平台通知发布器:转发逻辑由 Host 层实现,单元测试无需真实推送,
|
|
||||||
/// 此实现空跑返回 0,避免 NotificationService 构造依赖阻断。
|
|
||||||
/// </summary>
|
|
||||||
file sealed class NoOpPlatformNotificationPublisher : IPlatformNotificationPublisher
|
|
||||||
{
|
|
||||||
public Task<int> PublishAsync(
|
|
||||||
string type, string title, string content, string source,
|
|
||||||
string? relatedId, string? relatedType,
|
|
||||||
IEnumerable<Guid> recipientUserIds, CancellationToken ct = default)
|
|
||||||
=> Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user