本技能用于生成通联收付通协议支付全流程调用代码,覆盖从签约到退款的完整业务链路。
┌─────────────────────────────────────────────────────────────────────────────┐
│ 通联协议支付完整流程 │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 310001 │ │ 310002 │ │ 310011 │ │ 200004 │
│ 签约短信触发 │ ──► │ 签约确认 │ ──► │ 协议支付 │ ──► │ 交易查询 │
│ │ │ (验证码) │ │ │ │ (处理中时) │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
▼ ▼ ▼ ▼
发送验证码 返回协议号 返回交易流水 确认交易结果
保存REQ_SN 保存AGRMNO 保存REQ_SN 0000/4000成功
↓
┌─────────────┐ ┌─────────────┐ │
│ REFUND │ ──► │ 200004 │◄─────────────────────────┘
│ 交易退款 │ │ 退款查询 │ 需要退款时
│ │ │ (处理中时) │
└─────────────┘ └─────────────┘
▼ ▼
返回退款流水 确认退款结果
保存REQ_SN 0000/4000成功
| 步骤 | 接口 | 交易码 | 功能 | 关键输出 |
|---|---|---|---|---|
| ------ | ------ | -------- | ------ | ---------- |
| 1 | 签约短信触发 | 310001 | 发送短信验证码到用户手机 | REQ_SN(流水号) |
| 2 | 签约确认 | 310002 | 验证短信验证码,完成签约 | AGRMNO(协议号) |
| 3 | 协议支付 | 310011 | 使用协议号发起代扣支付 | REQ_SN(交易流水) |
| 4 | 交易查询 | 200004 | 查询支付交易结果 | DETAIL.RET_CODE |
| 5 | 交易退款 | REFUND | 对已成功的交易发起退款 | REQ_SN(退款流水) |
| 6 | 退款查询 | 200004 | 查询退款交易结果 | DETAIL.RET_CODE |
项目需要已安装通联SDK依赖:
<dependency>
<groupId>com.allinpay.cus.sdk</groupId>
<artifactId>allinpay-sft-sdk-java</artifactId>
<version>1.0.2</version>
</dependency>
// 使用Builder模式(推荐)
SftConfig config = SftConfig.builder()
.merchantId("您的商户号") // 通联分配的商户号
.userName("您的用户名") // 通联分配的用户名
.gatewayUrl("https://tlt.allinpay.com/aipg/ProcessServlet") // 生产环境
// .gatewayUrl("https://tlt-test.allinpay.com/aipg/ProcessServlet") // 测试环境
.privateKeyPath("/path/to/private.p12") // 商户私钥文件路径(p12格式)
.privateKeyPassword("您的私钥密码") // p12私钥文件密码
.allinpayPublicKeyPath("/path/to/allinpay-public.cer") // 通联公钥文件路径
.signType("SM2") // 签名类型:SM2/RSA(需与密钥格式匹配)
.encoding("GBK") // 编码:GBK/UTF-8
// .skipVerify(true) // 测试环境可跳过验签
.build();
// 创建客户端
SftClient client = new SftClient(config);
// 测试环境配置示例(跳过验签)
SftConfig config = SftConfig.builder()
.merchantId("您的商户号")
.userName("您的用户名")
.gatewayUrl("https://tlt-test.allinpay.com/aipg/ProcessServlet") // 测试环境网关
.privateKeyPath("/path/to/private.p12")
.privateKeyPassword("您的私钥密码")
.allinpayPublicKeyPath("/path/to/allinpay-public.cer")
.signType("SM2")
.encoding("GBK")
.skipVerify(true) // 测试环境跳过验签
.build();
// 使用Setter模式(传统方式)
SftConfig config = new SftConfig();
config.setMerchantId("您的商户号"); // 通联分配的商户号
config.setUserName("您的用户名"); // 通联分配的用户名
config.setGatewayUrl("https://tlt.allinpay.com/aipg/ProcessServlet"); // 生产环境
config.setPrivateKeyPath("/path/to/private.p12"); // 商户私钥文件路径(p12格式)
config.setPrivateKeyPassword("您的私钥密码"); // p12私钥文件密码
config.setAllinpayPublicKeyPath("/path/to/allinpay-public.cer"); // 通联公钥文件路径
config.setSignType("SM2"); // 签名类型:SM2/RSA(需与密钥格式匹配)
config.setEncoding("GBK"); // 编码:GBK/UTF-8
// 创建客户端
SftClient client = new SftClient(config);
| 项目 | 值 |
|---|---|
| ------ | ----- |
| 交易码 | 310001 |
| 接口名称 | 协议支付签约短信触发 |
| 业务模块 | FAGRA |
| 请求类型 | AgreementSignSmsRequest |
| 响应类型 | AgreementSignSmsResponse |
| 参数名 | 说明 | 示例 |
|---|---|---|
| -------- | ------ | ------ |
| merchantId | 商户代码 | 由通联分配 |
| accountNo | 账号(银行卡号) | 6225881234567890 |
| accountName | 账号名(持卡人姓名) | 张三 |
| accountProp | 账号属性 | 0-私人 1-公司 |
| idType | 证件类型 | 0-身份证 |
| id | 证件号 | 110101199001011234 |
| tel | 手机号 | 13800138000 |
| 参数名 | 说明 | 示例 |
|---|---|---|
| -------- | ------ | ------ |
| accountType | 账号类型 | 00-银行卡 02-信用卡 |
| bankCode | 银行代码 | 0308 |
| creditAcctNo | 信用卡卡号 | 工行、建行特色渠道需上送 |
| creditBankCode | 信用卡银行代码 | |
| cvv2 | 信用卡CVV2 | |
| validDate | 信用卡有效期 | MMYY |
| expired | 协议失效日 | yyyyMMdd |
| singleMaxAmt | 单笔最大限额 | 单位:分 |
| 报文头返回码 | 明细返回码 | 处理方式 | 分类 |
|---|---|---|---|
| ------------ | ----------- | --------- | ------ |
| 0000 | 0000 或无明细 | 签约申请成功,保存REQ_SN | 成功 |
| 0000 | 3XXX | 签约申请失败,检查客户信息 | 失败 |
| 其他返回码 | - | 请求失败,需重新发起签约申请 | 失败 |
// 创建签约短信请求
AgreementSignSmsRequest request = new AgreementSignSmsRequest();
request.setMerchantId("您的商户号");
request.setAccountNo("6225881234567890"); // 银行卡号
request.setAccountName("张三"); // 持卡人姓名
request.setAccountProp("0"); // 0-私人
request.setIdType("0"); // 0-身份证
request.setId("110101199001011234"); // 身份证号
request.setTel("13800138000"); // 手机号
// 发送请求
AgreementSignSmsResponse response = client.agreementSignSms(request);
// 处理响应
String retCode = response.getRetCode();
if ("0000".equals(retCode)) {
String detailRetCode = response.getDetailRetCode();
if (detailRetCode == null || "0000".equals(detailRetCode)) {
String srcReqSn = response.getReqSn(); // 保存此流水号,签约确认时使用
System.out.println("短信发送成功,流水号: " + srcReqSn);
System.out.println("请用户输入收到的验证码");
} else if (detailRetCode.startsWith("3")) {
System.out.println("签约申请失败: " + response.getRetMsg());
}
} else {
System.out.println("请求失败: " + response.getRetMsg());
}
| 项目 | 值 |
|---|---|
| ------ | ----- |
| 交易码 | 310002 |
| 接口名称 | 协议支付签约确认 |
| 业务模块 | FAGRC |
| 请求类型 | AgreementSignRequest |
| 响应类型 | AgreementSignResponse |
| 参数名 | 说明 | 来源 |
|---|---|---|
| -------- | ------ | ------ |
| merchantId | 商户代码 | 由通联分配 |
| srcReqSn | 原请求流水号 | 310001接口返回的REQ_SN |
| verCode | 验证码 | 用户收到的短信验证码 |
| 参数名 | 说明 | 示例 |
|---|---|---|
| -------- | ------ | ------ |
| accountNo | 账号(与签约短信一致) | 6225881234567890 |
| accountName | 账号名 | 张三 |
| id | 证件号 | 110101199001011234 |
| tel | 手机号 | 13800138000 |
| 报文头返回码 | 明细返回码 | 处理方式 | 分类 |
|---|---|---|---|
| ------------ | ----------- | --------- | ------ |
| 0000 | 0000 或无明细 | 签约成功,保存协议号(AGRMNO) | 成功 |
| 0000 | 3998 | 验证码错误或过期,需重新获取验证码 | 失败 |
| 0000 | 3XXX(其他) | 签约失败 | 失败 |
| 其他返回码 | - | 请求失败,需重新发起签约申请(从310001开始) | 失败 |
// 创建签约确认请求
AgreementSignRequest request = new AgreementSignRequest();
request.setMerchantId("您的商户号");
request.setSrcReqSn(srcReqSn); // 来自310001的流水号
request.setVerCode("111111"); // 用户输入的验证码
// 发送请求
AgreementSignResponse response = client.agreementSign(request);
// 处理响应
String retCode = response.getRetCode();
if ("0000".equals(retCode)) {
String detailRetCode = response.getDetailRetCode();
if (detailRetCode == null || "0000".equals(detailRetCode)) {
String agrmNo = response.getAgrmNo(); // 协议号,后续支付必需
System.out.println("签约成功,协议号: " + agrmNo);
// 重要:保存协议号到数据库
} else if ("3998".equals(detailRetCode)) {
System.out.println("验证码错误或已过期,请重新发送短信");
}
} else {
System.out.println("请求失败: " + response.getRetMsg());
}
| 项目 | 值 |
|---|---|
| ------ | ----- |
| 交易码 | 310011 |
| 接口名称 | 协议支付 |
| 业务模块 | FASTTRX |
| 请求类型 | AgreementPayRequest |
| 响应类型 | AgreementPayResponse |
| 参数名 | 说明 | 示例 |
|---|---|---|
| -------- | ------ | ------ |
| merchantId | 商户代码 | 由通联分配 |
| agrmNo | 协议号 | 签约时返回的协议号 |
| amount | 交易金额 | 单位:分(100元=10000分) |
| businessCode | 业务代码 | 由通联分配,协议支付专用业务代码 |
| submitTime | 提交时间 | 格式:yyyyMMddHHmmss(当前请求时间) |
| accountName | 账号名(持卡人姓名) | 张三 |
业务代码说明:
| 返回码 | 含义 | 处理方式 |
|---|---|---|
| -------- | ------ | ---------- |
| 0000 | 处理成功 | 需进一步判断交易状态 |
| 4000 | 已发送银行(默认成功) | 需进一步判断交易状态 |
| 2000 | 系统处理数据中 | 必须发起交易查询 |
| 2007 | 提交银行处理中 | 必须发起交易查询 |
| 2008 | 交易返回结果超时 | 必须发起交易查询 |
| 1108 | 批次号重复 | 必须发起交易查询 |
| 1000 | 报文处理中 | 必须发起交易查询 |
| 其他 | 失败 | 请求失败 |
协议支付响应中,通过 response.getDetailRetCode() 获取明细返回码,表示银行处理状态:
| 返回码 | 含义 | 处理方式 |
|---|---|---|
| -------- | ------ | ---------- |
| 0000 | 交易成功 | 成功,无需查询 |
| 4000 | 已发送银行(默认成功) | 成功,关注退票通知 |
| 2000/2007/2008 | 处理中 | 继续查询 |
| 3XXX | 银行返回错误 | 失败 |
重要说明:由于SDK内部实现差异,getDetailRetCode() 在某些情况下可能返回 null。
推荐使用以下方式判断交易状态:
isTrxSuccess() 和 isTrxFailed() 方法(SDK内部已正确解析FASTTRXRET)getErrMsg() 方法获取FASTTRXRET中的错误信息重要:HTTPS异常(读取超时、连接超时等)必须发起交易查询,不能直接判定失败!
// 创建协议支付请求
AgreementPayRequest request = new AgreementPayRequest();
request.setMerchantId("您的商户号");
request.setAgrmNo(agrmNo); // 签约时获取的协议号
request.setAmount("10000"); // 金额,单位分
request.setBusinessCode("09100"); // 业务代码(通联分配,协议支付专用)
request.setSubmitTime(new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())); // 提交时间(当前请求时间)
request.setAccountName("张三"); // 持卡人姓名
request.setNotifyUrl("https://your-server/notify");
try {
AgreementPayResponse response = client.agreementPay(request);
String reqSn = response.getReqSn(); // 保存流水号
// 【推荐方式】使用SDK提供的交易状态判断方法
if (response.isTrxSuccess()) {
System.out.println("支付成功,流水号: " + reqSn);
} else if (response.isTrxFailed()) {
System.out.println("支付失败: " + response.getErrMsg()); // 使用getErrMsg获取交易错误信息
return; // 失败,流程结束
} else if (response.isTrxProcessing()) {
System.out.println("处理中,必须发起交易查询(200004)");
// 继续查询流程...
} else {
// 兜底处理:无法确定状态时发起查询
String errMsg = response.getErrMsg();
if (errMsg != null && !errMsg.isEmpty()) {
System.out.println("支付失败: " + errMsg);
return;
}
System.out.println("无法确定状态,必须发起交易查询(200004)");
}
} catch (HttpTimeoutException e) {
// HTTP超时 - 必须发起查询
System.err.println("HTTP超时,必须发起交易查询(200004)确认状态!");
System.err.println("超时类型: " + (e.isConnectTimeout() ? "连接超时" : "读取超时"));
} catch (SftException e) {
// SDK异常
System.err.println("SDK异常: " + e.getErrorMessage());
} catch (Exception e) {
// 其他异常 - 必须发起查询
System.err.println("请求异常,必须发起交易查询(200004)确认状态!");
}
| 方法 | 返回值 | 说明 | 推荐使用 |
|---|---|---|---|
| ------ | -------- | ------ | ---------- |
isTrxSuccess() | boolean | 交易是否成功(SDK内部判断) | ✓ 推荐 |
isTrxFailed() | boolean | 交易是否失败(SDK内部判断) | ✓ 推荐 |
isTrxProcessing() | boolean | 交易是否处理中 | ✓ 推荐 |
getRetCode() | String | INFO节点返回码 | 用于日志记录 |
getRetMsg() | String | INFO节点返回信息 | 用于日志记录 |
getErrMsg() | String | FASTTRXRET节点错误信息 | ✓ 推荐获取交易错误 |
getDetailRetCode() | String | 明细返回码 | ⚠️ 可能返回null |
getReqSn() | String | 请求流水号 | 必须保存用于查询 |
getSettleDay() | String | 清算日期 | 交易成功时返回 |
getAcctSuffix() | String | 卡号后4位 | 交易成功时返回 |
getVoucherNo() | String | 银行流水号 | 交易成功时返回 |
| 项目 | 值 |
|---|---|
| ------ | ----- |
| 交易码 | 200004 |
| 接口名称 | 交易结果查询 |
| 业务模块 | QTRANSREQ |
| 请求类型 | TrxQueryRequest |
| 响应类型 | TrxQueryResponse |
| 参数名 | 说明 | 示例 |
|---|---|---|
| -------- | ------ | ------ |
| merchantId | 商户代码 | 由通联分配 |
| querySn | 要查询的交易流水号 | 原交易的REQ_SN |
| 返回码 | 含义 | 处理方式 |
|---|---|---|
| -------- | ------ | ---------- |
| 0000 | 查询成功 | 需进一步判断交易状态 |
| 2000/2007/2008 | 系统处理中 | 继续发起交易查询 |
| 1000 | 报文处理中 | 继续发起交易查询 |
| 1002 | 无此交易 | 特殊处理:10分钟后查询,30分钟无记录判失败 |
| 2002/2004/2006 | 失败 | 停止查询 |
| 其他 | 失败 | 查询失败 |
> 1.0.2升级说明:查询接口的状态判断逻辑已升级。isTrxSuccess() 表示"查询成功并返回了交易明细",每笔明细的交易状态需通过 QueryDetail.isSuccess() 逐笔判断。
| 方法 | 返回值 | 说明 |
|---|---|---|
| ------ | -------- | ------ |
isTrxSuccess() | boolean | 查询成功且返回了交易明细(header=0000/4000 且有明细) |
isTrxFailed() | boolean | 查询失败(已知失败码2002/2004/2006,或非成功非处理中) |
isTrxProcessing() | boolean | 处理中(header为处理码,或header成功但无明细) |
isNoTransaction() | boolean | 1002状态(无此交易记录) |
getDetails() | List | 交易明细列表 |
QueryDetail.isSuccess() | boolean | 明细交易是否成功(0000/4000) ← 1.0.2新增 |
推荐使用 SDK 提供的状态判断方法,避免手动解析返回码!
查询成功后,务必使用 detail.isSuccess() 判断每笔明细的交易状态!
交易查询响应中,通过 response.getDetails().get(0).getRetCode() 获取明细返回码,表示交易状态:
| 返回码 | 含义 | 处理方式 |
|---|---|---|
| -------- | ------ | ---------- |
| 0000 | 交易成功 | 成功,停止查询 |
| 4000 | 已发送银行(默认成功) | 成功,关注退票通知 |
| 2000/2007/2008 | 处理中 | 继续查询 |
| 其他(含3XXX) | 交易失败 | 失败,停止查询 |
> 1.0.2推荐:使用 QueryDetail.isSuccess() 方法判断明细交易状态(0000/4000为成功),避免手动比对返回码。
| 字段 | 方法 | 说明 |
|---|---|---|
| ------ | ------ | ------ |
| 批次号 | getBatchId() | 交易批次号 |
| 序号 | getSn() | 记录序号 |
| 交易方向 | getTrxDir() | 0付1收 |
| 清算日期 | getSettDay() | 清算日期 |
| 完成时间 | getFinTime() | 完成时间 |
| 账号 | getAccountNo() | 账号 |
| 金额 | getAmount() | 交易金额(分) |
| 返回码 | getRetCode() | 交易状态码 |
| 错误文本 | getErrMsg() | 错误信息 |
| 交易是否成功 | isSuccess() | 1.0.2新增,0000/4000为成功 |
| 时间 | 处理方式 |
|---|---|
| ------ | ---------- |
| 交易发起后10分钟内 | 不查询,等待 |
| 10分钟后 | 开始查询 |
| 30分钟后仍返回1002 | 停止查询,判定交易失败 |
// 创建查询请求
TrxQueryRequest request = new TrxQueryRequest();
request.setMerchantId("您的商户号");
request.setQuerySn(reqSn); // 原交易的流水号
TrxQueryResponse response = client.trxQuery(request);
// 【推荐方式】使用SDK提供的状态判断方法
if (response.isTrxSuccess()) {
// 查询成功并返回了交易明细
if (response.getDetails() != null && !response.getDetails().isEmpty()) {
TrxQueryResponse.QueryDetail detail = response.getDetails().get(0);
// 【1.0.2推荐】使用 detail.isSuccess() 判断每笔明细的交易状态
if (detail.isSuccess()) {
System.out.println("交易成功!");
System.out.println("交易金额: " + detail.getAmount() + " 分");
System.out.println("清算日期: " + detail.getSettDay());
System.out.println("银行流水: " + detail.getVoucherNo());
} else {
System.out.println("交易失败: " + detail.getErrMsg());
}
}
}
else if (response.isNoTransaction()) {
// 1002特殊处理:无此交易记录
System.out.println("无此交易,需在10分钟后查询,30分钟无记录判失败");
}
else if (response.isTrxProcessing()) {
System.out.println("交易仍在处理中,继续查询");
}
else if (response.isTrxFailed()) {
System.out.println("查询失败");
if (response.getDetails() != null && !response.getDetails().isEmpty()) {
TrxQueryResponse.QueryDetail detail = response.getDetails().get(0);
System.out.println("失败原因: " + detail.getErrMsg());
}
}
else {
System.out.println("查询失败: " + response.getRetMsg());
}
// 传统方式:手动判断返回码(不推荐,建议使用SDK状态判断方法)
String retCode = response.getRetCode();
if ("0000".equals(retCode) || "4000".equals(retCode)) {
if (response.getDetails() != null && !response.getDetails().isEmpty()) {
TrxQueryResponse.QueryDetail detail = response.getDetails().get(0);
String trxStatus = detail.getRetCode(); // 明细返回码表示交易状态
// 成功
if ("0000".equals(trxStatus) || "4000".equals(trxStatus)) {
System.out.println("交易成功");
}
// 处理中
else if ("2000".equals(trxStatus) || "2007".equals(trxStatus) || "2008".equals(trxStatus)) {
System.out.println("处理中,继续查询");
}
// 失败
else {
System.out.println("交易失败: " + trxStatus);
}
} else {
// header成功但无明细,结果尚未返回
System.out.println("处理中,继续查询");
}
}
// 1002特殊处理
else if ("1002".equals(retCode)) {
System.out.println("无此交易,需在10分钟后查询,30分钟无记录判失败");
}
// 已知失败码
else if ("2002".equals(retCode) || "2004".equals(retCode) || "2006".equals(retCode)) {
System.out.println("查询失败");
}
// 处理中
else if ("2000".equals(retCode) || "2007".equals(retCode) || "2008".equals(retCode)
|| "1000".equals(retCode) || "1001".equals(retCode)) {
System.out.println("系统处理中,继续查询");
}
| 项目 | 值 |
|---|---|
| ------ | ----- |
| 交易码 | REFUND |
| 接口名称 | 退款 |
| 业务模块 | REFUND |
| 请求类型 | RefundRequest |
| 响应类型 | RefundResponse |
| 参数名 | 说明 | 示例 |
|---|---|---|
| -------- | ------ | ------ |
| merchantId | 商户代码 | 由通联分配 |
| businessCode | 业务代码 | 由通联分配,退款专用业务代码 |
| orgBatchId | 原批次号 | 原交易的REQ_SN |
| orgBatchSn | 原批次序号 | 单笔实时交易填0 |
| amount | 退款金额 | 单位:分(不超过原交易金额) |
| 参数名 | 说明 | 示例 |
|---|---|---|
| -------- | ------ | ------ |
| submitTime | 提交时间 | 格式:yyyyMMddHHmmss(当前请求时间) |
| ledgerBack | 分账回退标识 | 1:走分账回退逻辑 |
| remark | 备注 | 用户申请退款 |
| notifyUrl | 通知地址 | 退款结果通知地址 |
| 业务代码 | 说明 | 使用场景 |
|---|---|---|
| ---------- | ------ | ---------- |
| 09200 | 商户退款 | 商户主动发起退款 |
| 09201 | 收付通退款 | 通联平台发起退款 |
重要提示:
| 方法 | 返回值 | 说明 |
|---|---|---|
| ------ | -------- | ------ |
isTrxSuccess() | boolean | 退款请求成功(0000/4000) |
isTrxFailed() | boolean | 退款请求失败 |
isTrxProcessing() | boolean | 退款请求处理中 |
getDetailErrMsg() | String | 明细错误信息(TRANSRET中的ERR_MSG) |
getSettleDay() | String | 清算日期 |
getVoucherNo() | String | 银行流水号 |
推荐使用 SDK 提供的状态判断方法!
| 返回码 | 含义 | 处理方式 |
|---|---|---|
| -------- | ------ | ---------- |
| 0000 | 处理成功 | 需进一步判断或发起查询 |
| 4000 | 已发送银行(默认成功) | 需进一步判断或发起查询 |
| 2000/2007/2008/1108/1000 | 处理中 | 必须发起交易查询 |
| 3999 | 其它错误 | 检查请求参数或联系技术支持 |
| 其他 | 失败 | 请求失败 |
常见3999错误原因:
重要:HTTPS异常必须发起交易查询,不能直接判定失败!
// 创建退款请求
RefundRequest request = new RefundRequest();
request.setMerchantId("您的商户号");
request.setBusinessCode("09200"); // 业务代码(通联分配,退款专用)
request.setOrgBatchId(payReqSn); // 原支付交易流水号(必须是成功的交易)
request.setOrgBatchSn("0"); // 单笔交易填0
request.setAmount("10000"); // 全额退款(不超过原交易金额)
request.setSubmitTime(new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())); // 提交时间
request.setRemark("用户申请退款");
try {
RefundResponse response = client.refund(request);
String reqSn = response.getReqSn(); // 保存退款流水号
// 【推荐方式】使用SDK提供的状态判断方法
if (response.isTrxSuccess()) {
System.out.println("退款请求成功,流水号: " + reqSn);
System.out.println("清算日期: " + response.getSettleDay());
System.out.println("建议调用200004查询确认最终状态");
}
else if (response.isTrxProcessing()) {
System.out.println("退款处理中,必须发起交易查询(200004)");
}
else if (response.isTrxFailed()) {
String errMsg = response.getDetailErrMsg();
if (errMsg != null && !errMsg.isEmpty()) {
System.out.println("退款请求失败: " + errMsg);
} else {
System.out.println("退款请求失败: " + response.getRetMsg());
}
}
else {
System.out.println("退款失败: " + response.getRetMsg());
}
} catch (HttpTimeoutException e) {
// HTTP超时 - 必须发起查询
System.err.println("HTTP超时,必须发起交易查询(200004)确认状态!");
System.err.println("超时类型: " + (e.isConnectTimeout() ? "连接超时" : "读取超时"));
if (e.getReqSn() != null) {
System.err.println("建议查询流水号: " + e.getReqSn());
}
} catch (SftException e) {
// SDK异常
System.err.println("SDK异常: " + e.getErrorMessage());
} catch (Exception e) {
System.err.println("请求异常,必须发起交易查询(200004)确认状态!");
}
// 传统方式:手动判断返回码(不推荐,建议使用SDK状态判断方法)
String retCode = response.getRetCode();
String reqSn = response.getReqSn(); // 保存退款流水号
if ("0000".equals(retCode) || "4000".equals(retCode)) {
System.out.println("退款请求成功,流水号: " + reqSn);
System.out.println("建议调用200004查询确认最终状态");
}
else if ("2000".equals(retCode) || "2007".equals(retCode) || "2008".equals(retCode)) {
System.out.println("退款处理中,必须发起交易查询(200004)");
}
else if ("3999".equals(retCode)) {
System.out.println("退款请求失败(其它错误): " + response.getRetMsg());
}
else {
System.out.println("退款失败: " + response.getRetMsg());
}
与步骤4使用相同接口(200004),传入退款请求的REQ_SN即可。
// 查询退款结果
TrxQueryRequest request = new TrxQueryRequest();
request.setMerchantId("您的商户号");
request.setQuerySn(refundReqSn); // 退款请求的流水号
TrxQueryResponse response = client.trxQuery(request);
// 【推荐方式】使用SDK提供的状态判断方法
if (response.isTrxSuccess()) {
System.out.println("退款成功!");
if (response.getDetails() != null && !response.getDetails().isEmpty()) {
TrxQueryResponse.QueryDetail detail = response.getDetails().get(0);
System.out.println("退款状态: " + detail.getRetCode());
System.out.println("退款金额: " + detail.getAmount() + " 分");
}
}
else if (response.isNoTransaction()) {
System.out.println("无此退款记录,建议稍后查询");
}
else if (response.isTrxProcessing()) {
System.out.println("退款仍在处理中,建议继续查询");
}
else if (response.isTrxFailed()) {
System.out.println("退款失败");
if (response.getDetails() != null && !response.getDetails().isEmpty()) {
TrxQueryResponse.QueryDetail detail = response.getDetails().get(0);
System.out.println("失败原因: " + detail.getErrMsg());
}
}
else {
System.out.println("退款查询失败: " + response.getRetMsg());
}
// 传统方式(不推荐)
if ("0000".equals(response.getRetCode())) {
if (response.getDetails() != null && !response.getDetails().isEmpty()) {
TrxQueryResponse.QueryDetail detail = response.getDetails().get(0);
String trxStatus = detail.getRetCode(); // 明细返回码表示交易状态
if ("0000".equals(trxStatus) || "4000".equals(trxStatus)) {
System.out.println("退款成功");
}
}
}
| 返回码 | 含义 | 处理方式 |
|---|---|---|
| -------- | ------ | ---------- |
| 0000 | 处理成功 | 成功,停止查询 |
| 4000 | 已发送银行(默认成功) | 成功,关注退票通知 |
核心规则:0000和4000都视为成功状态!
| 返回码 | 含义 | 处理方式 |
|---|---|---|
| -------- | ------ | ---------- |
| 2000 | 系统处理数据中 | 发起交易查询 |
| 2007 | 提交银行处理中 | 发起交易查询 |
| 2008 | 交易返回结果超时 | 发起交易查询 |
| 1108 | 批次号重复 | 发起交易查询 |
| 1000 | 报文处理中 | 发起交易查询 |
核心规则:以上返回码必须发起交易查询确认最终状态!
HTTPS异常(读取超时、连接超时等)必须发起交易查询,不能直接判定失败!
这是通联收付通的核心规则,所有接口都需遵守。
import com.allinpay.cus.sdk.sft.exception.HttpTimeoutException;
import com.allinpay.cus.sdk.sft.exception.SftException;
try {
AgreementPayResponse response = client.agreementPay(request);
// 正常响应处理...
} catch (HttpTimeoutException e) {
// HTTP超时(连接超时或读取超时) - 必须发起交易查询
System.err.println("HTTP超时,必须发起交易查询确认状态: " + e.getMessage());
if (e.isConnectTimeout()) {
System.err.println("超时类型: 连接超时");
} else if (e.isReadTimeout()) {
System.err.println("超时类型: 读取超时");
}
// 异常中包含请求流水号,可用于查询
String reqSn = e.getReqSn();
if (reqSn != null) {
System.err.println("建议查询流水号: " + reqSn);
TrxQueryRequest queryRequest = new TrxQueryRequest();
queryRequest.setMerchantId("您的商户号");
queryRequest.setQuerySn(reqSn); // 原请求流水号
// 建议设置定时任务轮询查询(如每5分钟查询一次)
}
} catch (SftException e) {
// SDK异常(签名失败、验签失败等)
System.err.println("SDK异常: " + e.getErrorMessage());
// 根据异常类型决定是否需要查询
if (e.getErrorCode() != null) {
System.err.println("错误码: " + e.getErrorCode());
}
} catch (Exception e) {
// 其他异常 - 必须发起交易查询
System.err.println("请求异常,必须发起交易查询确认状态: " + e.getMessage());
}
重要提示:
import com.allinpay.cus.sdk.sft.SftClient;
import com.allinpay.cus.sdk.sft.SftConfig;
import com.allinpay.cus.sdk.sft.request.*;
import com.allinpay.cus.sdk.sft.response.*;
import com.allinpay.cus.sdk.sft.response.TrxQueryResponse.QueryDetail;
import com.allinpay.cus.sdk.sft.exception.HttpTimeoutException;
import com.allinpay.cus.sdk.sft.exception.SftException;
import java.text.SimpleDateFormat;
import java.util.Date;
// ==================== 完整协议支付流程 ====================
// 配置初始化(推荐使用Builder模式)
SftConfig config = SftConfig.builder()
.merchantId("您的商户号")
.userName("您的用户名")
.gatewayUrl("https://tlt.allinpay.com/aipg/ProcessServlet")
.privateKeyPath("/path/to/private.p12")
.privateKeyPassword("您的私钥密码")
.allinpayPublicKeyPath("/path/to/allinpay-public.cer")
.signType("SM2")
.encoding("GBK")
.build();
SftClient client = new SftClient(config);
try {
// 1. 签约短信触发(310001)
AgreementSignSmsRequest smsRequest = new AgreementSignSmsRequest();
smsRequest.setAccountNo("6225881234567890");
smsRequest.setAccountName("张三");
smsRequest.setAccountProp("0");
smsRequest.setIdType("0");
smsRequest.setId("110101199001011234");
smsRequest.setTel("13800138000");
AgreementSignSmsResponse smsResponse = client.agreementSignSms(smsRequest);
if (!smsResponse.isTrxSuccess()) {
System.out.println("短信发送失败: " + smsResponse.getRetMsg());
return;
}
String srcReqSn = smsResponse.getReqSn(); // 保存流水号
System.out.println("短信发送成功,流水号: " + srcReqSn);
// 等待用户输入验证码...
String userCode = "111111"; // 测试环境固定验证码
// 2. 签约确认(310002)
AgreementSignRequest signRequest = new AgreementSignRequest();
signRequest.setSrcReqSn(srcReqSn);
signRequest.setVerCode(userCode);
AgreementSignResponse signResponse = client.agreementSign(signRequest);
if (!signResponse.isTrxSuccess()) {
String detailRetCode = signResponse.getDetailRetCode();
if ("3998".equals(detailRetCode)) {
System.out.println("验证码错误或过期,请重新发送短信");
} else {
System.out.println("签约失败: " + signResponse.getRetMsg());
}
return;
}
String agrmNo = signResponse.getAgrmNo();
if (agrmNo == null) {
System.out.println("签约失败,未获取协议号");
return;
}
System.out.println("签约成功,协议号: " + agrmNo);
// 重要:将协议号保存到数据库,后续支付使用
// 3. 协议支付(310011)
AgreementPayRequest payRequest = new AgreementPayRequest();
payRequest.setAgrmNo(agrmNo);
payRequest.setAmount("10000"); // 100元(单位:分)
payRequest.setBusinessCode("09100"); // 业务代码(通联分配,协议支付专用)
payRequest.setSubmitTime(new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())); // 当前请求时间
payRequest.setAccountName("张三"); // 持卡人姓名
AgreementPayResponse payResponse = client.agreementPay(payRequest);
String payReqSn = payResponse.getReqSn(); // 保存流水号用于查询和退款
System.out.println("支付请求流水号: " + payReqSn);
// 【推荐方式】使用SDK提供的交易状态判断方法
boolean paySuccess = false;
if (payResponse.isTrxSuccess()) {
paySuccess = true;
System.out.println("支付成功");
} else if (payResponse.isTrxFailed()) {
System.out.println("支付失败: " + payResponse.getErrMsg()); // 使用getErrMsg获取交易错误信息
return; // 流程结束
} else if (payResponse.isTrxProcessing()) {
System.out.println("支付处理中,需查询确认");
} else {
// 兜底处理
String errMsg = payResponse.getErrMsg();
if (errMsg != null && !errMsg.isEmpty()) {
System.out.println("支付失败: " + errMsg);
return;
}
System.out.println("无法确定状态,需查询确认");
}
// 4. 交易查询(200004)- 处理中时必须查询
if (!paySuccess) {
TrxQueryRequest queryRequest = new TrxQueryRequest();
queryRequest.setQuerySn(payReqSn);
TrxQueryResponse queryResponse = client.trxQuery(queryRequest);
// 使用SDK状态判断方法
if (queryResponse.isTrxSuccess()) {
paySuccess = true;
if (queryResponse.getDetails() != null && !queryResponse.getDetails().isEmpty()) {
QueryDetail detail = queryResponse.getDetails().get(0);
// 【1.0.2推荐】使用 detail.isSuccess() 判断明细交易状态
if (detail.isSuccess()) {
System.out.println("交易成功,状态: " + detail.getRetCode());
System.out.println("交易金额: " + detail.getAmount() + " 分");
} else {
System.out.println("交易失败: " + detail.getErrMsg());
paySuccess = false;
}
}
} else if (queryResponse.isNoTransaction()) {
System.out.println("无此交易记录(交易发起后10分钟内可能返回此状态)");
return;
} else if (queryResponse.isTrxProcessing()) {
System.out.println("交易仍在处理中,建议轮询查询");
return;
} else if (queryResponse.isTrxFailed()) {
System.out.println("交易失败");
if (queryResponse.getDetails() != null && !queryResponse.getDetails().isEmpty()) {
QueryDetail detail = queryResponse.getDetails().get(0);
System.out.println("失败原因: " + detail.getErrMsg());
}
return;
}
}
if (!paySuccess) {
System.out.println("支付未成功,流程结束");
return;
}
// 5. 交易退款(REFUND)- 需要退款时
RefundRequest refundRequest = new RefundRequest();
refundRequest.setBusinessCode("09200"); // 业务代码(通联分配,退款专用)
refundRequest.setOrgBatchId(payReqSn); // 原支付交易流水号
refundRequest.setOrgBatchSn("0"); // 单笔交易填0
refundRequest.setAmount("10000"); // 全额退款
refundRequest.setSubmitTime(new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())); // 提交时间
RefundResponse refundResponse = client.refund(refundRequest);
String refundReqSn = refundResponse.getReqSn(); // 保存退款流水号
System.out.println("退款请求流水号: " + refundReqSn);
// 使用SDK状态判断方法
if (refundResponse.isTrxSuccess()) {
System.out.println("退款请求已成功,需查询确认");
} else if (refundResponse.isTrxProcessing()) {
System.out.println("退款处理中,需查询确认");
} else if (refundResponse.isTrxFailed()) {
System.out.println("退款请求失败: " + refundResponse.getDetailErrMsg());
return;
} else {
System.out.println("退款请求失败: " + refundResponse.getRetMsg());
return;
}
// 6. 退款查询(200004)
TrxQueryRequest refundQuery = new TrxQueryRequest();
refundQuery.setQuerySn(refundReqSn); // 退款流水号
TrxQueryResponse refundResult = client.trxQuery(refundQuery);
// 使用SDK状态判断方法
if (refundResult.isTrxSuccess()) {
if (refundResult.getDetails() != null && !refundResult.getDetails().isEmpty()) {
QueryDetail detail = refundResult.getDetails().get(0);
// 【1.0.2推荐】使用 detail.isSuccess() 判断退款明细状态
if (detail.isSuccess()) {
System.out.println("退款成功");
} else {
System.out.println("退款失败: " + detail.getErrMsg());
}
}
}
} else if (refundResult.isNoTransaction()) {
System.out.println("无此退款记录,建议稍后查询");
} else if (refundResult.isTrxProcessing()) {
System.out.println("退款仍在处理中,建议继续查询");
} else if (refundResult.isTrxFailed()) {
System.out.println("退款失败");
if (refundResult.getDetails() != null && !refundResult.getDetails().isEmpty()) {
QueryDetail detail = refundResult.getDetails().get(0);
System.out.println("失败原因: " + detail.getErrMsg());
}
} else {
System.out.println("退款查询失败或无记录: " + refundResult.getRetMsg());
}
} catch (HttpTimeoutException e) {
// HTTP超时 - 必须发起交易查询
System.err.println("HTTP超时,必须发起交易查询确认状态!");
System.err.println("超时类型: " + (e.isConnectTimeout() ? "连接超时" : "读取超时"));
String reqSn = e.getReqSn();
if (reqSn != null) {
System.err.println("建议查询流水号: " + reqSn);
}
} catch (SftException e) {
// SDK异常(签名失败、验签失败等)
System.err.println("SDK异常: " + e.getErrorMessage());
} catch (Exception e) {
// 其他异常 - 必须发起交易查询
System.err.println("请求异常,必须发起交易查询确认状态: " + e.getMessage());
}
4000表示已发送银行但银行不能及时返回结果,默认成功。如果最终失败,银行会生成退票交易,商户需对接退票通知接口。
必须发起交易查询(200004)确认状态,不能直接判定失败。这是通联的核心规则。
无此交易记录。需在交易发起10分钟后查询,30分钟仍无记录则判定交易失败。
短信验证码有效期通常为5分钟。测试环境验证码固定为"111111"。
协议号(AGRMNO)是签约成功后的唯一标识,后续所有支付都需要使用协议号。必须妥善保存到数据库。
SDK在解析FASTTRXRET节点时可能未正确设置detailRetCode字段。推荐使用以下方法:
isTrxSuccess() - 判断交易是否成功isTrxFailed() - 判断交易是否失败getErrMsg() - 获取交易错误信息(FASTTRXRET中的ERR_MSG)> 1.0.2升级提示:对于交易查询(200004),isTrxSuccess() 语义已升级为"查询成功并返回明细",请使用 QueryDetail.isSuccess() 判断每笔明细的交易状态。
在SDK 1.0.2中,TrxQueryResponse.isTrxSuccess() 的含义已升级:
因此,在1.0.2中使用查询接口时,正确的判断流程为:
response.isTrxSuccess() → 查询成功,有明细结果detail.isSuccess() 判断每笔明细的交易是否成功(0000/4000)response.isTrxProcessing() → 交易仍在处理中(header处理码或header成功但无明细)response.isNoTransaction() → 1002(无此交易)退款返回3999表示"其它错误",具体原因需要查看返回的错误信息(ERR_MSG)。常见原因包括:
建议:先通过200004查询确认原支付交易状态,并检查退款参数是否正确。
submitTime应填写当前请求时间(发送请求的时间),格式为yyyyMMddHHmmss。这是用于防重和时间校验的必填参数。
签名类型必须与密钥格式匹配:
如果签名失败提示"can't identify EC private key",通常是因为签名类型与密钥不匹配。
p12格式的私钥文件需要密码才能读取,必须通过setPrivateKeyPassword()配置:
config.setPrivateKeyPath("/path/to/private.p12");
config.setPrivateKeyPassword("您的私钥密码"); // p12文件必须有密码
不能混用。业务代码由通联分配,不同业务类型使用不同的业务代码:
使用错误的业务代码会导致请求失败,返回"未开通该业务代码"等错误。请确认商户开通的业务代码类型。
| 文件 | 说明 | 业务场景 |
|---|---|---|
| ------ | ------ | ---------- |
| AgreementSignSmsExample.java | 签约短信触发示例(310001) | 用户首次签约,发送验证码 |
| AgreementSignExample.java | 签约确认示例(310002) | 验证验证码,获取协议号 |
| AgreementPayExample.java | 协议支付示例(310011) | 使用协议号发起代扣 |
| AgreementPayIntegrationExample.java | 完整流程示例 | 签约→支付→查询→退款全流程 |
| TrxQueryExample.java | 交易查询示例(200004) | 查询支付/退款交易结果 |
| RefundExample.java | 退款示例(REFUND) | 对已成功交易发起退款 |
每个示例文件包含详细的业务场景说明、前置条件、关键输出和注意事项。
共 3 个版本