Files
chuanggao-website/design/MinIO配置说明.md
2026-05-12 16:53:18 +08:00

14 KiB
Raw Blame History

创高家具官网 - MinIO配置说明

一、MinIO基础信息

配置项 说明
服务地址 http://localhost:9000 API端口
控制台 http://localhost:9001 Web管理界面
Bucket名称 chuanggao-images 统一图片存储桶
访问策略 公开读Public Read 图片可直接访问

二、MinIO部署步骤

2.1 Docker部署推荐

# docker-compose.yml
version: '3.8'

services:
  minio:
    image: minio/minio:latest
    container_name: minio-cg
    ports:
      - "9000:9000"  # API端口
      - "9001:9001"  # 控制台端口
    environment:
      MINIO_ROOT_USER: chuanggao-admin
      MINIO_ROOT_PASSWORD: YourStrongPassword123!
      MINIO_SERVER_URL: http://localhost:9000
      MINIO_BROWSER_REDIRECT_URL: http://localhost:9001
    volumes:
      - ./minio-data:/data
    command: server /data --console-address ":9001"
    restart: unless-stopped

# 创建并启动
# docker-compose up -d

2.2 验证部署

# 查看容器状态
docker ps | grep minio

# 查看日志
docker logs minio-cg

# 访问控制台
open http://localhost:9001
# 用户名: chuanggao-admin
# 密码: YourStrongPassword123!

三、Bucket创建与配置

3.1 创建Bucket

方式1Web控制台

  1. 访问 http://localhost:9001
  2. 登录后点击 "Create Bucket"
  3. 输入名称:chuanggao-images
  4. 确认创建

方式2mc命令行

# 安装mc客户端
wget https://dl.min.io/client/mc/release/linux-amd64/mc
chmod +x mc
sudo mv mc /usr/local/bin/

# 配置MinIO连接
mc alias set chuanggao http://localhost:9000 chuanggao-admin YourStrongPassword123!

# 创建bucket
mc mb chuanggao/chuanggao-images

# 验证
mc ls chuanggao

3.2 设置公开读权限

方式1Web控制台

  1. 进入 Bucket → chuanggao-images
  2. 点击 "Access Policy"
  3. 选择 "Public"
  4. 保存

方式2mc命令行

# 设置bucket为公开读
mc anonymous set download chuanggao/chuanggao-images

# 验证策略
mc anonymous get chuanggao/chuanggao-images

3.3 设置CORS跨域

# 创建cors配置文件
cat > cors-config.json << 'EOF'
{
  "CORSRules": [
    {
      "AllowedOrigins": ["*"],
      "AllowedMethods": ["GET", "PUT", "POST", "DELETE"],
      "AllowedHeaders": ["*"],
      "ExposeHeaders": ["ETag"],
      "MaxAgeSeconds": 3000
    }
  ]
}
EOF

# 应用CORS配置
mc admin bucket cors set chuanggao/chuanggao-images cors-config.json

四、Spring Boot集成配置

4.1 application.yml

# 文件存储配置
upload:
  storage: minio  # local 或 minio
  path: ./uploads  # local模式使用

# MinIO配置
minio:
  endpoint: http://localhost:9000
  access-key: ${MINIO_ACCESS_KEY:chuanggao-admin}
  secret-key: ${MINIO_SECRET_KEY:YourStrongPassword123!}
  bucket: chuanggao-images
  # 公开访问URL用于生成图片直链
  public-url: http://localhost:9000/chuanggao-images

4.2 MinioConfig.java

package com.chuanggao.config;

import io.minio.MinioClient;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Data
@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinioConfig {
    private String endpoint;
    private String accessKey;
    private String secretKey;
    private String bucket;
    private String publicUrl;

    @Bean
    public MinioClient minioClient() {
        return MinioClient.builder()
                .endpoint(endpoint)
                .credentials(accessKey, secretKey)
                .build();
    }
}

4.3 MinioService.java

package com.chuanggao.service;

import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.RemoveObjectArgs;
import io.minio.errors.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.UUID;

@Slf4j
@Service
@RequiredArgsConstructor
public class MinioService {

    private final MinioClient minioClient;
    private final MinioConfig minioConfig;

    /**
     * 上传文件
     */
    public String uploadFile(MultipartFile file, String directory) 
            throws IOException, InvalidKeyException, NoSuchAlgorithmException, 
                   InsufficientDataException, InternalException, InvalidResponseException, 
                   NoSuchBucketException, XmlParserException, ErrorResponseException, 
                   ServerException {
        
        // 生成唯一文件名
        String originalFilename = file.getOriginalFilename();
        String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
        String newFilename = UUID.randomUUID().toString().replace("-", "") + extension;
        
        // 构建存储路径: uploads/目录/文件名
        String objectKey = "uploads/" + directory + "/" + newFilename;
        
        // 上传
        minioClient.putObject(
            PutObjectArgs.builder()
                .bucket(minioConfig.getBucket())
                .object(objectKey)
                .stream(file.getInputStream(), file.getSize(), -1)
                .contentType(file.getContentType())
                .build()
        );
        
        // 返回访问路径
        return "/" + objectKey;
    }

    /**
     * 删除文件
     */
    public void deleteFile(String objectKey) 
            throws IOException, InvalidKeyException, NoSuchAlgorithmException,
                   InsufficientDataException, InternalException, InvalidResponseException,
                   NoSuchBucketException, XmlParserException, ErrorResponseException,
                   ServerException {
        
        // 移除开头的 /
        if (objectKey.startsWith("/")) {
            objectKey = objectKey.substring(1);
        }
        
        minioClient.removeObject(
            RemoveObjectArgs.builder()
                .bucket(minioConfig.getBucket())
                .object(objectKey)
                .build()
        );
    }

    /**
     * 获取完整访问URL
     */
    public String getFullUrl(String objectKey) {
        if (objectKey == null || objectKey.isEmpty()) {
            return null;
        }
        
        // 移除开头的 /
        if (objectKey.startsWith("/")) {
            objectKey = objectKey.substring(1);
        }
        
        return minioConfig.getPublicUrl() + "/" + objectKey;
    }
}

五、上传Controller

package com.chuanggao.controller.admin;

import com.chuanggao.service.MinioService;
import com.chuanggao.service.MediaLibraryService;
import com.chuanggao.common.Result;
import com.chuanggao.vo.MediaVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

@Slf4j
@RestController
@RequestMapping("/api/admin/upload")
@RequiredArgsConstructor
@SaCheckLogin
public class AdminUploadController {

    private final MinioService minioService;
    private final MediaLibraryService mediaLibraryService;

    /**
     * 单文件上传
     */
    @PostMapping("/single")
    public Result<MediaVO> uploadSingle(
            @RequestParam("file") MultipartFile file,
            @RequestParam(defaultValue = "other") String usedIn) {
        
        try {
            // 1. 上传到MinIO
            String directory = getDirectoryByUsedIn(usedIn);
            String filePath = minioService.uploadFile(file, directory);
            
            // 2. 获取完整URL
            String fullUrl = minioService.getFullUrl(filePath);
            
            // 3. 保存到媒体库
            MediaVO media = mediaLibraryService.saveMedia(file, filePath, fullUrl, usedIn);
            
            log.info("文件上传成功: {}, 路径: {}", file.getOriginalFilename(), filePath);
            return Result.success(media);
            
        } catch (Exception e) {
            log.error("文件上传失败", e);
            return Result.error("上传失败: " + e.getMessage());
        }
    }

    /**
     * 多文件上传
     */
    @PostMapping("/multiple")
    public Result<List<MediaVO>> uploadMultiple(
            @RequestParam("files") MultipartFile[] files,
            @RequestParam(defaultValue = "other") String usedIn) {
        
        // 批量上传逻辑...
        return Result.success(mediaList);
    }

    /**
     * 根据用途确定存储目录
     */
    private String getDirectoryByUsedIn(String usedIn) {
        return switch (usedIn) {
            case "carousel" -> "carousel";
            case "product" -> "products/" + DateUtil.format(new Date(), "yyyy/MM");
            case "case" -> "cases/" + DateUtil.format(new Date(), "yyyy/MM");
            case "news" -> "news/" + DateUtil.format(new Date(), "yyyy/MM");
            case "page" -> "pages";
            case "company" -> "company";
            case "honor" -> "honor";
            default -> "others";
        };
    }
}

六、存储目录结构

chuanggao-images (bucket)
├── uploads/
│   ├── carousel/           # 轮播图
│   │   ├── home-banner-01.jpg
│   │   └── about-banner-01.jpg
│   │
│   ├── products/           # 产品图片
│   │   ├── 2024/
│   │   │   ├── 01/
│   │   │   │   ├── abc123.jpg
│   │   │   │   └── def456.jpg
│   │   │   └── 02/
│   │   └── thumbs/         # 缩略图(自动生成)
│   │
│   ├── cases/              # 案例图片
│   │   └── 2024/
│   │       └── 01/
│   │
│   ├── news/               # 新闻封面
│   │   └── 2024/
│   │       └── 01/
│   │
│   ├── pages/              # 页面区块图片
│   │   ├── home/
│   │   └── about/
│   │
│   ├── company/            # 公司相关
│   │   ├── logo.png
│   │   └── wechat-qr.jpg
│   │
│   ├── honor/              # 荣誉证书
│   └── others/             # 其他
│
└── temp/                   # 临时文件(定期清理)

七、前端访问方式

7.1 直接访问(公开读)

由于bucket设置为public read图片可直接访问

<!-- HTML -->
<img src="http://localhost:9000/chuanggao-images/uploads/products/2024/01/abc123.jpg" alt="产品图">
<!-- Vue -->
<template>
  <img :src="imageUrl" alt="产品图">
</template>

<script setup>
const imageUrl = 'http://localhost:9000/chuanggao-images/uploads/products/2024/01/abc123.jpg'
</script>

7.2 通过API获取推荐

// 从后端获取完整URL
const getImageUrl = async (mediaId) => {
  const res = await fetch(`/api/admin/media/url/${mediaId}`)
  const data = await res.json()
  return data.data.url  // 返回MinIO直链
}

八、Nginx反代配置生产环境

server {
    listen 80;
    server_name www.chuanggao.com;
    
    # 前端
    location / {
        root /var/www/chuanggao-website/client/dist;
        try_files $uri $uri/ /index.html;
    }
    
    # 后端API
    location /api {
        proxy_pass http://localhost:8080/api;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
    
    # MinIO访问可选如需统一域名
    location /images {
        proxy_pass http://localhost:9000/chuanggao-images;
        proxy_set_header Host $host;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

九、安全建议

9.1 生产环境配置

# docker-compose.prod.yml
version: '3.8'

services:
  minio:
    image: minio/minio:latest
    container_name: minio-cg
    ports:
      - "127.0.0.1:9000:9000"  # 仅本地访问,不暴露公网
      - "127.0.0.1:9001:9001"
    environment:
      MINIO_ROOT_USER: ${MINIO_ROOT_USER}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}  # 从环境变量读取
    volumes:
      - /data/minio:/data
    command: server /data --console-address ":9001"
    restart: always

9.2 环境变量管理

# .env 文件不提交到git
MINIO_ROOT_USER=chuanggao-admin
MINIO_ROOT_PASSWORD=YourSuperSecretPassword!!!
MINIO_ACCESS_KEY=chuanggao-app
MINIO_SECRET_KEY=AnotherSecretKey123

9.3 备份策略

# 定时备份脚本 backup-minio.sh
#!/bin/bash
DATE=$(date +%Y%m%d)
mc mirror chuanggao/chuanggao-images /backup/minio/chuanggao-images-$DATE
find /backup/minio -name "chuanggao-images-*" -mtime +30 -delete

十、常见问题

Q1: 上传失败,提示连接拒绝

检查MinIO服务是否启动端口9000是否被占用

Q2: 图片无法访问返回403

检查bucket权限是否为public或CORS配置是否正确

Q3: 中文文件名乱码

上传前对文件名进行URL编码或改用UUID重命名

Q4: 大文件上传超时

调整spring.servlet.multipart.max-file-size和max-request-size


十一、测试命令

# 1. 测试MinIO连接
mc alias set local http://localhost:9000 chuanggao-admin YourStrongPassword123!

# 2. 上传测试文件
mc cp test.jpg local/chuanggao-images/uploads/test/

# 3. 查看文件
mc ls local/chuanggao-images/uploads/test/

# 4. 获取公开访问URL
echo "http://localhost:9000/chuanggao-images/uploads/test/test.jpg"

# 5. 浏览器访问测试
curl -I http://localhost:9000/chuanggao-images/uploads/test/test.jpg
# 应返回 200 OK