• 谷粒商城分布式基础(七)—— 商品服务API—品牌管理 (oss云存储 & 前端校验器 & 后端JSR303校验)


    一、商品服务API—品牌管理

    1、sql脚本

    DROP TABLE IF EXISTS `pms_brand`;
    CREATE TABLE `pms_brand` (
      `brand_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '品牌id',
      `name` char(50) DEFAULT NULL COMMENT '品牌名',
      `logo` varchar(2000) DEFAULT NULL COMMENT '品牌logo地址',
      `descript` longtext COMMENT '介绍',
      `show_status` tinyint(4) DEFAULT NULL COMMENT '显示状态[0-不显示;1-显示]',
      `first_letter` char(1) DEFAULT NULL COMMENT '检索首字母',
      `sort` int(11) DEFAULT NULL COMMENT '排序',
      PRIMARY KEY (`brand_id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COMMENT='品牌';
    
    -- ----------------------------
    -- Records of pms_brand
    -- ----------------------------
    INSERT INTO `pms_brand` VALUES ('9', '华为', 'https://gulimall-hello.oss-cn-beijing.aliyuncs.com/2019-11-18/de2426bd-a689-41d0-865a-d45d1afa7cde_huawei.png', '华为', '1', 'H', '1');
    INSERT INTO `pms_brand` VALUES ('10', '小米', 'https://gulimall-hello.oss-cn-beijing.aliyuncs.com/2019-11-18/1f9e6968-cf92-462e-869a-4c2331a4113f_xiaomi.png', '小米', '1', 'M', '1');
    INSERT INTO `pms_brand` VALUES ('11', 'oppo', 'https://gulimall-hello.oss-cn-beijing.aliyuncs.com/2019-11-18/5c8303f2-8b0c-4a5b-89a6-86513133d758_oppo.png', 'oppo', '1', 'O', '1');
    INSERT INTO `pms_brand` VALUES ('12', 'Apple', 'https://gulimall-hello.oss-cn-beijing.aliyuncs.com/2019-11-18/819bb0b1-3ed8-4072-8304-78811a289781_apple.png', '苹果', '1', 'A', '1');
    

    2、使用逆向工程的前后端代码

    1)添加菜单“品牌管理”
    2)将“”逆向工程得到的 resources\src\views\modules\product 文件拷贝到 gulimall/renren-fast-vue/src/views/modules/product目录下,也就是下面的两个文件
      brand.vue brand
    -add-or-update.vue
      添加了新的组件, 需要停掉当前项目,并且 npm run dev 重启一下项目
      但是显示的页面没有新增和删除功能,这是因为权限控制的原因

      查看“isAuth”的定义位置:

      它是在“index.js”中定义,暂时将它设置为返回值为true,即可显示添加和删除功能。

      再次刷新页面能够看到,按钮已经出现了:

      进行添加 测试成功, 进行修改 也会自动回显

      build/webpack.base.conf.js 中注释掉createLintingRule()函数体,不进行lint语法检查

      注释后需要重启vue项目 

    3、效果优化与快速显示开关

    brand.vue

    <template>
      <div class="mod-config">
        <el-form
          :inline="true"
          :model="dataForm"
          @keyup.enter.native="getDataList()"
        >
          <el-form-item>
            <el-input
              v-model="dataForm.key"
              placeholder="参数名"
              clearable
            ></el-input>
          </el-form-item>
          <el-form-item>
            <el-button @click="getDataList()">查询</el-button>
            <el-button
              v-if="isAuth('product:brand:save')"
              type="primary"
              @click="addOrUpdateHandle()"
              >新增</el-button
            >
            <el-button
              v-if="isAuth('product:brand:delete')"
              type="danger"
              @click="deleteHandle()"
              :disabled="dataListSelections.length <= 0"
              >批量删除</el-button
            >
          </el-form-item>
        </el-form>
        <el-table
          :data="dataList"
          border
          v-loading="dataListLoading"
          @selection-change="selectionChangeHandle"
          style=" 100%"
        >
          <el-table-column
            type="selection"
            header-align="center"
            align="center"
            width="50"
          >
          </el-table-column>
          <el-table-column
            prop="brandId"
            header-align="center"
            align="center"
            label="品牌id"
          >
          </el-table-column>
          <el-table-column
            prop="name"
            header-align="center"
            align="center"
            label="品牌名"
          >
          </el-table-column>
          <el-table-column
            prop="logo"
            header-align="center"
            align="center"
            label="品牌logo地址"
          >
          </el-table-column>
          <el-table-column
            prop="descript"
            header-align="center"
            align="center"
            label="介绍"
          >
          </el-table-column>
          <el-table-column
            prop="showStatus"
            header-align="center"
            align="center"
            label="显示状态"
          >
            <template slot-scope="scope">
              <el-switch
                v-model="scope.row.showStatus"
                active-color="#13ce66"
                inactive-color="#ff4949"
                :active-value="1"
                :inactive-value="0"
                @change="updateBrandStatus(scope.row)"
              ></el-switch>
            </template>
          </el-table-column>
          <el-table-column
            prop="firstLetter"
            header-align="center"
            align="center"
            label="检索首字母"
          >
          </el-table-column>
          <el-table-column
            prop="sort"
            header-align="center"
            align="center"
            label="排序"
          >
          </el-table-column>
          <el-table-column
            fixed="right"
            header-align="center"
            align="center"
            width="150"
            label="操作"
          >
            <template slot-scope="scope">
              <el-button
                type="text"
                size="small"
                @click="addOrUpdateHandle(scope.row.brandId)"
                >修改</el-button
              >
              <el-button
                type="text"
                size="small"
                @click="deleteHandle(scope.row.brandId)"
                >删除</el-button
              >
            </template>
          </el-table-column>
        </el-table>
        <el-pagination
          @size-change="sizeChangeHandle"
          @current-change="currentChangeHandle"
          :current-page="pageIndex"
          :page-sizes="[10, 20, 50, 100]"
          :page-size="pageSize"
          :total="totalPage"
          layout="total, sizes, prev, pager, next, jumper"
        >
        </el-pagination>
        <!-- 弹窗, 新增 / 修改 -->
        <add-or-update
          v-if="addOrUpdateVisible"
          ref="addOrUpdate"
          @refreshDataList="getDataList"
        ></add-or-update>
      </div>
    </template>
    
    <script>
    import AddOrUpdate from "./brand-add-or-update";
    export default {
      data() {
        return {
          dataForm: {
            key: "",
          },
          dataList: [],
          pageIndex: 1,
          pageSize: 10,
          totalPage: 0,
          dataListLoading: false,
          dataListSelections: [],
          addOrUpdateVisible: false,
        };
      },
      components: {
        AddOrUpdate,
      },
      activated() {
        this.getDataList();
      },
      methods: {
        // 获取数据列表
        getDataList() {
          this.dataListLoading = true;
          this.$http({
            url: this.$http.adornUrl("/product/brand/list"),
            method: "get",
            params: this.$http.adornParams({
              page: this.pageIndex,
              limit: this.pageSize,
              key: this.dataForm.key,
            }),
          }).then(({ data }) => {
            if (data && data.code === 0) {
              this.dataList = data.page.list;
              this.totalPage = data.page.totalCount;
            } else {
              this.dataList = [];
              this.totalPage = 0;
            }
            this.dataListLoading = false;
          });
        },
        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: "状态更新成功"
            })
          });
        },
        // 每页数
        sizeChangeHandle(val) {
          this.pageSize = val;
          this.pageIndex = 1;
          this.getDataList();
        },
        // 当前页
        currentChangeHandle(val) {
          this.pageIndex = val;
          this.getDataList();
        },
        // 多选
        selectionChangeHandle(val) {
          this.dataListSelections = val;
        },
        // 新增 / 修改
        addOrUpdateHandle(id) {
          this.addOrUpdateVisible = true;
          this.$nextTick(() => {
            this.$refs.addOrUpdate.init(id);
          });
        },
        // 删除
        deleteHandle(id) {
          var ids = id
            ? [id]
            : this.dataListSelections.map((item) => {
                return item.brandId;
              });
          this.$confirm(
            `确定对[id=${ids.join(",")}]进行[${id ? "删除" : "批量删除"}]操作?`,
            "提示",
            {
              confirmButtonText: "确定",
              cancelButtonText: "取消",
              type: "warning",
            }
          ).then(() => {
            this.$http({
              url: this.$http.adornUrl("/product/brand/delete"),
              method: "post",
              data: this.$http.adornData(ids, false),
            }).then(({ data }) => {
              if (data && data.code === 0) {
                this.$message({
                  message: "操作成功",
                  type: "success",
                  duration: 1500,
                  onClose: () => {
                    this.getDataList();
                  },
                });
              } else {
                this.$message.error(data.msg);
              }
            });
          });
        },
      },
    };
    </script>

    brand-add-or-update.vue

    <template>
      <el-dialog
        :title="!dataForm.brandId ? '新增' : '修改'"
        :close-on-click-modal="false"
        :visible.sync="visible"
      >
        <el-form
          :model="dataForm"
          :rules="dataRule"
          ref="dataForm"
          @keyup.enter.native="dataFormSubmit()"
          label-width="140px"
        >
          <el-form-item label="品牌名" prop="name">
            <el-input v-model="dataForm.name" placeholder="品牌名"></el-input>
          </el-form-item>
          <el-form-item label="品牌logo地址" prop="logo">
            <el-input v-model="dataForm.logo" placeholder="品牌logo地址"></el-input>
          </el-form-item>
          <el-form-item label="介绍" prop="descript">
            <el-input v-model="dataForm.descript" placeholder="介绍"></el-input>
          </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>
          <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="dataForm.sort" placeholder="排序"></el-input>
          </el-form-item>
        </el-form>
        <span slot="footer" class="dialog-footer">
          <el-button @click="visible = false">取消</el-button>
          <el-button type="primary" @click="dataFormSubmit()">确定</el-button>
        </span>
      </el-dialog>
    </template>
    
    <script>
    export default {
      data() {
        return {
          visible: false,
          dataForm: {
            brandId: 0,
            name: "",
            logo: "",
            descript: "",
            showStatus: "",
            firstLetter: "",
            sort: "",
          },
          dataRule: {
            name: [{ required: true, message: "品牌名不能为空", trigger: "blur" }],
            logo: [
              { required: true, message: "品牌logo地址不能为空", trigger: "blur" },
            ],
            descript: [
              { required: true, message: "介绍不能为空", trigger: "blur" },
            ],
            showStatus: [
              {
                required: true,
                message: "显示状态[0-不显示;1-显示]不能为空",
                trigger: "blur",
              },
            ],
            firstLetter: [
              { required: true, message: "检索首字母不能为空", trigger: "blur" },
            ],
            sort: [{ required: true, message: "排序不能为空", trigger: "blur" }],
          },
        };
      },
      methods: {
        init(id) {
          this.dataForm.brandId = id || 0;
          this.visible = true;
          this.$nextTick(() => {
            this.$refs["dataForm"].resetFields();
            if (this.dataForm.brandId) {
              this.$http({
                url: this.$http.adornUrl(
                  `/product/brand/info/${this.dataForm.brandId}`
                ),
                method: "get",
                params: this.$http.adornParams(),
              }).then(({ data }) => {
                if (data && data.code === 0) {
                  this.dataForm.name = data.brand.name;
                  this.dataForm.logo = data.brand.logo;
                  this.dataForm.descript = data.brand.descript;
                  this.dataForm.showStatus = data.brand.showStatus;
                  this.dataForm.firstLetter = data.brand.firstLetter;
                  this.dataForm.sort = data.brand.sort;
                }
              });
            }
          });
        },
        // 表单提交
        dataFormSubmit() {
          this.$refs["dataForm"].validate((valid) => {
            if (valid) {
              this.$http({
                url: this.$http.adornUrl(
                  `/product/brand/${!this.dataForm.brandId ? "save" : "update"}`
                ),
                method: "post",
                data: this.$http.adornData({
                  brandId: this.dataForm.brandId || undefined,
                  name: this.dataForm.name,
                  logo: this.dataForm.logo,
                  descript: this.dataForm.descript,
                  showStatus: this.dataForm.showStatus,
                  firstLetter: this.dataForm.firstLetter,
                  sort: this.dataForm.sort,
                }),
              }).then(({ data }) => {
                if (data && data.code === 0) {
                  this.$message({
                    message: "操作成功",
                    type: "success",
                    duration: 1500,
                    onClose: () => {
                      this.visible = false;
                      this.$emit("refreshDataList");
                    },
                  });
                } else {
                  this.$message.error(data.msg);
                }
              });
            }
          });
        },
      },
    };
    </script>

    4、文件上传(oss前后联调测试上传)

      请先参考文章 谷粒商城分布式基础(四)—— 分布式组件SpringCloud & SpringCloud Alibaba 的 “6、SpringCloud Alibaba-OSS【文件存储】” 搭建完毕第三方模块 gulimall-third-party,其中主要讲述了如何使用SpringCloud Alibaba的组件 OSS 搭建阿里云存储功能

    1、上传组件
    (1)放置项目提供的upload文件夹到components/目录下,一个是单文件上传,另外一个是多文件上传

      policy.js封装一个Promise,发送/thirdparty/oss/policy请求。vue项目会自动加上api前缀
      multiUpload.vue多文件上传。
      singleUpload.vue单文件上传。

     (2)修改upload文件

         修改multiUpload.vue 和 singleUpload.vue 

        要替换里面的action中的内容action=“http://gulimall-hr.oss-cn-beijing.aliyuncs.com”

        内容来源于阿里云平台

      (3)修改brand-add-or-update.vue

        (a)要使用文件上传组件,先导入import SingleUpload from “@/components/upload/singleUpload”;

        (b)写明要使用的组件components: { SingleUpload },填入<single-upload v-model="dataForm.logo"></single-upload>

        (c)修改el-form-item label="品牌logo地址"内容

      (4)最终样式效果

      点击一下文件上传,发现发送了两个请求

     

      为什么会报错呢?

      (5)上传问题分析

      我们在后端准备好了签名controller,那么前端是在哪里获取的呢

      而文件上传前调用的方法: :before-upload=“beforeUpload”

      发现该方法返回了一个new Promise,调用了policy(),该方法是policy.js中的

      import { policy } from "./policy"; 

      在vue中看是response.data.policy,在控制台看response.policy。所以去java里面改返回值为R。return R.ok().put(“data”,respMap);

      开始执行上传,但是在上传过程中,出现了跨域请求问题:

      Access to XMLHttpRequest at 'http://gulimall-f.oss-cn-qingdao.aliyuncs.com/' from origin 'http://localhost:8001' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

      这又是一个跨域的问题,解决方法就是在阿里云上开启跨域访问:

      再次测试上传,注意上传时他的key变成了response.data.dir +getUUID()+"_${filename}";

      ok,上传成功

    5、表单校验 & 自定义校验器

    (1)修改brand.vue的品牌logo显示

      效果:

    (2)修改brand-add-or-update如下: 
      :active-value="1"         
      :inactive-value="0" # 激活为1,不激活为0
      添加表单校验 & 自定义校验器
        firstLetter: [
              {
                validator: (rule, value, callback) => {
                  if (value == "") {
                    callback(new Error("首字母必须填写"));
                  } else if (!/^[a-zA-Z]$/.test(value)) {
                    callback(new Error("首字母必须a-z或者A-Z之间"));
                  } 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",
              },
            ],

    修改后的效果
    brand.vue
    <template>
      <div class="mod-config">
        <el-form
          :inline="true"
          :model="dataForm"
          @keyup.enter.native="getDataList()"
        >
          <el-form-item>
            <el-input
              v-model="dataForm.key"
              placeholder="参数名"
              clearable
            ></el-input>
          </el-form-item>
          <el-form-item>
            <el-button @click="getDataList()">查询</el-button>
            <el-button
              v-if="isAuth('product:brand:save')"
              type="primary"
              @click="addOrUpdateHandle()"
              >新增</el-button
            >
            <el-button
              v-if="isAuth('product:brand:delete')"
              type="danger"
              @click="deleteHandle()"
              :disabled="dataListSelections.length <= 0"
              >批量删除</el-button
            >
          </el-form-item>
        </el-form>
        <el-table
          :data="dataList"
          border
          v-loading="dataListLoading"
          @selection-change="selectionChangeHandle"
          style=" 100%"
        >
          <el-table-column
            type="selection"
            header-align="center"
            align="center"
            width="50"
          >
          </el-table-column>
          <el-table-column
            prop="brandId"
            header-align="center"
            align="center"
            label="品牌id"
          >
          </el-table-column>
          <el-table-column
            prop="name"
            header-align="center"
            align="center"
            label="品牌名"
          >
          </el-table-column>
          <el-table-column
            prop="logo"
            header-align="center"
            align="center"
            label="品牌logo地址"
          >
            <template slot-scope="scope">
              <!-- <el-image
                  style=" 100px; height: 80px"
                  :src="scope.row.logo"
              fit="fill"></el-image>-->
              <img :src="scope.row.logo" style=" 100px; height: 80px" />
            </template>
          </el-table-column>
          <el-table-column
            prop="descript"
            header-align="center"
            align="center"
            label="介绍"
          >
          </el-table-column>
          <el-table-column
            prop="showStatus"
            header-align="center"
            align="center"
            label="显示状态"
          >
            <template slot-scope="scope">
              <el-switch
                v-model="scope.row.showStatus"
                active-color="#13ce66"
                inactive-color="#ff4949"
                :active-value="1"
                :inactive-value="0"
                @change="updateBrandStatus(scope.row)"
              ></el-switch>
            </template>
          </el-table-column>
          <el-table-column
            prop="firstLetter"
            header-align="center"
            align="center"
            label="检索首字母"
          >
          </el-table-column>
          <el-table-column
            prop="sort"
            header-align="center"
            align="center"
            label="排序"
          >
          </el-table-column>
          <el-table-column
            fixed="right"
            header-align="center"
            align="center"
            width="150"
            label="操作"
          >
            <template slot-scope="scope">
              <el-button
                type="text"
                size="small"
                @click="addOrUpdateHandle(scope.row.brandId)"
                >修改</el-button
              >
              <el-button
                type="text"
                size="small"
                @click="deleteHandle(scope.row.brandId)"
                >删除</el-button
              >
            </template>
          </el-table-column>
        </el-table>
        <el-pagination
          @size-change="sizeChangeHandle"
          @current-change="currentChangeHandle"
          :current-page="pageIndex"
          :page-sizes="[10, 20, 50, 100]"
          :page-size="pageSize"
          :total="totalPage"
          layout="total, sizes, prev, pager, next, jumper"
        >
        </el-pagination>
        <!-- 弹窗, 新增 / 修改 -->
        <add-or-update
          v-if="addOrUpdateVisible"
          ref="addOrUpdate"
          @refreshDataList="getDataList"
        ></add-or-update>
      </div>
    </template>

    <script>
    import AddOrUpdate from "./brand-add-or-update";
    export default {
      data() {
        return {
          dataForm: {
            key: "",
          },
          dataList: [],
          pageIndex: 1,
          pageSize: 10,
          totalPage: 0,
          dataListLoading: false,
          dataListSelections: [],
          addOrUpdateVisible: false,
        };
      },
      components: {
        AddOrUpdate,
      },
      activated() {
        this.getDataList();
      },
      methods: {
        // 获取数据列表
        getDataList() {
          this.dataListLoading = true;
          this.$http({
            url: this.$http.adornUrl("/product/brand/list"),
            method: "get",
            params: this.$http.adornParams({
              page: this.pageIndex,
              limit: this.pageSize,
              key: this.dataForm.key,
            }),
          }).then(({ data }) => {
            if (data && data.code === 0) {
              this.dataList = data.page.list;
              this.totalPage = data.page.totalCount;
            } else {
              this.dataList = [];
              this.totalPage = 0;
            }
            this.dataListLoading = false;
          });
        },
        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: "状态更新成功",
            });
          });
        },
        // 每页数
        sizeChangeHandle(val) {
          this.pageSize = val;
          this.pageIndex = 1;
          this.getDataList();
        },
        // 当前页
        currentChangeHandle(val) {
          this.pageIndex = val;
          this.getDataList();
        },
        // 多选
        selectionChangeHandle(val) {
          this.dataListSelections = val;
        },
        // 新增 / 修改
        addOrUpdateHandle(id) {
          this.addOrUpdateVisible = true;
          this.$nextTick(() => {
            this.$refs.addOrUpdate.init(id);
          });
        },
        // 删除
        deleteHandle(id) {
          var ids = id
            ? [id]
            : this.dataListSelections.map((item) => {
                return item.brandId;
              });
          this.$confirm(
            `确定对[id=${ids.join(",")}]进行[${id ? "删除" : "批量删除"}]操作?`,
            "提示",
            {
              confirmButtonText: "确定",
              cancelButtonText: "取消",
              type: "warning",
            }
          ).then(() => {
            this.$http({
              url: this.$http.adornUrl("/product/brand/delete"),
              method: "post",
              data: this.$http.adornData(ids, false),
            }).then(({ data }) => {
              if (data && data.code === 0) {
                this.$message({
                  message: "操作成功",
                  type: "success",
                  duration: 1500,
                  onClose: () => {
                    this.getDataList();
                  },
                });
              } else {
                this.$message.error(data.msg);
              }
            });
          });
        },
      },
    };
    </script>
     
    brand-add-or-update.vue
    <template>
      <el-dialog
        :title="!dataForm.brandId ? '新增' : '修改'"
        :close-on-click-modal="false"
        :visible.sync="visible"
      >
        <el-form
          :model="dataForm"
          :rules="dataRule"
          ref="dataForm"
          @keyup.enter.native="dataFormSubmit()"
          label-width="140px"
        >
          <el-form-item label="品牌名" prop="name">
            <el-input v-model="dataForm.name" placeholder="品牌名"></el-input>
          </el-form-item>
          <el-form-item label="品牌logo地址" prop="logo">
            <!-- <el-input v-model="dataForm.logo" placeholder="品牌logo地址"></el-input> -->
            <single-upload v-model="dataForm.logo"></single-upload>
          </el-form-item>
          <el-form-item label="介绍" prop="descript">
            <el-input v-model="dataForm.descript" placeholder="介绍"></el-input>
          </el-form-item>
          <el-form-item label="显示状态" prop="showStatus">
            <el-switch
              v-model="dataForm.showStatus"
              active-color="#13ce66"
              inactive-color="#ff4949"
              :active-value="1"
              :inactive-value="0"
            >
            </el-switch>
          </el-form-item>
          <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="dataForm.sort" placeholder="排序"></el-input>
          </el-form-item>
        </el-form>
        <span slot="footer" class="dialog-footer">
          <el-button @click="visible = false">取消</el-button>
          <el-button type="primary" @click="dataFormSubmit()">确定</el-button>
        </span>
      </el-dialog>
    </template>

    <script>
    import SingleUpload from "@/components/upload/singleUpload";
    export default {
      components: { SingleUpload },
      data() {
        return {
          visible: false,
          dataForm: {
            brandId: 0,
            name: "",
            logo: "",
            descript: "",
            showStatus: 1,
            firstLetter: "",
            sort: 0,
          },
          dataRule: {
            name: [{ required: true, message: "品牌名不能为空", trigger: "blur" }],
            logo: [
              { required: true, message: "品牌logo地址不能为空", trigger: "blur" },
            ],
            descript: [
              { required: true, message: "介绍不能为空", trigger: "blur" },
            ],
            showStatus: [
              {
                required: true,
                message: "显示状态[0-不显示;1-显示]不能为空",
                trigger: "blur",
              },
            ],
            firstLetter: [
              {
                validator: (rule, value, callback) => {
                  if (value == "") {
                    callback(new Error("首字母必须填写"));
                  } else if (!/^[a-zA-Z]$/.test(value)) {
                    callback(new Error("首字母必须a-z或者A-Z之间"));
                  } 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",
              },
            ],
          },
        };
      },
      methods: {
        init(id) {
          this.dataForm.brandId = id || 0;
          this.visible = true;
          this.$nextTick(() => {
            this.$refs["dataForm"].resetFields();
            if (this.dataForm.brandId) {
              this.$http({
                url: this.$http.adornUrl(
                  `/product/brand/info/${this.dataForm.brandId}`
                ),
                method: "get",
                params: this.$http.adornParams(),
              }).then(({ data }) => {
                if (data && data.code === 0) {
                  this.dataForm.name = data.brand.name;
                  this.dataForm.logo = data.brand.logo;
                  this.dataForm.descript = data.brand.descript;
                  this.dataForm.showStatus = data.brand.showStatus;
                  this.dataForm.firstLetter = data.brand.firstLetter;
                  this.dataForm.sort = data.brand.sort;
                }
              });
            }
          });
        },
        // 表单提交
        dataFormSubmit() {
          this.$refs["dataForm"].validate((valid) => {
            if (valid) {
              this.$http({
                url: this.$http.adornUrl(
                  `/product/brand/${!this.dataForm.brandId ? "save" : "update"}`
                ),
                method: "post",
                data: this.$http.adornData({
                  brandId: this.dataForm.brandId || undefined,
                  name: this.dataForm.name,
                  logo: this.dataForm.logo,
                  descript: this.dataForm.descript,
                  showStatus: this.dataForm.showStatus,
                  firstLetter: this.dataForm.firstLetter,
                  sort: this.dataForm.sort,
                }),
              }).then(({ data }) => {
                if (data && data.code === 0) {
                  this.$message({
                    message: "操作成功",
                    type: "success",
                    duration: 1500,
                    onClose: () => {
                      this.visible = false;
                      this.$emit("refreshDataList");
                    },
                  });
                } else {
                  this.$message.error(data.msg);
                }
              });
            }
          });
        },
      },
    };
    </script>

    6、JSR303 数据校验

    问题引入:填写form时应该有前端校验,后端也应该有校验
    前端
      前端的校验是element-ui表单验证
      Form 组件提供了表单验证的功能,只需要通过 rules 属性传入约定的验证规则,并将 Form-Item 的 prop 属性设置为需校验的字段名即可。
    后端

    (1)@NotNull等,给Bean添加校验注解
      在Java中提供了一系列的校验方式,它这些校验方式在“javax.validation.constraints”包中,提供了如@Email,@NotNull等注解。
      <!--jsr3参数校验器-->
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
      </dependency>
    里面依赖了hibernate-validator,在非空处理方式上提供了@NotNull,@NotBlank和@NotEmpty
      (a)@NotNull
        The annotated element must not be null. Accepts any type. 注解元素禁止为null,能够接收任何类型
      (b)@NotEmpty
        the annotated element must not be null nor empty. 该注解修饰的字段不能为null或""
        Supported types are: 支持以下几种类型

          CharSequence (length of character sequence is evaluated)字符序列(字符序列长度的计算)
          Collection (collection size is evaluated)
          集合长度的计算
          Map (map size is evaluated)
          map长度的计算
          Array (array length is evaluated)
          数组长度的计算

       (c)@NotBlank

        The annotated element must not be null and must contain at least one non-whitespace character. Accepts CharSequence.
        该注解不能为null,并且至少包含一个非空格字符。接收字符序列。

     (2@Valid,controller中加校验注解@Valid,开启校验
      @RequestMapping("/save")
      //@RequiresPermissions("product:brand:save")
      public R save(@Valid @RequestBody BrandEntity brand){
       brandService.save(brand);
          return R.ok();
      }
      测试:http://localhost:88/api/product/brand/save
    
      在postman中发送上面的请求
    {
        "timestamp""2021-12-13T10:02:08.381+0000",
        "status"400,
        "error""Bad Request",
        "errors": [
            {
                "codes": [
                    "NotBlank.brandEntity.name",
                    "NotBlank.name",
                    "NotBlank.java.lang.String",
                    "NotBlank"
                ],
                "arguments": [
                    {
                        "codes": [
                            "brandEntity.name",
                            "name"
                        ],
                        "arguments"null,
                        "defaultMessage""name",
                        "code""name"
                    }
                ],
                "defaultMessage""不能为空",
                "objectName""brandEntity",
                "field""name",
                "rejectedValue""",
                "bindingFailure"false,
                "code""NotBlank"
            }
        ],
        "message""Validation failed for object='brandEntity'. Error count: 1",
        "path""/product/brand/save"
    }
    
    

      能够看到"defaultMessage": “不能为空”,这些错误消息定义在“hibernate-validator”的“\org\hibernate\validator\ValidationMessages_zh_CN.properties”文件中。在该文件中定义了很多的错误规则:

      javax.validation.constraints.AssertFalse.message     = 只能为false

      javax.validation.constraints.AssertTrue.message      = 只能为true

      javax.validation.constraints.DecimalMax.message      = 必须小于或等于{value}

      javax.validation.constraints.DecimalMin.message      = 必须大于或等于{value}

      javax.validation.constraints.Digits.message          = 数字的值超出了允许范围(只允许在{integer}位整数和{fraction}位小数范围内)

      javax.validation.constraints.Email.message           = 不是一个合法的电子邮件地址

      javax.validation.constraints.Future.message          = 需要是一个将来的时间

      javax.validation.constraints.FutureOrPresent.message = 需要是一个将来或现在的时间

      javax.validation.constraints.Max.message             = 最大不能超过{value}

      javax.validation.constraints.Min.message             = 最小不能小于{value}

      javax.validation.constraints.Negative.message        = 必须是负数

      javax.validation.constraints.NegativeOrZero.message  = 必须是负数或零

      javax.validation.constraints.NotBlank.message        = 不能为空

      javax.validation.constraints.NotEmpty.message        = 不能为空

      javax.validation.constraints.NotNull.message         = 不能为null

      javax.validation.constraints.Null.message            = 必须为null

      javax.validation.constraints.Past.message            = 需要是一个过去的时间

      javax.validation.constraints.PastOrPresent.message   = 需要是一个过去或现在的时间

      javax.validation.constraints.Pattern.message         = 需要匹配正则表达式"{regexp}"

      javax.validation.constraints.Positive.message        = 必须是正数

      javax.validation.constraints.PositiveOrZero.message  = 必须是正数或零

      javax.validation.constraints.Size.message            = 个数必须在{min}和{max}之间

     

      org.hibernate.validator.constraints.CreditCardNumber.message        = 不合法的信用卡号码

      org.hibernate.validator.constraints.Currency.message                = 不合法的货币 (必须是{value}其中之一)

      org.hibernate.validator.constraints.EAN.message                     = 不合法的{type}条形码

      org.hibernate.validator.constraints.Email.message                   = 不是一个合法的电子邮件地址

      org.hibernate.validator.constraints.Length.message                  = 长度需要在{min}和{max}之间

      org.hibernate.validator.constraints.CodePointLength.message         = 长度需要在{min}和{max}之间

     

      org.hibernate.validator.constraints.LuhnCheck.message               = ${validatedValue}的校验码不合法, Luhn模10校验和不匹配

      org.hibernate.validator.constraints.Mod10Check.message              = ${validatedValue}的校验码不合法, 模10校验和不匹配

      org.hibernate.validator.constraints.Mod11Check.message              = ${validatedValue}的校验码不合法, 模11校验和不匹配

      org.hibernate.validator.constraints.ModCheck.message                = ${validatedValue}的校验码不合法, ${modType}校验和不匹配

      org.hibernate.validator.constraints.NotBlank.message                = 不能为空

      org.hibernate.validator.constraints.NotEmpty.message                = 不能为空

      org.hibernate.validator.constraints.ParametersScriptAssert.message  = 执行脚本表达式"{script}"没有返回期望结果

      org.hibernate.validator.constraints.Range.message                   = 需要在{min}和{max}之间

      org.hibernate.validator.constraints.SafeHtml.message                = 可能有不安全的HTML内容

      org.hibernate.validator.constraints.ScriptAssert.message            = 执行脚本表达式"{script}"没有返回期望结果

      org.hibernate.validator.constraints.URL.message                     = 需要是一个合法的URL

    
    

      org.hibernate.validator.constraints.time.DurationMax.message        = 必须小于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'}

      org.hibernate.validator.constraints.time.DurationMin.message        = 必须大于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'}

      想要自定义错误消息,可以覆盖默认的错误提示信息,如@NotBlank的默认message是

    
    

      public @interface NotBlank {

    
    

    String message() default "{javax.validation.constraints.NotBlank.message}";

    
    

      }

      可以在添加注解的时候,修改message:

    @NotBlank(message = "品牌名必须非空")
    private String name;

      当再次发送请求时,得到的错误提示信息:

    {
        "timestamp""2021-12-13T10:06:44.246+0000",
        "status"400,
        "error""Bad Request",
        "errors": [
            {
                "codes": [
                    "NotBlank.brandEntity.name",
                    "NotBlank.name",
                    "NotBlank.java.lang.String",
                    "NotBlank"
                ],
                "arguments": [
                    {
                        "codes": [
                            "brandEntity.name",
                            "name"
                        ],
                        "arguments"null,
                        "defaultMessage""name",
                        "code""name"
                    }
                ],
                "defaultMessage""品牌名必须非空",
                "objectName""brandEntity",
                "field""name",
                "rejectedValue""",
                "bindingFailure"false,
                "code""NotBlank"
            }
        ],
        "message""Validation failed for object='brandEntity'. Error count: 1",
        "path""/product/brand/save"
    }

     (3)BindResult,给校验的Bean后,紧跟一个BindResult,就可以获取到校验的结果。拿到校验的结果,就可以自定义的封装

    /**
    * 保存
    */
    @RequestMapping("/save")
    //@RequiresPermissions("product:brand:save")
    public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){
    if( result.hasErrors()){
    Map<String,String> map=new HashMap<>();
    //1.获取错误的校验结果
    result.getFieldErrors().forEach((item)->{
    //获取发生错误时的message
    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();
    }

      postman测试结果:

    {
        "msg""提交的数据不合法",
        "code"400,
        "data": {
            "name""品牌名必须非空"
        }
    }

      这种是针对于该请求设置了一个内容校验,如果针对于每个请求都单独进行配置,显然不是太合适,实际上可以统一的对于异常进行处理

    7、统一异常处理

     可以使用SpringMvc所提供的@ControllerAdvice,通过“backPackages”能够说明处理哪些路径下的异常
    (1)抽取一个异常处理
    package com.atguigu.gulimall.product.exception;

    import com.atguigu.common.utils.R;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.validation.BindingResult;
    import org.springframework.web.bind.MethodArgumentNotValidException;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.RestControllerAdvice;

    import java.util.HashMap;
    import java.util.Map;

    @Slf4j
    @RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
    public class GulimallExceptionControllerAdvice {

    @ExceptionHandler(value = Exception.class) // 也可以返回ModelAndView
    public R handleValidException(MethodArgumentNotValidException exception){

    Map<String,String> map=new HashMap<>();
    // 获取数据校验的错误结果
    BindingResult bindingResult = exception.getBindingResult();
    bindingResult.getFieldErrors().forEach(fieldError -> {
    String message = fieldError.getDefaultMessage();
    String field = fieldError.getField();
    map.put(field,message);
    });

    log.error("数据校验出现问题{},异常类型{}",exception.getMessage(),exception.getClass());

    return R.error(400,"数据校验出现问题").put("data",map);
    }
    }
      测试
      http://localhost:88/api/product/brand/save
    2)默认异常处理
    @ExceptionHandler(value = Throwable.class)
    public R handleException(Throwable throwable){
    log.error("未知异常{},异常类型{}",throwable.getMessage(),throwable.getClass());
    return R.error(400,"数据校验出现问题");
    }
    3)错误状态代码
      上面代码中,针对于错误状态码,是我们进行随意定义的,然而正规开发过程中,错误状态码有着严格的定义规则,如该在项目中我们的错误状态码定义

      在gulimall-common中创建 com.atguigu.common.exception.BizCodeEnume

    package com.atguigu.common.exception;

    /***
    * 错误码和错误信息定义类
    * 1. 错误码定义规则为5为数字
    * 2. 前两位表示业务场景,最后三位表示错误码。例如:10000110:通用 001:系统未知异常
    * 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
    * 错误码列表:
    * 10: 通用
    * 001:参数格式校验
    * 11: 商品
    * 12: 订单
    * 13: 购物车
    * 14: 物流
    *
    *
    */
    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;
    }
    }

      修改统一异常处理

    package com.atguigu.gulimall.product.exception;

    import com.atguigu.common.exception.BizCodeEnume;
    import com.atguigu.common.utils.R;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.validation.BindingResult;
    import org.springframework.web.bind.MethodArgumentNotValidException;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.bind.annotation.RestControllerAdvice;
    import org.springframework.web.servlet.ModelAndView;

    import java.util.HashMap;
    import java.util.Map;

    /**
    * 集中处理所有异常
    */
    @Slf4j
    //@ResponseBody
    //@ControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
    @RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
    public class GulimallExceptionControllerAdvice {

    @ExceptionHandler(value= MethodArgumentNotValidException.class)
    public R handleVaildException(MethodArgumentNotValidException e){
    log.error("数据校验出现问题{},异常类型:{}",e.getMessage(),e.getClass());
    BindingResult bindingResult = e.getBindingResult();

    Map<String,String> errorMap = new HashMap<>();
    bindingResult.getFieldErrors().forEach((fieldError)->{
    errorMap.put(fieldError.getField(),fieldError.getDefaultMessage());
    });
    return R.error(BizCodeEnume.VAILD_EXCEPTION.getCode(),BizCodeEnume.VAILD_EXCEPTION.getMsg()).put("data",errorMap);
    }

    @ExceptionHandler(value = Throwable.class)
    public R handleException(Throwable throwable){

    log.error("错误:",throwable);
    return R.error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(),BizCodeEnume.UNKNOW_EXCEPTION.getMsg());
    }
    }

      测试:http://localhost:88/api/product/brand/save

    8、JSR303分组校验

    1)groups
      给校验注解,标注上groups,指定什么情况下才需要进行校验
      groups里面的内容要以接口的形式显示出来 如:指定在更新和添加的时候,都需要进行校验。新增时不需要带id,修改时必须带id
      
      @NotNull(message = "修改必须定制品牌id", groups = {UpdateGroup.class})   @Null(message = "新增不能指定id", groups = {AddGroup.class})   @TableId   private Long brandId;
      在这种情况下,没有指定分组的校验注解,默认是不起作用的。想要起作用就必须要加groups。
    2)@Validated
      业务方法参数上使用@Validated注解
      @Validated的value方法:
      
      Specify one or more validation groups to apply to the validation step kicked off by this annotation.   指定一个或多个验证组以应用于此注释启动的验证步骤。   JSR-303 defines validation groups as custom annotations which an application declares for the sole purpose of using them as type-safe group arguments, as implemented in SpringValidatorAdapter.   JSR-303 将验证组定义为自定义注释,应用程序声明的唯一目的是将它们用作类型安全组参数,如 SpringValidatorAdapter 中实现的那样。   Other SmartValidator implementations may support class arguments in other ways as well.   其他SmartValidator 实现也可以以其他方式支持类参数。
    @RequestMapping("/save")
    public R save(@Validated(AddGroup.class) @RequestBody BrandEntity brand) {
    brandService.save(brand);
    return R.ok();
    }
    @RequestMapping("/delete")
    //@RequiresPermissions("${moduleNamez}:brand:delete")
    public R delete(@RequestBody Long[] brandIds) {
    brandService.removeByIds(Arrays.asList(brandIds));
    return R.ok();
    }
    3)分组情况下,校验注解生效问题
      默认情况下,在分组校验情况下,没有指定分组的校验注解,将不会生效,它只会在不分组的情况下生效。

     9、自定义校验功能

    场景:要校验showStatus的01状态,可以用正则,但我们可以利用其他方式解决复杂场景。比如我们想要下面的场景
    /**
    * 显示状态[0-不显示;1-显示]
    */
    // @Pattern()
    @NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})
    @ListValue(vals={0,1},groups = {AddGroup.class, UpdateStatusGroup.class})
    private Integer showStatus;
    如何做:
    gulimall-common添加依赖
    <dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
    </dependency>
    (2)编写自定义的校验注解
      必须有3个属性   message()错误信息   groups()分组校验   payload()自定义负载信息
    package com.atguigu.common.valid;

    import javax.validation.Constraint;
    import javax.validation.Payload;
    import java.lang.annotation.Documented;
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;

    import static java.lang.annotation.ElementType.*;
    import static java.lang.annotation.RetentionPolicy.RUNTIME;

    /**
    * @Description: 自定义注解规则
    * @Created: with IntelliJ IDEA.
    * @author: 夏沫止水
    * @createTime: 2020-05-27 17:48
    **/

    @Documented
    @Constraint(validatedBy = { ListValueConstraintValidator.class })
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    public @interface ListValue {

    String message() default "{com.atguigu.common.valid.ListValue.message}";

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

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

    int[] vals() default { };
    }
    该属性值取哪里取呢? common创建文件ValidationMessages.properties 里面写上com.atguigu.common.valid.ListValue.message=必须提交指定的值 [0,1]

     (2)编写自定义的校验器

    package com.atguigu.common.valid;

    import javax.validation.ConstraintValidator;
    import javax.validation.ConstraintValidatorContext;
    import java.util.HashSet;
    import java.util.Set;

    /**
    * @Description:
    * @Created: with IntelliJ IDEA.
    * @author: 夏沫止水
    * @createTime: 2020-05-27 17:54
    **/
    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();

    for (int val : vals) {
    set.add(val);
    }

    }

    /**
    * 判断是否效验成功
    * @param value 需要效验的值
    * @param context
    * @return
    */
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {

    //判断是否有包含的值
    boolean contains = set.contains(value);

    return contains;
    }

    }

    (3)关联校验器和校验注解

      @Constraint(validatedBy = { ListValueConstraintValidator.class})

    (4)使用实例

    /**
    * 显示状态[0-不显示;1-显示]
    */
    @ListValue(vals={0,1},groups = {AddGroup.class, UpdateStatusGroup.class})
    private Integer showStatus;
  • 相关阅读:
    Atitit RSA非对称加密原理与解决方案
    Atitit RSA非对称加密原理与解决方案
    atitit.错误:找不到或无法加载主类 的解决 v4 qa15.doc
    atitit.错误:找不到或无法加载主类 的解决 v4 qa15.doc
    Mac设置su root密码
    Window系统命令行调用控制面板程序
    Ubuntu 安装最新版nodejs
    python中time.strftime不支持中文,报错UnicodeEncodeError: 'locale' codec can't encode character 'u5e74' in position 2: encoding error
    字节跳动——IT技术工程师面试题
    HTTP状态码
  • 原文地址:https://www.cnblogs.com/javahr/p/15673978.html
Copyright © 2020-2023  润新知