This commit is contained in:
2026-08-13 11:57:33 +08:00
parent 5a48992d78
commit 16dc03a2e2
102 changed files with 3656 additions and 120 deletions
@@ -27,7 +27,7 @@ public class FinancePhase1OperationController {
public FinancePhase1OperationController(FinancePhase1OperationService service) { this.service = service; }
@GetMapping("/accounts")
@PreAuthorize("hasAuthority('platform:finance:account:view')")
@PreAuthorize("hasAnyAuthority('platform:finance:customer-account:list','platform:finance:account:view')")
public Object listDerivedCustomerAccounts(@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int pageSize,
@RequestParam(required = false) String keyword,
@@ -36,7 +36,7 @@ public class FinancePhase1OperationController {
}
@GetMapping("/accounts/{customerId}/ledger")
@PreAuthorize("hasAuthority('platform:finance:account:view')")
@PreAuthorize("hasAnyAuthority('platform:finance:customer-account:list','platform:finance:account:view')")
public Object listDerivedCustomerAccountLedger(@PathVariable long customerId,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int pageSize,
@@ -47,7 +47,7 @@ public class FinancePhase1OperationController {
}
@GetMapping("/fee-sources/{feeSourceId}")
@PreAuthorize("hasAuthority('platform:finance:fee-source:view')")
@PreAuthorize("hasAnyAuthority('platform:finance:customer-arap:list','platform:finance:fee-source:view')")
public ApiResponse<?> getFinanceFeeSource(@PathVariable long feeSourceId) {
return FinanceResponses.success(service.getFinanceFeeSource(feeSourceId));
}
@@ -8,6 +8,7 @@ import com.baiyue.finance.dto.FinanceSettlementDtos.SettlementDetail;
import com.baiyue.finance.dto.FinanceSettlementDtos.SettlementDetailEnvelope;
import com.baiyue.finance.dto.FinanceSettlementDtos.SettlementListEnvelope;
import com.baiyue.finance.dto.FinanceSettlementDtos.SettlementSummary;
import com.baiyue.finance.dto.FinanceSettlementDtos.SettlementActionRequest;
import com.baiyue.finance.service.FinanceSettlementService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -40,7 +41,7 @@ public class FinanceSettlementController {
}
@GetMapping("/receivable-settlements")
@PreAuthorize("hasAuthority('platform:finance:settlement-ar:view')")
@PreAuthorize("hasAnyAuthority('platform:finance:settlement-ar:list','platform:finance:settlement-ar:view')")
@Operation(summary = "查询正式应收结算单")
public SettlementListEnvelope receivableSettlements(
@Parameter(description = "可选客户筛选;省略时查询全部客户")
@@ -49,7 +50,7 @@ public class FinanceSettlementController {
}
@GetMapping("/payable-settlements")
@PreAuthorize("hasAuthority('platform:finance:settlement-ap:view')")
@PreAuthorize("hasAnyAuthority('platform:finance:settlement-ap:list','platform:finance:settlement-ap:view')")
@Operation(summary = "查询正式应付结算单")
public SettlementListEnvelope payableSettlements(
@Parameter(description = "可选客户筛选;省略时查询全部客户")
@@ -76,13 +77,17 @@ public class FinanceSettlementController {
}
@GetMapping("/settlements/{settlementId}")
@PreAuthorize("hasAnyAuthority('platform:finance:settlement-ar:view','platform:finance:settlement-ap:view')")
@PreAuthorize("hasAnyAuthority('platform:finance:settlement-ar:list','platform:finance:settlement-ar:view','platform:finance:settlement-ap:list','platform:finance:settlement-ap:view')")
public SettlementDetailEnvelope getSettlement(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable long settlementId) {
SettlementDetail detail = service.getSettlement(settlementId);
String permission = "RECEIVABLE".equals(detail.summary().direction())
String direction = detail.summary().direction();
String listPermission = "RECEIVABLE".equals(direction)
? "platform:finance:settlement-ar:list" : "platform:finance:settlement-ap:list";
String legacyViewPermission = "RECEIVABLE".equals(direction)
? "platform:finance:settlement-ar:view" : "platform:finance:settlement-ap:view";
if (user == null || !user.permissions().contains(permission)) {
if (user == null || (!user.permissions().contains(listPermission)
&& !user.permissions().contains(legacyViewPermission))) {
throw new AccessDeniedException("无权查看该方向结算单");
}
return success(detail);
@@ -98,6 +103,16 @@ public class FinanceSettlementController {
return success(service.createSettlementPayment(settlementId, user.userId(), idempotencyKey, request));
}
@PostMapping("/settlements/{settlementId}/actions")
@PreAuthorize("hasAuthority('platform:finance:settlement:void')")
public SettlementDetailEnvelope actionSettlement(
@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable long settlementId,
@RequestHeader("Idempotency-Key") String idempotencyKey,
@Valid @RequestBody SettlementActionRequest request) {
return success(service.actionSettlement(settlementId, user.userId(), idempotencyKey, request));
}
private static SettlementDetailEnvelope success(SettlementDetail detail) {
String requestId = first(MDC.get("requestId"), MDC.get("request_id"));
if (!StringUtils.hasText(requestId)) requestId = UUID.randomUUID().toString();
@@ -45,14 +45,14 @@ public class PlatformFinanceController {
}
@GetMapping("/customer-accounts/{customerId}")
@PreAuthorize("hasAuthority('platform:finance:bill:view')")
@PreAuthorize("hasAnyAuthority('platform:finance:customer-account:list','platform:finance:account:view','platform:finance:bill:view')")
@Operation(summary = "客户人工应收账户及流水")
public ApiResponse<?> customerAccount(@PathVariable long customerId) {
return FinanceResponses.success(queryService.customerAccount(customerId));
}
@GetMapping("/fee-pool")
@PreAuthorize("hasAuthority('platform:finance:bill:view')")
@PreAuthorize("hasAnyAuthority('platform:finance:customer-arap:list','platform:finance:bill:view')")
@Operation(summary = "客户应收应付费用池")
public ApiResponse<?> feePool(
@Parameter(description = "可选客户筛选;省略时查询全部客户")
@@ -62,7 +62,7 @@ public class PlatformFinanceController {
}
@GetMapping("/statements")
@PreAuthorize("hasAuthority('platform:finance:bill:view')")
@PreAuthorize("hasAnyAuthority('platform:finance:customer-statement:list','platform:finance:bill:view')")
@Operation(summary = "客户对账单")
public ApiResponse<?> statements(
@Parameter(description = "可选客户筛选;省略时查询全部客户")
@@ -87,7 +87,7 @@ public class PlatformFinanceController {
public ApiResponse<?> terms(@PathVariable long tenantId) { return FinanceResponses.success(termsView(tenantId, service.customerTerms(tenantId))); }
@GetMapping("/bills")
@PreAuthorize("hasAuthority('platform:finance:bill:view')")
@PreAuthorize("hasAnyAuthority('platform:finance:customer-statement:list','platform:finance:bill:view')")
@Operation(summary = "查询平台账单")
public ApiResponse<?> bills(@RequestParam long customerId) { return FinanceResponses.success(queryService.statements(customerId, false)); }
@@ -115,11 +115,11 @@ public class PlatformFinanceController {
}
@GetMapping("/bills/{billId}")
@PreAuthorize("hasAuthority('platform:finance:bill:view')")
@PreAuthorize("hasAnyAuthority('platform:finance:customer-statement:list','platform:finance:bill:view')")
public ApiResponse<?> bill(@PathVariable long billId) { return FinanceResponses.success(queryService.platformStatement(billId)); }
@GetMapping("/bills/{billId}/revisions")
@PreAuthorize("hasAuthority('platform:finance:bill:view')")
@PreAuthorize("hasAnyAuthority('platform:finance:customer-statement:list','platform:finance:bill:view')")
public ApiResponse<?> revisions(@PathVariable long billId, @RequestParam long customerId) {
return FinanceResponses.success(queryService.billRevisions(customerId, billId));
}
@@ -36,7 +36,7 @@ public class ReconciliationStatementController {
}
@GetMapping("/reconciliation-statements/{statementId}")
@PreAuthorize("hasAuthority('platform:finance:bill:view')")
@PreAuthorize("hasAnyAuthority('platform:finance:customer-statement:list','platform:finance:bill:view')")
public ApiResponse<ReconciliationDetail> getReconciliationStatement(@PathVariable long statementId) {
return FinanceResponses.success(service.detailForPlatform(statementId));
}
@@ -56,7 +56,11 @@ public final class FinanceReadModels {
BigDecimal outstandingAmount,
String currency,
String status,
int version) {}
int version,
long createdBy,
LocalDateTime createdAt,
long updatedBy,
LocalDateTime updatedAt) {}
public record StatementDetail(Statement statement, List<StatementLine> lines) {}
@@ -18,6 +18,7 @@ public final class FinanceSettlementDtos {
public record CreateSettlementPaymentRequest(LocalDate transactionDate, BigDecimal amount, String currency,
String method, String referenceNo, List<String> voucherFileIds,
Integer expectedVersion, String remark) {}
public record SettlementActionRequest(String action, String reason, Integer expectedVersion) {}
public record SettlementSummary(long settlementId, String settlementNo, String direction, long customerId,
String customerName, Long supplierId, String supplierName,
@@ -23,7 +23,8 @@ public final class ReconciliationStatementDtos {
int creditDays, String currency, BigDecimal totalReceivable,
BigDecimal receivedAmount, BigDecimal outstandingAmount,
boolean settled, String status, int version,
LocalDateTime createdAt, LocalDateTime updatedAt, String remark,
long createdBy, LocalDateTime createdAt,
long updatedBy, LocalDateTime updatedAt, String remark,
List<ReconciliationLine> lines, List<PaymentHistory> paymentHistory,
List<ReconciliationRevision> revisions) {}
@@ -93,22 +93,24 @@ public class FinanceQueryService {
public List<Statement> statements(Long customerId, boolean clientVisibleOnly) {
validateOptionalCustomer(customerId);
String visible = clientVisibleOnly ? " AND status IN ('ISSUED','PARTIALLY_PAID','PAID')" : "";
String sql = "SELECT id,tenant_id,bill_no,customer_name_snapshot,bill_date,due_date,total_amount,received_amount,outstanding_amount,currency_code,status,version FROM fin_bill WHERE 1=1";
String sql = "SELECT id,tenant_id,bill_no,customer_name_snapshot,bill_date,due_date,total_amount,received_amount,outstanding_amount,currency_code,status,version,created_by,created_at,updated_by,updated_at FROM fin_bill WHERE 1=1";
Object[] args = new Object[0];
if (customerId != null) { sql += " AND tenant_id=?"; args = new Object[]{customerId}; }
return jdbc.query(sql + visible + " ORDER BY bill_date DESC,id DESC",
(rs, rowNum) -> new Statement(rs.getLong("id"), rs.getLong("tenant_id"), rs.getString("bill_no"), rs.getString("customer_name_snapshot"),
rs.getDate("bill_date").toLocalDate(), rs.getDate("due_date").toLocalDate(), rs.getBigDecimal("total_amount"), rs.getBigDecimal("received_amount"),
rs.getBigDecimal("outstanding_amount"), rs.getString("currency_code"), rs.getString("status"), rs.getInt("version")), args);
rs.getBigDecimal("outstanding_amount"), rs.getString("currency_code"), rs.getString("status"), rs.getInt("version"),
rs.getLong("created_by"), time(rs, "created_at"), rs.getLong("updated_by"), time(rs, "updated_at")), args);
}
public StatementDetail statement(long customerId, long billId, boolean clientVisibleOnly) {
requireCustomer(customerId);
String visible = clientVisibleOnly ? " AND status IN ('ISSUED','PARTIALLY_PAID','PAID')" : "";
List<Statement> rows = jdbc.query("SELECT id,tenant_id,bill_no,customer_name_snapshot,bill_date,due_date,total_amount,received_amount,outstanding_amount,currency_code,status,version FROM fin_bill WHERE tenant_id=? AND id=?" + visible,
List<Statement> rows = jdbc.query("SELECT id,tenant_id,bill_no,customer_name_snapshot,bill_date,due_date,total_amount,received_amount,outstanding_amount,currency_code,status,version,created_by,created_at,updated_by,updated_at FROM fin_bill WHERE tenant_id=? AND id=?" + visible,
(rs, rowNum) -> new Statement(rs.getLong("id"), rs.getLong("tenant_id"), rs.getString("bill_no"), rs.getString("customer_name_snapshot"),
rs.getDate("bill_date").toLocalDate(), rs.getDate("due_date").toLocalDate(), rs.getBigDecimal("total_amount"), rs.getBigDecimal("received_amount"),
rs.getBigDecimal("outstanding_amount"), rs.getString("currency_code"), rs.getString("status"), rs.getInt("version")), customerId, billId);
rs.getBigDecimal("outstanding_amount"), rs.getString("currency_code"), rs.getString("status"), rs.getInt("version"),
rs.getLong("created_by"), time(rs, "created_at"), rs.getLong("updated_by"), time(rs, "updated_at")), customerId, billId);
if (rows.isEmpty()) throw new BizException(ResultCode.NOT_FOUND, "账单不存在或不属于客户");
List<StatementLine> lines = jdbc.query("SELECT id,source_type,source_id,source_version,order_id,inbound_id,fee_item_code,fee_item_name,amount,currency_code FROM fin_bill_line WHERE tenant_id=? AND bill_id=? ORDER BY id",
(rs, rowNum) -> new StatementLine(rs.getLong("id"), rs.getString("source_type"), rs.getLong("source_id"), rs.getInt("source_version"),
@@ -9,6 +9,7 @@ import com.baiyue.finance.dto.FinanceSettlementDtos.SettlementLine;
import com.baiyue.finance.dto.FinanceSettlementDtos.SettlementRevision;
import com.baiyue.finance.dto.FinanceSettlementDtos.SettlementSummary;
import com.baiyue.finance.dto.FinanceSettlementDtos.SettlementTransaction;
import com.baiyue.finance.dto.FinanceSettlementDtos.SettlementActionRequest;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -250,6 +251,43 @@ public class FinanceSettlementService {
return getSettlement(settlementId);
}
@Transactional
public SettlementDetail actionSettlement(long settlementId, long operatorId, String key,
SettlementActionRequest request) {
validateSettlementAction(request);
Map<String, Object> settlement = one(
"SELECT id,tenant_id,total_amount,paid_amount,outstanding_amount,payment_status,status,version " +
"FROM fin_settlement WHERE id=? FOR UPDATE", "结算单不存在", settlementId);
long customerId = number(settlement, "tenant_id").longValue();
Long replay = reserve(customerId, operatorId, "SETTLEMENT_STATE_ACTION", key, "POST",
"/api/platform/finance/v1/settlements/" + settlementId + "/actions", request);
if (replay != null) return getSettlement(replay);
if (number(settlement, "version").intValue() != request.expectedVersion()) {
throw error("FIN_409", "结算单版本冲突");
}
String currentStatus = text(settlement, "status");
String targetStatus;
if ("VOID".equals(request.action())) {
if (!"ISSUED".equals(currentStatus) || decimal(settlement, "paid_amount").signum() != 0) {
throw error("FIN_409", "只有未发生收付款的生效结算单可以作废");
}
targetStatus = "VOIDED";
} else {
if (!"VOIDED".equals(currentStatus) || decimal(settlement, "paid_amount").signum() != 0) {
throw error("FIN_409", "只有未发生收付款的已作废结算单可以重开");
}
targetStatus = "ISSUED";
}
int changed = jdbc.update("UPDATE fin_settlement SET status=?,updated_by=?,version=version+1 " +
"WHERE id=? AND tenant_id=? AND version=?", targetStatus, operatorId, settlementId, customerId,
request.expectedVersion());
if (changed != 1) throw error("FIN_409", "结算单版本冲突");
audit(customerId, settlementId, request.action(), request.reason(), settlementStateSnapshot(settlement),
settlementStateSnapshot(settlement, targetStatus), operatorId);
complete(customerId, operatorId, "SETTLEMENT_STATE_ACTION", key, settlementId);
return getSettlement(settlementId);
}
private long insertSettlement(long customerId, String no, String direction, String customerName,
Long supplierId, String supplierName, Long reconciliationId,
String businessPeriod, String currency, BigDecimal total,
@@ -308,6 +346,14 @@ public class FinanceSettlementService {
if (request.remark() != null && request.remark().length() > 500) throw error("FIN_400", "付款备注不能超过500字");
}
private void validateSettlementAction(SettlementActionRequest request) {
if (request == null || !Set.of("VOID", "REOPEN").contains(request.action())
|| !StringUtils.hasText(request.reason()) || request.reason().length() > 500
|| request.expectedVersion() == null || request.expectedVersion() < 0) {
throw error("FIN_400", "结算单操作、原因或版本无效");
}
}
private void validateVoucherFiles(long customerId, List<String> fileIds) {
if (fileClient == null || !StringUtils.hasText(fileToken)) throw error("FIN_503", "File服务校验能力不可用");
for (String raw : fileIds) {
@@ -435,6 +481,16 @@ public class FinanceSettlementService {
"outstandingAmount", totalAmount.subtract(paidAmount));
}
private static Map<String, Object> settlementStateSnapshot(Map<String, Object> settlement) {
return settlementStateSnapshot(settlement, text(settlement, "status"));
}
private static Map<String, Object> settlementStateSnapshot(Map<String, Object> settlement, String status) {
return Map.of("totalAmount", decimal(settlement, "total_amount"),
"paidAmount", decimal(settlement, "paid_amount"),
"outstandingAmount", decimal(settlement, "outstanding_amount"), "status", status);
}
private String documentNo(String prefix) {
return prefix + "-" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) + "-" +
UUID.randomUUID().toString().replace("-", "").substring(0, 12).toUpperCase(Locale.ROOT);
@@ -169,8 +169,8 @@ public class ReconciliationStatementService {
(rs, row) -> revision(rs, row + 1), customerId, statementId);
return new ReconciliationDetail(bill.id(), bill.number(), bill.customerId(), bill.customerName(),
metadata.periodStart(), metadata.periodEnd(), bill.creditDays(), "CNY", bill.total(), bill.received(),
bill.outstanding(), bill.outstanding().signum() == 0, reconciliationStatus(bill.status()), bill.version(), bill.createdAt(),
bill.updatedAt(), bill.remark(), lines, payments, revisions);
bill.outstanding(), bill.outstanding().signum() == 0, reconciliationStatus(bill.status()), bill.version(),
bill.createdBy(), bill.createdAt(), bill.updatedBy(), bill.updatedAt(), bill.remark(), lines, payments, revisions);
}
public ReconciliationDetail detailForPlatform(long statementId) {
@@ -420,7 +420,8 @@ public class ReconciliationStatementService {
return new Bill(rs.getLong("id"), rs.getLong("tenant_id"), rs.getString("bill_no"),
rs.getString("customer_name_snapshot"), rs.getString("status"), rs.getBigDecimal("total_amount"),
rs.getBigDecimal("received_amount"), rs.getBigDecimal("outstanding_amount"), rs.getInt("version"),
rs.getInt("credit_days"), rs.getString("remark"), rs.getTimestamp("created_at").toLocalDateTime(),
rs.getInt("credit_days"), rs.getString("remark"), rs.getLong("created_by"),
rs.getTimestamp("created_at").toLocalDateTime(), rs.getLong("updated_by"),
rs.getTimestamp("updated_at").toLocalDateTime());
}
@@ -461,5 +462,6 @@ public class ReconciliationStatementService {
String feeItemName, BigDecimal amount) {}
private record Bill(long id, long customerId, String number, String customerName, String status,
BigDecimal total, BigDecimal received, BigDecimal outstanding, int version,
int creditDays, String remark, LocalDateTime createdAt, LocalDateTime updatedAt) {}
int creditDays, String remark, long createdBy, LocalDateTime createdAt,
long updatedBy, LocalDateTime updatedAt) {}
}