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( @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 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 autoGenerate(@RequestBody GenerateRequest req) { String month = req.publishMonth() != null && !req.publishMonth().isBlank() ? req.publishMonth() : LocalDate.now().toString().substring(0, 7); // 幂等:同月已有简报,直接返回 List existing = repo.findByPublishMonth(month); if (!existing.isEmpty()) { return ApiResp.ok(existing.get(0)); } // 统计宣传任务 List allTasks = taskRepo.findAll(); int total = 0, done = 0, overdue = 0; Set 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 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 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 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 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)); } }