You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
digital-rmb-backend/src/main/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectio...

471 lines
25 KiB
Java

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<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));
}
@Transactional(readOnly = true)
public WalletPrerequisiteReference referenceForWallet(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 referenceForSubject(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;
}
}
}