接入准备
GET STARTED
在开始调用 KairWallet API 之前,您需要完成以下准备工作:
- 注册 KairWallet 账号并完成企业认证
- 进入商户后台 → 开发者中心 → 获取 AppKey 与 AppSecret
- 配置 IP 白名单与回调地址(生产环境必需)
- 阅读签名算法说明,完成首次接口调用
提示: 所有 API 请求必须使用 HTTPS 协议,基础地址为 https://api.kairwallet.com
系统架构关系图
KairWallet 采用分层架构设计,商户侧系统与平台之间通过标准 API 交互,密钥全程自持,确保数据安全与合规。
商户侧系统
商户业务集群(多实例)
PaymentClient(SDK)
异步上报队列
本地账单同步工具
↕
↕
K商户密钥自持,平台不存储
R异步上报,本地失败重试
@HTTPS 加密传输
#云端自动对账
通用响应结构
所有接口统一采用以下响应格式,业务状态通过 code 判断:
{
"code": 0,
"message": "success",
"data": { ... },
"request_id": "req_8f2a9c7b8d9e3817",
"timestamp": 1718700000
}
认证与签名算法
SIGNATURE HMAC-SHA256
KairWallet 使用基于 HMAC-SHA256 的签名机制保障接口安全。每个请求必须在 HTTP Header 中携带以下字段:
| Header | 说明 |
| X-App-Key | 商户应用 AppKey,在开发者中心获取 |
| X-Timestamp | 当前 Unix 时间戳(秒),5 分钟内有效 |
| X-Nonce | 随机字符串,防止重放攻击 |
| X-Signature | 请求签名,使用 AppSecret 计算 |
签名步骤
- 将所有请求参数按键名升序排列,拼接为 key=value& 格式字符串
- 在字符串末尾拼接
×tamp=xxx&nonce=xxx
- 使用 AppSecret 作为密钥,对字符串进行 HMAC-SHA256 计算
- 将结果转换为小写十六进制字符串,作为 X-Signature 值
SDK 自动签名: 使用 Java SDK 时,签名流程由 PaymentClient 自动完成,您只需在初始化时传入 AppKey 和 AppSecret,无需手动拼接签名参数。
SDK 初始化示例
import com.kairwallet.payment.api.*;
import com.kairwallet.payment.config.*;
// 构建 PaymentClient,签名自动处理
PaymentClient client = PaymentClient.builder()
.appKey("hw_prod_8f2a9c7b8d9e3817")
.appSecret(System.getenv("KAIRWALLET_SECRET"))
.environment(Environment.PRODUCTION)
.build();
// 后续所有 client.pay() / client.query() 调用均自动携带签名
安全提醒: AppSecret 仅用于签名计算,切勿在客户端代码或前端页面中暴露。建议将签名逻辑放置在服务端,或使用我们的官方 SDK。
错误码手册
ERROR CODES
当接口调用失败时,响应中的 code 字段将返回非 0 值,并在 message 中给出具体原因。以下是常见错误码:
| 错误码 | HTTP 状态 | 说明 |
| 0 | 200 | 请求成功 |
| 40001 | 400 | 参数校验失败,检查必填字段与格式 |
| 40101 | 401 | AppKey 无效或已被禁用 |
| 40102 | 401 | 签名验证失败,请检查签名算法 |
| 40103 | 401 | 请求时间戳过期(超过 5 分钟) |
| 40301 | 403 | 请求 IP 不在白名单中 |
| 40302 | 403 | 账户无此接口调用权限,请联系管理员 |
| 40401 | 404 | 订单不存在或订单号有误 |
| 40901 | 409 | 商户订单号重复,请更换 merchantOrderNo |
| 42901 | 429 | 接口调用频率超限,请稍后重试 |
| 50001 | 500 | 服务端内部错误,请稍后重试或联系技术支持 |
| 50301 | 503 | 下游渠道服务不可用(如微信/支付宝维护) |
最佳实践: 遇到 5xx 或 429 错误时,建议采用「指数退避 + 随机抖动」的重试策略,避免瞬间大量请求冲击系统。官方 SDK 已内置该策略。
发起支付
PaymentResponse pay(PaymentRequest request)
通过 SDK 发起支付订单,支持微信支付和支付宝的多种产品类型。SDK 内部自动完成签名、序列化与异常处理。
PaymentRequest 参数
| 字段 | 类型 | 必填 | 说明 |
| merchantOrderNo | String | 必填 | 商户订单号,全局唯一,建议格式 ORD-日期-序号 |
| channel | Channel | 必填 | 支付渠道枚举:WECHAT / ALIPAY |
| productType | ProductType | 必填 | 产品类型:JSAPI / NATIVE / APP / H5 / FACE |
| amount | BigDecimal | 必填 | 支付金额,单位:元,精确到 0.01 |
| subject | String | 必填 | 商品标题,将展示在支付页 |
| notifyUrl | String | 必填 | 支付结果异步回调地址 |
| openId | String | 可选 | 微信 openid 或支付宝用户标识,JSAPI 必传 |
| clientIp | String | 可选 | 客户端 IP,风控与反欺诈场景使用 |
| returnUrl | String | 可选 | H5/APP 同步跳转地址 |
| authCode | String | 可选 | 付款码支付(FACE 模式)的授权码 |
| extra | Map<String,String> | 可选 | 渠道扩展参数 |
| profitSharing | boolean | 可选 | 是否需要分账,默认 false;分账时资金冻结 |
代码示例
import com.kairwallet.payment.api.*;
import com.kairwallet.payment.model.*;
// 微信 JSAPI 支付
PaymentRequest request = PaymentRequest.builder()
.merchantOrderNo("ORD-20260726-001")
.channel(Channel.WECHAT)
.productType(ProductType.JSAPI)
.amount(new BigDecimal("2480.00"))
.subject("企业服务年费")
.notifyUrl("https://example.com/api/notify/pay")
.openId("o8ydt0Zxxxxxxxxxxxxx")
.profitSharing(true)
.build();
PaymentResponse response = client.pay(request);
// 获取支付参数
System.out.println("平台订单号: " + response.getChannelOrderNo());
System.out.println("预支付参数: " + response.getPrepayData());
PaymentResponse 关键字段
| 字段 | 类型 | 说明 |
| channelOrderNo | String | 平台订单号 |
| merchantOrderNo | String | 商户订单号 |
| prepayData | String | 预支付参数(JSAPI 调起支付所需) |
| payUrl | String | 支付链接(NATIVE/H5) |
| qrCode | String | 二维码链接(NATIVE) |
| status | String | 订单状态:pending / success / closed |
分账注意: 如需分账,发起支付时必须设置 profitSharing(true),支付资金将冻结,直至调用 splitPayment() 完成分账或 finishSplit() 完结分账后解冻剩余资金。
查询订单
QueryResponse query(QueryRequest request)
通过商户订单号或渠道订单号查询订单最新状态,适用于未收到回调或需要主动对账的场景。
QueryRequest 参数
| 字段 | 类型 | 必填 | 说明 |
| channel | Channel | 必填 | 支付渠道:WECHAT / ALIPAY |
| merchantOrderNo | String | 可选 | 商户订单号(与 channelOrderNo 二选一) |
| channelOrderNo | String | 可选 | 渠道订单号(与 merchantOrderNo 二选一) |
代码示例
// 通过商户订单号查询
QueryRequest request = QueryRequest.builder()
.channel(Channel.WECHAT)
.merchantOrderNo("ORD-20260726-001")
.build();
QueryResponse response = client.query(request);
System.out.println("订单状态: " + response.getStatus());
System.out.println("支付时间: " + response.getPayTime());
System.out.println("渠道订单号: " + response.getChannelOrderNo());
QueryResponse 关键字段
| 字段 | 类型 | 说明 |
| status | String | 订单状态:pending / success / closed / refunding |
| channelOrderNo | String | 渠道订单号 |
| merchantOrderNo | String | 商户订单号 |
| amount | BigDecimal | 订单金额 |
| payTime | LocalDateTime | 支付完成时间 |
建议: merchantOrderNo 与 channelOrderNo 至少提供一个,优先使用 channelOrderNo 查询效率更高。
关闭订单
CloseResponse close(CloseRequest request)
关闭未支付的订单,防止继续支付。已支付成功的订单无法关闭,如需退款请使用 refund()。
CloseRequest 参数
| 字段 | 类型 | 必填 | 说明 |
| channel | Channel | 必填 | 支付渠道:WECHAT / ALIPAY |
| merchantOrderNo | String | 可选 | 商户订单号(与 channelOrderNo 至少提供一个) |
| channelOrderNo | String | 可选 | 渠道订单号(与 merchantOrderNo 至少提供一个) |
代码示例
CloseRequest request = CloseRequest.builder()
.channel(Channel.WECHAT)
.merchantOrderNo("ORD-20260726-001")
.build();
CloseResponse response = client.close(request);
System.out.println("关闭结果: " + response.isSuccess());
注意: 微信支付订单超过一定时间未支付会自动关闭,无需手动调用。支付宝订单关闭后不可重新发起支付。
发起退款
RefundResponse refund(RefundRequest request)
对已支付成功的订单发起退款,支持全额或部分退款。同一订单可多次部分退款,累计退款金额不超过原订单金额。
RefundRequest 参数
| 字段 | 类型 | 必填 | 说明 |
| channel | Channel | 必填 | 支付渠道:WECHAT / ALIPAY |
| merchantOrderNo | String | 可选 | 商户订单号(与 channelOrderNo 至少提供一个) |
| channelOrderNo | String | 可选 | 渠道订单号(与 merchantOrderNo 至少提供一个) |
| refundOrderNo | String | 必填 | 商户退款单号,全局唯一 |
| refundAmount | BigDecimal | 必填 | 退款金额,单位:元 |
| totalAmount | BigDecimal | 必填 | 原订单总金额,用于校验 |
| reason | String | 可选 | 退款原因 |
| notifyUrl | String | 可选 | 退款结果异步回调地址 |
| extra | Map<String,String> | 可选 | 渠道扩展参数 |
代码示例
RefundRequest request = RefundRequest.builder()
.channel(Channel.WECHAT)
.merchantOrderNo("ORD-20260726-001")
.refundOrderNo("REF-20260726-001")
.refundAmount(new BigDecimal("500.00"))
.totalAmount(new BigDecimal("2480.00"))
.reason("用户申请部分退款")
.notifyUrl("https://example.com/api/notify/refund")
.build();
RefundResponse response = client.refund(request);
System.out.println("退款状态: " + response.getRefundStatus());
System.out.println("渠道退款单号: " + response.getChannelRefundNo());
提示: totalAmount 必须等于原订单的支付总金额,此字段用于渠道侧校验退款金额是否超限。
查询退款
RefundResponse queryRefund(QueryRequest request)
查询退款订单的处理状态,适用于未收到退款回调或需要主动确认退款结果的场景。
QueryRequest 参数(退款查询)
| 字段 | 类型 | 必填 | 说明 |
| channel | Channel | 必填 | 支付渠道:WECHAT / ALIPAY |
| merchantOrderNo | String | 可选 | 商户订单号 |
| channelOrderNo | String | 可选 | 渠道订单号 |
| refundOrderNo | String | 必填 | 商户退款单号 |
代码示例
QueryRequest request = QueryRequest.builder()
.channel(Channel.WECHAT)
.merchantOrderNo("ORD-20260726-001")
.refundOrderNo("REF-20260726-001")
.build();
RefundResponse response = client.queryRefund(request);
System.out.println("退款状态: " + response.getRefundStatus());
System.out.println("退款到账时间: " + response.getRefundSuccessTime());
发起分账
SplitResponse splitPayment(SplitRequest request)
对已完成支付且设置了 profitSharing(true) 的订单发起分账。分账资金在支付时已被冻结。SDK 内置轮询机制,微信分账为异步受理,SDK 自动轮询查询结果。
SplitRequest 参数
| 字段 | 类型 | 必填 | 说明 |
| channel | Channel | 必填 | 支付渠道:WECHAT / ALIPAY |
| merchantOrderNo | String | 必填 | 原支付商户订单号 |
| channelTransactionId | String | 必填 | 渠道交易流水号 |
| outSplitNo | String | 必填 | 分账单号,全局唯一 |
| ruleId | Long | 可选 | SaaS 预设分账规则 ID(与 receivers 二选一) |
| receivers | List<SplitReceiver> | 可选 | 分账接收方列表(与 ruleId 二选一) |
| unfreezeRemaining | boolean | 可选 | 是否解冻剩余资金,默认 false |
| description | String | 可选 | 分账描述 |
方式一:使用 SaaS 预设规则(ruleId)
// 通过预设规则 ID 分账,平台自动匹配接收方与比例
SplitRequest request = SplitRequest.builder()
.channel(Channel.WECHAT)
.merchantOrderNo("ORD-20260726-001")
.channelTransactionId("4200001234202607260000000001")
.outSplitNo("SPLIT-20260726-001")
.ruleId(10086L)
.unfreezeRemaining(true)
.description("平台服务费分账")
.build();
SplitResponse response = client.splitPayment(request);
方式二:直接传入接收方(receivers)
import com.kairwallet.payment.model.SplitReceiver;
List<SplitReceiver> receivers = Arrays.asList(
SplitReceiver.builder()
.type("MERCHANT_ID")
.account("1900000001")
.amount(new BigDecimal("300.00"))
.description("平台服务费")
.build(),
SplitReceiver.builder()
.type("PERSONAL_OPENID")
.account("o8ydt0Zxxxxxxxxxxxxx")
.amount(new BigDecimal("180.00"))
.description("推广佣金")
.build()
);
SplitRequest request = SplitRequest.builder()
.channel(Channel.WECHAT)
.merchantOrderNo("ORD-20260726-001")
.channelTransactionId("4200001234202607260000000001")
.outSplitNo("SPLIT-20260726-002")
.receivers(receivers)
.build();
SplitResponse response = client.splitPayment(request);
前置条件: 发起分账前,支付订单必须设置 profitSharing(true),否则资金不会冻结,无法分账。微信分账受理为异步,SDK 内置自动轮询,无需手动查询。
查询分账
SplitResponse querySplit(QuerySplitRequest request)
查询分账订单的处理状态与结果。
QuerySplitRequest 参数
| 字段 | 类型 | 必填 | 说明 |
| channel | Channel | 必填 | 支付渠道 |
| outSplitNo | String | 必填 | 分账单号 |
代码示例
QuerySplitRequest request = QuerySplitRequest.builder()
.channel(Channel.WECHAT)
.outSplitNo("SPLIT-20260726-001")
.build();
SplitResponse response = client.querySplit(request);
System.out.println("分账状态: " + response.getSplitStatus());
System.out.println("分账明细: " + response.getReceivers());
分账回退
SplitResponse reverseSplit(ReverseSplitRequest request)
将已分账的资金回退到原商户账户,用于退款或分账错误纠正。
渠道限制: 仅微信支付支持分账回退,支付宝无此概念。接收方必须是 MERCHANT_ID 类型。
代码示例
ReverseSplitRequest request = ReverseSplitRequest.builder()
.channel(Channel.WECHAT)
.outSplitNo("SPLIT-20260726-001")
.outReturnNo("RETURN-20260726-001")
.returnAccount("1900000001")
.returnAmount(new BigDecimal("300.00"))
.description("分账回退-退款")
.build();
SplitResponse response = client.reverseSplit(request);
System.out.println("回退状态: " + response.getSplitStatus());
完结分账
SplitResponse finishSplit(FinishSplitRequest request)
完结分账,解冻剩余冻结资金。完结后不可再对该订单发起分账。
渠道差异: 仅微信支付需要调用完结分账,支付宝无此概念(支付宝分账后资金即时到账,无需完结操作)。
代码示例
FinishSplitRequest request = FinishSplitRequest.builder()
.channel(Channel.WECHAT)
.merchantOrderNo("ORD-20260726-001")
.channelTransactionId("4200001234202607260000000001")
.description("分账完结,解冻剩余资金")
.build();
SplitResponse response = client.finishSplit(request);
System.out.println("完结状态: " + response.getSplitStatus());
仅发起分账(不轮询)
SplitResponse splitPaymentNoPoll(SplitRequest request)
仅向渠道发起分账请求,不自动轮询查询最终结果。返回的 ACCEPTED 状态为受理态,并非终态,商户需自行调用 querySplit() 查询最终分账结果。
适用场景: 适合需要自定义轮询策略、对接 MQ 异步通知或对延迟敏感的场景。常规场景请使用 splitPayment(),SDK 会自动轮询至终态。
SplitRequest 参数
参数与 splitPayment() 完全一致,详见「发起分账」一节。
代码示例
SplitRequest request = SplitRequest.builder()
.channel(Channel.WECHAT)
.merchantOrderNo("ORD-20260726-001")
.channelTransactionId("4200001234202607260000000001")
.outSplitNo("SPLIT-20260726-003")
.ruleId(10086L)
.build();
// 仅发起,不轮询;返回 ACCEPTED 受理态
SplitResponse response = client.splitPaymentNoPoll(request);
System.out.println("受理状态: " + response.getSplitStatus());
// 商户自行轮询查询最终结果
QuerySplitRequest queryReq = QuerySplitRequest.builder()
.channel(Channel.WECHAT)
.outSplitNo("SPLIT-20260726-003")
.build();
SplitResponse finalResp = client.querySplit(queryReq);
注意: 返回 ACCEPTED 仅表示渠道已受理分账请求,不代表分账成功。必须通过 querySplit() 获取 SUCCESS / FAILED 终态后才可更新本地账务。
异步分账
CompletableFuture<SplitResponse> splitPaymentAsync(SplitRequest request)
在独立线程中执行分账 + 轮询,不阻塞调用线程。适用于 Web 请求等对响应时间敏感的场景。异步方法自动传播 MDC 上下文,确保链路日志追踪不断链。
代码示例
SplitRequest request = SplitRequest.builder()
.channel(Channel.WECHAT)
.merchantOrderNo("ORD-20260727-005")
.channelTransactionId("4200001234202607270000000001")
.outSplitNo("SPLIT-20260727-001")
.ruleId(10086L)
.build();
CompletableFuture<SplitResponse> future = client.splitPaymentAsync(request);
future.thenAccept(response -> {
System.out.println("分账完成: " + response.getSplitStatus());
// response.getSplitDetails() 可获取每笔分账明细
}).exceptionally(ex -> {
System.err.println("分账失败: " + ex.getMessage());
return null;
});
执行器: 默认使用 ForkJoinPool.commonPool()。可通过 PaymentClients.builder().asyncExecutor(yourExecutor) 自定义线程池。
拉取分账对账单
SplitStatementResponse fetchSplitStatements(Channel channel, LocalDate date)
调用微信支付 / 支付宝官方「下载分账账单」API,获取指定日期的分账对账数据。与 fetchChannelStatements() 的区别:本方法返回分账账单(含接收方明细),用于分账对账。
代码示例
import com.kairwallet.payment.api.response.SplitStatementResponse;
import java.time.LocalDate;
SplitStatementResponse resp = client.fetchSplitStatements(
Channel.WECHAT,
LocalDate.of(2026, 7, 27)
);
resp.getStatements().forEach(item -> {
System.out.println("分账单号: " + item.getOutSplitNo()
+ ", 接收方: " + item.getReceiverAccount()
+ ", 金额: " + item.getSplitAmount());
});
注意: 分账账单生成时间与普通对账单一致,每日 10:00 后可下载前一日数据。微信分账账单文件名格式为 mch_id_YYYYMMDD_SPLIT.csv。
分账规则缓存管理
SplitRuleCache getSplitRuleCache()
获取 SDK 内部的分账规则缓存实例。商户系统在接收到 SaaS Webhook 的 SPLIT_RULE_CHANGED 事件后,可通过此方法清空指定规则的缓存,确保下次分账请求拉取最新规则。
代码示例(Webhook 处理中刷新缓存)
import com.kairwallet.payment.api.spi.SplitRuleCache;
// 在接收到 SPLIT_RULE_CHANGED 事件后刷新缓存
void onSplitRuleChanged(long ruleId) {
SplitRuleCache cache = client.getSplitRuleCache();
if (cache != null) {
cache.invalidate(ruleId);
System.out.println("已刷新分账规则缓存: ruleId=" + ruleId);
}
}
// 也可以清空全部缓存
if (cache != null) {
cache.invalidateAll();
}
适用场景: 仅在使用 ruleId 模式分账(SDK 自动从 SaaS 拉取规则)时需要管理缓存。使用 receivers 直接传入分账接收方列表时无需关注。
支付回调
PayNotifyEvent parsePayNotify(Channel channel, Map<String,String> headers, String body)
解析支付渠道的异步回调通知。SDK 内部自动完成验签,验签失败将抛出异常。
代码示例(Spring Controller)
import com.kairwallet.payment.api.*;
import com.kairwallet.payment.model.*;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/notify")
public class PayNotifyController {
private final PaymentClient client;
public PayNotifyController(PaymentClient client) {
this.client = client;
}
@PostMapping("/pay")
public String handlePayNotify(
@RequestHeader Map<String, String> headers,
@RequestBody String body) {
// SDK 自动验签,验签失败抛出 SignatureVerifyException
PayNotifyEvent event = client.parsePayNotify(
Channel.WECHAT, headers, body);
// 处理业务逻辑
String orderNo = event.getMerchantOrderNo();
String status = event.getStatus(); // "SUCCESS"
BigDecimal amount = event.getAmount();
// ... 更新本地订单状态
// 返回渠道要求的成功响应
return event.getSuccessResponse();
}
}
幂等处理: 渠道可能重复发送回调,业务端必须做幂等处理,避免重复入账。建议通过 merchantOrderNo + status 做去重判断。
退款回调
RefundNotifyEvent parseRefundNotify(Channel channel, Map<String,String> headers, String body)
解析退款渠道的异步回调通知。SDK 内部自动完成验签,验签失败将抛出异常。
代码示例(Spring Controller)
import com.kairwallet.payment.api.*;
import com.kairwallet.payment.model.*;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/notify")
public class RefundNotifyController {
private final PaymentClient client;
public RefundNotifyController(PaymentClient client) {
this.client = client;
}
@PostMapping("/refund")
public String handleRefundNotify(
@RequestHeader Map<String, String> headers,
@RequestBody String body) {
// SDK 自动验签
RefundNotifyEvent event = client.parseRefundNotify(
Channel.WECHAT, headers, body);
String refundNo = event.getRefundOrderNo();
String status = event.getRefundStatus(); // "SUCCESS"
// ... 更新本地退款状态
return event.getSuccessResponse();
}
}
构建回调响应
String buildNotifySuccessResponse(Channel channel)
String buildNotifyFailResponse(Channel channel, String message)
根据不同支付渠道规范,构建商户回调处理完成后需要返回给渠道的响应内容。用于在 Controller 中处理完业务逻辑后,向渠道返回标准成功/失败应答。
方法说明
| 方法 | 参数 | 返回 | 说明 |
| buildNotifySuccessResponse | Channel channel | String | 返回渠道要求的成功响应(如微信 {"code":"SUCCESS","message":"成功"}) |
| buildNotifyFailResponse | Channel channel, String message | String | 返回失败响应,message 为失败原因 |
代码示例(Spring Controller)
import com.kairwallet.payment.api.*;
import com.kairwallet.payment.model.*;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/notify")
public class PayNotifyController {
private final PaymentClient client;
public PayNotifyController(PaymentClient client) {
this.client = client;
}
@PostMapping("/pay")
public String handlePayNotify(
@RequestHeader Map<String, String> headers,
@RequestBody String body) {
try {
PayNotifyEvent event = client.parsePayNotify(
Channel.WECHAT, headers, body);
// ... 业务处理
return client.buildNotifySuccessResponse(Channel.WECHAT);
} catch (Exception ex) {
return client.buildNotifyFailResponse(
Channel.WECHAT, ex.getMessage());
}
}
}
必须返回正确响应: 若未按渠道规范返回成功响应,渠道会在超时后反复重试回调(最长可达 24 小时)。建议业务处理后无论成败都返回成功响应,避免渠道反复重试,仅依赖本地幂等保障最终一致。
企业转账
TransferResponse transfer(TransferRequest request)
商户向用户发起转账,支持微信零钱和支付宝账户。
代码示例
import com.kairwallet.payment.api.*;
import com.kairwallet.payment.model.*;
TransferRequest request = TransferRequest.builder()
.channel(Channel.WECHAT)
.merchantTransferNo("TRF-20260726-001")
.openId("o8ydt0Zxxxxxxxxxxxxx")
.amount(new BigDecimal("200.00"))
.subject("用户提现")
.notifyUrl("https://example.com/api/notify/transfer")
.build();
TransferResponse response = client.transfer(request);
System.out.println("转账状态: " + response.getStatus());
System.out.println("渠道流水号: " + response.getChannelTransactionId());
提示: 微信企业付款到零钱需确保商户余额充足,且已开通企业付款产品权限。
拉取对账单
StatementResponse fetchChannelStatements(DownloadStatementRequest request)
StatementResponse fetchChannelStatements(Channel channel, LocalDate date)
从支付渠道拉取指定日期的对账单,用于商户与渠道之间的交易核对。提供完整参数和便捷两种调用方式。
代码示例
import com.kairwallet.payment.api.*;
import com.kairwallet.payment.model.*;
import java.time.LocalDate;
// 方式一:便捷方法,仅需渠道和日期
StatementResponse resp = client.fetchChannelStatements(
Channel.WECHAT,
LocalDate.of(2026, 7, 25)
);
System.out.println("账单下载链接: " + resp.getDownloadUrl());
// 方式二:完整参数
DownloadStatementRequest request = DownloadStatementRequest.builder()
.channel(Channel.ALIPAY)
.date(LocalDate.of(2026, 7, 25))
.billType("trade")
.build();
StatementResponse response = client.fetchChannelStatements(request);
System.out.println("账单条数: " + response.getTotalCount());
IP 说明: 对账单接口使用商户自有出口 IP 调用渠道 API,无集中 IP 风控。请确保商户 IP 已加入渠道白名单。
账单生成时间: 每日 10:00 后可下载前一日的对账单。建议在每日 10:30 之后调用此接口。
上传单据
ReceiptUploadResponse uploadReceipt(ReceiptUploadRequest request)
向 KairWallet VAS(增值服务)平台上传业务单据,用于 SaaS 平台统一归集各商户的交易凭证。支持两种模式:自动上报和手动上报。
ReceiptUploadRequest 参数
| 字段 | 类型 | 必填 | 说明 |
| businessNo | String | 必填 | 业务编号 |
| channel | Channel | 必填 | 支付渠道 |
| receiptType | ReceiptType | 必填 | 单据类型枚举 |
| amount | BigDecimal | 必填 | 金额 |
| tradeTime | LocalDateTime | 必填 | 交易时间 |
| status | String | 必填 | 交易状态 |
| intermediateState | String | 可选 | 中间状态 |
| merchantOrderNo | String | 可选 | 商户订单号 |
| channelTransactionId | String | 可选 | 渠道交易流水号 |
| relatedSplitNo | String | 可选 | 关联分账单号 |
| splitStatus | String | 可选 | 分账状态 |
| splitDetails | String | 可选 | 分账明细 |
| tags | Map<String,String> | 可选 | 自定义标签 |
| rawChannelBody | String | 可选 | 渠道原始响应体 |
方式一:自动上报(VasConfig 配置)
在 PaymentClient 初始化时配置 VasConfig,SDK 会在支付/退款成功后自动上报单据,无需手动调用。
import com.kairwallet.payment.config.VasConfig;
// 初始化时配置自动上报
PaymentClient client = PaymentClient.builder()
.appKey("hw_prod_8f2a9c7b8d9e3817")
.appSecret(System.getenv("KAIRWALLET_SECRET"))
.environment(Environment.PRODUCTION)
.vasConfig(VasConfig.builder()
.autoUpload(true)
.uploadUrl("https://vas.kairwallet.com/api/receipt/upload")
.build())
.build();
// pay() / refund() 成功后自动上报,无需额外代码
PaymentResponse response = client.pay(request);
方式二:手动上报(SaaSPlatformClients)
通过 SaaSPlatformClients 手动上传单据,适用于需要对上报内容做额外处理的场景。
import com.kairwallet.payment.api.SaaSPlatformClients;
import com.kairwallet.payment.model.ReceiptType;
ReceiptUploadRequest uploadReq = ReceiptUploadRequest.builder()
.businessNo("ORD-20260726-001")
.channel(Channel.WECHAT)
.receiptType(ReceiptType.PAYMENT)
.amount(new BigDecimal("2480.00"))
.tradeTime(LocalDateTime.now())
.status("SUCCESS")
.merchantOrderNo("ORD-20260726-001")
.channelTransactionId("4200001234202607260000000001")
.build();
ReceiptUploadResponse uploadResp = SaaSPlatformClients.uploadReceipt(uploadReq);
ReceiptType 字段映射表
| ReceiptType | 必填字段 | 说明 |
| PAYMENT | merchantOrderNo, channelTransactionId | 支付单据 |
| REFUND | merchantOrderNo, channelTransactionId | 退款单据 |
| TRANSFER | channelTransactionId | 转账单据 |
| CLOSE | merchantOrderNo | 关单单据 |
| SPLIT | relatedSplitNo, splitDetails | 分账单据 |
| SPLIT_REVERSE | relatedSplitNo | 分账回退单据 |
| SPLIT_FINISH | merchantOrderNo, splitStatus | 完结分账单据 |
批量上传单据
ReceiptBatchUploadResponse uploadReceipts(ReceiptBatchUploadRequest request)
批量上传业务单据到 VAS 平台,适用于定时任务归集、批量补传等场景。单次请求最多 100 条,超出需分批调用。
ReceiptBatchUploadRequest 参数
| 字段 | 类型 | 必填 | 说明 |
| receipts | List<ReceiptUploadRequest> | 必填 | 单据列表,单次 ≤ 100 条 |
| batchNo | String | 可选 | 批次号,便于关联查询 |
代码示例
import com.kairwallet.payment.api.SaaSPlatformClient;
import com.kairwallet.payment.api.SaaSPlatformClients;
// 创建 SaaSPlatformClient 实例
SaaSPlatformClient saasClient = SaaSPlatformClients.builder()
.appKey("hw_prod_8f2a9c7b8d9e3817")
.appSecret(System.getenv("KAIRWALLET_SECRET"))
.environment(Environment.PRODUCTION)
.build();
List<ReceiptUploadRequest> receipts = Arrays.asList(
ReceiptUploadRequest.builder()
.businessNo("ORD-20260726-001")
.channel(Channel.WECHAT)
.receiptType(ReceiptType.PAYMENT)
.amount(new BigDecimal("2480.00"))
.tradeTime(LocalDateTime.now())
.status("SUCCESS")
.build(),
ReceiptUploadRequest.builder()
.businessNo("ORD-20260726-002")
.channel(Channel.ALIPAY)
.receiptType(ReceiptType.PAYMENT)
.amount(new BigDecimal("990.00"))
.tradeTime(LocalDateTime.now())
.status("SUCCESS")
.build()
);
ReceiptBatchUploadResponse resp = saasClient.uploadReceipts(
ReceiptBatchUploadRequest.builder()
.receipts(receipts)
.batchNo("BATCH-20260726-001")
.build()
);
System.out.println("成功条数: " + resp.getSuccessCount());
限制: 单次请求 ≤ 100 条。超出部分会被拒绝并返回错误码 40001。建议按 100 条切片,串行或并行调用多批。
查询单据上传状态
UploadStatusResponse queryUploadStatus(UploadStatusQueryRequest request)
查询已上传单据的处理状态,适用于异步归集后核对最终入库结果、定位失败单据。
UploadStatusQueryRequest 参数
| 字段 | 类型 | 必填 | 说明 |
| businessNo | String | 可选 | 按业务编号查询(与 batchNo 二选一) |
| batchNo | String | 可选 | 按批次号查询(与 businessNo 二选一) |
| status | String | 可选 | 按状态过滤:PENDING / SUCCESS / FAILED |
代码示例
UploadStatusResponse resp = saasClient.queryUploadStatus(
UploadStatusQueryRequest.builder()
.batchNo("BATCH-20260726-001")
.status("FAILED")
.build()
);
resp.getRecords().forEach(r -> {
System.out.printf("业务号=%s 状态=%s 原因=%s%n",
r.getBusinessNo(), r.getStatus(), r.getFailReason());
});
开立钱包账户
WalletAccountResponse openWalletAccount(WalletAccountOpenRequest request)
在 VAS 平台为商户或用户开立钱包账户,作为后续充值、消费、退款等操作的载体。
WalletAccountOpenRequest 参数
| 字段 | 类型 | 必填 | 说明 |
| externalId | String | 必填 | 商户侧唯一标识(如 user_id / merchant_id) |
| accountType | WalletAccountType | 必填 | 账户类型:PERSONAL / MERCHANT |
| displayName | String | 必填 | 账户显示名称 |
| currency | String | 可选 | 币种,默认 CNY |
| tags | Map<String,String> | 可选 | 自定义标签 |
代码示例
WalletAccountResponse resp = saasClient.openWalletAccount(
WalletAccountOpenRequest.builder()
.externalId("user_10086")
.accountType(WalletAccountType.PERSONAL)
.displayName("张三的钱包")
.build()
);
System.out.println("钱包账号: " + resp.getWalletAccountId());
查询钱包账户
WalletAccountResponse queryWalletAccount(String walletAccountId)
按钱包账户 ID 查询账户详情,包括余额、状态、绑定信息等。
参数
| 字段 | 类型 | 必填 | 说明 |
| walletAccountId | String | 必填 | VAS 平台分配的钱包账户 ID |
代码示例
WalletAccountResponse resp = saasClient.queryWalletAccount("WA-2026-0000001");
System.out.println("可用余额: " + resp.getAvailableBalance());
System.out.println("冻结余额: " + resp.getFrozenBalance());
System.out.println("账户状态: " + resp.getStatus());
列出钱包账户
WalletAccountListResponse listWalletAccounts(WalletAccountListRequest request)
按条件分页列出当前商户下的钱包账户。
WalletAccountListRequest 参数
| 字段 | 类型 | 必填 | 说明 |
| accountType | WalletAccountType | 可选 | 按账户类型过滤 |
| status | String | 可选 | 按状态过滤:ACTIVE / FROZEN / CLOSED |
| page | int | 可选 | 页码,从 1 开始,默认 1 |
| pageSize | int | 可选 | 每页条数,默认 20,最大 100 |
代码示例
WalletAccountListResponse resp = saasClient.listWalletAccounts(
WalletAccountListRequest.builder()
.accountType(WalletAccountType.MERCHANT)
.status("ACTIVE")
.page(1)
.pageSize(50)
.build()
);
resp.getAccounts().forEach(a -> {
System.out.println(a.getWalletAccountId() + " - " + a.getDisplayName());
});
System.out.println("总数: " + resp.getTotal());
发起钱包充值
WalletRechargeResponse rechargeWallet(WalletRechargeRequest request)
从绑定的银行卡或渠道账户向钱包账户充值。充值为异步处理,需调用 queryRechargeStatus() 查询最终结果。
WalletRechargeRequest 参数
| 字段 | 类型 | 必填 | 说明 |
| walletAccountId | String | 必填 | 目标钱包账户 ID |
| outRechargeNo | String | 必填 | 商户充值单号,全局唯一 |
| amount | BigDecimal | 必填 | 充值金额,单位:元 |
| source | String | 必填 | 充值来源:BANK_CARD / CHANNEL |
| notifyUrl | String | 可选 | 充值结果异步回调地址 |
| remark | String | 可选 | 备注 |
代码示例
WalletRechargeResponse resp = saasClient.rechargeWallet(
WalletRechargeRequest.builder()
.walletAccountId("WA-2026-0000001")
.outRechargeNo("RC-20260726-001")
.amount(new BigDecimal("10000.00"))
.source("BANK_CARD")
.notifyUrl("https://example.com/api/notify/recharge")
.build()
);
System.out.println("充值订单号: " + resp.getRechargeOrderId());
System.out.println("受理状态: " + resp.getStatus());
查询充值状态
RechargeStatusResponse queryRechargeStatus(String rechargeOrderId)
通过 rechargeWallet() 返回的充值订单号查询充值最终状态。建议在发起充值后 5 秒开始轮询,最长 60 秒。
参数
| 字段 | 类型 | 必填 | 说明 |
| rechargeOrderId | String | 必填 | 充值订单号(rechargeWallet 返回值) |
代码示例
RechargeStatusResponse resp = saasClient.queryRechargeStatus("RC-20260726-001");
System.out.println("状态: " + resp.getStatus());
// SUCCESS / PROCESSING / FAILED
System.out.println("到账金额: " + resp.getAmount());
查询充值订单(兼容)
RechargeStatusResponse queryRecharge(String rechargeId)
查询充值订单的兼容方法,与 queryRechargeStatus() 行为一致,参数名不同。用于历史对接兼容场景。
参数
| 字段 | 类型 | 必填 | 说明 |
| rechargeId | String | 必填 | 充值单号(兼容字段,同 rechargeOrderId) |
代码示例
RechargeStatusResponse resp = saasClient.queryRecharge("RC-20260726-001");
System.out.println("状态: " + resp.getStatus());
提示: 新接入建议直接使用 queryRechargeStatus(),本方法仅用于历史兼容。
取消充值
CancelRechargeResponse cancelRecharge(String rechargeId)
取消处于 PROCESSING 状态的充值订单。已成功的充值不可取消,需通过退款流程处理。
参数
| 字段 | 类型 | 必填 | 说明 |
| rechargeId | String | 必填 | 要取消的充值单号 |
代码示例
CancelRechargeResponse resp = saasClient.cancelRecharge("RC-20260726-001");
System.out.println("取消结果: " + resp.isSuccess());
注意: 仅 PROCESSING 状态可取消。若充值已成功,需调用 refundWallet() 退款。
钱包消费扣款
WalletConsumeResponse consumeWallet(WalletConsumeRequest request)
从钱包账户扣款用于消费场景(如用户在商户内消费、服务费扣减等)。扣款成功后资金即时从可用余额转出。
WalletConsumeRequest 参数
| 字段 | 类型 | 必填 | 说明 |
| walletAccountId | String | 必填 | 扣款钱包账户 ID |
| outConsumeNo | String | 必填 | 商户消费单号,全局唯一 |
| amount | BigDecimal | 必填 | 扣款金额 |
| subject | String | 必填 | 消费描述 |
| notifyUrl | String | 可选 | 异步回调地址 |
代码示例
WalletConsumeResponse resp = saasClient.consumeWallet(
WalletConsumeRequest.builder()
.walletAccountId("WA-2026-0000001")
.outConsumeNo("CONS-20260726-001")
.amount(new BigDecimal("128.00"))
.subject("会员服务月费")
.build()
);
System.out.println("扣款状态: " + resp.getStatus());
钱包退款
WalletRefundResponse refundWallet(WalletRefundRequest request)
对历史消费单发起退款,资金原路退回钱包账户。支持全额或部分退款。
WalletRefundRequest 参数
| 字段 | 类型 | 必填 | 说明 |
| walletAccountId | String | 必填 | 退款入账钱包账户 ID |
| outRefundNo | String | 必填 | 商户退款单号,全局唯一 |
| originalConsumeNo | String | 必填 | 原消费单号 |
| refundAmount | BigDecimal | 必填 | 退款金额 |
| reason | String | 可选 | 退款原因 |
代码示例
WalletRefundResponse resp = saasClient.refundWallet(
WalletRefundRequest.builder()
.walletAccountId("WA-2026-0000001")
.outRefundNo("WR-20260726-001")
.originalConsumeNo("CONS-20260726-001")
.refundAmount(new BigDecimal("50.00"))
.reason("用户申请部分退款")
.build()
);
System.out.println("退款状态: " + resp.getStatus());
通用钱包交易
WalletTransactionResponse executeTransaction(WalletTransactionRequest request)
通用钱包交易接口,通过 transactionType 区分消费(CONSUME)或退款(REFUND)。适合对参数归一化、统一接入的交易系统。
WalletTransactionRequest 参数
| 字段 | 类型 | 必填 | 说明 |
| walletAccountId | String | 必填 | 钱包账户 ID |
| outTxnNo | String | 必填 | 商户交易单号,全局唯一 |
| transactionType | WalletTxnType | 必填 | 交易类型:CONSUME / REFUND |
| amount | BigDecimal | 必填 | 交易金额 |
| subject | String | 必填 | 交易描述 |
| originalTxnNo | String | 可选 | 原交易单号(REFUND 时必填) |
代码示例
// 消费
WalletTransactionResponse consume = saasClient.executeTransaction(
WalletTransactionRequest.builder()
.walletAccountId("WA-2026-0000001")
.outTxnNo("TXN-20260726-001")
.transactionType(WalletTxnType.CONSUME)
.amount(new BigDecimal("88.00"))
.subject("商品消费")
.build()
);
// 退款(须传 originalTxnNo)
WalletTransactionResponse refund = saasClient.executeTransaction(
WalletTransactionRequest.builder()
.walletAccountId("WA-2026-0000001")
.outTxnNo("TXN-20260726-002")
.transactionType(WalletTxnType.REFUND)
.amount(new BigDecimal("88.00"))
.subject("全额退款")
.originalTxnNo("TXN-20260726-001")
.build()
);
提示: CONSUME 等同于 consumeWallet(),REFUND 等同于 refundWallet()。功能场景二选一即可,推荐按业务系统风格选择。
冻结钱包余额
WalletFreezeResponse freezeWalletBalance(WalletFreezeRequest request)
冻结钱包可用余额中的指定金额,被冻结金额不可消费但仍在账户内。适用于订单预占、风控冻结等场景。
WalletFreezeRequest 参数
| 字段 | 类型 | 必填 | 说明 |
| walletAccountId | String | 必填 | 钱包账户 ID |
| outFreezeNo | String | 必填 | 冻结单号,全局唯一 |
| amount | BigDecimal | 必填 | 冻结金额 |
| reason | String | 必填 | 冻结原因 |
| expireTime | LocalDateTime | 可选 | 冻结到期时间,到期自动解冻 |
代码示例
WalletFreezeResponse resp = saasClient.freezeWalletBalance(
WalletFreezeRequest.builder()
.walletAccountId("WA-2026-0000001")
.outFreezeNo("FRZ-20260726-001")
.amount(new BigDecimal("500.00"))
.reason("订单预占")
.expireTime(LocalDateTime.now().plusHours(2))
.build()
);
System.out.println("冻结状态: " + resp.getStatus());
解冻钱包余额
WalletUnfreezeResponse unfreezeWalletBalance(WalletUnfreezeRequest request)
解冻已被冻结的余额,可全额或部分解冻。解冻后金额恢复为可用余额。
WalletUnfreezeRequest 参数
| 字段 | 类型 | 必填 | 说明 |
| walletAccountId | String | 必填 | 钱包账户 ID |
| originalFreezeNo | String | 必填 | 原冻结单号 |
| outUnfreezeNo | String | 必填 | 解冻单号,全局唯一 |
| amount | BigDecimal | 必填 | 解冻金额(≤ 原冻结金额) |
| action | String | 可选 | UNFREEZE(退回可用)/ DEDUCT(扣减出账),默认 UNFREEZE |
代码示例
// 方式一:解冻回可用余额
WalletUnfreezeResponse resp = saasClient.unfreezeWalletBalance(
WalletUnfreezeRequest.builder()
.walletAccountId("WA-2026-0000001")
.originalFreezeNo("FRZ-20260726-001")
.outUnfreezeNo("UNF-20260726-001")
.amount(new BigDecimal("500.00"))
.action("UNFREEZE")
.build()
);
// 方式二:扣减冻结金额(用于确认扣款)
WalletUnfreezeResponse deduct = saasClient.unfreezeWalletBalance(
WalletUnfreezeRequest.builder()
.walletAccountId("WA-2026-0000001")
.originalFreezeNo("FRZ-20260726-001")
.outUnfreezeNo("UNF-20260726-002")
.amount(new BigDecimal("500.00"))
.action("DEDUCT")
.build()
);
下载对账数据
ReconciliationResponse downloadReconciliation(ReconciliationRequest request)
下载 VAS 平台与商户之间的对账数据,返回 JSON 结构,包含交易明细、汇总金额、差异条目等。适合程序化核对。
ReconciliationRequest 参数
| 字段 | 类型 | 必填 | 说明 |
| date | LocalDate | 必填 | 对账日期 |
| walletAccountId | String | 可选 | 指定钱包账户,不传则为商户全部 |
| type | String | 可选 | 对账类型:RECHARGE / CONSUME / REFUND / ALL,默认 ALL |
代码示例
ReconciliationResponse resp = saasClient.downloadReconciliation(
ReconciliationRequest.builder()
.date(LocalDate.of(2026, 7, 25))
.type("ALL")
.build()
);
System.out.println("总交易数: " + resp.getTotalCount());
System.out.println("总金额: " + resp.getTotalAmount());
resp.getDetails().forEach(d -> System.out.println(d.getTxnNo() + " " + d.getAmount()));
与文件链接的区别: 若需下载 CSV / Excel 文件,请使用 getReconciliationFileUrl() 获取下载链接。
获取对账文件下载链接
ReconciliationFileResponse getReconciliationFileUrl(ReconciliationRequest request)
获取指定日期对账文件的下载链接(CSV / Excel),链接有效期 30 分钟。适合人工核对或大批量数据离线处理。
ReconciliationRequest 参数
参数同 downloadReconciliation(),另支持 fileFormat 指定文件格式。
代码示例
ReconciliationFileResponse resp = saasClient.getReconciliationFileUrl(
ReconciliationRequest.builder()
.date(LocalDate.of(2026, 7, 25))
.fileFormat("CSV")
.build()
);
System.out.println("下载链接: " + resp.getDownloadUrl());
System.out.println("过期时间: " + resp.getExpireTime());
链接时效: 下载链接有效期 30 分钟,过期后需重新调用获取新链接。文件为一次性生成,请及时下载。
上传渠道对账单
ChannelStatementUploadResponse uploadChannelStatements(ChannelStatementUploadRequest request)
将 PaymentClient 从渠道拉取的对账单上传至 VAS 平台,由 VAS 进行平台与渠道之间的对账。通常与 PaymentClient.fetchChannelStatements() 配合使用。
ChannelStatementUploadRequest 参数
| 字段 | 类型 | 必填 | 说明 |
| channel | Channel | 必填 | 渠道:WECHAT / ALIPAY |
| date | LocalDate | 必填 | 对账日期 |
| downloadUrl | String | 必填 | 渠道对账单下载地址(fetchChannelStatements 返回) |
| billType | String | 可选 | 账单类型:trade / fund,默认 trade |
代码示例
// 1. 通过 PaymentClient 从渠道拉取对账单
StatementResponse stmt = client.fetchChannelStatements(
Channel.WECHAT, LocalDate.of(2026, 7, 25));
// 2. 上传至 VAS 平台进行对账
ChannelStatementUploadResponse resp = saasClient.uploadChannelStatements(
ChannelStatementUploadRequest.builder()
.channel(Channel.WECHAT)
.date(LocalDate.of(2026, 7, 25))
.downloadUrl(stmt.getDownloadUrl())
.build()
);
System.out.println("上传状态: " + resp.getStatus());
拉取并上传渠道对账单(便捷)
ChannelStatementUploadResponse fetchAndUploadChannelStatements(PaymentClient paymentClient, Channel channel, LocalDate date)
便捷方法,等价于 fetchChannelStatements() + uploadChannelStatements() 一步完成。适用于定时任务直接调用,无需中间处理。
参数
| 字段 | 类型 | 必填 | 说明 |
| paymentClient | PaymentClient | 必填 | 用于拉取渠道对账单的 PaymentClient 实例 |
| channel | Channel | 必填 | 渠道:WECHAT / ALIPAY |
| date | LocalDate | 必填 | 对账日期 |
代码示例
// 一步完成拉取 + 上传,适合定时任务
ChannelStatementUploadResponse resp = saasClient.fetchAndUploadChannelStatements(
client, // PaymentClient 实例
Channel.WECHAT,
LocalDate.of(2026, 7, 25)
);
System.out.println("处理状态: " + resp.getStatus());
等价于: StatementResponse s = paymentClient.fetchChannelStatements(channel, date); 之后 saasClient.uploadChannelStatements(...)。如需在中间环节插入自定义逻辑(如本地备份),请拆分为两步调用。
上传分账对账单
SplitChannelStatementUploadResponse uploadSplitChannelStatements(SplitChannelStatementUploadRequest request)
将商户通过 PaymentClient.fetchSplitStatements() 拉取到的分账对账单上传到 SaaS 平台,用于 SaaS 端「渠道侧分账单 vs 本地 split_flow」对账,发现 SPLIT_NOT_REPORTED(漏报)和 SPLIT_LOCAL_EXTRA(多报)差异。
代码示例
import com.kairwallet.payment.vas.api.request.SplitChannelStatementUploadRequest;
import com.kairwallet.payment.vas.api.response.SplitChannelStatementUploadResponse;
SplitChannelStatementUploadRequest request = SplitChannelStatementUploadRequest.builder()
.channel(Channel.WECHAT)
.statementDate(LocalDate.of(2026, 7, 27))
.statements(fetchResponse.getStatements()) // 从 fetchSplitStatements 获取
.remark("每日自动对账上传")
.build();
SplitChannelStatementUploadResponse resp = saasClient.uploadSplitChannelStatements(request);
System.out.println("上传条数: " + resp.getTotalCount()
+ ", 成功: " + resp.getSuccessCount());
拉取并上传分账对账单
SplitChannelStatementUploadResponse fetchAndUploadSplitChannelStatements(PaymentClient paymentClient, Channel channel, LocalDate date)
便捷方法:先调用 PaymentClient.fetchSplitStatements() 拉取分账账单,再调用 uploadSplitChannelStatements() 上传到 SaaS。一行代码完成「拉取 → 转换 → 上传」全流程,内置异常处理。
代码示例
// 一行完成拉取 + 上传
SplitChannelStatementUploadResponse resp = saasClient.fetchAndUploadSplitChannelStatements(
paymentClient, // 商户自有的 PaymentClient 实例
Channel.WECHAT,
LocalDate.of(2026, 7, 27)
);
System.out.println("结果: " + (resp.isSuccess() ? "成功" : "失败: " + resp.getErrorMessage()));
System.out.println("上传条数: " + resp.getTotalCount());
内置容错: 拉取失败时返回 success=false 并附渠道错误码与信息,不会抛出异常,便于批量任务中逐条处理。
平台健康探测
VasHealthResponse health()
探测 VAS 平台健康状态,用于商户侧的健康检查、可用性监控。结果缓存 15 秒,频繁调用不会增加服务端压力。
VasHealthResponse 关键字段
| 字段 | 类型 | 说明 |
| status | String | UP / DOWN / DEGRADED |
| service | String | 服务名称 |
| version | String | 平台版本号 |
| timestamp | Long | 探测时间戳 |
| responseTimeMs | long | 本次探测耗时(毫秒) |
代码示例
VasHealthResponse resp = saasClient.health();
System.out.println("平台状态: " + resp.getStatus());
System.out.println("版本: " + resp.getVersion());
System.out.println("探测耗时: " + resp.getResponseTimeMs() + "ms");
缓存机制: 探测结果在 SDK 本地缓存 15 秒,可放心接入到 Spring Boot Actuator 等频繁调用的健康检查端点。
查询分账规则
SplitRule querySplitRule(Long ruleId)
根据规则 ID 从 SaaS 平台拉取分账规则详情。SDK 内部在使用 ruleId 模式分账时会自动调用此接口,商户也可主动调用于规则预览、缓存预热等场景。
SplitRule 关键字段
| 字段 | 类型 | 说明 |
| ruleId | Long | 规则 ID |
| ruleName | String | 规则名称 |
| mode | String | RATIO(比例)/ FIXED(固定金额) |
| receivers | List | 分账接收方列表(含账号、金额/比例、角色) |
| status | String | ACTIVE / INACTIVE |
代码示例
SplitRule rule = saasClient.querySplitRule(10086L);
if (rule != null) {
System.out.println("规则: " + rule.getRuleName());
System.out.println("分配方式: " + rule.getMode());
rule.getReceivers().forEach(r -> {
System.out.println(" 接收方: " + r.getReceiverAccount()
+ ", 比例: " + r.getRatio());
});
} else {
System.out.println("规则不存在或已停用");
}
套餐权限: 分账规则属于高级功能,需要专业版套餐。SaaS 平台在收到请求时会校验商户套餐权限,无权限时返回相应错误。
VAS 指标采集
VasMetricsCollector getMetricsCollector()
获取 SaaSPlatformClient 持有的指标采集器实例,用于查询 VAS 相关操作的统计数据(如单据上传成功数、对账任务完成数等)。
代码示例
VasMetricsCollector collector = saasClient.getMetricsCollector();
if (collector != null) {
System.out.println("单据上传总数: " + collector.getTotalUploads());
System.out.println("对账任务完成数: " + collector.getReconciliationCompleted());
System.out.println("平均处理延迟: " + collector.getAvgLatencyMs() + "ms");
}
注意: VAS 指标采集器独立于 PaymentClient 的 MetricsCollector。如需统一监控,建议在自定义实现中将两者数据聚合到同一监控系统。
VAS 优雅关闭
void shutdown()
释放 SaaSPlatformClient 持有的资源,包括 VAS 异步上报线程池、定时对账任务等。应在应用关闭时调用,与 PaymentClient.shutdown() 配对使用。
代码示例
// 应用关闭时依次关闭两个客户端
@PreDestroy
public void destroy() {
paymentClient.shutdown(); // 关闭支付客户端
saasClient.shutdown(); // 关闭 VAS 客户端
System.out.println("所有客户端已关闭");
}
关闭顺序: 建议先关闭 PaymentClient,再关闭 SaaSPlatformClient。SaaSPlatformClient 内部可能持有对 PaymentClient 的引用(如拉取对账单时),反向关闭可能导致异常。
监控指标采集
MetricsCollector getMetricsCollector()
void setMetricsCollector(MetricsCollector collector)
获取或替换 SDK 内置的指标采集器。SDK 默认使用 DefaultMetricsCollector(基于内存的计数器实现),可通过 setMetricsCollector() 注入自定义采集器,将指标对接到 Micrometer、Prometheus、Cat 等监控系统。
方法说明
| 方法 | 返回 | 说明 |
| getMetricsCollector() | MetricsCollector | 获取当前采集器实例,可读取累计调用次数、成功率、耗时分布等 |
| setMetricsCollector(collector) | void | 替换为自定义采集器,须在首次 API 调用前设置 |
默认采集器指标
| 指标 | 说明 |
| apiCallCount | 各 API 方法累计调用次数 |
| apiSuccessCount | 成功次数 |
| apiFailureCount | 失败次数(按错误码细分) |
| apiLatencyP50/P95/P99 | 接口耗时分位值 |
代码示例:自定义采集器对接 Micrometer
import com.kairwallet.payment.metrics.MetricsCollector;
import io.micrometer.core.instrument.MeterRegistry;
import java.util.concurrent.TimeUnit;
public class MicrometerMetricsCollector implements MetricsCollector {
private final MeterRegistry registry;
public MicrometerMetricsCollector(MeterRegistry registry) {
this.registry = registry;
}
@Override
public void recordCall(String method, boolean success,
long costMs, String errorCode) {
registry.counter("kairwallet.api.calls",
"method", method,
"result", success ? "success" : "failure",
"code", errorCode
).increment();
registry.timer("kairwallet.api.latency",
"method", method
).record(costMs, TimeUnit.MILLISECONDS);
}
}
// 在 SDK 初始化前注入
PaymentClient client = PaymentClient.builder()
.appKey("hw_prod_8f2a9c7b8d9e3817")
.appSecret(System.getenv("KAIRWALLET_SECRET"))
.environment(Environment.PRODUCTION)
.build();
client.setMetricsCollector(new MicrometerMetricsCollector(meterRegistry));
// 读取默认采集器指标
MetricsCollector mc = client.getMetricsCollector();
System.out.println("累计调用: " + mc.getCallCount("pay"));
时机: setMetricsCollector() 必须在首次 API 调用之前调用,否则部分累计指标会丢失。建议在应用启动阶段(如 Spring @PostConstruct)完成注入。
异步 API
CompletableFuture<?> xxxAsync(...)
所有同步 API 均有对应的异步版本,方法名加 Async 后缀,返回 CompletableFuture,适用于高并发或非阻塞场景。
异步方法列表
| 异步方法 | 返回类型 | 对应同步方法 |
| payAsync() | CompletableFuture<PaymentResponse> | pay() |
| queryAsync() | CompletableFuture<QueryResponse> | query() |
| closeAsync() | CompletableFuture<CloseResponse> | close() |
| refundAsync() | CompletableFuture<RefundResponse> | refund() |
| queryRefundAsync() | CompletableFuture<RefundResponse> | queryRefund() |
| transferAsync() | CompletableFuture<TransferResponse> | transfer() |
| splitPaymentAsync() | CompletableFuture<SplitResponse> | splitPayment() |
自定义异步执行器
Executor getAsyncExecutor()
获取当前异步任务执行器。默认使用 ForkJoinPool.commonPool(),可在构建 PaymentClient 时通过 Builder 自定义:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
// 构建时指定自定义线程池
ExecutorService customExecutor = Executors.newFixedThreadPool(16, r -> {
Thread t = new Thread(r, "kairwallet-async");
t.setDaemon(true);
return t;
});
PaymentClient client = PaymentClients.builder()
.wechat(wechatConfig)
.asyncExecutor(customExecutor) // 注入自定义线程池
.build();
// 运行时获取执行器
Executor executor = client.getAsyncExecutor();
代码示例
// 异步发起支付
CompletableFuture<PaymentResponse> future = client.payAsync(request);
future.thenAccept(response -> {
System.out.println("异步支付成功: " + response.getChannelOrderNo());
}).exceptionally(ex -> {
System.err.println("异步支付失败: " + ex.getMessage());
return null;
});
// 异步链式组合
client.payAsync(payRequest)
.thenCompose(resp -> client.queryAsync(queryRequest))
.thenAccept(queryResp -> {
System.out.println("链式查询完成: " + queryResp.getStatus());
});
MDC 传播: 异步 API 自动传播 MDC 上下文(traceId 等),无需手动传递,日志链路完整可追溯。
分账异步: splitPaymentAsync() 的详细说明和代码示例请参见「异步分账」一节。
优雅关闭
void shutdown()
释放 PaymentClient 持有的所有资源,包括定时器、线程池、HTTP 连接池等。应在应用关闭时调用,避免资源泄漏。
代码示例(ShutdownHook)
// 方式一:JVM ShutdownHook
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
client.shutdown();
System.out.println("PaymentClient 已关闭");
}));
代码示例(Spring @PreDestroy)
import javax.annotation.PreDestroy;
import org.springframework.stereotype.Component;
@Component
public class PaymentClientHolder {
private final PaymentClient client;
public PaymentClientHolder() {
this.client = PaymentClient.builder()
.appKey("hw_prod_8f2a9c7b8d9e3817")
.appSecret(System.getenv("KAIRWALLET_SECRET"))
.environment(Environment.PRODUCTION)
.build();
}
public PaymentClient getClient() {
return client;
}
@PreDestroy
public void destroy() {
client.shutdown();
}
}
重要: 调用 shutdown() 后,该 PaymentClient 实例不可再使用。如需继续调用,必须重新创建实例。建议在应用生命周期管理中统一处理关闭逻辑。