day12 实战需求

需求

取派件任务搜索

快递员端的操作,包含【任务搜索】和【最近搜索】

【最近搜索】已经在sl-express-ms-web-courier中实现,所以在实战中,只需要实现【任务搜索】即可。

任务搜索的需求如下:(仔细阅读需求)
image.png
功能界面:
image.png

任务介绍

1 在git中获取基础微服务代码

2 将数据库中的取派件任务表sl_work.sl_pickup_dispatch_task中的数据存储到es中,后续任务搜索查询es即可

test代码:查询所有取派件任务及相关订单的实体,封装为entity实体存储到es中

3 mq代码

配置es

1 进行Spring Boot配置文件部署时,发出警告Spring Boot Configuration Annotation Processor not configured,但是不影响运行。

解决:pom中引入以下依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

2 在nacos中找到以下配置,在local.yml文件中进行对应配置

未完成,es配置后打不开(可能配置不成功)

mq中代码

listenCourierTaskCreateMsg

逻辑:将msg消息转换并获得对应消息,在经过处理调用对应的service实现方法

此处监听的为CourierTaskCreateMsg,故使用CourierTaskMsg进行接收信息

根据CourierTaskMsg内容,及查询出的订单id的内容对CourierTaskDTO属性进行设置

将CourierTaskDTO传入,courierTaskService.saveOrUpdate()中实现数据保存到es数据库中

改:直接将msg消息转换为CourierTaskDTO即可

将CourierTaskDTO传入,courierTaskService.saveOrUpdate()中实现数据保存到es数据库中

public void listenCourierTaskCreateMsg(String msg) {
        log.info("接收到新增/更新快递员任务的消息 ({})-> {}", Constants.MQ.Queues.COURIER_TASK_SAVE_OR_UPDATE, msg);
        CourierTaskDTO courierTaskDTO = JSONUtil.toBean(msg, CourierTaskDTO.class);
        courierTaskService.saveOrUpdate(courierTaskDTO);
    }
listenTransportOrderCreatedMsg

逻辑:

1 将msg转换为TransportOrderMsg

2 通过订单id,派件任务(运单为派件,退货为取件)调用pickupDispatchTaskFeign.findByOrderId()查询到List<PickupDispatchTaskDTO>(存储到es中时是从数据库sl_pickup_dispatch_task表中获取)

3 将List<PickupDispatchTaskDTO>转换为List<CourierTaskDTO>

4 将每个CourierTaskDTO传入,courierTaskService.saveOrUpdate()中实现数据保存到es数据库

public void listenTransportOrderCreatedMsg(String msg) {
        log.info("接收到新增运单的消息 ({})-> {}", Constants.MQ.Queues.TRACK_TRANSPORT_ORDER_CREATED, msg);
        // TODO 具体业务逻辑的处理
        TransportOrderMsg transportOrderMsg = JSONUtil.toBean(msg, TransportOrderMsg.class);
        List<PickupDispatchTaskDTO> list = pickupDispatchTaskFeign.findByOrderId(transportOrderMsg.getOrderId(), PickupDispatchTaskType.DISPATCH);
        List<CourierTaskDTO> courierTaskDTOS = list.stream().map(dto -> {
            CourierTaskDTO courierTaskDTO = new CourierTaskDTO();
            BeanUtils.copyProperties(dto, courierTaskDTO);
            return courierTaskDTO;
        }).collect(Collectors.toList());
        for(CourierTaskDTO dto:courierTaskDTOS)
        {
            courierTaskService.saveOrUpdate(dto);
        }
    }

service代码

(此处es未配置好,直接查询mysql)

pageQuery()
逻辑
1//构建分页查询条件(快递员id,keywords不确定是什么,使用多个模糊查询)

LambdaQueryWrapper构建时,第一个参数可以为判非空

2//使用mybatis-plus进行page查询

搭建mybatis环境配置

1 函数头

public class CourierTaskServiceImpl extends
        ServiceImpl<CourierTaskMapper, CourierTaskEntity> implements CourierTaskService {
 

2 mapper

public interface CourierTaskMapper extends BaseMapper<CourierTaskEntity> {
 

3 pom文件引入依赖

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>

4entity实体类中加入注释,使知道查询数据库哪张表

@TableName("sl_courier_task")

5 数据库创建该表(在表sl_pickup_dispatch_task(任意表)中使用建表语句即可)

CREATE TABLE `sl_courier_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//将结果以dto对象返回

以下为两种返回方法参考

3.1 使用builder进行构建

灵活

public PageResponse<TransportOrderDTO> pageQueryByTaskId(Integer page, Integer pageSize, String taskId, String transportOrderId) {
        //构建分页查询条件
        Page<TransportOrderTaskEntity> transportOrderTaskPage = new Page<>(page, pageSize);
        LambdaQueryWrapper<TransportOrderTaskEntity> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(ObjectUtil.isNotEmpty(taskId), TransportOrderTaskEntity::getTransportTaskId, taskId)
                .like(ObjectUtil.isNotEmpty(transportOrderId), TransportOrderTaskEntity::getTransportOrderId, transportOrderId)
                .orderByDesc(TransportOrderTaskEntity::getCreated);
        //根据运输任务id、运单id查询运输任务与运单关联关系表
        Page<TransportOrderTaskEntity> pageResult = this.transportOrderTaskService.page(transportOrderTaskPage, queryWrapper);
        if (ObjectUtil.isEmpty(pageResult.getRecords())) {
            return new PageResponse<>(pageResult);
        }
        //根据运单id查询运单,并转化为dto
        List<TransportOrderDTO> transportOrderDTOList = pageResult.getRecords().stream().map(x -> {
            TransportOrderEntity transportOrderEntity = this.getById(x.getTransportOrderId());
            return BeanUtil.toBean(transportOrderEntity, TransportOrderDTO.class);
        }).collect(Collectors.toList());
        //构建分页结果
        return PageResponse.<TransportOrderDTO>builder()
                .page(page)
                .pageSize(pageSize)
                .pages(pageResult.getPages())
                .counts(pageResult.getTotal())
                .items(transportOrderDTOList)
                .build();
    }

3.2 直接构造

灵活性低。

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);
代码
 @Override
    public PageResponse<CourierTaskDTO> pageQuery(CourierTaskPageQueryDTO pageQueryDTO) {
        //构造分页查询,查询数据库的参数
        Page<CourierTaskEntity> courierTaskEntityPage=new Page<>(pageQueryDTO.getPage(),pageQueryDTO.getPgeSize());
        LambdaQueryWrapper<CourierTaskEntity> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(ObjectUtil.isNotEmpty(pageQueryDTO.getCourierId()),CourierTaskEntity::getCourierId,pageQueryDTO.getCourierId())
                .like(CourierTaskEntity::getTransportOrderId,pageQueryDTO.getKeyword())
                .like(CourierTaskEntity::getPhone,pageQueryDTO.getKeyword())
                .like(CourierTaskEntity::getAddress,pageQueryDTO.getKeyword())
                .orderByDesc(CourierTaskEntity::getCreated);
        Page<CourierTaskEntity> page = super.page(courierTaskEntityPage, queryWrapper);
        //将结果以dto对象返回
        if(ObjectUtil.isEmpty(page)) return new PageResponse<>();
        return new PageResponse<>(page,CourierTaskDTO.class);
    }
saveOrUpdate()
PickupDispatchTaskServiceImpl

需要在PickupDispatchTaskServiceImpl中的updateStatus(),updateCourierId(),saveTaskPickupDispatch()三个方法中添加发送消息到listenCourierTaskCreateMsg()中,从而新增/更新快递员任务

BeanUtil.toBean 的作用:将属性值相同的复制给目标对象

在发送消息的过程中,你并不需要显式地声明队列(queue),因为发送消息的目的是将消息推送到某个交换器(Exchange),由 RabbitMQ 的路由机制决定该消息最终会进入哪些队列。只有当消息的路由键和队列绑定的路由键完全一致时,消息才会被路由到该队列(即路由键和队列是相同的)。消息的路由是基于你发送时指定的交换器 (Exchange) 和路由键 (routingKey)。

发送消息例子

 public void sendPickupDispatchTaskMsgToDispatch(TransportOrderEntity transportOrder, OrderMsg orderMsg) {
        //查询订单对应的位置信息
        OrderLocationDTO orderLocationDTO = this.orderFeign.findOrderLocationByOrderId(orderMsg.getOrderId());
        //(1)运单为空:取件任务取消,取消原因为返回网点;重新调度位置取寄件人位置
        //(2)运单不为空:生成的是派件任务,需要根据拒收状态判断位置是寄件人还是收件人
        // 拒收:寄件人  其他:收件人
        String location;
        if (ObjectUtil.isEmpty(transportOrder)) {
            location = orderLocationDTO.getSendLocation();
        } else {
            location = transportOrder.getIsRejection() ? orderLocationDTO.getSendLocation() : orderLocationDTO.getReceiveLocation();
        }
        Double[] coordinate = Convert.convert(Double[].class, StrUtil.split(location, ","));
        Double longitude = coordinate[0];
        Double latitude = coordinate[1];
        //设置消息中的位置信息
        orderMsg.setLongitude(longitude);
        orderMsg.setLatitude(latitude);
        //发送消息,用于生成取派件任务
        this.mqFeign.sendMsg(Constants.MQ.Exchanges.ORDER_DELAYED, Constants.MQ.RoutingKeys.ORDER_CREATE,
                orderMsg.toJson(), Constants.MQ.NORMAL_DELAY);
    }
    private void sendUpdateStatusMsg(List<String> ids, TransportOrderStatus transportOrderStatus) {
        String msg = TransportOrderStatusMsg.builder()
                .idList(ids)
                .statusName(transportOrderStatus.name())
                .statusCode(transportOrderStatus.getCode())
                .build().toJson();
        //将状态名称写入到路由key中,方便消费方选择性的接收消息
        String routingKey = Constants.MQ.RoutingKeys.TRANSPORT_ORDER_UPDATE_STATUS_PREFIX + transportOrderStatus.name();
        this.mqFeign.sendMsg(Constants.MQ.Exchanges.TRANSPORT_ORDER_DELAYED, routingKey, msg, Constants.MQ.LOW_DELAY);
    }

1 发送消息同步快递员任务

private void sendUpdateCourierTaskMsg(PickupDispatchTaskDTO pickupDispatchTaskDTO)
    {
        CourierTaskMsg courierTaskMsg = new CourierTaskMsg();
        courierTaskMsg.setCourierId(pickupDispatchTaskDTO.getCourierId());
        courierTaskMsg.setAgencyId(pickupDispatchTaskDTO.getAgencyId());
        courierTaskMsg.setTaskType(pickupDispatchTaskDTO.getTaskType().getCode());
        courierTaskMsg.setOrderId(pickupDispatchTaskDTO.getOrderId());
        courierTaskMsg.setMark(pickupDispatchTaskDTO.getMark());
        courierTaskMsg.setEstimatedEndTime(pickupDispatchTaskDTO.getEstimatedEndTime());
        courierTaskMsg.setCreated(Convert.toLong(LocalDateTime.now()));
        // 将 CourierTaskMsg 对象转化为 JSON 字符串
        String msg = JSONUtil.toJsonStr(courierTaskMsg);
        //将状态名称写入到路由key中,方便消费方选择性的接收消息
        String routingKey = Constants.MQ.RoutingKeys.COURIER_TASK_SAVE_OR_UPDATE;
        this.mqFeign.sendMsg(Constants.MQ.Exchanges.COURIER_TASK, routingKey, msg, Constants.MQ.LOW_DELAY);
    }

2 发送消息生成取派件任务

OrderMsg orderMsg = OrderMsg.builder()
                    .agencyId(taskPickupDispatch.getAgencyId())
                    .orderId(taskPickupDispatch.getOrderId())
                    .created(DateUtil.current())
                    .taskType(PickupDispatchTaskType.PICKUP.getCode()) //取件任务
                    .mark(taskPickupDispatch.getMark())
                    .estimatedEndTime(taskPickupDispatch.getEstimatedEndTime()).build();
            //发送消息(取消任务发生在取件之前,没有运单,参数直接填入null)
            this.transportOrderService.sendPickupDispatchTaskMsgToDispatch(null, orderMsg);
saveOrUpdate()代码 

继承的iservice中有该方法,可以直接使用

public void saveOrUpdate(CourierTaskDTO courierTaskDTO) {
        this.saveOrUpdate(BeanUtil.toBean(courierTaskDTO,CourierTaskEntity.class));
    }
findById()
mapper
public CourierTaskDTO findById(@Param("id") Long id);

@Param作用:在 MyBatis 映射 SQL 查询时,明确地将方法参数与 SQL 语句中的参数进行匹配

public CourierTaskDTO findById(Long id) {
        return this.findById(id);
    }

另:接口不用写public

CourierTaskDTO findById(@Param("id") Long id);

 此处返回的对象应为CourierTaskEntity,CourierTaskDto在另一个包domain中,访问不到

修改返回对象,并修改xml中内容

CourierTaskEntity findById(@Param("id") Long id);
xml
<select id="findById" resultType="com.sl.ms.search.entity.CourierTaskEntity">
        select * from sl_work.sl_courier_task c
        where c.id=#{id}
    </select>
findByOrderId()
mapper
List<CourierTaskEntity> findByOrderId(@Param("orderId")Long orderId);
xml
<select id="findByOrderId" resultType="com.sl.ms.search.entity.CourierTaskEntity">
    select * from sl_work.sl_courier_task c
    where c.order_id=#{orderId}
    order by created
</select>

车辆轨迹

创建运单后会对整个运输路线进行规划(借助高德地图服务),规划完成后轨迹点数据存于MongoDB,用于展现轨迹。

车辆在运输中、快递员在派件中会上报位置自己的位置,具体由各自的APP进行上报,用于展现当前车辆所在的位置。

功能效果图如下:
image.png
image.png

通过domain看别人给你传了什么参数(dto)

create()

MongonDB使用语法

Query.query(Criteria.where("transportOrderId").is(transportOrderId));
逻辑

通过运单id在MongoDB进行查询有无轨迹数据,若有抛出错误。

若无,创建轨迹实体进行保存。 

完整代码
public boolean create(String transportOrderId) {
        Query query = Query.query(Criteria.where("transportOrderId").is(transportOrderId));
        TrackEntity trackEntity = mongoTemplate.findOne(query, TrackEntity.class);
        if(ObjectUtil.isNotEmpty(trackEntity))
        {
            throw new SLException(TrackExceptionEnum.TRACK_ALREADY_EXISTS);
        }
        TrackEntity entity = new TrackEntity();
        entity.setId(new ObjectId());
        entity.setTransportOrderId(transportOrderId);
        //别忘了订单为新建状态
        entity.setStatus(TrackStatusEnum.NEW);
        entity.setPlanGeoJsonLine(getGeoJsonLineString(transportOrderId));
        //此处使用System.currentTimeMillis()代替LocalDateTime.now()
        entity.setCreated(System.currentTimeMillis());
        entity.setUpdated(System.currentTimeMillis());
        mongoTemplate.save(entity);
        return true;
    }
getGeoJsonLineString()

 其中车辆轨迹信息点是通过当前位置所在的机构的经纬度获取

通过transportOrderDTO中获取完整的transportLine(string类型)

通过JSONUtil工具获取其中的TransportLineNodeDTO(其中有organDTO获取经纬度)

        订单转化运单时(新建运单时)将TransLineNodeDTO存入TransportOrderEntity实体中

将List<OrganDTO>对象用.stream().map().filter().collect(Collectors.toList())收集为List<Point>对象

此处获取运单order对象,而不是运输路线line对象

TransportLineDTO所属Transport微服务,用于获取两个节点间的路线(最短,花费最少)

注意:两种构造函数,一个为List<Points>,一个为点拼接成的string字符串

该MongoDB的数据类型需要的类型为List<Point>

private GeoJsonLineString getGeoJsonLineString(String transportOrderId)
    {
        TransportOrderDTO transportOrderDTO = transportOrderFeign.findById(transportOrderId);
        String transportLine = transportOrderDTO.getTransportLine();
        //因为订单转换运单时使用的是TransportLineNodeDTO,此处将json字符串再转换回来即可
        TransportLineNodeDTO transportLineNodeDTO = JSONUtil.toBean(transportLine, TransportLineNodeDTO.class);
        //将节点中的每一个点构造成为List<point>对象,之后再传入构造函数构造GeoJsonLineString

        List<Point> points = transportLineNodeDTO.getNodeList().stream().map(node -> {
            //经度 (Longitude):通常对应于 X 轴,纬度 (Latitude):通常对应于 Y 轴
            return new Point(node.getLongitude(), node.getLatitude());
        }).collect(Collectors.toList());
        
        return  new GeoJsonLineString(points);
    }

通过gpt修改,添加一些判空操作

.filter(node ->ObjectUtil.isNotEmpty(node)) // 筛选出符合括号中的点留下,即将非空点留下

private GeoJsonLineString getGeoJsonLineString(String transportOrderId) {
        // 根据运单ID查询运单信息
        TransportOrderDTO transportOrderDTO = transportOrderFeign.findById(transportOrderId);
        // 获取运单的路线信息,这里是一个JSON字符串
        String transportLine = transportOrderDTO.getTransportLine();
        //创建链表节点存储数据
        List<Point> points = new ArrayList<>();
        if (ObjectUtil.isNotEmpty(transportLine)) {
            // 将JSON字符串转换为 TransportLineNodeDTO 对象
            TransportLineNodeDTO transportLineNodeDTO = JSONUtil.toBean(transportLine, TransportLineNodeDTO.class);

            // 检查 nodeList 是否为空
            if (ObjectUtil.isEmpty(transportLineNodeDTO.getNodeList())) {
                log.warn("运单路线节点列表为空,运单ID: {}", transportOrderId);
                return null;
            }
            // 将每个节点中的经纬度数据转换为 Point 对象的列表
            points = transportLineNodeDTO.getNodeList().stream()
                    .map(node -> {
                        // 检查节点经纬度是否为空
                        if (node.getLongitude() == null || node.getLatitude() == null) {
                            log.warn("节点中存在空的经纬度信息,跳过该节点");
                            return null;
                        }
                        // 创建 Point 对象,将经度(Longitude)作为 X 轴,纬度(Latitude)作为 Y 轴
                        return new Point(node.getLongitude(), node.getLatitude());
                    })
                    .filter(node ->ObjectUtil.isNotEmpty(node)) // 筛选出符合括号中的点留下,即将非空点留下
                    .collect(Collectors.toList());
        }
        //若没有transportLine信息,说明收件人和发件人在同一个网点,此时传入
        else{
            OrganDTO organDTO = organFeign.queryById(transportOrderDTO.getCurrentAgencyId());
            if (ObjectUtil.isNotEmpty(organDTO)) {
                Point point = new Point(organDTO.getLongitude(), organDTO.getLatitude());
                points.add(point);
            }
        }
        // 使用生成的 Point 列表构造 GeoJsonLineString 对象并返回
        return new GeoJsonLineString(points);
    }

complete()

完成轨迹,修改为完成状态

注意1 query中匹配多个数据时可以用in(),2 MongonDB中update的写法

3return updateResult.getModifiedCount()>0判断修改数据条数>0即修改成功

public boolean complete(List<String> transportOrderIds) {
        
        Query query = Query.query(Criteria.where("transportOrderid").in(transportOrderIds));
        Update update = Update.update("status", TrackStatusEnum.COMPLETE);
        UpdateResult updateResult = mongoTemplate.updateMulti(query, update, TrackEntity.class);
        return updateResult.getModifiedCount()>0;
    }

queryByTransportOrderId()

通过运单号查询轨迹
public TrackEntity queryByTransportOrderId(String transportOrderId) {
        Query query = Query.query(Criteria.where("transportOrderId").is(transportOrderId));
        TrackEntity trackEntity = mongoTemplate.findOne(query, TrackEntity.class);
        return trackEntity;
    }

uploadFromTruck()

MongonDB更新操作,set可以使一次update更新多个属性

@Override
    public void update(Person person) {
        //条件
        Query query = Query.query(Criteria.where("id").is(person.getId()));
        //更新的数据
        Update update = Update.update("age", person.getAge())
                .set("name", person.getName())
                .set("location", person.getLocation())
                .set("address", person.getAddress());
        //更新数据
        this.mongoTemplate.updateFirst(query, update, Person.class);
    }

 逻辑

运输任务id找到对应实体,找到运单号列表,将对应轨迹实体的最新经纬度更新。

@Resource
    private TransportTaskFeign transportTaskFeign;
    @Override
    public boolean uploadFromTruck(Long transportTaskId, double lng, double lat) {
        TransportTaskDTO transportTaskDTO = transportTaskFeign.findById(transportTaskId);
        List<String> transportOrderIds = transportTaskDTO.getTransportOrderIds();
        //此处不用查询运单id,根据列表的每一个运单id进行更新,直接根据轨迹id进行批量更新
        //不同的运单有不同的轨迹,故不能用统一的轨迹id查,靠运单id一个一个查
        for(String id:transportOrderIds)
        {
            Query query = Query.query(Criteria.where("transportOrderId").is(id));
            Update update = Update.update("lastPoint", new GeoJsonPoint(lng, lat));
            UpdateResult updateResult = this.mongoTemplate.updateFirst(query, update, TrackEntity.class);
            if(updateResult.getModifiedCount()<=0) return false;
        }
        return true;
    }

uploadFromCourier()

@Override
    public boolean uploadFromCourier(List<String> transportOrderIds, double lng, double lat) {
        for(String id:transportOrderIds)
        {
            Query query = Query.query(Criteria.where("transportOrderId").is(id));
            Update update = Update.update("lastPoint", new GeoJsonPoint(lng, lat));
            UpdateResult updateResult = this.mongoTemplate.updateFirst(query, update, TrackEntity.class);
            if(updateResult.getModifiedCount()<=0) return false;
        }
        return true;
    }

短信

 运单开始派送时发送短信通知收件人。

关于短信发送渠道,自行选择,不做强制要求。建议选择阿里云平台。

使用短信的渠道

将短信发送给对接第三方服务商

调用SendSms发送短信_短信服务(SMS)-阿里云帮助中心

阿里短信操作过程

手把手教你对接阿里云短信服务_阿里云短信对接文档-优快云博客

0使用免费的验证码模版

1创建新用户组,授予短信服务权限。

2创建新子用户加入该用户组,并创建该用户的key

LTAI5tELRXNrKtT6Q67v26KT
k6udW2ijoGea0NUYiehYgc3zUnHMha

3为项目添加sdk

<dependency>
  <groupId>com.aliyun</groupId>
  <artifactId>dysmsapi20170525</artifactId>
  <version>3.0.0</version>
</dependency>

4上面的步骤做完后,可以参考API文档中的示例代码,调用阿里云短信接口

调用SendSms发送短信_短信服务(SMS)-阿里云帮助中心

代码

1将配置信息传给Client并创建

2根据要求的request对象设置接口需要的参数

3根据要求的reponse对象得到结果

public class AliYunSmsSendHandler implements SmsSendHandler {
    public static Client createClient() throws Exception {
        Config config = new Config()
                // 配置 AccessKey ID,请确保代码运行环境配置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。
                .setAccessKeyId(System.getenv("LTAI5tELRXNrKtT6Q67v26KT"))
                // 配置 AccessKey Secret,请确保代码运行环境配置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
                .setAccessKeySecret(System.getenv("k6udW2ijoGea0NUYiehYgc3zUnHMha"));

        // 配置 Endpoint
        config.endpoint = "dysmsapi.aliyuncs.com";
        return new Client(config);
    }
    @Override
    public void send(SmsThirdChannelEntity smsThirdChannelEntity, List<SmsRecordEntity> smsRecordEntities) {
        // TODO 第三方发送短信验证码
        log.info("smsRecordEntities:{}", smsRecordEntities);
        log.info("smsThirdChannelEntity:{}", smsThirdChannelEntity);
        CollUtil.forEach(smsRecordEntities, (value, index) -> {
            log.info("短信发送成功 ...");
            //设置发送成功状态
            if(ObjectUtil.equals(smsThirdChannelEntity.getSendChannel(),SendChannelEnum.ALI_YUN)){
                Client client = null;
                try {
                    client = this.createClient();
                } catch (Exception e) {
                    e.printStackTrace();
                }

                // 构造请求对象,请替换请求参数值
                SendSmsRequest sendSmsRequest = new SendSmsRequest()
                        .setPhoneNumbers("18098052816")
                        .setSignName("阿里云")
                        .setTemplateCode("SMS_305508669")
                        .setTemplateParam("{\"name\":\"张三\",\"number\":\"18098052816\"}"); // TemplateParam为序列化后的JSON字符串

                // 获取响应对象
                SendSmsResponse sendSmsResponse = null;
                try {
                    sendSmsResponse = client.sendSms(sendSmsRequest);
                } catch (Exception e) {
                    e.printStackTrace();
                }

                // 响应包含服务端响应的 body 和 headers
                if(ObjectUtil.isNotEmpty(sendSmsResponse)) {
                    value.setStatus(SendStatusEnum.SUCCESS);
                }
            }

        });
    }
}

优化

查询优化

定义全局线程池技术进行异步优化

tradejob

对以下查询数据库代码进行优化(否则同时4,50个查询时,速度变慢)

在for循环中,执行查询代码时使用

// 定义全局线程池
    private static final ExecutorService executorService = Executors.newFixedThreadPool(10); // 可以根据需要调整线程数量
    /**
     * 分片广播方式查询支付状态
     * 逻辑:每次最多查询{tradingCount}个未完成的交易单,交易单id与shardTotal取模,值等于shardIndex进行处理
     */
    @XxlJob("tradingJob")
    public void tradingJob() {
        // 分片参数
        int shardIndex = NumberUtil.max(XxlJobHelper.getShardIndex(), 0);
        int shardTotal = NumberUtil.max(XxlJobHelper.getShardTotal(), 1);
        //创建future对象
        List<Future<?>> futures = new ArrayList<>();
        //tradingCount为每次查询的数量,主动轮询为兜底,主要靠异步通知修改状态
        List<TradingEntity> list = this.tradingService.findListByTradingState(TradingStateEnum.FKZ, tradingCount);
        if (CollUtil.isEmpty(list)) {
            XxlJobHelper.log("查询到交易单列表为空!shardIndex = {}, shardTotal = {}", shardIndex, shardTotal);
            return;
        }

        List<TradeStatusMsg> tradeMsgList = new ArrayList<>();
        for (TradingEntity trading : list) {
            if (trading.getTradingOrderNo() % shardTotal != shardIndex) {
                continue;
            }
            // 创建一个任务并添加到任务列表中
            Runnable task = new Runnable() {
                @Override
                public void run() {
                    try {
                        // 查询交易单
                        TradingDTO tradingDTO = basicPayService.queryTrading(trading.getTradingOrderNo());
                        if (TradingStateEnum.FKZ != tradingDTO.getTradingState()) {
                            TradeStatusMsg tradeStatusMsg = TradeStatusMsg.builder()
                                    .tradingOrderNo(trading.getTradingOrderNo())
                                    .productOrderNo(trading.getProductOrderNo())
                                    .statusCode(tradingDTO.getTradingState().getCode())
                                    .statusName(tradingDTO.getTradingState().name())
                                    .build();

                            // 确保对 tradeMsgList 的访问是线程安全的,因为多个任务可能会同时尝试向列表中添加消息。
                            synchronized (tradeMsgList) {
                                tradeMsgList.add(tradeStatusMsg);
                            }
                        }
                    } catch (Exception e) {
                        XxlJobHelper.log("查询交易单出错!shardIndex = {}, shardTotal = {}, trading = {}", shardIndex, shardTotal, trading, e);
                    }
                }
            };
            // 提交所有任务到线程池,以便在后台异步执行
            //使用线程池提交这些任务,而不是在主线程中执行它们。
            // 将任务添加到列表并提交到线程池,并添加到future
            futures.add(executorService.submit(task));
        }
        // 等待所有任务完成
        for (Future<?> future : futures) {
            try {
                future.get(); // 阻塞,直到任务完成
            } catch (Exception e) {
                log.error("任务执行出错: {}", e.getMessage());
            }
        }
        executorService.shutdown();//关闭线程池
        // 发送消息通知其他系统
        String msg = JSONUtil.toJsonStr(tradeMsgList);
        this.mqFeign.sendMsg(Constants.MQ.Exchanges.TRADE, Constants.MQ.RoutingKeys.TRADE_UPDATE_STATUS, msg);
    }

使用朗姆达表达式

Runnable task = () -> {
    try {
        // 任务的具体逻辑
    } catch (Exception e) {
        // 处理异常
        log.error("XXXX: {}", e.getMessage());
    }
};
Runnable task = () -> {
        try {
            // 查询交易单
            TradingDTO tradingDTO = this.basicPayService.queryTrading(trading.getTradingOrderNo());
            if (TradingStateEnum.FKZ != tradingDTO.getTradingState()) {
                TradeStatusMsg tradeStatusMsg = TradeStatusMsg.builder()
                        .tradingOrderNo(trading.getTradingOrderNo())
                        .productOrderNo(trading.getProductOrderNo())
                        .statusCode(tradingDTO.getTradingState().getCode())
                        .statusName(tradingDTO.getTradingState().name())
                        .build();
                synchronized (tradeMsgList) {
                    tradeMsgList.add(tradeStatusMsg); // 线程安全地添加消息
                }
            }
        } catch (Exception e) {
            XxlJobHelper.log("查询交易单出错!shardIndex = {}, shardTotal = {}, trading = {}", shardIndex, shardTotal, trading, e);
        }
    };
        // 将任务添加到列表并提交到线程池,并添加到future
        futures.add(executorService.submit(task));
        }
        // 等待所有任务完成
        for (Future<?> future : futures) {
            try {
                future.get(); // 阻塞,直到任务完成
            } catch (Exception e) {
                log.error("任务执行出错: {}", e.getMessage());
            }
        }
executorService.shutdown();//关闭线程池

dispatachJob

private static final ExecutorService executorService = Executors.newFixedThreadPool(10); // 可以根据需要调整线程数量
    /**
     * 分片广播方式处理运单,生成运输任务
     */
    @XxlJob("transportTask")
    public void transportTask() {
        // 分片参数
        int shardIndex = XxlJobHelper.getShardIndex();
        int shardTotal = XxlJobHelper.getShardTotal();
        // 定义一个List来保存Future对象
        List<Future<?>> futures = new ArrayList<>();
        //根据分片参数 2小时内并且可用状态车辆
        // List<TruckPlanDto> truckDtoList = this.queryTruckPlanDtoList(shardIndex, shardTotal);
        List<TruckPlanDto> truckDtoList = this.truckPlanFeign.pullUnassignedPlan(shardTotal, shardIndex);
        if (CollUtil.isEmpty(truckDtoList)) {
            return;
        }
        // 对每一个车辆都进行处理
        // 为了相同目的地的运单尽可能的分配在一个运输任务中,所以需要在读取数据时进行锁定,一个车辆处理完成后再开始下一个车辆处理
        // 在这里,使用redis的分布式锁实现
        for (TruckPlanDto truckPlanDto : truckDtoList) {
            //校验车辆计划对象
            if (ObjectUtil.hasEmpty(truckPlanDto.getStartOrganId(), truckPlanDto.getEndOrganId(),
                    truckPlanDto.getTransportTripsId(), truckPlanDto.getId())) {
                log.error("车辆计划对象参数缺失, truckPlanDto -> {}", truckPlanDto);
                continue;
            }
            //根据该车辆的开始、结束机构id,来确定要处理的运单数据集合
            Long startOrganId = truckPlanDto.getStartOrganId();
            Long endOrganId = truckPlanDto.getEndOrganId();
            String redisKey = this.transportOrderDispatchMQListener.getListRedisKey(startOrganId, endOrganId);
            String lockRedisKey = Constants.LOCKS.DISPATCH_LOCK_PREFIX + redisKey;
            //获取锁
            Runnable task = () -> {
                try {
                    RLock lock = this.redissonClient.getFairLock(lockRedisKey);
                    List<DispatchMsgDTO> dispatchMsgDTOList = new ArrayList<>();
                    try {
                        //锁定,一直等待锁,一定要获取到锁,因为查询到车辆的调度状态设置为:已分配
                        lock.lock();
                        //计算车辆运力、合并运单
                        this.executeTransportTask(redisKey, truckPlanDto.getTruckDto(), dispatchMsgDTOList);
                    } finally {
                        lock.unlock();
                    }
                    //生成运输任务
                    this.createTransportTask(truckPlanDto, startOrganId, endOrganId, dispatchMsgDTOList);
                }catch (Exception e) {
                    log.error("任务执行出错: {}", e.getMessage());
                }
            };
            // 将任务添加到列表并提交到线程池,并添加到future
            futures.add(executorService.submit(task));
        }
        // 等待所有任务完成
        for (Future<?> future : futures) {
            try {
                future.get(); // 阻塞,直到任务完成
            } catch (Exception e) {
                log.error("任务执行出错: {}", e.getMessage());
            }
        }
        executorService.shutdown();
        //发送消息通过车辆已经完成调度
        this.completeTruckPlan(truckDtoList);
    }
使用future优化

防止任务为完成时关闭线程池导致任务丢失

// 定义一个List来保存Future对象
List<Future<?>> futures = new ArrayList<>();
// 提交任务并保存Future对象
futures.add(executorService.submit(task));
// 等待所有任务完成
for (Future<?> future : futures) {
    try {
        future.get(); // 阻塞,直到任务完成
    } catch (InterruptedException | ExecutionException e) {
        log.error("任务执行出错: {}", e.getMessage());
    }
}

缓存优化

reids使用
String redisKey = "carriageTemplate:" + carriageDto.getTemplateType() + ":" + carriageDto.getTransportType();
// 从 Redis 缓存中查询数据
String redisValue = stringRedisTemplate.opsForValue().get(redisKey);
//reids中查到,使用JSONUtil.toBean将字符串转换为dto对象返回
if(ObjectUtil.isNotEmpty(redisValue))
{
    return JSONUtil.toBean(redisValue,CarriageDTO.class);
}

使用JSONUtil.toJsonStr(carriageEntityList.get(0))而不是carriageEntityList.get(0).toString()

 //查询数据库,如果数据库查询不为空,即模版重复,更新redis
stringRedisTemplate.opsForValue().set(redisKey, JSONUtil.toJsonStr(carriageEntityList.get(0)), 10, TimeUnit.MINUTES); // 10 分钟缓存过期
代码
@Override
    public CarriageDTO saveOrUpdate(CarriageDTO carriageDto) {
        log.info("新增运费模板 --> {}", carriageDto);
        //校验运费模板是否存在,如果不存在直接插入(查询条件: 模板类型 运送类型 如果是修改排除当前id)运输类型
        // 定义 Redis key,以模板类型和运输类型为 key 的一部分
        String redisKey = "carriageTemplate:" + carriageDto.getTemplateType() + ":" + carriageDto.getTransportType();
        // 从 Redis 缓存中查询数据
        String redisValue = stringRedisTemplate.opsForValue().get(redisKey);
        //reids中查到,使用JSONUtil.toBean将字符串转换为dto对象返回
        if(ObjectUtil.isNotEmpty(redisValue))
        {
            return JSONUtil.toBean(redisValue,CarriageDTO.class);
        }
        //从reids中没查到,查询数据库
        else{
            //使用LambdaQueryWrapper<>构造查询。模板类型,运送类型相同,且id不同的数据
            LambdaQueryWrapper<CarriageEntity> queryWrapper = new LambdaQueryWrapper<>();
            //eq 方法用于在查询中添加等于条件
            queryWrapper.eq(CarriageEntity::getTemplateType, carriageDto.getTemplateType())
                    .eq(CarriageEntity::getTransportType, carriageDto.getTransportType())
                    //ne 方法用于添加不等于条件,如果为修改原有数据,则查询id不是carriageDto.getId()的数据
                    //ObjectUtil.isNotEmpty(carriageDto.getId())判断非空时
                    .ne(ObjectUtil.isNotEmpty(carriageDto.getId()), CarriageEntity::getId, carriageDto.getId());

            //使用LambdaQueryWrapper构造条件后,在数据库中进行查询,不存在直接插入
            List<CarriageEntity> carriageEntityList = super.list(queryWrapper);

            //如果数据库查询为空,可以直接插入或更新操作(DT0 转entity 保存成功 entity 转 DTO),并删除redis缓存
            //CollUtil.isEmpty判断,而不是 == null判断
            //CollUtil.isEmpty(collection) 更具体地判断集合是否为空集合(即集合对象不为 null 且集合大小为 0)。
            //== null 只能检查对象是否为 null,无法判断对象是否为空集合或者空数组。
            if (CollUtil.isEmpty(carriageEntityList)) {
                stringRedisTemplate.delete(redisKey);
                return saveOrUpdateCarriage(carriageDto);
            }
            //如果数据库查询不为空,即模版重复,更新redis
            //如果存在重复模板,判断此次插入的是否为经济区互寄.ECONOMIC_ZONE常量,非经济区互寄是不可以重复的
            if (ObjectUtil.notEqual(carriageDto.getTemplateType(), CarriageConstant.ECONOMIC_ZONE)) {
                //抛出异常:非经济区互寄是不可以重复
                throw new SLException(CarriageExceptionEnum.NOT_ECONOMIC_ZONE_REPEAT);
            }
            //如果是经济区互寄类型,需进一步判断关联城市是否重复,通过集合取交集判断是否重复
            List<String> associatedCityList = carriageEntityList.stream().map(CarriageEntity::getAssociatedCity)
                    .map(associatedCity -> StrUtil.splitToArray(associatedCity, ","))
                    //防止出现(2,3)的组合,使用flat重新拆散为单个元素
                    .flatMap(Arrays::stream)
                    .collect(Collectors.toList());
            //经济区重复
            Collection<String> intersection = CollUtil.intersection(associatedCityList, carriageDto.getAssociatedCityList());
            if (CollUtil.isNotEmpty(intersection)) {
                throw new SLException(CarriageExceptionEnum.ECONOMIC_ZONE_CITY_REPEAT);
            }
            //如果数据库查询不为空,即模版重复,更新redis
            stringRedisTemplate.opsForValue().set(redisKey, JSONUtil.toJsonStr(carriageEntityList.get(0)), 10, TimeUnit.MINUTES); // 10 分钟缓存过期
            //如果没有重复,可以新增或更新(DTO转entity 保存成功entity 转DTO)
            return saveOrUpdateCarriage(carriageDto);
        }
    }

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值