Merge remote-tracking branch 'origin/glms/feature/1.4.2' into feature/p132_74-risk-engine

This commit is contained in:
尹峰
2026-07-21 15:06:17 +08:00
101 changed files with 13790 additions and 1083 deletions
@@ -224,7 +224,7 @@ namespace YLErp.Modules.TradeModule.DealModule
#endregion
//审批流程
//审批流程(需求②:按交易状态查对应流程)
if (!req.SkipWorkflow && HasTradeProcess())
{
var isUnwind = tradeCash.Action.Contains("平仓");
@@ -235,14 +235,33 @@ namespace YLErp.Modules.TradeModule.DealModule
dbTrade.CheckStatus = Convert.ToInt32(TradeCheckEnum.StatusOfOld);
//检查是否有交易审批流程
if (valuedateBLL.SystemDate.CloseReApprove == 1)
//检查是否有交易审批流程(需求②:按交易状态查对应流程,而非只查开仓流程)
var needApproval = valuedateBLL.SystemDate.CloseReApprove == 1 && TradeProcessCountByCategory(dbTrade) > 0;
if (needApproval)
{
// 如果有审批
InitTradeProcessOrder(dbTrade, UserId);
// 需求①:进入审批前先判断触发条件——若所有节点都不满足触发条件,跳过审批
needApproval = NeedApprovalByTrigger(dbTrade);
}
AddTradeOperationHistoryAndSetParentTradeInfo(false, dbTrade, optType: isUnwind ? "平仓审核提交" : "行权审核提交", comments: req.ImportFlag);
if (needApproval)
{
// 需要审批:进入审批流程等待人工审批
InitTradeProcessOrder(dbTrade, UserId);
AddTradeOperationHistoryAndSetParentTradeInfo(false, dbTrade, optType: isUnwind ? "平仓审核提交" : "行权审核提交", comments: req.ImportFlag);
}
else
{
// 不需要审批:设 ProcessOrderId=审批通过,再调 UpdateTradeProcessLog(pass) 执行审批通过流程
dbTrade.ProcessOrderId = ProcessTradeLog.;
DbContext.SaveChanges();
new TradeOpenService(this).UpdateTradeProcessLog(new TradeOpenReqModel
{
tradeId = dbTrade.id,
status = "pass",
comments = "触发条件未满足,自动跳过审批",
notNeedOperationHistory = false
});
}
}
else
{
@@ -1,4 +1,5 @@
using YLErp.BLL;
using YLErp.Helpers;
using YLErp.Model;
using YLErp.Model.Enum;
using YLErp.Modules.TradeDalModule;
@@ -115,25 +116,40 @@ namespace YLErp.Modules.TradeModule.DealModule
td.OptId = UserId;
td.OptName = UserName;
td.OptDate = OptDate;
if (isFromExercise)
{
td.TradeStatus = ConsTrade.;
}
else
{
td.TradeStatus = ConsTrade.;
}
td.CheckStatus = Convert.ToInt32(TradeCheckEnum.StatusOfOld);
//检查是否有交易审批流程
if (valuedateBLL.SystemDate.CloseReApprove == 1 && hasTradeProcess)
// 先设状态,使流程类别能正确推断为 CloseProcess(需求②按交易状态推断开仓/了结)
td.TradeStatus = isFromExercise ? ConsTrade. : ConsTrade.;
//检查是否有交易审批流程(需求②:平仓/行权按交易状态查对应流程,而非只查开仓流程)
var needApproval = valuedateBLL.SystemDate.CloseReApprove == 1 && TradeProcessCountByCategory(td) > 0;
if (needApproval)
{
// 如果有审批
InitTradeProcessOrder(td, UserId);
// 需求①:进入审批前先判断触发条件——若所有节点都不满足触发条件,跳过审批
needApproval = NeedApprovalByTrigger(td);
}
AddTradeOperationHistoryAndSetParentTradeInfo(false, td, isFromExercise ? "部分行权审核提交" : "平仓审核提交");
if (needApproval)
{
// 需要审批:保持"平仓待复核/行权待复核",进入审批流程等待人工审批
InitTradeProcessOrder(td, UserId);
AddTradeOperationHistoryAndSetParentTradeInfo(false, td, isFromExercise ? "部分行权审核提交" : "平仓审核提交");
}
else
{
// 不需要审批:设 ProcessOrderId=审批通过,再调 UpdateTradeProcessLog(pass) 执行审批通过流程
// SetTradeOpen 会完成平仓、改状态、写日志,与人工审批通过完全一致)
td.ProcessOrderId = ProcessTradeLog.;
DbContext.SaveChanges();
new TradeOpenService(this).UpdateTradeProcessLog(new TradeOpenReqModel
{
tradeId = td.id,
status = "pass",
comments = "触发条件未满足,自动跳过审批",
notNeedOperationHistory = false
});
}
}
}
@@ -1,4 +1,5 @@
using YLErp.BLL;
using YLErp.Helpers;
using YLErp.Model;
using YLErp.Model.Enum;
@@ -105,12 +106,37 @@ namespace YLErp.Modules.TradeModule.DealModule
{
var td = DbContext.trade.Find(tradeId);
td.OptDate = DateTime.Now;
td.TradeStatus = ConsTrade.;
td.CheckStatus = Convert.ToInt32(TradeCheckEnum.StatusOfOld);
if (valuedateBLL.SystemDate.CloseReApprove == 1 && hasTradeProcess)
// 先设状态,使流程类别能正确推断为 CloseProcess
td.TradeStatus = ConsTrade.;
// 需求②:行权按交易状态查对应流程
var needApproval = valuedateBLL.SystemDate.CloseReApprove == 1 && TradeProcessCountByCategory(td) > 0;
if (needApproval)
{
//如果有审批
InitTradeProcessOrder(td,UserId);
// 需求①:进入审批前先判断触发条件——若所有节点都不满足触发条件,跳过审批
needApproval = NeedApprovalByTrigger(td);
}
if (needApproval)
{
// 需要审批:保持"行权待复核",进入审批流程等待人工审批
InitTradeProcessOrder(td, UserId);
}
else
{
// 不需要审批:设 ProcessOrderId=审批通过,再调 UpdateTradeProcessLog(pass) 执行审批通过流程
td.ProcessOrderId = ProcessTradeLog.;
DbContext.SaveChanges();
new TradeOpenService(this).UpdateTradeProcessLog(new TradeOpenReqModel
{
tradeId = td.id,
status = "pass",
comments = "触发条件未满足,自动跳过审批",
notNeedOperationHistory = false
});
return;
}
DbContext.SaveChanges();
@@ -140,7 +140,9 @@ namespace YLErp.Modules.TradeModule.DealModule
#region
var finalPrice = EodPriceQueryService.TryGetEodPrice(exerciseDate, td.UnderlyingCode, out var eodPrice)
// 债券标的需走中债估值表取价,原 TryGetEodPrice 只查期货/股票两表会漏掉债券,导致"结算价未找到"。
// 统一改用债券感知的 TryGetSettlementEodPrice(见 EodPriceQueryService)。
var finalPrice = EodPriceQueryService.TryGetSettlementEodPrice(exerciseDate, td.UnderlyingCode, out var eodPrice)
? eodPrice.GetPrice(td.SettlementType) : 0;
if (finalPrice <= 0)
@@ -280,8 +282,6 @@ namespace YLErp.Modules.TradeModule.DealModule
trade_cash tradeCash = null;
//日终价格
var underlyingIds = tradeUnwindTrades.Select(t => t.UnderlyingId).ToList();
var EodPriceProvider = new EodPriceProvider(valueDate);
//批量结算的全是现金流交易就不用结算价
if (!EodPriceQueryService.CheckDbExists(valueDate) && tradeQuery.Any(t => t.TradeType != "现金流交易"))
{
@@ -308,8 +308,8 @@ namespace YLErp.Modules.TradeModule.DealModule
var CountRatio = 1;
if (t.TradeType != "现金流交易")
{
//结算价
if (EodPriceProvider.TryGetEodPrice(t.UnderlyingCode, out var eodPrice))
//结算价(债券感知统一取价:债券走中债估值,期货/股票走原路径,见 EodPriceQueryService.TryGetSettlementEodPrice
if (EodPriceQueryService.TryGetSettlementEodPrice(valueDate, t.UnderlyingCode, out var eodPrice))
{
settlePrice = eodPrice.GetPrice(t.SettlementType);
}
@@ -1,6 +1,7 @@
using BaseOUDAL;
using YLErp.BLL;
using YLErp.BLL.Eod;
using YLErp.Helpers;
using YLErp.Model;
using YLErp.Model.Enum;
using YLErp.Modules.ClientModule;
@@ -140,7 +141,8 @@ namespace YLErp.Modules.TradeModule.DealModule
return result;
}
//TODO 如果是审批组
var tradeProessQuery = TradeProcess();
// 需求②:按交易状态推断开仓/了结流程。了结类(平仓/行权/互换待复核)走 CloseProcess。
var tradeProessQuery = TradeProcessByCategory(td);
var count = tradeProessQuery.Count();
if (count == 0 || td.ProcessOrderId == ProcessTradeLog.)//投资规模校验已经将数据设置为已通过
{
@@ -166,6 +168,8 @@ namespace YLErp.Modules.TradeModule.DealModule
//{
// td.ProcessOrderBranch = 0;
//}
// 节点触发条件(需求①):下一节点配置了 triggerCondition 时,满足才进入审批,不满足则跳过。
nextOrder = AdvanceThroughTriggerNodes(tradeProessQuery, nextOrder, BuildTriggerContext(td, UserId));
if (nextOrder==null)
{
@@ -225,7 +229,8 @@ namespace YLErp.Modules.TradeModule.DealModule
var childrenTrades = DbContext.trade.Where(x => childrenTradeIds.Contains(x.id)).ToList();
var result = new TradeOpenResult(td);
// 如果是审批组
var tradeProessQuery = TradeProcess();
// 需求②:分组了结按交易状态推断开仓/了结流程。
var tradeProessQuery = TradeProcessByCategory(td);
var count = tradeProessQuery.Count();
if (count == 0)
{
@@ -246,6 +251,8 @@ namespace YLErp.Modules.TradeModule.DealModule
{
nextOrder = tradeProessQuery.FirstOrDefault(x => x.order > td.ProcessOrderId && x.node == td.ProcessOrderBranch && x.approvalGroupId == 0);
}
// 节点触发条件(需求①):分组了结推进同样支持触发条件。
nextOrder = AdvanceThroughTriggerNodes(tradeProessQuery, nextOrder, BuildTriggerContext(td, UserId));
if (nextOrder == null)
{
SetParentTradeOpen(req, td, childrenTrades, parentTradeCash);
@@ -294,8 +294,8 @@ namespace YLErp.Modules.TradeModule.DealModule
}
var result = new TradeUnwindResultModel(td);
//存在审批流程时,走审批成功流程
if (valuedateBLL.SystemDate.CloseReApprove == 1 && HasTradeProcess())
//存在审批流程时,走审批成功流程(需求②:按交易状态查对应流程)
if (valuedateBLL.SystemDate.CloseReApprove == 1 && TradeProcessCountByCategory(td) > 0)
{
result.ApprovalProcess = true;
@@ -462,8 +462,8 @@ namespace YLErp.Modules.TradeModule.DealModule
throw new ServiceException("平仓费不一致!复核不通过");
}
}
//存在审批流程时,走审批成功流程
if (valuedateBLL.SystemDate.CloseReApprove == 1 && HasTradeProcess())
//存在审批流程时,走审批成功流程(需求②:按交易状态查对应流程)
if (valuedateBLL.SystemDate.CloseReApprove == 1 && TradeProcessCountByCategory(td) > 0)
{
var reqModel = new TradeOpenReqModel
{
@@ -1848,14 +1848,28 @@ namespace YLErp.Modules.TradeModule.DealModule
td.CheckStatus = Convert.ToInt32(TradeCheckEnum.StatusOfOld);
//检查是否有交易审批流程
if ((valuedateBLL.SystemDate.CloseReApprove == 1 || isSwap) && hasTradeProcess)
//检查是否有交易审批流程(需求②:按交易状态查对应流程,而非只查开仓流程)
var needApproval = (valuedateBLL.SystemDate.CloseReApprove == 1 || isSwap) && TradeProcessCountByCategory(td) > 0;
if (needApproval)
{
//如果有审批
InitTradeProcessOrder(td, UserId);
// 需求①:进入审批前先判断触发条件——若所有节点都不满足触发条件,跳过审批
needApproval = NeedApprovalByTrigger(td);
}
AddTradeOperationHistoryAndSetParentTradeInfo(false, td, isSwap ? "互换审核提交" : "平仓审核提交");
if (needApproval)
{
// 需要审批:进入审批流程等待人工审批
InitTradeProcessOrder(td, UserId);
AddTradeOperationHistoryAndSetParentTradeInfo(false, td, isSwap ? "互换审核提交" : "平仓审核提交");
}
else
{
// 不需要审批:只标记 ProcessOrderId=审批通过。
// 互换的 swap_event 记录要等调用方 ApplySwapTrade 的 SaveSwapDeal 执行后才创建,
// 所以互换不在这里调 UpdateTradeProcessLog(否则 FindSwapEvent 找不到记录),
// 由 ApplySwapTrade 在 SaveSwapDeal 之后判断并触发审批通过流程。
td.ProcessOrderId = ProcessTradeLog.;
}
}
}
@@ -0,0 +1,42 @@
namespace YLErp.Modules.TradeModule.QueryModule
{
public sealed class TradeConfirmationDocumentQueryItem
{
public trade_contract_document Document { get; set; }
public trade_contract_r Relation { get; set; }
}
public static class TradeConfirmationDocumentQuery
{
public static IQueryable<TradeConfirmationDocumentQueryItem> Create(
IQueryable<trade_contract_document> documents,
IQueryable<trade_contract_r> relations,
IQueryable<trade> trades,
DateTime? tradeDateStart,
DateTime? tradeDateEnd)
{
if (tradeDateStart.HasValue)
{
trades = trades.Where(t => t.TradeDate >= tradeDateStart.Value);
}
if (tradeDateEnd.HasValue)
{
var tradeDateEndExclusive = tradeDateEnd.Value.Date.AddDays(1);
trades = trades.Where(t => t.TradeDate < tradeDateEndExclusive);
}
return from document in documents
join relation in relations.Where(r => r.IsValid)
on document.Code equals relation.ContractCode
join trade in trades
on relation.TradeId equals trade.id
select new TradeConfirmationDocumentQueryItem
{
Document = document,
Relation = relation
};
}
}
}
+26 -7
View File
@@ -276,8 +276,25 @@ namespace YLErp.BLL
var logger = LogFactory.GetLogger<tradeBLL>();
var posDict = swapPositions.GroupBy(sp => sp.SwapTradeId).ToDictionary(g => g.Key, g => g.First().PosiNetPrice);
var umProvider = DataCacheProvider.GetUnderlyingDataSource();
// 需求②:平仓/行权/互换交易,审批角色应取 CloseProcess 流程的节点角色,而非 TradeProcess
var closeProcessRoles = db.approvalprocess
.Where(a => a.processType == "CloseProcess")
.Select(a => new { a.order, a.roleId })
.ToList()
.ToDictionary(a => a.order, a => a.roleId);
foreach (var tradeLinq in retListResult.rows)
{
// 了结类交易:用 CloseProcess 的角色覆盖
if (tradeLinq.TradeStatus == "平仓待复核" || tradeLinq.TradeStatus == "行权待复核" || tradeLinq.TradeStatus == "互换待复核"
&& closeProcessRoles.Count > 0)
{
if (closeProcessRoles.TryGetValue(tradeLinq.ProcessOrderId, out var closeRoleId))
{
tradeLinq.ProcessRoleId = closeRoleId;
}
}
if (tradeLinq.ProcessRoleId != null && listRoles.TryGetValue(tradeLinq.ProcessRoleId.Value, out var name))
{
tradeLinq.ProcessRoleName = name;
@@ -3081,6 +3098,8 @@ namespace YLErp.BLL
//如果是审批组
var approvalprocessQuery = db.approvalprocess.Where(a => a.processType == "TradeProcess");
var tradeOpenProcessOrder = approvalprocessQuery.Count();
// 需求②:了结流程节点数(用于平仓/行权/互换交易展示正确的审批进度)
var closeProcessOrder = db.approvalprocess.Count(a => a.processType == "CloseProcess");
var branch = approvalprocessQuery.FirstOrDefault(x => x.approvalGroupId != 0);//审批流程有分支情况
var firstBranch = approvalprocessQuery.Where(x => (x.node == 1 && x.approvalGroupId == 0) || x.node == 0);//分支一总流程
var secondBranch = approvalprocessQuery.Where(x => (x.node == 2 && x.approvalGroupId == 0) || x.node == 0);//分支二总流程
@@ -3288,7 +3307,7 @@ namespace YLErp.BLL
IQueryable<TradeLinq> query = null;
if (branch == null)
{
query = GetTradeLinqQuery(predicate, approvalprocessQuery, tradeOpenProcessOrder, 0);
query = GetTradeLinqQuery(predicate, approvalprocessQuery, tradeOpenProcessOrder, 0, closeProcessOrder);
}
else
{
@@ -3304,8 +3323,8 @@ namespace YLErp.BLL
{
nodeArr.Add(approvalConditionSecend.node);
}
query = GetTradeLinqQuery(predicate, firstBranch, firstCount, 1, branchIndex, nodeArr);
var secondQuery = GetTradeLinqQuery(predicate, secondBranch, secondCount, 2, branchIndex, nodeArr);
query = GetTradeLinqQuery(predicate, firstBranch, firstCount, 1, branchIndex, nodeArr, closeProcessOrder);
var secondQuery = GetTradeLinqQuery(predicate, secondBranch, secondCount, 2, branchIndex, nodeArr, closeProcessOrder);
query = query.Union(secondQuery);
}
@@ -3319,7 +3338,7 @@ namespace YLErp.BLL
query = query.OrderByDescending(s => s.OptDate);
return query;
}
private IQueryable<TradeLinq> GetTradeLinqQuery(Expression<Func<trade, bool>> predicate, IQueryable<approvalprocess> approvalprocessQuery, int tradeOpenProcessOrder, int branchOrder)
private IQueryable<TradeLinq> GetTradeLinqQuery(Expression<Func<trade, bool>> predicate, IQueryable<approvalprocess> approvalprocessQuery, int tradeOpenProcessOrder, int branchOrder, int closeProcessOrder = 0)
{
var query = from source in db.trade.Where(predicate)
join process in approvalprocessQuery on source.ProcessOrderId equals process.order into pro
@@ -3371,7 +3390,7 @@ namespace YLErp.BLL
OptId = source.OptId,
OptName = source.OptName,
OptDate = source.OptDate,
ProcessStatus = source.ProcessStatus == "审批中" ? source.ProcessStatus + " 流程" + (branchOrder != 0 && source.ProcessOrderId > branchOrder ? source.ProcessOrderId - 2 : source.ProcessOrderId - 1) + "/" + tradeOpenProcessOrder : source.ProcessStatus,
ProcessStatus = source.ProcessStatus == "审批中" ? source.ProcessStatus + " 流程" + (branchOrder != 0 && source.ProcessOrderId > branchOrder ? source.ProcessOrderId - 2 : source.ProcessOrderId - 1) + "/" + (source.TradeStatus == "平仓待复核" || source.TradeStatus == "行权待复核" || source.TradeStatus == "互换待复核" ? (closeProcessOrder > 0 ? closeProcessOrder : tradeOpenProcessOrder) : tradeOpenProcessOrder) : source.ProcessStatus,
ProcessOrderId = source.ProcessOrderId,
ProcessOrderBranch = source.ProcessOrderBranch,
ProcessRoleId = proce.roleId,
@@ -3393,7 +3412,7 @@ namespace YLErp.BLL
};
return query;
}
private IQueryable<TradeLinq> GetTradeLinqQuery(Expression<Func<trade, bool>> predicate, IQueryable<approvalprocess> approvalprocessQuery, int tradeOpenProcessOrder, int node, int branchOrder, List<int?> nodeArr)
private IQueryable<TradeLinq> GetTradeLinqQuery(Expression<Func<trade, bool>> predicate, IQueryable<approvalprocess> approvalprocessQuery, int tradeOpenProcessOrder, int node, int branchOrder, List<int?> nodeArr, int closeProcessOrder = 0)
{
var tradeStatus = new string[] { "平仓待复核", "行权待复核", "互换待复核" };
var query = from source in db.trade.Where(predicate)
@@ -3447,7 +3466,7 @@ namespace YLErp.BLL
OptId = source.OptId,
OptName = source.OptName,
OptDate = source.OptDate,
ProcessStatus = source.ProcessStatus == "审批中" ? source.ProcessStatus + " 流程" + (branchOrder != 0 && source.ProcessOrderId > branchOrder ? source.ProcessOrderId - 2 : source.ProcessOrderId - 1) + "/" + tradeOpenProcessOrder : source.ProcessStatus,
ProcessStatus = source.ProcessStatus == "审批中" ? source.ProcessStatus + " 流程" + (branchOrder != 0 && source.ProcessOrderId > branchOrder ? source.ProcessOrderId - 2 : source.ProcessOrderId - 1) + "/" + (source.TradeStatus == "平仓待复核" || source.TradeStatus == "行权待复核" || source.TradeStatus == "互换待复核" ? (closeProcessOrder > 0 ? closeProcessOrder : tradeOpenProcessOrder) : tradeOpenProcessOrder) : source.ProcessStatus,
ProcessOrderId = source.ProcessOrderId,
ProcessOrderBranch = source.ProcessOrderBranch,
ProcessRoleId = proce.roleId,
@@ -1,7 +1,8 @@
using BaseOUDAL;
using BaseOUDAL;
using YLErp.BLL;
using YLErp.DBModels.Consts;
using YLErp.DBModels.Enums;
using YLErp.Helpers;
namespace YLErp.Modules.TradeModule
{
@@ -46,23 +47,50 @@ namespace YLErp.Modules.TradeModule
return _tradeProcessCount.Value;
}
/// <summary>
/// 获取所有交易审批节点
/// 获取所有交易审批节点(开仓流程 TradeProcess)。
/// </summary>
/// <returns></returns>
public List<approvalprocess> TradeProcess()
{
var tradeOrders = DbContext.approvalprocess.Where(t => t.processType == "TradeProcess").OrderBy(o => o.order).ToList();
return tradeOrders;
}
/// <summary>
/// 按交易推断流程类别(开仓/了结)获取审批节点(需求②)。
/// <para>了结类(平仓待复核/行权待复核/互换待复核)取 CloseProcess;开仓类取 TradeProcess。</para>
/// <para>未配置对应流程时返回空列表(由调用方决定是否直接通过)。</para>
/// </summary>
public List<approvalprocess> TradeProcessByCategory(trade td)
{
var category = ResolveProcessCategory(td);
return DbContext.approvalprocess.Where(t => t.processType == category).OrderBy(o => o.order).ToList();
}
/// <summary>
/// 按类别统计审批节点数(需求②)。
/// </summary>
public int TradeProcessCountByCategory(trade td)
{
return TradeProcessByCategory(td).Count;
}
/// <summary>
/// 初始化交易 审批点
/// </summary>
/// <param name="td"></param>
public void InitTradeProcessOrder(trade td,int userId)
{
var tradeProcess = TradeProcessByCategory(td);
// 该交易类别未配置审批流程 → 直接审批通过,无需审核
if (tradeProcess == null || tradeProcess.Count == 0)
{
td.ProcessOrderId = ProcessTradeLog.;
td.ProcessStatus = ProcessTradeStatus..ToString();
td.ProcessOptDate = DateTime.Now;
return;
}
td.ProcessOrderId = ProcessTradeLog.;
td.ProcessStatus = ProcessTradeStatus..ToString();
var tradeProcess = TradeProcess();
var groupId = UserBLL.GetApprovalProcessGroup(userId);
bool approvalBranch = tradeProcess.Any(x => x.approvalGroupId != 0);//审批流程有分支情况
if (td.ProcessOrderId == 1)
@@ -88,6 +116,169 @@ namespace YLErp.Modules.TradeModule
{
td.ProcessOrderId = 2;
}
// 需求①:进入审批流程时即应用触发条件——从起始节点开始,跳过所有不满足触发条件的节点。
// 若所有节点均不满足 → 直接审批通过(无需任何人审核)。
ApplyTriggerFromStart(tradeProcess, td, userId);
}
/// <summary>
/// 从当前 ProcessOrderId 起点开始,按触发条件跳过无需审批的节点(需求①)。
/// <para>场景:交易提交进入审批流程时,若节点1、2配置的触发条件均不满足,则直接跳到节点3;
/// 若全部节点都不满足,则直接审批通过。</para>
/// </summary>
private void ApplyTriggerFromStart(List<approvalprocess> tradeProcess, trade td, int userId)
{
if (tradeProcess == null || tradeProcess.Count == 0) return;
// 取当前起点节点(主干 node=0)
var current = tradeProcess.FirstOrDefault(x => x.order == td.ProcessOrderId && x.node == 0);
// 若起点不在主干(如分支调整后 ProcessOrderId=2),取该 order 的主干节点
if (current == null)
{
current = tradeProcess.FirstOrDefault(x => x.order >= td.ProcessOrderId && x.node == 0);
}
if (current == null) return;
var ctx = BuildTriggerContext(td, userId);
var target = ConditionEvaluator.FindFirstTriggeredNode(tradeProcess, current, ctx);
if (target == null)
{
// 所有节点都不满足触发条件 → 直接审批通过,无需审核
td.ProcessOrderId = ProcessTradeLog.;
td.ProcessStatus = ProcessTradeStatus..ToString();
td.ProcessOptDate = DateTime.Now;
return;
}
// target 即为第一个满足触发条件、需实际审批的节点
if (target.order != td.ProcessOrderId)
{
td.ProcessOrderId = target.order;
}
}
/// <summary>
/// 节点触发条件(需求①):在已确定下一审批节点 nextOrder 后,若该节点配置了 triggerCondition
/// 则只有「满足触发条件」才进入该节点审批;不满足则跳过该节点,继续向后寻找,直到找到可进入的节点或抵达流程末尾。
/// <para>语义:triggerCondition 为空 → 无条件进入审批;非空 → 满足才进入,不满足则跳过。</para>
/// <para>非侵入式:原有分支推进逻辑不变,仅在其结果之上叠加触发判断循环。</para>
/// </summary>
/// <param name="tradeProcess">当前流程的全部节点(已按 order 排序)</param>
/// <param name="nextOrder">原逻辑计算出的下一节点(可能为 null)</param>
/// <param name="ctx">条件求值业务上下文(已含 trade、本次交易名义本金等)</param>
/// <returns>最终应推进到的节点;若应结束流程则返回 null</returns>
protected static approvalprocess AdvanceThroughTriggerNodes(
List<approvalprocess> tradeProcess,
approvalprocess nextOrder,
ConditionContext ctx)
{
// 不满足触发条件的节点需跳过:循环向后找第一个可进入的节点。
while (nextOrder != null && !string.IsNullOrWhiteSpace(nextOrder.triggerCondition))
{
if (ConditionEvaluator.Evaluate(nextOrder.triggerCondition, ctx))
{
break; // 满足触发条件 → 进入该节点审批,停止跳过。
}
// 不满足触发条件 → 跳过该节点,向后取下一个主干节点(node=0),继续判断。
nextOrder = tradeProcess.FirstOrDefault(x => x.order > nextOrder.order && x.node == 0);
}
return nextOrder;
}
/// <summary>
/// 构建触发条件求值上下文:从 trade 及其关联的 trade_cash 取本次交易名义本金(了结场景)。
/// <para>本次交易名义本金 = 本次了结操作的 trade_cash.UnwindStockEqvNotional 绝对值。</para>
/// </summary>
protected ConditionContext BuildTriggerContext(trade td, int userId)
{
var ctx = new ConditionContext
{
UserId = userId,
Trade = td,
ProcessCategory = ResolveProcessCategory(td),
InitGroupId = UserBLL.GetApprovalProcessGroup(userId)
};
// 了结场景:取本次平仓名义本金
if (td != null && ctx.ProcessCategory == ProcessCategoryConst.Close)
{
if (td.TradeType == "收益互换")
{
// 互换:从 swap_event.EventData(JSON) 反序列化取 CloseNotionalValue
var swapEvent = DbContext.swap_event
.Where(x => x.SwapTradeId == td.id && !x.Invalid
&& (x.EventType == (int)SwapEventTypeEnum. || x.EventType == (int)SwapEventTypeEnum.))
.OrderByDescending(x => x.id)
.FirstOrDefault();
ctx.CurrentNotional = CalcSwapCloseNotionalFromEventData(swapEvent?.EventData);
}
else
{
// 期权:当次平仓名义本金 = 期初名义本金 × 平仓比例(UnwindPercentRate),取绝对值
var tc = DbContext.trade_cash
.Where(t => t.TradeId == td.id && t.ValidState == ConsGlobal.InValid && !t.IsDeleted)
.OrderByDescending(t => t.id)
.FirstOrDefault();
ctx.CurrentNotional = CalcOptionCloseNotional(td.OriginalStockEqvNotional, tc?.UnwindPercentRate);
}
}
return ctx;
}
/// <summary>
/// 互换:从 swap_event.EventData(JSON) 反序列化取 CloseNotionalValue 绝对值。
/// 容错:EventData 为 null/空/非法 JSON / unwindData=null 时返回 0(不影响审批阈值判断)。
/// 抽为静态纯函数以支持无库单测。
/// </summary>
public static double CalcSwapCloseNotionalFromEventData(string eventData)
{
if (string.IsNullOrEmpty(eventData)) return 0;
try
{
var unwindData = Newtonsoft.Json.JsonConvert.DeserializeObject<UnwindData>(eventData);
return unwindData != null ? Math.Abs((double)unwindData.CloseNotionalValue) : 0;
}
catch
{
return 0;
}
}
/// <summary>
/// 期权:当次平仓名义本金 = 期初名义本金 × 平仓比例(UnwindPercentRate),取绝对值。
/// 容错:任一参数为 null 时返回 0。抽为静态纯函数以支持无库单测。
/// </summary>
public static double CalcOptionCloseNotional(double? originalStockEqvNotional, double? unwindPercentRate)
{
if (!originalStockEqvNotional.HasValue || !unwindPercentRate.HasValue) return 0;
return Math.Abs(originalStockEqvNotional.Value * unwindPercentRate.Value);
}
/// <summary>
/// 需求①:判断按触发条件是否需要审批。
/// <para>取该交易类别的审批流程,若所有节点配置的触发条件都不满足当前业务,则无需审批(返回 false)。</para>
/// <para>若无审批流程或节点无触发条件,返回 true(需要审批)。</para>
/// </summary>
protected bool NeedApprovalByTrigger(trade td)
{
var tradeProcess = TradeProcessByCategory(td);
if (tradeProcess == null || tradeProcess.Count == 0) return true;
var start = tradeProcess.FirstOrDefault(x => x.node == 0);
if (start == null) start = tradeProcess.OrderBy(x => x.order).First();
var ctx = BuildTriggerContext(td, UserId);
var target = ConditionEvaluator.FindFirstTriggeredNode(tradeProcess, start, ctx);
return target != null;
}
/// <summary>
/// 按交易状态推断流程类别:开仓(TradeProcess) / 了结(CloseProcess)。需求②。
/// <para>委托给 ProcessCategoryConst.Resolve,供全局复用。</para>
/// </summary>
protected static string ResolveProcessCategory(trade td)
{
return ProcessCategoryConst.Resolve(td);
}
/// <summary>
/// 添加交易操作日志
@@ -521,4 +712,31 @@ namespace YLErp.Modules.TradeModule
return rateCalcModeValue;
}
}
/// <summary>
/// 交易流程类别常量(需求②):对应 approvalprocess.processType 的取值。
/// </summary>
public static class ProcessCategoryConst
{
/// <summary>开仓审批流程</summary>
public const string Open = "TradeProcess";
/// <summary>了结/平仓/行权审批流程</summary>
public const string Close = "CloseProcess";
/// <summary>
/// 按交易状态推断流程类别:处于平仓/行权/互换待复核的交易视为「了结」类操作。
/// </summary>
public static string Resolve(trade td)
{
if (td == null) return Open;
if (td.TradeStatus == ConsTrade.
|| td.TradeStatus == ConsTrade.
|| td.TradeStatus == ConsTrade.)
{
return Close;
}
return Open;
}
}
}