fix: 日终债券来源固定中债估值、人工改动显示操作人并补上传溯源

china_bond_valuation 来源恒为中债估值(聚源仅转发,无人手工维护),不再按 JSID 推断系统/人工;eod 两表仍用自带 DataSource(人工/系统)。债券被人工改动(update_user 非空)时,日终列表来源列显示操作人姓名,否则显示中债估值;操作人经已有 create_user/update_user 列溯源(零 DDL)。手工上传 xlsx 的债券分支补戳操作人,与表单编辑口径一致。配套单测 EodPriceDtoTest / EodPriceGoldenReplayTest + golden JSON。
This commit is contained in:
hjhan
2026-07-13 11:00:33 +08:00
parent 3f5e9a0be9
commit 760ffd62e6
7 changed files with 543 additions and 20 deletions
@@ -0,0 +1,191 @@
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
}
}