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

243 lines
11 KiB
Java

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)。
*
* <p>补审计缺口"DevProject 500使得跨部门联动依赖立项数据的接口运行时断链":
* <ul>
* <li>GET /rd-dev-cross-dept/{devProjectId}/launch-push —— 产品上市推送聚合:拉取立项
* 基本信息 + 已上市标记 + WBS 完成情况 + 预算执行,供销售/市场部门消费;</li>
* <li>GET /rd-dev-cross-dept/{devProjectId}/improvement-summary —— 产品改进反馈汇总:
* 聚合与本立项 productLine 关联的售后改进反馈(来源 source=售后)及需求池改进条目,
* 供研发决策;</li>
* <li>GET /rd-dev-cross-dept/launched-products —— 已上市产品清单(供销售部门拿 SKU);</li>
* <li>GET /rd-dev-cross-dept/aftermarket-pending —— 待研发处理的售后改进需求数量(供仪表盘),
* 补充"销售/市场上市推送、售后产品改进反馈"跨部门链路。</li>
* </ul>
*
* <p>读口聚合跨部门财务数据,已登记进 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> launchPush(@PathVariable Long devProjectId) {
DevProject p = projectRepo.findById(devProjectId)
.orElseThrow(() -> new NotFoundException("研发立项不存在:" + devProjectId));
// WBS 汇总
List<DevWbsTask> 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<DevProjectBudget> 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<ImprovementItem> feedbacks,
List<ImprovementItem> reqPoolItems) {
}
@GetMapping("/{devProjectId}/improvement-summary")
public ApiResp<ImprovementSummary> improvementSummary(@PathVariable Long devProjectId) {
DevProject p = projectRepo.findById(devProjectId)
.orElseThrow(() -> new NotFoundException("研发立项不存在:" + devProjectId));
String pl = p.getProductLine();
// 售后改进反馈
List<RdAfterSalesFeedback> 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<ProductRequirement> 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<ImprovementItem> 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<ImprovementItem> 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<List<LaunchedProduct>> launchedProducts(
@RequestParam(required = false) String productLine) {
List<LaunchedProduct> 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> aftermarketPending() {
List<RdAfterSalesFeedback> 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;
}
}