Compare commits

..
13 Commits
Author SHA1 Message Date
锦麟 王 3d631e0a07 #EQD-7247 客户维度:维持保证金计算不正确 2026-08-28 16:20:54 +08:00
锦麟 王 601eb2bed7 BugFix 修改生效日之后,detail的生效日没变 2026-08-28 16:10:04 +08:00
hjhan fdc0331407 cleanup(trade): 收敛 IsGuoJun 死分支并删除失去调用方的 guojun 报表函数
国联下 PS.Config.IsGuoJun 恒为 false,以下分支只走 else 侧:
- tradeDayConfirmList.cshtml: IsGuoJun if/else 收敛为 sendReport() 单行
- maturityDateReport.cshtml: 发送结算确认书按钮的 if/else 收敛为 SendEmailSettleBill()
- maturityDateReport.cshtml: _chooseContractType 的三元收敛为 "sendReport()"

随之失去全部调用方的死函数一并删除(大小写不敏感全仓检索确认无其它引用,
包括无 window["xxx"] 之类的动态调用):
- tradeDayConfirmList.js sendReportGuojun()(713-749)
- maturityDateReport.cshtml sendReport_guojun()(913-939)

顺带清掉 percentColumnText.js 注释里对已删除文件 structure_dz.js 的引用。

注:后端 SendEmailSettleBill_Guojun / SendEmailUnwindReportGuojun 两个 Action
前端已无调用方,但属 C# 公开终结点,本次不动,留待单独决策。

净减 78 行。
2026-08-28 15:52:21 +08:00
hjhan ec11a1028a cleanup(pricing): 删除 structure.cshtml 中非国联的死定制分支
国联民生定制分支下 Company 恒为 CompanyEnum.国联,以下条件永假,对应分支永不渲染:
- @switch(Company) 的 CSS 分支(光大光子 / 国泰君安 专用样式表)
- GetTotalMargin 的 伴兴 / 茂川资本 / 弘业 覆盖(保留 :90 的无条件基础定义,国联本就走它)
- 东方财富 专用的 5 个输入项 ×2 组(InitialAdvance/PeriodAdvance/FontEarning/ConfirmedLine/ConfirmedFloor)

安全性:
- structure.js 对 #InitialAdvance 等 id 为无条件读取(parseFloat($(...).val()) || 0),
  国联下这些元素本就不存在、取值已恒为 0,删除不产生行为变化。
- pageData.showCCR / is厦门象屿 保留 key 仅将公司判断改为硬编码 false,
  JS 侧均为真假判断,序列化契约与取值均不变。

净减 107 行。
2026-08-28 15:48:11 +08:00
hjhan 4c6251164d fix(eod): FR007收盘检查日按计息取价日回拨,抽出EodCheckFr007Price
- 检查日改用 IndexFixerBase.GetFixingDate(与计息取价同源):
  交易日=当日(行为零变更),周末/节假日=上一交易日,
  解除非交易日收盘"必报FR007未入库"死锁(2026-08-28:0523周六收盘)
- 缺价报错附回拨说明与涉及FR007浮动的交易编号(上限20笔),
  交易日缺价文案与旧版格式一致
- FR007检查从EodCheckSettlePrice抽出为独立检查服务EodCheckFr007Price,
  报错沿用原步骤名;新增8个无库单测
2026-08-28 15:46:06 +08:00
hjhan 587c0d3fc2 fix: 实时持仓服务轮询循环增加等待间隔,修正新流水增量判断基准
- Worker: 风险/预付金计算由连续空转改为30秒一轮(可被取消令牌立即唤醒)
- ClientPosiTask: HasNewFlow 改为与上次计算时间比较(原误传当前时间致增量判断失效),轮询间隔2秒
- 30秒兜底全量重算语义保留不变
2026-08-28 15:45:08 +08:00
tengyufan fe38e59a9c fix: 修正OA期初价格展示 2026-08-28 15:06:14 +08:00
tengyufan a3e489fcd3 chore: 完善OA审批同步追踪日志 2026-08-28 14:51:46 +08:00
tengyufan d2c0e9a9cb Merge remote-tracking branch 'origin/glms/feature/1.4.2' into glms/feature/1.4.2 2026-08-28 14:43:33 +08:00
tengyufan 3a1f1958fc chore: 补充OA审批同步追踪日志 2026-08-28 14:42:59 +08:00
张名锐 dc9ef5770b Merge remote-tracking branch 'origin/glms/feature/1.4.2' into glms/feature/1.4.2 2026-08-28 14:37:12 +08:00
张名锐 1f3fb9240b fix(trade): 修正交易确认书中的基本费率计算逻辑
- 注释掉原有的基本费率计算公式
- 直接使用PosiTradingFeeUnit字段获取基础费率
- 避免通过PosiTradingFeePending除以名义本金的方式计算
- 确保基本费率显示的准确性
2026-08-28 14:36:54 +08:00
hjhan db95699aa2 feat: 基金管理人取数支持跨环境库名——新增 DbSchema 库名插值,bigdata 连接串指向 96/glms_bigdata
问题: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(数据覆盖问题)
2026-08-28 14:31:30 +08:00
21 changed files with 544 additions and 247 deletions
@@ -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
{
@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using YLErp.DBModels;
using YLErp.Modules.MarginModule;
namespace YLErp.Modules.CalcModules
{
/// <summary>
/// 模板同层多候选取舍纯函数测试(MarginTemplateV2RateHelper.PickByBookScopePreference2026-08-28 裁定):
/// 簿记条件满足者优先,其次生效日最新。不连库。
/// </summary>
[TestClass]
public class MarginTemplateV2BookScopePreferenceTest
{
private static readonly DateTime Newer = new DateTime(2026, 8, 1);
private static readonly DateTime Older = new DateTime(2026, 1, 1);
private static margin_template_v2 Tpl(int id, DateTime valueDate, string bookIds = null)
{
return new margin_template_v2 { id = id, ValueDate = valueDate, BookIds = bookIds, IsValid = true };
}
[TestMethod]
public void BP_01_双默认同日_限定簿记匹配_压过id更新的通配()
{
//复刻 dev 274/275 场景:同生效日按 id 降序,通配模板排前、限定簿记(45)排后;交易簿记=45
var candidates = new List<margin_template_v2> { Tpl(275, Older), Tpl(274, Older, "45") };
var picked = MarginTemplateV2RateHelper.PickByBookScopePreference(candidates, 45, out var byBook);
Assert.AreEqual(274, picked.id, "簿记条件满足的限定模板应压过同日 id 更新的通配模板");
Assert.IsTrue(byBook, "命中由簿记匹配优先产生时应置 preferredByBookScope");
}
[TestMethod]
public void BP_02_簿记不匹配_通配胜出()
{
//交易簿记=45 不在限定范围(38)内:限定模板不适用,按序取通配(2573 客需债券簿记场景)
var candidates = new List<margin_template_v2> { Tpl(275, Older), Tpl(274, Older, "38") };
var picked = MarginTemplateV2RateHelper.PickByBookScopePreference(candidates, 45, out var byBook);
Assert.AreEqual(275, picked.id);
Assert.IsFalse(byBook);
}
[TestMethod]
public void BP_03_无限定候选_取生效日最新()
{
var candidates = new List<margin_template_v2> { Tpl(2, Newer), Tpl(1, Older) };
var picked = MarginTemplateV2RateHelper.PickByBookScopePreference(candidates, 45, out var byBook);
Assert.AreEqual(2, picked.id, "无簿记限定候选时维持原口径:生效日最新胜出");
Assert.IsFalse(byBook);
}
[TestMethod]
public void BP_04_多限定都匹配_取生效日最新()
{
var candidates = new List<margin_template_v2> { Tpl(2, Newer, "45,46"), Tpl(1, Older, "45") };
var picked = MarginTemplateV2RateHelper.PickByBookScopePreference(candidates, 45, out var byBook);
Assert.AreEqual(2, picked.id, "多个簿记条件满足的候选之间仍按生效日最新取第一个");
Assert.IsFalse(byBook, "第一个适用候选即簿记匹配时不算'优先压过'");
}
[TestMethod]
public void BP_05_限定匹配_压过生效日更新的通配()
{
var candidates = new List<margin_template_v2> { Tpl(2, Newer), Tpl(1, Older, "45") };
var picked = MarginTemplateV2RateHelper.PickByBookScopePreference(candidates, 45, out var byBook);
Assert.AreEqual(1, picked.id, "簿记条件满足优先于生效日新旧");
Assert.IsTrue(byBook);
}
[TestMethod]
public void BP_06_全部不适用_返回null()
{
var candidates = new List<margin_template_v2> { Tpl(1, Newer, "38") };
var picked = MarginTemplateV2RateHelper.PickByBookScopePreference(candidates, 45, out _);
Assert.IsNull(picked, "候选均不适用当前簿记账户时应返回 null(穿透/未命中由调用方处理)");
}
[TestMethod]
public void BP_07_交易无簿记账户_限定视为不适用()
{
//tradeId 查不到 AssetId 时入参为 null:显式限定模板视为不适用,通配仍可用
var candidates = new List<margin_template_v2> { Tpl(2, Newer), Tpl(1, Older, "45") };
var picked = MarginTemplateV2RateHelper.PickByBookScopePreference(candidates, null, out var byBook);
Assert.AreEqual(2, picked.id);
Assert.IsFalse(byBook);
}
[TestMethod]
public void BP_08_空候选或null_返回null()
{
Assert.IsNull(MarginTemplateV2RateHelper.PickByBookScopePreference(new List<margin_template_v2>(), 45, out _));
Assert.IsNull(MarginTemplateV2RateHelper.PickByBookScopePreference(null, 45, out _));
}
}
}
@@ -55,7 +55,7 @@ namespace YLErp.Modules.CalcModules
db.SaveChanges();
}
private margin_template_v2 AddTemplate(string name, bool isDefault, bool isForClient, string tradeTypes = "收益互换", bool isValid = true, int ruleType = (int)MarginRuleTypeEnum.)
private margin_template_v2 AddTemplate(string name, bool isDefault, bool isForClient, string tradeTypes = "收益互换", bool isValid = true, int ruleType = (int)MarginRuleTypeEnum., string bookIds = null, DateTime? valueDate = null)
{
var t = new margin_template_v2
{
@@ -65,7 +65,8 @@ namespace YLErp.Modules.CalcModules
IsValid = isValid,
TradeTypes = tradeTypes,
RuleType = ruleType,
ValueDate = EffectiveDate
ValueDate = valueDate ?? EffectiveDate,
BookIds = bookIds
};
db.margin_template_v2.Add(t);
db.SaveChanges();
@@ -179,6 +180,54 @@ namespace YLErp.Modules.CalcModules
Assert.AreEqual(0.03m, rate.MaintainRate.Value);
}
/// <summary>
/// 双全局默认并存(2026-08-28 裁定回归,复刻 dev 274/275 结构):
/// 同生效日下通配模板(无簿记限制)id 更新排前,限定簿记且匹配的模板排后——
/// 原口径"先按生效日/id 取最新再验簿记"使通配恒胜、限定簿记模板永不生效;
/// 新口径簿记条件满足者优先。需一笔隔离的真实交易(有簿记账户、无交易绑定,客户无客户级绑定)。
/// </summary>
[TestMethod]
public void TF_009_双全局默认_限定簿记匹配优先于通配()
{
var boundTradeIds = db.trade_margin_template.AsNoTracking().Select(x => x.TradeId).ToList();
var boundClientIds = db.client_margin_template.AsNoTracking()
.Where(x => x.ClientId > 0).Select(x => x.ClientId).Distinct().ToList();
var levelBoundNames = db.client_margin_template.AsNoTracking()
.Where(x => x.ClientId == 0 && x.ClientLevel != null && x.ClientLevel != "")
.Select(x => x.ClientLevel).Distinct().ToList();
int tradeId, assetId, tradeClientId;
using (var clientDb = DbContextFactory.GetClientDbContext(OptUserInfo.SystemUser))
{
var excludedLevelIds = clientDb.clientlevel.AsNoTracking()
.Where(l => levelBoundNames.Contains(l.LevelName)).Select(l => l.id).ToList();
var excludedClientIds = clientDb.client.AsNoTracking()
.Where(c => boundClientIds.Contains(c.id) || (c.LevelId != null && excludedLevelIds.Contains(c.LevelId ?? 0)))
.Select(c => c.id).ToList();
var picked = db.trade.AsNoTracking()
.Where(t => t.AssetId > 0 && t.ValidState != "InValid"
&& !boundTradeIds.Contains(t.id) && !excludedClientIds.Contains(t.ClientId))
.OrderByDescending(t => t.id)
.Select(t => new { t.id, t.AssetId, t.ClientId }).FirstOrDefault();
if (picked == null)
{
Assert.Inconclusive("dev 库无可用的隔离测试交易(有簿记账户且交易/客户均无绑定)");
return;
}
tradeId = picked.id;
assetId = picked.AssetId;
tradeClientId = picked.ClientId;
}
//同生效日(今日,晚于 dev 既有默认模板)双全局默认:通配后建(id 更大排前)
var generic = AddTemplate(Marker + "全局通配", isDefault: true, isForClient: false, valueDate: DateTime.Today);
var bookScoped = AddTemplate(Marker + "全局限定簿记", isDefault: true, isForClient: false, bookIds: assetId.ToString(), valueDate: DateTime.Today);
var resolved = MarginTemplateV2RateHelper.ResolveTieredTemplate(tradeId, tradeClientId, DateTime.Today, db);
Assert.IsNotNull(resolved);
Assert.IsTrue(resolved.Name.StartsWith(Marker), $"应命中本用例创建的标记模板,实际命中模板{resolved.id}(dev 出现同日/更晚生效默认模板会干扰,请重跑)");
Assert.AreEqual(bookScoped.id, resolved.id, "限定簿记且匹配的全局默认应优先于同生效日的通配全局默认");
}
private void AssertGlobal(margin_template_v2 resolved, margin_template_v2 expected)
{
Assert.IsNotNull(resolved);
@@ -0,0 +1,86 @@
using YLErp.Modules.EodModule.SettlementModule;
namespace YLErp.Modules.EodModule
{
/// <summary>
/// 收盘 FR007 定盘检查(EodCheckFr007Price)行为契约(纯内存,不连库):
/// ① 检查日回拨:交易日 = 收盘日自身(对既有交易日收盘行为零变更);
/// 周末/节假日 = 上一交易日——非交易日无 FR007 定盘,按裸收盘日检查会形成
/// "收盘必报缺价"死锁(2026-08-280523 周六收盘即此症);
/// ② 回拨规则与计息取价同源(IndexFixerBase.GetFixingDate),检查的日期
/// 就是计息路径实际会读的日期;
/// ③ 报错文案:交易日缺价保持旧格式,非交易日注明回拨,附交易编号供定位。
/// </summary>
[TestClass]
public class EodCheckFr007PriceTest
{
[TestMethod]
public void _交易日收盘_等于收盘日自身()
{
// 2026-05-22 周五、2026-08-28 周五:正常交易日收盘,行为必须与改造前一致
Assert.AreEqual(new DateTime(2026, 5, 22), EodCheckFr007Price.GetCheckDate(new DateTime(2026, 5, 22)));
Assert.AreEqual(new DateTime(2026, 8, 28), EodCheckFr007Price.GetCheckDate(new DateTime(2026, 8, 28)));
}
[TestMethod]
public void _周六收盘_回拨到上一交易日周五()
{
// 2026-08-28 缺陷场景:0523 为周六,FR007 非发布日永无定盘行,
// 旧逻辑检查裸收盘日必报"未入库"且无法通过补交易要素绕过
Assert.AreEqual(new DateTime(2026, 5, 22), EodCheckFr007Price.GetCheckDate(new DateTime(2026, 5, 23)));
}
[TestMethod]
public void _周日收盘_回拨跳过周末到周五()
{
// 与 GetInterestsUnitTest_T0 记录的取价回拨先例一致:GetNonHolidayDefore(4/26周日)→4/24周五
Assert.AreEqual(new DateTime(2026, 4, 24), EodCheckFr007Price.GetCheckDate(new DateTime(2026, 4, 26)));
}
[TestMethod]
public void _与计息取价日同源()
{
// 同一收盘日,检查日必须等于计息路径 interest_rule=0 腿实际读的取价日
var settleDate = new DateTime(2026, 5, 23);
var interestPathFixingDate = YLErp.Derivatives.Interest.IndexFixerBase.GetFixingDate(settleDate, 0);
Assert.AreEqual(interestPathFixingDate, EodCheckFr007Price.GetCheckDate(settleDate));
}
[TestMethod]
public void _交易日缺价_格式与旧版一致()
{
// 交易日缺价:不带回拨说明、无交易则不带交易段,保持"yyyy年MM月dd日FR007价格未入库"原格式
var msg = EodCheckFr007Price.BuildMissingMessage(new DateTime(2026, 5, 22), new DateTime(2026, 5, 22), Array.Empty<string>());
Assert.AreEqual("2026年05月22日FR007价格未入库", msg);
}
[TestMethod]
public void _非交易日回拨缺价_注明回拨()
{
var msg = EodCheckFr007Price.BuildMissingMessage(new DateTime(2026, 5, 23), new DateTime(2026, 5, 22), null);
StringAssert.Contains(msg, "2026年05月22日FR007价格未入库");
StringAssert.Contains(msg, "收盘日2026年05月23日为非交易日,已回拨至上一交易日检查");
}
[TestMethod]
public void _带交易编号供定位()
{
var msg = EodCheckFr007Price.BuildMissingMessage(
new DateTime(2026, 5, 22), new DateTime(2026, 5, 22),
new[] { "TRS-001", "TRS-002" });
StringAssert.EndsWith(msg, ";涉及FR007浮动的交易: TRS-001、TRS-002");
}
[TestMethod]
public void _交易超过上限_截断并以等N笔收尾()
{
var trades = Enumerable.Range(1, 25).Select(i => $"TRS-{i:D3}").ToArray();
var msg = EodCheckFr007Price.BuildMissingMessage(
new DateTime(2026, 5, 22), new DateTime(2026, 5, 22), trades);
StringAssert.Contains(msg, $"TRS-{EodCheckFr007Price.MaxTradesInMessage:D3}");
StringAssert.Contains(msg, "等25笔");
Assert.IsFalse(msg.Contains("TRS-021"), "超过上限的交易编号不应完整列出");
Assert.IsFalse(msg.Contains("TRS-025、"), "截断后不应再拼接后续编号");
}
}
}
@@ -0,0 +1,46 @@
using Dapper;
using YLErp.Modules.UnderlyingModule;
namespace YLErp.UnitTestProject.Modules.UnderlyingModule
{
/// <summary>
/// 基金管理人取数(bigdata 数据源)连通性与库名插值验证(连 96 测试库):
/// - DbSchema.Of 从连接串解析物理库名(glms_bigdata),SQL 不再硬编码库名、跨环境库名不同也能命中;
/// - Lookup 全链路(连接 96 → 插值 SQL 执行 → 结果归并)不返回 Unavailable 即为连通且 SQL 有效。
/// </summary>
[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/511160SECUCODE 无后缀,与去后缀匹配逻辑一致),用库存代码验证全链路命中
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} 返回 Unavailable96 bigdata 库不可达或插值 SQL 执行失败");
}
}
[TestMethod]
public void Diag_查看库存secucode样例与行数()
{
using var connection = new MySqlConnector.MySqlConnection(AppManager.GetConnectionString("bigdata"));
var count = connection.ExecuteScalar<long>($"SELECT COUNT(*) FROM {DbSchema.Of("bigdata")}.mf_fundarchives");
var samples = connection.Query<string>($"SELECT SECUCODE FROM {DbSchema.Of("bigdata")}.mf_fundarchives LIMIT 8");
Console.WriteLine($"mf_fundarchives 行数={count}, SECUCODE样例=[{string.Join(",", samples)}]");
var advCount = connection.ExecuteScalar<long>($"SELECT COUNT(*) FROM {DbSchema.Of("bigdata")}.mf_investadvisoroutline");
Console.WriteLine($"mf_investadvisoroutline 行数={advCount}");
}
}
}
+2 -1
View File
@@ -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": "",
+41
View File
@@ -0,0 +1,41 @@
using MySqlConnector;
using System.Collections.Concurrent;
using System.Text.RegularExpressions;
namespace YLErp.BLL
{
/// <summary>
/// 跨库 SQL 的物理库名解析器:代码里只写逻辑连接名(appsettings ConnectionStrings 的 key
/// 与 Java @Mapper 指定数据源名同语义),物理库名由各环境连接串的 database= 决定——
/// 同一逻辑库在不同环境库名不同(如 bigdata 在测试环境为 glms_bigdata),SQL 中硬编码库名会跨环境失败。
/// 用法:{@DbSchema.Of("bigdata")}.mf_fundarchives(返回带反引号的库名,可直接内插)。
/// 仅标识自家的 appsettings 连接串,值不来自用户输入;仍做 \w+ 白名单校验防御配置笔误。
/// </summary>
public static class DbSchema
{
private static readonly ConcurrentDictionary<string, string> Cache = new();
private static readonly Regex SafeIdentifier = new(@"^\w+$", RegexOptions.Compiled);
/// <summary>
/// 取逻辑连接名对应的物理库名(形如 `glms_bigdata`,含反引号)。配置缺失或库名非法立即抛错——
/// 跨库 SQL 拼错库名在运行期才暴露更难排查,配置错误应尽早失败。
/// </summary>
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 + "`";
});
}
}
}
@@ -0,0 +1,98 @@
using YLErp.Derivatives.Interest;
using YLErp.Modules.DataProviderModule;
namespace YLErp.Modules.EodModule.SettlementModule
{
/// <summary>
/// 收盘 FR007 定盘入库检查(独立检查服务,由 EodCheckSettlePrice 在
/// "检查标的结算价格缺失"步骤内编排调用——报错前缀沿用该步骤名,保持既有报错口径)。
///
/// 职责边界:只回答"计息会读的那个取价日,FR007 定盘行是否已入库"。
/// 日期推导与计息路径共用同一套日历函数(IndexFixerBase.GetFixingDate),
/// 保证检查口径 ≡ 取数口径。第二步完整版(按各浮动腿重置日推导所需日期集合 +
/// 批量校验 + Fr007FixingCache 复用)也落在本类,不回填 EodCheckSettlePrice。
/// </summary>
public class EodCheckFr007Price : EodSettleServiceBaseV2
{
/// <summary>报错里最多列出的交易编号数,超出以"等N笔"收尾,防刷屏。</summary>
internal const int MaxTradesInMessage = 20;
public EodCheckFr007Price(EodSettlementContextV2 context) : base(context)
{
ResetDbContext();
}
/// <summary>
/// 检查结算日(回拨后的取价日)FR007 定盘是否入库;缺价抛 EodSettleException 终止收盘。
/// step 由调用方传入:本检查隶属于"检查标的结算价格缺失"步骤,报错前缀与其保持一致。
/// </summary>
public void Execute(string step)
{
var checkDate = GetCheckDate(_context.SettleDate);
if (!EodPriceQueryService.CheckFR007Price(checkDate))
{
var tradeNumbers = GetFloatTradeNumbers(); // 仅缺价时才查,正常收盘零开销
_context.RaiseError(step, BuildMissingMessage(_context.SettleDate, checkDate, tradeNumbers));
}
}
/// <summary>
/// 收盘 FR007 检查日:与计息取价共用同一套日历函数(IndexFixerBase.GetFixingDate),
/// 保证"检查的日期"与"计息实际读的日期"始终一致。
///
/// 回拨发生在日历层(GetNonHolidayDefore),与 interest_rule 取值无关:
/// 交易日 → 当日自身(工作日收盘行为零变更);
/// 周末/节假日 → 上一交易日(非交易日无定盘,按裸收盘日检查会形成
/// "非交易日收盘必报缺价"的死锁——2026-08-28 缺陷:0523 周六收盘
/// 报"0523 FR007未入库",且检查只看收盘日不看任何交易,改交易要素无法绕过)。
/// 抽成纯函数便于无数据库单测。
/// </summary>
internal static DateTime GetCheckDate(DateTime settleDate)
=> IndexFixerBase.GetFixingDate(settleDate.Date, 0);
/// <summary>
/// 组装 FR007 缺价报错文案(纯函数便于单测):
/// 显示实际检查日(= 计息会读的取价日);收盘日为非交易日时注明已回拨;
/// 附涉及 FR007 浮动的交易编号供定位——注意这是"涉及"而非"精确需要该日定盘"
/// (精确归因需按各腿重置日推导,属第二步完整版改造,此处宁多报不漏报)。
/// </summary>
internal static string BuildMissingMessage(DateTime settleDate, DateTime checkDate, IReadOnlyList<string> fr007TradeNumbers)
{
var rollbackNote = checkDate != settleDate.Date
? $"(收盘日{settleDate:yyyy年MM月dd日}为非交易日,已回拨至上一交易日检查)"
: string.Empty;
string tradeNote;
if (fr007TradeNumbers == null || fr007TradeNumbers.Count == 0)
{
tradeNote = string.Empty;
}
else
{
var listed = string.Join("、", fr007TradeNumbers.Take(MaxTradesInMessage));
tradeNote = fr007TradeNumbers.Count > MaxTradesInMessage
? $";涉及FR007浮动的交易: {listed} 等{fr007TradeNumbers.Count}笔"
: $";涉及FR007浮动的交易: {listed}";
}
return $"{checkDate:yyyy年MM月dd日}FR007价格未入库{rollbackNote}{tradeNote}";
}
/// <summary>
/// 结算范围内挂 FR007 浮动利率的交易编号。仅在缺价报错时调用(正常收盘零开销)。
/// 口径是"结算范围内 FR007 浮动腿所属交易",不含重置日归因——非重置日的交易当日
/// 不取价、也一并列出,定位时宁多报不漏报。
/// </summary>
private string[] GetFloatTradeNumbers()
{
var tradePredicate = _context.PredicateBuilder.GetOtcSwapTradePredicate();
return (from t in DbContext.trade.Where(tradePredicate)
join p in DbContext.swap_position on t.id equals p.SwapTradeId
where !p.Invalid && p.FloatRateUnderlyingCode == "FR007"
select t.TradeNumber)
.Distinct()
.OrderBy(n => n)
.ToArray();
}
}
}
@@ -32,10 +32,9 @@ namespace YLErp.Modules.EodModule.SettlementModule
var clienIds = _context.Request.ClientIds;
IQueryable<string> allQuery = null;
if (!EodPriceQueryService.CheckFR007Price(_context.SettleDate))
{
_context.RaiseError(Step, $"{_context.SettleDate:yyyy年MM月dd日}FR007价格未入库");
}
// FR007 定盘检查委托给独立检查服务(职责分离):检查日=计息实际读取的取价日,
// 交易日=当日、周末/节假日=上一交易日,报错沿用本步骤名保持既有口径。
new EodCheckFr007Price(_context).Execute(Step);
//判断当日结算价是否已经入库
if (!EodPriceQueryService.CheckDbExists(_context.SettleDate))
{
@@ -13,8 +13,8 @@ namespace YLErp.Modules.MarginModule
/// <summary>
/// 预付金模板V2 取数帮助类:按交易绑定的模板(trade_margin_template → margin_template_v2 → margin_template_detail
/// 取 初始预付金率x / 维持预付金率y,供交易确认书、预付金计算等消费点统一使用。
/// 匹配维度:簿记账户范围(BookIds,空=全部)→ 生效日期(ValueDate 最新)→
/// 利率债期限档(SpanConfig.BondTerm,四档,空=全部兜底)→ 标的资产类型(UnderlyingType 标志位)。
/// 匹配维度:簿记账户范围(BookIds 显式限定且匹配者优先于空=全部的通配,同层多候选 2026-08-28 裁定)→
/// 生效日期(ValueDate 最新)→ 利率债期限档(SpanConfig.BondTerm,四档,空=全部兜底)→ 标的资产类型(UnderlyingType 标志位)。
/// </summary>
public static class MarginTemplateV2RateHelper
{
@@ -214,6 +214,7 @@ namespace YLErp.Modules.MarginModule
/// 客户等级经 client.LevelId → clientlevel.LevelName 解析;ValueDate 最新 ≤ 业务日),
/// 模板须有效、适用结构含收益互换且适用当前交易簿记账户;
/// 3.全局默认 margin_template_v2IsDefault && !IsForClient && IsValid && 适用收益互换,且适用当前簿记账户,ValueDate 最新)。
/// 2/3 层多候选并存时按 PickByBookScopePreference 取舍:簿记条件满足者优先,其次生效日最新(2026-08-28)。
/// </summary>
public static margin_template_v2 ResolveTieredTemplate(int? tradeId, int clientId, DateTime valueDate, YLContext db)
{
@@ -268,25 +269,25 @@ namespace YLErp.Modules.MarginModule
&& mt.IsValid && mt.TradeTypes.Contains("收益互换")
orderby cmt.ValueDate descending, cmt.id descending
select mt).ToList();
var ret = clientTemplate.FirstOrDefault(x => x.IsApplicableToBook(tradeAssetId));
//同层多候选:簿记条件满足者优先,其次绑定生效日最新(2026-08-28 裁定)
var ret = PickByBookScopePreference(clientTemplate, tradeAssetId, out var clientByBook);
if (ret != null)
{
logger.Info($"预付金模板取数:交易{tradeId}(客户{clientId},等级{levelName ?? ""})命中层级=客户默认 → 模板{ret.id}");
logger.Info($"预付金模板取数:交易{tradeId}(客户{clientId},等级{levelName ?? ""})命中层级=客户默认 → 模板{ret.id}{(clientByBook ? "簿" : "")}");
return ret;
}
}
//3.全局默认
var globalDefault = db.margin_template_v2.AsNoTracking()
//3.全局默认(同层多候选:簿记条件满足者优先,其次模板生效日最新——双全局默认并存时通配模板不再凭 id 最新恒胜,2026-08-28
var globalDefault = PickByBookScopePreference(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)
.ToList()
.FirstOrDefault(x => x.IsApplicableToBook(tradeAssetId));
.ToList(), tradeAssetId, out var globalByBook);
if (globalDefault != null)
{
logger.Info($"预付金模板取数:交易{tradeId}(客户{clientId})命中层级=全局默认 → 模板{globalDefault.id}");
logger.Info($"预付金模板取数:交易{tradeId}(客户{clientId})命中层级=全局默认 → 模板{globalDefault.id}{(globalByBook ? "簿" : "")}");
}
return globalDefault;
}
@@ -298,6 +299,7 @@ namespace YLErp.Modules.MarginModule
/// 1.交易绑定(ValueDate 最新 ≤ 业务日,同日多条按 id 最新兜底)找到即停,绑定指向失效模板同样停止(不回退);
/// 2.客户默认(client_margin_template 按客户或按客户等级,模板须有效、适用结构含收益互换且适用当前簿记账户,先过滤有效再取绑定最新);
/// 3.全局默认(IsDefault&&!IsForClient&&IsValid&&适用收益互换,按每笔交易簿记账户取ValueDate/id 最新)。
/// 2/3 层多候选并存时按 PickByBookScopePreference 取舍:簿记条件满足者优先,其次生效日最新(2026-08-28)。
/// 返回 tradeId → 命中模板;未命中或因失效绑定停止的交易不在结果中。
/// </summary>
public static Dictionary<int, margin_template_v2> ResolveTieredTemplates(List<trade> trades, DateTime valueDate, YLContext db)
@@ -400,20 +402,23 @@ namespace YLErp.Modules.MarginModule
continue;
}
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));
//同层多候选:先按客户/等级筛出候选(保持绑定生效日降序),再簿记条件满足者优先、其次生效日最新(与单笔解析一致)
var clientMatchCandidates = clientTemplateCandidates
.Where(x => x.ClientId == trade.ClientId || (x.ClientId == 0 && x.ClientLevel == levelName))
.Select(x => x.Template)
.ToList();
var clientTemplate = PickByBookScopePreference(clientMatchCandidates, trade.AssetId, out var clientByBook);
if (clientTemplate != null)
{
result[trade.id] = clientTemplate.Template;
logger.Info($"预付金模板取数:交易{trade.id}(客户{trade.ClientId},等级{levelName ?? ""})命中层级=客户默认 → 模板{clientTemplate.Template.id}");
result[trade.id] = clientTemplate;
logger.Info($"预付金模板取数:交易{trade.id}(客户{trade.ClientId},等级{levelName ?? ""})命中层级=客户默认 → 模板{clientTemplate.id}{(clientByBook ? "簿" : "")}");
continue;
}
var globalDefault = globalDefaults.FirstOrDefault(x => x.IsApplicableToBook(trade.AssetId));
var globalDefault = PickByBookScopePreference(globalDefaults, trade.AssetId, out var globalByBook);
if (globalDefault != null)
{
result[trade.id] = globalDefault;
logger.Info($"预付金模板取数:交易{trade.id}(客户{trade.ClientId})命中层级=全局默认 → 模板{globalDefault.id}");
logger.Info($"预付金模板取数:交易{trade.id}(客户{trade.ClientId})命中层级=全局默认 → 模板{globalDefault.id}{(globalByBook ? "簿" : "")}");
}
else
{
@@ -424,6 +429,35 @@ namespace YLErp.Modules.MarginModule
return result;
}
/// <summary>
/// 同层级多候选并存时的取舍(2026-08-28 裁定,纯函数单测覆盖):
/// 入参候选序须已按生效日最新在前(客户层=绑定生效日,全局层=模板生效日,同日按 id 最新)。
/// 1.存在"簿记条件满足"的候选(BookIds 显式限定且经 IsApplicableToBook 命中当前交易簿记账户)→ 优先取之,
/// 多个命中时仍按传入顺序取生效日最新;
/// 2.否则取顺序第一个(生效日最新)。
/// 背景:双全局默认并存(如 无预付金限定簿记 + 区间追保通配)时,原"先按生效日/id 取最新再验簿记"
/// 使通配模板恒胜、限定簿记模板永不生效(交易2572/2573 实证)。
/// preferredByBookScope=true 表示本次由簿记匹配压过了顺序更前的通配候选(供日志辨识)。
/// </summary>
public static margin_template_v2 PickByBookScopePreference(List<margin_template_v2> candidatesByLatestFirst, int? tradeAssetId, out bool preferredByBookScope)
{
preferredByBookScope = false;
if (candidatesByLatestFirst == null || candidatesByLatestFirst.Count == 0)
{
return null;
}
var applicable = candidatesByLatestFirst.Where(x => x.IsApplicableToBook(tradeAssetId)).ToList();
var bookMatched = applicable.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.BookIds));
if (bookMatched == null)
{
return applicable.FirstOrDefault();
}
preferredByBookScope = applicable[0].id != bookMatched.id;
return bookMatched;
}
/// <summary>
/// 标的细分分类判定钩子(ETF 子类,2026-08-24 实装):基金类标的返回标的维护页维护的
/// underlying_manager.EtfSubType(取值来自数据字典"ETF 子类":国债 ETF/政金债 ETF/…/科创债 ETF/可转债 ETF
@@ -3,6 +3,7 @@ using Newtonsoft.Json.Linq;
using YLErp.DBModels;
using YLErp.DBModels.Consts;
using YLErp.DBModels.Enums;
using YLErp.Helpers;
using YLErp.Modules.ApiModule;
using YLErp.Modules.TradeModule.DealModule;
@@ -225,13 +226,13 @@ namespace YLErp.Modules.TradeModule
&& (string.IsNullOrWhiteSpace(requestId) || x.oa_fileid == requestId)
&& (x.status == Pending || (x.status == Syncing && (x.last_query_time == null || x.last_query_time < staleAt))))
.ToList();
_logger.Info($"开始轮询 OA 移动审批状态,requestId:{requestId ?? ""},待查询数量:{pending.Count}");
_logger.Info($"开始轮询 OA 移动审批状态,requestId:{requestId ?? ""},待查询数量:{pending.Count},同步中超时阈值:{staleAt:yyyy-MM-dd HH:mm:ss.fff}");
foreach (var record in pending)
{
var claimed = false;
try
{
_logger.Info($"查询 OA 移动审批状态,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid}");
_logger.Info($"查询 OA 移动审批状态,OA记录:{record.id}交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid},本地状态:{record.status},上次查询:{record.last_query_time:yyyy-MM-dd HH:mm:ss.fff}");
var response = Query(record.oa_fileid);
record.last_response = response.ToString();
var flow = response["data"] is JArray data
@@ -251,31 +252,37 @@ namespace YLErp.Modules.TradeModule
var isReturned = flowStatusType == "0" && flowNode == "退回";
var isApproved = flowStatusType == "3"
&& (flowNode == "结束" || flowNode == "强制归档");
_logger.Info($"OA 移动审批查询结果,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid}flowStatusType:{flowStatusType}flowNode:{flowNode},判定:{(isReturned ? "退" : isApproved ? "" : "")}");
_logger.Info($"OA 移动审批查询结果,OA记录:{record.id}交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid}是否命中流程:{flow != null}flowStatusType:{flowStatusType}flowNode:{flowNode}是否退回:{isReturned},是否通过:{isApproved}判定:{(isReturned ? "退" : isApproved ? "" : "")}");
if (!isApproved && !isReturned)
{
_logger.Info($"OA 移动审批继续等待,更新最近查询时间但不修改本地审批流程,OA记录:{record.id}requestId:{record.oa_fileid},本地状态保持:{record.status}");
record.last_query_time = DateTime.Now;
DbContext.SaveChanges();
continue;
}
DbContext.SaveChanges();
if (!TryClaimForSync(record.id))
_logger.Info($"OA 审批结果满足回写条件,准备将记录设为同步中,OA记录:{record.id},交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid},当前本地状态:{record.status}");
var claimAffectedRows = ClaimForSync(record.id);
if (claimAffectedRows != 1)
{
_logger.Info($"OA 审批结果同步未抢占记录,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid}");
_logger.Info($"OA 审批结果未能将记录设为同步中,OA记录:{record.id}requestId:{record.oa_fileid},更新行数:{claimAffectedRows}");
continue;
}
claimed = true;
_logger.Info($"OA 审批结果已将记录设为同步中,OA记录:{record.id}requestId:{record.oa_fileid},更新行数:{claimAffectedRows}");
var trade = DbContext.trade.Find(record.trade_id);
if (trade == null)
{
SetClaimedStatus(record.id, "同步失败", "交易不存在");
var statusAffectedRows = SetClaimedStatus(record.id, "同步失败", "交易不存在");
_logger.Error($"OA 审批结果不再回写,交易不存在,OA记录:{record.id}requestId:{record.oa_fileid},目标状态:同步失败,更新行数:{statusAffectedRows}");
continue;
}
if (GetCurrentNode(trade)?.id != record.approval_process_id)
{
SetClaimedStatus(record.id, "同步忽略", "当前审批节点已变化,忽略 OA 结果");
var statusAffectedRows = SetClaimedStatus(record.id, "同步忽略", "当前审批节点已变化,忽略 OA 结果");
_logger.Info($"OA 审批结果不再回写,当前审批节点已变化,OA记录:{record.id}requestId:{record.oa_fileid},目标状态:同步忽略,更新行数:{statusAffectedRows}");
continue;
}
var result = new TradeOpenService(this).UpdateTradeProcessLog(new TradeOpenReqModel
@@ -288,13 +295,14 @@ namespace YLErp.Modules.TradeModule
});
if (!string.IsNullOrWhiteSpace(result.ErrorMsg))
{
SetClaimedStatus(record.id, Pending, result.ErrorMsg);
_logger.Error($"OA 审批结果回写本地失败,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid},原因:{result.ErrorMsg}");
var statusAffectedRows = SetClaimedStatus(record.id, Pending, result.ErrorMsg);
_logger.Error($"OA 审批结果回写本地失败,OA记录:{record.id}交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid}目标状态:{Pending},更新行数:{statusAffectedRows}原因:{result.ErrorMsg}");
}
else
{
SetClaimedStatus(record.id, isReturned ? "退回" : "通过", null);
_logger.Info($"OA 审批结果回写本地成功,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid},结果:{(isReturned ? "退" : "")}");
var targetStatus = isReturned ? "退回" : "通过";
var statusAffectedRows = SetClaimedStatus(record.id, targetStatus, null);
_logger.Info($"OA 审批结果回写本地成功,OA记录:{record.id},交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid},本地审批结果:{(isReturned ? "" : "")}OA记录目标状态:{targetStatus},更新行数:{statusAffectedRows}");
}
messages.Add(record.trade_id + ":" + (string.IsNullOrWhiteSpace(result.ErrorMsg) ? (isReturned ? "退回" : "通过") : Pending));
}
@@ -302,7 +310,8 @@ namespace YLErp.Modules.TradeModule
{
if (claimed)
{
SetClaimedStatus(record.id, Pending, ex.Message);
var statusAffectedRows = SetClaimedStatus(record.id, Pending, ex.Message);
_logger.Error($"OA 轮询异常后恢复记录状态,OA记录:{record.id}requestId:{record.oa_fileid},目标状态:{Pending},更新行数:{statusAffectedRows}", ex);
}
else
{
@@ -310,7 +319,7 @@ namespace YLErp.Modules.TradeModule
record.oa_msg = ex.Message;
DbContext.SaveChanges();
}
_logger.Error($"轮询 OA 审批状态失败,requestId:{record.oa_fileid}", ex);
_logger.Error($"轮询 OA 审批状态失败,OA记录:{record.id},交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid},是否已设为同步中:{claimed}", ex);
}
}
_logger.Info($"OA 移动审批状态轮询结束,已处理数量:{messages.Count}");
@@ -365,13 +374,13 @@ namespace YLErp.Modules.TradeModule
|| trade.TradeStatus == ConsTrade.;
}
private bool TryClaimForSync(int recordId)
private int ClaimForSync(int recordId)
{
var now = DateTime.Now;
return DbContext.Database.ExecuteSqlRaw(
"UPDATE trade_approval_oa_result SET status = {0}, last_query_time = {1} " +
"WHERE id = {2} AND is_valid = 1 AND (status = {3} OR (status = {0} AND (last_query_time IS NULL OR last_query_time < {4})))",
Syncing, now, recordId, Pending, now.AddMinutes(-5)) == 1;
Syncing, now, recordId, Pending, now.AddMinutes(-5));
}
private bool TryChangeStatus(int recordId, string expectedStatus, string targetStatus, string message)
@@ -381,9 +390,9 @@ namespace YLErp.Modules.TradeModule
targetStatus, message, recordId, expectedStatus) == 1;
}
private void SetClaimedStatus(int recordId, string status, string message)
private int SetClaimedStatus(int recordId, string status, string message)
{
DbContext.Database.ExecuteSqlRaw(
return DbContext.Database.ExecuteSqlRaw(
"UPDATE trade_approval_oa_result SET status = {0}, oa_msg = {1} WHERE id = {2} AND is_valid = 1 AND status = {3}",
status, message, recordId, Syncing);
}
@@ -442,7 +451,11 @@ namespace YLErp.Modules.TradeModule
"标的代码=" + (floatPosition?.UnderlyingCode ?? string.Empty),
"标的名称=" + underlyingName,
"期初收益率=" + (floatPosition?.InitYtm?.ToString("0.####%") ?? string.Empty),
"期初价格=" + (floatPosition?.PosiNetPrice.ToString("0.####") ?? string.Empty),
"期初价格=" + (floatPosition == null
? string.Empty
: (ConsGlobal.InstrumentType.IsBond(floatPosition.UnderlyingInstrumentType)
? BondPriceConverter.ToDisplay(floatPosition.PosiNetPrice)
: floatPosition.PosiNetPrice).ToString("0.####")),
"行权方式=" + (trade.GetExerciseModeCn() ?? string.Empty),
"合约期限=" + contractDays,
"数量=" + (floatPosition?.PosiQuantity.ToString("0.####") ?? string.Empty),
@@ -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<FundManagerRow>(LookupSql, new { UnderlyingCode = normalizedCode }, commandTimeout: 10)
var matches = connection.Query<FundManagerRow>(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())
@@ -493,12 +493,16 @@ namespace YLErp.Web.Controllers
//BUG-14:明细生效日期回写——前端新增行只带占位日期(2020-01-01 前后),此前不回写使新行(含模板拷贝)
//落库 0001-01-01,被取数侧 Max(ValueDate) 分组静默丢弃;占位日期对齐本次提交行的最新生效日期,无有效行时用模板生效日期
//明细生效日期跟主表走:主表有真实生效日期时全部行强制对齐(含复制带入的源模板行日期、行上残留的旧日期),
//否则只改主表日期会出现"模板已生效但明细未生效"的取数断档;主表仍为占位日期时保持原兜底
var placeholderDate = new DateTime(2020, 1, 1);
var effectiveValueDate = marginTemplate.Details
.Where(d => d.ValueDate > placeholderDate)
.Select(d => d.ValueDate)
.DefaultIfEmpty(marginTemplate.ValueDate > placeholderDate ? marginTemplate.ValueDate : placeholderDate)
.Max();
var detailValueDate = marginTemplate.ValueDate > placeholderDate
? marginTemplate.ValueDate
: marginTemplate.Details
.Where(d => d.ValueDate > placeholderDate)
.Select(d => d.ValueDate)
.DefaultIfEmpty(placeholderDate)
.Max();
marginTemplate.Details.ForEach(x =>
{
@@ -509,7 +513,7 @@ namespace YLErp.Web.Controllers
detail = new margin_template_detail();
yldb.margin_template_detail.Add(detail);
}
detail.ValueDate = x.ValueDate > placeholderDate ? x.ValueDate : effectiveValueDate;
detail.ValueDate = detailValueDate;
detail.MarginTemplateId = marginTemplate.id;
detail.GroupRatio = x.GroupRatio;
detail.MarginRatio1 = x.MarginRatio1;
+2 -107
View File
@@ -32,23 +32,14 @@
};
var pageData = new
{
showCCR = PS.Config.Company == CompanyEnum.国海,
is厦门象屿 = PS.Config.Company == CompanyEnum.厦门象屿,
showCCR = false, // 国联恒 false(原 Company == 国海 定制分支)
is厦门象屿 = false, // 国联恒 false(原 Company == 厦门象屿 定制分支)
ShowMultiplier = PS.Config.ErpElement.AccumulatorShowMultiplier1,
};
var calcQuotas = CalcPriceShowConfigService.GetShowCalcQuota();
}
@section CSS{
<link href="~/Style/Css/pricing.structure.css?v=1" rel="stylesheet" />
@switch (PS.Config.Company)
{
case CompanyEnum.光大光子:
<link href="~/Style/GDGZ/pricing.structure.css" rel="stylesheet" />
break;
case CompanyEnum.国泰君安:
<link href="~/Style/GTJA/pricing.structure.css" rel="stylesheet" />
break;
}
<link href="~/Style/Css/settag.css?v=@(HtmlUtil.JsVersion)" rel="stylesheet" />
<style>
.pitem-cash:not(.pitem) { background: #f0f8ff }
@@ -103,44 +94,6 @@
ylotc.structureTypes = pageObj.structureTypes;
</script>
@if (PS.Config.Company == CompanyEnum.伴兴)
{
<script>
pageVue.GetTotalMargin = function (trades, structureType) {
if (trades.length === 1) return parseFloat(trades[0].InitialMargin) || 0;
if (!structureType || structureType === "结构化交易") {
return _.reduce(trades, (acc, cur) => acc += parseFloat(cur.InitialMargin) || 0, 0);
}
if (structureType.indexOf("跨式") >= 0) {
return _.max(_.map(trades, x => Math.abs(x.InitialMargin)));
}
return _.reduce(trades, (acc, cur) => acc += cur.BuySell === '卖出' ? 0 : parseFloat(cur.InitialMargin) || 0, 0);
};
</script>
}
else if (PS.Config.Company == CompanyEnum.茂川资本)
{
<script>
pageVue.GetTotalMargin = function (trades, structureType) {
if (structureType && structureType.indexOf("跨式") >= 0) {
return _.max(_.map(trades, x => Math.abs(x.InitialMargin)));
}
return _.reduce(trades, (acc, cur) => acc += parseFloat(cur.InitialMargin) || 0, 0);
};
</script>
}
else if (PS.Config.Company == CompanyEnum.弘业)
{
<script>
pageVue.GetTotalMargin = function (trades, structureType) {
if (structureType && structureType.indexOf("跨式") >= 0) {
return _.max(_.map(trades, x => Math.abs(x.InitialMargin)));
}
return _.reduce(trades, (acc, cur) => acc += parseFloat(cur.InitialMargin) || 0, 0);
};
</script>
}
<script src="~/Scripts/app/pricing/tradePricing.js?v=@HtmlUtil.JsVersion"></script>
<script src="~/Scripts/app/pricing/structure.js?v=@(HtmlUtil.JsVersion)"></script>
}
@@ -635,35 +588,6 @@
</select>
</div>
<div class="form-group" style="width:80px;border-bottom:1px solid red;"><div style="width:80px;font-weight:700;padding-left:10px;border-left:4px solid red">其他要素</div></div>
@if (PS.Config.Company == CompanyEnum.东方财富)
{
<div class="form-group">
<label class="formlabel">初始预付金率</label>
<input type="number" class="form-input" id="InitialAdvance" />
<span>% </span>
</div>
<div class="form-group">
<label class="formlabel">期间预付金率</label>
<input type="number" class="form-input" id="PeriodAdvance" />
<span>% </span>
</div>
<div class="form-group">
<label class="formlabel">前端收益费率</label>
<input type="number" class="form-input" id="FontEarning" />
<span>% </span>
</div>
<div class="form-group">
<label class="formlabel">追保线</label>
<input type="number" class="form-input" id="ConfirmedLine" />
<span>% </span>
</div>
<div class="form-group">
<label class="formlabel">追保比例下限</label>
<input type="number" class="form-input" id="ConfirmedFloor" />
<span>% </span>
</div>
}
@if (CurUser.交易管理_交易标签调整权限)
{
<div class='form-group'>
@@ -895,35 +819,6 @@
</select>
</div>
<div class="form-group" style="width:80px;border-bottom:1px solid red;"><div style="width:80px;font-weight:700;padding-left:10px;border-left:4px solid red">其他要素</div></div>
@if (PS.Config.Company == CompanyEnum.东方财富)
{
<div class="form-group">
<label class="formlabel">初始预付金率</label>
<input type="number" class="form-input" id="accumulatorInitialAdvance" />
<span>% </span>
</div>
<div class="form-group">
<label class="formlabel">期间预付金率</label>
<input type="number" class="form-input" id="accumulatorPeriodAdvance" />
<span>% </span>
</div>
<div class="form-group">
<label class="formlabel">前端收益费率</label>
<input type="number" class="form-input" id="accumulatorFontEarning" />
<span>% </span>
</div>
<div class="form-group">
<label class="formlabel">追保线</label>
<input type="number" class="form-input" id="accumulatorConfirmedLine" />
<span>% </span>
</div>
<div class="form-group">
<label class="formlabel">追保比例下限</label>
<input type="number" class="form-input" id="accumulatorConfirmedFloor" />
<span>% </span>
</div>
}
@if (CurUser.交易管理_交易标签调整权限)
{
<div class='form-group'>
+2 -36
View File
@@ -910,33 +910,6 @@
$("#modalChooseContractType").modal('hide');
}
function sendReport_guojun() {
var clientIds = $("#ClientId").val() == null ? "" : $("#ClientId").val().join(",");
var contractTypes = ContractTypeCtrl.getValue();
var data = { UnWindDateStart: $("#DateFromUnWindDate").val(), UnWindDateEnd: $("#DateToUnWindDate").val(), ClientIds: clientIds ,receiver : contractTypes};
var jgrid = jQuery('#listGrid');
var ids = main.GetGridIds(jgrid);
if (ids.length == 0) {
if (data.UnWindDateStart == undefined || data.UnWindDateStart == null || data.UnWindDateStart == ""
|| data.UnWindDateEnd == undefined || data.UnWindDateEnd == null || data.UnWindDateEnd == "") {
showmiddlemessage("发送结算确认书需要选择确切的实际行权日区间!");
return;
}
}
else { data.TradeCashIds = ids.join(","); }
if (ids.length == 0 && clientIds == "") {
main.confirm("确定发送所有结算确认书?可选择客户名称单个发送", function () {
main.post("/trade/SendEmailSettleBill_Guojun", data).done(function (data) {
});
})
}
else {
main.post("/trade/SendEmailSettleBill_Guojun", data).done(function (data) {
});
}
$("#modalChooseContractType").modal('hide');
}
function sentMail(id) {
ReportId = id;
var clientIds = $("#ClientId").val();
@@ -1161,14 +1134,7 @@
//@MyControls.Btn("客户行权日报告", "ClientMaturityTrade()")
}
@MyControls.Btn("批量下载结算确认书", "downloadMaturityReports()")
@if (PS.Config.IsGuoJun)
{
@MyControls.Btn("发送结算确认书", "sendReport_guojun()")
}
else
{
@MyControls.Btn("发送结算确认书", "SendEmailSettleBill()")
}
@MyControls.Btn("发送结算确认书", "SendEmailSettleBill()")
@if (isAutoSealAndUploadFiles && !isAutoSealAfterGenerateBook)
{
@MyControls.Btn("批量确认书用印", "BatchSealContracts()")
@@ -1187,4 +1153,4 @@
-->
@Html.Raw(JqGridSimple.OutTable())
@await Html.PartialAsync("/Views/Common/_chooseContractType.cshtml", PS.Config.IsGuoJun ? "sendReport_guojun()" : "sendReport()")
@await Html.PartialAsync("/Views/Common/_chooseContractType.cshtml", "sendReport()")
@@ -71,11 +71,4 @@
@Html.Raw(JqGridSimple.OutTable())
@if (PS.Config.IsGuoJun)
{
@await Html.PartialAsync("/Views/Common/_chooseContractType.cshtml", "sendReportGuojun()")
}
else
{
@await Html.PartialAsync("/Views/Common/_chooseContractType.cshtml", "sendReport()")
}
@await Html.PartialAsync("/Views/Common/_chooseContractType.cshtml", "sendReport()")
+2 -1
View File
@@ -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": "",
@@ -1,7 +1,7 @@
/**
* percentColumnText.js — 交易要素列的百分比展示文本(纯函数)
* ============================================================================
* 把散落在 structure.js ×3 / structure_dz.js / tradeEditV2.js / tradeEditGroup.js
* 把散落在 structure.js ×3 / tradeEditV2.js / tradeEditGroup.js
* 的六处逐字相同一行式显式化为命名函数。
* 【约定】ColumnType==4 = 百分比列:值以小数形式存储(如 0.05),展示时 ×100 追加 %;
* 若值文本已含 %(人工录入或后端已格式化)则原样展示,不做二次换算;
@@ -710,43 +710,6 @@ function sendReport() {
$("#modalChooseContractType").modal('hide');
}
function sendReportGuojun() {
var clientIds = $("#ClientId").val() == null ? "" : $("#ClientId").val().join(",");
var contractTypes = ContractTypeCtrl.getValue();
var data = { UnWindDateStart: $("#DateFromUnWindDate").val(), UnWindDateEnd: $("#DateToUnWindDate").val(), ClientIds: clientIds, receiver: contractTypes };
var jgrid = jQuery('#listGrid');
var rowIds = jgrid.jqGrid('getGridParam', 'selarrrow');
var ids = [];
for (var i = 0; i < rowIds.length; i++) {
var rowObjs = jgrid.getRowData();
for (var j = 0; j < rowObjs.length; j++) {
if (rowObjs[j]["trade_cash.id"] == rowIds[i]) {
ids.push(rowObjs[j]["trade_cash.id"]);
}
}
}
if (ids.length == 0) {
if (data.UnWindDateStart == undefined || data.UnWindDateStart == null || data.UnWindDateStart == ""
|| data.UnWindDateEnd == undefined || data.UnWindDateEnd == null || data.UnWindDateEnd == "") {
showmiddlemessage("发送清算确认书需要选择确切的实际终止日区间!");
return;
}
}
else { data.TradeCashIds = ids.join(","); }
if (ids.length == 0 && clientIds == "") {
main.confirm("确定发送所有提前终止确认书?可选择客户名称单个发送", function () {
main.post("/trade/SendEmailUnwindReportGuojun", data).done(function (data) {
});
})
}
else {
main.post("/trade/SendEmailUnwindReportGuojun", data).done(function (data) {
});
}
$("#modalChooseContractType").modal('hide');
}
function SearchClick(isSearchclick) {
var listGrid = $('#listGrid');
@@ -40,7 +40,10 @@ namespace RealTimeCalcPositionService
{
string calctime = memoryCache.Get<string>("calctime");
var currentTime = DateTime.Now;
bool hasNew= RealtimePnlCalc.HasNewFlow(currentTime);
//以上次计算时间为基准判断是否有新流水,避免无变化时空转重算
var hasNew = DateTime.TryParseExact(calctime, "yyyy-MM-dd HH:mm:ss.fff",
System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out var lastCalcTime)
&& RealtimePnlCalc.HasNewFlow(lastCalcTime);
if (string.IsNullOrEmpty(calctime)|| hasNew)
{
memoryCache.Set("calctime", currentTime.ToString("yyyy-MM-dd HH:mm:ss.fff"), TimeSpan.FromSeconds(30));
@@ -53,6 +56,7 @@ namespace RealTimeCalcPositionService
_logger.Error(ex, "实时持仓服务异常:" + ex.Message);
}
}
stoppingToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(2));
}
}
public async Task StopAsync(CancellationToken cancellationToken)
@@ -47,7 +47,9 @@ namespace RealTimeCalcPositionService
{
_logger.Error(ex, "实时预付金计算异常:" + ex.Message);
}
//估值数据盘中不变、交易另有事件触发,无需连续轮询
stoppingToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(30));
}
}
public async Task StopAsync(CancellationToken cancellationToken)