using BaseOUDAL; using NPOI.SS.Formula.Functions; using Qdp.Foundation.Utilities; using YLErp.DBModels; using YLErp.DBModels.Consts; using YLErp.DBModels.Enums; using YLErp.Model; using YLErp.Modules.ApiModule; using YLErp.Modules.TradeDalModule; using YLErp.Modules.UnderlyingModule; using static YLErp.ConsGlobal; namespace YLErp.Modules.TradeModule { /// /// 交易OA服务 /// public class TradeOAService : YLBaseService { private static readonly IYcLogger logger = LogFactory.GetLogger(); private static readonly System.Net.Http.HttpClient _httpClient = new System.Net.Http.HttpClient { Timeout = TimeSpan.FromMinutes(2) }; public TradeOAService(OptUserInfo userInfo) : base(userInfo) { } public TradeOAService(YLBaseService baseService) : base(baseService) { } /// /// 处理单个交易的OA提交 /// /// 交易ID /// 处理结果 public async Task ProcessSingleOAAsync(int tradeId) { try { // 获取交易信息 var trade = DbContext.trade.FirstOrDefault(x => x.id == tradeId); if (trade == null) { return new TradeOAResult { TradeId = tradeId, Success = false, Message = "未找到交易信息" }; } // 获取交易确认书信息 var tcr = DbContext.trade_contract_r.Where(t => t.Type == ContractTypeEnum.Trade && t.IsValid && t.TradeId == tradeId) .FirstOrDefault(); trade_contract_document tdoc = null; if (tcr != null) { tdoc = DbContext.trade_contract_document.Where(t => t.Code == tcr.ContractCode && t.Type == tcr.Type).FirstOrDefault(); } // 将之前的OA记录设置为无效 var existingOAResults = DbContext.tradeContractOaResult.Where(x => x.trade_id == tradeId && x.is_valid == true).ToList(); foreach (var existingResult in existingOAResults) { existingResult.is_valid = false; existingResult.SetOpt(UserInfo); } // 创建OA结果记录 var oaResult = new trade_contract_oa_result { trade_id = tradeId, contract_code = tcr?.ContractCode, status = "提交中", is_valid = true, }; oaResult.SetOpt(UserInfo); DbContext.tradeContractOaResult.Add(oaResult); await DbContext.SaveChangesAsync(); // 调用OA接口 var oaResp = await CallOAInterfaceAsync(trade, tcr?.ContractCode, tdoc, oaResult.id); if (oaResp != null && oaResp.code == 0) { oaResult.status = "提交成功"; oaResult.oa_fileid = oaResp.data?.requestid; oaResult.oa_msg = oaResp.data?.msg ?? "提交成功"; } else { oaResult.status = "提交失败"; oaResult.oa_msg = oaResp?.data?.msg ?? "OA接口调用失败"; } await DbContext.SaveChangesAsync(); return new TradeOAResult { TradeId = tradeId, Success = oaResult.status == "提交成功", Message = oaResult.oa_msg }; } catch (Exception ex) { logger.Error($"处理交易ID {tradeId} 的OA时发生错误: {ex.Message}", ex); return new TradeOAResult { TradeId = tradeId, Success = false, Message = $"提交OA失败: {ex.Message}" }; } } /// /// 上传附件到OA系统 /// /// 文件路径 /// 附件数据列表(包含 annexId 和 annexName) private async Task> UploadAttachmentAsync(string filePath) { try { // 从配置文件读取上传配置 var oaConfigSection = AppManager.GetConfiguration().GetSection("oa_confg"); var uploadUrl = oaConfigSection["UploadUrl"]; var systemToken = oaConfigSection["SystemToken"]; if (string.IsNullOrEmpty(uploadUrl)) { logger.Info("OA接口配置中UploadUrl为空"); throw new Exception("OA接口配置中上传地址为空"); } if (string.IsNullOrEmpty(systemToken)) { logger.Info("OA接口配置中SystemToken为空"); throw new Exception("OA接口配置中上传令牌为空"); } if (!File.Exists(filePath)) { logger.Info($"附件文件不存在:{filePath}"); throw new Exception("附件文件不存在"); } // 创建multipart/form-data请求 using (var httpClient = new HttpClient()) { // 设置超时时间为2分钟 httpClient.Timeout = TimeSpan.FromMinutes(2); using (var form = new MultipartFormDataContent()) { // 添加文件 using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read); var fileName = Path.GetFileName(filePath); form.Add(new StreamContent(fileStream), "file", fileName); // 添加systemToken form.Add(new StringContent(systemToken), "systemToken"); // 发送请求 try { var response = await httpClient.PostAsync(uploadUrl, form); response.EnsureSuccessStatusCode(); // 确保响应成功 var responseContent = await response.Content.ReadAsStringAsync(); logger.Info($"附件上传接口返回: {responseContent}"); // 解析响应 try { var uploadResponse = JsonHelper.Deserialize(responseContent); if (uploadResponse != null) { if (uploadResponse.code == "0" && uploadResponse.data != null) { // 返回完整的附件数据列表(包含 annexId 和 annexName) logger.Info($"成功上传附件,获取到{uploadResponse.data.Count}个附件"); return uploadResponse.data; } else { logger.Error($"附件上传失败:{uploadResponse.status},code:{uploadResponse.code}"); throw new Exception($"附件上传失败:{uploadResponse.status}"); } } else { logger.Error("附件上传响应解析为空"); throw new Exception("附件上传响应解析失败"); } } catch (Exception ex) { logger.Error($"解析附件上传响应失败:{ex.Message},响应内容:{responseContent}", ex); throw new Exception($"解析附件上传响应失败:{ex.Message}"); } } catch (TaskCanceledException ex) { logger.Error($"上传附件时任务被取消:{ex.Message}", ex); throw new Exception("上传附件超时,请检查网络连接"); } catch (Exception ex) { logger.Error($"上传附件时发生异常:{ex.Message}", ex); throw; } } } } catch (Exception ex) { logger.Error($"上传附件时发生异常:{ex.Message}", ex); throw; } } /// /// 调用OA接口 /// /// 交易信息 /// 合同编号 /// 交易确认书文档 /// OA结果记录ID /// 是否成功 private async Task CallOAInterfaceAsync(trade trade, string contractCode, trade_contract_document tdoc, int oaResultId) { try { // 获取OA配置 var oaConfigSection = AppManager.GetConfiguration().GetSection("oa_confg"); var baseUrl = oaConfigSection["BaseUrl"]; var subUrl = oaConfigSection["SubUrl"]; var systemToken = oaConfigSection["SystemToken"]; var systemName = oaConfigSection["SystemName"]; var isNextFlow = oaConfigSection["IsNextFlow"] ?? "0"; var flowId = oaConfigSection["FlowId"]; if (string.IsNullOrEmpty(baseUrl)) { logger.Info("OA接口配置中BaseUrl为空"); return new GuoLianOAResponse() { code = -1, data = new GuoLianOAData { msg = "OA接口配置中地址为空" } }; } if (string.IsNullOrEmpty(systemToken)) { logger.Info("OA接口配置中SystemToken为空"); return new GuoLianOAResponse() { code = -1, data = new GuoLianOAData { msg = "OA接口配置中SystemToken为空" } }; } // 处理附件 List attachmentDataList = null; if (tdoc != null && !string.IsNullOrEmpty(tdoc.AbsolutePath)) { try { attachmentDataList = await UploadAttachmentAsync(tdoc.AbsolutePath); logger.Info($"成功上传附件并获取到annexId:{string.Join(", ", attachmentDataList.Select(a => a.annexId))}"); } catch (Exception ex) { logger.Error($"上传附件失败:{tdoc.AbsolutePath},错误:{ex.Message}", ex); return new GuoLianOAResponse() { code = -1, data = new GuoLianOAData { msg = "上传附件失败" } }; } } if (tdoc == null) { logger.Info("未找到确认书文档"); return new GuoLianOAResponse() { code = -1, data = new GuoLianOAData { msg = "未找到确认书文档" } }; } //通过UserName找到LoginName var applicantLoginName = DbContextFactory.GetErpBaseContext().SystemUsers.Where(u => u.Name == UserInfo.UserName).Select(u => u.LoginName).FirstOrDefault(); // 构建detailList,yycl(应用场景/附件)放在明细列表中 var detailList = new List(); if (attachmentDataList != null && attachmentDataList.Any()) { foreach (var att in attachmentDataList) { detailList.Add(new { detailIndex = 1, yylxzx = 3, //用印类型 0公司章 1法人代表签章 2部门章 3营业部章 4无需用章 sl = 1,//用印数量 yzbgyx = "yangx", qysdzz = "2809700652352016752", yyslmx = 1, yycl = att }); } } // 构建dataMap var dataMap = new { title = $"固定收益部申请【{contractCode}】TRS交易确认书", urgentLevel = "正常",//未找到对应 applyDate = DateTime.Now.ToString("yyyy-MM-dd"), applicant = applicantLoginName, applicantComp = "国联民生证券总部", htlx = 0,//合同类型 0业务 1非业务 ywlx = 1, //业务类型 0资产管理业务金融产品筹设及调整事项合同(含ABS业务合同) 1金融交易类合同(除股票质押式回购业务部分购回、购回协议) 4采购类 3工会类 7党建活动类 2其他 6股票质押式回购业务部分购回、购回协议 8代销协议 htmj = 0, htmb = 0, //合同密级 0一般 1秘密 2绝密 qysdzz = "2809700652352016752", yyr = "yangx," + applicantLoginName, hehj = 0, //合同缓急 0一般 1紧急 2特急 //去除后缀 htmc = Path.GetFileNameWithoutExtension(tdoc.FileName),//合同名称 sfgljy = 1,//是否关联交易 0是 1否 jflx = 0,//甲方类型 0企业 1个人 jf = trade.ClientName,//甲方 yyfs = 0,//用印方式 0电子章 1物理章 yflx = 0,//乙方类型 0企业 1个人 yf = "国联民生证券股份有限公司",//乙方 sfdxtzdfqsjf = 1,//是否短信通知对方签署 0是 1否 sfdxtzyf = 1,//是否短信通知乙方 0是 1否 htje = trade.OriginalStockEqvNotional,//合同金额 htyxksrq = trade.StartDate?.ToString("yyyy-MM-dd"),//合同有效期开始日期 htyxjsrq = trade.ExerciseDate?.ToString("yyyy-MM-dd"),//合同有效期结束日期 bz = "人民币",//币种 htqdrq = DateTime.Now.ToString("yyyy-MM-dd"),//合同签订日期 sfqdljxy = 2, //是否签订廉洁协议 0已签 1未签 2无需签订 htbjjs = $"我司与{trade.ClientName}已完成NAFMII协议签署,并开展TRS交易:{contractCode},参与标的{trade.UnderlyingCode} {trade.UnderlyingName},参与标的数量面额{trade.OriginalStockEqvNotional}元人民币", //合同背景介绍 detailList = detailList.Any() ? detailList : null, creater = applicantLoginName }; // 构建查询参数(不包含dataMap,dataMap放在请求体中) // 注意:Java后端使用驼峰命名,参数名需与后端@RequestParam注解的名称一致 var query = $"flowId={Uri.EscapeDataString(flowId)}" + $"&isnextflow={Uri.EscapeDataString(isNextFlow ?? string.Empty)}" + $"&systemToken={Uri.EscapeDataString(systemToken)}" + $"&systemName={Uri.EscapeDataString(systemName ?? string.Empty)}"; var requestUrl = subUrl.Contains("?") ? $"{subUrl}&{query}" : $"{subUrl}?{query}"; logger.Info($"国联OA接口URL: {requestUrl}"); // 调用OA接口 (POST请求,dataMap放在请求体中) string response = null; try { // 设置较长的超时时间,默认15秒可能不够 var httpClient = new HttpClientWrap(baseUrl, 60); // 60秒超时 response = httpClient.PostJson(requestUrl, dataMap, null); logger.Info($"国联OA接口返回: {response}"); // 解析响应 var responseObj = JsonHelper.Deserialize(response); return responseObj; } catch (TaskCanceledException ex) { logger.Error($"调用OA接口时任务被取消:{ex.Message}", ex); return new GuoLianOAResponse() { code = -2, data = new GuoLianOAData { msg = "调用OA接口超时,请检查网络连接" } }; } catch (Exception ex) { logger.Error($"调用OA接口时发生异常:{ex.Message},响应内容:{response}", ex); return new GuoLianOAResponse() { code = -2, data = new GuoLianOAData { msg = $"调用OA接口时发生异常: {ex.Message}" } }; } } catch (Exception ex) { logger.Error($"调用OA接口时发生异常,交易ID:{trade.id},异常信息:{ex.Message}", ex); return new GuoLianOAResponse() { code = -2, data = new GuoLianOAData { msg = $"调用OA接口时发生异常: {ex.Message}" } }; } } /// /// 批量查询并同步 OA 用印状态 /// /// 处理结果列表 public List SyncOAStampStatus() { var results = new List(); // 1. 查询提交成功且有效的 OA 记录 var pendingOAs = DbContext.tradeContractOaResult .Where(x => x.is_valid && x.status == "提交成功" && !string.IsNullOrEmpty(x.oa_fileid)) .ToList(); if (!pendingOAs.Any()) { logger.Info("没有待同步的OA用印记录"); return results; } logger.Info($"找到 {pendingOAs.Count} 条待同步OA用印记录"); foreach (var oa in pendingOAs) { try { // 2. 调用 OA 查询接口 var queryResult = QueryOAStatus(oa.oa_fileid); if (queryResult == null) { logger.Info($"OA查询返回空,requestid:{oa.oa_fileid}"); continue; } if (queryResult.code != 0) { logger.Info($"OA查询失败,requestid:{oa.oa_fileid}, msg:{queryResult.msg}"); continue; } // 从 data 数组中找到匹配的 requestId var flowData = queryResult.data?.FirstOrDefault(d => d.requestId == oa.oa_fileid); if (flowData == null) { logger.Info($"OA查询结果中未找到匹配记录,requestid:{oa.oa_fileid}"); continue; } // 3. 判断用印是否完成:检查 detailMap 中是否有用印文件 if (!HasStampFiles(flowData.flowInfos)) { logger.Info($"OA用印未完成,detailMap 中无文件信息,requestid:{oa.oa_fileid}"); continue; } // 4. 获取用印文件并同步 var syncResult = SyncStampFileToDoc(oa, flowData); results.Add(syncResult); } catch (Exception ex) { logger.Error($"同步OA状态失败,OA ID:{oa.id}, requestid:{oa.oa_fileid}, 错误:{ex.Message}", ex); results.Add(new TradeOAResult { TradeId = oa.trade_id, Success = false, Message = $"同步失败: {ex.Message}" }); } } logger.Info($"OA用印同步完成,共处理 {results.Count} 条,成功 {results.Count(r => r.Success)} 条"); return results; } /// /// 查询 OA 审批状态 /// /// OA请求ID /// 查询结果 private OAStatusQueryResponse QueryOAStatus(string requestId) { try { var oaConfigSection = AppManager.GetConfiguration().GetSection("oa_confg"); var baseUrl = oaConfigSection["BaseUrl"]; var queryUrl = oaConfigSection["QueryUrl"]; var systemToken = oaConfigSection["SystemToken"]; var systemName = oaConfigSection["SystemName"]; if (string.IsNullOrEmpty(baseUrl) || string.IsNullOrEmpty(queryUrl)) { logger.Info("OA查询接口配置不完整"); return null; } var requestParam = $"{queryUrl}?requestId={Uri.EscapeDataString(requestId)}&systemToken={Uri.EscapeDataString(systemToken)}&systemName={Uri.EscapeDataString(systemName ?? string.Empty)}"; var httpClient = new HttpClientWrap(baseUrl, 30); var response = httpClient.Get(requestParam, null); logger.Info($"查询OA用印状态返回: {response}"); if (string.IsNullOrEmpty(response)) { return null; } return JsonHelper.Deserialize(response); } catch (Exception ex) { logger.Error($"查询OA状态异常,requestid:{requestId}, 错误:{ex.Message}", ex); return null; } } /// /// 通过 OA 接口下载用印文件 /// /// 文件 ID /// 文件字节数组 private byte[] DownloadOAFile(string fileId) { try { if (string.IsNullOrEmpty(fileId)) { return null; } var oaConfigSection = AppManager.GetConfiguration().GetSection("oa_confg"); var baseUrl = oaConfigSection["BaseUrl"]; var downloadFileUrl = oaConfigSection["DownloadFileUrl"]; var systemToken = oaConfigSection["SystemToken"]; var systemName = oaConfigSection["SystemName"]; if (string.IsNullOrEmpty(baseUrl) || string.IsNullOrEmpty(downloadFileUrl)) { logger.Info("OA下载接口配置不完整"); return null; } var requestUrl = $"{baseUrl.TrimEnd('/')}/{downloadFileUrl.TrimStart('/')}?fileid={Uri.EscapeDataString(fileId)}&systemToken={Uri.EscapeDataString(systemToken)}&systemName={Uri.EscapeDataString(systemName ?? string.Empty)}"; logger.Info($"通过OA接口下载用印文件: fileid={fileId}"); using (var httpClient = new System.Net.Http.HttpClient { Timeout = TimeSpan.FromMinutes(2) }) { var response = httpClient.GetAsync(requestUrl).Result; var fileBytes = response.Content.ReadAsByteArrayAsync().Result; // 检查返回的是否为错误 JSON if (response.Content.Headers.ContentType?.MediaType == "application/json" || (fileBytes.Length > 0 && fileBytes[0] == (byte)'{')) { var responseStr = System.Text.Encoding.UTF8.GetString(fileBytes); logger.Error($"下载用印文件接口返回错误: {responseStr}"); return null; } logger.Info($"通过OA接口下载用印文件成功,大小: {fileBytes.Length} 字节"); return fileBytes; } } catch (Exception ex) { logger.Error($"通过OA接口下载用印文件失败: fileid={fileId}, 错误:{ex.Message}", ex); return null; } } /// /// 将 OA 用印文件同步到确认书记录 /// /// OA结果记录 /// OA查询返回的流程数据 /// 处理结果 private TradeOAResult SyncStampFileToDoc(trade_contract_oa_result oa, OAFlowData queryData) { try { // 根据 contract_code 查找确认书文档(直接使用基类 DbContext) var contractDoc = DbContext.trade_contract_document.FirstOrDefault(d => d.Code == oa.contract_code); if (contractDoc == null) { logger.Info($"未找到确认书文档,contract_code:{oa.contract_code}"); return new TradeOAResult { TradeId = oa.trade_id, Success = false, Message = "未找到确认书文档" }; } // 从 fjFileInfo 中提取用印文件信息 if (!TryExtractFileFromFjFileInfo(queryData.flowInfos, out var fileId, out var fileName)) { logger.Info($"无法从 fjFileInfo 中提取用印文件信息,contract_code:{oa.contract_code}"); return new TradeOAResult { TradeId = oa.trade_id, Success = false, Message = "无法提取用印文件信息" }; } // 通过 OA 接口下载用印文件 var fileBytes = DownloadOAFile(fileId); if (fileBytes == null || fileBytes.Length == 0) { logger.Info($"未获取到用印文件,contract_code:{oa.contract_code}"); return new TradeOAResult { TradeId = oa.trade_id, Success = false, Message = "未获取到用印文件" }; } // 文件名安全过滤:防止路径遍历 fileName = SanitizeFileName(fileName); if (string.IsNullOrEmpty(fileName)) { fileName = $"{oa.contract_code}_用印.pdf"; } // 保存用印文件到本地 var tradeNumber = "未知"; var contractR = DbContext.trade_contract_r.FirstOrDefault(r => r.ContractCode == oa.contract_code && r.IsValid); if (contractR != null) { tradeNumber = contractR.TradeNumber; } var filePath = Path.Combine(OtcAppContext.AppDocsPath, "contractdoc", PS.Config.Company.ToString(), tradeNumber); if (!Directory.Exists(filePath)) { Directory.CreateDirectory(filePath); } var stampFileFullName = Path.Combine(filePath, fileName); if (File.Exists(stampFileFullName)) { File.Delete(stampFileFullName); } File.WriteAllBytes(stampFileFullName, fileBytes); logger.Info($"用印文件保存成功: {stampFileFullName}"); // 更新确认书文档状态 contractDoc.StampDocumentFileName = stampFileFullName.Replace(OtcAppContext.AppDocsPath, "/App_Docs").Replace("\\", "/"); // 根据 SourceType 判断用印状态:null或1=我方用印,2=双方用印 var sourceType = contractDoc.SourceType ?? 1; if (sourceType == 1) { contractDoc.Status = ContractStatusEnum.OurPartyStamped; // 我方用印 } else if (sourceType == 2) { contractDoc.Status = ContractStatusEnum.BothPartyStamped; // 双方用印 } contractDoc.OptDate = DateTime.Now; // 更新 OA 结果状态(oa 本身由基类 DbContext 追踪,直接修改即可) oa.status = "用印完成"; oa.OptDate = DateTime.Now; DbContext.SaveChanges(); logger.Info($"确认书 {oa.contract_code} 用印状态已更新为: {contractDoc.Status}"); return new TradeOAResult { TradeId = oa.trade_id, Success = true, Message = $"用印同步成功,状态: {contractDoc.Status}" }; } catch (Exception ex) { logger.Error($"同步用印文件失败,contract_code:{oa.contract_code}, 错误:{ex.Message}", ex); return new TradeOAResult { TradeId = oa.trade_id, Success = false, Message = $"同步失败: {ex.Message}" }; } } /// /// 判断 flowInfos 中是否存在用印文件 /// private bool HasStampFiles(Newtonsoft.Json.Linq.JObject flowInfos) { var fjFileInfo = flowInfos?["fjFileInfo"]; return fjFileInfo != null && fjFileInfo.Any(); } /// /// 从 fjFileInfo 中提取用印文件 ID 和文件名 /// private bool TryExtractFileFromFjFileInfo(Newtonsoft.Json.Linq.JObject flowInfos, out string fileId, out string fileName) { fileId = null; fileName = null; var fjFileInfo = flowInfos?["fjFileInfo"]; if (fjFileInfo == null || !fjFileInfo.Any()) { return false; } var firstFile = fjFileInfo.FirstOrDefault(); if (firstFile != null) { fileId = firstFile["id"]?.ToString(); fileName = firstFile["fileName"]?.ToString(); if (!string.IsNullOrEmpty(fileId)) { return true; } } return false; } /// /// 过滤文件名中的非法字符,防止路径遍历 /// private string SanitizeFileName(string fileName) { if (string.IsNullOrWhiteSpace(fileName)) { return null; } // 仅保留纯文件名,丢弃路径 fileName = Path.GetFileName(fileName); // 替换 Windows 非法字符 var invalidChars = Path.GetInvalidFileNameChars(); foreach (var c in invalidChars) { fileName = fileName.Replace(c, '_'); } // 防御 .. 路径遍历 fileName = fileName.Replace("..", "_"); return fileName; } } }