文章目录
- 前言
- 四 全笔记链接(链接给出)
- 五 基础篇开发部分
- 0 lambda表达式基础
- 一 商品服务-API
- 1 三级分类-查询 递归树形结构获取(查询所有目录)
- 2 三级分类-配置网关路由和路径重写
- 3 三级分类-网关统一配置跨域
- 4 三级分类-查询 树形显示三级目录
- 5 三级分类-页面展示效果
- 6 三级分类-删除-逻辑删除
- 7 三级分类-删除-删除效果细化
- 8 三级分类-新增
- 8 三级分类-修改-基本修改效果完成
- 剩下的三级分类暂且跳过
- 14 品牌管理 逆向生成前后端代码
- 14 品牌管理 效果优化 快速展示开关
- 15 品牌管理 云储存开通和使用
- 16 品牌管理 OSS整合测试
- 17 品牌管理 OSS获取服务端签名
- 18 品牌管理 OSS前后联调测试上传
- 19 品牌管理 表单校验,自定义校验器
- 20 品牌管理 JSR303校验
- 21 品牌管理- 统一异常处理
- 22 品牌管理 JSR303分组校验
- 22 JSR303 自定义校验注解
- 23 商品服务-概念-SPU SKU 规格参数 销售属性
- 24 商品服务-API-属性分组-前端组件抽取&父子组件交互
- 25 属性分组 获取分类属性分组
- 26 属性分组 分组新增&级联选择器
- 27 属性分组 分组修改-级联选择器回显
- 28 品牌管理 品牌分类关联与级联更新
- 下一个基础篇笔记链接
- 总结
前言
谷粒商城的基础部分2
四 全笔记链接(链接给出)
谷粒商城笔记(详细版) IDEA2021 基础篇(1/3).
谷粒商城笔记(详细版) IDEA2021 基础篇(2/3).
谷粒商城笔记(详细版) IDEA2021 基础篇(3/3).
五 基础篇开发部分
0 lambda表达式基础
可以看我的这篇博客 学习自mooc网
Lambda表达式学习.
一 商品服务-API
1 三级分类-查询 递归树形结构获取(查询所有目录)
1首先更改category实体类
package com.atguigu.gulimall.product.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
/**
* 商品三级分类
*
* @author yyl
* @email sunlightcs@gmail.com
* @date 2022-04-24 19:01:01
*/
@Data
@TableName("pms_category")
public class CategoryEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 分类id
*/
@TableId
private Long catId;
/**
* 分类名称
*/
private String name;
/**
* 父分类id
*/
private Long parentCid;
/**
* 层级
*/
private Integer catLevel;
/**
* 是否显示[0-不显示,1显示]
*/
private Integer showStatus;
/**
* 排序
*/
private Integer sort;
/**
* 图标地址
*/
private String icon;
/**
* 计量单位
*/
private String productUnit;
/**
* 商品数量
*/
private Integer productCount;
//标注在数据库中不是实际存在
@TableField(exist=false)
private List<CategoryEntity> children;
}
2 controller层
/**
* 查出所有分类以及子分类,以树形结构组装起来
*/
@RequestMapping("/list/tree")
public R list(){
List<CategoryEntity> entities = categoryService.listWithTree();
return R.ok().put("data", entities);
}
3 serviceImpl层
@Override
public List<CategoryEntity> listWithTree() {
//1、查出所有分类 因为这里使用了mybatis-plus 所以可以使用baseMapper
List<CategoryEntity> entities = baseMapper.selectList(null);
//2、组装成父子的树形结构
//2.1)、找到所有的一级分类
List<CategoryEntity> level1Menus = entities.stream().filter(categoryEntity ->
categoryEntity.getParentCid() == 0
).map((menu)->{
menu.setChildren(getChildrens(menu,entities));
return menu;
}).sorted((menu1,menu2)->{
return (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort());
}).collect(Collectors.toList());
return level1Menus;
}
//递归查找所有菜单的子菜单
//这个方法传入的是一个一级目录root 和全部的目录all
private List<CategoryEntity> getChildrens(CategoryEntity root,List<CategoryEntity> all){
//1 使用全部的目录all首先找出传入目录root的所有下一层目录
//2 使用map对root的所有下一层目录 的 子目录进行设置
//3 利用sorted对所有下一层目录进行排序
//4 collect集合返回listWithTree()函数 这时候一个一级目录root的子目录已经设置成功
List<CategoryEntity> children = all.stream().filter(categoryEntity -> {
return categoryEntity.getParentCid() == root.getCatId();
}).map(categoryEntity -> {
//1、找到子菜单
categoryEntity.setChildren(getChildrens(categoryEntity,all));
return categoryEntity;
}).sorted((menu1,menu2)->{
//2、菜单的排序
return (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort());
}).collect(Collectors.toList());
return children;
}
2 三级分类-配置网关路由和路径重写
基本按照视频一步一步来
0 基本步骤
1 让我们vue前端项目去访问网关
2 前端base URL 如下
‘http://localhost:88/api’
位于static的index.js之中
这样我们验证码访问url为
http://localhost:88/api/captcha.jpg
3 后端要求访问路径如下
http://localhost:8080/renren-fast/captcha.jpg
4 所以我们编写路由规则
- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**
filters:
- RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}
0.5 思路总结
1 首先变更前端vue访问的baseurl 让其访问gateway网关
2 把renren-fast后端注册到Nacos
3 利用网关编写路由规则使其访问到renren-fast项目
4 vue -> gateway -> renren-fast
1 (重要)新版本的renren-fast注册报错问题
附上官方版本对应关系链接
https://github.com/spring-cloud-incubator/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E
这里来解决一下新版本的renren-fast注册到Nacos配置中心的问题
因为对maven和依赖管理概念不明确
解决了很长时间
有两个概念
alibaba-cloud版本
spring-cloud版本
如果你还使用guli-common模块中的cloud版本
很可能会报错 (springboot版本与common模块中老的cloud版本冲突)
我们这里重新设置一个
使用2021.0.1 官方cloud版本
<properties>
<!-- 配置cloud-->
<spring-cloud.version>2021.0.1</spring-cloud.version>
</properties>
<!-- 统一管理cloud版本-->
<dependencyManagement>
<dependencies>
这个管理spring-cloud版本
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
这个管理alibaba-cloud版本
其实这个已经在common模块中定义了
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.7.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
还有记得引入这个依赖来避免一个报错
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1-jre</version>
</dependency>
总结
尽量把依赖版本(dependencyManagement)都在common中定义好
同时每个模块使用相似的springboot版本
避免版本相差过大无法使用共同common
解决成功
3 三级分类-网关统一配置跨域
1 跨域简介
2 跨域解决方案
在你的gateway网关下编写如下配置
参考结构
注意你的springboot版本
如果新版使用addAllowedOriginPattern
否则不生效
package com.atguigu.gulimall.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
@Configuration
public class GulimallCorsConfiguration {
@Bean
public CorsWebFilter corsWebFilter(){
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
//1、配置跨域
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
//老版本方法
// corsConfiguration.addAllowedOrigin("*");
//注意我这里springboot版本新 2.6.7所以要使用以下方法允许源头=访问
corsConfiguration.addAllowedOriginPattern("*");
corsConfiguration.setAllowCredentials(true);
source.registerCorsConfiguration("/**",corsConfiguration);
return new CorsWebFilter(source);
}
}
4 三级分类-查询 树形显示三级目录
1 首先把gulimall-product注册Nacos中
如果不懂可以到这篇博客前面的
Nacos做注册中心看一看
这里简单列出配置文件内容
参考结构
1 appliction,yml
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
url: jdbc:mysql://localhost:3306/gulimall_pms?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2B8
application:
name: gulimall-product
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
id-type: auto
server:
port: 10000
2 bootsrap.properties
spring.application.name=gulimall-product
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=4f0331c3-576a-44ad-bf4e-8af996e13d9f
#spring.cloud.nacos.config.group=prop
2 编写前端vue内容
代码如下
<template>
<el-tree :data="tableData" :props="defaultProps" @node-click="handleNodeClick"></el-tree>
</template>
<script>
export default {
data () {
return {
tableData: [],
defaultProps: {
children: 'children',
label: 'name'
}
}
},
methods: {
handleNodeClick (data) {
console.log(data)
},
// 获取数据列表
getDataList () {
this.dataListLoading = true
this.$http({
url: this.$http.adornUrl('/product/category/list/tree'),
method: 'get'
}).then(({data}) => {
console.log('成功获取到菜单数据', data.data)
this.tableData = data.data
})
}
},
created () {
// 获取数据列表
this.getDataList()
}
}
</script>
<style scoped>
</style>
3 前端的一个小坑
后端的朋友可以看一看
consle.log打印出来的都是object无法查看.
5 三级分类-页面展示效果
1 增加append和delete
我们首先为页面增加删除和添加按钮
我们去到element-ui的树形控件当中
把这段代码和对应的函数粘贴到tree里面
完整示例如下
<template>
<el-tree :data="tableData" :props="defaultProps" :expand-on-click-node="false" show-checkbox node-key="catId">
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button v-if="node.level<=2"
type="text"
size="mini"
@click="() => append(data)">
Append
</el-button>
<el-button v-if="node.childNodes.length==0"
type="text"
size="mini"
@click="() => remove(node, data)">
Delete
</el-button>
</span>
</span>
</el-tree>
</template>
<script>
export default {
data () {
return {
tableData: [],
defaultProps: {
children: 'children',
label: 'name'
}
}
},
methods: {
append (data) {
console.log('append', data)
},
remove (node, data) {
console.log('node', node, data)
},
// 获取数据列表
getDataList () {
this.dataListLoading = true
this.$http({
url: this.$http.adornUrl('/product/category/list/tree'),
method: 'get'
}).then(({data}) => {
console.log('成功获取到菜单数据', data.data)
this.tableData = data.data
})
}
},
created () {
// 获取数据列表
this.getDataList()
}
}
</script>
<style scoped>
</style>
2 为append和delete设置条件
思考:
1 我们要让一级目录和二级目录可以添加子目录
2 只有没有子节点的目录才可以进行目录删除
3 最终查看页面效果
6 三级分类-删除-逻辑删除
Mybatis-plus官方文档.
首先更改procut模块的配置文件
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
url: jdbc:mysql://localhost:3306/gulimall_pms?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2B8
application:
name: gulimall-product
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
id-type: auto
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
server:
port: 10001
接着对实体类字段进行设置
/**
* 是否显示[0-不显示,1显示] 注解表示这个字段为逻辑删除字段
*/
@TableLogic(value = "1",delval = "0")
private Integer showStatus;
接着启动进行测试就可以了
如果你使用的是navicat数据库
在你操作完成后可能需要翻到下一页进行查看
而不是操作失败了
7 三级分类-删除-删除效果细化
首先前后端联调代码如下
这样做已经可以删除成功了
由于逻辑删除的缘故
我们查不到逻辑删除的数据
remove (node, data) {
var ids = [data.catId]
// 1 index发送请求
// 'http://localhost:88/api'
// 2 拼装请求
// http://localhost:88/api/product/category/delete
// 3 网关转发后请求
// http://localhost:88/product/category/delete
this.$http({
url: this.$http.adornUrl('/product/category/delete'),
method: 'post',
data: this.$http.adornData(ids, false)
}).then(({data}) => {
// 获取数据列表
this.getDataList()
console.log('删除成功')
})
},
接着进行页面效果细化操作
1 确认删除操作
思路 增加一个MessageBox 把你的操作放在确认后进行
2 点击删除页面不收起
思路 el-tree中增加 :default-expanded-keys=“expandedKey”
同时在data中定义expandedKey
接着进行逻辑的编写
代码示例如下
remove (node, data) {
// 1 index发送请求
// 'http://localhost:88/api'
// 2 拼装请求
// http://localhost:88/api/product/category/delete
// 3 网关转发后请求
// http://localhost:88/product/category/delete
var ids = [data.catId]
this.$confirm(`此操作将永久删除${data.name}目录, 是否继续?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: this.$http.adornUrl('/product/category/delete'),
method: 'post',
data: this.$http.adornData(ids, false)
}).then(({data}) => {
// 获取数据列表
this.getDataList()
// 设置需要默认展开的菜单
this.expandedKey = [node.parent.data.catId]
console.log('删除成功')
})
this.$message({
type: 'success',
message: '删除成功!'
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
})
})
},
8 三级分类-新增
思路:
1 使用dialog对话框 里面嵌套一个el-form表单 设置dialogVisible
<el-dialog
:title="title"
:visible.sync="dialogVisible"
width="30%"
:close-on-click-modal="false"
>
<el-form :model="category">
<el-form-item label="分类名称">
<el-input v-model="category.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="图标">
<el-input v-model="category.icon" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="计量单位">
<el-input v-model="category.productUnit" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="addCategory">确 定</el-button>
</span>
</el-dialog>
2 点击append打开对话框 对我们要上传目录的parentCid catLevel 进行赋值
把一些数据进行重置
append (data) {
console.log('append', data)
// this.dialogType = "add";
// this.title = "添加分类";
this.dialogVisible = true
// 获得当前点击节点的id 其实是我们要添加目录的父id
this.category.parentCid = data.catId
// 获取添加目录的层级 为当前层级+1
this.category.catLevel = data.catLevel * 1 + 1
// 重置之前的数值 变为默认值
this.category.catId = null
this.category.name = ''
this.category.icon = ''
this.category.productUnit = ''
this.category.sort = 0
this.category.showStatus = 1
console.log('append', data)
},
3 编写addCategory函数 点击对话框中的确认时调用
里面编写代码连接后端进行数据添加
并设置默认展开
// 添加三级分类
addCategory () {
console.log('提交的三级分类数据', this.category)
this.$http({
url: this.$http.adornUrl('/product/category/save'),
method: 'post',
data: this.$http.adornData(this.category, false)
}).then(({ data }) => {
this.$message({
message: '菜单保存成功',
type: 'success'
})
// 关闭对话框
this.dialogVisible = false
// 刷新出新的菜单
this.getDataList()
// 设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid]
})
},
4 script中的data数据
data () {
return {
category: {
name: '',
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
productUnit: '',
icon: '',
catId: null
},
dialogVisible: false,
expandedKey: [],
tableData: [],
defaultProps: {
children: 'children',
label: 'name'
}
}
},
8 三级分类-修改-基本修改效果完成
注意:具体的细节请看我写的代码注释
思路:
1 我们的增加和修改共用一个dialog进行操作
所以在确定的时候应该调用一个函数进行选择
2 修改的时候我们从数据库拿出最新的节点数据
拿到数据后为data()中的catagory属性进行赋值
因为我们的dialog绑定的就是category中的数值
所以会进行回显
3 当我们点击dialog的确认更改时提交这些数据
最终代码
<template>
<div>
<el-tree :data="tableData" :props="defaultProps" :expand-on-click-node="false"
show-checkbox node-key="catId" :default-expanded-keys="expandedKey">
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button v-if="node.level<=2"
type="text"
size="mini"
@click="() => append(data)">
Append
</el-button>
<el-button type="text" size="mini" @click="edit(data)">edit</el-button>
<el-button v-if="node.childNodes.length==0"
type="text"
size="mini"
@click="() => remove(node, data)">
Delete
</el-button>
</span>
</span>
</el-tree>
<el-dialog
:title="title"
:visible.sync="dialogVisible"
width="30%"
:close-on-click-modal="false"
>
<el-form :model="category">
<el-form-item label="分类名称">
<el-input v-model="category.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="图标">
<el-input v-model="category.icon" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="计量单位">
<el-input v-model="category.productUnit" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="submitData">确 定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
export default {
data () {
return {
// 标题名称
title: '',
// dialog类型
dialogType: '', // edit,add
// category所需数据
category: {
name: '',
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
productUnit: '',
icon: '',
catId: null
},
// dialog默认不展开
dialogVisible: false,
// 默认展开哪些节点
expandedKey: [],
// 表格的数据
tableData: [],
// 树形结构的子节点是哪个字段 Label是树形结构展示哪个标签
defaultProps: {
children: 'children',
label: 'name'
}
}
},
methods: {
// 修改三级分类数据
editCategory () {
// 只把我们需要更新的数据进行上传
var { catId, name, icon, productUnit } = this.category
this.$http({
url: this.$http.adornUrl('/product/category/update'),
method: 'post',
data: this.$http.adornData({ catId, name, icon, productUnit }, false)
}).then(({ data }) => {
this.$message({
message: '菜单修改成功',
type: 'success'
})
// 关闭对话框
this.dialogVisible = false
// 刷新出新的菜单
this.getDataList()
// 设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid]
})
},
edit (data) {
console.log('要修改的数据', data)
// 设置DiglogType方便调用editCategory函数
this.dialogType = 'edit'
// 设置diglog名称
this.title = '修改分类'
// 打开Diglog
this.dialogVisible = true
// 发送请求获取当前节点最新的数据
this.$http({
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: 'get'
}).then(({ data }) => {
// 请求成功
console.log('要回显的数据', data)
this.category.name = data.data.name
this.category.catId = data.data.catId
this.category.icon = data.data.icon
this.category.productUnit = data.data.productUnit
this.category.parentCid = data.data.parentCid
this.category.catLevel = data.data.catLevel
this.category.sort = data.data.sort
this.category.showStatus = data.data.showStatus
/**
* parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
*/
})
},
submitData () {
if (this.dialogType === 'add') {
this.addCategory()
}
if (this.dialogType === 'edit') {
this.editCategory()
}
},
// 添加三级分类
addCategory () {
console.log('提交的三级分类数据', this.category)
this.$http({
url: this.$http.adornUrl('/product/category/save'),
method: 'post',
data: this.$http.adornData(this.category, false)
}).then(({ data }) => {
this.$message({
message: '菜单保存成功',
type: 'success'
})
// 关闭对话框
this.dialogVisible = false
// 刷新出新的菜单
this.getDataList()
// 设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid]
})
},
append (data) {
// 打印点击append获取的节点数据
console.log('append', data)
// 把diglog框的类型设置为add便于调用addcategoty函数
this.dialogType = 'add'
// 设置表格头
this.title = '添加分类'
// 打开dialog
this.dialogVisible = true
// 获得当前点击节点的id 其实是我们要添加目录的父id
this.category.parentCid = data.catId
// 获取添加目录的层级 为当前层级+1
this.category.catLevel = data.catLevel * 1 + 1
// 把数据置为默认数据 避免因为update操作影响append
this.category.catId = null
this.category.name = ''
this.category.icon = ''
this.category.productUnit = ''
this.category.sort = 0
this.category.showStatus = 1
},
remove (node, data) {
// 1 index发送请求
// 'http://localhost:88/api'
// 2 拼装请求
// http://localhost:88/api/product/category/delete
// 3 网关转发后请求
// http://localhost:88/product/category/delete
var ids = [data.catId]
this.$confirm(`此操作将永久删除${data.name}目录, 是否继续?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: this.$http.adornUrl('/product/category/delete'),
method: 'post',
data: this.$http.adornData(ids, false)
}).then(({data}) => {
// 获取数据列表
this.getDataList()
// 设置需要默认展开的菜单
this.expandedKey = [node.parent.data.catId]
console.log('删除成功')
})
this.$message({
type: 'success',
message: '删除成功!'
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
})
})
},
// 获取数据列表
getDataList () {
this.dataListLoading = true
this.$http({
url: this.$http.adornUrl('/product/category/list/tree'),
method: 'get'
}).then(({data}) => {
console.log('成功获取到菜单数据', data.data)
this.tableData = data.data
})
}
},
created () {
// 获取数据列表
this.getDataList()
}
}
</script>
<style scoped>
</style>
剩下的三级分类暂且跳过
14 品牌管理 逆向生成前后端代码
首先去你代码生成的product代码下找到这两个文件
把它粘贴到你前端对应的product下
接着放行权限 让新增能够在页面中显示出来
全局搜索isAuth
发现在index.js中
我们注释掉原有代码
一直让其返回True
最后打开页面效果如下
14 品牌管理 效果优化 快速展示开关
思路
我们定义一个开关来控制显示状态
最终效果如下
那我们首先自定义表格列
去element-ui找到
拿取其中代码
我们要的只是这个template>
带column是方便你们理解
<el-table-column
label="日期"
width="180">
<template slot-scope="scope">
<i class="el-icon-time"></i>
<span style="margin-left: 10px">{{ scope.row.date }}</span>
</template>
</el-table-column>
把这个template粘贴到显示状态那一列
思路:
当1 时显示 0时不显示
当我们的switch状态更改时实时改变数据库中的showstaus状态
于是我们编写的swtich代码如下
<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"
:active-value="1"
:inactive-value="0"
@change="updateBrandStatus(scope.row)"
></el-switch>
</template>
</el-table-column>
监听函数代码如下
这样我们一点击数据库中的值也会实时更新
updateBrandStatus (data) {
console.log('最新信息', data)
let { brandId, showStatus } = data
// 发送请求修改状态
this.$http({
// url: this.$http.adornUrl('/product/brand/update/status'),
url: this.$http.adornUrl('/product/brand/update'),
method: 'post',
data: this.$http.adornData({ brandId, showStatus }, false)
}).then(({ data }) => {
this.$message({
type: 'success',
message: '状态更新成功'
})
})
},
15 品牌管理 云储存开通和使用
1 云存储简介
首先去到阿里云官网
登录上你的账号
然后开通对象存储oss服务
这个项目总共花费也就几元钱
配置参照
选多了估计要付费多 孩子
2 上传方式的选择
我们选择服务器签名后直传的方式
避免了普通上传方式的繁琐
16 品牌管理 OSS整合测试
1 导入SDK依赖
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.10.2</version>
</dependency>
2 创建RAM用户
这个子用户来操作OSS对象存储系统
避免了主账号密码丢失造成的巨大风险
3 使用代码进行文件上传
import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import java.io.FileInputStream;
import java.io.InputStream;
public class Demo {
public static void main(String[] args) throws Exception {
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
String accessKeyId = "yourAccessKeyId";
String accessKeySecret = "yourAccessKeySecret";
// 填写Bucket名称,例如examplebucket。
String bucketName = "examplebucket";
// 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
String objectName = "exampledir/exampleobject.txt";
// 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
// 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
String filePath= "D:\\localpath\\examplefile.txt";
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
InputStream inputStream = new FileInputStream(filePath);
// 创建PutObject请求。
ossClient.putObject(bucketName, objectName, inputStream);
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
}
4 优化文件上传 --------------------------------------------------------(推荐使用这种方式而非传统的SDK,如果你之前已经学习过这个项目 请直接参照这个部分进行代码编写)
spring-cloud-oss官方文档.
1
首先引入依赖
因为可能别的模块也会用到文件上传
所以我们把这个简化依赖引入到guli-common模块当中
<!--alibaba-oss启动器-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alicloud-oss</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
</dependencies>
如果报错not found可以参考我的这个博客进行解决
Cannot resolve com.alibaba.cloud:spring-cloud-starter-alicloud-oss:unknown.
2 在配置文件中进行oss的配置
参考代码如下
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
alicloud:
access-key: cccc
secret-key: ccccc
oss:
endpoint: oss-cn-shanghai.aliyuncs.com
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
url: jdbc:mysql://localhost:3306/gulimall_pms?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2B8
application:
name: gulimall-product
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
id-type: auto
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
server:
port: 10001
3
测试使用
可以参照我的代码
编写代码进行上传测试
首先依赖注入OSSclient
@Resource
OSSClient ossClient;
@GetMapping("/oss")
public String oss(){
// // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
// String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
// // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
// String accessKeyId = "yourAccessKeyId";
// String accessKeySecret = "yourAccessKeySecret";
// 填写Bucket名称,例如examplebucket。
String bucketName = "gulimall-yyla1";
// 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
String objectName = "0d40c24b264aa511.jpg";
// 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
// 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
String filePath= "H:\\谷\\官方资料\\1.分布式基础(全栈开发篇)\\docs\\pics\\0d40c24b264aa511.jpg";
// // 创建OSSClient实例。
// OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
InputStream inputStream = new FileInputStream(filePath);
// 创建PutObject请求。
ossClient.putObject(bucketName, objectName, inputStream);
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
return "oss ok";
}
上传成功
17 品牌管理 OSS获取服务端签名
思路:
因为我们之前的方式文件上传还是经过服务器了
所以我们这次准备加以改进
使用如下的这种方式
1首先再来创建一个新的项目模块来进行第三方服务的操作
参考如下
接着转移依赖
把gulicommon中的spring-cloud-alibaba-oss转移到这个模块当中
不这样的话我们每个引入common的模块都要进行oss的配置
不然会报错
记得配置你的allibaba-cloud-dependencymaneger
完整依赖参考
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>yyl</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gulimall-third-party</name>
<description>gulimall-third-party</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2021.0.2</spring-cloud.version>
</properties>
<dependencies>
<!-- gulimall-common模块-->
<dependency>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall_common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<!-- 这个模块不使用数据库 排除-->
<exclusions>
<exclusion>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--alibaba-oss启动器-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alicloud-oss</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
<!--web模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- oppenfeign服务间调用-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<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.2.7.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2接着把这个模块注册到配置中心当中
完整代码参考
结构参考
1 appilicaiton.yml
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
# in nacos
# alicloud:
# access-key: LTAI5tLTqoGq426o9HB89xnn
# secret-key: dZdfBwxGaA7wmwDPiVctIqVpSl0o2t
# oss:
# endpoint: oss-cn-shanghai.aliyuncs.com
application:
name: gulimall-third-party
server:
port: 30000
2 bootstrap.properties
# ??Nacos-?????????
spring.application.name=gulimall-third-party
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=c392fc56-4da0-4ab3-9540-0d0c621721be
#spring.cloud.nacos.config.group=prop
#加载配置集
spring.cloud.nacos.config.extension-configs[0].data-id=oss.yml
spring.cloud.nacos.config.extension-configs[0].group=DEFAULT_GROUP
spring.cloud.nacos.config.extension-configs[0].refresh=true
3 启动测试
已经注册到了服务中心当中
4 进行获取oss签名的操作
参考结构如下
1 新建一个controller来进行获取签名操作
参考代码如下
OSScontroller
package com.atguigu.gulimall.thirdparty.controller;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClient;
import com.aliyun.oss.common.utils.BinaryUtil;
import com.aliyun.oss.model.MatchMode;
import com.aliyun.oss.model.PolicyConditions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
@RestController
public class OssController {
@Autowired
OSS client;
@Value("${spring.cloud.alicloud.oss.endpoint}")
private String endpoint;
@Value("${spring.cloud.alicloud.access-key}")
private String accessId;
@RequestMapping("/oss/policy")
public Map<String, String> policy(){
/**
* 因为配置了引入了Pom依赖 并且配置了aplliciton.yml文件 所以这几行用不到了
*/
// // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
// String accessId = "yourAccessKeyId";
// String accessKey = "yourAccessKeySecret";
// // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
// String endpoint = "oss-cn-hangzhou.aliyuncs.com";
// OSSClient client = new OSSClient(endpoint, accessId, accessKey);
//https://gulimall-hello.oss-cn-beijing.aliyuncs.com/hahaha.jpg
// 填写Bucket名称,例如examplebucket。
String bucket = "gulimall-yyla1";
// 填写Host地址,格式为https://bucketname.endpoint。
String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint
// String host = "https://examplebucket.oss-cn-hangzhou.aliyuncs.com";
/**
* 上传毁回调暂时用不到
*/
// 设置上传回调URL,即回调服务器地址,用于处理应用服务器与OSS之间的通信。OSS会在文件上传完成后,把文件上传信息通过此回调URL发送给应用服务器。
// String callbackUrl = "https://192.168.0.0:8888";
/**
* 这里设置成当天存储当天的
*/
// 设置上传到OSS文件的前缀,可置空此项。置空后,文件将上传至Bucket的根目录下。
String format = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
String dir = format + "/"; // 用户上传文件时指定的前缀。
Map<String, String> respMap = null;
try {
long expireTime = 30;
long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
Date expiration = new Date(expireEndTime);
PolicyConditions policyConds = new PolicyConditions();
policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);
String postPolicy = client.generatePostPolicy(expiration, policyConds);
byte[] binaryData = postPolicy.getBytes("utf-8");
String encodedPolicy = BinaryUtil.toBase64String(binaryData);
String postSignature = client.calculatePostSignature(postPolicy);
respMap = new LinkedHashMap<String, String>();
respMap.put("accessid", accessId);
respMap.put("policy", encodedPolicy);
respMap.put("signature", postSignature);
respMap.put("dir", dir);
respMap.put("host", host);
respMap.put("expire", String.valueOf(expireEndTime / 1000));
// respMap.put("expire", formatISO8601Date(expiration));
// JSONObject jasonCallback = new JSONObject();
// jasonCallback.put("callbackUrl", callbackUrl);
// jasonCallback.put("callbackBody",
// "filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}");
// jasonCallback.put("callbackBodyType", "application/x-www-form-urlencoded");
// String base64CallbackBody = BinaryUtil.toBase64String(jasonCallback.toString().getBytes());
// respMap.put("callback", base64CallbackBody);
//
// JSONObject ja1 = JSONObject.fromObject(respMap);
// // System.out.println(ja1.toString());
// response.setHeader("Access-Control-Allow-Origin", "*");
// response.setHeader("Access-Control-Allow-Methods", "GET, POST");
// response(request, response, ja1.toString());
} catch (Exception e) {
// Assert.fail(e.getMessage());
System.out.println(e.getMessage());
}
return respMap;
}
}
2 配置文件如下(controller中注入了很多依赖中定义的数据)
1 application.yml
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
alicloud:
access-key: xxxxxx
secret-key: xxxxxx
oss:
endpoint: oss-cn-shanghai.aliyuncs.com
application:
name: gulimall-third-party
server:
port: 30000
2 bootstrap.yml
# ??Nacos-?????????
spring.application.name=gulimall-third-party
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=c392fc56-4da0-4ab3-9540-0d0c621721be
#spring.cloud.nacos.config.group=prop
#加载配置集
spring.cloud.nacos.config.extension-configs[0].data-id=oss.yml
spring.cloud.nacos.config.extension-configs[0].group=DEFAULT_GROUP
spring.cloud.nacos.config.extension-configs[0].refresh=true
测试发现返回签名成功
3 配置网关转发路由
我们配置网关转发路由 访问网关来获取签名
代码如下
找到third_party_route
#Router And Predicates And Nacos discovery
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
routes:
- id: test_route
uri: https://www.baidu.com
predicates:
- Query=url,baidu
- id: qq_route
uri: https://www.qq.com
predicates:
- Query=url,qq
- id: product_route
uri: lb://gulimall-product
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?<segment>.*),/$\{segment}
- id: third_party_route
uri: lb://gulimall-third-party
predicates:
- Path=/api/thirdparty/**
filters:
- RewritePath=/api/thirdparty/(?<segment>.*),/$\{segment}
- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**
filters:
- RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}
## 前端项目,/api
## http://localhost:88/api/captcha.jpg http://localhost:8080/renren-fast/captcha.jpg
## http://localhost:88/api/product/category/list/tree http://localhost:10000/product/category/list/tree
application:
name: gulimall-gateway
server:
port: 88
logging:
level:
com.atguigu.gulimall: debug
配置好后重启网关
我们发送请求进行测试
http://localhost:88/api/thirdparty/oss/policy
18 品牌管理 OSS前后联调测试上传
把upload文件夹粘贴到自己前端项目的目录下
更改其中的
action 文件上传位置
这个位置设成自己bucket外网访问链接
接站在brand-add-or-update作为组件引入并进行使用
brand-add-or-update
完整代码示例如下
<template>
<el-dialog
:title="!dataForm.brandId ? '新增' : '修改'"
:close-on-click-modal="false"
:visible.sync="visible">
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" @keyup.enter.native="dataFormSubmit()" label-width="80px">
<el-form-item label="品牌名" prop="name">
<el-input v-model="dataForm.name" placeholder="品牌名"></el-input>
</el-form-item>
<el-form-item label="品牌logo地址" prop="logo">
<!-- <el-input v-model="dataForm.logo" placeholder="品牌logo地址"></el-input>-->
<single-upload v-model="dataForm.logo"></single-upload>
</el-form-item>
<el-form-item label="介绍" prop="descript">
<el-input v-model="dataForm.descript" placeholder="介绍"></el-input>
</el-form-item>
<el-form-item label="显示状态" prop="showStatus">
<el-input v-model="dataForm.showStatus" placeholder="显示状态[0-不显示;1-显示]"></el-input>
</el-form-item>
<el-form-item label="检索首字母" prop="firstLetter">
<el-input v-model="dataForm.firstLetter" placeholder="检索首字母"></el-input>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input v-model="dataForm.sort" placeholder="排序"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="dataFormSubmit()">确定</el-button>
</span>
</el-dialog>
</template>
<script>
import SingleUpload from "@/components/upload/singleUpload";
export default {
components: { SingleUpload },
data () {
return {
visible: false,
dataForm: {
brandId: 0,
name: '',
logo: '',
descript: '',
showStatus: '',
firstLetter: '',
sort: ''
},
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: [
{ required: true, message: '检索首字母不能为空', trigger: 'blur' }
],
sort: [
{ required: true, message: '排序不能为空', trigger: 'blur' }
]
}
}
},
methods: {
init (id) {
this.dataForm.brandId = id || 0
this.visible = true
this.$nextTick(() => {
this.$refs['dataForm'].resetFields()
if (this.dataForm.brandId) {
this.$http({
url: this.$http.adornUrl(`/product/brand/info/${this.dataForm.brandId}`),
method: 'get',
params: this.$http.adornParams()
}).then(({data}) => {
if (data && data.code === 0) {
this.dataForm.name = data.brand.name
this.dataForm.logo = data.brand.logo
this.dataForm.descript = data.brand.descript
this.dataForm.showStatus = data.brand.showStatus
this.dataForm.firstLetter = data.brand.firstLetter
this.dataForm.sort = data.brand.sort
}
})
}
})
},
// 表单提交
dataFormSubmit () {
this.$refs['dataForm'].validate((valid) => {
if (valid) {
this.$http({
url: this.$http.adornUrl(`/product/brand/${!this.dataForm.brandId ? 'save' : 'update'}`),
method: 'post',
data: this.$http.adornData({
'brandId': this.dataForm.brandId || undefined,
'name': this.dataForm.name,
'logo': this.dataForm.logo,
'descript': this.dataForm.descript,
'showStatus': this.dataForm.showStatus,
'firstLetter': this.dataForm.firstLetter,
'sort': this.dataForm.sort
})
}).then(({data}) => {
if (data && data.code === 0) {
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
this.visible = false
this.$emit('refreshDataList')
}
})
} else {
this.$message.error(data.msg)
}
})
}
})
}
}
}
</script>
设置自己bucket的跨域权限
(让浏览器能给你的oss对象存储发数据)
最后上传测试
19 品牌管理 表单校验,自定义校验器
1 首先更新你前端项目使用的组件库
更新如下
/**
* UI组件, 统一使用饿了么桌面端组件库(https://github.com/ElemeFE/element)
*
* 使用:
* 1. 项目中需要的组件进行释放(解开注释)
*
* 注意:
* 1. 打包只会包含释放(解开注释)的组件, 减少打包文件大小
*/
import Vue from 'vue'
import {
Pagination,
Dialog,
Autocomplete,
Dropdown,
DropdownMenu,
DropdownItem,
Menu,
Submenu,
MenuItem,
MenuItemGroup,
Input,
InputNumber,
Radio,
RadioGroup,
RadioButton,
Checkbox,
CheckboxButton,
CheckboxGroup,
Switch,
Select,
Option,
OptionGroup,
Button,
ButtonGroup,
Table,
TableColumn,
DatePicker,
TimeSelect,
TimePicker,
Popover,
Tooltip,
Breadcrumb,
BreadcrumbItem,
Form,
FormItem,
Tabs,
TabPane,
Tag,
Tree,
Alert,
Slider,
Icon,
Row,
Col,
Upload,
Progress,
Spinner,
Badge,
Card,
Rate,
Steps,
Step,
Carousel,
CarouselItem,
Collapse,
CollapseItem,
Cascader,
ColorPicker,
Transfer,
Container,
Header,
Aside,
Main,
Footer,
Timeline,
TimelineItem,
Link,
Divider,
Image,
Calendar,
Loading,
MessageBox,
Message,
Notification
} from 'element-ui'
Vue.use(Pagination)
Vue.use(Dialog)
Vue.use(Autocomplete)
Vue.use(Dropdown)
Vue.use(DropdownMenu)
Vue.use(DropdownItem)
Vue.use(Menu)
Vue.use(Submenu)
Vue.use(MenuItem)
Vue.use(MenuItemGroup)
Vue.use(Input)
Vue.use(InputNumber)
Vue.use(Radio)
Vue.use(RadioGroup)
Vue.use(RadioButton)
Vue.use(Checkbox)
Vue.use(CheckboxButton)
Vue.use(CheckboxGroup)
Vue.use(Switch)
Vue.use(Select)
Vue.use(Option)
Vue.use(OptionGroup)
Vue.use(Button)
Vue.use(ButtonGroup)
Vue.use(Table)
Vue.use(TableColumn)
Vue.use(DatePicker)
Vue.use(TimeSelect)
Vue.use(TimePicker)
Vue.use(Popover)
Vue.use(Tooltip)
Vue.use(Breadcrumb)
Vue.use(BreadcrumbItem)
Vue.use(Form)
Vue.use(FormItem)
Vue.use(Tabs)
Vue.use(TabPane)
Vue.use(Tag)
Vue.use(Tree)
Vue.use(Alert)
Vue.use(Slider)
Vue.use(Icon)
Vue.use(Row)
Vue.use(Col)
Vue.use(Upload)
Vue.use(Progress)
Vue.use(Spinner)
Vue.use(Badge)
Vue.use(Card)
Vue.use(Rate)
Vue.use(Steps)
Vue.use(Step)
Vue.use(Carousel)
Vue.use(CarouselItem)
Vue.use(Collapse)
Vue.use(CollapseItem)
Vue.use(Cascader)
Vue.use(ColorPicker)
Vue.use(Transfer)
Vue.use(Container)
Vue.use(Header)
Vue.use(Aside)
Vue.use(Main)
Vue.use(Footer)
Vue.use(Timeline)
Vue.use(TimelineItem)
Vue.use(Link)
Vue.use(Divider)
Vue.use(Image)
Vue.use(Calendar)
Vue.use(Loading.directive)
Vue.prototype.$loading = Loading.service
Vue.prototype.$msgbox = MessageBox
Vue.prototype.$alert = MessageBox.alert
Vue.prototype.$confirm = MessageBox.confirm
Vue.prototype.$prompt = MessageBox.prompt
Vue.prototype.$notify = Notification
Vue.prototype.$message = Message
Vue.prototype.$ELEMENT = { size: 'medium' }
2 接着引入在项目中展示你的Logo图片
<el-table-column
prop="logo"
header-align="center"
align="center"
label="品牌logo地址">
<template slot-scope="scope">
<!-- <i class="el-icon-time"></i>-->
<!-- <span style="margin-left: 10px">{{ scope.row.date }}</span>-->
<!-- <el-image-->
<!-- style="width: 100px; height: 80px"-->
<!-- :src="scope.row.logo"-->
<!-- :fit="fit"></el-image>-->
<img :src="scope.row.logo" style="width: 100px; height: 80px" />
</template>
</el-table-column>
3最后在新增中增加前端表单校验
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: (rule, value, callback) => {
if (value == "") {
callback(new Error("首字母必须填写"));
} else if (!/^[a-zA-Z]$/.test(value)) {
callback(new Error("首字母必须a-z或者A-Z之间"));
} else {
callback();
}
},
trigger: "blur"
}
],
sort: [
{
validator: (rule, value, callback) => {
if (value === '') {
callback(new Error("排序字段必须填写"));
} else if (!/^[0-9]$/.test(value) || value<0 ) {
callback(new Error("排序必须是一个大于等于0的整数"));
} else {
callback();
}
},
trigger: "blur"
}
]
}
完整代码
<template>
<el-dialog
:title="!dataForm.brandId ? '新增' : '修改'"
:close-on-click-modal="false"
:visible.sync="visible">
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" @keyup.enter.native="dataFormSubmit()" label-width="80px">
<el-form-item label="品牌名" prop="name">
<el-input v-model="dataForm.name" placeholder="品牌名"></el-input>
</el-form-item>
<el-form-item label="品牌logo地址" prop="logo">
<!-- <el-input v-model="dataForm.logo" placeholder="品牌logo地址"></el-input>-->
<single-upload v-model="dataForm.logo"></single-upload>
</el-form-item>
<el-form-item label="介绍" prop="descript">
<el-input v-model="dataForm.descript" placeholder="介绍"></el-input>
</el-form-item>
<el-form-item label="显示状态" prop="showStatus">
<!-- <el-input v-model="dataForm.showStatus" placeholder="显示状态[0-不显示;1-显示]"></el-input>-->
<el-switch
v-model="dataForm.showStatus"
active-color="#13ce66"
inactive-color="#ff4949"
:active-value="1"
:inactive-value="0"
></el-switch>
</el-form-item>
<el-form-item label="检索首字母" prop="firstLetter">
<el-input v-model="dataForm.firstLetter" placeholder="检索首字母"></el-input>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input v-model="dataForm.sort" placeholder="排序"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="dataFormSubmit()">确定</el-button>
</span>
</el-dialog>
</template>
<script>
import SingleUpload from "@/components/upload/singleUpload";
export default {
components: { SingleUpload },
data () {
return {
visible: false,
dataForm: {
brandId: 0,
name: '',
logo: '',
descript: '',
showStatus: '',
firstLetter: '',
sort: ''
},
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: (rule, value, callback) => {
if (value == "") {
callback(new Error("首字母必须填写"));
} else if (!/^[a-zA-Z]$/.test(value)) {
callback(new Error("首字母必须a-z或者A-Z之间"));
} else {
callback();
}
},
trigger: "blur"
}
],
sort: [
{
validator: (rule, value, callback) => {
if (value === '') {
callback(new Error("排序字段必须填写"));
} else if (!/^[0-9]$/.test(value) || value<0 ) {
callback(new Error("排序必须是一个大于等于0的整数"));
} else {
callback();
}
},
trigger: "blur"
}
]
}
}
},
methods: {
init (id) {
this.dataForm.brandId = id || 0
this.visible = true
this.$nextTick(() => {
this.$refs['dataForm'].resetFields()
if (this.dataForm.brandId) {
this.$http({
url: this.$http.adornUrl(`/product/brand/info/${this.dataForm.brandId}`),
method: 'get',
params: this.$http.adornParams()
}).then(({data}) => {
if (data && data.code === 0) {
this.dataForm.name = data.brand.name
this.dataForm.logo = data.brand.logo
this.dataForm.descript = data.brand.descript
this.dataForm.showStatus = data.brand.showStatus
this.dataForm.firstLetter = data.brand.firstLetter
this.dataForm.sort = data.brand.sort
}
})
}
})
},
// 表单提交
dataFormSubmit () {
this.$refs['dataForm'].validate((valid) => {
if (valid) {
this.$http({
url: this.$http.adornUrl(`/product/brand/${!this.dataForm.brandId ? 'save' : 'update'}`),
method: 'post',
data: this.$http.adornData({
'brandId': this.dataForm.brandId || undefined,
'name': this.dataForm.name,
'logo': this.dataForm.logo,
'descript': this.dataForm.descript,
'showStatus': this.dataForm.showStatus,
'firstLetter': this.dataForm.firstLetter,
'sort': this.dataForm.sort
})
}).then(({data}) => {
if (data && data.code === 0) {
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
this.visible = false
this.$emit('refreshDataList')
}
})
} else {
this.$message.error(data.msg)
}
})
}
})
}
}
}
</script>
20 品牌管理 JSR303校验
我们先给后端加上简单的JSR303校验
0 思路:
- JSR303
- 1)、给Bean添加校验注解:javax.validation.constraints,并定义自己的message提示
- 2)、开启校验功能@Valid
-
效果:校验错误以后会有默认的响应;
- 3)、给校验的bean后紧跟一个BindingResult,就可以获取到校验的结果
1 我们这里给brand实体类来加上注解
package com.atguigu.gulimall.product.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.*;
/**
* 品牌
*
* @author yyl
* @email sunlightcs@gmail.com
* @date 2022-04-24 19:01:01
*/
@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地址
*/
@NotBlank
@URL(message = "logo必须是一个合法的url地址")
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
@NotNull(message = "展示状态不能为空")
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty
@Pattern(regexp="^[a-zA-Z]$",message = "检索首字母必须是一个字母")
private String firstLetter;
/**
* 排序
*/
@NotNull
@Min(value = 0,message = "排序必须大于等于0")
private Integer sort;
}
2 错误解决 注解不生效问题
首先看看你导的是不是java.javax.validation.constraints.*;
如果不是引入一下依赖
<!-- 校验注解-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.6.7</version>
</dependency>
更新依赖引入后如果重启还不生效
maven
先clean 再 compile试试
3 controller层开启校验功能
代码如下
/**
* 保存
*/
@RequestMapping("/save")
//@RequiresPermissions("product:brand:save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){
if(result.hasErrors()){
Map<String,String> map = new HashMap<>();
//1、获取校验的错误结果
result.getFieldErrors().forEach((item)->{
//FieldError 获取到错误提示
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();
}
}
21 品牌管理- 统一异常处理
0 思路:
- 4、统一的异常处理
- @ControllerAdvice
- 1)、编写异常处理类,使用@ControllerAdvice。
- 2)、使用@ExceptionHandler标注方法可以处理的异常。
1 首先在product模块下建一个全局异常捕获类
这样我们再product.controller下的异常都能被这个类捕获到进行统一处理
package com.atguigu.gulimall.product.exception;
import com.atguigu.common.exception.BizCodeEnume;
import com.atguigu.common.utils.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
/**
* 集中处理所有异常
*/
@Slf4j
//@ResponseBody
//@ControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
@RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {
@ExceptionHandler(value= MethodArgumentNotValidException.class)
public R handleVaildException(MethodArgumentNotValidException e){
log.error("数据校验出现问题{},异常类型:{}",e.getMessage(),e.getClass());
BindingResult bindingResult = e.getBindingResult();
Map<String,String> errorMap = new HashMap<>();
bindingResult.getFieldErrors().forEach((fieldError)->{
errorMap.put(fieldError.getField(),fieldError.getDefaultMessage());
});
return R.error(BizCodeEnume.VAILD_EXCEPTION.getCode(),BizCodeEnume.VAILD_EXCEPTION.getMsg()).put("data",errorMap);
}
@ExceptionHandler(value = Throwable.class)
public R handleException(Throwable throwable){
log.error("错误:",throwable);
return R.error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(),BizCodeEnume.UNKNOW_EXCEPTION.getMsg());
}
}
2 我们在common模块下定义一个全局枚举
这样进行统一状态码的返回更加规范
我们在product模块下的全局异常捕获类中已经使用它了
/**
* Copyright (c) 2016-2019 人人开源 All rights reserved.
*
* https://www.renren.io
*
* 版权所有,侵权必究!
*/
package com.atguigu.common.exception;
/**
* 自定义异常
*
* @author Mark sunlightcs@gmail.com
*/
public class RRException extends RuntimeException {
private static final long serialVersionUID = 1L;
private String msg;
private int code = 500;
public RRException(String msg) {
super(msg);
this.msg = msg;
}
public RRException(String msg, Throwable e) {
super(msg, e);
this.msg = msg;
}
public RRException(String msg, int code) {
super(msg);
this.msg = msg;
this.code = code;
}
public RRException(String msg, int code, Throwable e) {
super(msg, e);
this.msg = msg;
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
22 品牌管理 JSR303分组校验
思路:
- 4)、分组校验(多场景的复杂校验)
-
1)、 @NotBlank(message = "品牌名必须提交",groups = {AddGroup.class,UpdateGroup.class})
-
给校验注解标注什么情况需要进行校验
-
2)、@Validated({AddGroup.class})
-
3)、默认没有指定分组的校验注解@NotBlank,在分组校验情况@Validated({AddGroup.class})下不生效,只会在@Validated生效;
1 在 common模块下创建一个vaild包
里面定义AddGroup接口和UpdateGroup接口
2 在实体类上定义组分类
形式(groups = {组接口.class})
package com.atguigu.gulimall.product.entity;
import com.atguigu.common.valid.AddGroup;
import com.atguigu.common.valid.ListValue;
import com.atguigu.common.valid.UpdateGroup;
import com.atguigu.common.valid.UpdateStatusGroup;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.*;
/**
* 品牌
*
* @author leifengyang
* @email leifengyang@gmail.com
* @date 2019-10-01 21:08:49
*/
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@NotNull(message = "修改必须指定品牌id",groups = {UpdateGroup.class})
@Null(message = "新增不能指定id",groups = {AddGroup.class})
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名必须提交",groups = {AddGroup.class,UpdateGroup.class})
private String name;
/**
* 品牌logo地址
*/
@NotBlank(groups = {AddGroup.class})
@URL(message = "logo必须是一个合法的url地址",groups={AddGroup.class,UpdateGroup.class})
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
// @Pattern()
@NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})
@ListValue(vals={0,1},groups = {AddGroup.class, UpdateStatusGroup.class})
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty(groups={AddGroup.class})
@Pattern(regexp="^[a-zA-Z]$",message = "检索首字母必须是一个字母",groups={AddGroup.class,UpdateGroup.class})
private String firstLetter;
/**
* 排序
*/
@NotNull(groups={AddGroup.class})
@Min(value = 0,message = "排序必须大于等于0",groups={AddGroup.class,UpdateGroup.class})
private Integer sort;
}
3 在controller层上使用@vaildated(接口.class)开启分组校验(这时候如果实体类上没有指定组的校验将会失效)
/**
* 修改
*/
@RequestMapping("/update")
//@RequiresPermissions("product:brand:update")
public R update(@Validated(UpdateGroup.class) @RequestBody BrandEntity brand){
brandService.updateById(brand);
return R.ok();
}
22 JSR303 自定义校验注解
思路:
- 5)、自定义校验
-
1)、编写一个自定义的校验注解
-
2)、编写一个自定义的校验器 ConstraintValidator
-
3)、关联自定义的校验器和自定义的校验注解 * @Documented * @Constraint(validatedBy = { ListValueConstraintValidator.class【可以指定多个不同的校验器,适配不同类型的校验】 }) * @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) * @Retention(RUNTIME) * public @interface ListValue {
1 在自己对应实体类上编写自定义注解
/**
* 显示状态[0-不显示;1-显示]
*/
// @Pattern()
@NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})
@ListValue(vals={0,1},groups = {AddGroup.class, UpdateStatusGroup.class})
private Integer showStatus;
2 编写注解接口
参考结构
package com.atguigu.common.valid;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Documented
//指定校验器 这里指定我们自定义的ListValueConstraintValidator
@Constraint(validatedBy = { ListValueConstraintValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
//使用我们自己定义的错误信息 在common模块下的resources中
// String message() default "{com.atguigu.common.valid.ListValue.message}";
String message() default "值必须是0或者1";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
//预先准备的值 vals={0,1}
int[] vals() default { };
}
//对应实体类的注解
//@ListValue(vals={0,1},groups = {AddGroup.class, UpdateStatusGroup.class})
3 编写注解校验器
package com.atguigu.common.valid;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;
//对应实体类的注解
//@ListValue(vals={0,1},groups = {AddGroup.class, UpdateStatusGroup.class})
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
private Set<Integer> set = new HashSet<>();
//初始化方法
@Override
public void initialize(ListValue constraintAnnotation) {
//从注解上获取值 遍历将其存入一个set
int[] vals = constraintAnnotation.vals();
for (int val : vals) {
set.add(val);
}
}
//判断是否校验成功
/**
*
* @param value 需要校验的值
* @param context
* @return
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
//拿到传入的值 判断set中是否包含这个值
//不包含返回false
return set.contains(value);
}
}
4 controller层开启
/**
* 修改状态
*/
@RequestMapping("/update/status")
//@RequiresPermissions("product:brand:update")
public R updateStatus(@Validated(UpdateStatusGroup.class) @RequestBody BrandEntity brand){
brandService.updateById(brand);
return R.ok();
}
5 流程总结
controller开启Vailid或者Validated进行校验 -> 到实体类的校验注解查看校验策略(分组 和注解内容,如notnull等) ->分支1 非定义注解 验证后通过或是返回错误信息——>分支2 自定义注解 去自定义注解中找到校验器
然后进行校验判断
23 商品服务-概念-SPU SKU 规格参数 销售属性
24 商品服务-API-属性分组-前端组件抽取&父子组件交互
1 前端组件抽取
1 首先更新gulimall-admin的sys-menu表
更新前端菜单展示的数据
跟着视频来导入sql
2 接着我们创建下面图片对应的前端目录
接着我们在其中引入element-ui的分栏间隔组件
3 把我们的目录树组件抽出为一个common方法供后续使用
<template>
<div>
<el-tree
:data="menus"
:props="defaultProps"
node-key="catId"
ref="menuTree"
></el-tree>
</div>
</template>
<script>
export default {
components: {},
props: {},
data() {
//这里存放数据
return {
filterText: "",
menus: [],
expandedKey: [],
defaultProps: {
children: "children",
label: "name"
}
};
},
computed: {},
//方法集合
methods: {
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: 'get'
}).then(({ data }) => {
this.menus = data.data;
});
},
},
created () {
this.getMenus();
},
};
</script>
<style>
</style>
4 接着在我们的属性分组下导入这个组件
<template>
<el-row :gutter="20">
<el-col :span="6">
<category></category>
<div class="grid-content bg-purple"></div></el-col>
<el-col :span="18">表格<div class="grid-content bg-purple"></div></el-col>
</el-row>
</template>
<script>
import Category from '../common/category';
export default {
components: {Category},
name: 'attrgroup',
data () {
return {}
},
methods: {}
}
</script>
<style scoped>
</style>
5 最终效果
2 对attrgroup.vue进行进一步完善
我们使用代码生成器自动生成的代码对这个
attrgroup.vue的页面进行进一步的完善
attrgroup.vue具体代码如下
<template>
<el-row :gutter="20">
<el-col :span="6">
<category></category>
<div class="grid-content bg-purple"></div></el-col>
<el-col :span="18">
<div class="mod-config">
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<el-form-item>
<el-input v-model="dataForm.key" placeholder="参数名" clearable></el-input>
</el-form-item>
<el-form-item>
<el-button @click="getDataList()">查询</el-button>
<el-button v-if="isAuth('product:attrgroup:save')" type="primary" @click="addOrUpdateHandle()">新增</el-button>
<el-button v-if="isAuth('product:attrgroup:delete')" type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0">批量删除</el-button>
</el-form-item>
</el-form>
<el-table
:data="dataList"
border
v-loading="dataListLoading"
@selection-change="selectionChangeHandle"
style="width: 100%;">
<el-table-column
type="selection"
header-align="center"
align="center"
width="50">
</el-table-column>
<el-table-column
prop="attrGroupId"
header-align="center"
align="center"
label="分组id">
</el-table-column>
<el-table-column
prop="attrGroupName"
header-align="center"
align="center"
label="组名">
</el-table-column>
<el-table-column
prop="sort"
header-align="center"
align="center"
label="排序">
</el-table-column>
<el-table-column
prop="descript"
header-align="center"
align="center"
label="描述">
</el-table-column>
<el-table-column
prop="icon"
header-align="center"
align="center"
label="组图标">
</el-table-column>
<el-table-column
prop="catelogId"
header-align="center"
align="center"
label="所属分类id">
</el-table-column>
<el-table-column
fixed="right"
header-align="center"
align="center"
width="150"
label="操作">
<template slot-scope="scope">
<el-button type="text" size="small" @click="addOrUpdateHandle(scope.row.attrGroupId)">修改</el-button>
<el-button type="text" size="small" @click="deleteHandle(scope.row.attrGroupId)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
:current-page="pageIndex"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
:total="totalPage"
layout="total, sizes, prev, pager, next, jumper">
</el-pagination>
<!-- 弹窗, 新增 / 修改 -->
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="getDataList"></add-or-update>
</div>
<div class="grid-content bg-purple"></div></el-col>
</el-row>
</template>
<script>
import AddOrUpdate from './attrgroup-add-or-update'
import Category from '../common/category'
export default {
components: {Category, AddOrUpdate},
name: 'attrgroup',
data () {
return {
dataForm: {
key: ''
},
dataList: [],
pageIndex: 1,
pageSize: 10,
totalPage: 0,
dataListLoading: false,
dataListSelections: [],
addOrUpdateVisible: false
}
},
methods: {
// 获取数据列表
getDataList () {
this.dataListLoading = true
this.$http({
url: this.$http.adornUrl('/product/attrgroup/list'),
method: 'get',
params: this.$http.adornParams({
'page': this.pageIndex,
'limit': this.pageSize,
'key': this.dataForm.key
})
}).then(({data}) => {
if (data && data.code === 0) {
this.dataList = data.page.list
this.totalPage = data.page.totalCount
} else {
this.dataList = []
this.totalPage = 0
}
this.dataListLoading = false
})
},
// 每页数
sizeChangeHandle (val) {
this.pageSize = val
this.pageIndex = 1
this.getDataList()
},
// 当前页
currentChangeHandle (val) {
this.pageIndex = val
this.getDataList()
},
// 多选
selectionChangeHandle (val) {
this.dataListSelections = val
},
// 新增 / 修改
addOrUpdateHandle (id) {
this.addOrUpdateVisible = true
this.$nextTick(() => {
this.$refs.addOrUpdate.init(id)
})
},
// 删除
deleteHandle (id) {
var ids = id ? [id] : this.dataListSelections.map(item => {
return item.attrGroupId
})
this.$confirm(`确定对[id=${ids.join(',')}]进行[${id ? '删除' : '批量删除'}]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: this.$http.adornUrl('/product/attrgroup/delete'),
method: 'post',
data: this.$http.adornData(ids, false)
}).then(({data}) => {
if (data && data.code === 0) {
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
this.getDataList()
}
})
} else {
this.$message.error(data.msg)
}
})
})
}
}
}
</script>
<style scoped>
</style>
3 点击attrgroup.vue中的子组件让父组件获取子组件信息
1 首先子组件category.vue上加上事件
@node-click 节点被点击时回调
<template>
<div>
<el-tree
:data="menus"
:props="defaultProps"
node-key="catId"
@node-click="nodeclick"
ref="menuTree"
></el-tree>
</div>
</template>
2 接着在回调函数nodeclick给父组件发送信息
回调参数
node-click 节点被点击时的回调 共三个参数,依次为:传递给 data 属性的数组中该节点所对应的对象、节点对应的 Node、节点组件本身。
nodeclick(data, node, component) {
console.log("子组件category的节点被点击", data, node, component);
//向父组件发送事件;
this.$emit("tree-node-click", data, node, component);
},
3 父组件接收到发送的事件并展示事件发来的数据
<el-col :span="6">
<category @tree-node-click="treenodeclick"></category>
<div class="grid-content bg-purple"></div></el-col>
<el-col :span="18">
//感知树节点被点击
treenodeclick(data, node, component) {
// if (node.level == 3) {
// this.catId = data.catId;
// this.getDataList(); //重新查询
// }
console.log("传来的数据是",data);
},
最终页面效果
25 属性分组 获取分类属性分组
1 后端调试
1 controller
/**
* 列表 传一个三级目录id查出其下的数据
*/
@RequestMapping("/list/{catelogId}")
//接受一个map 的 参数params 和catelogId
public R list(@RequestParam Map<String, Object> params,
@PathVariable("catelogId") Long catelogId){
//调用service层方法
PageUtils page = attrGroupService.queryPage(params,catelogId);
return R.ok().put("page", page);
}
2 service
public interface AttrGroupService extends IService<AttrGroupEntity> {
PageUtils queryPage(Map<String, Object> params);
PageUtils queryPage(Map<String, Object> params, Long catelogId);
}
3 serviceimpl
// 请求参数 params
// {
// page: 1,//当前页码
// limit: 10,//每页记录数
// sidx: 'id',//排序字段
// order: 'asc/desc',//排序方式
// key: '华为'//检索关键字
// }
@Override
public PageUtils queryPage(Map<String, Object> params, Long catelogId) {
//1 首先拿到传过来参数中的key对应的参数
String key = (String) params.get("key");
//select * from pms_attr_group where catelog_id=? and (attr_group_id=key or attr_group_name like %key%)
//2 创造一个对应实体类的queryWrapper
QueryWrapper<AttrGroupEntity> wrapper = new QueryWrapper<AttrGroupEntity>();
//3 如果key关键字对应的不为空的话
if(!StringUtils.isEmpty(key)){
//要满足and (attr_group_id=key or attr_group_name like %key%)
wrapper.and((obj)->{
obj.eq("attr_group_id",key).or().like("attr_group_name",key);
});
}
//4 如果传来的catelogId为0
if( catelogId == 0){
//直接根据params和wrapper生成page
//Query utils不过是把params 变为 page
IPage<AttrGroupEntity> page = this.page(new Query<AttrGroupEntity>().getPage(params),
wrapper);
//把Ipage对象通过PageUtils进行处理 处理成我们自定义的字段返回给前端
return new PageUtils(page);
}else {
// 5 如果传来的cateLogId不为0 wrapper再次匹配这个字段
//Query utils不过是把params 变为 page
wrapper.eq("catelog_id",catelogId);
IPage<AttrGroupEntity> page = this.page(new Query<AttrGroupEntity>().getPage(params),
wrapper);
//把Ipage对象通过PageUtils进行处理 处理成我们自定义的字段返回给前端
return new PageUtils(page);
}
}
4 最终结果测试
sql语句
测试成功
2 前端代码改写
思路: 我们点三级分类的时候才获取分类属性分组
点一级二级不响应
默认传值0
如果点击三级分类再次调用方法查询
created () {
this.catId = 0
this.getDataList();
},
//感知树节点被点击
treenodeclick(data, node, component) {
console.log("子组件传来的数据是",data);
if (node.level == 3) {
this.catId = data.catId
this.getDataList(); //重新查询
}
},
// 获取数据列表
getDataList () {
this.dataListLoading = true
this.$http({
url: this.$http.adornUrl(`/product/attrgroup/list/${this.catId}`),
method: 'get',
params: this.$http.adornParams({
'page': this.pageIndex,
'limit': this.pageSize,
'key': this.dataForm.key
})
}).then(({data}) => {
if (data && data.code === 0) {
this.dataList = data.page.list
this.totalPage = data.page.totalCount
} else {
this.dataList = []
this.totalPage = 0
}
this.dataListLoading = false
})
},
26 属性分组 分组新增&级联选择器
1 思路
我们在添加属性分组上
https://element.eleme.cn/#/zh-CN/component/cascader
<el-form-item label="所属分类id" prop="catelogId">
<!-- <el-input v-model="dataForm.catelogId" placeholder="所属分类id"></el-input>-->
<el-cascader v-model="dataForm.catelogIds" :options="categorys" :props="props"></el-cascader>
</el-form-item>
:options=“categorys”
代表选择器的数据来源
我们访问catagoty controller来获取并对其赋值
created () {
this.getMenus()
},
methods: {
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: 'get'
}).then(({ data }) => {
this.categorys = data.data;
});
},
:props="props"绑定属性设置
value是什么
显示的name是什么
子节点是什么
data () {
return {
props:{
value:"catId",
label:"name",
children:"children"
},
由于我们的三级目录仍然包含children[]空集合
因此我们要在后端进行设置
否则级联选择器在三级目录后会跟上一个空目录
最终页面效果
但我们选择三级目录级联选择器的value值是一个数组
因此我们在添加时要选择最后一个值
测试添加效果成功
全部代码
<template>
<el-dialog
:title="!dataForm.attrGroupId ? '新增' : '修改'"
:close-on-click-modal="false"
:visible.sync="visible">
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" @keyup.enter.native="dataFormSubmit()" label-width="80px">
<el-form-item label="组名" prop="attrGroupName">
<el-input v-model="dataForm.attrGroupName" placeholder="组名"></el-input>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input v-model="dataForm.sort" placeholder="排序"></el-input>
</el-form-item>
<el-form-item label="描述" prop="descript">
<el-input v-model="dataForm.descript" placeholder="描述"></el-input>
</el-form-item>
<el-form-item label="组图标" prop="icon">
<el-input v-model="dataForm.icon" placeholder="组图标"></el-input>
</el-form-item>
<el-form-item label="所属分类id" prop="catelogId">
<!-- <el-input v-model="dataForm.catelogId" placeholder="所属分类id"></el-input>-->
<el-cascader v-model="dataForm.catelogIds" :options="categorys" :props="props"></el-cascader>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="dataFormSubmit()">确定</el-button>
</span>
</el-dialog>
</template>
<script>
export default {
data () {
return {
props:{
value:"catId",
label:"name",
children:"children"
},
categorys:[],
visible: false,
dataForm: {
attrGroupId: 0,
attrGroupName: '',
sort: '',
descript: '',
icon: '',
catelogIds: [],
catelogId: 0
},
dataRule: {
attrGroupName: [
{ required: true, message: '组名不能为空', trigger: 'blur' }
],
sort: [
{ required: true, message: '排序不能为空', trigger: 'blur' }
],
descript: [
{ required: true, message: '描述不能为空', trigger: 'blur' }
],
icon: [
{ required: true, message: '组图标不能为空', trigger: 'blur' }
],
catelogId: [
{ required: true, message: '所属分类id不能为空', trigger: 'blur' }
]
}
}
},
created () {
this.getMenus()
},
methods: {
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: 'get'
}).then(({ data }) => {
this.categorys = data.data;
});
},
init (id) {
this.dataForm.attrGroupId = id || 0
this.visible = true
this.$nextTick(() => {
this.$refs['dataForm'].resetFields()
if (this.dataForm.attrGroupId) {
this.$http({
url: this.$http.adornUrl(`/product/attrgroup/info/${this.dataForm.attrGroupId}`),
method: 'get',
params: this.$http.adornParams()
}).then(({data}) => {
if (data && data.code === 0) {
this.dataForm.attrGroupName = data.attrGroup.attrGroupName
this.dataForm.sort = data.attrGroup.sort
this.dataForm.descript = data.attrGroup.descript
this.dataForm.icon = data.attrGroup.icon
this.dataForm.catelogId = this.dataForm.catelogIds[2]
}
})
}
})
},
// 表单提交
dataFormSubmit () {
this.$refs['dataForm'].validate((valid) => {
if (valid) {
this.$http({
url: this.$http.adornUrl(`/product/attrgroup/${!this.dataForm.attrGroupId ? 'save' : 'update'}`),
method: 'post',
data: this.$http.adornData({
'attrGroupId': this.dataForm.attrGroupId || undefined,
'attrGroupName': this.dataForm.attrGroupName,
'sort': this.dataForm.sort,
'descript': this.dataForm.descript,
'icon': this.dataForm.icon,
'catelogId': this.dataForm.catelogIds[2]
})
}).then(({data}) => {
if (data && data.code === 0) {
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
this.visible = false
this.$emit('refreshDataList')
}
})
} else {
this.$message.error(data.msg)
}
})
}
})
}
}
}
</script>
27 属性分组 分组修改-级联选择器回显
我们在点击修改时要设置出回显
我们可以让后端返回我们需要的这个数据
1 实体类修改(AttrGroupEntity)
新增一个字段 这个字段就是我们回显需要的数据
/**
* 修改组时候的回显需要的数据
*/
@TableField(exist = false)
private Long[] catelogPath;
2 Controller层修改
/**
* 信息
*/
@RequestMapping("/info/{attrGroupId}")
//@RequiresPermissions("product:attrgroup:info")
public R info(@PathVariable("attrGroupId") Long attrGroupId){
//根据attrGroupId查出对应的实体类
AttrGroupEntity attrGroup = attrGroupService.getById(attrGroupId);
//从实体类中获取catelogId
Long catelogId = attrGroup.getCatelogId();
//调用categoryService categoryService 查出 路径
Long[] path = categoryService.findCatelogPath(catelogId);
//把路径设置到其中 返回给前端
attrGroup.setCatelogPath(path);
return R.ok().put("attrGroup", attrGroup);
}
3 categroyServiceImpl修改
/**
* 找到一个三级目录对应的完整路径 [2,25,225] /电器/好电器/手机
*/
@Override
public Long[] findCatelogPath(Long catelogId) {
//新建一个ArrayList集合 接受Long
List<Long> paths = new ArrayList<>();
//调用方法找到完整路径[225,22,2]
List<Long> parentPath = findParentPath(catelogId, paths);
//调用逆序方法获得正确顺序[2,25,225]
Collections.reverse(parentPath);
return parentPath.toArray(new Long[parentPath.size()]);
}
/**
* 找到一个三级目录对应的完整路径 [2,25,225] /电器/好电器/手机
*/
private List<Long> findParentPath(Long catelogId,List<Long> paths){
//1、收集当前节点id
paths.add(catelogId);
//2 根据当前节点ID拿到其对应的实体类
CategoryEntity byId = this.getById(catelogId);
//3 当实体类父id不为0继续调用这个方法
if(byId.getParentCid()!=0){
findParentPath(byId.getParentCid(),paths);
}
//4 跳出递归循环返回数据
return paths;
}
4 修改前端代码
回显效果设置完成
5 绑定diglog事件
关闭dialog时清空Path
不影响我们新增时的操作
methods: {
dialogClose(){
this.dataForm.catelogPath = []
},
28 品牌管理 品牌分类关联与级联更新
1 我们要使用mybatis-plus进行分页
要配置分页插件很简单
接着配置一下分页插件
package com.atguigu.gulimall.product.config;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableTransactionManagement //开启事务
@MapperScan("com.atguigu.gulimall.product.dao")
public class MybatisPlusConfig {
//引入分页插件
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false
paginationInterceptor.setOverflow(true);
// 设置最大单页限制数量,默认 500 条,-1 不受限制
paginationInterceptor.setLimit(1000);
return paginationInterceptor;
}
}
发现分页已经出效果了
2 我们来让品牌管理中的搜索生效
来到list的serviceimpl层加一些代码
@Service("brandService")
public class BrandServiceImpl extends ServiceImpl<BrandDao, BrandEntity> implements BrandService {
@Override
public PageUtils queryPage(Map<String, Object> params) {
//参数列表中拿关键字
String key = (String) params.get("key");
QueryWrapper<BrandEntity> brandEntityQueryWrapper = new QueryWrapper<>();
if (StringUtils.isNotEmpty(key)){
brandEntityQueryWrapper.eq("brand_id",key).or().like("name",key);
}
IPage<BrandEntity> page = this.page(
new Query<BrandEntity>().getPage(params),
brandEntityQueryWrapper
);
return new PageUtils(page);
}
}
3 我们让品牌管理的关联分类生效
需要编写两个controller
首先来编写查询品牌关联分类的controller
/**
* 获取当前品牌关联的所有分类列表
*/
@GetMapping("/catelog/list")
//@RequiresPermissions("product:categorybrandrelation:list")
public R cateloglist(@RequestParam("brandId")Long brandId){
List<CategoryBrandRelationEntity> data = categoryBrandRelationService.list(
new QueryWrapper<CategoryBrandRelationEntity>().eq("brand_id",brandId)
);
return R.ok().put("data", data);
}
接着来编写新增品牌关联分类的controller
/**
* 保存
*/
@RequestMapping("/save")
//@RequiresPermissions("product:categorybrandrelation:save")
public R save(@RequestBody CategoryBrandRelationEntity categoryBrandRelation){
categoryBrandRelationService.saveDetail(categoryBrandRelation);
return R.ok();
}
service
public interface CategoryBrandRelationService extends IService<CategoryBrandRelationEntity> {
PageUtils queryPage(Map<String, Object> params);
void saveDetail(CategoryBrandRelationEntity categoryBrandRelation);
}
serviceimpl(这里面注入了两个DAO)
@Autowired
BrandDao brandDao;
@Autowired
CategoryDao categoryDao;
@Override
public void saveDetail(CategoryBrandRelationEntity categoryBrandRelation) {
Long brandId = categoryBrandRelation.getBrandId();
Long catelogId = categoryBrandRelation.getCatelogId();
//1、查询详细名字
BrandEntity brandEntity = brandDao.selectById(brandId);
CategoryEntity categoryEntity = categoryDao.selectById(catelogId);
categoryBrandRelation.setBrandName(brandEntity.getName());
categoryBrandRelation.setCatelogName(categoryEntity.getName());
this.save(categoryBrandRelation);
}
测试新增关联关系 显示关联关系没有问题
4 连带更新(避免数据不一致)
我们因为这个数据表中有冗余字段
当我们更新brand表时候这里并不会更新
因此我们在更新brand时需要连带更新这个表中的数据
否则会出现字段属性不一致的问题
1 来更改品牌名称时关系表中的冗余字段同步更新
1 brandcontroller
/**
* 修改
*/
@RequestMapping("/update")
//@RequiresPermissions("product:brand:update")
public R update(@Validated(UpdateGroup.class) @RequestBody BrandEntity brand){
// brandService.updateById(brand);
brandService.updateDetail(brand);
return R.ok();
}
2 BrandService
public interface BrandService extends IService<BrandEntity> {
PageUtils queryPage(Map<String, Object> params);
void updateDetail(BrandEntity brand);
}
3 BrandServiceImpl
@Service("brandService")
public class BrandServiceImpl extends ServiceImpl<BrandDao, BrandEntity> implements BrandService {
@Autowired
CategoryBrandRelationService categoryBrandRelationService;
@Transactional
@Override
public void updateDetail(BrandEntity brand) {
//保证冗余字段的数据一致
this.updateById(brand);
//如果brand中的name更改的话
if(!org.springframework.util.StringUtils.isEmpty(brand.getName())){
//同步更新其他关联表中的数据
categoryBrandRelationService.updateBrand(brand.getBrandId(),brand.getName());
//TODO 更新其他关联
}
}
4 CategoryBrandRelationService
public interface CategoryBrandRelationService extends IService<CategoryBrandRelationEntity> {
PageUtils queryPage(Map<String, Object> params);
void saveDetail(CategoryBrandRelationEntity categoryBrandRelation);
void updateBrand(Long brandId, String name);
}
5 CategoryBrandRelationServiceImpl
@Override
public void updateBrand(Long brandId, String name) {
//创建一个新的关系实体类
CategoryBrandRelationEntity relationEntity = new CategoryBrandRelationEntity();
//设置ID和name为我们传来的
relationEntity.setBrandId(brandId);
relationEntity.setBrandName(name);
//更新ID为xxx的name字段完成级联更新
this.update(relationEntity,new UpdateWrapper<CategoryBrandRelationEntity>().eq("brand_id",brandId));
}
2 来更改目录名称时关系表中的冗余字段同步更新
具体操作和1一样
在categorycontroller的update调用relation中的update
进行级联更新 具体就不再多赘述
下一个基础篇笔记链接
总结
123456789
987654321
(bornig)