package com.ylx.common.utils; import cn.hutool.core.util.ObjectUtil; import java.math.BigDecimal; import java.math.RoundingMode; /** * Haversine公式计算两点经纬度球面直线距离工具类 * 所有坐标入参统一使用 BigDecimal,保证数据库高精度存储兼容 */ public final class DistanceUtil { // 地球平均半径,单位:米 private static final double EARTH_RADIUS_M = 6_371_000D; // 角度转弧度系数 private static final double TO_RADIANS = Math.PI / 180.0D; // 米转公里换算值 private static final BigDecimal METER_TO_KM = new BigDecimal("1000"); // 经纬度极值常量 private static final BigDecimal MAX_LAT = new BigDecimal("90"); private static final BigDecimal MAX_LON = new BigDecimal("180"); private static final BigDecimal ZERO = BigDecimal.ZERO; // 私有构造,禁止实例化工具类 private DistanceUtil() { throw new AssertionError("工具类不可实例化"); } // ===================== 对外业务方法 ===================== /** * 获取距离(单位:米,四舍五入整数字符串) * @param userLat 用户纬度 * @param userLon 用户经度 * @param shopLat 门店纬度 * @param shopLon 门店经度 * @return 有效距离返回米数字,坐标为空/0/超出范围 返回 "未知" */ public static String formatDistance(BigDecimal userLat, BigDecimal userLon, BigDecimal shopLat, BigDecimal shopLon) { if (!isAllCoordinateValid(userLat, userLon, shopLat, shopLon)) { return "未知"; } double meter = getDistance(userLat.doubleValue(), userLon.doubleValue(), shopLat.doubleValue(), shopLon.doubleValue()); return String.valueOf(Math.round(meter)); } /** * 获取距离(单位:公里,保留2位小数) * @param userLat 用户纬度 * @param userLon 用户经度 * @param shopLat 门店纬度 * @param shopLon 门店经度 * @return 公里字符串,坐标非法返回 "未知" */ public static BigDecimal formatDistanceInKilometers(BigDecimal userLat, BigDecimal userLon, BigDecimal shopLat, BigDecimal shopLon) { if (!isAllCoordinateValid(userLat, userLon, shopLat, shopLon)) { return null; } double meterVal = getDistance(userLat.doubleValue(), userLon.doubleValue(), shopLat.doubleValue(), shopLon.doubleValue()); BigDecimal meter = new BigDecimal(meterVal); return meter.divide(METER_TO_KM, 2, RoundingMode.HALF_UP); } /** * 前端友好展示距离 * 小于1000米:xx米;大于等于1000米:xx.xx公里;坐标异常:未知 */ public static String formatDisplay(BigDecimal userLat, BigDecimal userLon, BigDecimal shopLat, BigDecimal shopLon) { String meterStr = formatDistance(userLat, userLon, shopLat, shopLon); if ("未知".equals(meterStr)) { return "未知"; } long meter = Long.parseLong(meterStr); if (meter < METER_TO_KM.longValue()) { return meter + "米"; } BigDecimal meterNum = new BigDecimal(meter); BigDecimal km = meterNum.divide(METER_TO_KM, 2, RoundingMode.HALF_UP); return km + "公里"; } /** * 底层计算:返回两点球面距离(单位米) */ public static double getDistance(double lat1, double lon1, double lat2, double lon2) { double lat1Rad = lat1 * TO_RADIANS; double lon1Rad = lon1 * TO_RADIANS; double lat2Rad = lat2 * TO_RADIANS; double lon2Rad = lon2 * TO_RADIANS; double deltaLat = lat2Rad - lat1Rad; double deltaLon = lon2Rad - lon1Rad; double a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) + Math.cos(lat1Rad) * Math.cos(lat2Rad) * Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2); double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return EARTH_RADIUS_M * c; } // ===================== 坐标校验私有方法 ===================== /** * 批量校验四个坐标:非空、不等于0、经纬度范围合法 */ private static boolean isAllCoordinateValid(BigDecimal lat1, BigDecimal lon1, BigDecimal lat2, BigDecimal lon2) { // 任意坐标为null if (ObjectUtil.hasNull(lat1, lon1, lat2, lon2)) { return false; } // 任意坐标数值等于0 if (lat1.compareTo(ZERO) == 0 || lon1.compareTo(ZERO) == 0 || lat2.compareTo(ZERO) == 0 || lon2.compareTo(ZERO) == 0) { return false; } // 两组坐标分别校验范围 return isSingleCoordinateValid(lat1, lon1) && isSingleCoordinateValid(lat2, lon2); } /** * 校验单组经纬度范围:纬度[-90,90] 经度[-180,180] */ private static boolean isSingleCoordinateValid(BigDecimal lat, BigDecimal lon) { return lat.abs().compareTo(MAX_LAT) <= 0 && lon.abs().compareTo(MAX_LON) <= 0; } }