从零到壹搭建一个商城架构--文件存储(SpringCloud Alibaba-OSS)

1、简介

对象存储服务(Object Storage Service,OSS)是一种海量、安全、低成本、高可靠的云存储服务,适合存放任务类型的文件。容量和处理能力弹性扩展,多种存储类型供选择,全面优化存储成本。

2、文件存储形式

文件存储形式主要是存储的位置不同,一个是存储到我们自己的服务器,另一个是存储到云服务器,比如阿里云存储,七牛等一些云存储。我们系统中主要是使用云存储的方式,这样是比较好扩展的,而且现在的存储文件的服务器也比较便宜,如果放在我们本地服务器,后面存储的文件增多了,还要扩容,比较麻烦,所以我建议放到云存储上,主要的对比图如下:
在这里插入图片描述

3、阿里云的存储服务

  • 第一种方式
    这种上传的方式是:直接把文件上传到我们的服务器,再由我们服务器上传到云存储上,这样我们服务器的压力会比较大,所以不推荐,这里主要是说明我们以前上传文件的形式。
    在这里插入图片描述
  • 第二种方式
    这种方式是:我们要上传文件的时候,用户先要向应用服务器请求上传签名,然后直接上传到云存储服务器上,文件不经过我们服务器,这样服务器压力会小很多。
    在这里插入图片描述
    服务端签名后直传的原理如下:
  1. 用户发送上传Policy请求到应用服务器。
  2. 应用服务器返回上传Policy和签名给用户。
  3. 用户直接上传数据到OSS。

4、在阿里云上申请秘钥

具体申请秘钥的操作查看官网文档

我们申请完密钥后必须当时就要记录下来,只显示一次,后面要用到。
一般情况下,我们需要创建一个子用户,给他授权操作所有文件存储的权限

5、在程序中使用

  • 前提条件

    应用服务器对应的域名可通过公网访问。

    确保应用服务器已经安装Java 1.6以上版本(执行命令java -version进行查看)。

    确保PC端浏览器支持JavaScript。

  • 首先我们创建一个工程,这个工程专门用来集成第三方应用的工程,我起名叫hslmall-third-party

  • 创建好后在pom文件中加入如下依赖

<dependencies>
<dependency>
    <!--加入公共依赖-->
	<groupId>com.hsl.halmall</groupId>
		<artifactId>hslmall-common</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <exclusions>
			<exclusion>
				<groupId>com.baomidou</groupId>
				<artifactId>mybatis-plus-boot-starter</artifactId>
			</exclusion>
		</exclusions>
	</dependency>
    <!--阿里云存储-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alicloud-oss</artifactId>
    </dependency>
</dependencies>
<dependencyManagement>
            <!--阿里管理 -->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2.2.0.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
  • 创建application.yml文件,加入如下内容
server:
  port: 30000
spring:
  application:
    name: hslmall-third-party
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
  • 在nacos注册中心和配置中心配置相关内容

    • 创建第三方集成的命名空间 thirdParty,并把生成的 862b6e4c-3c31-490f-bddf-c419e2be76b9配置到我们的bootstrap.properties文件中

    • 在配置列表的thirdParty添加oss.yml配置

      spring:
        cloud:
          alicloud:
            access-key: 自己申请的access-key
            secret-key: 自己申请的secret-key
            oss:
              endpoint: oss-cn-beijing.aliyuncs.com
              bucket: hslmall
      
  • 新建bootstrap.properties文件,加入如下内容

spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=862b6e4c-3c31-490f-bddf-c419e2be76b9
spring.application.name=hslmall-third-party

spring.cloud.nacos.config.extension-configs[0].data-id=oss.yml
spring.cloud.nacos.config.extension-configs[0].group=DEFAULT_GROUP
spring.cloud.nacos.config.extension-configs[0].refresh=true
  • 在启动主程序中添加**@EnableDiscoveryClient**注解,注册到注册中心

  • 创建一个controller

    package com.hsl.hslmall.thirdparty.controller;
    
    import com.aliyun.oss.OSS;
    import com.aliyun.oss.common.utils.BinaryUtil;
    import com.aliyun.oss.model.MatchMode;
    import com.aliyun.oss.model.PolicyConditions;
    import com.hsl.hslmall.common.utils.R;
    import lombok.extern.log4j.Log4j2;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.LinkedHashMap;
    import java.util.Map;
    
    @RequestMapping("oss")
    @RestController
    @Log4j2
    public class OSSController {
    
        @Autowired
        OSS ossClient;
    
        @Value("${spring.cloud.alicloud.oss.endpoint}")
        private String endpoint;
        @Value("${spring.cloud.alicloud.access-key}")
        private String accessId;
        @Value("${spring.cloud.alicloud.secret-key}")
        private String secretKey;
        @Value("${spring.cloud.alicloud.oss.bucket}")
        private String bucket;
    
    
        /**
         * 上传文件之前,需要先请求一个签名,然后在直接上传到阿里云上
         * @return
         */
        @GetMapping("/policy")
        public R policy(){
    
            //host的格式为bucketname.endpoint
            String host="https://"+bucket+"."+endpoint;
            SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd");
            //用户上传文件时指定的前缀
            String dir=sdf.format(new Date())+"/";
    
            Map<String,String> respMap=null;
            try {
                long expireTime=30;
                long expireEndTime=System.currentTimeMillis()+expireTime*1000;
                Date expiration=new Date(expireEndTime);
                PolicyConditions policyConditions=new PolicyConditions();
                //设置上传文件的大小,这个大小可以自己设定
                policyConditions.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE,0,1048576000);
                //设置保存文件的文件夹
                policyConditions.addConditionItem(MatchMode.StartWith,PolicyConditions.COND_KEY,dir);
    
                String postPolicy=ossClient.generatePostPolicy(expiration,policyConditions);
                byte[] binaryPolicy=postPolicy.getBytes("utf-8");
                String encodedPolicy= BinaryUtil.toBase64String(binaryPolicy);
                String postSignature=ossClient.calculatePostSignature(postPolicy);
    
                respMap=new LinkedHashMap<>();
                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));
    
            }catch (Exception e){
                log.error("获取上传文件签名失败,错误信息:"+e.getMessage());
                e.printStackTrace();
            }
            return R.ok().put("data",respMap);
        }
    }
    
  • 我们配置网关

    spring:
      application:
        name: hslmall-gateway
      cloud:
        nacos:
          # 配置注册中心的地址和命名空间
          discovery:
            server-addr: 127.0.0.1:8848
    #        namespace: 52a5124d-5f65-4b41-bdca-01caacb3f3c4
            # 配置网关路由信息
        gateway:
          routes:
            # 配置第三方服务路由
            - id: third_party_route
              uri: lb://hslmall-third-party
              predicates:
                - Path=/api/thirdparty/**
              filters:
                - RewritePath=/api/thirdparty/(?<segment>.*), /$\{segment}
    

我们的访问路径是:localhost:88/api/thirdparty/oss/policy,启动注册中心,网关和第三方服务

  • 前端vue使用element-ui的upoad组件

    • singleUpload.vue 单文件上传
      复制这个文件内容到自己本地/components/upload/下,后面页面会应用这个组件
    <template> 
      <div>
        <el-upload
          action="https://hslmall.oss-cn-beijing.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 => {
                _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 => {
                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>
    
  • 多文件上传

<template>
  <div>
    <el-upload
      action="https://hslmall.oss-cn-beijing.aliyuncs.com"
      :data="dataObj"
      list-type="picture-card"
      :file-list="fileList"
      :before-upload="beforeUpload"
      :on-remove="handleRemove"
      :on-success="handleUploadSuccess"
      :on-preview="handlePreview"
      :limit="maxCount"
      :on-exceed="handleExceed"
    >
 <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
    }
  },
  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 引用的js
import http from '@/utils/httpRequest.js'
export function policy () {
  return new Promise((resolve, reject) => {
    http({
      url: http.adornUrl('/thirdparty/oss/policy'),
      method: 'get',
      params: http.adornParams({})
    }).then(({ data }) => {
      resolve(data)
    })
  })
}
  • 在我们的页面中使用,并引用刚才写的组件

    <template>
      <el-dialog
        :title="!dataForm.id ? '新增' : '修改'"
        :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> -->
          <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-value="1" :inactive-value="0" active-color="#13ce66" inactive-color="#ff4949"></el-switch>
          <!-- <el-input v-model="dataForm.showStatus" placeholder="显示状态">!-->
        </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 {
        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: '显示状态不能为空', trigger: 'blur' }
              ],
              firstLetter: [
                { required: true, message: '检索首字母不能为空', trigger: 'blur' }
              ],
              sort: [
                { required: true, message: '排序不能为空', trigger: 'blur' }
              ]
            }
          }
        },
        components:{
          singleUpload
        },
        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、遇到的问题

  • 跨域请求报错

    根据官方文档,配置跨域相关的内容

  • 上传文件报400 Bad Response,返回内容提示上传内容超过最大限制

    纠结了半小时,不知道哪里有问题,上网查也没查到,最后在官网中也有碰到类似问题的,啰里啰嗦一大堆,也没解决,最后看到我写的controller中的代码是自己敲的,有个配置

    policyConditions.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE,0,1048576000);
    

    这是配置上传文件的大小的,我当时写的最大是1,郁闷了,后来根据官网实例,写成了和它官网一样的就可以了,

说明:

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));

返回的签名数据的key和value一定不能写错,否则无法上传
好了,以上就是在继承OOS服务到我们系统中的过程,希望有使用同样方法的猿们看到可以有共鸣

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章