using Microsoft.Data.SqlClient; using OrpaonVision.ConfigApp.Infrastructure.Persistence.Options; using OrpaonVision.Core.Abstractions; using OrpaonVision.Core.Configuration; using OrpaonVision.Core.Configuration.Contracts; using OrpaonVision.Core.Results; namespace OrpaonVision.ConfigApp.Infrastructure.Persistence; /// /// 规则配置持久化仓储实现。 /// public sealed class RuleConfigurationStore : IRuleConfigurationStore { private readonly PersistenceOptions _options; private readonly IAppLogger _logger; /// /// 构造函数。 /// public RuleConfigurationStore(PersistenceOptions options, IAppLogger logger) { _options = options; _logger = logger; } /// public Result SaveDraft(RuleConfigurationDraftDto draft) { if (draft == null) { return Result.Fail("RULE_DRAFT_REQUIRED", "规则草稿不能为空。"); } if (string.IsNullOrWhiteSpace(draft.ProductTypeCode)) { return Result.Fail("RULE_PRODUCT_CODE_REQUIRED", "机种编码不能为空。"); } var productTypeCode = draft.ProductTypeCode.Trim(); var productTypeName = string.IsNullOrWhiteSpace(draft.ProductTypeName) ? productTypeCode : draft.ProductTypeName.Trim(); var updatedBy = string.IsNullOrWhiteSpace(draft.UpdatedBy) ? "system" : draft.UpdatedBy.Trim(); var updatedAtUtc = DateTime.UtcNow; try { using var connection = CreateOpenConnection(); using var transaction = connection.BeginTransaction(); // 先尝试更新,如果不存在则插入 var updateResult = TryUpdateDraft(connection, transaction, productTypeCode, productTypeName, draft, updatedAtUtc, updatedBy); if (!updateResult.Succeeded) { transaction.Rollback(); return updateResult; } if (updateResult.Data == 0) // 没有更新任何行,说明记录不存在,执行插入 { var insertResult = TryInsertDraft(connection, transaction, productTypeCode, productTypeName, draft, updatedAtUtc, updatedBy); if (!insertResult.Succeeded) { transaction.Rollback(); return insertResult; } } transaction.Commit(); return Result.Success(message: "规则草稿保存成功。"); } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError($"保存规则草稿失败。TraceId: {traceId}", ex, traceId); var result = Result.FromException(ex, "RULE_DRAFT_SAVE_FAILED", "保存规则草稿失败。", traceId); return Result.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, [.. result.Errors]); } } /// public Result Publish(string productTypeCode, string publishedBy) { if (string.IsNullOrWhiteSpace(productTypeCode)) { return Result.Fail("RULE_PRODUCT_CODE_REQUIRED", "机种编码不能为空。"); } var trimmedCode = productTypeCode.Trim(); var operatorName = string.IsNullOrWhiteSpace(publishedBy) ? "system" : publishedBy.Trim(); try { using var connection = CreateOpenConnection(); using var transaction = connection.BeginTransaction(); // 获取草稿 var draft = GetDraft(connection, trimmedCode, transaction); if (draft == null) { transaction.Rollback(); return Result.Fail("RULE_DRAFT_NOT_FOUND", "未找到规则草稿,请先配置并保存草稿。"); } // 检查草稿是否为空 if (draft.Layers == null || draft.Layers.Count == 0) { transaction.Rollback(); return Result.Fail("RULE_DRAFT_EMPTY", "规则草稿为空,请至少配置一层规则。"); } // 生成版本号 var versionNo = GenerateVersionNo(connection, trimmedCode, transaction); // 插入版本记录 var publishedAtUtc = DateTime.UtcNow; var snapshotJson = System.Text.Json.JsonSerializer.Serialize(draft); using (var insertCommand = connection.CreateCommand()) { insertCommand.Transaction = transaction; insertCommand.CommandText = @" INSERT INTO dbo.cfg_rule_version (product_type_code, version_no, snapshot_json, published_at_utc, published_by) VALUES (@product_type_code, @version_no, @snapshot_json, @published_at_utc, @published_by);"; insertCommand.Parameters.AddWithValue("@product_type_code", trimmedCode); insertCommand.Parameters.AddWithValue("@version_no", versionNo); insertCommand.Parameters.AddWithValue("@snapshot_json", snapshotJson); insertCommand.Parameters.AddWithValue("@published_at_utc", publishedAtUtc); insertCommand.Parameters.AddWithValue("@published_by", operatorName); insertCommand.ExecuteNonQuery(); } // 记录审计 WriteAuditRecord( connection, transaction, trimmedCode, actionType: "PUBLISH", versionNo: versionNo, sourceVersionNo: null, targetVersionNo: null, operatorName, System.Text.Json.JsonSerializer.Serialize(new { Action = "Publish", VersionNo = versionNo, LayerCount = draft.Layers?.Count ?? 0 })); transaction.Commit(); return Result.Success(new RuleVersionDto { ProductTypeCode = trimmedCode, VersionNo = versionNo, PublishedAtUtc = publishedAtUtc, PublishedBy = operatorName, SnapshotJson = snapshotJson }, message: $"规则版本 {versionNo} 发布成功。"); } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError($"发布规则版本失败。TraceId: {traceId}", ex, traceId); var result = Result.FromException(ex, "RULE_VERSION_PUBLISH_FAILED", "发布规则版本失败。", traceId); return Result.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, [.. result.Errors]); } } /// public Result GetLatestPublishedVersion(string productTypeCode) { if (string.IsNullOrWhiteSpace(productTypeCode)) { return Result.Fail("RULE_PRODUCT_CODE_REQUIRED", "机种编码不能为空。"); } try { using var connection = CreateOpenConnection(); using var command = connection.CreateCommand(); command.CommandText = @" SELECT TOP(1) version_no, snapshot_json, published_at_utc, published_by ,disabled_at_utc, disabled_by ,CASE WHEN status = 0 THEN 1 ELSE 0 END AS is_disabled FROM dbo.cfg_rule_version WHERE product_type_code = @product_type_code ORDER BY published_at_utc DESC"; command.Parameters.AddWithValue("@product_type_code", productTypeCode.Trim()); using var reader = command.ExecuteReader(); if (!reader.Read()) { return Result.Success(null, message: "未找到已发布的规则版本。"); } var version = new RuleVersionDto { ProductTypeCode = productTypeCode.Trim(), VersionNo = reader.GetString(0), SnapshotJson = reader.GetString(1), PublishedAtUtc = reader.GetDateTime(2), PublishedBy = reader.GetString(3), DisabledAtUtc = reader.IsDBNull(4) ? null : reader.GetDateTime(4), DisabledBy = reader.IsDBNull(5) ? null : reader.GetString(5), IsDisabled = !reader.IsDBNull(4) }; return Result.Success(version, message: "获取最新规则版本成功。"); } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError($"获取最新规则版本失败。TraceId: {traceId}", ex, traceId); var result = Result.FromException(ex, "RULE_GET_VERSION_FAILED", "获取最新规则版本失败。", traceId); return Result.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, [.. result.Errors]); } } /// public Result CompareVersions(string productTypeCode, string sourceVersionNo, string targetVersionNo) { if (string.IsNullOrWhiteSpace(productTypeCode)) { return Result.Fail("RULE_PRODUCT_CODE_REQUIRED", "机种编码不能为空。"); } if (string.IsNullOrWhiteSpace(sourceVersionNo) || string.IsNullOrWhiteSpace(targetVersionNo)) { return Result.Fail("RULE_COMPARE_VERSION_REQUIRED", "对比版本号不能为空。"); } try { using var connection = CreateOpenConnection(); var source = GetVersionByNo(connection, productTypeCode.Trim(), sourceVersionNo.Trim()); var target = GetVersionByNo(connection, productTypeCode.Trim(), targetVersionNo.Trim()); if (source is null || target is null) { return Result.Fail("RULE_COMPARE_VERSION_NOT_FOUND", "待对比版本不存在,请检查版本号。"); } var normalizedSource = NormalizeJson(source.SnapshotJson); var normalizedTarget = NormalizeJson(target.SnapshotJson); var isSame = string.Equals(normalizedSource, normalizedTarget, StringComparison.Ordinal); var compare = new RuleVersionCompareDto { ProductTypeCode = productTypeCode.Trim(), SourceVersionNo = source.VersionNo, TargetVersionNo = target.VersionNo, SourceSnapshotJson = source.SnapshotJson, TargetSnapshotJson = target.SnapshotJson, IsSame = isSame, Summary = isSame ? "两个版本快照一致。" : "两个版本快照存在差异。" }; return Result.Success(compare, message: "规则版本对比完成。"); } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError($"对比规则版本失败。TraceId: {traceId}", ex, traceId); var result = Result.FromException(ex, "RULE_VERSION_COMPARE_FAILED", "对比规则版本失败。", traceId); return Result.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, [.. result.Errors]); } } /// public Result RollbackToVersion(string productTypeCode, string targetVersionNo, string rolledBackBy) { if (string.IsNullOrWhiteSpace(productTypeCode)) { return Result.Fail("RULE_PRODUCT_CODE_REQUIRED", "机种编码不能为空。"); } if (string.IsNullOrWhiteSpace(targetVersionNo)) { return Result.Fail("RULE_TARGET_VERSION_REQUIRED", "目标版本号不能为空。"); } var trimmedCode = productTypeCode.Trim(); var targetVersion = targetVersionNo.Trim(); var operatorName = string.IsNullOrWhiteSpace(rolledBackBy) ? "system" : rolledBackBy.Trim(); try { using var connection = CreateOpenConnection(); using var transaction = connection.BeginTransaction(); // 获取目标版本快照 var targetVersionInfo = GetVersionByNo(connection, trimmedCode, targetVersion, transaction); if (targetVersionInfo is null) { transaction.Rollback(); return Result.Fail("RULE_TARGET_VERSION_NOT_FOUND", $"目标版本 {targetVersion} 不存在。"); } string? sourceVersionNo; using (var latestCommand = connection.CreateCommand()) { latestCommand.Transaction = transaction; latestCommand.CommandText = @" SELECT TOP(1) version_no FROM dbo.cfg_rule_version WHERE product_type_code = @product_type_code ORDER BY id DESC"; latestCommand.Parameters.AddWithValue("@product_type_code", trimmedCode); sourceVersionNo = latestCommand.ExecuteScalar()?.ToString(); } // 计算新的回滚版本号 var rollbackIndex = 1; using (var countCommand = connection.CreateCommand()) { countCommand.Transaction = transaction; countCommand.CommandText = "SELECT COUNT(1) FROM dbo.cfg_rule_version WHERE product_type_code = @product_type_code"; countCommand.Parameters.AddWithValue("@product_type_code", trimmedCode); rollbackIndex = Convert.ToInt32(countCommand.ExecuteScalar()) + 1; } var rollbackVersionNo = $"RV-{DateTime.UtcNow:yyyyMMdd}-{rollbackIndex:000}"; var publishedAtUtc = DateTime.UtcNow; // 插入回滚版本记录 using (var insertCommand = connection.CreateCommand()) { insertCommand.Transaction = transaction; insertCommand.CommandText = @" INSERT INTO dbo.cfg_rule_version (product_type_code, version_no, snapshot_json, published_at_utc, published_by) VALUES (@product_type_code, @version_no, @snapshot_json, @published_at_utc, @published_by);"; insertCommand.Parameters.AddWithValue("@product_type_code", trimmedCode); insertCommand.Parameters.AddWithValue("@version_no", rollbackVersionNo); insertCommand.Parameters.AddWithValue("@snapshot_json", targetVersionInfo.SnapshotJson); insertCommand.Parameters.AddWithValue("@published_at_utc", publishedAtUtc); insertCommand.Parameters.AddWithValue("@published_by", operatorName); insertCommand.ExecuteNonQuery(); } WriteAuditRecord( connection, transaction, trimmedCode, actionType: "ROLLBACK", versionNo: rollbackVersionNo, sourceVersionNo, targetVersionNo: targetVersion, operatorName, System.Text.Json.JsonSerializer.Serialize(new { Action = "Rollback", SourceVersionNo = sourceVersionNo, TargetVersionNo = targetVersion, RollbackVersionNo = rollbackVersionNo })); transaction.Commit(); return Result.Success(new RuleVersionDto { ProductTypeCode = trimmedCode, VersionNo = rollbackVersionNo, PublishedAtUtc = publishedAtUtc, PublishedBy = operatorName, SnapshotJson = targetVersionInfo.SnapshotJson }, message: $"规则版本已成功回滚到 {targetVersion}。"); } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError($"回滚规则版本失败。TraceId: {traceId}", ex, traceId); var result = Result.FromException(ex, "RULE_VERSION_ROLLBACK_FAILED", "回滚规则版本失败。", traceId); return Result.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, [.. result.Errors]); } } /// public Result DisableVersion(string productTypeCode, string versionNo, string disabledBy) { if (string.IsNullOrWhiteSpace(productTypeCode)) { return Result.Fail("RULE_PRODUCT_CODE_REQUIRED", "机种编码不能为空。"); } if (string.IsNullOrWhiteSpace(versionNo)) { return Result.Fail("RULE_VERSION_NO_REQUIRED", "版本号不能为空。"); } try { using var connection = CreateOpenConnection(); using var transaction = connection.BeginTransaction(); // 检查版本是否存在 if (!VersionExists(connection, productTypeCode.Trim(), versionNo.Trim(), transaction)) { transaction.Rollback(); return Result.Fail("RULE_VERSION_NOT_FOUND", $"版本 {versionNo} 不存在。"); } // 检查版本是否已停用 var currentStatus = GetVersionStatus(connection, productTypeCode.Trim(), versionNo.Trim(), transaction); if (currentStatus == 0) { transaction.Rollback(); return Result.Fail("RULE_VERSION_ALREADY_DISABLED", $"版本 {versionNo} 已停用。", "请勿重复停用同一版本。"); } // 直接更新版本表的状态字段 using var updateCommand = connection.CreateCommand(); updateCommand.Transaction = transaction; updateCommand.CommandText = @" UPDATE dbo.cfg_rule_version SET status = 0, disabled_at_utc = @disabled_at_utc, disabled_by = @disabled_by WHERE product_type_code = @product_type_code AND version_no = @version_no"; updateCommand.Parameters.AddWithValue("@product_type_code", productTypeCode.Trim()); updateCommand.Parameters.AddWithValue("@version_no", versionNo.Trim()); updateCommand.Parameters.AddWithValue("@disabled_at_utc", DateTime.UtcNow); updateCommand.Parameters.AddWithValue("@disabled_by", string.IsNullOrWhiteSpace(disabledBy) ? "system" : disabledBy.Trim()); var rowsAffected = updateCommand.ExecuteNonQuery(); if (rowsAffected == 0) { transaction.Rollback(); return Result.Fail("RULE_VERSION_UPDATE_FAILED", "更新版本状态失败。"); } // 同时记录审计日志 WriteAuditRecord( connection, transaction, productTypeCode.Trim(), actionType: "DISABLE", versionNo: versionNo.Trim(), sourceVersionNo: null, targetVersionNo: null, operatorName: string.IsNullOrWhiteSpace(disabledBy) ? "system" : disabledBy.Trim(), System.Text.Json.JsonSerializer.Serialize(new { Action = "Disable", VersionNo = versionNo.Trim(), DisabledAt = DateTime.UtcNow, Method = "DirectStatusUpdate" })); transaction.Commit(); return Result.Success(message: $"版本 {versionNo} 已成功停用。"); } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError($"停用规则版本失败。TraceId: {traceId}", ex, traceId); var result = Result.FromException(ex, "RULE_VERSION_DISABLE_FAILED", "停用规则版本失败。", traceId); return Result.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, [.. result.Errors]); } } /// public Result GetVersionDetail(string productTypeCode, string versionNo) { if (string.IsNullOrWhiteSpace(productTypeCode)) { return Result.Fail("RULE_PRODUCT_CODE_REQUIRED", "机种编码不能为空。"); } if (string.IsNullOrWhiteSpace(versionNo)) { return Result.Fail("RULE_VERSION_NO_REQUIRED", "版本号不能为空。"); } try { using var connection = CreateOpenConnection(); var version = GetVersionByNo(connection, productTypeCode.Trim(), versionNo.Trim()); if (version is null) { return Result.Fail("RULE_VERSION_NOT_FOUND", $"版本 {versionNo} 不存在。"); } var versionDetail = new RuleVersionDto { ProductTypeCode = version.ProductTypeCode, VersionNo = version.VersionNo, SnapshotJson = version.SnapshotJson, PublishedAtUtc = version.PublishedAtUtc, PublishedBy = version.PublishedBy, IsDisabled = version.Status == 0, DisabledAtUtc = version.DisabledAtUtc, DisabledBy = version.DisabledBy }; return Result.Success(versionDetail, message: "获取版本详情成功。"); } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError($"获取规则版本详情失败。TraceId: {traceId}", ex, traceId); var result = Result.FromException(ex, "RULE_VERSION_DETAIL_FAILED", "获取规则版本详情失败。", traceId); return Result.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, [.. result.Errors]); } } /// public Result> GetVersionPagedList(string productTypeCode, int pageIndex = 1, int pageSize = 20) { if (string.IsNullOrWhiteSpace(productTypeCode)) { return Result>.Fail("RULE_PRODUCT_CODE_REQUIRED", "机种编码不能为空。"); } if (pageIndex < 1) { return Result>.Fail("RULE_PAGE_INDEX_INVALID", "页码必须大于0。"); } if (pageSize < 1 || pageSize > 100) { return Result>.Fail("RULE_PAGE_SIZE_INVALID", "每页大小必须在1-100之间。"); } try { using var connection = CreateOpenConnection(); // 查询总数 int totalCount; using (var countCommand = connection.CreateCommand()) { countCommand.CommandText = "SELECT COUNT(1) FROM dbo.cfg_rule_version WHERE product_type_code = @product_type_code"; countCommand.Parameters.AddWithValue("@product_type_code", productTypeCode.Trim()); totalCount = Convert.ToInt32(countCommand.ExecuteScalar()); } // 查询分页数据 var versions = new List(); using (var queryCommand = connection.CreateCommand()) { var offset = (pageIndex - 1) * pageSize; queryCommand.CommandText = @" SELECT version_no, snapshot_json, published_at_utc, published_by ,disabled_at_utc, disabled_by ,CASE WHEN status = 0 THEN 1 ELSE 0 END AS is_disabled FROM dbo.cfg_rule_version WHERE product_type_code = @product_type_code ORDER BY published_at_utc DESC OFFSET @offset ROWS FETCH NEXT @pageSize ROWS ONLY"; queryCommand.Parameters.AddWithValue("@product_type_code", productTypeCode.Trim()); queryCommand.Parameters.AddWithValue("@offset", offset); queryCommand.Parameters.AddWithValue("@pageSize", pageSize); using var reader = queryCommand.ExecuteReader(); while (reader.Read()) { versions.Add(new RuleVersionDto { ProductTypeCode = productTypeCode.Trim(), VersionNo = reader.GetString(0), SnapshotJson = reader.GetString(1), PublishedAtUtc = reader.GetDateTime(2), PublishedBy = reader.GetString(3), DisabledAtUtc = reader.IsDBNull(4) ? null : reader.GetDateTime(4), DisabledBy = reader.IsDBNull(5) ? null : reader.GetString(5), IsDisabled = reader.GetInt32(6) == 1 }); } } var pagedResult = PagedResult.Success(versions, totalCount, pageIndex, pageSize); return Result>.Success(pagedResult, message: "分页查询版本列表成功。"); } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError($"分页查询规则版本列表失败。TraceId: {traceId}", ex, traceId); var result = Result.FromException(ex, "RULE_VERSION_PAGED_LIST_FAILED", "分页查询规则版本列表失败。", traceId); return Result>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, [.. result.Errors]); } } /// public Result> GetRecentAudits(string productTypeCode, int take = 20) { if (string.IsNullOrWhiteSpace(productTypeCode)) { return Result>.Fail("RULE_PRODUCT_CODE_REQUIRED", "机种编码不能为空。"); } if (take < 1 || take > 100) { return Result>.Fail("RULE_TAKE_INVALID", "查询数量必须在1-100之间。"); } try { using var connection = CreateOpenConnection(); var audits = new List(); using (var command = connection.CreateCommand()) { command.CommandText = @" SELECT TOP(@take) action_type, version_no, source_version_no, target_version_no, operator_name, action_at_utc, detail_json FROM dbo.cfg_rule_version_audit WHERE product_type_code = @product_type_code ORDER BY action_at_utc DESC"; command.Parameters.AddWithValue("@product_type_code", productTypeCode.Trim()); command.Parameters.AddWithValue("@take", take); using var reader = command.ExecuteReader(); while (reader.Read()) { audits.Add(new RuleVersionAuditDto { ProductTypeCode = productTypeCode.Trim(), ActionType = reader.GetString(0), VersionNo = reader.IsDBNull(1) ? null : reader.GetString(1), SourceVersionNo = reader.IsDBNull(2) ? null : reader.GetString(2), TargetVersionNo = reader.IsDBNull(3) ? null : reader.GetString(3), OperatorName = reader.IsDBNull(4) ? "-" : reader.GetString(4), ActionAtUtc = reader.GetDateTime(5), DetailJson = reader.IsDBNull(6) ? null : reader.GetString(6) }); } } return Result>.Success(audits, message: "查询审计记录成功。"); } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError($"查询规则版本审计记录失败。TraceId: {traceId}", ex, traceId); var result = Result.FromException(ex, "RULE_AUDIT_QUERY_FAILED", "查询规则版本审计记录失败。", traceId); return Result>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, [.. result.Errors]); } } private SqlConnection CreateOpenConnection() { if (string.IsNullOrWhiteSpace(_options.ConnectionString)) { throw new InvalidOperationException("数据库连接字符串未配置。请设置 Persistence:ConnectionString。"); } var connection = new SqlConnection(_options.ConnectionString); connection.Open(); return connection; } private static Result TryUpdateDraft(SqlConnection connection, SqlTransaction transaction, string productTypeCode, string productTypeName, RuleConfigurationDraftDto draft, DateTime updatedAtUtc, string updatedBy) { try { using var command = connection.CreateCommand(); command.Transaction = transaction; command.CommandText = @" UPDATE dbo.cfg_product_type_draft SET product_type_name = @product_type_name, draft_json = @draft_json, updated_at = @updated_at, updated_by = @updated_by WHERE product_type_code = @product_type_code;"; command.Parameters.AddWithValue("@product_type_code", productTypeCode); command.Parameters.AddWithValue("@product_type_name", productTypeName); command.Parameters.AddWithValue("@draft_json", System.Text.Json.JsonSerializer.Serialize(draft)); command.Parameters.AddWithValue("@updated_at", updatedAtUtc); command.Parameters.AddWithValue("@updated_by", updatedBy); var rowsAffected = command.ExecuteNonQuery(); return Result.Success(rowsAffected, message: "规则草稿更新成功。"); } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); return Result.FailWithTrace("RULE_DRAFT_UPDATE_FAILED", "更新规则草稿失败。", traceId, [ex.Message]); } } private static Result TryInsertDraft(SqlConnection connection, SqlTransaction transaction, string productTypeCode, string productTypeName, RuleConfigurationDraftDto draft, DateTime updatedAtUtc, string updatedBy) { try { using var command = connection.CreateCommand(); command.Transaction = transaction; command.CommandText = @" INSERT INTO dbo.cfg_product_type_draft (product_type_code, product_type_name, draft_json, updated_at, updated_by) VALUES (@product_type_code, @product_type_name, @draft_json, @updated_at, @updated_by);"; var draftJson = System.Text.Json.JsonSerializer.Serialize(draft); command.Parameters.AddWithValue("@product_type_code", productTypeCode); command.Parameters.AddWithValue("@product_type_name", productTypeName); command.Parameters.AddWithValue("@draft_json", draftJson); command.Parameters.AddWithValue("@updated_at", updatedAtUtc); command.Parameters.AddWithValue("@updated_by", updatedBy); command.ExecuteNonQuery(); return Result.Success(message: "规则草稿插入成功。"); } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); return Result.FailWithTrace("RULE_DRAFT_INSERT_FAILED", "插入规则草稿失败。", traceId, [ex.Message]); } } private static RuleConfigurationDraftDto? GetDraft(SqlConnection connection, string productTypeCode, SqlTransaction? transaction = null) { using var command = connection.CreateCommand(); if (transaction != null) { command.Transaction = transaction; } command.CommandText = "SELECT draft_json FROM dbo.cfg_product_type_draft WHERE product_type_code = @product_type_code"; command.Parameters.AddWithValue("@product_type_code", productTypeCode); using var reader = command.ExecuteReader(); if (!reader.Read() || reader.IsDBNull(0)) { return null; } var draftJson = reader.GetString(0); return System.Text.Json.JsonSerializer.Deserialize(draftJson); } private static string GenerateVersionNo(SqlConnection connection, string productTypeCode, SqlTransaction transaction) { using var command = connection.CreateCommand(); command.Transaction = transaction; command.CommandText = "SELECT COUNT(1) FROM dbo.cfg_rule_version WHERE product_type_code = @product_type_code"; command.Parameters.AddWithValue("@product_type_code", productTypeCode); var count = Convert.ToInt32(command.ExecuteScalar()); return $"V{DateTime.UtcNow:yyyyMMdd}-{(count + 1):000}"; } private static RuleVersionDto? GetVersionByNo(SqlConnection connection, string productTypeCode, string versionNo, SqlTransaction? transaction = null) { using var command = connection.CreateCommand(); if (transaction != null) { command.Transaction = transaction; } command.CommandText = @" SELECT TOP(1) version_no, snapshot_json, published_at_utc, published_by, status, disabled_at_utc, disabled_by FROM dbo.cfg_rule_version WHERE product_type_code = @product_type_code AND version_no = @version_no"; command.Parameters.AddWithValue("@product_type_code", productTypeCode); command.Parameters.AddWithValue("@version_no", versionNo); using var reader = command.ExecuteReader(); if (!reader.Read()) { return null; } return new RuleVersionDto { ProductTypeCode = productTypeCode, VersionNo = reader.GetString(0), SnapshotJson = reader.GetString(1), PublishedAtUtc = reader.GetDateTime(2), PublishedBy = reader.GetString(3), Status = reader.GetInt32(4), DisabledAtUtc = reader.IsDBNull(5) ? null : reader.GetDateTime(5), DisabledBy = reader.IsDBNull(6) ? null : reader.GetString(6) }; } private static (bool IsDisabled, DateTime? DisabledAtUtc, string? DisabledBy) GetDisableInfo( SqlConnection connection, string productTypeCode, string versionNo, SqlTransaction? transaction = null) { using var command = connection.CreateCommand(); if (transaction != null) { command.Transaction = transaction; } command.CommandText = @" SELECT TOP(1) action_at_utc, operator_name FROM dbo.cfg_rule_version_audit WHERE product_type_code = @product_type_code AND version_no = @version_no AND action_type = 'DISABLE' ORDER BY action_at_utc DESC"; command.Parameters.AddWithValue("@product_type_code", productTypeCode); command.Parameters.AddWithValue("@version_no", versionNo); using var reader = command.ExecuteReader(); if (!reader.Read()) { return (false, null, null); } var disabledAtUtc = reader.IsDBNull(0) ? (DateTime?)null : reader.GetDateTime(0); var disabledBy = reader.IsDBNull(1) ? null : reader.GetString(1); return (true, disabledAtUtc, disabledBy); } /// /// 获取版本状态。 /// private static int GetVersionStatus( SqlConnection connection, string productTypeCode, string versionNo, SqlTransaction? transaction = null) { using var command = connection.CreateCommand(); if (transaction != null) { command.Transaction = transaction; } command.CommandText = "SELECT status FROM dbo.cfg_rule_version WHERE product_type_code = @product_type_code AND version_no = @version_no"; command.Parameters.AddWithValue("@product_type_code", productTypeCode); command.Parameters.AddWithValue("@version_no", versionNo); var result = command.ExecuteScalar(); return result != null ? Convert.ToInt32(result) : 1; // 默认为活跃状态 } private static string NormalizeJson(string json) { try { using var doc = System.Text.Json.JsonDocument.Parse(json); return System.Text.Json.JsonSerializer.Serialize(doc.RootElement); } catch { return json; } } private static void WriteAuditRecord( SqlConnection connection, SqlTransaction transaction, string productTypeCode, string actionType, string? versionNo, string? sourceVersionNo, string? targetVersionNo, string operatorName, string? detailJson) { using var command = connection.CreateCommand(); command.Transaction = transaction; command.CommandText = @" INSERT INTO dbo.cfg_rule_version_audit (product_type_code, action_type, version_no, source_version_no, target_version_no, operator_name, action_at_utc, detail_json) VALUES (@product_type_code, @action_type, @version_no, @source_version_no, @target_version_no, @operator_name, @action_at_utc, @detail_json);"; command.Parameters.AddWithValue("@product_type_code", productTypeCode); command.Parameters.AddWithValue("@action_type", actionType); command.Parameters.AddWithValue("@version_no", (object?)versionNo ?? DBNull.Value); command.Parameters.AddWithValue("@source_version_no", (object?)sourceVersionNo ?? DBNull.Value); command.Parameters.AddWithValue("@target_version_no", (object?)targetVersionNo ?? DBNull.Value); command.Parameters.AddWithValue("@operator_name", operatorName); command.Parameters.AddWithValue("@action_at_utc", DateTime.UtcNow); command.Parameters.AddWithValue("@detail_json", (object?)detailJson ?? DBNull.Value); command.ExecuteNonQuery(); } private static bool VersionExists( SqlConnection connection, string productTypeCode, string versionNo, SqlTransaction transaction) { using var command = connection.CreateCommand(); command.Transaction = transaction; command.CommandText = @" SELECT COUNT(1) FROM dbo.cfg_rule_version WHERE product_type_code = @product_type_code AND version_no = @version_no;"; command.Parameters.AddWithValue("@product_type_code", productTypeCode); command.Parameters.AddWithValue("@version_no", versionNo); var result = command.ExecuteScalar(); return result != null && Convert.ToInt32(result) > 0; } }