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:
DaxPay Dev
2026-07-01 19:09:29 +08:00
parent 58fef669c7
commit b28de59032
11 changed files with 254 additions and 13 deletions

View File

@@ -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 '支付能力编码(路由回填, 同步时用于解析通道应用凭证)';

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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(), "支付宝同步查询失败"));
}
}

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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;
// ===== 请求信息(低频,审计排查用)=====
/// 通道附加参数

View File

@@ -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