简介:geolog-app 是一套基于 Python 与 Django 框架开发的地质类 Web 应用源码包,面向希望掌握 Django 项目结构、地质数据管理与可视化展示的开发者。压缩包共 22 个文件,包含 14 个 Python 脚本、2 个文本说明、1 个 HTML 模板、1 个 CSS 样式表、1 个 SQLite 数据库文件及部署配置,整体仅 14KB,结构紧凑,便于快速通读。源码按标准 Django 工程组织,涵盖设置文件、路由映射、视图逻辑、模型定义、模板渲染与后台管理配置等核心部分,并附带一个轻量级数据库,可直观看出地质数据在模型中的存储与调用方式;同时包含依赖清单与云端部署配置,方便在本地或服务器环境运行。目前已有 162 人学习下载,适合作为地质信息管理、课程设计或 Django 入门实战的参考资料,尤其适合需要短时间内看懂项目代码组织方式的初学者与开发人员。
1. geolog-app 是什么:一套基于 Python 的地质野外记录与地图展示系统
野外实习回到寝室整理资料,最怕的不是当天走了多少山路,而是点号对不上照片、岩性描述还留在纸质记录本里需要重新录入。这个项目的出发点就是把这条链路压缩掉:geolog-app 用 Python 写成,后端基于 Django 和 Django REST Framework,数据交换格式统一用 GeoJSON,前端地图用 Leaflet 渲染。野外用手机浏览器新增点位,填写岩性、层号、样本编号,记录会连同经纬度一起入库;回到电脑前打开同一个页面,所有点位自动落在互动地图上,点击标记就能回看当时的野外描述。
它适合地质院校的课程实习,也适合工程勘察小团队做轻量化的野外数据采集。不需要安装桌面端 GIS 软件,有一个浏览器、一台能定位的移动设备就能跑起来。
2. 技术选型与项目结构:Django 做后端,GeoJSON 做数据桥梁
2.1 技术栈:为什么是这个组合
用 Python 做这类信息管理系统,Django 是最常见的选项。它自带 Admin 后台、ORM、迁移机制,写野外记录这类 CRUD 业务几乎不用额外搭架子。接口层用 Django REST Framework 暴露 JSON,前端不管是纯网页还是以后要接小程序、桌面端,接口都能直接复用。
地图组件我一般会用 Leaflet 而不是 OpenLayers。对几个打点记录的小项目来说,Leaflet 体积小、上手快、文档也直白,OpenLayers 的那套重型工具链在这个体量下完全是杀鸡用牛刀。底图默认用 OpenStreetMap 瓦片,以后想换 ArcGIS 或者内网瓦片服务,Leaflet 只需要改一行tileLayer的 URL。
数据库本地开发用 SQLite,部署到服务器后换 PostgreSQL。之所以留这个切换余地,是因为野外记录多了以后会涉及空间查询,PostgreSQL 装 PostGIS 扩展以后可以直接跑「找半径 500 米内所有点位」这类 SQL,SQLite 做不到。项目里没有强制依赖 GIS 扩展库,GeoJSON 只作为数据导出格式,所以下载后在普通电脑上就能直接跑,不需要装一堆地理相关的 Python 包。这一点对新手很友好,跑起来不会因为缺 GDAL 之类的东西直接翻车。
2.2 目录结构:一个适合小团队的项目布局
项目的目录规划比较克制,没有上多模块复杂架构,按「配置、业务、前端」三块切分,源码解压之后大致长这样。
geolog-app/ ├── config/ # Django 项目配置 │ ├── settings.py │ └── urls.py ├── geolog/ # 核心业务模块 │ ├── models.py # 点位、分层、样本三张表 │ ├── serializers.py │ ├── views.py # CRUD 接口 + GeoJSON 导出 │ ├── geo.py # 坐标转换工具 │ ├── migrations/ │ └── urls.py ├── frontend/ │ ├── index.html # 单页入口 │ ├── map.js # Leaflet 地图渲染 │ └── style.css ├── fixtures/ │ └── initial_data.json # 演示用点位数据 ├── requirements.txt └── manage.pyconfig文件夹只做全局配置和根路由,geolog文件夹承载全部业务逻辑。有个细节值得注意:frontend没有单独拉一个前端工程,而是直接作为 Django 的静态文件目录使用,视图层渲染index.html即可。对小团队来说少一套 Node 构建流程,省掉 npm install 和 webpack 配置,维护成本低很多。项目跑不起来的时候排查范围也更小,问题基本都集中在 Python 这一侧。
2.3 核心数据模型:点位、分层、样本三张表
野外记录可以抽象成三个业务对象:点位是空间位置,分层是点位下的地层/岩性描述,样本是采集的实物样品。它们天然形成一对多的关系:一个点位下面有多层地层描述,每层下面又关联若干样本。模型代码是这么设计的。
# geolog/models.py from django.db import models class Station(models.Model): """野外调查点 / 打点位置""" name = models.CharField('点位名称', max_length=100) longitude = models.FloatField('经度') latitude = models.FloatField('纬度') elevation = models.FloatField('高程', null=True, blank=True) description = models.TextField('野外描述', blank=True) created_at = models.DateTimeField('记录时间', auto_now_add=True) class Meta: ordering = ['-created_at'] def __str__(self): return self.name class Strata(models.Model): """地层 / 露头分层记录""" station = models.ForeignKey( Station, on_delete=models.CASCADE, related_name='strata', verbose_name='所属点位', ) layer_no = models.IntegerField('层号') rock_type = models.CharField('岩性', max_length=50) thickness = models.FloatField('厚度(m)', default=0) description = models.TextField('岩性描述', blank=True) class Meta: ordering = ['layer_no'] class Sample(models.Model): """采集样本""" station = models.ForeignKey( Station, on_delete=models.CASCADE, related_name='samples', verbose_name='所属点位', ) sample_code = models.CharField('样本编号', max_length=50) lithology = models.CharField('岩性', max_length=50) remark = models.TextField('备注', blank=True)经纬度字段选FloatField而不是DecimalField,这是有意的取舍。DecimalField精度更高、没有浮点数误差,但从接口接收 GPS 数据时前端传来的本来就是 JavaScript 浮点数,序列化阶段还要做字符串转换,徒增复杂度。对野外记录这个精度足够,保留到小数点后六位大概对应 0.1 米,远超手持 GPS 的定位精度。
外键上的related_name参数值得解释一下:写related_name='strata'之后,从点位反查分层数据就变成station.strata.all(),语义非常顺。如果不写这个参数,Django 默认生成station.strata_set.all(),模型多了以后可读性会差很多。on_delete=models.CASCADE表示点位删除时分层、样本记录一并删除,野外数据是强依赖关系,删了打点位置,下面的描述留着也没有意义。
2.4 序列化器:模型怎么暴露给前端
Django 模型不能直接被 JSON 序列化,DRF 的ModelSerializer负责把模型转成可传输的 JSON,同时承担接口数据的校验。
# geolog/serializers.py from rest_framework import serializers from .models import Station, Strata, Sample class StrataSerializer(serializers.ModelSerializer): class Meta: model = Strata fields = ['id', 'layer_no', 'rock_type', 'thickness', 'description'] class SampleSerializer(serializers.ModelSerializer): class Meta: model = Sample fields = ['id', 'sample_code', 'lithology', 'remark'] class StationSerializer(serializers.ModelSerializer): strata = StrataSerializer(many=True, read_only=True) samples = SampleSerializer(many=True, read_only=True) class Meta: model = Station fields = ['id', 'name', 'longitude', 'latitude', 'elevation', 'description', 'created_at', 'strata', 'samples']嵌套序列化器的写法是让接口一次返回完整层级的关键。前端拿到一个点位的数据,strata列表和samples列表直接附带在该点位下,不需要再发第二次请求。这里read_only=True表示嵌套数据只读,点位创建时不通过这个接口批量写入分层记录,分层和样本由各自独立的接口去增删改,职责更清晰。
创建一个新点位时走StationSerializer,写入字段是name、longitude、latitude、elevation、description;外层字段里rocks_type等并不出现,前端提交时按这个字段名传 JSON 即可。理解序列化器的字段列表,就相当于理解了接口的入参和出参协议。
3. 核心模块拆解:一条野外记录从保存到显示在地图上的完整链路
3.1 后端接口:用 ModelViewSet 管理点位数据
Django REST Framework 里最省事的做法是用ModelViewSet,它把list / create / retrieve / update / partial_update / destroy六个动作一次性生成,路由注册之后就直接冒出一套完整的 REST 接口。
# geolog/views.py from rest_framework import viewsets from .models import Station from .serializers import StationSerializer from .geo import to_wgs84 class StationViewSet(viewsets.ModelViewSet): queryset = Station.objects.all() serializer_class = StationSerializer def perform_create(self, serializer): lon = serializer.validated_data['longitude'] lat = serializer.validated_data['latitude'] lon, lat = to_wgs84(lon, lat) serializer.save(longitude=lon, latitude=lat)perform_create是写入前的最后一次拦截点。重写它的目的,是给坐标做一道兜底转换:手机浏览器定位返回的坐标大多是 GCJ-02(火星坐标系),而 Leaflet 底图用的 WGS-84,两者直接落图会有几百米偏差。常见的做法是把转换函数放到geolog/geo.py,接口层统一调to_wgs84处理,保证入库的经纬度永远统一。
路由注册也很简单,在geolog/urls.py里加一行router.register('stations', StationViewSet),前端就能通过/api/stations/访问点位资源。GET 请求拿列表,POST 请求创建点位,PUT / PATCH 更新,DELETE 删除,DRF 全包了。对一个以数据录入为主的项目,这套接口模式能覆盖绝大部分需求。
3.2 GeoJSON 导出:把数据库记录变成标准空间数据
数据库里存的是经纬度字段,但 GIS 软件不认这种自定义格式。GeoJSON 是地理空间数据的通用交换格式,QGIS、ArcGIS、PostGIS 都能直接识别,所以项目里加了一个专门生成 GeoJSON 的接口。
# geolog/views.py from django.http import JsonResponse from django.views.decorators.http import require_GET @require_GET def geojson_export(request): features = [] for station in Station.objects.all(): features.append({ "type": "Feature", "geometry": { "type": "Point", "coordinates": [station.longitude, station.latitude], }, "properties": { "name": station.name, "elevation": station.elevation, "description": station.description, }, }) return JsonResponse({ "type": "FeatureCollection", "features": features, })这个视图的核心是构造 FeatureCollection 结构。每个点位是一条 Feature,geometry里装空间位置,properties里装属性字段。coordinates 的顺序固定为经度在前、纬度在后,这是 GeoJSON 规范的要求,写反了地图上会直接跑到海里去。
常见的误用是把lat, lon按习惯塞进去,结果点位在渲染时出现奇怪的偏移。我一般在写这个接口时会顺手把坐标顺序写进注释,避免过几个月自己回来看也踩同样的坑。QGIS 里加载这个接口地址,所有点会按属性字段自动显示成标注图层。这个接口是整个项目和外部 GIS 生态打通的关键。
3.3 前端渲染:Leaflet 消费 GeoJSON
前端frontend/map.js负责把接口返回的 GeoJSON 渲染成地图上的点。
// frontend/map.js const map = L.map('map').setView([29.30, 94.20], 7); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 18, }).addTo(map); fetch('/api/stations/geojson/') .then(resp => resp.json()) .then(data => { L.geoJSON(data, { pointToLayer: (feature, latlng) => L.circleMarker(latlng, { radius: 6 }), onEachFeature: (feature, layer) => { const p = feature.properties; layer.bindPopup(`<b>${p.name}</b><br>${p.description || ''}`); } }).addTo(map); });setView的第一个参数是中心点坐标,这里默认放在项目演示区域附近,实际跑起来可以根据自己的数据改。fetch 请求的是上一节写的/api/stations/geojson/接口,L.geoJSON直接识别 FeatureCollection 结构,把每个 Point 渲染成circleMarker。
onEachFeature是交互逻辑的挂载点,这里给每个点绑定了 popup 弹窗,点击标记时显示点位名称和野外描述。整个链路其实只有几步:Django 从数据库把记录读出来,序列化成 GeoJSON,JS 拿到 GeoJSON 交给 Leaflet 渲染。理解这条链路,后面加什么功能都知道往哪个环节塞代码。
4. 本地运行指南:从拉取源码到浏览器出现点位
4.1 环境准备:虚拟环境与依赖安装
项目依赖写在requirements.txt里,核心是 Django 和 djangorestframework。拿到源码后先建虚拟环境,再装依赖,这一步别急着省。
git clone <仓库地址> geolog-app cd geolog-app python3 -m venv .venv source .venv/bin/activate # Windows 环境换成 .venv\Scripts\activate pip install -r requirements.txtvenv 这一步单独拎出来说,是因为我见过太多直接把依赖装进系统 Python 导致互相污染的情况。系统 Python 环境里装过旧版 Django,新项目一启动就报版本兼容错误,这种玄学问题本质上都是环境隔离没做。依赖安装完成后,可以用python manage.py version确认一下当前 Django 版本,避免后面接口行为对不上。
4.2 数据库初始化:迁移与示例数据
迁移是把模型定义转换成数据库表结构的过程。项目下载到本地时,如果migrations目录里已经有迁移文件,直接执行 migrate 即可;没有的话要先执行makemigrations让 Django 生成迁移文件。
python manage.py makemigrations geolog python manage.py migrate python manage.py loaddata fixtures/initial_data.json python manage.py createsuperuserloaddata这一步不是必需流程,但强烈建议做。initial_data.json里预置了几个演示点位,导入后地图上立刻有数据可看,不用自己一笔一笔录。createsuperuser是创建后台管理员账号,Django Admin 在这个项目里承担数据管理入口,访问/admin/登录后可以查看和编辑点位记录,相当于一个现成的后台面板。
4.3 启动与验证:手动测一遍完整流程
接着启动开发服务器。
python manage.py runserver 8000浏览器打开http://127.0.0.1:8000/应该是地图页面,能看到实验点铺在地图上。再用命令行手动创建一条记录,验证接口链路是否通。
curl -X POST http://127.0.0.1:8000/api/stations/ \ -H "Content-Type: application/json" \ -d '{"name":"测试点A","longitude":88.10,"latitude":29.20}'curl 实际执行时如果 JSON 里的双引号被 shell 吃掉,可以在 Windows PowerShell 里改用单引号包裹 JSON。正常响应会返回 201 和新增记录的完整 JSON,包含自动生成的id和created_at。刷新前端地图页面,新点应该出现在对应坐标位置。
到这里项目就完整跑起来了:数据库有表、接口有响应、地图能渲染、后台能管理。想验证 GeoJSON 的兼容性,把http://127.0.0.1:8000/api/stations/geojson/这个地址贴到 QGIS 的图层管理器里,选择数据类型为 GeoJSON,点就能直接加载进 GIS 软件。
5. 避坑指南:地图白屏、坐标偏移和序列化时区问题
5.1 地图白屏:Leaflet 容器没有高度
现象:页面打开后右上角和左下角能看到 Leaflet 的缩放按钮,但地图区域整块空白。
原因:<div id="map">没有设置 CSS 高度。Leaflet 初始化时读取容器的高度,如果高度为 0,地图切片就渲染不出来,但控件照常显示。
解决:在frontend/style.css里给地图容器指定高度。
#map { height: 500px; width: 100%; }这是 Leaflet 项目最常见的白屏原因,遇到地图区域显示不出来,先检查容器高度,八成是这个问题。
5.2 坐标偏移:手机定位和底图坐标系不一致
现象:手机浏览器上定位记录的点位,回到电脑地图上显示时位置偏移了几百米,而且偏移方向在不同区域还不一样。
原因:手机端 Web 定位接口返回的一般是 GCJ-02 坐标,而 OpenStreetMap 底图用的是 WGS-84。两者在大多数地区有几百米偏差,直接叠加自然会对不上。
解决:写一个坐标转换函数放进geolog/geo.py,在perform_create里对所有新增点位做一次转换。代码已经在第三章展示,核心是在serializer.save()之前调用to_wgs84覆盖经纬度值。转换算法的具体公式网上有现成实现,直接拿来找个测试点验证一下误差在可接受范围即可。
5.3 时间字段多出 +08:00 后缀
现象:接口返回的created_at值是2025-06-01T10:30:00+08:00,前端new Date()解析后显示正常,但直接字符串拼接展示就变成带时区后缀,和手填的日期格式对不上。
原因:DRF 默认把 DateTimeField 序列化成 ISO 8601 格式,带时区偏移。本地 SQLite 存的是2025-06-01 10:30:00,序列化后自动补上了东八区后缀。
解决:在StationSerializer里覆盖字段格式。
created_at = serializers.DateTimeField(format='%Y-%m-%d %H:%M')加这一行之后接口返回的时间就是2025-06-01 10:30,前端不用再做任何处理。需要注意,这个格式会影响所有用到该字段的接口,如果同时存在需要 ISO 格式的场景,可以再单独加一个只返回datetime类型的序列化器。
5.4 旧 fixtures 导入时报字段不存在
现象:python manage.py loaddata fixtures/initial_data.json执行时直接报错,提示某个字段在模型里找不到。
原因:fixtures 文件是用旧版本模型导出后留在项目里的,后来模型加了字段或者改了字段名,JSON 里的字段列表和当前模型对不上。
解决:把报错提到的字段在Station模型里找一下,确认是新版本里被删掉或者改名了,手动编辑initial_data.json删掉对应键值,或者直接重新生成一份 fixtures。
python manage.py dumpdata geolog > fixtures/initial_data.jsondumpdata导出的是当前模型结构下的合法数据,重新生成的 fixtures 一定能正常导入。这个小坑提醒我们,fixtures 文件不是永久有效的,模型一变它就要跟着动。
6. 二次开发思路:给项目加产状信息和 CSV 导出
拿到项目之后最想干的事通常是加野外产状数据。地质记录里走向、倾向、倾角是必填项,现在Strata模型没有这三个字段,改造步骤很简单:在模型定义中追加三个浮点数字段,生成并执行迁移。
# 在 Strata 模型里追加 strike = models.FloatField('走向', null=True, blank=True) dip_direction = models.FloatField('倾向', null=True, blank=True) dip_angle = models.FloatField('倾角', null=True, blank=True)改完执行python manage.py makemigrations geolog && python manage.py migrate,再把字段名加进StrataSerializer.fields列表,前端strata嵌套数据里就会自动带上产状信息。
加一个 CSV 导出接口是另一种实用改造。单位领导不会打开 GeoJSON,但 CSV 用 WPS 就能看。我一般在项目里这样加:
import csv from django.http import HttpResponse def export_csv(request): resp = HttpResponse(content_type='text/csv') resp['Content-Disposition'] = 'attachment; filename="stations.csv"' writer = csv.writer(resp) writer.writerow(['name', 'longitude', 'latitude', 'elevation', 'description']) for s in Station.objects.all(): writer.writerow([s.name, s.longitude, s.latitude, s.elevation, s.description]) return respContent-Disposition头的attachment会让浏览器直接把响应下载成文件。这条改造验证起来也快,访问新接口能拿到 CSV、字段列头和数据库记录一致即可。
从做这个项目以后我养成了一个习惯,每次加字段都强制走一遍「模型 → 迁移 → 序列化器 → 前端展示」这条全链路,而不是改完模型就当完成。跟着这条线检查一遍,字段丢失、接口报错这类问题基本都能在提交之前拦下来。希望帮到你。
本文还有配套的精品资源,点击获取