水边发现垃圾成堆、排污口黑水直排、河面上漂着死鱼,这类问题以前发现后想反馈,要么找不到部门,要么电话说不清位置。我这次做的就是一套微信小程序端配合 django/flask 后端的河流生态问题举报系统,让普通市民遇到环境问题掏出手机就能拍照、定位、提交,环保部门在后台接单、处理、反馈,整个流程线上闭环。这套项目的代号是 g2o4609q,属于典型的“小程序上报 + 服务端受理”业务模型,适合正在学 django 或 flask 的开发者当实战参考,也适合有环保、市政、社区治理类信息化需求的人做原型蓝本。
这篇文章我会把从需求拆解、技术选型、数据建模、接口设计、小程序端实现到部署上线的完整过程讲一遍,重点说清楚“为什么这么设计”,而不只是贴代码。
1. 项目背景与整体设计思路
1.1 河流生态举报的业务痛点与核心需求
河流生态问题举报,本质上是一个“发现问题 - 上报问题 - 流转处理 - 结果反馈”的闭环流程。但实际做下来你会发现,业务痛点不在“举报”本身,而在“信息完整度”和“处理可追踪”这两件事上。
先想一个场景:河边有人倾倒建筑垃圾,路人看见了,想举报,但电话里说不清具体位置,也说不清倒的是什么、倒了多少。等他找到相关部门的公众号或网站,可能已经过去一两个小时,人早走了。如果有一套小程序系统,让举报人直接在地图上定位当前位置,拍三张现场照片,选择问题类型,填写一段文字描述,几秒内提交,后台管理员就能看到精确的经纬度、时间、现场实拍图和问题分类,处理效率完全不一样。
所以这个系统的核心需求可以拆成四块:
- 用户端快速上报:定位准确、操作简单、支持图片和文字描述
- 后台受理流转:管理员能看到举报列表、详情、状态,并派单处理
- 全流程可追踪:用户能查自己提交的举报处理到哪一步了
- 数据可统计:按问题类型、区域、时间维度看趋势,方便后续治理
这四块需求,决定了前端要有地图定位能力,后端要有一套稳定的数据模型和状态机,部署要能扛住一定并发,小程序端则必须处理好权限授权、文件上传、HTTPS 请求这些微信平台的硬性要求。
1.2 技术选型:django 还是 flask
标题里写着 django_flask,其实当初做技术选型时还真在这两个框架之间纠结过。两个都是 Python 社区的主流 Web 框架,但定位差别很大,我把对比列在下面。
| 对比维度 | django | flask |
|---|---|---|
| 自带功能 | 内置 ORM、Admin 后台、认证、表单、迁移工具 | 核心极简,ORM、认证、Admin 全靠扩展 |
| 学习曲线 | 稍陡,概念多,但体系完善 | 平缓,上手快,自由度高 |
| 适合场景 | 中大型业务系统、需要后台管理的项目 | 轻量 API、微服务、快速原型 |
| 开发效率 | 高,很多功能开箱即用 | 高,但需要自己组装 |
| 数据迁移 | 内置 migrations,非常方便 | 常用 Flask-SQLAlchemy + Flask-Migrate |
| 后台管理 | 自带 admin,注册模型即可用 | 需要 django-admin 类似的第三方库(如 flask-admin) |
对于河流生态举报系统来说,管理员后台是刚需——环保部门的人需要看列表、改状态、填处理意见。django 自带的 admin 在这一类场景里简直是“白送”的福利,注册好模型就能生成可用的管理界面。所以我最终选了 django 作为主框架,flask 在这个项目里承担的是一个内部工具服务的角色——做数据统计的定时任务和对外数据接口。这也解释了项目标题里为什么两个框架同时出现。
选 django 还有两个实际理由。一是它的 ORM 在处理“举报单 - 处理记录 - 用户 - 图片”这类关系型数据时非常顺手,查询、聚合、事务都写得很省心。二是 django 的迁移机制在迭代改表结构时太友好了,开发阶段字段说改就改,一条 makemigrations 就搞定,不会出现手工维护 SQL 的噩梦。
1.3 系统架构与核心模块划分
整体的架构分三层:微信小程序前端、django 后端服务、MySQL 数据库(实际部署用 MySQL,开发时用 SQLite 也够)。
微信小程序(举报、查询、我的) ↓ HTTPS/JSON django 服务(drf 接口 + admin 后台 + 认证) ↓ ORM MySQL(用户 / 举报单 / 处理记录 / 问题类型)核心模块我划分为五个:
- 用户模块:复用 django 自带的 User 模型,加上微信登录时保存 openid、昵称、头像
- 举报模块:举报单的创建、查询、状态流转,是整个系统的心脏
- 图片模块:处理小程序上传的现场照片,限制大小、保存路径、生成访问 URL
- 统计模块:按日、周、月,按问题类型和区域统计举报数量
- 后台管理模块:django admin 的模型注册与自定义列表展示
这套划分方式的好处是边界清晰,每个模块之间只通过接口和数据表关联,后期加功能时不容易踩踏。比如以后想接一个短信通知,只需要在处理记录模块上加一个钩子,不需要动举报模块。我实际开发中感受最深的一点是:业务系统的设计不在于一开始多花哨,而在于模块边界划得干净,后面改动时才知道哪里下手。
2. 数据模型与管理后台设计
2.1 核心数据表结构与关系说明
数据模型是这类业务系统的地基,设计得好不好,直接影响后续的查询效率和扩展性。我用 django 的 models 定义了四张核心表。
第一张是问题类型表(ProblemType),用于保存“工业排污、生活污水、垃圾倾倒、违规采砂、水华、死鱼、岸线破坏、其他”这类分类项。之所以单独建表而不是写死在代码里,是因为环保治理的问题分类经常要调整,后台管理员直接增删比改代码方便得多。
第二张是举报单表(Report),字段设计如下:
- reporter:外键关联用户,记录谁举报的
- problem_type:外键关联问题类型
- title:一句话标题,比如“XX河段发现大量建筑垃圾”
- description:详细描述,补充现场情况
- location_name:具体地点名称,由小程序端逆地址解析得到
- longitude / latitude:经纬度,精确到小数点后 6 位
- photos:JSON 字段,存一组图片的路径列表
- status:状态,待受理、受理中、已处理、已驳回
- created_at / updated_at:创建和更新时间
第三张是处理记录表(HandlingRecord),记录每一单的处理过程。字段包括 report(外键)、handler(处理人)、content(处理意见)、status(处理后状态)、handled_at(处理时间)。这样设计的好处是保留了“过程留痕”——某张举报单从待受理变成已处理,中间经历了什么,每一步谁操作的、说了什么,全都能查。
第四张是举报图片表(ReportImage),单独建表是为了后续做图片的审核、压缩、删除等管理操作更方便。字段有 report、image_url、uploaded_at、is_valid。当时也考虑过直接塞在 Report.photos 里,后来想想图片是独立的资源,管理需求多,单独建表更合适。
三张业务表的关系很清晰:用户和举报单是 1 对 N,举报单和处理记录是 1 对 N,举报单和举报图片是 1 对 N。
2.2 状态流转机制设计
状态流转是这个系统最容易被忽略但非常重要的设计点。我定义的举报单状态有四个:
- 待受理:用户刚提交,管理员还没处理
- 受理中:管理员确认问题属实,已安排人员处理
- 已处理:问题已解决,处理结果已填写
- 已驳回:经核查,举报不属实或不属于受理范围
| 当前状态 | 允许流转到 | 触发动作 |
|---|---|---|
| 待受理 | 受理中、已驳回 | 管理员审核 |
| 受理中 | 已处理 | 处理完成 |
| 已驳回 | 受理中 | 管理员重新审核 |
这个状态机看着简单,但实现时要注意一点:状态迁移必须写操作记录,不能直接 update status。我在处理记录表里强制要求每次状态变更都生成一条 HandlingRecord,这样用户在端上看到的进度时间线就有数据支撑。用 django 的信号机制或者视图函数里统一处理都可以,关键是要让逻辑保持一致性。
状态流转设计里还有个小细节:已驳回的举报单,用户如果对结果不满意,能不能重新申诉?我当时没做申诉功能,但预留了一个 remark 字段,管理员可以在驳回时填写原因,用户在端上能看到为什么被驳回,这比单纯的“已驳回”三个字贴心很多。
2.3 管理后台的配置要点
django admin 是选型时的重要加分项,实际用下来也确实香。但默认的 admin 列表页不够贴合业务需求,需要做几个配置。
给 Report 模型注册 admin 时,我做了三件事:列表页默认按创建时间倒序,让最新举报排在最前面;列表展示的列包含举报标题、问题类型、状态、位置、创建时间;右侧过滤按状态和问题类型加 Filter(list_filter)。同时添加了一个 actions 操作,选中多条举报记录可以批量修改状态,处理批量巡查情况时效率提高很多。
还有一点是关于图片预览的。默认 admin 里外键图片字段只显示文件名字符串,不直观。我在 Report 的 admin 里添加了一个自定义列方法,返回 HTML 把图片缩略图拼出来,用 format_html 渲染,这样后台人员一眼就能看到现场照片,不用点进去再翻。
后台权限也要简单说下。django 自带的 User 权限系统可以直接用,我给管理员分了两类:普通管理员只有查看和修改状态的权限,超级管理员才有删除和配置问题类型的权限。这样即使操作失误,也最多改错了状态,不至于删掉历史数据。
3. 小程序端核心页面与交互实现
3.1 举报表单页:定位、拍照、描述的实战处理
小程序端最核心的页面就是举报表单页,一次完整的举报填写要经历四步:授权定位、现场拍照、选择类型、填写描述。
定位这块,微信小程序提供了 wx.getLocation 接口,但有个坑:用户首次使用时如果不授权位置信息,接口会直接报错。我处理的方法是先调用 wx.getSetting 检查授权状态,如果没授权就弹窗引导用户去设置页开启。实际测试下来,iOS 和 Android 授权交互还有细微差别,所以在代码里加了一个统一的引导函数,被拒绝授权时提示用户进入“设置 - 定位”手动开启。
拍照上传用的是 wx.chooseMedia,支持从相册选或直接调起相机。我建议在代码里限制 count 为 3 张以内,免得用户传一堆照片把服务器撑爆。图片选好以后先本地压缩再上传,微信的 canvas 压缩逻辑或者直接限制 chooseMedia 的 sizeType 选 compressed 都可以,压缩后的单张图片控制在 200KB 以内比较稳妥。
定位拿到经纬度后,还有个体验点要做:页面不能只显示数字经纬度,用户看不懂。我接入了腾讯地图的小程序 SDK,调用逆地址解析接口,把经纬度转成“XX区XX路XX号附近”这样的文字,回填到定位控件里,同时允许用户手动修正位置描述。这一步成本不高,但对体验提升非常大。
提交按钮的处理也要注意:点击后把表单数据先本地校验一遍,标题不能为空、图片至少一张、经纬度必须有效,校验通过后先调上传图片接口拿到图片 URL,再带着图片 URL 一起提交举报单。这两个请求按顺序来,不要并发。
3.2 问题列表与进度查询
举报提交后,用户去哪里查进度?我做了两个入口:首页的“我的举报”列表页和举报详情页。
列表页用微信小程序的 onShow 生命周期来刷新数据,用户从详情页返回、或者其他操作导致页面重新显示时,自动拉取最新列表。这里有个经验:列表页不要用 onLoad 拉数据,因为页面实例在 tab 页间切换时不会反复触发 onLoad,但 onShow 每次都会触发,配合一个 loading 状态就能实现不错的下拉刷新体验。我再加了一个“加载更多”的分页逻辑,每次加载 10 条,滑到底部自动加载下一页,避免数据量大时首屏卡顿。
详情页除了展示举报的时间、图片、描述和状态之外,最关键的是状态时间线。我让后端把 HandlingRecord 按时间倒序输出成一个列表,小程序端用一个简单的竖向时间轴样式展示:每条记录包含处理状态、处理意见、处理时间。用户看到“待受理 - 受理中 - 已处理”这样一条清晰的时间线,心里就踏实了。
3.3 小程序端接口对接与常见坑
小程序和 django 后端对接时,有几个微信平台的硬性限制必须提前了解。
第一是 HTTPS 要求。微信小程序正式环境要求所有请求域名必须是 HTTPS,而且要在小程序管理后台配置 request 合法域名。本地开发时可以在开发者工具里勾选“不校验合法域名”,但上线前必须配好。
第二是请求中的 data 类型。wx.request 的 data 参数如果是对象,会自动序列化成表单格式。我的后端接口接收 JSON,所以请求时要把 header 设置为Content-Type: application/json,data 用JSON.stringify转成字符串。这个坑我踩过一次,忘了设置头,后端一直收到None。
第三是登录态的传递。小程序没有传统的 cookie 概念,登录后拿到一个 token,后续请求都放在 header 的 Authorization 字段里传给后端。django 端用了一个简单的 TokenAuthentication,每次请求先解析 token,再拿到当前用户,权限控制也在这层做。
第四是图片域名。上传的图片 URL 也必须使用 HTTPS 域名,否则图片在小程序里显示不出来。这个属于部署层面的问题,后面第 5 节会详细说。
4. 后端接口开发与关键流程实现
4.1 API 设计规范与接口清单
后端接口我按照 RESTful 风格设计,核心接口如下:
| 方法 | 路径 | 功能 | 权限 |
|---|---|---|---|
| POST | /api/auth/login | 微信登录,换取 token | 公开 |
| POST | /api/reports/ | 提交举报单 | 登录用户 |
| GET | /api/reports/?page=1 | 获取我的举报列表(分页) | 登录用户 |
| GET | /api/reports/{id} | 获取举报详情 | 登录用户 |
| PUT | /api/reports/{id}/status | 更新举报状态 | 管理员 |
| POST | /api/upload/image/ | 上传举报图片 | 登录用户 |
| GET | /api/stats/summary | 获取统计数据 | 管理员 |
几个设计时要注意的点:
- 列表接口必须分页。返回内容里带上 total、page、page_size,方便小程序端做“加载更多”
- 列表接口的返回字段不要全量输出。description 这种长文本在列表场景下可以只返回前 50 个字,详情接口再返回完整内容
- 状态更新接口要校验流转合法性,不能允许“已处理 -> 待受理”这种倒流
- 举报详情接口除了举报单本身,还要返回处理记录列表,一次请求搞定,避免小程序端再发一次请求
4.2 举报提交与图片上传的完整流程
图片上传我用 django 自带的 FileSystemStorage 处理,保存在服务器 media 目录下,nginx 负责提供静态访问。上传接口接收 multipart/form-data 格式的文件,保存后返回图片 URL。
# views.py 中的图片上传核心逻辑 from django.views.decorators.http import require_POST from django.http import JsonResponse from django.core.files.storage import default_storage @require_POST def upload_image(request): file = request.FILES.get('file') if not file: return JsonResponse({'code': 400, 'msg': '缺少文件'}) # 限制图片大小,超过 2MB 直接拒绝 if file.size > 2 * 1024 * 1024: return JsonResponse({'code': 400, 'msg': '图片不能超过 2MB'}) # 校验扩展名 ext = file.name.rsplit('.', 1)[-1].lower() if ext not in ['jpg', 'jpeg', 'png', 'webp']: return JsonResponse({'code': 400, 'msg': '不支持的图片格式'}) path = default_storage.save(f'reports/{request.user.id}/{uuid4().hex}.{ext}', file) url = f'/media/{path}' return JsonResponse({'code': 200, 'data': {'url': url}})举报提交的接口稍微复杂一些,除了常规字段,还要校验经纬度范围——国内范围大致是经度 73 到 135,纬度 18 到 53,超出这个范围的数据大概率是前端拿错了或者恶意提交。然后创建 Report 对象,同时根据状态初始化一条处理记录,初始状态就是“待受理”。
这里有个容易被忽略的点:用户连传了三张图,但恶意刷接口时可能只传 URL 不传真实文件。所以后端在提交举报单时,会校验图片 URL 是否存在、是否属于当前用户上传。具体的做法是记录图片 URL 和上传用户的关联,提交时做一次归属校验。这个问题在纯前端校验时根本发现不了,实测上线后就会有各种异常数据进来,后端校验必须做扎实。
4.3 查询、状态更新与处理记录实现
查询接口的核心是过滤和分页逻辑。我用 django ORM 的 filter 做按用户筛选,order_by('-created_at') 做时间倒序,再用 django 内置的 Paginator 分页。这里有一个性能建议:查询列表时用 select_related 把外键的 problem_type 和 reporter 一次性查出来,否则每条记录都会产生额外的 SQL 查询,也就是经典的 N+1 问题。
状态更新接口的逻辑稍微绕一点。前端传来目标状态和处理意见,后端先做几件事:
- 校验当前状态是否允许流转到目标状态
- 校验当前用户是否有管理权限(is_staff 字段)
- 更新 Report 的 status 字段
- 创建一条 HandlingRecord,记录处理人和处理意见
# 状态流转合法性判断 ALLOWED_TRANSITIONS = { 'pending': ['processing', 'rejected'], # 待受理 -> 受理中 / 已驳回 'processing': ['processed'], # 受理中 -> 已处理 'rejected': ['processing'], # 已驳回 -> 受理中(复核通过) }我实际开发时遇到过一个问题:管理员想批量处理举报,逐个点详情修改太慢。后来在 admin 后台加了批量 action,选中多条直接统一改成“受理中”或“已处理”,并在 actions 里自动生成处理记录。这个改动让后台的使用体验好了很多,尤其是巡查后一次性处理十几单的场景。
4.4 数据统计接口的设计
统计模块是我额外加的一个功能,因为环保部门非常需要“这类问题多不多、集中在哪”的数据支撑。统计接口返回三个维度的数据:
- 总量趋势:按天统计最近 30 天的举报数量,画成折线图
- 类型分布:按问题类型统计数量,画成饼图
- 区域分布:按 location_name 的前缀(比如“XX区”)统计数量
django ORM 里做这种聚合查询很方便,用 annotate 加 Count,配合 ExtractDay 之类的函数按时间分组。关键是接口返回的数据格式要好用,我直接返回数组,小程序端拿到后渲染图表就行。
这个统计接口我用 flask 写了一个独立版,跑在同一个项目目录下的 flask_app.py 里,通过访问不同的端口区分。之所以用 flask 单独写,是因为这个统计模块逻辑简单、不需要 django admin 那一套,flask 写起来更轻快。这既验证了 flask 的灵活性,也避免把 django 项目搞得太臃脏。
5. 部署上线与常见问题排查
5.1 服务器部署的完整要点
部署环节我踩过的坑最多,单独拿出来讲。先说整体方案:一台云服务器,nginx 做反向代理和静态文件服务,django 用 gunicorn 跑,MySQL 作为数据库,小程序前端代码通过微信开发者工具上传到微信服务器。
nginx 配置里两个关键点:
- 将
/api/路径反向代理到 gunicorn 的 8000 端口 - 将
/media/路径直接指向存放图片的目录,图片访问不经过 django,速度更快
server { listen 443 ssl; server_name api.example.com; ssl_certificate /etc/nginx/ssl/server.crt; ssl_certificate_key /etc/nginx/ssl/server.key; location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /media/ { alias /var/www/report_system/media/; } }gunicorn 启动时要注意 worker 数和线程数。实测单台 2 核 4G 的服务器,配置 4 个 worker、每个 worker 2 线程,能稳定支撑住日均几千次请求。不要贪多,worker 太多反而会因为内存不足导致频繁重启。
还有一个小程序特有的部署要求:request 合法域名、uploadFile 合法域名、downloadFile 合法域名,三个域名配置都要在小程序后台设置,而且必须是备案过的 HTTPS 域名。图片的 media 域名也得在 downloadFile 合法域名里配好,否则图片裂开。
5.2 常见问题排查速查表
我把开发部署过程中真实遇到的问题整理成一个速查表,每个问题都附了排查思路。
| 问题现象 | 排查思路 | 解决方案 |
|---|---|---|
| 小程序请求接口报 404 | 检查 nginx 代理路径,以及 django 的 url 是否匹配 | 打开 nginx access.log,看实际请求路径 |
| 请求接口报 403 | 多半是 CSRF 中间件拦截 | 在视图上加 @csrf_exempt,或者统一配置排除 |
| 图片上传成功但访问 404 | media 路径未在 nginx 正确配置 | 确认 alias 路径和文件实际存储路径一致 |
| 用户定位不准 | 小程序 getLocation 返回坐标与地图 SDK 坐标体系不一致 | 统一使用 GCJ-02 坐标系,腾讯地图默认就是 |
| 列表数据加载慢 | 外键未预查,产生 N+1 查询 | 加 select_related / prefetch_related |
| 请求返回数据为 null | 客户端未设置 Content-Type 为 JSON | wx.request 里显式设置 header |
| 后台无法登录 | django 的 ALLOWED_HOSTS 未包含服务器域名 | settings 里配置 ALLOWED_HOSTS = ['api.example.com'] |
排查问题时我最常用的就是看 nginx 和 gunicorn 的日志,比什么都管用。真有接口报错了,先看 access.log 确认请求有没有到后端,再看 gunicorn 的 error.log 确认有没有 Python 异常,两步基本定位 90% 的问题。
5.3 性能与安全优化建议
系统上线跑了一段时间,我做了一轮优化,有些经验可以分享。
性能层面,最值得做的是 Django 缓存和数据库索引。状态过滤、时间排序这种高频查询加索引之后,速度提升非常明显。
class Report(models.Model): # ... 其他字段 class Meta: indexes = [ models.Index(fields=['status', 'created_at']), models.Index(fields=['reporter', 'created_at']), ]安全层面,有几件事是必须做但容易忽略的:
- 图片上传必须校验文件类型和大小,防上传恶意文件。JPG、PNG、WebP 之外的一律拒绝
- 所有接口都要做登录校验,不能在路由层遗漏
- 数据库备份要做定时任务,我用 cron 每天凌晨做一次 mysqldump 备份,保留最近 7 天
- django 的 DEBUG 在生产环境必须设为 False,否则报错信息会泄露服务器路径和配置信息
对于举报系统的信息展示,用户头像、昵称这类敏感信息在列表接口中做脱敏处理,只保留必要字段。管理员操作属于内部行为,后端要记录每次改动的操作人,防止误操作或越权操作无法追溯。
6. 实测过程中的经验与建议
项目从开发到上线,我最大的感受是:这种业务系统,七分靠流程设计,三分靠代码。
流程设计的关键是“状态机”。一开始我只设计了举报和查询两个页面,后台直接把状态改成“已完成”,用户看不到中间过程,结果大量用户反馈“提交了没动静”。后来补齐了“待受理 -> 受理中 -> 已处理”的完整状态流转,并把每一步处理意见都在小程序端展示出来,投诉率立刻降下来了。这件事给我的教训是:用户要的不是一个结果,而是过程可见,哪怕只是“正在处理中”几个字,也能解决绝大部分焦虑。
另外一个小技巧是关于测试数据的构建。开发阶段我直接用 django shell 写了一个脚本,批量生成 100 条带随机位置、随机类型、随机状态的举报数据。测试定位、图表、分页这些功能时,这份假数据帮了大忙。具体做法就是用 for 循环加 random.choice,几分钟就能生成一套看起来非常真实的测试集。
最后分享一个关于扩展的想法。当前系统已经可以支撑“发现问题 -> 线上举报 -> 后台受理 -> 结果反馈”的完整闭环。如果后续想做得更好,可以考虑接入短信或模板消息通知,状态流转时自动给用户发消息;也可以把统计数据做成大屏展示,对接地图组件把举报点直接标注在地图上;还可以做一个公众建议板,让用户对处理结果进行满意度评价。这些功能都不需要改动地基,我在设计时特意留了扩展位。
希望这篇文章对正在做类似小程序政务/环保项目的朋友有帮助,遇到具体的坑可以在评论区聊聊。