fix: use built-in payer for payment training

chore/issuance-comments-zh
chenyuan 3 weeks ago
parent 9931e835d0
commit c40b1d2878

@ -38,23 +38,30 @@ public class JdbcPaymentResourceRepository implements PaymentResourceRepository
private final WalletPrerequisiteProjectionService walletProjection; private final WalletPrerequisiteProjectionService walletProjection;
private final CurrencyOwnershipLockRepository ownershipLocks; private final CurrencyOwnershipLockRepository ownershipLocks;
private final TeachingPaymentPayeeWhitelist teachingPayeeWhitelist; private final TeachingPaymentPayeeWhitelist teachingPayeeWhitelist;
private final TeachingPaymentPayer teachingPaymentPayer;
public JdbcPaymentResourceRepository(JdbcTemplate jdbc, InstitutionIdentityCryptography cryptography, public JdbcPaymentResourceRepository(JdbcTemplate jdbc, InstitutionIdentityCryptography cryptography,
WalletPrivateKeyCipher walletCipher, InstitutionKeyService keyService, WalletPrivateKeyCipher walletCipher, InstitutionKeyService keyService,
WalletPrerequisiteProjectionService walletProjection, WalletPrerequisiteProjectionService walletProjection,
CurrencyOwnershipLockRepository ownershipLocks, CurrencyOwnershipLockRepository ownershipLocks,
TeachingPaymentPayeeWhitelist teachingPayeeWhitelist) { TeachingPaymentPayeeWhitelist teachingPayeeWhitelist,
TeachingPaymentPayer teachingPaymentPayer) {
this.jdbc = jdbc; this.cryptography = cryptography; this.walletCipher = walletCipher; this.jdbc = jdbc; this.cryptography = cryptography; this.walletCipher = walletCipher;
this.keyService = keyService; this.walletProjection = walletProjection; this.keyService = keyService; this.walletProjection = walletProjection;
this.ownershipLocks = ownershipLocks; this.ownershipLocks = ownershipLocks;
this.teachingPayeeWhitelist = teachingPayeeWhitelist; this.teachingPayeeWhitelist = teachingPayeeWhitelist;
this.teachingPaymentPayer = teachingPaymentPayer;
} }
@Override @Override
public PaymentParticipant loadPayer(PaymentActor actor) { public PaymentParticipant loadPayer(PaymentActor actor) {
WalletPrerequisiteReference reference = walletProjection.ensureForSubject( WalletPrerequisiteReference reference = teachingPaymentPayer.ensure(
new InstitutionKeySubject(actor.getUserId(), actor.getSchoolId(), actor.getClassId())); new InstitutionKeySubject(actor.getUserId(), actor.getSchoolId(), actor.getClassId()),
return loadParticipant(reference); 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 @Override
@ -334,19 +341,23 @@ public class JdbcPaymentResourceRepository implements PaymentResourceRepository
} }
private PaymentParticipant loadParticipant(WalletPrerequisiteReference reference) { private PaymentParticipant loadParticipant(WalletPrerequisiteReference reference) {
return loadParticipant(reference, reference.getUserId());
}
private PaymentParticipant loadParticipant(WalletPrerequisiteReference reference, String participantUserId) {
List<PaymentParticipant> values = jdbc.query("SELECT w.user_id,w.wallet_id,w.wallet_type,cert.certificate_serial,cert.public_key," + 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 " + "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 " + "FROM digital_wallet w JOIN wallet_certificate cert ON cert.wallet_id=w.wallet_id " +
"AND cert.status='VALID' AND cert.filing_status='REGISTERED' " + "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 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' " + "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", "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("wallet_type"), rs.getString("certificate_serial"), rs.getString("public_key"),
rs.getString("bank_code"), rs.getString("bank_name"), reference.getOrganizationId(), rs.getString("bank_code"), rs.getString("bank_name"), reference.getOrganizationId(),
rs.getBigDecimal("balance"), rs.getBigDecimal("frozen_amount")), rs.getBigDecimal("balance"), rs.getBigDecimal("frozen_amount")),
reference.getWalletId(), reference.getUserId(), reference.getBankCode()); reference.getWalletId(), reference.getBankCode());
if (values.isEmpty()) throw validation("付款方或收款方未完成钱包开通、证书备案或银行绑定"); if (values.isEmpty()) throw validation("付款方或收款方未完成钱包开通、证书备案或银行绑定");
return values.get(0); return values.get(0);
} }

@ -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;
}
}
}
Loading…
Cancel
Save