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(readOnly = true) public WalletPrerequisiteReference referenceForSubject(InstitutionKeySubject subject) { SourceWallet source = loadCompletedSource(subject); Institution institution = loadInstitution(subject); 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 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)); } @Transactional(readOnly = true) public WalletPrerequisiteReference referenceForWallet(String walletId) { if (blank(walletId)) throw prerequisite(); List 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 referenceForSubject(subjects.get(0)); } private SourceWallet loadCompletedSource(InstitutionKeySubject subject) { List 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 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 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 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 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 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 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 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 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 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 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 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; } } }