This commit is contained in:
2026-08-17 15:41:42 +08:00
parent 4abcb2b80f
commit 5f79616623
60 changed files with 1457 additions and 270 deletions
@@ -18,6 +18,7 @@ import org.springframework.jdbc.core.RowMapper;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
@@ -111,6 +112,37 @@ class FinancePhase1OperationBehaviorTest {
assertThat(jdbc.ledgerCustomerIds).containsExactly(9L, 9L, 9L);
}
@Test
void ledgerReturnsStableBusinessTypeDocumentAndOperatorNameSnapshot() throws Exception {
JdbcTemplate jdbc = mock(JdbcTemplate.class);
when(jdbc.query(startsWith("SELECT id,bill_no"), any(RowMapper.class), eq(9L))).thenAnswer(invocation -> {
@SuppressWarnings("unchecked") RowMapper<Object> mapper = invocation.getArgument(1);
ResultSet rs = mock(ResultSet.class);
when(rs.getLong("id")).thenReturn(81L);
when(rs.getLong("created_by")).thenReturn(71L);
when(rs.getString("bill_no")).thenReturn("REC-81");
when(rs.getString("created_by_name")).thenReturn("platform_admin");
when(rs.getString("remark")).thenReturn("八月账单");
when(rs.getBigDecimal("total_amount")).thenReturn(new BigDecimal("5200.00"));
when(rs.getTimestamp("created_at")).thenReturn(Timestamp.valueOf("2026-08-17 13:00:00"));
return List.of(mapper.mapRow(rs, 0));
});
when(jdbc.query(startsWith("SELECT id,receipt_no"), any(RowMapper.class), eq(9L))).thenReturn(List.of());
when(jdbc.query(startsWith("SELECT id,cost_no"), any(RowMapper.class), eq(9L))).thenReturn(List.of());
FinancePhase1OperationService service = new FinancePhase1OperationService(jdbc, objectMapper());
PageEnvelope<FinancePhase1Dtos.LedgerEntry> result =
service.listDerivedCustomerAccountLedger(9L, 1, 20, null, null, null);
assertThat(result.data()).singleElement().satisfies(entry -> {
assertThat(entry.businessType()).isEqualTo("SETTLEMENT_ISSUED");
assertThat(entry.documentType()).isEqualTo("BILL");
assertThat(entry.documentNo()).isEqualTo("REC-81");
assertThat(entry.occurredAt()).isEqualTo(LocalDateTime.of(2026, 8, 17, 13, 0));
assertThat(entry.operatorName()).isEqualTo("platform_admin");
});
}
@Test
void rejectedFeeCannotBeIncludedInSettlement() throws Exception {
JdbcTemplate jdbc = feeSourceJdbc("REJECTED", false, 2);
@@ -182,7 +214,7 @@ class FinancePhase1OperationBehaviorTest {
eq("APPROVED"), eq("核对无误"), eq(88L), eq(0))).thenReturn(1);
when(jdbc.update(startsWith("INSERT INTO fin_audit_log"), eq(9L), eq("FIN_FEE_SOURCE"),
eq(88L), eq("FEE_SOURCE_REVIEW_APPROVE"), eq("核对无误"), anyString(), anyString(),
eq(71L), eq("operator-71"), any(), any())).thenReturn(1);
eq(71L), eq("操作人 71"), any(), any())).thenReturn(1);
when(jdbc.update(startsWith("UPDATE fin_idempotency_record SET status='SUCCEEDED'"),
eq(88L), eq(9L), eq(71L), eq("FEE_SOURCE_REVIEW"), eq("review-key"))).thenReturn(1);
FinancePhase1OperationService service = new FinancePhase1OperationService(jdbc, objectMapper());
@@ -71,6 +71,29 @@ class FinancePlatformListContractTest {
.contains("customerId");
}
@Test
void financeDetailsKeepOperatorIdsAndExposeNameSnapshots() {
assertRecordFields(com.baiyue.finance.dto.FinanceReadModels.Statement.class,
"createdBy", "createdByName", "updatedBy", "updatedByName");
assertRecordFields(com.baiyue.finance.dto.FinanceReadModels.BillRevision.class,
"createdBy", "createdByName");
assertRecordFields(com.baiyue.finance.dto.ReconciliationStatementDtos.ReconciliationDetail.class,
"createdBy", "createdByName", "updatedBy", "updatedByName");
assertRecordFields(com.baiyue.finance.dto.ReconciliationStatementDtos.PaymentHistory.class,
"createdBy", "createdByName");
assertRecordFields(com.baiyue.finance.dto.ReconciliationStatementDtos.ReconciliationRevision.class,
"operatorId", "operatorName");
assertRecordFields(com.baiyue.finance.dto.FinanceSettlementDtos.SettlementTransaction.class,
"createdBy", "createdByName");
assertRecordFields(com.baiyue.finance.dto.FinanceSettlementDtos.SettlementRevision.class,
"operatorId", "operatorName");
}
private static void assertRecordFields(Class<?> type, String... names) {
assertThat(Arrays.stream(type.getRecordComponents()).map(java.lang.reflect.RecordComponent::getName))
.contains(names);
}
private static Method find(String name) throws Exception {
return switch (name) {
case "feePool" -> PlatformFinanceController.class.getMethod(name, Long.class, String.class);
@@ -171,7 +171,7 @@ class FinanceServiceValidationTest {
eq(new BigDecimal("40.00")), eq(new BigDecimal("40.00")),
eq(2L), eq(9L), eq(50L), eq(new BigDecimal("40.00")));
verify(jdbc).update(contains("status=CASE WHEN outstanding_amount=0"),
eq(new BigDecimal("40.00")), eq(new BigDecimal("40.00")), eq(2L), eq(50L), eq(9L));
eq(new BigDecimal("40.00")), eq(new BigDecimal("40.00")), eq(2L), isNull(), eq(50L), eq(9L));
}
@Test
@@ -234,11 +234,14 @@ class FinanceSettlementBehaviorTest {
eq("SETTLEMENT_PAYMENT_CREATE"), eq("pay-key"))).thenReturn(List.of());
when(jdbc.queryForList(startsWith("SELECT id,tenant_id,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.of(
"id", 501L, "transaction_type", "PAYMENT", "transaction_date", Date.valueOf(LocalDate.of(2026, 8, 11)),
"amount", new BigDecimal("40.00"), "method", "BANK_TRANSFER", "reference_no", "BANK-1",
"voucher_file_ids", "[\"91\"]", "created_by", 7L,
"created_at", Timestamp.valueOf(LocalDateTime.of(2026, 8, 11, 10, 0)), "remark", "首笔付款")));
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"),
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\"]"),
Map.entry("created_by", 7L), Map.entry("created_by_name", "platform_admin"),
Map.entry("created_at", Timestamp.valueOf(LocalDateTime.of(2026, 8, 11, 10, 0))),
Map.entry("remark", "首笔付款"))));
when(jdbc.queryForList(startsWith("SELECT action,reason"), eq(10L), eq(88L))).thenReturn(List.of());
when(fileClient.metadata("internal-token", 10L, 10L, 91L)).thenReturn(ApiResponse.success(
new FinanceFileClient.Metadata(91L, 10L, "FINANCE_VOUCHER", "10", "pay.png", "abc", "image/png", 10L)));
@@ -252,6 +255,7 @@ class FinanceSettlementBehaviorTest {
assertThat(result.summary().outstandingAmount()).isEqualByComparingTo("60.00");
assertThat(result.transactions()).singleElement().extracting(transaction -> transaction.transactionType())
.isEqualTo("PAYMENT");
assertThat(result.transactions().get(0).createdByName()).isEqualTo("platform_admin");
verify(jdbc).update(startsWith("UPDATE fin_settlement SET paid_amount"),
any(), any(), any(), any(), any(), any(), any(), any());
}
@@ -431,6 +435,7 @@ class FinanceSettlementBehaviorTest {
Map.entry("transaction_date", Date.valueOf("2026-08-11")), Map.entry("amount", new BigDecimal("40.00")),
Map.entry("method", "BANK_TRANSFER"), Map.entry("reference_no", "RCPT-701"),
Map.entry("voucher_file_ids", "[\"91\"]"), Map.entry("created_by", 7L),
Map.entry("created_by_name", "platform_admin"),
Map.entry("created_at", Timestamp.valueOf("2026-08-11 10:00:00")), Map.entry("remark", "首笔收款"))));
FinanceSettlementService service = service(jdbc, org.mockito.Mockito.mock(FinanceFileClient.class));
@@ -439,6 +444,7 @@ class FinanceSettlementBehaviorTest {
assertThat(result.transactions()).singleElement().extracting(transaction -> transaction.transactionType())
.isEqualTo("RECEIPT");
assertThat(result.transactions().get(0).amount()).isEqualByComparingTo("40.00");
assertThat(result.transactions().get(0).createdByName()).isEqualTo("platform_admin");
}
@Test
@@ -44,7 +44,7 @@ class FinanceSqlCommentContractTest {
}
}
assertThat(tableCount).isEqualTo(16);
assertThat(columnCount).isEqualTo(233);
assertThat(columnCount).isEqualTo(239);
assertThat(missing).as("缺少中文业务注释的Finance字段").isEmpty();
}
@@ -70,4 +70,25 @@ class FinanceSqlCommentContractTest {
.contains("ALTER TABLE baiyue_finance.fin_bill_line ADD COLUMN adjustment_reason")
.contains("queue_column_comment('baiyue_finance', 'fin_bill_line', 'adjustment_reason'");
}
@Test
void financeLedgerSourcesPersistOperatorNameSnapshotsInBaselineAndUnifiedSql() throws Exception {
String baseline = Files.readString(Path.of("src/main/resources/db/finance.sql"), StandardCharsets.UTF_8);
Path direct = Path.of("deploy", "sql", "头程数据库统一增量-MySQL8.sql");
Path unifiedPath = Files.exists(direct) ? direct : Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql");
String unified = Files.readString(unifiedPath, StandardCharsets.UTF_8);
assertThat(baseline.split("created_by_name VARCHAR\\(128\\) NULL COMMENT '创建操作人名称快照'", -1))
.hasSize(4);
assertThat(baseline)
.contains("updated_by_name VARCHAR(128) NULL COMMENT '最后修改操作人名称快照'")
.contains("created_by_name VARCHAR(128) NULL COMMENT '登记操作人名称快照'");
assertThat(unified)
.contains("CALL finance_add_operator_snapshot_column('fin_bill', 'created_by_name', 'created_by')")
.contains("CALL finance_add_operator_snapshot_column('fin_bill', 'updated_by_name', 'updated_by')")
.contains("CALL finance_add_operator_snapshot_column('fin_receipt', 'created_by_name', 'created_by')")
.contains("CALL finance_add_operator_snapshot_column('fin_cost', 'created_by_name', 'created_by')")
.contains("CALL finance_add_operator_snapshot_column('fin_settlement_payment', 'created_by_name', 'created_by')")
.contains("CALL finance_add_operator_snapshot_column('fin_bill_revision', 'created_by_name', 'created_by')");
}
}
@@ -33,6 +33,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.RETURNS_DEFAULTS;
import static org.mockito.Mockito.never;
@@ -103,6 +104,8 @@ class ReconciliationStatementBehaviorTest {
assertThat(result.statementId()).isEqualTo(501L);
assertThat(result.statementNo()).isEqualTo("REC-00000000000000000501");
assertThat(result.currency()).isEqualTo("CNY");
assertThat(result.createdByName()).isEqualTo("platform_admin");
assertThat(result.updatedByName()).isEqualTo("platform_admin");
assertThat(result.totalReceivable()).isEqualByComparingTo("100.00");
assertThat(result.lines()).hasSize(1);
verify(jdbc).update(org.mockito.ArgumentMatchers.startsWith("UPDATE fin_fee_source SET status='LOCKED'"), eq(88L), eq(9L));
@@ -147,11 +150,11 @@ class ReconciliationStatementBehaviorTest {
assertThat(result.status()).isEqualTo("PENDING_REVIEW");
assertThat(result.version()).isEqualTo(1);
assertThat(result.periodStart()).isEqualTo(LocalDate.of(2026, 8, 1));
verify(jdbc).update(anyString(), eq(9L), eq(501L), eq(0), eq("提交审核"), anyString(), anyString(), eq(71L));
verify(jdbc).update(anyString(), eq(9L), eq(501L), eq(0), eq("提交审核"), anyString(), anyString(), eq(71L), isNull());
verify(jdbc).update(org.mockito.ArgumentMatchers.startsWith("UPDATE fin_bill SET status="),
eq("PENDING_REVIEW"), eq("PENDING_REVIEW"), eq("PENDING_REVIEW"), eq(71L),
eq("PENDING_REVIEW"), eq("PENDING_REVIEW"), eq(71L), eq("PENDING_REVIEW"),
eq("提交审核"), eq(71L), eq(501L), eq(9L), eq("DRAFT"), eq(0));
eq("提交审核"), eq(71L), isNull(), eq(501L), eq(9L), eq("DRAFT"), eq(0));
verify(jdbc, never()).update(org.mockito.ArgumentMatchers.startsWith("UPDATE fin_fee_source f JOIN"), (Object[]) any());
}
@@ -230,7 +233,7 @@ class ReconciliationStatementBehaviorTest {
verify(jdbc).update(org.mockito.ArgumentMatchers.startsWith("UPDATE fin_bill_line SET amount="),
eq(new BigDecimal("280.00")), eq("运费更正"), eq(601L), eq(501L), eq(9L));
verify(jdbc).update(org.mockito.ArgumentMatchers.startsWith("UPDATE fin_bill SET total_amount="),
eq(new BigDecimal("280.00")), eq(new BigDecimal("280.00")), eq(71L), eq(501L), eq(9L), eq(3));
eq(new BigDecimal("280.00")), eq(new BigDecimal("280.00")), eq(71L), isNull(), eq(501L), eq(9L), eq(3));
}
@Test
@@ -465,6 +468,8 @@ class ReconciliationStatementBehaviorTest {
when(rs.getInt("version")).thenReturn(version);
when(rs.getInt("credit_days")).thenReturn(30);
when(rs.getString("remark")).thenReturn("八月对账");
when(rs.getString("created_by_name")).thenReturn("platform_admin");
when(rs.getString("updated_by_name")).thenReturn("platform_admin");
Timestamp timestamp = Timestamp.valueOf("2026-08-11 10:00:00");
when(rs.getTimestamp("created_at")).thenReturn(timestamp);
when(rs.getTimestamp("updated_at")).thenReturn(timestamp);
@@ -0,0 +1,37 @@
package com.baiyue.finance.service;
import com.baiyue.common.security.AuthenticatedUser;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
class FinanceOperatorSnapshotTest {
@AfterEach
void clearSecurityContext() {
SecurityContextHolder.clearContext();
}
@Test
void capturesAuthenticatedUsernameForMatchingOperator() {
AuthenticatedUser user = new AuthenticatedUser(71L, null, null, null,
"platform_admin", "ADMIN", "WEB", Set.of());
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(user, null));
assertThat(FinanceOperatorSnapshot.currentName(71L)).isEqualTo("platform_admin");
assertThat(FinanceOperatorSnapshot.requiredName(71L)).isEqualTo("platform_admin");
assertThat(FinanceOperatorSnapshot.currentName(72L)).isNull();
}
@Test
void labelsSystemGeneratedEntriesWithoutInventingAnOperator() {
assertThat(FinanceOperatorSnapshot.currentName(0L)).isEqualTo("系统");
assertThat(FinanceOperatorSnapshot.currentName(71L)).isNull();
assertThat(FinanceOperatorSnapshot.requiredName(71L)).isEqualTo("操作人 71");
}
}