using Newtonsoft.Json; using YLErp; namespace YLErp.Modules.EodModule { #region Golden 数据模型 /// /// 日终价格"标的种类 + 数据来源"golden 场景模型。 /// 每个 JSON 文件存:一组原始输入行 + 每行的期望输出(种类中文/来源/路由键)。 /// 结构与 SwapModule 的 GoldenScenarioModel 对齐(Scenario/Description/Source + Rows)。 /// public class EodPriceGoldenModel { public string Scenario { get; set; } public string Description { get; set; } /// synthetic(合成 Mock) / recorded(真实库录制) public string Source { get; set; } = "synthetic"; public DateTime? RecordedAt { get; set; } public List Rows { get; set; } = new(); } public class EodPriceGoldenRow { public string UnderlyingCode { get; set; } /// 存储表路由键 = DTO.UnderlyingInstrumentType(EodPriceView 靠它选表) public string RouteKey { get; set; } /// 真实标的种类 = underlying_manager.UnderlyingInstrumentType public string RealInstrumentType { get; set; } public bool IsBond { get; set; } /// 期望的"标的种类"列显示值 public string ExpectedTypeCn { get; set; } /// 期望的"数据来源"(仅债券行断言) public string ExpectedDataSource { get; set; } } #endregion /// /// 日终价格 Golden 回放测试 /// ============================================================================ /// 仿 SwapModule/DealInterestsGoldenReplayTest: /// - Record_* :连真实库拉数据生成 golden JSON(标 [Ignore],手动跑) /// - Replay_* :读 Mock/录制 JSON 重放并逐行断言(进 CI,不碰库) /// /// 守护点(回放时任何一行不符即失败): /// 1. 标的种类按真实类型显示(现券→信用债、贵金属→黄金现货…),不再一律"商品期货"; /// 2. 路由键 UnderlyingInstrumentType 保持不变(保证"查看"不串表); /// 3. 债券数据来源固定为中债估值(聚源仅转发,无人手工维护,不随 JSID 变化)。 /// ============================================================================ /// [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(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],手动跑) /// /// 从真实库拉一批 underlying_manager + china_bond_valuation, /// 按当前生产逻辑生成 recorded golden JSON。 /// 手动取消 [Ignore] 运行;生成后复制到 Resources/GoldenFiles/EodPriceGolden/ 持久化。 /// [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(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 } }