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 协同接口 - 售后部)。
*
*
补审计缺口"缺独立的售后改进需求反馈接口(仅有需求池内 reqType=改进但无专属售后来源标记)":
*
* - 售后部门通过专属端点提交产品故障分析 / 改进建议(固定 source=售后),
* 与普通需求池 /product-requirements 解耦;
* - 采纳后可一键转入 ProductRequirement 需求池(reqType=改进,source=售后),
* 回填 convertedReqId 形成售后→需求池联动;
* - 统计端点按 feedbackType / severity 聚合,暴露给研发仪表盘决策支持(模块 10)。
*
*
* 写口受 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(
@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 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 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 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 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 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 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 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 stats() {
List 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 "中";
}
}