新增用户支付与实训任务进度
parent
d252bdb043
commit
2da656b773
@ -0,0 +1,18 @@
|
||||
package com.yau.digitalrmb.payment.application.command;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public final class CreatePaymentCommand {
|
||||
private final String payerWalletId;
|
||||
private final String payeeWalletId;
|
||||
private final BigDecimal amount;
|
||||
private final String note;
|
||||
|
||||
public CreatePaymentCommand(String payerWalletId, String payeeWalletId, BigDecimal amount, String note) {
|
||||
this.payerWalletId = payerWalletId; this.payeeWalletId = payeeWalletId; this.amount = amount; this.note = note;
|
||||
}
|
||||
public String getPayerWalletId() { return payerWalletId; }
|
||||
public String getPayeeWalletId() { return payeeWalletId; }
|
||||
public BigDecimal getAmount() { return amount; }
|
||||
public String getNote() { return note; }
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
package com.yau.digitalrmb.payment.application.query;
|
||||
|
||||
import com.yau.digitalrmb.payment.domain.model.PaymentCoin;
|
||||
import com.yau.digitalrmb.payment.domain.model.PaymentNotification;
|
||||
import com.yau.digitalrmb.payment.domain.model.PaymentOrder;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class PaymentOrderView {
|
||||
private final String id;
|
||||
private final String paymentNo;
|
||||
private final String payerWalletId;
|
||||
private final String payerBankCode;
|
||||
private final String payeeWalletId;
|
||||
private final String payeeBankCode;
|
||||
private final BigDecimal amount;
|
||||
private final String note;
|
||||
private final String requestTimestamp;
|
||||
private final String paymentOriginalText;
|
||||
private final String paymentDigest;
|
||||
private final String payerSignature;
|
||||
private final String complianceReport;
|
||||
private final String complianceDigest;
|
||||
private final String payerBankSignature;
|
||||
private final String transactionId;
|
||||
private final String clearingNo;
|
||||
private final String settlementOriginalText;
|
||||
private final String settlementDigest;
|
||||
private final String centralBankSignature;
|
||||
private final int coinCount;
|
||||
private final BigDecimal payerBalanceAfter;
|
||||
private final BigDecimal payeeBalanceAfter;
|
||||
private final String status;
|
||||
private final Instant createdAt;
|
||||
private final Instant completedAt;
|
||||
private final List<PaymentCoin> coins;
|
||||
private final List<PaymentNotification> notifications;
|
||||
|
||||
private PaymentOrderView(PaymentOrder value, List<PaymentCoin> coins, List<PaymentNotification> notifications) {
|
||||
id = value.getId().toString(); paymentNo = value.getPaymentNo(); payerWalletId = value.getPayerWalletId();
|
||||
payerBankCode = value.getPayerBankCode(); payeeWalletId = value.getPayeeWalletId(); payeeBankCode = value.getPayeeBankCode();
|
||||
amount = value.getAmount(); note = value.getNote(); requestTimestamp = value.getRequestTimestamp();
|
||||
paymentOriginalText = value.getPaymentOriginalText(); paymentDigest = value.getPaymentDigest(); payerSignature = value.getPayerSignature();
|
||||
complianceReport = value.getComplianceReport(); complianceDigest = value.getComplianceDigest(); payerBankSignature = value.getPayerBankSignature();
|
||||
transactionId = value.getTransactionId(); clearingNo = value.getClearingNo(); settlementOriginalText = value.getSettlementOriginalText();
|
||||
settlementDigest = value.getSettlementDigest(); centralBankSignature = value.getCentralBankSignature(); coinCount = value.getCoinCount();
|
||||
payerBalanceAfter = value.getPayerBalanceAfter(); payeeBalanceAfter = value.getPayeeBalanceAfter(); status = value.getStatus().name();
|
||||
createdAt = value.getCreatedAt(); completedAt = value.getCompletedAt();
|
||||
this.coins = coins == null ? Collections.<PaymentCoin>emptyList() : coins;
|
||||
this.notifications = notifications == null ? Collections.<PaymentNotification>emptyList() : notifications;
|
||||
}
|
||||
public static PaymentOrderView from(PaymentOrder value, List<PaymentCoin> coins, List<PaymentNotification> notifications) {
|
||||
return new PaymentOrderView(value, coins, notifications);
|
||||
}
|
||||
public String getId() { return id; } public String getPaymentNo() { return paymentNo; }
|
||||
public String getPayerWalletId() { return payerWalletId; } public String getPayerBankCode() { return payerBankCode; }
|
||||
public String getPayeeWalletId() { return payeeWalletId; } public String getPayeeBankCode() { return payeeBankCode; }
|
||||
public BigDecimal getAmount() { return amount; } public String getNote() { return note; } public String getRequestTimestamp() { return requestTimestamp; }
|
||||
public String getPaymentOriginalText() { return paymentOriginalText; } public String getPaymentDigest() { return paymentDigest; }
|
||||
public String getPayerSignature() { return payerSignature; } public String getComplianceReport() { return complianceReport; }
|
||||
public String getComplianceDigest() { return complianceDigest; } public String getPayerBankSignature() { return payerBankSignature; }
|
||||
public String getTransactionId() { return transactionId; } public String getClearingNo() { return clearingNo; }
|
||||
public String getSettlementOriginalText() { return settlementOriginalText; } public String getSettlementDigest() { return settlementDigest; }
|
||||
public String getCentralBankSignature() { return centralBankSignature; } public int getCoinCount() { return coinCount; }
|
||||
public BigDecimal getPayerBalanceAfter() { return payerBalanceAfter; } public BigDecimal getPayeeBalanceAfter() { return payeeBalanceAfter; }
|
||||
public String getStatus() { return status; } public Instant getCreatedAt() { return createdAt; } public Instant getCompletedAt() { return completedAt; }
|
||||
public List<PaymentCoin> getCoins() { return coins; } public List<PaymentNotification> getNotifications() { return notifications; }
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package com.yau.digitalrmb.payment.domain.model;
|
||||
|
||||
public final class CentralSettlementResult {
|
||||
private final String transactionId;
|
||||
private final String clearingNo;
|
||||
private final String settlementOriginalText;
|
||||
private final String settlementDigest;
|
||||
private final String centralBankSignature;
|
||||
|
||||
public CentralSettlementResult(String transactionId, String clearingNo, String settlementOriginalText,
|
||||
String settlementDigest, String centralBankSignature) {
|
||||
this.transactionId = transactionId; this.clearingNo = clearingNo; this.settlementOriginalText = settlementOriginalText;
|
||||
this.settlementDigest = settlementDigest; this.centralBankSignature = centralBankSignature;
|
||||
}
|
||||
public String getTransactionId() { return transactionId; }
|
||||
public String getClearingNo() { return clearingNo; }
|
||||
public String getSettlementOriginalText() { return settlementOriginalText; }
|
||||
public String getSettlementDigest() { return settlementDigest; }
|
||||
public String getCentralBankSignature() { return centralBankSignature; }
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package com.yau.digitalrmb.payment.domain.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class PayerBankProcessingResult {
|
||||
private final String complianceReport;
|
||||
private final String complianceDigest;
|
||||
private final String bankSignature;
|
||||
private final List<PaymentCoin> coins;
|
||||
|
||||
public PayerBankProcessingResult(String complianceReport, String complianceDigest, String bankSignature,
|
||||
List<PaymentCoin> coins) {
|
||||
this.complianceReport = complianceReport; this.complianceDigest = complianceDigest;
|
||||
this.bankSignature = bankSignature; this.coins = coins;
|
||||
}
|
||||
public String getComplianceReport() { return complianceReport; }
|
||||
public String getComplianceDigest() { return complianceDigest; }
|
||||
public String getBankSignature() { return bankSignature; }
|
||||
public List<PaymentCoin> getCoins() { return coins; }
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package com.yau.digitalrmb.payment.domain.model;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class PaymentActor {
|
||||
private final String userId;
|
||||
private final String username;
|
||||
private final long schoolId;
|
||||
private final long classId;
|
||||
|
||||
public PaymentActor(String userId, String username, long schoolId, long classId) {
|
||||
this.userId = required(userId, "user id");
|
||||
this.username = required(username, "username");
|
||||
this.schoolId = schoolId;
|
||||
this.classId = classId;
|
||||
}
|
||||
|
||||
public String getUserId() { return userId; }
|
||||
public String getUsername() { return username; }
|
||||
public long getSchoolId() { return schoolId; }
|
||||
public long getClassId() { return classId; }
|
||||
|
||||
private static String required(String value, String name) {
|
||||
Objects.requireNonNull(value, name + " is required");
|
||||
if (value.trim().isEmpty()) throw new IllegalArgumentException(name + " is required");
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package com.yau.digitalrmb.payment.domain.model;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public final class PaymentCoin {
|
||||
private final String currencyId;
|
||||
private final BigDecimal denomination;
|
||||
private final String status;
|
||||
|
||||
public PaymentCoin(String currencyId, BigDecimal denomination, String status) {
|
||||
this.currencyId = currencyId; this.denomination = denomination; this.status = status;
|
||||
}
|
||||
public String getCurrencyId() { return currencyId; }
|
||||
public BigDecimal getDenomination() { return denomination; }
|
||||
public String getStatus() { return status; }
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package com.yau.digitalrmb.payment.domain.model;
|
||||
|
||||
public final class PaymentContext {
|
||||
private final PaymentParticipant payer;
|
||||
private final PaymentParticipant payee;
|
||||
|
||||
public PaymentContext(PaymentParticipant payer, PaymentParticipant payee) {
|
||||
this.payer = payer; this.payee = payee;
|
||||
}
|
||||
public PaymentParticipant getPayer() { return payer; }
|
||||
public PaymentParticipant getPayee() { return payee; }
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package com.yau.digitalrmb.payment.domain.model;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public final class PaymentCreditResult {
|
||||
private final BigDecimal payerBalanceAfter;
|
||||
private final BigDecimal payeeBalanceAfter;
|
||||
|
||||
public PaymentCreditResult(BigDecimal payerBalanceAfter, BigDecimal payeeBalanceAfter) {
|
||||
this.payerBalanceAfter = payerBalanceAfter; this.payeeBalanceAfter = payeeBalanceAfter;
|
||||
}
|
||||
public BigDecimal getPayerBalanceAfter() { return payerBalanceAfter; }
|
||||
public BigDecimal getPayeeBalanceAfter() { return payeeBalanceAfter; }
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package com.yau.digitalrmb.payment.domain.model;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public final class PaymentNotification {
|
||||
private final String notificationType;
|
||||
private final String walletId;
|
||||
private final String content;
|
||||
private final Instant createdAt;
|
||||
|
||||
public PaymentNotification(String notificationType, String walletId, String content, Instant createdAt) {
|
||||
this.notificationType = notificationType; this.walletId = walletId; this.content = content; this.createdAt = createdAt;
|
||||
}
|
||||
public String getNotificationType() { return notificationType; }
|
||||
public String getWalletId() { return walletId; }
|
||||
public String getContent() { return content; }
|
||||
public Instant getCreatedAt() { return createdAt; }
|
||||
}
|
||||
@ -0,0 +1,141 @@
|
||||
package com.yau.digitalrmb.payment.domain.model;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class PaymentOrder {
|
||||
private final PaymentOrderId id;
|
||||
private final String paymentNo;
|
||||
private final String payerUserId;
|
||||
private final String payerWalletId;
|
||||
private final String payerBankCode;
|
||||
private final String payerOrganizationId;
|
||||
private final String payeeUserId;
|
||||
private final String payeeWalletId;
|
||||
private final String payeeBankCode;
|
||||
private final String payeeOrganizationId;
|
||||
private final BigDecimal amount;
|
||||
private final String note;
|
||||
private final String requestTimestamp;
|
||||
private final String paymentOriginalText;
|
||||
private final String paymentDigest;
|
||||
private String payerSignature;
|
||||
private String complianceReport;
|
||||
private String complianceDigest;
|
||||
private String payerBankSignature;
|
||||
private String transactionId;
|
||||
private String clearingNo;
|
||||
private String settlementOriginalText;
|
||||
private String settlementDigest;
|
||||
private String centralBankSignature;
|
||||
private int coinCount;
|
||||
private BigDecimal payerBalanceAfter;
|
||||
private BigDecimal payeeBalanceAfter;
|
||||
private PaymentStatus status;
|
||||
private final Instant createdAt;
|
||||
private Instant completedAt;
|
||||
|
||||
private PaymentOrder(PaymentOrderId id, String paymentNo, String payerUserId, String payerWalletId,
|
||||
String payerBankCode, String payerOrganizationId, String payeeUserId, String payeeWalletId,
|
||||
String payeeBankCode, String payeeOrganizationId, BigDecimal amount, String note,
|
||||
String requestTimestamp, String paymentOriginalText, String paymentDigest, String payerSignature,
|
||||
String complianceReport, String complianceDigest, String payerBankSignature, String transactionId,
|
||||
String clearingNo, String settlementOriginalText, String settlementDigest, String centralBankSignature,
|
||||
int coinCount, BigDecimal payerBalanceAfter, BigDecimal payeeBalanceAfter, PaymentStatus status,
|
||||
Instant createdAt, Instant completedAt) {
|
||||
this.id = Objects.requireNonNull(id); this.paymentNo = required(paymentNo); this.payerUserId = required(payerUserId);
|
||||
this.payerWalletId = required(payerWalletId); this.payerBankCode = required(payerBankCode);
|
||||
this.payerOrganizationId = required(payerOrganizationId); this.payeeUserId = required(payeeUserId);
|
||||
this.payeeWalletId = required(payeeWalletId); this.payeeBankCode = required(payeeBankCode);
|
||||
this.payeeOrganizationId = required(payeeOrganizationId); this.amount = money(amount); this.note = note == null ? "" : note.trim();
|
||||
this.requestTimestamp = required(requestTimestamp); this.paymentOriginalText = required(paymentOriginalText);
|
||||
this.paymentDigest = required(paymentDigest); this.payerSignature = payerSignature; this.complianceReport = complianceReport;
|
||||
this.complianceDigest = complianceDigest; this.payerBankSignature = payerBankSignature; this.transactionId = transactionId;
|
||||
this.clearingNo = clearingNo; this.settlementOriginalText = settlementOriginalText; this.settlementDigest = settlementDigest;
|
||||
this.centralBankSignature = centralBankSignature; this.coinCount = coinCount; this.payerBalanceAfter = payerBalanceAfter;
|
||||
this.payeeBalanceAfter = payeeBalanceAfter; this.status = Objects.requireNonNull(status);
|
||||
this.createdAt = Objects.requireNonNull(createdAt); this.completedAt = completedAt;
|
||||
}
|
||||
|
||||
public static PaymentOrder create(PaymentOrderId id, String paymentNo, PaymentParticipant payer,
|
||||
PaymentParticipant payee, BigDecimal amount, String note, String requestTimestamp,
|
||||
String paymentOriginalText, String paymentDigest, Instant now) {
|
||||
return new PaymentOrder(id, paymentNo, payer.getUserId(), payer.getWalletId(), payer.getBankCode(),
|
||||
payer.getOrganizationId(), payee.getUserId(), payee.getWalletId(), payee.getBankCode(),
|
||||
payee.getOrganizationId(), amount, note, requestTimestamp, paymentOriginalText, paymentDigest,
|
||||
null, null, null, null, null, null, null, null, null, 0, null, null,
|
||||
PaymentStatus.REQUEST_PREPARED, now, null);
|
||||
}
|
||||
|
||||
public static PaymentOrder rehydrate(PaymentOrderId id, String paymentNo, String payerUserId, String payerWalletId,
|
||||
String payerBankCode, String payerOrganizationId, String payeeUserId,
|
||||
String payeeWalletId, String payeeBankCode, String payeeOrganizationId,
|
||||
BigDecimal amount, String note, String requestTimestamp, String paymentOriginalText,
|
||||
String paymentDigest, String payerSignature, String complianceReport,
|
||||
String complianceDigest, String payerBankSignature, String transactionId, String clearingNo,
|
||||
String settlementOriginalText, String settlementDigest, String centralBankSignature,
|
||||
int coinCount, BigDecimal payerBalanceAfter, BigDecimal payeeBalanceAfter,
|
||||
PaymentStatus status, Instant createdAt, Instant completedAt) {
|
||||
return new PaymentOrder(id, paymentNo, payerUserId, payerWalletId, payerBankCode, payerOrganizationId,
|
||||
payeeUserId, payeeWalletId, payeeBankCode, payeeOrganizationId, amount, note, requestTimestamp,
|
||||
paymentOriginalText, paymentDigest, payerSignature, complianceReport, complianceDigest,
|
||||
payerBankSignature, transactionId, clearingNo, settlementOriginalText, settlementDigest,
|
||||
centralBankSignature, coinCount, payerBalanceAfter, payeeBalanceAfter, status, createdAt, completedAt);
|
||||
}
|
||||
|
||||
public void sign(String signature) { require(PaymentStatus.REQUEST_PREPARED); payerSignature = required(signature); status = PaymentStatus.PAYER_SIGNED; }
|
||||
public void acceptByPayerBank(String report, String digest, String signature, int selectedCoinCount) {
|
||||
require(PaymentStatus.PAYER_SIGNED); if (selectedCoinCount <= 0) throw new IllegalArgumentException("coin count must be positive");
|
||||
complianceReport = required(report); complianceDigest = required(digest); payerBankSignature = required(signature);
|
||||
coinCount = selectedCoinCount; status = PaymentStatus.PAYER_BANK_ACCEPTED;
|
||||
}
|
||||
public void settle(String transactionId, String clearingNo, String originalText, String digest, String signature) {
|
||||
require(PaymentStatus.PAYER_BANK_ACCEPTED); this.transactionId = required(transactionId); this.clearingNo = required(clearingNo);
|
||||
settlementOriginalText = required(originalText); settlementDigest = required(digest); centralBankSignature = required(signature);
|
||||
status = PaymentStatus.CENTRAL_SETTLED;
|
||||
}
|
||||
public void creditPayee(BigDecimal payerAfter, BigDecimal payeeAfter) {
|
||||
require(PaymentStatus.CENTRAL_SETTLED); payerBalanceAfter = moneyOrZero(payerAfter); payeeBalanceAfter = moneyOrZero(payeeAfter);
|
||||
status = PaymentStatus.PAYEE_CREDITED;
|
||||
}
|
||||
public void complete(Instant now) { require(PaymentStatus.PAYEE_CREDITED); completedAt = Objects.requireNonNull(now); status = PaymentStatus.SUCCESS; }
|
||||
public void requireOwnedBy(String userId) { if (!payerUserId.equals(userId)) throw new SecurityException("payment order is not owned by current user"); }
|
||||
public boolean involves(String userId) { return payerUserId.equals(userId) || payeeUserId.equals(userId); }
|
||||
|
||||
private void require(PaymentStatus expected) { if (status != expected) throw new IllegalStateException("expected " + expected + " but was " + status); }
|
||||
private static String required(String value) { Objects.requireNonNull(value, "required value"); if (value.trim().isEmpty()) throw new IllegalArgumentException("required value"); return value.trim(); }
|
||||
private static BigDecimal money(BigDecimal value) { Objects.requireNonNull(value, "amount"); BigDecimal normalized = value.setScale(2); if (normalized.signum() <= 0) throw new IllegalArgumentException("amount must be positive"); return normalized; }
|
||||
private static BigDecimal moneyOrZero(BigDecimal value) { Objects.requireNonNull(value, "balance"); return value.setScale(2); }
|
||||
|
||||
public PaymentOrderId getId() { return id; }
|
||||
public String getPaymentNo() { return paymentNo; }
|
||||
public String getPayerUserId() { return payerUserId; }
|
||||
public String getPayerWalletId() { return payerWalletId; }
|
||||
public String getPayerBankCode() { return payerBankCode; }
|
||||
public String getPayerOrganizationId() { return payerOrganizationId; }
|
||||
public String getPayeeUserId() { return payeeUserId; }
|
||||
public String getPayeeWalletId() { return payeeWalletId; }
|
||||
public String getPayeeBankCode() { return payeeBankCode; }
|
||||
public String getPayeeOrganizationId() { return payeeOrganizationId; }
|
||||
public BigDecimal getAmount() { return amount; }
|
||||
public String getNote() { return note; }
|
||||
public String getRequestTimestamp() { return requestTimestamp; }
|
||||
public String getPaymentOriginalText() { return paymentOriginalText; }
|
||||
public String getPaymentDigest() { return paymentDigest; }
|
||||
public String getPayerSignature() { return payerSignature; }
|
||||
public String getComplianceReport() { return complianceReport; }
|
||||
public String getComplianceDigest() { return complianceDigest; }
|
||||
public String getPayerBankSignature() { return payerBankSignature; }
|
||||
public String getTransactionId() { return transactionId; }
|
||||
public String getClearingNo() { return clearingNo; }
|
||||
public String getSettlementOriginalText() { return settlementOriginalText; }
|
||||
public String getSettlementDigest() { return settlementDigest; }
|
||||
public String getCentralBankSignature() { return centralBankSignature; }
|
||||
public int getCoinCount() { return coinCount; }
|
||||
public BigDecimal getPayerBalanceAfter() { return payerBalanceAfter; }
|
||||
public BigDecimal getPayeeBalanceAfter() { return payeeBalanceAfter; }
|
||||
public PaymentStatus getStatus() { return status; }
|
||||
public Instant getCreatedAt() { return createdAt; }
|
||||
public Instant getCompletedAt() { return completedAt; }
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package com.yau.digitalrmb.payment.domain.model;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class PaymentOrderId {
|
||||
private final UUID value;
|
||||
|
||||
public PaymentOrderId(UUID value) { this.value = Objects.requireNonNull(value, "payment id"); }
|
||||
public UUID value() { return value; }
|
||||
@Override public String toString() { return value.toString(); }
|
||||
@Override public boolean equals(Object other) {
|
||||
return other instanceof PaymentOrderId && value.equals(((PaymentOrderId) other).value);
|
||||
}
|
||||
@Override public int hashCode() { return value.hashCode(); }
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
package com.yau.digitalrmb.payment.domain.model;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public final class PaymentParticipant {
|
||||
private final String userId;
|
||||
private final String walletId;
|
||||
private final String walletType;
|
||||
private final String certificateSerial;
|
||||
private final String publicKey;
|
||||
private final String bankCode;
|
||||
private final String bankName;
|
||||
private final String organizationId;
|
||||
private final BigDecimal balance;
|
||||
private final BigDecimal frozenAmount;
|
||||
|
||||
public PaymentParticipant(String userId, String walletId, String walletType, String certificateSerial, String publicKey,
|
||||
String bankCode, String bankName, String organizationId, BigDecimal balance,
|
||||
BigDecimal frozenAmount) {
|
||||
this.userId = userId; this.walletId = walletId; this.walletType = walletType;
|
||||
this.certificateSerial = certificateSerial; this.publicKey = publicKey; this.bankCode = bankCode;
|
||||
this.bankName = bankName; this.organizationId = organizationId; this.balance = balance;
|
||||
this.frozenAmount = frozenAmount;
|
||||
}
|
||||
|
||||
public String getUserId() { return userId; }
|
||||
public String getWalletId() { return walletId; }
|
||||
public String getWalletType() { return walletType; }
|
||||
public String getCertificateSerial() { return certificateSerial; }
|
||||
public String getPublicKey() { return publicKey; }
|
||||
public String getBankCode() { return bankCode; }
|
||||
public String getBankName() { return bankName; }
|
||||
public String getOrganizationId() { return organizationId; }
|
||||
public BigDecimal getBalance() { return balance; }
|
||||
public BigDecimal getFrozenAmount() { return frozenAmount; }
|
||||
public BigDecimal getAvailableBalance() { return balance.subtract(frozenAmount); }
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
package com.yau.digitalrmb.payment.domain.model;
|
||||
|
||||
public enum PaymentStatus {
|
||||
REQUEST_PREPARED,
|
||||
PAYER_SIGNED,
|
||||
PAYER_BANK_ACCEPTED,
|
||||
CENTRAL_SETTLED,
|
||||
PAYEE_CREDITED,
|
||||
SUCCESS
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package com.yau.digitalrmb.payment.domain.repository;
|
||||
|
||||
import com.yau.digitalrmb.payment.domain.model.PaymentOrder;
|
||||
import com.yau.digitalrmb.payment.domain.model.PaymentOrderId;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface PaymentOrderRepository {
|
||||
void save(PaymentOrder order, String operator);
|
||||
Optional<PaymentOrder> findById(PaymentOrderId id);
|
||||
Optional<PaymentOrder> findByIdForUpdate(PaymentOrderId id);
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package com.yau.digitalrmb.payment.domain.repository;
|
||||
|
||||
import com.yau.digitalrmb.payment.domain.model.PaymentActor;
|
||||
import com.yau.digitalrmb.payment.domain.model.CentralSettlementResult;
|
||||
import com.yau.digitalrmb.payment.domain.model.PaymentCoin;
|
||||
import com.yau.digitalrmb.payment.domain.model.PaymentContext;
|
||||
import com.yau.digitalrmb.payment.domain.model.PaymentCreditResult;
|
||||
import com.yau.digitalrmb.payment.domain.model.PaymentNotification;
|
||||
import com.yau.digitalrmb.payment.domain.model.PaymentOrder;
|
||||
import com.yau.digitalrmb.payment.domain.model.PayerBankProcessingResult;
|
||||
import java.util.List;
|
||||
|
||||
public interface PaymentResourceRepository {
|
||||
PaymentContext loadContext(String payerUserId, String payeeWalletId);
|
||||
String signWithWallet(String walletId, String digest);
|
||||
boolean verifyWalletSignature(String walletId, String digest, String signature);
|
||||
PayerBankProcessingResult processPayerBank(PaymentOrder order, PaymentActor actor);
|
||||
CentralSettlementResult settleAtCentralBank(PaymentOrder order, PaymentActor actor);
|
||||
PaymentCreditResult creditPayee(PaymentOrder order, PaymentActor actor);
|
||||
void createNotifications(PaymentOrder order, PaymentActor actor);
|
||||
List<PaymentCoin> findCoins(PaymentOrder order);
|
||||
List<PaymentNotification> findNotifications(PaymentOrder order, String currentUserId);
|
||||
void appendStepLog(PaymentOrder order, String stepCode, String stepName, String output, PaymentActor actor);
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
package com.yau.digitalrmb.payment.infrastructure.persistence;
|
||||
|
||||
import com.yau.digitalrmb.payment.domain.model.PaymentOrder;
|
||||
import com.yau.digitalrmb.payment.domain.model.PaymentOrderId;
|
||||
import com.yau.digitalrmb.payment.domain.model.PaymentStatus;
|
||||
import com.yau.digitalrmb.payment.domain.repository.PaymentOrderRepository;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public class JdbcPaymentOrderRepository implements PaymentOrderRepository {
|
||||
private final JdbcTemplate jdbc;
|
||||
|
||||
public JdbcPaymentOrderRepository(JdbcTemplate jdbc) { this.jdbc = jdbc; }
|
||||
|
||||
@Override
|
||||
public void save(PaymentOrder order, String operator) {
|
||||
int updated = jdbc.update("UPDATE payment_order SET payer_signature=?,compliance_report=?,compliance_digest=?," +
|
||||
"payer_bank_signature=?,transaction_id=?,clearing_no=?,settlement_original_text=?,settlement_digest=?," +
|
||||
"central_bank_signature=?,coin_count=?,payer_balance_after=?,payee_balance_after=?,status=?," +
|
||||
"updated_at=CURRENT_TIMESTAMP,completed_at=?,updated_by=? WHERE id=?",
|
||||
order.getPayerSignature(), order.getComplianceReport(), order.getComplianceDigest(), order.getPayerBankSignature(),
|
||||
order.getTransactionId(), order.getClearingNo(), order.getSettlementOriginalText(), order.getSettlementDigest(),
|
||||
order.getCentralBankSignature(), order.getCoinCount(), order.getPayerBalanceAfter(), order.getPayeeBalanceAfter(),
|
||||
order.getStatus().name(), timestamp(order.getCompletedAt()), operator, order.getId().toString());
|
||||
if (updated == 0) {
|
||||
jdbc.update("INSERT INTO payment_order (id,payment_no,payer_user_id,payer_wallet_id,payer_bank_code,payer_organization_id," +
|
||||
"payee_user_id,payee_wallet_id,payee_bank_code,payee_organization_id,amount,payment_note,request_timestamp," +
|
||||
"payment_original_text,payment_digest,payer_signature,compliance_report,compliance_digest,payer_bank_signature," +
|
||||
"transaction_id,clearing_no,settlement_original_text,settlement_digest,central_bank_signature,coin_count," +
|
||||
"payer_balance_after,payee_balance_after,status,created_at,updated_at,completed_at,created_by,updated_by) " +
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
order.getId().toString(), order.getPaymentNo(), order.getPayerUserId(), order.getPayerWalletId(),
|
||||
order.getPayerBankCode(), order.getPayerOrganizationId(), order.getPayeeUserId(), order.getPayeeWalletId(),
|
||||
order.getPayeeBankCode(), order.getPayeeOrganizationId(), order.getAmount(), order.getNote(),
|
||||
order.getRequestTimestamp(), order.getPaymentOriginalText(), order.getPaymentDigest(), order.getPayerSignature(),
|
||||
order.getComplianceReport(), order.getComplianceDigest(), order.getPayerBankSignature(), order.getTransactionId(),
|
||||
order.getClearingNo(), order.getSettlementOriginalText(), order.getSettlementDigest(), order.getCentralBankSignature(),
|
||||
order.getCoinCount(), order.getPayerBalanceAfter(), order.getPayeeBalanceAfter(), order.getStatus().name(),
|
||||
timestamp(order.getCreatedAt()), timestamp(order.getCreatedAt()), timestamp(order.getCompletedAt()), operator, operator);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public Optional<PaymentOrder> findById(PaymentOrderId id) { return find(id, false); }
|
||||
@Override public Optional<PaymentOrder> findByIdForUpdate(PaymentOrderId id) { return find(id, true); }
|
||||
|
||||
private Optional<PaymentOrder> find(PaymentOrderId id, boolean forUpdate) {
|
||||
List<PaymentOrder> values = jdbc.query("SELECT * FROM payment_order WHERE id=?" + (forUpdate ? " FOR UPDATE" : ""),
|
||||
(rs, row) -> PaymentOrder.rehydrate(new PaymentOrderId(UUID.fromString(rs.getString("id"))),
|
||||
rs.getString("payment_no"), rs.getString("payer_user_id"), rs.getString("payer_wallet_id"),
|
||||
rs.getString("payer_bank_code"), rs.getString("payer_organization_id"), rs.getString("payee_user_id"),
|
||||
rs.getString("payee_wallet_id"), rs.getString("payee_bank_code"), rs.getString("payee_organization_id"),
|
||||
rs.getBigDecimal("amount"), rs.getString("payment_note"), rs.getString("request_timestamp"),
|
||||
rs.getString("payment_original_text"), rs.getString("payment_digest"), rs.getString("payer_signature"),
|
||||
rs.getString("compliance_report"), rs.getString("compliance_digest"), rs.getString("payer_bank_signature"),
|
||||
rs.getString("transaction_id"), rs.getString("clearing_no"), rs.getString("settlement_original_text"),
|
||||
rs.getString("settlement_digest"), rs.getString("central_bank_signature"), rs.getInt("coin_count"),
|
||||
rs.getBigDecimal("payer_balance_after"), rs.getBigDecimal("payee_balance_after"),
|
||||
PaymentStatus.valueOf(rs.getString("status")), instant(rs.getTimestamp("created_at")),
|
||||
instant(rs.getTimestamp("completed_at"))), id.toString());
|
||||
return values.isEmpty() ? Optional.<PaymentOrder>empty() : Optional.of(values.get(0));
|
||||
}
|
||||
|
||||
private static Timestamp timestamp(Instant value) { return value == null ? null : Timestamp.from(value); }
|
||||
private static Instant instant(Timestamp value) { return value == null ? null : value.toInstant(); }
|
||||
}
|
||||
@ -0,0 +1,315 @@
|
||||
package com.yau.digitalrmb.payment.infrastructure.persistence;
|
||||
|
||||
import com.yau.digitalrmb.exchange.infrastructure.crypto.WalletPrivateKeyCipher;
|
||||
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyService;
|
||||
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
|
||||
import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography;
|
||||
import com.yau.digitalrmb.payment.domain.model.CentralSettlementResult;
|
||||
import com.yau.digitalrmb.payment.domain.model.PaymentActor;
|
||||
import com.yau.digitalrmb.payment.domain.model.PaymentCoin;
|
||||
import com.yau.digitalrmb.payment.domain.model.PaymentContext;
|
||||
import com.yau.digitalrmb.payment.domain.model.PaymentCreditResult;
|
||||
import com.yau.digitalrmb.payment.domain.model.PaymentNotification;
|
||||
import com.yau.digitalrmb.payment.domain.model.PaymentOrder;
|
||||
import com.yau.digitalrmb.payment.domain.model.PaymentParticipant;
|
||||
import com.yau.digitalrmb.payment.domain.model.PayerBankProcessingResult;
|
||||
import com.yau.digitalrmb.payment.domain.repository.PaymentResourceRepository;
|
||||
import com.yau.digitalrmb.shared.api.ErrorCode;
|
||||
import com.yau.digitalrmb.shared.exception.BusinessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public class JdbcPaymentResourceRepository implements PaymentResourceRepository {
|
||||
private final JdbcTemplate jdbc;
|
||||
private final InstitutionIdentityCryptography cryptography;
|
||||
private final WalletPrivateKeyCipher walletCipher;
|
||||
private final InstitutionKeyService keyService;
|
||||
|
||||
public JdbcPaymentResourceRepository(JdbcTemplate jdbc, InstitutionIdentityCryptography cryptography,
|
||||
WalletPrivateKeyCipher walletCipher, InstitutionKeyService keyService) {
|
||||
this.jdbc = jdbc; this.cryptography = cryptography; this.walletCipher = walletCipher; this.keyService = keyService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaymentContext loadContext(String payerUserId, String payeeWalletId) {
|
||||
PaymentParticipant payer = loadParticipant("w.user_id=?", payerUserId);
|
||||
PaymentParticipant payee = loadParticipant("w.wallet_id=?", payeeWalletId);
|
||||
if (payer.getWalletId().equals(payee.getWalletId())) throw validation("付款钱包和收款钱包不能相同");
|
||||
if (payer.getBankCode().equals(payee.getBankCode())) throw validation("本实验仅支持商业银行A向商业银行B的跨行支付");
|
||||
return new PaymentContext(payer, payee);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String signWithWallet(String walletId, String digest) {
|
||||
List<String> values = jdbc.query("SELECT cert.encrypted_private_key FROM wallet_certificate cert " +
|
||||
"JOIN digital_wallet w ON w.wallet_id=cert.wallet_id WHERE cert.wallet_id=? " +
|
||||
"AND cert.status='VALID' AND cert.filing_status='REGISTERED' AND w.status='ACTIVE'",
|
||||
(rs, row) -> rs.getString(1), walletId);
|
||||
if (values.isEmpty()) throw validation("付款钱包证书不存在或状态异常");
|
||||
return cryptography.sign(walletCipher.decrypt(values.get(0)), digest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verifyWalletSignature(String walletId, String digest, String signature) {
|
||||
List<String> values = jdbc.query("SELECT cert.public_key FROM wallet_certificate cert " +
|
||||
"JOIN digital_wallet w ON w.wallet_id=cert.wallet_id WHERE cert.wallet_id=? " +
|
||||
"AND cert.status='VALID' AND cert.filing_status='REGISTERED' AND w.status='ACTIVE'",
|
||||
(rs, row) -> rs.getString(1), walletId);
|
||||
return !values.isEmpty() && cryptography.verify(values.get(0), digest, signature);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayerBankProcessingResult processPayerBank(PaymentOrder order, PaymentActor actor) {
|
||||
WalletSnapshot payer = lockWallet(order.getPayerWalletId());
|
||||
if (!"ACTIVE".equals(payer.status) || payer.balance.subtract(payer.frozenAmount).compareTo(order.getAmount()) < 0) {
|
||||
throw validation("付款钱包可用余额不足或状态异常");
|
||||
}
|
||||
assertPayerContract(order, actor);
|
||||
List<PaymentCoin> coins = selectAndLockCoins(order, actor);
|
||||
int frozen = jdbc.update("UPDATE digital_wallet SET frozen_amount=frozen_amount+?,updated_at=CURRENT_TIMESTAMP " +
|
||||
"WHERE wallet_id=? AND balance-frozen_amount>=?",
|
||||
order.getAmount(), order.getPayerWalletId(), order.getAmount());
|
||||
if (frozen != 1) throw validation("付款钱包可用余额已变化,请重新发起支付");
|
||||
String report = "COMPLIANCE|" + order.getPaymentNo() + "|" + order.getPayerWalletId() + "|" +
|
||||
order.getPayeeWalletId() + "|" + order.getAmount().toPlainString() + "|" + order.getPaymentDigest() +
|
||||
"|SIGNATURE_VALID|CONTRACT_VALID|" + coins.size() + "|" + order.getPayerBankCode();
|
||||
String digest = cryptography.sm3(report);
|
||||
InstitutionKeySubject subject = subject(actor);
|
||||
String signature = keyService.signCommercialBank(subject, digest);
|
||||
if (!keyService.verifyCommercialBank(subject, digest, signature)) throw validation("商业银行A合规报告签名验证失败");
|
||||
return new PayerBankProcessingResult(report, digest, signature, coins);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CentralSettlementResult settleAtCentralBank(PaymentOrder order, PaymentActor actor) {
|
||||
InstitutionKeySubject subject = subject(actor);
|
||||
if (!keyService.verifyCommercialBank(subject, order.getComplianceDigest(), order.getPayerBankSignature())) {
|
||||
throw validation("中央银行验证商业银行A签名失败");
|
||||
}
|
||||
List<PaymentCoin> coins = findCoins(order);
|
||||
if (coins.isEmpty() || coins.size() != order.getCoinCount()) throw validation("付款币串锁定记录不完整");
|
||||
String transactionId = "PAY_TXN_" + order.getPaymentNo();
|
||||
String clearingNo = "ACS_" + order.getPaymentNo();
|
||||
String originalText = "SETTLEMENT|" + order.getPaymentNo() + "|" + transactionId + "|" +
|
||||
order.getPayerWalletId() + "|" + order.getPayeeWalletId() + "|" + coins.size() + "|" +
|
||||
order.getAmount().toPlainString() + "|" + order.getPayerBankCode() + "|" + order.getPayeeBankCode() +
|
||||
"|" + clearingNo + "|" + Instant.now().toString();
|
||||
String digest = cryptography.sm3(originalText);
|
||||
String signature = keyService.signCentralBank(subject, digest);
|
||||
if (!keyService.verifyCentralBank(subject, digest, signature)) throw validation("中央银行结算确认签名验证失败");
|
||||
|
||||
for (PaymentCoin coin : coins) {
|
||||
int ownership = jdbc.update("UPDATE central_bank_currency_ownership SET owner_type='WALLET',owner_id=?," +
|
||||
"status='AVAILABLE',last_transaction_id=NULL,updated_at=CURRENT_TIMESTAMP WHERE currency_id=? " +
|
||||
"AND owner_type='WALLET' AND owner_id=? AND status='AVAILABLE'",
|
||||
order.getPayeeWalletId(), coin.getCurrencyId(), order.getPayerWalletId());
|
||||
if (ownership != 1) throw validation("中央银行登记中心币串权属已变化:" + coin.getCurrencyId());
|
||||
int currency = jdbc.update("UPDATE commercial_bank_currency SET status='TRANSFERRED' " +
|
||||
"WHERE currency_id=? AND status='PAYMENT_LOCKED'", coin.getCurrencyId());
|
||||
if (currency != 1) throw validation("付款币串锁定状态异常:" + coin.getCurrencyId());
|
||||
jdbc.update("UPDATE payment_coin_reservation SET status='TRANSFERRED',transferred_at=CURRENT_TIMESTAMP " +
|
||||
"WHERE payment_id=? AND currency_id=? AND status='RESERVED'",
|
||||
order.getId().toString(), coin.getCurrencyId());
|
||||
}
|
||||
jdbc.update("INSERT INTO payment_ownership_transfer (transaction_id,payment_id,from_wallet_id,to_wallet_id,amount," +
|
||||
"coin_count,settlement_digest,central_bank_signature,status,confirmed_at,confirmed_by) " +
|
||||
"VALUES (?,?,?,?,?,?,?,?,'CONFIRMED',CURRENT_TIMESTAMP,?)",
|
||||
transactionId, order.getId().toString(), order.getPayerWalletId(), order.getPayeeWalletId(), order.getAmount(),
|
||||
coins.size(), digest, signature, actor.getUsername());
|
||||
jdbc.update("INSERT INTO payment_clearing_record (clearing_no,payment_id,payer_bank_code,payee_bank_code,amount,status,settled_at) " +
|
||||
"VALUES (?,?,?,?,?,'SETTLED',CURRENT_TIMESTAMP)",
|
||||
clearingNo, order.getId().toString(), order.getPayerBankCode(), order.getPayeeBankCode(), order.getAmount());
|
||||
return new CentralSettlementResult(transactionId, clearingNo, originalText, digest, signature);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaymentCreditResult creditPayee(PaymentOrder order, PaymentActor actor) {
|
||||
InstitutionKeySubject subject = subject(actor);
|
||||
if (!keyService.verifyCentralBank(subject, order.getSettlementDigest(), order.getCentralBankSignature())) {
|
||||
throw validation("商业银行B验证中央银行签名失败");
|
||||
}
|
||||
WalletSnapshot payer = lockWallet(order.getPayerWalletId());
|
||||
WalletSnapshot payee = lockWallet(order.getPayeeWalletId());
|
||||
if (payer.frozenAmount.compareTo(order.getAmount()) < 0 || payer.balance.compareTo(order.getAmount()) < 0) {
|
||||
throw validation("付款钱包冻结金额或余额异常");
|
||||
}
|
||||
if (!"ACTIVE".equals(payee.status)) throw validation("收款钱包状态异常");
|
||||
BigDecimal payerAfter = payer.balance.subtract(order.getAmount()).setScale(2);
|
||||
BigDecimal payeeAfter = payee.balance.add(order.getAmount()).setScale(2);
|
||||
jdbc.update("UPDATE digital_wallet SET balance=?,frozen_amount=frozen_amount-?,updated_at=CURRENT_TIMESTAMP WHERE wallet_id=?",
|
||||
payerAfter, order.getAmount(), order.getPayerWalletId());
|
||||
jdbc.update("UPDATE digital_wallet SET balance=?,updated_at=CURRENT_TIMESTAMP WHERE wallet_id=?",
|
||||
payeeAfter, order.getPayeeWalletId());
|
||||
updatePayerUsage(order);
|
||||
jdbc.update("INSERT INTO payment_wallet_ledger (ledger_no,payment_id,wallet_id,direction,amount,balance_after,created_at,created_by) " +
|
||||
"VALUES (?,?,?,'DEBIT',?,?,CURRENT_TIMESTAMP,?)",
|
||||
"PAY_DEBIT_" + order.getPaymentNo(), order.getId().toString(), order.getPayerWalletId(), order.getAmount(), payerAfter, actor.getUsername());
|
||||
jdbc.update("INSERT INTO payment_wallet_ledger (ledger_no,payment_id,wallet_id,direction,amount,balance_after,created_at,created_by) " +
|
||||
"VALUES (?,?,?,'CREDIT',?,?,CURRENT_TIMESTAMP,?)",
|
||||
"PAY_CREDIT_" + order.getPaymentNo(), order.getId().toString(), order.getPayeeWalletId(), order.getAmount(), payeeAfter, actor.getUsername());
|
||||
return new PaymentCreditResult(payerAfter, payeeAfter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createNotifications(PaymentOrder order, PaymentActor actor) {
|
||||
Instant now = Instant.now();
|
||||
jdbc.update("INSERT INTO payment_notification (id,payment_id,recipient_user_id,recipient_wallet_id,notification_type,content,status,created_at) " +
|
||||
"VALUES (?,?,?,?,?,?, 'CREATED',?)",
|
||||
UUID.randomUUID().toString(), order.getId().toString(), order.getPayerUserId(), order.getPayerWalletId(),
|
||||
"PAYMENT_RECEIPT", "支付成功:" + order.getPaymentNo(), java.sql.Timestamp.from(now));
|
||||
jdbc.update("INSERT INTO payment_notification (id,payment_id,recipient_user_id,recipient_wallet_id,notification_type,content,status,created_at) " +
|
||||
"VALUES (?,?,?,?,?,?, 'CREATED',?)",
|
||||
UUID.randomUUID().toString(), order.getId().toString(), order.getPayeeUserId(), order.getPayeeWalletId(),
|
||||
"CREDIT_NOTICE", "到账通知:" + order.getPaymentNo(), java.sql.Timestamp.from(now));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PaymentCoin> findCoins(PaymentOrder order) {
|
||||
return jdbc.query("SELECT currency_id,denomination,status FROM payment_coin_reservation WHERE payment_id=? " +
|
||||
"ORDER BY denomination DESC,currency_id",
|
||||
(rs, row) -> new PaymentCoin(rs.getString("currency_id"), rs.getBigDecimal("denomination"),
|
||||
rs.getString("status")), order.getId().toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PaymentNotification> findNotifications(PaymentOrder order, String currentUserId) {
|
||||
return jdbc.query("SELECT notification_type,recipient_wallet_id,content,created_at FROM payment_notification " +
|
||||
"WHERE payment_id=? AND recipient_user_id=? ORDER BY created_at",
|
||||
(rs, row) -> new PaymentNotification(rs.getString("notification_type"), rs.getString("recipient_wallet_id"),
|
||||
rs.getString("content"), rs.getTimestamp("created_at").toInstant()), order.getId().toString(), currentUserId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendStepLog(PaymentOrder order, String stepCode, String stepName, String output, PaymentActor actor) {
|
||||
jdbc.update("INSERT INTO payment_step_log (payment_id,step_code,step_name,status,output_text,operated_at,operator_user_id,operator_name) " +
|
||||
"VALUES (?,?,?,'SUCCESS',?,CURRENT_TIMESTAMP,?,?)",
|
||||
order.getId().toString(), stepCode, stepName, output, actor.getUserId(), actor.getUsername());
|
||||
}
|
||||
|
||||
private PaymentParticipant loadParticipant(String where, String value) {
|
||||
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 " +
|
||||
"FROM digital_wallet w JOIN wallet_certificate cert ON cert.wallet_id=w.wallet_id " +
|
||||
"AND cert.status='VALID' AND cert.filing_status='REGISTERED' " +
|
||||
"JOIN wallet_bank_binding binding ON binding.wallet_id=w.wallet_id AND binding.status='BOUND' " +
|
||||
"JOIN simulated_bank_account a ON a.account_id=binding.bank_account_id AND a.status='ACTIVE' " +
|
||||
"WHERE " + where + " AND w.status='ACTIVE' AND w.central_bank_confirmation_signature IS NOT NULL",
|
||||
(rs, row) -> new PaymentParticipant(rs.getString("user_id"), rs.getString("wallet_id"),
|
||||
rs.getString("wallet_type"), rs.getString("certificate_serial"), rs.getString("public_key"),
|
||||
rs.getString("bank_code"), rs.getString("bank_name"), organizationId(rs.getString("bank_code")),
|
||||
rs.getBigDecimal("balance"), rs.getBigDecimal("frozen_amount")), value);
|
||||
if (values.isEmpty()) throw validation("付款方或收款方未完成钱包开通、证书备案或银行绑定");
|
||||
return values.get(0);
|
||||
}
|
||||
|
||||
private String organizationId(String bankCode) {
|
||||
List<String> values = jdbc.query("SELECT institution_identifier FROM institution_identifier_application " +
|
||||
"WHERE bank_code=? AND status IN ('ISSUED','FEEDBACKED') AND deleted=FALSE " +
|
||||
"AND institution_identifier IS NOT NULL ORDER BY created_at DESC LIMIT 1",
|
||||
(rs, row) -> rs.getString(1), bankCode);
|
||||
if (values.isEmpty()) throw validation("请先完成机构标识实验并取得商业银行机构标识");
|
||||
return values.get(0);
|
||||
}
|
||||
|
||||
private WalletSnapshot lockWallet(String walletId) {
|
||||
List<WalletSnapshot> values = jdbc.query("SELECT balance,COALESCE(frozen_amount,0) frozen_amount,status " +
|
||||
"FROM digital_wallet WHERE wallet_id=? FOR UPDATE",
|
||||
(rs, row) -> new WalletSnapshot(rs.getBigDecimal("balance"), rs.getBigDecimal("frozen_amount"),
|
||||
rs.getString("status")), walletId);
|
||||
if (values.isEmpty()) throw validation("钱包不存在");
|
||||
return values.get(0);
|
||||
}
|
||||
|
||||
private void assertPayerContract(PaymentOrder order, PaymentActor actor) {
|
||||
List<ContractSnapshot> values = jdbc.query("SELECT single_payment_limit,daily_payment_limit,daily_used_amount,daily_counter_date," +
|
||||
"annual_payment_limit,annual_used_amount,annual_counter_year,status,valid_until FROM wallet_contract " +
|
||||
"WHERE wallet_id=? FOR UPDATE",
|
||||
(rs, row) -> new ContractSnapshot(rs.getBigDecimal("single_payment_limit"), rs.getBigDecimal("daily_payment_limit"),
|
||||
rs.getBigDecimal("daily_used_amount"), rs.getDate("daily_counter_date") == null ? null : rs.getDate("daily_counter_date").toLocalDate(),
|
||||
rs.getBigDecimal("annual_payment_limit"), rs.getBigDecimal("annual_used_amount"),
|
||||
rs.getObject("annual_counter_year") == null ? null : rs.getInt("annual_counter_year"), rs.getString("status"),
|
||||
rs.getTimestamp("valid_until") == null ? null : rs.getTimestamp("valid_until").toInstant()), order.getPayerWalletId());
|
||||
if (values.isEmpty() || !"ACTIVE".equals(values.get(0).status) || (values.get(0).validUntil != null && values.get(0).validUntil.isBefore(Instant.now()))) {
|
||||
throw validation("付款钱包合约不存在或未生效");
|
||||
}
|
||||
ContractSnapshot contract = values.get(0);
|
||||
BigDecimal usedToday = LocalDate.now().equals(contract.dailyDate) ? contract.dailyUsed : BigDecimal.ZERO.setScale(2);
|
||||
int year = LocalDate.now().getYear();
|
||||
BigDecimal usedYear = contract.annualYear != null && contract.annualYear == year ? contract.annualUsed : BigDecimal.ZERO.setScale(2);
|
||||
if (order.getAmount().compareTo(contract.singleLimit) > 0 || usedToday.add(order.getAmount()).compareTo(contract.dailyLimit) > 0 ||
|
||||
usedYear.add(order.getAmount()).compareTo(contract.annualLimit) > 0) throw validation("付款金额超过钱包合约支付限额");
|
||||
}
|
||||
|
||||
private List<PaymentCoin> selectAndLockCoins(PaymentOrder order, PaymentActor actor) {
|
||||
List<CoinCandidate> candidates = jdbc.query("SELECT c.currency_id,c.denomination,c.status FROM commercial_bank_currency c " +
|
||||
"JOIN central_bank_currency_ownership o ON o.currency_id=c.currency_id WHERE o.owner_type='WALLET' " +
|
||||
"AND o.owner_id=? AND o.status='AVAILABLE' AND c.status='TRANSFERRED' ORDER BY c.denomination DESC,c.currency_id FOR UPDATE",
|
||||
(rs, row) -> new CoinCandidate(rs.getString("currency_id"), rs.getBigDecimal("denomination"), rs.getString("status")),
|
||||
order.getPayerWalletId());
|
||||
BigDecimal remaining = order.getAmount();
|
||||
List<CoinCandidate> selected = new ArrayList<CoinCandidate>();
|
||||
for (CoinCandidate candidate : candidates) {
|
||||
if (candidate.denomination.compareTo(remaining) <= 0) {
|
||||
selected.add(candidate); remaining = remaining.subtract(candidate.denomination).setScale(2);
|
||||
if (remaining.signum() == 0) break;
|
||||
}
|
||||
}
|
||||
if (remaining.signum() != 0) throw validation("付款钱包中的数字货币无法组成支付金额");
|
||||
List<PaymentCoin> results = new ArrayList<PaymentCoin>();
|
||||
for (CoinCandidate coin : selected) {
|
||||
if (jdbc.update("UPDATE commercial_bank_currency SET status='PAYMENT_LOCKED' WHERE currency_id=? AND status='TRANSFERRED'",
|
||||
coin.currencyId) != 1) throw validation("付款币串已被其他支付占用");
|
||||
jdbc.update("INSERT INTO payment_coin_reservation (payment_id,currency_id,denomination,source_status,status,reserved_at) " +
|
||||
"VALUES (?,?,?,?, 'RESERVED',CURRENT_TIMESTAMP)",
|
||||
order.getId().toString(), coin.currencyId, coin.denomination, coin.status);
|
||||
results.add(new PaymentCoin(coin.currencyId, coin.denomination, "RESERVED"));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private void updatePayerUsage(PaymentOrder order) {
|
||||
List<ContractSnapshot> values = jdbc.query("SELECT daily_used_amount,daily_counter_date,annual_used_amount,annual_counter_year " +
|
||||
"FROM wallet_contract WHERE wallet_id=? FOR UPDATE",
|
||||
(rs, row) -> new ContractSnapshot(null, null, rs.getBigDecimal("daily_used_amount"),
|
||||
rs.getDate("daily_counter_date") == null ? null : rs.getDate("daily_counter_date").toLocalDate(), null,
|
||||
rs.getBigDecimal("annual_used_amount"), rs.getObject("annual_counter_year") == null ? null : rs.getInt("annual_counter_year"),
|
||||
"ACTIVE", null), order.getPayerWalletId());
|
||||
ContractSnapshot contract = values.get(0);
|
||||
BigDecimal day = LocalDate.now().equals(contract.dailyDate) ? contract.dailyUsed : BigDecimal.ZERO.setScale(2);
|
||||
int year = LocalDate.now().getYear();
|
||||
BigDecimal annual = contract.annualYear != null && contract.annualYear == year ? contract.annualUsed : BigDecimal.ZERO.setScale(2);
|
||||
jdbc.update("UPDATE wallet_contract SET daily_used_amount=?,daily_counter_date=CURRENT_DATE,annual_used_amount=?," +
|
||||
"annual_counter_year=?,updated_at=CURRENT_TIMESTAMP WHERE wallet_id=?",
|
||||
day.add(order.getAmount()), annual.add(order.getAmount()), year, order.getPayerWalletId());
|
||||
}
|
||||
|
||||
private InstitutionKeySubject subject(PaymentActor actor) { return new InstitutionKeySubject(actor.getUserId(), actor.getSchoolId(), actor.getClassId()); }
|
||||
private BusinessException validation(String message) { return new BusinessException(ErrorCode.VALIDATION_ERROR, message); }
|
||||
|
||||
private static final class WalletSnapshot {
|
||||
private final BigDecimal balance; private final BigDecimal frozenAmount; private final String status;
|
||||
private WalletSnapshot(BigDecimal balance, BigDecimal frozenAmount, String status) { this.balance = balance; this.frozenAmount = frozenAmount; this.status = status; }
|
||||
}
|
||||
private static final class ContractSnapshot {
|
||||
private final BigDecimal singleLimit; private final BigDecimal dailyLimit; private final BigDecimal dailyUsed;
|
||||
private final LocalDate dailyDate; private final BigDecimal annualLimit; private final BigDecimal annualUsed;
|
||||
private final Integer annualYear; private final String status; private final Instant validUntil;
|
||||
private ContractSnapshot(BigDecimal singleLimit, BigDecimal dailyLimit, BigDecimal dailyUsed, LocalDate dailyDate,
|
||||
BigDecimal annualLimit, BigDecimal annualUsed, Integer annualYear, String status, Instant validUntil) {
|
||||
this.singleLimit = singleLimit; this.dailyLimit = dailyLimit; this.dailyUsed = dailyUsed; this.dailyDate = dailyDate;
|
||||
this.annualLimit = annualLimit; this.annualUsed = annualUsed; this.annualYear = annualYear; this.status = status; this.validUntil = validUntil;
|
||||
}
|
||||
}
|
||||
private static final class CoinCandidate {
|
||||
private final String currencyId; private final BigDecimal denomination; private final String status;
|
||||
private CoinCandidate(String currencyId, BigDecimal denomination, String status) { this.currencyId = currencyId; this.denomination = denomination; this.status = status; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package com.yau.digitalrmb.payment.interfaces.dto;
|
||||
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
import javax.validation.constraints.Digits;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Size;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class CreatePaymentRequest {
|
||||
@NotBlank private String payerWalletId;
|
||||
@NotBlank private String payeeWalletId;
|
||||
@NotNull @DecimalMin("0.01") @Digits(integer = 18, fraction = 2) private BigDecimal amount;
|
||||
@Size(max = 256) private String note;
|
||||
public String getPayerWalletId() { return payerWalletId; } public void setPayerWalletId(String value) { payerWalletId = value; }
|
||||
public String getPayeeWalletId() { return payeeWalletId; } public void setPayeeWalletId(String value) { payeeWalletId = value; }
|
||||
public BigDecimal getAmount() { return amount; } public void setAmount(BigDecimal value) { amount = value; }
|
||||
public String getNote() { return note; } public void setNote(String value) { note = value; }
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
package com.yau.digitalrmb.training.application;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class TrainingTaskProgressService {
|
||||
private static final String GENERATION = "PBOC_CURRENCY_GENERATION";
|
||||
private static final String ISSUANCE = "PBOC_CURRENCY_ISSUANCE";
|
||||
private static final String WALLET = "WALLET_OPENING";
|
||||
private static final String EXCHANGE = "CURRENCY_EXCHANGE";
|
||||
private static final String PAYMENT = "CURRENCY_PAYMENT";
|
||||
|
||||
private final JdbcTemplate jdbc;
|
||||
private final Map<String, ModuleDefinition> definitions;
|
||||
|
||||
public TrainingTaskProgressService(JdbcTemplate jdbc) {
|
||||
this.jdbc = jdbc;
|
||||
this.definitions = definitions();
|
||||
}
|
||||
|
||||
public List<TrainingTaskView> all(String userId) {
|
||||
List<TrainingTaskView> result = new ArrayList<>();
|
||||
for (String moduleCode : definitions.keySet()) result.add(one(userId, moduleCode));
|
||||
return result;
|
||||
}
|
||||
|
||||
public TrainingTaskView one(String userId, String moduleCode) {
|
||||
ModuleDefinition module = definitions.get(moduleCode);
|
||||
if (module == null) throw new IllegalArgumentException("不支持的实训模块:" + moduleCode);
|
||||
List<String> completed = jdbc.queryForList("SELECT step_code FROM training_task_step_progress WHERE user_id = ? AND module_code = ?",
|
||||
String.class, userId, moduleCode);
|
||||
return view(module, completed);
|
||||
}
|
||||
|
||||
/** 真实业务步骤成功后调用;同步补齐此前步骤,避免历史业务数据导致进度断层。 */
|
||||
public void completeThrough(String userId, String moduleCode, String stepCode) {
|
||||
ModuleDefinition module = definitions.get(moduleCode);
|
||||
if (module == null || module.unavailable) return;
|
||||
int index = module.indexOf(stepCode);
|
||||
if (index < 0) return;
|
||||
Instant now = Instant.now();
|
||||
for (int i = 0; i <= index; i++) {
|
||||
String currentStep = module.steps.get(i).code;
|
||||
jdbc.update("INSERT INTO training_task_step_progress (user_id, module_code, step_code, completed_at) VALUES (?, ?, ?, ?) "
|
||||
+ "ON DUPLICATE KEY UPDATE completed_at = completed_at",
|
||||
userId, moduleCode, currentStep, now);
|
||||
}
|
||||
}
|
||||
|
||||
public void reset(String userId, String moduleCode) {
|
||||
jdbc.update("DELETE FROM training_task_step_progress WHERE user_id = ? AND module_code = ?", userId, moduleCode);
|
||||
}
|
||||
|
||||
private TrainingTaskView view(ModuleDefinition module, List<String> completed) {
|
||||
List<TrainingStepView> steps = new ArrayList<>();
|
||||
boolean prefixCompleted = true;
|
||||
int completedCount = 0;
|
||||
String currentStepCode = null;
|
||||
for (StepDefinition step : module.steps) {
|
||||
boolean done = prefixCompleted && completed.contains(step.code);
|
||||
String status;
|
||||
boolean actionAvailable = false;
|
||||
if (done) {
|
||||
status = "COMPLETED";
|
||||
completedCount++;
|
||||
} else if (module.unavailable && completedCount == 0) {
|
||||
status = "NOT_STARTED";
|
||||
prefixCompleted = false;
|
||||
} else if (prefixCompleted) {
|
||||
status = "CURRENT";
|
||||
actionAvailable = true;
|
||||
currentStepCode = step.code;
|
||||
prefixCompleted = false;
|
||||
} else {
|
||||
status = "LOCKED";
|
||||
}
|
||||
steps.add(new TrainingStepView(step.code, step.name, step.sortNo, status, actionAvailable));
|
||||
}
|
||||
String status = completedCount == module.steps.size() ? "COMPLETED" : completedCount == 0 ? "NOT_STARTED" : "IN_PROGRESS";
|
||||
return new TrainingTaskView(module.code, module.name, status, module.steps.size(), completedCount, currentStepCode,
|
||||
module.unavailable, module.unavailableReason, steps);
|
||||
}
|
||||
|
||||
private Map<String, ModuleDefinition> definitions() {
|
||||
Map<String, ModuleDefinition> result = new LinkedHashMap<>();
|
||||
result.put(GENERATION, module(GENERATION, "货币生成", false, null,
|
||||
"机构身份信息与密钥准备", "生成数字货币请求", "验证货币生成请求", "生成交易信息标识", "生成额度控制位请求", "验证额度控制位请求", "控制系统签名", "生成额度控制位", "生成标准面额币串"));
|
||||
result.put(ISSUANCE, module(ISSUANCE, "货币发行", false, null,
|
||||
"查看货币需求", "生成发行申请计划", "发行计划数字签名", "发送标准报文", "中央银行验证与业务核查", "准备金通知与ACS扣减", "发行并确权数字货币"));
|
||||
result.put(WALLET, module(WALLET, "开立数字钱包", true, "钱包开立真实业务接口尚未实现",
|
||||
"开立数字钱包"));
|
||||
result.put(EXCHANGE, module(EXCHANGE, "货币兑换", false, null,
|
||||
"确认银行卡、钱包和商业银行库存", "生成取币请求报文", "钱包私钥签名", "商业银行验签并扣款", "央行确认币串权属"));
|
||||
result.put(PAYMENT, module(PAYMENT, "数字货币支付", false, null,
|
||||
"初始化付款方和收款方钱包", "签名支付请求", "付款行预处理", "中央银行结算", "收款行入账", "生成支付回执与到账通知"));
|
||||
return Collections.unmodifiableMap(result);
|
||||
}
|
||||
|
||||
private ModuleDefinition module(String code, String name, boolean unavailable, String reason, String... stepNames) {
|
||||
List<StepDefinition> steps = new ArrayList<>();
|
||||
for (int i = 0; i < stepNames.length; i++) steps.add(new StepDefinition(String.format("%02d", i + 1), stepNames[i], (i + 1) * 10));
|
||||
return new ModuleDefinition(code, name, unavailable, reason, steps);
|
||||
}
|
||||
|
||||
@Getter @AllArgsConstructor
|
||||
public static class TrainingTaskView {
|
||||
private final String moduleCode; private final String moduleName; private final String status;
|
||||
private final int totalSteps; private final int completedSteps; private final String currentStepCode;
|
||||
private final boolean actionUnavailable; private final String unavailableReason; private final List<TrainingStepView> steps;
|
||||
}
|
||||
@Getter @AllArgsConstructor
|
||||
public static class TrainingStepView {
|
||||
private final String stepCode; private final String stepName; private final int sortNo; private final String status; private final boolean actionAvailable;
|
||||
}
|
||||
private static class ModuleDefinition {
|
||||
private final String code, name; private final boolean unavailable; private final String unavailableReason; private final List<StepDefinition> steps;
|
||||
private ModuleDefinition(String code, String name, boolean unavailable, String unavailableReason, List<StepDefinition> steps) { this.code = code; this.name = name; this.unavailable = unavailable; this.unavailableReason = unavailableReason; this.steps = steps; }
|
||||
private int indexOf(String code) { for (int i = 0; i < steps.size(); i++) if (steps.get(i).code.equals(code)) return i; return -1; }
|
||||
}
|
||||
private static class StepDefinition { private final String code, name; private final int sortNo; private StepDefinition(String code, String name, int sortNo) { this.code = code; this.name = name; this.sortNo = sortNo; } }
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
package com.yau.digitalrmb.training.infrastructure;
|
||||
|
||||
import com.yau.digitalrmb.security.application.CurrentUserService;
|
||||
import com.yau.digitalrmb.training.application.TrainingTaskProgressService;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/** 将已成功返回的真实业务操作映射为对应模块任务步骤;不暴露独立的人工完成接口。 */
|
||||
@Component
|
||||
public class TrainingTaskProgressInterceptor implements HandlerInterceptor {
|
||||
private final TrainingTaskProgressService progressService;
|
||||
private final CurrentUserService currentUserService;
|
||||
|
||||
public TrainingTaskProgressInterceptor(TrainingTaskProgressService progressService, CurrentUserService currentUserService) {
|
||||
this.progressService = progressService;
|
||||
this.currentUserService = currentUserService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
|
||||
if (response.getStatus() >= 400) return;
|
||||
String[] mapping = mapping(request.getMethod(), path(request));
|
||||
if (mapping == null) return;
|
||||
String userId = currentUserService.getCurrentUser().getUserId();
|
||||
if ("RESET".equals(mapping[1])) progressService.reset(userId, mapping[0]);
|
||||
else progressService.completeThrough(userId, mapping[0], mapping[1]);
|
||||
}
|
||||
|
||||
private String path(HttpServletRequest request) {
|
||||
String value = request.getRequestURI();
|
||||
String context = request.getContextPath();
|
||||
return context == null || context.isEmpty() ? value : value.substring(context.length());
|
||||
}
|
||||
|
||||
private String[] mapping(String method, String path) {
|
||||
if ("POST".equals(method) && "/api/v1/institution-identifiers/training/reset".equals(path)) return pair("PBOC_CURRENCY_GENERATION", "RESET");
|
||||
if ("POST".equals(method) && "/api/v1/institution-identifiers/applications/feedback".equals(path)) return pair("PBOC_CURRENCY_GENERATION", "01");
|
||||
if ("POST".equals(method) && "/api/v1/institution-identifiers/currency-requests/steps/send".equals(path)) return pair("PBOC_CURRENCY_GENERATION", "02");
|
||||
if ("POST".equals(method) && "/api/v1/institution-identifiers/currency-request-verifications/steps/return-response".equals(path)) return pair("PBOC_CURRENCY_GENERATION", "03");
|
||||
if ("POST".equals(method) && "/api/v1/institution-identifiers/transaction-information-identifiers/steps/assemble".equals(path)) return pair("PBOC_CURRENCY_GENERATION", "04");
|
||||
if ("POST".equals(method) && "/api/v1/institution-identifiers/quota-requests/steps/send".equals(path)) return pair("PBOC_CURRENCY_GENERATION", "05");
|
||||
if ("POST".equals(method) && "/api/v1/institution-identifiers/quota-verifications/steps/compare-digests".equals(path)) return pair("PBOC_CURRENCY_GENERATION", "06");
|
||||
if ("POST".equals(method) && "/api/v1/institution-identifiers/control-system-signatures/steps/send".equals(path)) return pair("PBOC_CURRENCY_GENERATION", "07");
|
||||
if ("POST".equals(method) && "/api/v1/institution-identifiers/quota-control-bits/steps/send".equals(path)) return pair("PBOC_CURRENCY_GENERATION", "08");
|
||||
if ("POST".equals(method) && "/api/v1/institution-identifiers/standard-currencies/steps/generate".equals(path)) return pair("PBOC_CURRENCY_GENERATION", "09");
|
||||
|
||||
if ("GET".equals(method) && "/api/v1/commercial-banks/issuance/inventory".equals(path)) return pair("PBOC_CURRENCY_ISSUANCE", "01");
|
||||
if ("POST".equals(method) && path.matches("/api/v1/commercial-banks/issuance/requests/[^/]+/prepare-message")) return pair("PBOC_CURRENCY_ISSUANCE", "02");
|
||||
if ("POST".equals(method) && path.matches("/api/v1/commercial-banks/issuance/requests/[^/]+/sign")) return pair("PBOC_CURRENCY_ISSUANCE", "03");
|
||||
if ("POST".equals(method) && path.matches("/api/v1/commercial-banks/issuance/requests/[^/]+/send")) return pair("PBOC_CURRENCY_ISSUANCE", "04");
|
||||
if ("POST".equals(method) && path.matches("/api/v1/central-banks/issuance/requests/[^/]+/business-review")) return pair("PBOC_CURRENCY_ISSUANCE", "05");
|
||||
if ("POST".equals(method) && (path.matches("/api/v1/central-banks/issuance/reserve-deductions/[^/]+/execute") || "/api/v1/central-banks/issuance/reserve-deductions".equals(path))) return pair("PBOC_CURRENCY_ISSUANCE", "06");
|
||||
if ("POST".equals(method) && path.matches("/api/v1/central-banks/issuance/requests/[^/]+/digital-currency-ownership")) return pair("PBOC_CURRENCY_ISSUANCE", "07");
|
||||
|
||||
if ("GET".equals(method) && "/api/v1/exchanges/context".equals(path)) return pair("CURRENCY_EXCHANGE", "01");
|
||||
if ("POST".equals(method) && "/api/v1/exchanges".equals(path)) return pair("CURRENCY_EXCHANGE", "02");
|
||||
if ("POST".equals(method) && path.matches("/api/v1/exchanges/[^/]+/sign")) return pair("CURRENCY_EXCHANGE", "03");
|
||||
if ("POST".equals(method) && path.matches("/api/v1/exchanges/[^/]+/bank-process")) return pair("CURRENCY_EXCHANGE", "04");
|
||||
if ("POST".equals(method) && path.matches("/api/v1/exchanges/[^/]+/confirm-ownership")) return pair("CURRENCY_EXCHANGE", "05");
|
||||
|
||||
if ("GET".equals(method) && "/api/v1/payments/context".equals(path)) return pair("CURRENCY_PAYMENT", "01");
|
||||
if ("POST".equals(method) && path.matches("/api/v1/payments/[^/]+/sign")) return pair("CURRENCY_PAYMENT", "02");
|
||||
if ("POST".equals(method) && path.matches("/api/v1/payments/[^/]+/payer-bank-process")) return pair("CURRENCY_PAYMENT", "03");
|
||||
if ("POST".equals(method) && path.matches("/api/v1/payments/[^/]+/central-bank-settle")) return pair("CURRENCY_PAYMENT", "04");
|
||||
if ("POST".equals(method) && path.matches("/api/v1/payments/[^/]+/payee-bank-credit")) return pair("CURRENCY_PAYMENT", "05");
|
||||
if ("POST".equals(method) && path.matches("/api/v1/payments/[^/]+/notifications")) return pair("CURRENCY_PAYMENT", "06");
|
||||
return null;
|
||||
}
|
||||
private String[] pair(String moduleCode, String stepCode) { return new String[]{moduleCode, stepCode}; }
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package com.yau.digitalrmb.training.infrastructure;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class TrainingTaskProgressWebConfig implements WebMvcConfigurer {
|
||||
private final TrainingTaskProgressInterceptor interceptor;
|
||||
public TrainingTaskProgressWebConfig(TrainingTaskProgressInterceptor interceptor) { this.interceptor = interceptor; }
|
||||
@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(interceptor).addPathPatterns("/api/v1/**"); }
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
package com.yau.digitalrmb.training.interfaces.rest;
|
||||
|
||||
import com.yau.digitalrmb.security.application.CurrentUserService;
|
||||
import com.yau.digitalrmb.shared.api.ApiResponse;
|
||||
import com.yau.digitalrmb.shared.web.TraceIdFilter;
|
||||
import com.yau.digitalrmb.training.application.TrainingTaskProgressService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/training-tasks")
|
||||
@Tag(name = "实训任务进度", description = "按当前用户分别返回五个实验模块的子步骤完成进度")
|
||||
public class TrainingTaskProgressController {
|
||||
private final TrainingTaskProgressService service;
|
||||
private final CurrentUserService currentUserService;
|
||||
public TrainingTaskProgressController(TrainingTaskProgressService service, CurrentUserService currentUserService) { this.service = service; this.currentUserService = currentUserService; }
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "查询当前用户全部实训任务进度")
|
||||
public ApiResponse<List<TrainingTaskProgressService.TrainingTaskView>> all() { return ok(service.all(userId())); }
|
||||
|
||||
@GetMapping("/{moduleCode}")
|
||||
@Operation(summary = "查询当前用户单个实训任务进度")
|
||||
public ApiResponse<TrainingTaskProgressService.TrainingTaskView> one(@PathVariable String moduleCode) { return ok(service.one(userId(), moduleCode)); }
|
||||
|
||||
private String userId() { return currentUserService.getCurrentUser().getUserId(); }
|
||||
private <T> ApiResponse<T> ok(T data) { return ApiResponse.success(data, MDC.get(TraceIdFilter.MDC_KEY)); }
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
-- 五个实验模块的用户步骤进度。每条记录只由真实业务接口成功后写入。
|
||||
CREATE TABLE IF NOT EXISTS training_task_step_progress (
|
||||
user_id VARCHAR(36) NOT NULL,
|
||||
module_code VARCHAR(64) NOT NULL,
|
||||
step_code VARCHAR(32) NOT NULL,
|
||||
completed_at TIMESTAMP NOT NULL,
|
||||
PRIMARY KEY (user_id, module_code, step_code),
|
||||
KEY idx_training_task_progress_user_module (user_id, module_code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
@ -0,0 +1,179 @@
|
||||
package com.yau.digitalrmb.payment.interfaces.rest;
|
||||
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import com.yau.digitalrmb.exchange.infrastructure.crypto.WalletPrivateKeyCipher;
|
||||
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyService;
|
||||
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
|
||||
import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography;
|
||||
import com.yau.digitalrmb.institutionidentity.domain.InstitutionSm2KeyPair;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.RequestPostProcessor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest(properties = "file.path=target/test-files")
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
class PaymentControllerTest {
|
||||
private static final String PAYER_USER = "00000000-0000-0000-0000-000000000901";
|
||||
private static final String PAYEE_USER = "00000000-0000-0000-0000-000000000902";
|
||||
private static final String PAYER_WALLET = "WALLET_PAYMENT_A";
|
||||
private static final String PAYEE_WALLET = "WALLET_PAYMENT_B";
|
||||
|
||||
@Autowired private MockMvc mockMvc;
|
||||
@Autowired private JdbcTemplate jdbc;
|
||||
@Autowired private InstitutionIdentityCryptography cryptography;
|
||||
@Autowired private WalletPrivateKeyCipher walletCipher;
|
||||
@Autowired private InstitutionKeyService keyService;
|
||||
|
||||
@BeforeEach
|
||||
void seedPrerequisites() {
|
||||
cleanPaymentData();
|
||||
insertUser(PAYER_USER, "payment-a", "3001", "2001");
|
||||
insertUser(PAYEE_USER, "payment-b", "3001", "2001");
|
||||
jdbc.update("INSERT INTO institution_identifier_application (id,bank_code,user_id,school_id,class_id,request_timestamp," +
|
||||
"original_text,status,institution_identifier,training_round,scoring_criteria,created_at,updated_at,created_by,updated_by,deleted) " +
|
||||
"VALUES (?,?,?,?,?,?,?,'ISSUED',?,1,0,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,'test','test',FALSE)",
|
||||
991001L, "BKCHCNBJ00001", PAYER_USER, 3001L, 2001L, "20260814090000", "BANK_A", "ORG_PAYMENT_A");
|
||||
jdbc.update("INSERT INTO institution_identifier_application (id,bank_code,user_id,school_id,class_id,request_timestamp," +
|
||||
"original_text,status,institution_identifier,training_round,scoring_criteria,created_at,updated_at,created_by,updated_by,deleted) " +
|
||||
"VALUES (?,?,?,?,?,?,?,'ISSUED',?,1,0,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,'test','test',FALSE)",
|
||||
991002L, "BKCHCNBJ00002", PAYEE_USER, 3001L, 2001L, "20260814090000", "BANK_B", "ORG_PAYMENT_B");
|
||||
InstitutionKeySubject subject = new InstitutionKeySubject(PAYER_USER, 3001L, 2001L);
|
||||
keyService.commercialBankKey(subject, "test");
|
||||
keyService.centralBankKey(subject, "test");
|
||||
insertWallet(PAYER_USER, PAYER_WALLET, "CERT_PAYMENT_A", "BKCHCNBJ00001", "ACCOUNT_PAYMENT_A", new BigDecimal("200.00"));
|
||||
insertWallet(PAYEE_USER, PAYEE_WALLET, "CERT_PAYMENT_B", "BKCHCNBJ00002", "ACCOUNT_PAYMENT_B", BigDecimal.ZERO);
|
||||
insertPayerContract();
|
||||
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
|
||||
void cleanUp() {
|
||||
cleanPaymentData();
|
||||
jdbc.update("DELETE FROM institution_sm2_key_audit WHERE user_id=?", PAYER_USER);
|
||||
jdbc.update("DELETE FROM institution_sm2_key WHERE user_id=?", PAYER_USER);
|
||||
jdbc.update("DELETE FROM institution_identifier_application WHERE id IN (991001,991002)");
|
||||
jdbc.update("DELETE FROM wallet_bank_binding WHERE wallet_id IN (?,?)", PAYER_WALLET, PAYEE_WALLET);
|
||||
jdbc.update("DELETE FROM wallet_contract WHERE wallet_id=?", PAYER_WALLET);
|
||||
jdbc.update("DELETE FROM wallet_certificate WHERE wallet_id IN (?,?)", PAYER_WALLET, PAYEE_WALLET);
|
||||
jdbc.update("DELETE FROM simulated_bank_account WHERE account_id IN ('ACCOUNT_PAYMENT_A','ACCOUNT_PAYMENT_B')");
|
||||
jdbc.update("DELETE FROM digital_wallet WHERE wallet_id IN (?,?)", PAYER_WALLET, PAYEE_WALLET);
|
||||
jdbc.update("DELETE FROM commercial_bank_currency WHERE issuance_request_id='PAYMENT_TEST_ISSUANCE'");
|
||||
jdbc.update("DELETE FROM sys_user WHERE user_id IN (?,?)", PAYER_USER, PAYEE_USER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void completesTheSixPrototypeStepsAcrossTwoBanks() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/payments/context").param("payeeWalletId", PAYEE_WALLET).with(payer()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.data.payer.walletId").value(PAYER_WALLET))
|
||||
.andExpect(jsonPath("$.data.payee.walletId").value(PAYEE_WALLET));
|
||||
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\":\"实验支付\"}"))
|
||||
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("REQUEST_PREPARED"))
|
||||
.andExpect(jsonPath("$.data.paymentOriginalText").value(org.hamcrest.Matchers.startsWith("PAY|")))
|
||||
.andReturn().getResponse().getContentAsString();
|
||||
String id = JsonPath.read(created, "$.data.id");
|
||||
|
||||
mockMvc.perform(post("/api/v1/payments/{id}/sign", id).with(payer()))
|
||||
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("PAYER_SIGNED"));
|
||||
mockMvc.perform(post("/api/v1/payments/{id}/payer-bank-process", id).with(payer()))
|
||||
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("PAYER_BANK_ACCEPTED"))
|
||||
.andExpect(jsonPath("$.data.coinCount").value(2));
|
||||
mockMvc.perform(post("/api/v1/payments/{id}/central-bank-settle", id).with(payer()))
|
||||
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("CENTRAL_SETTLED"))
|
||||
.andExpect(jsonPath("$.data.clearingNo").value(org.hamcrest.Matchers.startsWith("ACS_PAY_REQ_")));
|
||||
mockMvc.perform(post("/api/v1/payments/{id}/payee-bank-credit", id).with(payer()))
|
||||
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("PAYEE_CREDITED"))
|
||||
.andExpect(jsonPath("$.data.payerBalanceAfter").value(0.00))
|
||||
.andExpect(jsonPath("$.data.payeeBalanceAfter").value(200.00));
|
||||
mockMvc.perform(post("/api/v1/payments/{id}/notifications", id).with(payer()))
|
||||
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("SUCCESS"))
|
||||
.andExpect(jsonPath("$.data.notifications[0].notificationType").value("PAYMENT_RECEIPT"));
|
||||
|
||||
mockMvc.perform(get("/api/v1/training-tasks/CURRENCY_PAYMENT").with(payer()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.data.status").value("COMPLETED"))
|
||||
.andExpect(jsonPath("$.data.completedSteps").value(6))
|
||||
.andExpect(jsonPath("$.data.steps[5].status").value("COMPLETED"));
|
||||
mockMvc.perform(get("/api/v1/training-tasks").with(payer()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.length()").value(5))
|
||||
.andExpect(jsonPath("$.data[2].moduleCode").value("WALLET_OPENING"))
|
||||
.andExpect(jsonPath("$.data[2].actionUnavailable").value(true));
|
||||
|
||||
assertThat(jdbc.queryForObject("SELECT balance FROM digital_wallet WHERE wallet_id=?", BigDecimal.class, PAYER_WALLET))
|
||||
.isEqualByComparingTo("0.00");
|
||||
assertThat(jdbc.queryForObject("SELECT frozen_amount FROM digital_wallet WHERE wallet_id=?", BigDecimal.class, PAYER_WALLET))
|
||||
.isEqualByComparingTo("0.00");
|
||||
assertThat(jdbc.queryForObject("SELECT balance FROM digital_wallet WHERE wallet_id=?", BigDecimal.class, PAYEE_WALLET))
|
||||
.isEqualByComparingTo("200.00");
|
||||
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM payment_wallet_ledger WHERE payment_id=?", Integer.class, id)).isEqualTo(2);
|
||||
assertThat(jdbc.queryForObject("SELECT owner_id FROM central_bank_currency_ownership WHERE currency_id='DC_PAYMENT_1'", String.class))
|
||||
.isEqualTo(PAYEE_WALLET);
|
||||
}
|
||||
|
||||
private void insertUser(String id, String account, String schoolId, String classId) {
|
||||
jdbc.update("INSERT INTO sys_user (user_id,student_id,password,user_name,class_id,class_name,school_id,school_name,role_id,is_deleted,zy_user_id) " +
|
||||
"VALUES (?,?,?,?,?,'支付测试班',?,'延安大学',4,0,?)", id, account, "unused", account, classId, schoolId, account);
|
||||
}
|
||||
|
||||
private void insertWallet(String userId, String walletId, String certificate, String bankCode, String accountId, BigDecimal balance) {
|
||||
InstitutionSm2KeyPair key = cryptography.generateSm2KeyPair();
|
||||
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',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", walletId, userId, balance);
|
||||
jdbc.update("INSERT INTO wallet_certificate (certificate_serial,wallet_id,public_key,encrypted_private_key,filing_status,central_bank_root_signature,status,issued_at) " +
|
||||
"VALUES (?,?,?,?,'REGISTERED','CB_ROOT','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) " +
|
||||
"VALUES (?,?,?,?,'6222020000000001','0001',0,0,'ACTIVE',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", accountId, userId, bankCode, bankCode);
|
||||
jdbc.update("INSERT INTO wallet_bank_binding (wallet_id,bank_account_id,status,bound_at) VALUES (?,?,'BOUND',CURRENT_TIMESTAMP)", walletId, accountId);
|
||||
}
|
||||
|
||||
private void insertPayerContract() {
|
||||
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 ('CONTRACT_PAYMENT_A',?,'TYPE_II',50000,100000,500000,500000,NULL,'CONTRACT','DIGEST','ACTIVE',0,CURRENT_DATE,0,2026,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", PAYER_WALLET);
|
||||
}
|
||||
|
||||
private void cleanPaymentData() {
|
||||
jdbc.update("DELETE FROM training_task_step_progress WHERE user_id=? AND module_code='CURRENCY_PAYMENT'", PAYER_USER);
|
||||
jdbc.update("DELETE FROM payment_step_log");
|
||||
jdbc.update("DELETE FROM payment_notification");
|
||||
jdbc.update("DELETE FROM payment_wallet_ledger");
|
||||
jdbc.update("DELETE FROM payment_clearing_record");
|
||||
jdbc.update("DELETE FROM payment_ownership_transfer");
|
||||
jdbc.update("DELETE FROM payment_coin_reservation");
|
||||
jdbc.update("DELETE FROM payment_order");
|
||||
jdbc.update("DELETE FROM central_bank_currency_ownership WHERE currency_id LIKE 'DC_PAYMENT_%'");
|
||||
}
|
||||
|
||||
private RequestPostProcessor payer() {
|
||||
return jwt().jwt(jwt -> jwt.subject(PAYER_USER).claim("userId", PAYER_USER).claim("preferred_username", "payment-a"));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue