From b7000539ecfaa9114a02a40c1b28b5d81a631e94 Mon Sep 17 00:00:00 2001 From: chenyuan Date: Sat, 26 Sep 2026 19:36:45 +0800 Subject: [PATCH] fix: synchronize training module state --- .../ExchangeTrainingActionService.java | 10 +- .../IssuanceTrainingActionService.java | 12 +- .../application/query/PaymentOrderView.java | 8 +- .../service/PaymentApplicationService.java | 15 +- .../service/PaymentTrainingActionService.java | 12 +- .../payment/domain/model/PaymentOrder.java | 4 +- .../domain/model/PaymentParticipant.java | 32 ++- .../model/TeachingPaymentWalletIdentity.java | 105 ++++++++ .../repository/PaymentResourceRepository.java | 2 +- .../JdbcPaymentResourceRepository.java | 43 +++- .../TeachingPaymentPayeeWhitelist.java | 49 ++-- .../persistence/TeachingPaymentPayer.java | 108 +++++---- .../wallet/WalletRuntimeResetService.java | 21 +- .../ExperimentActionFailedEvent.java | 46 ++++ .../ExperimentActionFailureListener.java | 42 ++++ .../application/ExperimentAttemptService.java | 15 +- .../ExchangeTrainingActionServiceTest.java | 67 +++++ .../IssuanceTrainingActionServiceTest.java | 35 ++- .../rest/IssuanceControllerTest.java | 3 +- .../service/PaymentScoreServiceTest.java | 2 +- .../PaymentTrainingActionServiceTest.java | 33 +++ .../rest/PaymentControllerTest.java | 229 ++++++++++++------ .../ExperimentAttemptServiceTest.java | 22 ++ 23 files changed, 740 insertions(+), 175 deletions(-) create mode 100644 src/main/java/com/yau/digitalrmb/payment/domain/model/TeachingPaymentWalletIdentity.java create mode 100644 src/main/java/com/yau/digitalrmb/training/attempt/application/ExperimentActionFailedEvent.java create mode 100644 src/main/java/com/yau/digitalrmb/training/attempt/application/ExperimentActionFailureListener.java create mode 100644 src/test/java/com/yau/digitalrmb/exchange/application/service/ExchangeTrainingActionServiceTest.java diff --git a/src/main/java/com/yau/digitalrmb/exchange/application/service/ExchangeTrainingActionService.java b/src/main/java/com/yau/digitalrmb/exchange/application/service/ExchangeTrainingActionService.java index 2a17b00..b2da92a 100644 --- a/src/main/java/com/yau/digitalrmb/exchange/application/service/ExchangeTrainingActionService.java +++ b/src/main/java/com/yau/digitalrmb/exchange/application/service/ExchangeTrainingActionService.java @@ -106,7 +106,9 @@ public class ExchangeTrainingActionService { throw new TrainingInputException(exception.getMessage() + ",本次实训第" + errors + "次错误"); } }, next[0], next[1]); - if (scoreService != null) scoreService.markStepCompleted(subject.getUserId(), stepNumber(stepCode)); + if (scoreService != null && isLastActionInStep(index)) { + scoreService.markStepCompleted(subject.getUserId(), stepNumber(stepCode)); + } return result; } @@ -322,4 +324,10 @@ public class ExchangeTrainingActionService { try { return Integer.parseInt(stepCode); } catch (NumberFormatException ignored) { return 0; } } + + private boolean isLastActionInStep(int actionIndex) { + return actionIndex == ACTIONS.size() - 1 + || !ACTIONS.get(actionIndex).substring(0, 2) + .equals(ACTIONS.get(actionIndex + 1).substring(0, 2)); + } } diff --git a/src/main/java/com/yau/digitalrmb/issuance/application/service/IssuanceTrainingActionService.java b/src/main/java/com/yau/digitalrmb/issuance/application/service/IssuanceTrainingActionService.java index 495e305..f1febc5 100644 --- a/src/main/java/com/yau/digitalrmb/issuance/application/service/IssuanceTrainingActionService.java +++ b/src/main/java/com/yau/digitalrmb/issuance/application/service/IssuanceTrainingActionService.java @@ -129,7 +129,9 @@ public class IssuanceTrainingActionService { }; ExperimentActionView result = attempts.execute(attemptId, subject, stepCode, actionCode, fingerprint(key, input, requestId, subject), Object.class, work, next[0], next[1]); - if (scoreService != null) scoreService.markStepCompleted(subject.getUserId(), stepNumber(stepCode)); + if (scoreService != null && isLastActionInStep(actionIndex)) { + scoreService.markStepCompleted(subject.getUserId(), stepNumber(stepCode)); + } return result; } @@ -209,7 +211,7 @@ public class IssuanceTrainingActionService { case "07:sign-bank-receipt": return completed(ownershipService.signReceipt(requestId, actor, keySubject)); case "07:load-ownership-result": - return completed(ownershipService.findProduction(requestId) + return ActionOutcome.completedAttempt(ownershipService.findProduction(requestId) .orElseThrow(() -> validation("数字货币确权结果不存在"))); default: throw validation("不支持的发行实验动作:" + key); @@ -312,4 +314,10 @@ public class IssuanceTrainingActionService { try { return Integer.parseInt(stepCode); } catch (NumberFormatException ignored) { return 0; } } + + private boolean isLastActionInStep(int actionIndex) { + return actionIndex == ACTIONS.size() - 1 + || !ACTIONS.get(actionIndex).substring(0, 2) + .equals(ACTIONS.get(actionIndex + 1).substring(0, 2)); + } } diff --git a/src/main/java/com/yau/digitalrmb/payment/application/query/PaymentOrderView.java b/src/main/java/com/yau/digitalrmb/payment/application/query/PaymentOrderView.java index 58687a9..f5ab394 100644 --- a/src/main/java/com/yau/digitalrmb/payment/application/query/PaymentOrderView.java +++ b/src/main/java/com/yau/digitalrmb/payment/application/query/PaymentOrderView.java @@ -3,6 +3,7 @@ package com.yau.digitalrmb.payment.application.query; import com.yau.digitalrmb.payment.domain.model.PaymentCoin; import com.yau.digitalrmb.payment.domain.model.PaymentNotification; import com.yau.digitalrmb.payment.domain.model.PaymentOrder; +import com.yau.digitalrmb.payment.domain.model.TeachingPaymentWalletIdentity; import java.math.BigDecimal; import java.time.Instant; import java.util.Collections; @@ -39,8 +40,11 @@ public final class PaymentOrderView { private final List notifications; private PaymentOrderView(PaymentOrder value, List coins, List notifications) { - id = value.getId().toString(); paymentNo = value.getPaymentNo(); payerWalletId = value.getPayerWalletId(); - payerBankCode = value.getPayerBankCode(); payeeWalletId = value.getPayeeWalletId(); payeeBankCode = value.getPayeeBankCode(); + id = value.getId().toString(); paymentNo = value.getPaymentNo(); + payerWalletId = TeachingPaymentWalletIdentity.displayWalletId(value.getPayerWalletId()); + payerBankCode = value.getPayerBankCode(); + payeeWalletId = TeachingPaymentWalletIdentity.displayWalletId(value.getPayeeWalletId()); + payeeBankCode = value.getPayeeBankCode(); amount = value.getAmount(); note = value.getNote(); requestTimestamp = value.getRequestTimestamp(); paymentOriginalText = value.getPaymentOriginalText(); paymentDigest = value.getPaymentDigest(); payerSignature = value.getPayerSignature(); complianceReport = value.getComplianceReport(); complianceDigest = value.getComplianceDigest(); payerBankSignature = value.getPayerBankSignature(); diff --git a/src/main/java/com/yau/digitalrmb/payment/application/service/PaymentApplicationService.java b/src/main/java/com/yau/digitalrmb/payment/application/service/PaymentApplicationService.java index 0d5f9dc..40f137e 100644 --- a/src/main/java/com/yau/digitalrmb/payment/application/service/PaymentApplicationService.java +++ b/src/main/java/com/yau/digitalrmb/payment/application/service/PaymentApplicationService.java @@ -13,6 +13,7 @@ import com.yau.digitalrmb.payment.domain.model.PaymentOrderId; import com.yau.digitalrmb.payment.domain.model.PaymentParticipant; import com.yau.digitalrmb.payment.domain.model.PaymentStatus; import com.yau.digitalrmb.payment.domain.model.PayerBankProcessingResult; +import com.yau.digitalrmb.payment.domain.model.TeachingPaymentWalletIdentity; import com.yau.digitalrmb.payment.domain.repository.PaymentOrderRepository; import com.yau.digitalrmb.payment.domain.repository.PaymentResourceRepository; import com.yau.digitalrmb.shared.api.ErrorCode; @@ -58,8 +59,8 @@ public class PaymentApplicationService { return resourceRepository.loadPayer(actor); } - public PaymentParticipant payee(String payeeWalletId) { - return resourceRepository.loadPayee(payeeWalletId); + public PaymentParticipant payee(String payeeWalletId, PaymentActor actor) { + return resourceRepository.loadPayee(payeeWalletId, actor); } @Transactional @@ -130,8 +131,10 @@ public class PaymentApplicationService { throw validation("请先完成合规检查和资源锁定"); } List coins = resourceRepository.findCoins(order); - String report = "COMPLIANCE|" + order.getPaymentNo() + "|" + order.getPayerWalletId() + "|" + - order.getPayeeWalletId() + "|" + order.getAmount().toPlainString() + "|" + + String report = "COMPLIANCE|" + order.getPaymentNo() + "|" + + TeachingPaymentWalletIdentity.displayWalletId(order.getPayerWalletId()) + "|" + + TeachingPaymentWalletIdentity.displayWalletId(order.getPayeeWalletId()) + "|" + + order.getAmount().toPlainString() + "|" + order.getPaymentDigest() + "|SIGNATURE_VALID|CONTRACT_VALID|" + coins.size() + "|" + order.getPayerBankCode(); order.recordComplianceReport(report); @@ -221,7 +224,9 @@ public class PaymentApplicationService { } String clearingNo = "ACS_" + order.getPaymentNo(); String original = "SETTLEMENT|" + order.getPaymentNo() + "|" + order.getTransactionId() + "|" + - order.getPayerWalletId() + "|" + order.getPayeeWalletId() + "|" + order.getCoinCount() + "|" + + TeachingPaymentWalletIdentity.displayWalletId(order.getPayerWalletId()) + "|" + + TeachingPaymentWalletIdentity.displayWalletId(order.getPayeeWalletId()) + "|" + + order.getCoinCount() + "|" + order.getAmount().toPlainString() + "|" + order.getPayerBankCode() + "|" + order.getPayeeBankCode() + "|" + clearingNo + "|" + clock.instant().toString(); order.prepareSettlement(clearingNo, original); diff --git a/src/main/java/com/yau/digitalrmb/payment/application/service/PaymentTrainingActionService.java b/src/main/java/com/yau/digitalrmb/payment/application/service/PaymentTrainingActionService.java index 4318179..762c42b 100644 --- a/src/main/java/com/yau/digitalrmb/payment/application/service/PaymentTrainingActionService.java +++ b/src/main/java/com/yau/digitalrmb/payment/application/service/PaymentTrainingActionService.java @@ -98,7 +98,9 @@ public class PaymentTrainingActionService { throw new TrainingInputException(exception.getMessage() + ",本次实训第" + errors + "次错误"); } }, next[0], next[1]); - if (scoreService != null) scoreService.markStepCompleted(subject.getUserId(), stepNumber(stepCode)); + if (scoreService != null && isLastActionInStep(index)) { + scoreService.markStepCompleted(subject.getUserId(), stepNumber(stepCode)); + } return result; } @@ -112,7 +114,7 @@ public class PaymentTrainingActionService { String payeeWalletId = request.getPayeeWalletId(); return completed(payeeWalletId == null || payeeWalletId.trim().isEmpty() ? payments.context(null, actor).getPayee() - : payments.payee(payeeWalletId.trim())); + : payments.payee(payeeWalletId.trim(), actor)); } case "02:generate-timestamp": return completed(value("timestamp", TIMESTAMP.format(Instant.now()))); @@ -304,4 +306,10 @@ public class PaymentTrainingActionService { try { return Integer.parseInt(stepCode); } catch (NumberFormatException ignored) { return 0; } } + + private boolean isLastActionInStep(int actionIndex) { + return actionIndex == ACTIONS.size() - 1 + || !ACTIONS.get(actionIndex).substring(0, 2) + .equals(ACTIONS.get(actionIndex + 1).substring(0, 2)); + } } diff --git a/src/main/java/com/yau/digitalrmb/payment/domain/model/PaymentOrder.java b/src/main/java/com/yau/digitalrmb/payment/domain/model/PaymentOrder.java index ab3d639..f961b4b 100644 --- a/src/main/java/com/yau/digitalrmb/payment/domain/model/PaymentOrder.java +++ b/src/main/java/com/yau/digitalrmb/payment/domain/model/PaymentOrder.java @@ -61,8 +61,8 @@ public final class PaymentOrder { public static PaymentOrder create(PaymentOrderId id, String paymentNo, PaymentParticipant payer, PaymentParticipant payee, BigDecimal amount, String note, String requestTimestamp, String paymentOriginalText, String paymentDigest, Instant now) { - return new PaymentOrder(id, paymentNo, payer.getUserId(), payer.getWalletId(), payer.getBankCode(), - payer.getOrganizationId(), payee.getUserId(), payee.getWalletId(), payee.getBankCode(), + return new PaymentOrder(id, paymentNo, payer.getUserId(), payer.getStorageWalletId(), payer.getBankCode(), + payer.getOrganizationId(), payee.getUserId(), payee.getStorageWalletId(), payee.getBankCode(), payee.getOrganizationId(), amount, note, requestTimestamp, paymentOriginalText, paymentDigest, null, null, null, null, null, null, null, null, null, 0, null, null, PaymentStatus.REQUEST_PREPARED, now, null); diff --git a/src/main/java/com/yau/digitalrmb/payment/domain/model/PaymentParticipant.java b/src/main/java/com/yau/digitalrmb/payment/domain/model/PaymentParticipant.java index b9d3b5f..689d14a 100644 --- a/src/main/java/com/yau/digitalrmb/payment/domain/model/PaymentParticipant.java +++ b/src/main/java/com/yau/digitalrmb/payment/domain/model/PaymentParticipant.java @@ -1,12 +1,15 @@ package com.yau.digitalrmb.payment.domain.model; +import com.fasterxml.jackson.annotation.JsonIgnore; import java.math.BigDecimal; public final class PaymentParticipant { private final String userId; private final String walletId; + private final String displayWalletId; private final String walletType; private final String certificateSerial; + private final String displayCertificateSerial; private final String publicKey; private final String bankCode; private final String bankName; @@ -17,16 +20,37 @@ public final class PaymentParticipant { public PaymentParticipant(String userId, String walletId, String walletType, String certificateSerial, String publicKey, String bankCode, String bankName, String organizationId, BigDecimal balance, BigDecimal frozenAmount) { - this.userId = userId; this.walletId = walletId; this.walletType = walletType; - this.certificateSerial = certificateSerial; this.publicKey = publicKey; this.bankCode = bankCode; + this(userId, walletId, walletId, walletType, certificateSerial, certificateSerial, publicKey, bankCode, bankName, + organizationId, balance, frozenAmount); + } + + public PaymentParticipant(String userId, String walletId, String displayWalletId, String walletType, + String certificateSerial, String publicKey, String bankCode, String bankName, + String organizationId, BigDecimal balance, BigDecimal frozenAmount) { + this(userId, walletId, displayWalletId, walletType, certificateSerial, certificateSerial, publicKey, + bankCode, bankName, organizationId, balance, frozenAmount); + } + + public PaymentParticipant(String userId, String walletId, String displayWalletId, String walletType, + String certificateSerial, String displayCertificateSerial, String publicKey, + String bankCode, String bankName, String organizationId, BigDecimal balance, + BigDecimal frozenAmount) { + this.userId = userId; this.walletId = walletId; this.displayWalletId = displayWalletId; + this.walletType = walletType; + this.certificateSerial = certificateSerial; this.displayCertificateSerial = displayCertificateSerial; + this.publicKey = publicKey; this.bankCode = bankCode; this.bankName = bankName; this.organizationId = organizationId; this.balance = balance; this.frozenAmount = frozenAmount; } public String getUserId() { return userId; } - public String getWalletId() { return walletId; } + public String getWalletId() { return displayWalletId; } + @JsonIgnore + public String getStorageWalletId() { return walletId; } public String getWalletType() { return walletType; } - public String getCertificateSerial() { return certificateSerial; } + public String getCertificateSerial() { return displayCertificateSerial; } + @JsonIgnore + public String getStorageCertificateSerial() { return certificateSerial; } public String getPublicKey() { return publicKey; } public String getBankCode() { return bankCode; } public String getBankName() { return bankName; } diff --git a/src/main/java/com/yau/digitalrmb/payment/domain/model/TeachingPaymentWalletIdentity.java b/src/main/java/com/yau/digitalrmb/payment/domain/model/TeachingPaymentWalletIdentity.java new file mode 100644 index 0000000..610ca5d --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/payment/domain/model/TeachingPaymentWalletIdentity.java @@ -0,0 +1,105 @@ +package com.yau.digitalrmb.payment.domain.model; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +/** + * Separates the prototype's fixed wallet identifiers from learner-owned + * persistence identifiers. The latter are deterministic per training subject + * so retries reuse the same isolated runtime without exposing it to the UI. + */ +public final class TeachingPaymentWalletIdentity { + public static final String PAYER_DISPLAY_WALLET_ID = + "WALLET_A_3A4B5C6D7E8F9A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0C1D2E3F4"; + public static final String PAYEE_DISPLAY_WALLET_ID = + "WALLET_B_7D8E9F0A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0C1D2E3F4A5B6"; + + private static final String PAYER_PREFIX = "PAY_TEACH_A_"; + private static final String PAYEE_PREFIX = "PAY_TEACH_B_"; + + private TeachingPaymentWalletIdentity() { + } + + public static SubjectWallets forSubject(String userId, long schoolId, long classId) { + if (userId == null || userId.trim().isEmpty()) throw new IllegalArgumentException("userId is required"); + String subjectKey = userId.trim() + "|" + schoolId + "|" + classId; + String token = hex("PAYMENT_SUBJECT|" + subjectKey).substring(0, 32); + return new SubjectWallets(subjectKey, token); + } + + public static String displayWalletId(String storageWalletId) { + if (storageWalletId == null) return null; + if (storageWalletId.startsWith(PAYER_PREFIX)) return PAYER_DISPLAY_WALLET_ID; + if (storageWalletId.startsWith(PAYEE_PREFIX)) return PAYEE_DISPLAY_WALLET_ID; + return storageWalletId; + } + + private static String hex(String value) { + try { + byte[] digest = MessageDigest.getInstance("SHA-256") + .digest(value.getBytes(StandardCharsets.UTF_8)); + StringBuilder result = new StringBuilder(digest.length * 2); + for (byte item : digest) result.append(String.format("%02x", item & 0xff)); + return result.toString(); + } catch (NoSuchAlgorithmException exception) { + throw new IllegalStateException("SHA-256 is unavailable", exception); + } + } + + private static String uuid(String value) { + return UUID.nameUUIDFromBytes(value.getBytes(StandardCharsets.UTF_8)).toString(); + } + + private static long positiveLong(String value) { + try { + byte[] digest = MessageDigest.getInstance("SHA-256") + .digest(value.getBytes(StandardCharsets.UTF_8)); + long result = ByteBuffer.wrap(digest).getLong() & Long.MAX_VALUE; + return result == 0 ? 1 : result; + } catch (NoSuchAlgorithmException exception) { + throw new IllegalStateException("SHA-256 is unavailable", exception); + } + } + + public static final class SubjectWallets { + private final String subjectKey; + private final String token; + + private SubjectWallets(String subjectKey, String token) { + this.subjectKey = subjectKey; + this.token = token; + } + + public String payerWalletId() { return PAYER_PREFIX + token; } + public String payeeWalletId() { return PAYEE_PREFIX + token; } + public String payerUserId() { return uuid("PAYMENT_PAYER_USER|" + subjectKey); } + public String payeeUserId() { return uuid("PAYMENT_PAYEE_USER|" + subjectKey); } + public String payerCertificateSerial() { return "CERT_PAY_A_" + token; } + public String payeeCertificateSerial() { return "CERT_PAY_B_" + token; } + public String payerAccountId() { return "ACCOUNT_PAY_A_" + token; } + public String payeeAccountId() { return "ACCOUNT_PAY_B_" + token; } + public String payerContractId() { return "CONTRACT_PAY_A_" + token; } + public long payerSourceBatchId() { return positiveLong("PAYMENT_BATCH|" + subjectKey); } + public long payerSourceId(int index) { return positiveLong("PAYMENT_COIN|" + subjectKey + "|" + index); } + public String payerCurrencyId(int index) { + return PAYER_PREFIX + token + "_" + String.format("%02d", index); + } + public List walletIds() { + List values = new ArrayList(); + values.add(payerWalletId()); + values.add(payeeWalletId()); + return Collections.unmodifiableList(values); + } + public List payerCurrencyIds() { + List values = new ArrayList(); + for (int index = 1; index <= 10; index++) values.add(payerCurrencyId(index)); + return Collections.unmodifiableList(values); + } + } +} diff --git a/src/main/java/com/yau/digitalrmb/payment/domain/repository/PaymentResourceRepository.java b/src/main/java/com/yau/digitalrmb/payment/domain/repository/PaymentResourceRepository.java index d9094df..5f9fd66 100644 --- a/src/main/java/com/yau/digitalrmb/payment/domain/repository/PaymentResourceRepository.java +++ b/src/main/java/com/yau/digitalrmb/payment/domain/repository/PaymentResourceRepository.java @@ -13,7 +13,7 @@ import java.util.List; public interface PaymentResourceRepository { PaymentParticipant loadPayer(PaymentActor actor); - PaymentParticipant loadPayee(String payeeWalletId); + PaymentParticipant loadPayee(String payeeWalletId, PaymentActor actor); PaymentContext loadContext(PaymentActor actor, String payeeWalletId); String signWithWallet(String walletId, String digest); boolean verifyWalletSignature(String walletId, String digest, String signature); 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 ae7cd59..9b6cde9 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 @@ -13,6 +13,7 @@ import com.yau.digitalrmb.payment.domain.model.PaymentNotification; import com.yau.digitalrmb.payment.domain.model.PaymentOrder; import com.yau.digitalrmb.payment.domain.model.PaymentParticipant; import com.yau.digitalrmb.payment.domain.model.PayerBankProcessingResult; +import com.yau.digitalrmb.payment.domain.model.TeachingPaymentWalletIdentity; import com.yau.digitalrmb.payment.domain.repository.PaymentResourceRepository; import com.yau.digitalrmb.shared.api.ErrorCode; import com.yau.digitalrmb.shared.exception.BusinessException; @@ -61,13 +62,16 @@ public class JdbcPaymentResourceRepository implements PaymentResourceRepository // 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()); + return loadParticipant(reference, actor.getUserId(), TeachingPaymentPayer.WALLET_ID, + "00A1B2C3D4E5F678"); } @Override - public PaymentParticipant loadPayee(String payeeWalletId) { + public PaymentParticipant loadPayee(String payeeWalletId, PaymentActor actor) { if (teachingPayeeWhitelist.isTeachingWallet(payeeWalletId)) { - return loadParticipant(teachingPayeeWhitelist.resolve(payeeWalletId)); + WalletPrerequisiteReference reference = teachingPayeeWhitelist.resolve(payeeWalletId, subject(actor)); + return loadParticipant(reference, actor.getUserId(), TeachingPaymentPayeeWhitelist.WALLET_ID, + TeachingPaymentPayeeWhitelist.CERTIFICATE_SERIAL); } return loadParticipant(walletProjection.ensureForWallet(payeeWalletId)); } @@ -76,9 +80,13 @@ public class JdbcPaymentResourceRepository implements PaymentResourceRepository public PaymentContext loadContext(PaymentActor actor, String payeeWalletId) { PaymentParticipant payer = loadPayer(actor); PaymentParticipant payee = payeeWalletId == null || payeeWalletId.trim().isEmpty() - ? loadParticipant(teachingPayeeWhitelist.resolveForDifferentBank(payer.getBankCode())) - : loadPayee(payeeWalletId); - if (payer.getWalletId().equals(payee.getWalletId())) throw validation("付款钱包和收款钱包不能相同"); + ? loadParticipant(teachingPayeeWhitelist.resolveForDifferentBank(payer.getBankCode(), subject(actor)), + actor.getUserId(), TeachingPaymentPayeeWhitelist.WALLET_ID, + TeachingPaymentPayeeWhitelist.CERTIFICATE_SERIAL) + : loadPayee(payeeWalletId, actor); + if (payer.getStorageWalletId().equals(payee.getStorageWalletId())) { + throw validation("付款钱包和收款钱包不能相同"); + } if (payer.getBankCode().equals(payee.getBankCode())) throw validation("本实验仅支持商业银行A向商业银行B的跨行支付"); return new PaymentContext(payer, payee); } @@ -234,7 +242,8 @@ public class JdbcPaymentResourceRepository implements PaymentResourceRepository String transactionId = "PAY_TXN_" + order.getPaymentNo(); String clearingNo = "ACS_" + order.getPaymentNo(); String originalText = "SETTLEMENT|" + order.getPaymentNo() + "|" + transactionId + "|" + - order.getPayerWalletId() + "|" + order.getPayeeWalletId() + "|" + coins.size() + "|" + + TeachingPaymentWalletIdentity.displayWalletId(order.getPayerWalletId()) + "|" + + TeachingPaymentWalletIdentity.displayWalletId(order.getPayeeWalletId()) + "|" + coins.size() + "|" + order.getAmount().toPlainString() + "|" + order.getPayerBankCode() + "|" + order.getPayeeBankCode() + "|" + clearingNo + "|" + Instant.now().toString(); String digest = cryptography.sm3(originalText); @@ -329,7 +338,8 @@ public class JdbcPaymentResourceRepository implements PaymentResourceRepository public List findNotifications(PaymentOrder order, String currentUserId) { return jdbc.query("SELECT notification_type,recipient_wallet_id,content,created_at FROM payment_notification " + "WHERE payment_id=? AND recipient_user_id=? ORDER BY created_at", - (rs, row) -> new PaymentNotification(rs.getString("notification_type"), rs.getString("recipient_wallet_id"), + (rs, row) -> new PaymentNotification(rs.getString("notification_type"), + TeachingPaymentWalletIdentity.displayWalletId(rs.getString("recipient_wallet_id")), rs.getString("content"), rs.getTimestamp("created_at").toInstant()), order.getId().toString(), currentUserId); } @@ -345,6 +355,11 @@ public class JdbcPaymentResourceRepository implements PaymentResourceRepository } private PaymentParticipant loadParticipant(WalletPrerequisiteReference reference, String participantUserId) { + return loadParticipant(reference, participantUserId, reference.getWalletId(), null); + } + + private PaymentParticipant loadParticipant(WalletPrerequisiteReference reference, String participantUserId, + String displayWalletId, String displayCertificateSerial) { 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 " + @@ -353,8 +368,10 @@ public class JdbcPaymentResourceRepository implements PaymentResourceRepository "JOIN simulated_bank_account a ON a.account_id=binding.bank_account_id AND a.status='ACTIVE' " + "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(participantUserId, rs.getString("wallet_id"), - rs.getString("wallet_type"), rs.getString("certificate_serial"), rs.getString("public_key"), + (rs, row) -> new PaymentParticipant(participantUserId, rs.getString("wallet_id"), displayWalletId, + rs.getString("wallet_type"), rs.getString("certificate_serial"), + displayCertificateSerial == null ? rs.getString("certificate_serial") : displayCertificateSerial, + rs.getString("public_key"), rs.getString("bank_code"), rs.getString("bank_name"), reference.getOrganizationId(), rs.getBigDecimal("balance"), rs.getBigDecimal("frozen_amount")), reference.getWalletId(), reference.getBankCode()); @@ -428,8 +445,10 @@ public class JdbcPaymentResourceRepository implements PaymentResourceRepository } private String complianceReport(PaymentOrder order, List coins) { - return "COMPLIANCE|" + order.getPaymentNo() + "|" + order.getPayerWalletId() + "|" + - order.getPayeeWalletId() + "|" + order.getAmount().toPlainString() + "|" + order.getPaymentDigest() + + return "COMPLIANCE|" + order.getPaymentNo() + "|" + + TeachingPaymentWalletIdentity.displayWalletId(order.getPayerWalletId()) + "|" + + TeachingPaymentWalletIdentity.displayWalletId(order.getPayeeWalletId()) + "|" + + order.getAmount().toPlainString() + "|" + order.getPaymentDigest() + "|SIGNATURE_VALID|CONTRACT_VALID|" + coins.size() + "|" + order.getPayerBankCode(); } diff --git a/src/main/java/com/yau/digitalrmb/payment/infrastructure/persistence/TeachingPaymentPayeeWhitelist.java b/src/main/java/com/yau/digitalrmb/payment/infrastructure/persistence/TeachingPaymentPayeeWhitelist.java index 1f9dd02..ba652e7 100644 --- a/src/main/java/com/yau/digitalrmb/payment/infrastructure/persistence/TeachingPaymentPayeeWhitelist.java +++ b/src/main/java/com/yau/digitalrmb/payment/infrastructure/persistence/TeachingPaymentPayeeWhitelist.java @@ -1,7 +1,10 @@ package com.yau.digitalrmb.payment.infrastructure.persistence; +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.payment.domain.model.TeachingPaymentWalletIdentity; +import com.yau.digitalrmb.payment.domain.model.TeachingPaymentWalletIdentity.SubjectWallets; import com.yau.digitalrmb.shared.api.ErrorCode; import com.yau.digitalrmb.shared.exception.BusinessException; import com.yau.digitalrmb.shared.wallet.WalletPrerequisiteReference; @@ -19,19 +22,12 @@ import java.util.List; */ @Component public class TeachingPaymentPayeeWhitelist { - private static final String BANK_A = "BKCHCNBJ00001"; private static final String BANK_B = "BKCHCNBJ00002"; - private static final String PAYEE_A_USER = "00000000-0000-0000-0000-000000000951"; - private static final String PAYEE_B_USER = "00000000-0000-0000-0000-000000000953"; - private static final String PAYEE_A_WALLET = "WALLET_TRAINING_PAYEE_A"; - private static final String PAYEE_B_WALLET = - "WALLET_B_7D8E9F0A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0C1D2E3F4A5B6"; - private static final PayeeDefinition PAYEE_A = new PayeeDefinition( - PAYEE_A_USER, PAYEE_A_WALLET, "TYPE_II", "CERT_" + PAYEE_A_WALLET, - BANK_A, "教学商业银行A", "ORG_TRAINING_PAYEE_A"); - private static final PayeeDefinition PAYEE_B = new PayeeDefinition( - PAYEE_B_USER, PAYEE_B_WALLET, "一类钱包", "00C3D4E5F6A7B890", - BANK_B, "建设银行(BKCHCNBJ00002)", "ORG_5F6G7B8E9G0W"); + public static final String WALLET_ID = TeachingPaymentWalletIdentity.PAYEE_DISPLAY_WALLET_ID; + public static final String CERTIFICATE_SERIAL = "00C3D4E5F6A7B890"; + private static final String WALLET_TYPE = "一类钱包"; + private static final String BANK_NAME = "建设银行(BKCHCNBJ00002)"; + private static final String ORGANIZATION_ID = "ORG_5F6G7B8E9G0W"; private final JdbcTemplate jdbc; private final InstitutionIdentityCryptography cryptography; @@ -45,19 +41,19 @@ public class TeachingPaymentPayeeWhitelist { } @Transactional - public WalletPrerequisiteReference resolveForDifferentBank(String payerBankCode) { - return resolve(BANK_B.equals(payerBankCode) ? PAYEE_A : PAYEE_B); + public WalletPrerequisiteReference resolveForDifferentBank(String payerBankCode, InstitutionKeySubject subject) { + if (BANK_B.equals(payerBankCode)) throw conflict("教学付款方必须使用商业银行A钱包"); + return resolve(definition(subject)); } public boolean isTeachingWallet(String walletId) { - return definitionFor(walletId) != null; + return WALLET_ID.equals(walletId); } @Transactional - public WalletPrerequisiteReference resolve(String walletId) { - PayeeDefinition payee = definitionFor(walletId); - if (payee == null) throw conflict("教学收款方钱包不存在"); - return resolve(payee); + public WalletPrerequisiteReference resolve(String walletId, InstitutionKeySubject subject) { + if (!isTeachingWallet(walletId)) throw conflict("教学收款方钱包不存在"); + return resolve(definition(subject)); } private WalletPrerequisiteReference resolve(PayeeDefinition payee) { @@ -69,10 +65,11 @@ public class TeachingPaymentPayeeWhitelist { payee.bankCode, payee.organizationId); } - private PayeeDefinition definitionFor(String walletId) { - if (PAYEE_A_WALLET.equals(walletId)) return PAYEE_A; - if (PAYEE_B_WALLET.equals(walletId)) return PAYEE_B; - return null; + private PayeeDefinition definition(InstitutionKeySubject subject) { + SubjectWallets wallets = TeachingPaymentWalletIdentity.forSubject( + subject.getUserId(), subject.getSchoolId(), subject.getClassId()); + return new PayeeDefinition(wallets.payeeUserId(), wallets.payeeWalletId(), WALLET_TYPE, + wallets.payeeCertificateSerial(), wallets.payeeAccountId(), BANK_B, BANK_NAME, ORGANIZATION_ID); } private void ensureWallet(PayeeDefinition payee) { @@ -169,24 +166,26 @@ public class TeachingPaymentPayeeWhitelist { private final String walletId; private final String walletType; private final String certificateSerial; + private final String accountId; private final String bankCode; private final String bankName; private final String organizationId; private PayeeDefinition(String userId, String walletId, String walletType, String certificateSerial, - String bankCode, String bankName, + String accountId, String bankCode, String bankName, String organizationId) { this.userId = userId; this.walletId = walletId; this.walletType = walletType; this.certificateSerial = certificateSerial; + this.accountId = accountId; this.bankCode = bankCode; this.bankName = bankName; this.organizationId = organizationId; } private String accountId() { - return "ACCOUNT_" + walletId; + return accountId; } } 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 index 6a6da69..b43ae4a 100644 --- a/src/main/java/com/yau/digitalrmb/payment/infrastructure/persistence/TeachingPaymentPayer.java +++ b/src/main/java/com/yau/digitalrmb/payment/infrastructure/persistence/TeachingPaymentPayer.java @@ -4,6 +4,8 @@ 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.payment.domain.model.TeachingPaymentWalletIdentity; +import com.yau.digitalrmb.payment.domain.model.TeachingPaymentWalletIdentity.SubjectWallets; import com.yau.digitalrmb.shared.api.ErrorCode; import com.yau.digitalrmb.shared.exception.BusinessException; import com.yau.digitalrmb.shared.wallet.WalletPrivateKeyCipher; @@ -13,30 +15,23 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import java.math.BigInteger; +import java.math.BigDecimal; 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. + * case, not the learner's personal wallet from module three. Every learner gets + * an isolated persistence wallet while the API keeps displaying the prototype's + * fixed user-A wallet identifier. */ @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"; + public static final String WALLET_ID = TeachingPaymentWalletIdentity.PAYER_DISPLAY_WALLET_ID; 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 static final String WALLET_TYPE = "一类钱包"; private final JdbcTemplate jdbc; private final InstitutionIdentityCryptography cryptography; @@ -56,12 +51,14 @@ public class TeachingPaymentPayer { @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, + SubjectWallets wallets = TeachingPaymentWalletIdentity.forSubject( + subject.getUserId(), subject.getSchoolId(), subject.getClassId()); + ensureWallet(wallets); + ensureCertificate(wallets); + ensureAccount(wallets); + ensureContract(wallets); + ensureCoins(subject, wallets, institution.organizationId, operator); + return new WalletPrerequisiteReference(wallets.payerUserId(), 0L, 0L, wallets.payerWalletId(), BANK_CODE, institution.organizationId); } @@ -84,26 +81,33 @@ public class TeachingPaymentPayer { "请先完成模块一机构标识并取得当前机构标识")); } - private void ensureWallet() { + private void ensureWallet(SubjectWallets wallets) { + String walletId = wallets.payerWalletId(); Integer count = jdbc.queryForObject("SELECT COUNT(*) FROM digital_wallet WHERE wallet_id=?", - Integer.class, WALLET_ID); - if (count != null && count > 0) return; + Integer.class, walletId); + if (count != null && count > 0) { + jdbc.update("UPDATE digital_wallet SET wallet_type=?,updated_at=CURRENT_TIMESTAMP " + + "WHERE wallet_id=? AND wallet_type<>?", + WALLET_TYPE, walletId, WALLET_TYPE); + 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); + walletId, wallets.payerUserId(), WALLET_TYPE); } catch (DuplicateKeyException exception) { if (jdbc.queryForObject("SELECT COUNT(*) FROM digital_wallet WHERE wallet_id=?", - Integer.class, WALLET_ID) == 0) throw exception; + Integer.class, walletId) == 0) throw exception; } } - private void ensureCertificate() { + private void ensureCertificate(SubjectWallets wallets) { + String walletId = wallets.payerWalletId(); Integer count = jdbc.queryForObject("SELECT COUNT(*) FROM wallet_certificate WHERE wallet_id=?", - Integer.class, WALLET_ID); + Integer.class, walletId); if (count != null && count > 0) return; InstitutionSm2KeyPair pair = cryptography.generateSm2KeyPair(); try { @@ -111,40 +115,44 @@ public class TeachingPaymentPayer { "(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())); + wallets.payerCertificateSerial(), walletId, 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; + Integer.class, walletId) == 0) throw exception; } } - private void ensureAccount() { + private void ensureAccount(SubjectWallets wallets) { + String accountId = wallets.payerAccountId(); + String walletId = wallets.payerWalletId(); Integer count = jdbc.queryForObject("SELECT COUNT(*) FROM simulated_bank_account WHERE account_id=?", - Integer.class, ACCOUNT_ID); + Integer.class, accountId); 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"); + accountId, wallets.payerUserId(), 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.class, accountId) == 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); + Integer.class, walletId, accountId); 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); + "VALUES (?,?,'BOUND',CURRENT_TIMESTAMP)", walletId, accountId); } } - private void ensureContract() { + private void ensureContract(SubjectWallets wallets) { + String walletId = wallets.payerWalletId(); Integer count = jdbc.queryForObject("SELECT COUNT(*) FROM wallet_contract WHERE wallet_id=?", - Integer.class, WALLET_ID); + Integer.class, walletId); 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 " + @@ -152,21 +160,25 @@ public class TeachingPaymentPayer { "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", + wallets.payerContractId(), walletId, WALLET_TYPE, "50000.00", "100000.00", "500000.00", "500000.00", original, cryptography.sm3(original)); } - private void ensureCoins(InstitutionKeySubject subject, String organizationId, String operator) { + private void ensureCoins(InstitutionKeySubject subject, SubjectWallets wallets, + String organizationId, String operator) { + String walletId = wallets.payerWalletId(); 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); + Integer.class, walletId); 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 currencyId = wallets.payerCurrencyId(index); + BigDecimal denomination = teachingDenomination(index); + String original = "TEACHING_ISSUE|" + currencyId + "|" + organizationId + "|" + + denomination.toPlainString(); String requestId = UUID.nameUUIDFromBytes((currencyId + "|" + organizationId) .getBytes(StandardCharsets.UTF_8)).toString(); - long sourceId = COIN_SOURCE_BASE.add(BigInteger.valueOf(index)).longValue(); + long sourceId = wallets.payerSourceId(index); Integer exists = jdbc.queryForObject("SELECT COUNT(*) FROM commercial_bank_currency WHERE currency_id=?", Integer.class, currencyId); if (exists == null || exists == 0) { @@ -175,8 +187,8 @@ public class TeachingPaymentPayer { "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, + currencyId, sourceId, wallets.payerSourceBatchId(), requestId, BANK_CODE, organizationId, + denomination, "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=?", @@ -185,11 +197,17 @@ public class TeachingPaymentPayer { 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); + currencyId, walletId); } } } + private BigDecimal teachingDenomination(int index) { + if (index <= 2) return new BigDecimal("100.00"); + if (index < 10) return new BigDecimal("1000.00"); + return new BigDecimal("2800.00"); + } + private static final class Institution { private final String bankCode; private final String organizationId; diff --git a/src/main/java/com/yau/digitalrmb/shared/wallet/WalletRuntimeResetService.java b/src/main/java/com/yau/digitalrmb/shared/wallet/WalletRuntimeResetService.java index 4c5a0aa..975e783 100644 --- a/src/main/java/com/yau/digitalrmb/shared/wallet/WalletRuntimeResetService.java +++ b/src/main/java/com/yau/digitalrmb/shared/wallet/WalletRuntimeResetService.java @@ -1,6 +1,8 @@ package com.yau.digitalrmb.shared.wallet; import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; +import com.yau.digitalrmb.payment.domain.model.TeachingPaymentWalletIdentity; +import com.yau.digitalrmb.payment.domain.model.TeachingPaymentWalletIdentity.SubjectWallets; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -31,11 +33,19 @@ public class WalletRuntimeResetService { String.class, userId, userId); List ownWalletIds = jdbc.queryForList( "SELECT wallet_id FROM digital_wallet WHERE user_id=?", String.class, userId); + SubjectWallets teachingWallets = TeachingPaymentWalletIdentity.forSubject( + subject.getUserId(), subject.getSchoolId(), subject.getClassId()); + for (String walletId : teachingWallets.walletIds()) { + Integer exists = jdbc.queryForObject("SELECT COUNT(*) FROM digital_wallet WHERE wallet_id=?", + Integer.class, walletId); + if (exists != null && exists > 0) ownWalletIds.add(walletId); + } int restoredCoins = restoreCoinsToBankVault(exchangeIds, paymentIds, ownWalletIds); reverseCounterpartyBalances(paymentIds, userId); deleteExchangeRows(exchangeIds, userId); deletePaymentRows(paymentIds); + deleteTeachingCoins(teachingWallets.payerCurrencyIds()); int deletedWallets = deleteSharedRuntime(userId, ownWalletIds); int deletedAttempts = deleteAttempts(userId); deleteTaskProgress(userId); @@ -134,11 +144,20 @@ public class WalletRuntimeResetService { deleteIn("payment_order", "id", paymentIds); } + private void deleteTeachingCoins(List currencyIds) { + deleteIn("central_bank_currency_ownership", "currency_id", currencyIds); + deleteIn("commercial_bank_currency", "currency_id", currencyIds); + } + private int deleteSharedRuntime(String userId, List ownWalletIds) { + List accountIds = ownWalletIds.isEmpty() ? new ArrayList() : jdbc.queryForList( + "SELECT bank_account_id FROM wallet_bank_binding WHERE wallet_id IN (" + + in(ownWalletIds.size()) + ")", String.class, ownWalletIds.toArray()); deleteIn("wallet_bank_binding", "wallet_id", ownWalletIds); deleteIn("wallet_contract", "wallet_id", ownWalletIds); deleteIn("wallet_certificate", "wallet_id", ownWalletIds); - jdbc.update("DELETE FROM digital_wallet WHERE user_id=?", userId); + deleteIn("digital_wallet", "wallet_id", ownWalletIds); + deleteIn("simulated_bank_account", "account_id", accountIds); jdbc.update("DELETE FROM simulated_bank_account WHERE user_id=?", userId); return ownWalletIds.size(); } diff --git a/src/main/java/com/yau/digitalrmb/training/attempt/application/ExperimentActionFailedEvent.java b/src/main/java/com/yau/digitalrmb/training/attempt/application/ExperimentActionFailedEvent.java new file mode 100644 index 0000000..78dcde4 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/training/attempt/application/ExperimentActionFailedEvent.java @@ -0,0 +1,46 @@ +package com.yau.digitalrmb.training.attempt.application; + +import com.yau.digitalrmb.shared.exception.BusinessException; + +import java.util.UUID; + +final class ExperimentActionFailedEvent { + private static final int MAX_MESSAGE_LENGTH = 512; + + private final UUID attemptId; + private final String stepCode; + private final String actionCode; + private final String requestFingerprint; + private final String errorCode; + private final String errorMessage; + + private ExperimentActionFailedEvent(UUID attemptId, String stepCode, String actionCode, + String requestFingerprint, String errorCode, String errorMessage) { + this.attemptId = attemptId; + this.stepCode = stepCode; + this.actionCode = actionCode; + this.requestFingerprint = requestFingerprint; + this.errorCode = errorCode; + this.errorMessage = errorMessage; + } + + static ExperimentActionFailedEvent from(UUID attemptId, String stepCode, String actionCode, + String requestFingerprint, RuntimeException exception) { + String code = exception instanceof BusinessException + ? ((BusinessException) exception).getErrorCode().name() + : "INTERNAL_ERROR"; + String message = exception.getMessage() == null ? exception.getClass().getSimpleName() : exception.getMessage(); + if (message.length() > MAX_MESSAGE_LENGTH) { + message = message.substring(0, MAX_MESSAGE_LENGTH); + } + return new ExperimentActionFailedEvent( + attemptId, stepCode, actionCode, requestFingerprint, code, message); + } + + UUID getAttemptId() { return attemptId; } + String getStepCode() { return stepCode; } + String getActionCode() { return actionCode; } + String getRequestFingerprint() { return requestFingerprint; } + String getErrorCode() { return errorCode; } + String getErrorMessage() { return errorMessage; } +} diff --git a/src/main/java/com/yau/digitalrmb/training/attempt/application/ExperimentActionFailureListener.java b/src/main/java/com/yau/digitalrmb/training/attempt/application/ExperimentActionFailureListener.java new file mode 100644 index 0000000..7fc8831 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/training/attempt/application/ExperimentActionFailureListener.java @@ -0,0 +1,42 @@ +package com.yau.digitalrmb.training.attempt.application; + +import com.yau.digitalrmb.training.attempt.domain.ExperimentAction; +import com.yau.digitalrmb.training.attempt.domain.ExperimentAttemptRepository; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +import java.time.Instant; +import java.util.UUID; + +@Component +public class ExperimentActionFailureListener { + private final ExperimentAttemptRepository repository; + + public ExperimentActionFailureListener(ExperimentAttemptRepository repository) { + this.repository = repository; + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK) + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void recordFailure(ExperimentActionFailedEvent event) { + ExperimentAction action = repository.findActionForUpdate( + event.getAttemptId(), event.getStepCode(), event.getActionCode()) + .orElse(null); + boolean insert = action == null; + if (insert) { + action = ExperimentAction.running(UUID.randomUUID(), event.getAttemptId(), + event.getStepCode(), event.getActionCode(), event.getRequestFingerprint()); + } else if (!event.getRequestFingerprint().equals(action.getRequestFingerprint())) { + action.restart(event.getRequestFingerprint(), Instant.now()); + } + action.fail(event.getErrorCode(), event.getErrorMessage(), Instant.now()); + if (insert) { + repository.insertAction(action); + } else { + repository.saveAction(action); + } + } +} diff --git a/src/main/java/com/yau/digitalrmb/training/attempt/application/ExperimentAttemptService.java b/src/main/java/com/yau/digitalrmb/training/attempt/application/ExperimentAttemptService.java index 37b800e..b36a1a5 100644 --- a/src/main/java/com/yau/digitalrmb/training/attempt/application/ExperimentAttemptService.java +++ b/src/main/java/com/yau/digitalrmb/training/attempt/application/ExperimentAttemptService.java @@ -13,6 +13,7 @@ import com.yau.digitalrmb.training.attempt.domain.ExperimentModule; import com.yau.digitalrmb.training.attempt.domain.ExperimentSubject; import com.yau.digitalrmb.training.attempt.interfaces.dto.ExperimentActionView; import com.yau.digitalrmb.training.attempt.interfaces.dto.ExperimentAttemptView; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -26,12 +27,15 @@ public class ExperimentAttemptService { private final ExperimentAttemptRepository repository; private final ObjectMapper objectMapper; private final List cancellationHandlers; + private final ApplicationEventPublisher eventPublisher; public ExperimentAttemptService(ExperimentAttemptRepository repository, ObjectMapper objectMapper, - List cancellationHandlers) { + List cancellationHandlers, + ApplicationEventPublisher eventPublisher) { this.repository = repository; this.objectMapper = objectMapper; this.cancellationHandlers = cancellationHandlers; + this.eventPublisher = eventPublisher; } @Transactional @@ -102,7 +106,14 @@ public class ExperimentAttemptService { throw validation("该动作正在处理中,请勿重复提交"); } - ActionOutcome outcome = work.run(); + ActionOutcome outcome; + try { + outcome = work.run(); + } catch (RuntimeException exception) { + eventPublisher.publishEvent(ExperimentActionFailedEvent.from( + attemptId, stepCode, actionCode, requestFingerprint, exception)); + throw exception; + } if (outcome == null) { throw new BusinessException(ErrorCode.INTERNAL_ERROR, "动作未返回执行结果"); } diff --git a/src/test/java/com/yau/digitalrmb/exchange/application/service/ExchangeTrainingActionServiceTest.java b/src/test/java/com/yau/digitalrmb/exchange/application/service/ExchangeTrainingActionServiceTest.java new file mode 100644 index 0000000..e9ba772 --- /dev/null +++ b/src/test/java/com/yau/digitalrmb/exchange/application/service/ExchangeTrainingActionServiceTest.java @@ -0,0 +1,67 @@ +package com.yau.digitalrmb.exchange.application.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yau.digitalrmb.exchange.domain.model.ExchangeActor; +import com.yau.digitalrmb.exchange.interfaces.dto.ExchangeActionRequest; +import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography; +import com.yau.digitalrmb.training.attempt.application.ExperimentAttemptService; +import com.yau.digitalrmb.training.attempt.domain.ActionStatus; +import com.yau.digitalrmb.training.attempt.domain.AttemptStatus; +import com.yau.digitalrmb.training.attempt.domain.ExperimentModule; +import com.yau.digitalrmb.training.attempt.domain.ExperimentSubject; +import com.yau.digitalrmb.training.attempt.interfaces.dto.ExperimentActionView; +import com.yau.digitalrmb.training.attempt.interfaces.dto.ExperimentAttemptView; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class ExchangeTrainingActionServiceTest { + + @Test + void scoresOnlyAfterTheLastActionInAnExchangeStep() { + ExperimentAttemptService attempts = mock(ExperimentAttemptService.class); + ExchangeApplicationService exchanges = mock(ExchangeApplicationService.class); + ExchangeScoreService scoreService = mock(ExchangeScoreService.class); + ExchangeTrainingActionService service = new ExchangeTrainingActionService(attempts, exchanges, + mock(InstitutionIdentityCryptography.class), new ObjectMapper(), scoreService); + UUID attemptId = UUID.randomUUID(); + ExperimentSubject subject = new ExperimentSubject("user-1", 1L, 2L, ""); + ExchangeActor actor = new ExchangeActor("user-1", "tester", 1L, 2L); + when(attempts.execute(any(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenReturn(action(attemptId, "01", "refresh-wallet")); + when(attempts.detail(attemptId, subject)).thenReturn(attempt(attemptId)); + + service.execute(attemptId, "01", "refresh-wallet", new ExchangeActionRequest(), subject, actor); + verify(scoreService, never()).markStepCompleted(subject.getUserId(), 1); + + when(attempts.detail(attemptId, subject)).thenReturn(attempt(attemptId, + action(attemptId, "01", "refresh-wallet"), + action(attemptId, "01", "confirm-binding"))); + service.execute(attemptId, "01", "refresh-institution", new ExchangeActionRequest(), subject, actor); + + verify(scoreService, times(1)).markStepCompleted(subject.getUserId(), 1); + } + + private ExperimentAttemptView attempt(UUID attemptId, ExperimentActionView... completedActions) { + return new ExperimentAttemptView(attemptId, ExperimentModule.EXCHANGE, 1, + AttemptStatus.IN_PROGRESS, "01", "refresh-wallet", null, + Instant.now(), Instant.now(), null, + completedActions.length == 0 ? Collections.emptyList() : Arrays.asList(completedActions)); + } + + private ExperimentActionView action(UUID attemptId, String step, String action) { + return new ExperimentActionView(attemptId, ExperimentModule.EXCHANGE, step, action, + ActionStatus.COMPLETED, AttemptStatus.IN_PROGRESS, step, action, + Instant.now(), null); + } +} diff --git a/src/test/java/com/yau/digitalrmb/issuance/application/service/IssuanceTrainingActionServiceTest.java b/src/test/java/com/yau/digitalrmb/issuance/application/service/IssuanceTrainingActionServiceTest.java index c500a70..5c8a7e8 100644 --- a/src/test/java/com/yau/digitalrmb/issuance/application/service/IssuanceTrainingActionServiceTest.java +++ b/src/test/java/com/yau/digitalrmb/issuance/application/service/IssuanceTrainingActionServiceTest.java @@ -26,12 +26,16 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class IssuanceTrainingActionServiceTest { private ExperimentAttemptService attempts; private CommercialBankIssuanceApplicationService commercial; + private DigitalCurrencyOwnershipConfirmationService ownership; + private IssuanceScoreService scoreService; private IssuanceTrainingActionService service; private final UUID attemptId = UUID.fromString("00000000-0000-0000-0000-000000000201"); private final ExperimentSubject subject = new ExperimentSubject("user-1", 10L, 20L, ""); @@ -42,10 +46,12 @@ class IssuanceTrainingActionServiceTest { void setUp() { attempts = mock(ExperimentAttemptService.class); commercial = mock(CommercialBankIssuanceApplicationService.class); + ownership = mock(DigitalCurrencyOwnershipConfirmationService.class); + scoreService = mock(IssuanceScoreService.class); service = new IssuanceTrainingActionService(attempts, new ObjectMapper(), commercial, mock(CentralBankIssuanceQueryService.class), mock(CentralBankIssuanceBusinessReviewService.class), mock(ReserveDeductionNotificationService.class), mock(ReserveDeductionExecutionService.class), - mock(DigitalCurrencyDraftProductionService.class), mock(DigitalCurrencyOwnershipConfirmationService.class)); + mock(DigitalCurrencyDraftProductionService.class), ownership, scoreService); when(attempts.detail(attemptId, subject)).thenReturn(attemptView( "00000000-0000-0000-0000-000000000298")); when(attempts.execute(any(UUID.class), any(ExperimentSubject.class), anyString(), anyString(), anyString(), @@ -103,6 +109,33 @@ class IssuanceTrainingActionServiceTest { assertThat(new ObjectMapper().valueToTree(outcome.getOutput()).toString()).doesNotContain("privateKey"); } + @Test + void scoresOnlyAfterTheLastActionInAnIssuanceStep() { + service.execute(attemptId, "01", "refresh-quota", new IssuanceActionRequest(), subject, actor, keySubject); + + verify(scoreService, never()).markStepCompleted(subject.getUserId(), 1); + + service.execute(attemptId, "01", "send-application", new IssuanceActionRequest(), subject, actor, keySubject); + + verify(scoreService, times(1)).markStepCompleted(subject.getUserId(), 1); + } + + @Test + @SuppressWarnings("unchecked") + void completesTheAttemptWhenTheOwnershipResultIsLoaded() { + UUID requestId = UUID.fromString("00000000-0000-0000-0000-000000000298"); + when(ownership.findProduction(requestId)).thenReturn(java.util.Optional.of( + mock(com.yau.digitalrmb.issuance.application.query.DigitalCurrencyProductionBatchView.class))); + ArgumentCaptor> work = ArgumentCaptor.forClass(ActionWork.class); + + service.execute(attemptId, "07", "load-ownership-result", new IssuanceActionRequest(), + subject, actor, keySubject); + verify(attempts).execute(any(UUID.class), any(ExperimentSubject.class), anyString(), anyString(), anyString(), + any(Class.class), work.capture(), anyString(), anyString()); + + assertThat(work.getValue().run().isCompletesAttempt()).isTrue(); + } + private IssuanceActionRequest validCreateRequest() { IssuanceActionRequest request = new IssuanceActionRequest(); request.setBankCode("BKCHCNBJ00001"); diff --git a/src/test/java/com/yau/digitalrmb/issuance/interfaces/rest/IssuanceControllerTest.java b/src/test/java/com/yau/digitalrmb/issuance/interfaces/rest/IssuanceControllerTest.java index 72c0746..e2562f5 100644 --- a/src/test/java/com/yau/digitalrmb/issuance/interfaces/rest/IssuanceControllerTest.java +++ b/src/test/java/com/yau/digitalrmb/issuance/interfaces/rest/IssuanceControllerTest.java @@ -358,7 +358,8 @@ class IssuanceControllerTest { .with(jwt().jwt(jwt -> jwt.subject("00000000-0000-0000-0000-000000000487").claim("userId", "00000000-0000-0000-0000-000000000487").claim("preferred_username", "tzs001"))) .contentType(MediaType.APPLICATION_JSON).content(requestBody)) .andExpect(jsonPath("$.code").value(400)) - .andExpect(jsonPath("$.message").value("面额必须包含全部 11 种标准面额")); + .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.startsWith( + "面额必须包含全部 11 种标准面额"))); } @Test diff --git a/src/test/java/com/yau/digitalrmb/payment/application/service/PaymentScoreServiceTest.java b/src/test/java/com/yau/digitalrmb/payment/application/service/PaymentScoreServiceTest.java index 31aa73d..15d4760 100644 --- a/src/test/java/com/yau/digitalrmb/payment/application/service/PaymentScoreServiceTest.java +++ b/src/test/java/com/yau/digitalrmb/payment/application/service/PaymentScoreServiceTest.java @@ -43,7 +43,7 @@ class PaymentScoreServiceTest { service.recordError("user-1"); assertEquals("3", detail.getCompletionStatus()); - assertEquals(48.5, detail.getScoreProject()); + assertEquals(49.25, detail.getScoreProject()); verify(mapper).updateById(detail); } } diff --git a/src/test/java/com/yau/digitalrmb/payment/application/service/PaymentTrainingActionServiceTest.java b/src/test/java/com/yau/digitalrmb/payment/application/service/PaymentTrainingActionServiceTest.java index 46668b3..5009325 100644 --- a/src/test/java/com/yau/digitalrmb/payment/application/service/PaymentTrainingActionServiceTest.java +++ b/src/test/java/com/yau/digitalrmb/payment/application/service/PaymentTrainingActionServiceTest.java @@ -20,6 +20,9 @@ import java.util.UUID; import static org.assertj.core.api.Assertions.assertThatCode; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class PaymentTrainingActionServiceTest { @@ -40,6 +43,30 @@ class PaymentTrainingActionServiceTest { new PaymentActionRequest(), subject, actor)).doesNotThrowAnyException(); } + @Test + void scoresOnlyAfterTheLastActionInAPaymentStep() { + ExperimentAttemptService attempts = mock(ExperimentAttemptService.class); + PaymentApplicationService payments = mock(PaymentApplicationService.class); + PaymentScoreService scoreService = mock(PaymentScoreService.class); + PaymentTrainingActionService service = new PaymentTrainingActionService( + attempts, payments, new ObjectMapper(), scoreService); + UUID attemptId = UUID.randomUUID(); + ExperimentSubject subject = new ExperimentSubject("user-1", 1L, 2L, ""); + PaymentActor actor = new PaymentActor("user-1", "tester", 1L, 2L); + when(attempts.execute(any(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenReturn(action(attemptId, "01", "refresh-payer")); + when(attempts.detail(attemptId, subject)).thenReturn(attempt(attemptId)); + + service.execute(attemptId, "01", "refresh-payer", new PaymentActionRequest(), subject, actor); + verify(scoreService, never()).markStepCompleted(subject.getUserId(), 1); + + when(attempts.detail(attemptId, subject)).thenReturn(attempt( + attemptId, action(attemptId, "01", "refresh-payer"))); + service.execute(attemptId, "01", "refresh-payee", new PaymentActionRequest(), subject, actor); + + verify(scoreService, times(1)).markStepCompleted(subject.getUserId(), 1); + } + private ExperimentAttemptView completedThroughCompliance(UUID attemptId) { String[][] keys = { {"01", "refresh-payer"}, {"01", "refresh-payee"}, @@ -58,6 +85,12 @@ class PaymentTrainingActionServiceTest { Instant.now(), Instant.now(), null, actions); } + private ExperimentAttemptView attempt(UUID attemptId, ExperimentActionView... completedActions) { + return new ExperimentAttemptView(attemptId, ExperimentModule.PAYMENT, 1, + AttemptStatus.IN_PROGRESS, "01", "refresh-payer", null, + Instant.now(), Instant.now(), null, java.util.Arrays.asList(completedActions)); + } + private ExperimentActionView action(UUID attemptId, String step, String action) { return new ExperimentActionView(attemptId, ExperimentModule.PAYMENT, step, action, ActionStatus.COMPLETED, AttemptStatus.IN_PROGRESS, step, action, diff --git a/src/test/java/com/yau/digitalrmb/payment/interfaces/rest/PaymentControllerTest.java b/src/test/java/com/yau/digitalrmb/payment/interfaces/rest/PaymentControllerTest.java index 8a3d4ae..b2940c0 100644 --- a/src/test/java/com/yau/digitalrmb/payment/interfaces/rest/PaymentControllerTest.java +++ b/src/test/java/com/yau/digitalrmb/payment/interfaces/rest/PaymentControllerTest.java @@ -6,6 +6,11 @@ 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.payment.infrastructure.persistence.TeachingPaymentPayer; +import com.yau.digitalrmb.payment.infrastructure.persistence.TeachingPaymentPayeeWhitelist; +import com.yau.digitalrmb.payment.domain.model.TeachingPaymentWalletIdentity; +import com.yau.digitalrmb.payment.domain.model.TeachingPaymentWalletIdentity.SubjectWallets; +import com.yau.digitalrmb.shared.wallet.WalletRuntimeResetService; import com.yau.digitalrmb.testsupport.WalletOpeningTestData; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -34,14 +39,23 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. class PaymentControllerTest { private static final String PAYER_USER = "00000000-0000-0000-0000-000000000901"; private static final String PAYEE_USER = "00000000-0000-0000-0000-000000000902"; - private static final String PAYER_WALLET = "WALLET_PAYMENT_A"; + private static final String PAYER_WALLET = TeachingPaymentPayer.WALLET_ID; + private static final String TEACHING_PAYEE_WALLET = TeachingPaymentPayeeWhitelist.WALLET_ID; private static final String PAYEE_WALLET = "WALLET_PAYMENT_B"; + private static final SubjectWallets PAYER_TEACHING_WALLETS = + TeachingPaymentWalletIdentity.forSubject(PAYER_USER, 3001L, 2001L); + private static final String PAYER_STORAGE_WALLET = PAYER_TEACHING_WALLETS.payerWalletId(); + private static final String PAYEE_STORAGE_WALLET = PAYER_TEACHING_WALLETS.payeeWalletId(); + private static final String PAYER_COIN_1 = PAYER_TEACHING_WALLETS.payerCurrencyId(1); + private static final SubjectWallets PAYEE_TEACHING_WALLETS = + TeachingPaymentWalletIdentity.forSubject(PAYEE_USER, 3001L, 2001L); @Autowired private MockMvc mockMvc; @Autowired private JdbcTemplate jdbc; @Autowired private InstitutionIdentityCryptography cryptography; @Autowired private WalletPrivateKeyCipher walletCipher; @Autowired private InstitutionKeyService keyService; + @Autowired private WalletRuntimeResetService walletRuntimeResetService; @BeforeEach void seedPrerequisites() { @@ -76,21 +90,9 @@ class PaymentControllerTest { WalletOpeningTestData.insertCompletedWallet(jdbc, 991300L, PAYEE_USER, 3001L, 2001L, PAYEE_WALLET, payeeWalletKey.getPrivateKey(), payeeWalletKey.getPublicKey(), "BKCHCNBJ00002", "6222020000000001", BigDecimal.ZERO); - insertWallet(PAYER_USER, PAYER_WALLET, "CERT_991200", "BKCHCNBJ00001", - "ACCOUNT_PAYMENT_A", new BigDecimal("200.00"), payerWalletKey); insertWallet(PAYEE_USER, PAYEE_WALLET, "CERT_991300", "BKCHCNBJ00002", "ACCOUNT_PAYMENT_B", BigDecimal.ZERO, payeeWalletKey); - insertContract(PAYER_WALLET, "CONTRACT_991200"); insertContract(PAYEE_WALLET, "CONTRACT_991300"); - for (int index = 1; index <= 2; index++) { - 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 (?,?,?,?,?,?,100,'DC',?,'ISSUANCE',?,'TRANSFERRED',CURRENT_TIMESTAMP,?,'test')", - "DC_PAYMENT_" + index, 991100L + index, 991100L, "PAYMENT_TEST_ISSUANCE", "BKCHCNBJ00001", "ORG_PAYMENT_A", - "COMPLETE_PAYMENT_" + index, keyService.signCentralBank(subject, "ISSUANCE"), PAYER_USER); - jdbc.update("INSERT INTO central_bank_currency_ownership (currency_id,owner_type,owner_id,status,last_transaction_id,updated_at) " + - "VALUES (?,'WALLET',?,'AVAILABLE',NULL,CURRENT_TIMESTAMP)", "DC_PAYMENT_" + index, PAYER_WALLET); - } } @AfterEach @@ -116,11 +118,11 @@ class PaymentControllerTest { .andExpect(jsonPath("$.data.payer.organizationId").value("ORG_PAYMENT_A")) .andExpect(jsonPath("$.data.payee.walletId").value(PAYEE_WALLET)) .andExpect(jsonPath("$.data.payee.organizationId").value("ORG_PAYMENT_B")); - assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM digital_wallet", Integer.class)).isEqualTo(walletsBefore); + assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM digital_wallet", Integer.class)).isEqualTo(walletsBefore + 1); assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM wallet_certificate", Integer.class)) - .isEqualTo(certificatesBefore); + .isEqualTo(certificatesBefore + 1); assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM wallet_bank_binding", Integer.class)) - .isEqualTo(bindingsBefore); + .isEqualTo(bindingsBefore + 1); assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM payment_order", Integer.class)).isZero(); String created = mockMvc.perform(post("/api/v1/payments").with(payer()).contentType(MediaType.APPLICATION_JSON) .content("{\"payerWalletId\":\"" + PAYER_WALLET + "\",\"payeeWalletId\":\"" + PAYEE_WALLET + "\",\"amount\":200.00,\"note\":\"实验支付\"}")) @@ -137,13 +139,13 @@ class PaymentControllerTest { .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("PAYER_BANK_ACCEPTED")) .andExpect(jsonPath("$.data.coinCount").value(2)); assertThat(jdbc.queryForObject("SELECT balance FROM digital_wallet WHERE wallet_id=?", - BigDecimal.class, PAYER_WALLET)).isEqualByComparingTo("200.00"); + BigDecimal.class, PAYER_STORAGE_WALLET)).isEqualByComparingTo("10000.00"); assertThat(jdbc.queryForObject("SELECT frozen_amount FROM digital_wallet WHERE wallet_id=?", - BigDecimal.class, PAYER_WALLET)).isEqualByComparingTo("200.00"); + BigDecimal.class, PAYER_STORAGE_WALLET)).isEqualByComparingTo("200.00"); assertThat(jdbc.queryForObject("SELECT status FROM central_bank_currency_ownership " + - "WHERE currency_id='DC_PAYMENT_1'", String.class)).isEqualTo("PAYMENT_LOCKED"); + "WHERE currency_id=?", String.class, PAYER_COIN_1)).isEqualTo("PAYMENT_LOCKED"); assertThat(jdbc.queryForObject("SELECT lock_business_id FROM central_bank_currency_ownership " + - "WHERE currency_id='DC_PAYMENT_1'", String.class)).isEqualTo(id); + "WHERE currency_id=?", String.class, PAYER_COIN_1)).isEqualTo(id); mockMvc.perform(post("/api/v1/payments/{id}/central-bank-settle", id).with(payer())) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("CENTRAL_SETTLED")) .andExpect(jsonPath("$.data.clearingNo").value(org.hamcrest.Matchers.startsWith("ACS_PAY_REQ_"))); @@ -156,7 +158,7 @@ class PaymentControllerTest { .andExpect(jsonPath("$.code").value(400)); mockMvc.perform(post("/api/v1/payments/{id}/payee-bank-credit", id).with(payer())) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("PAYEE_CREDITED")) - .andExpect(jsonPath("$.data.payerBalanceAfter").value(0.00)) + .andExpect(jsonPath("$.data.payerBalanceAfter").value(9800.00)) .andExpect(jsonPath("$.data.payeeBalanceAfter").value(200.00)); mockMvc.perform(post("/api/v1/payments/{id}/payee-bank-credit", id).with(payer())) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("PAYEE_CREDITED")); @@ -181,21 +183,21 @@ class PaymentControllerTest { .andExpect(jsonPath("$.data[2].moduleCode").value("WALLET_OPENING")) .andExpect(jsonPath("$.data[2].actionUnavailable").value(true)); - assertThat(jdbc.queryForObject("SELECT balance FROM digital_wallet WHERE wallet_id=?", BigDecimal.class, PAYER_WALLET)) - .isEqualByComparingTo("0.00"); - assertThat(jdbc.queryForObject("SELECT frozen_amount FROM digital_wallet WHERE wallet_id=?", BigDecimal.class, PAYER_WALLET)) + assertThat(jdbc.queryForObject("SELECT balance FROM digital_wallet WHERE wallet_id=?", BigDecimal.class, PAYER_STORAGE_WALLET)) + .isEqualByComparingTo("9800.00"); + assertThat(jdbc.queryForObject("SELECT frozen_amount FROM digital_wallet WHERE wallet_id=?", BigDecimal.class, PAYER_STORAGE_WALLET)) .isEqualByComparingTo("0.00"); assertThat(jdbc.queryForObject("SELECT balance FROM digital_wallet WHERE wallet_id=?", BigDecimal.class, PAYEE_WALLET)) .isEqualByComparingTo("200.00"); assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM payment_wallet_ledger WHERE payment_id=?", Integer.class, id)).isEqualTo(2); - assertThat(jdbc.queryForObject("SELECT owner_id FROM central_bank_currency_ownership WHERE currency_id='DC_PAYMENT_1'", String.class)) + assertThat(jdbc.queryForObject("SELECT owner_id FROM central_bank_currency_ownership WHERE currency_id=?", String.class, PAYER_COIN_1)) .isEqualTo(PAYEE_WALLET); assertThat(jdbc.queryForObject("SELECT lock_business_id FROM central_bank_currency_ownership " + - "WHERE currency_id='DC_PAYMENT_1'", String.class)).isNull(); + "WHERE currency_id=?", String.class, PAYER_COIN_1)).isNull(); assertThat(jdbc.queryForObject("SELECT last_transaction_id FROM central_bank_currency_ownership " + - "WHERE currency_id='DC_PAYMENT_1'", String.class)).startsWith("PAY_TXN_"); - assertThat(jdbc.queryForObject("SELECT status FROM commercial_bank_currency WHERE currency_id='DC_PAYMENT_1'", - String.class)).isEqualTo("TRANSFERRED"); + "WHERE currency_id=?", String.class, PAYER_COIN_1)).startsWith("PAY_TXN_"); + assertThat(jdbc.queryForObject("SELECT status FROM commercial_bank_currency WHERE currency_id=?", + String.class, PAYER_COIN_1)).isEqualTo("可用"); } @Test @@ -209,7 +211,24 @@ class PaymentControllerTest { .andExpect(jsonPath("$.data.payee.bankCode").value("BKCHCNBJ00002")); assertThat(jdbc.queryForObject("SELECT status FROM digital_wallet WHERE wallet_id=?", String.class, - "WALLET_B_7D8E9F0A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0C1D2E3F4A5B6")).isEqualTo("ACTIVE"); + PAYEE_STORAGE_WALLET)).isEqualTo("ACTIVE"); + } + + @Test + void repairsTheLegacyTeachingPayerTypeWithoutResettingItsBalance() throws Exception { + mockMvc.perform(get("/api/v1/payments/context").with(payer())).andExpect(status().isOk()); + jdbc.update("UPDATE digital_wallet SET wallet_type='二类钱包',balance=321.00 WHERE wallet_id=?", + PAYER_STORAGE_WALLET); + + mockMvc.perform(get("/api/v1/payments/context").with(payer())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.payer.walletType").value("一类钱包")) + .andExpect(jsonPath("$.data.payer.balance").value(321.00)); + + assertThat(jdbc.queryForObject("SELECT wallet_type FROM digital_wallet WHERE wallet_id=?", + String.class, PAYER_STORAGE_WALLET)).isEqualTo("一类钱包"); + assertThat(jdbc.queryForObject("SELECT balance FROM digital_wallet WHERE wallet_id=?", + BigDecimal.class, PAYER_STORAGE_WALLET)).isEqualByComparingTo("321.00"); } @Test @@ -238,11 +257,76 @@ class PaymentControllerTest { "WALLET_B_7D8E9F0A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0C1D2E3F4A5B6")); } + @Test + void isolatesPrototypeWalletBalancesCoinsAndResetByLearner() throws Exception { + mockMvc.perform(get("/api/v1/payments/context").with(payer())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.payer.walletId").value(PAYER_WALLET)) + .andExpect(jsonPath("$.data.payee.walletId").value(TEACHING_PAYEE_WALLET)) + .andExpect(jsonPath("$.data.payer.storageWalletId").doesNotExist()) + .andExpect(jsonPath("$.data.payee.storageWalletId").doesNotExist()); + mockMvc.perform(get("/api/v1/payments/context").with(payee())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.payer.walletId").value(PAYER_WALLET)) + .andExpect(jsonPath("$.data.payee.walletId").value(TEACHING_PAYEE_WALLET)); + + assertThat(PAYER_TEACHING_WALLETS.payerWalletId()) + .isNotEqualTo(PAYEE_TEACHING_WALLETS.payerWalletId()); + assertThat(jdbc.queryForObject("SELECT balance FROM digital_wallet WHERE wallet_id=?", + BigDecimal.class, PAYEE_TEACHING_WALLETS.payerWalletId())).isEqualByComparingTo("10000.00"); + assertThat(jdbc.queryForObject("SELECT balance FROM digital_wallet WHERE wallet_id=?", + BigDecimal.class, PAYEE_TEACHING_WALLETS.payeeWalletId())).isEqualByComparingTo("0.00"); + + String created = mockMvc.perform(post("/api/v1/payments").with(payer()) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"payerWalletId\":\"" + PAYER_WALLET + "\",\"payeeWalletId\":\"" + + TEACHING_PAYEE_WALLET + "\",\"amount\":1000.00,\"note\":\"房租\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.payerWalletId").value(PAYER_WALLET)) + .andExpect(jsonPath("$.data.payeeWalletId").value(TEACHING_PAYEE_WALLET)) + .andExpect(jsonPath("$.data.paymentOriginalText").value( + org.hamcrest.Matchers.containsString("|" + TEACHING_PAYEE_WALLET + "|1000.00|"))) + .andReturn().getResponse().getContentAsString(); + String id = JsonPath.read(created, "$.data.id"); + mockMvc.perform(post("/api/v1/payments/{id}/sign", id).with(payer())).andExpect(status().isOk()); + mockMvc.perform(post("/api/v1/payments/{id}/payer-bank-process", id).with(payer())) + .andExpect(status().isOk()); + mockMvc.perform(post("/api/v1/payments/{id}/central-bank-settle", id).with(payer())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.settlementOriginalText").value( + org.hamcrest.Matchers.containsString("|" + PAYER_WALLET + "|" + + TEACHING_PAYEE_WALLET + "|"))); + mockMvc.perform(post("/api/v1/payments/{id}/payee-bank-credit", id).with(payer())) + .andExpect(status().isOk()); + + assertThat(jdbc.queryForObject("SELECT payer_wallet_id FROM payment_order WHERE id=?", + String.class, id)).isEqualTo(PAYER_STORAGE_WALLET); + assertThat(jdbc.queryForObject("SELECT payee_wallet_id FROM payment_order WHERE id=?", + String.class, id)).isEqualTo(PAYEE_STORAGE_WALLET); + assertThat(jdbc.queryForObject("SELECT balance FROM digital_wallet WHERE wallet_id=?", + BigDecimal.class, PAYER_STORAGE_WALLET)).isEqualByComparingTo("9000.00"); + assertThat(jdbc.queryForObject("SELECT balance FROM digital_wallet WHERE wallet_id=?", + BigDecimal.class, PAYEE_STORAGE_WALLET)).isEqualByComparingTo("1000.00"); + assertThat(jdbc.queryForObject("SELECT balance FROM digital_wallet WHERE wallet_id=?", + BigDecimal.class, PAYEE_TEACHING_WALLETS.payerWalletId())).isEqualByComparingTo("10000.00"); + assertThat(jdbc.queryForObject("SELECT balance FROM digital_wallet WHERE wallet_id=?", + BigDecimal.class, PAYEE_TEACHING_WALLETS.payeeWalletId())).isEqualByComparingTo("0.00"); + + walletRuntimeResetService.reset(new InstitutionKeySubject(PAYER_USER, 3001L, 2001L)); + assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM digital_wallet WHERE wallet_id IN (?,?)", + Integer.class, PAYER_STORAGE_WALLET, PAYEE_STORAGE_WALLET)).isZero(); + assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM digital_wallet WHERE wallet_id IN (?,?)", + Integer.class, PAYEE_TEACHING_WALLETS.payerWalletId(), + PAYEE_TEACHING_WALLETS.payeeWalletId())).isEqualTo(2); + assertThat(jdbc.queryForObject("SELECT balance FROM digital_wallet WHERE wallet_id=?", + BigDecimal.class, PAYEE_TEACHING_WALLETS.payerWalletId())).isEqualByComparingTo("10000.00"); + } + @Test void refreshActionsProjectCompletedModuleThreeWalletsWhenSharedRowsAreMissing() throws Exception { clearSharedWalletProjection(); assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM digital_wallet WHERE wallet_id IN (?,?)", - Integer.class, PAYER_WALLET, PAYEE_WALLET)).isZero(); + Integer.class, PAYER_STORAGE_WALLET, PAYEE_STORAGE_WALLET)).isZero(); String created = mockMvc.perform(post("/api/v1/payment/attempts").with(payer())) .andExpect(status().isOk()) @@ -262,13 +346,13 @@ class PaymentControllerTest { .andExpect(jsonPath("$.data.output.walletId").value(PAYEE_WALLET)); assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM digital_wallet WHERE wallet_id IN (?,?)", - Integer.class, PAYER_WALLET, PAYEE_WALLET)).isEqualTo(2); + Integer.class, PAYER_STORAGE_WALLET, PAYEE_STORAGE_WALLET)).isEqualTo(1); assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM wallet_certificate WHERE wallet_id IN (?,?)", - Integer.class, PAYER_WALLET, PAYEE_WALLET)).isEqualTo(2); + Integer.class, PAYER_STORAGE_WALLET, PAYEE_STORAGE_WALLET)).isEqualTo(1); assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM wallet_contract WHERE wallet_id IN (?,?)", - Integer.class, PAYER_WALLET, PAYEE_WALLET)).isEqualTo(2); + Integer.class, PAYER_STORAGE_WALLET, PAYEE_STORAGE_WALLET)).isEqualTo(1); assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM wallet_bank_binding WHERE wallet_id IN (?,?)", - Integer.class, PAYER_WALLET, PAYEE_WALLET)).isEqualTo(2); + Integer.class, PAYER_STORAGE_WALLET, PAYEE_STORAGE_WALLET)).isEqualTo(1); } @Test @@ -282,7 +366,7 @@ class PaymentControllerTest { mockMvc.perform(post("/api/v1/payments/{id}/sign", id).with(payer())) .andExpect(status().isOk()); - jdbc.update("DELETE FROM central_bank_currency_ownership WHERE owner_id=?", PAYER_WALLET); + jdbc.update("DELETE FROM central_bank_currency_ownership WHERE owner_id=?", PAYER_STORAGE_WALLET); mockMvc.perform(post("/api/v1/payments/{id}/payer-bank-process", id).with(payer())) .andExpect(status().isBadRequest()) @@ -290,7 +374,7 @@ class PaymentControllerTest { assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM payment_ownership_transfer WHERE payment_id=?", Integer.class, id)).isZero(); assertThat(jdbc.queryForObject("SELECT frozen_amount FROM digital_wallet WHERE wallet_id=?", - BigDecimal.class, PAYER_WALLET)).isEqualByComparingTo("0.00"); + BigDecimal.class, PAYER_STORAGE_WALLET)).isEqualByComparingTo("0.00"); } @Test @@ -304,7 +388,7 @@ class PaymentControllerTest { mockMvc.perform(post("/api/v1/payments/{id}/payer-bank-process", id).with(payer())) .andExpect(status().isBadRequest()); assertThat(jdbc.queryForObject("SELECT frozen_amount FROM digital_wallet WHERE wallet_id=?", - BigDecimal.class, PAYER_WALLET)).isEqualByComparingTo("0.00"); + BigDecimal.class, PAYER_STORAGE_WALLET)).isEqualByComparingTo("0.00"); mockMvc.perform(post("/api/v1/payments/{id}/sign", id).with(payer())).andExpect(status().isOk()); mockMvc.perform(post("/api/v1/payments/{id}/payer-bank-process", id).with(payer())).andExpect(status().isOk()); @@ -328,26 +412,12 @@ class PaymentControllerTest { } @Test - void transferredCoinsCanBeReservedByThePayeeInALaterPayment() throws Exception { - String firstId = createSignedPayment(PAYER_WALLET, PAYEE_WALLET, payer()); - mockMvc.perform(post("/api/v1/payments/{id}/payer-bank-process", firstId).with(payer())) - .andExpect(status().isOk()); - mockMvc.perform(post("/api/v1/payments/{id}/central-bank-settle", firstId).with(payer())) - .andExpect(status().isOk()); - mockMvc.perform(post("/api/v1/payments/{id}/payee-bank-credit", firstId).with(payer())) - .andExpect(status().isOk()); - - String secondId = createSignedPayment(PAYEE_WALLET, PAYER_WALLET, payee()); - mockMvc.perform(post("/api/v1/payments/{id}/payer-bank-process", secondId).with(payee())) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.coinCount").value(2)); - - assertThat(jdbc.queryForObject("SELECT status FROM central_bank_currency_ownership " + - "WHERE currency_id='DC_PAYMENT_1'", String.class)).isEqualTo("PAYMENT_LOCKED"); - assertThat(jdbc.queryForObject("SELECT lock_business_type FROM central_bank_currency_ownership " + - "WHERE currency_id='DC_PAYMENT_1'", String.class)).isEqualTo("PAYMENT"); - assertThat(jdbc.queryForObject("SELECT lock_business_id FROM central_bank_currency_ownership " + - "WHERE currency_id='DC_PAYMENT_1'", String.class)).isEqualTo(secondId); + void rejectsReplacingThePrototypePayerWithTheLoggedInUsersWallet() throws Exception { + mockMvc.perform(post("/api/v1/payments").with(payee()).contentType(MediaType.APPLICATION_JSON) + .content("{\"payerWalletId\":\"" + PAYEE_WALLET + "\",\"payeeWalletId\":\"" + + PAYER_WALLET + "\",\"amount\":200.00,\"note\":\"replace prototype payer\"}")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(400)); } @Test @@ -368,11 +438,11 @@ class PaymentControllerTest { .andExpect(jsonPath("$.data.status").value("PAYER_SIGNED")) .andExpect(jsonPath("$.data.coinCount").value(0)); assertThat(jdbc.queryForObject("SELECT frozen_amount FROM digital_wallet WHERE wallet_id=?", - BigDecimal.class, PAYER_WALLET)).isEqualByComparingTo("0.00"); + BigDecimal.class, PAYER_STORAGE_WALLET)).isEqualByComparingTo("0.00"); assertThat(jdbc.queryForObject("SELECT status FROM central_bank_currency_ownership " + - "WHERE currency_id='DC_PAYMENT_1'", String.class)).isEqualTo("AVAILABLE"); + "WHERE currency_id=?", String.class, PAYER_COIN_1)).isEqualTo("AVAILABLE"); assertThat(jdbc.queryForObject("SELECT lock_business_id FROM central_bank_currency_ownership " + - "WHERE currency_id='DC_PAYMENT_1'", String.class)).isNull(); + "WHERE currency_id=?", String.class, PAYER_COIN_1)).isNull(); assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM payment_coin_reservation WHERE payment_id=?", Integer.class, id)).isZero(); } @@ -391,11 +461,11 @@ class PaymentControllerTest { .andExpect(status().isOk()) .andExpect(jsonPath("$.data.status").value("CENTRAL_OWNERSHIP_TRANSFERRED")); assertThat(jdbc.queryForObject("SELECT owner_id FROM central_bank_currency_ownership " + - "WHERE currency_id='DC_PAYMENT_1'", String.class)).isEqualTo(PAYEE_WALLET); + "WHERE currency_id=?", String.class, PAYER_COIN_1)).isEqualTo(PAYEE_WALLET); assertThat(jdbc.queryForObject("SELECT balance FROM digital_wallet WHERE wallet_id=?", - BigDecimal.class, PAYER_WALLET)).isEqualByComparingTo("200.00"); + BigDecimal.class, PAYER_STORAGE_WALLET)).isEqualByComparingTo("10000.00"); assertThat(jdbc.queryForObject("SELECT frozen_amount FROM digital_wallet WHERE wallet_id=?", - BigDecimal.class, PAYER_WALLET)).isEqualByComparingTo("200.00"); + BigDecimal.class, PAYER_STORAGE_WALLET)).isEqualByComparingTo("200.00"); mockMvc.perform(post("/api/v1/payment/attempts/{id}/cancel", attemptId).with(payer())) .andExpect(status().isBadRequest()); @@ -444,8 +514,10 @@ class PaymentControllerTest { private void insertWallet(String userId, String walletId, String certificate, String bankCode, String accountId, BigDecimal balance, InstitutionSm2KeyPair key) { + String walletType = PAYER_WALLET.equals(walletId) ? "一类钱包" : "TYPE_II"; 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 (?,?,'TYPE_II','ACTIVE',?,0,'CB_FINAL_TEST',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", walletId, userId, balance); + "VALUES (?,?,?,'ACTIVE',?,0,'CB_FINAL_TEST',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", + walletId, userId, walletType, balance); 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','CB_ROOT_TEST','VALID',CURRENT_TIMESTAMP)", certificate, walletId, key.getPublicKey(), walletCipher.encrypt(key.getPrivateKey())); 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) " + @@ -454,9 +526,10 @@ class PaymentControllerTest { } private void insertContract(String walletId, String contractId) { + String walletType = PAYER_WALLET.equals(walletId) ? "一类钱包" : "TYPE_II"; 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 (?,?,'TYPE_II',50000,100000,500000,500000,NULL,'CONTRACT','DIGEST','ACTIVE',0,CURRENT_DATE,0,2026,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", - contractId, walletId); + "VALUES (?,?,?,50000,100000,500000,500000,NULL,'CONTRACT','DIGEST','ACTIVE',0,CURRENT_DATE,0,2026,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", + contractId, walletId, walletType); } private void cleanWalletState() { @@ -466,10 +539,26 @@ class PaymentControllerTest { } private void clearSharedWalletProjection() { + jdbc.update("DELETE FROM wallet_bank_binding WHERE wallet_id IN (?,?,?,?)", + PAYER_STORAGE_WALLET, PAYEE_STORAGE_WALLET, + PAYEE_TEACHING_WALLETS.payerWalletId(), PAYEE_TEACHING_WALLETS.payeeWalletId()); + jdbc.update("DELETE FROM wallet_contract WHERE wallet_id IN (?,?,?,?)", + PAYER_STORAGE_WALLET, PAYEE_STORAGE_WALLET, + PAYEE_TEACHING_WALLETS.payerWalletId(), PAYEE_TEACHING_WALLETS.payeeWalletId()); + jdbc.update("DELETE FROM wallet_certificate WHERE wallet_id IN (?,?,?,?)", + PAYER_STORAGE_WALLET, PAYEE_STORAGE_WALLET, + PAYEE_TEACHING_WALLETS.payerWalletId(), PAYEE_TEACHING_WALLETS.payeeWalletId()); + jdbc.update("DELETE FROM digital_wallet WHERE wallet_id IN (?,?,?,?)", + PAYER_STORAGE_WALLET, PAYEE_STORAGE_WALLET, + PAYEE_TEACHING_WALLETS.payerWalletId(), PAYEE_TEACHING_WALLETS.payeeWalletId()); + jdbc.update("DELETE FROM simulated_bank_account WHERE account_id IN (?,?,?,?)", + PAYER_TEACHING_WALLETS.payerAccountId(), PAYER_TEACHING_WALLETS.payeeAccountId(), + PAYEE_TEACHING_WALLETS.payerAccountId(), PAYEE_TEACHING_WALLETS.payeeAccountId()); jdbc.update("DELETE FROM wallet_bank_binding WHERE wallet_id IN (?,?)", PAYER_WALLET, PAYEE_WALLET); jdbc.update("DELETE FROM wallet_contract WHERE wallet_id IN (?,?)", PAYER_WALLET, PAYEE_WALLET); jdbc.update("DELETE FROM wallet_certificate WHERE wallet_id IN (?,?)", PAYER_WALLET, PAYEE_WALLET); - jdbc.update("DELETE FROM simulated_bank_account WHERE user_id IN (?,?)", PAYER_USER, PAYEE_USER); + jdbc.update("DELETE FROM simulated_bank_account WHERE account_id='ACCOUNT_TEACHING_PAYMENT_PAYER_A' " + + "OR user_id IN (?,?)", PAYER_USER, PAYEE_USER); jdbc.update("DELETE FROM digital_wallet WHERE wallet_id IN (?,?)", PAYER_WALLET, PAYEE_WALLET); } @@ -487,6 +576,10 @@ class PaymentControllerTest { jdbc.update("DELETE FROM payment_coin_reservation"); jdbc.update("DELETE FROM payment_order"); jdbc.update("DELETE FROM central_bank_currency_ownership WHERE currency_id LIKE 'DC_PAYMENT_%'"); + jdbc.update("DELETE FROM central_bank_currency_ownership WHERE currency_id LIKE 'PAYMENT_TEACHING_A_%'"); + jdbc.update("DELETE FROM commercial_bank_currency WHERE currency_id LIKE 'PAYMENT_TEACHING_A_%'"); + jdbc.update("DELETE FROM central_bank_currency_ownership WHERE currency_id LIKE 'PAY_TEACH_A_%'"); + jdbc.update("DELETE FROM commercial_bank_currency WHERE currency_id LIKE 'PAY_TEACH_A_%'"); } private RequestPostProcessor payer() { diff --git a/src/test/java/com/yau/digitalrmb/training/attempt/application/ExperimentAttemptServiceTest.java b/src/test/java/com/yau/digitalrmb/training/attempt/application/ExperimentAttemptServiceTest.java index a278654..6917580 100644 --- a/src/test/java/com/yau/digitalrmb/training/attempt/application/ExperimentAttemptServiceTest.java +++ b/src/test/java/com/yau/digitalrmb/training/attempt/application/ExperimentAttemptServiceTest.java @@ -12,6 +12,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import java.util.concurrent.atomic.AtomicInteger; @@ -125,6 +126,27 @@ class ExperimentAttemptServiceTest { assertEquals("business-101", service.detail(attempt.getAttemptId(), subject).getBusinessId()); } + @Test + @Transactional(propagation = Propagation.NOT_SUPPORTED) + void persistsAFailedActionAfterTheBusinessTransactionRollsBack() { + ExperimentSubject failureSubject = new ExperimentSubject( + "workflow-failure-user", 101L, 202L, "ORG-W"); + ExperimentAttemptView attempt = service.create(ExperimentModule.PAYMENT, failureSubject); + + BusinessException error = assertThrows(BusinessException.class, () -> service.execute( + attempt.getAttemptId(), failureSubject, "02", "generate-digest", "failure-fingerprint", + TestOutput.class, + () -> { throw new BusinessException(ErrorCode.VALIDATION_ERROR, "摘要输入不完整"); }, + "02", "wallet-sign")); + + assertEquals(ErrorCode.VALIDATION_ERROR, error.getErrorCode()); + ExperimentActionView failed = service.detail(attempt.getAttemptId(), failureSubject).getActions().stream() + .filter(action -> "02".equals(action.getStepCode()) + && "generate-digest".equals(action.getActionCode())) + .findFirst().orElseThrow(AssertionError::new); + assertEquals(ActionStatus.FAILED, failed.getActionStatus()); + } + public static final class TestOutput { private String value;