diff --git a/Framework/YLErp.Core/DBModels/margin_template_v2.cs b/Framework/YLErp.Core/DBModels/margin_template_v2.cs index bf4a5718..f58cb003 100644 --- a/Framework/YLErp.Core/DBModels/margin_template_v2.cs +++ b/Framework/YLErp.Core/DBModels/margin_template_v2.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations.Schema; +using System.Globalization; using YLErp.Enums; namespace YLErp.DBModels @@ -17,6 +18,109 @@ namespace YLErp.DBModels [DisplayName("模板名称")] public string Name { get; set; } + /// + /// 适用簿记账户ID列表,逗号分隔;为空表示适用全部簿记账户。 + /// + [DisplayName("适用簿记账户组")] + [Column("book_id")] + public string BookIds { get; set; } + + /// + /// 将簿记账户ID列表规范化为去空格、去重的逗号分隔字符串。 + /// + public static string NormalizeBookIds(string bookIds) + { + if (string.IsNullOrWhiteSpace(bookIds)) + { + return null; + } + + var ids = new List(); + foreach (var rawId in bookIds.Split(new[] { ',' }, StringSplitOptions.None)) + { + var token = rawId.Trim(); + if (token.Length == 0) + { + throw new ArgumentException("适用簿记账户组包含空账户ID。", nameof(bookIds)); + } + if (!int.TryParse(token, NumberStyles.None, CultureInfo.InvariantCulture, out var id) || id <= 0) + { + throw new ArgumentException("适用簿记账户组ID必须为正整数:" + token, nameof(bookIds)); + } + if (!ids.Contains(id)) + { + ids.Add(id); + } + } + + return ids.Count == 0 ? null : string.Join(",", ids); + } + + /// + /// 解析簿记账户ID列表。结果使用完整token,避免"1"误命中"11"。 + /// + public static HashSet ParseBookIds(string bookIds) + { + var ids = new HashSet(StringComparer.Ordinal); + if (string.IsNullOrWhiteSpace(bookIds)) + { + return ids; + } + + foreach (var rawId in bookIds.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) + { + var token = rawId.Trim(); + if (int.TryParse(token, NumberStyles.None, CultureInfo.InvariantCulture, out var id) && id > 0) + { + ids.Add(id.ToString(CultureInfo.InvariantCulture)); + } + } + return ids; + } + + /// + /// 判断模板是否适用于指定簿记账户;空范围表示全部。 + /// + public bool IsApplicableToBook(int? bookId) + { + if (string.IsNullOrWhiteSpace(BookIds)) + { + return true; + } + + return bookId.HasValue && ParseBookIds(BookIds).Contains(bookId.Value.ToString(CultureInfo.InvariantCulture)); + } + + /// + /// 判断两个簿记账户范围是否重叠;任一空范围均视为全部。 + /// + public static bool AreBookScopesOverlapping(string firstBookIds, string secondBookIds) + { + if (string.IsNullOrWhiteSpace(firstBookIds) || string.IsNullOrWhiteSpace(secondBookIds)) + { + return true; + } + + var first = ParseBookIds(firstBookIds); + var second = ParseBookIds(secondBookIds); + return first.Count > 0 && second.Count > 0 && first.Overlaps(second); + } + + /// + /// 收集与新模板簿记账户范围重叠的所有模板适用结构。 + /// + public static IEnumerable GetOverlappingTradeTypes(IEnumerable candidates, string bookIds) + { + if (candidates == null) + { + return Enumerable.Empty(); + } + + return candidates + .Where(x => x != null && AreBookScopesOverlapping(x.BookIds, bookIds)) + .SelectMany(x => (x.TradeTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)); + } + /// /// 单腿/多腿 /// diff --git a/YLErpDAL/AppManager.cs b/YLErpDAL/AppManager.cs index 0b244c2d..609c870a 100644 --- a/YLErpDAL/AppManager.cs +++ b/YLErpDAL/AppManager.cs @@ -171,6 +171,7 @@ namespace YLErp "yladmin" => _configuration.GetConnectionString("yladmin"), "ylclient" => _configuration.GetConnectionString("ylclient"), "bondoms" => _configuration.GetConnectionString("bondoms"), + "glms_bigdata" => _configuration.GetConnectionString("glms_bigdata"), "apex_oracle"=> _configuration.GetConnectionString("apex_oracle"), _ => string.Empty, }; diff --git a/YLErpDAL/Modules/MarginModule/MarginTemplateV2RateHelper.cs b/YLErpDAL/Modules/MarginModule/MarginTemplateV2RateHelper.cs index f29641a3..99e47346 100644 --- a/YLErpDAL/Modules/MarginModule/MarginTemplateV2RateHelper.cs +++ b/YLErpDAL/Modules/MarginModule/MarginTemplateV2RateHelper.cs @@ -13,12 +13,20 @@ namespace YLErp.Modules.MarginModule /// /// 预付金模板V2 取数帮助类:按交易绑定的模板(trade_margin_template → margin_template_v2 → margin_template_detail) /// 取 初始预付金率x / 维持预付金率y,供交易确认书、预付金计算等消费点统一使用。 - /// 匹配维度:生效日期(ValueDate 最新)→ 利率债期限档(SpanConfig.BondTerm,四档,空=全部兜底)→ 标的资产类型(UnderlyingType 标志位)。 + /// 匹配维度:簿记账户范围(BookIds,空=全部)→ 生效日期(ValueDate 最新)→ + /// 利率债期限档(SpanConfig.BondTerm,四档,空=全部兜底)→ 标的资产类型(UnderlyingType 标志位)。 /// public static class MarginTemplateV2RateHelper { private static readonly IYcLogger logger = LogFactory.GetLogger("预付金模板取数"); + private sealed class ClientTemplateCandidate + { + public int ClientId { get; set; } + public string ClientLevel { get; set; } + public margin_template_v2 Template { get; set; } + } + /// /// 取数结果。率为小数(如 0.1 表示 10%,与数据库一致),调用方直接用。 /// @@ -45,6 +53,19 @@ namespace YLErp.Modules.MarginModule public decimal? MaintainRate { get; set; } } + internal static int? GetTradeAssetId(int? tradeId, YLContext db) + { + if (!tradeId.HasValue || tradeId.Value <= 0) + { + return null; + } + + return db.trade.AsNoTracking() + .Where(x => x.id == tradeId.Value) + .Select(x => (int?)x.AssetId) + .FirstOrDefault(); + } + /// /// 按交易绑定的模板取 初始/维持预付金率。 /// @@ -182,11 +203,13 @@ namespace YLErp.Modules.MarginModule /// 绑定指向已失效模板时同样停止(不向下回退,避免"改绑定失效后悄悄换成别的规则"),返回 null 并留日志; /// 2.客户默认 client_margin_template(按客户 ClientId 或按客户等级 ClientId=0+ClientLevel, /// 客户等级经 client.LevelId → clientlevel.LevelName 解析;ValueDate 最新 ≤ 业务日), - /// 模板须有效且适用结构含收益互换; - /// 3.全局默认 margin_template_v2(IsDefault && !IsForClient && IsValid && 适用收益互换,ValueDate 最新)。 + /// 模板须有效、适用结构含收益互换且适用当前交易簿记账户; + /// 3.全局默认 margin_template_v2(IsDefault && !IsForClient && IsValid && 适用收益互换,且适用当前簿记账户,ValueDate 最新)。 /// public static margin_template_v2 ResolveTieredTemplate(int? tradeId, int clientId, DateTime valueDate, YLContext db) { + var tradeAssetId = GetTradeAssetId(tradeId, db); + //1.交易绑定(找到即停) if (tradeId > 0) { @@ -199,10 +222,15 @@ namespace YLErp.Modules.MarginModule if (bindingTemplateId > 0) { var bound = db.margin_template_v2.AsNoTracking().FirstOrDefault(x => x.id == bindingTemplateId.Value && x.IsValid); - if (bound != null) + if (bound != null && bound.IsApplicableToBook(tradeAssetId)) { return bound; } + if (bound != null) + { + logger.Info($"【警告】预付金模板取数:交易{tradeId}绑定的模板{bindingTemplateId}不适用簿记账户{tradeAssetId},按找到即停不向下回退"); + return null; + } logger.Info($"【警告】预付金模板取数:交易{tradeId}绑定的模板{bindingTemplateId}已失效或不存在,按找到即停不向下回退"); return null; } @@ -229,10 +257,12 @@ namespace YLErp.Modules.MarginModule && (cmt.ClientId == clientId || (cmt.ClientId == 0 && cmt.ClientLevel != "" && cmt.ClientLevel == levelName)) && mt.IsValid && mt.TradeTypes.Contains("收益互换") orderby cmt.ValueDate descending, cmt.id descending - select mt).FirstOrDefault(); - if (clientTemplate != null) + select mt).ToList(); + + var ret = clientTemplate.FirstOrDefault(x => x.IsApplicableToBook(tradeAssetId)); + if (ret != null) { - return clientTemplate; + return ret; } } @@ -241,7 +271,8 @@ namespace YLErp.Modules.MarginModule .Where(x => x.IsDefault && !x.IsForClient && x.IsValid && x.TradeTypes.Contains("收益互换") && x.ValueDate <= valueDate) .OrderByDescending(x => x.ValueDate) .ThenByDescending(x => x.id) - .FirstOrDefault(); + .ToList() + .FirstOrDefault(x => x.IsApplicableToBook(tradeAssetId)); } /// @@ -249,8 +280,8 @@ namespace YLErp.Modules.MarginModule /// 只配了客户默认/全局默认模板的交易与交易级绑定同权生效): /// 语义与单笔 ResolveTieredTemplate 一致—— /// 1.交易绑定(ValueDate 最新 ≤ 业务日,同日多条按 id 最新兜底)找到即停,绑定指向失效模板同样停止(不回退); - /// 2.客户默认(client_margin_template 按客户或按客户等级,模板须有效且适用结构含收益互换,先过滤有效再取绑定最新); - /// 3.全局默认(IsDefault&&!IsForClient&&IsValid&&适用收益互换,ValueDate/id 最新)。 + /// 2.客户默认(client_margin_template 按客户或按客户等级,模板须有效、适用结构含收益互换且适用当前簿记账户,先过滤有效再取绑定最新); + /// 3.全局默认(IsDefault&&!IsForClient&&IsValid&&适用收益互换,按每笔交易簿记账户取ValueDate/id 最新)。 /// 返回 tradeId → 命中模板;未命中或因失效绑定停止的交易不在结果中。 /// public static Dictionary ResolveTieredTemplates(List trades, DateTime valueDate, YLContext db) @@ -282,10 +313,10 @@ namespace YLErp.Modules.MarginModule //2.客户默认(client_margin_template:按客户 或 按客户等级;客户表在独立客户库,等级名批量解析) var clientIds = trades.Select(t => t.ClientId).Where(c => c > 0).Distinct().ToList(); - var clientTemplateByClient = new Dictionary(); + var levelNames = new Dictionary(); + var clientTemplateCandidates = new List(); if (clientIds.Any()) { - var levelNames = new Dictionary(); using (var clientDb = DbContextFactory.GetClientDbContext(OptUserInfo.SystemUser)) { var clientLevels = clientDb.client.AsNoTracking() @@ -305,33 +336,28 @@ namespace YLErp.Modules.MarginModule } //先按 有效+适用收益互换 过滤再按绑定取最新(与单笔解析一致,同日互换/期权并存不误穿透) - var clientBindingRows = (from cmt in db.client_margin_template.AsNoTracking() - join mt in db.margin_template_v2.AsNoTracking() on cmt.MarginTemplateId equals mt.id - where cmt.ValueDate <= valueDate && cmt.MarginTemplateId > 0 - && (clientIds.Contains(cmt.ClientId) || (cmt.ClientId == 0 && cmt.ClientLevel != "")) - && mt.IsValid && mt.TradeTypes.Contains("收益互换") - orderby cmt.ValueDate descending, cmt.id descending - select new { cmt.ClientId, cmt.ClientLevel, Template = mt }).ToList(); - foreach (var clientId in clientIds) - { - var levelName = levelNames.TryGetValue(clientId, out var name) ? name : null; - //每客户找到即停:按客户 或 按客户等级 同查询取绑定最新(与单笔解析一致; - //直接绑定与等级绑定并存的优先级方案未定义(BUG-16),维持"绑定日期最新"现状不改语义) - var hit = clientBindingRows - .FirstOrDefault(r => r.ClientId == clientId || (r.ClientId == 0 && r.ClientLevel == levelName)); - if (hit != null) + clientTemplateCandidates = (from cmt in db.client_margin_template.AsNoTracking() + join mt in db.margin_template_v2.AsNoTracking() on cmt.MarginTemplateId equals mt.id + where cmt.ValueDate <= valueDate && cmt.MarginTemplateId > 0 + && (clientIds.Contains(cmt.ClientId) || (cmt.ClientId == 0 && cmt.ClientLevel != "")) + && mt.IsValid && mt.TradeTypes.Contains("收益互换") + orderby cmt.ValueDate descending, cmt.id descending + select new { cmt.ClientId, cmt.ClientLevel, Template = mt }).ToList() + .Select(x => new ClientTemplateCandidate { - clientTemplateByClient[clientId] = hit.Template; - } - } + ClientId = x.ClientId, + ClientLevel = x.ClientLevel, + Template = x.Template + }) + .ToList(); } - //3.全局默认(全部交易共享,一次查询) - var globalDefault = db.margin_template_v2.AsNoTracking() + //3.全局默认(候选模板一次查询,每笔交易按簿记账户取最新适用模板) + var globalDefaults = db.margin_template_v2.AsNoTracking() .Where(x => x.IsDefault && !x.IsForClient && x.IsValid && x.TradeTypes.Contains("收益互换") && x.ValueDate <= valueDate) .OrderByDescending(x => x.ValueDate) .ThenByDescending(x => x.id) - .FirstOrDefault(); + .ToList(); foreach (var trade in trades) { @@ -342,21 +368,30 @@ namespace YLErp.Modules.MarginModule if (bindingByTrade.TryGetValue(trade.id, out var templateId)) { //一级找到即停:绑定指向失效模板时不向下回退(与单笔解析一致,仅留日志) - if (boundTemplates.TryGetValue(templateId, out var bound)) + if (boundTemplates.TryGetValue(templateId, out var bound) && bound.IsApplicableToBook(trade.AssetId)) { result[trade.id] = bound; } + else if (boundTemplates.ContainsKey(templateId)) + { + logger.Info($"【警告】预付金模板取数:交易{trade.id}绑定的模板{templateId}不适用簿记账户{trade.AssetId},按找到即停不向下回退"); + } else { logger.Info($"【警告】预付金模板取数:交易{trade.id}绑定的模板{templateId}已失效或不存在,按找到即停不向下回退"); } continue; } - if (clientTemplateByClient.TryGetValue(trade.ClientId, out var clientTemplate)) + var levelName = levelNames.TryGetValue(trade.ClientId, out var clientLevelName) ? clientLevelName : null; + var clientTemplate = clientTemplateCandidates.FirstOrDefault(x => + (x.ClientId == trade.ClientId || (x.ClientId == 0 && x.ClientLevel == levelName)) + && x.Template.IsApplicableToBook(trade.AssetId)); + if (clientTemplate != null) { - result[trade.id] = clientTemplate; + result[trade.id] = clientTemplate.Template; continue; } + var globalDefault = globalDefaults.FirstOrDefault(x => x.IsApplicableToBook(trade.AssetId)); if (globalDefault != null) { result[trade.id] = globalDefault; diff --git a/YLErpDAL/Modules/SwapModule/SwapMarginTemplateConfigService.cs b/YLErpDAL/Modules/SwapModule/SwapMarginTemplateConfigService.cs index 3f95bba5..8ea6e7f5 100644 --- a/YLErpDAL/Modules/SwapModule/SwapMarginTemplateConfigService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapMarginTemplateConfigService.cs @@ -9,7 +9,7 @@ namespace YLErp.Modules.SwapModule public static readonly string[] InitialTemplateNames = { "现金保证金", "授信保证金" }; - public static SwapMarginTemplateConfig GetConfig() + public static SwapMarginTemplateConfig GetConfig(int? assetId = null) { //保证金模板V2迁移:交易页"保证金模板"下拉的选项改为"预付金模板V2"页面维护的模板(自定义+全局默认,存模板名称),不再使用数据字典 //互换交易只显示"适用结构"包含 收益互换 的模板 @@ -17,7 +17,9 @@ namespace YLErp.Modules.SwapModule var items = db.margin_template_v2 .Where(item => !item.IsForClient && item.IsValid && item.TradeTypes.Contains("收益互换")) .OrderBy(item => item.IsDefault).ThenBy(item => item.id) - .Select(item => new SelectItem { Text = item.Name, Value = item.Name }) + .ToList() + .Where(item => !assetId.HasValue || assetId.Value <= 0 || item.IsApplicableToBook(assetId.Value)) + .Select(item => new SwapMarginTemplateOption { Text = item.Name, Value = item.Name, BookIds = item.BookIds }) .ToArray(); return new SwapMarginTemplateConfig @@ -31,8 +33,15 @@ namespace YLErp.Modules.SwapModule public class SwapMarginTemplateConfig { - public IEnumerable options { get; set; } + public IEnumerable options { get; set; } public string defaultValue { get; set; } } + + public class SwapMarginTemplateOption : SelectItem + { + public string BookIds { get; set; } + + public bool IsLegacy { get; set; } + } } diff --git a/YLErpDAL/Modules/SwapModule/SwapTradeService.cs b/YLErpDAL/Modules/SwapModule/SwapTradeService.cs index c38e770f..22b4b95f 100644 --- a/YLErpDAL/Modules/SwapModule/SwapTradeService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapTradeService.cs @@ -1,4 +1,4 @@ -using BaseOUDAL; +using BaseOUDAL; using ClosedXML.Report.Options; using Confluent.Kafka; using CsvHelper; @@ -118,6 +118,12 @@ namespace YLErp.Modules.SwapModule try { var isAddNew = req.id == 0; + var originalMarginTemplateName = isAddNew + ? null + : DbContext.trade + .Where(x => x.id == req.id) + .Select(x => x.MarginTemplateName) + .FirstOrDefault(); dbTrade = isAddNew ? InnerSaveNewTrade(req, tradeNumberGenerated) : InnerSaveEditTrade(req, out var changeConfirmStatus); if (req.SalesCommission != null) { @@ -139,7 +145,7 @@ namespace YLErp.Modules.SwapModule } } //保证金模板V2迁移:按交易页选择的保证金模板(MarginTemplateName 存模板V2名称)维护交易模板绑定 - SyncTradeMarginTemplate(dbTrade); + SyncTradeMarginTemplate(dbTrade, originalMarginTemplateName, isAddNew); trans.Commit(); } finally @@ -151,35 +157,72 @@ namespace YLErp.Modules.SwapModule } /// /// 保证金模板V2迁移:按交易上选择的保证金模板(MarginTemplateName 存预付金模板V2的名称)维护 trade_margin_template 绑定。 - /// 未选择或名称匹配不到有效自定义模板时仅清除旧绑定,不新增(保持无预付金占用的现状)。 + /// 未选择或存量交易原样保留的历史名称仅清除旧绑定,不新增(保持无预付金占用的现状)。 /// - private void SyncTradeMarginTemplate(trade dbTrade) + private void SyncTradeMarginTemplate(trade dbTrade, string originalMarginTemplateName, bool isNewTrade) { - var olds = DbContext.trade_margin_template.Where(x => x.TradeId == dbTrade.id).ToList(); - DbContext.trade_margin_template.RemoveRange(olds); + margin_template_v2 template = null; if (!string.IsNullOrWhiteSpace(dbTrade.MarginTemplateName)) { + var allTemplatesByName = DbContext.margin_template_v2 + .Where(x => x.Name == dbTrade.MarginTemplateName) + .ToList(); + var hasV2TemplateWithName = allTemplatesByName.Any(); //匹配自定义+全局默认模板(重名时优先自定义),模板"适用结构"需包含 收益互换 - var template = DbContext.margin_template_v2 - .Where(x => x.Name == dbTrade.MarginTemplateName && !x.IsForClient && x.IsValid && x.TradeTypes.Contains("收益互换")) + var templatesByName = allTemplatesByName + .Where(x => !x.IsForClient && x.IsValid && (x.TradeTypes ?? string.Empty).Contains("收益互换")) .OrderBy(x => x.IsDefault) - .FirstOrDefault(); - if (template != null) + .ToList(); + template = templatesByName.FirstOrDefault(x => x.IsApplicableToBook(dbTrade.AssetId)); + if (template == null) { - DbContext.trade_margin_template.Add(new trade_margin_template() + //存量交易未修改的非V2历史名称只保留在 trade.MarginTemplateName,不创建模板绑定。 + if (!CanKeepLegacyMarginTemplateName( + isNewTrade, + originalMarginTemplateName, + dbTrade.MarginTemplateName, + hasV2TemplateWithName)) { - TradeId = dbTrade.id, - ValueDate = new DateTime(2000, 1, 1), - MarginTemplateId = template.id, - IsLatest = true, - OptId = UserId, - OptName = UserName, - OptDate = DateTime.Now - }); + throw new ServiceException(templatesByName.Any() + ? $"所选预付金模板不适用当前簿记账户:{dbTrade.MarginTemplateName}" + : $"所选预付金模板不存在或已失效:{dbTrade.MarginTemplateName}"); + } } } + + var olds = DbContext.trade_margin_template.Where(x => x.TradeId == dbTrade.id).ToList(); + DbContext.trade_margin_template.RemoveRange(olds); + if (template != null) + { + DbContext.trade_margin_template.Add(new trade_margin_template() + { + TradeId = dbTrade.id, + ValueDate = new DateTime(2000, 1, 1), + MarginTemplateId = template.id, + IsLatest = true, + OptId = UserId, + OptName = UserName, + OptDate = DateTime.Now + }); + } DbContext.SaveChanges(); } + + /// + /// 判断是否允许存量交易原样保留未迁移的历史保证金模板名称。 + /// + internal static bool CanKeepLegacyMarginTemplateName( + bool isNewTrade, + string originalName, + string currentName, + bool currentNameIsV2Template) + { + return !isNewTrade + && !currentNameIsV2Template + && !string.IsNullOrWhiteSpace(originalName) + && !string.IsNullOrWhiteSpace(currentName) + && string.Equals(originalName, currentName, StringComparison.Ordinal); + } /// /// 单标的生成开仓事件 /// diff --git a/YLErpDAL/Modules/TradeModule/OrderModule/TradeSaveService.cs b/YLErpDAL/Modules/TradeModule/OrderModule/TradeSaveService.cs index e27bbbbc..e9808b54 100644 --- a/YLErpDAL/Modules/TradeModule/OrderModule/TradeSaveService.cs +++ b/YLErpDAL/Modules/TradeModule/OrderModule/TradeSaveService.cs @@ -656,7 +656,10 @@ namespace YLErp.Modules.TradeModule.OrderModule subTrade.InitialMargin = 0; trade.InitialMargin = 0; - var marginTemplate = DbContext.margin_template_v2.FirstOrDefault(x => x.Name == "无预付金"); + var marginTemplate = DbContext.margin_template_v2 + .Where(x => x.Name == "无预付金") + .ToList() + .FirstOrDefault(x => x.IsApplicableToBook(subTrade.AssetId)); if (marginTemplate == null) { marginTemplate = new margin_template_v2() @@ -1492,4 +1495,4 @@ namespace YLErp.Modules.TradeModule.OrderModule // dbTrade.ProcessOptDate = DateTime.Now; // dbTrade.ProcessStatus = "未审批"; // } -//} \ No newline at end of file +//} diff --git a/YLErpDAL/Modules/UnderlyingModule/FundManagerLookupService.cs b/YLErpDAL/Modules/UnderlyingModule/FundManagerLookupService.cs new file mode 100644 index 00000000..b37d8f5e --- /dev/null +++ b/YLErpDAL/Modules/UnderlyingModule/FundManagerLookupService.cs @@ -0,0 +1,94 @@ +using Dapper; +using MySqlConnector; +using YieldChain.Helpers; +using YLErp.BLL; + +namespace YLErp.Modules.UnderlyingModule +{ + public enum FundManagerLookupStatus + { + NotFound, + Unique, + Multiple, + Unavailable + } + + public sealed class FundManagerLookupResult + { + public FundManagerLookupStatus Status { get; init; } + public string InvestAdvisorName { get; init; } + } + + /// + /// 查询上游基金档案中的基金管理人。上游不可用时返回降级结果,不阻断页面编辑。 + /// + public sealed class FundManagerLookupService + { + private sealed class FundManagerRow + { + public string InvestAdvisorCode { get; set; } + public string InvestAdvisorName { get; set; } + } + + private const string LookupSql = @" +SELECT + ia.investadvisorcode AS InvestAdvisorCode, + ia.investadvisorname AS InvestAdvisorName +FROM glms_bigdata.mf_fundarchives AS fa +INNER JOIN glms_bigdata.mf_investadvisoroutline AS ia + ON CONVERT(fa.investadvisorcode USING utf8mb4) COLLATE utf8mb4_unicode_ci = + CONVERT(ia.investadvisorcode USING utf8mb4) COLLATE utf8mb4_unicode_ci +WHERE CONVERT(fa.secucode USING utf8mb4) COLLATE utf8mb4_unicode_ci = + CONVERT(TRIM(SUBSTRING_INDEX(@UnderlyingCode, '.', 1)) USING utf8mb4) COLLATE utf8mb4_unicode_ci"; + + public FundManagerLookupResult Lookup(string underlyingCode) + { + var normalizedCode = NormalizeCode(underlyingCode); + if (string.IsNullOrEmpty(normalizedCode)) + { + return new FundManagerLookupResult { Status = FundManagerLookupStatus.NotFound }; + } + + var connectionString = AppManager.GetConnectionString("glms_bigdata"); + if (string.IsNullOrWhiteSpace(connectionString)) + { + return new FundManagerLookupResult { Status = FundManagerLookupStatus.Unavailable }; + } + + try + { + using var connection = new MySqlConnection(connectionString); + var matches = connection.Query(LookupSql, new { UnderlyingCode = normalizedCode }, commandTimeout: 10) + .Where(row => !string.IsNullOrWhiteSpace(row.InvestAdvisorName)) + .GroupBy(row => (row.InvestAdvisorCode ?? string.Empty).Trim(), StringComparer.OrdinalIgnoreCase) + .Select(group => group.Select(row => row.InvestAdvisorName.Trim()).Distinct(StringComparer.OrdinalIgnoreCase).ToArray()) + .Where(names => names.Length > 0) + .ToArray(); + + return matches.Length switch + { + 0 => new FundManagerLookupResult { Status = FundManagerLookupStatus.NotFound }, + 1 when matches[0].Length == 1 => new FundManagerLookupResult { Status = FundManagerLookupStatus.Unique, InvestAdvisorName = matches[0][0] }, + _ => new FundManagerLookupResult { Status = FundManagerLookupStatus.Multiple } + }; + } + catch (Exception ex) + { + LogFactory.GetLogger().Error("查询基金管理人失败,代码:" + normalizedCode, ex); + return new FundManagerLookupResult { Status = FundManagerLookupStatus.Unavailable }; + } + } + + public static string NormalizeCode(string underlyingCode) + { + var trimmed = underlyingCode?.Trim(); + if (string.IsNullOrEmpty(trimmed)) + { + return null; + } + + var dotIndex = trimmed.IndexOf('.'); + return (dotIndex < 0 ? trimmed : trimmed.Substring(0, dotIndex)).TrimToNull(); + } + } +} diff --git a/YLErpWeb/Controllers/client_margin_templateController.cs b/YLErpWeb/Controllers/client_margin_templateController.cs index 0b500568..df68ab84 100644 --- a/YLErpWeb/Controllers/client_margin_templateController.cs +++ b/YLErpWeb/Controllers/client_margin_templateController.cs @@ -48,6 +48,10 @@ namespace YLErp.Web.Controllers } var marginTemplate = yldb.margin_template_v2.Find(clientMarginTemplate.MarginTemplateId); + if (marginTemplate == null) + { + throw new Exception("找不到有效的预付金模板"); + } var marginDetail = yldb.margin_template_detail.Where(x => x.MarginTemplateId == marginTemplate.id && x.ValueDate <= clientMarginTemplate.ValueDate).OrderByDescending(x => x.ValueDate).FirstOrDefault(); if (marginDetail == null && (marginTemplate.RuleType != (int)MarginRuleTypeEnum.无预付金 && marginTemplate.RuleType != (int)MarginRuleTypeEnum.自动赎回买入预付金规则 && marginTemplate.RuleType != (int)MarginRuleTypeEnum.含赔付预付金计算规则 && marginTemplate.RuleType != (int)MarginRuleTypeEnum.保底预付金规则)) @@ -57,8 +61,14 @@ namespace YLErp.Web.Controllers //一套规则多套默认参数的互斥验证 var existClientMarginTemplateOther = yldb.client_margin_template.Where(x => ((x.ClientId != 0 && x.ClientId == clientMarginTemplate.ClientId) || (x.ClientLevel != "" && x.ClientLevel == clientMarginTemplate.ClientLevel)) && x.ValueDate == clientMarginTemplate.ValueDate && x.id != clientMarginTemplate.id); - var existMarginTemplateIdsOther = existClientMarginTemplateOther.Select(x => x.MarginTemplateId).ToArray(); - + var existingTemplateIds = existClientMarginTemplateOther + .Select(x => x.MarginTemplateId) + .ToArray(); + var existingMarginTemplates = yldb.margin_template_v2 + .Where(x => existingTemplateIds.Contains(x.id)) + .ToList() + .Where(x => margin_template_v2.AreBookScopesOverlapping(x.BookIds, marginTemplate.BookIds)) + .ToList(); var msg = ""; if(clientMarginTemplate.ClientLevel == "") { @@ -72,17 +82,16 @@ namespace YLErp.Web.Controllers if (marginTemplate.DefaultParameterType != 0) { //验证不同规则是否已经存在该交易类型 - var marginTemplateDefault = yldb.margin_template_v2.Where(x => existMarginTemplateIdsOther.Contains(x.id) && x.RuleType != marginTemplate.RuleType); - var existTradeTypes = marginTemplateDefault.Select(x => x.TradeTypes).ToList(); - var tradeTypes = new List(); - existTradeTypes.ForEach(x => tradeTypes.AddRange(x.Split(','))); + var marginTemplateDefault = existingMarginTemplates + .Where(x => x.RuleType != marginTemplate.RuleType) + .ToList(); + var tradeTypes = margin_template_v2.GetOverlappingTradeTypes(marginTemplateDefault, marginTemplate.BookIds).ToList(); //验证相同规则相同的默认参数类型,是否包含相同的适用结构 - var sameRuleParameterRecord = yldb.margin_template_v2.FirstOrDefault(x => existMarginTemplateIdsOther.Contains(x.id) && x.RuleType == marginTemplate.RuleType && x.DefaultParameterType == marginTemplate.DefaultParameterType); - if (sameRuleParameterRecord != null) - { - tradeTypes.AddRange(sameRuleParameterRecord.TradeTypes.Split(',')); - } + var sameRuleParameterRecords = existingMarginTemplates + .Where(x => x.RuleType == marginTemplate.RuleType && x.DefaultParameterType == marginTemplate.DefaultParameterType) + .ToList(); + tradeTypes.AddRange(margin_template_v2.GetOverlappingTradeTypes(sameRuleParameterRecords, marginTemplate.BookIds)); tradeTypes.ForEach(x => { @@ -94,10 +103,8 @@ namespace YLErp.Web.Controllers } else { - var marginTemplateDefault = yldb.margin_template_v2.Where(x => existMarginTemplateIdsOther.Contains(x.id)); - var existTradeTypes = marginTemplateDefault.Select(x => x.TradeTypes).ToList(); - List tradeTypes = new List(); - existTradeTypes.ForEach(x => tradeTypes.AddRange(x.Split(','))); + var marginTemplateDefault = existingMarginTemplates; + var tradeTypes = margin_template_v2.GetOverlappingTradeTypes(marginTemplateDefault, marginTemplate.BookIds).ToList(); tradeTypes.ForEach(x => { if (marginTemplate.TradeTypes.Contains(x)) @@ -202,4 +209,4 @@ namespace YLErp.Web.Controllers } } } -} \ No newline at end of file +} diff --git a/YLErpWeb/Controllers/margin_template_v2Controller.cs b/YLErpWeb/Controllers/margin_template_v2Controller.cs index ea788f5d..a0452fae 100644 --- a/YLErpWeb/Controllers/margin_template_v2Controller.cs +++ b/YLErpWeb/Controllers/margin_template_v2Controller.cs @@ -247,6 +247,9 @@ namespace YLErp.Web.Controllers { try { + marginTemplate.BookIds = margin_template_v2.NormalizeBookIds(marginTemplate.BookIds); + ValidateBookIds(marginTemplate.BookIds); + if (string.IsNullOrEmpty(marginTemplate.TradeTypes)) { throw new Exception("默认规则的适用结构不能为空"); @@ -263,17 +266,18 @@ namespace YLErp.Web.Controllers if (marginTemplate.DefaultParameterType != 0) { //验证不同规则是否已经存在该交易类型 - var marginTemplateDefault = yldb.margin_template_v2.Where(x => x.id != marginTemplate.id && x.IsDefault && x.IsValid && !x.IsForClient && x.RuleType != marginTemplate.RuleType && x.ValueDate == marginTemplate.ValueDate); - var existTradeTypes = marginTemplateDefault.Select(x => x.TradeTypes).ToList(); - var tradeTypes = new List(); - existTradeTypes.ForEach(x => tradeTypes.AddRange(x.Split(','))); + var marginTemplateDefault = yldb.margin_template_v2 + .Where(x => x.id != marginTemplate.id && x.IsDefault && x.IsValid && !x.IsForClient && x.RuleType != marginTemplate.RuleType && x.ValueDate == marginTemplate.ValueDate) + .ToList() + .Where(x => margin_template_v2.AreBookScopesOverlapping(x.BookIds, marginTemplate.BookIds)); + var tradeTypes = margin_template_v2.GetOverlappingTradeTypes(marginTemplateDefault, marginTemplate.BookIds).ToList(); //验证相同规则相同的默认参数类型,是否包含相同的适用结构 - var sameRuleParameterRecord = yldb.margin_template_v2.FirstOrDefault(x => x.id != marginTemplate.id && x.IsDefault && x.IsValid && !x.IsForClient && x.RuleType == marginTemplate.RuleType && x.DefaultParameterType == marginTemplate.DefaultParameterType && (x.BuySellType & marginTemplate.BuySellType) > 0 && (x.MarginScenarios & marginTemplate.MarginScenarios) > 0 && x.ValueDate == marginTemplate.ValueDate); - if (sameRuleParameterRecord != null) - { - tradeTypes.AddRange(sameRuleParameterRecord.TradeTypes.Split(',')); - } + var sameRuleParameterRecords = yldb.margin_template_v2 + .Where(x => x.id != marginTemplate.id && x.IsDefault && x.IsValid && !x.IsForClient && x.RuleType == marginTemplate.RuleType && x.DefaultParameterType == marginTemplate.DefaultParameterType && (x.BuySellType & marginTemplate.BuySellType) > 0 && (x.MarginScenarios & marginTemplate.MarginScenarios) > 0 && x.ValueDate == marginTemplate.ValueDate) + .ToList() + .Where(x => margin_template_v2.AreBookScopesOverlapping(x.BookIds, marginTemplate.BookIds)); + tradeTypes.AddRange(margin_template_v2.GetOverlappingTradeTypes(sameRuleParameterRecords, marginTemplate.BookIds)); tradeTypes.ForEach(x => { @@ -285,10 +289,11 @@ namespace YLErp.Web.Controllers } else { - var marginTemplateDefault = yldb.margin_template_v2.Where(x => x.id != marginTemplate.id && x.IsDefault && x.IsValid && !x.IsForClient && (x.BuySellType & marginTemplate.BuySellType) > 0 && (x.MarginScenarios & marginTemplate.MarginScenarios) > 0 && x.ValueDate == marginTemplate.ValueDate); - var existTradeTypes = marginTemplateDefault.Select(x => x.TradeTypes).ToList(); - var tradeTypes = new List(); - existTradeTypes.ForEach(x => tradeTypes.AddRange(x.Split(','))); + var marginTemplateDefault = yldb.margin_template_v2 + .Where(x => x.id != marginTemplate.id && x.IsDefault && x.IsValid && !x.IsForClient && (x.BuySellType & marginTemplate.BuySellType) > 0 && (x.MarginScenarios & marginTemplate.MarginScenarios) > 0 && x.ValueDate == marginTemplate.ValueDate) + .ToList() + .Where(x => margin_template_v2.AreBookScopesOverlapping(x.BookIds, marginTemplate.BookIds)); + var tradeTypes = margin_template_v2.GetOverlappingTradeTypes(marginTemplateDefault, marginTemplate.BookIds).ToList(); tradeTypes.ForEach(x => { if (marginTemplate.TradeTypes.Contains(x)) @@ -471,6 +476,7 @@ namespace YLErp.Web.Controllers r.Name = marginTemplate.Name; r.IsDefault = marginTemplate.IsDefault; r.IsForClient = marginTemplate.IsForClient; + r.BookIds = marginTemplate.BookIds; r.Comments = marginTemplate.Comments; r.RuleType = marginTemplate.RuleType; r.TradeTypes = marginTemplate.TradeTypes; @@ -595,6 +601,27 @@ namespace YLErp.Web.Controllers } } + private void ValidateBookIds(string bookIds) + { + if (string.IsNullOrWhiteSpace(bookIds)) + { + return; + } + + var ids = margin_template_v2.ParseBookIds(bookIds) + .Select(x => int.Parse(x)) + .ToList(); + var existingIds = yldb.assetunit + .Where(x => ids.Contains(x.id)) + .Select(x => x.id) + .ToList(); + var missingIds = ids.Except(existingIds).ToList(); + if (missingIds.Any()) + { + throw new Exception("适用簿记账户组包含不存在的簿记账户ID:" + string.Join(",", missingIds)); + } + } + [HttpPost] public JsonResult deleteEditmargin_template_v2(string enid) { @@ -732,4 +759,4 @@ namespace YLErp.Web.Controllers } -} \ No newline at end of file +} diff --git a/YLErpWeb/Controllers/tradeController.cs b/YLErpWeb/Controllers/tradeController.cs index ce50fdab..2bd5d5e5 100644 --- a/YLErpWeb/Controllers/tradeController.cs +++ b/YLErpWeb/Controllers/tradeController.cs @@ -7980,7 +7980,11 @@ namespace YLErp.Web.Controllers var trade = yldb.trade.Find(tradeId); //R1 三层级(2026-08-24 配合修改):候选只列有效模板——已作废模板一旦绑定, //解析一级"找到即停"会让该交易直接视为无预付金(仅留日志),必须从源头挡住 - var marginTemplates = yldb.margin_template_v2.Where(x => x.IsValid && x.TradeTypes.Contains(trade.TradeType) && !x.IsDefault).ToList(); + var marginTemplates = yldb.margin_template_v2 + .Where(x => x.IsValid && x.TradeTypes.Contains(trade.TradeType) && !x.IsDefault) + .ToList() + .Where(x => x.IsApplicableToBook(trade.AssetId)) + .ToList(); marginTemplates.ForEach(x => x.Details = yldb.margin_template_detail.Where(y => y.MarginTemplateId == x.id).ToList()); var latestTradeMarginTemplate = yldb.trade_margin_template.Where(x => x.TradeId == tradeId).OrderByDescending(x => x.ValueDate).FirstOrDefault(); @@ -8025,6 +8029,26 @@ namespace YLErp.Web.Controllers throw new Exception("请选择有效的预付金模板"); } + var trade = yldb.trade.Find(tradeMarginTemplate.TradeId); + if (trade == null) + { + throw new Exception("交易不存在"); + } + + var marginTemplate = yldb.margin_template_v2 + .FirstOrDefault(x => x.id == tradeMarginTemplate.MarginTemplateId + && x.IsValid + && !x.IsDefault + && x.TradeTypes.Contains(trade.TradeType)); + if (marginTemplate == null) + { + throw new Exception("请选择有效的预付金模板"); + } + if (!marginTemplate.IsApplicableToBook(trade.AssetId)) + { + throw new Exception("所选预付金模板不适用当前簿记账户"); + } + var deletedRecords = yldb.trade_margin_template.Where(x => x.TradeId == tradeMarginTemplate.TradeId && x.ValueDate >= tradeMarginTemplate.ValueDate).ToArray(); yldb.trade_margin_template.RemoveRange(deletedRecords); var updatedRecords = yldb.trade_margin_template.Where(x => x.TradeId == tradeMarginTemplate.TradeId && x.ValueDate < tradeMarginTemplate.ValueDate).ToList(); @@ -8040,7 +8064,6 @@ namespace YLErp.Web.Controllers #region 重置初始预付金 - var trade = yldb.trade.Find(tradeMarginTemplate.TradeId); //R1 三层级(2026-08-24 配合修改):收益互换跳过系统重算——互换初始预付金为手工录入/预付金腿承载 //(方案B 引擎回落链消费 trade.InitialMargin),且试算链路(GetInitialMarginWithPreHandle 克隆 id=0) //对互换取不到模板绑定恒得 0,无条件重置会把手工录入值清零;与 TradeSaveService 保存守卫语义对齐 diff --git a/YLErpWeb/Controllers/underlying_managerController.cs b/YLErpWeb/Controllers/underlying_managerController.cs index 69ba8b66..fa9e7f35 100644 --- a/YLErpWeb/Controllers/underlying_managerController.cs +++ b/YLErpWeb/Controllers/underlying_managerController.cs @@ -525,6 +525,25 @@ namespace YLErp.Web.Controllers return JsonSuccess("", underlying); } + /// + /// 查询上游基金档案中的基金管理人。查询失败或结果不唯一时返回可降级结果。 + /// + [HttpGet] + public JsonResult GetFundManager(string code, string instrumentType) + { + if (!string.Equals(instrumentType, ConsGlobal.InstrumentType.Fund, StringComparison.OrdinalIgnoreCase)) + { + return JsonSuccess("", new FundManagerLookupResult { Status = FundManagerLookupStatus.NotFound }); + } + + var result = new FundManagerLookupService().Lookup(code); + return JsonSuccess("", new + { + result.InvestAdvisorName, + IsUnique = result.Status == FundManagerLookupStatus.Unique + }); + } + /// /// 预付金参数 /// diff --git a/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml b/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml index ce7eb5d6..3c3f3251 100644 --- a/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml +++ b/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml @@ -15,11 +15,20 @@ var jsClient = canChangeClient && Model.ClientId > 0 ? jsClients.FirstOrDefault(n => n.id == Model.ClientId) : null; var jsAssetUnits = JsDataModel.GetAssetUnits(CurUser); var tradeMarginTemplates = new tradeController().GetMarginTemplates(); - var swapMarginTemplateItems = SwapMarginTemplateConfigService.GetConfig().options.ToList(); + var allSwapMarginTemplateItems = SwapMarginTemplateConfigService.GetConfig().options.ToList(); + var swapMarginTemplateItems = allSwapMarginTemplateItems.ToList(); //保证金模板V2迁移:存量交易的保证金模板可能是历史文本值(如字典配置的"现金保证金"),不在模板V2选项中时追加显示,避免编辑保存时被静默清空 if (!string.IsNullOrWhiteSpace(Model.MarginTemplateName) && !swapMarginTemplateItems.Any(n => n.Value == Model.MarginTemplateName)) { - swapMarginTemplateItems.Add(new YLErp.Models.SelectItem { Text = Model.MarginTemplateName + "(历史值)", Value = Model.MarginTemplateName }); + var legacyMarginTemplateItem = new SwapMarginTemplateOption + { + Text = Model.MarginTemplateName + "(历史值)", + Value = Model.MarginTemplateName, + BookIds = null, + IsLegacy = true + }; + allSwapMarginTemplateItems.Add(legacyMarginTemplateItem); + swapMarginTemplateItems.Add(legacyMarginTemplateItem); } var tradeMarginTemplateItems = new tradeController().GetMarginTemplateItems(); var jsAssetUnit = Model.AssetId > 0 ? jsAssetUnits.FirstOrDefault(n => n.id == Model.AssetId) : null; @@ -75,6 +84,7 @@ jsTrader, jsTraders, tradeMarginTemplates = tradeMarginTemplates, + allSwapMarginTemplateItems = allSwapMarginTemplateItems, swapMarginTemplateItems = swapMarginTemplateItems, tradeMarginTemplateItems = tradeMarginTemplateItems, needRemark = !isAdd && valuedateBLL.SystemDate.EditTradeNeedRemark, diff --git a/YLErpWeb/Views/margin_template_v2/margin_template_v2ClientEdit.cshtml b/YLErpWeb/Views/margin_template_v2/margin_template_v2ClientEdit.cshtml index fcb0b1ee..47cf0db2 100644 --- a/YLErpWeb/Views/margin_template_v2/margin_template_v2ClientEdit.cshtml +++ b/YLErpWeb/Views/margin_template_v2/margin_template_v2ClientEdit.cshtml @@ -13,6 +13,7 @@ isGFSM = PS.Config.Company == CompanyEnum.广发商贸, etfSubtypeItems = YLErp.BLL.DictionaryBLL.GetDictionaryItems("ETF 子类").Select(x => x.Name).ToList() }; + var selectedBookIds = margin_template_v2.ParseBookIds(Model?.BookIds); } @section CSS{ @@ -63,6 +64,16 @@ +
+ + +
+ @if (PS.Config.Company != CompanyEnum.广发商贸) {
diff --git a/YLErpWeb/Views/margin_template_v2/margin_template_v2DefaultEdit.cshtml b/YLErpWeb/Views/margin_template_v2/margin_template_v2DefaultEdit.cshtml index 36deada4..2d33cd4b 100644 --- a/YLErpWeb/Views/margin_template_v2/margin_template_v2DefaultEdit.cshtml +++ b/YLErpWeb/Views/margin_template_v2/margin_template_v2DefaultEdit.cshtml @@ -1,4 +1,4 @@ -@using YLErp.Enums; +@using YLErp.Enums; @model margin_template_v2 @{ @@ -12,6 +12,7 @@ detail = new margin_template_detail() { ValueDate = new DateTime(2020, 1, 1), ComparedInitialMarginRate = 1 }, etfSubtypeItems = YLErp.BLL.DictionaryBLL.GetDictionaryItems("ETF 子类").Select(x => x.Name).ToList() }; + var selectedBookIds = margin_template_v2.ParseBookIds(Model?.BookIds); } @section CSS{ @@ -63,6 +64,16 @@
+
+ + +
+