feat: 收益互换开平仓及收益结算接入OA移动审批

#EQD-6425 国联民生-衍生品系统移动审批(提交OA流程)
- 审批节点支持配置“关联OA移动审批”
- 创建OA流程并记录请求、requestId、状态及异常备注
- 支持OA轮询同步审批通过、退回和归档状态
- 本地通过、拒绝、撤回时同步强制归档未结束OA流程
- 增加审批列表OA备注展示及本地Mock联调接口
- 新增OA审批记录表和数据库升级脚本
- OA移动审批使用独立查询地址及isnextflow配置,不影响交易确认书OA
This commit is contained in:
tengyufan
2026-08-21 15:54:54 +08:00
parent 23a02e06c4
commit 3f0984800a
19 changed files with 784 additions and 9 deletions
@@ -263,6 +263,10 @@ namespace YLErp.Modules.TradeModule.DealModule
td.OptName = UserName;
td.OptDate = OptDate;
int passStatus = new ApprovalProcessService(OptUser).CheckTradeApprovalStep(td, OptUser.UserId);
if (passStatus != 0 && passStatus != -3)
{
new TradeApprovalOAService(this).EnsureForCurrentNode(td);
}
if (passStatus == 0)
{
td.TradeStatus = ConsTrade.;
@@ -56,6 +56,23 @@ namespace YLErp.Modules.TradeModule.DealModule
throw new ServiceException("该交易状态已变更,不能被审批,请先刷新页面");
}
if (!req.isFromOa && (req.status == "pass" || req.status == "reject"))
{
var currentNode = TradeProcessByCategory(td)
.Where(x => x.order == td.ProcessOrderId && (x.node == 0 || x.node == td.ProcessOrderBranch))
.OrderByDescending(x => x.node == td.ProcessOrderBranch)
.FirstOrDefault();
if (currentNode?.isOaApproval == true)
{
var oa = DbContext.tradeApprovalOaResult.FirstOrDefault(x => x.trade_id == td.id
&& x.approval_process_id == currentNode.id
&& x.is_valid
&& (x.status == "提交成功" || x.status == "同步中" || x.status == "归档中"));
if (oa != null && !new TradeApprovalOAService(this).ArchiveForLocalAction(oa, out var errorMessage))
throw new ServiceException(errorMessage);
}
}
if (req.status == "pass")
{
return TradePass(req, td);
@@ -179,6 +196,13 @@ namespace YLErp.Modules.TradeModule.DealModule
return result;
}
// 关联 OA 的节点必须先成功创建 OA 流程,才允许本地审批推进到该节点。
if (nextOrder.isOaApproval && td.TradeType == "收益互换")
{
// OA 创建是节点通过后的外部动作;明确失败只记录在 OA 结果表,不能回滚本地节点通过。
new TradeApprovalOAService(this).CreateForNode(td, nextOrder);
}
//-----------------------------------------------
// 继续审批流转
//-----------------------------------------------
@@ -788,6 +812,7 @@ namespace YLErp.Modules.TradeModule.DealModule
public bool notNeedOperationHistory;
public bool isPartialExercise;
public bool isExpire = false;
public bool isFromOa;
}
/// <summary>
@@ -50,6 +50,11 @@
return;
}
foreach (var td in revokeTrades)
{
ArchiveActiveOaOrThrow(td);
}
var tradeids = revokeTrades.Select(n => n.id);
var revokeTrade_cashs = DbContext.trade_cash.Where(c => c.ValidState == "InValid" && !c.IsDeleted && tradeids.Contains(c.TradeId)).ToList();
revokeTrade_cashs.ForEach(x =>
@@ -94,6 +99,7 @@
var Withdraws = DbContext.trade.Where(t => tradeIds.Contains(t.id)).ToList();
foreach (var td in Withdraws)
{
ArchiveActiveOaOrThrow(td);
var changesTradeStatus = DbContext.TradeAuditLog.Where(x => (x.OptType == "确认交易" || x.OptType == "交易特批-确认交易") && x.TradeId == td.id).OrderByDescending(x => x.id)?.FirstOrDefault()?.Changes;
td.TradeStatus = changesTradeStatus == ConsTrade. ? ConsTrade. : ConsTrade.;
td.CheckStatus = null;
@@ -103,6 +109,12 @@
}
DbContext.SaveChanges();
}
private void ArchiveActiveOaOrThrow(trade td)
{
if (!new TradeApprovalOAService(this).ArchiveActiveForTrade(td, out var errorMessage))
throw new ServiceException(errorMessage);
}
/// <summary>
/// 获取包含黑箱结构的子交易集合
/// </summary>
@@ -0,0 +1,481 @@
using BaseOUDAL;
using Newtonsoft.Json.Linq;
using YLErp.DBModels;
using YLErp.DBModels.Consts;
using YLErp.DBModels.Enums;
using YLErp.Modules.ApiModule;
using YLErp.Modules.TradeModule.DealModule;
namespace YLErp.Modules.TradeModule
{
/// <summary>
/// 收益互换审批节点的国联民生 OA 对接。
/// 本期只负责 HTTP 创建、查询和归档;审批结果由轮询任务回写本地流程。
/// </summary>
public class TradeApprovalOAService : YLBaseService
{
private const string Pending = "提交成功";
private const string Syncing = "同步中";
private const string Ending = "归档中";
private readonly IYcLogger _logger = LogFactory.GetLogger<TradeApprovalOAService>();
public TradeApprovalOAService(OptUserInfo userInfo) : base(userInfo) { }
public TradeApprovalOAService(YLBaseService baseService) : base(baseService) { }
public bool IsSupported(trade trade)
{
return trade != null && trade.TradeType == "收益互换";
}
public trade_approval_oa_result CreateForNode(trade trade, approvalprocess node)
{
if (!IsSupported(trade) || node == null || !node.isOaApproval)
return null;
var scenario = node.processType == "TradeProcess"
? "开仓"
: (trade.TradeStatus == ConsTrade. ? "平仓" : "收益结算");
_logger.Info($"开始创建 OA 移动审批,交易:{trade.id},节点:{node.id},场景:{scenario}");
var record = new trade_approval_oa_result
{
trade_id = trade.id,
approval_process_id = node.id,
process_type = node.processType,
applicant_id = UserId,
applicant_login_name = Config("Applicant"),
status = "提交中",
is_valid = true
};
record.SetOpt(UserInfo);
DbContext.tradeApprovalOaResult.Add(record);
DbContext.SaveChanges();
try
{
var creator = Config("Creator");
var applicant = Config("Applicant");
if (string.IsNullOrWhiteSpace(creator) || string.IsNullOrWhiteSpace(applicant))
throw new ServiceException("OA 配置缺少 Creator 或 Applicant");
var spr = GetSprLoginName(node);
var payload = BuildPayload(trade, creator, applicant, spr, scenario);
record.request_payload = JsonHelper.Serialize(payload);
var responseText = Post("SubUrl", payload);
record.last_response = responseText;
var response = JObject.Parse(responseText);
var responseData = response["data"] as JObject;
var responseCode = (int?)response["code"];
var responseStatus = (string)response["status"];
_logger.Info($"OA 创建流程原始响应,交易:{trade.id},节点:{node.id},响应:{responseText}");
if ((int?)response["code"] != 0 || !string.Equals((string)response["status"], "SUCCESS", StringComparison.OrdinalIgnoreCase))
{
record.status = "提交失败";
record.oa_msg = (string)responseData?["msg"] ?? (string)response["msg"] ?? "OA 创建流程失败";
_logger.Error($"创建 OA 移动审批返回失败,交易:{trade.id},节点:{node.id}code:{responseCode}status:{responseStatus}");
}
else
{
record.status = Pending;
record.oa_fileid = (string)responseData?["requestid"] ?? (string)responseData?["requestId"];
record.oa_msg = (string)responseData?["msg"] ?? (string)response["msg"];
if (string.IsNullOrWhiteSpace(record.oa_fileid))
{
record.status = "提交失败";
_logger.Error($"创建 OA 移动审批未返回 requestId,交易:{trade.id},节点:{node.id}");
}
else
{
_logger.Info($"创建 OA 移动审批成功,交易:{trade.id},节点:{node.id}requestId:{record.oa_fileid}");
}
}
}
catch (Exception ex)
{
record.status = "提交失败";
record.oa_msg = ex.Message;
_logger.Error($"创建 OA 移动审批失败,交易:{trade.id},节点:{node.id}", ex);
}
DbContext.SaveChanges();
return record;
}
/// <summary>
/// 交易首次进入审批时,确保当前 OA 节点已创建对应流程。
/// </summary>
public trade_approval_oa_result EnsureForCurrentNode(trade trade)
{
if (!IsSupported(trade) || trade.ProcessOrderId <= 0)
return null;
var node = GetCurrentNode(trade);
if (node?.isOaApproval != true)
return null;
var hasActiveRecord = DbContext.tradeApprovalOaResult.Any(x => x.trade_id == trade.id
&& x.approval_process_id == node.id
&& x.is_valid
&& (x.status == "提交中" || x.status == Pending || x.status == Syncing || x.status == Ending));
return hasActiveRecord ? null : CreateForNode(trade, node);
}
/// <summary>
/// 本地撤回前归档交易仍在审批中的 OA 流程。
/// </summary>
public bool ArchiveActiveForTrade(trade trade, out string errorMessage)
{
errorMessage = null;
if (!IsSupported(trade))
return true;
var records = DbContext.tradeApprovalOaResult
.Where(x => x.trade_id == trade.id
&& x.is_valid
&& !string.IsNullOrWhiteSpace(x.oa_fileid)
&& (x.status == Pending || x.status == Syncing || x.status == Ending))
.OrderBy(x => x.id)
.ToList();
if (records.Count == 0)
{
_logger.Info($"本地撤回前未找到待归档 OA 流程,交易:{trade.id}");
return true;
}
foreach (var record in records)
{
if (!ArchiveForLocalAction(record, out errorMessage))
return false;
}
return true;
}
/// <summary>
/// 本地审批或撤回前归档 OA。通过条件更新抢占记录,避免与轮询回写同时推进同一节点。
/// </summary>
public bool ArchiveForLocalAction(trade_approval_oa_result record, out string errorMessage)
{
errorMessage = null;
if (record == null)
return true;
_logger.Info($"本地操作前开始归档 OA 流程,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid}");
if (!TryChangeStatus(record.id, Pending, Ending, null))
{
_logger.Info($"本地操作前归档 OA 流程未抢占记录,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid}");
errorMessage = "OA 审批结果正在同步,请稍后再试";
return false;
}
if (!ForceEnd(record.oa_fileid))
{
TryChangeStatus(record.id, Ending, Pending, "OA 流程归档失败,等待重试");
_logger.Error($"本地操作前归档 OA 流程失败,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid}");
errorMessage = "OA 流程归档失败,暂不能执行本地审批操作";
return false;
}
TryChangeStatus(record.id, Ending, "已归档", "本地操作前已归档 OA 流程");
_logger.Info($"本地操作前归档 OA 流程成功,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid}");
return true;
}
public JObject Query(string requestId)
{
var query = "requestId=" + Uri.EscapeDataString(requestId ?? string.Empty)
+ "&systemToken=" + Uri.EscapeDataString(Config("SystemToken"))
+ "&systemName=" + Uri.EscapeDataString(Config("SystemName"));
var queryUrl = Config("TradeApprovalQueryUrl");
if (string.IsNullOrWhiteSpace(queryUrl))
throw new ServiceException("OA 配置缺少 TradeApprovalQueryUrl");
var url = queryUrl + (queryUrl.Contains("?") ? "&" : "?") + query;
var client = new HttpClientWrap(Config("BaseUrl"), 60);
return JObject.Parse(client.Get(url, null));
}
public bool ForceEnd(string requestId)
{
try
{
// 客户 OA 的 requestId 由 @RequestParam 接收,必须放在 URL QueryString。
var response = JObject.Parse(Post("ForceEndUrl", new { }, requestId));
var code = (int?)response["code"];
var status = (string)response["status"];
var responseData = response["data"] as JObject;
var resultCode = (string)responseData?["resultcode"];
var success = code == 0
&& string.Equals((string)response["status"], "SUCCESS", StringComparison.OrdinalIgnoreCase)
&& resultCode == "0";
_logger.Info($"OA 强制归档返回,requestId:{requestId}code:{code}status:{status}resultcode:{resultCode}success:{success}");
return success;
}
catch (Exception ex)
{
_logger.Error($"归档 OA 流程失败,requestId:{requestId}", ex);
return false;
}
}
public List<string> SyncPendingStatuses(string requestId = null)
{
var messages = new List<string>();
var staleAt = DateTime.Now.AddMinutes(-5);
var pending = DbContext.tradeApprovalOaResult
.Where(x => x.is_valid
&& !string.IsNullOrWhiteSpace(x.oa_fileid)
&& (string.IsNullOrWhiteSpace(requestId) || x.oa_fileid == requestId)
&& (x.status == Pending || (x.status == Syncing && (x.last_query_time == null || x.last_query_time < staleAt))))
.ToList();
_logger.Info($"开始轮询 OA 移动审批状态,requestId:{requestId ?? ""},待查询数量:{pending.Count}");
foreach (var record in pending)
{
var claimed = false;
try
{
_logger.Info($"查询 OA 移动审批状态,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid}");
var response = Query(record.oa_fileid);
record.last_response = response.ToString();
var flow = response["data"] is JArray data
? data.FirstOrDefault(x => (string)x["requestId"] == record.oa_fileid) as JObject
: null;
var flowStatusType = (string)flow?["flowStatusType"];
var flowNode = (string)flow?["flowNode"];
var isReturned = flowStatusType == "0" && flowNode == "退回";
// 1=批准;3=OA 批准后的自然归档。TRS 主动归档的记录不会进入本次查询。
var isApproved = flowStatusType == "1" || flowStatusType == "3";
_logger.Info($"OA 移动审批查询结果,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid}flowStatusType:{flowStatusType}flowNode:{flowNode},判定:{(isReturned ? "退" : isApproved ? "" : "")}");
if (!isApproved && !isReturned)
{
record.last_query_time = DateTime.Now;
DbContext.SaveChanges();
continue;
}
DbContext.SaveChanges();
if (!TryClaimForSync(record.id))
{
_logger.Info($"OA 审批结果同步未抢占记录,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid}");
continue;
}
claimed = true;
var trade = DbContext.trade.Find(record.trade_id);
if (trade == null)
{
SetClaimedStatus(record.id, "同步失败", "交易不存在");
continue;
}
if (GetCurrentNode(trade)?.id != record.approval_process_id)
{
SetClaimedStatus(record.id, "同步忽略", "当前审批节点已变化,忽略 OA 结果");
continue;
}
var result = new TradeOpenService(this).UpdateTradeProcessLog(new TradeOpenReqModel
{
tradeId = trade.id,
status = isReturned ? "reject" : "pass",
comments = isReturned ? "OA 移动审批退回" : "OA 移动审批通过",
isFromOa = true,
notNeedOperationHistory = false
});
if (!string.IsNullOrWhiteSpace(result.ErrorMsg))
{
SetClaimedStatus(record.id, Pending, result.ErrorMsg);
_logger.Error($"OA 审批结果回写本地失败,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid},原因:{result.ErrorMsg}");
}
else
{
SetClaimedStatus(record.id, isReturned ? "退回" : "通过", null);
_logger.Info($"OA 审批结果回写本地成功,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid},结果:{(isReturned ? "退" : "")}");
}
messages.Add(record.trade_id + ":" + (string.IsNullOrWhiteSpace(result.ErrorMsg) ? (isReturned ? "退回" : "通过") : Pending));
}
catch (Exception ex)
{
if (claimed)
{
SetClaimedStatus(record.id, Pending, ex.Message);
}
else
{
record.last_query_time = DateTime.Now;
record.oa_msg = ex.Message;
DbContext.SaveChanges();
}
_logger.Error($"轮询 OA 审批状态失败,requestId:{record.oa_fileid}", ex);
}
}
_logger.Info($"OA 移动审批状态轮询结束,已处理数量:{messages.Count}");
return messages;
}
private string Post(string urlKey, object payload, string requestId = null)
{
var path = Config(urlKey);
if (string.IsNullOrWhiteSpace(path))
path = urlKey == "ForceEndUrl" ? "/gateway/oaflow/forceEndOaFlow" : "/gateway/oaflow/createOaFlow";
var query = "systemToken=" + Uri.EscapeDataString(Config("SystemToken"))
+ "&systemName=" + Uri.EscapeDataString(Config("SystemName"));
if (urlKey == "ForceEndUrl")
query += "&requestId=" + Uri.EscapeDataString(requestId ?? string.Empty);
else
query += "&flowId=" + Uri.EscapeDataString(Config("MobileFlowId", "245103"))
+ "&isnextflow=" + Uri.EscapeDataString(Config("TradeApprovalIsNextFlow", "0"));
var url = path + (path.Contains("?") ? "&" : "?") + query;
return new HttpClientWrap(Config("BaseUrl"), 60).PostJson(url, payload, null);
}
private string Config(string key, string fallback = "")
{
if (key == "BaseUrl" || key == "SubUrl" || key == "TradeApprovalQueryUrl" || key == "ForceEndUrl")
{
var mockEnabled = bool.TryParse(AppManager.GetConfiguration()["TradeApprovalOaMock:Enabled"], out var enabled) && enabled;
var mockValue = AppManager.GetConfiguration()[$"TradeApprovalOaMock:{key}"];
if (mockEnabled && !string.IsNullOrWhiteSpace(mockValue))
return mockValue;
}
var value = AppManager.GetConfiguration()[$"oa_confg:{key}"];
return string.IsNullOrWhiteSpace(value) ? fallback : value;
}
private approvalprocess GetCurrentNode(trade trade)
{
if (trade == null) return null;
var processType = IsCloseScenario(trade) ? "CloseProcess" : "TradeProcess";
return DbContext.approvalprocess
.Where(x => x.processType == processType
&& x.order == trade.ProcessOrderId
&& (x.node == trade.ProcessOrderBranch || x.node == 0))
.OrderByDescending(x => x.node == trade.ProcessOrderBranch)
.FirstOrDefault();
}
private static bool IsCloseScenario(trade trade)
{
return trade.TradeStatus == ConsTrade.
|| trade.TradeStatus == ConsTrade.
|| trade.TradeStatus == ConsTrade.;
}
private bool TryClaimForSync(int recordId)
{
var now = DateTime.Now;
return DbContext.Database.ExecuteSqlRaw(
"UPDATE trade_approval_oa_result SET status = {0}, last_query_time = {1} " +
"WHERE id = {2} AND is_valid = 1 AND (status = {3} OR (status = {0} AND (last_query_time IS NULL OR last_query_time < {4})))",
Syncing, now, recordId, Pending, now.AddMinutes(-5)) == 1;
}
private bool TryChangeStatus(int recordId, string expectedStatus, string targetStatus, string message)
{
return DbContext.Database.ExecuteSqlRaw(
"UPDATE trade_approval_oa_result SET status = {0}, oa_msg = {1} WHERE id = {2} AND is_valid = 1 AND status = {3}",
targetStatus, message, recordId, expectedStatus) == 1;
}
private void SetClaimedStatus(int recordId, string status, string message)
{
DbContext.Database.ExecuteSqlRaw(
"UPDATE trade_approval_oa_result SET status = {0}, oa_msg = {1} WHERE id = {2} AND is_valid = 1 AND status = {3}",
status, message, recordId, Syncing);
}
private string GetSprLoginName(approvalprocess node)
{
using var baseDb = DbContextFactory.GetErpBaseContext();
var users = (from roleUser in baseDb.RoleUsers
join user in baseDb.SystemUsers on roleUser.UserId equals user.Id
where roleUser.RoleId == node.roleId && user.State == (int)UserState.Enabled
select user.LoginName).ToList();
if (users.Count == 0)
throw new ServiceException($"关联 OA 的审批角色未配置启用用户,角色ID:{node.roleId}");
if (users.Count > 1)
throw new ServiceException($"关联 OA 的审批角色配置了多个启用用户,角色ID:{node.roleId}");
if (string.IsNullOrWhiteSpace(users[0]))
throw new ServiceException($"关联 OA 的审批用户未配置登录名,角色ID:{node.roleId}");
return users[0];
}
private Dictionary<string, object> BuildPayload(trade trade, string creator, string applicant, string spr, string scenario)
{
var positions = DbContext.swap_position
.Where(x => x.SwapTradeId == trade.id && x.IsInitial && !x.Invalid)
.ToList();
var floatPosition = positions.FirstOrDefault(x => x.IsInitial && !string.IsNullOrWhiteSpace(x.UnderlyingCode));
var underlyingName = floatPosition == null
? string.Empty
: DbContext.underlying_manager.Where(x => x.UnderlyingCode == floatPosition.UnderlyingCode)
.Select(x => x.UnderlyingName)
.FirstOrDefault() ?? string.Empty;
var interestPositions = positions.Where(x => string.IsNullOrWhiteSpace(x.UnderlyingCode)
&& x.InterestMode != (int)InterestModeEnum.
&& x.InterestMode != (int)InterestModeEnum.
&& x.InterestMode != (int)InterestModeEnum.Unknown).ToList();
var marginPositions = positions.Where(x => x.InterestMode == (int)InterestModeEnum.
|| x.InterestMode == (int)InterestModeEnum.).ToList();
var tradeDate = trade.TradeDate?.ToString("yyyy-MM-dd") ?? string.Empty;
var startDate = floatPosition?.PosiStartDate.ToString("yyyy-MM-dd") ?? string.Empty;
var maturityDate = floatPosition?.PosiMatuirityDate?.ToString("yyyy-MM-dd") ?? string.Empty;
var contractDays = floatPosition?.PosiMatuirityDate == null
? string.Empty
: ((floatPosition.PosiMatuirityDate.Value.Date - floatPosition.PosiStartDate.Date).Days).ToString();
var elementLines = new List<string>
{
"交易日期=" + tradeDate,
"审批角色=衍生品负责人",
"交易编号=" + (trade.TradeNumber ?? string.Empty),
"对手方=" + (trade.ClientName ?? string.Empty),
"交易类型=" + scenario,
"浮动端多空头=" + (floatPosition == null ? string.Empty : floatPosition.PositionType == 1 ? "多头" : "空头"),
"利息端方向=" + JoinValues(interestPositions.Select(x => x.InterestDirection == 1 ? "收取" : "支付")),
"簿记账户=" + (trade.AssetBookName ?? string.Empty),
"结构类型=" + (trade.StructureType ?? string.Empty),
"标的代码=" + (floatPosition?.UnderlyingCode ?? string.Empty),
"标的名称=" + underlyingName,
"期初收益率=" + (floatPosition?.InitYtm?.ToString("0.####%") ?? string.Empty),
"期初价格=" + (floatPosition?.PosiNetPrice.ToString("0.####") ?? string.Empty),
"行权方式=" + (trade.GetExerciseModeCn() ?? string.Empty),
"合约期限=" + contractDays,
"数量=" + (floatPosition?.PosiQuantity.ToString("0.####") ?? string.Empty),
"起始日=" + startDate,
"到期日=" + maturityDate,
"交易员=" + (trade.TraderName ?? string.Empty),
"支付日期=" + JoinValues(marginPositions.Select(x => x.HappenDate?.ToString("yyyy-MM-dd") ?? string.Empty)),
"期初预付金=" + JoinValues(marginPositions.Select(x => x.InterestPrincipalFix.ToString("0.00"))),
"期初预付金方向=" + JoinValues(marginPositions.Select(x => x.InterestDirection == 1 ? "收取" : "支付"))
};
if (scenario == "开仓")
{
elementLines.Add("平仓金额=");
elementLines.Add("平仓日期=");
}
else
{
var eventType = scenario == "平仓" ? (int)SwapEventTypeEnum. : (int)SwapEventTypeEnum.;
var swapEvent = DbContext.swap_event.Where(x => x.SwapTradeId == trade.id && !x.Invalid && x.EventType == eventType)
.OrderByDescending(x => x.id).FirstOrDefault();
var unwind = string.IsNullOrWhiteSpace(swapEvent?.EventData) ? null : JsonHelper.Deserialize<UnwindData>(swapEvent.EventData);
elementLines.Add("平仓金额=" + (unwind?.SwapCloseAmount.ToString("0.##") ?? ""));
elementLines.Add("平仓日期=" + (unwind?.UnwindDate?.ToString("yyyy-MM-dd") ?? swapEvent?.ValueDate.ToString("yyyy-MM-dd") ?? ""));
}
var elements = string.Join("<br/>", elementLines);
var payload = new Dictionary<string, object>
{
["title"] = "衍生品系统TRS移动审批-" + trade.TradeNumber,
["creater"] = creator,
["applicant"] = applicant,
["spr"] = spr,
["applyDate"] = DateTime.Now.ToString("yyyy-MM-dd"),
["elements"] = elements
};
return payload;
}
private static string JoinValues(IEnumerable<string> values)
{
return string.Join("", values.Where(x => !string.IsNullOrWhiteSpace(x)));
}
}
}
+30
View File
@@ -287,6 +287,13 @@ namespace YLErp.BLL
.Select(a => new { a.order, a.roleId })
.ToList()
.ToDictionary(a => a.order, a => a.roleId);
var approvalNodes = db.approvalprocess
.Select(a => new { a.id, a.processType, a.order, a.node })
.ToList();
var oaRecords = db.tradeApprovalOaResult
.Where(x => swapTradeIds.Contains(x.trade_id) && x.is_valid)
.Select(x => new { x.id, x.trade_id, x.approval_process_id, x.status, x.oa_msg })
.ToList();
foreach (var tradeLinq in retListResult.rows)
{
@@ -305,6 +312,29 @@ namespace YLErp.BLL
tradeLinq.ProcessRoleName = name;
}
if (tradeLinq.TradeType == "收益互换")
{
var processType = tradeLinq.TradeStatus == "平仓待复核" || tradeLinq.TradeStatus == "行权待复核" || tradeLinq.TradeStatus == "互换待复核"
? "CloseProcess"
: "TradeProcess";
var currentNode = approvalNodes
.Where(x => x.processType == processType
&& x.order == tradeLinq.ProcessOrderId
&& (x.node == tradeLinq.ProcessOrderBranch || x.node == 0))
.OrderByDescending(x => x.node == tradeLinq.ProcessOrderBranch)
.FirstOrDefault();
var oa = currentNode == null ? null : oaRecords
.Where(x => x.trade_id == tradeLinq.id && x.approval_process_id == currentNode.id)
.OrderByDescending(x => x.id)
.FirstOrDefault();
if (oa != null)
{
tradeLinq.OaRemark = string.IsNullOrWhiteSpace(oa.oa_msg)
? oa.status
: oa.status + "" + oa.oa_msg;
}
}
if (tradeLinq.TradeType == "远期" || tradeLinq.TradeType == "掉期")
{
var option = db.trade_forward.FirstOrDefault(x => x.TradeId == tradeLinq.id);
@@ -120,6 +120,7 @@ namespace YLErp.Modules.TradeModule
// 需求①:进入审批流程时即应用触发条件——从起始节点开始,跳过所有不满足触发条件的节点。
// 若所有节点均不满足 → 直接审批通过(无需任何人审核)。
ApplyTriggerFromStart(tradeProcess, td, userId);
new TradeApprovalOAService(this).EnsureForCurrentNode(td);
}
/// <summary>