checkinLocation.js 3.86 KB
import Taro from '@tarojs/taro'

const toRadians = degree => (degree * Math.PI) / 180

/**
 * @description 计算两个经纬度之间的距离(米)
 * @param {Object} start
 * @param {number|string} start.lng
 * @param {number|string} start.lat
 * @param {Object} end
 * @param {number|string} end.lng
 * @param {number|string} end.lat
 * @returns {number}
 */
export const calculateDistanceMeters = (start, end) => {
  const startLng = Number(start?.lng)
  const startLat = Number(start?.lat)
  const endLng = Number(end?.lng)
  const endLat = Number(end?.lat)

  if (![startLng, startLat, endLng, endLat].every(Number.isFinite)) {
    return NaN
  }

  const earthRadius = 6371000
  const deltaLat = toRadians(endLat - startLat)
  const deltaLng = toRadians(endLng - startLng)

  const a =
    Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
    Math.cos(toRadians(startLat)) *
      Math.cos(toRadians(endLat)) *
      Math.sin(deltaLng / 2) *
      Math.sin(deltaLng / 2)

  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))

  return earthRadius * c
}

/**
 * @description 判断当前位置是否在扫码打卡允许范围内
 * @param {Object} params
 * @param {boolean} params.geoEnabled
 * @param {number|string} params.userLng
 * @param {number|string} params.userLat
 * @param {number|string} params.centerLng
 * @param {number|string} params.centerLat
 * @param {number|string} params.radiusMeters
 * @returns {{allowed:boolean,distance:number,reason:string}}
 */
export const checkCheckinRange = (params = {}) => {
  const { geoEnabled, userLng, userLat, centerLng, centerLat, radiusMeters } = params

  if (geoEnabled !== true) {
    return {
      allowed: true,
      distance: 0,
      reason: 'geo_disabled',
    }
  }

  const distance = calculateDistanceMeters(
    { lng: userLng, lat: userLat },
    { lng: centerLng, lat: centerLat }
  )

  if (!Number.isFinite(distance)) {
    return {
      allowed: false,
      distance: NaN,
      reason: 'invalid_location',
    }
  }

  const rangeLimit = Number(radiusMeters)

  if (!Number.isFinite(rangeLimit) || rangeLimit < 0) {
    return {
      allowed: false,
      distance,
      reason: 'invalid_radius',
    }
  }

  return {
    allowed: distance <= rangeLimit,
    distance,
    reason: distance <= rangeLimit ? 'within_range' : 'out_of_range',
  }
}

/**
 * @description 静默获取当前位置并校验是否在扫码打卡范围内
 * @param {Object} params
 * @param {boolean} params.geoEnabled
 * @param {number|string} params.centerLng
 * @param {number|string} params.centerLat
 * @param {number|string} params.radiusMeters
 * @returns {Promise<{allowed:boolean,distance:number,reason:string,location?:{lng:number,lat:number}}>}
 */
export const verifyCheckinRangeWithCurrentLocation = async (params = {}) => {
  const { geoEnabled, centerLng, centerLat, radiusMeters } = params

  // 未开启地理围栏时直接放行,避免页面层重复写同样的短路判断。
  if (geoEnabled !== true) {
    return {
      allowed: true,
      distance: 0,
      reason: 'geo_disabled',
    }
  }

  try {
    // 重新拉取当前位置而不是复用缓存,确保扫码瞬间的位置判断更接近真实场景。
    const location = await Taro.getLocation({
      type: 'gcj02',
      altitude: false,
      isHighAccuracy: true,
      highAccuracyExpireTime: 4000,
    })

    const normalizedLocation = {
      lng: location.longitude,
      lat: location.latitude,
    }

    const rangeResult = checkCheckinRange({
      geoEnabled,
      userLng: normalizedLocation.lng,
      userLat: normalizedLocation.lat,
      centerLng,
      centerLat,
      radiusMeters,
    })

    return {
      ...rangeResult,
      location: normalizedLocation,
    }
  } catch (error) {
    console.error('获取扫码打卡位置失败:', error)
    return {
      allowed: false,
      distance: NaN,
      reason: 'location_fetch_failed',
    }
  }
}