From 0f1794b1c75c5255459eac033ecb1eab091d7ea6 Mon Sep 17 00:00:00 2001 From: chenyuan Date: Tue, 18 Aug 2026 10:55:36 +0800 Subject: [PATCH] feat: load payment wallet prerequisites on demand --- .../service/PaymentApplicationService.java | 4 +- .../repository/PaymentResourceRepository.java | 2 +- .../JdbcPaymentResourceRepository.java | 37 ++++----- .../rest/PaymentControllerTest.java | 76 +++++++++++++++---- 4 files changed, 84 insertions(+), 35 deletions(-) diff --git a/src/main/java/com/yau/digitalrmb/payment/application/service/PaymentApplicationService.java b/src/main/java/com/yau/digitalrmb/payment/application/service/PaymentApplicationService.java index 10ad823..69fac22 100644 --- a/src/main/java/com/yau/digitalrmb/payment/application/service/PaymentApplicationService.java +++ b/src/main/java/com/yau/digitalrmb/payment/application/service/PaymentApplicationService.java @@ -48,14 +48,14 @@ public class PaymentApplicationService { } public PaymentContext context(String payeeWalletId, PaymentActor actor) { - return resourceRepository.loadContext(actor.getUserId(), payeeWalletId); + return resourceRepository.loadContext(actor, payeeWalletId); } @Transactional public PaymentOrderView create(CreatePaymentCommand command, PaymentActor actor) { if (command == null) throw validation("支付请求不能为空"); BigDecimal amount = amount(command.getAmount()); - PaymentContext context = resourceRepository.loadContext(actor.getUserId(), command.getPayeeWalletId()); + PaymentContext context = resourceRepository.loadContext(actor, command.getPayeeWalletId()); if (!context.getPayer().getWalletId().equals(command.getPayerWalletId())) throw validation("付款钱包不属于当前用户"); Instant now = clock.instant(); UUID id = UUID.randomUUID(); diff --git a/src/main/java/com/yau/digitalrmb/payment/domain/repository/PaymentResourceRepository.java b/src/main/java/com/yau/digitalrmb/payment/domain/repository/PaymentResourceRepository.java index 73a964b..8222eaf 100644 --- a/src/main/java/com/yau/digitalrmb/payment/domain/repository/PaymentResourceRepository.java +++ b/src/main/java/com/yau/digitalrmb/payment/domain/repository/PaymentResourceRepository.java @@ -11,7 +11,7 @@ import com.yau.digitalrmb.payment.domain.model.PayerBankProcessingResult; import java.util.List; public interface PaymentResourceRepository { - PaymentContext loadContext(String payerUserId, String payeeWalletId); + PaymentContext loadContext(PaymentActor actor, String payeeWalletId); String signWithWallet(String walletId, String digest); boolean verifyWalletSignature(String walletId, String digest, String signature); PayerBankProcessingResult processPayerBank(PaymentOrder order, PaymentActor actor); diff --git a/src/main/java/com/yau/digitalrmb/payment/infrastructure/persistence/JdbcPaymentResourceRepository.java b/src/main/java/com/yau/digitalrmb/payment/infrastructure/persistence/JdbcPaymentResourceRepository.java index 436724e..bd21dcf 100644 --- a/src/main/java/com/yau/digitalrmb/payment/infrastructure/persistence/JdbcPaymentResourceRepository.java +++ b/src/main/java/com/yau/digitalrmb/payment/infrastructure/persistence/JdbcPaymentResourceRepository.java @@ -16,6 +16,8 @@ import com.yau.digitalrmb.payment.domain.model.PayerBankProcessingResult; import com.yau.digitalrmb.payment.domain.repository.PaymentResourceRepository; import com.yau.digitalrmb.shared.api.ErrorCode; import com.yau.digitalrmb.shared.exception.BusinessException; +import com.yau.digitalrmb.shared.wallet.WalletPrerequisiteProjectionService; +import com.yau.digitalrmb.shared.wallet.WalletPrerequisiteReference; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; @@ -32,16 +34,22 @@ public class JdbcPaymentResourceRepository implements PaymentResourceRepository private final InstitutionIdentityCryptography cryptography; private final WalletPrivateKeyCipher walletCipher; private final InstitutionKeyService keyService; + private final WalletPrerequisiteProjectionService walletProjection; public JdbcPaymentResourceRepository(JdbcTemplate jdbc, InstitutionIdentityCryptography cryptography, - WalletPrivateKeyCipher walletCipher, InstitutionKeyService keyService) { - this.jdbc = jdbc; this.cryptography = cryptography; this.walletCipher = walletCipher; this.keyService = keyService; + WalletPrivateKeyCipher walletCipher, InstitutionKeyService keyService, + WalletPrerequisiteProjectionService walletProjection) { + this.jdbc = jdbc; this.cryptography = cryptography; this.walletCipher = walletCipher; + this.keyService = keyService; this.walletProjection = walletProjection; } @Override - public PaymentContext loadContext(String payerUserId, String payeeWalletId) { - PaymentParticipant payer = loadParticipant("w.user_id=?", payerUserId); - PaymentParticipant payee = loadParticipant("w.wallet_id=?", payeeWalletId); + public PaymentContext loadContext(PaymentActor actor, String payeeWalletId) { + WalletPrerequisiteReference payerReference = walletProjection.ensureForSubject( + new InstitutionKeySubject(actor.getUserId(), actor.getSchoolId(), actor.getClassId())); + WalletPrerequisiteReference payeeReference = walletProjection.ensureForWallet(payeeWalletId); + PaymentParticipant payer = loadParticipant(payerReference); + PaymentParticipant payee = loadParticipant(payeeReference); if (payer.getWalletId().equals(payee.getWalletId())) throw validation("付款钱包和收款钱包不能相同"); if (payer.getBankCode().equals(payee.getBankCode())) throw validation("本实验仅支持商业银行A向商业银行B的跨行支付"); return new PaymentContext(payer, payee); @@ -194,31 +202,24 @@ public class JdbcPaymentResourceRepository implements PaymentResourceRepository order.getId().toString(), stepCode, stepName, output, actor.getUserId(), actor.getUsername()); } - private PaymentParticipant loadParticipant(String where, String value) { + private PaymentParticipant loadParticipant(WalletPrerequisiteReference reference) { List values = jdbc.query("SELECT w.user_id,w.wallet_id,w.wallet_type,cert.certificate_serial,cert.public_key," + "a.bank_code,a.bank_name,w.balance,COALESCE(w.frozen_amount,0) frozen_amount " + "FROM digital_wallet w JOIN wallet_certificate cert ON cert.wallet_id=w.wallet_id " + "AND cert.status='VALID' AND cert.filing_status='REGISTERED' " + "JOIN wallet_bank_binding binding ON binding.wallet_id=w.wallet_id AND binding.status='BOUND' " + "JOIN simulated_bank_account a ON a.account_id=binding.bank_account_id AND a.status='ACTIVE' " + - "WHERE " + where + " AND w.status='ACTIVE' AND w.central_bank_confirmation_signature IS NOT NULL", + "WHERE w.wallet_id=? AND w.user_id=? AND a.bank_code=? " + + "AND w.status='ACTIVE' AND w.central_bank_confirmation_signature IS NOT NULL", (rs, row) -> new PaymentParticipant(rs.getString("user_id"), rs.getString("wallet_id"), rs.getString("wallet_type"), rs.getString("certificate_serial"), rs.getString("public_key"), - rs.getString("bank_code"), rs.getString("bank_name"), organizationId(rs.getString("bank_code")), - rs.getBigDecimal("balance"), rs.getBigDecimal("frozen_amount")), value); + rs.getString("bank_code"), rs.getString("bank_name"), reference.getOrganizationId(), + rs.getBigDecimal("balance"), rs.getBigDecimal("frozen_amount")), + reference.getWalletId(), reference.getUserId(), reference.getBankCode()); if (values.isEmpty()) throw validation("付款方或收款方未完成钱包开通、证书备案或银行绑定"); return values.get(0); } - private String organizationId(String bankCode) { - List values = jdbc.query("SELECT institution_identifier FROM institution_identifier_application " + - "WHERE bank_code=? AND status IN ('ISSUED','FEEDBACKED') AND deleted=FALSE " + - "AND institution_identifier IS NOT NULL ORDER BY created_at DESC LIMIT 1", - (rs, row) -> rs.getString(1), bankCode); - if (values.isEmpty()) throw validation("请先完成机构标识实验并取得商业银行机构标识"); - return values.get(0); - } - private WalletSnapshot lockWallet(String walletId) { List values = jdbc.query("SELECT balance,COALESCE(frozen_amount,0) frozen_amount,status " + "FROM digital_wallet WHERE wallet_id=? FOR UPDATE", diff --git a/src/test/java/com/yau/digitalrmb/payment/interfaces/rest/PaymentControllerTest.java b/src/test/java/com/yau/digitalrmb/payment/interfaces/rest/PaymentControllerTest.java index 9030dd8..9511d9f 100644 --- a/src/test/java/com/yau/digitalrmb/payment/interfaces/rest/PaymentControllerTest.java +++ b/src/test/java/com/yau/digitalrmb/payment/interfaces/rest/PaymentControllerTest.java @@ -6,6 +6,7 @@ import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyService; import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography; import com.yau.digitalrmb.institutionidentity.domain.InstitutionSm2KeyPair; +import com.yau.digitalrmb.testsupport.WalletOpeningTestData; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -45,8 +46,14 @@ class PaymentControllerTest { @BeforeEach void seedPrerequisites() { cleanPaymentData(); + cleanWalletState(); insertUser(PAYER_USER, "payment-a", "3001", "2001"); insertUser(PAYEE_USER, "payment-b", "3001", "2001"); + jdbc.update("INSERT INTO institution_identifier_application (id,bank_code,user_id,school_id,class_id,request_timestamp," + + "original_text,status,institution_identifier,training_round,scoring_criteria,created_at,updated_at,created_by,updated_by,deleted) " + + "VALUES (?,?,?,?,?,?,?,'ISSUED',?,2,0,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,'other','other',FALSE)", + 991000L, "BKCHCNBJ00001", "other-payment-user", 9999L, 9999L, + "20260814080000", "OTHER_BANK", "ORG_WRONG_SCOPE"); jdbc.update("INSERT INTO institution_identifier_application (id,bank_code,user_id,school_id,class_id,request_timestamp," + "original_text,status,institution_identifier,training_round,scoring_criteria,created_at,updated_at,created_by,updated_by,deleted) " + "VALUES (?,?,?,?,?,?,?,'ISSUED',?,1,0,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,'test','test',FALSE)", @@ -58,8 +65,16 @@ class PaymentControllerTest { InstitutionKeySubject subject = new InstitutionKeySubject(PAYER_USER, 3001L, 2001L); keyService.commercialBankKey(subject, "test"); keyService.centralBankKey(subject, "test"); - insertWallet(PAYER_USER, PAYER_WALLET, "CERT_PAYMENT_A", "BKCHCNBJ00001", "ACCOUNT_PAYMENT_A", new BigDecimal("200.00")); - insertWallet(PAYEE_USER, PAYEE_WALLET, "CERT_PAYMENT_B", "BKCHCNBJ00002", "ACCOUNT_PAYMENT_B", BigDecimal.ZERO); + InstitutionSm2KeyPair payerWalletKey = cryptography.generateSm2KeyPair(); + InstitutionSm2KeyPair payeeWalletKey = cryptography.generateSm2KeyPair(); + WalletOpeningTestData.insertCompletedWallet(jdbc, 991200L, PAYER_USER, 3001L, 2001L, + PAYER_WALLET, payerWalletKey.getPrivateKey(), payerWalletKey.getPublicKey(), + "BKCHCNBJ00001", "6222020000000001", BigDecimal.ZERO); + WalletOpeningTestData.insertCompletedWallet(jdbc, 991300L, PAYEE_USER, 3001L, 2001L, + PAYEE_WALLET, payeeWalletKey.getPrivateKey(), payeeWalletKey.getPublicKey(), + "BKCHCNBJ00002", "6222020000000001", BigDecimal.ZERO); + insertWallet(PAYER_USER, PAYER_WALLET, "CERT_991200", "BKCHCNBJ00001", + "ACCOUNT_PAYMENT_A", new BigDecimal("200.00"), payerWalletKey); insertPayerContract(); for (int index = 1; index <= 2; index++) { jdbc.update("INSERT INTO commercial_bank_currency (currency_id,source_currency_id,source_batch_id,issuance_request_id," + @@ -77,12 +92,8 @@ class PaymentControllerTest { cleanPaymentData(); jdbc.update("DELETE FROM institution_sm2_key_audit WHERE user_id=?", PAYER_USER); jdbc.update("DELETE FROM institution_sm2_key WHERE user_id=?", PAYER_USER); - jdbc.update("DELETE FROM institution_identifier_application WHERE id IN (991001,991002)"); - jdbc.update("DELETE FROM wallet_bank_binding WHERE wallet_id IN (?,?)", PAYER_WALLET, PAYEE_WALLET); - jdbc.update("DELETE FROM wallet_contract WHERE wallet_id=?", PAYER_WALLET); - jdbc.update("DELETE FROM wallet_certificate WHERE wallet_id IN (?,?)", PAYER_WALLET, PAYEE_WALLET); - jdbc.update("DELETE FROM simulated_bank_account WHERE account_id IN ('ACCOUNT_PAYMENT_A','ACCOUNT_PAYMENT_B')"); - jdbc.update("DELETE FROM digital_wallet WHERE wallet_id IN (?,?)", PAYER_WALLET, PAYEE_WALLET); + jdbc.update("DELETE FROM institution_identifier_application WHERE id IN (991000,991001,991002)"); + cleanWalletState(); jdbc.update("DELETE FROM commercial_bank_currency WHERE issuance_request_id='PAYMENT_TEST_ISSUANCE'"); jdbc.update("DELETE FROM sys_user WHERE user_id IN (?,?)", PAYER_USER, PAYEE_USER); } @@ -93,7 +104,12 @@ class PaymentControllerTest { .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.payer.walletId").value(PAYER_WALLET)) - .andExpect(jsonPath("$.data.payee.walletId").value(PAYEE_WALLET)); + .andExpect(jsonPath("$.data.payer.organizationId").value("ORG_PAYMENT_A")) + .andExpect(jsonPath("$.data.payee.walletId").value(PAYEE_WALLET)) + .andExpect(jsonPath("$.data.payee.organizationId").value("ORG_PAYMENT_B")); + assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM digital_wallet WHERE wallet_id=?", Integer.class, PAYEE_WALLET)) + .isEqualTo(1); + assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM payment_order", Integer.class)).isZero(); String created = mockMvc.perform(post("/api/v1/payments").with(payer()).contentType(MediaType.APPLICATION_JSON) .content("{\"payerWalletId\":\"" + PAYER_WALLET + "\",\"payeeWalletId\":\"" + PAYEE_WALLET + "\",\"amount\":200.00,\"note\":\"实验支付\"}")) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("REQUEST_PREPARED")) @@ -140,17 +156,39 @@ class PaymentControllerTest { .isEqualTo(PAYEE_WALLET); } + @Test + void rejectsPaymentWhenPayerHasNoWalletOwnedCoins() throws Exception { + String created = mockMvc.perform(post("/api/v1/payments").with(payer()).contentType(MediaType.APPLICATION_JSON) + .content("{\"payerWalletId\":\"" + PAYER_WALLET + "\",\"payeeWalletId\":\"" + PAYEE_WALLET + + "\",\"amount\":200.00,\"note\":\"no coins\"}")) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + String id = JsonPath.read(created, "$.data.id"); + mockMvc.perform(post("/api/v1/payments/{id}/sign", id).with(payer())) + .andExpect(status().isOk()); + + jdbc.update("DELETE FROM central_bank_currency_ownership WHERE owner_id=?", PAYER_WALLET); + + mockMvc.perform(post("/api/v1/payments/{id}/payer-bank-process", id).with(payer())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(400)); + assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM payment_ownership_transfer WHERE payment_id=?", + Integer.class, id)).isZero(); + assertThat(jdbc.queryForObject("SELECT frozen_amount FROM digital_wallet WHERE wallet_id=?", + BigDecimal.class, PAYER_WALLET)).isEqualByComparingTo("0.00"); + } + private void insertUser(String id, String account, String schoolId, String classId) { jdbc.update("INSERT INTO sys_user (user_id,student_id,password,user_name,class_id,class_name,school_id,school_name,role_id,is_deleted,zy_user_id) " + "VALUES (?,?,?,?,?,'支付测试班',?,'延安大学',4,0,?)", id, account, "unused", account, classId, schoolId, account); } - private void insertWallet(String userId, String walletId, String certificate, String bankCode, String accountId, BigDecimal balance) { - InstitutionSm2KeyPair key = cryptography.generateSm2KeyPair(); + private void insertWallet(String userId, String walletId, String certificate, String bankCode, + String accountId, BigDecimal balance, InstitutionSm2KeyPair key) { jdbc.update("INSERT INTO digital_wallet (wallet_id,user_id,wallet_type,status,balance,frozen_amount,central_bank_confirmation_signature,opened_at,created_at,updated_at) " + - "VALUES (?,?,'TYPE_II','ACTIVE',?,0,'CB_FINAL',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", walletId, userId, balance); + "VALUES (?,?,'TYPE_II','ACTIVE',?,0,'CB_FINAL_TEST',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", walletId, userId, balance); jdbc.update("INSERT INTO wallet_certificate (certificate_serial,wallet_id,public_key,encrypted_private_key,filing_status,central_bank_root_signature,status,issued_at) " + - "VALUES (?,?,?,?,'REGISTERED','CB_ROOT','VALID',CURRENT_TIMESTAMP)", certificate, walletId, key.getPublicKey(), walletCipher.encrypt(key.getPrivateKey())); + "VALUES (?,?,?,?,'REGISTERED','CB_ROOT_TEST','VALID',CURRENT_TIMESTAMP)", certificate, walletId, key.getPublicKey(), walletCipher.encrypt(key.getPrivateKey())); jdbc.update("INSERT INTO simulated_bank_account (account_id,user_id,bank_code,bank_name,card_number,card_last4,balance,frozen_amount,status,created_at,updated_at) " + "VALUES (?,?,?,?,'6222020000000001','0001',0,0,'ACTIVE',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", accountId, userId, bankCode, bankCode); jdbc.update("INSERT INTO wallet_bank_binding (wallet_id,bank_account_id,status,bound_at) VALUES (?,?,'BOUND',CURRENT_TIMESTAMP)", walletId, accountId); @@ -158,7 +196,17 @@ class PaymentControllerTest { private void insertPayerContract() { jdbc.update("INSERT INTO wallet_contract (contract_id,wallet_id,wallet_type,single_payment_limit,daily_payment_limit,annual_payment_limit,balance_limit,valid_until,original_text,digest,status,daily_used_amount,daily_counter_date,annual_used_amount,annual_counter_year,created_at,updated_at) " + - "VALUES ('CONTRACT_PAYMENT_A',?,'TYPE_II',50000,100000,500000,500000,NULL,'CONTRACT','DIGEST','ACTIVE',0,CURRENT_DATE,0,2026,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", PAYER_WALLET); + "VALUES ('CONTRACT_991200',?,'TYPE_II',50000,100000,500000,500000,NULL,'CONTRACT','DIGEST','ACTIVE',0,CURRENT_DATE,0,2026,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", PAYER_WALLET); + } + + private void cleanWalletState() { + jdbc.update("DELETE FROM wallet_bank_binding WHERE wallet_id IN (?,?)", PAYER_WALLET, PAYEE_WALLET); + jdbc.update("DELETE FROM wallet_contract WHERE wallet_id IN (?,?)", PAYER_WALLET, PAYEE_WALLET); + jdbc.update("DELETE FROM wallet_certificate WHERE wallet_id IN (?,?)", PAYER_WALLET, PAYEE_WALLET); + jdbc.update("DELETE FROM simulated_bank_account WHERE user_id IN (?,?)", PAYER_USER, PAYEE_USER); + jdbc.update("DELETE FROM digital_wallet WHERE wallet_id IN (?,?)", PAYER_WALLET, PAYEE_WALLET); + WalletOpeningTestData.deleteWalletFacts(jdbc, PAYER_USER); + WalletOpeningTestData.deleteWalletFacts(jdbc, PAYEE_USER); } private void cleanPaymentData() {