908 lines
38 KiB
C#
908 lines
38 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 规则配置持久化仓储实现。
|
|
/// </summary>
|
|
public sealed class RuleConfigurationStore : IRuleConfigurationStore
|
|
{
|
|
private readonly PersistenceOptions _options;
|
|
private readonly IAppLogger _logger;
|
|
|
|
/// <summary>
|
|
/// 构造函数。
|
|
/// </summary>
|
|
public RuleConfigurationStore(PersistenceOptions options, IAppLogger logger)
|
|
{
|
|
_options = options;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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]);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Result<RuleVersionDto> Publish(string productTypeCode, string publishedBy)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(productTypeCode))
|
|
{
|
|
return Result<RuleVersionDto>.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<RuleVersionDto>.Fail("RULE_DRAFT_NOT_FOUND", "未找到规则草稿,请先配置并保存草稿。");
|
|
}
|
|
|
|
// 检查草稿是否为空
|
|
if (draft.Layers == null || draft.Layers.Count == 0)
|
|
{
|
|
transaction.Rollback();
|
|
return Result<RuleVersionDto>.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<RuleVersionDto>.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<RuleVersionDto>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, [.. result.Errors]);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Result<RuleVersionDto?> GetLatestPublishedVersion(string productTypeCode)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(productTypeCode))
|
|
{
|
|
return Result<RuleVersionDto?>.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<RuleVersionDto?>.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<RuleVersionDto?>.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<RuleVersionDto?>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, [.. result.Errors]);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Result<RuleVersionCompareDto> CompareVersions(string productTypeCode, string sourceVersionNo, string targetVersionNo)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(productTypeCode))
|
|
{
|
|
return Result<RuleVersionCompareDto>.Fail("RULE_PRODUCT_CODE_REQUIRED", "机种编码不能为空。");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(sourceVersionNo) || string.IsNullOrWhiteSpace(targetVersionNo))
|
|
{
|
|
return Result<RuleVersionCompareDto>.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<RuleVersionCompareDto>.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<RuleVersionCompareDto>.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<RuleVersionCompareDto>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, [.. result.Errors]);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Result<RuleVersionDto> RollbackToVersion(string productTypeCode, string targetVersionNo, string rolledBackBy)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(productTypeCode))
|
|
{
|
|
return Result<RuleVersionDto>.Fail("RULE_PRODUCT_CODE_REQUIRED", "机种编码不能为空。");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(targetVersionNo))
|
|
{
|
|
return Result<RuleVersionDto>.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<RuleVersionDto>.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<RuleVersionDto>.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<RuleVersionDto>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, [.. result.Errors]);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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]);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Result<RuleVersionDto> GetVersionDetail(string productTypeCode, string versionNo)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(productTypeCode))
|
|
{
|
|
return Result<RuleVersionDto>.Fail("RULE_PRODUCT_CODE_REQUIRED", "机种编码不能为空。");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(versionNo))
|
|
{
|
|
return Result<RuleVersionDto>.Fail("RULE_VERSION_NO_REQUIRED", "版本号不能为空。");
|
|
}
|
|
|
|
try
|
|
{
|
|
using var connection = CreateOpenConnection();
|
|
var version = GetVersionByNo(connection, productTypeCode.Trim(), versionNo.Trim());
|
|
|
|
if (version is null)
|
|
{
|
|
return Result<RuleVersionDto>.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<RuleVersionDto>.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<RuleVersionDto>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, [.. result.Errors]);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Result<PagedResult<RuleVersionDto>> GetVersionPagedList(string productTypeCode, int pageIndex = 1, int pageSize = 20)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(productTypeCode))
|
|
{
|
|
return Result<PagedResult<RuleVersionDto>>.Fail("RULE_PRODUCT_CODE_REQUIRED", "机种编码不能为空。");
|
|
}
|
|
|
|
if (pageIndex < 1)
|
|
{
|
|
return Result<PagedResult<RuleVersionDto>>.Fail("RULE_PAGE_INDEX_INVALID", "页码必须大于0。");
|
|
}
|
|
|
|
if (pageSize < 1 || pageSize > 100)
|
|
{
|
|
return Result<PagedResult<RuleVersionDto>>.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<RuleVersionDto>();
|
|
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<RuleVersionDto>.Success(versions, totalCount, pageIndex, pageSize);
|
|
return Result<PagedResult<RuleVersionDto>>.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<PagedResult<RuleVersionDto>>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, [.. result.Errors]);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Result<IReadOnlyList<RuleVersionAuditDto>> GetRecentAudits(string productTypeCode, int take = 20)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(productTypeCode))
|
|
{
|
|
return Result<IReadOnlyList<RuleVersionAuditDto>>.Fail("RULE_PRODUCT_CODE_REQUIRED", "机种编码不能为空。");
|
|
}
|
|
|
|
if (take < 1 || take > 100)
|
|
{
|
|
return Result<IReadOnlyList<RuleVersionAuditDto>>.Fail("RULE_TAKE_INVALID", "查询数量必须在1-100之间。");
|
|
}
|
|
|
|
try
|
|
{
|
|
using var connection = CreateOpenConnection();
|
|
var audits = new List<RuleVersionAuditDto>();
|
|
|
|
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<IReadOnlyList<RuleVersionAuditDto>>.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<IReadOnlyList<RuleVersionAuditDto>>.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<int> 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<int>.Success(rowsAffected, message: "规则草稿更新成功。");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var traceId = Guid.NewGuid().ToString("N");
|
|
return Result<int>.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<RuleConfigurationDraftDto>(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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取版本状态。
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|