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( @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 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 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 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 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 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 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 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 projectNames) { } /** * 文化年报:按归档年份聚合已归档项目的文化资产总量,自动生成年报摘要。 * 对应需求:"每年编制项目文化年报,汇总各项目文化建设成果"。 */ @GetMapping("/yearbook") public ApiResp> yearbook() { List archived = archiveRepo.findByStatus("已归档"); Map 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 result = new ArrayList<>(); for (Map.Entry 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 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> showcase() { List archived = archiveRepo.findByStatus("已归档"); List 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)); } }