概述

背景

谷粒商城中有许多需要保存图片的场景,正规做法是在数据库中保存图片地址,文件则保存在对象存储服务(OSS)中,官方视频教学的是使用阿里云的OSS,这样还可以搭配spring boot cloud alibaba使用,但是本人希望能自己部署的环境尽量自己部署,查询后找到了minIO这款工具,可以实现OSS的大部分功能,使用docker部署也非常方便,这样的话就需要重构很多前后端代码

分析问题

本来个人想要做成前端把文件传到后端,后端再上传到minIO图床,这套流程个人很熟悉。但是教程中使用的是签名直传的方案,想来少了一段文件传输的流程也确实速度更快也更稳定,这在minIO里也可以实现,只是需要一些技巧

  • 签名直传方案的原理是,前端向后端发送请求,携带文件名参数,后端返回给前端一个直传地址,前端直接请求该地址即可上传文件,这样保证了既不会需要在前端填写图床信息(通过返回参数获得),还可以直接由前端上传文件

  • 在el-upload中填入action参数是官方做法,这样该组件会自己调用封装好的post方法请求这个参数地址来直传文件

  • minIO这个软件给的api里,直传地址只能通过put请求访问

所以如果想使用el-upload组件的话,只能放弃自带的请求方法,自己编写上传请求

解决

环境部署

简单记录下该软件的部署过程

  1. 使用docker安装minIO

    1
    2
    docker pull minio/minio
    docker run -p 9000:9000 minio/minio server /data

    注意配置用户名密码的时候密码需要大于8位数,不然报错

  2. 在springboot中配置minIO相关库

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.5.10</version>
    </dependency>
    <dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.11.0</version>
    </dependency>

    注意该库需要高版本的okhttp库,如果版本不对应则需要单独引入

后端代码实现

在thirdparty模块中添加miniocontroller并添加以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package com.example.gulimall.thirdparty.demos.web;

import com.example.common.utils.Constant;
import com.example.common.utils.R;
import io.minio.GetPresignedObjectUrlArgs;
import io.minio.MinioClient;
import io.minio.http.Method;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;

/**
* @Author: destiny
* @Date: 2024/5/14 8:56
*/
@RestController
//@RequestMapping("thirdparty")
public class MinioController {
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.accessKey}")
private String accessKey;
@Value("${minio.secretKey}")
private String secretKey;
@Value("${minio.bucket}")
private String bucket;

@GetMapping("/minio/policy")
private R policy(@RequestParam("pic") String pic) {
String name = UUID.randomUUID() + "-" + pic; // 修改文件名防止上传相同文件被覆盖
//使用日期作为文件夹,目前文件数量较少,先不使用
// DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
// String format = dateTimeFormatter.format(LocalDateTime.now());
// String dir = format; // 用户上传文件时指定的前缀,即上传目录。
// pic = dir + "/" + name;// 文件上传目录+名称
String url = "";
String path = "/" + bucket + "/" + pic;// 该值暂没用到,前端不熟直接使用没用域名的图片地址不知道怎么处理

Map<String, String> respMap = null;
try {

MinioClient minioClient = MinioClient.builder().endpoint(endpoint)
.credentials(accessKey, secretKey).build();
Map<String, String> reqParams = new HashMap<>();
reqParams.put("response-content-type", "application/json");
url = minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.PUT)//这里必须是PUT,如果是GET的话就是文件访问地址了。如果是POST上传会报错.
.bucket(bucket)
.object(pic)
.expiry(60 * 60 * 24)
.extraQueryParams(reqParams)
.build());
System.out.println(url); // 前端直传需要的url地址
respMap = new LinkedHashMap<>();
respMap.put("name", name);
respMap.put("host", url);
respMap.put("path", path);
respMap.put("url", endpoint + path);
} catch (Exception e) {
System.out.println("Error occurred: " + e);
}
return R.ok().put("data", respMap);
}
}

前端代码实现

需要修改谷粒商城源代码的policy.js和upload文件

思路是自己在beforeUpload方法中添加请求直传地址的方法,使用自定义的代码直接将文件上传,不使用el-upload自带的post请求

需要先安装axios

1
npm install axios --save-dev

然后在src/main.js下全局引用一下

1
2
import axios from 'axios';
Vue.prototype.$axios = axios //全局注册,使用方法为:this.$axios
  • policy,js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import http from '@/utils/httpRequest.js'

    export function policy (pic) {
    return new Promise((resolve, reject) => {
    http({
    url: http.adornUrl('/thirdparty/minio/policy'),
    method: 'get',
    params: http.adornParams({pic})
    }).then(({ data }) => {
    resolve(data)
    })
    })
    }

  • singleUpload.Vue

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    <template>
    <div>
    <el-upload
    action=""
    :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'

    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) {
    // 上传之前先调用policy_minio组件的policy方法获取签名url
    return new Promise((resolve, reject) => {
    policy(file.name).then(response => {
    let url = response.data.url
    // 将文件名改为后台返回的(原文件名前拼了段uuid),不然同名文件会覆盖
    let newFileName = response.data.name
    let imageType = 'image/' + newFileName.substring(newFileName.lastIndexOf('.') + 1)
    let newFile = new File([file], response.data.name, {type: imageType})
    this.$axios.request({
    url: response.data.host,
    method: 'put',
    data: newFile
    }).then((res) => {
    this.showFileList = true
    this.fileList.pop()
    this.fileList.push({name: file.name, url: url})
    this.emitInput(this.fileList[0].url)
    }).catch(() => {
    console.log('响应数据:上传失败')
    })
    }).catch(err => {
    console.log(JSON.stringify(err))
    reject(err)
    })
    })
    },
    handleUploadSuccess (res, file) {
    }
    }
    }
    </script>
    <style>

    </style>

效果展示

image-20240515111249172

image-20240515111353311