商城的商品页面展示是一个三级分类的。有一级分类、二级分类、三级分类。这就是我们接下来要进行的操作。
gulimall_pms
这个数据库中的pms_category
这个表下插入数据gulimall-product
中的controller
包下的CategoryController
@RestController
@RequestMapping("product/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
/**
* 查出所有分类以及子分类,以树形结构组装起来
*/
@RequestMapping("/list/tree")
public R list(){
List<CategoryEntity> entities = categoryService.listWithTree();
return R.ok().put("data", entities);
}
}
接着我们使用idea自带的工具帮助我们生成相应的方法。
/**
* 商品三级分类
*/
public interface CategoryService extends IService<CategoryEntity> {
List<CategoryEntity> listWithTree();
}
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
// @Autowired
// CategoryDao categoryDao; //其实这里因为继承了ServiceImpl,且其泛型就是 CategoryDao,
// 所以我们可以直接使用 ServiceImpl里面的 baseMapper来直接注入
.......
/**
* 1、Lambda表达式
* 1、举例:(o1, o2)->Integer.compare(o1, o2)
*
* 2、格式:
*
* -> :lambda操作符 或 箭头操作符
* -> 左边: lambda形参列表(其实就是接口中的抽象方法的形参)
* -> 右边: lambda体(其实就是重写的抽象方法的方法体)
* 3、总结:
*
* -> 左边: lambda形参列表的参数类型可以省略(类型推断),如果形参列表只有一个参数,其一对()也可以省略
*
* -> 右边: lambda体应该使用一对{}包裹;如果lambda体只执行一条语句(可能是return语句),可以省略这一对{}和return关键字
*右边
*/
@Override
public List<CategoryEntity> listWithTree() {
//1.查出所有分类
//没有查询条件,就是代表查询所有
List<CategoryEntity> entities = baseMapper.selectList(null);
//2.组装成父子的树形结构
//2.1 找到所有的一级分类 (categoryEntity) -> {} lambda 表达式
List<CategoryEntity> level1Menus = entities.stream()
// .filter((categoryEntity) -> { return categoryEntity.getParentCid() == 0}) 下面的lambda表达式省略了return及{}及()
.filter(categoryEntity -> categoryEntity.getParentCid() == 0) //过滤出一级分类,因为其父类id是0
.map((menu) -> { //在菜单收集成list之前先通过递归找到菜单的所有子分类,放在map中,然后排序,即将当前菜单改了之后重新返回, 然后再收集菜单。
//设置一级分类的子分类
menu.setChildren(getChildren(menu, entities));
return menu;
}).sorted((menu1, menu2) -> {
//排序,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> getChildren(CategoryEntity root, List<CategoryEntity> all) {
List<CategoryEntity> children = all.stream().filter(categoryEntity -> {
return categoryEntity.getParentCid() == root.getCatId(); //二级菜单的父分类id == 一级分类的catid
}).map(categoryEntity -> {
//1.找到子菜单
//递归查找
categoryEntity.setChildren(getChildren(categoryEntity, all));//二级菜单下还有三级菜单,继续查找
return categoryEntity;
//2.菜单的排序
}).sorted((menu1, menu2) -> { //sorted() 定制排序
return (menu1.getSort() == null ? 0 : menu1.getSort() - (menu2.getSort() == null ? 0 : menu2.getSort()));
}).collect(Collectors.toList());
return children;
}
这里使用的是流式编程,对于这方面我们可去参考java8新特性的StreamAPI来进行相应的学习。
在学习的过程中,看到老师使用TODO才知道IDEA有一个类似备忘录的功能。
我们启动gulimall-product微服务进行测试查询。
我们接着进行测试,浏览器发送http://localhost:10000/product/category/list/tree
,测试结果如下图,显示正确。这里我们推荐浏览器装一个Json格式的处理的插件可以很好的帮助我们查看Json数据。
前后端联调:
启动后台:renren-fast微服务(idea);
启动前端:renren-fast-vue(vscode);
接着我们来到后台系统进行菜单模块的添加。
注意:避坑指南
如果系统登录不上,可能是 跨域配置默认不开启
登录成功之后,我们就可以开始进行后台系统的编辑和完善了。
在商品系统中新增一个分类维护的菜单。菜单的路由其实就是我们商品微服务中的访问路径。
希望的效果:在左侧点击【分类维护】,希望在此展示3级分类
注意地址栏http://localhost:8001/#/product-category 可以注意到product-category我们的/被替换为了-
我们在后台系统中修改的,在数据库的gulimall-admin中也会同步进行修改。
我们可以看到如果我们点击角色管理的话,地址栏是/sys-role
,但是我们实际发送的请求应该是/sys/role
,
sys-role 具体的视图在 renren-fast-vue/views/modules/sys/role.vue
所以由此可以知道后台会将 /
自动转换为 -
,同理我们去访问/product/category
也会自动被转换为/product-category
。
具体地址栏如下所示:
我们在renren-fast-vue
中可以看到有一个文件,对应的其实就是/sys-role
对应的页面视图,,即sys文件夹下的role.vue对应的就是角色管理这个页面的展示。所以对于商品分类/product/category
,我们接下来要做的就是在renren-fast-vue
下创建一个product文件夹,文件夹中创建一个category.vue来进行页面展示。
element.eleme.cn
中的快速开发指南进行编写。
测试中发现检查网页源代码发现,本来应该是给商品微服务10000端口发送的查询的,但是发送到了renren-fast 8080端口去了。
我们以后还会同时发向更多的端口,所以需要配置网关,前端只向网关发送请求,然后由网关自己路由到相应服务器端口。
renren-fast-vue中有一个 Index.js是管理 api 接口请求地址的,如下图。如果我们本次只是简单的将8080改为10000端口,那么当下次如果是10001呢?难道每次都要改吗?所以我们的下一步做法是使用网关进行路由。通过网关映射到具体的请求地址。
ps:此处也可以参考其他人的理解:
借鉴:他要给8080发请求读取数据,但是数据是在10000端口上,如果找到了这个请求改端口那改起来很麻烦。方法1是改vue项目里的全局配置,方法2是搭建个网关,让网关路由到10000。
ps: 上面这个图明显有错误,vscode 已经报错,这里我没有注意到,以致 后面处理 跨域问题的时候 白白浪费了我 9个半 小时的时间啊!!!!1
前端项目报错也会影响!!!
切记!!!!!!!!!!!!!!!!
在这里,对于微服务,后面我们统一改为加 api
前缀能路由过去。
// api接口请求地址
window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api'
接下来进行测试访问
我们发现 验证码 一直加载不出来。检查网页源代码发现是因为我们直接给网关发送验证码请求了。但是真实的应该是给 renren-fast 发送请求。
分析原因:前端给网关发验证码请求,但是验证码请求在 renren-fast服务里,所以要想使验证码好使,需要把 renren-fast服务注册到服务中心,并且由网关进行路由
问题引入:他要去 nacos 中查找api服务,但是nacos里是fast服务,就通过把api改成fast服务,所以让fast注册到服务注册中心,这样请求88网关转发到8080fast。
让fast里加入注册中心的依赖,所以引入common
在renren-fast的 application.yml文件中配置nacos注册中心地址
spring:
application:
name: renren-fast //给 renren-fast 起一个名字,方便nacos服务注册发现
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 //注册进nacos
注册成功
最开始报错,在b站看了评论和弹幕之后将gulimall-common这个依赖给取消了,因为启动报依赖循环报错。后面我将所有的依赖都换成老师的同样的版本之后就没有了。
启动报错:
java: Annotation processing is not supported for module cycles. Please ensure that all modules from cycle [gulimall-common,renren-fast] are excluded from annotation processing
指的是 循环依赖的问题
>解决办法:不要引入公共依赖,直接引入 nacos的服务注册发现的依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
<version>2.1.0.RELEASEversion>
dependency>
启动成功
鉴于上面出现很多错误,但是老师视频中没有出现这些错误,大概率是因为依赖的原因,所以对于gulimall中所有的依赖进行统一,按照老师的依赖进行配置。以防止后面出现很多突发的错误。
启动报错:Caused by: org.yaml.snakeyaml.scanner.ScannerException: mapping values are not allowed here
这个地方报错的原因大概率是yml文件语法错误:注意这个坑找了好久,id uri predicates filters都要对齐,同一层级。
完整代码示例如下:
# 在 yml 配置文件中配置,可以很方便的让我们在 项目上线后将配置直接转移到配置中心
spring:
cloud:
gateway:
routes:
- id: admin_route
uri: lb://renren-fast # 路由给renren-fast,lb代表负载均衡
predicates: # 什么情况下路由给它
- Path=/api/** # 把所有api开头的请求都转发给renren-fast:因为默认前端项目都带上api前缀,
filters:
- RewritePath=/api/(?>/?.*), /renren-fast/$\{segment}
# 默认规则, 请求过来:http://localhost:88/api/captcha.jpg 转发--> http://renren-fast:8080/api/captcha.jpg
# 但是真正的路径是http://renren-fast:8080/renren-fast/captcha.jpg
# 所以使用路径重写把/api/* 改变成 /renren-fast/*
修改后运行成功,验证码出现。
上面我们验证码出现了,但是我们登录却报错,原因在于浏览器的跨域问题。
从 8001访问88,引发 CORS 跨域请求,浏览器会拒绝跨域请求
跨域
问题描述:已拦截跨源请求:同源策略禁止读取位于 http://localhost:88/api/sys/login 的远程资源。(原因:CORS 头缺少 ‘Access-Control-Allow-Origin’)。
问题分析:这是一种跨域问题。访问的域名和端口和原来的请求不同,请求就会被限制
跨域:指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对js施加的安全限制。(ajax可以)
同源策略:是指协议,域名,端囗都要相同,其中有一个不同都会产生跨域;
引入浏览器跨域知识
跨域流程:
这个跨域请求的实现是通过预检请求实现的,先发送一个OPSTIONS探路,收到响应允许跨域后再发送真实请求
前面跨域的解决方案:
方法1:设置nginx包含admin和gateway
方法2:让服务器告诉预检请求能跨域
gulimall-gateway
中配置跨域配置列GulimallCorsConfiguration
解决跨域问题------配置filter,每个请求来了以后,返回给浏览器之前都添加上那些字段我们在gulimall-gateway中创建一个config来存放GulimallCorsConfiguration。注意这个包一定是要在gateway这个包下,否则启动报错(坑)。
@Configuration
public class GulimallCorsConfiguration {
@Bean // 添加过滤器,当请求一过来走完 corsWebFilter 就给他们添加上跨域的相应配置
public CorsWebFilter corsWebFilter(){
// 基于url路径跨域,选择reactive包下的
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// 跨域配置信息
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 允许跨域的头
corsConfiguration.addAllowedHeader("*");
// 允许跨域的请求方式
corsConfiguration.addAllowedMethod("*");
// 允许跨域的请求来源
corsConfiguration.addAllowedOrigin("*");
// 是否允许携带cookie跨域
corsConfiguration.setAllowCredentials(true);
// 任意url都要进行跨域配置
//对接口进行配置,“/*”代表所有,“/**”代表适配的所有接口
source.registerCorsConfiguration("/**",corsConfiguration);
//CorsWebFilter的构造器需要传递一个
//org.springframework.web.cors.reactive.CorsConfigurationSource的接口作为参数
//接口不能实例化,所以选择CorsConfigurationSource的实现类
//UrlBasedCorsConfigurationSource作为参数
return new CorsWebFilter(source);
}
}
浏览器检查报错,报错的原因是:renren-fast 中也配置了跨域,但是我们只需要一个,所以要给注释掉。
http://localhost:8001/renren已拦截跨源请求:同源策略禁止读取位于 http://localhost:88/api/sys/login 的远程资源。(原因:不允许有多个 ‘Access-Control-Allow-Origin’ CORS 头)n-fast/captcha.jpg?uuid=69c79f02-d15b-478a-8465-a07fd09001e6
出现了多个请求,并且也存在多个跨源请求。
为了解决这个问题,需要修改renren-fast项目,注释掉“io.renren.config.CorsConfig”类。然后再次进行访问。
跨域问题困扰了我 9个半小时的时间,最后发现 竟然是 renren-fast-vue 前端代码 格式问题,真是崩溃了。
这里也给了我一个 提醒,有时候需要从多方面进行问题的查找!!!!
前端 有时候也会报错,一定要注意。 其实只要依赖版本和老师的一样,有很多坑是可以避免的。
在显示商品系统/分类信息的时候,出现了404异常,请求的http://localhost:88/api/product/category/list/tree不存在
只有通过http://localhost:10000/product/category/list/tree路径才能够正常访问,所以会报404异常。这是路径映射错误。我们需要在网关中进行路径重写,让网关帮我们转到正确的地址。
首先我们需要将 gulimall-product 服务 注册进 nacos,方便网关进行路由。
我们在nacos中新建一个 product 命名空间,以后关于 product商品微服务下的配置就放在该命名空间下,目前我们注册微服务的话,都默认放在 public 命名空间下就行,配置文件放在各自微服务的命名空间下即可。
首先这里我们先回顾一下 nacos的配置步骤:
- 微服务注册进nacos:
- 首先 需要在 application.yml / application.properties 文件中配置nacos的服务注册地址,并且最好每一个微服务都有属于自己的一个 应用名字
spring: cloud: nacos: discovery: server-addr: 127.0.0.1:8848
- 微服务 配置 进 nacos
- 如果想要 用nacos作为配置中心 ,需要 新建 bootstrap.properties 文件,然后在里面配置nacos 配置中心的地址; 此外,我们规定每一个微服务都有属于自己的命名空间,以后隶属于该微服务下的配置文件都配置在 该命名空间中。
spring.application.name=gulimall-product # 配置nacos 配置中心地址 spring.cloud.nacos.config.server-addr=127.0.0.1:8848 spring.cloud.nacos.config.namespace=832f36b7-7878-47b7-8968-408f7b98b1e6
- 在启动类 上 添加注解 @EnableDiscoveryClient : 为了发现服务注册和配置
注册和配置成功。
在 gulimall-gateway 下的 application.yml中进行配置
- id: product_route
uri: lb://gulimall_product
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?>/?.*), /$\{segment}
# http://localhost:88/api/product/category/list/tree http://localhost:10000/product/category/list/tree
注意:
如果直接访问 localhost:88/api/product/category/list/tree invalid token这个url地址的话,会提示非法令牌,后台管理系统中没有登录,所以没有带令牌
原因:先匹配的先路由,renren-fast 和 product 路由重叠,fast 要求登录
修正:在路由规则的顺序上,将精确的路由规则放置到模糊的路由规则的前面,否则的话,精确的路由规则将不会被匹配到,类似于异常体系中try catch子句中异常的处理顺序。
http://localhost:88/api/product/category/list/tree 正常
访问http://localhost:8001/#/product-category,正常
原因是:先访问网关88,网关路径重写后访问nacos8848,nacos找到服务
成功访问。
因为我们 对 整个对象 中的 data 数据感兴趣 ,所以我们 将 对象中的 data 解构出来。
我们使用{}将data的数据进行解构:data.data是我们需要的数组内容
//获取菜单集合
methods: {
handleNodeClick(data) {
console.log(data);
},
//获取后台数据
getMenus() {
this.$http({
url: this.$http.adornUrl('/product/category/list/tree'),
method: 'get'
}).then(({data}) => { //将整个对象中的data数据结构出来,因为只有data才是我们需要的
console.log("成功了获取到菜单数据....", data.data)
this.menus = data.data; // 数组内容,把数据给menus,就是给了vue实例,最后绑定到视图上
})
}
},
此时有了3级结构,但是没有数据,在category.vue的模板中,数据是menus,而还有一个props。这是element-ui的规则
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
data() {
return {
menus: [], //真正的数据需要发送请求从数据库中进行查找
defaultProps: {
children: 'children', //子节点
label: 'name' //name属性作为标签的值,展示出来
}
};
},
修改完毕后,测试:
node 与 data
在element-ui的tree中,有2个非常重要的属性node代表当前节点对象(是否展开等信息,element-ui自带属性)
data是节点数据,是自己的数据。
data从哪里来:前面ajax发送请求,拿到data,赋值给menus属性,而menus属性绑定到标签的data属性。而node是 ui 的默认规则
删除效果预想:
- 在每一个菜单后面添加 append, delete
- 点击按钮时,不进行菜单的打开合并:expand-on-click-node=“false”
- 当没有子菜单或者没有引用(后台数据库判断是否有被引用,这里暂时不考虑)的时候,才可以显示delete按钮。当为一级、二级菜单时,才显示append按钮
- 利用 v-if 进行判断是否显示 按钮:
- 如果 当前节点 node 的等级 ≤ 2,表示是一级菜单或二级菜单,不显示删除按钮------- v-if=“node.level <= 2”, level表示当前 是几级节点;
- 如果 当前节点 的子节点的 数组长度为0,表示 没有子菜单----v-if=“node.childNodes.length == 0”
- 添加多选框 show-checkbox ,可以多选
- 设置 node-key=""标识每一个节点的不同
{{ node.label }}
append(data)"
>
Append
remove(node, data)"
>
Delete
效果展示:
测试删除数据,打开postman(APIfox也可以)输入“ http://localhost:88/api/product/category/delete ”,请求方式设置为POST,请求体body选 json 数组
可以看到删除成功,而且数据库中也没有该数据了。
ps:这里将限制行数给取消勾选,不然默认是只显示 1000行。
这是一种 物理删除(不推荐),数据库中也同样被修改了。
接下来我们正式编写删除逻辑。
@RequestMapping("/delete")
public R delete(@RequestBody Long[] catIds){
//删除之前需要判断待删除的菜单那是否被别的地方所引用。
// categoryService.removeByIds(Arrays.asList(catIds));
categoryService.removeMenuByIds(Arrays.asList(catIds));
return R.ok();
}
@Override
public void removeMenuByIds(List<Long> asList) {
//TODO 1.检查当前删除的菜单,是否被别的地方引用
//其实开发中使用的都是逻辑删除,并不是真正物理意义上的删除
baseMapper.deleteBatchIds(asList);
}
这里我们还不清楚后面有哪些服务需要用到product,所以我们建一个备忘录,以后再来补充。
在学习的过程中,看到老师使用TODO才知道IDEA有一个类似备忘录的功能。
对于开发中,我们常常采用的是逻辑删除(我们并不希望删除数据,而是标记它被删除了,这就是逻辑删除),即在数据库表设计时设计一个表示逻辑删除状态的字段,在pms_category
我们选择 show_status 字段,当它为0,表示被删除。
逻辑删除是mybatis-plus 的内容,会在项目中配置一些内容,告诉此项目执行delete语句时并不删除,只是标志位。
我们使用mybatis-plus中的逻辑删除语法:
1)、配置全局逻辑删除规则
application.yml中
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表示未删除
注意:这里有一个坑,数据库中我们最开始设置的是1:未删除,0:删除。这个坑马上解决。
/**
* 是否显示[0-不显示,1显示]
*/
@TableLogic(value = "1",delval = "0")//因为application.yml和数据库中的设置刚好相反,所以我们这里按数据库中的效果单独设置
private Integer showStatus;
配置之后,我们可以继续使用APIFox进行测试,实际测试成功。为了验证,我们也可以在application.yml设置一个全局打印日志,将sql语句打印出来。
logging:
level:
com.atguigu.gulimall: debug #设置日志打印级别
测试删除数据,打开postman或者是APIFox都可以(推荐使用APIFox)
输入“ http://localhost:88/api/product/category/delete ”,请求方式设置为POST,为了比对效果,可以在删除之前查询数据库的pms_category表:
delete请求传入的是数组,所以我们使用json数据。
删除1433,之后从 数据库中 show_status 1—>0,即逻辑删除正确。
控制台打印的SQL语句:
Preparing: UPDATE pms_category SET show_status=0 WHERE cat_id IN ( ? ) AND show_status=1
Parameters: 1433(Long)
Updates: 1
由此可见,逻辑删除成功,SQL语句为 更新字段。
发送的请求:delete
发送的数据:this.$http.adornData(ids, false)
util/httpRequest.js中,封装了一些拦截器
http.adornParams是封装get请求的数据
http.adornData封装post请求的数据
ajax 的 get 请求第一次向服务器请求数据之后,后续的请求可能会被缓存,就不会请求服务器要新的数据了。
所以为了不缓存,我们在url后面拼接个 date时间戳 或者一个随机数,让他每次都请求服务器获取实时的数据了。
- 编写前端 remove 方法,实现向后端发送请求
- 点击delete弹出提示框,是否删除这个节点: elementui 中 MessageBox 弹框中的确认消息添加到删除之前
- 删除成功后有消息提示: elementui 中 Message 消息提示
- 原来展开状态的菜单栏,在删除之后也应该展开: el-tree组件的 default-expanded-keys 属性,默认展开。 每次删除之后,把删除菜单的父菜单的id值赋给默认展开值即可。
注意:
前端向后端发送post请求和get请求。对于这个我们可以设置一个自定义的代码块。文件->首选项->用户片段
,以后我们就可以通过快捷键直接进行输出了。
"http-get请求": {
"prefix": "httpget",
"body":[
"this.\\$http({",
"url: this.\\$http.adornUrl(''),",
"method:'get',",
"params:this.\\$http.adornParams({})",
"}).then(({data})=>{",
"})"
],
"description":"httpGET请求"
},
"http-post请求":{
"prefix":"httppost",
"body":[
"this.\\$http({",
"url:this.\\$http.adornUrl(''),",
"method:'post',",
"data: this.\\$http.adornData(data, false)",
"}).then(({data})=>{ })"
],
"description":"httpPOST请求"
}
要求:删除之后,显示弹窗,而且展开的菜单仍然展开。
//在el-tree中设置默认展开属性,绑定给expandedKey
:default-expanded-keys="expandedKey"
//data中添加属性
expandedKey: [],
//完整的remove方法
remove(node, data) {
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.$message({
message: "菜单删除成功",
type: "success",
});
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [node.parent.data.catId]
});
})
.catch(() => {});
},
1、elementui中 Dialog
对话框
2、点击 append
,弹出对话框,输入分类名称
3、点击确定,添加到数据库: 新建方法addCategory发送post请求到后端; 因为要把数据添加到数据库,所以在前端数据中按照数据库的格式声明一个category。点击append时,计算category属性(比如 父id,以及当前层级等),点击确定时发送 post 请求(后台代码使用的是 @RequestBody 注解,需要发送 post请求)。
4、点击确定后,需要刷新菜单,显示出新的菜单;此外还需要展开菜单方便查看。
//data中新增数据
//按照数据库格式声明的数据
categroy: { name: "", parentCid: 0, catLevel: 0, showStatus: 1, sort: 0 },
//判断是否显示对话框
dialogVisible: false,
//修改append方法,新增addCategory方法
//点击append后,计算category属性,显示对话框
append(data) {
console.log("append", data);
this.dialogVisible = true;
this.categroy.parentCid = data.catId;
this.categroy.catLevel = data.catLevel * 1 + 1;
},
//点击确定后,发送post请求
//成功后显示添加成功,展开刚才的菜单
addCategory() {
console.log("提交的数据", this.categroy);
this.$http({
url: this.$http.adornUrl("/product/category/save"),
method: "post",
data: this.$http.adornData(this.categroy, false),
}).then(({ data }) => {
this.$message({
message: "添加成功",
type: "success",
});
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [this.categroy.parentCid];
this.dialogVisible = false;
});
gulimall-product
中的 CategoryController
下/**
* 信息
*/
@RequestMapping("/info/{catId}")
//@RequiresPermissions("product:category:info")
public R info(@PathVariable("catId") Long catId){
CategoryEntity category = categoryService.getById(catId);
return R.ok().put("data", category); //我们统一 为 data
}
2.前端代码
实现修改名称,图标,计量单位。
1、新增Edit按钮:复制之前的append
2、查看controller,发现updata方法是由id进行更新的,所以data中的category中新增catId
3、增加、修改的时候也修改图标和计量单位,所以data的category新增inco,productUnit
4、新建edit方法,用来绑定Edit按钮。新建editCategory方法,用来绑定对话框的确定按钮。
5、复用对话框:
data数据中新增dialogType,用来标记此时对话框是由 edit打开的,还是由 append打开的。
新建方法 submitData,与对话框的确定按钮进行绑定,在方法中判断,如果 dialogTypeadd调用addCategory(),如果 dialogTypeedit调用editCategory()
data数据中新增 title,绑定对话框的title,用来做提示信息。判断dialogType的值,来选择提示信息。
6、防止多个人同时操作,对话框中的回显的信息应该是由数据库中读出来的:点击Edit按钮,发送httpget请求。(看好返回的数据)7、编辑editCategory方法:
controller之中的更新是动态更新,根据id,发回去什么值修改什么值,所以把要修改的数据发回后端就好。
成功之后发送提示消息,展开刚才的菜单。
8、编辑之后,再点击添加,发现会回显刚才编辑的信息。所以在append方法中重置回显的信息。9、这里给 对话框 添加一个 close-on-click-modal = false:这样我们点对话框之外的空白处就不会直接不显示对话框了。
edit(data)">
Edit
//data, 新增了title、dialogType。 categroy中新增了inco、productUnit、catId
data() {
return {
title: "",
dialogType: "", //edit,add
dialogVisible: false,
menus: [],
expandedKey: [],
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
icon: "",
productUnit: "",
catId: null,
},
defaultProps: {
children: "children", //子节点
label: "name", //name属性作为标签的值,展示出来
},
};
},
//方法
//绑定对话框的确定按钮,根据dialogType判断调用哪个函数
submitData() {
if (this.dialogType == "add") {
this.addCategory();
}
if (this.dialogType == "edit") {
this.editCategory();
}
},
//绑定Edit按钮,设置dialogType、title,从后台读取数据,展示到对话框内
edit(data) {
console.log("要修改的数据", data);
this.dialogType = "edit";
this.title = "修改分类";
// 发送请求获取节点最新的数据
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.dialogVisible = true;
});
},
//修改三级分类数据
//绑定对话框的确定按钮,向后台发送更新请求,传过去想要修改的字段
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({
type: "success",
message: "菜单修改成功!",
});
// 关闭对话框
this.dialogVisible = false;
// 刷新出新的菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
})
.catch(() => { });
},
//点击append按钮,清空编辑之后的回显数据
append(data) {
console.log("append----", data);
this.dialogType = "add";
this.title = "添加分类";
this.category.parentCid = data.catId;
this.category.catLevel = data.catLevel * 1 + 1;
this.category.catId = null;
this.category.name = null;
this.category.icon = "";
this.category.productUnit = "";
this.category.sort = 0;
this.category.showStatus = 1;
this.dialogVisible = true;
},
1、拖拽功能的前端实现:ementui树型控件->可拖拽节点
- 在中加入属性 draggable表示节点可拖拽。
- 在中加入属性 :allow-drop=“allowDrop”,拖拽时判定目标节点能否被放置。
- allowDrop有三个参数: draggingNode表示拖拽的节点, dropNode表示拖拽到哪个节点,type表示拖拽的类型 ’prev’、‘inner’ 和 ‘next’,表示拖拽到目标节点之前、里面、之后。
- allowDrop函数实现判断,拖拽后必须保持树形的三层结构。
- 节点的深度 = 最深深度 - 当前深度 + 1
- 当拖拽节点拖拽到目标节点的内部,要满足: 拖拽节点的深度 + 目标节点的深度 <= 3
- 当拖拽节点拖拽的目标节点的两侧,要满足: 拖拽节点的深度 + 目标节点的父节点的深度 <= 3
draggable
:allow-drop="allowDrop"
// data中新增属性,用来记录当前节点的最大深度
maxLevel: 0,
//新增方法
allowDrop(draggingNode, dropNode, type) {
//1、被拖动的当前节点以及所在的父节点总层数不能>3
//1)、被拖动的当前节点总层数
console.log("allowDrop", draggingNode, dropNode, type);
this.countNodeLevel(draggingNode.data);
//当前正在拖动的节点+父节点所在的深度不大于3即可
let deep = this.maxLevel - draggingNode.data.catLevel + 1;
console.log("深度:", deep);
//this.maxLevel
if (type == "inner") {
return (deep + dropNode.level) <= 3;
} else {
return (deep + dropNode.parent.level) <= 3;
}
},
//计算当前节点的最大深度
countNodeLevel(node) {
//找到所有子节点,求出最大深度
if (node.children != null && node.children.length > 0) {
for (let i = 0; i < node.children.length; i++) {
if (node.children[i].catLevel > this.maxLevel) {
this.maxLevel = node.children[i].catLevel;
}
this.countNodeLevel(node.children[i]);
}
}
},
- 拖拽功能的数据收集
- 在中加入属性@node-drop=“handleDrop”, 表示拖拽事件结束后触发事件handleDrop,handleDrop共四个参数:
- draggingNode:被拖拽节点对应的 Node;
- dropNode:结束拖拽时最后进入的节点;
- dropType:被拖拽节点的放置位置(before、after、inner);
- ev:event
- 拖拽可能影响的节点的数据:parentCid、catLevel、sort
- data中新增updateNodes ,把所有要修改的节点都传进来。
- 要修改的数据:拖拽节点的parentCid、catLevel、sort
- 要修改的数据:新的兄弟节点的sort (把新的节点收集起来,然后重新排序)
- 要修改的数据:子节点的catLeve
//el-tree中新增属性,绑定handleDrop,表示拖拽完触发
@node-drop="handleDrop"
//data 中新增数据,用来记录需要更新的节点(拖拽的节点(parentCid、catLevel、sort),拖拽后的兄弟节点(sort),拖拽节点的子节点(catLevel))
updateNodes: [],
//新增方法
handleDrop(draggingNode, dropNode, dropType, ev) {
console.log("handleDrop: ", draggingNode, dropNode, dropType);
//1、当前节点最新父节点的id
let pCid = 0;
//拖拽后的兄弟节点,分两种情况,一种是拖拽到两侧,一种是拖拽到内部
let sibings = null;
if (dropType == "before" || dropType == "after") {
pCid = dropNode.parent.data.catId == undefined ? 0: dropNode.parent.data.catId;
sibings = dropNode.parent.childNodes;
} else {
pCid = dropNode.data.catId;
sibings = dropNode.childNodes;
}
//2、当前拖拽节点的最新顺序
//遍历所有的兄弟节点,如果是拖拽节点,传入(catId,sort,parentCid,catLevel),如果是兄弟节点传入(catId,sort)
for (let i = 0; i < sibings.length; i++) {
if (sibings[i].data.catId == draggingNode.data.catId){
//如果遍历的是当前正在拖拽的节点
let catLevel = draggingNode.level;
if (sibings[i].level != draggingNode.level){
//当前节点的层级发生变化
catLevel = sibings[i].level;
//修改他子节点的层级
this.updateChildNodeLevel(sibings[i]);
}
this.updateNodes.push({catId:sibings[i].data.catId, sort: i, parentCid: pCid, catLevel:catLevel});
}else{
this.updateNodes.push({catId:sibings[i].data.catId, sort: i});
}
}
//3 当前拖拽节点的最新层级
console.log("updateNodes", this.updateNodes);
}
// 修改拖拽节点的子节点的层级
updateChildNodeLevel(node){
if (node.childNodes.length > 0){
for (let i = 0; i < node.childNodes.length; i++){
//遍历子节点,传入(catId,catLevel)
var cNode = node.childNodes[i].data;
this.updateNodes.push({catId:cNode.catId,catLevel:node.childNodes[i].level});
//处理子节点的子节点
this.updateChildNodeLevel(node.childNodes[i]);
}
}
},
- 拖拽功能实现
- 在后端编写批量修改的方法
update/sort
- 前端发送post请求,把要修改的数据发送过来
- 提示信息,展开拖拽节点的父节点
CategoryController修改方法
@RestController
@RequestMapping("product/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
/**
* 批量修改分类
*/
@RequestMapping("/update/sort")
// @RequiresPermissions("product:category:update")
public R update(@RequestBody CategoryEntity[] category){
categoryService.updateBatchById(Arrays.asList(category));
return R.ok();
}
}
利用 APIfox 测试 批量修改效果
测试成功。接下来我们完善下 前端的代码。
前端发送请求:
//3 当前拖拽节点的最新层级
console.log("updateNodes", this.updateNodes);
this.$http({
url: this.$http.adornUrl("/product/category/update/sort"),
method: 'post',
data: this.$http.adornData(this.updateNodes, false)
}).then(({ data }) => {
this.$message({
message: "菜单顺序等修改成功",
type: "success",
});
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [pCid];
//每次拖拽后把数据清空,否则要修改的节点将会越拖越多
this.updateNodes = [],
this.maxLevel = 0
});
- 批量拖拽功能
- 添加开关,控制拖拽功能是否开启
- 每次拖拽都要和数据库交互,不合理。批量拖拽过后,一次性保存。
批量保存
//data中新增数据
pCid:[], //批量保存过后要展开的菜单id
draggable: false, //绑定拖拽开关是否打开
//修改了一些方法,修复bug,修改过的方法都贴在下面了
//点击批量保存按钮,发送请求
batchSave() {
this.$http({
url: this.$http.adornUrl("/product/category/update/sort"),
method: 'post',
data: this.$http.adornData(this.updateNodes, false)
}).then(({ data }) => {
this.$message({
message: "菜单顺序等修改成功",
type: "success",
});
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = this.pCid;
//每次拖拽后把数据清空,否则要修改的节点将会越拖越多
this.updateNodes = [],
this.maxLevel = 0;
// this.pCid = 0;
})
.catch(() => { });
},
handleDrop(draggingNode, dropNode, dropType, ev) {
console.log("handleDrop: ", draggingNode, dropNode, dropType);
//1、当前节点最新父节点的id
let pCid = 0;
//拖拽后的兄弟节点,分两种情况,一种是拖拽到两侧,一种是拖拽到内部
let sibings = null;
if (dropType == "before" || dropType == "after") {
pCid = dropNode.parent.data.catId == undefined ? 0 : dropNode.parent.data.catId;
sibings = dropNode.parent.childNodes;
} else {
pCid = dropNode.data.catId;
sibings = dropNode.childNodes;
}
this.pCid.push(pCid);
//2、当前拖拽节点的最新顺序
//遍历所有的兄弟节点,如果是拖拽节点,传入(catId,sort,parentCid,catLevel),如果是兄弟节点传入(catId,sort)
for (let i = 0; i < sibings.length; i++) {
if (sibings[i].data.catId == draggingNode.data.catId) {
//如果遍历的是当前正在拖拽的节点
let catLevel = draggingNode.level;
if (sibings[i].level != draggingNode.level) {
//当前节点的层级发生变化
catLevel = sibings[i].level;
//修改他子节点的层级
this.updateChildNodeLevel(sibings[i]);
}
this.updateNodes.push({ catId: sibings[i].data.catId, sort: i, parentCid: pCid, catLevel: catLevel });
} else {
this.updateNodes.push({ catId: sibings[i].data.catId, sort: i });
}
}
//3 当前拖拽节点的最新层级
console.log("updateNodes", this.updateNodes);
},
// 修改拖拽判断逻辑
allowDrop(draggingNode, dropNode, type) {
//1 被拖动的当前节点以及所在的父节点总层数不能大于3
//1 被拖动的当前节点总层数
console.log("allowDrop:", draggingNode, dropNode, type);
var level = this.countNodeLevel(draggingNode);
// 当前正在拖动的节点+父节点所在的深度不大于3即可
let deep = Math.abs(this.maxLevel - draggingNode.level) + 1;
console.log("深度:", deep);
// this.maxLevel
if (type == "innner") {
return deep + dropNode.level <= 3;
} else {
return deep + dropNode.parent.level <= 3;
}
},
//计算当前节点的最大深度
countNodeLevel(node) {
// 找到所有子节点,求出最大深度
if (node.childNodes != null && node.childNodes.length > 0) {
for (let i = 0; i < node.childNodes.length; i++) {
if (node.childNodes[i].level > this.maxLevel) {
this.maxLevel = node.childNodes[i].level;
}
this.countNodeLevel(node.childNodes[i]);
}
}
},
前端代码
批量删除
ref="menuTree"
//批量删除
batchDelete() {
let catIds = [];
let catNames = [];
let checkedNodes = this.$refs.menuTree.getCheckedNodes();
console.log("被选中的元素", checkedNodes);
for (let i = 0; i < checkedNodes.length; i++) {
catIds.push(checkedNodes[i].catId);
catNames.push(checkedNodes[i].name);
}
this.$confirm(`是否删除【${catNames}】菜单?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.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();
})
.catch(() => { });
})
.catch(() => { });
},
批量保存
批量删除
{{ node.label }}
append(data)">
Append
edit(data)">
Edit
remove(node, data)">
Delete
至此三级分类告一段落。
这次要用到的代码是通过renren-generator代码生成器中生成的前端代码。在前面中如果我们不小心进行删除了,可以通过idea自带的恢复功能进行恢复。
步骤:
isAuth
,全部返回为true这里提一嘴,我们可以将es6语法检查关闭。
效果如下:品牌logo地址显示在一栏了。
//brand.vue中新增方法,用来修改状态
updateBrandStatus(data) {
console.log("最新信息",data);
let { brandId, showStatus } = data;
this.$http({
url: this.$http.adornUrl("/product/brand/update"),
method: 'post',
data: this.$http.adornData({brandId,showStatus}, false)
}).then(({ data }) => {
this.$message({
type:"success",
message:"状态更新成功"
})
});
},
这里我们选用服务端签名后直传进行文件上传功能,好处是:
上传的账号信息存储在应用服务器
上传先找应用服务器要一个policy上传策略,生成防伪签名
https://help.aliyun.com/document_detail/32007.html sdk–java版本
阿里云关于文件上传的帮助文档
根据官网的文档,我们可以直接在项目中引入依赖进行安装
这个依赖是最原始的。配置什么要写一大堆。
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.15.0</version>
</dependency>
文件上传的具体配置,我们在 gulimall-product 的 test 包下的 GulimallProductApplicationTests类中进行测试,代码如下:
@Test
public void testUpload() throws FileNotFoundException {
// Endpoint以杭州为例,其它Region请按实际情况填写。
String endpoint = "oss-cn-hangzhou.aliyuncs.com";
// 云账号AccessKey有所有API访问权限,建议遵循阿里云安全最佳实践,创建并使用RAM子账号进行API访问或日常运维,请登录 https://ram.console.aliyun.com 创建。
String accessKeyId = "LTAI5tABh1pjUprZGrKi92w1";
String accessKeySecret = "enVYmXd9p1sHvVub5gBf21E3tjuIFJ";
// // 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 上传文件流。
InputStream inputStream = new FileInputStream("F:\\JAVA listen\\尚硅谷Java学科全套教程(总207.77GB)\\谷粒商城\\课件\\课件和文档\\基础篇\\资料\\pics\\0d40c24b264aa511.jpg");
ossClient.putObject("gulimall-wystart", "0d40c24b264aa511.jpg", inputStream);
// 关闭OSSClient。
ossClient.shutdown();
System.out.println("上传成功");
}
endpoint的取值:点击概览就可以看到你的endpoint信息,endpoint在
这里就是上海等地区,如 oss-cn-qingdao.aliyuncs.com
bucket域名:就是签名加上bucket,如gulimall-fermhan.oss-cn-qingdao.aliyuncs.com
accessKeyId和accessKeySecret需要创建一个RAM账号:
接下来就是具体如何获取的示例:
获取Endpoint、
AccessKey ID、
AccessKey Secret
对子账户分配权限,管理OSS对象存储服务。这里我们允许读和写,方便我们实现上传功能。
可以看到上传到云服务成功。
引入依赖(和老师版本一致)
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alicloud-ossartifactId>
<version>2.1.0.RELEASEversion>
dependency>
在 gulimall-product 的 application.yml文件中配置
1 创建“AccessKey ID”和“AccessKeySecret”
2 配置key,secret和endpoint相关信息
alicloud:
access-key: LTAI5tABh1pjUprZGrKi92w1
secret-key: enVYmXd9p1sHvVub5gBf21E3tjuIFJ
oss:
endpoint: oss-cn-hangzhou.aliyuncs.com
测试:
@Autowired
OSSClient ossClient;
@Test
public void testUpload() throws FileNotFoundException {
// // Endpoint以杭州为例,其它Region请按实际情况填写。
// String endpoint = "oss-cn-hangzhou.aliyuncs.com";
// // 云账号AccessKey有所有API访问权限,建议遵循阿里云安全最佳实践,创建并使用RAM子账号进行API访问或日常运维,请登录 https://ram.console.aliyun.com 创建。
// String accessKeyId = "LTAI5tABh1pjUprZGrKi92w1";
// String accessKeySecret = "enVYmXd9p1sHvVub5gBf21E3tjuIFJ";
//
// // // 创建OSSClient实例。
// OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 上传文件流。
InputStream inputStream = new FileInputStream("F:\\JAVA listen\\尚硅谷Java学科全套教程(总207.77GB)\\谷粒商城\\课件\\课件和文档\\基础篇\\资料\\pics\\0d40c24b264aa511.jpg");
ossClient.putObject("gulimall-wystart", "0d40c24b264aa511.jpg", inputStream);
// 关闭OSSClient。
ossClient.shutdown();
System.out.println("上传成功");
}
测试,同样可以成功上传。
注意:
视频中将阿里巴巴oss存储服务依赖加到gulimall-common中,但是这个时候如果启动product是会报错的,原因是其他微服务都依赖了gulimall-common服务,如果其他微服务没有进行相关配置,会报依赖循环的错误,导致启动失败。但是后面我们创建一个专属于第三方服务的微服务,所以如果你要在这里跟着老师的步骤,进行测试的话,最好的建议就是将阿里云服务的oss进行单独引入到product服务,并将common中的注释掉。
我们将文件上传或者以后的短信验证这些第三方服务抽取出来放到一个专门的第三方微服务的工程项目中。gulimall-third-party
oss依赖
添加依赖,将原来 gulimall-common 中的“spring-cloud-starter-alicloud-oss”依赖移动到该项目中,让该微服务专门管理第三方服务
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alicloud-ossartifactId>
<version>2.1.0.RELEASEversion>
dependency>
引入gulimall-common,注意在其中排除mybatisplus依赖。如果不排除,启动会报错。
<dependency>
<groupId>com.atguigu.gulimallgroupId>
<artifactId>gulimall-commonartifactId>
<version>0.0.1-SNAPSHOTversion>
<exclusions>
<exclusion>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
exclusion>
exclusions>
dependency>
另外也需要在“pom.xml”文件中,添加如下的依赖管理
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>2.1.0.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
将服务注册和配置到nacos中
新建 第三方服务的命名空间 ,以后相关配置我们就放在该命名空间下。
创建 oss.yml配置文件,以后线上生产时文件上传配置就放在此配置文件中
创建 bootstrap.properties文件,进行nacos的配置,此外每一个微服务都需要有对应的微服务名字
spring.application.name=gulimall-third-party
# nacos配置中心配置
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=844086b8-9b51-4e08-a69d-1e76cfbf4485
#以后我们就将文件上传的相关配置放在oss.yml下
spring.cloud.nacos.config.ext-config[0].data-id=oss.yml
spring.cloud.nacos.config.ext-config[0].group=DEFAULT_GROUP
spring.cloud
在 application.yml 文件中将服务注册进nacos:这里我们将 oss相关配置也先配置进来,以后线上生产的时候再放到 nacos 上。
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
alicloud:
access-key: LTAI5tABh1pjUprZGrKi92w1
secret-key: enVYmXd9p1sHvVub5gBf21E3tjuIFJ
oss:
endpoint: oss-cn-hangzhou.aliyuncs.com
bucket: gulimall-wystart
application:
name: gulimall-third-party
server:
port: 30000
在 主启动类中添加服务发现注解
@EnableDiscoveryClient //服务发现
@SpringBootApplication
public class GulimallThirdPartyApplication {
@Autowired
OSSClient ossClient;
//测试文件上传到云服务器
@Test
public void testUpload() throws FileNotFoundException {
// // Endpoint以杭州为例,其它Region请按实际情况填写。
// String endpoint = "oss-cn-hangzhou.aliyuncs.com";
// // 云账号AccessKey有所有API访问权限,建议遵循阿里云安全最佳实践,创建并使用RAM子账号进行API访问或日常运维,请登录 https://ram.console.aliyun.com 创建。
// String accessKeyId = "LTAI5tABh1pjUprZGrKi92w1";
// String accessKeySecret = "enVYmXd9p1sHvVub5gBf21E3tjuIFJ";
//
// // // 创建OSSClient实例。
// OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 上传文件流。
InputStream inputStream = new FileInputStream("F:\\JAVA listen\\尚硅谷Java学科全套教程(总207.77GB)\\谷粒商城\\课件\\课件和文档\\基础篇\\资料\\pics\\0d40c24b264aa511.jpg");
ossClient.putObject("gulimall-wystart", "hahaha.jpg", inputStream);
// 关闭OSSClient。
ossClient.shutdown();
System.out.println("上传成功");
}
成功上传。
接下来我们仔细讲解一下 利用 服务端签名后直传的原理
背景
采用JavaScript客户端直接签名(参见JavaScript客户端签名直传)时,AccessKeyID和AcessKeySecret会暴露在前端页面,因此存在严重的安全隐患。因此,OSS提供了服务端签名后直传的方案。
服务端签名后直传的原理如下:
用户发送上传Policy请求到应用服务器。
应用服务器返回上传Policy和签名给用户。
用户直接上传数据到OSS。
阿里云OSS存储服务中对于服务器签名直传这部分的文档。链接在下面:
https://help.aliyun.com/document_detail/91868.htm?spm=a2c4g.11186623.0.0.1607566a7iSEvF#concept-ahk-rfz-2fb
我们参考这个文档创建属于我们自己的配置。
编写 com.atguigu.gulimall.thirdparty.controller.OssController
@RestController
public class OssController {
@Autowired
OSS ossClient;
@Value("${spring.cloud.alicloud.oss.endpoint}")//从配置文件动态读取,不写死
private String endpoint;
@Value("${spring.cloud.alicloud.oss.bucket}")
private String bucket;
@Value("${spring.cloud.alicloud.access-key}")
private String accessId;
@RequestMapping("/oss/policy")
public Map<String,String> policy() {
// 填写Host地址,格式为https://bucketname.endpoint。
String host = "https://" + bucket + "." + endpoint;
//自定义日期格式文件夹,以后上传的文件统一放在当天的文件夹中
String format = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
// 设置上传到OSS文件的前缀,可置空此项。置空后,文件将上传至Bucket的根目录下。
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 = ossClient.generatePostPolicy(expiration, policyConds);
byte[] binaryData = postPolicy.getBytes("utf-8");
String encodedPolicy = BinaryUtil.toBase64String(binaryData);
String postSignature = ossClient.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));
} catch (Exception e) {
// Assert.fail(e.getMessage());
System.out.println(e.getMessage());
}
return respMap;
}
}
测试 http://localhost:30000/oss/policy
在“gulimall-gateway”中配置路由规则:
- id: third_party_route
uri: lb://gulimall-third-party
predicates:
- Path=/api/thirdparty/**
filters:
- RewritePath=/api/thirdparty(?>/?.*), /$\{segment}
#http://localhost:88/api/thirdparty/oss/policy http://localhost:30000/oss/policy
测试是否能够正常跳转: http://localhost:88/api/thirdparty/oss/policy
成功。
前后端联调,实现文件上传。
singleUpload.vue是单文件上传,multiUploca.vue是多文件上传。
brand-add-or-update.vue
//在
将逆向生成的前端代码复制到product下面。
在modules/product/下创建attgroup.vue组件
查询
新增
批量删除
修改
删除
踩坑:
Can't resolve './attrgroup-add-or-update' in 'C:\Users\hxld\Desktop\renren-fast-vue\src\views\modules\product'
解决办法:
原来是绝对路径,后面改为相对路径即可。错误原因是因为版本问题可能。
我们要实现的功能是点击左侧,右侧表格对应显示。
父子组件传递数据:category.vue
点击时,引用它的attgroup.vue
能感知到, 然后通知到add-or-update。
//组件绑定事件
//methods中新增方法
nodeclick(data,node,component){
console.log("子组件category的节点被点击",data,node,component);
//向父组件发送事件
this.$emit("tree-node-click",data,node,component);
}
//引用的组件,可能会发散tree-node-click事件,当接收到时,触发父组件的treenodeclick方法
//methods中新增treenodeclick方法,验证父组件是否接收到
//感知树节点被点击
treenodeclick(data,node,component){
console.log("attrgroup感知到category的节点被点击:",data,node,component);
console.log("刚才被点击的菜单id:",data.catId);
},
3、启动测试
ps:这里可以参考其他网友的课件
根据请求地址http://localhost:8001/#/product-attrgroup
所以应该有product/attrgroup.vue。我们之前写过product/cateory.vue,现在我们
要抽象到common//cateory.vue
1 左侧内容:
要在左面显示菜单,右面显示表格复制,放到attrgroup.vue的。20表示列间距
去element-ui文档里找到布局,
分为2个模块,分别占6列和18列
有了布局之后,要在里面放内容。接下来要抽象一个分类vue。新建
common/category,生成vue模板。把之前写的el-tree放到
所以他把menus绑定到了菜单上,
所以我们应该在export default {中有menus的信息
该具体信息会随着点击等事件的发生会改变值(或比如created生命周期时),
tree也就同步变化了
common/category写好后,就可以在attrgroup.vue中导入使用了