今年上半年我接到一个挺典型的练手项目——城市地铁查询系统。客户其实就是个即将毕业的朋友指定要用 Python 做后端前端要是 Vue开发工具用 Pycharm后端框架在 Django 和 Flask 之间二选一。聊完之后我意识到这不只是一个“做个网站”的事而是要把数据建模、接口设计、前端交互、算法选型全走一遍的综合性项目。正好借这个机会把从环境搭建到前后端联调的完整过程梳理出来。无论你是准备做毕业设计还是想搞一个能有实战体感的 Web 入门项目这篇文章都适用。我会把 Django 和 Flask 两种实现路径都讲清楚再说说我在实际开发中踩过的坑。城市地铁查询系统的核心说白了就是给用户一个界面输入起点和终点系统返回最佳换乘方案或者输入一条线路展示所有站点。它既不复杂又能把 Web 开发的常见知识点都串起来。数据上我建议先用城市公开的静态地铁线路数据后面想加分可以再加爬虫去实时抓取。下面直接进入正题。1. 项目整体设计与技术选型1.1 核心功能需求拆解在动手写代码之前先把需求拆明白。我当时列出来的核心功能有四块城市与线路列表展示用户进入系统能看到当前有哪些地铁线路每条线路经过哪些站点。站点信息查询点击某个站点能显示它属于哪些线路、当前是否换乘站、相邻站是什么。起点到终点的路径规划输入两个站返回最少经过多少站、换乘几次、具体要坐哪条线到哪一站换乘。线路详情与时刻信息可选比如首末班车时间这两项如果拿不到真实数据可以用模拟数据不影响主流程。这四块功能本身并不高深但每一块都对应着前后端的具体实现线路列表需要后端提供数据接口路径规划考验的是图算法功底站点详情要求前端处理好路由参数。把需求拆到这一步后面写代码才会有目标感而不是拿到需求就急着敲键盘。1.2 后端框架到底选 Django 还是 Flask“Django 还是 Flask”这个问题几乎每个新手都会卡住。我的结论是想快速敲定项目结构、自带后台管理选 Django想要轻量、自己掌控每一步选 Flask。但如果你只是做课程设计或毕业设计我更推荐 Django因为它在同一点上已经把 ORM、Admin 后台、表单校验都给你配好了减少了很多重复造轮子的时间。我实际做的项目里是用 Django Django REST FrameworkDRF搭的 API。DRF 可以非常方便地把模型直接序列化成 JSON而且自带了可视化 API 调试页面联调时省了很大力气。再说 Flask它更适合做小型 API 服务只要写一个app.py就能跑起来。我也在附录里给了基于 Flask Flask-CORS 的版本方便只想看轻量实现的朋友。下面用一个表格把两者对比一下方便你根据自己的场景去选。对比维度Django DRFFlask Flask-RESTX/Flask-CORS项目初始化自带完整目录结构有 settings、urls、admin只有一个入口文件所有路由自己写ORM 模型Models 是标配迁移命令自动生成默认没有需要自己装 SQLAlchemy后台管理自带 /admin 后台免费送需要自己接 flask-admin适合场景多人协作、功能复杂的项目单文件原型、微服务、小 API学习成本上手门槛稍高但体系化入门简单后期结构要自己约束我当时是先用 Django 把主流程全部跑通再用 Flask 写了一个极简版接口做对比这也是为什么这篇博客里两套代码都会有。1.3 前端为什么选择 Vue地铁查询系统的页面交互说复杂也复杂说简单也简单。复杂在于换乘方案的渲染需要动态展示“坐几号线—在哪个站换乘—再到哪个站下车”简单在于页面数量不多基本就是首页、线路列表、线路详情、站点详情、路径规划这几个视图。Vue 解决这个问题的思路很直接用组件拆页面用路由切换视图用状态管理保存用户当前搜索的起点和终点。特别是 Vue 的单文件组件SFC模板、脚本、样式写在一个文件里对写惯了 HTML JavaScript 的人来说非常友好。相比 React 要理解 JSX 语法Vue 的模板更贴近传统前端写法上手曲线平缓得多。我在这个项目里用的是 Vue 2 Vue Router 3 Axios。如果你本来就用 Vue 3也没关系核心逻辑是一样的。关键是“前后端分离”的思维前端跑在 8080 端口后端跑在 8000 端口两边通过 HTTP 接口互相通信。这跟你之前可能做过的“用 Django 直接渲染模板”是两个思路下面我会全程沿着这种分工来写。2. 开发环境准备与初始搭建2.1 Python 与 Pycharm 环境配置这一步没什么捷径但有很多小坑。我建议先装 Python 3.9 或 3.10别一上来追最新的 3.13因为很多第三方库对最新版本的支持会有滞后。装完之后一定要在命令行里验证一下python --version pip --versionPycharm 我用的是 Community 版也就是社区版开源免费足够做 Django 和 Flask 开发。专业版确实有一些 Web 开发增强功能比如数据库工具、运行配置模板但社区版加上这些插件完全能覆盖本项目需求。打开 Pycharm 后新建项目时一定要选择虚拟环境Virtualenv 或 Conda这一步很多人会忽略结果全局环境下依赖混乱项目换台机器就跑不起来了。我习惯在 Pycharm 的 Terminal 里执行下面几条命令python -m venv venv venv\Scripts\activate # Windows source venv/bin/activate # macOS/Linux激活虚拟环境后会看到命令行前面出现(venv)前缀。之后安装的所有依赖都只会进入这个项目目录里干净又安全。这一步做完基本的 Python 环境才算是真正可用。2.2 Vue 安装及环境配置Vue 的安装环境同样容易被卡住。核心是 Node.js不是 Python。先去 Node 官网下载 LTS 版本安装完检查node -v npm -v然后全局安装 Vue CLI。我用的是vue/cli4.x/5.x 版本推荐 5.xnpm install -g vue/cli vue --version如果你发现 npm 下载特别慢大概率是网络原因。这时候不要死磕官网直接换国内镜像源比如淘宝源会快很多。命令如下实测有效npm config set registry https://registry.npmmirror.com npm install -g vue/cli创建 Vue 项目的命令是vue create metro-frontend创建过程中 CLI 会问你选择 Vue 2 还是 Vue 3、是否安装 Vue Router、是否安装状态管理工具。我这里选 Vue 2 Router等生成完再单独装 Axios。到这步前端环境就算通了。之后你可以在vue.config.js里设置开发服务器代理我后面会讲到。2.3 创建 Django 后端骨架用 Pycharm 打开后端项目目录后在虚拟环境激活的前提下安装依赖pip install django djangorestframework django-cors-headers然后创建 Django 项目和一个专门的 app。注意区分项目与 app项目是整体的配置容器app 才是业务代码所在的地方。django-admin startproject metro_backend cd metro_backend python manage.py startapp line这里metro_backend是项目名称line是我给地铁业务取的 app 名。创建完你会看到settings.py、urls.py、views.py等文件。这时候记得去settings.py里把rest_framework和corsheaders加到INSTALLED_APPS再把corsheaders.middleware.CorsMiddleware加进中间件列表否则前后端联调时会遇到跨域问题。如果走 Flask 路线流程就更轻了pip install flask flask-cors新建app.py直接定义接口路由就行。Django 和 Flask 的骨架差异确实不小但它们的核心思想是一致的路由分发表、请求处理函数、JSON 返回格式。下面我会把两套代码都写清楚。3. 核心细节解析与数据建模3.1 地铁线路与站点数据结构设计做地铁查询第一步不是写接口而是设计数据库表。没有数据结构后面所有算法都无从谈起。我在项目里用了三张核心表Line线路、Station站点、LineStation线路与站点的关联表。之所以不把站点直接塞进线路表是因为一个换乘站会属于多条线路如果直接存一个字段根本没法高效查询。Django 里的模型可以这样写from django.db import models class Line(models.Model): name models.CharField(max_length50, verbose_name线路名称) color models.CharField(max_length20, verbose_name线路颜色) class Station(models.Model): name models.CharField(max_length50, verbose_name站点名称) class LineStation(models.Model): line models.ForeignKey(Line, on_deletemodels.CASCADE, related_nameline_stations) station models.ForeignKey(Station, on_deletemodels.CASCADE, related_namestation_lines) order models.IntegerField(verbose_name在该线路上的序号) is_transfer models.BooleanField(defaultFalse, verbose_name是否换乘站) class Meta: ordering [line_id, order]order字段非常重要它决定了某条线上站点的先后顺序。比如 1 号线的order1是“苹果园”order2是“古城”这样我们才能判断两个站点在地铁线上谁前谁后才能给路径规划算法提供基础数据。如果你想用 Flask那建表逻辑也是一样只是换成 Flask-SQLAlchemy 的语法。核心数据结构搞明白了换框架只是一个语法翻译的过程。3.2 换乘方案查询的算法实现路径规划是项目的灵魂。地铁换乘本质上是一个图的最短路径问题把每个站看成图上的一个点相邻两站之间有一条长度为主的边。换乘时要额外加上“换乘惩罚系数”这样算法才不会一味地追求少站数而让你折返跑换乘线路。我这里采用的是 Dijkstra 算法的变体。每条边的权重设为“时间成本”普通乘坐一段计 2 分钟换乘一次额外加 5 分钟惩罚。什么时候加惩罚当路径从一条线跳转到另一条线的瞬间。算法核心伪代码如下初始化优先队列起点站入队dist[w] 0 记录每个站点当前所在线路 line_for_station[w] 当队列不为空时弹出当前站 u 遍历 u 的所有邻站 v 若从 u 到 v 不需要换线line_for_station[u] line_for_station[v] 新权重 dist[u] ride_time 若需要换线 新权重 dist[u] ride_time transfer_penalty 如果 v 的新权重比 dist[v] 小则更新并记录前驱节点 最后从终点通过前驱节点回溯得到完整路径由于地铁图的节点数基本都在几百个以内Dijkstra 跑一次是毫秒级的完全不用优化成 A*。如果你只是想实现“最少换乘次数”也可以用 BFS 分层搜索先找出距离起点 0 次换乘可达的所有站点再找 1 次、2 次……直到包含终点。我在接口里同时返回了“最少站数方案”和“最少换乘方案”两个结果让用户自己选。3.3 后端接口设计与返回格式接口设计遵循“约定优于配置”我用了 REST 风格GET /api/lines/返回所有线路列表GET /api/lines/{line_id}/返回某条线路及其站点顺序GET /api/stations/{station_id}/返回站点详情及所属线路GET /api/route/?from站名to站名返回换乘方案Django 的 DRF 写起来非常顺手。比如线路列表视图可以这样写from rest_framework import generics from .models import Line, LineStation from .serializers import LineSerializer class LineList(generics.ListAPIView): queryset Line.objects.all() serializer_class LineSerializer序列化器再定义一下对应字段from rest_framework import serializers class LineSerializer(serializers.ModelSerializer): class Meta: model Line fields [id, name, color]接口返回的 JSON 结构我尽量保持下面这样前端解析起来会比较轻松{ code: 0, data: { lines: [ {id: 1, name: 1号线, color: #b50c16} ] }, message: success }统一返回code、data、message三层结构是前后端对接时减少沟通成本的好习惯。后面就算接口报错前端也只需判断code是否等于 0不需要一层层找 Bug。4. 前端页面实现与接口联调4.1 Vue 项目结构与路由配置Vue 项目的src目录我一般按功能拆成views、components、router、api四块。views放页面components放可复用组件router放路由表api统一封装接口请求。路由表我这样配import Vue from vue import Router from vue-router import LineList from ../views/LineList.vue import StationDetail from ../views/StationDetail.vue import RoutePlan from ../views/RoutePlan.vue Vue.use(Router) const routes [ { path: /, redirect: /lines }, { path: /lines, component: LineList }, { path: /station/:id, component: StationDetail, props: true }, { path: /route, component: RoutePlan } ]这里station/:id是动态路由传入的参数id会在StationDetail组件里通过this.$route.params.id或者 props 获取。Vue Router 的动态路由参数在站点点详情场景里特别好用不用为每个站点创建单独的页面。4.2 页面组件与数据请求封装我在api/request.js里统一封装了 Axios 实例import axios from axios const request axios.create({ baseURL: process.env.VUE_APP_BASE_API || http://127.0.0.1:8000/api, timeout: 5000 }) export function getLines() { return request.get(/lines/) } export function getRoute(from, to) { return request.get(/route/, { params: { from, to } }) }组件里调用getLines()后把返回的数据渲染到页面中。比如线路列表template div classline-card v-forline in lines :keyline.id click$router.push(/station/ line.id) span classline-color :style{ backgroundColor: line.color }/span span classline-name{{ line.name }}/span /div /template script export default { data() { return { lines: [] } }, async created() { const res await getLines() this.lines res.data.data.lines } } /script这里有个小技巧接口返回的 JSON 是两层data第一次是 Axios 包裹的响应体第二次是我在接口层定义的业务数据字段。很多新手会在这里搞混一定要看清楚res.data.data.lines的三层结构。4.3 前后端联调与跨域问题解决前端跑在 8080后端跑在 8000浏览器会认为这是两个不同源请求时就会触发跨域问题。解决方案有两种一种是在后端开 CORS另一种是前端开发时配代理。我两种都用了。Django 端安装django-cors-headers之后在settings.py里设置CORS_ALLOWED_ORIGINS [ http://localhost:8080, ]前端 Vue 开发环境里更推荐配 proxy这样前端请求时是发到自己的 8080再由 Node 的开发服务器中转给后端 8000。在vue.config.js里加module.exports { devServer: { proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true } } } }配好这个前端请求/api/lines/实际上会转发到127.0.0.1:8000/api/lines/跨域问题就消失了。注意修改代理配置后一定要重启npm run serve否则不生效。5. 实操过程与核心环节实现5.1 Django 后端完整启动步骤下面我会按实际操作顺序把 Django 后端从启动到提供接口的完整流程走一遍。首先激活虚拟环境在metro_backend目录下执行数据迁移python manage.py makemigrations line python manage.py migrate写完了模型就通过 Django 的管理后台往数据库里塞初始数据。创建超级用户python manage.py createsuperuser然后启动开发服务器python manage.py runserver 8000打开http://127.0.0.1:8000/admin登录后手动录入线路、站点、站点顺序。数据量如果只有几条手工录没问题如果录完一个城市的地铁图建议写一个data_import.py脚本批量导入。下面是我常用的数据导入脚本片段from line.models import Line, Station, LineStation def import_line_data(line_name, stations, color#000000): line, created Line.objects.get_or_create(nameline_name, defaults{color: color}) for order, station_name in enumerate(stations, start1): station, _ Station.objects.get_or_create(namestation_name) LineStation.objects.update_or_create( lineline, stationstation, defaults{order: order} )这个脚本只管插入数据还自动给站点排了序号是用在真实项目中最稳妥的一步。5.2 路径查询接口的完整实现路径查询接口我写在一个单独的任务文件里叫metro_service.py这样能在views.py里保持简洁。核心逻辑返回两个方案from heapq import heappop, heappush def calc_route(from_station, to_station): # 省略建图过程把相邻站点关系整理成 graph # 多轮 Dijkstra 同时记录换乘 ... return { min_stops: path_a, min_transfers: path_b }views.py中对外暴露接口from rest_framework.decorators import api_view from rest_framework.response import Response from .metro_service import calc_route api_view([GET]) def get_route(request): from_name request.GET.get(from) to_name request.GET.get(to) result calc_route(from_name, to_name) return Response({code: 0, data: result, message: success})这里需要注意一个细节站点名称在请求时可能存在同义词比如“北大东门”和“北京大学东门”。最稳妥的方案是不靠前端传站名而是传站点的 ID。站名由前端通过下拉框选择传给后端的是 ID这样能彻底避免歧义。5.3 Flask 轻量版后端如何实现如果你想换成 Flask或者想借这个题目熟悉 Flask那代码量会小不少。同样一段路径查询接口Flask 实现如下from flask import Flask, request, jsonify from flask_cors import CORS app Flask(__name__) CORS(app) app.route(/api/lines, methods[GET]) def lines(): # 从数据库查询线路数据这里用 SQLAlchemy 或直接读 JSON return jsonify({code: 0, data: lines_data, message: success}) app.route(/api/route, methods[GET]) def route(): from_id request.args.get(from) to_id request.args.get(to) result calc_route_by_id(from_id, to_id) return jsonify({code: 0, data: result, message: success}) if __name__ __main__: app.run(host127.0.0.1, port8000, debugTrue)Flask 的“怎么把接口绑定到网页元素”这个问题其实不用纠结“绑定”这个词。网页元素是通过 JavaScript 的fetch或 Axios 调用后端接口后端接口把 JSON 返回到前端前端再更新 DOM。你只需要保证 Flask 定义的接口路径和前端请求的 URL 完全一致即可。5.4 Vue 前端启动与效果记录前端写完路由、组件、接口请求之后在项目根目录启动npm run serve打开浏览器访问http://localhost:8080正常情况下首页会显示线路列表。我抽查了一次路径规划输入“苹果园”到“国贸”接口返回的换乘方案是“1号线坐到军事博物馆换乘 9 号线到国贸”这个结果基于我用北京地铁真实数据做的测试基本正确。如果你用别的城市数据结果会有所不同但算法逻辑是一致的。联调时我习惯把浏览器开发者工具打开重点看 Network 面板。如果某个接口挂了能看到请求状态是 404 还是 500再点进请求详情看返回值问题定位会快很多。这一步很多人不愿意做但恰恰是排查问题最有效的手段。6. 常见问题与排查技巧实录6.1 Django 静态文件不显示的问题很多人在 Vue 里写的img src/static/xxx.png在 Django 静态文件里加载不出来。原因很简单前端开发服务器是 8080 端口它的/static路径和后端 8000 端口没有任何关系。有两种处理方式如果图片放在 Vue 项目的src/assets下直接import img from ../assets/xxx.png交给 Webpack 处理。如果图片放在 Django 的static目录那请求的 URL 必须是http://127.0.0.1:8000/static/xxx.png不能让前端直接访问后端的静态资源通常也不该这么干。我最后是把图片都放在前端静态目录里后端只管数据。前后端分离项目里“静态文件归前端数据接口归后端”是一条铁律。6.2 跨域请求失败与安装慢项目里最常遇到的报错是“Access to XMLHttpRequest ... has been blocked by CORS policy”。遇到这个先别急着改代码检查三件事后端有没有开 CORS 中间件CORS_ALLOWED_ORIGINS是不是包含了当前前端地址如果是开发环境是不是没重启后端服务。只要后端改了配置就必须重启Django 的 runserver 不会自动加载新安装的中间件命令吗其实会但中间件改了之后我遇到过不清缓存不生效的情况干脆重启最省心。前端npm install慢的问题解决办法只有一个把 npm 镜像切到国内源。命令前面已经给了。另外注意vue create的时候如果问你”Use this mirror for downloads”就可以直接选淘宝镜像省得后面再切。6.3 数据库数据乱码与字段命名问题站点名称里有中文如果数据库表创建时没指定 UTF-8 字符集查询接口可能返回乱码。Django 默认会在创建表时使用数据库的默认字符集MySQL 有时候默认是 latin1。解决方法是建库时显式指定字符集CREATE DATABASE metro_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;另外字段命名上Django 里如果模型包含order字段表示排序没问题但如果你用的数据库是 MySQL 5.7 之前的版本order是保留字可能报错。我在迁移时确实遇到过一次后来改成order_num才顺利通过。这个坑虽然不常见但碰到了会卡很久提前写出来提醒你。6.4 常见问题速查表问题现象可能原因解决思路前端请求接口报 404后端路由路径和前端请求不一致先在后端浏览器直接访问接口地址确认接口存在接口返回 500后端代码异常或数据缺失看后端控制台报错日志重点看最后几行 Traceback页面渲染空白Vue 路由匹配失败或接口未返回打开 Vue Devtools 看组件路由状态路径规划结果绕路图的邻接关系没建完整检查LineStation的order是否按顺序录入换乘方案不显示“换乘”提示前端渲染逻辑判断有误确认接口返回的数据里有没有transfer_station字段这些小问题绝大部分都是细节问题只要日志看得仔细每条都能在一小时内定位出来。结尾一些经验把整个项目跑完之后我最深的体会是这类项目最花时间的不是写代码而是数据录入和联调。你辛辛苦苦写完算法结果因为有几条线路的站点顺序录错了路径规划出来就是绕远路。第二天再审核数据比写代码还痛苦。所以做数据录入脚本时一定要能自动校验“线路首站到尾站的连续性”比如相邻站点的order差值不能大于 1否则直接报错。另外一个小技巧是开发时不要一次性把所有功能都做完再做联调。我就是先做完“线路列表”前端npm run serve能展示第一屏后再开始做“站点详情”每完成一个接口就立刻验证一个页面。这种迭代方式虽然看起来慢但能让你随时知道自己写的代码有没有问题不会到最后攒了一大堆 Bug 再集中排查。这个项目后续做扩展的话我建议加两样东西一是用户反馈功能让用户能把错误的站点信息上报二是加入夜间运营时段提醒帮用户判断能不能赶上末班车。这些都是真实地铁查询产品的核心体验点做出来后整站的专业感会明显提升。如果你也正在做类似项目欢迎按这个流程走一遍遇到问题再对照第六部分的排查表基本能省掉很多瞎折腾的时间。