首页
喃喃低语
Search
1
【061】U9系统报表输出
147 阅读
2
【038】U9私有扩展字段枚举的多语言表的SQL更新
124 阅读
3
【064】使用脚本查询所有实体扩展字段信息
122 阅读
4
平台领域应用合集 - U9HUB
119 阅读
5
【067】U9C获取生产环境数据库连接串
114 阅读
用友U9
登录
Search
竹秋廿九
累计撰写
72
篇文章
累计收到
1
条评论
首页
栏目
用友U9
页面
喃喃低语
搜索到
72
篇与
用友U9
的结果
2026-09-03
【071】使用系统原生确认弹窗提示用户信息
在开发过程中,很多操作需要先在后台检查复杂的业务条件,如果满足某些预警条件,才弹出确认窗口向用户二次确认,点确定继续执行,点取消直接中止;条件不满足则直接执行业务,无需弹窗。之前【056】使用警告弹窗提示用户信息使用的是库存转总账的 4034d041 警告表单(600x152),界面偏大且不太美观;而标准单据内置的 PDFormMessage.ShowConfirmDialog 又是静态写死在按钮客户端事件上的,无法在后台动态校验业务分支。本篇整理 U9 原汁原味的标准弃审/删除同款确认小窗(FormId: c690cbc6-fb39-4130-bf13-ddd7e8e0517c,320x105 像素,带小蓝问号),并封装为通用的 U9ConfirmHelper 工具类。支持单次消费动态令牌(防重复弹窗)、DOM 隐藏域即时清理、以及上下文数据(支持任意 object 如 long[]、DTO 等)暂存与一键消费。一、通用辅助类 U9ConfirmHelper(直接复制进项目即可)需引用程序集:UBF.System.dll(位于 Portal\bin)以及 UFSoft.UBF.UI.Controls.Interface.dll、UFSoft.UBF.UI.FormProcess.dll 注意:必须使用 UFSoft.UBF.Security.BaseU9DESEncryptHelper(UTF-8 编码),与弹窗窗体的解密端保持完全对称,杜绝中文乱码。using System; using System.Web.UI; using UFSoft.UBF.Security; using UFSoft.UBF.UI; using UFSoft.UBF.UI.ControlModel; using UFSoft.UBF.UI.FormProcess; using UFSoft.UBF.UI.IView; namespace UFIDA.U9.SH.HaLi.QXUIPlugIn { /// <summary> /// U9 原生确认弹窗用户选择结果 /// </summary> public enum ConfirmResult { /// <summary> /// 未触发确认或非本次弹窗回发 /// </summary> None = 0, /// <summary> /// 用户点击了【确定】或【是】 /// </summary> Confirmed = 1, /// <summary> /// 用户点击了【否】(仅在三态弹窗中) /// </summary> Denied = 2 } /// <summary> /// U9 原生确认弹窗通用辅助工具(带蓝色小问号,支持二态【确定/取消】与三态【是/否/取消】) /// 支持:动态一次性令牌防重复、DOM隐藏域即时清理、上下文数据安全暂存与一键消费 /// </summary> public static class U9ConfirmHelper { private const string TokenPrefix = "__U9ConfirmToken_"; private const string DataPrefix = "__U9ConfirmData_"; /// <summary> /// 弹出 U9 系统原生 320x105 确认框(DeleteAlertWebPart,带【确定】【取消】按钮) /// 用户点击【确定】后自动触发局部 Ajax 回发;点击【取消】或右上角【X】关闭时彻底静默,原地停留 /// </summary> /// <param name="part">当前 IPart 页面对象(或 WebPart 实例)</param> /// <param name="actionKey">业务动作标识(如 "SplitPlanOrder"、"UndoApprove",用于区分不同弹窗)</param> /// <param name="message">弹窗提示内容</param> /// <param name="title">弹窗标题(默认"确认")</param> /// <param name="contextData">可选:需要随本次确认流转暂存的任意上下文数据(如实体、数组、字典、JSON等,类型为 object)</param> /// <param name="targetControl">可选:若希望确认后回发到特定按钮的 Click 事件,传入该按钮;若不传则走默认页面生命周期</param> public static void ShowConfirm(IPart part, string actionKey, string message, string title = "确认", object contextData = null, IUFControl targetControl = null) { if (part == null || string.IsNullOrEmpty(actionKey)) return; BaseWebForm webForm = part as BaseWebForm; if (webForm == null || webForm.Page == null) return; // 1. 生成单次唯一消费令牌(GUID)并暂存状态,防止静态参数残留导致死循环 string token = string.Format("{0}_{1}", actionKey, Guid.NewGuid().ToString("N")); part.CurrentState[TokenPrefix + actionKey] = token; // 2. 若传入了上下文数据,一并暂存(原生 object) if (contextData != null) { part.CurrentState[DataPrefix + actionKey] = contextData; } // 3. 确定局部回发目标(推荐显式传入 targetControl;若未传则沿控件树通用检索表单的主 UpdatePanel,杜绝整页刷新) Control postBackCtrl = ResolveAjaxTarget(webForm, targetControl as Control); string postBackTarget = postBackCtrl != null ? postBackCtrl.ClientID.Replace("_", "$") : string.Empty; // 4. 优先采用 U9 官方规范挂载到 TopLevelContainer,确保在异步回发中百分之百被输出给前端 Control contextCtrl = (Control)webForm.TopLevelContainer ?? (targetControl as Control) ?? (Control)webForm.Page; if (contextCtrl == null) return; // 5. 构造原生弹窗 URL(DeleteAlertWebPart) string formId = "c690cbc6-fb39-4130-bf13-ddd7e8e0517c"; #pragma warning disable 0618 string encMsg = BaseU9DESEncryptHelper.Encrypt(message ?? ""); #pragma warning restore 0618 string simpleUrl = webForm.ResolveClientUrl("~/ufsoft/simple.aspx"); string url = string.Format("{0}?lnk={1}&chromeType=4&message={2}", simpleUrl, formId, encMsg); // 6. 前端纯 JS 脚本(点确定触发局部回发;点取消或点右上角【X】均静默关闭,绝不回发) string script = string.Format( @"ShowModalDialogForAll('{0}', '{1}', 105, 320, true, false, function(ret) {{ if (ret === 'OK' || ret === true) {{ __doPostBack('{2}', '{3}'); setTimeout(function() {{ var arg = document.getElementById('__EVENTARGUMENT'); if (arg) arg.value = ''; }}, 10); }} }});", url, title, postBackTarget, token ); AtlasHelper.RegisterAtlasStartupScript(contextCtrl, contextCtrl.GetType(), "confirm_" + Guid.NewGuid().ToString("N"), script, true); } /// <summary> /// 弹出 U9 系统原生 350x105【是/否/取消】三态确认框(ConfirmUIFormWebPart,带原生【是】【否】【取消】三个按钮) /// 用户点击【是】或【否】后自动触发局部 Ajax 回发;点击【取消】或右上角【X】关闭时彻底静默,原地停留 /// 配合 GetConfirmResult 使用:Confirmed 代表点【是】,Denied 代表点【否】 /// </summary> /// <param name="part">当前 IPart 页面对象(或 WebPart 实例)</param> /// <param name="actionKey">业务动作标识</param> /// <param name="message">弹窗提示内容</param> /// <param name="title">弹窗标题(默认"确认当前操作")</param> /// <param name="contextData">可选:需要随本次确认流转暂存的任意上下文数据</param> /// <param name="targetControl">可选:若希望确认后回发到特定按钮的 Click 事件,传入该按钮;若不传则走默认页面生命周期</param> public static void ShowYesNoCancelConfirm(IPart part, string actionKey, string message, string title = "确认当前操作", object contextData = null, IUFControl targetControl = null) { if (part == null || string.IsNullOrEmpty(actionKey)) return; BaseWebForm webForm = part as BaseWebForm; if (webForm == null || webForm.Page == null) return; // 1. 生成单次唯一消费令牌(GUID)并暂存状态 string token = string.Format("{0}_{1}", actionKey, Guid.NewGuid().ToString("N")); part.CurrentState[TokenPrefix + actionKey] = token; // 2. 若传入了上下文数据,一并暂存 if (contextData != null) { part.CurrentState[DataPrefix + actionKey] = contextData; } // 3. 确定局部回发目标 Control postBackCtrl = ResolveAjaxTarget(webForm, targetControl as Control); string postBackTarget = postBackCtrl != null ? postBackCtrl.ClientID.Replace("_", "$") : string.Empty; // 4. 挂载点 Control contextCtrl = (Control)webForm.TopLevelContainer ?? (targetControl as Control) ?? (Control)webForm.Page; if (contextCtrl == null) return; // 5. 构造原生三态弹窗 URL(ConfirmUIFormWebPart,宽 350 高 105,带【是】【否】【取消】三个按钮) string formId = "297c8c52-d2cf-4c58-93bc-84d17a2176a4"; string simpleUrl = webForm.ResolveClientUrl("~/ufsoft/simple.aspx"); string encTitle = System.Web.HttpUtility.UrlEncode(title ?? "确认当前操作", System.Text.Encoding.UTF8); string encMessage = System.Web.HttpUtility.UrlEncode(message ?? "", System.Text.Encoding.UTF8); string url = string.Format("{0}?lnk={1}&chromeType=4&ShowType=ShowModal&Title={2}&Message={3}", simpleUrl, formId, encTitle, encMessage); // 6. 前端纯 JS 脚本: // 点【是】(ret === 'Yes') ➔ 回发 _Yes // 点【否】(ret === 'No') ➔ 回发 _No // 点【取消】(ret === 'Cancel') 或 点右上角【X】(ret 为空/undefined) ➔ 彻底静默,绝不回发! string script = string.Format( @"ShowModalDialogForAll('{0}', '{1}', 105, 350, true, false, function(ret) {{ if (!ret || ret === 'Cancel') return; if (ret === 'Yes') {{ __doPostBack('{2}', '{3}_Yes'); }} else if (ret === 'No') {{ __doPostBack('{2}', '{3}_No'); }} setTimeout(function() {{ var arg = document.getElementById('__EVENTARGUMENT'); if (arg) arg.value = ''; }}, 10); }});", url, title, postBackTarget, token ); AtlasHelper.RegisterAtlasStartupScript(contextCtrl, contextCtrl.GetType(), "confirm3_" + Guid.NewGuid().ToString("N"), script, true); } /// <summary> /// 检查当前请求针对指定动作的确认结果(适用于三态弹窗或二态开启 postBackOnCancel 的场景) /// 命中后会立即原子销毁 Token(单次消费),防止重复执行与参数残留 /// </summary> /// <param name="part">当前 IPart 页面对象</param> /// <param name="actionKey">业务动作标识</param> /// <returns>ConfirmResult: None(非本次回发), Confirmed(点确定/是), Denied(点否), Cancelled(点取消)</returns> public static ConfirmResult GetConfirmResult(IPart part, string actionKey) { object dummy; return GetConfirmResult(part, actionKey, out dummy); } /// <summary> /// 检查当前请求针对指定动作的确认结果,并一并取出暂存的上下文数据 /// </summary> /// <param name="part">当前 IPart 页面对象</param> /// <param name="actionKey">业务动作标识</param> /// <param name="contextData">输出:弹窗前暂存的上下文数据</param> /// <returns>ConfirmResult 枚举值</returns> public static ConfirmResult GetConfirmResult(IPart part, string actionKey, out object contextData) { contextData = null; if (part == null || string.IsNullOrEmpty(actionKey)) return ConfirmResult.None; BaseWebForm webForm = part as BaseWebForm; if (webForm == null || webForm.Page == null) return ConfirmResult.None; // 1. 读取期待的有效 Token string expectedToken = part.CurrentState[TokenPrefix + actionKey] as string; if (string.IsNullOrEmpty(expectedToken)) return ConfirmResult.None; // 2. 核对回发参数 string returnToken = webForm.Page.Request.Params["__EVENTARGUMENT"]; if (string.IsNullOrEmpty(returnToken)) return ConfirmResult.None; // 2.1 命中【确定/是】 if (returnToken.Equals(expectedToken + "_OK", StringComparison.OrdinalIgnoreCase) || returnToken.Equals(expectedToken + "_Yes", StringComparison.OrdinalIgnoreCase) || returnToken.Equals(expectedToken, StringComparison.OrdinalIgnoreCase)) { part.CurrentState[TokenPrefix + actionKey] = null; contextData = part.CurrentState[DataPrefix + actionKey]; part.CurrentState[DataPrefix + actionKey] = null; return ConfirmResult.Confirmed; } // 2.2 命中【否】(三态专属) if (returnToken.Equals(expectedToken + "_No", StringComparison.OrdinalIgnoreCase)) { part.CurrentState[TokenPrefix + actionKey] = null; contextData = part.CurrentState[DataPrefix + actionKey]; part.CurrentState[DataPrefix + actionKey] = null; return ConfirmResult.Denied; } return ConfirmResult.None; } /// <summary> /// 泛型重载:检查当前请求针对指定动作的确认结果,并将暂存的上下文数据自动转换为指定类型 T /// </summary> /// <typeparam name="T">目标数据类型</typeparam> /// <param name="part">当前 IPart 页面对象</param> /// <param name="actionKey">业务动作标识</param> /// <param name="contextData">输出:转换后的强类型数据</param> /// <returns>ConfirmResult 枚举值</returns> public static ConfirmResult GetConfirmResult<T>(IPart part, string actionKey, out T contextData) { object raw; ConfirmResult res = GetConfirmResult(part, actionKey, out raw); if (res != ConfirmResult.None && raw != null) { if (raw is T) { contextData = (T)raw; } else { try { contextData = (T)Convert.ChangeType(raw, typeof(T)); } catch { contextData = default(T); } } } else { contextData = default(T); } return res; } /// <summary> /// 检查当前请求是否为指定动作的确认回发(不关心暂存数据) /// 命中后会立即原子销毁 Token(单次消费),防止重复执行与参数残留 /// </summary> /// <param name="part">当前 IPart 页面对象</param> /// <param name="actionKey">业务动作标识</param> /// <returns>若为确认回发返回 true,否则返回 false</returns> public static bool IsConfirmed(IPart part, string actionKey) { object dummy; return TryGetConfirmed(part, actionKey, out dummy); } /// <summary> /// 检查当前请求是否为指定动作的确认回发,并一并取出弹窗前暂存的原生 object 上下文数据 /// 命中后会立即原子销毁 Token 与暂存数据(单次消费),防止重复执行与参数残留 /// </summary> /// <param name="part">当前 IPart 页面对象</param> /// <param name="actionKey">业务动作标识</param> /// <param name="contextData">输出:弹窗前暂存的原生 object 数据(如 long[]、DTO等),由调用方直接转换;未暂存或非确认回发时为 null</param> /// <returns>若为确认回发返回 true,否则返回 false</returns> public static bool TryGetConfirmed(IPart part, string actionKey, out object contextData) { contextData = null; if (part == null || string.IsNullOrEmpty(actionKey)) return false; BaseWebForm webForm = part as BaseWebForm; if (webForm == null || webForm.Page == null) return false; // 1. 读取期待的有效 Token string expectedToken = part.CurrentState[TokenPrefix + actionKey] as string; if (string.IsNullOrEmpty(expectedToken)) return false; // 2. 核对回发参数(兼容普通 token 与带 _OK 后缀的 token) string returnToken = webForm.Page.Request.Params["__EVENTARGUMENT"]; if (string.IsNullOrEmpty(returnToken)) return false; if (!expectedToken.Equals(returnToken, StringComparison.OrdinalIgnoreCase) && !(expectedToken + "_OK").Equals(returnToken, StringComparison.OrdinalIgnoreCase)) { return false; } // 3. 【核心防线】立即原子销毁 Token!确保该次确认终生只消费一次 part.CurrentState[TokenPrefix + actionKey] = null; // 4. 取出并清理暂存数据(原生 object,直接返回给调用方转换) contextData = part.CurrentState[DataPrefix + actionKey]; part.CurrentState[DataPrefix + actionKey] = null; return true; } /// <summary> /// 泛型重载:检查指定动作确认回发,并将暂存的上下文数据自动转换为指定类型 T /// </summary> /// <typeparam name="T">目标数据类型(如 long[], string, DTO等)</typeparam> /// <param name="part">当前 IPart 页面对象</param> /// <param name="actionKey">业务动作标识</param> /// <param name="contextData">输出:转换后的强类型数据</param> /// <returns>若为确认回发返回 true,否则返回 false</returns> public static bool TryGetConfirmed<T>(IPart part, string actionKey, out T contextData) { object rawData; if (TryGetConfirmed(part, actionKey, out rawData)) { if (rawData is T) { contextData = (T)rawData; } else { try { contextData = (T)Convert.ChangeType(rawData, typeof(T)); } catch { contextData = default(T); } } return true; } contextData = default(T); return false; } /// <summary> /// 解析用于 ASP.NET AJAX 局部异步回发的目标控件(UniqueID) /// 1. 优先使用显式传入的 targetControl; /// 2. 若未传入 targetControl,自动沿控件树通用检索表单的主 UpdatePanel(消除对特定 Card0/Toolbar 名称的死板依赖) /// </summary> private static Control ResolveAjaxTarget(BaseWebForm webForm, Control targetControl) { if (targetControl != null) { return targetControl; } if (webForm != null) { // U9 BuildCommonControls 默认将主 UpdatePanel 加在 form.Controls 中 foreach (Control c in webForm.Controls) { if (IsUpdatePanel(c)) return c; } // 兜底:在 TopLevelContainer 子树中递归检索 UpdatePanel 或主卡片容器 Control topCtrl = webForm.TopLevelContainer as Control; if (topCtrl != null) { Control foundUpdatePanel = FindFirstUpdatePanel(topCtrl); if (foundUpdatePanel != null) return foundUpdatePanel; return topCtrl; } } return null; } /// <summary> /// 递归探测控件树中的第一个 UpdatePanel /// </summary> private static Control FindFirstUpdatePanel(Control parent) { if (parent == null) return null; if (IsUpdatePanel(parent)) return parent; foreach (Control child in parent.Controls) { Control found = FindFirstUpdatePanel(child); if (found != null) return found; } return null; } /// <summary> /// 检查控件是否为 ASP.NET AJAX 的 UpdatePanel 类型(按类型名称与基类递归比对,避免直接依赖 System.Web.Extensions 程序集) /// </summary> private static bool IsUpdatePanel(Control ctrl) { if (ctrl == null) return false; Type t = ctrl.GetType(); while (t != null && t != typeof(object)) { if (t.Name.Equals("UpdatePanel", StringComparison.OrdinalIgnoreCase)) return true; t = t.BaseType; } return false; } } }二、用法一:在 UI 插件中使用(如 WebPartExtend / BaseUIPlugIn)UI 插件中,按钮通常是动态扩展到操作下拉菜单(DDBtnOperate)里的。点击按钮时先在 Click 事件中校验条件并弹窗,确认后的逻辑在 BeforeRender 生命周期中处理。1. 点击按钮事件中(发起弹窗)private void OnSplitPlanOrderClick(object sender, MenuItemClickEventArgs e) { if (_part == null) return; _part.OnDataCollect(this); // 1. 获取勾选行 long[] ids = _part.Model.PlanOrder.GetSelectedRecordIDs(); if (ids != null && ids.Length > 0) { // 场景 A:勾选了数据,无需弹窗,直接执行勾选行的拆分! DoSplitByIds(ids); return; } // 2. 未勾选行:校验条件,满足预警条件时弹出原生确认小窗 string filterJson = BuildCurrentFilterJson(); // 获取查询条件 string tip = string.Format("未勾选数据,将自动对【当前查询结果】中的自制计划订单进行整批拆分,是否继续?"); // 显式传入操作下拉按钮 ddBtn(走局部 Ajax 异步回发)并随确认流转暂存查询条件 filterJson IUFDropDownButton ddBtn = CommonFunction.FindControl(_part, "Card0", "DDBtnOperate") as IUFDropDownButton; U9ConfirmHelper.ShowConfirm(_part, "SplitPlanOrderAll", tip, "整批拆分确认", filterJson, ddBtn); }2. 在 BeforeRender 中接收确认结果(消费执行)用户在弹窗中点【取消】或点【X】关闭时,前端不产生回发,界面完全保持原样;点【确定】后回发,在此处执行。public override void BeforeRender(IPart Part, EventArgs args) { base.BeforeRender(Part, args); // 一行代码接收确认并自动取出暂存数据! // 内部会自动校验 Token 并立即原子销毁 Token,单次消费,绝对不会重复执行或死循环弹窗 if (U9ConfirmHelper.TryGetConfirmed(Part, "SplitPlanOrderAll", out string batchFilterJson)) { DoSplitByFilter(batchFilterJson); } }三、用法二:在客开页面/客开按钮中使用(Click_Extend 事件自闭环)典型业务场景:在 U9 建模设计器中为客开按钮【计算折扣价】(BtnCalcDiscount)生成的标准二开代码桩(位于 XXXWebPart_Extend.cs 分部类中的 BtnCalcDiscount_Click_Extend)。首次点击:单据明细行尚未有折扣价,直接无感计算并写入,不弹窗打扰用户;二次点击:已存在计算过的折扣价,重新计算将会覆盖现有数据,必须弹出 320x105 确认窗口提示;点【确定】后执行覆盖,点【取消】保持现状。按钮无论放置在上方 Toolbar 还是下方 Card,只需传入 targetControl: this.BtnCalcDiscount,弹窗确认后便会走局部 Ajax 回发精准重新进入当前 Click_Extend 事件,形成完全独立的自闭环处理:// 示例:U9 建模设计器标准生成的客开按钮点击事件代码桩(WebPart_Extend.cs) private void BtnCalcDiscount_Click_Extend(object sender, EventArgs e) { // 1. 优先检查:是否为用户在确认窗点击【确定】后的回发 if (U9ConfirmHelper.IsConfirmed(this, "CalcDiscount")) { DoCalculateDiscount(allowOverwrite: true); // 用户已确认,直接执行覆盖计算 return; } // 2. 正常点击时的业务逻辑:检查当前单据行是否已存在折扣价(防误触二次覆盖) bool hasExistingDiscount = CheckHasExistingDiscount(); if (hasExistingDiscount) { string tip = "当前单据已存在计算过的折扣价,重新计算将覆盖已有价格,是否确认继续?"; // 满足覆盖预警条件,弹出原生 320x105 确认框: // 显式传入 targetControl 为当前客开按钮自身(回发后走 Ajax 局部刷新重新进入当前 Click 事件,命中第 1 步分支) U9ConfirmHelper.ShowConfirm( this, "CalcDiscount", tip, "重算折扣确认", targetControl: this.BtnCalcDiscount ); return; } // 3. 首次计算(不存在已计算价格):直接无感计算,无需弹窗打扰用户 DoCalculateDiscount(allowOverwrite: false); } /// <summary> /// 检查单据明细行是否已有折扣价 /// </summary> private bool CheckHasExistingDiscount() { // 示例:遍历当前单据明细行视图 foreach (var row in this.Model.OrderLine.Records) { if (Convert.ToDecimal(row["DiscountPrice"] ?? 0) > 0) { return true; // 发现已有折扣价,需要预警 } } return false; }四、用法三:在客开中使用原生三态【是 / 否 / 取消】弹窗(带原生【是】【否】【取消】三个按钮)典型业务场景:在单据界面点击客开按钮【创建收款单】(BtnCreateRecBill)。数据未修改:当前勾选的明细行(或整个单据)未发生变更,直接调用后台接口创建收款单,无需打扰用户;数据发生修改(存在脏数据):弹出系统原生 350x105【保存确认】三态对话框(ConfirmUIFormWebPart),提示用户:"当前单据存在记录修改,是否需要先保存?":点【是】(Confirmed):先调用页面原生保存(BtnSave_Click_Extend),保存成功后自动继续创建收款单;点【否】(Denied):不调用保存,直接基于已有数据执行创建收款单;点【取消】或点右上角【X】:前端 100% 彻底静默拦截,原地停留,绝不执行任何保存或收款单创建!// 示例:U9 建模设计器标准生成的客开按钮代码桩(WebPart_Extend.cs) private void BtnCreateRecBill_Click_Extend(object sender, EventArgs e) { // 1. 优先检查:是否为用户在三态确认窗口点击后的回发 ConfirmResult result = U9ConfirmHelper.GetConfirmResult(this, "CreateRecBill"); if (result == ConfirmResult.Confirmed) { // ---------------------------------------------------- // 分支 1:用户点击了【是】➔ 先调用页面原生保存,成功后再创建收款单 // ---------------------------------------------------- this.BtnSave_Click_Extend(this, EventArgs.Empty); // 若保存触发了校验异常(如必填项缺失或后端报错),立即中止后续逻辑 if (this.Model.ErrorMessage.hasErrorMessage) { return; } DoCreateRecBill(); // 保存成功,继续创建收款单 return; } else if (result == ConfirmResult.Denied) { // ---------------------------------------------------- // 分支 2:用户点击了【否】➔ 不调用保存,直接继续创建收款单 // ---------------------------------------------------- DoCreateRecBill(); return; } // ---------------------------------------------------- // 分支 3:用户点击了【取消】或点右上角【X】关闭弹窗 // 前端直接 return 绝不发起任何网络回发,界面原地停留,不会进入当前方法! // ---------------------------------------------------- // 2. 正常点击时的业务逻辑:检查当前勾选的记录(或当前单据)是否存在脏数据改动 bool isModified = CheckSelectedRowsModified(); if (isModified) { // 存在未保存的数据变动,弹出系统原生 350x105 三态确认框(带【是】【否】【取消】三个按钮) U9ConfirmHelper.ShowYesNoCancelConfirm( this, "CreateRecBill", "当前单据存在记录修改,是否需要先保存?", "保存确认", targetControl: this.BtnCreateRecBill ); return; } // 3. 数据没有发生过任何变动:无需弹窗提示,直接执行创建收款单 DoCreateRecBill(); } /// <summary> /// 检查当前勾选的记录是否发生过修改(或检查整个页面是否有脏数据) /// </summary> private bool CheckSelectedRowsModified() { // 方式一:精准检查当前选中的单据行是否有脏数据 // (U9 UIModel 中的 RecordState: Unchanged, Modified, Inserted, Deleted) if (this.Model.Views["OrderLine"] != null) { foreach (IUIRecord row in this.Model.Views["OrderLine"].Records) { // 假设施图中有勾选字段(如 SelectRecord / Selected) bool isSelected = Convert.ToBoolean(row["SelectRecord"] ?? false); if (isSelected && row.RecordState != RecordState.Unchanged) { return true; // 勾选的行被修改过 } } } // 方式二:通用检查当前页面主视图(表头)是否被修改 if (this.Model.Views[0].FocusedRecord != null && this.Model.Views[0].FocusedRecord.RecordState != RecordState.Unchanged) { return true; } return false; }五、开发与调用注意事项推荐显式传入 targetControl(保障局部刷新):无论客开按钮位于上方工具栏(Toolbar)还是下方卡片容器(Card),调用时强烈建议显式传入当前按钮自身(如 targetControl: this.BtnCreateRecBill);这样弹窗确认后系统会走 Ajax 局部刷新精准重新进入当前按钮的 Click 事件,杜绝整页重载闪烁。动作标识 actionKey 保持唯一:同一页面若存在多个弹窗动作(如“覆盖计算确认”与“生成收款单确认”),请使用不同标识(如 "CalcDiscount"、"CreateRecBill"),防止上下文暂存数据相互覆盖。二态与三态按需选择:二态确认(ShowConfirm,320×105):用于常规“确定继续 / 取消放弃”操作,界面展示原生【确定】与【取消】;三态确认(ShowYesNoCancelConfirm,350×105):用于“二选一”业务分支(如“是先保存 / 否直接执行 / 取消放弃”),界面展示原生【是】、【否】、【取消】。程序集引用依赖:复制 U9ConfirmHelper.cs 时,需确保客开项目已引用 UBF.System.dll(位于 Portal\bin,提供原生 UTF-8 DES 加密,杜绝弹窗乱码)。六、弹窗效果图示二态弹窗:320x105 像素,经典小蓝问号,右下角【确定】与【取消】,原生弃审同款视觉体验;三态弹窗:350x105 像素,经典小蓝问号,右下角【是】、【否】、【取消】,原生放弃同款视觉体验。
2026年09月03日
9 阅读
0 评论
0 点赞
2026-03-26
【070】EntityViewQuery用法
EntityViewQuery用法示例反编译档案下发接口看到的一段集团使用EntityViewQuery的代码,让AI不改变逻辑优化了一下 而且还有in如何增加OqlParam参数 SysUtils在UFSoft.UBF.Sys.Database.dll using System; using System.Collections.Generic; using System.Data; using UFIDA.U9.CBO.SCM.Item; using UFSoft.UBF.Business; using UFSoft.UBF.PL; using UFSoft.UBF.Sys.Database; using UFSoft.UBF.View.Query; namespace UFIDA.U9.ISV.ControlObjSendSV { public class ControlObjSendSVImpementStrategy { public override object Do() { // 前面一大串... string fullName = ItemMaster.EntityRes.BE_FullName; // bpObj.FullName string fromOrg = "10"; // bpObj.FromOrg List<string> ids = new List<string> { "2001070014901", "2001070014902" }; // bpObj.IDs List<long> RealInputIDS = new List<long>(); string oql = $"select ID from {fullName.Replace(".", "::")} where Code in (@Codes) and Org.Code = @OrgCode"; EntityViewQuery viewQuery = new EntityViewQuery(); ObjectQuery q = viewQuery.CreateQuery(oql); OqlParam[] pars = { new OqlParam("Codes", SysUtils.ToXml(SysUtils.CreateIDTable(ids))), new OqlParam("OrgCode", fromOrg) }; using (IDataReader dr = viewQuery.ExecuteReader(q, pars)) { while (dr.Read()) { if (dr[0] is DBNull) throw new Exception("系统中找不到对应的料品ID"); RealInputIDS.Add(Convert.ToInt64(dr[0])); } } if (RealInputIDS.Count == 0) throw new Exception("未查询到匹配的数据"); // 后面一大串... } } } SysUtilsusing System; using System.Collections.Generic; using System.Data; using System.Globalization; using System.IO; using System.Security.Cryptography; using System.Text; namespace UFSoft.UBF.Sys.Database { // Token: 0x0200000F RID: 15 public class SysUtils { // Token: 0x060000A5 RID: 165 RVA: 0x00005A30 File Offset: 0x00003C30 public static DataTable CreateIDTable() { return new DataTable("IDT") { Columns = { { "ID", typeof(string) } } }; } // Token: 0x060000A6 RID: 166 RVA: 0x00005A6C File Offset: 0x00003C6C public static DataTable CreateIDTable(List<long> idList) { DataTable idTable = SysUtils.CreateIDTable(); foreach (long id in idList) { idTable.Rows.Add(new object[] { id }); } return idTable; } // Token: 0x060000A7 RID: 167 RVA: 0x00005AE0 File Offset: 0x00003CE0 public static DataTable CreateIDTable(string ids) { DataTable idTable = SysUtils.CreateIDTable(); bool flag = string.IsNullOrEmpty(ids); DataTable result; if (flag) { result = idTable; } else { string[] idList = ids.Split(new char[] { ',' }); foreach (string id in idList) { idTable.Rows.Add(new object[] { id }); } result = idTable; } return result; } // Token: 0x060000A8 RID: 168 RVA: 0x00005B50 File Offset: 0x00003D50 public static DataTable CreateIDTable(List<string> idList) { DataTable idTable = SysUtils.CreateIDTable(); foreach (string id in idList) { idTable.Rows.Add(new object[] { id }); } return idTable; } // Token: 0x060000A9 RID: 169 RVA: 0x00005BC0 File Offset: 0x00003DC0 public static string ToXml(DataTable table) { foreach (object obj in table.Columns) { DataColumn col = (DataColumn)obj; col.ColumnMapping = MappingType.Attribute; bool flag = col.DataType.Equals(Type.GetType("System.DateTime")); if (flag) { col.DateTimeMode = DataSetDateTime.Unspecified; } } StringBuilder sb = new StringBuilder(2048); StringWriter sw = new StringWriter(sb); table.WriteXml(sw, XmlWriteMode.WriteSchema); return sb.ToString(); } // Token: 0x060000AA RID: 170 RVA: 0x00005C70 File Offset: 0x00003E70 public static string ToXml(List<long> idList) { DataTable dt = SysUtils.CreateIDTable(idList); return SysUtils.ToXml(dt); } // Token: 0x060000AB RID: 171 RVA: 0x00005C90 File Offset: 0x00003E90 public static string ToXml(List<string> idList) { DataTable dt = SysUtils.CreateIDTable(idList); return SysUtils.ToXml(dt); } // Token: 0x060000AC RID: 172 RVA: 0x00005CB0 File Offset: 0x00003EB0 public static string ToXml(string ids) { DataTable dt = SysUtils.CreateIDTable(ids); return SysUtils.ToXml(dt); } // Token: 0x060000AD RID: 173 RVA: 0x00005CD0 File Offset: 0x00003ED0 public static string GetMD5Key(string str) { bool flag = string.IsNullOrEmpty(str); string result; if (flag) { result = str; } else { using (MD5 i = new MD5CryptoServiceProvider()) { byte[] s = i.ComputeHash(Encoding.UTF8.GetBytes(str.ToUpper(CultureInfo.InvariantCulture))); result = BitConverter.ToString(s).Replace("-", ""); } } return result; } } }
2026年03月26日
27 阅读
0 评论
0 点赞
2026-01-30
【069】配置文件读取
支持热更新,更新时间相差3秒以上的会自动重新加载 修改配置文件不用重启IIS服务OpenApiConfigusing System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml.Linq; namespace UFIDA.U9.SH.OPENAPISV.Common { /// <summary> /// 接口配置信息 /// </summary> public class OpenApiConfig { private static Dictionary<string, string> _configCache = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); private static DateTime _lastWriteTime; private static DateTime _lastCheckTime; private static readonly object _lock = new object(); private const int CheckIntervalSeconds = 3; private const string ConfigName = "SH_OpenApiConfig.config"; private static readonly string FilePath; static OpenApiConfig() { string basePath = AppDomain.CurrentDomain.BaseDirectory; // 核心逻辑:智能定位 Portal 目录(保证U9主服务、调度服务、审批流服务都统一读取Portal目录下的配置文件) // 兼容:Portal, Portal/AppServer, MailService, MailService/Libs string portalPath = FindPortalDirectory(basePath); FilePath = Path.Combine(portalPath, ConfigName); } /// <summary> /// 智能查找 Portal 目录(纯字符串操作优化版) /// </summary> private static string FindPortalDirectory(string startPath) { string currentPath = startPath; // 循环直到根目录(GetDirectoryName 返回 null 表示到达根目录) while (!string.IsNullOrEmpty(currentPath)) { // 1. 获取当前文件夹名称 // 注意:Path.GetFileName 对文件夹路径使用时,返回的是最后一级文件夹的名字 string dirName = Path.GetFileName(currentPath.TrimEnd(Path.DirectorySeparatorChar)); // 2. 【自身匹配】如果当前就是 Portal if (string.Equals(dirName, "Portal", StringComparison.OrdinalIgnoreCase)) { return currentPath; } // 3. 【子目录匹配】检查当前目录下是否有 Portal 子文件夹 // 场景:在 C:\yonyou\U9CE 下发现了 Portal 子目录 string potentialPortal = Path.Combine(currentPath, "Portal"); if (Directory.Exists(potentialPortal)) { return potentialPortal; } // 4. 向上移动一级 currentPath = Path.GetDirectoryName(currentPath); } // 如果没找到,返回原始路径 return startPath; } private static void EnsureConfigLoaded() { if ((DateTime.Now - _lastCheckTime).TotalSeconds < CheckIntervalSeconds && _configCache.Count > 0) return; _lastCheckTime = DateTime.Now; if (!File.Exists(FilePath)) { if (_configCache.Count == 0) throw new FileNotFoundException($"配置文件【{ConfigName}】未找到。\n目标路径: {FilePath}\n当前上下文: {AppDomain.CurrentDomain.BaseDirectory}"); return; } DateTime currentWriteTime = File.GetLastWriteTime(FilePath); if (_lastWriteTime >= currentWriteTime && _configCache.Count > 0) return; lock (_lock) { if (_lastWriteTime >= currentWriteTime && _configCache.Count > 0) return; LoadConfigFromXml(); _lastWriteTime = currentWriteTime; } } private static void LoadConfigFromXml() { try { // 使用 LINQ to XML 加载 XElement root = XElement.Load(FilePath); // 解析 <add key="..." value="..." /> 格式 var newCache = root.Descendants("add") .Where(x => x.Attribute("key") != null) .ToDictionary( x => x.Attribute("key").Value, x => x.Attribute("value")?.Value ?? "", StringComparer.OrdinalIgnoreCase ); _configCache = newCache; } catch (Exception ex) { throw new Exception($"加载配置文件【{ConfigName}】失败。\n路径: {FilePath}\n错误: {ex.Message}", ex); } } /// <summary> /// 获取配置值 /// </summary> public static string Get(string key, string orgCode = null) { EnsureConfigLoaded(); string value; // 优先匹配 Key_OrgCode if (!string.IsNullOrEmpty(orgCode) && _configCache.TryGetValue($"{key}_{orgCode}", out value)) return value; // 次优先匹配原始 Key if (_configCache.TryGetValue(key, out value)) return value; return null; } /// <summary> /// 获取整数配置值 /// </summary> public static int GetInt(string key, int defaultValue) { string val = Get(key); return int.TryParse(val, out int result) ? result : defaultValue; } /// <summary> /// 获取长整数配置值 /// </summary> public static long GetInt64(string key, long defaultValue) { string val = Get(key); return long.TryParse(val, out long result) ? result : defaultValue; } } }示例配置文件SH_OpenApiConfig.config<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <!-- 通用配置 --> <add key="Idempotency_Timeout" value="30" desc="幂等性僵尸进程超时时间 (单位: 分钟)" /> <add key="HttpReplay_Timeout" value="10" desc="日志列表重发请求,Http超时时间(分钟)" /> <add key="CenterOrg" value="1002412044341256" desc="数据中心组织ID,实体扩展段查询使用,如供应商可能在数据中心而不是当前组织" /> <!-- 业务配置 --> <add key="AutoDistributeOrgs" value="10" desc="自动下发组织(多个使用英文,号隔开)" /> <add key="PurPriceList_Create_DefCurrency" value="C001" desc="厂商价目表,默认币种" /> <add key="PurPriceAdjustment_DefDocumentType" value="PMTJ0001" desc="厂商价目表,默认调整单单据类型" /> <add key="SOLineThirdField" value="DescFlexField.PrivateDescSeg2" desc="NC销售订单行ID(销售订单行私有段2)" /> </appSettings> </configuration>PushConfigusing System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml.Linq; using UFIDA.U9.Base.Profile; using UFSoft.UBF.PL; namespace UFIDA.U9.SH.HER.QXPubBP.Common { /// <summary> /// 推送配置管理类 (JSON版) /// 特性:支持 // 注释、线程安全、热更新、字典缓存 /// </summary> public class PushConfig { /// <summary> /// 根据组织判断是否需要推送到NC /// </summary> /// <param name="orgCode">组织编码</param> /// <returns></returns> public static bool ShouldPushToNCByOrg(string orgCode) { ProfileValue pv = ProfileValue.Finder.Find("Profile=202601200001 and Organization.Code=@OrgCode", new OqlParam(orgCode)); if (pv == null) return false; bool.TryParse(pv.Value, out bool isPush); return isPush; } #region 内部成员 private static Dictionary<string, string> _configCache = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); private static DateTime _lastWriteTime; private static DateTime _lastCheckTime; private static readonly object _lock = new object(); private const int CheckIntervalSeconds = 3; private const string ConfigName = "SH_PushConfig.config"; private static readonly string FilePath; #endregion static PushConfig() { string basePath = AppDomain.CurrentDomain.BaseDirectory; // 核心逻辑:智能定位 Portal 目录(保证U9主服务、调度服务、审批流服务都统一读取Portal目录下的配置文件) // 兼容:Portal, Portal/AppServer, MailService, MailService/Libs string portalPath = FindPortalDirectory(basePath); FilePath = Path.Combine(portalPath, ConfigName); } /// <summary> /// 智能查找 Portal 目录(纯字符串操作优化版) /// </summary> private static string FindPortalDirectory(string startPath) { string currentPath = startPath; // 循环直到根目录(GetDirectoryName 返回 null 表示到达根目录) while (!string.IsNullOrEmpty(currentPath)) { // 1. 获取当前文件夹名称 // 注意:Path.GetFileName 对文件夹路径使用时,返回的是最后一级文件夹的名字 string dirName = Path.GetFileName(currentPath.TrimEnd(Path.DirectorySeparatorChar)); // 2. 【自身匹配】如果当前就是 Portal if (string.Equals(dirName, "Portal", StringComparison.OrdinalIgnoreCase)) { return currentPath; } // 3. 【子目录匹配】检查当前目录下是否有 Portal 子文件夹 // 场景:在 C:\yonyou\U9CE 下发现了 Portal 子目录 string potentialPortal = Path.Combine(currentPath, "Portal"); if (Directory.Exists(potentialPortal)) { return potentialPortal; } // 4. 向上移动一级 currentPath = Path.GetDirectoryName(currentPath); } // 如果没找到,返回原始路径 return startPath; } private static void EnsureConfigLoaded() { if ((DateTime.Now - _lastCheckTime).TotalSeconds < CheckIntervalSeconds && _configCache.Count > 0) return; _lastCheckTime = DateTime.Now; if (!File.Exists(FilePath)) { if (_configCache.Count == 0) throw new FileNotFoundException($"配置文件【{ConfigName}】未找到。\n目标路径: {FilePath}\n当前上下文: {AppDomain.CurrentDomain.BaseDirectory}"); return; } DateTime currentWriteTime = File.GetLastWriteTime(FilePath); if (_lastWriteTime >= currentWriteTime && _configCache.Count > 0) return; lock (_lock) { if (_lastWriteTime >= currentWriteTime && _configCache.Count > 0) return; LoadConfigFromXml(); _lastWriteTime = currentWriteTime; } } private static void LoadConfigFromXml() { try { // 使用 LINQ to XML 加载 XElement root = XElement.Load(FilePath); // 解析 <add key="..." value="..." /> 格式 var newCache = root.Descendants("add") .Where(x => x.Attribute("key") != null) .ToDictionary( x => x.Attribute("key").Value, x => x.Attribute("value")?.Value ?? "", StringComparer.OrdinalIgnoreCase ); _configCache = newCache; } catch (Exception ex) { throw new Exception($"加载配置文件【{ConfigName}】失败。\n路径: {FilePath}\n错误: {ex.Message}", ex); } } /// <summary> /// 获取配置值 /// </summary> public static string Get(string key, string orgCode = null) { EnsureConfigLoaded(); string value; // 优先匹配 Key_OrgCode if (!string.IsNullOrEmpty(orgCode) && _configCache.TryGetValue($"{key}_{orgCode}", out value)) return value; // 次优先匹配原始 Key if (_configCache.TryGetValue(key, out value)) return value; return null; } /// <summary> /// 获取整数配置值 /// </summary> public static int GetInt(string key, int defaultValue) { string val = Get(key); return int.TryParse(val, out int result) ? result : defaultValue; } /// <summary> /// 获取长整数配置值 /// </summary> public static long GetInt64(string key, long defaultValue) { string val = Get(key); return long.TryParse(val, out long result) ? result : defaultValue; } } } 示例配置文件SH_PushConfig.config<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="HttpPush_Timeout" value="10" desc="HTTP请求超时时间(分钟)" /> <add key="NCSync_BaseUrl" value="http://172.16.4.12:8054" desc="NC接口地址" /> <add key="NCSync_ItemMaster" value="/uapws/rest/service/u9/synMaterial" desc="NC料品同步接口路由" /> <add key="NCSync_ItemMaster_PushOrg" value="10" desc="NC料品同步的组织信息" /> <add key="NCSync_ItemMaster_DefMattaxes" value="CN001" desc="NC料品同步的默认税组合编码" /> </appSettings> </configuration>
2026年01月30日
27 阅读
0 评论
0 点赞
2026-01-30
【068】根据扩展字段的显示名称反向获取编码
根据实体全称、公私有段、段序号、私有段的名称值、组织ID、数据中心组织ID查找出这个私有段(值集)SQL执行存储过程因为很多时候调用存储过程都是通用代码调用,所以需要当前登录组织跟数据中心组织两个组织编码,因为私有段是实体值集类型的时候,对应实体可能不维护在当前组织而是在数据中心 比如供应商值集很多时候都是在数据中心,存储过程SH_GetFlexFieldCodeByName内部会先按照当前组织ID查找,没找到就按照数据中心查找DECLARE @ResultCode NVARCHAR(500) -- 用于接收返回的编码 DECLARE @Name NVARCHAR(500) = N'维生素EC颗粒' EXEC [dbo].[SH_GetFlexFieldCodeByName] @FullName = 'UFIDA.U9.CBO.SCM.Item.ItemMaster', -- 实体全称 @ContextValue = 'Private', -- 上下文: 'Public' (公有段) 或 'Private' (私有段) @DNumber = 14, -- 段号: 对应 PrivateDescSeg14 @Name = @Name, -- 要查找的显示名称 (注意加 N 前缀防止乱码) @Org = 1002406210000526, -- 当前组织 @CenterOrg = 1002406210000054, -- 数据中心组织 (需要数据中心组织是因为比如供应商档案可能不在当前组织,在数据中心) @ResultCode = @ResultCode OUTPUT -- 标记为输出参数 SELECT @ResultCode AS [FoundCode] --如果不为NULL,说明找到了对应的编码示例(自定义值集)料品私有段14,自定义值集项目大类示例(实体值集)料品私有段15,实体类型值集项目示例(自定义枚举)部门私有段1,枚举类型的值集代码调用存储过程orgID可以赋值Base.Context.LoginOrg.ID centerOrgId可以读取配置也可以写死(一般数据中心ID不会发生变化) fullName可以使用实体.EntityRes.BE_FullName,比如 ItemMaster.EntityRes.BE_FullName/// <summary> /// 根据扩展字段的显示名称反向获取编码 /// </summary> public static string GetCodeFromSP(string fullName, string context, int dNumber, string name, long orgId, long centerOrgId) { DataParamList pars = new DataParamList { DataParamFactory.CreateInput("FullName", fullName), DataParamFactory.CreateInput("ContextValue", context), DataParamFactory.CreateInput("DNumber", dNumber, DbType.Int32), DataParamFactory.CreateInput("Name", name), DataParamFactory.CreateInput("Org", orgId, DbType.Int64), DataParamFactory.CreateInput("CenterOrg", centerOrgId, DbType.Int64), DataParamFactory.CreateOutput("ResultCode", DbType.String) }; DataAccessor.RunSP("SH_GetFlexFieldCodeByName", pars); if (pars["ResultCode"].Value != null && pars["ResultCode"].Value != DBNull.Value) { return pars["ResultCode"].Value.ToString(); } return null; }调用示例long orgId = Base.Context.LoginOrg.ID; // 当前登录组织ID long centerOrgId = OpenApiConfig.GetInt64("CenterOrgId", 0); // 数据中心ID(读取配置) string 值集值编码 = GetCodeFromSP(ItemMaster.EntityRes.BE_FullName, "Private", 14, "值集值名称", orgId, centerOrgId);存储过程SH_GetFlexFieldCodeByName/****** Object: StoredProcedure [dbo].[SH_GetFlexFieldCodeByName] Script Date: 2026/01/30 ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO IF EXISTS (SELECT 1 FROM sysobjects WHERE id = object_id(N'[dbo].[SH_GetFlexFieldCodeByName]') AND xtype = 'P' ) BEGIN DROP PROCEDURE [dbo].[SH_GetFlexFieldCodeByName] END GO /* 功能:根据扩展字段的显示名称反向获取存储编码 (Name -> Code) 应用场景:接口传入“名称”,系统自动转换为“编码”存入 Pub/Pri 字段 */ CREATE PROC [dbo].[SH_GetFlexFieldCodeByName] ( @FullName VARCHAR(100), -- 宿主实体的全名 (如 UFIDA.U9.CBO.SCM.Item.ItemMaster) @ContextValue VARCHAR(80), -- 上下文 (Public 或 Global/Private) @DNumber INT, -- 段号 (1-50 或 1-30) @Name NVARCHAR(500), -- 要查找的名称值 @Org BIGINT = 0, -- 当前组织ID @CenterOrg BIGINT = 0, -- 数据中心组织ID (用于兜底查找) @ResultCode NVARCHAR(500) OUTPUT -- 【输出参数】找到的编码 ) AS BEGIN SET NOCOUNT ON; SET @ResultCode = NULL; -- 如果名称为空,无需查找 IF ISNULL(@Name, '') = '' RETURN; -- 0. 参数标准化处理 IF @ContextValue = 'Private' SET @ContextValue = 'Global'; -- 1. 变量定义 DECLARE @IsPublicSeg BIT DECLARE @RealNumber INT IF @ContextValue = 'Public' BEGIN SET @IsPublicSeg = 1 SET @RealNumber = @DNumber END ELSE BEGIN SET @IsPublicSeg = 0 SET @RealNumber = @DNumber END -- 2. 获取该段的定义信息 DECLARE @ValidateEnum INT DECLARE @ValueType INT DECLARE @ValueSetDefID BIGINT SELECT @ValidateEnum = E.ValidateType, @ValueType = E.ValueType, @ValueSetDefID = E.ID FROM UBF_MD_Class A INNER JOIN Base_DescFlexFieldDef B ON A.Local_ID = B.EntityType INNER JOIN Base_DescFlexContext C ON B.ID = C.DescFlexFieldDef INNER JOIN Base_DescFlexSegment D ON C.ID = D.DescFlexContext INNER JOIN base_valuesetdef E ON D.ValueSetDef = E.ID WHERE A.FullName = @FullName AND C.ContextValue = @ContextValue AND D.Number = @DNumber IF @ValidateEnum IS NULL RETURN; -- 3. 根据校验类型分别处理 ---------------------------------------------------------------- -- 类型 1: 实体引用 (Entity) - 包含回退逻辑 ---------------------------------------------------------------- IF @ValidateEnum = 1 BEGIN DECLARE @TargetTableName NVARCHAR(100) DECLARE @CodeColName NVARCHAR(100) DECLARE @NameColName NVARCHAR(100) DECLARE @NameIsGlob BIT DECLARE @HasOrgColumn INT DECLARE @BaseSQL NVARCHAR(MAX) -- 基础查询语句 DECLARE @FinalSQL NVARCHAR(MAX) DECLARE @ParamDef NVARCHAR(500) -- 获取目标实体对应的表名、编码列、名称列信息 SELECT @TargetTableName = cc.DefaultTableName, @CodeColName = codeAtt.Name, @NameColName = NameAtt.Name, @NameIsGlob = NameAtt.IsGlobalization FROM base_valuesetdef a LEFT JOIN UBF_MD_Class cc ON a.EntityType = cc.Local_ID LEFT JOIN UBF_MD_Attribute codeAtt ON a.CodeAttribute = codeAtt.Local_ID LEFT JOIN UBF_MD_Attribute NameAtt ON a.NameAttribute = NameAtt.Local_ID WHERE a.ID = @ValueSetDefID IF @TargetTableName IS NOT NULL AND @CodeColName IS NOT NULL AND @NameColName IS NOT NULL BEGIN -- 检查目标表是否有 Org 字段 SELECT @HasOrgColumn = COUNT(1) FROM sys.columns WHERE object_id = OBJECT_ID(@TargetTableName) AND name = 'Org' -- 构建基础查询 SQL (不包含 Org 条件) SET @BaseSQL = 'SELECT TOP 1 @Out = A.[' + @CodeColName + '] FROM ' + @TargetTableName + ' A ' IF @NameIsGlob = 1 BEGIN SET @BaseSQL = @BaseSQL + ' INNER JOIN ' + @TargetTableName + '_Trl B ON A.ID = B.ID ' SET @BaseSQL = @BaseSQL + ' WHERE B.[' + @NameColName + '] = @InName ' END ELSE BEGIN SET @BaseSQL = @BaseSQL + ' WHERE A.[' + @NameColName + '] = @InName ' END SET @ParamDef = N'@InName NVARCHAR(500), @InOrg BIGINT, @Out NVARCHAR(500) OUTPUT'; -- A. 尝试在当前组织查找 IF @HasOrgColumn > 0 AND @Org > 0 BEGIN SET @FinalSQL = @BaseSQL + ' AND A.Org = @InOrg ' EXEC sp_executesql @FinalSQL, @ParamDef, @InName = @Name, @InOrg = @Org, @Out = @ResultCode OUTPUT; END ELSE BEGIN -- 如果表没有Org字段,直接查即可,不需要过滤组织 EXEC sp_executesql @BaseSQL, @ParamDef, @InName = @Name, @InOrg = 0, @Out = @ResultCode OUTPUT; END -- B. 兜底逻辑:如果没找到,且表有Org字段,且提供了不同的中心组织ID IF @ResultCode IS NULL AND @HasOrgColumn > 0 AND @CenterOrg > 0 AND @CenterOrg <> @Org BEGIN SET @FinalSQL = @BaseSQL + ' AND A.Org = @InOrg ' -- 这里传入 @CenterOrg 作为 @InOrg 参数 EXEC sp_executesql @FinalSQL, @ParamDef, @InName = @Name, @InOrg = @CenterOrg, @Out = @ResultCode OUTPUT; END END END ---------------------------------------------------------------- -- 类型 4: 枚举 (Enum) ---------------------------------------------------------------- ELSE IF @ValidateEnum = 4 BEGIN SELECT TOP 1 @ResultCode = ev.Code FROM base_valuesetdef a INNER JOIN UBF_MD_Class cc ON a.EnumType = cc.Local_ID INNER JOIN UBF_Sys_ExtEnumType extType ON extType.UID = cc.ID INNER JOIN UBF_Sys_ExtEnumValue ev ON extType.ID = ev.ExtEnumType INNER JOIN UBF_Sys_ExtEnumValue_Trl evt ON ev.ID = evt.ID WHERE a.ID = @ValueSetDefID AND evt.Name = @Name END ---------------------------------------------------------------- -- 类型 3: 自定义值集 (Custom) ---------------------------------------------------------------- ELSE IF @ValidateEnum = 3 BEGIN DECLARE @CustomSQL NVARCHAR(MAX) DECLARE @CustomParam NVARCHAR(500) SET @CustomSQL = N'SELECT TOP 1 @Out = Code FROM dbo.f_getdefinevalue(@InFull, @InNum, @InCtx) WHERE Name = @InName' SET @CustomParam = N'@InFull VARCHAR(100), @InNum INT, @InCtx VARCHAR(80), @InName NVARCHAR(500), @Out NVARCHAR(500) OUTPUT' EXEC sp_executesql @CustomSQL, @CustomParam, @InFull = @FullName, @InNum = @DNumber, @InCtx = @ContextValue, @InName = @Name, @Out = @ResultCode OUTPUT END ---------------------------------------------------------------- -- 类型 2: 无档案/简单值 (Simple) ---------------------------------------------------------------- ELSE IF @ValidateEnum = 2 BEGIN IF @ValueType = 5 BEGIN IF @Name = '是' OR @Name = '√' OR @Name = 'Yes' OR @Name = 'True' OR @Name = '1' SET @ResultCode = 'True' ELSE SET @ResultCode = 'False' END ELSE BEGIN SET @ResultCode = @Name END END END GO扩展 - 接口自动查询并赋值配置文件读取:【069】配置文件读取 - 月光一线调用示例reqInfo.PubPriDt是PubPriSVData// 客户 var cusDto = new ISV.Customer.CustomerDTOData(); // ......客户Dto赋值 cusDto.DescFlexField = Utils.SetDescFlex(cusDto.DescFlexField, reqInfo.PubPriDt, Customer.EntityRes.BE_FullName); // 客户位置 var siteDto = new ISV.Customer.CustomerSiteDTOData(); // ......客户位置Dto赋值 siteDto.DescFlexField = Utils.SetDescFlex(siteDto.DescFlexField, siteInfo.PubPriDt, CustomerSite.EntityRes.BE_FullName);Utilsusing System.Collections.Generic; using UFIDA.U9.Base.FlexField.DescFlexField; using UFIDA.U9.Base.Profile; using UFIDA.U9.Base.Profile.Proxy; using UFIDA.U9.SH.LogServiceBP.Proxy; using UFIDA.U9.SH.OPENAPISV.Model; using UFSoft.UBF.Util.Context; namespace UFIDA.U9.SH.OPENAPISV.Common { /// <summary> /// 工具类 /// </summary> public static class Utils { /// <summary> /// 扩展字段赋值 /// </summary> /// <param name="descFlexField"></param> /// <param name="pubPriDt"></param> /// <param name="entityFullName">设置扩展段的实体名称</param> /// <returns></returns> public static DescFlexSegments SetDescFlex(DescFlexSegments descFlexField, PubPriSVData pubPriDt, string entityFullName) { long orgId = Base.Context.LoginOrg.ID; // 当前登录组织ID long centerOrgId = OpenApiConfig.GetInt64("CenterOrgId", 0); // 数据中心ID return FlexFieldExtensions.SetDescFlexSmart(descFlexField, pubPriDt, entityFullName, orgId, centerOrgId); } /// <summary> /// 扩展字段赋值 /// </summary> /// <param name="descFlexField"></param> /// <param name="pubPriDt"></param> /// <param name="entityFullName">设置扩展段的实体名称</param> /// <returns></returns> public static DescFlexSegmentsData SetDescFlex(DescFlexSegmentsData descFlexField, PubPriSVData pubPriDt, string entityFullName) { long orgId = Base.Context.LoginOrg.ID; // 当前登录组织ID long centerOrgId = OpenApiConfig.GetInt64("CenterOrgId", 0); // 数据中心ID return FlexFieldExtensions.SetDescFlexSmart(descFlexField, pubPriDt, entityFullName, orgId, centerOrgId); } FlexFieldExtensionsusing System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Data; using System.Linq.Expressions; using UFIDA.U9.SH.OPENAPISV.Model; using UFSoft.UBF.Util.DataAccess; namespace UFIDA.U9.SH.OPENAPISV.Common { /// <summary> /// 扩展字段赋值工具类 (性能优化版) /// </summary> public static class FlexFieldExtensions { // 缓存编译后的赋值委托 (T target, PubPriSVData source) private static readonly ConcurrentDictionary<Type, Action<object, PubPriSVData>> _assignmentCache = new ConcurrentDictionary<Type, Action<object, PubPriSVData>>(); // [核心优化] 缓存名称解析的元数据和访问器,避免运行时的 GetProperty 反射 private static readonly List<FlexFieldResolver> _resolvers = new List<FlexFieldResolver>(); /// <summary> /// 静态构造函数,初始化所有字段的访问委托 (只执行一次) /// </summary> static FlexFieldExtensions() { InitializeResolvers(); } #region 内部结构定义 /// <summary> /// 字段解析器元数据 /// </summary> private class FlexFieldResolver { public string Context { get; set; } // "Public" or "Private" public int Number { get; set; } public string FullName { get; set; } // 字段描述,用于报错 // 快速访问委托 public Func<PubPriSVData, string> GetCode { get; set; } // 读取 Code (用于优先判断) public Func<PubPriSVData, string> GetName { get; set; } // 读取 _Name public Action<PubPriSVData, string> SetCode { get; set; } // 写入 Code } #endregion #region 静态初始化逻辑 private static void InitializeResolvers() { // 预编译 50 个公有段 for (int i = 1; i <= 50; i++) { _resolvers.Add(BuildResolver("Public", i)); } // 预编译 30 个私有段 for (int i = 1; i <= 30; i++) { _resolvers.Add(BuildResolver("Private", i)); } } private static FlexFieldResolver BuildResolver(string context, int index) { string prefix = context == "Public" ? "Pub" : "Pri"; var sourceParam = Expression.Parameter(typeof(PubPriSVData), "s"); var valueParam = Expression.Parameter(typeof(string), "v"); // 1. 构建 GetCode 委托: (s) => s.Pub1 var codeProp = typeof(PubPriSVData).GetProperty($"{prefix}{index}"); var getCodeFunc = Expression.Lambda<Func<PubPriSVData, string>>( Expression.Property(sourceParam, codeProp), sourceParam).Compile(); // 2. 构建 GetName 委托: (s) => s.Pub1_Name var nameProp = typeof(PubPriSVData).GetProperty($"{prefix}{index}_Name"); var getNameFunc = Expression.Lambda<Func<PubPriSVData, string>>( Expression.Property(sourceParam, nameProp), sourceParam).Compile(); // 3. 构建 SetCode 委托: (s, v) => s.Pub1 = v var setCodeAction = Expression.Lambda<Action<PubPriSVData, string>>( Expression.Assign(Expression.Property(sourceParam, codeProp), valueParam), sourceParam, valueParam).Compile(); return new FlexFieldResolver { Context = context, Number = index, FullName = $"{context}DescSeg{index}", GetCode = getCodeFunc, GetName = getNameFunc, SetCode = setCodeAction }; } #endregion #region 公开方法 /// <summary> /// 智能扩展字段赋值核心方法 (泛型) /// </summary> public static T SetDescFlexSmart<T>(T target, PubPriSVData source) where T : class, new() { if (source == null) return target; if (target == null) target = new T(); var action = _assignmentCache.GetOrAdd(typeof(T), t => BuildMapper(t)); action(target, source); return target; } /// <summary> /// 智能扩展字段赋值核心方法 (带名称解析功能) /// </summary> public static T SetDescFlexSmart<T>(T target, PubPriSVData source, string entityFullName, long orgId, long centerOrgId = 0) where T : class, new() { if (source == null) return target; // 1. 执行名称解析 (使用优化后的逻辑) ResolveFlexFieldCodes(source, entityFullName, orgId, centerOrgId); // 2. 调用原有逻辑进行赋值 return SetDescFlexSmart(target, source); } #endregion #region 解析逻辑 (优化版) /// <summary> /// 解析 PubPriSVData 中的 Name 字段 /// </summary> private static void ResolveFlexFieldCodes(PubPriSVData source, string entityFullName, long orgId, long centerOrgId) { // 直接读取一次全局配置 bool ignoreCheck = source.IgnoreNameCheck; // 直接遍历预编译好的解析器列表 foreach (var resolver in _resolvers) { // 1. 优先检查 Code 是否已有值 (如果有,直接跳过解析,以Code为准) string existingCode = resolver.GetCode(source); if (!string.IsNullOrEmpty(existingCode)) continue; // 2. 快速读取 Name (委托调用) string nameVal = resolver.GetName(source); // 3. 如果 Name 为空,立即跳过 if (string.IsNullOrEmpty(nameVal)) continue; // 4. 只有当确实有值且无Code时,才执行数据库调用 string code = GetCodeFromSP(entityFullName, resolver.Context, resolver.Number, nameVal, orgId, centerOrgId); // 5. 回填 Code if (!string.IsNullOrEmpty(code)) { resolver.SetCode(source, code); } else { // 6. 校验逻辑 (根据全局 IgnoreNameCheck 决定) if (!ignoreCheck) { throw new SHApiCustException($"扩展字段解析失败: 无法根据名称 '{nameVal}' 找到对应的编码。\n字段: {resolver.FullName}\n实体: {entityFullName}\n组织: {orgId}{(centerOrgId > 0 ? "/" + centerOrgId : "")}"); } } } } /// <summary> /// 使用 U9 DataAccessor 调用存储过程 /// </summary> private static string GetCodeFromSP(string fullName, string context, int dNumber, string name, long orgId, long centerOrgId) { DataParamList pars = new DataParamList { DataParamFactory.CreateInput("FullName", fullName), DataParamFactory.CreateInput("ContextValue", context), DataParamFactory.CreateInput("DNumber", dNumber, DbType.Int32), DataParamFactory.CreateInput("Name", name), DataParamFactory.CreateInput("Org", orgId, DbType.Int64), DataParamFactory.CreateInput("CenterOrg", centerOrgId, DbType.Int64), DataParamFactory.CreateOutput("ResultCode", DbType.String) }; // 注意:DataAccessor.RunSP 内部通常会管理连接池 DataAccessor.RunSP("SH_GetFlexFieldCodeByName", pars); if (pars["ResultCode"].Value != null && pars["ResultCode"].Value != DBNull.Value) { return pars["ResultCode"].Value.ToString(); } return null; } #endregion #region 赋值逻辑 (原有的表达式树构建) private static Action<object, PubPriSVData> BuildMapper(Type targetType) { var targetParam = Expression.Parameter(typeof(object), "target"); var sourceParam = Expression.Parameter(typeof(PubPriSVData), "source"); var castTarget = Expression.Convert(targetParam, targetType); var statements = new List<Expression>(); for (int i = 1; i <= 30; i++) { AddMapping(statements, castTarget, sourceParam, targetType, $"Pri{i}", $"PrivateDescSeg{i}"); } for (int i = 1; i <= 50; i++) { AddMapping(statements, castTarget, sourceParam, targetType, $"Pub{i}", $"PubDescSeg{i}"); } var block = Expression.Block(statements); return Expression.Lambda<Action<object, PubPriSVData>>(block, targetParam, sourceParam).Compile(); } private static void AddMapping(List<Expression> statements, Expression target, ParameterExpression source, Type targetType, string sourcePropName, string targetPropName) { var sourceProp = typeof(PubPriSVData).GetProperty(sourcePropName); var targetProp = targetType.GetProperty(targetPropName); if (sourceProp != null && targetProp != null) { var sourceValue = Expression.Property(source, sourceProp); var isNullOrEmptyMethod = typeof(string).GetMethod(nameof(string.IsNullOrEmpty)); var check = Expression.Not(Expression.Call(isNullOrEmptyMethod, sourceValue)); var assign = Expression.Assign(Expression.Property(target, targetProp), sourceValue); statements.Add(Expression.IfThen(check, assign)); } } #endregion } }公私有段定义Model文件PubPriSVDatanamespace UFIDA.U9.SH.OPENAPISV.Model { /// <summary> /// 公私扩展字段 (带名称解析支持) /// </summary> public class PubPriSVData { /// <summary> /// 全局控制:是否忽略名称校验 /// <para>True: 当根据Name查不到Code时,忽略错误继续执行 (结果可能为空)</para> /// <para>False (默认): 当根据Name查不到Code时,抛出异常</para> /// </summary> public bool IgnoreNameCheck { get; set; } #region 共有段 (Pub1 - Pub50) /// <summary>共有段1</summary> public string Pub1 { get; set; } /// <summary>共有段1-名称</summary> public string Pub1_Name { get; set; } public string Pub2 { get; set; } public string Pub2_Name { get; set; } public string Pub3 { get; set; } public string Pub3_Name { get; set; } public string Pub4 { get; set; } public string Pub4_Name { get; set; } public string Pub5 { get; set; } public string Pub5_Name { get; set; } public string Pub6 { get; set; } public string Pub6_Name { get; set; } public string Pub7 { get; set; } public string Pub7_Name { get; set; } public string Pub8 { get; set; } public string Pub8_Name { get; set; } public string Pub9 { get; set; } public string Pub9_Name { get; set; } public string Pub10 { get; set; } public string Pub10_Name { get; set; } public string Pub11 { get; set; } public string Pub11_Name { get; set; } public string Pub12 { get; set; } public string Pub12_Name { get; set; } public string Pub13 { get; set; } public string Pub13_Name { get; set; } public string Pub14 { get; set; } public string Pub14_Name { get; set; } public string Pub15 { get; set; } public string Pub15_Name { get; set; } public string Pub16 { get; set; } public string Pub16_Name { get; set; } public string Pub17 { get; set; } public string Pub17_Name { get; set; } public string Pub18 { get; set; } public string Pub18_Name { get; set; } public string Pub19 { get; set; } public string Pub19_Name { get; set; } public string Pub20 { get; set; } public string Pub20_Name { get; set; } public string Pub21 { get; set; } public string Pub21_Name { get; set; } public string Pub22 { get; set; } public string Pub22_Name { get; set; } public string Pub23 { get; set; } public string Pub23_Name { get; set; } public string Pub24 { get; set; } public string Pub24_Name { get; set; } public string Pub25 { get; set; } public string Pub25_Name { get; set; } public string Pub26 { get; set; } public string Pub26_Name { get; set; } public string Pub27 { get; set; } public string Pub27_Name { get; set; } public string Pub28 { get; set; } public string Pub28_Name { get; set; } public string Pub29 { get; set; } public string Pub29_Name { get; set; } public string Pub30 { get; set; } public string Pub30_Name { get; set; } public string Pub31 { get; set; } public string Pub31_Name { get; set; } public string Pub32 { get; set; } public string Pub32_Name { get; set; } public string Pub33 { get; set; } public string Pub33_Name { get; set; } public string Pub34 { get; set; } public string Pub34_Name { get; set; } public string Pub35 { get; set; } public string Pub35_Name { get; set; } public string Pub36 { get; set; } public string Pub36_Name { get; set; } public string Pub37 { get; set; } public string Pub37_Name { get; set; } public string Pub38 { get; set; } public string Pub38_Name { get; set; } public string Pub39 { get; set; } public string Pub39_Name { get; set; } public string Pub40 { get; set; } public string Pub40_Name { get; set; } public string Pub41 { get; set; } public string Pub41_Name { get; set; } public string Pub42 { get; set; } public string Pub42_Name { get; set; } public string Pub43 { get; set; } public string Pub43_Name { get; set; } public string Pub44 { get; set; } public string Pub44_Name { get; set; } public string Pub45 { get; set; } public string Pub45_Name { get; set; } public string Pub46 { get; set; } public string Pub46_Name { get; set; } public string Pub47 { get; set; } public string Pub47_Name { get; set; } public string Pub48 { get; set; } public string Pub48_Name { get; set; } public string Pub49 { get; set; } public string Pub49_Name { get; set; } public string Pub50 { get; set; } public string Pub50_Name { get; set; } #endregion #region 私有段 (Pri1 - Pri30) /// <summary>私有段1</summary> public string Pri1 { get; set; } /// <summary>私有段1-名称</summary> public string Pri1_Name { get; set; } public string Pri2 { get; set; } public string Pri2_Name { get; set; } public string Pri3 { get; set; } public string Pri3_Name { get; set; } public string Pri4 { get; set; } public string Pri4_Name { get; set; } public string Pri5 { get; set; } public string Pri5_Name { get; set; } public string Pri6 { get; set; } public string Pri6_Name { get; set; } public string Pri7 { get; set; } public string Pri7_Name { get; set; } public string Pri8 { get; set; } public string Pri8_Name { get; set; } public string Pri9 { get; set; } public string Pri9_Name { get; set; } public string Pri10 { get; set; } public string Pri10_Name { get; set; } public string Pri11 { get; set; } public string Pri11_Name { get; set; } public string Pri12 { get; set; } public string Pri12_Name { get; set; } public string Pri13 { get; set; } public string Pri13_Name { get; set; } public string Pri14 { get; set; } public string Pri14_Name { get; set; } public string Pri15 { get; set; } public string Pri15_Name { get; set; } public string Pri16 { get; set; } public string Pri16_Name { get; set; } public string Pri17 { get; set; } public string Pri17_Name { get; set; } public string Pri18 { get; set; } public string Pri18_Name { get; set; } public string Pri19 { get; set; } public string Pri19_Name { get; set; } public string Pri20 { get; set; } public string Pri20_Name { get; set; } public string Pri21 { get; set; } public string Pri21_Name { get; set; } public string Pri22 { get; set; } public string Pri22_Name { get; set; } public string Pri23 { get; set; } public string Pri23_Name { get; set; } public string Pri24 { get; set; } public string Pri24_Name { get; set; } public string Pri25 { get; set; } public string Pri25_Name { get; set; } public string Pri26 { get; set; } public string Pri26_Name { get; set; } public string Pri27 { get; set; } public string Pri27_Name { get; set; } public string Pri28 { get; set; } public string Pri28_Name { get; set; } public string Pri29 { get; set; } public string Pri29_Name { get; set; } public string Pri30 { get; set; } public string Pri30_Name { get; set; } #endregion } }
2026年01月30日
28 阅读
0 评论
0 点赞
2025-11-28
【067】U9C获取生产环境数据库连接串
UBF调试获取调试获取有时候客户不情愿给生产环境的密码 但是测试库可以连接生产环境,为了方便查询一些东西 需要知道数据库连接串,提取用户名跟密码 U9登录报表之后,连接串会写到Runtime\environment.xml,可以明文查看 U9C登录报表之后,连接串中的密码是加密的,无法直接使用,只能使用调试启动的方式查看到解密后的密码 前提条件:有一个能登录生产环境的UBF使用DnSpy32位打开UBF打开UBFdevenv.exe,用于调试启动 打开~\UBFStudio\plugin\RuntimeReport\UFSoft.UBF.Report.Designer.RuntimeSolution.dll用于断点查看解密后的数据库连接串打断点命名空间:UFSoft.UBF.Report.Designer.RuntimeSolution 类名:RuntimeReportLoginForm 方法:InitEnterpriseList 方法中的enterpriseItem.ConnectionString赋值执行后,就是解密的数据库连接串开始调试选中UBFdevenv.exe开始调试 监视1添加enterpriseItem.ConnectionString的监视 调试启动后,切换到报表登录模式,点击登录 服务器输入要获取的应用服务器地址,点击刷新即可进入断点 (点击登录首次会默认localhost,并进入断点,直接跳过继续即可)获取结果生产环境配置文件如果能直接进生产环境,配置文件C:\yonyou\U9CE\Portal\SysManageServer\EA.log中会有附:报表服务器账密报表服务器账密一般也是数据库服务器账密 如果是测试环境,同时也是测试环境的账密附加C:\yonyou\U9ClientCE\ClientSystemManage\UFIDA.U9.SystemManage.SystemManagerClient.exe附加C:\yonyou\U9ClientCE\ClientSystemManage\UFIDA.UBF.SystemManage.dll断点设置在UFIDA.UBF.SystemManage.ReportServerAccountEidtForm搜索(string AccountPassword = )调试启动UFIDA.U9.SystemManage.SystemManagerClient.exe打开报表服务器->设置账户,账户名后面加个1234保证账户是错误的进入288行的断点后,F11进入解密方法,解密方法的形参就是账密附:数据库拼接一键登录地址设置好U9地址跟企业编码,执行以下SQL语句,在结果集中复制[一键登录地址]访问可直接登录 U9也可以用(不过EA会报错空指针引用,因为调用GetOrgsByUserCode获取EA用户组织信息会返回null,后续直接执行((ArrayList)userOrgsByUserCode["OrgSequence"])所以报错)U9C的EA用户没有这个问题,也可以一键登录(应该是集团修复了)declare @U9CUrl nvarchar(100)='http://localhost/U9C' -- U9地址 declare @EnterpriseId nvarchar(50)='001' -- 企业编码 select u.Code as 登录账号, u.Name as 用户名, u.PassWord, IIF(u.Code = 'EA', -10000, org.ID) as 组织ID, org.Code as 组织编码, @EnterpriseId as 企业编码, CONCAT( @U9CUrl, '/api/v1/autologin.aspx?enterprise_id=', @EnterpriseId, '&user_code=', u.Code, '&user_password=', u.PassWord, '&organization_id=', IIF(u.Code = 'EA', -10000, org.ID), '&e=m' ) AS [一键登录地址] from Base_User AS u left join Base_UserOrg AS uo on uo.[User]=u.ID left join Base_Organization as org on org.ID=uo.Org
2025年11月28日
114 阅读
0 评论
1 点赞
2025-11-26
【066】后台批量删除实体所有附件
后台批量删除实体所有附件期初OBA导入时,可能附件信息导入错误(客户提供错误) 但是OBA没有批量删除的功能 可以执行以下SQL语句,清理对应实体的所有附件信息 @TargetEntity可以在实体扩展字段处找到,类全名就是 注意:DB_NAME()会获取当前环境数据库名称,请务必切换到文档库所对应的业务库下执行语句/* * ============================================================================== * 脚本名称:U9 附件跨库强力清除工具 (含多语言表 Trl 清理版) * 作用:清理指定 Entity 的 Base_Attachment, Base_Attachment_Trl 及 FileInfo * 逻辑: * 1. 锁定 Base_Attachment.ID 和 Base_Attachment.FileHandler * 2. 删除 Base_Attachment_Trl (ID = Base_Attachment.ID) * 3. 删除 Base_Attachment * 4. 删除 FileInfo (跨库) * ============================================================================== */ -- 【1. 配置区域】请在此处修改数据库名称 DECLARE @BusinessDBName NVARCHAR(128) = DB_NAME(); -- 自动获取当前业务数据库名,请在文档库所对应的业务库环境下执行 DECLARE @DocDBName NVARCHAR(128) = 'U9TESTDoc'; -- 存储文件的数据库 (文档库) DECLARE @TargetEntity NVARCHAR(256) = 'UFIDA.U9.CBO.SCM.Item.ItemMaster'; -- 目标实体名称 -- ============================================================================== -- 以下逻辑自动执行,无需修改 -- ============================================================================== SET NOCOUNT ON; DECLARE @SQL NVARCHAR(MAX); DECLARE @Params NVARCHAR(MAX); PRINT '----------------------------------------------------------'; PRINT '任务开始: ' + CONVERT(VARCHAR, GETDATE(), 120); PRINT '当前业务库: [' + @BusinessDBName + ']'; PRINT '目标文档库: [' + @DocDBName + ']'; PRINT '----------------------------------------------------------'; IF DB_ID(@DocDBName) IS NULL BEGIN RAISERROR(N'错误: 未找到名为 [%s] 的文档数据库,请检查配置。', 16, 1, @DocDBName); RETURN; END -- 构建动态 SQL SET @SQL = N' USE [' + @BusinessDBName + ']; BEGIN TRY BEGIN TRANSACTION; -- 1. 创建临时表,同时锁定 主键ID (用于删表) 和 FileHandler (用于删文件) CREATE TABLE #TempDeleteIDs ( SysID BIGINT PRIMARY KEY, -- Base_Attachment.ID FileID NVARCHAR(50) -- Base_Attachment.FileHandler ); -- 填充待删除列表 INSERT INTO #TempDeleteIDs (SysID, FileID) SELECT A.ID, A.FileHandler FROM [dbo].[Base_Attachment] AS A INNER JOIN [' + @DocDBName + '].[dbo].[FileInfo] AS B ON A.FileHandler = B.ID WHERE A.EntityFullName = @p_EntityName; DECLARE @DelCount INT = (SELECT COUNT(1) FROM #TempDeleteIDs); IF @DelCount > 0 BEGIN PRINT ''>> 发现匹配记录: '' + CAST(@DelCount AS NVARCHAR(20)) + '' 条,开始清理...''; -- 2. 删除多语言表 (Base_Attachment_Trl) -- 关系: Base_Attachment_Trl.ID = Base_Attachment.ID DELETE T FROM [dbo].[Base_Attachment_Trl] T INNER JOIN #TempDeleteIDs TMP ON T.ID = TMP.SysID; PRINT ''>> [Base_Attachment_Trl] (多语言表) 清理完成。''; -- 3. 删除业务主表 (Base_Attachment) DELETE A FROM [dbo].[Base_Attachment] A INNER JOIN #TempDeleteIDs TMP ON A.ID = TMP.SysID; PRINT ''>> [Base_Attachment] (主表) 清理完成。''; -- 4. 删除文件库数据 (FileInfo) DELETE B FROM [' + @DocDBName + '].[dbo].[FileInfo] B INNER JOIN #TempDeleteIDs TMP ON B.ID = TMP.FileID; PRINT ''>> [' + @DocDBName + '].[dbo].[FileInfo] (文件数据) 清理完成。''; COMMIT TRANSACTION; PRINT ''>> SUCCESS: 事务已提交,3张表的数据已全部清除。''; END ELSE BEGIN ROLLBACK TRANSACTION; PRINT ''>> 提示: 未发现 EntityFullName 为指定名称的附件记录,无需执行删除。''; END END TRY BEGIN CATCH IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION; DECLARE @ErrMsg NVARCHAR(4000) = ERROR_MESSAGE(); PRINT ''!! 异常: 发生错误,事务已回滚。原因: '' + @ErrMsg; RAISERROR(@ErrMsg, 16, 1); END CATCH; '; -- 定义参数 SET @Params = N'@p_EntityName NVARCHAR(256)'; -- 执行动态 SQL EXEC sp_executesql @SQL, @Params, @p_EntityName = @TargetEntity; PRINT '----------------------------------------------------------'; PRINT '任务结束: ' + CONVERT(VARCHAR, GETDATE(), 120);TargetEntity参数示例
2025年11月26日
58 阅读
0 评论
1 点赞
2025-11-10
【065】客开插件按钮加入界面权限
客开UI插件按钮比如现在有一个出货单的【信用重检】按钮,想要加入界面权限,让顾问同事决定谁不能点这个按钮UI插件代码如下 权限主要代码就是UIControlBuilder.SetButtonAccessKey(btnItem); UIControlBuilder.BuilderToolbarButton最后一个参数不影响界面权限,主要是SQL语句有了这个资源ID,就赋值一下进行对应using Newtonsoft.Json; using System; using UFIDA.U9.SCM.SD.ShipUIModel; using UFSoft.UBF.UI.ControlModel; using UFSoft.UBF.UI.Engine.Builder; using UFSoft.UBF.UI.IView; using UFSoft.UBF.UI.WebControlAdapter; namespace UFIDA.U9.SCI.QXUIPlugIn { public class ShipMainUIFormUIPlugIn : UFSoft.UBF.UI.Custom.ExtendedPartBase { private ShipMainUIFormWebPart _part = null; private void SetStatusMsg(string value) { _part.CurrentSessionState["StatusMsg"] = value; } public override void AfterRender(IPart Part, EventArgs args) { base.AfterRender(Part, args); if (Part is ShipMainUIFormWebPart && Part.CurrentSessionState.ContainsKey("StatusMsg")) { string statusMsg = Part.CurrentSessionState["StatusMsg"]?.ToString(); Part.CurrentSessionState.Remove("StatusMsg"); if (!string.IsNullOrEmpty(statusMsg)) { Part.ShowWindowStatus(statusMsg, true); } } } public override void AfterInit(IPart Part, EventArgs args) { base.AfterInit(Part, args); _part = Part as ShipMainUIFormWebPart; if (_part == null) return; // 操作 增加【信用重检】按钮 // 实例化按钮 IUFButton btnItem = new UFWebButtonAdapter(); // 找到工具栏控件 IUFToolbar toolbar = (IUFToolbar)_part.GetUFControlByName(_part.TopLevelContainer, "Toolbar1"); // 将按钮添加到工具栏 btnItem = UIControlBuilder.BuilderToolbarButton(toolbar, "True", "BtnRecheckCreditLimit", "True", "True", 70, 28, "22", "", true, false, "", "", "6C3C553C-B981-8CE6-57AB-3EFB9FB517D7"); UIControlBuilder.SetButtonAccessKey(btnItem); ((UFWebToolbarAdapter)toolbar).Items.Add(btnItem as System.Web.UI.WebControls.WebControl); btnItem.Text = "信用重检"; btnItem.UIModel = _part.Model.ElementID; btnItem.Action = "RecheckCreditLimitClick"; btnItem.UFTabIndex = 48; btnItem.AutoPostBack = true; // 绑定按钮事件 btnItem.Click += new EventHandler(BtnRecheckCreditLimit_Click); } private void BtnRecheckCreditLimit_Click(object sender, EventArgs e) { if (_part == null) return; _part.Model.ClearErrorMessage(); _part.DataCollect(); var record = _part.Model.Ship.FocusedRecord; if (record == null || record.ID <= 0 || !record.Status.HasValue) return; if (record.Status.Value == 1) return; QXPubBP.JsonOper.Proxy.CommonProxy proxy = new QXPubBP.JsonOper.Proxy.CommonProxy { OptType = "RecheckCreditLimit", JsonData = JsonConvert.SerializeObject(new { ID = record.ID, UIVersion = record.SysVersion }) }; var res = proxy.Do(); _part.Action.NavigateAction.Refresh(null); SetStatusMsg($"单号:{record.DocNo},信用重检完成{(string.IsNullOrEmpty(res.Msg) ? "!" : ":" + res.Msg)}"); } } }SQL语句写入界面权限UI插件的BuilderToolbarButton方法的第三个参数就是@BtnCtrlName 最后一个参数就是@BtnGuid btnItem.Action就是@BtnActionName(一定要赋值,为''会关联出来系统一些别的动作) @BtnDisplayName就是显示在界面权限的动作名,跟btnItem.Text保持一致 DECLARE @parentPartFullName NVARCHAR(255)='UFIDA.U9.SCM.SD.ShipUIModel.ShipMainUIFormWebPart' -- UI插件配置的parentPartFullName declare @SNIndex bigint=202511100001 -- 年月日+4位流水 declare @BtnGuid nvarchar(50)='6C3C553C-B981-8CE6-57AB-3EFB9FB517D7' -- UI插件新增按钮给的guid,没有可以虚拟一个,这个不影响权限 DECLARE @BtnCtrlName NVARCHAR(50)='BtnRecheckCreditLimit' -- UI插件按钮的控件名称 DECLARE @BtnActionName NVARCHAR(50)='RecheckCreditLimitClick' -- UI插件按钮的.Action值,代码里面不能给''(需唯一,可以跟@BtnCtrlName一样) DECLARE @BtnDisplayName NVARCHAR(50)='信用重检' -- UI插件按钮的显示名称 DECLARE @UIControlUID NVARCHAR(50) DECLARE @ResourceValueUID NVARCHAR(50) -- 查询旧值,没有就NEWID(防止有些地方的关联是使用的UID,不使用旧值直接换一个NEWID会产生问题) SELECT @UIControlUID=UID FROM UBF_MD_UIControl WHERE ID=@SNIndex+1 IF ISNULL(@UIControlUID,'')='' SET @UIControlUID=NEWID() SELECT @ResourceValueUID=ResourceID FROM UBF_RES_ResourceValue WHERE ResourceName=@BtnGuid AND LanguageName = 'zh-CN' IF ISNULL(@ResourceValueUID,'')='' SET @ResourceValueUID=NEWID() -- 清空旧值 DELETE UBF_MD_UIAction WHERE UID=@BtnGuid DELETE UBF_MD_UIAction_Trl WHERE ResourceName=@BtnGuid DELETE UBF_MD_UIControl WHERE ID=@SNIndex+1 DELETE UBF_MD_UIEvent WHERE ID=@SNIndex+2 DELETE FROM UBF_RES_ResourceValue WHERE ResourceName=@BtnGuid AND LanguageName = 'zh-CN' declare @UIFormID bigint declare @UIFormUID nvarchar(50) declare @UIModel bigint declare @UIModelUID nvarchar(50) select @UIFormID=A.ID,@UIFormUID=A.UID,@UIModelUID=A.[DataSource],@UIModel=A1.ID from UBF_MD_UIForm AS A INNER JOIN UBF_MD_UIModel AS A1 ON A1.UID=A.[DataSource] where A.ClassName=@parentPartFullName -- 动作表 INSERT INTO UBF_MD_UIAction(ID, UID, Name, CreatedBy, CreatedOn, ModifiedBy, ModifiedOn, UIModel, Container, GroupName) VALUES(@SNIndex, @BtnGuid, @BtnActionName, 'SQL', GETDATE(), 'SQL', GETDATE(), @UIModel, @UIModelUID, 'Cust') -- 控件表 INSERT INTO UBF_MD_UIControl(ID, UID, Name, Container, UIForm, ControlType, ParentControl, IsBinding, UIView, UIField, CreatedBy, CreatedOn, ModifiedBy, ModifiedOn, RefType, Visible) VALUES(@SNIndex+1, @UIControlUID, @BtnCtrlName, @UIFormUID, @UIFormID, 'Button', null, 0, null, null, 'SQL', GETDATE(), 'SQL', GETDATE(), null, 1) -- 事件表 INSERT INTO UBF_MD_UIEvent(ID, Name, ActionName, CreatedBy, CreatedOn, ModifiedBy, ModifiedOn, UIControl, Container) VALUES(@SNIndex+2, 'Click', @BtnActionName, 'SQL', GETDATE(), 'SQL', GETDATE(), @SNIndex+1, @UIControlUID) -- 资源表 INSERT INTO UBF_RES_ResourceValue(LanguageName, ResourceName, Status, Description, DisplayName, Type, Help, ComponentID, ParentHelpResourceName, ResourceID, ParentResourceName) VALUES('zh-CN', @BtnGuid, 'T', @BtnDisplayName, @BtnDisplayName, 'UimUIActionInfo', null, @UIModelUID, null, @ResourceValueUID, null)执行后效果效果动图展示
2025年11月10日
61 阅读
0 评论
0 点赞
2025-10-14
【064】使用脚本查询所有实体扩展字段信息
使用脚本查询所有实体扩展字段信息有时候为了对比两个环境的实体扩展字段是否一致 但是U9实体扩展字段列表并不能把字段定义信息拉出来导出 可以使用以下SQL语句分别在两个环境的数据库中执行,在结果集中右键->将结果另存为->保存.csv文件后对比(没有标题) 也可以在结果集全选(Ctrl+A)后->右键->连同标题一起复制->粘贴到Excel表空白处,然后再做对比(可以把标题一起复制出来)select A3.DisplayName [实体] ,CONCAT(IIF(A4.ContextValue='Public','公共段','私有段'), A5.Number) [段序号] ,A6.Name [段名称] ,A7.Name [值集名称] ,A5.SourceAttribute [段数据来源快速设定] ,A9.DisplayName [来源实体] ,A8.AttrExpr [表达式] ,A8.AttrExprDisplayName [表达式名称] ,A8.ConditionExpr [条件表达式] ,A8.ConditionExprDisplayName [条件表达式名称] ,A10.ParaCode [参数名称] ,A10.AttrExpr [表达式] ,A10.AttrExprDisPlayName [表达式名称] from Base_DescFlexFieldDef AS A -- 实体扩展字段 JOIN UBF_MD_Class_Trl AS A3 ON A3.Local_ID=A.EntityType AND A3.SysMLFlag='zh-CN' -- 实体扩展字段.实体 JOIN Base_DescFlexContext AS A4 ON A4.DescFlexFieldDef=A.ID -- 上下文 JOIN Base_DescFlexSegment AS A5 ON A5.DescFlexContext=A4.ID -- 字段定义 JOIN Base_DescFlexSegment_Trl AS A6 ON A6.ID=A5.ID AND A6.SysMLFlag='zh-CN' JOIN Base_ValueSetDef_Trl AS A7 ON A7.ID=A5.ValueSetDef AND A7.SysMLFlag='zh-CN' -- 值集 LEFT JOIN Base_DescSegDataSource AS A8 ON A8.DescFlexSegment=A5.ID -- 段数据来源 LEFT JOIN UBF_MD_Class_Trl AS A9 ON A9.Local_ID=A8.SourceEntity AND A9.SysMLFlag='zh-CN' -- 段数据来源.来源实体 LEFT JOIN Base_DescSegDataSourcePara AS A10 ON A10.DescSegDataSource=A8.ID -- 段数据来源参数 order by A3.DisplayName,IIF(A4.ContextValue='Public',0,1),A5.Number结果另存为连同标题一起复制
2025年10月14日
122 阅读
0 评论
1 点赞
2025-10-13
【063】清除U9表所有弹性域字段数据脚本
清除U9表所有弹性域字段数据脚本有时候还原了另外一个系统的数据补丁到本地或者新库,有一些实体扩展字段的私有段已经有了 想要保留表数据,但重新设计私有段,特写此清空表私有段的脚本,方便清空后删除实体扩展字段的私有段 默认不执行,只生成语句,想要马上执行请更改@ExecuteFlag参数=1 复制整段脚本到SQL工具中执行即可-- ==================================================================== -- 动态清除表的弹性段(DescFlexField)数据的脚本 -- ==================================================================== -- 1. 定义输入参数 DECLARE @TableName NVARCHAR(128) = N'SM_Ship'; -- 【必填】需要清除扩展段数据的表名 DECLARE @ExecuteFlag BIT = 0; -- 【控制】0: 只输出拼接好的SQL语句; 1: 立即执行清除操作 -- 2. 内部变量声明 DECLARE @Prefix NVARCHAR(128); -- 存储查询到的扩展段前缀(如 'DescFlexField') DECLARE @SqlStatement NVARCHAR(MAX); -- 存储最终生成的 UPDATE SQL 语句 DECLARE @i INT; DECLARE @Delimiter NVARCHAR(2) = N''; -- 3. 动态查询表的扩展段前缀 -- 使用递归 CTE 查找当前表及其所有父类的 ID With CR As ( -- 锚定成员:从指定的表名开始查找 Class ID Select l.ID as ID From UBF_MD_Class as l Where L.DefaultTableName = @TableName Union All -- 递归成员:查找父类的 Class ID Select P.MD_ParentClass_ID as ID From UBF_MD_Class as P Inner Join CR ON P.ID=CR.ID -- 排除无效的父类 ID (通常为 NULL 或 '0000...' GUID) Where ISNULL(P.MD_ParentClass_ID, '00000000-0000-0000-0000-000000000000') != '00000000-0000-0000-0000-000000000000' ) -- 查找与 Class ID 关联且 DataTypeID 匹配的属性名称作为前缀 -- DataTypeID = '8FF5F5AE-FB0C-4E6E-B1C1-6A5FA355938A' 假设是扩展段数据类型的唯一标识 Select TOP 1 @Prefix = l.Name From UBF_MD_Attribute as l Inner Join CR on l.MD_Class_ID = CR.ID Where l.DataTypeID = '8FF5F5AE-FB0C-4E6E-B1C1-6A5FA355938A'; -- 4. 检查是否找到扩展段前缀 IF @Prefix IS NULL BEGIN PRINT N'错误:未在表 ' + QUOTENAME(@TableName) + N' 及其父类中找到匹配的扩展段前缀 (DataTypeID=''8FF5F5AE-FB0C-4E6E-B1C1-6A5FA355938A'')。'; RETURN; END ELSE BEGIN PRINT N'已找到扩展段前缀:' + @Prefix; PRINT N'开始生成清除 SQL 语句...'; END -- 5. 拼接 UPDATE SQL 语句 SET @SqlStatement = N'UPDATE ' + QUOTENAME(@TableName) + N' SET '; SET @Delimiter = N''; -- 5.1 拼接私有段 (_PrivateDescSeg1 到 _PrivateDescSeg30) SET @i = 1; WHILE @i <= 30 BEGIN SET @SqlStatement = @SqlStatement + @Delimiter + @Prefix + N'_PrivateDescSeg' + CAST(@i AS NVARCHAR(2)) + N'=NULL'; SET @Delimiter = N', '; SET @i = @i + 1; END -- 5.2 拼接公共段 (_PubDescSeg1 到 _PubDescSeg50) SET @i = 1; WHILE @i <= 50 BEGIN SET @SqlStatement = @SqlStatement + @Delimiter + @Prefix + N'_PubDescSeg' + CAST(@i AS NVARCHAR(2)) + N'=NULL'; SET @Delimiter = N', '; SET @i = @i + 1; END -- 5.3 结束 SQL 语句 SET @SqlStatement = @SqlStatement + N';'; -- 6. 根据 @ExecuteFlag 执行或输出 SQL 语句 IF @ExecuteFlag = 1 BEGIN PRINT N'------------------------------------------------------------'; PRINT N'【警告】@ExecuteFlag = 1,将立即执行以下 SQL 语句!'; PRINT N'------------------------------------------------------------'; -- 输出即将执行的语句 PRINT @SqlStatement; -- 执行动态 SQL EXEC sp_executesql @SqlStatement; -- 检查执行结果 IF @@ERROR = 0 PRINT N'SQL 语句执行成功。受影响行数:' + CAST(@@ROWCOUNT AS NVARCHAR(10)); ELSE PRINT N'SQL 语句执行失败,请检查错误信息。'; END ELSE BEGIN PRINT N'------------------------------------------------------------'; PRINT N'【只输出模式】@ExecuteFlag = 0,SQL 语句已生成,但未执行:'; PRINT N'------------------------------------------------------------'; SELECT @SqlStatement AS Generated_Clear_SQL; END(附)删除所有个性化模板UBF_PersonalizationPerorg 按组织分配的个性化模板 UBF_PersonalizationPerRole 按角色分配的个性化模板 UBF_PersonalizationPerUser 按用户分配的个性化模板truncate table [dbo].[UBF_PersonalizationPerorg] truncate table [dbo].[UBF_PersonalizationPerRole] truncate table [dbo].[UBF_PersonalizationPerUser] truncate table [dbo].[aspnet_PersonalizationPerUser]
2025年10月13日
25 阅读
0 评论
0 点赞
2025-08-26
【062】为指定用户和报表批量设置默认的查询方案
脚本 - 以序时账为例UBF_MD_ASRPT_UserCase表是查询方案 UBF_MD_ASRPT_UserCaseInfo表是默认查询方案 @OrgCode参数设置后,只改变设置组织的默认查询方案-- ================================================================================= -- 脚本说明:为指定用户和报表,批量设置默认的查询方案。 -- ================================================================================= SET NOCOUNT ON; -- 关闭返回受影响行数的消息,提升性能 BEGIN TRANSACTION; -- 开始事务 BEGIN TRY -- ============================================================================= -- 参数配置区域 -- ============================================================================= DECLARE @UserCode nvarchar(50) = 'demo'; -- 要设置默认查询方案的【用户编码】 DECLARE @ReportID nvarchar(50) = '1c76c192-36bc-4d1f-9efb-5d8081987b5e'; -- UBF登录后,在报表搜索功能中右键报表详情可看到【报表ID】 -- select ID from UBF_MD_ASRPT_Category where cName='明细账' -- 通过语句可以查询出@ReportID,但是明细账这种会有两个,就只能是看UBF报表了 DECLARE @CName nvarchar(50) = N'序时账-存档-BIP采集'; -- 要设置为默认的【查询方案名称】 -- 【组织编码】设置 (关键参数) -- 1. 设置为 '101,888' 这样用逗号分隔的字符串,则只处理指定的组织。 -- 2. 设置为 NULL 或 '',则处理该用户有权限的【所有】组织。 DECLARE @OrgCode nvarchar(max)-- = '101,888'; -- ============================================================================= -- 内部变量声明与赋值 -- ============================================================================= DECLARE @User bigint; DECLARE @UserCase uniqueidentifier; -- 根据用户编码获取用户ID SELECT @User = ID FROM Base_User WHERE Code = @UserCode; IF @User IS NULL BEGIN THROW 50001, N'指定的用户编码不存在,请检查 @UserCode 参数。', 1; END -- 根据方案名称和报表ID获取方案ID SELECT @UserCase = ID FROM UBF_MD_ASRPT_UserCase WHERE uReportID = @ReportID AND cName = @CName; IF @UserCase IS NULL BEGIN THROW 50002, N'指定的查询方案名称不存在,请检查 @CName 和 @ReportID 参数。', 1; END -- ============================================================================= -- 脚本核心逻辑 -- ============================================================================= -- 创建临时表,用于存放本次需要处理的组织 IF OBJECT_ID('tempdb..#userOrg') IS NOT NULL DROP TABLE #userOrg; CREATE TABLE #userOrg(Org BIGINT); -- 根据 @OrgCode 的值,决定是加载所有组织还是指定组织 IF @OrgCode IS NOT NULL AND LTRIM(RTRIM(@OrgCode)) <> '' BEGIN -- 【场景1】@OrgCode 有值,加载指定的组织 (兼容低版本SQL Server的XML拆分方法) DECLARE @OrgCodeXml XML; -- 将逗号分隔的字符串转换为XML格式,例如 '101,888' -> <o>101</o><o>888</o> SET @OrgCodeXml = CAST(('<o>' + REPLACE(@OrgCode, ',', '</o><o>') + '</o>') AS XML); INSERT INTO #userOrg(Org) SELECT A.Org FROM Base_UserOrg A INNER JOIN Base_Organization A1 ON A1.ID = A.Org WHERE A.[User] = @User AND A1.Code IN ( -- 使用XQuery从XML中提取每个节点的值,实现字符串到行的转换 SELECT T.c.value('.', 'NVARCHAR(100)') FROM @OrgCodeXml.nodes('/o') T(c) ); END ELSE BEGIN -- 【场景2】@OrgCode 为空,加载用户有权限的所有组织 INSERT INTO #userOrg(Org) SELECT Org FROM Base_UserOrg WHERE [User] = @User; END -- 创建临时表,查询该用户在此报表下,目前已有的所有默认查询方案设置 IF OBJECT_ID('tempdb..#userCaseDef') IS NOT NULL DROP TABLE #userCaseDef; SELECT A.ID AS UserCase, A.cName, A1.ID AS UserCaseInfo, A2.ID AS Org, A2.Code AS OrgCode INTO #userCaseDef FROM UBF_MD_ASRPT_UserCase A LEFT JOIN UBF_MD_ASRPT_UserCaseInfo A1 ON A.ID = A1.uReportCaseID LEFT JOIN Base_Organization A2 ON A2.ID = A1.lOrgID WHERE A.uReportID = @ReportID AND A1.lUserID = @User; -- 从待处理的组织列表(#userOrg)中,移除已经将【目标方案】设为默认的组织,避免重复插入 DELETE FROM #userOrg WHERE Org IN (SELECT Org FROM #userCaseDef WHERE cName = @CName); -- 对于待处理组织中,如果它们之前设置了【其他方案】为默认,则先删除这些旧的设置 DELETE FROM UBF_MD_ASRPT_UserCaseInfo WHERE ID IN ( SELECT ud.UserCaseInfo FROM #userCaseDef ud INNER JOIN #userOrg uo ON ud.Org = uo.Org WHERE ud.cName != @CName ); -- 为剩余的待处理组织,批量插入新的默认查询方案设置 INSERT INTO UBF_MD_ASRPT_UserCaseInfo(uReportCaseID, iUserCaseType, lUserID, lOrgID, cCreateBy, dCreateDate, cModifyBy, dModifyDate) SELECT @UserCase, 6, -- iUserCaseType=6 查看U9系统插入的就是6 @User, org.Org, 'System', -- 创建人 GETDATE(), -- 创建日期 'System', -- 修改人 GETDATE() -- 修改日期 FROM #userOrg AS org; -- 清理临时表 DROP TABLE #userOrg; DROP TABLE #userCaseDef; COMMIT TRANSACTION; -- 所有操作成功,提交事务 PRINT '默认查询方案设置成功!'; END TRY BEGIN CATCH -- 如果 TRY 块中发生任何错误,则执行此处的代码 IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION; -- 回滚所有未提交的更改 -- 打印并重新抛出错误信息,以便调用者或用户能够看到详细的错误原因 PRINT N'发生错误,操作已取消,所有更改均已回滚。'; THROW; END CATCH SET NOCOUNT OFF;报表ID查看 - 以序时账为例登录UBF报表,搜索序时账 选中要查看的报表,右键弹出菜单后,点击详情 详情弹出的窗口总,标识符(ID)下面的文本框内就是报表ID
2025年08月26日
96 阅读
0 评论
0 点赞
1
2
...
8