Files
zszq-trs/YLErpDAL/Modules/EodModule/EodPriceService.cs
T
hjhan 0fda5fd1cc fix: 日终价格「数据来源」改为价格行自身语义(人工/系统两档)
移除中债估值细分与操作人姓名显示,按价格行自身 DataSource 列判定:期货/股票读 DataSource(otc-marketdata 同步写系统、手工写人工);债券 update_user 非空→人工、否则→系统。筛选仅人工/系统两选项,异常来源值返回空。ResolveBondDisplaySource 精简为单参纯函数。
2026-07-13 15:42:28 +08:00

410 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)
{
}
/// <summary>
/// 解析日终价格列表的"估值日期"查询窗口。抽成 static 以便纯单测锁定行为(避免改坏)。
/// 规则:
/// - 起始日期年份 &gt; 2000(前端传了有效日期)→ 用传入值;否则回退到 今天-1年。
/// - 结束日期年份 &gt; 2000 → 用传入值+1天(闭区间转半开);否则回退到 今天+1年。
/// 注意:列表页默认把起止都设成"今天",于是窗口=[今天, 今天+1天)=仅今天 → 仅返回当天的记录
/// (即"页面始终5条"现象的真正成因,非分页/查询 bug)。要看历史须把起始日期调早。
/// </summary>
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);
}
public SearchListResult<EodUnderlyingPriceDto> SearchUnderlyingList(EodCommodityFuturePriceReq req)
{
var (valueDtStart, valueDtEnd) = ResolveValueDateWindow(req.ValueDateStart, req.ValueDateEnd);
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));
// 债券来源:自动同步(中债)归为"系统"、被手工改过的(update_user 非空)归为"人工"。
// 故按"系统"筛选命中全部债券;按"人工"筛选只命中被手工改过的债券。
if (req.DataSource == EodPriceBase.人工)
{
predicatEob = predicatEob.And(d => d.update_user != null);
}
else 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,
// 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;
}
/// <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 有值)→"人工";其余(中债自动同步)→"系统"。
/// 抽为纯静态函数,便于无库单元测试。
/// </summary>
public static string ResolveBondDisplaySource(long? updateUser)
{
return updateUser.HasValue ? EodPriceBase.人工 : 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; }
}
}