谷粒商城笔记

8 篇文章 0 订阅

项目概览

创建项目

数据库

模块

第三方模块renren-fast

代码生成

分布式

注册中心nacos

  • namespace
  • group
  • data-id

通信openfeign

网关Gateway

修改路由,对应前端项目

跨域请求

方案

通过NGINX可以将前后端变成同一个域

添加一个bean

@Configuration
public class GuliMallCorsConfiguration {

    @Bean
    public CorsWebFilter corsWebFilter(){
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.setAllowCredentials(true);
        source.registerCorsConfiguration("/**", corsConfiguration);

        return new CorsWebFilter(source);
    }
}

功能

逻辑删除

mybatis-plus配置一下

mybatis-plus:
  global-config:
    db-config:
      logic-delete-value: 0
      logic-not-delete-value: 1
      logic-delete-field: show-status

在entity中的对应字段上添加@TableLogic注解
然后就可以直接调用basemapperdelete方法

逻辑删除的方法是在com.baomidou.mybatisplus.core.injector.methods包里面设置的
我们通过log可以看到在查询的时候,默认是有where logic-delete-field = logic-not-delete-value的条件

添加/修改

mybatis-plus中的IService中有saveOrUpdate方法,他是根据主键判断是save还是update
卧槽,本来是觉得pms_category表里的name不能重复的,后来发现可以重复的,所以直接用saveorupdate就行了

但是因为没什么用,我们的update代码和insert代码没有多少重复的地方,所以我们还是选择用两个函数去实现,就不用saveOrUpdate

修改showStatus

showStatus是我设置的@TableLogic字段,所以mbp在自动生成MappedStatement的时候,是这样的SQL:

<script>
UPDATE pms_brand <set>
<if test="et['name'] != null">name=#{et.name},</if>
<if test="et['logo'] != null">logo=#{et.logo},</if>
<if test="et['descript'] != null">descript=#{et.descript},</if>
<if test="et['firstLetter'] != null">first_letter=#{et.firstLetter},</if>
<if test="et['sort'] != null">sort=#{et.sort},</if>
</set> WHERE brand_id=#{et.brandId}  AND show_status=1
</script>

可以在MapperMethod里的SqlCommandConstructor中找到MappedStatement

这里面作为@TableLogic的字段没有放在可修改的字段中,所以如果要单纯修改showStatus的话,只传入id,showStatus产生的SQL语句就会发生以下的错误:
UPDATE pms_brand WHERE brand_id=1 AND show_status=1

就尼玛离谱

为什么我们要单独修改showStatus他本身就是个逻辑删除字段,修改他就相当于删除他或恢复他

前端的bind

很多东西要v-bind 或者 :,因为这样子是双向绑定,数据改变了,对应的DOM也会改变,否则就是单相绑定,数据改变了,不会使DOM也跟着改变

拖拽

我们有要求:总共只有三层分类级别,所以要判断是否能拖拽到这个地方。
就是要遍历被拖拽的节点,最深有多少层,然后判断能否拖拽到目的节点
前端实现方法可以参考element-uitreedraggable属性

我的想法

在后端用同一个方法update
参数有catId,parentCid,catLevel,sort

哦哦 ,老师的想法

他是想把顺序就按照拖拽后的顺序精确排序
他遍历了新的位置的所有节点,然后按照该顺序全部重新设置sort

要注意的一些地方:
传过去的是一个列表
dragNode要传过去的数据有:catId,sort,parentCid,catLevel, productCount
siblings要传过去的就是catId,sort ,productCount

漏了个:
如果draggingNodecatLevel发生变化,那他的子节点的catLevel也要变化

批量拖拽

因为是批量拖拽,我们拖来拖去,其中的data没有实时更新,点击批量拖拽后才更新,所以我们应该使用node而不是data

这样子会导致有很多的重复更新,所以要不设置一个阈值,超过这个阈值的时候,我们的点击批量拖拽后,直接将当前的整个tree里的catId,sort,catLevel等等数据提交上去,而不是提交一个一直再增长的listForm

批量删除

MBP的SQL

由于批量删除我想着直接传一个Class过去能不能行,结果说我传的类型不对,不符合SQL语句
我就想找找看SQL语句在哪里

通过debug呢,我们发现SQL语句是在项目启动的时候就创建好的,在com.baomidou.mybatisplus.core.injector.methods.Insert之类的方法中,有injectMappedStatement方法,最后通过String.format方法来得到SQL语句,并由此创建SqlSource然后创建MappedStatement

运行的时候,在找preparedStatement的时候发现,还会有@Deprecated的class,com.baomidou.mybatisplus.core.executor.MybatisSimpleExecutor,存在于类似的类下面。各个方法中有stmt变量

分页查询

原本我看到有个page方法,我就直接调用了Page<PmsBrand> page = brandService.page(new Page<PmsBrand>(Long.parseLong((String) params.get("current")), Long.parseLong((String) params.get("size"))))方法,结果却发现SQL语句只是单纯的select,没有什么条件
看了文档发现要添加配置:

//Spring boot方式
@Configuration
@MapperScan("com.baomidou.cloud.service.*.mapper*")
public class MybatisPlusConfig {

    // 旧版
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求  默认false
        // paginationInterceptor.setOverflow(false);
        // 设置最大单页限制数量,默认 500 条,-1 不受限制
        // paginationInterceptor.setLimit(500);
        // 开启 count 的 join 优化,只针对部分 left join
        paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
        return paginationInterceptor;
    }
    
    // 最新版
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
        return interceptor;
    }
    
}

page里的参数就是current 当前页,size 页显示数,total 总数,

云存储

使用阿里云的OSS服务

逻辑

将文件上传给阿里云的方式:

  • 先上传给服务器,再由服务器将流上传给阿里云(太慢了)
  • 直接将阿里云的地址和密钥写在js,前端直接上传给阿里云(不安全)
  • 上传给阿里云,但是由服务器来进行签名(选用)

我们选择第三种,如何实现,就是用户先向前端发送上传文件请求,前端再向后端要签名,签名就是服务器根据账号密码而生成的一串东西,然后前端带着我们的防伪签名和文件直接提交给阿里云

实现

后端实现

接收前端的强求,返回给他签名:

@RequestMapping("/policy")
    public R policy(){
//        String accessId = "<yourAccessKeyId>"; // 请填写您的AccessKeyId。
//        String accessKey = "<yourAccessKeySecret>"; // 请填写您的AccessKeySecret。
        String bucket = "mine-gulimall"; // 请填写您的 bucketname 。
        String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint
        // callbackUrl为 上传回调服务器的URL,请将下面的IP和Port配置为您自己的真实信息。
        String callbackUrl = "http://88.88.88.88:8888";
        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);
            // 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(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));
            // respMap.put("expire", formatISO8601Date(expiration));


        } catch (Exception e) {
            // Assert.fail(e.getMessage());
            System.out.println(e.getMessage());
        } finally {
            ossClient.shutdown();
        }
        return R.ok().put("map", respMap);
    }

前端实现

使用element-uiel-upload组件

<el-upload
      class="upload-demo"
      action="https://mine-gulimall.oss-cn-shanghai.aliyuncs.com/"
      :data="dataObj"
      list-type="picture"
      :on-preview="handlePreview"
      :on-remove="handleRemove"
      :before-remove="beforeRemove"
      :before-upload="beforeUpload"
      :limit="1"
      :multiple="false"
      :on-exceed="handleExceed"
      :on-success="handleSuccess"
      :file-list="fileList">
      <el-button size="small" type="primary">Click to upload</el-button>
      <div slot="tip" class="el-upload__tip">jpg/png files with a size less than 500kb</div>
</el-upload>

然后在父组件中使用他:

 import multiUpload from '@/components/upload/multiUpload'
      <el-form-item label="品牌logo地址" prop="logo">
        <multi-upload v-model="dataForm.logo" placeholder="品牌logo"></multi-upload>
      </el-form-item>

这里的v-model我们要通过在子组件中设置 this.$emit('input', file.url),将需要传输的数据交给父组件。就设置在onSuccess中:

      handleSuccess (response, file, fileList) {
        console.log('response: ', response)
        console.log('file: ', file)
        console.log('fielist: ', fileList)
        this.$emit('input', file.url)
      },

获取文件路径

上传Object后,如果没有设置callbackUrl的话,oss服务器不会返回任何信息,只有个 200 OK,而如果要设置callbackurl的话,就是让oss服务器向这个callbackurl发送post请求,请求体是我们向oss服务器发送的callbackbody里面要求的信息。然后再由callbackurl对应的服务器将信息发给我们的前端。
这里面的问题就是,我们的callbackurl不能是本地地址,因为发个本地地址,oss服务器也访问不到,那么我在测试的时候,就不能通过这种方法来得到文件路径了。
于是只能自己根据文件路径名逻辑去编码出来了:
https://mine-gulimall.oss-cn-shanghai.aliyuncs.com/2021-04-01/01ca15a0-1201-42e6-8601-8f2edf55bb0dcungu.jpeg
也就是this.action + this.dataObj.key

Validation

在前端我们需要通过验证请求的合法性,同样,在后端我们也要验证
后端的验证我们需要添加

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-validation</artifactId>
		</dependency>

原本我们添加

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
</dependency>

就够了,但是在新版的spring boot中,只添加validation-api是不够的,因为这个只提供了JSR的规则,没有提供实现类

ExceptionHandler

不知道为什么我的默认异常处理很普通。我也懒得debug,直接搜了个处理方案

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Map<String, String> handleValidationExceptions(
            MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return errors;
    }

ExceptionHandler是spring boot的包里的类。他会检查到MethodArgumentNotValidException异常,并执行这个类

还是不行

发现自定义这个处理类后,只有一点点的错误信息,不希望每个错误都要自己写,想要用封装好的,然后发现封装好的错误信息里面message字段为空。通过debug,发现:
org.springframework.web.servlet.DispatcherServlet.processDispatchServlet方法中,有个mv = this.processHandlerException(request, response, handler, exception);,这个方法搜寻handlerExceptionResolvers,如果我们没有定义的话,搜寻到最后,调用的是HandlerExceptionResolverComposite,然后再去HandlerExceptionResolverComposite找到handlerExceptionResolver,有三个,其他的返回为null所以取消,DefaultHandlerExceptionResolver有返回,他的处理仅仅是:

    protected ModelAndView handleMethodArgumentNotValidException(MethodArgumentNotValidException ex, HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException {
        response.sendError(400);
        return new ModelAndView();
    }

所以我们的ex参数根本没有用到,也就是没有返回详细的错误信息。

自定义了ExceptionHandler

他有个ExceptionHandlerMethodResolver,这里会找到我们自定义的Method。

这段代码差点没看懂

    public static Set<Method> selectMethods(Class<?> targetType, MethodFilter methodFilter) {
        return selectMethods(targetType, (method) -> {
            return methodFilter.matches(method) ? Boolean.TRUE : null;
        }).keySet();
    }

是表示第二个selectMethods接收的第二个参数是一个接口,我们传入的方法是这个接口的重写方法,也可以说就是这个接口的匿名实现类。
不愧是我,没有记笔记也能及时反应过来

解决

哦行吧,是新版的spring-boot-autoconfigure包的问题,debug到最后,他是交给了BasicErrorController,他会根据this.errorProperties的属性来确定最后的错误返回信息包含什么,这个ErrorProperties类是在启动项目的时候就完成了的,他默认message属性为NEVER.所以我试试看在application.properties中设置,结果就成功了,server.error.include-message=always。当然也有on-param这个选项,不过我也不知道传的param有什么要求,还有怎么传。反正目前传入的parameters是没有。

统一异常处理

就如一开始写的一样。通过添加@ExceptionHandler可以将该方法放到HandlerExceptionResolverComposite,然后可以对异常进行处理。
我们把异常处理方法放到com.joiller.gulimall.product.exception.PmsExceptionHandler中,

@RestControllerAdvice
public class PmsExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public R argumentNotValid(MethodArgumentNotValidException e){
        return R.error(BizCodeEnum.VALID_EXCEPTION).put("error", e.getMessage());
    }

    @ExceptionHandler(Throwable.class)
    public R otherExcption(Throwable throwable) {
        return R.error(BizCodeEnum.UNKOWN_EXCEPTION).put("error", throwable.getMessage());
    }
}

这里的BizCodeEnum是自己额外添加的Enum:

@AllArgsConstructor
@Getter
public enum BizCodeEnum {
    UNKOWN_EXCEPTION(10000, "未知异常"),
    VALID_EXCEPTION(10001, "参数格式异常");

    private int code;
    private String message;

}

然后给R.error()方法添加一个BizCodeEnum的参数overload

分组校验

@Valid是一个空注解,
@Validated接收一个Class<?>[]作为参数。
在分组校验中,我们可以使用@Validated进行分组
只要创建一个空接口,就可以当做一个group。
在字段的校验要求上添加@NotBlank(groups = {UpdateGroup.class, AddGroup.class})就是说这个校验是在{UpdateGroup.class, AddGroup.class}这两个组中,当@Validated()中包含了这两个组的其中一个,那才会被校验到。如果@Validated不添加参数,那么只有无组别的校验才会被校验到。
比如说:@Validated对应@NotBlank@Validated(UpdateGroup.class)对应@NotBlank(groups = {UpdateGroup.class, AddGroup.class})

自定义校验

我们要自己写个校验注解
然后给这个校验注解写个处理类
然后关联一下。
直接参照现有的校验注解去实现:

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
//@Repeatable(ListValue.List.class)
@Documented
@Constraint(
        validatedBy = {ListIntegerValidator.class}
)
public @interface ListValue {
    String message() default "{com.joiller.gulimall.common.valid.ListValue.message}";

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

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

    int[] value();
}

这里的message属性是引用了ValidationMessages.properties里的属性。我们也创建一个这个ValidationMessages.properties文件,在其中添加一个关于我们注解的属性:

com.joiller.gulimall.common.valid.ListValue.message = must in list {value}

然后创建一个处理类:

public class ListIntegerValidator implements ConstraintValidator<ListValue, Integer> {

    HashSet<Integer> set = new HashSet<>();

    @Override
    public void initialize(ListValue constraintAnnotation) {
        int[] value = constraintAnnotation.value();
        for (int i : value) {
            set.add(i);
        }
    }

    @Override
    public boolean isValid(Integer integer, ConstraintValidatorContext constraintValidatorContext) {
        return set.contains(integer);
    }
}

initialize方法就是初始化,接收这个注解,通过这个注解,我们将注解中的数据得到。
然后在isValid方法中判断数据的合法性。第一个参数就是这个注解的Target所传过来的参数,我们要判断这个参数是否满足我们的注解参数value
要注意判断integer是否为null。因为当我们这个字段没有得到 参数的时候,也会进行校验,这个时候在isValid方法中发生了NullPointerException,就会抛出新的错误,然后在我们这个项目中会交给我们写的统一异常处理中的

   @ExceptionHandler(Throwable.class)
   public R otherExcption(Throwable throwable) {
       return R.error(BizCodeEnum.UNKOWN_EXCEPTION).put("error", >throwable.getMessage());
   } 

去处理

完整的首页菜单

首页菜单是根据gulimall_admin.sys_menus表格建立的,通过教程给的代码,生成了这个表格,然后可以看见首页的菜单增加了。

商品系统

品牌管理

spu (Standard Product Unit)和 sku(stock keeping unit)
电商里的概念
类目: 有手机 => 智能手机 => 苹果XS
SPU: 就是商品聚合信息的最小单位,就是一款产品他的公有信息,比如苹果XS
SKU: 就是具体的,确定的一种商品。比如在官网买苹果XS 的时候需要选择各种各样的属性,就是在确定具体哪一款,当然不包含商品序列号,因为SKU不是确定的一件,而是一款

属性分组

分页查询
前端

通过renren-generator生成前端,不过我们的需要添加一个组件,那就是common.category.vue,我们要求每个页面都是,左边是category,右边是具体信息。
这就需要我们在组件中引用category.vue了,利用Vue的$emit实现父子组件的交互

后端

这里我希望对分页查询作一个泛化?我也不确定这种说法对不对。
Service中添加方法:
public <E extends IPage<PmsAttrGroup>> E page(E page, String key, Integer catelogId)
主要问题在Page这个类,他有多个Constructor,可以接收这么多的参数:

    public Page(long current, long size) {
        this(current, size, 0L);
    }

    public Page(long current, long size, long total) {
        this(current, size, total, true);
    }

    public Page(long current, long size, boolean isSearchCount) {
        this(current, size, 0L, isSearchCount);
    }

    public Page(long current, long size, long total, boolean isSearchCount) {
    	...
    }

关键这些参数还都是primitive,这样子就不能传入null值了,否则会发生NullPointerException

controller传入的参数不一定全都有,就不确定选择哪一个Constructor了。现在我想到的解决方案:

  • controller中做if判断
  • 创建一个新的PageUtils类,在PageUtils类中做判断
  • 创建新的PageUtils类,给参数作默认值初始化,然后有值的赋值,调用全参构造

选择了第三个方案

public class PageUtils<T> {

    long total = 0L;
    long current = 1L;
    long size = 10L;
    boolean isSearchCount = true;

    public Page<T> page(Map<String, String> map) {
        if (map.get("current") != null) {
            current = Long.parseLong(map.get("current"));
        }
        if (map.get("size") != null) {
            size = Long.parseLong(map.get("size"));
        }
        if (map.get("total") != null) {
            total = Long.parseLong(map.get("total"));
        }
        if (map.get("isSearchCount") != null) {
            isSearchCount = Boolean.parseBoolean(map.get("isSearchCount"));
        }
        return new Page<>(current, size, total, isSearchCount);
    }
}
返回完整category路径

因为在前端使用的cascader标签是要接收一个数组,里面包含依照父子关系从左至右的数组。
我们在后端选择通过categoryService来获取数组,并且在attrBrandGroup中添加一个字段来接收这个数组:

@Service
public class PmsCategoryServiceImpl extends ServiceImpl<PmsCategoryMapper, PmsCategory> implements IPmsCategoryService {
	...
    @Override
    public List<Serializable> getPath(Serializable id) {
        return getPath(id, new LinkedList<>());
    }

    private List<Serializable> getPath(Serializable id, LinkedList<Serializable> path){
        path.addFirst(id);
        PmsCategory temp = this.getById(id);
        if (temp.getParentCid()==0){
            return path;
        }
        return getPath(temp.getParentCid(), path);
    }
}

@Data
@EqualsAndHashCode(callSuper = false)
public class PmsAttrGroup implements Serializable {
    @TableField(exist = false)
    private List<Serializable> catelogPath;
}

品牌分类关联与级联更新

品牌的查询的key字段也加上先

关系

一个品牌关联多个分类,一个分类下有多个品牌,所以是Many to Many表格:pms_category_brand_relation表格。

API

其他表的修改

因为在关联表中有brand_name,catelog_name字段,所以当这两个改变的时候,关联表要同步改变。
我们在这两个表进行update的时候,判断是否更新name字段,如果更新了,那么就将关联表中的name也更新。

有多种改法
可以在category service中调用relation service,然后创建relation entity通过relation service.update,将relation entityupdate wrapper传入。
或者可以在category service中调用relation service,然后创建新的relation service.updateDetailupdateDetail接收category类。

属性与属性分类的关联

attrattr_attrgroup
attr.save的时候,我们要在attr_attrgroup_relation中也添加相应的字段

    @Override
    public boolean save(AttrVo attrVo) {
        PmsAttr attr = new PmsAttr();
        BeanUtils.copyProperties(attrVo, attr);
        this.save(attr);
        PmsAttrAttrgroupRelation relation = new PmsAttrAttrgroupRelation();
        relation.setAttrId(attr.getAttrId());
        relation.setAttrGroupId(attrVo.getAttrGroupId());
        relation.setAttrSort(0);
        relationService.save(relation);
        return true;
    }

这里的VO是Value Object, 是一种概念,类似于DAO
VO在这里专门用来对前端的请求数据和后端的返回数据进行转换的。
因为我们的DAO经常不能满足前端的请求

这里我们准备在relation表格中添加新增的attr_id的时候,可以用DAO的attr.getAttrId(),刚刚save好了,这里的attrId也会变成相应的id

功能
  • 根据分组查询该分组下的属性
    • 点击新增弹出可以添加至该分组的属性
  • 添加属性时选择分组

逻辑较为简单,技术里用到了有mapper.xml的模板技巧,通过@Param("pname")来给mapper中的语句传参:

public interface PmsAttrAttrgroupRelationMapper extends BaseMapper<PmsAttrAttrgroupRelation> {

    boolean deleteBatchRelations(@Param("relations") PmsAttrAttrgroupRelation[] relations);
}
   <delete id="deleteBatchRelations">
        DELETE FROM `pms_attr_attrgroup_relation` WHERE
        <foreach collection="relations" item="item" separator=" OR ">
            attr_id = #{item.attrId} and attr_group_id = #{item.attrGroupId}
        </foreach>
    </delete>

前端的配置

关于vue cli中修改alias, 就是经常见到@这个符号,这个其实在@vue/cli也就是3.x的vue cli中,可以在root path 下创建vue.config.js,然后

module.exports = {
  resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      'vue$': 'vue/dist/vue.esm.js',
      '@': resolve('src'),
    }
  },
}

然而我们这个是老的vue cli,所以不会解析vue.config.js,取而代之是会出现可见的webpack.base.conf.js, webpack.dev.conf.js , webpack.prod.conf.js,其中base是会在devprod环境下都解析的。我们只需要把上面的东西放入到对应的webpack.conf.js中就行了。
是的,在新的vue cli中,这些webpack配置会隐藏

业务逻辑规范

这个老师说是controller里面最好分三部分:

  • 处理请求,接收和校验数据
  • 调用service,让service进行业务处理
  • 接收service的数据,封装页面指定的vo

商品维护

发布商品

页面1

进入页面会请求/member/memberlevel/list会员等级。第一个页面的选择分类有watcher,当catelogPath发生变化的时候,请求得到该分类下的品牌,/product/categorybrandrelation/brands/list

页面2

进入页面会请求/product/attrgroup/{catId}/withattr,得到该三级分类下的所有分组,并且每个分组也要带上该分组下的所有属性

页面3

没什么要请求的,就是制定一些其他的属性。指定好后就会将数据打包成JSON发送给product/spuinfo/save,数据比较多,我们将JSON复制到在线JSON处理工具中,自动生成JAVA BEAN,就会方便很多

自动生成

自动生成的Javabean的type一般不准,我们需要手动修改这些type

调用其他微服务

这个页面生成了很多数据,有些数据不在product数据库中,而在其他的数据库中,其他的数据库我们用的是其他的微服务。这时候就需要利用openfeign来调用其他的服务了。
调用逻辑主要是:

  • 将自己的请求数据转换为json
  • 找到对方服务,给对方的网络接口发送请求,将上一步转换的json放在请求体中
  • 对方服务收到请求,处理json数据

所以我们的请求体和对方接收的请求体不一定需要是同一个类,因为只要都能处理相同的json即可

怎么实现呢:
首先要各种enable,然后在自己的微服务中创建一个接口:

@FeignClient("gulimall-coupon")
public interface CouponFeignService {

    @PostMapping("/coupon/sms-spu-bounds/save")
    R saveSpuBounds(SpuBoundsTo spuBoundsTo);

    @PostMapping("/coupon/sms-member-price/saveBatch")
    R saveSkuMemberPrices(List<SkuMemberPriceTo> skuMemberPriceTos);

    @PostMapping("/coupon/sms-sku-full-reduction/save")
    R saveSkuFullReduction(SkuFullReductionTo skuFullReductionTo);
}

然后在相应的地方调用这个接口bean就可以了

Debug

由于这个业务很长,涉及很多的代码和数据库操作,难免会有bug,所以我们应该开启debug模式,一步一步的对照数据库
注意由于这个是一个事务,那么在事务结束前是不会commit的,如果想要看到没有commit的数据,就要执行以下的SQL语句:

Syntax Affected CharacteristicScope
SET GLOBAL TRANSACTION transaction_characteristicGlobal
SET SESSION TRANSACTION transaction_characteristicSession
SET TRANSACTION transaction_characteristicNext transaction only

这边发现个bug,就是该service添加了@Transcational注解,但是他也调用了openfeignopenfeign调用的方法又不归这个springboot application管,所以要如何令其他的微服务也和我们保持同一个transaction,就是一个问题了,以后再说吧,老师也没说。

(D)TO (Data) Transer Object

类似于VO,DAO, 他就是个用来传输数据的,主要用在微服务的交互上。‘
不过我网上搜的各种用法和这个项目不一样,反正没什么难的,到时候具体项目具体规定吧。

继承父类后lombok.EqualsAndHashCode

这个注解是为我们的类添加equalshashCode方法的,通过@EqualsAndHashCode.Exclude可以将指定field不被这两个方法考虑。设置@EqualsAndHashCode(callSuper = true)就可以让这两个方法将父类的字段也考虑进去。但是如果没有继承什么父类的话(继承了Object),将callSuper=true会是一个compile-time error,也就是不会运行。因为这样子做的话,equalshashCode会和父类一样,也就是只有是同一个对象的情况下才会equal才会有相同的hashCode

循环依赖

当我们多个service互相调用会出现这种问题,所以最好还是controller调用serviceservice调用mapper。同一层的东西不要互相调用。
不过spring会解决这个问题,首先开启allowCircularReferences,那么spring会有两个池子,一个成品池子,一个半成品池子,在注入bean,向容器获取bean的时候,会优先向成品池子要,要不到,然后再问半成品池子要。
这里再说一下。

  • Autowired是bytype,是构造注入
  • Resource是byname, 是setter注入

spu管理

分页查询

返回的时间戳,我们可以通过配置spring.jackson.date-format来格式化json返回的日期格式
…歪日我怎么不行? 我先上床了

库存系统

分页插件

这边忘了件事情,发现调用mybatis-pluspage方法的时候,select语句没有应用到page里的东西,原来是分页插件忘记了,在官方文档里就有。不过问题是我想把他放到common里,但是由于他是springboot compopentspringboot只会找到自己主启动类同级的包,所以也就不方便找到common里的配置。如果给springboot配置一下component位置的话,也要给每个微服务都配置,现在就是想,如果可以在配置文件中配置component位置,那么就可以让每个微服务都有一个共享的配置文件,在这个配置文件上添加component位置。

logging

之前的product微服务是用的logback来配置日志的:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml" />
<!--    <include resource="org/springframework/boot/logging/logback/file-appender.xml" />-->
    <root level="DEBUG">
        <appender-ref ref="CONSOLE" />
<!--        <appender-ref ref="FILE" />-->
    </root>
    <logger name="com.joiller.gulimall.product.mapper" level="DEBUG"/>
</configuration>

而spring boot给我们提供了简单点的方法,直接在配置文件中添加:

logging:
  level:
    com.joiller: debug

logging.level接收一个map

仓库维护

分页查询

带上key参数

商品库存

分页查询

采购单维护

逻辑

先有采购需求,然后合成为采购单,然后执行采购单,然后就会增加商品库存

合并到整单

采购需求: purchase_detail
采购单: purchase
就是将采购需求分配到采购单中,就是更新采购需求里的采购单id
如果没有指定采购单,就新建一个

领取/完成采购单

采购员领取采购单,采购单状态变为已领取,而且已领取的采购单中的采购需求的状态变为正在采购,也不能再被分配到别的采购单。
完成采购单,将采购单和采购需求状态变为已完成

这两个功能不是后台管理系统的功能,需要从postman来发送请求。

完成采购单:
除了更新采购单和采购需求的状态外,还要添加库存。
requestbody:

{
    "id": 5,
    "items": [
        {
            "itemId": 8,
            "status": 3,
            "reason": ""
        },
        {
            "itemId": 9,
            "status": 4,
            "reason": "没货"
        }
    ]
}

所以我们再添加VO:

@Data
public class PurchaseDoneVo {
    @NotNull
    private Integer id;
    private List<PurchaseItemDoneVo> items;

}

@Data
public class PurchaseItemDoneVo {
    @NotNull
    private Long itemId;
    private Integer status;
    private String reason;
}

添加库存就需要这几个参数: 将哪个sku添加到哪个ware多少stock个
sku_id,ware_id,stock

基础篇 的一些BONUS

做完准备再给product添加一个spu管理规格功能的时候,发现根本无法访问,弹出来是404,原来是前端router.index.js向后端发送/sys/menu/nav的请求,得到所有可以访问的路径,然后其他路径都是转向给404,所以根据后端该路径,可以找到是数据库gulimall_admin.sys_menu这里出现了遗漏,这个数据应该是可以通过前端页面的菜单管理功能添加的,但是我才不搞那么麻烦,直接插入数据库就完事儿了,反正不是我写的,有什么bug也无所谓。

又添加了个规格更新功能,这个是更新对应spu的各个属性,逻辑居然是把原本的属性记录全部delete,再将新的数据全部insert。因为这里要update的是多个,需要执行多次SQL,而insert就可以只执行一次了。

基础篇的总结

分布式基础概念

微服务, 注册中心, 配置中心, 远程调用, 网关

基础开发

springboot2.0, springcloud, mybatis-plus, Vue组件化, 阿里云对象存储oss

环境

Linux, docker, MySQL, redis, 逆向工程, 人人开源

开发规范

数据校验JSR303 , 全局异常处理, 全局统一返回, 全局跨域处理
枚举状态/业务状态码,VO/TO/PO的划分,逻辑删除
Lombok @Data, @Slf4j

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值