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

236 lines
10 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 "中";
}
}