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

182 lines
7.8 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.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 服务机构管理)。
*
* 管理咨询公司、会计师事务所、专利代理机构等合作伙伴,记录合作项目、累计付款、
* 以及多次服务质量评价滚动汇总的综合评分。
* <ul>
* <li>CRUD:机构主档(类型/服务范围/联系人/电话/状态);</li>
* <li>评价(/{id}/evaluate):本次评分(1-5)累加进 ratingSum/ratingCount
* 综合评分 avgRating 由控制器在出参里派生;可选记一次合作(合作数+1、合同额累加);</li>
* <li>列表出参带派生的 avgRating,支持按机构类型筛选。</li>
* </ul>
*
* 联系人/电话属 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<AgencyView>> list(@RequestParam(required = false) String agencyType,
@RequestParam(required = false) String status) {
List<RdServiceAgency> 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<AgencyView> out = new ArrayList<>();
for (RdServiceAgency a : src) {
out.add(toView(a));
}
return ApiResp.ok(out);
}
@GetMapping("/{id}")
public ApiResp<AgencyView> 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<AgencyView> 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<AgencyView> 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<Void> 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<AgencyView> 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)));
}
}