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 台账,无法务专项信用调查端点)。 * *

本控制器暴露 {@code /api/oa/legal-credit-invest} 路由,补全法务风险部对客户的专项信用调查能力: *

* *

调查报告含客户敏感财务信息,读口已加入 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( @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 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 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 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 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 conclude(@PathVariable Long id, @RequestBody ConcludeRequest req) { LegalCreditInvest inv = load(id); if ("已结案".equals(inv.getStatus())) { throw new ApiException(409, "该调查已结案,不可重复结案"); } List 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> 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 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)); } }