Files
zszq-trs/UnitTestProject/Modules/SwapModule/SwapPartialUnwindInterestDefaultTest.cs
T
hjhan cd03fc53fb 互换两个独立bug分析文档+诊断测试
Bug①利息默认值偏大: 互换结清后eod的InterestProfitSum(待实现)没归零
(=RealizedInterest=77.26), 导致后续平仓默认值=77.26(本该0)+增量→偏大。
6-29收盘后三天对比坐实: 6-30默认83.70, 7-1默认90.14, 均含已实现的77.26。
根因 eod公式cs:829 对互换场景扣减失效(TdInterestIncome≈TdCloseInterest抵消)。

Bug②收益结算审核后状态卡死: ApproveSwapTrade(cs:1525)漏ExerciseDate判断,
到期互换审核后状态退回'确认成交'(CloseMethod默认0≠全部平仓1),
导致EodCheckMaturityTrade一直阻止收盘。引入点083848fb(2025-05-28)。

- 新增利息偏大分析文档(根因/修复方案A扣RealizedInterest/B eod归零)
- 新增审核状态卡死分析文档(根因/修复补ExerciseDate判断)
- 新增SwapPartialUnwindInterestDefaultTest(Step0探查/Step0b诊断/Step0d三天对比)
2026-07-01 09:57:25 +08:00

455 lines
25 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 红灯)
/// ============================================================================
/// 背景:
/// 昨天收益结算(互换)→收盘→今天平仓,"预付金平仓盈亏"和"利息端平仓盈亏"
/// 默认值偏大。根因:CalcDailySimpleInterest(cs:771) 从 PosiStartDate 全程重算利息,
/// 只读 InterestProfitSum(待实现),不读 RealizedInterest(已实现),导致跨天重复计入。
/// 同日去重(cs:435) 只覆盖当天、算尾跳过,跨天不生效。
///
/// TDD 红灯→绿灯:
/// 红灯(当前):找一笔多次操作的交易 → 模拟默认值计算 → 断言默认值 > 应计基数(待实现-已实现)
/// 绿灯(修复后):默认值 ≤ 应计基数
///
/// 运行方式:全部 [Ignore]+[TestCategory("DBRecording")],不进 CI。
/// ============================================================================
[TestClass]
public class SwapPartialUnwindInterestDefaultTest
{
private static readonly string GoldenDir = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory, "Resources", "GoldenFiles", "SwapPartialUnwindInterest");
private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings
{
Formatting = Formatting.Indented,
NullValueHandling = NullValueHandling.Include,
DateFormatString = "yyyy-MM-ddTHH:mm:ss",
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
};
/// <summary>
/// Step0:探查测试库,列出有"多次平仓/互换操作"的互换交易,供挑选样本。
///
/// 复现条件:一笔交易 swap_flow_event 里 EventType IN(平仓,互换,自动互换) 且 DataState=完成
/// 的记录 ≥ 2 条(说明做过多次操作),且有 eod_swap_position(已收盘)。
/// </summary>
[TestMethod]
[TestCategory("DBRecording")]
public void Step0_ListMultiOperationTrades()
{
YLContext db;
try { db = DbContextFactory.GetYLDbContext(); }
catch (Exception ex)
{
Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}");
return;
}
try
{
// 找有多次操作的交易
var multiOpTrades = db.swap_flow_event
.Where(x => (x.EventType == (int)SwapFlowEventTypeEnum.平仓
|| x.EventType == (int)SwapFlowEventTypeEnum.互换
|| x.EventType == (int)SwapFlowEventTypeEnum.自动互换)
&& x.DataState == (int)SwapFlowDateStateEnum.完成)
.AsEnumerable()
.GroupBy(x => x.SwapTradeId)
.Where(g => g.Count() >= 2)
.Select(g => new
{
SwapTradeId = g.Key,
操作次数 = g.Count(),
平仓次数 = g.Count(x => x.EventType == (int)SwapFlowEventTypeEnum.平仓),
互换次数 = g.Count(x => x.EventType == (int)SwapFlowEventTypeEnum.互换 || x.EventType == (int)SwapFlowEventTypeEnum.自动互换),
最早操作日 = g.Min(x => x.EventDate),
最晚操作日 = g.Max(x => x.EventDate),
利息盈亏合计 = g.Sum(x => x.InterestClosePnL),
EodCount = db.eod_swap_position.Count(e => e.SwapTradeId == g.Key)
})
.Where(t => t.EodCount > 0)
.OrderByDescending(t => t.操作次数)
.Take(30)
.ToList();
Console.WriteLine($"=== 多次操作的互换交易数: {multiOpTrades.Count} ===\n");
Console.WriteLine($"{"TradeId",8} {"操作",6} {"平仓",6} {"互换",6} {"eod",6} {"利息盈亏合计",16} {"操作日期范围",-24}");
foreach (var t in multiOpTrades)
{
string dateRange = $"{t.最早操作日:yyyy-MM-dd}~{t.最晚操作日:yyyy-MM-dd}";
Console.WriteLine($"{t.SwapTradeId,8} {t.操作次数,6} {t.平仓次数,6} {t.互换次数,6} {t.EodCount,6} {t.利息盈亏合计,16:F2} {dateRange,-24}");
}
if (multiOpTrades.Count == 0)
{
Assert.Inconclusive("无多次操作的样本(需有≥2次平仓/互换且有eod的交易)。");
}
Assert.IsTrue(multiOpTrades.Count > 0);
}
finally
{
db?.Dispose();
}
}
/// <summary>
/// Step0b:对单笔交易做详细诊断——对比"待实现"vs"已实现"利息,判断默认值是否重复计入。
///
/// 核心逻辑(不改数据,纯查询):
/// - 默认值计算读 InterestProfitSum(待实现),不读 RealizedInterest(已实现)
/// - 若某持仓 InterestProfitSum >> 0 且已有多次操作(RealizedInterest >> 0),
/// 说明下次平仓默认值会基于"全程待实现"重算,重复计入已实现部分
/// - 真正应计基数 = InterestProfitSum - RealizedInterest(剩余未实现)
/// </summary>
[TestMethod]
[TestCategory("DBRecording")]
public void Step0b_DiagnoseSingleTradeInterestDuplication()
{
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. 该交易的利息腿(InterestDirection>0)最新 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;
}
var interestEods = db.eod_swap_position
.Where(x => x.SwapTradeId == tradeId
&& x.ValueDate == latestEodDate
&& x.InterestDirection > 0)
.OrderBy(x => x.PositionId)
.ToList();
Console.WriteLine($"[1] 最新eod({latestEodDate:yyyy-MM-dd})利息腿持仓: {interestEods.Count} 条\n");
Console.WriteLine($"{"PositionId",12} {"InterestMode",12} {"待实现InterestProfitSum",22} {"已实现RealizedInterest",22} {"应计基数(-)",18} {"重复风险",10}");
int riskCount = 0;
foreach (var e in interestEods)
{
decimal base_ = e.InterestProfitSum - e.RealizedInterest;
bool risk = e.InterestProfitSum != 0 && e.RealizedInterest != 0
&& Math.Abs(e.InterestProfitSum) > Math.Abs(base_);
if (risk) riskCount++;
string modeName = ((InterestModeEnum)(e.InterestMode)).ToString();
Console.WriteLine($"{e.PositionId,12} {modeName,12} {e.InterestProfitSum,22:F4} {e.RealizedInterest,22:F4} {base_,18:F4} {(risk ? "⚠有" : ""),10}");
}
// 2. 历史操作记录(看每次利息盈亏)
var history = db.swap_flow_event
.Where(x => x.SwapTradeId == tradeId
&& x.DataState == (int)SwapFlowDateStateEnum.完成
&& (x.EventType == (int)SwapFlowEventTypeEnum.平仓
|| x.EventType == (int)SwapFlowEventTypeEnum.互换
|| x.EventType == (int)SwapFlowEventTypeEnum.自动互换))
.OrderBy(x => x.EventDate).ThenBy(x => x.id)
.ToList();
Console.WriteLine($"\n[2] 历史操作记录: {history.Count} 条\n");
Console.WriteLine($"{"id",8} {"EventDate",12} {"EventType",10} {"PositionId",12} {"InterestClosePnL",18} {"InterestAmount",16}");
foreach (var h in history)
{
string etName = ((SwapFlowEventTypeEnum)h.EventType).ToString();
Console.WriteLine($"{h.id,8} {h.EventDate:yyyy-MM-dd} {etName,10} {h.PositionId,12} {h.InterestClosePnL,18:F4} {h.InterestAmount,16:F4}");
}
// 3. 诊断结论
Console.WriteLine($"\n[结论]");
if (riskCount > 0)
{
Console.WriteLine($"⚠ 有 {riskCount} 条利息腿存在重复计入风险:");
Console.WriteLine($" InterestProfitSum(待实现) 被用作下次平仓默认值计算基数(cs:774),");
Console.WriteLine($" 但它没有扣除 RealizedInterest(已实现)。");
Console.WriteLine($" → 部分平仓后再平仓,默认值会偏大(含已实现部分)。");
}
else
{
Console.WriteLine($" 未检测到重复计入风险(可能 InterestProfitSum 或 RealizedInterest 为0)。");
}
Assert.IsTrue(interestEods.Count > 0, "应有利息腿持仓");
}
finally
{
db?.Dispose();
}
}
/// <summary>
/// 样本交易ID。1889 = GLMS-20260616-000429号收益结算+收盘,30号平仓。
/// </summary>
private int SampleTradeId => 1889;
/// <summary>
/// Step0c:精确诊断——调用真实的 GetUnwindInterests 拿默认值,对比 eod 应计,定位偏差。
///
/// 这是最直接的验证:用平仓日的参数调 GetUnwindInterests(与前端拿默认值完全相同的路径),
/// 看返回的 InterestClosePnL 是否包含了"之前已通过互换实现的部分"。
/// </summary>
[TestMethod]
[TestCategory("DBRecording")]
public void Step0c_VerifyDefaultViaRealService()
{
int tradeId = SampleTradeId;
YLContext db;
try { db = DbContextFactory.GetYLDbContext(); }
catch (Exception ex)
{
Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}");
return;
}
try
{
// 找最后一次平仓事件,用它的参数模拟"打开平仓页"
var lastClose = db.swap_flow_event
.Where(x => x.SwapTradeId == tradeId
&& x.EventType == (int)SwapFlowEventTypeEnum.平仓
&& x.DataState == (int)SwapFlowDateStateEnum.完成)
.OrderByDescending(x => x.EventDate)
.FirstOrDefault();
if (lastClose == null)
{
Assert.Inconclusive($"交易 {tradeId} 无平仓记录");
return;
}
Console.WriteLine($"===== 调用 GetUnwindInterests 验证 SwapTradeId={tradeId} =====");
Console.WriteLine($"模拟平仓日: EventDate={lastClose.EventDate:yyyy-MM-dd} UnwindDate={lastClose.UnwindDate:yyyy-MM-dd}\n");
// 该交易平仓前的最近 eod(用于对比)
var preEodDate = db.eod_swap_position
.Where(x => x.SwapTradeId == tradeId && x.ValueDate < lastClose.UnwindDate)
.Max(x => (DateTime?)x.ValueDate);
var preEodInterests = db.eod_swap_position
.Where(x => x.SwapTradeId == tradeId && x.ValueDate == preEodDate && x.InterestDirection > 0)
.ToList();
Console.WriteLine($"[平仓前最近eod: {preEodDate:yyyy-MM-dd}]");
Console.WriteLine($"{"PositionId",12} {"InterestProfitSum(待实现起点)",28} {"RealizedInterest(已实现)",24}");
foreach (var e in preEodInterests)
{
Console.WriteLine($"{e.PositionId,12} {e.InterestProfitSum,28:F4} {e.RealizedInterest,24:F4}");
}
// 调用真实服务(与前端 GetUnwindInterestList 完全相同的路径)
var userInfo = new OptUserInfo(1, "UnitTest", OptUserFrom.UnitTest);
var service = new SwapDealService(userInfo);
// closePercent 取实际平仓的(从历史 flow_event 推断:InterestPrincipal / PosiNotionalValue
decimal closePercent = 1m; // 先用全平测试
var defaults = service.GetUnwindInterests(
lastClose.EventDate, lastClose.UnwindDate.Value, tradeId, closePercent,
(int)SwapEventTypeEnum.平仓);
Console.WriteLine($"\n[GetUnwindInterests 返回的默认值] closePercent={closePercent}");
Console.WriteLine($"{"PositionId",12} {"InterestMode",12} {"默认InterestClosePnL",22} {"默认InterestAmount",20} {"实际历史InterestClosePnL",24}");
foreach (var d in defaults.Where(x => x.InterestDirection > 0))
{
var hist = db.swap_flow_event.FirstOrDefault(x => x.SwapTradeId == tradeId
&& x.PositionId == d.PositionId && x.id == lastClose.id);
string modeName = ((InterestModeEnum)d.InterestMode).ToString();
Console.WriteLine($"{d.PositionId,12} {modeName,12} {d.InterestClosePnL,22:F4} {d.InterestAmount,20:F4} {hist?.InterestClosePnL ?? 0,24:F4}");
}
// 诊断:默认值 vs 历史实际值 的差异
Console.WriteLine($"\n[诊断]");
bool hasDiscrepancy = false;
foreach (var d in defaults.Where(x => x.InterestDirection > 0))
{
var hist = db.swap_flow_event.FirstOrDefault(x => x.SwapTradeId == tradeId
&& x.PositionId == d.PositionId && x.id == lastClose.id);
if (hist != null && Math.Abs(d.InterestClosePnL - hist.InterestClosePnL) > 0.01m)
{
Console.WriteLine($" PositionId={d.PositionId}: 默认值={d.InterestClosePnL:F4} vs 历史={hist.InterestClosePnL:F4} 差异={d.InterestClosePnL - hist.InterestClosePnL:F4}");
hasDiscrepancy = true;
}
}
if (hasDiscrepancy)
{
Console.WriteLine($" ⚠ 默认值与历史实际值有差异(可能是重算口径变化或bug)");
}
else
{
Console.WriteLine($" 默认值与历史实际值一致(该样本未复现偏差)");
}
Assert.IsTrue(defaults.Count > 0, "应返回利息腿默认值");
}
finally
{
db?.Dispose();
}
}
/// <summary>
/// Step0d:针对 1889GLMS-20260616-0004)的全面诊断。
///
/// 场景:29号收益结算(互换)+收盘 → 30号平仓。
/// 测试环境会不断回退复用同一笔交易,需甄别。
///
/// 本方法一次性查清:
/// 1. swap_event 全历史(含回退 EventType=5),甄别哪些是回退后的有效操作
/// 2. swap_flow_event 全历史(含 DataState≠完成的废弃事件)
/// 3. eod_swap_position 按日期序列,看 InterestProfitSum/RealizedInterest 逐日演变
/// 4. 调 GetUnwindInterests 拿30号平仓默认值,对比29号互换已实现的部分
/// </summary>
[TestMethod]
[TestCategory("DBRecording")]
public void Step0d_DiagnoseTrade1889_FullTimeline()
{
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. swap_event 全历史(含回退/删除)
var allEvents = db.swap_event
.Where(x => x.SwapTradeId == tradeId)
.OrderBy(x => x.id)
.ToList();
Console.WriteLine($"[1] swap_event 全历史: {allEvents.Count} 条(甄别回退)");
Console.WriteLine($" 仅显示 Invalid=False(有效)的事件:");
var validEvents = allEvents.Where(x => !x.Invalid).ToList();
Console.WriteLine($" {"id",8} {"EventType",10} {"ValueDate",12} {"ClientCashId",12} {"EventReason",-20}");
foreach (var e in validEvents)
{
string etName = ((SwapEventTypeEnum)e.EventType).ToString();
Console.WriteLine($" {e.id,8} {etName,10} {e.ValueDate:yyyy-MM-dd} {e.ClientCashId,12} {(e.EventReason ?? ""),-20}");
}
Console.WriteLine($" (另有 {allEvents.Count(x => x.Invalid)} 条 Invalid=True 的回退/历史事件,已隐藏)");
// 2. swap_flow_event 全历史(仅完成状态,过滤废弃)
var allFlowEvents = db.swap_flow_event
.Where(x => x.SwapTradeId == tradeId)
.OrderBy(x => x.id)
.ToList();
var validFlowEventsAll = allFlowEvents.Where(x => x.DataState == (int)SwapFlowDateStateEnum.完成).ToList();
Console.WriteLine($"\n[2] swap_flow_event 完成状态: {validFlowEventsAll.Count} 条(共{allFlowEvents.Count}条,已隐藏{allFlowEvents.Count - validFlowEventsAll.Count}条废弃)");
Console.WriteLine($" {"id",8} {"EventDate",12} {"UnwindDate",12} {"EventType",10} {"PositionId",10} {"InterestClosePnL",18} {"InterestAmount",16} {"MarkClosePnl",14}");
foreach (var f in validFlowEventsAll)
{
string etName = ((SwapFlowEventTypeEnum)f.EventType).ToString();
Console.WriteLine($" {f.id,8} {f.EventDate:yyyy-MM-dd} {f.UnwindDate?.ToString("yyyy-MM-dd") ?? "-",-12} {etName,10} {f.PositionId,10} {f.InterestClosePnL,18:F4} {f.InterestAmount,16:F4} {f.MarkClosePnl,14:F4}");
}
// 3. eod_swap_position 按日期序列(利息腿),看 InterestProfitSum/RealizedInterest 演变
var eodTimeline = db.eod_swap_position
.Where(x => x.SwapTradeId == tradeId && x.InterestDirection > 0)
.OrderBy(x => x.ValueDate).ThenBy(x => x.PositionId)
.ToList();
Console.WriteLine($"\n[3] eod_swap_position 利息腿按日序列: {eodTimeline.Count} 条");
Console.WriteLine($"{"ValueDate",12} {"PositionId",10} {"InterestProfitSum",18} {"RealizedInterest",18} {"TdCloseInterest",16} {"InterestIncomeSum",18}");
foreach (var e in eodTimeline)
{
Console.WriteLine($"{e.ValueDate:yyyy-MM-dd} {e.PositionId,10} {e.InterestProfitSum,18:F4} {e.RealizedInterest,18:F4} {e.TdCloseInterest,16:F4} {e.InterestIncomeSum,18:F4}");
}
// 4. 甄别:找出有效的 29号互换 和 30号平仓
var validFlowEvents = allFlowEvents
.Where(x => x.DataState == (int)SwapFlowDateStateEnum.完成)
.OrderBy(x => x.EventDate).ThenBy(x => x.id)
.ToList();
var swapOn29 = validFlowEvents.Where(x => x.EventDate == new DateTime(2026, 6, 29)
&& (x.EventType == (int)SwapFlowEventTypeEnum.互换 || x.EventType == (int)SwapFlowEventTypeEnum.自动互换)).ToList();
var closeOn30 = validFlowEvents.Where(x => x.EventDate == new DateTime(2026, 6, 30)
&& x.EventType == (int)SwapFlowEventTypeEnum.平仓).ToList();
Console.WriteLine($"\n[4] 关键操作甄别(DataState=完成)");
Console.WriteLine($" 29号互换/自动互换: {swapOn29.Count} 条");
foreach (var s in swapOn29)
Console.WriteLine($" id={s.id} PositionId={s.PositionId} InterestClosePnL={s.InterestClosePnL:F4} InterestAmount={s.InterestAmount:F4}");
Console.WriteLine($" 30号平仓: {closeOn30.Count} 条");
foreach (var c in closeOn30)
Console.WriteLine($" id={c.id} PositionId={c.PositionId} InterestClosePnL={c.InterestClosePnL:F4} InterestAmount={c.InterestAmount:F4}");
// 5. 模拟"打开平仓页"——分别测 6-29/6-30/7-1 三天,对比默认值变化
Console.WriteLine($"\n[5] 调 GetUnwindInterests 模拟打开平仓页(6-29/6-30/7-1 三天对比)");
var userInfo = new OptUserInfo(1, "UnitTest", OptUserFrom.UnitTest);
var service = new SwapDealService(userInfo);
var testDates = new[] {
new DateTime(2026, 6, 29),
new DateTime(2026, 6, 30),
new DateTime(2026, 7, 1),
};
Console.WriteLine($" {"日期",12} {"PositionId",10} {"InterestMode",14} {"默认InterestClosePnL",22} {"eod待实现IPS",14} {"eod已实现RI",14} {"Δ默认-待实现",14}");
foreach (var testDate in testDates)
{
var defaults = service.GetUnwindInterests(
testDate, testDate, tradeId, 1m, (int)SwapEventTypeEnum.平仓);
foreach (var d in defaults.Where(x => x.InterestDirection > 0))
{
// 找该日期前最近的 eod
var preEod = eodTimeline.Where(x => x.PositionId == d.PositionId && x.ValueDate < testDate)
.OrderByDescending(x => x.ValueDate).FirstOrDefault();
decimal ips = preEod?.InterestProfitSum ?? 0;
decimal ri = preEod?.RealizedInterest ?? 0;
decimal delta = d.InterestClosePnL - ips;
string modeName = ((InterestModeEnum)d.InterestMode).ToString();
string preEodDate = preEod?.ValueDate.ToString("MM-dd") ?? "无";
Console.WriteLine($" {testDate:yyyy-MM-dd} {d.PositionId,10} {modeName,14} {d.InterestClosePnL,22:F4} {ips,14:F4}({preEodDate}) {ri,14:F4} {delta,14:F4}");
}
}
// 6. 核心诊断
Console.WriteLine($"\n[6] 核心诊断");
Console.WriteLine($" 关键观察:29号互换已实现 77.26,看 eod 的 InterestProfitSum(待实现) 是否扣减了已实现部分");
var eod29 = eodTimeline.Where(x => x.ValueDate == new DateTime(2026, 6, 29)).ToList();
foreach (var e in eod29)
{
Console.WriteLine($" PositionId={e.PositionId} 6-29 eod:");
Console.WriteLine($" InterestProfitSum(待实现) = {e.InterestProfitSum:F4}");
Console.WriteLine($" RealizedInterest(已实现) = {e.RealizedInterest:F4}");
Console.WriteLine($" TdCloseInterest(当日实现) = {e.TdCloseInterest:F4}");
if (e.InterestProfitSum != 0 && e.RealizedInterest != 0 && Math.Abs(e.InterestProfitSum - e.RealizedInterest) < 0.1m)
{
Console.WriteLine($" ⚠ 待实现({e.InterestProfitSum:F4}) ≈ 已实现({e.RealizedInterest:F4}) → 互换结清后待实现没归零!");
Console.WriteLine($" → 导致后续平仓默认值仍基于待实现(77.26)算,偏大");
}
}
Console.WriteLine($"\n 用户反馈:6-29看平仓默认=0(正确,因为当天还没收盘/互换),6-30和7-1有问题");
Console.WriteLine($" 根因:29号收盘后 InterestProfitSum 没扣减已实现的 77.26(仍=77.26),");
Console.WriteLine($" 所以后续平仓默认值 = 77.26(应已归零的待实现) + 增量 → 偏大");
Assert.IsTrue(allEvents.Count > 0);
}
finally
{
db?.Dispose();
}
}
}
}