☰
Python停车场管理系统源码+数据库:课程设计完整项目与避坑指南
2026/10/15 1:44:30 网站建设 项目流程

简介:这份资源是面向高校学生与Python初学者的一套停车场管理系统课程设计完整源码包,采用Python编写后端逻辑,结合关系型数据库存储车辆进出、车位状态与收费记录,并借助Web框架与RESTful接口实现前后端交互,适合用作课程设计参考、毕业设计原型或Web开发练手项目。压缩包共46个文件,约1.48MB,以28个py源码文件为核心,辅以4个html模板、4个xml配置、1个sql建库脚本及css样式、md说明文档等,涵盖配置、模型、控制器、服务、视图与工具模块,目录划分清晰,便于按功能模块阅读与二次开发。目前已有4250人学习下载,热度较高。读者可从中获取完整的项目结构、数据库表设计、车位分配与计费算法思路、异常处理与权限管理示例,以及RSA加解密、日志封装等实用工具代码,快速理解一个中小型Web系统的落地流程。

1. 停车场管理系统到底在管什么:从一张入场小票说起

很多同学做课程设计时,第一反应是"停车场管理系统不就是记录车牌、算算钱吗",真动手才发现,一辆车从进场到出场,中间要处理的状态比想象中多得多。基于 Python 的停车场管理系统源码加数据库这套东西,本质上是在模拟一个真实车场的核心业务闭环:车辆入场登记、车位分配、计时计费、出场结算、历史记录留存。它解决的不是"能不能存数据",而是"状态怎么流转、费用怎么算准、并发怎么不崩"。

这套方案适合两类人:一类是正在做课程设计、需要一份能跑通、能讲清楚、能改得动的完整项目;另一类是刚学完 Python 基础语法和数据库入门,想找一个业务逻辑不太复杂但足够完整的练手项目。它不适合指望直接拿去做商业停车场运营的人,因为真实车场还要对接道闸硬件、车牌识别摄像头、支付通道,这些在课程设计里通常用模拟数据代替。但作为理解"业务系统怎么从零搭起来"的样本,它的结构足够典型,改起来也不至于无从下手。

2. 先把数据模型定下来:三张表撑起整个车场

2.1 为什么是车位表、车辆记录表、费率表这三张

很多人一上来就写代码,写着写着发现字段不够用,回头改表结构,前面的代码全得跟着动。血泪经验是:先把数据模型想清楚,后面写业务逻辑会顺很多。一个最小可用的停车场管理系统,核心数据就三块:车位资源、停车记录、计费规则。

车位表管的是"车场里有哪些位置、当前状态是空闲还是占用"。停车记录表管的是"哪辆车什么时候进来的、停在哪个位置、什么时候出去的、交了多少钱"。费率表管的是"不同车型或不同时段按什么标准收费"。这三张表之间的关系是:车辆入场时从车位表找一个空闲车位,写一条停车记录;出场时根据停车记录里的入场时间和费率表算出费用,更新记录状态,同时把车位释放回空闲。

常见做法是用 SQLite 做课程设计的数据库,因为不需要额外装服务,一个文件就能跑。下面是我一般会用的建表语句,字段命名尽量直白,方便后面写查询时不用来回翻表结构。

-- 车位表:记录每个车位的基本信息和当前占用状态 CREATE TABLE parking_spot ( spot_id INTEGER PRIMARY KEY AUTOINCREMENT, -- 车位编号,自增主键 spot_code VARCHAR(20) NOT NULL UNIQUE, -- 车位显示编号,如 A-01 spot_type VARCHAR(10) DEFAULT 'normal', -- 车位类型:normal / vip / disabled status INTEGER DEFAULT 0 -- 0 空闲,1 占用,2 维护中 ); -- 停车记录表:每次入场生成一条,出场时更新 CREATE TABLE parking_record ( record_id INTEGER PRIMARY KEY AUTOINCREMENT, plate_no VARCHAR(15) NOT NULL, -- 车牌号 spot_id INTEGER, -- 关联车位表的主键 entry_time DATETIME NOT NULL, -- 入场时间 exit_time DATETIME, -- 出场时间,未出场时为空 fee DECIMAL(10,2) DEFAULT 0, -- 结算费用 status INTEGER DEFAULT 0, -- 0 停车中,1 已出场 FOREIGN KEY (spot_id) REFERENCES parking_spot(spot_id) ); -- 费率表:按车型区分每小时单价,可扩展时段规则 CREATE TABLE rate_config ( rate_id INTEGER PRIMARY KEY AUTOINCREMENT, vehicle_type VARCHAR(10) NOT NULL, -- 车型:car / truck / motorcycle price_per_hour DECIMAL(8,2) NOT NULL, -- 每小时单价 free_minutes INTEGER DEFAULT 15 -- 免费时长(分钟) );

这三张表的逻辑说明:车位表的status字段是核心,入场时把它从 0 改成 1,出场时改回 0,这样查空闲车位就是一句WHERE status = 0。停车记录表的exit_time为空表示车还在场内,这个设计比额外加一个状态字段更直观。费率表的free_minutes是为了处理"进场 15 分钟内不收费"这种常见规则,没有这个字段的话,计费逻辑里就得写死数字,后面想改都麻烦。

参数方面,plate_no给 15 个字符是因为国内车牌加上新能源绿牌的位数,留点余量比卡着长度好。fee用DECIMAL(10,2)而不是FLOAT,是因为金额计算用浮点数会出现0.1 + 0.2 != 0.3这种玄学问题,课程设计里被导师问一句"为什么金额有误差"就尴尬了。

2.2 用 Python 建库建表的完整脚本

知道表结构之后,下一步是让 Python 把库建出来。我一般会单独写一个init_db.py,跑一次就行,后面业务代码直接连这个库文件。这样做的好处是:表结构变更时只改这一个文件,不用在业务代码里到处找建表语句。

import sqlite3 import os DB_PATH = os.path.join(os.path.dirname(__file__), "parking.db") def init_database(): """初始化数据库:建表并插入基础费率数据""" conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() # 建车位表 cursor.execute(""" CREATE TABLE IF NOT EXISTS parking_spot ( spot_id INTEGER PRIMARY KEY AUTOINCREMENT, spot_code VARCHAR(20) NOT NULL UNIQUE, spot_type VARCHAR(10) DEFAULT 'normal', status INTEGER DEFAULT 0 ) """) # 建停车记录表 cursor.execute(""" CREATE TABLE IF NOT EXISTS parking_record ( record_id INTEGER PRIMARY KEY AUTOINCREMENT, plate_no VARCHAR(15) NOT NULL, spot_id INTEGER, entry_time DATETIME NOT NULL, exit_time DATETIME, fee DECIMAL(10,2) DEFAULT 0, status INTEGER DEFAULT 0, FOREIGN KEY (spot_id) REFERENCES parking_spot(spot_id) ) """) # 建费率表 cursor.execute(""" CREATE TABLE IF NOT EXISTS rate_config ( rate_id INTEGER PRIMARY KEY AUTOINCREMENT, vehicle_type VARCHAR(10) NOT NULL, price_per_hour DECIMAL(8,2) NOT NULL, free_minutes INTEGER DEFAULT 15 ) """) # 插入默认费率:小车 5 元/小时,货车 8 元/小时,摩托车 2 元/小时 cursor.execute("SELECT COUNT(*) FROM rate_config") if cursor.fetchone()[0] == 0: cursor.executemany( "INSERT INTO rate_config (vehicle_type, price_per_hour, free_minutes) VALUES (?, ?, ?)", [("car", 5.00, 15), ("truck", 8.00, 15), ("motorcycle", 2.00, 15)] ) # 批量生成 50 个车位,编号从 A-01 到 A-50 cursor.execute("SELECT COUNT(*) FROM parking_spot") if cursor.fetchone()[0] == 0: spots = [(f"A-{i:02d}", "normal", 0) for i in range(1, 51)] cursor.executemany( "INSERT INTO parking_spot (spot_code, spot_type, status) VALUES (?, ?, ?)", spots ) conn.commit() conn.close() print(f"数据库初始化完成:{DB_PATH}") if __name__ == "__main__": init_database()

这段脚本的逻辑很直接:先连库(没有就创建),然后依次建三张表,最后判断如果表里没数据就插入默认费率和 50 个车位。IF NOT EXISTS保证重复执行不会报错,SELECT COUNT(*)判断避免每次跑都重复插入数据。参数上,车位数量我习惯给 50 个,课程设计演示够用,太多的话界面列表加载会变慢,太少又不容易看出"车位已满"的逻辑。

注意:DB_PATH用os.path.dirname(__file__)拼绝对路径,是为了避免从不同目录运行脚本时数据库文件跑到别的地方去。这个坑我在早期项目里踩过,明明建了库,换个目录跑就找不到数据。

3. 入场、计费、出场:把业务逻辑写成能复用的函数

3.1 入场登记:怎么找车位、怎么写记录

入场逻辑看起来简单,但有几个细节容易翻车。第一,得先判断车牌是不是已经在场内,不然同一辆车能重复入场。第二,找空闲车位时最好按编号顺序取第一个,这样车位利用率看起来比较整齐。第三,写记录和改车位状态要在同一个事务里,不然可能出现"记录写了但车位没占"的脏数据。

下面是我一般会写的入场函数,把校验、找位、写记录、改状态四步串起来:

import sqlite3 from datetime import datetime DB_PATH = "parking.db" def vehicle_entry(plate_no, vehicle_type="car"): """车辆入场:校验重复、分配车位、写入记录""" conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() # 检查该车牌是否已有未出场的记录 cursor.execute( "SELECT record_id FROM parking_record WHERE plate_no = ? AND status = 0", (plate_no,) ) if cursor.fetchone(): conn.close() return {"success": False, "msg": "该车辆已在场内,请勿重复入场"} # 找第一个空闲车位 cursor.execute( "SELECT spot_id, spot_code FROM parking_spot WHERE status = 0 ORDER BY spot_id LIMIT 1" ) spot = cursor.fetchone() if not spot: conn.close() return {"success": False, "msg": "车位已满,无法入场"} spot_id, spot_code = spot entry_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") try: # 写入停车记录 cursor.execute( "INSERT INTO parking_record (plate_no, spot_id, entry_time, status) VALUES (?, ?, ?, 0)", (plate_no, spot_id, entry_time) ) # 更新车位状态为占用 cursor.execute( "UPDATE parking_spot SET status = 1 WHERE spot_id = ?", (spot_id,) ) conn.commit() return {"success": True, "spot_code": spot_code, "entry_time": entry_time} except Exception as e: conn.rollback() return {"success": False, "msg": f"入场失败:{str(e)}"} finally: conn.close()

逻辑说明:先查parking_record里有没有status = 0的同车牌记录,有就拒绝。然后查parking_spot里第一个status = 0的车位,没有就返回"车位已满"。拿到车位后,插入记录和更新车位状态放在try块里,任何一步出错就rollback,保证不会出现半截数据。参数上,entry_time用datetime.now()取当前时间,格式化成字符串存进 SQLite,这样查询和显示都方便。

3.2 计费函数:时间差怎么算、免费时长怎么扣

计费是整个系统里最容易出 bug 的地方。常见翻车点有三个:时间差算出来是负数(出场时间早于入场时间)、免费时长扣得不干净(比如停了 20 分钟,免费 15 分钟,应该只收 5 分钟的钱,但代码里按 1 小时收了)、金额四舍五入方式不对(有的用round,有的用int直接截断)。

我一般会这样写计费函数,把时间差转成分钟,扣掉免费时长,再按小时单价折算:

from datetime import datetime from decimal import Decimal, ROUND_HALF_UP def calculate_fee(entry_time_str, exit_time_str, price_per_hour, free_minutes=15): """计算停车费用:按分钟折算,不足一小时按比例收""" fmt = "%Y-%m-%d %H:%M:%S" entry_time = datetime.strptime(entry_time_str, fmt) exit_time = datetime.strptime(exit_time_str, fmt) # 时间差转分钟 total_minutes = (exit_time - entry_time).total_seconds() / 60 if total_minutes < 0: raise ValueError("出场时间不能早于入场时间") # 扣掉免费时长 chargeable_minutes = max(0, total_minutes - free_minutes) # 按小时单价折算,保留两位小数,四舍五入 fee = Decimal(str(chargeable_minutes)) / Decimal("60") * Decimal(str(price_per_hour)) fee = fee.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) return { "total_minutes": round(total_minutes, 1), "chargeable_minutes": round(chargeable_minutes, 1), "fee": float(fee) }

逻辑说明:total_seconds() / 60把时间差转成分钟,浮点数保留一位小数够用了。max(0, total_minutes - free_minutes)保证免费时长扣完后不会出现负数。金额计算用Decimal而不是float,quantize指定保留两位小数并四舍五入,这样算出来的钱和收银台显示的一致。参数上,free_minutes默认 15 分钟,实际调用时从费率表里读,这样不同车型可以设不同的免费时长。

3.3 出场结算:更新记录、释放车位、返回费用

出场函数要把前面几步串起来:根据车牌找未出场的记录,算费,更新记录的出场时间和费用,把车位状态改回空闲。这里的关键是"先算费再更新",不然更新完记录状态就查不到入场时间了。

def vehicle_exit(plate_no): """车辆出场:算费、更新记录、释放车位""" conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() # 查未出场的记录,连带车位和费率信息 cursor.execute(""" SELECT r.record_id, r.spot_id, r.entry_time, s.spot_code, c.price_per_hour, c.free_minutes FROM parking_record r JOIN parking_spot s ON r.spot_id = s.spot_id JOIN rate_config c ON c.vehicle_type = 'car' WHERE r.plate_no = ? AND r.status = 0 """, (plate_no,)) row = cursor.fetchone() if not row: conn.close() return {"success": False, "msg": "未找到该车辆的在场记录"} record_id, spot_id, entry_time, spot_code, price, free_min = row exit_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") try: result = calculate_fee(entry_time, exit_time, price, free_min) cursor.execute( "UPDATE parking_record SET exit_time = ?, fee = ?, status = 1 WHERE record_id = ?", (exit_time, result["fee"], record_id) ) cursor.execute( "UPDATE parking_spot SET status = 0 WHERE spot_id = ?", (spot_id,) ) conn.commit() return { "success": True, "spot_code": spot_code, "entry_time": entry_time, "exit_time": exit_time, "fee": result["fee"], "total_minutes": result["total_minutes"] } except Exception as e: conn.rollback() return {"success": False, "msg": f"出场失败:{str(e)}"} finally: conn.close()

逻辑说明:查询用JOIN把停车记录、车位、费率三张表连起来,一次拿到算费需要的所有字段。calculate_fee返回的费用写进parking_record.fee,同时把status改成 1 表示已出场。车位表的status改回 0,释放车位。参数上,费率这里写死了vehicle_type = 'car',实际项目里应该根据入场时记录的车型来查,课程设计里为了简化可以先这样处理,但要在文档里说明。

4. 避坑与排查:课程设计里最容易翻车的五个地方

4.1 车牌重复入场:现象是同一辆车能开两条记录

现象:测试时连续调用两次入场函数,同一车牌生成了两条status = 0的记录,车位也被占了两个。原因:入场校验只查了parking_record,但没考虑并发情况下两个请求同时查到"没有未出场记录",然后都执行了插入。解决:在plate_no加唯一索引,或者用数据库事务的隔离级别来保证。课程设计里最简单的做法是给plate_no加一个条件唯一索引,只对status = 0的记录生效。

CREATE UNIQUE INDEX idx_plate_active ON parking_record(plate_no) WHERE status = 0;

4.2 时间格式不统一:现象是算费时报 ValueError

现象:从数据库读出来的entry_time是2024-01-01 10:00:00格式,但手动测试时传的是2024/01/01 10:00,strptime解析直接抛异常。原因:时间格式没有统一约定,写入和读取用了不同的格式。解决:所有时间字段统一用%Y-%m-%d %H:%M:%S,写入前用strftime格式化,读取后用strptime解析。在代码里定义一个TIME_FORMAT常量,所有地方引用它,避免手写格式字符串。

4.3 车位状态和记录状态不同步:现象是车位显示空闲但记录还在

现象:出场时更新了parking_record.status = 1,但parking_spot.status没改回 0,导致车位一直显示占用。原因:两个UPDATE没有放在同一个事务里,或者其中一个执行失败但没有回滚。解决:把两个更新放在try块里,任何一步失败就rollback。另外可以在出场函数最后加一个校验,查一下车位状态是否已释放,没有就手动修。

4.4 金额精度问题:现象是费用算出 4.999999 这种数

现象:停了 1 小时 20 分钟,单价 5 元,算出来费用是4.999999999而不是5.00。原因:用float做除法再乘法,浮点数精度丢失。解决:金额计算全程用Decimal,最后quantize到两位小数。数据库字段用DECIMAL(10,2)而不是FLOAT,写入前把Decimal转成字符串或浮点数。

4.5 数据库文件路径不对:现象是换了目录跑就找不到表

现象:在项目根目录跑init_db.py建了库,然后在src/目录跑业务代码,报no such table。原因:SQLite 连接时用的相对路径parking.db,不同工作目录下指向不同文件。解决:统一用绝对路径,基于__file__拼出数据库文件的完整路径,所有模块引用同一个DB_PATH常量。

提示:课程设计答辩时,导师经常会问"如果两辆车同时入场会怎样",上面第 4.1 条的索引方案就是答案。提前想好这个问题的回应,比现场卡壳好得多。

5. 从能跑到好用:三个让课程设计加分的小技巧

5.1 用视图把常用查询封装起来

业务代码里经常要查"当前在场车辆列表",每次写JOIN很啰嗦。可以在数据库里建一个视图,把车牌、车位编号、入场时间、已停时长拼好,查询时直接SELECT * FROM v_current_parking。这样代码更干净,答辩时也能体现你懂数据库的进阶用法。

CREATE VIEW v_current_parking AS SELECT r.plate_no, s.spot_code, r.entry_time, ROUND((JULIANDAY('now') - JULIANDAY(r.entry_time)) * 24 * 60, 1) AS parked_minutes FROM parking_record r JOIN parking_spot s ON r.spot_id = s.spot_id WHERE r.status = 0;

这个视图用JULIANDAY算时间差,SQLite 里算日期差比较方便。parked_minutes直接给出已停分钟数,前端显示"已停 1 小时 20 分钟"就不用再算一遍。

5.2 加一个简单的命令行交互入口

课程设计如果只提供函数,演示时得开 Python 交互环境一行行调,不够直观。花二三十行写一个命令行菜单,入场、出场、查在场车辆、退出四个选项,演示效果会好很多。

def main_menu(): while True: print("\n===== 停车场管理系统 =====") print("1. 车辆入场") print("2. 车辆出场") print("3. 查看在场车辆") print("4. 退出") choice = input("请选择操作:").strip() if choice == "1": plate = input("请输入车牌号:").strip() result = vehicle_entry(plate) print(result["msg"] if not result["success"] else f"入场成功,车位:{result['spot_code']}") elif choice == "2": plate = input("请输入车牌号:").strip() result = vehicle_exit(plate) if result["success"]: print(f"出场成功,费用:{result['fee']} 元,已停 {result['total_minutes']} 分钟") else: print(result["msg"]) elif choice == "3": conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute("SELECT plate_no, spot_code, entry_time FROM v_current_parking") rows = cursor.fetchall() conn.close() if rows: for row in rows: print(f"车牌:{row[0]},车位:{row[1]},入场:{row[2]}") else: print("当前无在场车辆") elif choice == "4": print("退出系统") break else: print("无效选择,请重新输入")

这段代码逻辑很直白,while True循环加input读选择,每个分支调对应的函数。参数上没什么特别的,注意strip()去掉输入前后的空格,避免用户多打一个空格就查不到记录。

5.3 用事务保证批量操作的原子性

如果后面要扩展"批量导入历史记录"或"批量释放车位"这类功能,记得把多条INSERT或UPDATE包在一个事务里。SQLite 默认每条语句自动提交,批量操作时性能差还容易出半截数据。显式用BEGIN和COMMIT包起来,出错时ROLLBACK,这是从课程设计走向实际项目必须养成的习惯。

我自己的习惯是:凡是涉及两张以上表的写操作,一律先想事务边界在哪,再动手写代码。这个习惯帮我省掉了无数次"数据对不上"的后悔药。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询