1429 lines
60 KiB
C#
1429 lines
60 KiB
C#
using BaseOUDAL;
|
||
using Newtonsoft.Json;
|
||
using Qdp.Foundation.Utilities;
|
||
using YLErp.BLL;
|
||
using YLErp.DBModels;
|
||
using YLErp.Model;
|
||
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>) 批量删除规则(逐条保护检查,单事务)
|
||
GetRuleApplications(long) 获取规则关联的应用配置列表
|
||
GetAllRuleList() 获取所有启用规则(轻量字段,供应用配置下拉)
|
||
|
||
── 应用配置管理(9 个) ───────────────────────────────────────────
|
||
QueryApplicationList(QueryRiskApplicationReq) 查询应用列表(多条件筛选,内存关联规则名称)
|
||
GetApplicationDetail(long) 获取应用详情(含关联规则信息)
|
||
CreateApplication(CreateRiskApplicationReq) 新建应用(关联规则 Active 校验,触发时点/Scope 校验)
|
||
UpdateApplication(long, UpdateRiskApplicationReq) 修改应用(乐观锁校验)
|
||
DeleteApplication(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 列表格式校验
|
||
TryRefreshCache() 安全调用 RefreshCache(失败仅记日志)
|
||
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. 缓存刷新:写操作后调用 TryRefreshCache(),失败仅记日志不影响操作结果
|
||
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. 所有写操作后必须调用 TryRefreshCache(),确保引擎缓存与数据库一致
|
||
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> { "是", "否" } }
|
||
};
|
||
|
||
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 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);
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
private void InvalidateVariableCache()
|
||
{
|
||
_variableExprCache = null;
|
||
}
|
||
|
||
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 (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");
|
||
}
|
||
}
|
||
}
|
||
|
||
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("公式表达式括号不匹配:缺少右括号");
|
||
}
|
||
|
||
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("至少需要指定一个触发时点");
|
||
}
|
||
|
||
private void ValidateScopeFields(CreateRiskApplicationReq req)
|
||
{
|
||
if (req.ScopeIsGlobal)
|
||
{
|
||
if (!string.IsNullOrEmpty(req.ScopeAssetBookIds) ||
|
||
!string.IsNullOrEmpty(req.ScopeClientIds) ||
|
||
!string.IsNullOrEmpty(req.ScopeUnderlyingTypes) ||
|
||
!string.IsNullOrEmpty(req.ScopeTradeTypes))
|
||
{
|
||
throw new ServiceException("全局适用时,不应指定其他适用范围维度");
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (!string.IsNullOrEmpty(req.ScopeAssetBookIds))
|
||
{
|
||
ValidateIdList(req.ScopeAssetBookIds, "适用账户");
|
||
}
|
||
|
||
if (!string.IsNullOrEmpty(req.ScopeClientIds))
|
||
{
|
||
ValidateIdList(req.ScopeClientIds, "适用对手方");
|
||
}
|
||
}
|
||
|
||
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");
|
||
}
|
||
}
|
||
|
||
private void TryRefreshCache()
|
||
{
|
||
try
|
||
{
|
||
RiskEngineService.GetInstance().RefreshCache();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.Error($"缓存刷新失败:{ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
private string GetDataTypeName(RiskVariableDataType dataType)
|
||
{
|
||
return dataType switch
|
||
{
|
||
RiskVariableDataType.Numeric => "数值",
|
||
RiskVariableDataType.Date => "日期",
|
||
RiskVariableDataType.Boolean => "布尔",
|
||
_ => "未知"
|
||
};
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
private void ValidateRuleIdsActive(string ruleIds)
|
||
{
|
||
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)
|
||
throw new ServiceException("部分关联规则不存在或已删除");
|
||
|
||
foreach (var rule in rules)
|
||
{
|
||
if (rule.Status == RiskRuleStatus.Disabled)
|
||
throw new ServiceException($"关联规则 '{rule.RuleName}' 已停用,无法创建/启用应用配置");
|
||
}
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Rule Management
|
||
|
||
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,
|
||
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.Contains(r.id.ToString()) && a.Status != RiskRuleStatus.Deleted)
|
||
});
|
||
|
||
return result.ToSearchList(req);
|
||
}
|
||
|
||
public RiskRuleDetail GetRuleDetail(long ruleId)
|
||
{
|
||
var rule = GetRuleOrThrow(ruleId);
|
||
|
||
var ruleIdStr = ruleId.ToString();
|
||
var applications = DbContext.glms_risk_rule_application
|
||
.Where(a => a.RuleIds.Contains(ruleIdStr) && a.Status != RiskRuleStatus.Deleted)
|
||
.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,
|
||
Status = rule.Status,
|
||
Version = rule.Version,
|
||
OptName = rule.OptName,
|
||
OptDate = rule.OptDate.GetValueOrDefault(),
|
||
UpdateOptName = rule.UpdateOptName,
|
||
UpdateDate = rule.UpdateDate ?? rule.OptDate.GetValueOrDefault(),
|
||
Applications = applications
|
||
};
|
||
}
|
||
|
||
public RiskRuleDetail CreateRule(CreateRiskRuleReq req)
|
||
{
|
||
ValidateRuleParams(req.RuleName, req.ConditionJson, req.RuleExpr);
|
||
|
||
var entity = new glms_risk_rule
|
||
{
|
||
RuleName = req.RuleName,
|
||
RuleText = req.RuleText,
|
||
ConditionJson = req.ConditionJson,
|
||
RuleExpr = req.RuleExpr,
|
||
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;
|
||
}
|
||
}
|
||
|
||
TryRefreshCache();
|
||
|
||
return GetRuleDetail(entity.id);
|
||
}
|
||
|
||
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);
|
||
|
||
rule.RuleName = req.RuleName;
|
||
rule.RuleText = req.RuleText;
|
||
rule.ConditionJson = req.ConditionJson;
|
||
rule.RuleExpr = req.RuleExpr;
|
||
rule.Version = rule.Version + 1;
|
||
rule.UpdateOptId = UserId;
|
||
rule.UpdateOptName = UserName;
|
||
rule.UpdateDate = DateTime.Now;
|
||
|
||
WriteAuditLog("RULE_UPDATE", "RULE", ruleId, req.RuleName, "修改规则");
|
||
DbContext.SaveChanges();
|
||
|
||
TryRefreshCache();
|
||
|
||
return GetRuleDetail(ruleId);
|
||
}
|
||
|
||
public void DeleteRule(long ruleId)
|
||
{
|
||
var rule = GetRuleOrThrow(ruleId);
|
||
|
||
var ruleIdStr = ruleId.ToString();
|
||
var activeCount = DbContext.glms_risk_rule_application
|
||
.Count(a => a.RuleIds.Contains(ruleIdStr) && a.Status == RiskRuleStatus.Active);
|
||
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();
|
||
|
||
TryRefreshCache();
|
||
}
|
||
|
||
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("仅已停用的规则可以启用");
|
||
|
||
rule.Status = RiskRuleStatus.Active;
|
||
rule.UpdateOptId = UserId;
|
||
rule.UpdateOptName = UserName;
|
||
rule.UpdateDate = DateTime.Now;
|
||
|
||
WriteAuditLog("RULE_ENABLE", "RULE", ruleId, rule.RuleName, "启用规则");
|
||
DbContext.SaveChanges();
|
||
|
||
TryRefreshCache();
|
||
}
|
||
|
||
public void 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, "停用规则");
|
||
DbContext.SaveChanges();
|
||
|
||
TryRefreshCache();
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
public BatchOperationResult BatchDeleteRules(List<long> ruleIds)
|
||
{
|
||
var rules = DbContext.glms_risk_rule
|
||
.Where(r => ruleIds.Contains(r.id) && r.Status != RiskRuleStatus.Deleted)
|
||
.ToList();
|
||
|
||
if (rules.Count != ruleIds.Count)
|
||
throw new ServiceException("部分规则不存在或已删除");
|
||
|
||
foreach (var rule in rules)
|
||
{
|
||
var ruleIdStr = rule.id.ToString();
|
||
var activeCount = DbContext.glms_risk_rule_application
|
||
.Count(a => a.RuleIds.Contains(ruleIdStr) && a.Status == RiskRuleStatus.Active);
|
||
if (activeCount > 0)
|
||
throw new ServiceException($"规则 '{rule.RuleName}' 仍有 {activeCount} 个生效中的应用配置,请先停用或删除相关应用");
|
||
}
|
||
|
||
foreach (var rule in rules)
|
||
{
|
||
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, "批量删除规则");
|
||
}
|
||
|
||
DbContext.SaveChanges();
|
||
TryRefreshCache();
|
||
|
||
return new BatchOperationResult
|
||
{
|
||
Success = true,
|
||
TotalCount = ruleIds.Count,
|
||
SuccessCount = ruleIds.Count
|
||
};
|
||
}
|
||
|
||
public List<RiskApplicationListItem> GetRuleApplications(long ruleId)
|
||
{
|
||
GetRuleOrThrow(ruleId);
|
||
|
||
var ruleIdStr = ruleId.ToString();
|
||
var apps = DbContext.glms_risk_rule_application
|
||
.Where(a => a.RuleIds.Contains(ruleIdStr) && a.Status != RiskRuleStatus.Deleted)
|
||
.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 = 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;
|
||
}
|
||
|
||
public List<RiskRuleSimpleItem> GetAllRuleList()
|
||
{
|
||
return DbContext.glms_risk_rule
|
||
.Where(r => r.Status == RiskRuleStatus.Active)
|
||
.OrderBy(r => r.id)
|
||
.Select(r => new RiskRuleSimpleItem
|
||
{
|
||
Id = r.id,
|
||
RuleName = r.RuleName,
|
||
RuleText = r.RuleText
|
||
})
|
||
.ToList();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Application Management
|
||
|
||
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
|
||
&& a.RuleIds.Contains(r.id.ToString())
|
||
&& r.RuleName.Contains(req.RuleName)));
|
||
}
|
||
|
||
var pagedResult = query.OrderByDescending(a => a.UpdateDate)
|
||
.Select(a => new RiskApplicationListItem
|
||
{
|
||
Id = a.id,
|
||
RuleIds = a.RuleIds,
|
||
Status = 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;
|
||
}
|
||
|
||
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,
|
||
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()
|
||
};
|
||
}
|
||
|
||
public RiskApplicationDetail CreateApplication(CreateRiskApplicationReq req)
|
||
{
|
||
ValidateRuleIdsActive(req.RuleIds);
|
||
ValidateTriggerPoints(req.TriggerPoints);
|
||
ValidateScopeFields(req);
|
||
|
||
var entity = new glms_risk_rule_application
|
||
{
|
||
RuleIds = req.RuleIds,
|
||
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(req.RuleIds),
|
||
$"创建应用配置:策略={req.ControlStrategy}, 触发时点={req.TriggerPoints}",
|
||
snapshotData: JsonConvert.SerializeObject(new { Version = entity.Version }));
|
||
DbContext.SaveChanges();
|
||
|
||
transaction.Commit();
|
||
}
|
||
catch
|
||
{
|
||
transaction.Rollback();
|
||
throw;
|
||
}
|
||
}
|
||
|
||
TryRefreshCache();
|
||
|
||
return GetApplicationDetail(entity.id);
|
||
}
|
||
|
||
public RiskApplicationDetail UpdateApplication(long applicationId, UpdateRiskApplicationReq req)
|
||
{
|
||
var app = GetApplicationOrThrow(applicationId);
|
||
|
||
if (app.Version != req.Version)
|
||
throw new ServiceException("应用配置已被其他用户修改,请重新加载后再编辑");
|
||
|
||
if (!string.IsNullOrWhiteSpace(req.RuleIds))
|
||
ValidateRuleIdsActive(req.RuleIds);
|
||
|
||
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 (!string.IsNullOrWhiteSpace(req.RuleIds))
|
||
app.RuleIds = req.RuleIds;
|
||
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),
|
||
$"修改应用配置:策略={req.ControlStrategy}, 触发时点={req.TriggerPoints}");
|
||
DbContext.SaveChanges();
|
||
|
||
TryRefreshCache();
|
||
|
||
return GetApplicationDetail(applicationId);
|
||
}
|
||
|
||
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("APPLICATION_DELETE", "APPLICATION", applicationId, ResolveRuleNames(app.RuleIds), "删除应用配置");
|
||
DbContext.SaveChanges();
|
||
|
||
TryRefreshCache();
|
||
}
|
||
|
||
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("仅已停用的应用配置可以启用");
|
||
|
||
ValidateRuleIdsActive(app.RuleIds);
|
||
|
||
app.Status = RiskRuleStatus.Active;
|
||
app.UpdateOptId = UserId;
|
||
app.UpdateOptName = UserName;
|
||
app.UpdateDate = DateTime.Now;
|
||
|
||
WriteAuditLog("APP_ENABLE", "APPLICATION", applicationId, ResolveRuleNames(app.RuleIds), "启用应用配置");
|
||
DbContext.SaveChanges();
|
||
|
||
TryRefreshCache();
|
||
}
|
||
|
||
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();
|
||
|
||
TryRefreshCache();
|
||
}
|
||
|
||
public BatchOperationResult BatchEnableApplications(List<long> applicationIds)
|
||
{
|
||
var apps = DbContext.glms_risk_rule_application
|
||
.Where(a => applicationIds.Contains(a.id) && a.Status == RiskRuleStatus.Disabled)
|
||
.ToList();
|
||
|
||
if (apps.Count != applicationIds.Count)
|
||
throw new ServiceException("部分应用配置不存在或当前状态不允许启用");
|
||
|
||
foreach (var app in apps)
|
||
{
|
||
ValidateRuleIdsActive(app.RuleIds);
|
||
}
|
||
|
||
foreach (var app in apps)
|
||
{
|
||
app.Status = RiskRuleStatus.Active;
|
||
app.UpdateOptId = UserId;
|
||
app.UpdateOptName = UserName;
|
||
app.UpdateDate = DateTime.Now;
|
||
|
||
WriteAuditLog("APP_BATCH_ENABLE", "APPLICATION", app.id, ResolveRuleNames(app.RuleIds), "批量启用应用配置");
|
||
}
|
||
|
||
DbContext.SaveChanges();
|
||
TryRefreshCache();
|
||
|
||
return new BatchOperationResult
|
||
{
|
||
Success = true,
|
||
TotalCount = applicationIds.Count,
|
||
SuccessCount = applicationIds.Count
|
||
};
|
||
}
|
||
|
||
public BatchOperationResult BatchDisableApplications(List<long> applicationIds)
|
||
{
|
||
var apps = DbContext.glms_risk_rule_application
|
||
.Where(a => applicationIds.Contains(a.id) && a.Status == RiskRuleStatus.Active)
|
||
.ToList();
|
||
|
||
if (apps.Count != applicationIds.Count)
|
||
throw new ServiceException("部分应用配置不存在或当前状态不允许停用");
|
||
|
||
foreach (var app in apps)
|
||
{
|
||
app.Status = RiskRuleStatus.Disabled;
|
||
app.UpdateOptId = UserId;
|
||
app.UpdateOptName = UserName;
|
||
app.UpdateDate = DateTime.Now;
|
||
|
||
WriteAuditLog("APPLICATION_BATCH_DISABLE", "APPLICATION", app.id, ResolveRuleNames(app.RuleIds), "批量停用应用配置");
|
||
}
|
||
|
||
DbContext.SaveChanges();
|
||
TryRefreshCache();
|
||
|
||
return new BatchOperationResult
|
||
{
|
||
Success = true,
|
||
TotalCount = applicationIds.Count,
|
||
SuccessCount = applicationIds.Count
|
||
};
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Variable Management
|
||
|
||
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 (!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);
|
||
}
|
||
|
||
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
|
||
};
|
||
}
|
||
|
||
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();
|
||
TryRefreshCache();
|
||
|
||
return GetVariableDetail(entity.id);
|
||
}
|
||
|
||
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 varExpr = variable.VariableExpr;
|
||
var activeRefCount = 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))));
|
||
if (activeRefCount > 0)
|
||
throw new ServiceException($"该变量被 {activeRefCount} 个生效中的规则引用,无法修改数据类型");
|
||
}
|
||
|
||
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;
|
||
|
||
WriteAuditLog("VAR_UPDATE", "VARIABLE", variableId, req.VariableName, $"修改变量:{variable.VariableName}");
|
||
DbContext.SaveChanges();
|
||
|
||
InvalidateVariableCache();
|
||
TryRefreshCache();
|
||
|
||
return GetVariableDetail(variableId);
|
||
}
|
||
|
||
public void DeleteVariable(long variableId)
|
||
{
|
||
var variable = GetVariableOrThrow(variableId);
|
||
|
||
var varExpr = variable.VariableExpr;
|
||
var activeRefCount = 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))));
|
||
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();
|
||
TryRefreshCache();
|
||
}
|
||
|
||
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
|
||
|
||
public SearchListResult<RiskAuditLogListItem> QueryAuditLogs(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.TargetType))
|
||
{
|
||
query = query.Where(l => l.TargetType == req.TargetType);
|
||
}
|
||
|
||
if (req.StartDate.HasValue)
|
||
{
|
||
query = query.Where(l => l.OptDate >= req.StartDate.Value);
|
||
}
|
||
|
||
if (req.EndDate.HasValue)
|
||
{
|
||
query = query.Where(l => l.OptDate <= req.EndDate.Value);
|
||
}
|
||
|
||
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.ToSearchList(req);
|
||
}
|
||
|
||
public byte[] ExportAuditLogs(QueryRiskAuditLogReq req)
|
||
{
|
||
throw new ServiceException("导出功能暂未实现");
|
||
}
|
||
|
||
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
|
||
}
|
||
}
|