商品服务
分类维护
1.三级分类
简介:使用renren-fast作为后台管理,维护商品的三级分类数据【增删改查】
1.在Entity类加上字段,代表不与数据库表映射字段@TableField(exist = false)private List<CategoryEntity> children;//子分类2.product/category/list/tree【三级分类】递归查询三级分类@Overridepublic 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 dev2、登录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.jswindow.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-fastcloud:nacos:discovery:server-addr: 127.0.0.1:88483)在Application上加上注解,将该服务注册到注册中心中@EnableDiscoveryClient4)设置网关转发规则:带负载均衡的,路径匹配转发规则例子:http://localhost:88/api/captcha.jpg http://localhost:8080/api/captcha.jpghttp://localhost:8080/renren-fast/captcha.jpg- id: admin_routeuri: lb://renren-fastpredicates:- Path=/api/**但是!正确的是还要带上项目名 server-servlet-context-path的路径,且去掉api **/最后版本:- id: admin_routeuri: lb://renren-fastpredicates:- Path=/api/**filters:- RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}5)重启renren-fast 、 gateway6)出现问题header contains multiple values 'http://localhost:8001, http://localhost:8001', but only one is allowed.配置了多个跨域,找到renren-fast的跨域,给注释掉7.在gateway中配置跨域@Configurationpublic class GulimallCorsConfiguration {/*** 跨域解决办法之一:* 过滤器,给所有请求增加请求头信息* 使得预检请求通过*/@Beanpublic 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-fastRequest URL: http://localhost:88/api/product/category/list/treeRequest Method: GETStatus Code: 404 Not FoundRemote Address: [::1]:88Referrer Policy: no-referrer-when-downgrade解决:api/product转到商品服务在gateway 配置路由规则- id: product_routeuri: lb://gulimall-productpredicates:- Path=/api/product/**filters:- RewritePath=/api/(?<segment>.*),/$\{segment}9、配置商品服务配置1)新建bootstrap.propertiesspring.application.name=gulimall-productspring.cloud.nacos.config.server-addr=127.0.0.1:8848spring.cloud.nacos.config.namespace=a152f0a8-3f55-4496-bc9a-c26df96bb2f9spring.cloud.nacos.config.group=dev#如果这个dev不放开的话,默认的gulimall-coupon不生效【会加载dev分组下的所有配置】spring.cloud.nacos.config.extension-configs[0].data-id=datasource.ymlspring.cloud.nacos.config.extension-configs[0].group=devspring.cloud.nacos.config.extension-configs[0].refresh=truespring.cloud.nacos.config.extension-configs[1].data-id=mybatis.ymlspring.cloud.nacos.config.extension-configs[1].group=devspring.cloud.nacos.config.extension-configs[1].refresh=truespring.cloud.nacos.config.extension-configs[2].data-id=spring.ymlspring.cloud.nacos.config.extension-configs[2].group=devspring.cloud.nacos.config.extension-configs[2].refresh=true2)在Nacos配置中心新建相关配置,服务发现、mybatis-plus、osshttp://127.0.0.1:8848/nacos3)开启服务注册发现功能,在Application添加注解@EnableDiscoveryClient4)重启,访问: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开通 对象存储OSS2.查看文档:常用入口=》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文件域名URLAccessKey:访问密钥读写权限:私有/公共度/公共读写服务端加密:无实施日志:不开通3.上传方式【采用方式二】方式一:文件先上传到应用服务器,然后在上传到OSS方式二:服务端签名后直传【https://www.baidu.com/index.php?tn=monline_3_dg】1)用户向应用服务器请求上传Policy2)应用服务器返回上传Policy【由服务器控制上传地址等信息】3)用户直接上传OSS4.获取子用户Accesskeys1)鼠标移至账号头像,点击Accesskeys管理,使用子用户Accesskeys【首次使用需要开通RAM访问控制】2)新增用户登录名称:gulimall-wan显示名称:gulimall访问方式:编程访问3)新增完成复制AccessKeyID和secretLTAI5t6jnyvWc34pU9BKRtwr/on5rU9Y06iNakTKMKCJ9KVOqv6OyZC4)添加权限:AliyunOSSFullAccess5)修改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.gulimallgulimall-third-party谷粒商城-第三方服务选择openFeign,SpringWeb2.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.ymlserver:port: 30000spring:application:name: gulimall-third-partycloud:nacos:discovery:server-addr: 127.0.0.1:8848alicloud:access-key: LTAI5t6jnyvWc34pU9BKRtwrsecret-key: on5rU9Y06iNakTKMKCJ9KVOqv6OyZCoss:endpoint: oss-cn-shanghai.aliyuncs.combucket: gulimall-wanlogging:level:com.atguigu.gulimall: debug5.创建OssController,返回policy凭证【获取对象签名】@RestControllerpublic class OssController {@AutowiredOSS 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.comString 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_routeuri: lb://gulimall-third-partypredicates:- 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) -> {// 获取到错误提示FieldErrorString 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*/@TableIdprivate 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) -> {// 获取错误的属性名字 + 获取到错误提示FieldErrorerrorMap.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.新增分组接口AddGroupUpdateGroupUpdateStatusGroup2.在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})@TableIdprivate 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和01.编写一个自定义的校验注解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.propertiesString message() default "{com.atguigu.common.valid.ListValue.message}";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};// 可以指定数据只能是vals数组指定的值int[] vals() default {};}3)创建message配置文件 ValidationMessages.propertiescom.atguigu.common.valid.ListValue.message=必须提交指定的值2.编写一个自定义的校验器【点击validatedBy进去查看需要一个什么类型的校验器,然后自己作实现类即可】public class ListValueConstraintValidator implements ConstraintValidator<ListValue, Integer> {private Set<Integer> set = new HashSet<>();/*** 初始化方法* @param constraintAnnotation*/@Overridepublic 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*/@Overridepublic 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)写法@Overridepublic 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*/@Overridepublic 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")@EnableTransactionManagementpublic class MybatisPlusConfig {@Beanpublic PaginationInterceptor paginationInterceptor() {PaginationInterceptor paginationInterceptor = new PaginationInterceptor();// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认falsepaginationInterceptor.setOverflow(true);// 设置最大单页限制数量,默认 500 条,-1 不受限制paginationInterceptor.setLimit(1000);// 开启 count 的 join 优化,只针对部分 left joinpaginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));return paginationInterceptor;}}2.分页查询品牌@Overridepublic 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中,并新增attrGroupId2.修改save方法中的入参为AttrVo3.修改service方法@Transactional@Overridepublic void saveAttr(AttrVo attr) {AttrEntity attrEntity = new AttrEntity();BeanUtils.copyProperties(attr, attrEntity);// 保存基本数据this.save(attrEntity);// 保存关联关系AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();relationEntity.setAttrGroupId(attr.getVttrGroupId());// 分组IDrelationEntity.setAttrId(attrEntity.getAttrId());// 属性IDrelationDao.insert(relationEntity);}
获取分类下规格参数
/product/attr/base/list/{catelogId}注意:返回参数包含catelogName/groupName,所以需要封装返回 VO,查询冗余数据@Overridepublic 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}@Overridepublic AttrRespVo getAttrInfo(Long attrId) {AttrEntity attrEntity = this.getById(attrId);AttrRespVo respVo = new AttrRespVo();BeanUtils.copyProperties(attrEntity, respVo);// 1.查询设置分组IDAttrAttrgroupRelationEntity 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.查询设置分类pathLong[] catelogPath = categoryService.findCatelogPath(attrEntity.getCatelogId());respVo.setCatelogPath(catelogPath);return respVo;}
修改
基本属性和分组是1对1的关系@Transactional@Overridepublic 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.html2.发布商品步骤// 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:sstime-zone: GMT+8
8.4.规格(SPU管理->规格)
1.spu商品规格更新逻辑1)根据spuId删除该商品所有规格2)然后根据spuId新增商品所有规格
库存服务(自营)
wms_purchase 采购单wms_purchase_detail 采购需求wms_ware_info 仓库wms_ware_order_taskwms_ware_order_task_detailwms_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,//仓库idskuId: 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, //整单iditems:[1,2,3,4] //合并项集合}
2.3.领取采购单
描述:采购人员领取采购单【可同时领取多条,并且校验是否可以被领取】1.切换采购单状态至已领取2.切换采购需求状态至正在采购
/ware/purchase/received参数:采购单ID[1,2,3,4]
2.4.完成采购,增加库存
请求参数:采购单IDList<采购需求>【采购需求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 catch2.TODO

