mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-11 15:15:33 +08:00
feat(alipay): 实现支付同步接口, 完整调用链路接入平台同步基础设施
- SQL: pay_normal_order 新增 channel_mch_no / capability 字段(同步时凭证解析) - NormalPayOrder 实体加对应字段, PayAssistService 写入路由参数 - 主应用: AlipayDirectSyncStrategy extends AbsSyncPayOrderStrategy - 主应用: AlipaySyncService 映射 trade_status → PayFundStatusEnum - 主应用: AlipayChannelClient 加 sync() 方法 - 子应用: Controller 加 /sync 端点, Service 加 sync()(alipay.trade.query) - 与商业版一致的同步状态映射: TRADE_NOT_EXIST→PROCESSING, TRADE_CLOSED+有付款时间→SUCCESS(全额退款), TRADE_CLOSED→CLOSE
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
-- 支付同步: pay_normal_order 冗余通道路由参数, 供同步时凭证解析使用
|
||||
|
||||
ALTER TABLE pay_normal_order ADD COLUMN IF NOT EXISTS channel_mch_no varchar(64);
|
||||
ALTER TABLE pay_normal_order ADD COLUMN IF NOT EXISTS capability varchar(64);
|
||||
|
||||
COMMENT ON COLUMN pay_normal_order.channel_mch_no IS '通道商户号(路由回填, 同步时用于解析通道应用凭证)';
|
||||
COMMENT ON COLUMN pay_normal_order.capability IS '支付能力编码(路由回填, 同步时用于解析通道应用凭证)';
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package cn.daxpay.open.channel.alipay.client;
|
||||
|
||||
import cn.daxpay.open.channel.alipay.client.req.AlipayPayReq;
|
||||
import cn.daxpay.open.channel.alipay.client.req.AlipaySyncReq;
|
||||
import cn.daxpay.open.channel.alipay.client.resp.AlipayPayResp;
|
||||
import cn.daxpay.open.channel.alipay.client.resp.AlipaySyncResp;
|
||||
import cn.daxpay.open.payment.common.result.DaxResult;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
@@ -17,4 +19,8 @@ public interface AlipayChannelClient {
|
||||
/// 支付下单
|
||||
@PostExchange("/channel/alipay/pay")
|
||||
DaxResult<AlipayPayResp> pay(@RequestBody AlipayPayReq req);
|
||||
|
||||
/// 支付同步(查询支付宝订单状态)
|
||||
@PostExchange("/channel/alipay/sync")
|
||||
DaxResult<AlipaySyncResp> sync(@RequestBody AlipaySyncReq req);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package cn.daxpay.open.channel.alipay.client.req;
|
||||
|
||||
import cn.daxpay.open.channel.alipay.client.credential.AlipaySdkCredential;
|
||||
import lombok.Data;
|
||||
|
||||
/// # 支付宝通道同步请求
|
||||
///
|
||||
/// 与子应用 dax-pay-channel-one 的 `AlipaySyncReq` 镜像, 经声明式 HTTP 客户端序列化传输, 字段对齐。
|
||||
@Data
|
||||
public class AlipaySyncReq {
|
||||
|
||||
/// 商户订单号(主应用支付交易号, 作为支付宝 out_trade_no)
|
||||
private String outTradeNo;
|
||||
|
||||
/// 支付宝交易号(trade_no, 下单成功后由支付宝返回, 可选)
|
||||
private String tradeNo;
|
||||
|
||||
/// 通道调用凭证
|
||||
private AlipaySdkCredential credential;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package cn.daxpay.open.channel.alipay.client.resp;
|
||||
|
||||
import cn.daxpay.open.channel.alipay.service.payment.sync.AlipaySyncService;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
/// # 支付宝通道同步响应
|
||||
///
|
||||
/// 与子应用 dax-pay-channel-one 的 `AlipaySyncResp` 镜像, 字段对齐。
|
||||
/// 子应用原样回传支付宝 trade.query 字段, trade_status 映射由主应用 [AlipaySyncService] 完成。
|
||||
@Data
|
||||
public class AlipaySyncResp {
|
||||
|
||||
/// 交易状态(WAIT_BUYER_PAY / TRADE_SUCCESS / TRADE_FINISHED / TRADE_CLOSED)
|
||||
private String tradeStatus;
|
||||
|
||||
/// 网关返回码(code, 10000=成功)
|
||||
private String code;
|
||||
|
||||
/// 业务返回码(sub_code, 如 ACQ.TRADE_NOT_EXIST)
|
||||
private String subCode;
|
||||
|
||||
/// 业务返回消息(sub_msg)
|
||||
private String subMsg;
|
||||
|
||||
/// 支付宝交易号(trade_no)
|
||||
private String tradeNo;
|
||||
|
||||
/// 商户订单号(out_trade_no, 透传请求)
|
||||
private String outTradeNo;
|
||||
|
||||
/// 交易付款时间(send_pay_date)
|
||||
private OffsetDateTime sendPayDate;
|
||||
|
||||
/// 买家支付宝用户ID(buyer_user_id)
|
||||
private String buyerUserId;
|
||||
|
||||
/// 买家支付宝开放ID(buyer_open_id)
|
||||
private String buyerOpenId;
|
||||
|
||||
/// 买家实付金额(buyer_pay_amount, 单位: 分)
|
||||
private Long buyerPayAmount;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.daxpay.open.channel.alipay.service.pay;
|
||||
package cn.daxpay.open.channel.alipay.service.payment.pay;
|
||||
|
||||
import cn.daxpay.open.channel.alipay.client.AlipayChannelClient;
|
||||
import cn.daxpay.open.channel.alipay.client.credential.AlipaySdkCredential;
|
||||
@@ -91,18 +91,11 @@ public class AlipayPayService {
|
||||
}
|
||||
|
||||
// 完成时间
|
||||
if (resp.getFinishTime() != null) {
|
||||
bo.setFinishTime(resp.getFinishTime());
|
||||
}
|
||||
bo.setFinishTime(resp.getFinishTime());
|
||||
// 实付金额(BARCODE 付款码同步成功时返回)
|
||||
if (resp.getRealAmount() != null) {
|
||||
bo.setRealAmount(resp.getRealAmount());
|
||||
}
|
||||
bo.setRealAmount(resp.getRealAmount());
|
||||
// 买家标识(BARCODE 付款码同步成功时返回)
|
||||
if (StrUtil.isNotBlank(resp.getBuyerId())) {
|
||||
bo.setBuyerId(resp.getBuyerId());
|
||||
}
|
||||
|
||||
bo.setBuyerId(resp.getBuyerId());
|
||||
return bo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package cn.daxpay.open.channel.alipay.service.payment.sync;
|
||||
|
||||
import cn.daxpay.open.channel.alipay.client.AlipayChannelClient;
|
||||
import cn.daxpay.open.channel.alipay.client.credential.AlipaySdkCredential;
|
||||
import cn.daxpay.open.channel.alipay.client.req.AlipaySyncReq;
|
||||
import cn.daxpay.open.channel.alipay.client.resp.AlipaySyncResp;
|
||||
import cn.daxpay.open.payment.common.result.DaxResult;
|
||||
import cn.daxpay.open.payment.common.enums.PayFundStatusEnum;
|
||||
import cn.daxpay.open.payment.pay.bo.PaySyncResultBo;
|
||||
import cn.daxpay.open.payment.pay.order.entity.PayTrade;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/// # 支付宝支付同步业务服务
|
||||
///
|
||||
/// 通过 [AlipayChannelClient] 调用子应用 dax-pay-channel-one 查询支付宝订单状态,
|
||||
/// 将支付宝 trade_status 映射为平台 [PayFundStatusEnum]。
|
||||
///
|
||||
/// 映射规则(参照商业版 AlipaySyncService):
|
||||
/// - TRADE_SUCCESS / TRADE_FINISHED → SUCCESS
|
||||
/// - WAIT_BUYER_PAY → PROCESSING
|
||||
/// - TRADE_CLOSED + 有付款时间 → SUCCESS(已退款)
|
||||
/// - TRADE_CLOSED + 无付款时间 → CLOSE
|
||||
/// - ACQ.TRADE_NOT_EXIST → PROCESSING(客户未操作, 订单未创建)
|
||||
/// - 其他查询失败 → syncSuccess=false
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AlipaySyncService {
|
||||
|
||||
private final AlipayChannelClient alipayChannelClient;
|
||||
|
||||
/// 支付宝交易状态: 交易支付成功
|
||||
private static final String TRADE_SUCCESS = "TRADE_SUCCESS";
|
||||
/// 支付宝交易状态: 交易结束(不可退款)
|
||||
private static final String TRADE_FINISHED = "TRADE_FINISHED";
|
||||
/// 支付宝交易状态: 等待买家付款
|
||||
private static final String WAIT_BUYER_PAY = "WAIT_BUYER_PAY";
|
||||
/// 支付宝交易状态: 未付款超时关闭, 或支付完成后全额退款
|
||||
private static final String TRADE_CLOSED = "TRADE_CLOSED";
|
||||
/// 支付宝业务码: 交易不存在
|
||||
private static final String ACQ_TRADE_NOT_EXIST = "ACQ.TRADE_NOT_EXIST";
|
||||
|
||||
/// 执行支付宝支付同步
|
||||
///
|
||||
/// @param trade 支付订单(tradeNo 作为 out_trade_no, outOrderNo 作为 trade_no)
|
||||
/// @param credential 通道调用凭证
|
||||
/// @return 同步结果(含映射后的资金状态)
|
||||
public PaySyncResultBo sync(PayTrade trade, AlipaySdkCredential credential) {
|
||||
// 构建请求
|
||||
AlipaySyncReq req = new AlipaySyncReq();
|
||||
req.setOutTradeNo(trade.getTradeNo());
|
||||
req.setTradeNo(trade.getOutOrderNo());
|
||||
req.setCredential(credential);
|
||||
|
||||
// 调用子应用
|
||||
DaxResult<AlipaySyncResp> result = alipayChannelClient.sync(req);
|
||||
if (result.getCode() != 0) {
|
||||
log.error("支付宝通道同步失败: outTradeNo={}, msg={}", trade.getTradeNo(), result.getMsg());
|
||||
PaySyncResultBo bo = new PaySyncResultBo();
|
||||
bo.setSyncSuccess(false)
|
||||
.setSyncErrorMsg(result.getMsg());
|
||||
return bo;
|
||||
}
|
||||
|
||||
return toSyncResult(result.getData());
|
||||
}
|
||||
|
||||
/// 解析子应用响应, 映射 trade_status → [PayFundStatusEnum]
|
||||
private PaySyncResultBo toSyncResult(AlipaySyncResp resp) {
|
||||
PaySyncResultBo bo = new PaySyncResultBo();
|
||||
bo.setOutOrderNo(resp.getTradeNo());
|
||||
|
||||
String tradeStatus = resp.getTradeStatus();
|
||||
|
||||
// 支付完成(TRADE_SUCCESS / TRADE_FINISHED)
|
||||
if (TRADE_SUCCESS.equals(tradeStatus) || TRADE_FINISHED.equals(tradeStatus)) {
|
||||
return bo.setPayStatus(PayFundStatusEnum.SUCCESS)
|
||||
.setFinishTime(resp.getSendPayDate())
|
||||
.setRealAmount(resp.getBuyerPayAmount())
|
||||
.setBuyerId(StrUtil.blankToDefault(resp.getBuyerOpenId(), resp.getBuyerUserId()));
|
||||
}
|
||||
|
||||
// 待支付(等待买家付款)
|
||||
if (WAIT_BUYER_PAY.equals(tradeStatus)) {
|
||||
return bo.setPayStatus(PayFundStatusEnum.PROCESSING);
|
||||
}
|
||||
|
||||
// 已关闭或支付完成后全额退款
|
||||
if (TRADE_CLOSED.equals(tradeStatus)) {
|
||||
// 有付款时间说明是支付后全额退款 → SUCCESS; 无付款时间 → CLOSE
|
||||
if (resp.getSendPayDate() != null) {
|
||||
return bo.setPayStatus(PayFundStatusEnum.SUCCESS)
|
||||
.setFinishTime(resp.getSendPayDate());
|
||||
}
|
||||
return bo.setPayStatus(PayFundStatusEnum.CLOSE);
|
||||
}
|
||||
|
||||
// 交易不存在(客户未操作, 订单未创建) → PROCESSING
|
||||
if (ACQ_TRADE_NOT_EXIST.equals(resp.getSubCode())) {
|
||||
return bo.setPayStatus(PayFundStatusEnum.PROCESSING);
|
||||
}
|
||||
|
||||
// 查询失败
|
||||
return bo.setSyncSuccess(false)
|
||||
.setSyncErrorCode(resp.getSubCode())
|
||||
.setSyncErrorMsg(StrUtil.blankToDefault(resp.getSubMsg(), "支付宝同步查询失败"));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package cn.daxpay.open.channel.alipay.strategy.direct;
|
||||
|
||||
import cn.daxpay.open.channel.alipay.service.direct.AlipayDirectConfigAssembler;
|
||||
import cn.daxpay.open.channel.alipay.service.pay.AlipayPayService;
|
||||
import cn.daxpay.open.channel.alipay.service.payment.pay.AlipayPayService;
|
||||
import cn.daxpay.open.payment.common.context.PayStrategyContext;
|
||||
import cn.daxpay.open.payment.pay.bo.PayTradeResultBo;
|
||||
import cn.daxpay.open.payment.strategy.pay.AbsPayStrategy;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package cn.daxpay.open.channel.alipay.strategy.direct;
|
||||
|
||||
import cn.daxpay.open.channel.alipay.client.credential.AlipaySdkCredential;
|
||||
import cn.daxpay.open.channel.alipay.service.direct.AlipayDirectConfigAssembler;
|
||||
import cn.daxpay.open.channel.alipay.service.payment.sync.AlipaySyncService;
|
||||
import cn.daxpay.open.payment.pay.bo.PaySyncResultBo;
|
||||
import cn.daxpay.open.payment.pay.order.dao.NormalPayOrderManager;
|
||||
import cn.daxpay.open.payment.pay.order.entity.NormalPayOrder;
|
||||
import cn.daxpay.open.payment.pay.order.entity.PayTrade;
|
||||
import cn.daxpay.open.payment.strategy.sync.AbsSyncPayOrderStrategy;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/// # 支付宝直连支付同步策略
|
||||
///
|
||||
/// 支付宝直连模式(ProductEnum.ALIPAY)下的支付同步策略。
|
||||
/// 从容器层 NormalPayOrder 读取通道路由参数(channelMchNo / capability),
|
||||
/// 组装通道凭证(委托 [AlipayDirectConfigAssembler]), 同步执行委托给 [AlipaySyncService]。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AlipayDirectSyncStrategy extends AbsSyncPayOrderStrategy {
|
||||
|
||||
private final AlipaySyncService alipaySyncService;
|
||||
private final AlipayDirectConfigAssembler alipayDirectConfigAssembler;
|
||||
private final NormalPayOrderManager normalPayOrderManager;
|
||||
|
||||
@Override
|
||||
public ProductEnum getProduct() {
|
||||
return ProductEnum.ALIPAY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaySyncResultBo doSync(PayTrade trade) {
|
||||
// 从容器层读取通道路由参数, 用于凭证解析
|
||||
NormalPayOrder normalOrder = normalPayOrderManager.findById(trade.getContainerId()).orElse(null);
|
||||
String channelMchNo = normalOrder != null ? normalOrder.getChannelMchNo() : null;
|
||||
String capability = normalOrder != null ? normalOrder.getCapability() : null;
|
||||
|
||||
// 组装通道调用凭证
|
||||
AlipaySdkCredential credential = alipayDirectConfigAssembler.buildConfig(
|
||||
trade.getMchNo(), channelMchNo, capability);
|
||||
|
||||
return alipaySyncService.sync(trade, credential);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import cn.daxpay.open.channel.alipay.entity.isv.AlipayIsvApp;
|
||||
import cn.daxpay.open.channel.alipay.entity.isv.AlipayIsvAppKeyConfig;
|
||||
import cn.daxpay.open.channel.alipay.entity.isv.AlipayIsvChannelMerchant;
|
||||
import cn.daxpay.open.channel.alipay.service.isv.AlipayIsvAppKeyConfigService;
|
||||
import cn.daxpay.open.channel.alipay.service.pay.AlipayPayService;
|
||||
import cn.daxpay.open.channel.alipay.service.payment.pay.AlipayPayService;
|
||||
import cn.daxpay.open.payment.common.context.PayStrategyContext;
|
||||
import cn.daxpay.open.payment.pay.bo.PayTradeResultBo;
|
||||
import cn.daxpay.open.payment.strategy.pay.AbsPayStrategy;
|
||||
|
||||
@@ -88,6 +88,15 @@ public class NormalPayOrder extends MchBaseEntity {
|
||||
/// 关闭时间
|
||||
private OffsetDateTime closeTime;
|
||||
|
||||
// ===== 通道路由(同步时用于解析通道应用凭证)=====
|
||||
|
||||
/// 通道商户号(路由回填)
|
||||
private String channelMchNo;
|
||||
|
||||
/// 支付能力编码(路由回填)
|
||||
/// @see cn.daxpay.open.platform.core.enums.pay.channel.PayCapabilityEnum
|
||||
private String capability;
|
||||
|
||||
// ===== 请求信息(低频,审计排查用)=====
|
||||
|
||||
/// 通道附加参数
|
||||
|
||||
@@ -83,6 +83,9 @@ public class PayAssistService {
|
||||
normalOrder.setExtraParam(payParam.getExtraParam());
|
||||
normalOrder.setGoodsDetail(payParam.getGoodsDetail());
|
||||
normalOrder.setTerminalNo(terminalNo);
|
||||
// 通道路由参数(同步时用于解析通道应用凭证)
|
||||
normalOrder.setChannelMchNo(payParam.getChannelMchNo());
|
||||
normalOrder.setCapability(payParam.getCapability());
|
||||
normalOrder.setAppId(appId);
|
||||
payNormalOrderManager.save(normalOrder);
|
||||
// 创建资金交易 PayTrade
|
||||
|
||||
Reference in New Issue
Block a user