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 f48a933..706867a 100644 --- a/src/main/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionService.java +++ b/src/main/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionService.java @@ -192,15 +192,23 @@ public class WalletPrerequisiteProjectionService { private void ensureWallet(SourceWallet source) { List existing = jdbc.query( - "SELECT wallet_id,user_id,wallet_type,central_bank_confirmation_signature " + + "SELECT wallet_id,user_id,wallet_type,central_bank_confirmation_signature,balance,frozen_amount " + "FROM digital_wallet WHERE wallet_id=? OR user_id=?", (rs, row) -> new Wallet(rs.getString("wallet_id"), rs.getString("user_id"), - rs.getString("wallet_type"), rs.getString("central_bank_confirmation_signature")), + rs.getString("wallet_type"), rs.getString("central_bank_confirmation_signature"), + rs.getBigDecimal("balance"), rs.getBigDecimal("frozen_amount")), source.walletId, source.userId); if (!existing.isEmpty()) { if (existing.size() != 1) throw conflict("共享钱包标识或用户归属冲突"); - validateWallet(existing.get(0), source); - return; + Wallet value = existing.get(0); + if (!isRetrainedUnusedWallet(value, source)) { + validateWallet(value, source); + return; + } + // Module 3 was redone after this wallet was first projected here, and the stale wallet + // was never actually used downstream, so retire it and let the fresh module 3 result + // through instead of surfacing a spurious "共享钱包与模块三最终结果不一致" conflict. + retireUnusedWalletChain(value.walletId); } try { jdbc.update("INSERT INTO digital_wallet " + @@ -213,6 +221,25 @@ public class WalletPrerequisiteProjectionService { } } + private boolean isRetrainedUnusedWallet(Wallet value, SourceWallet source) { + if (!source.userId.equals(value.userId) || source.walletId.equals(value.walletId)) return false; + if (value.balance.signum() != 0 || value.frozenAmount.signum() != 0) return false; + Integer usageCount = jdbc.queryForObject( + "SELECT (SELECT COUNT(*) FROM currency_exchange_order WHERE wallet_id=?) + " + + "(SELECT COUNT(*) FROM payment_order WHERE payer_wallet_id=? OR payee_wallet_id=?) + " + + "(SELECT COUNT(*) FROM wallet_balance_ledger WHERE wallet_id=?) + " + + "(SELECT COUNT(*) FROM payment_wallet_ledger WHERE wallet_id=?)", + Integer.class, value.walletId, value.walletId, value.walletId, value.walletId, value.walletId); + return usageCount != null && usageCount == 0; + } + + private void retireUnusedWalletChain(String staleWalletId) { + jdbc.update("DELETE FROM wallet_bank_binding WHERE wallet_id=?", staleWalletId); + jdbc.update("DELETE FROM wallet_contract WHERE wallet_id=?", staleWalletId); + jdbc.update("DELETE FROM wallet_certificate WHERE wallet_id=?", staleWalletId); + jdbc.update("DELETE FROM digital_wallet WHERE wallet_id=?", staleWalletId); + } + private void ensureCertificate(SourceWallet source) { List existing = jdbc.query( "SELECT certificate_serial,wallet_id,public_key,central_bank_root_signature " + @@ -470,9 +497,13 @@ public class WalletPrerequisiteProjectionService { private final String userId; private final String walletType; private final String finalSignature; - private Wallet(String walletId, String userId, String walletType, String finalSignature) { + private final BigDecimal balance; + private final BigDecimal frozenAmount; + private Wallet(String walletId, String userId, String walletType, String finalSignature, + BigDecimal balance, BigDecimal frozenAmount) { this.walletId = walletId; this.userId = userId; this.walletType = walletType; this.finalSignature = finalSignature; + this.balance = balance; this.frozenAmount = frozenAmount; } } 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 e56d428..7fdf339 100644 --- a/src/test/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionServiceTest.java +++ b/src/test/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionServiceTest.java @@ -250,6 +250,55 @@ class WalletPrerequisiteProjectionServiceTest { return jdbc.queryForObject("SELECT account_id FROM simulated_bank_account WHERE user_id=?", String.class, SUBJECT_USER); } + @Test + void retrainedModuleThreeReplacesUnusedStaleWalletInsteadOfConflicting() { + seedCompleted(); + service.ensureForSubject(subject()); + + String newWalletId = "WALLET_PROJECTION_TEST_RETRAIN"; + WalletOpeningTestData.deleteWalletFacts(jdbc, SUBJECT_USER); + WalletOpeningTestData.insertCompletedWallet(jdbc, 980100L, SUBJECT_USER, SCHOOL_ID, CLASS_ID, + newWalletId, keyPair.getPrivateKey(), keyPair.getPublicKey(), "测试银行", + "6216610100001234567", new BigDecimal("50000.00")); + + WalletPrerequisiteReference reference = service.ensureForSubject(subject()); + + assertThat(reference.getWalletId()).isEqualTo(newWalletId); + assertThat(count("digital_wallet", "wallet_id", WALLET_ID)).isZero(); + assertThat(count("digital_wallet", "wallet_id", newWalletId)).isEqualTo(1); + assertThat(count("wallet_certificate", "wallet_id", newWalletId)).isEqualTo(1); + assertThat(count("wallet_contract", "wallet_id", newWalletId)).isEqualTo(1); + assertThat(count("wallet_bank_binding", "wallet_id", newWalletId)).isEqualTo(1); + jdbc.update("DELETE FROM wallet_bank_binding WHERE wallet_id=?", newWalletId); + jdbc.update("DELETE FROM wallet_contract WHERE wallet_id=?", newWalletId); + jdbc.update("DELETE FROM wallet_certificate WHERE wallet_id=?", newWalletId); + jdbc.update("DELETE FROM digital_wallet WHERE wallet_id=?", newWalletId); + } + + @Test + void retrainedModuleThreeStillConflictsWhenStaleWalletWasActuallyUsed() { + seedCompleted(); + service.ensureForSubject(subject()); + String accountId = jdbc.queryForObject( + "SELECT account_id FROM simulated_bank_account WHERE user_id=?", String.class, SUBJECT_USER); + 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 " + + "('00000000-0000-0000-0000-000000000880','PROJECTION_USED',?,?,?,?," + + "'ORG_EXPECTED',1,'CNY','20260904090000','TEST','TEST','TEST','CANCELLED'," + + "CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,'test','test')", + SUBJECT_USER, WALLET_ID, accountId, BANK_CODE); + + String newWalletId = "WALLET_PROJECTION_TEST_RETRAIN_USED"; + WalletOpeningTestData.deleteWalletFacts(jdbc, SUBJECT_USER); + WalletOpeningTestData.insertCompletedWallet(jdbc, 980200L, SUBJECT_USER, SCHOOL_ID, CLASS_ID, + newWalletId, keyPair.getPrivateKey(), keyPair.getPublicKey(), "测试银行", + "6216610100001234567", new BigDecimal("50000.00")); + + assertThatThrownBy(() -> service.ensureForSubject(subject())) + .isInstanceOf(BusinessException.class).hasMessageContaining("共享钱包与模块三最终结果不一致"); + } + @Test void sameBankCodeFromAnotherScopeCannotSupplyOrganizationId() { seedCompleted();