Parcourir la source

feat(location): 实现 WGS84 到 GCJ02 坐标转换,优化定位功能

xieyong il y a 15 heures
Parent
commit
2bf2d19dc8
1 fichiers modifiés avec 133 ajouts et 21 suppressions
  1. 133 21
      utils/hr/attendance.js

+ 133 - 21
utils/hr/attendance.js

@@ -421,19 +421,136 @@ export function normalizeAttendanceRule(rule) {
 }
 
 // ============ 定位 ============
-export function getLocation(options) {
-  const timeout = (options && options.timeout) || 10000;
+// 后端「打卡范围 / 围栏」用的是 GCJ02(高德/腾讯地图上选的点),
+// 因此各端最终提交的坐标都统一到 GCJ02。
+const GCJ02_A = 6378245.0; // 克拉索夫斯基椭球长半轴
+const GCJ02_EE = 0.00669342162296594323; // 偏心率平方
+const PI = Math.PI;
+
+function outOfChina(lat, lng) {
+  return lng < 72.004 || lng > 137.8347 || lat < 0.8293 || lat > 55.8271;
+}
 
-  // #ifdef H5
+function transformLat(x, y) {
+  let ret =
+    -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
+  ret += ((20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0) / 3.0;
+  ret += ((20.0 * Math.sin(y * PI) + 40.0 * Math.sin((y / 3.0) * PI)) * 2.0) / 3.0;
+  ret += ((160.0 * Math.sin((y / 12.0) * PI) + 320 * Math.sin((y * PI) / 30.0)) * 2.0) / 3.0;
+  return ret;
+}
+
+function transformLng(x, y) {
+  let ret =
+    300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
+  ret += ((20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0) / 3.0;
+  ret += ((20.0 * Math.sin(x * PI) + 40.0 * Math.sin((x / 3.0) * PI)) * 2.0) / 3.0;
+  ret += ((150.0 * Math.sin((x / 12.0) * PI) + 300.0 * Math.sin((x / 30.0) * PI)) * 2.0) / 3.0;
+  return ret;
+}
+
+/**
+ * WGS84(gps 原始坐标)→ GCJ02(国测局坐标)
+ * App 端只集成系统定位时只能拿到 wgs84(系统定位仅支持 wgs84),
+ * 由前端转换成 gcj02,保证与后端围栏、后台地图是同一坐标系。
+ * 境外坐标不做转换。
+ */
+export function wgs84ToGcj02(latitude, longitude) {
+  const lat = Number(latitude);
+  const lng = Number(longitude);
+  if (!isFinite(lat) || !isFinite(lng)) return null;
+  if (outOfChina(lat, lng)) return { latitude: lat, longitude: lng };
+  let dLat = transformLat(lng - 105.0, lat - 35.0);
+  let dLng = transformLng(lng - 105.0, lat - 35.0);
+  const radLat = (lat / 180.0) * PI;
+  let magic = Math.sin(radLat);
+  magic = 1 - GCJ02_EE * magic * magic;
+  const sqrtMagic = Math.sqrt(magic);
+  dLat = (dLat * 180.0) / (((GCJ02_A * (1 - GCJ02_EE)) / (magic * sqrtMagic)) * PI);
+  dLng = (dLng * 180.0) / ((GCJ02_A / sqrtMagic) * Math.cos(radLat) * PI);
+  return { latitude: lat + dLat, longitude: lng + dLng };
+}
+
+/** uni.getLocation 包装,统一返回 { latitude, longitude, accuracy, address } */
+function requestUniLocation(type) {
+  return new Promise((resolve, reject) => {
+    uni.getLocation({
+      type,
+      geocode: false,
+      success: (res) =>
+        resolve({
+          latitude: res.latitude,
+          longitude: res.longitude,
+          accuracy: res.accuracy,
+          address: res.address,
+        }),
+      fail: (err) => reject(err),
+    });
+  });
+}
+
+/** 把平台原始 errMsg 翻译成人话 */
+function formatLocationError(err) {
+  const raw = String((err && (err.errMsg || err.message)) || "");
+  if (/not support gcj02/i.test(raw)) return "当前安装包不支持 gcj02 定位,请更新后重试";
+  if (/auth deny|auth denied|authorize|permission denied/i.test(raw))
+    return "未获得定位权限,请在手机设置中允许定位后重试";
+  if (/timeout/i.test(raw)) return "定位超时,请到开阔区域后重试";
+  if (/location switch off|system permission|ERROR_NOCELL/i.test(raw))
+    return "手机定位服务未开启,请打开系统定位后重试";
+  console.warn("[考勤] 定位失败:", raw);
+  return "获取位置失败,请稍后重试";
+}
+
+/** 是否「当前安装包没有 gcj02 能力」:App 未集成三方定位 SDK / 小程序未认证备案 */
+function isGcj02Unsupported(err) {
+  const raw = String((err && (err.errMsg || err.message)) || "");
+  const code = err && (err.errCode != null ? err.errCode : err.code);
+  return /not support gcj02/i.test(raw) || Number(code) === 18;
+}
+
+/**
+ * App / 小程序端定位:
+ * 1. 优先取 gcj02(微信小程序原生支持;App 端需要集成高德/腾讯定位模块,
+ *    正式包若只勾了「系统定位」会返回 getLocation:fail not support gcj02);
+ * 2. 当前包不支持 gcj02 时,回退取 wgs84 原始坐标,再在前端转成 gcj02 提交。
+ */
+async function getNativeLocation() {
+  try {
+    return await requestUniLocation("gcj02");
+  } catch (err) {
+    if (!isGcj02Unsupported(err)) throw new Error(formatLocationError(err));
+    let raw = null;
+    try {
+      raw = await requestUniLocation("wgs84");
+    } catch (fallbackErr) {
+      throw new Error(formatLocationError(fallbackErr));
+    }
+    const converted = wgs84ToGcj02(raw.latitude, raw.longitude);
+    console.warn("[考勤] 当前安装包不支持 gcj02,已回退 wgs84 并转换为 gcj02");
+    if (!converted) return raw;
+    return {
+      latitude: converted.latitude,
+      longitude: converted.longitude,
+      accuracy: raw.accuracy,
+      address: raw.address,
+    };
+  }
+}
+
+/** H5 端:浏览器定位给的是 wgs84,转成 gcj02 与后端围栏保持一致 */
+function getBrowserLocation(timeout) {
   if (typeof navigator !== "undefined" && navigator.geolocation) {
     return new Promise((resolve, reject) => {
       navigator.geolocation.getCurrentPosition(
-        (pos) =>
+        (pos) => {
+          const converted = wgs84ToGcj02(pos.coords.latitude, pos.coords.longitude);
           resolve({
-            latitude: pos.coords.latitude,
-            longitude: pos.coords.longitude,
+            latitude: converted ? converted.latitude : pos.coords.latitude,
+            longitude: converted ? converted.longitude : pos.coords.longitude,
             accuracy: pos.coords.accuracy,
-          }),
+          });
+        },
         (err) => {
           const msg =
             err && err.code === 1
@@ -446,21 +563,16 @@ export function getLocation(options) {
     });
   }
   return Promise.reject(new Error("当前环境不支持定位"));
+}
+
+export function getLocation(options) {
+  const timeout = (options && options.timeout) || 10000;
+
+  // #ifdef H5
+  return getBrowserLocation(timeout);
   // #endif
 
-  // #ifdef MP-WEIXIN || APP-PLUS
-  return new Promise((resolve, reject) => {
-    uni.getLocation({
-      type: "gcj02",
-      geocode: false,
-      success: (res) =>
-        resolve({
-          latitude: res.latitude,
-          longitude: res.longitude,
-          accuracy: res.accuracy,
-        }),
-      fail: (err) => reject(new Error((err && err.errMsg) || "获取位置失败")),
-    });
-  });
+  // #ifndef H5
+  return getNativeLocation();
   // #endif
 }