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.BankReconciliation; import com.kaidi.oa.repository.BankReconciliationRepository; 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.time.LocalDate; import java.util.List; /** * 结算中心·银行对账。一条对账记录把某账户某对账日的银行流水余额与账面余额做差, * 差额由后端自动算并落库;diff 为 0 视为「已对平」,否则「未对平」。 * 银企直连(自动取银行流水)为外部对接项,本控制器先支持手工 / 导入登记与差额自动核算。 * * 写口默认受 AuthInterceptor 的 default-deny(ADMIN/APPROVER) 保护,并需登记进 * FINANCE_PREFIXES + SENSITIVE_READ_PREFIXES(含银行账号/余额,属机密财务读)。 */ @RestController @RequestMapping("/api/oa/reconciliations") public class BankReconciliationController { private final BankReconciliationRepository repo; public BankReconciliationController(BankReconciliationRepository repo) { this.repo = repo; } @GetMapping public ApiResp> list(@RequestParam(required = false) String status, @RequestParam(required = false) String bankAccountNo) { if (status != null && !status.isBlank()) { return ApiResp.ok(repo.findByStatus(status)); } if (bankAccountNo != null && !bankAccountNo.isBlank()) { return ApiResp.ok(repo.findByBankAccountNoOrderByReconcileDateDesc(bankAccountNo)); } return ApiResp.ok(repo.findAll()); } @GetMapping("/{id}") public ApiResp get(@PathVariable Long id) { return ApiResp.ok(repo.findById(id) .orElseThrow(() -> new NotFoundException("reconciliation not found: " + id))); } public record ReconciliationRequest( String code, String bankAccount, String bankAccountNo, String bankName, String reconcileDate, Double bankBalance, Double bookBalance, String remark, String companySubject, String owner) { } @PostMapping public ApiResp create(@RequestBody ReconciliationRequest req) { if (req.bankAccount() == null || req.bankAccount().isBlank()) { throw new ApiException(400, "银行账户(bankAccount) 不能为空"); } BankReconciliation r = new BankReconciliation(); r.setCode(req.code() == null || req.code().isBlank() ? "DZ-" + (repo.count() + 1) : req.code()); r.setBankAccount(req.bankAccount()); r.setBankAccountNo(req.bankAccountNo()); r.setBankName(req.bankName()); r.setReconcileDate(req.reconcileDate() == null || req.reconcileDate().isBlank() ? LocalDate.now().toString() : req.reconcileDate()); r.setBankBalance(Money.of(req.bankBalance())); r.setBookBalance(Money.of(req.bookBalance())); applyDiff(r); r.setRemark(req.remark()); r.setCompanySubject(req.companySubject()); r.setOwner(req.owner()); r.setCreatedAt(Instant.now()); return ApiResp.ok(repo.save(r)); } @PatchMapping("/{id}") public ApiResp update(@PathVariable Long id, @RequestBody ReconciliationRequest req) { BankReconciliation r = repo.findById(id) .orElseThrow(() -> new NotFoundException("reconciliation not found: " + id)); if (req.bankAccount() != null && !req.bankAccount().isBlank()) r.setBankAccount(req.bankAccount()); if (req.bankAccountNo() != null) r.setBankAccountNo(req.bankAccountNo()); if (req.bankName() != null) r.setBankName(req.bankName()); if (req.reconcileDate() != null && !req.reconcileDate().isBlank()) r.setReconcileDate(req.reconcileDate()); if (req.bankBalance() != null) r.setBankBalance(Money.of(req.bankBalance())); if (req.bookBalance() != null) r.setBookBalance(Money.of(req.bookBalance())); if (req.remark() != null) r.setRemark(req.remark()); if (req.companySubject() != null) r.setCompanySubject(req.companySubject()); if (req.owner() != null) r.setOwner(req.owner()); // 任一余额变动都重算差额与对平状态。 applyDiff(r); return ApiResp.ok(repo.save(r)); } @DeleteMapping("/{id}") @Transactional public ApiResp delete(@PathVariable Long id) { if (!repo.existsById(id)) { throw new NotFoundException("reconciliation not found: " + id); } repo.deleteById(id); return ApiResp.ok(null); } /** 差额 = 银行余额 - 账面余额;为 0 即「已对平」,否则「未对平」。 */ private static void applyDiff(BankReconciliation r) { BigDecimal diff = Money.sub(r.getBankBalance(), r.getBookBalance()); r.setDiff(diff); r.setStatus(diff.signum() == 0 ? "已对平" : "未对平"); } }