☰
SpringBoot+Vue3 企业主数据设计:客户、供应商、合同与财务如何共用一套身份
2026/10/1 10:38:19 网站建设 项目流程

SpringBoot+Vue3 企业主数据设计:客户、供应商、合同与财务如何共用一套身份

🌐文档地址: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());MdmPartnerDOpartner=BeanUtils.toBean(createReqVO,MdmPartnerDO.class);Longid=allocateUnusedPartnerId();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-0086PARTNERP10001
ERP_OLD / S-0315PARTNERP10001
CRM_OLD / C-0087PARTNERP10002

上述是解释用的映射样例,第三行提醒我们:系统相同、编号接近,也不代表是同一家企业。映射既是接入协议,也是未来排错时的证据。

▲ 图中为物料的既有来源映射,用于说明通用字段;客商使用 PARTNER 类型,与图中记录不是同一业务对象。

现有入站服务先根据来源映射寻找客商;找到就按策略更新,未找到则走新建并建立映射。它不是一接到消息就按名称自动合并,也不是先按信用代码全库搜索后无条件复用。

这个区别很重要:重复发送同一来源记录,可以通过稳定映射命中同一目标;两个不同来源各自首次发送同一家企业,仍可能建立两个目标。前者是接入幂等的基础,后者是实体识别问题,不能用同一个“同步成功”掩盖。

接入设计因此需要区分两类工作。接口负责可靠地识别同一来源记录的更新;治理流程负责判断多个来源是否对应同一主体。若来源标识经常变更、环境标识不稳定,映射也会失去作用,所以来源系统命名应当作为长期协议维护,而不是临时脚本参数。

五、不要用“最后一次更新”裁决所有字段

远川科技在旧 ERP 中填写了银行账号,在 CRM 中填写了联系人。现在两边都要同步到客商中心,谁覆盖谁?

如果规则只是“谁最后同步谁说了算”,一个信息不完整的来源就可能把正确字段清空。即使消息时间更晚,也不表示它更权威:销售可能刚更新电话,但并不知道财务刚审核过的收款账户。

现有入站支持fill-missing与override两种模式。前者仅补本地缺失字段;后者允许上游非空值覆盖。字符串空白值会被跳过,对象字段的null也会被跳过。下面是实际辅助方法:

privatevoidsetField(booleanoverride,Supplier<String>getter,Consumer<String>setter,Stringvalue){if(StrUtil.isBlank(value)){return;}if(override||StrUtil.isBlank(getter.get())){setter.accept(value);}}private<T>voidsetObj(booleanoverride,Supplier<T>getter,Consumer<T>setter,Tvalue){if(value==null){return;}if(override||getter.get()==null){setter.accept(value);}}

注意布尔值的语义:false不是缺失。若供应商角色当前为false,补缺模式不会因为上游传来true就自动扩展角色。这是代码中的明确行为,不应想当然地把“补缺”理解成“把所有有价值的信息自动并起来”。

还有一个需要在多入口治理中核对的差异:手工更新会固定主编码为 ID 字符串,而入站更新分支对来源编码有自己的赋值策略。不能只看手工表单禁用了编码输入,就宣传“所有入口都绝对不可变”。稳定身份应优先依靠主键和映射,展示编码的可变规则另行定义。

如果业务规模继续扩大,建议从全局覆盖策略升级为字段权责表:主体名称由档案负责人确认,银行信息由财务审核,客户跟进联系人由销售维护。同步冲突进入待确认任务,保留原值、候选值、来源与处理人。这样的治理比增加更多“覆盖开关”更容易解释。

六、统一之后,业务模块应当“引用”,而不是再复制一份主档

主数据最容易做成一个孤立模块:客商页面很完整,业务页面仍使用自己的旧供应商列表。结果只是多了第五份数据。

真正的接入点在业务选择器和服务适配层。用户创建销售合同,需要的是“启用且具有客户角色的客商”;采购选择供应商,需要的是“启用且具有供应商角色的客商”。查询可以共用主身份,但筛选必须保留业务语义。

▲ 在合同表单中打开真实往来方选择器,候选来自主数据;联系信息已遮盖,未保存合同。

RuoYi Office 的合同往来方弹窗已经使用公共PartnerSelectGrid。该组件把角色转换成客户或供应商查询条件,并调用主数据分页选择接口,固定带上启用状态。下面是其中角色转换逻辑及查询参数的精简摘录:

functionroleParams(){if(props.role===1){return{customer:true};}if(props.role===2){return{supplier:true};}return{};}// 位于表格的分页查询回调中returnawaitgetPartnerSelectPage({pageNo:page.currentPage,pageSize:page.pageSize,status:0,...roleParams(),...formValues,});

这种复用的价值不是省一段下拉框代码,而是让角色过滤、分页加载和身份返回遵循同一套语义。记录较多时,也不必为了一个选择框把全部客商一次性加载到浏览器。

不过,前端过滤不是最终权限和有效性校验。用户选中供应商后到提交之前,记录可能被停用,也可能失去供应商角色。后端保存业务单据时仍应核验身份、状态与适用范围。

ERP 供应商服务提供了另一个实际落地点:新主数据写入入口会被拒绝并要求转向主数据;读取优先查询具有供应商角色的 MDM 客商;部分历史读取仍保留旧供应商表回退。页面需要的旧 DTO 由适配层转换,而不是要求所有采购页面同时重写。

这是一种渐进统一策略:先统一新增与选择,再逐步处理历史引用。它既没有假装旧表已经消失,也避免为了接主数据而一次性改动所有业务功能。对于 CRM 等其他写入入口,仍要逐个核实,不能从“供应商适配已完成”推导出“全域已统一”。

财务接入还要注意往来类型。客商身份能表达客户与供应商,员工报销的往来对象却可能是员工身份。不能把所有partnerId都理解为同一张客商表的 ID;应同时携带对象类型,防止数字相同却指向不同实体。

七、今天改了企业名称,去年的合同该不该跟着变

这是主数据接入之后很快会遇到的问题。远川科技更名后,新建合同应该带出新名称,但去年已经签署的合同展示什么?如果页面每次都关联主数据表取最新名称,旧合同可能悄悄“变了脸”。

要分清两种查询:经营汇总需要知道新旧名称属于同一身份;历史单据需要说明当时使用了什么信息。这两个目标并不冲突,只要把身份引用与历史快照分开。

▲ 主身份负责跨期关联;业务快照负责还原当时事实,虚线框表示建议设计。

在现有 MDM 合同台账模型中,可以看到对方主身份和对方名称是两个字段。这说明模型有同时保存引用与展示信息的基础,但不能仅凭两个字段,就断言所有合同、付款单和凭证都已实现了完整的不可变历史快照。

建议将单据分阶段处理:草稿可以提示主数据已更新,由用户决定是否刷新;提交审核时校验关键字段;正式生效后保留当时名称、税号、收款信息或相应版本引用。涉及银行账户等高风险变更,应走业务确认,不能只靠后台主数据一改就静默替换待付款对象。

使用场景身份如何使用名称或账户如何使用
新建业务单据选择当前启用主身份带出当前值
未提交草稿保持主身份引用提示变化,按规则刷新
已生效历史单据用主身份做关联分析使用当时固化值或版本
集团经营汇总按主体及集团关系归集展示当前名并保留历史追溯

快照不需要复制主档的所有字段。应围绕交易解释、审批与执行需要选择最小集合,避免把不相关联系人信息长期复制到每一张单据。主数据解决一致性,快照解决历史性,两者都需要权限和数据最小化意识。

八、查重是发现线索,合并是改变引用关系

运营一段时间后,可能发现两个“远川科技”记录。名称相同不能自动证明主体相同,名称不同也不能自动证明不是同一家。门店、分支机构、历史更名和录入错误,都需要不同处理。

当前查重逻辑分别按非空信用代码、精确名称分组,返回重复候选。它不是模糊匹配,也没有做名称相似度评分或自动主体识别。使用者应把结果理解为核查清单,而不是系统已经替自己作出了合并结论。

现有合并方法中最关键的一步,是把被合并方的来源映射改为指向保留身份:

List<MdmSourceMappingDO>mappings=sourceMappingMapper.selectListByMdmId(MdmDataTypeEnum.PARTNER.getType(),mergedId);for(MdmSourceMappingDOmapping:mappings){mapping.setMdmId(masterId);sourceMappingMapper.updateById(mapping);}MdmPartnerDOmerged=partnerMapper.selectById(mergedId);MdmPartnerDOupdateObj=newMdmPartnerDO();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/web
📦GitHub 源码:https://github.com/yuqing2026/ruoyi-office
📦Gitee 源码:https://gitee.com/yqzy1688/ruoyi-office
💬微信:17156169080(获取产品咨询)

打开演示地址直接查看系统。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询