feat: project completed wallet prerequisites on demand
parent
8388a3fb46
commit
ab5c2df7ed
@ -0,0 +1,448 @@
|
||||
package com.yau.digitalrmb.shared.wallet;
|
||||
|
||||
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
|
||||
import com.yau.digitalrmb.shared.api.ErrorCode;
|
||||
import com.yau.digitalrmb.shared.exception.BusinessException;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class WalletPrerequisiteProjectionService {
|
||||
private static final DateTimeFormatter MODULE_TIMESTAMP = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
|
||||
|
||||
private final JdbcTemplate jdbc;
|
||||
private final WalletPrivateKeyCipher privateKeyCipher;
|
||||
|
||||
public WalletPrerequisiteProjectionService(JdbcTemplate jdbc, WalletPrivateKeyCipher privateKeyCipher) {
|
||||
this.jdbc = jdbc;
|
||||
this.privateKeyCipher = privateKeyCipher;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public WalletPrerequisiteReference ensureForSubject(InstitutionKeySubject subject) {
|
||||
SourceWallet source = loadCompletedSource(subject);
|
||||
Institution institution = loadInstitution(subject);
|
||||
String accountId = ensureBankAccount(source, institution);
|
||||
ensureWallet(source);
|
||||
ensureCertificate(source);
|
||||
ensureContract(source);
|
||||
ensureBinding(source.walletId, accountId);
|
||||
return new WalletPrerequisiteReference(source.userId, source.schoolId, source.classId,
|
||||
source.walletId, institution.bankCode, institution.organizationId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public WalletPrerequisiteReference ensureForWallet(String walletId) {
|
||||
if (blank(walletId)) throw prerequisite();
|
||||
List<InstitutionKeySubject> subjects = jdbc.query(
|
||||
"SELECT DISTINCT user_id,school_id,class_id FROM central_wallet_activation " +
|
||||
"WHERE wallet_id=? AND wallet_activated=TRUE AND final_sent=TRUE " +
|
||||
"AND cb_final_signature IS NOT NULL AND deleted=FALSE",
|
||||
(rs, row) -> new InstitutionKeySubject(rs.getString("user_id"),
|
||||
rs.getLong("school_id"), rs.getLong("class_id")), walletId.trim());
|
||||
if (subjects.isEmpty()) throw prerequisite();
|
||||
if (subjects.size() != 1) throw conflict("钱包标识对应多个实验主体");
|
||||
return ensureForSubject(subjects.get(0));
|
||||
}
|
||||
|
||||
private SourceWallet loadCompletedSource(InstitutionKeySubject subject) {
|
||||
List<Activation> activations = jdbc.query(
|
||||
"SELECT wallet_id,cb_final_signature,final_time FROM central_wallet_activation " +
|
||||
"WHERE user_id=? AND school_id=? AND class_id=? AND wallet_activated=TRUE " +
|
||||
"AND final_sent=TRUE AND cb_final_signature IS NOT NULL AND status='FINAL_SENT' " +
|
||||
"AND deleted=FALSE ORDER BY created_at DESC LIMIT 1",
|
||||
(rs, row) -> new Activation(rs.getString("wallet_id"),
|
||||
rs.getString("cb_final_signature"), rs.getString("final_time")),
|
||||
subject.getUserId(), subject.getSchoolId(), subject.getClassId());
|
||||
if (activations.isEmpty()) throw prerequisite();
|
||||
Activation activation = activations.get(0);
|
||||
|
||||
List<Application> applications = jdbc.query(
|
||||
"SELECT COALESCE(account_bank,selected_bank) bank_name,bank_card_number,account_balance,wallet_type " +
|
||||
"FROM wallet_application WHERE user_id=? AND school_id=? AND class_id=? " +
|
||||
"AND status='SUBMITTED' AND deleted=FALSE AND bank_card_number IS NOT NULL " +
|
||||
"AND account_balance IS NOT NULL AND COALESCE(account_bank,selected_bank) IS NOT NULL " +
|
||||
"ORDER BY created_at DESC LIMIT 1",
|
||||
(rs, row) -> new Application(rs.getString("bank_name"), rs.getString("bank_card_number"),
|
||||
rs.getBigDecimal("account_balance"), rs.getString("wallet_type")),
|
||||
subject.getUserId(), subject.getSchoolId(), subject.getClassId());
|
||||
List<Certificate> certificates = jdbc.query(
|
||||
"SELECT cert_private_key,cert_public_key,cert_serial_number,cert_issued_time " +
|
||||
"FROM wallet_identifier_generation WHERE user_id=? AND school_id=? AND class_id=? " +
|
||||
"AND wallet_identifier=? AND status='CERT_ISSUED' AND deleted=FALSE " +
|
||||
"AND cert_private_key IS NOT NULL AND cert_public_key IS NOT NULL " +
|
||||
"AND cert_serial_number IS NOT NULL ORDER BY created_at DESC LIMIT 1",
|
||||
(rs, row) -> new Certificate(rs.getString("cert_private_key"), rs.getString("cert_public_key"),
|
||||
rs.getString("cert_serial_number"), rs.getString("cert_issued_time")),
|
||||
subject.getUserId(), subject.getSchoolId(), subject.getClassId(), activation.walletId);
|
||||
List<String> rootSignatures = jdbc.query(
|
||||
"SELECT cb_root_signature FROM central_wallet_registration " +
|
||||
"WHERE user_id=? AND school_id=? AND class_id=? AND wallet_id=? " +
|
||||
"AND wallet_registered=TRUE AND sent=TRUE AND cb_root_signature IS NOT NULL " +
|
||||
"AND deleted=FALSE ORDER BY created_at DESC LIMIT 1",
|
||||
(rs, row) -> rs.getString("cb_root_signature"), subject.getUserId(),
|
||||
subject.getSchoolId(), subject.getClassId(), activation.walletId);
|
||||
List<Contract> contracts = jdbc.query(
|
||||
"SELECT wallet_type,single_payment_limit,daily_payment_limit,annual_payment_limit,balance_ceiling," +
|
||||
"contract_plaintext,contract_digest,contract_id,contract_effective_time " +
|
||||
"FROM smart_contract_generation WHERE user_id=? AND school_id=? AND class_id=? " +
|
||||
"AND wallet_id=? AND sent=TRUE AND status='SENT' AND deleted=FALSE " +
|
||||
"AND contract_id IS NOT NULL AND single_payment_limit IS NOT NULL " +
|
||||
"AND daily_payment_limit IS NOT NULL AND annual_payment_limit IS NOT NULL " +
|
||||
"AND balance_ceiling IS NOT NULL AND contract_plaintext IS NOT NULL " +
|
||||
"AND contract_digest IS NOT NULL ORDER BY created_at DESC LIMIT 1",
|
||||
(rs, row) -> new Contract(rs.getString("wallet_type"),
|
||||
decimal(rs.getString("single_payment_limit")), decimal(rs.getString("daily_payment_limit")),
|
||||
decimal(rs.getString("annual_payment_limit")), decimal(rs.getString("balance_ceiling")),
|
||||
rs.getString("contract_plaintext"), rs.getString("contract_digest"),
|
||||
rs.getString("contract_id"), rs.getString("contract_effective_time")),
|
||||
subject.getUserId(), subject.getSchoolId(), subject.getClassId(), activation.walletId);
|
||||
if (applications.isEmpty() || certificates.isEmpty() || rootSignatures.isEmpty() || contracts.isEmpty()) {
|
||||
throw prerequisite();
|
||||
}
|
||||
Application application = applications.get(0);
|
||||
Contract contract = contracts.get(0);
|
||||
if (!blank(application.walletType) && !blank(contract.walletType)
|
||||
&& !application.walletType.equals(contract.walletType)) {
|
||||
throw conflict("模块三申请钱包类型与合约钱包类型不一致");
|
||||
}
|
||||
return new SourceWallet(subject.getUserId(), subject.getSchoolId(), subject.getClassId(),
|
||||
activation.walletId, activation.finalSignature, timestamp(activation.finalTime), application,
|
||||
certificates.get(0), rootSignatures.get(0), contract);
|
||||
}
|
||||
|
||||
private Institution loadInstitution(InstitutionKeySubject subject) {
|
||||
List<Institution> institutions = 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());
|
||||
if (institutions.isEmpty()) {
|
||||
throw new BusinessException(ErrorCode.VALIDATION_ERROR,
|
||||
"请先完成机构标识实验并取得当前用户、学校和班级对应的机构标识");
|
||||
}
|
||||
return institutions.get(0);
|
||||
}
|
||||
|
||||
private String ensureBankAccount(SourceWallet source, Institution institution) {
|
||||
String expectedId = "ACCOUNT_" + UUID.nameUUIDFromBytes(
|
||||
(source.userId + "|" + institution.bankCode).getBytes(StandardCharsets.UTF_8));
|
||||
List<BankAccount> existing = bankAccounts(source.userId, institution.bankCode);
|
||||
if (!existing.isEmpty()) {
|
||||
validateAccount(existing.get(0), source, institution);
|
||||
return existing.get(0).accountId;
|
||||
}
|
||||
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 (?,?,?,?,?,?,?,0,'ACTIVE',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)",
|
||||
expectedId, source.userId, institution.bankCode, source.application.bankName,
|
||||
source.application.cardNumber, lastFour(source.application.cardNumber),
|
||||
source.application.accountBalance);
|
||||
} catch (DuplicateKeyException exception) {
|
||||
existing = bankAccounts(source.userId, institution.bankCode);
|
||||
if (existing.isEmpty()) throw exception;
|
||||
validateAccount(existing.get(0), source, institution);
|
||||
return existing.get(0).accountId;
|
||||
}
|
||||
return expectedId;
|
||||
}
|
||||
|
||||
private void ensureWallet(SourceWallet source) {
|
||||
List<Wallet> existing = jdbc.query(
|
||||
"SELECT wallet_id,user_id,wallet_type,central_bank_confirmation_signature " +
|
||||
"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")),
|
||||
source.walletId, source.userId);
|
||||
if (!existing.isEmpty()) {
|
||||
if (existing.size() != 1) throw conflict("共享钱包标识或用户归属冲突");
|
||||
validateWallet(existing.get(0), source);
|
||||
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',0,0,?,?,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)",
|
||||
source.walletId, source.userId, source.contract.walletType,
|
||||
source.finalSignature, source.openedAt);
|
||||
} catch (DuplicateKeyException exception) {
|
||||
ensureWallet(source);
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureCertificate(SourceWallet source) {
|
||||
List<WalletCertificate> existing = jdbc.query(
|
||||
"SELECT certificate_serial,wallet_id,public_key,central_bank_root_signature " +
|
||||
"FROM wallet_certificate WHERE wallet_id=? OR certificate_serial=?",
|
||||
(rs, row) -> new WalletCertificate(rs.getString("certificate_serial"),
|
||||
rs.getString("wallet_id"), rs.getString("public_key"),
|
||||
rs.getString("central_bank_root_signature")),
|
||||
source.walletId, source.certificate.serialNumber);
|
||||
if (!existing.isEmpty()) {
|
||||
if (existing.size() != 1) throw conflict("共享钱包证书序列号或钱包归属冲突");
|
||||
WalletCertificate value = existing.get(0);
|
||||
if (!source.walletId.equals(value.walletId)
|
||||
|| !source.certificate.serialNumber.equals(value.serialNumber)
|
||||
|| !source.certificate.publicKey.equals(value.publicKey)
|
||||
|| !source.rootSignature.equals(value.rootSignature)) {
|
||||
throw conflict("共享钱包证书与模块三结果不一致");
|
||||
}
|
||||
return;
|
||||
}
|
||||
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',?,'VALID',?)",
|
||||
source.certificate.serialNumber, source.walletId, source.certificate.publicKey,
|
||||
privateKeyCipher.encrypt(source.certificate.privateKey), source.rootSignature,
|
||||
timestamp(source.certificate.issuedTime));
|
||||
} catch (DuplicateKeyException exception) {
|
||||
ensureCertificate(source);
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureContract(SourceWallet source) {
|
||||
List<WalletContract> existing = jdbc.query(
|
||||
"SELECT contract_id,wallet_id,wallet_type FROM wallet_contract WHERE wallet_id=? OR contract_id=?",
|
||||
(rs, row) -> new WalletContract(rs.getString("contract_id"),
|
||||
rs.getString("wallet_id"), rs.getString("wallet_type")),
|
||||
source.walletId, source.contract.contractId);
|
||||
if (!existing.isEmpty()) {
|
||||
if (existing.size() != 1) throw conflict("共享钱包合约标识或钱包归属冲突");
|
||||
WalletContract value = existing.get(0);
|
||||
if (!source.walletId.equals(value.walletId)
|
||||
|| !source.contract.contractId.equals(value.contractId)
|
||||
|| !source.contract.walletType.equals(value.walletType)) {
|
||||
throw conflict("共享钱包合约与模块三结果不一致");
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
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)",
|
||||
source.contract.contractId, source.walletId, source.contract.walletType,
|
||||
source.contract.singleLimit, source.contract.dailyLimit, source.contract.annualLimit,
|
||||
source.contract.balanceLimit, source.contract.originalText, source.contract.digest);
|
||||
} catch (DuplicateKeyException exception) {
|
||||
ensureContract(source);
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureBinding(String walletId, String accountId) {
|
||||
List<String> existing = jdbc.query(
|
||||
"SELECT bank_account_id FROM wallet_bank_binding WHERE wallet_id=?",
|
||||
(rs, row) -> rs.getString("bank_account_id"), walletId);
|
||||
if (!existing.isEmpty()) {
|
||||
if (existing.size() != 1 || !accountId.equals(existing.get(0))) {
|
||||
throw conflict("共享钱包银行卡绑定与模块三结果不一致");
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
jdbc.update("INSERT INTO wallet_bank_binding (wallet_id,bank_account_id,status,bound_at) " +
|
||||
"VALUES (?,?,'BOUND',CURRENT_TIMESTAMP)", walletId, accountId);
|
||||
} catch (DuplicateKeyException exception) {
|
||||
ensureBinding(walletId, accountId);
|
||||
}
|
||||
}
|
||||
|
||||
private List<BankAccount> bankAccounts(String userId, String bankCode) {
|
||||
return jdbc.query("SELECT account_id,user_id,bank_code,card_number FROM simulated_bank_account " +
|
||||
"WHERE user_id=? AND bank_code=?",
|
||||
(rs, row) -> new BankAccount(rs.getString("account_id"), rs.getString("user_id"),
|
||||
rs.getString("bank_code"), rs.getString("card_number")), userId, bankCode);
|
||||
}
|
||||
|
||||
private void validateAccount(BankAccount value, SourceWallet source, Institution institution) {
|
||||
if (!source.userId.equals(value.userId) || !institution.bankCode.equals(value.bankCode)
|
||||
|| !source.application.cardNumber.equals(value.cardNumber)) {
|
||||
throw conflict("共享银行账户与模块三银行卡结果不一致");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateWallet(Wallet value, SourceWallet source) {
|
||||
if (!source.walletId.equals(value.walletId) || !source.userId.equals(value.userId)
|
||||
|| !source.contract.walletType.equals(value.walletType)
|
||||
|| !source.finalSignature.equals(value.finalSignature)) {
|
||||
throw conflict("共享钱包与模块三最终结果不一致");
|
||||
}
|
||||
}
|
||||
|
||||
private Timestamp timestamp(String value) {
|
||||
if (!blank(value)) {
|
||||
try {
|
||||
return Timestamp.valueOf(LocalDateTime.parse(value.trim(), MODULE_TIMESTAMP));
|
||||
} catch (DateTimeParseException ignored) {
|
||||
// Module 3 historical rows may use display text; initialization time is the safe fallback.
|
||||
}
|
||||
}
|
||||
return Timestamp.valueOf(LocalDateTime.now());
|
||||
}
|
||||
|
||||
private BigDecimal decimal(String value) {
|
||||
try {
|
||||
return new BigDecimal(value).setScale(2);
|
||||
} catch (RuntimeException exception) {
|
||||
throw prerequisite();
|
||||
}
|
||||
}
|
||||
|
||||
private String lastFour(String cardNumber) {
|
||||
if (blank(cardNumber) || cardNumber.trim().length() < 4) throw prerequisite();
|
||||
String normalized = cardNumber.trim();
|
||||
return normalized.substring(normalized.length() - 4);
|
||||
}
|
||||
|
||||
private boolean blank(String value) {
|
||||
return value == null || value.trim().isEmpty();
|
||||
}
|
||||
|
||||
private BusinessException prerequisite() {
|
||||
return new BusinessException(ErrorCode.VALIDATION_ERROR,
|
||||
"请先完成个人数字钱包开通实验的申请、证书备案、合约生成和央行最终确认");
|
||||
}
|
||||
|
||||
private BusinessException conflict(String message) {
|
||||
return new BusinessException(ErrorCode.VALIDATION_ERROR, message);
|
||||
}
|
||||
|
||||
private static final class Activation {
|
||||
private final String walletId;
|
||||
private final String finalSignature;
|
||||
private final String finalTime;
|
||||
private Activation(String walletId, String finalSignature, String finalTime) {
|
||||
this.walletId = walletId; this.finalSignature = finalSignature; this.finalTime = finalTime;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Application {
|
||||
private final String bankName;
|
||||
private final String cardNumber;
|
||||
private final BigDecimal accountBalance;
|
||||
private final String walletType;
|
||||
private Application(String bankName, String cardNumber, BigDecimal accountBalance, String walletType) {
|
||||
this.bankName = bankName; this.cardNumber = cardNumber;
|
||||
this.accountBalance = accountBalance; this.walletType = walletType;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Certificate {
|
||||
private final String privateKey;
|
||||
private final String publicKey;
|
||||
private final String serialNumber;
|
||||
private final String issuedTime;
|
||||
private Certificate(String privateKey, String publicKey, String serialNumber, String issuedTime) {
|
||||
this.privateKey = privateKey; this.publicKey = publicKey;
|
||||
this.serialNumber = serialNumber; this.issuedTime = issuedTime;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Contract {
|
||||
private final String walletType;
|
||||
private final BigDecimal singleLimit;
|
||||
private final BigDecimal dailyLimit;
|
||||
private final BigDecimal annualLimit;
|
||||
private final BigDecimal balanceLimit;
|
||||
private final String originalText;
|
||||
private final String digest;
|
||||
private final String contractId;
|
||||
private final String effectiveTime;
|
||||
private Contract(String walletType, BigDecimal singleLimit, BigDecimal dailyLimit,
|
||||
BigDecimal annualLimit, BigDecimal balanceLimit, String originalText,
|
||||
String digest, String contractId, String effectiveTime) {
|
||||
this.walletType = walletType; this.singleLimit = singleLimit; this.dailyLimit = dailyLimit;
|
||||
this.annualLimit = annualLimit; this.balanceLimit = balanceLimit; this.originalText = originalText;
|
||||
this.digest = digest; this.contractId = contractId; this.effectiveTime = effectiveTime;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SourceWallet {
|
||||
private final String userId;
|
||||
private final long schoolId;
|
||||
private final long classId;
|
||||
private final String walletId;
|
||||
private final String finalSignature;
|
||||
private final Timestamp openedAt;
|
||||
private final Application application;
|
||||
private final Certificate certificate;
|
||||
private final String rootSignature;
|
||||
private final Contract contract;
|
||||
private SourceWallet(String userId, long schoolId, long classId, String walletId,
|
||||
String finalSignature, Timestamp openedAt, Application application,
|
||||
Certificate certificate, String rootSignature, Contract contract) {
|
||||
this.userId = userId; this.schoolId = schoolId; this.classId = classId;
|
||||
this.walletId = walletId; this.finalSignature = finalSignature; this.openedAt = openedAt;
|
||||
this.application = application; this.certificate = certificate;
|
||||
this.rootSignature = rootSignature; this.contract = contract;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class BankAccount {
|
||||
private final String accountId;
|
||||
private final String userId;
|
||||
private final String bankCode;
|
||||
private final String cardNumber;
|
||||
private BankAccount(String accountId, String userId, String bankCode, String cardNumber) {
|
||||
this.accountId = accountId; this.userId = userId;
|
||||
this.bankCode = bankCode; this.cardNumber = cardNumber;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Wallet {
|
||||
private final String walletId;
|
||||
private final String userId;
|
||||
private final String walletType;
|
||||
private final String finalSignature;
|
||||
private Wallet(String walletId, String userId, String walletType, String finalSignature) {
|
||||
this.walletId = walletId; this.userId = userId;
|
||||
this.walletType = walletType; this.finalSignature = finalSignature;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class WalletCertificate {
|
||||
private final String serialNumber;
|
||||
private final String walletId;
|
||||
private final String publicKey;
|
||||
private final String rootSignature;
|
||||
private WalletCertificate(String serialNumber, String walletId, String publicKey, String rootSignature) {
|
||||
this.serialNumber = serialNumber; this.walletId = walletId;
|
||||
this.publicKey = publicKey; this.rootSignature = rootSignature;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class WalletContract {
|
||||
private final String contractId;
|
||||
private final String walletId;
|
||||
private final String walletType;
|
||||
private WalletContract(String contractId, String walletId, String walletType) {
|
||||
this.contractId = contractId; this.walletId = walletId; this.walletType = walletType;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package com.yau.digitalrmb.shared.wallet;
|
||||
|
||||
public final class WalletPrerequisiteReference {
|
||||
private final String userId;
|
||||
private final long schoolId;
|
||||
private final long classId;
|
||||
private final String walletId;
|
||||
private final String bankCode;
|
||||
private final String organizationId;
|
||||
|
||||
public WalletPrerequisiteReference(String userId, long schoolId, long classId,
|
||||
String walletId, String bankCode, String organizationId) {
|
||||
this.userId = userId;
|
||||
this.schoolId = schoolId;
|
||||
this.classId = classId;
|
||||
this.walletId = walletId;
|
||||
this.bankCode = bankCode;
|
||||
this.organizationId = organizationId;
|
||||
}
|
||||
|
||||
public String getUserId() { return userId; }
|
||||
public long getSchoolId() { return schoolId; }
|
||||
public long getClassId() { return classId; }
|
||||
public String getWalletId() { return walletId; }
|
||||
public String getBankCode() { return bankCode; }
|
||||
public String getOrganizationId() { return organizationId; }
|
||||
}
|
||||
@ -0,0 +1,146 @@
|
||||
package com.yau.digitalrmb.shared.wallet;
|
||||
|
||||
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.exception.BusinessException;
|
||||
import com.yau.digitalrmb.testsupport.WalletOpeningTestData;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.Timestamp;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
class WalletPrerequisiteProjectionServiceTest {
|
||||
private static final String SUBJECT_USER = "00000000-0000-0000-0000-000000000880";
|
||||
private static final String OTHER_USER = "00000000-0000-0000-0000-000000000881";
|
||||
private static final long SCHOOL_ID = 1880L;
|
||||
private static final long CLASS_ID = 2880L;
|
||||
private static final String WALLET_ID = "WALLET_PROJECTION_TEST";
|
||||
private static final String BANK_CODE = "BKCHCNBJ00880";
|
||||
private static final long ACTIVATION_ID = 980004L;
|
||||
|
||||
@Autowired private JdbcTemplate jdbc;
|
||||
@Autowired private InstitutionIdentityCryptography cryptography;
|
||||
@Autowired private WalletPrerequisiteProjectionService service;
|
||||
|
||||
private InstitutionSm2KeyPair keyPair;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
clean();
|
||||
keyPair = cryptography.generateSm2KeyPair();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
clean();
|
||||
}
|
||||
|
||||
@Test
|
||||
void incompleteModuleThreeCreatesNoSharedRuntimeRows() {
|
||||
jdbc.update("INSERT INTO central_wallet_activation (id,user_id,school_id,class_id,wallet_id,wallet_activated," +
|
||||
"final_sent,status,created_at,updated_at,created_by,updated_by,deleted) " +
|
||||
"VALUES (?,?,?,?,?,FALSE,FALSE,'PENDING',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,'test','test',FALSE)",
|
||||
ACTIVATION_ID, SUBJECT_USER, SCHOOL_ID, CLASS_ID, WALLET_ID);
|
||||
|
||||
assertThatThrownBy(() -> service.ensureForSubject(subject()))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("个人数字钱包开通");
|
||||
assertThat(count("digital_wallet", "user_id", SUBJECT_USER)).isZero();
|
||||
assertThat(count("simulated_bank_account", "user_id", SUBJECT_USER)).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void completedModuleThreeCreatesAllSharedRuntimeRows() {
|
||||
seedCompleted();
|
||||
|
||||
WalletPrerequisiteReference reference = service.ensureForSubject(subject());
|
||||
|
||||
assertThat(reference.getWalletId()).isEqualTo(WALLET_ID);
|
||||
assertThat(reference.getOrganizationId()).isEqualTo("ORG_EXPECTED");
|
||||
assertThat(count("digital_wallet", "wallet_id", WALLET_ID)).isEqualTo(1);
|
||||
assertThat(count("wallet_certificate", "wallet_id", WALLET_ID)).isEqualTo(1);
|
||||
assertThat(count("wallet_contract", "wallet_id", WALLET_ID)).isEqualTo(1);
|
||||
assertThat(count("wallet_bank_binding", "wallet_id", WALLET_ID)).isEqualTo(1);
|
||||
String encrypted = jdbc.queryForObject(
|
||||
"SELECT encrypted_private_key FROM wallet_certificate WHERE wallet_id=?", String.class, WALLET_ID);
|
||||
assertThat(encrypted).isNotEqualTo(keyPair.getPrivateKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
void repeatedProjectionDoesNotOverwriteMutableRuntimeStateOrModuleThreeFacts() {
|
||||
seedCompleted();
|
||||
service.ensureForSubject(subject());
|
||||
jdbc.update("UPDATE digital_wallet SET balance=321.00,frozen_amount=20.00 WHERE wallet_id=?", WALLET_ID);
|
||||
jdbc.update("UPDATE wallet_contract SET daily_used_amount=45.00 WHERE wallet_id=?", WALLET_ID);
|
||||
Timestamp activationUpdatedAt = jdbc.queryForObject(
|
||||
"SELECT updated_at FROM central_wallet_activation WHERE id=?", Timestamp.class, ACTIVATION_ID);
|
||||
|
||||
service.ensureForSubject(subject());
|
||||
|
||||
assertThat(jdbc.queryForObject("SELECT balance FROM digital_wallet WHERE wallet_id=?",
|
||||
BigDecimal.class, WALLET_ID)).isEqualByComparingTo("321.00");
|
||||
assertThat(jdbc.queryForObject("SELECT frozen_amount FROM digital_wallet WHERE wallet_id=?",
|
||||
BigDecimal.class, WALLET_ID)).isEqualByComparingTo("20.00");
|
||||
assertThat(jdbc.queryForObject("SELECT daily_used_amount FROM wallet_contract WHERE wallet_id=?",
|
||||
BigDecimal.class, WALLET_ID)).isEqualByComparingTo("45.00");
|
||||
assertThat(jdbc.queryForObject("SELECT updated_at FROM central_wallet_activation WHERE id=?",
|
||||
Timestamp.class, ACTIVATION_ID)).isEqualTo(activationUpdatedAt);
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameBankCodeFromAnotherScopeCannotSupplyOrganizationId() {
|
||||
seedCompleted();
|
||||
seedInstitutionIdentifier(980011L, OTHER_USER, SCHOOL_ID + 1, CLASS_ID + 1,
|
||||
BANK_CODE, "ORG_WRONG");
|
||||
|
||||
assertThat(service.ensureForWallet(WALLET_ID).getOrganizationId()).isEqualTo("ORG_EXPECTED");
|
||||
}
|
||||
|
||||
private void seedCompleted() {
|
||||
WalletOpeningTestData.insertCompletedWallet(jdbc, 980000L, SUBJECT_USER, SCHOOL_ID, CLASS_ID,
|
||||
WALLET_ID, keyPair.getPrivateKey(), keyPair.getPublicKey(), "测试银行",
|
||||
"6216610100001234567", new BigDecimal("50000.00"));
|
||||
seedInstitutionIdentifier(980010L, SUBJECT_USER, SCHOOL_ID, CLASS_ID, BANK_CODE, "ORG_EXPECTED");
|
||||
}
|
||||
|
||||
private void seedInstitutionIdentifier(long id, String userId, long schoolId, long classId,
|
||||
String bankCode, String organizationId) {
|
||||
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 " +
|
||||
"(?,?,?,?,?,'20260818090000','TEST','ISSUED',?,1,0,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,?,?,FALSE)",
|
||||
id, bankCode, userId, schoolId, classId, organizationId, userId, userId);
|
||||
}
|
||||
|
||||
private InstitutionKeySubject subject() {
|
||||
return new InstitutionKeySubject(SUBJECT_USER, SCHOOL_ID, CLASS_ID);
|
||||
}
|
||||
|
||||
private int count(String table, String column, String value) {
|
||||
return jdbc.queryForObject("SELECT COUNT(*) FROM " + table + " WHERE " + column + "=?",
|
||||
Integer.class, value);
|
||||
}
|
||||
|
||||
private void clean() {
|
||||
jdbc.update("DELETE FROM wallet_bank_binding WHERE wallet_id=?", WALLET_ID);
|
||||
jdbc.update("DELETE FROM wallet_contract WHERE wallet_id=?", WALLET_ID);
|
||||
jdbc.update("DELETE FROM wallet_certificate WHERE wallet_id=?", WALLET_ID);
|
||||
jdbc.update("DELETE FROM simulated_bank_account WHERE user_id=?", SUBJECT_USER);
|
||||
jdbc.update("DELETE FROM digital_wallet WHERE user_id=?", SUBJECT_USER);
|
||||
WalletOpeningTestData.deleteWalletFacts(jdbc, SUBJECT_USER);
|
||||
WalletOpeningTestData.deleteWalletFacts(jdbc, OTHER_USER);
|
||||
jdbc.update("DELETE FROM institution_identifier_application WHERE id IN (980010,980011)");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
package com.yau.digitalrmb.testsupport;
|
||||
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public final class WalletOpeningTestData {
|
||||
private WalletOpeningTestData() {
|
||||
}
|
||||
|
||||
public static void insertCompletedWallet(JdbcTemplate jdbc, long baseId, String userId,
|
||||
long schoolId, long classId, String walletId,
|
||||
String privateKey, String publicKey, String bankName,
|
||||
String cardNumber, BigDecimal bankBalance) {
|
||||
jdbc.update("INSERT INTO wallet_application (id,user_id,school_id,class_id,account_bank,bank_card_number," +
|
||||
"account_balance,selected_bank,wallet_type,application_id,status,created_at,updated_at,created_by,updated_by,deleted) " +
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,'SUBMITTED',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,'test','test',FALSE)",
|
||||
baseId, userId, schoolId, classId, bankName, cardNumber, bankBalance, bankName,
|
||||
"TYPE_II", "APP_" + baseId);
|
||||
jdbc.update("INSERT INTO wallet_identifier_generation (id,user_id,school_id,class_id,wallet_identifier," +
|
||||
"cert_private_key,cert_public_key,cert_serial_number,cert_issued_time,status,created_at,updated_at,created_by,updated_by,deleted) " +
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,'CERT_ISSUED',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,'test','test',FALSE)",
|
||||
baseId + 1, userId, schoolId, classId, walletId, privateKey, publicKey,
|
||||
"CERT_" + baseId, "20260818090000");
|
||||
jdbc.update("INSERT INTO central_wallet_registration (id,user_id,school_id,class_id,wallet_id,cb_root_signature," +
|
||||
"wallet_registered,sent,status,created_at,updated_at,created_by,updated_by,deleted) " +
|
||||
"VALUES (?,?,?,?,?,'CB_ROOT_TEST',TRUE,TRUE,'SENT',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,'test','test',FALSE)",
|
||||
baseId + 2, userId, schoolId, classId, walletId);
|
||||
jdbc.update("INSERT INTO smart_contract_generation (id,user_id,school_id,class_id,wallet_id,wallet_type," +
|
||||
"single_payment_limit,daily_payment_limit,annual_payment_limit,balance_ceiling,validity,contract_plaintext," +
|
||||
"contract_digest,contract_id,contract_effective_time,sent,status,created_at,updated_at,created_by,updated_by,deleted) " +
|
||||
"VALUES (?,?,?,?,?,'TYPE_II','50000.00','100000.00','500000.00','500000.00','长期有效'," +
|
||||
"'CONTRACT_TEXT','0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',?," +
|
||||
"'20260818090000',TRUE,'SENT',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,'test','test',FALSE)",
|
||||
baseId + 3, userId, schoolId, classId, walletId, "CONTRACT_" + baseId);
|
||||
jdbc.update("INSERT INTO central_wallet_activation (id,user_id,school_id,class_id,wallet_id,wallet_activated," +
|
||||
"wallet_register_status,cb_final_signature,final_time,final_sent,status,created_at,updated_at,created_by,updated_by,deleted) " +
|
||||
"VALUES (?,?,?,?,?,TRUE,'AVAILABLE','CB_FINAL_TEST','20260818090000',TRUE,'FINAL_SENT'," +
|
||||
"CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,'test','test',FALSE)",
|
||||
baseId + 4, userId, schoolId, classId, walletId);
|
||||
}
|
||||
|
||||
public static void deleteWalletFacts(JdbcTemplate jdbc, String userId) {
|
||||
jdbc.update("DELETE FROM central_wallet_activation WHERE user_id=?", userId);
|
||||
jdbc.update("DELETE FROM smart_contract_generation WHERE user_id=?", userId);
|
||||
jdbc.update("DELETE FROM central_wallet_registration WHERE user_id=?", userId);
|
||||
jdbc.update("DELETE FROM wallet_identifier_generation WHERE user_id=?", userId);
|
||||
jdbc.update("DELETE FROM wallet_application WHERE user_id=?", userId);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue