portal是面向货主的一个门户网站。客户可以在portal上新增订单。现在要实现的就是portal新建的订单(包括出库单和入库单),能同步到OMS系统和WMS系统。
一、最初的方案:
OMS已经有一套对接ERP系统的接口,包括接收入库单和出库单。可谓前人种树后人乘凉,原本是按照约定参数拼起来直接调调就可以了。可是呢,测试着测试着发现荆棘满坑啊。
portal点击确认下单(出库单或者入库单),直接调用oms的接单接口(oms保存订单,同时推送订单到wms)。这种方式由于使用到分布式事务,太耗时,执行时长能到到30s以上,调用时总是出现超时发生熔断。
显而易见,此情此景简单的拿来主义是不行了,开始使用MQ,拆。
二、改进版本一:业务拆分,portal下单,不再直接调用接口,而是发送消息,消息主题:oms_receive_order。
1.增加portal订单状态:已下单、接单成功、接单失败。
当portal点击确认下单后,修改状态:已下单。
2.消费方OmsReceiveOrderConsumer:调用oms接单接口(oms保存订单,同时推送订单到wms)。当wms订单保存成功后,发消息回写接单成功,消息主题:storage_order_receive。
3.消费方StorageOrderReceiveConsumer消费消息,修改portal出库单状态(已接单或者接单失败)。
这样改进,portal订单下单操作不在超时,能立即响应。但是依旧不能保存wms订单,和上面原因一致。
继续拆分。
三、改进版本二:继续拆分。
1. portal确认下单,发送消息到MQ,主题:storage_portal_to_oms。
当portal点击确认下单后,修改状态:已下单。
2. 消费方StoragePortalToOmsConsumer消费消息,调用oms业务接口:生成订单接口。
oms生成订单接口:发消息到MQ(主题:storage_oms_to_wms),修改oms订单状态:已下发。
3. 消费方StorageOmsToWmsConsumer消费消息。
3.1 调用wms业务接口:生成wms订单。
3.1 消费方,起个线程更改portal订单状态。
使用线程池:ExecutorService executorService = Executors.newFixedThreadPool(10);
3.2 wms生成订单失败或者portal修改订单失败,会发消息到钉钉。
四、测试消息发送
1.测试工具:postman
本地启动mq服务,请求地址:127.0.0.1:8004/satMq/mqProduce
post请求body:{
"mqTypeEnum": "STORAGE_PORTAL_TO_OMS",
"code": "6",
"data": {"updateUser":0,"state":4,"customerOrderNo":"CKYWHS1201907310003"}
}
StoragePortalToOmsConsumer消费方就能接收到消息,进行消费。
2. 可以使用xxlmessage 页面管理工具,进行消息测试。
xxlmessage联调地址:http://192.168.173.21:8070/message
可以添加一条消息:消息主题、消息分组:default;消息数据;状态:NEW,点击保存就可以发送消息。
也可以重发消息,将发送失败的消息,状态改成NEW,就可以重发消息。
五、附录: 贴一下关键代码
5.1. 生产者
//通知oms接单
MqDTO mqDTO = new MqDTO();
mqDTO.setMqTypeEnum(STORAGE_PORTAL_TO_OMS);
mqDTO.setData(JSONObject.toJSONString(receiveOrderPortalDTO));
if (!mqProducerApi.produer(mqDTO)){
distributedLocker.unlock(String.valueOf(LockTypeEnum.STORAGE_SYNC_OMS.getCode()) + id);
throw new BaseException("oms入库单接单失败");
}
5.2.消费者
@Service
@Slf4j
@MqConsumer(topic = "storage_portal_to_oms")
public class StoragePortalToOmsConsumer implements IMqConsumer{
@Resource
ReceiveOrderApi receiveOrderApi;
/** 入库单,portal推送入库单到oms*/
@Override
public MqResult consume(String data) {
ReceiveOrderPortalDTO dto = JSONObject.parseObject(data,ReceiveOrderPortalDTO.class);
BaseResultVo vo = receiveOrderApi.receiveOrderInsert(dto);
if (vo.getStatus() == BaseResultVo.SUCCESS_CODE){
return MqResult.SUCCESS;
}
return MqResult.FAIL;
}
}
5.3 oms订单同步到wms
// 收货订单直接同步到wms
BaseResultVo vo = asnApi.syncOmsOrder(asnDTO);
Callable<BaseResultVo> c = new Callable<BaseResultVo>() {
@Override
public BaseResultVo call() throws Exception {
JSONObject json = new JSONObject();
if (BaseResultVo.SUCCESS_CODE.equals(vo.getStatus())) {
//接单成功
json.put("status", OrderEnum.StorageOrderStatusEnum.order_received.getId());
} else {
//接单失败
json.put("status", OrderEnum.StorageOrderStatusEnum.fail_received.getId());
}
json.put("customerOrderNo", asnDTO.getCustomerOrderNo());
json.put("updateUser", 0);
BaseResultVo resultVo = ptStorageOrderApi.updateStatus(json.toJSONString());
return resultVo;
}
};
Future<BaseResultVo> ft = executorService.submit(c);
Long startTime = System.currentTimeMillis();
while (true) {
if (System.currentTimeMillis() - startTime > 5000) {
executorService.shutdown();
//接单失败,发送钉钉
dingDingApi.sendDingMsgForService("storage_oms_to_wms 消费超时", "15967103436");
log.error("storage_oms_to_wms 消费超时:" + asnDTO.getCustomerOrderNo());
return MqResult.FAIL;
}
if (ft.isDone()){
BaseResultVo resultVo = ft.get();
if(resultVo.getStatus() == BaseResultVo.ERROR_CODE){
//接单失败,发送钉钉
dingDingApi.sendDingMsgForService("storage_oms_to_wms 接单失败", "15967103436");
return MqResult.FAIL;
}
return MqResult.SUCCESS;
}
}
5.4 更新订单状态时,使用数据库状态机做幂等。
int updateStatusCallBack(@Param("outboundNo") String outboundNo, @Param("state") Integer state, @Param("updateUser") Long updateUser);
<update id="updateStatusCallBack">
update pt_storage_order
set update_user = #{updateUser,jdbcType=BIGINT},update_time=now(),
status = (case when status=2 and #{status} in (3,5) then #{status,jdbcType=INTEGER}
when status=3 and #{status}=4 then 4
when status=4 and #{status}=6 then 6
else status end)
where storage_no= #{storageNo,jdbcType=VARCHAR}
</update>
出库单流程同上。
附上流程图:
上面略去了失败,发送钉钉消息的流程。
待考虑:
1. xxl消息失败告警:支持以Topic粒度监控消息,存在失败消息时主动推送告警邮件;默认提供邮件方式失败告警,同时预留扩展接口,可方面的扩展短信、钉钉等告警方式;
目前是按照消息的维度,程序中主动调用接口发送钉钉消息。
但是没有配置消息的重试次数, 即失败了就不再重试。目前的失败补偿策略就是发送钉钉消息,人工干预。不知道将来是否考虑增加重试,譬如衰减方式重试。
2. 订单处理的过程中多次用到了 一级单位和二级单位的转换。portal、oms、wms三个系统都有涉及。是否能一个地方转,其他都不做处理。