mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-09 22:45:58 +08:00
feat(lakala): 拉卡拉服务商通道对接(进件/支付/退款/同步/回调) + 配置表 + 菜单
- 完整服务商(ISV)模式: 密钥配置/通道商户绑定/进件 - 支付能力: 下单/关单/退款/退款同步/订单同步, 通过声明式HTTP客户端转发子应用 - 回调验签解析, 敏感字段加密存储 - LakalaProductStrategy isTerminal 调整为 false(服务商模式无独立终端) - channel/error 中英文补充拉卡拉错误消息 - update-datas 新增拉卡拉服务商配置菜单(40506)
This commit is contained in:
@@ -1,51 +1,19 @@
|
||||
-- 商户应用事件通知配置: 菜单(权限码 merchant:notify_config)
|
||||
INSERT INTO "public"."iam_perm_menu" VALUES (4040118, 40401, 'merchant:notify_config', 'admin', 'MchAppNotifyConfig', '通知配置', 'Notify Config', 'menu.payment.merchant.app.notifyConfig', NULL, 't', 'f', '/payment/merchant/app/MchAppNotifyConfig', '/payment/merchant/app/notify-config', NULL, 11, 'f', 't', 'f', 1, 1, 0, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-02 16:00:00+00', '2026-07-02 16:00:00+00');
|
||||
|
||||
-- 退款订单: 菜单(权限码 payment:refund)
|
||||
INSERT INTO "public"."iam_perm_menu" VALUES (603, 6, 'payment:refund', 'admin', 'RefundOrderList', '退款订单', 'Refund Orders', 'menu.trade.refundOrder', 'lucide:rotate-ccw', 'f', 'f', '/payment/order/RefundOrderList', '/trade/refund-order', NULL, 3, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-03 16:00:00+00', '2026-07-03 16:00:00+00');
|
||||
|
||||
-- 微信服务商通道商户应用(子商户应用): 菜单(权限码 channel:wechat:app)
|
||||
INSERT INTO "public"."iam_perm_menu" VALUES (4040119, 40401, 'channel:wechat:app', 'admin', 'WechatIsvMchAppManage', '微信服务商通道商户应用', 'WeChat ISV Channel Merchant App', 'menu.payment.merchant.channelMerchant.wechatIsvMchApp', NULL, 't', 'f', '/payment/channel/wechat/manage/mch/isv-app/WechatIsvMchAppManage', '/payment/merchant/channel-merchant/wechat-isv-mch-app-manage', NULL, 14, 'f', 't', 'f', 1, 1, 0, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-05 00:00:00+00', '2026-07-05 00:00:00+00');
|
||||
|
||||
-- ============================================================
|
||||
-- 菜单标题与 i18n_key 统一重命名
|
||||
-- 拉卡拉服务商配置菜单 + 权限挂载点
|
||||
-- ============================================================
|
||||
|
||||
-- 支付产品详情 → 产品配置详情
|
||||
UPDATE "public"."iam_perm_menu" SET title_cn='产品配置详情', title_en='Product Config Detail',
|
||||
i18n_key='menu.payment.config.detail' WHERE id=40501;
|
||||
-- 支付宝服务商应用: i18n_key 归入 config.*
|
||||
UPDATE "public"."iam_perm_menu" SET i18n_key='menu.payment.config.alipayApp' WHERE id=40502;
|
||||
-- 微信服务商应用: i18n_key 归入 config.*
|
||||
UPDATE "public"."iam_perm_menu" SET i18n_key='menu.payment.config.wechatApp' WHERE id=40503;
|
||||
|
||||
-- 商户信息(列表入口) → 商户列表
|
||||
UPDATE "public"."iam_perm_menu" SET title_cn='商户列表', title_en='Merchant List',
|
||||
i18n_key='menu.payment.merchant.list' WHERE id=40401;
|
||||
-- 商户管理(工作台) → 商户工作台
|
||||
UPDATE "public"."iam_perm_menu" SET title_cn='商户工作台', title_en='Merchant Workbench',
|
||||
i18n_key='menu.payment.merchant.workbench' WHERE id=4040101;
|
||||
-- 对接配置: i18n_key 去掉中间 manage 层
|
||||
UPDATE "public"."iam_perm_menu" SET i18n_key='menu.payment.merchant.credential' WHERE id=4040102;
|
||||
-- 商户信息(详情) → 商户资料
|
||||
UPDATE "public"."iam_perm_menu" SET title_cn='商户资料', title_en='Merchant Profile',
|
||||
i18n_key='menu.payment.merchant.profile' WHERE id=4040103;
|
||||
-- 通道路由: i18n_key 从 app.* 移出(实为商户级配置)
|
||||
UPDATE "public"."iam_perm_menu" SET i18n_key='menu.payment.merchant.payRoute' WHERE id=4040111;
|
||||
-- 通道商户详情 → 通道商户管理(静态通用名)
|
||||
UPDATE "public"."iam_perm_menu" SET title_cn='通道商户管理', title_en='Channel Merchant Management'
|
||||
WHERE id=4040112;
|
||||
-- 微信通道商户应用 → 微信应用
|
||||
UPDATE "public"."iam_perm_menu" SET title_cn='微信应用', title_en='WeChat App' WHERE id=4040113;
|
||||
-- 支付宝通道商户应用 → 支付宝应用
|
||||
UPDATE "public"."iam_perm_menu" SET title_cn='支付宝应用', title_en='Alipay App' WHERE id=4040114;
|
||||
-- 抖音通道商户应用 → 抖音应用
|
||||
UPDATE "public"."iam_perm_menu" SET title_cn='抖音应用', title_en='Douyin App' WHERE id=4040115;
|
||||
-- 微信服务商通道商户应用 → 微信服务商子商户应用
|
||||
UPDATE "public"."iam_perm_menu" SET title_cn='微信服务商子商户应用', title_en='WeChat ISV Sub-merchant App',
|
||||
i18n_key='menu.payment.merchant.channelMerchant.wechatIsvApp' WHERE id=4040119;
|
||||
|
||||
-- 移动端应用管理(平台级, 商户端/管理端/收银台应用配置)
|
||||
INSERT INTO "public"."iam_perm_menu" VALUES (40504, 405, 'payment:config:mobile_app', 'admin', 'MobileAppConfig', '移动端应用管理', 'Mobile App Management', 'menu.payment.config.mobileApp', 'lucide:smartphone', 'f', 'f', '/payment/config/mobileApp/MobileAppConfig', '/payment/config/mobile-app', NULL, 5, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-05 00:00:00+00', '2026-07-05 00:00:00+00');
|
||||
-- 移动端应用详情(按端类型配置各平台, 隐藏子页)
|
||||
INSERT INTO "public"."iam_perm_menu" VALUES (40505, 40504, 'payment:config:mobile_app_detail', 'admin', 'MobileAppDetail', '移动端应用详情', 'Mobile App Detail', 'menu.payment.config.mobileAppDetail', NULL, 't', 'f', '/payment/config/mobileApp/detail/MobileAppDetail', '/payment/config/mobile-app/detail/:appType', NULL, 1, 'f', 't', 'f', 1, 1, 0, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-05 00:00:00+00', '2026-07-05 00:00:00+00');
|
||||
-- 拉卡拉服务商配置入口(通过产品配置列表点击"拉卡拉"跳转 ProductDetailDispatch 分发到 LakalaManage)
|
||||
-- 同时作为 @PermCode(menuCode="payment:lakala:isv") 的权限挂载点
|
||||
-- parent_id=40105(支付产品管理), 与微信服务商(40503)/支付宝服务商同级
|
||||
INSERT INTO "public"."iam_perm_menu" VALUES (
|
||||
40506, 40105,
|
||||
'payment:lakala:isv', 'admin',
|
||||
'LakalaManage',
|
||||
'拉卡拉服务商配置', 'Lakala ISV Configuration',
|
||||
'menu.payment.lakala.config',
|
||||
NULL, 't', 'f',
|
||||
'/payment/channel/lakala/manage/LakalaManage', '/payment/config/product/lakala-manage',
|
||||
NULL, 5, 'f', 't', 'f', 1, 1, 0, 'f', 'subpage',
|
||||
NULL, NULL, NULL, NULL, NULL, NULL,
|
||||
'2026-07-05 00:00:00+00', '2026-07-05 00:00:00+00'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package cn.daxpay.open.channel.lakala.client;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.client.req.LakalaCloseReq;
|
||||
import cn.daxpay.open.channel.lakala.client.req.LakalaPayReq;
|
||||
import cn.daxpay.open.channel.lakala.client.req.LakalaRefundReq;
|
||||
import cn.daxpay.open.channel.lakala.client.req.LakalaRefundSyncReq;
|
||||
import cn.daxpay.open.channel.lakala.client.req.LakalaSyncReq;
|
||||
import cn.daxpay.open.channel.lakala.client.resp.LakalaCloseResp;
|
||||
import cn.daxpay.open.channel.lakala.client.resp.LakalaPayResp;
|
||||
import cn.daxpay.open.channel.lakala.client.resp.LakalaRefundResp;
|
||||
import cn.daxpay.open.channel.lakala.client.resp.LakalaRefundSyncResp;
|
||||
import cn.daxpay.open.channel.lakala.client.resp.LakalaSyncResp;
|
||||
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-one 的拉卡拉通道接口。
|
||||
/// 拉卡拉为聚合服务商模式, 不区分直连/服务商, 路径前缀 `/channel/lakala`。
|
||||
@HttpExchange(accept = MediaType.APPLICATION_JSON_VALUE, contentType = MediaType.APPLICATION_JSON_VALUE)
|
||||
public interface LakalaChannelClient {
|
||||
|
||||
/// 支付下单
|
||||
@PostExchange("/channel/lakala/pay")
|
||||
DaxResult<LakalaPayResp> pay(@RequestBody LakalaPayReq req);
|
||||
|
||||
/// 支付同步(查询拉卡拉订单状态)
|
||||
@PostExchange("/channel/lakala/sync")
|
||||
DaxResult<LakalaSyncResp> sync(@RequestBody LakalaSyncReq req);
|
||||
|
||||
/// 关闭订单
|
||||
@PostExchange("/channel/lakala/close")
|
||||
DaxResult<LakalaCloseResp> close(@RequestBody LakalaCloseReq req);
|
||||
|
||||
/// 退款
|
||||
@PostExchange("/channel/lakala/refund")
|
||||
DaxResult<LakalaRefundResp> refund(@RequestBody LakalaRefundReq req);
|
||||
|
||||
/// 退款同步(查询退款状态)
|
||||
@PostExchange("/channel/lakala/refund-sync")
|
||||
DaxResult<LakalaRefundSyncResp> refundSync(@RequestBody LakalaRefundSyncReq req);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package cn.daxpay.open.channel.lakala.client.credential;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/// # 拉卡拉 SDK 凭证(主应用侧)
|
||||
///
|
||||
/// 与子应用 dax-pay-channel-one 的 `LakalaSdkCredential` 镜像, 跨 HTTP 传输时字段对齐。
|
||||
/// 主应用从服务商密钥配置(LakalaIsvKeyConfig) + 通道商户绑定(LakalaIsvChannelMerchant) 组装,
|
||||
/// 下发给子应用发起拉卡拉 API 调用。
|
||||
@Data
|
||||
public class LakalaSdkCredential {
|
||||
/// 拉卡拉应用编号(lkl_app_id)
|
||||
private String lklAppId;
|
||||
/// 商户证书序列号
|
||||
private String mchSerialNo;
|
||||
/// 商户RSA私钥(PEM 格式 PKCS#8 字符串)
|
||||
private String privateKey;
|
||||
/// 拉卡拉RSA公钥证书(PEM 格式, 用于响应验签)
|
||||
private String publicKey;
|
||||
/// 拉卡拉商户编号(merchantNo)
|
||||
private String lakalaMchNo;
|
||||
/// 终端号(termNo)
|
||||
private String termNo;
|
||||
/// 是否沙箱环境
|
||||
private Boolean sandbox;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package cn.daxpay.open.channel.lakala.client.enums;
|
||||
|
||||
/// # 拉卡拉支付内容类型(主应用侧, 与子应用镜像)
|
||||
public enum LakalaPayBodyType {
|
||||
/// 跳转链接
|
||||
LINK,
|
||||
/// 二维码内容
|
||||
QR_CODE,
|
||||
/// JSAPI/小程序调起参数 JSON
|
||||
JSAPI,
|
||||
/// 通用标识码
|
||||
IDENTIFIER;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package cn.daxpay.open.channel.lakala.client.enums;
|
||||
|
||||
/// # 拉卡拉支付方式(主应用侧, 与子应用镜像)
|
||||
public enum LakalaPayMethod {
|
||||
/// 条码支付(付款码被扫)
|
||||
MICROPAY,
|
||||
/// 预下单(扫码/JSAPI/APP/小程序)
|
||||
PREORDER;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package cn.daxpay.open.channel.lakala.client.req;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.client.credential.LakalaSdkCredential;
|
||||
import lombok.Data;
|
||||
|
||||
/// # 拉卡拉通道关单请求(主应用侧, 与子应用镜像)
|
||||
@Data
|
||||
public class LakalaCloseReq {
|
||||
|
||||
private LakalaSdkCredential credential;
|
||||
|
||||
/// 原商户订单号(与 originTradeNo 二选一)
|
||||
private String originOutTradeNo;
|
||||
|
||||
/// 原拉卡拉交易号(与 originOutTradeNo 二选一)
|
||||
private String originTradeNo;
|
||||
|
||||
/// 客户端IP
|
||||
private String clientIp;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package cn.daxpay.open.channel.lakala.client.req;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.client.credential.LakalaSdkCredential;
|
||||
import cn.daxpay.open.channel.lakala.client.enums.LakalaPayBodyType;
|
||||
import cn.daxpay.open.channel.lakala.client.enums.LakalaPayMethod;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
/// # 拉卡拉通道支付请求(主应用侧, 与子应用镜像)
|
||||
@Data
|
||||
public class LakalaPayReq {
|
||||
|
||||
/// 通道调用凭证
|
||||
private LakalaSdkCredential credential;
|
||||
|
||||
/// 商户订单号(主应用支付交易号)
|
||||
private String outTradeNo;
|
||||
|
||||
/// 订单金额(单位: 分)
|
||||
private Long amount;
|
||||
|
||||
/// 商品标题
|
||||
private String title;
|
||||
|
||||
/// 商品描述
|
||||
private String description;
|
||||
|
||||
/// 支付方式(MICROPAY / PREORDER)
|
||||
private LakalaPayMethod method;
|
||||
|
||||
/// 账户类型(PREORDER 必填: WECHAT / ALIPAY / UQRCODEPAY)
|
||||
private String accountType;
|
||||
|
||||
/// 交易类型(PREORDER 必填: 41扫码 / 51 JSAPI / 61 APP / 71 小程序)
|
||||
private String transType;
|
||||
|
||||
/// 用户标识(JSAPI/MINI 必填)
|
||||
private String openId;
|
||||
|
||||
/// 付款码(MICROPAY 必填)
|
||||
private String authCode;
|
||||
|
||||
/// 客户端IP
|
||||
private String clientIp;
|
||||
|
||||
/// 异步通知地址
|
||||
private String notifyUrl;
|
||||
|
||||
/// 订单过期时间
|
||||
private OffsetDateTime expireTime;
|
||||
|
||||
/// 支付内容类型(主应用预先计算, 子应用透传到响应)
|
||||
private LakalaPayBodyType payBodyType;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package cn.daxpay.open.channel.lakala.client.req;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.client.credential.LakalaSdkCredential;
|
||||
import lombok.Data;
|
||||
|
||||
/// # 拉卡拉通道退款请求(主应用侧, 与子应用镜像)
|
||||
@Data
|
||||
public class LakalaRefundReq {
|
||||
|
||||
private LakalaSdkCredential credential;
|
||||
|
||||
/// 商户退款单号
|
||||
private String outRefundNo;
|
||||
|
||||
/// 原商户订单号
|
||||
private String originOutTradeNo;
|
||||
|
||||
/// 原拉卡拉交易号
|
||||
private String originTradeNo;
|
||||
|
||||
/// 退款金额(单位: 分)
|
||||
private Long amount;
|
||||
|
||||
/// 退款原因
|
||||
private String reason;
|
||||
|
||||
/// 客户端IP
|
||||
private String clientIp;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package cn.daxpay.open.channel.lakala.client.req;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.client.credential.LakalaSdkCredential;
|
||||
import lombok.Data;
|
||||
|
||||
/// # 拉卡拉通道退款查询请求(主应用侧, 与子应用镜像)
|
||||
@Data
|
||||
public class LakalaRefundSyncReq {
|
||||
|
||||
private LakalaSdkCredential credential;
|
||||
|
||||
/// 商户退款单号(与 originTradeNo 二选一)
|
||||
private String outRefundNo;
|
||||
|
||||
/// 原拉卡拉退款交易号(与 outRefundNo 二选一)
|
||||
private String originTradeNo;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package cn.daxpay.open.channel.lakala.client.req;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.client.credential.LakalaSdkCredential;
|
||||
import lombok.Data;
|
||||
|
||||
/// # 拉卡拉通道订单查询请求(主应用侧, 与子应用镜像)
|
||||
@Data
|
||||
public class LakalaSyncReq {
|
||||
|
||||
private LakalaSdkCredential credential;
|
||||
|
||||
/// 商户订单号(与 tradeNo 二选一)
|
||||
private String outTradeNo;
|
||||
|
||||
/// 拉卡拉交易号(与 outTradeNo 二选一)
|
||||
private String tradeNo;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package cn.daxpay.open.channel.lakala.client.resp;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/// # 拉卡拉通道关单响应(主应用侧, 与子应用镜像)
|
||||
@Data
|
||||
public class LakalaCloseResp {
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package cn.daxpay.open.channel.lakala.client.resp;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.client.enums.LakalaPayBodyType;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
/// # 拉卡拉通道支付响应(主应用侧, 与子应用镜像)
|
||||
@Data
|
||||
public class LakalaPayResp {
|
||||
|
||||
private String outTradeNo;
|
||||
private String tradeNo;
|
||||
private String payBody;
|
||||
private LakalaPayBodyType payBodyType;
|
||||
private Boolean complete;
|
||||
private Long totalAmount;
|
||||
private Long payerAmount;
|
||||
private OffsetDateTime finishTime;
|
||||
private String buyerId;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package cn.daxpay.open.channel.lakala.client.resp;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
/// # 拉卡拉通道退款响应(主应用侧, 与子应用镜像)
|
||||
@Data
|
||||
public class LakalaRefundResp {
|
||||
|
||||
private String outRefundNo;
|
||||
private String tradeNo;
|
||||
private Boolean complete;
|
||||
private OffsetDateTime finishTime;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package cn.daxpay.open.channel.lakala.client.resp;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
/// # 拉卡拉通道退款查询响应(主应用侧, 与子应用镜像)
|
||||
@Data
|
||||
public class LakalaRefundSyncResp {
|
||||
|
||||
private String outRefundNo;
|
||||
private String tradeNo;
|
||||
/// 退款状态: SUCCESS / FAIL / PROCESSING
|
||||
private String refundStatus;
|
||||
private OffsetDateTime finishTime;
|
||||
private String syncData;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package cn.daxpay.open.channel.lakala.client.resp;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
/// # 拉卡拉通道订单查询响应(主应用侧, 与子应用镜像)
|
||||
@Data
|
||||
public class LakalaSyncResp {
|
||||
|
||||
private String outTradeNo;
|
||||
private String tradeNo;
|
||||
/// 交易状态: SUCCESS / FAIL / CLOSED / 其他(处理中)
|
||||
private String tradeState;
|
||||
private Long totalAmount;
|
||||
private String buyerId;
|
||||
private OffsetDateTime finishTime;
|
||||
private String syncData;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package cn.daxpay.open.channel.lakala.config;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.client.LakalaChannelClient;
|
||||
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-one baseUrl 的实例。
|
||||
///
|
||||
/// baseUrl 从 [DaxpayChannelProperties] 统一读取。
|
||||
@Configuration
|
||||
public class LakalaClientConfig {
|
||||
|
||||
@Bean
|
||||
public LakalaChannelClient lakalaChannelClient(
|
||||
RestClient restClient,
|
||||
DaxpayChannelProperties channelProperties) {
|
||||
// mutate 派生: 继承全局 RestClient 的拦截器 / observation / requestFactory, 仅覆盖 baseUrl
|
||||
RestClient channelClient = restClient
|
||||
.mutate()
|
||||
.baseUrl(channelProperties.getOne().getBaseUrl())
|
||||
.build();
|
||||
return HttpServiceProxyFactory
|
||||
.builderFor(RestClientAdapter.create(channelClient))
|
||||
.build()
|
||||
.createClient(LakalaChannelClient.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package cn.daxpay.open.channel.lakala.controller.callback;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.service.callback.LakalaPayCallbackService;
|
||||
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/**`)。
|
||||
/// 拉卡拉回调验签只需全局服务商公钥(从 LakalaIsvKeyConfig 读取), 不需 channelMchNo,
|
||||
/// 因此路径不带 channelMchNo(与抖音不同), 凭 out_trade_no 反查 PayTrade。
|
||||
@Tag(name = "拉卡拉支付回调通知控制器")
|
||||
@RestController
|
||||
@RequestMapping("/unipay/callback/{mchNo}/{appId}/lakala")
|
||||
@RequiredArgsConstructor
|
||||
public class LakalaCallbackController {
|
||||
|
||||
private final LakalaPayCallbackService lakalaPayCallbackService;
|
||||
|
||||
/// 拉卡拉支付回调
|
||||
@Operation(summary = "拉卡拉支付回调")
|
||||
@PostMapping("/pay")
|
||||
public String payNotify(@PathVariable("mchNo") String mchNo,
|
||||
@PathVariable("appId") String appId,
|
||||
HttpServletRequest request) {
|
||||
return lakalaPayCallbackService.payHandle(request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package cn.daxpay.open.channel.lakala.controller.isv;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.param.isv.LakalaIsvChannelMerchantCreateParam;
|
||||
import cn.daxpay.open.channel.lakala.result.isv.LakalaIsvChannelMerchantResult;
|
||||
import cn.daxpay.open.channel.lakala.service.isv.LakalaIsvChannelMerchantService;
|
||||
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/lakala/isv-channel-merchant")
|
||||
@RequiredArgsConstructor
|
||||
public class LakalaIsvChannelMerchantController {
|
||||
|
||||
private final LakalaIsvChannelMerchantService lakalaIsvChannelMerchantService;
|
||||
|
||||
@PermCode(code = "view", nameCn = "通道商户查看", nameEn = "Channel Merchant View")
|
||||
@Operation(summary = "根据通道商户号查询拉卡拉通道商户配置")
|
||||
@GetMapping("/find-by-channel-mch-no")
|
||||
public Result<LakalaIsvChannelMerchantResult> findByChannelMchNo(
|
||||
@NotBlank(message = "{validation.field.channelMchNo.notBlank}") String channelMchNo) {
|
||||
return Res.ok(lakalaIsvChannelMerchantService.findByChannelMchNo(channelMchNo));
|
||||
}
|
||||
|
||||
@PermCode(code = "manage", nameCn = "通道商户管理", nameEn = "Channel Merchant Manage")
|
||||
@Operation(summary = "创建拉卡拉通道商户")
|
||||
@PostMapping("/create")
|
||||
public Result<Void> create(@RequestBody @Validated LakalaIsvChannelMerchantCreateParam param) {
|
||||
lakalaIsvChannelMerchantService.create(param);
|
||||
return Res.ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package cn.daxpay.open.channel.lakala.controller.isv;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.convert.isv.LakalaIsvKeyConfigConvert;
|
||||
import cn.daxpay.open.channel.lakala.param.isv.LakalaIsvKeyConfigParam;
|
||||
import cn.daxpay.open.channel.lakala.result.isv.LakalaIsvKeyConfigResult;
|
||||
import cn.daxpay.open.channel.lakala.service.isv.LakalaIsvKeyConfigService;
|
||||
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:lakala:isv")
|
||||
@Validated
|
||||
@Tag(name = "拉卡拉服务商密钥配置")
|
||||
@RestController
|
||||
@RequestMapping("/admin/lakala/isv-key-config")
|
||||
@RequiredArgsConstructor
|
||||
public class LakalaIsvKeyConfigController {
|
||||
|
||||
private final LakalaIsvKeyConfigService lakalaIsvKeyConfigService;
|
||||
|
||||
@PermCode(code = "view", nameCn = "拉卡拉服务商查看", nameEn = "Lakala ISV View")
|
||||
@Operation(summary = "查询拉卡拉服务商密钥配置")
|
||||
@GetMapping("/find-config")
|
||||
public Result<LakalaIsvKeyConfigResult> findConfig(
|
||||
@NotBlank(message = "{validation.field.product.notBlank}") String product) {
|
||||
return Res.ok(LakalaIsvKeyConfigConvert.CONVERT.toResult(lakalaIsvKeyConfigService.findByProduct(product)));
|
||||
}
|
||||
|
||||
@PermCode(code = "manage", nameCn = "拉卡拉服务商管理", nameEn = "Lakala ISV Manage")
|
||||
@Operation(summary = "保存拉卡拉服务商密钥配置")
|
||||
@PostMapping("/save-config")
|
||||
public Result<Void> saveConfig(@RequestBody @Validated LakalaIsvKeyConfigParam param) {
|
||||
lakalaIsvKeyConfigService.saveConfig(param);
|
||||
return Res.ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package cn.daxpay.open.channel.lakala.convert.isv;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.entity.isv.LakalaIsvChannelMerchant;
|
||||
import cn.daxpay.open.channel.lakala.result.isv.LakalaIsvChannelMerchantResult;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
/// # 拉卡拉通道商户绑定转换
|
||||
///
|
||||
@Mapper
|
||||
public interface LakalaIsvChannelMerchantConvert {
|
||||
|
||||
LakalaIsvChannelMerchantConvert CONVERT = Mappers.getMapper(LakalaIsvChannelMerchantConvert.class);
|
||||
|
||||
LakalaIsvChannelMerchantResult toResult(LakalaIsvChannelMerchant entity);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package cn.daxpay.open.channel.lakala.convert.isv;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.entity.isv.LakalaIsvKeyConfig;
|
||||
import cn.daxpay.open.channel.lakala.param.isv.LakalaIsvKeyConfigParam;
|
||||
import cn.daxpay.open.channel.lakala.result.isv.LakalaIsvKeyConfigResult;
|
||||
import org.mapstruct.BeanMapping;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import org.mapstruct.NullValuePropertyMappingStrategy;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
/// # 拉卡拉服务商密钥配置转换
|
||||
///
|
||||
@Mapper
|
||||
public interface LakalaIsvKeyConfigConvert {
|
||||
|
||||
LakalaIsvKeyConfigConvert CONVERT = Mappers.getMapper(LakalaIsvKeyConfigConvert.class);
|
||||
|
||||
/// 转换为返回对象
|
||||
LakalaIsvKeyConfigResult toResult(LakalaIsvKeyConfig entity);
|
||||
|
||||
/// 转换为实体
|
||||
LakalaIsvKeyConfig toEntity(LakalaIsvKeyConfigParam param);
|
||||
|
||||
/// 更新源数据到实体(空值不覆盖, 密钥/证书为空时保留原值)
|
||||
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
|
||||
void copy(LakalaIsvKeyConfigParam param, @MappingTarget LakalaIsvKeyConfig entity);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package cn.daxpay.open.channel.lakala.dao.isv;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.entity.isv.LakalaIsvChannelMerchant;
|
||||
import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/// # 拉卡拉通道商户绑定
|
||||
///
|
||||
@Repository
|
||||
public class LakalaIsvChannelMerchantManager extends BaseManager<LakalaIsvChannelMerchantMapper, LakalaIsvChannelMerchant> {
|
||||
|
||||
/// 校验同一商户下拉卡拉商户号不重复
|
||||
public boolean existsByMchNoAndLakalaMchNo(String mchNo, String lakalaMchNo) {
|
||||
return lambdaQuery()
|
||||
.eq(LakalaIsvChannelMerchant::getMchNo, mchNo)
|
||||
.eq(LakalaIsvChannelMerchant::getLakalaMchNo, lakalaMchNo)
|
||||
.exists();
|
||||
}
|
||||
|
||||
/// 根据通道商户号查询
|
||||
public Optional<LakalaIsvChannelMerchant> findByChannelMchNo(String channelMchNo) {
|
||||
return lambdaQuery()
|
||||
.eq(LakalaIsvChannelMerchant::getChannelMchNo, channelMchNo)
|
||||
.oneOpt();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package cn.daxpay.open.channel.lakala.dao.isv;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.entity.isv.LakalaIsvChannelMerchant;
|
||||
import com.github.yulichang.base.MPJBaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/// # 拉卡拉通道商户绑定
|
||||
///
|
||||
@Mapper
|
||||
public interface LakalaIsvChannelMerchantMapper extends MPJBaseMapper<LakalaIsvChannelMerchant> {
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package cn.daxpay.open.channel.lakala.dao.isv;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.entity.isv.LakalaIsvKeyConfig;
|
||||
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 LakalaIsvKeyConfigManager extends BaseManager<LakalaIsvKeyConfigMapper, LakalaIsvKeyConfig> {
|
||||
|
||||
/// 根据产品编码查询(平台为唯一服务商, 密钥全局唯一)
|
||||
public Optional<LakalaIsvKeyConfig> findByProduct(String product) {
|
||||
return lambdaQuery()
|
||||
.eq(LakalaIsvKeyConfig::getProduct, product)
|
||||
.oneOpt();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package cn.daxpay.open.channel.lakala.dao.isv;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.entity.isv.LakalaIsvKeyConfig;
|
||||
import com.github.yulichang.base.MPJBaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/// # 拉卡拉服务商密钥配置
|
||||
///
|
||||
@Mapper
|
||||
public interface LakalaIsvKeyConfigMapper extends MPJBaseMapper<LakalaIsvKeyConfig> {
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package cn.daxpay.open.channel.lakala.entity.isv;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.convert.isv.LakalaIsvChannelMerchantConvert;
|
||||
import cn.daxpay.open.channel.lakala.result.isv.LakalaIsvChannelMerchantResult;
|
||||
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;
|
||||
|
||||
/// # 拉卡拉通道商户绑定
|
||||
///
|
||||
/// 拉卡拉服务商模式下, 子商户只需商户号(merchantNo) + 终端号(termNo)即可发起支付,
|
||||
/// 密钥/证书由服务商全局配置([LakalaIsvKeyConfig])提供。
|
||||
/// 同一商户下拉卡拉商户号不重复。
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@TableName(value = "lakala_isv_channel_merchant", autoResultMap = true)
|
||||
public class LakalaIsvChannelMerchant extends MchBaseEntity implements ToResult<LakalaIsvChannelMerchantResult> {
|
||||
|
||||
/// 通道商户号(LAKALA+雪花)
|
||||
private String channelMchNo;
|
||||
|
||||
/// 所属支付产品
|
||||
/// @see cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum
|
||||
private String product;
|
||||
|
||||
/// 拉卡拉商户编号(merchantNo)
|
||||
private String lakalaMchNo;
|
||||
|
||||
/// 终端号(termNo)
|
||||
private String termNo;
|
||||
|
||||
/// 转换
|
||||
@Override
|
||||
public LakalaIsvChannelMerchantResult toResult() {
|
||||
return LakalaIsvChannelMerchantConvert.CONVERT.toResult(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package cn.daxpay.open.channel.lakala.entity.isv;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.convert.isv.LakalaIsvKeyConfigConvert;
|
||||
import cn.daxpay.open.channel.lakala.result.isv.LakalaIsvKeyConfigResult;
|
||||
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 查询),
|
||||
/// 子商户仅需商户号(merchantNo) + 终端号(termNo), 见 [LakalaIsvChannelMerchant]。
|
||||
///
|
||||
/// 签名算法: RSA2(SHA256withRSA), 私钥签名 / 公钥验签。
|
||||
/// 敏感字段(私钥/公钥/sm4Key)通过 [DataEncryptTypeHandler] 加密入库。
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@TableName(value = "lakala_isv_key_config", autoResultMap = true)
|
||||
public class LakalaIsvKeyConfig extends MpBaseEntity implements ToResult<LakalaIsvKeyConfigResult> {
|
||||
|
||||
/// 产品编码
|
||||
/// @see cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum
|
||||
private String product;
|
||||
|
||||
/// 拉卡拉应用编号(lkl_app_id)
|
||||
private String lklAppId;
|
||||
|
||||
/// 商户证书序列号
|
||||
private String mchSerialNo;
|
||||
|
||||
/// 商户RSA私钥(PEM, 加密存储)
|
||||
@TableField(typeHandler = DataEncryptTypeHandler.class)
|
||||
private String privateKey;
|
||||
|
||||
/// 拉卡拉RSA公钥(PEM, 加密存储)
|
||||
@TableField(typeHandler = DataEncryptTypeHandler.class)
|
||||
private String publicKey;
|
||||
|
||||
/// SM4对称密钥(进件敏感字段加密用, 加密存储)
|
||||
@TableField(typeHandler = DataEncryptTypeHandler.class)
|
||||
private String sm4Key;
|
||||
|
||||
/// 是否沙箱环境
|
||||
private Boolean sandbox;
|
||||
|
||||
/// 转换
|
||||
@Override
|
||||
public LakalaIsvKeyConfigResult toResult() {
|
||||
return LakalaIsvKeyConfigConvert.CONVERT.toResult(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package cn.daxpay.open.channel.lakala.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 LakalaIsvChannelMerchantCreateParam {
|
||||
|
||||
/// 商户号
|
||||
@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.lakalaMchNo.notBlank}")
|
||||
private String lakalaMchNo;
|
||||
|
||||
/// 终端号
|
||||
@Schema(description = "终端号")
|
||||
private String termNo;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package cn.daxpay.open.channel.lakala.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 LakalaIsvKeyConfigParam {
|
||||
|
||||
@NotBlank(message = "{validation.field.product.notBlank}")
|
||||
@Schema(description = "产品编码")
|
||||
private String product;
|
||||
|
||||
@NotBlank(message = "{validation.field.lklAppId.notBlank}")
|
||||
@Schema(description = "拉卡拉应用编号")
|
||||
private String lklAppId;
|
||||
|
||||
@Schema(description = "商户证书序列号")
|
||||
private String mchSerialNo;
|
||||
|
||||
@Schema(description = "商户RSA私钥(加密存储)")
|
||||
private String privateKey;
|
||||
|
||||
@Schema(description = "拉卡拉RSA公钥(加密存储)")
|
||||
private String publicKey;
|
||||
|
||||
@Schema(description = "SM4密钥(加密存储)")
|
||||
private String sm4Key;
|
||||
|
||||
@Schema(description = "是否沙箱环境")
|
||||
private Boolean sandbox;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package cn.daxpay.open.channel.lakala.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 LakalaIsvChannelMerchantResult extends MchTradeBaseResult {
|
||||
|
||||
@Schema(description = "通道商户号")
|
||||
private String channelMchNo;
|
||||
|
||||
@Schema(description = "所属支付产品")
|
||||
private String product;
|
||||
|
||||
@Schema(description = "拉卡拉商户编号")
|
||||
private String lakalaMchNo;
|
||||
|
||||
@Schema(description = "终端号")
|
||||
private String termNo;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package cn.daxpay.open.channel.lakala.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 LakalaIsvKeyConfigResult extends BaseResult {
|
||||
|
||||
@Schema(description = "产品编码")
|
||||
private String product;
|
||||
|
||||
@Schema(description = "拉卡拉应用编号")
|
||||
private String lklAppId;
|
||||
|
||||
@Schema(description = "商户证书序列号")
|
||||
private String mchSerialNo;
|
||||
|
||||
@SensitiveInfo(front = 12, end = 12)
|
||||
@Schema(description = "商户RSA私钥(加密存储)")
|
||||
private String privateKey;
|
||||
|
||||
@SensitiveInfo(front = 12, end = 12)
|
||||
@Schema(description = "拉卡拉RSA公钥(加密存储)")
|
||||
private String publicKey;
|
||||
|
||||
@SensitiveInfo(front = 12, end = 12)
|
||||
@Schema(description = "SM4密钥(加密存储)")
|
||||
private String sm4Key;
|
||||
|
||||
@Schema(description = "是否沙箱环境")
|
||||
private Boolean sandbox;
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package cn.daxpay.open.channel.lakala.service.callback;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.dao.isv.LakalaIsvKeyConfigManager;
|
||||
import cn.daxpay.open.channel.lakala.entity.isv.LakalaIsvKeyConfig;
|
||||
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.nio.charset.StandardCharsets;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.Signature;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/// # 拉卡拉支付回调处理服务
|
||||
///
|
||||
/// 拉卡拉异步通知 → 主应用接收原始 header + body → 用全局服务商公钥 RSA2 验签 →
|
||||
/// 构建 [CallbackData] 交由 [PayCallbackService] 更新订单状态。
|
||||
///
|
||||
/// 拉卡拉回调签名机制(安全统一通知规范):
|
||||
/// - Authorization header: `LKLAPI-SHA256withRSA timestamp="...",nonce_str="...",signature="..."`
|
||||
/// - 验签原文: `timestamp + "\n" + nonce_str + "\n" + body + "\n"`
|
||||
/// - 公钥为 X509 证书(PEM), 与响应验签共用
|
||||
///
|
||||
/// 主应用直接验签(签名算法简单, 无需转发子应用)。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LakalaPayCallbackService {
|
||||
|
||||
private static final String NOTIFY_SUCCESS = "success";
|
||||
private static final String NOTIFY_FAIL = "fail";
|
||||
private static final DateTimeFormatter LKL_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
|
||||
|
||||
private final LakalaIsvKeyConfigManager lakalaIsvKeyConfigManager;
|
||||
private final PayCallbackService payCallbackService;
|
||||
|
||||
/// 支付回调处理
|
||||
public String payHandle(HttpServletRequest request) {
|
||||
// 1. 提取回调原始数据
|
||||
String body = JakartaServletUtil.getBody(request);
|
||||
Map<String, String> headerMap = JakartaServletUtil.getHeaderMap(request);
|
||||
|
||||
// 2. 获取全局服务商公钥(只读查询, 不创建记录)
|
||||
LakalaIsvKeyConfig keyConfig = lakalaIsvKeyConfigManager.findByProduct(ProductEnum.LAKALA_PAY.getCode())
|
||||
.orElse(null);
|
||||
if (keyConfig == null || keyConfig.getPublicKey() == null) {
|
||||
log.error("拉卡拉支付回调: 服务商密钥未配置, 无法验签");
|
||||
return NOTIFY_FAIL;
|
||||
}
|
||||
|
||||
// 3. 验签
|
||||
if (!verifyCallbackSign(headerMap, body, keyConfig.getPublicKey())) {
|
||||
log.error("拉卡拉支付回调验签失败");
|
||||
return NOTIFY_FAIL;
|
||||
}
|
||||
|
||||
// 4. 解析回调数据并构建 CallbackData
|
||||
CallbackData callbackData = parseCallback(body);
|
||||
if (callbackData == null) {
|
||||
log.error("拉卡拉支付回调解析失败: body={}", body);
|
||||
return NOTIFY_FAIL;
|
||||
}
|
||||
try {
|
||||
// 5. 交由框架更新订单状态
|
||||
payCallbackService.payCallback(callbackData);
|
||||
} catch (Exception e) {
|
||||
log.error("拉卡拉支付回调业务处理失败: tradeNo={}", callbackData.getTradeNo(), e);
|
||||
return NOTIFY_FAIL;
|
||||
}
|
||||
return NOTIFY_SUCCESS;
|
||||
}
|
||||
|
||||
/// 验证回调签名(从 Authorization header 解析 timestamp/nonce_str/signature)
|
||||
@SuppressWarnings("unchecked")
|
||||
private boolean verifyCallbackSign(Map<String, String> headerMap, String body, String publicKey) {
|
||||
// Authorization 可能是任意大小写 header 名
|
||||
String authorization = headerMap.getOrDefault("Authorization",
|
||||
headerMap.getOrDefault("authorization", ""));
|
||||
if (authorization.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// 解析 authorization: LKLAPI-SHA256withRSA timestamp="xxx",nonce_str="xxx",signature="xxx"
|
||||
Map<String, String> authMap = parseAuthorization(authorization);
|
||||
String timestamp = authMap.get("timestamp");
|
||||
String nonceStr = authMap.get("nonce_str");
|
||||
String signature = authMap.get("signature");
|
||||
if (signature == null || timestamp == null || nonceStr == null) {
|
||||
return false;
|
||||
}
|
||||
// 验签原文: timestamp\n nonce_str\n body\n
|
||||
String source = timestamp + "\n" + nonceStr + "\n" + body + "\n";
|
||||
try {
|
||||
X509Certificate cert = loadCertificate(publicKey);
|
||||
Signature sign = Signature.getInstance("SHA256withRSA");
|
||||
sign.initVerify(cert);
|
||||
sign.update(source.getBytes(StandardCharsets.UTF_8));
|
||||
return sign.verify(Base64.getDecoder().decode(signature));
|
||||
} catch (Exception e) {
|
||||
log.error("拉卡拉回调验签异常", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析 Authorization header 为 key-value Map
|
||||
private Map<String, String> parseAuthorization(String authorization) {
|
||||
// 去掉方案前缀 LKLAPI-SHA256withRSA
|
||||
int spaceIdx = authorization.indexOf(' ');
|
||||
String kvPart = spaceIdx > 0 ? authorization.substring(spaceIdx + 1) : authorization;
|
||||
Map<String, String> map = new java.util.HashMap<>();
|
||||
for (String pair : kvPart.split(",")) {
|
||||
int eqIdx = pair.indexOf('=');
|
||||
if (eqIdx > 0) {
|
||||
String key = pair.substring(0, eqIdx).trim();
|
||||
// 去掉值的引号
|
||||
String value = pair.substring(eqIdx + 1).trim();
|
||||
if (value.startsWith("\"") && value.endsWith("\"")) {
|
||||
value = value.substring(1, value.length() - 1);
|
||||
}
|
||||
map.put(key, value);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// 加载 X509 公钥证书(PEM)
|
||||
private X509Certificate loadCertificate(String publicKey) throws Exception {
|
||||
String key = publicKey
|
||||
.replace("-----BEGIN CERTIFICATE-----", "")
|
||||
.replace("-----END CERTIFICATE-----", "")
|
||||
.replaceAll("\\s+", "");
|
||||
byte[] decoded = Base64.getDecoder().decode(key);
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X509");
|
||||
return (X509Certificate) cf.generateCertificate(new java.io.ByteArrayInputStream(decoded));
|
||||
}
|
||||
|
||||
/// 解析拉卡拉回调 JSON 为 CallbackData
|
||||
private CallbackData parseCallback(String body) {
|
||||
try {
|
||||
cn.hutool.json.JSONObject json = cn.hutool.json.JSONUtil.parseObj(body);
|
||||
// 拉卡拉回调外层为 resp_data 包裹(与响应一致), 兼容直接业务数据
|
||||
cn.hutool.json.JSONObject data = json.getJSONObject("resp_data");
|
||||
if (data == null) {
|
||||
data = json;
|
||||
}
|
||||
CallbackData callbackData = new CallbackData();
|
||||
// out_trade_no = 下单时传入的平台 tradeNo
|
||||
callbackData.setTradeNo(data.getStr("out_trade_no"));
|
||||
// trade_no = 拉卡拉交易号
|
||||
callbackData.setOutTradeNo(data.getStr("trade_no"));
|
||||
// 交易状态映射
|
||||
String tradeState = data.getStr("trade_state");
|
||||
if (Objects.equals(tradeState, "SUCCESS")) {
|
||||
callbackData.setTradeStatus(CallbackStatusEnum.SUCCESS.getCode());
|
||||
} else {
|
||||
callbackData.setTradeStatus(tradeState);
|
||||
callbackData.setCallbackErrorMsg("拉卡拉回调状态非成功: " + tradeState);
|
||||
}
|
||||
// 完成时间(yyyyMMddHHmmss → OffsetDateTime)
|
||||
String tradeTime = data.getStr("trade_time");
|
||||
if (tradeTime != null && !tradeTime.isBlank()) {
|
||||
try {
|
||||
callbackData.setFinishTime(OffsetDateTime.parse(tradeTime, LKL_TIME_FORMATTER));
|
||||
} catch (Exception e) {
|
||||
log.warn("拉卡拉回调时间解析失败: tradeTime={}", tradeTime);
|
||||
}
|
||||
}
|
||||
return callbackData;
|
||||
} catch (Exception e) {
|
||||
log.error("拉卡拉回调 JSON 解析失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package cn.daxpay.open.channel.lakala.service.isv;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.dao.isv.LakalaIsvChannelMerchantManager;
|
||||
import cn.daxpay.open.channel.lakala.entity.isv.LakalaIsvChannelMerchant;
|
||||
import cn.daxpay.open.channel.lakala.param.isv.LakalaIsvChannelMerchantCreateParam;
|
||||
import cn.daxpay.open.channel.lakala.result.isv.LakalaIsvChannelMerchantResult;
|
||||
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.hutool.core.util.IdUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/// # 拉卡拉通道商户管理
|
||||
///
|
||||
/// 拉卡拉服务商模式下, 子商户关联到服务商本身(密钥全局唯一), 不挂靠具体应用。
|
||||
/// 一个商户下同一拉卡拉商户号(lakalaMchNo)只允许绑定一次。
|
||||
///
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LakalaIsvChannelMerchantService {
|
||||
|
||||
private final ChannelMerchantManager channelMerchantManager;
|
||||
private final LakalaIsvChannelMerchantManager lakalaIsvChannelMerchantManager;
|
||||
|
||||
/// 创建拉卡拉通道商户
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void create(LakalaIsvChannelMerchantCreateParam param) {
|
||||
// 校验同一商户下拉卡拉商户号不重复
|
||||
if (lakalaIsvChannelMerchantManager.existsByMchNoAndLakalaMchNo(
|
||||
param.getMchNo(), param.getLakalaMchNo())) {
|
||||
// 拉卡拉: 该商户下已存在此拉卡拉商户号
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "error.channel.lakala.merchantDuplicate");
|
||||
}
|
||||
// 生成通道商户号: 前缀 LAKALA + 雪花ID(无分隔符, 符合 TradeNoGenerateUtil 约定)
|
||||
String channelMchNo = "LAKALA" + IdUtil.getSnowflakeNextId();
|
||||
// 写通用通道商户主表
|
||||
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 LakalaIsvChannelMerchant();
|
||||
entity.setMchNo(param.getMchNo());
|
||||
entity.setChannelMchNo(channelMchNo);
|
||||
entity.setProduct(param.getProduct());
|
||||
entity.setLakalaMchNo(param.getLakalaMchNo());
|
||||
entity.setTermNo(param.getTermNo());
|
||||
lakalaIsvChannelMerchantManager.save(entity);
|
||||
}
|
||||
|
||||
/// 根据通道商户号查询拉卡拉通道商户配置
|
||||
public LakalaIsvChannelMerchantResult findByChannelMchNo(String channelMchNo) {
|
||||
return lakalaIsvChannelMerchantManager.lambdaQuery()
|
||||
.eq(LakalaIsvChannelMerchant::getChannelMchNo, channelMchNo)
|
||||
.oneOpt()
|
||||
.map(LakalaIsvChannelMerchant::toResult)
|
||||
// 拉卡拉: 通道商户配置不存在
|
||||
.orElseThrow(() -> new DataNotExistException("error.payment.channel.channelMerchantNotExist"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package cn.daxpay.open.channel.lakala.service.isv;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.client.credential.LakalaSdkCredential;
|
||||
import cn.daxpay.open.channel.lakala.dao.isv.LakalaIsvChannelMerchantManager;
|
||||
import cn.daxpay.open.channel.lakala.entity.isv.LakalaIsvChannelMerchant;
|
||||
import cn.daxpay.open.channel.lakala.entity.isv.LakalaIsvKeyConfig;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum;
|
||||
import cn.daxpay.open.platform.core.exception.DataNotExistException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/// # 拉卡拉服务商通道凭证组装器
|
||||
///
|
||||
/// 从服务商密钥配置([LakalaIsvKeyConfig]) + 通道商户绑定([LakalaIsvChannelMerchant]) 组装通道调用凭证,
|
||||
/// 下发给子应用 dax-pay-channel-one 发起拉卡拉 API 调用。
|
||||
///
|
||||
/// 字段映射(对齐拉卡拉 V3 接口):
|
||||
/// - lkl_app_id ← [LakalaIsvKeyConfig.lklAppId] (拉卡拉应用编号, 全局唯一)
|
||||
/// - merchant_no ← [LakalaIsvChannelMerchant.lakalaMchNo] (拉卡拉商户号)
|
||||
/// - term_no ← [LakalaIsvChannelMerchant.termNo] (终端号)
|
||||
/// - 私钥/公钥/证书序列号 ← [LakalaIsvKeyConfig] (服务商级, 全局唯一)
|
||||
///
|
||||
/// 供支付策略([cn.daxpay.open.channel.lakala.strategy.isv.*])组装通道调用凭证。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LakalaIsvConfigAssembler {
|
||||
|
||||
private final LakalaIsvChannelMerchantManager lakalaIsvChannelMerchantManager;
|
||||
private final LakalaIsvKeyConfigService lakalaIsvKeyConfigService;
|
||||
|
||||
/// 组装拉卡拉通道调用凭证(下发给子应用)
|
||||
///
|
||||
/// @param mchNo 商户号(保留参数对齐签名, 服务商密钥全局唯一不依赖此字段)
|
||||
/// @param channelMchNo 通道商户号(拉卡拉商户绑定主键)
|
||||
/// @param capability 支付能力编码(拉卡拉聚合通道不按能力路由应用, 保留参数)
|
||||
/// @return 拉卡拉 SDK 凭证(含服务商密钥 + 商户号/终端号)
|
||||
public LakalaSdkCredential buildConfig(String mchNo, String channelMchNo, String capability) {
|
||||
// 服务商密钥(全局唯一, 含 lkl_app_id + 私钥/公钥; 缺失或关键字段为空时 fail-fast)
|
||||
LakalaIsvKeyConfig keyConfig = lakalaIsvKeyConfigService.getByProductForPay(ProductEnum.LAKALA_PAY.getCode());
|
||||
// 通道商户绑定(取 merchantNo + termNo)
|
||||
LakalaIsvChannelMerchant channelMerchant = lakalaIsvChannelMerchantManager.findByChannelMchNo(channelMchNo)
|
||||
// 拉卡拉: 通道商户配置不存在
|
||||
.orElseThrow(() -> new DataNotExistException("error.payment.channel.channelMerchantNotExist"));
|
||||
|
||||
LakalaSdkCredential credential = new LakalaSdkCredential();
|
||||
// 服务商身份与密钥
|
||||
credential.setLklAppId(keyConfig.getLklAppId());
|
||||
credential.setMchSerialNo(keyConfig.getMchSerialNo());
|
||||
credential.setPrivateKey(keyConfig.getPrivateKey());
|
||||
credential.setPublicKey(keyConfig.getPublicKey());
|
||||
credential.setSandbox(keyConfig.getSandbox());
|
||||
// 子商户身份
|
||||
credential.setLakalaMchNo(channelMerchant.getLakalaMchNo());
|
||||
credential.setTermNo(channelMerchant.getTermNo());
|
||||
return credential;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package cn.daxpay.open.channel.lakala.service.isv;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.convert.isv.LakalaIsvKeyConfigConvert;
|
||||
import cn.daxpay.open.channel.lakala.dao.isv.LakalaIsvKeyConfigManager;
|
||||
import cn.daxpay.open.channel.lakala.entity.isv.LakalaIsvKeyConfig;
|
||||
import cn.daxpay.open.channel.lakala.param.isv.LakalaIsvKeyConfigParam;
|
||||
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 LakalaIsvKeyConfigService {
|
||||
|
||||
private final LakalaIsvKeyConfigManager lakalaIsvKeyConfigManager;
|
||||
|
||||
/// 根据产品编码查询密钥配置, 不存在则创建默认记录
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public LakalaIsvKeyConfig findByProduct(String product) {
|
||||
var existing = lakalaIsvKeyConfigManager.findByProduct(product);
|
||||
if (existing.isPresent()) {
|
||||
return existing.get();
|
||||
}
|
||||
var config = new LakalaIsvKeyConfig()
|
||||
.setProduct(product)
|
||||
.setSandbox(false);
|
||||
lakalaIsvKeyConfigManager.save(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
/// 支付场景查询服务商密钥(必填校验, 不创建记录)
|
||||
///
|
||||
/// 与 [findByProduct] 的 upsert 语义不同, 此方法只读不写:
|
||||
/// 记录不存在或关键字段(lklAppId/privateKey/publicKey)任一为空时 fail-fast,
|
||||
/// 避免空凭证下发到子应用后子应用才发现问题。
|
||||
public LakalaIsvKeyConfig getByProductForPay(String product) {
|
||||
LakalaIsvKeyConfig config = lakalaIsvKeyConfigManager.findByProduct(product)
|
||||
// 拉卡拉: 服务商密钥未配置
|
||||
.orElseThrow(() -> new BizInfoException("error.channel.lakala.isvKeyNotConfigured"));
|
||||
if (StrUtil.hasBlank(config.getLklAppId(), config.getPrivateKey(), config.getPublicKey())) {
|
||||
throw new BizInfoException("error.channel.lakala.isvKeyNotConfigured");
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
/// 保存服务商密钥配置
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void saveConfig(LakalaIsvKeyConfigParam param) {
|
||||
var config = this.findByProduct(param.getProduct());
|
||||
LakalaIsvKeyConfigConvert.CONVERT.copy(param, config);
|
||||
lakalaIsvKeyConfigManager.updateById(config);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package cn.daxpay.open.channel.lakala.service.payment.isv;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.client.LakalaChannelClient;
|
||||
import cn.daxpay.open.channel.lakala.client.credential.LakalaSdkCredential;
|
||||
import cn.daxpay.open.channel.lakala.client.req.LakalaCloseReq;
|
||||
import cn.daxpay.open.channel.lakala.client.resp.LakalaCloseResp;
|
||||
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;
|
||||
|
||||
/// # 拉卡拉服务商关单业务服务
|
||||
///
|
||||
/// 通过 [LakalaChannelClient] 调用子应用关闭拉卡拉订单。
|
||||
/// 拉卡拉仅提供关单接口(`/v3/labs/relation/close`), 无撤销接口, useCancel 参数忽略。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LakalaIsvCloseService {
|
||||
|
||||
private final LakalaChannelClient lakalaChannelClient;
|
||||
|
||||
/// 关闭订单
|
||||
///
|
||||
/// @param order 支付订单
|
||||
/// @param credential 通道凭证
|
||||
/// @param useCancel 是否撤销(拉卡拉不支持撤销, 忽略此参数)
|
||||
/// @return 关闭类型(恒为 CLOSE)
|
||||
public CloseTypeEnum close(PayTrade order, LakalaSdkCredential credential, boolean useCancel) {
|
||||
LakalaCloseReq req = new LakalaCloseReq();
|
||||
req.setCredential(credential);
|
||||
req.setOriginOutTradeNo(order.getTradeNo());
|
||||
req.setOriginTradeNo(order.getOutOrderNo());
|
||||
|
||||
DaxResult<LakalaCloseResp> result = lakalaChannelClient.close(req);
|
||||
if (result.getCode() != 0) {
|
||||
throw new BizInfoException(DaxPayErrorCode.TRADE_FAIL, "channel.error.lakalaCloseFailed", result.getMsg());
|
||||
}
|
||||
// 拉卡拉只有关单, 返回 CLOSE
|
||||
return CloseTypeEnum.CLOSE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package cn.daxpay.open.channel.lakala.service.payment.isv;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.client.LakalaChannelClient;
|
||||
import cn.daxpay.open.channel.lakala.client.credential.LakalaSdkCredential;
|
||||
import cn.daxpay.open.channel.lakala.client.enums.LakalaPayBodyType;
|
||||
import cn.daxpay.open.channel.lakala.client.enums.LakalaPayMethod;
|
||||
import cn.daxpay.open.channel.lakala.client.req.LakalaPayReq;
|
||||
import cn.daxpay.open.channel.lakala.client.resp.LakalaPayResp;
|
||||
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;
|
||||
|
||||
/// # 拉卡拉服务商支付执行业务服务
|
||||
///
|
||||
/// 通过 [LakalaChannelClient] 调用子应用 dax-pay-channel-one 完成拉卡拉支付下单。
|
||||
/// 请求构建、响应解析全部在本类中完成。
|
||||
///
|
||||
/// 拉卡拉聚合通道: 一个接口承载微信/支付宝/银联三种底层渠道, 由 accountType + transType 决定路由。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LakalaIsvPayService {
|
||||
|
||||
private final LakalaChannelClient lakalaChannelClient;
|
||||
private final PlatformUrlConfigService platformUrlConfigService;
|
||||
|
||||
/// 执行拉卡拉支付
|
||||
///
|
||||
/// @param order 支付订单(tradeNo 作为 out_trade_no)
|
||||
/// @param payParam 支付参数
|
||||
/// @param credential 通道调用凭证(服务商密钥 + 商户号/终端号)
|
||||
/// @return 支付结果
|
||||
public PayTradeResultBo pay(PayTrade order, NormalPayParam payParam, LakalaSdkCredential credential) {
|
||||
// 平台支付方式 → 拉卡拉三要素(method + accountType + transType + bodyType)
|
||||
MethodMapping mapping = mapMethod(payParam.getMethod());
|
||||
|
||||
// 构建请求
|
||||
LakalaPayReq req = new LakalaPayReq();
|
||||
// 使用支付交易号作为商户订单号透传给拉卡拉, 回调时凭此反查 PayTrade
|
||||
req.setOutTradeNo(order.getTradeNo());
|
||||
req.setAmount(payParam.getAmount());
|
||||
req.setTitle(payParam.getTitle());
|
||||
req.setDescription(payParam.getDescription());
|
||||
req.setMethod(mapping.method);
|
||||
req.setAccountType(mapping.accountType);
|
||||
req.setTransType(mapping.transType);
|
||||
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<LakalaPayResp> result = lakalaChannelClient.pay(req);
|
||||
if (result.getCode() != 0) {
|
||||
throw new BizInfoException(DaxPayErrorCode.TRADE_FAIL, "channel.error.lakalaPayFailed", result.getMsg());
|
||||
}
|
||||
|
||||
return toPayResult(result.getData());
|
||||
}
|
||||
|
||||
/// 生成拉卡拉支付异步通知地址(拉卡拉→平台)
|
||||
///
|
||||
/// 路径约定: `{backendBaseUrl}/unipay/callback/{mchNo}/{appId}/lakala/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/{}/{}/lakala/pay",
|
||||
base, order.getMchNo(), order.getAppId());
|
||||
}
|
||||
|
||||
/// 平台支付方式([PayMethodEnum] code) → 拉卡拉三要素
|
||||
private static MethodMapping mapMethod(String methodCode) {
|
||||
PayMethodEnum m = PayMethodEnum.findByCode(methodCode);
|
||||
return switch (m) {
|
||||
// 条码支付(微信/支付宝/银联付款码, 走 micropay, accountType 由拉卡拉自动识别)
|
||||
case WECHAT_BARCODE, ALIPAY_BARCODE, UNION_PAY_BARCODE ->
|
||||
new MethodMapping(LakalaPayMethod.MICROPAY, null, null, null);
|
||||
// 微信预下单
|
||||
case WECHAT_JSAPI -> new MethodMapping(LakalaPayMethod.PREORDER, "WECHAT", "51", LakalaPayBodyType.JSAPI);
|
||||
case WECHAT_MINI -> new MethodMapping(LakalaPayMethod.PREORDER, "WECHAT", "71", LakalaPayBodyType.JSAPI);
|
||||
case WECHAT_APP -> new MethodMapping(LakalaPayMethod.PREORDER, "WECHAT", "61", LakalaPayBodyType.JSAPI);
|
||||
// 支付宝预下单
|
||||
case ALIPAY_QR -> new MethodMapping(LakalaPayMethod.PREORDER, "ALIPAY", "41", LakalaPayBodyType.QR_CODE);
|
||||
case ALIPAY_JSAPI, ALIPAY_MINI -> new MethodMapping(LakalaPayMethod.PREORDER, "ALIPAY", "51", LakalaPayBodyType.IDENTIFIER);
|
||||
// 银联预下单
|
||||
case UNION_QR -> new MethodMapping(LakalaPayMethod.PREORDER, "UQRCODEPAY", "41", LakalaPayBodyType.QR_CODE);
|
||||
case UNION_JSAPI -> new MethodMapping(LakalaPayMethod.PREORDER, "UQRCODEPAY", "51", LakalaPayBodyType.LINK);
|
||||
default -> throw new BizInfoException(CommonErrorCode.UN_SUPPORTED_OPERATE,
|
||||
"channel.error.lakalaUnsupportedPayMethod", methodCode);
|
||||
};
|
||||
}
|
||||
|
||||
/// 解析子应用响应为支付结果 BO
|
||||
private PayTradeResultBo toPayResult(LakalaPayResp resp) {
|
||||
PayTradeResultBo bo = new PayTradeResultBo()
|
||||
.setOutOrderNo(resp.getTradeNo())
|
||||
.setComplete(Boolean.TRUE.equals(resp.getComplete()))
|
||||
.setPayBody(resp.getPayBody());
|
||||
|
||||
// 支付内容类型映射(子应用 LakalaPayBodyType → 平台 PayBodyTypeEnum)
|
||||
if (resp.getPayBodyType() != null) {
|
||||
switch (resp.getPayBodyType()) {
|
||||
case LINK -> bo.setPayBodyType(PayBodyTypeEnum.LINK);
|
||||
case QR_CODE -> bo.setPayBodyType(PayBodyTypeEnum.QR_CODE);
|
||||
case JSAPI -> bo.setPayBodyType(PayBodyTypeEnum.JSAPI);
|
||||
case IDENTIFIER -> bo.setPayBodyType(PayBodyTypeEnum.IDENTIFIER);
|
||||
}
|
||||
}
|
||||
|
||||
// 完成时间与金额(条码同步成功时返回)
|
||||
bo.setFinishTime(resp.getFinishTime());
|
||||
bo.setTotalAmount(resp.getTotalAmount());
|
||||
bo.setRealAmount(resp.getPayerAmount());
|
||||
bo.setBuyerPayAmount(resp.getPayerAmount());
|
||||
// 用户标识
|
||||
bo.setBuyerId(resp.getBuyerId());
|
||||
return bo;
|
||||
}
|
||||
|
||||
/// 支付方式映射内部封装
|
||||
private record MethodMapping(LakalaPayMethod method, String accountType, String transType,
|
||||
LakalaPayBodyType bodyType) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package cn.daxpay.open.channel.lakala.service.payment.isv;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.client.LakalaChannelClient;
|
||||
import cn.daxpay.open.channel.lakala.client.credential.LakalaSdkCredential;
|
||||
import cn.daxpay.open.channel.lakala.client.req.LakalaRefundReq;
|
||||
import cn.daxpay.open.channel.lakala.client.resp.LakalaRefundResp;
|
||||
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;
|
||||
|
||||
/// # 拉卡拉服务商退款执行业务服务
|
||||
///
|
||||
/// 通过 [LakalaChannelClient] 调用子应用完成拉卡拉退款。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LakalaIsvRefundService {
|
||||
|
||||
private final LakalaChannelClient lakalaChannelClient;
|
||||
|
||||
/// 执行退款
|
||||
public RefundResultBo refund(PayRefundOrder refundOrder, LakalaSdkCredential credential) {
|
||||
LakalaRefundReq req = new LakalaRefundReq();
|
||||
req.setCredential(credential);
|
||||
// 退款单号(作为拉卡拉 out_trade_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<LakalaRefundResp> result = lakalaChannelClient.refund(req);
|
||||
if (result.getCode() != 0) {
|
||||
throw new BizInfoException(DaxPayErrorCode.TRADE_FAIL, "channel.error.lakalaRefundFailed", result.getMsg());
|
||||
}
|
||||
|
||||
LakalaRefundResp resp = result.getData();
|
||||
RefundResultBo bo = new RefundResultBo()
|
||||
.setOutRefundNo(resp.getTradeNo())
|
||||
.setFinishTime(resp.getFinishTime())
|
||||
.setComplete(Boolean.TRUE.equals(resp.getComplete()));
|
||||
// 退款状态: 有完成时间视为成功, 否则处理中
|
||||
bo.setStatus(Boolean.TRUE.equals(resp.getComplete())
|
||||
? RefundOrderStatusEnum.SUCCESS
|
||||
: RefundOrderStatusEnum.PROGRESS);
|
||||
return bo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package cn.daxpay.open.channel.lakala.service.payment.isv;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.client.LakalaChannelClient;
|
||||
import cn.daxpay.open.channel.lakala.client.credential.LakalaSdkCredential;
|
||||
import cn.daxpay.open.channel.lakala.client.req.LakalaRefundSyncReq;
|
||||
import cn.daxpay.open.channel.lakala.client.resp.LakalaRefundSyncResp;
|
||||
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;
|
||||
|
||||
/// # 拉卡拉服务商退款同步业务服务
|
||||
///
|
||||
/// 通过 [LakalaChannelClient] 调用子应用查询拉卡拉退款最终状态。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LakalaIsvRefundSyncService {
|
||||
|
||||
private final LakalaChannelClient lakalaChannelClient;
|
||||
|
||||
/// 同步退款状态
|
||||
public RefundResultBo sync(PayRefundOrder refundOrder, LakalaSdkCredential credential) {
|
||||
LakalaRefundSyncReq req = new LakalaRefundSyncReq();
|
||||
req.setCredential(credential);
|
||||
req.setOutRefundNo(refundOrder.getRefundNo());
|
||||
req.setOriginTradeNo(refundOrder.getOutRefundNo());
|
||||
|
||||
DaxResult<LakalaRefundSyncResp> result = lakalaChannelClient.refundSync(req);
|
||||
RefundResultBo bo = new RefundResultBo();
|
||||
if (result.getCode() != 0) {
|
||||
bo.setSyncSuccess(false);
|
||||
bo.setSyncErrorMsg(result.getMsg());
|
||||
return bo;
|
||||
}
|
||||
|
||||
LakalaRefundSyncResp resp = result.getData();
|
||||
bo.setSyncSuccess(true);
|
||||
bo.setOutRefundNo(resp.getTradeNo());
|
||||
bo.setFinishTime(resp.getFinishTime());
|
||||
bo.setStatus(mapRefundStatus(resp.getRefundStatus()));
|
||||
bo.setComplete(Objects.equals(resp.getRefundStatus(), "SUCCESS"));
|
||||
return bo;
|
||||
}
|
||||
|
||||
/// 拉卡拉退款状态 → 平台退款状态
|
||||
private RefundOrderStatusEnum mapRefundStatus(String refundStatus) {
|
||||
if (Objects.equals(refundStatus, "SUCCESS")) {
|
||||
return RefundOrderStatusEnum.SUCCESS;
|
||||
}
|
||||
if (Objects.equals(refundStatus, "FAIL")) {
|
||||
return RefundOrderStatusEnum.FAIL;
|
||||
}
|
||||
// 处理中
|
||||
return RefundOrderStatusEnum.PROGRESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package cn.daxpay.open.channel.lakala.service.payment.isv;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.client.LakalaChannelClient;
|
||||
import cn.daxpay.open.channel.lakala.client.credential.LakalaSdkCredential;
|
||||
import cn.daxpay.open.channel.lakala.client.req.LakalaSyncReq;
|
||||
import cn.daxpay.open.channel.lakala.client.resp.LakalaSyncResp;
|
||||
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;
|
||||
|
||||
/// # 拉卡拉服务商订单同步业务服务
|
||||
///
|
||||
/// 通过 [LakalaChannelClient] 调用子应用查询拉卡拉订单状态。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LakalaIsvSyncService {
|
||||
|
||||
private final LakalaChannelClient lakalaChannelClient;
|
||||
|
||||
/// 同步订单状态
|
||||
public PaySyncResultBo sync(PayTrade order, LakalaSdkCredential credential) {
|
||||
LakalaSyncReq req = new LakalaSyncReq();
|
||||
req.setCredential(credential);
|
||||
req.setOutTradeNo(order.getTradeNo());
|
||||
req.setTradeNo(order.getOutOrderNo());
|
||||
|
||||
DaxResult<LakalaSyncResp> result = lakalaChannelClient.sync(req);
|
||||
PaySyncResultBo bo = new PaySyncResultBo();
|
||||
if (result.getCode() != 0) {
|
||||
// 同步失败(不抛异常, 由核心层决定重试)
|
||||
bo.setSyncSuccess(false);
|
||||
bo.setSyncErrorMsg(result.getMsg());
|
||||
return bo;
|
||||
}
|
||||
|
||||
LakalaSyncResp 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;
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ public class LakalaProductStrategy extends AbsProductStrategy {
|
||||
public boolean isIsv() { return true; }
|
||||
|
||||
@Override
|
||||
public boolean isTerminal() { return true; }
|
||||
public boolean isTerminal() { return false; }
|
||||
|
||||
@Override
|
||||
public boolean isSandbox() { return true; }
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package cn.daxpay.open.channel.lakala.strategy.isv;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.client.credential.LakalaSdkCredential;
|
||||
import cn.daxpay.open.channel.lakala.service.isv.LakalaIsvConfigAssembler;
|
||||
import cn.daxpay.open.channel.lakala.service.payment.isv.LakalaIsvCloseService;
|
||||
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),
|
||||
/// 组装通道凭证(委托 [LakalaIsvConfigAssembler]), 关闭执行委托给 [LakalaIsvCloseService]。
|
||||
///
|
||||
/// 注意: 拉卡拉仅提供关单接口, 无撤销接口(useCancel 参数由 service 层忽略)。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LakalaIsvCloseStrategy extends AbsPayCloseStrategy {
|
||||
|
||||
private final LakalaIsvCloseService lakalaIsvCloseService;
|
||||
private final LakalaIsvConfigAssembler lakalaIsvConfigAssembler;
|
||||
|
||||
@Override
|
||||
public ProductEnum getProduct() {
|
||||
return ProductEnum.LAKALA_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();
|
||||
|
||||
// 组装通道调用凭证
|
||||
LakalaSdkCredential credential = lakalaIsvConfigAssembler.buildConfig(
|
||||
trade.getMchNo(), channelMchNo, capability);
|
||||
|
||||
return lakalaIsvCloseService.close(trade, credential, useCancel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package cn.daxpay.open.channel.lakala.strategy.isv;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.client.credential.LakalaSdkCredential;
|
||||
import cn.daxpay.open.channel.lakala.service.isv.LakalaIsvConfigAssembler;
|
||||
import cn.daxpay.open.channel.lakala.service.payment.isv.LakalaIsvPayService;
|
||||
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.LAKALA_PAY)下发起支付的具体执行策略。
|
||||
/// 配置组装在 [doBeforePay] 阶段完成(委托 [LakalaIsvConfigAssembler]), 支付执行委托给 [LakalaIsvPayService]。
|
||||
///
|
||||
/// 拉卡拉为聚合服务商模式, 不区分直连/服务商, 仅有一个产品 LAKALA_PAY。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LakalaIsvPayStrategy extends AbsNormalPayStrategy {
|
||||
|
||||
private final LakalaIsvPayService lakalaIsvPayService;
|
||||
private final LakalaIsvConfigAssembler lakalaIsvConfigAssembler;
|
||||
|
||||
@Override
|
||||
public ProductEnum getProduct() {
|
||||
return ProductEnum.LAKALA_PAY;
|
||||
}
|
||||
|
||||
/// 支付前预处理: 从容器读取通道路由参数, 组装通道凭证写入上下文
|
||||
/// 配置缺失在此阶段抛异常, fail-fast, 不会进入 doPay
|
||||
@Override
|
||||
public void doBeforePay(PayStrategyContext context) {
|
||||
NormalPayParam payParam = context.getPayParam();
|
||||
LakalaSdkCredential credential = lakalaIsvConfigAssembler.buildConfig(
|
||||
payParam.getMchNo(), payParam.getChannelMchNo(), payParam.getCapability());
|
||||
context.setChannelConfig(credential);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayTradeResultBo doPay(PayStrategyContext context) {
|
||||
// 直接使用 doBeforePay 预组装的通道凭证
|
||||
LakalaSdkCredential credential = context.getChannelConfig(LakalaSdkCredential.class);
|
||||
return lakalaIsvPayService.pay(context.getTrade(), context.getPayParam(), credential);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package cn.daxpay.open.channel.lakala.strategy.isv;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.client.credential.LakalaSdkCredential;
|
||||
import cn.daxpay.open.channel.lakala.service.isv.LakalaIsvConfigAssembler;
|
||||
import cn.daxpay.open.channel.lakala.service.payment.isv.LakalaIsvRefundService;
|
||||
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),
|
||||
/// 组装通道凭证(委托 [LakalaIsvConfigAssembler]), 退款执行委托给 [LakalaIsvRefundService]。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LakalaIsvRefundStrategy extends AbsRefundStrategy {
|
||||
|
||||
private final LakalaIsvRefundService lakalaIsvRefundService;
|
||||
private final LakalaIsvConfigAssembler lakalaIsvConfigAssembler;
|
||||
|
||||
@Override
|
||||
public ProductEnum getProduct() {
|
||||
return ProductEnum.LAKALA_PAY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RefundResultBo doRefund(PayRefundOrder refundOrder) {
|
||||
// 组装通道调用凭证
|
||||
LakalaSdkCredential credential = lakalaIsvConfigAssembler.buildConfig(
|
||||
refundOrder.getMchNo(), refundOrder.getChannelMchNo(), refundOrder.getCapability());
|
||||
return lakalaIsvRefundService.refund(refundOrder, credential);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package cn.daxpay.open.channel.lakala.strategy.isv;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.client.credential.LakalaSdkCredential;
|
||||
import cn.daxpay.open.channel.lakala.service.isv.LakalaIsvConfigAssembler;
|
||||
import cn.daxpay.open.channel.lakala.service.payment.isv.LakalaIsvRefundSyncService;
|
||||
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),
|
||||
/// 组装通道凭证(委托 [LakalaIsvConfigAssembler]), 同步执行委托给 [LakalaIsvRefundSyncService]。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LakalaIsvSyncRefundStrategy extends AbsSyncRefundStrategy {
|
||||
|
||||
private final LakalaIsvRefundSyncService lakalaIsvRefundSyncService;
|
||||
private final LakalaIsvConfigAssembler lakalaIsvConfigAssembler;
|
||||
|
||||
@Override
|
||||
public ProductEnum getProduct() {
|
||||
return ProductEnum.LAKALA_PAY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RefundResultBo doSync(PayRefundOrder refundOrder) {
|
||||
// 组装通道调用凭证
|
||||
LakalaSdkCredential credential = lakalaIsvConfigAssembler.buildConfig(
|
||||
refundOrder.getMchNo(), refundOrder.getChannelMchNo(), refundOrder.getCapability());
|
||||
return lakalaIsvRefundSyncService.sync(refundOrder, credential);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package cn.daxpay.open.channel.lakala.strategy.isv;
|
||||
|
||||
import cn.daxpay.open.channel.lakala.client.credential.LakalaSdkCredential;
|
||||
import cn.daxpay.open.channel.lakala.service.isv.LakalaIsvConfigAssembler;
|
||||
import cn.daxpay.open.channel.lakala.service.payment.isv.LakalaIsvSyncService;
|
||||
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),
|
||||
/// 组装通道凭证(委托 [LakalaIsvConfigAssembler]), 同步执行委托给 [LakalaIsvSyncService]。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LakalaIsvSyncStrategy extends AbsSyncPayOrderStrategy {
|
||||
|
||||
private final LakalaIsvSyncService lakalaIsvSyncService;
|
||||
private final LakalaIsvConfigAssembler lakalaIsvConfigAssembler;
|
||||
|
||||
@Override
|
||||
public ProductEnum getProduct() {
|
||||
return ProductEnum.LAKALA_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;
|
||||
|
||||
// 组装通道调用凭证
|
||||
LakalaSdkCredential credential = lakalaIsvConfigAssembler.buildConfig(
|
||||
context.getTrade().getMchNo(), channelMchNo, capability);
|
||||
|
||||
return lakalaIsvSyncService.sync(context.getTrade(), credential);
|
||||
}
|
||||
}
|
||||
@@ -21,5 +21,12 @@
|
||||
"alipayPreCreateFailed": "Alipay pre-create order failed: {0}",
|
||||
"alipayPayCallFailed": "Alipay payment call failed: {0}",
|
||||
"alipayCloseOrderFailed": "Alipay close order failed: {0}",
|
||||
"alipayClientCreateFailed": "Failed to create AlipayClient: {0}"
|
||||
"alipayClientCreateFailed": "Failed to create AlipayClient: {0}",
|
||||
"lakalaIsvKeyNotConfigured": "Lakala ISV key is not configured",
|
||||
"lakalaMerchantDuplicate": "This Lakala merchant number already exists under the merchant",
|
||||
"lakalaPayFailed": "Lakala payment failed: {0}",
|
||||
"lakalaRefundFailed": "Lakala refund failed: {0}",
|
||||
"lakalaSyncFailed": "Lakala order sync failed: {0}",
|
||||
"lakalaCloseFailed": "Lakala close order failed: {0}",
|
||||
"lakalaUnsupportedPayMethod": "Unsupported payment method for Lakala: {0}"
|
||||
}
|
||||
|
||||
@@ -21,5 +21,12 @@
|
||||
"alipayPreCreateFailed": "支付宝预创建订单失败: {0}",
|
||||
"alipayPayCallFailed": "支付宝支付调用失败: {0}",
|
||||
"alipayCloseOrderFailed": "支付宝关闭订单失败: {0}",
|
||||
"alipayClientCreateFailed": "创建AlipayClient失败: {0}"
|
||||
"alipayClientCreateFailed": "创建AlipayClient失败: {0}",
|
||||
"lakalaIsvKeyNotConfigured": "拉卡拉服务商密钥未配置",
|
||||
"lakalaMerchantDuplicate": "该商户下已存在此拉卡拉商户号",
|
||||
"lakalaPayFailed": "拉卡拉支付失败: {0}",
|
||||
"lakalaRefundFailed": "拉卡拉退款失败: {0}",
|
||||
"lakalaSyncFailed": "拉卡拉订单同步失败: {0}",
|
||||
"lakalaCloseFailed": "拉卡拉关闭订单失败: {0}",
|
||||
"lakalaUnsupportedPayMethod": "拉卡拉不支持的支付方式: {0}"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user