news 2026/9/22 8:54:13

3天手写实现公交车app,告别看教程不会写的尴尬

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
3天手写实现公交车app,告别看教程不会写的尴尬

3天手写实现公交车app,告别看教程不会写的尴尬

是不是也这样?B站收藏了99+个Python项目,CSDN存了上百篇架构设计,结果真要动手写个公交查询系统,脑子一片空白。卡在“需求拆解”这一步,连数据库表都建不起来。

今天不灌鸡汤,直接带你从零手写实现一个最小可用的公交车app后端。不是那种大而全的演示代码,而是能跑通、能扩展、面试能讲清楚的实战逻辑。跟着敲一遍,你会发现“写项目”没那么玄乎。

项目目标与边界界定

很多新人一上来就想做“高德地图”,结果第一周就崩了。我们得先砍掉90%的需求。

这个手写实现的公交车app,核心功能只有三个:

  1. 查询某条线路的所有站点及顺序。
  2. 查询某两个站点之间的换乘方案。
  3. 获取实时到站预测(模拟数据)。

不做什么同样重要:不做GPS实时定位、不做用户登录鉴权、不做支付、不做地图渲染。这些是前端和第三方API的事,后端只负责数据和逻辑。

为什么这么定?因为初学者最大的坑就是“过度设计”。你在CSDN上搜“公交系统设计”,看到的往往是百万行代码的工业级方案,对你毫无参考价值。我们要的是“最小闭环”,先把数据流转跑通。

目录结构:像搭积木一样组织代码

别把代码全塞在main.py里。哪怕只有500行,也要分层。这是工程化的第一步,也是面试官看重的“代码洁癖”。

bus_app/
├── main.py          # 入口,启动FastAPI服务
├── models/          # 数据模型,对应数据库表
│   ├── __init__.py
│   └── bus.py       # 线路、站点、班次定义
├── services/        # 业务逻辑层,核心算法在这里
│   ├── __init__.py
│   └── route.py     # 换乘路径计算逻辑
├── database/        # 数据库连接与初始化
│   ├── __init__.py
│   ├── db.py        # SQLAlchemy连接池
│   └── seed.py      # 初始测试数据插入
└── requirements.txt # 依赖管理

关键点services层不直接操作数据库,它调用modelsdatabase提供的方法。这样以后你想把MySQL换成PostgreSQL,只改database层,业务逻辑一行不动。这就是解耦,面试时能讲出这一层,比背八股文有用得多。

核心代码实现:逐行拆解

1. 数据模型:别乱写字段名

很多人用dict存数据,看似灵活,实则致命。结构化数据必须用Pydantic或SQLAlchemy ORM。

# models/bus.py
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from database.db import Baseclass Station(Base):__tablename__ = 'stations'id = Column(Integer, primary_key=True)name = Column(String(50), unique=True, nullable=False)latitude = Column(Float)  # 经纬度用于后续距离计算longitude = Column(Float)class Route(Base):__tablename__ = 'routes'id = Column(Integer, primary_key=True)name = Column(String(20), unique=True, nullable=False)  # 如 "K1路"description = Column(String(200))class RouteStop(Base):"""多对多关系:一条线路有多个站点,一个站点属于多条线路"""__tablename__ = 'route_stops'id = Column(Integer, primary_key=True)route_id = Column(Integer, ForeignKey('routes.id'))station_id = Column(Integer, ForeignKey('stations.id'))stop_order = Column(Integer, nullable=False)  # 关键:站点顺序!route = relationship("Route")station = relationship("Station")

注意stop_order字段。很多新手会漏掉这个,导致无法判断“下一站”。公交是有序的,不是无序集合。

2. 换乘算法:别用暴力递归

求两点间最短换乘,本质是图的最短路径问题。节点是站点,边是“同一条线路内相邻站点”或“同站换乘”。

# services/route.py
from collections import deque
from models.bus import RouteStop, Station, Route
from database.db import Sessiondef find_min_transfers(start_name: str, end_name: str, db: Session) -> list:"""使用BFS寻找最少换乘次数路径返回: [站点1, 线路1, 站点2, 线路2, ..., 终点]"""# 1. 构建邻接表:站点 -> [(下一站, 线路名), (换乘线路1), (换乘线路2)...]graph = {}stops = db.query(RouteStop).all()# 初始化所有站点stations = db.query(Station).all()for s in stations:graph[s.name] = []# 构建同线路相邻关系 + 同站换乘关系for stop in stops:# 同线路相邻:当前站 -> 下一站next_stop = db.query(RouteStop).filter_by(route_id=stop.route_id, stop_order=stop.stop_order + 1).first()if next_stop:graph[stop.station.name].append((next_stop.station.name, stop.route.name))# 同站换乘:当前站的其他线路other_routes = db.query(RouteStop).filter_by(station_id=stop.station_id).filter(RouteStop.route_id != stop.route_id).all()for other in other_routes:graph[stop.station.name].append((stop.station.name, other.route.name))# 2. BFS搜索queue = deque()queue.append((start_name, [], start_name))  # (当前站, 已走路径, 起始站)visited = {start_name}while queue:current, path, origin = queue.popleft()if current == end_name:return path + [current]for next_station, route_name in graph[current]:if next_station not in visited:visited.add(next_station)# 记录路径:站点 + 经过的线路new_path = path + [route_name, next_station]queue.append((next_station, new_path, origin))return []  # 无路径

逐行解读

  • graph 不是简单的站点连站点,而是包含了“通过哪条线路到达”。
  • BFS保证第一次找到终点时,就是换乘次数最少的路径(假设每站时间成本相同)。
  • visited 防止死循环,这是图论算法的基本功。

3. 实时到站预测:模拟而非真实

真实公交到站需要GPS数据流,这里我们用“固定间隔+随机误差”模拟。

# services/route.py 补充
import randomdef predict_arrival(station_name: str, db: Session) -> dict:"""模拟到站预测:基于历史平均间隔假设每条线路每10分钟一班,误差±2分钟"""# 实际项目中,这里应查询最近一辆车的GPS位置# 模拟:随机生成一个1-15分钟的等待时间wait_time = random.randint(1, 15)routes = db.query(Route).filter(Route.id.in_(db.query(RouteStop.route_id).filter_by(station_id=db.query(Station.id).filter_by(name=station_name).first()))).all()predictions = []for r in routes:predictions.append({"route": r.name,"eta_minutes": wait_time + random.randint(-2, 2),"status": "running"})return predictions

避坑:不要在这里写复杂的机器学习模型预测到站。那是算法工程师的事。后端先保证接口稳定返回结构化的JSON,前端再决定怎么展示。

运行与测试:别等上线才修Bug

1. 初始化测试数据

# database/seed.py
from database.db import SessionLocal
from models.bus import Station, Route, RouteStopdef seed_data():db = SessionLocal()# 清空旧数据db.query(RouteStop).delete()db.query(Route).delete()db.query(Station).delete()# 插入站点s1 = Station(name="人民广场", latitude=31.23, longitude=121.47)s2 = Station(name="南京路", latitude=31.24, longitude=121.48)s3 = Station(name="陆家嘴", latitude=31.25, longitude=121.49)db.add_all([s1, s2, s3])db.commit()# 插入线路r1 = Route(name="K1路", description="市中心环线")r2 = Route(name="K2路", description="东西向干线")db.add_all([r1, r2])db.commit()# 插入站点顺序# K1: 人民广场 -> 南京路 -> 陆家嘴# K2: 人民广场 -> 陆家嘴 (假设K2也经过,形成换乘)db.add(RouteStop(route_id=r1.id, station_id=s1.id, stop_order=1))db.add(RouteStop(route_id=r1.id, station_id=s2.id, stop_order=2))db.add(RouteStop(route_id=r1.id, station_id=s3.id, stop_order=3))db.add(RouteStop(route_id=r2.id, station_id=s1.id, stop_order=1))db.add(RouteStop(route_id=r2.id, station_id=s3.id, stop_order=2))db.commit()db.close()

2. 写测试用例

pytest,不要只靠print调试。

# tests/test_route.py
import pytest
from database.db import SessionLocal
from services.route import find_min_transfersdef test_min_transfers():db = SessionLocal()# 从人民广场到陆家嘴,K1直达,应返回K1path = find_min_transfers("人民广场", "陆家嘴", db)assert "K1路" in pathassert path[-1] == "陆家嘴"db.close()

为什么强调测试? 因为图算法很容易出边界错误:起点等于终点、无路径、孤立站点。这些场景在print调试中极易遗漏,测试用例能帮你兜底。

优化扩展:从能用到好用

1. 性能优化:缓存热点数据

公交线路和站点是相对静态的,每次查数据库是浪费。用Redis缓存graph结构。

# services/cache.py
import json
import redisr = redis.Redis(host='localhost', port=6379, db=0)def get_graph_from_cache():data = r.get('bus_graph')if data:return json.loads(data)return Nonedef update_graph_cache(graph: dict):r.set('bus_graph', json.dumps(graph), ex=3600)  # 1小时过期

注意:缓存一致性是坑。当线路调整时,必须主动失效缓存。别让用户看到半年前的老线路。

2. 扩展方向:接入真实数据

如果想进阶,可以接入:

  • OpenStreetMap:获取真实路网和站点坐标。
  • GTFS数据:谷歌通用 Transit Feed 规范,很多城市开放了公交实时数据。在CSDN上搜“GTFS解析Python”,有大量现成库,如gtfs-kit
  • 地图API:高德/百度提供POI搜索和路径规划,后端只负责聚合数据。

3. 避坑清单

  1. 时区问题:公交时刻表是本地时间,服务器可能在UTC。统一用datetime库处理,存数据库时用UTC,展示时转本地。
  2. 站点同名:不同城市可能有同名站点。Station表必须加city_id外键,查询时带上城市参数。
  3. 并发查询:BFS在大型城市(上万站点)可能慢。考虑预计算热门OD对(Origin-Destination),或改用Dijkstra(如果考虑时间成本而非仅换乘次数)。

小结

这个项目代码量不到500行,但覆盖了:ORM建模、图论算法、缓存策略、测试驱动

你不需要一开始就写微服务、K8s、消息队列。先把单体应用跑通,把数据流转逻辑理顺。当你能清晰解释“为什么用BFS而不是Dijkstra”、“为什么加stop_order字段”时,你才算真正入门。

这个知识点你面试被问过吗? 特别是“如何优化公共交通路径查找算法”或“如何处理动态实时数据与静态路网的冲突”,留言说说你的经历,我挑典型的展开聊聊。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/22 8:54:04

3行代码手写实现蓝思指数,面试不再卡壳

3行代码手写实现蓝思指数,面试不再卡壳 面试被问到降雨径流原理,你脑子里是不是只有“下大雨,水变多”这种模糊概念?面试官追问:“具体公式怎么推导?代码怎么落地?”你瞬间大脑空白,手心冒汗。这种尴尬,我太懂了。很多水利后端开发,天天和数据库打交道,却对最基础的物理模型一知半解,导致在做水文预警系统时,…

作者头像 李华
网站建设 2026/9/22 8:54:00

182是联通还是移动?3个高频面试题背后的号码段真相

182是联通还是移动?3个高频面试题背后的号码段真相 刚把同事发来的手机号校验代码复制到本地跑,直接报错了。 打开调试模式一看,逻辑里硬编码的号段判断全乱了。 这种“复制来的代码跑不通不知道怎么调”的坑,在面试和实战中太常见了。 很多后端同学在处理用户注册、短信发送时,习惯性地写死号段判断。…

作者头像 李华
网站建设 2026/9/22 8:53:59

域名转入避坑保姆级教程 新手看这篇就够了

域名转入避坑保姆级教程 新手看这篇就够了 版本升级后 API 全变了,很多老手都头大,更别说刚入行的新人了。如果你正在为域名转入流程繁琐、文档晦涩而抓狂,这篇 保姆级教程 就是为你准备的。别急着划走,我结合过去10年处理过几百个域名迁移项目的经验,把那些藏在官方文档缝隙里的坑,一次给你填平。…

作者头像 李华
网站建设 2026/9/22 8:53:56

2026最新英语一年级手写实现:3个方案横向对比,告别文档迷茫

2026最新英语一年级手写实现:3个方案横向对比,告别文档迷茫 官方文档堆砌了几千页,新手刚上手就晕头转向?这是大多数程序员遇到的真实困境。2026最新的技术栈迭代极快,但核心逻辑往往被繁复的API封装掩盖。我们需要的是直击本质的手写实现,而不是照搬框架的“黑盒”调用。…

作者头像 李华
网站建设 2026/9/22 8:53:46

2026最新苹果手机发布会技术复盘避坑指南

2026最新苹果手机发布会技术复盘避坑指南 配置环境就卡半天,这是很多转行做后端或全栈的同学在准备2026最新技术栈时的真实痛点。别以为看一场苹果手机发布会只是看个热闹,背后牵扯的实时流媒体处理、高并发架构、数据一致性,全是面试里的硬核考点。很多面试官喜欢用这种热点场景来考察你的系统设计能力,如果你…

作者头像 李华
网站建设 2026/9/22 8:53:38

电脑打开软件慢排查速查手册:3步定位根因与修复实战

电脑打开软件慢排查速查手册:3步定位根因与修复实战 面试被问“为什么电脑打开软件慢”答不上来?这不仅是性能优化问题,更是系统底层机制的考察。很多开发者只知重启大法,却不懂进程加载、磁盘I/O瓶颈与内存映射的本质。这份速查手册直击现场痛点,帮你在3分钟内定位卡死根源,彻底告别盲目重装系统。…

作者头像 李华