package com.mokee.admin.service.impl; import cn.hutool.crypto.digest.MD5; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.mokee.admin.mapper.SysFileMapper; import com.mokee.admin.service.ConfigService; import com.mokee.admin.service.FileService; import com.mokee.common.entity.SysFile; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; /** * 文件服务实现 — 文件二进制数据存储于本地磁盘 *
* 磁盘路径结构:{basePath}/{yyyy-MM}/{storageFileName} * 例:/data/mokee/gw/file/2026-06/a1b2c3d4e5f6.png *
* 向下兼容:存量 BLOB 数据(file_data 不为空)仍可正常读取。
*
* @author mokee
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class FileServiceImpl implements FileService {
private final SysFileMapper sysFileMapper;
private final ConfigService configService;
private static final DateTimeFormatter YYYY_MM = DateTimeFormatter.ofPattern("yyyy-MM");
private static final String DEFAULT_BASE_PATH = "/data/mokee/gw/file";
/** 获取配置的存储根目录 */
private String getBasePath() {
return configService.getValue("file.storage.disk.path", DEFAULT_BASE_PATH);
}
/**
* 从原始文件名提取扩展名
*/
private String ext(String fileName) {
if (fileName == null) return "";
int i = fileName.lastIndexOf('.');
return i > 0 ? fileName.substring(i) : "";
}
@Override
public SysFile upload(MultipartFile file, String fileType, String systemId,
String userId, String uploadUserName) throws IOException {
byte[] data = file.getBytes();
// 计算 MD5
String md5 = MD5.create().digestHex(data);
// 构建实体(先生成 ID 用于存储文件名)
String fileId = cn.hutool.core.lang.UUID.randomUUID().toString().replace("-", "");
String storageFileName = fileId + ext(file.getOriginalFilename());
LocalDateTime now = LocalDateTime.now();
SysFile sf = SysFile.builder()
.id(fileId)
.systemId(systemId)
.fileName(file.getOriginalFilename())
.storageFileName(storageFileName)
.fileType(fileType != null ? fileType : "other")
.fileSize(file.getSize())
.mimeType(file.getContentType())
.md5(md5)
.uploadUserId(userId)
.uploadUserName(uploadUserName)
.fileData(null) // 新文件不写入 BLOB
.createdAt(now)
.build();
// 写入磁盘
String basePath = getBasePath();
Path dir = Paths.get(basePath, now.format(YYYY_MM));
try {
Files.createDirectories(dir);
} catch (IOException e) {
log.error("创建存储目录失败: {} — 请检查磁盘权限和路径配置", dir);
throw new IOException("存储目录创建失败: " + dir + " (请检查磁盘权限和路径配置)", e);
}
Path filePath = dir.resolve(storageFileName);
try {
Files.write(filePath, data);
} catch (IOException e) {
log.error("写入文件失败: {} — 可能磁盘空间不足或权限不足", filePath);
throw new IOException("文件写入失败: " + filePath + " (可能磁盘空间不足或权限不足)", e);
}
log.info("文件已写入磁盘: {} ({} bytes, md5={})", filePath, file.getSize(), md5);
// 存元信息到 DB
sysFileMapper.insert(sf);
return sf;
}
@Override
public IPage