第一篇我们已经把"销售订单为什么要推送 MES、前端按钮怎么触发后端接口"讲清楚了。
这一篇继续往后走,重点看 CRM 后端拿到订单 ID 之后,如何查询真实订单、校验业务规则、组装 MES 需要的 DTO,并通过 OpenFeign 调用 MES 服务。
这篇文章适合三类读者:
- 刚接触 Spring Cloud / OpenFeign,不知道微服务之间到底怎么互相调用的人。
- 做过 CRUD,但还不太理解"跨系统业务链路"的后端开发同学。
- 想通过制造业 CRM / MES 项目,理解订单从销售侧流转到生产侧的同学。
读完这一篇,你至少应该能说清楚四件事:
- 前端为什么只传订单 ID,而不是传整张订单数据。
- CRM 为什么要重新查询真实订单,并在推送前做业务校验。
- DTO 转换到底是在转换什么,不只是"换个对象"。
- OpenFeign 如何把 CRM 的 Java 方法调用变成一次对 MES 的 HTTP 请求。

目录
text
一、这一篇先解决一个核心问题
二、先看 Controller:后端入口只接收订单 ID
三、批量推送不是一次性乱推,而是逐条处理
四、单条推送的核心:syncToMes(id)
五、为什么后端必须重新查订单?
六、推送前为什么要先校验?
七、订单明细 DTO 转换:CRM 字段变成 MES 看得懂的字段
八、订单主表 DTO 转换:一次推送的主体数据
九、OpenFeign 是怎么把 CRM 调到 MES 的?
十、MES 那边是谁接收?
十一、成功后 CRM 怎么记录状态?
十二、失败后为什么不能只抛异常?
十三、把整条链路串起来
十四、为什么这部分值得重点学习?
十五、读代码时建议按这个顺序看
十六、常见问题排查清单
十七、项目/面试中怎么表达这段经历
十八、本篇总结
一、这一篇先解决一个核心问题
很多刚接触微服务调用的同学,会把"调接口"理解成一句代码:
java
mesCrmFeign.syncSaleOrder(req);
但在真实业务系统里,真正重要的不是这一句调用本身,而是调用之前和调用之后的整套链路:
text
前端只传订单 ID
↓
CRM 后端根据 ID 查询真实订单
↓
校验订单是否允许推送 MES
↓
把 CRM 订单转换成 MES 接口 DTO
↓
通过 OpenFeign 调用 MES 服务
↓
记录推送成功 / 失败状态
这也是制造业系统和普通 CRUD 最大的区别之一:数据不是孤立存在的。CRM 的销售订单,后面可能要进入 MES 的排产、生产、质检、发货流程,所以每一次跨系统推送都要保证数据真实、字段完整、状态合理、失败可追踪。
本文结合项目里的真实代码来讲,不凭空设计接口。
二、先看 Controller:后端入口只接收订单 ID
代码位置:
text
D:\Tools\java\work\smartmake-platform\smartmake-platform-crm\src\main\java\com\smartmake\platform\crm\controller\OrderController.java
销售订单推送 MES 有两个入口:单条推送和批量推送。
java
@PostMapping("/sync/{id}")
public OrderBatchSyncItemRspDTO syncToMes(@PathVariable Long id) {
return orderService.syncToMes(id);
}
@PostMapping("/sync/batch")
public OrderBatchSyncRspDTO batchSyncToMes(@RequestBody List<Long> ids) {
return orderService.batchSyncToMes(ids);
}
这里有一个很关键的设计点:前端没有把整张订单、订单明细、客户信息、金额信息都传过来,而是只传订单 ID。
为什么这样更合理?
因为前端页面上的数据只能用于展示和选择,不能作为跨系统推送的最终依据。真正推送给 MES 的数据,应该由 CRM 后端根据订单 ID 重新查询数据库得到。
这能避免几个问题:
- 前端表格数据不完整,导致推送字段缺失。
- 用户页面停留时间太久,前端缓存数据已经过期。
- 前端被篡改参数,导致推送了不可信的数据。
- 批量推送时每条订单需要单独校验和记录结果。
所以这里的 Controller 做得很轻:接收 ID,然后交给 Service。
三、批量推送不是一次性乱推,而是逐条处理
代码位置:
text
D:\Tools\java\work\smartmake-platform\smartmake-platform-crm\src\main\java\com\smartmake\platform\crm\service\impl\OrderServiceImpl.java
批量推送的方法是:
java
public OrderBatchSyncRspDTO batchSyncToMes(List<Long> ids) {
OrderBatchSyncRspDTO result = new OrderBatchSyncRspDTO();
if (ids == null || ids.isEmpty()) {
result.setTotal(0);
result.setSuccessCount(0);
result.setFailureCount(0);
return result;
}
int successCount = 0;
int failureCount = 0;
for (Long id : ids) {
OrderBatchSyncItemRspDTO item;
try {
item = syncToMes(id);
} catch (Exception e) {
item = new OrderBatchSyncItemRspDTO();
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);
return result;
}
这一段代码的业务价值很高。
它不是简单地说"批量推送成功"或者"批量推送失败",而是每条订单单独调用 syncToMes(id),最后统计:
- 总共推送几条。
- 成功几条。
- 失败几条。
- 每条订单自己的推送结果。
这样前端才能给用户一个更清楚的反馈,比如:
text
推送完成:成功 3 条,失败 1 条
而不是把一堆异常堆给用户看。
这类设计在企业系统里很常见:批量操作最好支持"部分成功、部分失败",否则一条数据异常就会影响整批业务。
四、单条推送的核心:syncToMes(id)
真正的核心方法是 syncToMes(Long id)。
可以先把它理解成五步:
text
1. 根据订单 ID 查询 CRM 订单详情
2. 校验订单是否允许推送 MES
3. 转换订单明细 DTO
4. 转换订单主表 DTO
5. 调用 MES,并回写 CRM 推送状态
对应代码核心结构如下:
java
public OrderBatchSyncItemRspDTO syncToMes(Long id) {
OrderBatchSyncItemRspDTO result = new OrderBatchSyncItemRspDTO();
result.setOrderId(id);
OrderRspDTO dto = getById(id);
if (dto != null) {
result.setOrderCode(dto.getCode());
}
try {
validateMesSyncOrder(dto);
List<CrmOrderSyncItemReq> itemReqs = ...;
CrmOrderSyncReq req = new CrmOrderSyncReq()
.setExternalId(String.valueOf(dto.getId()))
.setSaleOrderNum(dto.getCode())
.setClientName(dto.getCustomerName())
.setClientCode(dto.getCustomerCode())
.setItems(itemReqs);
mesCrmFeign.syncSaleOrder(req);
// 推送成功后,回写 CRM 的 MES 同步状态
} catch (Exception e) {
// 推送失败后,记录失败原因
}
return result;
}
这一段就是本文的主线。
五、为什么后端必须重新查订单?
这句代码很关键:
java
OrderRspDTO dto = getById(id);
前端传过来的只是订单 ID,后端要通过 getById(id) 查出完整的订单数据。
这里查出来的 OrderRspDTO 不是只有订单主表字段,还会包含订单明细等关联信息。因为 MES 生产需要的不只是"有一张订单",还要知道:
- 订单编号是什么。
- 客户是谁。
- 预计交货日期是什么。
- 收货地址是什么。
- 订单金额是多少。
- 订单里有哪些产品。
- 每个产品的数量、规格、单位、价格是多少。
这就是业务系统里经常说的"真实数据关联"。不是前端表格上看到什么就推什么,而是后端按订单 ID 拉取当前数据库里的完整真实数据。
六、推送前为什么要先校验?
代码里有一个专门的方法:
java
private void validateMesSyncOrder(OrderRspDTO dto) {
if (dto == null) {
throw new IllegalArgumentException("订单不存在,无法推送 MES");
}
if (!MES_SYNCABLE_STATUSES.contains(dto.getStatus())) {
throw new IllegalArgumentException("当前订单状态不允许推送 MES");
}
if (StrUtil.isBlank(dto.getCode())) {
throw new IllegalArgumentException("订单编号为空,无法推送 MES");
}
if (StrUtil.isBlank(dto.getCustomerName())) {
throw new IllegalArgumentException("客户名称为空,请完善客户信息");
}
if (dto.getItems() == null || dto.getItems().isEmpty()) {
throw new IllegalArgumentException("订单明细为空,无法推送 MES");
}
}
实际代码里还会继续校验每一行订单明细,例如产品编号、产品名称、数量、价格等。
当前项目里允许推送 MES 的订单状态是:
java
private static final Set<String> MES_SYNCABLE_STATUSES = Set.of("confirmed", "producing");
也就是说,不是所有订单都能推送 MES。只有已经确认或已进入生产相关阶段的订单,才有资格同步到 MES。
这一步非常重要。
如果不校验,可能会出现这些问题:
- 草稿订单被推送到 MES,生产系统拿到未确认需求。
- 没有客户名称,MES 无法生成完整销售订单。
- 没有产品明细,MES 不知道生产什么。
- 产品数量异常,后续排产和库存都会出错。
所以跨系统调用前,一定不要只想"能不能调通接口",还要先问:"这份数据有没有资格进入下一个系统?"
七、订单明细 DTO 转换:CRM 字段变成 MES 看得懂的字段
订单明细转换用的是 CrmOrderSyncItemReq。
代码位置:
text
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 主要描述订单里的产品行:
java
public class CrmOrderSyncItemReq {
private Integer sequence;
private String productId;
private String productCode;
private String productName;
private String spec;
private String unit;
private BigDecimal qty;
private BigDecimal price;
private BigDecimal taxRate;
private BigDecimal amount;
private BigDecimal taxAmount;
private BigDecimal taxInclusiveAmount;
private String remark;
}
CRM 里每一行订单明细,会被转换成 MES 接口里的每一行产品明细:
java
List<CrmOrderSyncItemReq> itemReqs = dto.getItems()
.stream()
.map(item -> new CrmOrderSyncItemReq()
.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。
代码位置:
text
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
主要字段如下:
java
public class CrmOrderSyncReq {
private String externalId;
private String saleOrderNum;
private String clientCode;
private String clientName;
private String source;
private String sourceSystem;
private String deliveryDate;
private String address;
private BigDecimal includeTax;
private BigDecimal noIncludeTax;
private String isTax;
private String status;
private String remark;
private List<CrmOrderSyncItemReq> items;
}
CRM 组装请求的核心代码是:
java
CrmOrderSyncReq req = new CrmOrderSyncReq()
.setExternalId(String.valueOf(dto.getId()))
.setSaleOrderNum(dto.getCode())
.setClientName(dto.getCustomerName())
.setClientCode(dto.getCustomerCode())
.setDeliveryDate(dto.getDeliveryDate() != null
? new SimpleDateFormat("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。
java
.setExternalId(String.valueOf(dto.getId()))
它表示 CRM 订单在 MES 里的外部系统 ID。简单说,就是 MES 要知道这张单来自 CRM 的哪一条订单。
这对后续很重要:
- MES 可以根据
externalId判断是不是同一张 CRM 订单。 - CRM 后续查询 MES 进度时,也可以通过这个 ID 找回对应的 MES 订单。
- 如果后续要做幂等、防重复推送、状态回写,
externalId就是关键关联字段。
所以不要只把它当成一个普通字符串,它是 CRM 与 MES 订单关系的锚点。

九、OpenFeign 是怎么把 CRM 调到 MES 的?
代码位置:
text
D:\Tools\java\work\smartmake-platform\smartmake-platform-feign\smartmake-platform-mes-api\src\main\java\com\smartmake\platform\mes\feign\MesCrmFeign.java
Feign 接口是这样定义的:
java
@FeignClient(name = FeignConstants.MES_FEIGN_NAME)
public interface MesCrmFeign {
@PostMapping("/order/sale/inner/syncFromCrm")
void syncSaleOrder(@RequestBody CrmOrderSyncReq req);
@GetMapping("/order/sale/inner/progress/by-external-id/{externalId}")
MesOrderProgressRsp getOrderProgress(@PathVariable("externalId") String externalId);
}
这里有两个重点。
第一个重点:@FeignClient(name=FeignConstants.MES_FEIGN_NAME)。
它表示这个接口不是普通 Java 接口,而是一个远程服务客户端。CRM 服务调用这个接口时,OpenFeign 会根据服务名找到 MES 服务。
第二个重点:@PostMapping("/order/sale/inner/syncFromCrm")。
它表示 CRM 实际请求 MES 的这个内部接口:
text
POST /order/sale/inner/syncFromCrm
于是 CRM 里的这一句:
java
mesCrmFeign.syncSaleOrder(req);
就不再是普通方法调用,而是一次跨服务 HTTP 调用。
很多新手第一次看 OpenFeign,容易误解成"它只是帮我少写了一个 HTTP 工具类"。这个理解不算错,但不够完整。
在这个项目里,OpenFeign 至少承担了三层作用:
- 用 Java 接口声明远程服务契约,让 CRM 侧知道可以调用 MES 哪些内部接口。
- 通过服务名找到目标服务,而不是在业务代码里写死 IP 和端口。
- 把 Java 对象
CrmOrderSyncReq序列化成请求体,发送到 MES 对应 Controller。
所以 MesCrmFeign 本质上是 CRM 和 MES 之间的"接口契约层"。如果这个契约变了,比如字段名、路径、请求方式发生变化,CRM 和 MES 两边都要一起确认。
可以把它理解成:
text
CRM OrderServiceImpl
↓
MesCrmFeign.syncSaleOrder(req)
↓
OpenFeign 根据 MES 服务名找到 MES 实例
↓
发送 POST /order/sale/inner/syncFromCrm
↓
MES SaleController 接收 CrmOrderSyncReq
十、MES 那边是谁接收?
MES 接收接口在:
text
D:\Tools\java\work\smartmake-platform\smartmake-platform-mes\src\main\java\com\smartmake\platform\mes\controller\order\SaleController.java
代码是:
java
@PostMapping("/inner/syncFromCrm")
public void syncFromCrm(@RequestBody CrmOrderSyncReq req) {
saleService.syncFromCrm(req);
}
注意这里的方法路径是:
text
/inner/syncFromCrm
而 Feign 里写的是:
text
/order/sale/inner/syncFromCrm
这是因为 SaleController 自己通常还有类级别路径,Feign 调用时写的是完整访问路径。
这一篇我们重点讲 CRM 侧的 DTO 转换和 Feign 调用。MES 接收之后如何落库、如何避免重复同步、如何把生产进度回传给 CRM,可以放到第三篇继续讲。
十一、成功后 CRM 怎么记录状态?

调用 MES 成功后,CRM 会更新订单的 MES 同步状态:
java
lambdaUpdate()
.eq(Order::getId, id)
.set(Order::getMesSyncStatus, "success")
.set(Order::getMesSyncTime, new Date())
.set(Order::getMesSyncError, null)
.update();
这里的三个字段非常重要:
text
mesSyncStatus 推送状态
mesSyncTime 推送时间
mesSyncError 推送失败原因
这就是为什么前端销售订单列表可以展示"是否推送 MES、什么时候推送、失败原因是什么"。
如果没有这些字段,用户点了推送按钮之后就很难知道:
- 到底推送成功没有。
- 失败原因是什么。
- 能不能重新推送。
- 哪些订单已经进入 MES。
企业系统不是只要按钮点得动,还要让业务人员能追踪结果。
十二、失败后为什么不能只抛异常?
失败时,代码会把异常转成更友好的信息,然后记录到订单上:
java
String friendlyMessage = friendlyMesSyncErrorV2(e);
result.setSuccess(false);
result.setMessage(friendlyMessage);
if (dto != null) {
patch.setMesSyncStatus("failed");
patch.setMesSyncTime(new Date());
patch.setMesSyncError(friendlyMessage);
baseMapper.updateById(patch);
}
这一点也很关键。
如果直接把 Java 异常返回给前端,用户看到的可能是:
text
Connection refused
NullPointerException
FeignException
这些信息对开发排查有用,但对业务人员没有帮助。更合理的方式是:
- 后端日志里保留完整异常,方便开发排查。
- 前端展示业务人员能理解的失败原因。
- 数据库记录
mesSyncError,方便后续查看和重试。
这也是跨系统调用里非常重要的一点:失败要可解释、可追踪、可恢复。
十三、把整条链路串起来
这一篇的完整链路可以这样理解:
text
前端选择订单
↓
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 里跟着项目代码自己看一遍,可以按这个顺序:
text
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,请求是否打到了:
text
POST /crm/order/sync/batch
请求体是不是类似:
json
[1001, 1002, 1003]
如果这里没有 ID,后端就没有办法查询真实订单。
2. Gateway 路由有没有转到 CRM 服务?
第一篇已经讲过,前端访问的是 /crm/order/sync/batch,经过网关后才进入 CRM 的 /order/sync/batch。
如果网关路由、服务注册、前端代理配置不对,请求可能根本到不了 CRM 服务。
3. CRM 订单状态是否允许推送?
当前代码只允许这些状态推送:
java
Set.of("confirmed", "producing")
如果订单还是草稿、已取消、已关闭,就不应该进入 MES。
4. 订单数据是否完整?
重点看这些字段:
text
订单编号
客户名称
订单明细
产品编号
产品名称
产品数量
这些数据缺失时,不应该继续推送 MES。因为 MES 拿到不完整订单后,后面的排产、生产、质检都会受影响。
5. Feign 是否真的调到了 MES?
看 CRM 日志里有没有 Feign 调用异常,比如:
text
Connection refused
Read timed out
Load balancer does not contain an instance
这些通常说明不是 DTO 转换问题,而是 MES 服务、注册中心、网络、网关或服务名配置的问题。
6. CRM 有没有写回同步状态?
推送成功后应该能看到:
text
mesSyncStatus = success
mesSyncTime = 当前推送时间
mesSyncError = null
推送失败后应该能看到:
text
mesSyncStatus = failed
mesSyncTime = 当前推送时间
mesSyncError = 失败原因
如果状态没有写回,前端就很难判断订单是否已经同步到 MES。
十七、项目/面试中怎么表达这段经历
如果你要把这段经历写到项目总结、日报或者面试里,不建议只写:
text
完成销售订单推送 MES 接口。
这个表达太浅了,听起来像只是调了一个接口。
更好的表达是:
text
参与 CRM 销售订单推送 MES 链路建设,负责订单推送前的数据校验、CRM 订单到 MES 同步 DTO 的字段映射、OpenFeign 跨服务调用以及同步结果状态回写。支持批量推送后按订单维度返回成功/失败结果,并记录 MES 同步状态、同步时间和失败原因,提升订单从销售侧进入生产侧的可追踪性和异常处理能力。
如果面试官继续追问"你具体做了什么",可以按这条线回答:
text
前端只传订单 ID,CRM 后端根据 ID 查询真实订单,校验订单状态和明细完整性,再把订单主表和明细转换成 MES 约定的 CrmOrderSyncReq / CrmOrderSyncItemReq,通过 OpenFeign 调 MES 内部接口。调用成功后写入 success 和同步时间,调用失败后记录 failed 和失败原因,方便前端展示和后续重试。
这就能体现你理解的不只是接口调用,还有业务链路、真实数据一致性、状态流转和异常追踪。
十八、本篇总结
这一篇我们主要讲清楚了 CRM 推送 MES 的后端核心链路:
text
订单 ID → 查询真实订单 → 业务校验 → DTO 转换 → OpenFeign 调 MES → 回写同步状态
其中最重要的不是 Feign 那一行代码,而是围绕它前后的业务处理:
- 为什么不能相信前端传完整数据。
- 为什么推送前要校验订单状态和订单明细。
- 为什么要用 DTO 做系统之间的数据协议。
- 为什么成功和失败都要写回 CRM。
理解了这一篇,再去看第三篇 MES 如何接收、如何避免重复同步、如何把生产进度和状态回传给 CRM,就会顺很多。
