fix(numbering): expose coload and settlement payment business numbers
This commit is contained in:
@@ -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<String> voucherFileIds, long createdBy, String createdByName,
|
||||
LocalDateTime createdAt,
|
||||
|
||||
@@ -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<Map<String, Object>> 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<Map<String, Object>> 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<Map<String, Object>> 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<Map<String, Object>> 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<Map<String, Object>> audits(@Param("customerId") long customerId, @Param("settlementId") long settlementId);
|
||||
}
|
||||
+29
@@ -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<Map<String, Object>> rows(List<Map<String, Object>> rows) {
|
||||
return rows.stream().map(row -> { Map<String, Object> copy = new LinkedCaseInsensitiveMap<>(); copy.putAll(row); return copy; }).toList();
|
||||
}
|
||||
/** 读取平台结算主单及来源摘要。 */
|
||||
public List<Map<String, Object>> summary(long settlementId) { return rows(mapper.summary(settlementId)); }
|
||||
/** 读取该客户该结算的费用明细。 */
|
||||
public List<Map<String, Object>> lines(long customerId, long settlementId) { return rows(mapper.lines(customerId, settlementId)); }
|
||||
/** 读取该客户付款流水及落库业务号,不能用ID替代。 */
|
||||
public List<Map<String, Object>> payments(long customerId, long settlementId) { return rows(mapper.payments(customerId, settlementId)); }
|
||||
/** 读取对账单已确认收款;收款凭证号不是PAY业务号。 */
|
||||
public List<Map<String, Object>> receipts(long customerId, long statementId) { return rows(mapper.receipts(customerId, statementId)); }
|
||||
/** 读取该客户结算修订历史。 */
|
||||
public List<Map<String, Object>> audits(long customerId, long settlementId) { return rows(mapper.audits(customerId, settlementId)); }
|
||||
}
|
||||
@@ -45,23 +45,9 @@ import java.util.UUID;
|
||||
public class FinanceSettlementService {
|
||||
private static final String CNY = "CNY";
|
||||
private static final Set<String> 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<String, Object> row = one(
|
||||
SETTLEMENT_SUMMARY_SELECT + "WHERE s.id=?",
|
||||
"结算单不存在", settlementId);
|
||||
SettlementSummary summary = summary(row);
|
||||
List<SettlementLine> 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<Map<String, Object>> records = details.summary(settlementId);
|
||||
if (records.isEmpty()) throw error("FIN_404", "结算单不存在");
|
||||
SettlementSummary summary = summary(records.get(0));
|
||||
List<SettlementLine> lines = details.lines(summary.customerId(), settlementId)
|
||||
.stream().map(source -> line(source, summary)).toList();
|
||||
List<SettlementTransaction> 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<SettlementTransaction> 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<Map<String, Object>> 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<Map<String, Object>> audits = details.audits(summary.customerId(), settlementId);
|
||||
List<SettlementRevision> 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<String, Object> 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"),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -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<BusinessSummary> byInboundCode(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Size(min = 1, max = 40) String inboundCode) {
|
||||
|
||||
@@ -71,6 +71,7 @@ public final class ColoadGroupModels {
|
||||
|
||||
/** 页面只读业务摘要;内部组标识不作为用户识别键。 */
|
||||
public record BusinessSummary(
|
||||
String groupNo,
|
||||
String status,
|
||||
int memberCount,
|
||||
List<BusinessMember> members
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+20
@@ -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<ColoadPersistenceRows.InboundGroupLookupRow> groupsByInboundCode(String code) {
|
||||
return mapper.groupsByInboundCode(code);
|
||||
}
|
||||
/** 读取被冻结的成员,交由服务核对缺失和重复片段。 */
|
||||
public List<ColoadPersistenceRows.MemberCodeRow> effectiveMemberCodes(long groupId) {
|
||||
return mapper.effectiveMemberCodes(groupId);
|
||||
}
|
||||
}
|
||||
+1
@@ -59,6 +59,7 @@ public final class ColoadPersistenceRows {
|
||||
@Setter
|
||||
public static class InboundGroupLookupRow {
|
||||
private Long groupId;
|
||||
private String groupNo;
|
||||
private String groupStatus;
|
||||
}
|
||||
|
||||
|
||||
+6
-3
@@ -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<BusinessMember> 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) {
|
||||
|
||||
@@ -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:
|
||||
|
||||
+1
-1
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -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;
|
||||
}
|
||||
|
||||
+2
-1
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user