加固发行与兑换接口异常及并发处理

agent/payment-training-progress
chenyuan 2 weeks ago
parent 3afec5bcb9
commit 73fe422495

@ -21,6 +21,8 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Clock; import java.time.Clock;
import java.time.Instant; import java.time.Instant;
import java.time.ZoneOffset; import java.time.ZoneOffset;
@ -55,6 +57,8 @@ public class ExchangeApplicationService {
@Transactional @Transactional
public ExchangeOrderView create(CreateExchangeCommand command, ExchangeActor actor) { public ExchangeOrderView create(CreateExchangeCommand command, ExchangeActor actor) {
if (command == null) throw validation("兑换请求不能为空");
BigDecimal amount = amount(command.getAmount());
ExchangeContext context = resourceRepository.loadContext(actor); ExchangeContext context = resourceRepository.loadContext(actor);
if (!context.getWalletId().equals(command.getWalletId()) || !context.getBankAccountId().equals(command.getBankAccountId())) { if (!context.getWalletId().equals(command.getWalletId()) || !context.getBankAccountId().equals(command.getBankAccountId())) {
throw validation("钱包或银行卡不属于当前用户,或尚未完成绑定"); throw validation("钱包或银行卡不属于当前用户,或尚未完成绑定");
@ -63,13 +67,13 @@ public class ExchangeApplicationService {
UUID id = UUID.randomUUID(); UUID id = UUID.randomUUID();
String timestamp = TIMESTAMP.format(now); String timestamp = TIMESTAMP.format(now);
String requestNo = "WITHDRAW_REQ_" + timestamp + "_" + id.toString().substring(0, 8).toUpperCase(); String requestNo = "WITHDRAW_REQ_" + timestamp + "_" + id.toString().substring(0, 8).toUpperCase();
String message = "WITHDRAW|" + context.getWalletId() + "|" + command.getAmount().setScale(2).toPlainString() String message = "WITHDRAW|" + context.getWalletId() + "|" + amount.toPlainString()
+ "|" + context.getBankCardNumber().replace(" ", "") + "|" + timestamp; + "|" + context.getBankCardNumber().replace(" ", "") + "|" + timestamp;
String signingOriginal = "SIGN|" + requestNo + "|" + command.getAmount().setScale(2).toPlainString() String signingOriginal = "SIGN|" + requestNo + "|" + amount.toPlainString()
+ "|" + context.getBankCardLast4() + "|" + timestamp; + "|" + context.getBankCardLast4() + "|" + timestamp;
ExchangeOrder order = ExchangeOrder.create(new ExchangeOrderId(id), requestNo, actor.getUserId(), ExchangeOrder order = ExchangeOrder.create(new ExchangeOrderId(id), requestNo, actor.getUserId(),
context.getWalletId(), context.getBankAccountId(), context.getBankCode(), context.getOrganizationId(), context.getWalletId(), context.getBankAccountId(), context.getBankCode(), context.getOrganizationId(),
command.getAmount(), timestamp, message, signingOriginal, cryptography.sm3(signingOriginal), now); amount, timestamp, message, signingOriginal, cryptography.sm3(signingOriginal), now);
orderRepository.save(order, actor.getUsername()); orderRepository.save(order, actor.getUsername());
resourceRepository.appendStepLog(order, "02", "生成取币请求报文", message, actor); resourceRepository.appendStepLog(order, "02", "生成取币请求报文", message, actor);
return ExchangeOrderView.from(order, null); return ExchangeOrderView.from(order, null);
@ -77,7 +81,7 @@ public class ExchangeApplicationService {
@Transactional @Transactional
public ExchangeOrderView sign(UUID id, ExchangeActor actor) { public ExchangeOrderView sign(UUID id, ExchangeActor actor) {
ExchangeOrder order = owned(id, actor); ExchangeOrder order = ownedForUpdate(id, actor);
if (order.getStatus() != ExchangeStatus.MESSAGE_PREPARED) { if (order.getStatus() != ExchangeStatus.MESSAGE_PREPARED) {
if (order.getStatus().ordinal() > ExchangeStatus.MESSAGE_PREPARED.ordinal()) return current(order); if (order.getStatus().ordinal() > ExchangeStatus.MESSAGE_PREPARED.ordinal()) return current(order);
throw validation("当前兑换订单尚不能签名"); throw validation("当前兑换订单尚不能签名");
@ -90,7 +94,7 @@ public class ExchangeApplicationService {
@Transactional @Transactional
public ExchangeOrderView bankProcess(UUID id, ExchangeActor actor) { public ExchangeOrderView bankProcess(UUID id, ExchangeActor actor) {
ExchangeOrder order = owned(id, actor); ExchangeOrder order = ownedForUpdate(id, actor);
if (order.getStatus() != ExchangeStatus.SIGNED) { if (order.getStatus() != ExchangeStatus.SIGNED) {
if (order.getStatus().ordinal() > ExchangeStatus.SIGNED.ordinal()) return current(order); if (order.getStatus().ordinal() > ExchangeStatus.SIGNED.ordinal()) return current(order);
throw validation("请先完成钱包签名"); throw validation("请先完成钱包签名");
@ -106,7 +110,7 @@ public class ExchangeApplicationService {
@Transactional @Transactional
public ExchangeOrderView reserveCoins(UUID id, ExchangeActor actor) { public ExchangeOrderView reserveCoins(UUID id, ExchangeActor actor) {
ExchangeOrder order = owned(id, actor); ExchangeOrder order = ownedForUpdate(id, actor);
if (order.getStatus() != ExchangeStatus.BANK_PROCESSED) { if (order.getStatus() != ExchangeStatus.BANK_PROCESSED) {
if (order.getStatus().ordinal() > ExchangeStatus.BANK_PROCESSED.ordinal()) return current(order); if (order.getStatus().ordinal() > ExchangeStatus.BANK_PROCESSED.ordinal()) return current(order);
throw validation("请先完成商业银行验签和扣款"); throw validation("请先完成商业银行验签和扣款");
@ -119,7 +123,7 @@ public class ExchangeApplicationService {
@Transactional @Transactional
public ExchangeOrderView confirmOwnership(UUID id, ExchangeActor actor) { public ExchangeOrderView confirmOwnership(UUID id, ExchangeActor actor) {
ExchangeOrder order = owned(id, actor); ExchangeOrder order = ownedForUpdate(id, actor);
if (order.getStatus() == ExchangeStatus.SUCCESS) return current(order); if (order.getStatus() == ExchangeStatus.SUCCESS) return current(order);
if (order.getStatus() != ExchangeStatus.COINS_RESERVED) throw validation("请先锁定等额商业银行币串"); if (order.getStatus() != ExchangeStatus.COINS_RESERVED) throw validation("请先锁定等额商业银行币串");
List<ReservedCoin> coins = resourceRepository.findReservedCoins(order); List<ReservedCoin> coins = resourceRepository.findReservedCoins(order);
@ -164,9 +168,28 @@ public class ExchangeApplicationService {
return order; return order;
} }
private ExchangeOrder ownedForUpdate(UUID id, ExchangeActor actor) {
ExchangeOrder order = orderRepository.findByIdForUpdate(new ExchangeOrderId(id))
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "数字货币兑换订单不存在"));
try { order.requireOwnedBy(actor.getUserId()); }
catch (SecurityException exception) { throw new BusinessException(ErrorCode.FORBIDDEN, exception.getMessage()); }
return order;
}
private ExchangeOrderView current(ExchangeOrder order) { private ExchangeOrderView current(ExchangeOrder order) {
return ExchangeOrderView.from(order, resourceRepository.findReservedCoins(order)); return ExchangeOrderView.from(order, resourceRepository.findReservedCoins(order));
} }
private BusinessException validation(String message) { return new BusinessException(ErrorCode.VALIDATION_ERROR, message); } private BusinessException validation(String message) { return new BusinessException(ErrorCode.VALIDATION_ERROR, message); }
private BigDecimal amount(BigDecimal value) {
if (value == null) throw validation("兑换金额不能为空");
try {
BigDecimal normalized = value.setScale(2, RoundingMode.UNNECESSARY);
if (normalized.signum() <= 0) throw validation("兑换金额必须大于零");
return normalized;
} catch (ArithmeticException exception) {
throw validation("兑换金额最多保留两位小数");
}
}
} }

@ -8,4 +8,7 @@ import java.util.Optional;
public interface ExchangeOrderRepository { public interface ExchangeOrderRepository {
void save(ExchangeOrder order, String operator); void save(ExchangeOrder order, String operator);
Optional<ExchangeOrder> findById(ExchangeOrderId id); Optional<ExchangeOrder> findById(ExchangeOrderId id);
default Optional<ExchangeOrder> findByIdForUpdate(ExchangeOrderId id) {
return findById(id);
}
} }

@ -56,7 +56,17 @@ public class JdbcExchangeOrderRepository implements ExchangeOrderRepository {
@Override @Override
public Optional<ExchangeOrder> findById(ExchangeOrderId id) { public Optional<ExchangeOrder> findById(ExchangeOrderId id) {
List<ExchangeOrder> values = jdbc.query("SELECT * FROM currency_exchange_order WHERE id=?", (rs, row) -> return findById(id, false);
}
@Override
public Optional<ExchangeOrder> findByIdForUpdate(ExchangeOrderId id) {
return findById(id, true);
}
private Optional<ExchangeOrder> findById(ExchangeOrderId id, boolean forUpdate) {
String sql = "SELECT * FROM currency_exchange_order WHERE id=?" + (forUpdate ? " FOR UPDATE" : "");
List<ExchangeOrder> values = jdbc.query(sql, (rs, row) ->
ExchangeOrder.rehydrate(new ExchangeOrderId(java.util.UUID.fromString(rs.getString("id"))), ExchangeOrder.rehydrate(new ExchangeOrderId(java.util.UUID.fromString(rs.getString("id"))),
rs.getString("request_no"), rs.getString("user_id"), rs.getString("wallet_id"), rs.getString("request_no"), rs.getString("user_id"), rs.getString("wallet_id"),
rs.getString("bank_account_id"), rs.getString("bank_code"), rs.getString("organization_id"), rs.getString("bank_account_id"), rs.getString("bank_code"), rs.getString("organization_id"),

@ -1,6 +1,7 @@
package com.yau.digitalrmb.exchange.interfaces.dto; package com.yau.digitalrmb.exchange.interfaces.dto;
import javax.validation.constraints.DecimalMin; import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.Digits;
import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import java.math.BigDecimal; import java.math.BigDecimal;
@ -8,7 +9,7 @@ import java.math.BigDecimal;
public class CreateExchangeRequest { public class CreateExchangeRequest {
@NotBlank private String walletId; @NotBlank private String walletId;
@NotBlank private String bankAccountId; @NotBlank private String bankAccountId;
@NotNull @DecimalMin("0.01") private BigDecimal amount; @NotNull @DecimalMin("0.01") @Digits(integer = 18, fraction = 2) private BigDecimal amount;
public String getWalletId() { return walletId; } public String getWalletId() { return walletId; }
public void setWalletId(String walletId) { this.walletId = walletId; } public void setWalletId(String walletId) { this.walletId = walletId; }

@ -97,7 +97,7 @@ public class CommercialBankIssuanceApplicationService {
command.getDenominations(), auditActor(auditActor).getUserId()); command.getDenominations(), auditActor(auditActor).getUserId());
repository.save(request, auditActor); repository.save(request, auditActor);
return IssuanceRequestView.from(request); return IssuanceRequestView.from(request);
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) { } catch (IllegalArgumentException | IllegalStateException | ArithmeticException | NullPointerException exception) {
throw validationError(exception); throw validationError(exception);
} }
} }
@ -109,11 +109,11 @@ public class CommercialBankIssuanceApplicationService {
@Transactional @Transactional
public IssuanceRequestView update(UUID id, UpdateIssuanceRequestCommand command, IssuanceAuditActor auditActor) { public IssuanceRequestView update(UUID id, UpdateIssuanceRequestCommand command, IssuanceAuditActor auditActor) {
try { try {
IssuanceRequest request = requireOwnedRequest(id, auditActor); IssuanceRequest request = requireOwnedRequestForUpdate(id, auditActor);
request.updateDraft(command.getTotalAmount(), command.getCurrency(), command.getDenominations()); request.updateDraft(command.getTotalAmount(), command.getCurrency(), command.getDenominations());
repository.save(request, auditActor(auditActor)); repository.save(request, auditActor(auditActor));
return IssuanceRequestView.from(request); return IssuanceRequestView.from(request);
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) { } catch (IllegalArgumentException | IllegalStateException | ArithmeticException | NullPointerException exception) {
throw validationError(exception); throw validationError(exception);
} }
} }
@ -125,11 +125,11 @@ public class CommercialBankIssuanceApplicationService {
@Transactional @Transactional
public IssuanceRequestView captureTimestamp(UUID id, IssuanceAuditActor auditActor) { public IssuanceRequestView captureTimestamp(UUID id, IssuanceAuditActor auditActor) {
try { try {
IssuanceRequest request = requireOwnedRequest(id, auditActor); IssuanceRequest request = requireOwnedRequestForUpdate(id, auditActor);
request.captureTimestamp(LocalDateTime.now().format(TIMESTAMP_FORMAT)); request.captureTimestamp(LocalDateTime.now().format(TIMESTAMP_FORMAT));
repository.save(request, auditActor(auditActor)); repository.save(request, auditActor(auditActor));
return IssuanceRequestView.from(request); return IssuanceRequestView.from(request);
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) { } catch (IllegalArgumentException | IllegalStateException | ArithmeticException | NullPointerException exception) {
throw validationError(exception); throw validationError(exception);
} }
} }
@ -141,13 +141,13 @@ public class CommercialBankIssuanceApplicationService {
@Transactional @Transactional
public IssuanceRequestView prepareMessage(UUID id, IssuanceAuditActor auditActor) { public IssuanceRequestView prepareMessage(UUID id, IssuanceAuditActor auditActor) {
try { try {
IssuanceRequest request = requireOwnedRequest(id, auditActor); IssuanceRequest request = requireOwnedRequestForUpdate(id, auditActor);
String message = messageComposer.compose(request.getBankCode(), request.getOrganizationId(), String message = messageComposer.compose(request.getBankCode(), request.getOrganizationId(),
request.getTotalAmount(), request.getDenominations(), request.getCurrency(), request.getRequestTimestamp()); request.getTotalAmount(), request.getDenominations(), request.getCurrency(), request.getRequestTimestamp());
request.prepareMessage(message); request.prepareMessage(message);
repository.save(request, auditActor(auditActor)); repository.save(request, auditActor(auditActor));
return IssuanceRequestView.from(request); return IssuanceRequestView.from(request);
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) { } catch (IllegalArgumentException | IllegalStateException | ArithmeticException | NullPointerException exception) {
throw validationError(exception); throw validationError(exception);
} }
} }
@ -159,11 +159,11 @@ public class CommercialBankIssuanceApplicationService {
@Transactional @Transactional
public IssuanceRequestView digest(UUID id, IssuanceAuditActor auditActor) { public IssuanceRequestView digest(UUID id, IssuanceAuditActor auditActor) {
try { try {
IssuanceRequest request = requireOwnedRequest(id, auditActor); IssuanceRequest request = requireOwnedRequestForUpdate(id, auditActor);
request.recordDigest(signatureService.digest(request.getMessageText())); request.recordDigest(signatureService.digest(request.getMessageText()));
repository.save(request, auditActor(auditActor)); repository.save(request, auditActor(auditActor));
return IssuanceRequestView.from(request); return IssuanceRequestView.from(request);
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) { } catch (IllegalArgumentException | IllegalStateException | ArithmeticException | NullPointerException exception) {
throw validationError(exception); throw validationError(exception);
} }
} }
@ -175,7 +175,7 @@ public class CommercialBankIssuanceApplicationService {
@Transactional @Transactional
public IssuanceRequestView sign(UUID id, IssuanceAuditActor auditActor, InstitutionKeySubject subject) { public IssuanceRequestView sign(UUID id, IssuanceAuditActor auditActor, InstitutionKeySubject subject) {
try { try {
IssuanceRequest request = requireOwnedRequest(id, auditActor); IssuanceRequest request = requireOwnedRequestForUpdate(id, auditActor);
if (generationGateway == null) { if (generationGateway == null) {
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "生成数字货币模块密钥服务未配置"); throw new BusinessException(ErrorCode.INTERNAL_ERROR, "生成数字货币模块密钥服务未配置");
} }
@ -185,7 +185,7 @@ public class CommercialBankIssuanceApplicationService {
return IssuanceRequestView.from(request); return IssuanceRequestView.from(request);
} catch (BusinessException exception) { } catch (BusinessException exception) {
throw exception; throw exception;
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) { } catch (IllegalArgumentException | IllegalStateException | ArithmeticException | NullPointerException exception) {
throw validationError(exception); throw validationError(exception);
} }
} }
@ -193,11 +193,11 @@ public class CommercialBankIssuanceApplicationService {
@Transactional @Transactional
public IssuanceRequestView packagePayload(UUID id, IssuanceAuditActor auditActor) { public IssuanceRequestView packagePayload(UUID id, IssuanceAuditActor auditActor) {
try { try {
IssuanceRequest request = requireOwnedRequest(id, auditActor); IssuanceRequest request = requireOwnedRequestForUpdate(id, auditActor);
request.packagePayload(payloadFor(request)); request.packagePayload(payloadFor(request));
repository.save(request, auditActor(auditActor)); repository.save(request, auditActor(auditActor));
return IssuanceRequestView.from(request); return IssuanceRequestView.from(request);
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) { } catch (IllegalArgumentException | IllegalStateException | ArithmeticException | NullPointerException exception) {
throw validationError(exception); throw validationError(exception);
} }
} }
@ -209,13 +209,13 @@ public class CommercialBankIssuanceApplicationService {
@Transactional @Transactional
public IssuanceRequestView send(UUID id, IssuanceAuditActor auditActor) { public IssuanceRequestView send(UUID id, IssuanceAuditActor auditActor) {
try { try {
IssuanceRequest request = requireOwnedRequest(id, auditActor); IssuanceRequest request = requireOwnedRequestForUpdate(id, auditActor);
request.sendToCentralBank(Instant.now()); request.sendToCentralBank(Instant.now());
receiptRepository.saveIfAbsent(new CentralBankIssuanceReceipt(request.getId().value(), request.getRequestNo(), receiptRepository.saveIfAbsent(new CentralBankIssuanceReceipt(request.getId().value(), request.getRequestNo(),
request.getPayloadJson(), request.getCentralBankReceivedAt(), auditActor.getUserId(), auditActor.getUsername())); request.getPayloadJson(), request.getCentralBankReceivedAt(), auditActor.getUserId(), auditActor.getUsername()));
repository.save(request, auditActor(auditActor)); repository.save(request, auditActor(auditActor));
return IssuanceRequestView.from(request); return IssuanceRequestView.from(request);
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) { } catch (IllegalArgumentException | IllegalStateException | ArithmeticException | NullPointerException exception) {
throw validationError(exception); throw validationError(exception);
} }
} }
@ -301,6 +301,19 @@ public class CommercialBankIssuanceApplicationService {
return request; return request;
} }
private IssuanceRequest requireOwnedRequestForUpdate(UUID id, IssuanceAuditActor auditActor) {
if (id == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "发行申请标识不能为空");
}
IssuanceRequest request = repository.findByIdForUpdate(new IssuanceApplicationId(id)).orElseThrow(() ->
new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "发行申请不存在"));
IssuanceAuditActor effectiveActor = auditActor(auditActor);
if (!request.getCreatedByUserId().equals(effectiveActor.getUserId())) {
throw new BusinessException(ErrorCode.UNAUTHORIZED, "无权操作该发行申请");
}
return request;
}
private static IssuanceAuditActor legacyAuditActor(String username) { private static IssuanceAuditActor legacyAuditActor(String username) {
return new IssuanceAuditActor("SYSTEM", username); return new IssuanceAuditActor("SYSTEM", username);
} }

@ -16,5 +16,9 @@ public interface IssuanceRequestRepository {
Optional<IssuanceRequest> findById(IssuanceApplicationId id); Optional<IssuanceRequest> findById(IssuanceApplicationId id);
default Optional<IssuanceRequest> findByIdForUpdate(IssuanceApplicationId id) {
return findById(id);
}
Optional<IssuanceBankInventory> findBuiltInInventory(); Optional<IssuanceBankInventory> findBuiltInInventory();
} }

@ -3,7 +3,11 @@ package com.yau.digitalrmb.issuance.infrastructure.persistence.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yau.digitalrmb.issuance.infrastructure.persistence.entity.IssuanceRequestEntity; import com.yau.digitalrmb.issuance.infrastructure.persistence.entity.IssuanceRequestEntity;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper @Mapper
public interface IssuanceRequestMapper extends BaseMapper<IssuanceRequestEntity> { public interface IssuanceRequestMapper extends BaseMapper<IssuanceRequestEntity> {
@Select("SELECT * FROM issuance_request WHERE id = #{id} AND deleted = FALSE FOR UPDATE")
IssuanceRequestEntity selectByIdForUpdate(@Param("id") String id);
} }

@ -87,8 +87,18 @@ public class MybatisIssuanceRequestRepository implements IssuanceRequestReposito
@Override @Override
public Optional<IssuanceRequest> findById(IssuanceApplicationId id) { public Optional<IssuanceRequest> findById(IssuanceApplicationId id) {
return findById(id, false);
}
@Override
public Optional<IssuanceRequest> findByIdForUpdate(IssuanceApplicationId id) {
return findById(id, true);
}
private Optional<IssuanceRequest> findById(IssuanceApplicationId id, boolean forUpdate) {
String requestId = id.value().toString(); String requestId = id.value().toString();
IssuanceRequestEntity requestEntity = requestMapper.selectById(requestId); IssuanceRequestEntity requestEntity = forUpdate
? requestMapper.selectByIdForUpdate(requestId) : requestMapper.selectById(requestId);
if (requestEntity == null) { if (requestEntity == null) {
return Optional.empty(); return Optional.empty();
} }

@ -3,6 +3,7 @@ package com.yau.digitalrmb.issuance.interfaces.dto;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import javax.validation.Valid; import javax.validation.Valid;
import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Digits;
import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive; import javax.validation.constraints.Positive;
@ -13,7 +14,8 @@ import java.util.List;
public class CreateIssuanceRequest { public class CreateIssuanceRequest {
@NotBlank @Schema(description = "商业银行机构代码", example = "BKCHCNBJ00001") private String bankCode; @NotBlank @Schema(description = "商业银行机构代码", example = "BKCHCNBJ00001") private String bankCode;
@NotBlank @Schema(description = "机构标识", example = "ORG_3A4B5C6D7E8F") private String organizationId; @NotBlank @Schema(description = "机构标识", example = "ORG_3A4B5C6D7E8F") private String organizationId;
@NotNull @Positive @Schema(description = "发行总金额", example = "50000.00") private BigDecimal totalAmount; @NotNull @Positive @Digits(integer = 18, fraction = 2)
@Schema(description = "发行总金额", example = "50000.00") private BigDecimal totalAmount;
@NotBlank @Schema(description = "币种", example = "DC") private String currency; @NotBlank @Schema(description = "币种", example = "DC") private String currency;
@Valid @NotEmpty @Schema(description = "面额明细") private List<DenominationItemRequest> denominations; @Valid @NotEmpty @Schema(description = "面额明细") private List<DenominationItemRequest> denominations;
public String getBankCode(){return bankCode;} public void setBankCode(String v){bankCode=v;} public String getBankCode(){return bankCode;} public void setBankCode(String v){bankCode=v;}

@ -2,13 +2,20 @@ package com.yau.digitalrmb.issuance.interfaces.dto;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import javax.validation.constraints.Digits;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
import javax.validation.constraints.PositiveOrZero;
import java.math.BigDecimal; import java.math.BigDecimal;
@Schema(description = "数字货币面额明细") @Schema(description = "数字货币面额明细")
public class DenominationItemRequest { public class DenominationItemRequest {
@Schema(description = "面额", example = "100") @Schema(description = "面额", example = "100")
@NotNull @Positive @Digits(integer = 18, fraction = 2)
private BigDecimal denomination; private BigDecimal denomination;
@Schema(description = "数量", example = "400") @Schema(description = "数量", example = "400")
@NotNull @PositiveOrZero
private Integer quantity; private Integer quantity;
public BigDecimal getDenomination() { return denomination; } public BigDecimal getDenomination() { return denomination; }
public void setDenomination(BigDecimal denomination) { this.denomination = denomination; } public void setDenomination(BigDecimal denomination) { this.denomination = denomination; }

@ -1,10 +1,17 @@
package com.yau.digitalrmb.issuance.interfaces.dto; package com.yau.digitalrmb.issuance.interfaces.dto;
import javax.validation.Valid;
import javax.validation.constraints.Digits;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; import java.util.List;
public class DenominationPlanRequest { public class DenominationPlanRequest {
@NotNull @Positive @Digits(integer = 18, fraction = 2)
private BigDecimal totalAmount; private BigDecimal totalAmount;
@Valid @NotEmpty
private List<DenominationItemRequest> items; private List<DenominationItemRequest> items;
public BigDecimal getTotalAmount() { return totalAmount; } public BigDecimal getTotalAmount() { return totalAmount; }

@ -76,7 +76,7 @@ public class CommercialBankIssuanceController {
@PostMapping("/denomination-plan/validate") @PostMapping("/denomination-plan/validate")
@Operation(summary = "校验定制面额结构", description = "自动计算小计、总张数和总金额,并返回是否等于申请总额") @Operation(summary = "校验定制面额结构", description = "自动计算小计、总张数和总金额,并返回是否等于申请总额")
public ApiResponse<DenominationPlanView> validateDenominationPlan( public ApiResponse<DenominationPlanView> validateDenominationPlan(
@RequestBody DenominationPlanRequest request) { @Valid @RequestBody DenominationPlanRequest request) {
return ok(denominationPlanService.validate(request.getTotalAmount(), request.getItems())); return ok(denominationPlanService.validate(request.getTotalAmount(), request.getItems()));
} }
@ -155,9 +155,19 @@ public class CommercialBankIssuanceController {
} }
private List<DenominationItem> items(List<DenominationItemRequest> input) { private List<DenominationItem> items(List<DenominationItemRequest> input) {
if (input == null || input.isEmpty()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "面额明细不能为空");
}
List<DenominationItem> result = new ArrayList<>(); List<DenominationItem> result = new ArrayList<>();
for (DenominationItemRequest item : input) { for (DenominationItemRequest item : input) {
if (item == null || item.getDenomination() == null || item.getQuantity() == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "面额和数量不能为空");
}
try {
result.add(new DenominationItem(item.getDenomination(), item.getQuantity())); result.add(new DenominationItem(item.getDenomination(), item.getQuantity()));
} catch (IllegalArgumentException | ArithmeticException exception) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, exception.getMessage());
}
} }
return result; return result;
} }

@ -11,9 +11,12 @@ import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.http.converter.HttpMessageNotReadableException;
import java.util.UUID; import java.util.UUID;
@ -26,6 +29,13 @@ public class GlobalExceptionHandler {
return ApiResponse.failure(ErrorCode.VALIDATION_ERROR, "请求参数校验失败", traceId()); return ApiResponse.failure(ErrorCode.VALIDATION_ERROR, "请求参数校验失败", traceId());
} }
@ExceptionHandler({HttpMessageNotReadableException.class, MethodArgumentTypeMismatchException.class,
MissingServletRequestParameterException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResponse<Void> handleInvalidRequest(Exception exception) {
return ApiResponse.failure(ErrorCode.VALIDATION_ERROR, "请求参数格式或取值无效", traceId());
}
@ExceptionHandler(BusinessException.class) @ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResponse<Void>> handleBusiness(BusinessException exception) { public ResponseEntity<ApiResponse<Void>> handleBusiness(BusinessException exception) {
ApiResponse<Void> response = ApiResponse.failure(exception.getErrorCode(), exception.getMessage(), traceId()); ApiResponse<Void> response = ApiResponse.failure(exception.getErrorCode(), exception.getMessage(), traceId());

Loading…
Cancel
Save