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>
This commit is contained in:
Qiufeng
2026-06-15 19:19:15 +08:00
co-authored by Claude Opus 4.8
commit 5e51dc3f56
10584 changed files with 2501339 additions and 0 deletions
@@ -0,0 +1,235 @@
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.ProductRequirement;
import com.kaidi.oa.domain.RdAfterSalesFeedback;
import com.kaidi.oa.repository.ProductRequirementRepository;
import com.kaidi.oa.repository.RdAfterSalesFeedbackRepository;
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.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
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.Instant;
import java.util.List;
/**
* 创新研发中心 / 产品开发部 · 售后改进需求反馈专属接口(需求模块 9 协同接口 - 售后部)。
*
* <p>补审计缺口"缺独立的售后改进需求反馈接口(仅有需求池内 reqType=改进但无专属售后来源标记)":
* <ul>
* <li>售后部门通过专属端点提交产品故障分析 / 改进建议(固定 source=售后),
* 与普通需求池 /product-requirements 解耦;</li>
* <li>采纳后可一键转入 ProductRequirement 需求池(reqType=改进,source=售后),
* 回填 convertedReqId 形成售后→需求池联动;</li>
* <li>统计端点按 feedbackType / severity 聚合,暴露给研发仪表盘决策支持(模块 10)。</li>
* </ul>
*
* <p>写口受 AuthInterceptor FINANCE_PREFIXES/api/oa/rd-after-sales-feedback)收口,
* 敏感读已登记 SENSITIVE_READ_PREFIXES(见 sharedFileSnippets)。
*/
@RestController
@RequestMapping("/api/oa/rd-after-sales-feedback")
public class RdAfterSalesFeedbackController {
private final RdAfterSalesFeedbackRepository feedbackRepo;
private final ProductRequirementRepository reqRepo;
public RdAfterSalesFeedbackController(RdAfterSalesFeedbackRepository feedbackRepo,
ProductRequirementRepository reqRepo) {
this.feedbackRepo = feedbackRepo;
this.reqRepo = reqRepo;
}
// ---------- CRUD ----------
@GetMapping
public ApiResp<List<RdAfterSalesFeedback>> list(
@RequestParam(required = false) String status,
@RequestParam(required = false) String feedbackType,
@RequestParam(required = false) String severity) {
if (status != null && !status.isBlank()) {
return ApiResp.ok(feedbackRepo.findByStatus(status));
}
if (feedbackType != null && !feedbackType.isBlank()) {
return ApiResp.ok(feedbackRepo.findByFeedbackType(feedbackType));
}
if (severity != null && !severity.isBlank()) {
return ApiResp.ok(feedbackRepo.findBySeverity(severity));
}
return ApiResp.ok(feedbackRepo.findAll());
}
@GetMapping("/{id}")
public ApiResp<RdAfterSalesFeedback> get(@PathVariable Long id) {
return ApiResp.ok(find(id));
}
public record FeedbackRequest(
String title, String feedbackType, String productLine, String productCode,
String description, String submitter, String customerName, String severity) {
}
@PostMapping
public ApiResp<RdAfterSalesFeedback> create(@RequestBody FeedbackRequest req) {
if (req.title() == null || req.title().isBlank()) {
throw new ApiException(400, "反馈标题(title) 不能为空");
}
RdAfterSalesFeedback f = new RdAfterSalesFeedback();
f.setCode("ASF-" + (feedbackRepo.count() + 1));
f.setTitle(req.title());
f.setFeedbackType(blankTo(req.feedbackType(), "改进建议"));
f.setProductLine(req.productLine());
f.setProductCode(req.productCode());
f.setDescription(req.description());
f.setSubmitter(req.submitter());
f.setCustomerName(req.customerName());
f.setSeverity(blankTo(req.severity(), ""));
f.setStatus("待处理");
f.setSource("售后");
f.setCreatedAt(Instant.now());
return ApiResp.ok(feedbackRepo.save(f));
}
@PutMapping("/{id}")
public ApiResp<RdAfterSalesFeedback> update(@PathVariable Long id, @RequestBody FeedbackRequest req) {
RdAfterSalesFeedback f = find(id);
if (req.title() != null && !req.title().isBlank()) f.setTitle(req.title());
if (req.feedbackType() != null && !req.feedbackType().isBlank()) f.setFeedbackType(req.feedbackType());
if (req.productLine() != null) f.setProductLine(req.productLine());
if (req.productCode() != null) f.setProductCode(req.productCode());
if (req.description() != null) f.setDescription(req.description());
if (req.submitter() != null) f.setSubmitter(req.submitter());
if (req.customerName() != null) f.setCustomerName(req.customerName());
if (req.severity() != null && !req.severity().isBlank()) f.setSeverity(req.severity());
return ApiResp.ok(feedbackRepo.save(f));
}
@DeleteMapping("/{id}")
public ApiResp<Void> delete(@PathVariable Long id) {
if (!feedbackRepo.existsById(id)) {
throw new NotFoundException("rdAfterSalesFeedback not found: " + id);
}
feedbackRepo.deleteById(id);
return ApiResp.ok(null);
}
// ---------- 状态机:采纳 / 拒绝 ----------
public record OpinionRequest(String opinion) {
}
/** 研发侧采纳:待处理 → 已采纳。 */
@PostMapping("/{id}/accept")
public ApiResp<RdAfterSalesFeedback> accept(@PathVariable Long id,
@RequestBody(required = false) OpinionRequest req) {
RdAfterSalesFeedback f = find(id);
if (!"待处理".equals(f.getStatus())) {
throw new ApiException(409, "仅待处理的反馈可采纳,当前状态:" + f.getStatus());
}
f.setStatus("已采纳");
f.setRdOpinion(req == null ? null : req.opinion());
return ApiResp.ok(feedbackRepo.save(f));
}
/** 研发侧拒绝:待处理 → 已拒绝。 */
@PostMapping("/{id}/reject")
public ApiResp<RdAfterSalesFeedback> reject(@PathVariable Long id,
@RequestBody(required = false) OpinionRequest req) {
RdAfterSalesFeedback f = find(id);
if (!"待处理".equals(f.getStatus())) {
throw new ApiException(409, "仅待处理的反馈可拒绝,当前状态:" + f.getStatus());
}
f.setStatus("已拒绝");
f.setRdOpinion(req == null ? null : req.opinion());
return ApiResp.ok(feedbackRepo.save(f));
}
/**
* 一键转入需求池:已采纳 → 已转需求池,自动在 ProductRequirement 表建记录
* reqType=改进,source=售后,priority 继承 severity 映射),回填 convertedReqId。
*/
@PostMapping("/{id}/convert-to-req")
@Transactional
public ApiResp<RdAfterSalesFeedback> convertToReq(@PathVariable Long id) {
RdAfterSalesFeedback f = find(id);
if (!"已采纳".equals(f.getStatus())) {
throw new ApiException(409, "仅已采纳的反馈可转入需求池,当前状态:" + f.getStatus());
}
if (f.getConvertedReqId() != null) {
throw new ApiException(409, "该反馈已转需求池(需求编号 REQ-" + f.getConvertedReqId() + "),不可重复转换");
}
ProductRequirement r = new ProductRequirement();
r.setCode("REQ-ASF-" + f.getId());
r.setTitle("[售后改进] " + f.getTitle());
r.setDetail(f.getDescription());
r.setSource("售后");
r.setProposer(f.getSubmitter());
r.setReqType("改进");
r.setProductLine(f.getProductLine());
// 严重程度 → 优先级映射:高→高,中→中,低→低
r.setPriority(mapSeverity(f.getSeverity()));
r.setStatus("待评审");
r.setCreatedAt(Instant.now());
ProductRequirement saved = reqRepo.save(r);
f.setStatus("已转需求池");
f.setConvertedReqId(saved.getId());
feedbackRepo.save(f);
return ApiResp.ok(f);
}
// ---------- 统计聚合(供研发仪表盘)----------
public record FeedbackStats(int total, int pending, int accepted, int converted, int rejected,
int highSeverity, int midSeverity, int lowSeverity) {
}
@GetMapping("/stats")
public ApiResp<FeedbackStats> stats() {
List<RdAfterSalesFeedback> all = feedbackRepo.findAll();
int pending = 0, accepted = 0, converted = 0, rejected = 0;
int hi = 0, mid = 0, lo = 0;
for (RdAfterSalesFeedback f : all) {
switch (blankTo(f.getStatus(), "待处理")) {
case "待处理" -> pending++;
case "已采纳" -> accepted++;
case "已转需求池" -> converted++;
case "已拒绝" -> rejected++;
default -> { }
}
switch (blankTo(f.getSeverity(), "")) {
case "" -> hi++;
case "" -> mid++;
case "" -> lo++;
default -> { }
}
}
return ApiResp.ok(new FeedbackStats(all.size(), pending, accepted, converted, rejected, hi, mid, lo));
}
// ---------- helpers ----------
private RdAfterSalesFeedback find(Long id) {
return feedbackRepo.findById(id)
.orElseThrow(() -> new NotFoundException("rdAfterSalesFeedback not found: " + id));
}
private static String blankTo(String v, String dft) {
return v == null || v.isBlank() ? dft : v;
}
private static String mapSeverity(String severity) {
if ("".equals(severity)) return "";
if ("".equals(severity)) return "";
return "";
}
}