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,幂等写入):
*
* - POST /archive-batch/transfer — 批量转移档案分类(targetCategory);
* - POST /archive-batch/meta-update — 批量修改元数据(tags / summary / accessLevel / archiveDate);
* - POST /archive-batch/download-list — 批量下载清单(返回文件 URL 列表,前端逐一下载)。
*
*
* 注意: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();
}
}