feat(bond): 支持债券与股票基金公司行为现金流计算的差异化处理 - init2

- 修改 CalcPayment 方法添加 useBondPriceScale 参数区分债券和股票/基金的金额计算口径
- 债券利息按每100元面值票息通过BondPriceConverter转为入库金额,股票基金分红直接计算
- 在BondPaymentService中添加详细的参数说明文档注释
- 更新SwapDealService中分红计算逻辑,根据标的类型自动选择合适的金额转换方式
- 新增CorporateActionEventLifecycleTest单元测试验证公司行为事件生命周期管理
- 添加SplitCorporateActionTddTest测试验证拆合股功能
- 优化FundCorporateActionRollbackAndUnwindTest扩展到股票类型测试
- 更新前端OperationHistory页面表格列宽和显示格式支持更长的说明信息
This commit is contained in:
张名锐
2026-08-19 17:48:34 +08:00
parent ddf233678d
commit aa3548e77f
19 changed files with 1342 additions and 104 deletions
+110 -3
View File
@@ -1,6 +1,7 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
@@ -206,14 +207,120 @@ namespace YLErp.Modules.SwapModule
return events;
}
/// <summary>
/// 获取交易操作历史
/// 获取交易操作历史。返回前会过滤掉登记日创建且尚未应用(Applied=false
/// 的公司行为事件,避免交易详情在真实调整前展示一条已完成历史。
/// </summary>
/// <param name="tradeId">交易id</param>
/// <returns></returns>
public List<swap_event> GetOpreationHistorys(int tradeId)
{
List<swap_event> list = DbContext.swap_event.Where(x => x.SwapTradeId == tradeId).OrderByDescending(o => o.id).ToList();
return list;
List<swap_event> list = DbContext.swap_event
.Where(x => x.SwapTradeId == tradeId)
.OrderByDescending(o => o.id)
.ToList();
return FilterOperationHistory(list);
}
/// <summary>
/// 过滤登记日创建且 Applied=false 的待生效公司行为事件,避免交易详情在真正
/// 调整前展示一条“已完成”历史。其他事件仍保留;无法解析的旧格式公司行为也
/// 保持可见,审计查询不能因为新 JSON 结构而静默丢失历史记录。
/// </summary>
public static List<swap_event> FilterOperationHistory(IEnumerable<swap_event> events)
{
return (events ?? Enumerable.Empty<swap_event>())
.Where(x => !IsPendingCorporateActionEvent(x))
.ToList();
}
/// <summary>
/// 判断指定事件是否为待生效的公司行为事件:EventType 为公司行为(13)
/// 且 EventData 反序列化后 Applied=false(登记日写入、尚未在真实除权日补齐调整后数据)。
/// 非公司行为类型、无法解析的旧格式或已应用的事件均返回 false,保证历史审计记录不被误删。
/// </summary>
public static bool IsPendingCorporateActionEvent(swap_event swapEvent)
{
if (swapEvent == null || swapEvent.EventType != (int)SwapEventTypeEnum.)
{
return false;
}
if (!TryDeserializeCorporateActionEventData(swapEvent, out var data))
{
// 非快照格式的历史公司行为保持可见,避免误删审计记录。
return false;
}
return !data.Applied;
}
/// <summary>
/// 将 swap_event.EventData 安全反序列化为公司行为快照。事件为空、EventData
/// 为空白或 JSON 格式不匹配时返回 false 并将 data 置 null,调用方据此保留旧格式记录。
/// </summary>
public static bool TryDeserializeCorporateActionEventData(
swap_event swapEvent,
out CorporateActionEventData data)
{
data = null;
if (swapEvent == null || string.IsNullOrWhiteSpace(swapEvent.EventData))
{
return false;
}
try
{
data = JsonConvert.DeserializeObject<CorporateActionEventData>(swapEvent.EventData);
return data != null;
}
catch (JsonException)
{
return false;
}
}
/// <summary>
/// 公司行为说明使用稳定的键值格式,完整保留调整前后名义本金、价格、数量、
/// 待实现分红和现金流变化,操作历史无需重新计算即可核对。
/// </summary>
public static string BuildCorporateActionEventReason(CorporateActionEventData data)
{
if (data == null)
{
return "公司行为快照为空";
}
// 使用 InvariantCulture 固定小数与日期格式,说明文本不随服务器区域设置变化。
string D(decimal value) => value.ToString(CultureInfo.InvariantCulture);
string Date(DateTime? value) => value.HasValue
? value.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)
: "";
return string.Join("; ", new[]
{
$"公司行为[{data.UnderlyingCode}]",
$"ExDividendDate={Date(data.ExDividendDate)}",
$"EffectiveDate={Date(data.EffectiveDate)}",
$"ExDividendInfoId={data.ExDividendInfoId}",
$"PositionId={data.PositionId}",
$"GiveCashAmount={D(data.GiveCashAmount)}",
$"GiveShareAmount={D(data.GiveShareAmount)}",
$"Split={(data.Split.HasValue ? D(data.Split.Value) : "")}",
$"RationedSharesAmount={D(data.RationedSharesAmount)}",
$"RationedSharesPrice={D(data.RationedSharesPrice)}",
"调整前",
$"BeforeNotional={D(data.BeforeNotional)}",
$"BeforePrice={D(data.BeforePrice)}",
$"BeforeQuantity={D(data.BeforeQuantity)}",
$"BeforePendingDividend={D(data.BeforePendingDividend)}",
"调整后",
$"AfterNotional={D(data.AfterNotional)}",
$"AfterPrice={D(data.AfterPrice)}",
$"AfterQuantity={D(data.AfterQuantity)}",
$"AfterPendingDividend={D(data.AfterPendingDividend)}",
$"CashFlowChange={D(data.CashFlowChange)}",
$"Applied={data.Applied}"
});
}
public void DeleteEvent(int tradeId)