news 2026/10/26 2:04:22

校园摆渡车小程序:原生+云开发实时位置上报与预约占座

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
校园摆渡车小程序:原生+云开发实时位置上报与预约占座

简介:面向微信小程序初学者与校园交通应用开发者的原创实践项目,围绕校园摆渡车实时位置查询、上下车点、运行时间表和座位预订等场景展开,可用于理解小程序前端交互与后端接口协作的完整链路。压缩包共79个文件,整体约1.01MB,主要由Java类、XML配置、JSP页面、JSON/JS/WXSS小程序代码及PNG界面图片等构成,后端可见Servlet、DAO、Bean、Control分层与JSON处理依赖,前端包含页面、工具与配置模块。已有416人学习。项目突出原创设计与实际需求结合,目录按前后端拆分,便于按功能定位代码。读者可从中获得可运行的前后端分离样例,参考地图组件接入、网络请求封装、数据增删改查与REST风格接口设计,并借目录结构梳理从页面到服务端的数据流,适合作为课程设计或毕业项目的基础模板,也便于按模块替换数据与界面后二次开发。

1. 校园摆渡车小程序:一辆车、一块地图和每 5 秒一次的位置上报

早上七点四十,A同学站在校门口,摆渡车刚走还是没来,全靠猜。这个校园摆渡车小程序解决的就是这件事:司机点一下“上线”,手机开始按固定间隔上报位置;乘客打开小程序,地图上就能看到车在哪、还有几站、要不要现在下楼。整套代码是原生小程序加云开发,没有自建服务器,一个 zip 解压导入开发者工具就能跑。

它适合三类人:想做个能演示的校园/园区接驳 Demo 的学生;手里有真实摆渡车、但养不起后台的运营方;以及想找一个“地图 + 实时位置 + 预约”完整链路练手的小程序开发者。地图、司机上报、乘客查看、预约占座、后台数据这几块都有,代码量不大,但该有的边界都在。真正难的不是地图渲染,而是位置数据在弱网和手机省电策略下怎么稳定地持续上报,这份源码里处理这些细节的地方,正是最值得借鉴的部分。

2. 选型与骨架:原生小程序加云开发,三层结构怎么落地

2.1 为什么用原生小程序,不用 uni-app

先说明这个项目我改过一版。原作者的选型很保守:微信原生 + 云开发,没上任何跨端框架。刚开始我觉得这选择有点老派,拆完代码后理解了,摆渡车这种场景的核心诉求是“地图要准、定位要稳、启动要快”,跨端框架在这三点上都要打折扣。

原生小程序直接调用map组件和wx.onLocationChange,没有中间层,定位回调的时序是确定的。uni-app 编译到小程序后,位置相关的 API 往往要包一层 Promise,出问题时排查链路会变长,这在你半夜被“车不动了”的电话叫醒时是致命的。

方案首屏体积地图能力实时位置运维成本
原生 + 云开发小,无框架运行时直接用map组件wx.onLocationChange原生回调几乎为零,无需服务器
uni-app + 自建后端多一层运行时需适配层需自己封装要买服务器、配域名、备案
原生 + 自建 Node 服务小直接用原生回调要维护进程、证书、扩容

选云开发的另一个现实原因:摆渡车的并发模型是“一写多读”。一辆车 5 秒写一次,几百个乘客读,峰值 QPS 低得可怜,用云函数完全撑得住。这种情况下自己买一台 2 核 4G 的服务器,一年下来是纯浪费,还要处理备案和证书到期的问题,很容易出问题。

当然代价也有:云开发的冷启动在 1 秒左右,第一次打开小程序会感觉卡;数据库查询有单次 100 条的上限,做后台管理时要分页。这些在后面的章节里都会碰到。

2.2 目录结构:把司机端和乘客端放进同一个包

这是解压后的目录,我把注释补全了。你会发现作者没有做分包,因为总共就五个页面,包体积远没到 2MB 的红线。

campus-shuttle/ ├── miniprogram/ # 小程序前端根目录 │ ├── app.js # 云开发初始化、全局登录态 │ ├── app.json # 页面注册、位置权限声明 │ ├── app.wxss │ ├── pages/ │ │ ├── passenger/ # 乘客端:地图首页 │ │ │ ├── index.js │ │ │ ├── index.wxml │ │ │ └── index.wxss │ │ ├── driver/ # 司机端:上线/下线、位置上报 │ │ ├── order/ # 我的预约 │ │ └── me/ # 个人中心 │ ├── components/ │ │ ├── bus-marker/ # 车辆气泡卡片 │ │ └── station-list/ # 站点列表 │ ├── utils/ │ │ ├── geo.js # 距离计算、坐标精度处理 │ │ └── mockDriver.js # 开发期模拟司机(仅开发版) │ └── assets/ # 车辆、站点图标 ├── cloudfunctions/ │ ├── reportLocation/ # 司机位置写入 │ ├── listBuses/ # 乘客端拉取在线车辆 │ ├── reserveSeat/ # 预约占座 │ └── seedDemo/ # 一键造模拟数据 └── project.config.json # 指定 miniprogramRoot / cloudfunctionRoot

app.json里有三处容易漏配,漏了就是真机上“定位永远失败”:

{ "pages": [ "pages/passenger/index", "pages/driver/index", "pages/order/index", "pages/me/index" ], "permission": { "scope.userLocation": { "desc": "用于在地图上显示你附近的摆渡车" } }, "requiredPrivateInfos": ["getLocation", "onLocationChange", "startLocationUpdate"], "window": { "navigationBarTitleText": "校园摆渡车" } }

permission.scope.userLocation.desc是弹窗里给用户看的文案,写得含糊会被审核驳回;requiredPrivateInfos是小程序 2022 年之后的新要求,没声明onLocationChange的话,代码能编译、开发者工具也能跑,但真机上回调一次都不进。这一点后面避坑章节还会再提。

2.3 数据集合与云函数边界

云开发的数据库是文档型的,作者设计了四个集合,职责切得很干净:

集合关键字段说明
bus_routename、stations[]、polyline[]、firstBus线路与站点坐标,乘客端渲染用
bus_location_id= 车牌号、location(GeoPoint)、heading、updatedAt一车一文档,司机上报写入
bus_status_id= 车牌号、capacity、booked、online载客量、在线状态,预约时改它
order_openid、busId、status、createdAt预约单,靠_openid自动隔离

关键设计是bus_location用车牌号当_id,而不是用自增 ID 再建索引。这样司机上报时是一次doc(busId).set()的幂等写入,第一次是插入,之后是覆盖,云函数里不用写“查一下有没有再决定插还是改”的分支,省下一次读的配额,也避开了并发下的重复插入。

云函数只保留四个,入口很窄:写位置的只有reportLocation,读列表的只有listBuses。这样做的好处是权限收敛,客户端不直连数据库,所有读写走云函数,云函数里再用cloud.getWXContext().OPENID做身份校验。很多教学项目让前端直接db.collection().get(),然后集合权限一设成“所有用户可读”,别人拿你的 AppID 就能把整张表拉走。

// cloudfunctions/listBuses/index.js const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() const _ = db.command exports.main = async (event) => { const { routeId } = event const now = Date.now() // 只返回 60 秒内上报过的车,避免"幽灵车"停在地图上不动 const res = await db.collection('bus_location') .where({ routeId, online: true, updatedAt: _.gt(now - 60 * 1000) }) .field({ location: true, heading: true, updatedAt: true, plate: true }) .limit(50) .get() return { ok: true, list: res.data.map(item => ({ id: item._id, plate: item.plate, lat: item.location.latitude, lng: item.location.longitude, heading: item.heading || 0, // 让前端能显示"10 秒前更新",比一个静止的图标更有说服力 age: Math.round((now - item.updatedAt) / 1000) })) } }

这段代码有三个参数值得说:_.gt(now - 60000)是心跳超时阈值,摆渡车在校园里穿行会经过楼栋遮挡,偶尔丢一两个包是常态,阈值设太短车会在地图上反复闪;field()只取用得到的字段,能把返回体积压缩一半以上;limit(50)是兜底,一个校区的车不会超过这个数,真超了说明数据有问题。

3. 核心链路拆解:位置从司机手机到乘客地图的 3 秒

3.1 司机端:上报的节流、去抖与心跳

司机端页面只有一个开关和几行状态文字,但背后的逻辑不少。位置回调在安卓机上可能一秒来好几次,无脑转发会把云函数打到限流,所以要做三层过滤:时间节流、位移去抖、在线状态判断。

// pages/driver/index.js const REPORT_INTERVAL = 5000 // 最短上报间隔,毫秒 const MIN_MOVE = 8 // 位移小于 8 米视为 GPS 飘点,丢弃 Page({ data: { online: false, busId: '', lastLat: 0, lastLng: 0, lastTs: 0, lastReport: '' }, onLoad() { // 车牌号在"我的"页面绑定后存在本地,这里直接读 this.setData({ busId: wx.getStorageSync('busId') || '' }) }, async toggleOnline() { if (this.data.online) return this.stopReport() if (!this.data.busId) { return wx.showToast({ title: '请先绑定车牌号', icon: 'none' }) } // 先确认用户真的授权了定位,被拒过要引导去设置页 const setting = await wx.getSetting() if (!setting.authSetting['scope.userLocation']) { await wx.authorize({ scope: 'scope.userLocation' }).catch(() => { wx.showModal({ title: '需要定位权限', content: '请在设置中开启定位', }) }) } this.startReport() }, startReport() { // 持续定位,比 setInterval + getLocation 更省电,系统会做批量唤醒 wx.startLocationUpdate({ success: () => { this._onLoc = (loc) => this.report(loc) wx.onLocationChange(this._onLoc) this.setData({ online: true }) }, fail: (err) => { console.error('startLocationUpdate fail', err) wx.showToast({ title: '定位启动失败', icon: 'none' }) } }) }, report(loc) { const now = Date.now() if (now - this.data.lastTs < REPORT_INTERVAL) return // 时间节流 const moved = distance(loc.latitude, loc.longitude, this.data.lastLat, this.data.lastLng) if (this.data.lastTs && moved < MIN_MOVE) return // 位移去抖 this.setData({ lastLat: loc.latitude, lastLng: loc.longitude, lastTs: now }) wx.cloud.callFunction({ name: 'reportLocation', data: { busId: this.data.busId, lat: Number(loc.latitude.toFixed(6)), // 精度截断,见避坑章节 lng: Number(loc.longitude.toFixed(6)), heading: Math.round(loc.speed > 0.5 ? loc.direction : this.data.lastHeading || 0), speed: loc.speed || 0 } }).then(res => { this.setData({ lastReport: new Date().toLocaleTimeString() }) }).catch(err => { console.error('上报失败', err) // 失败不弹窗,下一轮自动补上 }) }, stopReport() { if (this._onLoc) wx.offLocationChange(this._onLoc) wx.stopLocationUpdate() wx.cloud.callFunction({ name: 'reportLocation', data: { busId: this.data.busId, offline: true } }) this.setData({ online: false }) }, onUnload() { this.stopReport() } })

参数解释一下:REPORT_INTERVAL取 5 秒是个平衡点,再短乘客端看不出差别,只多烧流量;MIN_MOVE取 8 米是为了过滤静止时的定位抖动,车停在站台等客时如果不去抖,地图上的车会像抽搐一样乱跳。stopReport()里那次offline: true的调用很关键,否则司机退出小程序后,车会以“最后位置”的形态一直挂在线上,乘客白等。

还有一个隐藏细节:toFixed(6)之后再Number()。原始经纬度是一串 15 位的小数,直接塞进setData会让视图层传输体积翻好几倍,截断到 6 位(约 0.1 米精度)对摆渡车完全够用。

3.2 乘客端:地图、站点与车辆图标

乘客端的 WXML 很薄,因为map组件几乎把活干完了:

<!-- pages/passenger/index.wxml --> <view class="page"> <map id="busMap" latitude="{{latitude}}" longitude="{{longitude}}" scale="{{scale}}" markers="{{markers}}" polyline="{{polyline}}" show-location bindmarkertap="onMarkerTap" bindcallouttap="onCalloutTap" /> <view class="bottom-bar"> <text>在线车辆 {{onlineCount}} 辆</text> <text wx:if="{{nearestAge}}">最近一辆 {{nearestAge}} 秒前更新</text> </view> </view>

markers是数组,每个元素对应地图上的一个点。这里容易出的问题是每次都重建整个数组,导致图标闪烁、callout 弹窗闭合。作者的写法是先在 JS 里做 diff,只对变化的车做替换:

// pages/passenger/index.js const db = wx.cloud.database() Page({ data: { latitude: 0, longitude: 0, scale: 17, markers: [], polyline: [], onlineCount: 0, nearestAge: 0 }, onLoad() { this.fetchRoute() this.fetchBuses() this._timer = setInterval(() => this.fetchBuses(), 5000) // 兜底轮询 }, onUnload() { clearInterval(this._timer) }, async fetchRoute() { const { result } = await wx.cloud.callFunction({ name: 'getRoute', data: { routeId: this.data.routeId } }) this.setData({ polyline: [{ points: result.stations.map(s => ({ latitude: s.lat, longitude: s.lng })), color: '#3B82F6', width: 6, arrowLine: true }], latitude: result.stations[0].lat, longitude: result.stations[0].lng }) }, async fetchBuses() { const { result } = await wx.cloud.callFunction({ name: 'listBuses', data: { routeId: this.data.routeId } }) if (!result.ok) return const markers = result.list.map(bus => ({ id: bus.id, latitude: bus.lat, longitude: bus.lng, iconPath: '/assets/bus.png', width: 34, height: 34, rotate: bus.heading, // 车头朝向,比一个圆点更直观 anchor: { x: 0.5, y: 0.5 }, callout: { content: `${bus.plate} ${bus.age}秒前`, color: '#111827', fontSize: 12, borderRadius: 6, bgColor: '#ffffff', padding: 6, display: 'BYCLICK' } })) this.setData({ markers, onlineCount: markers.length, nearestAge: markers.length ? Math.min(...result.list.map(b => b.age)) : 0 }) }, onCalloutTap(e) { const bus = this.data.markers.find(m => m.id === e.detail.markerId) wx.navigateTo({ url: `/pages/passenger/detail?id=${bus.id}` }) } })

rotate用司机上报的heading,实测发现高德坐标系下这个角度和图标本身有 90 度偏差,作者的图标资源是朝上的,所以直接赋值就对。你自己换图标时记得把方向掰回来,否则车会横着跑,看着特别怪。

display: 'BYCLICK'是刻意选的。用ALWAYS时车一多,屏幕上会堆满气泡,把地图压得看不见路;改成点击才弹,信息密度立刻降下来。

3.3 预约与到站提醒:把“抢座”当成一次条件更新

预约模块最容易写崩的地方是超卖。教学代码常见写法是先get()查当前人数,判断没满,再update()加一。两个请求撞在一起,18 座的车能约进去 22 个人。

作者用的是条件更新,把“判断”和“写入”压成一次原子操作:

// cloudfunctions/reserveSeat/index.js const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() const _ = db.command exports.main = async (event) => { const { OPENID } = cloud.getWXContext() const { busId } = event const now = Date.now() // 幂等:同一用户 10 分钟内只允许有一张有效预约 const dup = await db.collection('order').where({ _openid: OPENID, busId, status: 'valid' }).count() if (dup.total > 0) return { ok: false, code: 'DUPLICATE' } // 核心:只有 booked < capacity 时才会命中,命中数 0 说明满座 const res = await db.collection('bus_status') .where({ _id: busId, booked: _.lt(18) }) .update({ data: { booked: _.inc(1) } }) if (res.stats.updated === 0) { return { ok: false, code: 'FULL' } } const order = await db.collection('order').add({ data: { busId, status: 'valid', createdAt: now } }) return { ok: true, orderId: order._id } }

_.lt(18)里的 18 硬编码了,二次开发时应该从bus_status.capacity读出来再拼进条件,我在第 6 章给了改法。_.inc(1)是数据库层面的自增,不走读改写,天然并发安全。res.stats.updated === 0这个判断是整个函数的成败关键,它同时覆盖了“已满座”和“车牌号不存在”两种情况,前端只需要提示“当前车辆不可预约”。

取消预约要反向减回来,同样用_.inc(-1),并且要先确认订单状态是valid再减,否则用户连点两次取消,座位数会变成负数。

4. 本地跑通:从导入工程到地图上出现第一辆车

4.1 环境准备与工程导入

需要的东西不多:微信开发者工具稳定版、一个 AppID(测试号也行,但云开发必须用真实 AppID),以及开通云开发。步骤是解压、导入、改配置。

{ "miniprogramRoot": "miniprogram/", "cloudfunctionRoot": "cloudfunctions/", "appid": "替换成你自己的 AppID", "compileType": "miniprogram", "setting": { "urlCheck": false, "es6": true, "enhance": true, "minified": true } }

miniprogramRoot和cloudfunctionRoot两个字段必须和实际目录名对上,少一个斜杠,云开发面板里就看不到云函数列表,很多人卡在这一步。urlCheck关掉是因为开发阶段还没配合法域名,不关会报“不在以下 request 合法域名列表中”。

导入后点开发者工具左上角的“云开发”,开通一个环境。环境 ID 形如xxx-1a2b3c,把它填进app.js:

// app.js App({ onLaunch() { if (!wx.cloud) { return console.error('请使用 2.2.3 以上基础库以使用云能力') } wx.cloud.init({ env: 'your-env-id', // 换成你自己的环境 ID traceUser: true // 在控制台记录用户访问,方便排查 }) }, globalData: { routeId: '' } })

traceUser打开后,云开发控制台的“用户管理”能看到访问记录,调试登录态时非常有用。

4.2 初始化集合与数据:用 seedDemo 一次造完

手工在控制台建四个集合、一条条录数据太累,作者写了个seedDemo云函数,右键“上传并部署:云端安装依赖”,然后在控制台点测试就行。

// cloudfunctions/seedDemo/index.js const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() const COLLECTIONS = ['bus_route', 'bus_location', 'bus_status', 'order'] exports.main = async () => { // 建集合是幂等的,重复执行只会抛"已存在",忽略即可 for (const name of COLLECTIONS) { try { await db.createCollection(name) } catch (e) { /* -501001 */ } } // 一条环线,六个站点 const stations = [ { seq: 1, name: '南门', lat: 30.0001, lng: 120.0001 }, { seq: 2, name: '图书馆', lat: 30.0012, lng: 120.0008 }, { seq: 3, name: '教学楼', lat: 30.0021, lng: 120.0019 }, { seq: 4, name: '体育馆', lat: 30.0016, lng: 120.0031 }, { seq: 5, name: '宿舍区', lat: 30.0004, lng: 120.0026 }, { seq: 6, name: '食堂', lat: 29.9995, lng: 120.0014 } ] await db.collection('bus_route').add({ data: { name: '1 号线(环线)', stations, firstBus: '07:00', lastBus: '22:30', enabled: true } }) // 两辆车,初始状态离线 for (const plate of ['校A-001', '校A-002']) { await db.collection('bus_status').doc(plate).set({ data: { capacity: 18, booked: 0, online: false } }) } return { ok: true, collections: COLLECTIONS.length } }

注意doc(plate).set()而不是add(),这里必须和reportLocation用同一个_id规则,否则司机上线时写的是一份新文档,bus_status和bus_location就对不上了。

集合建好后要去控制台给bus_location建索引:routeId(升序)+online(升序)+updatedAt(降序)。不建索引,几十辆车时查询就是几百毫秒,listBuses的冷启动叠加起来,首页白屏能到 3 秒。

4.3 真机调试与模拟数据注入

开发者工具里能跑,但定位是假的,你拖鼠标它才动。想看到“车在跑”,最快的办法是开模拟司机。

// utils/mockDriver.js —— 开发期用,正式版记得关 const STEP = 0.00012 // 每次前进的度数,约 13 米 function createMockDriver(start, onTick) { let lat = start.lat let lng = start.lng return setInterval(() => { lat += STEP lng += STEP / 2 onTick({ latitude: lat, longitude: lng, speed: 3.2, direction: 45 }) }, 3000) } module.exports = { createMockDriver }

在司机端startReport()里加个开关,开发版走模拟、正式版走真实定位:

if (__wxConfig.envVersion === 'develop') { const { createMockDriver } = require('../../utils/mockDriver') this._mock = createMockDriver({ lat: 30.0001, lng: 120.0001 }, loc => this.report(loc)) } else { wx.onLocationChange(this._onLoc) }

__wxConfig.envVersion的值是develop/trial/release,用它做开关比自定义常量安全,不会因为忘改配置把模拟数据带上线。真机调试时记得在手机设置里给微信开定位权限,安卓还要额外开“始终允许”,否则熄屏后位置就断了。

5. 避坑与排查:真机才复现的 5 个典型故障

5.1 位置相关:飘点、失联与方向错乱

现象一:车停着不动,地图上的点却在半径 30 米内乱跳。原因:wx.onLocationChange拿到的是系统融合定位,静止时会有几十米误差,而代码里没做去抖,每次都setData重绘 markers。 解决:加MIN_MOVE阈值(第 3 章里的 8 米),并且在 UI 上做平滑——把新旧坐标按 0.3 / 0.7 加权,视觉上就顺了。这一步是纯前端处理,不用改后端。

现象二:开发者工具里一切正常,真机上onLocationChange一次回调都没有。原因:app.json里漏了requiredPrivateInfos: ["onLocationChange", "startLocationUpdate"]。这个字段是后来加的强制声明,缺失时接口静默失败,不报错,最坑。 解决:补齐requiredPrivateInfos,重新编译并杀掉小程序重进。热重载不会重新读这个配置,很多人改完以为没用,其实是没重启。

现象三:车头图标永远朝着同一个方向。原因:部分机型loc.direction返回-1或NaN,Math.round之后变成NaN,rotate被忽略,沿用上一次的值。 解决:在report()里加Number.isFinite(loc.direction)判断,无效时保留上一帧角度。这个属于平台差异,实测不同安卓 ROM 表现不一致,只能防御式写。

5.2 云开发相关:查不到、触发器不跑、并发超卖

现象四:控制台里明明有数据,乘客端列表永远是空的。原因:集合权限设成了“仅创建者可读写”,而乘客端是直接查库的(如果你照着别的教程改过的话)。云函数内是管理员权限不受限,客户端直查就会被规则拦掉,且返回空数组而不是报错。 解决:统一走云函数,客户端不碰db。如果确实要直查,权限改成“所有用户可读”,但要在字段层面做脱敏,别把司机的手机号之类的东西暴露出去。

现象五:预约人数超过座位数,18 座的车约进去 22 个。原因:先get()判断、再update()写入,两个请求之间有窗口期,并发时全部判到“没满”。 解决:换成第 3.3 节的条件更新写法,靠_.lt()+_.inc()的原子性兜住。上线前我用脚本并发打了 50 个请求,改成条件更新后成功数稳定停在 18,没再翻过车。

现象六:定时清理过期预约的云函数,怎么配都不触发。原因:config.json里的triggers[].name必须和触发器同名且全局唯一,作者一开始写成了cleanOrder,云函数名是cleanOrders,差一个字母,触发器上传成功但永远不跑。 解决:name和云函数名保持一致,改完在控制台“云函数 - 触发器”里确认状态是“启用”,并手动点一次“测试”看日志。定时触发器的 cron 是 UTC+8 的七段式,写0 0 3 * * * *表示每天凌晨 3 点。

提示:排查云函数问题,第一件事是看云开发控制台的“日志”,而不是console.log在本地。本地日志和云端日志是两套,callFunction失败时前端只拿到一个笼统的错误码。

6. 二次开发:把单线路摆渡车改成多线路调度系统

这份代码的骨架是能撑住的,但要真跑起来,绕不开三处改造。我按改动量从小到大排:

改造点涉及文件改动量注意
容量改成动态读取reserveSeat、bus_status半小时别再把 18 写死在条件里
多线路 + 线路切换bus_route、listBuses、乘客端首页半天车辆要加routeId字段
轮询换实时订阅乘客端onShow1 小时注意监听数上限和断线重连

容量那处最该先改。把capacity从bus_status读出来再拼条件,云数据库的where支持动态值,直接替换数字即可:

// cloudfunctions/reserveSeat/index.js 片段 const status = await db.collection('bus_status').doc(busId).get() const capacity = status.data.capacity const res = await db.collection('bus_status') .where({ _id: busId, booked: _.lt(capacity) }) // 不再写死 18 .update({ data: { booked: _.inc(1) } })

多线路改造的关键是给bus_location和bus_status都补上routeId,listBuses已经按routeId过滤了,前端只要把选中的线路 ID 传进去就行。乘客端首页加一个picker切换线路,切换时清掉旧 markers 再拉新数据,否则会看到两条线的车叠在一块。

轮询换实时订阅,体验提升最明显。云开发数据库自带watch,位置一写进去就推到客户端,延迟从 5 秒降到 1 秒以内:

// pages/passenger/index.js 用 watch 替换 setInterval onShow() { this._watcher = db.collection('bus_location') .where({ routeId: this.data.routeId, online: true }) .watch({ onChange: (snapshot) => this.renderMarkers(snapshot.docs), onError: (err) => { console.error('watch 断开', err) this._timer = setInterval(() => this.fetchBuses(), 5000) // 降级回轮询 } }) }, onHide() { this._watcher && this._watcher.close() // 不关会持续占用连接 }

验证改造是否到位,我的做法是造 20 辆车跑一遍:写个临时云函数循环调用reportLocation,每 3 秒一轮,同时开三个客户端(开发者工具、iOS、安卓)看地图刷新是否一致。这一步能同时暴露索引缺失、setData过大、watch 连接数超限三个问题,比单机点来点去靠谱得多。

我踩过最深的坑是第一次上线时忘了onHide里关 watch,用户切后台再回来,监听对象越滚越多,跑了半小时内存直接爆炸。从那以后我每次接实时订阅,都强制走一遍“切后台—等 30 秒—切回来”的流程,看控制台有没有重复的连接建立日志。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 2:04:07

蔚来后端一面+二面:16道高频真题(附答案PDF模板)

以上是蔚来后端岗位的真实面经&#xff0c;一面主要考察候选人的基础原理&#xff0c;二面考察场景设计能力&#xff0c;总的来说&#xff0c;我认为难度适中&#xff0c;这在我们真实工作中也是会经常会遇到的问题&#xff0c;下面我就上面的面试题&#xff0c;我做下自己的理…

作者头像 李华
网站建设 2026/10/26 2:04:07

_FAG INA 滚动轴承保持架断裂:断口、间隙、材料三条链_

滚动轴承保持架断裂大多数不是“保持架质量不好”&#xff0c;而是它替整套滚动轴承承受了不该由它承受的力。断口位置能区分三种力&#xff1a;兜孔根部的交变弯曲、引导面的摩擦热、材料老化后的脆断。先看断口&#xff0c;再谈加速度、间隙和材料&#xff1b;顺序反了&#…

作者头像 李华
网站建设 2026/10/26 2:03:29

人民币资产,正在从备选变成必选

《人民币资产&#xff0c;正在从备选变成必选》——资本愿意买入&#xff0c;是机会&#xff1b;资本愿意长期持有&#xff0c;才是实力全球约六分之一的经济体量&#xff0c;只换来储备货币不到百分之三的份额。这道缺口&#xff0c;不是问题&#xff0c;是空间。近日&#xf…

作者头像 李华
网站建设 2026/10/26 2:03:17

装修报价单的增项怎么查?星艺装饰对账的 6 个字段

把一份装修报价单拆成可对账的数据结构&#xff0c;只需要六个字段&#xff1a;项目编号、计量单位、计价规则、工程量、单价&#xff0c;以及变更触发条件。六个字段齐&#xff0c;增项就是一次可计算的差异&#xff1b;缺任何一个&#xff0c;结算只能靠口头解释。 装修报价单…

作者头像 李华
网站建设 2026/10/26 2:01:02

Vue知识点5-Ref + Reactive

使用Ref 为基本数据类型 创建响应式对象。通过ref函数包裹后的基本数据&#xff0c;实际就是一个RefImpl的实现了在函数中使用的话&#xff0c;取值需要通过 xxx.value 取到真实的值模板中无需使用 xxx.value&#xff0c;框架已经帮我们解析过了<template><div class&…

作者头像 李华