1、SaaS平台-简介
SaaS平台:供应商将应用软件统一部署在自己的服务器上,客户可以根据工作实际需求,通过互联网向厂商定购所需的应用软件服务,按定购的服务多少和时间长短向软件供应商支付费用,并通过互联网获得Saas平台供应商提供的服务<br /><br />
互联网特性
SaaS软件行业知名产品NetSuite所提供的在线ERP、在线CRM等模块产品都是基于网络的,这样的优势在于不必投入任何硬件费用,也不用请专业的系统维护人员就能上网,有浏览器就可以进行ERP、CRM系统的使用。快速的实施、便捷的使用、低廉的价格都有赖于SaaS产品的互联网特性。
多重租赁(Multi-tenancy)特性
SaaS服务通常基于一套标准软件系统为成百上千的不同客户(又称为租户)提供服务。这要求SaaS服务能够支持不同租户之间数据和配置的隔离,从而保证每个租户数据的安全与隐私,以及用户对诸如界面、业务逻辑、数据结构等的个性化需求。由于SaaS同时支持多个租户,每个租户又有很多用户,这对支撑软件的基础设施平台的性能、稳定性和扩展性提出很大挑战。SaaS作为一种基于互联网的软件交付模式,优化软件大规模应用后的性能和运营成本是架构师的核心任务。 [
服务(Service)特性
SaaS使软件以互联网为载体的服务形式被客户使用,所以很多服务合约的签订、服务使用的计量、在线服务质量的保证和服务费用的收取等问题都必须加以考虑。而这些问题通常是传统软件没有考虑到的。
@可扩展(Scalable)特性
可扩展性意味着最大限度地提高系统的并发性,更有效地使用系统资源。比如应用:优化资源锁的持久性,使用无状态的进程,使用资源池来共享线和数据库连接等关键资源,缓存参考数据,为大型数据库分区。
2、业务概述
餐饮SaaS管理系统就是==【运营商】==将餐饮管理系统部署到云端,商家只需要进行付费申请使用,而无需对技术、硬件、运维等各个方面再次投入,平台一般具有下列模块:
- 运营平台:运营商管理基础数据模块【统一权限、日志、图片、数字字典、短信服务】以及商家管理的平台
- 商家平台:点餐后台核心业务,提供员工、店铺、桌台、菜品、订单、结算等功能
- 点餐平台:H5点餐平台,客户实现开桌、点餐、追加菜品等功能
相对传统餐饮行业来说:连锁店数据还不能共享,老板管理店铺不方便,而SaaS餐饮管理软件,具有餐饮O2O应用场景所有功能,如自助点餐、线上外卖、自动收银等,既节约了顾客的用餐时间又节省了成本,还方便了店铺之间的管理,更提高了店铺线上与线下的数据实时更新。

3、核心架构
3.1、通用服务
对于一个公司架构设计来说,必须思考的问题是,这个功能在现在或者将来能满足多少业务场景?如果将来有新的业务出现,是不是能够复用?或者说,需要做多大的调整才可以复用?甚至于,这个功能有没有可能对外输出,提供SaaS化的服务建立通用服务业务为了业务的敏捷,创新!有下面三个特征:
- 敏捷 业务需求变化快,变更以天甚至更短的频率计算,一个单体大型应用,庞大的开发团队对单一应用的变更变得越来越困难。将大应用变为多个小的应用组合,才能适应外部的快速变化,实现业务的敏捷。
- 解耦 随着业务的发展,业务系统之间的交互通常会变得越来越复杂。一个功能的修改可能会影响很多方面。只有将需要大量交互的功能独立,从应用中拆解出来,这样可以使得应用之间耦合度大幅下降。
- 复用 一些公共的能力通过复用,大大提高了开发效率,避免了重复建设。同时使得数据和流程可以集中得以管理和优化。
餐掌柜项目中提供的通用服务
3.2、核心业务

请求接入:
H5:客户点餐接入运营商:运营管理接入商家:商家主业务接入阿里云:OSS、ECS、部署平台接入
网关:
Nginx:反向代理,路由承压力Gateway:第二代网关服务,路由分发、权限鉴定
核心业务:
点餐平台:负责客户点餐业务,为Android、IOS、H5提供统一服务接口商家平台:负责基础数据配置,同时提供店员及交易结算服务运营平台:负责商家管理及运营报表系统
通用业务:
通用非业务系统的中台系统:权限、支付、图片、数字自动、日志中心、邮件服务等
3.3、系统架构
餐掌柜项目是基于spring-cloud-alibaba的架构体系,关于spring-cloud-alibaba,其核心组件如下:

- Sentinel
阿里巴巴开源产品,把流量作为切入点,从流量控制,熔断降级,系统负载保护等多个维度保护服务的稳定性.- Nacos
阿里巴巴开源产品,一个更易于构建云原生应用的动态服务发现,配置管理和服务管理平台.- RocketMQ
Apache RocketMQ基于Java的高性能,高吞吐量的分布式消息和流计算平台.- Dubbo
Apache Dubbo是一款高性能的Java RPC框架.- Seata
阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案.- Alibaba Cloud OSS
阿里云对象存储服务器(Object Storage Service,简称OSS),是阿里云提供的海量,安全,低成本,高可靠的云存储服务.- Alibaba Cloud Schedulerx
阿里中间件团队开发的一款分布式调度产品,支持周期性的任务与固定时间点触发任务.

展现层:负载与用户的交互,分为Android、IOS、web应用,他们都是通过访问统一的gateway网关来实现业务数据的访问
代理层:选用高性能的nginx服务,通过域名与不同servrce的绑定,通过gateway路由到不同的服务器组
权限控制层:使用无状态的JWT认证,结合Spring Security实现统一的权限控制
服务治疗:使用nacos注册中心,配置中心实现服务的治理
服务调用:使用Spring Cloud alibaba 的核心组件dubbo进行服务之间的调用
流量控制:使用 Sentinel把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性
缓冲层:spring cache 配合redis轻松无侵入的实现业务数据的缓冲
基础业务支撑:基于spring boot脚手架,轻松集成OSS图片存储、sharding-jdbc分库分表、mybatis-plush 、docker、接口文档swagger2、分布式事务seate、MySQL、RocketMQ等组件
4、项目模块
在餐掌柜项目中采用maven的分层构架,来维护整体的项目架构,各个模块相对独立,达到功能及组件的复用,从而减少开发成本的投入,避免反复造轮子的现象,整体的分层构建如下图所示:

主项目结构说明:
|——restkeeper-super 负责整个项目的模块定义,pom.xml文件定义
|
|———— restkeeper-framework 核心组件模块,主要是对各个框架集成:mybatis-plus、seata、jwt、redis等等
|
|———— restkeeper-gateway 前后端分离的边界,对外的统一接口,集成对日志client、鉴权client、knife4j组件
|
|———— restkeeper-model-basic 基础服务模块,与业务无关的组件都在这里集成
|
|———— restkeeper-model-report 报表模块,提供统一的日志报表,对各个子系统报表提供接口支撑
|
|———— restkeeper-model-security 统一鉴权模块,依赖spring-security各个gateway只需要引入简单鉴权client则实现权限控制
|
|———— restkeeper-model-shop 商家中心模块:各个主业务功能的实现,并且提供H5点餐端的dubbo接口服务
|
|———— restkeeper-model-trading 交易平台,提供商家平台支付业务的结算功能
5、数据库结构
5.1、数据库概述
遵循领域模型设计【DDD】,我们按照功能模块领域垂直把数据库分为5个库,具体库的职能以及内部所含有的表详细情况如下图所示:

带来的提升:
- 解决业务层面的耦合,业务清晰
- 能对不同业务的数据进行分级管理、维护、监控、扩展等
- 高并发场景下,垂直分库一定程度的提升IO、数据库连接数、降低单机硬件资源的瓶颈
关于==【垂直分库、水平分库、垂直分表、水平分表】==在后续的项目中我们会使用sharding-jdbc来实现
5.2、表数据冗余
根据数据库设计的第三范式:在数据库设计过程中,应该尽量消除冗余。即设计数据库时,某一个字段属于一张表,但它同时出现在另一个或多个表,且完全等同于它在其本来所属表的意义表示,那么这个字段就是一个冗余字段。
随着企业数据量与并发量不断的增加,冗余字段的存在到底是好还是坏呢?
创建一个关系型数据库设计,我们有两种选择:
尽量遵循范式理论的规约,尽可能少的冗余字段,让数据库设计看起来精致、优雅、让人心醉。
合理的加入冗余字段这个润滑剂,减少join,让数据库执行性能更高更快。
所有问题出现必然因为场景问题,针对冗余字段问题,分为两个场景:
快照场景(副本场景):交易场景大部分是数据快照,而不是冗余,用户下单时候的用户名、地址、商品名称、商品描述等,若采用关联,商品在下单后发生了更新的话再去关联查询就会导致和用户操作时的数据不一致,从而产生纠纷,例如我们在项目中的设计:

冗余场景:一般数据改动的可能性少,而查询多的场景会使用冗余,例如淘宝的店铺名称,淘宝商家中心会有这个字段,可能里面的商家论坛也有,再假设聚划算这种独立的大业务自己也存一份,再来个垂直频道电器城的后台管理也独立存一份,这种场景是由于对查询性能要求高产生的,所以必须要冗余,在业务的取舍上,肯定是对让用户更快看到信息,那么不可避免的是带来维护成本的增加,对于数据一致性问题,只要做到最终一致就可以了,分布式的CAP原则的实际应用基本都是通过牺牲数据一致性(C)来保证高可用(A)和高可靠(P), 因为这种场景大部分都是可以接受短暂的数据不一致的,对业务的影响及其微小。
在餐掌柜的项目我们遵循的原则:
项目全部采用逻辑关联,没有采用主外键约束
尽可能少使用多表关联查询。冗余是为了效率,减少join
尽可能服务独立化,查询单表化,例如:查询用户信息又需要用户的头像,处理的原则是调用【用户服务】和【附件服务】在dubbo层组装数据
5.3、数据库导入
我们使用docker-compose第一次安装mysql的时候,会自动执行初始化脚本,具体会在【第二章-基础组件安装】中演示
6、团队组织及分工

微服务,顾名思义,微服务得从两个方面去理解,什么是"微"、什么是"服务", 微 狭义来讲就是体积小、著名的"2 pizza 团队"很好的诠释了这一解释【2 pizza 团队最早是亚马逊 CEO Bezos提出来的,意思是说单个服务的设计,所有参与人从设计、开发、测试、运维所有人加起来 只需要2个披萨就够了 】,而做为微服务,具备有下列四个要素:
- 小:微服务体积小,2 pizza 团队。
- 独:能够独立的部署和运行。
- 轻:使用轻量级的通信机制和架构。
- 松:为服务之间是松耦合的。
【了解】微服务的特点:
- 单一职责:微服务中每一个服务都对应唯一的业务能力,做到单一职责
- 微:微服务的服务拆分粒度很小,例如一个用户管理就可以作为一个服务。每个服务虽小,但“五脏俱全”。
- 面向服务:面向服务是说每个服务都要对外暴露Rest风格服务接口API。并不关心服务的技术实现,做到与平台和语言无关,也不限定用什么技术实现,只要提供Rest的接口即可。
- 自治:自治是说服务间互相独立,互不干扰
- 团队独立:每个服务都是一个独立的开发团队,人数不能过多。
- 技术独立:因为是面向服务,提供Rest接口,使用什么技术没有别人干涉
- 前后端分离:采用前后端分离开发,提供统一Rest接口,后端不用再为PC、移动段开发不同接口
- 数据库分离:每个服务都使用自己的数据源
- 部署独立,服务间虽然有调用,但要做到服务重启不影响其它服务。有利于持续集成和持续交付。每个服务都是独立的组件,可复用,可替换,降低耦合,易维护
第二章 项目快速启动
1、基础组件安装
首先想启动餐掌柜SaaS平台,则需要安装下列三方组件:
- 缓存服务:redis组件
- 注册配置中心:nacos平台
- 数据库:mysql
- 消息中间件:rabbitmq
- 分布式事务:seata-server
- 分布式调度中心:xxl-job-admin
- 反向服务器:nginx
这里我们采用docker-compose来进行安装,需要特别注意的:宿主机地址请设置为:192.168.112.77,【宿主机地址设置见课程资料】
1.1、docker安装
卸载老版本docker
yum remove docker
docker-client
docker-client-latest
docker-common
docker-latest
docker-latest-logrotate
docker-logrotate
docker-selinux
docker-engine-selinux
docker-engine
docker-ce设置仓库
yum install -y yum-utils device-mapper-persistent-data lvm2
更新本地镜像库
yum-config-manager —add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
更新镜像源缓存
sed -i ‘s/download.docker.com/mirrors.ustc.edu.cn/docker-ce/g’ /etc/yum.repos.d/docker-ce.repo
yum makecache fast
安装docker
yum install -y docker-ce
关闭防火墙
systemctl stop firewalld
systemctl disable firewalld
启动docker
systemctl start docker
开机自启动docker
systemctl enable docker
执行 docker version,查看安装情况

国内从 DockerHub 拉取镜像有时会遇到困难,此时可以配置镜像加速器。Docker 官方和国内很多云服务商都提供了国内加速器服务,
当配置某一个加速器地址之后,若发现拉取不到镜像,请切换到另一个加速器地址。国内各大云服务商均提供了 Docker 镜像加速服务,建议根据运行 Docker 的云平台选择对应的镜像加速服务。
阿里云镜像获取地址:https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors,登陆后,左侧菜单选中镜像加速器就可以看到你的专属地址了:

之前还有 Docker 官方加速器 https://registry.docker-cn.com ,现在好像已经不能使用了,我们可以多添加几个国内的镜像,如果有不能使用的,会切换到可以使用个的镜像来拉取。
配置镜像加速器
mkdir -p /etc/docker
配置镜像加速地址
tee /etc/docker/daemon.json <<-‘EOF’
{
“registry-mirrors”: [“https://3h1x6xgs.mirror.aliyuncs.com“]
}
EOF重启docker服务
systemctl daemon-reload
systemctl restart docker
1.2、docker-compose安装
安装docker-compose
curl -L “https://get.daocloud.io/docker/compose/releases/download/1.27.3/docker-compose-
-#card=math&code=%28uname%20-s%29-&id=bkwTB)(uname -m)” -o /usr/local/bin/docker-compose
修改docker-compose权限
chmod +x /usr/local/bin/docker-compose
ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
执行docker-compose version 查看安装情况

docker-compose —help 仓库其命令
[root@localhost docker-demo]# docker-compose --help利用Docker来定义和构建一个多容器的应用使用方式:docker-compose [-f <arg>...] [options] [COMMAND] [ARGS...]docker-compose -h|--helpOptions:-f, --file FILE 指定一个 compose 文件,(默认: docker-compose.yml)-p, --project-name NAME 指定project名字(默认: 目录名称)--verbose 显示更多日志--log-level LEVEL 日志级别 (DEBUG, INFO, WARNING, ERROR, CRITICAL)-v, --version 打印版本并退出-H, --host HOST Daemon socket to connect toCommands:build 构建多个serviceconfig 校验 Compose 文件,格式是否正确,若正确则显示配置,若格式错误显示错误原因down 停止并删除 容器, 网络, 镜像, 和 数据卷exec 进入一个指定的容器help Get help on a commandimages 列出该Compose中包含的各个镜像kill 通过发送 SIGKILL 信号来强制停止服务容器格式为 docker-compose kill [options] [SERVICE...]logs 查看服务容器的输出日志格式为 docker-compose logs [options] [SERVICE...]。pause 暂停一个容器port 打印某个容器端口所映射的公共端口ps 列出项目中目前的所有容器pull 拉取服务依赖的镜像push 推送服务依赖的镜像到 Docker 镜像仓库restart 重启项目中的服务rm 删除停止的容器(要先停止容器)run 在某个服务上运行指令scale 设定某个容器的运行个数start 启动多个 servicesstop 停止多个 servicestop 查看各个服务容器内运行的进程。unpause 恢复处于暂停状态中的服务。up 创建并启动多个service的容器version Show the Docker-Compose version information
1.3、初始化组件
上面我们完成docker和docker-compose的安装,下面我们来安装基础组件,首先把【restkeeper-day-01\03-项目资料】的文件上传到你centos中的==home==目录:<br />其中docker-compose.yml文件内容如下:
version: '3.5'services:#mysql数据库脚本mysql:image: mysql:5.7container_name: mysqlrestart: alwaysports:- 3306:3306volumes:- ./mysql/mydir:/mydir- ./mysql/data:/var/lib/mysql- ./mysql/conf/my.cnf:/etc/my.cnf- ./mysql/source:/docker-entrypoint-initdb.d/environment:MYSQL_ROOT_PASSWORD: passnetworks:extnetwork:ipv4_address: 172.21.0.2#nacos服务脚本nacos:image: nacos/nacos-server:1.4.0container_name: nacosrestart: alwaysports:- "8848:8848"environment:SPRING_DATASOURCE_PLATFORM: mysql #数据源平台 仅支持mysql或不保存emptyMODE: standaloneMYSQL_SERVICE_HOST: mysqlMYSQL_SERVICE_DB_NAME: nacosMYSQL_SERVICE_PORT: 3306MYSQL_SERVICE_USER: rootMYSQL_SERVICE_PASSWORD: passNACOS_APPLICATION_PORT: 8848JVM_XMS: 512mJVM_MMS: 256mJVM_XMN: 128mnetworks:extnetwork:ipv4_address: 172.21.0.3depends_on:- mysql#seata服务脚本seata-server:image: seataio/seata-server:1.3.0container_name: seata-serverrestart: alwaysports:- "9200:9200"volumes:- ./seata-server/config:/root/seata-configenvironment:#如果部署到阿里云上,需要修改此地址为阿里云宿主机地址SEATA_IP: 192.168.112.77SEATA_CONFIG_NAME: file:/root/seata-config/registrySEATA_PORT: 9200networks:extnetwork:ipv4_address: 172.21.0.4depends_on:- nacos#rabbitmq脚本rabbitmq:image: rabbitmq:3.8.3-managementcontainer_name: rabbitmqrestart: alwaysports:- 15672:15672- 5672:5672volumes:- ./rabbitmq/data:/var/lib/rabbitmqenvironment:RABBITMQ_DEFAULT_USER: adminRABBITMQ_DEFAULT_PASS: passnetworks:extnetwork:ipv4_address: 172.21.0.5xxl-job:image: xuxueli/xxl-job-admin:2.1.2container_name: xxl-job-adminrestart: alwaysports:- 8280:8080volumes:- ./xxl-job/data:/data/applogsenvironment:PARAMS: "--spring.datasource.url=jdbc:mysql://mysql:3306/xxl-job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai --spring.datasource.username=root --spring.datasource.password=pass"networks:extnetwork:ipv4_address: 172.21.0.9depends_on:- mysqlredis:image: redis:5.0.0container_name: redisrestart: alwayscommand: redis-server --requirepass passports:- 6379:6379volumes:- ./redis/data:/datanetworks:extnetwork:ipv4_address: 172.21.0.10# docker容器内网地址networks:extnetwork:name: extnetworkipam:config:- subnet: 172.21.0.0/16
在home目录下运行下列命令,整个创建过程需要一定的时间【5分钟左右】,启动完成执行命令:
docker-compose up -d
查看启动情况:
docker-compose ps -a

校验mysql数据库
同步mysql时间
docker cp /usr/share/zoneinfo/Asia/Shanghai mysql:/etc/localtime
==如果你要部署到阿里云服务器上,请执行下列脚本==
UPDATE config_info cSET c.content = REPLACE ( c.content, '192.168.112.77', '阿里云外网地址' )WHEREc.content LIKE '%192.168.112.77%'
==账号:root 密码:pass==

校验nacos平台
==http://192.168.112.77:8848/nacos 账号:nacos 密码:nacos==
校验rabbitMQ平台
http://192.168.112.77:15672 admin pass
校验xxl-job平台
==http://192.168.112.77:8280/xxl-job-admin/ 账号:admin 密码:123456==
校验seata-server:
seata-server我们使用的是nacos,只需要在服务列表中看见seata-server
校验redis平台
使用RedisDesktopManager连接192.168.112.77端口6379 ==记得输入密码:pass==
至此我们的基本组件安装完成
2、运营平台启动
在启动运营平台之前我们需要先来看下其调用的链路
2.1、调用链路

上图为运营平台的调用链路,从中我们可用看到:
- 用户发起请求访问restkeeper-vue-operator,在开发阶段我们在vue中模拟设置网关,生产中我们使用Nginx做网关设置
- vue中网关会调用restkeeper-gateway-operator网关,然后由restkeeper-gateway-operator网关路由对应业务系统web项目
- model-basic-log-client模块主要负责日志的收集,model-security-client模块主要负责统一鉴权【后面分析】
- 运营平台中有2个消费者核心web模块:model-security-web、model-basic-web的消费者模块
- 运营平台中有2个生产者核心producer模块:model-security-producer、model-basic-producer
我们需要启动上述的6个模块才可把运营平台启动
2.2、服务启动
2.2.1、后端服务启动
找到需要启动的模块下com.itheima.restkeeper包下以***Start结尾的启动类,直接启动即可,例如:

启动后端服务,包含下列6个模块:
- model-basic-job-listen
- model-security-producer
- model-basic-producer
- model-security-web
- model-basic-web
- gateway-operato
启动完成访问:http://192.168.112.77:8848/nacos,
注意:model-basic-job-listen模块不需要注册到nacos中,seata-server为分布式事务服务

2.2.2、vue项目启动
想使用vue必须先安装node.js:https://nodejs.org/zh-cn/,这里我下载的版本为v12.21版本,课程资料中有对应的安装包

安装直接全部点击下一步最后完成安装
安装结束后打开cmd命令窗口 输入以下命令验证是否安装成功,如出现版本号则安装成功
#查看node版本node -v#查看npm版本npm -v

安装成功后,在cmd命令窗口使用如下命令安装npm的国内镜像cnmp
npm install -g cnpm --registry=http://registry.npm.taobao.org
第一次运行项目之前我们需要执行下列命令,安装项目
npm install
在cmd窗口中切换到restkeeper-vue-operator项目所在模块:

==【非必须执行】==如果安装中有失败,这边可用在host
中做如下配置,这里是让github更快加载
140.82.114.4 github.com199.232.69.194 github.global.ssl.fastly.net185.199.108.153 assets-cdn.github.com185.199.110.153 assets-cdn.github.com185.199.111.153 assets-cdn.github.com
cmd中切换restkeeper-vue-operator项目所在目录,使用下列命令启动项目:
npm run dev

==注意:这里默认打开的访问路径为:http://localhost/#/login,但是我们的系统是基于saas系统,这里需要绑定域名,但是我们没有域名,所以这里需要设置hosts,再次找到hosts==
添加如下配置:
127.0.0.1 www.eehp.cn127.0.0.1 ppsk.shop.eehp.cn
访问http:// www.eehp.cn 账号:admin@qq.com 密码:pass
2.2.3、IDEA启动vue项目
使用idea启动vue项目,在上面安装好node.js环境并初始化完成和安装好依赖的前提下,打开idea,然后在File–Settings–Plugins–Makerplace下找到vue.js插件,安装并重启idea
第三章 项目开发规范
在项目开发过程中,如果开发人员过多又没有一个开发统一规范,在后期的维护、迭代升级的过程中会相当痛苦,也不利于新人接收项目,项目规范是为了统一一个开发标准,让代码易懂便于维护升级,下面我们为整个系统指定如下规范
1、基础父类定义
1.1、BasicPojo
==各个模块中POJO对象继承的基础父类==
隶属模块:framework-mybatis-plus
包路径:com.itheima.restkeeper.basic
作用:所有实体类公共字段的定义
其中结构如下:
package com.itheima.restkeeper.basic;import com.baomidou.mybatisplus.annotation.FieldFill;import com.baomidou.mybatisplus.annotation.TableField;import com.fasterxml.jackson.annotation.JsonFormat;import com.itheima.restkeeper.utils.ToString;import lombok.Data;import lombok.NoArgsConstructor;import org.springframework.format.annotation.DateTimeFormat;import java.io.Serializable;import java.util.Date;/*** @Description:实体基础类*/@Data@NoArgsConstructorpublic class BasicPojo implements Serializable {//主键@JsonFormat(shape = JsonFormat.Shape.STRING)public Long id;//分片键@TableField(fill = FieldFill.INSERT)@JsonFormat(shape = JsonFormat.Shape.STRING)public Long shardingId;//创建时间@TableField(fill = FieldFill.INSERT)//INSERT代表只在插入时填充@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")//set@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")//getpublic Date createdTime;//修改时间@TableField(fill = FieldFill.INSERT_UPDATE)// INSERT_UPDATE 首次插入、其次更新时填充(或修改)@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")//set@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")//getpublic Date updatedTime;//是否有效@TableField(fill = FieldFill.INSERT)public String enableFlag;//构造函数public BasicPojo(Long id) {this.id = id;}}
1.2、BasicVo
各个模块中VO对象继承的基础父类**隶属模块:**framework-vo**包路径:**com.itheima.restkeeper.basic**作用:**所有VO对象公共字段的定义
其中结构如下:
package com.itheima.restkeeper.basic;import com.fasterxml.jackson.annotation.JsonFormat;import io.swagger.annotations.ApiModelProperty;import lombok.Data;import lombok.NoArgsConstructor;import java.io.Serializable;import java.util.Date;/*** @ClassName BasicVo.java* @Description 基础请求*/@Data@NoArgsConstructorpublic class BasicVo implements Serializable {@ApiModelProperty(value = "主键")@JsonFormat(shape = JsonFormat.Shape.STRING)private Long id;@ApiModelProperty(value = "数据源分片Id")@JsonFormat(shape = JsonFormat.Shape.STRING)private Long shardingId;@ApiModelProperty(value = "创建时间")@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")//getprotected Date createdTime;@ApiModelProperty(value = "修改时间")@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")//getprotected Date updatedTime;@ApiModelProperty(value = "是否有效")protected String enableFlag;public BasicVo(Long id) {this.id = id;}}
1.3、IBasicEnum
各个模块中枚举对象实现的基础父接口
**隶属模块:**framework-vo**包路径:**com.itheima.restkeeper.basic**作用:**枚举对象实现的基础方法定义
其中结构如下:
package com.itheima.restkeeper.basic;/*** @Description:枚举接口*/public interface IBasicEnum {//编码public String getCode();//信息public String getMsg();}
2、统一返回数据
2.1、ResponseWrap
所有对前端接口的返回对象
**隶属模块:**framework-vo**包路径:**com.itheima.restkeeper.basic**作用:**定义返回对象的包装
其中结构如下:
package com.itheima.restkeeper.basic;import com.fasterxml.jackson.annotation.JsonFormat;import io.swagger.annotations.ApiModelProperty;import lombok.AllArgsConstructor;import lombok.Builder;import lombok.Data;import lombok.NoArgsConstructor;import java.io.Serializable;import java.util.Date;/*** @Description 返回结果*/@Data@Builder@AllArgsConstructor@NoArgsConstructorpublic class ResponseWrap<T> implements Serializable {//响应返回编码@ApiModelProperty(value = "状态码")private String code;//响应返回信息@ApiModelProperty(value = "状态信息")private String msg;//返回结果@ApiModelProperty(value = "返回结果")private T datas;//操作用户@ApiModelProperty(value = "操作人ID")private Long userId;//操作用户名称@ApiModelProperty(value = "操作人姓名")private String userName;//创建时间,处理json的时间参数解析@JsonFormat(pattern = "yyyy-MM-dd hh:mm:ss",timezone = "GMT+8")@ApiModelProperty(value = "操作时间")private Date operationTime;}
2.2、ResponseWrapBuild
构造ResponseWrap工具
**隶属模块:**framework-commons**包路径:**com.itheima.restkeeper.utils**作用:**构造ResponseWrap工具,并且从UserVoContext上下文中获取当前登录对象作为操作人信息填入
其中结构如下:
package com.itheima.restkeeper.utils;import com.alibaba.fastjson.JSONObject;import com.itheima.restkeeper.basic.IBasicEnum;import com.itheima.restkeeper.basic.ResponseWrap;import com.itheima.restkeeper.req.UserVo;import java.util.Date;/*** @Description 构造ResponseWrap工具*/public class ResponseWrapBuild {public static <T>ResponseWrap<T> build(IBasicEnum basicEnumIntface, T t){//从UserVoContext中拿到userVoStringString userVoString = UserVoContext.getUserVoString();UserVo userVo = null;if (!EmptyUtil.isNullOrEmpty(userVoString)){userVo = JSONObject.parseObject(userVoString, UserVo.class);}else {userVo = new UserVo();}//构建对象return ResponseWrap.<T>builder().code(basicEnumIntface.getCode()).msg(basicEnumIntface.getMsg()).operationTime(new Date()).userId(userVo.getId()).userName(userVo.getUsername()).datas(t).build();}}
3、vo与pojo
VO :value object 值对象 / view object 表现层对象,主要职能
作用:
- 对应页面显示的数据对象
- 页面请求参数封装
- dubbo层调用多个服务,服务返回结果封装 例如:用户信息——>【用户服务】返回VO+【附件服务】返回图片Vo
POJO :plain ordinary java object 无规则简单java对象,在餐掌柜中严格遵循POJO的实体类结构使用【驼峰规则】映射数据库字段
3.1、作用范围
以restkeeper-model-security模块为例,一个标准的模块其模块结构如下
|——restkeeper-model-security 统一权限模块
|
|———— model-security-interface dubbo接口定义层【被生产者、消费者依赖】
|
|———— model-security-producer dubbo接口实现【生产者】
|
|———— model-security-service 核心业务开发【最小开发单元】
|
|———— model-security-web 对外接口服务层【消费者】
VO和POJO的负责区域入下图所示:

其中model-security-producer是VO与POJO对象转换层。
3.2、对象转换工具
VO对象与POJO对象属成员变量拷贝工具
**隶属模块:**framework-commons**包路径:**com.itheima.restkeeper.basic**作用:**VO对象与POJO对象属成员变量拷贝工具**注意:**VO对象与POJO对象属成员变量【类型】和【成员变量名称】必须保持一致,支持父类属性拷贝
package com.itheima.restkeeper.utils;import lombok.extern.slf4j.Slf4j;import ma.glasnost.orika.MapperFacade;import ma.glasnost.orika.MapperFactory;import ma.glasnost.orika.MappingContext;import ma.glasnost.orika.converter.BidirectionalConverter;import ma.glasnost.orika.converter.ConverterFactory;import ma.glasnost.orika.impl.DefaultMapperFactory;import ma.glasnost.orika.metadata.Type;import java.time.LocalDate;import java.time.LocalDateTime;import java.time.LocalTime;import java.util.List;/*** @Description 对象转换工具*/@Slf4jpublic class BeanConv {private static MapperFacade mapper;private static MapperFacade notNullMapper;static {MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();ConverterFactory converterFactory = mapperFactory.getConverterFactory();converterFactory.registerConverter(new LocalDateTimeConverter());converterFactory.registerConverter(new LocalDateConverter());converterFactory.registerConverter(new LocalTimeConverter());mapper = mapperFactory.getMapperFacade();MapperFactory notNullMapperFactory =new DefaultMapperFactory.Builder().mapNulls(false).build();notNullMapper = notNullMapperFactory.getMapperFacade();}private static class LocalDateTimeConverterextends BidirectionalConverter<LocalDateTime, LocalDateTime> {@Overridepublic LocalDateTime convertTo(LocalDateTime localDateTime,Type<LocalDateTime> type,MappingContext mappingContext) {return LocalDateTime.from(localDateTime);}@Overridepublic LocalDateTime convertFrom(LocalDateTime localDateTime,Type<LocalDateTime> type,MappingContext mappingContext) {return LocalDateTime.from(localDateTime);}}private static class LocalDateConverterextends BidirectionalConverter<LocalDate, LocalDate> {@Overridepublic LocalDate convertTo(LocalDate localDate,Type<LocalDate> type,MappingContext mappingContext) {return LocalDate.from(localDate);}@Overridepublic LocalDate convertFrom(LocalDate localDate,Type<LocalDate> type,MappingContext mappingContext) {return LocalDate.from(localDate);}}private static class LocalTimeConverterextends BidirectionalConverter<LocalTime, LocalTime> {@Overridepublic LocalTime convertTo(LocalTime localTime,Type<LocalTime> type,MappingContext mappingContext) {return LocalTime.from(localTime);}@Overridepublic LocalTime convertFrom(LocalTime localTime,Type<LocalTime> type,MappingContext mappingContext) {return LocalTime.from(localTime);}}/*** 复制对象所有属性* @param source 源对象* @param destination 目标对象*/public static void toBean(Object source, Object destination) {mapper.map(source, destination);}/*** 复制对象非null属性** @param source 源对象* @param destination 目标对象*/public static void toBeanNotNull(Object source, Object destination) {notNullMapper.map(source, destination);}/*** 深度复制对象* @param source 源对象* @param destinationClass 目标类型* @return 复制出的目标对象*/public static <T> T toBean(Object source, Class<T> destinationClass) {if (EmptyUtil.isNullOrEmpty(source)){return null;}return mapper.map(source, destinationClass);}/*** 复制List* @param sourceList 源List* @param destinationClass 目标List的元素类型* @return 复制出的目标List*/public static <T> List<T> toBeanList(List<?> sourceList, Class<T> destinationClass) {if (EmptyUtil.isNullOrEmpty(sourceList)){return null;}return mapper.mapAsList(sourceList,destinationClass);}}
单个对象拷贝:

List对象拷贝:

4、异常处理
在项目开发过程中,异常是需要统一,如图所示,service—>producer—>interface—>web逐层向上抛出:
首先,自定义异常类ProjectException
隶属模块:framework-web
包路径:com.itheima.restkeeper.exception
作用:统一异常处理自定义异常类
package com.itheima.restkeeper.exception;import com.itheima.restkeeper.basic.IBasicEnum;/*** @Description:自定义异常*/public class ProjectException extends RuntimeException {//错误编码private String code;//提示信息private String message;//异常接口private IBasicEnum basicEnumIntface;public ProjectException() {}public ProjectException(IBasicEnum basicEnumIntface) {this.code = basicEnumIntface.getCode();this.message = basicEnumIntface.getMsg();this.basicEnumIntface = basicEnumIntface;}public String getCode() {return code;}public void setCode(String code) {this.code = code;}@Overridepublic String getMessage() {return message;}public void setMessage(String message) {this.message = message;}public IBasicEnum getBasicEnumIntface() {return basicEnumIntface;}public void setBasicEnumIntface(IBasicEnum basicEnumIntface) {this.basicEnumIntface = basicEnumIntface;}@Overridepublic String toString() {return "ProjectException{" +"code='" + code + '\'' +", message='" + message + '\'' +'}';}}
再定义BaseController类采用SpringMVC的统一异常处理,使用@ControllerAdvice、@ExceptionHandler注解,优雅的处理异常
隶属模块:framework-web
包路径:com.itheima.restkeeper.web
作用:统一异常处理
package com.itheima.restkeeper.web;import com.alibaba.fastjson.JSON;import com.itheima.restkeeper.basic.ResponseWrap;import com.itheima.restkeeper.enums.BasicEnum;import com.itheima.restkeeper.exception.ProjectException;import com.itheima.restkeeper.utils.ExceptionsUtil;import com.itheima.restkeeper.utils.ResponseWrapBuild;import lombok.extern.log4j.Log4j2;import lombok.extern.slf4j.Slf4j;import org.apache.dubbo.rpc.RpcException;import org.springframework.web.bind.annotation.ControllerAdvice;import org.springframework.web.bind.annotation.ExceptionHandler;import javax.servlet.http.HttpServletResponse;import java.io.IOException;/*** @ClassName BaseController.java* @Description 基础的controller* ControllerAdvice:对controller层的增强,其他的controller则不需要继承,也会被拦截处理*/@ControllerAdvice@Slf4jpublic class BaseController {//表示当请求发生异常时,被ExceptionHandler注释的方法会去处理@ExceptionHandlerpublic void ExceptionHandler(Exception ex, HttpServletResponse response) throws IOException {ResponseWrap<Object> responseWrap = null;//自定义异常if (ex instanceof ProjectException){ProjectException projectException = (ProjectException) ex;responseWrap = ResponseWrapBuild.build(projectException.getBasicEnumIntface(), null);//远程调用异常}else if (ex instanceof RpcException){responseWrap = ResponseWrapBuild.build(BasicEnum.DUBBO_FAIL, null);} else {//系统异常responseWrap = ResponseWrapBuild.build(BasicEnum.SYSYTEM_FAIL, null);log.error("系统异常:{}",ExceptionsUtil.getStackTraceAsString(ex));}//编码防止中文问题response.setContentType("application/json;charset =utf-8");response.getWriter().write(JSON.toJSONString(responseWrap));}}
4.2、异常枚举
为了规范异常处理后返回的信息,我们为每一个模块定义了异常处理枚举:
隶属模块:framework-vo
包路径:com.itheima.restkeeper.enums
作用:各模块的异常枚举
需要注意,每个enum都需要实现IBasicEnum接口,以UserEnum为例:
package com.itheima.restkeeper.enums;import com.itheima.restkeeper.basic.IBasicEnum;/*** @ClassName UserEnum.java* @Description TODO*/public enum UserEnum implements IBasicEnum {SUCCEED("200","操作成功"),LOGOUT_SUCCEED("1004","退出成功"),FAIL("1000","操作失败"),PAGE_FAIL("45001", "查询用户列表失败"),CREATE_FAIL("45002", "保存用户失败"),UPDATE_FAIL("45003", "修改用户失败"),DELETE_FAIL("45004", "修改用户失败"),SELECT_USER_FAIL("45005", "查询用户失败"),SELECT_ROLE_FAIL("45006", "查询用户对应角色失败"),SELECT_RESOURCE_FAIL("45007", "查询用户对应资源失败"),SELECT_CURRENT_USER("45008", "查询当前用户失败"),SELECT_USER_LIST_FAIL("45009", "查询用户list失败"),;private String code;private String msg;UserEnum(String code, String msg) {this.code = code;this.msg = msg;}public String getCode() {return code;}public String getMsg() {return msg;}}
课堂讨论
1、什么是SaaS平台,他有什么的特点?
2、餐掌柜业务有哪些,业务流程是什么样的?
3、你能说餐掌柜的核心架构【通用服务、核心业务、系统架构】、数据库结构吗?
4、实际开发过程中你们是怎么进行工作协调的?
5、spring-cloud-alibaba的生态圈有哪些,各个组件的作用是什么?
6、餐掌柜需要安装哪些组件,各个组件的作用是什么?
7、运营平台采用怎样的maven分层构建,每个模块有什么作用?
课后任务
1、完成当天课堂讨论,同步到git【☆☆☆☆☆】
2、完成2-5道sql练习【☆☆☆☆】
3、手绘:项目模块,数据库结构图形
4、启动运营商系统



