From 129e4bed0db7896572e6553a68bce96d0dcee7f9 Mon Sep 17 00:00:00 2001 From: hjhan Date: Fri, 3 Jul 2026 08:30:36 +0800 Subject: [PATCH] =?UTF-8?q?test(swap):=20=E8=A1=A5ApproveSwapTrade/ApplySw?= =?UTF-8?q?apTrade=E6=B5=8B=E8=AF=95+3=E4=B8=AAseam(=E5=80=9F=E9=89=B4test?= =?UTF-8?q?able=E5=88=86=E6=94=AF)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 借鉴 origin/glms/feature/refactor-swap-event-testable 的 SwapUnwindScenarioTest 场景4/5, 补全当前分支缺失的平仓审批流程覆盖。 SwapDealService 新增3个 protected virtual seam: - FindSwapEvent(查待审核事件) - FindFlowEventsByEventId(查事件关联流水) - CloseReCheckSetTrade(前置校验包装) ApproveSwapTrade/ApplySwapTrade 重构为调seam,生产行为不变。 SwapDealSettlementTest 新增2个测试(SD_005/006,共6个): - SD_005 ApproveSwapTrade 全平仓审核: 反序列化swap_event.EventData→资金流水→TradeStatus - SD_006 ApplySwapTrade 提交审核: CloseReCheck前置校验+SwapRealizedPnL=SwapCloseAmount 融合策略: 保留当前分支SwapIncome资金流水断言独特价值(SD_001/002), 借鉴testable分支审批流程覆盖(SD_005/006),不二选一。 SwapModule 157测试全绿,无回归。 --- .../SwapModule/SwapDealSettlementTest.cs | 110 +++++++++++++++++- .../Modules/SwapModule/SwapDealService.cs | 50 ++++---- 2 files changed, 138 insertions(+), 22 deletions(-) diff --git a/UnitTestProject/Modules/SwapModule/SwapDealSettlementTest.cs b/UnitTestProject/Modules/SwapModule/SwapDealSettlementTest.cs index fad4eb9c..85869479 100644 --- a/UnitTestProject/Modules/SwapModule/SwapDealSettlementTest.cs +++ b/UnitTestProject/Modules/SwapModule/SwapDealSettlementTest.cs @@ -1,3 +1,4 @@ +using Newtonsoft.Json; using YLErp.DBModels; using YLErp.DBModels.Enums; @@ -32,13 +33,21 @@ namespace YLErp.Modules.SwapModule private sealed class StubDealService : SwapDealService { private readonly trade _trade; + private readonly Dictionary _swapEvents; + private readonly Dictionary> _flowEventsByEventId; public List<(double amount, string action, DateTime date)> ClientCashCalls = new(); public List<(UnwindData data, int eventType, int clientCashId)> SaveSwapDealCalls = new(); public int SaveAllChangesCount; + public int CloseReCheckCallCount; - public StubDealService(trade td) : base(new OptUserInfo(0, nameof(SwapDealSettlementTest), OptUserFrom.UnitTest)) + public StubDealService(trade td, + Dictionary swapEvents = null, + Dictionary> flowEventsByEventId = null) + : base(new OptUserInfo(0, nameof(SwapDealSettlementTest), OptUserFrom.UnitTest)) { _trade = td; + _swapEvents = swapEvents ?? new Dictionary(); + _flowEventsByEventId = flowEventsByEventId ?? new Dictionary>(); } protected override trade FindTrade(int tradeId) => tradeId == _trade.id ? _trade : null; @@ -56,6 +65,24 @@ namespace YLErp.Modules.SwapModule return SaveSwapDealCalls.Count; // 返回自增 eventId } + // ApproveSwapTrade 查待审核事件:从内存字典取(key=eventType) + protected override swap_event FindSwapEvent(int tradeId, int eventType) + { + return _swapEvents.TryGetValue(eventType, out var evt) ? evt : null; + } + + // ApproveSwapTrade 查事件关联流水:从内存字典取 + protected override List FindFlowEventsByEventId(long eventId) + { + return _flowEventsByEventId.TryGetValue(eventId, out var list) ? list : new List(); + } + + // ApplySwapTrade 的前置校验:计数,不实际执行 + protected override void CloseReCheckSetTrade(int swapTradeId, bool isSwap, bool needCheck) + { + CloseReCheckCallCount++; + } + protected override void SaveAllChanges() { SaveAllChangesCount++; } protected override void ExecuteInTransaction(Action action) => action(); // 不包事务,直接执行 protected override void CallSaveSwapTradeClientCash(trade td, DateTime valueDate) { } // 空操作 @@ -247,5 +274,86 @@ namespace YLErp.Modules.SwapModule $"TradingAmount 应=ExitDirtyPrice(1.02)×CloseQty(1000)=1020,实际={closeEvent.TradingAmount}"); Console.WriteLine($"SD_004 通过:ExitDirtyFeePrice={closeEvent.TradingAmountFeeAvg},ExitCleanFeePrice={closeEvent.TradingAmountNetFeeAvg},TradingAmount={closeEvent.TradingAmount} ✅"); } + + // ================================================================ + // SD_005:ApproveSwapTrade 审核通过 —— 反序列化事件、资金流水、持仓状态 + // ================================================================ + + /// + /// [SD_005] ApproveSwapTrade 审核通过全部平仓 + /// ------------------------------------------------------------ + /// 后端 SwapDealService.ApproveSwapTrade:从 swap_event.EventData 反序列化 UnwindData, + /// 据此生成资金流水 + 更新持仓状态。 + /// 借鉴 testable 分支 SwapUnwindScenarioTest.Scenario4,验证: + /// - SwapRealizedPnL 从事件反序列化正确(EventData JSON) + /// - 资金流水金额 = -SwapRealizedPnL + /// - 全平仓 → TradeStatus=已平仓 + /// + [TestMethod] + public void SD_005_ApproveSwapTrade_全平仓审核_反序列化事件并记账() + { + var td = CreateTrade(); + // 构造待审核事件:EventData 里序列化了 UnwindData(含 SwapRealizedPnL=8000) + var unwindData = CreateUnwindData(swapRealizedPnL: 8000m, + closeMethod: (int)CloseMethodEnum.全部平仓, closePercent: 1m, + closeQty: 10000m, closeNotionalValue: 1000000m); + var swapEvent = new swap_event + { + id = 1, SwapTradeId = SwapTradeId, + EventType = (int)SwapEventTypeEnum.平仓, Invalid = false, + EventData = JsonConvert.SerializeObject(unwindData) + }; + var flowEvents = new Dictionary> + { + [1] = new List { new swap_flow_event { id = 1, EventId = 1, PositionId = 1 } } + }; + var service = new StubDealService(td, + swapEvents: new Dictionary { [(int)SwapEventTypeEnum.平仓] = swapEvent }, + flowEventsByEventId: flowEvents); + + service.ApproveSwapTrade(td, (int)SwapEventTypeEnum.平仓); + + // 资金流水:从反序列化的 SwapRealizedPnL(8000) 记账 → -8000 + Assert.AreEqual(1, service.ClientCashCalls.Count, "全平仓无预付金时应1条资金流水"); + Assert.AreEqual(-8000.0, service.ClientCashCalls[0].amount, 0.001, "资金流水=-反序列化的SwapRealizedPnL"); + // 持仓状态 + Assert.AreEqual("已平仓", td.TradeStatus, "审核全平仓 TradeStatus=已平仓"); + Console.WriteLine($"SD_005 通过:审核反序列化 SwapRealizedPnL=8000,资金流水={service.ClientCashCalls[0].amount},TradeStatus={td.TradeStatus} ✅"); + } + + // ================================================================ + // SD_006:ApplySwapTrade 提交审核 —— 前置校验 + 保存事件 + // ================================================================ + + /// + /// [SD_006] ApplySwapTrade 提交审核 + /// ------------------------------------------------------------ + /// 后端 SwapDealService.ApplySwapTrade:调 CloseReCheckSetTrade 前置校验 + SaveSwapDeal(approve=true)。 + /// 借鉴 testable 分支 SwapUnwindScenarioTest.Scenario5,验证: + /// - CloseReCheckSetTrade 被调用1次 + /// - SaveSwapDeal 以 approve=true 调用(事件类型正确) + /// - SwapRealizedPnL = SwapCloseAmount(ApplySwapTrade 内部赋值) + /// + [TestMethod] + public void SD_006_ApplySwapTrade_提交审核_前置校验与保存事件() + { + var td = CreateTrade(); + var service = new StubDealService(td); + // 前端提交时 SwapCloseAmount=6000(前端算好的总额),SwapRealizedPnL 初始可能为0 + var unwindData = CreateUnwindData(swapRealizedPnL: 0m); + unwindData.SwapCloseAmount = 6000m; // 模拟前端传入的平仓总额 + + service.ApplySwapTrade(unwindData, (int)SwapEventTypeEnum.平仓); + + // 前置校验被调用 + Assert.AreEqual(1, service.CloseReCheckCallCount, "应调用 CloseReCheckSetTrade 1次"); + // SaveSwapDeal 以 approve=true 调用 + Assert.AreEqual(1, service.SaveSwapDealCalls.Count, "应调用 SaveSwapDeal 1次"); + Assert.AreEqual((int)SwapEventTypeEnum.平仓, service.SaveSwapDealCalls[0].eventType, "事件类型=平仓"); + // SwapRealizedPnL 应被赋值为 SwapCloseAmount(ApplySwapTrade 内部 cs:1631) + Assert.AreEqual(6000m, service.SaveSwapDealCalls[0].data.SwapRealizedPnL, 0.001m, + "SwapRealizedPnL 应=SwapCloseAmount(6000)"); + Console.WriteLine($"SD_006 通过:CloseReCheck 调用{service.CloseReCheckCallCount}次,SwapRealizedPnL={service.SaveSwapDealCalls[0].data.SwapRealizedPnL} ✅"); + } } } diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index 72fe04b7..0fd4892f 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -95,6 +95,26 @@ namespace YLErp.Modules.SwapModule }); } + /// 查找待审核的互换/平仓事件(生产: DbContext.swap_event 查询;测试: 返回内存对象) + protected virtual swap_event FindSwapEvent(int tradeId, int eventType) + { + return DbContext.swap_event + .Where(x => x.SwapTradeId == tradeId && !x.Invalid && x.EventType == eventType) + .OrderByDescending(o => o.id).FirstOrDefault(); + } + + /// 查找事件关联的流水事件(生产: DbContext.swap_flow_event 查询;测试: 返回内存列表) + protected virtual List FindFlowEventsByEventId(long eventId) + { + return DbContext.swap_flow_event.Where(x => x.EventId == eventId).ToList(); + } + + /// 平仓/互换审核的前置校验与状态设置(生产: new TradeUnwindService;测试: 空操作或计数) + protected virtual void CloseReCheckSetTrade(int swapTradeId, bool isSwap, bool needCheck) + { + new TradeUnwindService(this).CloseReCheck_SetTrade(swapTradeId, isSwap, needCheck); + } + #endregion public SwapDealService(OptUserInfo optUser) : base(optUser) @@ -1578,14 +1598,13 @@ namespace YLErp.Modules.SwapModule /// public void ApproveSwapTrade(trade td, int eventType) { - Expression> eventExpression = x => x.SwapTradeId == td.id && !x.Invalid && x.EventType == eventType; - var swapEvent = DbContext.swap_event.Where(eventExpression).OrderByDescending(o => o.id).FirstOrDefault(); + var swapEvent = FindSwapEvent(td.id, eventType); if (swapEvent == null) { throw new Exception("该笔交易状态为平仓待复核,未找到相关记录,请检查该笔交易是否有效"); } swapEvent.unwindData = JsonConvert.DeserializeObject(swapEvent.EventData); - var flowList = DbContext.swap_flow_event.Where(x => x.EventId == swapEvent.id).ToList(); + var flowList = FindFlowEventsByEventId(swapEvent.id); string action = eventType == (int)SwapEventTypeEnum.互换 ? ClientCashInCashOut.系统操作_互换 : ClientCashInCashOut.系统操作_平仓费; int clientCashId = AddClientCash(td, Convert.ToDouble(-swapEvent.unwindData.SwapRealizedPnL), action, swapEvent.unwindData.ValueDate); if (swapEvent.unwindData.SwapMarginAmount != 0) @@ -1605,14 +1624,14 @@ namespace YLErp.Modules.SwapModule } td.UnWindDate = swapEvent.unwindData.UnwindDate; if (eventType != (int)SwapEventTypeEnum.互换) - { + { td.StockEqvNotional -= Convert.ToDouble(swapEvent.unwindData.CloseNotionalValue); td.TradeAmount -= Convert.ToDouble(swapEvent.unwindData.CloseQty); } td.Notional = td.TradeAmount; UpdateInitalPosition(flowList, swapEvent.unwindData, eventType); - DbContext.SaveChanges(); + SaveAllChanges(); } /// @@ -1623,30 +1642,19 @@ namespace YLErp.Modules.SwapModule /// public void ApplySwapTrade(UnwindData unwindData, int eventType) { - var td = DbContext.trade.Find(unwindData.SwapTradeId); + var td = FindTrade(unwindData.SwapTradeId); if (td == null) { throw new ServiceException("未找到交易信息"); } unwindData.SwapRealizedPnL = unwindData.SwapCloseAmount; - var trans = DbContext.Database.BeginTransaction(); string action = eventType == (int)SwapEventTypeEnum.互换 ? ClientCashInCashOut.系统操作_互换 : ClientCashInCashOut.系统操作_平仓费; - try + ExecuteInTransaction(() => { - new TradeUnwindService(this).CloseReCheck_SetTrade(unwindData.SwapTradeId, eventType == (int)SwapEventTypeEnum.互换, true); + CloseReCheckSetTrade(unwindData.SwapTradeId, eventType == (int)SwapEventTypeEnum.互换, true); SaveSwapDeal(unwindData, eventType, 0, action, true); - DbContext.SaveChanges(); - trans.Commit(); - } - catch (Exception ex) - { - trans.Rollback(); - throw; - } - finally - { - trans.Dispose(); - } + SaveAllChanges(); + }); } /// /// 保存平仓/互换事件