通联收付通协议支付完整流程技能
功能说明
本技能用于生成通联收付通协议支付全流程调用代码,覆盖从签约到退款的完整业务链路。
适用场景
- 后端Java项目需要接入通联收付通协议支付
- 实现银行卡签约、代扣支付、交易查询、退款等完整流程
- 降低SDK文档阅读负担,快速集成支付功能
流程概览
完整流程链路
┌─────────────────────────────────────────────────────────────────────────────┐
│ 通联协议支付完整流程 │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 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.1</version>
</dependency>
SDK初始化
推荐方式:Builder模式
// 使用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模式
// 使用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);
步骤1:协议签约短信触发(310001)
接口信息
| 项目 | 值 | |------|-----| | 交易码 | 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());
}
步骤2:协议签约确认(310002)
接口信息
| 项目 | 值 | |------|-----| | 交易码 | 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());
}
步骤3:协议支付(310011)
接口信息
| 项目 | 值 | |------|-----| | 交易码 | 310011 | | 接口名称 | 协议支付 | | 业务模块 | FASTTRX | | 请求类型 | AgreementPayRequest | | 响应类型 | AgreementPayResponse |
必填参数
| 参数名 | 说明 | 示例 | |--------|------|------| | merchantId | 商户代码 | 由通联分配 | | agrmNo | 协议号 | 签约时返回的协议号 | | amount | 交易金额 | 单位:分(100元=10000分) | | businessCode | 业务代码 | 由通联分配,协议支付专用业务代码 | | submitTime | 提交时间 | 格式:yyyyMMddHHmmss(当前请求时间) | | accountName | 账号名(持卡人姓名) | 张三 |
业务代码说明:
- 业务代码由通联分配,不同业务类型使用不同的业务代码
- 协议支付使用协议支付专用业务代码(如:09100)
- 退款使用退款专用业务代码(如:09200)
- 不能混用:协议支付的业务代码不能用于退款请求
响应码处理(重要)
| 返回码 | 含义 | 处理方式 | |--------|------|----------| | 0000 | 处理成功 | 需进一步判断交易状态 | | 4000 | 已发送银行(默认成功) | 需进一步判断交易状态 | | 2000 | 系统处理数据中 | 必须发起交易查询 | | 2007 | 提交银行处理中 | 必须发起交易查询 | | 2008 | 交易返回结果超时 | 必须发起交易查询 | | 1108 | 批次号重复 | 必须发起交易查询 | | 1000 | 报文处理中 | 必须发起交易查询 | | 其他 | 失败 | 请求失败 |
明细返回码(DETAIL_RET_CODE)
协议支付响应中,通过 response.getDetailRetCode() 获取明细返回码,表示银行处理状态:
| 返回码 | 含义 | 处理方式 | |--------|------|----------| | 0000 | 交易成功 | 成功,无需查询 | | 4000 | 已发送银行(默认成功) | 成功,关注退票通知 | | 2000/2007/2008 | 处理中 | 继续查询 | | 3XXX | 银行返回错误 | 失败 |
重要说明:由于SDK内部实现差异,getDetailRetCode() 在某些情况下可能返回 null。
推荐使用以下方式判断交易状态:
- 推荐方式:使用
isTrxSuccess()和isTrxFailed()方法(SDK内部已正确解析FASTTRXRET) - 获取错误信息:使用
getErrMsg()方法获取FASTTRXRET中的错误信息 - 兜底方案:调用200004查询接口获取明确的交易状态码
HTTPS异常处理
重要: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)确认状态!");
}
SDK方法说明
| 方法 | 返回值 | 说明 | 推荐使用 |
|------|--------|------|----------|
| 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 | 银行流水号 | 交易成功时返回 |
步骤4:交易查询(200004)
接口信息
| 项目 | 值 | |------|-----| | 交易码 | 200004 | | 接口名称 | 交易结果查询 | | 业务模块 | QTRANSREQ | | 请求类型 | TrxQueryRequest | | 响应类型 | TrxQueryResponse |
必填参数
| 参数名 | 说明 | 示例 | |--------|------|------| | merchantId | 商户代码 | 由通联分配 | | querySn | 要查询的交易流水号 | 原交易的REQ_SN |
响应码处理(重要)
| 返回码 | 含义 | 处理方式 | |--------|------|----------| | 0000 | 查询成功 | 需进一步判断交易状态 | | 2000/2007/2008 | 系统处理中 | 继续发起交易查询 | | 1000 | 报文处理中 | 继续发起交易查询 | | 1002 | 无此交易 | 特殊处理:10分钟后查询,30分钟无记录判失败 | | 2002/2004/2006 | 失败 | 停止查询 | | 其他 | 失败 | 查询失败 |
SDK状态判断方法(推荐)
| 方法 | 返回值 | 说明 |
|------|--------|------|
| isTrxSuccess() | boolean | 查询成功且交易已成功(0000/4000) |
| isTrxFailed() | boolean | 查询失败或交易失败 |
| isTrxProcessing() | boolean | 查询成功但交易仍在处理中 |
| isNoTransaction() | boolean | 1002状态(无此交易记录) |
| getDetails() | List | 交易明细列表(含详细状态) |
推荐使用 SDK 提供的状态判断方法,避免手动解析返回码!
明细返回码(DETAIL.RET_CODE)
交易查询响应中,通过 response.getDetails().get(0).getRetCode() 获取明细返回码,表示交易状态:
| 返回码 | 含义 | 处理方式 | |--------|------|----------| | 0000 | 交易成功 | 成功,停止查询 | | 4000 | 已发送银行(默认成功) | 成功,关注退票通知 | | 2000/2007/2008 | 处理中 | 继续查询 | | 其他(含3XXX) | 交易失败 | 失败,停止查询 |
交易明细字段(QueryDetail)
| 字段 | 方法 | 说明 |
|------|------|------|
| 批次号 | getBatchId() | 交易批次号 |
| 序号 | getSn() | 记录序号 |
| 交易方向 | getTrxDir() | 0付1收 |
| 清算日期 | getSettDay() | 清算日期 |
| 完成时间 | getFinTime() | 完成时间 |
| 账号 | getAccountNo() | 账号 |
| 金额 | getAmount() | 交易金额(分) |
| 返回码 | getRetCode() | 交易状态码 |
| 错误文本 | getErrMsg() | 错误信息 |
1002特殊处理规则
| 时间 | 处理方式 | |------|----------| | 交易发起后10分钟内 | 不查询,等待 | | 10分钟后 | 开始查询 | | 30分钟后仍返回1002 | 停止查询,判定交易失败 |
代码示例
// 创建查询请求
TrxQueryRequest request = new TrxQueryRequest();
request.setMerchantId("您的商户号");
request.setQuerySn(reqSn); // 原交易的流水号
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() + " 分");
System.out.println("清算日期: " + detail.getSettDay());
System.out.println("银行流水: " + detail.getVoucherNo());
}
}
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)) {
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);
}
}
}
// 1002特殊处理
else if ("1002".equals(retCode)) {
System.out.println("无此交易,需在10分钟后查询,30分钟无记录判失败");
}
// 处理中
else if ("2000".equals(retCode) || "2007".equals(retCode) || "2008".equals(retCode)) {
System.out.println("系统处理中,继续查询");
}
步骤5:交易退款(REFUND)
接口信息
| 项目 | 值 | |------|-----| | 交易码 | REFUND | | 接口名称 | 退款 | | 业务模块 | REFUND | | 请求类型 | RefundRequest | | 响应类型 | RefundResponse |
必填参数
| 参数名 | 说明 | 示例 | |--------|------|------| | merchantId | 商户代码 | 由通联分配 | | businessCode | 业务代码 | 由通联分配,退款专用业务代码 | | orgBatchId | 原批次号 | 原交易的REQ_SN | | orgBatchSn | 原批次序号 | 单笔实时交易填0 | | amount | 退款金额 | 单位:分(不超过原交易金额) |
可选参数
| 参数名 | 说明 | 示例 | |--------|------|------| | submitTime | 提交时间 | 格式:yyyyMMddHHmmss(当前请求时间) | | ledgerBack | 分账回退标识 | 1:走分账回退逻辑 | | remark | 备注 | 用户申请退款 | | notifyUrl | 通知地址 | 退款结果通知地址 |
业务代码说明
| 业务代码 | 说明 | 使用场景 | |----------|------|----------| | 09200 | 商户退款 | 商户主动发起退款 | | 09201 | 收付通退款 | 通联平台发起退款 |
重要提示:
- 业务代码由通联分配,不同业务类型使用不同的业务代码
- 退款业务代码与协议支付业务代码是独立的,不能混用
- 使用错误的业务代码会导致请求失败(如"未开通该业务代码"错误)
SDK状态判断方法(推荐)
| 方法 | 返回值 | 说明 |
|------|--------|------|
| 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异常处理
重要: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());
}
步骤6:退款查询(200004)
与步骤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异常处理
HTTPS异常(读取超时、连接超时等)必须发起交易查询,不能直接判定失败!
这是通联收付通的核心规则,所有接口都需遵守。
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());
}
重要提示:
- HTTPS异常时交易可能已在通联处理中,不能直接判定失败
- 必须通过交易查询(200004)确认最终状态
- 建议在异常后设置定时轮询查询机制
完整流程示例
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);
System.out.println("交易成功,状态: " + detail.getRetCode());
System.out.println("交易金额: " + detail.getAmount() + " 分");
}
} 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()) {
System.out.println("退款成功");
if (refundResult.getDetails() != null && !refundResult.getDetails().isEmpty()) {
QueryDetail detail = refundResult.getDetails().get(0);
System.out.println("退款状态: " + detail.getRetCode());
}
} 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());
}
常见问题
Q1: 为什么0000和4000都是成功状态?
4000表示已发送银行但银行不能及时返回结果,默认成功。如果最终失败,银行会生成退票交易,商户需对接退票通知接口。
Q2: HTTPS超时怎么处理?
必须发起交易查询(200004)确认状态,不能直接判定失败。这是通联的核心规则。
Q3: 1002返回码是什么意思?
无此交易记录。需在交易发起10分钟后查询,30分钟仍无记录则判定交易失败。
Q4: 验证码有效期多久?
短信验证码有效期通常为5分钟。测试环境验证码固定为"111111"。
Q5: 协议号有什么用?
协议号(AGRMNO)是签约成功后的唯一标识,后续所有支付都需要使用协议号。必须妥善保存到数据库。
Q6: getDetailRetCode()返回null怎么办?
SDK在解析FASTTRXRET节点时可能未正确设置detailRetCode字段。推荐使用以下方法:
isTrxSuccess()- 判断交易是否成功isTrxFailed()- 判断交易是否失败getErrMsg()- 获取交易错误信息(FASTTRXRET中的ERR_MSG)
Q7: 退款返回3999是什么原因?
退款返回3999表示"其它错误",具体原因需要查看返回的错误信息(ERR_MSG)。常见原因包括:
- 原支付交易不存在或未成功
- 退款金额超过原交易金额
- 原交易已全额退款
- 请求参数有误
- 商户配置问题
建议:先通过200004查询确认原支付交易状态,并检查退款参数是否正确。
Q8: submitTime应该填什么时间?
submitTime应填写当前请求时间(发送请求的时间),格式为yyyyMMddHHmmss。这是用于防重和时间校验的必填参数。
Q9: 签名类型怎么选择?
签名类型必须与密钥格式匹配:
- SM2 - 使用国密SM2算法,需使用SM2格式的密钥文件
- RSA - 使用RSA算法,需使用RSA格式的密钥文件(如p12格式)
如果签名失败提示"can't identify EC private key",通常是因为签名类型与密钥不匹配。
Q10: 私钥文件密码怎么配置?
p12格式的私钥文件需要密码才能读取,必须通过setPrivateKeyPassword()配置:
config.setPrivateKeyPath("/path/to/private.p12");
config.setPrivateKeyPassword("您的私钥密码"); // p12文件必须有密码
Q11: 协议支付和退款的业务代码能混用吗?
不能混用。业务代码由通联分配,不同业务类型使用不同的业务代码:
- 协议支付:使用协议支付专用业务代码(如09100)
- 退款:使用退款专用业务代码(如09200)
使用错误的业务代码会导致请求失败,返回"未开通该业务代码"等错误。请确认商户开通的业务代码类型。
参考文档
- 返回码说明
- 官方文档:https://prodoc.allinpay.com/
参考示例文件
| 文件 | 说明 | 业务场景 | |------|------|----------| | AgreementSignSmsExample.java | 签约短信触发示例(310001) | 用户首次签约,发送验证码 | | AgreementSignExample.java | 签约确认示例(310002) | 验证验证码,获取协议号 | | AgreementPayExample.java | 协议支付示例(310011) | 使用协议号发起代扣 | | AgreementPayIntegrationExample.java | 完整流程示例 | 签约→支付→查询→退款全流程 | | TrxQueryExample.java | 交易查询示例(200004) | 查询支付/退款交易结果 | | RefundExample.java | 退款示例(REFUND) | 对已成功交易发起退款 |
每个示例文件包含详细的业务场景说明、前置条件、关键输出和注意事项。
Scan to contact