Files
zszq-trs/UnitTestProject/Modules/SwapModule/SwapReEodDeleteManualCashRecordTest.cs
T
hjhan 3118c44365 互换三个bug的TDD红灯/绿灯测试
单利利息偏大(红灯): 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已修)。
2026-07-01 12:22:57 +08:00

528 lines
28 KiB
C#
Raw 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 Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using YLErp.DBModels;
using YLErp.DBModels.Enums;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// 互换重收盘误删"手动互换"资金记录 - 录制/验证测试(TDD 红灯)
/// ============================================================================
/// 背景:
/// SwapTradeBaseService.ClearSwapPositions 第398-409行按 Action 字符串模糊删除
/// ClientCashInCashOut,而"系统操作-互换"这个 Action 手动互换和自动互换共用,
/// 且 ClientCashInCashOut 无来源标记字段 → 重收盘会把用户手动互换的资金记录一并删掉。
/// 引入点:commit 44e897262026-05-14)把 系统操作_互换 加进删除 Action 列表。
///
/// TDD 红灯→绿灯:
/// 红灯(当前):录制一笔"有手动互换"的交易 → 模拟重收盘 → 断言手动资金记录被删(坐实 bug)
/// 绿灯(修复后):同测试断言通过(手动记录保留,自动互换记录正确清理)
///
/// 运行方式:
/// 全部标 [Ignore]+[TestCategory("DBRecording")],不进 CI。
/// 手动执行:vstest.console.exe UnitTestProject.dll /TestCaseFilter:"TestCategory=DBRecording"
/// ============================================================================
[TestClass]
public class SwapReEodDeleteManualCashRecordTest
{
private static readonly string GoldenDir = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory, "Resources", "GoldenFiles", "SwapReEodDeleteCash");
/// <summary>
/// Step0:探查测试库,列出所有"有手动互换操作"的互换交易,供挑选样本。
///
/// 筛选条件(同时满足才是有效复现样本):
/// 1. swap_event 存在 EventType=互换(3) 且 ClientCashId>0 的记录(手动互换且生成了资金记录)
/// 2. 该 ClientCashId 在 ClientCashInCashOut 中真实存在(未被删)
/// 3. 该交易有 eod_swap 记录(已收盘过,才能"重收盘")
///
/// 连不上测试库时 Inconclusive(CI 无 DB 环境正常跳过)。
/// </summary>
[TestMethod]
[TestCategory("DBRecording")]
public void Step0_ListManualSwapTrades()
{
YLContext db;
try { db = DbContextFactory.GetYLDbContext(); }
catch (Exception ex)
{
Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}");
return;
}
try
{
// 找所有手动互换事件(EventType=互换=3,且关联了资金记录)
var manualEvents = db.swap_event
.Where(x => x.EventType == (int)SwapEventTypeEnum.互换
&& x.ClientCashId > 0
&& !x.Invalid)
.ToList();
Console.WriteLine($"=== 手动互换事件(EventType=3, ClientCashId>0)数: {manualEvents.Count} ===\n");
// 按交易分组,附加资金记录和 eod 信息
var byTrade = manualEvents
.GroupBy(x => x.SwapTradeId)
.Select(g =>
{
var cashIds = g.Select(x => x.ClientCashId).Distinct().ToList();
var cashRecords = db.ClientCashInCashOut
.Where(c => cashIds.Contains(c.id)).ToList();
var eodCount = db.eod_swap.Count(e => e.SwapTradeId == g.Key);
var autoEvents = db.swap_event.Count(x => x.SwapTradeId == g.Key
&& x.EventType == (int)SwapEventTypeEnum.自动互换 && !x.Invalid);
return new
{
SwapTradeId = g.Key,
ManualEventCount = g.Count(),
CashIds = cashIds,
CashRecordsFound = cashRecords.Count,
CashAction = cashRecords.Select(c => c.Action).Distinct().ToList(),
CashAmounts = cashRecords.Select(c => c.Money).ToList(),
CashHappenDates = cashRecords.Select(c => c.HappenDate).ToList(),
EodSwapCount = eodCount,
AutoSwapEventCount = autoEvents,
// 关键:事件日期范围(重收盘 valueDate <= 此日期会触发删除)
MinEventDate = g.Min(x => x.ValueDate),
MaxEventDate = g.Max(x => x.ValueDate)
};
})
.OrderByDescending(t => t.EodSwapCount > 0) // 优先有eod的(可重收盘)
.ThenByDescending(t => t.ManualEventCount)
.ToList();
Console.WriteLine($"{"TradeId",8} {"手动事件",8} {"资金记录",8} {"Action",-20} {"金额",14} {"eod",6} {"自动互换",8} {"事件日期范围",-24} {"可复现",6}");
int reproducible = 0;
foreach (var t in byTrade.Take(30))
{
bool canReproduce = t.EodSwapCount > 0 && t.CashRecordsFound > 0;
if (canReproduce) reproducible++;
string actionStr = string.Join("|", t.CashAction);
string amountStr = t.CashAmounts.Any() ? string.Join("|", t.CashAmounts.Select(m => $"{m:F2}")) : "-";
string dateRange = $"{t.MinEventDate:yyyy-MM-dd}~{t.MaxEventDate:yyyy-MM-dd}";
Console.WriteLine($"{t.SwapTradeId,8} {t.ManualEventCount,8} {t.CashRecordsFound,8} {actionStr,-20} {amountStr,14} {t.EodSwapCount,6} {t.AutoSwapEventCount,8} {dateRange,-24} {(canReproduce ? "" : ""),6}");
}
Console.WriteLine($"\n=== 可复现样本数(有eod+有资金记录): {reproducible} ===");
if (reproducible == 0)
{
Assert.Inconclusive("无可复现样本(需要有 eod + 手动互换资金记录的交易)。请先在测试库构造数据。");
}
Assert.IsTrue(reproducible > 0, "应存在可复现样本");
}
finally
{
db?.Dispose();
}
}
/// <summary>
/// Step0b:对单个候选样本做详细诊断,确认"重收盘删除条件"确实会命中手动互换资金记录。
///
/// 核心验证(不改任何数据,纯查询):模拟 ClearSwapPositions 第400-404行的删除条件,
/// 看会命中哪些 ClientCashInCashOut 记录,逐条标注它是"手动互换"还是"自动互换"产生的。
/// 如果命中列表里有手动互换的记录 → bug 坐实(红灯前置证据)。
/// </summary>
[TestMethod]
[TestCategory("DBRecording")]
public void Step0b_DiagnoseSingleTradeDeleteCondition()
{
// 候选样本(从 Step0 输出中挑选):1903=最新,有自动互换,结构完整
int tradeId = SampleTradeId;
YLContext db;
try { db = DbContextFactory.GetYLDbContext(); }
catch (Exception ex)
{
Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}");
return;
}
try
{
Console.WriteLine($"===== 诊断 SwapTradeId={tradeId} 的资金记录删除命中情况 =====\n");
// 1. 该交易全部资金记录(互换/预付金相关)
var allCashRecords = db.ClientCashInCashOut
.Where(x => x.TradeId == tradeId
&& (x.Action == ClientCashInCashOut.系统操作_互换
|| x.Action == ClientCashInCashOut.系统操作_预付金返息))
.OrderBy(x => x.HappenDate).ThenBy(x => x.id)
.ToList();
Console.WriteLine($"[1] 该交易全部互换/预付金资金记录: {allCashRecords.Count} 条");
foreach (var c in allCashRecords)
{
Console.WriteLine($" id={c.id} Action={c.Action} Money={c.Money:F2} HappenDate={c.HappenDate:yyyy-MM-dd} OptName={c.OptName} CreateDate={c.CreateDate:yyyy-MM-dd HH:mm}");
}
// 2. 该交易全部 swap_event(区分手动互换 vs 自动互换)
var allEvents = db.swap_event
.Where(x => x.SwapTradeId == tradeId && !x.Invalid)
.OrderBy(x => x.ValueDate).ThenBy(x => x.id)
.ToList();
Console.WriteLine($"\n[2] 该交易全部 swap_event: {allEvents.Count} 条");
foreach (var e in allEvents)
{
string etName = ((SwapEventTypeEnum)e.EventType).ToString();
Console.WriteLine($" id={e.id} EventType={e.EventType}({etName}) ValueDate={e.ValueDate:yyyy-MM-dd} ClientCashId={e.ClientCashId}");
}
// 3. 关键:找出"自动互换"事件,确定重收盘的删除起点 valueDate
var autoEvents = allEvents.Where(x => x.EventType == (int)SwapEventTypeEnum.自动互换).ToList();
if (autoEvents.Count == 0)
{
Console.WriteLine($"\n⚠ 该交易无自动互换事件,重收盘不会触发 ClearSwapPositions 的资金删除逻辑。");
Console.WriteLine($" 改用 SwapPositionCompose 的合成持仓路径(delAfter=false)也不删资金。");
Console.WriteLine($" → 此样本不适合复现,需选有自动互换事件的样本。");
Assert.Inconclusive("此样本无自动互换事件,请换一个有自动互换的交易。");
return;
}
// 重收盘时 valueDate 取自动互换事件的最小 ValueDate(重收盘从该日起重算)
var minAutoDate = autoEvents.Min(x => x.ValueDate);
Console.WriteLine($"\n[3] 自动互换事件 {autoEvents.Count} 条,最早 ValueDate={minAutoDate:yyyy-MM-dd}(重收盘 valueDate 起点)");
// 4. 模拟 ClearSwapPositions 第399-404行的删除条件
var swapTradeIds = new List<int> { tradeId };
var actions = new List<string> { ClientCashInCashOut.系统操作_预付金返息, ClientCashInCashOut.系统操作_互换 };
var wouldDelete = allCashRecords
.Where(x => x.HappenDate >= minAutoDate && actions.Contains(x.Action))
.ToList();
Console.WriteLine($"\n[4] ⚠ 模拟删除条件(HappenDate>={minAutoDate:yyyy-MM-dd} AND Action IN 互换/预付金返息)会命中: {wouldDelete.Count} 条");
// 5. 逐条标注命中记录的来源(手动 vs 自动)
var manualCashIds = allEvents
.Where(x => x.EventType == (int)SwapEventTypeEnum.互换 && x.ClientCashId > 0)
.Select(x => x.ClientCashId).ToHashSet();
var autoCashIds = allEvents
.Where(x => x.EventType == (int)SwapEventTypeEnum.自动互换 && x.ClientCashId > 0)
.Select(x => x.ClientCashId).ToHashSet();
int manualHit = 0, autoHit = 0, unknownHit = 0;
Console.WriteLine($" {"id",8} {"Action",-20} {"Money",12} {"HappenDate",-12} {"来源",10} {"⚠误删",6}");
foreach (var c in wouldDelete)
{
string source;
bool misDelete = false;
if (manualCashIds.Contains(c.id)) { source = "手动互换"; misDelete = true; manualHit++; }
else if (autoCashIds.Contains(c.id)) { source = "自动互换"; autoHit++; }
else { source = "未知(孤儿)"; unknownHit++; }
Console.WriteLine($" {c.id,8} {c.Action,-20} {c.Money,12:F2} {c.HappenDate?.ToString("yyyy-MM-dd"),-12} {source,-10} {(misDelete ? "BUG" : ""),6}");
}
Console.WriteLine($"\n[结论] 删除命中 {wouldDelete.Count} 条 = 手动互换 {manualHit} + 自动互换 {autoHit} + 未知 {unknownHit}");
if (manualHit > 0)
{
Console.WriteLine($"⚠⚠⚠ 坐实 BUG:重收盘会误删 {manualHit} 条手动互换资金记录!");
}
Console.WriteLine($"\n(以上为纯查询诊断,未修改任何数据)");
Assert.IsTrue(wouldDelete.Count > 0, "删除条件应至少命中1条");
}
finally
{
db?.Dispose();
}
}
/// <summary>
/// 候选样本交易ID。从 Step0 输出中选有自动互换事件 + 有手动互换资金记录的交易。
/// </summary>
private int SampleTradeId => 1903;
/// <summary>
/// Step1:录制样本交易快照 + 模拟删除条件,把"会被误删的手动互换资金记录"固化为 golden。
///
/// 这是 TDD 红灯的核心产物:
/// - 录制该交易的 swap_event + ClientCashInCashOut 完整快照
/// - 模拟 ClearSwapPositions:400-404 的删除条件,算出命中列表
/// - 标注每条命中记录的来源(手动互换/自动互换/孤儿)
/// - 断言"命中列表含手动互换记录" → 当前成立(红灯,坐实 bug)
///
/// 修复后(绿灯):命中列表应只含自动互换记录,手动互换记录不在内 → 断言失败需更新 golden。
///
/// 为何不直接调 SwapPositionCompose
/// 那会真删测试库数据且难恢复。录制+模拟条件能等价坐实 bug,又不破坏数据。
/// </summary>
[TestMethod]
[TestCategory("DBRecording")]
// [Ignore] // 有写文件副作用,手动跑时取消注释
public void Step1_RecordAndDiagnoseDeleteBug()
{
int tradeId = SampleTradeId;
YLContext db;
try { db = DbContextFactory.GetYLDbContext(); }
catch (Exception ex)
{
Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}");
return;
}
using (db)
{
Directory.CreateDirectory(GoldenDir);
Console.WriteLine($"\n========== 录制 SwapTradeId={tradeId}(重收盘误删手动资金记录)==========\n");
// 1. 交易主信息
var trade = db.trade.FirstOrDefault(t => t.id == tradeId);
Assert.IsNotNull(trade, $"trade {tradeId} 不存在");
// 2. swap_event 快照(区分手动互换 vs 自动互换)
var allEvents = db.swap_event
.Where(x => x.SwapTradeId == tradeId && !x.Invalid)
.OrderBy(x => x.ValueDate).ThenBy(x => x.id)
.ToList();
// 3. ClientCashInCashOut 快照(互换/预付金相关,bug 影响范围)
var allCashRecords = db.ClientCashInCashOut
.Where(x => x.TradeId == tradeId
&& (x.Action == ClientCashInCashOut.系统操作_互换
|| x.Action == ClientCashInCashOut.系统操作_预付金返息))
.OrderBy(x => x.HappenDate).ThenBy(x => x.id)
.ToList();
// 4. 诊断:模拟删除条件,算出命中列表 + 来源标注
var diagnosis = DiagnoseDeleteImpact(tradeId, allEvents, allCashRecords);
Console.WriteLine(diagnosis.Summary);
// 5. 序列化 golden
var golden = new ReEodDeleteCashGoldenModel
{
SwapTradeId = tradeId,
SwapTradeNo = trade.TradeNumber,
RecordedAt = DateTime.Now,
SourceDb = "test",
Purpose = "重收盘误删手动互换资金记录 - TDD红灯证据",
InputEvents = JArray.FromObject(allEvents, JsonSerializer.Create(JsonSettings)),
InputCashRecords = JArray.FromObject(allCashRecords, JsonSerializer.Create(JsonSettings)),
Diagnosis = JObject.FromObject(diagnosis, JsonSerializer.Create(JsonSettings))
};
string filePath = Path.Combine(GoldenDir, $"reeod_delete_trade_{tradeId}.json");
File.WriteAllText(filePath, JsonConvert.SerializeObject(golden, JsonSettings));
Console.WriteLine($"\n✅ golden 已保存: {filePath}");
// 红灯断言(旧bug逻辑):按 Action 字符串模糊删会命中手动互换记录
Assert.IsTrue(diagnosis.手动互换误删记录数 > 0,
$"红灯:旧bug删除条件(Action模糊删)会误删 {diagnosis.手动互换误删记录数} 条手动互换资金记录 " +
$"(ids=[{string.Join(",", diagnosis.手动互换误删CashIds)}])。");
// 绿灯断言(修复后逻辑):排除 manualClientCashIds 后,手动互换记录不再被命中
Assert.AreEqual(0, diagnosis.修复后手动误删数,
$"绿灯:修复后逻辑(排除manualClientCashIds)不应再命中手动互换资金记录," +
$"实际仍命中 {diagnosis.修复后手动误删数} 条。");
}
}
/// <summary>
/// 模拟 ClearSwapPositions:400-404 的删除条件,诊断命中情况。
/// </summary>
private DeleteDiagnoseResult DiagnoseDeleteImpact(
int tradeId,
List<swap_event> allEvents,
List<ClientCashInCashOut> allCashRecords)
{
var r = new DeleteDiagnoseResult { SwapTradeId = tradeId };
var lines = new List<string>
{
$"--- 重收盘误删诊断 SwapTradeId={tradeId} ---",
"",
"[swap_event] 手动互换 vs 自动互换:"
};
foreach (var e in allEvents)
{
if (e.EventType == (int)SwapEventTypeEnum.互换 || e.EventType == (int)SwapEventTypeEnum.自动互换)
{
lines.Add($" event id={e.id} EventType={((SwapEventTypeEnum)e.EventType).ToString()} " +
$"ValueDate={e.ValueDate:yyyy-MM-dd} ClientCashId={e.ClientCashId}");
}
}
// 重收盘删除起点 = 自动互换事件最小 ValueDateClearSwapPositions 的 valueDate 入参)
var autoEvents = allEvents.Where(x => x.EventType == (int)SwapEventTypeEnum.自动互换).ToList();
r.自动互换事件数 = autoEvents.Count;
if (autoEvents.Count == 0)
{
r.Summary = string.Join("\n", lines) + "\n\n⚠ 无自动互换事件,删除逻辑不触发。";
return r;
}
var minAutoDate = autoEvents.Min(x => x.ValueDate);
r.删除起点ValueDate = minAutoDate;
// 来源标注:按 swap_event.ClientCashId 反查
var manualCashIds = allEvents
.Where(x => x.EventType == (int)SwapEventTypeEnum.互换 && x.ClientCashId > 0)
.Select(x => x.ClientCashId).ToHashSet();
var autoCashIds = autoEvents
.Where(x => x.ClientCashId > 0)
.Select(x => x.ClientCashId).ToHashSet();
lines.Add("");
lines.Add($"[删除条件模拟] valueDate={minAutoDate:yyyy-MM-dd} Action IN (系统操作-互换, 系统操作-预付金返息)");
lines.Add(string.Format(" {0,-8}{1,-22}{2,12}{3,-12}{4,-10}{5,-8}", "id", "Action", "Money", "HappenDate", "来源", "误删?"));
// 模拟 ClearSwapPositions:400-404 Where 条件
var actions = new List<string> { ClientCashInCashOut.系统操作_预付金返息, ClientCashInCashOut.系统操作_互换 };
var wouldDelete = allCashRecords
.Where(x => x.HappenDate >= minAutoDate && actions.Contains(x.Action))
.ToList();
r.删除命中总数 = wouldDelete.Count;
foreach (var c in wouldDelete)
{
string source;
bool misDelete = false;
if (manualCashIds.Contains(c.id)) { source = "手动互换"; misDelete = true; r.手动互换误删记录数++; r.手动互换误删CashIds.Add(c.id); }
else if (autoCashIds.Contains(c.id)) { source = "自动互换"; r.自动互换命中记录数++; }
else { source = "未知(孤儿)"; r.孤儿命中记录数++; }
r.命中明细.Add(new DeleteHitItem
{
CashId = c.id, Action = c.Action, Money = c.Money ?? 0,
HappenDate = c.HappenDate, 来源 = source, 会被误删 = misDelete
});
lines.Add(string.Format(" {0,-8}{1,-22}{2,12:F2}{3,-12}{4,-10}{5,-8}",
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 显式排除 manualClientCashIdscs: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.孤儿命中记录数}");
if (r.手动互换误删记录数 > 0)
{
lines.Add($" ⚠⚠⚠ 坐实 BUG:重收盘会误删 {r.手动互换误删记录数} 条手动互换资金记录 (ids=[{string.Join(",", r.手动互换误删CashIds)}])");
lines.Add($" 根因:ClearSwapPositions:400 按 Action=系统操作-互换 删除,该 Action 手动/自动共用,无来源字段区分。");
r.结论 = $"坐实BUG:误删 {r.手动互换误删记录数} 条手动互换资金记录";
r.BUG成立 = true;
}
else
{
lines.Add($" 未检测到误删手动互换记录(可能已修复)。");
r.结论 = "未检测到误删";
r.BUG成立 = false;
}
r.Summary = string.Join("\n", lines);
return r;
}
/// <summary>
/// Step2:离线校验已录制 golden 文件(不连库)。
/// 确认 json 含完整快照 + 诊断结论能正确反序列化。
/// 这是唯一不标 [Ignore] 且能进 CI 的测试(纯读文件,无外部依赖)。
/// </summary>
[TestMethod]
[TestCategory("DBRecording")]
public void Step2_VerifyRecordedGoldenFile()
{
if (!Directory.Exists(GoldenDir))
{
Assert.Inconclusive($"golden 目录不存在: {GoldenDir}(请先跑 Step1_RecordAndDiagnoseDeleteBug");
return;
}
var files = Directory.GetFiles(GoldenDir, "reeod_delete_trade_*.json");
Assert.IsTrue(files.Length > 0, $"应至少有 1 个 golden 文件 in {GoldenDir}");
foreach (var file in files)
{
var json = File.ReadAllText(file);
var golden = JsonConvert.DeserializeObject<ReEodDeleteCashGoldenModel>(json);
Assert.IsTrue(golden.SwapTradeId > 0, $"{file}: SwapTradeId 无效");
Assert.IsNotNull(golden.InputEvents, $"{file}: InputEvents 缺失");
Assert.IsTrue(golden.InputEvents.Count > 0, $"{file}: InputEvents 为空");
Assert.IsNotNull(golden.InputCashRecords, $"{file}: InputCashRecords 缺失");
Assert.IsTrue(golden.InputCashRecords.Count > 0, $"{file}: InputCashRecords 为空");
Assert.IsNotNull(golden.Diagnosis, $"{file}: Diagnosis 缺失");
Console.WriteLine($"✅ {Path.GetFileName(file)}: trade={golden.SwapTradeId}, " +
$"events={golden.InputEvents.Count}条, cash={golden.InputCashRecords.Count}条, " +
$"BUG成立={golden.Diagnosis?["BUG成立"]?.Value<bool>()}, " +
$"结论={golden.Diagnosis?["结论"]?.Value<string>()}");
}
}
private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings
{
Formatting = Formatting.Indented,
NullValueHandling = NullValueHandling.Include,
DateFormatString = "yyyy-MM-ddTHH:mm:ss",
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
};
}
/// <summary>
/// 重收盘误删 golden 模型:swap_event + ClientCashInCashOut 快照 + 删除命中诊断。
/// </summary>
public class ReEodDeleteCashGoldenModel
{
public int SwapTradeId { get; set; }
public string SwapTradeNo { get; set; }
public DateTime RecordedAt { get; set; }
public string SourceDb { get; set; }
public string Purpose { get; set; }
public JArray InputEvents { get; set; } // swap_event
public JArray InputCashRecords { get; set; } // ClientCashInCashOut
public JObject Diagnosis { get; set; }
}
/// <summary>
/// 重收盘删除命中诊断结果。
/// </summary>
public class DeleteDiagnoseResult
{
public int SwapTradeId { get; set; }
public int 自动互换事件数 { get; set; }
public DateTime 删除起点ValueDate { get; set; }
public int 删除命中总数 { get; set; }
public int 手动互换误删记录数 { get; set; }
public int 自动互换命中记录数 { get; set; }
public int 孤儿命中记录数 { get; set; }
/// <summary>修复后逻辑(排除manualClientCashIds)模拟命中手动记录数,应为0。</summary>
public int 修复后手动误删数 { get; set; }
public List<long> 手动互换误删CashIds { get; set; } = new List<long>();
public List<DeleteHitItem> 命中明细 { get; set; } = new List<DeleteHitItem>();
public bool BUG成立 { get; set; }
public string 结论 { get; set; }
public string Summary { get; set; }
}
public class DeleteHitItem
{
public long CashId { get; set; }
public string Action { get; set; }
public double Money { get; set; }
public DateTime? HappenDate { get; set; }
public string 来源 { get; set; }
public bool 会被误删 { get; set; }
}
}