OSS:图片、文件上传接入对象存储minio

This commit is contained in:
zhugongping 2026-09-29 09:55:45 +08:00
parent 6a803952a8
commit f8a8474f0f
20 changed files with 475 additions and 416 deletions

View File

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

View File

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

View File

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

View File

@ -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>

View File

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

View File

@ -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) {

View File

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

View File

@ -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());
}
}

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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();
}
return header + endpoint + "/" + 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 "";
}
String trimmed = host.trim();
if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
return trimmed;
}
return header + trimmed;
}
public String getPath(String prefix, String suffix) {

View File

@ -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;
}
}

View File

@ -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");
}
}

View File

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

View File

@ -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 下新增)
-- ------------------------------------------------------------

View File

@ -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) {
// 校检文件类型

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

View File

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

View 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;
}

View File

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