From 3118c4436543abddccf10ad031979a4f00582e92 Mon Sep 17 00:00:00 2001 From: hjhan Date: Wed, 1 Jul 2026 12:22:57 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BA=92=E6=8D=A2=E4=B8=89=E4=B8=AAbug?= =?UTF-8?q?=E7=9A=84TDD=E7=BA=A2=E7=81=AF/=E7=BB=BF=E7=81=AF=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 单利利息偏大(红灯): Step1_SimpleInterestRedTest 用1813坐实 单利路径(CalcDailySimpleInterest)未加consumedInterest扣除, 默认值偏大8271(历史已结利息未扣)。复利路径c6adb3bb已修。 审核状态卡死(红灯): SwapApproveStatusStuckTest 3个方法 坐实28/28互换事件CloseMethod=0走else分支(无ExerciseDate判断), 当前3笔交易实际卡死(到期+确认成交)。Step2分析副作用:Notional清零 需放cs:1577之后避免被td.Notional=td.TradeAmount覆盖。 重收盘误删(绿灯确认): Step1加修复后逻辑模拟,确认排除manualClientCashIds 后手动互换记录不再被命中(78751f0a已修)。 --- .../SwapModule/SwapApproveStatusStuckTest.cs | 246 ++++++++++++++++++ .../SwapPartialUnwindInterestDefaultTest.cs | 193 ++++++++++++++ .../SwapReEodDeleteManualCashRecordTest.cs | 37 ++- 3 files changed, 473 insertions(+), 3 deletions(-) create mode 100644 UnitTestProject/Modules/SwapModule/SwapApproveStatusStuckTest.cs diff --git a/UnitTestProject/Modules/SwapModule/SwapApproveStatusStuckTest.cs b/UnitTestProject/Modules/SwapModule/SwapApproveStatusStuckTest.cs new file mode 100644 index 00000000..013388ae --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/SwapApproveStatusStuckTest.cs @@ -0,0 +1,246 @@ +using YLErp.DBModels; +using YLErp.DBModels.Enums; + +namespace YLErp.Modules.SwapModule +{ + /// + /// 互换收益结算审核后状态卡死 - TDD 红灯测试 + /// ============================================================================ + /// 背景: + /// ApproveSwapTrade(cs:1556-1569) 的状态分支只判 CloseMethod==全部平仓, + /// 没有 ExerciseDate 到期判断。互换操作的 CloseMethod 默认 0(Unknown)≠1(全部平仓), + /// 必走 else → TradeStatus 退回"确认成交"。到期互换审核后状态不流转成"已到期", + /// 导致 EodCheckMaturityTrade 一直阻止收盘。 + /// 对比:免审核路径 SwapIncome(cs:1512-1517) 有正确的 ExerciseDate 判断。 + /// + /// TDD 红灯→绿灯: + /// 红灯(当前):存在"到期+确认成交+有互换事件"的卡死交易 + /// 绿灯(修复后):审核后状态正确变成"已到期",无卡死交易 + /// ============================================================================ + [TestClass] + public class SwapApproveStatusStuckTest + { + /// + /// Step0:探查"卡死"的交易——到期日已过 + TradeStatus=确认成交 + 有互换审核事件。 + /// + /// 这些交易就是被 ApproveSwapTrade bug 卡住的:审核通过了但状态没变"已到期"。 + /// + [TestMethod] + [TestCategory("DBRecording")] + public void Step0_ListStuckMaturityTrades() + { + YLContext db; + try { db = DbContextFactory.GetYLDbContext(); } + catch (Exception ex) + { + Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); + return; + } + + try + { + var today = DateTime.Today; + + // 找"到期日已过 + 状态仍确认成交"的互换交易(收盘会阻止的就是这些) + var stuckTrades = db.trade + .Where(t => t.TradeType == "收益互换" + && t.ValidState != ConsGlobal.InValid + && t.TradeStatus == ConsTrade.确认成交 + && t.ExerciseDate < today) + .ToList(); + + Console.WriteLine($"=== 到期日已过 + 状态=确认成交 的互换交易: {stuckTrades.Count} 笔 ===\n"); + + int stuckWithSwap = 0; + Console.WriteLine($"{"TradeId",8} {"TradeNumber",-24} {"ExerciseDate",12} {"到期天数",8} {"有互换事件",10} {"互换审核次数",10}"); + foreach (var t in stuckTrades.Take(30)) + { + // 检查是否有互换事件(EventType=互换=3) + var swapEvents = db.swap_event + .Where(x => x.SwapTradeId == t.id && x.EventType == (int)SwapEventTypeEnum.互换) + .ToList(); + var validSwapEvents = swapEvents.Where(x => !x.Invalid).ToList(); + bool hasSwap = validSwapEvents.Any(); + if (hasSwap) stuckWithSwap++; + + int daysExpired = (today - t.ExerciseDate.Value).Days; + Console.WriteLine($"{t.id,8} {t.TradeNumber,-24} {t.ExerciseDate:yyyy-MM-dd} {daysExpired,8} {(hasSwap ? "是" : "否"),10} {validSwapEvents.Count,10}"); + } + + Console.WriteLine($"\n其中有互换事件(疑似审核卡死): {stuckWithSwap} 笔"); + + if (stuckTrades.Count == 0) + { + Console.WriteLine("(无卡死交易,可能已全部修复或无到期交易)"); + } + } + finally + { + db?.Dispose(); + } + } + + /// + /// Step1:验证 ApproveSwapTrade 的状态流转缺陷(代码逻辑坐实,不依赖卡死数据)。 + /// + /// 核心验证: + /// 1. 统计所有有效互换事件(EventType=互换)的 CloseMethod 分布 + /// 2. 若 CloseMethod 普遍≠全部平仓(1),则 ApproveSwapTrade 的 else 分支(cs:1565)必被命中 + /// 3. 该 else 分支无 ExerciseDate 判断 → 到期互换审核后状态退回"确认成交" + /// → bug 逻辑坐实(不管当前是否有卡死的样本数据) + /// + /// 同时检查当前是否有实际卡死的交易(到期+确认成交+有互换事件)。 + /// + [TestMethod] + [TestCategory("DBRecording")] + public void Step1_VerifyApproveStatusStuckLogic() + { + YLContext db; + try { db = DbContextFactory.GetYLDbContext(); } + catch (Exception ex) + { + Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); + return; + } + + try + { + Console.WriteLine($"===== 验证 ApproveSwapTrade 状态流转缺陷 =====\n"); + + // 1. 统计所有有效互换事件的 CloseMethod 分布 + var swapEvents = db.swap_event + .Where(x => x.EventType == (int)SwapEventTypeEnum.互换 && !x.Invalid) + .ToList(); + Console.WriteLine($"[1] 有效互换事件(EventType=互换, Invalid=False): {swapEvents.Count} 条"); + + int closeMethodUnknown = 0; // CloseMethod=0 + int closeMethodFull = 0; // CloseMethod=1(全部平仓) + int closeMethodPartial = 0; // CloseMethod=2(部分平仓) + int closeMethodOther = 0; + + foreach (var e in swapEvents) + { + var unwindData = !string.IsNullOrEmpty(e.EventData) + ? Newtonsoft.Json.JsonConvert.DeserializeObject(e.EventData) + : null; + int cm = unwindData?.CloseMethod ?? -1; + switch (cm) + { + case (int)CloseMethodEnum.Unknown: closeMethodUnknown++; break; + case (int)CloseMethodEnum.全部平仓: closeMethodFull++; break; + case (int)CloseMethodEnum.部分平仓: closeMethodPartial++; break; + default: closeMethodOther++; break; + } + } + + Console.WriteLine($" CloseMethod 分布:"); + Console.WriteLine($" Unknown(0)={closeMethodUnknown} 全部平仓(1)={closeMethodFull} 部分平仓(2)={closeMethodPartial} 其他={closeMethodOther}"); + Console.WriteLine($" → {(closeMethodUnknown + closeMethodPartial + closeMethodOther)}/{swapEvents.Count} 条走 ApproveSwapTrade else 分支(cs:1565)"); + + // 2. 逻辑坐实:只要存在 CloseMethod≠全部平仓 的互换事件,else 分支必被命中 + int elseBranchCount = closeMethodUnknown + closeMethodPartial + closeMethodOther; + Console.WriteLine($"\n[2] 逻辑坐实"); + Console.WriteLine($" ApproveSwapTrade(cs:1556): if(CloseMethod==全部平仓) → 已平仓"); + Console.WriteLine($" else(cs:1565) → 确认成交 [无ExerciseDate判断]"); + Console.WriteLine($" 互换事件走 else 分支的比例: {elseBranchCount}/{swapEvents.Count}"); + + // 3. 检查当前实际卡死的交易 + Console.WriteLine($"\n[3] 当前实际卡死交易检查"); + var today = DateTime.Today; + var stuckTrades = db.trade + .Where(t => t.TradeType == "收益互换" + && t.ValidState != ConsGlobal.InValid + && t.TradeStatus == ConsTrade.确认成交 + && t.ExerciseDate < today) + .ToList(); + int actualStuck = 0; + foreach (var t in stuckTrades) + { + var hasSwap = db.swap_event.Any(x => x.SwapTradeId == t.id + && x.EventType == (int)SwapEventTypeEnum.互换 && !x.Invalid); + if (hasSwap) actualStuck++; + } + Console.WriteLine($" 到期+确认成交+有互换事件: {actualStuck} 笔(这些是当前真正卡住的)"); + if (actualStuck == 0) + { + Console.WriteLine($" (当前无卡死样本——可能已手动处理/回退,但代码 bug 仍存在)"); + } + + Console.WriteLine($"\n[结论]"); + Console.WriteLine($" 代码逻辑缺陷坐实:{elseBranchCount}/{swapEvents.Count} 互换事件走 else 分支,该分支无 ExerciseDate 判断。"); + + // 红灯断言:互换事件 CloseMethod 普遍≠全部平仓 → else 分支必命中 → bug 存在 + Assert.IsTrue(elseBranchCount > 0, + "红灯:存在 CloseMethod≠全部平仓 的互换事件,会走 ApproveSwapTrade else 分支(无到期判断)。修复后此断言逻辑变化。"); + } + finally + { + db?.Dispose(); + } + } + + /// + /// Step2:验证修复方案(ApproveSwapTrade 补 ExerciseDate 判断)不引入新 bug。 + /// + /// 修复方案:在 else 分支加 if(eventType==互换 && ExerciseDate<=ValueDate) → 已到期。 + /// 需验证的副作用风险: + /// 1. 不影响平仓审核(eventType=平仓=2,加 eventType==互换 条件排除) + /// 2. 到期互换清零 Notional 不被 cs:1577 td.Notional=td.TradeAmount 覆盖 + /// 3. 部分平仓(非到期)不受影响(ExerciseDate>ValueDate 不进到期分支) + /// + /// 本方法纯查询验证:确认修复条件 eventType==互换 能正确区分互换/平仓审核。 + /// + [TestMethod] + [TestCategory("DBRecording")] + public void Step2_VerifyFixNoSideEffect() + { + YLContext db; + try { db = DbContextFactory.GetYLDbContext(); } + catch (Exception ex) + { + Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); + return; + } + + try + { + Console.WriteLine($"===== 验证修复方案副作用风险 =====\n"); + + // 风险1:平仓审核(eventType=2)不应被到期判断影响 + // 验证:找平仓审核事件,确认它们 EventType=平仓(2),修复条件 eventType==互换 会排除它们 + Console.WriteLine("[风险1] 平仓审核不受影响验证"); + var closeApproveEvents = db.swap_event + .Where(x => x.EventType == (int)SwapEventTypeEnum.平仓 && !x.Invalid) + .Take(10).ToList(); + Console.WriteLine($" 有效平仓事件: {closeApproveEvents.Count} 条,EventType 均为 {(int)SwapEventTypeEnum.平仓}(平仓)"); + Console.WriteLine($" 修复条件 eventType==互换({(int)SwapEventTypeEnum.互换}) → 平仓审核不进到期分支 ✅"); + Assert.IsTrue(closeApproveEvents.All(x => x.EventType == (int)SwapEventTypeEnum.平仓)); + + // 风险2:Notional 清零被覆盖问题 + // cs:1577 td.Notional = td.TradeAmount 在到期判断之后执行,会覆盖 td.Notional=0 + Console.WriteLine($"\n[风险2] Notional 清零覆盖验证"); + Console.WriteLine($" cs:1577 td.Notional = td.TradeAmount 在 else 分支之后执行"); + Console.WriteLine($" 若在 else 内设 td.Notional=0,会被 cs:1577 覆盖"); + Console.WriteLine($" → 修复时清零应放在 cs:1577 之后,或仿 SwapIncome 在 SaveChanges 前处理"); + Console.WriteLine($" (这是代码审查点,非运行时验证)"); + + // 风险3:部分平仓(非到期)不受影响 + Console.WriteLine($"\n[风险3] 非到期互换不受影响验证"); + Console.WriteLine($" 修复条件 ExerciseDate <= ValueDate:只有到期日才触发"); + Console.WriteLine($" 非到期互换(ExerciseDate > ValueDate)不进到期分支,仍走确认成交+HasPartialUnWind ✅"); + + // 对比 SwapIncome 的正确实现 + Console.WriteLine($"\n[对照] SwapIncome(cs:1512-1517) 的正确实现:"); + Console.WriteLine($" if (td.ExerciseDate <= unwindData.ValueDate)"); + Console.WriteLine($" {{ td.Notional = 0; td.StockEqvNotional = 0; td.TradeStatus = \"已到期\"; }}"); + Console.WriteLine($" 注意:SwapIncome 在 SaveSwapDeal 之后、SaveChanges 之前执行,Notional=0 不会被覆盖"); + + Assert.IsTrue(true, "副作用风险分析完成"); + } + finally + { + db?.Dispose(); + } + } + } +} diff --git a/UnitTestProject/Modules/SwapModule/SwapPartialUnwindInterestDefaultTest.cs b/UnitTestProject/Modules/SwapModule/SwapPartialUnwindInterestDefaultTest.cs index a0da7e58..cec5f921 100644 --- a/UnitTestProject/Modules/SwapModule/SwapPartialUnwindInterestDefaultTest.cs +++ b/UnitTestProject/Modules/SwapModule/SwapPartialUnwindInterestDefaultTest.cs @@ -99,6 +99,192 @@ namespace YLErp.Modules.SwapModule } } + /// + /// Step0e:探查"单利 + 有互换历史"的样本,用于验证单利路径是否也需要 consumedInterest 扣除。 + /// + /// 复利路径(c6adb3bb)已修,单利路径(CalcDailySimpleInterest)未修。 + /// 需找:单利利息腿 + 该腿有历史互换/自动互换事件(InterestAmount≠0) + 有eod。 + /// + [TestMethod] + [TestCategory("DBRecording")] + public void Step0e_ListSimpleInterestSwapTrades() + { + YLContext db; + try { db = DbContextFactory.GetYLDbContext(); } + catch (Exception ex) + { + Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); + return; + } + + try + { + // 找单利利息腿(InterestType=0=单利)且有历史互换事件的交易 + var simplePositions = db.swap_position + .Where(x => !x.Invalid && x.InterestDirection > 0 && x.InterestType == (int)InterestTypeEnum.单利) + .Select(x => new { x.SwapTradeId, x.id, x.InterestMode, x.InterestPrincipalFix }) + .ToList(); + + Console.WriteLine($"=== 单利利息腿持仓: {simplePositions.Count} 条 ===\n"); + + // 关联历史互换事件(InterestAmount≠0 说明有实际利息结算) + var tradeIds = simplePositions.Select(x => x.SwapTradeId).Distinct().ToList(); + var swapEvents = db.swap_flow_event + .Where(x => tradeIds.Contains(x.SwapTradeId) + && (x.EventType == (int)SwapFlowEventTypeEnum.互换 || x.EventType == (int)SwapFlowEventTypeEnum.自动互换) + && x.DataState == (int)SwapFlowDateStateEnum.完成 + && x.InterestAmount != 0) + .ToList(); + + var byTrade = simplePositions + .Where(p => swapEvents.Any(s => s.SwapTradeId == p.SwapTradeId && s.PositionId == p.id)) + .GroupBy(p => p.SwapTradeId) + .Select(g => new + { + SwapTradeId = g.Key, + 单利腿数 = g.Count(), + 利息模式 = string.Join("|", g.Select(x => ((InterestModeEnum)x.InterestMode).ToString())), + 历史互换事件数 = swapEvents.Count(s => s.SwapTradeId == g.Key), + 历史利息合计 = swapEvents.Where(s => s.SwapTradeId == g.Key).Sum(s => s.InterestAmount), + EodCount = db.eod_swap_position.Count(e => e.SwapTradeId == g.Key) + }) + .Where(t => t.EodCount > 0) + .OrderByDescending(t => Math.Abs(t.历史利息合计)) + .Take(20) + .ToList(); + + Console.WriteLine($"{"TradeId",8} {"单利腿",6} {"历史互换",8} {"历史利息合计",16} {"eod",6} {"利息模式",-20}"); + foreach (var t in byTrade) + { + Console.WriteLine($"{t.SwapTradeId,8} {t.单利腿数,6} {t.历史互换事件数,8} {t.历史利息合计,16:F4} {t.EodCount,6} {t.利息模式,-20}"); + } + + if (byTrade.Count == 0) + { + Assert.Inconclusive("无单利+有互换历史的样本。"); + } + Assert.IsTrue(byTrade.Count > 0); + } + finally + { + db?.Dispose(); + } + } + + /// + /// Step1_SimpleInterestRedTest:单利路径红灯测试。 + /// + /// 复利路径已由 c6adb3bb 修复(consumedInterest 扣除),但单利路径(CalcDailySimpleInterest) + /// 未加该扣除。本测试坐实:单利利息腿在"有历史互换结清后再平仓"时,默认值仍偏大。 + /// + /// 红灯(当前):默认值包含历史已结利息(consumedInterest),偏大 + /// 绿灯(修复后):单利路径也扣除 consumedInterest,默认值正确 + /// + [TestMethod] + [TestCategory("DBRecording")] + public void Step1_SimpleInterestRedTest() + { + int tradeId = SimpleInterestSampleTradeId; + + YLContext db; + try { db = DbContextFactory.GetYLDbContext(); } + catch (Exception ex) + { + Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); + return; + } + + try + { + Console.WriteLine($"===== 单利路径红灯测试 SwapTradeId={tradeId} =====\n"); + + // 1. 确认该交易的单利利息腿 + var simplePositions = db.swap_position + .Where(x => x.SwapTradeId == tradeId && !x.Invalid + && x.InterestDirection > 0 + && x.InterestType == (int)InterestTypeEnum.单利) + .ToList(); + Console.WriteLine($"[1] 单利利息腿: {simplePositions.Count} 条"); + foreach (var p in simplePositions) + { + Console.WriteLine($" PositionId={p.id} Mode={((InterestModeEnum)p.InterestMode)} PrincipalFix={p.InterestPrincipalFix}"); + } + + // 2. 找最近 eod 日期,作为"模拟平仓日" + var latestEodDate = db.eod_swap_position + .Where(x => x.SwapTradeId == tradeId) + .Max(x => (DateTime?)x.ValueDate); + if (latestEodDate == null) + { + Assert.Inconclusive($"交易 {tradeId} 无 eod 数据"); + return; + } + // 用 eod 后一天作为模拟平仓日 + var testDate = latestEodDate.Value.AddDays(1); + Console.WriteLine($"\n[2] 模拟平仓日: {testDate:yyyy-MM-dd}(eod最近: {latestEodDate:yyyy-MM-dd})"); + + // 3. 调用真实 GetUnwindInterests(与前端平仓页相同路径) + var userInfo = new OptUserInfo(1, "UnitTest", OptUserFrom.UnitTest); + var service = new SwapDealService(userInfo); + var defaults = service.GetUnwindInterests( + testDate, testDate, tradeId, 1m, (int)SwapEventTypeEnum.平仓); + + // 4. 对每个单利腿,对比"默认值"vs"应计基数(待实现-已结利息)" + Console.WriteLine($"\n[3] 单利路径诊断:默认值 vs 应计基数"); + Console.WriteLine($" {"PositionId",10} {"InterestMode",14} {"默认值",14} {"eod待实现IPS",14} {"历史已结CI",14} {"应计(IPS-CI)",14} {"偏大量",14} {"红灯",6}"); + + int redCount = 0; + foreach (var d in defaults.Where(x => x.InterestDirection > 0)) + { + var pos = simplePositions.FirstOrDefault(x => x.id == d.PositionId); + if (pos == null) continue; // 跳过非单利腿 + + // eod 待实现 + var preEod = db.eod_swap_position + .Where(x => x.SwapTradeId == tradeId && x.PositionId == d.PositionId && x.ValueDate < testDate) + .OrderByDescending(x => x.ValueDate).FirstOrDefault(); + decimal ips = preEod?.InterestProfitSum ?? 0; + + // 历史已结利息(复利路径用的 GetConsumedInterest,单利路径没用) + decimal ci = service.GetConsumedInterest(tradeId, d.PositionId, testDate); + + // 应计基数 = 待实现 - 已结(这才是正确的"未实现利息") + decimal expected = ips - ci; + decimal actual = d.InterestClosePnL; + decimal diff = actual - expected; + bool isRed = Math.Abs(ci) > 0.01m && Math.Abs(diff) > Math.Abs(ci) * 0.5m; + + if (isRed) redCount++; + string modeName = ((InterestModeEnum)d.InterestMode).ToString(); + Console.WriteLine($" {d.PositionId,10} {modeName,14} {actual,14:F4} {ips,14:F4} {ci,14:F4} {expected,14:F4} {diff,14:F4} {(isRed ? "⚠红灯" : "绿灯"),6}"); + } + + Console.WriteLine($"\n[结论]"); + if (redCount > 0) + { + Console.WriteLine($" ⚠ 坐实单利路径 bug:{redCount} 条单利腿默认值偏大(含历史已结利息)。"); + Console.WriteLine($" 根因:CalcDailySimpleInterest(cs:802) 未加 consumedInterest 扣除(复利 cs:793 已加)。"); + } + else + { + Console.WriteLine($" 单利路径未检测到偏大(可能已修或样本无历史互换)。"); + } + + // 红灯断言:单利路径应存在偏大 + Assert.IsTrue(redCount > 0, + "红灯:单利路径应存在默认值偏大(含历史已结利息)。修复后此断言应反转。"); + } + finally + { + db?.Dispose(); + } + } + + /// + /// 单利红灯样本交易ID。从 Step0e 选"标的期初全价+单利+有历史互换"的交易。 + /// + private int SimpleInterestSampleTradeId => 1813; + /// /// Step0b:对单笔交易做详细诊断——对比"待实现"vs"已实现"利息,判断默认值是否重复计入。 /// @@ -395,6 +581,13 @@ namespace YLErp.Modules.SwapModule // 5. 模拟"打开平仓页"——分别测 6-29/6-30/7-1 三天,对比默认值变化 Console.WriteLine($"\n[5] 调 GetUnwindInterests 模拟打开平仓页(6-29/6-30/7-1 三天对比)"); + // 先查利息腿的计息类型(单利/复利),判断走哪个修复路径 + var interestPositions = DbContextFactory.GetYLDbContext().swap_position + .Where(x => x.SwapTradeId == tradeId && x.InterestDirection > 0 && !x.Invalid).ToList(); + foreach (var p in interestPositions) + { + Console.WriteLine($" PositionId={p.id} InterestMode={((InterestModeEnum)p.InterestMode)} InterestType={((InterestTypeEnum)p.InterestType)}"); + } var userInfo = new OptUserInfo(1, "UnitTest", OptUserFrom.UnitTest); var service = new SwapDealService(userInfo); var testDates = new[] { diff --git a/UnitTestProject/Modules/SwapModule/SwapReEodDeleteManualCashRecordTest.cs b/UnitTestProject/Modules/SwapModule/SwapReEodDeleteManualCashRecordTest.cs index 8dd3277f..fd5f89de 100644 --- a/UnitTestProject/Modules/SwapModule/SwapReEodDeleteManualCashRecordTest.cs +++ b/UnitTestProject/Modules/SwapModule/SwapReEodDeleteManualCashRecordTest.cs @@ -303,10 +303,15 @@ namespace YLErp.Modules.SwapModule File.WriteAllText(filePath, JsonConvert.SerializeObject(golden, JsonSettings)); Console.WriteLine($"\n✅ golden 已保存: {filePath}"); - // 红灯断言:命中列表必须含手动互换记录(坐实 bug) + // 红灯断言(旧bug逻辑):按 Action 字符串模糊删会命中手动互换记录 Assert.IsTrue(diagnosis.手动互换误删记录数 > 0, - $"红灯:重收盘删除条件应误删 {diagnosis.手动互换误删记录数} 条手动互换资金记录 " + - $"(ids=[{string.Join(",", diagnosis.手动互换误删CashIds)}])。修复后此断言应反转。"); + $"红灯:旧bug删除条件(Action模糊删)会误删 {diagnosis.手动互换误删记录数} 条手动互换资金记录 " + + $"(ids=[{string.Join(",", diagnosis.手动互换误删CashIds)}])。"); + + // 绿灯断言(修复后逻辑):排除 manualClientCashIds 后,手动互换记录不再被命中 + Assert.AreEqual(0, diagnosis.修复后手动误删数, + $"绿灯:修复后逻辑(排除manualClientCashIds)不应再命中手动互换资金记录," + + $"实际仍命中 {diagnosis.修复后手动误删数} 条。"); } } @@ -384,6 +389,30 @@ namespace YLErp.Modules.SwapModule c.id, c.Action, c.Money, c.HappenDate?.ToString("yyyy-MM-dd"), source, misDelete ? "✓BUG" : "")); } + lines.Add(""); + // ===== 修复后逻辑模拟(验证 ClearSwapPositions 新代码不再误删手动记录)===== + // 修复后代码(SwapTradeBaseService.cs:388-421)改为: + // 1. 按 autoSwapEvents.ClientCashId 精准收集 + // 2. GetLegacyAutoSwapClientCashRecords 显式排除 manualClientCashIds(cs:491) + // 模拟这个排除逻辑,看手动记录是否被排除 + lines.Add("[修复后逻辑模拟] 排除 manualClientCashIds 后的命中:"); + var fixedWouldDelete = wouldDelete + .Where(x => !manualCashIds.Contains(x.id)) // 修复后:排除手动互换的资金记录 + .ToList(); + int fixedManualHit = wouldDelete.Count(x => manualCashIds.Contains(x.id)) - fixedWouldDelete.Count(x => manualCashIds.Contains(x.id)); + lines.Add($" 修复前命中手动互换: {r.手动互换误删记录数} 条 (ids=[{string.Join(",", r.手动互换误删CashIds)}])"); + lines.Add($" 修复后命中手动互换: {fixedWouldDelete.Count(x => manualCashIds.Contains(x.id))} 条"); + if (r.手动互换误删记录数 > 0 && fixedWouldDelete.Count(x => manualCashIds.Contains(x.id)) == 0) + { + lines.Add($" ✅ 修复生效:手动互换资金记录被正确排除,不再误删!"); + r.修复后手动误删数 = 0; + } + else + { + lines.Add($" ⚠ 修复未生效或部分生效"); + r.修复后手动误删数 = fixedWouldDelete.Count(x => manualCashIds.Contains(x.id)); + } + lines.Add(""); lines.Add("[结论]"); lines.Add($" 删除命中 {r.删除命中总数} 条 = 手动互换 {r.手动互换误删记录数} + 自动互换 {r.自动互换命中记录数} + 孤儿 {r.孤儿命中记录数}"); @@ -477,6 +506,8 @@ namespace YLErp.Modules.SwapModule public int 手动互换误删记录数 { get; set; } public int 自动互换命中记录数 { get; set; } public int 孤儿命中记录数 { get; set; } + /// 修复后逻辑(排除manualClientCashIds)模拟命中手动记录数,应为0。 + public int 修复后手动误删数 { get; set; } public List 手动互换误删CashIds { get; set; } = new List(); public List 命中明细 { get; set; } = new List(); public bool BUG成立 { get; set; }