From db95699aa2390fce159401d76ab9d5cafbf28fbb Mon Sep 17 00:00:00 2001 From: hjhan Date: Fri, 28 Aug 2026 14:31:30 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E5=9F=BA=E9=87=91=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E4=BA=BA=E5=8F=96=E6=95=B0=E6=94=AF=E6=8C=81=E8=B7=A8?= =?UTF-8?q?=E7=8E=AF=E5=A2=83=E5=BA=93=E5=90=8D=E2=80=94=E2=80=94=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20DbSchema=20=E5=BA=93=E5=90=8D=E6=8F=92=E5=80=BC?= =?UTF-8?q?=EF=BC=8Cbigdata=20=E8=BF=9E=E6=8E=A5=E4=B8=B2=E6=8C=87?= =?UTF-8?q?=E5=90=91=2096/glms=5Fbigdata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:SQL 硬编码物理库名(bigdata./glms_bigdata.),关联库在不同环境库名不同,换环境即失效; 且仓库所有 appsettings 均未配 bigdata 连接串,本地/单测环境一直走 Unavailable 降级。 - 新增 YLErpDAL/DataBase/DbSchema:从连接串 database= 解析物理库名(含反引号、\w+ 白名单校验、进程级缓存), 跨库 SQL 写 DbSchema.Of("bigdata").mf_fundarchives ——代码只认逻辑连接名(等价 Java @Mapper 指定数据源), 物理库名归各环境 appsettings;将来 join ERP 主库用 DbSchema.Of("ylcms") 同法插值 - FundManagerLookupService:SQL 两处库名前缀改为插值(查找逻辑/CONVERT/COLLATE 不变),Unavailable 降级语义不变 - appsettings.local.json / UnitTestProject appsettings.json 补 bigdata=192.168.2.96:3306/glms_bigdata(抄现有 96 库连接串改库名) - 新增 FundManagerLookupServiceTest(连 96 实库):DbSchema 解析断言 + Lookup 连通性 实测 511160.SH → Unique「东财基金管理有限公司」全链路打通;161210/630006 因 mf_investadvisoroutline 仅2行无映射返回 NotFound(数据覆盖问题) --- .../FundManagerLookupServiceTest.cs | 46 +++++++++++++++++++ UnitTestProject/appsettings.json | 3 +- YLErpDAL/DataBase/DbSchema.cs | 41 +++++++++++++++++ .../FundManagerLookupService.cs | 13 ++++-- YLErpWeb/appsettings.local.json | 3 +- 5 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 UnitTestProject/Modules/UnderlyingModule/FundManagerLookupServiceTest.cs create mode 100644 YLErpDAL/DataBase/DbSchema.cs diff --git a/UnitTestProject/Modules/UnderlyingModule/FundManagerLookupServiceTest.cs b/UnitTestProject/Modules/UnderlyingModule/FundManagerLookupServiceTest.cs new file mode 100644 index 00000000..15ca2361 --- /dev/null +++ b/UnitTestProject/Modules/UnderlyingModule/FundManagerLookupServiceTest.cs @@ -0,0 +1,46 @@ +using Dapper; +using YLErp.Modules.UnderlyingModule; + +namespace YLErp.UnitTestProject.Modules.UnderlyingModule +{ + /// + /// 基金管理人取数(bigdata 数据源)连通性与库名插值验证(连 96 测试库): + /// - DbSchema.Of 从连接串解析物理库名(glms_bigdata),SQL 不再硬编码库名、跨环境库名不同也能命中; + /// - Lookup 全链路(连接 96 → 插值 SQL 执行 → 结果归并)不返回 Unavailable 即为连通且 SQL 有效。 + /// + [TestClass] + public class FundManagerLookupServiceTest + { + [TestMethod] + public void DbSchema_从连接串解析物理库名() + { + Assert.AreEqual("`glms_bigdata`", DbSchema.Of("bigdata")); + Assert.AreEqual("`glms_yltrs_ylcms`", DbSchema.Of("ylcms")); + } + + [TestMethod] + public void Lookup_连接96大数据库_非降级() + { + var service = new FundManagerLookupService(); + //96 库现有测试数据 161210/630006/511160(SECUCODE 无后缀,与去后缀匹配逻辑一致),用库存代码验证全链路命中 + foreach (var code in new[] { "511160.SH", "161210.SZ", "630006.SH" }) + { + var result = service.Lookup(code); + Console.WriteLine($"code={code} → Status={result.Status}, InvestAdvisorName={result.InvestAdvisorName}"); + Assert.AreNotEqual(FundManagerLookupStatus.Unavailable, result.Status, + $"code={code} 返回 Unavailable:96 bigdata 库不可达或插值 SQL 执行失败"); + } + } + + [TestMethod] + public void Diag_查看库存secucode样例与行数() + { + using var connection = new MySqlConnector.MySqlConnection(AppManager.GetConnectionString("bigdata")); + var count = connection.ExecuteScalar($"SELECT COUNT(*) FROM {DbSchema.Of("bigdata")}.mf_fundarchives"); + var samples = connection.Query($"SELECT SECUCODE FROM {DbSchema.Of("bigdata")}.mf_fundarchives LIMIT 8"); + Console.WriteLine($"mf_fundarchives 行数={count}, SECUCODE样例=[{string.Join(",", samples)}]"); + var advCount = connection.ExecuteScalar($"SELECT COUNT(*) FROM {DbSchema.Of("bigdata")}.mf_investadvisoroutline"); + Console.WriteLine($"mf_investadvisoroutline 行数={advCount}"); + } + } +} diff --git a/UnitTestProject/appsettings.json b/UnitTestProject/appsettings.json index 7505ceff..441d3eae 100644 --- a/UnitTestProject/appsettings.json +++ b/UnitTestProject/appsettings.json @@ -3,7 +3,8 @@ "ylcms": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_yltrs_ylcms;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;", "yladmin": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_yltrs_admin;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;", "ylclient": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_yltrs_client;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;", - "bondoms": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=zszq_bond_oms;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;" + "bondoms": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=zszq_bond_oms;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;", + "bigdata": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_bigdata;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;" }, "LibreOffice": { "ExePath": "", diff --git a/YLErpDAL/DataBase/DbSchema.cs b/YLErpDAL/DataBase/DbSchema.cs new file mode 100644 index 00000000..f48048f5 --- /dev/null +++ b/YLErpDAL/DataBase/DbSchema.cs @@ -0,0 +1,41 @@ +using MySqlConnector; +using System.Collections.Concurrent; +using System.Text.RegularExpressions; + +namespace YLErp.BLL +{ + /// + /// 跨库 SQL 的物理库名解析器:代码里只写逻辑连接名(appsettings ConnectionStrings 的 key, + /// 与 Java @Mapper 指定数据源名同语义),物理库名由各环境连接串的 database= 决定—— + /// 同一逻辑库在不同环境库名不同(如 bigdata 在测试环境为 glms_bigdata),SQL 中硬编码库名会跨环境失败。 + /// 用法:{@DbSchema.Of("bigdata")}.mf_fundarchives(返回带反引号的库名,可直接内插)。 + /// 仅标识自家的 appsettings 连接串,值不来自用户输入;仍做 \w+ 白名单校验防御配置笔误。 + /// + public static class DbSchema + { + private static readonly ConcurrentDictionary Cache = new(); + private static readonly Regex SafeIdentifier = new(@"^\w+$", RegexOptions.Compiled); + + /// + /// 取逻辑连接名对应的物理库名(形如 `glms_bigdata`,含反引号)。配置缺失或库名非法立即抛错—— + /// 跨库 SQL 拼错库名在运行期才暴露更难排查,配置错误应尽早失败。 + /// + public static string Of(string connectionKey) + { + return Cache.GetOrAdd(connectionKey, key => + { + var connectionString = AppManager.GetConnectionString(key); + if (string.IsNullOrWhiteSpace(connectionString)) + { + throw new InvalidOperationException($"跨库SQL依赖的连接串未配置:{key}"); + } + var database = new MySqlConnectionStringBuilder(connectionString).Database; + if (string.IsNullOrWhiteSpace(database) || !SafeIdentifier.IsMatch(database)) + { + throw new InvalidOperationException($"连接串 {key} 缺少 database 或库名非法:{database}"); + } + return "`" + database + "`"; + }); + } + } +} diff --git a/YLErpDAL/Modules/UnderlyingModule/FundManagerLookupService.cs b/YLErpDAL/Modules/UnderlyingModule/FundManagerLookupService.cs index 0b07fba8..8d3808ba 100644 --- a/YLErpDAL/Modules/UnderlyingModule/FundManagerLookupService.cs +++ b/YLErpDAL/Modules/UnderlyingModule/FundManagerLookupService.cs @@ -30,16 +30,21 @@ namespace YLErp.Modules.UnderlyingModule public string InvestAdvisorName { get; set; } } - private const string LookupSql = @" + //物理库名由连接串 database= 决定(DbSchema 解析),各环境库名不同(如测试环境 glms_bigdata), + //SQL 不硬编码库名;连接开在 bigdata 数据源上,将来跨库 join ERP 主库时用 DbSchema.Of("ylcms") 同法插值 + private string BuildLookupSql() + { + return $@" SELECT ia.investadvisorcode AS InvestAdvisorCode, ia.investadvisorname AS InvestAdvisorName -FROM bigdata.mf_fundarchives AS fa -INNER JOIN bigdata.mf_investadvisoroutline AS ia +FROM {DbSchema.Of("bigdata")}.mf_fundarchives AS fa +INNER JOIN {DbSchema.Of("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) { @@ -58,7 +63,7 @@ WHERE CONVERT(fa.secucode USING utf8mb4) COLLATE utf8mb4_unicode_ci = try { using var connection = new MySqlConnection(connectionString); - var matches = connection.Query(LookupSql, new { UnderlyingCode = normalizedCode }, commandTimeout: 10) + var matches = connection.Query(BuildLookupSql(), 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()) diff --git a/YLErpWeb/appsettings.local.json b/YLErpWeb/appsettings.local.json index e2956912..a7fb2c4f 100644 --- a/YLErpWeb/appsettings.local.json +++ b/YLErpWeb/appsettings.local.json @@ -10,7 +10,8 @@ "ylcms": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_yltrs_ylcms;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;", "yladmin": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_yltrs_admin;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;", "ylclient": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_yltrs_client;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;", - "bondoms": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_bond_oms;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;" + "bondoms": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_bond_oms;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;", + "bigdata": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_bigdata;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;" }, "AppSettings": { "VirtualPathRoot": "", From 1f3fb9240bd2c28f585f126656a4c028fcaa2b60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=90=8D=E9=94=90?= <1565842059@qq.com> Date: Fri, 28 Aug 2026 14:36:54 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(trade):=20=E4=BF=AE=E6=AD=A3=E4=BA=A4?= =?UTF-8?q?=E6=98=93=E7=A1=AE=E8=AE=A4=E4=B9=A6=E4=B8=AD=E7=9A=84=E5=9F=BA?= =?UTF-8?q?=E6=9C=AC=E8=B4=B9=E7=8E=87=E8=AE=A1=E7=AE=97=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 注释掉原有的基本费率计算公式 - 直接使用PosiTradingFeeUnit字段获取基础费率 - 避免通过PosiTradingFeePending除以名义本金的方式计算 - 确保基本费率显示的准确性 --- .../DocumentGenerator/TradeConfirmationGenerator.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Plugins/YLErp.Plugins.GuoLian/DocumentGenerator/TradeConfirmationGenerator.cs b/Plugins/YLErp.Plugins.GuoLian/DocumentGenerator/TradeConfirmationGenerator.cs index e626cb4b..7cf45a24 100644 --- a/Plugins/YLErp.Plugins.GuoLian/DocumentGenerator/TradeConfirmationGenerator.cs +++ b/Plugins/YLErp.Plugins.GuoLian/DocumentGenerator/TradeConfirmationGenerator.cs @@ -559,9 +559,11 @@ namespace YLErp.Plugins.GuoLian.DocumentGenerator } // 基本费率 = PosiTradingFeePending / 名义本金 * 100 - var tradingFee = (double)swapPosition.PosiTradingFeePending; - var basicFeeRate = notional == 0 ? 0 : tradingFee / notional * 100; - dic["基本费率"] = basicFeeRate.ToString("0.####"); + // var tradingFee = (double)swapPosition.PosiTradingFeePending; + // var basicFeeRate = notional == 0 ? 0 : tradingFee / notional * 100; + // dic["基本费率"] = basicFeeRate.ToString("0.####"); + // @张名锐: 直接取值PosiTradingFeeUnit(基础费率) + dic["基本费率"] = swapPosition.PosiTradingFeeUnit.ToString("0.####"); } else {