Files
zszq-trs/YLErpDAL/Modules/TradeModule/TradeApprovalOAService.cs
T

495 lines
27 KiB
C#
Raw 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.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 syncInstance = $"{Environment.MachineName}/pid:{Environment.ProcessId}/run:{Guid.NewGuid():N}";
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},实例:{syncInstance}");
foreach (var record in pending)
{
var claimed = false;
try
{
_logger.Info($"查询 OA 移动审批状态,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid},本地状态:{record.status},上次查询:{record.last_query_time:yyyy-MM-dd HH:mm:ss.fff},实例:{syncInstance}");
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"];
// OA 状态口径(以查询结果中的 flowStatusType + flowNode 为准):
// 0 + 退回:审批人退回,本地按拒绝处理;
// 3 + 结束:审批人批准,本地按通过处理;
// 3 + 强制归档:OA 流程被强制关闭。正常情况下,本地发起
// forceEndOaFlow 后记录会先变为“归档中”、成功后变为“已归档”,
// 不会进入本轮待查询集合;若仍被查询到,则视为同意。
// 0(非退回):草稿,继续等待。
// 1 + 审批人:OA 待办,继续等待。
// 其他组合也必须继续等待,不会仅凭 flowStatusType=1 或 3 推进本地流程。
var isReturned = flowStatusType == "0" && flowNode == "退回";
var isApproved = flowStatusType == "3"
&& (flowNode == "结束" || flowNode == "强制归档");
_logger.Info($"OA 移动审批查询结果,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid}flowStatusType:{flowStatusType}flowNode:{flowNode},判定:{(isReturned ? "退回" : isApproved ? "通过" : "等待")}");
if (!isApproved && !isReturned)
{
_logger.Info($"OA 移动审批继续等待,不会推进本地流程,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid}flowStatusType:{flowStatusType}flowNode:{flowNode},实例:{syncInstance}");
record.last_query_time = DateTime.Now;
DbContext.SaveChanges();
continue;
}
DbContext.SaveChanges();
_logger.Info($"OA 审批结果满足回写条件,准备抢占同步记录,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid},本地状态:{record.status},实例:{syncInstance}");
if (!TryClaimForSync(record.id))
{
_logger.Info($"OA 审批结果同步未抢占记录,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid},实例:{syncInstance}");
continue;
}
claimed = true;
_logger.Info($"OA 审批结果已抢占记录并置为同步中,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid},实例:{syncInstance}");
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},实例:{syncInstance}");
}
else
{
SetClaimedStatus(record.id, isReturned ? "退回" : "通过", null);
_logger.Info($"OA 审批结果回写本地成功,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid},结果:{(isReturned ? "退回" : "通过")},实例:{syncInstance}");
}
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},实例:{syncInstance}");
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)));
}
}
}