恢复点(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>
261 lines
12 KiB
Java
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));
|
|
}
|
|
}
|