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 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> page(@RequestBody java.util.Map 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(@RequestParam(required = false) String fileType, @RequestParam(required = false) String systemId) { return Result.ok(fileService.list(fileType, systemId)); } /** * 获取文件元信息(不含二进制数据) */ @GetMapping("/{id}") public Result getById(@PathVariable String id) { return Result.ok(fileService.getMetaById(id)); } /** * 预览文件 — 直接输出文件流 *

* 优先读磁盘文件;磁盘文件不存在时回退到 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; } } }