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;
}
}