package com.kaidi.oa.web;
import com.kaidi.oa.common.ApiResp;
import com.kaidi.oa.common.NotFoundException;
import com.kaidi.oa.domain.DevProject;
import com.kaidi.oa.domain.DevProjectBudget;
import com.kaidi.oa.domain.DevWbsTask;
import com.kaidi.oa.domain.EngineeringChange;
import com.kaidi.oa.domain.ProductRequirement;
import com.kaidi.oa.domain.RdAfterSalesFeedback;
import com.kaidi.oa.repository.DevProjectBudgetRepository;
import com.kaidi.oa.repository.DevProjectRepository;
import com.kaidi.oa.repository.DevWbsTaskRepository;
import com.kaidi.oa.repository.EngineeringChangeRepository;
import com.kaidi.oa.repository.ProductRequirementRepository;
import com.kaidi.oa.repository.RdAfterSalesFeedbackRepository;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
/**
* 创新研发中心 / 产品开发部 · 跨部门协同聚合接口(需求模块 9)。
*
*
补审计缺口"DevProject 500使得跨部门联动依赖立项数据的接口运行时断链":
*
* - GET /rd-dev-cross-dept/{devProjectId}/launch-push —— 产品上市推送聚合:拉取立项
* 基本信息 + 已上市标记 + WBS 完成情况 + 预算执行,供销售/市场部门消费;
* - GET /rd-dev-cross-dept/{devProjectId}/improvement-summary —— 产品改进反馈汇总:
* 聚合与本立项 productLine 关联的售后改进反馈(来源 source=售后)及需求池改进条目,
* 供研发决策;
* - GET /rd-dev-cross-dept/launched-products —— 已上市产品清单(供销售部门拿 SKU);
* - GET /rd-dev-cross-dept/aftermarket-pending —— 待研发处理的售后改进需求数量(供仪表盘),
* 补充"销售/市场上市推送、售后产品改进反馈"跨部门链路。
*
*
* 读口聚合跨部门财务数据,已登记进 SENSITIVE_READ_PREFIXES(见 sharedFileSnippets)。
*/
@RestController
@RequestMapping("/api/oa/rd-dev-cross-dept")
public class RdDevCrossDeptController {
private final DevProjectRepository projectRepo;
private final DevProjectBudgetRepository budgetRepo;
private final DevWbsTaskRepository wbsRepo;
private final EngineeringChangeRepository ecoRepo;
private final ProductRequirementRepository reqRepo;
private final RdAfterSalesFeedbackRepository feedbackRepo;
public RdDevCrossDeptController(DevProjectRepository projectRepo,
DevProjectBudgetRepository budgetRepo,
DevWbsTaskRepository wbsRepo,
EngineeringChangeRepository ecoRepo,
ProductRequirementRepository reqRepo,
RdAfterSalesFeedbackRepository feedbackRepo) {
this.projectRepo = projectRepo;
this.budgetRepo = budgetRepo;
this.wbsRepo = wbsRepo;
this.ecoRepo = ecoRepo;
this.reqRepo = reqRepo;
this.feedbackRepo = feedbackRepo;
}
// ======================================================================
// 1. 产品上市推送聚合(供销售/市场部门)
// ======================================================================
public record WbsSummary(int total, int done, int inProgress, int pending) {
}
public record BudgetExec(double totalBudget, double totalActual, double execPct) {
}
public record LaunchPush(
Long id, String code, String name, String projectType, String productLine,
String stage, int progress, boolean productLaunched, String launchDate,
String leader, String planEnd,
WbsSummary wbs, BudgetExec budget, int ecoCount,
String techRoute, String objective) {
}
@GetMapping("/{devProjectId}/launch-push")
public ApiResp launchPush(@PathVariable Long devProjectId) {
DevProject p = projectRepo.findById(devProjectId)
.orElseThrow(() -> new NotFoundException("研发立项不存在:" + devProjectId));
// WBS 汇总
List tasks = wbsRepo.findByDevProjectIdOrderBySeqAsc(devProjectId);
int done = 0, inProg = 0, pending = 0;
for (DevWbsTask t : tasks) {
if ("已完成".equals(t.getStatus())) done++;
else if ("进行中".equals(t.getStatus())) inProg++;
else pending++;
}
WbsSummary wbs = new WbsSummary(tasks.size(), done, inProg, pending);
// 预算执行
List rows = budgetRepo.findByDevProjectId(devProjectId);
BigDecimal totalBud = BigDecimal.ZERO;
BigDecimal totalAct = BigDecimal.ZERO;
for (DevProjectBudget b : rows) {
totalBud = totalBud.add(nz(b.getBudgetAmount()));
totalAct = totalAct.add(nz(b.getActualAmount()));
}
double execPct = totalBud.signum() == 0 ? 0
: totalAct.divide(totalBud, 4, RoundingMode.HALF_UP).doubleValue() * 100;
BudgetExec budget = new BudgetExec(totalBud.doubleValue(), totalAct.doubleValue(), round2(execPct));
// ECO 变更次数
long ecoCount = ecoRepo.findByDevProjectId(devProjectId).size();
return ApiResp.ok(new LaunchPush(
p.getId(), p.getCode(), p.getName(), p.getProjectType(), p.getProductLine(),
p.getStage(), p.getProgress(), p.isProductLaunched(), p.getLaunchDate(),
p.getLeader(), p.getPlanEnd(),
wbs, budget, (int) ecoCount,
p.getTechRoute(), p.getObjective()));
}
// ======================================================================
// 2. 产品改进反馈汇总(按立项 productLine 聚合售后反馈 + 需求池改进条目)
// ======================================================================
public record ImprovementItem(String source, Long id, String code, String title,
String feedbackType, String severity, String status,
String submitter, String createdDate) {
}
public record ImprovementSummary(String productLine,
int totalFeedbacks, int pendingFeedbacks,
int reqPoolImprovements,
List feedbacks,
List reqPoolItems) {
}
@GetMapping("/{devProjectId}/improvement-summary")
public ApiResp improvementSummary(@PathVariable Long devProjectId) {
DevProject p = projectRepo.findById(devProjectId)
.orElseThrow(() -> new NotFoundException("研发立项不存在:" + devProjectId));
String pl = p.getProductLine();
// 售后改进反馈
List feedbacks = new ArrayList<>();
for (RdAfterSalesFeedback f : feedbackRepo.findAll()) {
if (pl != null && !pl.isBlank()) {
if (pl.equals(f.getProductLine())) feedbacks.add(f);
} else {
feedbacks.add(f);
}
}
int pending = (int) feedbacks.stream().filter(f -> "待处理".equals(f.getStatus())).count();
// 需求池改进条目(source=售后 OR reqType=改进,按 productLine 过滤)
List reqItems = new ArrayList<>();
for (ProductRequirement r : reqRepo.findAll()) {
if (!"改进".equals(r.getReqType())) continue;
if (pl != null && !pl.isBlank() && !pl.equals(r.getProductLine())) continue;
reqItems.add(r);
}
List fbItems = new ArrayList<>();
for (RdAfterSalesFeedback f : feedbacks) {
fbItems.add(new ImprovementItem("售后反馈", f.getId(), f.getCode(), f.getTitle(),
f.getFeedbackType(), f.getSeverity(), f.getStatus(), f.getSubmitter(),
f.getCreatedAt() == null ? null : f.getCreatedAt().toString().substring(0, 10)));
}
List rqItems = new ArrayList<>();
for (ProductRequirement r : reqItems) {
rqItems.add(new ImprovementItem("需求池", r.getId(), r.getCode(), r.getTitle(),
r.getReqType(), r.getPriority(), r.getStatus(), r.getProposer(),
r.getCreatedAt() == null ? null : r.getCreatedAt().toString().substring(0, 10)));
}
return ApiResp.ok(new ImprovementSummary(pl, feedbacks.size(), pending,
reqItems.size(), fbItems, rqItems));
}
// ======================================================================
// 3. 已上市产品清单(供销售部门拿 SKU/产品线列表)
// ======================================================================
public record LaunchedProduct(Long id, String code, String name, String productLine,
String projectType, String launchDate, String dept, int progress) {
}
@GetMapping("/launched-products")
public ApiResp> launchedProducts(
@RequestParam(required = false) String productLine) {
List result = new ArrayList<>();
for (DevProject p : projectRepo.findAll()) {
if (!p.isProductLaunched()) continue;
if (productLine != null && !productLine.isBlank() && !productLine.equals(p.getProductLine())) continue;
result.add(new LaunchedProduct(p.getId(), p.getCode(), p.getName(), p.getProductLine(),
p.getProjectType(), p.getLaunchDate(),
notBlank(p.getDept(), p.getDepartment()), p.getProgress()));
}
return ApiResp.ok(result);
}
// ======================================================================
// 4. 待研发处理的售后改进需求数量(仪表盘快速聚合)
// ======================================================================
public record AftermarketPending(int pendingFeedbacks, int acceptedFeedbacks,
int highSeverityPending, int totalFeedbacks) {
}
@GetMapping("/aftermarket-pending")
public ApiResp aftermarketPending() {
List all = feedbackRepo.findAll();
int pending = 0, accepted = 0, highPending = 0;
for (RdAfterSalesFeedback f : all) {
if ("待处理".equals(f.getStatus())) {
pending++;
if ("高".equals(f.getSeverity())) highPending++;
}
if ("已采纳".equals(f.getStatus())) accepted++;
}
return ApiResp.ok(new AftermarketPending(pending, accepted, highPending, all.size()));
}
// ---------- helpers ----------
private static BigDecimal nz(BigDecimal v) {
return v == null ? BigDecimal.ZERO : v;
}
private static double round2(double v) {
return Math.round(v * 100d) / 100d;
}
private static String notBlank(String v, String dft) {
return v == null || v.isBlank() ? dft : v;
}
}