多服务器文件管理系统的实现方案
在没有对象存储服务(OSS)的情况下,本文实现了一个基于多台服务器的文件管理系统。系统通过数据库表维护文件存储位置信息,主要功能包括:
系统使用Spring Boot框架实现,数据库采用MySQL,表结构包含文件URL、MD5、创建时间、服务器IP等关键字段。通过本地存储+数据库索引的方式,解决了多服务器环境下文件定位问题。
以下为代码:
package com.hwxc.wms.server.service.impl;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.net.NetUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hwxc.wms.common.enums.ResultCode;
import com.hwxc.wms.common.exception.BusinessException;
import com.hwxc.wms.common.result.CommonResult;
import com.hwxc.wms.common.utils.FileUtil;
import com.hwxc.wms.common.vo.FileVo;
import com.hwxc.wms.server.entity.FileEntity;
import com.hwxc.wms.server.mapper.FileMapper;
import com.hwxc.wms.server.service.FileService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
/**
* @description 针对表【file(文件表)】的数据库操作Service实现
* @createDate 2025-05-26 16:50:45
*/
@Service
public class FileServiceImpl extends ServiceImpl
implements FileService {
@Value("${file.ip:10.1.12.10}")
private String ip;
@Value("${file.maxSize:10}")
private Integer maxSize;
@Value("${zt.imagesTmpDir:/images}")
private String imagesTmpDir;
/**
* 文件上传,及信息返回
* @param uploadFile
* @return
*/
@Override
public CommonResult upload(MultipartFile uploadFile, String type) throws IOException {
if (uploadFile.isEmpty()) {
throw new BusinessException(ResultCode.FAILED.getCode(), "上传失败,请选择文件");
}
if (uploadFile.getSize() > this.maxSize * 1024 * 1024) {
throw new BusinessException(ResultCode.FAILED.getCode(), "上传文件大小不能超过"+this.maxSize+"MB");
}
String fileDir = this.imagesTmpDir + "/" + DateUtil.format(DateUtil.date(), "yyyyMMdd");
File uploadDir = new File(fileDir);
if(!uploadDir.exists())
{
uploadDir.mkdirs();
}
String md5 = FileUtil.getMd5(uploadFile);
//查看数据库是否已经存在,存在直接返回
QueryWrapper query = Wrappers.query();
query.eq("file_md5", md5);
query.last("limit 1");
FileEntity one = this.getOne(query);
FileVo fileVo = new FileVo();
String url = "";
HashMap dateKv = null;
if( null == one ){
//上传文件到本地
String originalFilename = uploadFile.getOriginalFilename();
String suffixName = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);
String newFile = fileDir + "/" + md5 + "." +suffixName;
File newFileObj = new File(newFile);
uploadFile.transferTo(newFileObj);
if( !newFileObj.exists() ){
throw new BusinessException(ResultCode.FAILED.getCode(), "保存文件失败");
}
FileEntity fileEntity = new FileEntity();
fileEntity.setFileUrl(newFile);
fileEntity.setFileMd5(md5);
fileEntity.setCreateTime(DateUtil.date());
fileEntity.setUpdateTime(DateUtil.date());
fileEntity.setIp(this.getLocalIp());
fileEntity.setExt(suffixName);
this.save(fileEntity);
BeanUtils.copyProperties(fileEntity, fileVo);
url = "http://" + this.ip + ":9015/file/url/" + fileEntity.getId() + "." + suffixName;
}else{
BeanUtils.copyProperties(one, fileVo);
url = "http://" + this.ip + ":9015/file/url/" + one.getId() + "." + one.getExt();
}
//得到文件地址
fileVo.setUrl(url);
return CommonResult.success(fileVo);
}
@Override
public ResponseEntity url(String id) {
//截取id字符串.之前的内容
id = id.substring(0, id.lastIndexOf("."));
FileEntity fileEntity = this.getById(id);
if (fileEntity == null) {
throw new BusinessException(ResultCode.FAILED.getCode(), "id对应的资源不存在");
}
String fileUrl = fileEntity.getFileUrl();
String imageType = FileUtil.getImageType(fileUrl);
MediaType mediaType = FileUtil.getMediaType(fileUrl);
try {
File file = new File(fileEntity.getFileUrl());
Path path = file.toPath();
Resource resource = new InputStreamResource(Files.newInputStream(path));
if (!resource.exists() || !resource.isReadable()) {
throw new RuntimeException("找不到文件");
}
ResponseEntity.BodyBuilder builder = ResponseEntity.ok();
builder.contentType(mediaType);
//文件格式
if (imageType.equals(MediaType.APPLICATION_OCTET_STREAM_VALUE)) {
builder.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + file.getName() + "\"");
}
return builder.body(resource);
} catch (Exception e) {
throw new BusinessException(ResultCode.FAILED.getCode(), e.getMessage());
}
}
public String getLocalIp() {
LinkedHashSet localIpv4s = NetUtil.localIpv4s();
if( null == localIpv4s ){
return "";
}
//结果集遍历
String ip = this.ip;
if( StrUtil.isBlank(ip) ){
return "";
}
List ips = Arrays.asList(ip.split(","));
for (int i = 0; i < ips.size(); i++) {
String cip = ips.get(i);
for (String localIp : localIpv4s) {
if( cip.equals(localIp) ){
return localIp;
}
}
}
return "";
}
}
FileUtil
package com.hwxc.wms.common.utils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.MediaType;
import org.springframework.web.multipart.MultipartFile;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;
/**
* @Description
*/
@Slf4j
public class FileUtil {
public static String getFileExtension(String fileName) {
if (StringUtils.isBlank(fileName)) {
return "";
}
int index = fileName.lastIndexOf(".");
return fileName.substring(index + 1);
}
public static String getMd5(MultipartFile file) {
try {
byte[] uploadBytes = file.getBytes();
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] digest = md5.digest(uploadBytes);
String hashString = new BigInteger(1, digest).toString(16);
return hashString;
} catch (Exception e) {
log.info(e.toString(), e);
}
return null;
}
/**
* 获取文件类型
* @param filename
* @return
*/
public static String getImageType(String filename) {
HashMap map = new HashMap<>();
map.put("jpg", MediaType.IMAGE_JPEG_VALUE);
map.put("png", MediaType.IMAGE_PNG_VALUE);
map.put("gif", MediaType.IMAGE_GIF_VALUE);
int dotIndex = filename.lastIndexOf('.');
if (dotIndex == -1) return MediaType.APPLICATION_OCTET_STREAM_VALUE;
String ext = filename.substring(dotIndex + 1).toLowerCase();
return map.getOrDefault(ext, MediaType.APPLICATION_OCTET_STREAM_VALUE);
}
public static MediaType getMediaType(String filename) {
int dotIndex = filename.lastIndexOf('.');
if (dotIndex == -1) return MediaType.APPLICATION_OCTET_STREAM;
String ext = filename.substring(dotIndex + 1).toLowerCase();
switch (ext){
case "jpg":
return MediaType.IMAGE_JPEG;
case "png":
return MediaType.IMAGE_PNG;
case "gif":
return MediaType.IMAGE_GIF;
default:
return MediaType.APPLICATION_OCTET_STREAM;
}
}
/**
* 获取文件目录
* @param filename
* @return
*/
public static String getDir(String filename) {
return filename.substring(0, filename.lastIndexOf('/'));
}
/**
* 获取文件名称
* @param filename
* @return
*/
public static String getName(String filename) {
return filename.substring(filename.lastIndexOf('/') + 1);
}
}
对应的数据库表:
create table file
(
id bigint auto_increment comment '主键'
primary key,
file_url varchar(300) default '' not null comment '文件url',
file_md5 varchar(32) default '' not null comment '文件md5',
create_time datetime default CURRENT_TIMESTAMP null,
update_time datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP,
ip varchar(20) default '' not null,
ext varchar(10) default '' not null comment '图片类型'
)
comment '文件表';