ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于FastAPI+Vue3+微信小程序的网约车毕业设计实战

基于FastAPI+Vue3+微信小程序的网约车毕业设计实战 每年到毕业设计选题季总有一批同学会在“图书管理系统”“购物商城”“博客系统”这几个经典题目之间反复横跳。不是说这些题目不能做而是它们已经在太多届学长学姐的论文和答辩PPT里出现过老师一眼就能看出套路很难给出高分。如果你既想用 Python又想做一个“听起来真实、做起来有挑战、拆开又不至于失控”的题目网约车系统其实是一个非常被低估的选择。网约车表面上看只是“地图 叫车”但它实际上是一条完整的业务闭环乘客发单、司机接单、行程开始、距离计费、支付结算、后台管理。一套系统做完移动端、后端接口、管理后台、数据库设计、权限控制、实时通信全部覆盖。答辩时可以讲的东西非常多而且每一块都有真实业务场景支撑不像“图书管理”只能讲 CRUD。这篇文章要拆解的就是一套免费开源的网约车毕业设计项目微信小程序端使用原生小程序后端使用 Python FastAPI管理端使用 Vue3。它不是只能跑通 demo 的空壳而是一个订单状态完整、接口清晰、角色权限明确的项目适合 Python 微信小程序毕业设计、课程设计也可以作为学习 FastAPI 和 Vue3 的实战案例。在往下看之前先给你一个总体判断这套技术组合的性价比很高。FastAPI 负责后端接口和实时推送Vue3 做管理后台微信小程序天然具备定位、地图、扫码即用的能力三者分工明确不会出现“一个项目要写两套前端”的痛苦。下面我会从技术选型、业务设计、核心代码、运行验证、常见坑点五个维度把整个项目讲清楚。1. 为什么网约车项目比“图书管理”更有竞争力1.1 网约车毕设适合谁如果你是下面几类情况之一这个项目方向值得认真考虑有 Python 基础想选一个偏后端的题目但不想做纯 API 项目希望有移动端配合演示。对微信小程序感兴趣想做一个“扫码打开就能用”的毕设系统而不是只能在电脑上演示的网页。课程设计需要团队分工后端一人、管理端一人、小程序一人网约车系统天然可以按端拆分。希望简历上有完整项目经验而不是只有“博客系统”“在线商城”这种千篇一律的描述。网约车项目最大的优势在于业务场景真实角色清晰流程有状态变化技术栈覆盖广。它不会像搜索引擎系统那样算法太难也不会像图书管理那样功能单薄难度处于“跳一跳够得着”的区间。1.2 这套项目的定位与边界需要先说明一个边界这是一套“教学级完整闭环”项目不是生产级滴滴。生产级打车系统要处理并发抢单、路线规划、司机调度、风控、高德/腾讯地图服务端 API 对接这些对于毕设来说过于复杂。这套项目把重点放在用户端小程序登录、定位、选择目的地、发单、查看订单状态、模拟支付。司机端接单、开始行程、结束行程、查看收入。管理端Vue3 后台用户管理、司机审核、订单管理、基础统计。后端FastAPI统一鉴权、订单状态机、距离计算、实时推送。从教学角度看这些功能已经足够撑起一篇高质量的毕业设计论文也能在答辩时围绕“订单状态如何流转”“距离费用怎么算”“司机端如何实时收到新单”展开深度问答。2. 技术选型拆解FastAPI Vue3 微信小程序2.1 FastAPI 为什么适合 Python 毕设后端很多 Python 毕设默认选 Flask 或 Django这当然没问题。但 FastAPI 在这几年的上升势头非常明显尤其是适合做前后端分离项目。FastAPI 的核心优势有三个第一类型提示 Pydantic 校验。接口参数写清楚类型以后FastAPI 会自动做参数校验请求体不符合规范时直接返回 422 错误。这个特性在开发调试阶段非常舒服前端传错字段一眼就能看出来。第二自动生成 Swagger 文档。后端启动以后访问/docs就能看到所有接口点击即可调试。毕业设计提交文档时这部分的截图可以直接用老师也能直观看到接口设计。第三异步支持好。后面要做 WebSocket 实时推送FastAPI 原生支持 WebSocket不需要额外引入 Spring 那套复杂配置。from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware app FastAPI( title网约车后台接口, description基于 FastAPI 的网约车系统后端, version1.0.0 ) app.add_middleware( CORSMiddleware, allow_origins[*], allow_credentialsTrue, allow_methods[*], allow_headers[*], )2.2 Vue3 管理台的作用管理台是网约车系统里容易被忽视、但答辩时很加分的一部分。老师不会只看小程序他们会打开后台看数据管理。Vue3 管理台的主要职责管理员登录和权限校验。查看所有订单按状态筛选。管理注册用户、司机信息。查看基础统计比如今日订单数、总营收。技术选型上使用 Vue3 Vite Element Plus。Vite 启动非常快Element Plus 组件齐全表格、表单、对话框都能直接复用适合快速开发管理后台。2.3 微信小程序与三端分工为什么不用 uni-app 或者 H5 代替小程序因为微信小程序原生提供的定位、地图选点、登录能力是毕设演示中最直观的部分。三端分工如下表所示端技术栈主要职责乘客端微信小程序原生登录、定位、发单、查单、模拟支付司机端微信小程序或 H5接单、开始行程、结束行程管理端Vue3 Element Plus用户管理、订单管理、数据统计后端FastAPI SQLAlchemy统一鉴权、订单逻辑、计费、推送选择微信小程序还有一个现实原因演示效果好。答辩现场扫码就能打开不需要提前安装 App。老师第一眼看到的是一个完整可操作的移动应用而不是浏览器里的网页。3. 功能设计与订单状态机3.1 三种角色与核心功能设计网约车系统前先要理清角色边界。这套项目包含三种角色乘客端核心功能微信登录或手机号密码登录。获取当前定位。在地图上选择目的地。发布订单并查看司机接单状态。行程结束后支付费用。司机端核心功能司机注册或后台录入。查看待接单列表。接单、开始行程、结束行程。查看个人收入。管理员端核心功能登录后台。审核司机资质。查看订单列表和详情。查看用户列表。业务上不需要把司机端做得太重。毕设阶段的司机端可以简化成订单列表 接单按钮 行程操作核心是让订单状态能转起来。3.2 订单状态机是关键网约车系统最核心的部分不是地图而是订单状态机。几乎所有接口逻辑都围绕状态在转。建议定义以下几类状态pending 待接单 accepted 司机已接单 traveling 行程进行中 completed 已完成 cancelled 已取消订单状态流转规则乘客发单后订单状态为 pending。司机点击接单状态从 pending 变为 accepted。司机到达上车点并点击“开始行程”状态从 accepted 变为 traveling。司机点击“结束行程”系统根据里程和时长计算费用状态变为 completed。在 pending 状态下乘客或司机可取消订单状态变为 cancelled。这里真正容易踩坑的地方是学生容易把状态字段写成普通字符串到处if order.status pending结果状态一多就乱。更推荐用枚举常量来管理。# app/models/enums.py from enum import Enum class OrderStatus(str, Enum): PENDING pending ACCEPTED accepted TRAVELING traveling COMPLETED completed CANCELLED cancelled3.3 计费与模拟支付设计计费规则可以采用常见打车软件的方式起步价 里程费 时长费。举个例子起步价 10 元包含 3 公里 超出部分每公里 2.5 元 低速或等待时间每分钟 0.5 元这些参数建议放到数据库配置表或常量文件里不要散落在接口代码里。答辩时老师会问“价格怎么算”你能直接在代码里找到计算函数会显得工程素养更好。支付部分是很多毕设忽略的坑。个人主体的小程序无法开通微信支付所以不建议接入真实支付。更稳妥的做法是模拟支付用户余额充足时点击“确认支付”后端将订单标记为已完成并生成支付记录。这样流程完整又不需要处理复杂的支付回调。4. 环境准备与依赖安装4.1 后端环境后端推荐使用 Python 3.9 或更高版本具体解释器版本以你本机为准。建议创建独立虚拟环境避免污染系统 Python。cd backend python -m venv venv # Windows 激活方式 venv\Scripts\activate # macOS / Linux 激活方式 source venv/bin/activate pip install fastapi uvicorn[standard] pip install sqlalchemy pydantic python-jose[cryptography] passlib[bcrypt] python-multipart主要依赖说明fastapiWeb 框架。uvicorn[standard]ASGI 服务器带 WebSocket 支持。sqlalchemyORM 框架方便操作 MySQL 或 SQLite。python-jose[cryptography]生成和校验 JWT。passlib[bcrypt]密码哈希不要用明文存密码。4.2 管理端环境管理端使用 Vue3 Vite需要 Node.js 18 或更高版本。cd frontend npm install npm run dev如果网络较慢可以换用国内镜像源。4.3 小程序开发工具微信小程序端需要下载微信开发者工具。无需注册企业账号使用测试号即可开发调试。要注意的是小程序开发时有一个经典坑真机上不能访问localhost。开发阶段可以把后端地址改成电脑的局域网 IP例如http://192.168.1.100:8000同时在小程序开发者工具中勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”。5. FastAPI 后端核心代码实现5.1 项目目录结构后端代码建议按模块划分而不是把全部接口写在main.py里。一个清晰的目录结构既方便自己维护也能让毕业设计论文里的“系统架构”章节更好写。backend/ ├── main.py # 应用入口 ├── config.py # 配置项 ├── database.py # 数据库连接 ├── models/ # ORM 模型 │ ├── user.py │ └── order.py ├── schemas/ # Pydantic 请求/响应模型 │ ├── auth.py │ ├── order.py │ └── response.py ├── api/ # 路由模块 │ ├── auth.py │ ├── orders.py │ └── ws.py └── core/ ├── security.py # JWT 工具 └── distance.py # 距离计算5.2 统一接口返回格式很多 FastAPI 初学者会把接口返回值写成各种形状前端解析时经常要判断。更推荐在项目里定义一个统一响应结构这样可以呼应“fastapi 项目接口返回格式统一”的实践。# app/schemas/response.py from typing import TypeVar, Generic, Optional from pydantic import BaseModel T TypeVar(T) class ApiResponse(BaseModel, Generic[T]): code: int 0 message: str success data: Optional[T] None class ApiResponseError(BaseModel): code: int 1 message: str data: None None后端接口统一返回ApiResponse前端只需要判断code是否为 0可读性和可维护性都会提升很多。5.3 用户登录与 JWT 鉴权JWT 是现代前后端分离项目的常见鉴权方式。用户登录成功后后端签发一个带有效期的 token后续请求在请求头加上Authorization: Bearer token即可。# app/core/security.py from datetime import datetime, timedelta from typing import Optional from jose import jwt from passlib.context import CryptContext SECRET_KEY your-secret-key-change-in-production ALGORITHM HS256 ACCESS_TOKEN_EXPIRE_MINUTES 60 * 24 pwd_context CryptContext(schemes[bcrypt], deprecatedauto) def hash_password(password: str) - str: return pwd_context.hash(password) def verify_password(plain_password: str, hashed_password: str) - bool: return pwd_context.verify(plain_password, hashed_password) def create_access_token(data: dict, expires_minutes: Optional[int] None) - str: to_encode data.copy() expire datetime.utcnow() timedelta( minutesexpires_minutes or ACCESS_TOKEN_EXPIRE_MINUTES ) to_encode.update({exp: expire}) return jwt.encode(to_encode, SECRET_KEY, algorithmALGORITHM)登录接口示例# app/api/auth.py from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from sqlalchemy.orm import Session from app.database import get_db from app.models.user import User from app.core.security import verify_password, create_access_token from app.schemas.response import ApiResponse router APIRouter(prefix/api/auth, tags[认证]) class LoginRequest(BaseModel): phone: str password: str router.post(/login, response_modelApiResponse) def login(data: LoginRequest, db: Session Depends(get_db)): user db.query(User).filter(User.phone data.phone).first() if not user or not verify_password(data.password, user.hashed_password): raise HTTPException(status_code400, detail手机号或密码错误) token create_access_token({sub: str(user.id), role: user.role.value}) return ApiResponse(data{ access_token: token, token_type: bearer, user_id: user.id, role: user.role.value })这里有几个需要注意的细节一是密码必须哈希存储绝对不要明文保存二是SECRET_KEY不要写在代码里生产环境建议用环境变量管理三是 token 过期时间要根据系统使用场景设置毕设阶段设为 24 小时比较合理。5.4 创建订单接口乘客发单是网约车系统里最重要的业务操作。请求参数包括起点经纬度、终点经纬度、起点名称、终点名称后端创建订单后状态为 pending。# app/api/orders.py from fastapi import APIRouter, Depends from pydantic import BaseModel from sqlalchemy.orm import Session from app.database import get_db from app.models.order import Order from app.schemas.response import ApiResponse router APIRouter(prefix/api/orders, tags[订单]) class OrderCreate(BaseModel): start_lng: float start_lat: float start_address: str end_lng: float end_lat: float end_address: str router.post(/, response_modelApiResponse) def create_order(data: OrderCreate, user_id: int, db: Session Depends(get_db)): order Order( user_iduser_id, statuspending, start_lngdata.start_lng, start_latdata.start_lat, start_addressdata.start_address, end_lngdata.end_lng, end_latdata.end_lat, end_addressdata.end_address, ) db.add(order) db.commit() db.refresh(order) return ApiResponse(data{order_id: order.id, status: order.status})实际项目中user_id应该从 JWT 中解析而不是前端传入。过滤思路是写一个依赖函数统一从 token 解析出当前用户。5.5 距离计算与预估费用网约车的里程费用需要根据两个经纬度坐标计算距离。这里使用 Haversine 公式计算球面两点距离精度足够应付毕设场景。# app/core/distance.py import math def haversine(lng1: float, lat1: float, lng2: float, lat2: float) - float: 根据两个点的经纬度计算球面距离单位千米 R 6371.0 d_lat math.radians(lat2 - lat1) d_lng math.radians(lng2 - lng1) a ( math.sin(d_lat / 2) ** 2 math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(d_lng / 2) ** 2 ) c 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) return round(R * c, 2)费用计算函数# app/core/fare.py BASE_PRICE 10.0 BASE_DISTANCE 3.0 PRICE_PER_KM 2.5 PRICE_PER_MINUTE 0.5 def calculate_fare(distance_km: float, duration_minutes: float) - float: if distance_km BASE_DISTANCE: return BASE_PRICE extra_distance distance_km - BASE_DISTANCE fare BASE_PRICE extra_distance * PRICE_PER_KM duration_minutes * PRICE_PER_MINUTE return round(fare, 2)到了结束行程的接口后端根据订单的起点终点坐标计算距离再结合行程耗时生成费用更新订单状态为 completed。5.6 WebSocket 实时推送司机端实时接收新订单常见方案有两种轮询和 WebSocket。轮询实现简单但不够“高级”WebSocket 实时性好也能在答辩中体现技术深度。FastAPI 支持 WebSocket 非常直观# app/api/ws.py from fastapi import APIRouter, WebSocket, WebSocketDisconnect router APIRouter() class ConnectionManager: def __init__(self): self.active_connections: list[WebSocket] [] async def connect(self, websocket: WebSocket): await websocket.accept() self.active_connections.append(websocket) def disconnect(self, websocket: WebSocket): if websocket in self.active_connections: self.active_connections.remove(websocket) async def broadcast(self, message: dict): for connection in self.active_connections: await connection.send_json(message) manager ConnectionManager() router.websocket(/ws/orders) async def websocket_endpoint(websocket: WebSocket): await manager.connect(websocket) try: while True: await websocket.receive_text() except WebSocketDisconnect: manager.disconnect(websocket)当乘客创建订单后调用manager.broadcast给所有在线司机推送新订单消息。这样司机端不用反复刷新列表体验也更接近真实打车软件。如果 WebSocket 调试遇到困难可以先退回轮询方案每 5 秒请求一次待接单列表。轮询实现简单稳定性高作为毕设完全够用。6. Vue3 管理端核心代码实现6.1 axios 请求封装管理端所有接口请求建议统一走一个 axios 实例自动携带 token统一处理错误提示。// frontend/src/utils/request.js import axios from axios; import { ElMessage } from element-plus; import router from /router; const request axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || http://localhost:8000, timeout: 10000, }); request.interceptors.request.use((config) { const token localStorage.getItem(admin_token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }); request.interceptors.response.use( (response) { const res response.data; if (res.code ! 0) { ElMessage.error(res.message || 请求失败); return Promise.reject(new Error(res.message)); } return res; }, (error) { if (error.response?.status 401) { localStorage.removeItem(admin_token); router.push(/login); } ElMessage.error(error.response?.data?.detail || 网络错误); return Promise.reject(error); } ); export default request;6.2 登录状态与路由守卫Vue3 管理台的路由守卫用来控制访问权限未登录时自动跳转到登录页。// frontend/src/router/index.js import { createRouter, createWebHistory } from vue-router; import { ElMessage } from element-plus; const router createRouter({ history: createWebHistory(), routes: [ { path: /login, component: () import(/views/Login.vue) }, { path: /, redirect: /dashboard }, { path: /dashboard, component: () import(/layout/AdminLayout.vue), meta: { requiresAuth: true }, children: [ { path: , component: () import(/views/Dashboard.vue) }, { path: orders, component: () import(/views/order/OrderList.vue) }, { path: users, component: () import(/views/user/UserList.vue) }, ], }, ], }); router.beforeEach((to, from, next) { const token localStorage.getItem(admin_token); if (to.meta.requiresAuth !token) { ElMessage.warning(请先登录); next(/login); } else { next(); } }); export default router;6.3 订单管理页面订单管理页面是管理端最核心的页面用来展示订单列表、按状态筛选。!-- frontend/src/views/order/OrderList.vue -- script setup import { ref, onMounted } from vue; import request from /utils/request; const orders ref([]); const loading ref(false); const fetchOrders async () { loading.value true; try { const res await request.get(/api/orders); orders.value res.data; } finally { loading.value false; } }; onMounted(fetchOrders); /script template el-card shadownever template #header span订单管理/span /template el-table :dataorders v-loadingloading border stripe el-table-column propid label订单号 width80 / el-table-column propcustomer_name label乘客 width120 / el-table-column propstart_address label起点 show-overflow-tooltip / el-table-column propend_address label终点 show-overflow-tooltip / el-table-column propfare label费用(元) width100 / el-table-column propstatus label状态 width100 / /el-table /el-card /templateVue3 管理端不必一开始做得非常完整先把登录、订单列表、用户列表跑通后面按需扩展即可。7. 微信小程序端核心代码实现7.1 登录与用户信息获取微信小程序登录的常规流程是wx.login获取临时 code再通过后端接口换取自定义登录态。// pages/login/login.js Page({ data: { loading: false }, async onLoad() { const token wx.getStorageSync(token); if (token) { wx.switchTab({ url: /pages/index/index }); return; } this.login(); }, login() { this.setData({ loading: true }); wx.login({ success: (res) { if (res.code) { this.exchangeToken(res.code); } else { wx.showToast({ title: 登录失败, icon: none }); } }, complete: () this.setData({ loading: false }) }); }, exchangeToken(code) { wx.request({ url: http://localhost:8000/api/auth/wx_login, method: POST, data: { code }, success: (res) { if (res.statusCode 200 res.data.data.access_token) { wx.setStorageSync(token, res.data.data.access_token); wx.switchTab({ url: /pages/index/index }); } } }); } });这里要提醒一个常见问题新版微信调整了用户信息相关接口wx.getUserInfo已经不能直接弹窗授权需要改用头像昵称填写能力或者在后端只绑定 openid不强制要求用户头像昵称。建议毕设阶段只做wx.login绑定 openid这样流程最稳定不会因为用户拒绝授权导致登录失败。7.2 定位与地图选点小程序端定位和地图选点使用微信原生接口就能完成。// pages/publish/publish.js Page({ data: { startLocation: null, endLocation: null, distance: null, }, onLoad() { this.getCurrentLocation(); }, getCurrentLocation() { wx.getLocation({ type: gcj02, success: (res) { this.setData({ startLocation: { latitude: res.latitude, longitude: res.longitude, } }); }, fail: () { wx.showToast({ title: 无法获取当前位置, icon: none }); } }); }, chooseDestination() { wx.chooseLocation({ success: (res) { this.setData({ endLocation: { latitude: res.latitude, longitude: res.longitude, address: res.address, name: res.name, } }); } }); }, });需要注意wx.getLocation在小程序后台需要配置位置接口权限在app.json中声明requiredPrivateInfos和permission否则真机调试时会报错。模拟器开发时表现正常一到真机就不行这是非常常见的问题。7.3 提交订单用户选择起点终点后点击“立即叫车”提交订单。submitOrder() { const { startLocation, endLocation } this.data; if (!startLocation || !endLocation) { wx.showToast({ title: 请选择起点和终点, icon: none }); return; } const token wx.getStorageSync(token); wx.request({ url: http://localhost:8000/api/orders/, method: POST, data: { start_lng: startLocation.longitude, start_lat: startLocation.latitude, start_address: startLocation.address || 当前位置, end_lng: endLocation.longitude, end_lat: endLocation.latitude, end_address: endLocation.name || endLocation.address, }, header: { Content-Type: application/json, Authorization: Bearer ${token}, }, success: (res) { if (res.statusCode 200) { wx.navigateTo({ url: /pages/order-detail/order-detail?id${res.data.data.order_id} }); } } }); }订单详情页可以定时轮询订单状态接口根据状态显示不同按钮待接单时显示等待司机接单已接单时显示司机接驾中行程中显示当前司机位置已完成显示支付按钮。8. 运行与完整流程验证8.1 启动后端cd backend source venv/bin/activate uvicorn main:app --reload --host 0.0.0.0 --port 8000启动成功后浏览器访问http://localhost:8000/docs可以看到 FastAPI 自动生成的 Swagger 接口文档。8.2 启动管理端cd frontend npm install npm run dev默认情况下 Vite 会启动在http://localhost:5173打开后可以看到 Vue3 管理后台。8.3 打开小程序并进行闭环测试微信开发者工具导入小程序目录AppID 选择测试号勾选开发环境不校验域名。建议按以下顺序跑通完整流程小程序注册乘客账号。在小程序端模拟乘客登录获取定位。选择目的地提交订单。在管理端或司机端看到待接单订单。司机接单订单状态变为已接单。司机开始行程并结束行程。后端计算费用小程序端看到订单金额。模拟支付订单状态变为已完成。现在梳理一下预期结果和验证方式后端日志中能看到接口调用记录。Swagger 文档中订单接口返回code: 0说明请求成功。订单表数据从 pending 依次流转到 accepted、traveling、completed。管理端订单列表能看到状态变化。小程序端能正常展示从发单到支付的完整状态。如果某一步没有按预期执行先看后端终端日志确认请求是否到达后端再检查前端传参是否与接口定义一致。9. 常见问题与排查思路以下是开发这个项目时最常遇到的几个问题整理成表格方便你快速对照处理。问题现象可能原因排查方式解决方案小程序请求后端报“网络错误”使用了 localhost 或未关闭域名校验查看浏览器后端日志判断请求是否到达开发时改为局域网 IP并在开发者工具中勾选“不校验合法域名”getLocation 在真机上报错缺少位置权限配置查看小程序控制台报错信息在 app.json 中配置 permission 和 requiredPrivateInfos获取登录用户信息失败微信调整用户信息接口getUserInfo 不再弹窗检查登录用的接口和用户授权状态改为 wx.login 绑定 openid不使用 getInfo 强授权FastAPI 启动时报端口占用8000 端口已被其他程序使用查看启动日志中的 bind 错误换一个端口如--port 8001依赖版本冲突导致启动失败fastapi、pydantic、SQLAlchemy 版本不兼容查看完整错误堆栈用 requirements.txt 锁定已验证版本数据库查询中文乱码或写入失败SQLite/MySQL 字符集配置不正确查看数据库连接配置开发用 SQLite 较省事MySQL 注意 utf8
返回列表