返回 Skill 列表
extension
分类: 其它需要 API Key

通联支付收付产品skill

通联收付通协议支付完整流程技能,涵盖签约短信(310001)、签约确认(310002)、协议支付(310011)、交易查询(200004)、退款(REFUND)全流程

person作者: user_0512de0ahubcommunity

通联收付通协议支付完整流程技能

功能说明

本技能用于生成通联收付通协议支付全流程调用代码,覆盖从签约到退款的完整业务链路。

适用场景

  • 后端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。 推荐使用以下方式判断交易状态:

  1. 推荐方式:使用 isTrxSuccess()isTrxFailed() 方法(SDK内部已正确解析FASTTRXRET)
  2. 获取错误信息:使用 getErrMsg() 方法获取FASTTRXRET中的错误信息
  3. 兜底方案:调用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)

使用错误的业务代码会导致请求失败,返回"未开通该业务代码"等错误。请确认商户开通的业务代码类型。


参考文档


参考示例文件

| 文件 | 说明 | 业务场景 | |------|------|----------| | AgreementSignSmsExample.java | 签约短信触发示例(310001) | 用户首次签约,发送验证码 | | AgreementSignExample.java | 签约确认示例(310002) | 验证验证码,获取协议号 | | AgreementPayExample.java | 协议支付示例(310011) | 使用协议号发起代扣 | | AgreementPayIntegrationExample.java | 完整流程示例 | 签约→支付→查询→退款全流程 | | TrxQueryExample.java | 交易查询示例(200004) | 查询支付/退款交易结果 | | RefundExample.java | 退款示例(REFUND) | 对已成功交易发起退款 |

每个示例文件包含详细的业务场景说明、前置条件、关键输出和注意事项。