电商项目——初识电商——第一章——上篇
电商项目——分布式基础概念和电商项目微服务架构图,划分图的详解——第二章——上篇
电商项目——电商项目的虚拟机环境搭建_VirtualBox,Vagrant——第三章——上篇
电商项目——Linux虚拟机中安装docker,mysql,redis_VirtualBox——第四章——上篇
电商项目——电商项目的环境搭建_开发工具&环境搭建——第五章——上篇
电商项目——快速开发人人开源搭建后台管理系统&代码生成器逆向工程搭建——第六章——上篇
电商项目——分布式组件(SpringCloud Alibaba,SpringCloud)——第七章——上篇
电商项目——前端基础——第八章——上篇
电商项目——商品服务-API-三级分类——第九章——上篇
电商项目——商品服务-API-品牌管理——第十章——上篇
电商项目——商品服务-API-属性分组——第十一章——上篇
电商项目——商品服务-API-品牌管理——第十二章——上篇
电商项目——商品服务-API-平台属性——第十三章——上篇
电商项目——商品服务-API-新增商品——第十四章——上篇
电商项目——商品服务-API-商品管理——第十五章——上篇
电商项目——商品服务-API-仓库管理——第十六章——上篇
文章目录
1:使用逆向工程的前后端代码
我们先来看一下mall-product中关于商品表中的信息
第一步:在人人开源后台管理系统中的菜单管理,点击新增
第二步:在renren-fast-vue中找到对应的路径product/brand创建brand.vue
我们可以使用代码自动生成器(renren-generator),里的前端代码
解决办法:在src/utils/index.js中注释掉如下内容,并返回true
/**
* 是否有权限
* @param {*} key
*/
export function isAuth (key) {
// return JSON.parse(sessionStorage.getItem('permissions') || '[]').indexOf(key) !== -1 || false
return true;
}
如上完成就可以实现增删改查效果
2:效果优化与快速显示开关
实现:我们要完成在状态字段里显示一个开关按钮,并且状态按钮发生变化的时候就应该给服务器发送请求,修改品牌的状态
我们先优化一下逆向生成的品牌管理代码
由于ESLint的语法,太严格,我们删掉如下代码
第一步:优化开关,如下
代码演示
<el-table-column
prop="showStatus"
header-align="center"
align="center"
label="显示状态">
<!-- 通过 Scoped slot 可以获取到 row, column, $index 和 store(table 内部的状态管理)的数据,-->
<template slot-scope="scope">
<el-switch
v-model="scope.row.showStatus"
style="display: block"
:v-model="true"
active-color="#13ce66"
inactive-color="#ff4949"
>
</el-switch>
</template>
</el-table-column>
<el-form-item label="显示状态" prop="showStatus">
<el-switch
v-model="dataForm.showStatus"
style="display: block"
:v-model="true"
active-color="#13ce66"
inactive-color="#ff4949"
>
</el-switch>
</el-form-item>
第二步:监听开关状态,如果状态改变,就应该给服务器发送请求,修改品牌的状态。
我们在Switch开关中找到了change事件,我们使用它来进行监听
switch按钮
<template slot-scope="scope">
<el-switch
v-model="scope.row.showStatus"
:v-model="true"
:active-value="1"
:inactive-value="0"
@change="updateBrandStatus(scope.row)"
>
</el-switch>
</template>
监听事件的方法
methods: {
//修改商品的状态
updateBrandStatus(data){
console.log("最新状态",data)
//解构这个对象
let {brandId,showStatus} =data
//发送请求进行修改
this.$http({
url: this.$http.adornUrl('/product/brand/update'),
method: 'post',
params: this.$http.adornParams({
brandId,
showStatus
},false)
}).then(()=>{
this.$message({
type: 'sucess',
message: '状态更新成功!'
});
})
},
}
我们可以使用postman测试上面的路径
3:云存储开通与使用
接下来,我们来编写文件上传功能,
我们来看下如何将上传的文件进行存储
分为两种情况
- 单体应用
- 分布式应用
- 以前开发的是小商城——单体应用,项目就部署在一台服务器上,我们想要做文件上传,如上图,浏览器发送一个请求,叫服务器去保存在项目中的某一个位置,如果是分布式情况,一台服务器不够,我们就得布置多台服务器,还是像以前单体应用一样是不行的,因为我们下一次如果负载均衡到了其他服务器是获取不到文件的,就会有一些问题,我们该怎么做呢?我们可以将上传的文件都存储到统一的文件存储中去,这样任何一个文件都可以在同一一个地方写,同一个地方读,就不会出现自己持有的文件,别人找不到的情况,(文件存储可以自己搭建,也可以用第三方给我们做好的云存储)
- 推荐使用云存储功能:如今的项目要求快速开发,快速迭代,以及满足弹性成本,所以使用云存储是非常合理的
- 我们电商系统也最终使用第三方的云存储服务,借此熟悉第三方云存储开通和使用流程
3.1 SpringCloud Alibaba-OSS
- 对象存储服务(Oblect Storage Service,O5s)是一种海量、安全、低成本、高可靠的云存储服务,适合存放任意类型的文件。容量和处理能力弹性扩展,多种存储类型供选择,全面优化存储成本。
第一次使用阿里云,要进行账号绑定,实名认证等,然后在开通对象存储
- 存储空间(Bucket)
存储空间是用户用于存储对象(Object)的容器,所有的对象都必须隶属于某个存储空间。存储空间具有各种配置属性,包括地域、访问权限、存储类型等。用户可以根据实际需求,创建不同类型的存储空间来存储不同的数据。
同一个存储空间的内部是扁平的,没有文件系统的目录等概念,所有的对象都直接隶属于其对应的存储空间。
每个用户可以拥有多个存储空间。
存储空间的名称在OSS范围内必须是全局唯一的,一旦创建之后无法修改名称。
存储空间内部的对象数目没有限制。
演示创建一个bucket
演示如何上传文件,并访问
3.2 上传文件到阿里云的方法
我们以后的上传文件,是通过后台界面,点击上传,把图片保存到阿里与的bucket里的,并且拿到这个图片的真正地址,那我们要怎么做呢?
第一种:用户通过浏览器,选择文件上传,把图片先上传给应用服务器(把整个文件上传请求提交给网关,然后在交给商品服务,商品服务拿到图片流了,在使用java代码把图片的流数据,传给对象储存,最终存在bucket里面,并拿到地址)
缺点:我们文件上传还要过到我们的应用服务器里面(完全没必要,服务器会在大量的流量下,带来瓶颈)
优点:我们要上传到阿里云的bucket与我们的账号密码有关,但是这个东西不可以暴露,使用第一种方式的安全自处在于,文件交给我们的服务器,服务器用账号密码上传给阿里云
第二种:用浏览器直接上传给对象,我们将阿里云的账号密码直接写在js代码里面,让js控制,把表单直接提交给阿里云,风险就是泄露了阿里云的账号密码,所以我们可以使用服务端签名后直传,我们把操作对象的需要的账号密码直接存储在应用服务器里面,浏览器想要给阿里云上传数据,怎么办呢?上传之前先找我们的应用服务器,要到一个policy,服务器利用阿里云的账号密码,生成一个防伪签名(包含访问阿里云的授权令牌,以及我们要上传文件的地址),我们前端拿到这些信息以后,在带着这些信息(没有账号密码,只有防伪签名),以及要上传的文件,交给OSS,OSS验证这些是正确的就成功保存
4:OSS整合测试
5:OSS获取服务端签名
6:OSS前后端联调测试上传
第一步:把已经封装好的upload放到指定位置
第二步:修改如下的品牌logo地址,为文件上传
我们要先抽取我们第一步中的组件放到brand-add-or-update.vue中
//1.导入组件
import singleUpload from "../../../components/upload/singleUpload";
export default {
//2.写入组件中
components: {singleUpload},
<!-- //3:引入组件-->
<single-upload v-model="dataForm.logo"></single-upload>
我们第一次测试上传图片没有成功
我们寻找问题:看singleUpload.vue
在里面点击上传会触发 :before-upload="beforeUpload"中的beforeUpload事件
beforeUpload方法:
解决办法:
//..
public R policy(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {
//...
return R.ok().put("data",respMap);
}
进行上传测试,发现出现跨域问题,我们就要去OSS对象存储中的权限管理进行跨域配置如下
再次进行上传测试发现,上传成功
服务端签名后直传的url分析
7:表单校验与自定义校验器
问题一:
接下来我们调试一下品牌新增功能,我们点击新增可以弹出对话框,新增数据
解决方法如下:
<el-switch
:active-value="1"
:inactive-value="0"
>
</el-switch>
问题二:
解决方法
<el-table-column
prop="logo"
header-align="center"
align="center"
label="品牌logo地址">
<!-- 通过 Scoped slot 可以获取到 row, column, $index 和 store(table 内部的状态管理)的数据-->
<template slot-scope="scope">
<el-image
style="width: 140px; height: 80px"
:src="scope.row.logo"
fit="contain"></el-image>
</template>
</el-table-column>
进行测试发现图片没显示出来,检查发现报如下错误
报错如下
解决办法:
问题三:我们再来看一个数据校验的有关问题,如果我们点击新增什么都不填写,就确定就会报如下红线,如下的数据校验都是默认的不可以为空,如果我们乱填该怎么办呢?
解决办法:我们前端在给后台提交数据时,一定要做一个前端校验(基于表单的数据校验)
页面代码重要的是:rule=dataRule和prop
data () {
var firstLetter = (rule, value, callback) => {
console.log("dddeeeeed")
if (value === '') {
callback(new Error('首字母必须填写'));
} else {
if (!/^[a-zA-Z]$/.test(value)) {
callback(new Error('首字母必须在a-z和A-Z中'));
}else {
callback();
}
}
};
// 让排序接受一个数组v-model.number
var sort = (rule, value, callback) => {
console.log("dddd")
if (value === '') {
callback(new Error('排序字段必须填写'));
} else if (!number.isInteger(value) || value<0) {
callback(new Error('排序字段必须是一个大于0的数'));
} else {
callback();
}
};
return {
dataRule: {
name: [
{ required: true, message: '品牌名不能为空', trigger: 'blur' }
],
logo: [
{ required: true, message: '品牌logo地址不能为空', trigger: 'blur' }
],
descript: [
{ required: true, message: '介绍不能为空', trigger: 'blur' }
],
showStatus: [
{ required: true, message: '显示状态[0-不显示;1-显示]不能为空', trigger: 'blur' }
],
firstLetter: [
{ validator: firstLetter, trigger: 'blur' }
],
sort: [
{ validator: sort, trigger: 'blur' }
]
}
}
},
有了前端校验我们就可以保证后台管理系统前端页面,传递给服务器的数据是正确的,但是我还要加上服务端校验,因为就算把前端校验好的数据提交给服务器,服务器不校验也会很危险(绕过这个前端项目,知道你会发什么请求,比如postman,这样就会造成很大的危险)
服务端校验是很重要的
8:JSR303数据校验
这一章节我们就完成后端校验,完成新增服务的双版校验
思路:
3: JSR30
1)给bean添加校验注解
2)开启校验功能@Valid
校验错误以后会有默认的响应
3)给校验的bean后紧跟一个BindingResult。就可以获取到检验结果
代码演示
BrandEntity .java
/**
* 品牌
*/
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名不可以为空")
private String name;
/**
* 品牌logo地址
*/
@NotEmpty
@URL(message = "logo必须是一个合法的url地址")
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty
@Pattern(regexp ="/^[a-zA-Z]$/",message = "检索首字母必须是一个字母")
private String firstLetter;
/**
* 排序
*/
@NotNull
@Min(value = 0,message = "排序必须大于等于0")
private Integer sort;
}
BrandController.java
/**
* 保存
*/
@RequestMapping("/save")
// @RequiresPermissions("product:brand:save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult bindingResult){
Map<String,String> map=new HashMap<>();
if (bindingResult.hasErrors()){
bindingResult.getFieldErrors().forEach((item)->{
String message= item.getDefaultMessage();
String field = item.getField();
map.put(field,message);
});
return R.error(400,"提交数据不合法").put("data",map);
}else {
brandService.save(brand);
return R.ok();
}
}
9:统一异常处理
我们以后很多项目中都会用到校验功能,难道我们要在模块在一直写返回的校验结果的处理信息嘛?这样显然太麻烦了,我们可以做统一的异常处理
思路:我们如果不去感知错误的异常处理信息,那么遇到问题就会自动抛出去,抛到哪里呢?
可以抛到我们自定义的exception包下的MallExceptionController类中
MallExceptionController.java
//集中的异常处理器类
@Slf4j
//所有的json数据都要以json的方式返回给前端
//@ResponseBody
//@ControllerAdvice(basePackages = "com.atstudying.mall.product.controller")
@RestControllerAdvice(basePackages = "com.atstudying.mall.product.controller")
public class MallExceptionController {
//它告诉springmvc,它可以处理什么异常(MethodArgumentNotValidException)
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleValidException(MethodArgumentNotValidException e){
log.error("数据校验出现问题:{},异常类型:{}",e.getMessage(),e.getClass());
BindingResult result=e.getBindingResult();
Map<String,String> error=new HashMap<>();
result.getFieldErrors().forEach((fieldError -> {
error.put(fieldError.getField(),fieldError.getDefaultMessage());
}));
return R.error(400,"数据校验出错").put("data",error);
}
//对于异常处理,统一所有的异常用这个感知(如果异常抛出,上面的异常接受不到就会统一用这个异常)
@ExceptionHandler(value = Throwable.class)
public R handleValidException(Throwable e){
log.error("数据校验出现问题:{},异常类型:{}",e.getMessage(),e.getClass());
return R.error(400,"数据校验出错").put("data",e.getMessage());
}
}
BrandController.java
/**
* 保存
*/
//不去感知错误的异常处理信息,那么遇到问题就会自动抛出去
@RequestMapping("/save")
// @RequiresPermissions("product:brand:save")
public R save(@Valid @RequestBody BrandEntity brand){
brandService.save(brand);
return R.ok();
}
我们看如上的代码,在后来由于业务众多,业务的状态码(400)也众多,所以我们要把业务状态码集中起来,放在mall-common微服务中
系统错误码介绍:
public enum BigCodeEnume {
UNKNOW_EXCEPTION(10000,"系统未知异常"),
VALID_EXCEPTION(10001,"参数校验失败");
private int code;
private String msg;
private BigCodeEnume(int code,String msg){
this.code=code;
this.msg=msg;
}
public int getCode(){
return code;
}
public String getMsg(){
return msg;
}
}
我们就可以修改上面MallExceptionController类中的返回方法为如下
return R.error(BigCodeEnume.VALID_EXCEPTION.getCode(),BigCodeEnume.VALID_EXCEPTION.getMsg()).put("data",error);
return R.error(BigCodeEnume.UNKNOW_EXCEPTION.getCode(),BigCodeEnume.UNKNOW_EXCEPTION.getMsg()).put("data",e.getMessage());
10:JSR303分组校验
思路:
4)分组校验:(多场景分组校验
1)给校验注解标注什么组需要校验(UpdateGroup.class我们要去mall-common中编写一个valid/UpdateGroup.interface)
@NotNull(message = “修改必须指定品牌id”,groups ={UpdateGroup.class} )
2)在控制器中,写上对应校验需要的组 @Validated(value = {UpdateGroup.class})
3)默认没有分组校验的注解,在分组校验的情况下不生效
BrandEntity.java
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
// Class<?>[] groups() default {};
@NotNull(message = "修改必须指定品牌id",groups ={UpdateGroup.class} )
@Null(message = "新增不可以指定id",groups = {AddGroup.class})
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名不可以为空",groups = {UpdateGroup.class,AddGroup.class})
private String name;
/**
* 品牌logo地址
*/
@NotEmpty(groups = {AddGroup.class})
@URL(message = "logo必须是一个合法的url地址",groups = {AddGroup.class,UpdateGroup.class})
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty(groups = {UpdateGroup.class,AddGroup.class})
@Pattern(regexp ="^[a-zA-Z]$",message = "检索首字母必须是一个字母",groups = {UpdateGroup.class,AddGroup.class})
private String firstLetter;
/**
* 排序
*/
@NotNull(groups = {UpdateGroup.class,AddGroup.class})
@Min(value = 0,message = "排序必须大于等于0",groups = {UpdateGroup.class,AddGroup.class})
private Integer sort;
}
控制器
BrandController
/**
* 保存
*/
//不去感知错误的异常处理信息,那么遇到问题就会自动抛出去
@RequestMapping("/save")
public R save(@Validated(value = {AddGroup.class}) @RequestBody BrandEntity brand){
brandService.save(brand);
return R.ok();
}
/**
* 修改
* * * * @RequestBody:获取请求体,必须发送post请求(只有post请求才有请求体,get请求没有请求体)
* springmvc(web)自动将请求体的数据(json),转化为对应的对象
*/
@RequestMapping("/update")
public R update(@Validated(value = {UpdateGroup.class}) @RequestBody BrandEntity brand){
brandService.updateById(brand);
return R.ok();
}