本文用一个真实业务功能拆解微服务调用:CRM 销售订单推送 MES。第一篇先讲清楚最前面的入口链路,也就是用户在前端点击“推送 MES”之后,请求如何从 CRM 前端进入 CRM 后端 Controller。至于 CRM 后端如何查询订单、组装 DTO、通过 Feign 调 MES,会放到下一篇继续讲。

读完本文你能搞懂什么

如果你刚开始接触微服务,最容易卡住的地方通常不是代码语法,而是这几个问题:

  1. 前端点击一个按钮,到底请求了哪个接口?
  2. 为什么前端请求地址是 /crm/order/sync/batch,后端 Controller 却写的是 /order/sync/batch
  3. 前端推送 MES 时,是不是把整张订单数据都传给 MES?
  4. 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.javaCRM 后端订单 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订单号客户
101SO001客户 A
102SO002客户 B
103SO003客户 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订单号客户状态
101SO001客户 Aconfirmed
102SO002客户 Bdraft
103SO003客户 Cproducing

因为系统只允许 confirmedproducing 状态推送 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 后做了什么。

重点会看:

  1. CRM 如何根据订单 ID 查询完整销售订单。
  2. 如何查询订单明细、客户、产品等关联数据。
  3. 如何把 CRM 订单转换成 MES 需要的请求 DTO。
  4. 如何通过 Feign 调用 MES 内部接口。
  5. 推送成功或失败后,CRM 如何更新 MES 同步状态和失败原因。

也就是从这一行继续往下看:

orderService.batchSyncToMes(ids);

更多推荐