177 lines
6.9 KiB
Java
177 lines
6.9 KiB
Java
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;
|
||
|
||
/**
|
||
* 文件管理控制器
|
||
* <p>
|
||
* 文件二进制数据存储于本地磁盘,数据库仅保存元信息。
|
||
* 向下兼容存量 BLOB 数据(file_data 非空时优先读磁盘,磁盘无文件则回退 DB BLOB)。
|
||
* <p>
|
||
* 请求路径前缀:{@code /zgapi/v1/admin/file}
|
||
*
|
||
* @author mokee
|
||
*/
|
||
@RestController
|
||
@RequestMapping("/zgapi/v1/admin/file")
|
||
@RequiredArgsConstructor
|
||
public class FileController {
|
||
private final FileService fileService;
|
||
|
||
/**
|
||
* 上传文件(管理后台)
|
||
* <p>
|
||
* 上传者显示名从请求头 X-User-Name 获取(由网关 HeaderInjectFilter 注入,已 URL 解码)。
|
||
*/
|
||
@OperationLog(module = "文件管理", action = "上传")
|
||
@PostMapping("/upload")
|
||
public Result<SysFile> upload(@RequestParam("file") MultipartFile file,
|
||
@RequestParam(defaultValue = "other") String fileType,
|
||
@RequestParam(required = false) String systemId,
|
||
HttpServletRequest request) throws IOException {
|
||
String userId = request.getHeader("X-User-Id");
|
||
// X-User-Name 经网关 URL 编码(防 HTTP 转发中文损坏),需解码
|
||
String userName = decodeHeader(request.getHeader("X-User-Name"));
|
||
if (userName == null || userName.isEmpty()) {
|
||
userName = request.getHeader("X-User-Account");
|
||
}
|
||
SysFile sf = fileService.upload(file, fileType, systemId, userId, userName);
|
||
sf.setFileData(null); // 不返回二进制数据
|
||
return Result.ok(sf);
|
||
}
|
||
|
||
/**
|
||
* 分页查询文件列表(不含 file_data)
|
||
*/
|
||
@PostMapping("/page")
|
||
public Result<IPage<SysFile>> page(@RequestBody java.util.Map<String, Object> params) {
|
||
Integer page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
|
||
Integer size = params.get("pageSize") != null ? ((Number) params.get("pageSize")).intValue() : 10;
|
||
String fileType = (String) params.get("fileType");
|
||
String fileName = (String) params.get("fileName");
|
||
String uploadUserName = (String) params.get("uploadUserName");
|
||
String systemId = params.get("systemId") != null ? params.get("systemId").toString() : null;
|
||
return Result.ok(fileService.page(page, size, fileType, fileName, uploadUserName, systemId));
|
||
}
|
||
|
||
/**
|
||
* 文件列表查询(不分页,不含 file_data)
|
||
*/
|
||
@GetMapping("/list")
|
||
public Result<List<SysFile>> list(@RequestParam(required = false) String fileType,
|
||
@RequestParam(required = false) String systemId) {
|
||
return Result.ok(fileService.list(fileType, systemId));
|
||
}
|
||
|
||
/**
|
||
* 获取文件元信息(不含二进制数据)
|
||
*/
|
||
@GetMapping("/{id}")
|
||
public Result<SysFile> getById(@PathVariable String id) {
|
||
return Result.ok(fileService.getMetaById(id));
|
||
}
|
||
|
||
/**
|
||
* 预览文件 — 直接输出文件流
|
||
* <p>
|
||
* 优先读磁盘文件;磁盘文件不存在时回退到 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();
|
||
}
|
||
|
||
/**
|
||
* 下载文件 — 触发浏览器下载
|
||
* <p>
|
||
* 与 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;
|
||
}
|
||
}
|
||
}
|