@@ -1,9 +1,10 @@
using BaseOUDAL ;
using BaseOUDAL ;
using Newtonsoft.Json ;
using System.Linq ;
using System.Linq.Expressions ;
using System.Text.RegularExpressions ;
using YLErp.BLL ;
using YLErp.Commons ;
using YLErp.DBModels ;
using YLErp.Modules.RiskEngine.Dto ;
@@ -76,9 +77,11 @@ using YLErp.Modules.RiskEngine.Dto;
UpdateRule(long, UpdateRiskRuleReq) 修改规则(乐观锁校验,Version+1)
DeleteRule(long) 删除规则(强保护:有 Active 应用时拒绝)
EnableRule(long) 启用规则(仅 Disabled → Active)
DisableRule(long) 停用规则(仅 Active → Disabled)
DisableRule(long) 停用规则(仅 Active → Disabled,级联停用关联应用 )
GetRuleVersions(long) 获取规则版本历史(从审计日志 SnapshotData 提取版本号)
BatchDeleteRules(List<long>) 批量删除规则(逐条保护检查,单事务)
BatchEnableRules(List<long>) 批量启用规则(逐条编译校验)
BatchDisableRules(List<long>) 批量停用规则(级联停用关联应用)
GetRuleApplications(long) 获取规则关联的应用配置列表
GetAllRuleList() 获取所有启用规则(轻量字段,供应用配置下拉)
@@ -88,6 +91,7 @@ using YLErp.Modules.RiskEngine.Dto;
CreateApplication(CreateRiskApplicationReq) 新建应用(关联规则 Active 校验,触发时点/Scope 校验)
UpdateApplication(long, UpdateRiskApplicationReq) 修改应用(乐观锁校验)
DeleteApplication(long) 删除应用(软删除)
BatchDeleteApplications(List<long>) 批量删除应用(软删除)
EnableApplication(long) 启用应用(校验关联规则状态)
DisableApplication(long) 停用应用
BatchEnableApplications(List<long>) 批量启用(逐条校验关联规则)
@@ -207,6 +211,39 @@ namespace YLErp.Modules.RiskEngine
{ RiskVariableDataType . Boolean , new HashSet < string > { "是" , "否" } }
} ;
private static readonly Dictionary < string , string > AuditOperationTypeNames = new Dictionary < string , string > ( StringComparer . OrdinalIgnoreCase )
{
{ "RULE_CREATE" , "新建规则" } ,
{ "RULE_UPDATE" , "修改规则" } ,
{ "RULE_DELETE" , "删除规则" } ,
{ "RULE_ENABLE" , "启用规则" } ,
{ "RULE_DISABLE" , "停用规则" } ,
{ "RULE_BATCH_DELETE" , "批量删除规则" } ,
{ "RULE_BATCH_ENABLE" , "批量启用规则" } ,
{ "RULE_BATCH_DISABLE" , "批量停用规则" } ,
{ "APP_CREATE" , "新建应用" } ,
{ "APP_UPDATE" , "修改应用" } ,
{ "APP_DELETE" , "删除应用" } ,
{ "APP_ENABLE" , "启用应用" } ,
{ "APP_DISABLE" , "停用应用" } ,
{ "APP_BATCH_ENABLE" , "批量启用应用" } ,
{ "APP_BATCH_DISABLE" , "批量停用应用" } ,
{ "APP_BATCH_DELETE" , "批量删除应用" } ,
{ "RULE_CASCADE_DISABLE_APP" , "规则级联停用应用" } ,
{ "VAR_CREATE" , "新增变量" } ,
{ "VARIABLE_CREATE" , "新增变量" } ,
{ "VAR_UPDATE" , "修改变量" } ,
{ "VAR_DELETE" , "删除变量" } ,
{ "VAR_CASCADE_UPDATE_RULE" , "变量级联更新规则" }
} ;
private static readonly Dictionary < string , string > AuditTargetTypeNames = new Dictionary < string , string > ( StringComparer . OrdinalIgnoreCase )
{
{ "RULE" , "规则" } ,
{ "APPLICATION" , "应用" } ,
{ "VARIABLE" , "变量" }
} ;
public RiskRuleService ( OptUserInfo userInfo ) : base ( userInfo )
{
}
@@ -221,6 +258,39 @@ namespace YLErp.Modules.RiskEngine
#region Private Helpers
private static string GetAuditOperationTypeName ( string operationType )
{
if ( string . IsNullOrWhiteSpace ( operationType ) )
return string . Empty ;
return AuditOperationTypeNames . TryGetValue ( operationType . Trim ( ) , out var name )
? name
: operationType ;
}
private static string GetAuditTargetTypeName ( string targetType )
{
if ( string . IsNullOrWhiteSpace ( targetType ) )
return string . Empty ;
return AuditTargetTypeNames . TryGetValue ( targetType . Trim ( ) , out var name )
? name
: targetType ;
}
private static string FormatAuditId ( object value )
{
return value = = null ? string . Empty : value . ToString ( ) ;
}
private static string SanitizeFileNamePart ( string value )
{
if ( string . IsNullOrWhiteSpace ( value ) )
return string . Empty ;
var sanitized = Regex . Replace ( value . Trim ( ) , @"[\\/:*?""<>|\r\n]+" , "_" ) ;
return sanitized . Length < = 40 ? sanitized : sanitized . Substring ( 0 , 40 ) ;
}
/// <summary>
/// 写入审计日志
/// </summary>
@@ -398,24 +468,32 @@ namespace YLErp.Modules.RiskEngine
/// </summary>
private void ValidateScopeFields ( CreateRiskApplicationReq req )
{
var hasAnyScope =
! string . IsNullOrWhiteSpace ( req . ScopeAssetBookIds ) | |
! string . IsNullOrWhiteSpace ( req . ScopeClientIds ) | |
! string . IsNullOrWhiteSpace ( req . ScopeUnderlyingTypes ) | |
! string . IsNullOrWhiteSpace ( req . ScopeTradeTypes ) ;
if ( req . ScopeIsGlobal = = 1 )
{
if ( ! string . IsNullOrEmpty ( req . ScopeAssetBookIds ) | |
! string . IsNullOrEmpty ( req . ScopeClientIds ) | |
! string . IsNullOrEmpty ( req . ScopeUnderlyingTypes ) | |
! string . IsNullOrEmpty ( req . ScopeTradeTypes ) )
if ( hasAnyScope )
{
throw new ServiceException ( "全局适用时,不应指定其他适用范围维度" ) ;
}
return ;
}
if ( ! string . IsNullOrEmpty ( req . ScopeAssetBookIds ) )
if ( ! hasAnyScope )
{
throw new ServiceException ( "非全局应用至少需要指定一个适用范围" ) ;
}
if ( ! string . IsNullOrWhiteSpace ( req . ScopeAssetBookIds ) )
{
ValidateIdList ( req . ScopeAssetBookIds , "适用账户" ) ;
}
if ( ! string . IsNullOrEmpty ( req . ScopeClientIds ) )
if ( ! string . IsNullOrWhiteSpace ( req . ScopeClientIds ) )
{
ValidateIdList ( req . ScopeClientIds , "适用对手方" ) ;
}
@@ -540,6 +618,27 @@ namespace YLErp.Modules.RiskEngine
return ids ;
}
/// <summary>
/// 规范化逗号分隔的规则 ID:去除空格和重复项,统一为无空格格式。
/// </summary>
private string NormalizeRuleIds ( string ruleIds )
{
return string . Join ( "," , ParseRuleIds ( ruleIds ) . Distinct ( ) ) ;
}
/// <summary>
/// EF Core 可翻译的逗号分隔字段精确匹配表达式
/// </summary>
private static Expression < Func < glms_risk_rule_application , bool > > RuleIdsMatchExpr ( long ruleId )
{
var idStr = ruleId . ToString ( ) ;
return a = > a . RuleIds ! = null & &
( a . RuleIds = = idStr | |
a . RuleIds . StartsWith ( idStr + "," ) | |
a . RuleIds . EndsWith ( "," + idStr ) | |
a . RuleIds . Contains ( "," + idStr + "," ) ) ;
}
/// <summary>
/// 校验规则状态可用性并确保编译缓存就绪。
/// 返回不可用规则信息列表,列表为空表示全部通过。
@@ -722,7 +821,12 @@ namespace YLErp.Modules.RiskEngine
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 )
ApplicationCount = DbContext . glms_risk_rule_application
. Count ( a = > a . RuleIds ! = null & &
( a . RuleIds = = r . id . ToString ( ) | |
a . RuleIds . StartsWith ( r . id . ToString ( ) + "," ) | |
a . RuleIds . EndsWith ( "," + r . id . ToString ( ) ) | |
a . RuleIds . Contains ( "," + r . id . ToString ( ) + "," ) ) )
} ) ;
return result . ToSearchList ( req ) ;
@@ -735,9 +839,9 @@ namespace YLErp.Modules.RiskEngine
{
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 )
. Where ( RuleIdsMatchExpr ( ruleId ) )
. Select ( a = > new RiskRuleApplicationSummary
{
Id = a . id ,
@@ -858,9 +962,10 @@ namespace YLErp.Modules.RiskEngine
{
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 ) ;
. Where ( a = > a . Status = = RiskRuleStatus . Active )
. Count ( RuleIdsMatchExpr ( ruleId ) ) ;
if ( activeCount > 0 )
throw new ServiceException ( $"该规则仍有 {activeCount} 个生效中的应用配置,请先停用或删除相关应用" ) ;
@@ -903,7 +1008,7 @@ namespace YLErp.Modules.RiskEngine
/// <summary>
/// 停用规则(仅 Active → Disabled)
/// </summary>
public void DisableRule ( long ruleId )
public DisableRuleResult DisableRule ( long ruleId )
{
var rule = DbContext . glms_risk_rule . FirstOrDefault ( r = > r . id = = ruleId & & r . Status = = RiskRuleStatus . Active ) ;
if ( rule = = null )
@@ -915,9 +1020,38 @@ namespace YLErp.Modules.RiskEngine
rule . UpdateDate = DateTime . Now ;
WriteAuditLog ( "RULE_DISABLE" , "RULE" , ruleId , rule . RuleName , "停用规则" ) ;
// 级联停用关联的 Active 应用配置
var activeApps = DbContext . glms_risk_rule_application
. Where ( a = > a . Status = = RiskRuleStatus . Active )
. Where ( RuleIdsMatchExpr ( ruleId ) )
. ToList ( ) ;
var cascadedIds = new List < long > ( ) ;
foreach ( var app in activeApps )
{
app . Status = RiskRuleStatus . Disabled ;
app . UpdateOptId = UserId ;
app . UpdateOptName = UserName ;
app . UpdateDate = DateTime . Now ;
cascadedIds . Add ( app . id ) ;
WriteAuditLog ( "RULE_CASCADE_DISABLE_APP" , "APPLICATION" , app . id , $"应用{app.id}" ,
$"因规则'{rule.RuleName}'(ID:{ruleId})停用,应用配置被级联停用" ) ;
}
if ( cascadedIds . Any ( ) )
{
_logger . Info ( $"[停用规则] 规则ID: {ruleId}, 级联停用应用: {string.Join(" , ", cascadedIds)}" ) ;
}
DbContext . SaveChanges ( ) ;
RiskEngineService . GetInstance ( ) . RefreshOneRuleCache ( ruleId ) ;
if ( cascadedIds . Any ( ) )
RiskEngineService . GetInstance ( ) . RefreshApplication ( ) ;
return new DisableRuleResult { CascadedApplicationIds = cascadedIds } ;
}
/// <summary>
@@ -965,24 +1099,58 @@ namespace YLErp.Modules.RiskEngine
/// </summary>
public BatchOperationResult BatchDeleteRules ( List < long > ruleIds )
{
if ( ruleIds = = null | | ruleIds . Count = = 0 )
throw new ServiceException ( "规则ID列表不能为空" ) ;
_logger . Info ( $"[批量删除] 开始 - 传入规则数: {ruleIds.Count}, IDs: {string.Join(" , ", ruleIds)}" ) ;
var rules = DbContext . glms_risk_rule
. Where ( r = > ruleIds . Contains ( r . id ) & & r . Status ! = RiskRuleStatus . Deleted )
. ToList ( ) ;
if ( rules . Count ! = ruleIds . Count )
throw new ServiceException ( "部分规则不存在或已删除" ) ;
_logger . Info ( $"[批量删除] DB查询完成 - 匹配到 {rules.Count} 条规则" ) ;
var foundIds = rules . Select ( r = > ( long ) r . id ) . ToList ( ) ;
var missingIds = ruleIds . Except ( foundIds ) . ToList ( ) ;
if ( missingIds . Any ( ) )
{
return new BatchOperationResult
{
Success = false ,
TotalCount = ruleIds . Count ,
SuccessCount = 0 ,
ErrorMessage = $"以下规则不存在或已删除:{string.Join(" , ", missingIds)}"
} ;
}
var blockedRules = new List < string > ( ) ;
foreach ( var rule in rules )
{
var ruleIdStr = rule . id . ToString ( ) ;
var activeCount = DbContext . glms_risk_rule_application
. Count ( a = > a . RuleIds . Contains ( ruleIdStr ) & & a . Status = = RiskRuleStatus . Active ) ;
_logger . Info ( $"[批量删除] 检查关联应用 - 规则ID: {rule.id}" ) ;
var activeApps = DbContext . glms_risk_rule_application
. Where ( a = > a . Status = = RiskRuleStatus . Active )
. Where ( RuleIdsMatchExpr ( rule . id ) )
. Select ( a = > ( long ) a . id )
. ToList ( ) ;
var activeCount = activeApps . Count ;
_logger . Info ( $"[批量删除] 关联应用检查结果 - 规则ID: {rule.id}, ActiveCount: {activeCount}, AppIds: [{string.Join(" , ", activeApps)}]" ) ;
if ( activeCount > 0 )
throw new ServiceException ( $"规则 '{rule.RuleName}' 仍有 {activeCount} 个生效中的应用配置,请先停用或删除相关应用 " ) ;
blockedRules . Add ( $"规则 '{rule.RuleName}'(ID:{rule.id}) 仍有 {activeCount} 个生效中的应用配置(IDs: {string.Join(" , ", activeApps)}) ") ;
}
if ( blockedRules . Any ( ) )
{
return new BatchOperationResult
{
Success = false ,
TotalCount = ruleIds . Count ,
SuccessCount = 0 ,
ErrorMessage = string . Join ( "; " , blockedRules )
} ;
}
foreach ( var rule in rules )
{
_logger . Info ( $"[批量删除] 写入状态 - 规则ID: {rule.id}" ) ;
rule . Status = RiskRuleStatus . Deleted ;
rule . Version = rule . Version + 1 ;
rule . UpdateOptId = UserId ;
@@ -992,11 +1160,14 @@ namespace YLErp.Modules.RiskEngine
WriteAuditLog ( "RULE_BATCH_DELETE" , "RULE" , rule . id , rule . RuleName , "批量删除规则" ) ;
}
_logger . Info ( "[批量删除] 保存数据库..." ) ;
DbContext . SaveChanges ( ) ;
_logger . Info ( "[批量删除] 刷新规则缓存..." ) ;
foreach ( var ruleId in ruleIds )
{
RiskEngineService . GetInstance ( ) . RefreshOneRuleCache ( ruleId ) ;
}
_logger . Info ( "[批量删除] 完成" ) ;
return new BatchOperationResult
{
@@ -1006,6 +1177,210 @@ namespace YLErp.Modules.RiskEngine
} ;
}
/// <summary>
/// 批量启用规则(仅 Disabled → Active,逐条编译校验)
/// </summary>
public BatchOperationResult BatchEnableRules ( List < long > ruleIds )
{
if ( ruleIds = = null | | ruleIds . Count = = 0 )
throw new ServiceException ( "规则ID列表不能为空" ) ;
_logger . Info ( $"[批量启用规则] 开始 - 传入规则数: {ruleIds.Count}, IDs: {string.Join(" , ", ruleIds)}" ) ;
var rules = DbContext . glms_risk_rule
. Where ( r = > ruleIds . Contains ( r . id ) & & r . Status ! = RiskRuleStatus . Deleted )
. ToList ( ) ;
_logger . Info ( $"[批量启用规则] DB查询完成 - 匹配到 {rules.Count} 条规则" ) ;
var foundIds = rules . Select ( r = > ( long ) r . id ) . ToList ( ) ;
var missingIds = ruleIds . Except ( foundIds ) . ToList ( ) ;
if ( missingIds . Any ( ) )
{
return new BatchOperationResult
{
Success = false ,
TotalCount = ruleIds . Count ,
SuccessCount = 0 ,
ErrorMessage = $"以下规则不存在或已删除:{string.Join(" , ", missingIds)}"
} ;
}
// 跳过已启用的规则
var skippedIds = rules . Where ( r = > r . Status = = RiskRuleStatus . Active ) . Select ( r = > ( long ) r . id ) . ToList ( ) ;
if ( skippedIds . Any ( ) )
_logger . Info ( $"[批量启用规则] 跳过已启用规则 - IDs: {string.Join(" , ", skippedIds)}" ) ;
rules = rules . Where ( r = > r . Status ! = RiskRuleStatus . Active ) . ToList ( ) ;
if ( ! rules . Any ( ) )
{
_logger . Info ( "[批量启用规则] 所有规则均已启用,无需操作" ) ;
return new BatchOperationResult
{
Success = true ,
TotalCount = ruleIds . Count ,
SuccessCount = ruleIds . Count
} ;
}
// 逐条编译校验,任一失败则整体拒绝
var errors = new List < string > ( ) ;
foreach ( var rule in rules )
{
var compileResult = RuleCompiler . ValidateAndCompileFormula ( rule . id , rule . RuleExpr ) ;
if ( ! compileResult . Success )
errors . Add ( $"规则'{rule.RuleName}'(ID:{rule.id}): {compileResult.ErrorMessage}" ) ;
}
if ( errors . Any ( ) )
{
return new BatchOperationResult
{
Success = false ,
TotalCount = ruleIds . Count ,
SuccessCount = 0 ,
ErrorMessage = $"规则表达式编译失败:{string.Join(" ; ", errors)}"
} ;
}
foreach ( var rule in rules )
{
_logger . Info ( $"[批量启用规则] 写入状态 - 规则ID: {rule.id}" ) ;
rule . Status = RiskRuleStatus . Active ;
rule . UpdateOptId = UserId ;
rule . UpdateOptName = UserName ;
rule . UpdateDate = DateTime . Now ;
}
var enabledIds = rules . Select ( r = > r . id ) . ToList ( ) ;
WriteAuditLog ( "RULE_BATCH_ENABLE" , "RULE" , 0 , "" ,
$"批量启用规则(IDs: {string.Join(" , ", enabledIds)})" ) ;
_logger . Info ( "[批量启用规则] 保存数据库..." ) ;
DbContext . SaveChanges ( ) ;
_logger . Info ( "[批量启用规则] 刷新规则缓存..." ) ;
foreach ( var rule in rules )
{
RiskEngineService . GetInstance ( ) . RefreshOneRuleCache ( rule . id ) ;
}
_logger . Info ( "[批量启用规则] 完成" ) ;
return new BatchOperationResult
{
Success = true ,
TotalCount = ruleIds . Count ,
SuccessCount = ruleIds . Count
} ;
}
/// <summary>
/// 批量停用规则(仅 Active → Disabled,级联停用关联应用)
/// </summary>
public BatchDisableRulesResult BatchDisableRules ( List < long > ruleIds )
{
if ( ruleIds = = null | | ruleIds . Count = = 0 )
throw new ServiceException ( "规则ID列表不能为空" ) ;
_logger . Info ( $"[批量停用规则] 开始 - 传入规则数: {ruleIds.Count}, IDs: {string.Join(" , ", ruleIds)}" ) ;
var rules = DbContext . glms_risk_rule
. Where ( r = > ruleIds . Contains ( r . id ) & & r . Status ! = RiskRuleStatus . Deleted )
. ToList ( ) ;
_logger . Info ( $"[批量停用规则] DB查询完成 - 匹配到 {rules.Count} 条规则" ) ;
var foundIds = rules . Select ( r = > ( long ) r . id ) . ToList ( ) ;
var missingIds = ruleIds . Except ( foundIds ) . ToList ( ) ;
if ( missingIds . Any ( ) )
{
return new BatchDisableRulesResult
{
Success = false ,
TotalCount = ruleIds . Count ,
SuccessCount = 0 ,
ErrorMessage = $"以下规则不存在或已删除:{string.Join(" , ", missingIds)}"
} ;
}
// 跳过已停用的规则
var skippedIds = rules . Where ( r = > r . Status = = RiskRuleStatus . Disabled ) . Select ( r = > ( long ) r . id ) . ToList ( ) ;
if ( skippedIds . Any ( ) )
_logger . Info ( $"[批量停用规则] 跳过已停用规则 - IDs: {string.Join(" , ", skippedIds)}" ) ;
rules = rules . Where ( r = > r . Status ! = RiskRuleStatus . Disabled ) . ToList ( ) ;
if ( ! rules . Any ( ) )
{
_logger . Info ( "[批量停用规则] 所有规则均已停用,无需操作" ) ;
return new BatchDisableRulesResult
{
Success = true ,
TotalCount = ruleIds . Count ,
SuccessCount = ruleIds . Count
} ;
}
var allCascadedAppIds = new List < long > ( ) ;
foreach ( var rule in rules )
{
_logger . Info ( $"[批量停用规则] 写入状态 - 规则ID: {rule.id}" ) ;
rule . Status = RiskRuleStatus . Disabled ;
rule . UpdateOptId = UserId ;
rule . UpdateOptName = UserName ;
rule . UpdateDate = DateTime . Now ;
// 级联停用关联的 Active 应用配置
var activeApps = DbContext . glms_risk_rule_application
. Where ( a = > a . Status = = RiskRuleStatus . Active )
. Where ( RuleIdsMatchExpr ( rule . id ) )
. ToList ( ) ;
var cascadedIds = new List < long > ( ) ;
foreach ( var app in activeApps )
{
// 同一应用可能同时关联多条待停用规则,只对实际仍为 Active 的应用处理一次。
if ( app . Status ! = RiskRuleStatus . Active )
continue ;
app . Status = RiskRuleStatus . Disabled ;
app . UpdateOptId = UserId ;
app . UpdateOptName = UserName ;
app . UpdateDate = DateTime . Now ;
cascadedIds . Add ( app . id ) ;
WriteAuditLog ( "RULE_CASCADE_DISABLE_APP" , "APPLICATION" , app . id , $"应用{app.id}" ,
$"因规则'{rule.RuleName}'(ID:{rule.id})停用,应用配置被级联停用" ) ;
}
if ( cascadedIds . Any ( ) )
{
_logger . Info ( $"[批量停用规则] 规则ID: {rule.id}, 级联停用应用: {string.Join(" , ", cascadedIds)}" ) ;
allCascadedAppIds . AddRange ( cascadedIds ) ;
}
}
var disabledIds = rules . Select ( r = > r . id ) . ToList ( ) ;
WriteAuditLog ( "RULE_BATCH_DISABLE" , "RULE" , 0 , "" ,
$"批量停用规则(IDs: {string.Join(" , ", disabledIds)})" ) ;
_logger . Info ( "[批量停用规则] 保存数据库..." ) ;
DbContext . SaveChanges ( ) ;
_logger . Info ( "[批量停用规则] 刷新规则缓存..." ) ;
foreach ( var rule in rules )
{
RiskEngineService . GetInstance ( ) . RefreshOneRuleCache ( rule . id ) ;
}
if ( allCascadedAppIds . Any ( ) )
RiskEngineService . GetInstance ( ) . RefreshApplication ( ) ;
_logger . Info ( "[批量停用规则] 完成" ) ;
return new BatchDisableRulesResult
{
Success = true ,
TotalCount = ruleIds . Count ,
SuccessCount = ruleIds . Count ,
CascadedApplicationIds = allCascadedAppIds
} ;
}
/// <summary>
/// 获取规则关联的应用配置列表
/// </summary>
@@ -1013,9 +1388,9 @@ namespace YLErp.Modules.RiskEngine
{
GetRuleOrThrow ( ruleId ) ;
var ruleIdStr = ruleId . ToString ( ) ;
var apps = DbContext . glms_risk_rule_application
. Where ( a = > a . RuleIds . Contains ( ruleIdStr ) & & a . Status ! = RiskRuleStatus . Deleted )
. Where ( a = > a . Status ! = RiskRuleStatus . Deleted )
. Where ( RuleIdsMatchExpr ( ruleId ) )
. OrderByDescending ( a = > a . UpdateDate )
. ToList ( ) ;
@@ -1046,18 +1421,19 @@ namespace YLErp.Modules.RiskEngine
}
/// <summary>
/// 获取所有启用 规则列表(轻量字段,供下拉选择)
/// 获取所有未删除 规则列表(轻量字段,供下拉选择)
/// </summary>
public List < RiskRuleSimpleItem > GetAllRuleList ( )
{
return DbContext . glms_risk_rule
. Where ( r = > r . Status = = RiskRuleStatus . Active )
. Where ( r = > r . Status ! = RiskRuleStatus . Deleted )
. OrderBy ( r = > r . id )
. Select ( r = > new RiskRuleSimpleItem
{
Id = r . id ,
RuleName = r . RuleName ,
RuleText = r . RuleText
RuleText = r . RuleText ,
Status = r . Status
} )
. ToList ( ) ;
}
@@ -1093,8 +1469,12 @@ namespace YLErp.Modules.RiskEngine
{
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 ) ) ) ;
& & r . RuleName . Contains ( req . RuleName )
& & a . RuleIds ! = null
& & ( a . RuleIds = = r . id . ToString ( )
| | a . RuleIds . StartsWith ( r . id . ToString ( ) + "," )
| | a . RuleIds . EndsWith ( "," + r . id . ToString ( ) )
| | a . RuleIds . Contains ( "," + r . id . ToString ( ) + "," ) ) ) ) ;
}
if ( ! string . IsNullOrWhiteSpace ( req . ScopeClientIds ) )
@@ -1170,7 +1550,8 @@ namespace YLErp.Modules.RiskEngine
/// </summary>
public RiskApplicationDetail CreateApplication ( CreateRiskApplicationReq req )
{
var errors = ValidateAndEnsureRuleCache ( req . RuleIds ) ;
var normalizedRuleIds = NormalizeRuleIds ( req . RuleIds ) ;
var errors = ValidateAndEnsureRuleCache ( normalizedRuleIds ) ;
if ( errors . Any ( ) )
throw new ServiceException ( $"以下关联规则不可用:{string.Join(" ; ", errors)}" ) ;
ValidateTriggerPoints ( req . TriggerPoints ) ;
@@ -1178,7 +1559,7 @@ namespace YLErp.Modules.RiskEngine
var entity = new glms_risk_rule_application
{
RuleIds = req . RuleIds ,
RuleIds = normalizedRuleIds ,
Description = req . Description ,
Status = RiskRuleStatus . Active ,
ControlStrategy = req . ControlStrategy ,
@@ -1203,7 +1584,7 @@ namespace YLErp.Modules.RiskEngine
DbContext . glms_risk_rule_application . Add ( entity ) ;
DbContext . SaveChanges ( ) ;
WriteAuditLog ( "APP_CREATE" , "APPLICATION" , entity . id , ResolveRuleNames ( req . RuleIds ) ,
WriteAuditLog ( "APP_CREATE" , "APPLICATION" , entity . id , ResolveRuleNames ( entity . RuleIds ) ,
$"创建应用配置:策略={GetStrategyName(req.ControlStrategy)}, 触发时点={MapTriggerPointsToChinese(req.TriggerPoints)}" ,
snapshotData : JsonConvert . SerializeObject ( new { Version = entity . Version } ) ) ;
DbContext . SaveChanges ( ) ;
@@ -1232,9 +1613,11 @@ namespace YLErp.Modules.RiskEngine
if ( app . Version ! = req . Version )
throw new ServiceException ( "应用配置已被其他用户修改,请重新加载后再编辑" ) ;
string normalizedRuleIds = null ;
if ( ! string . IsNullOrWhiteSpace ( req . RuleIds ) )
{
var errors = ValidateAndEnsureRuleCache ( req . RuleIds ) ;
normalizedRuleIds = NormalizeRuleIds ( req . RuleIds ) ;
var errors = ValidateAndEnsureRuleCache ( normalizedRuleIds ) ;
if ( errors . Any ( ) )
throw new ServiceException ( $"以下关联规则不可用:{string.Join(" ; ", errors)}" ) ;
}
@@ -1250,8 +1633,8 @@ namespace YLErp.Modules.RiskEngine
} ;
ValidateScopeFields ( scopeReq ) ;
if ( ! string . IsNullOrWhiteSpace ( req . RuleIds ) )
app . RuleIds = req . RuleIds ;
if ( normalizedRuleIds ! = null )
app . RuleIds = normalizedRuleIds ;
app . Description = req . Description ;
app . ControlStrategy = req . ControlStrategy ;
app . TriggerPoints = req . TriggerPoints ;
@@ -1293,6 +1676,63 @@ namespace YLErp.Modules.RiskEngine
RiskEngineService . GetInstance ( ) . RefreshApplication ( ) ;
}
/// <summary>
/// 批量删除应用配置(软删除)
/// </summary>
public BatchOperationResult BatchDeleteApplications ( List < long > applicationIds )
{
if ( applicationIds = = null | | applicationIds . Count = = 0 )
throw new ServiceException ( "应用配置ID列表不能为空" ) ;
_logger . Info ( $"[批量删除应用] 开始 - 传入应用数: {applicationIds.Count}, IDs: {string.Join(" , ", applicationIds)}" ) ;
var apps = DbContext . glms_risk_rule_application
. Where ( a = > applicationIds . Contains ( a . id ) & & a . Status ! = RiskRuleStatus . Deleted )
. ToList ( ) ;
_logger . Info ( $"[批量删除应用] DB查询完成 - 匹配到 {apps.Count} 条应用" ) ;
var foundIds = apps . Select ( a = > ( long ) a . id ) . ToList ( ) ;
var missingIds = applicationIds . Except ( foundIds ) . ToList ( ) ;
if ( missingIds . Any ( ) )
{
return new BatchOperationResult
{
Success = false ,
TotalCount = applicationIds . Count ,
SuccessCount = 0 ,
ErrorMessage = $"以下应用配置不存在或已删除:{string.Join(" , ", missingIds)}"
} ;
}
foreach ( var app in apps )
{
_logger . Info ( $"[批量删除应用] 写入状态 - 应用ID: {app.id}" ) ;
app . Status = RiskRuleStatus . Deleted ;
app . Version = app . Version + 1 ;
app . UpdateOptId = UserId ;
app . UpdateOptName = UserName ;
app . UpdateDate = DateTime . Now ;
}
var deletedIds = apps . Select ( a = > a . id ) . ToList ( ) ;
WriteAuditLog ( "APP_BATCH_DELETE" , "APPLICATION" , 0 , "" ,
$"批量删除应用配置(IDs: {string.Join(" , ", deletedIds)})" ) ;
_logger . Info ( "[批量删除应用] 保存数据库..." ) ;
DbContext . SaveChanges ( ) ;
_logger . Info ( "[批量删除应用] 刷新应用缓存..." ) ;
RiskEngineService . GetInstance ( ) . RefreshApplication ( ) ;
_logger . Info ( "[批量删除应用] 完成" ) ;
return new BatchOperationResult
{
Success = true ,
TotalCount = applicationIds . Count ,
SuccessCount = applicationIds . Count
} ;
}
/// <summary>
/// 启用应用配置(校验关联规则状态)
/// </summary>
@@ -1342,16 +1782,46 @@ namespace YLErp.Modules.RiskEngine
/// </summary>
public BatchOperationResult BatchEnableApplications ( List < long > applicationIds )
{
_logger . Info ( $"[批量启用] 开始 - 传入应用数: {applicationIds?.Count ?? 0}, IDs: {string.Join(" , ", applicationIds ?? new List<long>())}" ) ;
if ( applicationIds = = null | | applicationIds . Count = = 0 )
throw new ServiceException ( "应用配置ID列表不能为空" ) ;
_logger . Info ( $"[批量启用] 开始 - 传入应用数: {applicationIds.Count}, IDs: {string.Join(" , ", applicationIds)}" ) ;
var apps = DbContext . glms_risk_rule_application
. Where ( a = > applicationIds . Contains ( a . id ) & & a . Status = = RiskRuleStatus . Disabled )
. Where ( a = > applicationIds . Contains ( a . id ) )
. ToList ( ) ;
_logger . Info ( $"[批量启用] DB查询完成 - 匹配到 {apps.Count} 条已停用 应用" ) ;
_logger . Info ( $"[批量启用] DB查询完成 - 匹配到 {apps.Count} 条应用" ) ;
if ( apps . Count ! = applicationIds . Count )
throw new ServiceException ( "部分应用配置不存在或当前状态不允许启用" ) ;
var foundIds = apps . Select ( a = > ( long ) a . id ) . ToList ( ) ;
var missingIds = applicationIds . Except ( foundIds ) . ToList ( ) ;
if ( missingIds . Any ( ) )
{
return new BatchOperationResult
{
Success = false ,
TotalCount = applicationIds . Count ,
SuccessCount = 0 ,
ErrorMessage = $"以下应用配置不存在:{string.Join(" , ", missingIds)}"
} ;
}
// 跳过已启用的应用,只处理已停用的
var skippedIds = apps . Where ( a = > a . Status = = RiskRuleStatus . Active ) . Select ( a = > ( long ) a . id ) . ToList ( ) ;
if ( skippedIds . Any ( ) )
_logger . Info ( $"[批量启用] 跳过已启用应用 - IDs: {string.Join(" , ", skippedIds)}" ) ;
apps = apps . Where ( a = > a . Status ! = RiskRuleStatus . Active ) . ToList ( ) ;
if ( ! apps . Any ( ) )
{
_logger . Info ( "[批量启用] 所有应用均已启用,无需操作" ) ;
return new BatchOperationResult
{
Success = true ,
TotalCount = applicationIds . Count ,
SuccessCount = applicationIds . Count
} ;
}
var allErrors = new List < string > ( ) ;
foreach ( var app in apps )
@@ -1360,7 +1830,7 @@ namespace YLErp.Modules.RiskEngine
var errors = ValidateAndEnsureRuleCache ( app . RuleIds ) ;
_logger . Info ( $"[批量启用] 规则校验结果 - 应用ID: {app.id}, Errors: {(errors.Any() ? string.Join(" ; ", errors) : " 无 ")}" ) ;
if ( errors . Any ( ) )
allErrors . Add ( $"应用 '{app.Description }': {string.Join(" ; ", errors)}" ) ;
allErrors . Add ( $"应用 '{app.id }': {string.Join(" ; ", errors)}" ) ;
}
if ( allErrors . Any ( ) )
@@ -1404,7 +1874,10 @@ namespace YLErp.Modules.RiskEngine
/// </summary>
public BatchOperationResult BatchDisableApplications ( List < long > applicationIds )
{
_logger . Info ( $"[批量停用] 开始 - 传入应用数: {applicationIds?.Count ?? 0}, IDs: {string.Join(" , ", applicationIds ?? new List<long>())}" ) ;
if ( applicationIds = = null | | applicationIds . Count = = 0 )
throw new ServiceException ( "应用配置ID列表不能为空" ) ;
_logger . Info ( $"[批量停用] 开始 - 传入应用数: {applicationIds.Count}, IDs: {string.Join(" , ", applicationIds)}" ) ;
var apps = DbContext . glms_risk_rule_application
. Where ( a = > applicationIds . Contains ( a . id ) & & a . Status = = RiskRuleStatus . Active )
@@ -1412,8 +1885,18 @@ namespace YLErp.Modules.RiskEngine
_logger . Info ( $"[批量停用] DB查询完成 - 匹配到 {apps.Count} 条已启用应用" ) ;
if ( apps . Count ! = applicationIds . Count )
throw new ServiceException ( "部分应用配置不存在或当前状态不允许停用" ) ;
var foundIds = apps . Select ( a = > ( long ) a . id ) . ToList ( ) ;
var missingIds = applicationIds . Except ( foundIds ) . ToList ( ) ;
if ( missingIds . Any ( ) )
{
return new BatchOperationResult
{
Success = false ,
TotalCount = applicationIds . Count ,
SuccessCount = apps . Count ,
ErrorMessage = $"以下应用配置不存在或当前状态不允许停用:{string.Join(" , ", missingIds)}"
} ;
}
foreach ( var app in apps )
{
@@ -1660,16 +2143,15 @@ namespace YLErp.Modules.RiskEngine
rule . UpdateOptId = UserId ;
rule . UpdateOptName = UserName ;
rule . UpdateDate = DateTime . Now ;
WriteAuditLog ( "VAR_CASCADE_UPDATE_RULE" , "RULE" , rule . id , rule . RuleName ,
$"因变量'{variable.VariableName}'(ID:{variableId})表达式变更,规则表达式被级联更新" ) ;
}
DbContext . SaveChanges ( ) ;
WriteAuditLog ( "VAR_UPDATE" , "VARIABLE" , variableId , req . VariableName , $"修改变量:{variable.VariableName}" ) ;
var cascadedRuleIds = affectedRules . Select ( r = > r . id . ToString ( ) ) . ToList ( ) ;
WriteAuditLog ( "VAR_UPDATE_CASCADE" , "VARIABLE" , variableId , req . VariableName ,
$"变量 [{variable.VariableName}] VariableExpr 变更,级联更新规则 {string.Join(" 、 ", cascadedRuleIds)}" ) ;
DbContext . SaveChanges ( ) ;
transaction . Commit ( ) ;
}
@@ -1746,6 +2228,11 @@ namespace YLErp.Modules.RiskEngine
/// 查询审计日志列表(分页)
/// </summary>
public SearchListResult < RiskAuditLogListItem > QueryAuditLogs ( QueryRiskAuditLogReq req )
{
return BuildAuditLogQuery ( req ) . ToSearchList ( req ) ;
}
private IQueryable < RiskAuditLogListItem > BuildAuditLogQuery ( QueryRiskAuditLogReq req )
{
var query = DbContext . glms_risk_rule_audit_log . AsQueryable ( ) ;
@@ -1806,7 +2293,55 @@ namespace YLErp.Modules.RiskEngine
OptDate = l . OptDate
} ) ;
return result . ToSearchList ( req ) ;
return result ;
}
/// <summary>
/// 根据查询条件生成审计日志导出文件名
/// </summary>
public string BuildAuditLogExportFileName ( QueryRiskAuditLogReq req )
{
req ? ? = new QueryRiskAuditLogReq ( ) ;
var conditions = new List < string > ( ) ;
var operationTypes = new List < string > ( ) ;
if ( ! string . IsNullOrWhiteSpace ( req . OperationType ) )
operationTypes . Add ( req . OperationType . Trim ( ) ) ;
if ( ! string . IsNullOrWhiteSpace ( req . OperationTypes ) )
{
operationTypes . AddRange ( req . OperationTypes . Split ( ',' ,
StringSplitOptions . RemoveEmptyEntries | StringSplitOptions . TrimEntries ) ) ;
}
var operationTypeNames = operationTypes
. Distinct ( StringComparer . OrdinalIgnoreCase )
. Select ( GetAuditOperationTypeName )
. ToList ( ) ;
if ( operationTypeNames . Any ( ) )
conditions . Add ( $"操作类型-{string.Join(" + ", operationTypeNames)}" ) ;
if ( ! string . IsNullOrWhiteSpace ( req . TargetType ) )
conditions . Add ( $"目标类型-{GetAuditTargetTypeName(req.TargetType)}" ) ;
if ( ! string . IsNullOrWhiteSpace ( req . TargetName ) )
conditions . Add ( $"目标名称-{SanitizeFileNamePart(req.TargetName)}" ) ;
if ( ! string . IsNullOrWhiteSpace ( req . OptName ) )
conditions . Add ( $"操作人-{SanitizeFileNamePart(req.OptName)}" ) ;
if ( req . StartDate . HasValue & & req . EndDate . HasValue )
conditions . Add ( $"日期-{req.StartDate.Value:yyyyMMdd}至{req.EndDate.Value:yyyyMMdd}" ) ;
else if ( req . StartDate . HasValue )
conditions . Add ( $"开始日期-{req.StartDate.Value:yyyyMMdd}" ) ;
else if ( req . EndDate . HasValue )
conditions . Add ( $"结束日期-{req.EndDate.Value:yyyyMMdd}" ) ;
if ( ! string . IsNullOrWhiteSpace ( req . Keyword ) )
conditions . Add ( $"关键词-{SanitizeFileNamePart(req.Keyword)}" ) ;
var suffix = conditions . Any ( ) ? string . Join ( "_" , conditions ) : "全部" ;
var fileNameWithoutExtension = $"风控操作日志_{suffix}" ;
if ( fileNameWithoutExtension . Length > 180 )
fileNameWithoutExtension = fileNameWithoutExtension . Substring ( 0 , 180 ) ;
return $"{fileNameWithoutExtension}.xlsx" ;
}
/// <summary>
@@ -1814,7 +2349,28 @@ namespace YLErp.Modules.RiskEngine
/// </summary>
public byte [ ] ExportAuditLogs ( QueryRiskAuditLogReq req )
{
throw new ServiceException ( "导出功能暂未实现" ) ;
var logs = BuildAuditLogQuery ( req ) . ToList ( ) ;
var columns = new List < ExcelHelper . DataColumnModel >
{
new ExcelHelper . DataColumnModel ( "日志ID" , "Id" , typeof ( string ) ,
( value , _ ) = > FormatAuditId ( value ) ) ,
new ExcelHelper . DataColumnModel ( "操作类型" , "OperationType" , typeof ( string ) ,
( value , _ ) = > GetAuditOperationTypeName ( value ? . ToString ( ) ) ) ,
new ExcelHelper . DataColumnModel ( "目标类型" , "TargetType" , typeof ( string ) ,
( value , _ ) = > GetAuditTargetTypeName ( value ? . ToString ( ) ) ) ,
new ExcelHelper . DataColumnModel ( "目标ID" , "TargetId" , typeof ( string ) ,
( value , _ ) = > FormatAuditId ( value ) ) ,
new ExcelHelper . DataColumnModel ( "目标名称" , "TargetName" , typeof ( string ) ) ,
new ExcelHelper . DataColumnModel ( "操作详情" , "OperationDetail" , typeof ( string ) ) ,
new ExcelHelper . DataColumnModel ( "操作结果" , "Result" , typeof ( string ) ) ,
new ExcelHelper . DataColumnModel ( "操作人" , "OptName" , typeof ( string ) ) ,
new ExcelHelper . DataColumnModel ( "操作时间" , "OptDate" , ( value , _ ) = >
value = = null ? string . Empty : ( ( DateTime ) value ) . ToString ( "yyyy-MM-dd HH:mm:ss" ) )
} ;
new ExcelHelper ( ) . ListToExcel < RiskAuditLogListItem > (
columns . ToArray ( ) , logs , "风控操作日志" , true , out var buffer ) ;
return buffer ;
}
/// <summary>