using Microsoft.AspNetCore.Authorization; using System.Text; using System.Text.RegularExpressions; using YieldChain.Helpers; using YLErp.Configuration; namespace YLErp.Web.Controllers { /// /// 专用于输出前端JS的控制器 /// 数据必须为非敏感数据 /// /// /// ============================================================ /// OtcFormat 配置链路 & 排查说明(改前端格式化"不起效果"先看这里) /// ============================================================ /// /// 【三个同名文件,角色不同】 /// 1. wwwroot/Scripts/init/otcformat.js —— 出厂默认值(兜底种子),Git 管理 /// 2. wwwroot/Scripts/base/otcformat.js —— 格式化引擎(运行时逻辑),Git 管理 /// 依赖 lodash.js / main.numberFormat / jQuery,且须先加载"配置段"再加载引擎 /// 3. App_Data/Config/otcformat.js —— 运行时配置文件(真正生效那份),非 Git 管理 /// 内容形如: var main = main || {}; main.formatOptions = { trading: {...} }; /// /// 【配置真相源:DB AppConfig 表】 /// PGroup = "ProjectConfig", PName = "OtcFormatConfig" (见 ConsAppConfig.OtcFormatConfig) /// App_Data/Config/otcformat.js 只是 DB 配置落盘的镜像。 /// /// 【写入链路】OtcConfigController.AjaxSaveOtcFormat (Admin 配置页"保存"): /// 表单 model → JsonConvert.SerializeObject /// → AppConfigService.SaveOtcFormatConfig 写 DB /// → File.WriteAllText(App_Data/Config/otcformat.js) 落盘(只写当前节点!) /// → OtcFormatHelper.Initialize 刷新服务端内存 /// /// 【读取链路】FrontController.OtcFormat (前端 GET /front/otcformat): /// 读 App_Data/Config/otcformat.js + Scripts/base/otcformat.js 拼接返回 /// 前端引擎 base/otcformat.js 用 main.formatOptions 覆盖内置默认值 /// /// 【应用启动】AppManager (subSystem==OtcWeb): /// DB 有配置 → 反序列化进 OtcFormatHelper + 落盘 App_Data/Config/otcformat.js /// DB 为空 → 用 Scripts/init/otcformat.js 兜底种子落盘 /// /// 【排查"前端不起效果"按此顺序】 /// ① 浏览器直访 /front/otcformat?v=随机数,看 main.formatOptions 是否最新 /// · 加 ?v= 正确 / 不加错误 → ResponseCache 缓存(本接口已改 NoStore,不应再出现) /// · 加了 ?v= 仍错误 → 进 ② /// ② 查 DB AppConfig(OtcFormatConfig).PValue 是否最新 /// · DB 空/旧 → AjaxSaveOtcFormat 没成功,查配置页 POST 响应 /// ③ 直接读服务器 App_Data/Config/otcformat.js 是否最新 /// · DB 对但文件旧 → AjaxSaveOtcFormat 的 File.WriteAllText 没生效, /// 或多节点负载均衡下只写了接收 POST 的那台(读取命中了另一台旧文件)。 /// 读取和写入都只操作本机 Server.MapPath,无跨节点同步——多节点部署需注意。 /// ④ 页面引用方式:必须走 /front/otcformat,不能直接引 base/otcformat.js /// (后者只有引擎默认值,没有配置段),例如 wwwroot/Scripts/test/tradeCalc.html 即如此。 /// ============================================================ /// [MyAuthorizeIgnore] public class FrontController : BaseController { //基础数据(缓存60s) [ResponseCache(Duration = 60, Location = ResponseCacheLocation.Any)] public ActionResult BasicData(IEnumerable types) { var js = new BasicDataJsBuilder(CurUser).Build(types); return Content(js, "text/javascript"); } //格式化选项(配置动态变更,禁用缓存——见下方 OtcFormat 排查说明) [AllowAnonymous] //重要:此处不能用 [ResponseCache(Duration=600,Location=Any)]。 // 原因:OtcFormat 是会动态变更的运行时配置(见 OtcConfigController.AjaxSaveOtcFormat), // 若启用缓存,Admin 页保存后最长 600s 内仍返回旧内容,"前端改了配置不起效果"。 // 此前曾用 Duration=600,导致保存后必须等缓存过期或加 ?v=随机数 才生效。 [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] public ActionResult OtcFormat() { var js = string.Empty; var filePath = Server.MapPath("~/App_Data/Config/otcformat.js"); if (System.IO.File.Exists(filePath)) { js = System.IO.File.ReadAllText(filePath); } filePath = Server.MapPath("~/Scripts/base/otcformat.js"); js += System.IO.File.ReadAllText(filePath); return Content(js, "text/javascript"); } [AllowAnonymous] [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] public ActionResult SwapPricePrecision() { var filePath = Server.MapPath("~/App_Data/Config/swappriceprecision.js"); var js = System.IO.File.Exists(filePath) ? System.IO.File.ReadAllText(filePath) : "var main = main || {}; main.swapPricePrecision = {};"; return Content(js, "text/javascript"); } //今年及前后两年的日历数据(缓存120s) [ResponseCache(Duration = 120, Location = ResponseCacheLocation.Any)] public ActionResult Calendar() { //相信数据的准确性同时年份确定为4位数字 var dic = CalendarBLL.GetRecentCalendarModel().Select(n => new { key = n.Year.ToString() + ("chn".Equals(n.Country, StringComparison.OrdinalIgnoreCase) ? "" : n.Country?.ToLowerInvariant()), val = string.IsNullOrWhiteSpace(n.HolidayJson) ? Enumerable.Empty() : Regex.Matches(n.HolidayJson, n.Year + @",\d\d,\d\d").Cast() .Select(m => m.Value.Substring(5).Remove(2, 1)) .OrderBy(m => m).ToArray() }).ToDictionary(n => n.key, n => n.val); var json = JsonHelper.Serialize(dic); var script = string.Concat("var ylotc=ylotc||{};ylotc.calendar=", json, Properties.Resources.ylotc_calendar); return Content(script, "text/javascript"); } [ResponseCache(Duration = 30, Location = ResponseCacheLocation.Any)] public ActionResult ClientEditConfig() { var json = Modules.SystemModule.ClientEditConfigService.GetConfigJson(); return Content(json, "text/javascript"); } /// /// 基础数据构建器 /// class BasicDataJsBuilder : BasicViewModel { const string ylotcDefine = "var ylotc=ylotc||{};ylotc.$basicData=1;"; StringBuilder _sb; public BasicDataJsBuilder(UserInfo userInfo) : base(userInfo) { } public string Build(IEnumerable types) { if (types == null || !types.Any()) { return ylotcDefine; } _sb = new StringBuilder(300); _sb.AppendLine(ylotcDefine); foreach (var type in types) { switch (type) { case "簿记": case "簿记账户": case nameof(AssetUnit): AssetUnit(); break; case "簿记ALL": case "簿记账户ALL": case nameof(AssetUnitALL): AssetUnitALL(); break; case "簿记账户组": AssetUnitGroup(); break; case "客户": case "客户信息": case nameof(DBModels.Client): Client(); break; case "标的": case "标的信息": case nameof(DBModels.underlying_manager): UnderlyingManager(false, false); break; case "标的Live": case "标的信息Live": UnderlyingManager(true, false); break; case "权益标的": case "权益标的信息": UnderlyingManager(false, true); break; case "权益标的Live": case "权益标的信息Live": UnderlyingManager(true, true); break; case "广发商贸标的信息": GFUnderlyingManager(true, false); break; case "品种": case "标的品种": case nameof(Variety): Variety(); break; case "品种类型": VarietyTypes(); break; case "标的板块": case "板块性质": UnderlyingBlocks(); break; case "交易员": Traders(); break; case "系统用户": SystemUsers(); break; } } return _sb.ToString(); } // 簿记账户 private void AssetUnit() { var datas = JsDataModel.GetAssetUnits(CurUser); var dataJson = JsonHelper.Serialize(datas); _sb.Append("ylotc.assetunits=").Append(dataJson).AppendLine(";"); } // 簿记账户(所有) private void AssetUnitALL() { var datas = JsDataModel.GetAllAssetUnits(); var dataJson = JsonHelper.Serialize(datas); _sb.Append("ylotc.assetunits=").Append(dataJson).AppendLine(";"); } // 簿记账户组 private void AssetUnitGroup() { var datas = JsDataModel.GetAssetUnitGroups(); var dataJson = JsonHelper.Serialize(datas); _sb.Append("ylotc.assetunitgroups=").Append(dataJson).AppendLine(";"); } // 客户信息 private void Client() { var datas = JsDataModel.GetClients(); var dataJson = JsonHelper.Serialize(datas); _sb.Append("ylotc.clients=").Append(dataJson).AppendLine(";"); } // 标的信息 private void UnderlyingManager(bool onlyLive, bool onlyEquity) { var datas = JsDataModel.GetUnderlyings(onlyLive, onlyEquity); var dataJson = JsonHelper.Serialize(datas); _sb.Append("ylotc.underlyings=").Append(dataJson).AppendLine(";"); } private void GFUnderlyingManager(bool onlyLive, bool onlyEquity) { var datas = JsDataModel.GetGFUnderlyings(onlyLive, onlyEquity); var dataJson = JsonHelper.Serialize(datas); _sb.Append("ylotc.gfunderlyings=").Append(dataJson).AppendLine(";"); } //标的品种 private void Variety() { var datas = JsDataModel.GetVarieties(CurUser); var dataJson = JsonHelper.Serialize(datas); _sb.Append("ylotc.varieties=").Append(dataJson).AppendLine(";"); } /// /// 标的品种类型 /// private void VarietyTypes() { var datas = JsDataModel.GetVarietyTypes(); var dataJson = JsonHelper.Serialize(datas); _sb.Append("ylotc.varietyGroups=").Append(dataJson).AppendLine(";"); } /// /// 标的板块性质 /// private void UnderlyingBlocks() { var datas = JsDataModel.GetUnderlyingBlocks(); var dataJson = JsonHelper.Serialize(datas); _sb.Append("ylotc.underlyingBlocks=").Append(dataJson).AppendLine(";"); } /// /// 交易员 /// private void Traders() { var traders = UserBLL.GetUsersByPosition(); var datas = traders.Select(n => new { id = n.Id, n.Name, n.UserGroup, PinYin = PingYinHelper.GetFirstPinYin(n.Name) }).ToArray(); var dataJson = JsonHelper.Serialize(datas); _sb.Append("ylotc.traders=").Append(dataJson).AppendLine(";"); } /// /// 系统所有用户 /// private void SystemUsers() { var systemUsers = UserBLL.GetSystemsByUserId(); var datas = systemUsers.Select(n => new { id = n.Id, n.Name, n.UserGroup, PinYin = PingYinHelper.GetFirstPinYin(n.Name) }).ToArray(); var dataJson = JsonHelper.Serialize(datas); _sb.Append("ylotc.sysusers=").Append(dataJson).AppendLine(";"); } public override string ToString() { return _sb.ToString(); } } } }