using Org.BouncyCastle.Asn1.Ocsp; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using YLErp.Model; namespace YLErp.Helpers { /// /// 计算器帮助类 /// 调用链:zszq-trs → zszq-bond-oms(/calc/cal_bond_value) → bond-calc 微服务。 /// 契约(与 bond-oms-ui / zszq-bond-oms 保持一致): /// 请求体 { bondId, price, priceType(DP全价/CP净价/YD收益率), targetDate?(估值日 yyyy-MM-dd,缺省代理取 T+1) } /// 成功响应 data:{ errCode:0, errMsg:null, dirtyPrice(全价), cleanPrice(净价), ytm(收益率%) } /// 失败响应:success=false 且 message=中文原因(债券不存在/信息不全/参数非法/服务异常) /// public class BondCalcHepler { private const string CalcUrl = "/calc/cal_bond_value"; /// /// 计算器(债券净价/全价/收益率互算)。 /// 兼容旧调用方(如 RealtimePnlCalc):返回 CalBondResult,失败返回 null。 /// targetDate 可选:估值日(yyyy-MM-dd)。前端(fe)已在债券互换录入页强制要求 StartDate(开始日)必填、 /// 为空则报错不调用本接口;故经 UI 的计算请求总会带上估值日,此处的"代理默认 T+1"仅兜底非 UI 调用方(如 RealtimePnlCalc)。 /// public static CalBondResult BondCalc(string underlyingCode, decimal price, string priceType = "DP", string targetDate = null) { string _err; return BondCalc(underlyingCode, price, priceType, out _err, targetDate); } /// /// 计算器(带错误信息输出)。 /// 任何失败(地址未配/无响应/success=false/业务错误码 errCode!=0/异常)都返回 null, /// 并通过 errorMsg 带上**真实原因**,供上层(BondController)清晰反馈给用户。 /// 关键:即使 success=true,只要内部 errCode!=0(债券不存在/信息不全/参数非法)也视为失败, /// 避免把 0/异常价格静默回写覆盖用户手工输入。 /// targetDate 可选:估值日(yyyy-MM-dd),透传给代理的 targetDate(底层 bond-calc 的 settlementDate); /// 不传则代理默认取 T+1(下一工作日)。 /// public static CalBondResult BondCalc(string underlyingCode, decimal price, string priceType, out string errorMsg, string targetDate = null) { errorMsg = null; var baseUrl = Environment.GetEnvironmentVariable("BondOmsInterface_BaseUrl"); CalcBondRequest request = new CalcBondRequest() { bondId = underlyingCode, price = price.ToString(), priceType = priceType, targetDate = targetDate, }; if (string.IsNullOrEmpty(baseUrl)) { errorMsg = "计算器服务地址未配置(BondOmsInterface_BaseUrl)"; return null; } try { LogFactory.GetLogger("BondCalcHepler").Info( $"债券计算器请求: url={baseUrl}{CalcUrl} bondId={underlyingCode} price={price} priceType={priceType} targetDate={(string.IsNullOrEmpty(targetDate) ? "(空→代理默认T+1)" : targetDate)}"); var httpHelper = new HttpHelper(baseUrl, null); // http 请求 Web项目接口 var result = httpHelper.PostRequestNoAuth(CalcUrl, request).Result; if (result == null) { errorMsg = "计算器服务无响应"; LogFactory.GetLogger("BondCalcHepler").Error("债券计算器无响应,地址:" + baseUrl + CalcUrl); return null; } if (!result.success) { errorMsg = result.message ?? "债券计算失败"; LogFactory.GetLogger("BondCalcHepler").Error("债券计算失败:" + errorMsg); return null; } if (result.data == null) { errorMsg = "计算器返回数据为空"; LogFactory.GetLogger("BondCalcHepler").Error("债券计算器返回 data 为空"); return null; } // 业务层错误码(债券不存在 / 债券信息不全 / 参数非法),zszq-bond-oms 在 success=true 时仍可能带 errCode!=0 if (result.data.errCode != 0) { errorMsg = result.data.errMsg ?? "债券计算业务错误"; LogFactory.GetLogger("BondCalcHepler").Error("债券计算业务错误(code=" + result.data.errCode + "):" + errorMsg); return null; } // 防御性值域校验:errCode=0 但数值离谱(如净价变负 / 收益率量级爆炸 / 行权收益率哨兵 -999999)。 // 这类"成功但不可信"的响应现有 errCode 守卫拦不住,此处兜底:宁可返回 null+提示用户核对, // 也绝不把垃圾值回写覆盖手工输入。 string absurdReason; if (IsResultAbsurd(result.data, out absurdReason)) { errorMsg = absurdReason; LogFactory.GetLogger("BondCalcHepler").Error( $"债券计算返回离谱值(bondId={underlyingCode}):{absurdReason} | cleanPrice={result.data.cleanPrice} dirtyPrice={result.data.dirtyPrice} ytm={result.data.ytm}"); return null; } LogFactory.GetLogger("BondCalcHepler").Info( $"债券计算器成功: bondId={underlyingCode} targetDate={targetDate} cleanPrice={result.data.cleanPrice} dirtyPrice={result.data.dirtyPrice} ytm={result.data.ytm}"); return result.data; } catch (Exception ex) { errorMsg = "请求债券计算器异常:" + ex.Message; LogFactory.GetLogger("BondCalcHepler").Error("请求债券计算器时发生异常", ex); return null; } } /// /// 按结算日计算(自动结算路径用)。与 BondCalc 同款健壮处理: /// 地址未配/无响应/success=false/业务错误码/异常 均返回 null(不回写坏数据)。 /// public static CalBondResult BondCalcByDate(string underlyingCode, decimal price, String targetDate, string priceType = "DP") { var baseUrl = Environment.GetEnvironmentVariable("BondOmsInterface_BaseUrl"); CalcBondRequest request = new CalcBondRequest() { bondId = underlyingCode, price = price.ToString(), priceType = priceType, targetDate = targetDate, }; if (!string.IsNullOrEmpty(baseUrl)) { var httpHelper = new HttpHelper(baseUrl, null); try { // http 请求 Web项目接口 var result = httpHelper.PostRequestNoAuth(CalcUrl, request).Result; if (result == null || !result.success || result.data == null || result.data.errCode != 0) { var msg = result != null ? (result.message ?? (result.data != null ? result.data.errMsg : null)) : "计算器服务无响应"; LogFactory.GetLogger("BondCalcHelper").Error("债券计算失败:" + msg); return null; } // 防御性值域校验(同 BondCalc):拦截"成功但数值离谱"的响应,不回写坏数据 string absurdReason; if (IsResultAbsurd(result.data, out absurdReason)) { LogFactory.GetLogger("BondCalcHelper").Error( $"债券计算返回离谱值(bondId={underlyingCode}):{absurdReason} | cleanPrice={result.data.cleanPrice} dirtyPrice={result.data.dirtyPrice} ytm={result.data.ytm}"); return null; } return result.data; } catch (Exception ex) { LogFactory.GetLogger("BondCalcHelper").Error("请求计算器时发生异常", ex); } } return null; } /// /// 防御性值域校验:判断计算器返回值是否"离谱"(errCode=0 但净价变负 / 收益率量级爆炸 / 命中哨兵值)。 /// 命中则上层返回 null、不回写,避免把不可信结果覆盖用户手工输入。规则: /// - 净价/全价:占面值百分比,正常约 20~300,必须为正且不破千(≤0 或 >1000 视为离谱); /// - 收益率(ytm,百分数口径):|ytm| > 100 视为爆炸(允许负利率债,但量级须合理); /// - 哨兵值:任一字段命中 -999999 或 -999999×100(如 yieldToCP 不可用哨兵透传)。 /// 与前端 swapCalc.js::getBondCalcErrorMessage 的值域闸门保持一致口径。 /// private static bool IsResultAbsurd(CalBondResult data, out string reason) { reason = null; if (data == null) return false; const decimal SENTINEL = -999999m; bool Absurd(decimal v) => v == SENTINEL || v == SENTINEL * 100m; if (Absurd(data.cleanPrice) || Absurd(data.dirtyPrice) || (data.ytm.HasValue && Absurd(data.ytm.Value))) { reason = "债券计算返回哨兵值(部分指标不可用),请检查估值日/价格输入或联系管理员核对债券计算服务"; return true; } if (data.cleanPrice <= 0m || data.cleanPrice > 1000m || data.dirtyPrice <= 0m || data.dirtyPrice > 1000m) { reason = "债券计算净价/全价超出合理范围(应为正值且接近面值百分比),请检查估值日/价格输入或联系管理员核对债券计算服务"; return true; } if (data.ytm.HasValue && Math.Abs(data.ytm.Value) > 100m) { reason = "债券计算收益率量级异常(" + data.ytm.Value + "),请检查估值日/价格输入或联系管理员核对债券计算服务"; return true; } return false; } } }