互换三个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已修)。
This commit is contained in:
hjhan
2026-07-01 12:22:57 +08:00
parent 3b610fe47c
commit 3118c44365
3 changed files with 473 additions and 3 deletions
@@ -0,0 +1,246 @@
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();
}
}
}
}
@@ -99,6 +99,192 @@ namespace YLErp.Modules.SwapModule
}
}
/// <summary>
/// Step0e:探查"单利 + 有互换历史"的样本,用于验证单利路径是否也需要 consumedInterest 扣除。
///
/// 复利路径(c6adb3bb)已修,单利路径(CalcDailySimpleInterest)未修。
/// 需找:单利利息腿 + 该腿有历史互换/自动互换事件(InterestAmount≠0) + 有eod。
/// </summary>
[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();
}
}
/// <summary>
/// Step1_SimpleInterestRedTest:单利路径红灯测试。
///
/// 复利路径已由 c6adb3bb 修复(consumedInterest 扣除),但单利路径(CalcDailySimpleInterest)
/// 未加该扣除。本测试坐实:单利利息腿在"有历史互换结清后再平仓"时,默认值仍偏大。
///
/// 红灯(当前):默认值包含历史已结利息(consumedInterest),偏大
/// 绿灯(修复后):单利路径也扣除 consumedInterest,默认值正确
/// </summary>
[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();
}
}
/// <summary>
/// 单利红灯样本交易ID。从 Step0e 选"标的期初全价+单利+有历史互换"的交易。
/// </summary>
private int SimpleInterestSampleTradeId => 1813;
/// <summary>
/// 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[] {
@@ -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 显式排除 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.孤儿命中记录数}");
@@ -477,6 +506,8 @@ namespace YLErp.Modules.SwapModule
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; }