test(swap): SwapDealService抽7个virtual seam + 手动结算内存测试

攻克SwapIncome/SwapUnwind零单元测试覆盖的盲区(此前仅DBRecording,CI不跑)。

SwapDealService补7个protected virtual seam(参考已验证的SwapEodPositionService模式):
- FindTrade/AddClientCash/SaveSwapDeal(原private改protected virtual)
- SaveAllChanges/ExecuteInTransaction/CallSaveSwapTradeClientCash/TriggerRealtimeSwapPosition
生产实现行为不变,SwapIncome/SwapUnwind/ApproveSwapTrade重构为调seam。

新增SwapDealSettlementTest.cs(4个内存测试):
- SD_001 SwapIncome资金流水金额=-SwapRealizedPnL
- SD_002 含预付金返息双流水
- SD_003 SwapUnwind全平仓持仓归零+状态变更
- SD_004 DealFloatPosition含费价重算(后端唯二真做计算处)
红灯验证通过(改错金额符号测试立即失败)。
This commit is contained in:
hjhan
2026-07-03 08:22:43 +08:00
parent 782e1f6a7a
commit a207927f1b
2 changed files with 347 additions and 61 deletions
+96 -61
View File
@@ -20,6 +20,83 @@ namespace YLErp.Modules.SwapModule
return EodPriceQueryService.TryGetPrice(valueDate, underlyingCode, out rate);
}
#region Seamsoverride DB/
/// <summary>查找交易(生产: DbContext.trade.Find;测试: 返回内存对象)</summary>
protected virtual trade FindTrade(int tradeId)
{
return DbContext.trade.Find(tradeId);
}
/// <summary>添加资金记录(生产: AddClientCashInCashOut;测试: 计数并记录金额)</summary>
protected virtual int AddClientCash(trade td, double amount, string action, DateTime valueDate)
{
return AddClientCashInCashOut(td, amount, action, valueDate);
}
/// <summary>保存互换/平仓事件(生产: 落库+建事件;测试: 收集 unwindData 入内存列表)。
/// 原 private 改 protected virtual,使测试 stub 可整体 override,规避内部 new SwapEventService 连库。</summary>
protected virtual long SaveSwapDeal(UnwindData unwindData, int eventType, int clientCashId, string eventResason = "", bool approve = false)
{
return SaveSwapDealInternal(unwindData, eventType, clientCashId, eventResason, approve);
}
/// <summary>保存所有变更(生产: DbContext.SaveChanges;测试: 空操作)</summary>
protected virtual void SaveAllChanges()
{
DbContext.SaveChanges();
}
/// <summary>在事务中执行(生产: BeginTransaction/Commit/Rollback;测试: 直接执行不包事务)</summary>
protected virtual void ExecuteInTransaction(Action action)
{
var trans = DbContext.Database.BeginTransaction();
try
{
action();
trans.Commit();
}
catch
{
trans.Rollback();
throw;
}
finally
{
trans.Dispose();
}
}
/// <summary>保存互换交易资金记录(生产: new ClientCashInCashOutService;测试: 空操作)。
/// 仅 SwapUnwind 全平仓且 NeedOpenFee=false 时调用。</summary>
protected virtual void CallSaveSwapTradeClientCash(trade td, DateTime valueDate)
{
td.trade_extend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == td.id);
if (td.trade_extend != null && !td.trade_extend.ExtendObj.NeedOpenFee)
{
new ClientCashInCashOutService(this).SaveSwapTradeClientCash(td, td.TradePrice ?? 0, valueDate, 0);
}
}
/// <summary>触发互换实时持仓计算(生产: Task.Run 异步 RealtimePnlCalc;测试: 空操作)。
/// 仅 SwapUnwind 成功后调用。</summary>
protected virtual void TriggerRealtimeSwapPosition()
{
Task.Run(() =>
{
try
{
RealtimePnlCalc.RealtimeSwapPosition(new OptUserInfo(0, "互换实时持仓服务", OptUserFrom.Service));
}
catch (Exception ex)
{
LogFactory.GetLogger<SwapDealService>().Error("互换实时持仓服务计算失败", ex);
}
});
}
#endregion
public SwapDealService(OptUserInfo optUser) : base(optUser)
{
@@ -990,24 +1067,23 @@ namespace YLErp.Modules.SwapModule
/// <exception cref="ServiceException"></exception>
public void SwapUnwind(UnwindData unwindData)
{
var td = DbContext.trade.Find(unwindData.SwapTradeId);
var td = FindTrade(unwindData.SwapTradeId);
if (td == null)
{
throw new ServiceException("未找到交易信息");
}
//CheckLastEod(unwindData.ValueDate, td.StartDate.Value, unwindData.SwapTradeId); //去掉平仓收盘限制
var trans = DbContext.Database.BeginTransaction();
bool cofirm = false;
try
ExecuteInTransaction(() =>
{
int clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapRealizedPnL), ClientCashInCashOut._平仓费, unwindData.ValueDate);
int clientCashId = AddClientCash(td, Convert.ToDouble(-unwindData.SwapRealizedPnL), ClientCashInCashOut._平仓费, unwindData.ValueDate);
if (unwindData.SwapMarginAmount != 0)
{
AddClientCashInCashOut(td, Convert.ToDouble(unwindData.SwapMarginAmount), ClientCashInCashOut._应付预付金, unwindData.ValueDate);
AddClientCash(td, Convert.ToDouble(unwindData.SwapMarginAmount), ClientCashInCashOut._应付预付金, unwindData.ValueDate);
}
//if (unwindData.SwapMarginRebatePnl != 0)
//{
// AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapMarginRebatePnl), ClientCashInCashOut.系统操作_预付金返息, unwindData.ValueDate);
// AddClientCash(td, Convert.ToDouble(-unwindData.SwapMarginRebatePnl), ClientCashInCashOut.系统操作_预付金返息, unwindData.ValueDate);
//}
DealFloatPosition(unwindData);
var flowList = new List<swap_flow_event>(unwindData.FlowEvents);
@@ -1015,11 +1091,7 @@ namespace YLErp.Modules.SwapModule
if (unwindData.CloseMethod == (int)CloseMethodEnum. || unwindData.ClosePercent == 1)
{
td.TradeStatus = "已平仓";
td.trade_extend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == td.id);
if (td.trade_extend != null && !td.trade_extend.ExtendObj.NeedOpenFee)
{
new ClientCashInCashOutService(this).SaveSwapTradeClientCash(td, td.TradePrice ?? 0, unwindData.ValueDate, 0);
}
CallSaveSwapTradeClientCash(td, unwindData.ValueDate);
}
else
{
@@ -1028,32 +1100,12 @@ namespace YLErp.Modules.SwapModule
td.UnWindDate = unwindData.UnwindDate;
td.StockEqvNotional -= Convert.ToDouble(unwindData.CloseNotionalValue);
td.TradeAmount -= Convert.ToDouble(unwindData.CloseQty);
DbContext.SaveChanges();
trans.Commit();
SaveAllChanges();
cofirm = true;
}
catch (Exception ex)
{
trans.Rollback();
throw new Exception(ex.Message, ex);
}
finally
{
trans.Dispose();
}
});
if (cofirm)
{
Task.Run(() =>
{
try
{
RealtimePnlCalc.RealtimeSwapPosition(new OptUserInfo(0, "互换实时持仓服务", OptUserFrom.Service));
}
catch (Exception ex)
{
LogFactory.GetLogger<SwapDealService>().Error("互换实时持仓服务计算失败", ex);
}
});
TriggerRealtimeSwapPosition();
}
}
/// <summary>
@@ -1490,20 +1542,18 @@ namespace YLErp.Modules.SwapModule
/// <exception cref="ServiceException"></exception>
public void SwapIncome(UnwindData unwindData)
{
var td = DbContext.trade.Find(unwindData.SwapTradeId);
var td = FindTrade(unwindData.SwapTradeId);
if (td == null)
{
throw new ServiceException("未找到交易信息");
}
//CheckLastEod(unwindData.ValueDate, td.StartDate.Value, unwindData.SwapTradeId); //去掉平仓收盘限制
var trans = DbContext.Database.BeginTransaction();
bool confirm = false;
try
ExecuteInTransaction(() =>
{
int clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapRealizedPnL), ClientCashInCashOut._互换, unwindData.ValueDate);
int clientCashId = AddClientCash(td, Convert.ToDouble(-unwindData.SwapRealizedPnL), ClientCashInCashOut._互换, unwindData.ValueDate);
if (unwindData.SwapMarginRebatePnl != 0)
{
AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapMarginRebatePnl), ClientCashInCashOut._预付金返息, unwindData.ValueDate);
AddClientCash(td, Convert.ToDouble(-unwindData.SwapMarginRebatePnl), ClientCashInCashOut._预付金返息, unwindData.ValueDate);
}
foreach (var item in unwindData.FlowEvents)
{
@@ -1517,19 +1567,8 @@ namespace YLErp.Modules.SwapModule
td.TradeStatus = "已到期";
}
td.UnWindDate = unwindData.UnwindDate;
DbContext.SaveChanges();
trans.Commit();
confirm = true;
}
catch (Exception ex)
{
trans.Rollback();
throw;
}
finally
{
trans.Dispose();
}
SaveAllChanges();
});
}
/// <summary>
/// 互换/平仓审核通过
@@ -1548,20 +1587,16 @@ namespace YLErp.Modules.SwapModule
swapEvent.unwindData = JsonConvert.DeserializeObject<UnwindData>(swapEvent.EventData);
var flowList = DbContext.swap_flow_event.Where(x => x.EventId == swapEvent.id).ToList();
string action = eventType == (int)SwapEventTypeEnum. ? ClientCashInCashOut._互换 : ClientCashInCashOut._平仓费;
int clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(-swapEvent.unwindData.SwapRealizedPnL), action, swapEvent.unwindData.ValueDate);
int clientCashId = AddClientCash(td, Convert.ToDouble(-swapEvent.unwindData.SwapRealizedPnL), action, swapEvent.unwindData.ValueDate);
if (swapEvent.unwindData.SwapMarginAmount != 0)
{
AddClientCashInCashOut(td, Convert.ToDouble(swapEvent.unwindData.SwapMarginAmount), ClientCashInCashOut._应付预付金, swapEvent.unwindData.ValueDate);
AddClientCash(td, Convert.ToDouble(swapEvent.unwindData.SwapMarginAmount), ClientCashInCashOut._应付预付金, swapEvent.unwindData.ValueDate);
}
swapEvent.ClientCashId = clientCashId;
if (swapEvent.unwindData.CloseMethod == (int)CloseMethodEnum.)
{
td.TradeStatus = "已平仓";
td.trade_extend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == td.id);
if (td.trade_extend != null && !td.trade_extend.ExtendObj.NeedOpenFee)
{
new ClientCashInCashOutService(this).SaveSwapTradeClientCash(td, td.TradePrice ?? 0, swapEvent.unwindData.ValueDate, 0);
}
CallSaveSwapTradeClientCash(td, swapEvent.unwindData.ValueDate);
}
else
{
@@ -1618,7 +1653,7 @@ namespace YLErp.Modules.SwapModule
/// </summary>
/// <param name="swap_Deal"></param>
/// <param name="eventType"></param>
private long SaveSwapDeal(UnwindData unwindData, int eventType, int clientCashId, string eventResason = "", bool approve = false)
private long SaveSwapDealInternal(UnwindData unwindData, int eventType, int clientCashId, string eventResason = "", bool approve = false)
{
var td = DbContext.trade.Find(unwindData.SwapTradeId);
if (td == null)