sql/增量sql/xprt_account.sql
@@ -0,0 +1,9 @@ -- 用户收藏地址:显式区分“站点收藏”和“场站收藏” -- 1. item_type: 1=站点 2=场站 -- 2. station_id: 场站收藏时保留场站关系,避免后续查询再从 point_id 反推 alter table xp_app_user_collect_addr add column item_type tinyint not null default 1 comment '收藏对象类型 1=站点 2=场站' after latitude, add column station_id bigint null comment '场站id,站点收藏时为空' after point_id; create index idx_app_user_collect_addr_item_type on xp_app_user_collect_addr (item_type); create index idx_app_user_collect_addr_station_id on xp_app_user_collect_addr (station_id); xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/controller/XpAppUserCollectAddrController.java
@@ -95,7 +95,31 @@ */ @GetMapping("/logan/rt/v1/appUser/collectAddr/getPointIds") public R<List<Long>> getPointIdsByUserId(@RequestParam Long uid){ List<Long> ids = xpAppUserCollectAddrService.list(new LambdaQueryWrapper<XpAppUserCollectAddr>().eq(XpAppUserCollectAddr::getAppUserId, uid)).stream().map(XpAppUserCollectAddr::getPointId).collect(Collectors.toList()); List<Long> ids = xpAppUserCollectAddrService.list(new LambdaQueryWrapper<XpAppUserCollectAddr>() .eq(XpAppUserCollectAddr::getAppUserId, uid).eq(XpAppUserCollectAddr::getItemType,1) .eq(XpAppUserCollectAddr::getDeleted, 0)) .stream() .map(XpAppUserCollectAddr::getPointId) .distinct() .collect(Collectors.toList()); return R.ok(ids); } /** * 获取用户收藏的stationIds * 仅返回“场站收藏”记录,避免和站点收藏共用 point_id 产生歧义。 */ @GetMapping("/logan/rt/v1/appUser/collectAddr/getStationIds") public R<List<Long>> getStationIdsByUserId(@RequestParam Long uid){ List<Long> ids = xpAppUserCollectAddrService.list(new LambdaQueryWrapper<XpAppUserCollectAddr>() .eq(XpAppUserCollectAddr::getAppUserId, uid) .eq(XpAppUserCollectAddr::getDeleted, 0) .eq(XpAppUserCollectAddr::getItemType, 2)) .stream() .map(XpAppUserCollectAddr::getStationId) .filter(java.util.Objects::nonNull) .distinct() .collect(Collectors.toList()); return R.ok(ids); } xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/mapper/XpAppUserCollectAddrMapper.java
@@ -3,6 +3,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.xiaopeng.account.model.query.XpUserCollectAddrListQuery; import com.xiaopeng.account.model.vo.CollectAddrTargetVO; import com.xiaopeng.account.model.vo.XpAppUserCollectAddrVO; import com.xiaopeng.api.model.account.XpAppUserCollectAddr; import org.apache.ibatis.annotations.Param; @@ -18,4 +19,8 @@ */ public interface XpAppUserCollectAddrMapper extends BaseMapper<XpAppUserCollectAddr> { Page<XpAppUserCollectAddrVO> userCollectAddrPageList(@Param("page") Page<XpAppUserCollectAddrVO> page, @Param("query") XpUserCollectAddrListQuery query, @Param("uid") Long uid); CollectAddrTargetVO selectPointCollectTarget(@Param("pointId") Long pointId); CollectAddrTargetVO selectStationCollectTarget(@Param("stationId") Long stationId); } xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/model/dto/CollectAddrDTO.java
@@ -1,6 +1,5 @@ package com.xiaopeng.account.model.dto; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; @@ -11,19 +10,22 @@ @ApiModel("收藏地址/取消收藏参数") public class CollectAddrDTO { @ApiModelProperty("1=收藏 2=取消收藏") @NotNull(message = "类型不能为空") @ApiModelProperty("操作类型 1=收藏 2=取消收藏") @NotNull(message = "操作类型不能为空") private Integer type; @ApiModelProperty(value = "经度") @NotNull(message = "经度不能为空") @ApiModelProperty("收藏对象类型 1=站点 2=场站,不传时默认按站点处理") private Integer itemType; @ApiModelProperty("站点id。收藏站点时必传;收藏场站时可传代表点位id作为兜底") private Long pointId; @ApiModelProperty("场站id。收藏/取消收藏场站时传入") private Long stationId; @ApiModelProperty("经度。兼容旧参数,最终以服务端解析结果为准") private Double longitude; @ApiModelProperty(value = "纬度") @NotNull(message = "纬度不能为空") @ApiModelProperty("纬度。兼容旧参数,最终以服务端解析结果为准") private Double latitude; @ApiModelProperty(value = "站点id") private Long pointId; } xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/model/vo/XpAppUserCollectAddrVO.java
@@ -1,32 +1,34 @@ package com.xiaopeng.account.model.vo; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.util.List; @Data @ApiModel("用户收藏地址-列表Vo") @ApiModel("用户收藏地址列表") public class XpAppUserCollectAddrVO { @ApiModelProperty(value = "经度") @ApiModelProperty("收藏记录id") private Long id; @ApiModelProperty("收藏对象类型 1=站点 2=场站") private Integer itemType; @ApiModelProperty("经度") private Double longitude; @ApiModelProperty(value = "纬度") @ApiModelProperty("纬度") private Double latitude; @ApiModelProperty(value = "站点id") @ApiModelProperty("落库使用的点位id") private Long pointId; @ApiModelProperty("所属场站id,无场站时为空") private Long stationId; @ApiModelProperty(value = "站点名称") @ApiModelProperty("展示名称。站点收藏展示点位名,场站收藏展示场站名") private String pointName; @ApiModelProperty(value = "站点地址") @ApiModelProperty("展示地址。站点收藏展示点位地址,场站收藏展示代表点位地址") private String pointAddress; } xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/service/impl/XpAppUserCollectAddrServiceImpl.java
@@ -6,19 +6,13 @@ import com.xiaopeng.account.mapper.XpAppUserCollectAddrMapper; import com.xiaopeng.account.model.dto.CollectAddrDTO; import com.xiaopeng.account.model.query.XpUserCollectAddrListQuery; import com.xiaopeng.account.model.vo.CollectAddrTargetVO; import com.xiaopeng.account.model.vo.XpAppUserCollectAddrVO; import com.xiaopeng.account.service.IXpAppUserCollectAddrService; import com.xiaopeng.api.dto.map.XpPointExportExcelQuery; import com.xiaopeng.api.feignClient.operation.XpPointClient; import com.xiaopeng.api.model.account.XpAppUserCollectAddr; import com.xiaopeng.api.model.operation.XpPoint; import com.xiaopeng.common.core.domain.R; import com.xiaopeng.common.core.exception.ServiceException; import com.xiaopeng.common.core.web.page.PageInfo; import org.springframework.beans.factory.annotation.Autowired; import java.util.List; import java.util.stream.Collectors; import org.springframework.stereotype.Service; /** * <p> @@ -28,84 +22,117 @@ * @author ck * @since 2026-04-10 */ @Service public class XpAppUserCollectAddrServiceImpl extends ServiceImpl<XpAppUserCollectAddrMapper, XpAppUserCollectAddr> implements IXpAppUserCollectAddrService { @Autowired private XpPointClient xpPointClient; private static final int COLLECT_TYPE_ADD = 1; private static final int COLLECT_TYPE_CANCEL = 2; private static final int ITEM_TYPE_STATION = 2; private static final int COLLECT_LIMIT = 50; /** * 收藏地址 * * @param dto * @param uid * @return * 收藏地址。 * 收藏表显式保存 item_type / station_id: * 1. 站点收藏:item_type=1,point_id 保存站点自身。 * 2. 场站收藏:item_type=2,station_id 保存场站id,point_id 保存代表点位id。 * 这样取消收藏、列表回显和后续扩展都不需要再做隐式推断。 */ @Override public Boolean collectAddr(CollectAddrDTO dto, String uid) { if (dto.getType() == 1) { Long userId = Long.parseLong(uid); CollectAddrTargetVO target = resolveCollectTarget(dto); if (target == null || target.getPointId() == null || target.getItemType() == null) { throw new ServiceException("收藏目标不存在"); } LambdaQueryWrapper<XpAppUserCollectAddr> sameTargetWrapper = buildSameTargetWrapper(userId, target); if (COLLECT_TYPE_ADD == dto.getType()) { Long count = this.count(new LambdaQueryWrapper<XpAppUserCollectAddr>() .eq(XpAppUserCollectAddr::getAppUserId, Long.parseLong(uid)) .eq(XpAppUserCollectAddr::getAppUserId, userId) .eq(XpAppUserCollectAddr::getDeleted, 0)); if (count >= 50) { if (count >= COLLECT_LIMIT) { throw new ServiceException("收藏夹已满,请先删除旧地址"); } Long old = this.count(new LambdaQueryWrapper<XpAppUserCollectAddr>() .eq(XpAppUserCollectAddr::getAppUserId, Long.parseLong(uid)) .eq(XpAppUserCollectAddr::getDeleted, 0).eq(XpAppUserCollectAddr::getPointId, dto.getPointId())); if (old > 0) { if (this.count(sameTargetWrapper) > 0) { return true; } XpAppUserCollectAddr xpAppUserCollectAddr = new XpAppUserCollectAddr(); xpAppUserCollectAddr.setAppUserId(Long.parseLong(uid)); xpAppUserCollectAddr.setLatitude(dto.getLatitude()); xpAppUserCollectAddr.setLongitude(dto.getLongitude()); xpAppUserCollectAddr.setPointId(dto.getPointId()); return this.save(xpAppUserCollectAddr); } else { XpAppUserCollectAddr xpAppUserCollectAddr = this.getOne(new LambdaQueryWrapper<XpAppUserCollectAddr>() .eq(XpAppUserCollectAddr::getAppUserId, Long.parseLong(uid)) .eq(XpAppUserCollectAddr::getDeleted, 0).eq(XpAppUserCollectAddr::getPointId, dto.getPointId()).last("limit 1")); if (xpAppUserCollectAddr != null) { return this.removeById(xpAppUserCollectAddr.getId()); } XpAppUserCollectAddr collectAddr = new XpAppUserCollectAddr(); collectAddr.setAppUserId(userId); collectAddr.setItemType(target.getItemType()); collectAddr.setPointId(target.getPointId()); collectAddr.setStationId(target.getStationId()); collectAddr.setLongitude(target.getLongitude()); collectAddr.setLatitude(target.getLatitude()); return this.save(collectAddr); } return false; if (COLLECT_TYPE_CANCEL == dto.getType()) { XpAppUserCollectAddr collectAddr = this.getOne(sameTargetWrapper.last("limit 1")); if (collectAddr == null) { return true; } return this.removeById(collectAddr.getId()); } throw new ServiceException("不支持的操作类型"); } /** * 收藏地址列表 * * @param uid * @return * 收藏地址列表。 * 列表查询直接联表回显,场站/站点通过 item_type 直接区分。 */ @Override public PageInfo<XpAppUserCollectAddrVO> userCollectAddrList(XpUserCollectAddrListQuery query, String uid) { Page<XpAppUserCollectAddrVO> page = new Page<>(query.getPageNum(), query.getPageSize()); Page<XpAppUserCollectAddrVO> pageData = this.baseMapper.userCollectAddrPageList(page, query, Long.parseLong(uid)); if(!pageData.getRecords().isEmpty()){ List<Long> pointIds = pageData.getRecords().stream().map(XpAppUserCollectAddrVO::getPointId).collect(Collectors.toList()); XpPointExportExcelQuery xpPointExportExcelQuery = new XpPointExportExcelQuery(); xpPointExportExcelQuery.setPointIds(pointIds); R<List<XpPoint>> pointClientByIds = xpPointClient.getByIds(xpPointExportExcelQuery); if(R.isSuccess(pointClientByIds)){ List<XpPoint> data = pointClientByIds.getData(); pageData.getRecords().forEach(item -> { data.forEach(point -> { if(item.getPointId().equals(point.getId())){ item.setPointName(point.getPointName()); item.setPointAddress(point.getPointAddress()); } }); }); } } PageInfo<XpAppUserCollectAddrVO> resultPage = new PageInfo<>(query.getPageNum(), query.getPageSize()); resultPage.setTotal(pageData.getTotal()); resultPage.setRecords(pageData.getRecords()); return resultPage; } /** * 将前端传入的站点/场站目标解析成统一收藏目标。 * 优先按 itemType 判断,未传时兼容旧逻辑按 pointId 处理。 */ private CollectAddrTargetVO resolveCollectTarget(CollectAddrDTO dto) { if (isStationItem(dto)) { if (dto.getStationId() == null) { throw new ServiceException("场站id不能为空"); } return this.baseMapper.selectStationCollectTarget(dto.getStationId()); } if (dto.getPointId() == null && dto.getStationId() != null) { return this.baseMapper.selectStationCollectTarget(dto.getStationId()); } if (dto.getPointId() == null) { throw new ServiceException("站点id不能为空"); } return this.baseMapper.selectPointCollectTarget(dto.getPointId()); } /** * 收藏唯一键规则: * 1. 站点收藏按 user_id + item_type + point_id 去重。 * 2. 场站收藏按 user_id + item_type + station_id 去重。 * point_id 在场站收藏里仍保留,方便联动现有“已收藏点位”能力。 */ private LambdaQueryWrapper<XpAppUserCollectAddr> buildSameTargetWrapper(Long userId, CollectAddrTargetVO target) { LambdaQueryWrapper<XpAppUserCollectAddr> wrapper = new LambdaQueryWrapper<XpAppUserCollectAddr>() .eq(XpAppUserCollectAddr::getAppUserId, userId) .eq(XpAppUserCollectAddr::getDeleted, 0) .eq(XpAppUserCollectAddr::getItemType, target.getItemType()); if (target.getItemType() == ITEM_TYPE_STATION) { wrapper.eq(XpAppUserCollectAddr::getStationId, target.getStationId()); } else { wrapper.eq(XpAppUserCollectAddr::getPointId, target.getPointId()); } return wrapper; } private boolean isStationItem(CollectAddrDTO dto) { return dto.getItemType() != null && dto.getItemType() == ITEM_TYPE_STATION; } } xp-robotaxi-account-boot/src/main/resources/mapper/account/XpAppUserCollectAddrMapper.xml
@@ -2,11 +2,90 @@ <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.xiaopeng.account.mapper.XpAppUserCollectAddrMapper"> <select id="userCollectAddrPageList" resultType="com.xiaopeng.account.model.vo.XpAppUserCollectAddrVO"> select c.id, c.item_type as itemType, c.longitude, c.latitude, c.point_id as pointId, c.station_id as stationId, case when c.item_type = 2 then s.station_name else p.point_name end as pointName, case when c.item_type = 2 then coalesce(dp.point_address, fp.point_address, p.point_address) else p.point_address end as pointAddress from xp_app_user_collect_addr c left join xp_point p on p.id = c.point_id and p.is_deleted = 0 left join xp_station s on s.id = c.station_id and s.is_deleted = 0 left join xp_point dp on dp.id = s.default_point_id and dp.is_deleted = 0 left join xp_point fp on fp.id = ( select p1.id from xp_point p1 where p1.station_id = s.id and p1.is_deleted = 0 and p1.point_type = 1 and p1.operation_status = 2 order by case when s.default_point_id is not null and p1.id = s.default_point_id then 0 else 1 end, p1.station_sort_no asc, p1.id asc limit 1 ) where c.app_user_id = #{uid} and c.is_deleted = 0 order by c.create_time asc </select> select longitude,latitude,point_id from xp_app_user_collect_addr <select id="selectPointCollectTarget" resultType="com.xiaopeng.account.model.vo.CollectAddrTargetVO"> select 1 as itemType, p.id as pointId, p.station_id as stationId, p.point_lon as longitude, p.point_lat as latitude from xp_point p inner join xp_electronic_fence_area a on a.id = p.electronic_area_id where p.is_deleted = 0 and p.point_type = 1 and p.operation_status = 2 and a.is_deleted = 0 and a.area_status = 1 and p.id = #{pointId} </select> WHERE app_user_id = #{uid} and is_deleted = 0 order by create_time asc <select id="selectStationCollectTarget" resultType="com.xiaopeng.account.model.vo.CollectAddrTargetVO"> select 2 as itemType, coalesce(dp.id, fp.id) as pointId, s.id as stationId, coalesce(s.center_lon, dp.point_lon, fp.point_lon) as longitude, coalesce(s.center_lat, dp.point_lat, fp.point_lat) as latitude from xp_station s inner join xp_electronic_fence_area a on a.id = s.electronic_area_id left join xp_point dp on dp.id = s.default_point_id and dp.is_deleted = 0 and dp.point_type = 1 and dp.operation_status = 2 left join xp_point fp on fp.id = ( select p1.id from xp_point p1 where p1.station_id = s.id and p1.is_deleted = 0 and p1.point_type = 1 and p1.operation_status = 2 order by case when s.default_point_id is not null and p1.id = s.default_point_id then 0 else 1 end, p1.station_sort_no asc, p1.id asc limit 1 ) where s.is_deleted = 0 and s.status = 6 and a.is_deleted = 0 and a.area_status = 1 and s.id = #{stationId} and coalesce(dp.id, fp.id) is not null </select> </mapper> xp-robotaxi-api/src/main/java/com/xiaopeng/api/factory/account/XpAppUserCollectAddrClientFallbackFactory.java
@@ -13,8 +13,12 @@ @Override public R<List<Long>> getPointIdsByUserId(Long uid) { R.fail("获取用户收藏地址失败" + cause.getMessage()); return null; return R.fail("获取用户收藏站点失败:" + cause.getMessage()); } @Override public R<List<Long>> getStationIdsByUserId(Long uid) { return R.fail("获取用户收藏场站失败:" + cause.getMessage()); } }; } xp-robotaxi-api/src/main/java/com/xiaopeng/api/feignClient/account/XpAppUserCollectAddrClient.java
@@ -1,22 +1,16 @@ package com.xiaopeng.api.feignClient.account; import com.xiaopeng.api.dto.statistics.StatisticsQueryDTO; import com.xiaopeng.api.dto.statistics.UserStatisticsDTO; import com.xiaopeng.api.factory.account.XpAppUserClientFallbackFactory; import com.xiaopeng.api.factory.account.XpAppUserCollectAddrClientFallbackFactory; import com.xiaopeng.api.model.account.XpAppUser; import com.xiaopeng.common.core.domain.R; import com.xiaopeng.common.core.dto.ServiceNameConstantsEnum; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import java.util.List; /** * 小程序用户Feign客户端 * 小程序用户收藏地址 Feign 客户端 * * @author mitao */ @@ -26,9 +20,14 @@ public interface XpAppUserCollectAddrClient { /** * 获取用户收藏的pointIds * 获取用户收藏的站点 pointIds */ @GetMapping("/logan/rt/v1/appUser/collectAddr/getPointIds") R<List<Long>> getPointIdsByUserId(@RequestParam Long uid); /** * 获取用户收藏的场站 stationIds */ @GetMapping("/logan/rt/v1/appUser/collectAddr/getStationIds") R<List<Long>> getStationIdsByUserId(@RequestParam Long uid); } xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/account/XpAppUserCollectAddr.java
@@ -10,8 +10,6 @@ import lombok.EqualsAndHashCode; import lombok.experimental.Accessors; import javax.validation.constraints.NotNull; /** * <p> * 用户收藏地址 @@ -24,7 +22,7 @@ @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) @TableName("xp_app_user_collect_addr") @ApiModel(value="XpAppUserCollectAddr对象", description="用户收藏地址") @ApiModel(value = "XpAppUserCollectAddr对象", description = "用户收藏地址") public class XpAppUserCollectAddr extends BaseModel { private static final long serialVersionUID = 1L; @@ -41,6 +39,12 @@ @ApiModelProperty(value = "纬度") private Double latitude; @ApiModelProperty(value = "站点id") @ApiModelProperty(value = "收藏对象类型 1=站点 2=场站") private Integer itemType; @ApiModelProperty(value = "点位id。场站收藏时保存代表点位id") private Long pointId; @ApiModelProperty(value = "场站id。站点收藏时为空") private Long stationId; } xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/order/XpPointsListVo.java
@@ -4,9 +4,14 @@ import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.util.List; @Data @ApiModel("搜索点位Vo") @ApiModel("搜索点位结果") public class XpPointsListVo { @ApiModelProperty("结果类型 1=站点 2=场站") private Integer itemType; @ApiModelProperty("名称") private String name; @@ -20,28 +25,39 @@ @ApiModelProperty("经度") private Double longitude; @ApiModelProperty("距离m") @ApiModelProperty("距离,单位米") private Double distance; @ApiModelProperty("站点id") @ApiModelProperty("结果id,站点时为point.id,场站时为station.id") private Long id; @ApiModelProperty("所属场站id,无场站时为空") private Long stationId; @ApiModelProperty("电子围栏区域id") private Long electronicAreaId; @ApiModelProperty(value = "运营类型 1=与运营区同步 2=自定义时间段") @ApiModelProperty("运营类型 1=与运营区同步 2=自定义时间段") private Integer operationType; @ApiModelProperty("是否收藏 1是 2否") private Integer isCollect=2; @ApiModelProperty("是否收藏 1=是 2=否") private Integer isCollect = 2; @ApiModelProperty("收藏绑定的点位id,场站收藏时用于落到具体point_id") private Long collectPointId; @ApiModelProperty("点位在场站内排序") private Integer stationSortNo; @ApiModelProperty("生效周期") private Integer effectivePeriod; @ApiModelProperty("生效时间段") private String effectiveTimePeriod; private Integer state=1; @ApiModelProperty("状态 1=可用 0=不可用") private Integer state = 1; @ApiModelProperty("场站子点位列表") private List<XpPointsListVo> children; } xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/mapper/XpPointMapper.java
@@ -28,7 +28,11 @@ List<XpPoint> listAll(@Param("query") PointListQuery query); List<XpPointsListVo> queryPointByName(@Param("query") PointByNameQuery query); List<XpPointsListVo> queryPointByName(@Param("query") PointByNameQuery query, @Param("excludeStationIds") List<Long> excludeStationIds); List<XpPointsListVo> queryStationByName(@Param("query") PointByNameQuery query); List<XpPointsListVo> queryStationChildren(@Param("stationIds") List<Long> stationIds); List<XpPoint> pageList(@Param("pageInfo") PageInfo<XpPoint> pageInfo, @Param("query") PointPageListListQuery query); } xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/impl/XpPointServiceImpl.java
@@ -470,85 +470,242 @@ @Override public XpPointsSearchVo queryPointByName(PointByNameQuery query,String uid) { XpPointsSearchVo searchVo = new XpPointsSearchVo(); List<XpPointsListVo> list = this.baseMapper.queryPointByName(query); if (list.isEmpty()) { List<XpPointsListVo> stationList = this.baseMapper.queryStationByName(query); List<Long> stationIds = stationList.stream() .map(this::getSearchResultStationId) .filter(Objects::nonNull) .distinct() .collect(Collectors.toList()); // 先把命中的场站id传给点位查询,避免同一场站下的点位和场站父胶囊重复返回。 List<XpPointsListVo> pointList = this.baseMapper.queryPointByName(query, stationIds); List<XpPointsListVo> stationChildren = CollUtil.isEmpty(stationIds) ? Collections.emptyList() : this.baseMapper.queryStationChildren(stationIds); if (CollUtil.isEmpty(stationList) && CollUtil.isEmpty(pointList)) { return searchVo; } LocalTime now = LocalTime.now(); List<Long> areaIds = list.stream().map(XpPointsListVo::getElectronicAreaId).collect(Collectors.toList()); List<XpElectronicFenceArea> xpElectronicFenceAreas = xpElectronicFenceAreaService.listByIds(areaIds); List<Long> fenceIds = xpElectronicFenceAreas.stream().map(XpElectronicFenceArea::getElectronicId).collect(Collectors.toList()); // 搜索页需要拆分两套收藏态: // 1. 站点胶囊按 pointId 判定。 // 2. 场站胶囊按 stationId 判定。 Set<Long> collectedPointIds = getCollectedPointIds(uid); Set<Long> collectedStationIds = getCollectedStationIds(uid); List<XpPointsListVo> allRows = new ArrayList<>(stationList.size() + pointList.size() + stationChildren.size()); allRows.addAll(stationList); allRows.addAll(pointList); allRows.addAll(stationChildren); List<XpElectronicFence> xpElectronicFences = new ArrayList<>(); if (CollUtil.isNotEmpty(fenceIds)) { xpElectronicFences = xpElectronicFenceService.listByIds(fenceIds); } Map<Long, XpElectronicFenceArea> electronicFenceAreaMap = buildElectronicFenceAreaMap(allRows); Map<Long, XpElectronicFence> electronicFenceMap = buildElectronicFenceMap(electronicFenceAreaMap); markSearchRows(allRows, collectedPointIds, collectedStationIds, electronicFenceAreaMap, electronicFenceMap); List<XpPointsListVo> resultList = new ArrayList<>(); List<XpPointsListVo> stationResults = buildStationSearchResults(stationList, stationChildren, query); Set<Long> displayedStationIds = stationResults.stream() .map(this::getSearchResultStationId) .filter(Objects::nonNull) .collect(Collectors.toSet()); resultList.addAll(stationResults); resultList.addAll(pointList.stream() .filter(vo -> vo.getState() == 1) .filter(vo -> vo.getStationId() == null || !displayedStationIds.contains(vo.getStationId())) .peek(vo -> fillDistance(vo, query)) .collect(Collectors.toList())); R<List<Long>> pointIdsByUserId =null; // 获取用户收藏的地址 if(uid!=null){ pointIdsByUserId = xpAppUserCollectAddrClient.getPointIdsByUserId(Long.valueOf(uid)); } for (XpPointsListVo xpPointsListVo : list) { // 判断是否收藏 if(pointIdsByUserId!=null && !pointIdsByUserId.getData().isEmpty()){ if(pointIdsByUserId.getData().contains(xpPointsListVo.getId())){ xpPointsListVo.setIsCollect(1); } } Integer operationType = xpPointsListVo.getOperationType(); if(1 == operationType){ //与运营区同步 XpElectronicFenceArea xpElectronicFenceArea = xpElectronicFenceAreas.stream().filter(s -> s.getId().equals(xpPointsListVo.getElectronicAreaId())).findFirst().orElse(null); if(null == xpElectronicFenceArea){ // 不在运营段 xpPointsListVo.setState(0); } XpElectronicFence xpElectronicFence = xpElectronicFences.stream().filter(s -> s.getId().equals(xpElectronicFenceArea.getElectronicId())).findFirst().orElse(null); if(null == xpElectronicFence){ // 不在运营段 xpPointsListVo.setState(0); } //失效类型 1=永不失效 2=定时失效 Integer failureType = xpElectronicFence.getFailureType(); //永不失效,但是还未到开始时间 if(1 == failureType && LocalDateTime.now().isBefore(xpElectronicFence.getEffectTime())){ xpPointsListVo.setState(0); }else if(2 == failureType && (LocalDateTime.now().isBefore(xpElectronicFence.getEffectTime()) || LocalDateTime.now().isAfter(xpElectronicFence.getFailureTime()))){ //定时失效,但是当前时间不在时间范围内 xpPointsListVo.setState(0); } }else{ //自定义时间段 // 判断是否在时间段内 boolean intime = getInTime(now, xpPointsListVo.getEffectivePeriod(), xpPointsListVo.getEffectiveTimePeriod()); if (!intime) { // 不在运营段 xpPointsListVo.setState(0); } } } // 组装数据 并排序 剔除不在运营段的 数据 List<XpPointsListVo> resultList = list.stream() .peek(vo -> vo.setDistance(BigDecimal.valueOf(FastDistanceCalculator.distanceFast(query.getLat(), query.getLon(), vo.getLatitude(), vo.getLongitude())).setScale(2, RoundingMode.HALF_UP).doubleValue())) .filter(vo -> vo.getState() == 1).collect(Collectors.toList()); // 可用点位 List<XpPointsListVo> availablePoints = resultList.stream().filter(e -> query.getElectronicAreaId() != null && e.getElectronicAreaId().equals(query.getElectronicAreaId())).sorted(Comparator.comparing(XpPointsListVo::getDistance)).collect(Collectors.toList());// // 不可用点位 List<XpPointsListVo> unavailablePoints = resultList.stream().filter(e -> query.getElectronicAreaId() == null || !e.getElectronicAreaId().equals(query.getElectronicAreaId())).sorted(Comparator.comparing(XpPointsListVo::getDistance)).collect(Collectors.toList()); List<XpPointsListVo> availablePoints = resultList.stream() .filter(e -> query.getElectronicAreaId() != null && e.getElectronicAreaId().equals(query.getElectronicAreaId())) .sorted(Comparator.comparing(this::distanceOrMax)) .collect(Collectors.toList()); List<XpPointsListVo> unavailablePoints = resultList.stream() .filter(e -> query.getElectronicAreaId() == null || !e.getElectronicAreaId().equals(query.getElectronicAreaId())) .sorted(Comparator.comparing(this::distanceOrMax)) .collect(Collectors.toList()); searchVo.setAvailablePoints(availablePoints); searchVo.setUnavailablePoints(unavailablePoints); return searchVo; } private List<XpPointsListVo> buildStationSearchResults(List<XpPointsListVo> stationList, List<XpPointsListVo> stationChildren, PointByNameQuery query) { if (CollUtil.isEmpty(stationList)) { return Collections.emptyList(); } Map<Long, List<XpPointsListVo>> childrenMap = stationChildren.stream() .filter(vo -> vo.getState() == 1) .peek(vo -> fillDistance(vo, query)) .collect(Collectors.groupingBy(this::getSearchResultStationId, LinkedHashMap::new, Collectors.toList())); List<XpPointsListVo> result = new ArrayList<>(); for (XpPointsListVo station : stationList) { if (station.getState() != 1) { continue; } Long stationId = getSearchResultStationId(station); List<XpPointsListVo> children = childrenMap.getOrDefault(stationId, Collections.emptyList()); if (CollUtil.isEmpty(children)) { continue; } XpPointsListVo firstChild = children.get(0); if (station.getCollectPointId() == null) { station.setCollectPointId(firstChild.getId()); } if (station.getLongitude() == null || station.getLatitude() == null) { station.setLongitude(firstChild.getLongitude()); station.setLatitude(firstChild.getLatitude()); } if (!StringUtils.hasLength(station.getAddress())) { station.setAddress(firstChild.getAddress()); } fillDistance(station, query); station.setChildren(children); result.add(station); } return result; } /** * 仅查询“站点收藏”。 * nearest / 历史上下车点等老接口仍然只对 pointId 打星,所以这里不能混入场站收藏。 */ private Set<Long> getCollectedPointIds(String uid) { if (uid == null) { return Collections.emptySet(); } R<List<Long>> pointIdsByUserId = xpAppUserCollectAddrClient.getPointIdsByUserId(Long.valueOf(uid)); if (pointIdsByUserId == null || CollUtil.isEmpty(pointIdsByUserId.getData())) { return Collections.emptySet(); } return new HashSet<>(pointIdsByUserId.getData()); } /** * 场站收藏需要单独查询。 * 搜索结果里的场站父级行应按 stationId 判断收藏态,不能继续复用 pointId 集合。 */ private Set<Long> getCollectedStationIds(String uid) { if (uid == null) { return Collections.emptySet(); } R<List<Long>> stationIdsByUserId = xpAppUserCollectAddrClient.getStationIdsByUserId(Long.valueOf(uid)); if (stationIdsByUserId == null || CollUtil.isEmpty(stationIdsByUserId.getData())) { return Collections.emptySet(); } return new HashSet<>(stationIdsByUserId.getData()); } private Map<Long, XpElectronicFenceArea> buildElectronicFenceAreaMap(List<XpPointsListVo> rows) { List<Long> areaIds = rows.stream() .map(XpPointsListVo::getElectronicAreaId) .filter(Objects::nonNull) .distinct() .collect(Collectors.toList()); if (CollUtil.isEmpty(areaIds)) { return Collections.emptyMap(); } return xpElectronicFenceAreaService.listByIds(areaIds).stream() .collect(Collectors.toMap(XpElectronicFenceArea::getId, area -> area, (left, right) -> left)); } private Map<Long, XpElectronicFence> buildElectronicFenceMap(Map<Long, XpElectronicFenceArea> electronicFenceAreaMap) { if (electronicFenceAreaMap.isEmpty()) { return Collections.emptyMap(); } List<Long> fenceIds = electronicFenceAreaMap.values().stream() .map(XpElectronicFenceArea::getElectronicId) .filter(Objects::nonNull) .distinct() .collect(Collectors.toList()); if (CollUtil.isEmpty(fenceIds)) { return Collections.emptyMap(); } return xpElectronicFenceService.listByIds(fenceIds).stream() .collect(Collectors.toMap(XpElectronicFence::getId, fence -> fence, (left, right) -> left)); } private void markSearchRows(List<XpPointsListVo> rows, Set<Long> collectedPointIds, Set<Long> collectedStationIds, Map<Long, XpElectronicFenceArea> electronicFenceAreaMap, Map<Long, XpElectronicFence> electronicFenceMap) { LocalTime now = LocalTime.now(); for (XpPointsListVo row : rows) { if (isCollectedSearchRow(row, collectedPointIds, collectedStationIds)) { row.setIsCollect(1); } if (!isSearchRowAvailable(row, electronicFenceAreaMap, electronicFenceMap, now)) { row.setState(0); } } } /** * 搜索结果收藏态判定规则: * 1. 站点行按 pointId / collectPointId 判定。 * 2. 场站行按 stationId 判定。 */ private boolean isCollectedSearchRow(XpPointsListVo row, Set<Long> collectedPointIds, Set<Long> collectedStationIds) { if (row.getItemType() != null && row.getItemType() == 2) { Long stationId = getSearchResultStationId(row); return stationId != null && collectedStationIds.contains(stationId); } Long collectPointId = row.getCollectPointId() != null ? row.getCollectPointId() : row.getId(); return collectPointId != null && collectedPointIds.contains(collectPointId); } private boolean isSearchRowAvailable(XpPointsListVo row, Map<Long, XpElectronicFenceArea> electronicFenceAreaMap, Map<Long, XpElectronicFence> electronicFenceMap, LocalTime now) { Integer operationType = row.getOperationType(); if (operationType == null) { return false; } if (operationType == 1) { Long electronicAreaId = row.getElectronicAreaId(); if (electronicAreaId == null) { return false; } XpElectronicFenceArea electronicFenceArea = electronicFenceAreaMap.get(electronicAreaId); if (electronicFenceArea == null || electronicFenceArea.getElectronicId() == null) { return false; } XpElectronicFence electronicFence = electronicFenceMap.get(electronicFenceArea.getElectronicId()); if (electronicFence == null) { return false; } Integer failureType = electronicFence.getFailureType(); if (failureType == null) { return true; } if (failureType == 1 && LocalDateTime.now().isBefore(electronicFence.getEffectTime())) { return false; } return failureType != 2 || (!LocalDateTime.now().isBefore(electronicFence.getEffectTime()) && !LocalDateTime.now().isAfter(electronicFence.getFailureTime())); } if (row.getEffectivePeriod() == null || !StringUtils.hasLength(row.getEffectiveTimePeriod())) { return true; } return getInTime(now, row.getEffectivePeriod(), row.getEffectiveTimePeriod()); } private void fillDistance(XpPointsListVo row, PointByNameQuery query) { if (row.getLatitude() == null || row.getLongitude() == null) { return; } row.setDistance(BigDecimal.valueOf(FastDistanceCalculator.distanceFast(query.getLat(), query.getLon(), row.getLatitude(), row.getLongitude())) .setScale(2, RoundingMode.HALF_UP) .doubleValue()); } private Long getSearchResultStationId(XpPointsListVo row) { return row.getStationId() != null ? row.getStationId() : row.getId(); } private Double distanceOrMax(XpPointsListVo row) { return row.getDistance() == null ? Double.MAX_VALUE : row.getDistance(); } @Override public OrderDistanceVo getOrderDistance() { OrderDistanceVo orderDistanceVo = new OrderDistanceVo(); xp-robotaxi-operation-boot/src/main/resources/mapper/operation/XpPointMapper.xml
@@ -94,19 +94,148 @@ <select id="queryPointByName" resultType="com.xiaopeng.api.vo.order.XpPointsListVo"> select p.id,p.operation_type as operationType ,p.electronic_area_id as electronicAreaId,p.point_name as name,p.point_address as address,p.point_lon as longitude,p.point_lat as latitude,p.effective_period,p.effective_time_period select 1 as itemType, p.id, p.station_id as stationId, p.id as collectPointId, p.operation_type as operationType, p.electronic_area_id as electronicAreaId, p.point_name as name, p.point_address as address, p.point_lon as longitude, p.point_lat as latitude, p.station_sort_no as stationSortNo, p.effective_period as effectivePeriod, p.effective_time_period as effectiveTimePeriod from xp_electronic_fence_area xefa left join xp_point p on p.electronic_area_id = xefa.id where p.is_deleted = 0 and p.operation_status =2 and xefa.is_deleted = 0 and xefa.area_status = 1 and p.electronic_area_id is not null and xefa.city_code = #{query.cityCode} inner join xp_point p on p.electronic_area_id = xefa.id where p.is_deleted = 0 and p.point_type = 1 and p.operation_status = 2 and xefa.is_deleted = 0 and xefa.area_status = 1 and p.electronic_area_id is not null and xefa.city_code = #{query.cityCode} <if test="query.pointPoi != null and query.pointPoi != ''"> and ( p.point_address like concat('%',#{query.pointPoi},'%') or p.point_name like concat('%',#{query.pointPoi},'%') ) and ( p.point_address like concat('%',#{query.pointPoi},'%') or p.point_name like concat('%',#{query.pointPoi},'%') or p.point_poi like concat('%',#{query.pointPoi},'%') ) </if> <if test="query.electronicAreaId != null"> and p.electronic_area_id = #{query.electronicAreaId} </if> <if test="excludeStationIds != null and excludeStationIds.size() > 0"> and (p.station_id is null or p.station_id not in <foreach collection="excludeStationIds" item="stationId" separator="," open="(" close=")"> #{stationId} </foreach> ) </if> </select> <select id="queryStationByName" resultType="com.xiaopeng.api.vo.order.XpPointsListVo"> select 2 as itemType, s.id, s.id as stationId, coalesce(s.default_point_id, dp.id, fp.id) as collectPointId, s.operation_type as operationType, s.electronic_area_id as electronicAreaId, s.station_name as name, coalesce(dp.point_address, fp.point_address) as address, coalesce(s.center_lon, dp.point_lon, fp.point_lon) as longitude, coalesce(s.center_lat, dp.point_lat, fp.point_lat) as latitude, s.effective_period as effectivePeriod, s.effective_time_period as effectiveTimePeriod from xp_station s inner join xp_electronic_fence_area xefa on xefa.id = s.electronic_area_id left join xp_point dp on dp.id = s.default_point_id and dp.is_deleted = 0 and dp.point_type = 1 and dp.operation_status = 2 left join xp_point fp on fp.id = ( select p1.id from xp_point p1 where p1.station_id = s.id and p1.is_deleted = 0 and p1.point_type = 1 and p1.operation_status = 2 order by case when s.default_point_id is not null and p1.id = s.default_point_id then 0 else 1 end, p1.station_sort_no asc, p1.id asc limit 1 ) where s.is_deleted = 0 and s.status = 6 and xefa.is_deleted = 0 and xefa.area_status = 1 and s.city_code = #{query.cityCode} and exists ( select 1 from xp_point p2 where p2.station_id = s.id and p2.is_deleted = 0 and p2.point_type = 1 and p2.operation_status = 2 ) <if test="query.pointPoi != null and query.pointPoi != ''"> and ( s.station_name like concat('%',#{query.pointPoi},'%') or exists ( select 1 from xp_point p3 where p3.station_id = s.id and p3.is_deleted = 0 and p3.point_type = 1 and p3.operation_status = 2 and ( p3.point_name like concat('%',#{query.pointPoi},'%') or p3.point_address like concat('%',#{query.pointPoi},'%') or p3.point_poi like concat('%',#{query.pointPoi},'%') ) ) ) </if> <if test="query.electronicAreaId != null"> and s.electronic_area_id = #{query.electronicAreaId} </if> order by s.sort_no asc, s.id asc </select> <select id="queryStationChildren" resultType="com.xiaopeng.api.vo.order.XpPointsListVo"> select 1 as itemType, p.id, p.station_id as stationId, p.id as collectPointId, p.operation_type as operationType, p.electronic_area_id as electronicAreaId, p.point_name as name, p.point_address as address, p.point_lon as longitude, p.point_lat as latitude, p.station_sort_no as stationSortNo, p.effective_period as effectivePeriod, p.effective_time_period as effectiveTimePeriod from xp_point p inner join xp_station s on s.id = p.station_id inner join xp_electronic_fence_area xefa on xefa.id = p.electronic_area_id where p.is_deleted = 0 and p.point_type = 1 and p.operation_status = 2 and s.is_deleted = 0 and s.status = 6 and xefa.is_deleted = 0 and xefa.area_status = 1 and p.station_id in <foreach collection="stationIds" item="stationId" separator="," open="(" close=")"> #{stationId} </foreach> order by p.station_id asc, case when s.default_point_id is not null and p.id = s.default_point_id then 0 else 1 end asc, p.station_sort_no asc, p.id asc </select> <select id="pageList" resultType="com.xiaopeng.api.model.operation.XpPoint"> select xp.id,xp.point_number as pointNumber,xp.point_name as pointName,xp.point_poi as pointPoi,