JhHardwareWRS/utils/locationGuard.js
2026-06-24 15:19:14 +08:00

184 lines
6.2 KiB
JavaScript

const api = require('./api')
const EARTH_RADIUS_METERS = 6371000
const LOCATION_RETRY_COUNT = 3
const LOCATION_RETRY_DELAY_MS = 600
const isValidNumber = value => typeof value === 'number' && Number.isFinite(value)
const toRadians = degrees => (degrees * Math.PI) / 180
const sleep = ms => new Promise(resolve => {
setTimeout(resolve, ms)
})
const distanceMeters = (from, to) => {
const lat1 = toRadians(from.latitude)
const lat2 = toRadians(to.latitude)
const deltaLat = toRadians(to.latitude - from.latitude)
const deltaLng = toRadians(to.longitude - from.longitude)
const a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2)
+ Math.cos(lat1) * Math.cos(lat2)
* Math.sin(deltaLng / 2) * Math.sin(deltaLng / 2)
return 2 * EARTH_RADIUS_METERS * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
}
const normalizePoint = point => {
const latitudeValue = point.latitude
const longitudeValue = point.longitude
const latitude = latitudeValue === '' || latitudeValue === null || latitudeValue === undefined
? NaN
: Number(latitudeValue)
const longitude = longitudeValue === '' || longitudeValue === null || longitudeValue === undefined
? NaN
: Number(longitudeValue)
return {
name: point.name || '',
latitude,
longitude,
radiusMeters: Number(point.radiusMeters || 500),
}
}
const getFactoryConfigs = async (scope = 'accessible') => {
const source = scope === 'public'
? await api.listPublicAttendancePoints()
: await api.listAccessibleAttendancePoints()
const points = source.map(normalizePoint)
const validPoints = points.filter(point => (
point.name
&& isValidNumber(point.latitude)
&& isValidNumber(point.longitude)
&& Number.isFinite(point.radiusMeters)
&& point.radiusMeters > 0
))
if (!validPoints.length) {
throw new Error('未配置考勤点经纬度,请经理先在考勤设置中配置')
}
return validPoints
}
const getCurrentLocation = () => new Promise((resolve, reject) => {
wx.getLocation({
type: 'gcj02',
isHighAccuracy: true,
highAccuracyExpireTime: 8000,
success: resolve,
fail: error => {
const message = error && error.errMsg ? error.errMsg : ''
if (message.indexOf('auth deny') >= 0 || message.indexOf('authorize no response') >= 0) {
reject(new Error('请允许获取位置信息后再扫码报工'))
return
}
reject(new Error('获取当前位置失败,请确认已开启定位权限'))
},
})
})
const locationAccuracyText = location => {
const accuracy = Number(location && location.accuracy)
return Number.isFinite(accuracy) && accuracy > 0 ? `,定位精度约${Math.round(accuracy)}` : ''
}
const evaluateLocation = (current, factories, attendancePointName = '') => {
const targetName = String(attendancePointName || '').trim()
const matches = factories
.map(factory => ({
current,
factory,
distance: distanceMeters(current, factory),
}))
.sort((a, b) => a.distance - b.distance)
const targetMatch = targetName
? matches.find(item => item.factory.name === targetName)
: null
const matched = targetMatch
? (targetMatch.distance <= targetMatch.factory.radiusMeters ? targetMatch : null)
: matches.find(item => item.distance <= item.factory.radiusMeters)
return matched || {
current,
factory: null,
distance: (targetMatch || matches[0]) ? (targetMatch || matches[0]).distance : 0,
nearest: targetMatch || matches[0] || null,
}
}
const getCurrentAttendancePoint = async (scope = 'accessible', attendancePointName = '') => {
const factories = await getFactoryConfigs(scope)
const attempts = []
let lastError = null
for (let index = 0; index < LOCATION_RETRY_COUNT; index += 1) {
try {
const current = await getCurrentLocation()
if (isValidNumber(current.latitude) && isValidNumber(current.longitude)) {
const result = evaluateLocation(current, factories, attendancePointName)
attempts.push(result)
if (result.factory) {
return {
...result,
sampleCount: attempts.length,
}
}
}
} catch (error) {
lastError = error
}
if (index < LOCATION_RETRY_COUNT - 1) {
await sleep(LOCATION_RETRY_DELAY_MS)
}
}
if (!attempts.length && lastError) {
throw lastError
}
const best = attempts.sort((a, b) => (a.distance || 0) - (b.distance || 0))[0]
return {
...(best || { current: null, factory: null, distance: 0, nearest: null }),
sampleCount: attempts.length,
}
}
const ensureFactoryLocation = async (attendancePointName = '', scope = 'accessible') => {
const result = await getCurrentAttendancePoint(scope, attendancePointName)
if (!result.factory) {
const nearest = result.nearest
if (nearest && nearest.factory) {
throw new Error(`已多次尝试定位,当前位置距离考勤点【${nearest.factory.name}】约${Math.round(nearest.distance)}米,超过${nearest.factory.radiusMeters}米范围${locationAccuracyText(result.current)},不能扫码报工`)
}
throw new Error('当前位置未进入任何考勤点,不能扫码报工')
}
if (attendancePointName && result.factory.name !== attendancePointName) {
throw new Error(`当前位置已进入【${result.factory.name}】考勤点,不是该二维码所属的【${attendancePointName}】考勤点`)
}
return {
current: result.current,
factory: result.factory,
distance: result.distance,
}
}
const ensureTemporaryWorkerLocation = async () => {
const result = await getCurrentAttendancePoint('public')
if (!result.factory) {
const nearest = result.nearest
if (nearest && nearest.factory) {
throw new Error(`已多次尝试定位,当前位置距离最近考勤点【${nearest.factory.name}】约${Math.round(nearest.distance)}米,超过${nearest.factory.radiusMeters}米范围${locationAccuracyText(result.current)},不能创建临时工`)
}
throw new Error('当前位置未进入任何考勤点,不能创建临时工')
}
return {
current: result.current,
factory: result.factory,
distance: result.distance,
}
}
module.exports = {
ensureFactoryLocation,
ensureTemporaryWorkerLocation,
getCurrentAttendancePoint,
distanceMeters,
}