最近接到两个需求,一个是通过小程序扫码开门的,我这边主要就是根据用户定位判断用户离扫码店铺距离小于多少米的时候才可以调远程调开门接口,另外一个就是获取用户周围有哪些店铺。
需求很简单,就是根据定位获取的经度维度计算两个点之间的球面距离,这里我们主要采用Haversine公式来计算,据说这是目前比较精确用来计算地球上两个点之间距离的算法
直接上代码
/**
* @author Sakura
* @date 2024/8/30 10:28
*/
public class HaversineCalculator {
private static final double EARTH_RADIUS = 6371000; // 地球半径,单位:米
public static double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
// 将纬度和经度从度转换为弧度
double lat1Rad = Math.toRadians(lat1);
double lon1Rad = Math.toRadians(lon1);
double lat2Rad = Math.toRadians(lat2);
double lon2Rad = Math.toRadians(lon2);
// 计算纬度和经度差值
double deltaLat = lat2Rad - lat1Rad;
double deltaLon = lon2Rad - lon1Rad;
// 使用Haversine公式计算距离
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 * c;
}
public static void main(String[] args) {
// 示例:计算北京(纬度:39.9042,经度:116.4074)和上海(纬度:31.2304,经度:121.4737)之间的距离
double distance = calculateDistance(39.9042, 116.4074, 31.2304, 121.4737);
System.out.println("北京到上海的距离: " + distance + " 米");
}
}
根据这个我们只需要在获取到用户位置坐标后就可以计算出用户和店铺之间的距离,scanShopCodeParam 里面就是前端传过来的用户经纬度坐标
// 扫码的时候顺便开门,目前无开门程序入口,待完善
// 根据经度维度判断距离,距离小于500m才开门(注意这个距离可以根据定位工具的精确度进行调整,目前不知道定位精确度)
if (shop.getLongitude() != null && shop.getLatitude() != null && scanShopCodeParam.getLongitude() != null
&& scanShopCodeParam.getLatitude() != null) {
// 使用Haversine公式计算两个坐标的球面距离
double distance = HaversineCalculator.calculateDistance(shop.getLatitude().doubleValue(),
shop.getLongitude().doubleValue(), scanShopCodeParam.getLatitude().doubleValue(),
scanShopCodeParam.getLongitude().doubleValue());
log.info("当前开门距离为:" + distance);
// 在这里面调用开门程序即可
if (distance < 500) {
log.info("执行开门程序+++++++++++++++++++++");
}
}
接下来我们看一下怎么获取用户周围的店铺,首先店铺信息肯定已经在数据库了,并且已经有了定位的经纬度数据,我们要做的就是获取当前用户的定位信息然后拿去和数据库里店铺坐标比较,获取周围多少米之内的店铺信息返回即可
我们的店铺信息里面有经度和维度两个信息
然后直接看 SQL
SELECT shop_no,
TRUNCATE(
6371000 * ACOS(
LEAST(1, GREATEST(-1,
COS(RADIANS(22.145874)) * COS(RADIANS(latitude)) *
COS(RADIANS(longitude) - RADIANS(113.876414)) +
SIN(RADIANS(22.145874)) * SIN(RADIANS(latitude))
))
), 0) AS distance
FROM
qy_shop;
看下执行结果,可以看到应该大差不差,毕竟我也没有实际量过,数据也是随手写的,准不准等后面上线了有真实数据我再更新一下
接着我们就给它加上条件,获取10公里内的店铺
SELECT shop_no,
TRUNCATE(
6371000 * ACOS(
LEAST(1, GREATEST(-1,
COS(RADIANS(22.145874)) * COS(RADIANS(latitude)) *
COS(RADIANS(longitude) - RADIANS(113.876414)) +
SIN(RADIANS(22.145874)) * SIN(RADIANS(latitude))
))
), 0) AS distance
FROM
qy_shop
WHERE
6371000 * ACOS(
LEAST(1, GREATEST(-1,
COS(RADIANS(22.145874)) * COS(RADIANS(latitude)) *
COS(RADIANS(longitude) - RADIANS(113.876414)) +
SIN(RADIANS(22.145874)) * SIN(RADIANS(latitude))
))
) <= 10000
ORDER BY distance;
看下查询结果,有两个店铺,应该差不多
把这个封装到代码里面,这里只写 mapper.xml,其它的都是业务逻辑
<select id="getAroundShopList" resultType="com.yike.user.vo.ShopVo">
SELECT shop_no, shop_name, logo_url, contact, mobile, prov_code,
city_code, dist_code, address, longitude, latitude, create_time,
TRUNCATE(6371000 * ACOS(
LEAST(1, GREATEST(-1,
COS(RADIANS(#{param.latitude,jdbcType=DECIMAL})) * COS(RADIANS(latitude)) *
COS(RADIANS(longitude) - RADIANS(#{param.longitude,jdbcType=DECIMAL})) +
SIN(RADIANS(#{param.latitude,jdbcType=DECIMAL})) * SIN(RADIANS(latitude))))
), 0) AS distance
FROM
qy_shop
<where>
6371000 * ACOS(
LEAST(1, GREATEST(-1,
COS(RADIANS(#{param.latitude,jdbcType=DECIMAL})) * COS(RADIANS(latitude)) *
COS(RADIANS(longitude) - RADIANS(#{param.longitude,jdbcType=DECIMAL})) +
SIN(RADIANS(#{param.latitude,jdbcType=DECIMAL})) * SIN(RADIANS(latitude))
))) <= #{param.distance,jdbcType=INTEGER}
<if test="param.keyword != null and param.keyword != ''">
AND shop_name LIKE CONCAT('%',#{param.keyword,jdbcType=VARCHAR},'%')
</if>
</where>
ORDER BY distance;
</select>
当然为了证明数据库和代码里面计算结果是否一致我还是测试了一下的
从下面结果可以看出代码计算的和SQL计算的结果是一致的 (小数点后面的忽略啊,我数据库里面计算是去掉了小数点后面的值的)