feat: 添加亚马逊S3协议云存储支持,移除七牛云相关代码和配置文件,更新Sql文件

deploy
Jie Zheng 2025-06-25 15:58:42 +08:00
parent 50140d8a2c
commit 7728306d5a
20 changed files with 703 additions and 776 deletions

View File

@ -24,7 +24,6 @@ import me.zhengjie.exception.BadRequestException;
import org.apache.poi.util.IOUtils;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@ -136,15 +135,15 @@ public class FileUtil extends cn.hutool.core.io.FileUtil {
String resultSize;
if (size / GB >= 1) {
//如果当前Byte的值大于等于1GB
resultSize = DF.format(size / (float) GB) + "GB ";
resultSize = DF.format(size / (float) GB) + "GB";
} else if (size / MB >= 1) {
//如果当前Byte的值大于等于1MB
resultSize = DF.format(size / (float) MB) + "MB ";
resultSize = DF.format(size / (float) MB) + "MB";
} else if (size / KB >= 1) {
//如果当前Byte的值大于等于1KB
resultSize = DF.format(size / (float) KB) + "KB ";
resultSize = DF.format(size / (float) KB) + "KB";
} else {
resultSize = size + "B ";
resultSize = size + "B";
}
return resultSize;
}

View File

@ -116,3 +116,22 @@ file:
# 文件大小 /M
maxSize: 100
avatarMaxSize: 5
# 亚马逊S3协议云存储配置
#支持七牛云阿里云OSS腾讯云COS华为云OBS移动云EOS等
amz:
s3:
# 地域
region: test
# 地域对应的 endpoint
endPoint: https://s3.test.com
# 访问的域名
domain: https://s3.test.com
# 账号的认证信息,或者子账号的认证信息
accessKey: 填写你的AccessKey
secretKey: 填写你的SecretKey
# 存储桶Bucket
defaultBucket: 填写你的存储桶名称
# 文件存储路径
timeformat: yyyy-MM

View File

@ -127,3 +127,21 @@ file:
# 文件大小 /M
maxSize: 100
avatarMaxSize: 5
# 亚马逊S3协议云存储配置
#支持七牛云阿里云OSS腾讯云COS华为云OBS移动云EOS等
amz:
s3:
# 地域
region: test
# 地域对应的 endpoint
endPoint: https://s3.test.com
# 访问的域名
domain: https://s3.test.com
# 账号的认证信息,或者子账号的认证信息
accessKey: 填写你的AccessKey
secretKey: 填写你的SecretKey
# 存储桶Bucket
defaultBucket: 填写你的存储桶名称
# 文件存储路径
timeformat: yyyy-MM

View File

@ -63,11 +63,6 @@ task:
# 队列容量
queue-capacity: 50
#七牛云
qiniu:
# 文件大小 /M
max-size: 15
#邮箱验证码有效时间/秒
code:
expiration: 300

View File

@ -44,5 +44,13 @@
<artifactId>alipay-sdk-java</artifactId>
<version>${alipay.version}</version>
</dependency>
<!--amazon s3 依赖-->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>2.30.13</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,78 @@
package me.zhengjie.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import java.net.URI;
/**
* @author Zheng Jie
* @date 2025-06-25
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "amz.s3")
public class AmzS3Config {
/**
* Amazon S3 "us-west-2"
* S3
*/
private String region;
/**
* Amazon S3 URL
* 访 S3
*/
private String endPoint;
/**
* Amazon S3
* 访 S3 URL
*/
private String domain;
/**
* Amazon S3 访 ID
* secretKey 使 S3 访
*/
private String accessKey;
/**
* Amazon S3 访
* accessKey 使 S3 访
*/
private String secretKey;
/**
* S3
*
*/
private String defaultBucket;
/**
* "yyyy-MM"
*/
private String timeformat;
/**
* AmazonS3
* 使 endPoint, region, accessKey secretKey
* @Bean Spring
*
* @return AmazonS3
*/
@Bean
public S3Client amazonS3Client() {
return S3Client.builder().region(Region.of(region))
.endpointOverride(URI.create(endPoint))
.credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKey, secretKey)))
.build();
}
}

View File

@ -1,69 +0,0 @@
/*
* Copyright 2019-2025 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.zhengjie.domain;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
/**
*
* @author Zheng Jie
* @date 2018-12-31
*/
@Data
@Entity
@Table(name = "tool_qiniu_config")
public class QiniuConfig implements Serializable {
@Id
@Column(name = "config_id")
@ApiModelProperty(value = "ID")
private Long id;
@NotBlank
@ApiModelProperty(value = "accessKey")
private String accessKey;
@NotBlank
@ApiModelProperty(value = "secretKey")
private String secretKey;
@NotBlank
@ApiModelProperty(value = "存储空间名称作为唯一的 Bucket 识别符")
private String bucket;
/**
* Zone
* Zone.zone0()
* Zone.zone1()
* Zone.zone2()
* Zone.zoneNa0()
* Zone.zoneAs0()
*/
@NotBlank
@ApiModelProperty(value = "Zone表示与机房的对应关系")
private String zone;
@NotBlank
@ApiModelProperty(value = "外链域名,可自定义,需在七牛云绑定")
private String host;
@ApiModelProperty(value = "空间类型:公开/私有")
private String type = "公开";
}

View File

@ -1,64 +0,0 @@
/*
* Copyright 2019-2025 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.zhengjie.domain;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.*;
import java.io.Serializable;
import java.sql.Timestamp;
/**
*
* @author Zheng Jie
* @date 2018-12-31
*/
@Data
@Entity
@Table(name = "tool_qiniu_content")
public class QiniuContent implements Serializable {
@Id
@Column(name = "content_id")
@ApiModelProperty(value = "ID", hidden = true)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
@ApiModelProperty(value = "文件名")
private String key;
@ApiModelProperty(value = "空间名")
private String bucket;
@ApiModelProperty(value = "大小")
private String size;
@ApiModelProperty(value = "文件地址")
private String url;
@ApiModelProperty(value = "文件类型")
private String suffix;
@ApiModelProperty(value = "空间类型:公开/私有")
private String type = "公开";
@UpdateTimestamp
@ApiModelProperty(value = "创建或更新时间")
@Column(name = "update_time")
private Timestamp updateTime;
}

View File

@ -0,0 +1,72 @@
/*
* Copyright 2019-2025 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.zhengjie.domain;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import me.zhengjie.base.BaseEntity;
import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
/**
* @description S3
* @author Zheng Jie
* @date 2025-06-25
**/
@Data
@Entity
@Table(name = "tool_s3_storage")
@EqualsAndHashCode(callSuper = true)
public class S3Storage extends BaseEntity implements Serializable {
@Id
@Column(name = "storage_id")
@ApiModelProperty(value = "ID", hidden = true)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
@ApiModelProperty(value = "文件名称")
private String fileName;
@NotBlank
@ApiModelProperty(value = "真实存储的名称")
private String fileRealName;
@NotBlank
@ApiModelProperty(value = "文件大小")
private String fileSize;
@NotBlank
@ApiModelProperty(value = "文件MIME 类型")
private String fileMimeType;
@NotBlank
@ApiModelProperty(value = "文件类型")
private String fileType;
@NotBlank
@ApiModelProperty(value = "文件路径")
private String filePath;
public void copy(S3Storage source){
BeanUtil.copyProperties(source,this, CopyOptions.create().setIgnoreNullValue(true));
}
}

View File

@ -1,36 +0,0 @@
/*
* Copyright 2019-2025 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.zhengjie.repository;
import me.zhengjie.domain.QiniuConfig;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
/**
* @author Zheng Jie
* @date 2018-12-31
*/
public interface QiNiuConfigRepository extends JpaRepository<QiniuConfig,Long> {
/**
*
* @param type /
*/
@Modifying
@Query(value = "update QiniuConfig set type = ?1")
void update(String type);
}

View File

@ -15,20 +15,22 @@
*/
package me.zhengjie.repository;
import me.zhengjie.domain.QiniuContent;
import me.zhengjie.domain.S3Storage;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
/**
* @author Zheng Jie
* @date 2018-12-31
*/
public interface QiniuContentRepository extends JpaRepository<QiniuContent,Long>, JpaSpecificationExecutor<QiniuContent> {
* @author Zheng Jie
* @date 2025-06-25
*/
public interface S3StorageRepository extends JpaRepository<S3Storage, Long>, JpaSpecificationExecutor<S3Storage> {
/**
* key
* @param key
* @return QiniuContent
*/
QiniuContent findByKey(String key);
}
/**
* ID
* @param id ID
* @return
*/
@Query(value = "SELECT file_path FROM s3_storage WHERE id = ?1", nativeQuery = true)
String selectFilePathById(Long id);
}

View File

@ -1,122 +0,0 @@
/*
* Copyright 2019-2025 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.zhengjie.rest;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.zhengjie.annotation.Log;
import me.zhengjie.domain.QiniuConfig;
import me.zhengjie.domain.QiniuContent;
import me.zhengjie.service.dto.QiniuQueryCriteria;
import me.zhengjie.service.QiNiuService;
import me.zhengjie.utils.PageResult;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
*
* @author
* @date 2018/09/28 6:55:53
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/qiNiuContent")
@Api(tags = "工具:七牛云存储管理")
public class QiniuController {
private final QiNiuService qiNiuService;
@GetMapping(value = "/config")
public ResponseEntity<QiniuConfig> queryQiNiuConfig(){
return new ResponseEntity<>(qiNiuService.find(), HttpStatus.OK);
}
@Log("配置七牛云存储")
@ApiOperation("配置七牛云存储")
@PutMapping(value = "/config")
public ResponseEntity<Object> updateQiNiuConfig(@Validated @RequestBody QiniuConfig qiniuConfig){
qiNiuService.config(qiniuConfig);
qiNiuService.update(qiniuConfig.getType());
return new ResponseEntity<>(HttpStatus.OK);
}
@ApiOperation("导出数据")
@GetMapping(value = "/download")
public void exportQiNiu(HttpServletResponse response, QiniuQueryCriteria criteria) throws IOException {
qiNiuService.downloadList(qiNiuService.queryAll(criteria), response);
}
@ApiOperation("查询文件")
@GetMapping
public ResponseEntity<PageResult<QiniuContent>> queryQiNiu(QiniuQueryCriteria criteria, Pageable pageable){
return new ResponseEntity<>(qiNiuService.queryAll(criteria,pageable),HttpStatus.OK);
}
@ApiOperation("上传文件")
@PostMapping
public ResponseEntity<Object> uploadQiNiu(@RequestParam MultipartFile file){
QiniuContent qiniuContent = qiNiuService.upload(file,qiNiuService.find());
Map<String,Object> map = new HashMap<>(3);
map.put("id",qiniuContent.getId());
map.put("errno",0);
map.put("data",new String[]{qiniuContent.getUrl()});
return new ResponseEntity<>(map,HttpStatus.OK);
}
@Log("同步七牛云数据")
@ApiOperation("同步七牛云数据")
@PostMapping(value = "/synchronize")
public ResponseEntity<Object> synchronizeQiNiu(){
qiNiuService.synchronize(qiNiuService.find());
return new ResponseEntity<>(HttpStatus.OK);
}
@Log("下载文件")
@ApiOperation("下载文件")
@GetMapping(value = "/download/{id}")
public ResponseEntity<Object> downloadQiNiu(@PathVariable Long id){
Map<String,Object> map = new HashMap<>(1);
map.put("url", qiNiuService.download(qiNiuService.findByContentId(id),qiNiuService.find()));
return new ResponseEntity<>(map,HttpStatus.OK);
}
@Log("删除文件")
@ApiOperation("删除文件")
@DeleteMapping(value = "/{id}")
public ResponseEntity<Object> deleteQiNiu(@PathVariable Long id){
qiNiuService.delete(qiNiuService.findByContentId(id),qiNiuService.find());
return new ResponseEntity<>(HttpStatus.OK);
}
@Log("删除多张图片")
@ApiOperation("删除多张图片")
@DeleteMapping
public ResponseEntity<Object> deleteAllQiNiu(@RequestBody Long[] ids) {
qiNiuService.deleteAll(ids, qiNiuService.find());
return new ResponseEntity<>(HttpStatus.OK);
}
}

View File

@ -0,0 +1,104 @@
/*
* Copyright 2019-2025 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.zhengjie.rest;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.zhengjie.annotation.Log;
import me.zhengjie.config.AmzS3Config;
import me.zhengjie.domain.S3Storage;
import me.zhengjie.service.S3StorageService;
import me.zhengjie.service.dto.S3StorageQueryCriteria;
import me.zhengjie.utils.PageResult;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* amz S3
* @author
* @date 2025-06-25
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/s3Storage")
@Api(tags = "工具S3协议云存储管理")
public class S3StorageController {
private final AmzS3Config amzS3Config;
private final S3StorageService s3StorageService;
@ApiOperation("导出数据")
@GetMapping(value = "/download")
@PreAuthorize("@el.check('storage:list')")
public void exportS3Storage(HttpServletResponse response, S3StorageQueryCriteria criteria) throws IOException {
s3StorageService.download(s3StorageService.queryAll(criteria), response);
}
@GetMapping
@ApiOperation("查询文件")
@PreAuthorize("@el.check('storage:list')")
public ResponseEntity<PageResult<S3Storage>> queryS3Storage(S3StorageQueryCriteria criteria, Pageable pageable){
return new ResponseEntity<>(s3StorageService.queryAll(criteria, pageable),HttpStatus.OK);
}
@PostMapping
@ApiOperation("上传文件")
public ResponseEntity<Object> uploadS3Storage(@RequestParam MultipartFile file){
S3Storage storage = s3StorageService.upload(file);
Map<String,Object> map = new HashMap<>(3);
map.put("id",storage.getId());
map.put("errno",0);
map.put("data",new String[]{amzS3Config.getDomain() + "/" + storage.getFilePath()});
return new ResponseEntity<>(map,HttpStatus.OK);
}
@Log("下载文件")
@ApiOperation("下载文件")
@GetMapping(value = "/download/{id}")
public ResponseEntity<Object> downloadS3Storage(@PathVariable Long id){
Map<String,Object> map = new HashMap<>(1);
S3Storage storage = s3StorageService.getById(id);
if (storage == null) {
map.put("message", "文件不存在或已被删除");
return new ResponseEntity<>(map, HttpStatus.NOT_FOUND);
}
// 仅适合公开文件访问,私有文件可以使用服务中的 privateDownload 方法
String url = amzS3Config.getDomain() + "/" + storage.getFilePath();
map.put("url", url);
return new ResponseEntity<>(map,HttpStatus.OK);
}
@Log("删除多个文件")
@DeleteMapping
@ApiOperation("删除多个文件")
@PreAuthorize("@el.check('storage:del')")
public ResponseEntity<Object> deleteAllS3Storage(@RequestBody List<Long> ids) {
s3StorageService.deleteAll(ids);
return new ResponseEntity<>(HttpStatus.OK);
}
}

View File

@ -1,119 +0,0 @@
/*
* Copyright 2019-2025 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.zhengjie.service;
import me.zhengjie.domain.QiniuConfig;
import me.zhengjie.domain.QiniuContent;
import me.zhengjie.service.dto.QiniuQueryCriteria;
import me.zhengjie.utils.PageResult;
import org.springframework.data.domain.Pageable;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
/**
* @author Zheng Jie
* @date 2018-12-31
*/
public interface QiNiuService {
/**
*
* @return QiniuConfig
*/
QiniuConfig find();
/**
*
* @param qiniuConfig
* @return QiniuConfig
*/
QiniuConfig config(QiniuConfig qiniuConfig);
/**
*
* @param criteria
* @param pageable
* @return /
*/
PageResult<QiniuContent> queryAll(QiniuQueryCriteria criteria, Pageable pageable);
/**
*
* @param criteria
* @return /
*/
List<QiniuContent> queryAll(QiniuQueryCriteria criteria);
/**
*
* @param file
* @param qiniuConfig
* @return QiniuContent
*/
QiniuContent upload(MultipartFile file, QiniuConfig qiniuConfig);
/**
*
* @param id ID
* @return QiniuContent
*/
QiniuContent findByContentId(Long id);
/**
*
* @param content
* @param config
* @return String
*/
String download(QiniuContent content, QiniuConfig config);
/**
*
* @param content
* @param config
*/
void delete(QiniuContent content, QiniuConfig config);
/**
*
* @param config
*/
void synchronize(QiniuConfig config);
/**
*
* @param ids ID
* @param config
*/
void deleteAll(Long[] ids, QiniuConfig config);
/**
*
* @param type
*/
void update(String type);
/**
*
* @param queryAll /
* @param response /
* @throws IOException /
*/
void downloadList(List<QiniuContent> queryAll, HttpServletResponse response) throws IOException;
}

View File

@ -0,0 +1,83 @@
/*
* Copyright 2019-2025 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.zhengjie.service;
import me.zhengjie.domain.S3Storage;
import me.zhengjie.service.dto.S3StorageQueryCriteria;
import me.zhengjie.utils.PageResult;
import org.springframework.data.domain.Pageable;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
* @description
* @author Zheng Jie
* @date 2025-06-25
**/
public interface S3StorageService {
/**
*
* @param criteria
* @param pageable
* @return PageResult
*/
PageResult<S3Storage> queryAll(S3StorageQueryCriteria criteria, Pageable pageable);
/**
*
* @param criteria
* @return List<S3StorageDto>
*/
List<S3Storage> queryAll(S3StorageQueryCriteria criteria);
/**
*
* @param ids /
*/
void deleteAll(List<Long> ids);
/**
*
* @param all
* @param response /
* @throws IOException /
*/
void download(List<S3Storage> all, HttpServletResponse response) throws IOException;
/**
*
* @param id ID
*/
Map<String, String> privateDownload(Long id);
/**
*
* @param file
* @return S3Storage
*/
S3Storage upload(MultipartFile file);
/**
* ID
* @param id ID
* @return S3Storage
*/
S3Storage getById(Long id);
}

View File

@ -1,39 +0,0 @@
/*
* Copyright 2019-2025 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.zhengjie.service.dto;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import me.zhengjie.annotation.Query;
import java.sql.Timestamp;
import java.util.List;
/**
* @author Zheng Jie
* @date 2019-6-4 09:54:37
*/
@Data
public class QiniuQueryCriteria{
@ApiModelProperty(value = "名称查询")
@Query(type = Query.Type.INNER_LIKE)
private String key;
@ApiModelProperty(value = "创建时间")
@Query(type = Query.Type.BETWEEN)
private List<Timestamp> createTime;
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2019-2025 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.zhengjie.service.dto;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import me.zhengjie.annotation.Query;
import java.sql.Timestamp;
import java.util.List;
/**
* @author Zheng Jie
* @date 2025-06-25
**/
@Data
public class S3StorageQueryCriteria {
@Query(type = Query.Type.INNER_LIKE)
@ApiModelProperty(value = "文件名称")
private String fileName;
@Query(type = Query.Type.BETWEEN)
@ApiModelProperty(value = "创建时间")
private List<Timestamp> createTime;
}

View File

@ -1,234 +0,0 @@
/*
* Copyright 2019-2025 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.zhengjie.service.impl;
import com.alibaba.fastjson2.JSON;
import com.qiniu.common.QiniuException;
import com.qiniu.http.Response;
import com.qiniu.storage.BucketManager;
import com.qiniu.storage.Configuration;
import com.qiniu.storage.UploadManager;
import com.qiniu.storage.model.DefaultPutRet;
import com.qiniu.storage.model.FileInfo;
import com.qiniu.util.Auth;
import lombok.RequiredArgsConstructor;
import me.zhengjie.domain.QiniuConfig;
import me.zhengjie.domain.QiniuContent;
import me.zhengjie.repository.QiniuContentRepository;
import me.zhengjie.service.dto.QiniuQueryCriteria;
import me.zhengjie.utils.*;
import me.zhengjie.exception.BadRequestException;
import me.zhengjie.repository.QiNiuConfigRepository;
import me.zhengjie.service.QiNiuService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;
/**
* @author Zheng Jie
* @date 2018-12-31
*/
@Service
@RequiredArgsConstructor
@CacheConfig(cacheNames = "qiNiu")
public class QiNiuServiceImpl implements QiNiuService {
private final QiNiuConfigRepository qiNiuConfigRepository;
private final QiniuContentRepository qiniuContentRepository;
@Value("${qiniu.max-size}")
private Long maxSize;
@Override
@Cacheable(key = "'config'")
public QiniuConfig find() {
Optional<QiniuConfig> qiniuConfig = qiNiuConfigRepository.findById(1L);
return qiniuConfig.orElseGet(QiniuConfig::new);
}
@Override
@CachePut(key = "'config'")
@Transactional(rollbackFor = Exception.class)
public QiniuConfig config(QiniuConfig qiniuConfig) {
qiniuConfig.setId(1L);
String http = "http://", https = "https://";
if (!(qiniuConfig.getHost().toLowerCase().startsWith(http)||qiniuConfig.getHost().toLowerCase().startsWith(https))) {
throw new BadRequestException("外链域名必须以http://或者https://开头");
}
return qiNiuConfigRepository.save(qiniuConfig);
}
@Override
public PageResult<QiniuContent> queryAll(QiniuQueryCriteria criteria, Pageable pageable){
return PageUtil.toPage(qiniuContentRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder),pageable));
}
@Override
public List<QiniuContent> queryAll(QiniuQueryCriteria criteria) {
return qiniuContentRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder));
}
@Override
@Transactional(rollbackFor = Exception.class)
public QiniuContent upload(MultipartFile file, QiniuConfig qiniuConfig) {
FileUtil.checkSize(maxSize, file.getSize());
if(qiniuConfig.getId() == null){
throw new BadRequestException("请先添加相应配置,再操作");
}
// 构造一个带指定Zone对象的配置类
Configuration cfg = new Configuration(QiNiuUtil.getRegion(qiniuConfig.getZone()));
UploadManager uploadManager = new UploadManager(cfg);
Auth auth = Auth.create(qiniuConfig.getAccessKey(), qiniuConfig.getSecretKey());
String upToken = auth.uploadToken(qiniuConfig.getBucket());
try {
String key = file.getOriginalFilename();
if(qiniuContentRepository.findByKey(key) != null) {
key = QiNiuUtil.getKey(key);
}
Response response = uploadManager.put(file.getBytes(), key, upToken);
//解析上传成功的结果
DefaultPutRet putRet = JSON.parseObject(response.bodyString(), DefaultPutRet.class);
QiniuContent content = qiniuContentRepository.findByKey(FileUtil.getFileNameNoEx(putRet.key));
if(content == null){
//存入数据库
QiniuContent qiniuContent = new QiniuContent();
qiniuContent.setSuffix(FileUtil.getExtensionName(putRet.key));
qiniuContent.setBucket(qiniuConfig.getBucket());
qiniuContent.setType(qiniuConfig.getType());
qiniuContent.setKey(FileUtil.getFileNameNoEx(putRet.key));
qiniuContent.setUrl(qiniuConfig.getHost()+"/"+putRet.key);
qiniuContent.setSize(FileUtil.getSize(Integer.parseInt(String.valueOf(file.getSize()))));
return qiniuContentRepository.save(qiniuContent);
}
return content;
} catch (Exception e) {
throw new BadRequestException(e.getMessage());
}
}
@Override
public QiniuContent findByContentId(Long id) {
QiniuContent qiniuContent = qiniuContentRepository.findById(id).orElseGet(QiniuContent::new);
ValidationUtil.isNull(qiniuContent.getId(),"QiniuContent", "id",id);
return qiniuContent;
}
@Override
public String download(QiniuContent content,QiniuConfig config){
String finalUrl;
String type = "公开";
if(type.equals(content.getType())){
finalUrl = content.getUrl();
} else {
Auth auth = Auth.create(config.getAccessKey(), config.getSecretKey());
// 1小时可以自定义链接过期时间
long expireInSeconds = 3600;
finalUrl = auth.privateDownloadUrl(content.getUrl(), expireInSeconds);
}
return finalUrl;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(QiniuContent content, QiniuConfig config) {
//构造一个带指定Zone对象的配置类
Configuration cfg = new Configuration(QiNiuUtil.getRegion(config.getZone()));
Auth auth = Auth.create(config.getAccessKey(), config.getSecretKey());
BucketManager bucketManager = new BucketManager(auth, cfg);
try {
bucketManager.delete(content.getBucket(), content.getKey() + "." + content.getSuffix());
qiniuContentRepository.delete(content);
} catch (QiniuException ex) {
qiniuContentRepository.delete(content);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void synchronize(QiniuConfig config) {
if(config.getId() == null){
throw new BadRequestException("请先添加相应配置,再操作");
}
//构造一个带指定Zone对象的配置类
Configuration cfg = new Configuration(QiNiuUtil.getRegion(config.getZone()));
Auth auth = Auth.create(config.getAccessKey(), config.getSecretKey());
BucketManager bucketManager = new BucketManager(auth, cfg);
//文件名前缀
String prefix = "";
//每次迭代的长度限制最大1000推荐值 1000
int limit = 1000;
//指定目录分隔符,列出所有公共前缀(模拟列出目录效果)。缺省值为空字符串
String delimiter = "";
//列举空间文件列表
BucketManager.FileListIterator fileListIterator = bucketManager.createFileListIterator(config.getBucket(), prefix, limit, delimiter);
while (fileListIterator.hasNext()) {
//处理获取的file list结果
QiniuContent qiniuContent;
FileInfo[] items = fileListIterator.next();
for (FileInfo item : items) {
if(qiniuContentRepository.findByKey(FileUtil.getFileNameNoEx(item.key)) == null){
qiniuContent = new QiniuContent();
qiniuContent.setSize(FileUtil.getSize(Integer.parseInt(String.valueOf(item.fsize))));
qiniuContent.setSuffix(FileUtil.getExtensionName(item.key));
qiniuContent.setKey(FileUtil.getFileNameNoEx(item.key));
qiniuContent.setType(config.getType());
qiniuContent.setBucket(config.getBucket());
qiniuContent.setUrl(config.getHost()+"/"+item.key);
qiniuContentRepository.save(qiniuContent);
}
}
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteAll(Long[] ids, QiniuConfig config) {
for (Long id : ids) {
delete(findByContentId(id), config);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void update(String type) {
qiNiuConfigRepository.update(type);
}
@Override
public void downloadList(List<QiniuContent> queryAll, HttpServletResponse response) throws IOException {
List<Map<String, Object>> list = new ArrayList<>();
for (QiniuContent content : queryAll) {
Map<String,Object> map = new LinkedHashMap<>();
map.put("文件名", content.getKey());
map.put("文件类型", content.getSuffix());
map.put("空间名称", content.getBucket());
map.put("文件大小", content.getSize());
map.put("空间类型", content.getType());
map.put("创建日期", content.getUpdateTime());
list.add(map);
}
FileUtil.downloadExcel(list, response);
}
}

View File

@ -0,0 +1,264 @@
/*
* Copyright 2019-2025 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.zhengjie.service.impl;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.zhengjie.config.AmzS3Config;
import me.zhengjie.domain.S3Storage;
import me.zhengjie.exception.BadRequestException;
import me.zhengjie.repository.S3StorageRepository;
import me.zhengjie.service.S3StorageService;
import me.zhengjie.service.dto.S3StorageQueryCriteria;
import me.zhengjie.utils.*;
import org.apache.commons.io.IOUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.core.ResponseInputStream;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.core.waiters.WaiterResponse;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.*;
import software.amazon.awssdk.services.s3.waiters.S3Waiter;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;
/**
* @description
* @author Zheng Jie
* @date 2025-06-25
**/
@Slf4j
@Service
@RequiredArgsConstructor
public class S3StorageServiceImpl implements S3StorageService {
private final S3Client s3Client;
private final AmzS3Config amzS3Config;
private final S3StorageRepository s3StorageRepository;
@Override
public S3Storage getById(Long id) {
return s3StorageRepository.findById(id).orElse(null);
}
@Override
public PageResult<S3Storage> queryAll(S3StorageQueryCriteria criteria, Pageable pageable){
Page<S3Storage> page = s3StorageRepository.findAll((root, criteriaQuery, criteriaBuilder)
-> QueryHelp.getPredicate(root,criteria,criteriaBuilder),pageable);
return PageUtil.toPage(page);
}
@Override
public List<S3Storage> queryAll(S3StorageQueryCriteria criteria){
return s3StorageRepository.findAll((root, criteriaQuery, criteriaBuilder)
-> QueryHelp.getPredicate(root,criteria,criteriaBuilder));
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteAll(List<Long> ids) {
// 检查桶是否存在
String bucketName = amzS3Config.getDefaultBucket();
if (!bucketExists(bucketName)) {
throw new BadRequestException("存储桶不存在,请检查配置或权限。");
}
// 遍历 ID 列表,删除对应的文件和数据库记录
for (Long id : ids) {
String filePath = s3StorageRepository.selectFilePathById(id);
if (filePath == null) {
System.err.println("未找到 ID 为 " + id + " 的文件记录,无法删除。");
continue;
}
try {
// 创建 DeleteObjectRequest指定存储桶和文件键
DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder()
.bucket(bucketName)
.key(filePath)
.build();
// 调用 deleteObject 方法
s3Client.deleteObject(deleteObjectRequest);
// 删除数据库数据
s3StorageRepository.deleteById(id);
} catch (S3Exception e) {
// 处理 AWS 特定的异常
log.error("从 S3 删除文件时出错: {}", e.awsErrorDetails().errorMessage(), e);
}
}
}
@Override
public S3Storage upload(MultipartFile file) {
String bucketName = amzS3Config.getDefaultBucket();
// 检查存储桶是否存在
if (!bucketExists(bucketName)) {
log.warn("存储桶 {} 不存在,尝试创建...", bucketName);
if (createBucket(bucketName)){
log.info("存储桶 {} 创建成功。", bucketName);
} else {
throw new BadRequestException("存储桶创建失败,请检查配置或权限。");
}
}
// 获取文件名
String originalName = file.getOriginalFilename();
if (StringUtils.isBlank(originalName)) {
throw new IllegalArgumentException("文件名不能为空");
}
// 生成存储路径和文件名
String folder = DateUtil.format(new Date(), amzS3Config.getTimeformat());
String fileName = IdUtil.simpleUUID() + "." + FileUtil.getExtensionName(originalName);
String filePath = folder + "/" + fileName;
// 构建上传请求
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(amzS3Config.getDefaultBucket())
.key(filePath)
.build();
// 创建 S3Storage 实例
S3Storage s3Storage = new S3Storage();
try {
// 上传文件到 S3
s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(file.getInputStream(), file.getSize()));
// 设置 S3Storage 属性
s3Storage.setFileMimeType(FileUtil.getMimeType(originalName));
s3Storage.setFileName(originalName);
s3Storage.setFileRealName(fileName);
s3Storage.setFileSize(FileUtil.getSize(file.getSize()));
s3Storage.setFileType(FileUtil.getExtensionName(originalName));
s3Storage.setFilePath(filePath);
// 保存入库
s3StorageRepository.save(s3Storage);
} catch (IOException e) {
throw new RuntimeException(e);
}
// 设置地址
return s3Storage;
}
@Override
public void download(List<S3Storage> all, HttpServletResponse response) throws IOException {
List<Map<String, Object>> list = new ArrayList<>();
for (S3Storage s3Storage : all) {
Map<String,Object> map = new LinkedHashMap<>();
map.put("文件名称", s3Storage.getFileName());
map.put("真实存储的名称", s3Storage.getFileRealName());
map.put("文件大小", s3Storage.getFileSize());
map.put("文件MIME 类型", s3Storage.getFileMimeType());
map.put("文件类型", s3Storage.getFileType());
map.put("文件路径", s3Storage.getFilePath());
map.put("创建者", s3Storage.getCreateBy());
map.put("更新者", s3Storage.getUpdateBy());
map.put("创建日期", s3Storage.getCreateTime());
map.put("更新时间", s3Storage.getUpdateTime());
list.add(map);
}
FileUtil.downloadExcel(list, response);
}
public Map<String, String> privateDownload(Long id) {
S3Storage storage = s3StorageRepository.findById(id).orElse(null);
if (storage == null) {
throw new BadRequestException("文件不存在或已被删除");
}
// 创建 GetObjectRequest指定存储桶和文件键
GetObjectRequest getObjectRequest = GetObjectRequest.builder()
.bucket(amzS3Config.getDefaultBucket())
.key(storage.getFilePath())
.build();
String base64Data;
// 使用 try-with-resources 确保流能被自动关闭
// s3Client.getObject() 返回一个 ResponseInputStream它是一个包含S3对象数据的输入流
try (ResponseInputStream<GetObjectResponse> s3InputStream = s3Client.getObject(getObjectRequest)) {
// 使用 IOUtils.toByteArray 将输入流直接转换为字节数组
byte[] fileBytes = IOUtils.toByteArray(s3InputStream);
// 使用 Java 内置的 Base64 编码器将字节数组转换为 Base64 字符串
base64Data = Base64.getEncoder().encodeToString(fileBytes);
} catch (S3Exception e) {
// 处理 AWS 特定的异常
throw new BadRequestException("从 S3 下载文件时出错: " + e.awsErrorDetails().errorMessage());
} catch (IOException e) {
// 处理通用的 IO 异常 (IOUtils.toByteArray 可能会抛出)
throw new BadRequestException("读取 S3 输入流时出错: " + e.getMessage());
}
// 构造返回数据
Map<String, String> responseData = new HashMap<>();
// 文件名
responseData.put("fileName", storage.getFileName());
// 文件类型
responseData.put("fileMimeType", storage.getFileMimeType());
// 文件内容
responseData.put("base64Data", base64Data);
return responseData;
}
/**
*
* @param bucketName
*/
@SuppressWarnings({"all"})
private boolean bucketExists(String bucketName) {
try {
HeadBucketRequest headBucketRequest = HeadBucketRequest.builder()
.bucket(bucketName)
.build();
s3Client.headBucket(headBucketRequest);
return true;
} catch (S3Exception e) {
// 如果状态码是 404 (Not Found), 说明存储桶不存在
if (e.statusCode() == 404) {
log.error("存储桶 '{}' 不存在。", bucketName);
return false;
}
// 其他异常 (如 403 Forbidden) 说明存在问题,但不能断定它不存在
throw new BadRequestException("检查存储桶时出错: " + e.awsErrorDetails().errorMessage());
}
}
/**
*
* @param bucketName
*/
private boolean createBucket(String bucketName) {
try {
// 使用 S3Waiter 等待存储桶创建完成
S3Waiter s3Waiter = s3Client.waiter();
CreateBucketRequest bucketRequest = CreateBucketRequest.builder()
.bucket(bucketName)
.acl(BucketCannedACL.PRIVATE)
.build();
s3Client.createBucket(bucketRequest);
// 等待直到存储桶创建完成
HeadBucketRequest bucketRequestWait = HeadBucketRequest.builder()
.bucket(bucketName)
.build();
// 使用 WaiterResponse 等待存储桶存在
WaiterResponse<HeadBucketResponse> waiterResponse = s3Waiter.waitUntilBucketExists(bucketRequestWait);
waiterResponse.matched().response().ifPresent(response ->
log.info("存储桶 '{}' 创建成功,状态: {}", bucketName, response.sdkHttpResponse().statusCode())
);
} catch (BucketAlreadyOwnedByYouException e) {
log.warn("存储桶 '{}' 已经被您拥有,无需重复创建。", bucketName);
} catch (S3Exception e) {
throw new BadRequestException("创建存储桶时出错: " + e.awsErrorDetails().errorMessage());
}
return true;
}
}

View File

@ -1,71 +0,0 @@
/*
* Copyright 2019-2025 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.zhengjie.utils;
import com.qiniu.storage.Region;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
*
* @author Zheng Jie
* @date 2018-12-31
*/
public class QiNiuUtil {
private static final String HUAD = "华东";
private static final String HUAB = "华北";
private static final String HUAN = "华南";
private static final String BEIM = "北美";
/**
*
* @param zone
* @return Region
*/
public static Region getRegion(String zone){
if(HUAD.equals(zone)){
return Region.huadong();
} else if(HUAB.equals(zone)){
return Region.huabei();
} else if(HUAN.equals(zone)){
return Region.huanan();
} else if (BEIM.equals(zone)){
return Region.beimei();
// 否则就是东南亚
} else {
return Region.qvmHuadong();
}
}
/**
* keyhash
* @param file
* @return String
*/
public static String getKey(String file){
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
Date date = new Date();
return FileUtil.getFileNameNoEx(file) + "-" +
sdf.format(date) +
"." +
FileUtil.getExtensionName(file);
}
}