机票预定系统 - Shuai-Xie - Github
一、机票预定系统
1.1 题目要求
要求具备如下基本功能
- 班机基本信息的管理;
- 航班信息的管理;
- 旅客预定机票、取消预约、付款取票、退票的管理;
- 查询航班信息、航班预定情况、旅客信息,计算航班满座率。
- 统计每周、每月,每年营业收入情况。
1.2 开发环境
- 语言:Python 3.5
- 框架:Django 1.10.6
- 前端设计:HTML, CSS, JavaScript
- 开发环境:PyCharm
二、需求分析
2.1 具体需求
为方便旅客出行,某航空公司(CSU Airlines)拟开发一个机票预定系统。旅客可通过网上订票平台查询公司航班情况,通过输入起飞地、目的地、起飞时间等信息系统为旅客安排航班,旅客可根据自身需要,按照起飞时间和机票价位来选择航班。订票成功之后,系统为旅客生成订单信息,旅客可以再个人信息页面查看自己的订票信息,并且可以向系统提出退票要求,系统针对具体情况计算手续费后进行相应退票处理。
2.2 功能分析
(1)用户界面
- 查询:用户对航班信息进行查询操作;
- 排序:用户根据自己的需求对查询结果进行排序筛选;
- 订票:对用户订票需求进去处理并记录旅客预定信息和更新数据库信息;
- 退票:对用户退票需求进行处理并更新数据库;
- 查看个人信息:用户查看自己的个人票务信息
- 帮助:提供系统使用帮助文档;
- 退出:关闭当前页面。
(2)管理员界面
- 航班信息管理:可对航班信息进行增删改查操作;
- 旅客信息管理:可对旅客信息进行增删改查操作;
- 财务信息管理:可以统计航空公司每周、每月,每年营业收入情况。
- 帮助:提供系统使用帮助文档;
- 退出:关闭当前页面。
2.3 系统主功能图
2.4 系统数据流图
三、逻辑设计
3.1 ER图
3.2 数据库表
(1)航班信息表
(2)旅客信息表
旅客信息表继承了 Django 模板中默认的 User 类,所以有一些继承来的别的字段,我们的 Passenger 对象只需要 id,username,password 即可。
(3)实体间多对多关系
通过在两表之间建立一张新表的方式,将 booksystem_flight 表和 auth_user 表的主键联系在一起,实现了多对多关系。
四、功能设计
本系统采用的 Python + Django + Sqlite 的设计方法,后台功能写在 views.py 文件中。
4.1 用户模块
4.1.1 订票模块
在用户订票的过程中,首先判定用户是否登录,如果没有登录,就加载登录页面;如果用户已经登陆了,通过前端用户选择的航班传入flight.id,然后判断如果用户已经订过这次航班,就反馈冲突信息,如果没有,订票成功,数据库更新,显示订票成功页面。
# 免除csrf
@csrf_exempt
def book_ticket(request, flight_id):
if not request.user.is_authenticated(): # 如果没登录就render登录页面
return render(request, 'booksystem/login.html')
else:
flight = Flight.objects.get(pk=flight_id)
# 查看乘客已经订购的flights
booked_flights = Flight.objects.filter(user=request.user) # 返回 QuerySet
if flight in booked_flights:
return render(request, 'booksystem/book_conflict.html')
# book_flight.html 点确认之后,request为 POST 方法,虽然没有传递什么值,但是传递了 POST 信号
# 确认订票,flight数据库改变
# 验证一下,同样的机票只能订一次
if request.method == 'POST':
if flight.capacity > 0:
flight.book_sum += 1
flight.capacity -= 1
flight.income += flight.price
flight.user.add(request.user)
flight.save() # 一定要记着save
# 传递更改之后的票务信息
context = {
'flight': flight,
'username': request.user.username
}
return render(request, 'booksystem/book_flight.html', context)
4.1.2 查询模块
前端表单接收用户传入的出发地、目的地和出发时间,然后在航班数据库中寻找满足条件的航班,分两步:
- 寻找出发地和目的地相同的航班;
- 寻找航班出发日期与旅客出发日期相同的航班。
为了给用户良好的体验,满足条件的航班信息按照不同的 key 值(起飞时间、降落时间、机票价格)进行升序排列。
# 搜索结果页面
def result(request):
if request.method == 'POST':
form = PassengerInfoForm(request.POST) # 绑定数据至表单
if form.is_valid():
passenger_lcity = form.cleaned_data.get('leave_city')
passenger_acity = form.cleaned_data.get('arrive_city')
passenger_ldate = form.cleaned_data.get('leave_date')
# print(type(passenger_ldate))
# 全设为naive比较
# china_tz = pytz.timezone('Asia/Shanghai')
# passenger_ltime = datetime.datetime(
# year=passenger_ldate.year,
# month=passenger_ldate.month,
# day=passenger_ldate.day,
# hour=0, minute=0, second=0,
# tzinfo=china_tz
# )
# 全设为aware比较
passenger_ltime = datetime.datetime.combine(passenger_ldate, datetime.time())
print(passenger_ltime)
# filter 可用航班
all_flights = Flight.objects.filter(leave_city=passenger_lcity, arrive_city=passenger_acity)
usable_flights = []
for flight in all_flights: # off-set aware
flight.leave_time = flight.leave_time.replace(tzinfo=None) # replace方法必须要赋值。。笑哭
if flight.leave_time.date() == passenger_ltime.date(): # 只查找当天的航班
usable_flights.append(flight)
# 按不同的key排序
usable_flights_by_ltime = sorted(usable_flights, key=attrgetter('leave_time')) # 起飞时间从早到晚
usable_flights_by_atime = sorted(usable_flights, key=attrgetter('arrive_time'))
usable_flights_by_price = sorted(usable_flights, key=attrgetter('price')) # 价格从低到高
# 转换时间格式
time_format = '%H:%M'
# for flight in usable_flights_by_ltime:
# flight.leave_time = flight.leave_time.strftime(time_format) # 转成了str
# flight.arrive_time = flight.arrive_time.strftime(time_format)
#
# for flight in usable_flights_by_atime:
# flight.leave_time = flight.leave_time.strftime(time_format) # 转成了str
# flight.arrive_time = flight.arrive_time.strftime(time_format)
# 虽然只转换了一个list,其实所有的都转换了
for flight in usable_flights_by_price:
flight.leave_time = flight.leave_time.strftime(time_format) # 转成了str
flight.arrive_time = flight.arrive_time.strftime(time_format)
# 决定 search_head , search_failure 是否显示
dis_search_head = 'block'
dis_search_failure = 'none'
if len(usable_flights_by_price) == 0:
dis_search_head = 'none'
dis_search_failure = 'block'
context = {
# 搜多框数据
'leave_city': passenger_lcity,
'arrive_city': passenger_acity,
'leave_date': str(passenger_ldate),
# 搜索结果
'usable_flights_by_ltime': usable_flights_by_ltime,
'usable_flights_by_atime': usable_flights_by_atime,
'usable_flights_by_price': usable_flights_by_price,
# 标记
'dis_search_head': dis_search_head,
'dis_search_failure': dis_search_failure
}
if request.user.is_authenticated():
context['username'] = request.user.username
return render(request, 'booksystem/result.html', context) # 最前面如果加了/就变成根目录了,url错误
else:
return render(request, 'booksystem/index.html') # 在index界面提交的表单无效,就保持在index界面
else:
context = {
'dis_search_head': 'none',
'dis_search_failure': 'none'
}
return render(request, 'booksystem/result.html', context)
4.1.3 退票模块
退票时需要更新数据库,更新航班的(capacity, book_sum, income)字段,并且在 booksystem_flight_user 表中删除这个订单。
# 退票
def refund_ticket(request, flight_id):
flight = Flight.objects.get(pk=flight_id)
flight.book_sum -= 1
flight.capacity += 1
flight.income -= flight.price
flight.user.remove(request.user)
flight.save()
return HttpResponseRedirect('/booksystem/user_info')
4.1.4 个人信息模块
由于管理员和用户共用一个登录窗口,所以在显示用户信息时,需要对登录的用户身份进行判定,如果登录的用户是管理员,则加载管理页面,如果是普通用户,则加载用户个人的订单页面。
# 显示用户订单信息
# 航班信息,退票管理
def user_info(request):
if request.user.is_authenticated():
# 如果用户是管理员,render公司航班收入统计信息页面 admin_finance
if request.user.id == ADMIN_ID:
context = admin_finance(request) # 获取要传入前端的数据
return render(request, 'booksystem/admin_finance.html', context)
# 如果用户是普通用户,render用户的机票信息 user_info
else:
booked_flights = Flight.objects.filter(user=request.user) # 从 booksystem_flight_user 表过滤出该用户订的航班
context = {
'booked_flights': booked_flights,
'username': request.user.username, # 导航栏信息更新
}
return render(request, 'booksystem/user_info.html', context)
return render(request, 'booksystem/login.html') # 用户如果没登录,render登录页面
4.2 管理员模块
4.2.1 航班信息管理
航班对象继承了 models.Model,航班信息管理在 Django 默认的后台管理界面中实现。
from django.contrib.auth.models import Permission, User
from django.db import models
# Create your models here.
# 添加primary_key会覆盖掉默认的主键
class Flight(models.Model):
user = models.ManyToManyField(User, default=1) # 有了这个字段之后,默认的后台添加失效,必须要自定义Form,除去这个字段
name = models.CharField(max_length=100) # 班次 南方航空CZ3969
leave_city = models.CharField(max_length=100, null=True) # 离开城市
arrive_city = models.CharField(max_length=100, null=True) # 到达城市
leave_airport = models.CharField(max_length=100, null=True) # 离开的机场
arrive_airport = models.CharField(max_length=100, null=True) # 到达的机场
leave_time = models.DateTimeField(null=True) # DateTimeField包括了DateField信息,并且添加了时间
arrive_time = models.DateTimeField(null=True)
capacity = models.IntegerField(default=0, null=True) # 座位总数
price = models.FloatField(default=0, null=True) # 价格
book_sum = models.IntegerField(default=0, null=True) # 订票总人数
income = models.FloatField(default=0, null=True) # 收入
def __str__(self):
return self.name
在使用 Django 默认的后台管理时,由于 Flight 中多了字段 user,而 user 对象是不能从后台输入的,所以在 Django 默认的表单管理中出去 user,因为 Flight 与 User 之间关系是多对多,所以 Django 建立的 book_system 表中是没有 user 字段的,通过下面的方法解决。
# 自定义Flight对象的输入信息
class FlightForm(forms.ModelForm):
class Meta:
model = Flight
exclude = ['user'] # user信息不能从后台输入
4.2.2 旅客信息管理
旅客继承了 django.contrib.auth.User 类,我们只需要自定义用户表单需要输入的对象,其他默认生成的字段不用考虑。
# 用户需要输入的字段
class UserForm(forms.ModelForm):
password = forms.CharField(widget=forms.PasswordInput)
class Meta:
model = User
fields = ['username', 'email', 'password']
4.2.3 航空公司财务统计
统计航空公司每周、每月、每年的收入,并且显示所有的订单信息。
# 管理员后台财务管理
# 统计航空公司每周、每月,每年营业收入情况。
def admin_finance(request):
all_flights = Flight.objects.all()
all_flights = sorted(all_flights, key=attrgetter('leave_time')) # 将所有航班按照起飞时间排序
# 将航班每天的输入打上不同的时间标签 [周,月,日]
week_day_incomes = []
month_day_incomes = []
year_day_incomes = []
# 用set存储所有的 周,月,年
week_set = set()
month_set = set()
year_set = set()
for flight in all_flights:
if flight.income > 0: # 只统计有收入的航班
# 打上周标签
this_week = flight.leave_time.strftime('%W') # datetime获取周
week_day_incomes.append((this_week, flight.income)) # 添加元组(week, income)
week_set.add(this_week)
# 打上月标签
this_month = flight.leave_time.strftime('%m') # datetime获取月
month_day_incomes.append((this_month, flight.income)) # 添加元组(month, income)
month_set.add(this_month)
# 打上年标签
this_year = flight.leave_time.strftime('%Y') # datetime获取年
year_day_incomes.append((this_year, flight.income)) # 添加元组(year, income)
year_set.add(this_year)
# 存储每周收入
# 将每周的收入用 IncomeMetric 类型存储在 week_incomes List中
week_incomes = []
for week in week_set:
income = sum(x[1] for x in week_day_incomes if x[0] == week) # 同周次的income求和
flight_sum = sum(1 for x in week_day_incomes if x[0] == week) # 同周次的航班总数目
week_income = IncomeMetric(week, flight_sum, income) # 将数据存储到IncomeMetric类中,方便jinja语法
week_incomes.append(week_income)
week_incomes = sorted(week_incomes, key=attrgetter('metric')) # 将List类型的 week_incomes 按周次升序排列
# 存储每月收入
# 将每月的收入用 IncomeMetric 类型存储在 month_incomes List中
month_incomes = []
for month in month_set:
income = sum(x[1] for x in month_day_incomes if x[0] == month)
flight_sum = sum(1 for x in month_day_incomes if x[0] == month)
month_income = IncomeMetric(month, flight_sum, income)
month_incomes.append(month_income)
month_incomes = sorted(month_incomes, key=attrgetter('metric')) # 将List类型的 month_incomes 按月份升序排列
# 存储每年收入
# 将每年的收入用 IncomeMetric 类型存储在 year_incomes List中
year_incomes = []
for year in year_set:
income = sum(x[1] for x in year_day_incomes if x[0] == year)
flight_sum = sum(1 for x in year_day_incomes if x[0] == year)
year_income = IncomeMetric(year, flight_sum, income)
year_incomes.append(year_income)
year_incomes = sorted(year_incomes, key=attrgetter('metric')) # 将List类型的 year_incomes 按年份升序排列
# 存储order信息
passengers = User.objects.exclude(pk=1) # 去掉管理员
order_set = set()
for p in passengers:
flights = Flight.objects.filter(user=p)
for f in flights:
route = f.leave_city + ' → ' + f.arrive_city
order = Order(p.username, f.name, route, f.leave_time, f.price)
order_set.add(order)
# 信息传给前端
context = {
'week_incomes': week_incomes,
'month_incomes': month_incomes,
'year_incomes': year_incomes,
'order_set': order_set
}
return context
五、界面设计
5.1 欢迎界面
拟定一趟行程(长沙→上海 2017/4/2)
5.2 查询界面
用户 Let’s Go 之后,加载查询结果页面。
默认的机票信息按照价格升序排列,用户通过点击机票信息上方的字段可以选择按照起飞时间或者到达时间升序排列,如下图,注意后两行的变化。
如果用户需要的航班数据库中不存在,就反馈错误信息。
将用户的目的地修改成中国(数据库中没有这趟航班)进行测试。
5.3 订票界面
由于用户还没有登录,会直接反馈到登录界面。
由于用户尚未注册,用户在该页面点击 Click here 进入注册账号页面,完成账号注册。
用户注册完账号直接加载到查询页面。
用户再次点击订票,如果用户尚未订过该趟航班,加载订票确认页面,如果用户已经订过了,加载订票冲突页面。
在正常订票页面点击确认,完成订票。
在个人中心用户可以查看自己的订票信息。
如果用户选择了自己已经订过的机票,加载订票冲突页面。
5.4 退票界面
在用户的个人中心,可以进行退票。
选择确认,完成退票,用户订票信息刷新。
5.5 管理员界面
在前面的 login_user 函数中已经有过判定,如果登录用户是管理员,加载航空公司的财务页面。
管理员登录成功。
5.6 后台管理界面
链接尾部输入 admin 进入后台管理
管理员登录账号
后台数据,包括 Flight,User 和 Django 默认生成的数据。
航班信息管理,显示所有航班信息,可以增删改查。
旅客信息管理,操作同航班信息管理,注册的用户的信息都会保存在这里。
六、结束语
其实从大三上的寒假开始,我就在为这次的数据库课设做准备,从学长们那里了解到了数据库课设最好的实现方法是写网站,于是寒假回去我就学习了基础的 html, css, js,对网站前端有了基本认识,又结合我目前常用的 Python 语言,学习了 Django 网页开发框架,对网站后端处理有了深层认识。虽然对网站前后端的交互还有点模糊,但是经过这次数据库课设之后,我很多的疑问得到了解答,对 Python + Django + Sqlite 开发更加熟练了。
这次数据库我用的是 Django 默认的数据库 Sqlite,这是一个轻量级的数据库,除了自带的一些指令与其他数据库有差异,大部分 SQL 语句与主流数据库都相同,但是 Sqlite 是一个本地型的数据库,无需安装和管理配置,并且占用空间非常小,用来做小型的网站开发完全够用。
在建设机票预订系统时,主要的问题就是建立实体,并确定实体之间的关系,一个旅客可以订购多架飞机,一个飞机可以承载多个用户,飞机和旅客之间是多对多关系,清楚这一点的前提下,才能建设合理的数据库完成事务需求。
另外,有一点很深的收获是,在用 web 开发的时候,对数据库的操作已经不是 SQL 语句了,而是通过高级语言(如Python)的语法来完成对数据库的增删改查操作。
举个简单的例子,在查询 booksystem_flight 表中的所有航班信息时:
- SQL: select * from booksystem_flight;
- Django: Flight.objects.all( )
可见高级语言和数据库的结合开发使得很多底层的数据操作也转化成了开发人员熟悉的高级语言程序,但无论如何,仍然是对数据库进行了操作,传统的 SQL 语句依然有用,方便我们验证代码逻辑是否正确,总之,收获很多。