package com.mokee.admin.controller; import com.baomidou.mybatisplus.core.metadata.IPage; import com.mokee.admin.service.FileService; import com.mokee.common.Result; import com.mokee.common.annotation.OperationLog; import com.mokee.common.entity.SysFile; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpHeaders; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; /** * 文件管理控制器 *
* 文件二进制数据存储于本地磁盘,数据库仅保存元信息。 * 向下兼容存量 BLOB 数据(file_data 非空时优先读磁盘,磁盘无文件则回退 DB BLOB)。 *
* 请求路径前缀:{@code /zgapi/v1/admin/file} * * @author mokee */ @RestController @RequestMapping("/zgapi/v1/admin/file") @RequiredArgsConstructor public class FileController { private final FileService fileService; /** * 上传文件(管理后台) *
* 上传者显示名从请求头 X-User-Name 获取(由网关 HeaderInjectFilter 注入,已 URL 解码)。
*/
@OperationLog(module = "文件管理", action = "上传")
@PostMapping("/upload")
public Result
* 优先读磁盘文件;磁盘文件不存在时回退到 DB BLOB(存量兼容)。
* 设置 CORS 和缓存头,支持被 img 标签跨域引用。
*/
@GetMapping("/{id}/view")
public void view(@PathVariable String id, HttpServletResponse response) throws IOException {
SysFile sf = fileService.getById(id);
if (sf == null) { response.setStatus(404); return; }
Path diskPath = fileService.getDiskPath(sf);
if (!Files.exists(diskPath)) {
response.setStatus(404);
response.getWriter().write("File not found");
return;
}
byte[] data = Files.readAllBytes(diskPath);
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
String mimeType = sf.getMimeType() != null ? sf.getMimeType() : "application/octet-stream";
response.setContentType(mimeType);
response.setHeader(HttpHeaders.CACHE_CONTROL, "public, max-age=86400");
response.setContentLengthLong(data.length);
response.getOutputStream().write(data);
response.getOutputStream().flush();
}
/**
* 下载文件 — 触发浏览器下载
*
* 与 view 逻辑相同,额外设置 Content-Disposition: attachment 头。
*/
@GetMapping("/{id}/download")
public void download(@PathVariable String id, HttpServletResponse response) throws IOException {
SysFile sf = fileService.getById(id);
if (sf == null) { response.setStatus(404); return; }
Path diskPath = fileService.getDiskPath(sf);
if (!Files.exists(diskPath)) {
response.setStatus(404);
response.getWriter().write("File not found");
return;
}
byte[] data = Files.readAllBytes(diskPath);
// Content-Disposition: filename=ASCII兜底, filename*=UTF-8编码的原始文件名
String safeName = "download" + ext(sf.getFileName());
String encodedName = URLEncoder.encode(sf.getFileName(), StandardCharsets.UTF_8)
.replace("+", "%20");
response.setContentType("application/octet-stream");
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + safeName + "\"; filename*=UTF-8''" + encodedName);
response.setContentLengthLong(data.length);
response.getOutputStream().write(data);
response.getOutputStream().flush();
}
/** 从文件名提取扩展名(含点) */
private static String ext(String name) {
if (name == null) return "";
int i = name.lastIndexOf('.');
return i > 0 ? name.substring(i) : "";
}
/**
* 删除文件 — 删磁盘文件 + DB 记录
*/
@OperationLog(module = "文件管理", action = "删除")
@DeleteMapping("/{id}")
public Result> delete(@PathVariable String id) throws IOException {
fileService.delete(id);
return Result.ok();
}
/** URL 解码 HTTP 头值(网关对中文值做了 URL 编码) */
private String decodeHeader(String value) {
if (value == null) return null;
try {
return URLDecoder.decode(value, StandardCharsets.UTF_8);
} catch (Exception e) {
return value;
}
}
}
> list(@RequestParam(required = false) String fileType,
@RequestParam(required = false) String systemId) {
return Result.ok(fileService.list(fileType, systemId));
}
/**
* 获取文件元信息(不含二进制数据)
*/
@GetMapping("/{id}")
public Result