From a5a78eab100a02dde5615ed3cd9dcb4d82be8330 Mon Sep 17 00:00:00 2001 From: hjhan Date: Thu, 20 Aug 2026 18:14:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(swap):=20EQD-5320=20=E7=BB=93=E7=AE=97?= =?UTF-8?q?=E7=A1=AE=E8=AE=A4=E4=B9=A6=E9=82=AE=E4=BB=B6=E9=93=BE=E8=B7=AF?= =?UTF-8?q?=E8=A1=A5=E5=AE=9A=E4=BD=8D=E6=97=A5=E5=BF=97=E2=80=94=E2=80=94?= =?UTF-8?q?=E8=B5=84=E9=87=91=E6=8F=90=E7=A4=BA/=E7=BB=93=E6=B8=85?= =?UTF-8?q?=E7=A1=AE=E8=AE=A4/=E7=94=9F=E6=88=90=E4=B8=89=E6=9D=A1?= =?UTF-8?q?=E9=93=BE=E8=B7=AF=E6=AD=A4=E5=89=8D=E5=87=A0=E4=B9=8E=E6=97=A0?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E4=B8=94=E5=AD=98=E5=9C=A8=E9=9D=99=E9=BB=98?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5:=E2=91=A0SendConfirmEamil=E6=9F=A5=E4=B8=8D?= =?UTF-8?q?=E5=88=B0=E5=90=88=E5=90=8C=E5=8E=9Freturn=E7=A9=BA=E4=B8=B2(?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E5=BD=93=E5=B7=B2=E5=8F=91=E9=80=81)?= =?UTF-8?q?=E6=94=B9Error=E6=97=A5=E5=BF=97+=E6=98=8E=E7=A1=AE=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E5=8F=91=E9=80=81=E5=A4=B1=E8=B4=A5=E5=8E=9F=E5=9B=A0?= =?UTF-8?q?;=E2=91=A1CheckConfirmDoc/CheckSettlementDoc=E6=8A=9B=E9=94=99?= =?UTF-8?q?=E5=89=8D=E8=A1=A5=E6=89=93=E5=BA=93=E5=86=85trade=5Fcontract?= =?UTF-8?q?=5Fr=E8=A1=8C=E7=8A=B6=E6=80=81(=E6=97=A0=E8=A1=8C=3D=E4=BB=8E?= =?UTF-8?q?=E6=9C=AA=E7=94=9F=E6=88=90/IsValid=3Dfalse=3D=E9=87=8D?= =?UTF-8?q?=E7=94=9F=E6=88=90=E4=BD=9C=E5=BA=9F/doc=E7=BC=BA=E5=A4=B1);?= =?UTF-8?q?=E2=91=A2DealNoDmaEmail=E4=B8=8E=E7=94=9F=E6=88=90=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E4=B8=A4=E5=A4=84=E5=AE=A2=E6=88=B7+=E6=97=A5?= =?UTF-8?q?=E6=9C=9F=E6=89=A9=E5=BC=A0=E6=9F=A5=E8=AF=A2=E8=A1=A5Info?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E5=AF=B9=E9=BD=90=E5=8B=BE=E9=80=89vs?= =?UTF-8?q?=E5=AE=9E=E9=99=85=E5=A4=84=E7=90=86;=E2=91=A3GetFileName?= =?UTF-8?q?=E7=89=A9=E7=90=86=E6=96=87=E4=BB=B6=E7=BC=BA=E5=A4=B1=E5=8E=9F?= =?UTF-8?q?=E9=9D=99=E9=BB=98null=E6=94=B9Error;=E2=91=A4=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=99=A8=E6=9C=AA=E7=94=9F=E6=88=90=E4=BA=A4=E6=98=93?= =?UTF-8?q?=E7=A1=AE=E8=AE=A4=E4=B9=A6=E6=8A=A5=E9=94=99=E8=A1=A5=E4=BA=8B?= =?UTF-8?q?=E4=BB=B6id/=E5=AE=A2=E6=88=B7/=E5=B9=B3=E4=BB=93=E6=97=A5?= =?UTF-8?q?=E4=BE=BF=E4=BA=8E=E5=85=B3=E8=81=94=E6=89=A9=E5=BC=A0=E6=97=A5?= =?UTF-8?q?=E5=BF=97;IYcLogger=E6=97=A0Warn=E7=BB=9F=E4=B8=80=E7=94=A8Erro?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TradeSettleBillGenerator.cs | 4 +- .../SwapModule/SwapEndConfirmService.cs | 10 +++- .../SwapModule/SwapEventEmailService.cs | 46 ++++++++++++++++++- .../SwapSettlementBillGenerateService.cs | 6 +++ 4 files changed, 63 insertions(+), 3 deletions(-) diff --git a/Plugins/YLErp.Plugins.GuoLian/DocumentGenerator/TradeSettleBillGenerator.cs b/Plugins/YLErp.Plugins.GuoLian/DocumentGenerator/TradeSettleBillGenerator.cs index e78f361b..adbb66b5 100644 --- a/Plugins/YLErp.Plugins.GuoLian/DocumentGenerator/TradeSettleBillGenerator.cs +++ b/Plugins/YLErp.Plugins.GuoLian/DocumentGenerator/TradeSettleBillGenerator.cs @@ -48,7 +48,9 @@ namespace YLErp.Plugins.GuoLian.DocumentGenerator var confirmNo = Context.Gettrade_contract_r(tradeId, ContractTypeEnum.Trade); if (string.IsNullOrEmpty(confirmNo)) { - throw new ServiceException($"{trade.TradeNumber}未生成交易确认书"); + // 定位要点:带上事件id便于与 SwapSettlementBillGenerateService 的"生成范围扩张日志"对齐—— + // 报错交易常是扩张拉入的同客户同日平仓,并非用户勾选的那笔。 + throw new ServiceException($"{trade.TradeNumber}未生成交易确认书(平仓事件id={flowEventGroup.id}, tradeId={tradeId}, 客户={client.Name}, 平仓日={flowEventGroup.UnwindDate?.ToString("yyyy-MM-dd")});请先为该笔交易生成交易确认书后重试"); } row.TradeNumber = confirmNo; row.ClientName = client.Name; diff --git a/YLErpDAL/Modules/SwapModule/SwapEndConfirmService.cs b/YLErpDAL/Modules/SwapModule/SwapEndConfirmService.cs index c8786ab1..05c0ec86 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEndConfirmService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEndConfirmService.cs @@ -175,7 +175,15 @@ namespace YLErp.Modules.SwapModule var tradeContract = DbContext.trade_contract_r.Where(x => x.IsValid && x.TradeId == tradeId && x.Type == ContractTypeEnum.Trade).FirstOrDefault(); if (tradeContract == null) { - return ""; + // 定位要点:此处历史上静默 return "",前端当"已发送"但实际未发任何邮件。 + // 打出 warn 并把失败原因透传给前端,便于区分"没生成过交易确认书"与"被重生成作废(IsValid=false)"。 + var rows = DbContext.trade_contract_r.Where(x => x.TradeId == tradeId).Select(x => new { x.Type, x.IsValid, x.ContractCode }).ToList(); + var detail = rows.Any() + ? string.Join(";", rows.Select(r => $"Type={r.Type},IsValid={r.IsValid},Code={r.ContractCode}")) + : "trade_contract_r 无任何行"; + LogFactory.GetLogger("SwapEndConfirm").Error( + $"发送交易确认书邮件中止: tradeId={tradeId} 无有效交易确认书(需 IsValid=true 且 Type={ContractTypeEnum.Trade}); 库内实际行: {detail}"); + return "发送失败:未找到有效交易确认书,请先生成后再发送"; } tradeContract.send_email_result = "发送中"; DbContext.SaveChanges(); diff --git a/YLErpDAL/Modules/SwapModule/SwapEventEmailService.cs b/YLErpDAL/Modules/SwapModule/SwapEventEmailService.cs index a6bad2cc..f6a8e720 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEventEmailService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEventEmailService.cs @@ -164,6 +164,12 @@ namespace YLErp.Modules.SwapModule && f.UnwindDate == valueDate && f.PayDirection > 0 && eventTypes.Contains(f.EventType) && f.DataState == (int)SwapFlowDateStateEnum.完成 select f; var flowEventList = flowQuery.ToList(); + // 定位要点:发送范围按【客户+日期】整组扩张——用户勾选 A 事件,同客户同日的 B/C 事件会被一并拉入, + // 后续校验报错常是被拉入的事件缺确认书(用户以为"刚生成了还报错")。此日志把勾选与扩张结果对齐打出来。 + LogFactory.GetLogger("SwapEventEmail").Info( + $"资金提示邮件-发送范围: 用户勾选事件[{string.Join(",", EventEmailEmails.Select(s => s.event_id))}] " + + $"客户[{string.Join(",", clientIds.Distinct())}] 日期[{valueDate:yyyy-MM-dd}] " + + $"扩张后实际处理事件[{string.Join(",", flowEventList.Select(f => $"{f.id}:{f.SwapTradeNo}:" + (f.EventType == (int)SwapFlowEventTypeEnum.开仓 ? "开仓" : f.EventType == (int)SwapFlowEventTypeEnum.平仓 ? "平仓" : "T" + f.EventType)))}]"); var docs = new List(); var openFlowEventList = flowEventList.Where(x => x.EventType == (int)SwapFlowEventTypeEnum.开仓).ToList(); var closeFlowEventList = flowEventList.Where(x => x.EventType == (int)SwapFlowEventTypeEnum.平仓).ToList(); @@ -216,6 +222,22 @@ namespace YLErp.Modules.SwapModule } if (tradeNumbers.Any()) { + // 定位要点:报错只带交易编号不够定位。补打每笔缺失交易的 trade_contract_r 实际行状态 + //(无行=从未生成 / IsValid=false=被重生成作废 / document 行缺失=确认书文档被清理),命中哪种一眼可辨。 + var missTradeIds = flowEvents.Where(w => tradeNumbers.Contains(w.SwapTradeNo)).Select(s => s.SwapTradeId).Distinct().ToList(); + var missRows = DbContext.trade_contract_r.Where(x => missTradeIds.Contains(x.TradeId)).ToList(); + var missCodes = missRows.Select(r => r.ContractCode).Distinct().ToList(); + var docCodes = DbContext.trade_contract_document.Where(d => missCodes.Contains(d.Code)).Select(d => d.Code).Distinct().ToList(); + var detail = string.Join(";", missTradeIds.Select(tid => + { + var tradeNo = flowEvents.First(f => f.SwapTradeId == tid).SwapTradeNo; + var rows = missRows.Where(x => x.TradeId == tid).ToList(); + if (!rows.Any()) return $"{tradeNo}: trade_contract_r 无任何行(从未生成交易确认书)"; + return $"{tradeNo}: " + string.Join(",", rows.Select(r => + $"[Type={r.Type},IsValid={r.IsValid},Code={r.ContractCode},doc存在={(docCodes.Contains(r.ContractCode) ? "是" : "否")}]")); + })); + LogFactory.GetLogger("SwapEventEmail").Error( + $"交易确认书校验失败: 交易编号[{string.Join(",", tradeNumbers.Distinct())}]找不到有效交易确认书; 库内明细: {detail}"); throw new Exception($"交易编号为{string.Join(",", tradeNumbers.Distinct())}找不到有效的交易确认书附件,请检查或生成后再发送邮件"); } return list; @@ -265,6 +287,24 @@ namespace YLErp.Modules.SwapModule } if (tradeNumbers.Any()) { + // 定位要点:结算确认书按 SwapFlowEventId 精确匹配。补打每笔缺失平仓事件的合同行状态—— + // 常见原因是生成时的扩张查询覆盖了本事件但用户实际生成的是另一批,或重生成后 IsValid 被作废。 + var missEventIds = flowEvents.Where(w => tradeNumbers.Contains(w.SwapTradeNo)).Select(s => s.id).Distinct().ToList(); + var missRows = DbContext.trade_contract_r.Where(x => missEventIds.Contains(x.SwapFlowEventId ?? 0)).ToList(); + var missTradeIds = flowEvents.Where(w => tradeNumbers.Contains(w.SwapTradeNo)).Select(s => s.SwapTradeId).Distinct().ToList(); + var tradeTypeRows = DbContext.trade_contract_r.Where(x => missTradeIds.Contains(x.TradeId) && x.Type == ContractTypeEnum.Clearing).ToList(); + var detail = string.Join(";", flowEvents.Where(w => tradeNumbers.Contains(w.SwapTradeNo)).GroupBy(g => g.SwapTradeNo).Select(g => + { + var rows = missRows.Where(x => x.SwapFlowEventId == g.First().id).ToList(); + var byTrade = tradeTypeRows.Where(x => x.TradeId == g.First().SwapTradeId).ToList(); + return $"{g.Key}(事件{g.First().id}): " + (rows.Any() + ? string.Join(",", rows.Select(r => $"[Type={r.Type},IsValid={r.IsValid},Code={r.ContractCode}]")) + : $"按事件无行; 按交易的Clearing行=" + (byTrade.Any() + ? string.Join(",", byTrade.Select(r => $"[SwapFlowEventId={r.SwapFlowEventId},IsValid={r.IsValid},Code={r.ContractCode}]")) + : "无")); + })); + LogFactory.GetLogger("SwapEventEmail").Error( + $"结算确认书校验失败: 交易编号[{string.Join(",", tradeNumbers.Distinct())}]找不到有效结算确认书(SwapFlowEventId匹配); 库内明细: {detail}"); throw new Exception($"交易编号为{string.Join(",", tradeNumbers.Distinct())}找不到有效的结算确认书附件,请检查或生成后再发送邮件"); } return list; @@ -764,7 +804,11 @@ namespace YLErp.Modules.SwapModule if (System.IO.File.Exists(fName)) { return fName; } else - { return null; } + { + // 定位要点:物理文件缺失历史上静默返回 null(附件列表混入 null,发送结果不可预期),补 warn 便于发现"库里行在、盘上文件丢"。 + LogFactory.GetLogger("SwapEventEmail").Error($"邮件附件物理文件缺失: trade_contract_document.Paths={baseName} 映射后={fName} 不存在"); + return null; + } } } } diff --git a/YLErpDAL/Modules/TradeModule/DocGenerateModule/SwapSettlementBillGenerateService.cs b/YLErpDAL/Modules/TradeModule/DocGenerateModule/SwapSettlementBillGenerateService.cs index 2a66f0a8..26b32983 100644 --- a/YLErpDAL/Modules/TradeModule/DocGenerateModule/SwapSettlementBillGenerateService.cs +++ b/YLErpDAL/Modules/TradeModule/DocGenerateModule/SwapSettlementBillGenerateService.cs @@ -53,6 +53,12 @@ namespace YLErp.Modules.TradeModule.DocGenerateModule var allFlowEvents = DbContext.swap_flow_event.Where(x => eventIds.Contains(x.EventId)).ToList(); var tradeContracts = DbContext.trade_contract_r.Where(x=> tradeIds.Contains(x.TradeId)&&x.Type== ContractTypeEnum.Clearing&&x.IsValid).ToList(); flowEvents = newFlowEvents.ToList(); + // 定位要点:生成范围按【客户+平仓日】扩张——传入的是用户勾选的 flowEventIds, + // 实际生成会包含同客户同日全部平仓流水(PositionType>0)。若某笔交易未生成过交易确认书, + // TradeSettleBillGenerator 会整批抛"XX未生成交易确认书",此日志用于对齐"勾选了什么 vs 实际生成什么"。 + LogFactory.GetLogger("GenerateSingleV2").Info( + $"生成结算确认书-生成范围: 传入flowEventIds[{string.Join(",", flowEventIds)}] " + + $"扩张后待生成[{string.Join(",", flowEvents.Select(f => $"{f.id}:{f.SwapTradeNo}"))}]"); //---------------------------------- // 交易确认书数据 //----------------------------------