优化
This commit is contained in:
@@ -8,6 +8,7 @@ import com.baiyue.finance.service.FinanceQueryService;
|
||||
import com.baiyue.finance.client.FinanceFileClient;
|
||||
import com.baiyue.finance.client.FirstlegCostSnapshotClient;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -53,7 +54,9 @@ public class PlatformFinanceController {
|
||||
@GetMapping("/fee-pool")
|
||||
@PreAuthorize("hasAuthority('platform:finance:bill:view')")
|
||||
@Operation(summary = "客户应收应付费用池")
|
||||
public ApiResponse<?> feePool(@RequestParam long customerId,
|
||||
public ApiResponse<?> feePool(
|
||||
@Parameter(description = "可选客户筛选;省略时查询全部客户")
|
||||
@RequestParam(required = false) Long customerId,
|
||||
@RequestParam(required = false) String direction) {
|
||||
return FinanceResponses.success(queryService.feePool(customerId, direction));
|
||||
}
|
||||
@@ -61,21 +64,27 @@ public class PlatformFinanceController {
|
||||
@GetMapping("/statements")
|
||||
@PreAuthorize("hasAuthority('platform:finance:bill:view')")
|
||||
@Operation(summary = "客户对账单")
|
||||
public ApiResponse<?> statements(@RequestParam long customerId) {
|
||||
public ApiResponse<?> statements(
|
||||
@Parameter(description = "可选客户筛选;省略时查询全部客户")
|
||||
@RequestParam(required = false) Long customerId) {
|
||||
return FinanceResponses.success(queryService.statements(customerId, false));
|
||||
}
|
||||
|
||||
@GetMapping("/receivable-settlements")
|
||||
@PreAuthorize("hasAuthority('platform:finance:bill:view')")
|
||||
@Operation(summary = "结算单应收")
|
||||
public ApiResponse<?> receivableSettlements(@RequestParam long customerId) {
|
||||
public ApiResponse<?> receivableSettlements(
|
||||
@Parameter(description = "可选客户筛选;省略时查询全部客户")
|
||||
@RequestParam(required = false) Long customerId) {
|
||||
return FinanceResponses.success(queryService.receivableSettlements(customerId, false));
|
||||
}
|
||||
|
||||
@GetMapping("/payable-settlements")
|
||||
@PreAuthorize("hasAuthority('platform:finance:cost:view')")
|
||||
@Operation(summary = "结算单应付")
|
||||
public ApiResponse<?> payableSettlements(@RequestParam long customerId) {
|
||||
public ApiResponse<?> payableSettlements(
|
||||
@Parameter(description = "可选客户筛选;省略时查询全部客户")
|
||||
@RequestParam(required = false) Long customerId) {
|
||||
return FinanceResponses.success(queryService.payableSettlements(customerId));
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ public final class FinanceReadModels {
|
||||
|
||||
public record FeePoolItem(
|
||||
String poolItemId,
|
||||
long customerId,
|
||||
String direction,
|
||||
String sourceType,
|
||||
long sourceId,
|
||||
|
||||
@@ -58,45 +58,48 @@ public class FinanceQueryService {
|
||||
return new CustomerAccount(customerId, CNY, "MANUAL_RECEIVABLE_LEDGER", total, received, outstanding, List.copyOf(entries));
|
||||
}
|
||||
|
||||
public List<FeePoolItem> feePool(long customerId, String direction) {
|
||||
requireCustomer(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);
|
||||
if (!List.of("ALL", "RECEIVABLE", "PAYABLE").contains(normalized)) throw bad("费用方向只能是RECEIVABLE或PAYABLE");
|
||||
List<FeePoolItem> result = new ArrayList<>();
|
||||
String sourceSql = "SELECT id,direction,source_type,source_fee_id,source_fee_version,order_id,inbound_id,container_id,fee_item_name,amount,currency_code,status,created_at FROM fin_fee_source WHERE customer_id=? AND status='AVAILABLE'";
|
||||
Object[] sourceArgs;
|
||||
if ("ALL".equals(normalized)) {
|
||||
sourceSql += " ORDER BY created_at DESC,id DESC";
|
||||
sourceArgs = new Object[]{customerId};
|
||||
} else {
|
||||
sourceSql += " AND direction=? ORDER BY created_at DESC,id DESC";
|
||||
sourceArgs = new Object[]{customerId, normalized};
|
||||
}
|
||||
String sourceSql = "SELECT id,customer_id,direction,source_type,source_fee_id,source_fee_version,order_id,inbound_id,container_id,fee_item_name,amount,currency_code,status,created_at FROM fin_fee_source WHERE status='AVAILABLE'";
|
||||
List<Object> sourceArgs = new ArrayList<>();
|
||||
if (customerId != null) { sourceSql += " AND customer_id=?"; sourceArgs.add(customerId); }
|
||||
if (!"ALL".equals(normalized)) { sourceSql += " AND direction=?"; sourceArgs.add(normalized); }
|
||||
sourceSql += " ORDER BY created_at DESC,id DESC";
|
||||
result.addAll(jdbc.query(sourceSql, (rs, rowNum) -> {
|
||||
String rowDirection = rs.getString("direction");
|
||||
return new FeePoolItem(rowDirection + ":" + rs.getLong("id"), rowDirection,
|
||||
return new FeePoolItem(rowDirection + ":" + rs.getLong("id"), rs.getLong("customer_id"), rowDirection,
|
||||
rs.getString("source_type"), rs.getLong("source_fee_id"), rs.getInt("source_fee_version"),
|
||||
nullableLong(rs, "order_id"), nullableLong(rs, "inbound_id"), nullableLong(rs, "container_id"),
|
||||
rs.getString("fee_item_name"), rs.getBigDecimal("amount"), rs.getString("currency_code"),
|
||||
rs.getString("status"), time(rs, "created_at"));
|
||||
}, sourceArgs));
|
||||
}, sourceArgs.toArray()));
|
||||
if (!"RECEIVABLE".equals(normalized)) {
|
||||
result.addAll(jdbc.query("SELECT id,order_id,inbound_id,container_id,fee_item_name,cost_amount,currency_code,status,created_at FROM fin_cost WHERE tenant_id=? ORDER BY created_at DESC,id DESC", (rs, rowNum) ->
|
||||
new FeePoolItem("PAYABLE:" + rs.getLong("id"), "PAYABLE", "MANUAL_COST", rs.getLong("id"), 1,
|
||||
String costSql = "SELECT id,tenant_id,order_id,inbound_id,container_id,fee_item_name,cost_amount,currency_code,status,created_at FROM fin_cost";
|
||||
Object[] costArgs = new Object[0];
|
||||
if (customerId != null) { costSql += " WHERE tenant_id=?"; costArgs = new Object[]{customerId}; }
|
||||
costSql += " ORDER BY created_at DESC,id DESC";
|
||||
result.addAll(jdbc.query(costSql, (rs, rowNum) ->
|
||||
new FeePoolItem("PAYABLE:" + rs.getLong("id"), rs.getLong("tenant_id"), "PAYABLE", "MANUAL_COST", rs.getLong("id"), 1,
|
||||
nullableLong(rs, "order_id"), nullableLong(rs, "inbound_id"), nullableLong(rs, "container_id"),
|
||||
rs.getString("fee_item_name"), rs.getBigDecimal("cost_amount"), rs.getString("currency_code"), rs.getString("status"), time(rs, "created_at")), customerId));
|
||||
rs.getString("fee_item_name"), rs.getBigDecimal("cost_amount"), rs.getString("currency_code"), rs.getString("status"), time(rs, "created_at")), costArgs));
|
||||
}
|
||||
result.sort(Comparator.comparing(FeePoolItem::createdAt, Comparator.nullsLast(Comparator.reverseOrder())));
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
public List<Statement> statements(long customerId, boolean clientVisibleOnly) {
|
||||
requireCustomer(customerId);
|
||||
public List<Statement> statements(Long customerId, boolean clientVisibleOnly) {
|
||||
validateOptionalCustomer(customerId);
|
||||
String visible = clientVisibleOnly ? " AND status IN ('ISSUED','PARTIALLY_PAID','PAID')" : "";
|
||||
return 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=?" + visible + " ORDER BY bill_date DESC,id DESC",
|
||||
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";
|
||||
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")), customerId);
|
||||
rs.getBigDecimal("outstanding_amount"), rs.getString("currency_code"), rs.getString("status"), rs.getInt("version")), args);
|
||||
}
|
||||
|
||||
public StatementDetail statement(long customerId, long billId, boolean clientVisibleOnly) {
|
||||
@@ -127,22 +130,25 @@ public class FinanceQueryService {
|
||||
rs.getString("bill_snapshot"), rs.getString("line_snapshot"), rs.getLong("created_by"), time(rs, "created_at")), customerId, billId);
|
||||
}
|
||||
|
||||
public List<ReceivableSettlement> receivableSettlements(long customerId, boolean clientVisibleOnly) {
|
||||
public List<ReceivableSettlement> receivableSettlements(Long customerId, boolean clientVisibleOnly) {
|
||||
return statements(customerId, clientVisibleOnly).stream().map(row -> new ReceivableSettlement(
|
||||
row.id(), row.customerId(), row.billNo(), row.billDate(), row.dueDate(), row.totalAmount(), row.receivedAmount(),
|
||||
row.outstandingAmount(), row.currency(), row.status(), row.version())).toList();
|
||||
}
|
||||
|
||||
public List<PayableSettlement> payableSettlements(long customerId) {
|
||||
requireCustomer(customerId);
|
||||
return jdbc.query("SELECT id,tenant_id,cost_no,cost_type,order_id,inbound_id,container_id,supplier_name_snapshot,fee_item_name,cost_amount,currency_code,status,created_at,version FROM fin_cost WHERE tenant_id=? ORDER BY created_at DESC,id DESC",
|
||||
public List<PayableSettlement> payableSettlements(Long customerId) {
|
||||
validateOptionalCustomer(customerId);
|
||||
String sql = "SELECT id,tenant_id,cost_no,cost_type,order_id,inbound_id,container_id,supplier_name_snapshot,fee_item_name,cost_amount,currency_code,status,created_at,version FROM fin_cost WHERE 1=1";
|
||||
Object[] args = new Object[0];
|
||||
if (customerId != null) { sql += " AND tenant_id=?"; args = new Object[]{customerId}; }
|
||||
return jdbc.query(sql + " ORDER BY created_at DESC,id DESC",
|
||||
(rs, rowNum) -> {
|
||||
BigDecimal amount = rs.getBigDecimal("cost_amount");
|
||||
return new PayableSettlement(rs.getLong("id"), rs.getLong("tenant_id"), rs.getString("cost_no"), rs.getString("cost_type"),
|
||||
nullableLong(rs, "order_id"), nullableLong(rs, "inbound_id"), nullableLong(rs, "container_id"), rs.getString("supplier_name_snapshot"),
|
||||
rs.getString("fee_item_name"), amount, BigDecimal.ZERO, amount, rs.getString("currency_code"), rs.getString("status"),
|
||||
time(rs, "created_at"), rs.getInt("version"));
|
||||
}, customerId);
|
||||
}, args);
|
||||
}
|
||||
|
||||
public CostDetail cost(long customerId, long costId) {
|
||||
@@ -206,6 +212,10 @@ public class FinanceQueryService {
|
||||
if (customerId <= 0) throw bad("客户范围无效");
|
||||
}
|
||||
|
||||
private static void validateOptionalCustomer(Long customerId) {
|
||||
if (customerId != null) requireCustomer(customerId);
|
||||
}
|
||||
|
||||
private static BizException bad(String message) {
|
||||
return new BizException(ResultCode.BAD_REQUEST, message);
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ class FinanceFeePoolDirectionBehaviorTest {
|
||||
}
|
||||
|
||||
private static FeePoolItem item(String direction, String sourceType, long sourceId) {
|
||||
return new FeePoolItem(direction + ":" + sourceId, direction, sourceType, sourceId, 1,
|
||||
return new FeePoolItem(direction + ":" + sourceId, 7L, direction, sourceType, sourceId, 1,
|
||||
11L, 21L, null, "清关费", new BigDecimal("100.00"), "CNY", "AVAILABLE",
|
||||
LocalDateTime.of(2026, 8, 11, 1, 0));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.baiyue.finance;
|
||||
|
||||
import com.baiyue.finance.controller.PlatformFinanceController;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class FinancePlatformListContractTest {
|
||||
@Test
|
||||
void platformCustomerFiltersAreOptionalAndDocumentAllCustomerDefault() throws Exception {
|
||||
for (String methodName : new String[]{"feePool", "statements", "receivableSettlements", "payableSettlements"}) {
|
||||
Method method = find(methodName);
|
||||
var customer = method.getParameters()[0];
|
||||
assertThat(customer.getType()).isEqualTo(Long.class);
|
||||
assertThat(customer.getAnnotation(RequestParam.class).required()).isFalse();
|
||||
assertThat(customer.getAnnotation(Parameter.class).description()).contains("省略时查询全部客户");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformListPermissionsRemainSeparatedByReceivableAndPayableData() throws Exception {
|
||||
Map<String, String> expected = Map.of(
|
||||
"feePool", "hasAuthority('platform:finance:bill:view')",
|
||||
"statements", "hasAuthority('platform:finance:bill:view')",
|
||||
"receivableSettlements", "hasAuthority('platform:finance:bill:view')",
|
||||
"payableSettlements", "hasAuthority('platform:finance:cost:view')");
|
||||
for (var entry : expected.entrySet()) {
|
||||
assertThat(find(entry.getKey()).getAnnotation(PreAuthorize.class).value()).isEqualTo(entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void allCustomerFeePoolResponseCarriesCustomerIdentity() {
|
||||
assertThat(Arrays.stream(com.baiyue.finance.dto.FinanceReadModels.FeePoolItem.class
|
||||
.getRecordComponents()).map(java.lang.reflect.RecordComponent::getName))
|
||||
.contains("customerId");
|
||||
}
|
||||
|
||||
private static Method find(String name) throws Exception {
|
||||
return switch (name) {
|
||||
case "feePool" -> PlatformFinanceController.class.getMethod(name, Long.class, String.class);
|
||||
default -> PlatformFinanceController.class.getMethod(name, Long.class);
|
||||
};
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package com.baiyue.finance;
|
||||
|
||||
import com.baiyue.finance.service.FinanceQueryService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class FinancePlatformOptionalCustomerQueryTest {
|
||||
@Test
|
||||
void allCustomerQueriesOmitCustomerPredicateAndArguments() {
|
||||
RecordingJdbc jdbc = new RecordingJdbc();
|
||||
FinanceQueryService service = new FinanceQueryService(jdbc, new ObjectMapper());
|
||||
|
||||
service.feePool(null, null);
|
||||
service.statements(null, false);
|
||||
service.receivableSettlements(null, false);
|
||||
service.payableSettlements(null);
|
||||
|
||||
assertThat(jdbc.calls).hasSize(5).allSatisfy(call -> {
|
||||
assertThat(call.sql()).doesNotContain("customer_id=?", "tenant_id=?");
|
||||
assertThat(call.args()).isEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void customerFilterAddsPredicateAndRealCustomerArgument() {
|
||||
RecordingJdbc jdbc = new RecordingJdbc();
|
||||
FinanceQueryService service = new FinanceQueryService(jdbc, new ObjectMapper());
|
||||
|
||||
service.feePool(31L, null);
|
||||
service.statements(31L, false);
|
||||
service.receivableSettlements(31L, false);
|
||||
service.payableSettlements(31L);
|
||||
|
||||
assertThat(jdbc.calls).hasSize(5).allSatisfy(call -> {
|
||||
assertThat(call.sql()).containsAnyOf("customer_id=?", "tenant_id=?");
|
||||
assertThat(call.args()).containsExactly(31L);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void optionalCustomerNeverTreatsZeroAsAllCustomers() {
|
||||
FinanceQueryService service = new FinanceQueryService(new RecordingJdbc(), new ObjectMapper());
|
||||
|
||||
assertThatThrownBy(() -> service.statements(0L, false)).hasMessageContaining("客户范围无效");
|
||||
assertThatThrownBy(() -> service.feePool(-1L, null)).hasMessageContaining("客户范围无效");
|
||||
}
|
||||
|
||||
private record Call(String sql, List<Object> args) {}
|
||||
|
||||
private static final class RecordingJdbc extends JdbcTemplate {
|
||||
private final List<Call> calls = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public <T> List<T> query(String sql, RowMapper<T> mapper, Object... args) {
|
||||
calls.add(new Call(sql, List.of(args)));
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@@ -75,4 +76,35 @@ class FinanceReadApiBehaviorTest {
|
||||
verify(queryService).receivableSettlements(11L, false);
|
||||
verify(queryService).payableSettlements(11L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformListsDefaultToAllCustomersAndKeepOptionalFilter() {
|
||||
var controller = new PlatformFinanceController(service, fileClient, snapshotClient, queryService);
|
||||
|
||||
controller.feePool(null, null);
|
||||
controller.statements(null);
|
||||
controller.receivableSettlements(null);
|
||||
controller.payableSettlements(null);
|
||||
controller.statements(31L);
|
||||
|
||||
verify(queryService).feePool(null, null);
|
||||
verify(queryService).statements(null, false);
|
||||
verify(queryService).receivableSettlements(null, false);
|
||||
verify(queryService).payableSettlements(null);
|
||||
verify(queryService).statements(31L, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void clientListsAlwaysUseAuthenticatedCustomerScope() {
|
||||
AuthenticatedUser user = new AuthenticatedUser(7L, 23L, null, null, "client", "MERCHANT", "WEB", Set.of());
|
||||
var controller = new ClientFinanceController(service, queryService);
|
||||
|
||||
controller.bills(user);
|
||||
controller.statements(user);
|
||||
controller.receivableSettlements(user);
|
||||
|
||||
verify(queryService, org.mockito.Mockito.times(2)).statements(23L, true);
|
||||
verify(queryService).receivableSettlements(23L, true);
|
||||
verifyNoMoreInteractions(queryService);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user