From 7a7aed1652b6a5fb16a26f6a6ed56e13ff2ccfe0 Mon Sep 17 00:00:00 2001 From: chenyuan Date: Fri, 18 Sep 2026 16:32:05 +0800 Subject: [PATCH] fix(wallet): let unused shared account follow retrained module 3 card --- .../WalletPrerequisiteProjectionService.java | 22 ++++---- ...lletPrerequisiteProjectionServiceTest.java | 53 +++++++++++++++++-- 2 files changed, 60 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionService.java b/src/main/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionService.java index 706867a..f08e38c 100644 --- a/src/main/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionService.java +++ b/src/main/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionService.java @@ -320,25 +320,25 @@ public class WalletPrerequisiteProjectionService { } private List bankAccounts(String userId, String bankCode) { - return jdbc.query("SELECT account_id,user_id,bank_code,card_number,bank_name,balance,frozen_amount,status " + + return jdbc.query("SELECT account_id,user_id,bank_code,card_number " + "FROM simulated_bank_account WHERE user_id=? AND bank_code=? FOR UPDATE", (rs, row) -> new BankAccount(rs.getString("account_id"), rs.getString("user_id"), - rs.getString("bank_code"), rs.getString("card_number"), - blank(rs.getString("bank_name")) && rs.getBigDecimal("balance").signum() == 0 - && rs.getBigDecimal("frozen_amount").signum() == 0 - && "ACTIVE".equals(rs.getString("status"))), userId, bankCode); + rs.getString("bank_code"), rs.getString("card_number")), userId, bankCode); } private void restoreUnusedBankAccount(BankAccount value, SourceWallet source, Institution institution) { - if (!value.emptyShell || !source.userId.equals(value.userId) - || !institution.bankCode.equals(value.bankCode) || blank(source.application.bankName) + if (!source.userId.equals(value.userId) || !institution.bankCode.equals(value.bankCode) + || source.application.cardNumber.equals(value.cardNumber) + || blank(source.application.bankName) || source.application.accountBalance.signum() < 0) { return; } - // Repair only unused legacy shells. Runtime balances must never be reset from an opening snapshot. + // Only accounts no downstream experiment has touched follow a re-run module 3 (guarded by the + // NOT EXISTS clauses, which cover every balance/freeze mutation path). A matching card never + // rewrites the row, so runtime balances must not be reset from an opening snapshot. jdbc.update("UPDATE simulated_bank_account SET bank_name=?,card_number=?,card_last4=?,balance=?," + "updated_at=CURRENT_TIMESTAMP WHERE account_id=? AND user_id=? AND bank_code=? " + - "AND TRIM(COALESCE(bank_name,''))='' AND balance=0 AND frozen_amount=0 AND status='ACTIVE' " + + "AND status='ACTIVE' " + "AND NOT EXISTS (SELECT 1 FROM currency_exchange_order WHERE bank_account_id=?) " + "AND NOT EXISTS (SELECT 1 FROM exchange_debit_record WHERE bank_account_id=?) " + "AND NOT EXISTS (SELECT 1 FROM wallet_bank_binding WHERE bank_account_id=? AND wallet_id<>?)", @@ -484,11 +484,9 @@ public class WalletPrerequisiteProjectionService { private final String userId; private final String bankCode; private final String cardNumber; - private final boolean emptyShell; - private BankAccount(String accountId, String userId, String bankCode, String cardNumber, boolean emptyShell) { + private BankAccount(String accountId, String userId, String bankCode, String cardNumber) { this.accountId = accountId; this.userId = userId; this.bankCode = bankCode; this.cardNumber = cardNumber; - this.emptyShell = emptyShell; } } diff --git a/src/test/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionServiceTest.java b/src/test/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionServiceTest.java index 7fdf339..2335489 100644 --- a/src/test/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionServiceTest.java +++ b/src/test/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionServiceTest.java @@ -168,11 +168,23 @@ class WalletPrerequisiteProjectionServiceTest { } @ParameterizedTest - @ValueSource(strings = {"balance=1", "frozen_amount=1", "bank_name='已有开户行'", "status='DISABLED'"}) - void nonShellAccountWithDifferentCardIsNeverOverwritten(String state) { + @ValueSource(strings = {"order_history", "debit_history", "status_disabled"}) + void accountWithDownstreamHistoryOrDisabledIsNeverOverwritten(String scenario) { seedCompleted(); String accountId = seedBoundShell(); - jdbc.update("UPDATE simulated_bank_account SET " + state + " WHERE account_id=?", accountId); + jdbc.update("UPDATE wallet_application SET input_bank_card_number='6216610100007654321' WHERE user_id=?", + SUBJECT_USER); + if ("order_history".equals(scenario)) { + insertCancelledOrder(accountId, "00000000-0000-0000-0000-000000000890", "PROJECTION_ORDER"); + } else if ("debit_history".equals(scenario)) { + insertCancelledOrder(accountId, "00000000-0000-0000-0000-000000000891", "PROJECTION_DEBIT"); + jdbc.update("INSERT INTO exchange_debit_record (voucher_no,exchange_id,bank_account_id," + + "before_balance,amount,after_balance,status,created_at,created_by) " + + "VALUES ('VOUCHER_PROJECTION_DEBIT',?,?,50000,1000,49000,'SETTLED'," + + "CURRENT_TIMESTAMP,'test')", "00000000-0000-0000-0000-000000000891", accountId); + } else { + jdbc.update("UPDATE simulated_bank_account SET status='DISABLED' WHERE account_id=?", accountId); + } assertThatThrownBy(() -> service.ensureForSubject(subject())) .isInstanceOf(BusinessException.class).hasMessageContaining("共享银行账户与模块三银行卡结果不一致"); @@ -180,6 +192,41 @@ class WalletPrerequisiteProjectionServiceTest { String.class, accountId)).isEqualTo("6222020000000000000"); } + @Test + void retrainedCardOnUnusedProjectedAccountIsOverwrittenInPlace() { + seedCompleted(); + service.ensureForSubject(subject()); + String accountId = jdbc.queryForObject( + "SELECT account_id FROM simulated_bank_account WHERE user_id=?", String.class, SUBJECT_USER); + + jdbc.update("UPDATE wallet_application SET input_bank_card_number='6216610100007654321' WHERE user_id=?", + SUBJECT_USER); + service.ensureForSubject(subject()); + + assertThat(jdbc.queryForObject("SELECT card_number FROM simulated_bank_account WHERE account_id=?", + String.class, accountId)).isEqualTo("6216610100007654321"); + assertThat(jdbc.queryForObject("SELECT card_last4 FROM simulated_bank_account WHERE account_id=?", + String.class, accountId)).isEqualTo("4321"); + assertThat(jdbc.queryForObject("SELECT bank_name FROM simulated_bank_account WHERE account_id=?", + String.class, accountId)).isEqualTo("测试银行"); + ExchangeContext context = exchangeResources.loadContext(new ExchangeActor(SUBJECT_USER, "test", SCHOOL_ID, CLASS_ID)); + assertThat(context.getBankCardNumber()).isEqualTo("6216610100007654321"); + + jdbc.update("UPDATE simulated_bank_account SET balance=49000.00 WHERE account_id=?", accountId); + service.ensureForSubject(subject()); + assertThat(jdbc.queryForObject("SELECT balance FROM simulated_bank_account WHERE account_id=?", + BigDecimal.class, accountId)).isEqualByComparingTo("49000.00"); + } + + private void insertCancelledOrder(String accountId, String orderId, String requestNo) { + jdbc.update("INSERT INTO currency_exchange_order (id,request_no,user_id,wallet_id,bank_account_id,bank_code," + + "organization_id,amount,currency,request_timestamp,message_text,signing_original_text,digest," + + "status,created_at,updated_at,created_by,updated_by) VALUES " + + "(?,?,?,?,?,?,?,1,'CNY','20260904090000','TEST','TEST','TEST','CANCELLED'," + + "CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,'test','test')", + orderId, requestNo, SUBJECT_USER, WALLET_ID, accountId, BANK_CODE, "ORG_EXPECTED"); + } + @Test void evenCancelledExchangeHistoryPreventsShellRestoration() { seedCompleted();