From 2f3bfceae3081bd164692b2d837295b7c270552c Mon Sep 17 00:00:00 2001 From: DaxPay Dev Date: Tue, 7 Jul 2026 21:04:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(vbill):=20=E9=9A=8F=E8=A1=8C=E4=BB=98(?= =?UTF-8?q?=E5=A4=A9=E9=98=99=E7=A7=91=E6=8A=80)=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E5=95=86=E9=80=9A=E9=81=93=E5=AF=B9=E6=8E=A5(=E8=BF=9B?= =?UTF-8?q?=E4=BB=B6/=E6=94=AF=E4=BB=98/=E9=80=80=E6=AC=BE/=E5=90=8C?= =?UTF-8?q?=E6=AD=A5/=E5=9B=9E=E8=B0=83)=20+=20=E9=94=99=E8=AF=AF=E7=A0=81?= =?UTF-8?q?i18n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- daxpay-channel/daxpay-channel-vbill/pom.xml | 16 ++ .../open/channel/vbill/VbillChannelApp.java | 16 ++ .../vbill/client/VbillChannelClient.java | 45 ++++ .../client/credential/VbillSdkCredential.java | 29 +++ .../vbill/client/enums/VbillPayBodyType.java | 15 ++ .../vbill/client/enums/VbillPayMethod.java | 15 ++ .../vbill/client/req/VbillCloseReq.java | 15 ++ .../channel/vbill/client/req/VbillPayReq.java | 72 +++++++ .../vbill/client/req/VbillRefundReq.java | 33 +++ .../vbill/client/req/VbillRefundSyncReq.java | 18 ++ .../vbill/client/req/VbillSyncReq.java | 18 ++ .../vbill/client/resp/VbillCloseResp.java | 8 + .../vbill/client/resp/VbillPayResp.java | 43 ++++ .../vbill/client/resp/VbillRefundResp.java | 21 ++ .../client/resp/VbillRefundSyncResp.java | 24 +++ .../vbill/client/resp/VbillSyncResp.java | 45 ++++ .../vbill/config/VbillClientConfig.java | 35 +++ .../callback/VbillCallbackController.java | 38 ++++ .../VbillIsvChannelMerchantController.java | 46 ++++ .../isv/VbillIsvKeyConfigController.java | 47 +++++ .../isv/VbillIsvChannelMerchantConvert.java | 15 ++ .../convert/isv/VbillIsvKeyConfigConvert.java | 27 +++ .../isv/VbillIsvChannelMerchantManager.java | 27 +++ .../isv/VbillIsvChannelMerchantMapper.java | 10 + .../dao/isv/VbillIsvKeyConfigManager.java | 21 ++ .../dao/isv/VbillIsvKeyConfigMapper.java | 10 + .../entity/isv/VbillIsvChannelMerchant.java | 38 ++++ .../vbill/entity/isv/VbillIsvKeyConfig.java | 50 +++++ .../VbillIsvChannelMerchantCreateParam.java | 33 +++ .../param/isv/VbillIsvKeyConfigParam.java | 30 +++ .../isv/VbillIsvChannelMerchantResult.java | 24 +++ .../result/isv/VbillIsvKeyConfigResult.java | 33 +++ .../callback/VbillPayCallbackService.java | 199 ++++++++++++++++++ .../isv/VbillIsvChannelMerchantService.java | 69 ++++++ .../service/isv/VbillIsvConfigAssembler.java | 74 +++++++ .../service/isv/VbillIsvKeyConfigService.java | 58 +++++ .../service/payment/VbillCloseService.java | 45 ++++ .../service/payment/VbillPayService.java | 143 +++++++++++++ .../service/payment/VbillRefundService.java | 54 +++++ .../payment/VbillRefundSyncService.java | 64 ++++++ .../service/payment/VbillSyncService.java | 74 +++++++ .../vbill/strategy/VbillCloseStrategy.java | 49 +++++ .../vbill/strategy/VbillPayStrategy.java | 50 +++++ .../vbill/strategy/VbillProductStrategy.java | 76 +++++++ .../vbill/strategy/VbillRefundStrategy.java | 38 ++++ .../strategy/VbillSyncRefundStrategy.java | 38 ++++ .../vbill/strategy/VbillSyncStrategy.java | 45 ++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../i18n/en-US/error/channel/vbill.json | 5 + .../i18n/zh-CN/error/channel/vbill.json | 5 + 50 files changed, 2004 insertions(+) create mode 100644 daxpay-channel/daxpay-channel-vbill/pom.xml create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/VbillChannelApp.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/VbillChannelClient.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/credential/VbillSdkCredential.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/enums/VbillPayBodyType.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/enums/VbillPayMethod.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/req/VbillCloseReq.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/req/VbillPayReq.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/req/VbillRefundReq.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/req/VbillRefundSyncReq.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/req/VbillSyncReq.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/resp/VbillCloseResp.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/resp/VbillPayResp.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/resp/VbillRefundResp.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/resp/VbillRefundSyncResp.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/resp/VbillSyncResp.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/config/VbillClientConfig.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/controller/callback/VbillCallbackController.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/controller/isv/VbillIsvChannelMerchantController.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/controller/isv/VbillIsvKeyConfigController.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/convert/isv/VbillIsvChannelMerchantConvert.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/convert/isv/VbillIsvKeyConfigConvert.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/dao/isv/VbillIsvChannelMerchantManager.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/dao/isv/VbillIsvChannelMerchantMapper.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/dao/isv/VbillIsvKeyConfigManager.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/dao/isv/VbillIsvKeyConfigMapper.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/entity/isv/VbillIsvChannelMerchant.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/entity/isv/VbillIsvKeyConfig.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/param/isv/VbillIsvChannelMerchantCreateParam.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/param/isv/VbillIsvKeyConfigParam.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/result/isv/VbillIsvChannelMerchantResult.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/result/isv/VbillIsvKeyConfigResult.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/callback/VbillPayCallbackService.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/isv/VbillIsvChannelMerchantService.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/isv/VbillIsvConfigAssembler.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/isv/VbillIsvKeyConfigService.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/payment/VbillCloseService.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/payment/VbillPayService.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/payment/VbillRefundService.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/payment/VbillRefundSyncService.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/payment/VbillSyncService.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/strategy/VbillCloseStrategy.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/strategy/VbillPayStrategy.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/strategy/VbillProductStrategy.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/strategy/VbillRefundStrategy.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/strategy/VbillSyncRefundStrategy.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/strategy/VbillSyncStrategy.java create mode 100644 daxpay-channel/daxpay-channel-vbill/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/channel/vbill.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/channel/vbill.json diff --git a/daxpay-channel/daxpay-channel-vbill/pom.xml b/daxpay-channel/daxpay-channel-vbill/pom.xml new file mode 100644 index 000000000..901d8eb06 --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/pom.xml @@ -0,0 +1,16 @@ + + + 4.0.0 + + + cn.daxpay.open + daxpay-channel + 4.0.0-SNAPSHOT + + + daxpay-channel-vbill + jar + 随行付支付通道 + diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/VbillChannelApp.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/VbillChannelApp.java new file mode 100644 index 000000000..53d78a56b --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/VbillChannelApp.java @@ -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 { +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/VbillChannelClient.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/VbillChannelClient.java new file mode 100644 index 000000000..46e6ebbc7 --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/VbillChannelClient.java @@ -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 pay(@RequestBody VbillPayReq req); + + /// 支付同步(查询随行付订单状态) + @PostExchange("/channel/vbill/sync") + DaxResult sync(@RequestBody VbillSyncReq req); + + /// 关闭订单 + @PostExchange("/channel/vbill/close") + DaxResult close(@RequestBody VbillCloseReq req); + + /// 退款 + @PostExchange("/channel/vbill/refund") + DaxResult refund(@RequestBody VbillRefundReq req); + + /// 退款同步(查询退款状态) + @PostExchange("/channel/vbill/refund-sync") + DaxResult refundSync(@RequestBody VbillRefundSyncReq req); +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/credential/VbillSdkCredential.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/credential/VbillSdkCredential.java new file mode 100644 index 000000000..54f6c1e10 --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/credential/VbillSdkCredential.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/enums/VbillPayBodyType.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/enums/VbillPayBodyType.java new file mode 100644 index 000000000..5c46f4ad3 --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/enums/VbillPayBodyType.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/enums/VbillPayMethod.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/enums/VbillPayMethod.java new file mode 100644 index 000000000..8a3718cd7 --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/enums/VbillPayMethod.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/req/VbillCloseReq.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/req/VbillCloseReq.java new file mode 100644 index 000000000..d66d0737e --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/req/VbillCloseReq.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/req/VbillPayReq.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/req/VbillPayReq.java new file mode 100644 index 000000000..93e526075 --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/req/VbillPayReq.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/req/VbillRefundReq.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/req/VbillRefundReq.java new file mode 100644 index 000000000..df06b0b55 --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/req/VbillRefundReq.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/req/VbillRefundSyncReq.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/req/VbillRefundSyncReq.java new file mode 100644 index 000000000..096b78afb --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/req/VbillRefundSyncReq.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/req/VbillSyncReq.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/req/VbillSyncReq.java new file mode 100644 index 000000000..9ae39c9a7 --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/req/VbillSyncReq.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/resp/VbillCloseResp.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/resp/VbillCloseResp.java new file mode 100644 index 000000000..6a50800ef --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/resp/VbillCloseResp.java @@ -0,0 +1,8 @@ +package cn.daxpay.open.channel.vbill.client.resp; + +import lombok.Data; + +@Data +public class VbillCloseResp { + /// 关单无额外业务返回字段 +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/resp/VbillPayResp.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/resp/VbillPayResp.java new file mode 100644 index 000000000..b4be530d0 --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/resp/VbillPayResp.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/resp/VbillRefundResp.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/resp/VbillRefundResp.java new file mode 100644 index 000000000..ef5d890a3 --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/resp/VbillRefundResp.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/resp/VbillRefundSyncResp.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/resp/VbillRefundSyncResp.java new file mode 100644 index 000000000..3af7e163c --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/resp/VbillRefundSyncResp.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/resp/VbillSyncResp.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/resp/VbillSyncResp.java new file mode 100644 index 000000000..826a5e750 --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/client/resp/VbillSyncResp.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/config/VbillClientConfig.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/config/VbillClientConfig.java new file mode 100644 index 000000000..44c11d61a --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/config/VbillClientConfig.java @@ -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); + } +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/controller/callback/VbillCallbackController.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/controller/callback/VbillCallbackController.java new file mode 100644 index 000000000..4659e9493 --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/controller/callback/VbillCallbackController.java @@ -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 payNotify(@PathVariable("mchNo") String mchNo, + @PathVariable("appId") String appId, + HttpServletRequest request) { + return vbillPayCallbackService.payHandle(request); + } +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/controller/isv/VbillIsvChannelMerchantController.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/controller/isv/VbillIsvChannelMerchantController.java new file mode 100644 index 000000000..13b317f9e --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/controller/isv/VbillIsvChannelMerchantController.java @@ -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 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 create(@RequestBody @Validated VbillIsvChannelMerchantCreateParam param) { + vbillIsvChannelMerchantService.create(param); + return Res.ok(); + } +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/controller/isv/VbillIsvKeyConfigController.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/controller/isv/VbillIsvKeyConfigController.java new file mode 100644 index 000000000..fd3d50e35 --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/controller/isv/VbillIsvKeyConfigController.java @@ -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 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 saveConfig(@RequestBody @Validated VbillIsvKeyConfigParam param) { + vbillIsvKeyConfigService.saveConfig(param); + return Res.ok(); + } +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/convert/isv/VbillIsvChannelMerchantConvert.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/convert/isv/VbillIsvChannelMerchantConvert.java new file mode 100644 index 000000000..3e753055c --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/convert/isv/VbillIsvChannelMerchantConvert.java @@ -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); +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/convert/isv/VbillIsvKeyConfigConvert.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/convert/isv/VbillIsvKeyConfigConvert.java new file mode 100644 index 000000000..92847ca98 --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/convert/isv/VbillIsvKeyConfigConvert.java @@ -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); +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/dao/isv/VbillIsvChannelMerchantManager.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/dao/isv/VbillIsvChannelMerchantManager.java new file mode 100644 index 000000000..1d69daeb0 --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/dao/isv/VbillIsvChannelMerchantManager.java @@ -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 { + + /// 校验同一商户号下天阙商户号不重复 + public boolean existsByMchNoAndVbillMchNo(String mchNo, String vbillMchNo) { + return lambdaQuery() + .eq(VbillIsvChannelMerchant::getMchNo, mchNo) + .eq(VbillIsvChannelMerchant::getVbillMchNo, vbillMchNo) + .exists(); + } + + /// 根据通道商户号查询 + public Optional findByChannelMchNo(String channelMchNo) { + return lambdaQuery() + .eq(VbillIsvChannelMerchant::getChannelMchNo, channelMchNo) + .oneOpt(); + } +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/dao/isv/VbillIsvChannelMerchantMapper.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/dao/isv/VbillIsvChannelMerchantMapper.java new file mode 100644 index 000000000..288d1fe32 --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/dao/isv/VbillIsvChannelMerchantMapper.java @@ -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 { +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/dao/isv/VbillIsvKeyConfigManager.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/dao/isv/VbillIsvKeyConfigManager.java new file mode 100644 index 000000000..0a8832cb8 --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/dao/isv/VbillIsvKeyConfigManager.java @@ -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 { + + /// 根据产品编码查询(平台为唯一服务商, 密钥全局唯一) + public Optional findByProduct(String product) { + return lambdaQuery() + .eq(VbillIsvKeyConfig::getProduct, product) + .oneOpt(); + } +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/dao/isv/VbillIsvKeyConfigMapper.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/dao/isv/VbillIsvKeyConfigMapper.java new file mode 100644 index 000000000..def75951a --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/dao/isv/VbillIsvKeyConfigMapper.java @@ -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 { +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/entity/isv/VbillIsvChannelMerchant.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/entity/isv/VbillIsvChannelMerchant.java new file mode 100644 index 000000000..63282604a --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/entity/isv/VbillIsvChannelMerchant.java @@ -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 { + + /// 通道商户号(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); + } +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/entity/isv/VbillIsvKeyConfig.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/entity/isv/VbillIsvKeyConfig.java new file mode 100644 index 000000000..f6ce211cb --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/entity/isv/VbillIsvKeyConfig.java @@ -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 { + + /// 产品编码 + /// @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); + } +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/param/isv/VbillIsvChannelMerchantCreateParam.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/param/isv/VbillIsvChannelMerchantCreateParam.java new file mode 100644 index 000000000..f8b2f40dc --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/param/isv/VbillIsvChannelMerchantCreateParam.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/param/isv/VbillIsvKeyConfigParam.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/param/isv/VbillIsvKeyConfigParam.java new file mode 100644 index 000000000..fd316165d --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/param/isv/VbillIsvKeyConfigParam.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/result/isv/VbillIsvChannelMerchantResult.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/result/isv/VbillIsvChannelMerchantResult.java new file mode 100644 index 000000000..548326478 --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/result/isv/VbillIsvChannelMerchantResult.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/result/isv/VbillIsvKeyConfigResult.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/result/isv/VbillIsvKeyConfigResult.java new file mode 100644 index 000000000..be667c3e4 --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/result/isv/VbillIsvKeyConfigResult.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/callback/VbillPayCallbackService.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/callback/VbillPayCallbackService.java new file mode 100644 index 000000000..436f9a83a --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/callback/VbillPayCallbackService.java @@ -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 payHandle(HttpServletRequest request) { + Map 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 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 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); + } +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/isv/VbillIsvChannelMerchantService.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/isv/VbillIsvChannelMerchantService.java new file mode 100644 index 000000000..ceef5252f --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/isv/VbillIsvChannelMerchantService.java @@ -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")); + } +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/isv/VbillIsvConfigAssembler.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/isv/VbillIsvConfigAssembler.java new file mode 100644 index 000000000..8cd700830 --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/isv/VbillIsvConfigAssembler.java @@ -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; + } +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/isv/VbillIsvKeyConfigService.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/isv/VbillIsvKeyConfigService.java new file mode 100644 index 000000000..08c29ff85 --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/isv/VbillIsvKeyConfigService.java @@ -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); + } +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/payment/VbillCloseService.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/payment/VbillCloseService.java new file mode 100644 index 000000000..81325824f --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/payment/VbillCloseService.java @@ -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 result = vbillChannelClient.close(req); + if (result.getCode() != 0) { + throw new BizInfoException(DaxPayErrorCode.TRADE_FAIL, "channel.error.vbillCloseFailed", result.getMsg()); + } + return CloseTypeEnum.CLOSE; + } +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/payment/VbillPayService.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/payment/VbillPayService.java new file mode 100644 index 000000000..63b3e3698 --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/payment/VbillPayService.java @@ -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 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) { + } +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/payment/VbillRefundService.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/payment/VbillRefundService.java new file mode 100644 index 000000000..335792bae --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/payment/VbillRefundService.java @@ -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 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; + } +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/payment/VbillRefundSyncService.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/payment/VbillRefundSyncService.java new file mode 100644 index 000000000..76a34ded5 --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/payment/VbillRefundSyncService.java @@ -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 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; + } +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/payment/VbillSyncService.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/payment/VbillSyncService.java new file mode 100644 index 000000000..28a70cbe3 --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/service/payment/VbillSyncService.java @@ -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 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; + } +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/strategy/VbillCloseStrategy.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/strategy/VbillCloseStrategy.java new file mode 100644 index 000000000..4616d020b --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/strategy/VbillCloseStrategy.java @@ -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); + } +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/strategy/VbillPayStrategy.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/strategy/VbillPayStrategy.java new file mode 100644 index 000000000..66982187a --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/strategy/VbillPayStrategy.java @@ -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); + } +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/strategy/VbillProductStrategy.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/strategy/VbillProductStrategy.java new file mode 100644 index 000000000..f95e55b69 --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/strategy/VbillProductStrategy.java @@ -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> 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 supportedPayProviders() { + return List.of(WECHAT, ALIPAY, UNION_PAY); + } + + @Override + public Map> methodCapabilityMapping() { + return METHOD_CAP_MAP; + } +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/strategy/VbillRefundStrategy.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/strategy/VbillRefundStrategy.java new file mode 100644 index 000000000..adcd0edf1 --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/strategy/VbillRefundStrategy.java @@ -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); + } +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/strategy/VbillSyncRefundStrategy.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/strategy/VbillSyncRefundStrategy.java new file mode 100644 index 000000000..718643940 --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/strategy/VbillSyncRefundStrategy.java @@ -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); + } +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/strategy/VbillSyncStrategy.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/strategy/VbillSyncStrategy.java new file mode 100644 index 000000000..7ab8b1a88 --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/strategy/VbillSyncStrategy.java @@ -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); + } +} diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/daxpay-channel/daxpay-channel-vbill/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..e958d75f8 --- /dev/null +++ b/daxpay-channel/daxpay-channel-vbill/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.daxpay.open.channel.vbill.VbillChannelApp diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/channel/vbill.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/channel/vbill.json new file mode 100644 index 000000000..e94a0ca62 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/channel/vbill.json @@ -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" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/channel/vbill.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/channel/vbill.json new file mode 100644 index 000000000..2077ab1a6 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/channel/vbill.json @@ -0,0 +1,5 @@ +{ + "isvKeyNotConfigured": "随行付服务商密钥未配置", + "mnoNotConfigured": "天阙商户号(mno)未配置, 请在商户配置中填写", + "merchantDuplicate": "该商户下已存在此天阙商户号" +}