diff --git a/YLErpDAL/Modules/ReportModule/SettlementMailTemplateHelper.cs b/YLErpDAL/Modules/ReportModule/SettlementMailTemplateHelper.cs
new file mode 100644
index 00000000..45edfdf4
--- /dev/null
+++ b/YLErpDAL/Modules/ReportModule/SettlementMailTemplateHelper.cs
@@ -0,0 +1,107 @@
+using System.Globalization;
+using Microsoft.EntityFrameworkCore;
+using YLErp.BLL;
+using YLErp.Modules.AppModule;
+
+namespace YLErp.Modules.ReportModule
+{
+ ///
+ /// 为结算确认书邮件准备交易编号和平仓总额模板变量。
+ ///
+ public static class SettlementMailTemplateHelper
+ {
+ ///
+ /// 根据当前客户的确认书反查对应交易和平仓记录。
+ /// 文档编号可能在不同文档类型中重复,因此必须同时匹配编号和类型。
+ ///
+ public static List GetSettlementRows(YLContext dbContext, IEnumerable documents, int clientId)
+ {
+ var documentKeys = documents
+ .Where(document => !string.IsNullOrWhiteSpace(document.Code))
+ .Select(document => BuildDocumentKey(document.Code, document.Type))
+ .Distinct()
+ .ToHashSet();
+
+ if (documentKeys.Count == 0)
+ {
+ return new List();
+ }
+
+ var contractCodes = documents.Select(document => document.Code).Distinct().ToList();
+ var rows = (from relation in dbContext.trade_contract_r
+ join tradeCash in dbContext.trade_cash on relation.TradeCashId equals tradeCash.id
+ join tradeData in dbContext.trade on tradeCash.TradeId equals tradeData.id
+ where contractCodes.Contains(relation.ContractCode)
+ && relation.IsValid
+ && !tradeCash.IsDeleted
+ && tradeData.ClientId == clientId
+ select new
+ {
+ relation.ContractCode,
+ relation.Type,
+ Trade = tradeData,
+ TradeCash = tradeCash
+ }).AsNoTracking().ToList();
+
+ return rows
+ // 数据库先按编号缩小范围,再在内存中用“编号+类型”精确匹配附件。
+ .Where(row => documentKeys.Contains(BuildDocumentKey(row.ContractCode, row.Type)))
+ .Select(row => new Datas
+ {
+ trade = row.Trade,
+ tradecash = row.TradeCash
+ }).ToList();
+ }
+
+ ///
+ /// 将批量加载的数据限制到当前结算日,避免跨日发送时变量汇总串日。
+ ///
+ public static List ForValueDate(IEnumerable rows, DateTime valueDate)
+ {
+ return (rows ?? Enumerable.Empty())
+ .Where(row => row?.tradecash != null && row.tradecash.ValueDate.Date == valueDate.Date)
+ .ToList();
+ }
+
+ ///
+ /// 将去重后的交易编号及客户视角平仓总额写入邮件模板参数。
+ ///
+ public static void PopulateSettlementFields(MailInfoRequestModel request, IEnumerable rows)
+ {
+ ArgumentNullException.ThrowIfNull(request);
+
+ // 同一平仓记录可能因关联多份文档重复出现,按 trade_cash.id 去重以避免重复计入金额。
+ var distinctRows = (rows ?? Enumerable.Empty())
+ .Where(row => row?.trade != null && row.tradecash != null)
+ .GroupBy(row => row.tradecash.id)
+ .Select(group => group.First())
+ .ToList();
+
+ request.TradeNumber = string.Join(",", distinctRows
+ .Select(row => row.trade.TradeNumber)
+ .Where(number => !string.IsNullOrWhiteSpace(number))
+ .Distinct()
+ .OrderBy(number => number));
+
+ if (distinctRows.Count == 0)
+ {
+ request.UnwindTotalAmount = string.Empty;
+ return;
+ }
+
+ // trade_cash.Amount 为交易员视角,邮件展示需取反转换为客户视角。
+ var customerAmount = -distinctRows.Sum(row => row.tradecash.Amount);
+ if (customerAmount == 0)
+ {
+ // 消除浮点求和可能产生的 -0.00。
+ customerAmount = 0;
+ }
+ request.UnwindTotalAmount = customerAmount.ToString("F2", CultureInfo.InvariantCulture);
+ }
+
+ private static string BuildDocumentKey(string code, string type)
+ {
+ return $"{code}\u001f{type}";
+ }
+ }
+}