using BaseOUDAL; using YLErp.BLL; using YLErp.Helpers; namespace YLErp.Modules.EodModule { /// /// 日终价格服务 /// public class EodPriceService : YLBaseService { public EodPriceService(OptUserInfo userInfo) : base(userInfo) { } /// /// 解析日终价格列表的"估值日期"查询窗口。抽成 static 以便纯单测锁定行为(避免改坏)。 /// 规则: /// - 起始日期年份 > 2000(前端传了有效日期)→ 用传入值;否则回退到 今天-1年。 /// - 结束日期年份 > 2000 → 用传入值+1天(闭区间转半开);否则回退到 今天+1年。 /// 注意:列表页默认把起止都设成"今天",于是窗口=[今天, 今天+1天)=仅今天 → 仅返回当天的记录 /// (即"页面始终5条"现象的真正成因,非分页/查询 bug)。要看历史须把起始日期调早。 /// public static (DateTime start, DateTime end) ResolveValueDateWindow(DateTime reqStart, DateTime reqEnd) { var start = reqStart.Year > 2000 ? reqStart : DateTime.Today.AddYears(-1); var end = reqEnd.Year > 2000 ? reqEnd.AddDays(1) : DateTime.Today.AddYears(1); 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) /// 按 UnderlyingCode(string) JOIN。两列一旦失同步(典型如 FR007 的价格行 UnderlyingId 被错写成 511160.SH 的 id), /// 会出现"网页能查到、结算却查不到"的错价缺失,进而 EodCheckSettlePrice 报"结算价格缺失"。 /// 这里以 UnderlyingCode 为准重新派生 UnderlyingId——该列才是上传/结算使用的自然键(FutureContractId), /// 在入库前强制两列一致,既阻止产生新的错行,又通过告警日志把失同步暴露给运维追查上游写入来源。 /// public static void SyncUnderlyingIdFromCode(YLContext db, eod_commodity_future_price row) { if (row == null || string.IsNullOrWhiteSpace(row.UnderlyingCode)) { return; } var um = db.underlying_manager.FirstOrDefault(u => u.UnderlyingCode == row.UnderlyingCode); 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={before}, 修正为={row.UnderlyingId}"); } } public SearchListResult SearchUnderlyingList(EodCommodityFuturePriceReq req) { var (valueDtStart, valueDtEnd) = ResolveValueDateWindow(req.ValueDateStart, req.ValueDateEnd); var predicatUn = PredicateBuilder.Create(d => d.LaunchState == "1"); var predicatEoc = PredicateBuilder.Create(source => source.ValueDate >= valueDtStart && source.ValueDate < valueDtEnd); var predicatEot = PredicateBuilder.Create(source => source.ValueDate >= valueDtStart && source.ValueDate < valueDtEnd); var predicatEob = PredicateBuilder.Create(source => source.valuation_date >= valueDtStart && source.valuation_date < valueDtEnd); if (!string.IsNullOrEmpty(req.DataSource)) { predicatEoc = predicatEoc.And(d => d.DataSource.Contains(req.DataSource)); predicatEot = predicatEot.And(d => d.DataSource.Contains(req.DataSource)); // 债券来源:自动同步(中债, update_user 为空)归为"系统"、被手工改过的(update_user 非空)归为"人工"。 // 筛选须与后处理显示口径一致:按"系统"只命中 update_user 为空(中债自动同步)的债券; // 按"人工"只命中被手工改过(update_user 非空)的债券;其他来源值视为无效→无命中。 if (req.DataSource == EodPriceBase.人工) { predicatEob = predicatEob.And(d => d.update_user != null); } else if (req.DataSource == EodPriceBase.系统) { predicatEob = predicatEob.And(d => d.update_user == null); } else { predicatEob = predicatEob.And(d => false); } } if (!string.IsNullOrEmpty(req.MarketName)) { predicatUn = predicatUn.And(d => d.MarketName == req.MarketName); } if (!string.IsNullOrEmpty(req.UnderlyingCode)) { predicatUn = predicatUn.And(d => d.UnderlyingCode.Contains(req.UnderlyingCode)); } var queryUn = DbContext.underlying_manager.Where(predicatUn).Select(n => new { n.id, n.LaunchState, n.MarketName, n.UnderlyingState, n.UnderlyingType, n.UnderlyingCode, n.UnderlyingName,n.UnderlyingInstrumentType }); var query1 = from un in queryUn join source in DbContext.eod_commodity_future_price.Where(predicatEoc) on un.id equals source.UnderlyingId select new EodUnderlyingPriceDto { IsBond=false, id = source.id, DataSource = source.DataSource, // EF Core Concat 要求各分支投影成员集合完全一致: // 债券分支设了 UpdateUser,故期货/股票分支也必须显式设(置 null),否则翻译期抛 // "The given key 'UpdateUser/DataSource' was not present in the dictionary"。 UpdateUser = (long?)null, LaunchState = un.LaunchState, MarketName = un.MarketName, UnderlyingId = un.id, UnderlyingCode = un.UnderlyingCode, UnderlyingName = un.UnderlyingName, UnderlyingState = un.UnderlyingState, UnderlyingType = un.UnderlyingType, UnderlyingInstrumentType = "CommodityFutures", RealInstrumentType = un.UnderlyingInstrumentType, ValueDate = source.ValueDate, SettlePrice = source.SettlePrice, ClosePrice = source.ClosePrice, UpdateTime = source.OptDate, ReferencePrice = source.ReferencePrice, SourceTime = source.SourceTime, DeciClosePrice=0, DeciSettlePrice = 0, DeciReferencePrice=0 }; var query2 = from un in queryUn join stockClose in DbContext.eod_stock_price.Where(predicatEot) on un.UnderlyingCode equals stockClose.UnderlyingCode select new EodUnderlyingPriceDto { IsBond = false, id = stockClose.id, DataSource = stockClose.DataSource, UpdateUser = (long?)null, // 对齐 Concat 投影成员,见 query1 注释 LaunchState = un.LaunchState, MarketName = un.MarketName, UnderlyingId = un.id, UnderlyingCode = un.UnderlyingCode, UnderlyingName = un.UnderlyingName, UnderlyingState = un.UnderlyingState, UnderlyingType = un.UnderlyingType, UnderlyingInstrumentType = "Stock", RealInstrumentType = un.UnderlyingInstrumentType, ValueDate = stockClose.ValueDate, SettlePrice = stockClose.ClosePrice, ClosePrice = stockClose.ClosePrice, UpdateTime = stockClose.OptDate, ReferencePrice = stockClose.ReferencePrice, SourceTime = stockClose.SourceTime, DeciClosePrice = 0, DeciSettlePrice = 0, DeciReferencePrice = 0 }; var query3 = from un in queryUn join bondClose in DbContext.china_bond_valuation.Where(predicatEob) on un.UnderlyingCode equals bondClose.bond_id select new EodUnderlyingPriceDto { IsBond = true, id = bondClose.id, UpdateUser = bondClose.update_user, // 债券 DataSource 在后处理统一置为"人工"/"系统"(见下方 foreach); // 此处仍须显式设 null 以对齐 Concat 各分支投影成员集合(见 query1 注释)。 DataSource = null, LaunchState = un.LaunchState, MarketName = un.MarketName, UnderlyingId = un.id, UnderlyingCode = un.UnderlyingCode, UnderlyingName = un.UnderlyingName, UnderlyingState = un.UnderlyingState, UnderlyingType = un.UnderlyingType, UnderlyingInstrumentType = un.UnderlyingInstrumentType, RealInstrumentType = un.UnderlyingInstrumentType, ValueDate = bondClose.valuation_date, SettlePrice=0, DeciSettlePrice =bondClose.net_price, ClosePrice=0, DeciClosePrice = bondClose.dirty_price_close, UpdateTime = bondClose.update_time, ReferencePrice=0, DeciReferencePrice = bondClose.yield, SourceTime="" }; var unionQuery = query1.Concat(query2); var finalQuery = unionQuery.Concat(query3); if (string.IsNullOrEmpty(req.sidx)) { req.sidx = "ValueDate"; req.sord = "desc"; } var result = finalQuery.ToSearchList(req); foreach (var item in result.rows) { if (item.IsBond) { // 债券来源:被手工改过的(update_user 非空)→"人工";其余(中债自动同步)→"系统"。 item.DataSource = ResolveBondDisplaySource(item.UpdateUser); 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); item.ReferencePrice = Convert.ToDouble(item.DeciReferencePrice); } } return result; } /// /// 保存日终期货价格 /// public eod_commodity_future_price SaveEodFuturePrice(eod_commodity_future_price req) { if (req is null) { throw new ArgumentNullException(nameof(req)); } if (DbContext.eod_commodity_future_price.Any(n => n.id != req.id && n.UnderlyingCode == req.UnderlyingCode && n.ValueDate == req.ValueDate)) { throw new ServiceException("已存在相同估值日期,相同合约的数据"); } eod_commodity_future_price dbmodel; if (req.id == 0) { DbContext.eod_commodity_future_price.Add(dbmodel = req); } else { dbmodel = DbContext.eod_commodity_future_price.Find(req.id); if (dbmodel == null) { throw new ServiceException("数据不存在"); } UpdateChanges(dbmodel, req); } SetDBModelOpt(dbmodel); dbmodel.DataSource = EodPriceBase.人工; // 入库前强制 UnderlyingId 与 UnderlyingCode(FutureContractId) 一致,避免网页/结算两套 JOIN 失同步。 SyncUnderlyingIdFromCode(DbContext, dbmodel); DbContext.SaveChanges(); return dbmodel; } public ChinaBondValuation SaveBondPrice(ChinaBondValuation req) { if (req is null) { throw new ArgumentNullException(nameof(req)); } ChinaBondValuation dbmodel; if (req.id == 0) { DbContext.china_bond_valuation.Add(dbmodel = req); } else { dbmodel = DbContext.china_bond_valuation.Find(req.id); if (dbmodel == null) { throw new ServiceException("数据不存在"); } 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; } /// /// 标记债券估值(china_bond_valuation)的操作人。 /// 该表已有 create_user/update_user 两列(bigint),但聚源同步路径不写入, /// 因此:NULL = 聚源/中债自动同步;有值 = 被人手工编辑(记录登录用户ID)。 /// 抽出为纯静态函数,供 SaveBondPrice 与单元测试共用。 /// /// 债券估值实体 /// 当前登录用户ID /// 是否为新增(true 时同时写 create_user) public static void StampBondOperator(ChinaBondValuation model, int userId, bool isNew) { model.update_user = userId; if (isNew) { model.create_user = userId; } } /// /// 债券来源列该显示什么:被手工改过的(update_user 有值)→"人工";其余(中债自动同步)→"系统"。 /// 抽为纯静态函数,便于无库单元测试。 /// public static string ResolveBondDisplaySource(long? updateUser) { return updateUser.HasValue ? EodPriceBase.人工 : EodPriceBase.系统; } /// /// 保存日终股票价格 /// public eod_stock_price SaveEodStockPrice(eod_stock_price req) { if (req is null) { throw new ArgumentNullException(nameof(req)); } if (DbContext.eod_stock_price.Any(n => n.id != req.id && n.UnderlyingCode == req.UnderlyingCode && n.ValueDate == req.ValueDate)) { throw new ServiceException("已存在相同估值日期,相同合约的数据"); } eod_stock_price dbmodel; if (req.id == 0) { DbContext.eod_stock_price.Add(dbmodel = req); } else { dbmodel = DbContext.eod_stock_price.Find(req.id); if (dbmodel == null) { throw new ServiceException("数据不存在"); } UpdateChanges(dbmodel, req); } SetDBModelOpt(dbmodel); dbmodel.DataSource = EodPriceBase.人工; DbContext.SaveChanges(); return dbmodel; } } /// /// /// public class EodCommodityFuturePriceReq : BaseSearchReq { /// /// 数据来源 /// public string DataSource { get; set; } public string MarketName { get; set; } public string LaunchState { get; set; } /// /// 标的代码 /// public string UnderlyingCode { get; set; } public DateTime ValueDateStart { get; set; } public DateTime ValueDateEnd { get; set; } } public class EodUnderlyingPriceDto { public string EncryptId { get { return DataProtectHelper.Encrypt(id.ToString()); } } public long id { get; set; } public DateTime ValueDate { get; set; } public double SettlePrice { get; set; } public double ClosePrice { get; set; } public double? ReferencePrice { get; set; } public string DataSource { get; set; } public string UnderlyingType { get; set; } public string UnderlyingInstrumentType { get; set; } /// /// 真实标的种类(取自 underlying_manager),仅供列表"标的种类"列显示。 /// UnderlyingInstrumentType 仍作为"存储表路由键"使用,二者解耦,避免改动历史路由逻辑。 /// public string RealInstrumentType { get; set; } public string UnderlyingInstrumentTypeCn => ConsGlobal.InstrumentType.GetDesc(RealInstrumentType ?? UnderlyingInstrumentType); public string UnderlyingState { get; set; } public string MarketName { get; set; } public string LaunchState { get; set; } public int UnderlyingId { get; set; } public string UnderlyingCode { get; set; } public string UnderlyingName { get; set; } public DateTime? UpdateTime { get; set; } public string SourceTime { get; set; } public decimal? DeciSettlePrice { get; set; } public decimal? DeciClosePrice { get; set; } public decimal? DeciReferencePrice { get; set; } public bool IsBond { get; set; } /// /// 手工改过估值时的操作人ID(china_bond_valuation.update_user)。 /// NULL = 中债自动同步;有值 = 被人手工改过(来源列显示"人工")。 /// 仅债券行可能非空,用于列表来源列区分"人工"/"系统"。 /// public long? UpdateUser { get; set; } } }