feat: load payment wallet prerequisites on demand

agent/payment-training-progress
chenyuan 2 weeks ago
parent 7f6738c2d6
commit 0f1794b1c7

@ -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();

@ -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);

@ -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<PaymentParticipant> 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<String> 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<WalletSnapshot> values = jdbc.query("SELECT balance,COALESCE(frozen_amount,0) frozen_amount,status " +
"FROM digital_wallet WHERE wallet_id=? FOR UPDATE",

@ -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() {

Loading…
Cancel
Save