从销售订单推送 MES 看懂微服务调用(一):业务流程与前端入口
本文用一个真实业务功能拆解微服务调用:CRM 销售订单推送 MES。第一篇先讲清楚最前面的入口链路,也就是用户在前端点击“推送 MES”之后,请求如何从 CRM 前端进入 CRM 后端 Controller。至于 CRM 后端如何查询订单、组装 DTO、通过 Feign 调 MES,会放到下一篇继续讲。
读完本文你能搞懂什么
如果你刚开始接触微服务,最容易卡住的地方通常不是代码语法,而是这几个问题:
- 前端点击一个按钮,到底请求了哪个接口?
- 为什么前端请求地址是
/crm/order/sync/batch,后端 Controller 却写的是/order/sync/batch? - 前端推送 MES 时,是不是把整张订单数据都传给 MES?
- Controller 拿到请求后,为什么又要交给 Service?
这篇文章就围绕这几个问题展开。
先给结论:
前端不会直接调用 MES。
前端也不会把完整订单数据推给 MES。
前端只负责把选中的订单 ID 发给 CRM 后端。
CRM 后端再根据订单 ID 查询真实订单数据,并继续完成后续推送逻辑。
这样设计更可靠。因为前端页面展示的数据不一定完整,也不能完全信任,真正用于推送 MES 的订单数据应该以后端数据库为准。
一、业务背景:为什么 CRM 订单要推送 MES
在企业系统里,CRM 和 MES 负责的业务阶段不同。
| 系统 | 主要负责内容 |
|---|---|
| CRM | 客户、商机、合同、销售订单、回款、发票 |
| MES | 排产、工单、生产执行、质检、发货 |
销售订单在 CRM 里创建和确认后,后续如果涉及生产,就需要把订单信息同步给 MES,让 MES 继续安排生产执行。
可以简单理解为:
CRM 负责把订单谈下来。
MES 负责把订单做出来。
所以“推送 MES”不是一个普通按钮,它代表的是:这张 CRM 销售订单开始进入生产系统。
这里还要区分两个概念:
| 概念 | 含义 |
|---|---|
| MES 同步状态 | CRM 订单是否已经成功推送给 MES |
| MES 生产进度 | MES 接收订单后,排产、生产、质检、发货进行到哪一步 |
也就是说:
推送 MES 成功,不代表订单已经生产完成。
推送 MES 成功,只代表 MES 已经接收到了这张 CRM 销售订单。
二、本文只看入口链路
完整的“销售订单推送 MES”其实可以拆成两大段。
第一段:前端入口到 CRM Controller。
销售订单页面
↓
勾选订单
↓
点击“推送 MES”按钮
↓
前端调用 CRM 接口
↓
请求经过网关
↓
进入 CRM 后端 OrderController
第二段:CRM Service 到 MES 服务。
OrderController
↓
OrderService
↓
查询订单和订单明细
↓
组装 MES 需要的请求 DTO
↓
通过 Feign 调用 MES 服务
↓
MES 接收并处理订单
本文只讲第一段。先把入口链路看明白,后面再看 Service 和 Feign 会轻松很多。
三、本文涉及的代码文件
这篇文章会对照以下几个文件讲解。
| 位置 | 作用 |
|---|---|
apps/crm-antd/src/views/crm/order/index.vue | 销售订单列表页面,包含勾选订单和“推送 MES”按钮 |
apps/crm-antd/src/api/crm.ts | 前端 API 封装,定义调用哪个后端接口 |
smartmake-platform-gateway/src/main/resources/gateway-route.yaml | 网关路由配置,负责把 /crm/** 转发给 CRM 服务 |
smartmake-platform-crm/src/main/java/com/smartmake/platform/crm/controller/OrderController.java | CRM 后端订单 Controller,接收推送请求 |
建议学习时不要只看其中一个文件。这个功能的关键点就在于:前端、网关、后端路径是连在一起的。
四、整体链路图
先用一张图把入口链路串起来。

这条链路里最容易混淆的是路径变化:
前端请求:/crm/order/sync/batch
网关处理:匹配 /crm/**,然后 StripPrefix=1 去掉 /crm
后端接收:/order/sync/batch
所以前端和后端路径看起来“不一样”,不是写错了,而是中间经过了网关处理。
五、第一步:表格勾选订单
销售订单页面在这个文件里:
apps/crm-antd/src/views/crm/order/index.vue
页面里有一段和批量推送 MES 相关的代码:
const selectedRowKeys = ref<number[]>([]);
const syncing = ref(false);
const MES_SYNCABLE = new Set(['confirmed', 'producing']);
const rowSelection = {
selectedRowKeys,
onChange: (keys: number[]) => {
selectedRowKeys.value = keys;
},
getCheckboxProps: (record: any) => ({
disabled: !MES_SYNCABLE.has(record.status),
title: MES_SYNCABLE.has(record.status) ? '' : '仅已确认/生产中状态可推送',
}),
};
这里先看 selectedRowKeys:
const selectedRowKeys = ref<number[]>([]);
它用来保存表格中被选中的行 key。
那这个 key 是什么?继续看表格配置:
<a-table
:data-source="list"
:loading="loading"
:row-selection="rowSelection"
row-key="id"
>
重点是这一行:
row-key="id"
它表示表格每一行的唯一标识使用订单的 id 字段。
举个例子,页面上有三条订单:
| id | 订单号 | 客户 |
|---|---|---|
| 101 | SO001 | 客户 A |
| 102 | SO002 | 客户 B |
| 103 | SO003 | 客户 C |
如果用户勾选第一条和第三条,那么前端保存的是:
selectedRowKeys.value = [101, 103];
注意,这里保存的是订单主键 ID,不是订单号,也不是客户名称。
这是批量操作最常见的写法:
表格展示很多字段,但真正提交给后端的是这一行数据的主键 ID。
六、为什么有些订单不能勾选
再看这段代码:
const MES_SYNCABLE = new Set(['confirmed', 'producing']);
它定义了哪些订单状态允许推送 MES。
继续看:
getCheckboxProps: (record: any) => ({
disabled: !MES_SYNCABLE.has(record.status),
title: MES_SYNCABLE.has(record.status) ? '' : '仅已确认/生产中状态可推送',
}),
意思是:
| 订单状态 | 是否允许勾选 |
|---|---|
| confirmed | 允许 |
| producing | 允许 |
| draft | 不允许 |
| cancelled | 不允许 |
| completed | 不允许 |
这样做是为了避免用户把不该推送的订单推给 MES。比如草稿订单还没有确认,数据可能不完整,不应该进入生产系统;已取消订单也不应该继续推送 MES。
七、第二步:点击“推送 MES”按钮
销售订单列表上有一个按钮:
<a-button
type="primary"
ghost
:loading="syncing"
:disabled="selectedRowKeys.length === 0"
@click="batchSyncToMes"
>
推送 MES
<a-badge
v-if="selectedRowKeys.length > 0"
:count="selectedRowKeys.length"
:offset="[4, -8]"
/>
</a-button>
这段代码里有三个重点。
第一,没选订单时按钮不可点:
:disabled="selectedRowKeys.length === 0"
第二,点击按钮会执行 batchSyncToMes 方法:
@click="batchSyncToMes"
第三,按钮旁边的数字显示当前选中了几条:
:count="selectedRowKeys.length"
也就是说,前端推送 MES 的入口不是自动触发,而是用户明确选择订单后,点击按钮触发。
八、第三步:前端方法做了什么
点击按钮后,会执行这个方法:
async function batchSyncToMes() {
if (selectedRowKeys.value.length === 0) {
message.warning('请先选择要推送的订单');
return;
}
syncing.value = true;
try {
const result: any = await orderApi.batchSyncToMes(selectedRowKeys.value);
const totalCount = Number(result?.total ?? selectedRowKeys.value.length);
const ok = Number(result?.successCount ?? 0);
const fail = Number(result?.failureCount ?? Math.max(totalCount - ok, 0));
if (fail === 0) {
message.success(`推送成功,成功 ${ok || totalCount} 条`);
}
selectedRowKeys.value = [];
loadList();
} finally {
syncing.value = false;
}
}
为了文章阅读方便,上面保留了核心逻辑,省略了部分失败弹窗展示代码。
这段方法主要做四件事:
| 步骤 | 代码含义 |
|---|---|
| 1 | 如果没有选择订单,提示“请先选择要推送的订单” |
| 2 | 设置 syncing=true,让按钮进入加载状态,避免重复点击 |
| 3 | 调用 orderApi.batchSyncToMes(selectedRowKeys.value) |
| 4 | 推送结束后清空选择,并重新加载列表 |
最关键的是这一行:
const result: any = await orderApi.batchSyncToMes(selectedRowKeys.value);
如果 selectedRowKeys.value 是:
[101, 103]
那前端实际交给 API 的就是:
orderApi.batchSyncToMes([101, 103]);
九、第四步:前端 API 请求哪个地址
前端 API 定义在:
apps/crm-antd/src/api/crm.ts
销售订单推送 MES 相关接口是:
// 推送 MES
syncToMes: (id: any) => defHttp.post(`/crm/order/sync/${id}`),
batchSyncToMes: (ids: number[]) => defHttp.post('/crm/order/sync/batch', ids),
这里有两个接口:
| 方法 | 作用 |
|---|---|
syncToMes | 推送单个订单 |
batchSyncToMes | 批量推送多个订单 |
本文重点看批量推送:
batchSyncToMes: (ids: number[]) => defHttp.post('/crm/order/sync/batch', ids)
这表示前端会发送一个 POST 请求:
POST /crm/order/sync/batch
请求体就是订单 ID 数组:
[101, 103]
到这里要再次强调:
前端传的是订单 ID 数组。
不是订单号数组。
不是客户名称数组。
也不是完整订单对象。
后端拿到 ID 后,会自己去数据库查询订单、订单明细、客户等真实数据。
十、第五步:为什么前端路径有 /crm
很多初学者看到这里会有疑问:
前端请求的是 /crm/order/sync/batch。
后端 Controller 上写的却是 /order/sync/batch。
这两个路径为什么对得上?
答案在网关配置里。
网关配置文件:
smartmake-platform-gateway/src/main/resources/gateway-route.yaml
CRM 路由配置如下:
- id: smartmake-platform-crm
uri: lb://smartmake-platform-crm
predicates:
- Path=/crm/**
filters:
- StripPrefix=1
- PreserveHostHeader
这段配置可以分成三部分理解。
第一部分:
Path=/crm/**
表示只要请求路径以 /crm 开头,就匹配这条路由。
第二部分:
uri: lb://smartmake-platform-crm
表示这个请求会被转发给 CRM 服务。这里的 lb 可以理解为 load balance,也就是通过服务名找到对应的 CRM 服务实例。
第三部分:
StripPrefix=1
表示转发给 CRM 服务之前,去掉路径的第一段。
所以路径会这样变化:
浏览器发出:/crm/order/sync/batch
网关匹配:/crm/**
网关去掉第一段:/order/sync/batch
CRM 实际接收:/order/sync/batch
这就是为什么前端 API 里要写 /crm,而后端 Controller 不写 /crm。

十一、第六步:CRM Controller 接收请求
CRM 后端订单 Controller 在这个文件里:
smartmake-platform-crm/src/main/java/com/smartmake/platform/crm/controller/OrderController.java
类上有统一路径:
@RestController
@RequestMapping("/order")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
}
这里的 @RequestMapping("/order") 表示:这个 Controller 下面的接口都以 /order 开头。
批量推送接口是:
@PostMapping("/sync/batch")
@Operation(summary = "批量推送销售订单到MES")
public OrderBatchSyncRspDTO batchSyncToMes(@RequestBody List<Long> ids) {
return orderService.batchSyncToMes(ids);
}
类路径和方法路径合起来就是:
/order + /sync/batch = /order/sync/batch
这刚好能接住网关转发后的请求。

十二、Controller 里的 ids 是什么
重点看这个参数:
@RequestBody List<Long> ids
它表示后端从请求体里接收一个 Long 类型的 ID 列表。
前端请求体是:
[101, 103]
后端接收到后,相当于:
List<Long> ids = Arrays.asList(101L, 103L);
然后 Controller 执行:
return orderService.batchSyncToMes(ids);
也就是说,Controller 并不负责真正推送 MES。它的职责是接收请求、接收参数,然后把业务处理交给 Service。
这是后端里常见的分层思想:
| 层 | 主要职责 |
|---|---|
| Controller | 接收请求、处理入参、返回结果 |
| Service | 编排业务逻辑,例如查订单、组装数据、调用 MES |
| Mapper | 操作数据库 |
| Feign | 调用其他微服务 |
在这个功能里,Controller 只是入口。真正的订单查询、状态判断、DTO 组装、MES 调用,都在 OrderService 里。
十三、用一个例子把流程串起来
假设销售订单列表中有三条数据:
| id | 订单号 | 客户 | 状态 |
|---|---|---|---|
| 101 | SO001 | 客户 A | confirmed |
| 102 | SO002 | 客户 B | draft |
| 103 | SO003 | 客户 C | producing |
因为系统只允许 confirmed 和 producing 状态推送 MES,所以用户能勾选 101 和 103,不能勾选 102。
用户勾选 101 和 103 后,前端得到:
selectedRowKeys.value = [101, 103];
用户点击“推送 MES”,执行:
batchSyncToMes();
方法内部调用:
orderApi.batchSyncToMes([101, 103]);
浏览器发出请求:
POST /crm/order/sync/batch
Body: [101, 103]
请求到达网关后,网关根据配置:
Path=/crm/**
StripPrefix=1
把路径变成:
POST /order/sync/batch
最后进入 CRM 后端:
@PostMapping("/sync/batch")
public OrderBatchSyncRspDTO batchSyncToMes(@RequestBody List<Long> ids) {
return orderService.batchSyncToMes(ids);
}
此时后端拿到的 ids 就是:
[101, 103]
到这里,本文要讲的入口链路就结束了。
十四、几个容易误解的点
1. 前端是不是直接调用 MES?
不是。
前端调用的是 CRM 后端接口:
/crm/order/sync/batch
后续是否调用 MES,是 CRM 后端 Service 的事情。
2. 前端是不是把完整订单数据传给后端?
不是。
前端只传订单 ID 数组:
[101, 103]
完整订单数据由 CRM 后端根据 ID 查询数据库。
3. 为什么前端路径比后端多了 /crm?
因为 /crm 是网关路由前缀。
前端请求先到网关,网关通过 Path=/crm/** 判断这是 CRM 服务的请求,然后通过 StripPrefix=1 去掉 /crm,再转发给 CRM 服务。
4. Controller 为什么不直接写所有业务?
因为 Controller 只应该做入口层工作。
如果把查订单、查明细、组装 DTO、调用 MES、更新同步状态都写在 Controller 里,代码会很难维护。更合理的做法是:Controller 接收请求,Service 处理业务。
十五、本篇总结
这一篇只讲了“销售订单推送 MES”的前端入口链路。
完整流程可以概括为:
用户勾选销售订单
↓
表格通过 row-key="id" 得到订单 ID
↓
selectedRowKeys 保存选中的订单 ID 数组
↓
用户点击“推送 MES”按钮
↓
前端调用 orderApi.batchSyncToMes(ids)
↓
发送 POST /crm/order/sync/batch
↓
网关匹配 /crm/**,并通过 StripPrefix=1 去掉 /crm
↓
CRM 后端 OrderController 接收 /order/sync/batch
↓
Controller 把 ids 交给 OrderService
这一篇最重要的结论是:
前端只负责触发推送动作,并把选中的订单 ID 发给 CRM 后端。
完整订单数据、订单明细、DTO 组装、MES 服务调用,都应该由 CRM 后端完成。
理解了这一点,再看后面的 Service、DTO、Feign 调用,就不会乱。
十六、下篇预告
下一篇继续分析 CRM 后端拿到订单 ID 后做了什么。
重点会看:
- CRM 如何根据订单 ID 查询完整销售订单。
- 如何查询订单明细、客户、产品等关联数据。
- 如何把 CRM 订单转换成 MES 需要的请求 DTO。
- 如何通过 Feign 调用 MES 内部接口。
- 推送成功或失败后,CRM 如何更新 MES 同步状态和失败原因。
也就是从这一行继续往下看:
orderService.batchSyncToMes(ids);
更多推荐
所有评论(0)