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 用于服务间的数据传输,会用到 OrganDTO
、TransportLineNodeDTO
。
/**
* 机构数据对象,网点、一级转运、二级转运都是看作是机构
* 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 路线管理
路线管理是在路线规划中核心的功能,用户在下单时、订单转运单时会进行调用路线规划,后台系统对路线进行维护管理。路线类型如下:
类型 | 说明 |
---|---|
干线 | 一级转运中心到一级转运中心 |
支线 | 一级转运中心与二级转运中心之间线路 |
接驳路线 | 二级转运中心到网点 |
专线(暂时不支持) | 任务城市到任意城市 |
临时线路(暂时不支持) | 任意转运中心到任意转运中心 |
新增路线业务规则:
- 干线:起点终点无顺序
- 支线:起点必须是二级转运中心
- 接驳路线:起点必须是网点
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