OSS:图片、文件上传接入对象存储minio
This commit is contained in:
parent
6a803952a8
commit
f8a8474f0f
@ -1,5 +1,6 @@
|
||||
package com.fastbee.web.controller.common;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import com.fastbee.common.config.RuoYiConfig;
|
||||
import com.fastbee.common.constant.Constants;
|
||||
import com.fastbee.common.core.domain.AjaxResult;
|
||||
@ -7,8 +8,11 @@ import com.fastbee.common.utils.MessageUtils;
|
||||
import com.fastbee.common.utils.StringUtils;
|
||||
import com.fastbee.common.utils.file.FileUploadUtils;
|
||||
import com.fastbee.common.utils.file.FileUtils;
|
||||
import com.fastbee.common.utils.file.MimeTypeUtils;
|
||||
import com.fastbee.common.utils.file.QRCodeUtils;
|
||||
import com.fastbee.framework.config.ServerConfig;
|
||||
import com.fastbee.oss.entity.UploadResult;
|
||||
import com.fastbee.oss.enums.OssBizPath;
|
||||
import com.fastbee.oss.service.OssStorageService;
|
||||
import com.google.zxing.client.j2se.MatrixToImageWriter;
|
||||
import com.google.zxing.common.BitMatrix;
|
||||
import io.swagger.annotations.Api;
|
||||
@ -26,6 +30,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@ -41,13 +46,14 @@ import java.util.List;
|
||||
public class CommonController {
|
||||
private static final Logger log = LoggerFactory.getLogger(CommonController.class);
|
||||
private static final String FILE_DELIMETER = ",";
|
||||
|
||||
@Autowired
|
||||
private ServerConfig serverConfig;
|
||||
private OssStorageService ossStorageService;
|
||||
|
||||
/**
|
||||
* 通用下载请求
|
||||
*
|
||||
* @param fileName 文件名称
|
||||
* @param fileName 文件名称,可为对象存储地址或本地相对路径
|
||||
* @param delete 是否删除
|
||||
*/
|
||||
@ApiOperation("文件下载")
|
||||
@ -57,13 +63,23 @@ public class CommonController {
|
||||
if (!FileUtils.checkAllowDownload(fileName)) {
|
||||
throw new Exception(StringUtils.format(MessageUtils.message("download.filename.not.valid"), fileName));
|
||||
}
|
||||
String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1);
|
||||
String filePath = RuoYiConfig.getDownloadPath() + fileName;
|
||||
|
||||
String realFileName = resolveDownloadName(fileName);
|
||||
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
|
||||
FileUtils.setAttachmentResponseHeader(response, realFileName);
|
||||
|
||||
if (ossStorageService.isOssUrl(fileName)) {
|
||||
try (InputStream inputStream = ossStorageService.open(fileName)) {
|
||||
IoUtil.copy(inputStream, response.getOutputStream());
|
||||
}
|
||||
if (Boolean.TRUE.equals(delete)) {
|
||||
ossStorageService.delete(fileName);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 历史遗留的本地磁盘资源,保留原读取链路
|
||||
String filePath = RuoYiConfig.getDownloadPath() + fileName;
|
||||
FileUtils.writeBytes(filePath, response.getOutputStream());
|
||||
if (delete) {
|
||||
if (Boolean.TRUE.equals(delete)) {
|
||||
FileUtils.deleteFile(filePath);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@ -78,15 +94,14 @@ public class CommonController {
|
||||
@PostMapping("/upload")
|
||||
public AjaxResult uploadFile(MultipartFile file) throws Exception {
|
||||
try {
|
||||
// 上传文件路径
|
||||
String filePath = RuoYiConfig.getUploadPath();
|
||||
// 上传并返回新文件名称
|
||||
String fileName = FileUploadUtils.upload(filePath, file);
|
||||
String url = serverConfig.getUrl() + fileName;
|
||||
// 沿用原有的体积与扩展名白名单校验;切到对象存储后不能丢掉这层门禁
|
||||
FileUploadUtils.assertAllowed(file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
|
||||
UploadResult upload = ossStorageService.upload(file, OssBizPath.UPLOAD.dir());
|
||||
AjaxResult ajax = AjaxResult.success();
|
||||
ajax.put("url", url);
|
||||
ajax.put("fileName", fileName);
|
||||
ajax.put("newFileName", FileUtils.getName(fileName));
|
||||
ajax.put("url", upload.getUrl());
|
||||
// fileName 语义为“落库并用于前端回显的可访问地址”,OSS 模式下即完整对象地址
|
||||
ajax.put("fileName", upload.getUrl());
|
||||
ajax.put("newFileName", StringUtils.substringAfterLast(upload.getFilename(), "/"));
|
||||
ajax.put("originalFilename", file.getOriginalFilename());
|
||||
return ajax;
|
||||
} catch (Exception e) {
|
||||
@ -101,20 +116,22 @@ public class CommonController {
|
||||
@PostMapping("/uploads")
|
||||
public AjaxResult uploadFiles(List<MultipartFile> files) throws Exception {
|
||||
try {
|
||||
// 上传文件路径
|
||||
String filePath = RuoYiConfig.getUploadPath();
|
||||
List<String> urls = new ArrayList<String>();
|
||||
List<String> fileNames = new ArrayList<String>();
|
||||
List<String> newFileNames = new ArrayList<String>();
|
||||
List<String> originalFilenames = new ArrayList<String>();
|
||||
// 先整体校验再上传,避免中途因某个文件不合法而留下已上传的孤儿对象
|
||||
for (MultipartFile file : files) {
|
||||
// 上传并返回新文件名称
|
||||
String fileName = FileUploadUtils.upload(filePath, file);
|
||||
String url = serverConfig.getUrl() + fileName;
|
||||
urls.add(url);
|
||||
fileNames.add(fileName);
|
||||
newFileNames.add(FileUtils.getName(fileName));
|
||||
originalFilenames.add(file.getOriginalFilename());
|
||||
FileUploadUtils.assertAllowed(file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
|
||||
}
|
||||
// 批量上传复用同一存储客户端,避免每个文件都读一次 OSS 配置
|
||||
List<UploadResult> uploads = ossStorageService.upload(files, OssBizPath.UPLOAD.dir());
|
||||
for (int i = 0; i < uploads.size(); i++) {
|
||||
UploadResult upload = uploads.get(i);
|
||||
urls.add(upload.getUrl());
|
||||
fileNames.add(upload.getUrl());
|
||||
newFileNames.add(StringUtils.substringAfterLast(upload.getFilename(), "/"));
|
||||
originalFilenames.add(files.get(i).getOriginalFilename());
|
||||
}
|
||||
AjaxResult ajax = AjaxResult.success();
|
||||
ajax.put("urls", StringUtils.join(urls, FILE_DELIMETER));
|
||||
@ -138,6 +155,15 @@ public class CommonController {
|
||||
if (!FileUtils.checkAllowDownload(resource)) {
|
||||
throw new Exception(StringUtils.format(MessageUtils.message("download.resource.not.valid"), resource));
|
||||
}
|
||||
if (ossStorageService.isOssUrl(resource)) {
|
||||
// 对象存储资源:直接从 OSS 拉取字节流写出
|
||||
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
|
||||
FileUtils.setAttachmentResponseHeader(response, StringUtils.substringAfterLast(resource, "/"));
|
||||
try (InputStream inputStream = ossStorageService.open(resource)) {
|
||||
IoUtil.copy(inputStream, response.getOutputStream());
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 本地资源路径
|
||||
String localPath = RuoYiConfig.getProfile();
|
||||
// 数据库资源地址
|
||||
@ -173,4 +199,16 @@ public class CommonController {
|
||||
//以流的形式输出到前端
|
||||
MatrixToImageWriter.writeToStream(bitMatrix, "jpg", stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析下载时展示给用户的文件名
|
||||
*
|
||||
* <p>对象存储地址取对象 KEY 末段;本地资源沿用原有的 "时间戳 + 下划线后段" 规则。
|
||||
*/
|
||||
private String resolveDownloadName(String fileName) {
|
||||
if (ossStorageService.isOssUrl(fileName)) {
|
||||
return StringUtils.substringAfterLast(fileName, "/");
|
||||
}
|
||||
return System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package com.fastbee.web.controller.system;
|
||||
|
||||
import com.fastbee.common.annotation.Log;
|
||||
import com.fastbee.common.config.RuoYiConfig;
|
||||
import com.fastbee.common.constant.UserConstants;
|
||||
import com.fastbee.common.core.controller.BaseController;
|
||||
import com.fastbee.common.core.domain.AjaxResult;
|
||||
@ -17,6 +16,9 @@ import com.fastbee.common.utils.file.MimeTypeUtils;
|
||||
import com.fastbee.framework.web.service.TokenService;
|
||||
import com.fastbee.iot.domain.UserSocialProfile;
|
||||
import com.fastbee.iot.service.IUserSocialProfileService;
|
||||
import com.fastbee.oss.entity.UploadResult;
|
||||
import com.fastbee.oss.enums.OssBizPath;
|
||||
import com.fastbee.oss.service.OssStorageService;
|
||||
import com.fastbee.system.service.ISysUserService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
@ -45,6 +47,9 @@ public class SysProfileController extends BaseController {
|
||||
@Autowired
|
||||
private IUserSocialProfileService iUserSocialProfileService;
|
||||
|
||||
@Autowired
|
||||
private OssStorageService ossStorageService;
|
||||
|
||||
/**
|
||||
* 个人信息
|
||||
*/
|
||||
@ -132,7 +137,10 @@ public class SysProfileController extends BaseController {
|
||||
public AjaxResult avatar(@RequestParam("avatarfile") MultipartFile file) throws Exception {
|
||||
if (!file.isEmpty()) {
|
||||
LoginUser loginUser = getLoginUser();
|
||||
String avatar = FileUploadUtils.upload(RuoYiConfig.getAvatarPath(), file, MimeTypeUtils.IMAGE_EXTENSION);
|
||||
// 沿用原有的图片类型与体积白名单校验,仅存储介质由本地磁盘切换为对象存储
|
||||
FileUploadUtils.assertAllowed(file, MimeTypeUtils.IMAGE_EXTENSION);
|
||||
UploadResult upload = ossStorageService.upload(file, OssBizPath.AVATAR.dir());
|
||||
String avatar = upload.getUrl();
|
||||
if (userService.updateUserAvatar(loginUser.getUsername(), avatar)) {
|
||||
AjaxResult ajax = AjaxResult.success();
|
||||
ajax.put("imgUrl", avatar);
|
||||
|
||||
@ -52,7 +52,7 @@ public class FileUploadUtils {
|
||||
* @return 文件名称
|
||||
* @throws Exception
|
||||
*/
|
||||
public static final String upload(MultipartFile file) throws IOException {
|
||||
public static String upload(MultipartFile file) throws IOException {
|
||||
try {
|
||||
return upload(getDefaultBaseDir(), file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
|
||||
} catch (Exception e) {
|
||||
@ -68,7 +68,7 @@ public class FileUploadUtils {
|
||||
* @return 文件名称
|
||||
* @throws IOException
|
||||
*/
|
||||
public static final String upload(String baseDir, MultipartFile file) throws IOException {
|
||||
public static String upload(String baseDir, MultipartFile file) throws IOException {
|
||||
try {
|
||||
return upload(baseDir, file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
|
||||
} catch (Exception e) {
|
||||
@ -88,7 +88,7 @@ public class FileUploadUtils {
|
||||
* @throws IOException 比如读写文件出错时
|
||||
* @throws InvalidExtensionException 文件校验异常
|
||||
*/
|
||||
public static final String upload(String baseDir, MultipartFile file, String[] allowedExtension)
|
||||
public static String upload(String baseDir, MultipartFile file, String[] allowedExtension)
|
||||
throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException,
|
||||
InvalidExtensionException {
|
||||
int fileNamelength = Objects.requireNonNull(file.getOriginalFilename()).length();
|
||||
@ -108,12 +108,12 @@ public class FileUploadUtils {
|
||||
/**
|
||||
* 编码文件名
|
||||
*/
|
||||
public static final String extractFilename(MultipartFile file) {
|
||||
public static String extractFilename(MultipartFile file) {
|
||||
return StringUtils.format("{}/{}_{}.{}", DateUtils.datePath(),
|
||||
FilenameUtils.getBaseName(file.getOriginalFilename()), Seq.getId(Seq.uploadSeqType), getExtension(file));
|
||||
}
|
||||
|
||||
public static final File getAbsoluteFile(String uploadDir, String fileName) throws IOException {
|
||||
public static File getAbsoluteFile(String uploadDir, String fileName) {
|
||||
File desc = new File(uploadDir + File.separator + fileName);
|
||||
|
||||
if (!desc.exists()) {
|
||||
@ -124,7 +124,7 @@ public class FileUploadUtils {
|
||||
return desc;
|
||||
}
|
||||
|
||||
public static final String getPathFileName(String uploadDir, String fileName) throws IOException {
|
||||
public static String getPathFileName(String uploadDir, String fileName) {
|
||||
int dirLastIndex = RuoYiConfig.getProfile().length() + 1;
|
||||
String currentDir = StringUtils.substring(uploadDir, dirLastIndex);
|
||||
return Constants.RESOURCE_PREFIX + "/" + currentDir + "/" + fileName;
|
||||
@ -138,7 +138,7 @@ public class FileUploadUtils {
|
||||
* @throws FileSizeLimitExceededException 如果超出最大大小
|
||||
* @throws InvalidExtensionException
|
||||
*/
|
||||
public static final void assertAllowed(MultipartFile file, String[] allowedExtension)
|
||||
public static void assertAllowed(MultipartFile file, String[] allowedExtension)
|
||||
throws FileSizeLimitExceededException, InvalidExtensionException {
|
||||
long size = file.getSize();
|
||||
if (size > DEFAULT_MAX_SIZE) {
|
||||
@ -173,7 +173,7 @@ public class FileUploadUtils {
|
||||
* @param allowedExtension
|
||||
* @return
|
||||
*/
|
||||
public static final boolean isAllowedExtension(String extension, String[] allowedExtension) {
|
||||
public static boolean isAllowedExtension(String extension, String[] allowedExtension) {
|
||||
for (String str : allowedExtension) {
|
||||
if (str.equalsIgnoreCase(extension)) {
|
||||
return true;
|
||||
@ -188,7 +188,7 @@ public class FileUploadUtils {
|
||||
* @param file 表单文件
|
||||
* @return 后缀名
|
||||
*/
|
||||
public static final String getExtension(MultipartFile file) {
|
||||
public static String getExtension(MultipartFile file) {
|
||||
String extension = FilenameUtils.getExtension(file.getOriginalFilename());
|
||||
if (StringUtils.isEmpty(extension)) {
|
||||
extension = MimeTypeUtils.getExtension(Objects.requireNonNull(file.getContentType()));
|
||||
|
||||
@ -13,6 +13,12 @@
|
||||
<description>controller层接口</description>
|
||||
<dependencies>
|
||||
|
||||
<!-- 对象存储:文件上传统一走 OSS,替换本地磁盘落盘 -->
|
||||
<dependency>
|
||||
<groupId>com.fastbee</groupId>
|
||||
<artifactId>fastbee-oss</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fastbee</groupId>
|
||||
<artifactId>mqtt-broker</artifactId>
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package com.fastbee.controller.goview;
|
||||
|
||||
import com.fastbee.common.annotation.Log;
|
||||
import com.fastbee.common.config.RuoYiConfig;
|
||||
import com.fastbee.common.core.controller.BaseController;
|
||||
import com.fastbee.common.core.domain.AjaxResult;
|
||||
import com.fastbee.common.core.domain.entity.SysUser;
|
||||
@ -15,13 +14,15 @@ import com.fastbee.iot.domain.GoviewProjectData;
|
||||
import com.fastbee.iot.model.goview.GoviewProjectVo;
|
||||
import com.fastbee.iot.service.IGoviewProjectDataService;
|
||||
import com.fastbee.iot.service.IGoviewProjectService;
|
||||
import com.fastbee.oss.entity.UploadResult;
|
||||
import com.fastbee.oss.enums.OssBizPath;
|
||||
import com.fastbee.oss.service.OssStorageService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -43,6 +44,9 @@ public class GoviewProjectController extends BaseController {
|
||||
@Autowired
|
||||
private IGoviewProjectDataService goviewProjectDataService;
|
||||
|
||||
@Autowired
|
||||
private OssStorageService ossStorageService;
|
||||
|
||||
/**
|
||||
* 查询项目列表
|
||||
*/
|
||||
@ -161,23 +165,14 @@ public class GoviewProjectController extends BaseController {
|
||||
@ApiOperation("文件上传")
|
||||
public AjaxResult uploadFile(@RequestBody MultipartFile object) throws Exception {
|
||||
try {
|
||||
String filePath = RuoYiConfig.getProfile();
|
||||
// 获取文件名和文件类型
|
||||
// 获取文件名
|
||||
String fileName = object.getOriginalFilename();
|
||||
fileName = "/goview/" + getLoginUser().getUserId().toString() + "/" + fileName;
|
||||
//创建目录
|
||||
File desc = new File(filePath + File.separator + fileName);
|
||||
if (!desc.exists()) {
|
||||
if (!desc.getParentFile().exists()) {
|
||||
desc.getParentFile().mkdirs();
|
||||
}
|
||||
}
|
||||
// 存储文件-覆盖存储(一个文件一个图,防止过多)
|
||||
object.transferTo(desc);
|
||||
String url = "/profile" + fileName;
|
||||
// 同一业务对象使用固定 KEY 覆盖存储,避免重复上传在对象存储里持续堆积
|
||||
String objectKey = OssBizPath.GOVIEW.of(getLoginUser().getUserId(), fileName);
|
||||
UploadResult upload = ossStorageService.uploadAs(object, objectKey);
|
||||
Map<String, Object> map = new HashMap<String, Object>(2);
|
||||
map.put("fileName", url);
|
||||
map.put("url", url);
|
||||
map.put("fileName", upload.getUrl());
|
||||
map.put("url", upload.getUrl());
|
||||
return AjaxResult.success("上传成功", map);
|
||||
} catch (Exception e) {
|
||||
return AjaxResult.error(e.getMessage());
|
||||
|
||||
@ -7,6 +7,7 @@ import com.fastbee.common.core.page.TableDataInfo;
|
||||
import com.fastbee.common.enums.BusinessType;
|
||||
import com.fastbee.common.utils.file.FileUploadUtils;
|
||||
import com.fastbee.common.utils.poi.ExcelUtil;
|
||||
import com.fastbee.oss.enums.OssBizPath;
|
||||
import com.fastbee.sip.domain.SipDevice;
|
||||
import com.fastbee.sip.service.ISipDeviceService;
|
||||
import io.swagger.annotations.Api;
|
||||
@ -125,7 +126,8 @@ public class SipDeviceController extends BaseController {
|
||||
@GetMapping("/snap/{deviceId}/{channelId}")
|
||||
public void getSnap(HttpServletResponse resp, @PathVariable String deviceId, @PathVariable String channelId) {
|
||||
try {
|
||||
final InputStream in = Files.newInputStream(new File(FileUploadUtils.getDefaultBaseDir() + File.separator + "snap" + File.separator + deviceId + "_" + channelId + ".jpg").toPath());
|
||||
final InputStream in = Files.newInputStream(new File(FileUploadUtils.getDefaultBaseDir() + File.separator
|
||||
+ OssBizPath.SNAP.dir() + File.separator + deviceId + "_" + channelId + ".jpg").toPath());
|
||||
resp.setContentType(MediaType.IMAGE_PNG_VALUE);
|
||||
IOUtils.copy(in, resp.getOutputStream());
|
||||
} catch (IOException e) {
|
||||
|
||||
@ -7,7 +7,6 @@ import com.dtflys.forest.http.ForestRequestType;
|
||||
import com.dtflys.forest.http.ForestResponse;
|
||||
import com.fastbee.common.ProtocolDeCodeService;
|
||||
import com.fastbee.common.annotation.Log;
|
||||
import com.fastbee.common.config.RuoYiConfig;
|
||||
import com.fastbee.common.constant.Constants;
|
||||
import com.fastbee.common.core.controller.BaseController;
|
||||
import com.fastbee.common.core.domain.AjaxResult;
|
||||
@ -36,6 +35,9 @@ import com.fastbee.iot.util.VelocityInitializer;
|
||||
import com.fastbee.iot.util.VelocityUtils;
|
||||
import com.fastbee.mqttclient.MqttClientConfig;
|
||||
import com.fastbee.mqttclient.PubMqttClient;
|
||||
import com.fastbee.oss.entity.UploadResult;
|
||||
import com.fastbee.oss.enums.OssBizPath;
|
||||
import com.fastbee.oss.service.OssStorageService;
|
||||
import com.fastbee.system.service.ISysConfigService;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
@ -57,17 +59,12 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import static com.fastbee.common.utils.file.FileUploadUtils.getExtension;
|
||||
|
||||
/**
|
||||
* 产品分类Controller
|
||||
*
|
||||
@ -106,6 +103,9 @@ public class ToolController extends BaseController {
|
||||
@Value("${server.broker.enabled}")
|
||||
private boolean brokerEnabled;
|
||||
|
||||
@Autowired
|
||||
private OssStorageService ossStorageService;
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*/
|
||||
@ -370,7 +370,6 @@ public class ToolController extends BaseController {
|
||||
@ApiOperation("文件上传")
|
||||
public AjaxResult uploadFile(MultipartFile file) throws Exception {
|
||||
try {
|
||||
String filePath = RuoYiConfig.getProfile();
|
||||
// 文件名长度限制
|
||||
int fileNamelength = file.getOriginalFilename().length();
|
||||
if (fileNamelength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH) {
|
||||
@ -379,26 +378,11 @@ public class ToolController extends BaseController {
|
||||
// 文件类型限制
|
||||
// assertAllowed(file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
|
||||
|
||||
// 获取文件名和文件类型
|
||||
String fileName = file.getOriginalFilename();
|
||||
String extension = getExtension(file);
|
||||
//设置日期格式
|
||||
SimpleDateFormat df = new SimpleDateFormat("yyyy-MMdd-HHmmss");
|
||||
fileName = "/iot/" + getLoginUser().getUserId().toString() + "/" + df.format(new Date()) + "." + extension;
|
||||
//创建目录
|
||||
File desc = new File(filePath + File.separator + fileName);
|
||||
if (!desc.exists()) {
|
||||
if (!desc.getParentFile().exists()) {
|
||||
desc.getParentFile().mkdirs();
|
||||
}
|
||||
}
|
||||
// 存储文件
|
||||
file.transferTo(desc);
|
||||
|
||||
String url = "/profile" + fileName;
|
||||
// 上传至对象存储,按用户隔离业务目录
|
||||
UploadResult upload = ossStorageService.upload(file, OssBizPath.IOT.of(getLoginUser().getUserId()));
|
||||
AjaxResult ajax = AjaxResult.success();
|
||||
ajax.put("fileName", url);
|
||||
ajax.put("url", url);
|
||||
ajax.put("fileName", upload.getUrl());
|
||||
ajax.put("url", upload.getUrl());
|
||||
return ajax;
|
||||
} catch (Exception e) {
|
||||
return AjaxResult.error(e.getMessage());
|
||||
|
||||
@ -1,266 +0,0 @@
|
||||
package com.fastbee.oss.core;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.amazonaws.ClientConfiguration;
|
||||
import com.amazonaws.HttpMethod;
|
||||
import com.amazonaws.Protocol;
|
||||
import com.amazonaws.auth.AWSCredentials;
|
||||
import com.amazonaws.auth.AWSCredentialsProvider;
|
||||
import com.amazonaws.auth.AWSStaticCredentialsProvider;
|
||||
import com.amazonaws.auth.BasicAWSCredentials;
|
||||
import com.amazonaws.client.builder.AwsClientBuilder;
|
||||
import com.amazonaws.services.s3.AmazonS3;
|
||||
import com.amazonaws.services.s3.AmazonS3Client;
|
||||
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
|
||||
import com.amazonaws.services.s3.model.*;
|
||||
import com.fastbee.common.exception.GlobalException;
|
||||
import com.fastbee.common.utils.DateUtils;
|
||||
import com.fastbee.common.utils.StringUtils;
|
||||
import com.fastbee.oss.entity.OssConstant;
|
||||
import com.fastbee.oss.entity.OssProperties;
|
||||
import com.fastbee.oss.entity.UploadResult;
|
||||
import com.fastbee.oss.enums.AccessPolicyType;
|
||||
import com.fastbee.oss.enums.PolicyType;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* S3 存储协议 所有兼容S3协议的云厂商均支持
|
||||
* 阿里云 腾讯云 七牛云 minio
|
||||
*/
|
||||
public class OssClient {
|
||||
|
||||
private final String configKey;
|
||||
|
||||
private final OssProperties properties;
|
||||
|
||||
private final AmazonS3 client;
|
||||
|
||||
public OssClient(String configKey, OssProperties ossProperties) {
|
||||
this.configKey = configKey;
|
||||
this.properties = ossProperties;
|
||||
try {
|
||||
AwsClientBuilder.EndpointConfiguration endpointConfig =
|
||||
new AwsClientBuilder.EndpointConfiguration(properties.getEndpoint(), properties.getRegion());
|
||||
|
||||
AWSCredentials credentials = new BasicAWSCredentials(properties.getAccessKey(), properties.getSecretKey());
|
||||
AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
|
||||
ClientConfiguration clientConfig = new ClientConfiguration();
|
||||
if (OssConstant.IS_HTTPS.equals(properties.getIsHttps())) {
|
||||
clientConfig.setProtocol(Protocol.HTTPS);
|
||||
} else {
|
||||
clientConfig.setProtocol(Protocol.HTTP);
|
||||
}
|
||||
AmazonS3ClientBuilder build = AmazonS3Client.builder()
|
||||
.withEndpointConfiguration(endpointConfig)
|
||||
.withClientConfiguration(clientConfig)
|
||||
.withCredentials(credentialsProvider)
|
||||
.disableChunkedEncoding();
|
||||
if (!StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE)) {
|
||||
// minio 使用https限制使用域名访问 需要此配置 站点填域名
|
||||
build.enablePathStyleAccess();
|
||||
}
|
||||
this.client = build.build();
|
||||
|
||||
createBucket();
|
||||
} catch (Exception e) {
|
||||
if (e instanceof GlobalException) {
|
||||
throw e;
|
||||
}
|
||||
throw new GlobalException("配置错误! 请检查系统配置:[" + e.getMessage() + "]");
|
||||
}
|
||||
}
|
||||
|
||||
private static String getPolicy(String bucketName, PolicyType policyType) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("{\n\"Statement\": [\n{\n\"Action\": [\n");
|
||||
switch (policyType) {
|
||||
case WRITE:
|
||||
builder.append("\"s3:GetBucketLocation\",\n\"s3:ListBucketMultipartUploads\"\n");
|
||||
case READ_WRITE:
|
||||
builder.append("\"s3:GetBucketLocation\",\n\"s3:ListBucket\",\n\"s3:ListBucketMultipartUploads\"\n");
|
||||
default:
|
||||
builder.append("\"s3:GetBucketLocation\"\n");
|
||||
}
|
||||
|
||||
builder.append("],\n\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
|
||||
builder.append(bucketName);
|
||||
builder.append("\"\n},\n");
|
||||
if (policyType == PolicyType.READ) {
|
||||
builder.append("{\n\"Action\": [\n\"s3:ListBucket\"\n],\n\"Effect\": \"Deny\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
|
||||
builder.append(bucketName);
|
||||
builder.append("\"\n},\n");
|
||||
}
|
||||
builder.append("{\n\"Action\": ");
|
||||
switch (policyType) {
|
||||
case WRITE:
|
||||
builder.append("[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n");
|
||||
case READ_WRITE:
|
||||
builder.append("[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:GetObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n");
|
||||
default:
|
||||
builder.append("\"s3:GetObject\",\n");
|
||||
}
|
||||
builder.append("\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
|
||||
builder.append(bucketName);
|
||||
builder.append("/*\"\n}\n],\n\"Version\": \"2012-10-17\"\n}\n");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public void createBucket() {
|
||||
try {
|
||||
String bucketName = properties.getBucketName();
|
||||
if (client.doesBucketExistV2(bucketName)) {
|
||||
return;
|
||||
}
|
||||
CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName);
|
||||
AccessPolicyType accessPolicy = getAccessPolicy();
|
||||
createBucketRequest.setCannedAcl(accessPolicy.getAcl());
|
||||
client.createBucket(createBucketRequest);
|
||||
client.setBucketPolicy(bucketName, getPolicy(bucketName, accessPolicy.getPolicyType()));
|
||||
} catch (Exception e) {
|
||||
throw new GlobalException("创建Bucket失败, 请核对配置信息:[" + e.getMessage() + "]");
|
||||
}
|
||||
}
|
||||
|
||||
public UploadResult upload(byte[] data, String path, String contentType) {
|
||||
return upload(new ByteArrayInputStream(data), path, contentType);
|
||||
}
|
||||
|
||||
public UploadResult upload(InputStream inputStream, String path, String contentType) {
|
||||
if (!(inputStream instanceof ByteArrayInputStream)) {
|
||||
inputStream = new ByteArrayInputStream(IoUtil.readBytes(inputStream));
|
||||
}
|
||||
try {
|
||||
ObjectMetadata metadata = new ObjectMetadata();
|
||||
metadata.setContentType(contentType);
|
||||
metadata.setContentLength(inputStream.available());
|
||||
PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, inputStream, metadata);
|
||||
// 设置上传对象的 Acl 为公共读
|
||||
putObjectRequest.setCannedAcl(getAccessPolicy().getAcl());
|
||||
client.putObject(putObjectRequest);
|
||||
} catch (Exception e) {
|
||||
throw new GlobalException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
|
||||
}
|
||||
return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
|
||||
}
|
||||
|
||||
public UploadResult upload(File file, String path) {
|
||||
try {
|
||||
PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, file);
|
||||
// 设置上传对象的 Acl 为公共读
|
||||
putObjectRequest.setCannedAcl(getAccessPolicy().getAcl());
|
||||
client.putObject(putObjectRequest);
|
||||
} catch (Exception e) {
|
||||
throw new GlobalException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
|
||||
}
|
||||
return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
|
||||
}
|
||||
|
||||
public void delete(String path) {
|
||||
path = path.replace(getUrl() + "/", "");
|
||||
try {
|
||||
client.deleteObject(properties.getBucketName(), path);
|
||||
} catch (Exception e) {
|
||||
throw new GlobalException("删除文件失败,请检查配置信息:[" + e.getMessage() + "]");
|
||||
}
|
||||
}
|
||||
|
||||
public UploadResult uploadSuffix(byte[] data, String suffix, String contentType) {
|
||||
return upload(data, getPath(properties.getPrefix(), suffix), contentType);
|
||||
}
|
||||
|
||||
public UploadResult uploadSuffix(InputStream inputStream, String suffix, String contentType) {
|
||||
return upload(inputStream, getPath(properties.getPrefix(), suffix), contentType);
|
||||
}
|
||||
|
||||
public UploadResult uploadSuffix(File file, String suffix) {
|
||||
return upload(file, getPath(properties.getPrefix(), suffix));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件元数据
|
||||
*
|
||||
* @param path 完整文件路径
|
||||
*/
|
||||
public ObjectMetadata getObjectMetadata(String path) {
|
||||
path = path.replace(getUrl() + "/", "");
|
||||
S3Object object = client.getObject(properties.getBucketName(), path);
|
||||
return object.getObjectMetadata();
|
||||
}
|
||||
|
||||
public InputStream getObjectContent(String path) {
|
||||
path = path.replace(getUrl() + "/", "");
|
||||
S3Object object = client.getObject(properties.getBucketName(), path);
|
||||
return object.getObjectContent();
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
String domain = properties.getDomain();
|
||||
String endpoint = properties.getEndpoint();
|
||||
String header = OssConstant.IS_HTTPS.equals(properties.getIsHttps()) ? "https://" : "http://";
|
||||
// 云服务商直接返回
|
||||
if (StringUtils.containsAny(endpoint, OssConstant.CLOUD_SERVICE)) {
|
||||
if (StringUtils.isNotBlank(domain)) {
|
||||
return header + domain;
|
||||
}
|
||||
return header + properties.getBucketName() + "." + endpoint;
|
||||
}
|
||||
// minio 单独处理
|
||||
if (StringUtils.isNotBlank(domain)) {
|
||||
return header + domain + "/" + properties.getBucketName();
|
||||
}
|
||||
return header + endpoint + "/" + properties.getBucketName();
|
||||
}
|
||||
|
||||
public String getPath(String prefix, String suffix) {
|
||||
// 生成uuid
|
||||
String uuid = IdUtil.fastSimpleUUID();
|
||||
// 文件路径
|
||||
String path = DateUtils.datePath() + "/" + uuid;
|
||||
if (StringUtils.isNotBlank(prefix)) {
|
||||
path = prefix + "/" + path;
|
||||
}
|
||||
return path + suffix;
|
||||
}
|
||||
|
||||
public String getConfigKey() {
|
||||
return configKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取私有URL链接
|
||||
*
|
||||
* @param objectKey 对象KEY
|
||||
* @param second 授权时间
|
||||
*/
|
||||
public String getPrivateUrl(String objectKey, Integer second) {
|
||||
GeneratePresignedUrlRequest generatePresignedUrlRequest =
|
||||
new GeneratePresignedUrlRequest(properties.getBucketName(), objectKey)
|
||||
.withMethod(HttpMethod.GET)
|
||||
.withExpiration(new Date(System.currentTimeMillis() + 1000L * second));
|
||||
URL url = client.generatePresignedUrl(generatePresignedUrlRequest);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查配置是否相同
|
||||
*/
|
||||
public boolean checkPropertiesSame(OssProperties properties) {
|
||||
return this.properties.equals(properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前桶权限类型
|
||||
*
|
||||
* @return 当前桶权限类型code
|
||||
*/
|
||||
public AccessPolicyType getAccessPolicy() {
|
||||
return AccessPolicyType.getByType(properties.getAccessPolicy());
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,58 +0,0 @@
|
||||
package com.fastbee.oss.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class OssProperties {
|
||||
|
||||
/**
|
||||
* 租户id
|
||||
*/
|
||||
private String tenantId;
|
||||
|
||||
/**
|
||||
* 访问站点
|
||||
*/
|
||||
private String endpoint;
|
||||
|
||||
/**
|
||||
* 自定义域名
|
||||
*/
|
||||
private String domain;
|
||||
|
||||
/**
|
||||
* 前缀
|
||||
*/
|
||||
private String prefix;
|
||||
|
||||
/**
|
||||
* ACCESS_KEY
|
||||
*/
|
||||
private String accessKey;
|
||||
|
||||
/**
|
||||
* SECRET_KEY
|
||||
*/
|
||||
private String secretKey;
|
||||
|
||||
/**
|
||||
* 存储空间名
|
||||
*/
|
||||
private String bucketName;
|
||||
|
||||
/**
|
||||
* 存储区域
|
||||
*/
|
||||
private String region;
|
||||
|
||||
/**
|
||||
* 是否https(Y=是,N=否)
|
||||
*/
|
||||
private String isHttps;
|
||||
|
||||
/**
|
||||
* 桶权限类型(0private 1public 2custom)
|
||||
*/
|
||||
private String accessPolicy;
|
||||
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
package com.fastbee.oss.enums;
|
||||
|
||||
import com.fastbee.common.utils.StringUtils;
|
||||
|
||||
/**
|
||||
* 对象存储业务目录(对象 KEY 的一级前缀)
|
||||
*
|
||||
* <p>所有上传动作都必须从这里取目录,业务代码禁止再硬编码 upload/avatar/iot 等字面量:
|
||||
* 前缀一旦散落各处,同桶对象的归属就无法统一盘点,迁移、清理、容量统计都会失控。
|
||||
*
|
||||
* <p>用法:
|
||||
* <ul>
|
||||
* <li>由 {@code OssClient} 生成随机对象名时传目录 —— {@code OssBizPath.AVATAR.dir()}</li>
|
||||
* <li>需要业务维度隔离时追加片段 —— {@code OssBizPath.IOT.of(userId)}</li>
|
||||
* <li>需要固定 KEY(覆盖写)时末位带文件名 —— {@code OssBizPath.GOVIEW.of(userId, fileName)}</li>
|
||||
* </ul>
|
||||
*/
|
||||
public enum OssBizPath {
|
||||
|
||||
/** 通用文件上传 */
|
||||
UPLOAD("upload"),
|
||||
|
||||
/** 用户头像 */
|
||||
AVATAR("avatar"),
|
||||
|
||||
/** IoT 工具箱上传 */
|
||||
IOT("iot"),
|
||||
|
||||
/** 数据大屏资源 */
|
||||
GOVIEW("goview"),
|
||||
|
||||
/** 设备监测报告 */
|
||||
REPORT("report"),
|
||||
|
||||
/** 视频设备截图(ZLM 抓图,命名 {deviceId}_{channelId}.jpg) */
|
||||
SNAP("snap"),
|
||||
|
||||
;
|
||||
|
||||
private final String dir;
|
||||
|
||||
OssBizPath(String dir) {
|
||||
this.dir = dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取纯目录名,用于由 {@code OssClient} 自动生成 uuid 对象名的场景
|
||||
*/
|
||||
public String dir() {
|
||||
return dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在业务目录下追加子路径片段
|
||||
*
|
||||
* @param segments 依次拼接的片段(可为 userId 等业务标识;末位也可为带后缀的文件名,此时结果即完整对象 KEY)
|
||||
* @return 形如 {@code avatar/1001} 的路径
|
||||
*/
|
||||
public String of(Object... segments) {
|
||||
StringBuilder key = new StringBuilder(dir);
|
||||
if (segments == null) {
|
||||
return key.toString();
|
||||
}
|
||||
for (Object segment : segments) {
|
||||
if (segment == null) {
|
||||
continue;
|
||||
}
|
||||
String part = String.valueOf(segment).trim();
|
||||
if (StringUtils.isEmpty(part) || "null".equalsIgnoreCase(part)) {
|
||||
continue;
|
||||
}
|
||||
key.append('/').append(part);
|
||||
}
|
||||
return key.toString();
|
||||
}
|
||||
}
|
||||
@ -208,15 +208,31 @@ public class OssClient {
|
||||
// 云服务商直接返回
|
||||
if (StringUtils.containsAny(endpoint, OssConstant.CLOUD_SERVICE)) {
|
||||
if (StringUtils.isNotBlank(domain)) {
|
||||
return header + domain;
|
||||
return appendScheme(domain, header);
|
||||
}
|
||||
return header + properties.getBucketName() + "." + endpoint;
|
||||
}
|
||||
// minio 单独处理
|
||||
if (StringUtils.isNotBlank(domain)) {
|
||||
return header + domain + "/" + properties.getBucketName();
|
||||
String host = StringUtils.isNotBlank(domain) ? domain : endpoint;
|
||||
return appendScheme(host, header) + "/" + properties.getBucketName();
|
||||
}
|
||||
|
||||
/**
|
||||
* 补全省略的协议头
|
||||
*
|
||||
* <p>oss_config 的 endpoint/domain 常被填成带协议的完整地址(如 {@code http://host:9000}),
|
||||
* 此时若再拼一次 header 会得到 {@code http://http://host:9000} 这类畸形地址,
|
||||
* 前端拿到后必然裂图。故 host 已自带协议时原样返回。
|
||||
*/
|
||||
private String appendScheme(String host, String header) {
|
||||
if (StringUtils.isEmpty(host)) {
|
||||
return "";
|
||||
}
|
||||
return header + endpoint + "/" + properties.getBucketName();
|
||||
String trimmed = host.trim();
|
||||
if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
|
||||
return trimmed;
|
||||
}
|
||||
return header + trimmed;
|
||||
}
|
||||
|
||||
public String getPath(String prefix, String suffix) {
|
||||
|
||||
@ -0,0 +1,203 @@
|
||||
package com.fastbee.oss.service;
|
||||
|
||||
import com.fastbee.common.core.redis.RedisCache;
|
||||
import com.fastbee.common.exception.GlobalException;
|
||||
import com.fastbee.common.utils.StringUtils;
|
||||
import com.fastbee.common.utils.file.MimeTypeUtils;
|
||||
import com.fastbee.oss.entity.UploadResult;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 对象存储统一入口
|
||||
*
|
||||
* <p>业务层通过本服务完成 OSS 读写,屏蔽 {@link OssFactory} / {@link OssClient} 的构造细节与
|
||||
* 配置来源差异。所有原先落本地磁盘的上传链路(通用上传、头像、大屏资源等)应统一改走本服务。
|
||||
*
|
||||
* <p>对象命名规则由 {@link OssClient#getPath(String, String)} 生成:{@code [bizPrefix]/yyyy/MM/dd/uuid.suffix},
|
||||
* 其中 bizPrefix 由调用方按业务域指定(如 avatar、goview/12),避免所有对象平铺在日期目录下。
|
||||
*/
|
||||
@Service
|
||||
public class OssStorageService {
|
||||
|
||||
@Autowired
|
||||
private RedisCache redisCache;
|
||||
|
||||
/**
|
||||
* 获取默认存储客户端
|
||||
*/
|
||||
public OssClient client() {
|
||||
return OssFactory.instance(redisCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传 MultipartFile 到指定业务目录
|
||||
*
|
||||
* @param file 上传文件
|
||||
* @param bizPrefix 业务目录,为空则直接归档到日期目录
|
||||
* @return 可直接访问的上传结果(私有桶由调用方另行生成签名地址)
|
||||
*/
|
||||
public UploadResult upload(MultipartFile file, String bizPrefix) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new GlobalException("上传文件不能为空");
|
||||
}
|
||||
return uploadWith(client(), file, bizPrefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量上传到指定业务目录
|
||||
*
|
||||
* <p>与循环调用 {@link #upload(MultipartFile, String)} 的区别:只取一次存储客户端。
|
||||
* {@code OssFactory#instance} 每次会读两次 Redis(默认配置 key + 配置详情),
|
||||
* 逐个调用会产生 2N 次读;批量场景统一复用同一实例。
|
||||
*
|
||||
* @param files 上传文件列表
|
||||
* @param bizPrefix 业务目录
|
||||
* @return 与入参顺序一致的上传结果
|
||||
*/
|
||||
public List<UploadResult> upload(List<MultipartFile> files, String bizPrefix) {
|
||||
if (files == null || files.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
OssClient storage = client();
|
||||
List<UploadResult> results = new ArrayList<>(files.size());
|
||||
for (MultipartFile file : files) {
|
||||
results.add(uploadWith(storage, file, bizPrefix));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 推断对象后缀:优先取文件名后缀,退化按 Content-Type
|
||||
*
|
||||
* <p>不直接复用 {@code FileUploadUtils#getExtension},因其内部对 contentType 做了
|
||||
* {@code requireNonNull},contentType 为空时会 NPE。
|
||||
*/
|
||||
private String resolveSuffix(MultipartFile file) {
|
||||
String original = file.getOriginalFilename();
|
||||
if (StringUtils.isNotEmpty(original)) {
|
||||
String suffix = FilenameUtils.getExtension(original);
|
||||
if (StringUtils.isNotEmpty(suffix)) {
|
||||
return suffix;
|
||||
}
|
||||
}
|
||||
String contentType = file.getContentType();
|
||||
return StringUtils.isEmpty(contentType) ? null : MimeTypeUtils.getExtension(contentType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传字节数据到指定业务目录
|
||||
*
|
||||
* @param data 数据
|
||||
* @param bizPrefix 业务目录
|
||||
* @param suffix 文件后缀,不含点号;为空时对象不带后缀
|
||||
* @param contentType 内容类型
|
||||
* @return 上传结果
|
||||
*/
|
||||
public UploadResult upload(byte[] data, String bizPrefix, String suffix, String contentType) {
|
||||
if (data == null || data.length == 0) {
|
||||
throw new GlobalException("上传数据不能为空");
|
||||
}
|
||||
OssClient storage = client();
|
||||
String path = storage.getPath(bizPrefix, normalizeSuffix(suffix));
|
||||
return storage.upload(data, path, contentType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 以指定对象 KEY 上传(同 KEY 覆盖)
|
||||
*
|
||||
* <p>适用于“一个业务实体固定对应一个资源”的场景,例如大屏封面:本地时代靠同名文件覆盖来避免堆积,
|
||||
* 对象存储若仍用随机对象名会持续沉淀无用对象,故此处保留按 KEY 覆盖的能力。
|
||||
*
|
||||
* @param file 上传文件
|
||||
* @param objectKey 完整对象 KEY(含业务目录与后缀)
|
||||
* @return 上传结果
|
||||
*/
|
||||
public UploadResult uploadAs(MultipartFile file, String objectKey) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new GlobalException("上传文件不能为空");
|
||||
}
|
||||
if (StringUtils.isEmpty(objectKey)) {
|
||||
throw new GlobalException("对象KEY不能为空");
|
||||
}
|
||||
try {
|
||||
return client().upload(file.getBytes(), objectKey, file.getContentType());
|
||||
} catch (IOException e) {
|
||||
throw new GlobalException("读取上传文件失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除对象,入参可为完整 URL 或对象 KEY
|
||||
*/
|
||||
public void delete(String urlOrKey) {
|
||||
if (StringUtils.isEmpty(urlOrKey)) {
|
||||
return;
|
||||
}
|
||||
client().delete(urlOrKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取对象内容流,入参可为完整 URL 或对象 KEY
|
||||
*
|
||||
* @return 对象内容流,调用方负责关闭
|
||||
*/
|
||||
public InputStream open(String urlOrKey) {
|
||||
if (StringUtils.isEmpty(urlOrKey)) {
|
||||
throw new GlobalException("文件路径不能为空");
|
||||
}
|
||||
return client().getObjectContent(urlOrKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前存储的对外访问根地址(含 bucket)
|
||||
*/
|
||||
public String getBaseUrl() {
|
||||
return client().getUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断路径是否为 OSS 地址,用于区分历史本地资源与新对象存储资源
|
||||
*/
|
||||
public boolean isOssUrl(String path) {
|
||||
if (StringUtils.isEmpty(path)) {
|
||||
return false;
|
||||
}
|
||||
return path.startsWith("http://") || path.startsWith("https://");
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用给定客户端上传,供单文件与批量两条路径复用
|
||||
*/
|
||||
private UploadResult uploadWith(OssClient storage, MultipartFile file, String bizPrefix) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new GlobalException("上传文件不能为空");
|
||||
}
|
||||
// 文件名无后缀时按 Content-Type 推断(前端裁剪上传的文件名常是 blob),
|
||||
// 否则落库对象没有后缀,会被 FileUtils#checkAllowDownload 的后缀白名单拦下
|
||||
String path = storage.getPath(bizPrefix, normalizeSuffix(resolveSuffix(file)));
|
||||
try {
|
||||
return storage.upload(file.getBytes(), path, file.getContentType());
|
||||
} catch (IOException e) {
|
||||
throw new GlobalException("读取上传文件失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一后缀格式为空或以点号开头,保持与 {@link OssClient#getPath} 的拼接语义一致
|
||||
*/
|
||||
private String normalizeSuffix(String suffix) {
|
||||
if (StringUtils.isEmpty(suffix)) {
|
||||
return "";
|
||||
}
|
||||
return suffix.startsWith(".") ? suffix : "." + suffix;
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@ import com.fastbee.common.core.redis.RedisCache;
|
||||
import com.fastbee.common.enums.MonitorCategory;
|
||||
import com.fastbee.common.utils.StringUtils;
|
||||
import com.fastbee.oss.entity.UploadResult;
|
||||
import com.fastbee.oss.enums.OssBizPath;
|
||||
import com.fastbee.oss.service.OssFactory;
|
||||
import com.fastbee.report.analyzer.ReportAnalyzer;
|
||||
import com.fastbee.report.analyzer.ReportAnalyzerFactory;
|
||||
@ -316,6 +317,6 @@ public class ReportAsyncExecutor {
|
||||
|
||||
private String buildObjectPath(Long deviceId, String reportNo) {
|
||||
LocalDate today = LocalDate.now(ZoneOffset.ofHours(8));
|
||||
return "report/" + today.format(PATH_DATE) + "/device/" + deviceId + "/" + reportNo + ".docx";
|
||||
return OssBizPath.REPORT.of(today.format(PATH_DATE), "device", deviceId, reportNo + ".docx");
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,7 @@ import com.alibaba.fastjson2.JSONObject;
|
||||
import com.dtflys.forest.http.ForestRequest;
|
||||
import com.dtflys.forest.http.ForestResponse;
|
||||
import com.fastbee.common.utils.file.FileUploadUtils;
|
||||
import com.fastbee.oss.enums.OssBizPath;
|
||||
import com.fastbee.sip.client.ZlmForestClient;
|
||||
import com.fastbee.sip.domain.MediaServer;
|
||||
import com.fastbee.sip.model.ZlmMediaServer;
|
||||
@ -251,7 +252,7 @@ public class ZlmApiUtils {
|
||||
ForestResponse res = request.execute(ForestResponse.class);
|
||||
if (res.isSuccess()) {
|
||||
try {
|
||||
File snapFolder = new File(FileUploadUtils.getDefaultBaseDir() + File.separator + "snap");
|
||||
File snapFolder = new File(FileUploadUtils.getDefaultBaseDir() + File.separator + OssBizPath.SNAP.dir());
|
||||
if (!snapFolder.exists()) {
|
||||
if (!snapFolder.mkdirs()) {
|
||||
log.warn("{}路径创建失败", snapFolder.getAbsolutePath());
|
||||
|
||||
@ -181,7 +181,12 @@ INSERT INTO `oss_config`
|
||||
VALUES
|
||||
(1, 'admin', 'minio', 'supermeminio', 'minio20260909', 'fastbee', 'report',
|
||||
'http://47.121.180.229:9000', '', 'N', '', '0', '0', '0', 'admin', NOW(), 'admin', NOW(), 'MinIO默认存储');
|
||||
|
||||
INSERT INTO fastbee.oss_config
|
||||
(tenant_id, tenant_name, config_key, access_key, secret_key, bucket_name, prefix,
|
||||
endpoint, `domain`, is_https, region, access_policy, status, ext1, del_flag, create_by, create_time, update_by, update_time, remark)
|
||||
VALUES
|
||||
(1, 'admin', 'minio', 'IQJELEHGNX45V1LS7TZY', 'VnnQhFXwUIQ19qT4PKvlIAQxSH9Rru0IzTiyZHg3', 'fastbee', '',
|
||||
'http://47.121.180.229:9000', '', 'N', '', '1', '0', '', '0', 'admin', NOW(), 'admin', NOW(), 'MinIO默认存储');
|
||||
-- ------------------------------------------------------------
|
||||
-- 4. 历史报表菜单与权限(数据中心 3343 下新增)
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
<!-- 文件列表 -->
|
||||
<transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
|
||||
<li :key="file.url" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
|
||||
<el-link :href="`${baseUrl}${file.url}`" :underline="false" target="_blank">
|
||||
<el-link :href="resolveResourceUrl(file.url, baseUrl)" :underline="false" target="_blank">
|
||||
<span class="el-icon-document"> {{ getFileName(file.name) }} </span>
|
||||
</el-link>
|
||||
<div class="ele-upload-list__item-content-action">
|
||||
@ -35,6 +35,7 @@
|
||||
<script>
|
||||
import { getToken } from "@/utils/auth";
|
||||
import request from "@/utils/request";
|
||||
import { resolveResourceUrl } from "@/utils/resource-url";
|
||||
|
||||
function getBaseUrl() {
|
||||
try {
|
||||
@ -116,6 +117,8 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// 资源地址:对象存储直链直接用,历史本地资源补后端前缀
|
||||
resolveResourceUrl,
|
||||
// 上传前校检格式和大小
|
||||
handleBeforeUpload(file) {
|
||||
// 校检文件类型
|
||||
|
||||
@ -31,6 +31,7 @@
|
||||
|
||||
<script>
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { isAbsoluteUrl } from '@/utils/resource-url';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@ -85,10 +86,12 @@ export default {
|
||||
// 然后将数组转为对象数组
|
||||
this.fileList = list.map((item) => {
|
||||
if (typeof item === 'string') {
|
||||
if (item.indexOf(this.baseUrl) === -1) {
|
||||
item = { name: this.baseUrl + item, url: this.baseUrl + item };
|
||||
} else {
|
||||
// 对象存储返回的是绝对地址,直接用;历史本地资源才需要补前缀
|
||||
if (isAbsoluteUrl(item)) {
|
||||
item = { name: item, url: item };
|
||||
} else {
|
||||
const fullUrl = this.baseUrl + item;
|
||||
item = { name: fullUrl, url: fullUrl };
|
||||
}
|
||||
}
|
||||
return item;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { login, logout, getInfo } from '@/api/login';
|
||||
import { getToken, setToken, removeToken, setUserId, removeUserId } from '@/utils/auth';
|
||||
import { resolveResourceUrl } from '@/utils/resource-url';
|
||||
|
||||
const user = {
|
||||
state: {
|
||||
@ -59,7 +60,10 @@ const user = {
|
||||
getInfo()
|
||||
.then((res) => {
|
||||
const user = res.user;
|
||||
const avatar = user.avatar === '' || user.avatar == null ? require('@/assets/images/profile.jpg') : process.env.VUE_APP_BASE_API + user.avatar;
|
||||
// 头像为空时回落到默认图;对象存储地址为绝对URL直接使用,历史本地路径才补后端前缀
|
||||
const avatar = user.avatar === '' || user.avatar == null
|
||||
? require('@/assets/images/profile.jpg')
|
||||
: resolveResourceUrl(user.avatar, process.env.VUE_APP_BASE_API);
|
||||
if (res.roles && res.roles.length > 0) {
|
||||
// 验证返回的roles是否是一个非空数组
|
||||
commit('SET_ROLES', res.roles);
|
||||
|
||||
36
yack-web/src/utils/resource-url.js
Normal file
36
yack-web/src/utils/resource-url.js
Normal file
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 资源地址解析
|
||||
*
|
||||
* 文件/图片在切换到对象存储前后存在两种形态:
|
||||
* - 相对路径(历史本地磁盘资源,如 /profile/upload/xxx.png),必须拼接后端 baseURL 才能访问
|
||||
* - 绝对地址(OSS/MinIO 直链,如 http://host:9000/bucket/xxx.png),可直接访问
|
||||
*
|
||||
* 统一在此判别,避免各组件各自拼串产生 /prod-apihttp://... 这类错误地址。
|
||||
*/
|
||||
|
||||
// 绝对 http(s) 地址,或浏览器本地 blob/data 预览地址
|
||||
const ABSOLUTE_URL_REGEX = /^(?:https?:)?\/\//i;
|
||||
|
||||
/**
|
||||
* 是否已经是可以直接访问的地址(无需再拼 baseURL)
|
||||
*
|
||||
* @param {string} url 地址
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isAbsoluteUrl(url) {
|
||||
if (!url) return false;
|
||||
return ABSOLUTE_URL_REGEX.test(url) || url.startsWith('blob:') || url.startsWith('data:');
|
||||
}
|
||||
|
||||
/**
|
||||
* 将业务存储的地址转换为浏览器可直接访问的地址
|
||||
*
|
||||
* @param {string} url 业务地址,相对路径或绝对地址
|
||||
* @param {string} baseUrl 后端接口前缀,仅在相对路径时参与拼接
|
||||
* @returns {string} 可直接用于 src / href 的地址
|
||||
*/
|
||||
export function resolveResourceUrl(url, baseUrl) {
|
||||
if (!url) return '';
|
||||
if (isAbsoluteUrl(url)) return url;
|
||||
return (baseUrl || '') + url;
|
||||
}
|
||||
@ -58,6 +58,7 @@ import store from '@/store';
|
||||
import { VueCropper } from 'vue-cropper';
|
||||
import { uploadAvatar } from '@/api/system/user';
|
||||
import { debounce } from '@/utils';
|
||||
import { resolveResourceUrl } from '@/utils/resource-url';
|
||||
|
||||
export default {
|
||||
components: { VueCropper },
|
||||
@ -139,7 +140,8 @@ export default {
|
||||
formData.append('avatarfile', data);
|
||||
uploadAvatar(formData).then((response) => {
|
||||
this.open = false;
|
||||
this.options.img = process.env.VUE_APP_BASE_API + response.imgUrl;
|
||||
// 头像已在切换为对象存储后返回绝对地址,历史本地路径才需要补后端前缀
|
||||
this.options.img = resolveResourceUrl(response.imgUrl, process.env.VUE_APP_BASE_API);
|
||||
store.commit('SET_AVATAR', this.options.img);
|
||||
this.$modal.msgSuccess(this.$t('updateSuccess'));
|
||||
this.visible = false;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user