当前位置: 首页 > news >正文

物流项目第八期(线路规划之Neo4j的应用)

 本项目专栏:

物流项目_Auc23的博客-CSDN博客

关于Neo4j的可以看这一期:

Neo4j入门第二期(Spring Data Neo4j的使用)-CSDN博客

业务流程

 新增路线

在新增路线的业务中,需要注意以下几点:

  • 路线不能重复
  • 新增路线业务规则:干线:起点终点无顺序,支线:起点必须是二级转运中心,接驳路线:起点必须是网点
  • 起点、终点机构不能相同
  • 路线需要成对创建,即:往返路线
  • 路线需要设置成本、距离等信息
    @ApiOperation(value = "新增路线", notes = "新增路线,干线:起点终点无顺序,支线:起点必须是二级转运中心,接驳路线:起点必须是网点")@PostMappingpublic void createLine(@RequestBody TransportLineDTO transportLineDTO) {TransportLine transportLine = TransportLineUtils.toEntity(transportLineDTO);Boolean result = this.transportLineService.createLine(transportLine);if (!result) {throw new SLException("新增路线失败!", HttpStatus.INTERNAL_SERVER_ERROR.value());}}

    @Resourceprivate TransportLineRepository transportLineRepository;@Resourceprivate EagleMapTemplate eagleMapTemplate;@Resourceprivate OrganService organService;@Resourceprivate CostConfigurationService costConfigurationService;// 新增路线业务规则:干线:起点终点无顺序,支线:起点必须是二级转运中心,接驳路线:起点必须是网点@Overridepublic Boolean createLine(TransportLine transportLine) {TransportLineEnum transportLineEnum = TransportLineEnum.codeOf(transportLine.getType());if (null == transportLineEnum) {throw new SLException(ExceptionEnum.TRANSPORT_LINE_TYPE_ERROR);}if (ObjectUtil.equal(transportLine.getStartOrganId(), transportLine.getEndOrganId())) {//起点终点不能相同throw new SLException(ExceptionEnum.TRANSPORT_LINE_ORGAN_CANNOT_SAME);}BaseEntity firstNode;BaseEntity secondNode;switch (transportLineEnum) {case TRUNK_LINE: {// 干线firstNode = OLTEntity.builder().bid(transportLine.getStartOrganId()).build();secondNode = OLTEntity.builder().bid(transportLine.getEndOrganId()).build();break;}case BRANCH_LINE: {// 支线,起点必须是 二级转运中心firstNode = TLTEntity.builder().bid(transportLine.getStartOrganId()).build();secondNode = OLTEntity.builder().bid(transportLine.getEndOrganId()).build();break;}case CONNECT_LINE: {// 接驳路线,起点必须是 网点firstNode = AgencyEntity.builder().bid(transportLine.getStartOrganId()).build();secondNode = TLTEntity.builder().bid(transportLine.getEndOrganId()).build();break;}default: {throw new SLException(ExceptionEnum.TRANSPORT_LINE_TYPE_ERROR);}}if (ObjectUtil.hasEmpty(firstNode, secondNode)) {throw new SLException(ExceptionEnum.START_END_ORGAN_NOT_FOUND);}//判断路线是否已经存在Long count = this.transportLineRepository.queryCount(firstNode, secondNode);if (count > 0) {throw new SLException(ExceptionEnum.TRANSPORT_LINE_ALREADY_EXISTS);}transportLine.setId(null);transportLine.setCreated(System.currentTimeMillis());transportLine.setUpdated(transportLine.getCreated());//补充信息this.infoFromMap(firstNode, secondNode, transportLine);count = this.transportLineRepository.create(firstNode, secondNode, transportLine);return count > 0;}/*** 通过地图查询距离、时间,计算成本** @param firstNode     开始节点* @param secondNode    结束节点* @param transportLine 路线对象*/private void infoFromMap(BaseEntity firstNode, BaseEntity secondNode, TransportLine transportLine) {//查询节点数据OrganDTO startOrgan = this.organService.findByBid(firstNode.getBid());if (ObjectUtil.hasEmpty(startOrgan, startOrgan.getLongitude(), startOrgan.getLatitude())) {throw new SLException("请先完善机构信息");}OrganDTO endOrgan = this.organService.findByBid(secondNode.getBid());if (ObjectUtil.hasEmpty(endOrgan, endOrgan.getLongitude(), endOrgan.getLatitude())) {throw new SLException("请先完善机构信息");}//查询地图服务商Coordinate origin = new Coordinate(startOrgan.getLongitude(), startOrgan.getLatitude());Coordinate destination = new Coordinate(endOrgan.getLongitude(), endOrgan.getLatitude());//设置高德地图参数,默认是不返回预计耗时的,需要额外设置参数Map<String, Object> param = MapUtil.<String, Object>builder().put("show_fields", "cost").build();String driving = this.eagleMapTemplate.opsForDirection().driving(ProviderEnum.AMAP, origin, destination, param);if (StrUtil.isEmpty(driving)) {return;}JSONObject jsonObject = JSONUtil.parseObj(driving);//时间,单位:秒Long duration = Convert.toLong(jsonObject.getByPath("route.paths[0].cost.duration"), -1L);transportLine.setTime(duration);//距离,单位:米Double distance = Convert.toDouble(jsonObject.getByPath("route.paths[0].distance"), -1d);transportLine.setDistance(NumberUtil.round(distance, 0).doubleValue());// 总成本 = 每公里平均成本 * 距离(单位:米) / 1000Double cost = costConfigurationService.findCostByType(transportLine.getType());transportLine.setCost(NumberUtil.round(cost * distance / 1000, 2).doubleValue());}
@Overridepublic 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];//定义cypher语句,成对创建路线String cypherQuery = StrUtil.format("MATCH (m:{} {bid : $firstBid})\n" +"WITH m\n" + "MATCH (n:{} {bid : $secondBid})\n" +"WITH m,n\n" +"CREATE\n" +" (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),\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)\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")))//对return值的处理.one();//获取一个值return optional.orElse(0L);}@Overridepublic 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:{})\n" +"WHERE m.bid = $firstBid AND n.bid = $secondBid\n" +"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);}

查询路线列表

    @ApiOperation(value = "分页查询路线", notes = "分页查询路线,如果有条件就进行筛选查询")@PostMapping("page")public PageResponse<TransportLineDTO> queryPageList(@RequestBody TransportLineSearchDTO transportLineSearchDTO) {PageResponse<TransportLine> pageResponse = this.transportLineService.queryPageList(transportLineSearchDTO);PageResponse<TransportLineDTO> result = new PageResponse<>();BeanUtil.copyProperties(pageResponse, result, "items");result.setItems(TransportLineUtils.toDTOList(pageResponse.getItems()));return result;}
    @Overridepublic PageResponse<TransportLine> queryPageList(TransportLineSearchDTO transportLineSearchDTO) {return this.transportLineRepository.queryPageList(transportLineSearchDTO);}
@Override
public PageResponse<TransportLine> queryPageList(TransportLineSearchDTO transportLineSearchDTO) {// 确保页码至少为1int page = Math.max(transportLineSearchDTO.getPage(), 1);// 获取每页大小int pageSize = transportLineSearchDTO.getPageSize();// 计算跳过的记录数int skip = (page - 1) * pageSize;// 将查询参数对象转换为Map,忽略null值和空集合Map<String, Object> searchParam = BeanUtil.beanToMap(transportLineSearchDTO, false, true);// 从查询参数中移除分页相关的参数(page和pageSize)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") // 绑定跳过记录数到Cypher查询中的变量"skip".bind(pageSize).to("limit") // 绑定页面大小到Cypher查询中的变量"limit".bindAll(searchParam) // 绑定所有查询参数.fetchAs(TransportLine.class) // 设置返回的数据类型.mappedBy((typeSystem, record) -> { // 自定义映射逻辑// 封装数据return this.toTransportLine(record); // 转换Record为TransportLine对象}).all());// 查询总数据量String countCypher = cyphers[1];Long total = this.neo4jClient.query(countCypher) // 执行计数查询.bindAll(searchParam) // 绑定所有查询参数.fetchAs(Long.class) // 设置返回的数据类型为Long.mappedBy((typeSystem, record) -> Convert.toLong(record.get("c"))) // 转换Record为总数.one().orElse(0L); // 如果没有结果,则返回0// 创建分页响应对象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; // 返回分页响应对象
}/*** 根据查询参数构建分页查询的Cypher语句*/
private String[] buildPageQueryCypher(Map<String, Object> searchParam) {String queryCypher;String countCypher;if (CollUtil.isEmpty(searchParam)) {// 如果没有查询参数,使用默认的Cypher查询语句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 {// 如果有查询参数,构建动态Cypher查询语句String cypherPrefix = "MATCH (m) -[r]-> (n)";StringBuilder sb = new StringBuilder();sb.append(cypherPrefix).append(" WHERE 1=1 ");for (String key : searchParam.keySet()) {Object value = searchParam.get(key);if (value instanceof String) {if (StrUtil.isNotBlank(Convert.toStr(value))) {// 对于字符串类型的参数,使用CONTAINS进行模糊匹配sb.append(StrUtil.format("AND r.{} CONTAINS ${} \n", key, key));}} else {// 对于非字符串类型的参数,使用等值匹配sb.append(StrUtil.format("AND r.{} = ${} \n", 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}; // 返回查询语句和计数语句
}/*** 将Neo4j的Record转换为TransportLine对象*/
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(); // 将关系转换为Map// 使用BeanUtil将Map转换为TransportLine对象TransportLine transportLine = BeanUtil.toBeanIgnoreError(map, TransportLine.class);// 设置起始节点的名称和IDtransportLine.setStartOrganName(startNode.get("name").asString());transportLine.setStartOrganId(startNode.get("bid").asLong());// 设置终点节点的名称和IDtransportLine.setEndOrganName(endNode.get("name").asString());transportLine.setEndOrganId(endNode.get("bid").asLong());// 设置关系ID作为TransportLine的IDtransportLine.setId(relationship.id());return transportLine; // 返回封装好的TransportLine对象
}

线路成本

/*** 成本配置相关业务对外提供接口服务*/
@Api(tags = "成本配置")
@RequestMapping("cost-configuration")
@Validated
@RestController
public class CostConfigurationController {@Resourceprivate CostConfigurationService costConfigurationService;@ApiOperation(value = "查询成本配置")@GetMappingpublic List<CostConfigurationDTO> findConfiguration() {return costConfigurationService.findConfiguration();}@ApiOperation(value = "保存成本配置")@PostMappingpublic void saveConfiguration(@RequestBody List<CostConfigurationDTO> dto) {costConfigurationService.saveConfiguration(dto);}
}

/*** 成本配置相关业务*/
public interface CostConfigurationService {/*** 查询成本配置** @return 成本配置*/List<CostConfigurationDTO> findConfiguration();/*** 保存成本配置* @param dto 成本配置*/void saveConfiguration(List<CostConfigurationDTO> dto);/*** 查询成本根据类型* @param type 类型* @return 成本*/Double findCostByType(Integer type);
}
/*** 成本配置相关业务*/
@Service
public class CostConfigurationServiceImpl implements CostConfigurationService {/*** 成本配置 redis key*/private static final String SL_TRANSPORT_COST_REDIS_KEY = "SL_TRANSPORT_COST_CONFIGURATION";/*** 默认成本配置*/private static final Map<Object, Object> DEFAULT_COST = Map.of(TransportLineEnum.TRUNK_LINE.getCode(), 0.8,TransportLineEnum.BRANCH_LINE.getCode(), 1.2,TransportLineEnum.CONNECT_LINE.getCode(), 1.5);@Resourceprivate StringRedisTemplate stringRedisTemplate;/*** 查询成本配置** @return 成本配置*/@Overridepublic List<CostConfigurationDTO> findConfiguration() {Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries(SL_TRANSPORT_COST_REDIS_KEY);if (ObjectUtil.isEmpty(entries)) {// 使用默认值entries = DEFAULT_COST;}// 返回return entries.entrySet().stream().map(v -> new CostConfigurationDTO(Convert.toInt(v.getKey()), Convert.toDouble(v.getValue()))).collect(Collectors.toList());}/*** 保存成本配置** @param dto 成本配置*/@Overridepublic void saveConfiguration(List<CostConfigurationDTO> dto) {Map<Object, Object> map = dto.stream().collect(Collectors.toMap(v -> v.getTransportLineType().toString(), v -> v.getCost().toString()));stringRedisTemplate.opsForHash().putAll(SL_TRANSPORT_COST_REDIS_KEY, map);}/*** 查询成本根据类型** @param type 类型* @return 成本*/@Overridepublic Double findCostByType(Integer type) {if (ObjectUtil.isEmpty(type)) {throw new SLException(ExceptionEnum.TRANSPORT_LINE_TYPE_ERROR);}// 查询redisObject o = stringRedisTemplate.opsForHash().get(SL_TRANSPORT_COST_REDIS_KEY, type.toString());if (ObjectUtil.isNotEmpty(o)) {return Convert.toDouble(o);}// 返回默认值return Convert.toDouble(DEFAULT_COST.get(type));}
}

调度策略规划路线

    @ApiImplicitParams({@ApiImplicitParam(name = "startId", value = "开始网点业务id", required = true),@ApiImplicitParam(name = "endId", value = "结束网点业务id", required = true)})@ApiOperation(value = "根据调度策略查询路线", notes = "根据调度策略选择最短路线或者成本最低路线")@GetMapping("/dispatchMethod/{startId}/{endId}")public TransportLineNodeDTO queryPathByDispatchMethod(@NotNull(message = "startId不能为空") @PathVariable("startId") Long startId,@NotNull(message = "endId不能为空") @PathVariable("endId") Long endId) {return this.transportLineService.queryPathByDispatchMethod(startId, endId);}
    /*** 根据调度策略查询路线** @param startId 开始网点id* @param endId   结束网点id* @return 路线*/@Overridepublic 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);}}
/*** 调度服务相关业务*/
@Service
public class DispatchConfigurationServiceImpl implements DispatchConfigurationService {@Resourceprivate StringRedisTemplate stringRedisTemplate;/*** 调度时间配置*/static final String DISPATCH_TIME_REDIS_KEY = "DISPATCH_CONFIGURATION:TIME";/*** 调度方式配置*/static final String DISPATCH_METHOD_REDIS_KEY = "DISPATCH_CONFIGURATION:METHOD";@Overridepublic DispatchConfigurationDTO findConfiguration() {//调度时间配置String dispatchTime = stringRedisTemplate.opsForValue().get(DISPATCH_TIME_REDIS_KEY);//调度方式配置String dispatchMethod = stringRedisTemplate.opsForValue().get(DISPATCH_METHOD_REDIS_KEY);//组装响应结果return DispatchConfigurationDTO.builder()//如果查不到调度时间,默认值为2小时.dispatchTime(Integer.parseInt(ObjectUtil.defaultIfBlank(dispatchTime, "2")))//如果查不到调度方式,默认值为2成本最低.dispatchMethod(Integer.parseInt(ObjectUtil.defaultIfBlank(dispatchMethod, "2"))).build();}@Overridepublic void saveConfiguration(DispatchConfigurationDTO dto) {//调度时间配置stringRedisTemplate.opsForValue().set(DISPATCH_TIME_REDIS_KEY, String.valueOf(dto.getDispatchTime()));//调度方式配置stringRedisTemplate.opsForValue().set(DISPATCH_METHOD_REDIS_KEY, String.valueOf(dto.getDispatchMethod()));}
}

相关文章:

  • 在 Vue 2中使用 dhtmlxGantt 7.1.13组件,并解决使用时遇到的问题汇总.“dhtmlx-gantt“: “^7.1.13“,
  • 前端八股之HTML
  • Qt不同布局添加不同控件
  • 方正字库助力华为,赋能鸿蒙电脑打造全场景字体解决方案
  • python-正则表达式
  • npm run build后将打包文件夹生成zip压缩包
  • OpenHarmony平台驱动使用(四),GPIO
  • 覆盖索引详解:原理、优势与面试要点
  • Dense和Moe模型
  • 链表:数据结构的灵动舞者
  • 本地部署dify爬坑指南
  • Streamlit 项目知识点总结
  • 【NLP基础知识系列课程-Tokenizer的前世今生第三课】多模态世界中的 Tokenizer 策略
  • c++复习(类型准换+动态数组+类与对象)
  • 第一章 LVS 负载均衡群集核心概念与体系架构
  • Vue3进阶教程:1.初次了解vue
  • 论文阅读笔记——Step1X-Edit: A Practical Framework for General Image Editing
  • python学习day30
  • 《100天精通Python——基础篇 2025 第21天:多线程性能剖析与 GIL 深入解析》
  • java集成Swagger2
  • 网站建设与管理 ppt/网站提交收录入口链接
  • 十里河网站建设/51趣优化网络seo工程师教程
  • 扬州 网站建设/百度账号快速注册
  • 鸡西各个网站/站长统计网站
  • 网络培训心得体会总结简短/seo是对网站进行什么优化
  • 免费的app制作软件/上海关键词优化排名软件