day09-智能调度之取派件调度

课程安排

  • 了解快递员取派件任务需求
  • 递员取派件任务相关功能开发
  • 调度中心任务调度
  • 整体业务功能测试

本章基础:对派件任务的增删改查

本章重点:

1更新取派件任务状态(根据不同的状态做不同的更新)

2调度中心(根据一定的规则选择一个快递员)

3生成任务(根据快递员id生成对应取派件任务,将数据保存入数据库)

1、背景说明

通过前面的学习,可以通过作业范围来确定网点或快递员。

下面要做的事情就是需要为快递员生成取派件任务

2、需求分析

快递员在登录到APP后,可以查看取派件任务列表

具体需求参见《快递员端产品》文档。

3、实现分析

3.1、表结构

快递员的取件和派件类型不同外其他的属性基本都是一样的,存储在一张表sl_pickup_dispatch_task中。

取派件任务存储在sl_work数据库中。

task_type:任务类型,1为取件任务,2为派件任务

status,sign_status,sign_recipient

agency_id,courier_id:关联的两个id

另:几个时间。estimated_start_time,actual_start_time,estimated_end_time,actual_end_time,cancel_time,created,updated

is_deleted:删除:0-否,1-是(取派件任务在快递员删除时为逻辑删除,防止后续需要数据恢复的情况)

CREATE TABLE `sl_pickup_dispatch_task` (
  `id` bigint NOT NULL COMMENT 'id',
  `order_id` bigint NOT NULL COMMENT '关联订单id',
  `task_type` tinyint DEFAULT NULL COMMENT '任务类型,1为取件任务,2为派件任务',
  `status` int DEFAULT NULL COMMENT '任务状态,1为新任务、2为已完成、3为已取消',
  `sign_status` int DEFAULT '0' COMMENT '签收状态(0为未签收, 1为已签收,2为拒收)',
  `sign_recipient` tinyint DEFAULT '0' COMMENT '签收人,1本人,2代收',
  `agency_id` bigint DEFAULT NULL COMMENT '网点ID',
  `courier_id` bigint DEFAULT NULL COMMENT '快递员ID',
  `estimated_start_time` datetime DEFAULT NULL COMMENT '预计取/派件开始时间',
  `actual_start_time` datetime DEFAULT NULL COMMENT '实际开始时间',
  `estimated_end_time` datetime DEFAULT NULL COMMENT '预计完成时间',
  `actual_end_time` datetime DEFAULT NULL COMMENT '实际完成时间',
  `cancel_time` datetime DEFAULT NULL COMMENT '取消时间',
  `cancel_reason` int DEFAULT NULL COMMENT '取消原因',
  `cancel_reason_description` varchar(100) CHARACTER SET armscii8 COLLATE armscii8_general_ci DEFAULT NULL COMMENT '取消原因具体描述',
  `assigned_status` int NOT NULL COMMENT '任务分配状态(1未分配2已分配3待人工分配)',
  `mark` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注',
  `created` datetime DEFAULT NULL COMMENT '创建时间',
  `updated` datetime DEFAULT NULL COMMENT '更新时间',
  `is_deleted` int DEFAULT '0' COMMENT '删除:0-否,1-是',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `order_id` (`order_id`) USING BTREE,
  KEY `created` (`created`) USING BTREE,
  KEY `task_type` (`task_type`) USING BTREE,
  KEY `courier_id` (`courier_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='取件、派件任务信息表';

3.2、业务流程

3.2.1、取件任务流程

用户在下单后,订单微服务会发消息出来,消息会在dispatch微服务中进行调度,最终会向work微服务发送消息,生成快递员的取件任务的实体,向数据库sl_pickup_dispatch_task中插入数据。

细节:1返回快递员列表为升序排序,选择任务少的快递员进行取派

2取件时间>2小时不着急,系统延迟发消息给快递员

取消任务重新生成取件时:发送实时消息

3.2.2、派件任务流程

都为实时任务

派件任务会在两个场景下生成:

  • 场景一,司机入库时,运单流转到最后一个节点,需要快递员派件
  • 场景二,发件人与收件人的服务网点是同一个网点时,无需转运,直接生成派件任务

场景一:

场景二:

4、取派件任务

针对于取派件任务进行相应的数据管理,下面我们将逐一进行实现。

4.1、新增取派件任务

新增取派件任务不对外开放,所以在Controller中是没有方法定义的,只是在消息处理中进行调用生成任务。

4.1.1、定义方法

/**
     * 新增取派件任务
     *
     * @param taskPickupDispatch 取派件任务信息
     * @return 取派件任务信息
     */
    PickupDispatchTaskEntity saveTaskPickupDispatch(PickupDispatchTaskEntity taskPickupDispatch);

4.1.2、实现方法

@Override
    public PickupDispatchTaskEntity saveTaskPickupDispatch(PickupDispatchTaskEntity taskPickupDispatch) {
        // 设置任务状态为新任务
        taskPickupDispatch.setStatus(PickupDispatchTaskStatus.NEW);
        boolean result = super.save(taskPickupDispatch);
        if (result) {
            //TODO 同步快递员任务到es
            //TODO 生成运单跟踪消息和快递员端取件/派件消息通知
            return taskPickupDispatch;
        }
        throw new SLException(WorkExceptionEnum.PICKUP_DISPATCH_TASK_SAVE_ERROR);
    }

4.2、分页查询取派件任务

LambdaQueryWrapper查询:模糊查询like,等值查询eq,范围查询between

4.2.1、Controller

@PostMapping("page")
    @ApiOperation(value = "分页查询", notes = "获取取派件任务分页数据")
    public PageResponse<PickupDispatchTaskDTO> findByPage(@RequestBody PickupDispatchTaskPageQueryDTO dto) {
        return this.pickupDispatchTaskService.findByPage(dto);
    }

4.2.2、Service

/**
     * 分页查询取派件任务
     *
     * @param dto 查询条件
     * @return 分页结果
     */
    PageResponse<PickupDispatchTaskDTO> findByPage(PickupDispatchTaskPageQueryDTO dto);

4.2.3、ServiceImpl

查询数据库时使用entity实体(格式)

PickupDispatchTaskEntity::getId后不加括号,不是调用的函数

/**
     * 分页查询取派件任务
     *
     * @param dto 查询条件
     * @return 分页结果
     */
    @Override
    public PageResponse<PickupDispatchTaskDTO> findByPage(PickupDispatchTaskPageQueryDTO dto) {
        //1.构造条件
        Page<PickupDispatchTaskEntity> iPage = new Page<>(dto.getPage(), dto.getPageSize());
        LambdaQueryWrapper<PickupDispatchTaskEntity> queryWrapper = Wrappers.<PickupDispatchTaskEntity>lambdaQuery()
                .like(ObjectUtil.isNotEmpty(dto.getId()), PickupDispatchTaskEntity::getId, dto.getId())
                .like(ObjectUtil.isNotEmpty(dto.getOrderId()), PickupDispatchTaskEntity::getOrderId, dto.getOrderId())
                .eq(ObjectUtil.isNotEmpty(dto.getAgencyId()), PickupDispatchTaskEntity::getAgencyId, dto.getAgencyId())
                .eq(ObjectUtil.isNotEmpty(dto.getCourierId()), PickupDispatchTaskEntity::getCourierId, dto.getCourierId())
                .eq(ObjectUtil.isNotEmpty(dto.getTaskType()), PickupDispatchTaskEntity::getTaskType, dto.getTaskType())
                .eq(ObjectUtil.isNotEmpty(dto.getStatus()), PickupDispatchTaskEntity::getStatus, dto.getStatus())
                .eq(ObjectUtil.isNotEmpty(dto.getAssignedStatus()), PickupDispatchTaskEntity::getAssignedStatus, dto.getAssignedStatus())
                .eq(ObjectUtil.isNotEmpty(dto.getSignStatus()), PickupDispatchTaskEntity::getSignStatus, dto.getSignStatus())
                .eq(ObjectUtil.isNotEmpty(dto.getIsDeleted()), PickupDispatchTaskEntity::getIsDeleted, dto.getIsDeleted())
                .between(ObjectUtil.isNotEmpty(dto.getMinEstimatedEndTime()), PickupDispatchTaskEntity::getEstimatedEndTime, dto.getMinEstimatedEndTime(), dto.getMaxEstimatedEndTime())
                .between(ObjectUtil.isNotEmpty(dto.getMinActualEndTime()), PickupDispatchTaskEntity::getActualEndTime, dto.getMinActualEndTime(), dto.getMaxActualEndTime())
                .orderByDesc(PickupDispatchTaskEntity::getUpdated);
        //2.分页查询
        Page<PickupDispatchTaskEntity> result = super.page(iPage, queryWrapper);
        if (ObjectUtil.isEmpty(result.getRecords())) {
            return new PageResponse<>(result);
        }
        //3.组装分页数据
        return new PageResponse(result, PickupDispatchTaskDTO.class);
    }

@Override
    public PageResponse<PickupDispatchTaskDTO> findByPage(PickupDispatchTaskPageQueryDTO dto) {
        Page<PickupDispatchTaskEntity> page = new Page<>(dto.getPage(),dto.getPageSize());
        LambdaQueryWrapper<PickupDispatchTaskEntity> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.like(ObjectUtil.isNotEmpty(dto.getId()), PickupDispatchTaskEntity::getId,dto.getId())
                .like(ObjectUtil.isNotEmpty(dto.getOrderId()), PickupDispatchTaskEntity::getOrderId, dto.getOrderId())
                .eq(ObjectUtil.isNotEmpty(dto.getAgencyId()), PickupDispatchTaskEntity::getAgencyId, dto.getAgencyId())
                .eq(ObjectUtil.isNotEmpty(dto.getCourierId()), PickupDispatchTaskEntity::getCourierId, dto.getCourierId())
                .eq(ObjectUtil.isNotEmpty(dto.getTaskType()), PickupDispatchTaskEntity::getTaskType, dto.getTaskType())
                .eq(ObjectUtil.isNotEmpty(dto.getStatus()), PickupDispatchTaskEntity::getStatus, dto.getStatus())
                .eq(ObjectUtil.isNotEmpty(dto.getAssignedStatus()), PickupDispatchTaskEntity::getAssignedStatus, dto.getAssignedStatus())
                .eq(ObjectUtil.isNotEmpty(dto.getIsDeleted()), PickupDispatchTaskEntity::getIsDeleted, dto.getIsDeleted())
                .between(ObjectUtil.isNotEmpty(dto.getMinEstimatedEndTime()), PickupDispatchTaskEntity::getEstimatedEndTime, dto.getMinEstimatedEndTime(), dto.getMaxEstimatedEndTime())
                .between(ObjectUtil.isNotEmpty(dto.getMinActualEndTime()), PickupDispatchTaskEntity::getActualEndTime, dto.getMinActualEndTime(), dto.getMaxActualEndTime())
                .orderByDesc(PickupDispatchTaskEntity::getUpdated);
        Page<PickupDispatchTaskEntity> result = super.page(page,queryWrapper);
        if(ObjectUtil.isEmpty(result)) return new PageResponse<>();
        return new PageResponse<>(result,PickupDispatchTaskDTO.class);
    }

另一种写法

注此处前面指定了泛型,后续也需有<>符号

LambdaQueryWrapper<PickupDispatchTaskEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like()

区别:Wrappers.lambdaQuery() 是 MyBatis Plus 提供的工具方法,用于创建 LambdaQueryWrapper 对象。不需要手动实例化 LambdaQueryWrapper,少了一行代码的冗余。

4.3、按照时间查询任务数

4.3.1、Controller

@GetMapping("count")
    @ApiOperation(value = "任务数量查询", notes = "按照当日快递员id列表查询每个快递员的取派件任务数")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "courierIds", value = "订单id列表", required = true),
            @ApiImplicitParam(name = "taskType", value = "任务类型", required = true),
            @ApiImplicitParam(name = "date", value = "日期,格式:yyyy-MM-dd 或 yyyyMMdd", required = true)
    })
    public List<CourierTaskCountDTO> findCountByCourierIds(@RequestParam("courierIds") List<Long> courierIds,
                                                           @RequestParam("taskType") PickupDispatchTaskType taskType,
                                                           @RequestParam("date") String date) {
        return this.pickupDispatchTaskService.findCountByCourierIds(courierIds, taskType, date);
    }

4.3.2、Service

/**
     * 按照当日快递员id列表查询每个快递员的取派件任务数
     *
     * @param courierIds             快递员id列表
     * @param pickupDispatchTaskType 任务类型
     * @param date                   日期,格式:yyyy-MM-dd 或 yyyyMMdd
     * @return 任务数
     */
    List<CourierTaskCountDTO> findCountByCourierIds(List<Long> courierIds, PickupDispatchTaskType pickupDispatchTaskType, String date);

4.2.3、ServiceImpl

需将前段字符串类型的date转换为日期类型DateTime数据 

参数DateTime的日期格式为:yyyy-MM-dd 或 yyyyMMdd

需要使用HuTool工具类DateUtil将其转换为某天的开始时间和结束时间进行查询  

代码:

DateUtil.parse(date)

LocalDateTime

DateUtil.beginOfDay(dateTime).toLocalDateTime()

@Override
    public List<CourierTaskCountDTO> findCountByCourierIds(List<Long> courierIds, PickupDispatchTaskType pickupDispatchTaskType, String date) {
        //计算一天的时间的边界
        DateTime dateTime = DateUtil.parse(date);
        LocalDateTime startDateTime = DateUtil.beginOfDay(dateTime).toLocalDateTime();
        LocalDateTime endDateTime = DateUtil.endOfDay(dateTime).toLocalDateTime();
        return this.taskPickupDispatchMapper
                .findCountByCourierIds(courierIds, pickupDispatchTaskType.getCode(), startDateTime, endDateTime);
    }

4.2.4、定义SQL

此方法的实现,调用自定义Mapper中的SQL实现。

<select id="findCountByCourierIds" resultType="com.sl.ms.work.domain.dto.CourierTaskCountDTO">
        SELECT
            COUNT(1) as count,courier_id FROM sl_pickup_dispatch_task t
        WHERE
            t.courier_id IN  <foreach collection="courierIds" item="courierId" open="(" close=")" separator=",">#{courierId}</foreach>
        AND t.created BETWEEN #{startDateTime} AND #{endDateTime}
        AND t.task_type = #{type}
        GROUP BY courier_id
    </select>
COUNT(1)中的1表示常量,与COUNT(0),COUNT(*)结果相同,都为统计行数
<foreach collection="courierIds" item="courierId" open="(" close=")" separator=",">#{courierId}</foreach>

其中item="courierId"与#{courierId}的名称需要相同,意义为集合中的每一项的命名和传参。取什么名字都可以,但二者需相同

4.4、查询当日任务

查询快递员当日的取派件任务列表。

4.4.1、Controller

@GetMapping("todayTasks/{courierId}")
    @ApiOperation(value = "查询当日任务", notes = "查询快递员当日任务")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "courierId", value = "快递员id", required = true)
    })
    public List<PickupDispatchTaskDTO> findTodayTasks(@PathVariable("courierId") Long courierId) {
        return pickupDispatchTaskService.findTodayTaskByCourierId(courierId);
    }

4.4.2、Service

/**
     * 查询快递员当日任务
     *
     * @return 任务列表
     */
    List<PickupDispatchTaskDTO> findTodayTaskByCourierId(Long courierId);

4.4.3、ServiceImpl

使用HuTool工具类中BeanUtil.copyToList转换list对象类型

@Override
    public List<PickupDispatchTaskDTO> findTodayTaskByCourierId(Long courierId) {
        //查询指定快递员当天所有的派件取件任务
        LambdaQueryWrapper<PickupDispatchTaskEntity> queryWrapper = Wrappers.<PickupDispatchTaskEntity>lambdaQuery()
                .eq(PickupDispatchTaskEntity::getCourierId, courierId)
                .ge(PickupDispatchTaskEntity::getEstimatedStartTime, LocalDateTimeUtil.beginOfDay(LocalDateTime.now()))
                .le(PickupDispatchTaskEntity::getEstimatedStartTime, LocalDateTimeUtil.endOfDay(LocalDateTime.now()))
                .eq(PickupDispatchTaskEntity::getIsDeleted, PickupDispatchTaskIsDeleted.NOT_DELETED);
        List<PickupDispatchTaskEntity> list = super.list(queryWrapper);
        return BeanUtil.copyToList(list, PickupDispatchTaskDTO.class);
    }

4.5、根据订单查询任务

根据订单id查询取派件任务。

4.5.1、Controller

@GetMapping("/orderId/{orderId}/{taskType}")
    @ApiOperation(value = "订单id查询", notes = "根据订单id获取取派件任务信息")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "orderId", value = "订单id"),
            @ApiImplicitParam(name = "taskType", value = "任务类型")
    })
    public List<PickupDispatchTaskDTO> findByOrderId(@PathVariable("orderId") Long orderId,
                                                     @PathVariable("taskType") PickupDispatchTaskType taskType) {
        List<PickupDispatchTaskEntity> entities = pickupDispatchTaskService.findByOrderId(orderId, taskType);
        return BeanUtil.copyToList(entities, PickupDispatchTaskDTO.class);
    }

4.5.2、Service

/**
     * 根据订单id查询取派件任务
     *
     * @param orderId  订单id
     * @param taskType 任务类型
     * @return 任务
     */
    List<PickupDispatchTaskEntity> findByOrderId(Long orderId, PickupDispatchTaskType taskType);
  

4.5.3、ServiceImpl

@Override
public List<PickupDispatchTaskEntity> findByOrderId(Long orderId, PickupDispatchTaskType taskType) {
    LambdaQueryWrapper<PickupDispatchTaskEntity> wrapper = Wrappers.<PickupDispatchTaskEntity>lambdaQuery()
            .eq(PickupDispatchTaskEntity::getOrderId, orderId)
            .eq(PickupDispatchTaskEntity::getTaskType, taskType)
            .eq(PickupDispatchTaskEntity::getIsDeleted,PickupDispatchTaskIsDeleted.NOT_DELETED)
            .orderByAsc(PickupDispatchTaskEntity::getCreated);
    return this.list(wrapper);
}

4.6、id批量删除

根据id批量删除取派件任务信息(逻辑删除)

4.6.1、Controller

@DeleteMapping("ids")
    @ApiOperation(value = "id批量删除", notes = "根据id批量删除取派件任务信息(逻辑删除)")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "ids", value = "任务id列表")
    })
    public boolean deleteByIds(@RequestParam("ids") List<Long> ids) {
        return this.pickupDispatchTaskService.deleteByIds(ids);
    }

4.6.2、Service

/**
     * 根据id批量删除取派件任务信息(逻辑删除)
     *
     * @param ids id列表
     * @return 是否成功
     */
    boolean deleteByIds(List<Long> ids);

4.6.3、ServiceImpl

 @Override
    public boolean deleteByIds(List<Long> ids) {

        if(ObjectUtil.isEmpty(ids)) return false;
        List<PickupDispatchTaskEntity> list = ids.stream().map(id -> {
            PickupDispatchTaskEntity pickupDispatchTaskEntity = new PickupDispatchTaskEntity();
            pickupDispatchTaskEntity.setId(id);
            pickupDispatchTaskEntity.setIsDeleted(PickupDispatchTaskIsDeleted.IS_DELETED);
            return pickupDispatchTaskEntity;
        }).collect(Collectors.toList());
        return super.updateBatchById(list);
    }

4.7、改派快递员

场景:本来属于A快递员的取件任务,由于某种原因,A快递员不能执行,此时A快递员可以改派给其他快递员,会用到此接口。

另外,派件不支持直接改派,需要客服在后台操作。

4.7.1、Controller

@PutMapping("courier")
    @ApiOperation(value = "改派快递员", notes = "改派快递员")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "id", value = "任务id", required = true),
            @ApiImplicitParam(name = "originalCourierId", value = "原快递员id", required = true),
            @ApiImplicitParam(name = "targetCourierId", value = "目标快递员id", required = true),
    })
    public Boolean updateCourierId(@RequestParam("id") Long id,
                                   @RequestParam("originalCourierId") Long originalCourierId,
                                   @RequestParam("targetCourierId") Long targetCourierId) {
        return this.pickupDispatchTaskService.updateCourierId(id, originalCourierId, targetCourierId);
    }

4.7.2、Service

/**
     * 改派快递员
     *
     * @param id                任务id
     * @param originalCourierId 原快递员id
     * @param targetCourierId   目标快递员id
     * @return 是否成功
     */
    Boolean updateCourierId(Long id, Long originalCourierId, Long targetCourierId);

4.7.3、ServiceImpl

1 三个id都不能为空。

用ObjectUtil.hasEmpty(id,originalCourierId,targetCourierId)判断

2 原快递员id不能和目标快递员id相同

3 根据id查询任务,任务不能为空

4 根据查询的任务,判断任务数据中的原快递员id与传入参数的是否一致

5更改快递员id,标识已分配状态。更新

@Override
    public Boolean updateCourierId(Long id, Long originalCourierId, Long targetCourierId) {
        if (ObjectUtil.hasEmpty(id, targetCourierId, originalCourierId)) {
            throw new SLException(WorkExceptionEnum.UPDATE_COURIER_PARAM_ERROR);
        }
        if (ObjectUtil.equal(originalCourierId, targetCourierId)) {
            throw new SLException(WorkExceptionEnum.UPDATE_COURIER_EQUAL_PARAM_ERROR);
        }
        PickupDispatchTaskEntity pickupDispatchTask = super.getById(id);
        if (ObjectUtil.isEmpty(pickupDispatchTask)) {
            throw new SLException(WorkExceptionEnum.PICKUP_DISPATCH_TASK_NOT_FOUND);
        }
        //校验原快递id是否正确(本来无快递员id的情况除外)
        if (ObjectUtil.isNotEmpty(pickupDispatchTask.getCourierId())
                && ObjectUtil.notEqual(pickupDispatchTask.getCourierId(), originalCourierId)) {
            throw new SLException(WorkExceptionEnum.UPDATE_COURIER_ID_PARAM_ERROR);
        }
        //更改快递员id
        pickupDispatchTask.setCourierId(targetCourierId);
        // 标识已分配状态
        pickupDispatchTask.setAssignedStatus(PickupDispatchTaskAssignedStatus.DISTRIBUTED);
        //TODO 发送消息,同步更新快递员任务(ES)
        return super.updateById(pickupDispatchTask);
    }

4.8、更新取派件状态

实现更新取派件任务状态功能时,需要考虑如下几点:

  • 更新的状态不能为【新任务】状态
  • 更新状态为【已完成】并且任务类型为派件任务时,必须设置签收状态和签收人
  • 更新状态为【已取消】,是取件任务的操作,根据不同的原因有不同的处理逻辑
    • 【因快递员个人无法取件,退回到网点】,需要发送消息重新生成取件任务
    • 【用户取消投递】,需要取消订单
    • 其他原因(用户恶意下单、禁用品、重复下单等),需要关闭订单

4.8.1、Controller

@PutMapping
    @ApiOperation(value = "更新取派件任务状态", notes = "更新状态,不允许 NEW 状态")
    public Boolean updateStatus(@RequestBody PickupDispatchTaskDTO pickupDispatchTaskDTO) {
        return this.pickupDispatchTaskService.updateStatus(pickupDispatchTaskDTO);
    }

4.8.2、Service

/**
     * 更新取派件状态,不允许 NEW 状态
     *
     * @param pickupDispatchTaskDTO 修改的数据
     * @return 是否成功
     */
    Boolean updateStatus(PickupDispatchTaskDTO pickupDispatchTaskDTO);

4.8.3、ServiceImpl

逻辑:

1任务id和status都不能为空

2根据id获取取派件任务

3switch判断想修改的状态

        3.1 修改为新建状态,报错

        3.2 修改为完成状态

                3.2.1设置已完成状态和完成时间

                3.2.2若是派件任务,判断dto参数签收状态非空,设置签收状态

                        3.2.2.1若设置签收状态,判断dto参数签收人不能为空,设置签收人

        3.3修改为取消状态,取消原因不能为空

                3.3.1设置参数Status,CancelReason,CancelReasonDescription,CancelTime

                3.3.2取消原因为快递员取消(返回网点),重新给调度中心发送消息

                3.3.3取消原因为用户取消,使用orderFeign接口修改订单状态

                3.3.4其他原因,设置订单为关闭状态

        3.4其他状态,抛异常

4 updatebyid()更新任务状态

 @Override
    public Boolean updateStatus(PickupDispatchTaskDTO pickupDispatchTaskDTO) {
        WorkExceptionEnum paramError = WorkExceptionEnum.PICKUP_DISPATCH_TASK_PARAM_ERROR;
        if(ObjectUtil.hasEmpty(pickupDispatchTaskDTO.getId(),pickupDispatchTaskDTO.getStatus()))
        {
            throw new SLException("更新取派件任务状态,id或status不能为空" ,paramError.getCode());
        }
        PickupDispatchTaskEntity pickupDispatchTaskEntity=super.getById(pickupDispatchTaskDTO.getId());
        if(ObjectUtil.isEmpty(pickupDispatchTaskEntity))
        {
            throw new SLException("未查询到相关的取派件任务",paramError.getCode());
        }
        switch(pickupDispatchTaskEntity.getStatus())
        {
            case NEW:{
                throw new SLException(WorkExceptionEnum.PICKUP_DISPATCH_TASK_STATUS_NOT_NEW);
            }
            case COMPLETED: {
                //任务完成
                pickupDispatchTaskEntity.setStatus(PickupDispatchTaskStatus.COMPLETED);
                //设置完成时间
                pickupDispatchTaskEntity.setActualEndTime(LocalDateTime.now());
                if (PickupDispatchTaskType.DISPATCH == pickupDispatchTaskEntity.getTaskType()) {
                    //如果是派件任务的完成,已签收需要设置签收状态和签收人,拒收只需要设置签收状态
                    if (ObjectUtil.isEmpty(pickupDispatchTaskDTO.getSignStatus())) {
                        throw new SLException("完成派件任务,签收状态不能为空", paramError.getCode());
                    }
                    pickupDispatchTaskEntity.setSignStatus(pickupDispatchTaskDTO.getSignStatus());
                    if (PickupDispatchTaskSignStatus.RECEIVED == pickupDispatchTaskDTO.getSignStatus()) {
                        if (ObjectUtil.isEmpty(pickupDispatchTaskDTO.getSignRecipient())) {
                            throw new SLException("完成派件任务,签收人不能为空", paramError.getCode());
                        }
                        pickupDispatchTaskEntity.setSignRecipient(pickupDispatchTaskDTO.getSignRecipient());
                    }
                }
                break;
            }
            case CANCELLED: {
                //任务取消
                if (ObjectUtil.isEmpty(pickupDispatchTaskDTO.getCancelReason())) {
                    throw new SLException("取消任务,原因不能为空", paramError.getCode());
                }
                pickupDispatchTaskEntity.setStatus(PickupDispatchTaskStatus.CANCELLED);
                pickupDispatchTaskEntity.setCancelReason(pickupDispatchTaskDTO.getCancelReason());
                pickupDispatchTaskEntity.setCancelReasonDescription(pickupDispatchTaskDTO.getCancelReasonDescription());
                pickupDispatchTaskEntity.setCancelTime(LocalDateTime.now());
                if (pickupDispatchTaskDTO.getCancelReason() == PickupDispatchTaskCancelReason.RETURN_TO_AGENCY) {
                    //发送分配快递员派件任务的消息
                    OrderMsg orderMsg = OrderMsg.builder()
                            .agencyId(pickupDispatchTaskEntity.getAgencyId())
                            .orderId(pickupDispatchTaskEntity.getOrderId())
                            .created(DateUtil.current())
                            .taskType(PickupDispatchTaskType.PICKUP.getCode()) //取件任务
                            .mark(pickupDispatchTaskEntity.getMark())
                            .estimatedEndTime(pickupDispatchTaskEntity.getEstimatedEndTime()).build();
                    //发送消息(取消任务发生在取件之前,没有运单,参数直接填入null)
                    this.transportOrderService.sendPickupDispatchTaskMsgToDispatch(null, orderMsg);
                } else if (pickupDispatchTaskDTO.getCancelReason() == PickupDispatchTaskCancelReason.CANCEL_BY_USER) {
                    //原因是用户取消,则订单状态改为取消
                    orderFeign.updateStatus(ListUtil.of(pickupDispatchTaskEntity.getOrderId()), OrderStatus.CANCELLED.getCode());
                } else {
                    //其他原因则关闭订单
                    orderFeign.updateStatus(ListUtil.of(pickupDispatchTaskEntity.getOrderId()), OrderStatus.CLOSE.getCode());
                }
                break;
            }
            default: {
                throw new SLException("其他未知状态,不能完成更新操作", paramError.getCode());
            }
        }
        //TODO 发送消息,同步更新快递员任务
        return super.updateById(pickupDispatchTaskEntity);
    }

注:

orderFeign.updateStatus()接口为更新订单状态列表
需ListUtil.of(pickupDispatchTaskEntity.getOrderId())将单个id转换为列表

4.9、今日任务分类计数

场景:用于统计今日的任务数量。

4.9.1、Controller

@GetMapping("todayTasks/count")
    @ApiOperation(value = "今日任务分类计数")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "courierId", value = "快递员id", required = true, dataTypeClass = Long.class),
            @ApiImplicitParam(name = "taskType", value = "任务类型,1为取件任务,2为派件任务", dataTypeClass = PickupDispatchTaskType.class),
            @ApiImplicitParam(name = "status", value = "任务状态,1新任务,2已完成,3已取消", dataTypeClass = PickupDispatchTaskStatus.class),
            @ApiImplicitParam(name = "isDeleted", value = "是否逻辑删除", dataTypeClass = PickupDispatchTaskIsDeleted.class)
    })
    public Integer todayTasksCount(@RequestParam(value = "courierId") Long courierId,
                                   @RequestParam(value = "taskType", required = false) PickupDispatchTaskType taskType,
                                   @RequestParam(value = "status", required = false) PickupDispatchTaskStatus status,
                                   @RequestParam(value = "isDeleted", required = false) PickupDispatchTaskIsDeleted isDeleted) {
        return pickupDispatchTaskService.todayTasksCount(courierId, taskType, status, isDeleted);
    }

4.9.2、Service

/**
     * 今日任务分类计数
     *
     * @param courierId 快递员id
     * @param taskType  任务类型,1为取件任务,2为派件任务
     * @param status    任务状态,1新任务,2已完成,3已取消
     * @param isDeleted 是否逻辑删除
     * @return 任务数量
     */
    Integer todayTasksCount(Long courierId, PickupDispatchTaskType taskType, PickupDispatchTaskStatus status, PickupDispatchTaskIsDeleted isDeleted);

4.9.3、ServiceImpl

/**
     * 今日任务分类计数
     *
     * @param courierId 快递员id
     * @param taskType  任务类型,1为取件任务,2为派件任务
     * @param status    任务状态,1新任务,2已完成,3已取消
     * @param isDeleted 是否逻辑删除
     * @return 任务数量
     */
    @Override
    public Integer todayTasksCount(Long courierId, PickupDispatchTaskType taskType, PickupDispatchTaskStatus status, PickupDispatchTaskIsDeleted isDeleted) {
        //构建查询条件
        LambdaQueryWrapper<PickupDispatchTaskEntity> queryWrapper = Wrappers.<PickupDispatchTaskEntity>lambdaQuery()
                .eq(ObjectUtil.isNotEmpty(courierId), PickupDispatchTaskEntity::getCourierId, courierId)
                .eq(ObjectUtil.isNotEmpty(taskType), PickupDispatchTaskEntity::getTaskType, taskType)
                .eq(ObjectUtil.isNotEmpty(status), PickupDispatchTaskEntity::getStatus, status)
                .eq(ObjectUtil.isNotEmpty(isDeleted), PickupDispatchTaskEntity::getIsDeleted, isDeleted);
        //根据任务状态限定查询的日期条件
        LocalDateTime startTime = LocalDateTimeUtil.of(DateUtil.beginOfDay(new Date()));
        LocalDateTime endTime = LocalDateTimeUtil.of(DateUtil.endOfDay(new Date()));
        if (status == null) {
            //没有任务状态,查询任务创建时间
            queryWrapper.between(PickupDispatchTaskEntity::getCreated, startTime, endTime);
        } else if (status == PickupDispatchTaskStatus.NEW) {
            //新任务状态,查询预计结束时间
            queryWrapper.between(PickupDispatchTaskEntity::getEstimatedEndTime, startTime, endTime);
        } else if (status == PickupDispatchTaskStatus.COMPLETED) {
            //完成状态,查询实际完成时间
            queryWrapper.between(PickupDispatchTaskEntity::getActualEndTime, startTime, endTime);
        } else if (status == PickupDispatchTaskStatus.CANCELLED) {
            //取消状态,查询取消时间
            queryWrapper.between(PickupDispatchTaskEntity::getCancelTime, startTime, endTime);
        }
        //结果返回integer类型值
        return Convert.toInt(super.count(queryWrapper));
    }

5、调度中心

在调度中心中对于生成取派件任务的消息进行处理,消息内容类似这样:

{
    "orderId": 123,
    "agencyId": 8001,
    "taskType": 1,
    "mark": "带包装",
    "longitude": 116.111,
    "latitude": 39.00,
    "created": 1654224658728,
    "estimatedStartTime": 1654224658728
}

实现的关键点:

  • 如果只查询到一个快递员,直接分配即可
  • 如果是多个快递员,需要查询这些快递员当日的任务数,按照最少的进行分配,这样可以做到相对均衡
  • 如果没有快递员,设置快递员id为空,可以在后台系统中,人为的进行调配快递员
  • 对于取件任务而言,需要考虑用户选择的【期望上门时间】
    • 与当前时间相比,大于2小时发送延时消息,否则发送实时消息

5.1、编码实现

逻辑:

0使用JSONUtil.toBean将string字符串转换为OrderMsg.class

1获取机构id和经纬度,预估到达时间

LocalDateTimeUtil.toEpochMilli转换为毫秒

2传入快递员微服务查询快递员id列表

快递员微服务(了解):

//1.根据经纬度查询服务范围内的快递员
//2.如果服务范围内有快递员,则在其中筛选结束取件时间当天有排班的快递员
        //2.1对满足服务范围、网点的快递员筛选排班
//3.存在同时满足服务范围、网点、排班的快递员,直接返回
        //3.1 如果服务范围内没有快递员,或服务范围内的快递员没有排班,则查询该网点的任一有排班快递员
        //3.2对满足网点的快递员筛选排班
@Override
    public List<Long> queryCourierIdListByCondition(Long agencyId, Double longitude, Double latitude, Long estimatedEndTime) {
        log.info("当前机构id为:{}", agencyId);

        //1.根据经纬度查询服务范围内的快递员
        List<ServiceScopeDTO> serviceScopeDTOS = serviceScopeFeign.queryListByLocation(2, longitude, latitude);

        List<WorkSchedulingDTO> workSchedulingDTOS = new ArrayList<>();
        List<Long> courierIds = null;
        //2.如果服务范围内有快递员,则在其中筛选结束取件时间当天有排班的快递员
        if (CollUtil.isNotEmpty(serviceScopeDTOS)) {
            List<Long> bids = serviceScopeDTOS.stream().map(ServiceScopeDTO::getBid).collect(Collectors.toList());
            log.info("根据经纬度查询到的快递员id有:{}", bids);

            String bidStr = CollUtil.isEmpty(bids) ? "" : CharSequenceUtil.join(",", bids);
            workSchedulingDTOS = workSchedulingFeign.monthSchedule(bidStr, agencyId, WorkUserTypeEnum.COURIER.getCode(), estimatedEndTime);
            log.info("满足服务范围、网点的快递员排班:{}", workSchedulingDTOS);
        }

        //2.1对满足服务范围、网点的快递员筛选排班
        if (CollUtil.isNotEmpty(workSchedulingDTOS)) {
            courierIds = workSchedulingDTOS.stream()
                    // 结束取件时间当天是否有排班
                    .filter(workSchedulingDTO -> workSchedulingDTO.getWorkSchedules().get(0))
                    .map(WorkSchedulingDTO::getUserId)
                    .collect(Collectors.toList());
            log.info("服务范围、网点、排班均满足的快递员id有:{}", courierIds);
        }

        //3.存在同时满足服务范围、网点、排班的快递员,直接返回
        if (CollUtil.isNotEmpty(courierIds)) {
            return courierIds;
        }

        //3.1 如果服务范围内没有快递员,或服务范围内的快递员没有排班,则查询该网点的任一有排班快递员
        workSchedulingDTOS = workSchedulingFeign.monthSchedule(null, agencyId, WorkUserTypeEnum.COURIER.getCode(), estimatedEndTime);
        log.info("查询该网点所有快递员排班:{}", workSchedulingDTOS);
        if (CollUtil.isEmpty(workSchedulingDTOS)) {
            return courierIds;
        }

        //3.2对满足网点的快递员筛选排班
        courierIds = workSchedulingDTOS.stream()
                // 结束取件时间当天是否有排班
                .filter(workSchedulingDTO -> workSchedulingDTO.getWorkSchedules().get(0))
                .map(WorkSchedulingDTO::getUserId)
                .collect(Collectors.toList());
        log.info("只满足网点、排班的快递员id有:{}", courierIds);

        return courierIds;
    }

3快递员id列表非空,选择快递员

        3.1列表只有一个,选中

        3.2根据id查询快递员任务,列表查询

DateUtil.date().toDateStr() 获取具体的日期并转换为字符串

        3.3没有查到任务数量,默认给第一个快递员分配任务

        3.4courierTaskCountDTOS查看任务数是否与快递员数相同,如果不相同需要补齐,设置任务数为0

        3.5升序排序courierTaskCountDTOS,选中任务最少得快递员分派任务

/**
     * 根据当日的任务数选取快递员
     *
     * @param courierIds 快递员列个表
     * @param taskType   任务类型
     * @return 选中的快递员id
     */
    private Long selectCourier(List<Long> courierIds, Integer taskType){
        if(courierIds.size()==1) return courierIds.get(0);
        List<CourierTaskCountDTO> courierTaskCountDTOS = pickupDispatchTaskFeign.findCountByCourierIds(courierIds, PickupDispatchTaskType.codeOf(taskType), DateUtil.date().toDateStr());
        if (CollUtil.isEmpty(courierTaskCountDTOS)) {
            //没有查到任务数量,默认给第一个快递员分配任务
            return courierIds.get(0);
        }
        //如果查出的快递员id与courierTaskCountDTOS的数量不等,则将多的快递员id的任务补0加入List<CourierTaskCountDTO>
        if (ObjectUtil.notEqual(courierIds.size(), courierTaskCountDTOS.size())){
            List<CourierTaskCountDTO> list = courierIds.stream()
                    .filter(courierId->{
                        //判断courierId在courierTaskCountDTOS集合中是否存在
                        int index = CollUtil.indexOf(courierTaskCountDTOS,dto->ObjectUtil.equal(dto.getCourierId(),courierId));
                        return index == -1;//如果为空,就返回-1,否则返回查询值
                    })
                    .map(courierId-> CourierTaskCountDTO.builder()
                            .courierId(courierId)
                            .count(0L)
                            .build())
                    .collect(Collectors.toList());
            courierTaskCountDTOS.addAll(list);
        }
        //按属性值进行升序排序
        CollUtil.sortByProperty(courierTaskCountDTOS,"count");
        return courierTaskCountDTOS.get(0).getCourierId();
    }

4发送mq消息

        4.0构建消息对象

        4.1计算时间差,分钟

        4.2设置常数默认延时发送(-1)

        4.3如果时间差大于120min,并且为取件任务计算延时时间,毫秒

                4.3.1计算发送消息时间,预估到达时间-2h

                4.3.2发送消息的时间减去现在的时间,为延迟时间,毫秒

        4.4发送,mq消息

package com.sl.ms.dispatch.mq;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.sl.ms.api.CourierFeign;
import com.sl.ms.base.api.common.MQFeign;
import com.sl.ms.work.api.PickupDispatchTaskFeign;
import com.sl.ms.work.domain.dto.CourierTaskCountDTO;
import com.sl.ms.work.domain.enums.pickupDispatchtask.PickupDispatchTaskType;
import com.sl.transport.common.constant.Constants;
import com.sl.transport.common.vo.CourierTaskMsg;
import com.sl.transport.common.vo.OrderMsg;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 订单业务消息,接收到新订单后,根据快递员的负载情况,分配快递员
 */
@Slf4j
@Component
public class OrderMQListener {
    @Resource
    private CourierFeign courierFeign;
    @Resource
    private PickupDispatchTaskFeign pickupDispatchTaskFeign;
    @Resource
    private MQFeign mqFeign;

    /**
     * 如果有多个快递员,需要查询快递员今日的取派件数,根据此数量进行计算
     * 计算的逻辑:优先分配取件任务少的,取件数相同的取第一个分配
     * <p>
     * 发送生成取件任务时需要计算时间差,如果小于2小时,实时发送;大于2小时,延时发送
     * 举例:
     * 1、现在10:30分,用户期望:11:00 ~ 12:00上门,实时发送
     * 2、现在10:30分,用户期望:13:00 ~ 14:00上门,延时发送,12点发送消息,延时1.5小时发送
     *
     * @param msg 消息内容
     * @return
     */
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = Constants.MQ.Queues.DISPATCH_ORDER_TO_PICKUP_DISPATCH_TASK),
            exchange = @Exchange(name = Constants.MQ.Exchanges.ORDER_DELAYED, type = ExchangeTypes.TOPIC, delayed = Constants.MQ.DELAYED),
            key = Constants.MQ.RoutingKeys.ORDER_CREATE
    ))
    public void listenOrderMsg(String msg) {
        //{"orderId":123, "agencyId": 8001, "taskType":1, "mark":"带包装", "longitude":116.111, "latitude":39.00, "created":1654224658728, "estimatedStartTime": 1654224658728}
        log.info("接收到订单的消息 >>> msg = {}", msg);
        OrderMsg orderMsg = JSONUtil.toBean(msg, OrderMsg.class);
        Long agencyId = orderMsg.getAgencyId();
        Double longitude = orderMsg.getLongitude();
        Double latitude = orderMsg.getLatitude();
        List<Long> courierIds = this.courierFeign.queryCourierIdListByCondition(agencyId, longitude, latitude, LocalDateTimeUtil.toEpochMilli(orderMsg.getEstimatedEndTime()));
        log.info("快递员微服务查出的ids:{}", courierIds);
        Long selectedCourierId = null;
        if (CollUtil.isNotEmpty(courierIds)) {
            //选中快递员
            selectedCourierId = this.selectCourier(courierIds, orderMsg.getTaskType());
            log.info("根据当日任务选出的快递员id:{}", selectedCourierId);
        }
        //发送消息
        //如果没有快递员,设置快递员id为空,可以在后台系统中,人为的进行调配快递员
        CourierTaskMsg courierTaskMsg = CourierTaskMsg.builder()
                .courierId(selectedCourierId)
                .agencyId(agencyId)
                .taskType(orderMsg.getTaskType())
                .orderId(orderMsg.getOrderId())
                .mark(orderMsg.getMark())
                .estimatedEndTime(orderMsg.getEstimatedEndTime())
                .created(System.currentTimeMillis())
                .build();
        //计算时间差
        long between = LocalDateTimeUtil.between(LocalDateTime.now(), orderMsg.getEstimatedEndTime(), ChronoUnit.MINUTES);
        int delay = Constants.MQ.DEFAULT_DELAY; //默认实时发送
        if (between > 120 && ObjectUtil.equal(orderMsg.getTaskType(), PickupDispatchTaskType.PICKUP)) {
            //计算延时时间,单位毫秒
            LocalDateTime sendDataTime = LocalDateTimeUtil.offset(orderMsg.getEstimatedEndTime(), -2, ChronoUnit.HOURS);
            delay = Convert.toInt(LocalDateTimeUtil.between(LocalDateTime.now(), sendDataTime, ChronoUnit.MILLIS));
        }
        this.mqFeign.sendMsg(Constants.MQ.Exchanges.PICKUP_DISPATCH_TASK_DELAYED,
                Constants.MQ.RoutingKeys.PICKUP_DISPATCH_TASK_CREATE, courierTaskMsg.toJson(), delay);
    }
    /**
     * 根据当日的任务数选取快递员
     *
     * @param courierIds 快递员列个表
     * @param taskType   任务类型
     * @return 选中的快递员id
     */
    private Long selectCourier(List<Long> courierIds, Integer taskType){
        if(courierIds.size()==1) return courierIds.get(0);
        List<CourierTaskCountDTO> courierTaskCountDTOS = pickupDispatchTaskFeign.findCountByCourierIds(courierIds, PickupDispatchTaskType.codeOf(taskType), DateUtil.date().toDateStr());
        if (CollUtil.isEmpty(courierTaskCountDTOS)) {
            //没有查到任务数量,默认给第一个快递员分配任务
            return courierIds.get(0);
        }
        //如果查出的快递员id与courierTaskCountDTOS的数量不等,则将多的快递员id的任务补0加入List<CourierTaskCountDTO>
        if (ObjectUtil.notEqual(courierIds.size(), courierTaskCountDTOS.size())){
            List<CourierTaskCountDTO> list = courierIds.stream()
                    .filter(courierId->{
                        //判断courierId在courierTaskCountDTOS集合中是否存在
                        int index = CollUtil.indexOf(courierTaskCountDTOS,dto->ObjectUtil.equal(dto.getCourierId(),courierId));
                        return index == -1;//如果为空,就返回-1,否则返回查询值
                    })
                    .map(courierId-> CourierTaskCountDTO.builder()
                            .courierId(courierId)
                            .count(0L)
                            .build())
                    .collect(Collectors.toList());
            courierTaskCountDTOS.addAll(list);
        }
        //按属性值进行升序排序
        CollUtil.sortByProperty(courierTaskCountDTOS,"count");
        return courierTaskCountDTOS.get(0).getCourierId();
    }
}

6、生成任务

在work微服务中可以接收到来自调度中心的消息,接下来,我们需要编写消费消息的逻辑,生成快递员的取派件任务。

6.1、消费消息

逻辑:

1将传来的string字符串msg消息转换为CourierTaskMsg

2幂等性处理

        2.1根据订单id查询取派件任务

        2.2根据状态判断任务是否已经存在PickupDispatchTaskStatus.NEW,存在则return

        2.3根据订单id查询订单,不存在则return(订单被中途删除等等原因)

        2.4判断列表(快递员可根据id设置多个任务,取消后又新建等等)中订单状态OrderStatus为已经取消或者删除,return

3设置对象属性值(任务类型,预计开始时间(LocalDateTimeUtil.offset结束时间向前推一小时),默认未签收状态,分配状态)

        根据调度中心选中快递员id判断,非空设为调度状态,空设为人工调度状态

4.pickupDispatchTaskService.saveTaskPickupDispatch()保存任务(保存失败抛异常不用写,save方法中有)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值