SpringBootVue3 企业主数据设计客户、供应商、合同与财务如何共用一套身份文档地址https://ruoyioffice.com 文章底部获取源码和演示地址 17156169080获取产品咨询销售录了一家客户采购又录了一家供应商合同里再手填一次名称到了财务对账时才发现四条记录其实是同一家企业。主数据设计要解决的不是少填几个字段而是让跨模块业务知道自己究竟在与谁发生关系。▲ 左侧汇聚不同业务来源中间管理主身份右侧由业务单据引用共享身份不等于共享所有业务状态。一、系统打通了接口为什么还是认不出同一家企业一家成长中的企业往往先上客户管理再上采购随后补合同和财务。每套模块单独运行都没问题但连接起来之后会出现一种尴尬接口每天都在同步数据却越来越难对。销售习惯用简称采购使用营业执照全称财务按开票名称录入旧系统还保留着公司更名前的名称。报表按名称分组同一家企业变成三行按各模块 ID 分组又完全无法对应。最终只能导出 Excel让熟悉业务的人判断“这几个是不是同一家”。问题不在于缺一条定时任务而在于没有确定一个跨模块稳定的身份。名称可以变联系方式可以变某个系统中的编号也可能因迁移改变。如果把这些变化中的属性当成唯一连接点接口越多纠错成本反而越高。本文用 RuoYi Office 的客商主数据、来源映射、合同选择组件和 ERP 供应商适配实现作为实例讨论一条完整路径建立身份、赋予角色、接入来源、被业务引用、处理变更与重复记录最后逐步迁移存量系统。需要先说明边界已核对的实现能证明部分入口和适配链路不能据此宣称全部 CRM、合同、采购、财务业务都已经完全统一。历史快照、可靠事件投递和迁移治理等部分会明确标成建议方案。二、用一家“双重角色”的企业把模型讲清楚假设我们与“远川科技有限公司”有两种合作。一方面我们向它销售项目实施服务它是客户另一方面我们向它采购设备它又是供应商。后来这家企业更名但法律主体未变。如果客户表与供应商表各维护一套名称、税号、地址和银行账号就有两个副本要同步。更糟的是更名时可能只有采购更新了资料合同仍显示旧名财务把它当成新单位再次建档。一种更稳定的设计是先建立一个客商身份例如主身份P10001再赋予客户和供应商两个角色。销售合同与采购单分别引用这个身份但保留各自业务字段。这里的编码只是算例现有手工新增实现使用分配到的 ID 字符串作为主编码。数据内容应回答的问题建议归属名称、主体标识、启停状态对方是谁客商主身份客户、供应商角色允许参与哪类业务主身份角色或角色扩展销售阶段、跟进记录销售进行到哪里CRM 业务域采购价格、交期、订单数量本次采购如何履约采购业务域合同金额、账套往来余额发生了什么交易合同与财务业务域统一身份不等于把所有表合成一张超级客商表。商机状态不是企业身份供应商报价也不是全公司唯一属性。如果把所有模块字段都塞进去所谓主数据中心最终会变成一个谁都想改、谁也不敢动的共享大表。同样“一家集团”也不一定是“一个主体”。母公司、子公司、分公司、结算单位可能有不同边界。业务希望统一看集团经营情况时应增加集团关系或组织关系而不是为了报表好看把不同主体合并成同一个 ID。三、先建立可复用身份再开放业务入口在产品界面中客商信息页集中维护名称、主编码、客户/供应商标志、信用代码、联系信息和结算相关字段。相比在每个模块里各新增一次用户可以先确认是否已经存在再决定补角色还是新建身份。▲ 列表是寻找已有身份的入口不应成为“一找不到就直接再建一条”的默认工作流。现有模型用两个布尔字段表示客户与供应商因此同一记录可以同时属于两种角色。创建和更新都会校验至少具备一种角色并检查分类和非空信用代码的重复情况。手工新增还会分配主身份 ID主编码由系统生成。下面摘录手工新增方法的主要逻辑省略注解与外围类定义publicLongcreatePartner(MdmPartnerSaveReqVOcreateReqVO){validateRole(createReqVO.getCustomer(),createReqVO.getSupplier());categoryService.validateCategoryExists(createReqVO.getCategoryId());validateCreditCodeUnique(null,createReqVO.getCreditCode());MdmPartnerDOpartnerBeanUtils.toBean(createReqVO,MdmPartnerDO.class);LongidallocateUnusedPartnerId();partner.setId(id);partner.setPartnerNo(String.valueOf(id));if(!StringUtils.hasText(partner.getSource())){partner.setSource(MANUAL);}partnerMapper.insert(partner);syncProducer.publishChange(MdmDataTypeEnum.PARTNER.getType(),MdmSyncOperationEnum.CREATE,partner.getId());returnpartner.getId();}这段实现值得借鉴的是角色、分类、身份分配与变更通知都在服务端完成而不是只依赖表单控件。具体采用“主编码等于主键”还是独立业务编码则是另外一个设计选择不是主数据成立的必要条件。▲ 同一身份可具有不同业务角色截图为已有学习记录打开表单不等于执行修改。对于更复杂的企业可以进一步拆出角色扩展表。例如供应商准入等级、采购组织范围、结算协议仅在供应商角色下出现客户授信额度则可能按销售组织或账套配置。两个布尔值适合简单角色判断但不能代替多组织下的全部业务授权模型。信用代码校验也要分层看。当前手工入口存在应用层重复检查但“先查询、再插入”本身不能证明两个并发创建请求一定不会重复。生产设计还应核对数据库唯一约束、租户隔离范围、空值处理与历史删除记录的规则不能把一个 Java 校验方法当成完整唯一性保证。四、外部系统的 ID 不消失而是有了明确的翻译关系一旦接入旧系统就不能要求所有来源立刻改成新 ID。旧 CRM 中客户可能是C-0086旧 ERP 中供应商可能是S-0315二者原先没有任何关联。主数据需要保留它们各自的身份同时说明它们最终指向谁。这就是来源映射的职责。映射的关键不只是sourceId而是“主数据类型 来源系统 来源业务 ID”。同一个数字86在两个系统中可能完全不是同一条记录物料 86 与客商 86 也不能混用。来源记录主数据类型目标主身份CRM_OLD / C-0086PARTNERP10001ERP_OLD / S-0315PARTNERP10001CRM_OLD / C-0087PARTNERP10002上述是解释用的映射样例第三行提醒我们系统相同、编号接近也不代表是同一家企业。映射既是接入协议也是未来排错时的证据。▲ 图中为物料的既有来源映射用于说明通用字段客商使用 PARTNER 类型与图中记录不是同一业务对象。现有入站服务先根据来源映射寻找客商找到就按策略更新未找到则走新建并建立映射。它不是一接到消息就按名称自动合并也不是先按信用代码全库搜索后无条件复用。这个区别很重要重复发送同一来源记录可以通过稳定映射命中同一目标两个不同来源各自首次发送同一家企业仍可能建立两个目标。前者是接入幂等的基础后者是实体识别问题不能用同一个“同步成功”掩盖。接入设计因此需要区分两类工作。接口负责可靠地识别同一来源记录的更新治理流程负责判断多个来源是否对应同一主体。若来源标识经常变更、环境标识不稳定映射也会失去作用所以来源系统命名应当作为长期协议维护而不是临时脚本参数。五、不要用“最后一次更新”裁决所有字段远川科技在旧 ERP 中填写了银行账号在 CRM 中填写了联系人。现在两边都要同步到客商中心谁覆盖谁如果规则只是“谁最后同步谁说了算”一个信息不完整的来源就可能把正确字段清空。即使消息时间更晚也不表示它更权威销售可能刚更新电话但并不知道财务刚审核过的收款账户。现有入站支持fill-missing与override两种模式。前者仅补本地缺失字段后者允许上游非空值覆盖。字符串空白值会被跳过对象字段的null也会被跳过。下面是实际辅助方法privatevoidsetField(booleanoverride,SupplierStringgetter,ConsumerStringsetter,Stringvalue){if(StrUtil.isBlank(value)){return;}if(override||StrUtil.isBlank(getter.get())){setter.accept(value);}}privateTvoidsetObj(booleanoverride,SupplierTgetter,ConsumerTsetter,Tvalue){if(valuenull){return;}if(override||getter.get()null){setter.accept(value);}}注意布尔值的语义false不是缺失。若供应商角色当前为false补缺模式不会因为上游传来true就自动扩展角色。这是代码中的明确行为不应想当然地把“补缺”理解成“把所有有价值的信息自动并起来”。还有一个需要在多入口治理中核对的差异手工更新会固定主编码为 ID 字符串而入站更新分支对来源编码有自己的赋值策略。不能只看手工表单禁用了编码输入就宣传“所有入口都绝对不可变”。稳定身份应优先依靠主键和映射展示编码的可变规则另行定义。如果业务规模继续扩大建议从全局覆盖策略升级为字段权责表主体名称由档案负责人确认银行信息由财务审核客户跟进联系人由销售维护。同步冲突进入待确认任务保留原值、候选值、来源与处理人。这样的治理比增加更多“覆盖开关”更容易解释。六、统一之后业务模块应当“引用”而不是再复制一份主档主数据最容易做成一个孤立模块客商页面很完整业务页面仍使用自己的旧供应商列表。结果只是多了第五份数据。真正的接入点在业务选择器和服务适配层。用户创建销售合同需要的是“启用且具有客户角色的客商”采购选择供应商需要的是“启用且具有供应商角色的客商”。查询可以共用主身份但筛选必须保留业务语义。▲ 在合同表单中打开真实往来方选择器候选来自主数据联系信息已遮盖未保存合同。RuoYi Office 的合同往来方弹窗已经使用公共PartnerSelectGrid。该组件把角色转换成客户或供应商查询条件并调用主数据分页选择接口固定带上启用状态。下面是其中角色转换逻辑及查询参数的精简摘录functionroleParams(){if(props.role1){return{customer:true};}if(props.role2){return{supplier:true};}return{};}// 位于表格的分页查询回调中returnawaitgetPartnerSelectPage({pageNo:page.currentPage,pageSize:page.pageSize,status:0,...roleParams(),...formValues,});这种复用的价值不是省一段下拉框代码而是让角色过滤、分页加载和身份返回遵循同一套语义。记录较多时也不必为了一个选择框把全部客商一次性加载到浏览器。不过前端过滤不是最终权限和有效性校验。用户选中供应商后到提交之前记录可能被停用也可能失去供应商角色。后端保存业务单据时仍应核验身份、状态与适用范围。ERP 供应商服务提供了另一个实际落地点新主数据写入入口会被拒绝并要求转向主数据读取优先查询具有供应商角色的 MDM 客商部分历史读取仍保留旧供应商表回退。页面需要的旧 DTO 由适配层转换而不是要求所有采购页面同时重写。这是一种渐进统一策略先统一新增与选择再逐步处理历史引用。它既没有假装旧表已经消失也避免为了接主数据而一次性改动所有业务功能。对于 CRM 等其他写入入口仍要逐个核实不能从“供应商适配已完成”推导出“全域已统一”。财务接入还要注意往来类型。客商身份能表达客户与供应商员工报销的往来对象却可能是员工身份。不能把所有partnerId都理解为同一张客商表的 ID应同时携带对象类型防止数字相同却指向不同实体。七、今天改了企业名称去年的合同该不该跟着变这是主数据接入之后很快会遇到的问题。远川科技更名后新建合同应该带出新名称但去年已经签署的合同展示什么如果页面每次都关联主数据表取最新名称旧合同可能悄悄“变了脸”。要分清两种查询经营汇总需要知道新旧名称属于同一身份历史单据需要说明当时使用了什么信息。这两个目标并不冲突只要把身份引用与历史快照分开。▲ 主身份负责跨期关联业务快照负责还原当时事实虚线框表示建议设计。在现有 MDM 合同台账模型中可以看到对方主身份和对方名称是两个字段。这说明模型有同时保存引用与展示信息的基础但不能仅凭两个字段就断言所有合同、付款单和凭证都已实现了完整的不可变历史快照。建议将单据分阶段处理草稿可以提示主数据已更新由用户决定是否刷新提交审核时校验关键字段正式生效后保留当时名称、税号、收款信息或相应版本引用。涉及银行账户等高风险变更应走业务确认不能只靠后台主数据一改就静默替换待付款对象。使用场景身份如何使用名称或账户如何使用新建业务单据选择当前启用主身份带出当前值未提交草稿保持主身份引用提示变化按规则刷新已生效历史单据用主身份做关联分析使用当时固化值或版本集团经营汇总按主体及集团关系归集展示当前名并保留历史追溯快照不需要复制主档的所有字段。应围绕交易解释、审批与执行需要选择最小集合避免把不相关联系人信息长期复制到每一张单据。主数据解决一致性快照解决历史性两者都需要权限和数据最小化意识。八、查重是发现线索合并是改变引用关系运营一段时间后可能发现两个“远川科技”记录。名称相同不能自动证明主体相同名称不同也不能自动证明不是同一家。门店、分支机构、历史更名和录入错误都需要不同处理。当前查重逻辑分别按非空信用代码、精确名称分组返回重复候选。它不是模糊匹配也没有做名称相似度评分或自动主体识别。使用者应把结果理解为核查清单而不是系统已经替自己作出了合并结论。现有合并方法中最关键的一步是把被合并方的来源映射改为指向保留身份ListMdmSourceMappingDOmappingssourceMappingMapper.selectListByMdmId(MdmDataTypeEnum.PARTNER.getType(),mergedId);for(MdmSourceMappingDOmapping:mappings){mapping.setMdmId(masterId);sourceMappingMapper.updateById(mapping);}MdmPartnerDOmergedpartnerMapper.selectById(mergedId);MdmPartnerDOupdateObjnewMdmPartnerDO();updateObj.setId(mergedId);updateObj.setStatus(CommonStatusEnum.DISABLE.getStatus());updateObj.setRemark(StringUtils.hasText(merged.getRemark())?merged.getRemark()已合并至#masterId:已合并至#masterId);partnerMapper.updateById(updateObj);syncProducer.publishChange(MdmDataTypeEnum.PARTNER.getType(),MdmSyncOperationEnum.UPDATE,mergedId);上面摘自带事务的mergePartner省略了参数和存在性校验。它迁移来源映射停用被合并记录并在备注写明去向没有自动重写所有历史合同、采购单和凭证的外键也没有自动把双方客户/供应商角色取并集。假设保留记录只有客户角色被合并记录只有供应商角色。映射迁移完成后采购选择器仍要求目标具有供应商角色。若不先核对角色与启用状态所谓“去重成功”可能让旧来源失去正确业务入口。因此完整合并治理建议至少包含核验主体一致性、选择保留记录、比较角色与关键字段、预览映射和引用影响、记录处理理由、合并后做来源回放验证。历史单据是否迁移应由业务制度决定不能一条批量更新把历史事实全部抹平。对于已被业务引用的身份优先停用通常比删除更容易保留追溯。但这是治理建议不能误写成当前所有删除入口都已经自动拦截引用现有服务仍有删除方法需要进一步校验引用约束与权限策略。九、变更通知能连接系统但不能替代可靠投递身份变更后下游可能需要刷新名称、停用选择项或更新本地索引。现有实现发布应用事件并在出站开关开启时通过 Redis Stream 发送主数据类型、操作和 ID。▲ 发送变更身份不等于所有下游已经接入消费者及可靠性机制需要明确落地。这里有三个边界值得保留。第一入站 MQ 与出站分发配置默认关闭部署了主数据模块并不会自动开启全公司同步。第二监听器配置事务提交后执行同时允许无事务时回退执行不能把所有调用都概括为“同一个事务内可靠分发”。第三代码中的应用事件监听并不自动意味着新开异步线程。更关键的是提交后发消息仍有失败窗口数据库已经提交进程却在发消息前退出下游可能收不到更新。这条现有链路不能被称作 Outbox也不能宣称保证不丢。如果下游一致性要求较高建议在主数据变更事务内保存待投递事件再由任务投递、重试和标记结果。消费者按事件 ID 防重按实体版本处理乱序周期性对账则负责发现长期差异。这些是工程增强方案不是当前代码片段中已经自动具备的全部能力。对仅发送 ID 的事件还需要决定消费者读取的是最新值还是事件发生时的值。刷新搜索索引通常更关心最新状态审计还原则需要事件版本或快照。先明确消费目的才能选择合适载荷不能为了“消息轻量”丢掉业务所需的信息。十、旧系统如何逐步迁移而不是一次切断全部入口主数据项目最危险的上线方式是周五把新表建好周一就要求所有模块切换并同时删除旧表。应用可能启动正常但一个历史合同详情、一个导入脚本或一条报表 SQL就足以暴露遗漏的引用。更稳妥的路线是先盘点入口谁在新增客商谁在更新银行账号哪些页面只读哪些批量导入绕过统一服务。随后统一新增入口建立来源映射让新业务优先引用主身份再逐步减少历史回退。迁移阶段主要动作进入下一阶段的条件盘点与比对列出来源、重复项、业务引用能解释每类数据的负责人建立身份映射绑定来源记录与主身份重复输入不再无故新建收口新增入口选择器和写入转向主数据新单不继续制造旧体系身份验证历史读取适配层回退、差异报表核对老单可查金额与引用可对逐步收敛处理剩余来源和旧接口已监测到回退使用持续减少迁移期间应关注的是剩余差异而不只是同步成功率。例如没有映射的历史主体还有多少同一来源是否出现多目标停用身份是否仍被新单使用旧表写入是否还在增长。只有这些指标收敛才说明统一正在真正发生。多租户与多公司边界也必须在迁移前确定。现有客商模型带租户信息意味着不能为了集团汇总就随意跨租户合并。若一个租户下需要多个公司共用身份但按公司分别控制交易资格应在主身份之外补公司级业务关系而不是把公司范围塞进名称。整个过程还要留出回退能力切换选择器可以回滚来源映射变更需要日志批量合并则需要影响清单。不要把“回滚代码”误认为“恢复已经改写的数据”两者成本完全不同。十一、如何验收主数据真的被业务用起来了一个有效的验收案例不是创建一条客商然后截图而是让同一个身份经历几种变化。可在隔离环境建立一个同时有客户和供应商角色的测试主体用不同来源编号映射到它再分别从客户和供应商选择入口查询。接着做四类验证。重复推送同一来源目标身份不应无故增加修改名称新选择项应按预期更新但历史生效单据按既定快照规则展示停用身份新业务不应继续选用历史查询仍有解释合并候选记录后来源回放应命中保留身份角色和映射不应丢失。对于冲突策略还应分别测试空字符串、null、false和有值字段。它们不是同一种“空”。如果没有这组测试补缺模式与覆盖模式很容易在产品说明里被写得相似实际运行时却产生完全不同结果。体验入口可从“主数据 → 基础数据 → 客商信息 / 来源映射”开始再到合同往来方选择和采购供应商选择查看业务消费。公开演示适合理解界面合并、删除、来源回放等操作应在授权测试环境中执行。常见问题1. 只有两个业务模块也需要独立的主数据中心吗不一定需要独立部署的服务但需要清晰的身份归属。可以先在单体内统一客商模型、选择接口和写入入口等跨系统接入复杂后再增加独立治理能力。不要把主数据等同于必须引入一套重平台。2. 信用代码相同就可以自动合并吗它是重要核查线索但仍要考虑来源质量、占位值、租户边界和业务关系。当前实现提供精确重复分组不代表已经完成所有主体核验。自动合并阈值应比提示重复更严格。3. 客商改名后报表按新名还是旧名统计统计归集应依靠身份展示名称则按场景选择。当前经营视图可显示当前名历史交易明细应保留当时事实。只按名称分组会把身份问题重新带回报表层。4. 有了 MQ同步是不是就不用对账了不是。消息投递、消费成功和业务数据正确是不同层次。映射错误、乱序覆盖、消费者长期失败仍需要差异检查发现。对账是验证同步效果不是承认架构失败。结语统一的是“谁”保留的是“发生了什么”主数据设计的价值不是给系统增加一个叫 MDM 的菜单而是让每个业务模块在谈论客户、供应商和往来单位时能够指向同一个稳定身份。身份由统一入口维护角色决定可参与的业务来源映射连接旧系统历史快照保留交易当时的信息变更机制负责把影响传递出去。把这些职责分清既能减少重复录入也不会为了统一而破坏业务差异与历史事实。如果这篇对你有用点个「在看」或收藏。演示地址https://ruoyioffice.com/webGitHub 源码https://github.com/yuqing2026/ruoyi-officeGitee 源码https://gitee.com/yqzy1688/ruoyi-office微信17156169080获取产品咨询打开演示地址直接查看系统。