软件设计文档

1. 前端

1.1 技术选型以及理由

前端使用了 Vue 来构建单页应用,Vuex 管理状态,webpack 来打包构建,axios 与后台进行数据交互,iView、 Vux 作为 UI 组件库,具体理由如下:

Vue

Vuex

webpack

axios

iView

Vux

1.2 架构设计

为了使得前端整体更容易管理以及拓展,我们将前端分为四层,按顺序分别是视图层、逻辑层、数据层和网络访问层,各个层次只与相邻层直接相关,以下是结合文件组织的详细说明:

整体架构图

1.3 模块划分

1.3.1 客户端

客户端系统前端分主要为4个页面
根据单例应用的思想,将各个部分抽象为一个个组件,通过组件的切换,从而实现跳转,组件设计、功能及包含关系如下
每个组件绑定的数据包含在一个数据模块,网络请求以及数据处理包含在数据处理模块中

1.3.2 商家端

商家端系统前端主要分为 8 个页面

将其中的状态管理、路由管理、权限管理、api 后台数据交互提取出来,独立作为 4 个模块,共 12 个模块。

1.4 组织结构

1.4.1 客户端

客户端项目中,主要包含以下包:

dist 为项目打包后文件

structure

1.4.2 商家端

.
├── LICENSE
├── README.md
├── package.json
├── ...
├── build   # 存放 webpack 配置文件
├── config  # 存放 Vue 项目中需要用到的配置
├── dist    # 编译后目录
└── src
    ├── api       # axios 与后台对接 api
    ├── images    # 存放图片
    ├── libs      # 存放通用函数
    ├── locale
    ├── router    # vue-router
    ├── store     # vuex
    ├── styles    # 存放通用样式
    ├── template  # HtmlWebpackPlugin 模板
    ├── vendors
    ├── views     # 视图组件等
    │   ├── error-page
    │   ├── analytics
    │   ├── home
    │   ├── kitchen
    │   ├── menu
    │   ├── order
    │   ├── promotion
    │   ├── qrcode
    │   ├── home
    │   ├── kitchen
    │   ├── main-components
    │   ├── my-components
    │   ├── login.less
    │   ├── login.vue
    │   ├── main.less
    │   └── Main.vue
    ├── app.vue
    └── main.js

1.5 软件设计技术

单例设计模式

采用单例设计模式,是因为在系统逻辑中,只应该存在一个实例,重复的对象将浪费内存资源,同时便于被访问,不必考虑不同对象之间的同步问题,也避免出错给用户带来误解,保证了实例的唯一性。在实现过程中,我们使用Vuex存储相应数据,使得数据相关实例为全局唯一状态。涉及到了数据存储模块,其中’user.js’, ‘menu.js’等为数据模块的子模块,为了便于拓展。

观察者设计模式

采用观察者设计模式,能使得观察者和被观察者之间建立了一个抽象的耦合,同时被观察者会向所有的登记过的观察者发出通知。在系统中保证了页面和数据的同步,页面的数据的改变能反应到数据上,对数据进行操作,也能反映到页面上。在实现过程中,我们使用Vue框架,实现了数据与视图的统一,即观察者设计模式的实现。这一模式贯穿前端整个模块,既包含所有组件模块,又包含了数据存储模块。

2. 后端

2.1 技术选型理由

后端架构为 Flask + MySQL;使用 Docker 部署

2.2 架构设计

由于我们使用前后端分离架构,后端只需要提供 API 服务。RESTful 风格的 API 是前后端分离的最佳实践,因此后端依靠 Flask 框架为前端提供 RESTful API 服务。后端的架构由以下几个层次组成:

2.3 模块划分

数据库模型设计

根据需求,应用中需要用到的模型有Restaurant, Menu, Category, Dish, Order, OrderItem, Promotion, PromotionRule, User.

他们的属性和关系如下图所示:

Restaurant

在该应用中只有一个餐馆实例,因此我们使用了单例模式。餐馆可以用有多个菜单。

一个餐馆可以拥有多个菜单,但每个菜单都属于同一个餐馆。特别地,isUsed标识了当前使用的菜单,因此只能有一个菜单的isUsed字段值为True.菜单不直接包含菜品,而是拥有着多个类别,而类别中才包含着菜品。

Category

一个菜单可以拥有多个类别,但是一个类别只对应于一个菜单。类别中的rank字段仅用于前端显示时的显示顺序。一个类别可以拥有属于该类别的多个菜品。

Dish

菜品的rank字段仅用于前端显示时的显示顺序。一个菜品只存在于一个类别之中,但一个类别可以拥有多个不同的菜品。

Order

payId只是用于标识订单的字段,实际情况下可能并非是一个id的形式,该字段主要用于使用第三方接口时找到实际支付的信息,用此信息与订单信息核对(主要是支付金额与due字段的值核对)。注意,一个订单可以包含多个菜品,而一个菜品也可以存在于多个订单中。因此,需要一个特殊的模型来描述一个订单中的每个菜的信息,在这里,我们使用了OrderItem模型。

OrderItem

注意,一个订单可以拥有多个OrderItem,但每个OrderItem只对应于一个菜品,而每个菜品可以拥有多个OrderItem,因为每个菜品都可以在多个订单中。OrderItem中所有的字段信息都用来描述某一订单下的某一菜品。

Promotion

每个活动都可以有多个优惠的规则PromotionRule

PromotionRule

User

2.4 API设计

为了沟通前端与后端,需要提供必需的API。

用户API

菜单、类别、菜品API

订单API

2.5 组织结构

根据架构与设计的 API,我们的文件组织结构为:

.
├── Dockerfile
├── LICENSE
├── README.md
├── app
│   ├── __init__.py
│   ├── api 逻辑层,每一个文件中实现对应 api 中的操作
│   │   └── v1
│   │       ├── __init__.py
│   │       ├── analytics.py
│   │       ├── buser.py
│   │       ├── cuser.py
│   │       ├── menu.py
│   │       ├── order.py
│   │       ├── photo.py
│   │       ├── promotion.py
│   │       ├── restrt.py
│   │       ├── rule.py
│   │       └── utils.py
│   ├── config.py  配置模块,存有各项默认配置
│   ├── gen_data.py
│   ├── login.py  登录、权限验证模块
│   └── models.py  数据访问层,主要有 Flask SQLAlchemy 提供
├── docker-entrypoint.sh
├── manage.py  控制层,主要由 Flask 提供
├── requirements.txt
├── static
│   └── images
│       ├── dishes
│       │   └── default.png
│       └── restrts
│           └── default.png
└── tests 单元测试模块
    ├── test_analytics.py
    ├── test_buser.py
    ├── test_category.py
    ├── test_cuser.py
    ├── test_data.py
    ├── test_dish.py
    ├── test_menu.py
    ├── test_order.py
    ├── test_promotion.py
    ├── test_restrt.py
    └── test_rule.py

2.6 软件设计技术

The Resource-Oriented Architecture

面向资源的架构设计,主要体现在我们使用 RESTful 的 API 设计。 所有的事物都被抽象为资源,对这些资源定义无状态的增删查改操作。详细的 API 设计可见 7.3 API Design, 这些 API 的实现都在 app/api/v1 中。

Objected-Oriented Programming

把数据库的每一张表的属性和方法封装成为一个类作为数据访问层,借助 Flask SQLAlchemy 实现 CRUD 的操作,给逻辑层提供 CRUD 的接口,这样实现逻辑时就不需要关注数据库操作的细节:

db = SQLAlchemy()

class User(db.Model, UserMixin):  # 继承 db.Model 中操作数据库 CRUD 的类方法

    __tablename__ = 'users'
	
    # 定义表中的属性
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(32), doc='用户名', unique=True)
    password = db.Column(db.String(100), doc='密码', nullable=True)
    authority = db.Column(db.String(32), doc='权限, [customer, manager, cook]', nullable=False)
    register_date = db.Column(db.DateTime, nullable=True)

Decorator

把登录与权限验证功能写成一个装饰器,从而能很方便地为函数添加权限验证功能。并且通过一个装饰器生成函数,根据传进的 authority 参数生成限制对应权限的装饰器

装饰器函数定义:

def login_required(authority="ANY"):
    """Custom login required decorator.
    If the method contains {userID_filed_name} args, the decorator would check whether
    the userId is the same as current user's.
    Args:
        authority (str, optional): {ANY, customer, manager, cook} The required role of the user
    Returns:
        function: The desired decorator wrapper.
    """
    def wrapper(func):
        @wraps(func)
        def decorated_view(*args, **kwargs):
            if not current_user.is_authenticated:
                # print(current_user.authority)
                return current_app.login_manager.unauthorized()
            if authority != "customer":
                if authority == 'cook' and current_user.authority == 'customer':
                    return {'message': 'Your authority is not valid.'}, 401
                if authority == 'manager' and current_user.authority != 'manager':
                    return {'message': 'Your authority is not valid.'}, 401

            return func(*args, **kwargs)
        return decorated_view
    return wrapper

装饰器使用:

# Menu
@api.route('/')
class Menus(Resource):

    # 新建菜单
    @login_required(authority='manager')
    def post(self):
		pass

    # 获取所有菜单
    @login_required(authority='cook')
    def get(self):
		pass