From 5a51d6697001bff00fd4bcf253a4d24d98d982a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=90=8D=E9=94=90?= <1565842059@qq.com> Date: Thu, 13 Aug 2026 10:53:57 +0800 Subject: [PATCH] =?UTF-8?q?refactor(dividend):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E9=99=A4=E6=9D=83=E9=99=A4=E6=81=AF=E8=AE=A1=E7=AE=97=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E5=B9=B6=E6=94=B9=E8=BF=9B=E6=95=B0=E6=8D=AE=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E7=B2=BE=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将除权除息相关数值字段从 double 类型改为 decimal 类型以提高精度 - 重构了红利比率计算方法 GetRatio,新增 GetRatioDecimal 方法使用 decimal 计算 - 修改价格和持仓数量计算逻辑,统一使用 decimal 进行高精度运算 - 更新数据库查询逻辑,将篮子标的判断从 IsBasket() 方法改为 CommodityCode 条件 - 优化 AddDividendInfos 方法中的批量处理逻辑,增加业务键冲突检测 - 添加数据源标识字段 DataSource 和来源更新时间字段 SourceUpdatedAt - 新增 FindExDividendByBusinessKey 和 MergeNonZeroDividendValues 辅助方法 - 更新结算服务中除权除息信息的获取方式,使用字典查找替代 LINQ JOIN - 修复前端保存除权信息时的响应处理逻辑 - 为基金类型也开放除权功能,不仅限于股票类型 - 添加单元测试验证篮子标的查询翻译逻辑的正确性 --- .../DividendBasketQueryTranslationTest.cs | 68 +++++ .../EodModule/EodSettlementTaskTest.cs | 10 +- YLErpDAL/Model/ExDividendInfo.cs | 26 +- .../SettlementModule/EodSettlementService.cs | 26 +- .../TradeModule/DealModule/DividendService.cs | 233 ++++++++++++++---- .../Controllers/ex_dividend_infoController.cs | 4 + .../app/underlying/underlyingDividendInfo.js | 7 +- .../Scripts/app/underlying/underlyinglist.js | 2 +- 8 files changed, 296 insertions(+), 80 deletions(-) create mode 100644 UnitTestProject/Modules/EodModule/DividendBasketQueryTranslationTest.cs diff --git a/UnitTestProject/Modules/EodModule/DividendBasketQueryTranslationTest.cs b/UnitTestProject/Modules/EodModule/DividendBasketQueryTranslationTest.cs new file mode 100644 index 00000000..babd8e9e --- /dev/null +++ b/UnitTestProject/Modules/EodModule/DividendBasketQueryTranslationTest.cs @@ -0,0 +1,68 @@ +using System; +using System.IO; + +namespace YLErp.Modules.EodModule +{ + [TestClass] + public class DividendBasketQueryTranslationTest + { + [TestMethod] + public void DividendBasketQueriesUseEfTranslatableCommodityCondition() + { + var source = ReadDividendServiceSource(); + var addDividendQuery = ExtractQuery( + source, + "var basketList =", + "IEnumerable priceList = null;"); + var executeStatusQuery = ExtractQuery( + source, + "var umList = DataCacheProvider.GetUnderlyingDataSource().AsQueryable(", + ").Select(O => O.UnderlyingCode).ToArray();"); + + AssertQueryUsesCommodityCondition(addDividendQuery, "AddDividendInfos"); + AssertQueryUsesCommodityCondition(executeStatusQuery, "checkDividendInfoExecuteStatus"); + } + + private static void AssertQueryUsesCommodityCondition(string query, string methodName) + { + Assert.IsFalse( + query.Contains("IsBasket()", StringComparison.Ordinal), + $"{methodName} must not put IsBasket() in an IQueryable predicate."); + Assert.IsTrue( + query.Contains("O.CommodityCode == \"篮子标的\"", StringComparison.Ordinal), + $"{methodName} must filter baskets with the EF-translatable CommodityCode condition."); + } + + private static string ExtractQuery(string source, string startMarker, string endMarker) + { + var start = source.IndexOf(startMarker, StringComparison.Ordinal); + Assert.IsTrue(start >= 0, $"Could not find query marker: {startMarker}"); + var end = source.IndexOf(endMarker, start + startMarker.Length, StringComparison.Ordinal); + Assert.IsTrue(end >= 0, $"Could not find query end marker: {endMarker}"); + return source.Substring(start, end + endMarker.Length - start); + } + + private static string ReadDividendServiceSource() + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory != null) + { + var path = Path.Combine( + directory.FullName, + "YLErpDAL", + "Modules", + "TradeModule", + "DealModule", + "DividendService.cs"); + if (File.Exists(path)) + { + return File.ReadAllText(path); + } + directory = directory.Parent; + } + + Assert.Fail("Could not locate DividendService.cs from the test output directory."); + return string.Empty; + } + } +} diff --git a/UnitTestProject/Modules/EodModule/EodSettlementTaskTest.cs b/UnitTestProject/Modules/EodModule/EodSettlementTaskTest.cs index aaec3654..95b4d343 100644 --- a/UnitTestProject/Modules/EodModule/EodSettlementTaskTest.cs +++ b/UnitTestProject/Modules/EodModule/EodSettlementTaskTest.cs @@ -123,7 +123,7 @@ namespace YLErp.Modules.EodModule { UnderlyingCode = "002043.SZ", ExDividendDate = new DateTime(2020, 7, 6), - GiveCashAmount = 2.5, + GiveCashAmount = 2.5m, GiveShareAmount = 0, RationedSharesAmount = 0, RationedSharesPrice = 0, @@ -135,7 +135,7 @@ namespace YLErp.Modules.EodModule { UnderlyingCode = "600406.SH", ExDividendDate = new DateTime(2020, 7, 8), - GiveCashAmount = 2.9, + GiveCashAmount = 2.9m, GiveShareAmount = 0, RationedSharesAmount = 0, RationedSharesPrice = 0, @@ -147,7 +147,7 @@ namespace YLErp.Modules.EodModule { UnderlyingCode = "600406.SH", ExDividendDate = new DateTime(2020, 7, 8), - GiveCashAmount = 2.9, + GiveCashAmount = 2.9m, GiveShareAmount = 0, RationedSharesAmount = 0, RationedSharesPrice = 0, @@ -159,7 +159,7 @@ namespace YLErp.Modules.EodModule { UnderlyingCode = "601021.SH", ExDividendDate = new DateTime(2020, 7, 8), - GiveCashAmount = 2.0006, + GiveCashAmount = 2.0006m, GiveShareAmount = 0, RationedSharesAmount = 0, RationedSharesPrice = 0, @@ -171,7 +171,7 @@ namespace YLErp.Modules.EodModule { UnderlyingCode = "300001.SZ", ExDividendDate = new DateTime(2020, 7, 13), - GiveCashAmount = 0.2, + GiveCashAmount = 0.2m, GiveShareAmount = 0, RationedSharesAmount = 0, RationedSharesPrice = 0, diff --git a/YLErpDAL/Model/ExDividendInfo.cs b/YLErpDAL/Model/ExDividendInfo.cs index ae77cd50..bbf76a41 100644 --- a/YLErpDAL/Model/ExDividendInfo.cs +++ b/YLErpDAL/Model/ExDividendInfo.cs @@ -5,6 +5,12 @@ using System.ComponentModel.DataAnnotations.Schema; namespace YLErp.DBModels { + public static class ExDividendDataSources + { + public const string Manual = "Manual"; + public const string MarketData = "MarketData"; + } + [Table("ex_dividend_info")] public class ex_dividend_info : DBModelWithOperator { @@ -35,29 +41,41 @@ namespace YLErp.DBModels /// 派息金额 /// [DisplayName("派息金额")] - public double GiveCashAmount { get; set; } + public decimal GiveCashAmount { get; set; } /// /// 送股手数 /// [DisplayName("送股股数")] - public double GiveShareAmount { get; set; } + public decimal GiveShareAmount { get; set; } /// /// 配股手数 /// [DisplayName("配股股数")] - public double RationedSharesAmount { get; set; } + public decimal RationedSharesAmount { get; set; } /// /// 配股手数 /// [DisplayName("配股价")] - public double RationedSharesPrice { get; set; } + public decimal RationedSharesPrice { get; set; } /// /// 是否有效 /// public bool ValidStatus { get; set; } + + /// + /// Ownership of the record. Manual records always take precedence over imports. + /// + [DisplayName("数据来源"), Required, MaxLength(32)] + public string DataSource { get; set; } = ExDividendDataSources.Manual; + + /// + /// Last update timestamp supplied by the market-data provider. + /// + [DisplayName("来源更新时间")] + public DateTime? SourceUpdatedAt { get; set; } } public class ex_dividend_infoReq : BaseSearchReq diff --git a/YLErpDAL/Modules/EodModule/SettlementModule/EodSettlementService.cs b/YLErpDAL/Modules/EodModule/SettlementModule/EodSettlementService.cs index 83a6007e..f87252f7 100644 --- a/YLErpDAL/Modules/EodModule/SettlementModule/EodSettlementService.cs +++ b/YLErpDAL/Modules/EodModule/SettlementModule/EodSettlementService.cs @@ -44,9 +44,6 @@ namespace YLErp.Modules.EodModule on new { eod.BookId, eod.TradeType, eod.PositionType, eod.UnderlyingCode, ExchangeOptionCode = eod.ExchangeOptionCode ?? string.Empty } equals new { bod.BookId, bod.TradeType, bod.PositionType, bod.UnderlyingCode, ExchangeOptionCode = bod.ExchangeOptionCode ?? string.Empty } into t_bod from bod in t_bod.DefaultIfEmpty() - join dividend in DbContext.ex_dividend_info.Where(O => O.ExDividendDate == settleDate && O.ValidStatus) - on eod.UnderlyingCode equals dividend.UnderlyingCode into t_dividend - from dividend in t_dividend.DefaultIfEmpty() select new { eod, @@ -55,24 +52,25 @@ namespace YLErp.Modules.EodModule bod.Amount, bod.Cost, //bod.AveragePrice - }, - dividend + } }; var datas = query.ToArray(); var diviService = new TradeModule.DealModule.DividendService(OptUser); + var dividendDict = diviService.GetExDividendQuery(settleDate) + .ToDictionary(O => O.UnderlyingCode, O => O, StringComparer.OrdinalIgnoreCase); var eodPriceProvider = new EodPriceProvider(settleDate); return datas.Select(data => { var eod = data.eod; var bod = data.bod; - if (data.dividend != null) + if (dividendDict.TryGetValue(eod.UnderlyingCode, out var dividend)) { if (data.eod.TradeType == "股票") { var SettlePrice = eodPriceProvider.GetPrice(data.eod.UnderlyingCode, SettlementTypeEnum.ClosePrice); - SettlePrice = diviService.GetPrice(SettlePrice, data.dividend); - var amount = diviService.GetPositionAmount(data.eod.Amount, data.dividend); + SettlePrice = diviService.GetPrice(SettlePrice, dividend); + var amount = diviService.GetPositionAmount(data.eod.Amount, dividend); eod.Pv = eod.Pv > 0 ? Math.Abs(amount * SettlePrice) : -Math.Abs(amount * SettlePrice); } } @@ -109,9 +107,6 @@ namespace YLErp.Modules.EodModule on new { eod.BookId, eod.TradeType, eod.PositionType, eod.UnderlyingCode, ExchangeOptionCode = eod.ExchangeOptionCode ?? string.Empty } equals new { bod.BookId, bod.TradeType, bod.PositionType, bod.UnderlyingCode, ExchangeOptionCode = bod.ExchangeOptionCode ?? string.Empty } into t_bod from bod in t_bod.DefaultIfEmpty() - join dividend in DbContext.ex_dividend_info.AsNoTracking().Where(O => O.ExDividendDate == settleDate && O.ValidStatus) - on eod.UnderlyingCode equals dividend.UnderlyingCode into t_dividend - from dividend in t_dividend.DefaultIfEmpty() join risk in DbContext.Set().AsNoTracking().Where(n => n.ValueDate == settleDate && n.TradeId > 0) on new { eod.ValueDate, eod.TradeId } equals new { risk.ValueDate, risk.TradeId } into risk_t from risk in risk_t.DefaultIfEmpty() select new @@ -123,24 +118,25 @@ namespace YLErp.Modules.EodModule bod.Cost, //bod.AveragePrice }, - dividend, risk }; var datas = query.ToArray(); var diviService = new TradeModule.DealModule.DividendService(OptUser); + var dividendDict = diviService.GetExDividendQuery(settleDate) + .ToDictionary(O => O.UnderlyingCode, O => O, StringComparer.OrdinalIgnoreCase); var eodPriceProvider = new EodPriceProvider(settleDate); return datas.Select(data => { var pos = data.eod; var bod = data.bod; - if (data.dividend != null) + if (dividendDict.TryGetValue(pos.UnderlyingCode, out var dividend)) { if (data.eod.TradeType == "股票") { var settlePrice = eodPriceProvider.GetPrice(data.eod.UnderlyingCode, SettlementTypeEnum.ClosePrice); - settlePrice = diviService.GetPrice(settlePrice, data.dividend); - var amount = diviService.GetPositionAmount(data.eod.Amount, data.dividend); + settlePrice = diviService.GetPrice(settlePrice, dividend); + var amount = diviService.GetPositionAmount(data.eod.Amount, dividend); pos.Pv = pos.Pv > 0 ? Math.Abs(amount * settlePrice) : -Math.Abs(amount * settlePrice); } } diff --git a/YLErpDAL/Modules/TradeModule/DealModule/DividendService.cs b/YLErpDAL/Modules/TradeModule/DealModule/DividendService.cs index 7242618e..033c8227 100644 --- a/YLErpDAL/Modules/TradeModule/DealModule/DividendService.cs +++ b/YLErpDAL/Modules/TradeModule/DealModule/DividendService.cs @@ -28,7 +28,8 @@ namespace YLErp.Modules.TradeModule.DealModule public List Execute(DateTime settleDate, IEnumerable positions) { var result = new List(); - var dict = DbContext.ex_dividend_info.Where(O => O.ValidStatus && O.ExDividendDate == settleDate).ToDictionary(K => K.UnderlyingId, V => V); + var dict = GetExDividendQuery(settleDate) + .ToDictionary(K => K.UnderlyingId, V => V); foreach (var item in positions) { double cost = item.Cost, @@ -76,7 +77,8 @@ namespace YLErp.Modules.TradeModule.DealModule useSaveTrades = new List(); useSaveUndedrlyings = new List(); var result = new List(); - var dict = DbContext.ex_dividend_info.Where(O => O.ValidStatus && O.ExDividendDate == settleDate).ToDictionary(K => K.UnderlyingId, V => V); + var dict = GetExDividendQuery(settleDate) + .ToDictionary(K => K.UnderlyingId, V => V); var tradeIds = trades.Select(O => O.id); var dividendRatioDict = new DbRecordChangesService(this).GetValue(ConsInfoChangeType.UserChange, tradeIds, nameof(trade.DividendRatio), settleDate).ToDictionary(K => K.RecordId, V => { return double.TryParse(V.NewValue, out var temp) ? (double?)temp : null; }); foreach (var t in trades) @@ -713,9 +715,15 @@ namespace YLErp.Modules.TradeModule.DealModule { return 0; } - var ratio = overrideDividendRatio != null ? overrideDividendRatio.Value : GetRatio(info); - double? result = price / ratio; - return Math.Round(result ?? 0, 4, MidpointRounding.AwayFromZero); + var decimalRatio = overrideDividendRatio.HasValue + ? (decimal)overrideDividendRatio.Value + : GetRatioDecimal(info); + if (decimalRatio == 0) + { + return 0; + } + var result = (decimal)price / decimalRatio; + return (double)Math.Round(result, 4, MidpointRounding.AwayFromZero); } /// @@ -725,10 +733,17 @@ namespace YLErp.Modules.TradeModule.DealModule /// public double GetRatio(ex_dividend_info info) { - var dividendRate = valuedateBLL.SystemDate.DividendRate / 100; + return (double)GetRatioDecimal(info); + } + + private decimal GetRatioDecimal(ex_dividend_info info) + { + var dividendRate = (decimal)valuedateBLL.SystemDate.DividendRate / 100m; var closePrice = new EodPriceProvider(info.ExDividendDate.Value).GetPrice(info.UnderlyingCode, SettlementTypeEnum.ClosePrice); - var cDivdPrice = (closePrice * 10.0 - (info.GiveCashAmount * (1 - dividendRate)) + info.RationedSharesAmount * info.RationedSharesPrice) / (10 + info.GiveShareAmount + info.RationedSharesAmount); - return closePrice / cDivdPrice; + var decimalClosePrice = (decimal)closePrice; + var cDivdPrice = (decimalClosePrice * 10m - (info.GiveCashAmount * (1m - dividendRate)) + info.RationedSharesAmount * info.RationedSharesPrice) / + (10m + info.GiveShareAmount + info.RationedSharesAmount); + return cDivdPrice == 0 ? 0 : decimalClosePrice / cDivdPrice; } /// @@ -751,18 +766,20 @@ namespace YLErp.Modules.TradeModule.DealModule /// public double GetPositionAmount(double amount, ex_dividend_info info) { - double? result = amount * (1 + info.GiveShareAmount / 10.0); - return Math.Round(result ?? 0, 12); + var result = (decimal)amount * (1m + info.GiveShareAmount / 10m); + return (double)Math.Round(result, 12, MidpointRounding.AwayFromZero); } public IQueryable GetExDividendQuery(DateTime valueDate) { - return DbContext.ex_dividend_info.Where(O => O.ValidStatus && O.ExDividendDate == valueDate); + return DbContext.ex_dividend_info + .Where(O => O.ValidStatus && O.ExDividendDate == valueDate); } public IQueryable GetExDividendQuery(DateTime dateStart, DateTime dateEnd) { - return DbContext.ex_dividend_info.Where(O => O.ValidStatus && O.ExDividendDate >= dateStart && O.ExDividendDate <= dateEnd); + return DbContext.ex_dividend_info + .Where(O => O.ValidStatus && O.ExDividendDate >= dateStart && O.ExDividendDate <= dateEnd); } public IEnumerable GetExDividends(DateTime valueDate, params int[] underlyingIds) @@ -772,7 +789,7 @@ namespace YLErp.Modules.TradeModule.DealModule { query = query.Where(n => underlyingIds.Contains(n.UnderlyingId)); } - return query.ToArray(); + return query; } public IQueryable GetExDividendInfos(string underlyingCode) @@ -797,17 +814,17 @@ namespace YLErp.Modules.TradeModule.DealModule { throw new ServiceException("请使用正确的模板上传"); } - var dict = new Dictionary(); + var dividendInfos = new List(); for (var i = 0; i < dt.Rows.Count; i++) { var info = new ex_dividend_info { UnderlyingCode = dt.Rows[i]["股票代码"]?.ToString(), ExDividendDate = DateTime.TryParse(getColValueFromTable(dt.Rows[i], "股权登记日"), out var date) ? date : DateTime.MinValue, - GiveCashAmount = double.TryParse(getColValueFromTable(dt.Rows[i], "派息金额"), out var value) ? value : 0, - GiveShareAmount = double.TryParse(getColValueFromTable(dt.Rows[i], "送股股数"), out value) ? value : 0, - RationedSharesAmount = double.TryParse(getColValueFromTable(dt.Rows[i], "配股股数"), out value) ? value : 0, - RationedSharesPrice = double.TryParse(getColValueFromTable(dt.Rows[i], "配股股价"), out value) ? value : 0, + GiveCashAmount = decimal.TryParse(getColValueFromTable(dt.Rows[i], "派息金额"), out var value) ? value : 0, + GiveShareAmount = decimal.TryParse(getColValueFromTable(dt.Rows[i], "送股股数"), out value) ? value : 0, + RationedSharesAmount = decimal.TryParse(getColValueFromTable(dt.Rows[i], "配股股数"), out value) ? value : 0, + RationedSharesPrice = decimal.TryParse(getColValueFromTable(dt.Rows[i], "配股股价"), out value) ? value : 0, OptId = OptUser.UserId, OptName = OptUser.UserName, OptDate = DateTime.Now @@ -824,9 +841,9 @@ namespace YLErp.Modules.TradeModule.DealModule { throw new ServiceException($"第{i + 1}行股权登记日不正确"); } - dict[$"{info.ExDividendDate}{info.UnderlyingCode}"] = info; + dividendInfos.Add(info); } - if (!AddDividendInfos(dict.Values, out var errMsg)) + if (!AddDividendInfos(dividendInfos, out var errMsg)) { throw new ServiceException(errMsg); } @@ -841,48 +858,149 @@ namespace YLErp.Modules.TradeModule.DealModule return ""; } + private ex_dividend_info FindExDividendByBusinessKey(int underlyingId, DateTime exDividendDate, int excludedId = 0) + { + return DbContext.ex_dividend_info.FirstOrDefault(O => O.UnderlyingId == underlyingId + && O.ExDividendDate >= exDividendDate + && O.ExDividendDate < exDividendDate.AddDays(1) + && (excludedId <= 0 || O.id != excludedId)); + } + + private static void MergeNonZeroDividendValues(ex_dividend_info target, ex_dividend_info source) + { + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (source.GiveCashAmount != 0m) + { + target.GiveCashAmount = source.GiveCashAmount; + } + if (source.GiveShareAmount != 0m) + { + target.GiveShareAmount = source.GiveShareAmount; + } + if (source.RationedSharesAmount != 0m) + { + target.RationedSharesAmount = source.RationedSharesAmount; + } + if (source.RationedSharesPrice != 0m) + { + target.RationedSharesPrice = source.RationedSharesPrice; + } + } + public bool AddDividendInfos(IEnumerable infos, out string errMsg) { try { - var keys = infos.Select(O => $"{O.ExDividendDate?.ToString("yyyy-MM-dd")}{O.UnderlyingCode}"); - var ids = infos.Select(O => O.id).ToHashSet(); - - var data = from dividendDb in DbContext.ex_dividend_info.Where(O => keys.Contains(O.ExDividendDate + O.UnderlyingCode) && O.ValidStatus) - where !ids.Contains(dividendDb.id) - select dividendDb; - if (data.Any()) + var dividendInfos = infos?.ToList(); + if (dividendInfos == null || dividendInfos.Count == 0) { - var dd = data.Select(O => O.UnderlyingCode + "_" + O.ExDividendDate).ToArray(); - errMsg = string.Join(",", dd) + "已存在除息信息,请修改原数据"; + errMsg = "没有可保存的除权除息信息"; return false; } - var basketList = - DataCacheProvider.GetUnderlyingDataSource() - .AsQueryable().Where(O => O.IsBasket() && O.SubData != null) - .Select(O => new { O.UnderlyingCode, O.SubData }); - IEnumerable priceList = null; - foreach (var item in infos) + var preparedInfos = new List<(ex_dividend_info Item, underlying_manager Underlying, DateTime ExDividendDate)>(); + var preparedIndexes = new Dictionary<(int UnderlyingId, DateTime ExDividendDate), int>(); + var recordKeys = new Dictionary(); + foreach (var item in dividendInfos) { + if (item == null || string.IsNullOrWhiteSpace(item.UnderlyingCode)) + { + errMsg = "标的代码信息不存在"; + return false; + } + var underlying = underlying_managerBLL.GetByCode(item.UnderlyingCode); if (underlying == null) { errMsg = $"{item.UnderlyingCode} 标的信息不存在"; return false; } + if (!item.ExDividendDate.HasValue) + { + errMsg = "股权登记日信息不存在"; + return false; + } + + var exDividendDate = item.ExDividendDate.Value.Date; + var businessKey = (underlying.id, exDividendDate); + if (item.id > 0 + && recordKeys.TryGetValue(item.id, out var existingRecordKey) + && existingRecordKey != businessKey) + { + errMsg = "同一除权信息不能重复保存"; + return false; + } + item.UnderlyingId = underlying.id; - item.GiveCashAmount = item.GiveCashAmount.FormatValue(6); - item.RationedSharesAmount = item.RationedSharesAmount.FormatValue(6); - item.RationedSharesPrice = item.RationedSharesPrice.FormatValue(6); - item.GiveShareAmount = item.GiveShareAmount.FormatValue(6); - item.ValidStatus = true; - item.OptId = OptUser.UserId; - item.OptName = OptUser.UserName; - item.OptDate = DateTime.Now; - var dividend = item.id > 0 ? DbContext.ex_dividend_info.Where(O => O.id == item.id).FirstOrDefault() : null; + item.ExDividendDate = exDividendDate; + item.GiveCashAmount = OtcFormatHelper.FormatValue(item.GiveCashAmount, 6); + item.RationedSharesAmount = OtcFormatHelper.FormatValue(item.RationedSharesAmount, 6); + item.RationedSharesPrice = OtcFormatHelper.FormatValue(item.RationedSharesPrice, 6); + item.GiveShareAmount = OtcFormatHelper.FormatValue(item.GiveShareAmount, 6); + + if (preparedIndexes.TryGetValue(businessKey, out var preparedIndex)) + { + var preparedItem = preparedInfos[preparedIndex].Item; + // 批量保存除权信息时,检测同一业务键(标的+日期)下是否存在冲突的数据库记录。 + if ((preparedItem.id == 0) != (item.id == 0) + || preparedItem.id > 0 && item.id > 0 && preparedItem.id != item.id) + { + errMsg = $"{item.UnderlyingCode} {exDividendDate:yyyy-MM-dd}除权信息不能合并不同记录"; + return false; + } + + MergeNonZeroDividendValues(preparedItem, item); + if (item.id > 0) + { + recordKeys[item.id] = businessKey; + } + continue; + } + + if (item.id > 0) + { + recordKeys[item.id] = businessKey; + } + preparedIndexes.Add(businessKey, preparedInfos.Count); + preparedInfos.Add((item, underlying, exDividendDate)); + } + + var basketList = + DataCacheProvider.GetUnderlyingDataSource() + .AsQueryable().Where(O => O.CommodityCode == "篮子标的" && O.SubData != null) + .Select(O => new { O.UnderlyingCode, O.SubData }); + IEnumerable priceList = null; + foreach (var prepared in preparedInfos) + { + var item = prepared.Item; + var underlying = prepared.Underlying; + var itemDate = prepared.ExDividendDate; + var dividend = item.id > 0 + ? DbContext.ex_dividend_info.FirstOrDefault(O => O.id == item.id) + : FindExDividendByBusinessKey(underlying.id, itemDate); if (dividend == null) - { DbContext.ex_dividend_info.Add(item); } + { + if (item.id > 0) + { + errMsg = "未找到要修改的除权除息信息"; + return false; + } + item.DataSource = ExDividendDataSources.Manual; + item.SourceUpdatedAt = null; + item.ValidStatus = true; + item.OptId = OptUser.UserId; + item.OptName = OptUser.UserName; + item.OptDate = DateTime.Now; + DbContext.ex_dividend_info.Add(item); + } else { if (checkDividendInfoExecuteStatus(dividend)) @@ -890,6 +1008,13 @@ namespace YLErp.Modules.TradeModule.DealModule errMsg = $"{dividend.UnderlyingCode} {dividend.ExDividendDate?.ToString("yyyy-MM-dd")}除权信息保存失败,该信息已被执行,不允许修改!"; return false; } + var conflictingDividend = FindExDividendByBusinessKey(underlying.id, itemDate, dividend.id); + if (conflictingDividend != null) + { + errMsg = $"{item.UnderlyingCode} {itemDate:yyyy-MM-dd}除权信息已存在,不能修改为该业务键"; + return false; + } + var sourceUpdatedAt = dividend.SourceUpdatedAt; dividend.UnderlyingCode = item.UnderlyingCode; dividend.UnderlyingId = item.UnderlyingId; dividend.ExDividendDate = item.ExDividendDate; @@ -897,21 +1022,23 @@ namespace YLErp.Modules.TradeModule.DealModule dividend.RationedSharesAmount = item.RationedSharesAmount; dividend.RationedSharesPrice = item.RationedSharesPrice; dividend.GiveShareAmount = item.GiveShareAmount; - dividend.ValidStatus = item.ValidStatus; - dividend.OptId = item.OptId; - dividend.OptName = item.OptName; - dividend.OptDate = item.OptDate; + dividend.ValidStatus = true; + dividend.DataSource = ExDividendDataSources.Manual; + dividend.SourceUpdatedAt = sourceUpdatedAt; + dividend.OptId = OptUser.UserId; + dividend.OptName = OptUser.UserName; + dividend.OptDate = DateTime.Now; } if (!basketList.Any()) { continue; } - var codes = basketList.Where(O => O.SubData.Contains(item.UnderlyingCode)).Select(O => O.UnderlyingCode); - if (!codes.Any()) + var basketCodes = basketList.Where(O => O.SubData.Contains(item.UnderlyingCode)).Select(O => O.UnderlyingCode); + if (!basketCodes.Any()) { continue; } - var removePriceList = DbContext.eod_stock_price.Where(O => codes.Contains(O.UnderlyingCode) && O.ValueDate > item.ExDividendDate); + var removePriceList = DbContext.eod_stock_price.Where(O => basketCodes.Contains(O.UnderlyingCode) && O.ValueDate > item.ExDividendDate); if (!removePriceList.Any()) { continue; @@ -954,7 +1081,7 @@ namespace YLErp.Modules.TradeModule.DealModule return true; } //查询篮子标的对应交易是否执行过收盘操作; - var umList = DataCacheProvider.GetUnderlyingDataSource().AsQueryable(O => O.IsBasket() && O.SubData != null && O.SubData.Contains(info.UnderlyingCode)).Select(O => O.UnderlyingCode).ToArray(); + var umList = DataCacheProvider.GetUnderlyingDataSource().AsQueryable(O => O.CommodityCode == "篮子标的" && O.SubData != null && O.SubData.Contains(info.UnderlyingCode)).Select(O => O.UnderlyingCode).ToArray(); tradeQuery = from t in DbContext.trade.Where(O => umList.Contains(O.UnderlyingCode) && O.TradeDate <= info.ExDividendDate && O.ExerciseDate >= info.ExDividendDate && O.DividendDate >= O.TradeDate) join et in DbContext.eod_trade.Where(O => ConsTrade.LiveTradeStatusList.Contains(O.TradeStatus)) on new { t.id, ValueDate = t.TradeDate.Value } equals new { id = et.TradeId, et.ValueDate } diff --git a/YLErpWeb/Controllers/ex_dividend_infoController.cs b/YLErpWeb/Controllers/ex_dividend_infoController.cs index 0783d33a..a366b8ae 100644 --- a/YLErpWeb/Controllers/ex_dividend_infoController.cs +++ b/YLErpWeb/Controllers/ex_dividend_infoController.cs @@ -98,6 +98,10 @@ namespace YLErp.Web.Controllers else { r.ValidStatus = false; + r.DataSource = ExDividendDataSources.Manual; + r.OptId = CurUser.UserId; + r.OptName = CurUser.UserName; + r.OptDate = DateTime.Now; yldb.SaveChanges(); return JsonSuccess("删除成功"); } diff --git a/YLErpWeb/wwwroot/Scripts/app/underlying/underlyingDividendInfo.js b/YLErpWeb/wwwroot/Scripts/app/underlying/underlyingDividendInfo.js index 02645196..8039367f 100644 --- a/YLErpWeb/wwwroot/Scripts/app/underlying/underlyingDividendInfo.js +++ b/YLErpWeb/wwwroot/Scripts/app/underlying/underlyingDividendInfo.js @@ -28,10 +28,13 @@ function saveInfo(dataId, rowId) { g_grid.jqGrid('saveRow', rowId, { successfunc: function (response) { - var msg = response.responseJSON.msg; + var result = response.responseJSON || {}; + var msg = result.msg || "保存失败"; main.message(msg); $("#systemTip").text(new Date().toLocaleString() + " " + msg); + if (!result.success) return false; g_grid.trigger('reloadGrid'); + return true; }, "url": "/ex_dividend_info/SaveDividend", "extraparam": data, @@ -150,4 +153,4 @@ $(function () { onPaging: onJqgridPaging }; g_grid = jQuery('#listGrid').jqGrid(obj); -}); \ No newline at end of file +}); diff --git a/YLErpWeb/wwwroot/Scripts/app/underlying/underlyinglist.js b/YLErpWeb/wwwroot/Scripts/app/underlying/underlyinglist.js index a2554cd8..6e61fece 100644 --- a/YLErpWeb/wwwroot/Scripts/app/underlying/underlyinglist.js +++ b/YLErpWeb/wwwroot/Scripts/app/underlying/underlyinglist.js @@ -164,7 +164,7 @@ $(function () { //设置除权 function SetDividEnd(cellValue, options, rowObject) { - if (rowObject.UnderlyingInstrumentType !== "Stock" || rowObject.CommodityCode === "篮子标的" || !g_dividend) return ""; + if (["Stock", "Fund"].indexOf(rowObject.UnderlyingInstrumentType) < 0 || rowObject.CommodityCode === "篮子标的" || !g_dividend) return ""; var imageHtml = ""; return imageHtml; }