diff --git a/_config/sql/update-tables.sql b/_config/sql/update-tables.sql index e69de29bb..838bde61f 100644 --- a/_config/sql/update-tables.sql +++ b/_config/sql/update-tables.sql @@ -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 '支付能力编码(路由回填, 同步时用于解析通道应用凭证)'; diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/client/AlipayChannelClient.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/client/AlipayChannelClient.java index 3274a2e6c..0050686d4 100644 --- a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/client/AlipayChannelClient.java +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/client/AlipayChannelClient.java @@ -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 pay(@RequestBody AlipayPayReq req); + + /// 支付同步(查询支付宝订单状态) + @PostExchange("/channel/alipay/sync") + DaxResult sync(@RequestBody AlipaySyncReq req); } diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/client/req/AlipaySyncReq.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/client/req/AlipaySyncReq.java new file mode 100644 index 000000000..9f7b8cc30 --- /dev/null +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/client/req/AlipaySyncReq.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/client/resp/AlipaySyncResp.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/client/resp/AlipaySyncResp.java new file mode 100644 index 000000000..97b75f318 --- /dev/null +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/client/resp/AlipaySyncResp.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/pay/AlipayPayService.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/payment/pay/AlipayPayService.java similarity index 92% rename from daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/pay/AlipayPayService.java rename to daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/payment/pay/AlipayPayService.java index e3b35bcac..9a3ac106b 100644 --- a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/pay/AlipayPayService.java +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/payment/pay/AlipayPayService.java @@ -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; } } diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/payment/sync/AlipaySyncService.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/payment/sync/AlipaySyncService.java new file mode 100644 index 000000000..2a2552998 --- /dev/null +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/payment/sync/AlipaySyncService.java @@ -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 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(), "支付宝同步查询失败")); + } +} diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/strategy/direct/AlipayDirectPayStrategy.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/strategy/direct/AlipayDirectPayStrategy.java index e76b24780..c0fab8395 100644 --- a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/strategy/direct/AlipayDirectPayStrategy.java +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/strategy/direct/AlipayDirectPayStrategy.java @@ -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; diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/strategy/direct/AlipayDirectSyncStrategy.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/strategy/direct/AlipayDirectSyncStrategy.java new file mode 100644 index 000000000..ed8bc6a1f --- /dev/null +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/strategy/direct/AlipayDirectSyncStrategy.java @@ -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); + } +} diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/strategy/isv/AlipayIsvPayStrategy.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/strategy/isv/AlipayIsvPayStrategy.java index 38927adbd..16e7ea3a7 100644 --- a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/strategy/isv/AlipayIsvPayStrategy.java +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/strategy/isv/AlipayIsvPayStrategy.java @@ -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; diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/pay/order/entity/NormalPayOrder.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/pay/order/entity/NormalPayOrder.java index bff2a8f6b..55da330ff 100644 --- a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/pay/order/entity/NormalPayOrder.java +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/pay/order/entity/NormalPayOrder.java @@ -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; + // ===== 请求信息(低频,审计排查用)===== /// 通道附加参数 diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/pay/service/PayAssistService.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/pay/service/PayAssistService.java index ca4ddf944..de32e8193 100644 --- a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/pay/service/PayAssistService.java +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/pay/service/PayAssistService.java @@ -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