From b4ff441a70302d969b5b9bd37abbcf99743b3f75 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 21 Oct 2026 11:43:55 +0800 Subject: [PATCH] fix(numbering): expose coload and settlement payment business numbers --- .../finance/dto/FinanceSettlementDtos.java | 2 +- .../mapper/FinanceSettlementDetailMapper.java | 43 ++++++++++++++ .../FinanceSettlementDetailRepository.java | 29 ++++++++++ .../service/FinanceSettlementService.java | 57 ++++++------------- .../FinancePlatformListContractTest.java | 2 +- .../FinanceSettlementBehaviorTest.java | 17 ++++-- .../FinanceSettlementDetailMapperTest.java | 36 ++++++++++++ .../PlatformColoadGroupController.java | 3 +- .../coload/model/ColoadGroupModels.java | 1 + .../coload/persistence/ColoadGroupMapper.java | 2 +- .../ColoadGroupQueryRepository.java | 20 +++++++ .../persistence/ColoadPersistenceRows.java | 1 + .../coload/service/ColoadGroupService.java | 9 ++- .../resources/openapi/coload-groups-v1.yaml | 7 ++- .../ColoadGroupOpenApiContractTest.java | 2 +- .../coload/ColoadGroupServiceTest.java | 4 +- .../coload/ColoadGroupSqlContractTest.java | 3 +- 17 files changed, 181 insertions(+), 57 deletions(-) create mode 100644 finance/src/main/java/com/baiyue/finance/mapper/FinanceSettlementDetailMapper.java create mode 100644 finance/src/main/java/com/baiyue/finance/repository/FinanceSettlementDetailRepository.java create mode 100644 finance/src/test/java/com/baiyue/finance/FinanceSettlementDetailMapperTest.java create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/loading/coload/persistence/ColoadGroupQueryRepository.java diff --git a/finance/src/main/java/com/baiyue/finance/dto/FinanceSettlementDtos.java b/finance/src/main/java/com/baiyue/finance/dto/FinanceSettlementDtos.java index 4b4621849..a202d904b 100644 --- a/finance/src/main/java/com/baiyue/finance/dto/FinanceSettlementDtos.java +++ b/finance/src/main/java/com/baiyue/finance/dto/FinanceSettlementDtos.java @@ -37,7 +37,7 @@ public final class FinanceSettlementDtos { BigDecimal quantity, BigDecimal unitPrice, BigDecimal amount, String currency, String reviewStatus, String sourceStatus, boolean statementGenerated) {} - public record SettlementTransaction(long transactionId, String transactionType, LocalDate transactionDate, + public record SettlementTransaction(long transactionId, String transactionNo, String transactionType, LocalDate transactionDate, BigDecimal amount, String method, String referenceNo, List voucherFileIds, long createdBy, String createdByName, LocalDateTime createdAt, diff --git a/finance/src/main/java/com/baiyue/finance/mapper/FinanceSettlementDetailMapper.java b/finance/src/main/java/com/baiyue/finance/mapper/FinanceSettlementDetailMapper.java new file mode 100644 index 000000000..3efe4fd28 --- /dev/null +++ b/finance/src/main/java/com/baiyue/finance/mapper/FinanceSettlementDetailMapper.java @@ -0,0 +1,43 @@ +package com.baiyue.finance.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import java.util.List; +import java.util.Map; + +/** 结算详情只读SQL;子查询均保留所属客户和原结算范围。 */ +@Mapper +public interface FinanceSettlementDetailMapper { + String SUMMARY_SELECT = + "SELECT s.id,s.tenant_id,s.settlement_no,s.direction,s.customer_name_snapshot,s.supplier_id," + + "s.supplier_name_snapshot,s.reconciliation_statement_id,b.bill_no reconciliation_statement_no," + + "s.business_period,(SELECT GROUP_CONCAT(DISTINCT fs.fee_item_name ORDER BY fs.fee_item_name SEPARATOR '、') " + + "FROM fin_settlement_line sl JOIN fin_fee_source fs ON fs.id=sl.fee_source_id " + + "WHERE sl.tenant_id=s.tenant_id AND sl.settlement_id=s.id) fee_item_summary," + + "s.currency_code,s.total_amount,s.paid_amount,s.outstanding_amount,s.generated_date,s.due_date," + + "s.payment_status,s.status,s.created_by,(SELECT a.operator_name_snapshot FROM fin_audit_log a " + + "WHERE a.tenant_id=s.tenant_id AND a.resource_type='FIN_SETTLEMENT' AND a.resource_id=s.id " + + "AND a.action='CREATE' ORDER BY a.created_at,a.id LIMIT 1) created_by_name,s.created_at,s.updated_at," + + "CASE WHEN s.direction='RECEIVABLE' THEN (SELECT MAX(r.created_at) FROM fin_receipt_bill rb " + + "JOIN fin_receipt r ON r.id=rb.receipt_id AND r.tenant_id=rb.tenant_id " + + "WHERE rb.tenant_id=s.tenant_id AND rb.bill_id=s.reconciliation_statement_id AND r.status='CONFIRMED') " + + "ELSE (SELECT MAX(p.created_at) FROM fin_settlement_payment p WHERE p.tenant_id=s.tenant_id " + + "AND p.settlement_id=s.id) END last_payment_at,s.version FROM fin_settlement s " + + "LEFT JOIN fin_bill b ON b.tenant_id=s.tenant_id AND b.id=s.reconciliation_statement_id "; + /** 读取平台结算主单及来源摘要。 */ + @Select(SUMMARY_SELECT + "WHERE s.id=#{settlementId}") + List> summary(@Param("settlementId") long settlementId); + /** 读取该客户该结算的费用明细。 */ + @Select("SELECT f.id fee_source_id,f.source_type,f.source_fee_id,f.source_fee_version,f.direction,f.customer_id,f.order_id,f.inbound_id,f.container_id,f.fee_item_name,f.amount,f.currency_code,f.review_status,f.status,f.source_snapshot FROM fin_settlement_line sl JOIN fin_fee_source f ON f.id=sl.fee_source_id WHERE sl.tenant_id=#{customerId} AND sl.settlement_id=#{settlementId} ORDER BY sl.id") + List> lines(@Param("customerId") long customerId, @Param("settlementId") long settlementId); + /** 读取该客户付款流水及落库业务号,不能用ID替代。 */ + @Select("SELECT id,transaction_no,transaction_type,transaction_date,amount,method,reference_no,voucher_file_ids,created_by,created_by_name,created_at,remark FROM fin_settlement_payment WHERE tenant_id=#{customerId} AND settlement_id=#{settlementId} ORDER BY created_at,id") + List> payments(@Param("customerId") long customerId, @Param("settlementId") long settlementId); + /** 读取对账单已确认收款;收款凭证号不是PAY业务号。 */ + @Select("SELECT r.id,'RECEIPT' transaction_type,DATE(r.received_at) transaction_date,rb.applied_amount amount,r.payment_method method,r.voucher_no reference_no,r.voucher_file_ids,r.created_by,r.created_by_name,r.created_at,r.remark FROM fin_receipt_bill rb JOIN fin_receipt r ON r.id=rb.receipt_id AND r.tenant_id=rb.tenant_id WHERE rb.tenant_id=#{customerId} AND rb.bill_id=#{statementId} AND r.status='CONFIRMED' ORDER BY r.created_at,r.id") + List> receipts(@Param("customerId") long customerId, @Param("statementId") long statementId); + /** 读取该客户结算修订历史。 */ + @Select("SELECT action,reason,before_data,after_data,operator_id,operator_name_snapshot,created_at FROM fin_audit_log WHERE tenant_id=#{customerId} AND resource_type='FIN_SETTLEMENT' AND resource_id=#{settlementId} ORDER BY created_at,id") + List> audits(@Param("customerId") long customerId, @Param("settlementId") long settlementId); +} diff --git a/finance/src/main/java/com/baiyue/finance/repository/FinanceSettlementDetailRepository.java b/finance/src/main/java/com/baiyue/finance/repository/FinanceSettlementDetailRepository.java new file mode 100644 index 000000000..9721c872c --- /dev/null +++ b/finance/src/main/java/com/baiyue/finance/repository/FinanceSettlementDetailRepository.java @@ -0,0 +1,29 @@ +package com.baiyue.finance.repository; + +import com.baiyue.finance.mapper.FinanceSettlementDetailMapper; +import org.springframework.stereotype.Repository; +import org.springframework.util.LinkedCaseInsensitiveMap; +import java.util.List; +import java.util.Map; + +/** 结算详情查询封装,列名大小写差异不改变已有字段映射语义。 */ +@Repository +public class FinanceSettlementDetailRepository { + private final FinanceSettlementDetailMapper mapper; + /** 注入结算详情持久化入口。 */ + public FinanceSettlementDetailRepository(FinanceSettlementDetailMapper mapper) { this.mapper = mapper; } + /** 保留原数据库列名不区分大小写的读取语义。 */ + private List> rows(List> rows) { + return rows.stream().map(row -> { Map copy = new LinkedCaseInsensitiveMap<>(); copy.putAll(row); return copy; }).toList(); + } + /** 读取平台结算主单及来源摘要。 */ + public List> summary(long settlementId) { return rows(mapper.summary(settlementId)); } + /** 读取该客户该结算的费用明细。 */ + public List> lines(long customerId, long settlementId) { return rows(mapper.lines(customerId, settlementId)); } + /** 读取该客户付款流水及落库业务号,不能用ID替代。 */ + public List> payments(long customerId, long settlementId) { return rows(mapper.payments(customerId, settlementId)); } + /** 读取对账单已确认收款;收款凭证号不是PAY业务号。 */ + public List> receipts(long customerId, long statementId) { return rows(mapper.receipts(customerId, statementId)); } + /** 读取该客户结算修订历史。 */ + public List> audits(long customerId, long settlementId) { return rows(mapper.audits(customerId, settlementId)); } +} diff --git a/finance/src/main/java/com/baiyue/finance/service/FinanceSettlementService.java b/finance/src/main/java/com/baiyue/finance/service/FinanceSettlementService.java index 56a110c45..3f1e8a1fa 100644 --- a/finance/src/main/java/com/baiyue/finance/service/FinanceSettlementService.java +++ b/finance/src/main/java/com/baiyue/finance/service/FinanceSettlementService.java @@ -45,23 +45,9 @@ import java.util.UUID; public class FinanceSettlementService { private static final String CNY = "CNY"; private static final Set PAYMENT_METHODS = Set.of("BANK_TRANSFER", "CASH", "OTHER"); - private static final String SETTLEMENT_SUMMARY_SELECT = - "SELECT s.id,s.tenant_id,s.settlement_no,s.direction,s.customer_name_snapshot,s.supplier_id," + - "s.supplier_name_snapshot,s.reconciliation_statement_id,b.bill_no reconciliation_statement_no," + - "s.business_period,(SELECT GROUP_CONCAT(DISTINCT fs.fee_item_name ORDER BY fs.fee_item_name SEPARATOR '、') " + - "FROM fin_settlement_line sl JOIN fin_fee_source fs ON fs.id=sl.fee_source_id " + - "WHERE sl.tenant_id=s.tenant_id AND sl.settlement_id=s.id) fee_item_summary," + - "s.currency_code,s.total_amount,s.paid_amount,s.outstanding_amount,s.generated_date,s.due_date," + - "s.payment_status,s.status,s.created_by,(SELECT a.operator_name_snapshot FROM fin_audit_log a " + - "WHERE a.tenant_id=s.tenant_id AND a.resource_type='FIN_SETTLEMENT' AND a.resource_id=s.id " + - "AND a.action='CREATE' ORDER BY a.created_at,a.id LIMIT 1) created_by_name,s.created_at,s.updated_at," + - "CASE WHEN s.direction='RECEIVABLE' THEN (SELECT MAX(r.created_at) FROM fin_receipt_bill rb " + - "JOIN fin_receipt r ON r.id=rb.receipt_id AND r.tenant_id=rb.tenant_id " + - "WHERE rb.tenant_id=s.tenant_id AND rb.bill_id=s.reconciliation_statement_id AND r.status='CONFIRMED') " + - "ELSE (SELECT MAX(p.created_at) FROM fin_settlement_payment p WHERE p.tenant_id=s.tenant_id " + - "AND p.settlement_id=s.id) END last_payment_at,s.version FROM fin_settlement s " + - "LEFT JOIN fin_bill b ON b.tenant_id=s.tenant_id AND b.id=s.reconciliation_statement_id "; + private static final String SETTLEMENT_SUMMARY_SELECT = com.baiyue.finance.mapper.FinanceSettlementDetailMapper.SUMMARY_SELECT; private final JdbcTemplate jdbc; + private final com.baiyue.finance.repository.FinanceSettlementDetailRepository details; private final ObjectMapper objectMapper; private final FinanceVoucherValidator voucherValidator; private final FinanceSupplierValidator supplierValidator; @@ -71,8 +57,10 @@ public class FinanceSettlementService { @Autowired public FinanceSettlementService(JdbcTemplate jdbc, ObjectMapper objectMapper, FinanceVoucherValidator voucherValidator, FinanceSupplierValidator supplierValidator, - FinanceDocumentNumberGenerator documentNumbers) { + FinanceDocumentNumberGenerator documentNumbers, + com.baiyue.finance.repository.FinanceSettlementDetailRepository details) { this.jdbc = jdbc; + this.details = details; this.objectMapper = objectMapper; this.voucherValidator = voucherValidator; this.supplierValidator = supplierValidator; @@ -183,33 +171,21 @@ public class FinanceSettlementService { return getSettlement(settlementId); } + /** 读取结算及其业务流水号;全部详情数据访问经Repository和Mapper。 */ public SettlementDetail getSettlement(long settlementId) { if (settlementId <= 0) throw error("FIN_404", "结算单不存在"); - Map row = one( - SETTLEMENT_SUMMARY_SELECT + "WHERE s.id=?", - "结算单不存在", settlementId); - SettlementSummary summary = summary(row); - List lines = jdbc.queryForList( - "SELECT f.id fee_source_id,f.source_type,f.source_fee_id,f.source_fee_version,f.direction,f.customer_id," + - "f.order_id,f.inbound_id,f.container_id,f.fee_item_name,f.amount,f.currency_code,f.review_status,f.status,f.source_snapshot " + - "FROM fin_settlement_line sl JOIN fin_fee_source f ON f.id=sl.fee_source_id " + - "WHERE sl.tenant_id=? AND sl.settlement_id=? ORDER BY sl.id", summary.customerId(), settlementId) + List> records = details.summary(settlementId); + if (records.isEmpty()) throw error("FIN_404", "结算单不存在"); + SettlementSummary summary = summary(records.get(0)); + List lines = details.lines(summary.customerId(), settlementId) .stream().map(source -> line(source, summary)).toList(); - List transactions = new ArrayList<>(jdbc.queryForList( - "SELECT id,transaction_type,transaction_date,amount,method,reference_no,voucher_file_ids,created_by,created_by_name,created_at,remark " + - "FROM fin_settlement_payment WHERE tenant_id=? AND settlement_id=? ORDER BY created_at,id", - summary.customerId(), settlementId).stream().map(this::transaction).toList()); + List transactions = new ArrayList<>(details.payments(summary.customerId(), settlementId) + .stream().map(this::transaction).toList()); if ("RECEIVABLE".equals(summary.direction()) && summary.reconciliationStatementId() != null) { - transactions.addAll(jdbc.queryForList( - "SELECT r.id,'RECEIPT' transaction_type,DATE(r.received_at) transaction_date,rb.applied_amount amount,r.payment_method method,r.voucher_no reference_no,r.voucher_file_ids,r.created_by,r.created_by_name,r.created_at,r.remark " + - "FROM fin_receipt_bill rb JOIN fin_receipt r ON r.id=rb.receipt_id AND r.tenant_id=rb.tenant_id " + - "WHERE rb.tenant_id=? AND rb.bill_id=? AND r.status='CONFIRMED' ORDER BY r.created_at,r.id", - summary.customerId(), summary.reconciliationStatementId()).stream().map(this::transaction).toList()); + transactions.addAll(details.receipts(summary.customerId(), summary.reconciliationStatementId()) + .stream().map(this::transaction).toList()); } - List> audits = jdbc.queryForList( - "SELECT action,reason,before_data,after_data,operator_id,operator_name_snapshot,created_at FROM fin_audit_log " + - "WHERE tenant_id=? AND resource_type='FIN_SETTLEMENT' AND resource_id=? ORDER BY created_at,id", - summary.customerId(), settlementId); + List> audits = details.audits(summary.customerId(), settlementId); List revisions = new ArrayList<>(); for (int index = 0; index < audits.size(); index++) revisions.add(revision(index + 1, audits.get(index))); return new SettlementDetail(summary, lines, transactions, List.copyOf(revisions)); @@ -477,8 +453,9 @@ public class FinanceSettlementService { text(row, "status"), true); } + /** 返回真实付款业务号;未生成PAY号的收款记录保持为空,不回退成内部ID。 */ private SettlementTransaction transaction(Map row) { - return new SettlementTransaction(number(row, "id").longValue(), text(row, "transaction_type"), + return new SettlementTransaction(number(row, "id").longValue(), text(row, "transaction_no"), text(row, "transaction_type"), date(row, "transaction_date"), decimal(row, "amount"), text(row, "method"), text(row, "reference_no"), stringList(text(row, "voucher_file_ids")), number(row, "created_by").longValue(), text(row, "created_by_name"), diff --git a/finance/src/test/java/com/baiyue/finance/FinancePlatformListContractTest.java b/finance/src/test/java/com/baiyue/finance/FinancePlatformListContractTest.java index 3fd92a3a3..0bf551cb0 100644 --- a/finance/src/test/java/com/baiyue/finance/FinancePlatformListContractTest.java +++ b/finance/src/test/java/com/baiyue/finance/FinancePlatformListContractTest.java @@ -93,7 +93,7 @@ class FinancePlatformListContractTest { assertRecordFields(com.baiyue.finance.dto.ReconciliationStatementDtos.ReconciliationRevision.class, "operatorId", "operatorName"); assertRecordFields(com.baiyue.finance.dto.FinanceSettlementDtos.SettlementTransaction.class, - "createdBy", "createdByName"); + "createdBy", "createdByName", "transactionNo"); assertRecordFields(com.baiyue.finance.dto.FinanceSettlementDtos.SettlementRevision.class, "operatorId", "operatorName"); assertRecordFields(com.baiyue.finance.dto.FinanceSettlementDtos.SettlementSummary.class, diff --git a/finance/src/test/java/com/baiyue/finance/FinanceSettlementBehaviorTest.java b/finance/src/test/java/com/baiyue/finance/FinanceSettlementBehaviorTest.java index 7898c139e..e5923f6ac 100644 --- a/finance/src/test/java/com/baiyue/finance/FinanceSettlementBehaviorTest.java +++ b/finance/src/test/java/com/baiyue/finance/FinanceSettlementBehaviorTest.java @@ -236,8 +236,8 @@ class FinanceSettlementBehaviorTest { eq("SETTLEMENT_PAYMENT_CREATE"), eq("pay-key"))).thenReturn(List.of()); when(jdbc.queryForList(startsWith("SELECT s.id,s.tenant_id,s.settlement_no"), eq(88L))).thenReturn(List.of(after)); when(jdbc.queryForList(startsWith("SELECT f.id fee_source_id"), eq(10L), eq(88L))).thenReturn(List.of()); - when(jdbc.queryForList(startsWith("SELECT id,transaction_type"), eq(10L), eq(88L))).thenReturn(List.of(Map.ofEntries( - Map.entry("id", 501L), Map.entry("transaction_type", "PAYMENT"), + when(jdbc.queryForList(startsWith("SELECT id,transaction_no,transaction_type"), eq(10L), eq(88L))).thenReturn(List.of(Map.ofEntries( + Map.entry("id", 501L), Map.entry("transaction_no", "PAY202610210001"), Map.entry("transaction_type", "PAYMENT"), Map.entry("transaction_date", Date.valueOf(LocalDate.of(2026, 8, 11))), Map.entry("amount", new BigDecimal("40.00")), Map.entry("method", "BANK_TRANSFER"), Map.entry("reference_no", "BANK-1"), Map.entry("voucher_file_ids", "[\"91\"]"), @@ -258,6 +258,7 @@ class FinanceSettlementBehaviorTest { assertThat(result.transactions()).singleElement().extracting(transaction -> transaction.transactionType()) .isEqualTo("PAYMENT"); assertThat(result.transactions().get(0).createdByName()).isEqualTo("platform_admin"); + assertThat(result.transactions().get(0).transactionNo()).isEqualTo("PAYMENT".equals(result.transactions().get(0).transactionType()) ? "PAY202610210001" : null); verify(jdbc).update(startsWith("UPDATE fin_settlement SET paid_amount"), any(), any(), any(), any(), any(), any(), any(), any()); } @@ -522,6 +523,7 @@ class FinanceSettlementBehaviorTest { .isEqualTo("RECEIPT"); assertThat(result.transactions().get(0).amount()).isEqualByComparingTo("40.00"); assertThat(result.transactions().get(0).createdByName()).isEqualTo("platform_admin"); + assertThat(result.transactions().get(0).transactionNo()).isEqualTo("PAYMENT".equals(result.transactions().get(0).transactionType()) ? "PAY202610210001" : null); } @Test @@ -623,8 +625,15 @@ class FinanceSettlementBehaviorTest { org.mockito.Mockito.lenient().when(documentNumbers.next("AR")).thenReturn("AR202608180001"); org.mockito.Mockito.lenient().when(documentNumbers.next("AP")).thenReturn("AP202608180001"); org.mockito.Mockito.lenient().when(documentNumbers.next("PAY")).thenReturn("PAY202608180001"); + // 既有业务测试保留JDBC替身,新详情路径通过真实Repository和Mapper接口适配执行。 + var mapper = org.mockito.Mockito.mock(com.baiyue.finance.mapper.FinanceSettlementDetailMapper.class, invocation -> { + var select = invocation.getMethod().getAnnotation(org.apache.ibatis.annotations.Select.class); + if (select == null) return RETURNS_DEFAULTS.answer(invocation); + String sql = String.join(" ", select.value()).replaceAll("#\\{[a-zA-Z]+\\}", "?"); + return jdbc.queryForList(sql, invocation.getArguments()); + }); return new FinanceSettlementService(jdbc, new ObjectMapper().findAndRegisterModules(), - new FinanceVoucherValidator(fileClient, "internal-token"), supplierValidator, documentNumbers); + new FinanceVoucherValidator(fileClient, "internal-token"), supplierValidator, documentNumbers, new com.baiyue.finance.repository.FinanceSettlementDetailRepository(mapper)); } private static FinanceSupplierValidator supplierValidator() { @@ -657,7 +666,7 @@ class FinanceSettlementBehaviorTest { when(jdbc.queryForList(startsWith("SELECT s.id,s.tenant_id,s.settlement_no"), eq(88L))).thenReturn(List.of(settlement)); long customerId = ((Number) settlement.get("tenant_id")).longValue(); when(jdbc.queryForList(startsWith("SELECT f.id fee_source_id"), eq(customerId), eq(88L))).thenReturn(List.of()); - when(jdbc.queryForList(startsWith("SELECT id,transaction_type"), eq(customerId), eq(88L))).thenReturn(List.of()); + when(jdbc.queryForList(startsWith("SELECT id,transaction_no,transaction_type"), eq(customerId), eq(88L))).thenReturn(List.of()); when(jdbc.queryForList(startsWith("SELECT action,reason"), eq(customerId), eq(88L))).thenReturn(List.of()); } diff --git a/finance/src/test/java/com/baiyue/finance/FinanceSettlementDetailMapperTest.java b/finance/src/test/java/com/baiyue/finance/FinanceSettlementDetailMapperTest.java new file mode 100644 index 000000000..60cdfb49f --- /dev/null +++ b/finance/src/test/java/com/baiyue/finance/FinanceSettlementDetailMapperTest.java @@ -0,0 +1,36 @@ +package com.baiyue.finance; + +import com.baiyue.finance.mapper.FinanceSettlementDetailMapper; +import com.baiyue.finance.repository.FinanceSettlementDetailRepository; +import org.apache.ibatis.mapping.Environment; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; +import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; +import org.junit.jupiter.api.Test; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DriverManagerDataSource; + +import static org.assertj.core.api.Assertions.assertThat; + +class FinanceSettlementDetailMapperTest { + /** 真实执行付款查询,业务号必须来自本客户本结算的流水,空号不以其他字段替代。 */ + @Test + void paymentNumbersRemainScopedToCustomerAndSettlement() { + var source = new DriverManagerDataSource("jdbc:h2:mem:settlement-number;MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", ""); + var jdbc = new JdbcTemplate(source); + jdbc.execute("CREATE TABLE fin_settlement_payment (id BIGINT PRIMARY KEY,tenant_id BIGINT,settlement_id BIGINT,transaction_no VARCHAR(32),transaction_type VARCHAR(32),transaction_date DATE,amount DECIMAL(18,2),method VARCHAR(32),reference_no VARCHAR(64),voucher_file_ids VARCHAR(255),created_by BIGINT,created_by_name VARCHAR(64),created_at TIMESTAMP,remark VARCHAR(255))"); + jdbc.update("INSERT INTO fin_settlement_payment (id,tenant_id,settlement_id,transaction_no,transaction_type,reference_no) VALUES (1,10,20,'PAY202610210001','PAYMENT','BANK-1'),(2,11,20,'PAY202610210002','PAYMENT','BANK-2'),(3,10,21,'PAY202610210003','PAYMENT','BANK-3'),(4,10,22,NULL,'PAYMENT','BANK-4')"); + var configuration = new Configuration(new Environment("test", new JdbcTransactionFactory(), source)); + configuration.addMapper(FinanceSettlementDetailMapper.class); + try (var session = new SqlSessionFactoryBuilder().build(configuration).openSession()) { + var repository = new FinanceSettlementDetailRepository(session.getMapper(FinanceSettlementDetailMapper.class)); + var rows = repository.payments(10, 20); + assertThat(rows).hasSize(1); + assertThat(rows.get(0).get("transaction_no")).isEqualTo("PAY202610210001"); + assertThat(rows.get(0).get("reference_no")).isEqualTo("BANK-1"); + assertThat(repository.payments(11, 21)).isEmpty(); + assertThat(repository.payments(10, 22)).hasSize(1); + assertThat(repository.payments(10, 22).get(0).get("transaction_no")).isNull(); + } + } +} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/controller/PlatformColoadGroupController.java b/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/controller/PlatformColoadGroupController.java index 62e4cd0a4..6f83c8ce0 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/controller/PlatformColoadGroupController.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/controller/PlatformColoadGroupController.java @@ -67,10 +67,11 @@ public class PlatformColoadGroupController { idempotencyKey, user.userId(), user.username())); } + /** 按入仓码返回同柜组业务编号及当前有效成员,保留平台只读权限边界。 */ @GetMapping("/by-inbound-code/{inboundCode}") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:list')") @Operation(summary = "按入仓码查询同柜组业务摘要", - description = "仅返回业务状态、有效成员数及去重后的全部成员入仓码;内部组ID和组号不作为页面识别键。") + description = "返回真实业务组号、业务状态、有效成员数及去重后的全部成员入仓码;不返回内部组ID。") public ApiResponse byInboundCode( @AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Size(min = 1, max = 40) String inboundCode) { diff --git a/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/model/ColoadGroupModels.java b/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/model/ColoadGroupModels.java index e6e7375d5..7c9d611d6 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/model/ColoadGroupModels.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/model/ColoadGroupModels.java @@ -71,6 +71,7 @@ public final class ColoadGroupModels { /** 页面只读业务摘要;内部组标识不作为用户识别键。 */ public record BusinessSummary( + String groupNo, String status, int memberCount, List members diff --git a/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/persistence/ColoadGroupMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/persistence/ColoadGroupMapper.java index 2be528ce5..2914c9c8f 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/persistence/ColoadGroupMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/persistence/ColoadGroupMapper.java @@ -108,7 +108,7 @@ public interface ColoadGroupMapper { @Param("inboundId") long inboundId); @Select(""" - SELECT DISTINCT group_row.id groupId,group_row.group_status groupStatus + SELECT DISTINCT group_row.id groupId,group_row.group_no groupNo,group_row.group_status groupStatus FROM fl_inbound_order requested JOIN fl_coload_group_member member ON member.tenant_id=requested.tenant_id AND member.inbound_id=requested.id diff --git a/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/persistence/ColoadGroupQueryRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/persistence/ColoadGroupQueryRepository.java new file mode 100644 index 000000000..f6fba0acf --- /dev/null +++ b/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/persistence/ColoadGroupQueryRepository.java @@ -0,0 +1,20 @@ +package com.baiyue.firstleg.loading.coload.persistence; + +import org.springframework.stereotype.Repository; +import java.util.List; + +/** 平台同柜组摘要的只读数据访问,保留原入仓片段和组状态范围。 */ +@Repository +public class ColoadGroupQueryRepository { + private final ColoadGroupMapper mapper; + /** 注入同服务持久化入口。 */ + public ColoadGroupQueryRepository(ColoadGroupMapper mapper) { this.mapper = mapper; } + /** 按业务入仓码查询当前组及其真实业务组号。 */ + public List groupsByInboundCode(String code) { + return mapper.groupsByInboundCode(code); + } + /** 读取被冻结的成员,交由服务核对缺失和重复片段。 */ + public List effectiveMemberCodes(long groupId) { + return mapper.effectiveMemberCodes(groupId); + } +} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/persistence/ColoadPersistenceRows.java b/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/persistence/ColoadPersistenceRows.java index 6ee1eca41..c252139aa 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/persistence/ColoadPersistenceRows.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/persistence/ColoadPersistenceRows.java @@ -59,6 +59,7 @@ public final class ColoadPersistenceRows { @Setter public static class InboundGroupLookupRow { private Long groupId; + private String groupNo; private String groupStatus; } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/service/ColoadGroupService.java b/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/service/ColoadGroupService.java index 9db82153a..851ef5450 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/service/ColoadGroupService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/service/ColoadGroupService.java @@ -47,6 +47,8 @@ public class ColoadGroupService implements ColoadGroupAssignmentPort { @Resource private ColoadGroupMapper mapper; + @Resource + private com.baiyue.firstleg.loading.coload.persistence.ColoadGroupQueryRepository queryRepository; @Resource private PendingReviewOrderClient pendingReviewOrders; @Resource private OrderCargoClient orderCargoClient; @Resource private InboundOrderCreationService inboundOrderCreationService; @@ -118,10 +120,11 @@ public class ColoadGroupService implements ColoadGroupAssignmentPort { return requiredSnapshot(groupId); } + /** 以入仓码读取同柜组业务号及全部有效成员,不返回内部组ID。 */ @Transactional(readOnly = true) public BusinessSummary queryByInboundCode(String inboundCode) { String code = normalizedInboundCode(inboundCode); - var groups = mapper.groupsByInboundCode(code).stream() + var groups = queryRepository.groupsByInboundCode(code).stream() .filter(row -> row.getGroupId() != null && CURRENT_GROUP_STATUSES.contains(row.getGroupStatus())) .toList(); long distinctGroups = groups.stream().map(row -> row.getGroupId()).distinct().limit(2).count(); @@ -131,7 +134,7 @@ public class ColoadGroupService implements ColoadGroupAssignmentPort { if (distinctGroups > 1) throw conflict("入仓码关联多个有效同柜不可拆组"); long groupId = groups.get(0).getGroupId(); String status = groups.get(0).getGroupStatus(); - var memberRows = mapper.effectiveMemberCodes(groupId); + var memberRows = queryRepository.effectiveMemberCodes(groupId); if (memberRows.isEmpty()) throw conflict("同柜不可拆组不存在有效入仓片段"); if (memberRows.stream().anyMatch(row -> row.getMemberId() == null || row.getInboundId() == null || trim(row.getInboundCode()) == null)) { @@ -142,7 +145,7 @@ public class ColoadGroupService implements ColoadGroupAssignmentPort { throw conflict("同柜不可拆组存在重复入仓片段业务号"); } List members = inboundCodes.stream().sorted().map(BusinessMember::new).toList(); - return new BusinessSummary(status, members.size(), members); + return new BusinessSummary(groups.get(0).getGroupNo(), status, members.size(), members); } private long requiredCurrentGroupId(String inboundCode) { diff --git a/firstleg/src/main/resources/openapi/coload-groups-v1.yaml b/firstleg/src/main/resources/openapi/coload-groups-v1.yaml index cf671fb3d..ad063bdc1 100644 --- a/firstleg/src/main/resources/openapi/coload-groups-v1.yaml +++ b/firstleg/src/main/resources/openapi/coload-groups-v1.yaml @@ -45,8 +45,8 @@ paths: summary: 按入仓码查询同柜组业务摘要 description: >- 需要 platform:firstleg:smart-loading:list。使用任一有效成员的全局唯一 inboundCode 查询; - 仅返回组业务状态、有效成员数和全部唯一成员入仓码。任一冻结成员缺少有效 inboundCode 时失败关闭; - 页面不持有或展示 groupId、groupNo、orderId,也不得从技术ID或订单号推导入仓码。 + 返回真实业务组号 groupNo、组业务状态、有效成员数和全部唯一成员入仓码。任一冻结成员缺少有效 inboundCode 时失败关闭; + 页面不持有或展示 groupId、orderId,也不得从技术ID或订单号推导入仓码。 parameters: - name: inboundCode in: path @@ -147,8 +147,9 @@ components: ColoadGroupBusinessSummary: type: object additionalProperties: false - required: [status, memberCount, members] + required: [groupNo, status, memberCount, members] properties: + groupNo: {type: string, pattern: "^CLG[0-9]{12}$", description: 真实同柜业务组号;与创建及重放结果一致} status: {type: string, enum: [ACTIVE, LOCKED, CONSUMED]} memberCount: {type: integer, minimum: 1} members: diff --git a/firstleg/src/test/java/com/baiyue/firstleg/loading/coload/ColoadGroupOpenApiContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/loading/coload/ColoadGroupOpenApiContractTest.java index 67c3004be..92692278c 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/loading/coload/ColoadGroupOpenApiContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/loading/coload/ColoadGroupOpenApiContractTest.java @@ -55,7 +55,7 @@ class ColoadGroupOpenApiContractTest { .doesNotContain("destinationCode") .doesNotContain("expectedVersion") .doesNotContain("platform:firstleg:coload"); - assertThat(summaryProperties).containsOnlyKeys("status", "memberCount", "members"); + assertThat(summaryProperties).containsOnlyKeys("groupNo", "status", "memberCount", "members"); assertThat(memberProperties).containsOnlyKeys("inboundCode"); } } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/loading/coload/ColoadGroupServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/loading/coload/ColoadGroupServiceTest.java index 9a05cf152..be14de3ef 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/loading/coload/ColoadGroupServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/loading/coload/ColoadGroupServiceTest.java @@ -37,7 +37,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -@SpringJUnitConfig({ColoadGroupService.class, ColoadGroupServiceTest.Numbers.class}) +@SpringJUnitConfig({ColoadGroupService.class, ColoadGroupServiceTest.Numbers.class, com.baiyue.firstleg.loading.coload.persistence.ColoadGroupQueryRepository.class}) class ColoadGroupServiceTest { @org.springframework.context.annotation.Configuration static class Numbers { @@ -71,6 +71,7 @@ class ColoadGroupServiceTest { var result = service.queryByInboundCode(" IN-A "); + assertThat(result.groupNo()).isEqualTo("CLG202610210001"); assertThat(result.status()).isEqualTo("ACTIVE"); assertThat(result.memberCount()).isEqualTo(2); assertThat(result.members()).extracting("inboundCode").containsExactly("IN-A", "IN-B"); @@ -574,6 +575,7 @@ class ColoadGroupServiceTest { private static InboundGroupLookupRow lookup(long groupId, String status) { InboundGroupLookupRow value = new InboundGroupLookupRow(); value.setGroupId(groupId); + value.setGroupNo("CLG202610210001"); value.setGroupStatus(status); return value; } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/loading/coload/ColoadGroupSqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/loading/coload/ColoadGroupSqlContractTest.java index 6609b0bcf..b1196cdb6 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/loading/coload/ColoadGroupSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/loading/coload/ColoadGroupSqlContractTest.java @@ -86,7 +86,8 @@ class ColoadGroupSqlContractTest { .contains("requested.inbound_code=#{inboundCode}") .contains("requested.deleted=0") .contains("'ACTIVE','LOCKED','CONSUMED'") - .doesNotContain("group_no", "order_id=#{"); + .contains("group_row.group_no groupNo") + .doesNotContain("order_id=#{"); assertThat(memberSql) .contains("member.id memberId", "member.inbound_id inboundId", "inbound.inbound_code inboundCode") .contains("LEFT JOIN fl_inbound_order inbound")