From c40b1d2878b7e4d132f348c4e38b08ea1e09c6ca Mon Sep 17 00:00:00 2001 From: chenyuan Date: Thu, 24 Sep 2026 16:09:19 +0800 Subject: [PATCH] fix: use built-in payer for payment training --- .../JdbcPaymentResourceRepository.java | 25 ++- .../persistence/TeachingPaymentPayer.java | 202 ++++++++++++++++++ 2 files changed, 220 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/yau/digitalrmb/payment/infrastructure/persistence/TeachingPaymentPayer.java 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 56bd367..ae7cd59 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 @@ -38,23 +38,30 @@ public class JdbcPaymentResourceRepository implements PaymentResourceRepository private final WalletPrerequisiteProjectionService walletProjection; private final CurrencyOwnershipLockRepository ownershipLocks; private final TeachingPaymentPayeeWhitelist teachingPayeeWhitelist; + private final TeachingPaymentPayer teachingPaymentPayer; public JdbcPaymentResourceRepository(JdbcTemplate jdbc, InstitutionIdentityCryptography cryptography, WalletPrivateKeyCipher walletCipher, InstitutionKeyService keyService, WalletPrerequisiteProjectionService walletProjection, CurrencyOwnershipLockRepository ownershipLocks, - TeachingPaymentPayeeWhitelist teachingPayeeWhitelist) { + TeachingPaymentPayeeWhitelist teachingPayeeWhitelist, + TeachingPaymentPayer teachingPaymentPayer) { this.jdbc = jdbc; this.cryptography = cryptography; this.walletCipher = walletCipher; this.keyService = keyService; this.walletProjection = walletProjection; this.ownershipLocks = ownershipLocks; this.teachingPayeeWhitelist = teachingPayeeWhitelist; + this.teachingPaymentPayer = teachingPaymentPayer; } @Override public PaymentParticipant loadPayer(PaymentActor actor) { - WalletPrerequisiteReference reference = walletProjection.ensureForSubject( - new InstitutionKeySubject(actor.getUserId(), actor.getSchoolId(), actor.getClassId())); - return loadParticipant(reference); + WalletPrerequisiteReference reference = teachingPaymentPayer.ensure( + new InstitutionKeySubject(actor.getUserId(), actor.getSchoolId(), actor.getClassId()), + actor.getUsername()); + // The teaching wallet is a built-in case wallet. The payment order still + // belongs to the current learner so the existing action-chain ownership + // and notification flow remains intact. + return loadParticipant(reference, actor.getUserId()); } @Override @@ -334,19 +341,23 @@ public class JdbcPaymentResourceRepository implements PaymentResourceRepository } private PaymentParticipant loadParticipant(WalletPrerequisiteReference reference) { + return loadParticipant(reference, reference.getUserId()); + } + + private PaymentParticipant loadParticipant(WalletPrerequisiteReference reference, String participantUserId) { 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 w.wallet_id=? AND w.user_id=? AND a.bank_code=? " + + "WHERE w.wallet_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, row) -> new PaymentParticipant(participantUserId, rs.getString("wallet_id"), rs.getString("wallet_type"), rs.getString("certificate_serial"), rs.getString("public_key"), rs.getString("bank_code"), rs.getString("bank_name"), reference.getOrganizationId(), rs.getBigDecimal("balance"), rs.getBigDecimal("frozen_amount")), - reference.getWalletId(), reference.getUserId(), reference.getBankCode()); + reference.getWalletId(), reference.getBankCode()); if (values.isEmpty()) throw validation("付款方或收款方未完成钱包开通、证书备案或银行绑定"); return values.get(0); } diff --git a/src/main/java/com/yau/digitalrmb/payment/infrastructure/persistence/TeachingPaymentPayer.java b/src/main/java/com/yau/digitalrmb/payment/infrastructure/persistence/TeachingPaymentPayer.java new file mode 100644 index 0000000..6a6da69 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/payment/infrastructure/persistence/TeachingPaymentPayer.java @@ -0,0 +1,202 @@ +package com.yau.digitalrmb.payment.infrastructure.persistence; + +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.shared.api.ErrorCode; +import com.yau.digitalrmb.shared.exception.BusinessException; +import com.yau.digitalrmb.shared.wallet.WalletPrivateKeyCipher; +import com.yau.digitalrmb.shared.wallet.WalletPrerequisiteReference; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.UUID; + +/** + * Module five's built-in payer. The prototype deliberately uses a teaching + * case, not the learner's personal wallet from module three. The wallet is + * shared by the case, while payment orders remain owned by the current learner + * through the participant returned by JdbcPaymentResourceRepository. + */ +@Component +public class TeachingPaymentPayer { + public static final String WALLET_ID = + "WALLET_A_3A4B5C6D7E8F9A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0C1D2E3F4"; + private static final String CASE_USER_ID = "00000000-0000-0000-0000-000000000950"; + private static final String BANK_CODE = "BKCHCNBJ00001"; + private static final String BANK_NAME = "中国银行(BKCHCNBJ00001)"; + private static final String CERTIFICATE_SERIAL = "00A1B2C3D4E5F678"; + private static final String ACCOUNT_ID = "ACCOUNT_TEACHING_PAYMENT_PAYER_A"; + private static final String CARD_NUMBER = "6216610100001234567"; + private static final String WALLET_TYPE = "二类钱包"; + private static final BigInteger COIN_SOURCE_BASE = new BigInteger("950000000000"); + + private final JdbcTemplate jdbc; + private final InstitutionIdentityCryptography cryptography; + private final WalletPrivateKeyCipher walletCipher; + private final InstitutionKeyService keyService; + + public TeachingPaymentPayer(JdbcTemplate jdbc, + InstitutionIdentityCryptography cryptography, + WalletPrivateKeyCipher walletCipher, + InstitutionKeyService keyService) { + this.jdbc = jdbc; + this.cryptography = cryptography; + this.walletCipher = walletCipher; + this.keyService = keyService; + } + + @Transactional + public WalletPrerequisiteReference ensure(InstitutionKeySubject subject, String operator) { + Institution institution = institution(subject); + ensureWallet(); + ensureCertificate(); + ensureAccount(); + ensureContract(); + ensureCoins(subject, institution.organizationId, operator); + return new WalletPrerequisiteReference(CASE_USER_ID, 0L, 0L, WALLET_ID, + BANK_CODE, institution.organizationId); + } + + public boolean isTeachingWallet(String walletId) { + return WALLET_ID.equals(walletId); + } + + private Institution institution(InstitutionKeySubject subject) { + return jdbc.query( + "SELECT bank_code,institution_identifier FROM institution_identifier_application " + + "WHERE user_id=? AND school_id=? AND class_id=? " + + "AND status IN ('ISSUED','FEEDBACKED') AND deleted=FALSE " + + "AND bank_code IS NOT NULL AND institution_identifier IS NOT NULL " + + "ORDER BY created_at DESC LIMIT 1", + (rs, row) -> new Institution(rs.getString("bank_code"), + rs.getString("institution_identifier")), + subject.getUserId(), subject.getSchoolId(), subject.getClassId()) + .stream().findFirst() + .orElseThrow(() -> new BusinessException(ErrorCode.VALIDATION_ERROR, + "请先完成模块一机构标识并取得当前机构标识")); + } + + private void ensureWallet() { + Integer count = jdbc.queryForObject("SELECT COUNT(*) FROM digital_wallet WHERE wallet_id=?", + Integer.class, WALLET_ID); + if (count != null && count > 0) return; + try { + 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 (?,?,?,'ACTIVE',10000.00,0,'TEACHING_PAYMENT_ACTIVATED',CURRENT_TIMESTAMP," + + "CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", + WALLET_ID, CASE_USER_ID, WALLET_TYPE); + } catch (DuplicateKeyException exception) { + if (jdbc.queryForObject("SELECT COUNT(*) FROM digital_wallet WHERE wallet_id=?", + Integer.class, WALLET_ID) == 0) throw exception; + } + } + + private void ensureCertificate() { + Integer count = jdbc.queryForObject("SELECT COUNT(*) FROM wallet_certificate WHERE wallet_id=?", + Integer.class, WALLET_ID); + if (count != null && count > 0) return; + InstitutionSm2KeyPair pair = cryptography.generateSm2KeyPair(); + try { + 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','TEACHING_PAYMENT_ROOT','VALID',CURRENT_TIMESTAMP)", + CERTIFICATE_SERIAL, WALLET_ID, pair.getPublicKey(), walletCipher.encrypt(pair.getPrivateKey())); + } catch (DuplicateKeyException exception) { + if (jdbc.queryForObject("SELECT COUNT(*) FROM wallet_certificate WHERE wallet_id=?", + Integer.class, WALLET_ID) == 0) throw exception; + } + } + + private void ensureAccount() { + Integer count = jdbc.queryForObject("SELECT COUNT(*) FROM simulated_bank_account WHERE account_id=?", + Integer.class, ACCOUNT_ID); + if (count == null || count == 0) { + try { + 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 (?,?,?,?,?,?,50000.00,0,'ACTIVE'," + + "CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", + ACCOUNT_ID, CASE_USER_ID, BANK_CODE, BANK_NAME, CARD_NUMBER, "4567"); + } catch (DuplicateKeyException exception) { + if (jdbc.queryForObject("SELECT COUNT(*) FROM simulated_bank_account WHERE account_id=?", + Integer.class, ACCOUNT_ID) == 0) throw exception; + } + } + Integer binding = jdbc.queryForObject("SELECT COUNT(*) FROM wallet_bank_binding " + + "WHERE wallet_id=? AND bank_account_id=? AND status='BOUND'", + Integer.class, WALLET_ID, ACCOUNT_ID); + if (binding == null || binding == 0) { + jdbc.update("INSERT INTO wallet_bank_binding (wallet_id,bank_account_id,status,bound_at) " + + "VALUES (?,?,'BOUND',CURRENT_TIMESTAMP)", WALLET_ID, ACCOUNT_ID); + } + } + + private void ensureContract() { + Integer count = jdbc.queryForObject("SELECT COUNT(*) FROM wallet_contract WHERE wallet_id=?", + Integer.class, WALLET_ID); + if (count != null && count > 0) return; + String original = "CONTRACT|" + WALLET_ID + "|" + WALLET_TYPE + "|50000.00|100000.00|500000.00|500000.00|长期有效|TEACHING"; + 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 (?,?,?,?,?,?,?,NULL,?,?, 'ACTIVE',0,NULL,0,NULL,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", + "CONTRACT_TEACHING_PAYMENT_A", WALLET_ID, WALLET_TYPE, "50000.00", "100000.00", + "500000.00", "500000.00", original, cryptography.sm3(original)); + } + + private void ensureCoins(InstitutionKeySubject subject, String organizationId, String operator) { + Integer count = jdbc.queryForObject("SELECT COUNT(*) FROM central_bank_currency_ownership " + + "WHERE owner_type='WALLET' AND owner_id=? AND status IN ('AVAILABLE','EXCHANGE_LOCKED')", + Integer.class, WALLET_ID); + if (count != null && count > 0) return; + for (int index = 1; index <= 10; index++) { + String currencyId = "PAYMENT_TEACHING_A_" + String.format("%02d", index); + String original = "TEACHING_ISSUE|" + currencyId + "|" + organizationId + "|1000.00"; + String requestId = UUID.nameUUIDFromBytes((currencyId + "|" + organizationId) + .getBytes(StandardCharsets.UTF_8)).toString(); + long sourceId = COIN_SOURCE_BASE.add(BigInteger.valueOf(index)).longValue(); + Integer exists = jdbc.queryForObject("SELECT COUNT(*) FROM commercial_bank_currency WHERE currency_id=?", + Integer.class, currencyId); + if (exists == null || exists == 0) { + jdbc.update("INSERT INTO commercial_bank_currency " + + "(currency_id,source_currency_id,source_batch_id,issuance_request_id,bank_code," + + "organization_id,denomination,currency,complete_currency,issuance_ownership_original_text," + + "issuance_ownership_signature,status,received_at,received_by_user_id,received_by) " + + "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP,?,?)", + currencyId, sourceId, COIN_SOURCE_BASE.longValue(), requestId, BANK_CODE, organizationId, + "1000.00", "DC", "TEACHING_CURRENCY|" + currencyId, original, + keyService.signCentralBank(subject, original), "可用", subject.getUserId(), operator); + } + Integer owned = jdbc.queryForObject("SELECT COUNT(*) FROM central_bank_currency_ownership WHERE currency_id=?", + Integer.class, currencyId); + if (owned == null || owned == 0) { + jdbc.update("INSERT INTO central_bank_currency_ownership " + + "(currency_id,owner_type,owner_id,status,lock_business_type,lock_business_id,last_transaction_id,updated_at) " + + "VALUES (?,'WALLET',?,'AVAILABLE',NULL,NULL,NULL,CURRENT_TIMESTAMP)", + currencyId, WALLET_ID); + } + } + } + + private static final class Institution { + private final String bankCode; + private final String organizationId; + + private Institution(String bankCode, String organizationId) { + this.bankCode = bankCode; + this.organizationId = organizationId; + } + } +}