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 { /// /// 收益互换审批节点的国联民生 OA 对接。 /// 本期只负责 HTTP 创建、查询和归档;审批结果由轮询任务回写本地流程。 /// public class TradeApprovalOAService : YLBaseService { private const string Pending = "提交成功"; private const string Syncing = "同步中"; private const string Ending = "归档中"; private readonly IYcLogger _logger = LogFactory.GetLogger(); 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; } /// /// 交易首次进入审批时,确保当前 OA 节点已创建对应流程。 /// 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); } /// /// 本地撤回前归档交易仍在审批中的 OA 流程。 /// 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; } /// /// 本地审批或撤回前归档 OA。通过条件更新抢占记录,避免与轮询回写同时推进同一节点。 /// 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 SyncPendingStatuses(string requestId = null) { var messages = new List(); 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 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 { "交易日期=" + 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(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("
", elementLines); var payload = new Dictionary { ["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 values) { return string.Join(";", values.Where(x => !string.IsNullOrWhiteSpace(x))); } } }