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=改进但无专属售后来源标记)": *

* *

写口受 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 "中"; } }