一、项目简介
1、项目背景
1)、电商模式
市面上有5种常见的电商模式:B2B、B2C、C2B、C2C、O2O;
- B2B模式
Business to Business,是指商家之间的商业关系,如:阿里巴巴;
- B2C模式
Business to Consumer,就是我们经常看到的供应商直接把商品卖给用户,即“商对客”模式,也就是通常说的商业零售,直接面向消费者销售产品和服务。如:苏宁易购、京东、天猫等;
- C2B模式
Customer to Business,即消费者对企业。先有消费者提出需求,后有企业组织生产;
- C2C模式
Customer to Consumer,客户之间的交易,如:淘宝、咸鱼等;
- O2O模式
Online to Offline,即是将线下商务的机会与互联网结合,让互联网称为线下交易的前台。线上快速支付,线下优质服务。如:饿了么、美团、淘票票、京东到家等;
2、架构图
技术架构图
服务架构图
3、项目技术
- 前后分离开发,基于vue的后台管理系统
- SpringCloud全新解决方案
- 应用监控、限流、网关、熔断降级等分布式方案
- SpringBoot
- SpringCloud
- Nacos、Sentinel、Elasticsearch、git、redis、linux、vue、k8s
二、分布式基础概念
1、微服务
微服务架构风格,就像是把一个单独的应用程序开发为一套小服务,每个小服务运行在自己的进程中,并使用轻量级机制通信,通常是HTTP API。这些服务围绕业务能力来构建,并通过完全自动化部署机制来独立部署。这些服务可以使用不同的编程语言书写,以及不同数据存储技术,并保持最低限度的集中式管理。
简而言之:拒绝大型单体应用,基于业务边界进行服务微化拆分,各个服务独立部署运行。
2、集群&分布式&节点
集群是个物理形态,分布式是个工作方式。
只要是一堆机器,就可以叫集群,他们是不是一起协作工作,这个谁也不知道。
《分布式系统原理与规范》
定义:
“分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统”
分布式系统(distribution system)是建立在网络之上的软件系统。
分布式是指将不同的业务分布在不同的地方。
集群是指将几台服务器集中在一起,实现同一业务。
例如:京东是一个分布式系统,众多业务运行在不同的机器,所有业务构成一个大型业务集群。每一个小的业务,比如用户系统,访问压力大时一个服务器不够用,我们就应该将用户系统部署到多个服务器,也就是每一个业务系统都可以做集群。
分布式中的每一个节点都可以做集群。集群并不一定是分布式。
节点:集群中的一个服务器。
3、远程调用
在分布式系统中,各个服务可能处于不同主机,但服务之间不可避免要互相调用,我们称为远程调用。
SpringCloud中使用HTTP + JSON的方式来完成远程调用
4、负载均衡
分布式系统中,A服务调用B服务,B服务部署集群,A调用任意一个均可完成功能。
为了使每一台服务器都不要太忙或太闲,我们可以负载均衡的调用每一个服务器,提升网站的健壮性。
常见的负载均衡算法:
**轮询:**按照顺序访问每一台服务器。
**最小连接:**优先选择连接数最少,也就是压力最小的服务器。
**散列:**根据请求源IP的hash值来选择要转发的服务器。可以解决session的问题。
5、服务注册/发现&注册中心
A服务调用B服务,但不知道B服务在哪台服务器上,哪些是正常或下线的。解决这个问题可以引入注册中心;
我们可以实时感知到服务的状态,从而避免调用不可用的服务。
6、配置中心
每一个服务最终都有大量配置,并且每个服务都可能部署在多台服务器上。我们经常需要变更配置,因此工作量巨大。我们可以引入配置中心,让每个服务在配置中心获取自己的配置。
**配置中心用来集中管理微服务的配置信息 **
7、服务熔断&服务降级
在微服务架构中,微服务之间通过网络通信,存在相互依赖,当一个服务不可用,有可能出现雪崩效应。要防止这样的情况,必须要有容错机制。
1)、服务熔断
设置服务的超时,当被调用的服务经常失败到达某个阈值,我们可以开启断路保护机制,后来的请求不再去调用这个服务。本地直接返回默认数据。
2)、服务降级
在运维期间,当系统处于高峰期,系统资源紧张,我们可以让非核心业务奖及运行。
降级:某些服务不处理,或者简单处理——抛异常 | 返回Null、调用Mock数据、调用faullback处理逻辑。
8、API网关
在微服务架构中,API Gateway作为整体架构的重要组件,它抽象了微服务中都需要的功能,同时提供客户端负载均衡,服务自动熔断,灰度发布,统一认证,限流留空、日志统计等丰富功能,帮助我们解决很多API管理难题。
三、环境搭建
-
安装Linux虚拟机
本次项目使用阿里的云服务器:ip:112.124.32.136
5、 开发环境
- Maven
- Idea插件
lombok
mybatisx
- VS Code 插件
- Git
6、创建项目微服务
商品服务、仓储服务、订单服务、优惠券服务、用户服务
共同:
- 依赖: web、openfeign
- groupId: com.qhit.shuang
- 包名: com.qhit.shuang.xxx(product/ware/order/coupon/member)
- 模块名: shuang-xxx((product/ware/order/coupon/member)
.gitignore添加以下内容
**/mvnw
**/mvnw.cmd
**/.mvn
**/target/
.idea
**/.gitignore
四、后台管理系统
本项目的后台管理系统采用人人开源提供的后台系统快速进行搭建
接下来使用renren-generator对各个模块进行代码生成
成功生成!
接下来创建公共模块shuang-common
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.21</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpcore -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.4.15</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>${commons.lang.version}</version>
</dependency>
<!--AOP-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
五、技术整合
1、mybatis-plus
1)、引入依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
2)、配置
-
配置数据源
1)、导入数据库驱动
<!--mysql驱动--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.21</version> </dependency>
2)、在application.yml中配置数据源
spring: # 数据源配置 datasource: type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.mysql.cj.jdbc.Driver url: jdbc:mysql://112.124.32.136:3306/shuang_oms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: root
-
配置mybatis-plus
1)、启动类添加MapperScan注解
@MapperScan("com.qhit.shuang.product.dao") //dao层接口全路径
2)、application.yml中配置sql映射文件位置
mybatis-plus: mapper-locations: classpath:/mapper/**/*.xml
2、微服务
1)、简介
SpringCloud的几大痛点
- 部分组件停止维护和更新,给开发带来不便
- 部分环境搭建复杂,没有完善的可视化界面,我们需要大量的二次开发和定制
- 配置复杂,难以上手,部分配置差别难以区分和合理应用
SpringCloud Alibaba的优势
- 性能强悍,设计合理,开源
- 成套的产品搭配,完善的可视化界面
- 搭建简单
结合Alibaba最终的技术搭配方案
- SpringCloud Alibaba - Nacos:
注册中心
(服务发现和注册) - SpringCloud Alibaba - Nacos:
配置中心
(动态配置管理) - SpringCloud - Ribbon:
负载均衡
- SpringCloud - Open Feign:
远程调用
(声明式HTTP客户端) - SpringCloud Alibaba - Sentinel:
服务容错
(限流、降级、熔断) - SpringCloud - Gateway:API
网关
(webflux编程模式) - SpringCloud - Sleuth:
调用链监控
- SpringCloud Alibaba - Seata:原Fescar,即
分布式事务
解决方案
github地址:https://github.com/alibaba/spring-cloud-alibaba
版本对应关系
2)、远程调用
-
引入
open-feign
依赖<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
-
编写本地
接口
作为远程方法映射
@FeignClient("服务名:application.name") public interface 接口名:自定义|示例:feign/UserFeignService.java { @RequestMapping("远程方法全路径") public R list(@RequestParam Map<String, Object> params); // 远程方法签名 }
-
开启远程调用功能
启动类上添加注解
@EnableFeignClients(basePackages = "映射接口所在包的全路径,以‘.’分隔")
3)、API网关
当请求到达网关,网关会断言请求是否符合某个路由规则,若符合,就按照路由规则将请求路由到指定服务,中间会经过一系列过滤器
- 创建项目shuang-gateway
- 添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifact>
</dependency>
- 使用断言
spring:
cloud:
gateway:
routes:
- id: after_route # 路由id
uri: https://example.org #路由地址
predicates:
#After断言:会匹配指定时间点之后的请求
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
- 使用过滤器
spring:
cloud:
gateway:
routes:
- id: add_request_header_route # 路由id
uri: https://example.org #路由地址
filters:
# 添加请求头过滤器:将为请求头添加red,blue键值对
- AddRequestHeader=X-Request-red, blue
前后端技术栈类比
4)、Nacos
https://blog.csdn.net/weixin_44338156/article/details/122338287
六、前端
前端使用Vue
和ElementUI
vue官网文档:https://cn.vuejs.org/v2/guide/
ElementUI官网文档:https://element-plus.gitee.io/zh-CN/
Vue组件模板:https://www.cnblogs.com/songjilong/p/12635448.html
1、分类维护
1)、三级分类
- 在后台管理系统添加商品系统目录
- 添加分类维护菜单
- 创建category视图
- 添加分类
ElementUI官网的树形控件:https://element.eleme.cn/#/zh-CN/component/tree
- 增加单个增删改功能
<el-button
type="text"
size="mini"
@click="append(data)"
v-if="node.level < 3" <!--三级菜单不允许添加子菜单-->
>
Append
</el-button>
<el-button
type="text"
size="mini"
@click="edit(data)"
>
Edit
</el-button>
<el-button
type="text"
size="mini"
@click="remove(node, data)"
v-if="node.childNodes.length == 0" <!--当子节点为空时显示删除按钮-->
>
Delete
</el-button>
在开发中,相较于物理删除,多使用逻辑删除
这里使用mybatis-plus提供的逻辑删除功能:https://mp.baomidou.com/guide/logic-delete.html
2)、拖拽节点
ElementUI的树形控件有一个参数draggable,将其设为true即可拖拽
但仅仅这样,拖拽可到达任意位置的同时,也会产生超过三级的列表;我们可以设置allow-drop参数来判断当前位置能否被放置
拖拽成功执行的函数
allowDrop(draggingNode, dropNode, type) { //是否允许拖拽
//求出被拖拽节点最大深度
this.countNodeLevel(draggingNode)
//被拖拽节点作为顶级节点的最大深度
let deep = Math.abs(this.maxLevel - draggingNode.level) + 1
this.maxLevel = 1
if (type == 'inner') //放置目标节点之中
return (deep + dropNode.level) <= 3
else //目标节点之前或之后
return (deep + dropNode.data.catLevel - 1) <= 3
},
countNodeLevel(node) { //求出被拖拽节点的最大深度,比如 1-node-3,深度为2
if (node.childNodes != null && node.childNodes.length > 0) { //是否存在子节点
for (let i = 0; i < node.childNodes.length; i++) { //迭代子节点
let child = node.childNodes[i]
if (child.level > this.maxLevel) //若子节点深度大于当前最大深度,则替换
this.maxLevel = child.level
this.countNodeLevel(child) //递归子节点求出最大深度
}
}
},
handleDrop(draggingNode, dropNode, dropType, ev) { //拖拽成功触发的函数
//1. 获取当前节点最新的父节点id
let pCid = 0
let siblings = null
if (dropType == 'inner') {
pCid = dropNode.data.catId
siblings = dropNode.childNodes
}
else {
pCid = dropNode.parent.data.catId || 0
siblings = dropNode.parent.childNodes
}
// 2. 被拖拽节点最新数据
siblings.forEach((node, index) => {
if (draggingNode.data.catId == node.data.catId) {
let level = draggingNode.level
if (node.level != level) {
level = node.level
this.updateChildsLevel(node)
}
this.updateNodes.push({ catId: node.data.catId, sort: index, parentCid: pCid, catLevel: level })
} else {
this.updateNodes.push({ catId: node.data.catId, sort: index })
}
})
this.pCid.push(pCid)
},
updateChildsLevel(node) { //拖拽后更新子节点
node.childNodes.forEach(ele => {
this.updateNodes.push({ catId: ele.data.catId, catLevel: ele.level })
this.updateChildsLevel(ele)
})
},
batchSave() { //批量拖拽保存
this.$http({
url: this.$http.adornUrl('/product/category/update/sort'),
method: 'post',
data: this.$http.adornData(this.updateNodes, false)
}).then(({ data }) => {
this.$message({
type: 'success',
message: '更新成功!'
})
this.getMenus()
this.expandedKeys = this.pCid
this.updateNodes = []
this.maxLevel = 1
})
}
3)、批量删除
getCheckedNodes:获取被选中的节点
// 批量删除
batchDel() {
let checkedNodes = this.$refs.menuTree.getCheckedNodes() // 获取选中节点
let catIds = checkedNodes.map(node => node.catId) // 选中节点的id
let names = checkedNodes.map(node => node.name) // 选中节点的name
let parentCids = checkedNodes.map(node => node.parentCid) // 删除节点的父id,用来展开
this.$confirm(`是否确认删除:【${names}】`, '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warnning'
}).then(() => {
this.$http({
url: this.$http.adornUrl('/product/category/delete'),
method: 'post',
data: this.$http.adornData(catIds, false)
}).then(({ data }) => {
this.$message({
message: '删除成功',
type: 'success'
})
this.getMenus()
this.expandedKeys = parentCids
});
}).catch(() => {
this.$message({
message: '已取消',
type: 'warnning'
})
})
}
2、品牌管理
- 添加品牌管理菜单
- 创建brand视图
将之前逆向工程生成的组件拿来
这里会判断是否拥有新增和删除的权限,将其移除或者return true
1)、显示状态
Table表格中有一个自定义模板,我们使用它来控制品牌的显示状态
Switch开关也拿来
使用Switch的Event来监听change
最终效果:
<el-table-column
prop="showStatus"
header-align="center"
align="center"
label="显示状态"
>
<template slot-scope="scope">
<el-switch
v-model="scope.row.showStatus"
active-color="#13ce66"
inactive-color="#ff4949"
@change="updateBrandStatus(scope.row)"
:active-value="1"
:inactive-value="0"
>
</el-switch>
</template>
</el-table-column>
// 更新显示状态
updateBrandStatus(brand) {
let { brandId, showStatus } = brand
this.$http({
url: this.$http.adornUrl('/product/brand/update'),
method: 'post',
data: this.$http.adornData({ brandId, showStatus }, false)
}).then(({ data }) => {
this.$message({
message: '已更新',
type: 'success'
})
})
}
2)、文件上传
https://element.eleme.cn/#/zh-CN/component/upload
policy.js
import http from '@/utils/httpRequest.js'
export function policy() {
return new Promise((resolve, reject) => {
http({
url: http.adornUrl('/thirdparty/oss/policy'),
method: 'get'
}).then(({ data }) => {
resolve(data)
})
})
}
beforeUpload() {
let _self = this;
return policy().then(response => {
console.log('响应的数据:', response);
_self.dataObj.policy = response.data.policy
_self.dataObj.signature = response.data.signature
_self.dataObj.ossaccessKeyId = response.data.ossaccessKeyId
_self.dataObj.key = response.data.dir + '/' + getUUID + '_${filename}'
_self.dataObj.dir = response.data.dir
_self.dataObj.host = response.data.host
console.log('响应数据:' + _self.dataObj);
}).catch(err => {
reject(false)
})
}
跨域
跨域错误
在这里开启跨域访问
七、第三方服务模块
此模块用来提供第三方服务
1. 创建
引入common模块
<dependency>
<groupId>com.qhit.shuang</groupId>
<artifactId>shuang-common</artifactId>
<version>1.0.0</version>
</dependency>
引入依赖管理
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.2.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2. 文件上传
SpringCloud Alibaba-OSS
1)、介绍
上传文件的方式:
- 用户上传到应用服务器,由应用服务器携带凭证发给OSS
- 用户直接携带凭证上传到OSS
- 用户向应用服务器请求上传Policy,应用服务器返回上传Policy,用户上传文件到OSS
第一种方式会给服务器带来很大的压力,第二种方式存在安全隐患,因此我们选择第三种
2)、使用
- 引入gav坐标
<!-- https://mvnrepository.com/artifact/com.alibaba.cloud/spring-cloud-starter-alicloud-oss --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alicloud-oss</artifactId> <version>2.1.0.RELEASE</version> </dependency>
- 配置AccessKey
spring: cloud: alicloud: access-key: your-ak secret-key: your-sk oss: endpoint: oss-cn-hangzhou.aliyuncs.com #地域节点,此为示例
3. 新增品牌
1)、前端校验
ElementUI表单校验:https://element.eleme.cn/#/zh-CN/component/form
Form 组件提供了表单验证的功能,只需要通过 rules 属性传入约定的验证规则,并将 Form-Item 的 prop 属性设置为需校验的字段名即可。校验规则参见 async-validator
还可以自定义校验规则:
2)、后端校验
前端校验方便用户的同时,也减轻了服务器压力
但当恶意访问者绕过浏览器,通过Postman等类似工具进行访问时,前端校验未必有用,此时我们需要添加后端校验
我们此次使用JSR303
错误码
自定义错误码以便快速精确定位错误
放在公共模块以便所有模块使用
BizCodeEnume
4. 属性分组
后端统一异常处理
ExceptionControllerAdvice.java
bug
找不到python问题
**问题描述:**在github上clone下的项目执行npm install时报python环境错误
确保windows已安装node.js/vue/vue-cli
一、cmd下运行npm install -globabl -production windows-build-tools一键安装
- python(v2.7 ,3.x不支持);
- visual C++ Build Tools,或者 (vs2015以上(包含15))
- .net framework 4.5.1
二、在控制台输入:npm install -g node-gyp安装node-gyp
三、安装后检查:node-gyp list
最后到项目下执行npm install…成功。。。
后记:
如果还是提示“python找不到或者环境不对”
npm config set python C:\Users\Administrator\.windows-build-tools\python27\python.exe
跨域问题
跨域:指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript施加的安全限制。
同源策略:是指协议,域名,端口都要相同,其中有一个不同都会产生跨域。
解决方案一:使用Nginx部署为同一域
解决方案二:配置当前请求允许跨域
- 添加响应头
- Access-Control-Allow-Origin:支持哪些来源的请求跨域
- Accss-Control-Allow-Methods:支持哪些方法跨域
- Access-Control-Allow-Credentials:跨域请求默认不包含cookie,设置true包含
- Access-Control-Expose-Headers:跨域请求暴漏的字段
- CORS请求时,XMLHttpRequest对下国内的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma,如果想要拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。
- Access-Control-Max-Age:表迷宫该响应的有效时间为多少秒。有效时间内,浏览器无需为同一请求再次发起预见请求。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将失效。