谷粒商城项目实战——03商品服务之品牌管理

二. 品牌管理

1. 前期工作

可以直接将代码生成器中生成的前端代码拷贝到前端页面, 从而实现简单的功能,

只需根据业务需求做相应的修改即可

1. 步骤
  1. 使用代码生成器生成相关代码

    注意代码生成器所配置时制定的数据库

  2. 将代码生成器中生成的brand.vuebrand-and-or-update.vue两个页面拷贝到项目中

    image-20210501094136258

    image-20210501094205812

2. 测试
  1. 由于生成的代码中有权限功能, 只有有权限的时候才会生成显示添加和删除等按钮, 所以我们现将权限获取全部设置为true, 后期再来验证权限

    image-20210501094508429

  2. 重新启动前端项目

  3. 简单验证

    image-20210501095217095

3. 页面优化
1. 快速显示开关
  1. 快速显示开关: 显示状态栏是通过0,1来展示是否显示, 我们需要通过按钮来显示

    <el-table-column
        prop="showStatus"
        header-align="center"
        align="center"
        label="显示状态">
        <template slot-scope="scope">
    		<!-- 
              @change: 按钮发生变化时调用的方法
              :active-value="1": 开关激活时的值
              :inactive-value="0: 开关关闭时的值
            -->
            <el-switch 
              v-model="scope.row.showStatus" 
              active-color="#13ce66" 
              @change="updateBrandStatus(scope.row)"
              :active-value="1"
              :inactive-value="0"
              inactive-color="#ff4949"> 
            </el-switch>
        </template>
    </el-table-column>
    
    <script>
    // 修改显示状态
    updateBrandStatus(data){
       console.log("最新信息: ", data)
       let {brandId, showStatus} = data
       this.$http({
       url: this.$http.adornUrl('/product/brand/update'),
       method: 'post',
       data: this.$http.adornData({brandId, showStatus}, false)
       }).then(({ data }) => { 
         this.$message({
           type: 'success',
           message: '状态更新成功'
         })
      });
    }
    </script>
    

    image-20210501105819100

  2. 添加修改页面也需要将是否显示设置为按钮, 同时需要将品牌logo地址设置为文件上传

    <el-form-item label="品牌logo地址" prop="logo">
        <el-upload class="upload-demo" action="" >
            <el-button size="small" type="primary">点击上传</el-button>
            <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div>
        </el-upload>
    </el-form-item>
    ...
    <el-form-item label="显示状态" prop="showStatus">
        <el-switch v-model="dataForm.showStatus" active-color="#13ce66" inactive-color="#ff4949"> 		</el-switch>
    </el-form-item>
    

    image-20210501110349722

2. 文件上传(OSS)
1. 说明
  1. 采用阿里云的OSS来存储文件

  2. 方式: 用户向应用服务器请求上传policy; 应用服务器返回上传policy; 用户直接上传数据到OSS

    而不是采用用户将图片传给应用服务器, 然后再由应用服务器上传到OSS, 因为会增加应用服务器负担

2. 测试
  1. 引入依赖

    <dependency>
        <groupId>com.aliyun.oss</groupId>
        <artifactId>aliyun-sdk-oss</artifactId>
        <version>3.10.2</version>
    </dependency>
    
  2. 测试代码

    @Test
    public void ossTest() throws FileNotFoundException {
        // 配置自己的accessKey信息
        String endpoint = "****************";
        String accessKeyId = "****************";
        String accessKeySecret = "****************";
    
        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
    
        // 上传文件流
        F:InputStream inputStream = new FileInputStream("F:\\测试.jpg");
        // 填写Bucket名称和Object完整路径。Object完整路径中不能包含Bucket名称。
        ossClient.putObject("gulimall-warehouse", "测试.jpg", inputStream);
    
        // 关闭OSSClient。
        ossClient.shutdown();
    
        System.out.println("上传成功");
    }
    
  3. 说明: 引入上面的依赖需要直接创建操作类, 所以引入集成好的依赖

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alicloud-oss</artifactId>
    </dependency>
    
  4. 在配置文件中配置相关信息

    image-20210501144329320

  5. 实现

    @Autowired
    OSSClient ossClient;
    
    @Test
    public void ossTest2() throws FileNotFoundException {
        // 上传文件流
        F:InputStream inputStream = new FileInputStream("F:\\测试.jpg");
        ossClient.putObject("gulimall-warehouse", "测试.jpg", inputStream);
    
        // 关闭OSSClient。
        ossClient.shutdown();
        System.out.println("上传成功");
    }
    
3. 第三方工具模块

创建一个第三方工具模块gulimall-third-party

1. maven
  1. 相关依赖

    <dependencies>
        <dependency>
            <groupId>com.hjf.gulimall</groupId>
            <artifactId>gulimall-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <!-- 不需要引入mybatis-plus相关的依赖 -->
            <exclusions>
                <exclusion>
                    <groupId>com.baomidou</groupId>
                    <artifactId>mybatis-plus-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </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>
    	<!--  oss -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alicloud-oss</artifactId>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <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>
            <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>
    
2. 配置信息
  1. bootstrap.properties

    spring.application.name=gulimall-third-party
    spring.cloud.nacos.config.server-addr=127.0.0.1:8848
    spring.cloud.nacos.config.namespace=4c9b6122-5d5d-4700-ade2-eaaf5dae222d
    
    spring.cloud.nacos.config.ext-config[0].data-id=oss.yml
    spring.cloud.nacos.config.ext-config[0].group=dev
    spring.cloud.nacos.config.ext-config[0].refresh=true
    
  2. application.yml

    spring:
      cloud:
        nacos:
          discovery:
            server-addr: 127.0.0.1:8848
        alicloud:
          access-key: LTAI5t6TkqFChXhH9WaDeP2e
          secret-key: Zsf5Ud8zd7vn3LQs0D7k36UFdVaidp
          oss:
            endpoint: oss-cn-hangzhou.aliyuncs.com
            bucket: gulimall-warehouse
      application:
        name: gulimall-third-party
    server:
      port: 15000
    
3. controller层
  1. 代码

    @RestController
    public class OssController {
        @Autowired
        OSS ossClient;
    
        @Value("${spring.cloud.alicloud.access-key}")
        private String accessId;
        @Value("${spring.cloud.alicloud.oss.endpoint}")
        private String endpoint;
        @Value("${spring.cloud.alicloud.oss.bucket}")
        private String bucket;
        
        @RequestMapping("/oss/policy")
        public Map<String, String> policy(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
            String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint
            // callbackUrl为 上传回调服务器的URL,请将下面的IP和Port配置为您自己的真实信息。
            //String callbackUrl = "http://88.88.88.88:8888";
    
            String format = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
            String dir = format + "/"; // 用户上传文件时指定的前缀。
    
            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));
            } catch (Exception e) {
                // Assert.fail(e.getMessage());
                System.out.println(e.getMessage());
            } finally {
                ossClient.shutdown();
            }
            return respMap;
        }
    }
    
4. gateway模块

配置网关路由

  1. 配置信息

    spring:
      cloud:
        gateway:
          routes:
          ...
            - id: third_party_route
              uri: lb://gulimall-third-party
              predicates:
                - Path=/api/thirdparty/**
              filters:
                - RewritePath=/api/thirdparty/(?<segment>.*),/$\{segment}
          ...
    
5. 测试
  1. localhost:15000/oss/policy

    image-20210501162253628

  2. localhost:88/api/thirdparty/oss/policy

    image-20210501162213605

4. 前后端联调
1. 组件导入
  1. 将文件夹中的upload目录拷贝到src/components

    image-20210501162822063

    image-20210501162902650

  2. 修改两个上传文件中的action地址

    image-20210501163401318

    image-20210501163452139

2. 跨域
  1. 添加和修改页面中引入组件

     <el-form-item label="品牌logo地址" prop="logo">
         <!-- <el-input v-model="dataForm.logo" placeholder="品牌logo地址"></el-input> -->
         <!--single-upload名称要和components中申明组件的名称相对于-->
         <single-upload v-model="dataForm.logo"></single-upload>
    </el-form-item>
    
    <script>
      // 导入组件
      import SingleUpload from "@/components/upload/singleUpload"
      export default {
        // 申明有哪些组件
        components: {SingleUpload},
    </script>
    
  2. 跨域问题

    此时上传文件时会出现跨域问题, 因为我们是通过客户端直接上传的

    image-20210501165744730

  3. 解决: 在阿里云oss中修改权限管理, 允许oss可以跨域访问

    image-20210501165721996

3. 表单校验
1. 步骤
  1. 给bean添加校验注解
  2. 开启校验功能: @Valid
  3. 给校验的bean后紧跟一个BindingResult, 就可以获取到校验结果
  4. 分组校验
2. 简单校验
1. 前端校验

主要校验首字母和排序两个字段

  1. 代码

    <el-form-item label="检索首字母" prop="firstLetter">
        <el-input v-model="dataForm.firstLetter" placeholder="检索首字母"></el-input>
    </el-form-item>
    <el-form-item label="排序" prop="sort">
        <el-input v-model.number="dataForm.sort" placeholder="排序"></el-input>
    </el-form-item>
    
    <script>
    // 校验规则
    dataRule: {
      firstLetter: [
        // rule: 校验规则, value: 输入的值, callback: 不满足规则时返回的方法
        { validator: (rule, value, callback) => {
           if(value == "") { // 如果为空
              callback(new Error("首字母必须填写"))
           } else if(!/^[a-zA-Z]$/.test(value)) { // 如果不是字母
              callback(new Error("请输入正确字母"))
           } else {
              callback()
           }
        }, trigger: 'blur' }
      ],
      sort: [
        { validator: (rule, value, callback) => {
           if(value == "") { // 如果为空
              callback(new Error("排序字段必须填写"))
           } else if(!Number.isInteger(value) || value < 0) { // 如果不是整数
              callback(new Error("排序字段必须是大于等于0的整数"))
           } else {
              callback()
           }
        }, trigger: 'blur' }
     ]
    }
    </script>
    
2. 后端校验

采用JSR303校验

  1. 实体内中添加注解: javax.validation.constraints, 并定义自己的message提示

    image-20210501230104422

  2. controller 方法中开启校验功能, 如果不启用, 则不会生效

    image-20210501223132605

  3. 给校验的bean后紧跟一个BindingResult, 就可以获取到校验的结果

    @RequestMapping("/save")
    public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){
        if (result.hasErrors()) {
            Map<String, String> map = new HashMap<>();
            // 1. 获取校验的错误结果
            result.getFieldErrors().forEach((item) -> {
                // 获取到错误提示
                String message = item.getDefaultMessage();
                // 获取到错误的属性名称
                String field = item.getField();
                map.put(field, message);
            });
            return R.error(400, "提交的数据不合法").put("data", map);
        } else  {
            brandService.save(brand);
        }
        return R.ok();
    }
    

    image-20210501225052846

3. 分组校验
  1. 创建分组接口, 里面不用填写任何内容, 只是起到区分的作用

    image-20210502002737330

    image-20210502002652197

  2. 修改实体类, 给校验注解标注什么情况下才需要验证; 对于没有指定分组的字段, 校验规则不会生效, 所以需要为每个字段添加分组

    @Data
    @TableName("pms_brand")
    public class BrandEntity implements Serializable {
    	private static final long serialVersionUID = 1L;
    	// 品牌id
    	@Null(message = "新增不能指定id", groups = {AddGroup.class})
    	@NotNull(message = "修改必须指定品牌id", groups = {UpdateGroup.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;
    	// 介绍
    	@NotBlank(message = "描述不能为空", groups = {AddGroup.class, UpdateGroup.class})
    	private String descript;
    	// 显示状态[0-不显示;1-显示]
    	private Integer showStatus;
    	// 检索首字母
    	@NotEmpty(groups = {AddGroup.class})
    	@Pattern(regexp = "^[a-zA-Z]$", message = "检索首字母必须是一个字母", 
    			groups = {AddGroup.class, UpdateGroup.class})
    	private String firstLetter;
    	//  排序
    	@NotNull(message = "排序不能为空", groups = {AddGroup.class})
    	@Min(value = 0, message = "排序必须大于等于0")
    	private Integer sort;
    }
    

    image-20210502003128654

  3. 在controller中指定方法的校验分组

    image-20210502004453597

4. 自定义校验
1. 步骤
  1. 编写一个自定义的校验注解
  2. 编写一个自定义的校验器
  3. 关联自定义的校验器和自定义的校验注解
2. 实现
  1. 引入依赖

    <dependency>
        <groupId>javax.validation</groupId>
        <artifactId>validation-api</artifactId>
        <version>2.0.1.Final</version>
    </dependency>
    
  2. 编写自定义校验器

    public class ListValueConstraintValidator implements ConstraintValidator<ListValue, Integer> {
        private Set<Integer> set = new HashSet<>();
        // 初始化方法
        @Override
        public void initialize(ListValue constraintAnnotation) {
            // 从注解上获取所有的值, 并将所获取的值放到set集合中
            int[] vals = constraintAnnotation.vals();
            for (int val: vals){
                set.add(val);
            }
        }
        /**
         * 是否校验成功
         * @param value 需要校验的值
         * @param context 校验的上下文环境信息
         * @return
         */
        @Override
        public boolean isValid(Integer value, ConstraintValidatorContext context) {
            return set.contains(value);
        }
    }
    
  3. 编写自定义校验注解, 并将自定义校验器和自定义的校验注解关联

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    @Constraint(validatedBy = {ListValueConstraintValidator.class})
    public @interface ListValue {
        // 可以创建配置文件ValidationMessages.properties来指定提示信息
        // 但是我的idea读取配置文件中的中文信息会乱码, 所以就在注解中添加了
        // com.hjf.gulimall.common.valid.ListValue.message=必须提交指定的值 (配置文件中的内容)
        // String message() default "{com.hjf.gulimall.common.valid.ListValue.message}";
        String message() default "";
        Class<?>[] groups() default { };
        Class<? extends Payload>[] payload() default { };
        int[] vals() default {};
    }
    

    image-20210502085010127

  4. 在实体类中使用对于的注解

    /**
     * 显示状态[0-不显示;1-显示]
     */
    @ListValue(vals = {0, 1}, message = "必须提交指定的值", groups = {AddGroup.class})
    private Integer showStatus;
    
5. bug修复
1. 原因分析
  1. 之前是将状态修改规则和表单修改方法合并在一起的, 修改状态时, 并不会传递表单中的必填项, 所以在表单校验时就会抛出异常

    image-20210502090509504

2. 解决:

将状态修改方法拆分出来, 然后再对状态修改方法重新写校验方法

  1. 新建一个检验分组

    image-20210502091236829

  2. 指定属性的验证分组

    image-20210502091333646

  3. 创建状态修改的方法

    /**
     * 状态修改
     */
    @RequestMapping("/update/status")
    public R updateStatus(@Validated({UpdateStatusGroup.class}) @RequestBody BrandEntity brand){
        brandService.updateById(brand);
        return R.ok();
    }
    
  4. 修改前端状态修改时调用的方法

    image-20210502091459670

4. 统一的异常处理
1. 说明

为每个方法编写异常处理方法, 太过麻烦, 同时会使controller层泰国复杂, 所以我们使用统一异常处理

  1. 去掉方法中的异常处理方法, 并抛出异常

    @RequestMapping("/save")
    // 不使用BindingResult参数时, 就会将异常抛出
    public R save(@Valid @RequestBody BrandEntity brand  /*, BindingResult result */){
        brandService.save(brand);
        return R.ok();
    }
    

    image-20210501231314936

2. 编写异常处理类
  1. 创建异常处理类, 集中处理各种异常

    // 日志记录
    @Slf4j
    //@ResponseBody
     指定包下出现的异常都会处理
    //@ControllerAdvice(basePackages = "com.hjf.gulimall.product.controller")
    @RestControllerAdvice(basePackages = "com.hjf.gulimall.product.controller")
    public class GulimallExceptionControllerAdvice {
    
        @ExceptionHandler(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((item) -> {
                errorMap.put(item.getField(), item.getDefaultMessage());
            });
            return R.error(BizCodeEnume.VAILD_EXCEPTION.getCode(),
                BizCodeEnume.VAILD_EXCEPTION.getMsg()).put("data", errorMap);
        }
    
        @ExceptionHandler(value = Throwable.class)
        public R handleException(Throwable throwable){
            return R.error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(),
                    BizCodeEnume.UNKNOW_EXCEPTION.getMsg());
        }
    }
    

    image-20210502000658545

  2. 创建错误码枚举类

    public enum  BizCodeEnume {
        UNKNOW_EXCEPTION(10000, "系统未知异常"),
        VAILD_EXCEPTION(10001, "参数格式化失败");
        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;
        }
    }
    

    image-20210502000808371

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值