道路隐患排查小程序开发实践:Python双框架数据采集与后台管理系统

发布时间:2026/10/10 11:01:07
道路隐患排查小程序开发实践:Python双框架数据采集与后台管理系统 1. 从纸质台账到小程序我为什么要做这么一套排查工具道路安全隐患排查是很多交通管理、公路养护、市政巡查单位每年雷打不动的常规工作。真正接触过这个流程的人都知道基层巡查人员的工作方式还相当传统拿着打印好的纸质表格到现场用笔勾选隐患类型、手写位置描述、用手机或者相机拍几张照片回到单位再一条一条录入电脑最后汇总成Excel报给上级。我接过一个需求来自某道路巡查单位的信息化改造任务。他们当时最头疼的问题非常具体一个季度要排查上千公里道路排查人员跑完外业回来光是整理当天拍的照片和手写记录就要两三个小时。照片多了之后哪张对应哪个桩号经常搞混笔录里字迹认不出来漏填等级、漏写位置描述也是常事。到了月底做统计又得把几十张Excel合并起来用公式去数不同隐患类型的数量整个过程不仅慢而且数据质量没保障。所以当对方提出想做一个移动端数据采集工具时我第一反应就是这个工具的核心不是把纸质表格变成电子表格而是要让外业采集本身变得更简单、更不容易出错。巡查人员在现场只需要做三件事定位、拍照、填关键字段其余的全部交给系统处理。最终落地的方案就是这篇博文要讲的——基于Python实现道路安全隐患排查数据采集小程序后端同时用了Django和Flask两套框架各管一段。这套系统的目标用户其实分三类。第一类是外业巡查人员他们要的是快速录入、快速提交最好在山区隧道等信号差的地方也能先存草稿第二类是内业审核人员他们要一条一条核对隐患描述和照片判断等级是否合理然后分派给养护单位处置第三类是管理层他们要按区域、按类型、按时间段看统计数据知道哪些路段隐患高发、哪些整改还没完成。三类角色对系统的诉求完全不同这也是我最终决定双框架而不是单框架的重要原因后面会详细展开。简单说一下项目的技术底座小程序端用的是微信小程序原生框架也可以换任意跨端方案接口逻辑大同小异后端拆成两个服务——Flask负责面向小程序端的所有数据采集APIDjango负责面向管理后台的Web应用和数据管理。数据库统一用一套MySQL两边通过各自的ORM连接同一套库。前端管理界面没有用复杂的前端框架直接用了Django自带的模板加少量JavaScript因为内部系统对交互要求不高够用就行。2. Django和Flask各司其职双框架的整体架构与数据流很多人问我为什么不用一个框架搞定非要上两个我的回答是这个项目我一开始也是用单一框架的但写到一半发现把面向小程序的API和面向管理人员的Web后台放在一起开发体验和维护成本都不理想。所以后来重构时我干脆拆开让每个框架干自己最擅长的事。2.1 为什么Flask负责小程序APIDjango负责管理后台先看Flask这边。小程序端请求的特点是简单、高频、数据结构固定无非就是提交隐患、上传图片、拉取任务列表、回传状态。Flask写这类REST接口非常轻快路由定义灵活请求参数解析直观一个文件就能串起整个接口层开发效率极高。而且Flask对JSON的处理非常顺手request.get_json()拿到的直接就是字典字段校验可以自己写很轻量的逻辑完全够用。再看Django这边。管理后台需要的是一整套重武器用户权限管理、数据模型管理、表单验证、Admin后台、ORM查询能力、批量导出。这些恰好是Django的看家本领。Django自带Admin后台建好模型之后几乎不用写代码就能获得一个可以增删改查的管理界面这对内部系统来说太实用了。另外Django的ORM查询语法在处理统计报表的时候非常舒适比如按隐患类型分组计数、按区县聚合、按时间段筛选几行代码就能出结果。双框架最关键的连接点是它们共享同一套数据库。我把MySQL库建好Django通过自己的模型管理这张表Flask这边用SQLAlchemy直连同一张表。实际使用中没有任何冲突因为两边对同一张表的字段定义保持一致只要注意字段名和类型对齐就行。如果你也想用这套方案务必约定好所有数据库操作最终都通过Django的ORM来维护表结构Flask只做查询和插入不做迁移。2.2 数据从采集到处置的完整链路我把系统的数据流画成一条流水线来描述这样方便理解巡查人员在路边发现一处路面坑槽打开小程序点击“新增隐患”。小程序先调用wx.getLocation拿GPS坐标再把拍摄的现场照片传给Flask的图片上传接口接着把隐患描述、类型、等级、定位地址这些字段POST给Flask的隐患创建接口。Flask收到之后做简单的字段校验然后把数据写入MySQL的隐患主表。数据落库的瞬间Django管理后台的列表中立刻就多了一条新记录状态是“待审核”。内业审核员登录后台点开这条记录看到完整的位置信息、照片和描述确认等级无误后点击“通过”这条隐患的作业状态就变成了“待处置”同时会通过后台的操作把它指派给对应的养护班组。养护班组处置完成后在后台回传处置结果和处置后照片审核员复核通过整条流程才算闭环。整个过程里小程序端不关心后台怎么流转后台也不关小程序怎么采集两边的沟通完全靠数据库这张表的状态字段来完成。这其实就是微服务里“共享数据库”模式的简化版——单体应用拆成两个服务但库只有一套既保持了拆分带来的开发灵活性又避免了分布式事务这种复杂问题。对中小型项目来说这个平衡点非常实用。2.3 数据库表设计隐患主表的核心字段数据表是整个系统的心脏字段设计直接决定了后续所有功能的复杂度。我的隐患主表核心字段如下字段名类型说明id自增主键隐患记录唯一标识codevarchar隐患编号规则H-日期-序号typevarchar隐患类型如路面坑槽/护栏破损/标线模糊/边坡塌方等leveltinyint隐患等级1严重/2较严重/3一般对应颜色和权值addressvarchar位置描述桩号路面名称longitude / latitudedecimalGPS坐标精确到6位小数photostext照片路径列表用JSON存储关联图片表也可以recordervarchar采集人姓名或工号remarktext现场情况补充描述statustinyint业务状态0待审核/1待处置/2已处置待复核/3已闭环create_time / update_timedatetime创建和更新时间设计上特别注意了两个细节。一是level没用字符串而是用了tinyint方便后续做等级权值计算和统计排序二是photos字段我用JSON字符串存了多张图片的路径列表而不是单独建一张图片表小项目用这种方式省事得多。如果后续图片量大再拆表也不迟。3. 小程序采集端定位、拍照、离线缓存是三个硬骨头小程序的开发工程量其实比想象中大因为在现场使用要考虑的细节非常多。核心功能就三个采集表单、定位、图片上传。每一个单独拿出来都不复杂但组合在一起在真实道路环境下就会出现各种意外情况。3.1 采集表单能短则短能选就不填外业采集最大的敌人是操作耗时长。巡查人员一天要在车里上上下下几十次如果每次录入都要花三分钟的键盘输入累加起来体感极差。所以我把表单设计原则定成能用选择器解决的绝不用手输能自动获取的绝不人工填写。具体来说隐患类型和等级用picker组件做成下拉选择类型选项事先在后台维护好小程序启动时从接口拉取并缓存。地址这一栏我先用wx.getLocation拿到经纬度再调逆地理编码接口把坐标转成文字地址自动填充到输入框里巡查人员只需要在偏差不大时做小幅修改。真正需要手动输入的只有“隐患描述”一个字段而且我在页面上给了常用的描述模板比如“路面出现明显坑洼面积约2平方米深度5厘米左右”点击即可填入再少量修改就能提交。表单提交的字段我做了严格的前端校验。比如等级必须选择位置坐标必须在有效范围内经度73-135纬度18-53照片至少一张。校验不通过直接在前端拦截避免把脏数据提交到服务端再被后面的逻辑拦截来回浪费网络请求。3.2 拍照上传先压缩再提交避免流量和超时道路现场拍照有两个特点一是拍得多一个隐患点通常要拍远景、近景、细节特写三张二是手机像素高随手一张就是3-5MB。如果原图直接上传在4G网络下每次提交都要等很长时间在信号弱的山区更是直接把请求卡死。所以我专门做了一个图片处理模块先用wx.chooseMedia拍照或选图拿到临时文件后通过wx.compressImage压缩压缩到宽度不超过1280像素、质量80%一张图基本能压到300KB以内。压缩完的照片并没有立刻上传而是先暂存在本地等用户填写完整个表单之后点击“提交”时才打包上传。这个设计的考虑是现场网络不稳定如果拍照一张传一张中途断网就会造成图片和隐患记录对不上号。打包上传的逻辑是先POST建隐患接口拿到返回的隐患ID再挨个上传图片到图片接口全部传完才把本地缓存的草稿标记为已提交。如果某张图传失败会进入重试队列最多重试三次仍然失败则提示用户检查网络后手动点击“重新提交”。3.3 定位纠偏GPS飘点问题不能靠前端解决定位是这个小程序最让我头疼的部分。wx.getLocation在城区街道能拿到比较准的坐标但到了山区公路和隧道附近误差动不动就是几十米甚至上百米。而道路隐患的位置恰恰要求尽可能精确因为养护人员要根据这个坐标找到现场。我的处理方案是“GPS文字辅助定位”双保险。拿到GPS坐标后小程序端调用逆地理编码把坐标转成“某省某市某区某路某桩号附近”这样的文字描述自动填进地址输入框。巡查人员如果发现坐标明显偏移可以直接手动修正地址文字同时保留原始GPS坐标不变。也就是说系统始终记录两套位置信息一套是机械采集的经纬度用于地图打点和轨迹回放一套是人工确认或修改的文字描述用于实际导航找点。数据入库时两者都会写入后台展示时优先展示文字地址。做的时候要注意一点微信小程序的wx.getLocation需要用户授权而且授权框只弹一次。如果用户误点了拒绝需要在页面里做一个“重新授权”的引导按钮调用openSetting让用户可以再次开启定位权限。这个细节很容易被忽略但不处理的话后续使用会频繁遇到定位失败的反馈。3.4 离线草稿给信号差的山区准备的兜底方案道路巡查经常走山路隧道里4G信号时有时无是常态。如果小程序强制要求在线才能录入基本无法在真正的现场恶劣环境下使用。所以离线能力是必须的不是可选项。我采用的方案是本地缓存池。用户填写的每一条完整记录包括压缩后的图片本地路径和所有字段值都先写入微信小程序的wx.setStorageSync。提交时先检查网络状态如果wx.getNetworkType返回的是none就直接把记录留在缓存池里页面上显示“未提交”的角标。等网络恢复后用户进入草稿箱点击“一键同步”程序会逐条把缓存记录提交到服务端提交成功的就从缓存池删除提交失败的保留并提示错误原因。这个功能上线后被巡查人员评价为“救命设计”。实测下来即使在完全无信号的隧道内连录了5条记录出来后找了个有网的地方花一两分钟全部同步完成数据一条不少。不过要提醒的是本地缓存涉及隐私和数据安全小程序端一定要在用户协议里明确说明本地存储了哪些数据并设置自动清理机制避免长期堆积泄露信息。4. Flask API层和小程序对接时最需要注意的几个环节Flask这层的定位是纯粹的数据采集API服务一共就七八个接口但麻雀虽小五脏俱全。我把对接过程中的几个关键环节单独拎出来说一下因为这些地方最容易被坑。4.1 接口清单与统一响应格式接口设计我遵循REST风格但没搞得很学术化一切从实用出发接口方法说明/api/auth/loginPOST用户登录返回Token/api/hazardsPOST创建隐患记录/api/hazards/ /photosPOST上传隐患照片/api/hazards/pendingGET获取当前用户未提交的草稿列表/api/hazards/offline-syncPOST离线记录批量同步/api/typesGET获取隐患类型配置/api/heartbeatGET健康检查响应格式我统一成一个字典结构{code: 0, message: success, data: {...}}。code0表示成功非0表示业务错误比如4001表示参数缺失4002表示Token过期5001表示服务内部异常。小程序端拿到响应后只需要判断code是否等于0大大简化了前端逻辑。这里有一个值得分享的经验接口报错时不要在message里写一堆技术细节而要写用户能看懂的话。比如后端校验发现经纬度超rangemessage直接写“经纬度参数超出有效范围请重新定位获取”小程序端拿到就直接弹toast给用户看省去前端再做一层错误码映射。这种设计在快速迭代的内部系统里非常实用。4.2 用户鉴权小程序端别用session用Token之前的项目里我用过Session来管理登录状态发现和小程序对接特别别扭。小程序没有Cookie机制还得手动维护SessionID而且天然存在跨端兼容问题。后来我全部改成Token方案用户拿工号密码调用登录接口服务端用itsdangerous生成一个带时效的签名Token返回给前端前端存在本地Storage里之后每次请求放在Authorization: Bearer token头里。Flask端写一个简单的装饰器做Token校验解析成功就把用户信息塞进g.user后续逻辑直接取用。Token过期时间我设的是7天因为巡查人员不可能每天重新登录这个时长在安全性和易用性之间比较平衡。另外要强调这个内部系统的使用范围仅限于微信实名或内部工号体系小程序端在登录页还要做手机号验证避免无关人员进入。4.3 图片上传接口multipart形式的文件接收Flask接收图片的核心代码非常简短但有几个坑需要绕开。第一是request.files要判断是否为空前端可能表单没传文件就发了请求第二是要限制文件格式和大小我规定只接受jpg、png、jpeg格式单张不超过10MB超出直接拒绝第三是文件名一定不能用前端传来的原始文件名要自己生成新文件名否则容易撞名且存在安全风险。文件名生成规则我是这么设计的hazard_{隐患ID}_{序号}_{8位随机字符}.jpg保存到服务器的/data/photos/2025/06/这样的按年月目录里方便后续归档和清理。图片URL通过Nginx的静态资源路径直接映射出来小程序端和后台都能通过拼接URL访问。完整的接收逻辑大概是下面这个样子app.route(/api/hazards/int:hazard_id/photos, methods[POST]) require_token def upload_photo(hazard_id): file request.files.get(file) if not file: return jsonify(code4001, message未接收到图片文件), 400 if file.content_type not in (image/jpeg, image/png): return jsonify(code4001, message仅支持jpg或png格式), 400 data file.read() if len(data) 10 * 1024 * 1024: return jsonify(code4001, message图片大小不能超过10MB), 400 filename generate_unique_filename(hazard_id) relative_path f/data/photos/{date.today():%Y/%m}/{filename} save_to_disk(data, relative_path) update_hazard_photo_list(hazard_id, relative_path) return jsonify(code0, messagesuccess, data{url: photo_url(relative_path)})4.4 和Django后台共建数据库SQLAlchemy直连MySQLFlask连数据库我用的还是SQLAlchemy的Flask扩展但模式上稍微特殊一点不定义完整的表模型而是在需要操作时用text()写原生的SQL语句。比如创建隐患记录就是用insert into hazard_records(...) values(...)这种原生SQL完成。这看起来很笨但在双框架共享数据库的场景下反而最安全。为什么因为凡是涉及表结构变更的比如增加一个字段、修改一个字段类型我都在Django那边通过migrations来做迁移迁移完成之后表结构就固定了。Flask这边如果把表模型定义一遍一旦两边模型定义不一致就会出现字段不知道是谁同步的混乱状态。而直接用原生SQLFlask这边就只是一个数据库访问工具不存在模型同步问题。实践证明这个取舍非常正确项目运行半年两边从来没有因为表结构冲突出过故障。5. Django管理后台让采集上来的隐患数据真正流转起来数据采集只是系统的前半段真正有价值的部分是后台如何把一条条零散的隐患记录管理起来变成可以处置、可以统计、可以追溯的有效信息。Django这一点上表现很棒。5.1 自定义Model和Admin不开代码也能改配置Django的models.py把隐患表结构定义好并迁移完成后admin.py里做一个简单的注册一个基础版的内部管理后台就出现了。管理员可以直接在后台列表页看到所有隐患按字段筛选、搜索点进去查看详情和照片然后编辑状态。这种开箱即用的能力在做内部系统时是真的能节省大量开发时间。不过默认Admin界面满足不了业务需求我做了一些定制。一是列表页默认只显示“待审核”状态的数据避免审核员一进来就被几百条历史数据淹没二是列表页展示字段我精简成隐患编号、类型、等级、位置、状态、采集时间六列字段多了反而让审核员抓不住重点三是详情页我把照片放到顶部用img标签直接一屏展示多张照片方便审核员快速浏览核对。admin.register(HazardRecord) class HazardRecordAdmin(admin.ModelAdmin): list_display (code, type, level, address, status, create_time) list_filter (status, type, level, create_time) search_fields (code, address, remark) # 详情页只读不允许审核员乱改 readonly_fields (longitude, latitude, photos_json, recorder, create_time)5.2 隐患状态流转不要做成想改就改的字典隐患数据的核心约束在状态流转。我一开始图省事状态就做成一个普通字段审核员爱怎么改怎么改。结果没跑多久就出了岔子有人把已经处置完的隐患直接改回了“待审核”流程全乱套了。后来我重构成了严格的状态机逻辑在Django的Model里定义好允许的转移路径。状态从“待审核”只能到“待处置”从“待处置”只能到“已处置待复核”从“已处置待复核”只能到“已闭环”或者退回“待处置”。凡是修改状态的请求都要通过Model层的一个专用方法先在方法里判断当前状态和目标状态是否构成合法转移不合法就拒绝并抛出异常。这套逻辑绕开了Admin自带的直接编辑用一个Action来替代普通修改。5.3 批量操作与统计报表审核员最喜欢的两个功能后台我实现的最受好评的功能是批量处理。审核员每天要面对几十条新隐患如果每条都要点进详情页看一眼再返回列表效率太低。我在Admin列表页加了两个Action一个是“批量通过”将选中的待审核记录一键置为待处置一个是“批量导出Excel”把筛选好的数据导成表格用于汇报和归档。统计报表部分我写了一个简单的Dashboard页面用高德或百度地图API把隐患点位标到地图上同时做了几个维度的统计卡片本月新增隐患数、待处置未处理数、各类型隐患占比、各等级隐患数量。数据查询全部用Django ORM的聚合函数比如按月分组的数量统计就是annotate加TruncMonth一行代码搞定Django在这种场景下的表达能力真的舒服。5.4 导出Excel编码问题和字段顺序的坑Excel导出是个看似简单实则容易踩坑的环节。我最初直接用csv模块写文件结果中文乱码后来改成用openpyxl生成xlsx格式才真正解决。有一段时间导出文件总是在Excel里提示“是否恢复”后来发现是文件流缺少必要的格式属性手动设置好列宽、表头样式、单元格格式才消停了。导出字段顺序也要仔细设计要按业务人员的阅读习惯排列而不是数据库字段顺序。我导出的列顺序是隐患编号、隐患类型、等级、位置地址、经度、纬度、照片数量、采集人、采集时间、当前状态。多亏了这个细致的排序列汇报的时候省了很多功夫。6. 部署上线后踩过的坑从GPS漂移到并发上传系统跑起来不难真正见真章的是上线后的稳定性。我在实际部署和运维过程中踩了不少坑挑几个有代表性的分享特别是排查思路新手遇到同类问题可以按这个流程走。6.1 GPS坐标明明有值地图上却定位偏了十几公里上线第二天就收到巡查人员反馈明明在路边拍的照片后台点开地图定位却偏到隔壁县城去了。我排查了一下发现不是地图API的问题而是采集端把经纬度作为字符串提交SQLAlchemy直连MySQL时字段类型写成了varchar导致经纬度被当成普通字符串存了起来。后台展示时用了字符串截取之类的逻辑自然就变形了。排查链路是先看数据库里存的值发现确实是一长串数字带了奇怪的格式标识再回看Web请求日志发现Flask收到的坐标是正确的最后检查数据库表结构才定位到字段类型是varchar而不是decimal。修复方案就是在Django端把字段类型改成decimal(10,6)重新迁移数据这样经纬度计算精度也保证了。这个坑很值得记下来经纬度字段一定要用decimal类型不要用float更不要用varchar。6.2 多人同时上传照片图片出现丢失和错乱上线一段时间后有几天巡检每天要处理三四百条隐患突然出现了部分记录照片缺失的情况。排查发现是并发上传的图片文件名撞了。当时生成文件名用的是时间戳加序号并发请求在同一毫秒内会被分配相同的名字后写的文件覆盖了先写的导致丢失。修复方案是生成文件名时加入随机串和隐患ID同时保存前检查目录里是否存在同名文件存在就重新生成。这说明了一个很基本的道理并发场景下不能依赖纯递增的序号做唯一标识。用UUID或者uuid4().hex生成随机文件名是最稳妥的。这也是我后来统一用hazard_{id}_{uuid4().hex[:8]}.jpg这种命名方式的原因。6.3 弱网环境下离线同步把数据库连接池打爆了这个坑很有意思。有一次一个班组在山里巡查全程没信号攒了上百条离线记录。下午回到有网的地方大家同时打开小程序点“一键同步”一瞬间Flask服务的数据库连接池直接被打满出现大量超时错误。问题根源在于同步接口是每收到一条记录就开一个数据库连接并发一高连接数就失控了。优化方案是两条腿走路第一Flask端改为一次同步请求接收一个记录列表循环插入时复用同一个数据库连接把异步逐条提交改成批量提交第二前端同步时加一个节流控制用一个全局队列无论用户怎么点同时只允许一个同步任务在执行。这样改完之后再遇到大规模离线补录服务端负载平稳多了。6.4 小程序审核与隐私合规的注意点最后提醒一下做过小程序的人都知道的隐私合规坑。因为小程序要使用定位和相机功能在微信平台提审时必须在app.json里声明权限相关用途同时在小程序的“用户隐私保护指引”里明确注明收集位置信息、照片信息的目的。如果漏填了审核会被打回即使上线了后续也会有隐私整改的要求。这套系统因为是内部工具我直接采用微信内部测试版本分发给指定成员使用绕开了公开审核的不少麻烦但内部使用也必须遵守平台规范。写在实际投入使用之后这套系统从开发到稳定使用前后大概花了两个月时间。真正让它跑起来的不是那些炫技的代码而是对业务场景的透彻理解——巡查人员要的是快审核人员要的是准管理人员要的是全。开发者如果只盯着技术指标不去现场跟几天巡查路线很难设计出真正好用的工具。我最深的体会是工具类系统最忌讳功能堆砌。表单能缩就缩流程能简则简能用选择器解决的绝不让用户打字。每次觉得“这个功能加上去也没什么”的时候问问自己一线人员是否真的需要如果答案不明确那就先砍掉。如果以后要在这个项目上继续扩展我可能会把照片数据接图像识别自动识别路面裂缝、护栏歪斜等常见隐患类型也会考虑把隐患数据做空间分析找出事故高发路段和隐患聚集区。不过这些都是后话了先把基础流程跑稳定让每天在路上的巡查人员用着顺手才是这套系统最大的价值。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询