From 68b21f2c985b1943b152f0134053497c37a6ef42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=B9=E5=B3=B0?= Date: Thu, 6 Aug 2026 13:09:27 +0800 Subject: [PATCH 1/3] feat: ai code review fix --- YLErpDAL/Modules/RiskEngine/RiskRule.cs | 2 +- YLErpDAL/Modules/RiskEngine/RiskRuleService.cs | 13 ++++++++++++- .../RiskEngine/RuleConditionExpressionBuilder.cs | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/YLErpDAL/Modules/RiskEngine/RiskRule.cs b/YLErpDAL/Modules/RiskEngine/RiskRule.cs index c4e298d0..b9089261 100644 --- a/YLErpDAL/Modules/RiskEngine/RiskRule.cs +++ b/YLErpDAL/Modules/RiskEngine/RiskRule.cs @@ -19,7 +19,7 @@ namespace YLErp.Modules.RiskEngine /// /// 【核心】条件JSON(结构化模式的条件列表存储),如 - /// {"conditions":[{"variableId":1,"operator":">","value":100000000}]} + /// {"conditions":[{"variableId":1,"operator":"gt","value":100000000}]} /// 由 RuleCompiler 解析并编译为 CompiledScript /// public string ConditionJson { get; set; } diff --git a/YLErpDAL/Modules/RiskEngine/RiskRuleService.cs b/YLErpDAL/Modules/RiskEngine/RiskRuleService.cs index 00e18987..6cd51d3e 100644 --- a/YLErpDAL/Modules/RiskEngine/RiskRuleService.cs +++ b/YLErpDAL/Modules/RiskEngine/RiskRuleService.cs @@ -1093,7 +1093,16 @@ namespace YLErp.Modules.RiskEngine foreach (var app in affectedApps) { - var remainingIds = ParseStoredRuleIds(app.RuleIds).Where(id => id != ruleId).ToList(); + List remainingIds; + try + { + remainingIds = ParseStoredRuleIds(app.RuleIds).Where(id => id != ruleId).ToList(); + } + catch (ServiceException ex) + { + _logger.Error($"删除规则时解析应用关联规则 ID 格式异常,跳过应用 {app.id}:{ex.Message}"); + continue; + } app.RuleIds = string.Join(",", remainingIds); app.Version++; app.UpdateOptId = UserId; @@ -1907,8 +1916,10 @@ namespace YLErp.Modules.RiskEngine /// private List GetRulesByVariableId(long variableId) { + var idText = variableId.ToString(); var allRules = DbContext.glms_risk_rule .Where(r => r.Status != RiskRuleStatus.Deleted && r.ConditionJson != null && r.ConditionJson != "") + .Where(r => r.ConditionJson.Contains(idText)) .ToList(); return allRules diff --git a/YLErpDAL/Modules/RiskEngine/RuleConditionExpressionBuilder.cs b/YLErpDAL/Modules/RiskEngine/RuleConditionExpressionBuilder.cs index 46945ca0..1c260c3b 100644 --- a/YLErpDAL/Modules/RiskEngine/RuleConditionExpressionBuilder.cs +++ b/YLErpDAL/Modules/RiskEngine/RuleConditionExpressionBuilder.cs @@ -72,7 +72,7 @@ namespace YLErp.Modules.RiskEngine var idText = variableId.ToString(CultureInfo.InvariantCulture); return VariableReferenceFields.Any(field => - Regex.IsMatch(conditionJson, $@"""{field}""\s*:\s*{idText}\b", RegexOptions.CultureInvariant)); + Regex.IsMatch(conditionJson, $@"""{field}""\s*:\s*{idText}(?=[,\s}}\]])", RegexOptions.CultureInvariant)); } public static string Build( From f256a623df6758b9a19fa885c3e089ddf344fa86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=B9=E5=B3=B0?= Date: Thu, 6 Aug 2026 13:10:05 +0800 Subject: [PATCH 2/3] feat: create_eod_bond_lending_rate_table.sql --- .../create_eod_bond_lending_rate_table.sql | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/create_eod_bond_lending_rate_table.sql diff --git a/Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/create_eod_bond_lending_rate_table.sql b/Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/create_eod_bond_lending_rate_table.sql new file mode 100644 index 00000000..65572e76 --- /dev/null +++ b/Framework/YLErp.Resources/DbUpdate/Ver-5.6.0/create_eod_bond_lending_rate_table.sql @@ -0,0 +1,26 @@ +-- 仅供参考,正式脚本以 TRS 为准 +CREATE TABLE `eod_bond_lending_rate` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `RecordCode` varchar(96) NOT NULL COMMENT '业务唯一代码:UnderlyingSecurityId_yyyyMMdd', + `ValueDate` date NOT NULL COMMENT '业务日期', + `UnderlyingSecurityId` varchar(64) NOT NULL COMMENT '标的债券代码', + `UnderlyingSymbol` varchar(255) DEFAULT NULL COMMENT '标的债券名称', + `PreCloseRate` decimal(18,8) DEFAULT NULL COMMENT '前收盘费率(%)', + `PreWeightedAvgRate` decimal(18,8) DEFAULT NULL COMMENT '前加权平均费率(%)', + `OpenRate` decimal(18,8) DEFAULT NULL COMMENT '开盘费率(%)', + `LatestRate` decimal(18,8) DEFAULT NULL COMMENT '最新费率(%)', + `HighRate` decimal(18,8) DEFAULT NULL COMMENT '最高费率(%)', + `LowRate` decimal(18,8) DEFAULT NULL COMMENT '最低费率(%)', + `CloseRate` decimal(18,8) DEFAULT NULL COMMENT '收盘费率(%)', + `WeightedAvgRate` decimal(18,8) DEFAULT NULL COMMENT '加权平均费率(%)', + `TurnoverAmount` decimal(24,4) DEFAULT NULL COMMENT '成交量(元)', + `OptId` int NOT NULL DEFAULT 0 COMMENT '操作人ID,行情自动同步固定为0', + `OptName` varchar(64) NOT NULL DEFAULT '行情中心' COMMENT '操作人名称', + `OptDate` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '操作时间', + `DataSource` varchar(32) NOT NULL DEFAULT '系统' COMMENT '数据来源:系统/人工', + `SourceTime` varchar(40) DEFAULT NULL COMMENT '源行情时间,优先使用transactTime', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_eod_bond_lending_rate_record_code` (`RecordCode`), + KEY `idx_eod_bond_lending_rate_security_date` (`UnderlyingSecurityId`, `ValueDate`), + KEY `idx_eod_bond_lending_rate_opt_date` (`OptDate`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='标的债券借贷费率日终行情(Topic 20028)'; \ No newline at end of file From 1bf517188cfa14f11cb51d2a681f7406c50ce4a0 Mon Sep 17 00:00:00 2001 From: hjhan Date: Fri, 7 Aug 2026 09:28:18 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(swap):=20=E5=BE=85=E7=BB=93=E7=AE=97?= =?UTF-8?q?=E5=88=86=E7=BA=A2=E6=94=B6=E7=9B=8A(DividendPending)=E6=94=B9?= =?UTF-8?q?=E7=94=A8=20EOD=20=E5=85=A8=E9=87=8F=E5=8F=A3=E5=BE=84=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20e3c473ba=20=E7=9A=84=E7=A1=AC0=E5=9B=9E?= =?UTF-8?q?=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit e3c473ba 把前端 DividendPending 硬编码 0,对 GLMS-20260105-0004(PosiDividendSum 恰好=0) 测试通过、掩盖问题。但对 PosiDividendSum≠0 的部分平仓交易,硬0 会落库 (SwapFlowEventService:588 冲账取负写入 swap_flow_event.DividendPending)并在事件列表 "待结算分红收益"列显示错误的 0——是确定的回归。 口径论证(代码证据,非拍板):DividendPending(待结算存量) ↔ EOD PosiDividendSum(未实现), 字段名、递推公式(SwapEodPositionService:1825 已扣当日实现)、列标签(SwapflowList:561)、 旧前端意图(posiQty×totalInterest 想表达"平仓后剩余")四处同指全量口径。是存量非流量, 不按本次平仓比例分摊。 改动: - SwapDealService.InitUnwind:GetPreEodDividendSum 返回值取一次,同时喂 DividendIn 与 DividendPending(避免重复查库);GetPreEodDividendSum 注释补口径论证+历史教训(防后人改回硬0/分摊) - 前端 unwindSwapTrade.js:删除 DividendPending=0 硬写,保持后端值不动 - 前端 incomeSwapTrade.js:DividendPending 保持 0(互换=全量结清,结清后待结算归0), 注释说明与平仓页口径差异 - 测试补非0场景:GLMS-20260706-0004(PosiDividendSum=-150,000)验证返回非0真实值, 直接针对"硬0被0掩盖"盲区,防止该回归复现 全量构建 0 错误,4 个分红测试全绿(含 0 场景、互换页、无EOD防御、非0场景)。 --- ...GLMS20260105PartialCloseDividendBugTest.cs | 61 +++++++++++++++++++ .../Modules/SwapModule/SwapDealService.cs | 28 +++++++-- .../Scripts/app/swaptrade/incomeSwapTrade.js | 4 +- .../Scripts/app/swaptrade/unwindSwapTrade.js | 12 ++-- 4 files changed, 94 insertions(+), 11 deletions(-) diff --git a/UnitTestProject/Modules/SwapModule/GLMS20260105PartialCloseDividendBugTest.cs b/UnitTestProject/Modules/SwapModule/GLMS20260105PartialCloseDividendBugTest.cs index a55c47b6..d3ee8e5f 100644 --- a/UnitTestProject/Modules/SwapModule/GLMS20260105PartialCloseDividendBugTest.cs +++ b/UnitTestProject/Modules/SwapModule/GLMS20260105PartialCloseDividendBugTest.cs @@ -154,5 +154,66 @@ namespace YLErp.Modules.SwapModule } finally { db?.Dispose(); } } + + // ============================================================================ + // 非0场景验收(关键防回归) + // ---------------------------------------------------------------------------- + // GLMS-20260105-0004 的 PosiDividendSum 恰好=0(3/2 已全额互换),无法暴露 + // "DividendPending 硬编码 0" 的回归。本测试用一笔 PosiDividendSum≠0 的活交易 + // GLMS-20260706-0004(标的 230004.IB,持仓 1亿,待实现分红 -150,000,多日稳定) + // 验证:GetPreEodDividendSum 返回非0的 -150,000 → 修复后 DividendIn/DividendPending + // 均应为此值(而非历史硬0回归)。 + // 若本测试因硬0回归而失败(实际=0),即说明有人把 DividendPending 改回了硬0, + // 或 GetPreEodDividendSum 传导链路被破坏。 + // ============================================================================ + + /// + /// 非0场景:PosiDividendSum≠0 时,GetPreEodDividendSum 返回真实非0值, + /// 该值应同时成为 DividendIn 与 DividendPending(全量口径)。 + /// 这是针对"硬0被0掩盖"盲区的核心防回归测试。 + /// + [TestMethod] + [TestCategory("DBRecording")] + public void Green_NonZeroPosiDividendSum_Returns_RealValue_NotHardZero() + { + const string nonZeroTrade = "GLMS-20260706-0004"; // PosiDividendSum=-150,000,确认成交活交易 + YLContext db; + try { db = DbContextFactory.GetYLDbContext(); } + catch (Exception ex) { Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); return; } + + try + { + var td = db.trade.FirstOrDefault(t => t.TradeNumber == nonZeroTrade); + if (td == null) { Assert.Inconclusive($"测试库不存在交易 {nonZeroTrade}"); return; } + + var floatLeg = db.swap_position + .Where(x => x.SwapTradeId == td.id && !x.Invalid && !x.IsInitial && x.UnderlyingCode != null && x.UnderlyingCode != "") + .FirstOrDefault(); + Assert.IsNotNull(floatLeg, $"未找到 {nonZeroTrade} 的浮动腿持仓"); + + // 取该交易最近一次 EOD 的 PosiDividendSum 作为期望值(多日稳定 -150,000) + var lastEod = db.eod_swap_position + .Where(x => x.SwapTradeId == td.id && !x.Invalid && x.PositionId == floatLeg.PositionId && x.PosiQuantity > 0) + .OrderByDescending(x => x.ValueDate).FirstOrDefault(); + Assert.IsNotNull(lastEod, $"未找到 {nonZeroTrade} 的有效 EOD"); + Assert.AreNotEqual(0m, lastEod.PosiDividendSum, 0.01m, + $"前置:{nonZeroTrade} 的 PosiDividendSum 应≠0(本测试专为非0场景设计),实际={lastEod.PosiDividendSum}。" + + $"若该交易已互换/平仓致归0,请换另一笔 PosiDividendSum≠0 的活交易。"); + + // dealDate 取 lastEod 次日,确保 GetPreEodDividendSum 读到这笔非0 EOD + var dealDate = lastEod.ValueDate.AddDays(1); + var service = new RealDbSwapDealService(); + decimal dividend = service.ExposeGetPreEodDividendSum(td.id, floatLeg.PositionId, dealDate); + + // ---- 核心:返回值=真实非0的 PosiDividendSum,不是硬0 ---- + Assert.AreEqual(lastEod.PosiDividendSum, dividend, 0.01m, + $"[非0场景·方案C验收] {nonZeroTrade} GetPreEodDividendSum 应返回 EOD PosiDividendSum={lastEod.PosiDividendSum}," + + $"实际={dividend}。该值将同时成为 DividendPending(待结算分红收益,全量口径)。" + + $"若实际=0,说明 DividendPending 硬0回归未修复,或传导链路被破坏(参见 SwapDealService.GetPreEodDividendSum 注释的口径论证)。"); + + Console.WriteLine($"[非0场景验证通过] {nonZeroTrade}: PosiDividendSum={dividend}(非0)→ DividendIn/DividendPending 均为此值,非硬0。"); + } + finally { db?.Dispose(); } + } } } diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index 3b44d623..2c0e6310 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -352,7 +352,13 @@ namespace YLErp.Modules.SwapModule floatEvent.EventReason = "交易"; // 方案C:分红收益改由上一收盘日 EOD PosiDividendSum 提供(单一可信源), // 前端 getDivindIn 不再覆盖;消除"期初持仓×totalInterest"对已平仓部分的重复计入。 - floatEvent.DividendIn = GetPreEodDividendSum(tradeId, position.PositionId, dealDate); + // 同一 EOD 值取一次喂两栏: + // DividendIn = "浮动端平仓盈亏·分红"(本次动作要落袋的,落库后被前端按需展示) + // DividendPending = "待结算分红收益"(仍挂在账上、未来才结的存量 = PosiDividendSum 全量口径, + // 见 GetPreEodDividendSum 注释的口径论证;切勿改回硬0或分摊,会落库回归) + decimal preEodDividendSum = GetPreEodDividendSum(tradeId, position.PositionId, dealDate); + floatEvent.DividendIn = preEodDividendSum; + floatEvent.DividendPending = preEodDividendSum; floatEvent.UnderlyingCode = position.UnderlyingCode; floatEvent.UnderlyingInstrumentType = position.UnderlyingInstrumentType; floatEvent.CloseFee = 0; @@ -962,7 +968,7 @@ namespace YLErp.Modules.SwapModule /// /// 读取"上一收盘日"浮动腿的待实现分红(eod_swap_position.PosiDividendSum), - /// 用于平仓/互换预览页展示"浮动端平仓盈亏·分红(DividendIn)"。 + /// 用于平仓/互换预览页展示"浮动端平仓盈亏·分红(DividendIn)" 与 "待结算分红收益(DividendPending)"。 /// 方案C:替代前端 totalInterest × 期初持仓 的重算——后者会把登记日前已平仓、 /// 不享有该笔分红的部分重复计入(GLMS-20260105-0004 误显 -36,160)。 /// EOD 的 PosiDividendSum 已按"实际持仓递推 + 当日实现扣除"算出待实现分红, @@ -973,10 +979,22 @@ namespace YLErp.Modules.SwapModule /// 抽为 protected virtual:与 GetMaxIncomeValueDate 一致,便于测试替身覆写、 /// 也兼容无 EOD 的边界(返回 0,与历史 DividendIn=0 行为一致)。 /// - /// 互换交易ID - /// 浮动腿持仓ID(swap_position.PositionId) - /// 预览/平仓业务日期 /// 上一收盘日该浮动腿的待实现分红;无 EOD 记录返回 0 + /// + /// 【口径论证·勿改】为什么 DividendPending 也用本方法的全量值(非分摊、非硬0): + /// 1. 字段语义直接对应:EOD PosiDividendSum 的 DisplayName="浮动端平仓盈亏·分红未实现" + /// (EodSwapPosition.cs:186),递推式 PosiDividendSum=前日+当日新计-当日实现 + /// (SwapEodPositionService.cs:1825),即"扣过当日实现后、还挂在账上未来才结的存量"。 + /// 前端列"待结算分红收益"(SwapflowList.js:561) 字面就是同一回事 → 直接取 PosiDividendSum。 + /// 2. 是"存量"非"流量":DividendPending 描述的是"账上还欠多少"(与本次平仓比例无关的总额), + /// 而 DividendIn 才是"本次动作落袋多少"。两者口径本就不同,各自正确。若把 DividendPending 改成 + /// 按本次平仓比例分摊,会把"存量"误当"流量",与列名"待结算"矛盾。 + /// 3. 历史教训:方案C 初版曾把前端 DividendPending 硬编码 0(commit e3c473ba),因测试交易 + /// PosiDividendSum 恰好=0(3/2 已全额互换)而测试通过、掩盖问题。但对 PosiDividendSum≠0 的部分 + /// 平仓交易,硬0 会落库(SwapFlowEventService.cs:588 冲账取负写入 swap_flow_event.DividendPending) + /// 并在事件列表"待结算分红收益"列显示错误的 0 —— 这是确定的回归。故本方法返回值同时喂两栏, + /// 前端不得再覆盖。例外:互换页 DividendPending 保持 0(互换语义=全量结清,结清后待结算归0)。 + /// protected virtual decimal GetPreEodDividendSum(int tradeId, long positionId, DateTime dealDate) { var lastEod = DbContext.eod_swap diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js index 264da52b..105f68c9 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js @@ -268,7 +268,9 @@ const vue = new Vue({ var thisObj = this; // 方案C:分红改由后端 InitIncome 读 EOD PosiDividendSum 填入 floatPosition.DividendIn(单一可信源)。 // 前端不再调用 GetBondPayMentInterest 自算——消除"期初持仓×totalInterest"对已平仓部分的重复计入。 - // floatPosition.DividendIn 保持后端返回值不动;互换为全量消费,DividendPending=0。 + // floatPosition.DividendIn 保持后端返回值不动(=本次互换要落袋的全量待实现)。 + // DividendPending(待结算)互换页保持 0:互换=全量结清,结清后账上无待结算。 + // ↳ 与平仓页不同:平仓只拿走一部分,剩余持仓仍有待结算 → 平仓页 DividendPending=PosiDividendSum(后端值)。 thisObj.floatPosition.DividendPending = 0; thisObj.calcFloatClosePnl(); thisObj.dataFormat(); diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js index 82db3722..1c038aef 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js @@ -387,11 +387,13 @@ const vue = new Vue({ }, getDivindIn() { var thisObj = this; - // 方案C:分红改由后端 InitUnwind 读 EOD PosiDividendSum 填入 floatPosition.DividendIn(单一可信源)。 - // 前端不再调用 GetBondPayMentInterest 自算——消除"期初持仓×totalInterest"对已平仓部分的重复计入 - // (GLMS-20260105-0004 平仓前部分平仓40%后,再平仓时分红误显 -36,160,应为 0)。 - // floatPosition.DividendIn 保持后端返回值不动;DividendPending(纯展示列)置 0。 - thisObj.floatPosition.DividendPending = 0; + // 方案C:分红改由后端 InitUnwind 读 EOD PosiDividendSum 填入 floatPosition.DividendIn 与 DividendPending + // (单一可信源)。前端不再调用 GetBondPayMentInterest 自算——消除"期初持仓×totalInterest"对已平仓 + // 部分的重复计入(GLMS-20260105-0004 平仓前部分平仓40%后,再平仓时分红误显 -36,160,应为 0)。 + // ⚠ floatPosition.DividendIn / DividendPending 均保持后端返回值不动,前端不得覆盖: + // - DividendIn(本次落袋)、DividendPending(待结算存量=PosiDividendSum 全量口径)。 + // - 互换页 DividendPending 保持 0(互换=全量结清,结清后待结算归0),见 incomeSwapTrade.js。 + // - 历史:曾硬编码 DividendPending=0,对 PosiDividendSum≠0 的部分平仓会落库错误的 0(回归)。 thisObj.calcFloatClosePnl(); thisObj.dataFormat(); },