diff --git a/Plugins/YLErp.Plugins.GuoLian/DocumentGenerator/TradeSettleBillGenerator.cs b/Plugins/YLErp.Plugins.GuoLian/DocumentGenerator/TradeSettleBillGenerator.cs index 1521fbb4..1109b485 100644 --- a/Plugins/YLErp.Plugins.GuoLian/DocumentGenerator/TradeSettleBillGenerator.cs +++ b/Plugins/YLErp.Plugins.GuoLian/DocumentGenerator/TradeSettleBillGenerator.cs @@ -74,11 +74,6 @@ namespace YLErp.Plugins.GuoLian.DocumentGenerator var underlying = Context.GetTradeUnderlying(flowEventGroup.UnderlyingCode); var closeNotionalValue = unwindData?.CloseNotionalValue ?? flowEventGroup.Quantity * flowEventGroup.ContractSize * posi.PosiGrossPrice; - var settlementDate = flowEventGroup.UnwindDate - ?? throw new ServiceException($"平仓事件{flowEventGroup.id}缺少结算日"); - var currentDayFloatingDividend = Context.GetEodPositions(tradeId, settlementDate) - .FirstOrDefault(x => x.PositionId == flowEventGroup.PositionId) - ?.TdPosiDividend ?? 0m; // 行构造器统一处理客户视角、结算公式、品种差异和模板展示精度。 var row = SwapSettlementBillRowBuilder.Build(new SwapSettlementBillRowInput @@ -91,7 +86,6 @@ namespace YLErp.Plugins.GuoLian.DocumentGenerator Positions = positions, UnderlyingInstrumentType = underlying?.UnderlyingInstrumentType, CloseNotionalValue = closeNotionalValue, - CurrentDayFloatingDividend = currentDayFloatingDividend, // 与提前终止详情页保持同一来源:读取 swap_flow_event 中的平仓浮动腿记录。 ExitYtm = flowEventGroup.ExitYtm, IncludePeriodPaymentInNetting = (tradeExtend?.ExtendObj?.DividendPayDate ?? 1) == 0 diff --git a/UnitTestProject/Modules/SwapModule/SwapSpanBalanceCalcTest.cs b/UnitTestProject/Modules/SwapModule/SwapSpanBalanceCalcTest.cs index fda8849a..0c0f2588 100644 --- a/UnitTestProject/Modules/SwapModule/SwapSpanBalanceCalcTest.cs +++ b/UnitTestProject/Modules/SwapModule/SwapSpanBalanceCalcTest.cs @@ -3,9 +3,12 @@ using YLErp.Modules.SwapModule.Margin; namespace YLErp.Modules.SwapModule { /// - /// R2 阶段三 §3.2 可用资金公式测试(需求拆分 R2 口径,实时/EOD/报告三处共用 SwapSpanBalanceCalc): - /// 客户维度 = Max(现金结存 + 授信 − 已使用授信 + 初始保证金 − 维持保证金, 0); - /// 合约维度 = Max(现金结存 + 授信 − 已使用授信 − 交易维度追加保证金合计, 0)。 + /// R2 阶段三 §3.2 可用资金/追保/可取资金公式测试(定稿 2026-08-28 口径,实时/EOD/报告三处共用 SwapSpanBalanceCalc): + /// 客户维度可用 = Max(现金结存 + 授信 − 已使用授信 + 初始保证金 − 维持保证金, 0); + /// 合约维度可用 = Max(现金结存 + 授信 − 已使用授信, 0)(定稿删除"−交易维度追加保证金",避免与现金结存/已使用授信双重扣减); + /// 追保金额两维度均 Max(...,0) 截断、恒 ≥ 0(只追不退);合约维度(2026-08-28 调整)= 当日追加保证金现金部分合计 + /// (不与现金结存轧差、闲置现金不冲抵、授信不追),客户维度 = Max((维持−初始) − (现金+授信−已使用), 0); + /// 可取资金只算现金部分:合约维度 = Max(现金结存 + min(持仓盈亏,0), 0),客户维度 = Max(现金结存 + 初始 − 维持 + min(持仓盈亏,0), 0)。 /// 授信额度为 credit.Credit 合计(保存时已折算比例),现金结存=期末结存(阶段二起授信不进资金)。 /// [TestClass] @@ -35,40 +38,27 @@ namespace YLErp.Modules.SwapModule cashBalance: 20, totalCredit: 100, usedCredit: 20, initialMargin: 100, maintenanceMargin: 130), 1e-6); } + /// + /// 合约维度可用资金(定稿 2026-08-28):Max(现金结存 + 授信 − 已使用授信, 0),不再减交易维度追加保证金。 + /// [TestMethod] - public void SB_004_合约维度_按交易追加合计扣减() + public void SB_004_合约维度_可用资金() { - //现金20 + 授信100 − 已使用20 − 追加合计30 = 70 - Assert.AreEqual(70, SwapSpanBalanceCalc.CalcContractDimensionAvailable( - cashBalance: 20, totalCredit: 100, usedCredit: 20, tradeAdditionalMarginSum: 30), 1e-6); + //现金20 + 授信100 − 已使用20 = 100 + Assert.AreEqual(100, SwapSpanBalanceCalc.CalcContractDimensionAvailable( + cashBalance: 20, totalCredit: 100, usedCredit: 20), 1e-6); } [TestMethod] public void SB_005_合约维度_扣尽截断为0() { + //现金20 + 授信0 − 已使用30 = −10 → 0 Assert.AreEqual(0, SwapSpanBalanceCalc.CalcContractDimensionAvailable( - cashBalance: 20, totalCredit: 0, usedCredit: 0, tradeAdditionalMarginSum: 30), 1e-6); + cashBalance: 20, totalCredit: 0, usedCredit: 30), 1e-6); } /// - /// 阶段三口径一致性:无追加保证金流水时(阶段四前),两维度公式数值一致—— - /// 交易追加合计 = Σ(维持−累计) = 维持 − 初始,与客户维度的 (初始−维持) 项互相抵消。 - /// - [TestMethod] - public void SB_006_阶段三两维度数值一致() - { - var cash = 20d; - var credit = 100d; - var used = 20d; - var initial = 100d; - var maintenance = 130d; - var clientDimension = SwapSpanBalanceCalc.CalcClientDimensionAvailable(cash, credit, used, initial, maintenance); - var contractDimension = SwapSpanBalanceCalc.CalcContractDimensionAvailable(cash, credit, used, maintenance - initial); - Assert.AreEqual(clientDimension, contractDimension, 1e-9); - } - - /// - /// 阶段四 §4.2 客户维度双向追保:正数=需追保,不以 0 截断。 + /// 客户维度追保金额(定稿 2026-08-28:Max(...,0) 截断,恒 ≥ 0,只追不退)。 /// [TestMethod] public void SB_010_客户维度追保金额_正数需追保() @@ -79,15 +69,15 @@ namespace YLErp.Modules.SwapModule } [TestMethod] - public void SB_011_客户维度追保金额_负数可返还() + public void SB_011_客户维度追保金额_盈余截断为0() { - //差额 (维持130−初始100)=30,资金 20+100−20=100 → 追保 = 30 − 100 = −70(可返还,不截断为 0) - Assert.AreEqual(-70, SwapSpanBalanceCalc.CalcClientDimensionCallMargin( + //差额 (维持130−初始100)=30,资金 20+100−20=100 → 30 − 100 = −70 → 截断为 0(资金富余不展示负数) + Assert.AreEqual(0, SwapSpanBalanceCalc.CalcClientDimensionCallMargin( cashBalance: 20, totalCredit: 100, usedCredit: 20, initialMargin: 100, maintenanceMargin: 130), 1e-6); } /// - /// 客户维度追保金额与可用资金公式互为反向(去 Max 截断):追保 = −(未截断可用资金)。 + /// 客户维度追保金额与可用资金公式互为反向(各加 Max 截断):追保 = Max(−(未截断可用资金), 0)。 /// [TestMethod] public void SB_012_客户维度追保与可用资金反向一致() @@ -100,7 +90,7 @@ namespace YLErp.Modules.SwapModule var available = SwapSpanBalanceCalc.CalcClientDimensionAvailable(cash, credit, used, initial, maintenance); var callMargin = SwapSpanBalanceCalc.CalcClientDimensionCallMargin(cash, credit, used, initial, maintenance); var availableUnfloored = cash + credit - used + initial - maintenance; - Assert.AreEqual(-availableUnfloored, callMargin, 1e-9); + Assert.AreEqual(Math.Max(-availableUnfloored, 0), callMargin, 1e-9); //可用资金被 0 截断时追保为正(需追保),两者不矛盾 if (availableUnfloored < 0) { @@ -110,22 +100,60 @@ namespace YLErp.Modules.SwapModule } /// - /// 合约维度追保金额(Max((现金+授信−已使用)×−1, 0)):账户透支为正=应补足,盈余截断为 0。 + /// 合约维度追保金额(2026-08-28 新口径):当日产生的追加保证金现金部分合计,不与现金结存轧差、 + /// 闲置现金不冲抵、授信部分不追;Max(...,0) 兜底截断(正常无负记录,追保回落不返还)。 /// [TestMethod] - public void SB_013_合约维度追保金额_透支为正() + public void SB_013_合约维度追保金额_当日现金追加全额() { - //现金−80 + 授信100 − 已使用30 = −10 → 追保 = 10(应补足) - Assert.AreEqual(10, SwapSpanBalanceCalc.CalcContractDimensionCallMargin( - cashBalance: -80, totalCredit: 100, usedCredit: 30), 1e-6); + //场景:入金60万、现金初保50万(闲置10万),当日需追加40万(现金)—— + //旧口径缺口法追 30万(闲置现金被冲抵);新口径追当日现金追加全额 40万 + Assert.AreEqual(400000, SwapSpanBalanceCalc.CalcContractDimensionCallMargin(400000), 1e-6); } [TestMethod] - public void SB_014_合约维度追保金额_盈余截断为0() + public void SB_014_合约维度追保金额_无当日追加为0() { - //现金50 + 授信100 − 已使用20 = 130 → 盈余,Max(...,0) 截断 → 追保 = 0 - Assert.AreEqual(0, SwapSpanBalanceCalc.CalcContractDimensionCallMargin( - cashBalance: 50, totalCredit: 100, usedCredit: 20), 1e-6); + //当日追加全走授信(无现金流水)/当日无追保 → 0;负值兜底截断为 0 + Assert.AreEqual(0, SwapSpanBalanceCalc.CalcContractDimensionCallMargin(0), 1e-6); + Assert.AreEqual(0, SwapSpanBalanceCalc.CalcContractDimensionCallMargin(-100), 1e-6); + } + + /// + /// 可取资金(定稿 2026-08-28):只算现金部分(授信不可取现);min(持仓盈亏,0) 只扣浮亏、浮盈不放行。 + /// + [TestMethod] + public void SB_015_客户维度可取资金_浮亏扣减() + { + //现金200 + 初始100 − 维持150 + min(−30,0) = 120 + Assert.AreEqual(120, SwapSpanBalanceCalc.CalcClientDimensionDesirableFund( + cashBalance: 200, initialMargin: 100, maintenanceMargin: 150, positionPnl: -30), 1e-6); + } + + [TestMethod] + public void SB_016_客户维度可取资金_浮盈不放行() + { + //现金200 + 初始100 − 维持150 + min(50,0)=0 → 150 + Assert.AreEqual(150, SwapSpanBalanceCalc.CalcClientDimensionDesirableFund( + cashBalance: 200, initialMargin: 100, maintenanceMargin: 150, positionPnl: 50), 1e-6); + } + + [TestMethod] + public void SB_017_客户维度可取资金_截断为0() + { + //现金20 + 初始100 − 维持150 + min(−40,0) = −70 → 0 + Assert.AreEqual(0, SwapSpanBalanceCalc.CalcClientDimensionDesirableFund( + cashBalance: 20, initialMargin: 100, maintenanceMargin: 150, positionPnl: -40), 1e-6); + } + + [TestMethod] + public void SB_018_合约维度可取资金() + { + //Max(100 + min(−30,0), 0) = 70;浮亏超过现金时截断为 0 + Assert.AreEqual(70, SwapSpanBalanceCalc.CalcContractDimensionDesirableFund(cashBalance: 100, positionPnl: -30), 1e-6); + Assert.AreEqual(0, SwapSpanBalanceCalc.CalcContractDimensionDesirableFund(cashBalance: 20, positionPnl: -50), 1e-6); + //浮盈不放行:Max(100 + min(80,0), 0) = 100 + Assert.AreEqual(100, SwapSpanBalanceCalc.CalcContractDimensionDesirableFund(cashBalance: 100, positionPnl: 80), 1e-6); } } } diff --git a/UnitTestProject/Modules/UnderlyingModule/FundManagerLookupServiceTest.cs b/UnitTestProject/Modules/UnderlyingModule/FundManagerLookupServiceTest.cs new file mode 100644 index 00000000..15ca2361 --- /dev/null +++ b/UnitTestProject/Modules/UnderlyingModule/FundManagerLookupServiceTest.cs @@ -0,0 +1,46 @@ +using Dapper; +using YLErp.Modules.UnderlyingModule; + +namespace YLErp.UnitTestProject.Modules.UnderlyingModule +{ + /// + /// 基金管理人取数(bigdata 数据源)连通性与库名插值验证(连 96 测试库): + /// - DbSchema.Of 从连接串解析物理库名(glms_bigdata),SQL 不再硬编码库名、跨环境库名不同也能命中; + /// - Lookup 全链路(连接 96 → 插值 SQL 执行 → 结果归并)不返回 Unavailable 即为连通且 SQL 有效。 + /// + [TestClass] + public class FundManagerLookupServiceTest + { + [TestMethod] + public void DbSchema_从连接串解析物理库名() + { + Assert.AreEqual("`glms_bigdata`", DbSchema.Of("bigdata")); + Assert.AreEqual("`glms_yltrs_ylcms`", DbSchema.Of("ylcms")); + } + + [TestMethod] + public void Lookup_连接96大数据库_非降级() + { + var service = new FundManagerLookupService(); + //96 库现有测试数据 161210/630006/511160(SECUCODE 无后缀,与去后缀匹配逻辑一致),用库存代码验证全链路命中 + foreach (var code in new[] { "511160.SH", "161210.SZ", "630006.SH" }) + { + var result = service.Lookup(code); + Console.WriteLine($"code={code} → Status={result.Status}, InvestAdvisorName={result.InvestAdvisorName}"); + Assert.AreNotEqual(FundManagerLookupStatus.Unavailable, result.Status, + $"code={code} 返回 Unavailable:96 bigdata 库不可达或插值 SQL 执行失败"); + } + } + + [TestMethod] + public void Diag_查看库存secucode样例与行数() + { + using var connection = new MySqlConnector.MySqlConnection(AppManager.GetConnectionString("bigdata")); + var count = connection.ExecuteScalar($"SELECT COUNT(*) FROM {DbSchema.Of("bigdata")}.mf_fundarchives"); + var samples = connection.Query($"SELECT SECUCODE FROM {DbSchema.Of("bigdata")}.mf_fundarchives LIMIT 8"); + Console.WriteLine($"mf_fundarchives 行数={count}, SECUCODE样例=[{string.Join(",", samples)}]"); + var advCount = connection.ExecuteScalar($"SELECT COUNT(*) FROM {DbSchema.Of("bigdata")}.mf_investadvisoroutline"); + Console.WriteLine($"mf_investadvisoroutline 行数={advCount}"); + } + } +} diff --git a/UnitTestProject/appsettings.json b/UnitTestProject/appsettings.json index 7505ceff..441d3eae 100644 --- a/UnitTestProject/appsettings.json +++ b/UnitTestProject/appsettings.json @@ -3,7 +3,8 @@ "ylcms": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_yltrs_ylcms;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;", "yladmin": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_yltrs_admin;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;", "ylclient": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_yltrs_client;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;", - "bondoms": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=zszq_bond_oms;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;" + "bondoms": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=zszq_bond_oms;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;", + "bigdata": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_bigdata;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;" }, "LibreOffice": { "ExePath": "", diff --git a/YLErpDAL/AppManager.cs b/YLErpDAL/AppManager.cs index 609c870a..588c857a 100644 --- a/YLErpDAL/AppManager.cs +++ b/YLErpDAL/AppManager.cs @@ -171,7 +171,7 @@ namespace YLErp "yladmin" => _configuration.GetConnectionString("yladmin"), "ylclient" => _configuration.GetConnectionString("ylclient"), "bondoms" => _configuration.GetConnectionString("bondoms"), - "glms_bigdata" => _configuration.GetConnectionString("glms_bigdata"), + "bigdata" => _configuration.GetConnectionString("bigdata"), "apex_oracle"=> _configuration.GetConnectionString("apex_oracle"), _ => string.Empty, }; diff --git a/YLErpDAL/BLL/EodSettlement/ClientBalanceUtility.cs b/YLErpDAL/BLL/EodSettlement/ClientBalanceUtility.cs index a65ba73f..51dfe898 100644 --- a/YLErpDAL/BLL/EodSettlement/ClientBalanceUtility.cs +++ b/YLErpDAL/BLL/EodSettlement/ClientBalanceUtility.cs @@ -21,6 +21,8 @@ namespace YLErp.BLL.EodSettlement /// public class ClientBalanceUtility { + private static readonly IYcLogger logger = LogFactory.GetLogger("客户资金结算"); + /// /// 获取startDate至endDate 所有客户结算信息统计 /// @@ -303,6 +305,8 @@ namespace YLErp.BLL.EodSettlement var usedCreditDic = ClientCreditInoutService.GetUsedCreditByClients(clientIdS, db); var swapInitMarginDic = SwapSpanBalanceQueryService.GetSwapInitMarginByClients(clientIdS, lastDate, db); var swapAdditionalDic = SwapSpanBalanceQueryService.GetTradeAdditionalMarginByClients(clientIdS, lastDate, db); + //合约维度追保金额新口径(2026-08-28):当日追加保证金现金部分合计 + var todayCashAddDic = SwapSpanBalanceQueryService.GetTodayCashAdditionalMarginByClients(clientIdS, lastDate, db); //原始授信额度(展示用):与 EOD 写入 clientbalancedaily.Credit 同批过滤条件(EodClientBalanceCalc :68), //取 Σ(OriginalCredit ?? Credit) 不经比例折算;TotalCredit 仍为折算后值供公式使用 var originalCreditDic = db.credit.AsNoTracking() @@ -387,8 +391,9 @@ namespace YLErp.BLL.EodSettlement // 未配置(MarginWatchRule=NULL,存量客户)维持旧口径:期末结存-追保账户余额 if (client?.MarginWatchRule == 0) { + //定稿 2026-08-28:Max(现金结存 + 授信额度 − 已使用授信, 0),不再减交易维度追加保证金(避免双重扣减) balance.AvailableAmount = SwapSpanBalanceCalc.CalcContractDimensionAvailable( - balance.AmountFund, balance.TotalCredit, balance.UsedCredit, balance.SwapAdditionalMarginTotal); + balance.AmountFund, balance.TotalCredit, balance.UsedCredit); } else if (client?.MarginWatchRule == 1) { @@ -401,9 +406,9 @@ namespace YLErp.BLL.EodSettlement { balance.AvailableAmount = balance.MarginBalance - (balance.VmInFundSum - balance.VmOutFundSum); } - // 是否追保/追保金额(阶段四 §4.2 按维度分流,允许负值=双向,不以 0 截断): - // 客户维度(==1)= (维持−初始) − (现金+授信−已使用),负=可返还; - // 合约维度(==0)= Max(−(现金+授信−已使用), 0)(需求原文公式,盈余截断为 0); + // 是否追保/追保金额(按维度分流,定稿 2026-08-28 起均 Max(...,0) 截断、恒 ≥ 0,只追不退): + // 客户维度(==1)= Max((维持−初始) − (现金+授信−已使用), 0); + // 合约维度(==0)= 当日追加保证金现金部分合计(2026-08-28 新口径:不与现金结存轧差、闲置现金不冲抵、授信部分不追); // 未配置(NULL 存量)维持旧口径:盯市低于维持时 = 初始保证金金额−盯市金额,否则 0 if (client?.MarginWatchRule == 1) { @@ -414,7 +419,7 @@ namespace YLErp.BLL.EodSettlement else if (client?.MarginWatchRule == 0) { balance.MarginByPayableMarginTotal = SwapSpanBalanceCalc.CalcContractDimensionCallMargin( - balance.AmountFund, balance.TotalCredit, balance.UsedCredit); + todayCashAddDic.TryGetValue(data.ClientId, out var todayCashAdd) ? todayCashAdd : 0); balance.NeedAddMargin = balance.MarginByPayableMarginTotal > 0; } else @@ -422,8 +427,25 @@ namespace YLErp.BLL.EodSettlement balance.NeedAddMargin = balance.SwapMarketAmount < balance.MaintenanceMargin; balance.MarginByPayableMarginTotal = balance.NeedAddMargin ? (balance.MySideMargin - balance.SwapMarketAmount) : 0; } - // 可取资金=Math.Max(期末结存-min(持仓盈亏,0)-初始保证金,0) - balance.DesirableFund = Math.Max(balance.MarginBalance + Math.Min(balance.RoundedPositionPnl, 0), 0); + // 可取资金(定稿 2026-08-28 分维度口径,只算现金部分——授信不可取现;min(持仓盈亏,0) 只扣浮亏、浮盈不放行): + // 合约维度(==0)= Max(现金结存 + min(持仓盈亏,0), 0)(追保已落账进现金结存,无需再叠加保证金约束); + // 客户维度(==1)= Max(现金结存 + 初始保证金 − 维持保证金 + min(持仓盈亏,0), 0)(追保不产现金流,显式叠加保证金约束); + // 未配置(NULL 存量)维持旧口径 + if (client?.MarginWatchRule == 0) + { + balance.DesirableFund = SwapSpanBalanceCalc.CalcContractDimensionDesirableFund( + balance.AmountFund, balance.RoundedPositionPnl); + } + else if (client?.MarginWatchRule == 1) + { + balance.DesirableFund = SwapSpanBalanceCalc.CalcClientDimensionDesirableFund( + balance.AmountFund, balance.SwapInitMargin, -balance.MySideMargin, balance.RoundedPositionPnl); + } + else + { + balance.DesirableFund = Math.Max(balance.MarginBalance + Math.Min(balance.RoundedPositionPnl, 0), 0); + } + logger.Info($"客户资金结算:客户{client?.id}({client?.Name}){balance.ValueDate:yyyy-MM-dd} MarginWatchRule={client?.MarginWatchRule?.ToString() ?? "NULL"} 可用资金={balance.AvailableAmount:0.00} 追保金额={balance.MarginByPayableMarginTotal:0.00} 是否追保={balance.NeedAddMargin} 可取资金={balance.DesirableFund:0.00}(期末结存{balance.AmountFund:0.00} 授信{balance.TotalCredit:0.00} 已用授信{balance.UsedCredit:0.00} 互换初始{balance.SwapInitMargin:0.00} 维持{-balance.MySideMargin:0.00} 追加合计{balance.SwapAdditionalMarginTotal:0.00})"); } } diff --git a/YLErpDAL/BLL/EodSettlement/RealTimeClientBanlanceService.cs b/YLErpDAL/BLL/EodSettlement/RealTimeClientBanlanceService.cs index f1740039..1c392885 100644 --- a/YLErpDAL/BLL/EodSettlement/RealTimeClientBanlanceService.cs +++ b/YLErpDAL/BLL/EodSettlement/RealTimeClientBanlanceService.cs @@ -30,6 +30,8 @@ namespace YLErp.BLL.Eod /// public class RealTimeClientBanlanceService : YLBaseService { + private static readonly IYcLogger logger = LogFactory.GetLogger("实时客户资金"); + readonly valuedate _systemDate; //系统参数 readonly DateTime _valueDate; //系统交易日 readonly Dictionary _clientBalanceDic; @@ -241,6 +243,8 @@ namespace YLErp.BLL.Eod var usedCreditDic = Modules.SwapModule.ClientCreditInoutService.GetUsedCreditByClients(clientIdList, DbContext); var swapInitMarginDic = SwapSpanBalanceQueryService.GetSwapInitMarginByClients(clientIdList, calcDate.Value, DbContext); var swapAdditionalDic = SwapSpanBalanceQueryService.GetTradeAdditionalMarginByClients(clientIdList, calcDate.Value, DbContext); + //合约维度追保金额新口径(2026-08-28):当日追加保证金现金部分合计(日间 EOD 未跑时无当日记录 → 0,与通知书口径一致) + var todayCashAddDic = SwapSpanBalanceQueryService.GetTodayCashAdditionalMarginByClients(clientIdList, calcDate.Value, DbContext); foreach (var item in _clientBalanceDic.Values) { @@ -286,8 +290,9 @@ namespace YLErp.BLL.Eod var ruleClient = DataCacheProvider.GetClientDataSource().GetData(clientId); if (ruleClient?.MarginWatchRule == 0) { + //定稿 2026-08-28:Max(现金结存 + 授信额度 − 已使用授信, 0),不再减交易维度追加保证金(避免双重扣减) item.AvailableAmount = SwapSpanBalanceCalc.CalcContractDimensionAvailable( - item.AmountFund, item.TotalCredit, item.UsedCredit, item.SwapAdditionalMarginTotal); + item.AmountFund, item.TotalCredit, item.UsedCredit); } else if (ruleClient?.MarginWatchRule == 1) { @@ -300,9 +305,10 @@ namespace YLErp.BLL.Eod { item.AvailableAmount = item.MarginBalance - item.FrozenMarginMoney; } - // 是否追保/追保金额(按维度分流,与 ClientBalanceUtility 报告口径一致): - // 客户维度(==1)双向追保 = (维持−初始) − (现金+授信−已使用),允许负值(负=可返还); - // 合约维度(==0)= Max(−(现金+授信−已使用), 0),盈余截断为 0;NULL 存量维持旧口径 + // 是否追保/追保金额(按维度分流,与 ClientBalanceUtility 报告口径一致,定稿 2026-08-28 起均 Max(...,0) 截断): + // 客户维度(==1)= Max((维持−初始) − (现金+授信−已使用), 0); + // 合约维度(==0)= 当日追加保证金现金部分合计(2026-08-28 新口径:不与现金结存轧差、闲置现金不冲抵、授信部分不追); + // NULL 存量维持旧口径 if (ruleClient?.MarginWatchRule == 1) { item.MarginByPayableMarginTotal = SwapSpanBalanceCalc.CalcClientDimensionCallMargin( @@ -312,7 +318,7 @@ namespace YLErp.BLL.Eod else if (ruleClient?.MarginWatchRule == 0) { item.MarginByPayableMarginTotal = SwapSpanBalanceCalc.CalcContractDimensionCallMargin( - item.AmountFund, item.TotalCredit, item.UsedCredit); + todayCashAddDic.TryGetValue(clientId, out var todayCashAdd) ? todayCashAdd : 0); item.NeedAddMargin = item.MarginByPayableMarginTotal > 0; } else @@ -322,8 +328,25 @@ namespace YLErp.BLL.Eod // 追保金额=初始保证金金额-盯市金额 item.MarginByPayableMarginTotal = item.NeedAddMargin ? (item.MySideMargin - item.SwapMarketAmount) : 0; } - // 可取资金=max(期末结存+min(持仓盈亏,0)-初始保证金,0) - item.DesirableFund =Math.Max( item.MarginBalance - item.FrozenMarginMoney + Math.Min(item.RoundedPositionPnl, 0),0); + // 可取资金(定稿 2026-08-28 分维度口径,只算现金部分;min(持仓盈亏,0) 只扣浮亏、浮盈不放行): + // 合约维度(==0)= Max(现金结存 + min(持仓盈亏,0), 0); + // 客户维度(==1)= Max(现金结存 + 初始保证金 − 维持保证金 + min(持仓盈亏,0), 0); + // NULL 存量维持旧口径(含冻结保证金扣减) + if (ruleClient?.MarginWatchRule == 0) + { + item.DesirableFund = SwapSpanBalanceCalc.CalcContractDimensionDesirableFund( + item.AmountFund, item.RoundedPositionPnl); + } + else if (ruleClient?.MarginWatchRule == 1) + { + item.DesirableFund = SwapSpanBalanceCalc.CalcClientDimensionDesirableFund( + item.AmountFund, item.SwapInitMargin, -item.MySideMargin, item.RoundedPositionPnl); + } + else + { + item.DesirableFund = Math.Max(item.MarginBalance - item.FrozenMarginMoney + Math.Min(item.RoundedPositionPnl, 0), 0); + } + logger.Info($"实时客户资金:客户{clientId} MarginWatchRule={ruleClient?.MarginWatchRule?.ToString() ?? "NULL"} 可用资金={item.AvailableAmount:0.00} 追保金额={item.MarginByPayableMarginTotal:0.00} 是否追保={item.NeedAddMargin} 可取资金={item.DesirableFund:0.00}(期末结存{item.AmountFund:0.00} 授信{item.TotalCredit:0.00} 已用授信{item.UsedCredit:0.00} 互换初始{item.SwapInitMargin:0.00} 维持{-item.MySideMargin:0.00} 追加合计{item.SwapAdditionalMarginTotal:0.00})"); } return _clientBalanceDic.Values; diff --git a/YLErpDAL/BLL/EodSettlement/RealtimePnlCalc.cs b/YLErpDAL/BLL/EodSettlement/RealtimePnlCalc.cs index d3a5b788..1a2e485c 100644 --- a/YLErpDAL/BLL/EodSettlement/RealtimePnlCalc.cs +++ b/YLErpDAL/BLL/EodSettlement/RealtimePnlCalc.cs @@ -2328,6 +2328,31 @@ namespace YLErp.BLL.Eod var clientBalance = clientBalances[0]; using (var db = new YLContext()) { + //修改/复核已簿记交易(trade.id!=0)按增量口径校验:本笔自身已簿记的成交金额(期权费记录) + //与初始预付金流水已计入现金结存、初始授信占用已计入已使用授信,比较前先剔除—— + //否则改备注等无关字段保存会被存量预付金二次拦截(例:入金60万、初始预付金已付50万、余额10万)。 + //只剔除确认时会重写的记录;追保/票息/平仓等生命周期记录确认后仍保留,不剔除。 + var ownCashBooked = 0d; + var ownCreditBooked = 0d; + if (trade.id != 0) + { + ownCashBooked = db.ClientCashInCashOut + .Where(x => x.TradeId == trade.id && x.ValidState != "InValid") + .ToList() + .Where(x => (x.State == ClientCashInCashOut.已结算 || x.State == ClientCashInCashOut.已确认) + && (x.Action == ClientCashInCashOut.系统操作_期权费 || x.Action == ClientCashInCashOut.系统操作_应付预付金)) + .Sum(x => x.Money ?? 0); + ownCreditBooked = db.client_credit_inout + .Where(x => x.trade_id == trade.id) + .ToList() + .Where(x => !Modules.SwapModule.ClientCreditInoutService.IsAdditionalMarginRecord(x)) + .Sum(x => x.amount); + clientBalance.AmountFund -= ownCashBooked; + clientBalance.AvailableAmount -= ownCashBooked; + } + var ownBookedNote = trade.id != 0 + ? $"(已剔除本笔已簿记资金{ownCashBooked:#,##0.000}与初始授信占用{ownCreditBooked:#,##0.000})" + : ""; var tradePrice = trade.TradePrice * (-TradeCalcHelper.GetSign(trade.BuySell)); var AvailableAmount = clientBalance.AvailablePremium(); @@ -2381,14 +2406,15 @@ namespace YLErp.BLL.Eod } } var usedCredit = Modules.SwapModule.ClientCreditInoutService.GetUsedCredit(clientId, db); - var creditCap = Math.Max(clientBalance.TotalCredit - usedCredit, 0); + //授信上限剔除本笔自身初始占用(ownCreditBooked,增量口径,见上方净扣说明) + var creditCap = Math.Max(clientBalance.TotalCredit - (usedCredit - ownCreditBooked), 0); var creditCovered = Math.Min(creditPayable, creditCap); //授信覆盖不足的回落现金部分 + 走现金部分 + 成交金额,合计必须 ≤ 现金结存 var cashNeed = tradePrice + cashPayable + (creditPayable - creditCovered); if (cashNeed > clientBalance.AmountFund) { var totalPayable = tradePrice + cashPayable + creditPayable; - errorMsg = $"当前交易应付总额:{totalPayable:#,##0.000}(走现金:{cashPayable + tradePrice:#,##0.000},选授信:{creditPayable:#,##0.000})。当前现金结存:{clientBalance.AmountFund:F3}(走现金部分只认现金结存),授信额度:{clientBalance.TotalCredit:F3},已使用授信:{usedCredit:F3},剩余授信:{creditCap:F3}(授信仅覆盖选授信部分,不足回落现金)。现金不足以覆盖应付的现金部分。"; + errorMsg = $"当前交易应付总额:{totalPayable:#,##0.000}(走现金:{cashPayable + tradePrice:#,##0.000},选授信:{creditPayable:#,##0.000})。当前现金结存:{clientBalance.AmountFund:F3}(走现金部分只认现金结存),授信额度:{clientBalance.TotalCredit:F3},已使用授信:{usedCredit - ownCreditBooked:F3},剩余授信:{creditCap:F3}(授信仅覆盖选授信部分,不足回落现金)。现金不足以覆盖应付的现金部分。{ownBookedNote}"; return false; } } diff --git a/YLErpDAL/BLL/MarginCalculation/MarginCalculationBase.cs b/YLErpDAL/BLL/MarginCalculation/MarginCalculationBase.cs index 4c84413d..dacdb6f6 100644 --- a/YLErpDAL/BLL/MarginCalculation/MarginCalculationBase.cs +++ b/YLErpDAL/BLL/MarginCalculation/MarginCalculationBase.cs @@ -531,7 +531,12 @@ namespace YLErp.BLL.MarginCalculation } } - return SwapSpanMarginCalc.CalcTradeMaintenanceMargin(trade.InitialMargin, spanCfg, legs, isInitialCalc, closePrice); + var margin = SwapSpanMarginCalc.CalcTradeMaintenanceMargin(trade.InitialMargin, spanCfg, legs, isInitialCalc, closePrice); + if (margin.HasValue) + { + logger.Info($"规则15新引擎:交易{trade.id} 标的{trade.UnderlyingCode} 收盘价={closePrice:0.####}(试算初始={isInitialCalc})→ 维持保证金={margin.Value:0.00}"); + } + return margin; } } } diff --git a/YLErpDAL/DataBase/DbSchema.cs b/YLErpDAL/DataBase/DbSchema.cs new file mode 100644 index 00000000..f48048f5 --- /dev/null +++ b/YLErpDAL/DataBase/DbSchema.cs @@ -0,0 +1,41 @@ +using MySqlConnector; +using System.Collections.Concurrent; +using System.Text.RegularExpressions; + +namespace YLErp.BLL +{ + /// + /// 跨库 SQL 的物理库名解析器:代码里只写逻辑连接名(appsettings ConnectionStrings 的 key, + /// 与 Java @Mapper 指定数据源名同语义),物理库名由各环境连接串的 database= 决定—— + /// 同一逻辑库在不同环境库名不同(如 bigdata 在测试环境为 glms_bigdata),SQL 中硬编码库名会跨环境失败。 + /// 用法:{@DbSchema.Of("bigdata")}.mf_fundarchives(返回带反引号的库名,可直接内插)。 + /// 仅标识自家的 appsettings 连接串,值不来自用户输入;仍做 \w+ 白名单校验防御配置笔误。 + /// + public static class DbSchema + { + private static readonly ConcurrentDictionary Cache = new(); + private static readonly Regex SafeIdentifier = new(@"^\w+$", RegexOptions.Compiled); + + /// + /// 取逻辑连接名对应的物理库名(形如 `glms_bigdata`,含反引号)。配置缺失或库名非法立即抛错—— + /// 跨库 SQL 拼错库名在运行期才暴露更难排查,配置错误应尽早失败。 + /// + public static string Of(string connectionKey) + { + return Cache.GetOrAdd(connectionKey, key => + { + var connectionString = AppManager.GetConnectionString(key); + if (string.IsNullOrWhiteSpace(connectionString)) + { + throw new InvalidOperationException($"跨库SQL依赖的连接串未配置:{key}"); + } + var database = new MySqlConnectionStringBuilder(connectionString).Database; + if (string.IsNullOrWhiteSpace(database) || !SafeIdentifier.IsMatch(database)) + { + throw new InvalidOperationException($"连接串 {key} 缺少 database 或库名非法:{database}"); + } + return "`" + database + "`"; + }); + } + } +} diff --git a/YLErpDAL/Modules/MarginModule/MarginTemplateV2RateHelper.cs b/YLErpDAL/Modules/MarginModule/MarginTemplateV2RateHelper.cs index 25999088..b6c4d466 100644 --- a/YLErpDAL/Modules/MarginModule/MarginTemplateV2RateHelper.cs +++ b/YLErpDAL/Modules/MarginModule/MarginTemplateV2RateHelper.cs @@ -110,12 +110,14 @@ namespace YLErp.Modules.MarginModule //2.无预付金规则:率直接为 0 if (template.RuleType == (int)MarginRuleTypeEnum.无预付金) { + logger.Info($"预付金模板取数:模板{template.id}(规则=无预付金)标的{underlyingCode},x=y=0"); return new MarginRateResult { Template = template, InitRate = 0m, MaintainRate = 0m }; } if (template.RuleType != (int)MarginRuleTypeEnum.区间追保结构) { //其他规则不在本帮助类支持范围,显式返回 null + logger.Info($"【警告】预付金模板取数:模板{template.id} 规则{template.RuleType}不在取数支持范围(仅 无预付金/区间追保结构),返回null由调用方兜底"); return null; } @@ -124,6 +126,7 @@ namespace YLErp.Modules.MarginModule .Where(x => x.MarginTemplateId == template.id && x.ValueDate <= valueDate); if (!detailQuery.Any()) { + logger.Info($"【警告】预付金模板取数:模板{template.id} 无生效明细行(ValueDate≤{valueDate:yyyy-MM-dd}),返回null由调用方兜底"); return null; } var latestValueDate = detailQuery.Max(x => x.ValueDate); @@ -158,6 +161,7 @@ namespace YLErp.Modules.MarginModule } if (!matched.Any()) { + logger.Info($"【警告】预付金模板取数:模板{template.id}(生效日{latestValueDate:yyyy-MM-dd})标的{underlyingCode}(品种{underlyingInstrumentType},期限档{term})无匹配明细行,返回null由调用方兜底"); return null; } @@ -170,12 +174,15 @@ namespace YLErp.Modules.MarginModule } var detail = matched.First(); + var initRate = ToDecimalRate(detail.MarginRatio1); + var maintainRate = ToDecimalRate(detail.MarginRatio2); + logger.Info($"预付金模板取数:模板{template.id}(生效日{latestValueDate:yyyy-MM-dd})标的{underlyingCode}(品种{underlyingInstrumentType},期限档{term})命中明细行{detail.id}:初始x={(initRate?.ToString("0.####") ?? "未配")},维持y={(maintainRate?.ToString("0.####") ?? "未配")}"); return new MarginRateResult { Template = template, Detail = detail, - InitRate = ToDecimalRate(detail.MarginRatio1), - MaintainRate = ToDecimalRate(detail.MarginRatio2) + InitRate = initRate, + MaintainRate = maintainRate }; } @@ -226,6 +233,7 @@ namespace YLErp.Modules.MarginModule var bound = db.margin_template_v2.AsNoTracking().FirstOrDefault(x => x.id == bindingTemplateId.Value && x.IsValid); if (bound != null && bound.IsApplicableToBook(tradeAssetId)) { + logger.Info($"预付金模板取数:交易{tradeId}(客户{clientId})命中层级=交易绑定 → 模板{bound.id}"); return bound; } if (bound != null) @@ -264,17 +272,23 @@ namespace YLErp.Modules.MarginModule var ret = clientTemplate.FirstOrDefault(x => x.IsApplicableToBook(tradeAssetId)); if (ret != null) { + logger.Info($"预付金模板取数:交易{tradeId}(客户{clientId},等级{levelName ?? "无"})命中层级=客户默认 → 模板{ret.id}"); return ret; } } //3.全局默认 - return db.margin_template_v2.AsNoTracking() + var globalDefault = db.margin_template_v2.AsNoTracking() .Where(x => x.IsDefault && !x.IsForClient && x.IsValid && x.TradeTypes.Contains("收益互换") && x.ValueDate <= valueDate) .OrderByDescending(x => x.ValueDate) .ThenByDescending(x => x.id) .ToList() .FirstOrDefault(x => x.IsApplicableToBook(tradeAssetId)); + if (globalDefault != null) + { + logger.Info($"预付金模板取数:交易{tradeId}(客户{clientId})命中层级=全局默认 → 模板{globalDefault.id}"); + } + return globalDefault; } /// @@ -373,6 +387,7 @@ namespace YLErp.Modules.MarginModule if (boundTemplates.TryGetValue(templateId, out var bound) && bound.IsApplicableToBook(trade.AssetId)) { result[trade.id] = bound; + logger.Info($"预付金模板取数:交易{trade.id}(客户{trade.ClientId})命中层级=交易绑定 → 模板{bound.id}"); } else if (boundTemplates.ContainsKey(templateId)) { @@ -391,12 +406,18 @@ namespace YLErp.Modules.MarginModule if (clientTemplate != null) { result[trade.id] = clientTemplate.Template; + logger.Info($"预付金模板取数:交易{trade.id}(客户{trade.ClientId},等级{levelName ?? "无"})命中层级=客户默认 → 模板{clientTemplate.Template.id}"); continue; } var globalDefault = globalDefaults.FirstOrDefault(x => x.IsApplicableToBook(trade.AssetId)); if (globalDefault != null) { result[trade.id] = globalDefault; + logger.Info($"预付金模板取数:交易{trade.id}(客户{trade.ClientId})命中层级=全局默认 → 模板{globalDefault.id}"); + } + else + { + logger.Info($"【警告】预付金模板取数:交易{trade.id}(客户{trade.ClientId})三层级(交易绑定/客户默认/全局默认)均未命中有效模板"); } } diff --git a/YLErpDAL/Modules/SwapModule/Margin/SwapAdditionalMarginService.cs b/YLErpDAL/Modules/SwapModule/Margin/SwapAdditionalMarginService.cs index 02b31082..94e1effc 100644 --- a/YLErpDAL/Modules/SwapModule/Margin/SwapAdditionalMarginService.cs +++ b/YLErpDAL/Modules/SwapModule/Margin/SwapAdditionalMarginService.cs @@ -3,6 +3,7 @@ using YLErp.DBModels; using YLErp.Enums; using YLErp.Modules.MarginModule; using YLErp.Modules.TradeModule; +using YLErp.Helpers; namespace YLErp.Modules.SwapModule.Margin { @@ -24,6 +25,8 @@ namespace YLErp.Modules.SwapModule.Margin /// public class SwapAdditionalMarginService : YLBaseService { + private static readonly IYcLogger logger = LogFactory.GetLogger("EOD追保"); + /// /// EOD 追保腿打标(OptName):与手工追加预付金腿(OptName=操作员实名)区分, /// 幂等清理、RemoveByTrade 保护与时间轴回退清理均以此识别。拆单现金腿落库时被打服务身份,簿记后回打本标识。 @@ -79,8 +82,23 @@ namespace YLErp.Modules.SwapModule.Margin /// /// 结算日逐客户逐交易产生追加保证金(clientFilter 为部分结算的客户过滤,与 EOD 请求一致)。 + /// 顶层兜异常日志(Error 级)后原样抛出,避免异常栈被 EOD 框架层吞掉无从定位。 /// public void SettleAdditionalMargin(DateTime settleDate, List clientFilter = null) + { + try + { + SettleAdditionalMarginCore(settleDate, clientFilter); + } + catch (Exception ex) + { + var filterDesc = clientFilter != null && clientFilter.Any() ? string.Join(",", clientFilter) : "全部"; + logger.Error($"EOD追保:结算日{settleDate:yyyy-MM-dd}(客户范围:{filterDesc})追保腿生成异常", ex); + throw; + } + } + + private void SettleAdditionalMarginCore(DateTime settleDate, List clientFilter) { //合约维度盯市客户 var watchClientIds = DbContextFactory.GetClientDbContext(OptUser).client.AsNoTracking() @@ -93,8 +111,10 @@ namespace YLErp.Modules.SwapModule.Margin } if (watchClientIds.Count == 0) { + logger.Info($"EOD追保:结算日{settleDate:yyyy-MM-dd} 无合约维度盯市(MarginWatchRule=0)客户,结束"); return; } + logger.Info($"EOD追保:结算日{settleDate:yyyy-MM-dd} 合约维度盯市客户{watchClientIds.Count}个({string.Join(",", watchClientIds)})"); //存续中的互换交易(状态口径与 eodSwapQuery 一致,含当日已了结) var tradeStatuses = ConsTrade.TradeStatusAfterConfirmed; @@ -107,17 +127,39 @@ namespace YLErp.Modules.SwapModule.Margin .ToList(); if (trades.Count == 0) { + logger.Info($"EOD追保:结算日{settleDate:yyyy-MM-dd} 盯市客户名下无存续收益互换交易,结束"); return; } + //开始日门槛:未到交易开始日(未起息)的不参与追保——维持保证金引擎从成交日就产出 trade_span, + //初始预付金流水却到开始日才入账,不过滤会在 成交日~开始日 之间把未到期的初始预付金 + //误判成缺口、按维持全额追加(2026-08-28 交易2571实证:07-31成交/08-03起息,重刷07-31误追500000) + var notStartedTrades = trades.Where(t => (t.StartDate ?? t.TradeDate) > settleDate).ToList(); + if (notStartedTrades.Count > 0) + { + logger.Info($"EOD追保:{notStartedTrades.Count}笔交易未到开始日(id=[{string.Join(",", notStartedTrades.Select(t => t.id))}]),不参与追保结算"); + trades = trades.Except(notStartedTrades).ToList(); + if (trades.Count == 0) + { + logger.Info($"EOD追保:结算日{settleDate:yyyy-MM-dd} 交易均未到开始日,结束"); + return; + } + } + //规则15(区间追保结构)交易:R1 三层级解析(BUG-02 修正,与引擎/确认书同口径)—— //交易绑定→客户默认→全局默认 找到即停,只配客户/全局默认模板的交易同样纳入追保结算 + var candidateCount = trades.Count; var templatesByTrade = MarginTemplateV2RateHelper.ResolveTieredTemplates(trades, settleDate, DbContext); trades = trades.Where(t => templatesByTrade.TryGetValue(t.id, out var tpl) && tpl.RuleType == (int)MarginRuleTypeEnum.区间追保结构).ToList(); + if (candidateCount > trades.Count) + { + logger.Info($"EOD追保:{candidateCount - trades.Count}笔交易模板非区间追保结构(或三级未命中),不参与追保结算"); + } var tradeIds = trades.Select(t => t.id).ToList(); if (tradeIds.Count == 0) { + logger.Info($"EOD追保:结算日{settleDate:yyyy-MM-dd} 规则15交易为0,结束"); return; } @@ -175,21 +217,28 @@ namespace YLErp.Modules.SwapModule.Margin var eodPositionService = new SwapEodPositionService(this); var flowEventService = new SwapFlowEventService(this); + var settledTrades = 0; + var settledLegs = 0; + var totalIncrement = 0d; + foreach (var clientGroup in trades.GroupBy(t => t.ClientId).OrderBy(g => g.Key)) { foreach (var td in clientGroup.OrderBy(t => t.id)) { if (!maintenanceByTrade.TryGetValue(td.id, out var maintenance) || maintenance <= 0) { + logger.Info($"EOD追保:交易{td.id}(客户{td.ClientId})跳过——当日无trade_span维持保证金或维持≤0(maintenance={maintenance:0.00})"); continue; } //目标追加 = 维持保证金 − 已缴初始保证金净额(现金应付预付金净收额 + 授信初始占用净额, //授信垫付与现金垫付同等对待,杜绝授信初始预付金被重复追加) - var payableNet = (payableNetByTrade.TryGetValue(td.id, out var payable) ? payable : 0) - + (initCreditByTrade.TryGetValue(td.id, out var initCredit) ? initCredit : 0); + var payable = payableNetByTrade.TryGetValue(td.id, out var p) ? p : 0; + var initCredit = initCreditByTrade.TryGetValue(td.id, out var ic) ? ic : 0; + var payableNet = payable + initCredit; var target = SwapAdditionalMarginCalc.CalcTarget(maintenance, payableNet); if (target <= 0) { + logger.Info($"EOD追保:交易{td.id}(客户{td.ClientId})跳过——目标追加≤0:维持={maintenance:0.00},已缴={payableNet:0.00}(应付净额{payable:0.00}+初始授信占用{initCredit:0.00})已覆盖"); continue; } var fundedCash = addRecordByTrade.TryGetValue(td.id, out var cash) ? cash : 0; @@ -198,8 +247,10 @@ namespace YLErp.Modules.SwapModule.Margin if (increment <= 0) { //已补足;追保回落(目标下降)不返还——负缺口在可用资金公式(Σ维持−累计)体现 + logger.Info($"EOD追保:交易{td.id}(客户{td.ClientId})跳过——增量≤0:目标={target:0.00},已补足(现金{fundedCash:0.00}+授信{fundedCredit:0.00}),追保回落不返还"); continue; } + logger.Info($"EOD追保:交易{td.id}(客户{td.ClientId})需追加:维持={maintenance:0.00},已缴={payableNet:0.00}(应付净额{payable:0.00}+初始授信占用{initCredit:0.00}),目标={target:0.00},已补足(现金{fundedCash:0.00}+授信{fundedCredit:0.00}),本次增量={increment:0.00}"); //幂等清理:先删本结算日起 EOD 旧追保腿及其簿记(腿/流水/占用/快照同生共死),再按最新增量重建; //手工追加预付金腿(OptName≠EOD追保)不受影响 @@ -228,6 +279,10 @@ namespace YLErp.Modules.SwapModule.Margin .ToList(); newLegs.ForEach(x => x.OptName = EodOptName); DbContext.SaveChanges(); + logger.Info($"EOD追保:交易{td.id} 追保腿落库完成——新腿{newLegs.Count}条(id=[{string.Join(",", newLegs.Select(x => x.id))}],含拆单现金腿)"); + settledTrades++; + settledLegs += newLegs.Count; + totalIncrement += increment; //实时持仓克隆 + 开仓事件(参照 TradeConfirmService 簿记后动作,但只针对本次新腿—— //整交易 InitialPosition 会把浮动腿实时持仓重置回开仓态、AddPositionEvent 会为全部腿重复建开仓事件,EOD 场景不可用); @@ -252,6 +307,8 @@ namespace YLErp.Modules.SwapModule.Margin } } } + + logger.Info($"EOD追保:结算日{settleDate:yyyy-MM-dd} 完成——规则15交易{tradeIds.Count}笔,{settledTrades}笔产生追保(腿{settledLegs}条,合计增量{totalIncrement:0.00})"); } /// @@ -289,6 +346,7 @@ namespace YLErp.Modules.SwapModule.Margin var oldEodRows = DbContext.eod_swap_position.Where(x => x.SwapTradeId == td.id && x.ValueDate >= settleDate && oldInitialIds.Contains(x.PositionId)) .ToList(); + logger.Info($"EOD追保:交易{td.id} 幂等清理(重跑)——删EOD追保腿{oldLegs.Count}条(id=[{string.Join(",", oldLegs.Select(x => x.id))}])及其簿记:现金流水{oldCashRecords.Count}条、授信占用{oldCreditRecords.Count}条、开仓事件{oldEvents.Count}条、eod快照{oldEodRows.Count}行"); DbContext.eod_swap_position.RemoveRange(oldEodRows); DbContext.swap_position.RemoveRange(oldLegs); DbContext.SaveChanges(); diff --git a/YLErpDAL/Modules/SwapModule/Margin/SwapSpanBalanceCalc.cs b/YLErpDAL/Modules/SwapModule/Margin/SwapSpanBalanceCalc.cs index 3450b796..c8b6246f 100644 --- a/YLErpDAL/Modules/SwapModule/Margin/SwapSpanBalanceCalc.cs +++ b/YLErpDAL/Modules/SwapModule/Margin/SwapSpanBalanceCalc.cs @@ -5,7 +5,7 @@ namespace YLErp.Modules.SwapModule.Margin /// 授信额度取 credit.Credit 合计(阶段一 §1.1 已在保存时折算 原始授信值×最大授信可用比例,消费方不再乘比例); /// 现金结存 = 期末结存 AmountFund(阶段二起授信不进资金,流水天然不含授信部分,无需排除); /// 已使用授信 = 授信出入表 Σ(amount); - /// 初始保证金(净收取为正)= 应付预付金流水收付净额(平仓返还自动冲减); + /// 初始保证金(净收取为正)= 初始预付金的和 = 应付预付金流水收付净额 + 初始预付金授信占用净额(2026-08-28 业务裁定:与页面"初始保证金金额"字段同口径); /// 维持保证金(净收取为正)= −MySideMargin(client_span 维持保证金写入 trade_span 后经 CalcClientMargin 反号聚合)。 /// 公式整体待业务校验(EQD-6948),参数化集中在此便于校验后调整。 /// @@ -24,34 +24,61 @@ namespace YLErp.Modules.SwapModule.Margin /// /// 可用资金(合约维度,MarginWatchRule=0): - /// Max(现金结存 + 授信额度 − 已使用授信 − 交易维度追加保证金合计, 0); - /// 交易维度追加保证金合计 = Σ(维持保证金 − 累计保证金)(阶段三:累计=应付预付金净额;阶段四含追加保证金流水)。 + /// Max(现金结存 + 授信额度 − 已使用授信, 0)(定稿 2026-08-28:删除"−交易维度追加保证金"—— + /// 每日追保落账后现金部分已进现金结存、授信部分已进已使用授信,再减属双重扣减)。 /// - public static double CalcContractDimensionAvailable(double cashBalance, double totalCredit, double usedCredit, - double tradeAdditionalMarginSum) + public static double CalcContractDimensionAvailable(double cashBalance, double totalCredit, double usedCredit) { - return Math.Max(cashBalance + totalCredit - usedCredit - tradeAdditionalMarginSum, 0); + return Math.Max(cashBalance + totalCredit - usedCredit, 0); } /// - /// 追保金额(客户维度,MarginWatchRule=1,阶段四 §4.2 双向——不以 0 截断): - /// (维持保证金 − 初始保证金) − (现金结存 + 授信额度 − 已使用授信),正=需追保、负=可返还。 - /// 即客户维度可用资金公式的反向值(去 Max 截断)——与需求原文"现金结存+授信额度−(维持−初始)"数值互为相反数, + /// 追保金额(客户维度,MarginWatchRule=1,定稿 2026-08-28:Max(...,0) 截断,恒 ≥ 0,只追不退): + /// Max((维持保证金 − 初始保证金) − (现金结存 + 授信额度 − 已使用授信), 0),正=需追保; + /// 资金富余时为 0(富余状态由可用资金/可取资金为正表达)。 + /// 即客户维度可用资金公式反向值的 Max 截断——与需求原文"现金结存+授信额度−(维持−初始)"数值互为相反数, /// 此处按 MarginByPayableMarginTotal 字段既有口径(正数=应追加,估值报告"应追加预付金X元")定向。 /// public static double CalcClientDimensionCallMargin(double cashBalance, double totalCredit, double usedCredit, double initialMargin, double maintenanceMargin) { - return Math.Round(maintenanceMargin - initialMargin - (cashBalance + totalCredit - usedCredit), 2, MidpointRounding.AwayFromZero); + return Math.Max(Math.Round(maintenanceMargin - initialMargin - (cashBalance + totalCredit - usedCredit), 2, MidpointRounding.AwayFromZero), 0); } /// - /// 追保金额(合约维度,MarginWatchRule=0):Max(−(现金结存 + 授信额度 − 已使用授信), 0)。 - /// 即需求原文公式 Max((现金结存+授信额度−已使用授信)×−1, 0):账户透支(现金+授信不足)为正=应补足,盈余截断为 0(不展示负数)。 + /// 追保金额(合约维度,MarginWatchRule=0,2026-08-28 新口径): + /// = 当日产生的追加保证金资金记录中"资金来源=现金"的金额合计(入参即该值,见 + /// SwapSpanBalanceQueryService.GetTodayCashAdditionalMarginByClients),Max(...,0) 兜底截断。 + /// 语义:当日该笔合约产生的现金追保额(不与现金结存轧差、闲置现金不冲抵;授信部分不追); + /// 恒 ≥ 0(只追不退,退还走平仓/出金)。通知书按当日现金追保全额展示, + /// 累计未补足欠款体现在账户余额(现金结存)中,不并入本字段。 /// - public static double CalcContractDimensionCallMargin(double cashBalance, double totalCredit, double usedCredit) + public static double CalcContractDimensionCallMargin(double todayCashAdditionalMargin) { - return Math.Max(Math.Round(-(cashBalance + totalCredit - usedCredit), 2, MidpointRounding.AwayFromZero), 0); + return Math.Max(Math.Round(todayCashAdditionalMargin, 2, MidpointRounding.AwayFromZero), 0); + } + + /// + /// 可取资金(客户维度,MarginWatchRule=1,定稿 2026-08-28 新增): + /// Max(现金结存 + 初始保证金 − 维持保证金 + min(持仓盈亏, 0), 0)。 + /// 可用资金含授信但授信不可取现,可取资金只算现金部分;min(持仓盈亏,0) 只扣未实现亏损、浮盈不放行; + /// 客户维度追保不产生现金流,需显式叠加保证金约束(+初始−维持)。 + /// initialMargin/maintenanceMargin 均为净收取为正。 + /// + public static double CalcClientDimensionDesirableFund(double cashBalance, double initialMargin, + double maintenanceMargin, double positionPnl) + { + return Math.Max(cashBalance + initialMargin - maintenanceMargin + Math.Min(positionPnl, 0), 0); + } + + /// + /// 可取资金(合约维度,MarginWatchRule=0,定稿 2026-08-28 新增): + /// Max(现金结存 + min(持仓盈亏, 0), 0)。 + /// 合约维度追保已通过资金记录反映在现金结存中,无需再叠加保证金约束。 + /// + public static double CalcContractDimensionDesirableFund(double cashBalance, double positionPnl) + { + return Math.Max(cashBalance + Math.Min(positionPnl, 0), 0); } } } diff --git a/YLErpDAL/Modules/SwapModule/Margin/SwapSpanBalanceQueryService.cs b/YLErpDAL/Modules/SwapModule/Margin/SwapSpanBalanceQueryService.cs index ec26f6cf..091c6d94 100644 --- a/YLErpDAL/Modules/SwapModule/Margin/SwapSpanBalanceQueryService.cs +++ b/YLErpDAL/Modules/SwapModule/Margin/SwapSpanBalanceQueryService.cs @@ -1,6 +1,7 @@ using YLErp.BLL; using YLErp.DBModels; using YLErp.Enums; +using YLErp.Helpers; using YLErp.Modules.MarginModule; namespace YLErp.Modules.SwapModule.Margin @@ -18,6 +19,8 @@ namespace YLErp.Modules.SwapModule.Margin /// public static class SwapSpanBalanceQueryService { + private static readonly IYcLogger logger = LogFactory.GetLogger("预付金缺口查询"); + /// /// 客户维度输入:互换初始保证金(净收取为正,按客户汇总)。 /// 初始保证金 = 应付预付金流水收付净额 + 初始预付金授信占用净额(非"追加保证金"前缀、关联交易)—— @@ -95,6 +98,11 @@ namespace YLErp.Modules.SwapModule.Margin .Select(t => t.id) .ToHashSet(); maintenance = maintenance.Where(x => rule15TradeIds.Contains(x.TradeId)).ToList(); + var droppedCount = spanTradeIds.Count - rule15TradeIds.Count; + if (droppedCount > 0) + { + logger.Info($"预付金缺口查询:{valueDate:yyyy-MM-dd} {droppedCount}笔有span交易模板非规则15(或三级未命中),不计入交易维度追加合计"); + } if (maintenance.Count == 0) { return result; @@ -142,7 +150,38 @@ namespace YLErp.Modules.SwapModule.Margin result[group.Key] = total; } + logger.Info($"预付金缺口查询:{valueDate:yyyy-MM-dd} 规则15交易{rule15TradeIds.Count}笔,客户{result.Count}个,交易维度追加合计:{string.Join(";", result.Select(kv => $"客户{kv.Key}={kv.Value:0.00}"))}"); + return result; } + + /// + /// 合约维度追保金额输入(2026-08-28 新口径):当日产生的追加保证金资金记录中"资金来源=现金"的金额合计(按客户)。 + /// 仅统计 Action=系统操作_追加保证金 且 HappenDate=当日 的现金流水(−Σ(Money),收取为正)—— + /// 授信部分只写 client_credit_inout 不产生资金记录(授信不追、现金才追); + /// 腿化改造后现金流水逐日增量(每日新腿一条、HappenDate=结算日,同日重跑先清后建,幂等); + /// 该 Action 仅 EOD 追保会写(手工链路写应付预付金,语义天然隔离);追保回落不返还(无负记录),结果天然 ≥ 0。 + /// 注意:EOD 未跑(日间实时)时当日无记录,结果为 0——与通知书口径一致。 + /// + public static Dictionary GetTodayCashAdditionalMarginByClients(List clientIds, DateTime valueDate, YLContext db) + { + if (clientIds == null || clientIds.Count == 0) + { + return new Dictionary(); + } + + var dayStart = valueDate.Date; + var dayEnd = dayStart.AddDays(1); + return db.ClientCashInCashOut.AsNoTracking() + .Where(x => clientIds.Contains(x.ClientId ?? 0) + && x.Action == ClientCashInCashOut.系统操作_追加保证金 + && x.HappenDate >= dayStart && x.HappenDate < dayEnd + && x.ValidState != ConsGlobal.InValid + && (x.State == ClientCashInCashOut.已确认 || x.State == ClientCashInCashOut.已结算) + && x.Money != null) + .GroupBy(x => x.ClientId ?? 0) + .Select(g => new { ClientId = g.Key, Sum = -g.Sum(x => x.Money ?? 0d) }) + .ToDictionary(x => x.ClientId, x => x.Sum); + } } } diff --git a/YLErpDAL/Modules/SwapModule/SwapFundTagService.cs b/YLErpDAL/Modules/SwapModule/SwapFundTagService.cs index 68570f69..8ec004f9 100644 --- a/YLErpDAL/Modules/SwapModule/SwapFundTagService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapFundTagService.cs @@ -2,6 +2,7 @@ using YLErp.BLL; using YLErp.DBModels; using YLErp.Modules.SwapModule.Margin; using YLErp.Modules.TradeModule; +using YLErp.Helpers; namespace YLErp.Modules.SwapModule { @@ -15,6 +16,8 @@ namespace YLErp.Modules.SwapModule /// public class SwapFundTagService : YLBaseService { + private static readonly IYcLogger logger = LogFactory.GetLogger("预付金簿记"); + public SwapFundTagService(OptUserInfo userInfo) : base(userInfo) { } @@ -167,6 +170,7 @@ namespace YLErp.Modules.SwapModule .ThenBy(x => x.Leg.id) .ToList(); var plans = FundTagCalc.AllocateByLegPreference(allocateLegs, (decimal)creditAvailable, ignoreMoneyCheck); + logger.Info($"预付金簿记:交易{td.id}(客户{td.ClientId})簿记日{valueDate:yyyy-MM-dd}(Action={cashAction})可用授信={creditAvailable:0.00},{marginLegs.Count}条预付金腿分配:{(plans.Any() ? string.Join(";", plans.Select(p => $"腿{p.Leg.id}应付{p.Amount:0.00}→授信{p.CreditAmount:0.00}/现金{p.CashAmount:0.00}{(p.NeedSplit ? "(拆单)" : "")}")) : "无正应付腿,全部直通现金标签")}"); //先落库拆分的新现金腿(需要 id 才能绑定现金流水) foreach (var plan in plans.Where(p => p.NeedSplit)) diff --git a/YLErpDAL/Modules/TradeModule/DocGenerateModule/SwapSettlementBillRowBuilder.cs b/YLErpDAL/Modules/TradeModule/DocGenerateModule/SwapSettlementBillRowBuilder.cs index d6063274..d67792bb 100644 --- a/YLErpDAL/Modules/TradeModule/DocGenerateModule/SwapSettlementBillRowBuilder.cs +++ b/YLErpDAL/Modules/TradeModule/DocGenerateModule/SwapSettlementBillRowBuilder.cs @@ -1,6 +1,7 @@ using YLErp.DBModels; using YLErp.DBModels.Consts; using YLErp.DBModels.Enums; +using YLErp.Helpers; using YLErp.Models; using YLErp.Modules.SwapModule; using YLErp.Modules.SwapModule.ReturnLegs; @@ -9,7 +10,7 @@ namespace YLErp.Modules.TradeModule.DocGenerateModule; /// /// 构造普通收益互换结算单单行数据所需的业务输入。 -/// 数据来自平仓事件、关联交易及结算日日终持仓。 +/// 数据来自平仓事件及其关联交易。 /// public sealed class SwapSettlementBillRowInput { @@ -40,9 +41,6 @@ public sealed class SwapSettlementBillRowInput /// 平仓事件浮动腿记录的期末结算收益率(展示态数值)。 public decimal? ExitYtm { get; set; } - /// 结算日日终持仓中的当日浮动端分红,保留原始收付方向。 - public decimal CurrentDayFloatingDividend { get; set; } - /// 期间付息或分红是否计入本次净额结算。 public bool IncludePeriodPaymentInNetting { get; set; } } @@ -89,6 +87,7 @@ public static class SwapSettlementBillRowBuilder var floatingAmount = -input.CloseFlow.MarkClosePnl; var fee = -(input.CloseFlow.TradingFee + input.CloseFlow.TradingFeePending); var marginInterest = -marginEvents.Sum(x => x.InterestClosePnL); + var periodAmount = -input.CloseFlow.DividendPending; var initialMargin = SumMargin(effectiveMargins, InterestModeEnum.初始预付金); var additionalMarginPositions = positions @@ -106,6 +105,16 @@ public static class SwapSettlementBillRowBuilder var isCashBond = ConsGlobal.InstrumentType.IsBond(input.UnderlyingInstrumentType); + var isEtf = ConsGlobal.InstrumentType.Fund.Equals( + input.UnderlyingInstrumentType, + StringComparison.OrdinalIgnoreCase); + var initialPrice = settlementPosition?.PosiGrossPrice ?? 0m; + var closePrice = input.CloseFlow.TradingAmountAvg; + if (isCashBond) + { + initialPrice = BondPriceConverter.ToDisplay(initialPrice); + closePrice = BondPriceConverter.ToDisplay(closePrice); + } return new ExcelReportModel @@ -121,16 +130,16 @@ public static class SwapSettlementBillRowBuilder PosiNotionalValue = input.CloseNotionalValue.ToString("0.00"), Quantity = input.CloseFlow.Quantity.ToString("0.00"), DividendIn = isCashBond - ? input.CurrentDayFloatingDividend.ToString("0.00") + ? periodAmount.ToString("0.00") : string.Empty, - PeriodDividend = isCashBond - ? string.Empty - : input.CurrentDayFloatingDividend.ToString("0.00"), - PosiNetPrice = ((settlementPosition?.PosiGrossPrice ?? 0m) * 100m).ToString("0.00000000"), + PeriodDividend = isEtf + ? periodAmount.ToString("0.00") + : string.Empty, + PosiNetPrice = initialPrice.ToString("0.00000000"), InitYtm = isCashBond && input.Trade.InitYtm.HasValue ? input.Trade.InitYtm.Value.ToString("0.####%") : string.Empty, - ClosePrice = (input.CloseFlow.TradingAmountAvg * 100m).ToString("0.00000000"), + ClosePrice = closePrice.ToString("0.00000000"), ExitYtm = input.ExitYtm.HasValue ? input.ExitYtm.Value.ToString("0.0000") : string.Empty, diff --git a/YLErpDAL/Modules/TradeModule/TradeBLL.cs b/YLErpDAL/Modules/TradeModule/TradeBLL.cs index 261f1ef8..d629451b 100644 --- a/YLErpDAL/Modules/TradeModule/TradeBLL.cs +++ b/YLErpDAL/Modules/TradeModule/TradeBLL.cs @@ -334,7 +334,9 @@ namespace YLErp.BLL .FirstOrDefault(); if (oa != null) { - tradeLinq.OaRemark = string.IsNullOrWhiteSpace(oa.oa_msg) + tradeLinq.OaRemark = oa.status == "提交失败" + ? oa.status + : string.IsNullOrWhiteSpace(oa.oa_msg) ? oa.status : oa.status + ":" + oa.oa_msg; } diff --git a/YLErpDAL/Modules/UnderlyingModule/FundManagerLookupService.cs b/YLErpDAL/Modules/UnderlyingModule/FundManagerLookupService.cs index b37d8f5e..8d3808ba 100644 --- a/YLErpDAL/Modules/UnderlyingModule/FundManagerLookupService.cs +++ b/YLErpDAL/Modules/UnderlyingModule/FundManagerLookupService.cs @@ -30,16 +30,21 @@ namespace YLErp.Modules.UnderlyingModule public string InvestAdvisorName { get; set; } } - private const string LookupSql = @" + //物理库名由连接串 database= 决定(DbSchema 解析),各环境库名不同(如测试环境 glms_bigdata), + //SQL 不硬编码库名;连接开在 bigdata 数据源上,将来跨库 join ERP 主库时用 DbSchema.Of("ylcms") 同法插值 + private string BuildLookupSql() + { + return $@" SELECT ia.investadvisorcode AS InvestAdvisorCode, ia.investadvisorname AS InvestAdvisorName -FROM glms_bigdata.mf_fundarchives AS fa -INNER JOIN glms_bigdata.mf_investadvisoroutline AS ia +FROM {DbSchema.Of("bigdata")}.mf_fundarchives AS fa +INNER JOIN {DbSchema.Of("bigdata")}.mf_investadvisoroutline AS ia ON CONVERT(fa.investadvisorcode USING utf8mb4) COLLATE utf8mb4_unicode_ci = CONVERT(ia.investadvisorcode USING utf8mb4) COLLATE utf8mb4_unicode_ci WHERE CONVERT(fa.secucode USING utf8mb4) COLLATE utf8mb4_unicode_ci = CONVERT(TRIM(SUBSTRING_INDEX(@UnderlyingCode, '.', 1)) USING utf8mb4) COLLATE utf8mb4_unicode_ci"; + } public FundManagerLookupResult Lookup(string underlyingCode) { @@ -49,7 +54,7 @@ WHERE CONVERT(fa.secucode USING utf8mb4) COLLATE utf8mb4_unicode_ci = return new FundManagerLookupResult { Status = FundManagerLookupStatus.NotFound }; } - var connectionString = AppManager.GetConnectionString("glms_bigdata"); + var connectionString = AppManager.GetConnectionString("bigdata"); if (string.IsNullOrWhiteSpace(connectionString)) { return new FundManagerLookupResult { Status = FundManagerLookupStatus.Unavailable }; @@ -58,7 +63,7 @@ WHERE CONVERT(fa.secucode USING utf8mb4) COLLATE utf8mb4_unicode_ci = try { using var connection = new MySqlConnection(connectionString); - var matches = connection.Query(LookupSql, new { UnderlyingCode = normalizedCode }, commandTimeout: 10) + var matches = connection.Query(BuildLookupSql(), new { UnderlyingCode = normalizedCode }, commandTimeout: 10) .Where(row => !string.IsNullOrWhiteSpace(row.InvestAdvisorName)) .GroupBy(row => (row.InvestAdvisorCode ?? string.Empty).Trim(), StringComparer.OrdinalIgnoreCase) .Select(group => group.Select(row => row.InvestAdvisorName.Trim()).Distinct(StringComparer.OrdinalIgnoreCase).ToArray()) diff --git a/YLErpWeb/Controllers/client_margin_templateController.cs b/YLErpWeb/Controllers/client_margin_templateController.cs index df68ab84..1f3b645c 100644 --- a/YLErpWeb/Controllers/client_margin_templateController.cs +++ b/YLErpWeb/Controllers/client_margin_templateController.cs @@ -33,6 +33,11 @@ namespace YLErp.Web.Controllers else { var marginTemplate = yldb.margin_template_v2.Find(template.MarginTemplateId); + //绑定模板不在上方下拉筛选范围内(如已失效)时,Find 得到的是未挂 Details 的新实例,右栏只读回显会空白 + if (marginTemplate.Details.Count == 0) + { + marginTemplate.Details = yldb.margin_template_detail.Where(y => y.MarginTemplateId == marginTemplate.id).ToList(); + } ViewBag.MarginTemplate = marginTemplate; } return View(template); diff --git a/YLErpWeb/Views/client_margin_template/client_margin_templateEdit.cshtml b/YLErpWeb/Views/client_margin_template/client_margin_templateEdit.cshtml index cf7f5f76..3e7dce42 100644 --- a/YLErpWeb/Views/client_margin_template/client_margin_templateEdit.cshtml +++ b/YLErpWeb/Views/client_margin_template/client_margin_templateEdit.cshtml @@ -51,7 +51,7 @@ @if (pageObj.isAdd) { @*新增:客户多选(类似适用簿记账户),保存时按选中客户逐条调用原有单条保存接口*@ - } @@ -104,8 +104,69 @@
+ @*区间追保结构(规则15)只读回显:参数组区块与 margin_template_v2Edit 同款样式(本页为绑定信息页,参数本身在预付金模板V2维护,故全部只读)*@ +