优化
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
package com.baiyue.finance.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.finance.service.FinanceService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "客户财务只读")
|
||||
@RestController
|
||||
@RequestMapping("/api/client/finance/v1")
|
||||
public class ClientFinanceController {
|
||||
private final FinanceService service;
|
||||
public ClientFinanceController(FinanceService service) { this.service = service; }
|
||||
|
||||
@GetMapping("/bills")
|
||||
@PreAuthorize("hasAuthority('merchant:finance:statement:list')")
|
||||
@Operation(summary = "客户只读查询账单")
|
||||
public ApiResponse<?> bills(@AuthenticationPrincipal AuthenticatedUser user) { return ApiResponse.success(service.listBills(user.tenantId())); }
|
||||
|
||||
@GetMapping("/bills/{billId}")
|
||||
@PreAuthorize("hasAuthority('merchant:finance:statement:view')")
|
||||
@Operation(summary = "客户只读查看账单详情")
|
||||
public ApiResponse<Map<String, Object>> bill(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long billId) { return ApiResponse.success(service.bill(user.tenantId(), billId)); }
|
||||
|
||||
@GetMapping("/receipts")
|
||||
@PreAuthorize("hasAuthority('merchant:finance:statement:view')")
|
||||
@Operation(summary = "客户只读查询收款记录")
|
||||
public ApiResponse<?> receipts(@AuthenticationPrincipal AuthenticatedUser user) { return ApiResponse.success(service.listReceipts(user.tenantId())); }
|
||||
}
|
||||
+69
-19
@@ -15,50 +15,100 @@ import java.util.Map;
|
||||
|
||||
@Tag(name = "人工财务台账")
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/finance")
|
||||
@RequestMapping("/api/platform/finance/v1")
|
||||
public class PlatformFinanceController {
|
||||
private final FinanceService service;
|
||||
public PlatformFinanceController(FinanceService service) { this.service = service; }
|
||||
|
||||
@GetMapping("/customers/{tenantId}/terms")
|
||||
@PreAuthorize("hasAuthority('finance:customer-terms:view')")
|
||||
@PreAuthorize("hasAuthority('platform:finance:customer-terms:view')")
|
||||
@Operation(summary = "查询客户账期")
|
||||
public ApiResponse<Map<String, Object>> terms(@PathVariable long tenantId) { return ApiResponse.success(service.customerTerms(tenantId)); }
|
||||
|
||||
@GetMapping("/bills")
|
||||
@PreAuthorize("hasAuthority('platform:finance:bill:view')")
|
||||
@Operation(summary = "查询平台账单")
|
||||
public ApiResponse<?> bills(@RequestParam long customerId) { return ApiResponse.success(service.listBills(customerId)); }
|
||||
|
||||
@PutMapping("/customers/{tenantId}/terms")
|
||||
@PreAuthorize("hasAuthority('finance:customer-terms:update')")
|
||||
@PreAuthorize("hasAuthority('platform:finance:customer-terms:update')")
|
||||
@BusinessAudit(event = "FINANCE_CUSTOMER_TERMS_UPDATE", resourceType = "CUSTOMER")
|
||||
public ApiResponse<Map<String, Object>> saveTerms(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long tenantId, @RequestHeader("Idempotency-Key") String key, @RequestBody TermsRequest request) {
|
||||
return ApiResponse.success(service.saveCustomerTerms(tenantId, user.userId(), request.customerName(), request.creditDays(), key));
|
||||
}
|
||||
|
||||
@PostMapping("/customers/{tenantId}/bills")
|
||||
@PreAuthorize("hasAuthority('finance:bill:create')")
|
||||
@PostMapping("/bills")
|
||||
@PreAuthorize("hasAuthority('platform:finance:bill:create')")
|
||||
@BusinessAudit(event = "FINANCE_BILL_CREATE", resourceType = "FINANCE_BILL")
|
||||
public ApiResponse<Map<String, Object>> createBill(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long tenantId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody FinanceService.BillRequest request) {
|
||||
return ApiResponse.success(service.createBill(tenantId, user.userId(), request == null ? "客户" + tenantId : request.billNo(), request, key));
|
||||
public ApiResponse<Map<String, Object>> createBill(@AuthenticationPrincipal AuthenticatedUser user, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody CreateBillRequest request) {
|
||||
var lines = request.lines().stream().map(line -> new FinanceService.BillLine("FIRSTLEG_FEE", line.sourceFeeId(), line.sourceFeeVersion(), null, line.orderId(), line.inboundId(), "MANUAL", line.feeItemName(), line.amount())).toList();
|
||||
var internal = new FinanceService.BillRequest(null, 1L, request.billDate(), null, lines);
|
||||
return ApiResponse.success(service.createBill(request.customerId(), user.userId(), "客户" + request.customerId(), internal, key));
|
||||
}
|
||||
|
||||
@PostMapping("/customers/{tenantId}/bills/{billId}/issue")
|
||||
@PreAuthorize("hasAuthority('finance:bill:issue')")
|
||||
@PostMapping("/bills/{billId}/issue")
|
||||
@PreAuthorize("hasAuthority('platform:finance:bill:issue')")
|
||||
@BusinessAudit(event = "FINANCE_BILL_ISSUE", resourceType = "FINANCE_BILL")
|
||||
public ApiResponse<Map<String, Object>> issueBill(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long tenantId, @PathVariable long billId, @RequestHeader("Idempotency-Key") String key) {
|
||||
return ApiResponse.success(service.issueBill(tenantId, user.userId(), billId, key));
|
||||
public ApiResponse<Map<String, Object>> issueBill(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long billId, @RequestHeader("Idempotency-Key") String key, @RequestBody VersionRequest request) {
|
||||
return ApiResponse.success(service.issueBillById(user.userId(), billId, request.expectedVersion(), key));
|
||||
}
|
||||
|
||||
@PostMapping("/customers/{tenantId}/receipts")
|
||||
@PreAuthorize("hasAuthority('finance:receipt:create')")
|
||||
@GetMapping("/bills/{billId}")
|
||||
@PreAuthorize("hasAuthority('platform:finance:bill:view')")
|
||||
public ApiResponse<Map<String, Object>> bill(@PathVariable long billId) { return ApiResponse.success(service.billById(billId)); }
|
||||
|
||||
@GetMapping("/receipts")
|
||||
@PreAuthorize("hasAuthority('platform:finance:receipt:view')")
|
||||
public ApiResponse<?> receipts(@RequestParam(required = false) Long customerId) { return ApiResponse.success(service.listReceipts(customerId)); }
|
||||
|
||||
@PostMapping("/receipts")
|
||||
@PreAuthorize("hasAuthority('platform:finance:receipt:create')")
|
||||
@BusinessAudit(event = "FINANCE_RECEIPT_CREATE", resourceType = "FINANCE_RECEIPT")
|
||||
public ApiResponse<Map<String, Object>> receipt(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long tenantId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody FinanceService.ReceiptRequest request) {
|
||||
return ApiResponse.success(service.createReceipt(tenantId, user.userId(), request, key));
|
||||
public ApiResponse<Map<String, Object>> receipt(@AuthenticationPrincipal AuthenticatedUser user, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ReceiptRequest request) {
|
||||
if (request.amount() == null || request.amount().signum() <= 0) throw new IllegalArgumentException("收款金额必须大于0");
|
||||
if (request.receivedDate() == null) throw new IllegalArgumentException("到账日期不能为空");
|
||||
if (request.allocations() == null || request.allocations().isEmpty()) throw new IllegalArgumentException("收款至少需要分配一张账单");
|
||||
if (request.currency() != null && !"CNY".equals(request.currency())) throw new IllegalArgumentException("首期仅支持CNY");
|
||||
var allocated = request.allocations().stream().map(ReceiptAllocationInput::amount).reduce(java.math.BigDecimal.ZERO, java.math.BigDecimal::add);
|
||||
if (request.amount().compareTo(allocated) != 0) throw new IllegalArgumentException("收款金额必须等于分配金额合计");
|
||||
var allocations = request.allocations().stream().map(a -> new FinanceService.ReceiptAllocation(a.billId(), a.amount())).toList();
|
||||
var internal = new FinanceService.ReceiptRequest(null, 1L, request.receivedDate().atStartOfDay(), request.paymentMethod(), null, request.voucherFileIds(), request.remark(), allocations);
|
||||
return ApiResponse.success(service.createReceipt(request.customerId(), user.userId(), internal, key));
|
||||
}
|
||||
|
||||
@PostMapping("/customers/{tenantId}/costs")
|
||||
@PreAuthorize("hasAuthority('finance:cost:create')")
|
||||
@GetMapping("/cost-entries")
|
||||
@PreAuthorize("hasAuthority('platform:finance:cost:view')")
|
||||
public ApiResponse<?> costs(@RequestParam(required = false) Long customerId) { return ApiResponse.success(service.listCosts(customerId)); }
|
||||
|
||||
@PostMapping("/cost-entries")
|
||||
@PreAuthorize("hasAuthority('platform:finance:cost:create')")
|
||||
@BusinessAudit(event = "FINANCE_COST_CREATE", resourceType = "FINANCE_COST")
|
||||
public ApiResponse<Map<String, Object>> cost(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long tenantId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody FinanceService.CostRequest request) {
|
||||
return ApiResponse.success(service.createCost(tenantId, user.userId(), request, key));
|
||||
public ApiResponse<Map<String, Object>> cost(@AuthenticationPrincipal AuthenticatedUser user, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody CostRequest request) {
|
||||
if (request.currency() != null && !"CNY".equals(request.currency())) throw new IllegalArgumentException("首期仅支持CNY");
|
||||
var internal = new FinanceService.CostRequest(null, request.costType(), request.orderId(), null, request.containerId(), null, null, "MANUAL", request.expenseName(), request.amount(), request.voucherFileIds(), request.remark());
|
||||
return ApiResponse.success(service.createCost(request.customerId(), user.userId(), internal, key));
|
||||
}
|
||||
|
||||
@PutMapping("/bills/{billId}")
|
||||
@PreAuthorize("hasAuthority('platform:finance:bill:update')")
|
||||
public ApiResponse<Map<String,Object>> update(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long billId, @RequestHeader("Idempotency-Key") String key, @RequestBody UpdateBillRequest request) { return ApiResponse.success(service.updateBill(user.userId(), billId, request.customerId(), request.billDate(), request.lines(), request.expectedVersion(), key)); }
|
||||
|
||||
@PostMapping("/bills/{billId}/adjust")
|
||||
@PreAuthorize("hasAuthority('platform:finance:bill:adjust')")
|
||||
public ApiResponse<Map<String,Object>> adjust(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long billId, @RequestHeader("Idempotency-Key") String key, @RequestBody AdjustBillRequest request) { return ApiResponse.success(service.adjustBill(user.userId(), billId, request.lines(), request.expectedVersion(), request.reason(), key)); }
|
||||
|
||||
@PostMapping("/bills/{billId}/void")
|
||||
@PreAuthorize("hasAuthority('platform:finance:bill:void')")
|
||||
public ApiResponse<Map<String,Object>> voidBill(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long billId, @RequestHeader("Idempotency-Key") String key, @RequestBody ReasonRequest request) { return ApiResponse.success(service.voidBill(user.userId(), billId, request.expectedVersion(), request.reason(), key)); }
|
||||
|
||||
public record TermsRequest(String customerName, Integer creditDays) {}
|
||||
public record CreateBillRequest(long customerId, java.time.LocalDate billDate, java.util.List<BillLineInput> lines) {}
|
||||
public record BillLineInput(long sourceFeeId, int sourceFeeVersion, Long orderId, Long inboundId, String feeItemName, java.math.BigDecimal amount) {}
|
||||
public record VersionRequest(int expectedVersion) {}
|
||||
public record ReasonRequest(int expectedVersion, String reason) {}
|
||||
public record UpdateBillRequest(long customerId, java.time.LocalDate billDate, java.util.List<BillLineInput> lines, int expectedVersion) {}
|
||||
public record AdjustBillRequest(int expectedVersion, String reason, java.util.List<BillLineInput> lines) {}
|
||||
public record ReceiptRequest(long customerId, java.time.LocalDate receivedDate, java.math.BigDecimal amount, String currency, String paymentMethod, java.util.List<String> voucherFileIds, String remark, java.util.List<ReceiptAllocationInput> allocations) {}
|
||||
public record ReceiptAllocationInput(long billId, java.math.BigDecimal amount) {}
|
||||
public record CostRequest(long customerId, String costType, Long orderId, Long containerId, String expenseName, java.math.BigDecimal amount, String currency, java.util.List<String> voucherFileIds, String remark) {}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,41 @@ public class FinanceService {
|
||||
return rows.isEmpty() ? Map.of("tenantId", tenantId, "creditDays", 30, "status", "ACTIVE") : rows.get(0);
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> listBills(long tenantId) {
|
||||
requireCustomer(tenantId);
|
||||
return jdbc.queryForList("SELECT id,bill_no,customer_name_snapshot,currency_code,bill_date,due_date,status,total_amount,received_amount,outstanding_amount,version FROM fin_bill WHERE tenant_id=? ORDER BY bill_date DESC,id DESC", tenantId);
|
||||
}
|
||||
|
||||
public Map<String, Object> bill(long tenantId, long billId) {
|
||||
requireCustomer(tenantId);
|
||||
Map<String, Object> result = jdbc.queryForMap("SELECT * FROM fin_bill WHERE tenant_id=? AND id=?", tenantId, billId);
|
||||
result.put("lines", jdbc.queryForList("SELECT * FROM fin_bill_line WHERE tenant_id=? AND bill_id=? ORDER BY id", tenantId, billId));
|
||||
return result;
|
||||
}
|
||||
|
||||
public Map<String, Object> billById(long billId) {
|
||||
Long tenantId = jdbc.queryForObject("SELECT tenant_id FROM fin_bill WHERE id=?", Long.class, billId);
|
||||
if (tenantId == null) throw bad("账单不存在");
|
||||
return bill(tenantId, billId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Map<String, Object> issueBillById(long operatorId, long billId, int expectedVersion, String idempotencyKey) {
|
||||
Long tenantId = jdbc.queryForObject("SELECT tenant_id FROM fin_bill WHERE id=?", Long.class, billId);
|
||||
if (tenantId == null) throw bad("账单不存在");
|
||||
return issueBill(tenantId, operatorId, billId, expectedVersion, idempotencyKey);
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> listReceipts(Long tenantId) {
|
||||
String sql = "SELECT r.id,r.tenant_id,r.receipt_no,r.received_amount,r.received_at,r.payment_method,r.voucher_no,r.voucher_file_ids,r.status,rb.bill_id,rb.applied_amount FROM fin_receipt r LEFT JOIN fin_receipt_bill rb ON rb.receipt_id=r.id";
|
||||
return tenantId == null ? jdbc.queryForList(sql + " ORDER BY r.received_at DESC,r.id DESC") : jdbc.queryForList(sql + " WHERE r.tenant_id=? ORDER BY r.received_at DESC,r.id DESC", tenantId);
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> listCosts(Long tenantId) {
|
||||
String sql = "SELECT * FROM fin_cost";
|
||||
return tenantId == null ? jdbc.queryForList(sql + " ORDER BY created_at DESC,id DESC") : jdbc.queryForList(sql + " WHERE tenant_id=? ORDER BY created_at DESC,id DESC", tenantId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Map<String, Object> saveCustomerTerms(long tenantId, long operatorId, String customerName,
|
||||
Integer creditDays, String idempotencyKey) {
|
||||
@@ -48,6 +83,7 @@ public class FinanceService {
|
||||
jdbc.update("INSERT INTO fin_customer_settlement_profile(tenant_id,customer_name_snapshot,credit_days,created_by,updated_by) VALUES(?,?,?,?,?) "
|
||||
+ "ON DUPLICATE KEY UPDATE customer_name_snapshot=VALUES(customer_name_snapshot), credit_days=VALUES(credit_days), updated_by=VALUES(updated_by), version=version+1",
|
||||
tenantId, StringUtils.hasText(customerName) ? customerName : "客户" + tenantId, days, operatorId, operatorId);
|
||||
complete(tenantId, operatorId, "CUSTOMER_TERMS_SAVE", idempotencyKey, tenantId);
|
||||
return customerTerms(tenantId);
|
||||
}
|
||||
|
||||
@@ -56,31 +92,37 @@ public class FinanceService {
|
||||
String idempotencyKey) {
|
||||
requireCustomer(tenantId); reserve(tenantId, operatorId, "BILL_CREATE", idempotencyKey);
|
||||
if (request.lines() == null || request.lines().isEmpty()) throw bad("账单至少需要一条费用明细");
|
||||
request.lines().forEach(line -> { if (line.sourceId() <= 0 || line.sourceVersion() <= 0) throw bad("费用来源版本无效"); if (line.amount() == null || line.amount().signum() < 0) throw bad("费用金额不能为负数"); });
|
||||
BigDecimal total = request.lines().stream().map(BillLine::amount).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
if (total.signum() < 0) throw bad("账单金额不能为负数");
|
||||
int days = ((Number) customerTerms(tenantId).get("credit_days")).intValue();
|
||||
Object credit = customerTerms(tenantId).get("credit_days");
|
||||
if (credit == null) credit = customerTerms(tenantId).get("creditDays");
|
||||
int days = credit == null ? 30 : ((Number) credit).intValue();
|
||||
LocalDate billDate = request.billDate() == null ? LocalDate.now() : request.billDate();
|
||||
KeyHolder holder = new GeneratedKeyHolder();
|
||||
jdbc.update(connection -> {
|
||||
PreparedStatement ps = connection.prepareStatement("INSERT INTO fin_bill(tenant_id,bill_no,subject_id,customer_name_snapshot,currency_code,bill_date,due_date,total_amount,outstanding_amount,remark,created_by,updated_by) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)", Statement.RETURN_GENERATED_KEYS);
|
||||
ps.setLong(1, tenantId); ps.setString(2, request.billNo()); ps.setLong(3, request.subjectId()); ps.setString(4, customerName);
|
||||
ps.setLong(1, tenantId); ps.setString(2, temporaryNo("BILL")); ps.setLong(3, request.subjectId()); ps.setString(4, customerName);
|
||||
ps.setString(5, "CNY"); ps.setObject(6, billDate); ps.setObject(7, billDate.plusDays(days)); ps.setBigDecimal(8, total); ps.setBigDecimal(9, total);
|
||||
ps.setString(10, request.remark()); ps.setLong(11, operatorId); ps.setLong(12, operatorId); return ps;
|
||||
}, holder);
|
||||
long billId = holder.getKey().longValue();
|
||||
jdbc.update("UPDATE fin_bill SET bill_no=? WHERE id=?", stableNo("BILL", billId), billId);
|
||||
for (BillLine line : request.lines()) {
|
||||
jdbc.update("INSERT INTO fin_bill_line(tenant_id,bill_id,source_type,source_id,source_version,source_no,order_id,inbound_id,fee_item_code,fee_item_name,amount) VALUES(?,?,?,?,?,?,?,?,?,?,?)",
|
||||
tenantId, billId, line.sourceType(), line.sourceId(), line.sourceVersion(), line.sourceNo(), line.orderId(), line.inboundId(), line.feeItemCode(), line.feeItemName(), line.amount());
|
||||
}
|
||||
complete(tenantId, operatorId, "BILL_CREATE", idempotencyKey, billId);
|
||||
return jdbc.queryForMap("SELECT * FROM fin_bill WHERE id=?", billId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Map<String, Object> issueBill(long tenantId, long operatorId, long billId, String idempotencyKey) {
|
||||
public Map<String, Object> issueBill(long tenantId, long operatorId, long billId, int expectedVersion, String idempotencyKey) {
|
||||
requireCustomer(tenantId); reserve(tenantId, operatorId, "BILL_ISSUE", idempotencyKey);
|
||||
int updated = jdbc.update("UPDATE fin_bill SET status='ISSUED', issued_at=?, issued_by=?, updated_by=?, version=version+1 WHERE id=? AND tenant_id=? AND status='DRAFT'",
|
||||
LocalDateTime.now(), operatorId, operatorId, billId, tenantId);
|
||||
int updated = jdbc.update("UPDATE fin_bill SET status='ISSUED', issued_at=?, issued_by=?, updated_by=?, version=version+1 WHERE id=? AND tenant_id=? AND status='DRAFT' AND version=?",
|
||||
LocalDateTime.now(), operatorId, operatorId, billId, tenantId, expectedVersion);
|
||||
if (updated != 1) throw bad("账单不存在或已出账");
|
||||
complete(tenantId, operatorId, "BILL_ISSUE", idempotencyKey, billId);
|
||||
return jdbc.queryForMap("SELECT * FROM fin_bill WHERE id=? AND tenant_id=?", billId, tenantId);
|
||||
}
|
||||
|
||||
@@ -88,47 +130,96 @@ public class FinanceService {
|
||||
public Map<String, Object> createReceipt(long tenantId, long operatorId, ReceiptRequest request, String idempotencyKey) {
|
||||
requireCustomer(tenantId); reserve(tenantId, operatorId, "RECEIPT_CREATE", idempotencyKey);
|
||||
if (request.allocations() == null || request.allocations().isEmpty()) throw bad("收款至少需要分配一张账单");
|
||||
BigDecimal total = request.allocations().stream().map(ReceiptAllocation::amount).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
if (request.allocations().stream().anyMatch(a -> a == null || a.amount() == null || a.amount().signum() <= 0)) {
|
||||
throw bad("分配金额必须大于0");
|
||||
}
|
||||
Map<Long, BigDecimal> byBill = request.allocations().stream().collect(
|
||||
java.util.stream.Collectors.toMap(ReceiptAllocation::billId, ReceiptAllocation::amount, BigDecimal::add));
|
||||
BigDecimal total = byBill.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
if (total.signum() <= 0) throw bad("收款金额必须大于0");
|
||||
for (ReceiptAllocation allocation : request.allocations()) {
|
||||
Map<String, Object> bill = jdbc.queryForMap("SELECT outstanding_amount,status FROM fin_bill WHERE id=? AND tenant_id=? FOR UPDATE", allocation.billId(), tenantId);
|
||||
for (var entry : byBill.entrySet()) {
|
||||
Map<String, Object> bill = jdbc.queryForMap("SELECT outstanding_amount,status FROM fin_bill WHERE id=? AND tenant_id=? FOR UPDATE", entry.getKey(), tenantId);
|
||||
BigDecimal outstanding = new BigDecimal(bill.get("outstanding_amount").toString());
|
||||
if ("VOIDED".equals(bill.get("status")) || allocation.amount().compareTo(outstanding) > 0) throw bad("收款金额超过账单未收余额");
|
||||
if ("DRAFT".equals(bill.get("status")) || "VOIDED".equals(bill.get("status"))
|
||||
|| entry.getValue().compareTo(outstanding) > 0) {
|
||||
throw bad("只能分配已出账且未作废账单,且金额不得超过未收余额");
|
||||
}
|
||||
}
|
||||
KeyHolder holder = new GeneratedKeyHolder();
|
||||
jdbc.update(connection -> {
|
||||
PreparedStatement ps = connection.prepareStatement("INSERT INTO fin_receipt(tenant_id,receipt_no,subject_id,received_amount,received_at,payment_method,voucher_no,voucher_file_ids,remark,created_by,updated_by) VALUES(?,?,?,?,?,?,?,?,?,?,?)", Statement.RETURN_GENERATED_KEYS);
|
||||
ps.setLong(1, tenantId); ps.setString(2, request.receiptNo()); ps.setLong(3, request.subjectId()); ps.setBigDecimal(4, total); ps.setObject(5, request.receivedAt() == null ? LocalDateTime.now() : request.receivedAt()); ps.setString(6, request.paymentMethod()); ps.setString(7, request.voucherNo()); ps.setString(8, json(request.voucherFileIds())); ps.setString(9, request.remark()); ps.setLong(10, operatorId); ps.setLong(11, operatorId); return ps;
|
||||
ps.setLong(1, tenantId); ps.setString(2, temporaryNo("RCPT")); ps.setLong(3, request.subjectId()); ps.setBigDecimal(4, total); ps.setObject(5, request.receivedAt() == null ? LocalDateTime.now() : request.receivedAt()); ps.setString(6, request.paymentMethod()); ps.setString(7, request.voucherNo()); ps.setString(8, json(request.voucherFileIds())); ps.setString(9, request.remark()); ps.setLong(10, operatorId); ps.setLong(11, operatorId); return ps;
|
||||
}, holder);
|
||||
long receiptId = holder.getKey().longValue();
|
||||
for (ReceiptAllocation allocation : request.allocations()) {
|
||||
jdbc.update("INSERT INTO fin_receipt_bill(tenant_id,receipt_id,bill_id,applied_amount,created_by) VALUES(?,?,?,?,?)", tenantId, receiptId, allocation.billId(), allocation.amount(), operatorId);
|
||||
jdbc.update("UPDATE fin_bill SET received_amount=received_amount+?, outstanding_amount=outstanding_amount-?, status=CASE WHEN outstanding_amount-?=0 THEN 'PAID' ELSE 'PARTIALLY_PAID' END, updated_by=?, version=version+1 WHERE id=? AND tenant_id=?", allocation.amount(), allocation.amount(), allocation.amount(), operatorId, allocation.billId(), tenantId);
|
||||
jdbc.update("UPDATE fin_receipt SET receipt_no=? WHERE id=?", stableNo("RCPT", receiptId), receiptId);
|
||||
for (var entry : byBill.entrySet()) {
|
||||
BigDecimal applied = entry.getValue();
|
||||
jdbc.update("INSERT INTO fin_receipt_bill(tenant_id,receipt_id,bill_id,applied_amount,created_by) VALUES(?,?,?,?,?)", tenantId, receiptId, entry.getKey(), applied, operatorId);
|
||||
jdbc.update("UPDATE fin_bill SET received_amount=received_amount+?, outstanding_amount=outstanding_amount-?, status=CASE WHEN outstanding_amount-?=0 THEN 'PAID' ELSE 'PARTIALLY_PAID' END, updated_by=?, version=version+1 WHERE id=? AND tenant_id=?", applied, applied, applied, operatorId, entry.getKey(), tenantId);
|
||||
}
|
||||
complete(tenantId, operatorId, "RECEIPT_CREATE", idempotencyKey, receiptId);
|
||||
return jdbc.queryForMap("SELECT * FROM fin_receipt WHERE id=?", receiptId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Map<String, Object> createCost(long tenantId, long operatorId, CostRequest request, String idempotencyKey) {
|
||||
requireCustomer(tenantId); reserve(tenantId, operatorId, "COST_CREATE", idempotencyKey);
|
||||
if (!"TICKET".equals(request.costType()) && !"CONTAINER".equals(request.costType())) throw bad("成本类型只能是TICKET或CONTAINER");
|
||||
if (!"SINGLE_ORDER".equals(request.costType()) && !"CONTAINER_SHARED".equals(request.costType())) throw bad("成本类型只能是SINGLE_ORDER或CONTAINER_SHARED");
|
||||
if (request.amount() == null || request.amount().signum() <= 0) throw bad("成本金额必须大于0");
|
||||
jdbc.update("INSERT INTO fin_cost(tenant_id,cost_no,cost_type,order_id,inbound_id,container_id,supplier_id,supplier_name_snapshot,fee_item_code,fee_item_name,cost_amount,voucher_file_ids,remark,created_by,updated_by) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
tenantId, request.costNo(), request.costType(), request.orderId(), request.inboundId(), request.containerId(), request.supplierId(), request.supplierName(), request.feeItemCode(), request.feeItemName(), request.amount(), json(request.voucherFileIds()), request.remark(), operatorId, operatorId);
|
||||
return jdbc.queryForMap("SELECT * FROM fin_cost WHERE tenant_id=? AND cost_no=?", tenantId, request.costNo());
|
||||
KeyHolder holder = new GeneratedKeyHolder();
|
||||
jdbc.update(connection -> { PreparedStatement ps = connection.prepareStatement("INSERT INTO fin_cost(tenant_id,cost_no,cost_type,order_id,inbound_id,container_id,supplier_id,supplier_name_snapshot,fee_item_code,fee_item_name,cost_amount,voucher_file_ids,remark,created_by,updated_by) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", Statement.RETURN_GENERATED_KEYS);
|
||||
ps.setLong(1, tenantId); ps.setString(2, temporaryNo("COST")); ps.setString(3, request.costType()); ps.setObject(4, request.orderId()); ps.setObject(5, request.inboundId()); ps.setObject(6, request.containerId()); ps.setObject(7, request.supplierId()); ps.setString(8, request.supplierName()); ps.setString(9, request.feeItemCode()); ps.setString(10, request.feeItemName()); ps.setBigDecimal(11, request.amount()); ps.setString(12, json(request.voucherFileIds())); ps.setString(13, request.remark()); ps.setLong(14, operatorId); ps.setLong(15, operatorId); return ps; }, holder);
|
||||
long costId = holder.getKey().longValue();
|
||||
jdbc.update("UPDATE fin_cost SET cost_no=? WHERE id=?", stableNo("COST", costId), costId);
|
||||
complete(tenantId, operatorId, "COST_CREATE", idempotencyKey, costId);
|
||||
return jdbc.queryForMap("SELECT * FROM fin_cost WHERE id=?", costId);
|
||||
}
|
||||
|
||||
private void reserve(long tenantId, long operatorId, String operation, String key) {
|
||||
if (!StringUtils.hasText(key) || key.length() > 128) throw bad("必须提供有效的 Idempotency-Key");
|
||||
Integer count = jdbc.queryForObject("SELECT COUNT(*) FROM fin_idempotency_record WHERE tenant_id=? AND operator_id=? AND operation=? AND idempotency_key=?", Integer.class, tenantId, operatorId, operation, key);
|
||||
if (count != null && count > 0) throw new BizException(ResultCode.CONFLICT, "相同请求已提交");
|
||||
jdbc.update("INSERT INTO fin_idempotency_record(tenant_id,operator_id,operation,idempotency_key,request_hash,status) VALUES(?,?,?,?,SHA2(?,256),'SUCCEEDED')", tenantId, operatorId, operation, key, key);
|
||||
jdbc.update("INSERT INTO fin_idempotency_record(tenant_id,operator_id,operation,idempotency_key,request_hash,status) VALUES(?,?,?,?,SHA2(?,256),'PROCESSING')", tenantId, operatorId, operation, key, key);
|
||||
}
|
||||
|
||||
private void complete(long tenantId, long operatorId, String operation, String key, long resourceId) {
|
||||
jdbc.update("UPDATE fin_idempotency_record SET status='SUCCEEDED',result_resource_id=? WHERE tenant_id=? AND operator_id=? AND operation=? AND idempotency_key=? AND status='PROCESSING'", resourceId, tenantId, operatorId, operation, key);
|
||||
}
|
||||
private static String temporaryNo(String prefix) { return prefix + "-PENDING-" + java.util.UUID.randomUUID(); }
|
||||
private static String stableNo(String prefix, long id) { return prefix + "-" + String.format(java.util.Locale.ROOT, "%020d", id); }
|
||||
|
||||
private static void requireCustomer(long tenantId) { if (tenantId <= 0) throw bad("客户范围无效"); }
|
||||
private static BizException bad(String message) { return new BizException(ResultCode.BAD_REQUEST, message); }
|
||||
private String json(Object value) { try { return objectMapper.writeValueAsString(value == null ? List.of() : value); } catch (JsonProcessingException e) { throw new IllegalArgumentException("凭证字段格式错误", e); } }
|
||||
|
||||
@Transactional
|
||||
public Map<String,Object> updateBill(long operatorId, long billId, long requestedCustomerId, LocalDate billDate, List<com.baiyue.finance.controller.PlatformFinanceController.BillLineInput> lines, int expectedVersion, String key) {
|
||||
Long tenantId = billTenant(billId); if (tenantId != requestedCustomerId) throw bad("账单不属于指定客户"); reserve(tenantId, operatorId, "BILL_UPDATE", key);
|
||||
replaceDraftLines(tenantId, billId, lines);
|
||||
BigDecimal total = lineTotal(lines);
|
||||
LocalDate effectiveDate = billDate == null ? LocalDate.now() : billDate;
|
||||
Object credit = customerTerms(tenantId).get("credit_days");
|
||||
if (credit == null) credit = customerTerms(tenantId).get("creditDays");
|
||||
LocalDate dueDate = effectiveDate.plusDays(credit == null ? 30 : ((Number) credit).intValue());
|
||||
int changed = jdbc.update("UPDATE fin_bill SET bill_date=?,due_date=?,version=version+1,updated_by=? WHERE id=? AND tenant_id=? AND status='DRAFT' AND version=?", effectiveDate,dueDate,operatorId,billId,tenantId,expectedVersion);
|
||||
if (changed != 1) throw bad("仅草稿账单可修改且版本必须匹配");
|
||||
jdbc.update("UPDATE fin_bill SET total_amount=?, outstanding_amount=? WHERE id=? AND tenant_id=?", total, total, billId, tenantId);
|
||||
complete(tenantId, operatorId, "BILL_UPDATE", key, billId);
|
||||
return bill(tenantId,billId);
|
||||
}
|
||||
@Transactional
|
||||
public Map<String,Object> adjustBill(long operatorId,long billId,List<com.baiyue.finance.controller.PlatformFinanceController.BillLineInput> lines,int expectedVersion,String reason,String key){ Long tenantId=billTenant(billId); reserve(tenantId,operatorId,"BILL_ADJUST",key); if (lines == null || lines.isEmpty()) throw bad("调整后至少需要一条费用明细"); Map<String,Object> current = bill(tenantId,billId); snapshot(tenantId,billId,expectedVersion,reason,current,operatorId); BigDecimal total = lineTotal(lines); BigDecimal received = new BigDecimal(current.get("received_amount").toString()); if (total.compareTo(received) < 0) throw bad("调整金额不能低于已收金额"); BigDecimal outstanding=total.subtract(received); String status=outstanding.signum()==0 ? "PAID" : (received.signum()==0 ? "ISSUED" : "PARTIALLY_PAID"); int c=jdbc.update("UPDATE fin_bill SET remark=?,total_amount=?,outstanding_amount=?,status=?,version=version+1,updated_by=? WHERE id=? AND tenant_id=? AND status IN ('ISSUED','PARTIALLY_PAID','PAID') AND version=?",reason,total,outstanding,status,operatorId,billId,tenantId,expectedVersion); if(c!=1) throw bad("账单状态或版本不允许调整"); jdbc.update("DELETE FROM fin_bill_line WHERE tenant_id=? AND bill_id=?",tenantId,billId); insertLines(tenantId,billId,lines); complete(tenantId,operatorId,"BILL_ADJUST",key,billId); return bill(tenantId,billId); }
|
||||
@Transactional
|
||||
public Map<String,Object> voidBill(long operatorId,long billId,int expectedVersion,String reason,String key){ Long tenantId=billTenant(billId); reserve(tenantId,operatorId,"BILL_VOID",key); if (!StringUtils.hasText(reason)) throw bad("作废原因不能为空"); int c=jdbc.update("UPDATE fin_bill SET status='VOIDED',voided_at=?,voided_by=?,void_reason=?,updated_by=?,version=version+1 WHERE id=? AND tenant_id=? AND status IN ('DRAFT','ISSUED') AND received_amount=0 AND version=?",LocalDateTime.now(),operatorId,reason.trim(),operatorId,billId,tenantId,expectedVersion); if(c!=1) throw bad("已有收款、账单状态或版本不允许作废"); complete(tenantId,operatorId,"BILL_VOID",key,billId); return bill(tenantId,billId); }
|
||||
private long billTenant(long billId){ Long t=jdbc.queryForObject("SELECT tenant_id FROM fin_bill WHERE id=?",Long.class,billId); if(t==null) throw bad("账单不存在"); return t; }
|
||||
private void snapshot(long tenantId,long billId,int version,String reason,Map<String,Object> bill,long operatorId){
|
||||
var lines=jdbc.queryForList("SELECT * FROM fin_bill_line WHERE tenant_id=? AND bill_id=? ORDER BY id",tenantId,billId);
|
||||
jdbc.update("INSERT INTO fin_bill_revision(tenant_id,bill_id,version,reason,bill_snapshot,line_snapshot,created_by) VALUES(?,?,?,?,?,?,?)",tenantId,billId,version,reason,json(bill),json(lines),operatorId);
|
||||
}
|
||||
private BigDecimal lineTotal(List<com.baiyue.finance.controller.PlatformFinanceController.BillLineInput> lines) { if (lines == null || lines.isEmpty()) throw bad("账单至少需要一条费用明细"); return lines.stream().map(l -> { if (l.amount() == null || l.amount().signum() < 0) throw bad("费用金额不能为负数"); return l.amount(); }).reduce(BigDecimal.ZERO, BigDecimal::add); }
|
||||
private void replaceDraftLines(long tenantId, long billId, List<com.baiyue.finance.controller.PlatformFinanceController.BillLineInput> lines) { lineTotal(lines); Map<String,Object> current=bill(tenantId,billId); if (!"DRAFT".equals(current.get("status"))) throw bad("仅草稿账单可修改"); jdbc.update("DELETE FROM fin_bill_line WHERE tenant_id=? AND bill_id=?",tenantId,billId); insertLines(tenantId,billId,lines); }
|
||||
private void insertLines(long tenantId,long billId,List<com.baiyue.finance.controller.PlatformFinanceController.BillLineInput> lines) { for (var line: lines) jdbc.update("INSERT INTO fin_bill_line(tenant_id,bill_id,source_type,source_id,source_version,order_id,inbound_id,fee_item_code,fee_item_name,amount) VALUES(?,?,?,?,?,?,?,?,?,?)",tenantId,billId,"FIRSTLEG_FEE",line.sourceFeeId(),line.sourceFeeVersion(),line.orderId(),line.inboundId(),"MANUAL",line.feeItemName(),line.amount()); }
|
||||
|
||||
public record BillRequest(String billNo, long subjectId, LocalDate billDate, String remark, List<BillLine> lines) {}
|
||||
public record BillLine(String sourceType, long sourceId, int sourceVersion, String sourceNo, Long orderId, Long inboundId, String feeItemCode, String feeItemName, BigDecimal amount) {}
|
||||
public record ReceiptRequest(String receiptNo, long subjectId, LocalDateTime receivedAt, String paymentMethod, String voucherNo, List<String> voucherFileIds, String remark, List<ReceiptAllocation> allocations) {}
|
||||
|
||||
Reference in New Issue
Block a user