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

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.beans.factory.annotation.Autowired;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
@ -55,6 +57,8 @@ public class ExchangeApplicationService {
@Transactional
public ExchangeOrderView create(CreateExchangeCommand command, ExchangeActor actor) {
if (command == null) throw validation("兑换请求不能为空");
BigDecimal amount = amount(command.getAmount());
ExchangeContext context = resourceRepository.loadContext(actor);
if (!context.getWalletId().equals(command.getWalletId()) || !context.getBankAccountId().equals(command.getBankAccountId())) {
throw validation("钱包或银行卡不属于当前用户,或尚未完成绑定");
@ -63,13 +67,13 @@ public class ExchangeApplicationService {
UUID id = UUID.randomUUID();
String timestamp = TIMESTAMP.format(now);
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;
String signingOriginal = "SIGN|" + requestNo + "|" + command.getAmount().setScale(2).toPlainString()
String signingOriginal = "SIGN|" + requestNo + "|" + amount.toPlainString()
+ "|" + context.getBankCardLast4() + "|" + timestamp;
ExchangeOrder order = ExchangeOrder.create(new ExchangeOrderId(id), requestNo, actor.getUserId(),
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());
resourceRepository.appendStepLog(order, "02", "生成取币请求报文", message, actor);
return ExchangeOrderView.from(order, null);
@ -77,7 +81,7 @@ public class ExchangeApplicationService {
@Transactional
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().ordinal() > ExchangeStatus.MESSAGE_PREPARED.ordinal()) return current(order);
throw validation("当前兑换订单尚不能签名");
@ -90,7 +94,7 @@ public class ExchangeApplicationService {
@Transactional
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().ordinal() > ExchangeStatus.SIGNED.ordinal()) return current(order);
throw validation("请先完成钱包签名");
@ -106,7 +110,7 @@ public class ExchangeApplicationService {
@Transactional
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().ordinal() > ExchangeStatus.BANK_PROCESSED.ordinal()) return current(order);
throw validation("请先完成商业银行验签和扣款");
@ -119,7 +123,7 @@ public class ExchangeApplicationService {
@Transactional
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.COINS_RESERVED) throw validation("请先锁定等额商业银行币串");
List<ReservedCoin> coins = resourceRepository.findReservedCoins(order);
@ -164,9 +168,28 @@ public class ExchangeApplicationService {
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) {
return ExchangeOrderView.from(order, resourceRepository.findReservedCoins(order));
}
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 {
void save(ExchangeOrder order, String operator);
Optional<ExchangeOrder> findById(ExchangeOrderId id);
default Optional<ExchangeOrder> findByIdForUpdate(ExchangeOrderId id) {
return findById(id);
}
}

@ -56,7 +56,17 @@ public class JdbcExchangeOrderRepository implements ExchangeOrderRepository {
@Override
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"))),
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"),

@ -1,6 +1,7 @@
package com.yau.digitalrmb.exchange.interfaces.dto;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.Digits;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
@ -8,7 +9,7 @@ import java.math.BigDecimal;
public class CreateExchangeRequest {
@NotBlank private String walletId;
@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 void setWalletId(String walletId) { this.walletId = walletId; }

@ -97,7 +97,7 @@ public class CommercialBankIssuanceApplicationService {
command.getDenominations(), auditActor(auditActor).getUserId());
repository.save(request, auditActor);
return IssuanceRequestView.from(request);
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) {
} catch (IllegalArgumentException | IllegalStateException | ArithmeticException | NullPointerException exception) {
throw validationError(exception);
}
}
@ -109,11 +109,11 @@ public class CommercialBankIssuanceApplicationService {
@Transactional
public IssuanceRequestView update(UUID id, UpdateIssuanceRequestCommand command, IssuanceAuditActor auditActor) {
try {
IssuanceRequest request = requireOwnedRequest(id, auditActor);
IssuanceRequest request = requireOwnedRequestForUpdate(id, auditActor);
request.updateDraft(command.getTotalAmount(), command.getCurrency(), command.getDenominations());
repository.save(request, auditActor(auditActor));
return IssuanceRequestView.from(request);
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) {
} catch (IllegalArgumentException | IllegalStateException | ArithmeticException | NullPointerException exception) {
throw validationError(exception);
}
}
@ -125,11 +125,11 @@ public class CommercialBankIssuanceApplicationService {
@Transactional
public IssuanceRequestView captureTimestamp(UUID id, IssuanceAuditActor auditActor) {
try {
IssuanceRequest request = requireOwnedRequest(id, auditActor);
IssuanceRequest request = requireOwnedRequestForUpdate(id, auditActor);
request.captureTimestamp(LocalDateTime.now().format(TIMESTAMP_FORMAT));
repository.save(request, auditActor(auditActor));
return IssuanceRequestView.from(request);
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) {
} catch (IllegalArgumentException | IllegalStateException | ArithmeticException | NullPointerException exception) {
throw validationError(exception);
}
}
@ -141,13 +141,13 @@ public class CommercialBankIssuanceApplicationService {
@Transactional
public IssuanceRequestView prepareMessage(UUID id, IssuanceAuditActor auditActor) {
try {
IssuanceRequest request = requireOwnedRequest(id, auditActor);
IssuanceRequest request = requireOwnedRequestForUpdate(id, auditActor);
String message = messageComposer.compose(request.getBankCode(), request.getOrganizationId(),
request.getTotalAmount(), request.getDenominations(), request.getCurrency(), request.getRequestTimestamp());
request.prepareMessage(message);
repository.save(request, auditActor(auditActor));
return IssuanceRequestView.from(request);
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) {
} catch (IllegalArgumentException | IllegalStateException | ArithmeticException | NullPointerException exception) {
throw validationError(exception);
}
}
@ -159,11 +159,11 @@ public class CommercialBankIssuanceApplicationService {
@Transactional
public IssuanceRequestView digest(UUID id, IssuanceAuditActor auditActor) {
try {
IssuanceRequest request = requireOwnedRequest(id, auditActor);
IssuanceRequest request = requireOwnedRequestForUpdate(id, auditActor);
request.recordDigest(signatureService.digest(request.getMessageText()));
repository.save(request, auditActor(auditActor));
return IssuanceRequestView.from(request);
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) {
} catch (IllegalArgumentException | IllegalStateException | ArithmeticException | NullPointerException exception) {
throw validationError(exception);
}
}
@ -175,7 +175,7 @@ public class CommercialBankIssuanceApplicationService {
@Transactional
public IssuanceRequestView sign(UUID id, IssuanceAuditActor auditActor, InstitutionKeySubject subject) {
try {
IssuanceRequest request = requireOwnedRequest(id, auditActor);
IssuanceRequest request = requireOwnedRequestForUpdate(id, auditActor);
if (generationGateway == null) {
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "生成数字货币模块密钥服务未配置");
}
@ -185,7 +185,7 @@ public class CommercialBankIssuanceApplicationService {
return IssuanceRequestView.from(request);
} catch (BusinessException exception) {
throw exception;
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) {
} catch (IllegalArgumentException | IllegalStateException | ArithmeticException | NullPointerException exception) {
throw validationError(exception);
}
}
@ -193,11 +193,11 @@ public class CommercialBankIssuanceApplicationService {
@Transactional
public IssuanceRequestView packagePayload(UUID id, IssuanceAuditActor auditActor) {
try {
IssuanceRequest request = requireOwnedRequest(id, auditActor);
IssuanceRequest request = requireOwnedRequestForUpdate(id, auditActor);
request.packagePayload(payloadFor(request));
repository.save(request, auditActor(auditActor));
return IssuanceRequestView.from(request);
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) {
} catch (IllegalArgumentException | IllegalStateException | ArithmeticException | NullPointerException exception) {
throw validationError(exception);
}
}
@ -209,13 +209,13 @@ public class CommercialBankIssuanceApplicationService {
@Transactional
public IssuanceRequestView send(UUID id, IssuanceAuditActor auditActor) {
try {
IssuanceRequest request = requireOwnedRequest(id, auditActor);
IssuanceRequest request = requireOwnedRequestForUpdate(id, auditActor);
request.sendToCentralBank(Instant.now());
receiptRepository.saveIfAbsent(new CentralBankIssuanceReceipt(request.getId().value(), request.getRequestNo(),
request.getPayloadJson(), request.getCentralBankReceivedAt(), auditActor.getUserId(), auditActor.getUsername()));
repository.save(request, auditActor(auditActor));
return IssuanceRequestView.from(request);
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) {
} catch (IllegalArgumentException | IllegalStateException | ArithmeticException | NullPointerException exception) {
throw validationError(exception);
}
}
@ -301,6 +301,19 @@ public class CommercialBankIssuanceApplicationService {
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) {
return new IssuanceAuditActor("SYSTEM", username);
}

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

@ -3,7 +3,11 @@ package com.yau.digitalrmb.issuance.infrastructure.persistence.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yau.digitalrmb.issuance.infrastructure.persistence.entity.IssuanceRequestEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper
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
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();
IssuanceRequestEntity requestEntity = requestMapper.selectById(requestId);
IssuanceRequestEntity requestEntity = forUpdate
? requestMapper.selectByIdForUpdate(requestId) : requestMapper.selectById(requestId);
if (requestEntity == null) {
return Optional.empty();
}

@ -3,6 +3,7 @@ package com.yau.digitalrmb.issuance.interfaces.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Digits;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
@ -13,7 +14,8 @@ import java.util.List;
public class CreateIssuanceRequest {
@NotBlank @Schema(description = "商业银行机构代码", example = "BKCHCNBJ00001") private String bankCode;
@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;
@Valid @NotEmpty @Schema(description = "面额明细") private List<DenominationItemRequest> denominations;
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 javax.validation.constraints.Digits;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
import javax.validation.constraints.PositiveOrZero;
import java.math.BigDecimal;
@Schema(description = "数字货币面额明细")
public class DenominationItemRequest {
@Schema(description = "面额", example = "100")
@NotNull @Positive @Digits(integer = 18, fraction = 2)
private BigDecimal denomination;
@Schema(description = "数量", example = "400")
@NotNull @PositiveOrZero
private Integer quantity;
public BigDecimal getDenomination() { return denomination; }
public void setDenomination(BigDecimal denomination) { this.denomination = denomination; }

@ -1,10 +1,17 @@
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.util.List;
public class DenominationPlanRequest {
@NotNull @Positive @Digits(integer = 18, fraction = 2)
private BigDecimal totalAmount;
@Valid @NotEmpty
private List<DenominationItemRequest> items;
public BigDecimal getTotalAmount() { return totalAmount; }

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

@ -11,9 +11,12 @@ import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.http.converter.HttpMessageNotReadableException;
import java.util.UUID;
@ -26,6 +29,13 @@ public class GlobalExceptionHandler {
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)
public ResponseEntity<ApiResponse<Void>> handleBusiness(BusinessException exception) {
ApiResponse<Void> response = ApiResponse.failure(exception.getErrorCode(), exception.getMessage(), traceId());

Loading…
Cancel
Save