需求
取派件任务搜索
快递员端的操作,包含【任务搜索】和【最近搜索】
【最近搜索】已经在sl-express-ms-web-courier中实现,所以在实战中,只需要实现【任务搜索】即可。
任务搜索的需求如下:(仔细阅读需求)

功能界面:


任务介绍
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进行上报,用于展现当前车辆所在的位置。
功能效果图如下:



通过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);
}
}
22万+






