首页
喃喃低语
Search
1
【038】U9私有扩展字段枚举的多语言表的SQL更新
70 阅读
2
【023】新增客开参数设置
68 阅读
3
【051】获取U9当前库所有枚举信息视图
60 阅读
4
平台领域应用合集 - U9HUB
58 阅读
5
【062】为指定用户和报表批量设置默认的查询方案
55 阅读
用友U9
登录
Search
竹秋廿九
累计撰写
70
篇文章
累计收到
2
条评论
首页
栏目
用友U9
页面
喃喃低语
搜索到
70
篇与
用友U9
的结果
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日
1 阅读
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日
4 阅读
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日
21 阅读
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日
16 阅读
0 评论
1 点赞
2025-11-10
【065】客开插件按钮加入界面权限
客开UI插件按钮比如现在有一个出货单的【信用重检】按钮,想要加入界面权限,让顾问同事决定谁不能点这个按钮UI插件代码如下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日
27 阅读
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日
18 阅读
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日
8 阅读
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日
55 阅读
0 评论
0 点赞
2025-08-20
【061】U9系统报表输出
本文以总账 - 明细账为例,其余系统报表大同小异 原理就是系统报表导出的URL其实在加载报表之后会在js里面,名为ExportUrlBase的属性 可以自己新窗口打开明细账报表加载数据之后,右键查看源代码,搜索ExportUrlBase,看后面的那一串URL值 对比一下自己点击导出->PDF或者Excel,会发现弹出来的URL地址就是ExportUrlBase属性的值拼接上了PDF或者EXCELOPENXML 因为我的程序只是需要这两种格式,所以也只是适配了这两种,看懂流程跟程序之后可以自行增加UI插件改变查询方案条件新增查询方案先给需要UI插件修改的条件给一个默认值(更方便UI插件直接修改) 比如示例:给账簿、会计期间设置默认条件(因为获取报表输出主要还是按组织+会计月)获取报表URL明细账报表界面新窗口打开可以拿到一串URL http://localhost/U9/erp/display.aspx?lnk=FI.GL.Process.Rpt.DetailsRpt&sId=3002nid&mId=1001001289685483&__curOId=1001008170100181&newopen=true&__dbg=true 去掉我们不需要的,留下 http://localhost/U9/erp/display.aspx?lnk=FI.GL.Process.Rpt.DetailsRpt&sId=3002&newopen=true (可以另起一个标签页验证能不能打开)右键查看页面源代码(附)附:查看到的ExportUrlBase,就是导出的URL增加自定义条件通过UI插件改变上面默认查询方案的三个条件UI插件配置示例WebPartExtend_ReportFilter.config<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="WebPartExtend" type="UFSoft.UBF.UI.Custom.ExtendedPartSection, UFSoft.UBF.UI.FormProcess" /> </configSections> <WebPartExtend> <!-- 总账-明细账 --> <ExtendedPart parentPartFullName="UFIDA.U9.GL.DetailsRptUI.DetailsRptUIFormWebPart" extendedPartFullName="UFIDA.U9.Cust.XXXX.UIPlugIn.DetailsRptUIPlugIn" extendedPartAssemblyName="UFIDA.U9.Cust.XXXX.UIPlugIn.dll"> </ExtendedPart> </WebPartExtend> </configuration>UI插件代码DetailsRptUIPlugIn.cs ReportID自己去UBF查看,或者调试一下代码,监听变量Part.CurrentState["ReportID"]的值using System; using System.Collections.Generic; using UFIDA.U9.GL.DetailsRptUI; using UFIDA.UBF.Query.CaseModel; using UFIDA.UBF.Report.App.UI; using UFSoft.UBF.Report.Filter.FilterModel; using UFSoft.UBF.UI.IView; using UFSoft.UBF.Util.Context; namespace UFIDA.U9.Cust.XXXX.UIPlugIn { /// <summary> /// 明细账 /// </summary> public class DetailsRptUIPlugIn : UFSoft.UBF.UI.Custom.ExtendedPartBase { private DetailsRptUIFormWebPart _part; // Part.CurrentState["ReportID"] private readonly string ReportID = "45b4acd1-f251-4140-b030-9d506d6da095"; private Case myCase = null; public override void AfterInit(IPart Part, EventArgs args) { base.AfterInit(Part, args); _part = Part as DetailsRptUIFormWebPart; if (_part != null) { SetMyCase(); } } public override void AfterDataLoad(IPart Part) { base.AfterDataLoad(Part); _part = Part as DetailsRptUIFormWebPart; if (_part != null) { ChangeUserCaseFilter(); } } private void SetMyCase() { string caseName = _part.NameValues["CaseName"]?.ToString(); if (string.IsNullOrEmpty(caseName)) return; myCase = Common.Utils.LoadAndEnsureDefaultCase(ReportID, caseName); } private void ChangeUserCaseFilter() { if (myCase != null) { Common.Utils.UpdateUserCaseDisplayNameByCurrentLanguage(_part.Action.CurrentState[ReportCommonAction.UserCaseDefineStateName] as CaseDefine, myCase); // 账簿(必须条件,不可能为空) FilterValue fvSOB = myCase.FilterValues.GetObjectByName("SOB_Code"); string vSOB = Common.Utils.GetMainSOBID(ReportAppService.GetLoginOrgID()); fvSOB.SetValue(0, vSOB); // 期间范围(必须条件,不可能为空) 3指定期间 FilterValue fvPeriodRange = myCase.FilterValues.GetObjectByName("PeriodRange"); if (fvPeriodRange.GetValue() != "3") { fvPeriodRange.SetValue(0, "3"); } // 记账期间 FilterValue fvAccountingPeriod = myCase.FilterValues.GetObjectByName("AccountingPeriod"); string v1 = _part.NameValues["AccountingPeriodStart"]?.ToString(); string v2 = _part.NameValues["AccountingPeriodEnd"]?.ToString(); if (fvAccountingPeriod == null) { fvAccountingPeriod = Common.Utils.CreatePeriod("AccountingPeriod", myCase.FilterValues.Count + 1, UFSoft.UBF.Report.Filter.enuOperatorListType.Between, new List<string>() { v1, v2 }); myCase.FilterValues.Add(fvAccountingPeriod); } else if (fvAccountingPeriod.RelationOperator == UFSoft.UBF.Report.Filter.enuOperatorListType.Between) { fvAccountingPeriod.SetValue(0, v1); fvAccountingPeriod.SetValue(1, v2); } else if (fvAccountingPeriod.RelationOperator == UFSoft.UBF.Report.Filter.enuOperatorListType.Equal) { if (v1 == v2) fvAccountingPeriod.SetValue(0, v1); } Common.Utils.SetCaseModelSession(this._part.Action, ReportID); _part.Action.CurrentState["IsCaseChange"] = true; _part.Action.CurrentState[ReportCommonAction.UserCaseStateName] = myCase; } } } }UI插件Common.Utilsusing System; using System.Collections.Generic; using System.Reflection; using UFIDA.U9.UI.PDHelper; using UFIDA.UBF.Query.CaseModel; using UFIDA.UBF.Report.App.UI; using UFIDA.UBF.Report.App.UI.CaseManager; using UFIDA.UBF.Report.App.UI.Interface; using UFIDA.UBF.Report.App.UI.ProcessStrategy; using UFSoft.UBF.Report.Filter; using UFSoft.UBF.Report.Filter.FilterModel; using UFSoft.UBF.UI; using UFSoft.UBF.UI.ActionProcess; using UFSoft.UBF.UI.FormProcess; using UFSoft.UBF.UI.IView; using UFSoft.UBF.Util.Context; using UFSoft.UBF.Util.DataAccess; namespace UFIDA.U9.Cust.XXXX.UIPlugIn.Common { public class Utils { /// <summary> /// 获取组织的主账簿ID /// </summary> /// <param name="orgID">组织ID</param> /// <returns></returns> public static string GetMainSOBID(string orgID) { string sql = "select ID from Base_SetofBooks where SOBType=0 and Org=" + orgID; DataAccessor.RunSQL(DataAccessor.GetConn(), sql, null, out object sobobj); return sobobj?.ToString(); } /// <summary> /// ReportCommonAction.UpdateUserCaseDisplayNameByCurrentLanguage方法调用 /// </summary> /// <param name="caseDefine"></param> /// <param name="userCase"></param> public static void UpdateUserCaseDisplayNameByCurrentLanguage(CaseDefine caseDefine, Case userCase) { // 获取类型 Type type = typeof(ReportCommonAction); // 定义参数类型数组 Type[] parameterTypes = new Type[] { typeof(CaseDefine), typeof(Case) }; // 获取方法信息 MethodInfo methodInfo = type.GetMethod("UpdateUserCaseDisplayNameByCurrentLanguage", BindingFlags.NonPublic | BindingFlags.Static, null, parameterTypes, null); if (methodInfo == null) throw new Exception("ReportCommonAction静态方法 UpdateUserCaseDisplayNameByCurrentLanguage 未找到"); // 调用方法,传递参数 methodInfo.Invoke(null, new object[] { caseDefine, userCase }); } /// <summary> /// ReportCommonAction.SetCaseModelSession方法调用 /// </summary> /// <param name="action"></param> /// <param name="reportID">原方法里面这个值是从args获取的,UI插件没有,直接指定</param> public static void SetCaseModelSession(BaseAction action, string reportID) { CaseModel caseModel = new CaseModel(); caseModel.Case = (action.CurrentState[ReportCommonAction.UserCaseStateName] as Case); ReportProcessStrategy reportProcessStrategy = action.CurrentState[ReportAppService.GetReportProcessStrategySessionName()] as ReportProcessStrategy; if (reportProcessStrategy != null) { try { caseModel.CaseDefine = reportProcessStrategy.ProcessCaseDefine(caseModel.Case, action.CurrentState[ReportCommonAction.UserCaseDefineStateName] as CaseDefine); ReportDrillHelper.AddExportExcelControlParameter(caseModel.CaseDefine, false); string text = reportProcessStrategy.VerifyUserCaseByCaseDefine(caseModel.CaseDefine, caseModel.Case); if (text.Length > 0) { action.CurrentState["AdjustUserCaseDisplayInfo"] = text; } } catch (Exception ex) { caseModel.CaseDefine = (action.CurrentState[ReportCommonAction.UserCaseDefineStateName] as CaseDefine); } } else { caseModel.CaseDefine = (action.CurrentState[ReportCommonAction.UserCaseDefineStateName] as CaseDefine); } caseModel.QryModelID = new Guid(reportID); action.CurrentState[ReportAppService.GetInputCaseModelSessionName()] = caseModel; } /// <summary> /// 创建会计期间条件项 /// </summary> /// <param name="name">条件名</param> /// <param name="itemID">条件项ID</param> /// <param name="relationOperator">关系操作符</param> /// <returns></returns> public static FilterValue CreatePeriod(string name, int itemID, enuOperatorListType relationOperator, List<string> values) { FilterValue filterValue = new FilterValue(); filterValue.Name = name; filterValue.FilterItemID = itemID; filterValue.RelationOperator = relationOperator; filterValue.PageType = enuPageOfInputFilterValueType.basicPage; filterValue.ReferenceType = enuReferenceType.reference; filterValue.LogicOperator = enuOperatorListType.And; filterValue.ValueType = enumFilterValueType.InputValue; filterValue.Values = new ValueContext(); filterValue.Values.Labels.Add(name); filterValue.Values.Values = values; return filterValue; } /// <summary> /// 创建会计年度条件项,等于 /// </summary> /// <param name="name">条件名</param> /// <param name="itemID">条件项ID</param> /// <param name="v1"></param> /// <returns></returns> public static FilterValue CreateAccountingYearEqual(string name, int itemID, string v1) { FilterValue filterValue = new FilterValue(); filterValue.Name = name; filterValue.FilterItemID = itemID; filterValue.RelationOperator = enuOperatorListType.Equal; filterValue.PageType = enuPageOfInputFilterValueType.basicPage; filterValue.ReferenceType = enuReferenceType.reference; filterValue.LogicOperator = enuOperatorListType.And; filterValue.ValueType = enumFilterValueType.InputValue; filterValue.Values = new ValueContext(); filterValue.Values.Labels.Add(name); filterValue.Values.Values = new List<string>() { v1 }; return filterValue; } /// <summary> /// 获取当前用户报表的指定查询方案,并确保有一个默认的查询方案 /// 如果当前用户报表没有默认查询方案,则设置指定查询方案为默认查询方案 /// </summary> /// <param name="reportID">报表ID</param> /// <param name="caseName">查询方案名</param> /// <returns></returns> public static Case LoadAndEnsureDefaultCase(string reportID, string caseName) { Case myCase = null; string userid = PlatformContext.Current.UserID + "#" + ReportAppService.GetLoginOrgID(); Case defCase = ReportAppService.LoadDefalutCase(reportID, userid); // 默认的方案就是要的查询方案 if (defCase != null && defCase.BasicInfo.Title == caseName) { myCase = defCase; } else { myCase = ReportAppService.LoadCase(reportID, userid, caseName); // 这个查询方案不是当前用户创建的(别的用户共享的查询方案) // 那就只能是从报表所有的查询方案中匹配 if (myCase == null) { ReportCaseManager caseManager = new ReportCaseManager(); var cases = caseManager.LoadCases(reportID); // 报表所有查询方案 foreach (Case item in cases) { if (item.BasicInfo.Title == caseName) { myCase = item; break; } } } if (myCase == null) throw new Exception($"未找到名称【{caseName}】的查询方案!"); if (defCase == null) { // 没有默认查询方案,把当前找到的查询方案设置成默认查询方案 SetMyDefaultCase(myCase); } } return myCase; } /// <summary> /// 设置为默认查询方案 /// </summary> /// <param name="case"></param> public static void SetMyDefaultCase(Case @case) { UserCaseInfo userCaseInfo = new UserCaseInfo(); userCaseInfo.CaseID = @case.BasicInfo.ReportCaseID; userCaseInfo.OrgID = Convert.ToInt64(ReportAppService.GetLoginOrgID()); userCaseInfo.UserID = Convert.ToInt64(PlatformContext.Current.UserID); userCaseInfo.UserCaseType = UserCaseType.Default; ICaseManager caseManager = new ReportCaseManager(); caseManager.SetMyDefaultUserCaseInfo(userCaseInfo); } } }报表SSO登录U9系统自带的auotlogin.aspx拼接的return_url会是以菜单的形式打开,获取iframe难度大 不如仿照SSO登录,直接写一个ReportLogin.aspx,登录后Response.Redirect() 以下为示例的ReportLogin.aspx,可以根据情况个性化ReportLogin.aspxReportLogin.aspx放到C:\yonyou\U9V60\Portal\api\v1目录下 _internalSecureToken是内网请求,所以直接定义了一个固定的值 如果是外网可访问,建议还是生成式token更好<%@ Page Language="C#" %> <%@ Import Namespace="UFSoft.UBF.UI.Portal.Components" %> <%@ Import Namespace="UFSoft.UBF.UI.WebControlAdapter" %> <%@ Import Namespace="UFSoft.UBF.UI.Portal" %> <%@ Import Namespace="System.Web.UI" %> <%@ Import Namespace="System.Web.Configuration" %> <%@ Import Namespace="System.Text.RegularExpressions" %> <%@ Import Namespace="System.Web.Security" %> <%@ Import Namespace="System.Text" %> <%@ Import Namespace="UFSoft.UBF.Util.Context" %> <%@ Import Namespace="UFSoft.UBF.UI" %> <%@ Import Namespace="UFSoft.UBF.UI.IProvider" %> <%@ Import Namespace="UFSoft.UBF.MVC" %> <%@ Import Namespace="System.Net" %> <%@ Import Namespace="System.IO" %> <%@ Import Namespace="System.Collections.Specialized" %> <%@ Import Namespace="System.Web" %> <script runat="server"> private const string _internalSecureToken = "2FFEAB291D1E7EAF76E94EBE93249C9D"; private string _username; private string _enterpriseId; private string _orgID; private string _enterpriseName = string.Empty; protected void Page_Load(object sender,EventArgs e) { Login(); } private void Login() { string receivedToken = Request.QueryString["securetoken"]; if (string.IsNullOrEmpty(receivedToken) || receivedToken != _internalSecureToken) { Response.StatusCode = 403; // Forbidden Response.Write("错误: 禁止访问。"); Response.End(); return; } _username = Request.QueryString["usercode"]; _enterpriseId = Request.QueryString["entcode"]; _orgID = Request.QueryString["org"]; if (string.IsNullOrEmpty(_username) || string.IsNullOrEmpty(_enterpriseId) || string.IsNullOrEmpty(_orgID)) { Response.StatusCode = 400; // Bad Request Response.Write("错误: 缺少 username, enterpriseId, 或 orgID 参数。"); Response.End(); return; } string reportUrl = Request.QueryString["reporttype"]; if (string.IsNullOrEmpty(reportUrl)) { Response.StatusCode = 400; Response.Write("错误: 缺少必需的 reporttype 参数。"); Response.End(); return; } CSUser user = new CSUser(); user.EnterpriseID = _enterpriseId; user.OrgId = _orgID; user.Username = _username; user.LoginDateTime = DateTime.Now; user.IP = this.Page.Request.UserHostAddress; user.EnterpriseName = _enterpriseName; user.UICulture = "zh-CN"; bool isToken = true; UFSoft.UBF.MVC.Helper.UserAuthHelper.TransferUser(user, "", "", true, isToken); try { UserCredential credential = UFSoft.UBF.UI.Portal.UserManagement.ValidUser(user,isToken); if (credential.IsAuthenticated == LoginUserStatus.Success) { CSContext.Current["AppSettings"] = WebConfigurationManager.AppSettings; CSContext.Current["ShowControlsTooltip"] = WebConfigurationManager.AppSettings["ShowControlsTooltip"]; CSContext.Current["ReferenceDisableClientCache"] = WebConfigurationManager.AppSettings["ReferenceDisableClientCache"]; CSContext.Current["quickmenusCache"] = null; CSContext.Current["_PassWordStrategy"] = credential.PassWordStrategy; CSContext.Current["_SearchSessionID"] = Guid.NewGuid().ToString(); CSContext.Current.OperationDate = DateTime.Now.Date; CSContext.Current.UiDefaultCulture = UFSoft.UBF.UI.Portal.UserManagement.Provider.getDefaultLanByOrg(long.Parse(_orgID)); if (string.IsNullOrEmpty(CSContext.Current.User.OrgName)) { CSOrganization csOrg = UFSoft.UBF.UI.Portal.UserManagement.getOrgByID(CSContext.Current.User.OrgId.ToString()); CSContext.Current.OrgCode = csOrg.Code; CSContext.Current.User.OrgName = csOrg.Name; } else { CSContext.Current.OrgCode = UFSoft.UBF.UI.Portal.UserManagement.getOrgCode(user.OrgId.ToString()); } SetDefaultTheme(user.OrgId.ToString()); SaveCookies(user); WriteCurrentContext(); UFSoft.UBF.UI.Portal.UserManagement.UserLogoned(user.Username, CSContext.Current.UserName); NameValueCollection incomingParams = new NameValueCollection(Request.QueryString); incomingParams.Remove("usercode"); incomingParams.Remove("username"); incomingParams.Remove("enterpriseId"); incomingParams.Remove("entcode"); incomingParams.Remove("orgID"); incomingParams.Remove("org"); incomingParams.Remove("securetoken"); incomingParams.Remove("reporttype"); UriBuilder uriBuilder = new UriBuilder(reportUrl); NameValueCollection baseParams = HttpUtility.ParseQueryString(uriBuilder.Query); baseParams.Add(incomingParams); uriBuilder.Query = baseParams.ToString(); string finalTargetUrl = uriBuilder.ToString(); Response.Redirect(finalTargetUrl); } } catch (Exception) { throw; } } private void WriteCurrentContext() { using (new SystemWritablePolicy()) { PlatformContext.Current.OrgID = CSContext.Current.User.OrgId.ToString(); PlatformContext.Current.UserID = CSContext.Current.User.UserId.ToString(); PlatformContext.Current.UserCode = CSContext.Current.User.UserCode as string; PlatformContext.Current.UserName = CSContext.Current.User.Username; PlatformContext.Current.UserClientIP = CSContext.Current.User.IP; } } private int CookiePeriodPolicy = 7; private void SaveCookie(string key, string value) { this.Page.Response.Cookies[key].Value = Convert.ToBase64String(Encoding.Unicode.GetBytes(value)); this.Page.Response.Cookies[key].Expires = DateTime.Now.AddDays((double)this.CookiePeriodPolicy); } private void SaveCookies(CSUser csUser) { SaveCookie("EnterpriseId", _enterpriseId); SaveCookie("SelectedOrg", csUser.OrgId as string); SaveCookie("SelectedLan", csUser.UICulture); SaveCookie("userName", csUser.Username); } private string SetDefaultTheme(string orgId) { string orgDefaultThemeByOrgID = UFSoft.UBF.UI.Portal.UserManagement.GetOrgDefaultThemeByOrgID(Convert.ToInt64(orgId)); if (((CSContext.Current != null) && (CSContext.Current.User != null)) && (CSContext.Current.User.Profile != null)) { CSContext.Current.User.Profile.Theme = orgDefaultThemeByOrgID; } return orgDefaultThemeByOrgID; } </script> <html xmlns="http://www.w3.org/1999/xhtml"> <head id="Head1" runat="server"> </head> </html>PuppeteerSharp实现读取html源代码并跳转下载示例新增了一个ashx一般处理程序来使用PuppeteerSharp 思路有了,可以根据自己的习惯用API或者别的方式,总之这里提供出去的URL才是请求报表输出的入口 PuppeteerSharp程序可以使用Nuget安装最新稳定版2.0 直接获取U9报表文件流有些报表可能会很大导致等待时间长,优化一下路径,分为三个ashx文件 DownloadReport.ashx主要进行SSO登录并解析ExportUrlBase后下载到临时目录 DownloadReportFile.ashx负责访问临时目录的文件,提供下载文件服务 DeleteReportFile.ashx负责删除临时目录的文件,提供删除文件服务PuppeteerSharp文件下载到临时目录DownloadReport.ashxExecutablePath = @"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" 这是edge浏览器,如果服务器没有,那就换成别的 p参数里面包含了timespan,避免URL被获取之后重复拿来使用,失效时间就是web.config配置的ExpirationTimeSeconds,以秒为单位 web.config里面PuppeteerTimeoutMinutes是访问报表URL等待的超时时间,因为报表首次访问会有冷启动,加上系统报表查询时间,最好是设置长一点,这里默认30分钟web.config配置在下面using Newtonsoft.Json; using PuppeteerSharp; using System; using System.Collections.Specialized; using System.Configuration; using System.Diagnostics; using System.IO; using System.Net; using System.Net.Http; using System.Security; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; namespace SHAPIPost { public class DownloadHandler : HttpTaskAsyncHandler { private static readonly string encryptionKey = ConfigurationManager.AppSettings["EncryptionKey"]; private static readonly string encryptionIV = ConfigurationManager.AppSettings["EncryptionIV"]; private static readonly string reportBaseUrl = ConfigurationManager.AppSettings["ReportBaseUrl"]; private static readonly string loginPageBaseUrl = ConfigurationManager.AppSettings["U9LoginBaseUrl"]; private static readonly string timeoutSetting = ConfigurationManager.AppSettings["PuppeteerTimeoutMinutes"]; private static readonly string expirationTimeSeconds = ConfigurationManager.AppSettings["ExpirationTimeSeconds"]; static DownloadHandler() { ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls; } // IHttpAsyncHandler 的 IsReusable 属性,我们用 override 实现 public override bool IsReusable { get { return false; } } // 【核心下载逻辑 - 全新改造版】 public override async Task ProcessRequestAsync(HttpContext context) { var stopwatch = Stopwatch.StartNew(); string requestId = context.Request.QueryString["taskID"]; if (string.IsNullOrEmpty(requestId)) requestId = Guid.NewGuid().ToString("N").Substring(0, 8); ReportLogger.Log(requestId, $"请求开始: {context.Request.Url}"); var Response = context.Response; Response.ContentType = "application/json"; // 响应类型改为JSON try { // --- 1. 认证和参数准备 --- var authResult = AuthenticateAndPrepare(context); string exportType = authResult.ExportType; string fileExtension = ".dat"; if (exportType.Equals("PDF", StringComparison.OrdinalIgnoreCase)) { fileExtension = ".pdf"; } else if (exportType.Equals("EXCELOPENXML", StringComparison.OrdinalIgnoreCase)) { fileExtension = ".xlsx"; } else if (exportType.Equals("CSV", StringComparison.OrdinalIgnoreCase)) { fileExtension = ".csv"; } // 使用GUID确保文件名唯一,避免冲突 string fileName = $"{authResult.Filename}{fileExtension}"; string uniqueFileName = $"{authResult.Filename}_{requestId}{fileExtension}"; // --- 2. 等待报表在服务器端完全生成 --- ReportLogger.Log(requestId, $"开始生成报表: {authResult.BrowserLoginUrl}"); byte[] fileBytes = await GenerateReportBytesAsync(authResult.BrowserLoginUrl, exportType, requestId, context.Server); ReportLogger.Log(requestId, $"报表生成完毕,大小: {fileBytes.Length} 字节。"); // --- 3. 将文件保存到服务器本地目录 --- string directoryPath = context.Server.MapPath("~/ReportFiles"); Directory.CreateDirectory(directoryPath); // 确保目录存在 string savePath = Path.Combine(directoryPath, uniqueFileName); if (File.Exists(savePath)) File.Delete(savePath); File.WriteAllBytes(savePath, fileBytes); ReportLogger.Log(requestId, $"文件已成功保存至磁盘: {savePath}"); // --- 4. 构建可访问的URL和返回结果 --- var appUrl = HttpContext.Current.Request.Url; string baseUrl = $"{appUrl.Scheme}://{appUrl.Authority}{HttpContext.Current.Request.ApplicationPath.TrimEnd('/')}"; string downloadUrl = $"{baseUrl}/DownloadReportFile.ashx?fileName={HttpUtility.UrlEncode(fileName)}&downName={HttpUtility.UrlEncode(uniqueFileName)}"; string delUrl = $"{baseUrl}/DeleteReportFile.ashx?delName={HttpUtility.UrlEncode(uniqueFileName)}"; var result = new { IsSuccess = true, Message = "文件生成成功。", Url = downloadUrl, DelUrl = delUrl }; ReportLogger.Log(requestId, "准备向客户端发送JSON响应。"); Response.Write(JsonConvert.SerializeObject(result)); ReportLogger.Log(requestId, "已向客户端发送JSON响应。"); } catch (Exception ex) { ReportLogger.Log(requestId, $"处理失败: {ex.Message} \n {ex.StackTrace}"); Response.StatusCode = 500; var result = new { IsSuccess = false, Message = "处理请求时发生错误: " + ex.Message }; Response.Write(JsonConvert.SerializeObject(result)); } finally { stopwatch.Stop(); ReportLogger.Log(requestId, $"请求结束,总耗时: {stopwatch.ElapsedMilliseconds} ms"); } } private async Task<byte[]> GenerateReportBytesAsync(string browserLoginUrl, string exportType, string requestId, HttpServerUtility Server) { IBrowser browser = null; try { ReportLogger.Log(requestId, "后台任务开始:启动Puppeteer..."); int timeoutMinutes = 30; if (!string.IsNullOrEmpty(timeoutSetting) && int.TryParse(timeoutSetting, out int parsedTimeout)) { timeoutMinutes = parsedTimeout; } browser = await Puppeteer.LaunchAsync(new LaunchOptions { Headless = true, IgnoreHTTPSErrors = true, ExecutablePath = @"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" }); IBrowserContext incognitoContext = null; try { // 1. 创建一个独立的、一次性的隐身浏览器上下文 incognitoContext = await browser.CreateIncognitoBrowserContextAsync(); ReportLogger.Log(requestId, "后台任务:已创建独立的Incognito上下文。"); // 2. 在这个纯净的上下文中创建一个新页面 var page = await incognitoContext.NewPageAsync(); // 后续所有操作都在这个隔离的页面中进行,不受缓存和Cookie影响 ReportLogger.Log(requestId, $"后台任务:Puppeteer访问SSO登录链接: {browserLoginUrl}"); page.DefaultNavigationTimeout = timeoutMinutes * 60 * 1000; await page.GoToAsync(browserLoginUrl, new NavigationOptions { WaitUntil = new[] { WaitUntilNavigation.Networkidle0 } }); string reportHtml = await page.GetContentAsync(); var regex = new Regex("\"ExportUrlBase\":\"([^\"]*)\""); Match match = regex.Match(reportHtml); if (!match.Success) { string debugDir = Server.MapPath("~/ReportDebug"); Directory.CreateDirectory(debugDir); string timestamp = DateTime.Now.ToString("yyyyMMddHHmmss"); string debugHtmlPath = Path.Combine(debugDir, $"debug_page_{timestamp}.html"); File.WriteAllText(debugHtmlPath, reportHtml); throw new Exception($"无法在目标报表页面中解析到导出链接(ExportUrlBase)。已将调试HTML内容保存至 {Path.GetFileName(debugHtmlPath)}。"); } string exportUrlBase = Regex.Unescape(match.Groups[1].Value); var reportUri = new Uri(page.Url); string finalDownloadUrl = $"{reportUri.Scheme}://{reportUri.Authority}{exportUrlBase}{exportType}"; ReportLogger.Log(requestId, $"后台任务:获取到下载链接,准备下载: {finalDownloadUrl}"); var cookies = await page.GetCookiesAsync(); var cookieContainer = new CookieContainer(); foreach (var cookie in cookies) { cookieContainer.Add(reportUri, new Cookie(cookie.Name, cookie.Value, cookie.Path, cookie.Domain)); } using (var handler = new HttpClientHandler { CookieContainer = cookieContainer }) using (var client = new HttpClient(handler)) { client.Timeout = TimeSpan.FromMinutes(timeoutMinutes); client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"); byte[] fileBytes; // 使用 GetAsync 而不是 GetByteArrayAsync,以便先检查响应头 using (var response = await client.GetAsync(finalDownloadUrl, HttpCompletionOption.ResponseHeadersRead)) { // 检查 Content-Type var contentType = response.Content.Headers.ContentType?.MediaType ?? string.Empty; if (contentType.Equals("text/html", StringComparison.OrdinalIgnoreCase)) { // 如果是HTML,尝试读取内容用于记录日志,然后抛出异常 string errorHtml = await response.Content.ReadAsStringAsync(); ReportLogger.Log(requestId, $"错误:服务器返回了HTML页面,可能因为会话过期。页面内容预览: {errorHtml.Substring(0, Math.Min(500, errorHtml.Length))}"); throw new InvalidDataException("下载失败:服务器返回了HTML错误页面,报表会话可能已过期或服务已重启。"); } response.EnsureSuccessStatusCode(); // 检查是否有其他HTTP错误 // Content-Type看起来正常,现在下载完整的文件内容 fileBytes = await response.Content.ReadAsByteArrayAsync(); } if (fileBytes == null || fileBytes.Length == 0) { throw new Exception("文件下载失败,获取到的文件流为空。"); } ReportLogger.Log(requestId, $"后台任务:文件下载成功,大小: {fileBytes.Length} 字节。"); // 可选,修复文件头 if (exportType.Equals("EXCELOPENXML", StringComparison.OrdinalIgnoreCase)) { ReportLogger.Log(requestId, $"文件原始大小: {fileBytes.Length} 字节。准备进行修复..."); // 调用修复器,获取干净的文件字节流 byte[] repairedFileBytes = ExcelRepairer.RepairExcelFile(fileBytes); ReportLogger.Log(requestId, $"文件修复后大小: {repairedFileBytes.Length} 字节。"); return repairedFileBytes; } return fileBytes; } } finally { // 3. 确保任务结束后,无论成功或失败,都关闭并销毁这个上下文及其所有数据(缓存、Cookie等) if (incognitoContext != null) { await incognitoContext.CloseAsync(); ReportLogger.Log(requestId, "后台任务:Incognito上下文已关闭并清理。"); } } } finally { if (browser != null) { // 使用带超时的关闭方式,防止主浏览器进程卡死 ReportLogger.Log(requestId, "后台任务:准备关闭主Puppeteer浏览器(带10秒超时)..."); var closeTask = browser.CloseAsync(); var timeoutTask = Task.Delay(TimeSpan.FromSeconds(10)); var completedTask = await Task.WhenAny(closeTask, timeoutTask); if (completedTask == closeTask) { ReportLogger.Log(requestId, "后台任务:主Puppeteer浏览器在10秒内成功关闭。"); } else { ReportLogger.Log(requestId, "警告:关闭主Puppeteer浏览器超时(超过10秒),已放弃等待。"); } } } } private AuthResult AuthenticateAndPrepare(HttpContext context) { var Request = context.Request; var routeData = context.Request.RequestContext.RouteData.Values; if (string.IsNullOrEmpty(encryptionKey) || string.IsNullOrEmpty(encryptionIV)) throw new ConfigurationErrorsException("未在配置中找到 EncryptionKey 或 EncryptionIV 设置。"); if (string.IsNullOrEmpty(loginPageBaseUrl)) throw new ConfigurationErrorsException("未在配置中找到 U9LoginBaseUrl 设置。"); string reportTypeKey = routeData["reporttype"] as string; string encryptedPayload = Request.QueryString["p"]; string filename = Request.QueryString["filename"]; string exportType = Request.QueryString["exporttype"] ?? "PDF"; if (string.IsNullOrEmpty(encryptedPayload) || string.IsNullOrEmpty(reportTypeKey)) throw new ArgumentException("缺少必需的 p 或 reporttype 参数。"); if (string.IsNullOrEmpty(filename)) filename = reportTypeKey + "_" + DateTime.Now.ToString("yyyyMMddHHmmss"); string reportTypeLink = ConfigurationManager.AppSettings[reportTypeKey]; if (string.IsNullOrEmpty(reportTypeLink)) throw new ConfigurationErrorsException("未在配置中为 reporttype 键 '" + reportTypeKey + "' 找到对应的link配置。"); string reportUrl = $"{reportBaseUrl}?{reportTypeLink}"; string decryptedPayload = Utils.DecryptString(encryptedPayload, encryptionKey, encryptionIV); if (string.IsNullOrEmpty(decryptedPayload)) throw new SecurityException("凭证解密失败。"); NameValueCollection authParams = HttpUtility.ParseQueryString(decryptedPayload); string userCode = authParams["usercode"], enterpriseId = authParams["entcode"], organizationId = authParams["org"], timestampStr = authParams["timestamp"]; if (string.IsNullOrEmpty(userCode) || string.IsNullOrEmpty(enterpriseId) || string.IsNullOrEmpty(organizationId) || string.IsNullOrEmpty(timestampStr)) { throw new ArgumentException("解密后的凭证信息不完整(缺少用户、企业、组织或时间戳)。"); } if (!Utils.IsUnixTimestamp(timestampStr)) { throw new SecurityException("无效的时间戳格式,请使用Unix时间戳。"); } if (!long.TryParse(timestampStr, out long unixTimestamp)) { throw new SecurityException("无效的时间戳格式。"); } int timeoutSeconds = 60; if (!string.IsNullOrEmpty(timeoutSetting)) { int.TryParse(expirationTimeSeconds, out timeoutSeconds); } DateTime requestTime = Utils.UnixTimestampToDateTime(unixTimestamp); bool isExpired = Utils.IsTimeExpired(requestTime, timeoutSeconds); if (isExpired) { throw new SecurityException("链接已过期。"); } var uriBuilder = new UriBuilder(loginPageBaseUrl); var query = HttpUtility.ParseQueryString(uriBuilder.Query); query["usercode"] = userCode; query["entcode"] = enterpriseId; query["org"] = organizationId; query["reporttype"] = reportUrl; var originalParams = new NameValueCollection(Request.QueryString); originalParams.Remove("p"); originalParams.Remove("reporttype"); originalParams.Remove("filename"); query.Add(originalParams); uriBuilder.Query = query.ToString(); return new AuthResult { BrowserLoginUrl = uriBuilder.ToString(), Filename = filename, ExportType = exportType }; } } internal class AuthResult { public string BrowserLoginUrl { get; set; } public string Filename { get; set; } public string ExportType { get; set; } } } 文件下载服务DownloadReportFile.ashx通过中转服务提供对临时目录的文件下载,比直接提供流更稳定 而且有下载的文件存档,能够更方便调试是哪一步出现错误using System; using System.IO; using System.Web; namespace SHAPIPost { public class DownloadReportFileHandler : IHttpHandler { public void ProcessRequest(HttpContext context) { // --- 1. 根据新的URL结构解析参数 --- // 用户下载时看到的友好文件名,来自 'fileName' 参数 string friendlyFileName = context.Request.QueryString["fileName"]; // 服务器上存储的唯一文件名,来自 'downName' 参数 string serverFileName = context.Request.QueryString["downName"]; // 提供一个回退机制,如果友好的fileName参数缺失,则使用服务器文件名 if (string.IsNullOrWhiteSpace(friendlyFileName)) { friendlyFileName = serverFileName; } // --- 2. 安全性校验 (使用 serverFileName) --- if (string.IsNullOrWhiteSpace(serverFileName)) { context.Response.StatusCode = 400; // Bad Request // 注意:根据新的URL结构,现在应该检查 'downName' context.Response.StatusDescription = "Missing required 'downName' parameter."; context.Response.End(); return; } try { string reportFilesDir = context.Server.MapPath("~/ReportFiles"); // 使用 serverFileName 在磁盘上查找文件 string physicalFilePath = Path.Combine(reportFilesDir, serverFileName); physicalFilePath = Path.GetFullPath(physicalFilePath); if (!physicalFilePath.StartsWith(reportFilesDir, StringComparison.OrdinalIgnoreCase)) { context.Response.StatusCode = 403; // Forbidden context.Response.StatusDescription = "Access to the requested file is forbidden."; context.Response.End(); return; } if (!File.Exists(physicalFilePath)) { context.Response.StatusCode = 404; // Not Found context.Response.StatusDescription = "The requested file was not found."; context.Response.End(); return; } // --- 3. 文件流式传输 (使用 friendlyFileName) --- context.Response.Clear(); context.Response.ContentType = GetMimeType(Path.GetExtension(serverFileName)); // 在 Content-Disposition 中使用 friendlyFileName context.Response.AddHeader("Content-Disposition", "attachment; filename=" + HttpUtility.UrlEncode(friendlyFileName, System.Text.Encoding.UTF8)); context.Response.TransmitFile(physicalFilePath); context.Response.Flush(); } catch (Exception) { context.Response.StatusCode = 500; // Internal Server Error context.Response.StatusDescription = "An error occurred while processing your request."; } finally { context.Response.End(); } } public bool IsReusable => false; private string GetMimeType(string extension) { switch (extension.ToLower()) { case ".pdf": return "application/pdf"; case ".xlsx": return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; case ".xls": return "application/vnd.ms-excel"; case ".csv": return "text/csv"; default: return "application/octet-stream"; } } } }文件删除服务DeleteReportFile.ashx按需调用文件删除服务,比如文件上传到Minio之后的第三方OSS,可以删除本地的留档 也可以在处理文件下载服务之后,配置一个变量控制是否下载后删除,平常都是开启下载后删除,线上有问题的时候,更改配置为关闭下载后删除,方便查看临时目录的原文件是否有问题using Newtonsoft.Json; using System; using System.IO; using System.Web; namespace SHAPIPost { public class DeleteReportFileHandler : IHttpHandler { public void ProcessRequest(HttpContext context) { context.Response.ContentType = "application/json"; try { // 从请求中获取要删除的文件名,推荐使用POST请求体传递 string fileNameToDelete = context.Request.Form["delName"]; if (string.IsNullOrEmpty(fileNameToDelete)) { fileNameToDelete = context.Request.QueryString["delName"]; } if (string.IsNullOrWhiteSpace(fileNameToDelete)) { throw new ArgumentException("缺少 delName 参数。"); } // --- 安全性校验 --- // 1. 获取基础存储目录的物理路径 string reportFilesDir = context.Server.MapPath("~/ReportFiles"); // 2. 组合出要删除文件的完整物理路径 string physicalFileToDelete = Path.Combine(reportFilesDir, fileNameToDelete); // 3. 规范化路径以防止路径遍历攻击 (e.g., "..\web.config") physicalFileToDelete = Path.GetFullPath(physicalFileToDelete); // 4. 【关键】确保要删除的文件确实在我们指定的目录内 if (!physicalFileToDelete.StartsWith(reportFilesDir, StringComparison.OrdinalIgnoreCase)) { throw new System.Security.SecurityException("禁止访问指定目录之外的文件。"); } if (File.Exists(physicalFileToDelete)) { File.Delete(physicalFileToDelete); var successResult = new { IsSuccess = true, Message = $"文件 '{fileNameToDelete}' 已成功删除。" }; context.Response.Write(JsonConvert.SerializeObject(successResult)); } else { throw new FileNotFoundException($"文件 '{fileNameToDelete}' 不存在。"); } } catch (Exception ex) { var errorResult = new { IsSuccess = false, Message = ex.Message }; context.Response.Write(JsonConvert.SerializeObject(errorResult)); } } public bool IsReusable { get { return false; } } } }Utils.csusing System; using System.Data; using System.Data.SqlClient; using System.IO; using System.Net; using System.Security.Cryptography; using System.Text; namespace SHAPIPost { public static class Utils { /// <summary> /// AES解密 /// </summary> /// <param name="cipherText">密文</param> /// <param name="encryptionKey">密钥</param> /// <param name="encryptionIV">iv向量</param> public static string DecryptString(string cipherText, string encryptionKey, string encryptionIV) { try { string base64 = cipherText.Replace('-', '+').Replace('_', '/'); int padding = base64.Length % 4; if (padding != 0) { base64 += new string('=', 4 - padding); } byte[] cipherTextBytes = Convert.FromBase64String(base64); using (Aes aesAlg = Aes.Create()) { aesAlg.Key = Encoding.UTF8.GetBytes(encryptionKey); aesAlg.IV = Encoding.UTF8.GetBytes(encryptionIV); ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV); using (MemoryStream msDecrypt = new MemoryStream(cipherTextBytes)) { using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read)) { using (StreamReader srDecrypt = new StreamReader(csDecrypt)) { return srDecrypt.ReadToEnd(); } } } } } catch (Exception) { return null; } } /// <summary> /// AES加密 /// </summary> /// <param name="plainText">明文文本</param> /// <param name="encryptionKey">密钥</param> /// <param name="encryptionIV">iv向量</param> public static string EncryptString(string plainText, string encryptionKey, string encryptionIV) { using (Aes aesAlg = Aes.Create()) { aesAlg.Key = Encoding.UTF8.GetBytes(encryptionKey); aesAlg.IV = Encoding.UTF8.GetBytes(encryptionIV); ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); using (MemoryStream msEncrypt = new MemoryStream()) { using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write)) { using (StreamWriter swEncrypt = new StreamWriter(csEncrypt)) { swEncrypt.Write(plainText); } byte[] encrypted = msEncrypt.ToArray(); return Convert.ToBase64String(encrypted) .Replace('+', '-') .Replace('/', '_') .TrimEnd('='); } } } } /// <summary> /// 判断是否为Unix时间戳格式 /// </summary> /// <param name="timestamp">时间戳字符串</param> /// <returns>是否为Unix时间戳</returns> public static bool IsUnixTimestamp(string timestamp) { if (long.TryParse(timestamp, out long value)) { // Unix时间戳通常在这个范围内(1970年到2038年左右) return value >= 0 && value <= 2147483647; } return false; } /// <summary> /// 将Unix时间戳转换为DateTime /// </summary> /// <param name="timestamp">Unix时间戳</param> /// <returns>DateTime对象</returns> public static DateTime UnixTimestampToDateTime(long timestamp) { DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); return epoch.AddSeconds(timestamp); } /// <summary> /// 检查请求时间是否已过期 /// </summary> /// <param name="requestTime">请求时间</param> /// <param name="timeoutSeconds">超时秒数</param> /// <returns>是否已过期</returns> public static bool IsTimeExpired(DateTime requestTime, int timeoutSeconds) { DateTime currentTime; if (requestTime.Kind == DateTimeKind.Utc) { currentTime = DateTime.UtcNow; } else { currentTime = DateTime.Now; } TimeSpan timeDiff = currentTime - requestTime; return Math.Abs(timeDiff.TotalSeconds) > timeoutSeconds; } } }ExcelRepairer.csExcelRepairer处理我们获取报表过程中写入了一些不干净的字节流到文件头,导致excel异常打不开。 此方法依赖于.NET的 ZipArchive 类能够容忍您文件中的损坏程度,并成功枚举出其中的所有条目(entries)。如果文件的中心目录损坏到连条目列表都无法读取,那么此方法也将失败。namespace SHAPIPost { using System.IO; using System.IO.Compression; public static class ExcelRepairer { /// <summary> /// 通过重建ZIP归档的方式,尝试修复一个.xlsx文件。 /// 此方法可以修复因中心目录与本地文件头元数据不一致(如Length不对)导致的问题。 /// </summary> /// <param name="corruptedFileBytes">原始的、损坏的.xlsx文件字节数组。</param> /// <returns>一个新的、结构正确的.xlsx文件字节数组。</returns> public static byte[] RepairExcelFile(byte[] corruptedFileBytes) { // 创建一个内存流用于输出新的、修复后的文件 using (var outputStream = new MemoryStream()) { // 在输出流上创建一个新的、空的ZIP归档 using (var destinationArchive = new ZipArchive(outputStream, ZipArchiveMode.Create, true)) { // 使用输入字节创建一个内存流来读取损坏的文件 using (var inputStream = new MemoryStream(corruptedFileBytes)) // 以“读取”模式打开损坏的归档,即使它有错误,也希望能读出条目 using (var sourceArchive = new ZipArchive(inputStream, ZipArchiveMode.Read)) { // 遍历损坏归档中的每一个条目(内部文件) foreach (var sourceEntry in sourceArchive.Entries) { // 在新的归档中,为当前条目创建一个同名的新条目 // 我们让.NET自己决定压缩级别等参数 var destinationEntry = destinationArchive.CreateEntry(sourceEntry.FullName); // 打开源条目的数据流进行读取 using (var sourceStream = sourceEntry.Open()) // 打开新条目的数据流进行写入 using (var destinationStream = destinationEntry.Open()) { // 将原始数据从损坏的归档中完整地复制到新的归档中 sourceStream.CopyTo(destinationStream); } } } } // 当 using (destinationArchive...) 代码块结束时, // .NET会自动根据我们实际写入的数据,在文件末尾生成一个全新的、100%正确的中心目录。 // 这就完成了修复。 return outputStream.ToArray(); } } } }ReportLogger.csusing System; using System.IO; using System.Threading.Tasks; using System.Web; namespace SHAPIPost { /// <summary> /// 轻量级的异步、线程安全日志记录器。 /// </summary> public static class ReportLogger { private static readonly string LogDirectory = HttpContext.Current.Server.MapPath("~/ReportLogs"); private static readonly object _lock = new object(); static ReportLogger() { try { Directory.CreateDirectory(LogDirectory); } catch { // 忽略在创建目录时发生的错误 } } public static void Log(string requestId, string message) { // 使用 Task.Run 将文件写入操作放入后台线程,避免阻塞主流程 Task.Run(() => { try { string logFilePath = Path.Combine(LogDirectory, $"log_{DateTime.Now:yyyy-MM-dd}.txt"); string logMessage = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] [RequestID: {requestId}] {message}{Environment.NewLine}"; // 使用锁确保多线程写入安全 lock (_lock) { File.AppendAllText(logFilePath, logMessage); } } catch { // 忽略日志写入失败 } }); } } }web.configsecuretoken就是ReportLogin.aspx验证的_internalSecureToken,保持一致即可<?xml version="1.0" encoding="utf-8"?> <!-- 有关如何配置 ASP.NET 应用程序的详细信息,请访问 https://go.microsoft.com/fwlink/?LinkId=169433 --> <configuration> <!-- 有关 web.config 更改的说明,请参见 http://go.microsoft.com/fwlink/?LinkId=235367。 可在 <httpRuntime> 标记上设置以下特性。 <system.Web> <httpRuntime targetFramework="4.8" /> </system.Web> --> <system.web> <compilation debug="true" targetFramework="4.8" /> <!-- 接口超时时间,1800秒=60*30(30分钟,配合报表下载),需要同步修改IIS的超时时间设置 --> <httpRuntime targetFramework="4.6.1" executionTimeout="1800"/> </system.web> <appSettings> <!-- U9报表SSO登录地址 --> <add key="U9LoginBaseUrl" value="http://localhost/U9/api/v1/ReportLogin.aspx?securetoken=2FFEAB291D1E7EAF76E94EBE93249C9D" /> <!-- U9报表前缀地址 --> <add key="ReportBaseUrl" value="http://localhost/U9/erp/display.aspx" /> <!-- p参数AES解密的密钥跟向量 --> <add key="EncryptionKey" value="106FE9250C1F5FC4A5BAB670EB663AF0" /> <add key="EncryptionIV" value="0C1F5FC4A5BAB670" /> <!-- Puppeteer程序下载报表超时时间,默认30分钟(预防首次报表访问冷启动等待时间长) --> <add key="PuppeteerTimeoutMinutes" value="30" /> <!-- DownloadReport接口超时时间,60秒 --> <add key="ExpirationTimeSeconds" value="60"/> <!-- U9报表类型对应的link地址 --> <!-- 总账-总账 --> <add key="FI_GL_Process_Rpt_GeneralLedgerRpt" value="lnk=FI.GL.Process.Rpt.GeneralLedgerRpt&sId=3002&newopen=true" /> <!-- 总账-明细账 --> <add key="FI_GL_Process_Rpt_DetailsRpt" value="lnk=FI.GL.Process.Rpt.DetailsRpt&sId=3002&newopen=true" /> <!-- 总账-序时账 --> <add key="FI_GL_Process_Rpt_SequenceBookRpt" value="lnk=FI.GL.Process.Rpt.SequenceBookRpt&sId=3002&newopen=true" /> <!-- 总账-发生额及余额表 --> <add key="FI_GL_Process_Rpt_TotalBalanceRpt" value="lnk=FI.GL.Process.Rpt.TotalBalanceRpt&sId=3002&newopen=true" /> <!-- 总账-资产明细表 --> <add key="FA_SubsidiaryLedgerRptMainUIForm" value="lnk=EAM.FA.Report.FA_SubsidiaryLedgerRptMainUIForm&sId=3009&newopen=true" /> </appSettings> <system.codedom> <compilers> <compiler language="c#;cs;csharp" extension=".cs" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.CSharpCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:default /nowarn:1659;1699;1701" /> <compiler language="vb;vbs;visualbasic;vbscript" extension=".vb" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.VBCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:default /nowarn:41008 /define:_MYTYPE=\"Web\" /optionInfer+" /> </compilers> </system.codedom> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="System.Web.Helpers" publicKeyToken="31bf3856ad364e35" /> <bindingRedirect oldVersion="1.0.0.0-3.0.0.0" newVersion="3.0.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Web.WebPages" publicKeyToken="31bf3856ad364e35" /> <bindingRedirect oldVersion="1.0.0.0-3.0.0.0" newVersion="3.0.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" /> <bindingRedirect oldVersion="1.0.0.0-5.3.0.0" newVersion="5.3.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-4.0.1.1" newVersion="4.0.1.1" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="Microsoft.Extensions.Primitives" publicKeyToken="adb9793829ddae60" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-2.2.0.0" newVersion="2.2.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Buffers" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-4.0.3.0" newVersion="4.0.3.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Text.Encodings.Web" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="Microsoft.Bcl.AsyncInterfaces" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="5.0.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Threading.Tasks.Extensions" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-4.2.0.1" newVersion="4.2.0.1" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Text.Json" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-5.0.0.1" newVersion="5.0.0.1" /> </dependentAssembly> </assemblyBinding> </runtime> </configuration>优化访问路由(附加)通过增加Global.asax文件,自定义路由 从/DownloadReport.ashx?reporttype=FI_GL_Process_Rpt_DetailsRpt&......的形式改为/DownloadReport/FI_GL_Process_Rpt_DetailsRpt?......的形式using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Security; using System.Web.SessionState; using System.Web.Routing; // 核心路由命名空间 using System.Web.Compilation; // 用于 BuildManager using System.Web.UI; // 用于 IRouteHandler using System.Web.Mvc; namespace SHAPIPost { // 为 .ashx 文件创建一个自定义的路由处理器 public class AshxRouteHandler : IRouteHandler { private readonly string _virtualPath; public AshxRouteHandler(string virtualPath) { _virtualPath = virtualPath; } public IHttpHandler GetHttpHandler(RequestContext requestContext) { // 使用 BuildManager 从虚拟路径动态创建处理程序实例 return BuildManager.CreateInstanceFromVirtualPath( _virtualPath, typeof(IHttpHandler)) as IHttpHandler; } } public class Global : System.Web.HttpApplication { protected void Application_Start(object sender, EventArgs e) { // 在应用程序启动时运行的代码 RegisterRoutes(RouteTable.Routes); } void RegisterRoutes(RouteCollection routes) { // 注册报表下载处理器路由 routes.Add("DownloadReportHandlerRoute", new Route( "DownloadReport/{reporttype}", // 路由模板 new AshxRouteHandler("~/DownloadReport.ashx") // 目标处理程序 )); } protected void Session_Start(object sender, EventArgs e) { } protected void Application_BeginRequest(object sender, EventArgs e) { } protected void Application_AuthenticateRequest(object sender, EventArgs e) { } protected void Application_Error(object sender, EventArgs e) { } protected void Session_End(object sender, EventArgs e) { } protected void Application_End(object sender, EventArgs e) { } } }测试程序使用控制台应用程序测试 我的一般处理程序是发布在8001端口的using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.IO; using System.Net; using System.Net.Http; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; namespace 测试用例 { /// <summary> /// 带有超时设置的WebClient /// </summary> public class WebClientWithTimeout : WebClient { public int Timeout { get; set; } protected override WebRequest GetWebRequest(Uri address) { WebRequest wr = base.GetWebRequest(address); if (wr != null) { wr.Timeout = this.Timeout; } return wr; } } class Program { // 密钥和IV必须与 DownloadReport.ashx 中的完全相同 private const string _encryptionKey = "106FE9250C1F5FC4A5BAB670EB663AF0"; private const string _encryptionIV = "0C1F5FC4A5BAB670"; private static readonly SemaphoreSlim _downloadLimiter = new SemaphoreSlim(50); private static readonly string BASE_URL = "http://localhost:8001"; static void Main(string[] args) { string username = "demo"; string enterpriseId = "666"; string orgID = "1001008170100181"; // 101组织的ID string orgName = "XXXX公司"; // 明细账 string accPeriodStart = "2025-01"; string accPeriodEnd = "2025-03"; string filename = GetFileName(accPeriodStart, accPeriodEnd, orgName, "明细账"); DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); long timestamp = (long)(DateTime.UtcNow - epoch).TotalSeconds; string payload = string.Format("usercode={0}&entcode={1}&org={2}×tamp={3}", username, enterpriseId, orgID, timestamp); string encryptedPayload = EncryptString(payload); string directDownloadUrl = $"{BASE_URL}/DownloadReport/FI_GL_Process_Rpt_DetailsRpt?p={encryptedPayload}&exporttype={exportType}&filename={filename}&taskID={taskID}&CaseName={caseName}&AccountingPeriodStart={accPeriodStart}&AccountingPeriodEnd={accPeriodEnd}"; DownloadFile(exportType, filename, directDownloadUrl); // 总账、序时账...其他报表的测试用例 Console.WriteLine("\n按任意键退出..."); Console.ReadKey(); } /// <summary> /// 同步下载文件的完整流程 /// </summary> private static void DownloadFile(string exportType, string filename, string requestUrl) { _downloadLimiter.Wait(); // 等待一个空位 try { Console.WriteLine($"\n[步骤 1] 请求生成报表: {filename}"); string fileUrl = null; string filePathToDelete = null; using (var webClient = new WebClientWithTimeout()) { webClient.Timeout = 1800000; // 设置30分钟超时 webClient.Encoding = Encoding.UTF8; // 1. 调用 DownloadReport.ashx 获取下载地址 try { string jsonResponse = webClient.DownloadString(requestUrl); Console.WriteLine($" - 服务器响应: {jsonResponse}"); var result = JsonConvert.DeserializeObject<JObject>(jsonResponse); if (result.Value<bool>("IsSuccess")) { fileUrl = result.Value<string>("Url"); filePathToDelete = result.Value<string>("DelUrl"); Console.WriteLine($" - 成功获取下载地址: {fileUrl}"); } else { Console.WriteLine($" - 错误: {result["Message"]}"); return; } } catch (Exception ex) { Console.WriteLine($" - [步骤 1] 失败: {ex.Message}"); return; } // 2. 从获取到的URL下载文件 if (!string.IsNullOrEmpty(fileUrl)) { Console.WriteLine($"\n[步骤 2] 开始从 {fileUrl} 下载文件..."); try { string fileExtension = exportType.Equals("PDF", StringComparison.OrdinalIgnoreCase) ? ".pdf" : (exportType.Equals("CSV", StringComparison.OrdinalIgnoreCase) ? ".csv" : ".xlsx"); string fileNameWithExt = $"{filename}{fileExtension}"; string savePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileNameWithExt); if (File.Exists(savePath)) File.Delete(savePath); byte[] fileBytes = webClient.DownloadData(fileUrl); File.WriteAllBytes(savePath, fileBytes); Console.WriteLine($" - 下载成功! 文件已保存至: {savePath} (大小: {fileBytes.Length} 字节)"); } catch (Exception ex) { Console.WriteLine($" - [步骤 2] 失败: {ex.Message}"); } } // 3. (可选) 删除服务器上的文件 //if (!string.IsNullOrEmpty(filePathToDelete)) //{ // Console.WriteLine($"\n[步骤 3] 请求删除服务器文件: {filePathToDelete}"); // try // { // using (var webClient = new System.Net.WebClient()) // { // webClient.Encoding = System.Text.Encoding.UTF8; // webClient.DownloadString(filePathToDelete); // } // } // catch (Exception ex) // { // Console.WriteLine($" - [步骤 3] 失败: {ex.Message}"); // } //} } } finally { _downloadLimiter.Release(); // 释放一个空位,让其他等待的请求可以进入 } } private static string GetFileName(string accPeriodStart, string accPeriodEnd, string orgName, string v) { if (accPeriodStart == accPeriodEnd) { return $"{orgName} {accPeriodStart}{v}"; } else { return $"{orgName} {accPeriodStart}至{accPeriodEnd}{v}"; } } public static string EncryptString(string plainText) { using (Aes aesAlg = Aes.Create()) { aesAlg.Key = Encoding.UTF8.GetBytes(_encryptionKey); aesAlg.IV = Encoding.UTF8.GetBytes(_encryptionIV); ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); using (MemoryStream msEncrypt = new MemoryStream()) { using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write)) { using (StreamWriter swEncrypt = new StreamWriter(csEncrypt)) { swEncrypt.Write(plainText); } byte[] encrypted = msEncrypt.ToArray(); // 使用不依赖 System.Web 的方式生成URL安全的Base64字符串 // 这段代码的功能与 HttpServerUtility.UrlTokenEncode 相同 return Convert.ToBase64String(encrypted) .Replace('+', '-') .Replace('/', '_') .TrimEnd('='); } } } } } }测试结果输出PDF把exportType=“PDF”即可; 可以看到上面我设置的默认方案是2025-06至2025-08的 输出的excel表已经是按照条件的2025-01至2025-03的
2025年08月20日
52 阅读
1 评论
0 点赞
2025-08-15
【060】客开UI插件示例(转)
原文地址:客开UI插件案例客开UI插件注意事项开发UI插件时确保不要开启热插件,即..\Portal\bin\environment.xml中属性uipluging必须设置为false(设置为true页面开发的dll拷贝不需要重启IIS)如何客开UI插件:PluginTool工具做UI插件时很多页面找不到webpart,特别是弹窗找不到webpart,做插件最后的方式是用如下方法做插件,不用这个工具,因为这个工具能做的东西非常有限先找webpart:先打开U9要做UI插件的页面,找到页面的uri或者formid,如下图所示:可以从如下界面找到URI(单点登录是也要获取这个URI)如果是弹窗页面,需要找FormID,如下图:找到URI或者FormID后,就可以在classview(数据字典)中找到webpart,如下图:写配置文件UI插件的.config配置文件必须以WebPartExtend_命名(建议从如下解决方案中用解决方案中的那个配置文件),节点一般是: extendedPartAssemblyName="UFIDA.U9.Cust.U9Demo.PlugUI.dll" />其中parentPartFullName是上一步我们找出的webpart,名字必须要完全对,extendedPartFullName是自己VS客开的这个类库的命名空间+自己的这个类名,如图:extendedPartAssemblyName为自己VS客开的这个解决方案的程序集名称,如图:参考客开解决方案(建议用这个解决方案改类库的程序集名称、命名空间、配置文件,改成自己项目的,建议改成UFIDA.U9.Cust.项目简称.模块简称PlugIn:链接:https://pan.baidu.com/s/1IpRiHFTlHPePJAQ9rNl7aQ?pwd=1234提取码:1234一、按钮操作1、业务场景(增加按钮)UI插件开发最常用的场景之一,对标准产品页面需要增加按钮来实现不同业务需要,先对U9标准页面增加按钮的三种方法分别举例说明(UI插件增加的按钮无法设置权限:一般情况下可以变通方案处理,处理思路:参数设置里面加个参数,录入用户编码或者角色编码(逗号隔开),代码里面取这个值判断UI插件增加的按钮可用或不可用)案例1:Toolbar上增加按钮(页面最顶层区域)// (1)、实例化按钮 IUFButton btnPRToPM = new UFWebButtonAdapter(); //(2)、加入功能栏Card中 IUFToolbar toolbar = (IUFToolDescFlexFieldbar)part.GetUFControlByName(part.TopLevelContainer, "Toolbar1");//可以使用F12在页面上找,一般情况下名字就叫Toolbar1 if (toolbar != null) { string guid = "047FC9F5-46C0-449A-83C2-2822BCF24012";// 在数据库生成下GUID,或者修改下这里的值。SELECT NEWID() btnPRToPM = UIControlBuilder.BuilderToolbarButton(toolbar, "True", "btnPRToPM", "True", "True", 70, 28, "7", "", true, false,guid, guid, guid); UIControlBuilder.SetButtonAccessKey(btnPRToPM); btnPRToPM.Text = "拉单销售订单"; btnPRToPM.ID = "btnPRToPM"; btnPRToPM.AutoPostBack = true; btnPRToPM.UIModel = part.Model.ElementID; ((UFWebToolbarAdapter)toolbar).Items.Add(btnPRToPM as System.Web.UI.WebControls.WebControl); btnPRToPM.Click += new EventHandler(btnPRToPM_Click);//自己的点击事件 } 案例2:页面最下方按钮区域增加按钮(页面最底层区域) IUFCard card = (IUFCard)part.GetUFControlByName(part.TopLevelContainer, "Card0");//一般情况下名称为Card0,具体请使用F12查看 IUFButton btnPRToPM2 = new UFWebButtonAdapter(); btnPRToPM2.Text = "拉单销售订单"; btnPRToPM2.ID = "btnPRToPM2"; btnPRToPM2.AutoPostBack = true; card.Controls.Add(btnPRToPM2); btnPRToPM2.Click += new EventHandler(BtnPRToPM2_Click); CommonFunction.Layout(card, btnPRToPM2, 16, 0); //一般为从左往右按钮个数乘以2 CommonFunction类代码如下:public class CommonFunction { public static void Layout(IContainer container, IUFControl ctrl, uint x, uint y) { Layout(container, ctrl, x, y, 1, 1, Unit.Pixel(0), Unit.Pixel(0), true); } public static void Layout(IContainer container, IUFControl ctrl, uint x, uint y, int width, int height) { Layout(container, ctrl, x, y, 1, 1, Unit.Pixel(width), Unit.Pixel(height), false); } public static void Layout(IContainer container, IUFControl ctrl, uint x, uint y, int xspan, int yspan,Unit width, Unit height, bool isAutoSize) { IGridLayout gl = container.Layout as IGridLayout; if (gl == null) return; GridLayoutInfo glInfo = new GridLayoutInfo((uint)x, (uint)y, (uint)xspan, (uint)yspan, width, height); glInfo.AutoSize = isAutoSize; gl.Controls.Add((Control)ctrl, glInfo); } public static IUFControl FindControl(IPart part, string parentControl, string control) { IUFCard card = (IUFCard)part.GetUFControlByName(part.TopLevelContainer, parentControl); if (card == null) return null; foreach (IUFControl ctrl in card.Controls) { if (ctrl.ID.Equals(control, StringComparison.OrdinalIgnoreCase)) { return ctrl; } } return null; } }案例3:页面最下方下拉按钮中增加按钮IUFMenu btnPRToPM1 = new UFWebMenuAdapter(); btnPRToPM1.Text = "拉单销售订单"; btnPRToPM1.ID = "BtnQurySaleOrder2"; btnPRToPM1.AutoPostBack = true; IUFDropDownButton menuButtion = (IUFDropDownButton)CommonFunction.FindControl(part, "Card0", "DDBtnOperation");//Card0为操作按钮区域名称,可在浏览器开发工具或F12看到,DDBtnOperation为下拉按钮的名称,使用F12一样可以看到 if (menuButtion != null) { //btnPRToPM1.ItemClick += BtnPRToPM1_ItemClick;//注意这里注册的是//ItemClick事件 menuButtion.MenuItems.Add(btnPRToPM1); }2、业务场景:点击页面上按钮后写判断逻辑点击按钮后执行产品按钮事件前和执行产品按钮事件后都可以做插件,可分别使用BeforeEventProcess和AfterEventProcess事件,可重写这两个事件。业务场景:用户点击提交后弹窗给用户提示是否提交或取消。方案:U9内部没有方法可实现弹窗点击确定后执行一个操作,点击取消后执行另一个操作,需要UBF开发一个自定义视图页面做弹窗(两个按钮,一个字段作为提示即可),以下是代码示例public override void BeforeEventProcess(UFSoft.UBF.UI.IView.IPart Part, string eventName, object sender, EventArgs args, out bool executeDefault) { UFSoft.UBF.UI.WebControlAdapter.UFWebButton4ToolbarAdapter webButton = sender as UFSoft.UBF.UI.WebControlAdapter.UFWebButton4ToolbarAdapter; //按钮不同区域这个类型可能不一样,调试状态下可以看出sender参数的对象类型 //审核按钮 if (webButton != null && (webButton.Action == "SubmitClick" || webButton.Action == "AppvoveClick")) { if ("需要弹窗") { part.ShowAtlasModalDialog(btnPRToPM2, "e37cea28-9138-43a4-bbe7-e747977e3db5", "已转成功请购单", "992", "504", "", null, true, false, false); //btnPRToPM2按钮是自己增加的自定义按钮,用于回调(弹窗关闭后执行的代码实现) //BtnCreatPR1为自定义按钮,默认隐藏,弹窗回调使用 //弹窗后需要将指令(点击了确定还是取消)写入到part.CurrentState["XXX"]中 executeDefault = false; //这里不执行审核后事件动作了 return; } } base.BeforeEventProcess(Part, eventName, sender, args, out executeDefault); } void BtnPRToPM2_Click(object sender, EventArgs e) { this.part.Model.ClearErrorMessage(); if ("点击了确定继续执行") //part.CurrentState["XXX"]中获取标识 _part.BtnApprove_Click(sender, e); else return; } private UFIDA.U9.SCM.SM.SOUIModel.StandardSOMainUIFormWebPart part; IUFDataGrid DataGrid4; IUFButton btnPRToPM2; public override void AfterInit(UFSoft.UBF.UI.IView.IPart Part, EventArgs args) { //首先调用原来的事件 base.AfterInit(Part, args); part = Part as UFIDA.U9.SCM.SM.SOUIModel.StandardSOMainUIFormWebPart; if (part == null) return; DataGrid4 = (IUFDataGrid)part.GetUFControlByName(part.TopLevelContainer, "DataGrid4"); //2.Card里面增加按钮 //设置按钮在容器中的位置 #region 2.Card里面增加按钮 IUFCard card = (IUFCard)part.GetUFControlByName(part.TopLevelContainer, "Card0"); btnPRToPM2 = new UFWebButtonAdapter(); btnPRToPM2.Text = "拉单销售订单"; btnPRToPM2.ID = "BtnQurySaleOrder1"; btnPRToPM2.AutoPostBack = true; card.Controls.Add(btnPRToPM2); btnPRToPM2.Click += new EventHandler(BtnPRToPM2_Click); CommonFunction.Layout(card, btnPRToPM2, 16, 0); //一般为从左往右按钮个数乘以2 #endregion }业务场景:标准查询列表客开如何干预查询结果 public override void BeforeDataBinding(IPart Part, out bool executeDefault) { base.BeforeDataBinding(Part, out executeDefault); if (_strongPart == null || this._strongPart.Model.PlanOrder == null) return; _strongPart.Model.PlanOrder.CurrentFilter.OPath += " and DocNo >='051025070200613'"; _strongPart.Action.NavigateAction.Refresh(null, true); }二、界面行数据(DataGridView)操作针对行的数据操作,经常有业务场景,需要根据行的数量、单价计算金额等其他复杂的计算,这种情况无论插件还是单据都需要借助Callback或PostBack进行计算。Callback和Postback的区别:Callback:页面赋值后只局部刷新,页面只刷新需要修改的值,修改的值实时反映到页面控件上,不联动修改其他字段,不引起其他任何字段的联动。PostBack:页面赋值后页面全局刷新,需要进行数据收集和绑定才会反映到控件上,会引起其他控件的联动。两种方式都可以实现页面简单计算、对行字段赋值。如果需要对字段联动或者新增行之类的操作比较多的字段可以选用PostBack实现。(具体可在实际使用过程中视情况来定,两种方法切换也较为方便)具体案例类型有:数量、单价计算金额等类似;DataGridView可注册事件: 可从..\Portal\js\DataGrid.js文件中查询到 DataGridEvent.OnRowClick = "OnRowClick"; DataGridEvent. " "OnSortData"; DataGridEvent. " DataGridEvent. "OnBeforeOpenDialog"; DataGridEvent.OnAfterOpenDialog = "OnAfterOpenDialog"; DataGridEvent.OnBeforeCustomerPostBack = "OnBeforeCustomerPostBack"; DataGridEvent.OnAfterRowAdded = "OnAfterRowAdded"; DataGridEvent.OnCellDataChanged = "OnCellDataChanged"; DataGridEvent.OnCellDataValueChanged = "OnCellDataValueChanged"; DataGridEvent.OnBeforeRowAdd = "OnBeforeRowAdd"; DataGridEvent. " "OnControlValueChange"; DataGridEvent.OnCustomFilter = "OnCustomFilter"; //响应过滤菜单事件. DataGridEvent. " "CustomerPostBack"; //服务器端自定义事件 DataGridEvent.OnBatchModify = "OnBatchModify"; //批量修改事件//region 自定义用户事件 function DataGridEvent() { } DataGridEvent.OnBodyRowSelectedChange = "OnBodyRowSelectedChange"; DataGridEvent.OnBodyRowSelectedValueChange = "OnBodyRowSelectedValueChange"; DataGridEvent.OnBodyRowSelected = "OnBodyRowSelected";DataGrid行checkbox的触发事件 DataGridEvent.OnBeforeRowInsert = "OnBeforeRowInsert"; DataGridEvent.OnBeforeRowDelete = "OnBeforeRowDelete"; DataGridEvent.OnAfterRowInserted = "OnAfterRowInserted"; DataGridEvent.OnAfterRowDeleted = "OnAfterRowDeleted"; DataGridEvent.OnCellFocusEnter = "OnCellFocusEnter"; DataGridEvent.OnCellFocusOut = "OnCellFocusOut"; DataGridEvent.OnBeforeCellFocusEnter = "OnBeforeCellFocusEnter"; //行 copy 功能 DataGridEvent.OnAfterRowCopyed = "OnAfterRowCopyed"; DataGridEvent.OnBeforeRowCopy = "OnBeforeRowCopy"; DataGridEvent.OnRowCopy = "OnRowCopy"; DataGridEvent.OnGridHeadClick = "OnGridHeadClick"; DataGridEvent.OnCellClick = "OnCellClick"; DataGridEvent.OnCellDBClick = "OnCellDbClick"; DataGridEvent.OnRowChanged = "OnRowChanged";获取行DatagridView控件//DataGrid4为页面DataGridView控件名称,可使用F12找到DataGridView控件名称。 IUFDataGrid dataGrid = (IUFDataGrid)part.GetUFControlByName(part.TopLevelContainer, "DataGrid4");业务场景:单元格数量改变callback使用callback举例,开发人员可使用postback实现一次。所有callback实现的多种场景案例大部分代码都类似。如果是插件的开发,需要先获取到行DataGridView控件,插件里面把下方的this改成插件的part即可public void AfterCreateChildControls() //插件注册到AfterInit() { //注册callback事件,调BP获取料品单价 RegisterGridCellDataChangedCallBack(); } #region 回调注册\处理专区 /// <summary> /// 注册表格单元格内容改变的回调事件 /// </summary> private void RegisterGridCellDataChangedCallBack() { AssociationControl gridCellDataChangedASC = new AssociationControl(); //基本固定代码 gridCellDataChangedASC.SourceServerControl = this.DataGrid8; gridCellDataChangedASC.SourceControl.EventName = "OnCellDataChanged"; //注册行的单元格改变事件 //CallBack处理方案 ((IUFClientAssoGrid)gridCellDataChangedASC.SourceControl).FireEventCols.Add("Item"); //触发源,Item为触发控件名称 ClientCallBackFrm gridCellDataChangedCBF = new ClientCallBackFrm(); gridCellDataChangedCBF.ParameterControls.Add(this.DataGrid8); gridCellDataChangedCBF.DoCustomerAction += new ClientCallBackFrm.ActionCustomer(gridCellDataChangedCBF_DoCustomerActionOfSubvillage); gridCellDataChangedCBF.Add(gridCellDataChangedASC); this.Controls.Add(gridCellDataChangedCBF); } /// <summary> /// 表格的CallBack处理方式 /// </summary> /// <param name="args"></param> /// <returns></returns> private object gridCellDataChangedCBF_DoCustomerActionOfSubvillage(CustomerActionEventArgs args) { UFWebClientGridAdapter grid = new UFWebClientGridAdapter(this.DataGrid8); //行的DataGrid控件 //取表格数据(当前行) ArrayList list = (ArrayList)args.ArgsHash[UFWebClientGridAdapter.ALL_GRIDDATA_SelectedRows]; //基本固定代码 int curIndex = int.Parse(list[0].ToString()); Hashtable table = (Hashtable)((ArrayList)args.ArgsHash[this.DataGrid8.ClientID])[curIndex]; long ItemID = long.Parse(table["Item"].ToString()); //获取触发源字段值 if (ItemID > 0) { // .......(略)写自己的业务逻辑 //单价 grid.CellValue.Add(new object[] { curIndex, "UnitPrice", new string[] { "XXX", "YYY", "ZZZ" } }); //UnitPrice为要更新的字段名称,如果要更新多个字段值,需要些多个Add。后面三个参数,如果为参照的话分别对应ID,Code,Name args.ArgsResult.Add(grid.ClientInstanceWithValue); // ..........(略) } return args; } #endregion 业务场景:行数值计算postbackprivate void Register_DataGrid_Qty_PoskBack() { AssociationControl assocControl = new AssociationControl(); assocControl.SourceServerControl = this.DataGrid0; assocControl.SourceControl.EventName = "OnCellDataValueChanged"; //注册单元格改变事件 ((IUFClientAssoGrid)assocControl.SourceControl).FireEventCols.Add("ApsQty"); //触发列 CodeBlock cb = new CodeBlock(); UFWebClientGridAdapter gridAdapter = new UFWebClientGridAdapter(this.DataGrid0); gridAdapter.IsPostBack = true; gridAdapter.PostBackTag = "OnCellDataValueChanged"; //同上 cb.TargetControls.addControl(gridAdapter); assocControl.addBlock(cb); UFGrid itemGrid = this.DataGrid0 as UFGrid; itemGrid.GridCustomerPostBackEvent += new GridCustomerPostBackDelegate(_Qty_GridCustomerPostBackEvent); } void _Qty_GridCustomerPostBackEvent(object sender, GridCustomerPostBackEventArgs e) { if (e.SrcColumnName != "ApsQty") //触发源字段=ApsQty时才执行下面的逻辑 return; string oldProductCode = this.Model.PlanOrderAPS.FocusedRecord.ProductionLine_Code; if (oldProductCode == "") return; this.OnDataCollect(this); this.IsDataBinding = true; //当前事件执行后会进行数据绑定 this.IsConsuming = false; this.DataGrid0.CollectData(); this.DataGrid0.BindData(); PlanOrderAPSRecord record = this.Model.PlanOrderAPS.FocusedRecord; record.XXX = YYY; //赋值 }业务场景:选择数据新增行(多选实现)postback选择料品后,可以根据料品信息新增单据行,并带出其他信息public void AfterCreateChildControls() { //注册callback事件,调BP获取料品单价 RegisterGridCellDataChangedPostBack(); } /// <summary> /// 注册表格单元格内容改变的回调事件 /// </summary> private void RegisterGridCellDataChangedPostBack() { AssociationControl assocControl = new AssociationControl(); assocControl.SourceServerControl = this.DataGrid5; assocControl.SourceControl.EventName = "OnCellDataValueChanged"; ((IUFClientAssoGrid)assocControl.SourceControl).FireEventCols.Add("Gift"); CodeBlock cb = new CodeBlock(); UFWebClientGridAdapter gridAdapter = new UFWebClientGridAdapter(this.DataGrid5); gridAdapter.IsPostBack = true; gridAdapter.PostBackTag = "OnCellDataValueChanged"; cb.TargetControls.addControl(gridAdapter); assocControl.addBlock(cb); UFGrid itemGrid = this.DataGrid5 as UFGrid; itemGrid.GridCustomerPostBackEvent += new GridCustomerPostBackDelegate(GridCell_GridCustomerPostBackEvent); } private void GridCell_GridCustomerPostBackEvent(object sender, GridCustomerPostBackEventArgs e) { if (e.PostBackTag == "OnCellDataValueChanged") { DataTable dt = this.CurrentState["CustItem_Table"] as DataTable; if (dt == null) { this.DataGrid5.CollectData(); this.DataGrid5.BindData(); return; } CurrentState.Remove("CustItem_Table"); //校验DT是否为空 if (dt.Rows.Count < 1) { this.DataGrid5.CollectData(); this.DataGrid5.BindData(); return; } //获取最后的行号 int rowNo = 10; int recordsCount = this.Model.GiftShip_GiftShipLine.RecordCount; if (recordsCount != 0) { rowNo = Convert.ToInt32(this.Model.GiftShip_GiftShipLine.Records[recordsCount - 1]["RowNO"]); } //若只返回一条,做数据收集即可 if (dt.Rows.Count == 1) { DataGrid5.CollectData(); DataGrid5.BindData(); } //循环传回来的表体,//当多选参照界面点击确定返回时,Model默认添加了第一条记录,故不做处理 for (int i = 1; i < dt.Rows.Count; i++) { GiftShip_GiftShipLineRecord rd = this.Model.GiftShip_GiftShipLine.AddNewUIRecord(); rd.Gift = !string.IsNullOrEmpty(Convert.ToString(dt.Rows[i]["ItemID"])) ? long.Parse(Convert.ToString(dt.Rows[i]["ItemID"])) : 0; rd.Gift_Code = Convert.ToString(dt.Rows[i]["ItemCode"]); rd.Gift_Name = Convert.ToString(dt.Rows[i]["ItemName"]); //..........(略) } this.DataCollect(); this.DataBind(); // rd.SetParentRecord(this.Model.GiftShip.FocusedRecord); // Note: 'rd' is out of scope here. DataGrid5.CollectData(); DataGrid5.BindData(); } } //弹窗页面点击确定后执行如下方法将选择的数据放到table缓存到session里面: private void ReturnSelectedValue() { DataTable dt = new DataTable(); dt.Columns.Add("ID", typeof(long)); dt.Columns.Add("Code", typeof(string)); dt.Columns.Add("Name", typeof(string)); dt.Columns.Add("PurchaseUOM_ID", typeof(long)); dt.Columns.Add("PurchaseUOM_Code", typeof(string)); dt.Columns.Add("PurchaseUOM_Name", typeof(string)); foreach (IUIRecord _frd in this.Model.cRef.SelectRecords) { DataRow dr = dt.NewRow(); dr["ID"] = _frd["ID"]; dr["Code"] = _frd["Code"]; dr["Name"] = _frd["Name"]; dr["PurchaseUOM_ID"] = _frd["PurchaseUOM_ID"]; dr["PurchaseUOM_Code"] = _frd["PurchaseUOM_Code"]; dr["PurchaseUOM_Name"] = _frd["PurchaseUOM_Name"]; dt.Rows.Add(dr); } this.CurrentState["CustItem_Table"] = dt; }业务场景:行参照根据其他字段过滤private void GridFilterCallBackEvents() { IUFDataGrid uFControlByName = this.DataGrid5; AssociationControl control = new AssociationControl(); control.SourceServerControl = uFControlByName; control.SourceControl.EventName = "OnBeforeCellFocusEnter"; ((UFWebClientGridAdapter)control.SourceControl).FireEventCols.Add("CurrentBin"); ClientCallBackFrm child = new ClientCallBackFrm(); child.DoCustomerAction += assoCGrid_DoBeforePackAction; child.ParameterControls.Add(uFControlByName); child.Add(control); this.Controls.Add(child); } private object assoCGrid_DoBeforePackAction(CustomerActionEventArgs args) { string str2 = string.Empty; IUFDataGrid uFControlByName = this.DataGrid5; int num = Convert.ToInt32(args.ArgsHash[UFWebClientGridAdapter.FocusRow]); if (num >= 0) { ArrayList list = (ArrayList)args.ArgsHash[uFControlByName.ClientID]; Hashtable hashtable = (Hashtable)list[num]; if (args.ArgsHash["ALL_GRIDDATA_FocusColumnName"].ToString() == "CurrentBin") { UFWebClientGridAdapter adapter; try { if (hashtable["CurrentWH"] != null && hashtable["CurrentWH"].ToString() != "" && hashtable["CurrentWH"].ToString() != "-1") { str2 = " Warehouse =" + hashtable["CurrentWH"].ToString() + ""; adapter = new UFWebClientGridAdapter(uFControlByName); adapter.ResetColumnEditorAttribute("CurrentBin", UFWebClientRefControlAdapter.Attributes_AddParam, new string[] { "UBF_CustomFilter", str2 }); args.ArgsResult.Add(adapter.ClientInstanceWithRefAddParam); return args; } } catch (Exception) { adapter = new UFWebClientGridAdapter(uFControlByName); adapter.ResetColumnEditorAttribute("CurrentBin", UFWebClientRefControlAdapter.Attributes_AddParam, new string[] { "UBF_CustomFilter", "ID=-1" }); args.ArgsResult.Add(adapter.ClientInstanceWithRefAddParam); return args; } } } return args; } 根据行上某个字段判断,让另外一个显示不同的参照参照的案例是材料出库行“来源单据类型和”来源单据“列。 事件注册在AfterCreateChildControls,插件注册在AfterInit中,示例如下: 核心思路就是,UBF的Model中要增加多个自定义字段,绑定不同的实体; 在UBF UIForm中绑定好各自的参照,隐藏掉;代码里面把显示的列参照根据条件替换成隐藏列的参照private void CallBack_SrcDoc() { AssociationControl assoCGrid = new AssociationControl(); assoCGrid.SourceServerControl = this.DataGrid8; assoCGrid.SourceControl.EventName = "OnBeforeCellFocusEnter"; ((IUFClientAssoGrid)assoCGrid.SourceControl).FireEventCols.Add("SourceDocNo"); UFWebClientGridAdapter grid = new UFWebClientGridAdapter(this.DataGrid8); CodeBlock codeBlock = new CodeBlock(); string sourceDocType = grid.getSelectedValueText("SourceDocType"); string expression = string.Empty; expression += "if( "; expression += new UFWebClientGridAdapter(this.DataGrid8).getSelectedValuePK("SourceDocType"); expression += " =='1')"; codeBlock.Condition = expression; grid.SwitchColumnControl("IssueApplyDocLine4SrcDoc", "SourceDocNo"); //IssueApplyDocLine4SrcDoc为申请单的参照控件 codeBlock.TargetControls.addControl(grid); assoCGrid.addBlock(codeBlock); codeBlock = new CodeBlock(); expression = " else if( "; expression += new UFWebClientGridAdapter(this.DataGrid8).getSelectedValuePK("SourceDocType"); expression += " =='-1')"; codeBlock.Condition = expression; UFWebClientGridAdapter grid2 = new UFWebClientGridAdapter(this.DataGrid8); grid2.SwitchColumnControl("SourceDocNo", "SourceDocNo"); //SourceDocNo为备料参照控件 codeBlock.TargetControls.addControl(grid2); assoCGrid.addBlock(codeBlock); codeBlock = new CodeBlock(); expression = " else if( "; expression += new UFWebClientGridAdapter(this.DataGrid8).getSelectedValuePK("SourceDocType"); expression += " =='2')"; codeBlock.Condition = expression; UFWebClientGridAdapter grid3 = new UFWebClientGridAdapter(this.DataGrid8); grid3.SwitchColumnControl("IssueApplyDocLineSum4SrcDoc", "SourceDocNo"); //IssueApplyDocLineSum4SrcDoc为领料申请汇总参照控件 codeBlock.TargetControls.addControl(grid3); assoCGrid.addBlock(codeBlock); codeBlock = new CodeBlock(); expression = " else if( ("; expression += new UFWebClientGridAdapter(this.DataGrid8).getSelectedValuePK("SourceDocType"); expression += " =='0') && ("; expression += new UFWebClientGridAdapter(this.DataGrid8).getSelectedValuePK("BizType"); //expression += " =='47')"; expression += " !='52')"; expression += ")"; codeBlock.Condition = expression; UFWebClientGridAdapter grid4 = new UFWebClientGridAdapter(this.DataGrid8); grid4.SwitchColumnControl("MOPick4SrcDoc", "SourceDocNo"); codeBlock.TargetControls.addControl(grid4); assoCGrid.addBlock(codeBlock); codeBlock = new CodeBlock(); expression = " else if( ("; expression += new UFWebClientGridAdapter(this.DataGrid8).getSelectedValuePK("SourceDocType"); expression += " =='0') && ("; expression += new UFWebClientGridAdapter(this.DataGrid8).getSelectedValuePK("BizType"); expression += " =='52')"; expression += ")"; codeBlock.Condition = expression; UFWebClientGridAdapter grid5 = new UFWebClientGridAdapter(this.DataGrid8); grid5.SwitchColumnControl("PLSPick4SrcDoc", "SourceDocNo"); codeBlock.TargetControls.addControl(grid5); assoCGrid.addBlock(codeBlock); } 业务场景:表头字段更新其他字段的值private void CallBack_UomTab_ProductQtyby() { AssociationControl assoC_ProductQty = new AssociationControl(); // 生产数量 assoC_ProductQty.SourceServerControl = this.ProductQty196; assoC_ProductQty.SourceControl.EventName = "OnValueChanged"; ClientCallBackFrm cF = new ClientCallBackFrm(); cF.ParameterControls.Add(this.ProductQty196); //看自己需要可以注册多个控件 // cF.ParameterControls.Add(this.ProductQty1); // cF.ParameterControls.Add(this.PUToPBURate166); /// cF.ParameterControls.Add(this.PBUToSBURate91); cF.DoCustomerAction += new ClientCallBackFrm.ActionCustomer(onUOMTabProductQtyCallBackAction); cF.Add(assoC_ProductQty); } object onUOMTabProductQtyCallBackAction(CustomerActionEventArgs args) { // 生产数量 decimal ProductQty = args.ArgsHash[this.ProductQty196.ClientID].ToString().Equals("") ? 1 : decimal.Parse(args.ArgsHash[this.ProductQty196.ClientID].ToString()); // Assuming ProductQtyByProductUOM is defined elsewhere in your code. // decimal ProductQtyByProductUOM = ...; args.ArgsResult.Add(new UFWebClientNumberAdapter(this.ProductQty1).ClientInstance + ".set_Value('" + ProductQty.ToString() + "')"); return args; }
2025年08月15日
20 阅读
0 评论
0 点赞
1
2
...
7