From 4158b7c28868fd025ceffd70d1a1305f7dd8795d Mon Sep 17 00:00:00 2001 From: DaxPay Dev Date: Tue, 7 Jul 2026 21:02:01 +0800 Subject: [PATCH] =?UTF-8?q?feat(hkrt):=20=E6=B5=B7=E7=A7=91=E8=9E=8D?= =?UTF-8?q?=E9=80=9A=E6=9C=8D=E5=8A=A1=E5=95=86=E9=80=9A=E9=81=93=E5=AF=B9?= =?UTF-8?q?=E6=8E=A5(=E6=94=AF=E4=BB=98/=E9=80=80=E6=AC=BE/=E5=90=8C?= =?UTF-8?q?=E6=AD=A5/=E5=85=B3=E5=8D=95/=E5=9B=9E=E8=B0=83)=20+=20?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E8=A1=A8=20+=20=E5=95=86=E6=88=B7=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- daxpay-channel/daxpay-channel-hkrt/pom.xml | 16 ++ .../open/channel/hkrt/HkrtChannelApp.java | 16 ++ .../hkrt/client/HkrtChannelClient.java | 56 +++++++ .../client/credential/HkrtSdkCredential.java | 26 ++++ .../hkrt/client/enums/HkrtPayBodyType.java | 13 ++ .../hkrt/client/enums/HkrtPayMethod.java | 17 +++ .../hkrt/client/req/HkrtCallbackParseReq.java | 24 +++ .../channel/hkrt/client/req/HkrtCloseReq.java | 20 +++ .../channel/hkrt/client/req/HkrtPayReq.java | 52 +++++++ .../hkrt/client/req/HkrtRefundReq.java | 29 ++++ .../hkrt/client/req/HkrtRefundSyncReq.java | 17 +++ .../channel/hkrt/client/req/HkrtSyncReq.java | 17 +++ .../client/resp/HkrtCallbackParseResp.java | 45 ++++++ .../hkrt/client/resp/HkrtCloseResp.java | 8 + .../channel/hkrt/client/resp/HkrtPayResp.java | 21 +++ .../hkrt/client/resp/HkrtRefundResp.java | 25 +++ .../hkrt/client/resp/HkrtRefundSyncResp.java | 17 +++ .../hkrt/client/resp/HkrtSyncResp.java | 19 +++ .../channel/hkrt/config/HkrtClientConfig.java | 35 +++++ .../callback/HkrtCallbackController.java | 45 ++++++ .../isv/HkrtIsvChannelMerchantController.java | 57 +++++++ .../isv/HkrtIsvKeyConfigController.java | 48 ++++++ .../isv/HkrtIsvChannelMerchantConvert.java | 16 ++ .../convert/isv/HkrtIsvKeyConfigConvert.java | 28 ++++ .../isv/HkrtIsvChannelMerchantManager.java | 28 ++++ .../dao/isv/HkrtIsvChannelMerchantMapper.java | 11 ++ .../hkrt/dao/isv/HkrtIsvKeyConfigManager.java | 22 +++ .../hkrt/dao/isv/HkrtIsvKeyConfigMapper.java | 11 ++ .../entity/isv/HkrtIsvChannelMerchant.java | 41 +++++ .../hkrt/entity/isv/HkrtIsvKeyConfig.java | 46 ++++++ .../HkrtIsvChannelMerchantCreateParam.java | 34 +++++ .../hkrt/param/isv/HkrtIsvKeyConfigParam.java | 29 ++++ .../isv/HkrtIsvChannelMerchantResult.java | 28 ++++ .../result/isv/HkrtIsvKeyConfigResult.java | 30 ++++ .../callback/HkrtPayCallbackService.java | 111 ++++++++++++++ .../callback/HkrtRefundCallbackService.java | 93 ++++++++++++ .../isv/HkrtIsvChannelMerchantService.java | 80 ++++++++++ .../service/isv/HkrtIsvConfigAssembler.java | 75 +++++++++ .../service/isv/HkrtIsvKeyConfigService.java | 60 ++++++++ .../service/payment/HkrtCloseService.java | 48 ++++++ .../hkrt/service/payment/HkrtPayService.java | 143 ++++++++++++++++++ .../service/payment/HkrtRefundService.java | 73 +++++++++ .../payment/HkrtRefundSyncService.java | 63 ++++++++ .../hkrt/service/payment/HkrtSyncService.java | 72 +++++++++ .../hkrt/strategy/HkrtCloseStrategy.java | 51 +++++++ .../hkrt/strategy/HkrtPayStrategy.java | 50 ++++++ .../hkrt/strategy/HkrtProductStrategy.java | 69 +++++++++ .../hkrt/strategy/HkrtRefundStrategy.java | 38 +++++ .../hkrt/strategy/HkrtSyncRefundStrategy.java | 38 +++++ .../hkrt/strategy/HkrtSyncStrategy.java | 45 ++++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + 51 files changed, 2057 insertions(+) create mode 100644 daxpay-channel/daxpay-channel-hkrt/pom.xml create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/HkrtChannelApp.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/HkrtChannelClient.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/credential/HkrtSdkCredential.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/enums/HkrtPayBodyType.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/enums/HkrtPayMethod.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/req/HkrtCallbackParseReq.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/req/HkrtCloseReq.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/req/HkrtPayReq.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/req/HkrtRefundReq.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/req/HkrtRefundSyncReq.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/req/HkrtSyncReq.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/resp/HkrtCallbackParseResp.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/resp/HkrtCloseResp.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/resp/HkrtPayResp.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/resp/HkrtRefundResp.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/resp/HkrtRefundSyncResp.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/resp/HkrtSyncResp.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/config/HkrtClientConfig.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/controller/callback/HkrtCallbackController.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/controller/isv/HkrtIsvChannelMerchantController.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/controller/isv/HkrtIsvKeyConfigController.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/convert/isv/HkrtIsvChannelMerchantConvert.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/convert/isv/HkrtIsvKeyConfigConvert.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/dao/isv/HkrtIsvChannelMerchantManager.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/dao/isv/HkrtIsvChannelMerchantMapper.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/dao/isv/HkrtIsvKeyConfigManager.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/dao/isv/HkrtIsvKeyConfigMapper.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/entity/isv/HkrtIsvChannelMerchant.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/entity/isv/HkrtIsvKeyConfig.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/param/isv/HkrtIsvChannelMerchantCreateParam.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/param/isv/HkrtIsvKeyConfigParam.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/result/isv/HkrtIsvChannelMerchantResult.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/result/isv/HkrtIsvKeyConfigResult.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/callback/HkrtPayCallbackService.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/callback/HkrtRefundCallbackService.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/isv/HkrtIsvChannelMerchantService.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/isv/HkrtIsvConfigAssembler.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/isv/HkrtIsvKeyConfigService.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/payment/HkrtCloseService.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/payment/HkrtPayService.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/payment/HkrtRefundService.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/payment/HkrtRefundSyncService.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/payment/HkrtSyncService.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/strategy/HkrtCloseStrategy.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/strategy/HkrtPayStrategy.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/strategy/HkrtProductStrategy.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/strategy/HkrtRefundStrategy.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/strategy/HkrtSyncRefundStrategy.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/strategy/HkrtSyncStrategy.java create mode 100644 daxpay-channel/daxpay-channel-hkrt/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports diff --git a/daxpay-channel/daxpay-channel-hkrt/pom.xml b/daxpay-channel/daxpay-channel-hkrt/pom.xml new file mode 100644 index 000000000..aea2f8afc --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/pom.xml @@ -0,0 +1,16 @@ + + + 4.0.0 + + + cn.daxpay.open + daxpay-channel + 4.0.0-SNAPSHOT + + + daxpay-channel-hkrt + jar + 海科融通支付通道 + diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/HkrtChannelApp.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/HkrtChannelApp.java new file mode 100644 index 000000000..0c730bb3e --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/HkrtChannelApp.java @@ -0,0 +1,16 @@ +package cn.daxpay.open.channel.hkrt; + +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 HkrtChannelApp { +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/HkrtChannelClient.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/HkrtChannelClient.java new file mode 100644 index 000000000..c45c8f4e5 --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/HkrtChannelClient.java @@ -0,0 +1,56 @@ +package cn.daxpay.open.channel.hkrt.client; + +import cn.daxpay.open.channel.hkrt.client.req.HkrtCallbackParseReq; +import cn.daxpay.open.channel.hkrt.client.req.HkrtCloseReq; +import cn.daxpay.open.channel.hkrt.client.req.HkrtPayReq; +import cn.daxpay.open.channel.hkrt.client.req.HkrtRefundReq; +import cn.daxpay.open.channel.hkrt.client.req.HkrtRefundSyncReq; +import cn.daxpay.open.channel.hkrt.client.req.HkrtSyncReq; +import cn.daxpay.open.channel.hkrt.client.resp.HkrtCallbackParseResp; +import cn.daxpay.open.channel.hkrt.client.resp.HkrtCloseResp; +import cn.daxpay.open.channel.hkrt.client.resp.HkrtPayResp; +import cn.daxpay.open.channel.hkrt.client.resp.HkrtRefundResp; +import cn.daxpay.open.channel.hkrt.client.resp.HkrtRefundSyncResp; +import cn.daxpay.open.channel.hkrt.client.resp.HkrtSyncResp; +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/hkrt`。 +/// 回调验签与解析统一转发子应用(parse-pay / parse-refund), 主应用不再自验签。 +@HttpExchange(accept = MediaType.APPLICATION_JSON_VALUE, contentType = MediaType.APPLICATION_JSON_VALUE) +public interface HkrtChannelClient { + + /// 支付下单 + @PostExchange("/channel/hkrt/pay") + DaxResult pay(@RequestBody HkrtPayReq req); + + /// 支付同步(查询海科融通订单状态) + @PostExchange("/channel/hkrt/sync") + DaxResult sync(@RequestBody HkrtSyncReq req); + + /// 关闭订单 + @PostExchange("/channel/hkrt/close") + DaxResult close(@RequestBody HkrtCloseReq req); + + /// 退款 + @PostExchange("/channel/hkrt/refund") + DaxResult refund(@RequestBody HkrtRefundReq req); + + /// 退款同步(查询退款状态) + @PostExchange("/channel/hkrt/refund-sync") + DaxResult refundSync(@RequestBody HkrtRefundSyncReq req); + + /// 支付回调验签解析(转发子应用) + @PostExchange("/channel/hkrt/callback/parse-pay") + DaxResult parsePayCallback(@RequestBody HkrtCallbackParseReq req); + + /// 退款回调验签解析(转发子应用) + @PostExchange("/channel/hkrt/callback/parse-refund") + DaxResult parseRefundCallback(@RequestBody HkrtCallbackParseReq req); +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/credential/HkrtSdkCredential.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/credential/HkrtSdkCredential.java new file mode 100644 index 000000000..b7938ef6e --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/credential/HkrtSdkCredential.java @@ -0,0 +1,26 @@ +package cn.daxpay.open.channel.hkrt.client.credential; + +import lombok.Data; + +/// # 海科融通 SDK 凭证(主应用侧) +/// +/// 与子应用 dax-pay-channel-two 的 `HkrtSdkCredential` 镜像, 跨 HTTP 传输时字段对齐。 +/// 主应用从服务商密钥配置(HkrtIsvKeyConfig) + 通道商户绑定(HkrtIsvChannelMerchant) 组装, +/// 下发给子应用发起海科融通 API 调用。 +/// +/// 海科融通签名机制: 仅用 accessKey 做 MD5 大写签名(参数字母升序 + 末尾拼 accessKey), 无需证书。 +@Data +public class HkrtSdkCredential { + /// 服务商编号(agent_no) + private String agentNo; + /// 海科商户编号(merch_no) + private String merchNo; + /// 接入机构标识(access_id) + private String accessId; + /// SAAS 终端号(pn) + private String pn; + /// 签名密钥(access_key, MD5 大写签名) + private String accessKey; + /// 是否沙箱环境 + private Boolean sandbox; +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/enums/HkrtPayBodyType.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/enums/HkrtPayBodyType.java new file mode 100644 index 000000000..b2243758f --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/enums/HkrtPayBodyType.java @@ -0,0 +1,13 @@ +package cn.daxpay.open.channel.hkrt.client.enums; + +/// # 海科融通支付内容类型(主应用侧, 与子应用镜像) +/// +/// 海科融通无 LINK(跳转链接) 类型, 仅 QR_CODE / JSAPI / IDENTIFIER 三种。 +public enum HkrtPayBodyType { + /// 二维码内容 + QR_CODE, + /// JSAPI/小程序调起参数 JSON + JSAPI, + /// 通用标识码 + IDENTIFIER; +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/enums/HkrtPayMethod.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/enums/HkrtPayMethod.java new file mode 100644 index 000000000..89925a875 --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/enums/HkrtPayMethod.java @@ -0,0 +1,17 @@ +package cn.daxpay.open.channel.hkrt.client.enums; + +/// # 海科融通支付方式(主应用侧, 与子应用镜像) +/// +/// 海科融通的支付方式由 method 单独决定, 不像拉卡拉需要 accountType + transType 三要素。 +public enum HkrtPayMethod { + /// 微信 JSAPI(公众号 / 小程序) + WECHAT_JSAPI, + /// 支付宝扫码(当面付二维码) + ALIPAY_QR, + /// 支付宝 JSAPI(生活号 / 小程序) + ALIPAY_JSAPI, + /// 银联二维码(云闪付) + UNION_QR, + /// 条码支付(微信 / 支付宝 / 银联付款码被扫) + BARCODE; +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/req/HkrtCallbackParseReq.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/req/HkrtCallbackParseReq.java new file mode 100644 index 000000000..7fb920b42 --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/req/HkrtCallbackParseReq.java @@ -0,0 +1,24 @@ +package cn.daxpay.open.channel.hkrt.client.req; + +import cn.daxpay.open.channel.hkrt.client.credential.HkrtSdkCredential; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/// # 海科融通回调验签解析请求(主应用侧, 与子应用镜像) +/// +/// 主应用收到海科异步通知后, 将原始报文转发到子应用做验签与字段解析。 +@Data +public class HkrtCallbackParseReq { + + /// 通道调用凭证(用 accessKey 验签) + @NotNull(message = "{validation.field.credential.notNull}") + private HkrtSdkCredential credential; + + /// 海科回调原始报文(JSON 字符串) + @NotBlank(message = "{validation.field.rawData.notBlank}") + private String rawData; + + /// 是否退款回调(true=退款回调, false=支付回调) + private boolean refund; +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/req/HkrtCloseReq.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/req/HkrtCloseReq.java new file mode 100644 index 000000000..280b0d5aa --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/req/HkrtCloseReq.java @@ -0,0 +1,20 @@ +package cn.daxpay.open.channel.hkrt.client.req; + +import cn.daxpay.open.channel.hkrt.client.credential.HkrtSdkCredential; +import lombok.Data; + +/// # 海科融通通道关单请求(主应用侧, 与子应用镜像) +@Data +public class HkrtCloseReq { + + private HkrtSdkCredential credential; + + /// 原商户订单号(与 originTradeNo 二选一) + private String originOutTradeNo; + + /// 原海科融通交易号(与 originOutTradeNo 二选一) + private String originTradeNo; + + /// 客户端IP + private String clientIp; +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/req/HkrtPayReq.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/req/HkrtPayReq.java new file mode 100644 index 000000000..cf696318a --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/req/HkrtPayReq.java @@ -0,0 +1,52 @@ +package cn.daxpay.open.channel.hkrt.client.req; + +import cn.daxpay.open.channel.hkrt.client.credential.HkrtSdkCredential; +import cn.daxpay.open.channel.hkrt.client.enums.HkrtPayBodyType; +import cn.daxpay.open.channel.hkrt.client.enums.HkrtPayMethod; +import lombok.Data; + +import java.time.OffsetDateTime; + +/// # 海科融通通道支付请求(主应用侧, 与子应用镜像) +/// +/// 海科融通的支付方式由 method 单独决定, 不需要 accountType/transType/payBodyType 三要素。 +/// amount 单位为分, 透传给子应用, 由子应用负责分→元转换。 +@Data +public class HkrtPayReq { + + /// 通道调用凭证 + private HkrtSdkCredential credential; + + /// 商户订单号(主应用支付交易号) + private String outTradeNo; + + /// 订单金额(单位: 分) + private Long amount; + + /// 商品标题 + private String title; + + /// 商品描述 + private String description; + + /// 支付方式(WECHAT_JSAPI / ALIPAY_QR / ALIPAY_JSAPI / UNION_QR / BARCODE) + private HkrtPayMethod method; + + /// 用户标识(JSAPI/MINI 必填) + private String openId; + + /// 付款码(BARCODE 必填) + private String authCode; + + /// 客户端IP + private String clientIp; + + /// 异步通知地址 + private String notifyUrl; + + /// 订单过期时间 + private OffsetDateTime expireTime; + + /// 支付内容类型(主应用预先计算, 子应用透传到响应) + private HkrtPayBodyType payBodyType; +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/req/HkrtRefundReq.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/req/HkrtRefundReq.java new file mode 100644 index 000000000..468c61397 --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/req/HkrtRefundReq.java @@ -0,0 +1,29 @@ +package cn.daxpay.open.channel.hkrt.client.req; + +import cn.daxpay.open.channel.hkrt.client.credential.HkrtSdkCredential; +import lombok.Data; + +/// # 海科融通通道退款请求(主应用侧, 与子应用镜像) +@Data +public class HkrtRefundReq { + + private HkrtSdkCredential credential; + + /// 商户退款单号 + private String outRefundNo; + + /// 原商户订单号 + private String originOutTradeNo; + + /// 原海科融通交易号 + private String originTradeNo; + + /// 退款金额(单位: 分) + private Long amount; + + /// 退款原因 + private String reason; + + /// 客户端IP + private String clientIp; +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/req/HkrtRefundSyncReq.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/req/HkrtRefundSyncReq.java new file mode 100644 index 000000000..dcea462b9 --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/req/HkrtRefundSyncReq.java @@ -0,0 +1,17 @@ +package cn.daxpay.open.channel.hkrt.client.req; + +import cn.daxpay.open.channel.hkrt.client.credential.HkrtSdkCredential; +import lombok.Data; + +/// # 海科融通通道退款查询请求(主应用侧, 与子应用镜像) +@Data +public class HkrtRefundSyncReq { + + private HkrtSdkCredential credential; + + /// 商户退款单号(与 originTradeNo 二选一) + private String outRefundNo; + + /// 原海科融通退款交易号(与 outRefundNo 二选一) + private String originTradeNo; +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/req/HkrtSyncReq.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/req/HkrtSyncReq.java new file mode 100644 index 000000000..514114961 --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/req/HkrtSyncReq.java @@ -0,0 +1,17 @@ +package cn.daxpay.open.channel.hkrt.client.req; + +import cn.daxpay.open.channel.hkrt.client.credential.HkrtSdkCredential; +import lombok.Data; + +/// # 海科融通通道订单查询请求(主应用侧, 与子应用镜像) +@Data +public class HkrtSyncReq { + + private HkrtSdkCredential credential; + + /// 商户订单号(与 tradeNo 二选一) + private String outTradeNo; + + /// 海科融通交易号(与 outTradeNo 二选一) + private String tradeNo; +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/resp/HkrtCallbackParseResp.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/resp/HkrtCallbackParseResp.java new file mode 100644 index 000000000..6ae17985c --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/resp/HkrtCallbackParseResp.java @@ -0,0 +1,45 @@ +package cn.daxpay.open.channel.hkrt.client.resp; + +import lombok.Data; + +import java.time.OffsetDateTime; + +/// # 海科融通回调解析响应(主应用侧, 与子应用镜像) +/// +/// 子应用对海科异步通知验签通过后, 将解析出的标准化字段回传主应用。 +@Data +public class HkrtCallbackParseResp { + + /// 是否验签通过 + private Boolean success; + + /// 海科交易号(trade_no) + private String tradeNo; + + /// 商户订单号(out_trade_no, 支付回调)或商户退款单号(退款回调) + private String outTradeNo; + + /// 银行交易号(bank_trade_no) + private String transOrderNo; + + /// 交易状态(抽象态 SUCCESS / FAIL, 已屏蔽海科数字码) + private String tradeStatus; + + /// 金额(单位: 分, order_amount / refund_amount 元转分) + private Long amount; + + /// 完成时间(东八区 OffsetDateTime) + private OffsetDateTime finishTime; + + /// 支付类型(扩展字段: WX/ALI/UNIONQR) + private String payType; + + /// 支付模式(扩展字段: JSAPI/NATIVE) + private String payMode; + + /// 买家标识(微信 openid / 支付宝 buyer_openid) + private String buyerId; + + /// 银行卡类型(银联回调时的 card_type) + private String bankType; +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/resp/HkrtCloseResp.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/resp/HkrtCloseResp.java new file mode 100644 index 000000000..561cb7b95 --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/resp/HkrtCloseResp.java @@ -0,0 +1,8 @@ +package cn.daxpay.open.channel.hkrt.client.resp; + +import lombok.Data; + +/// # 海科融通通道关单响应(主应用侧, 与子应用镜像) +@Data +public class HkrtCloseResp { +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/resp/HkrtPayResp.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/resp/HkrtPayResp.java new file mode 100644 index 000000000..0ca7a6afd --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/resp/HkrtPayResp.java @@ -0,0 +1,21 @@ +package cn.daxpay.open.channel.hkrt.client.resp; + +import cn.daxpay.open.channel.hkrt.client.enums.HkrtPayBodyType; +import lombok.Data; + +import java.time.OffsetDateTime; + +/// # 海科融通通道支付响应(主应用侧, 与子应用镜像) +@Data +public class HkrtPayResp { + + private String outTradeNo; + private String tradeNo; + private String payBody; + private HkrtPayBodyType payBodyType; + private Boolean complete; + private Long totalAmount; + private Long payerAmount; + private OffsetDateTime finishTime; + private String buyerId; +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/resp/HkrtRefundResp.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/resp/HkrtRefundResp.java new file mode 100644 index 000000000..92fc2640d --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/resp/HkrtRefundResp.java @@ -0,0 +1,25 @@ +package cn.daxpay.open.channel.hkrt.client.resp; + +import lombok.Data; + +import java.time.OffsetDateTime; + +/// # 海科融通通道退款响应(主应用侧, 与子应用镜像) +@Data +public class HkrtRefundResp { + + /// 商户退款单号(透传请求 outRefundNo) + private String outRefundNo; + + /// 海科退款订单号(refund_no) + private String tradeNo; + + /// 交易状态(抽象态 SUCCESS / FAIL / PROCESSING, 已屏蔽海科 trade_status 数字码) + private String tradeStatus; + + /// 是否已终态完成 + private Boolean complete; + + /// 完成时间 + private OffsetDateTime finishTime; +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/resp/HkrtRefundSyncResp.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/resp/HkrtRefundSyncResp.java new file mode 100644 index 000000000..c9d6701f6 --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/resp/HkrtRefundSyncResp.java @@ -0,0 +1,17 @@ +package cn.daxpay.open.channel.hkrt.client.resp; + +import lombok.Data; + +import java.time.OffsetDateTime; + +/// # 海科融通通道退款查询响应(主应用侧, 与子应用镜像) +@Data +public class HkrtRefundSyncResp { + + private String outRefundNo; + private String tradeNo; + /// 退款状态: SUCCESS / FAIL / PROCESSING + private String refundStatus; + private OffsetDateTime finishTime; + private String syncData; +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/resp/HkrtSyncResp.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/resp/HkrtSyncResp.java new file mode 100644 index 000000000..7cb3b66ba --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/client/resp/HkrtSyncResp.java @@ -0,0 +1,19 @@ +package cn.daxpay.open.channel.hkrt.client.resp; + +import lombok.Data; + +import java.time.OffsetDateTime; + +/// # 海科融通通道订单查询响应(主应用侧, 与子应用镜像) +@Data +public class HkrtSyncResp { + + private String outTradeNo; + private String tradeNo; + /// 交易状态: SUCCESS / FAIL / CLOSED / 其他(处理中) + private String tradeState; + private Long totalAmount; + private String buyerId; + private OffsetDateTime finishTime; + private String syncData; +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/config/HkrtClientConfig.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/config/HkrtClientConfig.java new file mode 100644 index 000000000..6babdd41e --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/config/HkrtClientConfig.java @@ -0,0 +1,35 @@ +package cn.daxpay.open.channel.hkrt.config; + +import cn.daxpay.open.channel.hkrt.client.HkrtChannelClient; +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 HkrtClientConfig { + + @Bean + public HkrtChannelClient hkrtChannelClient( + 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(HkrtChannelClient.class); + } +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/controller/callback/HkrtCallbackController.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/controller/callback/HkrtCallbackController.java new file mode 100644 index 000000000..37ba217fe --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/controller/callback/HkrtCallbackController.java @@ -0,0 +1,45 @@ +package cn.daxpay.open.channel.hkrt.controller.callback; + +import cn.daxpay.open.channel.hkrt.service.callback.HkrtPayCallbackService; +import cn.daxpay.open.channel.hkrt.service.callback.HkrtRefundCallbackService; +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; + +/// # 海科融通支付回调通知控制器 +/// +/// 海科融通异步通知入口(支付/退款), 不走 Sa-Token 认证(由安全配置放行 `/unipay/callback/**`)。 +/// 海科融通回调验签只需全局服务商 accessKey(从 HkrtIsvKeyConfig 读取), 不需 channelMchNo, +/// 因此路径不带 channelMchNo, 凭 out_trade_no 反查 PayTrade。 +@Tag(name = "海科融通支付回调通知控制器") +@RestController +@RequestMapping("/unipay/callback/{mchNo}/{appId}/hkrt") +@RequiredArgsConstructor +public class HkrtCallbackController { + + private final HkrtPayCallbackService hkrtPayCallbackService; + private final HkrtRefundCallbackService hkrtRefundCallbackService; + + /// 海科融通支付回调 + @Operation(summary = "海科融通支付回调") + @PostMapping("/pay") + public String payNotify(@PathVariable("mchNo") String mchNo, + @PathVariable("appId") String appId, + HttpServletRequest request) { + return hkrtPayCallbackService.payHandle(request); + } + + /// 海科融通退款回调 + @Operation(summary = "海科融通退款回调") + @PostMapping("/refund") + public String refundNotify(@PathVariable("mchNo") String mchNo, + @PathVariable("appId") String appId, + HttpServletRequest request) { + return hkrtRefundCallbackService.refundHandle(request); + } +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/controller/isv/HkrtIsvChannelMerchantController.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/controller/isv/HkrtIsvChannelMerchantController.java new file mode 100644 index 000000000..954a52f50 --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/controller/isv/HkrtIsvChannelMerchantController.java @@ -0,0 +1,57 @@ +package cn.daxpay.open.channel.hkrt.controller.isv; + +import cn.daxpay.open.channel.hkrt.param.isv.HkrtIsvChannelMerchantCreateParam; +import cn.daxpay.open.channel.hkrt.result.isv.HkrtIsvChannelMerchantResult; +import cn.daxpay.open.channel.hkrt.service.isv.HkrtIsvChannelMerchantService; +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/hkrt/isv-channel-merchant") +@RequiredArgsConstructor +public class HkrtIsvChannelMerchantController { + + private final HkrtIsvChannelMerchantService hkrtIsvChannelMerchantService; + + @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(hkrtIsvChannelMerchantService.findByChannelMchNo(channelMchNo)); + } + + @PermCode(code = "manage", nameCn = "通道商户管理", nameEn = "Channel Merchant Manage") + @Operation(summary = "创建海科融通通道商户") + @PostMapping("/create") + public Result create(@RequestBody @Validated HkrtIsvChannelMerchantCreateParam param) { + hkrtIsvChannelMerchantService.create(param); + return Res.ok(); + } + + @PermCode(code = "manage", nameCn = "通道商户管理", nameEn = "Channel Merchant Manage") + @Operation(summary = "更新SAAS终端号") + @PostMapping("/update-pn") + public Result updatePn( + @NotBlank(message = "{validation.field.channelMerchantNo.notBlank}") String channelMchNo, + @NotBlank(message = "{validation.field.pn.notBlank}") String pn) { + hkrtIsvChannelMerchantService.updatePn(channelMchNo, pn); + return Res.ok(); + } +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/controller/isv/HkrtIsvKeyConfigController.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/controller/isv/HkrtIsvKeyConfigController.java new file mode 100644 index 000000000..301f2bef4 --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/controller/isv/HkrtIsvKeyConfigController.java @@ -0,0 +1,48 @@ +package cn.daxpay.open.channel.hkrt.controller.isv; + +import cn.daxpay.open.channel.hkrt.convert.isv.HkrtIsvKeyConfigConvert; +import cn.daxpay.open.channel.hkrt.param.isv.HkrtIsvKeyConfigParam; +import cn.daxpay.open.channel.hkrt.result.isv.HkrtIsvKeyConfigResult; +import cn.daxpay.open.channel.hkrt.service.isv.HkrtIsvKeyConfigService; +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:hkrt:isv") +@Validated +@Tag(name = "海科融通服务商密钥配置") +@RestController +@RequestMapping("/admin/hkrt/isv-key-config") +@RequiredArgsConstructor +public class HkrtIsvKeyConfigController { + + private final HkrtIsvKeyConfigService hkrtIsvKeyConfigService; + + @PermCode(code = "view", nameCn = "海科融通服务商查看", nameEn = "Hkrt ISV View") + @Operation(summary = "查询海科融通服务商密钥配置") + @GetMapping("/find-config") + public Result findConfig( + @NotBlank(message = "{validation.field.product.notBlank}") String product) { + return Res.ok(HkrtIsvKeyConfigConvert.CONVERT.toResult(hkrtIsvKeyConfigService.findByProduct(product))); + } + + @PermCode(code = "manage", nameCn = "海科融通服务商管理", nameEn = "Hkrt ISV Manage") + @Operation(summary = "保存海科融通服务商密钥配置") + @PostMapping("/save-config") + public Result saveConfig(@RequestBody @Validated HkrtIsvKeyConfigParam param) { + hkrtIsvKeyConfigService.saveConfig(param); + return Res.ok(); + } +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/convert/isv/HkrtIsvChannelMerchantConvert.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/convert/isv/HkrtIsvChannelMerchantConvert.java new file mode 100644 index 000000000..75e8bd2a2 --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/convert/isv/HkrtIsvChannelMerchantConvert.java @@ -0,0 +1,16 @@ +package cn.daxpay.open.channel.hkrt.convert.isv; + +import cn.daxpay.open.channel.hkrt.entity.isv.HkrtIsvChannelMerchant; +import cn.daxpay.open.channel.hkrt.result.isv.HkrtIsvChannelMerchantResult; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/// # 海科融通通道商户绑定转换 +/// +@Mapper +public interface HkrtIsvChannelMerchantConvert { + + HkrtIsvChannelMerchantConvert CONVERT = Mappers.getMapper(HkrtIsvChannelMerchantConvert.class); + + HkrtIsvChannelMerchantResult toResult(HkrtIsvChannelMerchant entity); +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/convert/isv/HkrtIsvKeyConfigConvert.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/convert/isv/HkrtIsvKeyConfigConvert.java new file mode 100644 index 000000000..92606b2e4 --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/convert/isv/HkrtIsvKeyConfigConvert.java @@ -0,0 +1,28 @@ +package cn.daxpay.open.channel.hkrt.convert.isv; + +import cn.daxpay.open.channel.hkrt.entity.isv.HkrtIsvKeyConfig; +import cn.daxpay.open.channel.hkrt.param.isv.HkrtIsvKeyConfigParam; +import cn.daxpay.open.channel.hkrt.result.isv.HkrtIsvKeyConfigResult; +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; +import org.mapstruct.factory.Mappers; + +/// # 海科融通服务商密钥配置转换 +/// +@Mapper +public interface HkrtIsvKeyConfigConvert { + + HkrtIsvKeyConfigConvert CONVERT = Mappers.getMapper(HkrtIsvKeyConfigConvert.class); + + /// 转换为返回对象 + HkrtIsvKeyConfigResult toResult(HkrtIsvKeyConfig entity); + + /// 转换为实体 + HkrtIsvKeyConfig toEntity(HkrtIsvKeyConfigParam param); + + /// 更新源数据到实体(空值不覆盖, 密钥为空时保留原值) + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) + void copy(HkrtIsvKeyConfigParam param, @MappingTarget HkrtIsvKeyConfig entity); +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/dao/isv/HkrtIsvChannelMerchantManager.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/dao/isv/HkrtIsvChannelMerchantManager.java new file mode 100644 index 000000000..7c177df2e --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/dao/isv/HkrtIsvChannelMerchantManager.java @@ -0,0 +1,28 @@ +package cn.daxpay.open.channel.hkrt.dao.isv; + +import cn.daxpay.open.channel.hkrt.entity.isv.HkrtIsvChannelMerchant; +import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +/// # 海科融通通道商户绑定 +/// +@Repository +public class HkrtIsvChannelMerchantManager extends BaseManager { + + /// 校验同一商户下海科商户号不重复 + public boolean existsByMchNoAndMerchNo(String mchNo, String merchNo) { + return lambdaQuery() + .eq(HkrtIsvChannelMerchant::getMchNo, mchNo) + .eq(HkrtIsvChannelMerchant::getMerchNo, merchNo) + .exists(); + } + + /// 根据通道商户号查询 + public Optional findByChannelMchNo(String channelMchNo) { + return lambdaQuery() + .eq(HkrtIsvChannelMerchant::getChannelMchNo, channelMchNo) + .oneOpt(); + } +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/dao/isv/HkrtIsvChannelMerchantMapper.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/dao/isv/HkrtIsvChannelMerchantMapper.java new file mode 100644 index 000000000..375cf7eca --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/dao/isv/HkrtIsvChannelMerchantMapper.java @@ -0,0 +1,11 @@ +package cn.daxpay.open.channel.hkrt.dao.isv; + +import cn.daxpay.open.channel.hkrt.entity.isv.HkrtIsvChannelMerchant; +import com.github.yulichang.base.MPJBaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/// # 海科融通通道商户绑定 +/// +@Mapper +public interface HkrtIsvChannelMerchantMapper extends MPJBaseMapper { +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/dao/isv/HkrtIsvKeyConfigManager.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/dao/isv/HkrtIsvKeyConfigManager.java new file mode 100644 index 000000000..258c0ed5f --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/dao/isv/HkrtIsvKeyConfigManager.java @@ -0,0 +1,22 @@ +package cn.daxpay.open.channel.hkrt.dao.isv; + +import cn.daxpay.open.channel.hkrt.entity.isv.HkrtIsvKeyConfig; +import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +/// # 海科融通服务商密钥配置 +/// +@Slf4j +@Service +public class HkrtIsvKeyConfigManager extends BaseManager { + + /// 根据产品编码查询(平台为唯一服务商, 密钥全局唯一) + public Optional findByProduct(String product) { + return lambdaQuery() + .eq(HkrtIsvKeyConfig::getProduct, product) + .oneOpt(); + } +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/dao/isv/HkrtIsvKeyConfigMapper.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/dao/isv/HkrtIsvKeyConfigMapper.java new file mode 100644 index 000000000..9f3769604 --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/dao/isv/HkrtIsvKeyConfigMapper.java @@ -0,0 +1,11 @@ +package cn.daxpay.open.channel.hkrt.dao.isv; + +import cn.daxpay.open.channel.hkrt.entity.isv.HkrtIsvKeyConfig; +import com.github.yulichang.base.MPJBaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/// # 海科融通服务商密钥配置 +/// +@Mapper +public interface HkrtIsvKeyConfigMapper extends MPJBaseMapper { +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/entity/isv/HkrtIsvChannelMerchant.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/entity/isv/HkrtIsvChannelMerchant.java new file mode 100644 index 000000000..3b3780c50 --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/entity/isv/HkrtIsvChannelMerchant.java @@ -0,0 +1,41 @@ +package cn.daxpay.open.channel.hkrt.entity.isv; + +import cn.daxpay.open.channel.hkrt.convert.isv.HkrtIsvChannelMerchantConvert; +import cn.daxpay.open.channel.hkrt.result.isv.HkrtIsvChannelMerchantResult; +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; + +/// # 海科融通通道商户绑定 +/// +/// 海科融通服务商模式下, 子商户绑定海科商户号(merchNo) + SAAS 终端号(pn), +/// 密钥(agentNo/accessId/accessKey)由服务商全局配置([HkrtIsvKeyConfig])提供。 +/// 同一商户下海科商户号不重复。 +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@TableName(value = "hkrt_isv_channel_merchant", autoResultMap = true) +public class HkrtIsvChannelMerchant extends MchBaseEntity implements ToResult { + + /// 通道商户号(HKRT+雪花) + private String channelMchNo; + + /// 所属支付产品 + /// @see cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum + private String product; + + /// 海科商户号(merch_no) + private String merchNo; + + /// SAAS 终端号(pn) + private String pn; + + /// 转换 + @Override + public HkrtIsvChannelMerchantResult toResult() { + return HkrtIsvChannelMerchantConvert.CONVERT.toResult(this); + } +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/entity/isv/HkrtIsvKeyConfig.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/entity/isv/HkrtIsvKeyConfig.java new file mode 100644 index 000000000..5f75c3ed3 --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/entity/isv/HkrtIsvKeyConfig.java @@ -0,0 +1,46 @@ +package cn.daxpay.open.channel.hkrt.entity.isv; + +import cn.daxpay.open.channel.hkrt.convert.isv.HkrtIsvKeyConfigConvert; +import cn.daxpay.open.channel.hkrt.result.isv.HkrtIsvKeyConfigResult; +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 查询), +/// 子商户仅需商户号(merchNo) + SAAS 终端号(pn), 见 [HkrtIsvChannelMerchant]。 +/// +/// 签名算法: MD5 大写(参数字母升序 + 末尾拼 accessKey), 无需证书。 +/// 敏感字段(accessKey)通过 [DataEncryptTypeHandler] 加密入库。 +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@TableName(value = "hkrt_isv_key_config", autoResultMap = true) +public class HkrtIsvKeyConfig extends MpBaseEntity implements ToResult { + + /// 产品编码 + /// @see cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum + private String product; + + /// 服务商编号(agent_no) + private String agentNo; + + /// 接入机构标识(access_id) + private String accessId; + + /// 签名密钥(access_key, MD5 大写签名, 加密存储) + @TableField(typeHandler = DataEncryptTypeHandler.class) + private String accessKey; + + /// 转换 + @Override + public HkrtIsvKeyConfigResult toResult() { + return HkrtIsvKeyConfigConvert.CONVERT.toResult(this); + } +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/param/isv/HkrtIsvChannelMerchantCreateParam.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/param/isv/HkrtIsvChannelMerchantCreateParam.java new file mode 100644 index 000000000..c8901fab7 --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/param/isv/HkrtIsvChannelMerchantCreateParam.java @@ -0,0 +1,34 @@ +package cn.daxpay.open.channel.hkrt.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 HkrtIsvChannelMerchantCreateParam { + + /// 商户号 + @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; + + /// 海科商户号 + @Schema(description = "海科商户号") + @NotBlank(message = "{validation.field.merchNo.notBlank}") + private String merchNo; +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/param/isv/HkrtIsvKeyConfigParam.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/param/isv/HkrtIsvKeyConfigParam.java new file mode 100644 index 000000000..06c12d0ac --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/param/isv/HkrtIsvKeyConfigParam.java @@ -0,0 +1,29 @@ +package cn.daxpay.open.channel.hkrt.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 HkrtIsvKeyConfigParam { + + @NotBlank(message = "{validation.field.product.notBlank}") + @Schema(description = "产品编码") + private String product; + + @NotBlank(message = "{validation.field.agentNo.notBlank}") + @Schema(description = "服务商编号") + private String agentNo; + + @NotBlank(message = "{validation.field.accessId.notBlank}") + @Schema(description = "接入机构标识") + private String accessId; + + @Schema(description = "签名密钥(加密存储)") + private String accessKey; +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/result/isv/HkrtIsvChannelMerchantResult.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/result/isv/HkrtIsvChannelMerchantResult.java new file mode 100644 index 000000000..156b40659 --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/result/isv/HkrtIsvChannelMerchantResult.java @@ -0,0 +1,28 @@ +package cn.daxpay.open.channel.hkrt.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 HkrtIsvChannelMerchantResult extends MchTradeBaseResult { + + @Schema(description = "通道商户号") + private String channelMchNo; + + @Schema(description = "所属支付产品") + private String product; + + @Schema(description = "海科商户号") + private String merchNo; + + @Schema(description = "SAAS 终端号") + private String pn; +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/result/isv/HkrtIsvKeyConfigResult.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/result/isv/HkrtIsvKeyConfigResult.java new file mode 100644 index 000000000..247617e9b --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/result/isv/HkrtIsvKeyConfigResult.java @@ -0,0 +1,30 @@ +package cn.daxpay.open.channel.hkrt.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 HkrtIsvKeyConfigResult extends BaseResult { + + @Schema(description = "产品编码") + private String product; + + @Schema(description = "服务商编号") + private String agentNo; + + @Schema(description = "接入机构标识") + private String accessId; + + @SensitiveInfo(front = 4, end = 4) + @Schema(description = "签名密钥(加密存储)") + private String accessKey; +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/callback/HkrtPayCallbackService.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/callback/HkrtPayCallbackService.java new file mode 100644 index 000000000..e3de0ede5 --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/callback/HkrtPayCallbackService.java @@ -0,0 +1,111 @@ +package cn.daxpay.open.channel.hkrt.service.callback; + +import cn.daxpay.open.channel.hkrt.client.HkrtChannelClient; +import cn.daxpay.open.channel.hkrt.client.credential.HkrtSdkCredential; +import cn.daxpay.open.channel.hkrt.client.req.HkrtCallbackParseReq; +import cn.daxpay.open.channel.hkrt.client.resp.HkrtCallbackParseResp; +import cn.daxpay.open.channel.hkrt.dao.isv.HkrtIsvKeyConfigManager; +import cn.daxpay.open.channel.hkrt.entity.isv.HkrtIsvKeyConfig; +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.extra.servlet.JakartaServletUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Objects; + +/// # 海科融通支付回调处理服务 +/// +/// 海科融通异步通知 → 主应用接收原始 JSON body → 转发子应用验签与解析 → +/// 构建 [CallbackData] 交由 [PayCallbackService] 更新订单状态。 +/// +/// 验签与字段解析统一由子应用 dax-pay-channel-two 的 HkrtCallbackParseService 完成 +/// (MD5 大写签名, 参数字母升序 + 末尾拼 accessKey), 主应用不再自验签。 +/// 子应用返回抽象态 tradeStatus(SUCCESS/FAIL), 主应用映射为 CallbackStatusEnum 后交框架处理。 +@Slf4j +@Service +@RequiredArgsConstructor +public class HkrtPayCallbackService { + + private static final String NOTIFY_SUCCESS = "success"; + private static final String NOTIFY_FAIL = "fail"; + + private final HkrtIsvKeyConfigManager hkrtIsvKeyConfigManager; + private final PayCallbackService payCallbackService; + private final HkrtChannelClient hkrtChannelClient; + + /// 支付回调处理 + public String payHandle(HttpServletRequest request) { + // 1. 提取回调原始数据(JSON body) + String body = JakartaServletUtil.getBody(request); + + // 2. 获取全局服务商 accessKey(只读查询, 不创建记录) + HkrtIsvKeyConfig keyConfig = hkrtIsvKeyConfigManager.findByProduct(ProductEnum.HKRT_PAY.getCode()) + .orElse(null); + if (keyConfig == null || keyConfig.getAccessKey() == null) { + log.error("海科融通支付回调: 服务商密钥未配置, 无法验签"); + return NOTIFY_FAIL; + } + + // 3. 转发子应用验签与解析 + HkrtCallbackParseResp resp = parseCallback(body, keyConfig.getAccessKey(), false); + if (resp == null) { + return NOTIFY_FAIL; + } + try { + // 4. 构建 CallbackData 交框架更新订单状态 + CallbackData callbackData = toCallbackData(resp); + payCallbackService.payCallback(callbackData); + } catch (Exception e) { + log.error("海科融通支付回调业务处理失败: tradeNo={}", resp.getOutTradeNo(), e); + return NOTIFY_FAIL; + } + return NOTIFY_SUCCESS; + } + + /// 转发子应用验签解析(支付回调) + private HkrtCallbackParseResp parseCallback(String body, String accessKey, boolean refund) { + HkrtSdkCredential credential = new HkrtSdkCredential(); + credential.setAccessKey(accessKey); + HkrtCallbackParseReq req = new HkrtCallbackParseReq(); + req.setCredential(credential); + req.setRawData(body); + req.setRefund(refund); + var result = hkrtChannelClient.parsePayCallback(req); + if (result.getCode() != 0) { + log.error("海科融通支付回调: 子应用解析失败: {}", result.getMsg()); + return null; + } + HkrtCallbackParseResp resp = result.getData(); + if (resp == null || !Boolean.TRUE.equals(resp.getSuccess())) { + log.error("海科融通支付回调验签失败"); + return null; + } + return resp; + } + + /// 子应用解析结果 → CallbackData(抽象态 → 框架状态码) + /// + /// 框架 [PayCallbackService] 以 `CallbackStatusEnum.SUCCESS.getCode()`(success) 比对 tradeStatus: + /// - SUCCESS → success(走成功分支) + /// - 其他 → 不设 tradeStatus(走 fail 分支), 记错误信息 + private CallbackData toCallbackData(HkrtCallbackParseResp resp) { + CallbackData callbackData = new CallbackData(); + // out_trade_no = 下单时传入的平台 tradeNo + callbackData.setTradeNo(resp.getOutTradeNo()); + // trade_no = 海科融通交易号 + callbackData.setOutTradeNo(resp.getTradeNo()); + // 抽象态 → 框架状态码 + if (Objects.equals(resp.getTradeStatus(), "SUCCESS")) { + callbackData.setTradeStatus(CallbackStatusEnum.SUCCESS.getCode()); + } else { + callbackData.setCallbackErrorMsg("海科融通回调状态非成功: " + resp.getTradeStatus()); + } + callbackData.setFinishTime(resp.getFinishTime()); + return callbackData; + } +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/callback/HkrtRefundCallbackService.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/callback/HkrtRefundCallbackService.java new file mode 100644 index 000000000..fed831f0b --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/callback/HkrtRefundCallbackService.java @@ -0,0 +1,93 @@ +package cn.daxpay.open.channel.hkrt.service.callback; + +import cn.daxpay.open.channel.hkrt.client.HkrtChannelClient; +import cn.daxpay.open.channel.hkrt.client.credential.HkrtSdkCredential; +import cn.daxpay.open.channel.hkrt.client.req.HkrtCallbackParseReq; +import cn.daxpay.open.channel.hkrt.client.resp.HkrtCallbackParseResp; +import cn.daxpay.open.channel.hkrt.dao.isv.HkrtIsvKeyConfigManager; +import cn.daxpay.open.channel.hkrt.entity.isv.HkrtIsvKeyConfig; +import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum; +import cn.hutool.extra.servlet.JakartaServletUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Objects; + +/// # 海科融通退款回调处理服务 +/// +/// 海科融通退款异步通知 → 主应用接收原始 JSON body → 转发子应用验签与解析 → +/// 记录退款回调结果。 +/// +/// 验签与字段解析统一由子应用 dax-pay-channel-two 的 HkrtCallbackParseService 完成, +/// 主应用不再自验签。子应用返回抽象态 tradeStatus(SUCCESS/FAIL)。 +/// +/// TODO 退款单状态更新待接入退款回调框架(平台目前无独立 RefundCallbackService, +/// 后续可通过 PayRefundService 或新增退款回调入口完成状态流转)。本期仅记录日志。 +@Slf4j +@Service +@RequiredArgsConstructor +public class HkrtRefundCallbackService { + + private static final String NOTIFY_SUCCESS = "success"; + private static final String NOTIFY_FAIL = "fail"; + /// 退款成功(抽象态, 由子应用统一转换) + private static final String REFUND_STATUS_SUCCESS = "SUCCESS"; + + private final HkrtIsvKeyConfigManager hkrtIsvKeyConfigManager; + private final HkrtChannelClient hkrtChannelClient; + + /// 退款回调处理 + public String refundHandle(HttpServletRequest request) { + // 1. 提取回调原始数据(JSON body) + String body = JakartaServletUtil.getBody(request); + + // 2. 获取全局服务商 accessKey(只读查询, 不创建记录) + HkrtIsvKeyConfig keyConfig = hkrtIsvKeyConfigManager.findByProduct(ProductEnum.HKRT_PAY.getCode()) + .orElse(null); + if (keyConfig == null || keyConfig.getAccessKey() == null) { + log.error("海科融通退款回调: 服务商密钥未配置, 无法验签"); + return NOTIFY_FAIL; + } + + // 3. 转发子应用验签与解析 + HkrtCallbackParseResp resp = parseCallback(body, keyConfig.getAccessKey()); + if (resp == null) { + return NOTIFY_FAIL; + } + + // 4. 记录退款回调结果, TODO 接入退款单状态更新框架 + String outRefundNo = resp.getOutTradeNo(); + String refundNo = resp.getTradeNo(); + String refundStatus = resp.getTradeStatus(); + log.info("海科融通退款回调: outRefundNo={}, refundNo={}, refundStatus={}, amount={}, finishTime={}", + outRefundNo, refundNo, refundStatus, resp.getAmount(), resp.getFinishTime()); + if (Objects.equals(REFUND_STATUS_SUCCESS, refundStatus)) { + // TODO 退款成功, 更新退款单状态(待接入退款回调框架) + log.info("海科融通退款成功: outRefundNo={}", outRefundNo); + } + return NOTIFY_SUCCESS; + } + + /// 转发子应用验签解析(退款回调) + private HkrtCallbackParseResp parseCallback(String body, String accessKey) { + HkrtSdkCredential credential = new HkrtSdkCredential(); + credential.setAccessKey(accessKey); + HkrtCallbackParseReq req = new HkrtCallbackParseReq(); + req.setCredential(credential); + req.setRawData(body); + req.setRefund(true); + var result = hkrtChannelClient.parseRefundCallback(req); + if (result.getCode() != 0) { + log.error("海科融通退款回调: 子应用解析失败: {}", result.getMsg()); + return null; + } + HkrtCallbackParseResp resp = result.getData(); + if (resp == null || !Boolean.TRUE.equals(resp.getSuccess())) { + log.error("海科融通退款回调验签失败"); + return null; + } + return resp; + } +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/isv/HkrtIsvChannelMerchantService.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/isv/HkrtIsvChannelMerchantService.java new file mode 100644 index 000000000..8f2716bc9 --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/isv/HkrtIsvChannelMerchantService.java @@ -0,0 +1,80 @@ +package cn.daxpay.open.channel.hkrt.service.isv; + +import cn.daxpay.open.channel.hkrt.dao.isv.HkrtIsvChannelMerchantManager; +import cn.daxpay.open.channel.hkrt.entity.isv.HkrtIsvChannelMerchant; +import cn.daxpay.open.channel.hkrt.param.isv.HkrtIsvChannelMerchantCreateParam; +import cn.daxpay.open.channel.hkrt.result.isv.HkrtIsvChannelMerchantResult; +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; + +/// # 海科融通通道商户管理 +/// +/// 海科融通服务商模式下, 子商户关联到服务商本身(密钥全局唯一), 不挂靠具体应用。 +/// 一个商户下同一海科商户号(merchNo)只允许绑定一次。 +/// +@Slf4j +@Service +@RequiredArgsConstructor +public class HkrtIsvChannelMerchantService { + + private final ChannelMerchantManager channelMerchantManager; + private final HkrtIsvChannelMerchantManager hkrtIsvChannelMerchantManager; + + /// 创建海科融通通道商户 + @Transactional(rollbackFor = Exception.class) + public void create(HkrtIsvChannelMerchantCreateParam param) { + // 校验同一商户下海科商户号不重复 + if (hkrtIsvChannelMerchantManager.existsByMchNoAndMerchNo( + param.getMchNo(), param.getMerchNo())) { + // 海科融通: 该商户下已存在此海科商户号 + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "channel.error.hkrtMerchantDuplicate"); + } + // 生成通道商户号: 通道前缀 + 雪花ID(无分隔符, 仅供排障辨识) + String channelMchNo = ChannelMchNoGenerateUtil.generate("HKRT"); + // 写通用通道商户主表 + 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 HkrtIsvChannelMerchant(); + entity.setMchNo(param.getMchNo()); + entity.setChannelMchNo(channelMchNo); + entity.setProduct(param.getProduct()); + entity.setMerchNo(param.getMerchNo()); + hkrtIsvChannelMerchantManager.save(entity); + } + + /// 根据通道商户号查询海科融通通道商户配置 + public HkrtIsvChannelMerchantResult findByChannelMchNo(String channelMchNo) { + return hkrtIsvChannelMerchantManager.lambdaQuery() + .eq(HkrtIsvChannelMerchant::getChannelMchNo, channelMchNo) + .oneOpt() + .map(HkrtIsvChannelMerchant::toResult) + // 海科融通: 通道商户配置不存在 + .orElseThrow(() -> new DataNotExistException("error.payment.channel.channelMerchantNotExist")); + } + + /// 更新 SAAS 终端号(pn) + @Transactional(rollbackFor = Exception.class) + public void updatePn(String channelMchNo, String pn) { + var entity = hkrtIsvChannelMerchantManager.findByChannelMchNo(channelMchNo) + // 海科融通: 通道商户配置不存在 + .orElseThrow(() -> new DataNotExistException("error.payment.channel.channelMerchantNotExist")); + entity.setPn(pn); + hkrtIsvChannelMerchantManager.updateById(entity); + } +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/isv/HkrtIsvConfigAssembler.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/isv/HkrtIsvConfigAssembler.java new file mode 100644 index 000000000..dc37ec7f7 --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/isv/HkrtIsvConfigAssembler.java @@ -0,0 +1,75 @@ +package cn.daxpay.open.channel.hkrt.service.isv; + +import cn.daxpay.open.channel.hkrt.client.credential.HkrtSdkCredential; +import cn.daxpay.open.channel.hkrt.dao.isv.HkrtIsvChannelMerchantManager; +import cn.daxpay.open.channel.hkrt.entity.isv.HkrtIsvChannelMerchant; +import cn.daxpay.open.channel.hkrt.entity.isv.HkrtIsvKeyConfig; +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; + +/// # 海科融通服务商通道凭证组装器 +/// +/// 从服务商密钥配置([HkrtIsvKeyConfig]) + 通道商户绑定([HkrtIsvChannelMerchant]) 组装通道调用凭证, +/// 下发给子应用 dax-pay-channel-two 发起海科融通 API 调用。 +/// +/// 字段映射(对齐海科融通接口): +/// - agent_no / access_id / access_key ← [HkrtIsvKeyConfig](服务商级, 全局唯一) +/// - merch_no ← [HkrtIsvChannelMerchant.merchNo](海科商户号) +/// - pn ← [HkrtIsvChannelMerchant.pn](SAAS 终端号) +/// +/// 供支付策略([cn.daxpay.open.channel.hkrt.strategy.*])组装通道调用凭证。 +@Slf4j +@Service +@RequiredArgsConstructor +public class HkrtIsvConfigAssembler { + + private final HkrtIsvChannelMerchantManager hkrtIsvChannelMerchantManager; + private final HkrtIsvKeyConfigService hkrtIsvKeyConfigService; + private final PayProductConfigManager payProductConfigManager; + + /// 组装海科融通通道调用凭证(下发给子应用) + /// + /// @param mchNo 商户号(保留参数对齐签名, 服务商密钥全局唯一不依赖此字段) + /// @param channelMchNo 通道商户号(海科商户绑定主键) + /// @param capability 支付能力编码(保留参数, 海科融通不按能力路由) + /// @return 海科融通 SDK 凭证(含服务商密钥 + 商户号/终端号) + public HkrtSdkCredential buildConfig(String mchNo, String channelMchNo, String capability) { + // 服务商密钥(全局唯一, 含 agentNo/accessId/accessKey; 缺失或关键字段为空时 fail-fast) + HkrtIsvKeyConfig keyConfig = hkrtIsvKeyConfigService.getByProductForPay(ProductEnum.HKRT_PAY.getCode()); + // 通道商户绑定(取 merchNo + pn) + HkrtIsvChannelMerchant channelMerchant = hkrtIsvChannelMerchantManager.findByChannelMchNo(channelMchNo) + // 海科融通: 通道商户配置不存在 + .orElseThrow(() -> new DataNotExistException("error.payment.channel.channelMerchantNotExist")); + + HkrtSdkCredential credential = new HkrtSdkCredential(); + // 服务商身份与密钥 + credential.setAgentNo(keyConfig.getAgentNo()); + credential.setAccessId(keyConfig.getAccessId()); + credential.setAccessKey(keyConfig.getAccessKey()); + // 沙箱状态读取支付产品配置的生效环境(不挂在密钥配置上) + boolean sandbox = payProductConfigManager.findByProduct(ProductEnum.HKRT_PAY.getCode()) + .map(PayProductConfig::getActiveEnv) + .map(PayEnvEnum.SANDBOX.getCode()::equals) + .orElse(false); + credential.setSandbox(sandbox); + // 子商户身份 + credential.setMerchNo(channelMerchant.getMerchNo()); + // SAAS 终端号(从通道商户配置取) + String pn = channelMerchant.getPn(); + if (StrUtil.isBlank(pn)) { + // 海科融通: 终端号未配置, 请在商户配置中填写 + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "channel.error.hkrtTermNoNotConfigured"); + } + credential.setPn(pn); + return credential; + } +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/isv/HkrtIsvKeyConfigService.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/isv/HkrtIsvKeyConfigService.java new file mode 100644 index 000000000..780c60c14 --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/isv/HkrtIsvKeyConfigService.java @@ -0,0 +1,60 @@ +package cn.daxpay.open.channel.hkrt.service.isv; + +import cn.daxpay.open.channel.hkrt.convert.isv.HkrtIsvKeyConfigConvert; +import cn.daxpay.open.channel.hkrt.dao.isv.HkrtIsvKeyConfigManager; +import cn.daxpay.open.channel.hkrt.entity.isv.HkrtIsvKeyConfig; +import cn.daxpay.open.channel.hkrt.param.isv.HkrtIsvKeyConfigParam; +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 HkrtIsvKeyConfigService { + + private final HkrtIsvKeyConfigManager hkrtIsvKeyConfigManager; + + /// 根据产品编码查询密钥配置, 不存在则创建默认记录 + @Transactional(rollbackFor = Exception.class) + public HkrtIsvKeyConfig findByProduct(String product) { + var existing = hkrtIsvKeyConfigManager.findByProduct(product); + if (existing.isPresent()) { + return existing.get(); + } + var config = new HkrtIsvKeyConfig() + .setProduct(product); + hkrtIsvKeyConfigManager.save(config); + return config; + } + + /// 支付场景查询服务商密钥(必填校验, 不创建记录) + /// + /// 与 [findByProduct] 的 upsert 语义不同, 此方法只读不写: + /// 记录不存在或关键字段(agentNo/accessId/accessKey)任一为空时 fail-fast, + /// 避免空凭证下发到子应用后子应用才发现问题。 + public HkrtIsvKeyConfig getByProductForPay(String product) { + HkrtIsvKeyConfig config = hkrtIsvKeyConfigManager.findByProduct(product) + // 海科融通: 服务商密钥未配置 + .orElseThrow(() -> new BizInfoException("channel.error.hkrtIsvKeyNotConfigured")); + if (StrUtil.hasBlank(config.getAgentNo(), config.getAccessId(), config.getAccessKey())) { + throw new BizInfoException("channel.error.hkrtIsvKeyNotConfigured"); + } + return config; + } + + /// 保存服务商密钥配置 + @Transactional(rollbackFor = Exception.class) + public void saveConfig(HkrtIsvKeyConfigParam param) { + var config = this.findByProduct(param.getProduct()); + HkrtIsvKeyConfigConvert.CONVERT.copy(param, config); + hkrtIsvKeyConfigManager.updateById(config); + } +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/payment/HkrtCloseService.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/payment/HkrtCloseService.java new file mode 100644 index 000000000..b618ef93e --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/payment/HkrtCloseService.java @@ -0,0 +1,48 @@ +package cn.daxpay.open.channel.hkrt.service.payment; + +import cn.daxpay.open.channel.hkrt.client.HkrtChannelClient; +import cn.daxpay.open.channel.hkrt.client.credential.HkrtSdkCredential; +import cn.daxpay.open.channel.hkrt.client.req.HkrtCloseReq; +import cn.daxpay.open.channel.hkrt.client.resp.HkrtCloseResp; +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; + +/// # 海科融通服务商关单业务服务 +/// +/// 通过 [HkrtChannelClient] 调用子应用关闭海科融通订单。 +/// 海科融通仅提供关单接口, 无撤销接口, useCancel 参数忽略。 +@Slf4j +@Service +@RequiredArgsConstructor +public class HkrtCloseService { + + private final HkrtChannelClient hkrtChannelClient; + + /// 关闭订单 + /// + /// @param order 支付订单 + /// @param credential 通道凭证 + /// @param useCancel 是否撤销(海科融通不支持撤销, 忽略此参数) + /// @param clientIp 客户端IP(取自原下单订单, 透传至海科融通) + /// @return 关闭类型(恒为 CLOSE) + public CloseTypeEnum close(PayTrade order, HkrtSdkCredential credential, boolean useCancel, String clientIp) { + HkrtCloseReq req = new HkrtCloseReq(); + req.setCredential(credential); + req.setOriginOutTradeNo(order.getTradeNo()); + req.setOriginTradeNo(order.getOutOrderNo()); + req.setClientIp(clientIp); + + DaxResult result = hkrtChannelClient.close(req); + if (result.getCode() != 0) { + throw new BizInfoException(DaxPayErrorCode.TRADE_FAIL, "channel.error.hkrtCloseFailed", result.getMsg()); + } + // 海科融通只有关单, 返回 CLOSE + return CloseTypeEnum.CLOSE; + } +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/payment/HkrtPayService.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/payment/HkrtPayService.java new file mode 100644 index 000000000..f014a768a --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/payment/HkrtPayService.java @@ -0,0 +1,143 @@ +package cn.daxpay.open.channel.hkrt.service.payment; + +import cn.daxpay.open.channel.hkrt.client.HkrtChannelClient; +import cn.daxpay.open.channel.hkrt.client.credential.HkrtSdkCredential; +import cn.daxpay.open.channel.hkrt.client.enums.HkrtPayBodyType; +import cn.daxpay.open.channel.hkrt.client.enums.HkrtPayMethod; +import cn.daxpay.open.channel.hkrt.client.req.HkrtPayReq; +import cn.daxpay.open.channel.hkrt.client.resp.HkrtPayResp; +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; + +/// # 海科融通服务商支付执行业务服务 +/// +/// 通过 [HkrtChannelClient] 调用子应用 dax-pay-channel-two 完成海科融通支付下单。 +/// 请求构建、响应解析全部在本类中完成。 +/// +/// 海科融通聚合通道: 一个接口承载微信/支付宝/银联三种底层渠道, 由 method 单独决定路由, +/// 不像拉卡拉需要 accountType + transType 三要素。 +@Slf4j +@Service +@RequiredArgsConstructor +public class HkrtPayService { + + private final HkrtChannelClient hkrtChannelClient; + private final PlatformUrlConfigService platformUrlConfigService; + + /// 执行海科融通支付 + /// + /// @param order 支付订单(tradeNo 作为 out_trade_no) + /// @param payParam 支付参数 + /// @param credential 通道调用凭证(服务商密钥 + 商户号/终端号) + /// @return 支付结果 + public PayTradeResultBo pay(PayTrade order, NormalPayParam payParam, HkrtSdkCredential credential) { + // 平台支付方式 → 海科融通 method + bodyType + MethodMapping mapping = mapMethod(payParam.getMethod()); + + // 构建请求 + HkrtPayReq req = new HkrtPayReq(); + // 使用支付交易号作为商户订单号透传给海科融通, 回调时凭此反查 PayTrade + req.setOutTradeNo(order.getTradeNo()); + req.setAmount(payParam.getAmount()); + req.setTitle(payParam.getTitle()); + req.setDescription(payParam.getDescription()); + req.setMethod(mapping.method); + 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 = hkrtChannelClient.pay(req); + if (result.getCode() != 0) { + throw new BizInfoException(DaxPayErrorCode.TRADE_FAIL, "channel.error.hkrtPayFailed", result.getMsg()); + } + + return toPayResult(result.getData()); + } + + /// 生成海科融通支付异步通知地址(海科融通→平台) + /// + /// 路径约定: `{backendBaseUrl}/unipay/callback/{mchNo}/{appId}/hkrt/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/{}/{}/hkrt/pay", + base, order.getMchNo(), order.getAppId()); + } + + /// 平台支付方式([PayMethodEnum] code) → 海科融通 method + bodyType + private static MethodMapping mapMethod(String methodCode) { + PayMethodEnum m = PayMethodEnum.findByCode(methodCode); + HkrtPayMethod method = switch (m) { + // 微信 JSAPI / 小程序 + case WECHAT_JSAPI, WECHAT_MINI -> HkrtPayMethod.WECHAT_JSAPI; + // 支付宝扫码(当面付二维码) + case ALIPAY_QR -> HkrtPayMethod.ALIPAY_QR; + // 支付宝 JSAPI / 小程序 + case ALIPAY_JSAPI, ALIPAY_MINI -> HkrtPayMethod.ALIPAY_JSAPI; + // 银联二维码(云闪付) + case UNION_QR -> HkrtPayMethod.UNION_QR; + // 条码支付(微信/支付宝/银联付款码被扫) + case WECHAT_BARCODE, ALIPAY_BARCODE, UNION_PAY_BARCODE -> HkrtPayMethod.BARCODE; + default -> throw new BizInfoException(CommonErrorCode.UN_SUPPORTED_OPERATE, + "channel.error.hkrtUnsupportedPayMethod", methodCode); + }; + // 支付内容类型由 method 推导 + HkrtPayBodyType bodyType = switch (method) { + case WECHAT_JSAPI, ALIPAY_JSAPI -> HkrtPayBodyType.JSAPI; + case ALIPAY_QR, UNION_QR -> HkrtPayBodyType.QR_CODE; + case BARCODE -> null; + }; + return new MethodMapping(method, bodyType); + } + + /// 解析子应用响应为支付结果 BO + private PayTradeResultBo toPayResult(HkrtPayResp resp) { + PayTradeResultBo bo = new PayTradeResultBo() + .setOutOrderNo(resp.getTradeNo()) + .setComplete(Boolean.TRUE.equals(resp.getComplete())) + .setPayBody(resp.getPayBody()); + + // 支付内容类型映射(子应用 HkrtPayBodyType → 平台 PayBodyTypeEnum) + // 海科融通无 LINK 类型 + if (resp.getPayBodyType() != null) { + switch (resp.getPayBodyType()) { + 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.getPayerAmount()); + bo.setBuyerPayAmount(resp.getPayerAmount()); + // 用户标识 + bo.setBuyerId(resp.getBuyerId()); + return bo; + } + + /// 支付方式映射内部封装 + private record MethodMapping(HkrtPayMethod method, HkrtPayBodyType bodyType) { + } +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/payment/HkrtRefundService.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/payment/HkrtRefundService.java new file mode 100644 index 000000000..26314a092 --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/payment/HkrtRefundService.java @@ -0,0 +1,73 @@ +package cn.daxpay.open.channel.hkrt.service.payment; + +import cn.daxpay.open.channel.hkrt.client.HkrtChannelClient; +import cn.daxpay.open.channel.hkrt.client.credential.HkrtSdkCredential; +import cn.daxpay.open.channel.hkrt.client.req.HkrtRefundReq; +import cn.daxpay.open.channel.hkrt.client.resp.HkrtRefundResp; +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; + +/// # 海科融通服务商退款执行业务服务 +/// +/// 通过 [HkrtChannelClient] 调用子应用完成海科融通退款。 +@Slf4j +@Service +@RequiredArgsConstructor +public class HkrtRefundService { + + private final HkrtChannelClient hkrtChannelClient; + private final HkrtRefundSyncService hkrtRefundSyncService; + + /// 执行退款 + public RefundResultBo refund(PayRefundOrder refundOrder, HkrtSdkCredential credential) { + HkrtRefundReq req = new HkrtRefundReq(); + req.setCredential(credential); + // 退款单号(作为海科融通 out_refund_no) + req.setOutRefundNo(refundOrder.getRefundNo()); + // 原支付订单号(平台 tradeNo 作为原 out_trade_no) + req.setOriginOutTradeNo(refundOrder.getOrderNo()); + // 原通道交易号(退款同步查询用, 首次退款时可能为空) + req.setOriginTradeNo(refundOrder.getOutOrderNo()); + req.setAmount(refundOrder.getAmount()); + req.setReason(refundOrder.getReason()); + req.setClientIp(refundOrder.getClientIp()); + + DaxResult result = hkrtChannelClient.refund(req); + if (result.getCode() != 0) { + throw new BizInfoException(DaxPayErrorCode.TRADE_FAIL, "channel.error.hkrtRefundFailed", result.getMsg()); + } + + HkrtRefundResp resp = result.getData(); + RefundResultBo bo = new RefundResultBo() + .setOutRefundNo(resp.getTradeNo()) + .setFinishTime(resp.getFinishTime()) + .setComplete(Boolean.TRUE.equals(resp.getComplete())); + // 退款状态(子应用统一输出抽象态, 屏蔽海科数字码) + String tradeStatus = resp.getTradeStatus(); + if ("FAIL".equals(tradeStatus)) { + // 退款失败 + bo.setStatus(RefundOrderStatusEnum.FAIL); + throw new BizInfoException(DaxPayErrorCode.TRADE_FAIL, "channel.error.hkrtRefundFailed", + "refund trade_status=FAIL"); + } + bo.setStatus(Boolean.TRUE.equals(resp.getComplete()) + ? RefundOrderStatusEnum.SUCCESS + : RefundOrderStatusEnum.PROGRESS); + // 退款同步成功但无完成时间(海科退款接口不返回 end_time), 立即补查 refund-query 拿 end_time, + // 避免订单进 SUCCESS 终态后被退款同步跳过导致 finishTime 永久为空 + if (bo.isComplete() && bo.getFinishTime() == null) { + RefundResultBo syncBo = hkrtRefundSyncService.sync(refundOrder, credential); + if (syncBo.getFinishTime() != null) { + bo.setFinishTime(syncBo.getFinishTime()); + } + } + return bo; + } +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/payment/HkrtRefundSyncService.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/payment/HkrtRefundSyncService.java new file mode 100644 index 000000000..6c39a8775 --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/payment/HkrtRefundSyncService.java @@ -0,0 +1,63 @@ +package cn.daxpay.open.channel.hkrt.service.payment; + +import cn.daxpay.open.channel.hkrt.client.HkrtChannelClient; +import cn.daxpay.open.channel.hkrt.client.credential.HkrtSdkCredential; +import cn.daxpay.open.channel.hkrt.client.req.HkrtRefundSyncReq; +import cn.daxpay.open.channel.hkrt.client.resp.HkrtRefundSyncResp; +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; + +/// # 海科融通服务商退款同步业务服务 +/// +/// 通过 [HkrtChannelClient] 调用子应用查询海科融通退款最终状态。 +@Slf4j +@Service +@RequiredArgsConstructor +public class HkrtRefundSyncService { + + private final HkrtChannelClient hkrtChannelClient; + + /// 同步退款状态 + public RefundResultBo sync(PayRefundOrder refundOrder, HkrtSdkCredential credential) { + HkrtRefundSyncReq req = new HkrtRefundSyncReq(); + req.setCredential(credential); + req.setOutRefundNo(refundOrder.getRefundNo()); + req.setOriginTradeNo(refundOrder.getOutRefundNo()); + + DaxResult result = hkrtChannelClient.refundSync(req); + RefundResultBo bo = new RefundResultBo(); + if (result.getCode() != 0) { + bo.setSyncSuccess(false); + bo.setSyncErrorMsg(result.getMsg()); + return bo; + } + + HkrtRefundSyncResp resp = result.getData(); + bo.setSyncSuccess(true); + bo.setOutRefundNo(resp.getTradeNo()); + bo.setFinishTime(resp.getFinishTime()); + bo.setStatus(mapRefundStatus(resp.getRefundStatus())); + // 退款完成(子应用输出抽象态 SUCCESS) + bo.setComplete(Objects.equals(resp.getRefundStatus(), "SUCCESS")); + return bo; + } + + /// 抽象态(SUCCESS/FAIL/PROCESSING) → 平台退款状态 + private RefundOrderStatusEnum mapRefundStatus(String refundStatus) { + if (Objects.equals(refundStatus, "SUCCESS")) { + return RefundOrderStatusEnum.SUCCESS; + } + if (Objects.equals(refundStatus, "FAIL")) { + return RefundOrderStatusEnum.FAIL; + } + // 处理中 + return RefundOrderStatusEnum.PROGRESS; + } +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/payment/HkrtSyncService.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/payment/HkrtSyncService.java new file mode 100644 index 000000000..8a2ecde24 --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/service/payment/HkrtSyncService.java @@ -0,0 +1,72 @@ +package cn.daxpay.open.channel.hkrt.service.payment; + +import cn.daxpay.open.channel.hkrt.client.HkrtChannelClient; +import cn.daxpay.open.channel.hkrt.client.credential.HkrtSdkCredential; +import cn.daxpay.open.channel.hkrt.client.req.HkrtSyncReq; +import cn.daxpay.open.channel.hkrt.client.resp.HkrtSyncResp; +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; + +/// # 海科融通服务商订单同步业务服务 +/// +/// 通过 [HkrtChannelClient] 调用子应用查询海科融通订单状态。 +@Slf4j +@Service +@RequiredArgsConstructor +public class HkrtSyncService { + + private final HkrtChannelClient hkrtChannelClient; + + /// 同步订单状态 + public PaySyncResultBo sync(PayTrade order, HkrtSdkCredential credential) { + HkrtSyncReq req = new HkrtSyncReq(); + req.setCredential(credential); + req.setOutTradeNo(order.getTradeNo()); + req.setTradeNo(order.getOutOrderNo()); + + DaxResult result = hkrtChannelClient.sync(req); + PaySyncResultBo bo = new PaySyncResultBo(); + if (result.getCode() != 0) { + // 同步失败(不抛异常, 由核心层决定重试) + bo.setSyncSuccess(false); + bo.setSyncErrorMsg(result.getMsg()); + return bo; + } + + HkrtSyncResp resp = result.getData(); + bo.setSyncSuccess(true); + bo.setSyncData(resp.getSyncData()); + bo.setOutOrderNo(resp.getTradeNo()); + bo.setPayStatus(mapTradeState(resp.getTradeState())); + // 成功时补充金额/时间/买家 + if (Objects.equals(resp.getTradeState(), "SUCCESS")) { + bo.setAmount(resp.getTotalAmount()); + bo.setRealAmount(resp.getTotalAmount()); + bo.setFinishTime(resp.getFinishTime()); + bo.setBuyerId(resp.getBuyerId()); + } + return bo; + } + + /// 海科融通 trade_state → 平台资金状态 + 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; + } + // 其他(处理中) + return PayFundStatusEnum.PROCESSING; + } +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/strategy/HkrtCloseStrategy.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/strategy/HkrtCloseStrategy.java new file mode 100644 index 000000000..de5efa8c7 --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/strategy/HkrtCloseStrategy.java @@ -0,0 +1,51 @@ +package cn.daxpay.open.channel.hkrt.strategy; + +import cn.daxpay.open.channel.hkrt.client.credential.HkrtSdkCredential; +import cn.daxpay.open.channel.hkrt.service.isv.HkrtIsvConfigAssembler; +import cn.daxpay.open.channel.hkrt.service.payment.HkrtCloseService; +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), +/// 组装通道凭证(委托 [HkrtIsvConfigAssembler]), 关闭执行委托给 [HkrtCloseService]。 +/// +/// 注意: 海科融通仅提供关单接口, 无撤销接口(useCancel 参数由 service 层忽略)。 +@Slf4j +@Service +@RequiredArgsConstructor +public class HkrtCloseStrategy extends AbsPayCloseStrategy { + + private final HkrtCloseService hkrtCloseService; + private final HkrtIsvConfigAssembler hkrtIsvConfigAssembler; + + @Override + public ProductEnum getProduct() { + return ProductEnum.HKRT_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(); + + // 组装通道调用凭证 + HkrtSdkCredential credential = hkrtIsvConfigAssembler.buildConfig( + trade.getMchNo(), channelMchNo, capability); + + // 客户端IP取自原下单订单(超时关单等非HTTP场景亦可用) + String clientIp = normalOrder != null ? normalOrder.getClientIp() : null; + return hkrtCloseService.close(trade, credential, useCancel, clientIp); + } +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/strategy/HkrtPayStrategy.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/strategy/HkrtPayStrategy.java new file mode 100644 index 000000000..abce5730f --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/strategy/HkrtPayStrategy.java @@ -0,0 +1,50 @@ +package cn.daxpay.open.channel.hkrt.strategy; + +import cn.daxpay.open.channel.hkrt.client.credential.HkrtSdkCredential; +import cn.daxpay.open.channel.hkrt.service.isv.HkrtIsvConfigAssembler; +import cn.daxpay.open.channel.hkrt.service.payment.HkrtPayService; +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.HKRT_PAY)下发起支付的具体执行策略。 +/// 配置组装在 [doBeforePay] 阶段完成(委托 [HkrtIsvConfigAssembler]), 支付执行委托给 [HkrtPayService]。 +/// +/// 海科融通为聚合服务商模式, 仅有一个产品 HKRT_PAY。 +@Slf4j +@Service +@RequiredArgsConstructor +public class HkrtPayStrategy extends AbsNormalPayStrategy { + + private final HkrtPayService hkrtPayService; + private final HkrtIsvConfigAssembler hkrtIsvConfigAssembler; + + @Override + public ProductEnum getProduct() { + return ProductEnum.HKRT_PAY; + } + + /// 支付前预处理: 从容器读取通道路由参数, 组装通道凭证写入上下文 + /// 配置缺失在此阶段抛异常, fail-fast, 不会进入 doPay + @Override + public void doBeforePay(PayStrategyContext context) { + NormalPayParam payParam = context.getPayParam(); + HkrtSdkCredential credential = hkrtIsvConfigAssembler.buildConfig( + payParam.getMchNo(), payParam.getChannelMchNo(), payParam.getCapability()); + context.setChannelConfig(credential); + } + + @Override + public PayTradeResultBo doPay(PayStrategyContext context) { + // 直接使用 doBeforePay 预组装的通道凭证 + HkrtSdkCredential credential = context.getChannelConfig(HkrtSdkCredential.class); + return hkrtPayService.pay(context.getTrade(), context.getPayParam(), credential); + } +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/strategy/HkrtProductStrategy.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/strategy/HkrtProductStrategy.java new file mode 100644 index 000000000..25051c179 --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/strategy/HkrtProductStrategy.java @@ -0,0 +1,69 @@ +package cn.daxpay.open.channel.hkrt.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; + +/// # 海科融通支付产品策略 +/// +@Slf4j +@Service +@RequiredArgsConstructor +public class HkrtProductStrategy extends AbsProductStrategy { + + /// 海科融通支持: 微信 / 支付宝 / 银联 的扫码(JSAPI/MINI 归为 JSAPI) + 条码 + private static final Map> METHOD_CAP_MAP = Map.ofEntries( + Map.entry(PayMethodEnum.WECHAT_BARCODE, List.of(PayCapabilityEnum.WECHAT_BARCODE)), + Map.entry(PayMethodEnum.ALIPAY_BARCODE, List.of(PayCapabilityEnum.ALIPAY_BARCODE)), + Map.entry(PayMethodEnum.UNION_PAY_BARCODE, List.of(PayCapabilityEnum.UNION_PAY_BARCODE)), + Map.entry(PayMethodEnum.WECHAT_JSAPI, List.of(PayCapabilityEnum.WECHAT_JSAPI)), + Map.entry(PayMethodEnum.WECHAT_MINI, List.of(PayCapabilityEnum.WECHAT_MINI)), + Map.entry(PayMethodEnum.ALIPAY_QR, List.of(PayCapabilityEnum.ALIPAY_QR)), + Map.entry(PayMethodEnum.ALIPAY_JSAPI, List.of(PayCapabilityEnum.ALIPAY_JSAPI)), + Map.entry(PayMethodEnum.ALIPAY_MINI, List.of(PayCapabilityEnum.ALIPAY_MINI)), + Map.entry(PayMethodEnum.UNION_QR, List.of(PayCapabilityEnum.UNION_PAY_QR))); + + @Override + public ProductEnum getProduct() { + return ProductEnum.HKRT_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-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/strategy/HkrtRefundStrategy.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/strategy/HkrtRefundStrategy.java new file mode 100644 index 000000000..8cd059dae --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/strategy/HkrtRefundStrategy.java @@ -0,0 +1,38 @@ +package cn.daxpay.open.channel.hkrt.strategy; + +import cn.daxpay.open.channel.hkrt.client.credential.HkrtSdkCredential; +import cn.daxpay.open.channel.hkrt.service.isv.HkrtIsvConfigAssembler; +import cn.daxpay.open.channel.hkrt.service.payment.HkrtRefundService; +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), +/// 组装通道凭证(委托 [HkrtIsvConfigAssembler]), 退款执行委托给 [HkrtRefundService]。 +@Slf4j +@Service +@RequiredArgsConstructor +public class HkrtRefundStrategy extends AbsRefundStrategy { + + private final HkrtRefundService hkrtRefundService; + private final HkrtIsvConfigAssembler hkrtIsvConfigAssembler; + + @Override + public ProductEnum getProduct() { + return ProductEnum.HKRT_PAY; + } + + @Override + public RefundResultBo doRefund(PayRefundOrder refundOrder) { + // 组装通道调用凭证 + HkrtSdkCredential credential = hkrtIsvConfigAssembler.buildConfig( + refundOrder.getMchNo(), refundOrder.getChannelMchNo(), refundOrder.getCapability()); + return hkrtRefundService.refund(refundOrder, credential); + } +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/strategy/HkrtSyncRefundStrategy.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/strategy/HkrtSyncRefundStrategy.java new file mode 100644 index 000000000..4621a23eb --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/strategy/HkrtSyncRefundStrategy.java @@ -0,0 +1,38 @@ +package cn.daxpay.open.channel.hkrt.strategy; + +import cn.daxpay.open.channel.hkrt.client.credential.HkrtSdkCredential; +import cn.daxpay.open.channel.hkrt.service.isv.HkrtIsvConfigAssembler; +import cn.daxpay.open.channel.hkrt.service.payment.HkrtRefundSyncService; +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), +/// 组装通道凭证(委托 [HkrtIsvConfigAssembler]), 同步执行委托给 [HkrtRefundSyncService]。 +@Slf4j +@Service +@RequiredArgsConstructor +public class HkrtSyncRefundStrategy extends AbsSyncRefundStrategy { + + private final HkrtRefundSyncService hkrtRefundSyncService; + private final HkrtIsvConfigAssembler hkrtIsvConfigAssembler; + + @Override + public ProductEnum getProduct() { + return ProductEnum.HKRT_PAY; + } + + @Override + public RefundResultBo doSync(PayRefundOrder refundOrder) { + // 组装通道调用凭证 + HkrtSdkCredential credential = hkrtIsvConfigAssembler.buildConfig( + refundOrder.getMchNo(), refundOrder.getChannelMchNo(), refundOrder.getCapability()); + return hkrtRefundSyncService.sync(refundOrder, credential); + } +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/strategy/HkrtSyncStrategy.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/strategy/HkrtSyncStrategy.java new file mode 100644 index 000000000..7001402aa --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/strategy/HkrtSyncStrategy.java @@ -0,0 +1,45 @@ +package cn.daxpay.open.channel.hkrt.strategy; + +import cn.daxpay.open.channel.hkrt.client.credential.HkrtSdkCredential; +import cn.daxpay.open.channel.hkrt.service.isv.HkrtIsvConfigAssembler; +import cn.daxpay.open.channel.hkrt.service.payment.HkrtSyncService; +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), +/// 组装通道凭证(委托 [HkrtIsvConfigAssembler]), 同步执行委托给 [HkrtSyncService]。 +@Slf4j +@Service +@RequiredArgsConstructor +public class HkrtSyncStrategy extends AbsSyncPayOrderStrategy { + + private final HkrtSyncService hkrtSyncService; + private final HkrtIsvConfigAssembler hkrtIsvConfigAssembler; + + @Override + public ProductEnum getProduct() { + return ProductEnum.HKRT_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; + + // 组装通道调用凭证 + HkrtSdkCredential credential = hkrtIsvConfigAssembler.buildConfig( + context.getTrade().getMchNo(), channelMchNo, capability); + + return hkrtSyncService.sync(context.getTrade(), credential); + } +} diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/daxpay-channel/daxpay-channel-hkrt/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..ec29d5a76 --- /dev/null +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.daxpay.open.channel.hkrt.HkrtChannelApp