课程安排
- 了解快递员取派件任务需求
- 递员取派件任务相关功能开发
- 调度中心任务调度
- 整体业务功能测试
本章基础:对派件任务的增删改查
本章重点:
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方法中有)