fix: synchronize training module state

chore/issuance-comments-zh
chenyuan 3 weeks ago
parent c106ee56f0
commit b7000539ec

@ -106,7 +106,9 @@ public class ExchangeTrainingActionService {
throw new TrainingInputException(exception.getMessage() + ",本次实训第" + errors + "次错误"); throw new TrainingInputException(exception.getMessage() + ",本次实训第" + errors + "次错误");
} }
}, next[0], next[1]); }, 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; return result;
} }
@ -322,4 +324,10 @@ public class ExchangeTrainingActionService {
try { return Integer.parseInt(stepCode); } try { return Integer.parseInt(stepCode); }
catch (NumberFormatException ignored) { return 0; } 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));
}
} }

@ -129,7 +129,9 @@ public class IssuanceTrainingActionService {
}; };
ExperimentActionView<?> result = attempts.execute(attemptId, subject, stepCode, actionCode, ExperimentActionView<?> result = attempts.execute(attemptId, subject, stepCode, actionCode,
fingerprint(key, input, requestId, subject), Object.class, work, next[0], next[1]); 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; return result;
} }
@ -209,7 +211,7 @@ public class IssuanceTrainingActionService {
case "07:sign-bank-receipt": case "07:sign-bank-receipt":
return completed(ownershipService.signReceipt(requestId, actor, keySubject)); return completed(ownershipService.signReceipt(requestId, actor, keySubject));
case "07:load-ownership-result": case "07:load-ownership-result":
return completed(ownershipService.findProduction(requestId) return ActionOutcome.completedAttempt(ownershipService.findProduction(requestId)
.orElseThrow(() -> validation("数字货币确权结果不存在"))); .orElseThrow(() -> validation("数字货币确权结果不存在")));
default: default:
throw validation("不支持的发行实验动作:" + key); throw validation("不支持的发行实验动作:" + key);
@ -312,4 +314,10 @@ public class IssuanceTrainingActionService {
try { return Integer.parseInt(stepCode); } try { return Integer.parseInt(stepCode); }
catch (NumberFormatException ignored) { return 0; } 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));
}
} }

@ -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.PaymentCoin;
import com.yau.digitalrmb.payment.domain.model.PaymentNotification; import com.yau.digitalrmb.payment.domain.model.PaymentNotification;
import com.yau.digitalrmb.payment.domain.model.PaymentOrder; import com.yau.digitalrmb.payment.domain.model.PaymentOrder;
import com.yau.digitalrmb.payment.domain.model.TeachingPaymentWalletIdentity;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.Instant; import java.time.Instant;
import java.util.Collections; import java.util.Collections;
@ -39,8 +40,11 @@ public final class PaymentOrderView {
private final List<PaymentNotification> notifications; private final List<PaymentNotification> notifications;
private PaymentOrderView(PaymentOrder value, List<PaymentCoin> coins, List<PaymentNotification> notifications) { private PaymentOrderView(PaymentOrder value, List<PaymentCoin> coins, List<PaymentNotification> notifications) {
id = value.getId().toString(); paymentNo = value.getPaymentNo(); payerWalletId = value.getPayerWalletId(); id = value.getId().toString(); paymentNo = value.getPaymentNo();
payerBankCode = value.getPayerBankCode(); payeeWalletId = value.getPayeeWalletId(); payeeBankCode = value.getPayeeBankCode(); 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(); amount = value.getAmount(); note = value.getNote(); requestTimestamp = value.getRequestTimestamp();
paymentOriginalText = value.getPaymentOriginalText(); paymentDigest = value.getPaymentDigest(); payerSignature = value.getPayerSignature(); paymentOriginalText = value.getPaymentOriginalText(); paymentDigest = value.getPaymentDigest(); payerSignature = value.getPayerSignature();
complianceReport = value.getComplianceReport(); complianceDigest = value.getComplianceDigest(); payerBankSignature = value.getPayerBankSignature(); complianceReport = value.getComplianceReport(); complianceDigest = value.getComplianceDigest(); payerBankSignature = value.getPayerBankSignature();

@ -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.PaymentParticipant;
import com.yau.digitalrmb.payment.domain.model.PaymentStatus; import com.yau.digitalrmb.payment.domain.model.PaymentStatus;
import com.yau.digitalrmb.payment.domain.model.PayerBankProcessingResult; 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.PaymentOrderRepository;
import com.yau.digitalrmb.payment.domain.repository.PaymentResourceRepository; import com.yau.digitalrmb.payment.domain.repository.PaymentResourceRepository;
import com.yau.digitalrmb.shared.api.ErrorCode; import com.yau.digitalrmb.shared.api.ErrorCode;
@ -58,8 +59,8 @@ public class PaymentApplicationService {
return resourceRepository.loadPayer(actor); return resourceRepository.loadPayer(actor);
} }
public PaymentParticipant payee(String payeeWalletId) { public PaymentParticipant payee(String payeeWalletId, PaymentActor actor) {
return resourceRepository.loadPayee(payeeWalletId); return resourceRepository.loadPayee(payeeWalletId, actor);
} }
@Transactional @Transactional
@ -130,8 +131,10 @@ public class PaymentApplicationService {
throw validation("请先完成合规检查和资源锁定"); throw validation("请先完成合规检查和资源锁定");
} }
List<PaymentCoin> coins = resourceRepository.findCoins(order); List<PaymentCoin> coins = resourceRepository.findCoins(order);
String report = "COMPLIANCE|" + order.getPaymentNo() + "|" + order.getPayerWalletId() + "|" + String report = "COMPLIANCE|" + order.getPaymentNo() + "|" +
order.getPayeeWalletId() + "|" + order.getAmount().toPlainString() + "|" + TeachingPaymentWalletIdentity.displayWalletId(order.getPayerWalletId()) + "|" +
TeachingPaymentWalletIdentity.displayWalletId(order.getPayeeWalletId()) + "|" +
order.getAmount().toPlainString() + "|" +
order.getPaymentDigest() + "|SIGNATURE_VALID|CONTRACT_VALID|" + coins.size() + "|" + order.getPaymentDigest() + "|SIGNATURE_VALID|CONTRACT_VALID|" + coins.size() + "|" +
order.getPayerBankCode(); order.getPayerBankCode();
order.recordComplianceReport(report); order.recordComplianceReport(report);
@ -221,7 +224,9 @@ public class PaymentApplicationService {
} }
String clearingNo = "ACS_" + order.getPaymentNo(); String clearingNo = "ACS_" + order.getPaymentNo();
String original = "SETTLEMENT|" + order.getPaymentNo() + "|" + order.getTransactionId() + "|" + 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.getAmount().toPlainString() + "|" + order.getPayerBankCode() + "|" +
order.getPayeeBankCode() + "|" + clearingNo + "|" + clock.instant().toString(); order.getPayeeBankCode() + "|" + clearingNo + "|" + clock.instant().toString();
order.prepareSettlement(clearingNo, original); order.prepareSettlement(clearingNo, original);

@ -98,7 +98,9 @@ public class PaymentTrainingActionService {
throw new TrainingInputException(exception.getMessage() + ",本次实训第" + errors + "次错误"); throw new TrainingInputException(exception.getMessage() + ",本次实训第" + errors + "次错误");
} }
}, next[0], next[1]); }, 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; return result;
} }
@ -112,7 +114,7 @@ public class PaymentTrainingActionService {
String payeeWalletId = request.getPayeeWalletId(); String payeeWalletId = request.getPayeeWalletId();
return completed(payeeWalletId == null || payeeWalletId.trim().isEmpty() return completed(payeeWalletId == null || payeeWalletId.trim().isEmpty()
? payments.context(null, actor).getPayee() ? payments.context(null, actor).getPayee()
: payments.payee(payeeWalletId.trim())); : payments.payee(payeeWalletId.trim(), actor));
} }
case "02:generate-timestamp": case "02:generate-timestamp":
return completed(value("timestamp", TIMESTAMP.format(Instant.now()))); return completed(value("timestamp", TIMESTAMP.format(Instant.now())));
@ -304,4 +306,10 @@ public class PaymentTrainingActionService {
try { return Integer.parseInt(stepCode); } try { return Integer.parseInt(stepCode); }
catch (NumberFormatException ignored) { return 0; } 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));
}
} }

@ -61,8 +61,8 @@ public final class PaymentOrder {
public static PaymentOrder create(PaymentOrderId id, String paymentNo, PaymentParticipant payer, public static PaymentOrder create(PaymentOrderId id, String paymentNo, PaymentParticipant payer,
PaymentParticipant payee, BigDecimal amount, String note, String requestTimestamp, PaymentParticipant payee, BigDecimal amount, String note, String requestTimestamp,
String paymentOriginalText, String paymentDigest, Instant now) { String paymentOriginalText, String paymentDigest, Instant now) {
return new PaymentOrder(id, paymentNo, payer.getUserId(), payer.getWalletId(), payer.getBankCode(), return new PaymentOrder(id, paymentNo, payer.getUserId(), payer.getStorageWalletId(), payer.getBankCode(),
payer.getOrganizationId(), payee.getUserId(), payee.getWalletId(), payee.getBankCode(), payer.getOrganizationId(), payee.getUserId(), payee.getStorageWalletId(), payee.getBankCode(),
payee.getOrganizationId(), amount, note, requestTimestamp, paymentOriginalText, paymentDigest, payee.getOrganizationId(), amount, note, requestTimestamp, paymentOriginalText, paymentDigest,
null, null, null, null, null, null, null, null, null, 0, null, null, null, null, null, null, null, null, null, null, null, 0, null, null,
PaymentStatus.REQUEST_PREPARED, now, null); PaymentStatus.REQUEST_PREPARED, now, null);

@ -1,12 +1,15 @@
package com.yau.digitalrmb.payment.domain.model; package com.yau.digitalrmb.payment.domain.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import java.math.BigDecimal; import java.math.BigDecimal;
public final class PaymentParticipant { public final class PaymentParticipant {
private final String userId; private final String userId;
private final String walletId; private final String walletId;
private final String displayWalletId;
private final String walletType; private final String walletType;
private final String certificateSerial; private final String certificateSerial;
private final String displayCertificateSerial;
private final String publicKey; private final String publicKey;
private final String bankCode; private final String bankCode;
private final String bankName; private final String bankName;
@ -17,16 +20,37 @@ public final class PaymentParticipant {
public PaymentParticipant(String userId, String walletId, String walletType, String certificateSerial, String publicKey, public PaymentParticipant(String userId, String walletId, String walletType, String certificateSerial, String publicKey,
String bankCode, String bankName, String organizationId, BigDecimal balance, String bankCode, String bankName, String organizationId, BigDecimal balance,
BigDecimal frozenAmount) { BigDecimal frozenAmount) {
this.userId = userId; this.walletId = walletId; this.walletType = walletType; this(userId, walletId, walletId, walletType, certificateSerial, certificateSerial, publicKey, bankCode, bankName,
this.certificateSerial = certificateSerial; this.publicKey = publicKey; this.bankCode = bankCode; 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.bankName = bankName; this.organizationId = organizationId; this.balance = balance;
this.frozenAmount = frozenAmount; this.frozenAmount = frozenAmount;
} }
public String getUserId() { return userId; } 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 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 getPublicKey() { return publicKey; }
public String getBankCode() { return bankCode; } public String getBankCode() { return bankCode; }
public String getBankName() { return bankName; } public String getBankName() { return bankName; }

@ -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<String> walletIds() {
List<String> values = new ArrayList<String>();
values.add(payerWalletId());
values.add(payeeWalletId());
return Collections.unmodifiableList(values);
}
public List<String> payerCurrencyIds() {
List<String> values = new ArrayList<String>();
for (int index = 1; index <= 10; index++) values.add(payerCurrencyId(index));
return Collections.unmodifiableList(values);
}
}
}

@ -13,7 +13,7 @@ import java.util.List;
public interface PaymentResourceRepository { public interface PaymentResourceRepository {
PaymentParticipant loadPayer(PaymentActor actor); PaymentParticipant loadPayer(PaymentActor actor);
PaymentParticipant loadPayee(String payeeWalletId); PaymentParticipant loadPayee(String payeeWalletId, PaymentActor actor);
PaymentContext loadContext(PaymentActor actor, String payeeWalletId); PaymentContext loadContext(PaymentActor actor, String payeeWalletId);
String signWithWallet(String walletId, String digest); String signWithWallet(String walletId, String digest);
boolean verifyWalletSignature(String walletId, String digest, String signature); boolean verifyWalletSignature(String walletId, String digest, String signature);

@ -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.PaymentOrder;
import com.yau.digitalrmb.payment.domain.model.PaymentParticipant; import com.yau.digitalrmb.payment.domain.model.PaymentParticipant;
import com.yau.digitalrmb.payment.domain.model.PayerBankProcessingResult; 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.payment.domain.repository.PaymentResourceRepository;
import com.yau.digitalrmb.shared.api.ErrorCode; import com.yau.digitalrmb.shared.api.ErrorCode;
import com.yau.digitalrmb.shared.exception.BusinessException; 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 // The teaching wallet is a built-in case wallet. The payment order still
// belongs to the current learner so the existing action-chain ownership // belongs to the current learner so the existing action-chain ownership
// and notification flow remains intact. // and notification flow remains intact.
return loadParticipant(reference, actor.getUserId()); return loadParticipant(reference, actor.getUserId(), TeachingPaymentPayer.WALLET_ID,
"00A1B2C3D4E5F678");
} }
@Override @Override
public PaymentParticipant loadPayee(String payeeWalletId) { public PaymentParticipant loadPayee(String payeeWalletId, PaymentActor actor) {
if (teachingPayeeWhitelist.isTeachingWallet(payeeWalletId)) { 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)); return loadParticipant(walletProjection.ensureForWallet(payeeWalletId));
} }
@ -76,9 +80,13 @@ public class JdbcPaymentResourceRepository implements PaymentResourceRepository
public PaymentContext loadContext(PaymentActor actor, String payeeWalletId) { public PaymentContext loadContext(PaymentActor actor, String payeeWalletId) {
PaymentParticipant payer = loadPayer(actor); PaymentParticipant payer = loadPayer(actor);
PaymentParticipant payee = payeeWalletId == null || payeeWalletId.trim().isEmpty() PaymentParticipant payee = payeeWalletId == null || payeeWalletId.trim().isEmpty()
? loadParticipant(teachingPayeeWhitelist.resolveForDifferentBank(payer.getBankCode())) ? loadParticipant(teachingPayeeWhitelist.resolveForDifferentBank(payer.getBankCode(), subject(actor)),
: loadPayee(payeeWalletId); actor.getUserId(), TeachingPaymentPayeeWhitelist.WALLET_ID,
if (payer.getWalletId().equals(payee.getWalletId())) throw validation("付款钱包和收款钱包不能相同"); TeachingPaymentPayeeWhitelist.CERTIFICATE_SERIAL)
: loadPayee(payeeWalletId, actor);
if (payer.getStorageWalletId().equals(payee.getStorageWalletId())) {
throw validation("付款钱包和收款钱包不能相同");
}
if (payer.getBankCode().equals(payee.getBankCode())) throw validation("本实验仅支持商业银行A向商业银行B的跨行支付"); if (payer.getBankCode().equals(payee.getBankCode())) throw validation("本实验仅支持商业银行A向商业银行B的跨行支付");
return new PaymentContext(payer, payee); return new PaymentContext(payer, payee);
} }
@ -234,7 +242,8 @@ public class JdbcPaymentResourceRepository implements PaymentResourceRepository
String transactionId = "PAY_TXN_" + order.getPaymentNo(); String transactionId = "PAY_TXN_" + order.getPaymentNo();
String clearingNo = "ACS_" + order.getPaymentNo(); String clearingNo = "ACS_" + order.getPaymentNo();
String originalText = "SETTLEMENT|" + order.getPaymentNo() + "|" + transactionId + "|" + 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() + order.getAmount().toPlainString() + "|" + order.getPayerBankCode() + "|" + order.getPayeeBankCode() +
"|" + clearingNo + "|" + Instant.now().toString(); "|" + clearingNo + "|" + Instant.now().toString();
String digest = cryptography.sm3(originalText); String digest = cryptography.sm3(originalText);
@ -329,7 +338,8 @@ public class JdbcPaymentResourceRepository implements PaymentResourceRepository
public List<PaymentNotification> findNotifications(PaymentOrder order, String currentUserId) { public List<PaymentNotification> findNotifications(PaymentOrder order, String currentUserId) {
return jdbc.query("SELECT notification_type,recipient_wallet_id,content,created_at FROM payment_notification " + 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", "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); 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) { 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<PaymentParticipant> values = jdbc.query("SELECT w.user_id,w.wallet_id,w.wallet_type,cert.certificate_serial,cert.public_key," + List<PaymentParticipant> values = jdbc.query("SELECT w.user_id,w.wallet_id,w.wallet_type,cert.certificate_serial,cert.public_key," +
"a.bank_code,a.bank_name,w.balance,COALESCE(w.frozen_amount,0) frozen_amount " + "a.bank_code,a.bank_name,w.balance,COALESCE(w.frozen_amount,0) frozen_amount " +
"FROM digital_wallet w JOIN wallet_certificate cert ON cert.wallet_id=w.wallet_id " + "FROM digital_wallet w JOIN wallet_certificate cert ON cert.wallet_id=w.wallet_id " +
@ -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' " + "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=? " + "WHERE w.wallet_id=? AND a.bank_code=? " +
"AND w.status='ACTIVE' AND w.central_bank_confirmation_signature IS NOT NULL", "AND w.status='ACTIVE' AND w.central_bank_confirmation_signature IS NOT NULL",
(rs, row) -> new PaymentParticipant(participantUserId, rs.getString("wallet_id"), (rs, row) -> new PaymentParticipant(participantUserId, rs.getString("wallet_id"), displayWalletId,
rs.getString("wallet_type"), rs.getString("certificate_serial"), rs.getString("public_key"), 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.getString("bank_code"), rs.getString("bank_name"), reference.getOrganizationId(),
rs.getBigDecimal("balance"), rs.getBigDecimal("frozen_amount")), rs.getBigDecimal("balance"), rs.getBigDecimal("frozen_amount")),
reference.getWalletId(), reference.getBankCode()); reference.getWalletId(), reference.getBankCode());
@ -428,8 +445,10 @@ public class JdbcPaymentResourceRepository implements PaymentResourceRepository
} }
private String complianceReport(PaymentOrder order, List<PaymentCoin> coins) { private String complianceReport(PaymentOrder order, List<PaymentCoin> coins) {
return "COMPLIANCE|" + order.getPaymentNo() + "|" + order.getPayerWalletId() + "|" + return "COMPLIANCE|" + order.getPaymentNo() + "|" +
order.getPayeeWalletId() + "|" + order.getAmount().toPlainString() + "|" + order.getPaymentDigest() + TeachingPaymentWalletIdentity.displayWalletId(order.getPayerWalletId()) + "|" +
TeachingPaymentWalletIdentity.displayWalletId(order.getPayeeWalletId()) + "|" +
order.getAmount().toPlainString() + "|" + order.getPaymentDigest() +
"|SIGNATURE_VALID|CONTRACT_VALID|" + coins.size() + "|" + order.getPayerBankCode(); "|SIGNATURE_VALID|CONTRACT_VALID|" + coins.size() + "|" + order.getPayerBankCode();
} }

@ -1,7 +1,10 @@
package com.yau.digitalrmb.payment.infrastructure.persistence; 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.InstitutionIdentityCryptography;
import com.yau.digitalrmb.institutionidentity.domain.InstitutionSm2KeyPair; 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.api.ErrorCode;
import com.yau.digitalrmb.shared.exception.BusinessException; import com.yau.digitalrmb.shared.exception.BusinessException;
import com.yau.digitalrmb.shared.wallet.WalletPrerequisiteReference; import com.yau.digitalrmb.shared.wallet.WalletPrerequisiteReference;
@ -19,19 +22,12 @@ import java.util.List;
*/ */
@Component @Component
public class TeachingPaymentPayeeWhitelist { public class TeachingPaymentPayeeWhitelist {
private static final String BANK_A = "BKCHCNBJ00001";
private static final String BANK_B = "BKCHCNBJ00002"; private static final String BANK_B = "BKCHCNBJ00002";
private static final String PAYEE_A_USER = "00000000-0000-0000-0000-000000000951"; public static final String WALLET_ID = TeachingPaymentWalletIdentity.PAYEE_DISPLAY_WALLET_ID;
private static final String PAYEE_B_USER = "00000000-0000-0000-0000-000000000953"; public static final String CERTIFICATE_SERIAL = "00C3D4E5F6A7B890";
private static final String PAYEE_A_WALLET = "WALLET_TRAINING_PAYEE_A"; private static final String WALLET_TYPE = "一类钱包";
private static final String PAYEE_B_WALLET = private static final String BANK_NAME = "建设银行(BKCHCNBJ00002)";
"WALLET_B_7D8E9F0A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0C1D2E3F4A5B6"; private static final String ORGANIZATION_ID = "ORG_5F6G7B8E9G0W";
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");
private final JdbcTemplate jdbc; private final JdbcTemplate jdbc;
private final InstitutionIdentityCryptography cryptography; private final InstitutionIdentityCryptography cryptography;
@ -45,19 +41,19 @@ public class TeachingPaymentPayeeWhitelist {
} }
@Transactional @Transactional
public WalletPrerequisiteReference resolveForDifferentBank(String payerBankCode) { public WalletPrerequisiteReference resolveForDifferentBank(String payerBankCode, InstitutionKeySubject subject) {
return resolve(BANK_B.equals(payerBankCode) ? PAYEE_A : PAYEE_B); if (BANK_B.equals(payerBankCode)) throw conflict("教学付款方必须使用商业银行A钱包");
return resolve(definition(subject));
} }
public boolean isTeachingWallet(String walletId) { public boolean isTeachingWallet(String walletId) {
return definitionFor(walletId) != null; return WALLET_ID.equals(walletId);
} }
@Transactional @Transactional
public WalletPrerequisiteReference resolve(String walletId) { public WalletPrerequisiteReference resolve(String walletId, InstitutionKeySubject subject) {
PayeeDefinition payee = definitionFor(walletId); if (!isTeachingWallet(walletId)) throw conflict("教学收款方钱包不存在");
if (payee == null) throw conflict("教学收款方钱包不存在"); return resolve(definition(subject));
return resolve(payee);
} }
private WalletPrerequisiteReference resolve(PayeeDefinition payee) { private WalletPrerequisiteReference resolve(PayeeDefinition payee) {
@ -69,10 +65,11 @@ public class TeachingPaymentPayeeWhitelist {
payee.bankCode, payee.organizationId); payee.bankCode, payee.organizationId);
} }
private PayeeDefinition definitionFor(String walletId) { private PayeeDefinition definition(InstitutionKeySubject subject) {
if (PAYEE_A_WALLET.equals(walletId)) return PAYEE_A; SubjectWallets wallets = TeachingPaymentWalletIdentity.forSubject(
if (PAYEE_B_WALLET.equals(walletId)) return PAYEE_B; subject.getUserId(), subject.getSchoolId(), subject.getClassId());
return null; return new PayeeDefinition(wallets.payeeUserId(), wallets.payeeWalletId(), WALLET_TYPE,
wallets.payeeCertificateSerial(), wallets.payeeAccountId(), BANK_B, BANK_NAME, ORGANIZATION_ID);
} }
private void ensureWallet(PayeeDefinition payee) { private void ensureWallet(PayeeDefinition payee) {
@ -169,24 +166,26 @@ public class TeachingPaymentPayeeWhitelist {
private final String walletId; private final String walletId;
private final String walletType; private final String walletType;
private final String certificateSerial; private final String certificateSerial;
private final String accountId;
private final String bankCode; private final String bankCode;
private final String bankName; private final String bankName;
private final String organizationId; private final String organizationId;
private PayeeDefinition(String userId, String walletId, String walletType, String certificateSerial, private PayeeDefinition(String userId, String walletId, String walletType, String certificateSerial,
String bankCode, String bankName, String accountId, String bankCode, String bankName,
String organizationId) { String organizationId) {
this.userId = userId; this.userId = userId;
this.walletId = walletId; this.walletId = walletId;
this.walletType = walletType; this.walletType = walletType;
this.certificateSerial = certificateSerial; this.certificateSerial = certificateSerial;
this.accountId = accountId;
this.bankCode = bankCode; this.bankCode = bankCode;
this.bankName = bankName; this.bankName = bankName;
this.organizationId = organizationId; this.organizationId = organizationId;
} }
private String accountId() { private String accountId() {
return "ACCOUNT_" + walletId; return accountId;
} }
} }

@ -4,6 +4,8 @@ import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyService;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography; import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography;
import com.yau.digitalrmb.institutionidentity.domain.InstitutionSm2KeyPair; 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.api.ErrorCode;
import com.yau.digitalrmb.shared.exception.BusinessException; import com.yau.digitalrmb.shared.exception.BusinessException;
import com.yau.digitalrmb.shared.wallet.WalletPrivateKeyCipher; 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.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.math.BigInteger; import java.math.BigDecimal;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.UUID; import java.util.UUID;
/** /**
* Module five's built-in payer. The prototype deliberately uses a teaching * 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 * case, not the learner's personal wallet from module three. Every learner gets
* shared by the case, while payment orders remain owned by the current learner * an isolated persistence wallet while the API keeps displaying the prototype's
* through the participant returned by JdbcPaymentResourceRepository. * fixed user-A wallet identifier.
*/ */
@Component @Component
public class TeachingPaymentPayer { public class TeachingPaymentPayer {
public static final String WALLET_ID = public static final String WALLET_ID = TeachingPaymentWalletIdentity.PAYER_DISPLAY_WALLET_ID;
"WALLET_A_3A4B5C6D7E8F9A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0C1D2E3F4";
private static final String CASE_USER_ID = "00000000-0000-0000-0000-000000000950";
private static final String BANK_CODE = "BKCHCNBJ00001"; private static final String BANK_CODE = "BKCHCNBJ00001";
private static final String BANK_NAME = "中国银行(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 CARD_NUMBER = "6216610100001234567";
private static final String WALLET_TYPE = "二类钱包"; private static final String WALLET_TYPE = "一类钱包";
private static final BigInteger COIN_SOURCE_BASE = new BigInteger("950000000000");
private final JdbcTemplate jdbc; private final JdbcTemplate jdbc;
private final InstitutionIdentityCryptography cryptography; private final InstitutionIdentityCryptography cryptography;
@ -56,12 +51,14 @@ public class TeachingPaymentPayer {
@Transactional @Transactional
public WalletPrerequisiteReference ensure(InstitutionKeySubject subject, String operator) { public WalletPrerequisiteReference ensure(InstitutionKeySubject subject, String operator) {
Institution institution = institution(subject); Institution institution = institution(subject);
ensureWallet(); SubjectWallets wallets = TeachingPaymentWalletIdentity.forSubject(
ensureCertificate(); subject.getUserId(), subject.getSchoolId(), subject.getClassId());
ensureAccount(); ensureWallet(wallets);
ensureContract(); ensureCertificate(wallets);
ensureCoins(subject, institution.organizationId, operator); ensureAccount(wallets);
return new WalletPrerequisiteReference(CASE_USER_ID, 0L, 0L, WALLET_ID, ensureContract(wallets);
ensureCoins(subject, wallets, institution.organizationId, operator);
return new WalletPrerequisiteReference(wallets.payerUserId(), 0L, 0L, wallets.payerWalletId(),
BANK_CODE, institution.organizationId); 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 count = jdbc.queryForObject("SELECT COUNT(*) FROM digital_wallet WHERE wallet_id=?",
Integer.class, WALLET_ID); Integer.class, walletId);
if (count != null && count > 0) return; 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 { try {
jdbc.update("INSERT INTO digital_wallet " + jdbc.update("INSERT INTO digital_wallet " +
"(wallet_id,user_id,wallet_type,status,balance,frozen_amount," + "(wallet_id,user_id,wallet_type,status,balance,frozen_amount," +
"central_bank_confirmation_signature,opened_at,created_at,updated_at) " + "central_bank_confirmation_signature,opened_at,created_at,updated_at) " +
"VALUES (?,?,?,'ACTIVE',10000.00,0,'TEACHING_PAYMENT_ACTIVATED',CURRENT_TIMESTAMP," + "VALUES (?,?,?,'ACTIVE',10000.00,0,'TEACHING_PAYMENT_ACTIVATED',CURRENT_TIMESTAMP," +
"CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", "CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)",
WALLET_ID, CASE_USER_ID, WALLET_TYPE); walletId, wallets.payerUserId(), WALLET_TYPE);
} catch (DuplicateKeyException exception) { } catch (DuplicateKeyException exception) {
if (jdbc.queryForObject("SELECT COUNT(*) FROM digital_wallet WHERE wallet_id=?", 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 count = jdbc.queryForObject("SELECT COUNT(*) FROM wallet_certificate WHERE wallet_id=?",
Integer.class, WALLET_ID); Integer.class, walletId);
if (count != null && count > 0) return; if (count != null && count > 0) return;
InstitutionSm2KeyPair pair = cryptography.generateSm2KeyPair(); InstitutionSm2KeyPair pair = cryptography.generateSm2KeyPair();
try { try {
@ -111,40 +115,44 @@ public class TeachingPaymentPayer {
"(certificate_serial,wallet_id,public_key,encrypted_private_key,filing_status," + "(certificate_serial,wallet_id,public_key,encrypted_private_key,filing_status," +
"central_bank_root_signature,status,issued_at) VALUES (?,?,?,?," + "central_bank_root_signature,status,issued_at) VALUES (?,?,?,?," +
"'REGISTERED','TEACHING_PAYMENT_ROOT','VALID',CURRENT_TIMESTAMP)", "'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) { } catch (DuplicateKeyException exception) {
if (jdbc.queryForObject("SELECT COUNT(*) FROM wallet_certificate WHERE wallet_id=?", 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 count = jdbc.queryForObject("SELECT COUNT(*) FROM simulated_bank_account WHERE account_id=?",
Integer.class, ACCOUNT_ID); Integer.class, accountId);
if (count == null || count == 0) { if (count == null || count == 0) {
try { try {
jdbc.update("INSERT INTO simulated_bank_account " + jdbc.update("INSERT INTO simulated_bank_account " +
"(account_id,user_id,bank_code,bank_name,card_number,card_last4,balance," + "(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'," + "frozen_amount,status,created_at,updated_at) VALUES (?,?,?,?,?,?,50000.00,0,'ACTIVE'," +
"CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", "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) { } catch (DuplicateKeyException exception) {
if (jdbc.queryForObject("SELECT COUNT(*) FROM simulated_bank_account WHERE account_id=?", 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 " + Integer binding = jdbc.queryForObject("SELECT COUNT(*) FROM wallet_bank_binding " +
"WHERE wallet_id=? AND bank_account_id=? AND status='BOUND'", "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) { if (binding == null || binding == 0) {
jdbc.update("INSERT INTO wallet_bank_binding (wallet_id,bank_account_id,status,bound_at) " + 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 count = jdbc.queryForObject("SELECT COUNT(*) FROM wallet_contract WHERE wallet_id=?",
Integer.class, WALLET_ID); Integer.class, walletId);
if (count != null && count > 0) return; if (count != null && count > 0) return;
String original = "CONTRACT|" + WALLET_ID + "|" + WALLET_TYPE + "|50000.00|100000.00|500000.00|500000.00|长期有效|TEACHING"; String original = "CONTRACT|" + WALLET_ID + "|" + WALLET_TYPE + "|50000.00|100000.00|500000.00|500000.00|长期有效|TEACHING";
jdbc.update("INSERT INTO wallet_contract " + jdbc.update("INSERT INTO wallet_contract " +
@ -152,21 +160,25 @@ public class TeachingPaymentPayer {
"annual_payment_limit,balance_limit,valid_until,original_text,digest,status," + "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) " + "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)", "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)); "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 " + Integer count = jdbc.queryForObject("SELECT COUNT(*) FROM central_bank_currency_ownership " +
"WHERE owner_type='WALLET' AND owner_id=? AND status IN ('AVAILABLE','EXCHANGE_LOCKED')", "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; if (count != null && count > 0) return;
for (int index = 1; index <= 10; index++) { for (int index = 1; index <= 10; index++) {
String currencyId = "PAYMENT_TEACHING_A_" + String.format("%02d", index); String currencyId = wallets.payerCurrencyId(index);
String original = "TEACHING_ISSUE|" + currencyId + "|" + organizationId + "|1000.00"; BigDecimal denomination = teachingDenomination(index);
String original = "TEACHING_ISSUE|" + currencyId + "|" + organizationId + "|" +
denomination.toPlainString();
String requestId = UUID.nameUUIDFromBytes((currencyId + "|" + organizationId) String requestId = UUID.nameUUIDFromBytes((currencyId + "|" + organizationId)
.getBytes(StandardCharsets.UTF_8)).toString(); .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 exists = jdbc.queryForObject("SELECT COUNT(*) FROM commercial_bank_currency WHERE currency_id=?",
Integer.class, currencyId); Integer.class, currencyId);
if (exists == null || exists == 0) { if (exists == null || exists == 0) {
@ -175,8 +187,8 @@ public class TeachingPaymentPayer {
"organization_id,denomination,currency,complete_currency,issuance_ownership_original_text," + "organization_id,denomination,currency,complete_currency,issuance_ownership_original_text," +
"issuance_ownership_signature,status,received_at,received_by_user_id,received_by) " + "issuance_ownership_signature,status,received_at,received_by_user_id,received_by) " +
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP,?,?)", "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP,?,?)",
currencyId, sourceId, COIN_SOURCE_BASE.longValue(), requestId, BANK_CODE, organizationId, currencyId, sourceId, wallets.payerSourceBatchId(), requestId, BANK_CODE, organizationId,
"1000.00", "DC", "TEACHING_CURRENCY|" + currencyId, original, denomination, "DC", "TEACHING_CURRENCY|" + currencyId, original,
keyService.signCentralBank(subject, original), "可用", subject.getUserId(), operator); keyService.signCentralBank(subject, original), "可用", subject.getUserId(), operator);
} }
Integer owned = jdbc.queryForObject("SELECT COUNT(*) FROM central_bank_currency_ownership WHERE currency_id=?", 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 " + 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) " + "(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)", "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 static final class Institution {
private final String bankCode; private final String bankCode;
private final String organizationId; private final String organizationId;

@ -1,6 +1,8 @@
package com.yau.digitalrmb.shared.wallet; package com.yau.digitalrmb.shared.wallet;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; 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.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -31,11 +33,19 @@ public class WalletRuntimeResetService {
String.class, userId, userId); String.class, userId, userId);
List<String> ownWalletIds = jdbc.queryForList( List<String> ownWalletIds = jdbc.queryForList(
"SELECT wallet_id FROM digital_wallet WHERE user_id=?", String.class, userId); "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); int restoredCoins = restoreCoinsToBankVault(exchangeIds, paymentIds, ownWalletIds);
reverseCounterpartyBalances(paymentIds, userId); reverseCounterpartyBalances(paymentIds, userId);
deleteExchangeRows(exchangeIds, userId); deleteExchangeRows(exchangeIds, userId);
deletePaymentRows(paymentIds); deletePaymentRows(paymentIds);
deleteTeachingCoins(teachingWallets.payerCurrencyIds());
int deletedWallets = deleteSharedRuntime(userId, ownWalletIds); int deletedWallets = deleteSharedRuntime(userId, ownWalletIds);
int deletedAttempts = deleteAttempts(userId); int deletedAttempts = deleteAttempts(userId);
deleteTaskProgress(userId); deleteTaskProgress(userId);
@ -134,11 +144,20 @@ public class WalletRuntimeResetService {
deleteIn("payment_order", "id", paymentIds); deleteIn("payment_order", "id", paymentIds);
} }
private void deleteTeachingCoins(List<String> currencyIds) {
deleteIn("central_bank_currency_ownership", "currency_id", currencyIds);
deleteIn("commercial_bank_currency", "currency_id", currencyIds);
}
private int deleteSharedRuntime(String userId, List<String> ownWalletIds) { private int deleteSharedRuntime(String userId, List<String> ownWalletIds) {
List<String> accountIds = ownWalletIds.isEmpty() ? new ArrayList<String>() : 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_bank_binding", "wallet_id", ownWalletIds);
deleteIn("wallet_contract", "wallet_id", ownWalletIds); deleteIn("wallet_contract", "wallet_id", ownWalletIds);
deleteIn("wallet_certificate", "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); jdbc.update("DELETE FROM simulated_bank_account WHERE user_id=?", userId);
return ownWalletIds.size(); return ownWalletIds.size();
} }

@ -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; }
}

@ -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);
}
}
}

@ -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.domain.ExperimentSubject;
import com.yau.digitalrmb.training.attempt.interfaces.dto.ExperimentActionView; import com.yau.digitalrmb.training.attempt.interfaces.dto.ExperimentActionView;
import com.yau.digitalrmb.training.attempt.interfaces.dto.ExperimentAttemptView; import com.yau.digitalrmb.training.attempt.interfaces.dto.ExperimentAttemptView;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -26,12 +27,15 @@ public class ExperimentAttemptService {
private final ExperimentAttemptRepository repository; private final ExperimentAttemptRepository repository;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final List<AttemptCancellationHandler> cancellationHandlers; private final List<AttemptCancellationHandler> cancellationHandlers;
private final ApplicationEventPublisher eventPublisher;
public ExperimentAttemptService(ExperimentAttemptRepository repository, ObjectMapper objectMapper, public ExperimentAttemptService(ExperimentAttemptRepository repository, ObjectMapper objectMapper,
List<AttemptCancellationHandler> cancellationHandlers) { List<AttemptCancellationHandler> cancellationHandlers,
ApplicationEventPublisher eventPublisher) {
this.repository = repository; this.repository = repository;
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
this.cancellationHandlers = cancellationHandlers; this.cancellationHandlers = cancellationHandlers;
this.eventPublisher = eventPublisher;
} }
@Transactional @Transactional
@ -102,7 +106,14 @@ public class ExperimentAttemptService {
throw validation("该动作正在处理中,请勿重复提交"); throw validation("该动作正在处理中,请勿重复提交");
} }
ActionOutcome<T> outcome = work.run(); ActionOutcome<T> outcome;
try {
outcome = work.run();
} catch (RuntimeException exception) {
eventPublisher.publishEvent(ExperimentActionFailedEvent.from(
attemptId, stepCode, actionCode, requestFingerprint, exception));
throw exception;
}
if (outcome == null) { if (outcome == null) {
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "动作未返回执行结果"); throw new BusinessException(ErrorCode.INTERNAL_ERROR, "动作未返回执行结果");
} }

@ -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<Object> action(UUID attemptId, String step, String action) {
return new ExperimentActionView<Object>(attemptId, ExperimentModule.EXCHANGE, step, action,
ActionStatus.COMPLETED, AttemptStatus.IN_PROGRESS, step, action,
Instant.now(), null);
}
}

@ -26,12 +26,16 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock; 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.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
class IssuanceTrainingActionServiceTest { class IssuanceTrainingActionServiceTest {
private ExperimentAttemptService attempts; private ExperimentAttemptService attempts;
private CommercialBankIssuanceApplicationService commercial; private CommercialBankIssuanceApplicationService commercial;
private DigitalCurrencyOwnershipConfirmationService ownership;
private IssuanceScoreService scoreService;
private IssuanceTrainingActionService service; private IssuanceTrainingActionService service;
private final UUID attemptId = UUID.fromString("00000000-0000-0000-0000-000000000201"); private final UUID attemptId = UUID.fromString("00000000-0000-0000-0000-000000000201");
private final ExperimentSubject subject = new ExperimentSubject("user-1", 10L, 20L, ""); private final ExperimentSubject subject = new ExperimentSubject("user-1", 10L, 20L, "");
@ -42,10 +46,12 @@ class IssuanceTrainingActionServiceTest {
void setUp() { void setUp() {
attempts = mock(ExperimentAttemptService.class); attempts = mock(ExperimentAttemptService.class);
commercial = mock(CommercialBankIssuanceApplicationService.class); commercial = mock(CommercialBankIssuanceApplicationService.class);
ownership = mock(DigitalCurrencyOwnershipConfirmationService.class);
scoreService = mock(IssuanceScoreService.class);
service = new IssuanceTrainingActionService(attempts, new ObjectMapper(), commercial, service = new IssuanceTrainingActionService(attempts, new ObjectMapper(), commercial,
mock(CentralBankIssuanceQueryService.class), mock(CentralBankIssuanceBusinessReviewService.class), mock(CentralBankIssuanceQueryService.class), mock(CentralBankIssuanceBusinessReviewService.class),
mock(ReserveDeductionNotificationService.class), mock(ReserveDeductionExecutionService.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( when(attempts.detail(attemptId, subject)).thenReturn(attemptView(
"00000000-0000-0000-0000-000000000298")); "00000000-0000-0000-0000-000000000298"));
when(attempts.execute(any(UUID.class), any(ExperimentSubject.class), anyString(), anyString(), anyString(), 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"); 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<ActionWork<Object>> 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() { private IssuanceActionRequest validCreateRequest() {
IssuanceActionRequest request = new IssuanceActionRequest(); IssuanceActionRequest request = new IssuanceActionRequest();
request.setBankCode("BKCHCNBJ00001"); request.setBankCode("BKCHCNBJ00001");

@ -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"))) .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)) .contentType(MediaType.APPLICATION_JSON).content(requestBody))
.andExpect(jsonPath("$.code").value(400)) .andExpect(jsonPath("$.code").value(400))
.andExpect(jsonPath("$.message").value("面额必须包含全部 11 种标准面额")); .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.startsWith(
"面额必须包含全部 11 种标准面额")));
} }
@Test @Test

@ -43,7 +43,7 @@ class PaymentScoreServiceTest {
service.recordError("user-1"); service.recordError("user-1");
assertEquals("3", detail.getCompletionStatus()); assertEquals("3", detail.getCompletionStatus());
assertEquals(48.5, detail.getScoreProject()); assertEquals(49.25, detail.getScoreProject());
verify(mapper).updateById(detail); verify(mapper).updateById(detail);
} }
} }

@ -20,6 +20,9 @@ import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatCode;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; 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; import static org.mockito.Mockito.when;
class PaymentTrainingActionServiceTest { class PaymentTrainingActionServiceTest {
@ -40,6 +43,30 @@ class PaymentTrainingActionServiceTest {
new PaymentActionRequest(), subject, actor)).doesNotThrowAnyException(); 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) { private ExperimentAttemptView completedThroughCompliance(UUID attemptId) {
String[][] keys = { String[][] keys = {
{"01", "refresh-payer"}, {"01", "refresh-payee"}, {"01", "refresh-payer"}, {"01", "refresh-payee"},
@ -58,6 +85,12 @@ class PaymentTrainingActionServiceTest {
Instant.now(), Instant.now(), null, actions); 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<Object> action(UUID attemptId, String step, String action) { private ExperimentActionView<Object> action(UUID attemptId, String step, String action) {
return new ExperimentActionView<Object>(attemptId, ExperimentModule.PAYMENT, step, action, return new ExperimentActionView<Object>(attemptId, ExperimentModule.PAYMENT, step, action,
ActionStatus.COMPLETED, AttemptStatus.IN_PROGRESS, step, action, ActionStatus.COMPLETED, AttemptStatus.IN_PROGRESS, step, action,

@ -6,6 +6,11 @@ import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyService;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography; import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography;
import com.yau.digitalrmb.institutionidentity.domain.InstitutionSm2KeyPair; 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 com.yau.digitalrmb.testsupport.WalletOpeningTestData;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -34,14 +39,23 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
class PaymentControllerTest { class PaymentControllerTest {
private static final String PAYER_USER = "00000000-0000-0000-0000-000000000901"; 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 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 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 MockMvc mockMvc;
@Autowired private JdbcTemplate jdbc; @Autowired private JdbcTemplate jdbc;
@Autowired private InstitutionIdentityCryptography cryptography; @Autowired private InstitutionIdentityCryptography cryptography;
@Autowired private WalletPrivateKeyCipher walletCipher; @Autowired private WalletPrivateKeyCipher walletCipher;
@Autowired private InstitutionKeyService keyService; @Autowired private InstitutionKeyService keyService;
@Autowired private WalletRuntimeResetService walletRuntimeResetService;
@BeforeEach @BeforeEach
void seedPrerequisites() { void seedPrerequisites() {
@ -76,21 +90,9 @@ class PaymentControllerTest {
WalletOpeningTestData.insertCompletedWallet(jdbc, 991300L, PAYEE_USER, 3001L, 2001L, WalletOpeningTestData.insertCompletedWallet(jdbc, 991300L, PAYEE_USER, 3001L, 2001L,
PAYEE_WALLET, payeeWalletKey.getPrivateKey(), payeeWalletKey.getPublicKey(), PAYEE_WALLET, payeeWalletKey.getPrivateKey(), payeeWalletKey.getPublicKey(),
"BKCHCNBJ00002", "6222020000000001", BigDecimal.ZERO); "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", insertWallet(PAYEE_USER, PAYEE_WALLET, "CERT_991300", "BKCHCNBJ00002",
"ACCOUNT_PAYMENT_B", BigDecimal.ZERO, payeeWalletKey); "ACCOUNT_PAYMENT_B", BigDecimal.ZERO, payeeWalletKey);
insertContract(PAYER_WALLET, "CONTRACT_991200");
insertContract(PAYEE_WALLET, "CONTRACT_991300"); 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 @AfterEach
@ -116,11 +118,11 @@ class PaymentControllerTest {
.andExpect(jsonPath("$.data.payer.organizationId").value("ORG_PAYMENT_A")) .andExpect(jsonPath("$.data.payer.organizationId").value("ORG_PAYMENT_A"))
.andExpect(jsonPath("$.data.payee.walletId").value(PAYEE_WALLET)) .andExpect(jsonPath("$.data.payee.walletId").value(PAYEE_WALLET))
.andExpect(jsonPath("$.data.payee.organizationId").value("ORG_PAYMENT_B")); .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)) 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)) 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(); 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) 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\":\"实验支付\"}")) .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(status().isOk()).andExpect(jsonPath("$.data.status").value("PAYER_BANK_ACCEPTED"))
.andExpect(jsonPath("$.data.coinCount").value(2)); .andExpect(jsonPath("$.data.coinCount").value(2));
assertThat(jdbc.queryForObject("SELECT balance FROM digital_wallet WHERE wallet_id=?", 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=?", 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 " + 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 " + 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())) mockMvc.perform(post("/api/v1/payments/{id}/central-bank-settle", id).with(payer()))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("CENTRAL_SETTLED")) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("CENTRAL_SETTLED"))
.andExpect(jsonPath("$.data.clearingNo").value(org.hamcrest.Matchers.startsWith("ACS_PAY_REQ_"))); .andExpect(jsonPath("$.data.clearingNo").value(org.hamcrest.Matchers.startsWith("ACS_PAY_REQ_")));
@ -156,7 +158,7 @@ class PaymentControllerTest {
.andExpect(jsonPath("$.code").value(400)); .andExpect(jsonPath("$.code").value(400));
mockMvc.perform(post("/api/v1/payments/{id}/payee-bank-credit", id).with(payer())) mockMvc.perform(post("/api/v1/payments/{id}/payee-bank-credit", id).with(payer()))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("PAYEE_CREDITED")) .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)); .andExpect(jsonPath("$.data.payeeBalanceAfter").value(200.00));
mockMvc.perform(post("/api/v1/payments/{id}/payee-bank-credit", id).with(payer())) mockMvc.perform(post("/api/v1/payments/{id}/payee-bank-credit", id).with(payer()))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("PAYEE_CREDITED")); .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].moduleCode").value("WALLET_OPENING"))
.andExpect(jsonPath("$.data[2].actionUnavailable").value(true)); .andExpect(jsonPath("$.data[2].actionUnavailable").value(true));
assertThat(jdbc.queryForObject("SELECT balance 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("0.00"); .isEqualByComparingTo("9800.00");
assertThat(jdbc.queryForObject("SELECT frozen_amount FROM digital_wallet WHERE wallet_id=?", BigDecimal.class, PAYER_WALLET)) assertThat(jdbc.queryForObject("SELECT frozen_amount FROM digital_wallet WHERE wallet_id=?", BigDecimal.class, PAYER_STORAGE_WALLET))
.isEqualByComparingTo("0.00"); .isEqualByComparingTo("0.00");
assertThat(jdbc.queryForObject("SELECT balance FROM digital_wallet WHERE wallet_id=?", BigDecimal.class, PAYEE_WALLET)) assertThat(jdbc.queryForObject("SELECT balance FROM digital_wallet WHERE wallet_id=?", BigDecimal.class, PAYEE_WALLET))
.isEqualByComparingTo("200.00"); .isEqualByComparingTo("200.00");
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM payment_wallet_ledger WHERE payment_id=?", Integer.class, id)).isEqualTo(2); 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); .isEqualTo(PAYEE_WALLET);
assertThat(jdbc.queryForObject("SELECT lock_business_id FROM central_bank_currency_ownership " + 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 " + assertThat(jdbc.queryForObject("SELECT last_transaction_id FROM central_bank_currency_ownership " +
"WHERE currency_id='DC_PAYMENT_1'", String.class)).startsWith("PAY_TXN_"); "WHERE currency_id=?", String.class, PAYER_COIN_1)).startsWith("PAY_TXN_");
assertThat(jdbc.queryForObject("SELECT status FROM commercial_bank_currency WHERE currency_id='DC_PAYMENT_1'", assertThat(jdbc.queryForObject("SELECT status FROM commercial_bank_currency WHERE currency_id=?",
String.class)).isEqualTo("TRANSFERRED"); String.class, PAYER_COIN_1)).isEqualTo("可用");
} }
@Test @Test
@ -209,7 +211,24 @@ class PaymentControllerTest {
.andExpect(jsonPath("$.data.payee.bankCode").value("BKCHCNBJ00002")); .andExpect(jsonPath("$.data.payee.bankCode").value("BKCHCNBJ00002"));
assertThat(jdbc.queryForObject("SELECT status FROM digital_wallet WHERE wallet_id=?", String.class, 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 @Test
@ -238,11 +257,76 @@ class PaymentControllerTest {
"WALLET_B_7D8E9F0A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0C1D2E3F4A5B6")); "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 @Test
void refreshActionsProjectCompletedModuleThreeWalletsWhenSharedRowsAreMissing() throws Exception { void refreshActionsProjectCompletedModuleThreeWalletsWhenSharedRowsAreMissing() throws Exception {
clearSharedWalletProjection(); clearSharedWalletProjection();
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM digital_wallet WHERE wallet_id IN (?,?)", 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())) String created = mockMvc.perform(post("/api/v1/payment/attempts").with(payer()))
.andExpect(status().isOk()) .andExpect(status().isOk())
@ -262,13 +346,13 @@ class PaymentControllerTest {
.andExpect(jsonPath("$.data.output.walletId").value(PAYEE_WALLET)); .andExpect(jsonPath("$.data.output.walletId").value(PAYEE_WALLET));
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM digital_wallet WHERE wallet_id IN (?,?)", 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 (?,?)", 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 (?,?)", 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 (?,?)", 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 @Test
@ -282,7 +366,7 @@ class PaymentControllerTest {
mockMvc.perform(post("/api/v1/payments/{id}/sign", id).with(payer())) mockMvc.perform(post("/api/v1/payments/{id}/sign", id).with(payer()))
.andExpect(status().isOk()); .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())) mockMvc.perform(post("/api/v1/payments/{id}/payer-bank-process", id).with(payer()))
.andExpect(status().isBadRequest()) .andExpect(status().isBadRequest())
@ -290,7 +374,7 @@ class PaymentControllerTest {
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM payment_ownership_transfer WHERE payment_id=?", assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM payment_ownership_transfer WHERE payment_id=?",
Integer.class, id)).isZero(); Integer.class, id)).isZero();
assertThat(jdbc.queryForObject("SELECT frozen_amount FROM digital_wallet WHERE wallet_id=?", 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 @Test
@ -304,7 +388,7 @@ class PaymentControllerTest {
mockMvc.perform(post("/api/v1/payments/{id}/payer-bank-process", id).with(payer())) mockMvc.perform(post("/api/v1/payments/{id}/payer-bank-process", id).with(payer()))
.andExpect(status().isBadRequest()); .andExpect(status().isBadRequest());
assertThat(jdbc.queryForObject("SELECT frozen_amount FROM digital_wallet WHERE wallet_id=?", 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}/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}/payer-bank-process", id).with(payer())).andExpect(status().isOk());
@ -328,26 +412,12 @@ class PaymentControllerTest {
} }
@Test @Test
void transferredCoinsCanBeReservedByThePayeeInALaterPayment() throws Exception { void rejectsReplacingThePrototypePayerWithTheLoggedInUsersWallet() throws Exception {
String firstId = createSignedPayment(PAYER_WALLET, PAYEE_WALLET, payer()); mockMvc.perform(post("/api/v1/payments").with(payee()).contentType(MediaType.APPLICATION_JSON)
mockMvc.perform(post("/api/v1/payments/{id}/payer-bank-process", firstId).with(payer())) .content("{\"payerWalletId\":\"" + PAYEE_WALLET + "\",\"payeeWalletId\":\"" +
.andExpect(status().isOk()); PAYER_WALLET + "\",\"amount\":200.00,\"note\":\"replace prototype payer\"}"))
mockMvc.perform(post("/api/v1/payments/{id}/central-bank-settle", firstId).with(payer())) .andExpect(status().isBadRequest())
.andExpect(status().isOk()); .andExpect(jsonPath("$.code").value(400));
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);
} }
@Test @Test
@ -368,11 +438,11 @@ class PaymentControllerTest {
.andExpect(jsonPath("$.data.status").value("PAYER_SIGNED")) .andExpect(jsonPath("$.data.status").value("PAYER_SIGNED"))
.andExpect(jsonPath("$.data.coinCount").value(0)); .andExpect(jsonPath("$.data.coinCount").value(0));
assertThat(jdbc.queryForObject("SELECT frozen_amount FROM digital_wallet WHERE wallet_id=?", 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 " + 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 " + 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=?", assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM payment_coin_reservation WHERE payment_id=?",
Integer.class, id)).isZero(); Integer.class, id)).isZero();
} }
@ -391,11 +461,11 @@ class PaymentControllerTest {
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("CENTRAL_OWNERSHIP_TRANSFERRED")); .andExpect(jsonPath("$.data.status").value("CENTRAL_OWNERSHIP_TRANSFERRED"));
assertThat(jdbc.queryForObject("SELECT owner_id FROM central_bank_currency_ownership " + 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=?", 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=?", 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())) mockMvc.perform(post("/api/v1/payment/attempts/{id}/cancel", attemptId).with(payer()))
.andExpect(status().isBadRequest()); .andExpect(status().isBadRequest());
@ -444,8 +514,10 @@ class PaymentControllerTest {
private void insertWallet(String userId, String walletId, String certificate, String bankCode, private void insertWallet(String userId, String walletId, String certificate, String bankCode,
String accountId, BigDecimal balance, InstitutionSm2KeyPair key) { 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) " + 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) " + 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())); "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) " + 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) { 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) " + 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)", "VALUES (?,?,?,50000,100000,500000,500000,NULL,'CONTRACT','DIGEST','ACTIVE',0,CURRENT_DATE,0,2026,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)",
contractId, walletId); contractId, walletId, walletType);
} }
private void cleanWalletState() { private void cleanWalletState() {
@ -466,10 +539,26 @@ class PaymentControllerTest {
} }
private void clearSharedWalletProjection() { 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_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_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 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); 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_coin_reservation");
jdbc.update("DELETE FROM payment_order"); 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 '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() { private RequestPostProcessor payer() {

@ -12,6 +12,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@ -125,6 +126,27 @@ class ExperimentAttemptServiceTest {
assertEquals("business-101", service.detail(attempt.getAttemptId(), subject).getBusinessId()); 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 { public static final class TestOutput {
private String value; private String value;

Loading…
Cancel
Save