package com.xiaopeng.map.service.serviceimpl; import cn.hutool.core.collection.CollUtil; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.xiaopeng.api.dto.operation.PointAddDto; import com.xiaopeng.api.dto.operation.PointMapQuery; import com.xiaopeng.api.dto.order.HistoryEndPointsDto; import com.xiaopeng.api.dto.order.HistoryStartPointsDto; import com.xiaopeng.api.model.operation.XpElectronicFenceArea; import com.xiaopeng.api.model.operation.XpPoint; import com.xiaopeng.api.model.operation.XpRegion; import com.xiaopeng.api.model.operation.XpSystemSetting; import com.xiaopeng.api.query.operation.PointListQuery; import com.xiaopeng.api.query.operation.PointPageListListQuery; import com.xiaopeng.api.vo.order.XpHistoryEndPointsVo; import com.xiaopeng.api.vo.order.XpHistoryStartPointsVo; import com.xiaopeng.api.vo.order.XpPointsListVo; import com.xiaopeng.api.vo.order.XpPointsSearchVo; import com.xiaopeng.common.core.constant.CacheConstants; import com.xiaopeng.common.core.exception.ServiceException; import com.xiaopeng.common.core.utils.StringUtils; import com.xiaopeng.common.core.web.page.PageInfo; import com.xiaopeng.common.redis.service.RedisService; import com.xiaopeng.evi.oss.service.AliOssService; import com.xiaopeng.map.constant.I18nKeyConstant; import com.xiaopeng.map.dto.NearestDto; import com.xiaopeng.map.mapper.XpPointMapper; import com.xiaopeng.map.mapper.XpRegionMapper; import com.xiaopeng.map.mapper.XpSystemSettingMapper; import com.xiaopeng.map.query.PointByNameQuery; import com.xiaopeng.map.service.XpElectronicFenceAreaService; import com.xiaopeng.map.service.XpPointService; import com.xiaopeng.map.utils.TimeRangeChecker; import com.xiaopeng.map.utils.holiday.HolidayUtil; import com.xiaopeng.map.utils.map.map.FastDistanceCalculator; import com.xiaopeng.map.vo.NearestNextVo; import com.xiaopeng.map.vo.NearestVo; import com.xiaopeng.map.vo.OrderDistanceVo; import lombok.extern.slf4j.Slf4j; import net.sourceforge.pinyin4j.PinyinHelper; import org.apache.shardingsphere.transaction.annotation.ShardingSphereTransactionType; import org.apache.shardingsphere.transaction.core.TransactionType; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.io.UnsupportedEncodingException; import java.time.LocalTime; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** *

* 点位管理 服务实现类 *

* * @author liu * @author WuGuanFengYue * @since 2026-02-03 */ @Service @Slf4j public class XpPointServiceImpl extends ServiceImpl implements XpPointService { private static final String POINT_BIZ_TYPE = "PUD"; private static final int SERIAL_WIDTH = 6; private static final int LOCK_WAIT_SECONDS = 5; private static final int LOCK_LEASE_SECONDS = 30; @Resource private XpSystemSettingMapper xpSystemSettingMapper; @Resource private RedisService redisService; @Resource private HolidayUtil holidayUtil; @Resource private XpElectronicFenceAreaService xpElectronicFenceAreaService; @Resource private XpRegionMapper xpRegionMapper; @Autowired private RedissonClient redissonClient; @Autowired private AliOssService aliOssService; @Override @Transactional @ShardingSphereTransactionType(TransactionType.BASE) public XpPoint addPointWithGeneratedNumber(PointAddDto pointAddDto) { if (pointAddDto == null) { throw new ServiceException("参数不能为空", I18nKeyConstant.SYSTEM_EXCEPTION); } if (pointAddDto.getElectronicAreaId() == null) { throw new ServiceException("运营区域ID不能为空", I18nKeyConstant.POINT_ELECTRONIC_AREA_ID_EMPTY); } String cityPrefix = buildCityPrefix(pointAddDto.getElectronicAreaId()); String lockKey = buildPointNumberLockKey(cityPrefix, POINT_BIZ_TYPE); RLock lock = redissonClient.getLock(lockKey); boolean locked = false; String pointNumber; try { locked = lock.tryLock(LOCK_WAIT_SECONDS, LOCK_LEASE_SECONDS, TimeUnit.SECONDS); if (!locked) { throw new ServiceException("点位编号生成繁忙,请稍后重试", I18nKeyConstant.POINT_NUMBER_LOCK_FAIL); } String maxPointNumber = this.baseMapper.selectMaxPointNumber(cityPrefix, POINT_BIZ_TYPE); int nextSerial = 1; if (StringUtils.hasText(maxPointNumber)) { nextSerial = parseSerialOrThrow(maxPointNumber) + 1; } pointNumber = cityPrefix + "-" + POINT_BIZ_TYPE + "-" + String.format("%0" + SERIAL_WIDTH + "d", nextSerial); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new ServiceException("点位编号生成繁忙,请稍后重试", I18nKeyConstant.POINT_NUMBER_LOCK_FAIL); } finally { try { if (locked && lock.isHeldByCurrentThread()) { lock.unlock(); } } catch (Exception e) { log.warn("释放点位编号生成锁失败 lockKey={}", lockKey, e); } } XpPoint xpPoint = new XpPoint(); BeanUtils.copyProperties(pointAddDto, xpPoint); xpPoint.setFpElectronicAreaId(pointAddDto.getElectronicAreaId()); xpPoint.setPointNumber(pointNumber); this.save(xpPoint); return xpPoint; } @Override public NearestVo nearest(NearestDto dto) { // 走缓存 Object orderDistanceObj = redisService.getCacheObject(CacheConstants.NEAR_POINT_DISTANCE); Integer orderDistance; if (orderDistanceObj != null) { orderDistance = Integer.valueOf(orderDistanceObj.toString()); } else { XpSystemSetting xpSystemSetting = xpSystemSettingMapper.selectOne(null); orderDistance = xpSystemSetting.getNearPointDistance(); } // 获取所有的站点信息 List list = list(new LambdaQueryWrapper().eq(XpPoint::getOperationStatus, 2).eq(XpPoint::getPointType,1).isNotNull(XpPoint::getElectronicAreaId) // 添加距离计算字段 .apply("ST_Distance_Sphere(point({0}, {1}), point(point_lon, point_lat)) <= {2}", dto.getLongitude(), dto.getLatitude(), orderDistance)); NearestVo nearestVo = new NearestVo(); List nearestNextVos = new ArrayList<>(); LocalTime now = LocalTime.now(); for (XpPoint xpPoint : list) { // 判断是否在时间段内 boolean intime = getInTime(now, xpPoint.getEffectivePeriod(), xpPoint.getEffectiveTimePeriod()); if (!intime) { continue; } // 计算距离 小于的站点 double v = FastDistanceCalculator.distanceFast(dto.getLatitude(), dto.getLongitude(), xpPoint.getPointLat(), xpPoint.getPointLon()); if (v < orderDistance) { NearestNextVo nearestNextVo = new NearestNextVo(); nearestNextVo.setId(xpPoint.getId()); nearestNextVo.setName(xpPoint.getPointName()); nearestNextVo.setLatitude(xpPoint.getPointLat()); nearestNextVo.setLongitude(xpPoint.getPointLon()); nearestNextVo.setDistance(v); nearestNextVo.setAddress(xpPoint.getPointAddress()); nearestNextVo.setTime(xpPoint.getParkingTimeLimit()); nearestNextVo.setElectronicAreaId(xpPoint.getElectronicAreaId()); nearestNextVos.add(nearestNextVo); } } if (!nearestNextVos.isEmpty()) { nearestNextVos = nearestNextVos.stream().sorted(Comparator.comparing(NearestNextVo::getDistance)).collect(Collectors.toList()); } nearestVo.setPoints(nearestNextVos); nearestVo.setTotal(nearestNextVos.size()); return nearestVo; } private String buildCityPrefix(Long electronicAreaId) { //所有围栏范围之外,测试点位 if(0 == electronicAreaId){ return "TEST"; } XpElectronicFenceArea area = xpElectronicFenceAreaService.getById(electronicAreaId); if (area == null) { throw new ServiceException("运营区域不存在", I18nKeyConstant.POINT_ELECTRONIC_AREA_NOT_EXIST); } if (!StringUtils.hasText(area.getCityCode())) { throw new ServiceException("城市信息不存在", I18nKeyConstant.POINT_CITY_NOT_EXIST); } XpRegion region = xpRegionMapper.selectOne(new LambdaQueryWrapper() .eq(XpRegion::getCode, area.getCityCode())); if (region == null || !StringUtils.hasText(region.getName())) { throw new ServiceException("城市信息不存在", I18nKeyConstant.POINT_CITY_NOT_EXIST); } String name = region.getName(); String prefix = name.length() <= 2 ? name : name.substring(0, 2); return getPinyinInitials(prefix).toUpperCase(); } private static String buildPointNumberLockKey(String cityPrefix, String bizType) { return "xp:point:number:" + cityPrefix + ":" + bizType; } private static String getPinyinInitials(String chinese) { StringBuilder pinyinInitials = new StringBuilder(); for (char c : chinese.toCharArray()) { String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(c); if (pinyinArray != null && pinyinArray.length > 0) { pinyinInitials.append(pinyinArray[0].charAt(0)); } } return pinyinInitials.toString(); } private static int parseSerialOrThrow(String pointNumber) { String[] parts = pointNumber.split("-"); if (parts.length < 3) { throw new ServiceException("点位编号生成失败(历史编号格式异常)", I18nKeyConstant.POINT_NUMBER_PARSE_FAIL); } String serialPart = parts[parts.length - 1]; if (serialPart.length() != SERIAL_WIDTH) { throw new ServiceException("点位编号生成失败(历史编号格式异常)", I18nKeyConstant.POINT_NUMBER_PARSE_FAIL); } try { return Integer.parseInt(serialPart); } catch (NumberFormatException e) { throw new ServiceException("点位编号生成失败(历史编号格式异常)", I18nKeyConstant.POINT_NUMBER_PARSE_FAIL); } } public boolean getInTime(LocalTime now, Integer effectivePeriod, String effectiveTimePeriod){ // 2非工作日 0工作日 1节假日 3调休日 Integer holidayToday = holidayUtil.isHolidayToday(); if(effectivePeriod==1){ boolean inTime = TimeRangeChecker.isCurrentTimeInRanges(now,effectiveTimePeriod); if (!inTime) { return false; } }else if(effectivePeriod==2){ if(holidayToday!=0){ return false; } boolean inTime = TimeRangeChecker.isCurrentTimeInRanges(now,effectiveTimePeriod); if (!inTime) { return false; } }else if(effectivePeriod==3){ if(holidayToday!=2){ return false; } boolean inTime = TimeRangeChecker.isCurrentTimeInRanges(now,effectiveTimePeriod); if (!inTime) { return false; } }else if(effectivePeriod==4){ if(holidayToday!=1){ return false; } boolean inTime = TimeRangeChecker.isCurrentTimeInRanges(now,effectiveTimePeriod); if (!inTime) { return false; } } return true; } @Override public List pointMap(PointMapQuery query) { List list = this.baseMapper.pointMap(query); return list; } @Override public List listAll(PointListQuery query) { List list = this.baseMapper.listAll(query); for (XpPoint xpPoint : list) { String pointPictures = xpPoint.getPointPictures(); if (org.apache.commons.lang3.StringUtils.isNotBlank(pointPictures)) { JSONArray jsonArray = JSONArray.parseArray(pointPictures); for (int i = 0; i < jsonArray.size(); i++) { JSONObject jsonObject = jsonArray.getJSONObject(i); if (Objects.isNull(jsonObject)) { continue; } String downloadUrl = jsonObject.getString("url");; try { downloadUrl = aliOssService.getDownloadUrl(jsonObject.getString("id"), jsonObject.getString("name"), true); } catch (UnsupportedEncodingException e) { log.error("获取图片下载地址失败", e); } jsonObject.put("url", downloadUrl); } xpPoint.setPointPictures(JSONArray.toJSONString(jsonArray)); } } return list; } @Override public List getHistoryStartPoints(HistoryStartPointsDto historyStartPointsDto) { List xpPoints = this.baseMapper.selectBatchIds(historyStartPointsDto.getPointIds()); ArrayList resultList = new ArrayList<>(10); for (XpPoint xpPoint : xpPoints) { XpHistoryStartPointsVo xpHistoryStartPointsVo = new XpHistoryStartPointsVo(); xpHistoryStartPointsVo.setName(xpPoint.getPointName()); xpHistoryStartPointsVo.setAddress(xpPoint.getPointAddress()); xpHistoryStartPointsVo.setLatitude(xpPoint.getPointLat()); xpHistoryStartPointsVo.setLongitude(xpPoint.getPointLon()); xpHistoryStartPointsVo.setId(xpPoint.getId()); double distanced = FastDistanceCalculator.distanceFastKm(historyStartPointsDto.getLat(), historyStartPointsDto.getLon(), xpPoint.getPointLat(), xpPoint.getPointLon()); xpHistoryStartPointsVo.setDistance(distanced); xpHistoryStartPointsVo.setTime(xpPoint.getParkingTimeLimit()); xpHistoryStartPointsVo.setElectronicAreaId(xpPoint.getElectronicAreaId()); resultList.add(xpHistoryStartPointsVo); } return resultList; } @Override public List getHistoryEndPoints(HistoryEndPointsDto historyEndPointsDto) { List xpPoints = this.baseMapper.selectBatchIds(historyEndPointsDto.getPointIds()); ArrayList resultList = new ArrayList<>(10); for (XpPoint xpPoint : xpPoints) { XpHistoryEndPointsVo xpHistoryEndPointsVo = new XpHistoryEndPointsVo(); xpHistoryEndPointsVo.setName(xpPoint.getPointName()); xpHistoryEndPointsVo.setAddress(xpPoint.getPointAddress()); xpHistoryEndPointsVo.setLatitude(xpPoint.getPointLat()); xpHistoryEndPointsVo.setLongitude(xpPoint.getPointLon()); xpHistoryEndPointsVo.setId(xpPoint.getId()); double distanced = FastDistanceCalculator.distanceFastKm(historyEndPointsDto.getLat(), historyEndPointsDto.getLon(), xpPoint.getPointLat(), xpPoint.getPointLon()); xpHistoryEndPointsVo.setDistance(distanced); xpHistoryEndPointsVo.setTime(xpPoint.getParkingTimeLimit()); xpHistoryEndPointsVo.setElectronicAreaId(xpPoint.getElectronicAreaId()); resultList.add(xpHistoryEndPointsVo); } return resultList; } @Override public XpPointsSearchVo queryPointByName(PointByNameQuery query) { XpPointsSearchVo searchVo = new XpPointsSearchVo(); List list = this.baseMapper.queryPointByName(query); if (list.isEmpty()) { return searchVo; } LocalTime now = LocalTime.now(); for (XpPointsListVo xpPointsListVo : list) { boolean inTime = getInTime(now, xpPointsListVo.getEffectivePeriod(), xpPointsListVo.getEffectiveTimePeriod()); if (!inTime) { // 不在运营段 xpPointsListVo.setState(0); } } // 组装数据 并排序 剔除不在运营段的 数据 List resultList = list.stream().peek(vo -> vo.setDistance(FastDistanceCalculator.distanceFast(query.getLat(), query.getLon(), vo.getLatitude(), vo.getLongitude()))).filter(vo -> vo.getState() == 1).collect(Collectors.toList()); // 可用点位 List availablePoints = resultList.stream().filter(e -> query.getElectronicAreaId() != null && e.getElectronicAreaId().equals(query.getElectronicAreaId())).sorted(Comparator.comparing(XpPointsListVo::getDistance)).collect(Collectors.toList());// // 不可用点位 List unavailablePoints = resultList.stream().filter(e -> query.getElectronicAreaId() == null || !e.getElectronicAreaId().equals(query.getElectronicAreaId())).sorted(Comparator.comparing(XpPointsListVo::getDistance)).collect(Collectors.toList()); searchVo.setAvailablePoints(availablePoints); searchVo.setUnavailablePoints(unavailablePoints); return searchVo; } @Override public OrderDistanceVo getOrderDistance() { OrderDistanceVo orderDistanceVo = new OrderDistanceVo(); XpSystemSetting xpSystemSetting = xpSystemSettingMapper.selectOne(null); if (xpSystemSetting != null) { orderDistanceVo.setMaxDistance(xpSystemSetting.getOrderDistance()); orderDistanceVo.setMinDistance(xpSystemSetting.getMinOrderDistance()); orderDistanceVo.setAdsorptionDistance(xpSystemSetting.getAdsorptionDistance()); orderDistanceVo.setNearPointDistance(xpSystemSetting.getNearPointDistance()); return orderDistanceVo; } return orderDistanceVo; } @Override public PageInfo pageList(PointPageListListQuery query) { PageInfo pageInfo = new PageInfo<>(query.getPageNum(),query.getPageSize()); if (StringUtils.isNotBlank(query.getPointLonAndLat())) { List lonAndLat = query.getPointLonAndLat().trim().contains(",") ? Arrays.asList(query.getPointLonAndLat().split(",")) : query.getPointLonAndLat().trim().contains(" ") ? Arrays.asList(query.getPointLonAndLat().split(" ")) : Collections.emptyList(); if (CollUtil.isNotEmpty(lonAndLat)) { query.setPointLon(lonAndLat.get(0)); query.setPointLat(lonAndLat.get(1)); query.setPointLonAndLat(null); } } List list = this.baseMapper.pageList(pageInfo,query); pageInfo.setRecords(list); return pageInfo; } }