优化
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
package com.baiyue.finance;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/** Finance 独立服务启动入口。首期只承载人工账单、收款和成本台账。 */
|
||||
@SpringBootApplication
|
||||
public class BaiyueFinanceApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(BaiyueFinanceApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.baiyue.finance.controller;
|
||||
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
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 jakarta.validation.Valid;
|
||||
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/platform/v1/finance")
|
||||
public class PlatformFinanceController {
|
||||
private final FinanceService service;
|
||||
public PlatformFinanceController(FinanceService service) { this.service = service; }
|
||||
|
||||
@GetMapping("/customers/{tenantId}/terms")
|
||||
@PreAuthorize("hasAuthority('finance:customer-terms:view')")
|
||||
@Operation(summary = "查询客户账期")
|
||||
public ApiResponse<Map<String, Object>> terms(@PathVariable long tenantId) { return ApiResponse.success(service.customerTerms(tenantId)); }
|
||||
|
||||
@PutMapping("/customers/{tenantId}/terms")
|
||||
@PreAuthorize("hasAuthority('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')")
|
||||
@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));
|
||||
}
|
||||
|
||||
@PostMapping("/customers/{tenantId}/bills/{billId}/issue")
|
||||
@PreAuthorize("hasAuthority('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));
|
||||
}
|
||||
|
||||
@PostMapping("/customers/{tenantId}/receipts")
|
||||
@PreAuthorize("hasAuthority('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));
|
||||
}
|
||||
|
||||
@PostMapping("/customers/{tenantId}/costs")
|
||||
@PreAuthorize("hasAuthority('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 record TermsRequest(String customerName, Integer creditDays) {}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package com.baiyue.finance.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.support.GeneratedKeyHolder;
|
||||
import org.springframework.jdbc.support.KeyHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.Statement;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** 首期人工财务台账:账单、收款和成本,不承载钱包、发票或复杂核销。 */
|
||||
@Service
|
||||
public class FinanceService {
|
||||
private final JdbcTemplate jdbc;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public FinanceService(JdbcTemplate jdbc, ObjectMapper objectMapper) {
|
||||
this.jdbc = jdbc;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public Map<String, Object> customerTerms(long tenantId) {
|
||||
requireCustomer(tenantId);
|
||||
List<Map<String, Object>> rows = jdbc.queryForList(
|
||||
"SELECT tenant_id, customer_name_snapshot, credit_days, status FROM fin_customer_settlement_profile WHERE tenant_id=?",
|
||||
tenantId);
|
||||
return rows.isEmpty() ? Map.of("tenantId", tenantId, "creditDays", 30, "status", "ACTIVE") : rows.get(0);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Map<String, Object> saveCustomerTerms(long tenantId, long operatorId, String customerName,
|
||||
Integer creditDays, String idempotencyKey) {
|
||||
requireCustomer(tenantId);
|
||||
reserve(tenantId, operatorId, "CUSTOMER_TERMS_SAVE", idempotencyKey);
|
||||
int days = creditDays == null ? 30 : creditDays;
|
||||
if (days < 0 || days > 3650) throw bad("账期必须在0到3650天之间");
|
||||
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);
|
||||
return customerTerms(tenantId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Map<String, Object> createBill(long tenantId, long operatorId, String customerName, BillRequest request,
|
||||
String idempotencyKey) {
|
||||
requireCustomer(tenantId); reserve(tenantId, operatorId, "BILL_CREATE", idempotencyKey);
|
||||
if (request.lines() == null || request.lines().isEmpty()) 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();
|
||||
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.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();
|
||||
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());
|
||||
}
|
||||
return jdbc.queryForMap("SELECT * FROM fin_bill WHERE id=?", billId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Map<String, Object> issueBill(long tenantId, long operatorId, long billId, 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);
|
||||
if (updated != 1) throw bad("账单不存在或已出账");
|
||||
return jdbc.queryForMap("SELECT * FROM fin_bill WHERE id=? AND tenant_id=?", billId, tenantId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
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 (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);
|
||||
BigDecimal outstanding = new BigDecimal(bill.get("outstanding_amount").toString());
|
||||
if ("VOIDED".equals(bill.get("status")) || allocation.amount().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;
|
||||
}, 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);
|
||||
}
|
||||
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 (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());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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); } }
|
||||
|
||||
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) {}
|
||||
public record ReceiptAllocation(long billId, BigDecimal amount) {}
|
||||
public record CostRequest(String costNo, String costType, Long orderId, Long inboundId, Long containerId, Long supplierId, String supplierName, String feeItemCode, String feeItemName, BigDecimal amount, List<String> voucherFileIds, String remark) {}
|
||||
}
|
||||
Reference in New Issue
Block a user