184 lines
6.2 KiB
JavaScript
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,
|
|
}
|