首页 > 其他分享 >04-路线规划

04-路线规划

时间:2024-04-18 09:15:16浏览次数:16  
标签:return name 04 com 路线 sl import 规划 transport

1. Cypher 入门

1.1 查询数据

a. 基本查询

// 查询所有的数据,数据量大是勿用
MATCH (n) RETURN n
// 查询所有的网点(AGENCY)
MATCH (n:AGENCY) RETURN n
// 查询所有与“北京市转运中心”有关系的节点
MATCH (n:OLT {name: "北京市转运中心"}) -- (m) RETURN n,m
// 查询所有"北京市转运中心"关联的一级转运中心
MATCH (n:OLT {name:"北京市转运中心"}) --> (m:OLT) RETURN n,m
// 可以指定关系标签查询
MATCH (n:OLT {name:"北京市转运中心"}) -[r:IN_LINE]- (m) RETURN n,r,m
// 将查询赋值与变量
MATCH p = (n:OLT {name:"北京市转运中心"}) --> (m:OLT) RETURN p
// 通过 type()函数查询关系类型
MATCH (n:OLT {name:"北京市转运中心"}) -[r]-> (m:OLT {name:"南京市转运中心"}) RETURN type(r)

b. 关系深度查询

可以指定关系的深度进行查询,语法格式:-[:TYPE*minHops..maxHops]->

// 查询【北京市转运中心】关系中深度为1~2层关系的节点
MATCH (n:OLT {name:"北京市转运中心"}) -[*1..2]->(m) RETURN *
// 也可以这样
MATCH (n:OLT {name:"北京市转运中心"}) -[*..2]->(m) RETURN *
// 也可以通过变量的方式查询
MATCH path = (n:OLT {name:"北京市转运中心"}) -[*..2]->(m) RETURN path
// 查询关系,relationships()获取结果中的关系,WITH向后传递数据
MATCH path = (n:OLT {name:"北京市转运中心"}) -[*..2]->(m)
WITH n,m, relationships(path) AS r
RETURN r
// 查询两个网点之间所有的路线,最大深度为6,可以查询到2条路线
MATCH path = (n:AGENCY) -[*..6]->(m:AGENCY)
WHERE n.name = "北京市昌平区定泗路" AND m.name = "上海市浦东新区南汇"
RETURN path
// 查询两个网点之间最短路径,查询深度最大为10
MATCH path = shortestPath((n:AGENCY) -[*..10]->(m:AGENCY))
WHERE n.name = "北京市昌平区定泗路" AND m.name = "上海市浦东新区南汇"
RETURN path
// 查询两个网点之间所有的路线中成本最低的路线,最大深度为10(如果成本相同,转运节点最少)
MATCH path = (n:AGENCY) -[*..10]->(m:AGENCY)
WHERE n.name = "北京市昌平区定泗路" AND m.name = "上海市浦东新区南汇"
UNWIND relationships(path) AS r
WITH sum(r.cost) AS cost, path
RETURN path ORDER BY cost ASC, LENGTH(path) ASC LIMIT 1
// UNWIND是将列表数据展开操作
// sum()是聚合统计函数,类似还有:avg()、max()、min()等

c. 分页查询

// 分页查询网点,按照bid正序排序,每页查询2条数据
// 第一页
MATCH (n:AGENCY) 
RETURN n ORDER BY n.bid ASC SKIP 0 LIMIT 2
// 第二页
MATCH (n:AGENCY) 
RETURN n ORDER BY n.bid ASC SKIP 2 LIMIT 2

1.2 更新数据

更新数据是使用 SET 语句进行标签、属性的更新。SET 操作是幂等性的。

// 更新/设置 属性
MATCH (n:AGENCY {name:"北京市昌平区新龙城"})
SET n.address = "龙跃苑四区3号楼底商101号"
RETURN n
// 通过remove移除属性
MATCH (n:AGENCY {name:"北京市昌平区新龙城"}) REMOVE n.address RETURN n
// 没有address属性的增加属性
MATCH (n:AGENCY) WHERE n.address IS NULL SET n.address = "暂无地址" RETURN n

1.3 删除数据

删除数据通过 DELETE、DETACH DELETE 完成。其中 DELETE 不能删除有关系的节点,删除关系就需要 DETACH DELETE 了。

// 删除节点
MATCH (n:AGENCY {name:"航头营业部"}) DELETE n
// 有关系的节点是不能直接删除的
MATCH (n:AGENCY {name:"北京市昌平区新龙城"}) DELETE n
// 删除节点和关系
MATCH (n:AGENCY {name:"北京市昌平区新龙城"}) DETACH DELETE n
// 删除所有节点和关系,慎用!
MATCH (n) DETACH DELETE n

1.4 索引

在 Neo4j 中同样也支持索引,对字段做索引可以提升查询速度。

// 创建索引语法(OPTIONS子句指定索引提供程序和配置):
CREATE [TEXT] INDEX [index_name] [IF NOT EXISTS]
FOR (n:LabelName)
ON (n.propertyName)
[OPTIONS "{" option: value[, ...] "}"]
// 示例:
CREATE TEXT INDEX agency_index_bid IF NOT EXISTS FOR (n:AGENCY) ON (n.bid)
// 删除索引语法:
DROP INDEX index_name
// 示例:
DROP INDEX agency_index_bid

2. SDN 快速入门

Spring Data Neo4j 简称 SDN,是 Spring 对 Neo4j 数据库操作的封装,其底层基于 neo4j-java-driver 实现。

2.1 创建工程

创建工程 sl-express-sdn,导入依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.sl-express</groupId>
        <artifactId>sl-express-parent</artifactId>
        <version>1.3</version>
    </parent>
    <groupId>com.sl-express.sdn</groupId>
    <artifactId>sl-express-sdn</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <sl-express-common.version>1.1-SNAPSHOT</sl-express-common.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.sl-express.common</groupId>
            <artifactId>sl-express-common</artifactId>
            <version>${sl-express-common.version}</version>
        </dependency>
        <!-- SDN依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-neo4j</artifactId>
        </dependency>
    </dependencies>
</project>

2.2 编写配置文件

server:
  port: 9902
logging:
  level:
    org.springframework.data.neo4j: debug
spring:
  application:
    name: sl-express-sdn
  mvc:
    pathmatch:
      # 解决异常:swagger Failed to start bean 'documentationPluginsBootstrapper'; 
      # nested exception is java.lang.NullPointerException
      # 因为Springfox使用的路径匹配是基于AntPathMatcher的,而Spring Boot 2.6.X使用的是PathPatternMatcher
      matching-strategy: ant_path_matcher
  data:
    neo4j:
      database: neo4j
  neo4j:
    authentication:
      username: neo4j
      password: neo4j123
    uri: neo4j://192.168.150.101:7687

2.3 基础代码

a. 启动类

package com.sl.sdn;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SDNApplication {
    public static void main(String[] args) {
        SpringApplication.run(SDNApplication.class, args);
    }
}

b. 实体类

编写实体,在物流中,会存在网点、二级转运中心、一级转运中心,我们分别用 Agency、TLT、OLT 表示。

由于以上三个机构的属性是相同的,但在 Neo4j 中的标签是不一样的,所以既要保证不同的类,也有相同的属性,这种场景比较适合将属性写到父类中,自己继承父类来实现,这里我们采用抽象类的来实现。

package com.sl.sdn.entity.node;
import com.sl.sdn.enums.OrganTypeEnum;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.springframework.data.geo.Point;
import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.Id;

@Data
@SuperBuilder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
public abstract class BaseEntity {
    
    @Id
    @GeneratedValue
    @ApiModelProperty(value = "Neo4j ID", hidden = true)
    private Long id;
    
    @ApiModelProperty(value = "业务id", required = true)
    private Long bid;
    
    @ApiModelProperty(value = "名称", required = true)
    private String name;
    
    @ApiModelProperty(value = "电话", required = true)
    private String phone;
    
    @ApiModelProperty(value = "地址", required = true)
    private String address;
    
    @ApiModelProperty(value = "位置坐标, x: 纬度,y: 经度", required = true)
    private Point location;

    /** 机构类型 */
    public abstract OrganTypeEnum getAgencyType();
}

机构枚举:

package com.sl.sdn.enums;
import cn.hutool.core.util.EnumUtil;
import com.sl.transport.common.enums.BaseEnum;

/**
 * 机构类型枚举
 */
public enum OrganTypeEnum implements BaseEnum {
    
    OLT(1, "一级转运中心"),
    TLT(2, "二级转运中心"),
    AGENCY(3, "网点");
    
    /**
     * 类型编码
     */
    private final Integer code;
    
    /**
     * 类型值
     */
    private final String value;
    
    OrganTypeEnum(Integer code, String value) {
        this.code = code;
        this.value = value;
    }
    
    public Integer getCode() {
        return code;
    }
    
    public String getValue() {
        return value;
    }
    
    public static OrganTypeEnum codeOf(Integer code) {
        return EnumUtil.getBy(OrganTypeEnum::getCode, code);
    }
    
}

各个实体类:

/**
 * 网点实体
 */
@Node("AGENCY")
@Data
@ToString(callSuper = true)
@SuperBuilder(toBuilder = true)
@NoArgsConstructor
public class AgencyEntity extends BaseEntity {
    @Override
    public OrganTypeEnum getAgencyType() {
        return OrganTypeEnum.AGENCY;
    }
}

/**
 * 一级转运中心实体 (OneLevelTransportEntity)
 */
@Node("OLT")
@Data
@ToString(callSuper = true)
@SuperBuilder(toBuilder = true)
@NoArgsConstructor
public class OLTEntity extends BaseEntity {
    @Override
    public OrganTypeEnum getAgencyType() {
        return OrganTypeEnum.OLT;
    }
}

/**
 * 二级转运中心实体(TwoLevelTransportEntity)
 */
@Node("TLT")
@Data
@ToString(callSuper = true)
@SuperBuilder(toBuilder = true)
@NoArgsConstructor
public class TLTEntity extends BaseEntity {
    @Override
    public OrganTypeEnum getAgencyType() {
        return OrganTypeEnum.TLT;
    }
}

/**
 * 运输路线实体
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TransportLine {
    private Long id;
    private Double cost;
}

c. 传输实体类

DTO 用于服务间的数据传输,会用到 OrganDTOTransportLineNodeDTO

/**
 * 机构数据对象,网点、一级转运、二级转运都是看作是机构
 * BaseEntity中的location无法序列化,需要将经纬度拆开封装对象
 */
@Data
public class OrganDTO {
    @Alias("bid") // 业务id作为id进行封装
    @ApiModelProperty(value = "机构id", required = true)
    private Long id;
    @ApiModelProperty(value = "名称", required = true)
    private String name;
    @ApiModelProperty(value = "类型,1:一级转运,2:二级转运,3:网点", required = true)
    private Integer type;
    @ApiModelProperty(value = "电话", required = true)
    private String phone;
    @ApiModelProperty(value = "地址", required = true)
    private String address;
    @ApiModelProperty(value = "纬度", required = true)
    private Double latitude;
    @ApiModelProperty(value = "经度", required = true)
    private Double longitude;
}

/**
 * 运输路线对象
 */
@Data
public class TransportLineNodeDTO {
    @ApiModelProperty(value = "节点列表", required = true)
    private List<OrganDTO> nodeList = new ArrayList<>();
    @ApiModelProperty(value = "路线成本", required = true)
    private Double cost = 0d;
}

2.4 Repository

SDN 也是遵循了 Spring Data JPA 规范,同时也提供了 Neo4jRepository,该接口中提供了基本的 CRUD 操作,我们定义 Repository 需要继承该接口。

a. AgencyRepository

package com.sl.sdn.repository;

import com.sl.sdn.entity.node.AgencyEntity;
import org.springframework.data.neo4j.repository.Neo4jRepository;

/**
 * 网点操作
 */
public interface AgencyRepository extends Neo4jRepository<AgencyEntity, Long> {
    
    /**
     * 根据bid查询
     *
     * @param bid 业务id
     * @return 网点数据
     */
    AgencyEntity findByBid(Long bid);
    
    /**
     * 根据bid删除
     *
     * @param bid 业务id
     * @return 删除的数据条数
     */
    Long deleteByBid(Long bid);
    
}

b. OLTRepository

package com.sl.sdn.repository;

import com.sl.sdn.entity.node.OLTEntity;
import org.springframework.data.neo4j.repository.Neo4jRepository;

/**
 * 一级转运中心数据操作
 */
public interface OLTRepository extends Neo4jRepository<OLTEntity, Long> {
    
    /**
     * 根据bid查询
     *
     * @param bid 业务id
     * @return 一级转运中心数据
     */
    OLTEntity findByBid(Long bid);
    
    /**
     * 根据bid删除
     *
     * @param bid 业务id
     * @return 删除的数据条数
     */
    Long deleteByBid(Long bid);
    
}

c. TLTRepository

package com.sl.sdn.repository;

import com.sl.sdn.entity.node.TLTEntity;
import org.springframework.data.neo4j.repository.Neo4jRepository;

/**
 * 二级转运中心数据操作
 */
public interface TLTRepository extends Neo4jRepository<TLTEntity, Long> {
    
    /**
     * 根据bid查询
     *
     * @param bid 业务id
     * @return 二级转运中心数据
     */
    TLTEntity findByBid(Long bid);
    
    /**
     * 根据bid删除
     *
     * @param bid 业务id
     * @return 删除的数据条数
     */
    Long deleteByBid(Long bid);
    
}

d. OrganRepository

package com.sl.sdn.repository;

import com.sl.sdn.dto.OrganDTO;
import java.util.List;

/**
 * 通用机构查询
 */
public interface OrganRepository {
    
    /**
     * 无需指定type,根据id查询
     *
     * @param bid 业务id
     * @return 机构数据
     */
    OrganDTO findByBid(Long bid);
    
    /**
     * 查询所有的机构,如果name不为空的按照name模糊查询
     *
     * @param name 机构名称
     * @return 机构列表
     */
    List<OrganDTO> findAll(String name);
    
}

e. JPA 自定义方法规则

使用 JPA 中的规则,进行自定义查询:

Keyword Sample Cypher snippet
After findByLaunchDateAfter(Date date) n.launchDate > date
Before findByLaunchDateBefore(Date date) n.launchDate < date
Containing (String) findByNameContaining(String namePart) n.name CONTAINS namePart
Containing (Collection) findByEmailAddressesContains(Collection addresses) findByEmailAddressesContains(String address) ANY(collectionFields IN [addresses] WHERE collectionFields in n.emailAddresses) ANY(collectionFields IN address WHERE collectionFields in n.emailAddresses)
In findByNameIn(Iterable names) n.name IN names
Between findByScoreBetween(double min, double max) findByScoreBetween(Range range) n.score >= min AND n.score <= max Depending on the Range definition n.score >= min AND n.score <= max or n.score > min AND n.score < max
StartingWith findByNameStartingWith(String nameStart) n.name STARTS WITH nameStart
EndingWith findByNameEndingWith(String nameEnd) n.name ENDS WITH nameEnd
Exists findByNameExists() EXISTS(n.name)
True findByActivatedIsTrue() n.activated = true
False findByActivatedIsFalse() NOT(n.activated = true)
Is findByNameIs(String name) n.name = name
NotNull findByNameNotNull() NOT(n.name IS NULL)
Null findByNameNull() n.name IS NULL
GreaterThan findByScoreGreaterThan(double score) n.score > score
GreaterThanEqual findByScoreGreaterThanEqual(double score) n.score >= score
LessThan findByScoreLessThan(double score) n.score < score
LessThanEqual findByScoreLessThanEqual(double score) n.score <= score
Like findByNameLike(String name) n.name =~ name
NotLike findByNameNotLike(String name) NOT(n.name =~ name)
Near findByLocationNear(Distance distance, Point point) distance( point(n),point({latitude:lat, longitude:lon}) ) < distance
Regex findByNameRegex(String regex) n.name =~ regex
And findByNameAndDescription(String name, String description) n.name = name AND n.description = description
Or findByNameOrDescription(String name, String description) n.name = name OR n.description = description (Cannot be used to OR nested properties)

2.5 复杂查询

通过继承 Neo4jRepository 实现简单的查询是非常方便的,如果要实现复杂的查询就需要定义 Cypher 查询实现了,需要通过 Neo4jClient 进行查询操作,下面我们以查询两个网点间最短运输路线为例进行查询。

a. 定义 Repository

package com.sl.sdn.repository;

import com.sl.sdn.dto.TransportLineNodeDTO;
import com.sl.sdn.entity.node.AgencyEntity;

/**
 * 运输路线相关操作
 */
public interface TransportLineRepository {
    
    /**
     * 查询两个网点之间最短的路线,查询深度为:10
     *
     * @param start 开始网点
     * @param end   结束网点
     * @return 路线
     */
    TransportLineNodeDTO findShortestPath(AgencyEntity start, AgencyEntity end);
    
}

b. 编写实现

package com.sl.sdn.repository.impl;
import com.sl.sdn.dto.TransportLineNodeDTO;
import com.sl.sdn.entity.node.AgencyEntity;
import com.sl.sdn.repository.TransportLineRepository;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
@Component
public class TransportLineRepositoryImpl implements TransportLineRepository {
    
    @Resource
    private Neo4jClient neo4jClient;
    
    /**
     * 根据起止网点  查询转运路线最短的路线信息
     * @param start 开始网点
     * @param end   结束网点
     * @return
     */
    @Override
    public TransportLineNodeDTO findShortestPath(AgencyEntity start, AgencyEntity end) {
        return findShortestPath(start, end, 10);
    }

    @Override
    public TransportLineNodeDTO findShortestPath(AgencyEntity start, AgencyEntity end, int depth) {

        String type = AgencyEntity.class.getAnnotation(Node.class).value()[0];

        String cypherQuery = StrUtil.format(
            "MATCH path = shortestPath((start:{}) -[*..{}]-> (end:{})) "
                + "WHERE start.bid = $startId AND end.bid = $endId "
                + "AND start.status = true AND end.status = true " 
                + "RETURN path", type, depth, type);
        Collection<TransportLineNodeDTO> line = this.executeQueryPath(cypherQuery, start, end);
        if (CollUtil.isEmpty(line)) {
            return null;
        }
        for (TransportLineNodeDTO transportLineNodeDTO : line) {
            return transportLineNodeDTO;
        }
        return null;
    }
}

3. 路线规划服务

拉取 sl-express-ms-transport 相关的代码:

工程名
sl-express-ms-transport-api
sl-express-ms-transport-domain
sl-express-ms-transport-service

3.1 代码结构

(0)在配置文件 bootstrap-local.yml 中引入了如下共享配置

server:
  port: 18083
  tomcat:
    uri-encoding: UTF-8
    threads:
      max: 1000
      min-spare: 30
spring:
  cloud:
    nacos:
      username: nacos
      password: nacos
      server-addr: 192.168.150.101:8848
      discovery:
        namespace: ecae68ba-7b43-4473-a980-4ddeb6157bdc
      config:
        namespace: ecae68ba-7b43-4473-a980-4ddeb6157bdc
        shared-configs:
          # 关于rabbitmq的统一配置,其中有对于消息消费失败处理的配置项
          - data-id: shared-spring-rabbitmq.yml
            group: SHARED_GROUP
            refresh: false
          # 自研对接地图服务商的中台服务EagleMap的配置
          - data-id: shared-spring-eaglemap.yml
            group: SHARED_GROUP
            refresh: false
          # Neo4j的相关配置
          - data-id: shared-spring-neo4j.yml
            group: SHARED_GROUP
            refresh: false
          - data-id: shared-spring-redis.yml
            group: SHARED_GROUP
            refresh: false

(1)下面是路线规划微服务代码结构,主要是实现下面选中的部分

关于 Entity,与 sl-express-sdn 工程的类似,只是属性多了一些,按照项目的业务需求制定的。

(2)Feign 接口定义

(3)Domain 定义

3.2 机构同步

机构的新增、更新、删除是在权限管家中完成的,需要是操作后同步到路线规划微服务中,这里采用的是 MQ 消息通知的方式。

上图是在权限管家中新增组织的界面,可以从界面中看出,添加的组织并没有标识是【网点】还是【转运中心】,所以,在这里我们做一下约定,按照机构名称的后缀进行区分,具体规则如下:

  • xxx转运中心 → 一级转运中心(OLT)
  • xxx分拣中心 → 二级转运中心 (TLT)
  • xxx营业部 → 网点(AGENCY)

AuthMQListener

package com.sl.transport.mq;

/**
 * 对于权限管家系统消息的处理
 */
@Slf4j
@Component
public class AuthMQListener {
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = Constants.MQ.Queues.AUTH_TRANSPORT),
            exchange = @Exchange(name = "${rabbitmq.exchange}", type = ExchangeTypes.TOPIC),
            key = "#"
    ))
    public void listenAgencyMsg(String msg) {
        // {"type":"ORG","operation":"ADD","content":[{"id":"977263044792942657",
        // "name":"55","parentId":"0","managerId":null,"status":true}]}
        log.info("接收到消息 -> {}", msg);
        JSONObject jsonObject = JSONUtil.parseObj(msg);
        String type = jsonObject.getStr("type");
        // 非机构消息
        if (!StrUtil.equalsIgnoreCase(type, "ORG")) {
            return;
        }
        String operation = jsonObject.getStr("operation");
        JSONObject content = (JSONObject) jsonObject.getJSONArray("content").getObj(0);
        String name = content.getStr("name");
        Long parentId = content.getLong("parentId");
        IService iService;
        BaseEntity entity;
        if (StrUtil.endWith(name, "转运中心")) {
            // 一级转运中心
            iService = OrganServiceFactory.getBean(OrganTypeEnum.OLT.getCode());
            entity = new OLTEntity();
            entity.setParentId(0L);
        } else if (StrUtil.endWith(name, "分拣中心")) {
            // 二级转运中心
            iService = OrganServiceFactory.getBean(OrganTypeEnum.TLT.getCode());
            entity = new TLTEntity();
            entity.setParentId(parentId);
        } else if (StrUtil.endWith(name, "营业部")) {
            // 网点
            iService = OrganServiceFactory.getBean(OrganTypeEnum.AGENCY.getCode());
            entity = new AgencyEntity();
            entity.setParentId(parentId);
        } else {
            return;
        }
        // 设置参数
        entity.setBid(content.getLong("id"));
        entity.setName(name);
        entity.setStatus(content.getBool("status"));
        switch (operation) {
            case "ADD": {
                iService.create(entity);
                break;
            }
            case "UPDATE": {
                iService.update(entity);
                break;
            }
            case "DEL": {
                iService.deleteByBid(entity.getBid());
                break;
            }
        }
    }
}

3.3 ISerivce

在 Service 中一些方法是通用的,比如新增、更新、删除等,这个通用的方法可以写到一个 Service 中,其他的 Service 继承该 Service 即可。

IService

package com.sl.transport.service;

import com.sl.transport.entity.node.BaseEntity;

/**
 * 基础服务实现
 */
public interface IService<T extends BaseEntity> {

    /**
     * 根据业务id查询数据
     *
     * @param bid 业务id
     * @return 节点数据
     */
    T queryByBid(Long bid);

    /**
     * 新增节点
     *
     * @param t 节点数据
     * @return 新增的节点数据
     */
    T create(T t);

    /**
     * 更新节点
     *
     * @param t 节点数据
     * @return 更新的节点数据
     */
    T update(T t);

    /**
     * 根据业务id删除数据
     *
     * @param bid 业务id
     * @return 是否删除成功
     */
    Boolean deleteByBid(Long bid);

}

ServiceImpl

package com.sl.transport.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import com.sl.transport.common.util.ObjectUtil;
import com.sl.transport.entity.node.BaseEntity;
import com.sl.transport.repository.BaseRepository;
import com.sl.transport.service.IService;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * 基础服务的实现
 */
public class ServiceImpl<R extends BaseRepository, T extends BaseEntity> implements IService<T> {

    @Autowired
    private R repository;

    @Override
    public T queryByBid(Long bid) {
        return (T) this.repository.findByBid(bid).orElse(null);
    }

    @Override
    public T create(T t) {
        // id 由 Neo4j 自动生成
        t.setId(null);
        return (T) this.repository.save(t);
    }

    @Override
    public T update(T t) {
        // 先查询,再更新
        T tData = this.queryByBid(t.getBid());
        if (ObjectUtil.isEmpty(tData)) {
            return null;
        }
        BeanUtil.copyProperties(t, tData, 
					CopyOptions.create().ignoreNullValue().setIgnoreProperties("id", "bid"));
        return (T) this.repository.save(tData);
    }

    @Override
    public Boolean deleteByBid(Long bid) {
        return this.repository.deleteByBid(bid) > 0;
    }
}

(1)AgencyServiceImpl

package com.sl.transport.service.impl;

import com.sl.transport.entity.node.AgencyEntity;
import com.sl.transport.repository.AgencyRepository;
import com.sl.transport.service.AgencyService;
import org.springframework.stereotype.Service;

@Service
public class AgencyServiceImpl extends ServiceImpl<AgencyRepository, AgencyEntity>  implements AgencyService {}

(2)OLTServiceImpl

package com.sl.transport.service.impl;

import com.sl.transport.entity.node.OLTEntity;
import com.sl.transport.repository.OLTRepository;
import com.sl.transport.service.OLTService;
import org.springframework.stereotype.Service;

@Service
public class OLTServiceImpl extends ServiceImpl<OLTRepository, OLTEntity> implements OLTService {}

(3)TLTServiceImpl

package com.sl.transport.service.impl;

import com.sl.transport.entity.node.TLTEntity;
import com.sl.transport.repository.TLTRepository;
import com.sl.transport.service.TLTService;
import org.springframework.stereotype.Service;

@Service
public class TLTServiceImpl extends ServiceImpl<TLTRepository, TLTEntity> implements TLTService {}

3.4 机构管理

按照业务系统的需求,会通过 bid 查询机构,无需指定 type,也就是说,我们需要将网点和转运中心都看作是机构,需要实现两个查询方法:

  • 根据 bid 查询
  • 查询机构列表

a. 接口定义

package com.sl.transport.service;

import com.sl.transport.domain.OrganDTO;
import java.util.List;

/**
 * 机构业务操作
 */
public interface OrganService {
    
    /**
     * 无需指定type,根据id查询
     *
     * @param bid
     * @return
     */
    OrganDTO findByBid(Long bid);
    
    /**
     * 查询所有的机构,如果name不为空的按照name模糊查询
     *
     * @param name 机构名称
     * @return 机构列表
     */
    List<OrganDTO> findAll(String name);
    
}

b. 具体实现

OrganServiceImpl

package com.sl.transport.service.impl;

import cn.hutool.core.util.ObjectUtil;
import com.sl.transport.common.exception.SLException;
import com.sl.transport.domain.OrganDTO;
import com.sl.transport.enums.ExceptionEnum;
import com.sl.transport.repository.OrganRepository;
import com.sl.transport.service.OrganService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;

@Service
public class OrganServiceImpl implements OrganService {
    
    @Resource
    private OrganRepository organRepository;
    
    @Override
    public OrganDTO findByBid(Long bid) {
        OrganDTO organDTO = this.organRepository.findByBid(bid);
        if (ObjectUtil.isNotEmpty(organDTO)) {
            return organDTO;
        }
        throw new SLException(ExceptionEnum.ORGAN_NOT_FOUND);
    }
    
    @Override
    public List<OrganDTO> findAll(String name) {
        return this.organRepository.findAll(name);
    }
}

OrganRepositoryImpl

package com.sl.transport.repository.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.sl.transport.domain.OrganDTO;
import com.sl.transport.enums.OrganTypeEnum;
import com.sl.transport.repository.OrganRepository;
import org.neo4j.driver.internal.InternalPoint2D;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;

@Component
public class OrganRepositoryImpl implements OrganRepository {
    
    @Resource
    private Neo4jClient neo4jClient;
    
    @Override
    public OrganDTO findByBid(Long bid) {
        String cypherQuery = StrUtil.format("MATCH (n)\n" +
                "WHERE n.bid = {}\n" +
                "RETURN n", bid);
        return CollUtil.getFirst(executeQuery(cypherQuery));
    }
    
    @Override
    public List<OrganDTO> findAll(String name) {
        name = StrUtil.removeAll(name, '\'', '"');
        String cypherQuery = StrUtil.isEmpty(name) ?
                "MATCH (n) RETURN n" :
                StrUtil.format("MATCH (n) WHERE n.name CONTAINS '{}' RETURN n", name);
        return executeQuery(cypherQuery);
    }
    
    private List<OrganDTO> executeQuery(String cypherQuery) {
        return ListUtil.toList(this.neo4jClient.query(cypherQuery)
                // 设置响应的类型
                .fetchAs(OrganDTO.class)
                // 对结果进行封装处理
                .mappedBy((typeSystem, record) -> {
                    Map<String, Object> map = record.get("n").asMap();
                    OrganDTO organDTO = BeanUtil.toBean(map, OrganDTO.class);
                    InternalPoint2D location = (InternalPoint2D) map.get("location");
                    if (ObjectUtil.isNotEmpty(location)) {
                        organDTO.setLongitude(location.x());
                        organDTO.setLatitude(location.y());
                    }
                    // 获取类型
                    String type = CollUtil.getFirst(record.get("n").asNode().labels());
                    organDTO.setType(OrganTypeEnum.valueOf(type).getCode());
                    return organDTO;
                }).all());
    }
}

3.5 路线管理

路线管理是在路线规划中核心的功能,用户在下单时、订单转运单时会进行调用路线规划,后台系统对路线进行维护管理。路线类型如下:

类型 说明
干线 一级转运中心到一级转运中心
支线 一级转运中心与二级转运中心之间线路
接驳路线 二级转运中心到网点
专线(暂时不支持) 任务城市到任意城市
临时线路(暂时不支持) 任意转运中心到任意转运中心

新增路线业务规则:

  1. 干线:起点终点无顺序
  2. 支线:起点必须是二级转运中心
  3. 接驳路线:起点必须是网点

a. 业务流程

b. Repository

接口定义:

package com.sl.transport.repository;

import com.sl.transport.common.util.PageResponse;
import com.sl.transport.domain.TransportLineNodeDTO;
import com.sl.transport.domain.TransportLineSearchDTO;
import com.sl.transport.entity.line.TransportLine;
import com.sl.transport.entity.node.AgencyEntity;
import com.sl.transport.entity.node.BaseEntity;

import java.util.List;

/**
 * 运输路线查询
 */
public interface TransportLineRepository {

    /**
     * 查询两个网点之间最短的路线,查询深度为:10
     *
     * @param start 开始网点
     * @param end   结束网点
     * @return 路线
     */
    TransportLineNodeDTO findShortestPath(AgencyEntity start, AgencyEntity end);

    /**
     * 查询两个网点之间最短的路线,最大查询深度为:10
     *
     * @param start 开始网点
     * @param end   结束网点
     * @param depth 查询深度,最大为:10
     * @return 路线
     */
    TransportLineNodeDTO findShortestPath(AgencyEntity start, AgencyEntity end, int depth);

    /**
     * 查询两个网点之间的路线列表,成本优先 > 转运节点优先
     *
     * @param start 开始网点
     * @param end   结束网点
     * @param depth 查询深度
     * @param limit 返回路线的数量
     * @return 路线
     */
    List<TransportLineNodeDTO> findPathList(AgencyEntity start, AgencyEntity end, int depth, int limit);

    /**
     * 查询数据节点之间的关系数量
     *
     * @param firstNode  第一个节点
     * @param secondNode 第二个节点
     * @return 数量
     */
    Long queryCount(BaseEntity firstNode, BaseEntity secondNode);

    /**
     * 新增路线
     *
     * @param firstNode     第一个节点
     * @param secondNode    第二个节点
     * @param transportLine 路线数据
     * @return 新增关系的数量
     */
    Long create(BaseEntity firstNode, BaseEntity secondNode, TransportLine transportLine);

    /**
     * 更新路线
     *
     * @param transportLine 路线数据
     * @return 更新的数量
     */
    Long update(TransportLine transportLine);

    /**
     * 删除路线
     *
     * @param lineId 关系id
     * @return 删除关系的数量
     */
    Long remove(Long lineId);

    /**
     * 分页查询路线
     *
     * @param transportLineSearchDTO 搜索参数
     * @return 路线列表
     */
    PageResponse<TransportLine> queryPageList(TransportLineSearchDTO transportLineSearchDTO);


    /**
     * 根据ids批量查询路线
     *
     * @param ids id列表
     * @return 路线列表
     */
    List<TransportLine> queryByIds(Long... ids);

    /**
     * 根据id查询路线
     *
     * @param id 路线id
     * @return 路线数据
     */
    TransportLine queryById(Long id);
}

接口实现:

package com.sl.transport.repository.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.PageUtil;
import cn.hutool.core.util.StrUtil;
import com.sl.transport.common.util.PageResponse;
import com.sl.transport.domain.TransportLineNodeDTO;
import com.sl.transport.domain.TransportLineSearchDTO;
import com.sl.transport.entity.line.TransportLine;
import com.sl.transport.entity.node.AgencyEntity;
import com.sl.transport.entity.node.BaseEntity;
import com.sl.transport.repository.TransportLineRepository;
import com.sl.transport.utils.TransportLineUtils;
import org.neo4j.driver.Record;
import org.neo4j.driver.internal.value.PathValue;
import org.neo4j.driver.types.Relationship;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
 * 对于路线的各种操作
 */
@Component
public class TransportLineRepositoryImpl implements TransportLineRepository {

    @Resource
    private Neo4jClient neo4jClient;

    @Override
    public TransportLineNodeDTO findShortestPath(AgencyEntity start, AgencyEntity end) {
        return findShortestPath(start, end, 10);
    }

    @Override
    public TransportLineNodeDTO findShortestPath(AgencyEntity start, AgencyEntity end, int depth) {

        String type = AgencyEntity.class.getAnnotation(Node.class).value()[0];

        String cypherQuery = StrUtil.format(
                "MATCH path = shortestPath((start:{}) -[*..{}]-> (end:{})) " +
                        "WHERE start.bid = $startId AND end.bid = $endId AND start.status = true AND end.status = true " +
                        "RETURN path", type, depth, type);
        Collection<TransportLineNodeDTO> transportLineNodeDTOS = this.executeQueryPath(cypherQuery, start, end);
        if (CollUtil.isEmpty(transportLineNodeDTOS)) {
            return null;
        }
        for (TransportLineNodeDTO transportLineNodeDTO : transportLineNodeDTOS) {
            return transportLineNodeDTO;
        }
        return null;
    }

    private List<TransportLineNodeDTO> executeQueryPath(String cypherQuery, AgencyEntity start, AgencyEntity end) {
        return ListUtil.toList(this.neo4jClient.query(cypherQuery)
                // 设置参数
                .bind(start.getBid()).to("startId")
                // 设置参数
                .bind(end.getBid()).to("endId")
                // 设置响应的类型
                .fetchAs(TransportLineNodeDTO.class)
                // 对结果进行封装处理
                .mappedBy((typeSystem, record) -> {
                    PathValue pathValue = (PathValue) record.get(0);
                    return TransportLineUtils.convert(pathValue);
                }).all());
    }

    @Override
    public List<TransportLineNodeDTO> findPathList(AgencyEntity start, AgencyEntity end, int depth, int limit) {
        // 获取网点数据在Neo4j中的类型
        String type = AgencyEntity.class.getAnnotation(Node.class).value()[0];
        // 查找成本最低的路线
        String cypherQuery = StrUtil.format(
                "MATCH path = (start:{}) -[*..{}]-> (end:{}) " +
                        "WHERE start.bid = $startId AND end.bid = $endId AND start.status = true AND end.status = true " +
                        "UNWIND relationships(path) AS r " +
                        "WITH sum(r.cost) AS cost, path " +
                        "RETURN path ORDER BY cost ASC, LENGTH(path) ASC LIMIT {}", type, depth, type, limit);
        return this.executeQueryPath(cypherQuery, start, end);
    }

    @Override
    public Long queryCount(BaseEntity firstNode, BaseEntity secondNode) {
        String firstNodeType = firstNode.getClass().getAnnotation(Node.class).value()[0];
        String secondNodeType = secondNode.getClass().getAnnotation(Node.class).value()[0];
        // 查找起点到终点有多少关系 => 有无路线
        String cypherQuery = StrUtil.format(
                "MATCH (m:{}) -[r]- (n:{}) " +
                        "WHERE m.bid = $firstBid AND n.bid = $secondBid " +
                        "RETURN count(r) AS c", firstNodeType, secondNodeType);
        Optional<Long> optional = this.neo4jClient.query(cypherQuery)
                .bind(firstNode.getBid()).to("firstBid")
                .bind(secondNode.getBid()).to("secondBid")
                .fetchAs(Long.class)
                .mappedBy((typeSystem, record) -> Convert.toLong(record.get("c")))
                .one();
        return optional.orElse(0L);
    }

    @Override
    public Long create(BaseEntity firstNode, BaseEntity secondNode, TransportLine transportLine) {
        String firstNodeType = firstNode.getClass().getAnnotation(Node.class).value()[0];
        String secondNodeType = secondNode.getClass().getAnnotation(Node.class).value()[0];
        // 创建两个节点的关系
        String cypherQuery = StrUtil.format(
                "MATCH (m:{} {bid : $firstBid}) " +
                        "WITH m " +
                        "MATCH (n:{} {bid : $secondBid}) " +
                        "WITH m,n " +
                        "CREATE " +
                        " (m) -[r:IN_LINE {cost:$cost, number:$number, type:$type, name:$name, distance:$distance, time:$time, extra:$extra, startOrganId:$startOrganId, endOrganId:$endOrganId,created:$created, updated:$updated}]-> (n), " +
                        " (m) <-[:OUT_LINE {cost:$cost, number:$number, type:$type, name:$name, distance:$distance, time:$time, extra:$extra, startOrganId:$endOrganId, endOrganId:$startOrganId, created:$created, updated:$updated}]- (n) " +
                        "RETURN count(r) AS c", firstNodeType, secondNodeType);
        Optional<Long> optional = this.neo4jClient.query(cypherQuery)
                .bindAll(BeanUtil.beanToMap(transportLine))
                .bind(firstNode.getBid()).to("firstBid")
                .bind(secondNode.getBid()).to("secondBid")
                .fetchAs(Long.class)
                .mappedBy((typeSystem, record) -> Convert.toLong(record.get("c")))
                .one();
        return optional.orElse(0L);
    }

    @Override
    public Long update(TransportLine transportLine) {
        // 更新关系属性
        String cypherQuery = "MATCH () -[r]-> () "
                + "WHERE id(r) = $id "
                + "SET r.cost=$cost, r.number=$number, r.name=$name, r.distance=$distance, r.time=$time, "
                    + "r.startOrganId=$startOrganId, r.endOrganId=$endOrganId, r.updated=$updated, r.extra=$extra "
                + "RETURN count(r) AS c";
        Optional<Long> optional = this.neo4jClient.query(cypherQuery)
                .bindAll(BeanUtil.beanToMap(transportLine))
                .fetchAs(Long.class)
                .mappedBy((typeSystem, record) -> Convert.toLong(record.get("c")))
                .one();
        return optional.orElse(0L);
    }

    @Override
    public Long remove(Long lineId) {
        // 删除关系
        String cypherQuery = "MATCH () -[r]-> () " +
                "WHERE id(r) = $lineId " +
                "DETACH DELETE r " +
                "RETURN count(r) AS c";
        Optional<Long> optional = this.neo4jClient.query(cypherQuery)
                .bind(lineId).to("lineId")
                .fetchAs(Long.class)
                .mappedBy((typeSystem, record) -> Convert.toLong(record.get("c")))
                .one();
        return optional.orElse(0L);
    }

    @Override
    public PageResponse<TransportLine> queryPageList(TransportLineSearchDTO transportLineSearchDTO) {
        int page = Math.max(transportLineSearchDTO.getPage(), 1);
        int pageSize = transportLineSearchDTO.getPageSize();
        int skip = (page - 1) * pageSize;
        Map<String, Object> searchParam = BeanUtil.beanToMap(transportLineSearchDTO, false, true);
        MapUtil.removeAny(searchParam, "page", "pageSize");
        //构建查询语句,第一个是查询数据,第二个是查询数量
        String[] cyphers = this.buildPageQueryCypher(searchParam);
        String cypherQuery = cyphers[0];
        // 数据
        List<TransportLine> list = ListUtil.toList(this.neo4jClient.query(cypherQuery)
                .bind(skip).to("skip")
                .bind(pageSize).to("limit")
                .bindAll(searchParam)
                .fetchAs(TransportLine.class)
                .mappedBy((typeSystem, record) -> {
                    // 封装数据
                    return this.toTransportLine(record);
                }).all());
        // 数据总数
        String countCypher = cyphers[1];
        Long total = this.neo4jClient.query(countCypher)
                .bindAll(searchParam)
                .fetchAs(Long.class)
                .mappedBy((typeSystem, record) -> Convert.toLong(record.get("c")))
                .one().orElse(0L);
        PageResponse<TransportLine> pageResponse = new PageResponse<>();
        pageResponse.setPage(page);
        pageResponse.setPageSize(pageSize);
        pageResponse.setItems(list);
        pageResponse.setCounts(total);
        Long pages = Convert.toLong(PageUtil.totalPage(Convert.toInt(total), pageSize));
        pageResponse.setPages(pages);
        return pageResponse;
    }

    private String[] buildPageQueryCypher(Map<String, Object> searchParam) {
        String queryCypher;
        String countCypher;
        if (CollUtil.isEmpty(searchParam)) {
            // 无参数
            queryCypher = "MATCH (m) -[r]-> (n) RETURN m,r,n ORDER BY id(r) DESC SKIP $skip LIMIT $limit";
            countCypher = "MATCH () -[r]-> () RETURN count(r) AS c";
        } else {
            // 有参数
            String cypherPrefix = "MATCH (m) -[r]-> (n)";
            StringBuilder sb = new StringBuilder();
            sb.append(cypherPrefix + " WHERE 1=1 ");
            for (String key : searchParam.keySet()) {
                Object value = searchParam.get(key);
                if (value instanceof String) {
                    if (StrUtil.isNotBlank(Convert.toStr(value))) {
                        sb.append(StrUtil.format("AND r.{} CONTAINS ${} ", key, key));
                    }
                } else {
                    sb.append(StrUtil.format("AND r.{} = ${} ", key, key));
                }
            }
            String cypher = sb.toString();
            queryCypher = cypher + "RETURN m,r,n ORDER BY id(r) DESC SKIP $skip LIMIT $limit";
            countCypher = cypher + "RETURN count(r) AS c";
        }
        return new String[]{queryCypher, countCypher};
    }

    @Override
    public List<TransportLine> queryByIds(Long... ids) {
        String cypherQuery = "MATCH (m) -[r]-> (n) " +
                "WHERE id(r) in $ids " +
                "RETURN m,r,n";
        return ListUtil.toList(this.neo4jClient.query(cypherQuery)
                .bind(ids).to("ids")
                .fetchAs(TransportLine.class)
                .mappedBy((typeSystem, record) -> {
                    // 封装数据
                    return this.toTransportLine(record);
                }).all());
    }

    private TransportLine toTransportLine(Record record) {
        org.neo4j.driver.types.Node startNode = record.get("m").asNode();
        org.neo4j.driver.types.Node endNode = record.get("n").asNode();
        Relationship relationship = record.get("r").asRelationship();
        Map<String, Object> map = relationship.asMap();
        TransportLine transportLine = BeanUtil.toBeanIgnoreError(map, TransportLine.class);
        transportLine.setStartOrganName(startNode.get("name").asString());
        transportLine.setStartOrganId(startNode.get("bid").asLong());
        transportLine.setEndOrganName(endNode.get("name").asString());
        transportLine.setEndOrganId(endNode.get("bid").asLong());
        transportLine.setId(relationship.id());
        return transportLine;
    }

    @Override
    public TransportLine queryById(Long id) {
        List<TransportLine> transportLines = this.queryByIds(id);
        if (CollUtil.isNotEmpty(transportLines)) {
            return transportLines.get(0);
        }
        return null;
    }
}

c. Service

接口定义:

package com.sl.transport.service;

import com.sl.transport.common.util.PageResponse;
import com.sl.transport.domain.TransportLineNodeDTO;
import com.sl.transport.domain.TransportLineSearchDTO;
import com.sl.transport.entity.line.TransportLine;

import java.util.List;

/**
 * 计算路线相关业务
 */
public interface TransportLineService {

    /**
     * 新增路线
     *
     * @param transportLine 路线数据
     * @return 是否成功
     */
    Boolean createLine(TransportLine transportLine);

    /**
     * 更新路线
     *
     * @param transportLine 路线数据
     * @return 是否成功
     */
    Boolean updateLine(TransportLine transportLine);

    /**
     * 删除路线
     *
     * @param id 路线id
     * @return 是否成功
     */
    Boolean deleteLine(Long id);

    /**
     * 分页查询路线
     *
     * @param transportLineSearchDTO 搜索参数
     * @return 路线列表
     */
    PageResponse<TransportLine> queryPageList(TransportLineSearchDTO transportLineSearchDTO);

    /**
     * 查询两个网点之间最短的路线,最大查询深度为:10
     *
     * @param startId 开始网点id
     * @param endId   结束网点id
     * @return 路线
     */
    TransportLineNodeDTO queryShortestPath(Long startId, Long endId);

    /**
     * 查询两个网点之间成本最低的路线,最大查询深度为:10
     *
     * @param startId 开始网点id
     * @param endId   结束网点id
     * @return 路线集合
     */
    TransportLineNodeDTO findLowestPath(Long startId, Long endId);

    /**
     * 根据调度策略查询路线
     *
     * @param startId 开始网点id
     * @param endId   结束网点id
     * @return 路线
     */
    TransportLineNodeDTO queryPathByDispatchMethod(Long startId, Long endId);

    /**
     * 根据ids批量查询路线
     *
     * @param ids id列表
     * @return 路线列表
     */
    List<TransportLine> queryByIds(Long... ids);

    /**
     * 根据id查询路线
     *
     * @param id 路线id
     * @return 路线数据
     */
    TransportLine queryById(Long id);

}

接口实现:

package com.sl.transport.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.itheima.em.sdk.EagleMapTemplate;
import com.itheima.em.sdk.enums.ProviderEnum;
import com.itheima.em.sdk.vo.Coordinate;
import com.sl.transport.common.enums.DispatchMethodEnum;
import com.sl.transport.common.exception.SLException;
import com.sl.transport.common.util.PageResponse;
import com.sl.transport.domain.DispatchConfigurationDTO;
import com.sl.transport.domain.OrganDTO;
import com.sl.transport.domain.TransportLineNodeDTO;
import com.sl.transport.domain.TransportLineSearchDTO;
import com.sl.transport.entity.line.TransportLine;
import com.sl.transport.entity.node.AgencyEntity;
import com.sl.transport.entity.node.BaseEntity;
import com.sl.transport.entity.node.OLTEntity;
import com.sl.transport.entity.node.TLTEntity;
import com.sl.transport.enums.ExceptionEnum;
import com.sl.transport.enums.TransportLineCostEnum;
import com.sl.transport.enums.TransportLineEnum;
import com.sl.transport.repository.TransportLineRepository;
import com.sl.transport.service.DispatchConfigurationService;
import com.sl.transport.service.OrganService;
import com.sl.transport.service.TransportLineService;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;
import java.util.Map;

/**
 * 路线相关业务
 */
@Service
public class TransportLineServiceImpl implements TransportLineService {

    @Resource
    private TransportLineRepository transportLineRepository;
    @Resource
    private EagleMapTemplate eagleMapTemplate;
    @Resource
    private OrganService organService;
    @Resource
    private DispatchConfigurationService dispatchConfigurationService;


    /**
     * 新增路线业务规则:干线:起点终点无顺序,支线:起点必须是二级转运中心,接驳路线:起点必须是网点
     * 路线类型枚举: {@link TransportLineEnum}
     *
     * @param transportLine 路线数据
     * @return
     */
    @Override
    public Boolean createLine(TransportLine transportLine) {
        // 校验路线类型不能为空 tips: 根据type获取路线枚举在判断哦
        TransportLineEnum transportLineEnum = TransportLineEnum.codeOf(transportLine.getType());
        if (ObjectUtil.isEmpty(transportLineEnum)) {
            throw new SLException(ExceptionEnum.TRANSPORT_LINE_TYPE_ERROR);
        }
        // 定义 出发路线实体 到达路线实体 类型:BaseEntity
        BaseEntity firstNode, secondNode;
        // 判断路线类型枚举,根据不同类型 baseEntity实现类不同
        switch (transportLineEnum) {
            case TRUNK_LINE:
                //      如果是 干线,  则出发路线实体 到达路线实体 都为 OLTEntity
                firstNode = OLTEntity.builder().bid(transportLine.getStartOrganId()).build();
                secondNode = OLTEntity.builder().bid(transportLine.getEndOrganId()).build();
                break;
            case BRANCH_LINE:
                //      如果是 支线,  则出发路线实体为二级分拣中心TLTEntity  到达路线实体为一级转运中心OLTEntity
                firstNode = TLTEntity.builder().bid(transportLine.getStartOrganId()).build();
                secondNode = OLTEntity.builder().bid(transportLine.getEndOrganId()).build();
                break;
            case CONNECT_LINE:
                //      如果是 接驳路线  则出发路线实体为三级营业部AgencyEntity 到达路线实体为二级分拣中心TLTEntity
                firstNode = AgencyEntity.builder().bid(transportLine.getStartOrganId()).build();
                secondNode = TLTEntity.builder().bid(transportLine.getEndOrganId()).build();
                break;
            default:
                //      如果都不是 抛出路线类型错误异常
                throw new SLException(ExceptionEnum.TRANSPORT_LINE_TYPE_ERROR);
        }

        // 判断路线是否已经存在,存在抛以存在异常  tips: transportLineRepository根据两个节点查询count路线数量即可
        Long count = transportLineRepository.queryCount(firstNode, secondNode);
        if (count > 0) {
            throw new SLException(ExceptionEnum.TRANSPORT_LINE_ALREADY_EXISTS);
        }
        // 补全路线属性: id=null created updated当前时间
        transportLine.setId(null);
        transportLine.setCreated(System.currentTimeMillis());
        transportLine.setUpdated(System.currentTimeMillis());

        // 补充其它信息  tips: 调用infoFromMap补全
        infoFromMap(firstNode, secondNode, transportLine);

        // 调用创建路线方法 tips: transportLineRepository创建方法
        count = transportLineRepository.create(firstNode, secondNode, transportLine);

        return count > 0;
    }

    /**
     * 通过地图查询距离、时间,计算成本
     *
     * @param firstNode     开始节点
     * @param secondNode    结束节点
     * @param transportLine 路线对象
     */
    private void infoFromMap(BaseEntity firstNode, BaseEntity secondNode, TransportLine transportLine) {
        // 根据Bid查询 发起机构节点数据
        OrganDTO start = organService.findByBid(firstNode.getBid());
        // 校验: 不能为空, 经纬度不能为空  如果为空  抛异常提示请先完善机构信息
        if (ObjectUtil.hasNull(start, start.getLatitude(), start.getLongitude())) {
            throw new SLException("请先完善机构信息");
        }
        // 根据Bid查询 到达机构节点数据
        OrganDTO end = organService.findByBid(secondNode.getBid());
        // 校验: 不能为空, 经纬度不能为空  如果为空  抛异常提示请先完善机构信息
        if (ObjectUtil.hasNull(end, end.getLatitude(), end.getLongitude())) {
            throw new SLException("请先完善机构信息");
        }
        // 查询高德地图行驶路线方法
        //  - eagleMapTemplate.opsForDirection().driving() 行驶路线
        //  - Coordinate坐标点参数
        //  - 设置高德地图参数,默认是不返回预计耗时的,需要额外设置参数
        Map<String, Object> param = MapUtil.<String, Object>builder().put("show_fields", "cost").build();
        Coordinate startCoordinate = new Coordinate(start.getLongitude(), start.getLatitude());
        Coordinate endCoordinate = new Coordinate(end.getLongitude(), end.getLatitude());
        String driving = eagleMapTemplate.opsForDirection().driving(ProviderEnum.AMAP, startCoordinate, endCoordinate, param);
        // 得到驾驶路线信息,如果为空return, 不为空转JSON对象  tips: 使用JSONUtil工具类
        if (StrUtil.isEmpty(driving)) {
            return;
        }
        JSONObject jsonObj = JSONUtil.parseObj(driving);
        // 获取预计消耗时间,单位:秒 ,设置到路线中   tips: route.paths[0].cost.duration
        Long time = Convert.toLong(jsonObj.getByPath("route.paths[0].cost.duration"), -1L);
        transportLine.setTime(time);
        // 获取路线距离,单位:米, 设置到路线中  tips: route.paths[0].distance
        Double distance = Convert.toDouble(jsonObj.getByPath("route.paths[0].distance"), -1D);
        transportLine.setDistance(distance);
        // 获取路线成本,单位: 元  tips: route.taxi_cost
        TransportLineCostEnum transportLineCostEnum = TransportLineCostEnum.codeOf(transportLine.getType());
        double cost = NumberUtil.mul(NumberUtil.div(distance.doubleValue(), 1000D), Convert.toDouble(transportLineCostEnum.getValue()).doubleValue());
        transportLine.setCost(NumberUtil.round(cost, 2).doubleValue());
        // 说明: 这里按照高德地图的预计打车费用作为成本计算,同一标准在计算路线时是可行的,但是不能作为真实的成本进行利润计算
    }

    @Override
    public Boolean updateLine(TransportLine transportLine) {
        // 先根据路线ID查询路线,不存在抛出异常
        TransportLine transportLineData = this.queryById(transportLine.getId());
        if (null == transportLineData) {
            throw new SLException(ExceptionEnum.TRANSPORT_LINE_NOT_FOUND);
        }
        // 拷贝数据,忽略null值以及不能修改的字段
        BeanUtil.copyProperties(transportLine, transportLineData, 
     				CopyOptions.create().setIgnoreNullValue(true).setIgnoreProperties(
                    "type", "startOrganId", "startOrganName", "endOrganId", "endOrganName"));
        // 设置updated修改时间
        transportLineData.setUpdated(System.currentTimeMillis());
        // 修改路线
        Long count = transportLineRepository.update(transportLineData);
        return count > 0;
    }

    @Override
    public Boolean deleteLine(Long id) {
        return transportLineRepository.remove(id) > 0;
    }

    @Override
    public PageResponse<TransportLine> queryPageList(TransportLineSearchDTO transportLineSearchDTO) {
        return transportLineRepository.queryPageList(transportLineSearchDTO);
    }

    @Override
    public TransportLineNodeDTO queryShortestPath(Long startId, Long endId) {
        AgencyEntity start = AgencyEntity.builder().bid(startId).build();
        AgencyEntity end = AgencyEntity.builder().bid(endId).build();
        if (ObjectUtil.hasEmpty(start, end)) {
            throw new SLException(ExceptionEnum.START_END_ORGAN_NOT_FOUND);
        }
        return transportLineRepository.findShortestPath(start, end);
    }

    @Override
    public TransportLineNodeDTO findLowestPath(Long startId, Long endId) {
        AgencyEntity start = AgencyEntity.builder().bid(startId).build();
        AgencyEntity end = AgencyEntity.builder().bid(endId).build();
        if (ObjectUtil.hasEmpty(start, end)) {
            throw new SLException(ExceptionEnum.START_END_ORGAN_NOT_FOUND);
        }
        List<TransportLineNodeDTO> pathList = transportLineRepository.findPathList(start, end, 10, 1);
        if (CollUtil.isNotEmpty(pathList)) {
            return pathList.get(0);
        }
        return null;
    }

    /**
     * 根据调度策略查询路线
     *
     * @param startId 开始网点id
     * @param endId   结束网点id
     * @return 路线
     */
    @Override
    public TransportLineNodeDTO queryPathByDispatchMethod(Long startId, Long endId) {
        // 获取系统中调度方式配置
        DispatchConfigurationDTO configuration = this.dispatchConfigurationService.findConfiguration();
        int method = configuration.getDispatchMethod();
        // 调度方式,1-转运次数最少,2-成本最低
        if (ObjectUtil.equal(DispatchMethodEnum.SHORTEST_PATH.getCode(), method)) {
            return this.queryShortestPath(startId, endId);
        } else {
            return this.findLowestPath(startId, endId);
        }
    }

    @Override
    public List<TransportLine> queryByIds(Long... ids) {
        return this.transportLineRepository.queryByIds(ids);
    }

    @Override
    public TransportLine queryById(Long id) {
        return this.transportLineRepository.queryById(id);
    }
}

标签:return,name,04,com,路线,sl,import,规划,transport
From: https://www.cnblogs.com/liujiaqi1101/p/18142186

相关文章

  • 前端【小程序】04-小程序基础篇【分包加载】
    一、分包加载官方文档:https://developers.weixin.qq.com/miniprogram/dev/framework/subpackages.html​分包加载是优化小程序加载速度的一种手段。1.1为什么?​微信平台对小程序单个包的代码体积限制为2M,超过2M的情况下可以采用分包来解决即使小程序代码体积没......
  • 前端【小程序】04-小程序基础篇【生命周期】
    生命周期生命周期是一些名称固定自动执行的函数。 页面生命周期​onLoad 在页面加载完成时执行,只会执行1次,常用于获取地址参数和网络请求onShow 在页面处于可见状态时执行,常用于动态更新数据或状态onReady 在页面初次渲染完成时执行,只会执行1次,常用于节点操作或......
  • 动态规划、回溯、BFS、二分、滑动窗口总结
    动态规划动态规划的核心问题:重叠子问题,最优子结构,状态转移方程动态规划与记忆递归的区别:记忆递归为自顶而上的递归剪枝,动态规划为自底向上的循环迭代;正确的状态转移方程+dp[]数组:确定状态(原问题和子问题中变化的变量)->确定dp数组的定义dp[i]->确定当前状态的'选择'并确定......
  • 2024-04-17:用go语言,欢迎各位勇者莅临力扣城,本次的挑战游戏名为「力扣泡泡龙」。 游戏
    2024-04-17:用go语言,欢迎各位勇者莅临力扣城,本次的挑战游戏名为「力扣泡泡龙」。游戏的起点是一颗形状如二叉树的泡泡树,其中每个节点的值代表该泡泡的分值。勇者们有一次机会可以击破一个节点泡泡,但需要满足以下规则:被击破的节点泡泡最多只能有一个子节点泡泡。如果被击破的节点......
  • 【2024-04-16】辩证压力
    20:00逆境不仅仅让我们知道谁是酒肉朋友,谁是可以患难与共的好友,还会强化人际关系,让人们打开心扉。                                                 ——乔纳森·海特......
  • 【笔记】RedmiBookPro15锐龙板(7840hs)安装ubuntu2204注意事项
    /** 2024-04-17 12:53:52*/1、不要安装ubuntu2004,驱动问题很烦入,尤其是AMD的显卡驱动,不论哪个版本都不要打AMD的官方驱动,经常花屏,卡的完全不能操作,自带的开源驱动就行了,偶尔出现一两道花屏的,不影响使用,而且一会就消失了。如果经常出现在bios里调大显存试试,默认512估计不够,我......
  • 解决.Net6 部署到ubuntu22.04中使用DotNetCore.NPOI 导出报 Could not open display (
    在Ubuntu22环境下,出现"Couldnotopendisplay(X-Serverrequired.CheckyourDISPLAYenvironmentvariable)"错误可能是由于缺少X服务器或未正确配置DISPLAY环境变量导致的。以下是你可以尝试的解决方法:检查DISPLAY环境变量:确保DISPLAY环境变量已正确设置。使......
  • 3-04. 实现箱子储物空间的保存和数据交换
    实现箱子与背包数据交换修改SlotUI修改InventoryManager修改SlotUI实现箱子数据保存目标当场景切换之后,箱子里面的数据不能丢失修改InventoryManager修改Box修改InventoryManager修改Box修改DataCollection修改ItemManager修改Box修改It......
  • 2024年04月17日 凌晨3点的梦——我是一个么得感情的杀手
    接了一单杀手生意,我自己有一把枪,在空旷的田野上试抢瞄准射击。然后有人来了挡住了我的目标位置,我让他们让一让,不然我打不中我这单子做不成,钱要退回去还得赔违约金。他们让开了。然后我试了一下小手枪的威力,打在门上的玻璃,结果玻璃裂了,我说这威力还可以哎。然后玻璃里面就有血色......
  • 20240416打卡
    第八周第一天第二天第三天第四天第五天第六天第七天所花时间5h5h代码量(行)4690博客量(篇)11知识点了解完成了地铁查询系统完成团队开发演讲......