恢复点(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>
243 lines
11 KiB
Java
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;
|
|
}
|
|
}
|