履约系统接单制作流程设计方案

2024-01-24 13:30

本文主要是介绍履约系统接单制作流程设计方案,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

1.制作单创建

1.1消费MQ消息创建制作单流程

 1.1.1创建制作单入库

1.1.2 基于ZSET结构对订单拆分后的所有制作单按 时间先后排序 进行存储

1.1.3 基于HASH结构对每个制作单组合中的每个单产品制作单的详情进行存储

1.1.4 为订单创建一个或多个门店下制作单组合标识存储到SET结构的门店制作单池中

 1.2任务调度pull方案流程

2.制作单接单和制作中状态流转

2.1基于分片策略的任务调度实现自动接单和制作单状态流转

 2.2自动接单和制作中状态流转的实现

2.2.1遍历当前门店下的产品组合单号SET集合,获取【最近30s】内创建的单菜品制作单号集合

2.2.2获取系统预设的该类产品组合的单菜品的最大可同时制作数量

2.2.3获取该类产品组合下的所有单菜品制作单信息

2.2.4遍历该产品组合下的所有单菜品制作单集合信息 筛选出该产品组处于【制作中状态】的制作单待制作的菜品的数量

2.2.5.遍历产品组合编号下的所有制作单集合,筛选出可接单的制作单集合,可加入制作流转为制作中的制作单集合

2.2.6执行接单和制作中状态流转


履约系统上游是订单支付操作完成,履约流程从支付完成后创建制作单,接单制作,制作完成,出餐配送一系列流程节点,还包括制作单取消(制作单创建但未接单制作),退货等流程节点。

1.制作单创建

考虑高峰期存在订单量激增的情况,订单支付到履约单创建环节的通信使用MQ消息+任务调度pull做兜底方案实现支付后创建履约单的流程操作。

1.1消费MQ消息创建制作单流程

  1. 接收到MQ消息,创建制作单入库(状态与订单同步为待商家接单状态);
  2. 以门店维度按支付完成时间排序基于Redis的zset结构保存制作单到制作队列中
  3. 以门店维度基于Hash结构保存制作单号和制作单的详情信息。
  4. 设置门店订单标识用于后续节点流程处理。

制作单是单菜品粒度的制作信息,一个订单到达履约系统后会被拆分为一个或多个产品组合,一个或多个产品组合与原订单是一对多的逻辑关联关系;每个产品组合下包含一个或多个单菜品,这个菜品的制作信息即制作单。所以要定位到一个制作单,它的层级是某个门店下的某个产品组合下的一个元素。 

 1.1.1创建制作单入库

这一步就是根据订单拆分创建制作单集合,保存原订单基本信息和制作单列表集合。

1.1.2 基于ZSET结构对订单拆分后的所有制作单按 时间先后排序 进行存储

把订单拆分后单分组后的产品组单号ProductNo,以订单提交时间为对应的score放入ZSET中 。这里要说明一下,制作队列中数据是以一个制作单组合为原子粒度,也就是说制作队列中不区分订单,之所以这么存储,是因为这样能保证产品的制作在订单粒度是并行的。怎么理解呢?

举个例子:像一些现成的产品例如饮料不用等待上一个订单的全部产品制作单都完成后再进行制作,但是单产品的制作顺序还是按照订单时间顺序来制作的。

// KEY结构:"UNACCEPT_MAKEORDER_SHOPS_" +shopMdCode+'_'+productNopublic void tempSaveUnAcceptMakeOrderShop(final Long shopMdCode,List<MakeOrderItemDTO> makeOrders) {//用当前时间times作为scoreLong times = Calendar.getInstance().getTimeInMillis() / 1000;//把一个订单的制作单数据 按产品组合分类 几个产品为一个组合,每个组合对应一个ProductNoMap<String, List<MakeOrderItemDTO>> productSetNoMap = makeOrders.stream().collect(Collectors.groupingBy(MakeOrderItemDTO::getProductSetNo));productSetNoMap.forEach( (productNo,productSetNoOrderList) -> {//加入门店制作单ZSET集合中Set<ZSetOperations.TypedTuple<String>> typedTupleSet = new HashSet<>(16);productSetNoOrderList.forEach(m -> {typedTupleSet.add(new DefaultTypedTuple( m.getMakeOrderNo(), times));});//存储时以门店下的产品组合为最小粒度redisTemplate.opsForZSet().add("UNACCEPT_MAKEORDER_SHOPS_" +shopMdCode+'_'+productNo, typedTupleSet);log.info("存储新建状态的制作单"+getUnAcceptKey(shopMdCode,productNo) +",typedTupleSet"+JSON.toJSONString(typedTupleSet));});
}

1.1.3 基于HASH结构对每个制作单组合中的每个单产品制作单的详情进行存储

以制作单拆分后的产品组号为KEY,存储门店下某个制作单产品组下的各个产品的制作详情数据,在整个流程中,这个hash结构中存储已新建和制作中状态的详情信息。基于这种存储结构,在处理制作单时可以根据1步骤中设置在ZSET中的产品组号和产品制作单号 从当前存储结构中 获取对应的单产品的制作单的详情信息。 

"MAKE_POOL_"+shopCode+"_"+productNo orderNo oderItempublic void putMakeOrderList(final List<MakeOrderItemDTO> makeOrders) {if (makeOrders != null) {makeOrders.forEach((orderItemDTO)->{if (StringUtils.isBlank(orderItemDTO.getMakeOrderNo())) {throw new RuntimeException("制作单编号不能为空");}MakeOrderItemDTO makeOrderNew = new MakeOrderItemDTO();BeanUtils.copyProperties(orderItemDTO, makeOrderNew);redisTemplate.opsForHash().put('MAKE_POOL_'+makeOrderNew.getShopMdCode()+'_'+makeOrderNew.getProductNo()), makeOrderNew.getMakeOrderNo(), makeOrderNew);});}
}

1.1.4 为订单创建一个或多个门店下制作单组合标识存储到SET结构的门店制作单池中

池中的一个或多个标识对应一笔订单,后续流程以门店粒度去处理门店下的订单的制作单时,根据池中的一个或多个制作单组合标识(对应一个订单),去ZSET中获取到该门店下某一笔订单的所有的单产品制作单号,然后根据单号在HASH结构中获取每个单产品制作单的详情信息。

public void setShopMdCodeSetInRedis(List<MakeOrderItemDTO> makeOrderItemList) {makeOrderItemList.forEach(makeOrderItem ->{redisTemplate.opsForSet().add("ShopCode", makeOrderItem.getShopMdCode() + "_" + makeOrderItem.getProductNo());});
}

 1.2任务调度pull方案流程

任务调度是作为订单支付后发送创建制作单的MQ消息丢失的一个兜底方案。设置合理频率例如五分钟执行一次主动以RPC方式加分页限制拉取当前时间前十分钟前推两小时范围内处于提交履约单状态的订单号集合,然后查询DB中同时间范围内的制作单的订单号集合,两个集合取差集,剩下的可以认为是MQ未成功通知创建制作单的订单信息。对这些订单执行与上面MQ消费创建制作单相同操作。

2.制作单接单和制作中状态流转

制作单创建后出于创建(待商家接单)状态。后续使用任务调度框架使用按门店进行分片的方案进行自动接单和制作单后续状态流转处理。该方案分摊了每个服务节点的压力。

2.1基于分片策略的任务调度实现自动接单和制作单状态流转

从Redis中获取制作单池中所有的的订单产品组合标识的元素集合,遍历产品组合单号SET集合,根据分片逻辑筛选出当前服务节点要处理的门店下的所有订单的产品组合单号,按门店分类基于map存储,key是门店编号,value是产品组合单号的Set集合。

遍历门店单号,基于线程池提交异步执行每个门店下的制作单处理任务,即每个线程处理某一个门店下的所有产品组合下的所有单产品制作单的状态。

//key是门店编号,set集合存放所有该门店下的订单产品组合标识
Map<Long,Set<String>> shopMdCodeMap = new HashMap<>();
//获取创建制作单流程中保存的所有的订单的产品组合标识
Set<String> set = redisTemplate.opsForSet().members("SHOP_MD_CODE");
//遍历产品组合标识,根据分片逻辑筛选指定门店下的所有产品组合标识,以门店为单位分组
shopMdCodeSetRedis.forEach((strKey) -> {String[] strs = strKey.split("_");Long shopMdCode = Long.parseLong(strs[0]);Long remainder = shopMdCode % shardTotal;if(Long.toString(remainder).equals(Integer.toString(shardIndex))){Set<String> set = shopMdCodeMap.get(shopMdCode);if(null == set){set = new HashSet<>();}set.add(strs[1]);shopMdCodeMap.put(shopMdCode,set);}
});
for(Map.Entry entry:shopMdCodeMap.entrySet()){Long shopMdCode = (Long) entry.getKey();//门店编号Set<String> productSetNoSet = (Set<String>)entry.getValue();//某一门店下所有订单的所有产品组合编号的集合ExecutorUtils.submit(() -> {doMakeOrderStateFlow(shopMdCode,productSetNoSet);});
}

 2.2自动接单和制作中状态流转的实现

任务调度使用线程池实现异步处理每个门店下的制作单接单和状态流转。线程任务逻辑流程如下:

2.2.1遍历当前门店下的产品组合单号SET集合,获取【最近30s】内创建的单菜品制作单号集合

从Redis的制作队列中获取每个产品组合编号下的所有单产品制作单号,按产品组合编号分组;这一步其实是根据创建制作单流程中ZSET制作队列的存储设计,从ZSET中获取门店下的所有单菜品制作单号集合。

2.2.2获取系统预设的该类产品组合的单菜品的最大可同时制作数量

这个数量值决定了处于接单状态的制作单多久能流转为制作中状态进行制作。

2.2.3获取该类产品组合下的所有单菜品制作单信息

根据当前门店编号和当前的产品组合编号批量获取门店下该产品组合下的所有单菜品制作单信息,即得到一个单品制作单信息对象的集合。

2.2.4遍历该产品组合下的所有单菜品制作单集合信息 筛选出该产品组处于【制作中状态】的制作单待制作的菜品的数量

如果当前出于制作中的制作单小于最大可同时制作数量,则可以把一定数量的接单状态的制作单流转为制作中状态,制作单可以开始制作。

2.2.5.遍历产品组合编号下的所有制作单集合,筛选出可接单的制作单集合,可加入制作流转为制作中的制作单集合

此处判断如果4步骤中【待制作的菜品的数量 】小于【单菜品的最大可同时制作数量】,则筛选出超过等待时长可流转为接单状态的制作单(设置为接单状态)和已经出于接单状态的制作单,按制作单创建时间排序生成【接单状态的制作集合】,然后从集合取最大可同时制作数量与当前待制作数量之差 个制作单,生成新的【可加入制作的制作单集合】。

如果【待制作的菜品的数量 】大于【单菜品的最大可同时制作数量】,也就是此时无法将任何接单状态的制作单加入制作,所以只筛选出超过等待时长可流转为接单状态的制作单,设置为接单状态,生成【接单状态的制作单集合】

2.2.6执行接单和制作中状态流转

将步骤5中筛选好的【接单状态的制作单集合】和【可加入制作的制作单集合】两个集合分别进行接单状态流转处理和制作中状态流转处理。

public doMakeOrderStateFlow(shopMdCode,productSetNoSet){//key是产品组合编号,value是组合下所有的制作单号的set集合Map<String,Set<String>> makeOrderNoMap = new HashMap<>(16);productSetNoSet.forEach(productSetNo ->{// 从待结单队列中获取该门店最近30秒内的已创建的(待接单状态)制作单列表//【此处的扫描区间一定要大于后面判断是否接单逻辑中的接单等待时间,否则就会造成某些创建状态的制作单因扫描不到而不能接单的情况】Long times = (Calendar.getInstance().getTimeInMillis() - 30000L) / 1000;//取出score{times}秒前面的门店制作单数据Set<ZSetOperations.TypedTuple<String>> createdMakeOrderNoSetForProductNo =  (Set<ZSetOperations.TypedTuple<String>>) redisTemplate.opsForZSet().rangeByScoreWithScores("UNACCEPT_MAKEORDER_SHOPS_" +shopMdCode+'_'+productSetNo, 0, times);//如果为空直接返回if (CollectionUtils.isEmpty(createdMakeOrderNoSetForProductNo)) {return;}createdMakeOrderNoSetForProductNo.forEach(makeOrderNo -> {if (makeOrderNo == null || StringUtils.isEmpty(makeOrderNo.getValue())) {return;}Set<String> makeOrderNoSet = makeOrderNoMap.get(makeOrderNo);if(null == makeOrderNoSet){makeOrderNoSet = new HashSet<>();}makeOrderNoSet.add(makeOrderNo.getValue());makeOrderNoMap.put(productSetNo,makeOrderNoSet);});});//根据系统后台配置的产品组合配置信息获取某单菜品可同时制作的数量上限int maxQuantity = 0;ProductSetDTO productSetDTO = productSetService.getProductSetByNo(productSetNo);maxQuantity = productSetDTO.getMaxQuantity();//可进入制作中状态的制作单集合List<MakeOrderItemDTO> inMakingCandidateList = new ArrayList<>();//可进入接单状态和已经处于接单状态的制作单集合List<MakeOrderItemDTO> acceptCandidateList = new ArrayList<>();productSetNos.forEach(productSetNo -> {Set<String> makeOrderNoSet = makeOrderNoMap.get(productSetNo);if(null == makeOrderNoSet){log.info("productSetNo制作单位空"+productSetNo);return;}//根据门店编号和产品组合编号定位到HASH结构的key,批量获取key中多个单菜品制作单号对应的详情信息,即制作单信息对象的集合。List<MakeOrderItemDTO>  makeOrders = new ArrayList<>();List<Object> list = redisTemplate.opsForHash().multiGet("MAKE_POOL_"+shopMdCode+"_"+productSetNo, makeOrderNoSet);list.forEach(obj -> {if(null != obj){makeOrders.add((MakeOrderItemDTO) obj);}});long timeInMillis = Calendar.getInstance().getTimeInMillis();if (CollectionUtils.isNotEmpty(makeOrders)) {MakeOrderPoolConfigPO poolConfig = CfcenterConfigs.MAKE_ORDER_POOL_CONFIG.get();//遍历制作池中的单菜品制作单集合信息 筛选出处于制作中状态的制作单需要的菜品的数量long inMakingCount = makeOrders.stream().filter(m -> MakeOrderStateEnum.IN_MAKING.getCode().equals(m.getMakeStatus())).mapToInt(MakeOrderItemDTO::getQuantity).sum();log.info("当前制作中的菜品数量inMakingCount"+inMakingCount);//如果制作中的菜品数没有达到菜品设置的同时制作上限if (inMakingCount < maxQuantity) {//筛选出已接单的制作单集合,同时把超过30s等待的创建状态的制作单也设置为已接单状态放进集合里筛选出来,按制作单创建时间排序。List<MakeOrderItemDTO> candidateList = makeOrders.stream().filter(makeOrderItemDTO -> {//已经过了30s等待期的处于创建状态的制作,可以作为候选制作单if (makeOrderItemDTO.getCreateTime().getTime() + 30000L < timeInMillis && MakeOrderStateEnum.CREATED.getCode().equals(makeOrderItemDTO.getMakeStatus())) {log.info("进入已接单"+makeOrderItemDTO.getOrderNo());makeOrderItemDTO.setMakeStatus(MakeOrderStateEnum.ACCEPTED.getCode());acceptCandidateList.add(makeOrderItemDTO);return true;}log.info("当前制作getMakeStatus()"+makeOrderItemDTO.getMakeStatus());return MakeOrderStateEnum.ACCEPTED.getCode().equals(makeOrderItemDTO.getMakeStatus());}).sorted(Comparator.comparing(MakeOrderItemDTO::getCreateTime)).collect(Collectors.toList());//如果接单状态的制作单集合不为空,按先后顺序选取接单状态的制作单设置为制作中,达到菜品制作上限为止。if (!CollectionUtils.isEmpty(candidateList)) {log.info("制作中candidateList"+ JSON.toJSONString(candidateList));for (MakeOrderItemDTO m : candidateList) {if (inMakingCount < maxQuantity) {log.info("进入制作中"+m.getOrderNo()+"inMakingCount"+inMakingCount+",maxQuantity"+maxQuantity);//制作中的商品数量未达最大值,直接进入制作池开始制作,无需判断是否超出队列inMakingCount += m.getQuantity();m.setMakeStatus(MakeOrderStateEnum.IN_MAKING.getCode());inMakingCandidateList.add(m);}}}} else {//制作池已满 仅仅筛选超过【10s】等待接单时间的创建状态的制作单流转为已接单状态,放入接单集合List<MakeOrderItemDTO> candidateList = makeOrders.stream().filter(m -> {//是否已经过了等待期if (m.getCreateTime().getTime() + poolConfig.getWaitAcceptTimeByType(m.getOrigin() + "") < timeInMillis && MakeOrderStateEnum.CREATED.getCode().equals(m.getMakeStatus())) {//变为已接单m.setMakeStatus(MakeOrderStateEnum.ACCEPTED.getCode());return true;}return false;}).collect(Collectors.toList());acceptCandidateList.addAll(candidateList);}}});//如果可进入接单状态和已经处于接单状态的制作单集合不为空,对这些制作单做【流转为接单状态操作】if (!CollectionUtils.isEmpty(acceptCandidateList)) {log.error("门店{}触发制作单进入接单节点数据{}", shopMdCode, JSONObject.toJSONString(makeOrderCandidate));MakeOrderContext makeOrderContext = new MakeOrderContext();makeOrderContext.setShopMdCode(shopMdCode);makeOrderContext.setLastState(MakeOrderStateEnum.CREATED);makeOrderContext.setCurState(MakeOrderStateEnum.ACCEPTED);makeOrderContext.setMakeOrderItemList(makeOrderCandidate.getAcceptCandidateList());makeOrderContext.setOperateSource(MakeOrderOperateSourceEnum.SYSTEM.getCode());makeOrderContext.setOperateEmp(Constant.SYSTEM_USER_ID);makeOrderContext.setOperateEmpName(Constant.SYSTEM_USER_NAME);makeOrderContext.setOperateTime(new Date());makeOrderFlowService.doAction(makeOrderContext);}//如果可进入制作中状态的制作单集合不为空,对这些制作单做【流转为制作中状态的操作】if (!CollectionUtils.isEmpty(inMakingCandidateList)) {log.error("门店{}触发制作单进入制作中节点数据{}", shopMdCode, JSONObject.toJSONString(makeOrderCandidate));MakeOrderContext makeOrderContext = new MakeOrderContext();makeOrderContext.setShopMdCode(shopMdCode);makeOrderContext.setLastState(null);makeOrderContext.setCurState(MakeOrderStateEnum.IN_MAKING);makeOrderContext.setMakeOrderItemList(makeOrderCandidate.getInmakingCandidateList());makeOrderContext.setOperateSource(MakeOrderOperateSourceEnum.SYSTEM.getCode());makeOrderContext.setOperateEmp(Constant.SYSTEM_USER_ID);makeOrderContext.setOperateEmpName(Constant.SYSTEM_USER_NAME);makeOrderContext.setOperateTime(new Date());makeOrderFlowService.doAction(makeOrderContext);}
}

这篇关于履约系统接单制作流程设计方案的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/639841

相关文章

使用Node.js制作图片上传服务的详细教程

《使用Node.js制作图片上传服务的详细教程》在现代Web应用开发中,图片上传是一项常见且重要的功能,借助Node.js强大的生态系统,我们可以轻松搭建高效的图片上传服务,本文将深入探讨如何使用No... 目录准备工作搭建 Express 服务器配置 multer 进行图片上传处理图片上传请求完整代码示例

利用Python快速搭建Markdown笔记发布系统

《利用Python快速搭建Markdown笔记发布系统》这篇文章主要为大家详细介绍了使用Python生态的成熟工具,在30分钟内搭建一个支持Markdown渲染、分类标签、全文搜索的私有化知识发布系统... 目录引言:为什么要自建知识博客一、技术选型:极简主义开发栈二、系统架构设计三、核心代码实现(分步解析

Python FastAPI+Celery+RabbitMQ实现分布式图片水印处理系统

《PythonFastAPI+Celery+RabbitMQ实现分布式图片水印处理系统》这篇文章主要为大家详细介绍了PythonFastAPI如何结合Celery以及RabbitMQ实现简单的分布式... 实现思路FastAPI 服务器Celery 任务队列RabbitMQ 作为消息代理定时任务处理完整

Linux系统中卸载与安装JDK的详细教程

《Linux系统中卸载与安装JDK的详细教程》本文详细介绍了如何在Linux系统中通过Xshell和Xftp工具连接与传输文件,然后进行JDK的安装与卸载,安装步骤包括连接Linux、传输JDK安装包... 目录1、卸载1.1 linux删除自带的JDK1.2 Linux上卸载自己安装的JDK2、安装2.1

Linux系统之主机网络配置方式

《Linux系统之主机网络配置方式》:本文主要介绍Linux系统之主机网络配置方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、查看主机的网络参数1、查看主机名2、查看IP地址3、查看网关4、查看DNS二、配置网卡1、修改网卡配置文件2、nmcli工具【通用

Linux系统之dns域名解析全过程

《Linux系统之dns域名解析全过程》:本文主要介绍Linux系统之dns域名解析全过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、dns域名解析介绍1、DNS核心概念1.1 区域 zone1.2 记录 record二、DNS服务的配置1、正向解析的配置

Linux系统中配置静态IP地址的详细步骤

《Linux系统中配置静态IP地址的详细步骤》本文详细介绍了在Linux系统中配置静态IP地址的五个步骤,包括打开终端、编辑网络配置文件、配置IP地址、保存并重启网络服务,这对于系统管理员和新手都极具... 目录步骤一:打开终端步骤二:编辑网络配置文件步骤三:配置静态IP地址步骤四:保存并关闭文件步骤五:重

Spring AI ectorStore的使用流程

《SpringAIectorStore的使用流程》SpringAI中的VectorStore是一种用于存储和检索高维向量数据的数据库或存储解决方案,它在AI应用中发挥着至关重要的作用,本文给大家介... 目录一、VectorStore的基本概念二、VectorStore的核心接口三、VectorStore的

python之流程控制语句match-case详解

《python之流程控制语句match-case详解》:本文主要介绍python之流程控制语句match-case使用,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐... 目录match-case 语法详解与实战一、基础值匹配(类似 switch-case)二、数据结构解构匹

Windows系统下如何查找JDK的安装路径

《Windows系统下如何查找JDK的安装路径》:本文主要介绍Windows系统下如何查找JDK的安装路径,文中介绍了三种方法,分别是通过命令行检查、使用verbose选项查找jre目录、以及查看... 目录一、确认是否安装了JDK二、查找路径三、另外一种方式如果很久之前安装了JDK,或者在别人的电脑上,想