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

238 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.PmtBankEvaluation;
import com.kaidi.oa.domain.PmtBankRelation;
import com.kaidi.oa.repository.PmtBankEvaluationRepository;
import com.kaidi.oa.repository.PmtBankRelationRepository;
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.time.Instant;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 金融机构定期评价管理(金融办·融资管理·机构综合评分与分级)。
* 覆盖 Gap-1:「机构『定期评价』无专属评价记录工作流(当前 rating 字段为手填文本),
* 机构综合评分与分级管理无流程承载」。
* <p>
* 功能:
* 1. 评价记录 CRUD(四维打分→综合评分→自动定级 A/B/C/D)。
* 2. 查询指定机构的评价历史(时序分析)。
* 3. 评价完成后自动回写 PmtBankRelation.rating 字段(保持主档评级最新)。
* 4. 分机构综合评分趋势摘要(用于「分级统计与对比」)。
* <p>
* 写口通过 AuthInterceptor FINANCE_PREFIXES (/api/oa/pmt-bank-evals) 保护,ADMIN/APPROVER 方可录入。
*/
@RestController
@RequestMapping("/api/oa/pmt-bank-evals")
public class PmtBankEvalController {
private final PmtBankEvaluationRepository evalRepo;
private final PmtBankRelationRepository bankRelRepo;
public PmtBankEvalController(PmtBankEvaluationRepository evalRepo,
PmtBankRelationRepository bankRelRepo) {
this.evalRepo = evalRepo;
this.bankRelRepo = bankRelRepo;
}
// ---------- CRUD ----------
@GetMapping
public ApiResp<List<PmtBankEvaluation>> list(
@RequestParam(required = false) Long bankRelId,
@RequestParam(required = false) String status) {
if (bankRelId != null) {
return ApiResp.ok(evalRepo.findByBankRelIdOrderByEvalDateDesc(bankRelId));
}
if (status != null && !status.isBlank()) {
return ApiResp.ok(evalRepo.findByStatus(status));
}
return ApiResp.ok(evalRepo.findAll());
}
@GetMapping("/{id}")
public ApiResp<PmtBankEvaluation> get(@PathVariable Long id) {
return ApiResp.ok(evalRepo.findById(id)
.orElseThrow(() -> new NotFoundException("评价记录不存在: " + id)));
}
public record EvalRequest(
Long bankRelId, String evalPeriod, String evalDate,
Integer scoreApprovalSpeed, Integer scoreServiceQuality,
Integer scoreRateCompetitiveness, Integer scoreCooperation,
String remark, String evaluator, String status) {
}
/**
* 新建评价记录:四维打分,后端自动计算综合评分(均值)和评级(A≥80/B≥60/C≥40/D<40)。
* 评价状态「已完成」时自动回写 PmtBankRelation.rating 字段,更新主档评级。
*/
@PostMapping
@Transactional
public ApiResp<PmtBankEvaluation> create(@RequestBody EvalRequest req) {
if (req.bankRelId() == null) {
throw new ApiException(400, "bankRelId 不能为空,请指定关联的金融机构档案");
}
PmtBankRelation rel = bankRelRepo.findById(req.bankRelId())
.orElseThrow(() -> new NotFoundException("金融机构档案不存在: " + req.bankRelId()));
PmtBankEvaluation ev = new PmtBankEvaluation();
ev.setBankRelId(req.bankRelId());
ev.setInstitutionName(rel.getInstitutionName());
ev.setEvalPeriod(req.evalPeriod() == null ? currentPeriod() : req.evalPeriod());
ev.setEvalDate(req.evalDate() == null ? LocalDate.now().toString() : req.evalDate());
ev.setScoreApprovalSpeed(clampScore(req.scoreApprovalSpeed()));
ev.setScoreServiceQuality(clampScore(req.scoreServiceQuality()));
ev.setScoreRateCompetitiveness(clampScore(req.scoreRateCompetitiveness()));
ev.setScoreCooperation(clampScore(req.scoreCooperation()));
int total = calcTotal(ev);
ev.setTotalScore(total);
ev.setGrade(calcGrade(total));
ev.setRemark(req.remark());
ev.setEvaluator(req.evaluator());
ev.setStatus(req.status() == null || req.status().isBlank() ? "已完成" : req.status());
ev.setCreatedAt(Instant.now());
PmtBankEvaluation saved = evalRepo.save(ev);
// 评价完成后自动回写机构档案评级
if ("已完成".equals(saved.getStatus())) {
rel.setRating(saved.getGrade());
bankRelRepo.save(rel);
}
return ApiResp.ok(saved);
}
@PatchMapping("/{id}")
@Transactional
public ApiResp<PmtBankEvaluation> update(@PathVariable Long id, @RequestBody EvalRequest req) {
PmtBankEvaluation ev = evalRepo.findById(id)
.orElseThrow(() -> new NotFoundException("评价记录不存在: " + id));
if (req.evalPeriod() != null) ev.setEvalPeriod(req.evalPeriod());
if (req.evalDate() != null) ev.setEvalDate(req.evalDate());
if (req.scoreApprovalSpeed() != null) ev.setScoreApprovalSpeed(clampScore(req.scoreApprovalSpeed()));
if (req.scoreServiceQuality() != null) ev.setScoreServiceQuality(clampScore(req.scoreServiceQuality()));
if (req.scoreRateCompetitiveness() != null) ev.setScoreRateCompetitiveness(clampScore(req.scoreRateCompetitiveness()));
if (req.scoreCooperation() != null) ev.setScoreCooperation(clampScore(req.scoreCooperation()));
// 重算评分
int total = calcTotal(ev);
ev.setTotalScore(total);
ev.setGrade(calcGrade(total));
if (req.remark() != null) ev.setRemark(req.remark());
if (req.evaluator() != null) ev.setEvaluator(req.evaluator());
if (req.status() != null && !req.status().isBlank()) ev.setStatus(req.status());
PmtBankEvaluation saved = evalRepo.save(ev);
// 如果评价已完成,同步回写机构档案评级
if ("已完成".equals(saved.getStatus()) && saved.getBankRelId() != null) {
bankRelRepo.findById(saved.getBankRelId()).ifPresent(rel -> {
rel.setRating(saved.getGrade());
bankRelRepo.save(rel);
});
}
return ApiResp.ok(saved);
}
@DeleteMapping("/{id}")
@Transactional
public ApiResp<Void> delete(@PathVariable Long id) {
if (!evalRepo.existsById(id)) {
throw new NotFoundException("评价记录不存在: " + id);
}
evalRepo.deleteById(id);
return ApiResp.ok(null);
}
// ---------- 评分趋势与分级统计 ----------
public record EvalSummaryRow(Long bankRelId, String institutionName,
String currentGrade, int evalCount,
double avgTotalScore, String trend) {
}
/**
* 各机构综合评分摘要(用于分级管理与对比)。
* 输出:每个机构的当前评级、历史评价次数、平均综合评分、
* 趋势(最近2次:上升/下降/持平/仅1次)。
*/
@GetMapping("/summary")
public ApiResp<List<EvalSummaryRow>> summary() {
List<PmtBankRelation> relations = bankRelRepo.findAll();
List<PmtBankEvaluation> allEvals = evalRepo.findAll();
// 按机构分组评价记录
Map<Long, List<PmtBankEvaluation>> byRel = allEvals.stream()
.filter(e -> e.getBankRelId() != null)
.collect(Collectors.groupingBy(PmtBankEvaluation::getBankRelId));
List<EvalSummaryRow> rows = relations.stream().map(rel -> {
List<PmtBankEvaluation> evals = byRel.getOrDefault(rel.getId(), List.of());
// 按日期倒序
List<PmtBankEvaluation> sorted = evals.stream()
.sorted((a, b) -> nvl(b.getEvalDate()).compareTo(nvl(a.getEvalDate())))
.toList();
double avg = sorted.stream()
.mapToInt(e -> e.getTotalScore() == null ? 0 : e.getTotalScore())
.average().orElse(0);
String trend = "无评价记录";
if (sorted.size() == 1) {
trend = "仅1次评价";
} else if (sorted.size() >= 2) {
int latest = sorted.get(0).getTotalScore() == null ? 0 : sorted.get(0).getTotalScore();
int prev = sorted.get(1).getTotalScore() == null ? 0 : sorted.get(1).getTotalScore();
trend = latest > prev ? "上升" : latest < prev ? "下降" : "持平";
}
return new EvalSummaryRow(rel.getId(), rel.getInstitutionName(),
rel.getRating() == null ? "-" : rel.getRating(),
sorted.size(), avg, trend);
}).toList();
return ApiResp.ok(rows);
}
// ---------- helpers ----------
private static int clampScore(Integer v) {
if (v == null) return 0;
return Math.max(0, Math.min(100, v));
}
private static int calcTotal(PmtBankEvaluation ev) {
int s1 = ev.getScoreApprovalSpeed() == null ? 0 : ev.getScoreApprovalSpeed();
int s2 = ev.getScoreServiceQuality() == null ? 0 : ev.getScoreServiceQuality();
int s3 = ev.getScoreRateCompetitiveness() == null ? 0 : ev.getScoreRateCompetitiveness();
int s4 = ev.getScoreCooperation() == null ? 0 : ev.getScoreCooperation();
return (s1 + s2 + s3 + s4) / 4;
}
private static String calcGrade(int total) {
if (total >= 80) return "A";
if (total >= 60) return "B";
if (total >= 40) return "C";
return "D";
}
private static String currentPeriod() {
java.time.YearMonth ym = java.time.YearMonth.now();
return ym.getYear() + "Q" + ((ym.getMonthValue() - 1) / 3 + 1);
}
private static String nvl(String s) {
return s == null ? "" : s;
}
}