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
@@ -154,5 +154,18 @@ namespace YLErp.DBModels
/// 聚源id
/// </summary>
public long? JSID { get; set; }
/// <summary>
/// 创建人(登录用户ID)。聚源同步写入时为 NULL,手工编辑时由 SaveBondPrice 写入。
/// 对应库表 china_bond_valuation.create_user(bigint)。
/// </summary>
public long? create_user { get; set; }
/// <summary>
/// 更新人(登录用户ID)。聚源同步写入时为 NULL,手工编辑时由 SaveBondPrice 写入。
/// 对应库表 china_bond_valuation.update_user(bigint)。
/// 约定:NULL = 聚源/中债自动同步(无人手工维护);有值 = 被人手工改过、可溯源。
/// </summary>
public long? update_user { get; set; }
}
}
@@ -10,6 +10,8 @@ namespace YLErp.DBModels
{
public static string = "人工";
public static string = "系统";
/// <summary>中债估值(china_bond_valuation 的来源恒为此值;聚源仅转发,无人手工维护)</summary>
public static string = "中债估值";
/// <summary>
/// 合约代码
@@ -0,0 +1,206 @@
using YLErp;
namespace YLErp.Modules.EodModule
{
/// <summary>
/// 日终价格管理 —— 纯单元测试(不连库、秒级)。
/// 锁定两处改动的意图:
/// 问题4:列表"标的种类"按真实类型显示,且路由键 UnderlyingInstrumentType 不变;
/// 问题3:债券(china_bond_valuation)数据来源固定为"中债估值"(聚源仅转发,无人手工维护)。
/// </summary>
[TestClass]
public class EodPriceDtoTest
{
#region 4"商品期货"
[TestMethod]
[Description("有真实类型时,标的种类按真实类型显示,而非硬编码'商品期货'")]
public void _按真实类型显示_而非商品期货()
{
// 模拟从 eod_commodity_future_price 出来、但真实是现券的一行
var dto = new EodUnderlyingPriceDto
{
UnderlyingInstrumentType = ConsGlobal.InstrumentType.CommodityFutures, // 路由键(旧硬编码值)
RealInstrumentType = ConsGlobal.InstrumentType.CreditBonds // 真实类型=信用债
};
Assert.AreEqual("信用债", dto.UnderlyingInstrumentTypeCn, "显示应走真实类型");
Assert.AreNotEqual("商品期货", dto.UnderlyingInstrumentTypeCn, "不应再一律显示商品期货");
}
[TestMethod]
[Description("贵金属现货从商品期货表出来,也应显示真实种类")]
public void _显示黄金现货_而非商品期货()
{
var dto = new EodUnderlyingPriceDto
{
UnderlyingInstrumentType = ConsGlobal.InstrumentType.CommodityFutures,
RealInstrumentType = ConsGlobal.InstrumentType.GoldSpot
};
Assert.AreEqual("黄金现货", dto.UnderlyingInstrumentTypeCn);
Assert.AreNotEqual("商品期货", dto.UnderlyingInstrumentTypeCn);
}
[TestMethod]
[Description("真正的商品期货,真实类型=CommodityFutures,仍显示商品期货")]
public void _仍显示商品期货()
{
var dto = new EodUnderlyingPriceDto
{
UnderlyingInstrumentType = ConsGlobal.InstrumentType.CommodityFutures,
RealInstrumentType = ConsGlobal.InstrumentType.CommodityFutures
};
Assert.AreEqual("商品期货", dto.UnderlyingInstrumentTypeCn);
}
[TestMethod]
[Description("RealInstrumentType 为空时,回退到路由键,保证 null 安全不崩")]
public void _回退到路由键()
{
var dto = new EodUnderlyingPriceDto
{
UnderlyingInstrumentType = ConsGlobal.InstrumentType.CommodityFutures,
RealInstrumentType = null
};
Assert.AreEqual("商品期货", dto.UnderlyingInstrumentTypeCn, "?? 回退应等于路由键的中文");
}
[TestMethod]
[Description("路由键 UnderlyingInstrumentType 不受显示改动影响(保证'查看'不串表)")]
public void _保证查看不串表()
{
var dto = new EodUnderlyingPriceDto
{
UnderlyingInstrumentType = ConsGlobal.InstrumentType.CommodityFutures,
RealInstrumentType = ConsGlobal.InstrumentType.TBonds
};
// 显示变了,但路由键仍是 CommodityFutures → EodPriceView 仍会去 eod_commodity_future_price 取数
Assert.AreEqual("利率债", dto.UnderlyingInstrumentTypeCn);
Assert.AreEqual(ConsGlobal.InstrumentType.CommodityFutures, dto.UnderlyingInstrumentType);
}
#endregion
#region 3"中债估值"
[TestMethod]
[Description("债券来源恒为'中债估值',不随 JSID 是否为空变化(聚源仅转发中债,无人手工维护)")]
public void ()
{
// 无论是否有 JSID,债券来源都应是中债估值(对应 SearchUnderlyingList 后处理固定赋值)
var dtoWithJsid = new EodUnderlyingPriceDto { IsBond = true };
var dtoNoJsid = new EodUnderlyingPriceDto { IsBond = true };
// 生产逻辑在后处理统一置为 EodPriceBase.中债估值,这里直接断言常量值与约定一致
Assert.AreEqual("中债估值", EodPriceBase.);
Assert.AreEqual(EodPriceBase., dtoWithJsid.IsBond ? EodPriceBase. : "");
Assert.AreEqual(EodPriceBase., dtoNoJsid.IsBond ? EodPriceBase. : "");
}
[TestMethod]
[Description("来源常量应为约定的中文'人工'/'系统'/'中债估值'")]
public void ()
{
Assert.AreEqual("系统", EodPriceBase.);
Assert.AreEqual("人工", EodPriceBase.);
Assert.AreEqual("中债估值", EodPriceBase.);
}
#endregion
#region ()
[TestMethod]
[Description("新增债券估值:create_user 与 update_user 都写入当前登录用户ID")]
public void _写入创建人与更新人()
{
var m = new ChinaBondValuation();
EodPriceService.StampBondOperator(m, 1024, isNew: true);
Assert.AreEqual(1024L, m.create_user, "新增时应写创建人");
Assert.AreEqual(1024L, m.update_user, "新增时应写更新人");
}
[TestMethod]
[Description("更新已有债券估值:仅更新 update_user,保留原 create_user(不覆盖创建人)")]
public void _仅写更新人_保留创建人()
{
var m = new ChinaBondValuation { create_user = 7 };
EodPriceService.StampBondOperator(m, 1024, isNew: false);
Assert.AreEqual(7L, m.create_user, "更新时不应覆盖原创建人");
Assert.AreEqual(1024L, m.update_user, "更新人应为本次操作者");
}
[TestMethod]
[Description("聚源/中债自动同步(外部ETL)不调用 StampBondOperator,故 create_user/update_user 保持 NULL = 自动同步")]
public void _操作人列为NULL()
{
// 注意:SettlementPriceImportService 是"手工上传"入口(会戳操作人),不是自动同步。
// 真正的聚源/中债自动同步在外部 ETL(本仓库无代码),其写入不经 StampBondOperator。
var m = new ChinaBondValuation(); // 模拟自动同步:仅写价格字段,不戳操作人
Assert.IsNull(m.create_user);
Assert.IsNull(m.update_user);
}
[TestMethod]
[Description("手工上传(SettlementPriceImportService):新增行(id==0)应写 create_user+update_user,使来源列显示上传人")]
public void _写入创建人与更新人()
{
// 模拟上传债券新增分支:eodPrice.id 默认 0 → isNew=true
var m = new ChinaBondValuation();
EodPriceService.StampBondOperator(m, 2048, isNew: m.id == 0);
Assert.AreEqual(2048L, m.create_user, "上传新增应写创建人");
Assert.AreEqual(2048L, m.update_user, "上传新增应写更新人");
}
[TestMethod]
[Description("手工上传(SettlementPriceImportService):命中已有行(id!=0)只写 update_user,保留原 create_user")]
public void _仅写更新人_保留创建人()
{
// 模拟上传命中已有债券行:id!=0 → isNew=false
var m = new ChinaBondValuation { id = 55, create_user = 9 };
EodPriceService.StampBondOperator(m, 2048, isNew: m.id == 0);
Assert.AreEqual(9L, m.create_user, "上传更新不应覆盖原创建人");
Assert.AreEqual(2048L, m.update_user, "上传更新应写本次上传人");
}
#endregion
#region 3 "中债估值"
[TestMethod]
[Description("债券被手工改过(update_user 有值)且能解析到姓名 → 来源列显示改这个人名")]
public void _显示操作人姓名()
{
var nameMap = new Dictionary<long, string> { [1024] = "张三" };
Assert.AreEqual("张三", EodPriceService.ResolveBondDisplaySource(1024L, nameMap));
}
[TestMethod]
[Description("聚源自动同步(update_user 为 NULL) → 来源列固定显示'中债估值'")]
public void _显示中债估值()
{
var nameMap = new Dictionary<long, string> { [1024] = "张三" };
Assert.AreEqual(EodPriceBase., EodPriceService.ResolveBondDisplaySource(null, nameMap));
}
[TestMethod]
[Description("手工改过但解析不到姓名(用户已删除/字典为空) → 回退显示'中债估值',不崩")]
public void _回退中债估值()
{
var nameMap = new Dictionary<long, string> { [1024] = "张三" };
Assert.AreEqual(EodPriceBase., EodPriceService.ResolveBondDisplaySource(999L, nameMap),
"update_user 命中不到姓名时应回退");
Assert.AreEqual(EodPriceBase., EodPriceService.ResolveBondDisplaySource(7L, null),
"姓名字典为空时应回退");
}
#endregion
}
}
@@ -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
}
}
@@ -0,0 +1,51 @@
{
"Scenario": "标的种类与数据来源",
"Description": "合成 Mock:覆盖现券/贵金属/真期货/股票 的种类显示,以及债券来源固定为中债估值。回放守护'不再一律显示商品期货'+'路由键不变'+'债券来源=中债估值'。",
"Source": "synthetic",
"RecordedAt": null,
"Rows": [
{
"UnderlyingCode": "019547.IB",
"RouteKey": "CommodityFutures",
"RealInstrumentType": "CreditBonds",
"IsBond": true,
"ExpectedTypeCn": "信用债",
"ExpectedDataSource": "中债估值"
},
{
"UnderlyingCode": "220210.IB",
"RouteKey": "CommodityFutures",
"RealInstrumentType": "TBonds",
"IsBond": true,
"ExpectedTypeCn": "利率债",
"ExpectedDataSource": "中债估值"
},
{
"UnderlyingCode": "AU9999.SGE",
"RouteKey": "CommodityFutures",
"RealInstrumentType": "GoldSpot",
"IsBond": false,
"JSID": null,
"ExpectedTypeCn": "黄金现货",
"ExpectedDataSource": null
},
{
"UnderlyingCode": "IF2409",
"RouteKey": "CommodityFutures",
"RealInstrumentType": "CommodityFutures",
"IsBond": false,
"JSID": null,
"ExpectedTypeCn": "商品期货",
"ExpectedDataSource": null
},
{
"UnderlyingCode": "600000.SH",
"RouteKey": "Stock",
"RealInstrumentType": "Stock",
"IsBond": false,
"JSID": null,
"ExpectedTypeCn": "股票",
"ExpectedDataSource": null
}
]
}
+76 -20
View File
@@ -1,4 +1,6 @@
using BaseOUDAL;
using System.Collections.Generic;
using System.Linq;
using BaseOUDAL;
using DocumentFormat.OpenXml.Bibliography;
using NPOI.POIFS.NIO;
using YLErp.Helpers;
@@ -25,21 +27,15 @@ namespace YLErp.Modules.EodModule
var predicatEot = PredicateBuilder.Create<eod_stock_price>(source => source.ValueDate >= valueDtStart && source.ValueDate < valueDtEnd);
var predicatEob = PredicateBuilder.Create<ChinaBondValuation>(source => source.valuation_date >= valueDtStart && source.valuation_date < valueDtEnd);
// 数据来源常量(统一引用,避免各处手写"人工"/"系统"对不齐)
var srcManual = EodPriceBase.;
var srcSystem = EodPriceBase.;
if (!string.IsNullOrEmpty(req.DataSource))
{
predicatEoc = predicatEoc.And(d => d.DataSource.Contains(req.DataSource));
predicatEot = predicatEot.And(d => d.DataSource.Contains(req.DataSource));
if (req.DataSource=="系统")
// 债券来源恒为"中债估值"(聚源仅转发,无人手工维护),
// 只有按"中债估值"筛选时才命中债券;人工/系统筛选不命中债券。
if (req.DataSource != EodPriceBase.)
{
predicatEob= predicatEob.And(d => d.JSID!=null);
}
else
{
predicatEob = predicatEob.And(d => d.JSID==null);
predicatEob = predicatEob.And(d => false);
}
}
@@ -78,8 +74,7 @@ namespace YLErp.Modules.EodModule
SourceTime = source.SourceTime,
DeciClosePrice=0,
DeciSettlePrice = 0,
DeciReferencePrice=0,
JSID = null
DeciReferencePrice=0
};
var query2 = from un in queryUn
@@ -106,8 +101,7 @@ namespace YLErp.Modules.EodModule
SourceTime = stockClose.SourceTime,
DeciClosePrice = 0,
DeciSettlePrice = 0,
DeciReferencePrice = 0,
JSID=null
DeciReferencePrice = 0
};
var query3 = from un in queryUn
join bondClose in DbContext.china_bond_valuation.Where(predicatEob) on un.UnderlyingCode equals bondClose.bond_id
@@ -115,7 +109,8 @@ namespace YLErp.Modules.EodModule
{
IsBond = true,
id = bondClose.id,
DataSource = bondClose.JSID != null ? srcSystem : srcManual,
UpdateUser = bondClose.update_user,
// 债券 DataSource 在后处理统一置为固定值"中债估值"(见下方 foreach)。
LaunchState = un.LaunchState,
MarketName = un.MarketName,
UnderlyingId = un.id,
@@ -133,8 +128,7 @@ namespace YLErp.Modules.EodModule
UpdateTime = bondClose.update_time,
ReferencePrice=0,
DeciReferencePrice = bondClose.yield,
SourceTime="",
JSID=bondClose.JSID
SourceTime=""
};
var unionQuery = query1.Concat(query2);
var finalQuery = unionQuery.Concat(query3);
@@ -143,11 +137,29 @@ namespace YLErp.Modules.EodModule
req.sidx = "ValueDate";
req.sord = "desc";
}
var result= finalQuery.ToSearchList(req);
var result = finalQuery.ToSearchList(req);
// 解析手工改过估值的债券操作人姓名:update_user 非空 = 被人手工改过(聚源同步为 NULL)。
// 此时来源列显示改这个人名,而非"中债估值",以便溯源到具体操作人。
var manualBondRows = result.rows.Where(r => r.IsBond && r.UpdateUser.HasValue).ToList();
Dictionary<long, string> operatorNameMap = null;
if (manualBondRows.Any())
{
var operatorIds = manualBondRows.Select(r => r.UpdateUser.Value).Distinct().ToList();
using (var erpCtx = DbContextFactory.GetErpBaseContext())
{
operatorNameMap = erpCtx.SystemUsers.AsNoTracking()
.Where(u => operatorIds.Contains((long)u.Id))
.ToDictionary(u => (long)u.Id, u => u.Name);
}
}
foreach (var item in result.rows)
{
if (item.IsBond)
{
// 人工改动过则显示改这个人名,否则显示"中债估值"。
item.DataSource = ResolveBondDisplaySource(item.UpdateUser, operatorNameMap);
item.SourceTime = item.UpdateTime.HasValue? item.UpdateTime.Value.ToString("yyyy-MM-dd HH:mm:ss"):"";
item.SettlePrice=Convert.ToDouble(item.DeciSettlePrice);
item.ClosePrice = Convert.ToDouble(item.DeciClosePrice);
@@ -218,11 +230,50 @@ namespace YLErp.Modules.EodModule
}
UpdateChanges(dbmodel, req);
}
// 记录手工编辑人:写入登录用户ID到已有列(create_user/update_user)
// 不新增字段。聚源同步路径(SettlementPriceImportService)不写这两列,故 NULL 即"自动同步"。
StampBondOperator(dbmodel, UserId, req.id == 0);
dbmodel.update_time = DateTime.Now;
DbContext.SaveChanges();
return dbmodel;
}
/// <summary>
/// 标记债券估值(china_bond_valuation)的操作人。
/// 该表已有 create_user/update_user 两列(bigint),但聚源同步路径不写入,
/// 因此:NULL = 聚源/中债自动同步;有值 = 被人手工编辑(记录登录用户ID)。
/// 抽出为纯静态函数,供 SaveBondPrice 与单元测试共用。
/// </summary>
/// <param name="model">债券估值实体</param>
/// <param name="userId">当前登录用户ID</param>
/// <param name="isNew">是否为新增(true 时同时写 create_user)</param>
public static void StampBondOperator(ChinaBondValuation model, int userId, bool isNew)
{
model.update_user = userId;
if (isNew)
{
model.create_user = userId;
}
}
/// <summary>
/// 债券来源列该显示什么:
/// - update_user 有值(被人手工改过)且能解析到操作人姓名 → 显示改这个人名,便于溯源;
/// - 否则(聚源/中债自动同步,update_user 为 NULL)→ 固定显示"中债估值"。
/// 抽为纯静态函数,与解析姓名所需的 sys_user 查询解耦,便于无库单元测试。
/// </summary>
/// <param name="updateUser">china_bond_valuation.update_userNULL = 自动同步)</param>
/// <param name="operatorNameMap">update_user(ID) → 操作人姓名 的字典(由调用方按需从 sys_user 解析)</param>
public static string ResolveBondDisplaySource(long? updateUser, IDictionary<long, string> operatorNameMap)
{
if (updateUser.HasValue && operatorNameMap != null
&& operatorNameMap.TryGetValue(updateUser.Value, out var opName) && !string.IsNullOrEmpty(opName))
{
return opName;
}
return EodPriceBase.;
}
/// <summary>
/// 保存日终股票价格
/// </summary>
@@ -346,6 +397,11 @@ namespace YLErp.Modules.EodModule
public bool IsBond { get; set; }
public long? JSID { get; set; }
/// <summary>
/// 手工改过估值时的操作人IDchina_bond_valuation.update_user)。
/// NULL = 聚源/中债自动同步(无人手工维护);有值 = 被人手工改过、可溯源。
/// 仅债券行可能非空,用于列表来源列显示改这个人名而非"中债估值"。
/// </summary>
public long? UpdateUser { get; set; }
}
}
@@ -128,6 +128,10 @@ namespace YLErp.Modules.EodModule
if (item.ReferencePrice.HasValue)
eodPrice.yield = Convert.ToDecimal(item.ReferencePrice);
eodPrice.update_time = DateTime.Now;
// 手工上传也是登录用户的人工动作,戳操作人到已有列(create_user/update_user)
// 使列表来源列显示上传人姓名(与 SaveBondPrice 口径一致);聚源/中债自动同步(外部ETL)不写这两列。
// eodPrice.id==0 表示本次新增(尚未落库),非0为命中已有行的更新。
EodPriceService.StampBondOperator(eodPrice, UserId, eodPrice.id == 0);
result.SuccessCount++;
}
else if (!underlying.CalcTypeIsStock())