Files
zszq-trs/YLErpDAL/Modules/RiskEngine/RiskRuleService.cs
T

2403 lines
103 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using BaseOUDAL;
using Newtonsoft.Json;
using System.Linq;
using System.Linq.Expressions;
using System.Text.RegularExpressions;
using YLErp.BLL;
using YLErp.Commons;
using YLErp.DBModels;
using YLErp.Modules.RiskEngine.Dto;
/*
================================================================================
风控规则管理服务 — RiskRuleService 技术方案说明
================================================================================
【职责定位】
RiskRuleService 是风控引擎模块的 CRUD 管理服务层,负责规则(Rule)、应用配置
Application)、变量(Variable)和操作审计日志(AuditLog)的全生命周期管理。
与 RiskEngineService(引擎执行层)的关系:
- 本服务:配置管理(增删改查 + 审计日志)
- RiskEngineService:运行时执行(缓存 + 公式解析 + 风控判定)
- 交互方式:本服务在每次写操作后调用 RiskEngineService.RefreshCache() 通知引擎刷新
【设计文档】
详细设计文档:.trae/documents/RiskRuleService详细设计.mdv1.1
需求来源文档:.trae/documents/风控引擎需求详细设计.mdv1.17)
核心概念:
- Rule(规则):定义"什么情况下触发",支持两种公式模式(互斥):
· 结构化模式(ConditionJson 有值):变量 + 操作符 + 阈值/变量 的条件列表,前端拼接 RuleExpr
· 自由文本模式(RuleExpr 有值,ConditionJson 为空):前端直接编辑类 C# 表达式
- Application(应用配置):定义"何时、对谁、怎么处理",关联多条规则(RuleIds 逗号分隔)+ 策略/时点/范围
- Variable(变量):变量池中的可选项,用于构建规则条件(含取值表达式 VariableExpr
- AuditLog(审计日志):记录所有 CRUD 操作,支持版本追溯(SnapshotData 存储版本号快照)
【架构位置】
┌─────────────────────────────────────────────────────────────────┐
│ API 层 │
│ RiskRuleController27 个端点) │
│ - /api/risk-rules/* 规则 CRUD10 个端点) │
│ - /api/risk-applications/* 应用配置 CRUD9 个端点) │
│ - /api/risk-variables/* 变量池 CRUD6 个端点) │
│ - /api/risk-audit-logs/* 审计日志(3 个端点) │
└──────────────────────────┬──────────────────────────────────────┘
┌──────────────────────────▼──────────────────────────────────────┐
│ 服务层(本文件所在层) │
│ │
│ RiskRuleService(本文件) │
│ - 规则 / 应用 / 变量 / 审计日志的 CRUD │
│ - ConditionJson 完整校验(结构化模式:变量存在性、操作符、类型匹配)│
│ - RuleExpr 基础语法校验(自由文本模式:括号匹配、变量引用校验) │
│ - 乐观锁并发控制(Version 字段) │
│ - 删除保护(有 Active 引用时拒绝删除) │
│ - CRUD 后触发 RiskEngineService.RefreshCache() │
│ │
│ RiskEngineService(另一团队开发,单例) │
│ - 内存缓存管理 + 公式执行 + 风控判定 │
│ - RefreshCache():从数据库重新加载 Active 规则/应用/变量 │
└──────────────────────────┬──────────────────────────────────────┘
┌──────────────────────────▼──────────────────────────────────────┐
│ 数据访问层(EF Core DbContext 直接访问,无独立 Repository
│ - glms_risk_rule 规则定义表 │
│ - glms_risk_rule_application 规则应用配置表(RuleIds 逗号分隔) │
│ - glms_risk_variable 变量池定义表 │
│ - glms_risk_rule_audit_log 操作审计日志表 │
└─────────────────────────────────────────────────────────────────┘
【方法清单】(共 27 个公开方法 + 9 个私有辅助方法)
── 规则管理(10 个) ──────────────────────────────────────────────
QueryRuleList(QueryRiskRuleReq) 查询规则列表(分页,含 ApplicationCount 子查询)
GetRuleDetail(long) 获取规则详情(含关联应用摘要)
CreateRule(CreateRiskRuleReq) 新建规则(ConditionJson 完整校验)
UpdateRule(long, UpdateRiskRuleReq) 修改规则(乐观锁校验,Version+1)
DeleteRule(long) 删除规则(强保护:有 Active 应用时拒绝)
EnableRule(long) 启用规则(仅 Disabled → Active
DisableRule(long) 停用规则(仅 Active → Disabled,级联停用关联应用)
GetRuleVersions(long) 获取规则版本历史(从审计日志 SnapshotData 提取版本号)
BatchDeleteRules(List<long>) 批量删除规则(逐条保护检查,单事务)
BatchEnableRules(List<long>) 批量启用规则(逐条编译校验)
BatchDisableRules(List<long>) 批量停用规则(级联停用关联应用)
GetRuleApplications(long) 获取规则关联的应用配置列表
GetAllRuleList() 获取所有启用规则(轻量字段,供应用配置下拉)
── 应用配置管理(9 个) ───────────────────────────────────────────
QueryApplicationList(QueryRiskApplicationReq) 查询应用列表(多条件筛选,内存关联规则名称)
GetApplicationDetail(long) 获取应用详情(含关联规则信息)
CreateApplication(CreateRiskApplicationReq) 新建应用(关联规则 Active 校验,触发时点/Scope 校验)
UpdateApplication(long, UpdateRiskApplicationReq) 修改应用(乐观锁校验)
DeleteApplication(long) 删除应用(软删除)
BatchDeleteApplications(List<long>) 批量删除应用(软删除)
EnableApplication(long) 启用应用(校验关联规则状态)
DisableApplication(long) 停用应用
BatchEnableApplications(List<long>) 批量启用(逐条校验关联规则)
BatchDisableApplications(List<long>) 批量停用
── 变量管理(6 个) ───────────────────────────────────────────────
QueryVariableList(QueryRiskVariableReq) 查询变量列表(全量返回,Category/Keyword 筛选)
GetVariableDetail(long) 获取变量详情(含 ReferenceCount 引用统计)
CreateVariable(CreateRiskVariableReq) 新建变量(VariableExpr 长度校验 ≤10000
UpdateVariable(long, UpdateRiskVariableReq) 修改变量(乐观锁,DataType 变更保护)
DeleteVariable(long) 删除变量(引用保护:有 Active 规则引用时拒绝,硬删除)
GetAllVariableList() 获取所有已实现变量(轻量字段,供规则编辑器下拉)
── 审计日志(3 个) ───────────────────────────────────────────────
QueryAuditLogs(QueryRiskAuditLogReq) 查询审计日志(多条件筛选,分页)
ExportAuditLogs(QueryRiskAuditLogReq) 导出审计日志(占位实现,后续迭代 Excel 导出)
GetAuditLogDetail(long) 获取审计日志详情(含完整 SnapshotData
── 私有辅助方法 ───────────────────────────────────────────────────
WriteAuditLog(...) 写入审计日志(含 snapshotData 可选参数)
GetVariableExprSet() 获取变量表达式集合(5 分钟本地缓存)
InvalidateVariableCache() 清除变量缓存
ValidateConditionJson(conditionJson) ConditionJson 完整校验(结构化模式)
ValidateRuleExpr(ruleExpr) RuleExpr 基础语法校验(自由文本模式:括号匹配 + 变量引用校验)
ValidateTriggerPoints(triggerPoints) 触发时点校验(5 个合法时点)
ValidateScopeFields(req) Scope 字段校验(全局互斥 + ID 格式校验)
ValidateIdList(idList, fieldName) ID 列表格式校验
RefreshOneRuleCache(ruleId) 单条规则缓存刷新(规则变更后调用)
GetRuleOrThrow / GetApplicationOrThrow / GetVariableOrThrow 实体获取快捷方法
【关键设计决策】
1. 数据访问:直接使用 EF Core DbContext(无独立 Repository,与项目风格一致)
2. 事务策略:Create* 方法使用 BeginTransaction 包裹双 SaveChanges(实体 + 审计日志同事务)
3. 并发控制:乐观锁(Version 字段),修改时版本不匹配抛 ServiceException
4. 删除保护:强保护模式,有 Active 引用时拒绝删除规则/变量
5. ConditionJson 校验:变量存在性(按 VariableId+ 操作符合法性(按 DataType 映射)+ 类型匹配 + 阈值变量引用
6. RuleExpr:自由文本模式下前端直接编辑,后端做基础语法校验(括号匹配 + 变量引用校验)
7. Scope 字段:逗号分隔字符串存储,全局适用时不允许指定其他维度
8. 缓存刷新:规则变更后调用 RefreshOneRuleCache(ruleId),应用变更后调用 RefreshApplication()
9. 批量操作:单事务批量,全部成功或全部失败
10. 变量缓存:5 分钟本地 HashSet<string> 缓存,CRUD 后 InvalidateVariableCache()
11. 审计日志:16 种操作类型,CRUD + 审计日志同一事务,SnapshotData 存储版本快照
12. 多规则支持:Application 通过 RuleIds(逗号分隔)关联多条规则,内存解析关联关系
【数据库表】(MySQL,迁移脚本位于 Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/prod.sql
glms_risk_rule 规则定义表
- 普通索引:Status, OptDate
- 关键字段:ConditionJson(结构化条件列表), RuleExpr(类 C# 表达式,引擎执行用), Version(乐观锁)
glms_risk_rule_application 规则应用配置表
- 普通索引:Status, OptDate
- 关键字段:RuleIds(逗号分隔的规则 ID 列表), ControlStrategy(禁止/审批/提示), TriggerPoints(触发时点), Scope*(适用范围)
glms_risk_variable 变量池定义表
- 关键字段:VariableExpr(取值表达式), DataType(数值/日期/布尔)
glms_risk_rule_audit_log 操作审计日志表
- 普通索引:TargetType+TargetId, OperationType, OptDate
- 关键字段:SnapshotDataJSON 快照,存储版本号等)
【关键文件清单】
本服务:
- RiskRuleService.cs(本文件):规则/应用/变量/日志 CRUD 管理
- Dto/ 目录:请求/响应 DTO 模型
关联服务:
- RiskEngineService.cs:引擎执行层(单例,本服务通过 GetInstance() 获取引用)
- RiskRuleController.csYLErpWeb/Controllers/):API 控制器(27 个端点)
实体模型:
- Framework/YLErp.Core/DBModels/glms_risk_rule.cs
- Framework/YLErp.Core/DBModels/glms_risk_rule_application.cs
- Framework/YLErp.Core/DBModels/glms_risk_variable.cs
- Framework/YLErp.Core/DBModels/glms_risk_rule_audit_log.cs
枚举定义:
- Framework/YLErp.Core/DBModels/Enums/RiskRuleStatus.csActive/Disabled/Deleted
- Framework/YLErp.Core/DBModels/Enums/RiskControlStrategy.csBlock/Approve/Warn
- Framework/YLErp.Core/DBModels/Enums/RiskVariableCategory.cs
- Framework/YLErp.Core/DBModels/Enums/RiskVariableDataType.csNumeric/Date/Boolean
【命名空间】
YLErp.Modules.RiskEngine
【注意事项】
1. RiskEngineService 由另一个团队开发实现,本服务通过 GetInstance() 获取单例引用
2. 规则变更后调用 RefreshOneRuleCache(ruleId),应用变更后调用 RefreshApplication(),变量变更不触发引擎缓存刷新
3. 变量删除为硬删除(物理删除),规则/应用删除为软删除(Status=Deleted
4. ExportAuditLogs 当前为占位实现,Excel 导出功能待后续迭代
5. VariableExpr 当前仅做基础长度校验(≤10000),完整编译校验待引入 Roslyn 库
6. ValidateScopeFields 的 ID 存在性校验已跳过(前端下拉选择器保证有效性)
================================================================================
*/
namespace YLErp.Modules.RiskEngine
{
public class RiskRuleService : YLBaseService
{
private readonly IYcLogger _logger = LogFactory.GetLogger("RiskRuleService");
private HashSet<string> _variableExprCache;
private DateTime _variableCacheUpdateTime;
private static readonly HashSet<string> ValidTriggerPoints = new HashSet<string>
{
"BOOK_CONFIRM",
"CLOSE_REVIEW",
"UPLOAD_CONFIRMATION",
"EVENT_TRIGGER",
"FUND_PAYMENT"
};
private static readonly Dictionary<RiskVariableDataType, HashSet<string>> ValidOperatorsByType = new Dictionary<RiskVariableDataType, HashSet<string>>
{
{ RiskVariableDataType.Numeric, new HashSet<string> { ">", "<", ">=", "<=", "=", "≠", "介于", "不介于" } },
{ RiskVariableDataType.Date, new HashSet<string> { "早于", "晚于", "等于", "不早于", "不晚于", "介于", "不介于" } },
{ RiskVariableDataType.Boolean, new HashSet<string> { "是", "否" } }
};
private static readonly Dictionary<string, string> AuditOperationTypeNames = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "RULE_CREATE", "新建规则" },
{ "RULE_UPDATE", "修改规则" },
{ "RULE_DELETE", "删除规则" },
{ "RULE_ENABLE", "启用规则" },
{ "RULE_DISABLE", "停用规则" },
{ "RULE_BATCH_DELETE", "批量删除规则" },
{ "RULE_BATCH_ENABLE", "批量启用规则" },
{ "RULE_BATCH_DISABLE", "批量停用规则" },
{ "APP_CREATE", "新建应用" },
{ "APP_UPDATE", "修改应用" },
{ "APP_DELETE", "删除应用" },
{ "APP_ENABLE", "启用应用" },
{ "APP_DISABLE", "停用应用" },
{ "APP_BATCH_ENABLE", "批量启用应用" },
{ "APP_BATCH_DISABLE", "批量停用应用" },
{ "APP_BATCH_DELETE", "批量删除应用" },
{ "RULE_CASCADE_DISABLE_APP", "规则级联停用应用" },
{ "VAR_CREATE", "新增变量" },
{ "VARIABLE_CREATE", "新增变量" },
{ "VAR_UPDATE", "修改变量" },
{ "VAR_DELETE", "删除变量" },
{ "VAR_CASCADE_UPDATE_RULE", "变量级联更新规则" }
};
private static readonly Dictionary<string, string> AuditTargetTypeNames = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "RULE", "规则" },
{ "APPLICATION", "应用" },
{ "VARIABLE", "变量" }
};
public RiskRuleService(OptUserInfo userInfo) : base(userInfo)
{
}
public RiskRuleService(YLBaseService baseService) : base(baseService)
{
}
public RiskRuleService(OptUserInfo optUser, YLContext dbContext) : base(optUser, dbContext)
{
}
#region Private Helpers
private static string GetAuditOperationTypeName(string operationType)
{
if (string.IsNullOrWhiteSpace(operationType))
return string.Empty;
return AuditOperationTypeNames.TryGetValue(operationType.Trim(), out var name)
? name
: operationType;
}
private static string GetAuditTargetTypeName(string targetType)
{
if (string.IsNullOrWhiteSpace(targetType))
return string.Empty;
return AuditTargetTypeNames.TryGetValue(targetType.Trim(), out var name)
? name
: targetType;
}
private static string FormatAuditId(object value)
{
return value == null ? string.Empty : value.ToString();
}
private static string SanitizeFileNamePart(string value)
{
if (string.IsNullOrWhiteSpace(value))
return string.Empty;
var sanitized = Regex.Replace(value.Trim(), @"[\\/:*?""<>|\r\n]+", "_");
return sanitized.Length <= 40 ? sanitized : sanitized.Substring(0, 40);
}
/// <summary>
/// 写入审计日志
/// </summary>
private void WriteAuditLog(string operationType, string targetType, long targetId,
string targetName, string operationDetail, string result = "成功", string snapshotData = null)
{
var log = new glms_risk_rule_audit_log
{
OperationType = operationType,
TargetType = targetType,
TargetId = targetId,
TargetName = targetName,
OperationDetail = operationDetail,
Result = result,
SnapshotData = snapshotData,
OptId = UserId,
OptName = UserName,
OptDate = DateTime.Now
};
DbContext.glms_risk_rule_audit_log.Add(log);
}
/// <summary>
/// 获取变量表达式集合(5 分钟本地缓存)
/// </summary>
private HashSet<string> GetVariableExprSet()
{
if (_variableExprCache == null ||
DateTime.Now - _variableCacheUpdateTime > TimeSpan.FromMinutes(5))
{
_variableExprCache = DbContext.glms_risk_variable
.Where(v => v.VariableExpr != null && v.VariableExpr != "")
.Select(v => v.VariableExpr)
.ToHashSet();
_variableCacheUpdateTime = DateTime.Now;
}
return _variableExprCache;
}
/// <summary>
/// 失效变量表达式缓存
/// </summary>
private void InvalidateVariableCache()
{
_variableExprCache = null;
}
/// <summary>
/// 校验 ConditionJson(结构化模式:变量存在性、操作符、类型匹配)
/// </summary>
private void ValidateConditionJson(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("公式条件列表不能为空");
for (int i = 0; i < conditions.Count; i++)
{
var cond = conditions[i];
var condLabel = $"条件{i + 1}";
var variableDef = DbContext.glms_risk_variable
.FirstOrDefault(v => v.id == cond.VariableId);
if (variableDef == null)
throw new ServiceException($"{condLabel}:变量 ID '{cond.VariableId}' 不存在");
var validOperators = ValidOperatorsByType[variableDef.DataType];
if (!validOperators.Contains(cond.Operator))
throw new ServiceException($"{condLabel}:操作符 '{cond.Operator}' 不适用于{GetDataTypeName(variableDef.DataType)}类型变量");
if (cond.ThresholdType == "Fixed")
{
if (cond.Value == null)
throw new ServiceException($"{condLabel}:固定阈值不能为空");
if (cond.Operator == "介于" || cond.Operator == "不介于")
{
// 介于/不介于:Value 必须为双元素数组 [下限, 上限]
if (!(cond.Value is Newtonsoft.Json.Linq.JArray arr && arr.Count == 2))
throw new ServiceException($"{condLabel}:操作符 '{cond.Operator}' 的阈值必须为双元素数组 [下限, 上限]");
if (variableDef.DataType == RiskVariableDataType.Numeric)
{
if (!decimal.TryParse(arr[0].ToString(), out _) || !decimal.TryParse(arr[1].ToString(), out _))
throw new ServiceException($"{condLabel}:数值型介于/不介于的下限和上限必须为数字");
}
if (variableDef.DataType == RiskVariableDataType.Date)
{
if (!DateTime.TryParse(arr[0].ToString(), out _) || !DateTime.TryParse(arr[1].ToString(), out _))
throw new ServiceException($"{condLabel}:日期型介于/不介于的起始和结束日期必须为合法日期");
}
}
else
{
if (variableDef.DataType == RiskVariableDataType.Numeric)
{
if (!decimal.TryParse(cond.Value.ToString(), out _))
throw new ServiceException($"{condLabel}:数值型变量的阈值必须为数字");
}
if (variableDef.DataType == RiskVariableDataType.Date)
{
if (!DateTime.TryParse(cond.Value.ToString(), out _))
throw new ServiceException($"{condLabel}:日期型变量的阈值必须为合法日期");
}
}
}
else if (cond.ThresholdType == "Variable")
{
if (!cond.ThresholdVariableId.HasValue)
throw new ServiceException($"{condLabel}:变量阈值引用的变量 ID 不能为空");
var thresholdVarDef = DbContext.glms_risk_variable
.FirstOrDefault(v => v.id == cond.ThresholdVariableId.Value);
if (thresholdVarDef == null)
throw new ServiceException($"{condLabel}:阈值变量 ID '{cond.ThresholdVariableId.Value}' 不存在");
if (thresholdVarDef.DataType != variableDef.DataType)
throw new ServiceException($"{condLabel}:阈值变量与条件变量的数据类型不一致");
}
else
{
throw new ServiceException($"{condLabel}:阈值类型 '{cond.ThresholdType}' 不合法,仅支持 Fixed/Variable");
}
}
}
/// <summary>
/// 校验 RuleExpr(自由文本模式:括号匹配)
/// </summary>
private void ValidateRuleExpr(string ruleExpr)
{
if (ruleExpr.Length > 10000)
throw new ServiceException("公式表达式长度不能超过 10000 字符");
int parenDepth = 0;
foreach (char c in ruleExpr)
{
if (c == '(') parenDepth++;
else if (c == ')') parenDepth--;
if (parenDepth < 0)
throw new ServiceException("公式表达式括号不匹配:多余的右括号");
}
if (parenDepth != 0)
throw new ServiceException("公式表达式括号不匹配:缺少右括号");
}
/// <summary>
/// 校验触发时点
/// </summary>
private void ValidateTriggerPoints(string triggerPoints)
{
if (string.IsNullOrWhiteSpace(triggerPoints))
throw new ServiceException("触发时点不能为空");
var points = triggerPoints.Split(',');
foreach (var point in points)
{
var trimmed = point.Trim();
if (!ValidTriggerPoints.Contains(trimmed))
throw new ServiceException($"触发时点 '{trimmed}' 不合法");
}
if (points.All(p => string.IsNullOrWhiteSpace(p)))
throw new ServiceException("至少需要指定一个触发时点");
}
/// <summary>
/// 校验 Scope 适用范围字段
/// </summary>
private void ValidateScopeFields(CreateRiskApplicationReq req)
{
var hasAnyScope =
!string.IsNullOrWhiteSpace(req.ScopeAssetBookIds) ||
!string.IsNullOrWhiteSpace(req.ScopeClientIds) ||
!string.IsNullOrWhiteSpace(req.ScopeUnderlyingTypes) ||
!string.IsNullOrWhiteSpace(req.ScopeTradeTypes);
if (req.ScopeIsGlobal == 1)
{
if (hasAnyScope)
{
throw new ServiceException("全局适用时,不应指定其他适用范围维度");
}
return;
}
if (!hasAnyScope)
{
throw new ServiceException("非全局应用至少需要指定一个适用范围");
}
if (!string.IsNullOrWhiteSpace(req.ScopeAssetBookIds))
{
ValidateIdList(req.ScopeAssetBookIds, "适用账户");
}
if (!string.IsNullOrWhiteSpace(req.ScopeClientIds))
{
ValidateIdList(req.ScopeClientIds, "适用对手方");
}
}
/// <summary>
/// 校验 ID 列表格式
/// </summary>
private void ValidateIdList(string idList, string fieldName)
{
var parts = idList.Split(',');
foreach (var part in parts)
{
if (!int.TryParse(part.Trim(), out _))
throw new ServiceException($"{fieldName}列表格式不合法,应为逗号分隔的数字 ID");
}
}
/// <summary>
/// 尝试刷新引擎缓存(不抛异常)
/// </summary>
private void TryRefreshCache()
{
try
{
RiskEngineService.GetInstance().RefreshCache();
}
catch (Exception ex)
{
_logger.Error($"缓存刷新失败:{ex.Message}", ex);
}
}
/// <summary>
/// 获取数据类型中文名
/// </summary>
private string GetDataTypeName(RiskVariableDataType dataType)
{
return dataType switch
{
RiskVariableDataType.Numeric => "数值",
RiskVariableDataType.Date => "日期",
RiskVariableDataType.Boolean => "布尔",
_ => "未知"
};
}
/// <summary>
/// 获取规则实体,不存在则抛异常
/// </summary>
private glms_risk_rule GetRuleOrThrow(long ruleId)
{
var rule = DbContext.glms_risk_rule.FirstOrDefault(r => r.id == ruleId && r.Status != RiskRuleStatus.Deleted);
if (rule == null)
throw new ServiceException("规则不存在或已删除");
return rule;
}
/// <summary>
/// 获取应用配置实体,不存在则抛异常
/// </summary>
private glms_risk_rule_application GetApplicationOrThrow(long applicationId)
{
var app = DbContext.glms_risk_rule_application.FirstOrDefault(a => a.id == applicationId && a.Status != RiskRuleStatus.Deleted);
if (app == null)
throw new ServiceException("应用配置不存在或已删除");
return app;
}
/// <summary>
/// 获取变量实体,不存在则抛异常
/// </summary>
private glms_risk_variable GetVariableOrThrow(long variableId)
{
var variable = DbContext.glms_risk_variable.FirstOrDefault(v => v.id == variableId);
if (variable == null)
throw new ServiceException("变量不存在");
return variable;
}
/// <summary>
/// 校验规则参数(名称 + ConditionJson + RuleExpr
/// </summary>
private void ValidateRuleParams(string ruleName, string conditionJson, string ruleExpr)
{
if (string.IsNullOrWhiteSpace(ruleName))
throw new ServiceException("规则名称不能为空");
if (ruleName.Length > 200)
throw new ServiceException("规则名称长度不能超过200字符");
if (string.IsNullOrWhiteSpace(ruleExpr))
throw new ServiceException("规则表达式不能为空");
if (!string.IsNullOrWhiteSpace(conditionJson))
{
ValidateConditionJson(conditionJson);
}
ValidateRuleExpr(ruleExpr);
}
/// <summary>
/// 解析逗号分隔的规则 ID 字符串为 List
/// </summary>
private List<long> ParseRuleIds(string ruleIds)
{
if (string.IsNullOrWhiteSpace(ruleIds))
throw new ServiceException("关联规则 ID 不能为空");
var ids = new List<long>();
foreach (var part in ruleIds.Split(','))
{
if (long.TryParse(part.Trim(), out long id))
ids.Add(id);
else
throw new ServiceException($"规则 ID 列表格式不合法:'{part.Trim()}'");
}
if (ids.Count == 0)
throw new ServiceException("关联规则 ID 不能为空");
return ids;
}
/// <summary>
/// 规范化逗号分隔的规则 ID:去除空格和重复项,统一为无空格格式。
/// </summary>
private string NormalizeRuleIds(string ruleIds)
{
return string.Join(",", ParseRuleIds(ruleIds).Distinct());
}
/// <summary>
/// EF Core 可翻译的逗号分隔字段精确匹配表达式
/// </summary>
private static Expression<Func<glms_risk_rule_application, bool>> RuleIdsMatchExpr(long ruleId)
{
var idStr = ruleId.ToString();
return a => a.RuleIds != null &&
(a.RuleIds == idStr ||
a.RuleIds.StartsWith(idStr + ",") ||
a.RuleIds.EndsWith("," + idStr) ||
a.RuleIds.Contains("," + idStr + ","));
}
/// <summary>
/// 校验规则状态可用性并确保编译缓存就绪。
/// 返回不可用规则信息列表,列表为空表示全部通过。
/// 规则不可用的情况:已删除、已停用、编译缓存缺失且刷新失败。
/// </summary>
private List<string> ValidateAndEnsureRuleCache(string ruleIds)
{
var errors = new List<string>();
var ids = ParseRuleIds(ruleIds);
var rules = DbContext.glms_risk_rule
.Where(r => ids.Contains(r.id) && r.Status != RiskRuleStatus.Deleted)
.ToList();
if (rules.Count != ids.Count)
{
var missingIds = ids.Except(rules.Select(r => (long)r.id)).ToList();
foreach (var id in missingIds)
errors.Add($"规则ID {id} 不存在或已删除");
return errors;
}
foreach (var rule in rules)
{
if (rule.Status == RiskRuleStatus.Disabled)
{
errors.Add($"规则 '{rule.RuleName}' 已停用");
continue;
}
// 检查编译缓存,未命中则刷新
if (!RuleCompiledCache.TryGet(rule.id.ToString(), out _))
{
var result = RiskEngineService.GetInstance().RefreshOneRuleCache(rule.id);
if (!result.Success)
errors.Add($"规则 '{rule.RuleName}' 编译失败:{result.ErrorMessage}");
}
}
return errors;
}
/// <summary>
/// 根据规则 ID 字符串解析为规则名称字符串
/// </summary>
private string ResolveRuleNames(string ruleIds)
{
if (string.IsNullOrWhiteSpace(ruleIds))
return "";
var ids = ruleIds.Split(',')
.Select(s => long.TryParse(s.Trim(), out long id) ? id : (long?)null)
.Where(id => id.HasValue)
.Select(id => id.Value)
.ToList();
if (ids.Count == 0)
return "";
var names = DbContext.glms_risk_rule
.Where(r => ids.Contains(r.id) && r.Status != RiskRuleStatus.Deleted)
.Select(r => r.RuleName)
.ToList();
return string.Join(", ", names);
}
/// <summary>
/// 构建 Scope 字段的 OR 过滤谓词(逗号分隔值精确匹配)
/// </summary>
private static IQueryable<glms_risk_rule_application> ApplyScopeFilter(
IQueryable<glms_risk_rule_application> query,
Expression<Func<glms_risk_rule_application, string>> fieldSelector,
string csvValues)
{
var ids = csvValues.Split(',', StringSplitOptions.RemoveEmptyEntries);
if (ids.Length == 0)
return query;
Expression<Func<glms_risk_rule_application, bool>> predicate = null;
var param = fieldSelector.Parameters[0];
var member = fieldSelector.Body;
foreach (var trimmed in ids.Select(id => id.Trim()))
{
var eq = Expression.Equal(member, Expression.Constant(trimmed));
var startsExpr = Expression.Call(member, nameof(string.StartsWith), null,
Expression.Constant(trimmed + ","));
var endsExpr = Expression.Call(member, nameof(string.EndsWith), null,
Expression.Constant("," + trimmed));
var containsExpr = Expression.Call(member, nameof(string.Contains), null,
Expression.Constant("," + trimmed + ","));
var orExpr = Expression.OrElse(Expression.OrElse(eq, startsExpr),
Expression.OrElse(endsExpr, containsExpr));
var lambda = Expression.Lambda<Func<glms_risk_rule_application, bool>>(orExpr, param);
predicate = predicate == null ? lambda : predicate.Or(lambda);
}
return predicate != null ? query.Where(predicate) : query;
}
/// <summary>
/// 获取控制策略中文名
/// </summary>
private static string GetStrategyName(RiskControlStrategy strategy)
{
return strategy switch
{
RiskControlStrategy.Block => "阻断",
RiskControlStrategy.Approval => "审批",
RiskControlStrategy.ShowTip => "提醒",
_ => strategy.ToString()
};
}
private static readonly Dictionary<string, string> TriggerPointCnMap = new Dictionary<string, string>
{
["BOOK_CONFIRM"] = "交易录入确认",
["CLOSE_REVIEW"] = "平仓审核",
["UPLOAD_CONFIRMATION"] = "上传确认书",
["EVENT_TRIGGER"] = "事件触发",
["FUND_PAYMENT"] = "资金支付"
};
/// <summary>
/// 将触发时点映射为中文
/// </summary>
private static string MapTriggerPointsToChinese(string triggerPoints)
{
if (string.IsNullOrWhiteSpace(triggerPoints))
return "";
var parts = triggerPoints.Split(',');
var translated = parts.Select(p => TriggerPointCnMap.TryGetValue(p.Trim(), out var cn) ? cn : p.Trim());
return string.Join(", ", translated);
}
#endregion
#region Rule Management
/// <summary>
/// 查询规则列表(分页,含 ApplicationCount 子查询)
/// </summary>
public SearchListResult<RiskRuleListItem> QueryRuleList(QueryRiskRuleReq req)
{
var query = DbContext.glms_risk_rule
.Where(r => r.Status != RiskRuleStatus.Deleted);
if (!string.IsNullOrWhiteSpace(req.Keyword))
{
query = query.Where(r => r.RuleName.Contains(req.Keyword)
|| r.RuleText.Contains(req.Keyword));
}
if (req.Status.HasValue)
{
query = query.Where(r => r.Status == req.Status.Value);
}
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 result = query.OrderByDescending(r => r.UpdateDate)
.Select(r => new RiskRuleListItem
{
Id = r.id,
RuleName = r.RuleName,
RuleText = r.RuleText,
ConditionJson = r.ConditionJson,
RuleExpr = r.RuleExpr,
Description = r.Description,
Status = r.Status,
Version = r.Version,
OptName = r.OptName,
OptDate = r.OptDate.GetValueOrDefault(),
UpdateOptName = r.UpdateOptName,
UpdateDate = r.UpdateDate ?? r.OptDate.GetValueOrDefault(),
ApplicationCount = DbContext.glms_risk_rule_application
.Count(a => a.RuleIds != null &&
(a.RuleIds == r.id.ToString() ||
a.RuleIds.StartsWith(r.id.ToString() + ",") ||
a.RuleIds.EndsWith("," + r.id.ToString()) ||
a.RuleIds.Contains("," + r.id.ToString() + ",")))
});
return result.ToSearchList(req);
}
/// <summary>
/// 获取规则详情(含关联应用摘要)
/// </summary>
public RiskRuleDetail GetRuleDetail(long ruleId)
{
var rule = GetRuleOrThrow(ruleId);
var applications = DbContext.glms_risk_rule_application
.Where(RuleIdsMatchExpr(ruleId))
.Select(a => new RiskRuleApplicationSummary
{
Id = a.id,
Status = a.Status,
ControlStrategy = a.ControlStrategy,
TriggerPoints = a.TriggerPoints
})
.ToList();
return new RiskRuleDetail
{
Id = rule.id,
RuleName = rule.RuleName,
RuleText = rule.RuleText,
ConditionJson = rule.ConditionJson,
RuleExpr = rule.RuleExpr,
Description = rule.Description,
Status = rule.Status,
Version = rule.Version,
OptName = rule.OptName,
OptDate = rule.OptDate.GetValueOrDefault(),
UpdateOptName = rule.UpdateOptName,
UpdateDate = rule.UpdateDate ?? rule.OptDate.GetValueOrDefault(),
Applications = applications
};
}
/// <summary>
/// 创建规则(ConditionJson 完整校验,事务包裹)
/// </summary>
public RiskRuleDetail CreateRule(CreateRiskRuleReq req)
{
ValidateRuleParams(req.RuleName, req.ConditionJson, req.RuleExpr);
var compileResult = RuleCompiler.ValidateAndCompileFormula(null, req.RuleExpr);
if (!compileResult.Success)
throw new ServiceException($"规则表达式编译失败:{compileResult.ErrorMessage}");
var entity = new glms_risk_rule
{
RuleName = req.RuleName,
RuleText = req.RuleText,
ConditionJson = req.ConditionJson,
RuleExpr = req.RuleExpr,
Description = req.Description,
Status = RiskRuleStatus.Active,
Version = 1
};
SetDBModelOpt(entity);
entity.UpdateOptId = UserId;
entity.UpdateOptName = UserName;
entity.UpdateDate = DateTime.Now;
using (var transaction = DbContext.Database.BeginTransaction())
{
try
{
DbContext.glms_risk_rule.Add(entity);
DbContext.SaveChanges();
WriteAuditLog("RULE_CREATE", "RULE", entity.id, req.RuleName, "创建规则",
snapshotData: JsonConvert.SerializeObject(new { Version = entity.Version }));
DbContext.SaveChanges();
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
RiskEngineService.GetInstance().RefreshOneRuleCache(entity.id);
return GetRuleDetail(entity.id);
}
/// <summary>
/// 更新规则(乐观锁校验,Version+1)
/// </summary>
public RiskRuleDetail UpdateRule(long ruleId, UpdateRiskRuleReq req)
{
var rule = GetRuleOrThrow(ruleId);
if (rule.Version != req.Version)
throw new ServiceException("规则已被其他用户修改,请重新加载后再编辑");
ValidateRuleParams(req.RuleName, req.ConditionJson, req.RuleExpr);
var compileResult = RuleCompiler.ValidateAndCompileFormula(ruleId, req.RuleExpr);
if (!compileResult.Success)
throw new ServiceException($"规则表达式编译失败:{compileResult.ErrorMessage}");
rule.RuleName = req.RuleName;
rule.RuleText = req.RuleText;
rule.ConditionJson = req.ConditionJson;
rule.RuleExpr = req.RuleExpr;
rule.Description = req.Description;
rule.Version = rule.Version + 1;
rule.UpdateOptId = UserId;
rule.UpdateOptName = UserName;
rule.UpdateDate = DateTime.Now;
WriteAuditLog("RULE_UPDATE", "RULE", ruleId, req.RuleName, "修改规则");
DbContext.SaveChanges();
RiskEngineService.GetInstance().RefreshOneRuleCache(ruleId);
return GetRuleDetail(ruleId);
}
/// <summary>
/// 删除规则(软删除,有 Active 应用引用时拒绝)
/// </summary>
public void DeleteRule(long ruleId)
{
var rule = GetRuleOrThrow(ruleId);
var activeCount = DbContext.glms_risk_rule_application
.Where(a => a.Status == RiskRuleStatus.Active)
.Count(RuleIdsMatchExpr(ruleId));
if (activeCount > 0)
throw new ServiceException($"该规则仍有 {activeCount} 个生效中的应用配置,请先停用或删除相关应用");
rule.Status = RiskRuleStatus.Deleted;
rule.Version = rule.Version + 1;
rule.UpdateOptId = UserId;
rule.UpdateOptName = UserName;
rule.UpdateDate = DateTime.Now;
WriteAuditLog("RULE_DELETE", "RULE", ruleId, rule.RuleName, "删除规则");
DbContext.SaveChanges();
RiskEngineService.GetInstance().RefreshOneRuleCache(ruleId);
}
/// <summary>
/// 启用规则(仅 Disabled → Active
/// </summary>
public void EnableRule(long ruleId)
{
var rule = DbContext.glms_risk_rule.FirstOrDefault(r => r.id == ruleId && r.Status == RiskRuleStatus.Disabled);
if (rule == null)
throw new ServiceException("仅已停用的规则可以启用");
var compileResult = RuleCompiler.ValidateAndCompileFormula(ruleId, rule.RuleExpr);
if (!compileResult.Success)
throw new ServiceException($"规则表达式编译失败:{compileResult.ErrorMessage}");
rule.Status = RiskRuleStatus.Active;
rule.UpdateOptId = UserId;
rule.UpdateOptName = UserName;
rule.UpdateDate = DateTime.Now;
WriteAuditLog("RULE_ENABLE", "RULE", ruleId, rule.RuleName, "启用规则");
DbContext.SaveChanges();
RiskEngineService.GetInstance().RefreshOneRuleCache(ruleId);
}
/// <summary>
/// 停用规则(仅 Active → Disabled
/// </summary>
public DisableRuleResult DisableRule(long ruleId)
{
var rule = DbContext.glms_risk_rule.FirstOrDefault(r => r.id == ruleId && r.Status == RiskRuleStatus.Active);
if (rule == null)
throw new ServiceException("仅已启用的规则可以停用");
rule.Status = RiskRuleStatus.Disabled;
rule.UpdateOptId = UserId;
rule.UpdateOptName = UserName;
rule.UpdateDate = DateTime.Now;
WriteAuditLog("RULE_DISABLE", "RULE", ruleId, rule.RuleName, "停用规则");
// 级联停用关联的 Active 应用配置
var activeApps = DbContext.glms_risk_rule_application
.Where(a => a.Status == RiskRuleStatus.Active)
.Where(RuleIdsMatchExpr(ruleId))
.ToList();
var cascadedIds = new List<long>();
foreach (var app in activeApps)
{
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:{ruleId})停用,应用配置被级联停用");
}
if (cascadedIds.Any())
{
_logger.Info($"[停用规则] 规则ID: {ruleId}, 级联停用应用: {string.Join(",", cascadedIds)}");
}
DbContext.SaveChanges();
RiskEngineService.GetInstance().RefreshOneRuleCache(ruleId);
if (cascadedIds.Any())
RiskEngineService.GetInstance().RefreshApplication();
return new DisableRuleResult { CascadedApplicationIds = cascadedIds };
}
/// <summary>
/// 获取规则版本历史(从审计日志 SnapshotData 提取)
/// </summary>
public List<RiskRuleVersionItem> GetRuleVersions(long ruleId)
{
GetRuleOrThrow(ruleId);
var logs = DbContext.glms_risk_rule_audit_log
.Where(l => l.TargetType == "RULE" && l.TargetId == ruleId)
.OrderByDescending(l => l.OptDate)
.ToList();
var result = new List<RiskRuleVersionItem>();
foreach (var l in logs)
{
int version = 0;
if (!string.IsNullOrEmpty(l.SnapshotData))
{
try
{
var snapshot = JsonConvert.DeserializeAnonymousType(l.SnapshotData, new { Version = 0 });
if (snapshot != null)
version = snapshot.Version;
}
catch { }
}
result.Add(new RiskRuleVersionItem
{
Version = version,
OperationType = l.OperationType,
OperationDetail = l.OperationDetail,
OptName = l.OptName,
OptDate = l.OptDate
});
}
return result;
}
/// <summary>
/// 批量删除规则(逐条保护检查)
/// </summary>
public BatchOperationResult BatchDeleteRules(List<long> 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)
.ToList();
_logger.Info($"[批量删除] DB查询完成 - 匹配到 {rules.Count} 条规则");
var foundIds = rules.Select(r => (long)r.id).ToList();
var missingIds = ruleIds.Except(foundIds).ToList();
if (missingIds.Any())
{
return new BatchOperationResult
{
Success = false,
TotalCount = ruleIds.Count,
SuccessCount = 0,
ErrorMessage = $"以下规则不存在或已删除:{string.Join(", ", missingIds)}"
};
}
var blockedRules = new List<string>();
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
{
Success = false,
TotalCount = ruleIds.Count,
SuccessCount = 0,
ErrorMessage = string.Join("", blockedRules)
};
}
foreach (var rule in rules)
{
_logger.Info($"[批量删除] 写入状态 - 规则ID: {rule.id}");
rule.Status = RiskRuleStatus.Deleted;
rule.Version = rule.Version + 1;
rule.UpdateOptId = UserId;
rule.UpdateOptName = UserName;
rule.UpdateDate = DateTime.Now;
WriteAuditLog("RULE_BATCH_DELETE", "RULE", rule.id, rule.RuleName, "批量删除规则");
}
_logger.Info("[批量删除] 保存数据库...");
DbContext.SaveChanges();
_logger.Info("[批量删除] 刷新规则缓存...");
foreach (var ruleId in ruleIds)
{
RiskEngineService.GetInstance().RefreshOneRuleCache(ruleId);
}
_logger.Info("[批量删除] 完成");
return new BatchOperationResult
{
Success = true,
TotalCount = ruleIds.Count,
SuccessCount = ruleIds.Count
};
}
/// <summary>
/// 批量启用规则(仅 Disabled → Active,逐条编译校验)
/// </summary>
public BatchOperationResult BatchEnableRules(List<long> 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)
.ToList();
_logger.Info($"[批量启用规则] DB查询完成 - 匹配到 {rules.Count} 条规则");
var foundIds = rules.Select(r => (long)r.id).ToList();
var missingIds = ruleIds.Except(foundIds).ToList();
if (missingIds.Any())
{
return new BatchOperationResult
{
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
{
Success = true,
TotalCount = ruleIds.Count,
SuccessCount = ruleIds.Count
};
}
// 逐条编译校验,任一失败则整体拒绝
var errors = new List<string>();
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)}"
};
}
foreach (var rule in rules)
{
_logger.Info($"[批量启用规则] 写入状态 - 规则ID: {rule.id}");
rule.Status = RiskRuleStatus.Active;
rule.UpdateOptId = UserId;
rule.UpdateOptName = UserName;
rule.UpdateDate = DateTime.Now;
}
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)
{
RiskEngineService.GetInstance().RefreshOneRuleCache(rule.id);
}
_logger.Info("[批量启用规则] 完成");
return new BatchOperationResult
{
Success = true,
TotalCount = ruleIds.Count,
SuccessCount = ruleIds.Count
};
}
/// <summary>
/// 批量停用规则(仅 Active → Disabled,级联停用关联应用)
/// </summary>
public BatchDisableRulesResult BatchDisableRules(List<long> 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)
.ToList();
_logger.Info($"[批量停用规则] DB查询完成 - 匹配到 {rules.Count} 条规则");
var foundIds = rules.Select(r => (long)r.id).ToList();
var missingIds = ruleIds.Except(foundIds).ToList();
if (missingIds.Any())
{
return new BatchDisableRulesResult
{
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
{
Success = true,
TotalCount = ruleIds.Count,
SuccessCount = ruleIds.Count
};
}
var allCascadedAppIds = new List<long>();
foreach (var rule in rules)
{
_logger.Info($"[批量停用规则] 写入状态 - 规则ID: {rule.id}");
rule.Status = RiskRuleStatus.Disabled;
rule.UpdateOptId = UserId;
rule.UpdateOptName = UserName;
rule.UpdateDate = DateTime.Now;
// 级联停用关联的 Active 应用配置
var activeApps = DbContext.glms_risk_rule_application
.Where(a => a.Status == RiskRuleStatus.Active)
.Where(RuleIdsMatchExpr(rule.id))
.ToList();
var cascadedIds = new List<long>();
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})停用,应用配置被级联停用");
}
if (cascadedIds.Any())
{
_logger.Info($"[批量停用规则] 规则ID: {rule.id}, 级联停用应用: {string.Join(",", cascadedIds)}");
allCascadedAppIds.AddRange(cascadedIds);
}
}
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
};
}
/// <summary>
/// 获取规则关联的应用配置列表
/// </summary>
public List<RiskApplicationListItem> GetRuleApplications(long ruleId)
{
GetRuleOrThrow(ruleId);
var apps = DbContext.glms_risk_rule_application
.Where(a => a.Status != RiskRuleStatus.Deleted)
.Where(RuleIdsMatchExpr(ruleId))
.OrderByDescending(a => a.UpdateDate)
.ToList();
var result = new List<RiskApplicationListItem>();
foreach (var app in apps)
{
result.Add(new RiskApplicationListItem
{
Id = app.id,
RuleIds = app.RuleIds,
Status = (int)app.Status,
ControlStrategy = app.ControlStrategy,
TriggerPoints = app.TriggerPoints,
ScopeAssetBookIds = app.ScopeAssetBookIds,
ScopeClientIds = app.ScopeClientIds,
ScopeUnderlyingTypes = app.ScopeUnderlyingTypes,
ScopeTradeTypes = app.ScopeTradeTypes,
ScopeIsGlobal = app.ScopeIsGlobal,
Version = app.Version,
OptName = app.OptName,
OptDate = app.OptDate.GetValueOrDefault(),
UpdateOptName = app.UpdateOptName,
UpdateDate = app.UpdateDate ?? app.OptDate.GetValueOrDefault()
});
}
return result;
}
/// <summary>
/// 获取所有未删除规则列表(轻量字段,供下拉选择)
/// </summary>
public List<RiskRuleSimpleItem> GetAllRuleList()
{
return DbContext.glms_risk_rule
.Where(r => r.Status != RiskRuleStatus.Deleted)
.OrderBy(r => r.id)
.Select(r => new RiskRuleSimpleItem
{
Id = r.id,
RuleName = r.RuleName,
RuleText = r.RuleText,
Status = r.Status
})
.ToList();
}
#endregion
#region Application Management
/// <summary>
/// 查询应用配置列表(多条件筛选,内存关联规则名称)
/// </summary>
public SearchListResult<RiskApplicationListItem> QueryApplicationList(QueryRiskApplicationReq req)
{
var query = DbContext.glms_risk_rule_application
.Where(a => a.Status != RiskRuleStatus.Deleted);
if (req.Status.HasValue)
{
query = query.Where(a => a.Status == req.Status.Value);
}
if (req.Strategy.HasValue)
{
query = query.Where(a => a.ControlStrategy == req.Strategy.Value);
}
if (!string.IsNullOrWhiteSpace(req.TriggerPoint))
{
query = query.Where(a => a.TriggerPoints.Contains(req.TriggerPoint));
}
if (!string.IsNullOrWhiteSpace(req.RuleName))
{
query = query.Where(a => DbContext.glms_risk_rule
.Any(r => r.Status != RiskRuleStatus.Deleted
&& r.RuleName.Contains(req.RuleName)
&& a.RuleIds != null
&& (a.RuleIds == r.id.ToString()
|| a.RuleIds.StartsWith(r.id.ToString() + ",")
|| a.RuleIds.EndsWith("," + r.id.ToString())
|| a.RuleIds.Contains("," + r.id.ToString() + ","))));
}
if (!string.IsNullOrWhiteSpace(req.ScopeClientIds))
query = ApplyScopeFilter(query, a => a.ScopeClientIds, req.ScopeClientIds);
if (!string.IsNullOrWhiteSpace(req.ScopeUnderlyingTypes))
query = ApplyScopeFilter(query, a => a.ScopeUnderlyingTypes, req.ScopeUnderlyingTypes);
if (!string.IsNullOrWhiteSpace(req.ScopeTradeTypes))
query = ApplyScopeFilter(query, a => a.ScopeTradeTypes, req.ScopeTradeTypes);
if (!string.IsNullOrWhiteSpace(req.ScopeAssetBookIds))
query = ApplyScopeFilter(query, a => a.ScopeAssetBookIds, req.ScopeAssetBookIds);
var pagedResult = query.OrderByDescending(a => a.UpdateDate)
.Select(a => new RiskApplicationListItem
{
Id = a.id,
RuleIds = a.RuleIds,
Description = a.Description,
Status = (int)a.Status,
ControlStrategy = a.ControlStrategy,
TriggerPoints = a.TriggerPoints,
ScopeAssetBookIds = a.ScopeAssetBookIds,
ScopeClientIds = a.ScopeClientIds,
ScopeUnderlyingTypes = a.ScopeUnderlyingTypes,
ScopeTradeTypes = a.ScopeTradeTypes,
ScopeIsGlobal = a.ScopeIsGlobal,
Version = a.Version,
OptName = a.OptName,
OptDate = a.OptDate.GetValueOrDefault(),
UpdateOptName = a.UpdateOptName,
UpdateDate = a.UpdateDate ?? a.OptDate.GetValueOrDefault()
})
.ToSearchList(req);
return pagedResult;
}
/// <summary>
/// 获取应用配置详情(含关联规则信息)
/// </summary>
public RiskApplicationDetail GetApplicationDetail(long applicationId)
{
var app = DbContext.glms_risk_rule_application
.FirstOrDefault(a => a.id == applicationId && a.Status != RiskRuleStatus.Deleted);
if (app == null)
throw new ServiceException("应用配置不存在或已删除");
return new RiskApplicationDetail
{
Id = app.id,
RuleIds = app.RuleIds,
Description = app.Description,
Status = app.Status,
ControlStrategy = app.ControlStrategy,
TriggerPoints = app.TriggerPoints,
ScopeAssetBookIds = app.ScopeAssetBookIds,
ScopeClientIds = app.ScopeClientIds,
ScopeUnderlyingTypes = app.ScopeUnderlyingTypes,
ScopeTradeTypes = app.ScopeTradeTypes,
ScopeIsGlobal = app.ScopeIsGlobal,
Version = app.Version,
OptName = app.OptName,
OptDate = app.OptDate.GetValueOrDefault(),
UpdateOptName = app.UpdateOptName,
UpdateDate = app.UpdateDate ?? app.OptDate.GetValueOrDefault()
};
}
/// <summary>
/// 创建应用配置(关联规则 Active 校验,事务包裹)
/// </summary>
public RiskApplicationDetail CreateApplication(CreateRiskApplicationReq req)
{
var normalizedRuleIds = NormalizeRuleIds(req.RuleIds);
var errors = ValidateAndEnsureRuleCache(normalizedRuleIds);
if (errors.Any())
throw new ServiceException($"以下关联规则不可用:{string.Join("", errors)}");
ValidateTriggerPoints(req.TriggerPoints);
ValidateScopeFields(req);
var entity = new glms_risk_rule_application
{
RuleIds = normalizedRuleIds,
Description = req.Description,
Status = RiskRuleStatus.Active,
ControlStrategy = req.ControlStrategy,
TriggerPoints = req.TriggerPoints,
ScopeAssetBookIds = req.ScopeAssetBookIds,
ScopeClientIds = req.ScopeClientIds,
ScopeUnderlyingTypes = req.ScopeUnderlyingTypes,
ScopeTradeTypes = req.ScopeTradeTypes,
ScopeIsGlobal = req.ScopeIsGlobal,
Version = 1
};
SetDBModelOpt(entity);
entity.UpdateOptId = UserId;
entity.UpdateOptName = UserName;
entity.UpdateDate = DateTime.Now;
using (var transaction = DbContext.Database.BeginTransaction())
{
try
{
DbContext.glms_risk_rule_application.Add(entity);
DbContext.SaveChanges();
WriteAuditLog("APP_CREATE", "APPLICATION", entity.id, ResolveRuleNames(entity.RuleIds),
$"创建应用配置:策略={GetStrategyName(req.ControlStrategy)}, 触发时点={MapTriggerPointsToChinese(req.TriggerPoints)}",
snapshotData: JsonConvert.SerializeObject(new { Version = entity.Version }));
DbContext.SaveChanges();
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
RiskEngineService.GetInstance().RefreshApplication();
return GetApplicationDetail(entity.id);
}
/// <summary>
/// 更新应用配置(乐观锁校验)
/// </summary>
public RiskApplicationDetail UpdateApplication(long applicationId, UpdateRiskApplicationReq req)
{
var app = GetApplicationOrThrow(applicationId);
if (app.Version != req.Version)
throw new ServiceException("应用配置已被其他用户修改,请重新加载后再编辑");
string normalizedRuleIds = null;
if (!string.IsNullOrWhiteSpace(req.RuleIds))
{
normalizedRuleIds = NormalizeRuleIds(req.RuleIds);
var errors = ValidateAndEnsureRuleCache(normalizedRuleIds);
if (errors.Any())
throw new ServiceException($"以下关联规则不可用:{string.Join("", errors)}");
}
ValidateTriggerPoints(req.TriggerPoints);
var scopeReq = new CreateRiskApplicationReq
{
ScopeAssetBookIds = req.ScopeAssetBookIds,
ScopeClientIds = req.ScopeClientIds,
ScopeUnderlyingTypes = req.ScopeUnderlyingTypes,
ScopeTradeTypes = req.ScopeTradeTypes,
ScopeIsGlobal = req.ScopeIsGlobal
};
ValidateScopeFields(scopeReq);
if (normalizedRuleIds != null)
app.RuleIds = normalizedRuleIds;
app.Description = req.Description;
app.ControlStrategy = req.ControlStrategy;
app.TriggerPoints = req.TriggerPoints;
app.ScopeAssetBookIds = req.ScopeAssetBookIds;
app.ScopeClientIds = req.ScopeClientIds;
app.ScopeUnderlyingTypes = req.ScopeUnderlyingTypes;
app.ScopeTradeTypes = req.ScopeTradeTypes;
app.ScopeIsGlobal = req.ScopeIsGlobal;
app.Version = app.Version + 1;
app.UpdateOptId = UserId;
app.UpdateOptName = UserName;
app.UpdateDate = DateTime.Now;
WriteAuditLog("APP_UPDATE", "APPLICATION", applicationId, ResolveRuleNames(app.RuleIds),
$"修改应用配置:策略={GetStrategyName(req.ControlStrategy)}, 触发时点={MapTriggerPointsToChinese(req.TriggerPoints)}");
DbContext.SaveChanges();
RiskEngineService.GetInstance().RefreshApplication();
return GetApplicationDetail(applicationId);
}
/// <summary>
/// 删除应用配置(软删除)
/// </summary>
public void DeleteApplication(long applicationId)
{
var app = GetApplicationOrThrow(applicationId);
app.Status = RiskRuleStatus.Deleted;
app.Version = app.Version + 1;
app.UpdateOptId = UserId;
app.UpdateOptName = UserName;
app.UpdateDate = DateTime.Now;
WriteAuditLog("APP_DELETE", "APPLICATION", applicationId, ResolveRuleNames(app.RuleIds), "删除应用配置");
DbContext.SaveChanges();
RiskEngineService.GetInstance().RefreshApplication();
}
/// <summary>
/// 批量删除应用配置(软删除)
/// </summary>
public BatchOperationResult BatchDeleteApplications(List<long> applicationIds)
{
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())
{
return new BatchOperationResult
{
Success = false,
TotalCount = applicationIds.Count,
SuccessCount = 0,
ErrorMessage = $"以下应用配置不存在或已删除:{string.Join(", ", missingIds)}"
};
}
foreach (var app in apps)
{
_logger.Info($"[批量删除应用] 写入状态 - 应用ID: {app.id}");
app.Status = RiskRuleStatus.Deleted;
app.Version = app.Version + 1;
app.UpdateOptId = UserId;
app.UpdateOptName = UserName;
app.UpdateDate = DateTime.Now;
}
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
{
Success = true,
TotalCount = applicationIds.Count,
SuccessCount = applicationIds.Count
};
}
/// <summary>
/// 启用应用配置(校验关联规则状态)
/// </summary>
public void EnableApplication(long applicationId)
{
var app = DbContext.glms_risk_rule_application.FirstOrDefault(a => a.id == applicationId && a.Status == RiskRuleStatus.Disabled);
if (app == null)
throw new ServiceException("仅已停用的应用配置可以启用");
var errors = ValidateAndEnsureRuleCache(app.RuleIds);
if (errors.Any())
throw new ServiceException($"以下关联规则不可用:{string.Join("", errors)}");
app.Status = RiskRuleStatus.Active;
app.UpdateOptId = UserId;
app.UpdateOptName = UserName;
app.UpdateDate = DateTime.Now;
WriteAuditLog("APP_ENABLE", "APPLICATION", applicationId, ResolveRuleNames(app.RuleIds), "启用应用配置");
DbContext.SaveChanges();
RiskEngineService.GetInstance().RefreshApplication();
}
/// <summary>
/// 停用应用配置
/// </summary>
public void DisableApplication(long applicationId)
{
var app = DbContext.glms_risk_rule_application.FirstOrDefault(a => a.id == applicationId && a.Status == RiskRuleStatus.Active);
if (app == null)
throw new ServiceException("仅已启用的应用配置可以停用");
app.Status = RiskRuleStatus.Disabled;
app.UpdateOptId = UserId;
app.UpdateOptName = UserName;
app.UpdateDate = DateTime.Now;
WriteAuditLog("APP_DISABLE", "APPLICATION", applicationId, ResolveRuleNames(app.RuleIds), "停用应用配置");
DbContext.SaveChanges();
RiskEngineService.GetInstance().RefreshApplication();
}
/// <summary>
/// 批量启用应用配置(逐条校验关联规则)
/// </summary>
public BatchOperationResult BatchEnableApplications(List<long> applicationIds)
{
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())
{
return new BatchOperationResult
{
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
{
Success = true,
TotalCount = applicationIds.Count,
SuccessCount = applicationIds.Count
};
}
var allErrors = new List<string>();
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)
};
}
foreach (var app in apps)
{
_logger.Info($"[批量启用] 写入状态 - 应用ID: {app.id}");
app.Status = RiskRuleStatus.Active;
app.UpdateOptId = UserId;
app.UpdateOptName = UserName;
app.UpdateDate = DateTime.Now;
WriteAuditLog("APP_BATCH_ENABLE", "APPLICATION", app.id, ResolveRuleNames(app.RuleIds), "批量启用应用配置");
}
_logger.Info("[批量启用] 保存数据库...");
DbContext.SaveChanges();
_logger.Info("[批量启用] 刷新应用缓存...");
RiskEngineService.GetInstance().RefreshApplication();
_logger.Info("[批量启用] 完成");
return new BatchOperationResult
{
Success = true,
TotalCount = applicationIds.Count,
SuccessCount = applicationIds.Count
};
}
/// <summary>
/// 批量停用应用配置
/// </summary>
public BatchOperationResult BatchDisableApplications(List<long> applicationIds)
{
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())
{
return new BatchOperationResult
{
Success = false,
TotalCount = applicationIds.Count,
SuccessCount = apps.Count,
ErrorMessage = $"以下应用配置不存在或当前状态不允许停用:{string.Join(", ", missingIds)}"
};
}
foreach (var app in apps)
{
_logger.Info($"[批量停用] 写入状态 - 应用ID: {app.id}");
app.Status = RiskRuleStatus.Disabled;
app.UpdateOptId = UserId;
app.UpdateOptName = UserName;
app.UpdateDate = DateTime.Now;
WriteAuditLog("APP_BATCH_DISABLE", "APPLICATION", app.id, ResolveRuleNames(app.RuleIds), "批量停用应用配置");
}
_logger.Info("[批量停用] 保存数据库...");
DbContext.SaveChanges();
_logger.Info("[批量停用] 刷新应用缓存...");
RiskEngineService.GetInstance().RefreshApplication();
_logger.Info("[批量停用] 完成");
return new BatchOperationResult
{
Success = true,
TotalCount = applicationIds.Count,
SuccessCount = applicationIds.Count
};
}
#endregion
/// <summary>
/// 查询 ConditionJson 中引用了指定变量 ID 的规则(仅结构化模式)
/// 精确匹配 "VariableId":{id} 或 "ThresholdVariableId":{id}\b 防止 id=1 误匹配 id=10
/// </summary>
private List<glms_risk_rule> GetRulesByVariableId(long variableId)
{
var allRules = DbContext.glms_risk_rule
.Where(r => r.Status != RiskRuleStatus.Deleted && r.ConditionJson != null)
.ToList();
if (allRules.Count == 0) return allRules;
var idPattern = $@"""VariableId""\s*:\s*{variableId}\b";
var thresholdPattern = $@"""ThresholdVariableId""\s*:\s*{variableId}\b";
return allRules.Where(r =>
Regex.IsMatch(r.ConditionJson, idPattern) ||
Regex.IsMatch(r.ConditionJson, thresholdPattern)
).ToList();
}
#region Variable Management
/// <summary>
/// 查询变量列表(全量返回,Category/Keyword 筛选)
/// </summary>
public SearchListResult<RiskVariableListItem> QueryVariableList(QueryRiskVariableReq req)
{
var query = DbContext.glms_risk_variable.AsQueryable();
if (req.Category.HasValue)
{
query = query.Where(v => v.Category == req.Category.Value);
}
if (req.DataType.HasValue)
{
query = query.Where(v => v.DataType == req.DataType.Value);
}
if (!string.IsNullOrWhiteSpace(req.VariableName))
{
query = query.Where(v => v.VariableName.Contains(req.VariableName));
}
var result = query.OrderBy(v => v.SortOrder).ThenBy(v => v.VariableName)
.Select(v => new RiskVariableListItem
{
Id = v.id,
VariableName = v.VariableName,
Category = v.Category,
DataType = v.DataType,
Unit = v.Unit,
ValueDomain = v.ValueDomain,
Description = v.Description,
VariableExpr = v.VariableExpr,
Version = v.Version,
SortOrder = v.SortOrder
});
return result.ToSearchList(req);
}
/// <summary>
/// 获取变量详情(含 ReferenceCount 引用统计)
/// </summary>
public RiskVariableDetail GetVariableDetail(long variableId)
{
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))));
return new RiskVariableDetail
{
Id = variable.id,
VariableName = variable.VariableName,
Category = variable.Category,
DataType = variable.DataType,
Unit = variable.Unit,
ValueDomain = variable.ValueDomain,
Description = variable.Description,
VariableExpr = variable.VariableExpr,
Version = variable.Version,
SortOrder = variable.SortOrder,
OptName = variable.OptName,
OptDate = variable.OptDate.GetValueOrDefault(),
UpdateOptName = variable.UpdateOptName,
UpdateDate = variable.UpdateDate ?? variable.OptDate.GetValueOrDefault(),
ReferenceCount = referenceCount
};
}
/// <summary>
/// 创建变量(VariableExpr 长度校验 ≤10000,事务包裹)
/// </summary>
public RiskVariableDetail CreateVariable(CreateRiskVariableReq req)
{
if (string.IsNullOrWhiteSpace(req.VariableName))
throw new ServiceException("变量名称不能为空");
if (string.IsNullOrWhiteSpace(req.VariableExpr))
throw new ServiceException("取值表达式不能为空");
if (req.VariableExpr.Length > 10000)
throw new ServiceException("取值表达式长度不能超过 10000 字符");
var entity = new glms_risk_variable
{
VariableName = req.VariableName,
Category = req.Category,
DataType = req.DataType,
Unit = req.Unit,
ValueDomain = req.ValueDomain,
Description = req.Description,
VariableExpr = req.VariableExpr,
SortOrder = req.SortOrder,
Version = 1
};
SetDBModelOpt(entity);
entity.UpdateOptId = UserId;
entity.UpdateOptName = UserName;
entity.UpdateDate = DateTime.Now;
using (var transaction = DbContext.Database.BeginTransaction())
{
try
{
DbContext.glms_risk_variable.Add(entity);
DbContext.SaveChanges();
WriteAuditLog("VARIABLE_CREATE", "VARIABLE", entity.id, req.VariableName, $"创建变量:{req.VariableName}",
snapshotData: JsonConvert.SerializeObject(new { Version = entity.Version }));
DbContext.SaveChanges();
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
InvalidateVariableCache();
return GetVariableDetail(entity.id);
}
/// <summary>
/// 更新变量(乐观锁,DataType 变更保护,级联更新引用规则 RuleExpr)
/// </summary>
public RiskVariableDetail UpdateVariable(long variableId, UpdateRiskVariableReq req)
{
var variable = GetVariableOrThrow(variableId);
if (variable.Version != req.Version)
throw new ServiceException("变量已被其他用户修改,请重新加载后再编辑");
if (string.IsNullOrWhiteSpace(req.VariableName))
throw new ServiceException("变量名称不能为空");
if (string.IsNullOrWhiteSpace(req.VariableExpr))
throw new ServiceException("取值表达式不能为空");
if (req.DataType != variable.DataType)
{
var activeRefCount = GetRulesByVariableId(variableId)
.Count(r => r.Status == RiskRuleStatus.Active);
if (activeRefCount > 0)
throw new ServiceException($"该变量被 {activeRefCount} 个生效中的规则引用,无法修改数据类型");
}
var oldVariableExpr = variable.VariableExpr;
variable.VariableName = req.VariableName;
variable.Category = req.Category;
variable.DataType = req.DataType;
variable.Unit = req.Unit;
variable.ValueDomain = req.ValueDomain;
variable.Description = req.Description;
variable.VariableExpr = req.VariableExpr;
variable.SortOrder = req.SortOrder;
variable.Version = variable.Version + 1;
variable.UpdateOptId = UserId;
variable.UpdateOptName = UserName;
variable.UpdateDate = DateTime.Now;
var variableExprChanged = oldVariableExpr != req.VariableExpr;
if (variableExprChanged)
{
// 结构化模式:通过 ConditionJson 中的 VariableId 找到受影响的规则
var affectedRules = GetRulesByVariableId(variableId)
.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}");
using (var transaction = DbContext.Database.BeginTransaction())
{
try
{
foreach (var rule in affectedRules)
{
rule.RuleExpr = rule.RuleExpr.Replace(oldVariableExpr, req.VariableExpr);
rule.Version = rule.Version + 1;
rule.UpdateOptId = UserId;
rule.UpdateOptName = UserName;
rule.UpdateDate = DateTime.Now;
WriteAuditLog("VAR_CASCADE_UPDATE_RULE", "RULE", rule.id, rule.RuleName,
$"因变量'{variable.VariableName}'(ID:{variableId})表达式变更,规则表达式被级联更新");
}
DbContext.SaveChanges();
WriteAuditLog("VAR_UPDATE", "VARIABLE", variableId, req.VariableName, $"修改变量:{variable.VariableName}");
DbContext.SaveChanges();
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
InvalidateVariableCache();
foreach (var rule in affectedRules)
{
RiskEngineService.GetInstance().RefreshOneRuleCache(rule.id);
}
}
else
{
WriteAuditLog("VAR_UPDATE", "VARIABLE", variableId, req.VariableName, $"修改变量:{variable.VariableName}");
DbContext.SaveChanges();
InvalidateVariableCache();
}
return GetVariableDetail(variableId);
}
/// <summary>
/// 删除变量(有 Active 引用时拒绝)
/// </summary>
public void DeleteVariable(long variableId)
{
var variable = GetVariableOrThrow(variableId);
var activeRefCount = GetRulesByVariableId(variableId)
.Count(r => r.Status == RiskRuleStatus.Active);
if (activeRefCount > 0)
throw new ServiceException($"该变量被 {activeRefCount} 个生效中的规则引用,无法删除");
DbContext.glms_risk_variable.Remove(variable);
WriteAuditLog("VAR_DELETE", "VARIABLE", variableId, variable.VariableName, $"删除变量:{variable.VariableName}");
DbContext.SaveChanges();
InvalidateVariableCache();
}
/// <summary>
/// 获取所有变量列表(供下拉选择)
/// </summary>
public List<RiskVariableSimpleItem> GetAllVariableList()
{
var result = DbContext.glms_risk_variable
.Where(v => v.VariableExpr != null && v.VariableExpr != "")
.OrderBy(v => v.SortOrder)
.Select(v => new RiskVariableSimpleItem
{
Id = v.id,
VariableName = v.VariableName,
Category = v.Category,
DataType = v.DataType,
Unit = v.Unit,
VariableExpr = v.VariableExpr
})
.ToList();
return result;
}
#endregion
#region Audit Log
/// <summary>
/// 查询审计日志列表(分页)
/// </summary>
public SearchListResult<RiskAuditLogListItem> QueryAuditLogs(QueryRiskAuditLogReq req)
{
return BuildAuditLogQuery(req).ToSearchList(req);
}
private IQueryable<RiskAuditLogListItem> BuildAuditLogQuery(QueryRiskAuditLogReq req)
{
var query = DbContext.glms_risk_rule_audit_log.AsQueryable();
if (!string.IsNullOrWhiteSpace(req.OperationType))
{
query = query.Where(l => l.OperationType == req.OperationType);
}
if (!string.IsNullOrWhiteSpace(req.OperationTypes))
{
var types = req.OperationTypes.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
query = query.Where(l => types.Contains(l.OperationType));
}
if (!string.IsNullOrWhiteSpace(req.TargetType))
{
query = query.Where(l => l.TargetType == req.TargetType);
}
if (!string.IsNullOrWhiteSpace(req.TargetName))
{
query = query.Where(l => l.TargetName.Contains(req.TargetName));
}
if (!string.IsNullOrWhiteSpace(req.OptName))
{
query = query.Where(l => l.OptName.Contains(req.OptName));
}
if (req.StartDate.HasValue)
{
query = query.Where(l => l.OptDate >= req.StartDate.Value.Date);
}
if (req.EndDate.HasValue)
{
var end = req.EndDate.Value.Date.AddDays(1);
query = query.Where(l => l.OptDate < end);
}
if (!string.IsNullOrWhiteSpace(req.Keyword))
{
query = query.Where(l => l.TargetName.Contains(req.Keyword)
|| l.OperationDetail.Contains(req.Keyword));
}
var result = query.OrderByDescending(l => l.OptDate)
.Select(l => new RiskAuditLogListItem
{
Id = l.id,
OperationType = l.OperationType,
TargetType = l.TargetType,
TargetId = l.TargetId,
TargetName = l.TargetName,
OperationDetail = l.OperationDetail,
Result = l.Result,
OptName = l.OptName,
OptDate = l.OptDate
});
return result;
}
/// <summary>
/// 根据查询条件生成审计日志导出文件名
/// </summary>
public string BuildAuditLogExportFileName(QueryRiskAuditLogReq req)
{
req ??= new QueryRiskAuditLogReq();
var conditions = new List<string>();
var operationTypes = new List<string>();
if (!string.IsNullOrWhiteSpace(req.OperationType))
operationTypes.Add(req.OperationType.Trim());
if (!string.IsNullOrWhiteSpace(req.OperationTypes))
{
operationTypes.AddRange(req.OperationTypes.Split(',',
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries));
}
var operationTypeNames = operationTypes
.Distinct(StringComparer.OrdinalIgnoreCase)
.Select(GetAuditOperationTypeName)
.ToList();
if (operationTypeNames.Any())
conditions.Add($"操作类型-{string.Join("+", operationTypeNames)}");
if (!string.IsNullOrWhiteSpace(req.TargetType))
conditions.Add($"目标类型-{GetAuditTargetTypeName(req.TargetType)}");
if (!string.IsNullOrWhiteSpace(req.TargetName))
conditions.Add($"目标名称-{SanitizeFileNamePart(req.TargetName)}");
if (!string.IsNullOrWhiteSpace(req.OptName))
conditions.Add($"操作人-{SanitizeFileNamePart(req.OptName)}");
if (req.StartDate.HasValue && req.EndDate.HasValue)
conditions.Add($"日期-{req.StartDate.Value:yyyyMMdd}至{req.EndDate.Value:yyyyMMdd}");
else if (req.StartDate.HasValue)
conditions.Add($"开始日期-{req.StartDate.Value:yyyyMMdd}");
else if (req.EndDate.HasValue)
conditions.Add($"结束日期-{req.EndDate.Value:yyyyMMdd}");
if (!string.IsNullOrWhiteSpace(req.Keyword))
conditions.Add($"关键词-{SanitizeFileNamePart(req.Keyword)}");
var suffix = conditions.Any() ? string.Join("_", conditions) : "全部";
var fileNameWithoutExtension = $"风控操作日志_{suffix}";
if (fileNameWithoutExtension.Length > 180)
fileNameWithoutExtension = fileNameWithoutExtension.Substring(0, 180);
return $"{fileNameWithoutExtension}.xlsx";
}
/// <summary>
/// 导出审计日志为 Excel
/// </summary>
public byte[] ExportAuditLogs(QueryRiskAuditLogReq req)
{
var logs = BuildAuditLogQuery(req).ToList();
var columns = new List<ExcelHelper.DataColumnModel>
{
new ExcelHelper.DataColumnModel("日志ID", "Id", typeof(string),
(value, _) => FormatAuditId(value)),
new ExcelHelper.DataColumnModel("操作类型", "OperationType", typeof(string),
(value, _) => GetAuditOperationTypeName(value?.ToString())),
new ExcelHelper.DataColumnModel("目标类型", "TargetType", typeof(string),
(value, _) => GetAuditTargetTypeName(value?.ToString())),
new ExcelHelper.DataColumnModel("目标ID", "TargetId", typeof(string),
(value, _) => FormatAuditId(value)),
new ExcelHelper.DataColumnModel("目标名称", "TargetName", typeof(string)),
new ExcelHelper.DataColumnModel("操作详情", "OperationDetail", typeof(string)),
new ExcelHelper.DataColumnModel("操作结果", "Result", typeof(string)),
new ExcelHelper.DataColumnModel("操作人", "OptName", typeof(string)),
new ExcelHelper.DataColumnModel("操作时间", "OptDate", (value, _) =>
value == null ? string.Empty : ((DateTime)value).ToString("yyyy-MM-dd HH:mm:ss"))
};
new ExcelHelper().ListToExcel<RiskAuditLogListItem>(
columns.ToArray(), logs, "风控操作日志", true, out var buffer);
return buffer;
}
/// <summary>
/// 获取审计日志详情
/// </summary>
public RiskAuditLogDetail GetAuditLogDetail(long logId)
{
var log = DbContext.glms_risk_rule_audit_log.FirstOrDefault(l => l.id == logId);
if (log == null)
throw new ServiceException("审计日志不存在");
return new RiskAuditLogDetail
{
Id = log.id,
OperationType = log.OperationType,
TargetType = log.TargetType,
TargetId = log.TargetId,
TargetName = log.TargetName,
OperationDetail = log.OperationDetail,
Result = log.Result,
SnapshotData = log.SnapshotData,
OptId = log.OptId,
OptName = log.OptName,
OptDate = log.OptDate
};
}
#endregion
}
}