Day400&401.商品服务 -谷粒商城

商品服务

一、品牌管理

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

Day400&401.商品服务 -谷粒商城_第1张图片

将逆向工程product得到的resources\src\views\modules\product文件拷贝到achangmall/renren-fast-vue/src/views/modules/product目录下,也就是下面的两个文件

Day400&401.商品服务 -谷粒商城_第2张图片

brand.vue : 显示的表单
brand-add-or-update.vue:添加和更改功能

  • 但是显示的页面没有新增和删除功能,这是因为权限控制的原因

Day400&401.商品服务 -谷粒商城_第3张图片

<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>
  • 查看“isAuth”的定义位置:

Day400&401.商品服务 -谷粒商城_第4张图片

它是在“index.js”中定义,暂时将它设置为返回值为true,即可显示添加和删除功能。 再次刷新页面能够看到,按钮已经出现了:

Day400&401.商品服务 -谷粒商城_第5张图片

进行添加 测试成功

  • 进行修改 也会自动回显 build/webpack.base.conf.js 中注释掉createLintingRule()函数体,不进行lint语法检

Day400&401.商品服务 -谷粒商城_第6张图片

Day400&401.商品服务 -谷粒商城_第7张图片

  • 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="width: 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: {
       
            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: showStatus }, false),
                }).then(({
         data }) => {
       
                    this.$message({
       
                        type: "success",
                        message: "状态更新成功",
                    });
                });
            },

            // 获取数据列表
            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;
                });
            },
            // 每页数
            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="80px"
                 >
            <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>


2、添加上传

这里我们选择将图片放置到阿里云上,使用对象存储。 阿里云上使使用对象存储方式:

Day400&401.商品服务 -谷粒商城_第8张图片

  • 创建Bucket(作为项目)

Day400&401.商品服务 -谷粒商城_第9张图片

  • 上传文件:上传成功后,取得图片的URL

Day400&401.商品服务 -谷粒商城_第10张图片

  • 这种方式是手动上传图片,实际上我们可以在程序中设置自动上传图片到阿里云对象存储。

Day400&401.商品服务 -谷粒商城_第11张图片

上传的账号信息存储在应用服务器 上传先找应用服务器要一个policy上传策略,生成防伪签名

  • 使用代码上传 查看阿里云关于文件上传的帮助:

    https://help.aliyun.com/document_detail/32009.html?spm=a2c4g.11186623.6.768.549d59aaWuZMGJ

    • achangmall-product/pom.xml中添加依赖包
    <dependency>
        <groupId>com.aliyun.ossgroupId>
        <artifactId>aliyun-sdk-ossartifactId>
        <version>3.10.2version>
    dependency>
    
    • 上传文件流

    使用文件上传,您可以将本地文件上传到OSS文件。

    以下代码用于将本地文件examplefile.txt上传到目标存储空间examplebucket中exampledir目录下的exampleobject.txt文件。

    // yourEndpoint填写Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。
    String endpoint = "yourEndpoint";
    // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
    String accessKeyId = "yourAccessKeyId";
    String accessKeySecret = "yourAccessKeySecret";
    
    // 创建OSSClient实例。
    OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
    
    // 创建PutObjectRequest对象。
    // 依次填写Bucket名称(例如examplebucket)、Object完整路径(例如exampledir/exampleobject.txt)和本地文件的完整路径。Object完整路径中不能包含Bucket名称。
    // 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。
    PutObjectRequest putObjectRequest = new PutObjectRequest("examplebucket", "exampledir/exampleobject.txt", new File("D:\\localpath\\examplefile.txt"));
    
    // 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。
    // ObjectMetadata metadata = new ObjectMetadata();
    // metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
    // metadata.setObjectAcl(CannedAccessControlList.Private);
    // putObjectRequest.setMetadata(metadata);
    
    // 上传文件。
    ossClient.putObject(putObjectRequest);
    
    // 关闭OSSClient。
    ossClient.shutdown();     
    

    上面代码的信息可以通过如下查找:

    • endpoint的取值:

    • 点击概览就可以看到你的endpoint信息,endpoint在这里就是上海等地区,如 oss-cn-qingdao.aliyuncs.com

    • bucket域名:

    • 就是签名加上bucket,如achangmall0.oss-cn-hangzhou.aliyuncs.com
      accessKeyId和accessKeySecret需要创建一个RAM账号:

      Day400&401.商品服务 -谷粒商城_第12张图片

    • 选上编程访问 创建用户完毕后,会得到一个“AccessKey ID”和“AccessKeySecret”,

      然后复制这两个值到代码的“AccessKey ID”和“AccessKeySecret”。

      另外还需要添加访问控制权限:

      @Test
      void test0() throws FileNotFoundException {
               
          // Endpoint以杭州为例,其它Region请按实际情况填写。
          String endpoint = "oss-cn-hangzhou.aliyuncs.com";
          // 云账号AccessKey有所有API访问权限,建议遵循阿里云安全最佳实践,创建并使用RAM子账号进行API访问或日常运维,请登录 https://ram.console.aliyun.com 创建。
          String accessKeyId = "你的accessKeyId";
          String accessKeySecret = "你的accessKeySecret";
      
          // 创建OSSClient实例。
          OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
      
          // 上传文件流。
          InputStream inputStream = new FileInputStream("C:\\Users\\PePe\\Pictures\\Camera Roll\\321.png");
          ossClient.putObject("achangmall0", "321.png", inputStream);
      
          // 关闭OSSClient。
          ossClient.shutdown();
          System.out.println("上传成功.");
      }
      
    • 更为简单的使用方式,是使用SpringCloud Alibaba

      https://github.com/alibaba/aliyun-spring-boot/blob/master/aliyun-spring-boot-samples/aliyun-oss-spring-boot-sample/README-zh.md

      • achangmall-common/pom.xml引入依赖

      具体的可以在maven中央仓库查找

      <dependency>
          <groupId>com.alibaba.cloudgroupId>
          <artifactId>spring-cloud-alicloud-ossartifactId>
          <version>2.1.1.RELEASEversion>
      dependency>
      
      • 在配置文件中配置 OSS 服务对应的 accessKey、secretKey 和 endpoint。
          alicloud:
            access-key: xxx
            secret-key: xxx
            oss:
              endpoint: oss-cn-hangzhou.aliyuncs.com
      
      • 注入OSSClient并进行文件上传下载等操作
      @RunWith(SpringRunner.class)
      @SpringBootTest
      public class OssTest {
               
      
          @Resource
          private OSSClient ossClient;
      
          @Test
          void test1() throws FileNotFoundException {
               
              // 上传文件流。
              InputStream inputStream = new FileInputStream("C:\\Users\\PePe\\Pictures\\Camera Roll\\321.png");
              ossClient.putObject("achangmall0", "321.png", inputStream);
      
              // 关闭OSSClient。
              ossClient.shutdown();
              System.out.println("上传完成...");
          }
      }
      

但是这样来做还是比较麻烦,如果以后的上传任务都交给achangmall-product来完成,显然耦合度高。最好单独新建一个Module来完成文件上传任务。

  • 创建第三方模块

Day400&401.商品服务 -谷粒商城_第13张图片

  • 添加依赖,将原来achangmall-common中的“spring-cloud-starter-alicloud-oss”依赖移动到该项目中
<dependencies>
    <dependency>
        <groupId>com.alibaba.cloudgroupId>
        <artifactId>spring-cloud-alicloud-ossartifactId>
        <version>2.1.1.RELEASEversion>
    dependency>
    <dependency>
        <groupId>com.achang.achangmallgroupId>
        <artifactId>achangmall-commonartifactId>
        <version>0.0.1-SNAPSHOTversion>
    dependency>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-testartifactId>
        <scope>testscope>
    dependency>
dependencies>
  • 主启动类@EnableDiscoveryClient // 在主启动类中开启服务的注册和发现

  • 在nacos中注册 在nacos创建命名空间“ achangmall-third-party ”

Day400&401.商品服务 -谷粒商城_第14张图片

  • 在“ achangmall-third-party”命名空间中,创建“ achangmall-third-service.yml”文件

Day400&401.商品服务 -谷粒商城_第15张图片

  • 编写配置文件 application.yml
server:
  port: 30000

spring:
  application:
    name: achangmall-third-service
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
  • bootstrap.properties
spring.cloud.nacos.config.name=achangmall-third-service
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=ad0431b9-a77f-4220-bf61-b48c7e117250
spring.cloud.nacos.config.extension-configs[0].data-id=achangmall-third-service.yml
spring.cloud.nacos.config.extension-configs[0].group=DEFAULT_GROUP
spring.cloud.nacos.config.extension-configs[0].refresh=true
  • 编写测试类
@SpringBootTest
@RunWith(SpringRunner.class)
class AchangmallThirdServiceApplicationTests {
     
    @Resource
    OSSClient ossClient;

    @Test
    void contextLoads() throws FileNotFoundException {
     
        //上传文件流。
        InputStream inputStream = new FileInputStream("C:\\Users\\PePe\\Pictures\\Camera Roll\\123.jpg");
        ossClient.putObject("achangmall0", "333.jpg", inputStream);

        // 关闭OSSClient。
        ossClient.shutdown();
        System.out.println("上传成功.");
    }
}

Day400&401.商品服务 -谷粒商城_第16张图片


  • 改进:服务端签名后直传

采用JavaScript客户端直接签名(参见JavaScript客户端签名直传)时,AccessKeyID和AcessKeySecret会暴露在前端页面,因此存在严重的安全隐患。

因此,OSS提供了服务端签名后直传的方案。

  • 向服务器获取到签名,再去请求oss服务器

Day400&401.商品服务 -谷粒商城_第17张图片

  • 服务端签名后直传的原理如下:

用户发送上传Policy请求到应用服务器。 应用服务器返回上传Policy和签名给用户。

用户直接上传数据到OSS。

Day400&401.商品服务 -谷粒商城_第18张图片

  • 在com.achang.achangmall.controller.OssController编写controller
/******
 @author 阿昌
 @create 2021-09-25 14:32
 *******
 */
@RestController
@RequestMapping("third-service/oss")
public class OssController {
     

    @Resource
    private OSSClient ossClient;

    @Value("${spring.cloud.alicloud.oss.endpoint}")
    public String endpoint;

    @Value("${spring.cloud.alicloud.oss.bucket}")
    public String bucket;

    @Value("${spring.cloud.alicloud.access-key}")
    public String accessId;

    private final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");

    @GetMapping("/policy")
    public Map<String, String> getPolicy(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 dir = format.format(new Date())+"/"; // 用户上传文件时指定的前缀。以日期格式存储

        // 创建OSSClient实例。
        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;
    }
    
}
  • 测试请求,http://localhost:30000/third-service/oss/policy,成功获取

    Day400&401.商品服务 -谷粒商城_第19张图片

  • 然后,我们通过gateway网关代理转发,在上传文件时的访问路径为“ http://localhost:88/api/third/oss/policy”,

  • 配置网关

        - id: oss_route
          uri: lb://achangmall-third-service
          predicates:
            - Path=/api/third-service/**
          filters:
            - RewritePath=/api/third-service/(?>.*),/$\{
     segment}
  • 访问http://localhost:88/api/third-service/third-service/oss/policy,测试是否可以转发到我们的接口,如下成功访问

Day400&401.商品服务 -谷粒商城_第20张图片


  • 上传组件

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

Day400&401.商品服务 -谷粒商城_第21张图片

  • policy.js封装一个Promise,发送/thirdparty/oss/policy请求。vue项目会自动加上api前缀

  • multiUpload.vue多文件上传。要改,改方式如下

  • singleUpload.vue单文件上传。

    • 要替换里面的action中的内容action=“http://achangmall0.oss-cn-hangzhou.aliyuncs.com”,你的阿里云指定的bucket域名

    Day400&401.商品服务 -谷粒商城_第22张图片

  • singleUpload.vue代码

<template>
  <div>
    <el-upload
      action="http://achangmall0.oss-cn-hangzhou.aliyuncs.com"
      :data="dataObj"
      list-type="picture"
      :multiple="false"
      :show-file-list="showFileList"
      :file-list="fileList"
      :before-upload="beforeUpload"
      :on-remove="handleRemove"
      :on-success="handleUploadSuccess"
      :on-preview="handlePreview"
    >
      <el-button size="small" type="primary">点击上传el-button>
      <div slot="tip" class="el-upload__tip">
        只能上传jpg/png文件,且不超过10MB
      div>
    el-upload>
    <el-dialog :visible.sync="dialogVisible">
      <img width="100%" :src="fileList[0].url" alt="" />
    el-dialog>
  div>
template>
<script>
import {
        policy } from "./policy";
import {
        getUUID } from "@/utils";
export default {
       
  name: "singleUpload",
  props: {
       
    value: String,
  },
  computed: {
       
    imageUrl() {
       
      return this.value;
    },
    imageName() {
       
      if (this.value != null && this.value !== "") {
       
        return this.value.substr(this.value.lastIndexOf("/") + 1);
      } else {
       
        return null;
      }
    },
    fileList() {
       
      return [
        {
       
          name: this.imageName,
          url: this.imageUrl,
        },
      ];
    },
    showFileList: {
       
      get: function () {
       
        return (
          this.value !== null && this.value !== "" && this.value !== undefined
        );
      },
      set: function (newValue) {
       },
    },
  },
  data() {
       
    return {
       
      dataObj: {
       
        policy: "",
        signature: "",
        key: "",
        ossaccessKeyId: "",
        dir: "",
        host: "",
        // callback:'',
      },
      dialogVisible: false,
    };
  },
  methods: {
       
    emitInput(val) {
       
      this.$emit("input", val);
    },
    handleRemove(file, fileList) {
       
      this.emitInput("");
    },
    handlePreview(file) {
       
      this.dialogVisible = true;
    },
    beforeUpload(file) {
       
      let _self = this;
      return new Promise((resolve, reject) => {
       
        policy()
          .then((response) => {
       
            console.log("响应的数据", response);
            _self.dataObj.policy = response.policy;
            _self.dataObj.signature = response.signature;
            _self.dataObj.ossaccessKeyId = response.accessid;
            _self.dataObj.key = response.dir + getUUID() + "_${filename}";
            _self.dataObj.dir = response.dir;
            _self.dataObj.host = response.host;
            console.log("响应的数据222。。。", _self.dataObj);
            resolve(true);
          })
          .catch((err) => {
       
            reject(false);
          });
      });
    },
    handleUploadSuccess(res, file) {
       
      console.log("上传成功...");
      this.showFileList = true;
      this.fileList.pop();
      this.fileList.push({
       
        name: file.name,
        url:
          this.dataObj.host +
          "/" +
          this.dataObj.key.replace("${filename}", file.name),
      });
      this.emitInput(this.fileList[0].url);
    },
  },
};
script>
<style>
style>
  • multiUpload.vue代码
<template>
    <div>
        <el-upload
                   action="http://achangmall0.oss-cn-hangzhou.aliyuncs.com"
                   :data="dataObj"
                   :list-type="listType"
                   :file-list="fileList"
                   :before-upload="beforeUpload"
                   :on-remove="handleRemove"
                   :on-success="handleUploadSuccess"
                   :on-preview="handlePreview"
                   :limit="maxCount"
                   :on-exceed="handleExceed"
                   :show-file-list="showFile"
                   >
            <i class="el-icon-plus">i>
        el-upload>
        <el-dialog :visible.sync="dialogVisible">
            <img width="100%" :src="dialogImageUrl" alt />
        el-dialog>
    div>
template>
<script>
    import {
        policy } from "./policy";
    import {
        getUUID } from "@/utils";
    export default {
       
        name: "multiUpload",
        props: {
       
            //图片属性数组
            value: Array,
            //最大上传图片数量
            maxCount: {
       
                type: Number,
                default: 30,
            },
            listType: {
       
                type: String,
                default: "picture-card",
            },
            showFile: {
       
                type: Boolean,
                default: true,
            },
        },
        data() {
       
            return {
       
                dataObj: {
       
                    policy: "",
                    signature: "",
                    key: "",
                    ossaccessKeyId: "",
                    dir: "",
                    host: "",
                    uuid: "",
                },
                dialogVisible: false,
                dialogImageUrl: null,
            };
        },
        computed: {
       
            fileList() {
       
                let fileList = [];
                for (let i = 0; i < this.value.length; i++) {
       
                    fileList.push({
        url: this.value[i] });
                }
                return fileList;
            },
        },
        mounted() {
       },
        methods: {
       
            emitInput(fileList) {
       
                let value = [];
                for (let i = 0; i < fileList.length; i++) {
       
                    value.push(fileList[i].url);
                }
                this.$emit("input", value);
            },
            handleRemove(file, fileList) {
       
                this.emitInput(fileList);
            },
            handlePreview(file) {
       
                this.dialogVisible = true;
                this.dialogImageUrl = file.url;
            },
            beforeUpload(file) {
       
                let _self = this;
                return new Promise((resolve, reject) => {
       
                    policy()
                        .then((response) => {
       
                        console.log("这是什么${filename}");
                        _self.dataObj.policy = response.data.policy;
                        _self.dataObj.signature = response.data.signature;
                        _self.dataObj.ossaccessKeyId = response.data.accessid;
                        _self.dataObj.key = response.data.dir + getUUID() + "_${filename}";
                        _self.dataObj.dir = response.data.dir;
                        _self.dataObj.host = response.data.host;
                        resolve(true);
                    })
                        .catch((err) => {
       
                        console.log("出错了...", err);
                        reject(false);
                    });
                });
            },
            handleUploadSuccess(res, file) {
       
                this.fileList.push({
       
                    name: file.name,
                    // url: this.dataObj.host + "/" + this.dataObj.dir + "/" + file.name; 替换${filename}为真正的文件名
                    url:
                    this.dataObj.host +
                    "/" +
                    this.dataObj.key.replace("${filename}", file.name),
                });
                this.emitInput(this.fileList);
            },
            handleExceed(files, fileList) {
       
                this.$message({
       
                    message: "最多只能上传" + this.maxCount + "张图片",
                    type: "warning",
                    duration: 1000,
                });
            },
        },
    };
script>
<style>
style>
  • policy.js代码

/third-service/third-service/oss/policy为你88网关代理的oss服务路由uri地址

import http from '@/utils/httpRequest.js'
export function policy() {
     
    return new Promise((resolve, reject) => {
     
        http({
     
            #修改你88网关代理的oss服务路由uri地址
            url: http.adornUrl("/third-service/third-service/oss/policy"),
            method: "get",
            params: http.adornParams({
     })
        }).then(({
       data }) => {
     
            resolve(data);
        })
    });
}

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

而文件上传前调用的方法::before-upload=“beforeUpload”发现该方法返回了一个new Promise,调用了policy(),该方法是policy.js中的 import { policy } from “./policy”;

Day400&401.商品服务 -谷粒商城_第23张图片

Day400&401.商品服务 -谷粒商城_第24张图片

  • 在vue中看是response.data.policy,在控制台看response.policy。所以去java里面改返回值为R。return R.ok().put(“data”,respMap);
  • 也可以像上面,阿昌这样子直接修改前端代码,选择一个即可

  • 阿里云开启跨域
    开始执行上传,但是在上传过程中,出现了跨域请求问题:
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XndpSwcj-1632569865637)(C:/Users/PePe/AppData/Roaming/Typora/typora-user-images/image-20210925154348021.png)]

    这又是一个跨域的问题,解决方法就是在阿里云上开启跨域访问:
    Day400&401.商品服务 -谷粒商城_第25张图片

  • 配置oss跨域

Day400&401.商品服务 -谷粒商城_第26张图片

Day400&401.商品服务 -谷粒商城_第27张图片

再次执行文件上传。 注意上传时他的key变成了response.dir +getUUID()+"_${filename}";

Day400&401.商品服务 -谷粒商城_第28张图片


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

  • 修改brand-add-or-update如下: :active-value=“1” :inactive-value=“0” # 激活为1,不激活为0
<el-switch
           v-model="dataForm.showStatus"
           active-color="#13ce66"
           inactive-color="#ff4949"
           :active-value="1"
           :inactive-value="0"
           >
el-switch>
  • 添加表单校验&自定义校验器
<script>
    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(parseInt(value)) || parseInt(value) < 0){
       
                callback(new Error("排序字段必须是一个整数"));
            } else {
       
                callback();
            }
        }, trigger: "blur" }]
script>
  • 完整brand-add-or-update修改代码
<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="80px"
                 >
            <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">
                
                <singleUpload v-model="dataForm.logo">singleUpload>
            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: singleUpload,
        },
        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: [
                        {
       
                            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(parseInt(value)) ||
                                    parseInt(value) < 0
                                ) {
       
                                    callback(new Error("排序字段必须是一个整数"));
                                } 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>

4、JSR303数据校验

  • 问题引入

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

  • 如果你的springboot版本没有默认引入,就导入依赖


<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-validationartifactId>
dependency>

里面依赖了hibernate-validator 在非空处理方式上提供了@NotNull,@NotBlank和@NotEmpty

在实体类的属性上使用如上的注解等

@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
     
    private static final long serialVersionUID = 1L;

    /**
	 * 品牌id
	 */
    @TableId
    private Long brandId;
    /**
	 * 品牌名
	 */
    @NotBlank
    private String name;
    /**
	 * 品牌logo地址
	 */
    private String logo;
    /**
	 * 介绍
	 */
    private String descript;
    /**
	 * 显示状态[0-不显示;1-显示]
	 */
	@NotNull
    private Integer showStatus;
    /**
	 * 检索首字母
	 */
    @NotEmpty
    private String firstLetter;
	/**
	 * 排序
	 */
	@NotNull
	@Min(0)
	private Integer sort;

}
  • 步骤2:controller中加校验注解@Valid,开启校验,
@RequestMapping("/save")
public R save(@RequestBody @Valid BrandEntity brand){
     
    brandService.save(brand);
    return R.ok();
}
  • 可以在添加注解的时候,修改message

    @NotBlank(message = "品牌名必须非空")
    private String name; 
    
  • 但是这种返回的错误结果并不符合我们的业务需要。

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

@RequestMapping("/save")
public R save(@RequestBody @Valid 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);
    }
    brandService.save(brand);
    return R.ok();
}

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

  • 统一异常处理@ControllerAdvice步骤4:统一异常处理

可以使用SpringMvc所提供的@ControllerAdvice,通过“basePackages”能够说明处理哪些路径下的异常。

在com.achang.achangmall.product.exception.AchangExceptionControllerAdvice编写

@Slf4j
@RestControllerAdvice(basePackages = "com.achang.achangmall.product")
public class AchangExceptionControllerAdvice {
     

    @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

Day400&401.商品服务 -谷粒商城_第29张图片

如果没有用,可能是spring没有扫描到,在主函数上添加@ComponentScan("com.achang.achangmall.product")

  • 默认异常处理
    @ExceptionHandler(value = Throwable.class)
    public R handleException(Throwable throwable){
     
        log.error("未知异常{},异常类型{}",throwable.getMessage(),throwable.getClass());
        return R.error(400,"数据校验出现问题");
    }
  • 错误状态码

上面代码中,针对于错误状态码,是我们进行随意定义的,然而正规开发过程中,错误状态码有着严格的定义规则,如该在项目中我们的错误状态码定义

Day400&401.商品服务 -谷粒商城_第30张图片

  • 为了定义这些错误状态码,我们可以单独定义一个常量类,用来存储这些错误状态码

com.achang.common.exception,在通用模块中

/***
 * 错误码和错误信息定义类
 * 1. 错误码定义规则为5为数字
 * 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常
 * 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
 * 错误码列表:
 *  10: 通用
 *      001:参数格式校验
 *  11: 商品
 *  12: 订单
 *  13: 购物车
 *  14: 物流
 */
public enum BizCodeEnum {
     
    UNKNOW_EXEPTION(10000,"系统未知异常"),

    VALID_EXCEPTION( 10001,"参数格式校验失败");

    private int code;
    private String msg;

    BizCodeEnum(int code, String msg) {
     
        this.code = code;
        this.msg = msg;
    }

    public int getCode() {
     
        return code;
    }

    public String getMsg() {
     
        return msg;
    }
}

5、分组校验功能(完成多场景的复杂校验)

  • 给校验注解,标注上groups,指定什么情况下才需要进行校验

groups里面的内容要以接口的形式显示出来

如:指定在更新和添加的时候,都需要进行校验。新增时不需要带id,修改时必须带id

  • 在通用模块中创建校验用的空接口,他只是个标识

achangmall-common中的com.achang.common.vail

//更新校验
public interface UpdateVail {
     }

//新增校验
public interface AddVail {
     }
  • 在实例类上groups标志接口标识

在这种情况下,没有指定分组的校验注解,默认是不起作用的。想要起作用就必须要加groups。

@NotNull(message = "修改必须定制品牌id", groups = {
     UpdateVailGroup.class})
@Null(message = "新增不能指定id", groups = {
     AddVailGroup.class})
private Long brandId;
  • 业务方法参数上使用@Validated注解,并用@Validated指定使用校验的接口标识

Day400&401.商品服务 -谷粒商城_第31张图片

  • 分组情况下,校验注解生效问题

  • 默认情况下,在分组校验情况下,没有指定指定分组的校验注解,将不会生效,它只会在不分组的情况下生效。


6、自定义校验功能

  • 场景

    • 要校验showStatus的01状态,可以用正则,但我们可以利用其他方式解决复杂场景。比如我们想要下面的场景
  • 添加依赖

<dependency>
    <groupId>javax.validationgroupId>
    <artifactId>validation-apiartifactId>
    <version>2.0.1.Finalversion>
dependency>
  • 编写自定义的校验注解
/**
 * 自定义校验注解
 */
@Documented
@Constraint(validatedBy = {
      ListValueConstraintValidator.class})
@Target({
      METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
     
    // 使用该属性去Validation.properties中取
    String message() default "{com.atguigu.common.valid.ListValue.message}";

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

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

    int[] value() default {
     };//传入可通过校验的值[]
}
  • 自定义校验器
/**
 * 自定义校验器
 */
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
     //泛型左边:自定义校验注解,泛型右边:校验的类型

    private Set<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 value, ConstraintValidatorContext context) {
     
        return  set.contains(value);
    }
}
  • 关联校验器和校验注解

一个校验注解可以匹配多个校验器

Day400&401.商品服务 -谷粒商城_第32张图片

  • 使用实例
/**
* 显示状态[0-不显示;1-显示]
* 标识只能接受0,1;其他值都不能通过校验
*/
@ListValue(value = {
     0,1},groups ={
     AddGroup.class})
private Integer showStatus;

你可能感兴趣的:(谷粒商城,阿里云oss,oss,注解校验)