Files
ERP/oa-backend/src/main/java/com/kaidi/oa/web/CultureArchiveController.java
T
QiufengandClaude Opus 4.8 5e51dc3f56 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>
2026-06-15 19:19:15 +08:00

311 lines
14 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.kaidi.oa.web;
import com.kaidi.oa.common.ApiException;
import com.kaidi.oa.common.ApiResp;
import com.kaidi.oa.common.NotFoundException;
import com.kaidi.oa.domain.CultureArchive;
import com.kaidi.oa.repository.CultureArchiveRepository;
import com.kaidi.oa.repository.CultureAwardRepository;
import com.kaidi.oa.repository.CultureCaseRepository;
import com.kaidi.oa.repository.CultureMaterialRepository;
import com.kaidi.oa.repository.CultureStoryRepository;
import com.kaidi.oa.repository.CultureSurveyRepository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.Instant;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 项目文化资产归档(品牌推广部·模块6,整模块从0建起)。
*
* - CRUD + 归档状态机(草稿→归档中→已归档/已撤销)。
* - /pack/{projectCode}:自动打包聚合——跨 CultureStory/CultureMaterial/CultureCase/
* CultureAward/CultureSurvey 取该项目各类资产计数,回填到 CultureArchive,一键生成档案。
* - /yearbook:文化年报——按年份聚合已归档项目的所有资产计数,自动生成年报摘要。
* - /showcase:数字展厅/文化墙——按项目返回归档亮点(代替地图/时间轴数据源),
* 含各项目文化资产总量与代表性故事/案例摘要。
*
* 含归档项目信息(机密管理),登记进 AuthInterceptor 的 SENSITIVE_READ_PREFIXES。
*/
@RestController
@RequestMapping("/api/oa/culture-archives")
public class CultureArchiveController {
private final CultureArchiveRepository archiveRepo;
private final CultureStoryRepository storyRepo;
private final CultureMaterialRepository materialRepo;
private final CultureCaseRepository caseRepo;
private final CultureAwardRepository awardRepo;
private final CultureSurveyRepository surveyRepo;
public CultureArchiveController(CultureArchiveRepository archiveRepo,
CultureStoryRepository storyRepo,
CultureMaterialRepository materialRepo,
CultureCaseRepository caseRepo,
CultureAwardRepository awardRepo,
CultureSurveyRepository surveyRepo) {
this.archiveRepo = archiveRepo;
this.storyRepo = storyRepo;
this.materialRepo = materialRepo;
this.caseRepo = caseRepo;
this.awardRepo = awardRepo;
this.surveyRepo = surveyRepo;
}
@GetMapping
public ApiResp<List<CultureArchive>> list(
@RequestParam(required = false) String status,
@RequestParam(required = false) String year) {
if (status != null && !status.isBlank()) {
return ApiResp.ok(archiveRepo.findByStatus(status));
}
if (year != null && !year.isBlank()) {
return ApiResp.ok(archiveRepo.findByArchiveDateStartingWith(year));
}
return ApiResp.ok(archiveRepo.findAll());
}
@GetMapping("/{id}")
public ApiResp<CultureArchive> get(@PathVariable Long id) {
return ApiResp.ok(require(id));
}
public record ArchiveRequest(
String code, String projectCode, String projectName,
String completionDate, String operator, String remark, String archiveFileUrl) {
}
@PostMapping
public ApiResp<CultureArchive> create(@RequestBody ArchiveRequest req) {
if (req.projectCode() == null || req.projectCode().isBlank()) {
throw new ApiException(400, "项目编号(projectCode) 不能为空");
}
if (archiveRepo.findByProjectCode(req.projectCode()).isPresent()) {
throw new ApiException(409, "项目「" + req.projectCode() + "」已存在文化档案,不能重复创建");
}
CultureArchive a = new CultureArchive();
a.setCode(req.code() == null || req.code().isBlank()
? "CA-" + (archiveRepo.count() + 1) : req.code());
a.setProjectCode(req.projectCode());
a.setProjectName(req.projectName());
a.setCompletionDate(req.completionDate());
a.setOperator(req.operator());
a.setRemark(req.remark());
a.setArchiveFileUrl(req.archiveFileUrl());
a.setStatus("草稿");
a.setCreatedAt(Instant.now());
return ApiResp.ok(archiveRepo.save(a));
}
@PatchMapping("/{id}")
public ApiResp<CultureArchive> update(@PathVariable Long id, @RequestBody ArchiveRequest req) {
CultureArchive a = require(id);
if ("已归档".equals(a.getStatus())) {
throw new ApiException(409, "已归档的档案不能修改");
}
if (req.projectName() != null) a.setProjectName(req.projectName());
if (req.completionDate() != null) a.setCompletionDate(req.completionDate());
if (req.operator() != null) a.setOperator(req.operator());
if (req.remark() != null) a.setRemark(req.remark());
if (req.archiveFileUrl() != null) a.setArchiveFileUrl(req.archiveFileUrl());
return ApiResp.ok(archiveRepo.save(a));
}
@DeleteMapping("/{id}")
@Transactional
public ApiResp<Void> delete(@PathVariable Long id) {
CultureArchive a = require(id);
if ("已归档".equals(a.getStatus())) {
throw new ApiException(409, "已归档档案不能删除");
}
archiveRepo.deleteById(id);
return ApiResp.ok(null);
}
// ---------- 自动打包聚合(核心能力)----------
/**
* 自动打包:跨模块聚合该项目的所有文化资产计数(故事/素材/案例/评优/调查),
* 回填到 CultureArchive,生成标准档案数据。若档案不存在则自动新建。
* 对应需求:"项目完工后自动打包文化资产归档"。
*/
@PostMapping("/pack/{projectCode}")
@Transactional
public ApiResp<CultureArchive> pack(@PathVariable String projectCode,
@RequestBody(required = false) PackRequest req) {
// 找到或新建档案
CultureArchive a = archiveRepo.findByProjectCode(projectCode).orElseGet(() -> {
CultureArchive na = new CultureArchive();
na.setCode("CA-" + (archiveRepo.count() + 1));
na.setProjectCode(projectCode);
na.setProjectName(req != null && req.projectName() != null ? req.projectName() : projectCode);
na.setStatus("归档中");
na.setCreatedAt(Instant.now());
return na;
});
if ("已归档".equals(a.getStatus())) {
throw new ApiException(409, "项目「" + projectCode + "」已完成归档,不可重复打包");
}
// 跨模块聚合:故事(已发布)
long stories = storyRepo.countByProjectCodeAndStatus(projectCode, "已发布");
a.setStoryCount((int) stories);
// 素材(已通过)
long materials = materialRepo.countByProjectCode(projectCode);
a.setMaterialCount((int) materials);
// 案例(已入库 + 优秀案例)
long cases = caseRepo.findByProjectCode(projectCode).stream()
.filter(c -> "已入库".equals(c.getStatus()) || "优秀案例".equals(c.getStatus()))
.count();
a.setCaseCount((int) cases);
// 评优(已表彰)
long awards = awardRepo.findAll().stream()
.filter(aw -> projectCode.equals(aw.getProjectCode()) && "已表彰".equals(aw.getStatus()))
.count();
a.setAwardCount((int) awards);
// 调查(已结束)
long surveys = surveyRepo.findAll().stream()
.filter(s -> projectCode.equals(s.getProjectCode()) && "已结束".equals(s.getStatus()))
.count();
a.setSurveyCount((int) surveys);
a.setStatus("归档中");
if (req != null && req.completionDate() != null) a.setCompletionDate(req.completionDate());
if (req != null && req.operator() != null) a.setOperator(req.operator());
return ApiResp.ok(archiveRepo.save(a));
}
public record PackRequest(String projectName, String completionDate, String operator) {
}
/** 确认归档:归档中 → 已归档,设定归档日。 */
@PostMapping("/{id}/confirm")
public ApiResp<CultureArchive> confirm(@PathVariable Long id) {
CultureArchive a = require(id);
if (!"归档中".equals(a.getStatus())) {
throw new ApiException(409, "仅「归档中」可确认归档,当前「" + a.getStatus() + "」");
}
a.setStatus("已归档");
a.setArchiveDate(LocalDate.now().toString());
return ApiResp.ok(archiveRepo.save(a));
}
/** 撤销归档:已归档 → 已撤销(管理员操作)。 */
@PostMapping("/{id}/revoke")
public ApiResp<CultureArchive> revoke(@PathVariable Long id) {
CultureArchive a = require(id);
if (!"已归档".equals(a.getStatus())) {
throw new ApiException(409, "仅「已归档」可撤销,当前「" + a.getStatus() + "」");
}
a.setStatus("已撤销");
return ApiResp.ok(archiveRepo.save(a));
}
// ---------- 文化年报(模块6 gap 2----------
public record YearbookEntry(
String year, int archiveCount, int totalStories, int totalMaterials,
int totalCases, int totalAwards, int totalSurveys, List<String> projectNames) {
}
/**
* 文化年报:按归档年份聚合已归档项目的文化资产总量,自动生成年报摘要。
* 对应需求:"每年编制项目文化年报,汇总各项目文化建设成果"。
*/
@GetMapping("/yearbook")
public ApiResp<List<YearbookEntry>> yearbook() {
List<CultureArchive> archived = archiveRepo.findByStatus("已归档");
Map<String, YearbookAccum> byYear = new LinkedHashMap<>();
for (CultureArchive a : archived) {
String year = a.getArchiveDate() != null && a.getArchiveDate().length() >= 4
? a.getArchiveDate().substring(0, 4) : "未知年份";
byYear.computeIfAbsent(year, k -> new YearbookAccum()).add(a);
}
List<YearbookEntry> result = new ArrayList<>();
for (Map.Entry<String, YearbookAccum> e : byYear.entrySet()) {
YearbookAccum acc = e.getValue();
result.add(new YearbookEntry(e.getKey(), acc.count,
acc.stories, acc.materials, acc.cases, acc.awards, acc.surveys,
acc.projectNames));
}
return ApiResp.ok(result);
}
private static class YearbookAccum {
int count, stories, materials, cases, awards, surveys;
List<String> projectNames = new ArrayList<>();
void add(CultureArchive a) {
count++;
stories += a.getStoryCount() == null ? 0 : a.getStoryCount();
materials += a.getMaterialCount() == null ? 0 : a.getMaterialCount();
cases += a.getCaseCount() == null ? 0 : a.getCaseCount();
awards += a.getAwardCount() == null ? 0 : a.getAwardCount();
surveys += a.getSurveyCount() == null ? 0 : a.getSurveyCount();
if (a.getProjectName() != null) projectNames.add(a.getProjectName());
}
}
// ---------- 数字展厅/文化墙(模块6 gap 3----------
public record ShowcaseItem(
String projectCode, String projectName, String completionDate,
int storyCount, int materialCount, int caseCount, int awardCount,
int totalAssets) {
}
/**
* 数字展厅/文化墙:返回所有已归档项目的文化亮点数据(按时间轴排列)。
* 前端可据此渲染时间轴卡片或地图标注。
* 对应需求:"以地图或时间轴形式展示各项目文化亮点"。
*/
@GetMapping("/showcase")
public ApiResp<List<ShowcaseItem>> showcase() {
List<CultureArchive> archived = archiveRepo.findByStatus("已归档");
List<ShowcaseItem> items = new ArrayList<>();
for (CultureArchive a : archived) {
int total = (a.getStoryCount() == null ? 0 : a.getStoryCount())
+ (a.getMaterialCount() == null ? 0 : a.getMaterialCount())
+ (a.getCaseCount() == null ? 0 : a.getCaseCount())
+ (a.getAwardCount() == null ? 0 : a.getAwardCount());
items.add(new ShowcaseItem(
a.getProjectCode(), a.getProjectName(), a.getCompletionDate(),
a.getStoryCount() == null ? 0 : a.getStoryCount(),
a.getMaterialCount() == null ? 0 : a.getMaterialCount(),
a.getCaseCount() == null ? 0 : a.getCaseCount(),
a.getAwardCount() == null ? 0 : a.getAwardCount(),
total));
}
// 按完工日倒排
items.sort((x, y) -> {
String a2 = x.completionDate() == null ? "" : x.completionDate();
String b2 = y.completionDate() == null ? "" : y.completionDate();
return b2.compareTo(a2);
});
return ApiResp.ok(items);
}
private CultureArchive require(Long id) {
return archiveRepo.findById(id)
.orElseThrow(() -> new NotFoundException("culture archive not found: " + id));
}
}