优化变量保存校验和变量公式替换逻辑

This commit is contained in:
尹峰
2026-07-17 11:03:43 +08:00
parent 3385113133
commit 180f113433
2 changed files with 211 additions and 24 deletions
+208 -23
View File
@@ -1,5 +1,7 @@
using BaseOUDAL;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Text.RegularExpressions;
@@ -105,9 +107,10 @@ using YLErp.Modules.RiskEngine.Dto;
DeleteVariable(long) 删除变量(引用保护:有 Active 规则引用时拒绝,硬删除)
GetAllVariableList() 获取所有已实现变量(轻量字段,供规则编辑器下拉)
── 审计日志(3 个) ───────────────────────────────────────────────
── 审计日志(4 个) ───────────────────────────────────────────────
QueryAuditLogs(QueryRiskAuditLogReq) 查询审计日志(多条件筛选,分页)
ExportAuditLogs(QueryRiskAuditLogReq) 导出审计日志(占位实现,后续迭代 Excel 导出)
BuildAuditLogExportFileName(...) 根据查询条件生成导出文件名
ExportAuditLogs(QueryRiskAuditLogReq) 导出审计日志 Excel(ID 保持整数文本、类型中文映射)
GetAuditLogDetail(long) 获取审计日志详情(含完整 SnapshotData
── 私有辅助方法 ───────────────────────────────────────────────────
@@ -180,7 +183,7 @@ using YLErp.Modules.RiskEngine.Dto;
1. RiskEngineService 由另一个团队开发实现,本服务通过 GetInstance() 获取单例引用
2. 规则变更后调用 RefreshOneRuleCache(ruleId),应用变更后调用 RefreshApplication(),变量变更不触发引擎缓存刷新
3. 变量删除为硬删除(物理删除),规则/应用删除为软删除(Status=Deleted
4. ExportAuditLogs 当前为占位实现,Excel 导出功能待后续迭代
4. ExportAuditLogs 按查询条件导出,并生成包含查询条件的文件名
5. VariableExpr 当前仅做基础长度校验(≤10000),完整编译校验待引入 Roslyn 库
6. ValidateScopeFields 的 ID 存在性校验已跳过(前端下拉选择器保证有效性)
================================================================================
@@ -291,6 +294,7 @@ namespace YLErp.Modules.RiskEngine
var sanitized = Regex.Replace(value.Trim(), @"[\\/:*?""<>|\r\n]+", "_");
return sanitized.Length <= 40 ? sanitized : sanitized.Substring(0, 40);
}
/// <summary>
/// 写入审计日志
/// </summary>
@@ -801,9 +805,10 @@ namespace YLErp.Modules.RiskEngine
if (req.VariableId.HasValue)
{
var varIdStr = req.VariableId.Value.ToString();
query = query.Where(r => (r.ConditionJson != null && r.ConditionJson.Contains(varIdStr))
|| (r.RuleExpr != null && r.RuleExpr.Contains(varIdStr)));
var referencedRuleIds = GetRulesByVariableId(req.VariableId.Value)
.Select(r => (long)r.id)
.ToList();
query = query.Where(r => referencedRuleIds.Contains(r.id));
}
var result = query.OrderByDescending(r => r.UpdateDate)
@@ -822,7 +827,8 @@ namespace YLErp.Modules.RiskEngine
UpdateOptName = r.UpdateOptName,
UpdateDate = r.UpdateDate ?? r.OptDate.GetValueOrDefault(),
ApplicationCount = DbContext.glms_risk_rule_application
.Count(a => a.RuleIds != null &&
.Count(a => a.Status != RiskRuleStatus.Deleted &&
a.RuleIds != null &&
(a.RuleIds == r.id.ToString() ||
a.RuleIds.StartsWith(r.id.ToString() + ",") ||
a.RuleIds.EndsWith("," + r.id.ToString()) ||
@@ -841,6 +847,7 @@ namespace YLErp.Modules.RiskEngine
var applications = DbContext.glms_risk_rule_application
.Where(a => a.Status != RiskRuleStatus.Deleted)
.Where(RuleIdsMatchExpr(ruleId))
.Select(a => new RiskRuleApplicationSummary
{
@@ -1946,6 +1953,183 @@ namespace YLErp.Modules.RiskEngine
).ToList();
}
/// <summary>
/// 根据结构化 ConditionJson 和当前变量池定义重新生成可执行 RuleExpr。
/// ConditionJson 是结构化规则的事实来源,避免通过字符串 Replace 级联修改表达式。
/// </summary>
private string BuildRuleExprFromConditionJson(string conditionJson)
{
List<RuleCondition> conditions;
try
{
conditions = JsonConvert.DeserializeObject<List<RuleCondition>>(conditionJson);
}
catch
{
throw new ServiceException("公式表达式 JSON 格式不合法");
}
if (conditions == null || conditions.Count == 0)
throw new ServiceException("公式条件列表不能为空");
var variableIds = conditions
.Select(c => c.VariableId)
.Concat(conditions.Where(c => c.ThresholdVariableId.HasValue)
.Select(c => c.ThresholdVariableId.Value))
.Distinct()
.ToList();
var variables = DbContext.glms_risk_variable
.Where(v => variableIds.Contains(v.id))
.ToList()
.ToDictionary(v => (long)v.id);
var missingVariableIds = variableIds.Where(id => !variables.ContainsKey(id)).ToList();
if (missingVariableIds.Any())
throw new ServiceException($"公式引用的变量不存在:{string.Join(",", missingVariableIds)}");
var conditionExpressions = conditions
.Select((condition, index) => BuildConditionExpression(condition, variables, index + 1))
.ToList();
return string.Join(" && ", conditionExpressions.Select(expr => $"({expr})"));
}
private string BuildConditionExpression(
RuleCondition condition,
IReadOnlyDictionary<long, glms_risk_variable> variables,
int conditionIndex)
{
var variable = variables[condition.VariableId];
if (string.IsNullOrWhiteSpace(variable.VariableExpr))
throw new ServiceException($"条件{conditionIndex}:变量'{variable.VariableName}'的取值表达式为空");
if (!ValidOperatorsByType.TryGetValue(variable.DataType, out var validOperators) ||
!validOperators.Contains(condition.Operator))
{
throw new ServiceException($"条件{conditionIndex}:操作符'{condition.Operator}'不适用于{GetDataTypeName(variable.DataType)}类型变量");
}
if (variable.DataType == RiskVariableDataType.Boolean)
{
return condition.Operator switch
{
"是" => $"({variable.VariableExpr}) == true",
"否" => $"({variable.VariableExpr}) == false",
_ => throw new ServiceException($"条件{conditionIndex}:不支持的布尔操作符'{condition.Operator}'")
};
}
var leftExpression = BuildComparableExpression(variable);
if (condition.Operator == "介于" || condition.Operator == "不介于")
{
if (!string.Equals(condition.ThresholdType, "Fixed", StringComparison.Ordinal))
throw new ServiceException($"条件{conditionIndex}:介于/不介于暂只支持固定上下限");
if (condition.Value is not JArray range || range.Count != 2)
throw new ServiceException($"条件{conditionIndex}:介于/不介于的阈值必须为双元素数组");
var lowerExpression = BuildFixedValueExpression(range[0], variable.DataType, conditionIndex);
var upperExpression = BuildFixedValueExpression(range[1], variable.DataType, conditionIndex);
var lowerOperator = condition.IncludeLowerBound ? ">=" : ">";
var upperOperator = condition.IncludeUpperBound ? "<=" : "<";
var rangeExpression = $"(({leftExpression}) {lowerOperator} ({lowerExpression}) && ({leftExpression}) {upperOperator} ({upperExpression}))";
return condition.Operator == "不介于" ? $"!{rangeExpression}" : rangeExpression;
}
var rightExpression = BuildThresholdExpression(condition, variable.DataType, variables, conditionIndex);
var comparisonOperator = GetComparisonOperator(condition.Operator, conditionIndex);
return $"({leftExpression}) {comparisonOperator} ({rightExpression})";
}
private static string BuildComparableExpression(glms_risk_variable variable)
{
return variable.DataType == RiskVariableDataType.Numeric
? $"Convert.ToDecimal(({variable.VariableExpr}))"
: variable.VariableExpr;
}
private string BuildThresholdExpression(
RuleCondition condition,
RiskVariableDataType dataType,
IReadOnlyDictionary<long, glms_risk_variable> variables,
int conditionIndex)
{
if (string.Equals(condition.ThresholdType, "Fixed", StringComparison.Ordinal))
return BuildFixedValueExpression(condition.Value, dataType, conditionIndex);
if (string.Equals(condition.ThresholdType, "Variable", StringComparison.Ordinal))
{
if (!condition.ThresholdVariableId.HasValue ||
!variables.TryGetValue(condition.ThresholdVariableId.Value, out var thresholdVariable))
{
throw new ServiceException($"条件{conditionIndex}:阈值变量不存在");
}
if (thresholdVariable.DataType != dataType)
throw new ServiceException($"条件{conditionIndex}:阈值变量与条件变量的数据类型不一致");
if (string.IsNullOrWhiteSpace(thresholdVariable.VariableExpr))
throw new ServiceException($"条件{conditionIndex}:阈值变量'{thresholdVariable.VariableName}'的取值表达式为空");
return BuildComparableExpression(thresholdVariable);
}
throw new ServiceException($"条件{conditionIndex}:阈值类型'{condition.ThresholdType}'不合法,仅支持 Fixed/Variable");
}
private static string BuildFixedValueExpression(object value, RiskVariableDataType dataType, int conditionIndex)
{
if (value == null)
throw new ServiceException($"条件{conditionIndex}:固定阈值不能为空");
var rawValue = value is JValue jsonValue
? Convert.ToString(jsonValue.Value, CultureInfo.InvariantCulture)
: Convert.ToString(value, CultureInfo.InvariantCulture);
if (dataType == RiskVariableDataType.Numeric)
{
if (!decimal.TryParse(rawValue, NumberStyles.Number | NumberStyles.AllowExponent,
CultureInfo.InvariantCulture, out var numericValue))
{
throw new ServiceException($"条件{conditionIndex}:数值型阈值必须为数字");
}
return numericValue.ToString(CultureInfo.InvariantCulture) + "m";
}
if (dataType == RiskVariableDataType.Date)
{
if (!DateTime.TryParse(rawValue, CultureInfo.InvariantCulture,
DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.RoundtripKind, out var dateValue) &&
!DateTime.TryParse(rawValue, out dateValue))
{
throw new ServiceException($"条件{conditionIndex}:日期型阈值必须为合法日期");
}
return $"new DateTime({dateValue.Ticks}L, DateTimeKind.{dateValue.Kind})";
}
throw new ServiceException($"条件{conditionIndex}:不支持的数据类型");
}
private static string GetComparisonOperator(string ruleOperator, int conditionIndex)
{
return ruleOperator switch
{
">" => ">",
"<" => "<",
">=" => ">=",
"<=" => "<=",
"=" => "==",
"≠" => "!=",
"早于" => "<",
"晚于" => ">",
"等于" => "==",
"不早于" => ">=",
"不晚于" => "<=",
_ => throw new ServiceException($"条件{conditionIndex}:不支持的操作符'{ruleOperator}'")
};
}
#region Variable Management
/// <summary>
@@ -1995,11 +2179,8 @@ namespace YLErp.Modules.RiskEngine
{
var variable = GetVariableOrThrow(variableId);
var varExpr = variable.VariableExpr;
var referenceCount = DbContext.glms_risk_rule
.Count(r => r.Status == RiskRuleStatus.Active &&
((r.ConditionJson != null && r.ConditionJson.Contains(variable.id.ToString())) ||
(r.RuleExpr != null && varExpr != null && r.RuleExpr.Contains(varExpr))));
var referenceCount = GetRulesByVariableId(variableId)
.Count(r => r.Status == RiskRuleStatus.Active);
return new RiskVariableDetail
{
@@ -2123,14 +2304,18 @@ namespace YLErp.Modules.RiskEngine
.Where(r => r.RuleExpr != null)
.ToList();
var compileTasks = affectedRules.Select(r =>
Task.Run(() => RuleCompiler.ValidateAndCompileFormula(
r.id, r.RuleExpr.Replace(oldVariableExpr, req.VariableExpr)))
);
var compileResults = Task.WhenAll(compileTasks).GetAwaiter().GetResult();
var failed = compileResults.FirstOrDefault(r => !r.Success);
if (failed is not null)
throw new ServiceException($"变量表达式变更导致规则编译失败:{failed.ErrorMessage}");
var rebuiltRuleExpressions = new Dictionary<long, string>();
foreach (var rule in affectedRules)
{
var rebuiltRuleExpr = BuildRuleExprFromConditionJson(rule.ConditionJson);
ValidateRuleExpr(rebuiltRuleExpr);
var compileResult = RuleCompiler.ValidateAndCompileFormula(rule.id, rebuiltRuleExpr);
if (!compileResult.Success)
throw new ServiceException($"变量表达式变更导致规则'{rule.RuleName}'(ID:{rule.id})编译失败:{compileResult.ErrorMessage}");
rebuiltRuleExpressions[rule.id] = rebuiltRuleExpr;
}
using (var transaction = DbContext.Database.BeginTransaction())
{
@@ -2138,7 +2323,7 @@ namespace YLErp.Modules.RiskEngine
{
foreach (var rule in affectedRules)
{
rule.RuleExpr = rule.RuleExpr.Replace(oldVariableExpr, req.VariableExpr);
rule.RuleExpr = rebuiltRuleExpressions[rule.id];
rule.Version = rule.Version + 1;
rule.UpdateOptId = UserId;
rule.UpdateOptName = UserName;
@@ -2314,13 +2499,13 @@ namespace YLErp.Modules.RiskEngine
}
var operationTypeNames = operationTypes
.Distinct(StringComparer.OrdinalIgnoreCase)
.Select(GetAuditOperationTypeName)
.Select(type => SanitizeFileNamePart(GetAuditOperationTypeName(type)))
.ToList();
if (operationTypeNames.Any())
conditions.Add($"操作类型-{string.Join("+", operationTypeNames)}");
if (!string.IsNullOrWhiteSpace(req.TargetType))
conditions.Add($"目标类型-{GetAuditTargetTypeName(req.TargetType)}");
conditions.Add($"目标类型-{SanitizeFileNamePart(GetAuditTargetTypeName(req.TargetType))}");
if (!string.IsNullOrWhiteSpace(req.TargetName))
conditions.Add($"目标名称-{SanitizeFileNamePart(req.TargetName)}");
if (!string.IsNullOrWhiteSpace(req.OptName))
+3 -1
View File
@@ -773,7 +773,9 @@ namespace YLErp.Web.Controllers
var service = GetRiskRuleService();
var bytes = await Task.Run(() => service.ExportAuditLogs(req));
var fileName = service.BuildAuditLogExportFileName(req);
return File(bytes, xlsxMimeType, fileName);
var encodedFileName = Uri.EscapeDataString(fileName);
Response.Headers["Content-Disposition"] = $"attachment; filename*=UTF-8''{encodedFileName}";
return File(bytes, xlsxMimeType);
}
catch (ServiceException ex)
{