# kingdee.MetadataDrivenFieldExtractor **Repository Path**: dihl/kingdee.-metadata-driven-field-extractor ## Basic Information - **Project Name**: kingdee.MetadataDrivenFieldExtractor - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2026-08-14 - **Last Updated**: 2026-08-17 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # MetadataDrivenFieldExtractor · 元数据驱动字段提取器 > 用元数据驱动消灭 if-else —— 一套通用遍历算法,统一提取金蝶 K/3 Cloud 各单据体中分布在不同层级的字段。 在 ERP 集成 / 二次开发中,"从单据里取一个字段"看似简单,但不同单据的字段位置差异极大:有的在单据头、有的在单据体、有的深藏在第三层子单据体。传统做法是为每种单据写一份 `if-else` 分支,新增一种单据就要改一遍代码。 本项目把**结构差异收敛为数据**,用一条 `FieldInfoVo` 单向链表描述"字段路径",遍历逻辑只写一遍,一套算法通吃所有单据结构。 ## 特性 - **元数据驱动**:字段在单据头 / 单据体 / 多层子单据体,全部抽象为一条字段路径链表 - **一套算法通吃所有层级**:`while (parent != null)` 收集父链 + `Reverse()` 自外向内逐层平铺,无任何特殊分支 - **开箱即用**:内置 17 种库存单据的字段元数据,新增单据只需增加几行配置,遍历代码零改动 - **线程安全**:基于 `ConcurrentDictionary` 的泛型缓存基类 + 双检锁单例,读多写少场景性能更优 - **结果去重**:`Distinct()` + `HashSet` 双重去重,即使同一仓库被多个字段捞到也只保留一份 ## 目录结构 ``` code/MetadataDrivenFieldExtractor/ ├── Cache/ │ ├── CacheDictBase.cs # 泛型并发缓存基类 │ └── CacheInvBillInfo.cs # 17 种单据字段元数据(单例预填) ├── Service/ │ └── BillAuditPlugInInvDailyClear.cs # 示例:单据审核服务插件(生成日清表) └── Vo/ ├── FieldInfoVo.cs # 字段路径链表节点 ├── InvBillInfoVo.cs # 单据元数据 + 通用遍历算法 getStockData() ├── BaseDataVo.cs # 基础数据(内码 / 编码 / 名称) └── InvStockOrgVo.cs # 仓库 + 组织 聚合 VO ``` ## 核心设计 ### 1. 字段路径链表 `FieldInfoVo` ```csharp public class FieldInfoVo { public string id { get; set; } // 字段标识,如 FStockID public string prop { get; set; } // 绑定实体属性,如 StockID public FieldInfoVo parent { get; set; } // 所在单据体(可无限嵌套) } ``` `parent` 是单向链表指针:指向"字段所在的单据体",单据体本身又可能在更外层单据体中。一个字段在第几层嵌套,就串几个 `parent`。 ### 2. 单据元数据 `InvBillInfoVo` 每种单据的差异变成一段纯数据配置。以最复杂的**组装拆卸单**为例,其仓库分布在两层:单据体 `ProductEntity` 和第三层子单据体 `ProductEntity → STK_ASSEMBLYSUBITEM`: ```csharp info = new InvBillInfoVo(); this.addOrUpdate("STK_AssembledApp".ToLower(), info); info.org = new FieldInfoVo() { id = "FStockOrgId", prop = "StockOrgID" }; info.vStock = new List(); // 单据体一层:ProductEntity.FStockID var stock = new FieldInfoVo(); info.vStock.Add(stock); stock.id = "FStockID"; stock.prop = "StockID"; stock.parent = new FieldInfoVo() { id = "FEntity", prop = "ProductEntity" }; // 第三层子单据体:ProductEntity -> STK_ASSEMBLYSUBITEM.FStockIDSETY stock = new FieldInfoVo(); info.vStock.Add(stock); stock.id = "FStockIDSETY"; stock.prop = "FStockIDSETY"; stock.parent = new FieldInfoVo() { id = "FSubEntity", prop = "STK_ASSEMBLYSUBITEM" }; stock.parent.parent = new FieldInfoVo() { id = "FEntity", prop = "ProductEntity" }; ``` ### 3. 通用遍历算法 `getStockData()` ```csharp // 收集字段到最外层的完整父链 List vParent = new List(); var parent = stock.parent; while (parent != null) { vParent.Add(parent); parent = parent.parent; } vParent.Reverse(); // 从最外层单据体开始向内遍历 List vData = null; for (int i = 0; i < vParent.Count; i++) { parent = vParent[i]; if (vData == null) { var doc = billData.GetObject(parent.prop) as DynamicObjectCollection; vData = doc.ToList(); } else { // 对上一层的每一行,取其子单据体并平铺合并 List v2 = new List(); foreach (var data in vData) { var doc = data.GetObject(parent.prop) as DynamicObjectCollection; foreach (var item in doc) v2.Add(item); } vData = v2; } } ``` **精妙之处**:`while` 收集父链 + `Reverse()` 后自外向内逐层平铺——"三层嵌套"与"一层嵌套"对算法而言只是链条长短不同,**没有任何特殊分支**。最终用 `Distinct()` 做字段内去重、`HashSet ids` 做跨字段去重。 ## 快速开始 ### 环境要求 - .NET Framework 4.8 - 金蝶 K/3 Cloud(或金蝶云苍穹)安装环境,引用其 WebSite/Bin 下的程序集: - `Kingdee.BOS.dll` - `Kingdee.BOS.Core.dll` - `Kingdee.BOS.DataEntity.dll` - `Kingdee.BOS.Mobile.dll` - `Newtonsoft.Json.dll` - `Dihl.Kingdee.Extensions.dll` > 项目 csproj 中的 `` 指向了本机 K/3 Cloud 安装目录,实际使用请按你的环境调整引用路径。 ### 在服务插件中使用 ```csharp public override void EndOperationTransaction(EndOperationTransactionArgs e) { var formId = this.BusinessInfo.GetForm().Id; var invInfo = CacheInvBillInfo.Instance.getInvInfo(formId); // 拿元数据 if (invInfo == null) { throw new Exception("生成日清表,不支持当前单据"); } foreach (var billData in e.DataEntitys) { var vStock = invInfo.getStockData(billData); // 通用遍历 if (vStock == null) continue; foreach (var stock in vStock) { // stock.id / stock.number / stock.name —— 直接使用 } } } ``` ## 内置单据支持 | 单据标识 | 单据名称 | 仓库位置 | |---|---|---| | `STK_InStock` | 采购入库单 | 单据体 `InStockEntry` | | `PUR_MRB` | 采购退料单 | 单据体 `PUR_MRBENTRY` | | `SAL_OUTSTOCK` | 销售出库单 | 单据体 `SAL_OUTSTOCKENTRY` | | `SAL_RETURNSTOCK` | 销售退货单 | 单据体 `SAL_RETURNSTOCKENTRY` | | `STK_MisDelivery` | 其他出库单 | 单据体 `BillEntry` | | `STK_MISCELLANEOUS` | 其他入库单 | 单据体 `STK_MISCELLANEOUSENTRY` | | `STK_TransferDirect` | 直接调拨单 | 单据体 `TransferDirectEntry`(源 + 目标仓库) | | `PRD_PickMtrl` | 生产领料单 | 单据体 `Entity` | | `PRD_ReturnMtrl` | 生产退料单 | 单据体 `Entity` | | `STK_StockCountLoss` | 盘亏单 | 单据体 `BillEntry` | | `STK_StockCountGain` | 盘盈单 | 单据体 `BillEntry` | | `PRD_INSTOCK` | 生产入库单 | 单据体 `Entity` | | `STK_AssembledApp` | 组装拆卸单 | 单据体 `ProductEntity` + 第三层 `STK_ASSEMBLYSUBITEM` | | `STK_TRANSFERIN` | 分步式调入单 | 单据体 `STK_STKTRANSFERINENTRY` | | `STK_TRANSFEROUT` | 分步式调出单 | 单据体 `STK_STKTRANSFEROUTENTRY` | | `SUB_PickMtrl` | 委外领料单 | 单据体 `Entity` | | `SUB_RETURNMTRL` | 委外退料单 | 单据体 `Entity` | ### 新增一种单据 1. 在 `CacheInvBillInfo.init()` 中追加一个 `#region`,按模板登记单据标识、组织字段、仓库字段(及其 `parent` 链); 2. 遍历代码、服务插件**零改动**; 3. 完成。 ## 设计理念:何时使用元数据驱动 **判断标准**:如果差异可以通过"增加一条数据"解决,就不要通过"增加一段代码"解决。 | 场景 | 差异维度 | 用元数据表达什么 | |---|---|---| | 表单字段提取 | 字段在不同层级 | 字段路径链表 | | 报表列生成 | 不同报表列集合 | 列定义配置 | | 数据校验规则 | 不同单据不同规则 | 规则表达式配置 | | 审批流分支 | 不同单据不同流转 | 流程节点配置 |