电速宝
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

app.js 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. // app.js
  2. const api = require('./api/index.js'); // 注意路径
  3. App({
  4. globalData: {
  5. socketTask: null,
  6. isConnected: false,
  7. reconnectTimer: null,
  8. heartbeatTimer: null,
  9. currentWorkorderId: '',
  10. userInfo: wx.getStorageSync('user') || {}
  11. },
  12. onLaunch() {
  13. // wx.setStorageSync('user', { userId: 'driver_1001', operationRole: 4 });
  14. this.initWebSocketService();
  15. wx.onAppShow(() => this.handleAppShow());
  16. wx.onAppHide(() => this.handleAppHide());
  17. this.locationService = {
  18. isTracking: false,
  19. currentWorkorderId: '', // 存储当前跟踪的工单ID
  20. isLocationUpdateStarted: false // 标记是否已启动定位服务
  21. };
  22. },
  23. initWebSocketService() {
  24. if (!this.wsService) {
  25. this.wsService = {
  26. connect: (url) => {
  27. const that = this;
  28. if (that.globalData.socketTask) {
  29. that.wsService.close();
  30. }
  31. console.log('正在连接WebSocket:', url);
  32. const socketTask = wx.connectSocket({ url, header: { 'content-type': 'application/json' } });
  33. socketTask.onOpen(() => {
  34. console.log('WebSocket 连接成功');
  35. that.globalData.isConnected = true;
  36. that.globalData.socketTask = socketTask;
  37. that.clearReconnectTimer();
  38. // that.startHeartbeat();
  39. if (that.globalData.currentWorkorderId) {
  40. that.wsService.send({ type: 'subscribe', workorderId: that.globalData.currentWorkorderId });
  41. }
  42. wx.$emit('socketOpen');
  43. });
  44. socketTask.onMessage((res) => {
  45. try {
  46. console.log(res);
  47. const message = JSON.parse(res.data);
  48. console.log('收到消息:', message);
  49. wx.$emit('wsMessage', message);
  50. } catch (err) { console.error('消息解析失败', err); }
  51. });
  52. socketTask.onClose((res) => {
  53. console.log('WebSocket 连接关闭, 状态码:', res.code);
  54. that.globalData.isConnected = false;
  55. that.globalData.socketTask = null;
  56. that.stopHeartbeat();
  57. wx.$emit('socketClose');
  58. if (res.code !== 1000) { that.startReconnect(); }
  59. });
  60. socketTask.onError((err) => {
  61. console.error('WebSocket 连接错误:', err);
  62. that.globalData.isConnected = false;
  63. that.globalData.socketTask = null;
  64. that.stopHeartbeat();
  65. wx.$emit('socketError', err);
  66. });
  67. },
  68. send: (data) => {
  69. const that = this;
  70. if (that.globalData.isConnected && that.globalData.socketTask) {
  71. try {
  72. that.globalData.socketTask.send({
  73. data: JSON.stringify(data),
  74. fail: (err) => {
  75. console.error('消息发送失败:', err);
  76. wx.showToast({ title: '消息发送失败', icon: 'none' });
  77. }
  78. });
  79. } catch (e) { console.error('消息序列化失败:', e); }
  80. } else {
  81. console.warn('WebSocket 未连接');
  82. wx.showToast({ title: '网络连接中...', icon: 'none' });
  83. }
  84. },
  85. close: (code = 1000, reason = 'normal close') => {
  86. const that = this;
  87. if (that.globalData.socketTask) {
  88. console.log(`关闭WebSocket连接: ${code} - ${reason}`);
  89. that.globalData.socketTask.close({ code, reason });
  90. }
  91. }
  92. };
  93. }
  94. },
  95. handleAppShow() {
  96. const that = this;
  97. console.log(!that.globalData.isConnected);
  98. console.log(!that.globalData.reconnectTimer);
  99. if (!that.globalData.isConnected && !that.globalData.reconnectTimer) {
  100. const { operationId, operationRole } = that.globalData.userInfo;
  101. console.log(operationRole);
  102. // 修复:startReconnect 中用了 operationId,这里统一字段
  103. if (!operationId) {
  104. console.error('用户operationId不存在,无法连接WebSocket');
  105. return;
  106. }
  107. const wsUrl = `wss://esos-iot.com:9443/communication/update/${operationId}`;
  108. that.wsService.connect(wsUrl);
  109. }
  110. },
  111. handleAppHide() {
  112. // 可以选择在后台关闭连接
  113. // this.wsService.close(1001, 'app enter background');
  114. },
  115. startHeartbeat() {
  116. const that = this;
  117. that.stopHeartbeat();
  118. that.heartbeatTimer = setInterval(() => {
  119. if (that.globalData.isConnected) {
  120. that.wsService.send({ type: 'heartbeat', timestamp: Date.now() });
  121. }
  122. }, 5000);
  123. },
  124. stopHeartbeat() {
  125. if (this.heartbeatTimer) {
  126. clearInterval(this.heartbeatTimer);
  127. this.heartbeatTimer = null;
  128. }
  129. },
  130. startReconnect() {
  131. const that = this;
  132. that.clearReconnectTimer();
  133. let delay = 1000;
  134. that.globalData.reconnectTimer = setInterval(() => {
  135. if (!that.globalData.isConnected) {
  136. console.log(`尝试重连 in ${delay}ms...`);
  137. const { operationId, operationRole } = that.globalData.userInfo; // 修复:用 operationId 而非 userId
  138. if (operationId) {
  139. const wsUrl = `wss://esos-iot.com:9443/communication/update/${operationId}`;
  140. that.wsService.connect(wsUrl);
  141. }
  142. delay = Math.min(delay * 2, 8000);
  143. } else {
  144. that.clearReconnectTimer();
  145. }
  146. }, delay);
  147. },
  148. clearReconnectTimer() {
  149. if (this.globalData.reconnectTimer) {
  150. clearInterval(this.globalData.reconnectTimer);
  151. this.globalData.reconnectTimer = null;
  152. }
  153. },
  154. /**
  155. * 司机端开始后台定位并上传(单工单)
  156. * @param {string} workorderId - 当前工单ID
  157. */
  158. startDriverLocationUpload(workorderId) {
  159. const service = this.locationService;
  160. // 入参校验
  161. if (!workorderId) {
  162. console.error('工单ID不能为空');
  163. wx.showToast({ title: '工单ID异常', icon: 'none' });
  164. return;
  165. }
  166. // 如果已经在为当前工单上传,则忽略
  167. if (service.isTracking && service.currentWorkorderId === workorderId) {
  168. console.log(`已经在为工单 ${workorderId} 上传位置`);
  169. return;
  170. }
  171. // 如果已经在为其他工单上传,先停止
  172. if (service.isTracking && service.currentWorkorderId !== workorderId) {
  173. console.log(`当前正在为工单 ${service.currentWorkorderId} 上传,切换到 ${workorderId},先停止旧定位`);
  174. this.stopDriverLocationUpload();
  175. }
  176. // 权限校验
  177. wx.getSetting({
  178. success: (res) => {
  179. if (!res.authSetting['scope.userLocationBackground']) {
  180. wx.authorize({
  181. scope: 'scope.userLocationBackground',
  182. success: () => {
  183. this._doStartLocationUpload(workorderId, service);
  184. },
  185. fail: () => {
  186. wx.showModal({
  187. title: '权限不足',
  188. content: '需要开启后台定位权限才能实时上传位置,请前往设置页开启',
  189. confirmText: '去设置',
  190. success: (modalRes) => {
  191. if (modalRes.confirm) {
  192. wx.openSetting({
  193. success: (settingRes) => {
  194. if (settingRes.authSetting['scope.userLocationBackground']) {
  195. this._doStartLocationUpload(workorderId, service);
  196. }
  197. }
  198. });
  199. }
  200. }
  201. });
  202. }
  203. });
  204. } else {
  205. this._doStartLocationUpload(workorderId, service);
  206. }
  207. }
  208. });
  209. },
  210. /**
  211. * 私有方法:执行真正的定位启动逻辑(核心修复:兼容低版本,改用 wx.onLocationChange)
  212. */
  213. _doStartLocationUpload(workorderId, service) {
  214. console.log(`开始为工单 ${workorderId} 启动后台定位上传...`);
  215. service.currentWorkorderId = workorderId;
  216. // 修复1:先移除旧的定位监听(防止重复监听)
  217. this._removeLocationListener();
  218. // 修复2:启动后台定位(兼容低版本,用 startLocationUpdate 兜底)
  219. const startLocationApi = wx.startLocationUpdateBackground || wx.startLocationUpdate;
  220. startLocationApi({
  221. type: 'gcj02',
  222. success: (res) => {
  223. console.log('定位服务已启动', res);
  224. service.isLocationUpdateStarted = true;
  225. // 存储上一次的定位信息
  226. let lastLocation = null;
  227. // 距离阈值(单位:米,根据需求调整,5米)
  228. const DISTANCE_THRESHOLD = 5;
  229. // 修复3:改用低版本支持的 wx.onLocationChange(替代 wx.watchPosition)
  230. wx.onLocationChange((location) => {
  231. if (!lastLocation) {
  232. lastLocation = location;
  233. console.log('位置变化,准备上传:', location);
  234. this._uploadLocation(location, workorderId);
  235. return;
  236. }
  237. // 计算当前定位与上一次的直线距离(Haversine公式)
  238. const distance = this.calculateDistance(
  239. lastLocation.latitude,
  240. lastLocation.longitude,
  241. location.latitude,
  242. location.longitude
  243. );
  244. console.log(distance);
  245. // 只有距离超过阈值,才认为是“有效变化”
  246. if (distance >= DISTANCE_THRESHOLD) {
  247. lastLocation = location; // 更新上一次定位
  248. this._uploadLocation(location, workorderId);
  249. }
  250. });
  251. // 标记定位中状态
  252. service.isTracking = true;
  253. console.log('位置监听已注册,开始实时上传');
  254. },
  255. fail: (err) => {
  256. let errMsg = '启动定位失败';
  257. if (err.errMsg.includes('auth deny')) errMsg = '定位权限被拒绝';
  258. else if (err.errMsg.includes('system')) errMsg = '系统不支持定位';
  259. console.error(errMsg, err);
  260. wx.showToast({ title: errMsg, icon: 'none', duration: 2000 });
  261. }
  262. });
  263. },
  264. // Haversine公式:计算两点经纬度之间的直线距离(单位:米)
  265. calculateDistance(lat1, lng1, lat2, lng2) {
  266. const R = 6371000; // 地球半径(米)
  267. const radLat1 = (lat1 * Math.PI) / 180;
  268. const radLat2 = (lat2 * Math.PI) / 180;
  269. const deltaLat = radLat2 - radLat1;
  270. const deltaLng = (lng2 - lng1) * Math.PI / 180;
  271. const a =
  272. Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
  273. Math.cos(radLat1) * Math.cos(radLat2) *
  274. Math.sin(deltaLng / 2) * Math.sin(deltaLng / 2);
  275. const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  276. return R * c; // 距离(米)
  277. },
  278. /**
  279. * 司机端停止定位上传(核心修复:对应移除 wx.onLocationChange 监听)
  280. */
  281. stopDriverLocationUpload() {
  282. const service = this.locationService;
  283. if (!service.isTracking) {
  284. return;
  285. }
  286. console.log(`停止为工单 ${service.currentWorkorderId} 上传位置`);
  287. // 1. 移除位置监听(wx.onLocationChange 对应 wx.offLocationChange)
  288. this._removeLocationListener();
  289. // 2. 停止定位服务(兼容低版本,用 stopLocationUpdate 兜底)
  290. this._stopLocationService();
  291. // 3. 重置状态
  292. service.isTracking = false;
  293. service.currentWorkorderId = '';
  294. service.isLocationUpdateStarted = false;
  295. },
  296. /**
  297. * 私有方法:移除位置监听(兼容低版本)
  298. */
  299. _removeLocationListener() {
  300. if (wx.offLocationChange) {
  301. wx.offLocationChange();
  302. console.log('位置监听已移除');
  303. } else {
  304. console.warn('当前基础库不支持 wx.offLocationChange,可能存在重复监听风险');
  305. }
  306. },
  307. /**
  308. * 私有方法:统一停止定位服务(兼容低版本)
  309. */
  310. _stopLocationService() {
  311. const stopLocationApi = wx.stopLocationUpdateBackground || wx.stopLocationUpdate;
  312. if (stopLocationApi && this.locationService.isLocationUpdateStarted) {
  313. stopLocationApi({
  314. success: (res) => {
  315. console.log('定位服务已停止', res);
  316. },
  317. fail: (err) => {
  318. console.error('停止定位服务失败:', err);
  319. }
  320. });
  321. }
  322. },
  323. /**
  324. * 私有方法:上传位置到服务器(无改动)
  325. */
  326. _uploadLocation(location, workorderId, retryCount = 0) {
  327. const maxRetry = 2;
  328. const isLatValid = location.latitude >= 3.86 && location.latitude <= 53.55;
  329. const isLngValid = location.longitude >= 73.66 && location.longitude <= 135.05;
  330. if (!isLatValid || !isLngValid) {
  331. console.warn(`无效位置信息(工单: ${workorderId}),跳过上传`, location);
  332. return;
  333. }
  334. const data = {
  335. workorderId: workorderId,
  336. latitude: location.latitude,
  337. longitude: location.longitude,
  338. createTime: Date.now(),
  339. accuracy: location.accuracy
  340. };
  341. api.request(`/sysworkorder/insercoordinateredis`, 'post', data, { isPublic: false })
  342. .then((response) => {
  343. if (response.code !== 200) {
  344. throw new Error(`响应码异常: ${response.code}`);
  345. }
  346. console.log(`位置上传成功 (工单: ${workorderId})`, data);
  347. })
  348. .catch((err) => {
  349. console.error(`位置上传失败(第${retryCount+1}次)(工单: ${workorderId})`, err);
  350. if (retryCount < maxRetry) {
  351. setTimeout(() => {
  352. this._uploadLocation(location, workorderId, retryCount + 1);
  353. }, 10000 * (retryCount + 1));
  354. } else {
  355. wx.showToast({ title: '位置上传失败,请检查网络', icon: 'none' });
  356. }
  357. });
  358. },
  359. });
  360. // 全局事件总线
  361. wx.$on = function (eventName, callback) {
  362. if (!this.$events) this.$events = {};
  363. if (!this.$events[eventName]) this.$events[eventName] = [];
  364. this.$events[eventName].push(callback);
  365. };
  366. wx.$emit = function (eventName, data) {
  367. if (!this.$events || !this.$events[eventName]) return;
  368. this.$events[eventName].forEach(callback => callback(data));
  369. };
  370. wx.$off = function (eventName, callback) {
  371. if (!this.$events || !this.$events[eventName]) return;
  372. if (callback) {
  373. this.$events[eventName] = this.$events[eventName].filter(cb => cb !== callback);
  374. } else {
  375. this.$events[eventName] = [];
  376. }
  377. };