Files
zszq-trs/YLErpDAL/Modules/EodModule/EodPriceService.cs
T
hjhan 760ffd62e6 fix: 日终债券来源固定中债估值、人工改动显示操作人并补上传溯源
china_bond_valuation 来源恒为中债估值(聚源仅转发,无人手工维护),不再按 JSID 推断系统/人工;eod 两表仍用自带 DataSource(人工/系统)。债券被人工改动(update_user 非空)时,日终列表来源列显示操作人姓名,否则显示中债估值;操作人经已有 create_user/update_user 列溯源(零 DDL)。手工上传 xlsx 的债券分支补戳操作人,与表单编辑口径一致。配套单测 EodPriceDtoTest / EodPriceGoldenReplayTest + golden JSON。
2026-07-13 11:00:33 +08:00

408 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using System.Linq;
using BaseOUDAL;
using DocumentFormat.OpenXml.Bibliography;
using NPOI.POIFS.NIO;
using YLErp.Helpers;
namespace YLErp.Modules.EodModule
{
/// <summary>
/// 日终价格服务
/// </summary>
public class EodPriceService : YLBaseService
{
public EodPriceService(OptUserInfo userInfo) : base(userInfo)
{
}
public SearchListResult<EodUnderlyingPriceDto> SearchUnderlyingList(EodCommodityFuturePriceReq req)
{
var valueDtStart = req.ValueDateStart.Year > 2000 ? req.ValueDateStart : DateTime.Today.AddYears(-1);
var valueDtEnd = req.ValueDateEnd.Year > 2000 ? req.ValueDateEnd.AddDays(1) : DateTime.Today.AddYears(1);
var predicatUn = PredicateBuilder.Create<underlying_manager>(d => d.LaunchState == "1");
var predicatEoc = PredicateBuilder.Create<eod_commodity_future_price>(source => source.ValueDate >= valueDtStart && source.ValueDate < valueDtEnd);
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);
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 != EodPriceBase.中债估值)
{
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,
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,
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)。
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);
// 解析手工改过估值的债券操作人姓名: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);
item.ReferencePrice = Convert.ToDouble(item.DeciReferencePrice);
}
}
return result;
}
/// <summary>
/// 保存日终期货价格
/// </summary>
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.人工;
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;
}
/// <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>
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;
}
}
/// <summary>
///
/// </summary>
public class EodCommodityFuturePriceReq : BaseSearchReq
{
/// <summary>
/// 数据来源
/// </summary>
public string DataSource { get; set; }
public string MarketName { get; set; }
public string LaunchState { get; set; }
/// <summary>
/// 标的代码
/// </summary>
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; }
/// <summary>
/// 真实标的种类(取自 underlying_manager),仅供列表"标的种类"列显示。
/// UnderlyingInstrumentType 仍作为"存储表路由键"使用,二者解耦,避免改动历史路由逻辑。
/// </summary>
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; }
/// <summary>
/// 手工改过估值时的操作人IDchina_bond_valuation.update_user)。
/// NULL = 聚源/中债自动同步(无人手工维护);有值 = 被人手工改过、可溯源。
/// 仅债券行可能非空,用于列表来源列显示改这个人名而非"中债估值"。
/// </summary>
public long? UpdateUser { get; set; }
}
}