1、导入三级分类数据尚硅谷谷粒商城电商项目\1.分布式基础(全栈开发篇)\docs\代码\sql\pms_catelog.sql
2、修改“com.atguigu.gulimall.product.controller.CategoryController”类,添加如下代码:
@Autowired
private CategoryService categoryService;
/**
* 查出所有分类及子分类,以树形结构组装起来
*/
@RequestMapping("/list/tree")
public R list(){
List<CategoryEntity> entities = categoryService.listWithTree();
return R.ok().put("data", entities);
}
3、修改‘com.atguigu.gulimall.product.service.CategoryService’类,代码如下
List<CategoryEntity> listWithTree();
4、如何区别是哪种分类级别?
答:可以通过分类的parent_cid来进行判断,如果是一级分类,其值为0.
修改‘com.atguigu.gulimall.product.service.impl.CategoryServiceImpl’类,代码如下
@Override
public List<CategoryEntity> listWithTree() {
//1、查出所有分类
List<CategoryEntity> entities = baseMapper.selectList(null);
//2、组装成父子的树形结构
//2.1、找到所有一级分类
List<CategoryEntity> level1Menus = entities.stream()
.filter(categoryEntity -> categoryEntity.getParentCid() == 0)
.map(menu->{
menu.setChildren(getChildren(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;
}
//递归查找所有菜单的子菜单
private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> all){
List<CategoryEntity> children = all.stream()
.filter(e -> e.getParentCid() == root.getCatId())
.map(menu->{
//1、找到子菜单
menu.setChildren(getChildren(menu,all));
return menu;
})
//2、菜单的排序
.sorted((menu1,menu2)->{
return (menu1.getSort()==null?0: menu1.getSort())-(menu2.getSort()==null?0: menu2.getSort());
})
.collect(Collectors.toList());
return children;
}
5、启动商品服务,测试:http://localhost:10000/product/category/list/tree
部分数据:
[{catId: 1, name: "图书、音像、电子书刊", parentCid: 0, catLevel: 1, showStatus: 1, sort: 0, icon: null,…},…]
启动后端项目renren-fast
启动前端项目renren-fast-vue:
npm run dev
创建目录:商品系统和一级菜单:分类维护:
创建成功后:
打开RENREN-FAST-VUE前端代码,创建renren-fast-vue\src\views\modules\product目录,之所以是这样来创建,是因为product/category,对应于product-category
在该目录下,新建“category.vue”文件,使用vue模板创建:
<template>
<el-tree :data="menus" :props="defaultProps" @node-click="handleNodeClick"></el-tree>
</template>
<script>
//这里可以导入其他文件(比如:组件,工具 js,第三方插件 js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
export default {
//import 引入的组件需要注入到对象中才能使用
components: {},
props: {},
data() {
//这里存放数据
return {
menus: [],
defaultProps: {
children: "children",
label: "name"
},
};
},
//计算属性 类似于 data 概念
computed: {},
//监控 data 中的数据变化
watch: {},
//方法集合
methods: {
handleNodeClick(data) {
console.log(data);
},
getMenus() {
this.dataListLoading = true;
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get"
}).then(({ data }) => {
console.log("获取到数据", data);
this.menus=data;
});
}
},
//生命周期 - 创建完成(可以访问当前 this 实例)
created() {
this.getMenus();
},
//生命周期 - 挂载完成(可以访问 DOM 元素)
mounted() {
},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {}, //如果页面有 keep-alive 缓存功能,这个函数会触发
}
</script>
<style lang='scss' scoped>
//@import url(); 引入公共 css 类
</style>
刷新页面出现404异常,查看请求发现,请求的是“http://localhost:8080/renren-fast/product/category/list/tree”
这个请求是不正确的,正确的请求是:http://localhost:10000/product/category/list/tree,
修正这个问题:
替换“static\config\index.js”文件中的“window.SITE_CONFIG[‘baseUrl’]”
替换前:
window.SITE_CONFIG['baseUrl'] = 'http://localhost:8080/renren-fast';
替换后:
window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api';
http://localhost:88,这个地址是我们网关微服务的接口。
但是这样做也引入了另外的一个问题,再次访问:http://localhost:8001/#/login,发现验证码不再显示:
分析原因:
现在的验证码请求路径为,http://localhost:88/captcha.jpg
原始的验证码请求路径:http://localhost:8080/renren-fast/captcha.jpg
在renren-fast模块的pom.xml中引入依赖:
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-nacos-discoveryartifactId>
<version>2.1.0.RELEASEversion>
dependency>
这里我们需要通过网关来完成路径的映射,因此将renren-fast注册到nacos注册中心中,并添加配置中心
application:
name: renren-fast
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
config:
name: renren-fast
server-addr: 127.0.0.1.8848
namespace: ee409c3f-3206-4a3b-ba65-7376922a886d
配置网关路由,前台的所有请求都是经由“http://localhost:88/api”来转发的,在“gulimall-gateway”中添加路由规则:
- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**
在admin_route的路由规则下,在访问路径中包含了“api”,因此它会将它转发到renren-fast,网关在转发的时候,会使用网关的前缀信息,为了能够正常的取得验证码,我们需要对请求路径进行重写
关于请求路径重写:
6.16. The RewritePath GatewayFilter Factory
The RewritePath GatewayFilter factory takes a path regexp parameter and a replacement parameter. This uses Java regular expressions for a flexible way to rewrite the request path. The following listing configures a RewritePath GatewayFilter:
Example 41. application.yml
spring:
cloud:
gateway:
routes:
- id: rewritepath_route
uri: https://example.org
predicates:
- Path=/foo/**
filters:
- RewritePath=/red(?>/?.*), $\{segment}
For a request path of /red/blue, this sets the path to /blue before making the downstream request. Note that the $ should be replaced with $\ because of the YAML specification.
修改“admin_route”路由规则:
- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**
filters:
- RewritePath=/api/(?>/?.*),/renren-fast/$\{segment}
再次访问:http://localhost:8001/#/login,验证码能够正常的加载了。
但是很不幸新的问题又产生了,访问被拒绝了
问题描述:已拦截跨源请求:同源策略禁止读取位于 http://localhost:88/api/sys/login 的远程资源。(原因:CORS 头缺少 ‘Access-Control-Allow-Origin’)。
问题分析:这是一种跨域问题。访问的域名和端口和原来的请求不同,请求就会被限制
跨域流程:
解决方法:在网关中定义“GulimallCorsConfiguration”类,该类用来做过滤,允许所有的请求跨域。
@Configuration
public class GulimallCorsConfiguration {
@Bean
public CorsWebFilter corsWebFilter(){
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
//1、配置跨域
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.setAllowCredentials(true);
source.registerCorsConfiguration("/**",config);
return new CorsWebFilter(source);
}
}
http://localhost:8001/renren已拦截跨源请求:同源策略禁止读取位于 http://localhost:88/api/sys/login 的远程资源。(原因:不允许有多个 ‘Access-Control-Allow-Origin’ CORS 头)n-fast/captcha.jpg
出现了多个请求,并且也存在多个跨源请求。
为了解决这个问题,需要修改renren-fast项目,注释掉“io.renren.config.CorsConfig”类。然后再次进行访问。
@Configuration
public class CorsConfig implements WebMvcConfigurer {
// @Override
// public void addCorsMappings(CorsRegistry registry) {
// registry.addMapping("/**")
// .allowedOrigins("*")
// .allowCredentials(true)
// .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
// .maxAge(3600);
// }
}
再次运行:http://localhost:8001/#/login,OK了
在显示分类信息的时候,出现了404异常,请求的http://localhost:88/api/product/category/list/tree不存在
这是因为网关上所做的路径映射不正确,映射后的路径为http://localhost:88/renren-fast/product/category/list/tree
但是只有通过http://localhost:10000/product/category/list/tree路径才能够正常访问,所以会报404异常。
解决方法就是定义一个product路由规则,进行路径重写:
- id: product_route
uri: lb://gulimall-product
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?>/?.*),/$\{segment}
在路由规则的顺序上,将精确的路由规则放置到模糊的路由规则的前面,否则的话,精确的路由规则将不会被匹配到,类似于异常体系中try catch子句中异常的处理顺序。
同时在gulimall-product目录的resources目录下新建bootstrap.properties配置文件
spring.application.name=gulimall-product
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=10f5ddf4-4255-424f-a545-2f252a0d1f0b
重启product模块,分类信息就不会报错了。
修改category.vue
保存后自动运行,就可以正确显示三级分类数据了
VSCode快捷键:CTRL+D:删除一行
ALT+SHIFT+F:格式化代码
ALT+SHIFT+上箭头/下箭头:向上/向下拷贝一行
ALT+上箭头/下箭头:向上/向下移动一行
添加delete和append标识,并且增加复选框
<el-tree
:data="menus"
show-checkbox //显示复选框
:props="defaultProps"
:expand-on-click-node="false" //设置节点点击时不展开
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>
测试删除数据,打开postman输入“ http://localhost:88/api/product/category/delete ”,请求方式设置为POST,由于delete请求接收的是一个数组,所以这里使用JSON方式,传入了一个数组:
再次查询数据库能够看到cat_id为1000的数据已经被删除了,说明逆向生成的代码是OK的。但是真正的删除没有这么简单。
修改“com.atguigu.gulimall.product.controller.CategoryController”类,添加如下代码:
/**
* 删除
* @RequestBody:获取请求体,必须发送post请求
* springMVC自动将请求体的数据(json),转为对应的对象
*/
@RequestMapping("/delete")
public R delete(@RequestBody Long[] catIds){
//检查当前删除的菜单,是否被别的地方引用
//categoryService.removeByIds(Arrays.asList(catIds));
categoryService.removeMenusByIds(Arrays.asList(catIds));
return R.ok();
}
com.atguigu.gulimall.product.service.impl.CategoryServiceImpl
@Override
public void removeMenusByIds(List<Long> asList) {
//TODO 检查当前删除的菜单,是否被别的地方引用
//逻辑删除
baseMapper.deleteBatchIds(asList);
}
然而多数时候,我们并不希望删除数据,而是标记它被删除了,这就是逻辑删除;
可以设置show_status为0,标记它已经被删除。
IDEA快捷键:CTRL+N或者双击SHIFT:打开全局搜索
mybatis-plus的逻辑删除:
https://baomidou.com/pages/6b03c5/
配置全局的逻辑删除规则,在“src/main/resources/application.yml”文件中添加如下内容:
mybatis-plus:
global-config:
db-config:
id-type: auto
logic-delete-value: 1
logic-not-delete-value: 0
修改“com.atguigu.gulimall.product.entity.CategoryEntity”类,添加上@TableLogic,表明使用逻辑删除:
/**
* 是否显示[0-不显示,1显示]
*/
@TableLogic(value = "1",delval = "0")
private Integer showStatus;
然后在Postman中测试一下是否能够满足需要。另外在“src/main/resources/application.yml”文件中,设置日志级别,打印出SQL语句:
logging:
level:
com.bigdata.gulimall.product: debug
打印的日志:
==> Preparing: UPDATE pms_category SET show_status=0 WHERE cat_id IN ( ? ) AND show_status=1
==> Parameters: 1431(Long)
<== Updates: 1
get changedGroupKeys:[]
8、菜单拖动
同一个菜单内拖动 | 正常 |
拖动到父菜单的前面或后面 | 正常 |
拖动到父菜单同级的另外一个菜单中 | 正常 |
关注的焦点在于,拖动到目标节点中,使得目标节点的catlevel+deep小于3即可。
需要考虑两种类型节点的catLevel
一种关系是:如果是同一个节点下的子节点的前后移动,则不需要修改其catLevel
如果是拖动到另外一个节点内或父节点中,则要考虑修改其catLevel
如果拖动到与父节点平级的节点关系中,则要将该拖动的节点的catLevel,设置为兄弟节点Level,
先考虑parentCid还是先考虑catLevel?
两种关系在耦合
另外还有一种是前后拖动的情况
哪个范围最大?
肯定是拖动类型关系最大,
如果是前后拖动,则拖动后需要看待拖动节点的层级和设置待拖动节点的parentId,
如果待拖动节点和目标节点的层级相同,则认为是同级拖动,只需要修改节点的先后顺序即可;
否则认为是跨级拖动,则需要修改层级和重新设置parentID
如果以拖动类型来分,并不合适,比较合适的是跨级拖动和同级拖动
如何判断是跨级拖动还是同级拖动,根据拖动的层级来看,如果是同一级的拖动,只需要修改先后顺序即可,但是这样也会存在一个问题,就是当拖动到另外一个分组下的同级目录中,显然也需要修改parentID,究竟什么样的模型最好呢?
另外也可以判断在跨级移动时,跨级后的parentID是否相同,如果不相同,则认为是在不同目录下的跨级移动需要修改parentID。
顺序、catLevel和parentID
同级移动:
(1)首先判断待移动节点和目标节点的catLevel是否相同,
(2)相同则认为是同级移动,
如果此时移动后目标节点的parentID和待移动节点的相同,但是移动类型是前后移动,只需要调整顺序即可,此时移动类型是inner,则需要修改catLevel和parentId和顺序
如果此时移动后目标节点的parentID和待移动节点的不相同,但是移动类型是前后移动,则需要调整顺序和parentId,此时移动类型是inner,则需要修改catLevel和parentId和顺序
通过这两步的操作能看到一些共性,如果抽取移动类型作为大的分类,则在这种分类下,
如果是前后移动,则分为下面几种情况:
同级别下的前后移动:界定标准为catLevel相同,但是又可以分为parentID相同和parentID不同,parent相同时,只需要修改顺序即可;parentID不同时,需要修改parentID和顺序
不同级别下的前后移动:界定标准为catLevel不同,此时无论如何都要修改parentID,顺序和catLevel
如果是inner类型移动,则分为一下的几种情况。
此时不论是同级inner,还是跨级innner,都需要修改parentID,顺序和catLevel
哪种情况需要更新子节点呢?
那就要看要拖拽的节点是否含有子节点,如果有子节点,则需要更新子节点的catLevel,不需要更新它之间的顺序和parentId,只需要更新catLevel即可。这种更新子节点的Level应该归类,目前的目标是只要有子节点就更新它的catLevel,
(2)如果待移动节点和目标节点的catLevel不同,则认为是跨级移动。如果是移动到父节点中,则需要设置catLevel,parentID和顺序。此时需要分两种情况来考虑,如果是移动到父节点中,则需要设置catLevel,parentID和顺序,如果是移动到兄弟节点中,则需要设置
包含移动到父节点同级目录,兄弟节点中。
设置菜单拖动开关
<el-switch v-model="draggable" active-text="开启拖拽" inactive-text="关闭拖拽"></el-switch>
批量删除
<el-button type="danger" plain size="small" @click="batchDelete">批量删除</el-button>
//批量删除
batchDelete() {
let checkNodes = this.$refs.menuTree.getCheckedNodes();
// console.log("被选中的节点:",checkNodes);
let catIds = [];
for (let i = 0; i < checkNodes.length; i++) {
catIds.push(checkNodes[i].catId);
}
this.$confirm(`确定要删除?`, "提示", {
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.getMeus();
});
})
.catch(() => {
//取消删除
});
},
<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>
edit(data) {
console.log("要修改的数据", data);
this.dialogType = "edit"
this.title = "修改分类"
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.icon = data.data.icon
this.category.productUnit = data.data.productUnit
this.category.catId = data.data.catId
this.category.parentCid = data.data.parentCid
this.category.catLevel = data.data.catLevel
this.category.sort = data.data.sort
this.category.showStatus = data.data.showStatus
})
},
append(data) {
console.log("append", data);
this.dialogType = "add"
this.title = "添加分类"
this.dialogVisible = true
this.category.parentCid = data.catId
this.category.catLevel = data.catLevel * 1 + 1
this.category.name = ""
this.category.icon = ""
this.category.productUnit = ""
this.category.catId = null
this.category.sort = 0
this.category.showStatus = 1
},
系统管理->菜单管理新增品牌管理
将“”逆向工程得到的resources\src\views\modules\product文件拷贝到gulimall/renren-fast-vue/src/views/modules/product目录下,也就是下面的两个文件
brand.vue brand-add-or-update.vue
显示的页面没有新增和删除功能,这是因为权限控制的原因
<el-button v-if="isAuth('product:brand:save')" type="primary" @click="addOrUpdateHandle()">新增</el-button>
<el-button v-if="isAuth('product:brand:delete')" type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0">批量删除</el-button>
查看“isAuth”的定义位置在“index.js”中定义,现在将它设置为返回值为true,即可显示添加和删除功能。
export function isAuth (key) {
// return JSON.parse(sessionStorage.getItem('permissions') || '[]').indexOf(key) !== -1 || false
return true
}
再次刷新页面能够看到,按钮就可以出现了。
brand.vue
<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>
brand-add-or-update.vue
<el-form-item label="显示状态" prop="showStatus">
<el-switch v-model="dataForm.showStatus" active-color="#13ce66" inactive-color="#ff4949"></el-switch>
</el-form-item>
//更新开关的状态
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({
message: "状态更新成功",
type: "success"
});
});
},
和传统的单体应用不同,这里我们选择将数据上传到分布式文件服务器上。
这里我们选择将图片放置到阿里云上,使用对象存储OSS。
基本概念
存储空间(Bucket)
对象(Object)
Endpoint(访问域名)
AccessKey(访问密钥)
Java简单上传
添加依赖在product模块的pom.xml
<dependency>
<groupId>com.aliyun.ossgroupId>
<artifactId>aliyun-sdk-ossartifactId>
<version>3.10.2version>
dependency>
@Test
public void testUpload(){
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
String endpoint = "https://oss-cn-beijing.aliyuncs.com";
// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
String accessKeyId = "创建的RAM账号的accessKeyId";
String accessKeySecret = "创建的RAM账号的accessKeySecret";
// 填写Bucket名称,例如examplebucket。
String bucketName = "gulimall-fxz";
// 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
String objectName = "7ae0120ec27dc3a7.jpg";
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
FileInputStream inputStream = new FileInputStream(
"D:\\docs\\pics\\7ae0120ec27dc3a7.jpg");
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) {
throw new RuntimeException(e);
} finally {
if (ossClient != null) {
ossClient.shutdown();
System.out.println("上传完成...");
}
}
}
参考:https://help.aliyun.com/document_detail/108650.html
(1)添加依赖在product模块的pom.xml
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alicloud-ossartifactId>
<version>2.2.0.RELEASEversion>
dependency>
(2)创建“AccessKey ID”和“AccessKeySecret”
(3)配置key,secret和endpoint相关信息
cloud:
alicloud:
access-key: 创建的RAM账号的accessKeyId
secret-key: 创建的RAM账号的accessKeySecret
oss:
endpoint: https://oss-cn-beijing.aliyuncs.com
4)注入OSSClient并进行文件的操作
@Autowired
OSS ossClient;
@Test
public void testUpload(){
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
//String endpoint = "https://oss-cn-beijing.aliyuncs.com";
// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
//String accessKeyId = "创建的RAM账号的accessKeyId";
//String accessKeySecret = "创建的RAM账号的accessKeySecret";
// 填写Bucket名称,例如examplebucket。
String bucketName = "gulimall-fxz";
// 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
String objectName = "7ae0120ec27dc3a7.jpg";
// 创建OSSClient实例。
//OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
FileInputStream inputStream = new FileInputStream(
"D:\\Projects\\Java培训相关\\尚硅谷\\尚硅谷谷粒商城电商项目\\1.分布式基础(全栈开发篇)\\docs\\pics\\7ae0120ec27dc3a7.jpg");
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) {
throw new RuntimeException(e);
} finally {
if (ossClient != null) {
ossClient.shutdown();
System.out.println("上传完成...");
}
}
}
建模块过程中遇到编译错误
1、确保springboot和springcloud版本一致
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.1.8.RELEASEversion>
<properties>
<java.version>1.8java.version>
<spring-cloud.version>Greenwich.SR3spring-cloud.version>
properties>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
2、排除datasource
@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, DruidDataSourceAutoConfigure.class})
public class GulimallThirdPartyApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallThirdPartyApplication.class, args);
}
}
3、测试类和方法都是public
@RunWith(SpringRunner.class)
@SpringBootTest
public class GulimallThirdPartyApplicationTests {
@Test
public void contextLoads() {
}
}
服务端签名后直传 - 对象存储 OSS - 阿里云
采用JavaScript客户端直接签名(参见JavaScript客户端签名直传)时,AccessKeyID和AcessKeySecret会暴露在前端页面,因此存在严重的安全隐患。因此,OSS提供了服务端签名后直传的方案。
服务端签名后直传的原理如下:
编写“com.atguigu.gulimall.thridparty.controller.OSSController”类:
/**
* ClassName: OssController
* Package: com.atguigu.gulimall.thirdparty.controller
* Description:
*
* @Author: Mr-Feng
* @Create: 2023/4/20 - 12:30
* @Version: v1.0
*/
@RestController
public class OssController {
@Autowired
OSS ossClient;
@RequestMapping("/oss/policy")
public R policy(){
// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
String accessId = "你的accessId";
String accessKey = "你的accessKey";
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
String endpoint = "https://oss-cn-beijing.aliyuncs.com";
// 填写Bucket名称,例如examplebucket。
String bucket = "gulimall-fxz";
// 填写Host地址,格式为https://bucketname.endpoint。
String host = "https://"+bucket+"."+endpoint;
// 设置上传回调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+"/";
// 创建ossClient实例。
// OSS ossClient = new OSSClientBuilder().build(endpoint, accessId, accessKey);
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 R.ok().put("data", respMap);
}
}
以后在上传文件时的访问路径为“ http://localhost:88/api/thirdparty/oss/policy”,
在“gulimall-gateway”中配置路由规则:
- id: third_part_route
uri: lb://gulimall-third-party
predicates:
- Path=/api/thirdparty/**
filters:
- RewritePath=/api/thirdparty/(?>/?.*),/$\{segment}
放置项目提供的upload文件夹到components目录下,一个是单文件上传,另外一个是多文件上传。
前端上传使用ElementUI组件的Upload 上传,文件选择后后触发before-upload钩子函数
beforeUpload(file) {
let _self = this;
return new Promise((resolve, reject) => {
policy().then(response => { //获取服务端返回的policy和签名
console.log("响应的数据",response);
_self.dataObj.policy = response.data.policy;
_self.dataObj.signature = response.data.signature;
_self.dataObj.ossaccessKeyId = response.data.accessId;
_self.dataObj.key = response.data.dir + getUUID()+'_${filename}';
_self.dataObj.dir = response.data.dir;
_self.dataObj.host = response.data.host;
resolve(true)
}).catch(err => {
reject(false)
})
})
},
policy.js
import http from '@/utils/httpRequest.js'
export function policy() {
return new Promise((resolve,reject)=>{//发送获取服务端签名的请求
http({
url: http.adornUrl("/thirdparty/oss/policy"),
method: "get",
params: http.adornParams({})
}).then(({ data }) => {
resolve(data);
})
});
}
在Java中提供了一系列的校验方式,它这些校验方式在“javax.validation.constraints”包中,提供了如@Email,@NotNull等注解。
在非空处理方式上提供了@NotNull,@Blank和@
(1)@NotNull
The annotated element must not be null. Accepts any type.
注解元素禁止为null,能够接收任何类型
(2)@NotEmpty
the annotated element must not be null nor empty.
该注解修饰的字段不能为null或""
Supported types are:
支持以下几种类型
CharSequence (length of character sequence is evaluated)
字符序列(字符序列长度的计算)
Collection (collection size is evaluated)
集合长度的计算
Map (map size is evaluated)
map长度的计算
Array (array length is evaluated)
数组长度的计算
(3)@NotBlank
The annotated element must not be null and must contain at least one non-whitespace character. Accepts CharSequence.
该注解不能为null,并且至少包含一个非空白字符。接收字符序列。
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand){
brandService.save(brand);
return R.ok();
}
测试: http://localhost:88/api/product/brand/save
在postman种发送上面的请求
{
"timestamp": "2023-04-21T09:20:46.383+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotBlank.brandEntity.name",
"NotBlank.name",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"brandEntity.name",
"name"
],
"arguments": null,
"defaultMessage": "name",
"code": "name"
}
],
"defaultMessage": "不能为空",
"objectName": "brandEntity",
"field": "name",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotBlank"
}
],
"message": "Validation failed for object='brandEntity'. Error count: 1",
"path": "/product/brand/save"
}
能够看到"defaultMessage": “不能为空”,这些错误消息定义在“hibernate-validator”的“\org\hibernate\validator\ValidationMessages_zh_CN.properties”文件中。在该文件中定义了很多的错误规则:
javax.validation.constraints.AssertFalse.message = 只能为false
javax.validation.constraints.AssertTrue.message = 只能为true
javax.validation.constraints.DecimalMax.message = 必须小于或等于{value}
javax.validation.constraints.DecimalMin.message = 必须大于或等于{value}
javax.validation.constraints.Digits.message = 数字的值超出了允许范围(只允许在{integer}位整数和{fraction}位小数范围内)
javax.validation.constraints.Email.message = 不是一个合法的电子邮件地址
javax.validation.constraints.Future.message = 需要是一个将来的时间
javax.validation.constraints.FutureOrPresent.message = 需要是一个将来或现在的时间
javax.validation.constraints.Max.message = 最大不能超过{value}
javax.validation.constraints.Min.message = 最小不能小于{value}
javax.validation.constraints.Negative.message = 必须是负数
javax.validation.constraints.NegativeOrZero.message = 必须是负数或零
javax.validation.constraints.NotBlank.message = 不能为空
javax.validation.constraints.NotEmpty.message = 不能为空
javax.validation.constraints.NotNull.message = 不能为null
javax.validation.constraints.Null.message = 必须为null
javax.validation.constraints.Past.message = 需要是一个过去的时间
javax.validation.constraints.PastOrPresent.message = 需要是一个过去或现在的时间
javax.validation.constraints.Pattern.message = 需要匹配正则表达式"{regexp}"
javax.validation.constraints.Positive.message = 必须是正数
javax.validation.constraints.PositiveOrZero.message = 必须是正数或零
javax.validation.constraints.Size.message = 个数必须在{min}和{max}之间
org.hibernate.validator.constraints.CreditCardNumber.message = 不合法的信用卡号码
org.hibernate.validator.constraints.Currency.message = 不合法的货币 (必须是{value}其中之一)
org.hibernate.validator.constraints.EAN.message = 不合法的{type}条形码
org.hibernate.validator.constraints.Email.message = 不是一个合法的电子邮件地址
org.hibernate.validator.constraints.Length.message = 长度需要在{min}和{max}之间
org.hibernate.validator.constraints.CodePointLength.message = 长度需要在{min}和{max}之间
org.hibernate.validator.constraints.LuhnCheck.message = ${validatedValue}的校验码不合法, Luhn模10校验和不匹配
org.hibernate.validator.constraints.Mod10Check.message = ${validatedValue}的校验码不合法, 模10校验和不匹配
org.hibernate.validator.constraints.Mod11Check.message = ${validatedValue}的校验码不合法, 模11校验和不匹配
org.hibernate.validator.constraints.ModCheck.message = ${validatedValue}的校验码不合法, ${modType}校验和不匹配
org.hibernate.validator.constraints.NotBlank.message = 不能为空
org.hibernate.validator.constraints.NotEmpty.message = 不能为空
org.hibernate.validator.constraints.ParametersScriptAssert.message = 执行脚本表达式"{script}"没有返回期望结果
org.hibernate.validator.constraints.Range.message = 需要在{min}和{max}之间
org.hibernate.validator.constraints.SafeHtml.message = 可能有不安全的HTML内容
org.hibernate.validator.constraints.ScriptAssert.message = 执行脚本表达式"{script}"没有返回期望结果
org.hibernate.validator.constraints.URL.message = 需要是一个合法的URL
org.hibernate.validator.constraints.time.DurationMax.message = 必须小于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'}
org.hibernate.validator.constraints.time.DurationMin.message = 必须大于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'}
想要自定义错误消息,可以覆盖默认的错误提示信息,如@NotBlank的默认message是
public @interface NotBlank {
String message() default "{javax.validation.constraints.NotBlank.message}";
可以在添加注解的时候,修改message:
@NotBlank(message = "品牌名必须非空")
private String name;
当再次发送请求时,得到的错误提示信息:
{
"timestamp": "2023-04-21T09:36:04.125+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotBlank.brandEntity.name",
"NotBlank.name",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"brandEntity.name",
"name"
],
"arguments": null,
"defaultMessage": "name",
"code": "name"
}
],
"defaultMessage": "品牌名必须非空",
"objectName": "brandEntity",
"field": "name",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotBlank"
}
],
"message": "Validation failed for object='brandEntity'. Error count: 1",
"path": "/product/brand/save"
}
但是这种返回的错误结果并不符合我们的业务需要。
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){
if( result.hasErrors()){
Map<String,String> map=new HashMap<>();
//1.获取错误的校验结果
result.getFieldErrors().forEach((item)->{
//获取发生错误时的message
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();
}
这种是针对于该请求设置了一个内容校验,如果针对于每个请求都单独进行配置,显然不是太合适,实际上可以统一的对于异常进行处理。
可以使用SpringMvc所提供的@ControllerAdvice,通过“basePackages”能够说明处理哪些路径下的异常。
(1)抽取一个异常处理类
/**
* ClassName: GulimallExceptionControllerAdvice
* Package: com.atguigu.gulimall.product.exception
* Description: 集中处理所有异常
*
* @Author: Mr-Feng
* @Create: 2023/4/26 - 13:20
* @Version: v1.0
*/
@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 handleValidException(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){
return R.error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(),
BizCodeEnume.UNKNOW_EXCEPTION.getMsg());
}
}
(2)测试: http://localhost:88/api/product/brand/save
(3)默认异常处理
@ExceptionHandler(value = Throwable.class)
public R handleException(Throwable throwable){
log.error("未知异常{},异常类型{}",throwable.getMessage(),throwable.getClass());
return R.error(BizCodeEnum.UNKNOW_EXEPTION.getCode(),BizCodeEnum.UNKNOW_EXEPTION.getMsg());
}
(4)错误状态码
上面代码中,针对于错误状态码,是我们进行随意定义的,然而正规开发过程中,错误状态码有着严格的定义规则,如该在项目中我们的错误状态码定义
@ControllerAdvice+@ExceptionHandler
系统错误码
/***
* 错误码和错误信息定义类
* 1. 错误码定义规则为 5 为数字
* 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知
异常
* 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
* 错误码列表:
* 10: 通用
* 001:参数格式校验
* 11: 商品
* 12: 订单
* 13: 购物车
* 14: 物流
*
*
*/
为了定义这些错误状态码,我们可以单独定义一个常量类,用来存储这些错误状态码
/**
* ClassName: BizCodeEnume
* Package: com.atguigu.common.exception
* Description:
* 错误码和错误信息定义类
* * 1. 错误码定义规则为5为数字
* * 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常
* * 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
* * 错误码列表:
* * 10: 通用
* * 001:参数格式校验
* * 11: 商品
* * 12: 订单
* * 13: 购物车
* * 14: 物流
* @Author: Mr-Feng
* @Create: 2023/5/15 - 17:01
* @Version: v1.0
*/
public enum BizCodeEnume {
UNKNOW_EXCEPTION(10000,"系统未知异常"),
VAILD_EXCEPTION(10001,"参数格式校验失败");
private int code;
private String msg;
BizCodeEnume(int code,String msg){
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
(5)测试: http://localhost:88/api/product/brand/save
1、给校验注解,标注上groups,指定什么情况下才需要进行校验
如:指定在更新和添加的时候,都需要进行校验
@NotEmpty
@NotBlank(message = "品牌名必须非空",groups = {UpdateGroup.class,AddGroup.class})
private String name;
在这种情况下,没有指定分组的校验注解,默认是不起作用的。想要起作用就必须要加groups。
2、业务方法参数上使用@Validated注解
@Validated的value方法:
Specify one or more validation groups to apply to the validation step kicked off by this annotation.
指定一个或多个验证组以应用于此注释启动的验证步骤。
JSR-303 defines validation groups as custom annotations which an application declares for the sole purpose of using
them as type-safe group arguments, as implemented in SpringValidatorAdapter.
JSR-303 将验证组定义为自定义注释,应用程序声明的唯一目的是将它们用作类型安全组参数,如 SpringValidatorAdapter 中实现的那样。
Other SmartValidator implementations may support class arguments in other ways as well.
其他SmartValidator 实现也可以以其他方式支持类参数。
3、默认情况下,在分组校验情况下,没有指定分组的校验注解,将不会生效,它只会在分组的情况下生效。
1、编写一个自定义的校验注解
/**
* ClassName: ListValue
* Package: com.atguigu.common.valid
* Description:
*
* @Author: Mr-Feng
* @Create: 2023/5/15 - 20:02
* @Version: v1.0
*/
@Documented
@Constraint(validatedBy = { ListValueConstraintValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
String message() default "{com.atguigu.common.valid.ListValue.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
int[] vals() default {};
}
2、编写一个自定义的校验器
/**
* ClassName: ListValueConstraintValidator
* Package: com.atguigu.common.valid
* Description:
*
* @Author: Mr-Feng
* @Create: 2023/5/15 - 20:18
* @Version: v1.0
*/
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
private Set<Integer> set = new HashSet<>();
//初始化方法
@Override
public void initialize(ListValue constraintAnnotation) {
int[] vals = constraintAnnotation.vals();
for (int val : vals) {
set.add(val);
}
}
//判断是否校验成功
/**
*
* @param value 需要校验的值
* @param context
* @return
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return set.contains(value);
}
}
3、关联自定义的校验器和自定义的校验注解
@Constraint(validatedBy = { ListValueConstraintValidator.class})
4、使用实例
/**
* 显示状态[0-不显示;1-显示]
*/
@ListValue(value = {0,1},groups ={AddGroup.class})
private Integer showStatus;
SPU:Standard Product Unit(标准化产品单元)
是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一
个产品的特性。
iphoneX 是 SPU、MI 8 是 SPU
iphoneX 64G 黑曜石 是 SKU
MI8 8+64G+黑色 是 SKU
SKU:Stock Keeping Unit(库存量单位)
即库存进出计量的基本单元,可以是以件,盒,托盘等为单位。SKU 这是对于大型连锁超市
DC(配送中心)物流管理的一个必要的方法。现在已经被引申为产品统一编号的简称,每
种产品均对应有唯一的 SKU 号。
每个分类下的商品共享规格参数,与销售属性。只是有些商品不一定要用这个分类下全部的属性;
3、属性分组-效果
POST /product/attr/save
请求参数
{
"attrGroupId": 0, //属性分组id
"attrName": "string",//属性名
"attrType": 0, //属性类型
"catelogId": 0, //分类id
"enable": 0, //是否可用
"icon": "string", //图标
"searchType": 0, //是否检索
"showDesc": 0, //快速展示
"valueSelect": "string", //可选值列表
"valueType": 0 //可选值模式
}
分页数据
响应数据
{
"msg": "success",
"code": 0
}
规格参数新增时,请求的URL:Request URL:
http://localhost:88/api/product/attr/base/list/0?t=1588731762158&page=1&limit=10&key=
当有新增字段时,我们往往会在entity实体类中新建一个字段,并标注数据库中不存在该字段,然而这种方式并不规范。
比较规范的做法是,新建一个vo文件夹,将每种不同的对象,按照它的功能进行了划分。在java中,涉及到了这几种类型。
Request URL: http://localhost:88/api/product/attr/save,现在的情况是,它在保存的时候,只是保存了attr,并没有保存attrgroup,为了解决这个问题,我们新建了一个vo/AttrVo,在原AttrEntity基础上增加了attrGroupId字段,使得保存新增数据的时候,也保存了它们之间的关系。
通过" BeanUtils.copyProperties(attr,attrEntity);"能够实现在两个Bean之间拷贝数据,但是两个Bean的字段要相同
修改“com.atguigu.gulimall.product.controller.AttrController”类,代码如下:
/**
* 保存
*/
@RequestMapping("/save")
public R save(@RequestBody AttrVo attr){
attrService.saveAttr(attr);
return R.ok();
}
修改“com.atguigu.gulimall.product.service.AttrService”类,代码如下:
void saveAttr(AttrVo attr);
修改“com.atguigu.gulimall.product.service.impl.AttrServiceImpl”类,代码如下:
@Transactional
@Override
public void saveAttr(AttrVo attr) {
AttrEntity attrEntity = new AttrEntity();
BeanUtils.copyProperties(attr,attrEntity);//Spring提供的工具类,用于复制对象属性,使用这个工具的前提条件是属性的字段名必须一致。
//1、保存基本数据
this.save(attrEntity);
//2、保存关联关系
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
relationEntity.setAttrGroupId(attr.getAttrGroupId());
relationEntity.setAttrId(attrEntity.getAttrId());
attrAttrgroupRelationDao.insert(relationEntity);
}
GET /product/attr/base/list/{catelogId}
请求参数
{
page: 1,//当前页码
limit: 10,//每页记录数
sidx: 'id',//排序字段
order: 'asc/desc',//排序方式
key: '华为'//检索关键字
}
分页数据
响应数据
{
"msg": "success",
"code": 0,
"page": {
"totalCount": 0,
"pageSize": 10,
"totalPage": 0,
"currPage": 1,
"list": [{
"attrId": 0, //属性id
"attrName": "string", //属性名
"attrType": 0, //属性类型,0-销售属性,1-基本属性
"catelogName": "手机/数码/手机", //所属分类名字
"groupName": "主体", //所属分组名字
"enable": 0, //是否启用
"icon": "string", //图标
"searchType": 0,//是否需要检索[0-不需要,1-需要]
"showDesc": 0,//是否展示在介绍上;0-否 1-是
"valueSelect": "string",//可选值列表[用逗号分隔]
"valueType": 0//值类型[0-为单个值,1-可以选择多个值]
}]
}
}
响应数据不仅包括AttrVo基本数据,还多包含了2个字段,因此新建AttrRespVo,用于规格参数列表显示。
/**
* ClassName: AttrRespVo
* Package: com.atguigu.gulimall.product.vo
* Description:
*
* @Author: Mr-Feng
* @Create: 2023/5/24 - 10:02
* @Version: v1.0
*/
@Data
public class AttrRespVo extends AttrVo{
/*
* "catelogName": "手机/数码/手机", //所属分类名字
* "groupName": "主体", //所属分组名字
* */
private String catelogName;
private String groupName;
}
修改“com.atguigu.gulimall.product.controller.AttrController”类,代码如下:
// /product/attr/base/list/{catelogId}
@GetMapping("/base/list/{catelogId}")
public R baseList(@RequestParam Map<String, Object> params, @PathVariable("catelogId")Long catelogId){
PageUtils page = attrService.queryBaseAttrPage(params, catelogId);
return R.ok().put("page", page);
}
修改“com.atguigu.gulimall.product.service.AttrService”类,代码如下:
PageUtils queryBaseAttrPage(Map<String, Object> params, Long catelogId)
修改“com.atguigu.gulimall.product.service.impl.AttrServiceImpl”类,代码如下:
@Override
public PageUtils queryBaseAttrPage(Map<String, Object> params, Long catelogId) {
QueryWrapper<AttrEntity> wrapper = new QueryWrapper<>();
if (catelogId != 0) {
wrapper.eq("catelogId",catelogId);
}
String key = (String) params.get("key");
if(StringUtils.isEmpty(key)){
//attr_id attr_name
wrapper.and((obj)->{
obj.eq("attr_id", key).or().like("attr_name",key);
});
}
IPage<AttrEntity> page = this.page(
new Query<AttrEntity>().getPage(params),
wrapper
);
PageUtils pageUtils = new PageUtils(page);
List<AttrEntity> records = page.getRecords();
List<AttrRespVo> respVos = records.stream().map((attrEntity) -> {
AttrRespVo attrRespVo = new AttrRespVo();
BeanUtils.copyProperties(attrEntity, attrRespVo);
//1、设置分类和分组的名字
AttrAttrgroupRelationEntity attrid = relationDao.selectOne(
new QueryWrapper<AttrAttrgroupRelationEntity>()
.eq("attr_id", attrEntity.getAttrId()));
if (attrid != null) {
AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrid.getAttrGroupId());
attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName());
}
CategoryEntity categoryEntity = categoryDao.selectById(attrEntity.getCatelogId());
if (categoryEntity != null) {
attrRespVo.setCatelogName(categoryEntity.getName());
}
return attrRespVo;
}).collect(Collectors.toList());
pageUtils.setList(respVos);
return pageUtils;
}
商品管理需要会员等级,先把资料前端文件夹里的其他modules:member、order、ware、coupon里的文件导入vsCode里重新运行,添加几个会员。
在商品发布章节,如果遇到提示 ”PubSub “未定义错误,则需要安装 pubsub-js,具体步骤:
(1)安装 pubsub-js:
npm install --save pubsub-js --legacy-peer-deps
(2)在 main.js 中引入
//导入
import PubSub from 'pubsub-js'
//挂载全局
Vue.prototype.PubSub = PubSub
POST /product/spuinfo/save
请求参数
{
"spuName": "Apple XR",
"spuDescription": "Apple XR",
"catalogId": 225,
"brandId": 12,
"weight": 0.048,
"publishStatus": 0,
"decript": ["https://gulimall-hello.oss-cn-beijing.aliyuncs.com/2019-11-22//66d30b3f-e02f-48b1-8574-e18fdf454a32_f205d9c99a2b4b01.jpg"],
"images": ["https://gulimall-hello.oss-cn-beijing.aliyuncs.com/2019-11-22//dcfcaec3-06d8-459b-8759-dbefc247845e_5b5e74d0978360a1.jpg", "https://gulimall-hello.oss-cn-beijing.aliyuncs.com/2019-11-22//5b15e90a-a161-44ff-8e1c-9e2e09929803_749d8efdff062fb0.jpg"],
"bounds": {
"buyBounds": 500,
"growBounds": 6000
},
"baseAttrs": [{
"attrId": 7,
"attrValues": "aaa;bb",
"showDesc": 1
}, {
"attrId": 8,
"attrValues": "2019",
"showDesc": 0
}],
"skus": [{
"attr": [{
"attrId": 9,
"attrName": "颜色",
"attrValue": "黑色"
}, {
"attrId": 10,
"attrName": "内存",
"attrValue": "6GB"
}],
"skuName": "Apple XR 黑色 6GB",
"price": "1999",
"skuTitle": "Apple XR 黑色 6GB",
"skuSubtitle": "Apple XR 黑色 6GB",
"images": [{
"imgUrl": "https://gulimall-hello.oss-cn-beijing.aliyuncs.com/2019-11-22//dcfcaec3-06d8-459b-8759-dbefc247845e_5b5e74d0978360a1.jpg",
"defaultImg": 1
}, {
"imgUrl": "https://gulimall-hello.oss-cn-beijing.aliyuncs.com/2019-11-22//5b15e90a-a161-44ff-8e1c-9e2e09929803_749d8efdff062fb0.jpg",
"defaultImg": 0
}],
"descar": ["黑色", "6GB"],
"fullCount": 5,
"discount": 0.98,
"countStatus": 1,
"fullPrice": 1000,
"reducePrice": 10,
"priceStatus": 0,
"memberPrice": [{
"id": 1,
"name": "aaa",
"price": 1998.99
}]
}, {
"attr": [{
"attrId": 9,
"attrName": "颜色",
"attrValue": "黑色"
}, {
"attrId": 10,
"attrName": "内存",
"attrValue": "12GB"
}],
"skuName": "Apple XR 黑色 12GB",
"price": "2999",
"skuTitle": "Apple XR 黑色 12GB",
"skuSubtitle": "Apple XR 黑色 6GB",
"images": [{
"imgUrl": "",
"defaultImg": 0
}, {
"imgUrl": "",
"defaultImg": 0
}],
"descar": ["黑色", "12GB"],
"fullCount": 0,
"discount": 0,
"countStatus": 0,
"fullPrice": 0,
"reducePrice": 0,
"priceStatus": 0,
"memberPrice": [{
"id": 1,
"name": "aaa",
"price": 1998.99
}]
}, {
"attr": [{
"attrId": 9,
"attrName": "颜色",
"attrValue": "白色"
}, {
"attrId": 10,
"attrName": "内存",
"attrValue": "6GB"
}],
"skuName": "Apple XR 白色 6GB",
"price": "1998",
"skuTitle": "Apple XR 白色 6GB",
"skuSubtitle": "Apple XR 黑色 6GB",
"images": [{
"imgUrl": "",
"defaultImg": 0
}, {
"imgUrl": "",
"defaultImg": 0
}],
"descar": ["白色", "6GB"],
"fullCount": 0,
"discount": 0,
"countStatus": 0,
"fullPrice": 0,
"reducePrice": 0,
"priceStatus": 0,
"memberPrice": [{
"id": 1,
"name": "aaa",
"price": 1998.99
}]
}, {
"attr": [{
"attrId": 9,
"attrName": "颜色",
"attrValue": "白色"
}, {
"attrId": 10,
"attrName": "内存",
"attrValue": "12GB"
}],
"skuName": "Apple XR 白色 12GB",
"price": "2998",
"skuTitle": "Apple XR 白色 12GB",
"skuSubtitle": "Apple XR 黑色 6GB",
"images": [{
"imgUrl": "",
"defaultImg": 0
}, {
"imgUrl": "",
"defaultImg": 0
}],
"descar": ["白色", "12GB"],
"fullCount": 0,
"discount": 0,
"countStatus": 0,
"fullPrice": 0,
"reducePrice": 0,
"priceStatus": 0,
"memberPrice": [{
"id": 1,
"name": "aaa",
"price": 1998.99
}]
}]
}
分页数据
响应数据
{
"msg": "success",
"code": 0
}
通过工具将JSON字符串转换为Java实体类,将请求参数转换为VO。包括:SpuSaveVo、Skus、MemberPrice、Images、Bounds、BaseAttrs、Attr。
SpuSaveVo.java
@Data
public class SpuSaveVo {
private String spuName;
private String spuDescription;
private Long catalogId;
private Long brandId;
private BigDecimal weight;
private int publishStatus;
private List<String> decript;
private List<String> images;
private Bounds bounds;
private List<BaseAttrs> baseAttrs;
private List<Skus> skus;
}
修改“com.atguigu.gulimall.product.controller.SpuInfoController”类,代码如下:
/**
* 保存
*/
@RequestMapping("/save")
public R save(@RequestBody SpuSaveVo vo){
//spuInfoService.save(spuInfo);
spuInfoService.save(vo);
return R.ok();
}
修改“com.atguigu.gulimall.product.service.SpuInfoService”类,代码如下:
void save(SpuSaveVo vo);
void saveBaseSpuInfo(SpuInfoEntity infoEntity);
修改“com.atguigu.gulimall.product.service.impl.SpuInfoServiceImpl”类,代码如下:
//5、保存spu的积分信息 gulimall_sms --> sms_spu_bounds
注意重点: \textcolor{red} {注意重点:} 注意重点:
1、创建openFeign配置(前提第三方服务已经注册和配置到注册中心了)
/**
* ClassName: CouponFeignService
* Package: com.atguigu.gulimall.product.feign
* Description:
*
* @Author: Mr-Feng
* @Create: 2023/6/1 - 10:09
* @Version: v1.0
*/
@FeignClient("gulimall-coupon")//1、声明调用那个远程服务
public interface CouponFeignService {
}
2、在主程序类中加上@EnableFeignClients(basePackages = “com.atguigu.gulimall.product.feign”)
@EnableFeignClients(basePackages = "com.atguigu.gulimall.product.feign")//2、开启远程调用功能
@MapperScan("com.atguigu.gulimall.product.dao")
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallProductApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallProductApplication.class, args);
}
}
3、在gulimall-common添加服务与服务之间调用的to类“com.atguigu.common.to.SpuBoundTo”类,代码如下:
@Data
public class SpuBoundTo {
private Long SpuId;
private BigDecimal buyBounds;
private BigDecimal growBounds;
}
修改“com.atguigu.gulimall.product.feign.CouponFeignService”类,代码如下:
/**
* ClassName: CouponFeignService
* Package: com.atguigu.gulimall.product.feign
* Description:
*
* @Author: Mr-Feng
* @Create: 2023/6/1 - 10:09
* @Version: v1.0
*/
@FeignClient("gulimall-coupon")
public interface CouponFeignService {
/**
* 1、couponFeignService.saveSpuBounds(spuBoundTo)
* 1)、@RequestBody将这个对象转为json
* 2)、找到gulimall-coupon服务,给/coupon/spubounds/save发送请求。将上一步转的json放在请求体的位置发送请求
* 3)、对方服务收到请求请求体有json数据。
* (@RequestBody SpuBoundsEntity spuBounds)将请求体里的json转为SpuBoundsEntity
* 只要json数据模型是兼容的。对方服务无需使用同一个to
* @param spuBoundTo
* @return
*/
@PostMapping("/coupon/spubounds/save")
R saveSpuBounds(@RequestBody SpuBoundTo spuBoundTo);
4、服务接口
/**
* 保存
*/
@PostMapping("/save")
//@RequiresPermissions("coupon:spubounds:save")
public R save(@RequestBody SpuBoundsEntity spuBounds){
spuBoundsService.save(spuBounds);
return R.ok();
}
设置数据库隔离级别为未提交可读,方便调试
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
修改主键为手动输入非自动
@TableId(type = IdType.INPUT)//指定该主键不是自增的,是我们手动输入的
/**
* spu信息介绍
*
* @author xinzhouf
* @email [email protected]
* @date 2023-03-24 09:26:12
*/
@Data
@TableName("pms_spu_info_desc")
public class SpuInfoDescEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 商品id
*/
@TableId(type = IdType.INPUT)//指定该主键不是自增的,是我们手动输入的
private Long spuId;
/**
* 商品介绍
*/
private String decript;
}
gateway网关模块的application.yml添加ware路由
- id: ware_route
uri: lb://gulimall-ware
predicates:
- Path=/api/ware/**
filters:
- RewritePath=/api/(?>/?.*),/$\{segment}
GulimallWareApplication.java中添加Mapper扫描和事务管理
@EnableTransactionManagement
@MapperScan("com.atguigu.gulimall.ware.dao")
创建feign.ProductFeignService的接口
/**
* ClassName: ProductFeignService
* Package: com.atguigu.gulimall.ware.feign
* Description:
* 1)、让所有请求过网关;
* 1、@FeignClient("gulimall-gateway"):给gulimall-gateway所在的机器发请求
* 2、/api/product/skuinfo/info/{skuId}
* 2)、直接让后台指定服务处理
* 1、@FeignClient("gulimall-product")
* 2、/product/skuinfo/info/{skuId}
*
* @Author: Mr-Feng
* @Create: 2023/6/2 - 14:40
* @Version: v1.0
*/
@FeignClient("gulimall-product") //调用远程服务的模块
public interface ProductFeignService {
@RequestMapping("/product/skuinfo/info/{skuId}")
public R info(@PathVariable("skuId") Long skuId);
}
GulimallWareApplication.java中开启服务
@EnableFeignClients //开启远程Feign服务
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallWareApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallWareApplication.class, args);
}
}
修改WareSkuServiceImpl.java
...
@Autowired
ProductFeignService productFeignService;
...
...
//TODO 远程查询sku的名字,如果失败,整个事务无需回滚
//1、自己catch异常
//TODO 还可以用什么办法让异常出现以后不回滚?高级
try {
R info = productFeignService.info(skuId);
Map<String,Object> data = (Map<String, Object>) info.get("skuInfo");
if(info.getCode() == 0){
skuEntity.setSkuName((String) data.get("skuName"));
}
}catch (Exception e){
}
...
解决:
1、在数据库gulimall_admin执行以下sql再刷新页面:
INSERT INTO sys_menu (menu_id, parent_id, name, url, perms, type, icon, order_num) VALUES (76, 37, '规格维护', 'product/attrupdate', '', 2, 'log', 0);
2、在 /src/router/index.js 在mainRoutes->children里面加上:
{ path: '/product-attrupdate', component: _import('modules/product/attrupdate'), name: 'attr-update', meta: { title: '规格维护', isTab: true } }