2403 lines
103 KiB
C#
2403 lines
103 KiB
C#
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详细设计.md(v1.1)
|
||
需求来源文档:.trae/documents/风控引擎需求详细设计.md(v1.17)
|
||
|
||
核心概念:
|
||
- Rule(规则):定义"什么情况下触发",支持两种公式模式(互斥):
|
||
· 结构化模式(ConditionJson 有值):变量 + 操作符 + 阈值/变量 的条件列表,前端拼接 RuleExpr
|
||
· 自由文本模式(RuleExpr 有值,ConditionJson 为空):前端直接编辑类 C# 表达式
|
||
- Application(应用配置):定义"何时、对谁、怎么处理",关联多条规则(RuleIds 逗号分隔)+ 策略/时点/范围
|
||
- Variable(变量):变量池中的可选项,用于构建规则条件(含取值表达式 VariableExpr)
|
||
- AuditLog(审计日志):记录所有 CRUD 操作,支持版本追溯(SnapshotData 存储版本号快照)
|
||
|
||
【架构位置】
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ API 层 │
|
||
│ RiskRuleController(27 个端点) │
|
||
│ - /api/risk-rules/* 规则 CRUD(10 个端点) │
|
||
│ - /api/risk-applications/* 应用配置 CRUD(9 个端点) │
|
||
│ - /api/risk-variables/* 变量池 CRUD(6 个端点) │
|
||
│ - /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
|
||
- 关键字段:SnapshotData(JSON 快照,存储版本号等)
|
||
|
||
【关键文件清单】
|
||
本服务:
|
||
- RiskRuleService.cs(本文件):规则/应用/变量/日志 CRUD 管理
|
||
- Dto/ 目录:请求/响应 DTO 模型
|
||
|
||
关联服务:
|
||
- RiskEngineService.cs:引擎执行层(单例,本服务通过 GetInstance() 获取引用)
|
||
- RiskRuleController.cs(YLErpWeb/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.cs(Active/Disabled/Deleted)
|
||
- Framework/YLErp.Core/DBModels/Enums/RiskControlStrategy.cs(Block/Approve/Warn)
|
||
- Framework/YLErp.Core/DBModels/Enums/RiskVariableCategory.cs
|
||
- Framework/YLErp.Core/DBModels/Enums/RiskVariableDataType.cs(Numeric/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
|
||
}
|
||
} |