前言
做谷粒商城,我并不关注业务,但是为了有一个比较完整的体验,我需要把相关业务也写一遍,笔记可能会只做后端代码部分,需要其他更详细的笔记,不建议看此笔记。
完成谷粒商城中的以下内容:
- 三级分类
- 品牌管理
- SKU & SPU(代码省略)
- 基础篇的其他业务逻辑(代码省略)
- 分布式基础篇总结
三级分类实现
后端
CategotyController
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96package com.imxushuai.gulimall.product.controller;
import com.imxushuai.common.utils.PageUtils;
import com.imxushuai.common.utils.R;
import com.imxushuai.gulimall.product.entity.CategoryEntity;
import com.imxushuai.gulimall.product.service.CategoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* 商品三级分类
*
* @author imxushuai
* @email imxushuai@gmail.com
* @date 2021-12-14 11:27:24
*/
"product/category") (
public class CategoryController {
private CategoryService categoryService;
"/list/tree") (
public R listWithTree() {
List<CategoryEntity> tree = categoryService.listWithTree();
return R.ok().put("data", tree);
}
/**
* 列表
*/
"/list") (
public R list(@RequestParam Map<String, Object> params){
PageUtils page = categoryService.queryPage(params);
return R.ok().put("page", page);
}
/**
* 信息
*/
"/info/{catId}") (
public R info(@PathVariable("catId") Long catId){
CategoryEntity category = categoryService.getById(catId);
return R.ok().put("data", category);
}
/**
* 保存
*/
"/save") (
public R save(@RequestBody CategoryEntity category){
categoryService.save(category);
return R.ok();
}
/**
* 修改
*/
"/update") (
public R update(@RequestBody CategoryEntity category){
categoryService.updateById(category);
return R.ok();
}
/**
* 修改
*/
"/update/sort") (
public R updateSort(@RequestBody List<CategoryEntity> categoryList){
categoryService.updateBatchById(categoryList);
return R.ok();
}
/**
* 删除
*/
"/delete") (
public R delete(@RequestBody Long[] catIds){
categoryService.removeBatch(Arrays.asList(catIds));
return R.ok();
}
}CatewayService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24package com.imxushuai.gulimall.product.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.imxushuai.common.utils.PageUtils;
import com.imxushuai.gulimall.product.entity.CategoryEntity;
import java.util.List;
import java.util.Map;
/**
* 商品三级分类
*
* @author imxushuai
* @email imxushuai@gmail.com
* @date 2021-12-14 11:27:24
*/
public interface CategoryService extends IService<CategoryEntity> {
PageUtils queryPage(Map<String, Object> params);
List<CategoryEntity> listWithTree();
void removeBatch(List<Long> asList);
}CatewayServiceImpl
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72package com.imxushuai.gulimall.product.service.impl;
import org.springframework.stereotype.Service;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.imxushuai.common.utils.PageUtils;
import com.imxushuai.common.utils.Query;
import com.imxushuai.gulimall.product.dao.CategoryDao;
import com.imxushuai.gulimall.product.entity.CategoryEntity;
import com.imxushuai.gulimall.product.service.CategoryService;
"categoryService") (
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
public PageUtils queryPage(Map<String, Object> params) {
IPage<CategoryEntity> page = this.page(
new Query<CategoryEntity>().getPage(params),
new QueryWrapper<CategoryEntity>()
);
return new PageUtils(page);
}
public List<CategoryEntity> listWithTree() {
// 查询所有的分类
List<CategoryEntity> categoryList = baseMapper.selectList(null);
// 递归分类
List<CategoryEntity> level1List = categoryList.stream()
.filter(category -> category.getParentCid() == 0)
.peek(category -> category.setChildren(getChildrenList(category, categoryList)))
.sorted(Comparator.comparingInt(c -> (c.getSort() == null ? 0 : c.getSort())))
.collect(Collectors.toList());
return level1List;
}
public void removeBatch(List<Long> asList) {
// TODO 检查菜单, 若被其他分类引用则不允许删除
baseMapper.deleteBatchIds(asList);
}
/**
* 查询分类的所有子分类
*
* @param category 父分类
* @param categoryList 所有分类列表
*/
private List<CategoryEntity> getChildrenList(CategoryEntity root, List<CategoryEntity> categoryList) {
List<CategoryEntity> childrenList = categoryList.stream()
.filter(category -> category.getParentCid().equals(root.getCatId()))
.peek(category -> category.setChildren(getChildrenList(category, categoryList)))
.sorted(Comparator.comparingInt(c -> (c.getSort() == null ? 0 : c.getSort())))
.collect(Collectors.toList());
return childrenList;
}
}
配置路由网关与路径重写
在网关的routing.yml
中新增配置
1 | spring: |
网关跨域配置
在gulimall-gateway
中添加配置类用于允许跨域
1 | package com.imxushuai.gulimall.gulimallgateway.config; |
关闭renren-fast
中自带的跨域
1 | /** |
品牌管理
品牌管理的业务代码基本全部使用人人逆向工程生成的代码,主要记录文件上传部分
第三方服务
集成 Spring Cloud OSS完成图片上传
创建Module子工程,名称:gulimall-third-party
引入依赖
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.imxushuai.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>
<spring-cloud.version>Greenwich.SR5</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>com.imxushuai.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>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alicloud-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>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>启动类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package com.imxushuai.gulimall.thirdparty;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
public class GulimallThirdPartyApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallThirdPartyApplication.class, args);
}
}配置文件
application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15server:
port: 30000
spring:
application:
name: gulimall-third-party
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
alicloud:
access-key: your-access-key
secret-key: your-secret-key
oss:
endpoint: oss-cn-chengdu.aliyuncs.com
bucket: imxushuai-gulimallbootstrap.yml
1
2
3
4
5
6spring:
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
namespace: f15863d6-6739-4543-9aa6-51e9ad55d5af
修改
gulimall-gateway
的routing.yml
,配置gulimall-third-party
的路由1
2
3
4
5
6
7
8
9
10spring:
cloud:
gateway:
routes:
- id: third_party_route
uri: lb://gulimall-third-party
predicates:
- Path=/api/thirdparty/**
filters:
- RewritePath=/api/thirdparty/(?<segment>.*),/$\{segment}在
gulimall-third-party
新增API1
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74package com.imxushuai.gulimall.thirdparty.controller;
import com.aliyun.oss.OSS;
import com.aliyun.oss.common.utils.BinaryUtil;
import com.aliyun.oss.model.MatchMode;
import com.aliyun.oss.model.PolicyConditions;
import com.imxushuai.common.utils.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
public class OssController {
private OSS ossClient;
"${spring.cloud.alicloud.oss.endpoint}") (
private String endpoint;
"${spring.cloud.alicloud.oss.bucket}") (
private String bucket;
"${spring.cloud.alicloud.access-key}") (
private String accessId;
"${spring.cloud.alicloud.secret-key}") (
private String accessKey;
"/oss/policy") (
public R policy() {
String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint
String dir = new SimpleDateFormat("yyyy-MM-dd").format(new Date()) + "/"; // 用户上传文件时指定的前缀。
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(StandardCharsets.UTF_8);
String encodedPolicy = BinaryUtil.toBase64String(binaryData);
String postSignature = ossClient.calculatePostSignature(postPolicy);
respMap = new LinkedHashMap<String, String>();
respMap.put("accessid", accessId);
respMap.put("policy", encodedPolicy);
respMap.put("signature", postSignature);
respMap.put("dir", dir);
respMap.put("host", host);
respMap.put("expire", String.valueOf(expireEndTime / 1000));
} catch (Exception e) {
// Assert.fail(e.getMessage());
System.out.println(e.getMessage());
} finally {
ossClient.shutdown();
}
return R.ok().put("data", respMap);
}
}测试文件上传
JRS-303校验
JSR-303 是 Java EE 6 中的一项子规范,叫做 Bean Validation,官方参考实现是hibernate Validator。
之前写另一篇的文章的时候,也有用到Spring MVC的相关注解进行参数校验,但没这么全面,我重新单开了一篇文章把JRS-303重新记录一下。
文章链接:
实体类的字段添加注解,表示其字段的校验规则
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70package com.imxushuai.gulimall.product.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import com.imxushuai.common.valid.AddGroup;
import com.imxushuai.common.valid.ListValue;
import com.imxushuai.common.valid.UpdateGroup;
import com.imxushuai.common.valid.UpdateStatusGroup;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.*;
/**
* 品牌
*
* @author leifengyang
* @email leifengyang@gmail.com
* @date 2019-10-01 21:08:49
*/
"pms_brand") (
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
"修改必须指定品牌id", groups = {UpdateGroup.class}) (message =
"新增不能指定id", groups = {AddGroup.class}) (message =
private Long brandId;
/**
* 品牌名
*/
"品牌名必须提交", groups = {AddGroup.class, UpdateGroup.class}) (message =
private String name;
/**
* 品牌logo地址
*/
(groups = {AddGroup.class})
"logo必须是一个合法的url地址", groups = {AddGroup.class, UpdateGroup.class}) (message =
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
(groups = {AddGroup.class, UpdateStatusGroup.class})
0, 1}, groups = {AddGroup.class, UpdateStatusGroup.class}) (values = {
private Integer showStatus;
/**
* 检索首字母
*/
(groups = {AddGroup.class})
"^[a-zA-Z]$", message = "检索首字母必须是一个字母", groups = {AddGroup.class, UpdateGroup.class}) (regexp =
private String firstLetter;
/**
* 排序
*/
(groups = {AddGroup.class})
0, message = "排序必须大于等于0", groups = {AddGroup.class, UpdateGroup.class}) (value =
private Integer sort;
}在
gulimall-common
中创建校验分组:AddGroup
、UpdateGroup
和UpdateStatusGroup
AddGroup
1
2
3
4package com.imxushuai.common.valid;
public interface AddGroup {
}UpdateGroup
1
2
3
4package com.imxushuai.common.valid;
public interface UpdateGroup {
}UpdateStatusGroup
1
2
3
4package com.imxushuai.common.valid;
public interface UpdateStatusGroup {
}
在
gulimall-common
中创建自定义校验注解1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25package com.imxushuai.common.valid;
import javax.validation.Constraint;
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;
(validatedBy = {ListValueConstraintValidator.class})
({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
(RUNTIME)
public ListValue {
String message() default "必须提交指定的值";
Class[] groups() default {};
Class[] payload() default {};
int[] values() default {};
}在
gulimall-common
中创建自定义校验器,用于对自定义注解标注的字段进行校验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
27package com.imxushuai.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<>();
public void initialize(ListValue constraintAnnotation) {
int[] values = constraintAnnotation.values();
for (int value : values) {
set.add(value);
}
}
public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {
// 校验值
return set.contains(value);
}
}在
gulimall-product
的BrandController
中的API上添加@Validated
注解标注进行参数校验以及所使用的校验分组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/**
* 保存
*/
"/save") (
public R save(@Validated(AddGroup.class) @RequestBody BrandEntity brand){
brandService.save(brand);
return R.ok();
}
/**
* 修改
*/
"/update") (
public R update(@Validated(UpdateGroup.class) @RequestBody BrandEntity brand){
brandService.updateById(brand);
return R.ok();
}
/**
* 修改状态
*/
"/update/status") (
public R updateStatus(@Validated(UpdateStatusGroup.class) @RequestBody BrandEntity brand){
brandService.updateById(brand);
return R.ok();
}在
gulimall-product
中新增统一异常处理类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
41package com.imxushuai.gulimall.product.exception;
import com.imxushuai.common.exception.BizCodeEnum;
import com.imxushuai.common.utils.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
/**
* 集中处理所有异常
*/
4j
"com.imxushuai.gulimall.product.controller") (basePackages =
public class GulimallExceptionControllerAdvice {
(value = MethodArgumentNotValidException.class)
public R handleValidException(MethodArgumentNotValidException e) {
log.error("数据校验出现问题{},异常类型:{}", e.getMessage(), e.getClass());
BindingResult bindingResult = e.getBindingResult();
Map<String, String> errorMap = new HashMap<>();
bindingResult.getFieldErrors().forEach((fieldError) -> {
errorMap.put(fieldError.getField(), fieldError.getDefaultMessage());
});
return R.error(BizCodeEnum.VALID_EXCEPTION.getCode(), BizCodeEnum.VALID_EXCEPTION.getMsg()).put("data", errorMap);
}
(value = Throwable.class)
public R handleException(Throwable throwable) {
log.error("错误:", throwable);
return R.error(BizCodeEnum.UNKNOW_EXCEPTION.getCode(), BizCodeEnum.UNKNOW_EXCEPTION.getMsg());
}
}在
gulimall-common
中定义统一异常Code与消息的枚举类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
34package com.imxushuai.common.exception;
/***
* 错误码和错误信息定义类
* 1. 错误码定义规则为5为数字
* 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常
* 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
* 错误码列表:
* 10: 通用
* 001:参数格式校验
* 11: 商品
* 12: 订单
* 13: 购物车
* 14: 物流
*/
public enum BizCodeEnum {
UNKNOW_EXCEPTION(10000,"系统未知异常"),
VALID_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;
}
}重启服务,测试校验是否正常生效
SPU & SKU
此部分的代码太多了,所以就不写笔记了。
详情请参照视频。
分布式基础篇总结
分布式基础概念
微服务、注册中心、配置中心、远程调用、Feign、网关
基础开发
Spring Boot、Spring Cloud、Mybatis-Plus、Vue组件、阿里云存储
环境
虚拟机、Linux、Docker、MySQL、Redis、人人开源相关项目
开发规范
- 数据校验JSR303、全局异常处理、全局统一返回、全局跨域处理
- 枚举状态、业务状态码、VO/TP/PO划分、逻辑删除(假删除)
- Lombok