【权限控制】 - FunctionRight.xml 注册 16 个风控引擎权限标识(规则/应用/变量/日志 四类 Tab+操作权限) - RiskRuleController 28 个 Action 均添加 [MyAuthorize] 校验 - 权限管理员角色自动获得全部风控权限(UserInfo.cs) 【需求对齐】对照 v1.15 需求文档修复差异: - 审计日志操作类型命名对齐:APPLICATION_* → APP_*,VARIABLE_* → VAR_* - 规则列表新增按变量编码筛选(QueryRiskRuleReq.VariableCode → FormulaJson.Contains) - 变量列表新增分页支持(QueryRiskVariableReq 继承 BaseSearchReq,QueryVariableList → SearchListResult) - 需求文档同步至 v1.16
1415 lines
62 KiB
C#
1415 lines
62 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.14)
|
||
|
||
核心概念:
|
||
- Rule(规则):定义"什么情况下触发",由变量 + 操作符 + 阈值组成的条件列表(FormulaJson)
|
||
- Application(应用配置):定义"何时、对谁、怎么处理",关联规则 + 策略/时点/范围(1个规则可配N个应用)
|
||
- Variable(变量):变量池中的可选项,用于构建规则条件(含实现脚本 ImplementationScript)
|
||
- 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 │
|
||
│ - FormulaJson 完整校验(变量存在性、操作符、类型匹配、阈值引用) │
|
||
│ - 乐观锁并发控制(Version 字段) │
|
||
│ - 删除保护(有 Active 引用时拒绝删除) │
|
||
│ - CRUD 后触发 RiskEngineService.RefreshCache() │
|
||
│ │
|
||
│ RiskEngineService(另一团队开发,单例) │
|
||
│ - 内存缓存管理 + 公式执行 + 风控判定 │
|
||
│ - RefreshCache():从数据库重新加载 Active 规则/应用/变量 │
|
||
└──────────────────────────┬──────────────────────────────────────┘
|
||
│
|
||
┌──────────────────────────▼──────────────────────────────────────┐
|
||
│ 数据访问层(EF Core DbContext 直接访问,无独立 Repository) │
|
||
│ - glms_risk_rule 规则定义表 │
|
||
│ - glms_risk_rule_application 规则应用配置表(RuleId 外键) │
|
||
│ - glms_risk_variable 变量池定义表 │
|
||
│ - glms_risk_rule_audit_log 操作审计日志表 │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
|
||
【方法清单】(共 27 个公开方法 + 10 个私有辅助方法)
|
||
|
||
── 规则管理(10 个) ──────────────────────────────────────────────
|
||
QueryRuleList(QueryRiskRuleReq) 查询规则列表(分页,含 ApplicationCount 子查询)
|
||
GetRuleDetail(long) 获取规则详情(含关联应用摘要)
|
||
CreateRule(CreateRiskRuleReq) 新建规则(RuleCode 自动生成,FormulaJson 完整校验)
|
||
UpdateRule(long, UpdateRiskRuleReq) 修改规则(乐观锁校验,Version+1)
|
||
DeleteRule(long) 删除规则(强保护:有 Active 应用时拒绝)
|
||
EnableRule(long) 启用规则(仅 Disabled → Active)
|
||
DisableRule(long) 停用规则(仅 Active → Disabled)
|
||
GetRuleVersions(long) 获取规则版本历史(从审计日志 SnapshotData 提取版本号)
|
||
BatchDeleteRules(List<long>) 批量删除规则(逐条保护检查,单事务)
|
||
GetRuleApplications(long) 获取规则关联的应用配置列表
|
||
|
||
── 应用配置管理(9 个) ───────────────────────────────────────────
|
||
QueryApplicationList(QueryRiskApplicationReq) 查询应用列表(Join 规则表,多条件筛选)
|
||
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) 新建变量(编码唯一性校验,脚本长度校验 ≤10000)
|
||
UpdateVariable(long, UpdateRiskVariableReq) 修改变量(乐观锁,DataType 变更保护)
|
||
DeleteVariable(long) 删除变量(引用保护:有 Active 规则引用时拒绝,硬删除)
|
||
GetAllVariableList() 获取所有已实现变量(轻量字段,供规则编辑器下拉)
|
||
|
||
── 审计日志(3 个) ───────────────────────────────────────────────
|
||
QueryAuditLogs(QueryRiskAuditLogReq) 查询审计日志(多条件筛选,分页)
|
||
ExportAuditLogs(QueryRiskAuditLogReq) 导出审计日志(占位实现,后续迭代 Excel 导出)
|
||
GetAuditLogDetail(long) 获取审计日志详情(含完整 SnapshotData)
|
||
|
||
── 私有辅助方法 ───────────────────────────────────────────────────
|
||
WriteAuditLog(...) 写入审计日志(含 snapshotData 可选参数)
|
||
GenerateCode(prefix, existingCodes) 编码生成(RISK/APP-YYYYMMDD-NNNN,每日上限 9999)
|
||
GetVariableCodeSet() 获取变量编码集合(5 分钟本地缓存)
|
||
InvalidateVariableCache() 清除变量缓存
|
||
ValidateFormulaJson(formulaJson) FormulaJson 完整校验
|
||
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. RuleCode/ApplicationCode:Service 层自动生成,格式 RISK/APP-YYYYMMDD-NNNN
|
||
6. FormulaJson 校验:变量存在性 + 操作符合法性(按 DataType 映射)+ 类型匹配 + 阈值变量引用
|
||
7. FormulaText:前端生成,后端仅校验非空并存储
|
||
8. Scope 字段:逗号分隔字符串存储,全局适用时不允许指定其他维度
|
||
9. 缓存刷新:写操作后调用 TryRefreshCache(),失败仅记日志不影响操作结果
|
||
10. 批量操作:单事务批量,全部成功或全部失败
|
||
11. 变量缓存:5 分钟本地 HashSet<string> 缓存,CRUD 后 InvalidateVariableCache()
|
||
12. 审计日志:16 种操作类型,CRUD + 审计日志同一事务,SnapshotData 存储版本快照
|
||
|
||
【数据库表】(MySQL,迁移脚本位于 Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/prod.sql)
|
||
glms_risk_rule 规则定义表
|
||
- 唯一索引:RuleCode
|
||
- 普通索引:Status, OptDate
|
||
- 关键字段:FormulaJson(公式定义), FormulaText(可读文本), Version(乐观锁)
|
||
|
||
glms_risk_rule_application 规则应用配置表
|
||
- 唯一索引:ApplicationCode
|
||
- 普通索引:RuleId, Status, OptDate
|
||
- 外键:RuleId → glms_risk_rule.id
|
||
- 关键字段:ControlStrategy(禁止/审批/提示), TriggerPoints(触发时点), Scope*(适用范围)
|
||
|
||
glms_risk_variable 变量池定义表
|
||
- 唯一索引:VariableCode
|
||
- 关键字段:ImplementationScript(实现脚本), DataType(数值/日期/布尔), IsImplemented
|
||
|
||
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. ImplementationScript 当前仅做基础长度校验(≤10000),完整编译校验待引入 Roslyn 库
|
||
6. ValidateScopeFields 的 ID 存在性校验已跳过(前端下拉选择器保证有效性)
|
||
================================================================================
|
||
*/
|
||
|
||
namespace YLErp.Modules.RiskEngine
|
||
{
|
||
public class RiskRuleService : YLBaseService
|
||
{
|
||
private readonly IYcLogger _logger = LogFactory.GetLogger("RiskRuleService");
|
||
private readonly RiskEngineService _riskEngineService;
|
||
|
||
private HashSet<string> _variableCodeCache;
|
||
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)
|
||
{
|
||
_riskEngineService = RiskEngineService.GetInstance();
|
||
}
|
||
|
||
public RiskRuleService(YLBaseService baseService) : base(baseService)
|
||
{
|
||
_riskEngineService = RiskEngineService.GetInstance();
|
||
}
|
||
|
||
public RiskRuleService(OptUserInfo optUser, YLContext dbContext) : base(optUser, dbContext)
|
||
{
|
||
_riskEngineService = RiskEngineService.GetInstance();
|
||
}
|
||
|
||
#region Private Helpers
|
||
|
||
private void WriteAuditLog(string operationType, string targetType, long targetId,
|
||
string targetName, string targetCode, string operationDetail, string result = "成功", string snapshotData = null)
|
||
{
|
||
var log = new glms_risk_rule_audit_log
|
||
{
|
||
OperationType = operationType,
|
||
TargetType = targetType,
|
||
TargetId = targetId,
|
||
TargetName = targetName,
|
||
TargetCode = targetCode,
|
||
OperationDetail = operationDetail,
|
||
Result = result,
|
||
SnapshotData = snapshotData,
|
||
OptId = UserId,
|
||
OptName = UserName,
|
||
OptDate = DateTime.Now
|
||
};
|
||
DbContext.glms_risk_rule_audit_log.Add(log);
|
||
}
|
||
|
||
private string GenerateCode(string prefix, IQueryable<string> existingCodes)
|
||
{
|
||
var today = DateTime.Now.ToString("yyyyMMdd");
|
||
var pattern = $"{prefix}-{today}-";
|
||
|
||
var maxCode = existingCodes
|
||
.Where(c => c.StartsWith(pattern))
|
||
.OrderByDescending(c => c)
|
||
.FirstOrDefault();
|
||
|
||
int sequence;
|
||
if (string.IsNullOrEmpty(maxCode))
|
||
{
|
||
sequence = 1;
|
||
}
|
||
else
|
||
{
|
||
var lastPart = maxCode.Substring(pattern.Length);
|
||
if (!int.TryParse(lastPart, out int lastSequence))
|
||
throw new ServiceException("编码格式异常");
|
||
sequence = lastSequence + 1;
|
||
}
|
||
|
||
if (sequence > 9999)
|
||
throw new ServiceException("今日编码已达上限,请明日再试");
|
||
|
||
return $"{pattern}{sequence:D4}";
|
||
}
|
||
|
||
private HashSet<string> GetVariableCodeSet()
|
||
{
|
||
if (_variableCodeCache == null ||
|
||
DateTime.Now - _variableCacheUpdateTime > TimeSpan.FromMinutes(5))
|
||
{
|
||
_variableCodeCache = DbContext.glms_risk_variable
|
||
.Where(v => v.IsImplemented)
|
||
.Select(v => v.VariableCode)
|
||
.ToHashSet();
|
||
_variableCacheUpdateTime = DateTime.Now;
|
||
}
|
||
return _variableCodeCache;
|
||
}
|
||
|
||
private void InvalidateVariableCache()
|
||
{
|
||
_variableCodeCache = null;
|
||
}
|
||
|
||
private void ValidateFormulaJson(string formulaJson)
|
||
{
|
||
FormulaDefinition formula;
|
||
try
|
||
{
|
||
formula = JsonConvert.DeserializeObject<FormulaDefinition>(formulaJson);
|
||
}
|
||
catch
|
||
{
|
||
throw new ServiceException("公式表达式 JSON 格式不合法");
|
||
}
|
||
|
||
if (formula.Conditions == null || formula.Conditions.Count == 0)
|
||
throw new ServiceException("公式条件列表不能为空");
|
||
|
||
if (formula.LogicOperator != "AND")
|
||
throw new ServiceException("当前仅支持 AND 逻辑运算符");
|
||
|
||
var allVariableCodes = GetVariableCodeSet();
|
||
|
||
for (int i = 0; i < formula.Conditions.Count; i++)
|
||
{
|
||
var cond = formula.Conditions[i];
|
||
var condLabel = $"条件{i + 1}";
|
||
|
||
if (!allVariableCodes.Contains(cond.VariableCode))
|
||
throw new ServiceException($"{condLabel}:变量编码 '{cond.VariableCode}' 不存在或未实现");
|
||
|
||
var variableDef = DbContext.glms_risk_variable
|
||
.FirstOrDefault(v => v.VariableCode == cond.VariableCode);
|
||
|
||
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 (string.IsNullOrEmpty(cond.ThresholdVariableCode))
|
||
throw new ServiceException($"{condLabel}:变量阈值引用的变量编码不能为空");
|
||
if (!allVariableCodes.Contains(cond.ThresholdVariableCode))
|
||
throw new ServiceException($"{condLabel}:阈值变量编码 '{cond.ThresholdVariableCode}' 不存在或未实现");
|
||
var thresholdVarDef = DbContext.glms_risk_variable
|
||
.FirstOrDefault(v => v.VariableCode == cond.ThresholdVariableCode);
|
||
if (thresholdVarDef.DataType != variableDef.DataType)
|
||
throw new ServiceException($"{condLabel}:阈值变量与条件变量的数据类型不一致");
|
||
}
|
||
else
|
||
{
|
||
throw new ServiceException($"{condLabel}:阈值类型 '{cond.ThresholdType}' 不合法,仅支持 fixed/variable");
|
||
}
|
||
}
|
||
}
|
||
|
||
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.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 formulaJson, string formulaText)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(ruleName))
|
||
throw new ServiceException("规则名称不能为空");
|
||
if (ruleName.Length > 200)
|
||
throw new ServiceException("规则名称长度不能超过200字符");
|
||
if (string.IsNullOrWhiteSpace(formulaJson))
|
||
throw new ServiceException("公式表达式不能为空");
|
||
if (string.IsNullOrWhiteSpace(formulaText))
|
||
throw new ServiceException("公式可读文本不能为空");
|
||
ValidateFormulaJson(formulaJson);
|
||
}
|
||
|
||
#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.RuleCode.Contains(req.Keyword)
|
||
|| r.RuleName.Contains(req.Keyword)
|
||
|| r.RuleDescription.Contains(req.Keyword));
|
||
}
|
||
|
||
if (req.Status.HasValue)
|
||
{
|
||
query = query.Where(r => r.Status == req.Status.Value);
|
||
}
|
||
|
||
if (!string.IsNullOrWhiteSpace(req.VariableCode))
|
||
{
|
||
query = query.Where(r => r.FormulaJson.Contains(req.VariableCode));
|
||
}
|
||
|
||
var result = query.OrderByDescending(r => r.UpdateDate)
|
||
.Select(r => new RiskRuleListItem
|
||
{
|
||
Id = r.id,
|
||
RuleCode = r.RuleCode,
|
||
RuleName = r.RuleName,
|
||
RuleDescription = r.RuleDescription,
|
||
FormulaText = r.FormulaText,
|
||
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.RuleId == r.id && a.Status != RiskRuleStatus.Deleted)
|
||
});
|
||
|
||
return result.ToSearchList(req);
|
||
}
|
||
|
||
public RiskRuleDetail GetRuleDetail(long ruleId)
|
||
{
|
||
var rule = GetRuleOrThrow(ruleId);
|
||
|
||
var applications = DbContext.glms_risk_rule_application
|
||
.Where(a => a.RuleId == ruleId && a.Status != RiskRuleStatus.Deleted)
|
||
.Select(a => new RiskRuleApplicationSummary
|
||
{
|
||
Id = a.id,
|
||
ApplicationCode = a.ApplicationCode,
|
||
Status = a.Status,
|
||
ControlStrategy = a.ControlStrategy,
|
||
TriggerPoints = a.TriggerPoints
|
||
})
|
||
.ToList();
|
||
|
||
return new RiskRuleDetail
|
||
{
|
||
Id = rule.id,
|
||
RuleCode = rule.RuleCode,
|
||
RuleName = rule.RuleName,
|
||
RuleDescription = rule.RuleDescription,
|
||
FormulaJson = rule.FormulaJson,
|
||
FormulaText = rule.FormulaText,
|
||
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.FormulaJson, req.FormulaText);
|
||
|
||
var ruleCode = GenerateCode("RISK", DbContext.glms_risk_rule.Select(r => r.RuleCode));
|
||
|
||
var entity = new glms_risk_rule
|
||
{
|
||
RuleCode = ruleCode,
|
||
RuleName = req.RuleName,
|
||
RuleDescription = req.RuleDescription,
|
||
FormulaJson = req.FormulaJson,
|
||
FormulaText = req.FormulaText,
|
||
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, entity.RuleCode, "创建规则",
|
||
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.ExpectedVersion)
|
||
throw new ServiceException("规则已被其他用户修改,请重新加载后再编辑");
|
||
|
||
ValidateRuleParams(req.RuleName, req.FormulaJson, req.FormulaText);
|
||
|
||
rule.RuleName = req.RuleName;
|
||
rule.RuleDescription = req.RuleDescription;
|
||
rule.FormulaJson = req.FormulaJson;
|
||
rule.FormulaText = req.FormulaText;
|
||
rule.Version = rule.Version + 1;
|
||
rule.UpdateOptId = UserId;
|
||
rule.UpdateOptName = UserName;
|
||
rule.UpdateDate = DateTime.Now;
|
||
|
||
WriteAuditLog("RULE_UPDATE", "RULE", ruleId, req.RuleName, rule.RuleCode, "修改规则");
|
||
DbContext.SaveChanges();
|
||
|
||
TryRefreshCache();
|
||
|
||
return GetRuleDetail(ruleId);
|
||
}
|
||
|
||
public void DeleteRule(long ruleId)
|
||
{
|
||
var rule = GetRuleOrThrow(ruleId);
|
||
|
||
var activeCount = DbContext.glms_risk_rule_application
|
||
.Count(a => a.RuleId == ruleId && 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, rule.RuleCode, "删除规则");
|
||
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.Version = rule.Version + 1;
|
||
rule.UpdateOptId = UserId;
|
||
rule.UpdateOptName = UserName;
|
||
rule.UpdateDate = DateTime.Now;
|
||
|
||
WriteAuditLog("RULE_ENABLE", "RULE", ruleId, rule.RuleName, rule.RuleCode, "启用规则",
|
||
snapshotData: JsonConvert.SerializeObject(new { Version = rule.Version }));
|
||
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.Version = rule.Version + 1;
|
||
rule.UpdateOptId = UserId;
|
||
rule.UpdateOptName = UserName;
|
||
rule.UpdateDate = DateTime.Now;
|
||
|
||
WriteAuditLog("RULE_DISABLE", "RULE", ruleId, rule.RuleName, rule.RuleCode, "停用规则");
|
||
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 activeCount = DbContext.glms_risk_rule_application
|
||
.Count(a => a.RuleId == rule.id && 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, rule.RuleCode, "批量删除规则");
|
||
}
|
||
|
||
DbContext.SaveChanges();
|
||
TryRefreshCache();
|
||
|
||
return new BatchOperationResult
|
||
{
|
||
Success = true,
|
||
TotalCount = ruleIds.Count,
|
||
SuccessCount = ruleIds.Count
|
||
};
|
||
}
|
||
|
||
public List<RiskApplicationListItem> GetRuleApplications(long ruleId)
|
||
{
|
||
GetRuleOrThrow(ruleId);
|
||
|
||
var result = from app in DbContext.glms_risk_rule_application
|
||
join rule in DbContext.glms_risk_rule on app.RuleId equals rule.id
|
||
where app.RuleId == ruleId && app.Status != RiskRuleStatus.Deleted
|
||
orderby app.UpdateDate descending
|
||
select new RiskApplicationListItem
|
||
{
|
||
Id = app.id,
|
||
ApplicationCode = app.ApplicationCode,
|
||
RuleId = app.RuleId,
|
||
RuleCode = rule.RuleCode,
|
||
RuleName = rule.RuleName,
|
||
FormulaText = rule.FormulaText,
|
||
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.ToList();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Application Management
|
||
|
||
public SearchListResult<RiskApplicationListItem> QueryApplicationList(QueryRiskApplicationReq req)
|
||
{
|
||
var query = from app in DbContext.glms_risk_rule_application
|
||
join rule in DbContext.glms_risk_rule on app.RuleId equals rule.id
|
||
where app.Status != RiskRuleStatus.Deleted && rule.Status != RiskRuleStatus.Deleted
|
||
select new { app, rule };
|
||
|
||
if (!string.IsNullOrWhiteSpace(req.Keyword))
|
||
{
|
||
query = query.Where(x => x.app.ApplicationCode.Contains(req.Keyword)
|
||
|| x.rule.RuleName.Contains(req.Keyword));
|
||
}
|
||
|
||
if (req.Status.HasValue)
|
||
{
|
||
query = query.Where(x => x.app.Status == req.Status.Value);
|
||
}
|
||
|
||
if (req.Strategy.HasValue)
|
||
{
|
||
query = query.Where(x => x.app.ControlStrategy == req.Strategy.Value);
|
||
}
|
||
|
||
if (!string.IsNullOrWhiteSpace(req.TriggerPoint))
|
||
{
|
||
query = query.Where(x => x.app.TriggerPoints.Contains(req.TriggerPoint));
|
||
}
|
||
|
||
var result = query.OrderByDescending(x => x.app.UpdateDate)
|
||
.Select(x => new RiskApplicationListItem
|
||
{
|
||
Id = x.app.id,
|
||
ApplicationCode = x.app.ApplicationCode,
|
||
RuleId = x.app.RuleId,
|
||
RuleCode = x.rule.RuleCode,
|
||
RuleName = x.rule.RuleName,
|
||
FormulaText = x.rule.FormulaText,
|
||
Status = x.app.Status,
|
||
ControlStrategy = x.app.ControlStrategy,
|
||
TriggerPoints = x.app.TriggerPoints,
|
||
ScopeAssetBookIds = x.app.ScopeAssetBookIds,
|
||
ScopeClientIds = x.app.ScopeClientIds,
|
||
ScopeUnderlyingTypes = x.app.ScopeUnderlyingTypes,
|
||
ScopeTradeTypes = x.app.ScopeTradeTypes,
|
||
ScopeIsGlobal = x.app.ScopeIsGlobal,
|
||
Version = x.app.Version,
|
||
OptName = x.app.OptName,
|
||
OptDate = x.app.OptDate.GetValueOrDefault(),
|
||
UpdateOptName = x.app.UpdateOptName,
|
||
UpdateDate = x.app.UpdateDate ?? x.app.OptDate.GetValueOrDefault()
|
||
});
|
||
|
||
return result.ToSearchList(req);
|
||
}
|
||
|
||
public RiskApplicationDetail GetApplicationDetail(long applicationId)
|
||
{
|
||
var detail = (from app in DbContext.glms_risk_rule_application
|
||
join rule in DbContext.glms_risk_rule on app.RuleId equals rule.id
|
||
where app.id == applicationId && app.Status != RiskRuleStatus.Deleted
|
||
select new RiskApplicationDetail
|
||
{
|
||
Id = app.id,
|
||
ApplicationCode = app.ApplicationCode,
|
||
RuleId = app.RuleId,
|
||
RuleCode = rule.RuleCode,
|
||
RuleName = rule.RuleName,
|
||
FormulaJson = rule.FormulaJson,
|
||
FormulaText = rule.FormulaText,
|
||
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()
|
||
}).FirstOrDefault();
|
||
|
||
if (detail == null)
|
||
throw new ServiceException("应用配置不存在或已删除");
|
||
|
||
return detail;
|
||
}
|
||
|
||
public RiskApplicationDetail CreateApplication(CreateRiskApplicationReq req)
|
||
{
|
||
if (req.RuleId <= 0)
|
||
throw new ServiceException("关联规则 ID 不合法");
|
||
|
||
var rule = DbContext.glms_risk_rule.FirstOrDefault(r => r.id == req.RuleId && r.Status != RiskRuleStatus.Deleted);
|
||
if (rule == null)
|
||
throw new ServiceException("关联规则不存在或已删除");
|
||
if (rule.Status == RiskRuleStatus.Disabled)
|
||
throw new ServiceException("关联规则已停用,无法创建应用配置");
|
||
|
||
ValidateTriggerPoints(req.TriggerPoints);
|
||
ValidateScopeFields(req);
|
||
|
||
var applicationCode = GenerateCode("APP", DbContext.glms_risk_rule_application.Select(a => a.ApplicationCode));
|
||
|
||
var entity = new glms_risk_rule_application
|
||
{
|
||
ApplicationCode = applicationCode,
|
||
RuleId = req.RuleId,
|
||
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, entity.ApplicationCode, entity.ApplicationCode,
|
||
$"创建应用配置:策略={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.ExpectedVersion)
|
||
throw new ServiceException("应用配置已被其他用户修改,请重新加载后再编辑");
|
||
|
||
ValidateTriggerPoints(req.TriggerPoints);
|
||
var scopeReq = new CreateRiskApplicationReq
|
||
{
|
||
ScopeAssetBookIds = req.ScopeAssetBookIds,
|
||
ScopeClientIds = req.ScopeClientIds,
|
||
ScopeUnderlyingTypes = req.ScopeUnderlyingTypes,
|
||
ScopeTradeTypes = req.ScopeTradeTypes,
|
||
ScopeIsGlobal = req.ScopeIsGlobal
|
||
};
|
||
ValidateScopeFields(scopeReq);
|
||
|
||
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("APPLICATION_UPDATE", "APPLICATION", applicationId, app.ApplicationCode, app.ApplicationCode,
|
||
$"修改应用配置:策略={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, app.ApplicationCode, app.ApplicationCode, "删除应用配置");
|
||
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("仅已停用的应用配置可以启用");
|
||
|
||
var rule = DbContext.glms_risk_rule.FirstOrDefault(r => r.id == app.RuleId && r.Status != RiskRuleStatus.Deleted);
|
||
if (rule == null)
|
||
throw new ServiceException("关联规则不存在或已删除,无法启用应用配置");
|
||
if (rule.Status == RiskRuleStatus.Disabled)
|
||
throw new ServiceException("关联规则已停用,无法启用应用配置");
|
||
|
||
app.Status = RiskRuleStatus.Active;
|
||
app.Version = app.Version + 1;
|
||
app.UpdateOptId = UserId;
|
||
app.UpdateOptName = UserName;
|
||
app.UpdateDate = DateTime.Now;
|
||
|
||
WriteAuditLog("APP_ENABLE", "APPLICATION", applicationId, app.ApplicationCode, app.ApplicationCode, "启用应用配置");
|
||
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.Version = app.Version + 1;
|
||
app.UpdateOptId = UserId;
|
||
app.UpdateOptName = UserName;
|
||
app.UpdateDate = DateTime.Now;
|
||
|
||
WriteAuditLog("APPLICATION_DISABLE", "APPLICATION", applicationId, app.ApplicationCode, app.ApplicationCode, "停用应用配置");
|
||
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)
|
||
{
|
||
var rule = DbContext.glms_risk_rule.FirstOrDefault(r => r.id == app.RuleId && r.Status != RiskRuleStatus.Deleted);
|
||
if (rule == null)
|
||
throw new ServiceException($"应用配置 '{app.ApplicationCode}' 的关联规则不存在或已删除");
|
||
if (rule.Status != RiskRuleStatus.Active)
|
||
throw new ServiceException($"应用配置 '{app.ApplicationCode}' 的关联规则未生效,无法启用");
|
||
}
|
||
|
||
foreach (var app in apps)
|
||
{
|
||
app.Status = RiskRuleStatus.Active;
|
||
app.Version = app.Version + 1;
|
||
app.UpdateOptId = UserId;
|
||
app.UpdateOptName = UserName;
|
||
app.UpdateDate = DateTime.Now;
|
||
|
||
WriteAuditLog("APPLICATION_BATCH_ENABLE", "APPLICATION", app.id, app.ApplicationCode, app.ApplicationCode, "批量启用应用配置");
|
||
}
|
||
|
||
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.Version = app.Version + 1;
|
||
app.UpdateOptId = UserId;
|
||
app.UpdateOptName = UserName;
|
||
app.UpdateDate = DateTime.Now;
|
||
|
||
WriteAuditLog("APPLICATION_BATCH_DISABLE", "APPLICATION", app.id, app.ApplicationCode, app.ApplicationCode, "批量停用应用配置");
|
||
}
|
||
|
||
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.Keyword))
|
||
{
|
||
query = query.Where(v => v.VariableCode.Contains(req.Keyword)
|
||
|| v.VariableName.Contains(req.Keyword));
|
||
}
|
||
|
||
var result = query.OrderBy(v => v.SortOrder).ThenBy(v => v.VariableCode)
|
||
.Select(v => new RiskVariableListItem
|
||
{
|
||
Id = v.id,
|
||
VariableCode = v.VariableCode,
|
||
VariableName = v.VariableName,
|
||
Category = v.Category,
|
||
DataType = v.DataType,
|
||
Unit = v.Unit,
|
||
ValueDomain = v.ValueDomain,
|
||
Description = v.Description,
|
||
IsImplemented = v.IsImplemented,
|
||
Version = v.Version,
|
||
SortOrder = v.SortOrder
|
||
});
|
||
|
||
return result.ToSearchList(req);
|
||
}
|
||
|
||
public RiskVariableDetail GetVariableDetail(long variableId)
|
||
{
|
||
var variable = GetVariableOrThrow(variableId);
|
||
|
||
var referenceCount = DbContext.glms_risk_rule
|
||
.Count(r => r.Status == RiskRuleStatus.Active && r.FormulaJson.Contains(variable.VariableCode));
|
||
|
||
return new RiskVariableDetail
|
||
{
|
||
Id = variable.id,
|
||
VariableCode = variable.VariableCode,
|
||
VariableName = variable.VariableName,
|
||
Category = variable.Category,
|
||
DataType = variable.DataType,
|
||
Unit = variable.Unit,
|
||
ValueDomain = variable.ValueDomain,
|
||
Description = variable.Description,
|
||
ImplementationScript = variable.ImplementationScript,
|
||
SourceExpression = variable.SourceExpression,
|
||
IsImplemented = variable.IsImplemented,
|
||
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.VariableCode))
|
||
throw new ServiceException("变量编码不能为空");
|
||
if (string.IsNullOrWhiteSpace(req.VariableName))
|
||
throw new ServiceException("变量名称不能为空");
|
||
if (string.IsNullOrWhiteSpace(req.ImplementationScript))
|
||
throw new ServiceException("实现脚本不能为空");
|
||
if (req.ImplementationScript.Length > 10000)
|
||
throw new ServiceException("实现脚本长度不能超过 10000 字符");
|
||
|
||
var exists = DbContext.glms_risk_variable.Any(v => v.VariableCode == req.VariableCode);
|
||
if (exists)
|
||
throw new ServiceException($"变量编码 '{req.VariableCode}' 已存在");
|
||
|
||
var entity = new glms_risk_variable
|
||
{
|
||
VariableCode = req.VariableCode,
|
||
VariableName = req.VariableName,
|
||
Category = req.Category,
|
||
DataType = req.DataType,
|
||
Unit = req.Unit,
|
||
ValueDomain = req.ValueDomain,
|
||
Description = req.Description,
|
||
ImplementationScript = req.ImplementationScript,
|
||
SourceExpression = req.SourceExpression,
|
||
IsImplemented = req.IsImplemented,
|
||
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.VariableCode, $"创建变量:{req.VariableCode}",
|
||
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.ExpectedVersion)
|
||
throw new ServiceException("变量已被其他用户修改,请重新加载后再编辑");
|
||
|
||
if (string.IsNullOrWhiteSpace(req.VariableName))
|
||
throw new ServiceException("变量名称不能为空");
|
||
if (string.IsNullOrWhiteSpace(req.ImplementationScript))
|
||
throw new ServiceException("实现脚本不能为空");
|
||
|
||
if (req.DataType != variable.DataType)
|
||
{
|
||
var activeRefCount = DbContext.glms_risk_rule
|
||
.Count(r => r.Status == RiskRuleStatus.Active && r.FormulaJson.Contains(variable.VariableCode));
|
||
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.ImplementationScript = req.ImplementationScript;
|
||
variable.SourceExpression = req.SourceExpression;
|
||
variable.IsImplemented = req.IsImplemented;
|
||
variable.SortOrder = req.SortOrder;
|
||
variable.Version = variable.Version + 1;
|
||
variable.UpdateOptId = UserId;
|
||
variable.UpdateOptName = UserName;
|
||
variable.UpdateDate = DateTime.Now;
|
||
|
||
WriteAuditLog("VARIABLE_UPDATE", "VARIABLE", variableId, req.VariableName, variable.VariableCode, $"修改变量:{variable.VariableCode}");
|
||
DbContext.SaveChanges();
|
||
|
||
InvalidateVariableCache();
|
||
TryRefreshCache();
|
||
|
||
return GetVariableDetail(variableId);
|
||
}
|
||
|
||
public void DeleteVariable(long variableId)
|
||
{
|
||
var variable = GetVariableOrThrow(variableId);
|
||
|
||
var activeRefCount = DbContext.glms_risk_rule
|
||
.Count(r => r.Status == RiskRuleStatus.Active && r.FormulaJson.Contains(variable.VariableCode));
|
||
if (activeRefCount > 0)
|
||
throw new ServiceException($"该变量被 {activeRefCount} 个生效中的规则引用,无法删除");
|
||
|
||
DbContext.glms_risk_variable.Remove(variable);
|
||
|
||
WriteAuditLog("VAR_DELETE", "VARIABLE", variableId, variable.VariableName, variable.VariableCode, $"删除变量:{variable.VariableCode}");
|
||
DbContext.SaveChanges();
|
||
|
||
InvalidateVariableCache();
|
||
TryRefreshCache();
|
||
}
|
||
|
||
public List<RiskVariableSimpleItem> GetAllVariableList()
|
||
{
|
||
var result = DbContext.glms_risk_variable
|
||
.Where(v => v.IsImplemented)
|
||
.OrderBy(v => v.SortOrder)
|
||
.Select(v => new RiskVariableSimpleItem
|
||
{
|
||
Id = v.id,
|
||
VariableCode = v.VariableCode,
|
||
VariableName = v.VariableName,
|
||
Category = v.Category,
|
||
DataType = v.DataType,
|
||
Unit = v.Unit
|
||
})
|
||
.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.TargetCode.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,
|
||
TargetCode = l.TargetCode,
|
||
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,
|
||
TargetCode = log.TargetCode,
|
||
OperationDetail = log.OperationDetail,
|
||
Result = log.Result,
|
||
SnapshotData = log.SnapshotData,
|
||
OptId = log.OptId,
|
||
OptName = log.OptName,
|
||
OptDate = log.OptDate
|
||
};
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
}
|