SYSU-xiaoxin-catering 小欣餐饮点餐系统

The Documents about Group Project of Software Analysis & Design

View the Project on GitHub LeonhardE/Dashboard

软件架构文档

小组成员:杨元昊,曾宪欣,李帅霖,谭晓,朱宇泽,张恒瑞

​ 我们实现了一个微信点餐系统,可以为多家餐厅提供服务。其中,我们采用了前后端分离的设计思路进行微信小程序的项目开发,并利用服务器渲染的技术方案来满足餐厅管理人员的显示需求。

​ 前端分别是基于微信小程序的页面设计和Bootstrap/jquey的网页设计;后端则以 Python Flask 服务器为核心,用于提供 RESTful API和管理员操作服务;我们还搭建了 Nginx 服务器作为后端和前端的中间层,通过静态文件服务和 HTTP 代理来满足可扩展,高并发的要求。另外为了更好地提高性能,我们利用mysql-cluster实现了分布式数据库设计。

​ 微信客户端与后台的数据交互依赖 RESTful API 进行对接。这样的设计让微信客户端只关注业务逻辑的实现而不必考虑向后端请求的复杂路由,微信小程序的开发与后台的开发可以同步进行。同时我们考虑到管理员界面相较于微信客户端而言使用频率较少,但页面加载内容较多,与后台的逻辑更为紧密,我们让后台管理页面在服务器上渲染,这样我们在不用担心过高负载的情况下既减少了首次页面加载时间又提高了后台开发人员效率。

逻辑架构图

WechatIMG720

物理架构图:

2 根据以上架构,我们可以把项目划分为三个子项目,Wechat_Client, Browser_Client和App Server。

一、Wechat_Client

1.技术选型

前端采用的技术栈如下:

-WXML:微信标记语言,用于展示UI模板,类似HTML,文件后缀名为“.wxml”。 -WXSS:WeiXin Style Sheets,用于规定UI样式,具有 CSS 大部分特性。在CSS基础上扩展了尺寸单位和样式导入。文件后缀名为“.wxss” -WXS:WeiXin Script。封装后的JS,一般内嵌于WXML页面,很方便构建页面,标签为”“。 -JS:普通的JavaScript,常用于控制整个页面的逻辑。后缀名为“.js” -JSON:用于项目全局配置或单页面配置,后缀名为“.json”。 -MVVM:类似Vue.js风格。

2.架构设计

下面是开发的目录结构:

3.项目结构

下面是微信前端结构:

└──Frontend:微信小程序开发的源码
     ├─auth:用户认证获取信息
     |    ├─auth.wxml:用户登陆界面
     │    ├─auth.js:获取用户授权信息
     │    ├─auth.wxss:样式表
     |	  ├─auth.json:配置
     │
     ├─index:欢迎登陆界面
     |    ├─index.wxml:欢迎界面
     │    ├─index.js:显示用户名称与欢迎信息
     │    ├─index.wxss:样式表
     |	  ├─index.json:配置
     |
     ├─menu:菜单
     |    ├─menu.wxml:菜单界面
     │    ├─menu.js:菜单分类切换,获取分类,添加购物车等功能
     │    ├─menu.wxss:样式表
     |	  ├─menu.json:配置
     |
     ├─review:评论
     |    ├─review.wxml:评论界面
     │    ├─review.js:获取评论
     │    ├─review.wxss:样式表
     |	  ├─review.json:配置
     |    ├─write:写评论
     │         ├─write.wxml:写评论界面
     │         ├─write.js:写评论发送与评价星级
     │         ├─write.wxss:样式表
     |	       ├─write.json:配置
     |
     ├─history:历史订单
     |    ├─history.wxml:历史订单界面
     │    ├─history.js:显示历史订单详细
     │    ├─history.wxss:样式表
     |	  ├─history.json:配置
     |
     ├─order:确认订单界面
     |    ├─order.wxml:订单显示与更改
     │    ├─order.js:统计订单
     │    ├─order.wxss:样式表
     |	  ├─order.json:配置
     |
     ├─check:付款界面
     |    ├─check.wxml:显示订单与付款
     │    ├─check.js:付款,发送订单给后端
     │    ├─check.wxss:样式表
     |	  ├─check.json:配置	
     |
     └─log:日志

各个组件的逻辑如下:

4.所用的软件设计技术

减小耦合

模块之间只传递必要的数据,页面跳转时只带必要的数据,充分利用后端交互而不是本地更改。

tabMenu: function (event) {//切换类别
    let index = event.target.dataset.index;
    this.setData({
      tabIndex: index
    });
    requestmenudata(index);//从后端获取对应类别菜单
  }

结构化方法

分析后端提供ui和前端展示内容,并设计过程,使每个模块尽量只执行一个功能(坚持功能性内聚),模块间共用的信息(如参数等)尽量少。

onLoad: function () {
    let that = this;
    // 取出订单传过来的数据
    wx.getStorage({
      key: 'orders',
      success: function (res) {
        that.setData({
          items: res.data
        });
        // 价格统计汇总
        let money = 0;
        let num = res.data.length;
        res.data.forEach(item => {
          money += (item.price * item.num); // 总价格求和
        });
        let orderCount = {
          num,
          money
        }
        // 设置显示对应的总数和全部价钱
        that.setData({
          orderCount
        });
      }
    })
  }

依赖倒置原则

微信前端针对必要的用户数据,以及提供的api接口数据进行编程。

数据key绑定

绑定数据时位置可能发生改变,使用wx:key,wx:item进行定义固定位置。

<view wx:for="" class='orders' wx:for-item='outter' wx:for-index='index'>
  <!--<text class='itemtitle'>订单 \n</text>!-->
  <view wx:for='' class='list' wx:for-item='inner'>
    <image src="" class='img'></image>
    <text class='name'></text>
    <text class='price'>  价格: ¥ \n × </text>
  </view>
  <text class='state'>订单状态: 已完成</text>
  <button class='another' bindtap='gotomenu'>再来一单</button>
</view>
<view class="item" wx:for="" wx:key="">
        <image src="image/item-m.jpg"></image>
        <text class="title"></text>
        <text class="summary"></text>
        <text class="price"></text>
        <text class="" bindtap='addOrder' data-id="" data-index="">购买</text>
      </view>
    </view>

切换样式

切换样式表(如显示,隐藏,或者变成不同的样子)来完成一些逻辑与变换,比如评价星级,加入购物车等实现。

<view class="comment1-description">
    <view class="star-pos" style="display:flex;flex-direction:row;">
      <view class="stars  " bindtap="changeColor1"></view>
      <view class="stars  " bindtap="changeColor2"></view>
      <view class="stars  " bindtap="changeColor3"></view>
      <view class="stars  " bindtap="changeColor4"></view>
      <view class="stars  " bindtap="changeColor5"></view>
    </view>
  </view>

二、Browser_Client

1.技术选型及理由

项目管理员页面采用的技术栈为Bootstrap和Jquey 。

2.架构设计

管理员页面在服务器渲染,给餐厅管理者一个可视化的管理页面,项目主要有以下几个部分:

3.模块划分

根据架构设计所述, 我们绘出整体架构如下

2

4.所用的软件设计技术

页面组件复用

多个页面有许多部分是共用的,如侧边栏,tab组件等,我们使用jinjia的模板的相关知识,可以实现页面组件复用,减少代码量和修改难度,提高效率。

屏幕快照2019-06-29下午9.55.08

异步http请求

异步请求发出后,即使服务器没有响应,剩余代码也将继续执行。当存在大量IO请求时,异步方法可以大大降低网站响应时间。

jQuery.ajax({
	url: "/invite/sendEmailAjax.pt",
	type: "post",
	dataType: "text",
	async: true,
	data: "inviteEmails="+email,
	success: function(data){
		console.log(2);
	}

静态文件管理

项目使用了nginx来实现静态资源管理,当有对静态资源的请求时,将通过nginx进行转发,实现页面负载均衡。

三、APP Server

1.技术选型及理由

项目后端采用的技术栈为 Nginx + Gunicorn + Flask + MySQL 。

2.架构设计

服务端开发主要涉及 RESTful API的开发,同时还要提供私密配置项用于配置项目有关的账号密码等配置,项目主要有以下几个部分:

3.模块划分

根据业务需求,我们分析出一共需要Account、Job、Image、Label这四张表,并在此基础设计出了符合RESTful规范的API

有了以上的设计,我们很清晰地划分出了项目的模块:

└──Backend:服务端开发的源码
     ├─web:服务端渲染主要代码
     |    ├─index.py:首页显示餐厅运营数据
     │    ├─auth.py:管理用户登录注册
     │    ├─chart.py:封装好图表数据
     |	  ├─comment.py:管理餐厅评论
     |	  ├─finance.py:管理餐厅订单
     |	  ├─food.py:管理餐厅菜单
     |    ├─Login.py:管理用户信息
     |    ├─Stat.py:统计餐厅运营数据
     |    ├─upload.py:管理图片上传
     │
     ├─Viewmodel:json数据封装
     | 	  ├─Category.py:餐厅菜品种类
     │    ├─Coupon.py: 用户优惠券
     │    ├─Comment.py: 任务数据表模块 
     │    ├─Dish.py: 餐厅菜品信息
     │    └─Restaurant.py: 餐厅信息	
     |
     ├─Config:json数据封装
     │    ├─secure.py: 密钥相关
     │    └─settings.py: 应用主要常数常量
     |
     |─Form:处理服务端post请求数据
     |    ├─Auth_admin.py:用户登录注册数据预处理
     │    ├─Base.py:Form基类
     │    ├─cart.py:单个订单物品数据数据预处理
     |	  ├─comment.py:餐厅评论数据预处理
     |	  ├─Order.py:餐厅订单数据预处理
     |
     ├─libs:存放常用工具函数
     |    ├─auth.py:处理token验证
     │    ├─email.py:异步发送邮件
     |	  ├─exception_api.py:发送错误提示api
     |	  ├─MyBluePrint.py:自定义蓝图,实现api版本更新
     |	  ├─UrlManager.py:管理静态文件url
     |    ├─web_help.py:获取时间,分页数据等方法
     |
     ├─Model:数据库文件
     |    ├─adminstratior.py:管理员字段
     │    ├─base.py:model基类
     │    ├─Cart.py:单个订单类
     |	  ├─comment.py:餐厅评论
     |	  ├─Coupon.py:餐厅优惠券
     |	  ├─Dish.py:餐厅菜单
     |    ├─FoodCategory.py:菜品种类
     |    ├─FoodStockChangelog.py:餐厅库存
     |    ├─Image.py:图片
     |    ├─Order.py:用户订单
     |    ├─User.py:顾客
     |    ├─restaurant.py:餐厅信息
     |
     ├─Service:服务方法封装
     | 	  ├─Food.py:菜品库存管理
     │    ├─Login.py: 用户登录管理
     │    ├─Restaurant.py: 餐厅首页数据管理 
     │    ├─Statistics.py: 餐厅运营统计管理 
     │    └─Upload.py: 图片上传管理 	
     |
     ├─API:API主要代码
     |    ├─v1:版本1
     |         ├─Category.py:餐厅菜品种类
     |         ├─Comment.py:餐厅评论
     |         ├─Coupon.py:用户优惠券
     |         ├─Dish.py:餐厅菜品
     |         ├─Order.py:用户订单
     |         ├─Restaurant.py:餐厅信息
     |
     ├─init.py:app初始化
     ├─app.py:入口文件
     └─Pipfile:第三方包需求文件

4.所用的软件设计技术

装饰器模式

装饰器模式把复杂的功能简单化,分散化,然后在运行期间根据需要来动态组合,从而实现动态的为对象添加功能。在项目中使用了非常多的装饰器,如某些API的登录验证:

@login_required
def get(self, account_id):
    """Retrieve a single account by id."""
    ......

工厂模式

server/app/__init__.py中实现了一个create_app的函数,完成了其他模块的初始化工作之后,再返回这个 app 对象。把应用实例创建的过程交给工厂函数,通过工厂函数选择所要使用的配置,然后在run.py中创建适用于不同环境下的应用:

app = create_app(config_name)
app.run(host=app.config.get('HOST'), port=port)

面向对象编程

项目使用了面向对象编程的封装思想,按照业务逻辑将代码的数据表模块和API模块分别进行了封装,如所有数据库对象都有基类

class Base(db.Model):
    __abstract__ = True
    create_time = Column(DateTime(timezone=True), server_default=func.now() )
    status = Column(SmallInteger, default=1)
    update_time =  Column( 'update_time', Integer)


面向切面编程

面向切面编程的含义就是抽离相似逻辑, 将相似的逻辑进行封装并复用。项目中对全局错误进行了处理

@web.app_errorhandler(404)
def page_not_found(e):
    """
        AOP,处理所有的404请求
    """
    return render_template('404.html'), 404

注册器模式

注册器模式是把多个类的实例注册到一个注册器类中去,然后需要哪个类,由这个注册器类统一调取。我们为了应用的可扩展性,自定义蓝图“v1”表示api第一个版本,并将多个视图函数注册其中。

class MyBluePrint:
    def __init__(self, name):
        self.name = name
        self.mound = []

    def route(self, rule, **options):
        def decorator(f):
            self.mound.append((f, rule, options))
            return f

        return decorator

    def register(self, bp, url_prefix=None):
        if url_prefix is None:
            url_prefix = '/' + self.name
        for f, rule, options in self.mound:
            endpoint = self.name + '+' + \
                       options.pop("endpoint", f.__name__)
            bp.add_url_rule(url_prefix + rule, endpoint, f, **options)

动态赋值

我们在数据库中并不明文保存密码,而是利用动态赋值的方法来实时对保存的密文进行解密和把明文密码加密保存。

	@property
    def password(self):
        return self._pwd

    @password.setter
    def password(self, raw):
        self._pwd = generate_password_hash(raw)

    def check_pwd(self, raw):
        if not self._pwd:
            return False
        return check_password_hash(self._pwd, raw)

上下文管理器

上下文管理器允许我们在有需要的时候,精确地分配和释放资源。在项目中,我们用上下文管理器来自动实现了数据库update失败时的回滚操作

	@contextmanager
    def auto_commit(self, throw = True):
        try:
            yield
            self.session.commit()
        except Exception as e:
            self.session.rollback()
            current_app.logger.exception('%r' % e)
            if throw:
                raise e

虚拟环境

我们使用pipenv来创建一个独立(隔离)的Python环境。