简介geolog-app 是一份基于 Python 与 Django 的地质学相关 Web 应用源码面向地球科学领域开发者、Python 后端初学者以及想通过小型完整项目理解 Web 应用从模型到视图全流程的读者。压缩包共 22 个文件、约 14KB包含 14 个 py 文件覆盖 settings、urls、views、models、admin、migrations 等 Django 核心模块另有 HTML/CSS 前端页面、SQLite 数据库、requirements.txt 依赖清单、README 说明文档与 Procfile 部署配置目录结构清晰便于定位代码与资源。资源已有 162 人浏览学习虽然包体很小但项目自带数据库和测试文件可直接运行调试适合快速梳理 Django 的配置、ORM 模型映射、路由分发与视图渲染流程也能作为地质数据展示、记录或管理类工具的基础模板。结合资源描述中涉及的 NumPy、Pandas、Matplotlib、GeoPandas 等 Python 地质分析常用库读者可把该项目当作入口进一步扩展数据导入、可视化或空间处理功能用于课程设计或功能练习。1. 野外编录还在纸上geolog-app 把这块石板搬进手机做矿区地质编录的人应该都有过这样的经历在钻机棚里蹲着抄岩芯一手拿打印好的编录表格一手拿记号笔费劲写完两页纸后回了驻地还得把表格内容重新敲进电脑才能进数据库、画柱状图。这套流程既慢又容易出错纸质表格一旦被雨水打湿或者折坏当天的编录记录就算废了。geolog-app 这个方向就是针对这一整套野外采集流程做移动端替代——它把编录单元、分层区间、岩性描述、产状测量、照片和样品编号都搬进手机让地质师在机台旁直接完成数据录入回到室内一键导出对接桌面端。适合谁用一线地质编录员、矿业数据化项目组、以及想把手写编录流程数字化的工程团队。本文沿着数据到落地的链路把这个方案的骨架拆开讲清楚。2. 编录数据模型先建骨架再谈采集2.1 编录单元、分层、产状的层级关系野外地质编录的对象看着零散但归根到底是一棵层级树最顶层是工程钻孔号或探槽号往下是编录单元每 1 米或者每层一个记录再往下才是具体的岩性分层、构造现象、样品、产状和照片。这棵树如果不先定好做 App 的人很容易把界面做成「一张大表单」什么字段都堆在一起结果就是采集时看着啥都能填导出时却发现结构根本对不上桌面端数据库。我一般先做的是把对象关系固定下来。一个钻孔下有若干个编录单元每个编录单元记录一个深度区间比如 12.00 m 到 13.50 m内部可以再细分为多个分层分层是岩性描述的载体产状和样品则挂在分层或者编录单元上看项目习惯。注意一个常见误区很多人喜欢直接把「分层」当顶层对象忽略编录单元的存在。这在浅表探槽里问题不大但在深孔里非常致命——因为钻进回次有起止深度岩芯摆放有回次顺序没有编录单元这层做容器深度区间很容易算乱。建议落地的层级结构工程borehole_id编录单元interval_id——表示一个独立记录段含起止深度分层layer_id——挂在编录单元下岩性描述与深度归属产状、样品、照片——挂在分层或编录单元上用外键关联2.2 用 JSON Schema 定下 App 与桌面端的交换契约编录 App 的价值在数据能回到室内所以移动端和桌面端之间必须有一份明确的交换契约。我的做法是先把一条移动端记录的结构写成 JSON 样例再据此建桌面端解析脚本。以下是一段核心结构的示意{ schema_version: 1.0.0, borehole_id: ZK-2024-018, interval: { interval_id: 25f3a1e7-9c40-4b2e-8c61-7d4f2b1e4a00, start_depth_m: 12.0, end_depth_m: 13.5, casing_depth_m: 0.5, layers: [ { layer_id: 0a3c9f21-6c42-4c11-a5e2-0e1f4e8c7d33, layer_start_m: 12.0, layer_end_m: 13.1, lithology_code: GRN, lithology_text: 灰绿色中粒花岗岩, weathering: W2, texture: 中粒等粒结构, note: 局部见绿泥石化蚀变沿节理面发育 } ], attitudes: [ { attitude_id: c5410e82-4a6e-4d7b-b0d7-6a2e1c9f5a01, dip_direction_deg: 120, dip_angle_deg: 45, type: joint, layer_id: 0a3c9f21-6c42-4c11-a5e2-0e1f4e8c7d33 } ], samples: [ { sample_id: S-2024-033, sample_type: core, depth_m: 12.6, layer_id: 0a3c9f21-6c42-4c11-a5e2-0e1f4e8c7d33 } ], photos: [ { photo_id: photo_grn_0126_01, file_path: local://images/photo_grn_0126_01.jpg, taken_at: 2024-07-26T10:24:3308:00, depth_m: 12.4 } ] } }这段 JSON 里最关键的是所有对象都带独立 ID且引用关系明确。schema_version 字段很重要它解决的是版本漂移问题——野外 App 升级后旧数据还能让桌面端按版本走不同解析逻辑。start_depth_m 和 end_depth_m 必须保留两位小数单位统一为米后面所有代码和数据库都沿着一套单位不要中途混入厘米或英尺这是最容易埋雷的地方。lithology_code 用代码而不直接用汉字是为了方便桌面端统计和出图时映射颜色与符号。2.3 结构化字段与自由文本哪些必须拉直哪些保留长文本编录数据里有一类字段适合做成枚举另一类必须保留自由文本这个边界拿不准会让后面做统计的人非常痛苦。我的判断标准只有一个将来要不要拿这个字段做筛选、统计、分组或映射图例。如果需要就拉直成代码字段如果只是让地质师把现象记下来供参考就保留长文本。按这个标准岩性代码、风化程度、矿石类型、构造类型、颜色代码必须做成枚举或下拉选择。具体操作上我会在 App 内置一份常用代码表比如岩性用三个字母缩写GRN 花岗岩、DIO 闪长岩、QZT 石英岩、SST 砂岩风化程度用 W1—W4 标准分级。注意代码表不能写死在代码里要放进本地 SQLite 的 code_list 表方便室内端远程更新——因为野外回来发现代码值漏了或写错了隔天就要重新打包 App 是很麻烦的事。自由文本则留给岩性描述、构造现象、备注这类字段。很多人做 App 时容易把「岩性名称」又做成自由文本又保留下拉结果就是同一块岩芯人录成「花岗岩」另一个人录成「中粒花岗岩」桌面端合并时一对就崩。规章上我会坚持代码字段选值名称字段自动带出描述框只写文字不让代码进描述框。3. 移动端采集交互分层、产状、照片三大件的实现3.1 分层录入半自动推算深度与岩性快选分层是野外编录里频率最高的操作交互设计得不好会直接劝退现场的人。最常见的糟糕设计是让用户每次弹出一个大表单从头填起。正确做法是把默认值从上一个分层带过来用户只需要改差异部分。我通常把分层录入做成两段式先调深度再选岩性。深度这一块App 在新增分层时会自动取上一个分层的终止深度作为起始深度终止深度默认也是同一个值用户只需要修正末尾的数值岩性则做成一行快选按钮常见的岩性代码排在前面点一下就完成选择再点一次同按钮可以进入详细描述页。这样熟练后一层岩芯大概十几秒就能记完不熟练的也能在半分钟内搞定。以下是典型的添加分层逻辑function addLayer(currentInterval) { const lastLayer currentInterval.layers[currentInterval.layers.length - 1]; const newLayer { layer_id: uuid(), // 起始深度默认取上一个分层的终止深度 layer_start_m: lastLayer ? lastLayer.layer_end_m : currentInterval.start_depth_m, // 终止深度先等于起始深度等待用户微调 layer_end_m: lastLayer ? lastLayer.layer_end_m : currentInterval.start_depth_m, lithology_code: lastLayer ? lastLayer.lithology_code : null, weathering: lastLayer ? lastLayer.weathering : W1, texture: , note: }; // 这里只插入到内存等待用户确认后才写入本地库 return newLayer; }参数说明layer_start_m 的默认值逻辑是所有分层数据不出错的关键——如果上一个分层不存在就从编录单元的起始深度开始存在则继承终止深度。weathering 默认 W1 是行业惯例里最轻的风化级别实际项目里可以根据地区岩性特征改默认值比如某些矿区强风化段多默认 W2 反而更省事。注意这里没有直接写入本地库而是先进内存用户确认了才 commit避免误触。这个交互把这批动作的频率充分压下来了。熟手在机台旁编录时眼睛始终盯岩芯手机只需要点两三个按钮就能完成一个分层的记录这就是移动编录能替代纸质表格的核心竞争力。3.2 产状测量手输方位加倾角的兜底方案产状测量是编录 App 里一个麻烦点。理论上可以用手机罗盘和陀螺仪直接量产状但实际上机台旁有铁质钻杆和套管磁罗盘会被干扰测出来的方位角常常偏得离谱。我在多个项目里都被这个坑教育过手机放岩芯上测出的走向和实际地质锤量出来的能差 20 度以上。所以我的产状模块取舍很明确不依赖手机传感器自动测量提供手输方位角和倾角的方案蓝牙外接地质罗盘作为可选升级项。这是目前最可靠的野外方案。界面上就两个数字输入框一个填方位角0—359 度一个填倾角0—90 度加一个类型选择层理面 / 节理面 / 断层面 / 片理面。录入时我会加一个简单的校验防止常见的低级错误function validateAttitude(input) { if (input.dipDirection 0 || input.dipDirection 360) { alert(方位角应在 0 至 359 度之间); return false; } if (input.dipAngle 0 || input.dipAngle 90) { alert(倾角应在 0 至 90 度之间); return false; } // 层理面产状倾角通常不小于 5 度小于 5 度视为水平层理需要复核 if (input.type bedding input.dipAngle 5) { alert(层理面倾角小于 5 度确认是否水平层理); } return true; }这段校验的用意是产状数据最终要进数据库做赤平投影或统计如果源头录入的是「方位角 420 度」这种非法值后面清洗数据非常痛苦。倾角小于 5 度属于水平层理的边界情况提示一次让地质师复核但允许确认通过不强行拦截。这种「软提示」比「硬拦截」更符合野外实际——有些膜层理就是近水平发育。实际录入效率上手输两个数字比自动测量稳定得多。这也是我对 geolog-app 这类工具的一个基本判断在传感器精度没有好到可以在矿区强磁干扰下稳定工作时手输加校验就够用盲目追求自动测量反而浪费现场时间。3.3 照片绑定与样品编号避免「拍了一堆图不知道是哪段岩芯」照片和样品是编录数据里最容易出乱子的部分。现场的人随手拍一张岩芯照片等回到室内整理时才发现不知道这张照片对应哪段深度。所以 App 里照片不能脱离编录单元存在拍照入口必须置于编录单元上下文内。我的做法是拍照时强制三要素绑定编录单元 ID、深度允许照片拍摄时手动修正、拍照时间。照片文件名就在本地按规则生成不依赖手机自带命名# 由程序生成稳定文件名 photo_id fphoto_{layer_code}_{interval_index}_{shot_seq:02d} # 示例: photo_GRN_0126_01.jpg # 参数说明 # layer_code岩性代码取三位字母 # interval_index编录单元序号按钻孔深度递增 # shot_seq同一单元内照片序号两位补零这个命名规则的好处是即使照片从手机导出后和 App 数据库分离了室内端仍然可以通过文件名猜出照片所属的编录单元和大致深度快速回补关联。这里的关键点在于文件名里不含时间戳而含深度信息——时间文件系统里到处都是但深度信息不在文件名里就丢了。样品编号同理会取一个式样工程号加采样顺序比如 ZK2024018 的第 33 件样品写成 S-ZK2024018-033。注意编号必须在 App 里自动生成并显示在录入界面上不能靠现场人员心算。如果多台手机同时采同一个钻孔的样编号由各自手机按时间递增回到室内合并时冲突概率不大但我会做一个「编号冲突检测」发现重复就提示用户确认或重新编号。4. 让 App 在无信号矿区也能干活离线优先与草稿落盘4.1 本地 SQLite 的表设计与同步状态机矿区无信号是常态。编录 App 必须做到离线优先本地是主数据库网络只是同步通道。我的方案是用 SQLite 作为本地存储引擎表结构尽量贴近第 2 章的 JSON 结构但增加两个关键字段sync_status 和 updated_at。以下是本地库的核心表结构CREATE TABLE interval( interval_id TEXT PRIMARY KEY, borehole_id TEXT NOT NULL, start_depth_m REAL NOT NULL DEFAULT 0, end_depth_m REAL NOT NULL DEFAULT 0, casing_depth_m REAL NOT NULL DEFAULT 0, sync_status INTEGER NOT NULL DEFAULT 0, -- 0 未同步 / 1 已同步 / 2 本地修改待同步 created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE layer( layer_id TEXT PRIMARY KEY, interval_id TEXT NOT NULL REFERENCES interval(interval_id), layer_start_m REAL NOT NULL, layer_end_m REAL NOT NULL, lithology_code TEXT NOT NULL, weathering TEXT, texture TEXT, note TEXT, sync_status INTEGER NOT NULL DEFAULT 0, updated_at TEXT NOT NULL ); CREATE TABLE attitude( attitude_id TEXT PRIMARY KEY, interval_id TEXT NOT NULL REFERENCES interval(interval_id), layer_id TEXT REFERENCES layer(layer_id), dip_direction_deg INTEGER NOT NULL, dip_angle_deg INTEGER NOT NULL, type TEXT NOT NULL, sync_status INTEGER NOT NULL DEFAULT 0, updated_at TEXT NOT NULL );几个设计细节所有业务表都使用 TEXT 类型的主键不用自增 ID因为自增 ID 在多台机器上会撞车而 UUID 字符串在离线环境下可以各自独立生成。sync_status 用整型不用字符串是因为我习惯用数值做判断0、1、2 三个状态足够用——不要设计成五六个状态状态多了同步逻辑复杂度成倍上升实际维护时最怕这种「看起来严谨、实际没人能说清」的状态机。updated_at 用 ISO 8601 带时区偏移的文本格式不要用 unix 时间戳因为两个人手机时间不一样时文本格式至少还能看出时区差异。本地库落盘后即使 App 被杀掉、手机重启数据也不会丢。我一般还在本地开一个 draft 表专门放半成品草稿用户在编录界面没填完就退出的记录整体搬进 draft等下次打开时恢复。这张表只存草稿不同步。4.2 同步合并策略以编录单元 ID 为主键的幂等写入同步是离线优先 App 最容易出错的地方。我的做法是坚持一条原则任何写入都以 ID 为主键做幂等 upsert不做按时间覆盖的全量替换。也就是说同一个 layer_id不管同步几次操作结果都是一样的——有这个 ID 就更新没有就插入。def upsert_layer(conn, layer): # 参数说明 # connSQLite 连接对象 # layer要从本地推送或从对端拉取的层记录字典 cursor conn.execute( INSERT INTO layer( layer_id, interval_id, layer_start_m, layer_end_m, lithology_code, weathering, texture, note, sync_status, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?) ON CONFLICT(layer_id) DO UPDATE SET interval_id excluded.interval_id, layer_start_m excluded.layer_start_m, layer_end_m excluded.layer_end_m, lithology_code excluded.lithology_code, weather excluded.weather, texture excluded.texture, note excluded.note, sync_status 1, updated_at excluded.updated_at WHERE excluded.updated_at layer.updated_at , [ layer[layer_id], layer[interval_id], layer[layer_start_m], layer[layer_end_m], layer[lithology_code], layer[weather], layer[texture], layer[note], layer[updated_at], ], ) return cursor.rowcount这里的 syncing 逻辑有一个关键细节WHERE excluded.updated_at layer.updated_at 表示只允许更新时间更晚的一方覆盖更早的一方这是并发冲突的最后防线。但我不建议把这个逻辑放在最后一刻才判断——更好的做法是同步时就比较本地和对端记录的更新时间生成一份「变更清单」再逐条处理。否则两个用户各改一头回来同步只剩最后更新的人说了算先提交的人数据会被默默覆盖掉。光有同步还不够还要有删除标记。我通常不提供物理删除而是加一个 deleted 字段同步时把所有 deleted1 的记录一并推送对端收到后也做逻辑删除。这样即使某个记录被误删也能在数据库层面抢救回来这算是我在项目现场学到的后悔药。5. 避坑geolog-app 落地中最常见的五个错5.1 手机时间不对编录顺序全乱现象两台手机分别录同一个钻孔的两段岩芯回到室内合并数据后发现记录的时间顺序是乱的深度区间互相穿插柱状图画出来地层像是倒着长。原因现场手机离线系统时间没有 NTP 校准两台手机时间差了一两个小时。记录顺序绑的是 created_at而不是编录单元序号。解决编录单元要认真用一个「序号」字段而不是用时间戳来判断顺序。在 App 里每次新建编录单元时序号必须读取本地库的 max(interval_index) 加一跟手机系统时间完全解耦。时间戳只作为参考信息如果需要校准就在同步时以室内端服务器时间为准统一修正。5.2 照片落盘了记录却没存进去现象野外拍了一批岩芯照片回到室内发现照片在手机相册里但编录 App 里对应的记录是半截的照片和深度对不上。原因很多初版实现把拍照和记录写入做成两个独立操作——拍照只是让系统相机返回一个文件路径这个路径没有立刻和当前编录单元关联结果手滑退出或 App 被杀掉关联信息丢失。解决拍照后立刻把 file_path、interval_id、depth_m 写进本地库的 photo 表做到落盘和入库在同一个事务里。不要在内存里暂存路径等着用户确认野外用户不会记得回来确认。如果确实拍完就要走至少也要先写一条完整的 photo 记录照片本身可以稍后补拍。5.3 自动保存和手动编辑打架数据被悄悄冲掉现象用户在某层岩性描述框里手动改了一段文字过了会又改了一处回到室内发现第一次的改动丢失了但用户明明记得自己改过。原因自动保存机制触发时间不稳定——有些实现是失焦才保存有些是定时保存定时保存的那次把用户还没输完的中间状态写进了数据库下一次手动编辑又基于旧数据操作覆盖了自动保存的结果。解决把草稿和定稿分开。我们最后采用了「内存编辑 显式提交」的策略用户编辑内容时只在内存维护草稿点击「保存这层」才写入业务表。自动保存只是把草稿写进 draft 表同步不包含 draft 表。这样即使自动保存先触发了也永远不会污染业务数据因为用户手动提交会重新生成一条完整的当前状态。5.4 岩性代码一人一个写法室内合并时让人抓狂现象桌面端统计时发现「花岗岩」有三种表达GRN、Grn、花-岗-岩完全没法做分组。原因代码字段做了自由输入没有限制为枚举值。有些野外人员习惯自己写缩写或者从旧表格导入时带了不规则字段。解决在 UI 层只允许选择预设代码值不允许自定义输入。如果现场确实遇到代码表里没有的岩性做「申请新代码」流程——先在本地临时记录原始文本并标记为 pending室内管理员审核后在代码表里加新值下次同步时再映射过去。这比让现场人员自由发挥好得多。5.5 深度单位不统一柱状图整体错位现象一个钻孔的编录数据画柱状图时图标深度的井深和实际岩芯摆放完全对不上有的段差出四五米。原因编录员在室内端导出的数据源来自不同设备一部分沿用了老的表格文件深度单位是英尺直接导入到以米为单位的数据库里数值看起来是 12.2其实是 12.2 英尺而不是 12.2 米。解决在导入入口做深度单位显式声明不允许「假设默认单位」。App 和桌面端都要在导入界面让用户选择单位然后换算成统一的标准米值进入数据库。加一个校验规则同一钻孔的编录单元起始深度必须单调递增且相邻单元首尾相接如果不满足就终止导入并指出错位位置。这种「硬校验」能尽早发现单位混杂问题不用等到画柱状图才暴露。6. 进阶把编录数据直接接回室内画第一张柱状图编录 App 的最终价值是让回到室内的人不再从纸质记录开始重新录入。我的做法是把 App 的导出文件直接作为柱状图绘制的输入中间不做人工转抄。室内端拿到 JSON 导出文件后解析出分层深度和岩性代码映射到柱状图符号库再按深度绘制柱体段。绘制时我会做一件事把岩性代码映射成统一的颜色和纹理标准这个映射表要和野外 App 的代码表保持一致。比如 GRN 用粉红色、DIO 用灰绿色、QZT 用白色加虚线纹理、SST 用黄色网点。映射表放在一个独立的 JSON 文件里室内端和移动端共用改一处两端同步更新。这样野外选什么代码室内就出什么颜色不用再人工统一。第一张柱状图画出来后建议专门核对三个位置钻孔顶部的表层覆盖段、岩性频繁互层的区段、以及标注了构造现象的分层。这三个地方是编录数据错误最密集的位置核对通过后再批量出全孔的柱状图才不会把错误扩散到整套图件。这是我踩过几次坑之后的习惯——先验证一张再铺开跑量不是偷懒是替团队省时间。另外一个小技巧把导出的 JSON 自动生成一份「编录核对清单」内容包括编录单元数量、分层总数、最大深度、产状条数、照片数量和纸质版的老编录表做一个总数对比。两边数字一致基本可以确认数据迁移完整不一致就能立即锁定是哪个编录单元丢段了。这套核对流程现在成了我们项目的标准动作。我自己养成的习惯是所有编录 App 的开发第一版就要做数据校验和状态机设计它们不是后期的锦上添花而是前期要不要返工的分界线。别人的案例里常听到「先跑通流程再补状态」的说法但编录数据是野外几个月的持续产出一旦中途要修补同步逻辑现场的人每天在等代价远大于前期多花的一周设计时间。做 geolog-app 这个方向最划算的投资就是先把这个骨架搞扎实希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?