ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

基于Django的公园定位系统开发实战:从数据建模到路径规划

基于Django的公园定位系统开发实战:从数据建模到路径规划 接到这个基于Django的公园定位系统项目时我先去了一趟附近的公园待了一下午。真不是偷懒是为了亲眼看看游客在公园里到底怎么找路——拿着手机地图在岔路口反复打量方向走半天发现绕了远路推着婴儿车到处找无障碍通道。看完之后回来动手设计这套公园定位系统我心里就踏实了这个项目要做的不是炫技而是真把找路这件事拆开、理顺、落到实处。如果你正准备做类似的毕设课题或者想用Django地图API来练手一个完整Web项目这篇分享可以当成一份开发笔记来看。它不只会讲技术选型和代码怎么写更会讲清楚我为什么要这样设计数据表、为什么路径规划要用最短路算法、远程调试该怎么配才能少走弯路。整个项目从零开始到交付演示所有关键流程都在下面。1. 先把业务想明白公园定位系统到底要定位什么1.1 游客在公园里的真实困境公园和商场有个本质区别——商场有清晰的楼层索引和扶梯动线而公园面积动辄几十公顷路网复杂岔路口多。游客走进公园通常只有三个目标找到想去的景点、找到卫生间或服务设施、尽量少走冤枉路。这三个目标落到系统上核心就是两件事一是在地图上告诉游客你在哪、目标在哪二是给出一条相对合理的路线。其实很多同类项目容易做成地图展示系统就是把景点标出来、点开看简介这就结束了。但我做这个公园定位系统时坚持把路径规划作为核心模块因为这才是定位二字和普通地图展示拉开差距的地方。1.2 功能边界游客端做减法管理端做加法做毕设项目最忌讳功能堆砌。我把系统拆成两个视角游客端尽量简单管理端尽量灵活。游客端公园景点地图浏览、景点详情查看、定位与路径规划、路线文字指引。管理端景点信息的增删改查、路线边数据的维护、公告发布、基础统计。这套划分的逻辑很明确游客端是前台追求的是打开就能用、不迷路管理端是后台追求的是公园运营者能自己维护数据不需要改代码。对于答辩演示来说这种分工也容易讲清楚前端怎么调用、后端怎么支撑、Karawane数据从哪来、改数据后页面怎么跟着变。1.3 为什么选Django而不是其他框架技术选型得考虑毕设场景开发周期有限、需要快速迭代、必须稳定不出幺蛾子。Django的优势恰好集中在这几点——自带Admin后台能省下大量CRUD页面开发时间ORM能让数据操作直观且安全模板系统和DRFDjango REST Framework各成一个方案进可做API后端、退可做全栈页面。再加上Python生态里的数据处理和算法库都很成熟做路径规划算法的时候不用纠结语言层面的问题。2. 环境准备与项目骨架跑通之前的那些坑我先替你踩了2.1 开发环境与依赖安装建议直接用Python 3.10以上版本配Django 4.2 LTS这个组合稳定且资料最多。先用虚拟环境隔离依赖避免和系统环境互相污染。python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate pip install django4.2.* pip install django-rest-framework # 如果走API路线 pip install mysqlclient # 如果数据库用MySQLWindows装不上就换pymysql如果你是在Windows上开发mysqlclient是个传统老大难问题。不想折腾的话开发阶段用SQLite部署上线再切MySQLDjango的ORM在这两者之间迁移几乎无感。我给这个公园定位系统做的时候就是这么干的本地零配置跑得飞快上服务器换MySQL之后只需要改settings.py里的数据库连接。2.2 创建工程和应用用命令先生成工程骨架再创建一个专门放核心业务的app。django-admin startproject park_project . python manage.py startapp parks python manage.py startapp users这里有个经验startproject后面加了一个点意思是把工程文件放在当前目录而不是再包一层目录结构看起来更扁平、更顺手。app拆成parks和users两个前者管景点和路线后者管用户和后台权限职责清晰后面扩展也好找文件。2.3 settings.py里最容易被忽略的几个配置初学Django最容易栽在配置上。这个项目我一开始也吃过亏重点提醒这几处# settings.py INSTALLED_APPS [ # 默认的app... parks, users, ] LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai USE_TZ True STATIC_URL /static/ STATICFILES_DIRS [ BASE_DIR / static, ] MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media LOGIN_URL /users/login/LANGUAGE_CODE不改成zh-hansDjango后台显示的英文会让你不耐烦STATICFILES_DIRS不配置后面Leaflet的js/css文件加载全是404MEDIA_ROOT不配置景点图片上传了也不知道落在哪个目录。这些都是看一眼文档就懂、但实际动手必踩的坑。2.4 数据库初始化与第一个页面python manage.py makemigrations python manage.py migrate python manage.py createsuperuser python manage.py runserver跑完这几条命令能打开http://127.0.0.1:8000/admin看到后台登录页项目骨架就算通了。我的习惯是每搭好一个app、每加完一个模型就马上makemigrations migrate绝不让迁移文件积压一堆再清理。理由很简单迁移文件是增量记录一旦和实际模型脱节排查起来极其痛苦。3. 数据模型设计景点、路网、反馈的数据关系3.1 景点表一切都围绕经纬度展开公园定位系统的根基就是景点坐标。没有精确的经纬度后面所有地图标注和路径规划都是空中楼阁。Django模型里存经纬度我用DecimalField而不是FloatField因为float在打印和序列化时容易出一堆小数尾巴而decimal可以精确控制到6位小数精度大约0.1米完全够用。# parks/models.py from django.db import models class Spot(models.Model): name models.CharField(景点名称, max_length100) description models.TextField(景点介绍, blankTrue) category models.CharField(景点分类, max_length50, choices[ (scenic, 自然景观), (facility, 公共服务), (sport, 运动场地), (culture, 文化场馆), ]) longitude models.DecimalField(经度, max_digits9, decimal_places6) latitude models.DecimalField(纬度, max_digits9, decimal_places6) image models.ImageField(图片, upload_tospots/, blankTrue, nullTrue) open_time models.CharField(开放时间, max_length100, blankTrue) is_active models.BooleanField(是否开放, defaultTrue) created_at models.DateTimeField(auto_now_addTrue) def __str__(self): return self.name这里有个设计细节把category设成公共服务、运动场地等分类是因为游客找卫生间停车场这类设施的需求非常高频。导航时按分类筛选比在一长串景点列表里翻找快得多。这也是我前面说先把需求想清楚在数据层面的一种体现。3.2 路网边表路径规划的数据基础路径规划不能靠在地图上画直线公园的路是弯曲的、有岔路的A点到B点必须沿实际道路走。所以我把园区路网抽象成一个图每个交叉点或景点是结点相邻两个结点之间的路段是边。这张边表就是整个路径规划算法的地图。class PathEdge(models.Model): start_spot models.ForeignKey( Spot, on_deletemodels.CASCADE, related_namestart_edges, verbose_name起点 ) end_spot models.ForeignKey( Spot, on_deletemodels.CASCADE, related_nameend_edges, verbose_name终点 ) distance models.FloatField(距离米, default100) duration models.FloatField(步行时间秒, default120) is_bidirectional models.BooleanField(双向通行, defaultTrue)括号里注释了一个关键字段is_bidirectional。为什么要这个字段因为公园里有些路段是单行步道或特殊路线不能默认所有边都双向可达。我实际给景区做数据时靠这个字段就规避了好几次算法导出一条不可逆走的路的问题。3.3 用户反馈表与公告表让系统有活的感觉毕设答辩时评委经常问一句这个系统除了展示还有没有交互所以我还加了两张表Feedback游客提交的评价、建议、设施报修。Notice后台发布的公告比如东门临时封闭某某景点维护中。公告表看起来不起眼但演示价值很高——你在Django后台改一条公告前台页面马上出现一条横幅提示。这个数据驱动展示的效果比讲一百句系统可维护性都直观。4. 地图呈现与路径规划最核心模块的完整实现4.1 地图API选型为什么我选了Leaflet做定位系统绕不开地图API。市面主流方案有三类百度地图、高德地图、LeafletOpenStreetMap。方案优点缺点百度地图国内数据丰富中文POI全需要申请AK并发配额有限UI偏重高德地图JS API好用步行路线接口成熟需要Key且商业使用有约束Leaflet开源免费、轻量、完全可控国内底图数据依赖OSM/高德瓦片需自己配样式我给这个项目选的是Leaflet。原因很实在Django后台和前端都是自己的地图基础能力不需要花哨Leaflet用官方CDN引入即可不需要申请任何第三方密钥底图可以自己配置中文瓦片源。最关键的——毕设场景下你不用把时间耗在第三方平台审核和配额调优上。4.2 景点地图标注的完整实现前端模板里引入Leaflet然后把景点列表循环渲染成标记。核心逻辑是先把Django后端传来的景点JSON数据存进一个数组再遍历生成marker和弹窗。// parks/templates/park_map.html 中的核心片段 let map L.map(map).setView([30.57, 114.30], 15); L.tileLayer(https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png, { attribution: © OpenStreetMap }).addTo(map); let spots JSON.parse(document.getElementById(spots-data).textContent); spots.forEach(function (spot) { let marker L.marker([spot.latitude, spot.longitude]) .addTo(map) .bindPopup( h3 spot.name /h3 p spot.description /p a href/park/detail/ spot.id /查看详情/a ); });注意这里JSON.parse取的是隐藏在script typeapplication/json idspots-data标签里的内容而不是直接把Python列表塞进JS变量。这样处理不会出现引号转义问题也是我在多个项目里碰撞出来的经验——Django模板变量直接进JS一旦字段里有引号或换行页面直接就崩了。4.3 最短路径算法Dijkstra在公园里的实践公园路径规划本质是给定起点和目标点在带权图中找最短路。我采用Dijkstra算法。带权图的权用的是每段路的步行时长秒而不是物理距离。为什么用时长因为在公园里慢坡、台阶、宽路和窄路的体验差异很大距离短不代表走得快。用步行动态调整权的思路规划结果更符合实际感受。核心实现直接用Python写一个函数放在parks/services.py里# parks/services.py import heapq from .models import Spot, PathEdge def dijkstra(start_id, end_id): # 先从数据库取出所有边构建邻接表 edges PathEdge.objects.select_related(start_spot, end_spot).all() graph {} for edge in edges: graph.setdefault(edge.start_spot_id, []).append( (edge.end_spot_id, edge.duration) ) if edge.is_bidirectional: graph.setdefault(edge.end_spot_id, []).append( (edge.start_spot_id, edge.duration) ) # 标准的Dijkstra优先队列不断取最小边权结点 distances {start_id: 0} prev {} pq [(0, start_id)] while pq: current_dist, current_id heapq.heappop(pq) if current_id end_id: break if current_dist distances.get(current_id, float(inf)): continue for neighbor_id, weight in graph.get(current_id, []): new_dist current_dist weight if new_dist distances.get(neighbor_id, float(inf)): distances[neighbor_id] new_dist prev[neighbor_id] current_id heapq.heappush(pq, (new_dist, neighbor_id)) # 回溯还原路径 if end_id not in prev: return None, float(inf) path [] node end_id while node ! start_id: path.append(node) node prev[node] path.append(start_id) path.reverse() return path, distances.get(end_id, float(inf))这里要特别说明两点。第一select_related在取外键关联时很关键。PathEdge里有两个外键指向Spot如果不select_related每读一条边就多两次数据库查询假设路网有200条边那就要200次以上的额外查询。加上之后一次连表查询就把数据全带出来了页面响应快一个数量级。第二判断if current_dist distances.get(current_id, float(inf))是堆优化Dijkstra的经典剪枝防止重复处理旧记录。这个细节我在给同学讲代码时发现很多人会漏掉漏掉之后的后果是算法还能跑但效率明显下降。4.4 路径可视化和文字指引路径规划的结果是一串景点ID列表。前端拿到这个列表之后在地图上把所有路径点连成一条polyline用户一眼就能看出该怎么走。// 路径可视化 let pathPoint routes.map(id { let spot spots.find(s s.id id); return [spot.latitude, spot.longitude]; }); let routeLine L.polyline(pathPoint, { color: #ff5722, weight: 5, dashArray: 8, 6 }).addTo(map); map.fitBounds(routeLine.getBounds());文字指引也不能省——很多游客在地图上看半天还是不知道朝哪个方向走。我额外生成了一份按路段拆分的指引信息从景点A向东走200米到景点B再向东南走150米到景点C。这个向东向东南的方位词通过计算相邻两个坐标点的方位角得出纯Python标准库math.atan2就能实现不需要引入额外依赖。5. Django后端的查询、API与权限开发中容易踩的细节5.1 ORM查询的常用姿势与对象删除的坑做后台管理功能时景点和路径的查询需求五花八门。最常用的几类我列一下# 查询所有开放景点并按创建时间倒序 spots Spot.objects.filter(is_activeTrue).order_by(-created_at) # 按分类和名称模糊查询的复合查询 from django.db.models import Q spots Spot.objects.filter( Q(categoryfacility) Q(name__icontains卫生间) ) # 统计各分类景点数量 from django.db.models import Count Spot.objects.values(category).annotate(countCount(id))说到删除对象这是Django初学者高频翻车区。Spot.objects.get(pk1).delete()这种写法如果景点被其他表外键引用默认会触发级联删除——游客反馈没了、路径边也没了。我踩过这个坑之后给删除操作加了一个二次确认页面并且对外键关系超过一层的对象会先展示关联数据再决定是否删除。还有一个经验如果某些删除只是想下架而不是物理消失千万不要用delete用一个is_active False软删除。这个设计在公园场景里特别重要比如某个景点只是临时封闭施工你把它删了游客历史游览记录里的关联信息就全乱了。软删除之后前台自动不再显示但数据保留可追溯后台也能一键恢复。业界的说法叫逻辑删除在管理系统里几乎是最稳妥的默认方案。5.2 接口设计用简单视图还是DRF公园定位系统的数据交互有两类一类是后台管理页面直接在模板里渲染用Django自带视图就够另一类是游客端页面和地图交互我统一返回JSON方便前端JavaScript消费数据。如果只是在视图函数里直接返回JSON可以用JsonResponse但数据量起来之后手写序列化太累了。我引入了Django REST Framework专门写了一个轻量接口层。# parks/api.py from rest_framework import serializers, viewsets from .models import Spot, PathEdge class SpotSerializer(serializers.ModelSerializer): class Meta: model Spot fields [id, name, category, longitude, latitude, description, open_time] class SpotViewSet(viewsets.ReadOnlyModelViewSet): queryset Spot.objects.filter(is_activeTrue) serializer_class SpotSerializerReadOnlyModelViewSet是故意的——游客端的查询接口只需要只读权限不需要让外部随意改数据。这个设计省了我好多权限漏洞的麻烦。URL路由用DRF的DefaultRouter自动注册接口地址清晰前端也好维护。5.3 后台权限与Admin定制Django自带的Admin后台就能满足公园数据管理人员90%的需求。我只做了两件事注册模型和在Admin里配置列表字段、搜索和筛选。# parks/admin.py from django.contrib import admin from .models import Spot, PathEdge admin.register(Spot) class SpotAdmin(admin.ModelAdmin): list_display (id, name, category, longitude, latitude, is_active) list_filter (category, is_active) search_fields (name,)由于是毕设场景我基于Django自带的User做简单登录权限用户分成is_staff和超级管理员。普通运营人员只进Admin管景点数据系统管理员才有用户管理权限。不需要引入复杂的django-guardian之类对象级权限框架够用且好讲。6. 远程调试的完整链路从本机到服务器不迷路6.1 为什么必须提前搞定远程调试毕设项目从开发到最终演示基本会经历本机开发 - 服务器部署 - 远程改问题这三个阶段。这个过程中最痛苦的不是代码逻辑本身而是本地好好的服务器上就是不行这种环境差异问题。我先花半天把远程调试配好后面所有问题排查效率直接翻倍。6.2 VSCode Remote-SSH配置实战远程调试我优先推荐VSCode的Remote-SSH。买一台轻量云服务器最低配即可系统用Ubuntu然后在VSCode里装Remote - SSH扩展配置好SSH免密连接。# 本地生成密钥并复制到服务器 ssh-keygen -t ed25519 ssh-copy-id useryour_server_ip然后在VSCode里操作CtrlShiftP输入Remote-SSH: Connect to Host。选择或填写服务器IP。连接成功后直接把本地项目文件夹打开到服务器。在服务器上创建虚拟环境、安装依赖、迁移数据库。设置断点运行python manage.py runserver 0.0.0.0:8000。跑起来之后VSCode的调试面板可以正常打断点、看变量、逐过程调试体验和本地开发几乎一模一样。我第一次在服务器上打断点调试通的时候有种原来还能这么玩的感觉——之前都是靠print打日志猜问题效率完全不是一个级别。6.3 远程调试常见的三个坑端口没开云服务器安全组要放行8000端口否则浏览器根本访问不到。我在腾讯云和阿里云上都踩过安全组规则改完要等一两分钟才生效。虚拟环境没激活登录服务器后直接跑manage.py会提示找不到Django因为装了虚拟环境但没source venv/bin/activate。这个看起来弱智但太容易发生了。静态文件404runserver在DEBUG模式下能自动服务静态文件但如果你是按生产模式配置的静态文件路径不对会全部404。调试阶段最简单粗暴的做法是DEBUG True优先保证页面渲染部署时再切回False并收集静态文件。6.4 部署生产的最终配置演示阶段我用的是NginxGunicorn的组合。这里只给最小可用的配置能跑通演示就够了。pip install gunicorn gunicorn park_project.wsgi:application --bind 0.0.0.0:8000然后Nginx做反向代理顺便接管静态文件server { listen 80; server_name your_domain_or_ip; location /static/ { alias /path/to/project/static/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这一步有一个非常容易踩的坑settings.py里的DEBUG False之后Django就不会帮你处理静态文件了必须先执行python manage.py collectstatic把散落在各app里的静态资源全部收集到STATIC_ROOT目录Nginx才能找到。我第一部署时忘记执行结果页面html是出来了但css和js全挂了那叫一个难看。7. 数据填充与测试如何让项目看起来像真的一样7.1 造一批真实感强的测试数据毕设演示最怕的是页面上空空如也所以数据填充要趁早。我的做法是根据真实公园平面图手工整理一批景点和路径数据。景点数据尽量有实际坐标。比如某公园东门是(30.5900, 114.3100)湖心亭是(30.5970, 114.3200)。不需要精确定位到厘米游客能分清相对位置就行但必须成体系不能东一个西一个。路径数据我维护一份Excel表每一行是一条边起点、终点、距离、预计步行时间。填数据的时候顺便做一次一致性检查——图必须连通不能出现某个景点无法从入口到达的情况。# script/load_data.py批量导入脚本 import json from parks.models import Spot with open(spots.json, r, encodingutf-8) as f: data json.load(f) for item in data: Spot.objects.create( nameitem[name], categoryitem[category], longitudeitem[lng], latitudeitem[lat], descriptionitem.get(description, ) )7.2 功能测试清单演示前必须过一遍确定功能都写完了我整理了一份演示前验收清单建议每一个做这类项目的同学也照着列一份检查项执行方式预期结果地图加载打开首页瓦片正常显示无404报错景点标注查看景点标记名称、分类、弹窗详情正确路径规划选A到B地图出现高亮路线文字指引合理后台管理新增景点前台自动出现新标记软删除下架某景点前台不再显示后台保留数据权限控制非登录用户访问Admin跳转登录页7.3 性能与异常情况的简单处理地图页面加载的数据量如果很大比如景点几百个、路径上千条前端渲染会有压力。我提前做了两个优化第一地图页的景点数据接口只返回必需字段description这种大文本不塞进列表接口等用户点详情时再查。小程序、网页开发里这个做法叫列表轻、详情重深刻理解后再做就顺了。第二对路径规划类接口加缓存。Django提供cache_page装饰器给热门景点之间路径接口加上几分钟缓存服务器压力立刻降下来。from django.views.decorators.cache import cache_page cache_page(60 * 5) def route_detail_view(request, start_id, end_id): # 路径规划逻辑 pass毕设答辩时能说出我做了缓存优化和一个我做了一个路径规划接口是两种印象完全不同的表述。因为评委关心的是你有没有做过大数据量下的思考而这个缓存就是实证。8. 项目交付与后续扩展毕设之外的真正资产8.1 源码、文档和讲解演示怎么组织这套项目的交付物分三层源码、文档、演示视频。源码不用多说重点在文档。我写完代码之后给自己定了规矩所有核心算法都写了注释和设计说明尤其是Dijkstra那段把为什么用堆、为什么选时长当权重都写了清楚。这不是给别人看的是给未来恢复记忆的自己看的——毕设做完三个月后你再看代码如果没有注释基本等于看陌生人的代码。演示视频不用长五分钟以内按照启动项目 - 地图浏览 - 路径规划 - 后台管理的顺序来录能让评委在完全不了解项目的情况下看懂全部亮点。8.2 可以继续扩展的方向项目做完之后我认真考虑过后续扩展路径分享三个我自己觉得最自然的方向。移动端适配现在的地图页面是PC为主毕设项目有余力的同学可以直接套Bootstrap或Tailwind做响应式或者干脆做成微信小程序版本本质上是复用同一套Django API。实时位置共享公园里的大型活动需要实时看到人流分布可以引入WebSocket或者轮询接口让游客位置上报并在地图上热力展示。Django Channels是现成的方案。多园区支持现在所有数据都围绕一个公园设计如果想做成平台化项目可以加一个Park表把景点和路径归到不同公园下一个系统管理多个公园数据。这些扩展方向我在开发时都提前留好了设计余量景点表里没有写死某个公园的字段路径边表也设计了通用图结构。将来任何方向要落地都不需要推翻现有模型。8.3 最后几点实际体会做这个公园定位系统最有价值的不是代码量而是把一个看起来简单但细节极多的问题真正拆解并落地的过程。你写景点表时考虑软删除写路径规划时考虑图的连通性配远程调试时考虑环境差异——这些才是项目做完之后留给你的经验。如果只能给一条建议我会说先花足够时间把业务和数据结构理顺再急着写代码。这个系统最重的一笔是路网边的设计而不是地图API的调用。数据模型稳了后面的所有功能实现都是一马平川数据模型乱了哪怕页面再漂亮也经不起评委几个问题追问。希望这份笔记对你的毕设或者你的公园定位项目有实实在在的帮助。
返回列表