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(); } } } }