同步平台业务模块能力
This commit is contained in:
@@ -6,10 +6,10 @@ import com.xspaceagi.pay.application.support.PayOrderBillNotifySupport;
|
||||
import com.xspaceagi.pay.application.support.PayOrderBizNotifySupport;
|
||||
import com.xspaceagi.pay.application.support.PayOrderGatewayQueryMerger;
|
||||
import com.xspaceagi.pay.application.support.PayOrderGatewaySyncFailureSupport;
|
||||
import com.xspaceagi.pay.domain.gateway.PaymentScanGateway;
|
||||
import com.xspaceagi.pay.application.support.PayOrderStatusQuerySupport;
|
||||
import com.xspaceagi.pay.domain.model.PayOrderModel;
|
||||
import com.xspaceagi.pay.domain.repository.PayOrderRepository;
|
||||
import com.xspaceagi.pay.sdk.dto.ScanOrderStatusQueryResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentStatusQueryResponse;
|
||||
import com.xspaceagi.pay.sdk.enums.PaymentStatus;
|
||||
import com.xspaceagi.pay.spec.enums.PayBizNotifyStatus;
|
||||
import com.xspaceagi.pay.spec.exception.PayGatewayClientException;
|
||||
@@ -44,7 +44,7 @@ public class PayOrderGatewayReconcileRunner {
|
||||
public static final int LOOKBACK_DAYS = 7;
|
||||
|
||||
private final PayOrderRepository payOrderRepository;
|
||||
private final PaymentScanGateway paymentScanGateway;
|
||||
private final PayOrderStatusQuerySupport payOrderStatusQuerySupport;
|
||||
private final PayGatewayOutboundExecutor payGatewayOutboundExecutor;
|
||||
private final IBillRpcService billRpcService;
|
||||
|
||||
@@ -116,9 +116,9 @@ public class PayOrderGatewayReconcileRunner {
|
||||
e.getMessage());
|
||||
return false;
|
||||
}
|
||||
ScanOrderStatusQueryResponse last;
|
||||
PaymentStatusQueryResponse last;
|
||||
try {
|
||||
last = paymentScanGateway.queryOrderStatus(outbound, row.getGatewayPaymentOrderNo(), true);
|
||||
last = payOrderStatusQuerySupport.queryOrderStatus(outbound, row, true);
|
||||
} catch (PayGatewayClientException e) {
|
||||
log.warn(
|
||||
"[pay-reconcile] query failed payOrderId={} tenantId={} gatewayNo={} gatewayCode={} gatewayMessage={}",
|
||||
@@ -152,7 +152,7 @@ public class PayOrderGatewayReconcileRunner {
|
||||
}
|
||||
|
||||
private boolean tryNotifyTerminalAndMarkNotified(
|
||||
PayOrderModel row, ScanOrderStatusQueryResponse last, PayBizNotifyStatus expectedCurrent) {
|
||||
PayOrderModel row, PaymentStatusQueryResponse last, PayBizNotifyStatus expectedCurrent) {
|
||||
if (!PayOrderBillNotifySupport.notifyBillPaymentCallback(billRpcService, row, last, false)) {
|
||||
log.warn("[pay-reconcile] bill notify failed payOrderId={}, will retry next tick", row.getId());
|
||||
return false;
|
||||
|
||||
@@ -3,10 +3,10 @@ package com.xspaceagi.pay.application.schedule;
|
||||
import com.xspaceagi.bill.sdk.rpc.IBillRpcService;
|
||||
import com.xspaceagi.pay.application.support.PayOrderBillNotifySupport;
|
||||
import com.xspaceagi.pay.application.support.PayOrderGatewayQueryMerger;
|
||||
import com.xspaceagi.pay.domain.gateway.PaymentScanGateway;
|
||||
import com.xspaceagi.pay.application.support.PayOrderStatusQuerySupport;
|
||||
import com.xspaceagi.pay.domain.model.PayOrderModel;
|
||||
import com.xspaceagi.pay.domain.repository.PayOrderRepository;
|
||||
import com.xspaceagi.pay.sdk.dto.ScanOrderStatusQueryResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentStatusQueryResponse;
|
||||
import com.xspaceagi.pay.spec.enums.PayBizNotifyStatus;
|
||||
import com.xspaceagi.pay.sdk.enums.PaymentStatus;
|
||||
import com.xspaceagi.pay.spec.enums.PayOrderGatewaySyncStatus;
|
||||
@@ -32,19 +32,19 @@ public class PayOrderPollRunner {
|
||||
public static final String TASK_BEAN_ID = "payOrderPollTaskExecuteService";
|
||||
|
||||
private final PayOrderRepository payOrderRepository;
|
||||
private final PaymentScanGateway paymentScanGateway;
|
||||
private final PayOrderStatusQuerySupport payOrderStatusQuerySupport;
|
||||
private final PayGatewayOutboundExecutor payGatewayOutboundExecutor;
|
||||
private final IBillRpcService billRpcService;
|
||||
private final ScheduleTaskApiService scheduleTaskApiService;
|
||||
|
||||
public PayOrderPollRunner(
|
||||
PayOrderRepository payOrderRepository,
|
||||
PaymentScanGateway paymentScanGateway,
|
||||
PayOrderStatusQuerySupport payOrderStatusQuerySupport,
|
||||
PayGatewayOutboundExecutor payGatewayOutboundExecutor,
|
||||
IBillRpcService billRpcService,
|
||||
ScheduleTaskApiService scheduleTaskApiService) {
|
||||
this.payOrderRepository = payOrderRepository;
|
||||
this.paymentScanGateway = paymentScanGateway;
|
||||
this.payOrderStatusQuerySupport = payOrderStatusQuerySupport;
|
||||
this.payGatewayOutboundExecutor = payGatewayOutboundExecutor;
|
||||
this.billRpcService = billRpcService;
|
||||
this.scheduleTaskApiService = scheduleTaskApiService;
|
||||
@@ -98,7 +98,7 @@ public class PayOrderPollRunner {
|
||||
return true;
|
||||
}
|
||||
|
||||
ScanOrderStatusQueryResponse last = null;
|
||||
PaymentStatusQueryResponse last = null;
|
||||
PayGatewayOutbound outbound;
|
||||
try {
|
||||
outbound = payGatewayOutboundExecutor.resolveCached(row.getTenantId());
|
||||
@@ -107,7 +107,7 @@ public class PayOrderPollRunner {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
last = paymentScanGateway.queryOrderStatus(outbound, row.getGatewayPaymentOrderNo(), true);
|
||||
last = payOrderStatusQuerySupport.queryOrderStatus(outbound, row, true);
|
||||
PayOrderGatewayQueryMerger.mergeIntoRow(row, last);
|
||||
payOrderRepository.save(row);
|
||||
|
||||
@@ -136,7 +136,7 @@ public class PayOrderPollRunner {
|
||||
/**
|
||||
* 先通知业务,成功后再 CAS 为 NOTIFIED,避免 NOTIFIED 但业务未收到回调。
|
||||
*/
|
||||
private boolean tryNotifyAndMarkNotified(PayOrderModel row, ScanOrderStatusQueryResponse last, Long payOrderId) {
|
||||
private boolean tryNotifyAndMarkNotified(PayOrderModel row, PaymentStatusQueryResponse last, Long payOrderId) {
|
||||
if (!PayOrderBillNotifySupport.notifyBillPaymentCallback(billRpcService, row, last, false)) {
|
||||
log.warn("[pay-poll] bill notify failed orderId={}, will retry", payOrderId);
|
||||
return false;
|
||||
@@ -153,7 +153,7 @@ public class PayOrderPollRunner {
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean tryNotifyTimeoutAndMark(PayOrderModel row, ScanOrderStatusQueryResponse last, Long payOrderId) {
|
||||
private boolean tryNotifyTimeoutAndMark(PayOrderModel row, PaymentStatusQueryResponse last, Long payOrderId) {
|
||||
if (!PayOrderBillNotifySupport.notifyBillPaymentCallback(billRpcService, row, last, true)) {
|
||||
log.warn("[pay-poll] bill notify timeout failed orderId={}, will retry", payOrderId);
|
||||
return false;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.xspaceagi.pay.application.service;
|
||||
|
||||
import com.xspaceagi.pay.sdk.dto.FileEntryUploadResponse;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
public interface FileEntryApplicationService {
|
||||
|
||||
FileEntryUploadResponse uploadMerchantOnboardingImage(MultipartFile file, String replaceFileKey);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.xspaceagi.pay.application.service.impl;
|
||||
|
||||
import com.xspaceagi.pay.application.service.FileEntryApplicationService;
|
||||
import com.xspaceagi.pay.application.support.PayGatewayOutboundExecutor;
|
||||
import com.xspaceagi.pay.domain.gateway.FileEntryGateway;
|
||||
import com.xspaceagi.pay.sdk.dto.FileEntryUploadResponse;
|
||||
import com.xspaceagi.system.spec.common.RequestContext;
|
||||
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
|
||||
import com.xspaceagi.system.spec.exception.BizException;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.io.IOException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class FileEntryApplicationServiceImpl implements FileEntryApplicationService {
|
||||
|
||||
@Resource
|
||||
private FileEntryGateway payFileEntryGateway;
|
||||
|
||||
@Resource
|
||||
private PayGatewayOutboundExecutor payGatewayOutboundExecutor;
|
||||
|
||||
@Override
|
||||
public FileEntryUploadResponse uploadMerchantOnboardingImage(MultipartFile file, String replaceFileKey) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new IllegalArgumentException("请选择文件");
|
||||
}
|
||||
final byte[] fileBytes;
|
||||
try {
|
||||
fileBytes = file.getBytes();
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("read upload file failed", e);
|
||||
}
|
||||
String filename = file.getOriginalFilename();
|
||||
String contentType = file.getContentType();
|
||||
return payGatewayOutboundExecutor.invoke(
|
||||
resolveTenantId(),
|
||||
outbound -> payFileEntryGateway.uploadMerchantOnboardingImage(
|
||||
outbound, fileBytes, filename, contentType, replaceFileKey));
|
||||
}
|
||||
|
||||
private static long resolveTenantId() {
|
||||
RequestContext<?> ctx = RequestContext.get();
|
||||
if (ctx != null && ctx.getTenantId() != null) {
|
||||
return ctx.getTenantId();
|
||||
}
|
||||
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "无法确定租户:请登录后重试");
|
||||
}
|
||||
}
|
||||
@@ -3,22 +3,37 @@ package com.xspaceagi.pay.application.service.impl;
|
||||
import com.xspaceagi.bill.sdk.rpc.IBillRpcService;
|
||||
import com.xspaceagi.pay.application.schedule.PayOrderPollRunner;
|
||||
import com.xspaceagi.pay.application.support.PayGatewayOutboundExecutor;
|
||||
import com.xspaceagi.pay.application.support.PayOrderExtSupport;
|
||||
import com.xspaceagi.pay.application.support.PayOrderGatewayQueryMerger;
|
||||
import com.xspaceagi.pay.application.support.PayOrderGatewaySyncFailureSupport;
|
||||
import com.xspaceagi.pay.application.support.PayOrderSettlementSyncService;
|
||||
import com.xspaceagi.pay.application.support.PayOrderStatusQuerySupport;
|
||||
import com.xspaceagi.pay.spec.PayOrderScheduleConstants;
|
||||
import com.xspaceagi.pay.domain.gateway.PaymentAppGateway;
|
||||
import com.xspaceagi.pay.domain.gateway.PaymentH5Gateway;
|
||||
import com.xspaceagi.pay.domain.gateway.PaymentMiniPayGateway;
|
||||
import com.xspaceagi.pay.domain.gateway.PaymentScanGateway;
|
||||
import com.xspaceagi.pay.domain.model.PayOrderModel;
|
||||
import com.xspaceagi.pay.domain.repository.PayOrderRepository;
|
||||
import com.xspaceagi.pay.sdk.dto.AppOrderRpcCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.AppTransactionRpcCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.CashierSessionCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.CashierSessionCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentOrderCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.H5OrderRpcCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.H5TransactionRpcCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.MiniPayOrderRpcCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.MiniPayTransactionRpcCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.OrderAndTransactionCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentStatusQueryRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.ScanOrderCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.ScanOrderStatusQueryResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentStatusQueryResponse;
|
||||
import com.xspaceagi.pay.sdk.service.IPaymentRpcService;
|
||||
import com.xspaceagi.pay.spec.enums.PayBizNotifyStatus;
|
||||
import com.xspaceagi.pay.sdk.dto.OrderCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.enums.PayChannel;
|
||||
import com.xspaceagi.pay.sdk.enums.PayClientScene;
|
||||
import com.xspaceagi.pay.sdk.enums.PayMode;
|
||||
import com.xspaceagi.pay.sdk.enums.PaymentStatus;
|
||||
import com.xspaceagi.pay.spec.enums.PayOrderGatewaySyncStatus;
|
||||
import com.xspaceagi.pay.spec.exception.PayGatewayClientException;
|
||||
import com.xspaceagi.system.spec.common.RequestContext;
|
||||
@@ -30,7 +45,9 @@ import jakarta.annotation.Resource;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
|
||||
@@ -43,6 +60,18 @@ public class PaymentRpcServiceImpl implements IPaymentRpcService {
|
||||
@Resource
|
||||
private PaymentScanGateway paymentScanGateway;
|
||||
|
||||
@Resource
|
||||
private PaymentMiniPayGateway paymentMiniPayGateway;
|
||||
|
||||
@Resource
|
||||
private PaymentH5Gateway paymentH5Gateway;
|
||||
|
||||
@Resource
|
||||
private PaymentAppGateway paymentAppGateway;
|
||||
|
||||
@Resource
|
||||
private PayOrderStatusQuerySupport payOrderStatusQuerySupport;
|
||||
|
||||
@Resource
|
||||
private PayGatewayOutboundExecutor payGatewayOutboundExecutor;
|
||||
|
||||
@@ -55,52 +84,189 @@ public class PaymentRpcServiceImpl implements IPaymentRpcService {
|
||||
@Resource
|
||||
private PayOrderSettlementSyncService payOrderSettlementSyncService;
|
||||
|
||||
public PaymentOrderCreateResponse createOrderForScan(ScanOrderCreateRequest request) {
|
||||
Assert.hasText(request.getBizOrderNo(), "bizOrderNo cannot be left blank");
|
||||
Assert.notNull(request.getOrderAmount(), "orderAmount cannot be left blank");
|
||||
Assert.isTrue(request.getOrderAmount() > 0, "orderAmount must be greater than 0");
|
||||
Assert.hasText(request.getSubject(), "subject cannot be left blank");
|
||||
|
||||
long tenantId = resolveTenantId();
|
||||
PayOrderModel existing =
|
||||
payOrderRepository.findByTenantIdAndBizOrderNo(tenantId, request.getBizOrderNo()).orElse(null);
|
||||
if (existing != null) {
|
||||
return resumeExistingOrder(existing);
|
||||
}
|
||||
|
||||
PayOrderModel row = PayOrderModel.builder()
|
||||
.tenantId(tenantId)
|
||||
.bizOrderNo(request.getBizOrderNo())
|
||||
.bizScene(null)
|
||||
.orderAmount(request.getOrderAmount())
|
||||
.subject(request.getSubject())
|
||||
.ext(request.getExt())
|
||||
.payMode(PayMode.scan)
|
||||
.platformFee(0L)
|
||||
.providerFee(0L)
|
||||
.netAmount(0L)
|
||||
.gatewaySyncStatus(PayOrderGatewaySyncStatus.PENDING)
|
||||
.build();
|
||||
try {
|
||||
row = payOrderRepository.save(row);
|
||||
} catch (DuplicateKeyException e) {
|
||||
PayOrderModel race = payOrderRepository.findByTenantIdAndBizOrderNo(tenantId, request.getBizOrderNo()).orElse(null);
|
||||
if (race == null) {
|
||||
throw new BizException(ErrorCodeEnum.SYS_ERROR.getCode(), "创建订单冲突,请重试");
|
||||
}
|
||||
return reconcileAfterDuplicate(race);
|
||||
}
|
||||
return callGatewayAndFinish(row);
|
||||
@Override
|
||||
public OrderCreateResponse createOrderForScan(ScanOrderCreateRequest request) {
|
||||
return createPayOrder(
|
||||
PayMode.scan,
|
||||
request.getBizOrderNo(),
|
||||
request.getOrderAmount(),
|
||||
request.getSubject(),
|
||||
request.getExt());
|
||||
}
|
||||
|
||||
@Override
|
||||
public OrderCreateResponse createOrderForMiniPay(MiniPayOrderRpcCreateRequest request) {
|
||||
return createPayOrder(
|
||||
PayMode.minipay,
|
||||
request.getBizOrderNo(),
|
||||
request.getOrderAmount(),
|
||||
request.getSubject(),
|
||||
request.getExt());
|
||||
}
|
||||
|
||||
@Override
|
||||
public OrderCreateResponse createOrderForH5(H5OrderRpcCreateRequest request) {
|
||||
return createPayOrder(
|
||||
PayMode.h5,
|
||||
request.getBizOrderNo(),
|
||||
request.getOrderAmount(),
|
||||
request.getSubject(),
|
||||
request.getExt());
|
||||
}
|
||||
|
||||
@Override
|
||||
public OrderCreateResponse createOrderForApp(AppOrderRpcCreateRequest request) {
|
||||
return createPayOrder(
|
||||
PayMode.app,
|
||||
request.getBizOrderNo(),
|
||||
request.getOrderAmount(),
|
||||
request.getSubject(),
|
||||
request.getExt());
|
||||
}
|
||||
|
||||
@Override
|
||||
public OrderAndTransactionCreateResponse createMiniPayTransaction(MiniPayTransactionRpcCreateRequest request) {
|
||||
Assert.hasText(request.getBizOrderNo(), "bizOrderNo cannot be left blank");
|
||||
Assert.notNull(request.getPayChannel(), "payChannel cannot be null");
|
||||
validateMiniPayChannelParams(request);
|
||||
|
||||
long tenantId = resolveTenantId();
|
||||
PayOrderModel row = payOrderRepository
|
||||
.findByTenantIdAndBizOrderNo(tenantId, request.getBizOrderNo().trim())
|
||||
.orElse(null);
|
||||
if (row == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.pay_order_not_found);
|
||||
}
|
||||
if (row.getPayMode() != PayMode.minipay) {
|
||||
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "非小程序支付单");
|
||||
}
|
||||
if (row.getGatewaySyncStatus() != PayOrderGatewaySyncStatus.SUCCESS) {
|
||||
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "支付单尚未同步网关,无法调起支付");
|
||||
}
|
||||
|
||||
try {
|
||||
OrderAndTransactionCreateResponse resp = payGatewayOutboundExecutor.invoke(
|
||||
tenantId,
|
||||
outbound -> paymentMiniPayGateway.createTransaction(
|
||||
outbound,
|
||||
row.getGatewayPaymentOrderNo(),
|
||||
request.getPayChannel(),
|
||||
request.getSubAppid(),
|
||||
request.getOpenId(),
|
||||
request.getBuyerId(),
|
||||
request.getClientIp(),
|
||||
request.getPayClientScene()));
|
||||
mergeTransactionResponseIntoRow(row, resp);
|
||||
if (request.getPayClientScene() != null) {
|
||||
row.setExt(PayOrderExtSupport.mergePayClientScene(row.getExt(), request.getPayClientScene()));
|
||||
}
|
||||
if (row.getBizNotifyStatus() != PayBizNotifyStatus.POLLING) {
|
||||
row.setBizNotifyStatus(PayBizNotifyStatus.POLLING);
|
||||
}
|
||||
payOrderRepository.save(row);
|
||||
// 重试调起时若调度任务已结束,同 taskId 的 start 会将其拉回 CREATE 并继续轮询
|
||||
payOrderPayPollRunner.startPoll(row.getId(), row.getTenantId());
|
||||
return resp;
|
||||
} catch (PayGatewayClientException e) {
|
||||
throw PayGatewayExceptionMapper.toBizException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public OrderAndTransactionCreateResponse createH5Transaction(H5TransactionRpcCreateRequest request) {
|
||||
Assert.hasText(request.getBizOrderNo(), "bizOrderNo cannot be left blank");
|
||||
Assert.notNull(request.getPayChannel(), "payChannel cannot be null");
|
||||
Assert.hasText(request.getFrontNotifyUrl(), "frontNotifyUrl cannot be left blank");
|
||||
String frontNotifyUrl = validateFrontNotifyUrl(request.getFrontNotifyUrl().trim());
|
||||
|
||||
long tenantId = resolveTenantId();
|
||||
PayOrderModel row = payOrderRepository
|
||||
.findByTenantIdAndBizOrderNo(tenantId, request.getBizOrderNo().trim())
|
||||
.orElse(null);
|
||||
if (row == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.pay_order_not_found);
|
||||
}
|
||||
if (row.getPayMode() != PayMode.h5) {
|
||||
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "非H5支付单");
|
||||
}
|
||||
if (row.getGatewaySyncStatus() != PayOrderGatewaySyncStatus.SUCCESS) {
|
||||
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "支付单尚未同步网关,无法调起支付");
|
||||
}
|
||||
|
||||
try {
|
||||
OrderAndTransactionCreateResponse resp = payGatewayOutboundExecutor.invoke(
|
||||
tenantId,
|
||||
outbound -> paymentH5Gateway.createTransaction(
|
||||
outbound,
|
||||
row.getGatewayPaymentOrderNo(),
|
||||
request.getPayChannel(),
|
||||
request.getClientIp(),
|
||||
frontNotifyUrl));
|
||||
mergeTransactionResponseIntoRow(row, resp);
|
||||
row.setExt(PayOrderExtSupport.mergePayClientScene(row.getExt(), PayClientScene.H5_WEB));
|
||||
if (row.getBizNotifyStatus() != PayBizNotifyStatus.POLLING) {
|
||||
row.setBizNotifyStatus(PayBizNotifyStatus.POLLING);
|
||||
}
|
||||
payOrderRepository.save(row);
|
||||
payOrderPayPollRunner.startPoll(row.getId(), row.getTenantId());
|
||||
return resp;
|
||||
} catch (PayGatewayClientException e) {
|
||||
throw PayGatewayExceptionMapper.toBizException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public OrderAndTransactionCreateResponse createAppTransaction(AppTransactionRpcCreateRequest request) {
|
||||
Assert.hasText(request.getBizOrderNo(), "bizOrderNo cannot be left blank");
|
||||
Assert.notNull(request.getPayChannel(), "payChannel cannot be null");
|
||||
validateAppPayChannel(request.getPayChannel());
|
||||
|
||||
long tenantId = resolveTenantId();
|
||||
PayOrderModel row = payOrderRepository
|
||||
.findByTenantIdAndBizOrderNo(tenantId, request.getBizOrderNo().trim())
|
||||
.orElse(null);
|
||||
if (row == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.pay_order_not_found);
|
||||
}
|
||||
if (row.getPayMode() != PayMode.app) {
|
||||
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "非 App 原生支付单");
|
||||
}
|
||||
if (row.getGatewaySyncStatus() != PayOrderGatewaySyncStatus.SUCCESS) {
|
||||
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "支付单尚未同步网关,无法调起支付");
|
||||
}
|
||||
|
||||
try {
|
||||
OrderAndTransactionCreateResponse resp = payGatewayOutboundExecutor.invoke(
|
||||
tenantId,
|
||||
outbound -> paymentAppGateway.createTransaction(
|
||||
outbound,
|
||||
row.getGatewayPaymentOrderNo(),
|
||||
request.getPayChannel(),
|
||||
request.getClientIp(),
|
||||
request.getPayClientScene() != null
|
||||
? request.getPayClientScene()
|
||||
: PayClientScene.NATIVE_APP));
|
||||
mergeTransactionResponseIntoRow(row, resp);
|
||||
row.setExt(PayOrderExtSupport.mergePayClientScene(row.getExt(), PayClientScene.NATIVE_APP));
|
||||
if (row.getBizNotifyStatus() != PayBizNotifyStatus.POLLING) {
|
||||
row.setBizNotifyStatus(PayBizNotifyStatus.POLLING);
|
||||
}
|
||||
payOrderRepository.save(row);
|
||||
payOrderPayPollRunner.startPoll(row.getId(), row.getTenantId());
|
||||
return resp;
|
||||
} catch (PayGatewayClientException e) {
|
||||
throw PayGatewayExceptionMapper.toBizException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CashierSessionCreateResponse createCashierSession(CashierSessionCreateRequest request) {
|
||||
Assert.notNull(request, "request cannot be null");
|
||||
Assert.hasText(request.getGatewayPaymentOrderNo(), "gatewayPaymentOrderNo cannot be left blank");
|
||||
|
||||
long tenantId = resolveTenantId();
|
||||
PayOrderModel row;
|
||||
String gatewayNo = request.getGatewayPaymentOrderNo().trim();
|
||||
row = payOrderRepository.findByTenantIdAndGatewayPaymentOrderNo(tenantId, gatewayNo).orElse(null);
|
||||
PayOrderModel row = payOrderRepository.findByTenantIdAndGatewayPaymentOrderNo(tenantId, gatewayNo).orElse(null);
|
||||
if (row == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.pay_order_not_found);
|
||||
}
|
||||
@@ -117,7 +283,8 @@ public class PaymentRpcServiceImpl implements IPaymentRpcService {
|
||||
request.getBizRedirectUrl()));
|
||||
}
|
||||
|
||||
public ScanOrderStatusQueryResponse queryStatus(PaymentStatusQueryRequest request) {
|
||||
@Override
|
||||
public PaymentStatusQueryResponse queryStatus(PaymentStatusQueryRequest request) {
|
||||
Assert.hasText(request.getGatewayPaymentOrderNo(), "gatewayPaymentOrderNo cannot be left blank");
|
||||
|
||||
long tenantId = resolveTenantId();
|
||||
@@ -127,9 +294,9 @@ public class PaymentRpcServiceImpl implements IPaymentRpcService {
|
||||
if (row == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.pay_order_not_found);
|
||||
}
|
||||
ScanOrderStatusQueryResponse resp = payGatewayOutboundExecutor.invoke(
|
||||
PaymentStatusQueryResponse resp = payGatewayOutboundExecutor.invoke(
|
||||
tenantId,
|
||||
outbound -> paymentScanGateway.queryOrderStatus(outbound, request.getGatewayPaymentOrderNo(), true));
|
||||
outbound -> payOrderStatusQuerySupport.queryOrderStatus(outbound, row, true));
|
||||
PayOrderGatewayQueryMerger.mergeIntoRow(row, resp);
|
||||
payOrderRepository.save(row);
|
||||
return resp;
|
||||
@@ -141,36 +308,101 @@ public class PaymentRpcServiceImpl implements IPaymentRpcService {
|
||||
return payOrderSettlementSyncService.syncSettlementForBizOrderNo(tenantId, bizOrderNo);
|
||||
}
|
||||
|
||||
private PaymentOrderCreateResponse reconcileAfterDuplicate(PayOrderModel race) {
|
||||
return resumeExistingOrder(race);
|
||||
private OrderCreateResponse createPayOrder(
|
||||
PayMode payMode, String bizOrderNo, Long orderAmount, String subject, String ext) {
|
||||
Assert.hasText(bizOrderNo, "bizOrderNo cannot be left blank");
|
||||
Assert.notNull(orderAmount, "orderAmount cannot be left blank");
|
||||
Assert.isTrue(orderAmount > 0, "orderAmount must be greater than 0");
|
||||
Assert.hasText(subject, "subject cannot be left blank");
|
||||
|
||||
long tenantId = resolveTenantId();
|
||||
PayOrderModel existing =
|
||||
payOrderRepository.findByTenantIdAndBizOrderNo(tenantId, bizOrderNo).orElse(null);
|
||||
if (existing != null) {
|
||||
if (existing.getPayMode() != null && existing.getPayMode() != payMode) {
|
||||
return recreateForSwitchedPayMode(existing, payMode, orderAmount, subject, ext);
|
||||
}
|
||||
return resumeExistingOrder(existing, payMode);
|
||||
}
|
||||
|
||||
PayOrderModel row = PayOrderModel.builder()
|
||||
.tenantId(tenantId)
|
||||
.bizOrderNo(bizOrderNo)
|
||||
.bizScene(null)
|
||||
.orderAmount(orderAmount)
|
||||
.subject(subject)
|
||||
.ext(ext)
|
||||
.payMode(payMode)
|
||||
.platformFee(0L)
|
||||
.providerFee(0L)
|
||||
.netAmount(0L)
|
||||
.gatewaySyncStatus(PayOrderGatewaySyncStatus.PENDING)
|
||||
.build();
|
||||
try {
|
||||
row = payOrderRepository.save(row);
|
||||
} catch (DuplicateKeyException e) {
|
||||
PayOrderModel race = payOrderRepository.findByTenantIdAndBizOrderNo(tenantId, bizOrderNo).orElse(null);
|
||||
if (race == null) {
|
||||
throw new BizException(ErrorCodeEnum.SYS_ERROR.getCode(), "创建订单冲突,请重试");
|
||||
}
|
||||
assertPayMode(race, payMode);
|
||||
return resumeExistingOrder(race, payMode);
|
||||
}
|
||||
return callGatewayAndFinish(row, payMode);
|
||||
}
|
||||
|
||||
private static void assertPayMode(PayOrderModel row, PayMode expectedPayMode) {
|
||||
if (row.getPayMode() == null || row.getPayMode() != expectedPayMode) {
|
||||
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "订单支付方式不匹配");
|
||||
}
|
||||
}
|
||||
|
||||
private OrderCreateResponse recreateForSwitchedPayMode(
|
||||
PayOrderModel row, PayMode targetPayMode, long orderAmount, String subject, String ext) {
|
||||
row.setPayMode(targetPayMode);
|
||||
row.setOrderAmount(orderAmount);
|
||||
row.setSubject(subject);
|
||||
row.setExt(ext);
|
||||
row.setPayChannel(null);
|
||||
row.setPlatformFee(0L);
|
||||
row.setProviderFee(0L);
|
||||
row.setNetAmount(0L);
|
||||
row.setGatewayPaymentOrderNo(null);
|
||||
row.setGatewayOrderStatus(null);
|
||||
row.setGatewayLastError(null);
|
||||
row.setPaidAt(null);
|
||||
row.setGatewaySyncStatus(PayOrderGatewaySyncStatus.PENDING);
|
||||
row.setBizNotifyStatus(null);
|
||||
payOrderRepository.save(row);
|
||||
return callGatewayAndFinish(row, targetPayMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 已存在行的续跑逻辑。FAILED→PENDING 使用 CAS(仅一行匹配 FAILED 才更新),降低多实例同时重试时对网关的重复调用。
|
||||
* 网关已 SUCCESS 时幂等返回已有单号(minipay 可能尚未调起渠道)。
|
||||
*/
|
||||
private PaymentOrderCreateResponse resumeExistingOrder(PayOrderModel row) {
|
||||
private OrderCreateResponse resumeExistingOrder(PayOrderModel row, PayMode payMode) {
|
||||
if (PayOrderGatewaySyncStatus.SUCCESS == row.getGatewaySyncStatus()) {
|
||||
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "订单已存在");
|
||||
return toOrderCreateResponse(row);
|
||||
}
|
||||
if (PayOrderGatewaySyncStatus.PENDING == row.getGatewaySyncStatus()) {
|
||||
if (!isStalePending(row)) {
|
||||
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "订单处理中,请稍后再试");
|
||||
}
|
||||
// 长时间pending,可能是因为即将调用网关时发生了重启,没有调下游,允许重新调用,依赖下游幂等
|
||||
return callGatewayAndFinish(row);
|
||||
return callGatewayAndFinish(row, payMode);
|
||||
}
|
||||
if (PayOrderGatewaySyncStatus.FAILED == row.getGatewaySyncStatus()) {
|
||||
return casRetryFailedToPendingThenCallGateway(row);
|
||||
return casRetryFailedToPendingThenCallGateway(row, payMode);
|
||||
}
|
||||
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "订单状态异常,无法创建");
|
||||
}
|
||||
|
||||
private PaymentOrderCreateResponse casRetryFailedToPendingThenCallGateway(PayOrderModel row) {
|
||||
private OrderCreateResponse casRetryFailedToPendingThenCallGateway(PayOrderModel row, PayMode payMode) {
|
||||
int n = payOrderRepository.casUpdateGatewaySyncFromFailedToPending(row.getId(), row.getTenantId(), row.getBizOrderNo());
|
||||
if (n == 1) {
|
||||
row.setGatewaySyncStatus(PayOrderGatewaySyncStatus.PENDING);
|
||||
row.setGatewayLastError(null);
|
||||
return callGatewayAndFinish(row);
|
||||
return callGatewayAndFinish(row, payMode);
|
||||
}
|
||||
PayOrderModel fresh = payOrderRepository
|
||||
.findByTenantIdAndBizOrderNo(row.getTenantId(), row.getBizOrderNo())
|
||||
@@ -178,7 +410,25 @@ public class PaymentRpcServiceImpl implements IPaymentRpcService {
|
||||
if (fresh == null) {
|
||||
throw new BizException(ErrorCodeEnum.SYS_ERROR.getCode(), "订单冲突,请重试");
|
||||
}
|
||||
return resumeExistingOrder(fresh);
|
||||
assertPayMode(fresh, payMode);
|
||||
return resumeExistingOrder(fresh, payMode);
|
||||
}
|
||||
|
||||
private static OrderCreateResponse toOrderCreateResponse(PayOrderModel row) {
|
||||
PaymentStatus status = null;
|
||||
if (StringUtils.hasText(row.getGatewayOrderStatus())) {
|
||||
try {
|
||||
status = PaymentStatus.valueOf(row.getGatewayOrderStatus());
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
// ignore unmapped gateway status string
|
||||
}
|
||||
}
|
||||
return OrderCreateResponse.builder()
|
||||
.gatewayPaymentOrderNo(row.getGatewayPaymentOrderNo())
|
||||
.bizOrderNo(row.getBizOrderNo())
|
||||
.status(status)
|
||||
.payMode(row.getPayMode())
|
||||
.build();
|
||||
}
|
||||
|
||||
private static boolean isStalePending(PayOrderModel row) {
|
||||
@@ -189,22 +439,48 @@ public class PaymentRpcServiceImpl implements IPaymentRpcService {
|
||||
return anchor.toInstant().plus(PayOrderScheduleConstants.STALE_GATEWAY_SYNC_PENDING).isBefore(Instant.now());
|
||||
}
|
||||
|
||||
private PaymentOrderCreateResponse callGatewayAndFinish(PayOrderModel row) {
|
||||
private OrderCreateResponse callGatewayAndFinish(PayOrderModel row, PayMode payMode) {
|
||||
try {
|
||||
PaymentOrderCreateResponse resp =
|
||||
paymentScanGateway.createOrder(
|
||||
payGatewayOutboundExecutor.resolve(row.getTenantId()),
|
||||
row.getBizOrderNo(),
|
||||
row.getOrderAmount(),
|
||||
row.getSubject(),
|
||||
row.getExt());
|
||||
OrderCreateResponse resp;
|
||||
if (payMode == PayMode.minipay) {
|
||||
resp = paymentMiniPayGateway.createOrder(
|
||||
payGatewayOutboundExecutor.resolve(row.getTenantId()),
|
||||
row.getBizOrderNo(),
|
||||
row.getOrderAmount(),
|
||||
row.getSubject(),
|
||||
row.getExt());
|
||||
} else if (payMode == PayMode.h5) {
|
||||
resp = paymentH5Gateway.createOrder(
|
||||
payGatewayOutboundExecutor.resolve(row.getTenantId()),
|
||||
row.getBizOrderNo(),
|
||||
row.getOrderAmount(),
|
||||
row.getSubject(),
|
||||
row.getExt());
|
||||
} else if (payMode == PayMode.app) {
|
||||
resp = paymentAppGateway.createOrder(
|
||||
payGatewayOutboundExecutor.resolve(row.getTenantId()),
|
||||
row.getBizOrderNo(),
|
||||
row.getOrderAmount(),
|
||||
row.getSubject(),
|
||||
row.getExt());
|
||||
} else {
|
||||
resp = paymentScanGateway.createOrder(
|
||||
payGatewayOutboundExecutor.resolve(row.getTenantId()),
|
||||
row.getBizOrderNo(),
|
||||
row.getOrderAmount(),
|
||||
row.getSubject(),
|
||||
row.getExt());
|
||||
}
|
||||
row.setGatewayPaymentOrderNo(resp.getGatewayPaymentOrderNo());
|
||||
row.setGatewayOrderStatus(resp.getStatus() != null ? resp.getStatus().name() : null);
|
||||
row.setGatewaySyncStatus(PayOrderGatewaySyncStatus.SUCCESS);
|
||||
row.setGatewayLastError(null);
|
||||
row.setBizNotifyStatus(PayBizNotifyStatus.POLLING);
|
||||
payOrderRepository.save(row);
|
||||
payOrderPayPollRunner.startPoll(row.getId(), row.getTenantId());
|
||||
if (payMode == PayMode.scan) {
|
||||
row.setBizNotifyStatus(PayBizNotifyStatus.POLLING);
|
||||
payOrderRepository.save(row);
|
||||
payOrderPayPollRunner.startPoll(row.getId(), row.getTenantId());
|
||||
}
|
||||
return resp;
|
||||
} catch (PayGatewayClientException e) {
|
||||
PayOrderGatewaySyncFailureSupport.finishGatewayCreateFailed(
|
||||
@@ -213,6 +489,43 @@ public class PaymentRpcServiceImpl implements IPaymentRpcService {
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateMiniPayChannelParams(MiniPayTransactionRpcCreateRequest request) {
|
||||
if (request.getPayChannel() == PayChannel.WxPay) {
|
||||
Assert.hasText(request.getSubAppid(), "subAppid cannot be left blank for WxPay");
|
||||
Assert.hasText(request.getOpenId(), "openId cannot be left blank for WxPay");
|
||||
return;
|
||||
}
|
||||
if (request.getPayChannel() == PayChannel.AliPay) {
|
||||
Assert.hasText(request.getBuyerId(), "buyerId cannot be left blank for AliPay");
|
||||
return;
|
||||
}
|
||||
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "小程序支付仅支持 WxPay 与 AliPay");
|
||||
}
|
||||
|
||||
private static void validateAppPayChannel(PayChannel payChannel) {
|
||||
if (payChannel != PayChannel.WxPay && payChannel != PayChannel.AliPay) {
|
||||
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "App 原生支付仅支持 WxPay 与 AliPay");
|
||||
}
|
||||
}
|
||||
|
||||
private static void mergeTransactionResponseIntoRow(PayOrderModel row, OrderAndTransactionCreateResponse resp) {
|
||||
if (resp.getPlatformFee() != null) {
|
||||
row.setPlatformFee(resp.getPlatformFee());
|
||||
}
|
||||
if (resp.getProviderFee() != null) {
|
||||
row.setProviderFee(resp.getProviderFee());
|
||||
}
|
||||
if (resp.getNetAmount() != null) {
|
||||
row.setNetAmount(resp.getNetAmount());
|
||||
}
|
||||
if (resp.getPayChannel() != null) {
|
||||
row.setPayChannel(resp.getPayChannel());
|
||||
}
|
||||
if (resp.getStatus() != null) {
|
||||
row.setGatewayOrderStatus(resp.getStatus().name());
|
||||
}
|
||||
}
|
||||
|
||||
private static long resolveTenantId() {
|
||||
Long tenantId = RequestContext.get() != null ? RequestContext.get().getTenantId() : null;
|
||||
if (tenantId == null) {
|
||||
@@ -227,4 +540,21 @@ public class PaymentRpcServiceImpl implements IPaymentRpcService {
|
||||
}
|
||||
return s.length() > max ? s.substring(0, max) : s;
|
||||
}
|
||||
|
||||
private static String validateFrontNotifyUrl(String url) {
|
||||
try {
|
||||
URI uri = URI.create(url);
|
||||
String scheme = uri.getScheme();
|
||||
if (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme)) {
|
||||
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "frontNotifyUrl must start with http or https");
|
||||
}
|
||||
String host = uri.getHost();
|
||||
if (host == null || host.isBlank()) {
|
||||
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "Invalid frontNotifyUrl");
|
||||
}
|
||||
return url;
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "Invalid frontNotifyUrl");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
package com.xspaceagi.pay.application.support;
|
||||
|
||||
import com.xspaceagi.bill.sdk.rpc.IBillRpcService;
|
||||
import com.xspaceagi.bill.sdk.dto.AddRevenueRequest;
|
||||
import com.xspaceagi.bill.spec.enums.PayStatusEnum;
|
||||
import com.xspaceagi.bill.spec.enums.RevenueTargetTypeEnum;
|
||||
import com.xspaceagi.bill.spec.enums.RevenueTypeEnum;
|
||||
import com.xspaceagi.pay.domain.model.PayOrderModel;
|
||||
import com.xspaceagi.pay.sdk.dto.ScanOrderStatusQueryResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentStatusQueryResponse;
|
||||
import com.xspaceagi.pay.sdk.enums.PaymentStatus;
|
||||
import com.xspaceagi.pay.sdk.support.PayOrderExtKeys;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/** 支付结果通知 Bill(进程内 RPC)。 */
|
||||
@Slf4j
|
||||
public final class PayOrderBillNotifySupport {
|
||||
@@ -44,9 +53,13 @@ public final class PayOrderBillNotifySupport {
|
||||
* @return true 表示业务通知已成功(或无 Bill 回调目标)
|
||||
*/
|
||||
public static boolean notifyBillPaymentCallback(
|
||||
IBillRpcService billRpcService, PayOrderModel row, ScanOrderStatusQueryResponse gatewayResp, boolean pollTimeout) {
|
||||
IBillRpcService billRpcService, PayOrderModel row, PaymentStatusQueryResponse gatewayResp, boolean pollTimeout) {
|
||||
Long orderId = parseNumericBizOrderNo(row.getBizOrderNo());
|
||||
if (orderId == null) {
|
||||
// 通用项目订单(GP1_/GP2_)— 无 Bill 回调目标;若已支付则计入开发者收益
|
||||
if (gatewayResp != null && gatewayResp.getStatus() == PaymentStatus.PAID) {
|
||||
return addGeneralProjectRevenue(billRpcService, row);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
PaymentStatus st = gatewayResp != null ? gatewayResp.getStatus() : null;
|
||||
@@ -66,6 +79,87 @@ public final class PayOrderBillNotifySupport {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用项目支付成功 → 计入开发者(项目创建者)收益。
|
||||
* 从 ext 解析 generalProjectUserId 和 generalProjectId。
|
||||
* userId 为 null 时跳过(不阻断通知流程);addRevenue 幂等,异常返回 false 触发重试。
|
||||
*
|
||||
* @return true 表示收益已记录或无需记录;false 表示记录失败应重试
|
||||
*/
|
||||
private static boolean addGeneralProjectRevenue(IBillRpcService billRpcService, PayOrderModel row) {
|
||||
Map<String, Object> extMap = PayOrderExtSupport.parseExt(row.getExt());
|
||||
Long userId = parseLongExt(extMap.get(PayOrderExtKeys.GENERAL_PROJECT_USER_ID));
|
||||
Long projectId = parseLongExt(extMap.get(PayOrderExtKeys.GENERAL_PROJECT_ID));
|
||||
if (userId == null) {
|
||||
log.warn(
|
||||
"[pay-notify] general-project revenue skipped (no userId) payOrderId={} tenantId={}",
|
||||
row.getId(),
|
||||
row.getTenantId());
|
||||
return true;
|
||||
}
|
||||
AddRevenueRequest req = new AddRevenueRequest();
|
||||
req.setTenantId(row.getTenantId());
|
||||
req.setUserId(userId);
|
||||
req.setBizNo(row.getBizOrderNo());
|
||||
req.setType(RevenueTypeEnum.PROJECT_PAY);
|
||||
req.setTargetType(RevenueTargetTypeEnum.PROJECT);
|
||||
if (projectId != null) {
|
||||
req.setTargetId(projectId);
|
||||
}
|
||||
req.setRemark(row.getSubject());
|
||||
Map<String, Object> extra = new HashMap<>();
|
||||
extra.put("payOrderId", row.getId());
|
||||
if (row.getBizOrderNo() != null) {
|
||||
extra.put("bizOrderNo", row.getBizOrderNo());
|
||||
}
|
||||
if (row.getGatewayPaymentOrderNo() != null) {
|
||||
extra.put("gatewayPaymentOrderNo", row.getGatewayPaymentOrderNo());
|
||||
}
|
||||
req.setExtra(extra);
|
||||
try {
|
||||
if (row.getOrderAmount() == null) {
|
||||
log.warn(
|
||||
"[pay-notify] general-project revenue skipped (null orderAmount) payOrderId={} tenantId={}",
|
||||
row.getId(),
|
||||
row.getTenantId());
|
||||
return true;
|
||||
}
|
||||
req.setAmount(new BigDecimal(row.getOrderAmount()).divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP));
|
||||
billRpcService.addRevenue(req);
|
||||
log.info(
|
||||
"[pay-notify] general-project revenue added payOrderId={} tenantId={} userId={} projectId={} amount={}",
|
||||
row.getId(),
|
||||
row.getTenantId(),
|
||||
userId,
|
||||
projectId,
|
||||
req.getAmount());
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"[pay-notify] general-project revenue failed payOrderId={} tenantId={} userId={} msg={}",
|
||||
row.getId(),
|
||||
row.getTenantId(),
|
||||
userId,
|
||||
e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 安全解析 ext 中的数字字段(Integer / Long / String → Long)。 */
|
||||
private static Long parseLongExt(Object val) {
|
||||
if (val == null) {
|
||||
return null;
|
||||
}
|
||||
if (val instanceof Number) {
|
||||
return ((Number) val).longValue();
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(val.toString().trim());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static Long parseNumericBizOrderNo(String bizOrderNo) {
|
||||
if (bizOrderNo == null || bizOrderNo.isBlank()) {
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.xspaceagi.pay.application.support;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.xspaceagi.pay.sdk.support.PayOrderExtKeys;
|
||||
import com.xspaceagi.pay.sdk.enums.PayClientScene;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class PayOrderExtSupport {
|
||||
|
||||
private PayOrderExtSupport() {}
|
||||
|
||||
public static String mergePayClientScene(String extJson, PayClientScene scene) {
|
||||
if (scene == null) {
|
||||
return extJson;
|
||||
}
|
||||
Map<String, Object> map = parseExt(extJson);
|
||||
map.put(PayOrderExtKeys.PAY_CLIENT_SCENE, scene.name());
|
||||
return JSON.toJSONString(map);
|
||||
}
|
||||
|
||||
public static Map<String, Object> parseExt(String extJson) {
|
||||
if (extJson == null || extJson.isBlank()) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
try {
|
||||
Map<String, Object> parsed = JSON.parseObject(extJson);
|
||||
return parsed != null ? parsed : new HashMap<>();
|
||||
} catch (Exception e) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.xspaceagi.pay.application.support;
|
||||
|
||||
import com.xspaceagi.pay.domain.model.PayOrderModel;
|
||||
import com.xspaceagi.pay.sdk.dto.ScanOrderStatusQueryResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentStatusQueryResponse;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
@@ -20,7 +20,7 @@ public final class PayOrderGatewayQueryMerger {
|
||||
|
||||
private PayOrderGatewayQueryMerger() {}
|
||||
|
||||
public static void mergeIntoRow(PayOrderModel row, ScanOrderStatusQueryResponse r) {
|
||||
public static void mergeIntoRow(PayOrderModel row, PaymentStatusQueryResponse r) {
|
||||
if (r == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package com.xspaceagi.pay.application.support;
|
||||
|
||||
import com.xspaceagi.bill.sdk.rpc.IBillRpcService;
|
||||
import com.xspaceagi.pay.domain.gateway.PaymentScanGateway;
|
||||
import com.xspaceagi.pay.domain.model.PayOrderModel;
|
||||
import com.xspaceagi.pay.domain.repository.PayOrderRepository;
|
||||
import com.xspaceagi.pay.sdk.dto.ScanOrderStatusQueryResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentStatusQueryResponse;
|
||||
import com.xspaceagi.pay.sdk.enums.PaymentStatus;
|
||||
import com.xspaceagi.pay.spec.enums.PayBizNotifyStatus;
|
||||
import com.xspaceagi.pay.spec.enums.PayOrderGatewaySyncStatus;
|
||||
@@ -25,7 +24,7 @@ import org.springframework.util.StringUtils;
|
||||
public class PayOrderSettlementSyncService {
|
||||
|
||||
private final PayOrderRepository payOrderRepository;
|
||||
private final PaymentScanGateway paymentScanGateway;
|
||||
private final PayOrderStatusQuerySupport payOrderStatusQuerySupport;
|
||||
private final PayGatewayOutboundExecutor payGatewayOutboundExecutor;
|
||||
private final IBillRpcService billRpcService;
|
||||
|
||||
@@ -50,10 +49,10 @@ public class PayOrderSettlementSyncService {
|
||||
return false;
|
||||
}
|
||||
|
||||
ScanOrderStatusQueryResponse last;
|
||||
PaymentStatusQueryResponse last;
|
||||
try {
|
||||
PayGatewayOutbound outbound = payGatewayOutboundExecutor.resolveCached(tenantId);
|
||||
last = paymentScanGateway.queryOrderStatus(outbound, row.getGatewayPaymentOrderNo(), true);
|
||||
last = payOrderStatusQuerySupport.queryOrderStatus(outbound, row, true);
|
||||
PayOrderGatewayQueryMerger.mergeIntoRow(row, last);
|
||||
payOrderRepository.save(row);
|
||||
} catch (PayGatewayClientException e) {
|
||||
@@ -78,7 +77,7 @@ public class PayOrderSettlementSyncService {
|
||||
}
|
||||
|
||||
private boolean notifyBillAndMark(
|
||||
PayOrderModel row, ScanOrderStatusQueryResponse last, PayBizNotifyStatus expectedCurrent) {
|
||||
PayOrderModel row, PaymentStatusQueryResponse last, PayBizNotifyStatus expectedCurrent) {
|
||||
if (!PayOrderBillNotifySupport.notifyBillPaymentCallback(billRpcService, row, last, false)) {
|
||||
log.warn("[pay-settlement-sync] bill notify failed bizOrderNo={}", row.getBizOrderNo());
|
||||
return false;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.xspaceagi.pay.application.support;
|
||||
|
||||
import com.xspaceagi.pay.domain.gateway.PaymentAppGateway;
|
||||
import com.xspaceagi.pay.domain.gateway.PaymentH5Gateway;
|
||||
import com.xspaceagi.pay.domain.gateway.PaymentMiniPayGateway;
|
||||
import com.xspaceagi.pay.domain.gateway.PaymentScanGateway;
|
||||
import com.xspaceagi.pay.domain.model.PayOrderModel;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentStatusQueryResponse;
|
||||
import com.xspaceagi.pay.sdk.enums.PayMode;
|
||||
import com.xspaceagi.pay.spec.gateway.PayGatewayOutbound;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** 查询支付状态。 */
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class PayOrderStatusQuerySupport {
|
||||
|
||||
private final PaymentScanGateway paymentScanGateway;
|
||||
private final PaymentMiniPayGateway paymentMiniPayGateway;
|
||||
private final PaymentH5Gateway paymentH5Gateway;
|
||||
private final PaymentAppGateway paymentAppGateway;
|
||||
|
||||
public PaymentStatusQueryResponse queryOrderStatus(
|
||||
PayGatewayOutbound outbound, PayOrderModel row, boolean syncFromChannel) {
|
||||
if (row.getPayMode() == PayMode.minipay) {
|
||||
return paymentMiniPayGateway.queryOrderStatus(outbound, row.getGatewayPaymentOrderNo(), syncFromChannel);
|
||||
}
|
||||
if (row.getPayMode() == PayMode.h5) {
|
||||
return paymentH5Gateway.queryOrderStatus(outbound, row.getGatewayPaymentOrderNo(), syncFromChannel);
|
||||
}
|
||||
if (row.getPayMode() == PayMode.app) {
|
||||
return paymentAppGateway.queryOrderStatus(outbound, row.getGatewayPaymentOrderNo(), syncFromChannel);
|
||||
}
|
||||
return paymentScanGateway.queryOrderStatus(outbound, row.getGatewayPaymentOrderNo(), syncFromChannel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.xspaceagi.pay.domain.gateway;
|
||||
|
||||
import com.xspaceagi.pay.sdk.dto.FileEntryUploadResponse;
|
||||
import com.xspaceagi.pay.spec.gateway.PayGatewayOutbound;
|
||||
|
||||
/** 调用支付网关 {@code /open-api/file};出站上下文由应用层解析后传入。 */
|
||||
public interface FileEntryGateway {
|
||||
|
||||
FileEntryUploadResponse uploadMerchantOnboardingImage(
|
||||
PayGatewayOutbound outbound,
|
||||
byte[] fileBytes,
|
||||
String filename,
|
||||
String contentType,
|
||||
String replaceFileKey);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.xspaceagi.pay.domain.gateway;
|
||||
|
||||
import com.xspaceagi.pay.sdk.dto.OrderAndTransactionCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.OrderCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentStatusQueryResponse;
|
||||
import com.xspaceagi.pay.sdk.enums.PayChannel;
|
||||
import com.xspaceagi.pay.sdk.enums.PayClientScene;
|
||||
import com.xspaceagi.pay.spec.gateway.PayGatewayOutbound;
|
||||
|
||||
/** 调用支付网关 App 原生支付 Open API。 */
|
||||
public interface PaymentAppGateway {
|
||||
|
||||
OrderCreateResponse createOrder(
|
||||
PayGatewayOutbound outbound, String bizOrderNo, long orderAmount, String subject, String ext);
|
||||
|
||||
OrderAndTransactionCreateResponse createTransaction(
|
||||
PayGatewayOutbound outbound,
|
||||
String gatewayPaymentOrderNo,
|
||||
PayChannel payChannel,
|
||||
String clientIp,
|
||||
PayClientScene payClientScene);
|
||||
|
||||
PaymentStatusQueryResponse queryOrderStatus(
|
||||
PayGatewayOutbound outbound, String gatewayPaymentOrderNo, boolean syncFromChannel);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.xspaceagi.pay.domain.gateway;
|
||||
|
||||
import com.xspaceagi.pay.sdk.dto.OrderAndTransactionCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.OrderCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentStatusQueryResponse;
|
||||
import com.xspaceagi.pay.sdk.enums.PayChannel;
|
||||
import com.xspaceagi.pay.spec.gateway.PayGatewayOutbound;
|
||||
|
||||
/** 调用支付网关 H5 Open API;用于 H5 支付。 */
|
||||
public interface PaymentH5Gateway {
|
||||
|
||||
OrderCreateResponse createOrder(
|
||||
PayGatewayOutbound outbound, String bizOrderNo, long orderAmount, String subject, String ext);
|
||||
|
||||
OrderAndTransactionCreateResponse createTransaction(
|
||||
PayGatewayOutbound outbound,
|
||||
String gatewayPaymentOrderNo,
|
||||
PayChannel payChannel,
|
||||
String clientIp,
|
||||
String frontNotifyUrl);
|
||||
|
||||
PaymentStatusQueryResponse queryOrderStatus(
|
||||
PayGatewayOutbound outbound, String gatewayPaymentOrderNo, boolean syncFromChannel);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.xspaceagi.pay.domain.gateway;
|
||||
|
||||
import com.xspaceagi.pay.sdk.dto.OrderAndTransactionCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.OrderCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentStatusQueryResponse;
|
||||
import com.xspaceagi.pay.sdk.enums.PayChannel;
|
||||
import com.xspaceagi.pay.sdk.enums.PayClientScene;
|
||||
import com.xspaceagi.pay.spec.gateway.PayGatewayOutbound;
|
||||
|
||||
/** 调用支付网关小程序支付 Open API;出站上下文由应用层解析后传入。 */
|
||||
public interface PaymentMiniPayGateway {
|
||||
|
||||
OrderCreateResponse createOrder(
|
||||
PayGatewayOutbound outbound, String bizOrderNo, long orderAmount, String subject, String ext);
|
||||
|
||||
OrderAndTransactionCreateResponse createTransaction(
|
||||
PayGatewayOutbound outbound,
|
||||
String gatewayPaymentOrderNo,
|
||||
PayChannel payChannel,
|
||||
String subAppid,
|
||||
String openId,
|
||||
String buyerId,
|
||||
String clientIp,
|
||||
PayClientScene payClientScene);
|
||||
|
||||
/**
|
||||
* @param syncFromChannel true 时网关会向支付渠道查询并落库
|
||||
*/
|
||||
PaymentStatusQueryResponse queryOrderStatus(
|
||||
PayGatewayOutbound outbound, String gatewayPaymentOrderNo, boolean syncFromChannel);
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
package com.xspaceagi.pay.domain.gateway;
|
||||
|
||||
import com.xspaceagi.pay.sdk.dto.CashierSessionCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentOrderCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.ScanOrderStatusQueryResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.OrderCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentStatusQueryResponse;
|
||||
import com.xspaceagi.pay.spec.gateway.PayGatewayOutbound;
|
||||
|
||||
/** 调用 qiming-pay 扫码支付 Open API;出站上下文由应用层解析后传入。 */
|
||||
public interface PaymentScanGateway {
|
||||
|
||||
PaymentOrderCreateResponse createOrder(
|
||||
OrderCreateResponse createOrder(
|
||||
PayGatewayOutbound outbound, String bizOrderNo, long orderAmount, String subject, String ext);
|
||||
|
||||
CashierSessionCreateResponse createCashierSession(
|
||||
@@ -21,6 +21,6 @@ public interface PaymentScanGateway {
|
||||
/**
|
||||
* @param syncFromChannel true 时网关会向支付渠道查询并落库
|
||||
*/
|
||||
ScanOrderStatusQueryResponse queryOrderStatus(
|
||||
PaymentStatusQueryResponse queryOrderStatus(
|
||||
PayGatewayOutbound outbound, String gatewayPaymentOrderNo, boolean syncFromChannel);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.xspaceagi.pay.infra.gateway.client;
|
||||
|
||||
import com.xspaceagi.pay.domain.gateway.FileEntryGateway;
|
||||
import com.xspaceagi.pay.infra.gateway.utils.PayGatewayClientUtils;
|
||||
import com.xspaceagi.pay.sdk.dto.ApiResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.FileEntryOpenApiUploadRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.FileEntryUploadResponse;
|
||||
import com.xspaceagi.pay.sdk.sign.OpenApiFileEntryMultipartSupport;
|
||||
import com.xspaceagi.pay.sdk.sign.OpenApiFileEntryMultipartSupport.SignedUploadFields;
|
||||
import com.xspaceagi.pay.sdk.sign.OpenApiFileEntrySign;
|
||||
import com.xspaceagi.pay.spec.gateway.PayGatewayOutbound;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class FileEntryGatewayClient implements FileEntryGateway {
|
||||
|
||||
private static final String BIZ_MERCHANT_ONBOARDING_IMAGE = "merchant_onboarding_image";
|
||||
|
||||
private static final ParameterizedTypeReference<ApiResponse<FileEntryUploadResponse>> UPLOAD_RESPONSE_TYPE =
|
||||
new ParameterizedTypeReference<ApiResponse<FileEntryUploadResponse>>() {};
|
||||
|
||||
private final RestTemplate payGatewayRestTemplate;
|
||||
|
||||
public FileEntryGatewayClient(@Qualifier("payGatewayRestTemplate") RestTemplate payGatewayRestTemplate) {
|
||||
this.payGatewayRestTemplate = payGatewayRestTemplate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileEntryUploadResponse uploadMerchantOnboardingImage(
|
||||
PayGatewayOutbound outbound,
|
||||
byte[] fileBytes,
|
||||
String filename,
|
||||
String contentType,
|
||||
String replaceFileKey) {
|
||||
if (fileBytes == null || fileBytes.length == 0) {
|
||||
throw new IllegalArgumentException("请选择文件");
|
||||
}
|
||||
FileEntryOpenApiUploadRequest meta = new FileEntryOpenApiUploadRequest();
|
||||
meta.setClientId(outbound.clientId());
|
||||
meta.setBizType(BIZ_MERCHANT_ONBOARDING_IMAGE);
|
||||
if (StringUtils.hasText(replaceFileKey)) {
|
||||
meta.setReplaceFileKey(replaceFileKey.trim());
|
||||
}
|
||||
byte[] signedJson = OpenApiFileEntrySign.signUpload(meta, outbound.clientSecret());
|
||||
SignedUploadFields fields = OpenApiFileEntryMultipartSupport.parseSignedMetadata(signedJson);
|
||||
String url = outbound.baseUrl() + OpenApiFileEntrySign.PATH_UPLOAD;
|
||||
log.info("[pay-gateway] POST {} clientId={} bizType={}", url, fields.getClientId(), fields.getBizType());
|
||||
return PayGatewayClientUtils.postMultipartSigned(
|
||||
payGatewayRestTemplate,
|
||||
url,
|
||||
toMultipartBody(fields, fileBytes, filename, contentType),
|
||||
UPLOAD_RESPONSE_TYPE,
|
||||
true);
|
||||
}
|
||||
|
||||
private static MultiValueMap<String, Object> toMultipartBody(
|
||||
SignedUploadFields fields, byte[] fileBytes, String filename, String contentType) {
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
parts.add("clientId", fields.getClientId());
|
||||
parts.add("bizType", fields.getBizType());
|
||||
if (StringUtils.hasText(fields.getReplaceFileKey())) {
|
||||
parts.add("replaceFileKey", fields.getReplaceFileKey());
|
||||
}
|
||||
parts.add("timestamp", String.valueOf(fields.getTimestamp()));
|
||||
parts.add("nonce", fields.getNonce());
|
||||
parts.add("signature", fields.getSignature());
|
||||
String safeName = StringUtils.hasText(filename) ? filename : "upload";
|
||||
ByteArrayResource resource = new ByteArrayResource(fileBytes) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return safeName;
|
||||
}
|
||||
};
|
||||
HttpHeaders fileHeaders = new HttpHeaders();
|
||||
if (StringUtils.hasText(contentType)) {
|
||||
fileHeaders.setContentType(MediaType.parseMediaType(contentType));
|
||||
}
|
||||
parts.add("file", new HttpEntity<>(resource, fileHeaders));
|
||||
return parts;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.xspaceagi.pay.infra.gateway.client;
|
||||
|
||||
import com.xspaceagi.pay.domain.gateway.PaymentAppGateway;
|
||||
import com.xspaceagi.pay.infra.gateway.utils.PayGatewayClientUtils;
|
||||
import com.xspaceagi.pay.sdk.dto.ApiResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.AppOrderCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.AppTransactionCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.OrderAndTransactionCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.OrderCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentStatusQueryRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentStatusQueryResponse;
|
||||
import com.xspaceagi.pay.sdk.enums.PayChannel;
|
||||
import com.xspaceagi.pay.sdk.enums.PayClientScene;
|
||||
import com.xspaceagi.pay.sdk.sign.OpenApiPaymentAppSign;
|
||||
import com.xspaceagi.pay.spec.gateway.PayGatewayOutbound;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class PaymentAppGatewayClient implements PaymentAppGateway {
|
||||
|
||||
private static final ParameterizedTypeReference<ApiResponse<OrderCreateResponse>> CREATE_TYPE =
|
||||
new ParameterizedTypeReference<ApiResponse<OrderCreateResponse>>() {};
|
||||
|
||||
private static final ParameterizedTypeReference<ApiResponse<OrderAndTransactionCreateResponse>> TX_TYPE =
|
||||
new ParameterizedTypeReference<ApiResponse<OrderAndTransactionCreateResponse>>() {};
|
||||
|
||||
private static final ParameterizedTypeReference<ApiResponse<PaymentStatusQueryResponse>> STATUS_TYPE =
|
||||
new ParameterizedTypeReference<ApiResponse<PaymentStatusQueryResponse>>() {};
|
||||
|
||||
private final RestTemplate payGatewayRestTemplate;
|
||||
|
||||
public PaymentAppGatewayClient(@Qualifier("payGatewayRestTemplate") RestTemplate payGatewayRestTemplate) {
|
||||
this.payGatewayRestTemplate = payGatewayRestTemplate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OrderCreateResponse createOrder(
|
||||
PayGatewayOutbound outbound, String bizOrderNo, long orderAmount, String subject, String ext) {
|
||||
AppOrderCreateRequest request = new AppOrderCreateRequest();
|
||||
request.setClientId(outbound.clientId());
|
||||
request.setBizOrderNo(bizOrderNo);
|
||||
request.setOrderAmount(orderAmount);
|
||||
request.setSubject(subject);
|
||||
request.setExt(ext);
|
||||
byte[] body = OpenApiPaymentAppSign.signCreateOrder(request, outbound.clientSecret());
|
||||
String url = outbound.baseUrl() + OpenApiPaymentAppSign.PATH_CREATE_ORDER;
|
||||
log.info("[pay-gateway] POST {}", url);
|
||||
return PayGatewayClientUtils.postSigned(payGatewayRestTemplate, url, body, CREATE_TYPE, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OrderAndTransactionCreateResponse createTransaction(
|
||||
PayGatewayOutbound outbound,
|
||||
String gatewayPaymentOrderNo,
|
||||
PayChannel payChannel,
|
||||
String clientIp,
|
||||
PayClientScene payClientScene) {
|
||||
AppTransactionCreateRequest request = new AppTransactionCreateRequest();
|
||||
request.setClientId(outbound.clientId());
|
||||
request.setGatewayPaymentOrderNo(gatewayPaymentOrderNo);
|
||||
request.setPayChannel(payChannel);
|
||||
request.setClientIp(clientIp);
|
||||
request.setPayClientScene(payClientScene);
|
||||
byte[] body = OpenApiPaymentAppSign.signCreateTransaction(request, outbound.clientSecret());
|
||||
String url = outbound.baseUrl() + OpenApiPaymentAppSign.PATH_CREATE_TRANSACTION;
|
||||
log.info("[pay-gateway] POST {} payChannel={}", url, payChannel);
|
||||
return PayGatewayClientUtils.postSigned(payGatewayRestTemplate, url, body, TX_TYPE, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaymentStatusQueryResponse queryOrderStatus(
|
||||
PayGatewayOutbound outbound, String gatewayPaymentOrderNo, boolean syncFromChannel) {
|
||||
PaymentStatusQueryRequest request = new PaymentStatusQueryRequest();
|
||||
request.setClientId(outbound.clientId());
|
||||
request.setGatewayPaymentOrderNo(gatewayPaymentOrderNo);
|
||||
request.setSyncFromChannel(syncFromChannel);
|
||||
byte[] body = OpenApiPaymentAppSign.signQueryStatus(request, outbound.clientSecret());
|
||||
String url = outbound.baseUrl() + OpenApiPaymentAppSign.PATH_STATUS;
|
||||
log.info("[pay-gateway] POST {} syncFromChannel={}", url, syncFromChannel);
|
||||
return PayGatewayClientUtils.postSigned(payGatewayRestTemplate, url, body, STATUS_TYPE, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.xspaceagi.pay.infra.gateway.client;
|
||||
|
||||
import com.xspaceagi.pay.domain.gateway.PaymentH5Gateway;
|
||||
import com.xspaceagi.pay.infra.gateway.utils.PayGatewayClientUtils;
|
||||
import com.xspaceagi.pay.sdk.dto.ApiResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.H5OrderCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.H5TransactionCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.OrderAndTransactionCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.OrderCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentStatusQueryRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentStatusQueryResponse;
|
||||
import com.xspaceagi.pay.sdk.enums.PayChannel;
|
||||
import com.xspaceagi.pay.sdk.sign.OpenApiPaymentH5Sign;
|
||||
import com.xspaceagi.pay.spec.gateway.PayGatewayOutbound;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class PaymentH5GatewayClient implements PaymentH5Gateway {
|
||||
|
||||
private static final ParameterizedTypeReference<ApiResponse<OrderCreateResponse>> CREATE_TYPE =
|
||||
new ParameterizedTypeReference<ApiResponse<OrderCreateResponse>>() {};
|
||||
|
||||
private static final ParameterizedTypeReference<ApiResponse<OrderAndTransactionCreateResponse>> TX_TYPE =
|
||||
new ParameterizedTypeReference<ApiResponse<OrderAndTransactionCreateResponse>>() {};
|
||||
|
||||
private static final ParameterizedTypeReference<ApiResponse<PaymentStatusQueryResponse>> STATUS_TYPE =
|
||||
new ParameterizedTypeReference<ApiResponse<PaymentStatusQueryResponse>>() {};
|
||||
|
||||
private final RestTemplate payGatewayRestTemplate;
|
||||
|
||||
public PaymentH5GatewayClient(@Qualifier("payGatewayRestTemplate") RestTemplate payGatewayRestTemplate) {
|
||||
this.payGatewayRestTemplate = payGatewayRestTemplate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OrderCreateResponse createOrder(
|
||||
PayGatewayOutbound outbound, String bizOrderNo, long orderAmount, String subject, String ext) {
|
||||
H5OrderCreateRequest request = new H5OrderCreateRequest();
|
||||
request.setClientId(outbound.clientId());
|
||||
request.setBizOrderNo(bizOrderNo);
|
||||
request.setOrderAmount(orderAmount);
|
||||
request.setSubject(subject);
|
||||
request.setExt(ext);
|
||||
byte[] body = OpenApiPaymentH5Sign.signCreateOrder(request, outbound.clientSecret());
|
||||
String url = outbound.baseUrl() + OpenApiPaymentH5Sign.PATH_CREATE_ORDER;
|
||||
log.info("[pay-gateway] POST {}", url);
|
||||
return PayGatewayClientUtils.postSigned(payGatewayRestTemplate, url, body, CREATE_TYPE, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OrderAndTransactionCreateResponse createTransaction(
|
||||
PayGatewayOutbound outbound,
|
||||
String gatewayPaymentOrderNo,
|
||||
PayChannel payChannel,
|
||||
String clientIp,
|
||||
String frontNotifyUrl) {
|
||||
H5TransactionCreateRequest request = new H5TransactionCreateRequest();
|
||||
request.setClientId(outbound.clientId());
|
||||
request.setGatewayPaymentOrderNo(gatewayPaymentOrderNo);
|
||||
request.setPayChannel(payChannel);
|
||||
request.setClientIp(clientIp);
|
||||
request.setFrontNotifyUrl(frontNotifyUrl);
|
||||
byte[] body = OpenApiPaymentH5Sign.signCreateTransaction(request, outbound.clientSecret());
|
||||
String url = outbound.baseUrl() + OpenApiPaymentH5Sign.PATH_CREATE_TRANSACTION;
|
||||
log.info("[pay-gateway] POST {} payChannel={}", url, payChannel);
|
||||
return PayGatewayClientUtils.postSigned(payGatewayRestTemplate, url, body, TX_TYPE, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaymentStatusQueryResponse queryOrderStatus(
|
||||
PayGatewayOutbound outbound, String gatewayPaymentOrderNo, boolean syncFromChannel) {
|
||||
PaymentStatusQueryRequest request = new PaymentStatusQueryRequest();
|
||||
request.setClientId(outbound.clientId());
|
||||
request.setGatewayPaymentOrderNo(gatewayPaymentOrderNo);
|
||||
request.setSyncFromChannel(syncFromChannel);
|
||||
byte[] body = OpenApiPaymentH5Sign.signQueryStatus(request, outbound.clientSecret());
|
||||
String url = outbound.baseUrl() + OpenApiPaymentH5Sign.PATH_STATUS;
|
||||
log.info("[pay-gateway] POST {} syncFromChannel={}", url, syncFromChannel);
|
||||
return PayGatewayClientUtils.postSigned(payGatewayRestTemplate, url, body, STATUS_TYPE, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.xspaceagi.pay.infra.gateway.client;
|
||||
|
||||
import com.xspaceagi.pay.domain.gateway.PaymentMiniPayGateway;
|
||||
import com.xspaceagi.pay.infra.gateway.utils.PayGatewayClientUtils;
|
||||
import com.xspaceagi.pay.sdk.dto.ApiResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.MiniPayOrderCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.MiniPayTransactionCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.OrderAndTransactionCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.OrderCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentStatusQueryRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentStatusQueryResponse;
|
||||
import com.xspaceagi.pay.sdk.enums.PayChannel;
|
||||
import com.xspaceagi.pay.sdk.enums.PayClientScene;
|
||||
import com.xspaceagi.pay.sdk.sign.OpenApiPaymentMiniPaySign;
|
||||
import com.xspaceagi.pay.spec.gateway.PayGatewayOutbound;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class PaymentMiniPayGatewayClient implements PaymentMiniPayGateway {
|
||||
|
||||
private static final ParameterizedTypeReference<ApiResponse<OrderCreateResponse>> CREATE_TYPE =
|
||||
new ParameterizedTypeReference<ApiResponse<OrderCreateResponse>>() {};
|
||||
|
||||
private static final ParameterizedTypeReference<ApiResponse<OrderAndTransactionCreateResponse>> TX_TYPE =
|
||||
new ParameterizedTypeReference<ApiResponse<OrderAndTransactionCreateResponse>>() {};
|
||||
|
||||
private static final ParameterizedTypeReference<ApiResponse<PaymentStatusQueryResponse>> STATUS_TYPE =
|
||||
new ParameterizedTypeReference<ApiResponse<PaymentStatusQueryResponse>>() {};
|
||||
|
||||
private final RestTemplate payGatewayRestTemplate;
|
||||
|
||||
public PaymentMiniPayGatewayClient(@Qualifier("payGatewayRestTemplate") RestTemplate payGatewayRestTemplate) {
|
||||
this.payGatewayRestTemplate = payGatewayRestTemplate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OrderCreateResponse createOrder(
|
||||
PayGatewayOutbound outbound, String bizOrderNo, long orderAmount, String subject, String ext) {
|
||||
MiniPayOrderCreateRequest request = new MiniPayOrderCreateRequest();
|
||||
request.setClientId(outbound.clientId());
|
||||
request.setBizOrderNo(bizOrderNo);
|
||||
request.setOrderAmount(orderAmount);
|
||||
request.setSubject(subject);
|
||||
byte[] body = OpenApiPaymentMiniPaySign.signCreateOrder(request, outbound.clientSecret());
|
||||
String url = outbound.baseUrl() + OpenApiPaymentMiniPaySign.PATH_CREATE_ORDER;
|
||||
log.info("[pay-gateway] POST {}", url);
|
||||
return PayGatewayClientUtils.postSigned(payGatewayRestTemplate, url, body, CREATE_TYPE, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OrderAndTransactionCreateResponse createTransaction(
|
||||
PayGatewayOutbound outbound,
|
||||
String gatewayPaymentOrderNo,
|
||||
PayChannel payChannel,
|
||||
String subAppid,
|
||||
String openId,
|
||||
String buyerId,
|
||||
String clientIp,
|
||||
PayClientScene payClientScene) {
|
||||
MiniPayTransactionCreateRequest request = new MiniPayTransactionCreateRequest();
|
||||
request.setClientId(outbound.clientId());
|
||||
request.setGatewayPaymentOrderNo(gatewayPaymentOrderNo);
|
||||
request.setPayChannel(payChannel);
|
||||
request.setSubAppid(subAppid);
|
||||
request.setOpenId(openId);
|
||||
request.setBuyerId(buyerId);
|
||||
request.setClientIp(clientIp);
|
||||
request.setPayClientScene(payClientScene);
|
||||
byte[] body = OpenApiPaymentMiniPaySign.signCreateTransaction(request, outbound.clientSecret());
|
||||
String url = outbound.baseUrl() + OpenApiPaymentMiniPaySign.PATH_CREATE_TRANSACTION;
|
||||
log.info("[pay-gateway] POST {} payChannel={}", url, payChannel);
|
||||
return PayGatewayClientUtils.postSigned(payGatewayRestTemplate, url, body, TX_TYPE, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaymentStatusQueryResponse queryOrderStatus(
|
||||
PayGatewayOutbound outbound, String gatewayPaymentOrderNo, boolean syncFromChannel) {
|
||||
PaymentStatusQueryRequest request = new PaymentStatusQueryRequest();
|
||||
request.setClientId(outbound.clientId());
|
||||
request.setGatewayPaymentOrderNo(gatewayPaymentOrderNo);
|
||||
request.setSyncFromChannel(syncFromChannel);
|
||||
byte[] body = OpenApiPaymentMiniPaySign.signQueryStatus(request, outbound.clientSecret());
|
||||
String url = outbound.baseUrl() + OpenApiPaymentMiniPaySign.PATH_STATUS;
|
||||
log.info("[pay-gateway] POST {} syncFromChannel={}", url, syncFromChannel);
|
||||
return PayGatewayClientUtils.postSigned(payGatewayRestTemplate, url, body, STATUS_TYPE, true);
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,10 @@ import com.xspaceagi.pay.infra.gateway.utils.PayGatewayClientUtils;
|
||||
import com.xspaceagi.pay.sdk.dto.ApiResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.CashierSessionCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.CashierSessionCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentOrderCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.OrderCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentStatusQueryRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.ScanOrderCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.ScanOrderStatusQueryResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentStatusQueryResponse;
|
||||
import com.xspaceagi.pay.sdk.sign.OpenApiCashierSign;
|
||||
import com.xspaceagi.pay.sdk.sign.OpenApiPaymentScanSign;
|
||||
import com.xspaceagi.pay.spec.gateway.PayGatewayOutbound;
|
||||
@@ -22,11 +22,11 @@ import org.springframework.web.client.RestTemplate;
|
||||
@Component
|
||||
public class PaymentScanGatewayClient implements PaymentScanGateway {
|
||||
|
||||
private static final ParameterizedTypeReference<ApiResponse<PaymentOrderCreateResponse>> CREATE_TYPE =
|
||||
new ParameterizedTypeReference<ApiResponse<PaymentOrderCreateResponse>>() {};
|
||||
private static final ParameterizedTypeReference<ApiResponse<OrderCreateResponse>> CREATE_TYPE =
|
||||
new ParameterizedTypeReference<ApiResponse<OrderCreateResponse>>() {};
|
||||
|
||||
private static final ParameterizedTypeReference<ApiResponse<ScanOrderStatusQueryResponse>> STATUS_TYPE =
|
||||
new ParameterizedTypeReference<ApiResponse<ScanOrderStatusQueryResponse>>() {};
|
||||
private static final ParameterizedTypeReference<ApiResponse<PaymentStatusQueryResponse>> STATUS_TYPE =
|
||||
new ParameterizedTypeReference<ApiResponse<PaymentStatusQueryResponse>>() {};
|
||||
|
||||
private static final ParameterizedTypeReference<ApiResponse<CashierSessionCreateResponse>> CASHIER_SESSION_TYPE =
|
||||
new ParameterizedTypeReference<ApiResponse<CashierSessionCreateResponse>>() {};
|
||||
@@ -38,7 +38,7 @@ public class PaymentScanGatewayClient implements PaymentScanGateway {
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaymentOrderCreateResponse createOrder(
|
||||
public OrderCreateResponse createOrder(
|
||||
PayGatewayOutbound outbound, String bizOrderNo, long orderAmount, String subject, String ext) {
|
||||
ScanOrderCreateRequest request = new ScanOrderCreateRequest();
|
||||
request.setClientId(outbound.clientId());
|
||||
@@ -53,7 +53,7 @@ public class PaymentScanGatewayClient implements PaymentScanGateway {
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScanOrderStatusQueryResponse queryOrderStatus(
|
||||
public PaymentStatusQueryResponse queryOrderStatus(
|
||||
PayGatewayOutbound outbound, String gatewayPaymentOrderNo, boolean syncFromChannel) {
|
||||
PaymentStatusQueryRequest request = new PaymentStatusQueryRequest();
|
||||
request.setClientId(outbound.clientId());
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.HttpStatusCodeException;
|
||||
import org.springframework.web.client.ResourceAccessException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
@@ -67,6 +68,31 @@ public final class PayGatewayClientUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> T postMultipartSigned(
|
||||
RestTemplate restTemplate,
|
||||
String url,
|
||||
MultiValueMap<String, Object> parts,
|
||||
ParameterizedTypeReference<ApiResponse<T>> typeRef,
|
||||
boolean requireNonNullData) {
|
||||
try {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(parts, headers);
|
||||
ResponseEntity<ApiResponse<T>> response = restTemplate.exchange(url, HttpMethod.POST, entity, typeRef);
|
||||
return unwrap(response.getBody(), url, requireNonNullData);
|
||||
} catch (HttpStatusCodeException e) {
|
||||
log.warn(
|
||||
"[pay-gateway] POST multipart failed url={} status={} body={}",
|
||||
url,
|
||||
e.getStatusCode(),
|
||||
e.getResponseBodyAsString());
|
||||
throw toHttpErrorException(e);
|
||||
} catch (ResourceAccessException e) {
|
||||
log.warn("[pay-gateway] POST multipart network error url={} msg={}", url, e.getMessage(), e);
|
||||
throw new PayGatewayClientException();
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> T unwrap(ApiResponse<T> resp, String url, boolean requireNonNullData) {
|
||||
if (resp == null) {
|
||||
log.warn("[pay-gateway] empty response url={}", url);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import com.xspaceagi.pay.sdk.enums.PayChannel;
|
||||
import com.xspaceagi.pay.sdk.enums.PayClientScene;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class AppOrderAndTransactionCreateRequest {
|
||||
private String clientId;
|
||||
private Long timestamp;
|
||||
private String nonce;
|
||||
private String signature;
|
||||
private String bizOrderNo;
|
||||
private PayChannel payChannel;
|
||||
private Long orderAmount;
|
||||
private String subject;
|
||||
private String ext;
|
||||
private String clientIp;
|
||||
private PayClientScene payClientScene;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class AppOrderCreateRequest {
|
||||
private String clientId;
|
||||
private Long timestamp;
|
||||
private String nonce;
|
||||
private String signature;
|
||||
private String bizOrderNo;
|
||||
private Long orderAmount;
|
||||
private String subject;
|
||||
private String ext;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/** 创建 App 原生支付单(仅落网关支付单,不调渠道)。 */
|
||||
@Data
|
||||
public class AppOrderRpcCreateRequest {
|
||||
private String bizOrderNo;
|
||||
private Long orderAmount;
|
||||
private String subject;
|
||||
private String ext;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import com.xspaceagi.pay.sdk.enums.PayChannel;
|
||||
import com.xspaceagi.pay.sdk.enums.PayClientScene;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class AppTransactionCreateRequest {
|
||||
private String clientId;
|
||||
private Long timestamp;
|
||||
private String nonce;
|
||||
private String signature;
|
||||
private String gatewayPaymentOrderNo;
|
||||
private PayChannel payChannel;
|
||||
private String clientIp;
|
||||
private PayClientScene payClientScene;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import com.xspaceagi.pay.sdk.enums.PayChannel;
|
||||
import com.xspaceagi.pay.sdk.enums.PayClientScene;
|
||||
import lombok.Data;
|
||||
|
||||
/** 对已有 App 支付单调渠道下单,返回原生 SDK 调起参数。 */
|
||||
@Data
|
||||
public class AppTransactionRpcCreateRequest {
|
||||
private String bizOrderNo;
|
||||
private PayChannel payChannel;
|
||||
private String clientIp;
|
||||
private PayClientScene payClientScene;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/** Open API 文件上传元数据(不含文件二进制;与 multipart 表单字段一致)。 */
|
||||
@Data
|
||||
public class FileEntryOpenApiUploadRequest {
|
||||
private String clientId;
|
||||
private String bizType;
|
||||
private String replaceFileKey;
|
||||
private Long timestamp;
|
||||
private String nonce;
|
||||
private String signature;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class FileEntryUploadResponse {
|
||||
/** 32 位小写 hex,无中划线 */
|
||||
private String fileKey;
|
||||
/** 对外可访问 URL(含 site.base-url) */
|
||||
private String publicUrl;
|
||||
private String contentType;
|
||||
private int sizeBytes;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class H5OrderCreateRequest {
|
||||
private String clientId;
|
||||
private Long timestamp;
|
||||
private String nonce;
|
||||
private String signature;
|
||||
private String bizOrderNo;
|
||||
private Long orderAmount;
|
||||
private String subject;
|
||||
private String ext;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/** 创建 H5支付单(仅落网关支付单,不调渠道)。 */
|
||||
@Data
|
||||
public class H5OrderRpcCreateRequest {
|
||||
private String bizOrderNo;
|
||||
private Long orderAmount;
|
||||
private String subject;
|
||||
private String ext;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import com.xspaceagi.pay.sdk.enums.PayChannel;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class H5TransactionCreateRequest {
|
||||
private String clientId;
|
||||
private Long timestamp;
|
||||
private String nonce;
|
||||
private String signature;
|
||||
private String gatewayPaymentOrderNo;
|
||||
private PayChannel payChannel;
|
||||
private String clientIp;
|
||||
private String frontNotifyUrl;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import com.xspaceagi.pay.sdk.enums.PayChannel;
|
||||
import lombok.Data;
|
||||
|
||||
/** 对已有 h5 支付单调渠道下单,返回前端调起参数。 */
|
||||
@Data
|
||||
public class H5TransactionRpcCreateRequest {
|
||||
private String bizOrderNo;
|
||||
private PayChannel payChannel;
|
||||
private String clientIp;
|
||||
private String frontNotifyUrl;
|
||||
}
|
||||
@@ -54,18 +54,25 @@ public class MerchantOnboardingResponse {
|
||||
|
||||
/** 营业执照 */
|
||||
private String orgCertificateUrl;
|
||||
private String orgCertificateFileKey;
|
||||
/** 法人身份证正面 */
|
||||
private String legalPersonIdCardFrontUrl;
|
||||
private String legalPersonIdCardFrontFileKey;
|
||||
/** 法人身份证背面 */
|
||||
private String legalPersonIdCardBackUrl;
|
||||
private String legalPersonIdCardBackFileKey;
|
||||
/** 财务室照片 */
|
||||
private String photoFinanceRoomUrl;
|
||||
private String photoFinanceRoomFileKey;
|
||||
/** 门头照片 */
|
||||
private String photoGateUrl;
|
||||
private String photoGateFileKey;
|
||||
/** 地标照片 */
|
||||
private String photoLandmarkUrl;
|
||||
private String photoLandmarkFileKey;
|
||||
/** 银行开户证明 */
|
||||
private String bankAccountProofUrl;
|
||||
private String bankAccountProofFileKey;
|
||||
|
||||
private LocalDateTime created;
|
||||
private LocalDateTime modified;
|
||||
|
||||
@@ -5,7 +5,8 @@ import com.xspaceagi.pay.sdk.enums.MerchantOnboardingType;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 创建/全量更新进件资料(图片字段均为 URL)。
|
||||
* 创建/全量更新进件资料。
|
||||
* 影像优先传 {@code *FileKey}(本系统上传);{@code *Url} 保留兼容外部链接。
|
||||
*/
|
||||
@Data
|
||||
public class MerchantOnboardingUpsertRequest {
|
||||
@@ -56,4 +57,16 @@ public class MerchantOnboardingUpsertRequest {
|
||||
private String photoGateUrl;
|
||||
private String photoLandmarkUrl;
|
||||
private String bankAccountProofUrl;
|
||||
|
||||
|
||||
/** 营业执照;本系统上传时为 file_entry.file_key */
|
||||
private String orgCertificateFileKey;
|
||||
/** 法人身份证人像面/正面 file_key */
|
||||
private String legalPersonIdCardFrontFileKey;
|
||||
/** 法人身份证国徽面/反面 file_key */
|
||||
private String legalPersonIdCardBackFileKey;
|
||||
private String photoFinanceRoomFileKey;
|
||||
private String photoGateFileKey;
|
||||
private String photoLandmarkFileKey;
|
||||
private String bankAccountProofFileKey;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import com.xspaceagi.pay.sdk.enums.PayChannel;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class MiniPayOrderAndTransactionCreateRequest {
|
||||
private String clientId;
|
||||
private Long timestamp;
|
||||
private String nonce;
|
||||
private String signature;
|
||||
private String bizOrderNo;
|
||||
private PayChannel payChannel;
|
||||
private Long orderAmount;
|
||||
private String subject;
|
||||
/** 微信小程序 AppID(安心付 channel_params.sub_appid,WxPay 必填) */
|
||||
private String subAppid;
|
||||
/** 微信:用户 OpenID(安心付 channel_params.open_id) */
|
||||
private String openId;
|
||||
/** 支付宝小程序:用户 buyer_id(安心付 channel_params.buyer_id) */
|
||||
private String buyerId;
|
||||
private String ext;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class MiniPayOrderCreateRequest {
|
||||
private String clientId;
|
||||
private Long timestamp;
|
||||
private String nonce;
|
||||
private String signature;
|
||||
private String bizOrderNo;
|
||||
private Long orderAmount;
|
||||
private String subject;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/** 创建小程序支付单(仅落网关支付单,不调渠道)。 */
|
||||
@Data
|
||||
public class MiniPayOrderRpcCreateRequest {
|
||||
private String bizOrderNo;
|
||||
private Long orderAmount;
|
||||
private String subject;
|
||||
private String ext;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import com.xspaceagi.pay.sdk.enums.PayChannel;
|
||||
import com.xspaceagi.pay.sdk.enums.PayClientScene;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class MiniPayTransactionCreateRequest {
|
||||
private String clientId;
|
||||
private Long timestamp;
|
||||
private String nonce;
|
||||
private String signature;
|
||||
private String gatewayPaymentOrderNo;
|
||||
private PayChannel payChannel;
|
||||
/** WxPay minipay:channel_params.sub_appid */
|
||||
private String subAppid;
|
||||
/** WxPay minipay:channel_params.open_id */
|
||||
private String openId;
|
||||
/** AliPay minipay:channel_params.buyer_id */
|
||||
private String buyerId;
|
||||
private String clientIp;
|
||||
/** 调起场景 */
|
||||
private PayClientScene payClientScene;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import com.xspaceagi.pay.sdk.enums.PayChannel;
|
||||
import com.xspaceagi.pay.sdk.enums.PayClientScene;
|
||||
import lombok.Data;
|
||||
|
||||
/** 对已有 minipay 支付单调渠道下单,返回调起支付参数。 */
|
||||
@Data
|
||||
public class MiniPayTransactionRpcCreateRequest {
|
||||
private String bizOrderNo;
|
||||
private PayChannel payChannel;
|
||||
/** WxPay minipay:小程序或公众号 AppID(sub_appid) */
|
||||
private String subAppid;
|
||||
/** WxPay minipay:用户 OpenID */
|
||||
private String openId;
|
||||
/** AliPay minipay:用户 buyer_id */
|
||||
private String buyerId;
|
||||
private String clientIp;
|
||||
/** minipay 调起场景:小程序 / 微信JSAPI,写入 pay_order.ext */
|
||||
private PayClientScene payClientScene;
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import com.xspaceagi.pay.sdk.enums.PayChannel;
|
||||
import com.xspaceagi.pay.sdk.enums.PayInvokeType;
|
||||
import com.xspaceagi.pay.sdk.enums.PayMode;
|
||||
import com.xspaceagi.pay.sdk.enums.PaymentStatus;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/** 创建支付单/渠道下单后的统一响应 */
|
||||
@Data
|
||||
@Builder
|
||||
public class ScanOrderCreateResponse {
|
||||
public class OrderAndTransactionCreateResponse {
|
||||
private String gatewayPaymentOrderNo;
|
||||
private String gatewayPaymentTxNo;
|
||||
/** 渠道服务费,单位分 */
|
||||
@@ -20,6 +22,14 @@ public class ScanOrderCreateResponse {
|
||||
private PaymentStatus status;
|
||||
private PayChannel payChannel;
|
||||
private PayMode payMode;
|
||||
private PayInvokeType invokeType;
|
||||
/** 主扫 scan:渠道返回的二维码/链接原文(payparam) */
|
||||
private String qrCodeContent;
|
||||
private String formHtml;
|
||||
private String redirectUrl;
|
||||
/** minipay + WxPay:wx.requestPayment 参数 */
|
||||
private WxPayInvokeParams wxPayParams;
|
||||
/** minipay + AliPay:tradeNO,用于 my.tradePay */
|
||||
private String alipayTradeNo;
|
||||
private String expiredAt;
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class PaymentOrderCreateResponse {
|
||||
public class OrderCreateResponse {
|
||||
private String gatewayPaymentOrderNo;
|
||||
private String bizOrderNo;
|
||||
private PaymentStatus status;
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import com.xspaceagi.pay.sdk.enums.PayChannel;
|
||||
import com.xspaceagi.pay.sdk.enums.PayInvokeType;
|
||||
import com.xspaceagi.pay.sdk.enums.PayMode;
|
||||
import com.xspaceagi.pay.sdk.enums.PaymentStatus;
|
||||
import lombok.Builder;
|
||||
@@ -8,7 +9,7 @@ import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class ScanOrderStatusQueryResponse {
|
||||
public class PaymentStatusQueryResponse {
|
||||
private String gatewayPaymentOrderNo;
|
||||
private String gatewayPaymentTxNo;
|
||||
private PaymentStatus status;
|
||||
@@ -19,6 +20,9 @@ public class ScanOrderStatusQueryResponse {
|
||||
private Long providerFee;
|
||||
private Long platformFee;
|
||||
private Long netAmount;
|
||||
private PayInvokeType invokeType;
|
||||
private String qrCodeContent;
|
||||
private String formHtml;
|
||||
private String redirectUrl;
|
||||
private String paidAt;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/** 微信小程序 {@code wx.requestPayment} 所需字段(来自渠道 payparam JSON)。 */
|
||||
@Data
|
||||
@Builder
|
||||
public class WxPayInvokeParams {
|
||||
private String appId;
|
||||
private String timeStamp;
|
||||
private String nonceStr;
|
||||
/** 对应微信字段 package,值为 prepay_id=... */
|
||||
private String packageValue;
|
||||
private String signType;
|
||||
private String paySign;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.xspaceagi.pay.sdk.enums;
|
||||
|
||||
/** 调起场景 */
|
||||
public enum PayClientScene {
|
||||
MINI_PROGRAM("微信小程序"), //渠道 pay_mode 均为 minipay
|
||||
WECHAT_JSAPI("微信JSAPI"), //渠道 pay_mode 均为 minipay
|
||||
H5_WEB("系统浏览器H5"),
|
||||
NATIVE_APP("App原生SDK"); // gateway_back:微信 common / 支付宝 scan
|
||||
|
||||
private final String name;
|
||||
|
||||
PayClientScene(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.xspaceagi.pay.sdk.enums;
|
||||
|
||||
public enum PayInvokeType {
|
||||
FORM_HTML,
|
||||
REDIRECT_URL,
|
||||
QRCODE_FALLBACK
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.xspaceagi.pay.sdk.enums;
|
||||
|
||||
public enum PayMode {
|
||||
scan("主扫");
|
||||
scan("主扫"),
|
||||
minipay("小程序支付"),
|
||||
h5("H5支付"),
|
||||
app("App原生支付");
|
||||
|
||||
private final String name;
|
||||
|
||||
|
||||
@@ -1,17 +1,42 @@
|
||||
package com.xspaceagi.pay.sdk.service;
|
||||
|
||||
import com.xspaceagi.pay.sdk.dto.AppOrderRpcCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.AppTransactionRpcCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.CashierSessionCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.CashierSessionCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentOrderCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.H5OrderRpcCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.H5TransactionRpcCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.MiniPayOrderRpcCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.MiniPayTransactionRpcCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.OrderAndTransactionCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.OrderCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentStatusQueryRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.ScanOrderCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.ScanOrderStatusQueryResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentStatusQueryResponse;
|
||||
|
||||
/** 平台内支付 RPC 契约(由 pay-application 实现,供 bill 等外部模块依赖调用)。 */
|
||||
public interface IPaymentRpcService {
|
||||
|
||||
/** 创建扫码支付订单 */
|
||||
PaymentOrderCreateResponse createOrderForScan(ScanOrderCreateRequest request);
|
||||
OrderCreateResponse createOrderForScan(ScanOrderCreateRequest request);
|
||||
|
||||
/** 创建小程序支付单(仅落网关支付单,不调渠道;调起支付见 {@link #createMiniPayTransaction}) */
|
||||
OrderCreateResponse createOrderForMiniPay(MiniPayOrderRpcCreateRequest request);
|
||||
|
||||
/** 创建 H5支付单(仅落网关支付单,不调渠道;调起支付见 {@link #createH5Transaction}) */
|
||||
OrderCreateResponse createOrderForH5(H5OrderRpcCreateRequest request);
|
||||
|
||||
/** 创建 App 原生支付单(仅落网关支付单,不调渠道;调起支付见 {@link #createAppTransaction}) */
|
||||
OrderCreateResponse createOrderForApp(AppOrderRpcCreateRequest request);
|
||||
|
||||
/** 对已有小程序支付单调渠道下单,返回 wx.requestPayment / my.tradePay 参数 */
|
||||
OrderAndTransactionCreateResponse createMiniPayTransaction(MiniPayTransactionRpcCreateRequest request);
|
||||
|
||||
/** 对已有 h5 支付单调渠道下单,返回 formHtml / redirectUrl / qrcode 兜底 */
|
||||
OrderAndTransactionCreateResponse createH5Transaction(H5TransactionRpcCreateRequest request);
|
||||
|
||||
/** 对已有 App 支付单调渠道下单,返回 wxPayParams / redirectUrl */
|
||||
OrderAndTransactionCreateResponse createAppTransaction(AppTransactionRpcCreateRequest request);
|
||||
|
||||
/**
|
||||
* 获取收银台地址;会话有过期时间,过期后需重新调用。
|
||||
@@ -19,7 +44,7 @@ public interface IPaymentRpcService {
|
||||
CashierSessionCreateResponse createCashierSession(CashierSessionCreateRequest request);
|
||||
|
||||
/** 查询支付状态(仅更新 pay_order,不通知 Bill) */
|
||||
ScanOrderStatusQueryResponse queryStatus(PaymentStatusQueryRequest request);
|
||||
PaymentStatusQueryResponse queryStatus(PaymentStatusQueryRequest request);
|
||||
|
||||
/**
|
||||
* 结算页主动同步:按业务单号查网关终态并通知 Bill(与支付轮询任务单 tick 一致,幂等)。
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.xspaceagi.pay.sdk.sign;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
|
||||
/** 将已签名的 Open API JSON 元数据解析为 multipart 表单字段。 */
|
||||
public final class OpenApiFileEntryMultipartSupport {
|
||||
|
||||
private OpenApiFileEntryMultipartSupport() {}
|
||||
|
||||
@Value
|
||||
@Builder
|
||||
public static class SignedUploadFields {
|
||||
String clientId;
|
||||
String bizType;
|
||||
String replaceFileKey;
|
||||
long timestamp;
|
||||
String nonce;
|
||||
String signature;
|
||||
}
|
||||
|
||||
public static SignedUploadFields parseSignedMetadata(byte[] signedJson) {
|
||||
if (signedJson == null || signedJson.length == 0) {
|
||||
throw new IllegalArgumentException("signed metadata must not be empty");
|
||||
}
|
||||
try {
|
||||
JsonNode root = OpenApiSignJson.mapper().readTree(signedJson);
|
||||
JsonNode clientId = root.get("clientId");
|
||||
JsonNode bizType = root.get("bizType");
|
||||
JsonNode timestamp = root.get("timestamp");
|
||||
JsonNode nonce = root.get("nonce");
|
||||
JsonNode signature = root.get("signature");
|
||||
if (clientId == null
|
||||
|| !clientId.isTextual()
|
||||
|| bizType == null
|
||||
|| !bizType.isTextual()
|
||||
|| timestamp == null
|
||||
|| !timestamp.isNumber()
|
||||
|| nonce == null
|
||||
|| !nonce.isTextual()
|
||||
|| signature == null
|
||||
|| !signature.isTextual()) {
|
||||
throw new IllegalArgumentException("invalid signed file upload metadata");
|
||||
}
|
||||
String replaceFileKey = null;
|
||||
JsonNode replaceNode = root.get("replaceFileKey");
|
||||
if (replaceNode != null && replaceNode.isTextual() && !replaceNode.asText().isBlank()) {
|
||||
replaceFileKey = replaceNode.asText().trim();
|
||||
}
|
||||
return SignedUploadFields.builder()
|
||||
.clientId(clientId.asText().trim())
|
||||
.bizType(bizType.asText().trim())
|
||||
.replaceFileKey(replaceFileKey)
|
||||
.timestamp(timestamp.longValue())
|
||||
.nonce(nonce.asText().trim())
|
||||
.signature(signature.asText().trim())
|
||||
.build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("invalid signed file upload metadata", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.xspaceagi.pay.sdk.sign;
|
||||
|
||||
import com.xspaceagi.pay.sdk.dto.FileEntryOpenApiUploadRequest;
|
||||
|
||||
/** 文件 Open API 路径与签名辅助 */
|
||||
public final class OpenApiFileEntrySign {
|
||||
|
||||
public static final String PATH_UPLOAD = "/open-api/file/upload";
|
||||
|
||||
private OpenApiFileEntrySign() {}
|
||||
|
||||
public static byte[] signUpload(FileEntryOpenApiUploadRequest request, String clientSecret) {
|
||||
return OpenApiSignSupport.sign(PATH_UPLOAD, request, clientSecret);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.xspaceagi.pay.sdk.sign;
|
||||
|
||||
import com.xspaceagi.pay.sdk.dto.AppOrderAndTransactionCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.AppOrderCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.AppTransactionCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentStatusQueryRequest;
|
||||
|
||||
/** App 原生支付 Open API 路径与签名辅助。 */
|
||||
public final class OpenApiPaymentAppSign {
|
||||
|
||||
public static final String BASE = "/open-api/payment/app";
|
||||
public static final String PATH_CREATE_ORDER_AND_TRANSACTION = BASE + "/create-order-and-transaction";
|
||||
public static final String PATH_CREATE_ORDER = BASE + "/create-order";
|
||||
public static final String PATH_CREATE_TRANSACTION = BASE + "/create-transaction";
|
||||
public static final String PATH_STATUS = BASE + "/status";
|
||||
|
||||
private OpenApiPaymentAppSign() {}
|
||||
|
||||
public static byte[] signCreateOrderAndTransaction(
|
||||
AppOrderAndTransactionCreateRequest request, String clientSecret) {
|
||||
return OpenApiSignSupport.sign(PATH_CREATE_ORDER_AND_TRANSACTION, request, clientSecret);
|
||||
}
|
||||
|
||||
public static byte[] signCreateOrder(AppOrderCreateRequest request, String clientSecret) {
|
||||
return OpenApiSignSupport.sign(PATH_CREATE_ORDER, request, clientSecret);
|
||||
}
|
||||
|
||||
public static byte[] signCreateTransaction(AppTransactionCreateRequest request, String clientSecret) {
|
||||
return OpenApiSignSupport.sign(PATH_CREATE_TRANSACTION, request, clientSecret);
|
||||
}
|
||||
|
||||
public static byte[] signQueryStatus(PaymentStatusQueryRequest request, String clientSecret) {
|
||||
return OpenApiSignSupport.sign(PATH_STATUS, request, clientSecret);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.xspaceagi.pay.sdk.sign;
|
||||
|
||||
import com.xspaceagi.pay.sdk.dto.H5OrderCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.H5TransactionCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentStatusQueryRequest;
|
||||
|
||||
public final class OpenApiPaymentH5Sign {
|
||||
|
||||
public static final String BASE = "/open-api/payment/h5";
|
||||
public static final String PATH_CREATE_ORDER = BASE + "/create-order";
|
||||
public static final String PATH_CREATE_TRANSACTION = BASE + "/create-transaction";
|
||||
public static final String PATH_STATUS = BASE + "/status";
|
||||
|
||||
private OpenApiPaymentH5Sign() {}
|
||||
|
||||
public static byte[] signCreateOrder(H5OrderCreateRequest request, String clientSecret) {
|
||||
return OpenApiSignSupport.sign(PATH_CREATE_ORDER, request, clientSecret);
|
||||
}
|
||||
|
||||
public static byte[] signCreateTransaction(H5TransactionCreateRequest request, String clientSecret) {
|
||||
return OpenApiSignSupport.sign(PATH_CREATE_TRANSACTION, request, clientSecret);
|
||||
}
|
||||
|
||||
public static byte[] signQueryStatus(PaymentStatusQueryRequest request, String clientSecret) {
|
||||
return OpenApiSignSupport.sign(PATH_STATUS, request, clientSecret);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.xspaceagi.pay.sdk.sign;
|
||||
|
||||
import com.xspaceagi.pay.sdk.dto.MiniPayOrderAndTransactionCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.MiniPayOrderCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.MiniPayTransactionCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentStatusQueryRequest;
|
||||
|
||||
/** 微信/支付宝小程序支付(minipay)Open API 路径与签名辅助 */
|
||||
public final class OpenApiPaymentMiniPaySign {
|
||||
|
||||
public static final String BASE = "/open-api/payment/minipay";
|
||||
public static final String PATH_CREATE_ORDER_AND_TRANSACTION = BASE + "/create-order-and-transaction";
|
||||
public static final String PATH_CREATE_ORDER = BASE + "/create-order";
|
||||
public static final String PATH_CREATE_TRANSACTION = BASE + "/create-transaction";
|
||||
public static final String PATH_STATUS = BASE + "/status";
|
||||
|
||||
private OpenApiPaymentMiniPaySign() {}
|
||||
|
||||
public static byte[] signCreateOrderAndTransaction(
|
||||
MiniPayOrderAndTransactionCreateRequest request, String clientSecret) {
|
||||
return OpenApiSignSupport.sign(PATH_CREATE_ORDER_AND_TRANSACTION, request, clientSecret);
|
||||
}
|
||||
|
||||
public static byte[] signCreateOrder(MiniPayOrderCreateRequest request, String clientSecret) {
|
||||
return OpenApiSignSupport.sign(PATH_CREATE_ORDER, request, clientSecret);
|
||||
}
|
||||
|
||||
public static byte[] signCreateTransaction(MiniPayTransactionCreateRequest request, String clientSecret) {
|
||||
return OpenApiSignSupport.sign(PATH_CREATE_TRANSACTION, request, clientSecret);
|
||||
}
|
||||
|
||||
public static byte[] signQueryStatus(PaymentStatusQueryRequest request, String clientSecret) {
|
||||
return OpenApiSignSupport.sign(PATH_STATUS, request, clientSecret);
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 各 Open API 路径参与 body 哈希的字段(不含 {@code timestamp}/{@code nonce}/{@code signature})。
|
||||
* <p>path 与各 {@code OpenApi*Sign#PATH_*} 保持一致,为单一来源。</p>
|
||||
* 各 Open API 路径参与 body 哈希的字段
|
||||
*/
|
||||
public final class OpenApiSignPolicies {
|
||||
|
||||
@@ -24,6 +23,39 @@ public final class OpenApiSignPolicies {
|
||||
Map.entry(
|
||||
OpenApiPaymentScanSign.PATH_STATUS,
|
||||
List.of("clientId", "gatewayPaymentOrderNo", "syncFromChannel")),
|
||||
Map.entry(
|
||||
OpenApiPaymentMiniPaySign.PATH_CREATE_ORDER_AND_TRANSACTION,
|
||||
List.of("bizOrderNo", "buyerId", "clientId", "openId", "orderAmount", "payChannel", "subAppid")),
|
||||
Map.entry(
|
||||
OpenApiPaymentMiniPaySign.PATH_CREATE_ORDER,
|
||||
List.of("bizOrderNo", "clientId", "orderAmount")),
|
||||
Map.entry(
|
||||
OpenApiPaymentMiniPaySign.PATH_CREATE_TRANSACTION,
|
||||
List.of("buyerId", "clientId", "gatewayPaymentOrderNo", "openId", "payChannel", "subAppid")),
|
||||
Map.entry(
|
||||
OpenApiPaymentMiniPaySign.PATH_STATUS,
|
||||
List.of("clientId", "gatewayPaymentOrderNo", "syncFromChannel")),
|
||||
Map.entry(
|
||||
OpenApiPaymentH5Sign.PATH_CREATE_ORDER,
|
||||
List.of("bizOrderNo", "clientId", "orderAmount")),
|
||||
Map.entry(
|
||||
OpenApiPaymentH5Sign.PATH_CREATE_TRANSACTION,
|
||||
List.of("clientId", "frontNotifyUrl", "gatewayPaymentOrderNo", "payChannel")),
|
||||
Map.entry(
|
||||
OpenApiPaymentH5Sign.PATH_STATUS,
|
||||
List.of("clientId", "gatewayPaymentOrderNo", "syncFromChannel")),
|
||||
Map.entry(
|
||||
OpenApiPaymentAppSign.PATH_CREATE_ORDER_AND_TRANSACTION,
|
||||
List.of("bizOrderNo", "clientId", "orderAmount", "payChannel")),
|
||||
Map.entry(
|
||||
OpenApiPaymentAppSign.PATH_CREATE_ORDER,
|
||||
List.of("bizOrderNo", "clientId", "orderAmount")),
|
||||
Map.entry(
|
||||
OpenApiPaymentAppSign.PATH_CREATE_TRANSACTION,
|
||||
List.of("clientId", "gatewayPaymentOrderNo", "payChannel")),
|
||||
Map.entry(
|
||||
OpenApiPaymentAppSign.PATH_STATUS,
|
||||
List.of("clientId", "gatewayPaymentOrderNo", "syncFromChannel")),
|
||||
Map.entry(OpenApiPaymentConfigSign.PATH_QUERY, List.of("clientId")),
|
||||
Map.entry(
|
||||
OpenApiMerchantOnboardingSign.PATH_ADD,
|
||||
@@ -38,7 +70,10 @@ public final class OpenApiSignPolicies {
|
||||
List.of("clientId", "onboardingType", "page", "pageSize", "status")),
|
||||
Map.entry(
|
||||
OpenApiCashierSign.PATH_SESSION,
|
||||
List.of("clientId", "gatewayPaymentOrderNo", "orderAmount")));
|
||||
List.of("clientId", "gatewayPaymentOrderNo", "orderAmount")),
|
||||
Map.entry(
|
||||
OpenApiFileEntrySign.PATH_UPLOAD,
|
||||
List.of("clientId", "bizType", "replaceFileKey")));
|
||||
|
||||
public static List<String> signFieldsForPath(String path) {
|
||||
String normalized = normalizePath(path);
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.xspaceagi.pay.sdk.support;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 识别 App 壳 / WebView 内发起的 HTTP 请求(非系统浏览器、非微信内置浏览器)。
|
||||
* 平台 App 请求会携带 {@value #HEADER_CLIENT_TYPE};兜底解析 User-Agent 中的 App 标记。
|
||||
*/
|
||||
public final class PayAppWebViewDetector {
|
||||
|
||||
/** App 客户端标识,与 AuthInterceptor 续期逻辑一致:非空即视为 App 请求。 */
|
||||
public static final String HEADER_CLIENT_TYPE = "X-Client-Type";
|
||||
|
||||
private static final Set<String> APP_CLIENT_TYPE_VALUES = Set.of(
|
||||
"app",
|
||||
"native",
|
||||
"ios",
|
||||
"android",
|
||||
"mobile",
|
||||
"mobile-app",
|
||||
"qiming-app",
|
||||
"nuwax-app");
|
||||
|
||||
private static final Set<String> WEB_CLIENT_TYPE_VALUES = Set.of("web", "h5", "browser", "wap");
|
||||
|
||||
private PayAppWebViewDetector() {}
|
||||
|
||||
/**
|
||||
* 是否为 App WebView 内请求。H5 支付在此环境下会被渠道判定违规,应改用 App 原生 SDK 支付。
|
||||
*/
|
||||
public static boolean isAppWebView(String clientTypeHeader, String userAgent) {
|
||||
if (clientTypeHeader != null && !clientTypeHeader.isBlank()) {
|
||||
String normalized = clientTypeHeader.trim().toLowerCase(Locale.ROOT);
|
||||
if (WEB_CLIENT_TYPE_VALUES.contains(normalized)) {
|
||||
return false;
|
||||
}
|
||||
if (APP_CLIENT_TYPE_VALUES.contains(normalized) || normalized.startsWith("app")) {
|
||||
return true;
|
||||
}
|
||||
// 平台 App 统一注入 X-Client-Type;非 web 类取值视为 App 壳(与鉴权续期策略一致)
|
||||
return true;
|
||||
}
|
||||
return matchesAppWebViewUserAgent(userAgent);
|
||||
}
|
||||
|
||||
private static boolean matchesAppWebViewUserAgent(String userAgent) {
|
||||
if (userAgent == null || userAgent.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
String ua = userAgent;
|
||||
if (ua.contains("QimingApp") || ua.contains("QIMING_APP") || ua.contains("qiming-app")
|
||||
|| ua.contains("NuwaxApp") || ua.contains("NUWAX_APP") || ua.contains("nuwax-app")) {
|
||||
return true;
|
||||
}
|
||||
String lower = ua.toLowerCase(Locale.ROOT);
|
||||
if ((lower.contains("qiming") || lower.contains("nuwax"))
|
||||
&& (lower.contains("webview") || lower.contains("; wv)"))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.xspaceagi.pay.sdk.support;
|
||||
|
||||
/** pay_order.ext / Bill order extra 中与支付相关的扩展键。 */
|
||||
public final class PayOrderExtKeys {
|
||||
|
||||
public static final String PAY_CLIENT_SCENE = "payClientScene";
|
||||
|
||||
public static final String GENERAL_PROJECT_ID = "generalProjectId";
|
||||
public static final String GENERAL_PROJECT_USER_ID = "generalProjectUserId";
|
||||
|
||||
private PayOrderExtKeys() {}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.xspaceagi.pay.spec.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 通用项目支付 - App 原生第一步:创建订单请求。
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class GeneralProjectAppOrderCreateRequest {
|
||||
|
||||
/** 项目 ID(必传),前端从 DEV_PROJECT_ID 环境变量获取 */
|
||||
private String projectId;
|
||||
|
||||
/** 业务订单号,可空;传了用 GP1_ 前缀(幂等),不传用 GP2_ 前缀自动生成 */
|
||||
private String bizOrderNo;
|
||||
|
||||
/** 订单金额(单位:分) */
|
||||
private Long orderAmount;
|
||||
|
||||
/** 订单标题/商品描述 */
|
||||
private String subject;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.xspaceagi.pay.spec.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 通用项目支付 - App 原生第一步:创建订单响应。
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class GeneralProjectAppOrderCreateResponse {
|
||||
|
||||
private String orderNo;
|
||||
|
||||
private String gatewayOrderNo;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.xspaceagi.pay.spec.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 通用项目支付 - App 原生第二步:调起渠道支付请求。
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class GeneralProjectAppPayRequest {
|
||||
|
||||
/** 业务订单号(第一步返回的 orderNo) */
|
||||
private String orderNo;
|
||||
|
||||
/** 支付渠道:WxPay / AliPay */
|
||||
private String payChannel;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.xspaceagi.pay.spec.dto;
|
||||
|
||||
import com.xspaceagi.pay.sdk.dto.WxPayInvokeParams;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 通用项目支付 - App 原生第二步:调起渠道支付响应。
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class GeneralProjectAppPayResponse {
|
||||
|
||||
private String orderNo;
|
||||
|
||||
private String gatewayOrderNo;
|
||||
|
||||
/** WxPay | AliPay */
|
||||
private String payChannel;
|
||||
|
||||
/** 微信:其他渠道 prepay JSON(当前安心付渠道为 null);支付宝:不使用 */
|
||||
private WxPayInvokeParams wxPayParams;
|
||||
|
||||
/**
|
||||
* 微信:weixin://dl/business/...
|
||||
* 支付宝:https://qr.alipay.com/... 或 alipays://
|
||||
*/
|
||||
private String redirectUrl;
|
||||
|
||||
/** 支付宝 tradeNO(部分 SDK 使用) */
|
||||
private String alipayTradeNo;
|
||||
|
||||
/** REDIRECT_URL | QRCODE_FALLBACK */
|
||||
private String invokeType;
|
||||
|
||||
/** 支付宝 invokeType=QRCODE_FALLBACK 时的二维码内容 */
|
||||
private String qrCodeContent;
|
||||
|
||||
private String status;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.xspaceagi.pay.spec.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 通用项目支付 - 收银台模式请求。
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class GeneralProjectCashierRequest {
|
||||
|
||||
/** 项目 ID(必传),前端从 DEV_PROJECT_ID 环境变量获取 */
|
||||
private String projectId;
|
||||
|
||||
/** 业务订单号,可空;传了用 GP1_ 前缀(幂等),不传用 GP2_ 前缀自动生成 */
|
||||
private String bizOrderNo;
|
||||
|
||||
/** 订单金额(单位:分) */
|
||||
private Long orderAmount;
|
||||
|
||||
/** 订单标题/商品描述 */
|
||||
private String subject;
|
||||
|
||||
/** 支付完成后前端回跳地址,可空 */
|
||||
private String frontNotifyUrl;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.xspaceagi.pay.spec.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 通用项目支付 - 收银台模式响应。
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class GeneralProjectCashierResponse {
|
||||
|
||||
/** 业务订单号(GP1_ 或 GP2_ 前缀) */
|
||||
private String orderNo;
|
||||
|
||||
/** 网关支付订单号 */
|
||||
private String gatewayOrderNo;
|
||||
|
||||
/** 收银台跳转地址(浏览器直接跳转) */
|
||||
private String cashierUrl;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.xspaceagi.pay.spec.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 通用项目支付 - H5 第一步:创建订单请求。
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class GeneralProjectH5OrderCreateRequest {
|
||||
|
||||
/** 项目 ID(必传),前端从 DEV_PROJECT_ID 环境变量获取 */
|
||||
private String projectId;
|
||||
|
||||
/** 业务订单号,可空;传了用 GP1_ 前缀(幂等),不传用 GP2_ 前缀自动生成 */
|
||||
private String bizOrderNo;
|
||||
|
||||
/** 订单金额(单位:分) */
|
||||
private Long orderAmount;
|
||||
|
||||
/** 订单标题/商品描述 */
|
||||
private String subject;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.xspaceagi.pay.spec.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 通用项目支付 - H5 第一步:创建订单响应。
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class GeneralProjectH5OrderCreateResponse {
|
||||
|
||||
/** 业务订单号(GP1_ 或 GP2_ 前缀) */
|
||||
private String orderNo;
|
||||
|
||||
/** 网关支付订单号 */
|
||||
private String gatewayOrderNo;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.xspaceagi.pay.spec.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 通用项目支付 - H5 第二步:调起渠道支付请求。
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class GeneralProjectH5PayRequest {
|
||||
|
||||
/** 业务订单号(第一步返回的 orderNo) */
|
||||
private String orderNo;
|
||||
|
||||
/** 支付渠道:WxPay / AliPay */
|
||||
private String payChannel;
|
||||
|
||||
/** 支付完成后前端回跳地址 */
|
||||
private String frontNotifyUrl;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.xspaceagi.pay.spec.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 通用项目支付 - H5 第二步:调起渠道支付响应。
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class GeneralProjectH5PayResponse {
|
||||
|
||||
/** 业务订单号 */
|
||||
private String orderNo;
|
||||
|
||||
/** 网关支付订单号 */
|
||||
private String gatewayOrderNo;
|
||||
|
||||
/** 表单 HTML(invokeType=FORM_HTML 时,直接写入页面) */
|
||||
private String formHtml;
|
||||
|
||||
/** 跳转地址(invokeType=REDIRECT_URL 时,浏览器跳转) */
|
||||
private String redirectUrl;
|
||||
|
||||
/** 调起方式:FORM_HTML / REDIRECT_URL / QRCODE_FALLBACK */
|
||||
private String invokeType;
|
||||
|
||||
/** 当前支付单状态 */
|
||||
private String status;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.xspaceagi.pay.spec.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 通用项目支付 - 查询支付状态请求。
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class GeneralProjectPaymentStatusRequest {
|
||||
|
||||
/** 网关支付订单号 */
|
||||
private String gatewayOrderNo;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.xspaceagi.pay.spec.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 通用项目支付 - 查询支付状态响应。
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class GeneralProjectPaymentStatusResponse {
|
||||
|
||||
/** 支付状态:INIT / PENDING / PAID / FAILED / CLOSED */
|
||||
private String status;
|
||||
|
||||
/** 支付渠道:WxPay / AliPay / UnionPay */
|
||||
private String payChannel;
|
||||
|
||||
/** 支付模式:scan / h5 / minipay */
|
||||
private String payMode;
|
||||
|
||||
/** 订单金额(单位:分) */
|
||||
private Long orderAmount;
|
||||
|
||||
/** 支付完成时间 */
|
||||
private String paidAt;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.xspaceagi.pay.spec.support;
|
||||
|
||||
import com.xspaceagi.pay.sdk.support.PayAppWebViewDetector;
|
||||
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
|
||||
import com.xspaceagi.system.spec.exception.BizException;
|
||||
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
|
||||
|
||||
/**
|
||||
* App 原生支付仅允许在 App WebView 内调起;系统/手机浏览器应使用 H5 接口。
|
||||
*/
|
||||
public final class PayAppNativeInAppGuard {
|
||||
|
||||
private PayAppNativeInAppGuard() {}
|
||||
|
||||
public static void assertAppNativePayRequired(String clientTypeHeader, String userAgent) {
|
||||
if (!PayAppWebViewDetector.isAppWebView(clientTypeHeader, userAgent)) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.pay_app_native_requires_app);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.xspaceagi.pay.spec.support;
|
||||
|
||||
import com.xspaceagi.pay.sdk.support.PayAppWebViewDetector;
|
||||
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
|
||||
import com.xspaceagi.system.spec.exception.BizException;
|
||||
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
|
||||
|
||||
/**
|
||||
* App WebView 内禁止调起 H5 支付(渠道违规);应使用 App 原生 SDK 接口。
|
||||
*/
|
||||
public final class PayH5InAppGuard {
|
||||
|
||||
private PayH5InAppGuard() {}
|
||||
|
||||
public static void assertH5PayAllowed(String clientTypeHeader, String userAgent) {
|
||||
if (PayAppWebViewDetector.isAppWebView(clientTypeHeader, userAgent)) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.pay_h5_not_allowed_in_app);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,5 +24,9 @@
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>system-spec</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-custom-page-sdk</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.xspaceagi.pay.web.controller.admin;
|
||||
|
||||
import com.xspaceagi.pay.application.service.FileEntryApplicationService;
|
||||
import com.xspaceagi.pay.application.service.MerchantOnboardingApplicationService;
|
||||
import com.xspaceagi.pay.sdk.dto.FileEntryUploadResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.MerchantOnboardingResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.MerchantOnboardingUpdateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.MerchantOnboardingUpsertRequest;
|
||||
@@ -14,10 +16,14 @@ import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import static com.xspaceagi.system.spec.enums.ResourceEnum.PAY_EARNINGS_MODIFY;
|
||||
import static com.xspaceagi.system.spec.enums.ResourceEnum.PAY_EARNINGS_QUERY;
|
||||
@@ -31,6 +37,25 @@ public class MerchantOnboardingAdminController {
|
||||
@Resource
|
||||
private MerchantOnboardingApplicationService merchantOnboardingAppService;
|
||||
|
||||
@Resource
|
||||
private FileEntryApplicationService fileEntryAppService;
|
||||
|
||||
@RequireResource(PAY_EARNINGS_MODIFY)
|
||||
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@Operation(summary = "上传进件影像", description = "上传文件,返回 fileKey 与 publicUrl;提交进件请传 *FileKey,重传同字段请带 replaceFileKey")
|
||||
public ReqResult<FileEntryUploadResponse> upload(
|
||||
@RequestParam("file") MultipartFile file,
|
||||
@RequestParam(value = "replaceFileKey", required = false) String replaceFileKey) {
|
||||
Long tenantId = RequestContext.get() == null ? null : RequestContext.get().getTenantId();
|
||||
Assert.notNull(tenantId, "tenantId must be non-null");
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new IllegalArgumentException("请选择文件");
|
||||
}
|
||||
String replaceKey = StringUtils.hasText(replaceFileKey) ? replaceFileKey.trim() : null;
|
||||
log.info("api /pay/merchant-onboarding/upload-image tenantId={}, size={}", tenantId, file.getSize());
|
||||
return ReqResult.success(fileEntryAppService.uploadMerchantOnboardingImage(file, replaceKey));
|
||||
}
|
||||
|
||||
@RequireResource(PAY_EARNINGS_MODIFY)
|
||||
@PostMapping("/add")
|
||||
@Operation(summary = "新增进件", description = "仅支持租户进件、用户进件;不支持平台进件。clientId 由服务端按登录租户凭证写入,请求体勿传")
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
package com.xspaceagi.pay.web.controller.user;
|
||||
|
||||
import com.xspaceagi.pay.sdk.dto.AppOrderRpcCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.AppTransactionRpcCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.CashierSessionCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.CashierSessionCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.H5OrderRpcCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.H5TransactionRpcCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.OrderAndTransactionCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.OrderCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentStatusQueryRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentStatusQueryResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.ScanOrderCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.enums.PayChannel;
|
||||
import com.xspaceagi.pay.sdk.enums.PayClientScene;
|
||||
import com.xspaceagi.pay.sdk.service.IPaymentRpcService;
|
||||
import com.xspaceagi.pay.sdk.support.PayOrderExtKeys;
|
||||
import com.xspaceagi.pay.spec.dto.GeneralProjectAppOrderCreateRequest;
|
||||
import com.xspaceagi.pay.spec.dto.GeneralProjectAppOrderCreateResponse;
|
||||
import com.xspaceagi.pay.spec.dto.GeneralProjectAppPayRequest;
|
||||
import com.xspaceagi.pay.spec.dto.GeneralProjectAppPayResponse;
|
||||
import com.xspaceagi.pay.spec.dto.GeneralProjectCashierRequest;
|
||||
import com.xspaceagi.pay.spec.dto.GeneralProjectCashierResponse;
|
||||
import com.xspaceagi.pay.spec.dto.GeneralProjectH5OrderCreateRequest;
|
||||
import com.xspaceagi.pay.spec.dto.GeneralProjectH5OrderCreateResponse;
|
||||
import com.xspaceagi.pay.spec.dto.GeneralProjectH5PayRequest;
|
||||
import com.xspaceagi.pay.spec.dto.GeneralProjectH5PayResponse;
|
||||
import com.xspaceagi.pay.spec.dto.GeneralProjectPaymentStatusRequest;
|
||||
import com.xspaceagi.pay.spec.dto.GeneralProjectPaymentStatusResponse;
|
||||
import com.xspaceagi.pay.spec.support.PayAppNativeInAppGuard;
|
||||
import com.xspaceagi.pay.spec.support.PayH5InAppGuard;
|
||||
import com.xspaceagi.pay.sdk.support.PayAppWebViewDetector;
|
||||
import com.xspaceagi.custompage.sdk.dto.CustomPageDto;
|
||||
import com.xspaceagi.custompage.sdk.ICustomPageRpcService;
|
||||
import com.xspaceagi.system.spec.dto.ReqResult;
|
||||
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
|
||||
import com.xspaceagi.system.spec.exception.BizException;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* 通用项目支付接入(用户端)。
|
||||
* 面向项目沙箱/线上前端,无需登录态,projectId 必传(从 DEV_PROJECT_ID 获取)。
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/pay/general")
|
||||
@Tag(name = "支付-通用项目(用户端)")
|
||||
public class PayGeneralProjectController {
|
||||
|
||||
@Resource
|
||||
private IPaymentRpcService paymentRpcService;
|
||||
|
||||
@Resource
|
||||
private ICustomPageRpcService iCustomPageRpcService;
|
||||
|
||||
@PostMapping("/cashier")
|
||||
@Operation(summary = "收银台模式", description = "创建扫码支付订单并生成收银台跳转地址")
|
||||
public ReqResult<GeneralProjectCashierResponse> cashier(@RequestBody GeneralProjectCashierRequest request) {
|
||||
Assert.notNull(request, "request must not be null");
|
||||
Assert.hasText(request.getProjectId(), "projectId must not be blank");
|
||||
Assert.notNull(request.getOrderAmount(), "orderAmount must not be null");
|
||||
Assert.hasText(request.getSubject(), "subject must not be blank");
|
||||
|
||||
String bizOrderNo = resolveBizOrderNo(request.getBizOrderNo(), request.getProjectId());
|
||||
String ext = buildGeneralProjectExt(request.getProjectId());
|
||||
|
||||
ScanOrderCreateRequest scanReq = new ScanOrderCreateRequest();
|
||||
scanReq.setBizOrderNo(bizOrderNo);
|
||||
scanReq.setOrderAmount(request.getOrderAmount());
|
||||
scanReq.setSubject(request.getSubject());
|
||||
scanReq.setExt(ext);
|
||||
OrderCreateResponse orderResp = paymentRpcService.createOrderForScan(scanReq);
|
||||
|
||||
CashierSessionCreateRequest cashierReq = new CashierSessionCreateRequest();
|
||||
cashierReq.setGatewayPaymentOrderNo(orderResp.getGatewayPaymentOrderNo());
|
||||
cashierReq.setOrderAmount(request.getOrderAmount());
|
||||
cashierReq.setSubject(request.getSubject());
|
||||
cashierReq.setBizRedirectUrl(request.getFrontNotifyUrl());
|
||||
CashierSessionCreateResponse cashierResp = paymentRpcService.createCashierSession(cashierReq);
|
||||
|
||||
GeneralProjectCashierResponse resp = GeneralProjectCashierResponse.builder()
|
||||
.orderNo(bizOrderNo)
|
||||
.gatewayOrderNo(orderResp.getGatewayPaymentOrderNo())
|
||||
.cashierUrl(cashierResp.getCashierUrl())
|
||||
.build();
|
||||
return ReqResult.success(resp);
|
||||
}
|
||||
|
||||
@PostMapping("/app/create-order")
|
||||
@Operation(summary = "App原生-创建订单", description = "创建 App 原生支付订单(不调渠道)。仅限 App WebView 内调用")
|
||||
public ReqResult<GeneralProjectAppOrderCreateResponse> createAppOrder(@RequestBody GeneralProjectAppOrderCreateRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
assertAppNativePayRequired(httpRequest);
|
||||
Assert.notNull(request, "request must not be null");
|
||||
Assert.hasText(request.getProjectId(), "projectId must not be blank");
|
||||
Assert.notNull(request.getOrderAmount(), "orderAmount must not be null");
|
||||
Assert.hasText(request.getSubject(), "subject must not be blank");
|
||||
|
||||
String bizOrderNo = resolveBizOrderNo(request.getBizOrderNo(), request.getProjectId());
|
||||
String ext = buildGeneralProjectExt(request.getProjectId());
|
||||
|
||||
AppOrderRpcCreateRequest appReq = new AppOrderRpcCreateRequest();
|
||||
appReq.setBizOrderNo(bizOrderNo);
|
||||
appReq.setOrderAmount(request.getOrderAmount());
|
||||
appReq.setSubject(request.getSubject());
|
||||
appReq.setExt(ext);
|
||||
OrderCreateResponse orderResp = paymentRpcService.createOrderForApp(appReq);
|
||||
|
||||
GeneralProjectAppOrderCreateResponse resp = GeneralProjectAppOrderCreateResponse.builder()
|
||||
.orderNo(bizOrderNo)
|
||||
.gatewayOrderNo(orderResp.getGatewayPaymentOrderNo())
|
||||
.build();
|
||||
return ReqResult.success(resp);
|
||||
}
|
||||
|
||||
@PostMapping("/app/pay")
|
||||
@Operation(summary = "App原生-调起渠道支付", description = "仅限 App WebView。微信(当前渠道):invokeType=REDIRECT_URL + redirectUrl(weixin://...),前端解析 query 后 wx.miniapp.launchMiniProgram;"
|
||||
+ "支付宝:invokeType=REDIRECT_URL + redirectUrl(https/alipays) 或 QRCODE_FALLBACK + qrCodeContent")
|
||||
public ReqResult<GeneralProjectAppPayResponse> appPay(@RequestBody GeneralProjectAppPayRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
assertAppNativePayRequired(httpRequest);
|
||||
Assert.notNull(request, "request must not be null");
|
||||
Assert.hasText(request.getOrderNo(), "orderNo must not be blank");
|
||||
Assert.hasText(request.getPayChannel(), "payChannel must not be blank");
|
||||
|
||||
PayChannel payChannel = PayChannel.parse(request.getPayChannel());
|
||||
String clientIp = resolveClientIp(httpRequest);
|
||||
|
||||
AppTransactionRpcCreateRequest txReq = new AppTransactionRpcCreateRequest();
|
||||
txReq.setBizOrderNo(request.getOrderNo());
|
||||
txReq.setPayChannel(payChannel);
|
||||
txReq.setClientIp(clientIp);
|
||||
txReq.setPayClientScene(PayClientScene.NATIVE_APP);
|
||||
OrderAndTransactionCreateResponse txResp = paymentRpcService.createAppTransaction(txReq);
|
||||
|
||||
GeneralProjectAppPayResponse resp = GeneralProjectAppPayResponse.builder()
|
||||
.orderNo(request.getOrderNo())
|
||||
.gatewayOrderNo(txResp.getGatewayPaymentOrderNo())
|
||||
.payChannel(txResp.getPayChannel() == null ? null : txResp.getPayChannel().name())
|
||||
.wxPayParams(txResp.getWxPayParams())
|
||||
.redirectUrl(txResp.getRedirectUrl())
|
||||
.alipayTradeNo(txResp.getAlipayTradeNo())
|
||||
.invokeType(txResp.getInvokeType() == null ? null : txResp.getInvokeType().name())
|
||||
.qrCodeContent(txResp.getQrCodeContent())
|
||||
.status(txResp.getStatus() == null ? null : txResp.getStatus().name())
|
||||
.build();
|
||||
return ReqResult.success(resp);
|
||||
}
|
||||
|
||||
@PostMapping("/h5/create-order")
|
||||
@Operation(summary = "H5支付-创建订单", description = "创建 H5 支付订单(不调渠道),返回订单号。App WebView 内禁止,请使用收银台或 App 原生支付")
|
||||
public ReqResult<GeneralProjectH5OrderCreateResponse> createH5Order(@RequestBody GeneralProjectH5OrderCreateRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
assertH5PayAllowed(httpRequest);
|
||||
Assert.notNull(request, "request must not be null");
|
||||
Assert.hasText(request.getProjectId(), "projectId must not be blank");
|
||||
Assert.notNull(request.getOrderAmount(), "orderAmount must not be null");
|
||||
Assert.hasText(request.getSubject(), "subject must not be blank");
|
||||
|
||||
String bizOrderNo = resolveBizOrderNo(request.getBizOrderNo(), request.getProjectId());
|
||||
String ext = buildGeneralProjectExt(request.getProjectId());
|
||||
|
||||
H5OrderRpcCreateRequest h5Req = new H5OrderRpcCreateRequest();
|
||||
h5Req.setBizOrderNo(bizOrderNo);
|
||||
h5Req.setOrderAmount(request.getOrderAmount());
|
||||
h5Req.setSubject(request.getSubject());
|
||||
h5Req.setExt(ext);
|
||||
OrderCreateResponse orderResp = paymentRpcService.createOrderForH5(h5Req);
|
||||
|
||||
GeneralProjectH5OrderCreateResponse resp = GeneralProjectH5OrderCreateResponse.builder()
|
||||
.orderNo(bizOrderNo)
|
||||
.gatewayOrderNo(orderResp.getGatewayPaymentOrderNo())
|
||||
.build();
|
||||
return ReqResult.success(resp);
|
||||
}
|
||||
|
||||
@PostMapping("/h5/pay")
|
||||
@Operation(summary = "H5支付-调起渠道支付", description = "对已有 H5 订单调起微信/支付宝 H5 支付。App WebView 内禁止")
|
||||
public ReqResult<GeneralProjectH5PayResponse> h5Pay(@RequestBody GeneralProjectH5PayRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
assertH5PayAllowed(httpRequest);
|
||||
Assert.notNull(request, "request must not be null");
|
||||
Assert.hasText(request.getOrderNo(), "orderNo must not be blank");
|
||||
Assert.hasText(request.getPayChannel(), "payChannel must not be blank");
|
||||
Assert.hasText(request.getFrontNotifyUrl(), "frontNotifyUrl must not be blank");
|
||||
|
||||
PayChannel payChannel = PayChannel.parse(request.getPayChannel());
|
||||
String clientIp = resolveClientIp(httpRequest);
|
||||
|
||||
String frontNotifyUrl = validateFrontNotifyUrl(request.getFrontNotifyUrl().trim());
|
||||
|
||||
H5TransactionRpcCreateRequest txReq = new H5TransactionRpcCreateRequest();
|
||||
txReq.setBizOrderNo(request.getOrderNo());
|
||||
txReq.setPayChannel(payChannel);
|
||||
txReq.setClientIp(clientIp);
|
||||
txReq.setFrontNotifyUrl(buildChannelFrontNotifyUrl(frontNotifyUrl));
|
||||
OrderAndTransactionCreateResponse txResp = paymentRpcService.createH5Transaction(txReq);
|
||||
|
||||
GeneralProjectH5PayResponse resp = GeneralProjectH5PayResponse.builder()
|
||||
.orderNo(request.getOrderNo())
|
||||
.gatewayOrderNo(txResp.getGatewayPaymentOrderNo())
|
||||
.formHtml(txResp.getFormHtml())
|
||||
.redirectUrl(txResp.getRedirectUrl())
|
||||
.invokeType(txResp.getInvokeType() == null ? null : txResp.getInvokeType().name())
|
||||
.status(txResp.getStatus() == null ? null : txResp.getStatus().name())
|
||||
.build();
|
||||
return ReqResult.success(resp);
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/h5/front-notify", method = {RequestMethod.GET, RequestMethod.POST})
|
||||
@Operation(
|
||||
summary = "H5 渠道前台回跳中转",
|
||||
description = "接收渠道前台回调(GET/POST),统一 302 到前端回跳地址")
|
||||
public void handleH5FrontNotify(@RequestParam String returnUrl, HttpServletResponse response) {
|
||||
String target = validateFrontNotifyUrl(returnUrl.trim());
|
||||
response.setStatus(HttpServletResponse.SC_FOUND);
|
||||
response.setHeader("Location", target);
|
||||
}
|
||||
|
||||
@PostMapping("/status")
|
||||
@Operation(summary = "查询支付状态", description = "按网关支付订单号查询支付结果,会同步渠道")
|
||||
public ReqResult<GeneralProjectPaymentStatusResponse> queryStatus(@RequestBody GeneralProjectPaymentStatusRequest request) {
|
||||
Assert.notNull(request, "request must not be null");
|
||||
Assert.hasText(request.getGatewayOrderNo(), "gatewayOrderNo must not be blank");
|
||||
|
||||
PaymentStatusQueryRequest statusReq = new PaymentStatusQueryRequest();
|
||||
statusReq.setGatewayPaymentOrderNo(request.getGatewayOrderNo());
|
||||
statusReq.setSyncFromChannel(Boolean.TRUE);
|
||||
PaymentStatusQueryResponse statusResp = paymentRpcService.queryStatus(statusReq);
|
||||
|
||||
GeneralProjectPaymentStatusResponse resp = GeneralProjectPaymentStatusResponse.builder()
|
||||
.status(statusResp.getStatus() == null ? null : statusResp.getStatus().name())
|
||||
.payChannel(statusResp.getPayChannel() == null ? null : statusResp.getPayChannel().name())
|
||||
.payMode(statusResp.getPayMode() == null ? null : statusResp.getPayMode().name())
|
||||
.orderAmount(statusResp.getOrderAmount())
|
||||
.paidAt(statusResp.getPaidAt())
|
||||
.build();
|
||||
return ReqResult.success(resp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建通用项目 ext JSON,包含 generalProjectId 和 generalProjectUserId(项目创建者)。
|
||||
* 项目查不到或 creatorId 为 null 时不阻断支付(ext 中只有 projectId、无 userId)。
|
||||
*/
|
||||
private String buildGeneralProjectExt(String projectId) {
|
||||
Long creatorId = null;
|
||||
try {
|
||||
CustomPageDto projectDto = iCustomPageRpcService.queryDetail(Long.parseLong(projectId.trim()));
|
||||
if (projectDto != null) {
|
||||
creatorId = projectDto.getCreatorId();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[pay-general] queryDetail failed projectId={} msg={}", projectId, e.getMessage());
|
||||
}
|
||||
Map<String, Object> ext = new HashMap<>();
|
||||
ext.put(PayOrderExtKeys.GENERAL_PROJECT_ID, projectId);
|
||||
if (creatorId != null) {
|
||||
ext.put(PayOrderExtKeys.GENERAL_PROJECT_USER_ID, creatorId);
|
||||
}
|
||||
return JSON.toJSONString(ext);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析业务订单号,projectId 必传:
|
||||
* <ul>
|
||||
* <li>传了 bizOrderNo → GP1_{projectId}_{bizOrderNo}(幂等,同 projectId + bizOrderNo 复用)</li>
|
||||
* <li>没传 bizOrderNo → GP2_{projectId}_{timestamp}_{random6}(自动生成,每次唯一)</li>
|
||||
* </ul>
|
||||
*/
|
||||
private String resolveBizOrderNo(String bizOrderNo, String projectId) {
|
||||
String pid = projectId.trim();
|
||||
if (StringUtils.hasText(bizOrderNo)) {
|
||||
return "GP1_" + pid + "_" + bizOrderNo.trim();
|
||||
}
|
||||
long ts = System.currentTimeMillis();
|
||||
int rand = ThreadLocalRandom.current().nextInt(1_000_000);
|
||||
return "GP2_" + pid + "_" + ts + "_" + String.format("%06d", rand);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将前端回跳地址包装为渠道前台回调地址:支付完成后先回到平台后端,再 302 到前端目标页。
|
||||
* 与 Bill H5 一致,避免渠道直接把用户带回 SPA 并携带不可控 query 参数。
|
||||
*/
|
||||
private static String buildChannelFrontNotifyUrl(String frontNotifyUrl) {
|
||||
URI frontendUri = URI.create(frontNotifyUrl);
|
||||
String origin = frontendUri.getScheme() + "://" + frontendUri.getAuthority();
|
||||
String encodedReturnUrl = URLEncoder.encode(frontNotifyUrl, StandardCharsets.UTF_8);
|
||||
return origin + "/api/pay/general/h5/front-notify?returnUrl=" + encodedReturnUrl;
|
||||
}
|
||||
|
||||
private static String validateFrontNotifyUrl(String url) {
|
||||
try {
|
||||
URI uri = URI.create(url);
|
||||
String scheme = uri.getScheme();
|
||||
if (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme)) {
|
||||
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "frontNotifyUrl must start with http or https");
|
||||
}
|
||||
if (!StringUtils.hasText(uri.getHost())) {
|
||||
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "Invalid frontNotifyUrl");
|
||||
}
|
||||
return url;
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "Invalid frontNotifyUrl");
|
||||
}
|
||||
}
|
||||
|
||||
/** App WebView 内禁止 H5 支付(渠道违规),应使用 App 原生 SDK。 */
|
||||
private static void assertH5PayAllowed(HttpServletRequest httpRequest) {
|
||||
PayH5InAppGuard.assertH5PayAllowed(
|
||||
httpRequest.getHeader(PayAppWebViewDetector.HEADER_CLIENT_TYPE),
|
||||
httpRequest.getHeader("User-Agent"));
|
||||
}
|
||||
|
||||
/** App 原生支付仅限 App WebView;手机浏览器请用 H5。 */
|
||||
private static void assertAppNativePayRequired(HttpServletRequest httpRequest) {
|
||||
PayAppNativeInAppGuard.assertAppNativePayRequired(
|
||||
httpRequest.getHeader(PayAppWebViewDetector.HEADER_CLIENT_TYPE),
|
||||
httpRequest.getHeader("User-Agent"));
|
||||
}
|
||||
|
||||
/** 提取客户端 IP,优先 X-Forwarded-For 首段,兜底 getRemoteAddr */
|
||||
private String resolveClientIp(HttpServletRequest httpRequest) {
|
||||
String xff = httpRequest.getHeader("X-Forwarded-For");
|
||||
if (StringUtils.hasText(xff)) {
|
||||
String first = xff.split(",")[0].trim();
|
||||
if (StringUtils.hasText(first)) {
|
||||
return first;
|
||||
}
|
||||
}
|
||||
return httpRequest.getRemoteAddr();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user