大家好,我是你们的老朋友。这段时间在帮助一些学生朋友完成毕业设计时,发现很多同学对“微信小程序 + Python 后端”这套组合非常感兴趣,尤其是网约车、打车这一类贴近真实业务场景的项目。网约车项目虽然看起来功能多、业务复杂,但只要把技术栈选对、把项目结构拆清楚,它反而是一个很适合用来做毕设或者课程设计的完整案例:既有用户端的小程序交互,又有后端接口的权限管理,还能涉及到地图定位、订单状态机等比较有含金量的设计。
本文将基于Python(FastAPI)+ 微信小程序 + Vue3 管理后台这套技术栈,手把手带你从零搭建一个网约车(在线叫车 / 移动出行)项目的核心功能。文章会覆盖技术选型逻辑、数据库设计、后端核心接口实现、小程序前端页面调用、后台管理系统搭建,以及微信登录、订单派单、位置上报等高频难点的解决方案。无论你是准备做毕业设计,还是想系统学习全栈开发,这篇文章都可以直接作为项目脚手架来参考。
1. 项目背景与核心概念
1.1 网约车项目的业务闭环是什么
网约车项目,本质上是一个多角色、多端协同的实时交易系统。从用户打开小程序输入目的地,到附近司机接单,再到乘客上车、行程开始、支付完成,整个链路涉及四个核心角色:
- 乘客(用户端):通过微信小程序发起叫车请求,查看司机位置,支付车费,评价行程。
- 司机(司机端):接收订单推送,前往乘客出发地,开始/结束行程,收入结算。
- 平台管理员(管理后台):审核司机资质,查看实时运营数据,处理订单异常。
- 系统后端(服务端):处理登录鉴权、订单匹配、计价、位置上报、支付回调等核心业务。
对于毕业设计而言,我们不需要把滴滴那样复杂的调度系统全部实现,但至少要打通“用户下单 → 司机接单 → 行程开始 → 行程结束 → 支付 → 评价”这个最小闭环。只要把这条主链路做通,项目的完整度和答辩说服力都会非常强。
1.2 为什么选择 FastAPI + Vue3 + 微信小程序
很多同学可能在纠结“后端用 Django 还是 Spring Boot”,我给出的建议是:如果你是 Python 技术栈,FastAPI 是当前性价比最高的选择。原因如下:
- FastAPI 基于 Python 类型注解,自动生成 OpenAPI 文档(Swagger UI),调试接口非常方便。
- 性能表现优于 Flask 和 Django 传统同步框架,支持 async/await 异步处理,适合地图定位、订单推送这类 IO 密集型场景。
- 代码量少、学习曲线平缓,对于需要快速完成毕设的同学来说非常友好。
前端方面,微信小程序是必须的,因为题目要求明确是微信小程序项目。而管理后台选择 Vue3 + Element Plus,是因为 Vue3 的 Composition API 语法简洁,后台管理系统的表格、表单、弹窗等场景有 Element Plus 组件库可以直接复用,开发效率很高。
1.3 项目适合的人群
本文适合以下读者:
- 正在准备 Python 或微信小程序方向毕业设计的本科生。
- 想学习 FastAPI 后端接口开发的初级开发者。
- 想了解微信小程序定位、登录、支付等核心 API 用法的前端学习者。
- 需要一套完整前后端分离项目作为课程设计作品的学生。
如果你已经有了 Python 基础语法和基本的 JavaScript 概念,学完本文后,你应该能够独立搭建出一个可以演示的网约车小程序项目,并理解整个业务链路背后的技术方案。
2. 环境准备与版本说明
在开始写代码之前,先把开发环境准备好。版本需要根据你的项目实际情况调整,本文示例以常见环境为例,重点演示配置思路。
2.1 开发环境清单
| 分类 | 工具/框架 | 版本建议 | 说明 |
|---|---|---|---|
| 后端 | Python | 3.9+ | 建议 3.10 或 3.11,支持新版 FastAPI 语法 |
| 后端 | FastAPI | 0.100+ | 异步 Web 框架 |
| 后端 | SQLAlchemy | 2.x | ORM 框架,操作数据库 |
| 后端 | Pydantic | 2.x | 数据校验与序列化 |
| 后端 | MySQL | 5.7 或 8.0 | 业务数据存储 |
| 后端 | Redis | 6.x+ | 用户会话、验证码、司机位置缓存 |
| 前端小程序 | 微信开发者工具 | 最新稳定版 | 编写和调试小程序 |
| 管理后台 | Vue3 + Vite | Vite 5.x | 后台管理系统 |
| 管理后台 | Element Plus | 2.x | UI 组件库 |
| 管理后台 | Pinia | 2.x | 状态管理 |
| 接口调试 | 微信开发者工具 / Apifox | 任意 | 调试后端接口 |
2.2 示例项目目录结构
一个清晰的项目结构是毕设能否顺利通过检查的关键。建议采用backend(后端服务)+ miniprogram(用户端小程序)+ admin(管理后台)三个目录并列的方式:
ride-hailing/ ├── backend/ # FastAPI 后端服务 │ ├── app/ │ │ ├── api/ # 路由层 │ │ ├── core/ # 配置与安全 │ │ ├── models/ # ORM 模型 │ │ ├── schemas/ # Pydantic 模型 │ │ ├── services/ # 业务逻辑层 │ │ └── main.py # 入口文件 │ ├── requirements.txt │ └── .env # 环境变量配置 ├── miniprogram/ # 微信小程序用户端 │ ├── pages/ │ │ ├── index/ # 首页(地图+叫车) │ │ ├── order/ # 订单详情 │ │ ├── profile/ # 个人中心 │ │ └── login/ # 登录页 │ ├── utils/ # 请求封装工具 │ └── app.js └── admin/ # Vue3 管理后台 ├── src/ │ ├── api/ # 接口请求封装 │ ├── views/ # 页面组件 │ ├── router/ # 路由配置 │ └── store/ # Pinia 状态 ├── package.json └── vite.config.js2.3 后端依赖安装
在后端目录下创建requirements.txt,内容如下:
fastapi==0.104.1 uvicorn[standard]==0.24.0 sqlalchemy==2.0.23 pymysql==1.1.0 pydantic==2.5.2 pydantic-settings==2.1.0 redis==5.0.1 httpx==0.25.2 python-jose[cryptography]==3.3.0 passlib[bcrypt]==1.7.4 python-multipart==0.0.6然后执行安装命令:
pip install -r requirements.txt这里需要注意的是,passlib和bcrypt在部分 Python 3.11 环境下可能会有版本兼容问题。如果安装后出现bcrypt报错,可以单独指定版本安装:
pip install bcrypt==4.0.12.4 环境变量配置
在.env文件中配置项目的基本变量:
# 数据库连接 DATABASE_URL=mysql+pymysql://root:yourpassword@localhost:3306/ride_hailing?charset=utf8mb4 # Redis 连接 REDIS_URL=redis://localhost:6379/0 # JWT 密钥与过期时间 SECRET_KEY=your-secret-key-change-in-production ACCESS_TOKEN_EXPIRE_MINUTES=1440 # 微信小程序配置 WX_APPID=your-wx-appid WX_SECRET=your-wx-secret注意:微信小程序的appid和secret需要登录微信公众平台后获取。如果你还没有注册小程序账号,可以先使用测试号,之后在公众平台配置真实信息。
3. 核心概念拆解与后端设计
这部分是项目的核心原理讲解。很多同学拿到项目不知道从哪里下手,本质上是没有把业务逻辑和技术模块对应起来。下面我把网约车项目拆成几个技术关键点,逐一说明。
3.1 FastAPI 路由设计与路径参数
FastAPI 的路由设计非常直观。我们以订单模块为例,看一下如何使用装饰器定义接口,以及路径参数的用法。
# 文件路径:backend/app/api/order.py from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from typing import Optional from app.core.database import get_db from app.services import order_service from app.schemas.order import OrderCreate, OrderOut, OrderDetail router = APIRouter(prefix="/api/order", tags=["订单模块"]) @router.post("/create", response_model=OrderOut) def create_order(order_in: OrderCreate, db: Session = Depends(get_db)): """ 乘客创建订单接口 :param order_in: 包含起点经纬度、终点经纬度、用户ID等 :param db: 数据库会话 """ order = order_service.create_order(db, order_in) return order @router.get("/detail/{order_id}", response_model=OrderDetail) def get_order_detail(order_id: int, db: Session = Depends(get_db)): """ 获取订单详情接口,路径参数 order_id 用于定位具体订单 """ order = order_service.get_order_by_id(db, order_id) if not order: raise HTTPException(status_code=404, detail="订单不存在") return order @router.get("/list") def get_order_list( user_id: int, status: Optional[str] = "all", page: int = 1, page_size: int = 10, db: Session = Depends(get_db), ): """订单列表接口,支持按状态筛选与分页""" return order_service.get_order_list(db, user_id, status, page, page_size)这里的要点是:
{order_id}是路径参数,FastAPI 会自动做类型转换和校验。如果传入的不是整数,接口会直接返回 422 校验错误,不需要额外写判断逻辑。Depends(get_db)是 FastAPI 的依赖注入机制,每个请求会自动创建数据库会话并在请求结束后关闭。- 参数
user_id、status、page是查询参数,也就是 URL 中以?拼接的参数。
3.2 Pydantic 的 Union 作用与统一返回格式
在开发接口时,经常会遇到“同一个字段可能是不同类型”的情况,比如订单金额在未计价时是None,计价后是float。这时就可以用到Union类型。
# 文件路径:backend/app/schemas/order.py from typing import Optional, Union from pydantic import BaseModel, Field class OrderCreate(BaseModel): user_id: int start_lng: float start_lat: float end_lng: float end_lat: float start_address: str = Field(..., max_length=255) end_address: str = Field(..., max_length=255) class OrderOut(BaseModel): id: int order_no: str status: str driver_id: Optional[int] = None price: Union[float, None] = None create_time: str class Config: from_attributes = TrueUnion[float, None]等价于Optional[float],表示该字段可以是浮点数,也可以是空值。
为了让前端小程序统一处理接口返回,我们还需要设计一个统一的响应格式。推荐所有接口返回如下结构:
{ "code": 200, "message": "success", "data": { } }在 FastAPI 中,可以通过自定义响应模型或中间件实现。这里给出一个简单但有效的方案:
# 文件路径:backend/app/core/response.py from typing import Any from fastapi.responses import JSONResponse def success(data: Any = None, message: str = "success"): return JSONResponse(content={"code": 200, "message": message, "data": data}) def error(message: str = "error", code: int = 400, data: Any = None): return JSONResponse( status_code=code, content={"code": code, "message": message, "data": data} )当然,这种方式适合中小型项目。如果想更规范,可以自定义APIRoute,在路由执行完成后自动包装返回值。
3.3 微信小程序登录与身份鉴权流程
微信小程序登录是网约车项目的第一个难点。小程序端通过wx.login()获取临时code,后端拿着code去微信接口换取openid和session_key。整体流程如下:
- 小程序调用
wx.login()获取code。 - 小程序将
code发送到后端/api/auth/wx_login接口。 - 后端调用微信接口
https://api.weixin.qq.com/sns/jscode2session,传入appid + secret + code。 - 微信返回
openid和session_key。 - 后端用
openid查找或创建用户,签发JWT token返回给小程序。 - 小程序之后的所有请求,在请求头
Authorization中携带token。
后端核心代码如下:
# 文件路径:backend/app/services/auth_service.py import httpx from app.core.config import settings from app.core.security import create_access_token from app.models.user import User from sqlalchemy.orm import Session async def wx_login(db: Session, code: str): """微信登录服务""" url = "https://api.weixin.qq.com/sns/jscode2session" params = { "appid": settings.WX_APPID, "secret": settings.WX_SECRET, "js_code": code, "grant_type": "authorization_code" } async with httpx.AsyncClient() as client: resp = await client.get(url, params=params) data = resp.json() if "errcode" in data: raise Exception(f"微信登录失败: {data.get('errmsg')}") openid = data.get("openid") session_key = data.get("session_key") user = db.query(User).filter(User.openid == openid).first() if not user: user = User(openid=openid, nickname="微信用户", avatar="", role="passenger") db.add(user) db.commit() db.refresh(user) token = create_access_token({"sub": str(user.id)}) return {"token": token, "user_id": user.id, "nickname": user.nickname}对应的小程序端请求封装如下:
// 文件路径:miniprogram/utils/request.js const BASE_URL = 'http://localhost:8000/api' const request = (url, method = 'GET', data = {}) => { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + url, method: method, data: data, header: { 'Content-Type': 'application/json', 'Authorization': wx.getStorageSync('token') || '' }, success: (res) => { if (res.data.code === 200) { resolve(res.data.data) } else { wx.showToast({ title: res.data.message, icon: 'none' }) reject(res.data) } }, fail: (err) => reject(err) }) }) } module.exports = { request }这里需要特别提醒大家的是:在本地开发时,微信小程序默认无法请求 http://localhost 地址。你需要在微信开发者工具中勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”,否则请求会被拦截。
3.4 地图与位置上报设计
网约车项目离不开地图。在小程序端,我们使用微信小程序自带的<map>组件来展示地图,并使用wx.getLocation()获取当前定位。由于微信小程序的map组件默认使用的是腾讯地图数据,所以很多同学会问“能不能用高德地图?”——答案是:小程序原生组件只能用腾讯地图,但你可以接入高德地图的 Web 服务 API 来做地理编码、路线规划等后端计算。
前端的定位获取代码如下:
// 文件路径:miniprogram/pages/index/index.js Page({ data: { latitude: 39.908823, longitude: 116.397470, markers: [], destination: '', }, onLoad() { this.getLocation() }, getLocation() { wx.getLocation({ type: 'gcj02', success: (res) => { this.setData({ latitude: res.latitude, longitude: res.longitude }) // 将当前位置上报给后端,便于后续派单 this.reportLocation(res.latitude, res.longitude) }, fail: () => { wx.showToast({ title: '定位失败,请检查授权', icon: 'none' }) } }) }, reportLocation(lat, lng) { // 调用后端接口上报乘客位置 const { request } = require('../../utils/request') request('/user/report_location', 'POST', { latitude: lat, longitude: lng }) } })后端收到位置上报后,将实时位置写入 Redis,并使用有序集合(Sorted Set)按经纬度或者订单状态进行标记。不过对于毕设来说,直接用一张driver_location表存最新位置即可,不必引入复杂的地理索引。
3.5 订单状态机设计
订单状态是网约车项目的核心业务逻辑。推荐使用状态机来管理订单生命周期:
待接单(WAITING) → 已接单(ACCEPTED) → 行程中(ON_TRIP) → 已完成(FINISHED) ↓ ↓ 已取消(CANCELLED) 已支付(PAID)在数据库层面,订单表增加一个status字段,每次状态变更时不仅更新数据库,还需要记录一条状态流转日志,方便以后排查问题。示例订单表结构如下:
CREATE TABLE `orders` ( `id` INT NOT NULL AUTO_INCREMENT, `order_no` VARCHAR(64) NOT NULL COMMENT '订单编号', `user_id` INT NOT NULL COMMENT '乘客ID', `driver_id` INT DEFAULT NULL COMMENT '司机ID', `start_lng` DECIMAL(10, 6) NOT NULL, `start_lat` DECIMAL(10, 6) NOT NULL, `end_lng` DECIMAL(10, 6) NOT NULL, `end_lat` DECIMAL(10, 6) NOT NULL, `start_address` VARCHAR(255) NOT NULL, `end_address` VARCHAR(255) NOT NULL, `status` VARCHAR(20) NOT NULL DEFAULT 'WAITING' COMMENT '订单状态', `price` DECIMAL(10, 2) DEFAULT NULL COMMENT '订单金额', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;4. 完整实战:从后端到小程序前端
下面我们进入完整实战环节。我会按照“先搭后端,再做小程序,最后写管理后台”的顺序展开。
4.1 创建 FastAPI 后端入口
首先创建main.py文件:
# 文件路径:backend/app/main.py from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from app.api import auth, user, order, driver from app.core.database import Base, engine # 创建数据库表(首次启动时执行) Base.metadata.create_all(bind=engine) app = FastAPI( title="网约车出行 API", description="基于 FastAPI 的微信小程序网约车后端接口", version="1.0.0" ) # 允许跨域,方便本地调试管理后台 app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # 注册路由 app.include_router(auth.router, prefix="/api/auth", tags=["认证"]) app.include_router(user.router, prefix="/api/user", tags=["用户"]) app.include_router(order.router, prefix="/api/order", tags=["订单"]) app.include_router(driver.router, prefix="/api/driver", tags=["司机"]) @app.get("/") def read_root(): return {"message": "网约车后端服务运行中"}启动服务:
cd backend uvicorn app.main:app --reload --host 0.0.0.0 --port 8000启动后,访问http://localhost:8000/docs就能看到 Swagger 自动化接口文档。这是 FastAPI 非常方便的一个特性,也是答辩时可以展示的亮点。
4.2 设计核心数据表
除了前面已经给出的订单表,我们还需要用户表和司机表。
用户表:
CREATE TABLE `users` ( `id` INT NOT NULL AUTO_INCREMENT, `openid` VARCHAR(128) NOT NULL COMMENT '微信openid', `nickname` VARCHAR(64) DEFAULT '' COMMENT '昵称', `avatar` VARCHAR(255) DEFAULT '' COMMENT '头像', `phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号', `role` VARCHAR(20) DEFAULT 'passenger' COMMENT '角色: passenger/driver/admin', `status` TINYINT DEFAULT 1 COMMENT '状态: 1正常 0禁用', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_openid` (`openid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;司机表(可以认为是用户的扩展表):
CREATE TABLE `drivers` ( `id` INT NOT NULL AUTO_INCREMENT, `user_id` INT NOT NULL COMMENT '关联用户ID', `name` VARCHAR(32) NOT NULL COMMENT '司机姓名', `license_no` VARCHAR(32) NOT NULL COMMENT '驾驶证号', `plate_no` VARCHAR(16) NOT NULL COMMENT '车牌号', `car_model` VARCHAR(64) DEFAULT '' COMMENT '车型', `status` TINYINT DEFAULT 0 COMMENT '审核状态: 0待审核 1通过 2拒绝', `current_lng` DECIMAL(10, 6) DEFAULT NULL, `current_lat` DECIMAL(10, 6) DEFAULT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_id` (`user_id`), UNIQUE KEY `uk_license_no` (`license_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;4.3 实现乘客下单接口
乘客在小程序首页选择上车点和下车点后,点击“立即叫车”,后端需要创建订单并把订单状态设为待接单。
订单服务层代码:
# 文件路径:backend/app/services/order_service.py import random import string from datetime import datetime from sqlalchemy.orm import Session from app.models.order import Order from app.schemas.order import OrderCreate def generate_order_no(): """生成订单号:时间戳 + 随机字符""" ts = datetime.now().strftime("%Y%m%d%H%M%S") rand = ''.join(random.choices(string.digits, k=6)) return f"{ts}{rand}" def create_order(db: Session, order_in: OrderCreate): order_no = generate_order_no() order = Order( order_no=order_no, user_id=order_in.user_id, start_lng=order_in.start_lng, start_lat=order_in.start_lat, end_lng=order_in.end_lng, end_lat=order_in.end_lat, start_address=order_in.start_address, end_address=order_in.end_address, status="WAITING" ) db.add(order) db.commit() db.refresh(order) return order这里之所以要手动生成order_no,而不是直接用自增 ID,是为了便于在订单列表中展示,也避免用户看到真实的数据量规模。
创建订单后,后端还需要触发“司机派单”逻辑。在真实项目中,派单需要结合实时位置、司机评分、接单距离等条件。在毕业设计中,我们可以简化实现:查询当前所有状态为“空闲”的司机,将订单推送给前 10 个距离最近的司机。
由于订单推送涉及 WebSocket 或消息队列,这里给出一个简化的示例思路:
# 文件路径:backend/app/services/dispatch_service.py from sqlalchemy.orm import Session from app.models.driver import Driver def find_nearby_drivers(db: Session, lat: float, lng: float, radius_km: float = 5.0): """ 查找附近司机。 简化实现:直接查询所有在线司机,然后通过经纬度距离公式过滤。 生产环境建议使用 Redis GEO 或 MySQL 空间索引。 """ drivers = db.query(Driver).filter(Driver.status == 1).all() nearby = [] for driver in drivers: distance = haversine(lat, lng, driver.current_lat, driver.current_lng) if distance <= radius_km: nearby.append({"driver_id": driver.id, "distance": distance}) # 按距离排序 nearby.sort(key=lambda x: x["distance"]) return nearby[:10] def haversine(lat1, lng1, lat2, lng2): """计算两个经纬度之间的距离(单位:公里)""" import math 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 return R * 2 * math.asin(math.sqrt(a))如果是演示环境,可以把这 10 个司机以列表形式存入 Redis 的订单临时 key 中,模拟“订单广播”。
4.4 小程序前端核心页面
在小程序端,最重要的页面是首页的叫车地图页。我们使用map组件 + 底部面板来实现。
<!-- 文件路径:miniprogram/pages/index/index.wxml --> <view class="page"> <map id="map" latitude="{{latitude}}" longitude="{{longitude}}" markers="{{markers}}" show-location style="width: 100%; height: 70vh;" ></map> <view class="order-panel"> <view class="input-item"> <text class="label">上车点</text> <input value="{{startAddress}}" disabled placeholder="定位中..." /> </view> <view class="input-item"> <text class="label">目的地</text> <input bindinput="onDestinationInput" placeholder="请输入目的地" /> </view> <button type="primary" bindtap="callRide" loading="{{calling}}">立即叫车</button> </view> </view>对应的 JS 逻辑:
// 文件路径:miniprogram/pages/index/index.js const { request } = require('../../utils/request') Page({ data: { latitude: 39.908823, longitude: 116.397470, markers: [], startAddress: '', destination: '', calling: false, }, onLoad() { this.getLocation() }, getLocation() { wx.getLocation({ type: 'gcj02', success: (res) => { this.setData({ latitude: res.latitude, longitude: res.longitude }) this.reverseGeocode(res.latitude, res.longitude) } }) }, reverseGeocode(lat, lng) { // 调用腾讯地图逆地理编码接口,将经纬度转为文字地址 wx.request({ url: 'https://apis.map.qq.com/ws/geocoder/v1/', data: { location: `${lat},${lng}`, key: 'YOUR_TENCENT_MAP_KEY' }, success: (res) => { if (res.data.status === 0) { this.setData({ startAddress: res.data.result.address }) } } }) }, onDestinationInput(e) { this.setData({ destination: e.detail.value }) }, callRide() { if (!this.data.destination) { wx.showToast({ title: '请先输入目的地', icon: 'none' }) return } this.setData({ calling: true }) request('/order/create', 'POST', { user_id: wx.getStorageSync('user_id'), start_lng: this.data.longitude, start_lat: this.data.latitude, end_lng: 0, end_lat: 0, start_address: this.data.startAddress, end_address: this.data.destination }).then((res) => { wx.showToast({ title: '叫车成功,等待司机接单', icon: 'success' }) // 跳转到订单详情页 wx.navigateTo({ url: `/pages/order/order?id=${res.id}` }) }).catch(() => { wx.showToast({ title: '叫车失败', icon: 'none' }) }).finally(() => { this.setData({ calling: false }) }) } })注意,这里调用逆地理编码时用到了腾讯地图 WebService API,需要在小程序后台配置 request 合法域名,或者在开发工具中关闭校验。如果你不想申请腾讯地图 Key,也可以直接显示“当前位置”不展示详细地址,影响不大。
4.5 订单详情与司机接单
订单创建后,需要给用户展示一个订单等待页面。这个页面最关键的是轮询或 WebSocket 实时刷新订单状态。毕业设计中用轮询最简单,每 3 秒请求一次订单详情接口。
// 文件路径:miniprogram/pages/order/order.js const { request } = require('../../utils/request') Page({ data: { orderId: null, order: null, timer: null, }, onLoad(options) { this.setData({ orderId: options.id }) this.fetchOrder() this.startPolling() }, fetchOrder() { request(`/order/detail/${this.data.orderId}`, 'GET').then((data) => { this.setData({ order }) if (data.status === 'FINISHED' || data.status === 'CANCELLED') { this.stopPolling() } }) }, startPolling() { this.data.timer = setInterval(() => { this.fetchOrder() }, 3000) }, stopPolling() { if (this.data.timer) { clearInterval(this.data.timer) this.setData({ timer: null }) } }, onUnload() { this.stopPolling() } })在真正的生产项目中,轮询不是最优解,应该使用 WebSocket 做双向实时通信。但作为毕业设计,轮询方案更简单、更直观,也更容易在答辩时讲清楚。
4.6 管理后台(Vue3)搭建
管理后台的主要功能是:登录、司机审核、订单查询、数据统计看板。下面演示一个简单的用户列表页面。
首先创建 Vue3 项目:
npm create vite@latest admin -- --template vue cd admin npm install element-plus @element-plus/icons-vue axios vue-router pinia然后在src/main.js中引入 Element Plus:
// 文件路径:admin/src/main.js import { createApp } from 'vue' import { createPinia } from 'pinia' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import App from './App.vue' import router from './router' const app = createApp(App) app.use(createPinia()) app.use(router) app.use(ElementPlus) app.mount('#app')订单列表页面使用 Element Plus 的el-table和el-tag展示订单状态:
<!-- 文件路径:admin/src/views/OrderList.vue --> <template> <div class="order-list"> <el-card> <el-table :data="orders" stripe style="width: 100%"> <el-table-column prop="order_no" label="订单号" width="200" /> <el-table-column prop="start_address" label="起点" /> <el-table-column prop="end_address" label="终点" /> <el-table-column prop="status" label="状态" width="120"> <template #default="{ row }"> <el-tag :type="statusMap[row.status].type"> {{ statusMap[row.status].label }} </el-tag> </template> </el-table-column> <el-table-column prop="price" label="金额(元)" width="120" /> <el-table-column prop="create_time" label="下单时间" width="180" /> </el-table> </el-card> </div> </template> <script setup> import { ref, onMounted } from 'vue' import { getOrderList } from '../api/order' const orders = ref([]) const statusMap = { WAITING: { label: '待接单', type: 'warning' }, ACCEPTED: { label: '已接单', type: 'primary' }, ON_TRIP: { label: '行程中', type: 'success' }, FINISHED: { label: '已完成', type: 'info' }, CANCELLED: { label: '已取消', type: 'danger' } } onMounted(() => { loadOrders() }) const loadOrders = async () => { const res = await getOrderList({ page: 1, page_size: 20 }) orders.value = res.records } </script>对应的接口封装:
// 文件路径:admin/src/api/order.js import request from '../utils/request' export function getOrderList(params) { return request({ url: '/api/order/admin/list', method: 'get', params }) }对于后台管理系统,有几个 Vue3 的常见用法值得注意:
- 使用
<script setup>语法,代码更简洁。 - 使用
computed计算属性来处理状态映射或筛选逻辑,避免在模板中写复杂表达式。 - 用
Pinia管理登录状态和用户信息,而不是简单挂到全局对象上。
5. 常见问题与排查思路
在开发这个网约车项目的过程中,最容易踩坑的几个问题我整理成了表格,方便大家按图索骥。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 小程序请求后端接口失败 | 本地调试未开启“不校验合法域名” | 在微信开发者工具详情设置中勾选“不校验合法域名” |
| 小程序获取登录后的微信用户失败 | code过期、appid/secret配置错误、接口返回errcode | 检查 appid 是否匹配、确认 code 是一次性的、解码微信返回的 errcode |
| 每次请求都是 401 未授权 | token 过期或未在请求头携带 | 在小程序请求封装中统一添加Authorization头,并做好 token 失效后的重新登录逻辑 |
| 数据库插入中文乱码 | MySQL 表字符集不是 utf8mb4 | 建表时指定DEFAULT CHARSET=utf8mb4,连接串添加?charset=utf8mb4 |
启动 FastAPI 报bcrypt错误 | passlib 与 bcrypt 版本不兼容 | 单独安装bcrypt==4.0.1,或改用pwdlib作为密码哈希库 |
| 小程序地图组件不显示 | 缺少授权scope.userLocation | 在app.json中添加permission配置,用户拒绝后引导重新授权 |
| 管理后台跨域请求失败 | 后端未配置 CORS | 在 FastAPI 中添加CORSMiddleware,allow_origins设置为*或指定域名 |
6. 最佳实践与工程建议
6.1 安全与权限设计
网约车项目涉及用户真实行程数据,在安全方面一定不能马虎。即使只是毕业设计,也要养成好习惯:
- 所有
/api/user、/api/order下的接口都应该做 JWT 鉴权,不能直接裸奔。 - 微信小程序的
secret绝对不能暴露在小程序代码里,必须存放在后端环境变量中。 - 管理后台和用户端的数据要隔离,管理端接口应该单独加管理员权限校验。
- 涉及支付回调时,要校验微信支付签名,防止伪造回调。
- 数据库中用户密码(如果有)必须使用哈希加密存储,推荐
bcrypt或argon2。
微信登录接口的完整权限校验中间件可以参考下面这个简化实现:
# 文件路径:backend/app/core/security.py from fastapi import Request, HTTPException from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from jose import jwt, JWTError from app.core.config import settings security = HTTPBearer() def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)): token = credentials.credentials try: payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"]) user_id = payload.get("sub") if user_id is None: raise HTTPException(status_code=401, detail="无效的token") return user_id except JWTError: raise HTTPException(status_code=401, detail="无效的token")6.2 性能与体验优化
网约车场景下,乘客最关心的是“叫车速度快不快”、“司机位置准不准”。针对这两个痛点,建议在项目中优先做以下优化:
- 司机位置上报频率:不建议前端每 1 秒上报一次位置,高并发下服务器压力很大。合理频率是 3 到 5 秒上报一次,并且写入 Redis 而不是直接更新 MySQL。
- 订单状态刷新:小程序端采用 WebSocket 替代轮询,服务端在有新司机接单时主动推送消息。如果毕设时间紧张,可以保留轮询方案,但要在答辩时说明“生产环境应该用 WebSocket 优化”。
- 数据库索引:对
orders表的user_id、status字段建立联合索引,对drivers表的status字段建立索引。订单量增长后,没有索引的查询会非常慢。 - 接口响应统一:不要让每个接口返回不同的格式,前端会很痛苦。尽量统一为
{ code, message, data }的结构。
6.3 代码可维护性建议
网络上有大量的网约车项目代码,但很多同学的毕设代码存在“所有逻辑都写在路由函数里”的问题。为了让代码更容易维护,也是为了让答辩老师看着更专业,建议遵守以下分层规范:
api/路由层:只做参数校验和结果返回,不写业务逻辑。services/业务层:处理订单、支付、派单等业务流程。models/数据模型层:定义 ORM 模型,不写复杂查询。schemas/数据校验层:定义请求体和响应结构。core/核心配置层:数据库、Redis、安全配置。
这样一个请求从进入到返回的完整链路是:小程序 → 路由层 → 业务层 → 数据模型层 → 数据库 → 反向返回。每一层职责单一,出了问题能快速定位。
7. 总结与下一步学习方向
到这里,一个基于 Python FastAPI + 微信小程序 + Vue3 的网约车项目核心流程已经全部打通了。我们完成了后端环境搭建、用户微信登录、用户位置上报、创建订单、订单列表与详情查询、管理后台订单查看等关键功能。同时,针对微信登录失败、跨域请求失败、数据库中文乱码等高频问题,也给出了排查思路。
项目还有很多可以继续完善的方向,按优先级排序如下:
- 司机端小程序:当前只实现了乘客端,司机端需要实现接单、开始行程、结束行程、收入明细等功能。
- 微信支付:接入微信支付 JSAPI 下单,完成乘客支付和司机结算的闭环。
- 实时消息推送:将订单轮询改为 WebSocket 或使用微信订阅消息,让司机接单更及时。
- 派单算法优化:在简单的“距离最近”基础上,增加司机评分、接单率、道路拥堵情况等权重。
- 数据可视化大屏:在管理后台增加订单量趋势图、热门叫车区域热力图等,提升项目的演示效果。
如果你正在准备毕业设计答辩,建议一定把“订单状态流转”和“微信登录流程”这两个点彻底吃透,因为这是评委老师最常追问的地方。不要只看代码能跑起来,还要能说清楚每一步做了什么、为什么这样做。
最后,如果本文对你有帮助,可以收藏备用。有任何问题也欢迎在评论区交流,后续我会抽时间更新司机端小程序和微信支付的实战教程,我们下一篇见。