Files
ERP/oa-backend/src/main/java/com/kaidi/oa/web/CultureMonthlyBriefingController.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

259 lines
11 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.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));
}
}