mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-12 23:45:39 +08:00
feat(vbill): 随行付(天阙科技)服务商通道对接(进件/支付/退款/同步/回调) + 错误码i18n
This commit is contained in:
16
daxpay-channel/daxpay-channel-vbill/pom.xml
Normal file
16
daxpay-channel/daxpay-channel-vbill/pom.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>cn.daxpay.open</groupId>
|
||||
<artifactId>daxpay-channel</artifactId>
|
||||
<version>4.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>daxpay-channel-vbill</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<description>随行付支付通道</description>
|
||||
</project>
|
||||
@@ -0,0 +1,16 @@
|
||||
package cn.daxpay.open.channel.vbill;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
|
||||
/// # 随行付支付实现
|
||||
///
|
||||
@AutoConfiguration
|
||||
@ConfigurationPropertiesScan
|
||||
@MapperScan(annotationClass = Mapper.class)
|
||||
@ComponentScan
|
||||
public class VbillChannelApp {
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package cn.daxpay.open.channel.vbill.client;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.client.req.VbillCloseReq;
|
||||
import cn.daxpay.open.channel.vbill.client.req.VbillPayReq;
|
||||
import cn.daxpay.open.channel.vbill.client.req.VbillRefundReq;
|
||||
import cn.daxpay.open.channel.vbill.client.req.VbillRefundSyncReq;
|
||||
import cn.daxpay.open.channel.vbill.client.req.VbillSyncReq;
|
||||
import cn.daxpay.open.channel.vbill.client.resp.VbillCloseResp;
|
||||
import cn.daxpay.open.channel.vbill.client.resp.VbillPayResp;
|
||||
import cn.daxpay.open.channel.vbill.client.resp.VbillRefundResp;
|
||||
import cn.daxpay.open.channel.vbill.client.resp.VbillRefundSyncResp;
|
||||
import cn.daxpay.open.channel.vbill.client.resp.VbillSyncResp;
|
||||
import cn.daxpay.open.payment.common.result.DaxResult;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.service.annotation.HttpExchange;
|
||||
import org.springframework.web.service.annotation.PostExchange;
|
||||
|
||||
/// # 随行付通道客户端
|
||||
///
|
||||
/// 声明式 HTTP 接口, 调用子应用 dax-pay-channel-two 的随行付通道接口。
|
||||
/// 随行付为聚合服务商模式, 不区分直连/服务商, 路径前缀 `/channel/vbill`。
|
||||
@HttpExchange(accept = MediaType.APPLICATION_JSON_VALUE, contentType = MediaType.APPLICATION_JSON_VALUE)
|
||||
public interface VbillChannelClient {
|
||||
|
||||
/// 支付下单
|
||||
@PostExchange("/channel/vbill/pay")
|
||||
DaxResult<VbillPayResp> pay(@RequestBody VbillPayReq req);
|
||||
|
||||
/// 支付同步(查询随行付订单状态)
|
||||
@PostExchange("/channel/vbill/sync")
|
||||
DaxResult<VbillSyncResp> sync(@RequestBody VbillSyncReq req);
|
||||
|
||||
/// 关闭订单
|
||||
@PostExchange("/channel/vbill/close")
|
||||
DaxResult<VbillCloseResp> close(@RequestBody VbillCloseReq req);
|
||||
|
||||
/// 退款
|
||||
@PostExchange("/channel/vbill/refund")
|
||||
DaxResult<VbillRefundResp> refund(@RequestBody VbillRefundReq req);
|
||||
|
||||
/// 退款同步(查询退款状态)
|
||||
@PostExchange("/channel/vbill/refund-sync")
|
||||
DaxResult<VbillRefundSyncResp> refundSync(@RequestBody VbillRefundSyncReq req);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package cn.daxpay.open.channel.vbill.client.credential;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/// # 随行付 SDK 凭证(主应用侧)
|
||||
///
|
||||
/// 主应用从服务商密钥配置(VbillIsvKeyConfig) + 通道商户绑定(VbillIsvChannelMerchant) 提取
|
||||
/// 机构号 / 私钥 / 公钥 / 商户号后组装, 经声明式 HTTP 客户端下发给子应用 dax-pay-channel-two。
|
||||
///
|
||||
/// 与子应用 [cn.daxpay.open.channel.vbill.config.VbillSdkCredential] 字段对称, 但属于主应用 client 包,
|
||||
/// 独立序列化以避免主子应用包路径耦合。
|
||||
@Data
|
||||
public class VbillSdkCredential {
|
||||
|
||||
/// 天阙合作机构ID(orgId)
|
||||
private String orgId;
|
||||
|
||||
/// 商户RSA私钥(Base64, PKCS8, 去头尾)
|
||||
private String privateKey;
|
||||
|
||||
/// 天阙RSA公钥(Base64, X509, 去头尾, 用于响应/回调验签)
|
||||
private String publicKey;
|
||||
|
||||
/// 天阙商户号(mno, 子商户级)
|
||||
private String mno;
|
||||
|
||||
/// 是否沙箱环境
|
||||
private Boolean sandbox;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package cn.daxpay.open.channel.vbill.client.enums;
|
||||
|
||||
/// # 随行付支付内容类型(主应用侧)
|
||||
///
|
||||
/// 与子应用 [cn.daxpay.open.channel.vbill.enums.VbillPayBodyType] 对称。
|
||||
public enum VbillPayBodyType {
|
||||
/// 跳转链接(银联 JSAPI redirectUrl)
|
||||
LINK,
|
||||
/// 二维码内容(扫码支付 payUrl)
|
||||
QR_CODE,
|
||||
/// JSAPI/小程序调起参数 JSON(微信/收银台调起参数)
|
||||
JSAPI,
|
||||
/// 通用标识码(支付宝 JSAPI source)
|
||||
IDENTIFIER;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package cn.daxpay.open.channel.vbill.client.enums;
|
||||
|
||||
/// # 随行付支付方式(主应用侧)
|
||||
///
|
||||
/// 与子应用 [cn.daxpay.open.channel.vbill.enums.VbillPayMethod] 对称。
|
||||
public enum VbillPayMethod {
|
||||
/// 聚合支付(统一下单, JSAPI/小程序)
|
||||
UNI_PAY,
|
||||
/// 扫码支付(主扫, 返回二维码)
|
||||
QR_CODE,
|
||||
/// 付款码支付(被扫, 同步扣款)
|
||||
BAR_CODE,
|
||||
/// 小程序收银台
|
||||
APPLET_CASHIER;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package cn.daxpay.open.channel.vbill.client.req;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.client.credential.VbillSdkCredential;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class VbillCloseReq {
|
||||
|
||||
@NotNull(message = "{validation.field.credential.notNull}")
|
||||
private VbillSdkCredential credential;
|
||||
|
||||
/// 随行付网关订单号(uuid)
|
||||
private String outOrderNo;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package cn.daxpay.open.channel.vbill.client.req;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.client.credential.VbillSdkCredential;
|
||||
import cn.daxpay.open.channel.vbill.client.enums.VbillPayBodyType;
|
||||
import cn.daxpay.open.channel.vbill.client.enums.VbillPayMethod;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
/// # 随行付通道支付请求(主应用侧)
|
||||
///
|
||||
/// 字段与子应用 [cn.daxpay.open.channel.vbill.req.VbillPayReq] 对称。
|
||||
@Data
|
||||
public class VbillPayReq {
|
||||
|
||||
/// 通道调用凭证
|
||||
@NotNull(message = "{validation.field.credential.notNull}")
|
||||
private VbillSdkCredential credential;
|
||||
|
||||
/// 商户订单号(主应用支付交易号)
|
||||
@NotBlank(message = "{validation.field.outTradeNo.notBlank}")
|
||||
private String outTradeNo;
|
||||
|
||||
/// 订单金额(单位: 分)
|
||||
@NotNull(message = "{validation.field.amount.notNull}")
|
||||
@Positive(message = "{validation.field.amount.positive}")
|
||||
private Long amount;
|
||||
|
||||
/// 商品标题
|
||||
@NotBlank(message = "{validation.field.title.notBlank}")
|
||||
private String title;
|
||||
|
||||
/// 商品描述(随行付接口不使用, 仅 DTO 对称保留)
|
||||
private String description;
|
||||
|
||||
/// 支付方式
|
||||
@NotNull(message = "{validation.field.method.notNull}")
|
||||
private VbillPayMethod method;
|
||||
|
||||
/// 聚合支付底层渠道标识(UNI_PAY 必填): WECHAT / ALIPAY / UNIONPAY
|
||||
private String payType;
|
||||
|
||||
/// 聚合支付方式(UNI_PAY 必填): 02 公众号/JSAPI, 03 微信小程序
|
||||
private String payWay;
|
||||
|
||||
/// 微信 AppId(UNI_PAY 微信场景, 可选)
|
||||
private String wxAppId;
|
||||
|
||||
/// 用户标识(UNI_PAY 必填): 微信 openid / 支付宝 buyerId
|
||||
private String openId;
|
||||
|
||||
/// 付款码(BAR_CODE 必填)
|
||||
private String authCode;
|
||||
|
||||
/// 小程序收银台类型(APPLET_CASHIER 用): 00 小程序支付插件 / 01 半屏小程序收银台
|
||||
private String appletSource;
|
||||
|
||||
/// 客户端IP
|
||||
private String clientIp;
|
||||
|
||||
/// 异步通知地址(由子应用透传给随行付)
|
||||
private String notifyUrl;
|
||||
|
||||
/// 订单过期时间
|
||||
private OffsetDateTime expireTime;
|
||||
|
||||
/// 支付内容类型(主应用预先计算好, 子应用透传到响应)
|
||||
private VbillPayBodyType payBodyType;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package cn.daxpay.open.channel.vbill.client.req;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.client.credential.VbillSdkCredential;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class VbillRefundReq {
|
||||
|
||||
@NotNull(message = "{validation.field.credential.notNull}")
|
||||
private VbillSdkCredential credential;
|
||||
|
||||
/// 商户退款单号(主应用退款交易号)
|
||||
@NotBlank(message = "{validation.field.outRefundNo.notBlank}")
|
||||
private String outRefundNo;
|
||||
|
||||
/// 原支付订单的网关订单号(随行付 uuid)
|
||||
@NotBlank(message = "{validation.field.outOrderNo.notBlank}")
|
||||
private String outOrderNo;
|
||||
|
||||
/// 退款金额(单位: 分)
|
||||
@NotNull(message = "{validation.field.amount.notNull}")
|
||||
@Positive(message = "{validation.field.amount.positive}")
|
||||
private Long amount;
|
||||
|
||||
/// 退款原因
|
||||
private String reason;
|
||||
|
||||
/// 退款异步通知地址(由子应用透传给随行付)
|
||||
private String notifyUrl;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package cn.daxpay.open.channel.vbill.client.req;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.client.credential.VbillSdkCredential;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class VbillRefundSyncReq {
|
||||
|
||||
@NotNull(message = "{validation.field.credential.notNull}")
|
||||
private VbillSdkCredential credential;
|
||||
|
||||
/// 商户退款单号(主应用退款交易号, 对应随行付 ordNo)
|
||||
private String outRefundNo;
|
||||
|
||||
/// 随行付网关退款单号(uuid, 优先使用)
|
||||
private String outRefundOrderNo;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package cn.daxpay.open.channel.vbill.client.req;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.client.credential.VbillSdkCredential;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class VbillSyncReq {
|
||||
|
||||
@NotNull(message = "{validation.field.credential.notNull}")
|
||||
private VbillSdkCredential credential;
|
||||
|
||||
/// 商户订单号(主应用支付交易号, 对应随行付 ordNo)
|
||||
private String outTradeNo;
|
||||
|
||||
/// 随行付网关订单号(uuid, 优先使用)
|
||||
private String outOrderNo;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package cn.daxpay.open.channel.vbill.client.resp;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class VbillCloseResp {
|
||||
/// 关单无额外业务返回字段
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package cn.daxpay.open.channel.vbill.client.resp;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.client.enums.VbillPayBodyType;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
@Data
|
||||
public class VbillPayResp {
|
||||
|
||||
/// 商户订单号(透传 VbillPayReq.outTradeNo)
|
||||
private String outTradeNo;
|
||||
|
||||
/// 随行付网关订单号(uuid)
|
||||
private String outOrderNo;
|
||||
|
||||
/// 支付内容(二维码链接 / JSAPI 调起参数 JSON / 跳转链接)
|
||||
private String payBody;
|
||||
|
||||
/// 支付内容类型
|
||||
private VbillPayBodyType payBodyType;
|
||||
|
||||
/// 是否已终态完成(付款码同步成功时为 true)
|
||||
private Boolean complete;
|
||||
|
||||
/// 订单总金额(单位: 分, 付款码同步成功时返回)
|
||||
private Long totalAmount;
|
||||
|
||||
/// 用户实付金额(单位: 分, 付款码同步成功时返回, 来自 settleAmt)
|
||||
private Long realAmount;
|
||||
|
||||
/// 完成时间
|
||||
private OffsetDateTime finishTime;
|
||||
|
||||
/// 买家标识
|
||||
private String buyerId;
|
||||
|
||||
/// 渠道交易单号(transactionId)
|
||||
private String transOrderNo;
|
||||
|
||||
/// 支付厂商(WECHAT/ALIPAY/UNIONPAY)
|
||||
private String tradeProduct;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package cn.daxpay.open.channel.vbill.client.resp;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
@Data
|
||||
public class VbillRefundResp {
|
||||
|
||||
/// 商户退款单号
|
||||
private String outRefundNo;
|
||||
|
||||
/// 随行付网关退款单号(uuid)
|
||||
private String outRefundOrderNo;
|
||||
|
||||
/// 是否已终态完成(REFUNDSUC / REFUNDFAIL 时为 true)
|
||||
private Boolean complete;
|
||||
|
||||
/// 退款完成时间
|
||||
private OffsetDateTime finishTime;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package cn.daxpay.open.channel.vbill.client.resp;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
@Data
|
||||
public class VbillRefundSyncResp {
|
||||
|
||||
/// 商户退款单号
|
||||
private String outRefundNo;
|
||||
|
||||
/// 随行付网关退款单号(uuid)
|
||||
private String outRefundOrderNo;
|
||||
|
||||
/// 退款状态(REFUNDSUC / REFUNDFAIL / REFUNDING)
|
||||
private String refundStatus;
|
||||
|
||||
/// 退款完成时间
|
||||
private OffsetDateTime finishTime;
|
||||
|
||||
/// 同步原始数据
|
||||
private String syncData;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package cn.daxpay.open.channel.vbill.client.resp;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
@Data
|
||||
public class VbillSyncResp {
|
||||
|
||||
/// 商户订单号
|
||||
private String outTradeNo;
|
||||
|
||||
/// 随行付网关订单号(uuid)
|
||||
private String outOrderNo;
|
||||
|
||||
/// 交易状态(SUCCESS / PAYING / FAIL / CLOSED)
|
||||
private String tradeState;
|
||||
|
||||
/// 订单总金额(单位: 分)
|
||||
private Long totalAmount;
|
||||
|
||||
/// 实付金额(单位: 分)
|
||||
private Long realAmount;
|
||||
|
||||
/// 完成时间
|
||||
private OffsetDateTime finishTime;
|
||||
|
||||
/// 支付厂商
|
||||
private String tradeProduct;
|
||||
|
||||
/// 支付方式
|
||||
private String tradeWay;
|
||||
|
||||
/// 买家标识
|
||||
private String buyerId;
|
||||
|
||||
/// 渠道交易单号
|
||||
private String transOrderNo;
|
||||
|
||||
/// 同步原始数据
|
||||
private String syncData;
|
||||
|
||||
/// 同步错误信息
|
||||
private String syncErrorMsg;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package cn.daxpay.open.channel.vbill.config;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.client.VbillChannelClient;
|
||||
import cn.daxpay.open.platform.common.config.properties.DaxpayChannelProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.client.support.RestClientAdapter;
|
||||
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
|
||||
|
||||
/// # 随行付通道客户端配置
|
||||
///
|
||||
/// 复用全局 [RestClient](由 common-spring 的 RestClientConfiguration 提供, 已预装:
|
||||
/// OTel traceparent 透传 / BusinessContextInterceptor 业务上下文透传 / Apache HttpClient5 连接池),
|
||||
/// 经 `mutate()` 派生出绑定 channel-two baseUrl 的实例。
|
||||
///
|
||||
/// baseUrl 从 [DaxpayChannelProperties] 统一读取。
|
||||
@Configuration
|
||||
public class VbillClientConfig {
|
||||
|
||||
@Bean
|
||||
public VbillChannelClient vbillChannelClient(
|
||||
RestClient restClient,
|
||||
DaxpayChannelProperties channelProperties) {
|
||||
// mutate 派生: 继承全局 RestClient 的拦截器 / observation / requestFactory, 仅覆盖 baseUrl
|
||||
RestClient channelClient = restClient
|
||||
.mutate()
|
||||
.baseUrl(channelProperties.getTwo().getBaseUrl())
|
||||
.build();
|
||||
return HttpServiceProxyFactory
|
||||
.builderFor(RestClientAdapter.create(channelClient))
|
||||
.build()
|
||||
.createClient(VbillChannelClient.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package cn.daxpay.open.channel.vbill.controller.callback;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.service.callback.VbillPayCallbackService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/// # 随行付支付回调通知控制器
|
||||
///
|
||||
/// 随行付(天阙科技)异步通知入口(支付), 不走 Sa-Token 认证(由安全配置放行 `/unipay/callback/**`)。
|
||||
/// 随行付回调验签只需全局服务商公钥(从 VbillIsvKeyConfig 读取), 不需 channelMchNo,
|
||||
/// 凭 ordNo 反查 PayTrade。
|
||||
@Tag(name = "随行付支付回调通知控制器")
|
||||
@RestController
|
||||
@RequestMapping("/unipay/callback/{mchNo}/{appId}/vbill")
|
||||
@RequiredArgsConstructor
|
||||
public class VbillCallbackController {
|
||||
|
||||
private final VbillPayCallbackService vbillPayCallbackService;
|
||||
|
||||
/// 随行付支付回调
|
||||
///
|
||||
/// 响应格式: `{"code":"success","msg":"成功"}` 表示已收到, 其他视为失败(随行付会重试)
|
||||
@Operation(summary = "随行付支付回调")
|
||||
@PostMapping("/pay")
|
||||
public Map<String, String> payNotify(@PathVariable("mchNo") String mchNo,
|
||||
@PathVariable("appId") String appId,
|
||||
HttpServletRequest request) {
|
||||
return vbillPayCallbackService.payHandle(request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package cn.daxpay.open.channel.vbill.controller.isv;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.param.isv.VbillIsvChannelMerchantCreateParam;
|
||||
import cn.daxpay.open.channel.vbill.result.isv.VbillIsvChannelMerchantResult;
|
||||
import cn.daxpay.open.channel.vbill.service.isv.VbillIsvChannelMerchantService;
|
||||
import cn.daxpay.open.platform.core.annotation.PermCode;
|
||||
import cn.daxpay.open.platform.core.rest.Res;
|
||||
import cn.daxpay.open.platform.core.rest.result.Result;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
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.RestController;
|
||||
|
||||
/// # 随行付通道商户管理
|
||||
@PermCode(menuCode = "channel:merchant")
|
||||
@Validated
|
||||
@Tag(name = "随行付通道商户管理")
|
||||
@RestController
|
||||
@RequestMapping("/admin/vbill/isv-channel-merchant")
|
||||
@RequiredArgsConstructor
|
||||
public class VbillIsvChannelMerchantController {
|
||||
|
||||
private final VbillIsvChannelMerchantService vbillIsvChannelMerchantService;
|
||||
|
||||
@PermCode(code = "view", nameCn = "通道商户查看", nameEn = "Channel Merchant View")
|
||||
@Operation(summary = "根据通道商户号查询随行付通道商户配置")
|
||||
@GetMapping("/find-by-channel-mch-no")
|
||||
public Result<VbillIsvChannelMerchantResult> findByChannelMchNo(
|
||||
@NotBlank(message = "{validation.field.channelMchNo.notBlank}") String channelMchNo) {
|
||||
return Res.ok(vbillIsvChannelMerchantService.findByChannelMchNo(channelMchNo));
|
||||
}
|
||||
|
||||
@PermCode(code = "manage", nameCn = "通道商户管理", nameEn = "Channel Merchant Manage")
|
||||
@Operation(summary = "创建随行付通道商户")
|
||||
@PostMapping("/create")
|
||||
public Result<Void> create(@RequestBody @Validated VbillIsvChannelMerchantCreateParam param) {
|
||||
vbillIsvChannelMerchantService.create(param);
|
||||
return Res.ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package cn.daxpay.open.channel.vbill.controller.isv;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.convert.isv.VbillIsvKeyConfigConvert;
|
||||
import cn.daxpay.open.channel.vbill.param.isv.VbillIsvKeyConfigParam;
|
||||
import cn.daxpay.open.channel.vbill.result.isv.VbillIsvKeyConfigResult;
|
||||
import cn.daxpay.open.channel.vbill.service.isv.VbillIsvKeyConfigService;
|
||||
import cn.daxpay.open.platform.core.annotation.PermCode;
|
||||
import cn.daxpay.open.platform.core.rest.Res;
|
||||
import cn.daxpay.open.platform.core.rest.result.Result;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
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.RestController;
|
||||
|
||||
/// # 随行付服务商密钥配置
|
||||
@PermCode(menuCode = "payment:vbill:isv")
|
||||
@Validated
|
||||
@Tag(name = "随行付服务商密钥配置")
|
||||
@RestController
|
||||
@RequestMapping("/admin/vbill/isv-key-config")
|
||||
@RequiredArgsConstructor
|
||||
public class VbillIsvKeyConfigController {
|
||||
|
||||
private final VbillIsvKeyConfigService vbillIsvKeyConfigService;
|
||||
|
||||
@PermCode(code = "view", nameCn = "随行付服务商查看", nameEn = "VBill ISV View")
|
||||
@Operation(summary = "查询随行付服务商密钥配置")
|
||||
@GetMapping("/find-config")
|
||||
public Result<VbillIsvKeyConfigResult> findConfig(
|
||||
@NotBlank(message = "{validation.field.product.notBlank}") String product) {
|
||||
return Res.ok(VbillIsvKeyConfigConvert.CONVERT.toResult(vbillIsvKeyConfigService.findByProduct(product)));
|
||||
}
|
||||
|
||||
@PermCode(code = "manage", nameCn = "随行付服务商管理", nameEn = "VBill ISV Manage")
|
||||
@Operation(summary = "保存随行付服务商密钥配置")
|
||||
@PostMapping("/save-config")
|
||||
public Result<Void> saveConfig(@RequestBody @Validated VbillIsvKeyConfigParam param) {
|
||||
vbillIsvKeyConfigService.saveConfig(param);
|
||||
return Res.ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package cn.daxpay.open.channel.vbill.convert.isv;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.entity.isv.VbillIsvChannelMerchant;
|
||||
import cn.daxpay.open.channel.vbill.result.isv.VbillIsvChannelMerchantResult;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
/// # 随行付通道商户绑定转换
|
||||
@Mapper
|
||||
public interface VbillIsvChannelMerchantConvert {
|
||||
|
||||
VbillIsvChannelMerchantConvert CONVERT = Mappers.getMapper(VbillIsvChannelMerchantConvert.class);
|
||||
|
||||
VbillIsvChannelMerchantResult toResult(VbillIsvChannelMerchant entity);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package cn.daxpay.open.channel.vbill.convert.isv;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.entity.isv.VbillIsvKeyConfig;
|
||||
import cn.daxpay.open.channel.vbill.param.isv.VbillIsvKeyConfigParam;
|
||||
import cn.daxpay.open.channel.vbill.result.isv.VbillIsvKeyConfigResult;
|
||||
import org.mapstruct.BeanMapping;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import org.mapstruct.NullValuePropertyMappingStrategy;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
/// # 随行付服务商密钥配置转换
|
||||
@Mapper
|
||||
public interface VbillIsvKeyConfigConvert {
|
||||
|
||||
VbillIsvKeyConfigConvert CONVERT = Mappers.getMapper(VbillIsvKeyConfigConvert.class);
|
||||
|
||||
/// 转换为返回对象
|
||||
VbillIsvKeyConfigResult toResult(VbillIsvKeyConfig entity);
|
||||
|
||||
/// 转换为实体
|
||||
VbillIsvKeyConfig toEntity(VbillIsvKeyConfigParam param);
|
||||
|
||||
/// 更新源数据到实体(空值不覆盖, 密钥为空时保留原值)
|
||||
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
|
||||
void copy(VbillIsvKeyConfigParam param, @MappingTarget VbillIsvKeyConfig entity);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package cn.daxpay.open.channel.vbill.dao.isv;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.entity.isv.VbillIsvChannelMerchant;
|
||||
import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/// # 随行付通道商户绑定 Manager
|
||||
@Repository
|
||||
public class VbillIsvChannelMerchantManager extends BaseManager<VbillIsvChannelMerchantMapper, VbillIsvChannelMerchant> {
|
||||
|
||||
/// 校验同一商户号下天阙商户号不重复
|
||||
public boolean existsByMchNoAndVbillMchNo(String mchNo, String vbillMchNo) {
|
||||
return lambdaQuery()
|
||||
.eq(VbillIsvChannelMerchant::getMchNo, mchNo)
|
||||
.eq(VbillIsvChannelMerchant::getVbillMchNo, vbillMchNo)
|
||||
.exists();
|
||||
}
|
||||
|
||||
/// 根据通道商户号查询
|
||||
public Optional<VbillIsvChannelMerchant> findByChannelMchNo(String channelMchNo) {
|
||||
return lambdaQuery()
|
||||
.eq(VbillIsvChannelMerchant::getChannelMchNo, channelMchNo)
|
||||
.oneOpt();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package cn.daxpay.open.channel.vbill.dao.isv;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.entity.isv.VbillIsvChannelMerchant;
|
||||
import com.github.yulichang.base.MPJBaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/// # 随行付通道商户绑定 Mapper
|
||||
@Mapper
|
||||
public interface VbillIsvChannelMerchantMapper extends MPJBaseMapper<VbillIsvChannelMerchant> {
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package cn.daxpay.open.channel.vbill.dao.isv;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.entity.isv.VbillIsvKeyConfig;
|
||||
import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/// # 随行付服务商密钥配置 Manager
|
||||
@Slf4j
|
||||
@Service
|
||||
public class VbillIsvKeyConfigManager extends BaseManager<VbillIsvKeyConfigMapper, VbillIsvKeyConfig> {
|
||||
|
||||
/// 根据产品编码查询(平台为唯一服务商, 密钥全局唯一)
|
||||
public Optional<VbillIsvKeyConfig> findByProduct(String product) {
|
||||
return lambdaQuery()
|
||||
.eq(VbillIsvKeyConfig::getProduct, product)
|
||||
.oneOpt();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package cn.daxpay.open.channel.vbill.dao.isv;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.entity.isv.VbillIsvKeyConfig;
|
||||
import com.github.yulichang.base.MPJBaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/// # 随行付服务商密钥配置 Mapper
|
||||
@Mapper
|
||||
public interface VbillIsvKeyConfigMapper extends MPJBaseMapper<VbillIsvKeyConfig> {
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package cn.daxpay.open.channel.vbill.entity.isv;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.convert.isv.VbillIsvChannelMerchantConvert;
|
||||
import cn.daxpay.open.channel.vbill.result.isv.VbillIsvChannelMerchantResult;
|
||||
import cn.daxpay.open.payment.common.entity.merchant.MchBaseEntity;
|
||||
import cn.daxpay.open.platform.common.mybatisplus.function.ToResult;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/// # 随行付通道商户绑定
|
||||
///
|
||||
/// 随行付(天阙科技)服务商模式下, 子商户绑定天阙商户号(mno),
|
||||
/// 机构号/密钥由服务商全局配置([VbillIsvKeyConfig])提供。
|
||||
/// 同一商户号下天阙商户号不重复。
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@TableName(value = "vbill_isv_channel_merchant", autoResultMap = true)
|
||||
public class VbillIsvChannelMerchant extends MchBaseEntity implements ToResult<VbillIsvChannelMerchantResult> {
|
||||
|
||||
/// 通道商户号(VBILL+雪花)
|
||||
private String channelMchNo;
|
||||
|
||||
/// 所属支付产品
|
||||
/// @see cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum
|
||||
private String product;
|
||||
|
||||
/// 天阙商户号(mno)
|
||||
private String vbillMchNo;
|
||||
|
||||
/// 转换
|
||||
@Override
|
||||
public VbillIsvChannelMerchantResult toResult() {
|
||||
return VbillIsvChannelMerchantConvert.CONVERT.toResult(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package cn.daxpay.open.channel.vbill.entity.isv;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.convert.isv.VbillIsvKeyConfigConvert;
|
||||
import cn.daxpay.open.channel.vbill.result.isv.VbillIsvKeyConfigResult;
|
||||
import cn.daxpay.open.platform.common.mybatisplus.base.MpBaseEntity;
|
||||
import cn.daxpay.open.platform.common.mybatisplus.function.ToResult;
|
||||
import cn.daxpay.open.platform.common.mybatisplus.handler.encrypt.DataEncryptTypeHandler;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/// # 随行付服务商密钥配置
|
||||
///
|
||||
/// 随行付(天阙科技)为收单机构服务商模式, 服务商密钥全局唯一(按 product 查询),
|
||||
/// 子商户仅需天阙商户号(mno), 见 [VbillIsvChannelMerchant]。
|
||||
///
|
||||
/// 签名算法: SHA1withRSA, 私钥签名 / 公钥验签。
|
||||
/// 敏感字段(私钥/公钥)通过 [DataEncryptTypeHandler] 加密入库。
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@TableName(value = "vbill_isv_key_config", autoResultMap = true)
|
||||
public class VbillIsvKeyConfig extends MpBaseEntity implements ToResult<VbillIsvKeyConfigResult> {
|
||||
|
||||
/// 产品编码
|
||||
/// @see cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum
|
||||
private String product;
|
||||
|
||||
/// 天阙合作机构ID(orgId)
|
||||
private String orgId;
|
||||
|
||||
/// 天阙RSA公钥(X509 Base64, 用于响应/回调验签, 加密存储)
|
||||
@TableField(typeHandler = DataEncryptTypeHandler.class)
|
||||
private String publicKey;
|
||||
|
||||
/// 商户RSA私钥(PKCS8 Base64, SHA1withRSA 签名, 加密存储)
|
||||
@TableField(typeHandler = DataEncryptTypeHandler.class)
|
||||
private String privateKey;
|
||||
|
||||
/// 是否沙箱环境
|
||||
private Boolean sandbox;
|
||||
|
||||
/// 转换
|
||||
@Override
|
||||
public VbillIsvKeyConfigResult toResult() {
|
||||
return VbillIsvKeyConfigConvert.CONVERT.toResult(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package cn.daxpay.open.channel.vbill.param.isv;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/// # 随行付通道商户创建参数
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(title = "随行付通道商户创建参数")
|
||||
public class VbillIsvChannelMerchantCreateParam {
|
||||
|
||||
/// 商户号
|
||||
@Schema(description = "商户号")
|
||||
@NotBlank(message = "{validation.field.mchNo.notBlank}")
|
||||
private String mchNo;
|
||||
|
||||
/// 通道商户名称
|
||||
@Schema(description = "通道商户名称")
|
||||
@NotBlank(message = "{validation.field.channelMerchantName.notBlank}")
|
||||
private String channelMerchantName;
|
||||
|
||||
/// 所属支付产品
|
||||
@Schema(description = "所属支付产品")
|
||||
@NotBlank(message = "{validation.field.product.notBlank}")
|
||||
private String product;
|
||||
|
||||
/// 天阙商户号(mno)
|
||||
@Schema(description = "天阙商户号(mno)")
|
||||
@NotBlank(message = "{validation.field.vbillMchNo.notBlank}")
|
||||
private String vbillMchNo;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package cn.daxpay.open.channel.vbill.param.isv;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/// # 随行付服务商密钥配置保存参数
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(title = "随行付服务商密钥配置保存参数")
|
||||
public class VbillIsvKeyConfigParam {
|
||||
|
||||
@NotBlank(message = "{validation.field.product.notBlank}")
|
||||
@Schema(description = "产品编码")
|
||||
private String product;
|
||||
|
||||
@NotBlank(message = "{validation.field.orgId.notBlank}")
|
||||
@Schema(description = "天阙合作机构ID(orgId)")
|
||||
private String orgId;
|
||||
|
||||
@Schema(description = "天阙RSA公钥(X509 Base64, 加密存储)")
|
||||
private String publicKey;
|
||||
|
||||
@Schema(description = "商户RSA私钥(PKCS8 Base64, 加密存储)")
|
||||
private String privateKey;
|
||||
|
||||
@Schema(description = "是否沙箱环境")
|
||||
private Boolean sandbox;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package cn.daxpay.open.channel.vbill.result.isv;
|
||||
|
||||
import cn.daxpay.open.payment.common.result.MchTradeBaseResult;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/// # 随行付通道商户绑定结果
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(title = "随行付通道商户绑定结果")
|
||||
public class VbillIsvChannelMerchantResult extends MchTradeBaseResult {
|
||||
|
||||
@Schema(description = "通道商户号")
|
||||
private String channelMchNo;
|
||||
|
||||
@Schema(description = "所属支付产品")
|
||||
private String product;
|
||||
|
||||
@Schema(description = "天阙商户号(mno)")
|
||||
private String vbillMchNo;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package cn.daxpay.open.channel.vbill.result.isv;
|
||||
|
||||
import cn.daxpay.open.platform.common.json.sensitive.SensitiveInfo;
|
||||
import cn.daxpay.open.platform.core.result.BaseResult;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/// # 随行付服务商密钥配置结果
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(title = "随行付服务商密钥配置")
|
||||
public class VbillIsvKeyConfigResult extends BaseResult {
|
||||
|
||||
@Schema(description = "产品编码")
|
||||
private String product;
|
||||
|
||||
@Schema(description = "天阙合作机构ID(orgId)")
|
||||
private String orgId;
|
||||
|
||||
@SensitiveInfo(front = 12, end = 12)
|
||||
@Schema(description = "天阙RSA公钥(加密存储)")
|
||||
private String publicKey;
|
||||
|
||||
@SensitiveInfo(front = 12, end = 12)
|
||||
@Schema(description = "商户RSA私钥(加密存储)")
|
||||
private String privateKey;
|
||||
|
||||
@Schema(description = "是否沙箱环境")
|
||||
private Boolean sandbox;
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package cn.daxpay.open.channel.vbill.service.callback;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.dao.isv.VbillIsvKeyConfigManager;
|
||||
import cn.daxpay.open.channel.vbill.entity.isv.VbillIsvKeyConfig;
|
||||
import cn.daxpay.open.payment.common.callback.CallbackData;
|
||||
import cn.daxpay.open.payment.core.trade.service.PayCallbackService;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum;
|
||||
import cn.daxpay.open.platform.core.enums.pay.notice.CallbackStatusEnum;
|
||||
import cn.hutool.core.date.DatePattern;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.servlet.JakartaServletUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/// # 随行付支付回调处理服务
|
||||
///
|
||||
/// 随行付(天阙科技)异步通知 → 主应用接收 JSON body → 用全局服务商公钥 SHA1withRSA 验签 →
|
||||
/// 构建 [CallbackData] 交由 [PayCallbackService] 更新订单状态。
|
||||
///
|
||||
/// 随行付回调签名机制(与响应签名一致):
|
||||
/// - 整个回调 JSON(去 sign 字段)按 key 字典序拼接 `k1=v1&k2=v2`
|
||||
/// - 用天阙公钥 SHA1withRSA 验证 sign(Base64)
|
||||
///
|
||||
/// 成功响应: 返回 `{"code":"success","msg":"成功"}` JSON; 验签/解析失败返回 code=fail。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VbillPayCallbackService {
|
||||
|
||||
/// 回调成功响应 code
|
||||
private static final String RESP_CODE_SUCCESS = "success";
|
||||
private static final String RESP_CODE_FAIL = "fail";
|
||||
|
||||
/// 随行付属国内通道, 回调时间字段(payTime)为东八区本地时间字面量(yyyyMMddHHmmss),
|
||||
/// 显式钉死 +08:00 偏移, 避免 OffsetDateTime.parse 因缺 OFFSET_SECONDS 解析失败
|
||||
private static final DateTimeFormatter PAY_TIME_FORMATTER =
|
||||
DateTimeFormatter.ofPattern(DatePattern.PURE_DATETIME_PATTERN).withZone(ZoneOffset.ofHours(8));
|
||||
|
||||
private final VbillIsvKeyConfigManager vbillIsvKeyConfigManager;
|
||||
private final PayCallbackService payCallbackService;
|
||||
|
||||
/// 支付回调处理
|
||||
public Map<String, String> payHandle(HttpServletRequest request) {
|
||||
Map<String, String> resp = new HashMap<>(4);
|
||||
// 1. 读取回调原始 JSON
|
||||
String body = readBody(request);
|
||||
if (StrUtil.isBlank(body)) {
|
||||
log.error("随行付支付回调: body 为空");
|
||||
resp.put("code", RESP_CODE_FAIL);
|
||||
resp.put("msg", "body 为空");
|
||||
return resp;
|
||||
}
|
||||
|
||||
// 2. 获取全局服务商公钥(只读查询, 不创建记录)
|
||||
VbillIsvKeyConfig keyConfig = vbillIsvKeyConfigManager.findByProduct(ProductEnum.VBILL_PAY.getCode())
|
||||
.orElse(null);
|
||||
if (keyConfig == null || keyConfig.getPublicKey() == null) {
|
||||
log.error("随行付支付回调: 服务商密钥未配置, 无法验签");
|
||||
resp.put("code", RESP_CODE_FAIL);
|
||||
resp.put("msg", "密钥未配置");
|
||||
return resp;
|
||||
}
|
||||
|
||||
// 3. 验签(整个回调 JSON 去 sign 后字典序签名)
|
||||
if (!verifyCallbackSign(body, keyConfig.getPublicKey())) {
|
||||
log.error("随行付支付回调验签失败");
|
||||
resp.put("code", RESP_CODE_FAIL);
|
||||
resp.put("msg", "验签失败");
|
||||
return resp;
|
||||
}
|
||||
|
||||
// 4. 解析回调数据并构建 CallbackData
|
||||
CallbackData callbackData = parseCallback(body);
|
||||
if (callbackData == null) {
|
||||
log.error("随行付支付回调解析失败: body={}", body);
|
||||
resp.put("code", RESP_CODE_FAIL);
|
||||
resp.put("msg", "解析失败");
|
||||
return resp;
|
||||
}
|
||||
|
||||
try {
|
||||
// 5. 交由框架更新订单状态
|
||||
payCallbackService.payCallback(callbackData);
|
||||
resp.put("code", RESP_CODE_SUCCESS);
|
||||
resp.put("msg", "成功");
|
||||
} catch (Exception e) {
|
||||
log.error("随行付支付回调业务处理失败: tradeNo={}", callbackData.getTradeNo(), e);
|
||||
resp.put("code", RESP_CODE_FAIL);
|
||||
resp.put("msg", "业务处理失败");
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
|
||||
/// 验证回调签名: 整个 JSON 去 sign, 字典序拼接, SHA1withRSA 验签
|
||||
private boolean verifyCallbackSign(String body, String publicKeyStr) {
|
||||
try {
|
||||
JSONObject json = JSONUtil.parseObj(body);
|
||||
String sign = json.getStr("sign");
|
||||
if (StrUtil.isBlank(sign)) {
|
||||
return false;
|
||||
}
|
||||
// 构建待验签字符串(去 sign, 其余按字典序拼接; 嵌套对象保留 JSON 字符串)
|
||||
Map<String, String> flatMap = new TreeMap<>();
|
||||
for (String key : json.keySet()) {
|
||||
if ("sign".equals(key)) {
|
||||
continue;
|
||||
}
|
||||
Object value = json.get(key);
|
||||
if (value != null && !value.toString().isEmpty()) {
|
||||
flatMap.put(key, value.toString());
|
||||
}
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
boolean first = true;
|
||||
for (Map.Entry<String, String> entry : flatMap.entrySet()) {
|
||||
if (!first) {
|
||||
sb.append("&");
|
||||
}
|
||||
sb.append(entry.getKey()).append("=").append(entry.getValue());
|
||||
first = false;
|
||||
}
|
||||
String signSource = sb.toString();
|
||||
|
||||
// SHA1withRSA 验签
|
||||
byte[] keyBytes = Base64.getDecoder().decode(publicKeyStr.replaceAll("[\\s*\t\n\r]", ""));
|
||||
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
|
||||
PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(keySpec);
|
||||
Signature signature = Signature.getInstance("SHA1withRSA");
|
||||
signature.initVerify(publicKey);
|
||||
signature.update(signSource.getBytes(StandardCharsets.UTF_8));
|
||||
return signature.verify(Base64.getDecoder().decode(sign));
|
||||
} catch (Exception e) {
|
||||
log.error("随行付回调验签异常", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析随行付回调 JSON 为 CallbackData
|
||||
private CallbackData parseCallback(String body) {
|
||||
try {
|
||||
JSONObject json = JSONUtil.parseObj(body);
|
||||
CallbackData callbackData = new CallbackData();
|
||||
// ordNo = 下单时传入的平台 tradeNo
|
||||
callbackData.setTradeNo(json.getStr("ordNo"));
|
||||
// uuid = 随行付网关订单号
|
||||
callbackData.setOutTradeNo(json.getStr("uuid"));
|
||||
// 支付状态(随行付支付回调恒为成功才会通知)
|
||||
callbackData.setTradeStatus(CallbackStatusEnum.SUCCESS.getCode());
|
||||
// 完成时间(yyyyMMddHHmmss → OffsetDateTime)
|
||||
String payTime = json.getStr("payTime");
|
||||
if (StrUtil.isNotBlank(payTime)) {
|
||||
try {
|
||||
callbackData.setFinishTime(OffsetDateTime.parse(payTime.trim(), PAY_TIME_FORMATTER));
|
||||
} catch (Exception e) {
|
||||
log.warn("随行付回调时间解析失败: payTime={}", payTime);
|
||||
}
|
||||
}
|
||||
return callbackData;
|
||||
} catch (Exception e) {
|
||||
log.error("随行付回调 JSON 解析失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 元(BigDecimal 字符串) → 分(Long) [备用, 当前 CallbackData 不含金额字段]
|
||||
private Long yuanToFen(String yuan) {
|
||||
try {
|
||||
return new BigDecimal(yuan.trim()).multiply(new BigDecimal("100")).longValue();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 读取 request body
|
||||
private String readBody(HttpServletRequest request) {
|
||||
return JakartaServletUtil.getBody(request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package cn.daxpay.open.channel.vbill.service.isv;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.dao.isv.VbillIsvChannelMerchantManager;
|
||||
import cn.daxpay.open.channel.vbill.entity.isv.VbillIsvChannelMerchant;
|
||||
import cn.daxpay.open.channel.vbill.param.isv.VbillIsvChannelMerchantCreateParam;
|
||||
import cn.daxpay.open.channel.vbill.result.isv.VbillIsvChannelMerchantResult;
|
||||
import cn.daxpay.open.payment.channel.dao.mch.ChannelMerchantManager;
|
||||
import cn.daxpay.open.payment.channel.entity.mch.ChannelMerchant;
|
||||
import cn.daxpay.open.platform.core.code.CommonErrorCode;
|
||||
import cn.daxpay.open.platform.core.enums.channel.ChannelMerchantSourceEnum;
|
||||
import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
import cn.daxpay.open.platform.core.exception.DataNotExistException;
|
||||
import cn.daxpay.open.platform.core.util.ChannelMchNoGenerateUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/// # 随行付通道商户管理
|
||||
///
|
||||
/// 随行付(天阙科技)服务商模式下, 子商户关联到服务商(密钥全局唯一)。
|
||||
/// 一个商户号下同一天阙商户号(vbillMchNo/mno)只允许绑定一次。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VbillIsvChannelMerchantService {
|
||||
|
||||
private final ChannelMerchantManager channelMerchantManager;
|
||||
private final VbillIsvChannelMerchantManager vbillIsvChannelMerchantManager;
|
||||
|
||||
/// 创建随行付通道商户
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void create(VbillIsvChannelMerchantCreateParam param) {
|
||||
// 校验同一商户号下天阙商户号不重复
|
||||
if (vbillIsvChannelMerchantManager.existsByMchNoAndVbillMchNo(
|
||||
param.getMchNo(), param.getVbillMchNo())) {
|
||||
// 随行付: 该商户下已存在此天阙商户号
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "error.channel.vbill.merchantDuplicate");
|
||||
}
|
||||
// 生成通道商户号: 通道前缀 + 雪花ID
|
||||
String channelMchNo = ChannelMchNoGenerateUtil.generate("VBILL");
|
||||
// 写通用通道商户主表
|
||||
ChannelMerchant channelMerchant = new ChannelMerchant();
|
||||
channelMerchant.setMchNo(param.getMchNo());
|
||||
channelMerchant.setChannelMchNo(channelMchNo);
|
||||
channelMerchant.setChannelMerchantName(param.getChannelMerchantName());
|
||||
channelMerchant.setProduct(param.getProduct());
|
||||
channelMerchant.setSource(ChannelMerchantSourceEnum.MANUAL.getCode());
|
||||
channelMerchant.setEnable(true);
|
||||
channelMerchantManager.save(channelMerchant);
|
||||
// 写随行付绑定表
|
||||
var entity = new VbillIsvChannelMerchant();
|
||||
entity.setMchNo(param.getMchNo());
|
||||
entity.setChannelMchNo(channelMchNo);
|
||||
entity.setProduct(param.getProduct());
|
||||
entity.setVbillMchNo(param.getVbillMchNo());
|
||||
vbillIsvChannelMerchantManager.save(entity);
|
||||
}
|
||||
|
||||
/// 根据通道商户号查询随行付通道商户配置
|
||||
public VbillIsvChannelMerchantResult findByChannelMchNo(String channelMchNo) {
|
||||
return vbillIsvChannelMerchantManager.lambdaQuery()
|
||||
.eq(VbillIsvChannelMerchant::getChannelMchNo, channelMchNo)
|
||||
.oneOpt()
|
||||
.map(VbillIsvChannelMerchant::toResult)
|
||||
// 随行付: 通道商户配置不存在
|
||||
.orElseThrow(() -> new DataNotExistException("error.payment.channel.channelMerchantNotExist"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package cn.daxpay.open.channel.vbill.service.isv;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.client.credential.VbillSdkCredential;
|
||||
import cn.daxpay.open.channel.vbill.dao.isv.VbillIsvChannelMerchantManager;
|
||||
import cn.daxpay.open.channel.vbill.entity.isv.VbillIsvChannelMerchant;
|
||||
import cn.daxpay.open.channel.vbill.entity.isv.VbillIsvKeyConfig;
|
||||
import cn.daxpay.open.payment.masterdata.constants.product.dao.PayProductConfigManager;
|
||||
import cn.daxpay.open.payment.masterdata.constants.product.entity.PayProductConfig;
|
||||
import cn.daxpay.open.platform.core.code.CommonErrorCode;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum;
|
||||
import cn.daxpay.open.platform.core.enums.pay.config.PayEnvEnum;
|
||||
import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
import cn.daxpay.open.platform.core.exception.DataNotExistException;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/// # 随行付服务商通道凭证组装器
|
||||
///
|
||||
/// 从服务商密钥配置([VbillIsvKeyConfig]) + 通道商户绑定([VbillIsvChannelMerchant]) 组装通道调用凭证,
|
||||
/// 下发给子应用 dax-pay-channel-two 发起随行付 API 调用。
|
||||
///
|
||||
/// 字段映射(对齐随行付天阙开放平台接口):
|
||||
/// - orgId ← [VbillIsvKeyConfig.orgId] (天阙合作机构ID, 全局唯一)
|
||||
/// - privateKey/publicKey ← [VbillIsvKeyConfig] (服务商级, 全局唯一)
|
||||
/// - mno ← [VbillIsvChannelMerchant.vbillMchNo] (天阙商户号, 子商户级)
|
||||
/// - sandbox ← PayProductConfig.activeEnv (沙箱/生产)
|
||||
///
|
||||
/// 供支付策略([cn.daxpay.open.channel.vbill.strategy.*])组装通道调用凭证。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VbillIsvConfigAssembler {
|
||||
|
||||
private final VbillIsvChannelMerchantManager vbillIsvChannelMerchantManager;
|
||||
private final VbillIsvKeyConfigService vbillIsvKeyConfigService;
|
||||
private final PayProductConfigManager payProductConfigManager;
|
||||
|
||||
/// 组装随行付通道调用凭证(下发给子应用)
|
||||
///
|
||||
/// @param mchNo 商户号(保留参数对齐签名, 服务商密钥全局唯一不依赖此字段)
|
||||
/// @param channelMchNo 通道商户号(随行付商户绑定主键)
|
||||
/// @param capability 支付能力编码(保留参数, 随行付不按能力路由)
|
||||
/// @return 随行付 SDK 凭证(含服务商密钥 + 天阙商户号)
|
||||
public VbillSdkCredential buildConfig(String mchNo, String channelMchNo, String capability) {
|
||||
// 服务商密钥(全局唯一, 含机构号 + 私钥/公钥; 缺失或关键字段为空时 fail-fast)
|
||||
VbillIsvKeyConfig keyConfig = vbillIsvKeyConfigService.getByProductForPay(ProductEnum.VBILL_PAY.getCode());
|
||||
// 通道商户绑定(取天阙商户号 mno)
|
||||
VbillIsvChannelMerchant channelMerchant = vbillIsvChannelMerchantManager.findByChannelMchNo(channelMchNo)
|
||||
// 随行付: 通道商户配置不存在
|
||||
.orElseThrow(() -> new DataNotExistException("error.payment.channel.channelMerchantNotExist"));
|
||||
|
||||
VbillSdkCredential credential = new VbillSdkCredential();
|
||||
// 服务商身份与密钥
|
||||
credential.setOrgId(keyConfig.getOrgId());
|
||||
credential.setPrivateKey(keyConfig.getPrivateKey());
|
||||
credential.setPublicKey(keyConfig.getPublicKey());
|
||||
// 沙箱状态读取支付产品配置的生效环境
|
||||
boolean sandbox = payProductConfigManager.findByProduct(ProductEnum.VBILL_PAY.getCode())
|
||||
.map(PayProductConfig::getActiveEnv)
|
||||
.map(PayEnvEnum.SANDBOX.getCode()::equals)
|
||||
.orElse(false);
|
||||
credential.setSandbox(sandbox);
|
||||
// 子商户身份(天阙商户号 mno)
|
||||
String mno = channelMerchant.getVbillMchNo();
|
||||
if (StrUtil.isBlank(mno)) {
|
||||
// 随行付: 天阙商户号未配置
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "error.channel.vbill.mnoNotConfigured");
|
||||
}
|
||||
credential.setMno(mno);
|
||||
return credential;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package cn.daxpay.open.channel.vbill.service.isv;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.convert.isv.VbillIsvKeyConfigConvert;
|
||||
import cn.daxpay.open.channel.vbill.dao.isv.VbillIsvKeyConfigManager;
|
||||
import cn.daxpay.open.channel.vbill.entity.isv.VbillIsvKeyConfig;
|
||||
import cn.daxpay.open.channel.vbill.param.isv.VbillIsvKeyConfigParam;
|
||||
import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/// # 随行付服务商密钥配置
|
||||
///
|
||||
/// 管理服务商密钥配置, 查询时不存在则创建默认记录(平台为唯一服务商, 密钥全局唯一)。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VbillIsvKeyConfigService {
|
||||
|
||||
private final VbillIsvKeyConfigManager vbillIsvKeyConfigManager;
|
||||
|
||||
/// 根据产品编码查询密钥配置, 不存在则创建默认记录
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public VbillIsvKeyConfig findByProduct(String product) {
|
||||
var existing = vbillIsvKeyConfigManager.findByProduct(product);
|
||||
if (existing.isPresent()) {
|
||||
return existing.get();
|
||||
}
|
||||
var config = new VbillIsvKeyConfig()
|
||||
.setProduct(product);
|
||||
vbillIsvKeyConfigManager.save(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
/// 支付场景查询服务商密钥(必填校验, 不创建记录)
|
||||
///
|
||||
/// 记录不存在或关键字段(orgId/privateKey/publicKey)任一为空时 fail-fast,
|
||||
/// 避免空凭证下发到子应用后子应用才发现问题。
|
||||
public VbillIsvKeyConfig getByProductForPay(String product) {
|
||||
VbillIsvKeyConfig config = vbillIsvKeyConfigManager.findByProduct(product)
|
||||
// 随行付: 服务商密钥未配置
|
||||
.orElseThrow(() -> new BizInfoException("error.channel.vbill.isvKeyNotConfigured"));
|
||||
if (StrUtil.hasBlank(config.getOrgId(), config.getPrivateKey(), config.getPublicKey())) {
|
||||
throw new BizInfoException("error.channel.vbill.isvKeyNotConfigured");
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
/// 保存服务商密钥配置
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void saveConfig(VbillIsvKeyConfigParam param) {
|
||||
var config = this.findByProduct(param.getProduct());
|
||||
VbillIsvKeyConfigConvert.CONVERT.copy(param, config);
|
||||
vbillIsvKeyConfigManager.updateById(config);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package cn.daxpay.open.channel.vbill.service.payment;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.client.VbillChannelClient;
|
||||
import cn.daxpay.open.channel.vbill.client.credential.VbillSdkCredential;
|
||||
import cn.daxpay.open.channel.vbill.client.req.VbillCloseReq;
|
||||
import cn.daxpay.open.channel.vbill.client.resp.VbillCloseResp;
|
||||
import cn.daxpay.open.payment.common.result.DaxResult;
|
||||
import cn.daxpay.open.payment.core.trade.entity.PayTrade;
|
||||
import cn.daxpay.open.platform.core.code.DaxPayErrorCode;
|
||||
import cn.daxpay.open.platform.core.enums.pay.pay.CloseTypeEnum;
|
||||
import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/// # 随行付服务商关单业务服务
|
||||
///
|
||||
/// 通过 [VbillChannelClient] 调用子应用关闭随行付订单(`/query/close`)。
|
||||
/// 随行付仅提供关单接口, 无撤销接口, useCancel 参数忽略。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VbillCloseService {
|
||||
|
||||
private final VbillChannelClient vbillChannelClient;
|
||||
|
||||
/// 关闭订单
|
||||
///
|
||||
/// @param order 支付订单
|
||||
/// @param credential 通道凭证
|
||||
/// @param useCancel 是否撤销(随行付不支持撤销, 忽略此参数)
|
||||
/// @return 关闭类型(恒为 CLOSE)
|
||||
public CloseTypeEnum close(PayTrade order, VbillSdkCredential credential, boolean useCancel) {
|
||||
VbillCloseReq req = new VbillCloseReq();
|
||||
req.setCredential(credential);
|
||||
// 随行付关单凭网关订单号(uuid)
|
||||
req.setOutOrderNo(order.getOutOrderNo());
|
||||
|
||||
DaxResult<VbillCloseResp> result = vbillChannelClient.close(req);
|
||||
if (result.getCode() != 0) {
|
||||
throw new BizInfoException(DaxPayErrorCode.TRADE_FAIL, "channel.error.vbillCloseFailed", result.getMsg());
|
||||
}
|
||||
return CloseTypeEnum.CLOSE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package cn.daxpay.open.channel.vbill.service.payment;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.client.VbillChannelClient;
|
||||
import cn.daxpay.open.channel.vbill.client.credential.VbillSdkCredential;
|
||||
import cn.daxpay.open.channel.vbill.client.enums.VbillPayBodyType;
|
||||
import cn.daxpay.open.channel.vbill.client.enums.VbillPayMethod;
|
||||
import cn.daxpay.open.channel.vbill.client.req.VbillPayReq;
|
||||
import cn.daxpay.open.channel.vbill.client.resp.VbillPayResp;
|
||||
import cn.daxpay.open.payment.common.result.DaxResult;
|
||||
import cn.daxpay.open.payment.core.trade.bo.PayTradeResultBo;
|
||||
import cn.daxpay.open.payment.core.trade.entity.PayTrade;
|
||||
import cn.daxpay.open.payment.unipay.param.trade.pay.NormalPayParam;
|
||||
import cn.daxpay.open.platform.core.code.CommonErrorCode;
|
||||
import cn.daxpay.open.platform.core.code.DaxPayErrorCode;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.PayMethodEnum;
|
||||
import cn.daxpay.open.platform.core.enums.unipay.PayBodyTypeEnum;
|
||||
import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
import cn.daxpay.open.platform.system.service.config.PlatformUrlConfigService;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/// # 随行付服务商支付执行业务服务
|
||||
///
|
||||
/// 通过 [VbillChannelClient] 调用子应用 dax-pay-channel-two 完成随行付支付下单。
|
||||
/// 请求构建、响应解析全部在本类中完成。
|
||||
///
|
||||
/// 随行付聚合通道: 4 个接口承载微信/支付宝/银联的 JSAPI/扫码/付款码/小程序收银台,
|
||||
/// 由 [VbillPayMethod] + payType + payWay 决定路由。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VbillPayService {
|
||||
|
||||
private final VbillChannelClient vbillChannelClient;
|
||||
private final PlatformUrlConfigService platformUrlConfigService;
|
||||
|
||||
/// 执行随行付支付
|
||||
///
|
||||
/// @param order 支付订单(tradeNo 作为 ordNo)
|
||||
/// @param payParam 支付参数
|
||||
/// @param credential 通道调用凭证(服务商密钥 + 天阙商户号)
|
||||
/// @return 支付结果
|
||||
public PayTradeResultBo pay(PayTrade order, NormalPayParam payParam, VbillSdkCredential credential) {
|
||||
// 平台支付方式 → 随行付三要素(method + payType + payWay + bodyType)
|
||||
MethodMapping mapping = mapMethod(payParam.getMethod());
|
||||
|
||||
// 构建请求
|
||||
VbillPayReq req = new VbillPayReq();
|
||||
// 使用支付交易号作为商户订单号透传给随行付, 回调时凭此反查 PayTrade
|
||||
req.setOutTradeNo(order.getTradeNo());
|
||||
req.setAmount(payParam.getAmount());
|
||||
req.setTitle(payParam.getTitle());
|
||||
req.setDescription(payParam.getDescription());
|
||||
req.setMethod(mapping.method);
|
||||
req.setPayType(mapping.payType);
|
||||
req.setPayWay(mapping.payWay);
|
||||
req.setPayBodyType(mapping.bodyType);
|
||||
// 通道专属参数透传
|
||||
req.setOpenId(payParam.getOpenId());
|
||||
req.setAuthCode(payParam.getAuthCode());
|
||||
req.setClientIp(payParam.getClientIp());
|
||||
req.setNotifyUrl(this.buildNotifyUrl(order));
|
||||
req.setExpireTime(order.getExpiredTime());
|
||||
req.setCredential(credential);
|
||||
|
||||
// 调用子应用
|
||||
DaxResult<VbillPayResp> result = vbillChannelClient.pay(req);
|
||||
if (result.getCode() != 0) {
|
||||
throw new BizInfoException(DaxPayErrorCode.TRADE_FAIL, "channel.error.vbillPayFailed", result.getMsg());
|
||||
}
|
||||
|
||||
return toPayResult(result.getData());
|
||||
}
|
||||
|
||||
/// 生成随行付支付异步通知地址(随行付→平台)
|
||||
///
|
||||
/// 路径约定: `{backendBaseUrl}/unipay/callback/{mchNo}/{appId}/vbill/pay`
|
||||
private String buildNotifyUrl(PayTrade order) {
|
||||
String base = platformUrlConfigService.getUrlConfig().getBackendBaseUrl();
|
||||
if (StrUtil.isBlank(base)) {
|
||||
throw new BizInfoException(DaxPayErrorCode.CONFIG_ERROR, "error.common.backendBaseUrlNotConfigured");
|
||||
}
|
||||
return StrUtil.format("{}/unipay/callback/{}/{}/vbill/pay",
|
||||
base, order.getMchNo(), order.getAppId());
|
||||
}
|
||||
|
||||
/// 平台支付方式([PayMethodEnum] code) → 随行付四要素
|
||||
private static MethodMapping mapMethod(String methodCode) {
|
||||
PayMethodEnum m = PayMethodEnum.findByCode(methodCode);
|
||||
return switch (m) {
|
||||
// 微信
|
||||
case WECHAT_JSAPI -> new MethodMapping(VbillPayMethod.UNI_PAY, "WECHAT", "02", VbillPayBodyType.JSAPI);
|
||||
case WECHAT_MINI -> new MethodMapping(VbillPayMethod.UNI_PAY, "WECHAT", "03", VbillPayBodyType.JSAPI);
|
||||
case WECHAT_QR -> new MethodMapping(VbillPayMethod.QR_CODE, null, null, VbillPayBodyType.QR_CODE);
|
||||
case WECHAT_BARCODE -> new MethodMapping(VbillPayMethod.BAR_CODE, null, null, null);
|
||||
case WECHAT_CASHIER -> new MethodMapping(VbillPayMethod.APPLET_CASHIER, null, null, VbillPayBodyType.JSAPI);
|
||||
// 支付宝
|
||||
case ALIPAY_JSAPI, ALIPAY_MINI -> new MethodMapping(VbillPayMethod.UNI_PAY, "ALIPAY", "02", VbillPayBodyType.IDENTIFIER);
|
||||
case ALIPAY_QR -> new MethodMapping(VbillPayMethod.QR_CODE, null, null, VbillPayBodyType.QR_CODE);
|
||||
case ALIPAY_BARCODE -> new MethodMapping(VbillPayMethod.BAR_CODE, null, null, null);
|
||||
// 银联
|
||||
case UNION_JSAPI -> new MethodMapping(VbillPayMethod.UNI_PAY, "UNIONPAY", "02", VbillPayBodyType.LINK);
|
||||
case UNION_QR -> new MethodMapping(VbillPayMethod.QR_CODE, null, null, VbillPayBodyType.QR_CODE);
|
||||
case UNION_PAY_BARCODE -> new MethodMapping(VbillPayMethod.BAR_CODE, null, null, null);
|
||||
default -> throw new BizInfoException(CommonErrorCode.UN_SUPPORTED_OPERATE,
|
||||
"channel.error.vbillUnsupportedPayMethod", methodCode);
|
||||
};
|
||||
}
|
||||
|
||||
/// 解析子应用响应为支付结果 BO
|
||||
private PayTradeResultBo toPayResult(VbillPayResp resp) {
|
||||
PayTradeResultBo bo = new PayTradeResultBo()
|
||||
.setOutOrderNo(resp.getOutOrderNo())
|
||||
.setComplete(Boolean.TRUE.equals(resp.getComplete()))
|
||||
.setPayBody(resp.getPayBody());
|
||||
|
||||
// 支付内容类型映射(子应用 VbillPayBodyType → 平台 PayBodyTypeEnum)
|
||||
if (resp.getPayBodyType() != null) {
|
||||
switch (resp.getPayBodyType()) {
|
||||
case LINK -> bo.setPayBodyType(PayBodyTypeEnum.LINK);
|
||||
case QR_CODE -> bo.setPayBodyType(PayBodyTypeEnum.QR_CODE);
|
||||
case JSAPI -> bo.setPayBodyType(PayBodyTypeEnum.JSAPI);
|
||||
case IDENTIFIER -> bo.setPayBodyType(PayBodyTypeEnum.IDENTIFIER);
|
||||
}
|
||||
}
|
||||
|
||||
// 完成时间与金额(付款码同步成功时返回)
|
||||
bo.setFinishTime(resp.getFinishTime());
|
||||
bo.setTotalAmount(resp.getTotalAmount());
|
||||
bo.setRealAmount(resp.getRealAmount());
|
||||
bo.setBuyerPayAmount(resp.getRealAmount());
|
||||
// 用户标识
|
||||
bo.setBuyerId(resp.getBuyerId());
|
||||
return bo;
|
||||
}
|
||||
|
||||
/// 支付方式映射内部封装
|
||||
private record MethodMapping(VbillPayMethod method, String payType, String payWay,
|
||||
VbillPayBodyType bodyType) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package cn.daxpay.open.channel.vbill.service.payment;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.client.VbillChannelClient;
|
||||
import cn.daxpay.open.channel.vbill.client.credential.VbillSdkCredential;
|
||||
import cn.daxpay.open.channel.vbill.client.req.VbillRefundReq;
|
||||
import cn.daxpay.open.channel.vbill.client.resp.VbillRefundResp;
|
||||
import cn.daxpay.open.payment.common.enums.RefundOrderStatusEnum;
|
||||
import cn.daxpay.open.payment.common.result.DaxResult;
|
||||
import cn.daxpay.open.payment.core.trade.bo.RefundResultBo;
|
||||
import cn.daxpay.open.payment.core.trade.entity.PayRefundOrder;
|
||||
import cn.daxpay.open.platform.core.code.DaxPayErrorCode;
|
||||
import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/// # 随行付服务商退款执行业务服务
|
||||
///
|
||||
/// 通过 [VbillChannelClient] 调用子应用完成随行付退款(`/order/refund`)。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VbillRefundService {
|
||||
|
||||
private final VbillChannelClient vbillChannelClient;
|
||||
|
||||
/// 执行退款
|
||||
public RefundResultBo refund(PayRefundOrder refundOrder, VbillSdkCredential credential) {
|
||||
VbillRefundReq req = new VbillRefundReq();
|
||||
req.setCredential(credential);
|
||||
// 退款单号(作为随行付 ordNo)
|
||||
req.setOutRefundNo(refundOrder.getRefundNo());
|
||||
// 原支付订单的网关订单号(随行付 uuid)
|
||||
req.setOutOrderNo(refundOrder.getOutOrderNo());
|
||||
req.setAmount(refundOrder.getAmount());
|
||||
req.setReason(refundOrder.getReason());
|
||||
|
||||
DaxResult<VbillRefundResp> result = vbillChannelClient.refund(req);
|
||||
if (result.getCode() != 0) {
|
||||
throw new BizInfoException(DaxPayErrorCode.TRADE_FAIL, "channel.error.vbillRefundFailed", result.getMsg());
|
||||
}
|
||||
|
||||
VbillRefundResp resp = result.getData();
|
||||
RefundResultBo bo = new RefundResultBo()
|
||||
.setOutRefundNo(resp.getOutRefundOrderNo())
|
||||
.setFinishTime(resp.getFinishTime())
|
||||
.setComplete(Boolean.TRUE.equals(resp.getComplete()));
|
||||
// complete=true 表示已终态(REFUNDSUC/REFUNDFAIL), complete=false 表示处理中(REFUNDING)
|
||||
bo.setStatus(Boolean.TRUE.equals(resp.getComplete())
|
||||
? RefundOrderStatusEnum.SUCCESS
|
||||
: RefundOrderStatusEnum.PROGRESS);
|
||||
return bo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package cn.daxpay.open.channel.vbill.service.payment;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.client.VbillChannelClient;
|
||||
import cn.daxpay.open.channel.vbill.client.credential.VbillSdkCredential;
|
||||
import cn.daxpay.open.channel.vbill.client.req.VbillRefundSyncReq;
|
||||
import cn.daxpay.open.channel.vbill.client.resp.VbillRefundSyncResp;
|
||||
import cn.daxpay.open.payment.common.enums.RefundOrderStatusEnum;
|
||||
import cn.daxpay.open.payment.common.result.DaxResult;
|
||||
import cn.daxpay.open.payment.core.trade.bo.RefundResultBo;
|
||||
import cn.daxpay.open.payment.core.trade.entity.PayRefundOrder;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/// # 随行付服务商退款同步业务服务
|
||||
///
|
||||
/// 通过 [VbillChannelClient] 调用子应用查询随行付退款最终状态(`/query/refundQuery`)。
|
||||
/// tranSts: REFUNDSUC / REFUNDFAIL / REFUNDING。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VbillRefundSyncService {
|
||||
|
||||
private final VbillChannelClient vbillChannelClient;
|
||||
|
||||
/// 同步退款状态
|
||||
public RefundResultBo sync(PayRefundOrder refundOrder, VbillSdkCredential credential) {
|
||||
VbillRefundSyncReq req = new VbillRefundSyncReq();
|
||||
req.setCredential(credential);
|
||||
// 优先用网关退款单号 uuid, 兜底用商户退款单号
|
||||
req.setOutRefundOrderNo(refundOrder.getOutRefundNo());
|
||||
req.setOutRefundNo(refundOrder.getRefundNo());
|
||||
|
||||
DaxResult<VbillRefundSyncResp> result = vbillChannelClient.refundSync(req);
|
||||
RefundResultBo bo = new RefundResultBo();
|
||||
if (result.getCode() != 0) {
|
||||
bo.setSyncSuccess(false);
|
||||
bo.setSyncErrorMsg(result.getMsg());
|
||||
return bo;
|
||||
}
|
||||
|
||||
VbillRefundSyncResp resp = result.getData();
|
||||
bo.setSyncSuccess(true);
|
||||
bo.setOutRefundNo(resp.getOutRefundOrderNo());
|
||||
bo.setFinishTime(resp.getFinishTime());
|
||||
bo.setStatus(mapRefundStatus(resp.getRefundStatus()));
|
||||
bo.setComplete(Objects.equals(resp.getRefundStatus(), "REFUNDSUC"));
|
||||
return bo;
|
||||
}
|
||||
|
||||
/// 随行付退款状态 → 平台退款状态
|
||||
private RefundOrderStatusEnum mapRefundStatus(String refundStatus) {
|
||||
if (Objects.equals(refundStatus, "REFUNDSUC")) {
|
||||
return RefundOrderStatusEnum.SUCCESS;
|
||||
}
|
||||
if (Objects.equals(refundStatus, "REFUNDFAIL")) {
|
||||
return RefundOrderStatusEnum.FAIL;
|
||||
}
|
||||
// REFUNDING 处理中
|
||||
return RefundOrderStatusEnum.PROGRESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package cn.daxpay.open.channel.vbill.service.payment;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.client.VbillChannelClient;
|
||||
import cn.daxpay.open.channel.vbill.client.credential.VbillSdkCredential;
|
||||
import cn.daxpay.open.channel.vbill.client.req.VbillSyncReq;
|
||||
import cn.daxpay.open.channel.vbill.client.resp.VbillSyncResp;
|
||||
import cn.daxpay.open.payment.common.enums.PayFundStatusEnum;
|
||||
import cn.daxpay.open.payment.common.result.DaxResult;
|
||||
import cn.daxpay.open.payment.core.trade.bo.PaySyncResultBo;
|
||||
import cn.daxpay.open.payment.core.trade.entity.PayTrade;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/// # 随行付服务商订单同步业务服务
|
||||
///
|
||||
/// 通过 [VbillChannelClient] 调用子应用查询随行付订单状态(`/query/tradeQuery`)。
|
||||
/// tradeState: SUCCESS / PAYING / FAIL / CLOSED。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VbillSyncService {
|
||||
|
||||
private final VbillChannelClient vbillChannelClient;
|
||||
|
||||
/// 同步订单状态
|
||||
public PaySyncResultBo sync(PayTrade order, VbillSdkCredential credential) {
|
||||
VbillSyncReq req = new VbillSyncReq();
|
||||
req.setCredential(credential);
|
||||
// 优先用网关订单号 uuid, 兜底用商户订单号
|
||||
req.setOutOrderNo(order.getOutOrderNo());
|
||||
req.setOutTradeNo(order.getTradeNo());
|
||||
|
||||
DaxResult<VbillSyncResp> result = vbillChannelClient.sync(req);
|
||||
PaySyncResultBo bo = new PaySyncResultBo();
|
||||
if (result.getCode() != 0) {
|
||||
// 同步失败(不抛异常, 由核心层决定重试)
|
||||
bo.setSyncSuccess(false);
|
||||
bo.setSyncErrorMsg(result.getMsg());
|
||||
return bo;
|
||||
}
|
||||
|
||||
VbillSyncResp resp = result.getData();
|
||||
bo.setSyncSuccess(true);
|
||||
bo.setSyncData(resp.getSyncData());
|
||||
bo.setOutOrderNo(resp.getOutOrderNo());
|
||||
bo.setPayStatus(mapTradeState(resp.getTradeState()));
|
||||
// 成功时补充金额/时间/买家
|
||||
if (Objects.equals(resp.getTradeState(), "SUCCESS")) {
|
||||
bo.setAmount(resp.getTotalAmount());
|
||||
bo.setRealAmount(resp.getRealAmount());
|
||||
bo.setFinishTime(resp.getFinishTime());
|
||||
bo.setBuyerId(resp.getBuyerId());
|
||||
}
|
||||
return bo;
|
||||
}
|
||||
|
||||
/// 随行付 tranSts → 平台资金状态
|
||||
private PayFundStatusEnum mapTradeState(String tradeState) {
|
||||
if (Objects.equals(tradeState, "SUCCESS")) {
|
||||
return PayFundStatusEnum.SUCCESS;
|
||||
}
|
||||
if (Objects.equals(tradeState, "FAIL")) {
|
||||
return PayFundStatusEnum.FAIL;
|
||||
}
|
||||
if (Objects.equals(tradeState, "CLOSED")) {
|
||||
return PayFundStatusEnum.CLOSE;
|
||||
}
|
||||
// 其他(PAYING/未知)视为处理中
|
||||
return PayFundStatusEnum.PROCESSING;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package cn.daxpay.open.channel.vbill.strategy;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.client.credential.VbillSdkCredential;
|
||||
import cn.daxpay.open.channel.vbill.service.isv.VbillIsvConfigAssembler;
|
||||
import cn.daxpay.open.channel.vbill.service.payment.VbillCloseService;
|
||||
import cn.daxpay.open.payment.core.strategy.pay.AbsPayCloseStrategy;
|
||||
import cn.daxpay.open.payment.core.strategy.pay.PayStrategyContext;
|
||||
import cn.daxpay.open.payment.core.trade.entity.NormalPayOrder;
|
||||
import cn.daxpay.open.payment.core.trade.entity.PayTrade;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum;
|
||||
import cn.daxpay.open.platform.core.enums.pay.pay.CloseTypeEnum;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/// # 随行付服务商支付关闭策略
|
||||
///
|
||||
/// 从上下文容器读取通道路由参数(channelMchNo / capability),
|
||||
/// 组装通道凭证(委托 [VbillIsvConfigAssembler]), 关闭执行委托给 [VbillCloseService]。
|
||||
///
|
||||
/// 注意: 随行付仅提供关单接口(`/query/close`), 无撤销接口(useCancel 参数由 service 层忽略)。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VbillCloseStrategy extends AbsPayCloseStrategy {
|
||||
|
||||
private final VbillCloseService vbillCloseService;
|
||||
private final VbillIsvConfigAssembler vbillIsvConfigAssembler;
|
||||
|
||||
@Override
|
||||
public ProductEnum getProduct() {
|
||||
return ProductEnum.VBILL_PAY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CloseTypeEnum doClose(PayStrategyContext context, boolean useCancel) {
|
||||
// 从上下文容器读取通道路由参数
|
||||
NormalPayOrder normalOrder = context.getContainer();
|
||||
String channelMchNo = normalOrder != null ? normalOrder.getChannelMchNo() : null;
|
||||
String capability = normalOrder != null ? normalOrder.getCapability() : null;
|
||||
PayTrade trade = context.getTrade();
|
||||
|
||||
// 组装通道调用凭证
|
||||
VbillSdkCredential credential = vbillIsvConfigAssembler.buildConfig(
|
||||
trade.getMchNo(), channelMchNo, capability);
|
||||
|
||||
return vbillCloseService.close(trade, credential, useCancel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package cn.daxpay.open.channel.vbill.strategy;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.client.credential.VbillSdkCredential;
|
||||
import cn.daxpay.open.channel.vbill.service.isv.VbillIsvConfigAssembler;
|
||||
import cn.daxpay.open.channel.vbill.service.payment.VbillPayService;
|
||||
import cn.daxpay.open.payment.core.strategy.pay.AbsNormalPayStrategy;
|
||||
import cn.daxpay.open.payment.core.strategy.pay.PayStrategyContext;
|
||||
import cn.daxpay.open.payment.core.trade.bo.PayTradeResultBo;
|
||||
import cn.daxpay.open.payment.unipay.param.trade.pay.NormalPayParam;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/// # 随行付服务商支付策略
|
||||
///
|
||||
/// 随行付支付(ProductEnum.VBILL_PAY)下发起支付的具体执行策略。
|
||||
/// 配置组装在 [doBeforePay] 阶段完成(委托 [VbillIsvConfigAssembler]), 支付执行委托给 [VbillPayService]。
|
||||
///
|
||||
/// 随行付为聚合服务商模式, 不区分直连/服务商, 仅有一个产品 VBILL_PAY。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VbillPayStrategy extends AbsNormalPayStrategy {
|
||||
|
||||
private final VbillPayService vbillPayService;
|
||||
private final VbillIsvConfigAssembler vbillIsvConfigAssembler;
|
||||
|
||||
@Override
|
||||
public ProductEnum getProduct() {
|
||||
return ProductEnum.VBILL_PAY;
|
||||
}
|
||||
|
||||
/// 支付前预处理: 从容器读取通道路由参数, 组装通道凭证写入上下文
|
||||
/// 配置缺失在此阶段抛异常, fail-fast, 不会进入 doPay
|
||||
@Override
|
||||
public void doBeforePay(PayStrategyContext context) {
|
||||
NormalPayParam payParam = context.getPayParam();
|
||||
VbillSdkCredential credential = vbillIsvConfigAssembler.buildConfig(
|
||||
payParam.getMchNo(), payParam.getChannelMchNo(), payParam.getCapability());
|
||||
context.setChannelConfig(credential);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayTradeResultBo doPay(PayStrategyContext context) {
|
||||
// 直接使用 doBeforePay 预组装的通道凭证
|
||||
VbillSdkCredential credential = context.getChannelConfig(VbillSdkCredential.class);
|
||||
return vbillPayService.pay(context.getTrade(), context.getPayParam(), credential);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package cn.daxpay.open.channel.vbill.strategy;
|
||||
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.ChannelApiCallMode;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.ChannelPayIdType;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.PayCapabilityEnum;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.PayMethodEnum;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.PayProviderEnum;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum;
|
||||
import cn.daxpay.open.payment.core.strategy.product.AbsProductStrategy;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.daxpay.open.platform.core.enums.pay.channel.PayProviderEnum.ALIPAY;
|
||||
import static cn.daxpay.open.platform.core.enums.pay.channel.PayProviderEnum.UNION_PAY;
|
||||
import static cn.daxpay.open.platform.core.enums.pay.channel.PayProviderEnum.WECHAT;
|
||||
|
||||
/// # 随行付支付产品策略
|
||||
///
|
||||
/// 声明随行付(天阙科技)聚合通道支持的服务商模式 + 微信/支付宝/银联 的 JSAPI/扫码/付款码/小程序收银台。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VbillProductStrategy extends AbsProductStrategy {
|
||||
|
||||
/// 支付方式 → 支付能力映射(对齐 vbill 4 个接口能力)
|
||||
private static final Map<PayMethodEnum, List<PayCapabilityEnum>> METHOD_CAP_MAP = Map.ofEntries(
|
||||
// 微信
|
||||
Map.entry(PayMethodEnum.WECHAT_JSAPI, List.of(PayCapabilityEnum.WECHAT_JSAPI)),
|
||||
Map.entry(PayMethodEnum.WECHAT_MINI, List.of(PayCapabilityEnum.WECHAT_MINI)),
|
||||
Map.entry(PayMethodEnum.WECHAT_QR, List.of(PayCapabilityEnum.WECHAT_QR)),
|
||||
Map.entry(PayMethodEnum.WECHAT_BARCODE, List.of(PayCapabilityEnum.WECHAT_BARCODE)),
|
||||
Map.entry(PayMethodEnum.WECHAT_CASHIER, List.of(PayCapabilityEnum.WECHAT_CASHIER)),
|
||||
// 支付宝
|
||||
Map.entry(PayMethodEnum.ALIPAY_JSAPI, List.of(PayCapabilityEnum.ALIPAY_JSAPI)),
|
||||
Map.entry(PayMethodEnum.ALIPAY_MINI, List.of(PayCapabilityEnum.ALIPAY_MINI)),
|
||||
Map.entry(PayMethodEnum.ALIPAY_QR, List.of(PayCapabilityEnum.ALIPAY_QR)),
|
||||
Map.entry(PayMethodEnum.ALIPAY_BARCODE, List.of(PayCapabilityEnum.ALIPAY_BARCODE)),
|
||||
// 银联
|
||||
Map.entry(PayMethodEnum.UNION_JSAPI, List.of(PayCapabilityEnum.UNION_PAY_JSAPI)),
|
||||
Map.entry(PayMethodEnum.UNION_QR, List.of(PayCapabilityEnum.UNION_PAY_QR)),
|
||||
Map.entry(PayMethodEnum.UNION_PAY_BARCODE, List.of(PayCapabilityEnum.UNION_PAY_BARCODE)));
|
||||
|
||||
@Override
|
||||
public ProductEnum getProduct() {
|
||||
return ProductEnum.VBILL_PAY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isIsv() { return true; }
|
||||
|
||||
@Override
|
||||
public boolean isTerminal() { return false; }
|
||||
|
||||
@Override
|
||||
public boolean isSandbox() { return true; }
|
||||
|
||||
@Override
|
||||
public ChannelApiCallMode getApiCallMode() { return ChannelApiCallMode.ISV; }
|
||||
|
||||
@Override
|
||||
public ChannelPayIdType getPayIdType() { return ChannelPayIdType.MCH; }
|
||||
|
||||
@Override
|
||||
public List<PayProviderEnum> supportedPayProviders() {
|
||||
return List.of(WECHAT, ALIPAY, UNION_PAY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<PayMethodEnum, List<PayCapabilityEnum>> methodCapabilityMapping() {
|
||||
return METHOD_CAP_MAP;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package cn.daxpay.open.channel.vbill.strategy;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.client.credential.VbillSdkCredential;
|
||||
import cn.daxpay.open.channel.vbill.service.isv.VbillIsvConfigAssembler;
|
||||
import cn.daxpay.open.channel.vbill.service.payment.VbillRefundService;
|
||||
import cn.daxpay.open.payment.core.strategy.refund.AbsRefundStrategy;
|
||||
import cn.daxpay.open.payment.core.trade.bo.RefundResultBo;
|
||||
import cn.daxpay.open.payment.core.trade.entity.PayRefundOrder;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/// # 随行付服务商退款策略
|
||||
///
|
||||
/// 从退款订单读取通道路由参数(channelMchNo / capability),
|
||||
/// 组装通道凭证(委托 [VbillIsvConfigAssembler]), 退款执行委托给 [VbillRefundService]。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VbillRefundStrategy extends AbsRefundStrategy {
|
||||
|
||||
private final VbillRefundService vbillRefundService;
|
||||
private final VbillIsvConfigAssembler vbillIsvConfigAssembler;
|
||||
|
||||
@Override
|
||||
public ProductEnum getProduct() {
|
||||
return ProductEnum.VBILL_PAY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RefundResultBo doRefund(PayRefundOrder refundOrder) {
|
||||
// 组装通道调用凭证
|
||||
VbillSdkCredential credential = vbillIsvConfigAssembler.buildConfig(
|
||||
refundOrder.getMchNo(), refundOrder.getChannelMchNo(), refundOrder.getCapability());
|
||||
return vbillRefundService.refund(refundOrder, credential);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package cn.daxpay.open.channel.vbill.strategy;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.client.credential.VbillSdkCredential;
|
||||
import cn.daxpay.open.channel.vbill.service.isv.VbillIsvConfigAssembler;
|
||||
import cn.daxpay.open.channel.vbill.service.payment.VbillRefundSyncService;
|
||||
import cn.daxpay.open.payment.core.strategy.refund.AbsSyncRefundStrategy;
|
||||
import cn.daxpay.open.payment.core.trade.bo.RefundResultBo;
|
||||
import cn.daxpay.open.payment.core.trade.entity.PayRefundOrder;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/// # 随行付服务商退款同步策略
|
||||
///
|
||||
/// 从退款订单读取通道路由参数(channelMchNo / capability),
|
||||
/// 组装通道凭证(委托 [VbillIsvConfigAssembler]), 同步执行委托给 [VbillRefundSyncService]。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VbillSyncRefundStrategy extends AbsSyncRefundStrategy {
|
||||
|
||||
private final VbillRefundSyncService vbillRefundSyncService;
|
||||
private final VbillIsvConfigAssembler vbillIsvConfigAssembler;
|
||||
|
||||
@Override
|
||||
public ProductEnum getProduct() {
|
||||
return ProductEnum.VBILL_PAY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RefundResultBo doSync(PayRefundOrder refundOrder) {
|
||||
// 组装通道调用凭证
|
||||
VbillSdkCredential credential = vbillIsvConfigAssembler.buildConfig(
|
||||
refundOrder.getMchNo(), refundOrder.getChannelMchNo(), refundOrder.getCapability());
|
||||
return vbillRefundSyncService.sync(refundOrder, credential);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package cn.daxpay.open.channel.vbill.strategy;
|
||||
|
||||
import cn.daxpay.open.channel.vbill.client.credential.VbillSdkCredential;
|
||||
import cn.daxpay.open.channel.vbill.service.isv.VbillIsvConfigAssembler;
|
||||
import cn.daxpay.open.channel.vbill.service.payment.VbillSyncService;
|
||||
import cn.daxpay.open.payment.core.strategy.pay.PayStrategyContext;
|
||||
import cn.daxpay.open.payment.core.strategy.sync.AbsSyncPayOrderStrategy;
|
||||
import cn.daxpay.open.payment.core.trade.bo.PaySyncResultBo;
|
||||
import cn.daxpay.open.payment.core.trade.entity.NormalPayOrder;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/// # 随行付服务商支付同步策略
|
||||
///
|
||||
/// 从上下文容器读取通道路由参数(channelMchNo / capability),
|
||||
/// 组装通道凭证(委托 [VbillIsvConfigAssembler]), 同步执行委托给 [VbillSyncService]。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VbillSyncStrategy extends AbsSyncPayOrderStrategy {
|
||||
|
||||
private final VbillSyncService vbillSyncService;
|
||||
private final VbillIsvConfigAssembler vbillIsvConfigAssembler;
|
||||
|
||||
@Override
|
||||
public ProductEnum getProduct() {
|
||||
return ProductEnum.VBILL_PAY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaySyncResultBo doSync(PayStrategyContext context) {
|
||||
// 从上下文容器读取通道路由参数
|
||||
NormalPayOrder normalOrder = context.getContainer();
|
||||
String channelMchNo = normalOrder != null ? normalOrder.getChannelMchNo() : null;
|
||||
String capability = normalOrder != null ? normalOrder.getCapability() : null;
|
||||
|
||||
// 组装通道调用凭证
|
||||
VbillSdkCredential credential = vbillIsvConfigAssembler.buildConfig(
|
||||
context.getTrade().getMchNo(), channelMchNo, capability);
|
||||
|
||||
return vbillSyncService.sync(context.getTrade(), credential);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
cn.daxpay.open.channel.vbill.VbillChannelApp
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"isvKeyNotConfigured": "VBill ISV key not configured",
|
||||
"mnoNotConfigured": "VBill merchant number (mno) not configured, please fill in merchant config",
|
||||
"merchantDuplicate": "This Tianque merchant number already exists under this merchant"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"isvKeyNotConfigured": "随行付服务商密钥未配置",
|
||||
"mnoNotConfigured": "天阙商户号(mno)未配置, 请在商户配置中填写",
|
||||
"merchantDuplicate": "该商户下已存在此天阙商户号"
|
||||
}
|
||||
Reference in New Issue
Block a user