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

261 lines
12 KiB
Java

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.LegalCreditInvest;
import com.kaidi.oa.repository.ContractRepository;
import com.kaidi.oa.repository.CustomerCreditRepository;
import com.kaidi.oa.repository.CustomerRepository;
import com.kaidi.oa.repository.LegalCreditInvestRepository;
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.time.Instant;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 法务专项客户信用调查端点(Module 10 协同缺口:
* 法务「客户信用调查」仅有 CustomerCredit 台账,无法务专项信用调查端点)。
*
* <p>本控制器暴露 {@code /api/oa/legal-credit-invest} 路由,补全法务风险部对客户的专项信用调查能力:
* <ul>
* <li>法务专项调查记录 CRUD(独立于市场部授信档案,专注合规/诉讼/失信/调查意见)</li>
* <li>关联 CustomerCredit 授信档案(聚合客户信用全景)</li>
* <li>{@code GET /api/oa/legal-credit-invest/aggregate?customerId=} — 法务视角信用全景:授信档案+调查记录+合同履约</li>
* <li>{@code POST /api/oa/legal-credit-invest/{id}/conclude} — 调查结论(通过/驳回/需进一步调查)</li>
* </ul>
*
* <p>调查报告含客户敏感财务信息,读口已加入 AuthInterceptor SENSITIVE_READ_PREFIXES(见 sharedFileSnippets)。
*/
@RestController
@RequestMapping("/api/oa/legal-credit-invest")
public class LegalCreditInvestController {
private final LegalCreditInvestRepository investRepo;
private final CustomerCreditRepository creditRepo;
private final CustomerRepository customerRepo;
private final ContractRepository contractRepo;
public LegalCreditInvestController(LegalCreditInvestRepository investRepo,
CustomerCreditRepository creditRepo,
CustomerRepository customerRepo,
ContractRepository contractRepo) {
this.investRepo = investRepo;
this.creditRepo = creditRepo;
this.customerRepo = customerRepo;
this.contractRepo = contractRepo;
}
// ---------- 调查记录 CRUD ----------
@GetMapping
public ApiResp<List<LegalCreditInvest>> list(
@RequestParam(required = false) Long customerId,
@RequestParam(required = false) String status,
@RequestParam(required = false) String keyword) {
if (customerId != null) {
return ApiResp.ok(investRepo.findByCustomerId(customerId));
}
if (status != null && !status.isBlank()) {
return ApiResp.ok(investRepo.findByStatus(status));
}
if (keyword != null && !keyword.isBlank()) {
return ApiResp.ok(investRepo.findByCustomerNameContaining(keyword));
}
return ApiResp.ok(investRepo.findAll());
}
@GetMapping("/{id}")
public ApiResp<LegalCreditInvest> get(@PathVariable Long id) {
return ApiResp.ok(load(id));
}
public record InvestRequest(Long customerId, String customerName,
String investigationType, String scope,
String courtRecord, Boolean blacklisted,
String litigationHistory, String creditScore,
String investigator, String targetCompletionDate,
String remark) {
}
@PostMapping
@Transactional
public ApiResp<LegalCreditInvest> create(@RequestBody InvestRequest req) {
if (req.customerId() == null && (req.customerName() == null || req.customerName().isBlank())) {
throw new ApiException(400, "客户ID(customerId)或客户名称(customerName)至少填写一项");
}
LegalCreditInvest inv = new LegalCreditInvest();
String custName = req.customerName();
if (custName == null || custName.isBlank()) {
custName = customerRepo.findById(req.customerId())
.map(c -> c.getName())
.orElse("(客户" + req.customerId() + ")");
}
inv.setCustomerId(req.customerId());
inv.setCustomerName(custName);
inv.setInvestigationType(req.investigationType() == null ? "综合调查" : req.investigationType());
inv.setScope(req.scope());
inv.setCourtRecord(req.courtRecord());
inv.setBlacklisted(Boolean.TRUE.equals(req.blacklisted()));
inv.setLitigationHistory(req.litigationHistory());
inv.setCreditScore(req.creditScore());
inv.setInvestigator(req.investigator());
inv.setTargetCompletionDate(req.targetCompletionDate());
inv.setRemark(req.remark());
inv.setStatus("调查中");
inv.setCreatedAt(Instant.now());
return ApiResp.ok(investRepo.save(inv));
}
@PatchMapping("/{id}")
@Transactional
public ApiResp<LegalCreditInvest> update(@PathVariable Long id, @RequestBody InvestRequest req) {
LegalCreditInvest inv = load(id);
if (req.investigationType() != null) inv.setInvestigationType(req.investigationType());
if (req.scope() != null) inv.setScope(req.scope());
if (req.courtRecord() != null) inv.setCourtRecord(req.courtRecord());
if (req.blacklisted() != null) inv.setBlacklisted(req.blacklisted());
if (req.litigationHistory() != null) inv.setLitigationHistory(req.litigationHistory());
if (req.creditScore() != null) inv.setCreditScore(req.creditScore());
if (req.investigator() != null) inv.setInvestigator(req.investigator());
if (req.targetCompletionDate() != null) inv.setTargetCompletionDate(req.targetCompletionDate());
if (req.remark() != null) inv.setRemark(req.remark());
return ApiResp.ok(investRepo.save(inv));
}
@DeleteMapping("/{id}")
@Transactional
public ApiResp<Void> delete(@PathVariable Long id) {
if (!investRepo.existsById(id)) {
throw new NotFoundException("法务调查记录不存在:" + id);
}
investRepo.deleteById(id);
return ApiResp.ok(null);
}
// ---------- 调查结论 ----------
public record ConcludeRequest(String conclusion, String opinion, String conclusionDate) {
}
/**
* POST /{id}/conclude — 调查结论(通过 / 驳回 / 需进一步调查)。
* 结论为"驳回"时:关联 CustomerCredit 自动追加法务风险警示 remark,供合同审批联动参考。
*/
@PostMapping("/{id}/conclude")
@Transactional
public ApiResp<LegalCreditInvest> conclude(@PathVariable Long id, @RequestBody ConcludeRequest req) {
LegalCreditInvest inv = load(id);
if ("已结案".equals(inv.getStatus())) {
throw new ApiException(409, "该调查已结案,不可重复结案");
}
List<String> validConclusions = List.of("通过", "驳回", "需进一步调查");
if (req.conclusion() == null || !validConclusions.contains(req.conclusion())) {
throw new ApiException(400, "结论(conclusion)必须为:通过/驳回/需进一步调查");
}
inv.setConclusion(req.conclusion());
inv.setConclusionOpinion(req.opinion());
inv.setConclusionDate(req.conclusionDate());
inv.setStatus("已结案");
// 驳回时:若有关联授信档案,追加法务风险警示标记
if ("驳回".equals(req.conclusion()) && inv.getCustomerId() != null) {
creditRepo.findByCustomerId(inv.getCustomerId()).forEach(cc -> {
String existRemark = cc.getRemark() == null ? "" : cc.getRemark();
if (!existRemark.contains("[法务风险警示]")) {
String warn = req.opinion() != null ? req.opinion() : "法务审查驳回";
cc.setRemark(existRemark + " [法务风险警示:" + warn + "]");
creditRepo.save(cc);
}
});
}
return ApiResp.ok(investRepo.save(inv));
}
// ---------- 法务视角信用全景(聚合)----------
/**
* GET /aggregate?customerId=&customerName= — 法务视角客户信用全景:
* 授信档案 + 法务调查记录 + 合同履约概况,统一聚合,无需手动跨控制器取数(补完 PARTIAL 缺口)。
*/
@GetMapping("/aggregate")
public ApiResp<Map<String, Object>> aggregate(
@RequestParam(required = false) Long customerId,
@RequestParam(required = false) String customerName) {
if (customerId == null && (customerName == null || customerName.isBlank())) {
throw new ApiException(400, "customerId 或 customerName 必须提供一项");
}
// 授信档案
var credits = customerId != null
? creditRepo.findByCustomerId(customerId)
: creditRepo.findAll().stream()
.filter(c -> customerName.equals(c.getCustomerName())).toList();
// 法务调查
var invests = customerId != null
? investRepo.findByCustomerId(customerId)
: investRepo.findByCustomerNameContaining(customerName);
// 合同履约概况(按甲方名匹配)
String matchName = customerName;
if ((matchName == null || matchName.isBlank()) && !credits.isEmpty()) {
matchName = credits.get(0).getCustomerName();
}
final String finalName = matchName;
long contractCount = 0;
BigDecimal contractTotal = BigDecimal.ZERO;
BigDecimal paidTotal = BigDecimal.ZERO;
if (finalName != null && !finalName.isBlank()) {
for (var ct : contractRepo.findAll()) {
if (finalName.equals(ct.getPartyA())) {
contractCount++;
contractTotal = Money.add(contractTotal, Money.nz(ct.getAmount()));
paidTotal = Money.add(paidTotal, Money.nz(ct.getPaidAmount()));
}
}
}
boolean hasRisk = invests.stream().anyMatch(i -> "驳回".equals(i.getConclusion()));
boolean blacklisted = invests.stream().anyMatch(LegalCreditInvest::isBlacklisted);
BigDecimal receivable = Money.sub(contractTotal, paidTotal);
if (receivable.compareTo(BigDecimal.ZERO) < 0) {
receivable = BigDecimal.ZERO;
}
Map<String, Object> result = new LinkedHashMap<>();
result.put("customerId", customerId);
result.put("customerName", finalName);
result.put("creditFiles", credits);
result.put("legalInvestigations", invests);
result.put("contractCount", contractCount);
result.put("contractTotal", contractTotal.doubleValue());
result.put("paidTotal", paidTotal.doubleValue());
result.put("receivable", receivable.doubleValue());
result.put("legalRiskFlag", hasRisk);
result.put("blacklisted", blacklisted);
return ApiResp.ok(result);
}
// ---------- helpers ----------
private LegalCreditInvest load(Long id) {
return investRepo.findById(id)
.orElseThrow(() -> new NotFoundException("法务调查记录不存在:" + id));
}
}