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