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, }