恢复点(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>
236 lines
10 KiB
Java
236 lines
10 KiB
Java
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 "中";
|
||
}
|
||
}
|