349 lines
14 KiB
C#
349 lines
14 KiB
C#
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>tokens:token 序列,支持「且/或」混合与括号,按表达式顺序排列。</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,见 FieldResolver(initGroup/notional/tradeType 等)。</summary>
|
||
public string field { get; set; }
|
||
|
||
/// <summary>操作符:> < >= <= == != = 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;
|
||
}
|
||
}
|
||
}
|
||
}
|