五、微服务分布式电商系统--商品服务API接口

【摘要】商品服务API接口~

前言

商品服务-API-三级分类-查询-递归树形结构数据获取

所有的分类数据从数据库读取,对应数据库pms_category。

首先在数据库准备数据,然后再我们zheli-product中找到和分类有关的代码。
Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
@RequestMapping("product/category")
public class 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("data",entities);
}

//...
}

Service

1
2
3
4
5
6
7
/**
* 商品三级分类
*/
public interface CategoryService extends IService<CategoryEntity> {

List<CategoryEntity> listWithTree();
}

ServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

// @Autowired
// CategoryDao categoryDao;

@Override
public PageUtils queryPage(Map<String, Object> params) {
IPage<CategoryEntity> page = this.page(
new Query<CategoryEntity>().getPage(params),
new QueryWrapper<CategoryEntity>()
);

return new PageUtils(page);
}

@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(getChildrens(menu, entities));
return menu;
}).sorted((menu1, menu2) -> {
return (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort());
}).collect(Collectors.toList());

return level1Menus;
}

//递归查找所有菜单的子菜单
private List<CategoryEntity> getChildrens(CategoryEntity root, List<CategoryEntity> all) {
List<CategoryEntity> children = all.stream().filter(categoryEntity -> {
return categoryEntity.getParentCid() == root.getCatId();
}).map(categoryEntity -> {
//1.找到子菜单
categoryEntity.setChildren(getChildrens(categoryEntity, all));
return categoryEntity;
}).sorted((menu1, menu2) -> {
//2.菜单的排序
return (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort());
}).collect(Collectors.toList());
return children;
}
}

测试运行。

商品服务-API-三级分类-配置网关路由和路径重写

启动人人快速开发平台。在菜单管理新建商品系统目录,并在商品系统目录下新建分类维护菜单。

在前端项目中新建category.vue文件。

使用ElementUI的树形控件:https://element.eleme.cn/#/zh-CN/component/tree

之后在category.vue编写前端代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<template>
<el-tree :data="data" :props="defaultProps" @node-click="handleNodeClick"></el-tree>
</template>

<script>
export default {
components: {},
data() {
return {
data: [],
defaultProps: {
children: "children",
label: "label"
}
};
},
methods: {
handleNodeClick(data) {
console.log(data);
},
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get"
}).then(data=>{
console.log("成功获取到菜单数据...",data)
});
}
},
created() {
this.getMenus();
}
};
</script>
<style lang='scss' scoped>
</style>

页面访问发现没有数据,打开浏览器控制台发现404。发现这里是对http://localhost:8080/renren-fast/product/category/list/tree进行请求获取数据。实际上,需要从http://localhost:8000/product/category/list/tree获取数据。

其实如果只是改变http://localhost:8080/renren-fast/这部分基准路径,那么后面分布式情况下需要给8001``8002``...发送请求就还会出现问题。所以找到前端基准路径设置成给网关发请求。

上面的设置成功之后,发现验证码请求不到。原因是因为验证码功能是renren-fast后台提供的,现在所有请求都会去网关服务找。要让网关可以发现这个验证码服务,就需要把renren-fast后台服务注册到nacos注册中心中。1.引入zheli-common。2.在application.yml中配置服务名字。 3.配置注册中心地址。4.开启服务的注册发现@EnableDiscoveryClient。5.重启renren-fast。

之后配置网关路由和路径重写。

重新登录遇到跨域问题。

商品服务-API-三级分类-网关统一配置跨域





这里选择第二种配置请求允许跨域。

配置完成后,重启网关服务和renren-fast服务。然后在页面重写登录。

商品服务-API-三级分类-查询-树形展示三级分类数据

进入分类维护页面的获取数据请求仍然是404。所以需要在网关服务中添加商品服务的网关路由和路径重写。


配置之后重启访问发现可以成功获取数据了。

修改前端代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<template>
<el-tree :data="menus" :props="defaultProps" @node-click="handleNodeClick"></el-tree>
</template>

<script>
export default {
components: {},
data() {
return {
menus: [],
defaultProps: {
children: "children",
label: "name"
}
};
},
methods: {
handleNodeClick(data) {
console.log(data);
},
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get"
}).then(({data})=>{
console.log("成功获取到菜单数据...",data.data)
this.menus = data.data;
});
}
},
created() {
this.getMenus();
}
};
</script>
<style lang='scss' scoped>
</style>

效果展示:

商品服务-API-三级分类-删除-页面效果

使用 scoped slot。

没有子菜单可以删除,一级、二级菜单可以添加。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<template>
<el-tree :data="menus" :props="defaultProps" @node-click="handleNodeClick" :expand-on-click-node="false" show-checkbox node-key="catId">
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button v-if="node.level<=2" type="text" size="mini" @click="() => append(data)">Append</el-button>
<el-button v-if="node.childNodes.length==0" type="text" size="mini" @click="() => remove(node, data)">Delete</el-button>
</span>
</span>
</el-tree>
</template>

<script>
export default {
components: {},
data() {
return {
menus: [],
defaultProps: {
children: "children",
label: "name"
}
};
},
methods: {
handleNodeClick(data) {
console.log(data);
},
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get"
}).then(({ data }) => {
console.log("成功获取到菜单数据...", data.data);
this.menus = data.data;
});
},
append(data) {
console.log("append", data);
},
remove(node, data) {
console.log("remove", node, data);
}
},
created() {
this.getMenus();
},
};
</script>
<style lang='scss' scoped>
</style>

商品服务-API-三级分类-删除-逻辑删除

下载安装Postman https://www.postman.com/downloads/
测试删除接口。

使用mybatis逻辑删除。

商品服务-API-三级分类-删除-逻辑删除

商品服务-API-三级分类-删除-删除效果细化

商品服务-API-三级分类-新增-新增效果完成

商品服务-API-三级分类-修改-基本修改效果完成

商品服务-API-三级分类-修改-拖拽效果

商品服务-API-三级分类-修改-拖拽数据收集

商品服务-API-三级分类-修改-拖拽功能完成

商品服务-API-三级分类-修改-批量拖拽效果

商品服务-API-三级分类-删除-批量删除&小结

商品服务-API-品牌管理-使用逆向工程的前后端代码

商品服务-API-品牌管理-效果优化与快速显示开关

商品服务-API-品牌管理-云存储开通与使用


SpringCloud-Alibaba-OSS

简介
对象存储服务(Object Storage Service,OSS)是一种海量、安全、低成本、高可靠的云存储服务,适合存放任意类型的文件。容量和处理能力弹性扩展,多钟存储类型供选择,全面优化存储成本。

这个是对象存储的页面。

推荐一个项目创建一个Bucket。上传一张图片就可以外网访问。

普通上传方式

服务端签名后直传

商品服务-API-品牌管理-OSS整合测试

使用代码给阿里云OSS中的zheli-mall上传图片。

阿里云主账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM账号进行API访问或日常运维,请登录 https://ram.console.aliyun.com 创建RAM账号。
创建子AccessKey并设置权限。

首先我们创建一个工程,这个工程专门用来集成第三方应用的工程,我起名叫zhelimall-third-party。
1.在pom文件中加入依赖。

1
2
3
4
5
<!--阿里云存储-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alicloud-oss</artifactId>
</dependency>

2.配置key,endpoint相关信息

1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
alicloud:
access-key: LTAI4GAGUKKkE4agXXXXXXxX
secret-key: ggruUCv4r9juD8QkmBXXXXXxxXXXX
oss:
endpoint: oss-cn-shanghai.aliyuncs.com
bucket: zheli-mall
application:
name: zheli-third-party

3.使用OSSClient 进行相关操作

商品服务-API-品牌管理-OSS获取服务端签名

编写代码:OssController.java
配置网关:
http://localhost:30000/oss/policy
http://localhost:88/api/thirdparty/oss/policy

商品服务-API-品牌管理-OSS前后联调测试上传

Element-UI中的

商品服务-API-品牌管理-表单校验&自定义校验器

前端校验方法

商品服务-API-品牌管理-JSR303数据校验

后端校验方法
BrandEntity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/** 三.JSR303
* (一).给Bean添加校验注解 @Email @NotNull在javax.validation.constraints包下
* @NotBlank
* private String name;
* (二).开启校验功能@Valid
* public R save(@Valid @RequestBody BrandEntity brand)
* 效果:校验错误以后会有默认的响应;
* (三).给校验的Bean后紧跟一个BindingResult,就可以获取到校验的结果
*/
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;

/**
* 品牌id
*/
@NotNull(message = "修改必须指定品牌id")
@Null(message = "新增不能指定id")
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名必须提交")
private String name;
/**
* 品牌logo地址
*/
@NotBlank
@URL(message = "logo必须是一个合法的url地址")
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
@NotNull
//@ListValue(vals={0,1})
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty
@Pattern(regexp = "/^[a-zA-z]$/",message = "检索首字母必须是一个字母")
private String firstLetter;
/**
* 排序
*/
@NotNull
@Min(value = 0,message = "排序必须大于等于0")
private Integer sort;

}

BrandController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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) -> {
// FileError获取到错误提示
String defaultMessage = item.getDefaultMessage();
// 获取错误的属性的名字
String field = item.getField();
map.put("field", defaultMessage);
});
return R.error(400, "提交的数据不合法").put("data", map);
} else {
brandService.save(brand);
return R.ok();
}
}

使用PostMan测试。

商品服务-API-品牌管理-统一异常处理

@ResponseBody + @ControllerAdvice
系统错误码
错误码和错误信息定义类
1.错误码定义规则为5位数
2.前两位表示业务场景,最后三位表示错误码。例如:10001。10:通用。 001:系统未知异常。
3.维护错误码后需要维护错误描述,将他们定义为枚举形式。
错误码列表:
10:通用
001:参数格式校验
11:商品
12:订单
13:购物车
14:物流

错误码BizCodeEnum.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public enum BizCodeEnum {
UNKNOW_EXCEPTION(10000,"系统未知异常"),
VAILD_EXCEPTION(10001,"参数格式校验失败");

private int code;
private String msg;

BizCodeEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}

public int getCode() {
return code;
}

public String getMsg() {
return msg;
}
}

集中处理所有异常ZhelimallExceptionControllerAdvice.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Slf4j
@RestControllerAdvice(basePackages = "com.aiz.zhelimall.product.controller")//相当于@ResponseBody + @ControllerAdvice
public class ZhelimallExceptionControllerAdvice {
@ResponseBody
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleVaildException(MethodArgumentNotValidException e){
log.error("数据校验出现问题:{},异常类型:{}",e.getMessage(),e.getClass());
BindingResult bindingResult = e.getBindingResult();
Map<String,String> errorMap = new HashMap<>();
bindingResult.getFieldErrors().forEach((fieldError -> {
errorMap.put(fieldError.getField(),fieldError.getDefaultMessage());
}));
return R.error(BizCodeEnum.VAILD_EXCEPTION.getCode(),BizCodeEnum.VAILD_EXCEPTION.getMsg()).put("data",errorMap);
}

@ExceptionHandler(value = Throwable.class)
public R handleException(){

return R.error();
}
}

商品服务-API-品牌管理-JSR303分组校验

分组校验(多场景的复杂校验)
1.@NotBlank(message = “品牌名必须提交”,groups = {AddGroup.class,UpdateGroup.class})
给校验注解标注什么情况进行校验
2.@Validated(AddGroup.class)
3.默认没有指定分组的校验注解@Notblank,在分组校验情况@Validated(AddGroup.class)下不生效,只会在@Validated生效
BrandEntity.java

1
2
3
4
5
6
/**
* 检索首字母
*/
@NotEmpty(groups={AddGroup.class})
@Pattern(regexp="^[a-zA-Z]$",message = "检索首字母必须是一个字母",groups={AddGroup.class,UpdateGroup.class})
private String firstLetter;
1
2
3
4
5
6
7
8
9
/**
* 修改
*/
@RequestMapping("/update")
public R update(@Validated(UpdateGroup.class)@RequestBody BrandEntity brand) {
brandService.updateById(brand);

return R.ok();
}

商品服务-API-品牌管理-JSR303自定义校验注解

导入javax依赖

1
2
3
4
5
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>

自定义校验
(一).编写一个自定义的校验注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Documented
@Constraint(
validatedBy = {ListValueConstraintValidator.class}//可以指定多个不同的校验器,适配不同类型的校验
)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ListValue {
String message() default "{com.aiz.common.valid.ListValue.message}";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

int[] vals() default {};
}

配置文件ValidationMessages.properties

1
com.aiz.common.valid.ListValue.message = 必须提交指定的值

(二).编写一个自定义的校验器 ConstraintValidator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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);
}
}

(三).关联自定义的校验器和自定义的校验注解

1
2
3
@Constraint(
validatedBy = {ListValueConstraintValidator.class}//可以指定多个不同的校验器,适配不同类型的校验
)

使用
BrandEntity.java

1
2
3
4
5
6
/**
* 显示状态[0-不显示;1-显示]
*/
@NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})
@ListValue(vals={0,1},groups = {AddGroup.class, UpdateStatusGroup.class})
private Integer showStatus;

BrandController.java

1
2
3
4
5
6
7
8
9
/**
* 修改状态
*/
@RequestMapping("/update/status")
public R updateStatus(@Validated(UpdateStatusGroup.class)@RequestBody BrandEntity brand) {
brandService.updateById(brand);

return R.ok();
}

检查前端接口调用。

商品服务-概念-SPU&SKU&规格参数&销售属性

SPU和SKU

SPU:Standard Product Unit(标准化产品单元)
是商品信息聚合的最小单元,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。
SKU:Stock Keeping Unit(库存量单元)
即库存进出计量的基本单元,可以是以件、盒、托盘等为单位。SKU这对大型连锁超市DC(配送中心)物流管理的一个必要的方法。现在已经被引申为产品统一编号的简称,每种产品均对应有唯一的SKU号。
ex:iphone12是SPU、MI10的SPU
iphone12 64G 黑曜石 是SKU
MI10+64G+黑色 是SKU

基本属性【规格参数】与销售属性

每个分类下的商品共享规格参数,与销售属性。只是有些商品不一定要用这个分类下全部属性。

  • 属性是以三级分类组织起来的
  • 规格参数中有些是可以提供检索的
  • 规格参数也是基本属性,他们具有自己的价值
  • 属性的分组也是以三级分类组织起来的
  • 属性名确定的,但是值是每一个商品不同来决定的




商品服务-API-属性分组-前端组件抽取&父子组件交互

父子组件传递数据
子组件给父组件传递数据,事件机制。子组件给父组件发送一个事件,携带上数据。
this.$emit(“事件名称”, 需要携带的数据…);

抽取子组件../common/category.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
<el-tree :data="menus" :props="defaultProps" node-key="catId" ref="menuTree" @node-click="nodeclick"></el-tree>
</template>
<script>
export default {
data() {
return {
menus: [],
expandedKey: [],
defaultProps: {
children: "children",
label: "name"
}
};
},
methods: {
//共三个参数,依次为:传递给 data 属性的数组中该节点所对应的对象、节点对应的 Node、节点组件本身。
nodeclick(data, node, component) {
console.log("子组件category的节点被点击", data, node, component);
//向父组件发送事件;
this.$emit("tree-node-click", data, node, component);
}
}
};
</script>
<style scoped>
</style>

父组件/product/attrgroup.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div>
<category @tree-node-click="treenodeclick"></category>
</div>
</template>
<script>
import Category from "../common/category";
export default {
methods: {
//感知树节点被点击
treenodeclick(data, node, component) {
console.log("attrgroup感知到category的节点被点击",data, node, component);
console.log("刚才被点击的菜单id:",data.catId);
if (node.level == 3) {
this.catId = data.catId;
this.getDataList(); //重新查询
}
},
}

</script>}
<style scoped>
</style>

商品服务-API-属性分组-获取分类属性分组

接口文档:https://easydoc.xyz/s/78237135/ZUqEdvA4/hKJTcbfd

AttrGroupController.java

1
2
3
4
5
6
7
@RequestMapping("/list/{catelogId}")
public R list(@RequestParam Map<String, Object> params,
@PathVariable("catelogId") Long catelogId){
PageUtils page = attrGroupService.queryPage(params,catelogId);

return R.ok().put("page", page);
}

AttrGroupServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public PageUtils queryPage(Map<String, Object> params, Long catelogId) {
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 (attr_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);
}
}

商品服务-API-属性分组-分组新增&级联选择器

商品服务-API-属性分组-分组修改&级联选择器回显

Element-UI中Cascader级联选择器的使用

商品服务-API-品牌管理-品牌分类关联与级联更新

MyBatis Plus分页插件使用 https://mp.baomidou.com/guide/page.html
com.aiz.zhelimall.product.config.MyBatisConfig.java

一个品牌会有多个分类,一个分类也会包含多个品牌。分类&品牌是多对多的关系。

查询接口:product/categorybrandrelation/catelog/list?brandId=4
添加接口:product/categorybrandrelation/save
在电商系统中对于大表尽量避免关联查询,所以这边存放brand_name和catelog_name冗余字段。

因为有冗余字段设计,所以在业务代码中修改品牌名称还需要同时修改关联表中的字段。

使用QueryWrapper方式:

BrandController.java

1
2
3
4
5
6
7
@RequestMapping("/update")
public R update(@Validated(UpdateGroup.class)@RequestBody BrandEntity brand) {
//brandService.updateById(brand);
//这里不能只更新品牌表 因为其他表包含品牌表里面的字段(字段冗余设计造成的)
brandService.updateDetail(brand);
return R.ok();
}

BrandServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
@Transactional
@Override
public void updateDetail(BrandEntity brand) {
//保证冗余字段的数据一致
this.updateById(brand);
if(!StringUtils.isEmpty(brand.getName())){
//同步更新其他关联表中的数据
categoryBrandRelationService.updateBrand(brand.getBrandId(),brand.getName());
// TODO 更新其他关联
}
}

CategoryBrandRelationServiceImpl.java

1
2
3
4
5
6
public void updateBrand(Long brandId, String name) {
CategoryBrandRelationEntity relationEntity = new CategoryBrandRelationEntity();
relationEntity.setBrandId(brandId);
relationEntity.setBrandName(name);
this.update(relationEntity,new UpdateWrapper<CategoryBrandRelationEntity>().eq("brand_id",brandId));
}

使用自定义SQL语句方式:

CategoryController.java

1
2
3
4
5
@RequestMapping("/update")
public R update(@RequestBody CategoryEntity category){
categoryService.updateCascade(category);
return R.ok();
}

CategoryServiceImpl.java 这里包含事务控制。

1
2
3
4
5
6
7
8
9
10
/**
* 级联更新所有关联的数据
* @param category
*/
@Transactional
@Override
public void updateCascade(CategoryEntity category) {
this.updateById(category);
categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
}

CategoryBrandRelationDao.java

1
void updateCategory(@Param("catId") Long catId, @Param("name") String name);

CategoryBrandRelationDao.xml

1
2
3
<update id="updateCategory">
UPDATE `pms_category_brand_relation` SET catelog_name=#{name} WHERE catelog_id=#{catId}
</update>

商品服务-API-平台属性-规格参数新增与VO

Object划分

  • PO(Persistant Object)持久对象
    PO就是对应数据库中某个表中的一条记录,多个记录可以用PO的集合。PO中应该不包含任何对数据库的操作。
  • DO(Domain Object)领域对象
    就是从现实世界中抽象出来的有形或无形的业务实体。
  • TO(Transfer Object)数据传输对象
    不同应用程序之间传输的对象。
  • DTO(Data Transfer Object)数据传输对象
    这个概念来源于J2EE的设计模式,原来的目的是为了EJB的分布式应用提供粗颗粒度的数据实体,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载,但在这里,泛指用于展示层与服务层之间的数据传输对象。
  • VO(Value Object)值对象
    通常用于业务层之间的数据传递,和PO一样也是仅仅包含数据而已。但应是抽象出的业务对象,可以和表对应,也可以不。这根据业务的需要。用new关键字创建,由GC回收的。
    View Object:视图对象。接收页面传递来的数据,封装对象。将业务处理完的对象封装成页面要用的数据。
  • BO(Business Object)业务对象
    从业务模型的角度看,见UML元件领域模型中的领域对象。封装业务逻辑的Java对象,通过调用DAO方法,结合PO/VO进行业务操作。business object:业务对象 主要作用是把业务逻辑封装为一个对象。这个对象可以包括一个或多个其他的对象。比如一个简历,有教育经历、工作经历、社会关系等等。我们可以把教育经历对应一个PO,工作经历对应一个PO,社会关系对应一个PO。建立一个对应简历的BO对象处理简历,每个BO包含这些PO。这样处理业务逻辑时,我们就可以针对BO去处理。
  • POJO(plain ordinary java object)简单无规则Java对象
    传统意义的java对象。就是说在一些Object/Relation Mapping工具中,能够做到维护数据库表记录的persisent object完全是一个符合Java Bean规范的纯Java对象,没有增加别的属性和方法。我的理解就是最基本的Java Bean,只有属性字段以及setter和getter方法。
    POJO是DO/DTO/BO/VO的统称。
  • DAO(Data Accsee Object)数据访问对象
    是一个sun的一个标准j2ee设计模式,这个模式中有个接口就是DAO,它负责持久层的操作。为业务层通过接口。此对象用于访问数据库。通过和PO结合使用,DAO中包含了各种数据库的操作方法。通过它的方法,结合PO对数据库进行相关的操作。夹在业务逻辑与数据库资源终究,配合VO,提供数据库的CRUD操作。

规格参数新增

因为在接受页面传来的属性数据的时候,会把属性的分组信息也带过来。所以新建AttrVo.java用于存放属性信息和属性分组attrGroupId信息。
AttrVo.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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;
/**
* 属性分组id
*/
private Long attrGroupId;
}

页面提交信息时,会涉及到属性信息和属性分组信息两张数据库表。
AttrController.java

1
2
3
4
5
@RequestMapping("/save")
public R save(@RequestBody AttrVo attr){
attrService.saveAttr(attr);
return R.ok();
}

AttrServiceImpl.java在给attrEntity设置属性值的时候,可以(1).一个一个set。(2).使用spring的BeanUtils.copyProperties(),需要注意的是这个浅拷贝,也就是说如果存在子对象且子对象还需要改变就一定不能使用这个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Transactional
@Override
public void saveAttr(AttrVo attr) {
AttrEntity attrEntity = new AttrEntity();
// attrEntity.setAttrName(attr.getAttrName());
// ...
BeanUtils.copyProperties(attr,attrEntity);
//1.保存基本数据
this.save(attrEntity);
//2.保存关联关系
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
relationEntity.setAttrGroupId(attr.getAttrGroupId());
relationEntity.setAttrId(attrEntity.getAttrId());
relationDao.insert(relationEntity);
}

效果。

属性表

属性分组关系表

商品服务-API-平台属性-规格参数列表


这个页面展示的是属性相关信息,数据来源于【属性表pms_attr】。但是【属性表pms_attr】中并没有【所属分类】和【所属分组】的信息。(1).【属性表pms_attr】中有【分类ID:catelog_id】,就是拿到分类ID再去分类表找到分类名称。(2).需要从【属性&属性分组表pms_attr_attrgroup_relation】中获取【分组ID】【分组名称attr_group_name】。

1
2
3
4
5
select attr.*,category.`name` as category_name,attr_group.attr_group_name as group_name
from pms_attr attr
inner join pms_category category on attr.catelog_id=category.cat_id
left join pms_attr_attrgroup_relation relation on relation.attr_id=attr.attr_id
left join pms_attr_group attr_group on relation.attr_group_id = attr_group.attr_group_id;

新建响应数据AttrRespVo.java

1
2
3
4
5
6
7
8
9
10
11
public class AttrRespVo extends AttrVo{
/**
* 所属分类名字
*/
private String catelogName;

/**
* 所属分组名字
*/
private String groupName;
}

AttrController.java

1
2
3
4
5
6
@GetMapping("/base/list/{catelogId}")
public R baseAttrList(@RequestParam Map<String, Object> params,
@PathVariable("catelogId") Long catelogId){
PageUtils page = attrService.queryBaseAttrPage(params,catelogId);
return R.ok().put("page", page);
}

AttrServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Override
public PageUtils queryBaseAttrPage(Map<String, Object> params, Long catelogId) {
QueryWrapper<AttrEntity> queryWrapper = new QueryWrapper<>();
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> attrRespVos = records.stream().map(attrEntity -> {
AttrRespVo attrRespVo = new AttrRespVo();
BeanUtils.copyProperties(attrEntity, attrRespVo);
//设置分类和分组的名字
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(attrRespVos);
return pageUtils;
}

商品服务-API-平台属性-规格修改


在回显数据的时候三级分类路径缺少。所以在AttrRespVo中添加属性catelogPath。

1
2
3
4
public class AttrRespVo extends AttrVo{
// ...
private Long[] catelogPath;
}

AttrController.java

1
2
3
4
5
@RequestMapping("/info/{attrId}")
public R info(@PathVariable("attrId") Long attrId){
AttrVo attrVo = attrService.getAttrInfo(attrId);
return R.ok().put("attr", attrVo);
}

AttrServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Override
public AttrVo getAttrInfo(Long attrId) {
AttrRespVo attrRespVo = new AttrRespVo();
AttrEntity attrEntity = this.getById(attrId);
BeanUtils.copyProperties(attrEntity,attrRespVo);

if(attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()){
//1.设置分组信息
AttrAttrgroupRelationEntity attrgroupRelation = relationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrEntity.getAttrId()));
if(attrgroupRelation!=null){
attrRespVo.setAttrGroupId(attrgroupRelation.getAttrGroupId());
AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrgroupRelation.getAttrGroupId());
if(attrGroupEntity!=null){
attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName());
}
}
}
//2.设置分类信息
Long catelogId = attrEntity.getCatelogId();
Long[] catelogPath = categoryService.findCatelogPath(catelogId);
attrRespVo.setCatelogPath(catelogPath);
CategoryEntity categoryEntity = categoryDao.selectById(catelogId);
if(categoryEntity!=null){
attrRespVo.setCatelogName(categoryEntity.getName());
}

return attrRespVo;
}

商品服务-API-平台属性-销售属性维护

AttrController.java

1
2
3
4
5
6
7
@GetMapping("/{attrType}/list/{catelogId}")
public R baseAttrList(@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);
}

商品服务-API-平台属性-查询分组关联属性&删除关联

商品服务-API-平台属性-查询分组未关联的属性

商品服务-API-新增商品-调试会员等级相关接口

商品服务-API-新增商品-调试会员等级相关接口

商品服务-API-新增商品-获取分类关联的品牌

商品服务-API-新增商品-获取分类下所有分组以及属性

商品服务-API-新增商品-商品新增vo抽取

对于前端返回的json,可以使用bejson.com转化为JavaBean。下载代码拷贝到项目对应位置。

商品服务-API-新增商品-商品新增业务流程分析

SpuInfoServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Transactional
@Override
public void saveSpuInfo(SpuSaveVo vo) {
//1.保存spu基本信息 pms_spu_info

//2.保存spu的描述图片 pms_spu_info_desc

//3.保存spu的图片集 pms_sku_images

//4.保存spu的规格参数 pms_product_attr_value

//5.保存spu的积分信息 `zhelimall_sms`.sms_spu_bounds
//6.保存当前spu对应的sku信息
//6.1 sku基本信息 pms_sku_info
//6.2 sku的图片信息 pms_sku_images
//6.3 sku的销售属性信息 pms_sku_sale_attr_value
//6.4 sku的优惠信息、满减等信息 `zhelimall_sms`.sms_sku_ladder/sms_sku_full_reduction/sms_member_price
}

商品服务-API-新增商品-保存SPU基本信息

商品服务-API-新增商品-保存SKU基本信息

商品服务-API-新增商品-调用远程服务保存优惠等信息

在远程调用的时候,A服务想要给B服务发送数据,Spring Cloud会把数据封装成json发送过去。所以引申出新的领域模型TO。
还因为这个TO对象A服务和B服务都要使用,所以建议把这个TO对象放在common模块中。

注:Mybatis-Plus会自动插入一个ID到实体类,导致设置的id失效。所以需要在@TableId指定type的类型。
可参考链接

1
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

SpuInfoDescEntity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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;

}

介绍一下Mybatis-Plus几种生成ID类型枚举类的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Getter
public enum IdType {
/**
* 数据库ID自增
*/
AUTO(0),
/**
* 该类型为未设置主键类型(将跟随全局)
*/
NONE(1),
/**
* 用户输入ID
* <p>该类型可以通过自己注册自动填充插件进行填充</p>
*/
INPUT(2),

/* 以下3种类型、只有当插入对象ID 为空,才自动填充。 */
/**
* 全局唯一ID (idWorker)
*/
ID_WORKER(3),
/**
* 全局唯一ID (UUID)
*/
UUID(4),
/**
* 字符串全局唯一ID (idWorker 的字符串表示)
*/
ID_WORKER_STR(5);

private final int key;

IdType(int key) {
this.key = key;
}
}

商品服务-API-新增商品-商品保存其他问题处理

商品服务-API-商品管理-SPU检索

Spring配置时间格式

1
2
3
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss

商品服务-API-商品管理-SKU检索

结束语

评论