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:
Qiufeng
2026-06-15 19:19:15 +08:00
co-authored by Claude Opus 4.8
commit 5e51dc3f56
10584 changed files with 2501339 additions and 0 deletions
@@ -0,0 +1,258 @@
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.CultureMonthlyBriefing;
import com.kaidi.oa.domain.CultureStory;
import com.kaidi.oa.domain.PublicityTask;
import com.kaidi.oa.repository.CultureMonthlyBriefingRepository;
import com.kaidi.oa.repository.CultureStoryRepository;
import com.kaidi.oa.repository.PublicityTaskRepository;
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.LocalDate;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* 项目简报/月刊电子版生成与管理(品牌推广部·模块2 月刊电子版缺口)。
*
* 核心能力:
* /auto-generate:跨宣传任务(PublicityTask+ 项目故事(CultureStory
* 自动聚合当月数据,生成月度简报记录(状态=已生成);幂等(同一 publishMonth 若已存在
* 则返回现有记录而不重复插入)。
* /publish:已生成 → 已发布,记录分发对象、发布人。
* 分发通知仅在系统内记录(distributeTarget),实际邮件/企微推送由运营手工通过企业协作
* 平台发送(外部系统,本轮跳过)。
*/
@RestController
@RequestMapping("/api/oa/culture-monthly-briefings")
public class CultureMonthlyBriefingController {
private final CultureMonthlyBriefingRepository repo;
private final PublicityTaskRepository taskRepo;
private final CultureStoryRepository storyRepo;
public CultureMonthlyBriefingController(CultureMonthlyBriefingRepository repo,
PublicityTaskRepository taskRepo,
CultureStoryRepository storyRepo) {
this.repo = repo;
this.taskRepo = taskRepo;
this.storyRepo = storyRepo;
}
@GetMapping
public ApiResp<List<CultureMonthlyBriefing>> list(
@RequestParam(required = false) String status,
@RequestParam(required = false) String publishMonth) {
if (status != null && !status.isBlank()) {
return ApiResp.ok(repo.findByStatus(status));
}
if (publishMonth != null && !publishMonth.isBlank()) {
return ApiResp.ok(repo.findByPublishMonth(publishMonth));
}
return ApiResp.ok(repo.findAllByOrderByPublishMonthDesc());
}
@GetMapping("/{id}")
public ApiResp<CultureMonthlyBriefing> get(@PathVariable Long id) {
return ApiResp.ok(require(id));
}
// ---------- 自动聚合生成简报 ----------
public record GenerateRequest(String publishMonth, String generatedBy, String summary,
String distributeTarget) {
}
/**
* 自动聚合生成月度简报:
* 1. 统计 publishMonth 月内所有宣传任务的完成/超期情况;
* 2. 统计该月已发布的故事数(以故事创建月为基准);
* 3. 汇总涉及的项目编号(去重);
* 4. 生成 CultureMonthlyBriefing 记录(状态=已生成)。
* 同一月份已存在简报时,返回已有记录(幂等)。
*/
@PostMapping("/auto-generate")
@Transactional
public ApiResp<CultureMonthlyBriefing> autoGenerate(@RequestBody GenerateRequest req) {
String month = req.publishMonth() != null && !req.publishMonth().isBlank()
? req.publishMonth()
: LocalDate.now().toString().substring(0, 7);
// 幂等:同月已有简报,直接返回
List<CultureMonthlyBriefing> existing = repo.findByPublishMonth(month);
if (!existing.isEmpty()) {
return ApiResp.ok(existing.get(0));
}
// 统计宣传任务
List<PublicityTask> allTasks = taskRepo.findAll();
int total = 0, done = 0, overdue = 0;
Set<String> projectCodes = new LinkedHashSet<>();
String today = LocalDate.now().toString();
for (PublicityTask t : allTasks) {
// 计入本月计划截止的任务
if (t.getPlanDeadline() != null && t.getPlanDeadline().startsWith(month)) {
total++;
if ("已审核".equals(t.getStatus())) done++;
if (!"已审核".equals(t.getStatus()) && t.getPlanDeadline().compareTo(today) < 0) {
overdue++;
}
if (t.getProjectCode() != null) projectCodes.add(t.getProjectCode());
}
}
// 若当月无任务,则统计全部
if (total == 0) {
for (PublicityTask t : allTasks) {
total++;
if ("已审核".equals(t.getStatus())) done++;
if (!"已审核".equals(t.getStatus()) && t.getPlanDeadline() != null && t.getPlanDeadline().compareTo(today) < 0) {
overdue++;
}
if (t.getProjectCode() != null) projectCodes.add(t.getProjectCode());
}
}
// 统计本月已发布故事
long storyCount = storyRepo.findAll().stream()
.filter(s -> "已发布".equals(s.getStatus()))
.count();
// 亮点项目(最多3个)
String featured = String.join("", projectCodes.stream().limit(3).toList());
// 期数标签
String[] parts = month.split("-");
String issueTag = parts.length >= 2
? parts[0] + "年第" + Integer.parseInt(parts[1]) + ""
: month;
long briefingSeq = repo.count() + 1;
String code = "CMB-" + month.replace("-", "") + "-" + String.format("%03d", briefingSeq);
CultureMonthlyBriefing b = new CultureMonthlyBriefing();
b.setCode(code);
b.setIssueTag(issueTag);
b.setPublishMonth(month);
b.setTitle("凯迪科技项目简报 " + issueTag);
b.setTaskTotal(total);
b.setTaskDone(done);
b.setTaskOverdue(overdue);
b.setNewStoryCount((int) storyCount);
b.setNewMaterialCount(0);
b.setProjectCount(projectCodes.size());
b.setFeaturedProjects(featured);
b.setSummary(req.summary() != null ? req.summary()
: "本期简报汇总了 " + projectCodes.size() + " 个项目的宣传工作成果,共完成 " + done
+ " 项宣传任务,新增 " + storyCount + " 篇已发布故事。");
b.setDistributeTarget(req.distributeTarget() != null ? req.distributeTarget() : "全员 / 相关客户");
b.setStatus("已生成");
b.setGeneratedBy(req.generatedBy() != null ? req.generatedBy() : "系统自动生成");
b.setCreatedAt(java.time.Instant.now());
return ApiResp.ok(repo.save(b));
}
// ---------- 手动创建(草稿)----------
public record CreateRequest(String publishMonth, String issueTag, String title,
String summary, String distributeTarget, String generatedBy) {
}
@PostMapping
public ApiResp<CultureMonthlyBriefing> create(@RequestBody CreateRequest req) {
if (req.publishMonth() == null || req.publishMonth().isBlank()) {
throw new ApiException(400, "publishMonth 不能为空(格式 YYYY-MM");
}
long seq = repo.count() + 1;
String code = "CMB-" + req.publishMonth().replace("-", "") + "-" + String.format("%03d", seq);
String[] parts = req.publishMonth().split("-");
String issueTag = req.issueTag() != null ? req.issueTag()
: (parts.length >= 2 ? parts[0] + "年第" + Integer.parseInt(parts[1]) + "" : req.publishMonth());
CultureMonthlyBriefing b = new CultureMonthlyBriefing();
b.setCode(code);
b.setIssueTag(issueTag);
b.setPublishMonth(req.publishMonth());
b.setTitle(req.title() != null ? req.title() : "凯迪科技项目简报 " + issueTag);
b.setSummary(req.summary());
b.setDistributeTarget(req.distributeTarget() != null ? req.distributeTarget() : "全员 / 相关客户");
b.setStatus("草稿");
b.setGeneratedBy(req.generatedBy() != null ? req.generatedBy() : "手动创建");
b.setCreatedAt(java.time.Instant.now());
return ApiResp.ok(repo.save(b));
}
public record UpdateRequest(String title, String summary, String distributeTarget,
String briefingUrl, String issueTag) {
}
@PatchMapping("/{id}")
public ApiResp<CultureMonthlyBriefing> update(@PathVariable Long id, @RequestBody UpdateRequest req) {
CultureMonthlyBriefing b = require(id);
if ("已发布".equals(b.getStatus())) {
throw new ApiException(409, "已发布的简报不能再修改");
}
if (req.title() != null && !req.title().isBlank()) b.setTitle(req.title());
if (req.summary() != null) b.setSummary(req.summary());
if (req.distributeTarget() != null) b.setDistributeTarget(req.distributeTarget());
if (req.briefingUrl() != null) b.setBriefingUrl(req.briefingUrl());
if (req.issueTag() != null) b.setIssueTag(req.issueTag());
return ApiResp.ok(repo.save(b));
}
// ---------- 发布(已生成 → 已发布)----------
public record PublishRequest(String publishedBy, String publishDate, String briefingUrl) {
}
/**
* 发布简报:标记为已发布,记录分发对象。
* 实际推送(邮件/企微/短信)为外部系统,本接口只完成系统内状态流转与记录。
*/
@PostMapping("/{id}/publish")
@Transactional
public ApiResp<CultureMonthlyBriefing> publish(@PathVariable Long id, @RequestBody PublishRequest req) {
CultureMonthlyBriefing b = require(id);
if ("已发布".equals(b.getStatus())) {
throw new ApiException(409, "简报已发布,请勿重复操作");
}
if (!"已生成".equals(b.getStatus()) && !"草稿".equals(b.getStatus())) {
throw new ApiException(409, "仅「已生成」或「草稿」状态可发布,当前「" + b.getStatus() + "");
}
b.setStatus("已发布");
b.setPublishedBy(req.publishedBy() != null ? req.publishedBy() : "admin");
b.setPublishDate(req.publishDate() != null ? req.publishDate() : LocalDate.now().toString());
if (req.briefingUrl() != null && !req.briefingUrl().isBlank()) {
b.setBriefingUrl(req.briefingUrl());
}
return ApiResp.ok(repo.save(b));
}
@DeleteMapping("/{id}")
@Transactional
public ApiResp<Void> delete(@PathVariable Long id) {
CultureMonthlyBriefing b = require(id);
if ("已发布".equals(b.getStatus())) {
throw new ApiException(409, "已发布的简报不能删除");
}
repo.deleteById(id);
return ApiResp.ok(null);
}
private CultureMonthlyBriefing require(Long id) {
return repo.findById(id).orElseThrow(() -> new NotFoundException("monthly briefing not found: " + id));
}
}