From 5332ff386716ffa63b9a511a6020277595cf0cd1 Mon Sep 17 00:00:00 2001 From: xiangning Date: Sun, 19 Jul 2026 13:09:43 +0800 Subject: [PATCH] =?UTF-8?q?refactor(monitoring):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E7=9B=91=E6=8E=A7=E6=A8=A1=E5=9D=97=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E8=A1=A8=E5=90=8D=E5=92=8C=E6=9F=A5=E8=AF=A2=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将监控模块表前缀从 mon_ 改为 b_ - 为工作流审批任务处理器添加流程引擎图加载逻辑 - 移除外部系统相关DTO和查询类的直接引用改为查询对象模式 - 为脚本执行服务添加模块程序集诊断日志 - 为分享表单提交功能增加密码验证支持 - 集成脚本模块数据库迁移支持 - 更新通知服务测试以兼容IOptionsMonitor接口变化 --- .../Configuration/DatabaseInitializer.cs | 4 + .../CreateExternalSystemEndpoint.cs | 10 - .../Monitoring/CreateExternalSystemRequest.cs | 11 + .../GetExternalSystemDetailEndpoint.cs | 4 +- .../GetExternalSystemDetailRequest.cs | 4 + .../Monitoring/GetSummaryEndpoint.cs | 9 +- .../Endpoints/Monitoring/GetSummaryRequest.cs | 11 + .../Endpoints/Monitoring/GetTrendEndpoint.cs | 9 +- .../Endpoints/Monitoring/GetTrendRequest.cs | 11 + .../Monitoring/ListExternalSystemsEndpoint.cs | 26 +- .../Monitoring/LoginExternalSystemEndpoint.cs | 10 +- .../Monitoring/LoginExternalSystemRequest.cs | 9 + .../Monitoring/TriggerSyncEndpoint.cs | 5 +- .../Monitoring/TriggerSyncRequest.cs | 4 + .../UpdateExternalSystemEndpoint.cs | 9 - .../Monitoring/UpdateExternalSystemRequest.cs | 13 + .../Shares/Public/SubmitSharedFormEndpoint.cs | 4 +- .../Shares/Public/SubmitSharedFormRequest.cs | 2 + .../LoginExternalSystemCommand.cs | 9 +- .../Commands/UpdateExternalSystemCommand.cs | 3 - .../ExternalSystems/DTOs/CredentialInput.cs | 4 + .../{ => DTOs}/ExternalSystemDetailDto.cs | 0 .../ExternalSystems/DTOs/LoginResult.cs | 7 + .../GetExternalSystemDetailQueryHandler.cs | 8 +- .../ListExternalSystemsQueryHandler.cs | 36 + .../LoginExternalSystemCommandHandler.cs | 3 +- .../GetExternalSystemDetailQuery.cs | 2 +- .../Queries/ListExternalSystemsQuery.cs | 7 + .../GetDailyTrendQueryHandler.cs | 3 +- .../GetSyncSummaryQueryHandler.cs | 5 +- .../{ => Queries}/GetDailyTrendQuery.cs | 2 +- .../{ => Queries}/GetSyncSummaryQuery.cs | 2 +- .../Sync/{ => Commands}/TriggerSyncCommand.cs | 2 +- .../TriggerSyncCommandHandler.cs | 5 +- .../monitoring/Domain/Entities/JsonModels.cs | 111 - .../Configurations/AlertRuleConfiguration.cs | 2 +- .../ExternalSystemConfiguration.cs | 2 +- .../Configurations/SyncJobConfiguration.cs | 2 +- .../SyncedRecordConfiguration.cs | 2 +- ...19044352_RenameTablesToBPrefix.Designer.cs | 350 +++ .../20260719044352_RenameTablesToBPrefix.cs | 192 ++ .../MonitoringDbContextModelSnapshot.cs | 30 +- .../Services/ScriptExecutionService.cs | 6 +- .../Commands/SubmitSharedFormCommand.cs | 4 +- .../SubmitSharedFormCommandHandler.cs | 10 + .../Engine/WorkflowGraphAdapter.cs | 12 + .../Handlers/ApproveTaskCommandHandler.cs | 13 +- ...xWorkflowTaskAttachmentsColumn.Designer.cs | 2041 +++++++++++++++++ ...041812_FixWorkflowTaskAttachmentsColumn.cs | 28 + .../Notification/NotificationServiceTests.cs | 29 +- 50 files changed, 2850 insertions(+), 237 deletions(-) create mode 100644 src/BuildFlow.Host/Endpoints/Monitoring/CreateExternalSystemRequest.cs create mode 100644 src/BuildFlow.Host/Endpoints/Monitoring/GetExternalSystemDetailRequest.cs create mode 100644 src/BuildFlow.Host/Endpoints/Monitoring/GetSummaryRequest.cs create mode 100644 src/BuildFlow.Host/Endpoints/Monitoring/GetTrendRequest.cs create mode 100644 src/BuildFlow.Host/Endpoints/Monitoring/LoginExternalSystemRequest.cs create mode 100644 src/BuildFlow.Host/Endpoints/Monitoring/TriggerSyncRequest.cs create mode 100644 src/BuildFlow.Host/Endpoints/Monitoring/UpdateExternalSystemRequest.cs rename src/modules/monitoring/Application/ExternalSystems/{ => Commands}/LoginExternalSystemCommand.cs (65%) create mode 100644 src/modules/monitoring/Application/ExternalSystems/DTOs/CredentialInput.cs rename src/modules/monitoring/Application/ExternalSystems/{ => DTOs}/ExternalSystemDetailDto.cs (100%) create mode 100644 src/modules/monitoring/Application/ExternalSystems/DTOs/LoginResult.cs rename src/modules/monitoring/Application/ExternalSystems/{ => Handlers}/GetExternalSystemDetailQueryHandler.cs (81%) create mode 100644 src/modules/monitoring/Application/ExternalSystems/Handlers/ListExternalSystemsQueryHandler.cs rename src/modules/monitoring/Application/ExternalSystems/{ => Queries}/GetExternalSystemDetailQuery.cs (83%) create mode 100644 src/modules/monitoring/Application/ExternalSystems/Queries/ListExternalSystemsQuery.cs rename src/modules/monitoring/Application/Reports/{ => Handlers}/GetDailyTrendQueryHandler.cs (91%) rename src/modules/monitoring/Application/Reports/{ => Handlers}/GetSyncSummaryQueryHandler.cs (95%) rename src/modules/monitoring/Application/Reports/{ => Queries}/GetDailyTrendQuery.cs (89%) rename src/modules/monitoring/Application/Reports/{ => Queries}/GetSyncSummaryQuery.cs (89%) rename src/modules/monitoring/Application/Sync/{ => Commands}/TriggerSyncCommand.cs (86%) rename src/modules/monitoring/Application/Sync/{ => Handlers}/TriggerSyncCommandHandler.cs (93%) delete mode 100644 src/modules/monitoring/Domain/Entities/JsonModels.cs create mode 100644 src/modules/monitoring/Infrastructure/Persistence/Migrations/20260719044352_RenameTablesToBPrefix.Designer.cs create mode 100644 src/modules/monitoring/Infrastructure/Persistence/Migrations/20260719044352_RenameTablesToBPrefix.cs create mode 100644 src/modules/workflow/Infrastructure/Persistence/Migrations/20260719041812_FixWorkflowTaskAttachmentsColumn.Designer.cs create mode 100644 src/modules/workflow/Infrastructure/Persistence/Migrations/20260719041812_FixWorkflowTaskAttachmentsColumn.cs diff --git a/src/BuildFlow.Host/Configuration/DatabaseInitializer.cs b/src/BuildFlow.Host/Configuration/DatabaseInitializer.cs index 798c35d..d9828b4 100644 --- a/src/BuildFlow.Host/Configuration/DatabaseInitializer.cs +++ b/src/BuildFlow.Host/Configuration/DatabaseInitializer.cs @@ -6,6 +6,7 @@ using IM.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore.Diagnostics; using MongoDB.Entities; using Monitoring.Infrastructure.Persistence; +using Scripting.Infrastructure.Persistence; using Workflow.Infrastructure.Persistence; using PlatformSeedData = BuildFlow.Platform.Infrastructure.Persistence.SeedData; using WorkflowSeedData = Workflow.Infrastructure.Persistence.SeedData; @@ -36,6 +37,9 @@ public static class DatabaseInitializer await MigrateAsync(sp, logger); await MigrateAsync(sp, logger, suppressPendingChanges: true); + // Scripting 与 Workflow 共用 workflow 库,迁移创建 funcs / func_execution_logs 表 + await MigrateAsync(sp, logger, suppressPendingChanges: true); + await MigrateAsync(sp, logger); // IM 列补丁:AddPinAndPinnedAtColumns 迁移因 ImDbContext Snapshot 与实体不匹配 diff --git a/src/BuildFlow.Host/Endpoints/Monitoring/CreateExternalSystemEndpoint.cs b/src/BuildFlow.Host/Endpoints/Monitoring/CreateExternalSystemEndpoint.cs index b04a5cd..282373c 100644 --- a/src/BuildFlow.Host/Endpoints/Monitoring/CreateExternalSystemEndpoint.cs +++ b/src/BuildFlow.Host/Endpoints/Monitoring/CreateExternalSystemEndpoint.cs @@ -23,13 +23,3 @@ public class CreateExternalSystemEndpoint(IMediator mediator) await Send.ResponseAsync(result, 201, ct); } } - -/// 创建外部系统请求体。 -public class CreateExternalSystemRequest -{ - public string Name { get; set; } = default!; - public int SystemType { get; set; } - public string BaseUrl { get; set; } = default!; - public int PollIntervalSeconds { get; set; } - public string? ParametersJson { get; set; } -} diff --git a/src/BuildFlow.Host/Endpoints/Monitoring/CreateExternalSystemRequest.cs b/src/BuildFlow.Host/Endpoints/Monitoring/CreateExternalSystemRequest.cs new file mode 100644 index 0000000..9ab2ed8 --- /dev/null +++ b/src/BuildFlow.Host/Endpoints/Monitoring/CreateExternalSystemRequest.cs @@ -0,0 +1,11 @@ +namespace BuildFlow.Host.Endpoints.Monitoring; + +/// 创建外部系统请求体。 +public class CreateExternalSystemRequest +{ + public string Name { get; set; } = default!; + public int SystemType { get; set; } + public string BaseUrl { get; set; } = default!; + public int PollIntervalSeconds { get; set; } + public string? ParametersJson { get; set; } +} diff --git a/src/BuildFlow.Host/Endpoints/Monitoring/GetExternalSystemDetailEndpoint.cs b/src/BuildFlow.Host/Endpoints/Monitoring/GetExternalSystemDetailEndpoint.cs index d55f8eb..f8b61de 100644 --- a/src/BuildFlow.Host/Endpoints/Monitoring/GetExternalSystemDetailEndpoint.cs +++ b/src/BuildFlow.Host/Endpoints/Monitoring/GetExternalSystemDetailEndpoint.cs @@ -1,6 +1,6 @@ using MediatR; -using Monitoring.Application.ExternalSystems; using Monitoring.Application.ExternalSystems.DTOs; +using Monitoring.Application.ExternalSystems.Queries; namespace BuildFlow.Host.Endpoints.Monitoring; @@ -20,5 +20,3 @@ public class GetExternalSystemDetailEndpoint(IMediator mediator) await Send.OkAsync(result, ct); } } - -public sealed record GetExternalSystemDetailRequest(Guid Id); diff --git a/src/BuildFlow.Host/Endpoints/Monitoring/GetExternalSystemDetailRequest.cs b/src/BuildFlow.Host/Endpoints/Monitoring/GetExternalSystemDetailRequest.cs new file mode 100644 index 0000000..bca599a --- /dev/null +++ b/src/BuildFlow.Host/Endpoints/Monitoring/GetExternalSystemDetailRequest.cs @@ -0,0 +1,4 @@ +namespace BuildFlow.Host.Endpoints.Monitoring; + +/// 查询外部系统详情请求。Id 由路由 {id} 绑定。 +public sealed record GetExternalSystemDetailRequest(Guid Id); diff --git a/src/BuildFlow.Host/Endpoints/Monitoring/GetSummaryEndpoint.cs b/src/BuildFlow.Host/Endpoints/Monitoring/GetSummaryEndpoint.cs index 5df51da..9cf73d5 100644 --- a/src/BuildFlow.Host/Endpoints/Monitoring/GetSummaryEndpoint.cs +++ b/src/BuildFlow.Host/Endpoints/Monitoring/GetSummaryEndpoint.cs @@ -1,6 +1,6 @@ using MediatR; -using Monitoring.Application.Reports; using Monitoring.Application.Reports.DTOs; +using Monitoring.Application.Reports.Queries; namespace BuildFlow.Host.Endpoints.Monitoring; @@ -21,10 +21,3 @@ public class GetSummaryEndpoint(IMediator mediator) : Endpoint同步汇总请求。SystemId 由路由 {systemId} 绑定。 +public class GetSummaryRequest +{ + public Guid SystemId { get; set; } + + /// 要返回的最近任务条数,默认 10。 + [QueryParam] + public int RecentJobs { get; set; } = 10; +} diff --git a/src/BuildFlow.Host/Endpoints/Monitoring/GetTrendEndpoint.cs b/src/BuildFlow.Host/Endpoints/Monitoring/GetTrendEndpoint.cs index 1d6358a..511e01d 100644 --- a/src/BuildFlow.Host/Endpoints/Monitoring/GetTrendEndpoint.cs +++ b/src/BuildFlow.Host/Endpoints/Monitoring/GetTrendEndpoint.cs @@ -1,6 +1,6 @@ using MediatR; -using Monitoring.Application.Reports; using Monitoring.Application.Reports.DTOs; +using Monitoring.Application.Reports.Queries; namespace BuildFlow.Host.Endpoints.Monitoring; @@ -21,10 +21,3 @@ public class GetTrendEndpoint(IMediator mediator) await Send.OkAsync(result, ct); } } - -public class GetTrendRequest -{ - public Guid SystemId { get; set; } - [QueryParam] - public int Days { get; set; } = 14; -} diff --git a/src/BuildFlow.Host/Endpoints/Monitoring/GetTrendRequest.cs b/src/BuildFlow.Host/Endpoints/Monitoring/GetTrendRequest.cs new file mode 100644 index 0000000..290c0c0 --- /dev/null +++ b/src/BuildFlow.Host/Endpoints/Monitoring/GetTrendRequest.cs @@ -0,0 +1,11 @@ +namespace BuildFlow.Host.Endpoints.Monitoring; + +/// 按天趋势请求。SystemId 由路由 {systemId} 绑定。 +public class GetTrendRequest +{ + public Guid SystemId { get; set; } + + /// 最近 N 天,默认 14。 + [QueryParam] + public int Days { get; set; } = 14; +} diff --git a/src/BuildFlow.Host/Endpoints/Monitoring/ListExternalSystemsEndpoint.cs b/src/BuildFlow.Host/Endpoints/Monitoring/ListExternalSystemsEndpoint.cs index fea2b9b..e06df96 100644 --- a/src/BuildFlow.Host/Endpoints/Monitoring/ListExternalSystemsEndpoint.cs +++ b/src/BuildFlow.Host/Endpoints/Monitoring/ListExternalSystemsEndpoint.cs @@ -1,12 +1,12 @@ -using System.Text.Json; -using Microsoft.EntityFrameworkCore; +using MediatR; using Monitoring.Application.ExternalSystems.DTOs; -using Monitoring.Infrastructure.Persistence; +using Monitoring.Application.ExternalSystems.Queries; namespace BuildFlow.Host.Endpoints.Monitoring; /// 外部系统列表。GET /monitoring/systems。 -public class ListExternalSystemsEndpoint(MonitoringDbContext db) : EndpointWithoutRequest> +public class ListExternalSystemsEndpoint(IMediator mediator) + : EndpointWithoutRequest> { public override void Configure() { @@ -16,21 +16,7 @@ public class ListExternalSystemsEndpoint(MonitoringDbContext db) : EndpointWitho public override async Task HandleAsync(CancellationToken ct) { - // json_data 在表达式树里翻译不了,先物化实体再在内存映射 DTO(外部系统数量很少,无需担心性能) - var systems = await db.ExternalSystems - .OrderByDescending(s => s.CreatedAt) - .ToListAsync(ct); - - var dtos = systems.Select(s => - { - var d = s.Data; - return new ExternalSystemDto( - s.Id, d.Name, (int)d.SystemType, d.SystemType.ToString(), - d.IsEnabled, d.BaseUrl, d.PollIntervalSeconds, - JsonSerializer.Serialize(d.Parameters), - d.LastSyncedAt, d.LastSyncJobId); - }).ToList(); - - await Send.OkAsync(dtos, ct); + var result = await mediator.Send(new ListExternalSystemsQuery(), ct); + await Send.OkAsync(result, ct); } } diff --git a/src/BuildFlow.Host/Endpoints/Monitoring/LoginExternalSystemEndpoint.cs b/src/BuildFlow.Host/Endpoints/Monitoring/LoginExternalSystemEndpoint.cs index fec8b1b..1695d7b 100644 --- a/src/BuildFlow.Host/Endpoints/Monitoring/LoginExternalSystemEndpoint.cs +++ b/src/BuildFlow.Host/Endpoints/Monitoring/LoginExternalSystemEndpoint.cs @@ -1,5 +1,6 @@ using MediatR; -using Monitoring.Application.ExternalSystems; +using Monitoring.Application.ExternalSystems.Commands; +using Monitoring.Application.ExternalSystems.DTOs; namespace BuildFlow.Host.Endpoints.Monitoring; @@ -21,10 +22,3 @@ public class LoginExternalSystemEndpoint(IMediator mediator) await Send.OkAsync(result, ct); } } - -public sealed record LoginExternalSystemRequest( - Guid Id, - string Account, - string Password, - string Ticket, - string Randstr); diff --git a/src/BuildFlow.Host/Endpoints/Monitoring/LoginExternalSystemRequest.cs b/src/BuildFlow.Host/Endpoints/Monitoring/LoginExternalSystemRequest.cs new file mode 100644 index 0000000..5826310 --- /dev/null +++ b/src/BuildFlow.Host/Endpoints/Monitoring/LoginExternalSystemRequest.cs @@ -0,0 +1,9 @@ +namespace BuildFlow.Host.Endpoints.Monitoring; + +/// 登录外部系统请求。Id 由路由 {id} 绑定。 +public sealed record LoginExternalSystemRequest( + Guid Id, + string Account, + string Password, + string Ticket, + string Randstr); diff --git a/src/BuildFlow.Host/Endpoints/Monitoring/TriggerSyncEndpoint.cs b/src/BuildFlow.Host/Endpoints/Monitoring/TriggerSyncEndpoint.cs index 7de1a3a..e7e9872 100644 --- a/src/BuildFlow.Host/Endpoints/Monitoring/TriggerSyncEndpoint.cs +++ b/src/BuildFlow.Host/Endpoints/Monitoring/TriggerSyncEndpoint.cs @@ -1,5 +1,5 @@ using MediatR; -using Monitoring.Application.Sync; +using Monitoring.Application.Sync.Commands; using Monitoring.Application.Sync.DTOs; namespace BuildFlow.Host.Endpoints.Monitoring; @@ -19,6 +19,3 @@ public class TriggerSyncEndpoint(IMediator mediator) : Endpoint触发同步请求。SystemId 由路由 {systemId} 绑定(与 PublishScriptRequest 同构)。 -public sealed record TriggerSyncRequest(Guid SystemId); diff --git a/src/BuildFlow.Host/Endpoints/Monitoring/TriggerSyncRequest.cs b/src/BuildFlow.Host/Endpoints/Monitoring/TriggerSyncRequest.cs new file mode 100644 index 0000000..186b5cc --- /dev/null +++ b/src/BuildFlow.Host/Endpoints/Monitoring/TriggerSyncRequest.cs @@ -0,0 +1,4 @@ +namespace BuildFlow.Host.Endpoints.Monitoring; + +/// 触发同步请求。SystemId 由路由 {systemId} 绑定。 +public sealed record TriggerSyncRequest(Guid SystemId); diff --git a/src/BuildFlow.Host/Endpoints/Monitoring/UpdateExternalSystemEndpoint.cs b/src/BuildFlow.Host/Endpoints/Monitoring/UpdateExternalSystemEndpoint.cs index f200390..17e8a3b 100644 --- a/src/BuildFlow.Host/Endpoints/Monitoring/UpdateExternalSystemEndpoint.cs +++ b/src/BuildFlow.Host/Endpoints/Monitoring/UpdateExternalSystemEndpoint.cs @@ -24,12 +24,3 @@ public class UpdateExternalSystemEndpoint(IMediator mediator) await Send.OkAsync(result, ct); } } - -public sealed record UpdateExternalSystemRequest( - Guid Id, - string? Name, - string? BaseUrl, - int? PollIntervalSeconds, - bool? IsEnabled, - string? ParametersJson, - List? Credentials); diff --git a/src/BuildFlow.Host/Endpoints/Monitoring/UpdateExternalSystemRequest.cs b/src/BuildFlow.Host/Endpoints/Monitoring/UpdateExternalSystemRequest.cs new file mode 100644 index 0000000..4733dbb --- /dev/null +++ b/src/BuildFlow.Host/Endpoints/Monitoring/UpdateExternalSystemRequest.cs @@ -0,0 +1,13 @@ +using Monitoring.Application.ExternalSystems.DTOs; + +namespace BuildFlow.Host.Endpoints.Monitoring; + +/// 更新外部系统请求。Id 由路由 {id} 绑定,其余字段可选(null 表示不改)。 +public sealed record UpdateExternalSystemRequest( + Guid Id, + string? Name, + string? BaseUrl, + int? PollIntervalSeconds, + bool? IsEnabled, + string? ParametersJson, + List? Credentials); diff --git a/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/Shares/Public/SubmitSharedFormEndpoint.cs b/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/Shares/Public/SubmitSharedFormEndpoint.cs index a16117d..0033675 100644 --- a/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/Shares/Public/SubmitSharedFormEndpoint.cs +++ b/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/Shares/Public/SubmitSharedFormEndpoint.cs @@ -18,8 +18,10 @@ public class SubmitSharedFormEndpoint(IMediator mediator) public override async Task HandleAsync(SubmitSharedFormRequest req, CancellationToken ct) { + // 密码优先取 body,兜底取 query(前端 form-share.vue 通过 query 传 password) + var password = req.Password ?? Query("password", isRequired: false); var formDataId = await mediator.Send( - new SubmitSharedFormCommand(req.Token, req.DataJson), + new SubmitSharedFormCommand(req.Token, req.DataJson, password), ct); await Send.ResponseAsync(new SubmitSharedFormResponse(formDataId), 201, ct); } diff --git a/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/Shares/Public/SubmitSharedFormRequest.cs b/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/Shares/Public/SubmitSharedFormRequest.cs index aaecc80..0b83170 100644 --- a/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/Shares/Public/SubmitSharedFormRequest.cs +++ b/src/BuildFlow.Host/Endpoints/Workflow/BuildFlow/Shares/Public/SubmitSharedFormRequest.cs @@ -6,4 +6,6 @@ public sealed class SubmitSharedFormRequest public string Token { get; init; } = default!; /// 表单数据 JSON 字符串。 public string DataJson { get; init; } = default!; + /// 分享密码(分享设置了密码时必传)。通过 query 的 password 或 body 均可。 + public string? Password { get; init; } } diff --git a/src/modules/monitoring/Application/ExternalSystems/LoginExternalSystemCommand.cs b/src/modules/monitoring/Application/ExternalSystems/Commands/LoginExternalSystemCommand.cs similarity index 65% rename from src/modules/monitoring/Application/ExternalSystems/LoginExternalSystemCommand.cs rename to src/modules/monitoring/Application/ExternalSystems/Commands/LoginExternalSystemCommand.cs index f9e799e..93b3a47 100644 --- a/src/modules/monitoring/Application/ExternalSystems/LoginExternalSystemCommand.cs +++ b/src/modules/monitoring/Application/ExternalSystems/Commands/LoginExternalSystemCommand.cs @@ -1,6 +1,7 @@ using MediatR; +using Monitoring.Application.ExternalSystems.DTOs; -namespace Monitoring.Application.ExternalSystems; +namespace Monitoring.Application.ExternalSystems.Commands; /// /// 登录外部系统命令。用账号密码 + 腾讯验证码票据调目标系统登录, @@ -17,9 +18,3 @@ public record LoginExternalSystemCommand( string Password, string Ticket, string Randstr) : IRequest; - -/// 登录结果。 -/// 是否成功。 -/// 提示信息。 -/// 获取到的凭证数量。 -public record LoginResult(bool Success, string? Message, int CredentialCount); diff --git a/src/modules/monitoring/Application/ExternalSystems/Commands/UpdateExternalSystemCommand.cs b/src/modules/monitoring/Application/ExternalSystems/Commands/UpdateExternalSystemCommand.cs index 990ac86..0c83bf4 100644 --- a/src/modules/monitoring/Application/ExternalSystems/Commands/UpdateExternalSystemCommand.cs +++ b/src/modules/monitoring/Application/ExternalSystems/Commands/UpdateExternalSystemCommand.cs @@ -22,6 +22,3 @@ public record UpdateExternalSystemCommand( bool? IsEnabled, string? ParametersJson, List? Credentials) : IRequest; - -/// 凭证输入(前端传入,写入 json_data.Credentials)。 -public record CredentialInput(string Type, string Key, string Value, bool IsEnabled, string? Remark); diff --git a/src/modules/monitoring/Application/ExternalSystems/DTOs/CredentialInput.cs b/src/modules/monitoring/Application/ExternalSystems/DTOs/CredentialInput.cs new file mode 100644 index 0000000..521a10c --- /dev/null +++ b/src/modules/monitoring/Application/ExternalSystems/DTOs/CredentialInput.cs @@ -0,0 +1,4 @@ +namespace Monitoring.Application.ExternalSystems.DTOs; + +/// 凭证输入(前端传入,写入 json_data.Credentials)。 +public record CredentialInput(string Type, string Key, string Value, bool IsEnabled, string? Remark); diff --git a/src/modules/monitoring/Application/ExternalSystems/ExternalSystemDetailDto.cs b/src/modules/monitoring/Application/ExternalSystems/DTOs/ExternalSystemDetailDto.cs similarity index 100% rename from src/modules/monitoring/Application/ExternalSystems/ExternalSystemDetailDto.cs rename to src/modules/monitoring/Application/ExternalSystems/DTOs/ExternalSystemDetailDto.cs diff --git a/src/modules/monitoring/Application/ExternalSystems/DTOs/LoginResult.cs b/src/modules/monitoring/Application/ExternalSystems/DTOs/LoginResult.cs new file mode 100644 index 0000000..c381933 --- /dev/null +++ b/src/modules/monitoring/Application/ExternalSystems/DTOs/LoginResult.cs @@ -0,0 +1,7 @@ +namespace Monitoring.Application.ExternalSystems.DTOs; + +/// 登录结果。 +/// 是否成功。 +/// 提示信息。 +/// 获取到的凭证数量。 +public record LoginResult(bool Success, string? Message, int CredentialCount); diff --git a/src/modules/monitoring/Application/ExternalSystems/GetExternalSystemDetailQueryHandler.cs b/src/modules/monitoring/Application/ExternalSystems/Handlers/GetExternalSystemDetailQueryHandler.cs similarity index 81% rename from src/modules/monitoring/Application/ExternalSystems/GetExternalSystemDetailQueryHandler.cs rename to src/modules/monitoring/Application/ExternalSystems/Handlers/GetExternalSystemDetailQueryHandler.cs index 07eeadb..b5e41f0 100644 --- a/src/modules/monitoring/Application/ExternalSystems/GetExternalSystemDetailQueryHandler.cs +++ b/src/modules/monitoring/Application/ExternalSystems/Handlers/GetExternalSystemDetailQueryHandler.cs @@ -1,18 +1,20 @@ using System.Text.Json; -using BuildFlow.Shared.Domain.Exceptions; using BuildFlow.Shared.Domain.Errors; +using BuildFlow.Shared.Domain.Exceptions; using MediatR; using Microsoft.EntityFrameworkCore; using Monitoring.Application.ExternalSystems.DTOs; +using Monitoring.Application.ExternalSystems.Queries; using Monitoring.Infrastructure.Persistence; -namespace Monitoring.Application.ExternalSystems; +namespace Monitoring.Application.ExternalSystems.Handlers; /// 外部系统详情查询处理器(含凭证,供前端编辑回填)。 public class GetExternalSystemDetailQueryHandler(MonitoringDbContext db) : IRequestHandler { - public async Task Handle(GetExternalSystemDetailQuery request, CancellationToken ct) + public async Task Handle( + GetExternalSystemDetailQuery request, CancellationToken ct) { var system = await db.ExternalSystems.FirstOrDefaultAsync(s => s.Id == request.Id, ct) ?? throw new NotFoundException(ErrorCode.NotFound, $"外部系统 {request.Id} 不存在"); diff --git a/src/modules/monitoring/Application/ExternalSystems/Handlers/ListExternalSystemsQueryHandler.cs b/src/modules/monitoring/Application/ExternalSystems/Handlers/ListExternalSystemsQueryHandler.cs new file mode 100644 index 0000000..0f64530 --- /dev/null +++ b/src/modules/monitoring/Application/ExternalSystems/Handlers/ListExternalSystemsQueryHandler.cs @@ -0,0 +1,36 @@ +using System.Text.Json; +using BuildFlow.Shared.Domain.Errors; +using BuildFlow.Shared.Domain.Exceptions; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Monitoring.Application.ExternalSystems.DTOs; +using Monitoring.Application.ExternalSystems.Queries; +using Monitoring.Infrastructure.Persistence; + +namespace Monitoring.Application.ExternalSystems.Handlers; + +/// +/// 外部系统列表查询处理器。按创建时间倒序返回全部系统。 +/// json_data 在表达式树里翻译不了,先物化实体再在内存映射 DTO(外部系统数量很少,无需担心性能)。 +/// +public class ListExternalSystemsQueryHandler(MonitoringDbContext db) + : IRequestHandler> +{ + public async Task> Handle( + ListExternalSystemsQuery request, CancellationToken ct) + { + var systems = await db.ExternalSystems + .OrderByDescending(s => s.CreatedAt) + .ToListAsync(ct); + + return systems.Select(s => + { + var d = s.Data; + return new ExternalSystemDto( + s.Id, d.Name, (int)d.SystemType, d.SystemType.ToString(), + d.IsEnabled, d.BaseUrl, d.PollIntervalSeconds, + JsonSerializer.Serialize(d.Parameters), + d.LastSyncedAt, d.LastSyncJobId); + }).ToList(); + } +} diff --git a/src/modules/monitoring/Application/ExternalSystems/Handlers/LoginExternalSystemCommandHandler.cs b/src/modules/monitoring/Application/ExternalSystems/Handlers/LoginExternalSystemCommandHandler.cs index e65251d..04b3599 100644 --- a/src/modules/monitoring/Application/ExternalSystems/Handlers/LoginExternalSystemCommandHandler.cs +++ b/src/modules/monitoring/Application/ExternalSystems/Handlers/LoginExternalSystemCommandHandler.cs @@ -2,7 +2,8 @@ using BuildFlow.Shared.Domain.Exceptions; using BuildFlow.Shared.Domain.Errors; using MediatR; using Microsoft.EntityFrameworkCore; -using Monitoring.Application.ExternalSystems; +using Monitoring.Application.ExternalSystems.Commands; +using Monitoring.Application.ExternalSystems.DTOs; using Monitoring.Domain.Enums; using Monitoring.Infrastructure.Connectors; using Monitoring.Infrastructure.Persistence; diff --git a/src/modules/monitoring/Application/ExternalSystems/GetExternalSystemDetailQuery.cs b/src/modules/monitoring/Application/ExternalSystems/Queries/GetExternalSystemDetailQuery.cs similarity index 83% rename from src/modules/monitoring/Application/ExternalSystems/GetExternalSystemDetailQuery.cs rename to src/modules/monitoring/Application/ExternalSystems/Queries/GetExternalSystemDetailQuery.cs index bc45bcb..04d9aed 100644 --- a/src/modules/monitoring/Application/ExternalSystems/GetExternalSystemDetailQuery.cs +++ b/src/modules/monitoring/Application/ExternalSystems/Queries/GetExternalSystemDetailQuery.cs @@ -1,7 +1,7 @@ using MediatR; using Monitoring.Application.ExternalSystems.DTOs; -namespace Monitoring.Application.ExternalSystems; +namespace Monitoring.Application.ExternalSystems.Queries; /// 查询外部系统详情(含凭证,供前端凭证编辑回填)。 /// 系统 Id。 diff --git a/src/modules/monitoring/Application/ExternalSystems/Queries/ListExternalSystemsQuery.cs b/src/modules/monitoring/Application/ExternalSystems/Queries/ListExternalSystemsQuery.cs new file mode 100644 index 0000000..0cc1688 --- /dev/null +++ b/src/modules/monitoring/Application/ExternalSystems/Queries/ListExternalSystemsQuery.cs @@ -0,0 +1,7 @@ +using MediatR; +using Monitoring.Application.ExternalSystems.DTOs; + +namespace Monitoring.Application.ExternalSystems.Queries; + +/// 查询全部外部系统列表(按创建时间倒序)。供列表页展示。 +public record ListExternalSystemsQuery : IRequest>; diff --git a/src/modules/monitoring/Application/Reports/GetDailyTrendQueryHandler.cs b/src/modules/monitoring/Application/Reports/Handlers/GetDailyTrendQueryHandler.cs similarity index 91% rename from src/modules/monitoring/Application/Reports/GetDailyTrendQueryHandler.cs rename to src/modules/monitoring/Application/Reports/Handlers/GetDailyTrendQueryHandler.cs index f295f78..7160a71 100644 --- a/src/modules/monitoring/Application/Reports/GetDailyTrendQueryHandler.cs +++ b/src/modules/monitoring/Application/Reports/Handlers/GetDailyTrendQueryHandler.cs @@ -1,9 +1,10 @@ using MediatR; using Microsoft.EntityFrameworkCore; using Monitoring.Application.Reports.DTOs; +using Monitoring.Application.Reports.Queries; using Monitoring.Infrastructure.Persistence; -namespace Monitoring.Application.Reports; +namespace Monitoring.Application.Reports.Handlers; /// 按天趋势查询处理器。在数据库侧按 occurred_at 日期分组聚合记录数。 public class GetDailyTrendQueryHandler(MonitoringDbContext db) diff --git a/src/modules/monitoring/Application/Reports/GetSyncSummaryQueryHandler.cs b/src/modules/monitoring/Application/Reports/Handlers/GetSyncSummaryQueryHandler.cs similarity index 95% rename from src/modules/monitoring/Application/Reports/GetSyncSummaryQueryHandler.cs rename to src/modules/monitoring/Application/Reports/Handlers/GetSyncSummaryQueryHandler.cs index 5be3d94..e9755ec 100644 --- a/src/modules/monitoring/Application/Reports/GetSyncSummaryQueryHandler.cs +++ b/src/modules/monitoring/Application/Reports/Handlers/GetSyncSummaryQueryHandler.cs @@ -1,12 +1,13 @@ -using BuildFlow.Shared.Domain.Exceptions; using BuildFlow.Shared.Domain.Errors; +using BuildFlow.Shared.Domain.Exceptions; using MediatR; using Microsoft.EntityFrameworkCore; using Monitoring.Application.Reports.DTOs; +using Monitoring.Application.Reports.Queries; using Monitoring.Domain.Enums; using Monitoring.Infrastructure.Persistence; -namespace Monitoring.Application.Reports; +namespace Monitoring.Application.Reports.Handlers; /// 同步汇总查询处理器。 public class GetSyncSummaryQueryHandler(MonitoringDbContext db) diff --git a/src/modules/monitoring/Application/Reports/GetDailyTrendQuery.cs b/src/modules/monitoring/Application/Reports/Queries/GetDailyTrendQuery.cs similarity index 89% rename from src/modules/monitoring/Application/Reports/GetDailyTrendQuery.cs rename to src/modules/monitoring/Application/Reports/Queries/GetDailyTrendQuery.cs index 565fce6..39d5f1e 100644 --- a/src/modules/monitoring/Application/Reports/GetDailyTrendQuery.cs +++ b/src/modules/monitoring/Application/Reports/Queries/GetDailyTrendQuery.cs @@ -1,7 +1,7 @@ using MediatR; using Monitoring.Application.Reports.DTOs; -namespace Monitoring.Application.Reports; +namespace Monitoring.Application.Reports.Queries; /// 查询某外部系统按天的记录数趋势(基于 SyncedRecord.OccurredAt 聚合)。 /// 外部系统 Id。 diff --git a/src/modules/monitoring/Application/Reports/GetSyncSummaryQuery.cs b/src/modules/monitoring/Application/Reports/Queries/GetSyncSummaryQuery.cs similarity index 89% rename from src/modules/monitoring/Application/Reports/GetSyncSummaryQuery.cs rename to src/modules/monitoring/Application/Reports/Queries/GetSyncSummaryQuery.cs index 548f67b..9746b0a 100644 --- a/src/modules/monitoring/Application/Reports/GetSyncSummaryQuery.cs +++ b/src/modules/monitoring/Application/Reports/Queries/GetSyncSummaryQuery.cs @@ -1,7 +1,7 @@ using MediatR; using Monitoring.Application.Reports.DTOs; -namespace Monitoring.Application.Reports; +namespace Monitoring.Application.Reports.Queries; /// 查询某外部系统的同步汇总:最近一次成功的汇总指标 + 最近 N 次任务执行情况。 /// 外部系统 Id。 diff --git a/src/modules/monitoring/Application/Sync/TriggerSyncCommand.cs b/src/modules/monitoring/Application/Sync/Commands/TriggerSyncCommand.cs similarity index 86% rename from src/modules/monitoring/Application/Sync/TriggerSyncCommand.cs rename to src/modules/monitoring/Application/Sync/Commands/TriggerSyncCommand.cs index 1c7cf13..70ae6fb 100644 --- a/src/modules/monitoring/Application/Sync/TriggerSyncCommand.cs +++ b/src/modules/monitoring/Application/Sync/Commands/TriggerSyncCommand.cs @@ -1,7 +1,7 @@ using MediatR; using Monitoring.Application.Sync.DTOs; -namespace Monitoring.Application.Sync; +namespace Monitoring.Application.Sync.Commands; /// 手动触发某外部系统立即同步。返回本次 SyncJob 概览。 /// 目标外部系统 Id。 diff --git a/src/modules/monitoring/Application/Sync/TriggerSyncCommandHandler.cs b/src/modules/monitoring/Application/Sync/Handlers/TriggerSyncCommandHandler.cs similarity index 93% rename from src/modules/monitoring/Application/Sync/TriggerSyncCommandHandler.cs rename to src/modules/monitoring/Application/Sync/Handlers/TriggerSyncCommandHandler.cs index fb40c80..c59de88 100644 --- a/src/modules/monitoring/Application/Sync/TriggerSyncCommandHandler.cs +++ b/src/modules/monitoring/Application/Sync/Handlers/TriggerSyncCommandHandler.cs @@ -1,11 +1,12 @@ -using BuildFlow.Shared.Domain.Exceptions; using BuildFlow.Shared.Domain.Errors; +using BuildFlow.Shared.Domain.Exceptions; using MediatR; using Microsoft.EntityFrameworkCore; +using Monitoring.Application.Sync.Commands; using Monitoring.Application.Sync.DTOs; using Monitoring.Infrastructure.Persistence; -namespace Monitoring.Application.Sync; +namespace Monitoring.Application.Sync.Handlers; /// 手动触发同步处理器。 public class TriggerSyncCommandHandler( diff --git a/src/modules/monitoring/Domain/Entities/JsonModels.cs b/src/modules/monitoring/Domain/Entities/JsonModels.cs deleted file mode 100644 index 535abd9..0000000 --- a/src/modules/monitoring/Domain/Entities/JsonModels.cs +++ /dev/null @@ -1,111 +0,0 @@ -using Monitoring.Domain.Enums; - -namespace Monitoring.Domain.Entities; - -/// -/// 外部系统 json_data 投影:承载可变/扩展字段。 -/// 凭证(Cookie/Header/Token)也内聚于此,避免单独建表;Credentials 列表为空时表示无凭证。 -/// Parameters 承载 Connector 专属配置(如 u号租的 sellerId/lookbackDays)。 -/// -public class SystemData -{ - /// 系统显示名称。 - public string Name { get; set; } = default!; - - /// 系统类型:1=UHaoZu, 100=GenericHttp。 - public ExternalSystemType SystemType { get; set; } - - /// Base URL。 - public string BaseUrl { get; set; } = default!; - - /// 轮询间隔(秒),0=用默认。 - public int PollIntervalSeconds { get; set; } - - /// 是否启用。 - public bool IsEnabled { get; set; } = true; - - /// Connector 专属参数(如 sellerId/lookbackDays)。 - public Dictionary Parameters { get; set; } = new(); - - /// 凭证列表(Cookie/Header/Token),不再单独建表。 - public List Credentials { get; set; } = new(); - - /// 最近同步时间(UTC 毫秒时间戳)。 - public long? LastSyncedAt { get; set; } - - /// 最近同步任务 Id。 - public Guid? LastSyncJobId { get; set; } -} - -/// 凭证条目(内聚于 SystemData,无独立表)。 -public class CredentialEntry -{ - /// 类型:Cookie / Header / BearerToken / Query。 - public string Type { get; set; } = default!; - - /// 键名(Cookie 名/Header 名/参数名)。 - public string Key { get; set; } = default!; - - /// 值。TODO 后续加密。 - public string Value { get; set; } = default!; - - /// 是否启用。 - public bool IsEnabled { get; set; } = true; - - /// 备注。 - public string? Remark { get; set; } -} - -/// 同步任务 json_data 投影。 -public class JobData -{ - /// 触发来源:scheduler / manual。 - public string TriggerSource { get; set; } = "scheduler"; - - /// 汇总指标(Connector 产出,如 {order_count:8283, actual_rent:9813.80})。 - public Dictionary SummaryMetrics { get; set; } = new(); - - /// 错误信息(成功时为 null)。 - public string? ErrorMessage { get; set; } -} - -/// 同步记录 json_data 投影:拉取到的外部原始数据。 -public class RecordData -{ - /// 记录类型:order / finance / metric(由 Connector 定义)。 - public string RecordType { get; set; } = default!; - - /// 外部业务 Id(订单号/流水号)。 - public string ExternalId { get; set; } = default!; - - /// 原始数据(已换算后的业务字段,如金额已从分转元)。 - public Dictionary Payload { get; set; } = new(); -} - -/// 告警规则 json_data 投影。 -public class RuleData -{ - /// 规则名称。 - public string Name { get; set; } = default!; - - /// 监控指标名(对应 JobData.SummaryMetrics 的键)。 - public string MetricName { get; set; } = default!; - - /// 比较运算符:gt/gte/lt/lte/eq。 - public string Operator { get; set; } = "gt"; - - /// 阈值。 - public string Threshold { get; set; } = default!; - - /// 严重级别。 - public AlertSeverity Severity { get; set; } = AlertSeverity.Warning; - - /// 是否启用。 - public bool IsEnabled { get; set; } = true; - - /// 触发冷却(秒)。 - public int CooldownSeconds { get; set; } = 3600; - - /// 最近触发时间(UTC 毫秒时间戳)。 - public long? LastTriggeredAt { get; set; } -} diff --git a/src/modules/monitoring/Infrastructure/Persistence/Configurations/AlertRuleConfiguration.cs b/src/modules/monitoring/Infrastructure/Persistence/Configurations/AlertRuleConfiguration.cs index 194e06f..60bfa1b 100644 --- a/src/modules/monitoring/Infrastructure/Persistence/Configurations/AlertRuleConfiguration.cs +++ b/src/modules/monitoring/Infrastructure/Persistence/Configurations/AlertRuleConfiguration.cs @@ -9,7 +9,7 @@ public class AlertRuleConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.ToTable("mon_alert_rules"); + builder.ToTable("b_alert_rules"); builder.HasKey(e => e.Id); builder.Property(e => e.Id).ValueGeneratedNever().HasComment("规则ID"); builder.Property(e => e.ExternalSystemId).HasComment("绑定的外部系统ID"); diff --git a/src/modules/monitoring/Infrastructure/Persistence/Configurations/ExternalSystemConfiguration.cs b/src/modules/monitoring/Infrastructure/Persistence/Configurations/ExternalSystemConfiguration.cs index defdb69..7f915b0 100644 --- a/src/modules/monitoring/Infrastructure/Persistence/Configurations/ExternalSystemConfiguration.cs +++ b/src/modules/monitoring/Infrastructure/Persistence/Configurations/ExternalSystemConfiguration.cs @@ -9,7 +9,7 @@ public class ExternalSystemConfiguration : IEntityTypeConfiguration builder) { - builder.ToTable("mon_external_systems"); + builder.ToTable("b_external_systems"); builder.HasKey(e => e.Id); builder.Property(e => e.Id).ValueGeneratedNever().HasComment("系统ID"); builder.Property(e => e.JsonData).HasColumnType("jsonb").IsRequired().HasComment("业务数据: 名称/类型/URL/参数/凭证/同步状态"); diff --git a/src/modules/monitoring/Infrastructure/Persistence/Configurations/SyncJobConfiguration.cs b/src/modules/monitoring/Infrastructure/Persistence/Configurations/SyncJobConfiguration.cs index 53265bb..9d27979 100644 --- a/src/modules/monitoring/Infrastructure/Persistence/Configurations/SyncJobConfiguration.cs +++ b/src/modules/monitoring/Infrastructure/Persistence/Configurations/SyncJobConfiguration.cs @@ -9,7 +9,7 @@ public class SyncJobConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.ToTable("mon_sync_jobs"); + builder.ToTable("b_sync_jobs"); builder.HasKey(e => e.Id); builder.Property(e => e.Id).ValueGeneratedNever().HasComment("任务ID"); builder.Property(e => e.ExternalSystemId).HasComment("外部系统ID"); diff --git a/src/modules/monitoring/Infrastructure/Persistence/Configurations/SyncedRecordConfiguration.cs b/src/modules/monitoring/Infrastructure/Persistence/Configurations/SyncedRecordConfiguration.cs index d16eb02..9431c47 100644 --- a/src/modules/monitoring/Infrastructure/Persistence/Configurations/SyncedRecordConfiguration.cs +++ b/src/modules/monitoring/Infrastructure/Persistence/Configurations/SyncedRecordConfiguration.cs @@ -9,7 +9,7 @@ public class SyncedRecordConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.ToTable("mon_synced_records"); + builder.ToTable("b_synced_records"); builder.HasKey(e => e.Id); builder.Property(e => e.Id).ValueGeneratedNever().HasComment("记录ID"); builder.Property(e => e.ExternalSystemId).HasComment("来源外部系统ID"); diff --git a/src/modules/monitoring/Infrastructure/Persistence/Migrations/20260719044352_RenameTablesToBPrefix.Designer.cs b/src/modules/monitoring/Infrastructure/Persistence/Migrations/20260719044352_RenameTablesToBPrefix.Designer.cs new file mode 100644 index 0000000..c7f0991 --- /dev/null +++ b/src/modules/monitoring/Infrastructure/Persistence/Migrations/20260719044352_RenameTablesToBPrefix.Designer.cs @@ -0,0 +1,350 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Monitoring.Infrastructure.Persistence; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Monitoring.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(MonitoringDbContext))] + [Migration("20260719044352_RenameTablesToBPrefix")] + partial class RenameTablesToBPrefix + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Monitoring.Domain.Entities.AlertRule", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("规则ID"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人"); + + b.Property("ExternalSystemId") + .HasColumnType("uuid") + .HasColumnName("external_system_id") + .HasComment("绑定的外部系统ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否已软删除"); + + b.Property("JsonData") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("json_data") + .HasComment("业务数据: 名称/指标/阈值/运算符/严重级别/启用/冷却"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("operator_ip") + .HasComment("操作者IP"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人"); + + b.HasKey("Id") + .HasName("pk_b_alert_rules"); + + b.HasIndex("ExternalSystemId") + .HasDatabaseName("ix_b_alert_rules_external_system_id"); + + b.ToTable("b_alert_rules", (string)null); + }); + + modelBuilder.Entity("Monitoring.Domain.Entities.ExternalSystem", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("系统ID"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否已软删除"); + + b.Property("JsonData") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("json_data") + .HasComment("业务数据: 名称/类型/URL/参数/凭证/同步状态"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("operator_ip") + .HasComment("操作者IP"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人"); + + b.HasKey("Id") + .HasName("pk_b_external_systems"); + + b.HasIndex("JsonData") + .HasDatabaseName("ix_b_external_systems_json_data"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("JsonData"), "gin"); + NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("JsonData"), new[] { "jsonb_path_ops" }); + + b.ToTable("b_external_systems", (string)null); + }); + + modelBuilder.Entity("Monitoring.Domain.Entities.SyncJob", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("任务ID"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms") + .HasComment("耗时(毫秒)"); + + b.Property("ExternalSystemId") + .HasColumnType("uuid") + .HasColumnName("external_system_id") + .HasComment("外部系统ID"); + + b.Property("FinishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_at") + .HasComment("结束时间UTC"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否已软删除"); + + b.Property("JsonData") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("json_data") + .HasComment("业务数据: 触发来源/汇总指标/错误信息"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("operator_ip") + .HasComment("操作者IP"); + + b.Property("RecordCount") + .HasColumnType("integer") + .HasColumnName("record_count") + .HasComment("拉取记录数"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at") + .HasComment("开始时间UTC"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("任务状态"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人"); + + b.HasKey("Id") + .HasName("pk_b_sync_jobs"); + + b.HasIndex("Status") + .HasDatabaseName("ix_b_sync_jobs_status"); + + b.HasIndex("ExternalSystemId", "CreatedAt") + .HasDatabaseName("ix_b_sync_jobs_external_system_id_created_at"); + + b.ToTable("b_sync_jobs", (string)null); + }); + + modelBuilder.Entity("Monitoring.Domain.Entities.SyncedRecord", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("记录ID"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人"); + + b.Property("ExternalId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("external_id") + .HasComment("外部业务Id(订单号等)"); + + b.Property("ExternalSystemId") + .HasColumnType("uuid") + .HasColumnName("external_system_id") + .HasComment("来源外部系统ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否已软删除"); + + b.Property("JsonData") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("json_data") + .HasComment("原始业务数据JSON"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_at") + .HasComment("业务发生时间UTC"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("operator_ip") + .HasComment("操作者IP"); + + b.Property("RecordType") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasColumnName("record_type") + .HasComment("记录类型:order/finance/metric"); + + b.Property("SyncJobId") + .HasColumnType("uuid") + .HasColumnName("sync_job_id") + .HasComment("来源同步任务ID"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人"); + + b.HasKey("Id") + .HasName("pk_b_synced_records"); + + b.HasIndex("JsonData") + .HasDatabaseName("ix_b_synced_records_json_data"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("JsonData"), "gin"); + NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("JsonData"), new[] { "jsonb_path_ops" }); + + b.HasIndex("ExternalSystemId", "OccurredAt") + .HasDatabaseName("ix_b_synced_records_external_system_id_occurred_at"); + + b.HasIndex("ExternalSystemId", "ExternalId", "RecordType") + .IsUnique() + .HasDatabaseName("ix_b_synced_records_external_system_id_external_id_record_type"); + + b.ToTable("b_synced_records", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/monitoring/Infrastructure/Persistence/Migrations/20260719044352_RenameTablesToBPrefix.cs b/src/modules/monitoring/Infrastructure/Persistence/Migrations/20260719044352_RenameTablesToBPrefix.cs new file mode 100644 index 0000000..5753aa7 --- /dev/null +++ b/src/modules/monitoring/Infrastructure/Persistence/Migrations/20260719044352_RenameTablesToBPrefix.cs @@ -0,0 +1,192 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Monitoring.Infrastructure.Persistence.Migrations +{ + /// + public partial class RenameTablesToBPrefix : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropPrimaryKey( + name: "pk_mon_synced_records", + table: "mon_synced_records"); + + migrationBuilder.DropPrimaryKey( + name: "pk_mon_sync_jobs", + table: "mon_sync_jobs"); + + migrationBuilder.DropPrimaryKey( + name: "pk_mon_external_systems", + table: "mon_external_systems"); + + migrationBuilder.DropPrimaryKey( + name: "pk_mon_alert_rules", + table: "mon_alert_rules"); + + migrationBuilder.RenameTable( + name: "mon_synced_records", + newName: "b_synced_records"); + + migrationBuilder.RenameTable( + name: "mon_sync_jobs", + newName: "b_sync_jobs"); + + migrationBuilder.RenameTable( + name: "mon_external_systems", + newName: "b_external_systems"); + + migrationBuilder.RenameTable( + name: "mon_alert_rules", + newName: "b_alert_rules"); + + migrationBuilder.RenameIndex( + name: "ix_mon_synced_records_json_data", + table: "b_synced_records", + newName: "ix_b_synced_records_json_data"); + + migrationBuilder.RenameIndex( + name: "ix_mon_synced_records_external_system_id_occurred_at", + table: "b_synced_records", + newName: "ix_b_synced_records_external_system_id_occurred_at"); + + migrationBuilder.RenameIndex( + name: "ix_mon_synced_records_external_system_id_external_id_record_ty", + table: "b_synced_records", + newName: "ix_b_synced_records_external_system_id_external_id_record_type"); + + migrationBuilder.RenameIndex( + name: "ix_mon_sync_jobs_status", + table: "b_sync_jobs", + newName: "ix_b_sync_jobs_status"); + + migrationBuilder.RenameIndex( + name: "ix_mon_sync_jobs_external_system_id_created_at", + table: "b_sync_jobs", + newName: "ix_b_sync_jobs_external_system_id_created_at"); + + migrationBuilder.RenameIndex( + name: "ix_mon_external_systems_json_data", + table: "b_external_systems", + newName: "ix_b_external_systems_json_data"); + + migrationBuilder.RenameIndex( + name: "ix_mon_alert_rules_external_system_id", + table: "b_alert_rules", + newName: "ix_b_alert_rules_external_system_id"); + + migrationBuilder.AddPrimaryKey( + name: "pk_b_synced_records", + table: "b_synced_records", + column: "id"); + + migrationBuilder.AddPrimaryKey( + name: "pk_b_sync_jobs", + table: "b_sync_jobs", + column: "id"); + + migrationBuilder.AddPrimaryKey( + name: "pk_b_external_systems", + table: "b_external_systems", + column: "id"); + + migrationBuilder.AddPrimaryKey( + name: "pk_b_alert_rules", + table: "b_alert_rules", + column: "id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropPrimaryKey( + name: "pk_b_synced_records", + table: "b_synced_records"); + + migrationBuilder.DropPrimaryKey( + name: "pk_b_sync_jobs", + table: "b_sync_jobs"); + + migrationBuilder.DropPrimaryKey( + name: "pk_b_external_systems", + table: "b_external_systems"); + + migrationBuilder.DropPrimaryKey( + name: "pk_b_alert_rules", + table: "b_alert_rules"); + + migrationBuilder.RenameTable( + name: "b_synced_records", + newName: "mon_synced_records"); + + migrationBuilder.RenameTable( + name: "b_sync_jobs", + newName: "mon_sync_jobs"); + + migrationBuilder.RenameTable( + name: "b_external_systems", + newName: "mon_external_systems"); + + migrationBuilder.RenameTable( + name: "b_alert_rules", + newName: "mon_alert_rules"); + + migrationBuilder.RenameIndex( + name: "ix_b_synced_records_json_data", + table: "mon_synced_records", + newName: "ix_mon_synced_records_json_data"); + + migrationBuilder.RenameIndex( + name: "ix_b_synced_records_external_system_id_occurred_at", + table: "mon_synced_records", + newName: "ix_mon_synced_records_external_system_id_occurred_at"); + + migrationBuilder.RenameIndex( + name: "ix_b_synced_records_external_system_id_external_id_record_type", + table: "mon_synced_records", + newName: "ix_mon_synced_records_external_system_id_external_id_record_ty"); + + migrationBuilder.RenameIndex( + name: "ix_b_sync_jobs_status", + table: "mon_sync_jobs", + newName: "ix_mon_sync_jobs_status"); + + migrationBuilder.RenameIndex( + name: "ix_b_sync_jobs_external_system_id_created_at", + table: "mon_sync_jobs", + newName: "ix_mon_sync_jobs_external_system_id_created_at"); + + migrationBuilder.RenameIndex( + name: "ix_b_external_systems_json_data", + table: "mon_external_systems", + newName: "ix_mon_external_systems_json_data"); + + migrationBuilder.RenameIndex( + name: "ix_b_alert_rules_external_system_id", + table: "mon_alert_rules", + newName: "ix_mon_alert_rules_external_system_id"); + + migrationBuilder.AddPrimaryKey( + name: "pk_mon_synced_records", + table: "mon_synced_records", + column: "id"); + + migrationBuilder.AddPrimaryKey( + name: "pk_mon_sync_jobs", + table: "mon_sync_jobs", + column: "id"); + + migrationBuilder.AddPrimaryKey( + name: "pk_mon_external_systems", + table: "mon_external_systems", + column: "id"); + + migrationBuilder.AddPrimaryKey( + name: "pk_mon_alert_rules", + table: "mon_alert_rules", + column: "id"); + } + } +} diff --git a/src/modules/monitoring/Infrastructure/Persistence/Migrations/MonitoringDbContextModelSnapshot.cs b/src/modules/monitoring/Infrastructure/Persistence/Migrations/MonitoringDbContextModelSnapshot.cs index 4456387..a3176a5 100644 --- a/src/modules/monitoring/Infrastructure/Persistence/Migrations/MonitoringDbContextModelSnapshot.cs +++ b/src/modules/monitoring/Infrastructure/Persistence/Migrations/MonitoringDbContextModelSnapshot.cs @@ -79,12 +79,12 @@ namespace Monitoring.Infrastructure.Persistence.Migrations .HasComment("更新人"); b.HasKey("Id") - .HasName("pk_mon_alert_rules"); + .HasName("pk_b_alert_rules"); b.HasIndex("ExternalSystemId") - .HasDatabaseName("ix_mon_alert_rules_external_system_id"); + .HasDatabaseName("ix_b_alert_rules_external_system_id"); - b.ToTable("mon_alert_rules", (string)null); + b.ToTable("b_alert_rules", (string)null); }); modelBuilder.Entity("Monitoring.Domain.Entities.ExternalSystem", b => @@ -139,15 +139,15 @@ namespace Monitoring.Infrastructure.Persistence.Migrations .HasComment("更新人"); b.HasKey("Id") - .HasName("pk_mon_external_systems"); + .HasName("pk_b_external_systems"); b.HasIndex("JsonData") - .HasDatabaseName("ix_mon_external_systems_json_data"); + .HasDatabaseName("ix_b_external_systems_json_data"); NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("JsonData"), "gin"); NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("JsonData"), new[] { "jsonb_path_ops" }); - b.ToTable("mon_external_systems", (string)null); + b.ToTable("b_external_systems", (string)null); }); modelBuilder.Entity("Monitoring.Domain.Entities.SyncJob", b => @@ -232,15 +232,15 @@ namespace Monitoring.Infrastructure.Persistence.Migrations .HasComment("更新人"); b.HasKey("Id") - .HasName("pk_mon_sync_jobs"); + .HasName("pk_b_sync_jobs"); b.HasIndex("Status") - .HasDatabaseName("ix_mon_sync_jobs_status"); + .HasDatabaseName("ix_b_sync_jobs_status"); b.HasIndex("ExternalSystemId", "CreatedAt") - .HasDatabaseName("ix_mon_sync_jobs_external_system_id_created_at"); + .HasDatabaseName("ix_b_sync_jobs_external_system_id_created_at"); - b.ToTable("mon_sync_jobs", (string)null); + b.ToTable("b_sync_jobs", (string)null); }); modelBuilder.Entity("Monitoring.Domain.Entities.SyncedRecord", b => @@ -324,22 +324,22 @@ namespace Monitoring.Infrastructure.Persistence.Migrations .HasComment("更新人"); b.HasKey("Id") - .HasName("pk_mon_synced_records"); + .HasName("pk_b_synced_records"); b.HasIndex("JsonData") - .HasDatabaseName("ix_mon_synced_records_json_data"); + .HasDatabaseName("ix_b_synced_records_json_data"); NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("JsonData"), "gin"); NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("JsonData"), new[] { "jsonb_path_ops" }); b.HasIndex("ExternalSystemId", "OccurredAt") - .HasDatabaseName("ix_mon_synced_records_external_system_id_occurred_at"); + .HasDatabaseName("ix_b_synced_records_external_system_id_occurred_at"); b.HasIndex("ExternalSystemId", "ExternalId", "RecordType") .IsUnique() - .HasDatabaseName("ix_mon_synced_records_external_system_id_external_id_record_ty"); + .HasDatabaseName("ix_b_synced_records_external_system_id_external_id_record_type"); - b.ToTable("mon_synced_records", (string)null); + b.ToTable("b_synced_records", (string)null); }); #pragma warning restore 612, 618 } diff --git a/src/modules/scripting/Application/Services/ScriptExecutionService.cs b/src/modules/scripting/Application/Services/ScriptExecutionService.cs index 6713f65..d6cbcea 100644 --- a/src/modules/scripting/Application/Services/ScriptExecutionService.cs +++ b/src/modules/scripting/Application/Services/ScriptExecutionService.cs @@ -272,7 +272,11 @@ public partial class ScriptExecutionService( || name.EndsWith(".Application") || name.EndsWith(".Domain")) && !name.EndsWith(".Infrastructure") - && !name.Contains(".Tests")); + && !name.Contains(".Tests")) + .ToList(); + + // 诊断日志:看脚本编译引用了哪些模块程序集 + Console.WriteLine($"[ScriptAssemblies] 白名单匹配 {moduleAssemblies.Count} 个: {string.Join(", ", moduleAssemblies.Select(a => a.GetName().Name))}"); return baseline.Concat(moduleAssemblies).Distinct() .Select(a => MetadataReference.CreateFromFile(a.Location)) diff --git a/src/modules/workflow/Application/BuildFlow/Shares/Commands/SubmitSharedFormCommand.cs b/src/modules/workflow/Application/BuildFlow/Shares/Commands/SubmitSharedFormCommand.cs index 4aaa86e..dc0ab75 100644 --- a/src/modules/workflow/Application/BuildFlow/Shares/Commands/SubmitSharedFormCommand.cs +++ b/src/modules/workflow/Application/BuildFlow/Shares/Commands/SubmitSharedFormCommand.cs @@ -5,7 +5,9 @@ namespace Workflow.Application.BuildFlow.Shares.Commands; /// /// 匿名通过分享 token 提交表单数据命令。 /// 仅当分享 Permission 允许写入(Edit/Admin)时才能成功。 +/// 若分享设置了密码,需通过 Password 校验后才能提交。 /// public sealed record SubmitSharedFormCommand( string Token, - string DataJson) : IRequest; + string DataJson, + string? Password = null) : IRequest; diff --git a/src/modules/workflow/Application/BuildFlow/Shares/Handlers/SubmitSharedFormCommandHandler.cs b/src/modules/workflow/Application/BuildFlow/Shares/Handlers/SubmitSharedFormCommandHandler.cs index e16dfcd..9cb983b 100644 --- a/src/modules/workflow/Application/BuildFlow/Shares/Handlers/SubmitSharedFormCommandHandler.cs +++ b/src/modules/workflow/Application/BuildFlow/Shares/Handlers/SubmitSharedFormCommandHandler.cs @@ -33,6 +33,16 @@ public class SubmitSharedFormCommandHandler(WorkflowDbContext db) : IRequestHand throw new BusinessException("当前分享链接为只读,不允许提交数据"); } + // 若分享设置了密码,提交时也必须校验(防止绕过密码直接提交) + if (!string.IsNullOrEmpty(share.Password)) + { + if (string.IsNullOrEmpty(req.Password) || + !BCrypt.Net.BCrypt.Verify(req.Password, share.Password)) + { + throw new BusinessException("需要密码或密码错误"); + } + } + // 基于 share.Id 构造确定性 InstanceId,便于同一分享下的多笔提交聚合到一个虚拟实例。 var instanceId = DeterministicInstanceId(share.Id); diff --git a/src/modules/workflow/Application/Engine/WorkflowGraphAdapter.cs b/src/modules/workflow/Application/Engine/WorkflowGraphAdapter.cs index 7ad0d16..fb06bb1 100644 --- a/src/modules/workflow/Application/Engine/WorkflowGraphAdapter.cs +++ b/src/modules/workflow/Application/Engine/WorkflowGraphAdapter.cs @@ -35,6 +35,9 @@ public static class WorkflowGraphAdapter Config = gn.Data is { ValueKind: JsonValueKind.Object or JsonValueKind.Array } data ? data.GetRawText() : null, + // 审批节点绑定的表单定义 id(钉钉式树编译进 data.formDefinitionId)。 + // ApproveTaskCommandHandler.SaveNodeFormDataAsync 据此加载表单做字段权限校验。 + FormDefinitionId = TryGetFormDefinitionId(gn.Data), PositionX = (int)Math.Round(gn.X), PositionY = (int)Math.Round(gn.Y), // 审计字段填充非空默认值(实体是 IFullAudit,避免下游 null 引用) @@ -78,4 +81,13 @@ public static class WorkflowGraphAdapter if (string.IsNullOrWhiteSpace(id)) return Guid.Empty; return Guid.TryParse(id, out var g) ? g : Guid.Empty; } + + /// 从节点 data.formDefinitionId(钉钉式树编译产物)提取表单定义 id。 + private static Guid? TryGetFormDefinitionId(System.Text.Json.JsonElement? data) + { + if (data is not { ValueKind: System.Text.Json.JsonValueKind.Object } obj) return null; + if (!obj.TryGetProperty("formDefinitionId", out var prop)) return null; + var raw = prop.ValueKind == System.Text.Json.JsonValueKind.String ? prop.GetString() : null; + return Guid.TryParse(raw, out var g) ? g : null; + } } diff --git a/src/modules/workflow/Application/Features/WorkflowTasks/Handlers/ApproveTaskCommandHandler.cs b/src/modules/workflow/Application/Features/WorkflowTasks/Handlers/ApproveTaskCommandHandler.cs index 6adb9f5..397d909 100644 --- a/src/modules/workflow/Application/Features/WorkflowTasks/Handlers/ApproveTaskCommandHandler.cs +++ b/src/modules/workflow/Application/Features/WorkflowTasks/Handlers/ApproveTaskCommandHandler.cs @@ -56,9 +56,18 @@ public class ApproveTaskCommandHandler( string formDataJson, CancellationToken ct) { - var node = await db.WorkflowNodes + // 先确保 ProcessEngine 已加载该实例的图(从 DefinitionJson/快照), + // 否则下方 FindNodePublicAsync 在新请求里拿不到内存图。优先用图查 node(含 config), + // 回退关系表(迁移期兼容)。 + var instanceForGraph = await db.WorkflowInstances .AsNoTracking() - .FirstOrDefaultAsync(n => n.Id == task.NodeId, ct) + .FirstOrDefaultAsync(i => i.Id == task.InstanceId, ct); + if (instanceForGraph is not null) + { + await processEngine.EnsureGraphLoadedForAsync(instanceForGraph); + } + var node = await processEngine.FindNodePublicAsync(task.NodeId) + ?? await db.WorkflowNodes.AsNoTracking().FirstOrDefaultAsync(n => n.Id == task.NodeId, ct) ?? throw new BusinessException("Task node not found"); if (!node.FormDefinitionId.HasValue) diff --git a/src/modules/workflow/Infrastructure/Persistence/Migrations/20260719041812_FixWorkflowTaskAttachmentsColumn.Designer.cs b/src/modules/workflow/Infrastructure/Persistence/Migrations/20260719041812_FixWorkflowTaskAttachmentsColumn.Designer.cs new file mode 100644 index 0000000..baf033d --- /dev/null +++ b/src/modules/workflow/Infrastructure/Persistence/Migrations/20260719041812_FixWorkflowTaskAttachmentsColumn.Designer.cs @@ -0,0 +1,2041 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Workflow.Infrastructure.Persistence; + +#nullable disable + +namespace Workflow.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(WorkflowDbContext))] + [Migration("20260719041812_FixWorkflowTaskAttachmentsColumn")] + partial class FixWorkflowTaskAttachmentsColumn + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Workflow.Domain.Entities.BuildFlowActionExecutionLog", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("ButtonCode") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("button_code") + .HasComment("按钮编码"); + + b.Property("ButtonDefinitionId") + .HasColumnType("uuid") + .HasColumnName("button_definition_id") + .HasComment("按钮定义ID"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms") + .HasComment("执行耗时(毫秒)"); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("error_message") + .HasComment("错误信息"); + + b.Property("InputJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("input_json") + .HasComment("按钮输入JSON"); + + b.Property("ResultJson") + .HasColumnType("jsonb") + .HasColumnName("result_json") + .HasComment("动作链结果JSON"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("执行状态"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_action_execution_logs"); + + b.HasIndex("ButtonDefinitionId") + .HasDatabaseName("ix_b_action_execution_logs_button_definition_id"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("ix_b_action_execution_logs_created_at"); + + b.ToTable("b_action_execution_logs", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.BuildFlowApplication", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("code") + .HasComment("应用编码,平台内唯一"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description") + .HasComment("应用描述"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name") + .HasComment("应用名称"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("应用状态"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version") + .HasComment("应用元数据版本"); + + b.HasKey("Id") + .HasName("pk_b_apps"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_b_apps_code"); + + b.ToTable("b_apps", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.BuildFlowButtonDefinition", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("ActionsJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("actions_json") + .HasComment("执行中(主链)动作链JSON"); + + b.Property("AfterActionsJson") + .HasColumnType("jsonb") + .HasColumnName("after_actions_json") + .HasComment("执行后(后置)动作链JSON"); + + b.Property("ApplicationId") + .HasColumnType("uuid") + .HasColumnName("application_id") + .HasComment("所属 BuildFlow 应用ID"); + + b.Property("BeforeActionsJson") + .HasColumnType("jsonb") + .HasColumnName("before_actions_json") + .HasComment("执行期(前置)动作链JSON"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("code") + .HasComment("按钮编码,作用域内唯一"); + + b.Property("ConfirmJson") + .HasColumnType("jsonb") + .HasColumnName("confirm_json") + .HasComment("按钮确认弹窗JSON"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DisabledWhenJson") + .HasColumnType("jsonb") + .HasColumnName("disabled_when_json") + .HasComment("按钮禁用条件JSON"); + + b.Property("FormDefinitionId") + .HasColumnType("uuid") + .HasColumnName("form_definition_id") + .HasComment("关联表单定义ID"); + + b.Property("Icon") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("icon") + .HasComment("按钮图标"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("label") + .HasComment("按钮显示文本"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("PageDefinitionId") + .HasColumnType("uuid") + .HasColumnName("page_definition_id") + .HasComment("所属页面定义ID"); + + b.Property("PermissionCode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("permission_code") + .HasComment("按钮权限码"); + + b.Property("Placement") + .HasColumnType("integer") + .HasColumnName("placement") + .HasComment("按钮展示位置"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order") + .HasComment("排序号"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("按钮状态"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("VisibleWhenJson") + .HasColumnType("jsonb") + .HasColumnName("visible_when_json") + .HasComment("按钮可见条件JSON"); + + b.Property("WorkflowDefinitionId") + .HasColumnType("uuid") + .HasColumnName("workflow_definition_id") + .HasComment("关联流程定义ID"); + + b.HasKey("Id") + .HasName("pk_b_buttons"); + + b.HasIndex("Code") + .HasDatabaseName("ix_b_buttons_code"); + + b.HasIndex("FormDefinitionId") + .HasDatabaseName("ix_b_buttons_form_definition_id"); + + b.HasIndex("PageDefinitionId") + .HasDatabaseName("ix_b_buttons_page_definition_id"); + + b.ToTable("b_buttons", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.BuildFlowFormShare", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("AllowAnonymous") + .HasColumnType("boolean") + .HasColumnName("allow_anonymous") + .HasComment("是否允许匿名访问"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at") + .HasComment("过期时间(UTC),null 表示永不过期"); + + b.Property("FormDefinitionId") + .HasColumnType("uuid") + .HasColumnName("form_definition_id") + .HasComment("被分享的表单定义ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("MaxViews") + .HasColumnType("integer") + .HasColumnName("max_views") + .HasComment("最大查看次数,null 表示不限"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("Password") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("password") + .HasComment("bcrypt 密码哈希,null 表示无需密码"); + + b.Property("Permission") + .HasColumnType("integer") + .HasColumnName("permission") + .HasComment("分享权限级别(View/Edit/Admin)"); + + b.Property("Remark") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("remark") + .HasComment("分享备注"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("token") + .HasComment("匿名访问 token(32位hex)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("ViewCount") + .HasColumnType("integer") + .HasColumnName("view_count") + .HasComment("已被查看次数"); + + b.HasKey("Id") + .HasName("pk_b_form_shares"); + + b.HasIndex("FormDefinitionId") + .HasDatabaseName("ix_b_form_shares_form_definition_id"); + + b.HasIndex("Token") + .IsUnique() + .HasDatabaseName("ix_b_form_shares_token"); + + b.ToTable("b_form_shares", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.BuildFlowImportJob", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("ErrorJson") + .HasColumnType("jsonb") + .HasColumnName("error_json") + .HasComment("错误明细JSON数组"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("error_message") + .HasComment("任务级错误消息"); + + b.Property("FailedRows") + .HasColumnType("integer") + .HasColumnName("failed_rows") + .HasComment("失败行数"); + + b.Property("FileUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("file_url") + .HasComment("上传文件存储路径"); + + b.Property("FinishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_at") + .HasComment("处理完成时间(UTC)"); + + b.Property("FormDefinitionId") + .HasColumnType("uuid") + .HasColumnName("form_definition_id") + .HasComment("导入目标表单定义ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("OriginalFileName") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("original_file_name") + .HasComment("原始上传文件名"); + + b.Property("ResultUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("result_url") + .HasComment("结果报告下载URL"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at") + .HasComment("处理开始时间(UTC)"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("任务状态(Pending/Running/Success/Failed/PartialSuccess)"); + + b.Property("SuccessRows") + .HasColumnType("integer") + .HasColumnName("success_rows") + .HasComment("成功行数"); + + b.Property("TotalRows") + .HasColumnType("integer") + .HasColumnName("total_rows") + .HasComment("总行数(不含表头)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_import_jobs"); + + b.HasIndex("FormDefinitionId") + .HasDatabaseName("ix_b_import_jobs_form_definition_id"); + + b.HasIndex("Status") + .HasDatabaseName("ix_b_import_jobs_status"); + + b.ToTable("b_import_jobs", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.BuildFlowPageDefinition", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("ApplicationId") + .HasColumnType("uuid") + .HasColumnName("application_id") + .HasComment("所属 BuildFlow 应用ID"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("code") + .HasComment("页面编码,应用内唯一"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name") + .HasComment("页面名称"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("PageType") + .HasColumnType("integer") + .HasColumnName("page_type") + .HasComment("页面类型"); + + b.Property("SchemaJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("schema_json") + .HasComment("页面定义JSON"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("页面状态"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version") + .HasComment("页面元数据版本"); + + b.HasKey("Id") + .HasName("pk_b_pages"); + + b.HasIndex("PageType") + .HasDatabaseName("ix_b_pages_page_type"); + + b.HasIndex("ApplicationId", "Code") + .IsUnique() + .HasDatabaseName("ix_b_pages_application_id_code"); + + b.ToTable("b_pages", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.FormComponentRegistry", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("category") + .HasComment("组件分类"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DefaultSchema") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("default_schema") + .HasComment("组件默认Schema JSON"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("display_name") + .HasComment("组件显示名称"); + + b.Property("Icon") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("icon") + .HasComment("组件图标"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active") + .HasComment("是否启用"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name") + .HasComment("组件名称(英文标识)"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order") + .HasComment("排序号"); + + b.Property("SupportedProps") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("supported_props") + .HasComment("组件支持的属性配置JSON"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_form_component_registry"); + + b.HasIndex("Category") + .HasDatabaseName("ix_b_form_component_registry_category"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_b_form_component_registry_name"); + + b.ToTable("b_form_component_registry", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.FormData", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DataJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("data_json") + .HasComment("表单数据JSON"); + + b.Property("FormDefinitionId") + .HasColumnType("uuid") + .HasColumnName("form_definition_id") + .HasComment("关联的表单定义ID"); + + b.Property("InstanceId") + .HasColumnType("uuid") + .HasColumnName("instance_id") + .HasComment("关联的流程实例ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_form_data"); + + b.HasIndex("DataJson") + .HasDatabaseName("ix_b_form_data_data_json"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("DataJson"), "GIN"); + NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("DataJson"), new[] { "jsonb_path_ops" }); + + b.HasIndex("FormDefinitionId") + .HasDatabaseName("ix_b_form_data_form_definition_id"); + + b.HasIndex("InstanceId") + .HasDatabaseName("ix_b_form_data_instance_id"); + + b.ToTable("b_form_data", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.FormDefinition", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("code") + .HasComment("表单编码,唯一标识"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description") + .HasComment("表单描述"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name") + .HasComment("表单名称"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("SchemaJson") + .HasColumnType("jsonb") + .HasColumnName("schema_json") + .HasComment("表单Schema JSON"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("表单状态(Draft=草稿, Published=已发布, Disabled=已禁用)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version") + .HasComment("版本号"); + + b.HasKey("Id") + .HasName("pk_b_form_definitions"); + + b.HasIndex("Code") + .HasDatabaseName("ix_b_form_definitions_code"); + + b.ToTable("b_form_definitions", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.FormDefinitionVersion", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("ChangeSummary") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("change_summary") + .HasComment("变更摘要"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("FormDefinitionId") + .HasColumnType("uuid") + .HasColumnName("form_definition_id") + .HasComment("所属表单定义ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("SchemaJson") + .HasColumnType("jsonb") + .HasColumnName("schema_json") + .HasComment("该版本的Schema JSON快照"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source") + .HasComment("触发来源 Update/Publish"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version") + .HasComment("版本号"); + + b.HasKey("Id") + .HasName("pk_b_form_definition_versions"); + + b.HasIndex("FormDefinitionId") + .HasDatabaseName("ix_b_form_definition_versions_form_definition_id"); + + b.HasIndex("FormDefinitionId", "Version") + .HasDatabaseName("ix_b_form_definition_versions_form_definition_id_version"); + + b.ToTable("b_form_definition_versions", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.Notification", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("category") + .HasComment("通知分类"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("content") + .HasComment("通知正文"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("IsRead") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_read") + .HasComment("是否已读"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("read_at") + .HasComment("阅读时间"); + + b.Property("RecipientRole") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("recipient_role") + .HasComment("收件角色名(role:规则)"); + + b.Property("RecipientUserId") + .HasColumnType("uuid") + .HasColumnName("recipient_user_id") + .HasComment("收件人用户ID(user:规则)"); + + b.Property("RelatedInstanceId") + .HasColumnType("uuid") + .HasColumnName("related_instance_id") + .HasComment("关联流程实例ID"); + + b.Property("RelatedTaskId") + .HasColumnType("uuid") + .HasColumnName("related_task_id") + .HasComment("关联任务ID"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("title") + .HasComment("通知标题"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_notifications"); + + b.HasIndex("RelatedInstanceId") + .HasDatabaseName("ix_wf_notifications_related_instance_id"); + + b.HasIndex("RecipientUserId", "IsRead") + .HasDatabaseName("ix_wf_notifications_recipient_user_id_is_read"); + + b.ToTable("b_notifications", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WebhookDelivery", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts") + .HasComment("已尝试次数"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("category") + .HasComment("通知分类"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("HttpMethod") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("http_method") + .HasComment("HTTP方法"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error") + .HasComment("最近一次错误信息"); + + b.Property("MaxAttempts") + .HasColumnType("integer") + .HasColumnName("max_attempts") + .HasComment("最大尝试次数"); + + b.Property("NextRetryAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("next_retry_at") + .HasComment("下次重试时间"); + + b.Property("Payload") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("payload") + .HasComment("投递JSON body"); + + b.Property("RelatedInstanceId") + .HasColumnType("uuid") + .HasColumnName("related_instance_id") + .HasComment("关联流程实例ID"); + + b.Property("RelatedTaskId") + .HasColumnType("uuid") + .HasColumnName("related_task_id") + .HasComment("关联任务ID"); + + b.Property("ResponseBody") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("response_body") + .HasComment("响应体"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("status") + .HasComment("投递状态:pending/delivered/failed"); + + b.Property("StatusCode") + .HasColumnType("integer") + .HasColumnName("status_code") + .HasComment("HTTP响应码"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("url") + .HasComment("目标URL"); + + b.HasKey("Id") + .HasName("pk_b_webhook_deliveries"); + + b.HasIndex("NextRetryAt") + .HasDatabaseName("ix_wf_webhook_deliveries_next_retry_at"); + + b.HasIndex("Status") + .HasDatabaseName("ix_wf_webhook_deliveries_status"); + + b.ToTable("b_webhook_deliveries", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowDefinition", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("code") + .HasComment("工作流编码,唯一标识"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DefinitionJson") + .HasColumnType("jsonb") + .HasColumnName("definition_json") + .HasComment("工作流定义JSON(含节点/边的完整结构)"); + + b.Property("DefinitionTree") + .HasColumnType("jsonb") + .HasColumnName("definition_tree") + .HasComment("钉钉式审批流树JSON(编辑态单一事实源,发布时编译为DefinitionJson)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description") + .HasComment("工作流描述"); + + b.Property("FormDefinitionId") + .HasColumnType("uuid") + .HasColumnName("form_definition_id") + .HasComment("关联的表单定义ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled") + .HasComment("是否启用"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name") + .HasComment("工作流名称"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("定义状态(Draft=草稿, Published=已发布)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version") + .HasComment("版本号"); + + b.HasKey("Id") + .HasName("pk_b_workflow_definitions"); + + b.HasIndex("Code") + .HasDatabaseName("ix_b_workflow_definitions_code"); + + b.ToTable("b_workflow_definitions", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowDefinitionSnapshot", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("ChangeSummary") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("change_summary") + .HasComment("变更摘要"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DefinitionId") + .HasColumnType("uuid") + .HasColumnName("definition_id") + .HasComment("所属工作流定义ID"); + + b.Property("DefinitionJson") + .HasColumnType("jsonb") + .HasColumnName("definition_json") + .HasComment("该版本的完整X6 graph JSON快照(不可变)"); + + b.Property("DefinitionTree") + .HasColumnType("jsonb") + .HasColumnName("definition_tree") + .HasComment("该版本的钉钉式树JSON快照(审计回显用,引擎不读)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source") + .HasComment("触发来源 Publish"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version") + .HasComment("版本号(发布时的 WorkflowDefinition.Version)"); + + b.HasKey("Id") + .HasName("pk_b_workflow_definition_snapshots"); + + b.HasIndex("DefinitionId") + .HasDatabaseName("ix_b_workflow_definition_snapshots_definition_id"); + + b.HasIndex("DefinitionId", "Version") + .IsUnique() + .HasDatabaseName("ix_b_workflow_definition_snapshots_definition_id_version"); + + b.ToTable("b_workflow_definition_snapshots", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowEdge", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Condition") + .HasColumnType("text") + .HasColumnName("condition") + .HasComment("条件表达式"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DefinitionId") + .HasColumnType("uuid") + .HasColumnName("definition_id") + .HasComment("所属工作流定义ID"); + + b.Property("EdgeType") + .HasColumnType("integer") + .HasColumnName("edge_type") + .HasComment("边类型(Normal=普通, Condition=条件)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("Label") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("label") + .HasComment("边标签"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("Order") + .HasColumnType("integer") + .HasColumnName("order") + .HasComment("排序号"); + + b.Property("SourceNodeId") + .HasColumnType("uuid") + .HasColumnName("source_node_id") + .HasComment("起始节点ID"); + + b.Property("TargetNodeId") + .HasColumnType("uuid") + .HasColumnName("target_node_id") + .HasComment("目标节点ID"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_workflow_edges"); + + b.HasIndex("DefinitionId") + .HasDatabaseName("ix_b_workflow_edges_definition_id"); + + b.ToTable("b_workflow_edges", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowInstance", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed_at") + .HasComment("完成时间"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DefinitionCode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("definition_code") + .HasComment("启动时快照的流程定义Code(审计追溯)"); + + b.Property("DefinitionId") + .HasColumnType("uuid") + .HasColumnName("definition_id") + .HasComment("关联的工作流定义ID"); + + b.Property("DefinitionVersion") + .HasColumnType("integer") + .HasColumnName("definition_version") + .HasComment("启动时绑定的流程定义版本号(版本隔离)"); + + b.Property("InitiatorId") + .HasColumnType("uuid") + .HasColumnName("initiator_id") + .HasComment("发起人ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("ParentInstanceId") + .HasColumnType("uuid") + .HasColumnName("parent_instance_id") + .HasComment("父流程实例ID(子流程场景)"); + + b.Property("ParentTokenId") + .HasColumnType("uuid") + .HasColumnName("parent_token_id") + .HasComment("父流程中等待的令牌ID"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at") + .HasComment("启动时间"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("实例状态(Running/Suspended/Completed/Terminated)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("title") + .HasComment("流程实例标题"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.Property("Variables") + .HasColumnType("jsonb") + .HasColumnName("variables") + .HasComment("流程变量JSON"); + + b.HasKey("Id") + .HasName("pk_b_workflow_instances"); + + b.HasIndex("DefinitionId") + .HasDatabaseName("ix_b_workflow_instances_definition_id"); + + b.ToTable("b_workflow_instances", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowNode", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("Config") + .HasColumnType("jsonb") + .HasColumnName("config") + .HasComment("节点配置JSON(审批人、表单绑定等)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DefinitionId") + .HasColumnType("uuid") + .HasColumnName("definition_id") + .HasComment("所属工作流定义ID"); + + b.Property("FormDefinitionId") + .HasColumnType("uuid") + .HasColumnName("form_definition_id") + .HasComment("关联的表单定义ID,用于审批/抄送节点"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name") + .HasComment("节点名称"); + + b.Property("NodeType") + .HasColumnType("integer") + .HasColumnName("node_type") + .HasComment("节点类型(Start/End/Approval/Cc/Condition/Parallel/SubProcess)"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("PositionX") + .HasColumnType("integer") + .HasColumnName("position_x") + .HasComment("画布X坐标"); + + b.Property("PositionY") + .HasColumnType("integer") + .HasColumnName("position_y") + .HasComment("画布Y坐标"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_workflow_nodes"); + + b.HasIndex("DefinitionId") + .HasDatabaseName("ix_b_workflow_nodes_definition_id"); + + b.HasIndex("FormDefinitionId") + .HasDatabaseName("ix_b_workflow_nodes_form_definition_id"); + + b.ToTable("b_workflow_nodes", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowTask", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("AssigneeId") + .HasColumnType("uuid") + .HasColumnName("assignee_id") + .HasComment("当前处理人ID"); + + b.Property("AssigneeRole") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("assignee_role") + .HasComment("当前处理人角色"); + + b.Property("Attachments") + .HasColumnType("text") + .HasColumnName("attachments"); + + b.Property("CandidateRoles") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("candidate_roles") + .HasComment("候选角色列表,逗号分隔"); + + b.Property("CandidateUsers") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("candidate_users") + .HasComment("候选用户ID列表,逗号分隔"); + + b.Property("Comment") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("comment") + .HasComment("审批意见"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed_at") + .HasComment("完成时间"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("DelegatedFromId") + .HasColumnType("uuid") + .HasColumnName("delegated_from_id") + .HasComment("原处理人ID(委托场景)"); + + b.Property("DueAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("due_at") + .HasComment("截止时间"); + + b.Property("InstanceId") + .HasColumnType("uuid") + .HasColumnName("instance_id") + .HasComment("所属流程实例ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("NodeId") + .HasColumnType("uuid") + .HasColumnName("node_id") + .HasComment("关联的节点定义ID"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("Result") + .HasColumnType("jsonb") + .HasColumnName("result") + .HasComment("处理结果JSON"); + + b.Property("SequentialAssignees") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("sequential_assignees") + .HasComment("依次审批:完整 assigneeRules JSON 数组,仅 sequential 模式"); + + b.Property("SequentialOrder") + .HasColumnType("integer") + .HasColumnName("sequential_order") + .HasComment("依次审批:当前审批人序号(0起),仅 sequential 模式"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("任务状态(Pending/Approved/Rejected/Transferred/Delegated)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("title") + .HasComment("任务标题"); + + b.Property("TokenId") + .HasColumnType("uuid") + .HasColumnName("token_id") + .HasComment("关联的令牌ID"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type") + .HasComment("任务类型(Approval=审批, Cc=抄送)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_workflow_tasks"); + + b.HasIndex("InstanceId") + .HasDatabaseName("ix_b_workflow_tasks_instance_id"); + + b.ToTable("b_workflow_tasks", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowToken", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("主键ID"); + + b.Property("ArrivedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("arrived_at") + .HasComment("到达当前节点的时间"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed_at") + .HasComment("令牌完成/消费时间"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("created_by") + .HasComment("创建人ID"); + + b.Property("InstanceId") + .HasColumnType("uuid") + .HasColumnName("instance_id") + .HasComment("所属流程实例ID"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted") + .HasComment("是否软删除"); + + b.Property("NodeId") + .HasColumnType("uuid") + .HasColumnName("node_id") + .HasComment("当前所在节点ID"); + + b.Property("OperatorIP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("operator_ip") + .HasComment("操作人IP地址"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status") + .HasComment("令牌状态(Active/Consumed/Terminated)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("updated_by") + .HasComment("更新人ID"); + + b.HasKey("Id") + .HasName("pk_b_workflow_tokens"); + + b.HasIndex("InstanceId") + .HasDatabaseName("ix_b_workflow_tokens_instance_id"); + + b.ToTable("b_workflow_tokens", (string)null); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowEdge", b => + { + b.HasOne("Workflow.Domain.Entities.WorkflowDefinition", null) + .WithMany("Edges") + .HasForeignKey("DefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_b_workflow_edges_b_workflow_definitions_definition_id"); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowNode", b => + { + b.HasOne("Workflow.Domain.Entities.WorkflowDefinition", null) + .WithMany("Nodes") + .HasForeignKey("DefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_b_workflow_nodes_b_workflow_definitions_definition_id"); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowTask", b => + { + b.HasOne("Workflow.Domain.Entities.WorkflowInstance", null) + .WithMany("Tasks") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_b_workflow_tasks_b_workflow_instances_instance_id"); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowToken", b => + { + b.HasOne("Workflow.Domain.Entities.WorkflowInstance", null) + .WithMany("Tokens") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_b_workflow_tokens_b_workflow_instances_instance_id"); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowDefinition", b => + { + b.Navigation("Edges"); + + b.Navigation("Nodes"); + }); + + modelBuilder.Entity("Workflow.Domain.Entities.WorkflowInstance", b => + { + b.Navigation("Tasks"); + + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/workflow/Infrastructure/Persistence/Migrations/20260719041812_FixWorkflowTaskAttachmentsColumn.cs b/src/modules/workflow/Infrastructure/Persistence/Migrations/20260719041812_FixWorkflowTaskAttachmentsColumn.cs new file mode 100644 index 0000000..7174717 --- /dev/null +++ b/src/modules/workflow/Infrastructure/Persistence/Migrations/20260719041812_FixWorkflowTaskAttachmentsColumn.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Workflow.Infrastructure.Persistence.Migrations +{ + /// + public partial class FixWorkflowTaskAttachmentsColumn : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // 模型漂移修复:WorkflowTask.Attachments 字段早已存在于实体与 ModelSnapshot, + // 但建表迁移遗漏了它(RenameTablesToBPrefix 只 rename,更早的建表迁移在 attachments 加入前)。 + // 用幂等 SQL 补列:全新部署建列;已 ALTER 补过的库跳过。 + // attachments 为自由 JSON 数组字符串(引用文件分享链接/S3 key),无校验约束,text 类型。 + migrationBuilder.Sql( + "ALTER TABLE b_workflow_tasks ADD COLUMN IF NOT EXISTS attachments text"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( + "ALTER TABLE b_workflow_tasks DROP COLUMN IF EXISTS attachments"); + } + } +} diff --git a/tests/BuildFlow.Workflow.Tests/Notification/NotificationServiceTests.cs b/tests/BuildFlow.Workflow.Tests/Notification/NotificationServiceTests.cs index 86677d9..8e9f7ae 100644 --- a/tests/BuildFlow.Workflow.Tests/Notification/NotificationServiceTests.cs +++ b/tests/BuildFlow.Workflow.Tests/Notification/NotificationServiceTests.cs @@ -30,6 +30,17 @@ public class NotificationServiceTests } }; + /// 包装 IOptionsMonitor(NotificationService 现用 IOptionsMonitor 而非 IOptions)。 + private static IOptionsMonitor OptionsMonitorOf(NotificationOptions options) => + new ConstantOptionsMonitor(options); + + private sealed class ConstantOptionsMonitor(NotificationOptions options) : IOptionsMonitor + { + public NotificationOptions CurrentValue => options; + public NotificationOptions Get(string? name) => options; + public IDisposable? OnChange(Action listener) => null!; + } + private static WorkflowTask NewTask(Guid? assigneeId = null, string? role = null) { var instanceId = Guid.NewGuid(); @@ -66,7 +77,7 @@ public class NotificationServiceTests db.WorkflowInstances.Add(NewInstance(task.InstanceId, Guid.NewGuid())); await db.SaveChangesAsync(); - var svc = new NotificationService(db, Options.Create(DefaultOptions()), new NoOpPlatformNotificationPublisher()); + var svc = new NotificationService(db, OptionsMonitorOf(DefaultOptions()), new NoOpPlatformNotificationPublisher()); await svc.NotifyTaskArrivedAsync(task); await db.SaveChangesAsync(); @@ -87,7 +98,7 @@ public class NotificationServiceTests db.WorkflowInstances.Add(NewInstance(task.InstanceId, Guid.NewGuid())); await db.SaveChangesAsync(); - var svc = new NotificationService(db, Options.Create(DefaultOptions()), new NoOpPlatformNotificationPublisher()); + var svc = new NotificationService(db, OptionsMonitorOf(DefaultOptions()), new NoOpPlatformNotificationPublisher()); await svc.NotifyTaskArrivedAsync(task); await db.SaveChangesAsync(); @@ -106,7 +117,7 @@ public class NotificationServiceTests db.WorkflowInstances.Add(NewInstance(task.InstanceId, initiatorId)); await db.SaveChangesAsync(); - var svc = new NotificationService(db, Options.Create(DefaultOptions()), new NoOpPlatformNotificationPublisher()); + var svc = new NotificationService(db, OptionsMonitorOf(DefaultOptions()), new NoOpPlatformNotificationPublisher()); await svc.NotifyTaskApprovedAsync(task); await db.SaveChangesAsync(); @@ -126,7 +137,7 @@ public class NotificationServiceTests db.WorkflowInstances.Add(NewInstance(task.InstanceId, userId)); await db.SaveChangesAsync(); - var svc = new NotificationService(db, Options.Create(DefaultOptions()), new NoOpPlatformNotificationPublisher()); + var svc = new NotificationService(db, OptionsMonitorOf(DefaultOptions()), new NoOpPlatformNotificationPublisher()); await svc.NotifyTaskApprovedAsync(task); await db.SaveChangesAsync(); @@ -143,7 +154,7 @@ public class NotificationServiceTests db.WorkflowInstances.Add(NewInstance(task.InstanceId, initiatorId)); await db.SaveChangesAsync(); - var svc = new NotificationService(db, Options.Create(DefaultOptions()), new NoOpPlatformNotificationPublisher()); + var svc = new NotificationService(db, OptionsMonitorOf(DefaultOptions()), new NoOpPlatformNotificationPublisher()); await svc.NotifyTaskRejectedAsync(task); await db.SaveChangesAsync(); @@ -159,7 +170,7 @@ public class NotificationServiceTests db.WorkflowInstances.Add(NewInstance(task.InstanceId, Guid.NewGuid())); await db.SaveChangesAsync(); - var svc = new NotificationService(db, Options.Create(DefaultOptions()), new NoOpPlatformNotificationPublisher()); + var svc = new NotificationService(db, OptionsMonitorOf(DefaultOptions()), new NoOpPlatformNotificationPublisher()); await svc.NotifyTaskTimeoutAsync(task, autoApproved: true); await db.SaveChangesAsync(); @@ -185,7 +196,7 @@ public class NotificationServiceTests DefaultUrl = "https://example.com/hook" } }; - var svc = new NotificationService(db, Options.Create(options), new NoOpPlatformNotificationPublisher()); + var svc = new NotificationService(db, OptionsMonitorOf(options), new NoOpPlatformNotificationPublisher()); await svc.NotifyTaskArrivedAsync(task); await db.SaveChangesAsync(); @@ -213,7 +224,7 @@ public class NotificationServiceTests AllowedHosts = ["example.com"] } }; - var svc = new NotificationService(db, Options.Create(options), new NoOpPlatformNotificationPublisher()); + var svc = new NotificationService(db, OptionsMonitorOf(options), new NoOpPlatformNotificationPublisher()); svc.IsHostAllowed(url).Should().Be(expected); } @@ -235,7 +246,7 @@ public class NotificationServiceTests DefaultUrl = "https://evil.com/hook" } }; - var svc = new NotificationService(db, Options.Create(options), new NoOpPlatformNotificationPublisher()); + var svc = new NotificationService(db, OptionsMonitorOf(options), new NoOpPlatformNotificationPublisher()); await svc.NotifyTaskArrivedAsync(task); await db.SaveChangesAsync();