diff --git a/Framework/YLErp.Core/DBModels/glms_risk_rule.cs b/Framework/YLErp.Core/DBModels/glms_risk_rule.cs index 8359f712..87b0d880 100644 --- a/Framework/YLErp.Core/DBModels/glms_risk_rule.cs +++ b/Framework/YLErp.Core/DBModels/glms_risk_rule.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace YLErp.DBModels @@ -19,8 +20,9 @@ namespace YLErp.DBModels /// 规则说明 public string Description { get; set; } /// 规则状态 - public RiskRuleStatus Status { get; set; } = RiskRuleStatus.Active; + public RiskRuleStatus Status { get; set; } = RiskRuleStatus.Disabled; /// 版本号(乐观锁) + [ConcurrencyCheck] public int Version { get; set; } = 1; /// 最后更新人工号 public int? UpdateOptId { get; set; } diff --git a/Framework/YLErp.Core/DBModels/glms_risk_rule_application.cs b/Framework/YLErp.Core/DBModels/glms_risk_rule_application.cs index 89da2889..81f0de94 100644 --- a/Framework/YLErp.Core/DBModels/glms_risk_rule_application.cs +++ b/Framework/YLErp.Core/DBModels/glms_risk_rule_application.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace YLErp.DBModels @@ -13,7 +14,7 @@ namespace YLErp.DBModels /// 应用说明 public string Description { get; set; } /// 应用配置状态 - public RiskRuleStatus Status { get; set; } = RiskRuleStatus.Active; + public RiskRuleStatus Status { get; set; } = RiskRuleStatus.Disabled; /// 风控策略(预警/阻断) public RiskControlStrategy ControlStrategy { get; set; } /// 触发点列表(JSON 数组) @@ -29,6 +30,7 @@ namespace YLErp.DBModels /// 是否全局生效(0:否 1:是) public int ScopeIsGlobal { get; set; } /// 版本号(乐观锁) + [ConcurrencyCheck] public int Version { get; set; } = 1; /// 最后更新人工号 public int? UpdateOptId { get; set; } diff --git a/Framework/YLErp.Core/DBModels/glms_risk_variable.cs b/Framework/YLErp.Core/DBModels/glms_risk_variable.cs index 2a856819..5dad1ea6 100644 --- a/Framework/YLErp.Core/DBModels/glms_risk_variable.cs +++ b/Framework/YLErp.Core/DBModels/glms_risk_variable.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace YLErp.DBModels @@ -23,6 +24,7 @@ namespace YLErp.DBModels /// 取值表达式(C# 表达式,用于计算变量值) public string VariableExpr { get; set; } /// 版本号(乐观锁) + [ConcurrencyCheck] public int Version { get; set; } = 1; /// 排序序号 public int SortOrder { get; set; } diff --git a/UnitTestProject/Modules/RiskEngine/RiskApplicationQueryContractTest.cs b/UnitTestProject/Modules/RiskEngine/RiskApplicationQueryContractTest.cs new file mode 100644 index 00000000..419eb075 --- /dev/null +++ b/UnitTestProject/Modules/RiskEngine/RiskApplicationQueryContractTest.cs @@ -0,0 +1,36 @@ +using YLErp.Modules.RiskEngine.Dto; + +namespace YLErp.Modules.RiskEngine +{ + [TestClass] + public class RiskApplicationQueryContractTest + { + [TestMethod] + public void QueryRequest_UsesFrontendParameterNames() + { + var request = new QueryRiskApplicationReq + { + page = 1, + rows = 20, + Keyword = "本金", + ControlStrategy = (RiskControlStrategy)2, + Status = (RiskRuleStatus)1, + ScopeAssetBookIds = "2000041", + ScopeClientIds = "1000017", + ScopeUnderlyingTypes = "深交所债券ETF", + ScopeTradeTypes = "收益互换" + }; + + Assert.AreEqual("本金", request.Keyword); + Assert.AreEqual(RiskControlStrategy.Approval, request.ControlStrategy); + Assert.AreEqual(RiskRuleStatus.Active, request.Status); + Assert.AreEqual("2000041", request.ScopeAssetBookIds); + Assert.AreEqual("1000017", request.ScopeClientIds); + Assert.AreEqual("深交所债券ETF", request.ScopeUnderlyingTypes); + Assert.AreEqual("收益互换", request.ScopeTradeTypes); + + Assert.IsNull(typeof(QueryRiskApplicationReq).GetProperty("RuleName")); + Assert.IsNull(typeof(QueryRiskApplicationReq).GetProperty("Strategy")); + } + } +} \ No newline at end of file diff --git a/UnitTestProject/Modules/RiskEngine/RiskConcurrencyTokenTest.cs b/UnitTestProject/Modules/RiskEngine/RiskConcurrencyTokenTest.cs new file mode 100644 index 00000000..62ef4ab6 --- /dev/null +++ b/UnitTestProject/Modules/RiskEngine/RiskConcurrencyTokenTest.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; +using System.Reflection; + +namespace YLErp.Modules.RiskEngine +{ + [TestClass] + public class RiskConcurrencyTokenTest + { + [DataTestMethod] + [DataRow(typeof(glms_risk_rule))] + [DataRow(typeof(glms_risk_rule_application))] + [DataRow(typeof(glms_risk_variable))] + public void Version_IsConfiguredAsConcurrencyToken(Type entityType) + { + var versionProperty = entityType.GetProperty("Version", BindingFlags.Instance | BindingFlags.Public); + + Assert.IsNotNull(versionProperty, $"{entityType.Name} 缺少 Version 属性"); + Assert.IsNotNull( + versionProperty.GetCustomAttribute(), + $"{entityType.Name}.Version 未配置为 EF concurrency token"); + } + } +} \ No newline at end of file diff --git a/UnitTestProject/Modules/RiskEngine/RiskRuleDefaultStatusTest.cs b/UnitTestProject/Modules/RiskEngine/RiskRuleDefaultStatusTest.cs new file mode 100644 index 00000000..4b3c8453 --- /dev/null +++ b/UnitTestProject/Modules/RiskEngine/RiskRuleDefaultStatusTest.cs @@ -0,0 +1,22 @@ +namespace YLErp.Modules.RiskEngine +{ + [TestClass] + public class RiskRuleDefaultStatusTest + { + [TestMethod] + public void NewRiskRule_DefaultStatus_IsDisabled() + { + var rule = new glms_risk_rule(); + + Assert.AreEqual(RiskRuleStatus.Disabled, rule.Status); + } + + [TestMethod] + public void NewRiskRuleApplication_DefaultStatus_IsDisabled() + { + var application = new glms_risk_rule_application(); + + Assert.AreEqual(RiskRuleStatus.Disabled, application.Status); + } + } +} \ No newline at end of file diff --git a/YLErpDAL/Modules/RiskEngine/Dto/BatchDisableRulesResult.cs b/YLErpDAL/Modules/RiskEngine/Dto/BatchDisableRulesResult.cs deleted file mode 100644 index cb2e2fcd..00000000 --- a/YLErpDAL/Modules/RiskEngine/Dto/BatchDisableRulesResult.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; - -namespace YLErp.Modules.RiskEngine.Dto -{ - /// - /// 批量停用规则结果 - /// - public class BatchDisableRulesResult - { - /// 是否全部成功 - public bool Success { get; set; } - /// 总数量 - public int TotalCount { get; set; } - /// 成功数量 - public int SuccessCount { get; set; } - /// 错误信息 - public string ErrorMessage { get; set; } - /// 级联停用的应用 ID 列表 - public List CascadedApplicationIds { get; set; } = new List(); - } -} diff --git a/YLErpDAL/Modules/RiskEngine/Dto/BatchOperationResult.cs b/YLErpDAL/Modules/RiskEngine/Dto/BatchOperationResult.cs index c6c7aafd..0ae0cdcb 100644 --- a/YLErpDAL/Modules/RiskEngine/Dto/BatchOperationResult.cs +++ b/YLErpDAL/Modules/RiskEngine/Dto/BatchOperationResult.cs @@ -1,32 +1,48 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + namespace YLErp.Modules.RiskEngine.Dto { - /// - /// 批量操作结果 - /// - public class BatchOperationResult + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum BatchOperationOutcome { - /// 是否全部成功 - public bool Success { get; set; } - /// 总数量 - public int TotalCount { get; set; } - /// 成功数量 - public int SuccessCount { get; set; } - /// 错误信息 - public string ErrorMessage { get; set; } + [EnumMember(Value = "AllSucceeded")] + AllSucceeded, + [EnumMember(Value = "PartiallySucceeded")] + PartiallySucceeded, + [EnumMember(Value = "AllFailed")] + AllFailed } - /// - /// 批量删除变量结果 - /// - public class BatchDeleteVariablesResult : BatchOperationResult + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum BatchOperationItemStatus { - /// 成功删除的变量 ID - public List DeletedIds { get; set; } - /// 不存在的变量 ID - public List MissingIds { get; set; } - /// 因被生效规则引用而跳过的变量 ID - public List BlockedIds { get; set; } - /// 被跳过变量的原因,Key 为变量 ID - public Dictionary BlockedReasons { get; set; } + [EnumMember(Value = "Succeeded")] + Succeeded, + [EnumMember(Value = "Unchanged")] + Unchanged, + [EnumMember(Value = "Failed")] + Failed + } + + public class BatchOperationItemResult + { + public long Id { get; set; } + public BatchOperationItemStatus Status { get; set; } + public string Code { get; set; } + public string Message { get; set; } + public List CascadedApplicationIds { get; set; } = new List(); + } + + public class BatchOperationResult + { + public BatchOperationOutcome Outcome { get; set; } + public int TotalCount { get; set; } + public int SucceededCount { get; set; } + public int UnchangedCount { get; set; } + public int FailedCount { get; set; } + public bool HasChanges { get; set; } + public List Items { get; set; } = new List(); + public List CascadedApplicationIds { get; set; } = new List(); } } diff --git a/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskApplicationReq.cs b/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskApplicationReq.cs index 0a501867..36916100 100644 --- a/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskApplicationReq.cs +++ b/YLErpDAL/Modules/RiskEngine/Dto/QueryRiskApplicationReq.cs @@ -8,12 +8,12 @@ namespace YLErp.Modules.RiskEngine.Dto /// public class QueryRiskApplicationReq : BaseSearchReq { - /// 规则名称 - public string RuleName { get; set; } + /// 规则名称关键词 + public string Keyword { get; set; } /// 应用配置状态 public RiskRuleStatus? Status { get; set; } /// 风控策略 - public RiskControlStrategy? Strategy { get; set; } + public RiskControlStrategy? ControlStrategy { get; set; } /// 触发点 public string TriggerPoint { get; set; } /// 适用范围-客户 ID 列表 diff --git a/YLErpDAL/Modules/RiskEngine/RiskRuleService.cs b/YLErpDAL/Modules/RiskEngine/RiskRuleService.cs index 737aa940..d0abff89 100644 --- a/YLErpDAL/Modules/RiskEngine/RiskRuleService.cs +++ b/YLErpDAL/Modules/RiskEngine/RiskRuleService.cs @@ -1,4 +1,5 @@ -using BaseOUDAL; +using BaseOUDAL; +using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System.Globalization; @@ -261,6 +262,23 @@ namespace YLErp.Modules.RiskEngine #region Private Helpers + /// + /// 保存变更,并将 EF 乐观并发冲突转换为可识别的业务异常。 + /// Version 仍由业务代码手工递增;并发令牌只负责将原 Version 加入 UPDATE/DELETE 条件。 + /// + private void SaveChangesWithConcurrencyCheck() + { + try + { + DbContext.SaveChanges(); + } + catch (DbUpdateConcurrencyException ex) + { + _logger.Error("风控配置发生并发修改冲突", ex); + throw new ServiceException("数据已被其他用户修改,请重新加载后再操作", ex); + } + } + private static string GetAuditOperationTypeName(string operationType) { if (string.IsNullOrWhiteSpace(operationType)) @@ -780,6 +798,44 @@ namespace YLErp.Modules.RiskEngine return string.Join(", ", translated); } + private static List DistinctIds(List ids) + { + return ids.Distinct().ToList(); + } + + private static BatchOperationItemResult BatchItem(long id, BatchOperationItemStatus status, + string code, string message, IEnumerable cascadedApplicationIds = null) + { + return new BatchOperationItemResult + { + Id = id, + Status = status, + Code = code, + Message = message, + CascadedApplicationIds = cascadedApplicationIds?.Distinct().ToList() ?? new List() + }; + } + + private static BatchOperationResult BuildBatchResult(List items) + { + var result = new BatchOperationResult + { + Items = items, + TotalCount = items.Count, + SucceededCount = items.Count(i => i.Status == BatchOperationItemStatus.Succeeded), + UnchangedCount = items.Count(i => i.Status == BatchOperationItemStatus.Unchanged), + FailedCount = items.Count(i => i.Status == BatchOperationItemStatus.Failed), + CascadedApplicationIds = items.SelectMany(i => i.CascadedApplicationIds).Distinct().ToList() + }; + result.HasChanges = result.SucceededCount > 0; + result.Outcome = result.FailedCount == 0 + ? BatchOperationOutcome.AllSucceeded + : result.FailedCount == result.TotalCount + ? BatchOperationOutcome.AllFailed + : BatchOperationOutcome.PartiallySucceeded; + return result; + } + #endregion #region Rule Management @@ -894,7 +950,7 @@ namespace YLErp.Modules.RiskEngine ConditionJson = req.ConditionJson, RuleExpr = req.RuleExpr, Description = req.Description, - Status = RiskRuleStatus.Active, + Status = RiskRuleStatus.Disabled, Version = 1 }; @@ -908,11 +964,11 @@ namespace YLErp.Modules.RiskEngine try { DbContext.glms_risk_rule.Add(entity); - DbContext.SaveChanges(); + SaveChangesWithConcurrencyCheck(); WriteAuditLog("RULE_CREATE", "RULE", entity.id, req.RuleName, "创建规则", snapshotData: JsonConvert.SerializeObject(new { Version = entity.Version })); - DbContext.SaveChanges(); + SaveChangesWithConcurrencyCheck(); transaction.Commit(); } @@ -955,7 +1011,7 @@ namespace YLErp.Modules.RiskEngine rule.UpdateDate = DateTime.Now; WriteAuditLog("RULE_UPDATE", "RULE", ruleId, req.RuleName, "修改规则"); - DbContext.SaveChanges(); + SaveChangesWithConcurrencyCheck(); RiskEngineService.GetInstance().RefreshOneRuleCache(ruleId); @@ -983,7 +1039,7 @@ namespace YLErp.Modules.RiskEngine rule.UpdateDate = DateTime.Now; WriteAuditLog("RULE_DELETE", "RULE", ruleId, rule.RuleName, "删除规则"); - DbContext.SaveChanges(); + SaveChangesWithConcurrencyCheck(); RiskEngineService.GetInstance().RefreshOneRuleCache(ruleId); } @@ -1002,12 +1058,14 @@ namespace YLErp.Modules.RiskEngine throw new ServiceException($"规则表达式编译失败:{compileResult.ErrorMessage}"); rule.Status = RiskRuleStatus.Active; + + rule.Version = rule.Version + 1; rule.UpdateOptId = UserId; rule.UpdateOptName = UserName; rule.UpdateDate = DateTime.Now; WriteAuditLog("RULE_ENABLE", "RULE", ruleId, rule.RuleName, "启用规则"); - DbContext.SaveChanges(); + SaveChangesWithConcurrencyCheck(); RiskEngineService.GetInstance().RefreshOneRuleCache(ruleId); } @@ -1022,6 +1080,8 @@ namespace YLErp.Modules.RiskEngine throw new ServiceException("仅已启用的规则可以停用"); rule.Status = RiskRuleStatus.Disabled; + + rule.Version = rule.Version + 1; rule.UpdateOptId = UserId; rule.UpdateOptName = UserName; rule.UpdateDate = DateTime.Now; @@ -1038,6 +1098,7 @@ namespace YLErp.Modules.RiskEngine foreach (var app in activeApps) { app.Status = RiskRuleStatus.Disabled; + app.Version = app.Version + 1; app.UpdateOptId = UserId; app.UpdateOptName = UserName; app.UpdateDate = DateTime.Now; @@ -1052,7 +1113,7 @@ namespace YLErp.Modules.RiskEngine _logger.Info($"[停用规则] 规则ID: {ruleId}, 级联停用应用: {string.Join(",", cascadedIds)}"); } - DbContext.SaveChanges(); + SaveChangesWithConcurrencyCheck(); RiskEngineService.GetInstance().RefreshOneRuleCache(ruleId); if (cascadedIds.Any()) @@ -1109,79 +1170,48 @@ namespace YLErp.Modules.RiskEngine if (ruleIds == null || ruleIds.Count == 0) throw new ServiceException("规则ID列表不能为空"); - _logger.Info($"[批量删除] 开始 - 传入规则数: {ruleIds.Count}, IDs: {string.Join(",", ruleIds)}"); - - var rules = DbContext.glms_risk_rule - .Where(r => ruleIds.Contains(r.id) && r.Status != RiskRuleStatus.Deleted) + var ids = DistinctIds(ruleIds); + var rules = DbContext.glms_risk_rule.Where(r => ids.Contains(r.id)).ToDictionary(r => (long)r.id); + var activeApps = DbContext.glms_risk_rule_application + .Where(a => a.Status == RiskRuleStatus.Active) .ToList(); + var items = new List(); + var changedIds = new List(); - _logger.Info($"[批量删除] DB查询完成 - 匹配到 {rules.Count} 条规则"); - - var foundIds = rules.Select(r => (long)r.id).ToList(); - var missingIds = ruleIds.Except(foundIds).ToList(); - if (missingIds.Any()) + foreach (var id in ids) { - return new BatchOperationResult + if (!rules.TryGetValue(id, out var rule) || rule.Status == RiskRuleStatus.Deleted) { - Success = false, - TotalCount = ruleIds.Count, - SuccessCount = 0, - ErrorMessage = $"以下规则不存在或已删除:{string.Join(", ", missingIds)}" - }; - } + items.Add(BatchItem(id, BatchOperationItemStatus.Failed, "NOT_FOUND", "规则不存在或已删除")); + continue; + } - var blockedRules = new List(); - foreach (var rule in rules) - { - _logger.Info($"[批量删除] 检查关联应用 - 规则ID: {rule.id}"); - var activeApps = DbContext.glms_risk_rule_application - .Where(a => a.Status == RiskRuleStatus.Active) - .Where(RuleIdsMatchExpr(rule.id)) - .Select(a => (long)a.id) - .ToList(); - var activeCount = activeApps.Count; - _logger.Info($"[批量删除] 关联应用检查结果 - 规则ID: {rule.id}, ActiveCount: {activeCount}, AppIds: [{string.Join(",", activeApps)}]"); - if (activeCount > 0) - blockedRules.Add($"规则 '{rule.RuleName}'(ID:{rule.id}) 仍有 {activeCount} 个生效中的应用配置(IDs: {string.Join(",", activeApps)})"); - } - if (blockedRules.Any()) - { - return new BatchOperationResult + var referenceCount = activeApps.Count(a => ParseRuleIds(a.RuleIds).Contains(id)); + if (referenceCount > 0) { - Success = false, - TotalCount = ruleIds.Count, - SuccessCount = 0, - ErrorMessage = string.Join(";", blockedRules) - }; - } + items.Add(BatchItem(id, BatchOperationItemStatus.Failed, "ACTIVE_APPLICATION_REFERENCE", + $"该规则仍有 {referenceCount} 个生效中的应用配置,请先停用或删除相关应用")); + continue; + } - foreach (var rule in rules) - { - _logger.Info($"[批量删除] 写入状态 - 规则ID: {rule.id}"); rule.Status = RiskRuleStatus.Deleted; - rule.Version = rule.Version + 1; + rule.Version++; rule.UpdateOptId = UserId; rule.UpdateOptName = UserName; rule.UpdateDate = DateTime.Now; - WriteAuditLog("RULE_BATCH_DELETE", "RULE", rule.id, rule.RuleName, "批量删除规则"); + changedIds.Add(id); + items.Add(BatchItem(id, BatchOperationItemStatus.Succeeded, "DELETED", "规则已删除")); } - _logger.Info("[批量删除] 保存数据库..."); - DbContext.SaveChanges(); - _logger.Info("[批量删除] 刷新规则缓存..."); - foreach (var ruleId in ruleIds) + if (changedIds.Any()) { - RiskEngineService.GetInstance().RefreshOneRuleCache(ruleId); + SaveChangesWithConcurrencyCheck(); + foreach (var id in changedIds) + RiskEngineService.GetInstance().RefreshOneRuleCache(id); } - _logger.Info("[批量删除] 完成"); - return new BatchOperationResult - { - Success = true, - TotalCount = ruleIds.Count, - SuccessCount = ruleIds.Count - }; + return BuildBatchResult(items); } /// @@ -1192,200 +1222,115 @@ namespace YLErp.Modules.RiskEngine if (ruleIds == null || ruleIds.Count == 0) throw new ServiceException("规则ID列表不能为空"); - _logger.Info($"[批量启用规则] 开始 - 传入规则数: {ruleIds.Count}, IDs: {string.Join(",", ruleIds)}"); + var ids = DistinctIds(ruleIds); + var rules = DbContext.glms_risk_rule.Where(r => ids.Contains(r.id)).ToDictionary(r => (long)r.id); + var items = new List(); + var changedIds = new List(); - var rules = DbContext.glms_risk_rule - .Where(r => ruleIds.Contains(r.id) && r.Status != RiskRuleStatus.Deleted) - .ToList(); - - _logger.Info($"[批量启用规则] DB查询完成 - 匹配到 {rules.Count} 条规则"); - - var foundIds = rules.Select(r => (long)r.id).ToList(); - var missingIds = ruleIds.Except(foundIds).ToList(); - if (missingIds.Any()) + foreach (var id in ids) { - return new BatchOperationResult + if (!rules.TryGetValue(id, out var rule) || rule.Status == RiskRuleStatus.Deleted) { - Success = false, - TotalCount = ruleIds.Count, - SuccessCount = 0, - ErrorMessage = $"以下规则不存在或已删除:{string.Join(", ", missingIds)}" - }; - } - - // 跳过已启用的规则 - var skippedIds = rules.Where(r => r.Status == RiskRuleStatus.Active).Select(r => (long)r.id).ToList(); - if (skippedIds.Any()) - _logger.Info($"[批量启用规则] 跳过已启用规则 - IDs: {string.Join(",", skippedIds)}"); - - rules = rules.Where(r => r.Status != RiskRuleStatus.Active).ToList(); - if (!rules.Any()) - { - _logger.Info("[批量启用规则] 所有规则均已启用,无需操作"); - return new BatchOperationResult + items.Add(BatchItem(id, BatchOperationItemStatus.Failed, "NOT_FOUND", "规则不存在或已删除")); + continue; + } + if (rule.Status == RiskRuleStatus.Active) { - Success = true, - TotalCount = ruleIds.Count, - SuccessCount = ruleIds.Count - }; - } + items.Add(BatchItem(id, BatchOperationItemStatus.Unchanged, "ALREADY_ENABLED", "规则已启用,无需重复操作")); + continue; + } - // 逐条编译校验,任一失败则整体拒绝 - var errors = new List(); - foreach (var rule in rules) - { var compileResult = RuleCompiler.ValidateAndCompileFormula(rule.id, rule.RuleExpr); if (!compileResult.Success) - errors.Add($"规则'{rule.RuleName}'(ID:{rule.id}):{compileResult.ErrorMessage}"); - } - if (errors.Any()) - { - return new BatchOperationResult { - Success = false, - TotalCount = ruleIds.Count, - SuccessCount = 0, - ErrorMessage = $"规则表达式编译失败:{string.Join(";", errors)}" - }; - } + items.Add(BatchItem(id, BatchOperationItemStatus.Failed, "COMPILE_FAILED", + $"规则表达式编译失败:{compileResult.ErrorMessage}")); + continue; + } - foreach (var rule in rules) - { - _logger.Info($"[批量启用规则] 写入状态 - 规则ID: {rule.id}"); rule.Status = RiskRuleStatus.Active; + rule.Version++; rule.UpdateOptId = UserId; rule.UpdateOptName = UserName; rule.UpdateDate = DateTime.Now; + WriteAuditLog("RULE_BATCH_ENABLE", "RULE", rule.id, rule.RuleName, "批量启用规则"); + changedIds.Add(id); + items.Add(BatchItem(id, BatchOperationItemStatus.Succeeded, "ENABLED", "规则已启用")); } - var enabledIds = rules.Select(r => r.id).ToList(); - WriteAuditLog("RULE_BATCH_ENABLE", "RULE", 0, "", - $"批量启用规则(IDs: {string.Join(",", enabledIds)})"); - - _logger.Info("[批量启用规则] 保存数据库..."); - DbContext.SaveChanges(); - _logger.Info("[批量启用规则] 刷新规则缓存..."); - foreach (var rule in rules) + if (changedIds.Any()) { - RiskEngineService.GetInstance().RefreshOneRuleCache(rule.id); + SaveChangesWithConcurrencyCheck(); + foreach (var id in changedIds) + RiskEngineService.GetInstance().RefreshOneRuleCache(id); } - _logger.Info("[批量启用规则] 完成"); - return new BatchOperationResult - { - Success = true, - TotalCount = ruleIds.Count, - SuccessCount = ruleIds.Count - }; + return BuildBatchResult(items); } /// /// 批量停用规则(仅 Active → Disabled,级联停用关联应用) /// - public BatchDisableRulesResult BatchDisableRules(List ruleIds) + public BatchOperationResult BatchDisableRules(List ruleIds) { if (ruleIds == null || ruleIds.Count == 0) throw new ServiceException("规则ID列表不能为空"); - _logger.Info($"[批量停用规则] 开始 - 传入规则数: {ruleIds.Count}, IDs: {string.Join(",", ruleIds)}"); - - var rules = DbContext.glms_risk_rule - .Where(r => ruleIds.Contains(r.id) && r.Status != RiskRuleStatus.Deleted) + var ids = DistinctIds(ruleIds); + var rules = DbContext.glms_risk_rule.Where(r => ids.Contains(r.id)).ToDictionary(r => (long)r.id); + var activeRules = ids.Where(id => rules.TryGetValue(id, out var rule) && rule.Status == RiskRuleStatus.Active).ToList(); + var activeApps = DbContext.glms_risk_rule_application + .Where(a => a.Status == RiskRuleStatus.Active) .ToList(); + var cascadesByRule = activeRules.ToDictionary( + id => id, + id => activeApps.Where(a => ParseRuleIds(a.RuleIds).Contains(id)).Select(a => (long)a.id).Distinct().ToList()); + var items = new List(); - _logger.Info($"[批量停用规则] DB查询完成 - 匹配到 {rules.Count} 条规则"); - - var foundIds = rules.Select(r => (long)r.id).ToList(); - var missingIds = ruleIds.Except(foundIds).ToList(); - if (missingIds.Any()) + foreach (var id in ids) { - return new BatchDisableRulesResult + if (!rules.TryGetValue(id, out var rule) || rule.Status == RiskRuleStatus.Deleted) { - Success = false, - TotalCount = ruleIds.Count, - SuccessCount = 0, - ErrorMessage = $"以下规则不存在或已删除:{string.Join(", ", missingIds)}" - }; - } - - // 跳过已停用的规则 - var skippedIds = rules.Where(r => r.Status == RiskRuleStatus.Disabled).Select(r => (long)r.id).ToList(); - if (skippedIds.Any()) - _logger.Info($"[批量停用规则] 跳过已停用规则 - IDs: {string.Join(",", skippedIds)}"); - - rules = rules.Where(r => r.Status != RiskRuleStatus.Disabled).ToList(); - if (!rules.Any()) - { - _logger.Info("[批量停用规则] 所有规则均已停用,无需操作"); - return new BatchDisableRulesResult + items.Add(BatchItem(id, BatchOperationItemStatus.Failed, "NOT_FOUND", "规则不存在或已删除")); + continue; + } + if (rule.Status == RiskRuleStatus.Disabled) { - Success = true, - TotalCount = ruleIds.Count, - SuccessCount = ruleIds.Count - }; - } + items.Add(BatchItem(id, BatchOperationItemStatus.Unchanged, "ALREADY_DISABLED", "规则已停用,无需重复操作")); + continue; + } - var allCascadedAppIds = new List(); - foreach (var rule in rules) - { - _logger.Info($"[批量停用规则] 写入状态 - 规则ID: {rule.id}"); rule.Status = RiskRuleStatus.Disabled; + rule.Version++; rule.UpdateOptId = UserId; rule.UpdateOptName = UserName; rule.UpdateDate = DateTime.Now; + WriteAuditLog("RULE_BATCH_DISABLE", "RULE", rule.id, rule.RuleName, "批量停用规则"); + items.Add(BatchItem(id, BatchOperationItemStatus.Succeeded, "DISABLED", "规则已停用", cascadesByRule[id])); + } - // 级联停用关联的 Active 应用配置 - var activeApps = DbContext.glms_risk_rule_application - .Where(a => a.Status == RiskRuleStatus.Active) - .Where(RuleIdsMatchExpr(rule.id)) - .ToList(); - - var cascadedIds = new List(); - foreach (var app in activeApps) - { - // 同一应用可能同时关联多条待停用规则,只对实际仍为 Active 的应用处理一次。 - if (app.Status != RiskRuleStatus.Active) - continue; - - app.Status = RiskRuleStatus.Disabled; - app.UpdateOptId = UserId; - app.UpdateOptName = UserName; - app.UpdateDate = DateTime.Now; - cascadedIds.Add(app.id); - - WriteAuditLog("RULE_CASCADE_DISABLE_APP", "APPLICATION", app.id, $"应用{app.id}", - $"因规则'{rule.RuleName}'(ID:{rule.id})停用,应用配置被级联停用"); - } + var cascadedIds = items.SelectMany(i => i.CascadedApplicationIds).Distinct().ToHashSet(); + foreach (var app in activeApps.Where(a => cascadedIds.Contains(a.id))) + { + app.Status = RiskRuleStatus.Disabled; + app.Version++; + app.UpdateOptId = UserId; + app.UpdateOptName = UserName; + app.UpdateDate = DateTime.Now; + WriteAuditLog("RULE_CASCADE_DISABLE_APP", "APPLICATION", app.id, $"应用{app.id}", + $"因规则批量停用,应用配置被级联停用(规则IDs:{string.Join(",", activeRules.Where(id => cascadesByRule[id].Contains(app.id)))})"); + } + if (items.Any(i => i.Status == BatchOperationItemStatus.Succeeded)) + { + SaveChangesWithConcurrencyCheck(); + foreach (var id in activeRules) + RiskEngineService.GetInstance().RefreshOneRuleCache(id); if (cascadedIds.Any()) - { - _logger.Info($"[批量停用规则] 规则ID: {rule.id}, 级联停用应用: {string.Join(",", cascadedIds)}"); - allCascadedAppIds.AddRange(cascadedIds); - } + RiskEngineService.GetInstance().RefreshApplication(); } - var disabledIds = rules.Select(r => r.id).ToList(); - WriteAuditLog("RULE_BATCH_DISABLE", "RULE", 0, "", - $"批量停用规则(IDs: {string.Join(",", disabledIds)})"); - - _logger.Info("[批量停用规则] 保存数据库..."); - DbContext.SaveChanges(); - _logger.Info("[批量停用规则] 刷新规则缓存..."); - foreach (var rule in rules) - { - RiskEngineService.GetInstance().RefreshOneRuleCache(rule.id); - } - if (allCascadedAppIds.Any()) - RiskEngineService.GetInstance().RefreshApplication(); - _logger.Info("[批量停用规则] 完成"); - - return new BatchDisableRulesResult - { - Success = true, - TotalCount = ruleIds.Count, - SuccessCount = ruleIds.Count, - CascadedApplicationIds = allCascadedAppIds - }; + return BuildBatchResult(items); } /// @@ -1462,9 +1407,9 @@ namespace YLErp.Modules.RiskEngine query = query.Where(a => a.Status == req.Status.Value); } - if (req.Strategy.HasValue) + if (req.ControlStrategy.HasValue) { - query = query.Where(a => a.ControlStrategy == req.Strategy.Value); + query = query.Where(a => a.ControlStrategy == req.ControlStrategy.Value); } if (!string.IsNullOrWhiteSpace(req.TriggerPoint)) @@ -1472,11 +1417,11 @@ namespace YLErp.Modules.RiskEngine query = query.Where(a => a.TriggerPoints.Contains(req.TriggerPoint)); } - if (!string.IsNullOrWhiteSpace(req.RuleName)) + if (!string.IsNullOrWhiteSpace(req.Keyword)) { query = query.Where(a => DbContext.glms_risk_rule .Any(r => r.Status != RiskRuleStatus.Deleted - && r.RuleName.Contains(req.RuleName) + && r.RuleName.Contains(req.Keyword) && a.RuleIds != null && (a.RuleIds == r.id.ToString() || a.RuleIds.StartsWith(r.id.ToString() + ",") @@ -1568,7 +1513,7 @@ namespace YLErp.Modules.RiskEngine { RuleIds = normalizedRuleIds, Description = req.Description, - Status = RiskRuleStatus.Active, + Status = RiskRuleStatus.Disabled, ControlStrategy = req.ControlStrategy, TriggerPoints = req.TriggerPoints, ScopeAssetBookIds = req.ScopeAssetBookIds, @@ -1589,12 +1534,12 @@ namespace YLErp.Modules.RiskEngine try { DbContext.glms_risk_rule_application.Add(entity); - DbContext.SaveChanges(); + SaveChangesWithConcurrencyCheck(); WriteAuditLog("APP_CREATE", "APPLICATION", entity.id, ResolveRuleNames(entity.RuleIds), $"创建应用配置:策略={GetStrategyName(req.ControlStrategy)}, 触发时点={MapTriggerPointsToChinese(req.TriggerPoints)}", snapshotData: JsonConvert.SerializeObject(new { Version = entity.Version })); - DbContext.SaveChanges(); + SaveChangesWithConcurrencyCheck(); transaction.Commit(); } @@ -1657,7 +1602,7 @@ namespace YLErp.Modules.RiskEngine WriteAuditLog("APP_UPDATE", "APPLICATION", applicationId, ResolveRuleNames(app.RuleIds), $"修改应用配置:策略={GetStrategyName(req.ControlStrategy)}, 触发时点={MapTriggerPointsToChinese(req.TriggerPoints)}"); - DbContext.SaveChanges(); + SaveChangesWithConcurrencyCheck(); RiskEngineService.GetInstance().RefreshApplication(); @@ -1678,7 +1623,7 @@ namespace YLErp.Modules.RiskEngine app.UpdateDate = DateTime.Now; WriteAuditLog("APP_DELETE", "APPLICATION", applicationId, ResolveRuleNames(app.RuleIds), "删除应用配置"); - DbContext.SaveChanges(); + SaveChangesWithConcurrencyCheck(); RiskEngineService.GetInstance().RefreshApplication(); } @@ -1691,53 +1636,32 @@ namespace YLErp.Modules.RiskEngine if (applicationIds == null || applicationIds.Count == 0) throw new ServiceException("应用配置ID列表不能为空"); - _logger.Info($"[批量删除应用] 开始 - 传入应用数: {applicationIds.Count}, IDs: {string.Join(",", applicationIds)}"); - - var apps = DbContext.glms_risk_rule_application - .Where(a => applicationIds.Contains(a.id) && a.Status != RiskRuleStatus.Deleted) - .ToList(); - - _logger.Info($"[批量删除应用] DB查询完成 - 匹配到 {apps.Count} 条应用"); - - var foundIds = apps.Select(a => (long)a.id).ToList(); - var missingIds = applicationIds.Except(foundIds).ToList(); - if (missingIds.Any()) + var ids = DistinctIds(applicationIds); + var apps = DbContext.glms_risk_rule_application.Where(a => ids.Contains(a.id)).ToDictionary(a => (long)a.id); + var items = new List(); + foreach (var id in ids) { - return new BatchOperationResult + if (!apps.TryGetValue(id, out var app) || app.Status == RiskRuleStatus.Deleted) { - Success = false, - TotalCount = applicationIds.Count, - SuccessCount = 0, - ErrorMessage = $"以下应用配置不存在或已删除:{string.Join(", ", missingIds)}" - }; - } + items.Add(BatchItem(id, BatchOperationItemStatus.Failed, "NOT_FOUND", "应用配置不存在或已删除")); + continue; + } - foreach (var app in apps) - { - _logger.Info($"[批量删除应用] 写入状态 - 应用ID: {app.id}"); app.Status = RiskRuleStatus.Deleted; - app.Version = app.Version + 1; + app.Version++; app.UpdateOptId = UserId; app.UpdateOptName = UserName; app.UpdateDate = DateTime.Now; + WriteAuditLog("APP_BATCH_DELETE", "APPLICATION", app.id, ResolveRuleNames(app.RuleIds), "批量删除应用配置"); + items.Add(BatchItem(id, BatchOperationItemStatus.Succeeded, "DELETED", "应用配置已删除")); } - var deletedIds = apps.Select(a => a.id).ToList(); - WriteAuditLog("APP_BATCH_DELETE", "APPLICATION", 0, "", - $"批量删除应用配置(IDs: {string.Join(",", deletedIds)})"); - - _logger.Info("[批量删除应用] 保存数据库..."); - DbContext.SaveChanges(); - _logger.Info("[批量删除应用] 刷新应用缓存..."); - RiskEngineService.GetInstance().RefreshApplication(); - _logger.Info("[批量删除应用] 完成"); - - return new BatchOperationResult + if (items.Any(i => i.Status == BatchOperationItemStatus.Succeeded)) { - Success = true, - TotalCount = applicationIds.Count, - SuccessCount = applicationIds.Count - }; + SaveChangesWithConcurrencyCheck(); + RiskEngineService.GetInstance().RefreshApplication(); + } + return BuildBatchResult(items); } /// @@ -1754,12 +1678,14 @@ namespace YLErp.Modules.RiskEngine throw new ServiceException($"以下关联规则不可用:{string.Join(";", errors)}"); app.Status = RiskRuleStatus.Active; + + app.Version = app.Version + 1; app.UpdateOptId = UserId; app.UpdateOptName = UserName; app.UpdateDate = DateTime.Now; WriteAuditLog("APP_ENABLE", "APPLICATION", applicationId, ResolveRuleNames(app.RuleIds), "启用应用配置"); - DbContext.SaveChanges(); + SaveChangesWithConcurrencyCheck(); RiskEngineService.GetInstance().RefreshApplication(); } @@ -1774,12 +1700,14 @@ namespace YLErp.Modules.RiskEngine throw new ServiceException("仅已启用的应用配置可以停用"); app.Status = RiskRuleStatus.Disabled; + + app.Version = app.Version + 1; app.UpdateOptId = UserId; app.UpdateOptName = UserName; app.UpdateDate = DateTime.Now; WriteAuditLog("APP_DISABLE", "APPLICATION", applicationId, ResolveRuleNames(app.RuleIds), "停用应用配置"); - DbContext.SaveChanges(); + SaveChangesWithConcurrencyCheck(); RiskEngineService.GetInstance().RefreshApplication(); } @@ -1792,88 +1720,50 @@ namespace YLErp.Modules.RiskEngine if (applicationIds == null || applicationIds.Count == 0) throw new ServiceException("应用配置ID列表不能为空"); - _logger.Info($"[批量启用] 开始 - 传入应用数: {applicationIds.Count}, IDs: {string.Join(",", applicationIds)}"); - - var apps = DbContext.glms_risk_rule_application - .Where(a => applicationIds.Contains(a.id)) - .ToList(); - - _logger.Info($"[批量启用] DB查询完成 - 匹配到 {apps.Count} 条应用"); - - var foundIds = apps.Select(a => (long)a.id).ToList(); - var missingIds = applicationIds.Except(foundIds).ToList(); - if (missingIds.Any()) + var ids = DistinctIds(applicationIds); + var apps = DbContext.glms_risk_rule_application.Where(a => ids.Contains(a.id)).ToDictionary(a => (long)a.id); + var items = new List(); + foreach (var id in ids) { - return new BatchOperationResult + if (!apps.TryGetValue(id, out var app)) { - Success = false, - TotalCount = applicationIds.Count, - SuccessCount = 0, - ErrorMessage = $"以下应用配置不存在:{string.Join(", ", missingIds)}" - }; - } - - // 跳过已启用的应用,只处理已停用的 - var skippedIds = apps.Where(a => a.Status == RiskRuleStatus.Active).Select(a => (long)a.id).ToList(); - if (skippedIds.Any()) - _logger.Info($"[批量启用] 跳过已启用应用 - IDs: {string.Join(",", skippedIds)}"); - - apps = apps.Where(a => a.Status != RiskRuleStatus.Active).ToList(); - if (!apps.Any()) - { - _logger.Info("[批量启用] 所有应用均已启用,无需操作"); - return new BatchOperationResult + items.Add(BatchItem(id, BatchOperationItemStatus.Failed, "NOT_FOUND", "应用配置不存在")); + continue; + } + if (app.Status == RiskRuleStatus.Deleted) { - Success = true, - TotalCount = applicationIds.Count, - SuccessCount = applicationIds.Count - }; - } + items.Add(BatchItem(id, BatchOperationItemStatus.Failed, "DELETED_ENTITY", "已删除应用不能启用")); + continue; + } + if (app.Status == RiskRuleStatus.Active) + { + items.Add(BatchItem(id, BatchOperationItemStatus.Unchanged, "ALREADY_ENABLED", "应用配置已启用,无需重复操作")); + continue; + } - var allErrors = new List(); - foreach (var app in apps) - { - _logger.Info($"[批量启用] 校验规则 - 应用ID: {app.id}, RuleIds: {app.RuleIds}"); var errors = ValidateAndEnsureRuleCache(app.RuleIds); - _logger.Info($"[批量启用] 规则校验结果 - 应用ID: {app.id}, Errors: {(errors.Any() ? string.Join(";", errors) : "无")}"); if (errors.Any()) - allErrors.Add($"应用 '{app.id}':{string.Join(";", errors)}"); - } - - if (allErrors.Any()) - { - return new BatchOperationResult { - Success = false, - TotalCount = applicationIds.Count, - SuccessCount = 0, - ErrorMessage = string.Join(";", allErrors) - }; - } + items.Add(BatchItem(id, BatchOperationItemStatus.Failed, "RELATED_RULE_UNAVAILABLE", + $"关联规则不可用:{string.Join(";", errors)}")); + continue; + } - foreach (var app in apps) - { - _logger.Info($"[批量启用] 写入状态 - 应用ID: {app.id}"); app.Status = RiskRuleStatus.Active; + app.Version++; app.UpdateOptId = UserId; app.UpdateOptName = UserName; app.UpdateDate = DateTime.Now; - WriteAuditLog("APP_BATCH_ENABLE", "APPLICATION", app.id, ResolveRuleNames(app.RuleIds), "批量启用应用配置"); + items.Add(BatchItem(id, BatchOperationItemStatus.Succeeded, "ENABLED", "应用配置已启用")); } - _logger.Info("[批量启用] 保存数据库..."); - DbContext.SaveChanges(); - _logger.Info("[批量启用] 刷新应用缓存..."); - RiskEngineService.GetInstance().RefreshApplication(); - _logger.Info("[批量启用] 完成"); - - return new BatchOperationResult + if (items.Any(i => i.Status == BatchOperationItemStatus.Succeeded)) { - Success = true, - TotalCount = applicationIds.Count, - SuccessCount = applicationIds.Count - }; + SaveChangesWithConcurrencyCheck(); + RiskEngineService.GetInstance().RefreshApplication(); + } + return BuildBatchResult(items); } /// @@ -1884,50 +1774,42 @@ namespace YLErp.Modules.RiskEngine if (applicationIds == null || applicationIds.Count == 0) throw new ServiceException("应用配置ID列表不能为空"); - _logger.Info($"[批量停用] 开始 - 传入应用数: {applicationIds.Count}, IDs: {string.Join(",", applicationIds)}"); - - var apps = DbContext.glms_risk_rule_application - .Where(a => applicationIds.Contains(a.id) && a.Status == RiskRuleStatus.Active) - .ToList(); - - _logger.Info($"[批量停用] DB查询完成 - 匹配到 {apps.Count} 条已启用应用"); - - var foundIds = apps.Select(a => (long)a.id).ToList(); - var missingIds = applicationIds.Except(foundIds).ToList(); - if (missingIds.Any()) + var ids = DistinctIds(applicationIds); + var apps = DbContext.glms_risk_rule_application.Where(a => ids.Contains(a.id)).ToDictionary(a => (long)a.id); + var items = new List(); + foreach (var id in ids) { - return new BatchOperationResult + if (!apps.TryGetValue(id, out var app)) { - Success = false, - TotalCount = applicationIds.Count, - SuccessCount = apps.Count, - ErrorMessage = $"以下应用配置不存在或当前状态不允许停用:{string.Join(", ", missingIds)}" - }; - } + items.Add(BatchItem(id, BatchOperationItemStatus.Failed, "NOT_FOUND", "应用配置不存在")); + continue; + } + if (app.Status == RiskRuleStatus.Deleted) + { + items.Add(BatchItem(id, BatchOperationItemStatus.Failed, "DELETED_ENTITY", "已删除应用不能停用")); + continue; + } + if (app.Status == RiskRuleStatus.Disabled) + { + items.Add(BatchItem(id, BatchOperationItemStatus.Unchanged, "ALREADY_DISABLED", "应用配置已停用,无需重复操作")); + continue; + } - foreach (var app in apps) - { - _logger.Info($"[批量停用] 写入状态 - 应用ID: {app.id}"); app.Status = RiskRuleStatus.Disabled; + app.Version++; app.UpdateOptId = UserId; app.UpdateOptName = UserName; app.UpdateDate = DateTime.Now; - WriteAuditLog("APP_BATCH_DISABLE", "APPLICATION", app.id, ResolveRuleNames(app.RuleIds), "批量停用应用配置"); + items.Add(BatchItem(id, BatchOperationItemStatus.Succeeded, "DISABLED", "应用配置已停用")); } - _logger.Info("[批量停用] 保存数据库..."); - DbContext.SaveChanges(); - _logger.Info("[批量停用] 刷新应用缓存..."); - RiskEngineService.GetInstance().RefreshApplication(); - _logger.Info("[批量停用] 完成"); - - return new BatchOperationResult + if (items.Any(i => i.Status == BatchOperationItemStatus.Succeeded)) { - Success = true, - TotalCount = applicationIds.Count, - SuccessCount = applicationIds.Count - }; + SaveChangesWithConcurrencyCheck(); + RiskEngineService.GetInstance().RefreshApplication(); + } + return BuildBatchResult(items); } #endregion @@ -2237,11 +2119,11 @@ namespace YLErp.Modules.RiskEngine try { DbContext.glms_risk_variable.Add(entity); - DbContext.SaveChanges(); + SaveChangesWithConcurrencyCheck(); WriteAuditLog("VARIABLE_CREATE", "VARIABLE", entity.id, req.VariableName, $"创建变量:{req.VariableName}", snapshotData: JsonConvert.SerializeObject(new { Version = entity.Version })); - DbContext.SaveChanges(); + SaveChangesWithConcurrencyCheck(); transaction.Commit(); } @@ -2333,11 +2215,11 @@ namespace YLErp.Modules.RiskEngine $"因变量'{variable.VariableName}'(ID:{variableId})表达式变更,规则表达式被级联更新"); } - DbContext.SaveChanges(); + SaveChangesWithConcurrencyCheck(); WriteAuditLog("VAR_UPDATE", "VARIABLE", variableId, req.VariableName, $"修改变量:{variable.VariableName}"); - DbContext.SaveChanges(); + SaveChangesWithConcurrencyCheck(); transaction.Commit(); } catch @@ -2356,7 +2238,7 @@ namespace YLErp.Modules.RiskEngine else { WriteAuditLog("VAR_UPDATE", "VARIABLE", variableId, req.VariableName, $"修改变量:{variable.VariableName}"); - DbContext.SaveChanges(); + SaveChangesWithConcurrencyCheck(); InvalidateVariableCache(); } @@ -2378,7 +2260,7 @@ namespace YLErp.Modules.RiskEngine DbContext.glms_risk_variable.Remove(variable); WriteAuditLog("VAR_DELETE", "VARIABLE", variableId, variable.VariableName, $"删除变量:{variable.VariableName}"); - DbContext.SaveChanges(); + SaveChangesWithConcurrencyCheck(); InvalidateVariableCache(); } @@ -2386,53 +2268,47 @@ namespace YLErp.Modules.RiskEngine /// /// 批量删除变量(不存在或被生效规则引用的变量将被跳过) /// - public BatchDeleteVariablesResult BatchDeleteVariables(List variableIds) + public BatchOperationResult BatchDeleteVariables(List variableIds) { if (variableIds == null || variableIds.Count == 0) throw new ServiceException("变量ID列表不能为空"); - var distinctIds = variableIds.Distinct().ToList(); - var variables = DbContext.glms_risk_variable - .Where(v => distinctIds.Contains(v.id)) + var ids = DistinctIds(variableIds); + var variables = DbContext.glms_risk_variable.Where(v => ids.Contains(v.id)).ToDictionary(v => (long)v.id); + var activeRules = DbContext.glms_risk_rule + .Where(r => r.Status == RiskRuleStatus.Active && r.ConditionJson != null) .ToList(); - var foundIds = variables.Select(v => (long)v.id).ToList(); - var missingIds = distinctIds.Except(foundIds).ToList(); - var blockedIds = new List(); - var blockedReasons = new Dictionary(); - var deletedIds = new List(); + var items = new List(); - foreach (var variable in variables) + foreach (var id in ids) { - var activeRefCount = GetRulesByVariableId(variable.id) - .Count(r => r.Status == RiskRuleStatus.Active); + if (!variables.TryGetValue(id, out var variable)) + { + items.Add(BatchItem(id, BatchOperationItemStatus.Failed, "NOT_FOUND", "变量不存在")); + continue; + } + + var idPattern = $@"""VariableId""\s*:\s*{id}\b"; + var thresholdPattern = $@"""ThresholdVariableId""\s*:\s*{id}\b"; + var activeRefCount = activeRules.Count(r => Regex.IsMatch(r.ConditionJson, idPattern) || Regex.IsMatch(r.ConditionJson, thresholdPattern)); if (activeRefCount > 0) { - blockedIds.Add(variable.id); - blockedReasons[variable.id] = $"该变量被 {activeRefCount} 个生效中的规则引用,无法删除"; + items.Add(BatchItem(id, BatchOperationItemStatus.Failed, "ACTIVE_RULE_REFERENCE", + $"该变量被 {activeRefCount} 个生效中的规则引用,无法删除")); continue; } DbContext.glms_risk_variable.Remove(variable); WriteAuditLog("VAR_BATCH_DELETE", "VARIABLE", variable.id, variable.VariableName, $"批量删除变量:{variable.VariableName}"); - deletedIds.Add(variable.id); + items.Add(BatchItem(id, BatchOperationItemStatus.Succeeded, "DELETED", "变量已删除")); } - if (deletedIds.Count > 0) + if (items.Any(i => i.Status == BatchOperationItemStatus.Succeeded)) { - DbContext.SaveChanges(); + SaveChangesWithConcurrencyCheck(); InvalidateVariableCache(); } - - return new BatchDeleteVariablesResult - { - Success = true, - TotalCount = distinctIds.Count, - SuccessCount = deletedIds.Count, - DeletedIds = deletedIds, - MissingIds = missingIds, - BlockedIds = blockedIds, - BlockedReasons = blockedReasons - }; + return BuildBatchResult(items); } /// diff --git a/YLErpWeb/Controllers/RiskRuleController.cs b/YLErpWeb/Controllers/RiskRuleController.cs index e47aa4f7..e90a2087 100644 --- a/YLErpWeb/Controllers/RiskRuleController.cs +++ b/YLErpWeb/Controllers/RiskRuleController.cs @@ -1,4 +1,4 @@ -using Qdp.Foundation.Utilities; +using Qdp.Foundation.Utilities; using System.Threading.Tasks; using YLErp.DBModels; using YLErp.Modules.RiskEngine; @@ -157,8 +157,6 @@ namespace YLErp.Web.Controllers { var service = GetRiskRuleService(); var result = await Task.Run(() => service.BatchDeleteRules(req?.Ids)); - if (!result.Success) - return Json(new { success = false, message = result.ErrorMessage }); return Json(new { success = true, data = result }); } catch (ServiceException ex) @@ -230,8 +228,6 @@ namespace YLErp.Web.Controllers { var service = GetRiskRuleService(); var result = await Task.Run(() => service.BatchEnableRules(req?.Ids)); - if (!result.Success) - return Json(new { success = false, message = result.ErrorMessage }); return Json(new { success = true, data = result }); } catch (ServiceException ex) @@ -254,15 +250,7 @@ namespace YLErp.Web.Controllers { var service = GetRiskRuleService(); var result = await Task.Run(() => service.BatchDisableRules(req?.Ids)); - if (!result.Success) - return Json(new { success = false, message = result.ErrorMessage }); - return Json(new - { - success = true, - data = result, - cascadedAppIds = result.CascadedApplicationIds, - cascadedAppCount = result.CascadedApplicationIds.Count - }); + return Json(new { success = true, data = result }); } catch (ServiceException ex) { @@ -486,8 +474,6 @@ namespace YLErp.Web.Controllers { var service = GetRiskRuleService(); var result = await Task.Run(() => service.BatchEnableApplications(req?.Ids)); - if (!result.Success) - return Json(new { success = false, message = result.ErrorMessage }); return Json(new { success = true, data = result }); } catch (ServiceException ex) @@ -510,8 +496,6 @@ namespace YLErp.Web.Controllers { var service = GetRiskRuleService(); var result = await Task.Run(() => service.BatchDisableApplications(req?.Ids)); - if (!result.Success) - return Json(new { success = false, message = result.ErrorMessage }); return Json(new { success = true, data = result }); } catch (ServiceException ex) @@ -534,8 +518,6 @@ namespace YLErp.Web.Controllers { var service = GetRiskRuleService(); var result = await Task.Run(() => service.BatchDeleteApplications(req?.Ids)); - if (!result.Success) - return Json(new { success = false, message = result.ErrorMessage }); return Json(new { success = true, data = result }); } catch (ServiceException ex)