一、新建项目
每个子模块用spring初始插件导入 web、和 openfeign 依赖
聚合pom
<?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>
<groupId>com.hong.gulimall</groupId>
<artifactId>gmail</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gmail</name>
<description>聚合</description>
<packaging>pom</packaging>
<modules>
<module>gulimall-product</module>
<module>gulimall-order</module>
<module>gulimall-member</module>
<module>gulimall-ware</module>
<module>gulimall-coupon</module>
<module>renren-fast</module>
<module>renren-generator</module>
</modules>
</project>
二、导入人人开源脚手架
启动renren-fast后台模块
renren-fast是一个轻量级的,前后端分离的Java快速开发平台,能快速开发项目并交付
修改
yml配置文件,导入自带数据库文件,即可启动了
启动renren-fast-vue前端管理界面
安装node.js环境,npm是随同Node.js一起安装的包管理工具,运行renren-fast-vue
只需要执行npm install下载项目,npm run dev执行
如果觉得下的慢的话,可以用国内镜像
npm config set registry http://registry.npm.taobao.org/
由于版本问题,这一步可能会报“Module build failed: Error: Cannot find module 'node-sass”的错误,这里我们就需要多执行一步
这个地方踩了无数的坑,一刷的时候就没有毛病,这里版本升级,我们需要降低sass版本
vscode下在项目根目录打开终端,npm install node-sass@4.13.1,项目默认的sass地址一直404的话可以考虑这样做- 安装完成后打开
package-lock.jsonctrl+f查找sass,当version="4.13.1"时证明安装成功(当然前提是上一步没报错) npm run dev- 补充一点 我事先将 npm版本切换到 6.9.0了
启动renren-generator模块
renren-generator是人人开源项目的代码生成器,可在线生成entity、xml、dao、service、html、js、sql代码,减少70%以上的开发任务
修改配置
我们只需要更改数据库地址和生成模板renren-generator/src/main/resources/generator.properties,启动项目。
选择想生成相关的表格。
将生成代码导入相关模块。由于每个模块都需要很多相似的依赖和测试类,我们可以抽取出一个共同模块gulimall-common
修改
pom文件依赖,并在每个模块导入该模块。
<parent>
<artifactId>gmail</artifactId>
<groupId>com.hong.gulimall</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>gulimall-common</artifactId>
<description>每一个微服务公共的依赖,工具类</description>
<dependency>
<groupId>com.hong.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
给每个模块配置相关数据
- 导入依赖
- 配置数据源
- 配置MyBatis-Plus
- 配置@MapperScan配置扫描
- 配置sql映射文件位置
三、微服务注册中心
Nacos是以服务为主要服务对象的中间件,Nacos支持所有主流的服务发现、配置和管理。
Nacos主要提供以下四大功能:
- 服务发现和服务健康监测
- 动态配置服务
- 动态DNS服务
- 服务及其元数据管理
导入spring-cloud-alibaba-dependencies依赖
只需在common模块中导入即可
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
修改配置
application:
name: gulimall-coupon
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
加注解@EnableDiscoveryClient
启动nacos,然后分别启动各个微服务,将它们注册到Nacos中。
四、使用open-feign调用服务
比如说我们想获取每个会员的所有优惠卷信息。这里我们就需要用到coupon和member两个模块,我们需要在coupon模块查询当前会员所有优惠卷返回给member模块
编写一个接口,告诉SpringCLoud这个接口需要调用远程服务
修改com.hong.gulimall.coupon.controller.CouponController,添加以下controller方法:
// 会员的所有优惠卷信息
@RequestMapping("/member/list")
public R membercoupons(){
CouponEntity couponEntity = new CouponEntity();
couponEntity.setCouponName("满100减10");
return R.ok().put("coupons",Arrays.asList(couponEntity));
}
新建com.hong.gulimall.member.feign.CouponFeignService接口
// 告诉客户端要调用哪个远程服务(声明式远程调用)
@FeignClient("gulimall-coupon")
public interface CouponFeignService {
// 获取会员的优惠卷
@RequestMapping("coupon/coupon/member/list")
public R membercoupons();
}
修改主配置类GulimallMemberApplication类,添加@EnableFeignClients注解,声明接口的每一个方法都是调用哪个远程服务的那个请求
@EnableFeignClients(basePackages = "com.hong.gulimall.member.feign")
测试远程调用功能,在MemberController中添加
@RequestMapping("/coupon")
public R test(){
MemberEntity memberEntity = new MemberEntity();
memberEntity.setNickname("张三");
R membercoupons = couponFeignService.membercoupons();
return R.ok().put("member",membercoupons).put("coupons",membercoupons.get("coupons"));
}
把两个模块都启动,访问地址http://localhost:8000/member/member/coupon
停止
gulimall-coupon服务,能够看到注册中心显示该服务的健康值为0
再次访问
http://localhost:8000/member/member/coupon报错
五、Nacos作为配置中心
导入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
创建bootstrap.properties配置文件
spring.application.name=gulimall-coupon
# 兼顾了配置中心和注册中心地址
spring.cloud.nacos.config.name=127.0.0.1:8848
传统配置模式
创建“application.properties”配置文件
coupon.user.name="zhangsan"
coupon.user.age=30
CouponController中写测试接口
@Value("${coupon.user.name}")
private String name;
@Value("${coupon.user.age}")
private Integer age;
@RequestMapping("/test")
public R getConfigInfo(){
return R.ok().put("name",name).put("age",age);
}
访问http://localhost:7000/coupon/coupon/test
这样做存在的一个问题,如果频繁的修改
application.properties,在需要频繁重新打包部署。下面我们将采用Nacos的配置中心来解决这个问题。
Nacos配置方式
在Nacos注册中心中,点击“配置列表”,添加配置规则:
配置格式:properties
文件的命名规则为:${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
${spring.application.name}:为微服务名
${spring.profiles.active}:指明是哪种环境下的配置,如dev、test或info
${spring.cloud.nacos.config.file-extension}:配置文件的扩展名,可以为properties、yml等
CouponController添加@RefreshScope注解
只要配置文件刷新,数据也跟着自动刷新,动态的从配置中心读取配置.
注意:同时存在properties配置文件和nacos配置中心时,优先加载nacos配置中心
Nacos支持三种配置加载方方案
Nacos支持“Namespace+group+data ID”的配置解决方案。
1、创建命名空间:
“命名空间”—>“创建命名空间”:
创建三个命名空间,分别为dev,test和prop
2、回到配置列表中,能够看到所创建的三个命名空间
下面我们需要在dev命名空间下,创建“gulimall-coupon.properties”配置规则:
3、访问:http://localhost:7000/coupon/coupon/test
并没有使用我们在dev命名空间下所配置的规则,而是使用的是public命名空间下所配置的规则,这是怎么回事呢?
查看“gulimall-coupon”服务的启动日志:
2020-04-24 16:37:24.158 WARN 32792 --- [ main] c.a.c.n.c.NacosPropertySourceBuilder : Ignore the empty nacos configuration and get it based on dataId[gulimall-coupon] & group[DEFAULT_GROUP]
2020-04-24 16:37:24.163 INFO 32792 --- [ main] c.a.nacos.client.config.utils.JVMUtil : isMultiInstance:false
2020-04-24 16:37:24.169 INFO 32792 --- [ main] b.c.PropertySourceBootstrapConfiguration : Located property source: [BootstrapPropertySource {name='bootstrapProperties-gulimall-coupon.properties,DEFAULT_GROUP'}, BootstrapPropertySource {name='bootstrapProperties-gulimall-coupon,DEFAULT_GROUP'}]
"gulimall-coupon.properties" ,默认就是public命名空间中的内容中所配置的规则。
4、指定命名空间
如果想要使得我们自定义的命名空间生效,需要在“bootstrap.properties”文件中,指定使用哪个命名空间:
spring.cloud.nacos.config.namespace=a2c83f0b-e0a8-40fb-9b26-1e9d61be7d6d
这个命名空间ID来源于我们在第一步所创建的命名空间
5、重启“gulimall-coupon”,再次访问:http://localhost:7000/coupon/coupon/test
但是这种命名空间的粒度还是不够细化,对此我们可以为项目的每个微服务module创建一个命名空间。
6、为所有微服务创建命名空间
7、回到配置列表选项卡,克隆pulic的配置规则到coupon命名空间下
切换到coupon命名空间下,查看所克隆的规则:
8、修改“gulimall-coupon”下的bootstrap.properties文件,添加如下配置信息
spring.cloud.nacos.config.namespace=7905c915-64ad-4066-8ea9-ef63918e5f79
这里指明的是,读取时使用coupon命名空间下的配置。
9、重启“gulimall-coupon”,访问:http://localhost:7000/coupon/coupon/test
DataID方案
通过指定spring.profile.active和配置文件的DataID,来使不同环境下读取不同的配置,读取配置时,使用的是默认命名空间public,默认分组(default_group)下的DataID。
默认情况,Namespace=public,Group=DEFAULT GROUP,默认Cluster是DEFAULT
Group方案
通过Group实现环境区分
实例:通过使用不同的组,来读取不同的配置,还是以上面的gulimall-coupon微服务为例
1、新建“gulimall-coupon.properties”,将它置于“tmp”组下
2、修改“bootstrap.properties”配置,添加如下的配置
spring.cloud.nacos.config.group=tmp
3、重启“gulimall-coupon”,访问:http://localhost:7000/coupon/coupon/test
同时加载多个配置集
当微服务数量很庞大时,将所有配置都书写到一个配置文件中,显然不是太合适。对此我们可以将配置按照功能的不同,拆分为不同的配置文件。
数据源有关的配置写到一个配置文件中,和框架有关的写到另外一个配置文件中:
1、创建“datasource.yml”,用于存储和数据源有关的配置
spring:
datasource:
#MySQL配置
url: jdbc:mysql://localhost:3306/gulimall_sms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
2、将和mybatis相关的配置,放置到“mybatis.yml”中
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
# 自增主键
global-config:
db-config:
id-type: auto
3、创建“other.yml”配置,保存其他的配置信息
server:
port: 7000
spring:
application:
name: gulimall-coupon
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
4、修改“gulimall-coupon”的“bootstrap.properties”文件,加载“mybatis.yml”、“datasource.yml”和“other.yml”配置
spring.cloud.nacos.config.extension-configs[0].data-id=mybatis.yml
spring.cloud.nacos.config.extension-configs[0].group=dev
spring.cloud.nacos.config.extension-configs[0].refresh=true
spring.cloud.nacos.config.extension-configs[1].data-id=datasource.yml
spring.cloud.nacos.config.extension-configs[1].group=dev
spring.cloud.nacos.config.extension-configs[1].refresh=true
spring.cloud.nacos.config.extension-configs[2].data-id=other.yml
spring.cloud.nacos.config.extension-configs[2].group=dev
spring.cloud.nacos.config.extension-configs[2].refresh=true
5、注释“application.yml”文件中的所有配置
6、重启“gulimall-coupon”服务,然后访问http://localhost:7000/coupon/coupon/test
7、访问:http://localhost:7000/coupon/coupon/list,查看是否能够正常的访问数据库
小结:
- 微服务任何配置信息,任何配置文件都可以放在配置中心;
- 只需要在
bootstrap.properties中,说明加载配置中心的哪些配置文件即可; @Value,@ConfigurationProperties。都可以用来获取配置中心中所配置的信息;- 配置中心有的优先使用配置中心中的,没有则使用本地的配置。
六、写入网关模块
创建gulimall-gateway模块
导入依赖
<dependency>
<groupId>com.hong.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
加入注册中心nacos
主启动类GulimallGatewayApplication加上@EnableDiscoveryClient注解
排除和数据源相关的配置 主启动类加、
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
启动网关
七、编写商品分类业务
三级分类菜单
业务具体要求:我们想把分类写成三级菜单树形展示的模式。由于每个category有自己的分类id字段、父id字段parent_cid,这里我们可以用sql自查询语句,不过处理起来比较麻烦,所以我们只需拿出所有分类信息,在service层来处理
一个分类下可能又对应一堆分类,所以在gulimall-product模块下的分类实体类中添加一个List集合,在数据库中并没有该字段,这里我们用Mybatis-Plus的注解
/**
* 商品三级分类
*
* @author hong
* @email 1365797108@qq.com
* @date 2022-04-16 11:44:00
*/
@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;
}
在控制层CategoryController添加方法
@Autowired
private CategoryService categoryService;
/**
* 查出所有分类以及子分类,以树形结构组装起来
*/
@RequestMapping("/list/tree")
public R list(@RequestParam Map<String, Object> params){
List<CategoryEntity> entities = categoryService.listWithTree();
return R.ok().put("page", entities);
}
Service层:这里只需要拿到所有的分类,所以不需要编写dao层,直接用mp的api拿到所有数据交给服务层处理。
@Override
public List<CategoryEntity> listWithTree() {
// 1、查出所有分类
List<CategoryEntity> entities = baseMapper.selectList(null);
// 2、组装成父子的树形结构
// 2.1)、filter找到所有的一级分类
// 2.2)、map把当前菜单子菜单封装进去
// 2.3)、排序
List<CategoryEntity> level1Menu = entities.stream().filter((categoryEntity -> {
return 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 level1Menu;
}
// 在all中找到当前菜单所有子菜单
private List<CategoryEntity> getChildren(CategoryEntity root,List<CategoryEntity> all){
List<CategoryEntity> children = all.stream().filter(categoryEntity -> {
return categoryEntity.getParentCid() == root.getCatId();
}).map(categoryEntity -> {
// 1、找到子菜单
categoryEntity.setChildren(getChildren(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;
}
然后启动后台管理,新增一个一级菜单和二级菜单,注意URL规则
product/category对应http://localhost:8001/#/product-category地址,对应view/product/category.vue组件。
网关配置
编写前端业务,我们可以用如下代码测试
//方法集合
methods: {
getMenus(){
this.$http({
url: this.$http.adornUrl('/product/category/list/tree'),
method: 'get'
}).then(data => {
console.log("成功获取到菜单数据...",data)
})
}
},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
this.getMenus();
},
这里我们发现404报错,后台
product的业务端口是10000,renren-fast端口为8000,所以这里肯定是访问不到的。
查看static/config/index.js文件
这里我们需要改成网关地址,因为后台服务器的端口并不唯一,我们只需要用网关来接收后端服务,再转发到前端
// api接口请求地址
// 定义规则:前端项目, /api
window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api';
由于更改了配置,后台的验证码也会失效,这时就需要我们去配置网关
renren-fast注册到服务中心
这里网关配置过程,前端请求网关地址为http://localhost:88/api/captcha.jpg?uuid=...
renren-fast服务实际地址http://localhost:8080/renren-fast/captcha.jpg?uuid=...
我们按照路径来断言,先将请求负载均衡到renren-fast,那么此时就会去注册中心找renren-fast服务,找到后拼接成地址为http://renren-fast/api/captcha.jpg?uuid=...,此时我们再用网关的过滤器网关重写功能去掉/api
gateway:
routes:
- id: admin_route
# lb:负载均衡
uri: lb://renren-fast
# 按照路径来断言,任意请求先到renren-fast
predicates:
- Path=/api/**
filters:
- RewritePath=/api/(?<segment>.*),/renren-fast/${segment}
访问成功.
解决跨域问题
-
跨域:指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对javascript施加的安全限制。
-
同源策略:是指协议,域名,端口都要相同,其中有一个不同都会产生跨域;
跨域流程
两种解决办法
- 使用nginx部署为同一域
nginx进行反向代理时,对外暴露的时同一个域,不会产生跨域问题.
- 配置当次请求允许跨域
修改预先请求Option,让真正请求发过来
1、添加响应头
• Access-Control-Allow-Origin:支持哪些来源的请求跨域
• Access-Control-Allow-Methods:支持哪些方法跨域
• Access-Control-Allow-Credentials:跨域请求默认不包含cookie,设置为true可以包含cookie
• Access-Control-Expose-Headers:跨域请求暴露的字段
• CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。
• Access-Control-Max-Age:表明该响应的有效时间为多少秒。在有效时间内,浏览器无
须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。
在网关里面统一解决跨域
在网关模块中编写配置类使用Spring自带的CorsWebFilter
package com.hong.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(){
// 基于url跨域,选择reactive包下的
UrlBasedCorsConfigurationSource source=new UrlBasedCorsConfigurationSource();
// 跨域配置信息
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 允许跨域的头
corsConfiguration.addAllowedHeader("*");
// 允许跨域的请求方式
corsConfiguration.addAllowedMethod("*");
// 允许跨域的请求来源
corsConfiguration.addAllowedOrigin("*");
// corsConfiguration.addAllowedOriginPattern("*");
// 是否允许携带cookie跨域
corsConfiguration.setAllowCredentials(true);
// 任意url都要进行跨域配置
source.registerCorsConfiguration("/**",corsConfiguration);
return new CorsWebFilter(source);
}}
若出现如上错误,只需要将renren-fast中自带的跨域配置文件注释即可。
编写product路由
防止出现如上错误,我们需要把精确的路由放在上面
gateway:
routes:
- id: product_route
# lb:负载均衡
uri: lb://gulimall-product
# 按照路径来断言,任意请求先到renren-fast
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?<segment>.*),/${segment}
获取到数据,我们只用到
data,所以解构出来{data}
逻辑删除
mp配置
mybatis-plus:
global-config:
db-config:
id-type: auto
logic-delete-value: 1
logic-not-delete-value: 0
低版本可能要配置全局逻辑删除规则和组件Bean
加注解@TableLogic
/**
* 是否显示[0-不显示,1显示]
当前项目逻辑反了,加参数即可
*/
@TableLogic(value = "1",delval = "0")
private Integer showStatus;
分类增删改和拖曳
增删改后端逆向工程都已经自己生成,这里只需要处理前端了。使用element ui引入相关组件
<div>
<el-switch v-model="draggable" active-text="开启拖拽" inactive-text="关闭拖拽"></el-switch>
<el-button v-if="draggable" @click="batchSave">批量保存</el-button>
<el-button type="danger" @click="batchDelete">批量删除</el-button>
<el-tree
:data="menus"
show-checkbox
:props="defaultProps"
node-key="id"
:default-expanded-keys="expandedKey"
:draggable="draggable"
:allow-drop="allowDrop"
@node-drop="handleDrop"
:expand-on-click-node="false">
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<!--只有当前节点层级不为3显示-->
<el-button v-if="data.catLevel <= 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="data.children.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>
这里一个对话框既要处理编辑业务,又要处理添加业务,我们需要维护一个dialogType变量来判断是add还是edit,从而选取对应的处理。
data(){
return {
title: "",
dialogType: "", //edit,add
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
productUnit: "",
icon: "",
// 必须置空
catId: null
},
dialogVisible: false,
expandedKey: [],
menus: [],
defaultProps: {
children: 'children',
label: 'name'
}
}
}
methods:{
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.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,
*/
});
},
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;
// id自增
this.category.catId = null;
this.category.name = "";
this.category.icon = "";
this.category.productUnit = "";
this.category.sort = 0;
this.category.showStatus = 1;
},
submitData() {
if (this.dialogType == "add") {
this.addCategory();
}
if (this.dialogType == "edit") {
this.editCategory();
}
},
//修改三级分类数据
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.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
//添加三级分类
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.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
remove(node, data) {
console.log("node",node)
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"
});
console.log("删除成功",data)
// 刷新菜单
this.getMenus();
// 设置展开的位置
this.expandedKey = [node.parent.data.catId]
});
}).catch(() => {
});
}
}
}
八、商品品牌管理实现
新建菜单,直接导入逆向工程生成的相关vue文件.
实现文件上传服务器
文件存储
文件的几种上传方式
这里我们在项目中使用阿里云的云对象存储
oss
阿里云对象存储-普通上传方式
阿里云对象存储-服务端签名后直传
Post Policy的使用规则在服务端通过各种语言代码完成签名,然后通过表单直传数据到OSS。由于服务端签名直传无需将AccessKey暴露在前端页面,相比JavaScript客户端签名直传具有更高的安全性
开通阿里云oss服务,下面我们来测试上传一个照片到oss服务器
导入依赖
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.5.0</version>
</dependency>
编写测试代码。
@Test
public void testUpload() throws FileNotFoundException {
// Endpoint以杭州为例,其它Region请按实际情况填写。
String endpoint = "oss-cn-beijing.aliyuncs.com";
// 安全问题,并不会账号密码,开通子用户
// 云账号AccessKey有所有API访问权限,建议遵循阿里云安全最佳实践,创建并使用RAM子账号进行API访问或日常运维,请登录 https://ram.console.aliyun.com 创建。
String accessKeyId = "LTAI5tBJp2ZeSRhUxxCZBqHB";
String accessKeySecret = "youraccessKeySecret";
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 上传文件流。
InputStream inputStream = new FileInputStream("C:\Users\鸿\Desktop\Guli Mall\分布式基础\资源\pics\2b1837c6c50add30.jpg");
// hongmall 实例对象 ,上传的名字
ossClient.putObject("hongmall", "1.png", inputStream);
// 关闭OSSClient。
ossClient.shutdown();
System.out.println("上传成功...");
}
SpringCloud整合oss
我们可以直接引入,阿里整合好的组件依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alicloud-oss</artifactId>
<version>0.2.2.RELEASE</version>
</dependency>
只需要在配置文件中配置相关配置并直接注入oss对象即可.
spring:
cloud:
alicloud:
access-key: Jp2ZeSRhUxxCZBqHB
secret-key: cZ2VmMmrR
oss:
endpoint: oss-cn-beijing.aliyuncs.com
@Autowired
OSSClient ossClient;
编写第三方模块gulimall-third-party
我们可以专门编写一个模块把第三方依赖聚合起来。 pom.xml
<?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>
<groupId>com.hong.gulimall</groupId>
<artifactId>gulimall-third-party</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gulimall-third-party</name>
<description>第三方服务</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.1.8.RELEASE</spring-boot.version>
<spring-cloud.version>Greenwich.SR3</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>com.hong.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>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alicloud-oss</artifactId>
<version>0.2.2.RELEASE</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>com.aliyun.oss</groupId>-->
<!-- <artifactId>aliyun-sdk-oss</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<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>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<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>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</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>
编写controller层,服务端签名后直传业务代码
package com.hong.gulimall.thirdparty.controller;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.common.utils.BinaryUtil;
import com.aliyun.oss.model.MatchMode;
import com.aliyun.oss.model.PolicyConditions;
import com.hong.common.utils.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
@RestController
public class OssController {
// @Autowired
// OSS ossClient;
@RequestMapping("/oss/policy")
public R policy() {
// https://hongmall.oss-cn-beijing.aliyuncs.com/1.png
// Endpoint以杭州为例,其它Region请按实际情况填写。
String endpoint = "oss-cn-beijing.aliyuncs.com";
// 安全问题,并不会账号密码,开通子用户
// 云账号AccessKey有所有API访问权限,建议遵循阿里云安全最佳实践,创建并使用RAM子账号进行API访问或日常运维,请登录 https://ram.console.aliyun.com 创建。
String accessKeyId = "LTAI5tBJp2ZeSRhUxxCZBqHB";
String accessKeySecret = "C69ep2mbVIhKMDJT7bdrYcZ2VmMmrR";
String bucket = "hongmall"; // 请填写您的 bucketname 。
String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint
// callbackUrl为 上传回调服务器的URL,请将下面的IP和Port配置为您自己的真实信息。
//String callbackUrl = "http://88.88.88.88:8888";
String format = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
String dir = format + "/"; // 用户上传文件时指定的前缀。
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
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", accessKeyId);
respMap.put("policy", encodedPolicy);
respMap.put("signature", postSignature);
respMap.put("dir", dir);
respMap.put("host", host);
respMap.put("expire", String.valueOf(expireEndTime / 1000));
} catch (Exception e) {
// Assert.fail(e.getMessage());
System.out.println(e.getMessage());
} finally {
ossClient.shutdown();
}
return R.ok().put("data",respMap);
}
}
启动测试
网关配置
- id: product_route
# lb:负载均衡
uri: lb://gulimall-third-party
# 按照路径来断言,任意请求先到renren-fast
predicates:
- Path=/api/thirdparty/**
filters:
- RewritePath=/api/thirdparty/(?<segment>.*),/${segment}
编写品牌logo图片上传业务
将服务器文件上传方法封装到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);
})
});
}
编写单文件上传和多文件上传组件
singeUpload.vue
<template>
<div>
<el-upload
action="https://hongmall.oss-cn-beijing.aliyuncs.com"
:data="dataObj"
list-type="picture"
:multiple="false" :show-file-list="showFileList"
:file-list="fileList"
:before-upload="beforeUpload"
:on-remove="handleRemove"
:on-success="handleUploadSuccess"
:on-preview="handlePreview">
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过10MB</div>
</el-upload>
<el-dialog :visible.sync="dialogVisible">
<img width="100%" :src="fileList[0].url" alt="">
</el-dialog>
</div>
</template>
<script>
import {policy} from './policy'
import { getUUID } from '@/utils'
export default {
name: 'singleUpload',
props: {
value: String
},
computed: {
imageUrl() {
return this.value;
},
imageName() {
if (this.value != null && this.value !== '') {
return this.value.substr(this.value.lastIndexOf("/") + 1);
} else {
return null;
}
},
fileList() {
return [{
name: this.imageName,
url: this.imageUrl
}]
},
showFileList: {
get: function () {
return this.value !== null && this.value !== ''&& this.value!==undefined;
},
set: function (newValue) {
}
}
},
data() {
return {
dataObj: {
policy: '',
signature: '',
key: '',
ossaccessKeyId: '',
dir: '',
host: '',
// callback:'',
},
dialogVisible: false
};
},
methods: {
emitInput(val) {
this.$emit('input', val)
},
handleRemove(file, fileList) {
this.emitInput('');
},
handlePreview(file) {
this.dialogVisible = true;
},
beforeUpload(file) {
let _self = this;
return new Promise((resolve, reject) => {
policy().then(response => {
console.log("响应的数据",response);
_self.dataObj.policy = response.data.policy;
_self.dataObj.signature = response.data.signature;
_self.dataObj.ossaccessKeyId = response.data.accessid;
// uuid 防止路径重复
_self.dataObj.key = response.data.dir +getUUID()+'_${filename}';
_self.dataObj.dir = response.data.dir;
_self.dataObj.host = response.data.host;
console.log("响应的数据222",_self.dataObj);
resolve(true)
}).catch(err => {
reject(false)
})
})
},
handleUploadSuccess(res, file) {
console.log("上传成功...")
this.showFileList = true;
this.fileList.pop();
this.fileList.push({name: file.name, url: this.dataObj.host + '/' + this.dataObj.key.replace("${filename}",file.name) });
this.emitInput(this.fileList[0].url);
}
}
}
</script>
<style>
</style>
上传文件测试
由于前端是拿到
response响应信息直接向oss服务器发送post请求,这时候就会出现跨越问题。只需要修改一下即可
测试品牌上传
这里由于switch组件的开关是true和false,数据库中为1和0,需要加上:active-value="1" :inactive-value="0"属性。
将表格图片内容进行自定义显示
<el-table-column
prop="logo"
header-align="center"
align="center"
label="品牌logo地址">
<template slot-scope="scope">
<img :src="scope.row.logo" style="width:100px;height:8px">
</template>
</el-table-column>
校验功能
前端自定义校验器
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 (!Number.isInteger(value) || value<0) {
callback(new Error("排序必须是一个大于等于0的整数"));
} else {
callback();
}
},
trigger: "blur"
}
]
}
}
后端使用JSR303校验
- 给
Bean添加校验注解 - 开启校验功能
@Valid,检验错误后有响应的响应,也可以自定义。 - 校验
Bean的后面紧跟一个BindingResult可以获取校验的结果。
/**
* 保存
*/
@RequestMapping("/save")
// 告诉springmvc校验注解
public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){
if (result.hasErrors()){
Map<String, String> map = new HashMap<>();
// 获取校验结果
result.getFieldErrors().forEach((fieldError) -> {
String message = fieldError.getDefaultMessage();
// 获取错误属性名字
String field = fieldError.getField();
map.put(field,message);
});
return R.error().put("400","提交数据不合法").put("data",map);
}else {
brandService.save(brand);
}
return R.ok();
}
测试
统一异常处理
可以使用springmvc的@ControllerAdvice,这里只要发现异常就可以自动捕获,前面代码controller代码功能也将被取代。
package com.hong.gulimall.product.exception;
import com.hong.common.exception.BizCodeEnum;
import com.hong.common.utils.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
/**
* 集中处理异常
*/
@Slf4j
@RestControllerAdvice(basePackages = "com.xunqi.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {
/**
* 参数非法(效验参数)异常 MethodArgumentNotValidException
* @param e
* @return
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public R handleValidException(MethodArgumentNotValidException e) {
log.error("数据效验出现问题{},异常类型{}",e.getMessage(),e.getClass());
BindingResult bindingResult = e.getBindingResult();
Map<String,String> errMap = new HashMap<>();
bindingResult.getFieldErrors().forEach((fieldError) -> {
errMap.put(fieldError.getField(),fieldError.getDefaultMessage());
});
return R.error(BizCodeEnum.VAILD_EXCEPTION.getCode(),BizCodeEnum.VAILD_EXCEPTION.getMessage())
.put("data",errMap);
}
@ExceptionHandler(value = Throwable.class)
public R handleException(Throwable throwable) {
log.error("错误异常{}",throwable);
return R.error(BizCodeEnum.UNKNOW_EXCEPTION.getCode(),BizCodeEnum.UNKNOW_EXCEPTION.getMessage());
}
}
在common模块新建异常枚举类
package com.hong.common.exception;
/**
错误码和错误信息定义类
* 1. 错误码定义规则为5为数字
* 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常
* 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
* 错误码列表:
* 10: 通用
* 001:参数格式校验
* 002:短信验证码频率太高
* 11: 商品
* 12: 订单
* 13: 购物车
* 14: 物流
* 15:用户
**/
public enum BizCodeEnum {
UNKNOW_EXCEPTION(10000,"系统未知异常"),
VAILD_EXCEPTION(10001,"参数格式校验失败"),
TO_MANY_REQUEST(10002,"请求流量过大,请稍后再试"),
SMS_CODE_EXCEPTION(10002,"验证码获取频率太高,请稍后再试"),
PRODUCT_UP_EXCEPTION(11000,"商品上架异常"),
USER_EXIST_EXCEPTION(15001,"存在相同的用户"),
PHONE_EXIST_EXCEPTION(15002,"存在相同的手机号"),
NO_STOCK_EXCEPTION(21000,"商品库存不足"),
LOGINACCT_PASSWORD_EXCEPTION(15003,"账号或密码错误"),
;
private Integer code;
private String message;
BizCodeEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
}
JSR303分组校验
比如一些字段修改时并不用处理,增加时才需要全部处理,这里我们就可以用JSR303分组校验功能
package com.hong.gulimall.product.entity;
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 hong
* @email 1365797108@qq.com
* @date 2022-04-16 11:44:00
*/
@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;
}
给校验注解标注什么情况需要进行校验,默认没有指定分组的校验注解@NotBlank,在分组校验情况下不生效只会在@Valid生效。
自定义校验
- 编写一个自定义的校验注解。
- 编写一个自定义的校验器。
- 关联自定义的校验器和自定义的校验注解。
package com.hong.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;
/**
* @Description: 自定义注解规则
**/
@Documented
@Constraint(validatedBy = { ListValueConstraintValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
String message() default "{com.xunqi.common.valid.ListValue.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
int[] vals() default { };
}
package com.hong.common.valid;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
private Set<Integer> set = new HashSet<>();
/**
* 初始化方法
* @param constraintAnnotation
*/
@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) {
//判断是否有包含的值
boolean contains = set.contains(value);
return contains;
}
}
九、属性分组-规格参数-销售属性
属性分组-规格参数-销售属性-三级分类关联关系。
在写属性相关业务之前,我们先了解一下数据库设计。
- 每个三级分类对应一个属性分组
- 一个属性分组里又有多个属性
- 分类既与属性分组有对应关系,又和属性有对应关系。
比如一个手机的分为主体和屏幕两个属性分组,而屏幕有分为内存、像素两个属性。
SPU-SKU-属性
一、SPU全称Standard Product Unit (标准化产品单元)。译为:最小包装单元;
SPU可以直接认为是很多个产品打包组成的一个新物品,有更多的新特性和更多的形态。
二、SKU全称stock keeping unit(库存量单位)。译为:最小主要单元;
SKU不同于SPU,它可以认为就是一个很简单的物品。而这些个简单的物品打包组合就是SPU,比如,现在有5个iPhone(SKU),如果5个为一个生产最小单位,那么这5个iPhone就是组合打包产品(SPU)。又比如一款iPhone有很多种配置可选,每种配置就是SKU,但最终都还是一个iPhone(SPU).
我们看下该项目中有关SKU和SPU相关表
由图可看出一个
spu可以对应多个sku,又对应多个属性。
比如:id为 1的spu的分别对应一个内存和容量6+128G和4+64G这个两个sku.
分页条件检索查找每个分类下所有属性分组
/**
* 查找每个分类下所有属性分组
*/
@RequestMapping("/list/{catelogId}")
// @RequestParam:将请求参数绑定到你控制器的方法参数上(是springmvc中接收普通参数的注解)
public R list(@RequestParam Map<String, Object> params,
@PathVariable("catelogId") Long catelogId){
PageUtils page = attrGroupService.queryPage(params,catelogId);
return R.ok().put("page", page);
}
MyBatis-Plus已经将分页查询通过IPage接口封装起来,里面包含很多分页属性。
MP自定义了一个查询参数Query类,将IPage的实现类Page封装在里面,同时初始化一些分页所需的变量。
package com.hong.common.utils;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hong.common.xss.SQLFilter;
import org.apache.commons.lang.StringUtils;
import java.util.Map;
/**
* 查询参数
*/
public class Query<T> {
public IPage<T> getPage(Map<String, Object> params) {
return this.getPage(params, null, false);
}
public IPage<T> getPage(Map<String, Object> params, String defaultOrderField, boolean isAsc) {
//分页参数
long curPage = 1;
long limit = 10;
if(params.get(Constant.PAGE) != null){
curPage = Long.parseLong((String)params.get(Constant.PAGE));
}
if(params.get(Constant.LIMIT) != null){
limit = Long.parseLong((String)params.get(Constant.LIMIT));
}
//分页对象
Page<T> page = new Page<>(curPage, limit);
//分页参数
params.put(Constant.PAGE, page);
//排序字段
//防止SQL注入(因为sidx、order是通过拼接SQL实现排序的,会有SQL注入风险)
String orderField = SQLFilter.sqlInject((String)params.get(Constant.ORDER_FIELD));
String order = (String)params.get(Constant.ORDER);
//前端字段排序
if(StringUtils.isNotEmpty(orderField) && StringUtils.isNotEmpty(order)){
if(Constant.ASC.equalsIgnoreCase(order)) {
return page.addOrder(OrderItem.asc(orderField));
}else {
return page.addOrder(OrderItem.desc(orderField));
}
}
//没有排序字段,则不排序
if(StringUtils.isBlank(defaultOrderField)){
return page;
}
//默认排序
if(isAsc) {
page.addOrder(OrderItem.asc(defaultOrderField));
}else {
page.addOrder(OrderItem.desc(defaultOrderField));
}
return page;
}
}
同时自定义了封装分页数据返回类,聚合一个Page类。
package com.hong.common.utils;
import com.baomidou.mybatisplus.core.metadata.IPage;
import java.io.Serializable;
import java.util.List;
/**
* 分页工具类
*
* @author Mark sunlightcs@gmail.com
*/
public class PageUtils implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 总记录数
*/
private int totalCount;
/**
* 每页记录数
*/
private int pageSize;
/**
* 总页数
*/
private int totalPage;
/**
* 当前页数
*/
private int currPage;
/**
* 列表数据
*/
private List<?> list;
/**
* 分页
* @param list 列表数据
* @param totalCount 总记录数
* @param pageSize 每页记录数
* @param currPage 当前页数
*/
public PageUtils(List<?> list, int totalCount, int pageSize, int currPage) {
this.list = list;
this.totalCount = totalCount;
this.pageSize = pageSize;
this.currPage = currPage;
this.totalPage = (int)Math.ceil((double)totalCount/pageSize);
}
/**
* 分页
*/
public PageUtils(IPage<?> page) {
this.list = page.getRecords();
this.totalCount = (int)page.getTotal();
this.pageSize = (int)page.getSize();
this.currPage = (int)page.getCurrent();
this.totalPage = (int)page.getPages();
}
public int getTotalCount() {
return totalCount;
}
public void setTotalCount(int totalCount) {
this.totalCount = totalCount;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public int getTotalPage() {
return totalPage;
}
public void setTotalPage(int totalPage) {
this.totalPage = totalPage;
}
public int getCurrPage() {
return currPage;
}
public void setCurrPage(int currPage) {
this.currPage = currPage;
}
public List<?> getList() {
return list;
}
public void setList(List<?> list) {
this.list = list;
}
}
业务实现类AttrGroupServiceImpl
@Override
public PageUtils queryPage(Map<String, Object> params, Long catelogId) {
// catelogId == 0查询所有
if (catelogId == 0){
IPage<AttrGroupEntity> page = this.page(new Query<AttrGroupEntity>().getPage(params),
new QueryWrapper<AttrGroupEntity>());
return new PageUtils(page);
}else {
String key = (String) params.get("key");
// 按照三级分类
// select * from pms_attr_group where catelog_id = ? and (sttr_group_id=key or attr_group_name like key)
QueryWrapper<AttrGroupEntity> wrapper = new QueryWrapper<AttrGroupEntity>().eq("catelog_id", catelogId);
if (!StringUtils.isEmpty(key)){
// 条件检索
wrapper.and((obj) -> {
obj.eq("attr_group_id",key).or().like("attr_group_name",key);
});
}
IPage<AttrGroupEntity> page = this.page(new Query<AttrGroupEntity>().getPage(params),
wrapper);
return new PageUtils(page);
}
}
编写前端业务
抽取出category公共组件
<template>
<div>
<el-input placeholder="输入关键字进行过滤" v-model="filterText"></el-input>
<el-tree
:data="menus"
:props="defaultProps"
node-key="catId"
ref="menuTree"
@node-click="nodeclick"
:filter-node-method="filterNode"
:highlight-current = "true"
></el-tree>
</div>
</template>
<script>
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
props: {},
data() {
//这里存放数据
return {
filterText: "",
menus: [],
expandedKey: [],
defaultProps: {
children: "children",
label: "name"
}
};
},
//计算属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {
filterText(val) {
this.$refs.menuTree.filter(val);
}
},
//方法集合
methods: {
//树节点过滤
filterNode(value, data) {
if (!value) return true;
return data.name.indexOf(value) !== -1;
},
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get"
}).then(({ data }) => {
this.menus = data.page;
});
},
nodeclick(data, node, component) {
console.log("子组件category的节点被点击", data, node, component);
//向父组件发送事件;
this.$emit("tree-node-click", data, node, component);
}
},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
this.getMenus();
}
</script>
父子组件通信
要求只有点击第三级分类时才查找数据并显示在右边表格,当子组件category.vue的三级分类菜单被点击时,在父组件sttrgroup.vue中感知子组件被点击同时触发相应方法。
- 在父组件模板中,为子组件绑定自定义事件
<category @tree-node-click="treenodeclick"></category>
- 在子组件中 通过$emit发布消息,传递子组件中数据
nodeclick(data, node, component) {
console.log("子组件category的节点被点击", data, node, component);
//向父组件发送事件;
this.$emit("tree-node-click", data, node, component);
}
- 在父组件事件回调函数中接收数据并存储数据
/**
* 父子组件传递数据
* 1)、子组件给父组件传递数据,事件机制;
* 子组件给父组件发送一个事件,携带上数据。
* // this.$emit("事件名",携带的数据...)
*/
import Category from "../common/category";
import AddOrUpdate from "./attrgroup-add-or-update";
import RelationUpdate from "./attr-group-relation";
export default {
//import引入的组件需要注入到对象中才能使用
components: { Category, AddOrUpdate, RelationUpdate }
//感知树节点被点击
treenodeclick(data, node, component) {
console.log("attrgroup感知到category的节点被点击",data,node,component);
console.log("刚才被点击的菜单id:"+data.id);
if (node.level == 3) {
this.catId = data.catId;
this.getDataList(); //重新查询
}
},
getAllDataList(){
this.catId = 0;
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;
});
}
前端
<template>
<el-row :gutter="20">
<el-col :span="6">
<category @tree-node-click="treenodeclick"></category>
</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 type="success" @click="getAllDataList()">查询全部</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="relationHandle(scope.row.attrGroupId)">关联</el-button>
<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>
<!-- 修改关联关系 -->
<relation-update v-if="relationVisible" ref="relationUpdate" @refreshData="getDataList"></relation-update>
</div>
</el-col>
</el-row>
</template>
打开MP日志,测试结果
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
添加属性分组时实现级联选择器显示分类
引入级联选择器组件
<el-dialog
:title="!dataForm.id ? '新增' : '修改'"
:close-on-click-modal="false"
:visible.sync="visible"
@closed="dialogClose"
>
<el-form-item label="所属分类" prop="catelogId">
<!--filterable对话框可搜索-->
<el-cascader filterable placeholder="试试搜索:手机" v-model="catelogPath" :options="categorys" :props="props"></el-cascader>
</el-form-item>
props:{
value:"catId",
label:"name",
children:"children"
},
categorys: [],
catelogPath: []
@JsonInclude注解
由于查询数据时记录进了空数组,这里我们并不希望将空值也记录其中,只需要对该字段加上
@JsonInclude注解。
// 指定不为空时返回才带上
@JsonInclude(JsonInclude.Include.NON_EMPTY)
// 数据库不存在,封装在该类
@TableField(exist = false)
private List<CategoryEntity> children;
修改属性分组
<template slot-scope="scope">
<el-button type="text" size="small" @click="relationHandle(scope.row.attrGroupId)">关联</el-button>
<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>
点击修改会弹出attrgroup-add-or-update组件,并调用子组件的init方法
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="getDataList"></add-or-update>
// 新增 / 修改
addOrUpdateHandle(id) {
this.addOrUpdateVisible = true;
this.$nextTick(() => {
this.$refs.addOrUpdate.init(id);
});
}
init(id) {
this.dataForm.attrGroupId = id || 0;
this.visible = true;
this.$nextTick(() => {
this.$refs["dataForm"].resetFields();
// 如果有属性分组的值,那么就向服务器请求数据
if (this.dataForm.attrGroupId) {
this.$http({
// 用id检索出详细信息
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 = data.attrGroup.catelogId;
//查出catelogId的完整路径
this.catelogPath = data.attrGroup.catelogPath;
}
});
}
});
}
这里发现分类并不会回显,分类应该为一整个三级菜单分类,而这里赋值的是一个三级菜单的分类id,我们还需要查出catelogId的完整路径,这里我们就希望后台传过来的数据能包含一个catelogPath完整路径,我们需要给后台AttrGroupEntity添加一个Long[]类型的完整路径catelogPath。
package com.hong.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 lombok.Data;
/**
* 属性分组
*
* @author hong
* @email 1365797108@qq.com
* @date 2022-04-16 11:44:00
*/
@Data
@TableName("pms_attr_group")
public class AttrGroupEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 分组id
*/
@TableId
private Long attrGroupId;
/**
* 组名
*/
private String attrGroupName;
/**
* 排序
*/
private Integer sort;
/**
* 描述
*/
private String descript;
/**
* 组图标
*/
private String icon;
/**
* 所属分类id
*/
private Long catelogId;
@TableField(exist = false)
private Long[] catelogPath;
}
服务器业务
AttrGroupController
@Autowired
private CategoryService categoryService;
/**
* 查询属性信息
*/
@RequestMapping("/info/{attrGroupId}")
public R info(@PathVariable("attrGroupId") Long attrGroupId){
AttrGroupEntity attrGroup = attrGroupService.getById(attrGroupId);
Long catelogId = attrGroup.getCatelogId();
// 查找父类
Long[] path = categoryService.findCatelogPath(catelogId);
attrGroup.setCatelogPath(path);
return R.ok().put("attrGroup", attrGroup);
}
CategoryServiceImpl
/**
* 根据第三级分类查找完整路径
* @param catelogId
* @return
*/
@Override
public Long[] findCatelogPath(Long catelogId) {
List<Long> paths = new ArrayList<>();
List<Long> parentPath = findParentPath(catelogId, paths);
return parentPath.toArray(new Long[parentPath.size()]);
}
private List<Long> findParentPath(Long catelogId, List<Long> paths) {
// 1、收集当前节点
paths.add(catelogId);
CategoryEntity byId = this.getById(catelogId);
if (byId.getParentCid() != 0){
findParentPath(byId.getParentCid(),paths);
}
return paths;
}
前端实现
这里子组件改变给父组件发事件刷新数值,
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="getDataList"></add-or-update>
// 表单提交
dialogClose(){
// 关闭清空
this.catelogPath = [];
},
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.catelogPath[this.catelogPath.length-1]
})
}).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);
}
});
}
});
}
},
created(){
this.getCategorys();
}
修改关联关系
一个分类可以关联多个品牌。
获取品牌关联的分类
这里只需要传递一个brandId参数即可。
CategoryBrandRelationController
/**
* 获取品牌关联的分类
*/
@GetMapping("/catelog/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);
}
新增品牌和分类关联关系
分别传入
brandId和catelogId两个参数。这里由于只需要回显分类名、品牌名,而如果每次都去数据库中查就会影响性能,电商项目一般不从数据库一个个查,所以我们建一个如下结构的数据库表。这样添加冗余字段只需要查询一次,但修改需要同步
/**
* 新增品牌和分类关联关系
*/
@RequestMapping("/save")
public R save(@RequestBody CategoryBrandRelationEntity categoryBrandRelation){
categoryBrandRelationService.saveDetail(categoryBrandRelation);
return R.ok();
}
@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);
}
前端(新增品牌和分类关联关系)
这里我们把级联选择器抽取出来成一个组件。
<template>
<!--
使用说明:
1)、引入category-cascader.vue
2)、语法:<category-cascader :catelogPath.sync="catelogPath"></category-cascader>
解释:
catelogPath:指定的值是cascader初始化需要显示的值,应该和父组件的catelogPath绑定;
由于有sync修饰符,所以cascader路径变化以后自动会修改父的catelogPath,这是结合子组件this.$emit("update:catelogPath",v);做的
-->
<div>
<el-cascader
filterable
clearable
placeholder="试试搜索:手机"
v-model="paths"
:options="categorys"
:props="setting"
></el-cascader>
</div>
</template>
<script>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
//接受父组件传来的值
props: {
catelogPath: {
type: Array,
default(){
return [];
}
}
},
data() {
//这里存放数据
return {
setting: {
value: "catId",
label: "name",
children: "children"
},
categorys: [],
paths: this.catelogPath
};
},
watch:{
catelogPath(v){
this.paths = this.catelogPath;
},
paths(v){
this.$emit("update:catelogPath",v);
//还可以使用pubsub-js进行传值
// this.PubSub.publish("catPath",v);
}
},
//方法集合
methods: {
getCategorys() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get"
}).then(({ data }) => {
this.categorys = data.page;
});
}
},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
this.getCategorys();
}
};
</script>
<style scoped>
</style>
这样添加冗余字段只需要查询一次,但修改需要同步
修改更新同步业务
BrandController
/**
* 更新(添加冗余字段只需要查询一次,但修改需要同步)
*/
@RequestMapping("/update")
public R update(@RequestBody BrandEntity brand){
brandService.updateDetail(brand);
return R.ok();
}
@Transactional(rollbackFor = Exception.class)
@Override
public void updateDetail(BrandEntity brand) {
// 保证冗余字段的数据一致
this.updateById(brand);
if (!StringUtils.isEmpty(brand.getBrandId())){
// 同步更新其他关联表中的数据
categoryBrandRelationService.updateBrand(brand.getBrandId(),brand.getName());
// TODO 更新其他关联
}
}
CategoryBrandRelationServiceImpl
@Override
public void updateBrand(Long brandId, String name) {
CategoryBrandRelationEntity categoryBrandRelationEntity = new CategoryBrandRelationEntity();
categoryBrandRelationEntity.setBrandId(brandId);
// 只带了哪个字段更新哪个字段
categoryBrandRelationEntity.setBrandName(name);
this.update(categoryBrandRelationEntity,
new UpdateWrapper<CategoryBrandRelationEntity>().eq("brand_id",brandId));
}
修改更新同步业务级联分类
/**
* 修改(级联更新同步)
*/
@RequestMapping("/update")
public R update(@RequestBody CategoryEntity category){
categoryService.updateByCascade(category);
return R.ok();
}
/**
* 级联更新所有关联的数据
* @param category
*/
// 开启事务
@Transactional
@Override
public void updateByCascade(CategoryEntity category) {
this.updateById(category);
categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
}
void updateCategory(@Param("catId") Long catId,@Param("name") String name);
<update id="updateCategory" >
update pms_category_brand_relation set catelog_name = {name} WHERE catelog_id = {catId}
</update>
获取分类规格参数和商品参数
这两个的业务代码非常相似,路径分别为product/attr/sale/list/0、product/attr/base/list/0?,我们将路径改为/{attrType}/list/{catelogId},两个路径变量分别对应base规格参数,sale销售参数,将这两个参数抽取成常量;
/**
* 查询规格参数信息
*/
//product/attr/sale/list/0?
@RequestMapping("/{attrType}/list/{catelogId}")
public R list(@RequestParam Map<String, Object> params,
@PathVariable("catelogId") Long catelogId,
@PathVariable("attrType")String type){
PageUtils page = attrService.queryBaseAttrPage(params,catelogId,type);
return R.ok().put("page", page);
}
这里前端要的数据并不只实体里包含的这些,所以我们封装一个响应
vo传给前台。
@Override
public PageUtils queryBaseAttrPage(Map<String, Object> params, Long catelogId, String attrType) {
QueryWrapper<AttrEntity> queryWrapper = new QueryWrapper<AttrEntity>()
.eq("attr_type","base".equalsIgnoreCase(attrType) ?
ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode() : ProductConstant.AttrEnum.ATTR_TYPE_SALE.getCode());
//根据catelogId查询信息
if (catelogId != 0) {
queryWrapper.eq("catelog_id",catelogId);
}
String key = (String) params.get("key");
if (!StringUtils.isEmpty(key)) {
//attr_id attr_name
queryWrapper.and((wrapper) -> {
wrapper.eq("attr_id",key).or().like("attr_name",key);
});
}
IPage<AttrEntity> page = this.page(
new Query<AttrEntity>().getPage(params),
queryWrapper
);
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);
//设置分类和分组的名字
if ("base".equalsIgnoreCase(attrType)) {
AttrAttrgroupRelationEntity relationEntity =
relationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id",attrEntity.getAttrId()));
if (relationEntity != null && relationEntity.getAttrGroupId() != null) {
AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(relationEntity.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;
}
获取属性分组有关联的其他属性
/**
* 获取属性分组有关联的其他属性
* @param attrgroupId
* @return
*/
//product/attrgroup/{attrgroupId}/attr/relation
@GetMapping(value = "/{attrgroupId}/attr/relation")
public R attrRelation(@PathVariable("attrgroupId") Long attrgroupId) {
List<AttrEntity> entities = attrService.getRelationAttr(attrgroupId);
return R.ok().put("data",entities);
}
/**
* 根据分组id找到关联的所有属性
* @param attrgroupId
* @return
*/
@Override
public List<AttrEntity> getRelationAttr(Long attrgroupId) {
List<AttrAttrgroupRelationEntity> entities = relationDao.selectList
(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_group_id", attrgroupId));
List<Long> attrIds = entities.stream().map((attr) -> {
return attr.getAttrId();
}).collect(Collectors.toList());
//根据attrIds查找所有的属性信息
//Collection<AttrEntity> attrEntities = this.listByIds(attrIds);
//如果attrIds为空就直接返回一个null值出去
if (attrIds == null || attrIds.size() == 0) {
return null;
}
List<AttrEntity> attrEntityList = this.baseMapper.selectBatchIds(attrIds);
return attrEntityList;
}
查询属性详细
这里的回显数据不仅仅包含实体的,我们还需要回显分组和分类。
/**
* 信息
*/
@RequestMapping("/info/{attrId}")
public R info(@PathVariable("attrId") Long attrId){
AttrRespVo respVo = attrService.getAttrInfo(attrId);
return R.ok().put("attr", respVo);
}
这里要拿到分类信息,分组信息和路径。
@Override
public AttrRespVo getAttrInfo(Long attrId) {
//查询详细信息
AttrEntity attrEntity = this.getById(attrId);
//查询分组信息
AttrRespVo respVo = new AttrRespVo();
BeanUtils.copyProperties(attrEntity,respVo);
//判断是否是基本类型
if (attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()) {
//1、设置分组信息
AttrAttrgroupRelationEntity relationEntity = relationDao.selectOne
(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrId));
if (relationEntity != null) {
respVo.setAttrGroupId(relationEntity.getAttrGroupId());
//获取分组名称
AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(relationEntity.getAttrGroupId());
if (attrGroupEntity != null) {
respVo.setGroupName(attrGroupEntity.getAttrGroupName());
}
}
}
//2、设置分类信息
Long catelogId = attrEntity.getCatelogId();
Long[] catelogPath = categoryService.findCatelogPath(catelogId);
respVo.setCatelogPath(catelogPath);
CategoryEntity categoryEntity = categoryDao.selectById(catelogId);
if (categoryEntity != null) {
respVo.setCatelogName(categoryEntity.getName());
}
return respVo;
}
修改和删除属性
/**
* 修改
*/
@RequestMapping("/update")
public R update(@RequestBody AttrVo attr){
attrService.updateAttrById(attr);
return R.ok();
}
修改分组和分类,查询对应表时,结果大于0 即为修改,否则则为新增。
@Transactional(rollbackFor = Exception.class)
@Override
public void updateAttrById(AttrVo attr) {
AttrEntity attrEntity = new AttrEntity();
BeanUtils.copyProperties(attr,attrEntity);
this.updateById(attrEntity);
if (attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()) {
//1、修改分组关联
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
relationEntity.setAttrGroupId(attr.getAttrGroupId());
relationEntity.setAttrId(attr.getAttrId());
Integer count = relationDao.selectCount(new QueryWrapper<AttrAttrgroupRelationEntity>()
.eq("attr_id", attr.getAttrId()));
if (count > 0) {
relationDao.update(relationEntity,
new UpdateWrapper<AttrAttrgroupRelationEntity>().eq("attr_id",attr.getAttrId()));
} else {
relationDao.insert(relationEntity);
}
}
}
属性组下关联新增新属性
我们想要新增没有被关联的属性,那么就应该先得到未被关联的属性。
获取属性分组没有关联的其他属性
/**
* 获取属性分组没有关联的其他属性
*/
@GetMapping(value = "/{attrgroupId}/noattr/relation")
public R attrNoattrRelation(@RequestParam Map<String, Object> params,
@PathVariable("attrgroupId") Long attrgroupId) {
PageUtils page = attrService.getNoRelationAttr(params,attrgroupId);
return R.ok().put("page",page);
}
我们这里查询到的属性必须符合以下规则:
-
当前分组只能关联自己所属的分类里面的所有属性
-
当前分组只能关联别的分组没有引用的属性
/**
* 获取当前分组没有被关联的所有属性
* @param params
* @param attrgroupId
* @return
*/
@Override
public PageUtils getNoRelationAttr(Map<String, Object> params, Long attrgroupId) {
//1、当前分组只能关联自己所属的分类里面的所有属性
AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrgroupId);
//获取当前分类的id
Long catelogId = attrGroupEntity.getCatelogId();
//2、当前分组只能关联别的分组没有引用的属性
//2.1)、当前分类下的其它分组
List<AttrGroupEntity> groupEntities = attrGroupDao.selectList(new QueryWrapper<AttrGroupEntity>()
.eq("catelog_id", catelogId));
//获取到所有的attrGroupId
List<Long> collect = groupEntities.stream().map((item) -> {
return item.getAttrGroupId();
}).collect(Collectors.toList());
//2.2)、这些分组关联的属性
List<AttrAttrgroupRelationEntity> groupId = relationDao.selectList
(new QueryWrapper<AttrAttrgroupRelationEntity>().in("attr_group_id", collect));
List<Long> attrIds = groupId.stream().map((item) -> {
return item.getAttrId();
}).collect(Collectors.toList());
//2.3)、从当前分类的所有属性移除这些属性
QueryWrapper<AttrEntity> queryWrapper = new QueryWrapper<AttrEntity>()
.eq("catelog_id", catelogId).eq("attr_type",ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode());
if (attrIds != null && attrIds.size() > 0) {
queryWrapper.notIn("attr_id", attrIds);
}
//判断是否有参数进行模糊查询
String key = (String) params.get("key");
if (!StringUtils.isEmpty(key)) {
queryWrapper.and((w) -> {
w.eq("attr_id",key).or().like("attr_name",key);
});
}
IPage<AttrEntity> page = this.page(new Query<AttrEntity>().getPage(params), queryWrapper);
PageUtils pageUtils = new PageUtils(page);
return pageUtils;
}
新增分组与属性关联
///product/attrgroup/attr/relation
@PostMapping(value = "/attr/relation")
public R addRelation(@RequestBody List<AttrGroupRelationVo> vos) {
attrAttrgroupRelationService.saveBatch(vos);
return R.ok();
}
/**
* 批量添加属性与分组关联关系
* @param vos
*/
@Override
public void saveBatch(List<AttrGroupRelationVo> vos) {
List<AttrAttrgroupRelationEntity> collect = vos.stream().map((item) -> {
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
BeanUtils.copyProperties(item, relationEntity);
return relationEntity;
}).collect(Collectors.toList());
this.saveBatch(collect);
}
新增属性
AttrEntity实体类中并没有这个attrGroupId分组属性,我们可以像以前一样用下面这种注解来实现,但是这样实现并不优雅,我们采用另一种方法来实现,我们自己编写一个vo来收集前端传递过来的数据。
这样写并不规范
@TableField(exist = false)
private Long attrGroupId;
我们新增一个AttrVo类
@Data
public class AttrVo {
/**
* 属性id
*/
private Long attrId;
/**
* 属性名
*/
private String attrName;
/**
* 是否需要检索[0-不需要,1-需要]
*/
private Integer searchType;
/**
* 属性图标
*/
private String icon;
/**
* 可选值列表[用逗号分隔]
*/
private String valueSelect;
/**
* 属性类型[0-销售属性,1-基本属性,2-既是销售属性又是基本属性]
*/
private Integer attrType;
/**
* 启用状态[0 - 禁用,1 - 启用]
*/
private Long enable;
/**
* 所属分类
*/
private Long catelogId;
/**
* 快速展示【是否展示在介绍上;0-否 1-是】,在sku中仍然可以调整
*/
private Integer showDesc;
private Long attrGroupId;
}
/**
* 保存
*/
@RequestMapping("/save")
public R save(@RequestBody AttrVo attr){
attrService.saveAttr(attr);
return R.ok();
}
这里新增操作不仅仅新增一个Attr,还要维护属性分组
/**
* 新增Attr
* @param attr
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void saveAttr(AttrVo attr) {
AttrEntity attrEntity = new AttrEntity();
BeanUtils.copyProperties(attr,attrEntity);
//1、保存基本数据
this.save(attrEntity);
//2、保存关联关系
//判断类型,如果是基本属性就设置分组id
if (attr.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode() && attr.getAttrGroupId() != null) {
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
relationEntity.setAttrGroupId(attr.getAttrGroupId());
relationEntity.setAttrId(attrEntity.getAttrId());
relationDao.insert(relationEntity);
}
}
package com.hong.common.constant;
/**
* @Description: 商品常量属性
**/
public class ProductConstant {
public enum AttrEnum {
ATTR_TYPE_BASE(1,"基本属性"),
ATTR_TYPE_SALE(0,"销售属性");
private int code;
private String msg;
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
AttrEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
public enum ProductStatusEnum {
NEW_SPU(0,"新建"),
SPU_UP(1,"商品上架"),
SPU_DOWN(2,"商品下架"),
;
private int code;
private String msg;
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
ProductStatusEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
}
Object 划分
1.PO(persistant object) 持久对象
PO 就是对应数据库中某个表中的一条记录,多个记录可以用 PO 的集合。 PO 中应该不包含任何对数据库的操作。
2.DO(Domain Object)领域对象
就是从现实世界中抽象出来的有形或无形的业务实体。
3.TO(Transfer Object) ,数据传输对象
不同的应用程序之间传输的对象
4.DTO(Data Transfer Object)数据传输对象
这个概念来源于 J2EE 的设计模式,原来的目的是为了 EJB 的分布式应用提供粗粒度的数据实体,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载,但在这里,泛指用于展示层与服务层之间的数据传输对象。
5.VO(value object) 值对象
通常用于业务层之间的数据传递,和 PO 一样也是仅仅包含数据而已。但应是抽象出的业务对象 , 可以和表对应 , 也可以不 , 这根据业务的需要 。用 new 关键字创建,由GC 回收的。
View object:视图对象;接受页面传递来的数据,封装对象将业务处理完成的对象,封装成页面要用的数据
6.BO(business object) 业务对象
从业务模型的角度看 , 见 UML 元件领域模型中的领域对象。封装业务逻辑的 java 对象 , 通过调用 DAO 方法 , 结合 PO,VO 进行业务操作。business object: 业务对象 主要作用是把业务逻辑封装为一个对象。这个对象可以包括一个或多个其它的对象。 比如一个简历,有教育经历、工作经历、社会关系等等。 我们可以把教育经历对应一个 PO ,工作经历对应一个 PO ,社会关系对应一个 PO 。 建立一个对应简历的 BO 对象处理简历,每个 BO 包含这些 PO 。 这样处理业务逻辑时,我们就可以针对 BO 去处理。
7.POJO(plain ordinary java object) 简单无规则 java 对象
传统意义的 java 对象。就是说在一些 Object/Relation Mapping 工具中,能够做到维护数据库表记录的 persisent object 完全是一个符合 Java Bean 规范的纯 Java 对象,没有增加别的属性和方法。我的理解就是最基本的 java Bean ,只有属性字段及 setter 和 getter方法!。
POJO 是 DO/DTO/BO/VO 的统称。
8.DAO(data access object) 数据访问对象
是一个 sun 的一个标准 j2ee 设计模式, 这个模式中有个接口就是 DAO ,它负持久层的操作。为业务层提供接口。此对象用于访问数据库。通常和 PO 结合使用, DAO 中包含了各种数据库的操作方法。通过它的方法 , 结合 PO 对数据库进行相关的操作。夹在业务逻辑与数据库资源中间。配合 VO, 提供数据库的 CRUD 操作.