This commit is contained in:
2026-08-13 12:22:31 +08:00
parent 16dc03a2e2
commit e742abd1cf
49 changed files with 821 additions and 50 deletions
@@ -27,7 +27,7 @@ public class FinancePhase1OperationController {
public FinancePhase1OperationController(FinancePhase1OperationService service) { this.service = service; }
@GetMapping("/accounts")
@PreAuthorize("hasAnyAuthority('platform:finance:customer-account:list','platform:finance:account:view')")
@PreAuthorize("hasAuthority('platform:finance:customer-account:list')")
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("hasAnyAuthority('platform:finance:customer-account:list','platform:finance:account:view')")
@PreAuthorize("hasAuthority('platform:finance:customer-account:list')")
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("hasAnyAuthority('platform:finance:customer-arap:list','platform:finance:fee-source:view')")
@PreAuthorize("hasAuthority('platform:finance:customer-arap:list')")
public ApiResponse<?> getFinanceFeeSource(@PathVariable long feeSourceId) {
return FinanceResponses.success(service.getFinanceFeeSource(feeSourceId));
}
@@ -41,7 +41,7 @@ public class FinanceSettlementController {
}
@GetMapping("/receivable-settlements")
@PreAuthorize("hasAnyAuthority('platform:finance:settlement-ar:list','platform:finance:settlement-ar:view')")
@PreAuthorize("hasAuthority('platform:finance:settlement-ar:list')")
@Operation(summary = "查询正式应收结算单")
public SettlementListEnvelope receivableSettlements(
@Parameter(description = "可选客户筛选;省略时查询全部客户")
@@ -50,7 +50,7 @@ public class FinanceSettlementController {
}
@GetMapping("/payable-settlements")
@PreAuthorize("hasAnyAuthority('platform:finance:settlement-ap:list','platform:finance:settlement-ap:view')")
@PreAuthorize("hasAuthority('platform:finance:settlement-ap:list')")
@Operation(summary = "查询正式应付结算单")
public SettlementListEnvelope payableSettlements(
@Parameter(description = "可选客户筛选;省略时查询全部客户")
@@ -77,17 +77,14 @@ public class FinanceSettlementController {
}
@GetMapping("/settlements/{settlementId}")
@PreAuthorize("hasAnyAuthority('platform:finance:settlement-ar:list','platform:finance:settlement-ar:view','platform:finance:settlement-ap:list','platform:finance:settlement-ap:view')")
@PreAuthorize("hasAnyAuthority('platform:finance:settlement-ar:list','platform:finance:settlement-ap:list')")
public SettlementDetailEnvelope getSettlement(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable long settlementId) {
SettlementDetail detail = service.getSettlement(settlementId);
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(listPermission)
&& !user.permissions().contains(legacyViewPermission))) {
if (user == null || !user.permissions().contains(listPermission)) {
throw new AccessDeniedException("无权查看该方向结算单");
}
return success(detail);
@@ -45,14 +45,21 @@ public class PlatformFinanceController {
}
@GetMapping("/customer-accounts/{customerId}")
@PreAuthorize("hasAnyAuthority('platform:finance:customer-account:list','platform:finance:account:view','platform:finance:bill:view')")
@PreAuthorize("hasAuthority('platform:finance:customer-account:list')")
@Operation(summary = "客户人工应收账户及流水")
public ApiResponse<?> customerAccount(@PathVariable long customerId) {
return FinanceResponses.success(queryService.customerAccount(customerId));
}
@GetMapping("/customer-accounts/{customerId}/summary")
@PreAuthorize("hasAuthority('platform:finance:customer-account:list')")
@Operation(summary = "客户正式应收应付汇总")
public ApiResponse<?> customerAccountSummary(@PathVariable long customerId) {
return FinanceResponses.success(queryService.customerAccountSummary(customerId));
}
@GetMapping("/fee-pool")
@PreAuthorize("hasAnyAuthority('platform:finance:customer-arap:list','platform:finance:bill:view')")
@PreAuthorize("hasAuthority('platform:finance:customer-arap:list')")
@Operation(summary = "客户应收应付费用池")
public ApiResponse<?> feePool(
@Parameter(description = "可选客户筛选;省略时查询全部客户")
@@ -62,7 +69,7 @@ public class PlatformFinanceController {
}
@GetMapping("/statements")
@PreAuthorize("hasAnyAuthority('platform:finance:customer-statement:list','platform:finance:bill:view')")
@PreAuthorize("hasAuthority('platform:finance:customer-statement:list')")
@Operation(summary = "客户对账单")
public ApiResponse<?> statements(
@Parameter(description = "可选客户筛选;省略时查询全部客户")
@@ -87,7 +94,7 @@ public class PlatformFinanceController {
public ApiResponse<?> terms(@PathVariable long tenantId) { return FinanceResponses.success(termsView(tenantId, service.customerTerms(tenantId))); }
@GetMapping("/bills")
@PreAuthorize("hasAnyAuthority('platform:finance:customer-statement:list','platform:finance:bill:view')")
@PreAuthorize("hasAuthority('platform:finance:customer-statement:list')")
@Operation(summary = "查询平台账单")
public ApiResponse<?> bills(@RequestParam long customerId) { return FinanceResponses.success(queryService.statements(customerId, false)); }
@@ -115,11 +122,11 @@ public class PlatformFinanceController {
}
@GetMapping("/bills/{billId}")
@PreAuthorize("hasAnyAuthority('platform:finance:customer-statement:list','platform:finance:bill:view')")
@PreAuthorize("hasAuthority('platform:finance:customer-statement:list')")
public ApiResponse<?> bill(@PathVariable long billId) { return FinanceResponses.success(queryService.platformStatement(billId)); }
@GetMapping("/bills/{billId}/revisions")
@PreAuthorize("hasAnyAuthority('platform:finance:customer-statement:list','platform:finance:bill:view')")
@PreAuthorize("hasAuthority('platform:finance:customer-statement:list')")
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("hasAnyAuthority('platform:finance:customer-statement:list','platform:finance:bill:view')")
@PreAuthorize("hasAuthority('platform:finance:customer-statement:list')")
public ApiResponse<ReconciliationDetail> getReconciliationStatement(@PathVariable long statementId) {
return FinanceResponses.success(service.detailForPlatform(statementId));
}
@@ -3,6 +3,7 @@ package com.baiyue.finance.dto;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Instant;
import java.util.List;
/** Stable read projections used by the Finance pages. */
@@ -18,6 +19,17 @@ public final class FinanceReadModels {
BigDecimal outstandingAmount,
List<AccountEntry> entries) {}
public record CustomerAccountSummary(
long customerId,
String currency,
BigDecimal totalReceivable,
BigDecimal receivedAmount,
BigDecimal outstandingReceivable,
BigDecimal pendingBilling,
BigDecimal outstandingPayable,
Instant asOf,
LocalDateTime lastChangedAt) {}
public record AccountEntry(
String entryType,
long resourceId,
@@ -7,6 +7,7 @@ import com.baiyue.finance.dto.FinanceReadModels.ClientReceipt;
import com.baiyue.finance.dto.FinanceReadModels.ClientReceiptAllocation;
import com.baiyue.finance.dto.FinanceReadModels.BillRevision;
import com.baiyue.finance.dto.FinanceReadModels.CustomerAccount;
import com.baiyue.finance.dto.FinanceReadModels.CustomerAccountSummary;
import com.baiyue.finance.dto.FinanceReadModels.CostAllocation;
import com.baiyue.finance.dto.FinanceReadModels.CostDetail;
import com.baiyue.finance.dto.FinanceReadModels.FeePoolItem;
@@ -26,6 +27,7 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
@@ -58,6 +60,28 @@ public class FinanceQueryService {
return new CustomerAccount(customerId, CNY, "MANUAL_RECEIVABLE_LEDGER", total, received, outstanding, List.copyOf(entries));
}
public CustomerAccountSummary customerAccountSummary(long customerId) {
requireCustomer(customerId);
String sql = "SELECT " +
"COALESCE(SUM(CASE WHEN s.direction='RECEIVABLE' AND s.status<>'VOIDED' THEN s.total_amount ELSE 0 END),0) total_receivable," +
"COALESCE(SUM(CASE WHEN s.direction='RECEIVABLE' AND s.status<>'VOIDED' THEN s.paid_amount ELSE 0 END),0) received_amount," +
"COALESCE(SUM(CASE WHEN s.direction='RECEIVABLE' AND s.status<>'VOIDED' THEN s.outstanding_amount ELSE 0 END),0) outstanding_receivable," +
"COALESCE(SUM(CASE WHEN s.direction='PAYABLE' AND s.status<>'VOIDED' THEN s.outstanding_amount ELSE 0 END),0) outstanding_payable," +
"(SELECT COALESCE(SUM(f.amount),0) FROM fin_fee_source f WHERE f.customer_id=? AND f.direction='RECEIVABLE' " +
"AND f.currency_code='CNY' AND f.status='AVAILABLE' " +
"AND JSON_UNQUOTE(JSON_EXTRACT(f.source_snapshot,'$.reviewStatus'))='APPROVED' " +
"AND JSON_UNQUOTE(JSON_EXTRACT(f.source_snapshot,'$.settlementIncluded'))='true' " +
"AND NOT EXISTS (SELECT 1 FROM fin_bill_line bl JOIN fin_bill b ON b.id=bl.bill_id AND b.tenant_id=bl.tenant_id " +
"WHERE bl.tenant_id=f.customer_id AND bl.source_id=f.id AND b.status<>'VOIDED')) pending_billing," +
"GREATEST(COALESCE(MAX(CASE WHEN s.status<>'VOIDED' THEN s.updated_at END),'1970-01-01')," +
"COALESCE((SELECT MAX(f.updated_at) FROM fin_fee_source f WHERE f.customer_id=?),'1970-01-01')) last_changed_at " +
"FROM fin_settlement s WHERE s.tenant_id=?";
return jdbc.queryForObject(sql, (rs, rowNum) -> new CustomerAccountSummary(customerId, CNY,
money(rs, "total_receivable"), money(rs, "received_amount"), money(rs, "outstanding_receivable"),
money(rs, "pending_billing"), money(rs, "outstanding_payable"), Instant.now(), time(rs, "last_changed_at")),
customerId, customerId, customerId);
}
public List<FeePoolItem> feePool(Long customerId, String direction) {
validateOptionalCustomer(customerId);
String normalized = direction == null || direction.isBlank() ? "ALL" : direction.trim().toUpperCase(java.util.Locale.ROOT);
@@ -200,6 +224,11 @@ public class FinanceQueryService {
return value == null ? BigDecimal.ZERO : value;
}
private static BigDecimal money(ResultSet rs, String column) throws SQLException {
BigDecimal value = rs.getBigDecimal(column);
return value == null ? BigDecimal.ZERO : value;
}
private static Long nullableLong(ResultSet rs, String column) throws SQLException {
long value = rs.getLong(column);
return rs.wasNull() ? null : value;