@TOC
- 基于 MySQL,设计并实现一个简单的旅行预订系统。该系统涉及的信息有航班、大巴班车、宾馆房间和客户数据等信息
- 全部代码见 Github
- 为该旅行预订系统设置两种用户:
- (1) 预订系统管理者
admin。管理者可以直接对航班、大巴、宾馆、客户信息进行增删改查,还可以查询预约信息 (不允许admin直接在后台更改客户的预约信息,如果航班取消或航班号修改,数据库会自动利用触发器对相应的预约信息进行删改)。同时也支持admin直接输入 SQL 语句来操纵数据库 - (2) 普通客户。支持客户的登录与注册。普通客户可以对航班/大巴/宾馆进行预约/取消预约、查询预约订单情况、查询旅行线路、检查预定线路合理性
- (1) 预订系统管理者
请忽视上图中的箭头...
- FLIGHTS 表用来记录航班信息。这里假设在一个航班上,所有座位的价格都一样,因此
FLIGHTS表中,航班号flightNum为主键。用户定义完整性主要是确保各个属性的取值有实际意义,具体定义如下:
create table FLIGHTS(
flightNum char(5) primary key,
price int check(price > 0),
numSeats int check(numSeats > 0),
numAvail int check(numAvail >= 0),
FromCity varchar(50) not null,
ArivCity varchar(50) not null
) CHARSET=utf8;- HOTELS 表用来记录宾馆信息。这里假设在一个宾馆内,所有房间的价格都一样,因此
HOTELS表中,宾馆号hotelNum为主键。用户定义完整性主要是确保各个属性的取值有实际意义,具体定义如下:
create table HOTELS(
hotelNum char(5) primary key,
location varchar(50) not null,
price int check(price > 0),
numRooms int check(numRooms > 0),
numAvail int check(numAvail >= 0)
) CHARSET=utf8;- BUS 表用来记录大巴班车信息。这里假设同一个大巴车上所有座位价格一样,因此
BUS表中,车号BusNum为主键。用户定义完整性主要是确保各个属性的取值有实际意义,具体定义如下:
create table BUS(
BusNum char(5) primary key,
location varchar(50) not null,
price int check(price > 0),
numSeats int check(numSeats > 0),
numAvail int check(numAvail >= 0)
) CHARSET=utf8;- CUSTOMERS 表用来记录客户信息,客户 ID
custID为主键。同时用户定义完整性规定custName不能为空,否则就没有了实际意义:
create table CUSTOMERS(
custID char(5) primary key,
custName varchar(50) not null
) CHARSET=utf8;- RESERVATIONS 表用来记录客户的预约信息。规定预约号
resvNum为主键 ((resvType,resvKey) 为候选码)。custID为外码,参照 CUSTOMERS 表的custID属性;用户定义完整性规定resvType只能取值 1 / 2 / 3,分别表示预约类型为航班/大巴车/宾馆;规定resvKey对应 FLIGHTS / HOTELS / BUS 表的一个主键,用来表示预约的航班号/车号/宾馆号:
create table RESERVATIONS(
resvNum char(5) primary key,
custID char(5) not null,
resvType int check(resvType in (1, 2, 3)),
resvKey char(5) check(
resvKey in (
select flightNum from FLIGHTS
union select hotelNum from HOTELS
union select BusNum from BUS
)
),
foreign key (custID) references CUSTOMERS(custID)
) CHARSET=utf8;- MySQL 在表的主键上都建立了索引。除此之外,因为在查询预约对应的航班号/车号/宾馆号时经常要查询 (
resvType,resvKey) 这一候补码,因此在resvType和resvKey属性上也建立索引:
create unique index resv_idx
on RESERVATIONS(resvType, resvKey);航班、大巴车、宾馆的实现都类似,因此下面主要以航班 FLIGHTS 表为例进行讲解
主要使用了以下触发器保持数据库的一致性:
- (1) 在
RESERVATIONS表上设置插入触发器,如果插入一条航班预约信息 (new.resvType = 1) 且该航班号存在 (航班号在 FLIGHTS 表中),则查看该航班的剩余座位数,如果该航班还有剩余座位,则到航班表中将该航班的剩余座位数减一,否则禁止此次预约 (回滚)
create trigger make_reservation
before insert on RESERVATIONS
for each row
begin
if new.resvType = 1 then
if (select numAvail from FLIGHTS where flightNum = new.resvKey) > 0
and exists(select * from FLIGHTS where flightNum = new.resvKey)
then
update FLIGHTS
set numAvail = numAvail - 1
where flightNum = new.resvKey;
else
# 预约数已满,不允许预约
# 由于 MySQL 触发器中不允许使用 rollback,
# 因此通过将主键设为非法值,数据库插入报错来阻止插入
set new.resvNum = '??????????????';
end if;
end if;
end;- (2) 在
RESERVATIONS表上设置删除触发器,如果取消航班预约 (old.resvType = 1) 且 FLIGHTS 表中存在该航班信息,则将 FLIGHTS 表中对应航班的可用座位数加一- 这里一定要先检查 FLIGHTS 表中存在该航班信息是因为取消航班预约也可能是由对应航班取消导致的 (这是后面要讲的一个触发器的功能),因此如果不检查的话可能会导致触发器循环调用 (删除 FLIGHTS 表中的航班信息导致删除对应航班的预约信息,进而触发
RESERVATIONS表上的删除触发器,该触发器又企图对 FLIGHTS 表进行更新减少对应航班的可用座位数 (尽管这个更新实际不会发生,但 MySQL 依然会报错)),这在 MySQL 中是不允许的
- 这里一定要先检查 FLIGHTS 表中存在该航班信息是因为取消航班预约也可能是由对应航班取消导致的 (这是后面要讲的一个触发器的功能),因此如果不检查的话可能会导致触发器循环调用 (删除 FLIGHTS 表中的航班信息导致删除对应航班的预约信息,进而触发
create trigger cancel_reservation
after delete on RESERVATIONS
for each row
begin
if old.resvType = 1
and exists(select * from FLIGHTS where flightNum = old.resvKey) then
update FLIGHTS
set numAvail = numAvail + 1
where flightNum = old.resvKey;
end if;
end;- (3) 在
FLIGHTS表上设置删除触发器,如果有航班被取消,则删除该航班对应的预约信息
create trigger delete_reservation
after delete on FLIGHTS
for each row
begin
delete from RESERVATIONS
where old.flightNum = resvKey and resvType = 1;
end;- (4) 在
FLIGHTS表上设置更新触发器,如果有航班号被更改,则同步更新该航班对应的预约信息
create trigger update_reservation
after update on FLIGHTS
for each row
begin
if new.flightNum != old.flightNum then
update RESERVATIONS
set resvKey = new.flightNum
where old.flightNum = resvKey and resvType = 1;
end if;
end;Database 类
- 首先定义
Database类,它主要通过 pymysql 库实现数据库连接、执行 sql 语句、返回 sql 语句的查询结果、用 MySQL 风格打印查询结果等功能
Menu 类
- 主要提供各级菜单的显示以及用户功能选择的获取
BookingSys 类
- 继承自
Database类,旅行预订系统的各项功能主要都是由它提供的,下面主要讲述该类中方法的实现
下面不会贴太多代码。代码的基本思路都是让用户输入必要的信息,然后组成 SQL 语句,通过 pymysql 库提交数据库执行
(1) 直接输入 SQL 语句操纵数据库
- 这个实现比较简单,就是通过 pymysql 库执行用户输入的 SQL 语句,如果发生错误则进行回滚
(2) 对航班、大巴、宾馆、客户信息进行增删改
航班、大巴、宾馆、客户信息的增删改查都比较类似,下面以航班为例进行说明
- 注意:这里不允许
admin直接在后台更改客户的预约信息,如果航班取消或航班号修改,数据库会自动利用触发器对相应的预约信息进行删改 - 增加航班:要求用户输入
flightNum,price,numSeats,FromCity,ArivCity这五个属性的值,然后构建出insert语句,利用Database类提供的 SQL 语句执行功能进行插入 - 删除航班:要求用户输入要删除的航班号
flightNum,然后构建出delete语句,利用Database类提供的 SQL 语句执行功能进行删除 - 修改航班信息:要求用户输入要修改的航班号
flightNum,然后输入要修改的属性名和新值,最后构建出update语句,利用Database类提供的 SQL 语句执行功能进行更新
(3) 对航班、大巴、宾馆、客户、预约信息进行查询
- 查询航班信息时 (大巴、宾馆、客户信息查询同理),直接利用
Database类提供的 SQL 语句执行功能执行select * from FLIGHTS语句,然后利用Database类提供的查询结果打印功能将其规整地打印出来 - 查询预约信息时,还要求客户选择查询航班/大巴/宾馆中哪一个的预约情况。以航班为例,将
RESERVATIONS,CUSTOMERS,FLIGHTS表进行自然连接并筛选出航班的预约信息 (resvType = 1),最后输出预约号、客户 id、预约的航班号、航班价格、航班起点及终点。也就是执行以下 SQL 语句:
select resvNum, CUSTOMERS.custID, flightNum, price, FromCity, ArivCity
from RESERVATIONS, CUSTOMERS, FLIGHTS
where resvKey = flightNum and resvType = 1
and RESERVATIONS.custID = CUSTOMERS.custID;以下说明均以航班为例,大巴/宾馆的实现过程同理
(1) 客户登录与注册
- 在用户输入其 id 后,首先构建
select custID from CUSTOMERS的 SQL 查询语句,然后通过Database类提供的查询功能进行查询,返回查询结果 - 如果用户输入的 id 在查询结果中,说明该客户存在,打印出登录成功的信息,之后再构建
select custName from CUSTOMERS where custid = %s的 SQL 语句进行查询 (%s这里为输入的客户 id),打印出该客户 id 对应的名字 - 如果用户输入的 id 不在查询结果中,则要求客户继续提供新用户名 name,最后构建
insert into CUSTOMERS values('%s', '%s')插入 SQL 语句向客户表中插入新的客户信息 (id 和 name),并打印出注册成功的提示信息
(2) 预约
- 客户选择预约模式后,还要再选择预约航班/大巴车/宾馆;下面以预约航班为例进行说明:首先要求客户输入航班起点
FromCity与到达城市ArivCity,然后构建查询 SQL 语句:select * from FLIGHTS where FromCity = '%s' and ArivCity = '%s' and numAvail > 0来选出所有符合起点与终点条件以及机上还有剩余座位的航班信息并将其打印出来 - 接着查询
RESERVATIONS表中所有预约信息的resvNum并随机生成一个不同于预约表中已有预约号的新的预约号 - 接着要求客户继续输入想要预约的航班号,在进行确认后在
RESERVATIONS表中插入一条预约信息,其中resvType = 1表示预约的是航班,resvKey为flightNum,custID为进行预约的客户 id,resvNum为刚才随机生成的新预约号
(3) 取消预约
- 同样以取消预约航班为例,首先如下面的代码所示 (
id为当前取消预约的客户 id,verbose设为True表示打印预约信息,output_resvNum设为True表示除了打印航班的起点终点,还打印航班的预约号),将CUSTOMERS,FLIGHTS,RESERVATIONS三个表做自然连接,然后选出指定客户的预约信息 (预约号、航班号、起点、终点),并将选出的预约信息打印出来以供客户选择要取消哪个预约
def query_flights(self, id, verbose=False, output_resvNum=False):
"""查询指定客户的航班信息"""
key = "RESERVATIONS.resvNum, " if output_resvNum else ""
sql = """select %s FLIGHTS.flightNum, FromCity, ArivCity
from CUSTOMERS, FLIGHTS, RESERVATIONS
where CUSTOMERS.custID = RESERVATIONS.custID
and FLIGHTS.flightNum = RESERVATIONS.resvKey
and resvType = %d
and CUSTOMERS.custID = '%s'""" %(key, self.RESERVE_FLIGHT, id)
flights = self.query(sql)
if verbose:
if len(flights) == 0:
print("You haven't reserved any flight yet.")
else :
print("Reserved Flight:")
self.print_query_results(flights)
return flights- 在打印了航班预约信息之后,要求客户输入要取消的预约号
resvNum。此时将RESERVATIONS和FLIGHTS表做自然连接来查询客户输入的resvNum是否为航班预约,如果是,则构建delete from RESERVATIONS where resvNum = '%s'SQL 语句对该条预约记录进行删除
(4) 查询预约情况
- 下面同样以查询航班信息为例进行说明:这里查询预约情况调用的就是上面的
query_flights方法来打印用户的航班预约信息 (不同的是将output_resvNum设为False来避免打印出预约号)
(5) 查询旅行线路
- 这里默认只有航班才能跨城市移动,大巴车只能在本地移动,因此查询旅行线路同样是调用
query_flights方法查询客户已经预约的航班信息,最终打印出每一个航班的起点和终点
(6) 检查线路合理性
- 主要是检查客户预约的大巴车和宾馆是否在航班途经的城市上,如果不在,则线路不合理。因此先调用
query_flights方法查询客户已经预约的航班信息,然后将所有途经城市都存到一个集合cities中,之后分别调用query_buses和query_hotels方法查询客户预约的大巴车和宾馆记录,如果其中有大巴车或宾馆的地点不在cities中,则线路不合理
登录
- 登录界面: 要求输入用户 id
- 这里输入一个新客户 id
00001,系统提示需要先注册,要求输入客户名 - 这里输入客户名为
lianlio,系统提示注册成功,同时弹出客户服务菜单,从 0 ~ 5 依次表示退出登录、预约、查询预约、取消预约、查询旅行线路、检查旅行线路合理性 - 此时如果我退出系统再次输入 id 进行登录,系统就会提示登陆成功,并显示欢迎信息:
预约
- 输入 1 来选择预约服务,这里系统继续询问要预约航班、大巴还是宾馆
- 这里以预约航班为例,输入 1 以预约航班;系统继续询问要预约从哪里到哪里的航班
- 假设预约大连到无锡的航班,系统经过查询后输出了所有大连到无锡的航班信息,并进一步询问要预约的航班号
- 这里选择 17593 号航班,在确认预约后,系统提示预约成功
预约查询
取消预约
- 选择客户服务 3 (取消预约),并选择取消航班预约。此时系统自动打印出了当前客户的航班预约情况 (这里我事先又增加了几个航班预约),并询问客户要取消哪个航班的预约
- 选择取消 19213 号从深圳飞往上海的航班,对应的预约号为 90670。之后系统会询问是否删除从深圳到上海的航班预约,确认后系统显示删除成功的提示
查询旅行线路
检查预定线路合理性
- 如下图所示,我目前预约的宾馆均在航班途经城市上
- 此时选择"检查预定线路合理性"服务,系统显示旅行计划合理
- 下面我再多预定一个深圳的宾馆,这个宾馆不在我的旅行路线上
- 此时再次选择"检查预定线路合理性"服务,系统显示旅行计划不合理
- 在登录界面输入
admin进入系统管理者界面,可以直接对航班、大巴、宾馆、客户信息进行增删改查,还可以查询预约信息 (不允许admin直接在后台更改客户的预约信息,如果航班取消或航班号修改,数据库会自动利用触发器对相应的预约信息进行删改)。同时也支持admin直接输入 SQL 语句来操纵数据库
下面以航班为例演示增删改查
(1) 查询航班信息
(2) 新增航班
- 在选择对 FLIGHTS 表进行操作后,选择 1. Insert 进行航班信息的插入。系统要求输入航班的各项信息
- 下面插入一趟航班号为
12345,票价 999,载客量 100,从无锡飞往大连的航班。系统显示插入成功 - 再次进行查询,可以观察到这趟航班已被成功添加
(3) 修改航班信息
- 在选择对 FLIGHTS 表进行操作后,选择 4. Update 进行航班信息的更新。系统要求输入想要修改的航班号
- 这里就对刚才添加的航班
12345进行修改。输入航班号后,系统要求输入想要更改的航班属性与新值。这里将航班号改为54321.最后系统提示修改成功: - 此时对航班信息进行查询,发现
12345航班被修改为了54321航班:
(4) 删除航班信息
- 在选择对 FLIGHTS 表进行操作后,选择 2.Delete 进行航班信息的删除。系统要求输入想要删除的航班号,这里我们就选择删除航班
54321;最后系统提示删除成功: - 再次查询航班信息,可以看到航班
54321已被删除
(5) 查询预约信息
直接执行 SQL 语句
- 管理者还可以选择直接使用 SQL 语句来更灵活的操纵数据库
- 如下图所示,查询航班信息,目前最后一行上航班
19213的票价为 999 - 下面将航班
19213的票价涨价 301 元。输入 SQL 语句后系统提示执行成功: - 再次查询航班信息,航班
19213的票价涨价到了 1300








































