Files
zszq-trs/UnitTestProject/Modules/SwapModule/SwapApproveStatusStuckTest.cs

247 lines
13 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using YLErp.DBModels;
using YLErp.DBModels.Enums;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// 互换收益结算审核后状态卡死 - TDD 红灯测试
/// ============================================================================
/// 背景:
/// ApproveSwapTrade(cs:1556-1569) 的状态分支只判 CloseMethod==全部平仓,
/// 没有 ExerciseDate 到期判断。互换操作的 CloseMethod 默认 0(Unknown)≠1(全部平仓)
/// 必走 else → TradeStatus 退回"确认成交"。到期互换审核后状态不流转成"已到期",
/// 导致 EodCheckMaturityTrade 一直阻止收盘。
/// 对比:免审核路径 SwapIncome(cs:1512-1517) 有正确的 ExerciseDate 判断。
///
/// TDD 红灯→绿灯:
/// 红灯(当前):存在"到期+确认成交+有互换事件"的卡死交易
/// 绿灯(修复后):审核后状态正确变成"已到期",无卡死交易
/// ============================================================================
[TestClass]
public class SwapApproveStatusStuckTest
{
/// <summary>
/// Step0:探查"卡死"的交易——到期日已过 + TradeStatus=确认成交 + 有互换审核事件。
///
/// 这些交易就是被 ApproveSwapTrade bug 卡住的:审核通过了但状态没变"已到期"。
/// </summary>
[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();
}
}
/// <summary>
/// Step1:验证 ApproveSwapTrade 的状态流转缺陷(代码逻辑坐实,不依赖卡死数据)。
///
/// 核心验证:
/// 1. 统计所有有效互换事件(EventType=互换)的 CloseMethod 分布
/// 2. 若 CloseMethod 普遍≠全部平仓(1),则 ApproveSwapTrade 的 else 分支(cs:1565)必被命中
/// 3. 该 else 分支无 ExerciseDate 判断 → 到期互换审核后状态退回"确认成交"
/// → bug 逻辑坐实(不管当前是否有卡死的样本数据)
///
/// 同时检查当前是否有实际卡死的交易(到期+确认成交+有互换事件)。
/// </summary>
[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<UnwindData>(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();
}
}
/// <summary>
/// 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==互换 能正确区分互换/平仓审核。
/// </summary>
[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.平仓));
// 风险2Notional 清零被覆盖问题
// 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();
}
}
}
}