
停车场管理系统表面看就是一张表的事谁的车什么时间进来停了多久该收多少钱。但真正动手做过的人都知道这里面的坑远比想象中多——车位状态不同步、费用跨天算错、出场并发时同一个车位被分配两次、Vue刷新一下页面直接404。这套基于Python Vue的停车场管理系统是典型的源码数据库配套文档完整项目后端Python负责接口与业务逻辑前端Vue负责管理界面把车辆入场、车位分配、出场计费、报表统计、用户权限这一整条流程真正跑通了。如果你正在做课程设计或毕业设计或者想给小型停车场搭一套内部管理系统这份项目很值得仔细看一遍。1. 需求拆解与技术选型为什么用Python Vue做这套管理系统1.1 业务需求清单这套系统真正要解决什么问题很多源码项目拿下来之后乍一看功能列表很长实际上只是把数据库表做了个增删改查。停车场管理的核心不是维护一张车辆表而是要把一套完整业务流程串起来。这套系统的基本需求如下用户登录与权限管理区分管理员和普通操作员管理员管车位、规则、用户配置操作员只负责出入场登记和收费。车位信息管理车位数量的初始配置、车位编号、区域划分、状态的实时更新空闲/占用/维修。车辆入场登记记录车辆入场时间、车牌号分配空闲车位更新车位状态。车辆出场结算根据入场时间和计费规则自动算费完成支付后释放车位。停车记录查询与财务报表按日期、车牌、区域筛选出入场记录统计停车时长和应收金额。收费规则配置免费时长、每小时的单价、单日封顶金额这些参数要能在后台灵活改。这套系统相当于把停车场前台收银员、车位管理员、财务统计员三个角色的工作压缩到了一个Web管理系统里。1.2 技术栈选择的依据Python后端 Vue前端的组合优势为什么选择Python而不是Java或者PHP原因很现实。Python的Flask、Django这类Web框架写业务接口就是定义路由处理函数操作ORM学习曲线平缓。停车场系统的业务逻辑量并不大用Python开发效率很高。对比一下Java Spring Boot虽然工程化强但对一台小停车场的管理系统来说体量过重配置多、编译慢、部署也要单独装JRE和Tomcat。而PHP老方案的维护成本也高很多老停车系统还在跑PHP5时代的代码改起来头疼。前端为什么用Vue这套系统的核心页面是数据看板和大量表格表单Vue配合Element UI这类组件库表格、弹窗、表单校验、日期选择器都是现成组件直接拼装比拿jQuery手写DOM要快得多。而且Vue组件化的写法把入场登记出场结算车位管理拆成独立组件修改一个模块不影响其他页面后续扩展起来也方便。整个项目在逻辑上分四层Vue页面发送HTTP请求到Python接口接口层负责参数校验和权限验证再往下调用ORM操作数据库数据库返回结果后接口层以JSON格式回给前端。遇到问题排查时也按这个链路逐层看不容易被绕晕。2. 数据库设计核心表与字段是如何支撑业务闭环的2.1 五张核心表和它们的分工数据库设计是这套系统里最值得琢磨的部分。表设计得好业务代码能省一半力气设计得不好后期会不停打补丁。这套系统的核心表可以拆成五张表名职责关键字段sys_user用户账号和角色username, password, roleparking_space车位的静态信息与动态状态space_no, area, space_type, statusvehicle车辆档案登记plate_number, owner_name, phone, car_typeparking_record车辆出入场记录核心流水表plate_number, space_id, entry_time, exit_time, duration_minutes, fee, statusfee_rule计费规则配置free_minutes, price_per_hour, cap_amountparking_record是整个业务的枢纽。入场时写入一条status为1的记录出场时更新这条记录的exit_time、duration_minutes和fee并把status改成2。财务报表直接查这张表而不需要把费用信息分散到多张表里去。下面给出核心表的初始化SQL可以直接在MySQL中执行CREATE DATABASE IF NOT EXISTS parking_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE parking_system; CREATE TABLE sys_user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录账号, password varchar(255) NOT NULL COMMENT 密码bcrypt哈希存储, nickname varchar(50) DEFAULT NULL, role varchar(20) NOT NULL DEFAULT staff COMMENT admin/staff, phone varchar(20) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT系统用户表; CREATE TABLE parking_space ( id int(11) NOT NULL AUTO_INCREMENT, space_no varchar(20) NOT NULL COMMENT 车位编号如A-01, area varchar(50) DEFAULT NULL COMMENT 区域如A区/B区, space_type varchar(20) DEFAULT normal COMMENT normal/disabled/newenergy, status tinyint(4) DEFAULT 0 COMMENT 0空闲 1占用 2维修, PRIMARY KEY (id), UNIQUE KEY uk_space_no (space_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT车位信息表; CREATE TABLE parking_record ( id int(11) NOT NULL AUTO_INCREMENT, plate_number varchar(20) NOT NULL COMMENT 车牌号, space_id int(11) DEFAULT NULL COMMENT 关联车位, entry_time datetime NOT NULL COMMENT 入场时间, exit_time datetime DEFAULT NULL COMMENT 出场时间, duration_minutes int(11) DEFAULT 0 COMMENT 停车时长分钟, fee decimal(10,2) DEFAULT 0.00 COMMENT 应收费用, status tinyint(4) DEFAULT 1 COMMENT 1在停 2已离场, operator_id int(11) DEFAULT NULL COMMENT 操作员ID, PRIMARY KEY (id), KEY idx_plate (plate_number), KEY idx_entry_time (entry_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT车辆出入场记录表; CREATE TABLE fee_rule ( id int(11) NOT NULL AUTO_INCREMENT, rule_name varchar(50) DEFAULT 默认规则, free_minutes int(11) DEFAULT 15 COMMENT 免费分钟数, price_per_hour decimal(10,2) DEFAULT 5.00 COMMENT 每小时单价, cap_amount decimal(10,2) DEFAULT 30.00 COMMENT 单日封顶金额, updated_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT计费规则表;2.2 关键字段背后的业务逻辑有几个字段看着不起眼实际上决定了业务能不能正确跑起来。第一个是parking_record里的status字段。很多人习惯用exit_time是否为空来判断车辆是否还在场这本身没错但加一个显式status字段之后查询当前在场车辆可以直接走索引过滤逻辑也更好理解尤其是遇到手动补录数据和异常数据需要修正的场景光靠时间字段判断会很混乱。第二个是duration_minutes字段。这是出场结算时计算的结果但它有实际价值——报表统计时要按小时分组、按车型统计停车时长分布直接查这个字段就可以不需要每次用exit_time减entry_time重新计算。数据冗余一点换查询效率值得。第三个是fee字段的类型。费用必须用DECIMAL(10,2)不要用FLOAT或DOUBLE。金额涉及零点几元的累加浮点类型在计算时会出现0.10.2这种精度问题累计到月末对账的时候差几分钱很难查。用DECIMAL虽然会稍微影响一点计算性能但对这种规模的项目完全无感。2.3 初始化数据账号、车位和计费规则数据库脚本不能只建表不灌数据否则程序起了也会出现一堆空数据导致的显示问题。我建议至少准备以下初始化数据-- 默认账号密码为 admin123 / staff123bcrypt哈希值在源码里生成后填入 INSERT INTO sys_user (username, password, nickname, role) VALUES (admin, $2b$12$xxxxxxxxxxxxxxxxxxxxx, 超级管理员, admin), (staff, $2b$12$yyyyyyyyyyyyyyyyyyyyy, 值班收费员, staff); -- 初始化车位A区20个B区20个包含2个无障碍车位 INSERT INTO parking_space (space_no, area, space_type, status) VALUES (A-01, A区, normal, 0), (A-02, A区, normal, 0), (B-01, B区, disabled, 0); -- 默认计费规则免费15分钟每小时5元每日封顶30元 INSERT INTO fee_rule (rule_name, free_minutes, price_per_hour, cap_amount) VALUES (默认规则, 15, 5.00, 30.00);这里注意一个细节密码不要直接在SQL脚本里写明文。虽然这套系统是本地演示但一旦部署到公网环境密码明文存数据库等于把系统大门敞开。正确做法是在Python里用bcrypt库生成哈希值然后把哈希串写进SQL。2.4 关于SQLite与MySQL的选型建议我在测试时用的是SQLite交付给实际环境时切换到了MySQL两者切换成本非常低因为ORM层屏蔽了大部分差异。如果你只是做毕业设计演示SQLite零配置、一个文件搞定拷贝就走如果要在真实停车场的机器上部署建议用MySQL 5.7及以上版本并发写入更稳定也能匹配mysqldump的备份方案。项目管理工具我习惯用Navicat或者开源的DBeaver连上之后直接跑SQL脚本和查看表数据比命令行敲SQL直观很多。3. Flask后端实现从登录到计费的核心流程拆解3.1 后端项目结构与统一返回格式这套系统后端采用Flask实现项目结构建议这样组织parking-server/ ├─ app.py # 应用入口与蓝图注册 ├─ config.py # 数据库连接、SECRET_KEY等配置 ├─ models.py # SQLAlchemy模型定义 ├─ api/ │ ├─ user_api.py # 登录、用户管理 │ ├─ space_api.py # 车位管理 │ ├─ parking_api.py # 车辆出入场 │ └─ report_api.py # 报表统计 ├─ utils/ │ ├─ auth.py # JWT鉴权装饰器 │ ├─ fee.py # 计费计算 │ └─ response.py # 统一返回JSON格式 └─ requirements.txt接口统一返回格式很重要前端不用每个接口单独处理特殊情况。我习惯用这种格式{ code: 200, msg: 操作成功, data: {} }code为200表示成功401表示未登录或Token失效403表示权限不足500表示服务器异常。前端axios拦截器只看code这一层逻辑统一。3.2 登录鉴权与角色权限JWT怎么用才不糊弄登录接口返回一个JWT Token前端把它存在localStorage里每次请求时放到Authorization头。JWT的好处是无状态后端不用在Session里存用户信息多台机器部署也方便。后端的鉴权装饰器代码大概是这样import jwt from functools import wraps from flask import request, g SECRET_KEY your-secret-key-here def login_required(f): wraps(f) def decorated(*args, **kwargs): auth request.headers.get(Authorization, ) token auth.replace(Bearer , ) try: payload jwt.decode(token, SECRET_KEY, algorithms[HS256]) g.user_id payload[user_id] g.role payload[role] except jwt.ExpiredSignatureError: return {code: 401, msg: 登录已过期}, 401 except jwt.InvalidTokenError: return {code: 401, msg: 无效Token}, 401 return f(*args, **kwargs) return decorated def admin_required(f): wraps(f) def decorated(*args, **kwargs): if getattr(g, role, None) ! admin: return {code: 403, msg: 无权限操作}, 403 return f(*args, **kwargs) return decorated在生成Token时把user_id和role都放进去前端按钮的显隐可以根据角色判断后端接口再做一层校验而不是只靠前端隐藏按钮来保护接口。实际操作中我见过很多人只在路由里加了登录校验没加角色校验普通用户直接拼一个接口地址就能改管理员配置这种漏洞很低级但很常见。3.3 入场流程分配车位时为什么要加行锁入场流程的逻辑并不复杂但这里有非常重要的并发问题必须提前处理。入场时后端做的事情是校验车牌号是否为空、车辆是否已在场校验选中的车位是否存在、状态是否空闲写入一条status为1的parking_record记录把车位status置为1。如果用两个收费员同时操作或者摄像机自动识别车牌后自动入场两个请求同时查到一个空闲车位就会把同一车位分配给两辆车。解决办法是在查询车位时加上数据库行锁from sqlalchemy import func from datetime import datetime def vehicle_entry(): data request.get_json() plate data.get(plate_number, ).strip().upper() space_id data.get(space_id) if not plate: return {code: 400, msg: 车牌号不能为空} # 同一车牌不能重复在场 existed ParkingRecord.query.filter_by( plate_numberplate, status1 ).first() if existed: return {code: 400, msg: 该车辆已在场内} # 加行锁防止并发分配同一个车位 space ParkingSpace.query.filter_by( idspace_id, status0 ).with_for_update().first() if not space: return {code: 400, msg: 车位不可用} record ParkingRecord( plate_numberplate, space_idspace.id, entry_timedatetime.now(), status1 ) space.status 1 db.session.add(record) db.session.commit() return {code: 200, msg: 入场成功, data: { record_id: record.id, entry_time: record.entry_time.strftime(%Y-%m-%d %H:%M:%S) }}这个场景和酒店前台开房的逻辑很像两拨人同时到前台前台系统必须先从数据库里锁住房间否则同一间房就可能开给两拨客人。with_for_update就是用来干这个的它在查询结果上加了行级排他锁其他事务必须等当前事务提交之后才能操作同一行数据。3.4 出场流程与计费逻辑时长计算里最容易出错的三个点出场结算的逻辑核心是计算费用代码实现如下import math from datetime import datetime def calc_fee(entry_time, exit_time, rule): minutes (exit_time - entry_time).total_seconds() / 60 if minutes 0: return 0 # 免费时长判断 billable_minutes minutes - rule.free_minutes if billable_minutes 0: return 0.00 # 不足1小时按1小时计费 hours math.ceil(billable_minutes / 60) fee hours * float(rule.price_per_hour) # 单日封顶 if rule.cap_amount and fee float(rule.cap_amount): fee float(rule.cap_amount) return round(fee, 2) def vehicle_exit(): data request.get_json() record_id data.get(record_id) record ParkingRecord.query.filter_by( idrecord_id, status1 ).with_for_update().first() if not record: return {code: 400, msg: 记录不存在或已结算} rule FeeRule.query.first() record.exit_time datetime.now() record.duration_minutes int( (record.exit_time - record.entry_time).total_seconds() // 60 ) record.fee calc_fee(record.entry_time, record.exit_time, rule) record.status 2 space ParkingSpace.query.get(record.space_id) if space: space.status 0 db.session.commit() return {code: 200, msg: 结算完成, data: { fee: str(record.fee), duration_minutes: record.duration_minutes }}时长计算里有三个容易出错的点实际测试时必须覆盖第一免费时长的边界。免费15分钟那么入场后恰好停15分钟整到底免不免费代码里用billable_minutes 0判断等于15分钟时billable为0返回免费符合免费15分钟的字面规则。如果把判断写成minutes free_minutes也没问题但语义上要前后统一。第二不足一小时按一小时算。停车62分钟如果按小时单价*1就对了如果直接按分钟算会算成1.03小时单价乘以小时数后金额就不对。所以要先用math.ceil向上取整到小时。第三跨天封顶逻辑。车辆停了3天单日封顶30元那么3天应收90元而不是直接整体封顶30元。上面这个简单实现没有处理多天×每日封顶的情况会让系统少收钱。实际项目中我会再加上按天数累计的逻辑先算出总小时数拆成完整天数和剩余小时数费用 天数×封顶价 min(剩余小时×单价, 封顶价)。这里强烈建议在文档里写明这个边界规则很多测试数据不覆盖跨天场景导致上线后才发现。4. Vue前端实现管理页面如何从0到1搭起来4.1 路由设计菜单、权限和刷新404Vue前端部分路由结构是整个项目的骨架。这套系统的路由建议拆成登录页和主框架两层const routes [ { path: /login, component: Login }, { path: /, component: Layout, children: [ { path: dashboard, component: Dashboard, meta: { title: 数据看板, roles: [admin, staff] } }, { path: parking/entry, component: ParkingEntry, meta: { title: 车辆入场, roles: [admin, staff] } }, { path: parking/exit, component: ParkingExit, meta: { title: 车辆出场, roles: [admin, staff] } }, { path: space, component: SpaceManage, meta: { title: 车位管理, roles: [admin] } }, { path: finance, component: FinanceReport, meta: { title: 财务报表, roles: [admin] } }, { path: user, component: UserManage, meta: { title: 用户管理, roles: [admin] } } ] } ]我建议在路由的meta里定义roles数组然后在路由守卫里校验当前用户角色router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next(/login) return } const role localStorage.getItem(role) if (to.meta.roles !to.meta.roles.includes(role)) { next(/dashboard) return } next() })这里有个细节Vue路由有两种模式hash模式和history模式。开发时用history模式没问题但打包上线后如果Nginx没有配置try_files刷新一个子页面比如直接访问/space就会出现404。最简单的规避方案是开发环境下用hash模式地址栏会多一个#号但不会404生产环境配Nginx的try_files $uri $uri/ /index.html。这个坑我在第五节展开讲。4.2 axios封装请求拦截器与401处理前端和后端的通信我习惯封装一个统一的axios实例。拦截器的核心逻辑是请求发出前把Token塞进header响应返回后统一处理业务码。import axios from axios import router from /router const service axios.create({ baseURL: process.env.VUE_APP_API_URL || http://127.0.0.1:5000/api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) service.interceptors.response.use( res { if (res.data.code 401) { localStorage.removeItem(token) localStorage.removeItem(role) router.push(/login) return Promise.reject(new Error(未登录或登录过期)) } if (res.data.code ! 200) { return Promise.reject(new Error(res.data.msg)) } return res.data }, err { if (err.response err.response.status 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(err) } ) export default service这样一来每个页面就不用重复写Token和401的逻辑了。在组件里直接这样调用import service from /api/request service.post(/parking/entry, { plate_number: this.plate, space_id: this.spaceId }) .then(res { // res.data 已经是后端返回的业务数据 })4.3 核心页面拆解入场登记、车位状态、统计看板入场登记页面是这个系统最常被操作的页面需要做到以下几点一个车牌号输入框带校验自动过滤空格、转大写、一个可用车位下拉列表选项只包含status为0的车位、入场成功后的弹窗提示。这里我建议车牌号输入加一个简单的正则校验例如普通蓝牌 C[A-Z0-9]{5} 这类规则避免格式错误的数据直接进入数据库。车位状态页面是做给管理员看的用一张表格展示所有车位编号、区域、类型、状态。状态用Tag标签显示绿色是空闲、红色是占用、灰色是维修。页面还要提供手动更改状态的操作比如某个车位因为设备损坏要设置为维修状态这个操作不应该和入场出场流程混淆它是一个独立的管理操作。统计看板页面比较出效果但是实现上不难。调一个报表接口后端返回今日入场车辆数、今日应收金额、当前场内车辆数、近7天收支趋势这四组数据。前端用ECharts画柱状图和折线图再配几个统计卡片。如果想让页面更直观可以做一个模拟车位地图用网格方块展示车位占用情况鼠标悬停时显示车位号和车牌号。4.4 视频监控画面接入在Vue里播放HLS流m3u8停车场管理系统做到后期很多需求方会提出在页面上直接看摄像头画面。停车场出入口的摄像头如果是局域网IP摄像头常见的视频流协议有RTSP和HLS。RTSP不能直接在浏览器播放需要后端转流而HLS流通常是.m3u8结尾的地址。Vue里播放m3u8可以这样做安装video.js和videojs-contrib-hls然后封装一个视频组件npm install video.js7 videojs-contrib-hls6template div classvideo-panel video refvideoEl classvideo-js vjs-default-skin controls playsinline /video /div /template script import videojs from video.js import video.js/dist/video-js.css import videojs-contrib-hls export default { props: { src: { type: String, required: true } // 形如 http://ip:port/live/stream.m3u8 }, mounted() { this.player videojs(this.$refs.videoEl, { sources: [{ src: this.src, type: application/x-mpegURL }], autoplay: true, fluid: true }) }, beforeDestroy() { if (this.player) { this.player.dispose() } } } /script这里要提醒一句videojs-contrib-hls插件虽然免安装、直接用但它是基于hls.js封装的对HLS流的兼容性没有问题。如果摄像头只支持RTSP就需要在后端做一次流转码比如用FFmpeg把RTSP拉流转成HLS推流前端照样用m3u8地址播放。这一步在文档里说清楚能帮使用者少走很多弯路。5. 源码落地本地环境配置、数据库初始化与联调5.1 环境版本对照与准备步骤很多用户拿到源码后第一步就卡在环境上。这套系统的环境要求不算高但版本必须对得上。软件建议版本说明Python3.8及以上代码中使用类型注解和f-stringPython 3.6以下跑不了Node.js14及以上Vue项目构建依赖npm版本随Node一起装MySQL5.7及以上如果不装数据库可以用SQLite浏览器Chrome / Edge前端调试基本固定在这两个浏览器后端的安装启动命令我习惯在Windows下用虚拟环境cd parking-server python -m venv venv venv\Scripts\activate pip install -r requirements.txt python app.pyLinux环境下把激活命令换成source venv/bin/activate其余一致。前端的安装启动cd parking-web npm install npm run serve如果npm install速度太慢可以设置淘宝镜像源但要注意这只是拉包用的镜像不影响项目本身的功能。以上两步都成功后打开浏览器访问Vue启动页面上的地址即可。5.2 后端启动与数据库配置对齐拿到源码后第一件事不是急着启动程序而是先把配置文件里的数据库连接信息改对。常见项目的config.py里会有一段这样的配置DB_HOST 127.0.0.1 DB_PORT 3306 DB_USER root DB_PASSWORD your_password DB_NAME parking_system你需要做的就是把DB_PASSWORD改成你本机MySQL的实际密码。注意MySQL 8.x默认认证插件是caching_sha2_password和PyMySQL老版本存在兼容问题如果连接报Authentication plugin caching_sha2_password cannot be loaded要么升级pymysql到1.0以上要么把MySQL用户改成mysql_native_password认证方式。配置文件对齐之后执行sql脚本mysql -u root -p parking.sql或者直接在Navicat、DBeaver里打开parking.sql文件选中全部执行。执行成功后进入MySQL确认一下表是否存在SHOW TABLES;这里有个很实用的习惯数据库脚本要设计成可重复执行也就是脚本开头加DROP TABLE IF EXISTS这样重新初始化时不会报错。如果脚本设计得不好第二次执行时各种Table already exists会浪费不少时间。5.3 前端启动与跨域问题处理前端和后端分开开发时会遇到经典的跨域问题。Vue页面跑在8080端口后端跑在5000端口浏览器会拦截跨域的AJAX。解决方式有两种我推荐在生产环境和本地都使用后端CORS配置。Flask安装flask-cors之后只需要注册一次from flask_cors import CORS CORS(app, resources{r/api/*: {origins: *}})开发环境如果不想在后端开CORS也可以用Vue的代理配置在vue.config.js里设置module.exports { devServer: { proxy: { /api: { target: http://127.0.0.1:5000, changeOrigin: true } } } }这样前端请求/api/parking/entry会被代理到http://127.0.0.1:5000/api/parking/entry浏览器层面上是同源请求不会出现跨域问题。5.4 生产环境打包与部署要点本地跑通之后如果要把系统部署到一台服务器上流程是cd parking-web npm run build构建产物是dist目录里面是纯静态文件。用Nginx托管dist目录同时做反向代理把/api开头的请求转发给Python后端。一个典型的Nginx配置片段server { listen 80; server_name your-domain.com; root /var/www/parking-web/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:5000/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 解决history模式刷新404 location / { try_files $uri $uri/ /index.html; } }后端的启动方式建议用gunicorn代替直接python app.pygunicorn -w 4 -b 127.0.0.1:5000 app:app这里我不建议在生产环境用Flask自带的开发服务器启动并发能力差而且默认只适合调试。用gunicorn加上4个worker对小停车场的访问量来说绰绰有余。6. 实际跑过之后踩坑记录与改进方向6.1 时间与时区问题导致费用错乱这是我实际测试时遇到的最典型问题。数据库里的entry_time和exit_time如果直接用datetime.now()写入存的是服务器本地时间但如果系统部署在云服务器上服务器默认时区可能是UTC而用户期望的是北京时间。两个时间一个按UTC存、一个按本地时间算跨天的时候费用就会差出好几十。解决方式很简单统一成一个时区来存。我习惯在入场时就明确用带时区的时间生成from datetime import datetime, timezone, timedelta CN_TZ timezone(timedelta(hours8)) def now_cn(): return datetime.now(CN_TZ).replace(tzinfoNone)存入数据库前把tzinfo去掉存成naive datetime显示和计算都以北京时间为准。这里最重要的是文档里要把时区约定写清楚否则换一台服务器部署这个坑就会再炸一次。6.2 并发入场导致车位超卖这个问题在3.3节已经写了对应的代码我再说一遍过程。数据库事务开启后代码执行顺序是先查车位状态再写入记录再更新车位状态。如果不加锁两个请求同时查到status0的空闲车位然后各自插入记录最终一个车位被两辆车占用。解决办法是查询时加with_for_update()把行锁加在车位这一行第二个请求必须等第一个请求提交事务后才能继续查询。如果你用的是SQLite而不是MySQL要注意SQLite对行锁的支持和MySQL不一样SQLite的写入锁是数据库级别的并发写入高时会出现database is locked错误。这也是我建议在真实业务环境用MySQL的原因之一。6.3 前端刷新页面404与后端路由冲突开发时用Vue Router的history模式刷新某个子页面出现404这是很多人第一次打包部署时必踩的坑。原因很简单刷新时浏览器请求的是服务器上的物理路径比如/space但服务器在dist目录下找不到space这个文件于是返回404。Nginx配置里加上try_files $uri $uri/ /index.html让所有请求都回退到index.html由前端路由自己解析。如果你是开发模式下遇到这个问题最简单的做法是先用hash模式刷新不会404等部署时再切换history模式并配置Nginx。还有一个类似的问题是刷新Vue项目时Nginx返回200但页面白屏这通常是静态资源路径写死了相对路径而部署时项目放在子目录下。解决方式是打包时把publicPath设置为相对路径或具体域名路径module.exports { publicPath: ./ }6.4 可以继续完善的方向这套系统已经满足基础管理需求但我实际操作后觉得还有几个方向值得继续改属于性价比很高的扩展车牌识别集成在入场接口加一个车牌号自动识别服务摄像头抓到车牌后调用识别模型自动填充入场页面。这个功能现在很成熟接入成本不高。会员与月卡管理增加月卡、季卡用户出场时直接按套餐核销而不是按临时费率计费。这个需要新增一张member表和一个会员封顶判断逻辑。数据导出把报表接口的结果导出成Excel文件用Python的openpyxl库生成就行财务对账刚需。操作日志记录每次入场、出场、改配置的操作人、操作时间和具体变更内容。这个功能看似不起眼等想排查谁改了收费规则的时候就很有用。最后补充一点实际经验拿到这套停车场管理系统源码后如果一次性启动失败绝大多数情况不是代码问题而是环境的配置文件没对齐。我建议按这个顺序排查先确认MySQL密码是否填对再确认parking.sql是否执行成功最后看Flask启动日志里有没有报错信息。前端页面打开后按F12看Console和Network请求状态码是401就是Token没带上是404就是接口路径写错是跨域就是CORS或代理配置没生效。把这些常见问题写进配套文档里使用者会轻松很多。另外一个很实用的经验是正式交付前一定要准备一套干净的测试数据覆盖免费时长边界、整点计费、跨天停放、并发入场这四个场景。把每种情况都跑一遍系统才能真正拿去用而不是只在演示页面上看着好看。