package com.kaidi.oa.web; import com.kaidi.oa.common.ApiException; import com.kaidi.oa.common.ApiResp; import com.kaidi.oa.domain.Archive; import com.kaidi.oa.domain.ArchiveAccessLog; import com.kaidi.oa.repository.ArchiveAccessLogRepository; import com.kaidi.oa.repository.ArchiveRepository; import jakarta.servlet.http.HttpServletRequest; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; /** * 资料室·Module 3「电子文件管理」批量操作补完。 * *

补缺口:「批量下载/转移/修改元数据无专门端点」。 * 本控制器提供三个批量端点(均 @Transactional,幂等写入): *

* * 注意:PDF/A 格式转换、OCR 正文识别、区块链哈希存证属外部系统,本轮跳过。 */ @RestController @RequestMapping("/api/oa/archive-batch") public class ArchiveBatchController { private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private final ArchiveRepository archiveRepo; private final ArchiveAccessLogRepository accessLogRepo; private final CurrentUserResolver currentUser; public ArchiveBatchController(ArchiveRepository archiveRepo, ArchiveAccessLogRepository accessLogRepo, CurrentUserResolver currentUser) { this.archiveRepo = archiveRepo; this.accessLogRepo = accessLogRepo; this.currentUser = currentUser; } // ───────────────────────────────────────────────────────────────────────── // 1. 批量转移分类 // ───────────────────────────────────────────────────────────────────────── public record TransferRequest(List ids, String targetCategory) {} public record BatchResult(int total, int affected, String message) {} /** * 批量将选定档案转移到目标分类。 * ids 最多 200 条,targetCategory 不可为空。 */ @PostMapping("/transfer") @Transactional public ApiResp transfer(@RequestBody TransferRequest req, HttpServletRequest http) { validateIds(req.ids()); if (req.targetCategory() == null || req.targetCategory().isBlank()) { throw new ApiException(400, "目标分类不可为空"); } List archives = archiveRepo.findAllById(req.ids()); String operator = resolveOperator(http); String now = LocalDateTime.now().format(FMT); for (Archive a : archives) { String oldCat = a.getCategory(); a.setCategory(req.targetCategory()); archiveRepo.save(a); writeLog(a.getId(), a.getTitle(), "批量转移", operator, "分类 " + oldCat + " → " + req.targetCategory(), http, now); } return ApiResp.ok(new BatchResult(req.ids().size(), archives.size(), "已将 " + archives.size() + " 条档案转移至【" + req.targetCategory() + "】")); } // ───────────────────────────────────────────────────────────────────────── // 2. 批量修改元数据 // ───────────────────────────────────────────────────────────────────────── /** * 批量修改元数据字段。 * 仅修改非空字段(null 表示不改)。 * 可修改:tags / summary / accessLevel / archiveDate。 */ public record MetaUpdateRequest( List ids, String tags, String summary, String accessLevel, String archiveDate) {} @PostMapping("/meta-update") @Transactional public ApiResp metaUpdate(@RequestBody MetaUpdateRequest req, HttpServletRequest http) { validateIds(req.ids()); boolean anyField = req.tags() != null || req.summary() != null || req.accessLevel() != null || req.archiveDate() != null; if (!anyField) { throw new ApiException(400, "至少需要指定一个要修改的元数据字段"); } List archives = archiveRepo.findAllById(req.ids()); String operator = resolveOperator(http); String now = LocalDateTime.now().format(FMT); for (Archive a : archives) { if (req.tags() != null) { a.setTags(req.tags()); } if (req.summary() != null) { a.setSummary(req.summary()); } if (req.accessLevel() != null) { a.setAccessLevel(req.accessLevel()); } if (req.archiveDate() != null) { a.setArchiveDate(req.archiveDate()); } archiveRepo.save(a); writeLog(a.getId(), a.getTitle(), "批量修改元数据", operator, buildMetaDetail(req), http, now); } return ApiResp.ok(new BatchResult(req.ids().size(), archives.size(), "已批量更新 " + archives.size() + " 条档案的元数据")); } // ───────────────────────────────────────────────────────────────────────── // 3. 批量下载清单(返回 fileUrl 列表,前端逐一触发下载) // ───────────────────────────────────────────────────────────────────────── public record DownloadListRequest(List ids) {} public record DownloadItem(Long id, String title, String fileType, String fileUrl) {} public record DownloadListResult(int total, List items, String note) {} /** * 批量下载清单:返回可下载档案的 fileUrl 列表(已有 fileUrl 的条目)。 * 前端逐条调用浏览器 <a download> 完成下载,避免后端 ZIP 依赖。 */ @PostMapping("/download-list") @Transactional public ApiResp downloadList(@RequestBody DownloadListRequest req, HttpServletRequest http) { validateIds(req.ids()); List archives = archiveRepo.findAllById(req.ids()); String operator = resolveOperator(http); String now = LocalDateTime.now().format(FMT); List items = archives.stream() .filter(a -> a.getFileUrl() != null && !a.getFileUrl().isBlank()) .map(a -> { writeLog(a.getId(), a.getTitle(), "批量下载", operator, "批量下载请求", http, now); return new DownloadItem(a.getId(), a.getTitle(), a.getFileType(), a.getFileUrl()); }) .toList(); int noUrl = archives.size() - items.size(); String note = noUrl > 0 ? noUrl + " 条档案无文件链接(实体档案或未上传电子版),已跳过" : "全部档案均有文件链接"; return ApiResp.ok(new DownloadListResult(items.size(), items, note)); } // ───────────────────────────────────────────────────────────────────────── // helpers // ───────────────────────────────────────────────────────────────────────── private void validateIds(List ids) { if (ids == null || ids.isEmpty()) { throw new ApiException(400, "ids 不可为空"); } if (ids.size() > 200) { throw new ApiException(400, "单次批量操作最多 200 条"); } } private String resolveOperator(HttpServletRequest http) { var u = currentUser.resolve(http); return u != null ? u.getDisplayName() : "未知用户"; } private void writeLog(Long archiveId, String title, String action, String operator, String detail, HttpServletRequest http, String now) { ArchiveAccessLog log = new ArchiveAccessLog(); log.setArchiveId(archiveId); log.setArchiveTitle(title); log.setAction(action); log.setOperator(operator); log.setIp(http.getRemoteAddr()); log.setDetail(detail); log.setOccurredAt(now); accessLogRepo.save(log); } private String buildMetaDetail(MetaUpdateRequest req) { java.util.LinkedHashMap m = new java.util.LinkedHashMap<>(); if (req.tags() != null) m.put("tags", req.tags()); if (req.summary() != null) m.put("summary", req.summary().length() > 20 ? req.summary().substring(0, 20) + "…" : req.summary()); if (req.accessLevel() != null) m.put("accessLevel", req.accessLevel()); if (req.archiveDate() != null) m.put("archiveDate", req.archiveDate()); StringBuilder sb = new StringBuilder("批量修改:"); m.forEach((k, v) -> sb.append(" ").append(k).append("=").append(v).append(";")); return sb.toString(); } }