package com.kaidi.oa.web; import com.kaidi.oa.common.ApiException; import com.kaidi.oa.common.ApiResp; import com.kaidi.oa.common.Money; import com.kaidi.oa.common.NotFoundException; import com.kaidi.oa.domain.RdServiceAgency; import com.kaidi.oa.repository.RdServiceAgencyRepository; 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.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; 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.math.BigDecimal; import java.math.RoundingMode; import java.time.Instant; import java.util.ArrayList; import java.util.List; /** * 创新研发中心·申报服务部 — 外部服务机构管理(需求 §6 服务机构管理)。 * * 管理咨询公司、会计师事务所、专利代理机构等合作伙伴,记录合作项目、累计付款、 * 以及多次服务质量评价滚动汇总的综合评分。 * * * 联系人/电话属 PII,读侧收口(SENSITIVE_READ_PREFIXES,与 experts 一致)。 * 金额一律 {@link Money}(BigDecimal)。评分用 BigDecimal 标度 2。 */ @RestController @RequestMapping("/api/oa/rd-service-agencies") public class RdServiceAgencyController { private final RdServiceAgencyRepository repo; public RdServiceAgencyController(RdServiceAgencyRepository repo) { this.repo = repo; } /** 出参视图:机构 + 派生综合评分(avgRating = ratingSum/ratingCount,标度 2)。 */ public record AgencyView(RdServiceAgency agency, double avgRating) { } private AgencyView toView(RdServiceAgency a) { return new AgencyView(a, avgRating(a)); } private static double avgRating(RdServiceAgency a) { int n = a.getRatingCount() == null ? 0 : a.getRatingCount(); if (n <= 0) { return 0d; } return Money.nz(a.getRatingSum()) .divide(BigDecimal.valueOf(n), 2, RoundingMode.HALF_UP) .doubleValue(); } @GetMapping public ApiResp> list(@RequestParam(required = false) String agencyType, @RequestParam(required = false) String status) { List src; if (agencyType != null && !agencyType.isBlank()) { src = repo.findByAgencyType(agencyType); } else if (status != null && !status.isBlank()) { src = repo.findByStatus(status); } else { src = repo.findAll(); } List out = new ArrayList<>(); for (RdServiceAgency a : src) { out.add(toView(a)); } return ApiResp.ok(out); } @GetMapping("/{id}") public ApiResp get(@PathVariable Long id) { RdServiceAgency a = repo.findById(id) .orElseThrow(() -> new NotFoundException("service agency not found: " + id)); return ApiResp.ok(toView(a)); } public record AgencyRequest( String name, String agencyType, String serviceScope, String contact, String phone, Long contractId, String contractNo, String status, String remark) { } @PostMapping public ApiResp create(@RequestBody AgencyRequest req) { if (req.name() == null || req.name().isBlank()) { throw new ApiException(400, "机构名称(name) 不能为空"); } RdServiceAgency a = new RdServiceAgency(); a.setName(req.name().trim()); a.setAgencyType(req.agencyType() == null || req.agencyType().isBlank() ? "咨询公司" : req.agencyType()); a.setServiceScope(req.serviceScope()); a.setContact(req.contact()); a.setPhone(req.phone()); a.setContractId(req.contractId()); a.setContractNo(req.contractNo()); a.setStatus(req.status() == null || req.status().isBlank() ? "合作中" : req.status()); a.setRemark(req.remark()); a.setCooperationCount(0); a.setContractAmount(Money.ZERO); a.setRatingSum(Money.ZERO); a.setRatingCount(0); a.setCreatedAt(Instant.now()); return ApiResp.ok(toView(repo.save(a))); } @PatchMapping("/{id}") public ApiResp update(@PathVariable Long id, @RequestBody AgencyRequest req) { RdServiceAgency a = repo.findById(id) .orElseThrow(() -> new NotFoundException("service agency not found: " + id)); if (req.name() != null && !req.name().isBlank()) a.setName(req.name().trim()); if (req.agencyType() != null && !req.agencyType().isBlank()) a.setAgencyType(req.agencyType()); if (req.serviceScope() != null) a.setServiceScope(req.serviceScope()); if (req.contact() != null) a.setContact(req.contact()); if (req.phone() != null) a.setPhone(req.phone()); if (req.contractId() != null) a.setContractId(req.contractId()); if (req.contractNo() != null) a.setContractNo(req.contractNo()); if (req.status() != null && !req.status().isBlank()) a.setStatus(req.status()); if (req.remark() != null) a.setRemark(req.remark()); // cooperationCount / contractAmount / ratingSum / ratingCount 仅由 evaluate 推进,禁止客户端直写。 return ApiResp.ok(toView(repo.save(a))); } @DeleteMapping("/{id}") public ApiResp delete(@PathVariable Long id) { if (!repo.existsById(id)) { throw new NotFoundException("service agency not found: " + id); } repo.deleteById(id); return ApiResp.ok(null); } public record EvaluateRequest(Double score, Boolean recordCooperation, Double contractAmount, String remark) { } /** * 服务质量评价:本次评分(1-5)累加进 ratingSum/ratingCount,综合评分滚动更新。 * recordCooperation=true 时同步记一次合作(合作数+1、合同额累加 contractAmount)。 */ @PostMapping("/{id}/evaluate") @Transactional public ApiResp evaluate(@PathVariable Long id, @RequestBody EvaluateRequest req) { RdServiceAgency a = repo.findById(id) .orElseThrow(() -> new NotFoundException("service agency not found: " + id)); if (req == null || req.score() == null) { throw new ApiException(400, "评分(score) 不能为空"); } double s = req.score(); if (!Double.isFinite(s) || s < 1 || s > 5) { throw new ApiException(400, "评分必须在 1-5 之间"); } a.setRatingSum(Money.add(a.getRatingSum(), BigDecimal.valueOf(s))); a.setRatingCount((a.getRatingCount() == null ? 0 : a.getRatingCount()) + 1); if (Boolean.TRUE.equals(req.recordCooperation())) { a.setCooperationCount((a.getCooperationCount() == null ? 0 : a.getCooperationCount()) + 1); if (req.contractAmount() != null) { a.setContractAmount(Money.add(a.getContractAmount(), Money.of(req.contractAmount()))); } } if (req.remark() != null && !req.remark().isBlank()) { a.setRemark(req.remark()); } return ApiResp.ok(toView(repo.save(a))); } }