@@ -53,7 +53,7 @@ using YLErp.Modules.RiskEngine.Dto;
│ - ConditionJson 完整校验(结构化模式:变量存在性、操作符、类型匹配)│
│ - RuleExpr 基础语法校验(自由文本模式:括号匹配、变量引用校验) │
│ - 乐观锁并发控制(Version 字段) │
│ - 删除保护(有 Active 引用时拒绝删除) │
│ - 规则删除同步解除应用关联;变量被任一未删除规则引用时禁止删除 │
│ - CRUD 后触发 RiskEngineService.RefreshCache() │
│ │
│ RiskEngineService(另一团队开发,单例) │
@@ -76,26 +76,26 @@ using YLErp.Modules.RiskEngine.Dto;
GetRuleDetail(long) 获取规则详情(含关联应用摘要)
CreateRule(CreateRiskRuleReq) 新建规则(ConditionJson 完整校验)
UpdateRule(long, UpdateRiskRuleReq) 修改规则(乐观锁校验,Version+1)
DeleteRule(long) 删除规则(强保护:有 Active 应用时拒绝 )
DeleteRule(long) 删除规则(从未删除应用中移除关联 )
EnableRule(long) 启用规则(仅 Disabled → Active)
DisableRule(long) 停用规则(仅 Active → Disabled, 级联停用关联应用 )
DisableRule(long) 停用规则(仅 Active → Disabled, 不修改应用状态 )
GetRuleVersions(long) 获取规则版本历史(从审计日志 SnapshotData 提取版本号)
BatchDeleteRules(List<long>) 批量删除规则(逐条保护检查,单事务 )
BatchDeleteRules(List<long>) 批量删除规则(同步移除应用关联,单次提交 )
BatchEnableRules(List<long>) 批量启用规则(逐条编译校验)
BatchDisableRules(List<long>) 批量停用规则(级联停用关联应用 )
BatchDisableRules(List<long>) 批量停用规则(不修改应用状态 )
GetRuleApplications(long) 获取规则关联的应用配置列表
GetAllRuleList() 获取所有启用 规则(轻量字段,供应用配置下拉)
GetAllRuleList() 获取所有未删除 规则(轻量字段,供应用配置下拉)
── 应用配置管理(9 个) ───────────────────────────────────────────
QueryApplicationList(QueryRiskApplicationReq) 查询应用列表(多条件筛选,内存关联规则名称 )
QueryApplicationList(QueryRiskApplicationReq) 查询应用列表(分页后批量计算规则统计 )
GetApplicationDetail(long) 获取应用详情(含关联规则信息)
CreateApplication(CreateRiskApplicationReq) 新建应用(关联规则 Active 校验,触发时点/Scope 校验)
UpdateApplication(long, UpdateRiskApplicationReq) 修改应用(乐观锁校验)
DeleteApplication(long) 删除应用(软删除)
BatchDeleteApplications(List<long>) 批量删除应用(软删除)
EnableApplication(long) 启用应用(校验关联规则状态 )
EnableApplication(long) 启用应用(至少一条关联规则 Active )
DisableApplication(long) 停用应用
BatchEnableApplications(List<long>) 批量启用(逐条校验关联 规则)
BatchEnableApplications(List<long>) 批量启用(逐条校验至少一条 Active 规则)
BatchDisableApplications(List<long>) 批量停用
── 变量管理(6 个) ───────────────────────────────────────────────
@@ -103,7 +103,7 @@ using YLErp.Modules.RiskEngine.Dto;
GetVariableDetail(long) 获取变量详情(含 ReferenceCount 引用统计)
CreateVariable(CreateRiskVariableReq) 新建变量(VariableExpr 长度校验 ≤10000)
UpdateVariable(long, UpdateRiskVariableReq) 修改变量(乐观锁,DataType 变更保护)
DeleteVariable(long) 删除变量(引用保护:有 Active 规则引用时拒绝,硬 删除)
DeleteVariable(long) 删除变量(引用保护:有未删除 规则引用时拒绝,软 删除)
GetAllVariableList() 获取所有已实现变量(轻量字段,供规则编辑器下拉)
── 审计日志(4 个) ───────────────────────────────────────────────
@@ -128,14 +128,14 @@ using YLErp.Modules.RiskEngine.Dto;
1. 数据访问:直接使用 EF Core DbContext(无独立 Repository,与项目风格一致)
2. 事务策略:Create* 方法使用 BeginTransaction 包裹双 SaveChanges(实体 + 审计日志同事务)
3. 并发控制:乐观锁(Version 字段),修改时版本不匹配抛 ServiceException
4. 删除保护:强保护模式,有 Active 引用时拒绝 删除规则/变量
4. 删除策略:规则删除同步从应用移除;变量删除保护所有未 删除规则引用
5. ConditionJson 校验:变量存在性(按 VariableId) + 操作符合法性(按 DataType 映射)+ 类型匹配 + 阈值变量引用
6. RuleExpr:自由文本模式下前端直接编辑,后端做基础语法校验(括号匹配 + 变量引用校验)
7. Scope 字段:逗号分隔字符串存储,全局适用时不允许指定其他维度
8. 缓存刷新:规则变更后调用 RefreshOneRuleCache(ruleId),应用变更后调用 RefreshApplication()
9. 批量操作:单事务批量,全部成功或全部失败
9. 批量操作:逐项返回成功/未变更/失败,成功项统一提交
10. 变量缓存:5 分钟本地 HashSet<string> 缓存,CRUD 后 InvalidateVariableCache()
11. 审计日志:16 种操作类型, CRUD + 审计日志同一事务 , SnapshotData 存储版本快照
11. 审计日志:CRUD + 审计日志同次提交 , SnapshotData 存储版本快照
12. 多规则支持:Application 通过 RuleIds(逗号分隔)关联多条规则,内存解析关联关系
【数据库表】(MySQL,迁移脚本位于 Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/prod.sql)
@@ -181,7 +181,7 @@ using YLErp.Modules.RiskEngine.Dto;
【注意事项】
1. RiskEngineService 由另一个团队开发实现,本服务通过 GetInstance() 获取单例引用
2. 规则变更后调用 RefreshOneRuleCache(ruleId),应用变更后调用 RefreshApplication(),变量变更不触发引擎缓存刷新
3. 变量删除为硬删除(物理删除), 规则/应用删除为软删除(Status=Deleted)
3. 变量/ 规则/应用删除均 为软删除(Status=Deleted) ,新建变量默认 Active
4. ExportAuditLogs 按查询条件导出,并生成包含查询条件的文件名
5. VariableExpr 当前仅做基础长度校验(≤10000),完整编译校验待引入 Roslyn 库
6. ValidateScopeFields 的 ID 存在性校验已跳过(前端下拉选择器保证有效性)
@@ -225,7 +225,9 @@ namespace YLErp.Modules.RiskEngine
{ "APP_BATCH_ENABLE" , "批量启用应用" } ,
{ "APP_BATCH_DISABLE" , "批量停用应用" } ,
{ "APP_BATCH_DELETE" , "批量删除应用" } ,
// 仅用于历史审计日志展示;解耦后不再写入该操作类型。
{ "RULE_CASCADE_DISABLE_APP" , "规则级联停用应用" } ,
{ "RULE_REMOVE_FROM_APP" , "规则从应用移除" } ,
{ "VAR_CREATE" , "新增变量" } ,
{ "VARIABLE_CREATE" , "新增变量" } ,
{ "VAR_UPDATE" , "修改变量" } ,
@@ -504,9 +506,10 @@ namespace YLErp.Modules.RiskEngine
/// </summary>
private glms_risk_variable GetVariableOrThrow ( long variableId )
{
var variable = DbContext . glms_risk_variable . FirstOrDefault ( v = > v . id = = variableId ) ;
var variable = DbContext . glms_risk_variable
. FirstOrDefault ( v = > v . id = = variableId & & v . Status ! = RiskRuleStatus . Deleted ) ;
if ( variable = = null )
throw new ServiceException ( "变量不存在" ) ;
throw new ServiceException ( "变量不存在或已删除 " ) ;
return variable ;
}
@@ -559,6 +562,84 @@ namespace YLErp.Modules.RiskEngine
return ids ;
}
/// <summary>
/// 解析数据库中已保存的规则 ID。删除应用关联的最后一条规则后允许为空。
/// </summary>
private static List < long > ParseStoredRuleIds ( string ruleIds )
{
if ( string . IsNullOrWhiteSpace ( ruleIds ) )
return new List < long > ( ) ;
var ids = new List < long > ( ) ;
foreach ( var part in ruleIds . Split ( ',' ) )
{
var value = part . Trim ( ) ;
if ( string . IsNullOrEmpty ( value ) )
continue ;
if ( ! long . TryParse ( value , out var id ) )
throw new ServiceException ( $"规则 ID 列表格式不合法:'{value}'" ) ;
ids . Add ( id ) ;
}
return ids . Distinct ( ) . ToList ( ) ;
}
private bool HasAnyActiveRule ( string ruleIds )
{
var ids = ParseStoredRuleIds ( ruleIds ) ;
return ids . Count > 0 & & DbContext . glms_risk_rule
. Any ( r = > ids . Contains ( r . id ) & & r . Status = = RiskRuleStatus . Active ) ;
}
private Dictionary < long , RiskRuleStatus > LoadRuleStatuses ( IEnumerable < string > storedRuleIds )
{
var ids = storedRuleIds . SelectMany ( ParseStoredRuleIds ) . Distinct ( ) . ToList ( ) ;
if ( ids . Count = = 0 )
return new Dictionary < long , RiskRuleStatus > ( ) ;
return DbContext . glms_risk_rule
. Where ( r = > ids . Contains ( r . id ) )
. Select ( r = > new { r . id , r . Status } )
. ToDictionary ( r = > ( long ) r . id , r = > r . Status ) ;
}
private static void PopulateApplicationAvailability (
RiskApplicationListItem item , IReadOnlyDictionary < long , RiskRuleStatus > ruleStatuses )
{
var statuses = ParseStoredRuleIds ( item . RuleIds )
. Where ( ruleStatuses . ContainsKey )
. Select ( id = > ruleStatuses [ id ] )
. ToList ( ) ;
item . AssociatedRuleCount = statuses . Count ( status = > status ! = RiskRuleStatus . Deleted ) ;
item . EffectiveRuleCount = statuses . Count ( status = > status = = RiskRuleStatus . Active ) ;
item . CanEnable = item . Status = = ( int ) RiskRuleStatus . Disabled & & item . EffectiveRuleCount > 0 ;
item . CanDisable = item . Status = = ( int ) RiskRuleStatus . Active & & item . EffectiveRuleCount > 0 ;
item . EnableButtonTooltip = item . Status = = ( int ) RiskRuleStatus . Disabled & & ! item . CanEnable
? "该应用下所有规则均已全局停用,无法启用应用。请先在规则管理中启用至少一条规则"
: string . Empty ;
item . DisableButtonTooltip = item . Status = = ( int ) RiskRuleStatus . Active & & ! item . CanDisable
? "该应用下所有规则均已全局停用,应用无法生效。请先在规则管理中启用至少一条关联规则"
: string . Empty ;
}
private static void PopulateApplicationAvailability (
RiskApplicationDetail item , IReadOnlyDictionary < long , RiskRuleStatus > ruleStatuses )
{
var statuses = ParseStoredRuleIds ( item . RuleIds )
. Where ( ruleStatuses . ContainsKey )
. Select ( id = > ruleStatuses [ id ] )
. ToList ( ) ;
item . AssociatedRuleCount = statuses . Count ( status = > status ! = RiskRuleStatus . Deleted ) ;
item . EffectiveRuleCount = statuses . Count ( status = > status = = RiskRuleStatus . Active ) ;
item . CanEnable = item . Status = = RiskRuleStatus . Disabled & & item . EffectiveRuleCount > 0 ;
item . CanDisable = item . Status = = RiskRuleStatus . Active & & item . EffectiveRuleCount > 0 ;
item . EnableButtonTooltip = item . Status = = RiskRuleStatus . Disabled & & ! item . CanEnable
? "该应用下所有规则均已全局停用,无法启用应用。请先在规则管理中启用至少一条规则"
: string . Empty ;
item . DisableButtonTooltip = item . Status = = RiskRuleStatus . Active & & ! item . CanDisable
? "该应用下所有规则均已全局停用,应用无法生效。请先在规则管理中启用至少一条关联规则"
: string . Empty ;
}
/// <summary>
/// 规范化逗号分隔的规则 ID:去除空格和重复项,统一为无空格格式。
/// </summary>
@@ -723,7 +804,8 @@ namespace YLErp.Modules.RiskEngine
}
private static BatchOperationItemResult BatchItem ( long id , BatchOperationItemStatus status ,
string code , string message , IEnumerable < long > cascadedApplicationIds = null )
string code , string message , IEnumerable < long > affectedApplicationIds = null ,
IEnumerable < long > causedNoAvailableRuleApplicationIds = null )
{
return new BatchOperationItemResult
{
@@ -731,7 +813,9 @@ namespace YLErp.Modules.RiskEngine
Status = status ,
Code = code ,
Message = message ,
CascadedApplicationIds = cascadedApplicationIds ? . Distinct ( ) . ToList ( ) ? ? new List < long > ( )
AffectedApplicationIds = affectedApplicationIds ? . Distinct ( ) . ToList ( ) ? ? new List < long > ( ) ,
CausedNoAvailableRuleApplicationIds = causedNoAvailableRuleApplicationIds ? . Distinct ( ) . ToList ( ) ? ? new List < long > ( ) ,
CascadedApplicationIds = new List < long > ( )
} ;
}
@@ -747,7 +831,9 @@ namespace YLErp.Modules.RiskEngine
SucceededCount = items . Count ( i = > i . Status = = BatchOperationItemStatus . Succeeded ) ,
UnchangedCount = items . Count ( i = > i . Status = = BatchOperationItemStatus . Unchanged ) ,
FailedCount = items . Count ( i = > i . Status = = BatchOperationItemStatus . Failed ) ,
CascadedApplicationIds = items . SelectMany ( i = > i . CascadedApplicationIds ) . Distinct ( ) . ToList ( )
AffectedApplicationIds = items . SelectMany ( i = > i . AffectedApplicationIds ) . Distinct ( ) . ToList ( ) ,
CausedNoAvailableRuleApplicationIds = items . SelectMany ( i = > i . CausedNoAvailableRuleApplicationIds ) . Distinct ( ) . ToList ( ) ,
CascadedApplicationIds = new List < long > ( )
} ;
result . HasChanges = result . SucceededCount > 0 ;
result . Outcome = result . FailedCount = = 0
@@ -974,28 +1060,57 @@ namespace YLErp.Modules.RiskEngine
}
/// <summary>
/// 删除规则(软删除,有 Active 应用引用时拒绝 )
/// 删除规则(软删除,并从所有未删除应用中移除关联 )
/// </summary>
public void DeleteRule ( long ruleId )
public DeleteRuleResult DeleteRule ( long ruleId )
{
var rule = GetRuleOrThrow ( ruleId ) ;
var activeCount = DbContext . glms_risk_rule_application
. Where ( a = > a . Status = = RiskRuleStatus . Active )
. Count ( RuleIdsMatchExpr ( ruleId ) ) ;
if ( activeCount > 0 )
throw new ServiceException ( $"该规则仍有 {activeCount} 个生效中的应用配置,请先停用或删除相关应用" ) ;
var affectedApps = DbContext . glms_risk_rule_application
. Where ( a = > a . Status ! = RiskRuleStatus . Deleted )
. Where ( RuleIdsMatchExpr ( ruleId ) )
. ToList ( ) ;
var affectedActiveAppCount = affectedApps . Count ( a = > a . Status = = RiskRuleStatus . Active ) ;
var activeRuleIds = DbContext . glms_risk_rule
. Where ( r = > r . Status = = RiskRuleStatus . Active & & r . id ! = ruleId )
. Select ( r = > ( long ) r . id )
. ToHashSet ( ) ;
var causedNoAvailableRuleAppIds = new List < long > ( ) ;
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 , "删除规则" ) ;
SaveChangesWithConcurrencyCheck ( ) ;
RiskEngineService . GetInstance ( ) . RefreshOneRuleCache ( ruleId ) ;
foreach ( var app in affectedApps )
{
var remainingIds = ParseStoredRuleIds ( app . RuleIds ) . Where ( id = > id ! = ruleId ) . ToList ( ) ;
app . RuleIds = string . Join ( "," , remainingIds ) ;
app . Version + + ;
app . UpdateOptId = UserId ;
app . UpdateOptName = UserName ;
app . UpdateDate = DateTime . Now ;
if ( ! remainingIds . Any ( activeRuleIds . Contains ) )
causedNoAvailableRuleAppIds . Add ( app . id ) ;
WriteAuditLog ( "RULE_REMOVE_FROM_APP" , "APPLICATION" , app . id , $"应用{app.id}" ,
$"删除规则'{rule.RuleName}'(ID:{ruleId}),已从应用关联规则中移除" ) ;
}
SaveChangesWithConcurrencyCheck ( ) ;
TryRefreshCache ( ( ) = > RiskEngineService . GetInstance ( ) . RefreshOneRuleCache ( ruleId ) ,
"风控规则缓存" , new [ ] { ruleId } ) ;
if ( affectedApps . Any ( ) )
TryRefreshCache ( ( ) = > RiskEngineService . GetInstance ( ) . RefreshApplication ( ) ,
"风控应用缓存" , affectedApps . Select ( a = > ( long ) a . id ) ) ;
return new DeleteRuleResult
{
RuleId = ruleId ,
AffectedActiveAppCount = affectedActiveAppCount ,
CausedNoAvailableRuleAppIds = causedNoAvailableRuleAppIds . Distinct ( ) . ToList ( )
} ;
}
/// <summary>
@@ -1025,7 +1140,7 @@ namespace YLErp.Modules.RiskEngine
}
/// <summary>
/// 停用规则(仅 Active → Disabled)
/// 停用规则(仅 Active → Disabled,不修改关联应用状态 )
/// </summary>
public DisableRuleResult DisableRule ( long ruleId )
{
@@ -1033,47 +1148,24 @@ namespace YLErp.Modules.RiskEngine
if ( rule = = null )
throw new ServiceException ( "仅已启用的规则可以停用" ) ;
rule . Status = RiskRuleStatus . Disabled ;
var affectedActiveAppCount = DbContext . glms_risk_rule_application
. Where ( a = > a . Status = = RiskRuleStatus . Active )
. Count ( RuleIdsMatchExpr ( ruleId ) ) ;
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 , "停用规则" ) ;
// 级联停用关联的 Active 应用配置
var activeApps = DbContext . glms_risk_rule_application
. Where ( a = > a . Status = = RiskRuleStatus . Active )
. Where ( RuleIdsMatchExpr ( ruleId ) )
. ToList ( ) ;
var cascadedIds = new List < long > ( ) ;
foreach ( var app in activeApps )
{
app . Status = RiskRuleStatus . Disabled ;
app . Version = app . Version + 1 ;
app . UpdateOptId = UserId ;
app . UpdateOptName = UserName ;
app . UpdateDate = DateTime . Now ;
cascadedIds . Add ( app . id ) ;
WriteAuditLog ( "RULE_CASCADE_DISABLE_APP" , "APPLICATION" , app . id , $"应用{app.id}" ,
$"因规则'{rule.RuleName}'(ID:{ruleId})停用,应用配置被级联停用" ) ;
}
if ( cascadedIds . Any ( ) )
{
_logger . Info ( $"[停用规则] 规则ID: {ruleId}, 级联停用应用: {string.Join(" , ", cascadedIds)}" ) ;
}
SaveChangesWithConcurrencyCheck ( ) ;
RiskEngineService . GetInstance ( ) . RefreshOneRuleCache ( ruleId ) ;
if ( cascadedIds . Any ( ) )
RiskEngineService . GetInstance ( ) . RefreshApplication ( ) ;
return new DisableRuleResult { CascadedApplicationIds = cascadedIds } ;
return new DisableRuleResult
{
RuleId = ruleId ,
AffectedActiveAppCount = affectedActiveAppCount
} ;
}
/// <summary>
@@ -1117,7 +1209,7 @@ namespace YLErp.Modules.RiskEngine
}
/// <summary>
/// 批量删除规则(逐条保护检查)
/// 批量删除规则,并从所有未删除应用中移除成功删除的规则关联
/// </summary>
public BatchOperationResult BatchDeleteRules ( List < long > ruleIds )
{
@@ -1126,45 +1218,75 @@ namespace YLErp.Modules.RiskEngine
var ids = DistinctIds ( ruleIds ) ;
var rules = DbContext . glms_risk_rule . Where ( r = > ids . Contains ( r . id ) ) . ToDictionary ( r = > ( long ) r . id ) ;
var activeApps = DbContext . glms_risk_rule_application
. Where ( a = > a . Status = = RiskRuleStatus . Active )
var changedIds = ids
. Where ( id = > rules . TryGetValue ( id , out var rule ) & & rule . Status ! = RiskRuleStatus . Deleted )
. ToList ( ) ;
var items = new List < BatchOperationItemResult > ( ) ;
var changedIds = new List < long > ( ) ;
var changedIdSet = changedIds . ToHashSet ( ) ;
var affectedApps = changedIds . Count = = 0
? new List < glms_risk_rule_application > ( )
: DbContext . glms_risk_rule_application
. Where ( a = > a . Status ! = RiskRuleStatus . Deleted & & a . RuleIds ! = null & & a . RuleIds ! = "" )
. ToList ( )
. Where ( a = > ParseStoredRuleIds ( a . RuleIds ) . Any ( changedIdSet . Contains ) )
. ToList ( ) ;
var affectedByRule = changedIds . ToDictionary ( id = > id , _ = > new List < long > ( ) ) ;
var noAvailableByRule = changedIds . ToDictionary ( id = > id , _ = > new List < long > ( ) ) ;
var activeRuleIdsAfterDelete = DbContext . glms_risk_rule
. Where ( r = > r . Status = = RiskRuleStatus . Active & & ! changedIds . Contains ( r . id ) )
. Select ( r = > ( long ) r . id )
. ToHashSet ( ) ;
foreach ( var app in affectedApps )
{
var currentIds = ParseStoredRuleIds ( app . RuleIds ) ;
var removedIds = currentIds . Where ( changedIdSet . Contains ) . Distinct ( ) . ToList ( ) ;
var remainingIds = currentIds . Where ( id = > ! changedIdSet . Contains ( id ) ) . ToList ( ) ;
app . RuleIds = string . Join ( "," , remainingIds ) ;
app . Version + + ;
app . UpdateOptId = UserId ;
app . UpdateOptName = UserName ;
app . UpdateDate = DateTime . Now ;
var hasActiveRule = remainingIds . Any ( activeRuleIdsAfterDelete . Contains ) ;
foreach ( var removedId in removedIds )
{
affectedByRule [ removedId ] . Add ( app . id ) ;
if ( ! hasActiveRule )
noAvailableByRule [ removedId ] . Add ( app . id ) ;
WriteAuditLog ( "RULE_REMOVE_FROM_APP" , "APPLICATION" , app . id , $"应用{app.id}" ,
$"批量删除规则'{rules[removedId].RuleName}'(ID:{removedId}),已从应用关联规则中移除" ) ;
}
}
var items = new List < BatchOperationItemResult > ( ) ;
foreach ( var id in ids )
{
if ( ! rules . TryGetValue ( id , out var rule ) | | rule . Status = = RiskRuleStatus . Deleted )
if ( ! changedIdSet . Contains ( id ) )
{
items . Add ( BatchItem ( id , BatchOperationItemStatus . Failed , "NOT_FOUND" , "规则不存在或已删除" ) ) ;
continue ;
}
var referenceCount = activeApps . Count ( a = > ParseRuleIds ( a . RuleIds ) . Contains ( id ) ) ;
if ( referenceCount > 0 )
{
items . Add ( BatchItem ( id , BatchOperationItemStatus . Failed , "ACTIVE_APPLICATION_REFERENCE" ,
$"该规则仍有 {referenceCount} 个生效中的应用配置,请先停用或删除相关应用" ) ) ;
continue ;
}
var rule = rules [ id ] ;
rule . Status = RiskRuleStatus . Deleted ;
rule . Version + + ;
rule . UpdateOptId = UserId ;
rule . UpdateOptName = UserName ;
rule . UpdateDate = DateTime . Now ;
WriteAuditLog ( "RULE_BATCH_DELETE" , "RULE" , rule . id , rule . RuleName , "批量删除规则" ) ;
changedIds . Add ( id ) ;
items . Add ( BatchItem ( id , BatchOperationItemStatus . Succeeded , "DELETED" , "规则已删除" ) ) ;
items . Add ( BatchItem ( id , BatchOperationItemStatus . Succeeded , "DELETED" , "规则已删除" ,
affectedByRule [ id ] , noAvailableByRule [ id ] ) ) ;
}
if ( changedIds . Any ( ) )
{
// 所有成功项一次原子提交;提交完成后,缓存失败仅告警,不改变数据库成功结果。
SaveChangesWithConcurrencyCheck ( ) ;
TryRefreshCache (
( ) = > changedIds . ForEach ( id = > RiskEngineService . GetInstance ( ) . RefreshOneRuleCache ( id ) ) ,
"风控规则缓存" , changedIds ) ;
if ( affectedApps . Any ( ) )
TryRefreshCache ( ( ) = > RiskEngineService . GetInstance ( ) . RefreshApplication ( ) ,
"风控应用缓存" , affectedApps . Select ( a = > ( long ) a . id ) ) ;
}
var result = BuildBatchResult ( items ) ;
@@ -1231,7 +1353,7 @@ namespace YLErp.Modules.RiskEngine
}
/// <summary>
/// 批量停用规则(仅 Active → Disabled, 级联停用 关联应用)
/// 批量停用规则(仅 Active → Disabled, 不修改 关联应用状态 )
/// </summary>
public BatchOperationResult BatchDisableRules ( List < long > ruleIds )
{
@@ -1240,14 +1362,11 @@ namespace YLErp.Modules.RiskEngine
var ids = DistinctIds ( ruleIds ) ;
var rules = DbContext . glms_risk_rule . Where ( r = > ids . Contains ( r . id ) ) . ToDictionary ( r = > ( long ) r . id ) ;
var activeRules = ids . Where ( id = > rules . TryGetValue ( id , out var rule ) & & rule . Status = = RiskRuleStatus . Active ) . ToList ( ) ;
var activeApps = DbContext . glms_risk_rule_application
. Where ( a = > a . Status = = RiskRuleStatus . Active )
. Where ( a = > a . Status = = RiskRuleStatus . Active & & a . RuleIds ! = null & & a . RuleIds ! = "" )
. ToList ( ) ;
var cascadesByRule = activeRules . ToDictionary (
id = > id ,
id = > activeApps . Where ( a = > ParseRuleIds ( a . RuleIds ) . Contains ( id ) ) . Select ( a = > ( long ) a . id ) . Distinct ( ) . ToList ( ) ) ;
var items = new List < BatchOperationItemResult > ( ) ;
var changedIds = new List < long > ( ) ;
foreach ( var id in ids )
{
@@ -1262,38 +1381,28 @@ namespace YLErp.Modules.RiskEngine
continue ;
}
var affectedApplicationIds = activeApps
. Where ( a = > ParseStoredRuleIds ( a . RuleIds ) . Contains ( id ) )
. Select ( a = > ( long ) a . id )
. Distinct ( )
. ToList ( ) ;
rule . Status = RiskRuleStatus . Disabled ;
rule . Version + + ;
rule . UpdateOptId = UserId ;
rule . UpdateOptName = UserName ;
rule . UpdateDate = DateTime . Now ;
WriteAuditLog ( "RULE_BATCH_DISABLE" , "RULE" , rule . id , rule . RuleName , "批量停用规则" ) ;
items . Add ( BatchItem ( id , BatchOperationItemStatus . Succeeded , "DISABLED" , "规则已停用" , cascadesByRule [ id ] ) ) ;
changedIds . Add ( id ) ;
items . Add ( BatchItem ( id , BatchOperationItemStatus . Succeeded , "DISABLED" , "规则已停用" ,
affectedApplicationIds ) ) ;
}
// 多条规则可能关联同一应用;实际状态修改、审计和顶层级联结果均按应用 ID 去重。
var cascadedIds = items . SelectMany ( i = > i . CascadedApplicationIds ) . Distinct ( ) . ToHashSet ( ) ;
foreach ( var app in activeApps . Where ( a = > cascadedIds . Contains ( a . id ) ) )
if ( changedIds . Any ( ) )
{
app . Status = RiskRuleStatus . Disabled ;
app . Version + + ;
app . UpdateOptId = UserId ;
app . UpdateOptName = UserName ;
app . UpdateDate = DateTime . Now ;
WriteAuditLog ( "RULE_CASCADE_DISABLE_APP" , "APPLICATION" , app . id , $"应用{app.id}" ,
$"因规则批量停用,应用配置被级联停用(规则IDs:{string.Join(" , ", activeRules.Where(id => cascadesByRule[id].Contains(app.id)))})" ) ;
}
if ( items . Any ( i = > i . Status = = BatchOperationItemStatus . Succeeded ) )
{
// 规则及其级联应用在同一次数据库提交中生效。
SaveChangesWithConcurrencyCheck ( ) ;
TryRefreshCache (
( ) = > activeRules . ForEach ( id = > RiskEngineService . GetInstance ( ) . RefreshOneRuleCache ( id ) ) ,
"风控规则缓存" , activeRules ) ;
if ( cascadedIds . Any ( ) )
TryRefreshCache ( ( ) = > RiskEngineService . GetInstance ( ) . RefreshApplication ( ) ,
"风控应用缓存" , cascadedIds ) ;
( ) = > changedIds . ForEach ( id = > RiskEngineService . GetInstance ( ) . RefreshOneRuleCache ( id ) ) ,
"风控规则缓存" , changedIds ) ;
}
var result = BuildBatchResult ( items ) ;
@@ -1313,14 +1422,15 @@ namespace YLErp.Modules.RiskEngine
. Where ( RuleIdsMatchExpr ( ruleId ) )
. OrderByDescending ( a = > a . UpdateDate )
. ToList ( ) ;
var ruleStatuses = LoadRuleStatuses ( apps . Select ( a = > a . RuleIds ) ) ;
var result = new List < RiskApplicationListItem > ( ) ;
foreach ( var app in apps )
{
result . Add ( new RiskApplicationListItem
var item = new RiskApplicationListItem
{
Id = app . id ,
RuleIds = app . RuleIds ,
Description = app . Description ,
Status = ( int ) app . Status ,
ControlStrategy = app . ControlStrategy ,
TriggerPoints = app . TriggerPoints ,
@@ -1334,7 +1444,9 @@ namespace YLErp.Modules.RiskEngine
OptDate = app . OptDate . GetValueOrDefault ( ) ,
UpdateOptName = app . UpdateOptName ,
UpdateDate = app . UpdateDate ? ? app . OptDate . GetValueOrDefault ( )
} ) ;
} ;
PopulateApplicationAvailability ( item , ruleStatuses ) ;
result . Add ( item ) ;
}
return result ;
@@ -1363,7 +1475,7 @@ namespace YLErp.Modules.RiskEngine
#region Application Management
/// <summary>
/// 查询应用配置列表(多条件筛选,内存 关联规则名称 )
/// 查询应用配置列表(多条件筛选,批量计算 关联规则统计 )
/// </summary>
public SearchListResult < RiskApplicationListItem > QueryApplicationList ( QueryRiskApplicationReq req )
{
@@ -1371,20 +1483,11 @@ namespace YLErp.Modules.RiskEngine
. Where ( a = > a . Status ! = RiskRuleStatus . Deleted ) ;
if ( req . Status . HasValue )
{
query = query . Where ( a = > a . Status = = req . Status . Value ) ;
}
if ( req . ControlStrategy . HasValue )
{
query = query . Where ( a = > a . ControlStrategy = = req . ControlStrategy . Value ) ;
}
if ( ! string . IsNullOrWhiteSpace ( req . TriggerPoint ) )
{
query = query . Where ( a = > a . TriggerPoints . Contains ( req . TriggerPoint ) ) ;
}
if ( ! string . IsNullOrWhiteSpace ( req . Keyword ) )
{
query = query . Where ( a = > DbContext . glms_risk_rule
@@ -1396,16 +1499,12 @@ namespace YLErp.Modules.RiskEngine
| | a . RuleIds . EndsWith ( "," + r . id . ToString ( ) )
| | a . RuleIds . Contains ( "," + r . id . ToString ( ) + "," ) ) ) ) ;
}
if ( ! string . IsNullOrWhiteSpace ( req . ScopeClientIds ) )
query = ApplyScopeFilter ( query , a = > a . ScopeClientIds , req . ScopeClientIds ) ;
if ( ! string . IsNullOrWhiteSpace ( req . ScopeUnderlyingTypes ) )
query = ApplyScopeFilter ( query , a = > a . ScopeUnderlyingTypes , req . ScopeUnderlyingTypes ) ;
if ( ! string . IsNullOrWhiteSpace ( req . ScopeTradeTypes ) )
query = ApplyScopeFilter ( query , a = > a . ScopeTradeTypes , req . ScopeTradeTypes ) ;
if ( ! string . IsNullOrWhiteSpace ( req . ScopeAssetBookIds ) )
query = ApplyScopeFilter ( query , a = > a . ScopeAssetBookIds , req . ScopeAssetBookIds ) ;
@@ -1431,11 +1530,16 @@ namespace YLErp.Modules.RiskEngine
} )
. ToSearchList ( req ) ;
var items = pagedResult . rows ? . ToList ( ) ? ? new List < RiskApplicationListItem > ( ) ;
var ruleStatuses = LoadRuleStatuses ( items . Select ( item = > item . RuleIds ) ) ;
foreach ( var item in items )
PopulateApplicationAvailability ( item , ruleStatuses ) ;
pagedResult . rows = items ;
return pagedResult ;
}
/// <summary>
/// 获取应用配置详情(含关联规则信息)
/// 获取应用配置详情(含关联规则信息和可用状态 )
/// </summary>
public RiskApplicationDetail GetApplicationDetail ( long applicationId )
{
@@ -1444,7 +1548,7 @@ namespace YLErp.Modules.RiskEngine
if ( app = = null )
throw new ServiceException ( "应用配置不存在或已删除" ) ;
return new RiskApplicationDetail
var result = new RiskApplicationDetail
{
Id = app . id ,
RuleIds = app . RuleIds ,
@@ -1463,6 +1567,8 @@ namespace YLErp.Modules.RiskEngine
UpdateOptName = app . UpdateOptName ,
UpdateDate = app . UpdateDate ? ? app . OptDate . GetValueOrDefault ( )
} ;
PopulateApplicationAvailability ( result , LoadRuleStatuses ( new [ ] { app . RuleIds } ) ) ;
return result ;
}
/// <summary>
@@ -1636,7 +1742,7 @@ namespace YLErp.Modules.RiskEngine
}
/// <summary>
/// 启用应用配置(校验 关联规则状态 )
/// 启用应用配置(至少存在一条 Active 关联规则)
/// </summary>
public void EnableApplication ( long applicationId )
{
@@ -1644,12 +1750,10 @@ namespace YLErp.Modules.RiskEngine
if ( app = = null )
throw new ServiceException ( "仅已停用的应用配置可以启用" ) ;
var errors = ValidateAndEnsureRuleCache ( app . RuleIds ) ;
if ( errors . Any ( ) )
throw new ServiceException ( $"以下关联规则不可用:{string.Join(" ; ", errors)}" ) ;
if ( ! HasAnyActiveRule ( app . RuleIds ) )
throw new ServiceException ( "该应用下所有规则均已停用,无法启用应用。请先在规则管理中启用至少一条关联规则" ) ;
app . Status = RiskRuleStatus . Active ;
app . Version = app . Version + 1 ;
app . UpdateOptId = UserId ;
app . UpdateOptName = UserName ;
@@ -1657,7 +1761,6 @@ namespace YLErp.Modules.RiskEngine
WriteAuditLog ( "APP_ENABLE" , "APPLICATION" , applicationId , ResolveRuleNames ( app . RuleIds ) , "启用应用配置" ) ;
SaveChangesWithConcurrencyCheck ( ) ;
RiskEngineService . GetInstance ( ) . RefreshApplication ( ) ;
}
@@ -1684,7 +1787,7 @@ namespace YLErp.Modules.RiskEngine
}
/// <summary>
/// 批量启用应用配置(逐条校验关联规则)
/// 批量启用应用配置(逐条校验至少一条 关联规则已启用 )
/// </summary>
public BatchOperationResult BatchEnableApplications ( List < long > applicationIds )
{
@@ -1713,11 +1816,10 @@ namespace YLErp.Modules.RiskEngine
continue ;
}
var errors = ValidateAndEnsureRuleCache ( app . RuleIds ) ;
if ( errors . Any ( ) )
if ( ! HasAnyActiveRule ( app . RuleIds ) )
{
items . Add ( BatchItem ( id , BatchOperationItemStatus . Failed , "RELATED_RULE_UNAVAILAB LE" ,
$"关联规则不可用:{string.Join(" ; ", errors)} ") ) ;
items . Add ( BatchItem ( id , BatchOperationItemStatus . Failed , "NO_ACTIVE_RU LE" ,
"该应用下所有规则均已停用,无法启用应用。请先在规则管理中启用至少一条关联规则 ") ) ;
continue ;
}
@@ -1806,6 +1908,20 @@ namespace YLErp.Modules.RiskEngine
. ToList ( ) ;
}
/// <summary>
/// 生成变量删除被阻止时的引用规则提示。
/// </summary>
private static string BuildVariableDeleteReferenceMessage ( IEnumerable < glms_risk_rule > referencedRules )
{
var ruleIds = referencedRules
. Select ( rule = > rule . id )
. Distinct ( )
. OrderBy ( id = > id )
. ToList ( ) ;
return $"该变量被 {ruleIds.Count} 个规则引用(规则ID: {string.Join(" 、 ", ruleIds)}),无法删除" ;
}
/// <summary>
/// 根据结构化 ConditionJson 和当前变量池定义重新生成简洁 RuleExpr。
/// </summary>
@@ -1814,7 +1930,7 @@ namespace YLErp.Modules.RiskEngine
var conditions = RuleConditionExpressionBuilder . DeserializeConditions ( conditionJson ) ;
var variableIds = RuleConditionExpressionBuilder . GetReferencedVariableIds ( conditions ) . ToList ( ) ;
var variables = DbContext . glms_risk_variable
. Where ( variable = > variableIds . Contains ( variable . id ) )
. Where ( variable = > variableIds . Contains ( variable . id ) & & variable . Status ! = RiskRuleStatus . Deleted )
. ToList ( )
. ToDictionary ( variable = > ( long ) variable . id ) ;
@@ -1831,7 +1947,8 @@ namespace YLErp.Modules.RiskEngine
/// </summary>
public SearchListResult < RiskVariableListItem > QueryVariableList ( QueryRiskVariableReq req )
{
var query = DbContext . glms_risk_variable . AsQueryable ( ) ;
var query = DbContext . glms_risk_variable
. Where ( v = > v . Status ! = RiskRuleStatus . Deleted ) ;
if ( req . Category . HasValue )
{
@@ -1848,6 +1965,11 @@ namespace YLErp.Modules.RiskEngine
query = query . Where ( v = > v . VariableName . Contains ( req . VariableName ) ) ;
}
if ( req . Status . HasValue )
{
query = query . Where ( v = > v . Status = = req . Status . Value ) ;
}
var result = query . OrderBy ( v = > v . SortOrder ) . ThenBy ( v = > v . VariableName )
. Select ( v = > new RiskVariableListItem
{
@@ -1860,7 +1982,8 @@ namespace YLErp.Modules.RiskEngine
Description = v . Description ,
VariableExpr = v . VariableExpr ,
Version = v . Version ,
SortOrder = v . SortOrder
SortOrder = v . SortOrder ,
Status = v . Status
} ) ;
return result . ToSearchList ( req ) ;
@@ -1888,6 +2011,7 @@ namespace YLErp.Modules.RiskEngine
VariableExpr = variable . VariableExpr ,
Version = variable . Version ,
SortOrder = variable . SortOrder ,
Status = variable . Status ,
OptName = variable . OptName ,
OptDate = variable . OptDate . GetValueOrDefault ( ) ,
UpdateOptName = variable . UpdateOptName ,
@@ -1917,6 +2041,7 @@ namespace YLErp.Modules.RiskEngine
ValueDomain = req . ValueDomain ,
Description = req . Description ,
VariableExpr = req . VariableExpr ,
Status = RiskRuleStatus . Active ,
SortOrder = req . SortOrder ,
Version = 1
} ;
@@ -2061,18 +2186,21 @@ namespace YLErp.Modules.RiskEngine
}
/// <summary>
/// 删除变量(有 Active 引用时拒绝)
/// 删除变量(软删除:有未删除规则 引用时拒绝)
/// </summary>
public void DeleteVariable ( long variableId )
{
var variable = GetVariableOrThrow ( variableId ) ;
var activeRefCount = GetRulesByVariableId ( variableId )
. Count ( r = > r . Status = = RiskRuleStatus . Active ) ;
if ( activeRefCount > 0 )
throw new ServiceException ( $"该变量被 {activeRefCount} 个生效中的规则引用,无法删除" ) ;
var referencedRules = GetRulesByVariableId ( variableId ) ;
if ( referencedRules . Count > 0 )
throw new ServiceException ( BuildVariableDeleteReferenceMessage ( referencedRules ) ) ;
DbContext . glms_risk_variable . Remove ( variable ) ;
variable . Status = RiskRuleStatus . Deleted ;
variable . Version = variable . Version + 1 ;
variable . UpdateOptId = UserId ;
variable . UpdateOptName = UserName ;
variable . UpdateDate = DateTime . Now ;
WriteAuditLog ( "VAR_DELETE" , "VARIABLE" , variableId , variable . VariableName , $"删除变量:{variable.VariableName}" ) ;
SaveChangesWithConcurrencyCheck ( ) ;
@@ -2081,7 +2209,7 @@ namespace YLErp.Modules.RiskEngine
}
/// <summary>
/// 批量删除变量(不存在或被生效 规则引用的变量将被跳过)
/// 批量删除变量(不存在或被未删除 规则引用的变量将被跳过)
/// </summary>
public BatchOperationResult BatchDeleteVariables ( List < long > variableIds )
{
@@ -2089,9 +2217,11 @@ namespace YLErp.Modules.RiskEngine
throw new ServiceException ( "变量ID列表不能为空" ) ;
var ids = DistinctIds ( variableIds ) ;
var variables = DbContext . glms_risk_variable . Where ( v = > ids . Contains ( v . id ) ) . ToDictionary ( v = > ( long ) v . id ) ;
var activeRules = DbContext . glms_risk_rule
. Where ( r = > r . Status = = RiskRuleStatus . Active & & r . ConditionJson ! = null )
var variables = DbContext . glms_risk_variable
. Where ( v = > ids . Contains ( v . id ) & & v . Status ! = RiskRuleStatus . Deleted )
. ToDictionary ( v = > ( long ) v . id ) ;
var rules = DbContext . glms_risk_rule
. Where ( r = > r . Status ! = RiskRuleStatus . Deleted & & r . ConditionJson ! = null & & r . ConditionJson ! = "" )
. ToList ( ) ;
var items = new List < BatchOperationItemResult > ( ) ;
@@ -2099,20 +2229,25 @@ namespace YLErp.Modules.RiskEngine
{
if ( ! variables . TryGetValue ( id , out var variable ) )
{
items . Add ( BatchItem ( id , BatchOperationItemStatus . Failed , "NOT_FOUND" , "变量不存在" ) ) ;
items . Add ( BatchItem ( id , BatchOperationItemStatus . Failed , "NOT_FOUND" , "变量不存在或已删除 " ) ) ;
continue ;
}
var activeRefCount = activeRules . Count ( rule = >
RuleConditionExpressionBuilder . IsVariableReferenced ( rule . ConditionJson , id ) ) ;
if ( activeRefCount > 0 )
var referencedRules = rules . Where ( rule = >
RuleConditionExpressionBuilder . IsVariableReferenced ( rule . ConditionJson , id ) )
. ToList ( ) ;
if ( referencedRules . Count > 0 )
{
items . Add ( BatchItem ( id , BatchOperationItemStatus . Failed , "ACTIVE_ RULE_REFERENCE" ,
$"该变量被 {activeRefCount} 个生效中的规则引用,无法删除" ) ) ;
items . Add ( BatchItem ( id , BatchOperationItemStatus . Failed , "RULE_REFERENCE" ,
BuildVariableDeleteReferenceMessage ( referencedRules ) ) ) ;
continue ;
}
DbContext . glms_risk_variable . Remove ( variable ) ;
variable . Status = RiskRuleStatus . Deleted ;
variable . Version = variable . Version + 1 ;
variable . UpdateOptId = UserId ;
variable . UpdateOptName = UserName ;
variable . UpdateDate = DateTime . Now ;
WriteAuditLog ( "VAR_BATCH_DELETE" , "VARIABLE" , variable . id , variable . VariableName , $"批量删除变量:{variable.VariableName}" ) ;
items . Add ( BatchItem ( id , BatchOperationItemStatus . Succeeded , "DELETED" , "变量已删除" ) ) ;
}
@@ -2135,7 +2270,7 @@ namespace YLErp.Modules.RiskEngine
public List < RiskVariableSimpleItem > GetAllVariableList ( )
{
var result = DbContext . glms_risk_variable
. Where ( v = > v . VariableExpr ! = null & & v . VariableExpr ! = "" )
. Where ( v = > v . VariableExpr ! = null & & v . VariableExpr ! = "" & & v . Status ! = RiskRuleStatus . Deleted )
. OrderBy ( v = > v . SortOrder )
. Select ( v = > new RiskVariableSimpleItem
{
@@ -2144,7 +2279,8 @@ namespace YLErp.Modules.RiskEngine
Category = v . Category ,
DataType = v . DataType ,
Unit = v . Unit ,
VariableExpr = v . VariableExpr
VariableExpr = v . VariableExpr ,
Status = v . Status
} )
. ToList ( ) ;
@@ -2331,4 +2467,4 @@ namespace YLErp.Modules.RiskEngine
#endregion
}
}
}