数据库课程设计 - 机票预订系统

机票预定系统 - Shuai-Xie - Github

一、机票预定系统

1.1 题目要求

要求具备如下基本功能

  1. 班机基本信息的管理;
  2. 航班信息的管理;
  3. 旅客预定机票、取消预约、付款取票、退票的管理;
  4. 查询航班信息、航班预定情况、旅客信息,计算航班满座率。
  5. 统计每周、每月,每年营业收入情况。

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图

航班和旅客间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 查询模块

前端表单接收用户传入的出发地、目的地和出发时间,然后在航班数据库中寻找满足条件的航班,分两步:

  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 管理员界面

管理员 admin 登录账号

在前面的 login_user 函数中已经有过判定,如果登录用户是管理员,加载航空公司的财务页面。

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 语句依然有用,方便我们验证代码逻辑是否正确,总之,收获很多。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容