:DTO 转换与 OpenFeign 调用)
第一篇我们已经把“销售订单为什么要推送 MES、前端按钮怎么触发后端接口”讲清楚了。这一篇继续往后走重点看 CRM 后端拿到订单 ID 之后如何查询真实订单、校验业务规则、组装 MES 需要的 DTO并通过 OpenFeign 调用 MES 服务。这篇文章适合三类读者刚接触 Spring Cloud / OpenFeign不知道微服务之间到底怎么互相调用的人。做过 CRUD但还不太理解“跨系统业务链路”的后端开发同学。想通过制造业 CRM / MES 项目理解订单从销售侧流转到生产侧的同学。读完这一篇你至少应该能说清楚四件事前端为什么只传订单 ID而不是传整张订单数据。CRM 为什么要重新查询真实订单并在推送前做业务校验。DTO 转换到底是在转换什么不只是“换个对象”。OpenFeign 如何把 CRM 的 Java 方法调用变成一次对 MES 的 HTTP 请求。目录一、这一篇先解决一个核心问题 二、先看 Controller后端入口只接收订单 ID 三、批量推送不是一次性乱推而是逐条处理 四、单条推送的核心syncToMes(id) 五、为什么后端必须重新查订单 六、推送前为什么要先校验 七、订单明细 DTO 转换CRM 字段变成 MES 看得懂的字段 八、订单主表 DTO 转换一次推送的主体数据 九、OpenFeign 是怎么把 CRM 调到 MES 的 十、MES 那边是谁接收 十一、成功后 CRM 怎么记录状态 十二、失败后为什么不能只抛异常 十三、把整条链路串起来 十四、为什么这部分值得重点学习 十五、读代码时建议按这个顺序看 十六、常见问题排查清单 十七、项目/面试中怎么表达这段经历 十八、本篇总结一、这一篇先解决一个核心问题很多刚接触微服务调用的同学会把“调接口”理解成一句代码mesCrmFeign.syncSaleOrder(req);但在真实业务系统里真正重要的不是这一句调用本身而是调用之前和调用之后的整套链路前端只传订单 ID ↓ CRM 后端根据 ID 查询真实订单 ↓ 校验订单是否允许推送 MES ↓ 把 CRM 订单转换成 MES 接口 DTO ↓ 通过 OpenFeign 调用 MES 服务 ↓ 记录推送成功 / 失败状态这也是制造业系统和普通 CRUD 最大的区别之一数据不是孤立存在的。CRM 的销售订单后面可能要进入 MES 的排产、生产、质检、发货流程所以每一次跨系统推送都要保证数据真实、字段完整、状态合理、失败可追踪。本文结合项目里的真实代码来讲不凭空设计接口。二、先看 Controller后端入口只接收订单 ID代码位置D:\Tools\java\work\smartmake-platform\smartmake-platform-crm\src\main\java\com\smartmake\platform\crm\controller\OrderController.java销售订单推送 MES 有两个入口单条推送和批量推送。PostMapping(/sync/{id})publicOrderBatchSyncItemRspDTOsyncToMes(PathVariableLongid){returnorderService.syncToMes(id);}PostMapping(/sync/batch)publicOrderBatchSyncRspDTObatchSyncToMes(RequestBodyListLongids){returnorderService.batchSyncToMes(ids);}这里有一个很关键的设计点前端没有把整张订单、订单明细、客户信息、金额信息都传过来而是只传订单 ID。为什么这样更合理因为前端页面上的数据只能用于展示和选择不能作为跨系统推送的最终依据。真正推送给 MES 的数据应该由 CRM 后端根据订单 ID 重新查询数据库得到。这能避免几个问题前端表格数据不完整导致推送字段缺失。用户页面停留时间太久前端缓存数据已经过期。前端被篡改参数导致推送了不可信的数据。批量推送时每条订单需要单独校验和记录结果。所以这里的 Controller 做得很轻接收 ID然后交给 Service。三、批量推送不是一次性乱推而是逐条处理代码位置D:\Tools\java\work\smartmake-platform\smartmake-platform-crm\src\main\java\com\smartmake\platform\crm\service\impl\OrderServiceImpl.java批量推送的方法是publicOrderBatchSyncRspDTObatchSyncToMes(ListLongids){OrderBatchSyncRspDTOresultnewOrderBatchSyncRspDTO();if(idsnull||ids.isEmpty()){result.setTotal(0);result.setSuccessCount(0);result.setFailureCount(0);returnresult;}intsuccessCount0;intfailureCount0;for(Longid:ids){OrderBatchSyncItemRspDTOitem;try{itemsyncToMes(id);}catch(Exceptione){itemnewOrderBatchSyncItemRspDTO();item.setOrderId(id);item.setSuccess(false);item.setMessage(friendlyMesSyncErrorV2(e));}if(Boolean.TRUE.equals(item.getSuccess())){successCount;}else{failureCount;}result.getItems().add(item);}result.setTotal(ids.size());result.setSuccessCount(successCount);result.setFailureCount(failureCount);returnresult;}这一段代码的业务价值很高。它不是简单地说“批量推送成功”或者“批量推送失败”而是每条订单单独调用syncToMes(id)最后统计总共推送几条。成功几条。失败几条。每条订单自己的推送结果。这样前端才能给用户一个更清楚的反馈比如推送完成成功 3 条失败 1 条而不是把一堆异常堆给用户看。这类设计在企业系统里很常见批量操作最好支持“部分成功、部分失败”否则一条数据异常就会影响整批业务。四、单条推送的核心syncToMes(id)真正的核心方法是syncToMes(Long id)。可以先把它理解成五步1. 根据订单 ID 查询 CRM 订单详情 2. 校验订单是否允许推送 MES 3. 转换订单明细 DTO 4. 转换订单主表 DTO 5. 调用 MES并回写 CRM 推送状态对应代码核心结构如下publicOrderBatchSyncItemRspDTOsyncToMes(Longid){OrderBatchSyncItemRspDTOresultnewOrderBatchSyncItemRspDTO();result.setOrderId(id);OrderRspDTOdtogetById(id);if(dto!null){result.setOrderCode(dto.getCode());}try{validateMesSyncOrder(dto);ListCrmOrderSyncItemReqitemReqs...;CrmOrderSyncReqreqnewCrmOrderSyncReq().setExternalId(String.valueOf(dto.getId())).setSaleOrderNum(dto.getCode()).setClientName(dto.getCustomerName()).setClientCode(dto.getCustomerCode()).setItems(itemReqs);mesCrmFeign.syncSaleOrder(req);// 推送成功后回写 CRM 的 MES 同步状态}catch(Exceptione){// 推送失败后记录失败原因}returnresult;}这一段就是本文的主线。五、为什么后端必须重新查订单这句代码很关键OrderRspDTOdtogetById(id);前端传过来的只是订单 ID后端要通过getById(id)查出完整的订单数据。这里查出来的OrderRspDTO不是只有订单主表字段还会包含订单明细等关联信息。因为 MES 生产需要的不只是“有一张订单”还要知道订单编号是什么。客户是谁。预计交货日期是什么。收货地址是什么。订单金额是多少。订单里有哪些产品。每个产品的数量、规格、单位、价格是多少。这就是业务系统里经常说的“真实数据关联”。不是前端表格上看到什么就推什么而是后端按订单 ID 拉取当前数据库里的完整真实数据。六、推送前为什么要先校验代码里有一个专门的方法privatevoidvalidateMesSyncOrder(OrderRspDTOdto){if(dtonull){thrownewIllegalArgumentException(订单不存在无法推送 MES);}if(!MES_SYNCABLE_STATUSES.contains(dto.getStatus())){thrownewIllegalArgumentException(当前订单状态不允许推送 MES);}if(StrUtil.isBlank(dto.getCode())){thrownewIllegalArgumentException(订单编号为空无法推送 MES);}if(StrUtil.isBlank(dto.getCustomerName())){thrownewIllegalArgumentException(客户名称为空请完善客户信息);}if(dto.getItems()null||dto.getItems().isEmpty()){thrownewIllegalArgumentException(订单明细为空无法推送 MES);}}实际代码里还会继续校验每一行订单明细例如产品编号、产品名称、数量、价格等。当前项目里允许推送 MES 的订单状态是privatestaticfinalSetStringMES_SYNCABLE_STATUSESSet.of(confirmed,producing);也就是说不是所有订单都能推送 MES。只有已经确认或已进入生产相关阶段的订单才有资格同步到 MES。这一步非常重要。如果不校验可能会出现这些问题草稿订单被推送到 MES生产系统拿到未确认需求。没有客户名称MES 无法生成完整销售订单。没有产品明细MES 不知道生产什么。产品数量异常后续排产和库存都会出错。所以跨系统调用前一定不要只想“能不能调通接口”还要先问“这份数据有没有资格进入下一个系统”七、订单明细 DTO 转换CRM 字段变成 MES 看得懂的字段订单明细转换用的是CrmOrderSyncItemReq。代码位置D:\Tools\java\work\smartmake-platform\smartmake-platform-feign\smartmake-platform-mes-api\src\main\java\com\smartmake\platform\mes\feign\domain\req\CrmOrderSyncItemReq.java这个 DTO 主要描述订单里的产品行publicclassCrmOrderSyncItemReq{privateIntegersequence;privateStringproductId;privateStringproductCode;privateStringproductName;privateStringspec;privateStringunit;privateBigDecimalqty;privateBigDecimalprice;privateBigDecimaltaxRate;privateBigDecimalamount;privateBigDecimaltaxAmount;privateBigDecimaltaxInclusiveAmount;privateStringremark;}CRM 里每一行订单明细会被转换成 MES 接口里的每一行产品明细ListCrmOrderSyncItemReqitemReqsdto.getItems().stream().map(item-newCrmOrderSyncItemReq().setSequence(item.getSequence()).setProductId(item.getProductId()!null?String.valueOf(item.getProductId()):null).setProductCode(item.getProductCode()).setProductName(item.getProductName()).setSpec(item.getSpec()).setUnit(item.getUnit()).setQty(item.getQty()).setPrice(item.getPrice()).setTaxRate(item.getTaxRate()).setAmount(item.getAmount()).setTaxAmount(item.getTaxAmount()).setTaxInclusiveAmount(item.getTaxInclusiveAmount()).setRemark(item.getRemark())).collect(Collectors.toList());这里要理解一个点DTO 转换不是机械复制字段而是在做两个系统之间的数据协议适配。CRM 关心的是销售订单MES 关心的是后续生产执行。所以 CRM 推过去的明细至少要让 MES 知道生产哪个产品productCode、productName。产品规格是什么spec。数量是多少qty。单位是什么unit。金额、税额如何amount、taxAmount、taxInclusiveAmount。明细备注是什么remark。如果这些字段没有处理好MES 即使接到了请求也很难进入后续业务流程。八、订单主表 DTO 转换一次推送的主体数据订单主表转换用的是CrmOrderSyncReq。代码位置D:\Tools\java\work\smartmake-platform\smartmake-platform-feign\smartmake-platform-mes-api\src\main\java\com\smartmake\platform\mes\feign\domain\req\CrmOrderSyncReq.java主要字段如下publicclassCrmOrderSyncReq{privateStringexternalId;privateStringsaleOrderNum;privateStringclientCode;privateStringclientName;privateStringsource;privateStringsourceSystem;privateStringdeliveryDate;privateStringaddress;privateBigDecimalincludeTax;privateBigDecimalnoIncludeTax;privateStringisTax;privateStringstatus;privateStringremark;privateListCrmOrderSyncItemReqitems;}CRM 组装请求的核心代码是CrmOrderSyncReqreqnewCrmOrderSyncReq().setExternalId(String.valueOf(dto.getId())).setSaleOrderNum(dto.getCode()).setClientName(dto.getCustomerName()).setClientCode(dto.getCustomerCode()).setDeliveryDate(dto.getDeliveryDate()!null?newSimpleDateFormat(yyyy-MM-dd).format(dto.getDeliveryDate()):null).setNoIncludeTax(dto.getTotalAmount()).setIncludeTax(dto.getTaxInclusiveAmount()).setIsTax(1).setAddress(dto.getReceiveAddress()).setRemark(dto.getRemark()).setSource(CRM).setSourceSystem(smartmake-CRM).setStatus(0).setItems(itemReqs);这里最值得注意的是externalId。.setExternalId(String.valueOf(dto.getId()))它表示 CRM 订单在 MES 里的外部系统 ID。简单说就是 MES 要知道这张单来自 CRM 的哪一条订单。这对后续很重要MES 可以根据externalId判断是不是同一张 CRM 订单。CRM 后续查询 MES 进度时也可以通过这个 ID 找回对应的 MES 订单。如果后续要做幂等、防重复推送、状态回写externalId就是关键关联字段。所以不要只把它当成一个普通字符串它是 CRM 与 MES 订单关系的锚点。九、OpenFeign 是怎么把 CRM 调到 MES 的代码位置D:\Tools\java\work\smartmake-platform\smartmake-platform-feign\smartmake-platform-mes-api\src\main\java\com\smartmake\platform\mes\feign\MesCrmFeign.javaFeign 接口是这样定义的FeignClient(nameFeignConstants.MES_FEIGN_NAME)publicinterfaceMesCrmFeign{PostMapping(/order/sale/inner/syncFromCrm)voidsyncSaleOrder(RequestBodyCrmOrderSyncReqreq);GetMapping(/order/sale/inner/progress/by-external-id/{externalId})MesOrderProgressRspgetOrderProgress(PathVariable(externalId)StringexternalId);}这里有两个重点。第一个重点FeignClient(nameFeignConstants.MES_FEIGN_NAME)。它表示这个接口不是普通 Java 接口而是一个远程服务客户端。CRM 服务调用这个接口时OpenFeign 会根据服务名找到 MES 服务。第二个重点PostMapping(/order/sale/inner/syncFromCrm)。它表示 CRM 实际请求 MES 的这个内部接口POST /order/sale/inner/syncFromCrm于是 CRM 里的这一句mesCrmFeign.syncSaleOrder(req);就不再是普通方法调用而是一次跨服务 HTTP 调用。很多新手第一次看 OpenFeign容易误解成“它只是帮我少写了一个 HTTP 工具类”。这个理解不算错但不够完整。在这个项目里OpenFeign 至少承担了三层作用用 Java 接口声明远程服务契约让 CRM 侧知道可以调用 MES 哪些内部接口。通过服务名找到目标服务而不是在业务代码里写死 IP 和端口。把 Java 对象CrmOrderSyncReq序列化成请求体发送到 MES 对应 Controller。所以MesCrmFeign本质上是 CRM 和 MES 之间的“接口契约层”。如果这个契约变了比如字段名、路径、请求方式发生变化CRM 和 MES 两边都要一起确认。可以把它理解成CRM OrderServiceImpl ↓ MesCrmFeign.syncSaleOrder(req) ↓ OpenFeign 根据 MES 服务名找到 MES 实例 ↓ 发送 POST /order/sale/inner/syncFromCrm ↓ MES SaleController 接收 CrmOrderSyncReq十、MES 那边是谁接收MES 接收接口在D:\Tools\java\work\smartmake-platform\smartmake-platform-mes\src\main\java\com\smartmake\platform\mes\controller\order\SaleController.java代码是PostMapping(/inner/syncFromCrm)publicvoidsyncFromCrm(RequestBodyCrmOrderSyncReqreq){saleService.syncFromCrm(req);}注意这里的方法路径是/inner/syncFromCrm而 Feign 里写的是/order/sale/inner/syncFromCrm这是因为SaleController自己通常还有类级别路径Feign 调用时写的是完整访问路径。这一篇我们重点讲 CRM 侧的 DTO 转换和 Feign 调用。MES 接收之后如何落库、如何避免重复同步、如何把生产进度回传给 CRM可以放到第三篇继续讲。十一、成功后 CRM 怎么记录状态调用 MES 成功后CRM 会更新订单的 MES 同步状态lambdaUpdate().eq(Order::getId,id).set(Order::getMesSyncStatus,success).set(Order::getMesSyncTime,newDate()).set(Order::getMesSyncError,null).update();这里的三个字段非常重要mesSyncStatus 推送状态 mesSyncTime 推送时间 mesSyncError 推送失败原因这就是为什么前端销售订单列表可以展示“是否推送 MES、什么时候推送、失败原因是什么”。如果没有这些字段用户点了推送按钮之后就很难知道到底推送成功没有。失败原因是什么。能不能重新推送。哪些订单已经进入 MES。企业系统不是只要按钮点得动还要让业务人员能追踪结果。十二、失败后为什么不能只抛异常失败时代码会把异常转成更友好的信息然后记录到订单上StringfriendlyMessagefriendlyMesSyncErrorV2(e);result.setSuccess(false);result.setMessage(friendlyMessage);if(dto!null){patch.setMesSyncStatus(failed);patch.setMesSyncTime(newDate());patch.setMesSyncError(friendlyMessage);baseMapper.updateById(patch);}这一点也很关键。如果直接把 Java 异常返回给前端用户看到的可能是Connection refused NullPointerException FeignException这些信息对开发排查有用但对业务人员没有帮助。更合理的方式是后端日志里保留完整异常方便开发排查。前端展示业务人员能理解的失败原因。数据库记录mesSyncError方便后续查看和重试。这也是跨系统调用里非常重要的一点失败要可解释、可追踪、可恢复。十三、把整条链路串起来这一篇的完整链路可以这样理解前端选择订单 ↓ POST /crm/order/sync/batch ↓ OrderController.batchSyncToMes(ids) ↓ OrderServiceImpl.batchSyncToMes(ids) ↓ 逐条调用 syncToMes(id) ↓ getById(id) 查询 CRM 真实订单 ↓ validateMesSyncOrder(dto) 做业务校验 ↓ 订单明细转换成 CrmOrderSyncItemReq ↓ 订单主表转换成 CrmOrderSyncReq ↓ MesCrmFeign.syncSaleOrder(req) ↓ MES SaleController.syncFromCrm(req) ↓ CRM 记录 mesSyncStatus / mesSyncTime / mesSyncError如果用一句话总结CRM 不是把前端表格数据直接扔给 MES而是根据订单 ID 查询真实订单完成业务校验和 DTO 转换再通过 OpenFeign 调用 MES并把同步结果记录回 CRM。十四、为什么这部分值得重点学习这段代码看起来只是“订单推送 MES”但它背后包含了很多真实项目里非常有价值的能力跨服务调用CRM 通过 OpenFeign 调 MES。真实数据组装前端只传 ID后端重新查完整订单。DTO 协议设计CRM 字段转换成 MES 能理解的请求对象。业务校验不是所有订单都允许进入 MES。批量处理支持成功几条、失败几条的结构化反馈。状态追踪记录 MES 同步状态、同步时间、失败原因。异常处理把技术异常转换成业务可理解的提示。这些能力比单纯写一个增删改查页面更有业务含金量。十五、读代码时建议按这个顺序看如果你想在 IDEA 里跟着项目代码自己看一遍可以按这个顺序1. OrderController.java 看 /sync/{id} 和 /sync/batch 两个入口 2. OrderService.java 看 syncToMes 和 batchSyncToMes 方法定义 3. OrderServiceImpl.java 重点看 batchSyncToMes、syncToMes、validateMesSyncOrder 4. CrmOrderSyncReq.java 看 CRM 推给 MES 的订单主表字段 5. CrmOrderSyncItemReq.java 看 CRM 推给 MES 的订单明细字段 6. MesCrmFeign.java 看 CRM 如何通过 OpenFeign 调 MES 7. SaleController.java 看 MES 侧如何接收 CRM 推送这样看代码会比直接在项目里乱搜更清晰因为你是在沿着真实业务链路走。十六、常见问题排查清单如果销售订单推送 MES 出问题可以按下面顺序排查。1. 前端有没有把订单 ID 传到后端先看浏览器 Network请求是否打到了POST /crm/order/sync/batch请求体是不是类似[1001,1002,1003]如果这里没有 ID后端就没有办法查询真实订单。2. Gateway 路由有没有转到 CRM 服务第一篇已经讲过前端访问的是/crm/order/sync/batch经过网关后才进入 CRM 的/order/sync/batch。如果网关路由、服务注册、前端代理配置不对请求可能根本到不了 CRM 服务。3. CRM 订单状态是否允许推送当前代码只允许这些状态推送Set.of(confirmed,producing)如果订单还是草稿、已取消、已关闭就不应该进入 MES。4. 订单数据是否完整重点看这些字段订单编号 客户名称 订单明细 产品编号 产品名称 产品数量这些数据缺失时不应该继续推送 MES。因为 MES 拿到不完整订单后后面的排产、生产、质检都会受影响。5. Feign 是否真的调到了 MES看 CRM 日志里有没有 Feign 调用异常比如Connection refused Read timed out Load balancer does not contain an instance这些通常说明不是 DTO 转换问题而是 MES 服务、注册中心、网络、网关或服务名配置的问题。6. CRM 有没有写回同步状态推送成功后应该能看到mesSyncStatus success mesSyncTime 当前推送时间 mesSyncError null推送失败后应该能看到mesSyncStatus failed mesSyncTime 当前推送时间 mesSyncError 失败原因如果状态没有写回前端就很难判断订单是否已经同步到 MES。十七、项目/面试中怎么表达这段经历如果你要把这段经历写到项目总结、日报或者面试里不建议只写完成销售订单推送 MES 接口。这个表达太浅了听起来像只是调了一个接口。更好的表达是参与 CRM 销售订单推送 MES 链路建设负责订单推送前的数据校验、CRM 订单到 MES 同步 DTO 的字段映射、OpenFeign 跨服务调用以及同步结果状态回写。支持批量推送后按订单维度返回成功/失败结果并记录 MES 同步状态、同步时间和失败原因提升订单从销售侧进入生产侧的可追踪性和异常处理能力。如果面试官继续追问“你具体做了什么”可以按这条线回答前端只传订单 IDCRM 后端根据 ID 查询真实订单校验订单状态和明细完整性再把订单主表和明细转换成 MES 约定的 CrmOrderSyncReq / CrmOrderSyncItemReq通过 OpenFeign 调 MES 内部接口。调用成功后写入 success 和同步时间调用失败后记录 failed 和失败原因方便前端展示和后续重试。这就能体现你理解的不只是接口调用还有业务链路、真实数据一致性、状态流转和异常追踪。十八、本篇总结这一篇我们主要讲清楚了 CRM 推送 MES 的后端核心链路订单 ID → 查询真实订单 → 业务校验 → DTO 转换 → OpenFeign 调 MES → 回写同步状态其中最重要的不是 Feign 那一行代码而是围绕它前后的业务处理为什么不能相信前端传完整数据。为什么推送前要校验订单状态和订单明细。为什么要用 DTO 做系统之间的数据协议。为什么成功和失败都要写回 CRM。理解了这一篇再去看第三篇 MES 如何接收、如何避免重复同步、如何把生产进度和状态回传给 CRM就会顺很多。