diff --git a/UnitTestProject/Modules/EodModule/EodPriceUnderlyingIdGuardTest.cs b/UnitTestProject/Modules/EodModule/EodPriceUnderlyingIdGuardTest.cs
new file mode 100644
index 00000000..4427770b
--- /dev/null
+++ b/UnitTestProject/Modules/EodModule/EodPriceUnderlyingIdGuardTest.cs
@@ -0,0 +1,88 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using YLErp.DBModels;
+
+namespace YLErp.Modules.EodModule
+{
+ ///
+ /// FR007 错行根因的校正决策单测(GLMS-20260701)。
+ /// 对应最近提交的 bugfix:eod_commodity_future_price 入库前以 UnderlyingCode(=FutureContractId) 为准
+ /// 重派生 UnderlyingId,防止 UnderlyingId 与 FutureContractId 失同步导致"网页能查到、EOD 结算查不到"。
+ /// 这里只测纯函数 ResolveUnderlyingIdForCode,不依赖数据库。
+ ///
+ /// 生产事故还原:FR007 价格行的 FutureContractId='FR007',但 UnderlyingId 被错写成
+ /// 511160.SH 的 2173889 / 159111.SZ 的 2173890,正确应为 FR007 的 2170838。
+ /// 网页端按 UnderlyingId(int) JOIN underlying_manager 把 FR007 行误挂到 511160.SH;
+ /// 而 EOD 结算按 UnderlyingCode(string) JOIN 查不到,报"结算价格缺失"。
+ ///
+ [TestClass]
+ public class EodPriceUnderlyingIdGuardTest
+ {
+ private const int Fr007CorrectId = 2170838;
+ private const int Id511160 = 2173889; // 511160.SH 的 id(被错写)
+ private const int Id159111 = 2173890; // 159111.SZ 的 id(被错写)
+
+ [TestMethod]
+ public void 空UnderlyingCode_维持原值()
+ {
+ Assert.AreEqual(123, EodPriceService.ResolveUnderlyingIdForCode("", 123, null));
+ Assert.AreEqual(123, EodPriceService.ResolveUnderlyingIdForCode(null, 123, 999));
+ Assert.AreEqual(123, EodPriceService.ResolveUnderlyingIdForCode(" ", 123, 999));
+ }
+
+ [TestMethod]
+ public void 标的不存在_维持原值()
+ {
+ // resolvedId=null 表示 underlying_manager 无此代码,无法校正
+ Assert.AreEqual(Id511160,
+ EodPriceService.ResolveUnderlyingIdForCode("FR007", Id511160, null));
+ }
+
+ [TestMethod]
+ public void 已一致_维持原值()
+ {
+ Assert.AreEqual(Fr007CorrectId,
+ EodPriceService.ResolveUnderlyingIdForCode("FR007", Fr007CorrectId, Fr007CorrectId));
+ }
+
+ [TestMethod]
+ public void 不一致_校正为正确id_FR007生产错行_511160()
+ {
+ // 生产事故:FR007 行 UnderlyingId=2173889(511160.SH) → 应校正为 2170838
+ Assert.AreEqual(Fr007CorrectId,
+ EodPriceService.ResolveUnderlyingIdForCode("FR007", Id511160, Fr007CorrectId));
+ }
+
+ [TestMethod]
+ public void 不一致_校正为正确id_FR007生产错行_159111()
+ {
+ // 生产事故:另两条错行 UnderlyingId=2173890(159111.SZ) → 应校正为 2170838
+ Assert.AreEqual(Fr007CorrectId,
+ EodPriceService.ResolveUnderlyingIdForCode("FR007", Id159111, Fr007CorrectId));
+ }
+
+ [TestMethod]
+ public void 行对象_端到端校正_FR007()
+ {
+ var row = new eod_commodity_future_price
+ {
+ UnderlyingCode = "FR007",
+ UnderlyingId = Id511160
+ };
+ // 模拟 db.underlying_manager 解析到的正确 id
+ int resolved = Fr007CorrectId;
+ int? before = row.UnderlyingId;
+ row.UnderlyingId = EodPriceService.ResolveUnderlyingIdForCode(row.UnderlyingCode, row.UnderlyingId ?? 0, resolved);
+
+ Assert.AreNotEqual(before, row.UnderlyingId);
+ Assert.AreEqual((int?)Fr007CorrectId, row.UnderlyingId);
+ }
+
+ [TestMethod]
+ public void 典型非错配_不同标的_各自正确不互相覆盖()
+ {
+ // 511160.SH 自己的行(FutureContractId='511160.SH'),UnderlyingId 已是 2173889 → 不动
+ Assert.AreEqual(Id511160,
+ EodPriceService.ResolveUnderlyingIdForCode("511160.SH", Id511160, Id511160));
+ }
+ }
+}
diff --git a/YLErpDAL/Modules/EodModule/EodPriceService.cs b/YLErpDAL/Modules/EodModule/EodPriceService.cs
index ce2ced60..449a4c75 100644
--- a/YLErpDAL/Modules/EodModule/EodPriceService.cs
+++ b/YLErpDAL/Modules/EodModule/EodPriceService.cs
@@ -29,6 +29,36 @@ namespace YLErp.Modules.EodModule
return (start, end);
}
+ ///
+ /// 校正 eod_commodity_future_price 行的 UnderlyingId,使其与 UnderlyingCode(=FutureContractId) 一致。
+ /// 背景:网页日终价格列表按 UnderlyingId(int) JOIN underlying_manager,而 EOD 结算(EodPriceProvider.Initialize)
+ /// 按 UnderlyingCode(string) JOIN。两列一旦失同步(典型如 FR007 的价格行 UnderlyingId 被错写成 511160.SH 的 id),
+ /// 会出现"网页能查到、结算却查不到"的错价缺失,进而 EodCheckSettlePrice 报"结算价格缺失"。
+ /// 这里以 UnderlyingCode 为准重新派生 UnderlyingId——该列才是上传/结算使用的自然键(FutureContractId),
+ /// 在入库前强制两列一致,既阻止产生新的错行,又通过告警日志把失同步暴露给运维追查上游写入来源。
+ ///
+ ///
+ /// 纯函数:根据 UnderlyingCode 校正决策。给定当前 UnderlyingId 与从 underlying_manager 解析到的正确 id,
+ /// 返回应使用的 UnderlyingId。UnderlyingCode 为空或库中无对应标的(resolvedId=null)时维持原值,
+ /// 已一致时也维持原值,仅在不一致时返回正确 id。抽成纯函数便于无数据库单测(覆盖 GLMS-20260701 FR007 错行根因)。
+ ///
+ public static int ResolveUnderlyingIdForCode(string underlyingCode, int currentId, int? resolvedId)
+ {
+ if (string.IsNullOrWhiteSpace(underlyingCode))
+ {
+ return currentId;
+ }
+ if (resolvedId == null)
+ {
+ return currentId;
+ }
+ if (resolvedId.Value == currentId)
+ {
+ return currentId;
+ }
+ return resolvedId.Value;
+ }
+
///
/// 校正 eod_commodity_future_price 行的 UnderlyingId,使其与 UnderlyingCode(=FutureContractId) 一致。
/// 背景:网页日终价格列表按 UnderlyingId(int) JOIN underlying_manager,而 EOD 结算(EodPriceProvider.Initialize)
@@ -45,18 +75,14 @@ namespace YLErp.Modules.EodModule
}
var um = db.underlying_manager.FirstOrDefault(u => u.UnderlyingCode == row.UnderlyingCode);
- if (um == null)
- {
- // 标的代码在 underlying_manager 不存在:无法校正,交由既有"标的代码不存在"等校验处理。
- return;
- }
-
- if (row.UnderlyingId != um.id)
+ var resolvedId = um == null ? (int?)null : um.id;
+ var before = row.UnderlyingId;
+ row.UnderlyingId = ResolveUnderlyingIdForCode(row.UnderlyingCode, row.UnderlyingId ?? 0, resolvedId);
+ if (row.UnderlyingId != before)
{
LogFactory.GetLogger("EodPrice").Info(
$"eod_commodity_future_price.UnderlyingId 与 UnderlyingCode 不一致,已自动校正: " +
- $"FutureContractId={row.UnderlyingCode}, 原UnderlyingId={row.UnderlyingId}, 修正为={um.id}");
- row.UnderlyingId = um.id;
+ $"FutureContractId={row.UnderlyingCode}, 原UnderlyingId={before}, 修正为={row.UnderlyingId}");
}
}