SNAPSHOT W7 已部署稳定态 — 凯迪ERP+OA一体化平台 (MET 73.3%)
恢复点(restore point)。别人改崩后可 git reset --hard 回到此提交。 == 此快照内容 == - 后端 oa-backend: 734 控制器 / 711 实体 (Spring Boot 3.2.5 + SQLite, 端口8091) - 前端 modern-ui/app: Vue3+Vite, 约700页 (构建产物已在 oa-backend/src/main/resources/static) - 数据库 oa-backend/data/oa.db: 含全部演示数据 (强制入库, 6.6MB) - 交接文档 go.md + go-code-reference/endpoints/entities/database.md - 多代理建设脚本 .claude/wf-*.js == 状态 == - 对 凯迪科技ERP_20260507.xlsx 合规 MET ~73.3% (PARTIAL 75: 34可建+6种子/bug+35外部硬天花板) - 安全: 5轮红队+5轮复检, default-deny分级鉴权, 连续零可利用 - W3~W7 累计补完436缺口; W8末轮(40缺口)为半成品(源码树可编译但未集成) - 运行: cd oa-backend; java -jar build/libs/oa-backend-0.1.0.jar --server.port=8091; admin/123456 == 排除(gitignore, 可再生) == node_modules / oa-backend/build / .jdks / *.log / Backup-ERP-* / 弃用的OFBiz核心(只保留modern-ui) 完整文件夹备份见同目录 Backup-ERP-20260615-191517/ (含上述全部, 仅缺 node_modules) 时间戳: 20260615-191517 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
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「电子文件管理」批量操作补完。
|
||||
*
|
||||
* <p>补缺口:「批量下载/转移/修改元数据无专门端点」。
|
||||
* 本控制器提供三个批量端点(均 @Transactional,幂等写入):
|
||||
* <ul>
|
||||
* <li>POST /archive-batch/transfer — 批量转移档案分类(targetCategory);</li>
|
||||
* <li>POST /archive-batch/meta-update — 批量修改元数据(tags / summary / accessLevel / archiveDate);</li>
|
||||
* <li>POST /archive-batch/download-list — 批量下载清单(返回文件 URL 列表,前端逐一下载)。</li>
|
||||
* </ul>
|
||||
*
|
||||
* 注意: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<Long> ids, String targetCategory) {}
|
||||
|
||||
public record BatchResult(int total, int affected, String message) {}
|
||||
|
||||
/**
|
||||
* 批量将选定档案转移到目标分类。
|
||||
* ids 最多 200 条,targetCategory 不可为空。
|
||||
*/
|
||||
@PostMapping("/transfer")
|
||||
@Transactional
|
||||
public ApiResp<BatchResult> transfer(@RequestBody TransferRequest req,
|
||||
HttpServletRequest http) {
|
||||
validateIds(req.ids());
|
||||
if (req.targetCategory() == null || req.targetCategory().isBlank()) {
|
||||
throw new ApiException(400, "目标分类不可为空");
|
||||
}
|
||||
List<Archive> 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<Long> ids,
|
||||
String tags,
|
||||
String summary,
|
||||
String accessLevel,
|
||||
String archiveDate) {}
|
||||
|
||||
@PostMapping("/meta-update")
|
||||
@Transactional
|
||||
public ApiResp<BatchResult> 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<Archive> 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<Long> ids) {}
|
||||
|
||||
public record DownloadItem(Long id, String title, String fileType, String fileUrl) {}
|
||||
|
||||
public record DownloadListResult(int total, List<DownloadItem> items, String note) {}
|
||||
|
||||
/**
|
||||
* 批量下载清单:返回可下载档案的 fileUrl 列表(已有 fileUrl 的条目)。
|
||||
* 前端逐条调用浏览器 <a download> 完成下载,避免后端 ZIP 依赖。
|
||||
*/
|
||||
@PostMapping("/download-list")
|
||||
@Transactional
|
||||
public ApiResp<DownloadListResult> downloadList(@RequestBody DownloadListRequest req,
|
||||
HttpServletRequest http) {
|
||||
validateIds(req.ids());
|
||||
List<Archive> archives = archiveRepo.findAllById(req.ids());
|
||||
String operator = resolveOperator(http);
|
||||
String now = LocalDateTime.now().format(FMT);
|
||||
List<DownloadItem> 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<Long> 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<String, String> 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user