合伙人运营小程序
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

app.js 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  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 partnerId = that.globalData.userInfo;
  101. console.log(partnerId);
  102. // 修复:startReconnect 中用了 partnerId,这里统一字段
  103. if (!partnerId) {
  104. console.error('用户partnerId不存在,无法连接WebSocket');
  105. return;
  106. }
  107. const wsUrl = `wss://esos-iot.com:9443/communication/update/${partnerId.partnerId}`;
  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 partnerId = that.globalData.userInfo; // 修复:用 partnerId 而非 userId
  138. console.log(partnerId);
  139. if (partnerId) {
  140. const wsUrl = `wss://esos-iot.com:9443/communication/update/${partnerId.partnerId}`;
  141. that.wsService.connect(wsUrl);
  142. }
  143. delay = Math.min(delay * 2, 8000);
  144. } else {
  145. that.clearReconnectTimer();
  146. }
  147. }, delay);
  148. },
  149. clearReconnectTimer() {
  150. if (this.globalData.reconnectTimer) {
  151. clearInterval(this.globalData.reconnectTimer);
  152. this.globalData.reconnectTimer = null;
  153. }
  154. },
  155. /**
  156. * 司机端开始后台定位并上传(单工单)
  157. * @param {string} workorderId - 当前工单ID
  158. */
  159. startDriverLocationUpload(workorderId) {
  160. const service = this.locationService;
  161. // 入参校验
  162. if (!workorderId) {
  163. console.error('工单ID不能为空');
  164. wx.showToast({ title: '工单ID异常', icon: 'none' });
  165. return;
  166. }
  167. // 如果已经在为当前工单上传,则忽略
  168. if (service.isTracking && service.currentWorkorderId === workorderId) {
  169. console.log(`已经在为工单 ${workorderId} 上传位置`);
  170. return;
  171. }
  172. // 如果已经在为其他工单上传,先停止
  173. if (service.isTracking && service.currentWorkorderId !== workorderId) {
  174. console.log(`当前正在为工单 ${service.currentWorkorderId} 上传,切换到 ${workorderId},先停止旧定位`);
  175. this.stopDriverLocationUpload();
  176. }
  177. // 权限校验
  178. wx.getSetting({
  179. success: (res) => {
  180. if (!res.authSetting['scope.userLocationBackground']) {
  181. wx.authorize({
  182. scope: 'scope.userLocationBackground',
  183. success: () => {
  184. this._doStartLocationUpload(workorderId, service);
  185. },
  186. fail: () => {
  187. wx.showModal({
  188. title: '权限不足',
  189. content: '需要开启后台定位权限才能实时上传位置,请前往设置页开启',
  190. confirmText: '去设置',
  191. success: (modalRes) => {
  192. if (modalRes.confirm) {
  193. wx.openSetting({
  194. success: (settingRes) => {
  195. if (settingRes.authSetting['scope.userLocationBackground']) {
  196. this._doStartLocationUpload(workorderId, service);
  197. }
  198. }
  199. });
  200. }
  201. }
  202. });
  203. }
  204. });
  205. } else {
  206. this._doStartLocationUpload(workorderId, service);
  207. }
  208. }
  209. });
  210. },
  211. /**
  212. * 私有方法:执行真正的定位启动逻辑(核心修复:兼容低版本,改用 wx.onLocationChange)
  213. */
  214. _doStartLocationUpload(workorderId, service) {
  215. console.log(`开始为工单 ${workorderId} 启动后台定位上传...`);
  216. service.currentWorkorderId = workorderId;
  217. // 修复1:先移除旧的定位监听(防止重复监听)
  218. this._removeLocationListener();
  219. // 修复2:启动后台定位(兼容低版本,用 startLocationUpdate 兜底)
  220. const startLocationApi = wx.startLocationUpdateBackground || wx.startLocationUpdate;
  221. startLocationApi({
  222. type: 'gcj02',
  223. success: (res) => {
  224. console.log('定位服务已启动', res);
  225. service.isLocationUpdateStarted = true;
  226. // 存储上一次的定位信息
  227. let lastLocation = null;
  228. // 距离阈值(单位:米,根据需求调整,5米)
  229. const DISTANCE_THRESHOLD = 5;
  230. // 修复3:改用低版本支持的 wx.onLocationChange(替代 wx.watchPosition)
  231. wx.onLocationChange((location) => {
  232. if (!lastLocation) {
  233. lastLocation = location;
  234. console.log('位置变化,准备上传:', location);
  235. this._uploadLocation(location, workorderId);
  236. return;
  237. }
  238. // 计算当前定位与上一次的直线距离(Haversine公式)
  239. const distance = this.calculateDistance(
  240. lastLocation.latitude,
  241. lastLocation.longitude,
  242. location.latitude,
  243. location.longitude
  244. );
  245. console.log(distance);
  246. // 只有距离超过阈值,才认为是“有效变化”
  247. if (distance >= DISTANCE_THRESHOLD) {
  248. lastLocation = location; // 更新上一次定位
  249. this._uploadLocation(location, workorderId);
  250. }
  251. });
  252. // 标记定位中状态
  253. service.isTracking = true;
  254. console.log('位置监听已注册,开始实时上传');
  255. },
  256. fail: (err) => {
  257. let errMsg = '启动定位失败';
  258. if (err.errMsg.includes('auth deny')) errMsg = '定位权限被拒绝';
  259. else if (err.errMsg.includes('system')) errMsg = '系统不支持定位';
  260. console.error(errMsg, err);
  261. wx.showToast({ title: errMsg, icon: 'none', duration: 2000 });
  262. }
  263. });
  264. },
  265. // Haversine公式:计算两点经纬度之间的直线距离(单位:米)
  266. calculateDistance(lat1, lng1, lat2, lng2) {
  267. const R = 6371000; // 地球半径(米)
  268. const radLat1 = (lat1 * Math.PI) / 180;
  269. const radLat2 = (lat2 * Math.PI) / 180;
  270. const deltaLat = radLat2 - radLat1;
  271. const deltaLng = (lng2 - lng1) * Math.PI / 180;
  272. const a =
  273. Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
  274. Math.cos(radLat1) * Math.cos(radLat2) *
  275. Math.sin(deltaLng / 2) * Math.sin(deltaLng / 2);
  276. const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  277. return R * c; // 距离(米)
  278. },
  279. /**
  280. * 司机端停止定位上传(核心修复:对应移除 wx.onLocationChange 监听)
  281. */
  282. stopDriverLocationUpload() {
  283. const service = this.locationService;
  284. if (!service.isTracking) {
  285. return;
  286. }
  287. console.log(`停止为工单 ${service.currentWorkorderId} 上传位置`);
  288. // 1. 移除位置监听(wx.onLocationChange 对应 wx.offLocationChange)
  289. this._removeLocationListener();
  290. // 2. 停止定位服务(兼容低版本,用 stopLocationUpdate 兜底)
  291. this._stopLocationService();
  292. // 3. 重置状态
  293. service.isTracking = false;
  294. service.currentWorkorderId = '';
  295. service.isLocationUpdateStarted = false;
  296. },
  297. /**
  298. * 私有方法:移除位置监听(兼容低版本)
  299. */
  300. _removeLocationListener() {
  301. if (wx.offLocationChange) {
  302. wx.offLocationChange();
  303. console.log('位置监听已移除');
  304. } else {
  305. console.warn('当前基础库不支持 wx.offLocationChange,可能存在重复监听风险');
  306. }
  307. },
  308. /**
  309. * 私有方法:统一停止定位服务(兼容低版本)
  310. */
  311. _stopLocationService() {
  312. const stopLocationApi = wx.stopLocationUpdateBackground || wx.stopLocationUpdate;
  313. if (stopLocationApi && this.locationService.isLocationUpdateStarted) {
  314. stopLocationApi({
  315. success: (res) => {
  316. console.log('定位服务已停止', res);
  317. },
  318. fail: (err) => {
  319. console.error('停止定位服务失败:', err);
  320. }
  321. });
  322. }
  323. },
  324. /**
  325. * 私有方法:上传位置到服务器(无改动)
  326. */
  327. _uploadLocation(location, workorderId, retryCount = 0) {
  328. const maxRetry = 2;
  329. const isLatValid = location.latitude >= 3.86 && location.latitude <= 53.55;
  330. const isLngValid = location.longitude >= 73.66 && location.longitude <= 135.05;
  331. if (!isLatValid || !isLngValid) {
  332. console.warn(`无效位置信息(工单: ${workorderId}),跳过上传`, location);
  333. return;
  334. }
  335. const data = {
  336. workorderId: workorderId,
  337. latitude: location.latitude,
  338. longitude: location.longitude,
  339. createTime: Date.now(),
  340. accuracy: location.accuracy
  341. };
  342. api.request(`/sysworkorder/insercoordinateredis`, 'post', data, { isPublic: false })
  343. .then((response) => {
  344. if (response.code !== 200) {
  345. throw new Error(`响应码异常: ${response.code}`);
  346. }
  347. console.log(`位置上传成功 (工单: ${workorderId})`, data);
  348. })
  349. .catch((err) => {
  350. console.error(`位置上传失败(第${retryCount+1}次)(工单: ${workorderId})`, err);
  351. if (retryCount < maxRetry) {
  352. setTimeout(() => {
  353. this._uploadLocation(location, workorderId, retryCount + 1);
  354. }, 10000 * (retryCount + 1));
  355. } else {
  356. wx.showToast({ title: '位置上传失败,请检查网络', icon: 'none' });
  357. }
  358. });
  359. },
  360. });
  361. // 全局事件总线
  362. wx.$on = function (eventName, callback) {
  363. if (!this.$events) this.$events = {};
  364. if (!this.$events[eventName]) this.$events[eventName] = [];
  365. this.$events[eventName].push(callback);
  366. };
  367. wx.$emit = function (eventName, data) {
  368. if (!this.$events || !this.$events[eventName]) return;
  369. this.$events[eventName].forEach(callback => callback(data));
  370. };
  371. wx.$off = function (eventName, callback) {
  372. if (!this.$events || !this.$events[eventName]) return;
  373. if (callback) {
  374. this.$events[eventName] = this.$events[eventName].filter(cb => cb !== callback);
  375. } else {
  376. this.$events[eventName] = [];
  377. }
  378. };