微信小程序电箱巡检:扫码定位拍照与离线提交方案

微信小程序电箱巡检:扫码定位拍照与离线提交方案 简介这份资源是一份围绕微信小程序实现电箱巡检信息化的技术文档面向施工现场管理人员、安全巡检负责人以及希望将传统巡检流程数字化的工程技术人员。内容以扫码巡检替代纸质记录为主线涵盖二维码一次性设置、巡检时间与位置约束、云端记录存储与导出等关键环节并给出成本节约与社会效益的量化测算。资源包共1个文件为docx格式的Word文档压缩包约11KB属于篇幅精炼、便于直接查阅与二次引用的方案说明文档。目前已有55人学习下载。读者可从中获得一套可直接落地的巡检签到小程序操作思路包括电箱信息创建、检查项设置、二维码生成与张贴、巡检内容填写提交及后台记录导出等完整流程同时附带按工期与人工单价推算的节费数据便于在项目汇报或工法编制中参考引用适合需要推进施工现场用电安全信息化的读者借鉴。1. 电箱巡检信息化真正的底线位置、时间、照片三件事对得上配电箱巡检这件事纸质表格时代的最大漏洞不是记录不全而是记录无法自证。巡检员在值班室把一叠表填完字迹工整、项目齐全但没人能证明他到底去没去过那台箱体。管理者想追责时手里只有一张纸纸不会说话。微信小程序之所以适合承接这件事是因为它同时握有三样纸质表格给不了的东西扫码能锁定是哪台箱wx.getLocation 能锁定人在哪wx.chooseMedia 调起相机能锁定当时拍了什么。这三样加上服务端时间戳才构成一条可信的巡检记录。这篇文章面向的读者是需要把分散在楼宇、园区、配电室里的几百上千台电箱管起来的运维负责人以及被派去实现它的前端和后端工程师。下面按数据模型、定位取证、接口联调、统计预警的顺序把一套能真正跑在现场的方案拆开讲参数和阈值都会给到具体数值坑也一并标出来。2. 用微信小程序搭电箱巡检的最小闭环扫码、填单、提交2.1 为什么是微信小程序而不是原生 App 或 H5一线巡检人员大多不是本公司员工而是物业电工、外委班组、临时支援人员。让他们下载安装一个 App推广成本高得离谱装完干完活就卸载下次还找不到。小程序扫个码就进退出即走这是决定性的。技术上还有两个隐性优势。H5 调摄像头扫码在 iOS 上的兼容性一直很糟而wx.scanCode是原生能力随手扫随手出结果H5 的navigator.geolocation在室内和地下配电室几乎不可用小程序的wx.getLocation至少能拿到系统级的精度反馈。反过来小程序的约束也得接受主包 2MB 上限、基础库版本受用户微信版本影响、本地 storage 总共只有 10MB。这些约束在后面的离线队列设计里会直接体现。2.2 电箱巡检怎么落表四张核心表的结构表设计决定了后面所有查询能不能写出来。常见的做法是四张表巡检明细不单独拆表而是用 JSON 存在记录里理由是明细几乎不会跨记录做聚合拆表反而增加写入事务的复杂度。表名关键字段说明boxid, box_no, name, area, longitude, latitude, fence_radius, cycle_days, enabled电箱台账fence_radius 是每台箱自己的围栏半径inspect_itemid, item_key, label, input_type, required, sort_no巡检项模板input_type 取 radio / number / textinspect_taskid, task_date, area, inspector_id, status任务按区域派发inspection_recordid, record_uuid, box_id, inspector_id, longitude, latitude, gps_accuracy, distance_m, in_fence, inspect_time, submit_time, items_json一次巡检一条fence_radius放在箱体上而不是写成全局常量是因为露天箱体和地下配电室没法共用同一个阈值。record_uuid由客户端生成服务端加唯一索引这是整条链路里最简单也最有效的幂等手段后面讲提交接口还会展开。items_json存的是[{itemId, result, value, remark}]数组读写方便代价是不能用 SQL 直接按单项过滤如果后续要做近三个月温升异常统计再把它拆成独立的明细表。2.3 用 wx.scanCode 扫电箱二维码拿到编号扫码这一步是整条链路的入口也是一次防作弊的关键点代码里有两个参数必须这么写。// pages/inspect/scan.js Page({ data: { boxNo: , box: null, formDisabled: true }, onScan() { wx.scanCode({ onlyFromCamera: true, // 只允许实时对着码拍禁止从相册选图挡住旧截图伪造 scanType: [qrCode], // 电箱贴的都是二维码限定类型能减少误扫条形码 success: (res) { const boxNo this.parseBoxNo(res.result) if (!boxNo) { wx.showToast({ title: 不是有效的电箱码, icon: none }) return } this.loadBox(boxNo) }, fail: () { // 用户主动取消会走 fail这里不能弹提示否则每次取消都打扰一次 } }) }, // 二维码内容可能是纯编号也可能是带跳转的 URL统一按正则提取 parseBoxNo(raw) { const m /PD-\d{4,}/.exec(String(raw).trim()) return m ? m[0] : }, loadBox(boxNo) { wx.request({ url: ${getApp().globalData.base}/api/box/${boxNo}, success: (r) { if (r.data.code ! 0) { wx.showToast({ title: 电箱未登记, icon: none }) return } this.setData({ boxNo, box: r.data.data, formDisabled: false }) } }) } })onlyFromCamera是防作弊的第一道闸。如果放开相册巡检员完全可以在办公室里对着以前拍的二维码截图扫一下整条取证链就废了。scanType限定qrCode能减少现场误扫箱体旁边经常贴着设备铭牌和资产条码不限定类型会经常扫错。fail分支务必静默处理否则用户每次点开扫码又反悔都会被弹一次提示。parseBoxNo对返回值做了trim处理部分安卓机型扫出来的字符串尾部会带空白字符不处理会直接匹配失败。2.4 巡检项的输入类型与单选框校验巡检项模板里最常用的是单选比如运行状态正常 / 异常 / 待确认。这类项用radio-group承载注意它的change事件是整个组触发的不是单选项触发的取值时要区分通道。!-- 巡检项渲染片段 -- block wx:for{{items}} wx:keyid view classitem text classlabel {{item.required ? req : }}{{item.label}}/text radio-group wx:if{{item.input_type radio}} >onRadioChange(e) { const itemId e.currentTarget.dataset.itemId // 注意这里取的是 e.detail.value不是 e.target.value this.setData({ [answers.${itemId}]: e.detail.value }) }, // 提交前的必填校验一次性把缺失项列全别让用户点一次报一个 validate() { const missing this.data.items .filter(it it.required !this.data.answers[it.id]) .map(it it.label) if (missing.length) { wx.showModal({ title: 还有必填项未填, content: missing.join(、), showCancel: false }) return false } return true }>wx.getLocation({ type: gcj02, // 与国内地图底图一致别用 wgs84否则会有几百米偏移 isHighAccuracy: true, // 开启高精度室内电井里的误差能压下来一些 highAccuracyExpireTime: 5000, // 5 秒内拿不到高精度就降级返回避免一直转圈 success: (res) { // res: latitude, longitude, accuracy(水平精度半径米), speed, altitude if (res.accuracy 100) { // 精度太差的点仍然记录但打上低置信标记不作为围栏判定依据 this.setData({ locationLowConfidence: true }) } this.setData({ location: res }) }, fail: (err) { // 用户拒绝授权、系统定位服务未开启、隐私协议未配置都会走这里 console.error(getLocation fail, err) wx.showModal({ title: 需要定位权限, content: 请在小程序设置里允许获取位置否则无法提交巡检记录, success: (r) { if (r.confirm) wx.openSetting() } }) } })配置上有两个必配项少一个就真机失败。第一是在app.json里声明{ requiredPrivateInfos: [getLocation], permission: { scope.userLocation: { desc: 用于校验巡检人员是否已到达电箱现场 } } }基础库从 2.25.0 起getLocation这类接口必须在requiredPrivateInfos里显式声明否则开发者工具可能正常、真机直接走fail。第二是在小程序管理后台配置《小程序用户隐私保护指引》把位置信息和相册/摄像头勾上。没配的话真机上这两个接口会静默失败日志里还不一定有明显报错这是排查起来最耗时的一类问题。3.2 围栏距离怎么算、阈值设多少距离用 Haversine 公式在这个尺度上精度完全够。更简单的等距圆柱投影也能用但没必要省这几行。// 两个坐标点之间的球面距离单位米 function distanceMeter(lat1, lng1, lat2, lng2) { const R 6371008.8 // 地球平均半径 const rad Math.PI / 180 const dLat (lat2 - lat1) * rad const dLng (lng2 - lng1) * rad const a Math.sin(dLat / 2) ** 2 Math.cos(lat1 * rad) * Math.cos(lat2 * rad) * Math.sin(dLng / 2) ** 2 return 2 * R * Math.asin(Math.sqrt(a)) }阈值不要设成全局常量我一般按箱体所在环境分档存进box.fence_radius环境建议半径说明露天/路边箱变80–120 米定位稳定可以收紧楼内配电间150–250 米有建筑遮挡误差增大地下配电室300 米或免判定基本收不到卫星信号靠人工报备标记高精度点 accuracy 100不参与判定记录但不拦截这里有个反直觉的点值得单独说围栏不要做成硬门槛当场拦截。地下配电室定位漂移几百米是常态硬拦的结果是巡检员要么放弃提交任务、要么在楼下先定位成功再上楼拍照记录本身反而更假。正确做法是照常提交把distance_m和in_fence两个字段都落到库里作为事后审计和抽查依据。系统要解决的是造不了假不是当场抓住。3.3 wx.chooseMedia 到 wx.uploadFile 的图片链路拍照和上传是两段中间隔着弱网和用户切换应用。// 第一步拍照。同样只允许相机禁止相册 wx.chooseMedia({ count: 6, // 一台箱最多 6 张超过考虑拆成多次巡检 mediaType: [image], sourceType: [camera], sizeType: [compressed], // 压缩模式单张通常 100–300KB弱网也能传 camera: back, success: (res) { const paths res.tempFiles.map(f f.tempFilePath) this.setData({ photos: this.data.photos.concat(paths) }) } }) // 第二步串行上传。并发上传在弱网下失败率明显更高 async uploadAll(recordUuid) { const keys [] for (const p of this.data.photos) { const key await this.uploadOne(p, recordUuid) // 失败内部重试 3 次 keys.push(key) } return keys }, uploadOne(filePath, recordUuid) { return new Promise((resolve, reject) { let retry 0 const doUpload () { wx.uploadFile({ url: ${getApp().globalData.base}/api/inspection/upload, filePath, name: file, // 后端读取的字段名 formData: { recordUuid }, // 会进入后端的表单参数 header: { X-Token: getApp().globalData.token }, timeout: 60000, success: (r) { const d JSON.parse(r.data) d.code 0 ? resolve(d.data.key) : reject(new Error(d.msg)) }, fail: (e) { if (retry 3) return setTimeout(doUpload, 1000 * retry) reject(e) } }) } doUpload() }) }sizeType: compressed是必须的。原图在弱网下经常传失败而且服务端存储成本会翻好几倍巡检照片只要看得清铭牌和表针就够了。串行上传而不是Promise.all是因为并发多个uploadFile在信号差的配电间几乎必然超时串行失败一张重传一张反而更快传完。formData里带上recordUuid服务端可以把照片直接挂到记录上不用等记录 id 回传。3.4 照片水印和防复用前端用离屏画布给照片打水印内容包含拍摄时间、经纬度和箱体编号压到右下角。这样即使照片被单独导出去也能看出是哪一次拍的。更省事的做法是后端读 EXIF 的DateTimeOriginal和客户端提交的inspect_time比对相差超过 5 分钟就标黄。防复用还有一层同一台箱同一个角度反复借用同一张照片是现场最常见的偷懒方式。服务端对上传文件做哈希同一box_id下出现重复哈希直接判为异常。小程序端没有现成的文件哈希 API可以用一份纯 JS 的 md5 实现处理tempFilePath对应的ArrayBuffer或者干脆交给服务端算代码量更小。4. 后端接口与真机联调请求到不了服务器时按顺序查4.1 提交接口的字段设计与幂等提交接口是整个系统的收口字段要一次定清楚不然后面加字段要兼容老版本小程序。// POST /api/inspection/submit router.post(/submit, async (req, res) { const d req.body // 1) 幂等客户端生成的 recordUuid 加唯一索引重复提交直接返回上次结果 const exist await db.query( SELECT id, status FROM inspection_record WHERE record_uuid ?, [d.recordUuid]) if (exist.length) { return res.json({ code: 0, data: { id: exist[0].id, duplicated: true } }) } // 2) 服务端重算距离客户端传上来的距离只做参考不能信任 const [box] await db.query(SELECT * FROM box WHERE box_no ?, [d.boxNo]) if (!box) return res.json({ code: 1001, msg: 电箱不存在 }) const dist haversine(d.latitude, d.longitude, box.latitude, box.longitude) const inFence dist box.fence_radius ? 1 : 0 // 3) 事务写主表明细以 JSON 存储 const conn await db.getConnection() await conn.beginTransaction() try { const [r] await conn.query( INSERT INTO inspection_record (record_uuid, box_id, inspector_id, longitude, latitude, gps_accuracy, distance_m, in_fence, inspect_time, submit_time, items_json, status) VALUES (?,?,?,?,?,?,?,?,?,NOW(),?, submitted), [d.recordUuid, box.id, req.user.id, d.longitude, d.latitude, d.accuracy, Math.round(dist), inFence, d.inspectTime, JSON.stringify(d.items)]) await conn.commit() res.json({ code: 0, data: { id: r.insertId, distance: Math.round(dist), inFence } }) } catch (e) { await conn.rollback() throw e } finally { conn.release() } })关键点是距离在服务端用库里的箱体坐标重算一遍。客户端上报的distance是可以被改的服务端自己算的才可信。in_fence落库但不参与业务拦截审批和考核的时候才有东西可看。4.2 合法域名、HTTPS 与真机调试的排查顺序小程序的request和uploadFile只能访问管理后台配置过的域名要求 HTTPS、已完成备案、不能是 IP 地址。注意request合法域名和uploadFile合法域名是分开配的只配了前者会导致照片上传一直失败而记录提交正常症状很容易被误判成后端问题。开发者工具里可以勾不校验合法域名、web-view、TLS 版本以及 HTTPS 证书但真机预览默认仍然校验。真机调试请求无法到达后端是最常被搜到的问题按下面顺序查基本能定位排查顺序检查项典型症状1手机浏览器直接打开该 HTTPS 地址打不开说明域名或证书本身有问题与小程序无关2证书链是否完整浏览器正常、小程序报 fail常见于只装了站点证书没装中间证书3后端监听地址只监听 127.0.0.1工具能通是因为代理真机必然不通4域名解析范围用了公司内网域名手机切到 4G 后解析不到5uploadFile 域名单独配置记录能提交、图片全失败6请求超时时间弱网下默认 60 秒长传大图容易超时第 2 条特别容易被忽略。开发者工具走电脑网络栈对证书链的宽容度比真机高所以经常出现工具里一切正常真机全部报错的现象。4.3 弱网、离线队列与本地缓存上限配电室基本没有信号提交失败是常态而非异常。做法是在提交前把整份 payload 写进 storage成功后再删掉。const QUEUE_PREFIX q_ // 提交前先入队保证断网也不丢数据 function enqueue(payload) { try { wx.setStorageSync(QUEUE_PREFIX payload.recordUuid, { payload, createdAt: Date.now() }) } catch (e) { // storage 总容量只有 10MB单 key 上限 1MB写满会直接抛异常 wx.showToast({ title: 本地存储已满请先联网提交, icon: none }) throw e } } // 每次进入巡检页和提交成功后都尝试清队 async function flushQueue() { const keys wx.getStorageInfoSync().keys.filter(k k.startsWith(QUEUE_PREFIX)) for (const k of keys) { const { payload } wx.getStorageSync(k) try { const r await request(/api/inspection/submit, payload) if (r.code 0) wx.removeStorageSync(k) } catch (e) { // 仍然失败就留着下次再试不要无限重试耗电 break } } }上限数字要记住小程序 storage 单个 key 最大 1MB总体 10MB。照片不要往 storage 里塞 base64本地临时文件路径放在wx.env.USER_DATA_PATH下管理超过一定天数由小程序自己在启动时清理。队列重试不要写成死循环失败一次就跳出等用户下次进入页面再触发。5. 从巡检记录到漏检清单SQL 聚合与二维码的一个细节5.1 一条 SQL 查出今天的漏检箱体巡检系统真正被管理者每天打开的页面不是记录列表而是漏检清单。用LEFT JOIN ... IS NULL找未完成的箱体比NOT IN快得多箱体量上万时差距很明显。-- 今日应巡但没有任何有效记录的箱体 SELECT b.box_no, b.name, b.area, b.cycle_days FROM box b LEFT JOIN inspection_record r ON r.box_id b.id AND DATE(r.inspect_time) CURDATE() AND r.status submitted WHERE b.enabled 1 AND b.cycle_days 0 AND r.id IS NULL ORDER BY b.area, b.box_no;r.status submitted这个条件要放在 JOIN 的 ON 里而不是 WHERE 里否则会把左连接变成内连接结果永远为空。周检、月检的箱体用b.cycle_days过滤把CURDATE()换成按周期计算的下次应检日期即可。5.2 异常项升级与订阅消息items_json里result abnormal的项按严重程度触发提醒。轻度的次日汇总重度的立即推送。推送用订阅消息这里有个限制必须提前设计wx.requestSubscribeMessage只能在用户点击的同步链路里调用不能放在wx.request的success回调里否则会直接被拒。而且它是一次性订阅用户点一次同意只能推一条所以要在巡检员每天开始工作时就引导他勾选总是保持以上选择一次性攒够多条授权额度别等到异常发生了再去要授权。5.3 二维码内容带上版本号最后说一个现场用起来很管用的小技巧。二维码里不要只写箱体编号写成PD-0012#v2这种带版本号的形式。当这台箱更换了设备、调整了巡检项模板、或者位置搬迁时台账里的版本号加一旧的#v1贴纸扫出来会被识别为过期强制要求现场重新贴码。这么做挡住的是最隐蔽的一类造假巡检员手上留着几年前的旧照片旧码还贴在箱体上没换扫一下、传张老图记录看起来完美无缺。版本号一升级旧码立刻失效必须有人真正到现场把新贴纸换上去顺便也就完成了对新设备的确认。版本号不用每次巡检都变只在设备或模板变更时递增维护成本几乎为零。本文还有配套的精品资源点击获取