商品服务
分类维护
1.三级分类
简介:
使用renren-fast作为后台管理,维护商品的三级分类数据【增删改查】
1.在Entity类加上字段,代表不与数据库表映射字段
@TableField(exist = false)
private List<CategoryEntity> children;//子分类
2.product/category/list/tree【三级分类】
递归查询三级分类
@Override
public List<CategoryEntity> listWithTree() {
//查出所有分类
List<CategoryEntity> entities = baseMapper.selectList(null);
//组装成父子树形结构
//1级分类
List<CategoryEntity> level1Menus = entities.stream().
filter(entity -> entity.getParentCid() == 0).
map(entity -> {
entity.setChildren(getChildren(entity, entities));
return entity;
}).
sorted((entity1, entity2) ->
(entity1.getSort() == null ? 0 : entity1.getSort()) -
(entity2.getSort() == null ? 0 : entity2.getSort())).
collect(Collectors.toList());
return level1Menus;
}
//递归查找所有菜单的子菜单
private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> all) {
List<CategoryEntity> children = all.stream().filter(entity -> entity.getParentCid().equals(root.getCatId())).
map(entity -> {
entity.setChildren(getChildren(entity, all));
return entity;
}).sorted((entity1, entity2) ->
(entity1.getSort() == null ? 0 : entity1.getSort()) -
(entity2.getSort() == null ? 0 : entity2.getSort())).
collect(Collectors.toList());
return children;
}
1.1.配置网关路由+跨域
1、启动renren-fast后端,启动前端项目npm run dev
2、登录localhost:8001,admin admin
新增后台管理的一级目录:商品系统
系统管理->菜单管理->新增-> 目录
3、新增菜单:分类维护,选中商品系统【可看下图】
4、菜单的url:localhost:8008/product/category 会转换成product-category作为请求路由【会根据不同的路由加载不同的组件(这是vue的功能)】
新建category.vue文件放在目录:src->views->modules->product
输入vue根据模板生成代码
5、这个是展示属性数据用的
doc:https://element.eleme.cn/#/zh-CN/component/tree
使用<el-tree>展示三级分类
6、真实数据应该是调用商品服务的接口来的,所以可以查看项目内部sys->user.vue是怎么调接口的
问题:要将请求发送到网关,而不是8080【全局查询localhost:8080,修改为网关的地址:80】
1)修改static/config/index.js
window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api';
7、让gulimall-fast后端项目注册到网关中
1)引入依赖
<dependency>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
2)添加Nacos配置
spring:
application:
name: renren-fast
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
3)在Application上加上注解,将该服务注册到注册中心中
@EnableDiscoveryClient
4)设置网关转发规则:带负载均衡的,路径匹配转发规则
例子:http://localhost:88/api/captcha.jpg http://localhost:8080/api/captcha.jpg
http://localhost:8080/renren-fast/captcha.jpg
- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**
但是!正确的是还要带上项目名 server-servlet-context-path的路径,且去掉api **/
最后版本:
- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**
filters:
- RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}
5)重启renren-fast 、 gateway
6)出现问题header contains multiple values 'http://localhost:8001, http://localhost:8001', but only one is allowed.
配置了多个跨域,找到renren-fast的跨域,给注释掉
7.在gateway中配置跨域
@Configuration
public class GulimallCorsConfiguration {
/**
* 跨域解决办法之一:
* 过滤器,给所有请求增加请求头信息
* 使得预检请求通过
*/
@Bean
public CorsWebFilter corsWebFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 1、配置跨域
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.setAllowCredentials(true);// 否则跨域请求会丢失cookie信息
source.registerCorsConfiguration("/**", corsConfiguration);
return new CorsWebFilter(source);
}
}
8、商品管理请求404
原因:所有api请求转发到了renren-fast
Request URL: http://localhost:88/api/product/category/list/tree
Request Method: GET
Status Code: 404 Not Found
Remote Address: [::1]:88
Referrer Policy: no-referrer-when-downgrade
解决:api/product转到商品服务
在gateway 配置路由规则
- id: product_route
uri: lb://gulimall-product
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?<segment>.*),/$\{segment}
9、配置商品服务配置
1)新建bootstrap.properties
spring.application.name=gulimall-product
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=a152f0a8-3f55-4496-bc9a-c26df96bb2f9
spring.cloud.nacos.config.group=dev
#如果这个dev不放开的话,默认的gulimall-coupon不生效【会加载dev分组下的所有配置】
spring.cloud.nacos.config.extension-configs[0].data-id=datasource.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=mybatis.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=spring.yml
spring.cloud.nacos.config.extension-configs[2].group=dev
spring.cloud.nacos.config.extension-configs[2].refresh=true
2)在Nacos配置中心新建相关配置,服务发现、mybatis-plus、oss
http://127.0.0.1:8848/nacos
3)开启服务注册发现功能,在Application添加注解
@EnableDiscoveryClient
4)重启,访问:http://localhost:88/api/product/category/list/tree
错误:{"msg":"invalid token","code":401},没有令牌。说明请求被renren-fast拦截了
原因:路由 api/product被api/** 拦截了
解决:把gateway精确的路由放在前面,防止api/**优先拦截 **/
10、这个时候已经可以显示数据了,看图
2.拖拽分类
前端整理好数据,提交更新数据。后台只做updater(List<Entity>)
品牌管理
1.OSS云存储(阿里云)
步骤:
1.https://oss.console.aliyun.com/overview
开通 对象存储OSS
2.查看文档:
常用入口=》API文档=》在帮助中心打开【https://help.aliyun.com/document_detail/31947.html?spm=5176.8465980.help.dexternal.4e701450Bu0s0M】
1)专业术语【https://help.aliyun.com/document_detail/31947.html】
Bucket:一个项目创建一个Bucket,存储空间
Object:对象是 OSS 存储数据的基本单元
Region:地域表示 OSS 的数据中心所在物理位置
Endpoint:访问OSS文件域名URL
AccessKey:访问密钥
读写权限:私有/公共度/公共读写
服务端加密:无
实施日志:不开通
3.上传方式【采用方式二】
方式一:
文件先上传到应用服务器,然后在上传到OSS
方式二:
服务端签名后直传【https://www.baidu.com/index.php?tn=monline_3_dg】
1)用户向应用服务器请求上传Policy
2)应用服务器返回上传Policy【由服务器控制上传地址等信息】
3)用户直接上传OSS
4.获取子用户Accesskeys
1)鼠标移至账号头像,点击Accesskeys管理,使用子用户Accesskeys【首次使用需要开通RAM访问控制】
2)新增用户
登录名称:gulimall-wan
显示名称:gulimall
访问方式:编程访问
3)新增完成复制AccessKeyID和secret
LTAI5t6jnyvWc34pU9BKRtwr/on5rU9Y06iNakTKMKCJ9KVOqv6OyZC
4)添加权限:AliyunOSSFullAccess
5)修改CORS
打开bucket -> 权限管理 -> 跨域设置 -> 设置 -> 创建规则
来源:*
允许Headers:*
允许Methods:POST
使用原生sdk上传的demo:
https://help.aliyun.com/document_detail/91868.html?spm=a2c4g.11186623.2.16.1d2f7eaeOSyN4O#concept-ahk-rfz-2fb
使用aliyun封装的SDK实现上传,通过以下网址找到演示demo
https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md
***OSS放在单独第三方微服务模块
1.创建第三方微服务:
com.atguigu.gulimall
gulimall-third-party
谷粒商城-第三方服务
选择openFeign,SpringWeb
2.common模块引入版本管理
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.1.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>
</dependencies>
</dependencyManagement>
3.引入common,并修改springboot springcloud版本号,引入oss,并且引入版本管理
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR6</spring-cloud.version>
</properties>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alicloud-oss</artifactId>
</dependency>
<dependency>
<groupId>com.atguigu.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>
版本管理
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.1.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>
</dependencies>
</dependencyManagement>
3.启动类注解添加,主pom添加模块
@EnableDiscoveryClient
<module>gulimall-third-party</module>
4.application.yml
server:
port: 30000
spring:
application:
name: gulimall-third-party
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
alicloud:
access-key: LTAI5t6jnyvWc34pU9BKRtwr
secret-key: on5rU9Y06iNakTKMKCJ9KVOqv6OyZC
oss:
endpoint: oss-cn-shanghai.aliyuncs.com
bucket: gulimall-wan
logging:
level:
com.atguigu.gulimall: debug
5.创建OssController,返回policy凭证【获取对象签名】
@RestController
public class OssController {
@Autowired
OSS ossClient;
@Value("${spring.cloud.alicloud.oss.endpoint}")
private String endpoint;
@Value("${spring.cloud.alicloud.oss.bucket}")
private String bucket;
@Value("${spring.cloud.alicloud.access-key}")
private String accessId;
@RequestMapping("/oss/policy")
public R policy() {
// https://gulimall-wan.oss-cn-shanghai.aliyuncs.com
String host = "https://" + bucket + "." + endpoint;
// callbackUrl为 上传回调服务器的URL,请将下面的IP和Port配置为您自己的真实信息。
// String callbackUrl = "http://88.88.88.88:8888";
// 文件在bucket存储目录,若不存在则会自动创建路径。使用日期作为目录
String dir = new SimpleDateFormat("yyyy-MM-dd").format(new Date()) + "/";
// 创建OSSClient实例。这里是alicloud starter自动配置,可自动注入
//OSS ossClient = new OSSClientBuilder().build(endpoint, accessId, accessKey);
Map<String, String> respMap = null;
try {
long expireTime = 30;
long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
Date expiration = new Date(expireEndTime);//
// PostObject请求最大可支持的文件大小为5 GB,即CONTENT_LENGTH_RANGE为5*1024*1024*1024。
PolicyConditions policyConds = new PolicyConditions();
policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);
String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
byte[] binaryData = postPolicy.getBytes("utf-8");
String encodedPolicy = BinaryUtil.toBase64String(binaryData);
String postSignature = ossClient.calculatePostSignature(postPolicy);
respMap = new LinkedHashMap<String, String>();
respMap.put("accessid", accessId);
respMap.put("policy", encodedPolicy);
respMap.put("signature", postSignature);
respMap.put("dir", dir);
respMap.put("host", host);
respMap.put("expire", String.valueOf(expireEndTime / 1000));
// respMap.put("expire", formatISO8601Date(expiration));
// 下面是跨域设置,在网关统一解决跨域
// JSONObject jasonCallback = new JSONObject();
// jasonCallback.put("callbackUrl", callbackUrl);
// jasonCallback.put("callbackBody",
// "filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}");
// jasonCallback.put("callbackBodyType", "application/x-www-form-urlencoded");
// String base64CallbackBody = BinaryUtil.toBase64String(jasonCallback.toString().getBytes());
// respMap.put("callback", base64CallbackBody);
//
// JSONObject ja1 = JSONObject.fromObject(respMap);
// // System.out.println(ja1.toString());
// response.setHeader("Access-Control-Allow-Origin", "*");
// response.setHeader("Access-Control-Allow-Methods", "GET, POST");
// response(request, response, ja1.toString());
} catch (Exception e) {
// Assert.fail(e.getMessage());
System.out.println(e.getMessage());
} finally {
ossClient.shutdown();
}
return R.ok().put("data", respMap);
}
}
6.配置网关
- id: third_party_route
uri: lb://gulimall-third-party
predicates:
- Path=/api/thirdparty/**
filters:
- RewritePath=/api/thirdparty/(?<segment>.*),/$\{segment}
前端配置
1.拷贝upload组件放到component中,修改multiUpload.vue和singleUpload.vue文件
修改成自己的外网Bucket域名
action="http://gulimall-wan.oss-cn-shanghai.aliyuncs.com"
2.然后在图片上传地址栏修改vue代码,使用单文件上传
在brand-add-or-update.vue中导入
1)import SingleUpload from "@/componets/upload/singleUpload"
2)在data 的components:{SingleUpload}
3)在</template>中就可以使用了 <single-upload>来代替之前的<el-input>标签【自定义节点】
3.设置跨域,允许bucket跨域请求
在oss里面修改管理控制台修改:
https://oss.console.aliyun.com/bucket/oss-cn-shanghai/gulimall-wan/permission/cors
2.后端数据校验JSR303
提示:springboot2.3.0版本没有集成validation包,需要导入
<!--Valid-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.3.2.RELEASE</version>
</dependency>
每个controller请求都作处理
1.在controller接口参数上添加@Valid表示开启校验【校验不通过接口请求返回400 bad request】
并追加BindingResult接收异常,包装R.error统一返回
/**
* 保存
*/
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult result) {
if (result.hasErrors()) {
Map<String, String> map = new HashMap<>();
// 获取校验的错误结果
result.getFieldErrors().forEach((item) -> {
// 获取到错误提示FieldError
String message = item.getDefaultMessage();
// 获取错误的属性名字
String field = item.getField();
map.put(field, message);
});
return R.ok().error(400, "提交的数据不合法").put("data", map);
}else {
brandService.save(brand);
return R.ok();
}
}
2.在Entity类添加注解(javax.validation.constraints)
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名非空")
private String name;
/**
* 品牌logo地址
*/
@NotBlank(message = "logo非空")
@URL(message = "logo不合法")
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
@NotNull(message = "显示状态非空")
@ListValue(vals={0,1}, message = "显示状态非法取值")
private Integer showStatus;
/**
* 首字母
*/
@NotBlank(message = "首字母非空")
@Pattern(regexp = "^[a-zA-Z]$]", message = "检索首字母必须是一个字母")
private String firstLetter;
/**
* 排序
*/
@NotNull(message = "排序非空")
@Min(value = 0, message = "排序必须大于等于0")
private Integer sort;
}
3.统一校验(统一异常处理)
统一异常处理【去掉controller中的BindingResult,将异常处理继续外抛统一处理】
1.添加统一异常处理类
@Slf4j
@RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {
/**
* 统一处理异常,可以使用Exception.class先打印一下异常类型来确定具体异常
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleValidException(MethodArgumentNotValidException e) {
log.error("数据校验出现问题{}, 异常类型:{}", e.getMessage(), e.getClass());
BindingResult result = e.getBindingResult();
Map<String, String> errorMap = new HashMap<>();
// 获取校验的错误结果
result.getFieldErrors().forEach((item) -> {
// 获取错误的属性名字 + 获取到错误提示FieldError
errorMap.put(item.getField(), item.getDefaultMessage());
});
return R.ok().error(BizCodeEnume.VALID_EXCEPTION.getCode(), BizCodeEnume.VALID_EXCEPTION.getMsg()).put("data", errorMap);
}
@ExceptionHandler(value = Throwable.class)
public R handleValidException(Throwable throwable) {
log.error("Throwable错误,未处理:" + throwable);
return R.ok().error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(), BizCodeEnume.UNKNOW_EXCEPTION.getMsg());
}
}
2.增加错误枚举类
public enum BizCodeEnume {
UNKNOW_EXCEPTION(10000, "系统未知异常"),
VALID_EXCEPTION(10001, "参数格式验证失败"),
PRODUCT_UP_EXCEPTION(11000, "商品上架异常"),
SMS_CODE_EXCEPTION(10002,"验证码获取频率太高,请稍后再试"),
TO_MANY_REQUEST(10002,"请求流量过大,请稍后再试"),
USER_EXIST_EXCEPTION(15001,"存在相同的用户"),
PHONE_EXIST_EXCEPTION(15002,"存在相同的手机号"),
NO_STOCK_EXCEPTION(21000,"商品库存不足"),
LOGINACCT_PASSWORD_EXCEPTION(15003,"账号或密码错误");
private int code;
private String msg;
BizCodeEnume(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
4.分组校验(多场复杂校验)
简介:
例如新增和修改操作,新增时不带id,修改时带id
步骤:
1.新增分组接口
AddGroup
UpdateGroup
UpdateStatusGroup
2.在Entity类的校验注解上添加分组,并指定对应校验接口
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@NotNull(message = "修改必须指定品牌id", groups = {UpdateGroup.class, UpdateStatusGroup.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-显示]
*/
@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;
}
3.替换controller接口中@Valid注解为@Validated,并且增加分组class
/**
* 保存
*/
@RequestMapping("/save")
public R save(@Validated(AddGroup.class) @RequestBody BrandEntity brand){
brandService.save(brand);
return R.ok();
}
5.自定义校验
例如Integer showStatus的值必须是指定值1和0
1.编写一个自定义的校验注解
2.编写一个自定义的校验器
3.关联自定义的校验器和自定义的校验注解
步骤:
1.编写一个自定义的校验注解
1)common模块添加依赖
<!--自定义注解-->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
2)创建自定义注解(可参照@NotEmpty注解)
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Documented//
@Constraint(validatedBy = {ListValueConstraintValidator.class})// 使用哪个校验器进行校验的(这里不指定,在初始化的时候指定)
public @interface ListValue {
// 默认会找ValidationMessages.properties
String message() default "{com.atguigu.common.valid.ListValue.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
// 可以指定数据只能是vals数组指定的值
int[] vals() default {};
}
3)创建message配置文件 ValidationMessages.properties
com.atguigu.common.valid.ListValue.message=必须提交指定的值
2.编写一个自定义的校验器【点击validatedBy进去查看需要一个什么类型的校验器,然后自己作实现类即可】
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();
if (vals != null && vals.length != 0) {
for (int val : vals) {
set.add(val);
}
}
}
/**
* 校验逻辑
* @param value 需要校验的值
* @param context 上下文
* @return
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return set.contains(value);// 如果set length==0,会返回false
}
}
3.关联自定义的校验器和自定义的校验注解
自定义注解上关联:@Constraint(validatedBy = {ListValueConstraintValidator.class})
4.在Entity属性上增加自定义校验
/**
* 显示状态[0-不显示;1-显示]
*/
@NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})
@ListValue(vals = {0, 1}, groups = {AddGroup.class, UpdateStatusGroup.class})
private Integer showStatus;
6.表关系讲解
6.1.SPU、SKU
简介:
SPU:标准化产品单元 standard product unit,是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。
SKU:库存量单位 Stock Keeping Unit,
SPU:iphone XS、iphone XS max、iphone XR、MI8、
SKU:iphonex 64G 黑曜石、MI8 8+64G+黑色
6.2.属性相关表设计(规格参数、销售属性)
每个分类下的商品共享规格参数,与销售属性。只是有些商品不一定要用这个分类下全部的属性;
属性是以三级分类组织起来的【attr关联分类id】
规格参数中有些是可以提供检索的
规格参数也是基本属性,他们具有自己的分组
属性的分组也是以三级分类组织起来的
属性名确定的,但是值是每一个商品不同来决定的【例如手机都有相同的基本属性,只是值不同】
基本属性表:attr_id,search_type,attr_type属性类型(规格参数、销售属性),catelog_id(三级分类)【demo:入网型号】
属性分组表:attr_group_id,catelog_id【demo:主体,关联了 分类表。手机分类下有哪些分组】
属性+属性分组关联关系表:relation_id,attr_id,attr_group_id【1对多】
商品表spu_info:spu_id,spu_name,spu_description,catelog_id,brand_id
商品属性值表product_attr_value:id,spu_id,attr_id,attr_value属性值
商品库存表sku_info:sku_id,spu_id,price
销售属性值表sku_sale_attr_value:id,sku_id,attr_id,attr_value属性值【1对多】
基本属性:组成spu
销售属性:组成sku
7.属性分组管理
7.1.父子组件冒泡(事件传递)
7.2.mp查询(and (() or () or))写法
分页请求参数:
{
page: 1,//当前页码
limit: 10,//每页记录数
sidx: 'id',//排序字段
order: 'asc/desc',//排序方式
key: '华为'//检索关键字
}
简介:查询分类下所有属性分组
1.根据catelogId查询【catelogId = 0查询所有】
/**
* 列表
*/
@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);
}
2.多字段模糊匹配 param.key 是否空=》 and (() or () or)写法
@Override
public PageUtils queryPage(Map<String, Object> params, Long catelogId) {
String key = (String) params.get("key");
QueryWrapper<AttrGroupEntity> wrapper = new QueryWrapper<AttrGroupEntity>();
if (!StringUtils.isEmpty(key)) {
// 根据key多字段模糊查询
// and (() or ())
wrapper.and((obj) -> {
obj.eq("attr_group_id", key).or().like("attr_group_name", key);
});
}
if (catelogId == 0) {
// 查询所有
IPage<AttrGroupEntity> page = this.page(
new Query<AttrGroupEntity>().getPage(params),
wrapper);
return new PageUtils(page);
} else {
// 根据catelogId查询
wrapper.eq("catelog_id", catelogId);
IPage<AttrGroupEntity> page = this.page(
new Query<AttrGroupEntity>().getPage(params),
wrapper
);
return new PageUtils(page);
}
}
7.3.新增属性分组(不返回空集合@JsonInclude(value = JsonInclude.Include.NON_EMPTY))
1.查询三级分类接口 返回的json数据字段不返回children空集合,在分组Entity下添加以下注解
@JsonInclude(value = JsonInclude.Include.NON_EMPTY)
@TableField(exist = false)
private List<CategoryEntity> children;
2.查询属性分组时,封装三级分类完整路径,用于回显完整分类路径
1)在属性分组Entity下新增属性路径字段
/**
* 分类路径
*/
@TableField(exist = false)
private Long[] catelogPath;
2)查询属性分组接口
@RequestMapping("/info/{attrGroupId}")
public R info(@PathVariable("attrGroupId") Long attrGroupId){
AttrGroupEntity attrGroup = attrGroupService.getById(attrGroupId);
// 查询三级分类路径
Long[] path = categoryService.findCatelogPath(attrGroup.getCatelogId());
attrGroup.setCatelogPath(path);
return R.ok().put("attrGroup", attrGroup);
}
3)service方法
/**
* 根据catelogId查询所有父分类ID
*/
@Override
public Long[] findCatelogPath(Long catelogId) {
List<Long> paths = new ArrayList<>();
// 递归查询父类
paths = findParentPath(catelogId, paths);
// 逆序,父在前
Collections.reverse(paths);
return paths.toArray(new Long[paths.size()]);
}
/**
* 递归查找父路径
*/
private List<Long> findParentPath(Long catelogId, List<Long> paths) {
paths.add(catelogId);
CategoryEntity category = this.getById(catelogId);
if (category.getParentCid() != 0) {
findParentPath(category.getParentCid(), paths);
}
return paths;
}
7.4.mp分页查询
解析:
controller统一使用Map接收参数,其次使用工具类Query封装分页请求Page对象(可以自己在Constant修改请求参数key值)
分页请求参数:
{
page: 1,//当前页码
limit: 10,//每页记录数
sidx: 'id',//排序字段
order: 'asc/desc',//排序方式
key: '华为'//检索关键字
}
步骤:
1.添加配置类,开启事务
@Configuration
@MapperScan("com.atguigu.gulimall.product.dao")
@EnableTransactionManagement
public class MybatisPlusConfig {
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false
paginationInterceptor.setOverflow(true);
// 设置最大单页限制数量,默认 500 条,-1 不受限制
paginationInterceptor.setLimit(1000);
// 开启 count 的 join 优化,只针对部分 left join
paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
return paginationInterceptor;
}
}
2.分页查询品牌
@Override
public PageUtils queryPage(Map<String, Object> params) {
// 获取关键字
String key = (String) params.get("key");
QueryWrapper<BrandEntity> queryWrapper = new QueryWrapper<>();
if (!StringUtils.isEmpty(key)) {
// 关键字非空,拼接关键字查询条件
queryWrapper.eq("brand_id", key).or().like("name", key);
}
IPage<BrandEntity> page = this.page(
new Query<BrandEntity>().getPage(params),
queryWrapper
);
return new PageUtils(page);
}
7.5.品牌关联分类(冗余表同步更新+事务)
品牌可以关联多个分类,分类可以关联多个品牌,所以需要一个中间表(pms_category_brand_relation)
1.电商表里 大表不作关联表操作,一个一个查询然后设置冗余字段
2.如果冗余字段修改后要保持数据一致性,例如品牌名/分类名
解决:修改品牌表/分类表时,要修改所有冗余数据
1.查询品牌关联的所有分类
/product/categorybrandrelation/catelog/list
2.新增品牌与分类的关联关系
/product/categorybrandrelation/save
3.修改品牌
/product/brand/update
需要同时修改冗余表
4.修改分类
/product/category/update
需要同时修改冗余表
7.6.基本属性(VO的概念)
新增基本属性
添加以下属性:
三级分类
所属分组(选择了三级分类后会显示当前分类下所有分组)
可检索
快速展示(会放入商品介绍)
重写新增基本属性的方法,因为没有使用的是逆向生成的代码实现,没有在 属性/属性分组关联表中添加关联关系
所以提交参数需要包含 属性分组ID,该字段Entity中没有,所以新增Vo类
1.PO(Persistent Object),持久化对象,主要用于持久化层,与数据库表结构一一对应,通过DAO层向上传输数据源对象。
2.DTO(Data Transfer Object),数据传输对象,Service或Manager向外传输的对象
3.VO(View object),视图对象,接收页面请求数据封装的对象, 封装返回给页面的对象
4.BO(business object)业务对象,由多个不同类型的PO组成,例如一个简历对象,由教育经历PO,工作经历PO组成
5.POJO(plain ordinary java object)简单无规则java对象,是 PO/DTO/BO/VO的统称,只有基本的setter、getter方法
6.DAO(data access object)数据访问对象是一个sun的一个标准j2ee 设计模式,这个模式中有个接口就是DAO,它负持久层的操作。为业务层提供接口。此对象用于访问数据库。通常和PO结合使用,DAO中包含了各种数据库的操作方法。通过它的方法,结合PO对数据库进行相关的操作。夹在业务逻辑与数据库资源中间。配合vo,提供数据库的 CRUD 操作.
步骤:
1.新增AttrVo,复制entity中所有的属性放到Vo中,并新增attrGroupId
2.修改save方法中的入参为AttrVo
3.修改service方法
@Transactional
@Override
public void saveAttr(AttrVo attr) {
AttrEntity attrEntity = new AttrEntity();
BeanUtils.copyProperties(attr, attrEntity);
// 保存基本数据
this.save(attrEntity);
// 保存关联关系
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
relationEntity.setAttrGroupId(attr.getVttrGroupId());// 分组ID
relationEntity.setAttrId(attrEntity.getAttrId());// 属性ID
relationDao.insert(relationEntity);
}
获取分类下规格参数
/product/attr/base/list/{catelogId}
注意:返回参数包含catelogName/groupName,所以需要封装返回 VO,查询冗余数据
@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)) {
queryWrapper.and(wrapper -> wrapper.eq("attr_id", key).or().like("attr_name", key));
}
// 分页查询
IPage<AttrEntity> page = this.page(new Query<AttrEntity>().getPage(params), queryWrapper);
// 封装回参
return new PageUtils(page);
}
根据ID查询基本属性,用于回显
/product/attr/info/{attrId}
@Override
public AttrRespVo getAttrInfo(Long attrId) {
AttrEntity attrEntity = this.getById(attrId);
AttrRespVo respVo = new AttrRespVo();
BeanUtils.copyProperties(attrEntity, respVo);
// 1.查询设置分组ID
AttrAttrgroupRelationEntity relationEntity = relationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrId));
if (relationEntity != null) {
respVo.setAttrGroupId(relationEntity.getAttrGroupId());
// 查询分组名
AttrGroupEntity groupEntity = attrGroupDao.selectById(relationEntity.getAttrGroupId());
respVo.setGroupName(groupEntity.getAttrGroupName());
}
// 2.查询设置分类path
Long[] catelogPath = categoryService.findCatelogPath(attrEntity.getCatelogId());
respVo.setCatelogPath(catelogPath);
return respVo;
}
修改
基本属性和分组是1对1的关系
@Transactional
@Override
public void updateAttr(AttrVo attr) {
AttrEntity attrEntity = new AttrEntity();
BeanUtils.copyProperties(attr, attrEntity);
this.updateById(attrEntity);
// 修改分组关联
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
relationEntity.setAttrId(attr.getAttrId());
relationEntity.setAttrGroupId(attr.getAttrGroupId());
int count = relationDao.update(relationEntity, new UpdateWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attr.getAttrId()));
if (count == 0) {
// 新增分组关联
relationDao.insert(relationEntity);
}
}
7.7.销售属性
修改查询方法,将属性类型作为参数传入
新增销售属性
不新增销售属性关联属性分组数据
7.8.属性分组
新建关联(属性分组关联基本属性)
分组 只能关联当前 分类下 其他分组未关联的基本属性
8.商品维护
8.1.获取所有会员等级
获取所有会员等级
/member/memberlevel/list
请求参数
{
page: 1,//当前页码
limit: 10,//每页记录数
sidx: 'id',//排序字段
order: 'asc/desc',//排序方式
key: '华为'//检索关键字
}
响应数据
{
"msg": "success",
"code": 0,
"page": {
"totalCount": 0,
"pageSize": 10,
"totalPage": 0,
"currPage": 1,
"list": [{
"id": 1,
"name": "aaa",
"growthPoint": null,
"defaultStatus": null,
"freeFreightPoint": null,
"commentGrowthPoint": null,
"priviledgeFreeFreight": null,
"priviledgeMemberPrice": null,
"priviledgeBirthday": null,
"note": null
}]
}
}
添加三个会员等级
8.2.发布商品
简介:
1.使用json参数直接生成vo类
https://www.json.cn/json/json2java.html
2.发布商品步骤
// 1.保存spu基本信息 pms_spu_info
// 2.保存spu描述图片 pms_spu_info_desc
// 3.保存spu图片集 pms_spu_images
// 4.保存spu基本参数值 pms_product_attr_value
// 5.保存spu的积分信息(购买产生积分,现阶段绑定spu,可以绑定sku) 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的优惠、满减信息、会员价格:
// sms_sku_ladder\sms_sku_full_reduction\sms_member_price
{
"spuName": "Apple XR",
"spuDescription": "Apple XR",
"catalogId": 225,
"brandId": 12,
"weight": 0.048,
"publishStatus": 0,
"decript": ["https://gulimall-hello.oss-cn-beijing.aliyuncs.com/2019-11-22//66d30b3f-e02f-48b1-8574-e18fdf454a32_f205d9c99a2b4b01.jpg"],
"images": ["https://gulimall-hello.oss-cn-beijing.aliyuncs.com/2019-11-22//dcfcaec3-06d8-459b-8759-dbefc247845e_5b5e74d0978360a1.jpg", "https://gulimall-hello.oss-cn-beijing.aliyuncs.com/2019-11-22//5b15e90a-a161-44ff-8e1c-9e2e09929803_749d8efdff062fb0.jpg"],
"bounds": {
"buyBounds": 500,
"growBounds": 6000
},
"baseAttrs": [{
"attrId": 7,
"attrValues": "aaa;bb",
"showDesc": 1
}, {
"attrId": 8,
"attrValues": "2019",
"showDesc": 0
}],
"skus": [{
"attr": [{
"attrId": 9,
"attrName": "颜色",
"attrValue": "黑色"
}, {
"attrId": 10,
"attrName": "内存",
"attrValue": "6GB"
}],
"skuName": "Apple XR 黑色 6GB",
"price": "1999",
"skuTitle": "Apple XR 黑色 6GB",
"skuSubtitle": "Apple XR 黑色 6GB",
"images": [{
"imgUrl": "https://gulimall-hello.oss-cn-beijing.aliyuncs.com/2019-11-22//dcfcaec3-06d8-459b-8759-dbefc247845e_5b5e74d0978360a1.jpg",
"defaultImg": 1
}, {
"imgUrl": "https://gulimall-hello.oss-cn-beijing.aliyuncs.com/2019-11-22//5b15e90a-a161-44ff-8e1c-9e2e09929803_749d8efdff062fb0.jpg",
"defaultImg": 0
}],
"descar": ["黑色", "6GB"],
"fullCount": 5,
"discount": 0.98,
"countStatus": 1,
"fullPrice": 1000,
"reducePrice": 10,
"priceStatus": 0,
"memberPrice": [{
"id": 1,
"name": "aaa",
"price": 1998.99
}]
}, {
"attr": [{
"attrId": 9,
"attrName": "颜色",
"attrValue": "黑色"
}, {
"attrId": 10,
"attrName": "内存",
"attrValue": "12GB"
}],
"skuName": "Apple XR 黑色 12GB",
"price": "2999",
"skuTitle": "Apple XR 黑色 12GB",
"skuSubtitle": "Apple XR 黑色 6GB",
"images": [{
"imgUrl": "",
"defaultImg": 0
}, {
"imgUrl": "",
"defaultImg": 0
}],
"descar": ["黑色", "12GB"],
"fullCount": 0,
"discount": 0,
"countStatus": 0,
"fullPrice": 0,
"reducePrice": 0,
"priceStatus": 0,
"memberPrice": [{
"id": 1,
"name": "aaa",
"price": 1998.99
}]
}, {
"attr": [{
"attrId": 9,
"attrName": "颜色",
"attrValue": "白色"
}, {
"attrId": 10,
"attrName": "内存",
"attrValue": "6GB"
}],
"skuName": "Apple XR 白色 6GB",
"price": "1998",
"skuTitle": "Apple XR 白色 6GB",
"skuSubtitle": "Apple XR 黑色 6GB",
"images": [{
"imgUrl": "",
"defaultImg": 0
}, {
"imgUrl": "",
"defaultImg": 0
}],
"descar": ["白色", "6GB"],
"fullCount": 0,
"discount": 0,
"countStatus": 0,
"fullPrice": 0,
"reducePrice": 0,
"priceStatus": 0,
"memberPrice": [{
"id": 1,
"name": "aaa",
"price": 1998.99
}]
}, {
"attr": [{
"attrId": 9,
"attrName": "颜色",
"attrValue": "白色"
}, {
"attrId": 10,
"attrName": "内存",
"attrValue": "12GB"
}],
"skuName": "Apple XR 白色 12GB",
"price": "2998",
"skuTitle": "Apple XR 白色 12GB",
"skuSubtitle": "Apple XR 黑色 6GB",
"images": [{
"imgUrl": "",
"defaultImg": 0
}, {
"imgUrl": "",
"defaultImg": 0
}],
"descar": ["白色", "12GB"],
"fullCount": 0,
"discount": 0,
"countStatus": 0,
"fullPrice": 0,
"reducePrice": 0,
"priceStatus": 0,
"memberPrice": [{
"id": 1,
"name": "aaa",
"price": 1998.99
}]
}]
}
BUG汇总
为了测试方便,使用以下命令将当前mysql连接窗口设置为读未提交(mysql默认是可重复读)
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
1.插入pms_spu_info_desc报错,Field 'spu_id' doesn't have a default value
原因,mybatisPlus标注了@TableId的主键默认是IdType.NONE;且数据库未设置自增,导致主键未空
修改:entity类上@TableId(type = IdType.INPUT)
2、过滤sku未选中图片
3、优惠无意义数据
1)满0件打0折
2)满0元减0元
3)会员价格为0的数据
8.3.spu管理(后台返回前端时间格式化处理)
未格式化之前返回数据:
2020-08-13T01:38:11.000+00:00
添加配置:(同时修改时区)
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
8.4.规格(SPU管理->规格)
1.spu商品规格更新逻辑
1)根据spuId删除该商品所有规格
2)然后根据spuId新增商品所有规格
库存服务(自营)
wms_purchase 采购单
wms_purchase_detail 采购需求
wms_ware_info 仓库
wms_ware_order_task
wms_ware_order_task_detail
wms_ware_sku 各仓库各商品件数
1.人工/系统库存预警 两种方式创建采购需求(采购需求与仓库+sku绑定)
2.多个采购需求可以合并为一个采购单
3.
1、仓库指定sku商品库存
2、采购单分配采配人员【分配状态】
3、采购需求合并【合并到采购单,采购单状态->分配状态】
1.查询商品库存wms_ware_sku
根据skuId或 仓库Id查询库存
/ware/waresku/list
请求参数
{
page: 1,//当前页码
limit: 10,//每页记录数
sidx: 'id',//排序字段
order: 'asc/desc',//排序方式
wareId: 123,//仓库id
skuId: 123//商品id
}
响应数据
{
"msg": "success",
"code": 0,
"page": {
"totalCount": 0,
"pageSize": 10,
"totalPage": 0,
"currPage": 1,
"list": [{
"id": 1,
"skuId": 1,
"wareId": 1,
"stock": 1,
"skuName": "dd",
"stockLocked": 1
}]
}
}
2.采购需求/采购单
多条采购需求需要合并到 一条未领取的采购单中(采购单是一条已存在的新建状态采购单记录)
2.1.新增采购需求wms_purchase_detail
{
"skuId": "商品ID",
"skuNum": "商品数量",
"wareId": "仓库ID"
}
2.2.合并采购单
1.新增一条采购单
2.管理员列表-》新增采购人员
3.分配采购单到新增的采购人员上
4.合并多条采购需求到一条采购单上(采购单创建、未领取状态)【查询已创建and未领取状态采购单】
若未选中采购单,则会创建一条新的采购单(采购单ID为空)
合并:
/ware/purchase/merge
{
purchaseId: 1, //整单id
items:[1,2,3,4] //合并项集合
}
2.3.领取采购单
描述:
采购人员领取采购单【可同时领取多条,并且校验是否可以被领取】
1.切换采购单状态至已领取
2.切换采购需求状态至正在采购
/ware/purchase/received
参数:采购单ID
[1,2,3,4]
2.4.完成采购,增加库存
请求参数:
采购单ID
List<采购需求>【采购需求ID+采购需求状态】
业务逻辑
1.改变采购单状态【所有采购需求完成则采购单状态已完成,任一采购需求未完成则采购单状态异常】
2.改变采购需求状态【后期还有可能部分成功,例如采购需求数量10,实际采购数量8】
3.采购成功的采购需求,进行入库操作【在指定仓库增加商品库存,如果指定仓库不存在sku库存则新增】
{
"id": 3,
"items": [
{
"itemId": 1,
"status":3,
"reason":""
},
{
"itemId": 2,
"status":4,
"reason":"无货"
}]
}
总结
feignClient的两种写法
两种写法:
* 1.过网关,@FeignClient("gulimall-gateway"),然后所有请求前缀加/api/
* 2.不过网关,@FeignClient("gulimall-product"),请求前缀不加/api/,直接访问模块
feign远程调用失败的处理方法
1.try catch
2.TODO