从销售订单推送 MES 看懂微服务调用(三):MES 接收、幂等设计与状态回写
前两篇我们已经讲清楚了两件事:
第一,前端按钮如何触发 CRM 后端的“推送 MES”接口;
第二,CRM 后端如何查询真实销售订单、组装 DTO,并通过 OpenFeign 调用 MES。
这一篇继续往后看:MES 收到 CRM 推过来的销售订单之后,到底做了什么?它如何避免重复生成订单?如何补齐客户、产品、分类这些基础资料?又如何把同步结果和生产进度回传给 CRM?
这篇是整个系列里业务含金量最高的一篇。因为真实项目里的微服务调用,不是“接口调通了”就结束了,而是要回答这些问题:
- CRM 推过来的订单,MES 能不能可靠接住?
- 同一张订单重复推送,会不会在 MES 生成多张销售订单?
- MES 没有对应客户、产品、分类时,是报错还是自动补齐?
- MES 保存成功以后,CRM 怎么知道是否真的落库成功?
- 后续排产、生产、质检、发货进度,CRM 怎么查回来?
可以先记住这一条主线:
CRM 销售订单
-> OpenFeign 推送 MES
-> MES 接收订单
-> MES 幂等判断
-> MES 补齐客户、产品、分类资料
-> MES 保存销售订单主表和明细
-> MES 返回同步结果
-> CRM 记录同步状态
-> CRM 后续查询 MES 生产进度
第一张图只讲“CRM 推送 MES 并落库”:
第二张图只讲“CRM 回查 MES 生产进度”:
读完这一篇,你应该能搞清楚三件事:
- 业务上:CRM 销售订单为什么不能只停留在“推送成功”,还要能追踪 MES 生产进度;
- 技术上:MES 如何通过 Controller、Service、DTO、数据库主从表把 CRM 订单接住;
- 排查上:遇到“推送失败、重复订单、详情没明细、查不到进度”时,应该沿着哪几个字段和哪几段代码去看。
本文仍然只基于当前项目真实代码来讲,不额外虚构接口和字段。
一、这一篇要解决什么问题
第二篇最后,我们看到 CRM 里有这样一行代码:
MesOrderProgressRsp mesSyncRsp = mesCrmFeign.syncSaleOrder(req);
这行代码表面上只是一个 Feign 调用,但它背后代表的是一次完整的跨系统业务同步。
真正的调用链路是:
CRM OrderServiceImpl.syncToMes
-> MesCrmFeign.syncSaleOrder
-> MES SaleController.syncFromCrm
-> MES SaleServiceImpl.syncFromCrm
所以第三篇重点不是再讲“Feign 怎么写”,而是讲 MES 这一侧如何处理业务数据。
这也是新手最容易忽略的地方:
微服务调用的难点不在于把 HTTP 请求发出去,而在于另一个服务收到数据后,如何保证数据一致、业务不重复、状态可追踪。
二、先看 CRM 和 MES 之间的 Feign 接口
代码位置:
D:\Tools\java\work\smartmake-platform\smartmake-platform-feign\smartmake-platform-mes-api\src\main\java\com\smartmake\platform\mes\feign\MesCrmFeign.java
核心代码:
@FeignClient(name = FeignConstants.MES_FEIGN_NAME)
public interface MesCrmFeign {
@PostMapping("/md/client/inner/syncFromCrm")
MesCustomerSyncRsp syncClient(@RequestBody CrmCustomerSyncReq req);
@PostMapping("/order/sale/inner/syncFromCrm")
MesOrderProgressRsp syncSaleOrder(@RequestBody CrmOrderSyncReq req);
@PostMapping("/order/returns/inner/syncFromCrm")
MesReturnSyncRsp syncSaleReturn(@RequestBody CrmReturnSyncReq req);
@GetMapping("/order/sale/inner/progress/by-external-id/{externalId}")
MesOrderProgressRsp getOrderProgress(@PathVariable("externalId") String externalId);
}
当前这个 Feign 接口已经不只是销售订单,它还包含客户同步、退货同步和销售订单进度查询。也就是说,CRM 和 MES 的集成不是单点功能,而是一组围绕客户、订单、退货展开的内部服务调用。
本文重点看销售订单,所以先抓住这两个方法:
syncSaleOrder:CRM 主动把销售订单同步到 MES
getOrderProgress:CRM 根据 externalId 查询 MES 中这张订单的生产进度
其他两个方法可以先理解成同一套集成思路的扩展:
syncClient:CRM 客户资料同步到 MES 客户资料
syncSaleReturn:CRM 退货单同步到 MES 退货业务
所以 CRM 和 MES 的关系不是“CRM 发完就结束”,而是一个闭环:
CRM 发订单给 MES
MES 保存订单并返回结果
CRM 记录 MES 同步状态
CRM 后续再查 MES 的生产进度
这一点非常关键。制造业系统里,CRM 关心客户、合同、销售订单;MES 关心生产、排产、工单、质检、发货。两边必须通过订单关联起来。

三、为什么 CRM 要检查 MES 的返回值
在看返回值之前,先看 CRM 推给 MES 的请求对象。
代码位置:
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
这个 DTO 不是随便定义的,它基本对应一张销售订单同步到 MES 所需要的核心信息:
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;
}
这里可以分成两类字段来看:
订单主信息:externalId、saleOrderNum、clientCode、clientName、deliveryDate、address、金额、状态、备注
订单明细:items,也就是产品、数量、单价、分类路径等明细行
这说明 CRM 推送 MES 时,不只是传一个订单 ID,而是把 MES 创建销售订单所需的主表信息和明细信息一起传过去。
为了更直观,可以把这次同步理解成一张字段映射表:
| CRM 同步字段 | MES 侧字段/作用 | 说明 |
|---|---|---|
externalId |
Sale.externalId |
CRM 订单 ID,是 CRM-MES 关联锚点 |
saleOrderNum |
Sale.saleOrderNum |
销售订单号,用于业务展示和兜底匹配 |
clientCode |
Sale.clientCode / MES 客户编码 |
用于查找或创建 MES 客户 |
clientName |
Sale.clientName / MES 客户名称 |
客户编码不存在时,可按客户名称兜底查询 |
source |
Sale.source |
标记订单来源,例如 CRM |
sourceSystem |
Sale.sourceSystem |
标记来源系统,例如 smartmake-CRM |
deliveryDate |
Sale.deliveryDate |
预计交货日期,后续生产履约会关注 |
address |
Sale.address |
收货地址或交付地址 |
includeTax / noIncludeTax |
Sale.includeTax / Sale.noIncludeTax |
订单金额信息 |
items |
SaleItem 明细集合 |
产品、数量、单价、单位、分类路径等明细行 |
这里要注意,第二篇已经讲过 CRM 如何组装这些字段,所以本文不再重复前面的组装过程。第三篇更关注的是:MES 收到这些字段后,如何校验、匹配、保存和返回结果。
CRM 推送 MES 的代码在:
D:\Tools\java\work\smartmake-platform\smartmake-platform-crm\src\main\java\com\smartmake\platform\crm\service\impl\OrderServiceImpl.java
CRM 不是简单调用一下 MES 就算成功,而是会检查 MES 返回的同步结果:
MesOrderProgressRsp mesSyncRsp = mesCrmFeign.syncSaleOrder(req);
if (mesSyncRsp == null || !mesSyncRsp.isSynced() || mesSyncRsp.getMesSaleId() == null) {
throw new IllegalArgumentException("MES未返回同步销售订单,请检查MES是否成功落库");
}
这段代码的业务含义是:
- MES 没有返回结果,不算成功;
- MES 返回了结果,但 synced 不是 true,不算成功;
- MES 没有返回 mesSaleId,也不算成功。
为什么一定要看 mesSaleId?
因为 mesSaleId 表示 MES 侧真正生成或匹配到的销售订单 ID。只有拿到这个 ID,CRM 才能确认:
这张 CRM 销售订单,已经在 MES 侧有了对应订单。
如果只根据 HTTP 200 判断成功,就会有风险。比如接口没报错,但 MES 实际没有落库,CRM 却显示“推送成功”,后续销售人员看到的状态就是假的。
所以 CRM 这里的判断是在保护跨系统数据一致性。
CRM 成功后会记录:
mesSyncStatus = success
mesSyncTime = 当前时间
mesSyncError = 清空
失败后会记录:
mesSyncStatus = failed
mesSyncTime = 当前时间
mesSyncError = 失败原因
这就是“推送 MES 状态”在订单列表和订单详情中能够展示的来源。
四、MES Controller 如何接收 CRM 推送
CRM Feign 接口里写的是:
@PostMapping("/order/sale/inner/syncFromCrm")
MES 侧真正接收这个请求的是:
D:\Tools\java\work\smartmake-platform\smartmake-platform-mes\src\main\java\com\smartmake\platform\mes\controller\order\SaleController.java
类上有路径:
@RequestMapping("/order/sale")
方法上有路径:
@PostMapping("/inner/syncFromCrm")
public MesOrderProgressRsp syncFromCrm(@RequestBody CrmOrderSyncReq req) {
return saleService.syncFromCrm(req);
}
两个路径拼起来,就是:
/order/sale/inner/syncFromCrm
这正好对应 CRM 里的 Feign 调用地址。
这里 Controller 没有写复杂业务,只做了一件事:
接收 CRM 请求 -> 交给 saleService.syncFromCrm 处理 -> 返回 MES 同步结果
这是比较标准的分层方式:Controller 负责接请求,Service 负责业务处理,Mapper/Repository 负责数据库操作。
真正重要的逻辑在 MES 的 Service 里面。
五、MES Service 才是真正的处理核心
代码位置:
D:\Tools\java\work\smartmake-platform\smartmake-platform-mes\src\main\java\com\smartmake\platform\mes\service\impl\order\SaleServiceImpl.java
核心方法是:
public MesOrderProgressRsp syncFromCrm(CrmOrderSyncReq req)
这个方法大致做了几件事:
1. 校验 CRM 推过来的订单数据
2. 根据 externalId 判断 MES 是否已经同步过
3. 如果没有 externalId 命中,再根据 saleOrderNum 判断是否已经存在
4. 如果已存在,直接返回现有 MES 订单的同步结果
5. 如果不存在,补齐客户资料
6. 补齐产品资料和产品分类
7. 保存 MES 销售订单主表
8. 保存 MES 销售订单明细
9. 返回 MES 同步结果给 CRM
从业务角度看,这个方法做的不是普通新增,而是“跨系统订单同步”。它要考虑重复推送、基础资料缺失、主从表保存、状态回写等问题。
还有一个容易被忽略但很重要的点:这个方法上有事务控制。
@Transactional(rollbackFor = Exception.class)
public MesOrderProgressRsp syncFromCrm(CrmOrderSyncReq req) {
// 接收 CRM 订单并保存到 MES
}
为什么这里需要事务?
因为 MES 接收一张 CRM 销售订单时,不是只插入一张表,而是可能同时涉及:
客户资料
产品资料
产品分类
销售订单主表 Sale
销售订单明细 SaleItem
如果其中某一步失败,最安全的结果应该是整体回滚,而不是留下一半数据。
比如产品创建成功了,但销售订单明细保存失败;或者主表保存成功了,但明细没有保存。这些都会造成后续页面“看起来有订单,但数据不完整”。所以这里用事务,是为了保证 MES 接收 CRM 订单时的数据完整性。
如果你在 IDEA 里阅读这段代码,建议按这个顺序看:
1. 先看方法签名:syncFromCrm(CrmOrderSyncReq req)
2. 再看 externalId 和 saleOrderNum 的查询逻辑
3. 接着看 existing != null 时为什么直接 return
4. 然后看客户 client 的先查后建
5. 再看 Sale 主表字段如何从 req 映射
6. 继续看 items 循环里产品如何先查后建
7. 最后看 saveOrderSale(sale) 和 buildCrmSyncRsp(sale)
这样读不会被一大段业务代码绕晕。先抓住“是否已存在”,再看“不存在时怎么创建”,最后看“创建成功后返回什么”。
六、externalId 是 CRM-MES 关联的关键
CRM 推送 DTO 时,会把 CRM 订单 ID 放到 externalId 里面:
.setExternalId(String.valueOf(dto.getId()))
.setSaleOrderNum(dto.getCode())
.setClientName(dto.getCustomerName())
.setClientCode(dto.getCustomerCode())
.setSource("CRM")
.setSourceSystem("smartmake-CRM")
这里最重要的是:
externalId = CRM 销售订单 ID
MES 保存销售订单时,也会保存这个 externalId。这样后续 MES 就知道:
这张 MES 销售订单来自哪一张 CRM 销售订单。
为什么不用订单编号 saleOrderNum 作为唯一关联?
因为订单编号可能存在这些情况:
- 编号规则调整;
- 历史数据编号不规范;
- 不同系统中编号格式不同;
- 人工录入或导入时可能出现重复风险。
而 CRM 订单 ID 是 CRM 数据库里的主键,更适合做跨系统关联锚点。
可以把 externalId 理解为 CRM 和 MES 之间的“桥梁字段”。没有这个字段,后续 CRM 想查 MES 生产进度时,就不知道该查 MES 里的哪张订单。
举个很贴近项目的例子:
假设 CRM 原来的订单编号是 SO202608120001,后来编号规则调整成 XSDD202608120001。
如果 MES 只靠 saleOrderNum 关联订单,历史订单、新订单、导入订单之间就可能出现匹配风险。
但 externalId 存的是 CRM 订单主键。
只要这张 CRM 订单的 ID 不变,MES 就能稳定知道它对应的是哪张 CRM 订单。
所以 saleOrderNum 更像业务展示编号,externalId 更像系统关联编号。两个字段都重要,但职责不一样:
saleOrderNum:给业务人员看的订单编号
externalId:给系统之间做稳定关联用的锚点
七、MES 如何做幂等,避免重复生成销售订单
跨系统推送一定要考虑重复请求。
比如这些情况都可能导致 CRM 重复推送同一张订单:
- 用户连续点击两次“推送 MES”;
- 网络超时后前端重试;
- 后端第一次调用成功,但 CRM 没收到返回;
- 运维或业务人员手动重新推送。
如果 MES 不做幂等,就可能出现:
CRM 一张销售订单 -> MES 多张销售订单
这在制造业系统里是非常严重的问题。因为 MES 后面会接排产、工单、领料、质检、出库,一旦订单重复,后续业务都会被带偏。
MES 当前逻辑是先查 externalId:
根据 externalId 查询 MES 销售订单
如果查到,说明这张 CRM 订单已经同步过
直接返回已有 MES 订单信息
如果 externalId 没查到,再用 saleOrderNum 兜底:
根据 saleOrderNum 查询 MES 销售订单
如果查到,说明 MES 里可能已有同编号订单
补上 externalId 后返回
这个逻辑的业务意义是:
- externalId 是最准确的跨系统关联字段;
- saleOrderNum 是兼容历史数据或异常数据的兜底方式;
- 已存在的订单不重复新增,而是返回已有结果。
可以用一句话概括 MES 的幂等逻辑:
同一张 CRM 订单无论推送几次,MES 都应该只对应一张销售订单。
如果要验证这段逻辑,可以按下面的测试思路来:
测试 1:第一次推送
操作:CRM 选择一张未推送过的销售订单,点击推送 MES。
期望:MES 新增一张销售订单,CRM 记录 mesSyncStatus = success。
测试 2:第二次推送同一张订单
操作:CRM 对同一张订单再次点击推送 MES。
期望:MES 不新增第二张销售订单,而是返回第一次生成的 MES 销售订单信息。
测试 3:MES 已有同订单号但缺少 externalId
操作:MES 中存在相同 saleOrderNum 的历史订单,CRM 再推送这张订单。
期望:MES 不新增重复订单,而是给已有订单补上 externalId 并返回。
这三个测试能覆盖“新增、重复推送、历史数据兜底”三种关键场景。

八、MES 如何处理客户资料:先查后建
CRM 推过来的订单里带了客户信息,例如:
clientCode
clientName
MES 收到订单后,需要给 MES 销售订单关联客户。
但问题是:MES 里不一定已经有这个客户。
所以代码里会先查询客户:
1. 优先根据 clientCode 查询客户
2. 查不到时根据 clientName 查询客户
3. 还查不到,就创建一个新的 MES 客户
这个设计比较符合真实业务。
因为 CRM 是客户数据的主要来源,MES 不一定提前维护完整客户资料。CRM 推销售订单时,如果 MES 没有客户资料,自动补齐客户,可以减少人工重复维护。
但这里也有一个需要注意的点:
自动创建客户只能补齐必要字段,客户更详细的资料仍然应该以 CRM 为主。
也就是说,MES 这里创建客户不是为了取代 CRM 客户管理,而是为了让 MES 销售订单能够正常落库、后续生产业务能够继续走下去。
九、MES 如何保存销售订单主表
客户处理完以后,MES 会构建销售订单主表 Sale。
主要映射关系可以理解为:
CRM externalId -> MES Sale.externalId
CRM saleOrderNum -> MES Sale.saleOrderNum
CRM clientCode -> MES Sale.clientCode
CRM clientName -> MES Sale.clientName
CRM deliveryDate -> MES Sale.deliveryDate
CRM address -> MES Sale.address
CRM source -> MES Sale.source
CRM sourceSystem -> MES Sale.sourceSystem
这里特别要看 source 和 sourceSystem:
source = CRM
sourceSystem = smartmake-CRM
这两个字段的意义是标记 MES 订单来源。
后续在 MES 里看订单时,可以知道这张订单不是 MES 手工创建的,而是 CRM 推送过来的。这对排查问题很有用。比如 MES 订单数据不对时,可以反向去 CRM 查原始订单。
十、MES 如何保存销售订单明细
销售订单不是只有主表,还包含产品明细。
CRM 推过来的 items 会转换成 MES 的 SaleItem。
明细里一般会包含这些信息:
产品编号
产品名称
规格型号
单位
数量
单价
金额
交付日期
备注
产品分类路径
MES 这边会逐条处理订单明细。处理时不只是保存 SaleItem,还会处理产品资料。
如果 MES 中已经有这个产品,就关联已有产品;如果没有,就根据 CRM 推过来的产品信息创建 MES 产品。
这也是制造业系统联动里很常见的逻辑:
订单同步时,顺带补齐生产系统所需的基础资料。
但要注意,产品资料自动创建也应该有边界。它适合补齐生产所需的基本字段,不适合在同步订单时补全所有复杂的产品工艺、BOM、质检标准等资料。
十一、为什么产品分类路径也要同步
CRM 在推送订单前,会构建产品分类路径:
Map<Long, String> categoryPathMap = buildProductCategoryPathMap(dto.getItems());
然后在订单明细里设置:
.setProductCategoryPath(categoryPathMap.get(item.getProductId()))
MES 收到明细后,如果需要创建产品,会调用类似这样的逻辑:
ensureCrmProductCategoryId(item)
它的作用是根据 CRM 传来的产品分类路径,在 MES 侧找到或创建对应产品分类。
为什么要这么做?
因为产品分类不是单纯展示字段,它会影响 MES 中产品资料的归类和后续检索。
如果不同步分类,就可能出现:
CRM 里产品属于 A 分类
MES 自动创建产品后却没有分类
MES 里产品资料越来越乱
后续排产、查询、统计都不方便
所以产品分类路径同步,本质上是在保证 CRM 和 MES 的产品基础资料结构尽量一致。
十二、saveOrderSale 如何保存主从表
MES 最终保存销售订单时,会调用保存订单的方法。
核心逻辑可以概括为:
1. 计算订单总数量
2. 保存 Sale 主表
3. 拿到 Sale 主表 ID
4. 给每条 SaleItem 设置 saleOrderId
5. 批量保存 SaleItem 明细
伪代码理解如下:
sale.setSaleItemList(saleItems);
saveOrderSale(sale);
return buildCrmSyncRsp(sale);
主从表保存里最关键的是这一步:
SaleItem.saleOrderId = Sale.id
因为只有明细关联上主表,MES 才能知道每一条产品明细属于哪张销售订单。
如果这一步漏了,就会出现:
MES 有销售订单主表
但是查看详情时没有产品明细
这也是我们平时排查“详情为空”“明细展示不出来”时要重点检查的地方。
十三、MES 返回给 CRM 的同步结果是什么
MES 保存或匹配到销售订单后,会构建返回结果:
MesOrderProgressRsp
这个返回对象里至少要表达这些信息:
synced 是否已同步
mesSaleId MES 销售订单 ID
mesSaleOrderNum MES 销售订单编号
mesSaleStatus MES 销售订单状态
mesSaleStatusText MES 销售订单状态文本
approver MES 审批人
approveTime MES 审批时间
approveRemark MES 审批备注或驳回原因
planCount 生产计划数量
workOrderCount 生产工单数量
qualityRecordCount 质量报告数量
scheduleStatus 计划排布状态
productionStatus 生产状态
qualityStatus 质量状态
CRM 为什么需要这些字段?
因为 CRM 不能只知道“接口调用成功”,它还要知道:
MES 侧具体是哪张订单接住了这次同步。
所以 CRM 成功判断里会看:
mesSyncRsp != null
mesSyncRsp.isSynced()
mesSyncRsp.getMesSaleId() != null
只要 MES 返回的结果不完整,CRM 就会认为同步不可靠,并把 mesSyncStatus 记为 failed。
这样做看起来严格,但在跨系统订单同步里是必要的。
这里还有一个值得注意的点:MesOrderProgressRsp 既承担“同步结果返回”,也承担“进度汇总展示”。
同步刚完成时,MES 可能只返回销售订单本身的信息,例如 synced、mesSaleId、mesSaleOrderNum、mesSaleStatus。等 MES 后续产生生产计划、工单、质量报告后,再通过进度查询接口返回 planCount、workOrderCount、qualityRecordCount、scheduleStatus、productionStatus、qualityStatus 等字段。
所以它不是一个单纯的“成功/失败对象”,而是 CRM 订单查看 MES 履约进度的展示模型。
一个正常返回结构可以这样理解。注意下面是示例结构,用来帮助理解字段含义,不代表真实数据库中的固定数据:
{
"synced": true,
"mesSaleId": 10086,
"mesSaleOrderNum": "SO202608120001",
"mesSaleStatus": "0",
"mesSaleStatusText": "待审批",
"approver": null,
"approveTime": null,
"approveRemark": null,
"planCount": 0,
"workOrderCount": 0,
"qualityRecordCount": 0,
"scheduleStatus": "pending",
"productionStatus": "pending",
"qualityStatus": "pending",
"scheduleDescription": "暂未排产",
"productionDescription": "暂未开始生产",
"qualityDescription": "暂未生成质量报告"
}
刚同步成功时,synced 和 mesSaleId 最关键;后续 MES 开始排产、生成工单、产生质检记录后,planCount、workOrderCount、qualityRecordCount 以及几个状态字段才会逐步变得有业务意义。

十四、CRM 后续如何查询 MES 生产进度
CRM 不仅可以推送订单,还可以查询 MES 进度。
Feign 接口里有:
@GetMapping("/order/sale/inner/progress/by-external-id/{externalId}")
MesOrderProgressRsp getOrderProgress(@PathVariable("externalId") String externalId);
CRM 查询时传的 externalId,仍然是 CRM 订单 ID:
externalId = CRM 销售订单 ID
MES 接收后,会根据 externalId 找到对应的 MES 销售订单。
这就是为什么前面说 externalId 非常重要。如果同步订单时没有保存 externalId,后续 CRM 就无法通过自己的订单 ID 查到 MES 的生产进度。
十五、MES 进度来自哪些真实业务数据
MES 侧查询进度不是随便写死一个状态,而是从真实业务数据链路里取。
从代码链路看,MES 进度会关联这些对象:
Sale 销售订单
-> ProductionPlan 生产计划
-> WorkOrder 生产工单
-> Outbound 出库单
-> OqcRecord OQC 质检记录
也就是说,CRM 订单详情里展示的 MES 进度,应该来自 MES 的真实业务流转。
可以理解为:
CRM 负责订单入口
MES 负责生产履约
CRM 通过 externalId 回查 MES 履约进度
这比单纯展示“已推送”“未推送”更有业务价值。
因为销售人员真正关心的不是订单有没有推过去,而是:
- 是否已经排产;
- 是否已经生成工单;
- 是否已经生产;
- 是否已经质检;
- 是否已经出库;
- 客户什么时候能交付。
所以 CRM-MES 集成的最终价值,是让销售订单从客户侧一路追踪到生产交付侧。

十六、这套链路常见问题怎么排查
1. CRM 显示推送失败
优先看 CRM 订单上的:
mesSyncStatus
mesSyncError
mesSyncTime
如果 mesSyncError 里有错误信息,先看是参数缺失、MES 返回为空,还是 MES 服务调用失败。
2. MES 没有生成销售订单
检查 MES 的接收接口是否进入:
SaleController.syncFromCrm
SaleServiceImpl.syncFromCrm
再看是否因为参数校验失败、客户或产品处理失败、数据库保存失败导致事务回滚。
3. CRM 重复推送后 MES 出现重复订单
重点检查幂等逻辑:
是否按 externalId 查询已有订单
是否按 saleOrderNum 做兜底查询
已存在时是否直接返回,而不是继续新增
如果 externalId 没保存成功,重复推送就很容易出问题。
4. CRM 查询不到 MES 进度
优先检查:
CRM 订单 ID 是否正确传给 MES
MES Sale.externalId 是否保存了 CRM 订单 ID
MES 是否已经生成生产计划、工单、出库、质检等后续数据
如果 MES 只有销售订单,还没有排产或生产数据,那么 CRM 查到的进度自然也不会完整。
5. 产品明细为空
重点看主从表关系:
Sale 是否保存成功
SaleItem 是否保存成功
SaleItem.saleOrderId 是否正确设置为 Sale.id
很多“详情没有明细”的问题,根源都在主从表关联字段没有写对。
6. 前端不应该直接暴露底层异常
CRM 推送 MES 失败时,后端可以记录详细错误,例如 MES 服务不可用、字段缺失、产品分类未维护、数据库保存失败等。
但前端展示给业务人员时,不建议直接弹出一大段 Java 异常栈或数据库错误。更合理的方式是:
页面提示:推送失败,请查看失败原因或联系管理员
详情字段:展示简短失败原因,例如“MES未返回同步销售订单”
后端日志:保留完整异常栈,方便开发排查
也就是说,错误信息要分层:
业务人员看到可理解的提示
开发人员在日志里看到完整技术原因
这样既不会把页面弄得很吓人,也不会影响开发定位问题。
十七、这段项目经历应该怎么表达
如果写日报,可以这样写:
完善 CRM 销售订单推送 MES 的接收与状态回写链路,梳理 MES 侧订单幂等接收、客户/产品基础资料补齐、销售订单主从表保存、同步结果返回及 CRM 回查 MES 进度逻辑,提升 CRM-MES 跨系统订单数据一致性和生产进度可追踪性。
如果写简历,可以这样写:
参与制造业 CRM-MES 订单集成链路建设,基于 OpenFeign 实现 CRM 销售订单向 MES 同步,完善 MES 侧幂等接收、客户与产品资料自动补齐、订单主从表落库、同步状态回写及生产进度回查能力,保障销售订单从客户侧到生产履约侧的数据一致性与可追踪性。
面试时可以按这个顺序讲:
1. CRM 销售订单审核或手动触发后,需要推送到 MES;
2. CRM 后端查询真实订单、客户、产品明细,组装 CrmOrderSyncReq;
3. CRM 通过 OpenFeign 调用 MES 内部接口;
4. MES 根据 externalId 做幂等判断,避免重复生成订单;
5. MES 先查后建客户、产品、产品分类;
6. MES 保存销售订单主表和明细;
7. MES 返回 mesSaleId、同步状态和订单状态;
8. CRM 保存 mesSyncStatus、mesSyncTime、mesSyncError;
9. CRM 后续通过 externalId 查询 MES 排产、工单、出库、质检进度。
这套表达比单纯说“我写了一个 Feign 接口”含金量高很多,因为它讲清楚了业务闭环和数据一致性。
十八、本系列总结
回头看这条链路,真正值得学习的不是某一个注解,而是这几个设计意识:
1. 跨服务调用要有明确的业务边界:CRM 管销售入口,MES 管生产履约;
2. 跨系统数据要有稳定关联键:externalId 负责把 CRM 订单和 MES 订单连起来;
3. 推送接口要考虑幂等:重复点击、重试、超时都不能造成重复订单;
4. 基础资料要有补齐策略:客户、产品、分类缺失时要能支撑业务继续走;
5. 调用结果要可追踪:CRM 不能只看接口是否 200,还要记录 MES 是否真实落库;
6. 后续状态要能回查:销售人员最终关心的是生产履约进度,而不是一次接口调用。
这也是这条 CRM-MES 集成链路比普通 CRUD 更有业务含金量的地方。
到这里,三篇文章就把“销售订单推送 MES”这条链路串起来了。
第一篇重点是:
业务流程与前端入口
用户在哪里点击按钮,前端如何调用 CRM 后端接口
第二篇重点是:
DTO 转换与 OpenFeign 调用
CRM 后端如何查询真实订单数据,组装跨服务 DTO,并调用 MES
第三篇重点是:
MES 接收、幂等设计与状态回写
MES 如何可靠接住订单,避免重复,补齐基础资料,保存主从表,并让 CRM 能查回进度
如果从学习微服务调用的角度看,这个系列不是只讲语法,而是按真实项目链路去理解:
前端入口
-> CRM 接口
-> CRM Service
-> DTO 组装
-> Feign 调用
-> MES Controller
-> MES Service
-> 数据落库
-> 状态返回
-> 进度回查
这才是真实业务系统里一条跨服务调用链路应该具备的完整思路。
更多推荐
所有评论(0)