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,310 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user