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