Files
zszq-trs/YLErpDAL/Helpers/ConditionEvaluator.cs

349 lines
14 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using BaseOUDAL;
using Newtonsoft.Json;
namespace YLErp.Helpers
{
/// <summary>
/// 审批条件求值器:统一支撑「节点触发条件」与「分支网关条件」。
/// <para>需求①(节点触发)与需求③(多分支)共用同一套条件模型,避免两套条件语义。</para>
/// <para>支持混合「且/或」与「括号」的布尔表达式,如 A and (B or C)。</para>
/// </summary>
public static class ConditionEvaluator
{
/// <summary>
/// 求值条件 JSON。
/// </summary>
/// <param name="conditionJson">条件 JSON(结构见 ConditionExpressionConfig);为空/null 视为无条件,返回 false(不触发)。</param>
/// <param name="context">业务上下文(交易/发起人等)。</param>
/// <returns>是否满足条件</returns>
public static bool Evaluate(string conditionJson, ConditionContext context)
{
if (string.IsNullOrWhiteSpace(conditionJson))
{
return false;
}
ConditionExpressionConfig config;
try
{
config = JsonConvert.DeserializeObject<ConditionExpressionConfig>(conditionJson);
}
catch
{
// 容错:非法 JSON 不阻断审批主流程,视为不触发。
return false;
}
if (config == null || config.tokens == null || config.tokens.Count == 0)
{
return false;
}
// 递归下降求值(支持括号、且/或混合)。
var parser = new ConditionParser(config.tokens, context);
return parser.Parse();
}
/// <summary>
/// 从起点节点开始,向后查找第一个满足触发条件(或无触发条件)的审批节点(需求①)。
/// <para>用于交易提交进入审批流程时确定起始审批节点:若起点节点配置了触发条件且当前业务不满足,
/// 则跳过该节点继续向后找,直到找到可进入的节点;若从起点到末尾均不满足则返回 null(表示无需审批,直接通过)。</para>
/// </summary>
/// <param name="tradeProcess">流程全部节点(已按 order 排序)</param>
/// <param name="start">起点节点</param>
/// <param name="ctx">条件求值上下文</param>
/// <returns>第一个应进入审批的节点;若无需审批则返回 null</returns>
public static approvalprocess FindFirstTriggeredNode(
List<approvalprocess> tradeProcess,
approvalprocess start,
ConditionContext ctx)
{
if (start == null) return null;
var current = start;
while (current != null)
{
// 无触发条件,或满足触发条件 → 该节点需审批
if (string.IsNullOrWhiteSpace(current.triggerCondition)
|| Evaluate(current.triggerCondition, ctx))
{
return current;
}
// 不满足 → 向后取下一个主干节点(node=0)
current = tradeProcess.FirstOrDefault(x => x.order > current.order && x.node == 0);
}
return null;
}
/// <summary>求值单个条件。</summary>
internal static bool EvaluateSingle(ConditionItem cond, ConditionContext context)
{
if (cond == null || string.IsNullOrEmpty(cond.field) || string.IsNullOrEmpty(cond.op))
{
return false;
}
var leftValue = FieldResolver.ResolveValue(cond.field, context);
return OperatorCompare(leftValue, cond.op, cond.value);
}
/// <summary>比较:能转数值时按数值比,否则按字符串比。</summary>
private static bool OperatorCompare(object left, string op, object right)
{
if (TryToDouble(left, out var ld) && TryToDouble(right, out var rd))
{
return CompareNumeric(ld, op, rd);
}
var ls = left?.ToString() ?? string.Empty;
var rs = right?.ToString() ?? string.Empty;
return CompareString(ls, op, rs);
}
private static bool CompareNumeric(double left, string op, double right)
{
return NormalizeOp(op) switch
{
">" => left > right,
"<" => left < right,
">=" => left >= right,
"<=" => left <= right,
"==" => Math.Abs(left - right) < 1e-9,
"!=" => Math.Abs(left - right) >= 1e-9,
_ => false
};
}
private static bool CompareString(string left, string op, string right)
{
return NormalizeOp(op) switch
{
"==" => left == right,
"!=" => left != right,
">" => string.Compare(left, right, StringComparison.Ordinal) > 0,
"<" => string.Compare(left, right, StringComparison.Ordinal) < 0,
">=" => string.Compare(left, right, StringComparison.Ordinal) >= 0,
"<=" => string.Compare(left, right, StringComparison.Ordinal) <= 0,
"in" => right.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Any(r => string.Equals(r.Trim(), left, StringComparison.OrdinalIgnoreCase)),
_ => false
};
}
/// <summary>
/// 归一化操作符:兼容字母标识符(gt/lt/gte/lte/eq/neq)与符号(> < >= <= == != =)。
/// </summary>
private static string NormalizeOp(string op)
{
if (string.IsNullOrEmpty(op)) return op;
return op.ToLowerInvariant() switch
{
"gt" or ">" => ">",
"lt" or "<" => "<",
"gte" or ">=" => ">=",
"lte" or "<=" => "<=",
"eq" or "==" or "=" => "==",
"neq" or "!=" or "<>" => "!=",
_ => op
};
}
private static bool TryToDouble(object value, out double result)
{
result = 0;
if (value == null) return false;
return double.TryParse(value.ToString(), out result);
}
}
/// <summary>
/// 递归下降解析器:按 token 顺序求值布尔表达式,支持括号与「且/或」优先级。
/// <para>文法:Expr := Term (("and"|"or") Term)* Term := condition | "(" Expr ")"。</para>
/// <para>优先级:and 高于 or(与常规布尔代数一致);同级从左到右。</para>
/// </summary>
internal class ConditionParser
{
private readonly List<ConditionToken> _tokens;
private readonly ConditionContext _context;
private int _pos;
public ConditionParser(List<ConditionToken> tokens, ConditionContext context)
{
_tokens = tokens ?? new List<ConditionToken>();
_context = context;
_pos = 0;
}
public bool Parse()
{
if (_tokens.Count == 0) return false;
return ParseOr();
}
// 低优先级:ParseOr := ParseAnd ( "or" ParseAnd )* 左结合,遇 or 短路(为 true 直接返回后续不再求值)
private bool ParseOr()
{
var left = ParseAnd();
while (true)
{
var op = Peek();
if (op == null || op.type != "operator" || !IsOr(op.connector)) break;
_pos++; // 消费 or
var right = ParseAnd();
left = left || right;
}
return left;
}
// 高优先级:ParseAnd := ParseTerm ( "and" ParseTerm )* 左结合,遇 and 短路(为 false 直接返回)
private bool ParseAnd()
{
var left = ParseTerm();
while (true)
{
var op = Peek();
if (op == null || op.type != "operator" || IsOr(op.connector)) break;
_pos++; // 消费 and
var right = ParseTerm();
left = left && right;
}
return left;
}
// Term := condition | "(" ParseOr ")"
private bool ParseTerm()
{
var tok = Peek();
if (tok == null) return false;
if (tok.type == "lparen")
{
_pos++; // 消费 "("
var val = ParseOr();
var rp = Peek();
if (rp != null && rp.type == "rparen") _pos++; // 消费 ")"
return val;
}
if (tok.type == "condition")
{
_pos++;
return ConditionEvaluator.EvaluateSingle(tok.condition, _context);
}
return false;
}
private ConditionToken Peek() => _pos < _tokens.Count ? _tokens[_pos] : null;
private static bool IsOr(string connector)
=> string.Equals(connector, "or", StringComparison.OrdinalIgnoreCase);
}
/// <summary>条件业务上下文:求值时由调用方构造,封装可参与判断的业务字段。</summary>
public class ConditionContext
{
/// <summary>发起人 userId(用于查发起人审批组)。</summary>
public int? UserId { get; set; }
/// <summary>交易实体(期初名义本金等字段来源)。开户场景可为 null。</summary>
public trade Trade { get; set; }
/// <summary>交易流程类别:开仓(TradeProcess) / 了结(CloseProcess)。需求②用。</summary>
public string ProcessCategory { get; set; }
/// <summary>预解析的发起人审批组(避免重复查库;为空时由 FieldResolver 查)。</summary>
public int? InitGroupId { get; set; }
/// <summary>本次交易名义本金的取值(了结场景由调用方从 trade_cash.UnwindStockEqvNotional 取绝对值传入)。需求①。</summary>
public double? CurrentNotional { get; set; }
}
/// <summary>
/// 条件表达式配置(对应 conditionConfig / triggerCondition 列的 JSON 结构)。
/// <para>tokenstoken 序列,支持「且/或」混合与括号,按表达式顺序排列。</para>
/// </summary>
public class ConditionExpressionConfig
{
/// <summary>token 序列:条件 / 且或连接符 / 左右括号,按表达式顺序排列。</summary>
public List<ConditionToken> tokens { get; set; }
}
/// <summary>
/// 表达式 token:一个条件、一个连接符、或一个括号。
/// </summary>
public class ConditionToken
{
/// <summary>token 类型:condition | operator | lparen | rparen</summary>
public string type { get; set; }
/// <summary>当 type=condition 时的条件体。</summary>
public ConditionItem condition { get; set; }
/// <summary>当 type=operator 时的连接符:and | or</summary>
public string connector { get; set; }
}
/// <summary>单个条件:左值字段 + 操作符 + 右值。</summary>
public class ConditionItem
{
/// <summary>左值字段 key,见 FieldResolverinitGroup/notional/tradeType 等)。</summary>
public string field { get; set; }
/// <summary>操作符:> &lt; &gt;= &lt;= == != = in</summary>
public string op { get; set; }
/// <summary>右值</summary>
public object value { get; set; }
}
/// <summary>
/// 条件左值解析:把 field key 映射到具体业务字段值。
/// <para>触发条件字段(需求①):</para>
/// <para>- initialNotional:交易的期初名义本金(trade.OriginalStockEqvNotional,已取绝对值)</para>
/// <para>- currentNotional :本次交易名义本金(了结场景,由调用方从 trade_cash 取本次影响金额绝对值传入)</para>
/// <para>历史兼容:initGroup/tradeType/processCategory/notional 仍可解析。</para>
/// </summary>
public static class FieldResolver
{
public static object ResolveValue(string field, ConditionContext context)
{
if (string.IsNullOrEmpty(field) || context == null)
{
return null;
}
switch (field.ToLowerInvariant())
{
// 需求①:触发条件字段(仅这两个对外暴露)
case "initialnotional":
// 交易的期初名义本金
return context.Trade?.OriginalStockEqvNotional ?? 0;
case "currentnotional":
// 本次交易名义本金(了结时按本次影响金额绝对值,由调用方传入)
return context.CurrentNotional ?? 0;
// 以下为历史兼容,前端不再暴露
case "notional":
return context.Trade?.StockEqvNotional ?? 0;
case "initgroup":
if (context.InitGroupId.HasValue)
{
return context.InitGroupId.Value;
}
return context.UserId.HasValue
? UserBLL.GetApprovalProcessGroup(context.UserId.Value)
: 0;
case "tradetype":
return context.Trade?.TradeType ?? string.Empty;
case "processcategory":
return context.ProcessCategory ?? string.Empty;
default:
return null;
}
}
}
}