今年帮几位师弟改毕设的时候发现一个现象:校园里电动车已经成了刚需,但“买车怕丢、骑车怕没电、充电排长队”的问题几乎每个学校都存在,于是不少人都把“校园电动车短租平台”当成选题方向。用Flask还是Django、前端选什么框架、怎么把租车-计费-还车整条链路跑通,这些问题反反复复被问到。今天干脆把我自己做的一个Flask + Vue校园电动车短租平台的完整开发过程拆开来讲,从技术选型、数据库设计到后端API、前端页面,再到联调阶段踩过的坑,一次性说清楚。
这个项目面向的读者很明确:正在做毕业设计的学生、想练手前后端分离全栈流程的开发者,以及学校里真实想落地一个分时租赁小系统的同学。后端我用的是Python + Flask,前端是Vue,配合PyCharm开发,数据库用MySQL(本地也可以用SQLite过渡)。如果你正卡在“不知道从哪下手”“模块怎么划分”“跨域报错怎么解决”这类问题上,这篇文章应该能给你一条直接可以照着走的路径。
1. 为什么是Flask + Vue,而不是Django全家桶
1.1 业务规模决定技术骨架
很多人在选题初期都会纠结:既然标题里写着“python+flask”,为什么还要看到Django?实际上这个项目用Flask还是Django都能做,区别在于你的业务复杂度和个人对框架的掌控力。
校园电动车短租平台的核心业务是:用户注册登录、车辆浏览、下单租车、计费结算、还车、订单查询和管理员后台。这个规模放在Flask里,用Blueprint划分模块后每个模块只有几百行代码,非常清晰;如果上Django,ORM、Admin、迁移工具这些重量级武器确实方便,但也会让你把大量时间花在“学习框架约定”而不是“理解业务逻辑”上。
我的建议是:如果这是你的毕设或者全栈练手项目,Flask的轻量反而是一个优势——你能更清楚地看到请求进来之后,路由怎么分发、视图怎么处理、ORM怎么操作数据库,整个链路没有魔法。如果以后需要快速上线一个带复杂后台权限管理的商业项目,Django当然更合适,但那是另一个故事了。
1.2 Flask和Django的实感对比
用表格直接说结论:
| 对比维度 | Flask | Django |
|---|---|---|
| 上手门槛 | 低,一个文件能跑通 | 中高,概念多、约定多 |
| 自由度 | 高,想怎么组织就怎么组织 | 受框架约束,但结构规范 |
| ORM | 需单独使用SQLAlchemy | 自带ORM,迁移方便 |
| Admin后台 | 需自己写或用第三方库 | 自带admin,开箱即用 |
| 适合场景 | API服务、中小型系统、教学项目 | 内容管理、大型业务系统 |
| 本项目中的角色 | 后端API主力 | 仅在对比和设计中参考 |
实际开发时,我用Flask的Blueprint划分了auth、vehicle、order、admin四个模块,配合Flask-SQLAlchemy操作数据库,用Flask-JWT-Extended做登录态,前后端完全分离。Flask的灵活在这种中小型项目中体现得很明显:我完全按照自己的业务习惯组织目录,不需要被框架自带结构绑架。
1.3 PyCharm下的项目初始化与虚拟环境
开发环境我用的是PyCharm Professional,创建项目时选择虚拟环境,Python版本用的3.10。初始化依赖只需要五个核心库,其他用到再加:
Flask==3.0.3 Flask-SQLAlchemy==3.1.1 Flask-JWT-Extended==4.6.0 Flask-CORS==4.0.1 PyMySQL==1.1.0项目目录结构我是这样划分的:
electric_scooter_backend/ ├── app.py # 应用入口,注册蓝图 ├── config.py # 配置类:数据库、JWT密钥 ├── models/ # SQLAlchemy模型 │ ├── __init__.py │ ├── user.py │ ├── vehicle.py │ └── order.py ├── api/ # 蓝图模块 │ ├── __init__.py │ ├── auth.py │ ├── vehicle.py │ ├── order.py │ └── admin.py ├── utils/ # 工具函数:统一响应、计费等 │ ├── __init__.py │ ├── response.py │ └── billing.py └── requirements.txt配置类单独放在config.py里,方便本地和部署环境切换。核心配置就三块:数据库连接地址、JWT密钥、前后端跨域白名单。密钥在本地可以用secrets.token_hex(32)生成一串,提交代码前一定不要暴露到公共仓库,这点单独提一句是因为我见过太多demo把密钥直接明文写到配置里。
2. 校园电动车短租的业务建模:先理清需求再动手
2.1 三类角色和核心流程
动手写代码之前,我习惯把业务规则先列成文字。这个项目里有三类角色:学生用户、管理员、维护人员(可以合到管理员角色里)。用户的完整流程是:注册登录 → 浏览车辆 → 选择车辆发起租车 → 模拟支付押金和预扣费用 → 开始计时 → 还车结算 → 查看订单。
管理员的核心操作是:管理车辆信息(上下架、修改电量)、查看所有订单、处理异常还车(比如用户上报故障)。开发时我用一个role字段区分用户和管理员,没有单独建用户表。
租车流程里的关键规则是:同一辆车同一时刻只能被一个用户租用;租车订单必须有“使用中”状态,还车后才能生成费用;费用计算按分钟计费,部分场景可以叠加里程系数。这些规则不提前定清楚,后端的接口设计就一定反复返工。
2.2 数据库表的划分与设计思路
数据库我设计了五张核心表:用户表、车辆表、订单表、车辆维护记录表、优惠券表。对毕设规模来说足够,没有过度设计。
用户表字段:id、username、password_hash、real_name、student_id、phone、balance(余额)、role、create_time。
车辆表字段:id、plate_number(车牌号)、brand、battery_level(电量百分比)、latitude、longitude、status(0空闲 1使用中 2维护中 3下线)、hourly_rate(时租价)、deposit(押金)、image_url。
订单表字段:id、order_no(订单号)、user_id、vehicle_id、start_time、end_time、start_mileage、end_mileage、total_amount、status(0待支付 1使用中 2已完成 3已取消 4异常)、create_time。
这里有三个细节容易忽略:第一,订单号和主键id分开,订单号用于前端展示和后期对账,用时间戳+随机数生成;第二,车辆表里必须存经纬度,因为前端地图找车功能需要直接读取车辆位置来打点;第三,里程和时长都要记录,计费策略变化时历史订单还能回溯。
2.3 计费规则的设计
计费是这个项目的核心展示点。我采用的规则是:起步价 + 时长费 + 里程费叠加,起步价定为2元包含前30分钟,超出部分每分钟0.1元,里程每公里0.3元。这个规则在utils/billing.py里实现为一个纯函数,输入租用时长和骑行里程,输出费用:
# utils/billing.py def calculate_amount(duration_minutes: int, mileage_km: float) -> float: """ 计费规则: - 起步价 2 元,包含前 30 分钟 - 超出时间按 0.1 元/分钟计 - 里程按 0.3 元/公里计 """ base_price = 2.0 if duration_minutes <= 30: duration_fee = 0.0 else: duration_fee = (duration_minutes - 30) * 0.1 mileage_fee = mileage_km * 0.3 total = round(base_price + duration_fee + mileage_fee, 2) return total把计费逻辑抽成纯函数而不是散落在视图函数里,是我这次做的比较正确的一个决定。后期调整计费规则只改这个文件,所有引入它的接口自动生效。测试的时候直接写个循环用例,把边界情况(29分钟、30分钟、31分钟、50公里)全跑一遍,不用起服务。
3. Flask后端API设计:从目录结构到接口令牌
3.1 用Blueprint划分模块
后端不把所有路由写在一个文件里。Blueprint是Flask里组织模块的标准方式,我的四个模块这样注册:
# app.py from flask import Flask from flask_cors import CORS from flask_sqlalchemy import SQLAlchemy from flask_jwt_extended import JWTManager from api.auth import auth_bp from api.vehicle import vehicle_bp from api.order import order_bp from api.admin import admin_bp db = SQLAlchemy() jwt = JWTManager() def create_app(): app = Flask(__name__) app.config.from_object('config.Config') db.init_app(app) jwt.init_app(app) CORS(app, resources={r"/*": {"origins": "http://localhost:5173"}}) app.register_blueprint(auth_bp, url_prefix='/api/auth') app.register_blueprint(vehicle_bp, url_prefix='/api/vehicle') app.register_blueprint(order_bp, url_prefix='/api/order') app.register_blueprint(admin_bp, url_prefix='/api/admin') return appcreate_app工厂模式在这里的主要好处是方便测试——测试环境可以直接创建一个带不同配置的app实例,不用在环境变量和启动命令之间反复切换。
3.2 JWT登录态与统一响应格式
前端每次请求都要知道当前用户是谁,我用Flask-JWT-Extended签发token,登录成功后在response里返回access_token,前端把它存在localStorage,后续请求在header里带上Authorization: Bearer <token>。需要当前用户信息的接口,直接通过get_jwt_identity()拿到用户id。
为了保证前后端对接不出歧义,我统一了所有接口的返回格式:
# utils/response.py def ok(data=None, message="success"): return {"code": 0, "message": message, "data": data} def fail(message="error", code=1, data=None): return {"code": 1, "message": message, "data": data}前端axios只需要拦截code !== 0的情况统一弹错误提示,不用每个接口单独处理异常分支。实际开发中统一响应格式的价值远比想象中大,尤其是你一个人同时写前后端的时候,能省下大量对接口的时间。
3.3 车辆与订单API的核心逻辑
车辆模块中最核心的接口是获取车辆列表和获取车辆详情。列表接口支持按状态筛选,前端地图页只需要空闲车辆。订单模块是重点,我拆成五个接口:
- 创建订单(租车):检查车辆状态 → 检查用户余额 → 创建订单 → 锁定车辆
- 模拟支付(支付押金+预扣费):更新订单状态为使用中、生成支付记录
- 还车:计算费用 → 扣费 → 更新车辆状态
- 取消订单:仅限使用前取消,超过时间不可取消
- 订单列表:区分用户视角和管理员视角
创建订单的代码重点在于车辆状态的并发检查:
# api/order.py @order_bp.route('/create', methods=['POST']) @jwt_required() def create_order(): user_id = get_jwt_identity() data = request.get_json() vehicle_id = data.get('vehicle_id') vehicle = Vehicle.query.filter_by(id=vehicle_id, status=0).first() if not vehicle: return fail("该车辆当前不可租用") user = User.query.get(user_id) if user.balance < vehicle.deposit: return fail("余额不足,请先充值") order = Order( order_no=generate_order_no(), user_id=user_id, vehicle_id=vehicle.id, start_time=datetime.now(), start_mileage=vehicle.current_mileage, status=0 ) db.session.add(order) vehicle.status = 1 # 锁定车辆 db.session.commit() return ok({"order_id": order.id, "order_no": order.order_no})注意这个操作不是一个严格意义上的事务性原子操作,高并发下两个用户同时抢同一辆车可能都通过了状态检查。我在本地开发时靠vehicle.status = 1加数据库唯一约束兜底,后续可以引入行级锁或者用select_for_update()彻底解决。
3.4 SQLAlchemy模型定义要点
模型定义层面,我踩过的坑是字段类型精度。金额相关的字段一定不要用Float,用Numeric(10, 2),避免出现0.1 + 0.2不等于0.3的情况。订单号字段直接设置unique约束。车辆表的经纬度用Numeric(10, 6)存,精度足够定位。
# models/vehicle.py class Vehicle(db.Model): __tablename__ = 'vehicle' id = db.Column(db.Integer, primary_key=True) plate_number = db.Column(db.String(20), unique=True, nullable=False) battery_level = db.Column(db.Integer, default=100) latitude = db.Column(db.Numeric(10, 6)) longitude = db.Column(db.Numeric(10, 6)) status = db.Column(db.Integer, default=0) hourly_rate = db.Column(db.Numeric(10, 2), default=10.00) deposit = db.Column(db.Numeric(10, 2), default=50.00)用PyCharm的Database面板连接MySQL后,可以直接可视化看表结构和测SQL,开发体验比命令行好不少。表结构改动频繁的初期,可以先不建外键约束,靠应用层保证数据一致性,后期稳定了再补约束。
4. Vue前端实战:从地图找车到订单结算
4.1 前端项目初始化与UI组件库选择
前端我用Vue 3 + Vite搭建,UI组件库用Element Plus,HTTP库用axios。创建项目直接用官方脚手架:
npm create vue@latest npm install element-plus axios vue-router pinia组件库我选Element Plus而不是Vant,原因是这个项目既有移动端小程序风格的页面(找车、租车),也有后台管理端的表格页面(订单列表、车辆管理),Element Plus的表格和表单组件能覆盖两种场景。如果只做移动端H5,Vant会更轻。
前端目录结构按功能拆成四个view:LoginView、MapView(地图找车)、OrderView(订单流程)、AdminView(后台管理)。路由用createRouter创建,配上登录守卫,未登录的用户统一跳转到登录页。
4.2 地图找车与列表模式的实现
地图找车是项目的门面功能。我接入了高德地图JS API,在MapView中初始化地图后,从后端拉取空闲车辆列表,用AMap.Marker在经纬度位置打点。每个Marker绑定点击事件,弹窗里展示车辆信息和“立即租用”按钮。
这里有一个实际操作上的注意点:高德地图在本地开发用file://方式打开会报安全错误,必须通过本地服务访问。开发时直接npm run dev,生产环境build后放到后端静态目录或用Nginx托管。
列表模式作为地图模式的补充,用表格展示车辆名称、电量、租金、距离,方便按电量排序筛选。我在页面上加了一个电量显示条,电量低于20%的车辆直接标注“电量低”并置灰,避免用户租到骑不了的车。
4.3 订单流程的状态拆解
订单流程在前端可以分为四个状态视图:待支付(创建订单后)、使用中(支付成功后)、待结算(点击还车后)、已完成(费用结算完显示详情)。前端用一个orderStatus字段控制页面显示,不同状态下展示不同按钮组。
https://api.xxx.com/upload 这个接口是模拟上传还车照片的,实际操作中我把它设计成:用户点击还车,先强制上传一张车辆照片(用于确认车辆无损),然后调用还车接口。后端接图片上传后可以直接存本地,把文件路径存到订单表里。前端代码如下:
const returnScooter = async () => { if (!photoUrl.value) { ElMessage.warning('请先拍摄或上传还车照片') return } const res = await returnOrder({ order_id: orderId.value, end_mileage: endMileage.value, photo_url: photoUrl.value }) if (res.code === 0) { orderStatus.value = 'completed' totalAmount.value = res.data.total_amount } }这个设计逻辑是:还车不是简单点一下按钮,必须确认车辆物理归还和实际里程数,业务上合理,答辩演示时也更有话讲。
4.4 axios封装与路由守卫
axios封装我单独放在utils/request.js,拦截器里做两件事:取token加到请求头;统一处理响应码。401状态码跳转登录页。这样每个页面调接口时不用重复写token逻辑。
路由守卫用的是Vue Router的beforeEach:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path !== '/login' && !token) { next('/login') } else { next() } })这个守卫逻辑要放在项目初期就写好,不然后续每写一个页面都要重复判断登录态,很容易漏。我在实际开发中就因为一开始没写守卫,导致某些接口在没有token时直接返回401,排查了半夜才发现是前端没跳转。
5. 联调阶段踩过的坑:跨域、时间、并发与Mock支付
5.1 跨域问题与CORS配置
前后端分离开发最常见的第一个坑就是跨域。前端跑在5173端口,后端跑在5000端口,直接请求必然报CORS错。我在后端用Flask-CORS解决:
# app.py 中 CORS(app, resources={r"/api/*": {"origins": "http://localhost:5173"}})注意resources要写在/api/*前缀上,不是全放行*。全放行开发时省事,但上线有一点安全隐患,关闭后反而能逼着你理清哪些域名可以访问你的API。
5.2 时间字段的时区问题
Flask默认存的是UTC时间,前端拿到后直接用会显示比北京时间慢8小时。我在创建订单时手动指定datetime.now()就会用服务器本地时间,但为了统一还是建议在config里配置:
APP_TIMEZONE = 'Asia/Shanghai'同时前端在展示时间时用dayjs做格式化,不要直接用字符串。我测试时发现订单列表页的时间显示错乱,排查思路就是先看接口返回的原始时间字符串,再确认前端格式化时有没有指定时区,最后发现问题出在数据库连接参数里没加时间设置。
5.3 并发租车的锁车逻辑
两个用户同时点击租同一辆空闲车,如果没有并发控制,两个订单都能创建成功,车辆状态被覆盖。我的处理方案是:在车辆表中增加一个version字段,更新时用乐观锁:
updated = Vehicle.query.filter_by(id=vehicle_id, status=0, version=version) \ .update({'status': 1, 'version': version + 1}) if updated == 0: return fail("车辆已被其他用户租用")这是我在测试接口时用两个浏览器标签同时提交请求抓到的bug,当时很震惊,页面看起来一切正常,查数据库才发现记录重复。这个坑写出来是想提醒大家,只要涉及状态变更,必须考虑并发,不要觉得本地测试没有并发就跳过。
5.4 Mock支付与演示环境
真实对接第三方支付接口需要商户资质,而且需要企业认证,学生项目很难申请通过。我采用了Mock支付方案:前端调用“模拟支付”接口,后端直接生成一条支付流水,把订单状态改为已支付,支付结果页面提示“演示环境,未发生真实扣款”。
Mock支付模块设计为独立接口,这样以后真的接入微信或支付宝,只要替换支付实现,订单主流程不用动。
6. 验收清单与经验沉淀
6.1 上线前必须跑通的验收路径
项目做完之后,我给自己列了一张验收清单,按顺序逐项检查:
- 注册 → 登录 → 获取token → 获取用户信息
- 地图加载 → 获取空闲车辆列表 → 车辆打点 → 点击弹窗
- 创建订单 → 检查余额 → Mock支付 → 车辆锁定
- 开始骑行 → 结束骑行 → 上传还车照片 → 费用计算
- 用户订单列表 → 管理员订单列表 → 车辆上下架
- 边界测试:重复租车、余额不足、电量不足
每一项都是一个完整的用户故事,按这个顺序跑一遍,基本能覆盖主流程。不要跳着测,不然经常是“功能都能启动”但“关键路径是堵的”。
6.2 答辩演示时值得讲的三个技术点
如果这个项目是毕设,答辩时不要从头到尾念PPT,重点讲三件事:一是计费模块的纯函数设计与边界测试,这体现你的代码抽象能力;二是并发锁车的乐观锁方案,这体现你思考过真实业务问题;三是Mock支付的分层设计,这体现你的架构意识。
演示时提前准备一辆水温正常的车:数据库里造好3-5辆车的数据、一个余额充足的测试账号、几笔历史订单。不要让评委看着你现场注册账号。
6.3 我在实际开发中的几点体会
把整个项目从零跑通之后,我最大的感觉是:一个校园电动车短租平台的难点不在某个技术点,而在状态流转。车辆状态、订单状态、支付状态,任何一个状态没走通,整条链路就断。先花一天时间把状态图画清楚,写代码的效率会高出一大截。
另外我强烈建议开发过程中多打调试日志,后端每个核心接口在关键节点加一行logger.info,比如“创建订单成功,订单号xxx,车辆id为xxx”。联调时出了问题,通过日志能快速定位是前端没调接口、后端逻辑错了还是数据库没落库。
还有一个小技巧:数据库种子脚本一定不要省。启动项目之后手动一条条插数据太浪费时间。准备一个scripts/seed.py,执行后自动创建5辆车、2个用户、3笔订单,每次重置数据库后一键恢复演示数据,省下来的时间足够多做两个功能。