Files
zszq-trs/UnitTestProject/Modules/EodModule/EodPriceGoldenReplayTest.cs
T
hjhan 760ffd62e6 fix: 日终债券来源固定中债估值、人工改动显示操作人并补上传溯源
china_bond_valuation 来源恒为中债估值(聚源仅转发,无人手工维护),不再按 JSID 推断系统/人工;eod 两表仍用自带 DataSource(人工/系统)。债券被人工改动(update_user 非空)时,日终列表来源列显示操作人姓名,否则显示中债估值;操作人经已有 create_user/update_user 列溯源(零 DDL)。手工上传 xlsx 的债券分支补戳操作人,与表单编辑口径一致。配套单测 EodPriceDtoTest / EodPriceGoldenReplayTest + golden JSON。
2026-07-13 11:00:33 +08:00

192 lines
8.3 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 YLErp;
namespace YLErp.Modules.EodModule
{
#region Golden 数据模型
/// <summary>
/// 日终价格"标的种类 + 数据来源"golden 场景模型。
/// 每个 JSON 文件存:一组原始输入行 + 每行的期望输出(种类中文/来源/路由键)。
/// 结构与 SwapModule 的 GoldenScenarioModel 对齐(Scenario/Description/Source + Rows)。
/// </summary>
public class EodPriceGoldenModel
{
public string Scenario { get; set; }
public string Description { get; set; }
/// <summary>synthetic(合成 Mock) / recorded(真实库录制)</summary>
public string Source { get; set; } = "synthetic";
public DateTime? RecordedAt { get; set; }
public List<EodPriceGoldenRow> Rows { get; set; } = new();
}
public class EodPriceGoldenRow
{
public string UnderlyingCode { get; set; }
/// <summary>存储表路由键 = DTO.UnderlyingInstrumentTypeEodPriceView 靠它选表)</summary>
public string RouteKey { get; set; }
/// <summary>真实标的种类 = underlying_manager.UnderlyingInstrumentType</summary>
public string RealInstrumentType { get; set; }
public bool IsBond { get; set; }
/// <summary>期望的"标的种类"列显示值</summary>
public string ExpectedTypeCn { get; set; }
/// <summary>期望的"数据来源"(仅债券行断言)</summary>
public string ExpectedDataSource { get; set; }
}
#endregion
/// <summary>
/// 日终价格 Golden 回放测试
/// ============================================================================
/// 仿 SwapModule/DealInterestsGoldenReplayTest
/// - Record_* :连真实库拉数据生成 golden JSON(标 [Ignore],手动跑)
/// - Replay_* :读 Mock/录制 JSON 重放并逐行断言(进 CI,不碰库)
///
/// 守护点(回放时任何一行不符即失败):
/// 1. 标的种类按真实类型显示(现券→信用债、贵金属→黄金现货…),不再一律"商品期货";
/// 2. 路由键 UnderlyingInstrumentType 保持不变(保证"查看"不串表);
/// 3. 债券数据来源固定为中债估值(聚源仅转发,无人手工维护,不随 JSID 变化)。
/// ============================================================================
/// </summary>
[TestClass]
public class EodPriceGoldenReplayTest
{
private static readonly string GoldenDir = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory, "Resources", "GoldenFiles", "EodPriceGolden");
#region 回放:读 golden 重放 + 逐行断言(进 CI
[TestMethod]
public void Replay_AllGoldenFiles()
{
if (!Directory.Exists(GoldenDir))
{
Assert.Inconclusive($"golden 目录不存在: {GoldenDir}");
return;
}
var files = Directory.GetFiles(GoldenDir, "*.json").OrderBy(f => f).ToArray();
Assert.IsTrue(files.Length > 0, "应至少有 1 个 golden 文件");
int rowsChecked = 0;
foreach (var file in files)
{
var golden = JsonConvert.DeserializeObject<EodPriceGoldenModel>(File.ReadAllText(file));
Console.WriteLine($"\n回放: {Path.GetFileName(file)} - {golden.Scenario} [{golden.Source}]");
foreach (var row in golden.Rows)
{
// 用原始输入重建 DTO(等价于 SearchUnderlyingList 的投影结果)
var dto = new EodUnderlyingPriceDto
{
UnderlyingCode = row.UnderlyingCode,
UnderlyingInstrumentType = row.RouteKey, // 路由键
RealInstrumentType = row.RealInstrumentType, // 真实类型
IsBond = row.IsBond
};
// 债券来源固定为中债估值(等价 SearchUnderlyingList 后处理赋值)
if (dto.IsBond)
{
dto.DataSource = EodPriceBase.中债估值;
}
// 守护点1:显示按真实类型
Assert.AreEqual(row.ExpectedTypeCn, dto.UnderlyingInstrumentTypeCn,
$"[{row.UnderlyingCode}] 标的种类显示不符");
// 守护点2:路由键不变
Assert.AreEqual(row.RouteKey, dto.UnderlyingInstrumentType,
$"[{row.UnderlyingCode}] 路由键被改动,会导致查看串表");
// 守护点3:债券来源
if (row.IsBond)
{
Assert.AreEqual(row.ExpectedDataSource, dto.DataSource,
$"[{row.UnderlyingCode}] 债券数据来源判定不符");
}
rowsChecked++;
Console.WriteLine($" ✅ {row.UnderlyingCode}: {dto.UnderlyingInstrumentTypeCn}" +
(row.IsBond ? $" / {dto.DataSource}" : ""));
}
}
Console.WriteLine($"\n回放完成,共校验 {rowsChecked} 行");
Assert.IsTrue(rowsChecked > 0, "至少应校验 1 行");
}
#endregion
#region 录制:连真实库拉数据生成 golden(标 [Ignore],手动跑)
/// <summary>
/// 从真实库拉一批 underlying_manager + china_bond_valuation
/// 按当前生产逻辑生成 recorded golden JSON。
/// 手动取消 [Ignore] 运行;生成后复制到 Resources/GoldenFiles/EodPriceGolden/ 持久化。
/// </summary>
[TestMethod]
[Ignore]
[TestCategory("GoldenRecord")]
public void Record_FromRealDb()
{
Directory.CreateDirectory(GoldenDir);
var golden = new EodPriceGoldenModel
{
Scenario = "标的种类与来源(真实库录制)",
Description = "从 underlying_manager/china_bond_valuation 采样,快照当前生产映射",
Source = "recorded",
RecordedAt = DateTime.Now
};
using (var db = DbContextFactory.GetYLDbContext())
{
// 采样若干上线标的(含真实类型)
var uns = db.underlying_manager
.Where(x => x.LaunchState == "1")
.Select(x => new { x.UnderlyingCode, x.UnderlyingInstrumentType })
.Take(30).ToList();
// 债券估值采样(来源恒为中债估值,无需按 JSID 判定)
var bonds = db.china_bond_valuation
.Select(b => new { b.bond_id })
.Take(200).ToList();
var bondCodes = new HashSet<string>(bonds.Select(b => b.bond_id));
foreach (var un in uns)
{
bool isBond = bondCodes.Contains(un.UnderlyingCode);
// 路由键:债券走真实类型,其余按来源表默认(这里录制以真实类型近似,
// 因为 recorded 主要用于快照真实分布;CI 用 synthetic 覆盖精确路由)。
string routeKey = isBond
? un.UnderlyingInstrumentType
: ConsGlobal.InstrumentType.CommodityFutures;
golden.Rows.Add(new EodPriceGoldenRow
{
UnderlyingCode = un.UnderlyingCode,
RouteKey = routeKey,
RealInstrumentType = un.UnderlyingInstrumentType,
IsBond = isBond,
ExpectedTypeCn = ConsGlobal.InstrumentType.GetDesc(un.UnderlyingInstrumentType),
ExpectedDataSource = isBond ? EodPriceBase.中债估值 : null
});
}
}
var path = Path.Combine(GoldenDir, "golden_标的种类与来源_recorded.json");
File.WriteAllText(path, JsonConvert.SerializeObject(golden, Formatting.Indented));
Console.WriteLine($"✅ 录制 {golden.Rows.Count} 行 -> {path}");
}
#endregion
}
}