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

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

相关文章

MyBatis分页查询实战案例完整流程

《MyBatis分页查询实战案例完整流程》MyBatis是一个强大的Java持久层框架,支持自定义SQL和高级映射,本案例以员工工资信息管理为例,详细讲解如何在IDEA中使用MyBatis结合Page... 目录1. MyBATis框架简介2. 分页查询原理与应用场景2.1 分页查询的基本原理2.1.1 分

JWT + 拦截器实现无状态登录系统

《JWT+拦截器实现无状态登录系统》JWT(JSONWebToken)提供了一种无状态的解决方案:用户登录后,服务器返回一个Token,后续请求携带该Token即可完成身份验证,无需服务器存储会话... 目录✅ 引言 一、JWT 是什么? 二、技术选型 三、项目结构 四、核心代码实现4.1 添加依赖(pom

redis-sentinel基础概念及部署流程

《redis-sentinel基础概念及部署流程》RedisSentinel是Redis的高可用解决方案,通过监控主从节点、自动故障转移、通知机制及配置提供,实现集群故障恢复与服务持续可用,核心组件包... 目录一. 引言二. 核心功能三. 核心组件四. 故障转移流程五. 服务部署六. sentinel部署

SpringBoot集成XXL-JOB实现任务管理全流程

《SpringBoot集成XXL-JOB实现任务管理全流程》XXL-JOB是一款轻量级分布式任务调度平台,功能丰富、界面简洁、易于扩展,本文介绍如何通过SpringBoot项目,使用RestTempl... 目录一、前言二、项目结构简述三、Maven 依赖四、Controller 代码详解五、Service

基于Python实现自动化邮件发送系统的完整指南

《基于Python实现自动化邮件发送系统的完整指南》在现代软件开发和自动化流程中,邮件通知是一个常见且实用的功能,无论是用于发送报告、告警信息还是用户提醒,通过Python实现自动化的邮件发送功能都能... 目录一、前言:二、项目概述三、配置文件 `.env` 解析四、代码结构解析1. 导入模块2. 加载环

linux系统上安装JDK8全过程

《linux系统上安装JDK8全过程》文章介绍安装JDK的必要性及Linux下JDK8的安装步骤,包括卸载旧版本、下载解压、配置环境变量等,强调开发需JDK,运行可选JRE,现JDK已集成JRE... 目录为什么要安装jdk?1.查看linux系统是否有自带的jdk:2.下载jdk压缩包2.解压3.配置环境

MySQL 临时表与复制表操作全流程案例

《MySQL临时表与复制表操作全流程案例》本文介绍MySQL临时表与复制表的区别与使用,涵盖生命周期、存储机制、操作限制、创建方法及常见问题,本文结合实例代码给大家介绍的非常详细,感兴趣的朋友跟随小... 目录一、mysql 临时表(一)核心特性拓展(二)操作全流程案例1. 复杂查询中的临时表应用2. 临时

Linux查询服务器系统版本号的多种方法

《Linux查询服务器系统版本号的多种方法》在Linux系统管理和维护工作中,了解当前操作系统的版本信息是最基础也是最重要的操作之一,系统版本不仅关系到软件兼容性、安全更新策略,还直接影响到故障排查和... 目录一、引言:系统版本查询的重要性二、基础命令解析:cat /etc/Centos-release详

更改linux系统的默认Python版本方式

《更改linux系统的默认Python版本方式》通过删除原Python软链接并创建指向python3.6的新链接,可切换系统默认Python版本,需注意版本冲突、环境混乱及维护问题,建议使用pyenv... 目录更改系统的默认python版本软链接软链接的特点创建软链接的命令使用场景注意事项总结更改系统的默

MySQL 升级到8.4版本的完整流程及操作方法

《MySQL升级到8.4版本的完整流程及操作方法》本文详细说明了MySQL升级至8.4的完整流程,涵盖升级前准备(备份、兼容性检查)、支持路径(原地、逻辑导出、复制)、关键变更(空间索引、保留关键字... 目录一、升级前准备 (3.1 Before You Begin)二、升级路径 (3.2 Upgrade