生活缴费中台 业务通知接口文档(物业 SaaS 对接版)

通知方: 银盛支付 生活缴费中台 被通知方: 物业 SaaS 协议: HTTP POST Content-Type: application/json; charset=utf-8 文档版本: V1.1 更新日期: 2026-10-26


1. 概述

生活缴费中台在以下四类业务节点向物业 SaaS 发起业务通知。银盛主动调用物业 SaaS 提供的通知地址,SaaS 需同步返回处理结果。

序号 通知类型 触发时机 通知地址配置项
1 主动支付核销通知 用户在微信侧完成主动缴费、中台完成销账后 账单核销通知URL
2 代扣结果通知 自动缴费(代扣)扣费完成后 代扣结果通知URL
3 代扣签约通知 用户代扣签约成功后 代扣签约URL
4 代扣解约通知 用户代扣解约成功后 解约签约URL

2. 接入准备

2.1 物业 SaaS 需提供的信息

请物业 SaaS 在联调前提供以下信息,由银盛配置:

配置项 说明 示例
主动支付核销通知 接收主动支付核销通知的地址 https://saas.example.com/notify/writeoff
代扣结果通知URL 接收代扣扣费结果通知的地址 https://saas.example.com/notify/deduct
代扣签约URL 接收代扣签约结果通知的地址 https://saas.example.com/notify/sign
解约签约URL 接收代扣解约结果通知的地址 https://saas.example.com/notify/unsign
发起方商户号 银盛分配给该物业的商户号,决定加签方式与验签公钥 826551253990363
商户公钥 用于银盛验证 SaaS 响应报文签名 RSA / SM2 公钥

注意

  1. 四个 URL 必须是 HTTPS,且可从银盛生产网络访问。
  2. 四个 URL 可复用同一地址,通过报文内容区分业务类型;建议按业务分设便于运维。
  3. URL 变更需提前联系银盛运营配置,配置生效后旧地址立即失效。

3. 通知报文格式

银盛发给物业 SaaS 的 HTTP 请求 Body 为如下 JSON:

{
  "bizContent": "{\"merchantNo\":\"826551253990363\",\"billKey\":\"BK2026100001\"}",
  "charset": "UTF-8",
  "reqId": "NTF20261026101530000001",
  "src": "life",
  "timeStamp": "2026-10-26 10:15:30",
  "sign": "MEUCIQDx...Base64编码的平台签名..."
}
字段 类型 说明
bizContent String 业务通知内容,JSON 字符串(四类通知的具体字段见第 4 章)
charset String 编码,固定 UTF-8
reqId String 本次通知请求ID,全局唯一,可用于幂等与问题追踪
src String 通知来源,暂定固定值 life(生活缴费),后续如有调整另行通知
timeStamp String 通知时间戳,格式 yyyy-MM-dd HH:mm:ss
sign String 平台签名(Base64),SaaS 必须验签,规则见 3.1

⚠️ bizContent 是字符串,不是 JSON 对象。解析时需先取出字符串,再二次反序列化为业务对象。

3.1 通知验签规则(SaaS 验银盛的签名)

项 规则
验签原文 按字段名字典序拼接 key=value&key=value...
参与字段 bizContent、charset、reqId、src、timeStamp(排除 sign 字段,跳过 null 值)
拼接示例 bizContent={...}&charset=UTF-8&reqId=NTF...&src=life&timeStamp=2026-10-26 10:15:30
编码 UTF-8
签名算法 发起方商户号配置的类型:SHA256withRSA 或 SM3withSM2
验签公钥 平台公钥(银盛提供,RSA 为 X.509 证书,SM2 为 .cer 证书)
签名值编码 Base64

验签失败应直接返回 HTTP 4xx,银盛会按通知失败处理并重试。


4. 通知业务内容(bizContent)

4.1 主动支付核销通知

用户在微信生活缴费平台完成主动缴费支付后,微信通知银盛生活中台核销账单,银盛通物业 SaaS 进行账单核销。

4.1.1 请求字段

名称 类型 必填 说明
merchantNo String 是 商户号(微信生活缴费商户号,对应物业公司)
companyId String 是 项目id(微信生活缴费项目id)
initSideMerchantNo String 是 发起方商户号
billKey String 是 户号(房屋编号)
orderNo String 是 银盛支付流水号
outTradeNo String 是 商户订单号
transactionId String 是 微信支付订单号
payDate String 是 缴费单支付日期,格式 yyyyMMddHHmmss
customerName String 否 客户姓名
payAmount Long 是 缴费金额,单位:分
userId String 否 用户ID
feeItems Array 是 费用主项列表,见下表

feeItems 元素结构

名称 类型 必填 说明
feeCycle String 是 费用周期,格式 yyyy.MM.dd-yyyy.MM.dd
feeCode String 是 费用编号,物业公司业务系统定义的唯一编码
feeName String 是 费用名称
feeAmount Long 是 费用金额,单位:分

4.1.2 bizContent 示例

{
  "merchantNo": "826551253990363",
  "companyId": "wx_project_001",
  "initSideMerchantNo": "826551253990363",
  "billKey": "BK2026100001",
  "orderNo": "P2026102600000001",
  "outTradeNo": "WXLP7f3a9c2e1b4d",
  "transactionId": "4200002026102612345678901",
  "payDate": "20261026101530",
  "customerName": "张三",
  "payAmount": 12000,
  "userId": "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o",
  "feeItems": [
    {
      "feeCycle": "2026.01.01-2026.12.31",
      "feeCode": "FEE001",
      "feeName": "物业管理费",
      "feeAmount": 12000
    }
  ]
}

4.1.3 响应业务字段(SaaS → 银盛)

SaaS 在响应信封的 businessData 中返回以下内容:

名称 类型 必填 说明
retCode String 是 核销结果码,见下表
errMsg String 否 错误描述

retCode 取值约定(非常重要,直接决定后续资金动作)

取值 含义 银盛后续动作
SUCCESS 核销成功 向微信回传销账成功
DUPLICATE 重复核销(该账单已核销过) 按成功处理,向微信回传销账成功
FAIL 业务失败(如参数不匹配、账单状态异常) 回传销账成功并进入重试(差错补偿)
ERROR 用户已完成缴费(该账单此前已被他人缴清) 触发退款:向微信发起退款申请。返回此应答码,银盛生活缴费平台会自动出发退款

⚠️ ERROR 与 FAIL 语义完全不同,请勿混用。

  • 若账单此前已缴费,必须返回 ERROR,银盛会为用户退款。
  • 若仅是暂时性处理失败(如数据库繁忙、下游超时),返回 FAIL,银盛会重试。
  • 返回未知码值时,银盛按「核销处理中」处理,不视为成功。

4.1.4 businessData 示例

{
  "retCode": "SUCCESS",
  "errMsg": ""
}

4.2 代扣结果通知

自动缴费(代扣)扣费完成后通知物业 SaaS。

4.2.1 请求字段

名称 类型 必填 说明
signRecordNo String 是 签约记录号(银盛内部签约流水号)
merchantNo String 是 商户号
billKey String 是 房屋编号(户号)
deductStatus String 是 扣费结果:SUCCESS / FAIL
payAmount Long 是 扣费金额,单位:分
payDate String 否 扣费完成时间,格式 yyyyMMddHHmmss(失败场景可能为空)
orderNo String 否 银盛支付流水号(失败场景可能为空)
failReason String 否 失败原因(deductStatus=FAIL 时有值)
feeCode String 是 费用编号
outRequestNo String 否 商户订单号(银盛条码侧订单号)

channelBillNo 为历史遗留字段,已废弃,不再下发,请勿依赖。

4.2.2 bizContent 示例(扣费成功)

{
  "signRecordNo": "SR7f3a9c2e1b4d5f6a",
  "merchantNo": "826551253990363",
  "billKey": "BK2026100001",
  "deductStatus": "SUCCESS",
  "payAmount": 12000,
  "payDate": "20261026093015",
  "orderNo": "P2026102600000001",
  "failReason": null,
  "feeCode": "FEE001",
  "outRequestNo": "LP20261026093015001"
}

4.2.3 bizContent 示例(扣费失败)

{
  "signRecordNo": "SR7f3a9c2e1b4d5f6a",
  "merchantNo": "826551253990363",
  "billKey": "BK2026100001",
  "deductStatus": "FAIL",
  "payAmount": 12000,
  "payDate": null,
  "orderNo": null,
  "failReason": "余额不足",
  "feeCode": "FEE001",
  "outRequestNo": "LP20261026093015001"
}

4.2.4 响应业务字段(SaaS → 银盛)

名称 类型 必填 说明
retCode String 是 SUCCESS / FAIL
errMsg String 否 错误描述
取值 含义 银盛后续动作
SUCCESS SaaS 处理成功 通知流程结束
FAIL SaaS 处理失败 落差错任务重试(按退避策略自动重试,超上限转人工)

4.2.5 businessData 示例

{
  "retCode": "SUCCESS",
  "errMsg": ""
}

4.3 申请代扣签约通知

用户在微信生活缴费平台完成签约后,微信生活缴费平台通知银盛生活缴费中台,银盛调用物业SaaS平台的申请签约通知接口,物业SaaS平台响应签约结果

4.3.1 请求字段

名称 类型 必填 说明
signRecordNo String 是 签约记录号(银盛内部签约流水号)
merchantNo String 是 商户号(物业公司)
billKey String 是 房屋编号(户号)
changeType String 是 变更类型:ADD=签约 / DELETE=解约
openid String 是 用户 openid
accountType String 否 账户类型: postpaid(后付费)
deductDate String 否 扣费日(每月固定扣费日期),物业缴费场景默认为空
rechargeLimit Long 否 充值/扣费上限,单位:分,物业缴费场景默认为空
rechargeAmount Long 否 单次充值/扣费金额,单位:分,物业缴费场景默认为空
mobile String 否 用户手机号(明文,已解密)
idCard String 否 用户身份证号(明文,已解密)
userName String 否 用户姓名(明文,已解密)
cancelType String 否 解约类型(changeType=DELETE 时有值)
extend1 String 否 扩展字段 1
extend2 String 否 扩展字段 2

⚠️ 敏感信息提示:mobile / idCard / userName 为明文传输。物业 SaaS 收到后须按个人信息保护要求加密存储,不得落明文日志。

4.3.2 bizContent 示例

{
  "signRecordNo": "SR7f3a9c2e1b4d5f6a",
  "merchantNo": "826551253990363",
  "billKey": "BK2026100001",
  "changeType": "ADD",
  "openid": "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o",
  "accountType": "postpaid",
  "deductDate": "10",
  "rechargeLimit": 500000,
  "rechargeAmount": 12000,
  "mobile": "13800138000",
  "idCard": "440101199001011234",
  "userName": "张三",
  "cancelType": null,
  "extend1": null,
  "extend2": null
}

4.3.3 响应业务字段(SaaS → 银盛)

名称 类型 必填 说明
retCode String 是 SUCCESS / FAIL
errMsg String 否 错误描述
取值 含义
SUCCESS SaaS 处理成功,用户签约或解约成功
FAIL SaaS 处理失败,用户签约或解约失败

4.3.4 businessData 示例

{
  "retCode": "SUCCESS",
  "errMsg": ""
}

4.4 申请代扣解约通知

报文结构与「申请代扣签约通知」完全一致,仅以下差异:

差异项 取值
通知地址 使用「解约签约URL」
changeType 固定为 DELETE
cancelType 携带解约类型

4.4.1 bizContent 示例

{
  "signRecordNo": "SR7f3a9c2e1b4d5f6a",
  "merchantNo": "826551253990363",
  "billKey": "BK2026100001",
  "changeType": "DELETE",
  "openid": "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o",
  "accountType": "postpaid",
  "deductDate": "10",
  "rechargeLimit": 500000,
  "rechargeAmount": 12000,
  "mobile": "13800138000",
  "idCard": "440101199001011234",
  "userName": "张三",
  "cancelType": "01",
  "extend1": null,
  "extend2": null
}

4.4.2 响应业务字段

与 4.3.3 / 4.3.4 完全一致。


5. 响应规范(SaaS → 银盛)

5.1 响应结构

物业 SaaS 必须在 HTTP 200 响应体中返回如下 JSON 信封:

{
  "businessData": "{\"retCode\":\"SUCCESS\",\"errMsg\":\"\"}",
  "sign": "MEUCIQDx...Base64编码的商户签名..."
}
字段 类型 必填 说明
businessData String 是 业务响应内容,JSON 序列化后的字符串
sign String 是 对 businessData 的签名,规则见 5.2

⚠️ businessData 必须是字符串(内部 JSON 需转义),不能直接返回 JSON 对象。这是最常见的联调失败原因。

5.2 响应签名规则(SaaS 加签)

项 规则
签名原文 按字典序拼接 businessData=<值>(含 businessData= 前缀,值原样拼接,不加引号)
签名算法 与发起方商户号配置一致:SHA256withRSA 或 SM3withSM2
签名私钥 物业 SaaS 自己的商户私钥
签名值编码 Base64
验签方 银盛用物业 SaaS 提供的商户公钥验签,不通过则按通知失败重试

6. 示例代码(Java)

以下为物业 SaaS 侧参考实现,覆盖:接收通知 → 验平台签名 → 解析业务内容 → 组装响应 → 商户加签返回。

6.1 通知报文对象

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 网关业务通知请求报文
 */
public class NotifyPayloadDTO {

    /** 业务参数(JSON 字符串,由平台序列化) */
    private String bizContent;
    /** 编码,固定 UTF-8 */
    private String charset;
    /** 通知请求ID */
    private String reqId;
    /** 通知来源,固定 life */
    private String src;
    /** 通知时间戳,格式 yyyy-MM-dd HH:mm:ss */
    private String timeStamp;
    /** 平台签名(Base64) */
    private String sign;

    /** 构建验签数据 Map(不含 sign,与平台约定一致) */
    public Map<String, Object> toSignMap() {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("bizContent", bizContent);
        map.put("charset", charset);
        map.put("reqId", reqId);
        map.put("src", src);
        map.put("timeStamp", timeStamp);
        return map;
    }

    // getter / setter 省略
}

6.2 响应报文对象

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 被通知方响应报文
 */
public class NotifyResponseDTO {

    /** 业务参数(自行序列化的不透明字符串) */
    private String businessData;
    /** 商户对响应报文的签名(Base64) */
    private String sign;

    /** 构建验签数据 Map(不含 sign,与平台约定一致) */
    public Map<String, Object> toSignMap() {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("businessData", businessData);
        return map;
    }

    // getter / setter 省略
}

6.3 加验签原文构建工具

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
 * 加验签原文构建工具
 * 与平台约定一致:按 key 字典序拼接 key1=value1&key2=value2...,
 * 跳过 sign 字段和 null 值,UTF-8 编码。
 */
public class SignUtils {

    public static byte[] getSignDataBytes(Map<String, ?> map) {
        return getSignDataStr(map).getBytes(StandardCharsets.UTF_8);
    }

    private static String getSignDataStr(Map<String, ?> map) {
        List<String> keys = new ArrayList<>(map.keySet());
        Collections.sort(keys);
        StringBuilder sb = new StringBuilder();
        for (String key : keys) {
            if (map.get(key) == null || "sign".equals(key)) {
                continue;
            }
            sb.append(key).append("=").append(map.get(key)).append("&");
        }
        if (sb.length() > 0) {
            sb.setLength(sb.length() - 1);
        }
        return sb.toString();
    }
}

6.4 商户加验签服务

import cfca.sadk.algorithm.common.Mechanism;
import cfca.sadk.algorithm.sm2.SM2PublicKey;
import cfca.sadk.lib.crypto.JCrypto;
import cfca.sadk.lib.crypto.Session;
import cfca.sadk.util.KeyUtil;
import cfca.sadk.util.Signature;
import cfca.sadk.x509.certificate.X509Cert;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import javax.annotation.PostConstruct;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Security;
import java.security.cert.CertificateFactory;
import java.util.Base64;

/**
 * 商户侧加验签服务
 * <ul>
 *   <li>SM2:CFCA p1SignMessage / p1VerifyMessage(Mechanism.SM3_SM2),Base64-DER</li>
 *   <li>RSA:SHA256withRSA,Base64</li>
 * </ul>
 */
public class MercSignService {

    private PrivateKey sm2PrivateKey;          // 商户 SM2 私钥(.sm2 文件)
    private SM2PublicKey platformSm2PublicKey; // 平台 SM2 公钥证书(验平台通知)
    private PrivateKey rsaPrivateKey;          // 商户 RSA 私钥(PKCS12 .pfx)
    private PublicKey platformRsaPublicKey;    // 平台 RSA 公钥证书(验平台通知)

    @PostConstruct
    public void init() throws Exception {
        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
            Security.addProvider(new BouncyCastleProvider());
        }
        // 商户 SM2 私钥
        this.sm2PrivateKey = KeyUtil.getPrivateKeyFromSM2(readResource("cert/商户私钥.sm2"), "私钥密码");
        // 平台 SM2 公钥证书
        this.platformSm2PublicKey = (SM2PublicKey) new X509Cert(readResource("cert/平台sm2.cer")).getPublicKey();

        // 商户 RSA 私钥(PKCS12)
        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        try (InputStream is = openResource("cert/商户私钥.pfx")) {
            keyStore.load(is, "私钥密码".toCharArray());
        }
        String alias = keyStore.aliases().nextElement();
        this.rsaPrivateKey = (PrivateKey) keyStore.getKey(alias, "私钥密码".toCharArray());

        // 平台 RSA 公钥证书
        this.platformRsaPublicKey = CertificateFactory.getInstance("X.509")
                .generateCertificate(openResource("cert/平台rsa.cer")).getPublicKey();
    }

    /** SM2 加签(SM3withSM2),返回 Base64-DER */
    public String signSm2(byte[] sourceData) throws Exception {
        Session session = JCrypto.getInstance().openSession(JCrypto.JSOFT_LIB);
        Signature sig = new Signature(true);
        byte[] signed = sig.p1SignMessage(Mechanism.SM3_SM2, sourceData, sm2PrivateKey, session);
        // p1SignMessage 返回 Base64-DER,trim 去掉可能的空格填充
        return new String(signed, StandardCharsets.UTF_8).trim();
    }

    /** SM2 验签(平台公钥,SM3withSM2) */
    public boolean verifySm2(byte[] sourceData, String sign) throws Exception {
        Session session = JCrypto.getInstance().openSession(JCrypto.JSOFT_LIB);
        Signature sig = new Signature(true);
        return sig.p1VerifyMessage(Mechanism.SM3_SM2, sourceData,
                Base64.getDecoder().decode(sign), platformSm2PublicKey, session);
    }

    /** RSA 加签(SHA256withRSA),返回 Base64 */
    public String signRsa(byte[] sourceData) throws Exception {
        java.security.Signature sig = java.security.Signature.getInstance("SHA256withRSA");
        sig.initSign(rsaPrivateKey);
        sig.update(sourceData);
        return Base64.getEncoder().encodeToString(sig.sign());
    }

    /** RSA 验签(平台公钥,SHA256withRSA) */
    public boolean verifyRsa(byte[] sourceData, String sign) throws Exception {
        java.security.Signature sig = java.security.Signature.getInstance("SHA256withRSA");
        sig.initVerify(platformRsaPublicKey);
        sig.update(sourceData);
        return sig.verify(Base64.getDecoder().decode(sign));
    }

    private InputStream openResource(String path) {
        // 实际实现按 classpath / 文件系统读取
        return getClass().getClassLoader().getResourceAsStream(path);
    }

    private byte[] readResource(String path) throws Exception {
        try (InputStream is = openResource(path);
             java.io.ByteArrayOutputStream bos = new java.io.ByteArrayOutputStream()) {
            byte[] buf = new byte[4096];
            int n;
            while ((n = is.read(buf)) != -1) {
                bos.write(buf, 0, n);
            }
            return bos.toByteArray();
        }
    }
}

依赖:cfca-sadk(SM2 加验签)、bcprov-jdk15on(BouncyCastle)、JDK 自带 SHA256withRSA。

6.5 通知接收端点(完整实现)

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class NotifyReceiverController {

    private static final Logger log = LoggerFactory.getLogger(NotifyReceiverController.class);

    private static final String SIGN_TYPE_SM = "SM";
    private static final String SIGN_TYPE_RSA = "RSA";

    private final MercSignService mercSignService;

    public NotifyReceiverController(MercSignService mercSignService) {
        this.mercSignService = mercSignService;
    }

    /**
     * 接收业务通知
     * 生产建议按业务分设端点:/notify/writeoff、/notify/deduct、/notify/sign、/notify/unsign
     */
    @PostMapping("/notify/lifepay")
    public ResponseEntity<?> notify(@RequestBody NotifyPayloadDTO payload) throws Exception {
        log.info("<<< 收到通知: reqId={}, src={}, timeStamp={}",
                payload.getReqId(), payload.getSrc(), payload.getTimeStamp());

        // 1. 验平台签名(必须)
        String signType = SIGN_TYPE_SM; // 按商户配置的签名类型传入 SM 或 RSA
        if (!verifyPlatformSign(payload.toSignMap(), payload.getSign(), signType)) {
            log.error("平台通知验签不通过: reqId={}, payload={}", payload.getReqId(), payload);
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("平台通知验签不通过");
        }

        // 2. 解析业务内容
        String bizContent = payload.getBizContent();
        if (bizContent == null || bizContent.isEmpty()) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("bizContent 为空");
        }

        // 3. 业务处理(示例直接回显,真实场景替换为处理结果)
        //    核销场景应返回 {"retCode":"SUCCESS","errMsg":""}
        //    代扣/签约场景应返回 {"retCode":"SUCCESS","errMsg":""}
        String businessData = bizContent;

        // 4. 组装响应信封,用商户私钥对 businessData 加签
        NotifyResponseDTO response = new NotifyResponseDTO();
        response.setBusinessData(businessData);
        byte[] sourceData = SignUtils.getSignDataBytes(response.toSignMap());
        response.setSign(mercSignService.signSm2(sourceData));

        log.info(">>> 响应: reqId={}, response={}", payload.getReqId(), response);
        return ResponseEntity.ok(response);
    }

    private boolean verifyPlatformSign(Map<String, ?> signSource, String sign, String signType) {
        if (sign == null || sign.isEmpty()) {
            return false;
        }
        try {
            byte[] sourceData = SignUtils.getSignDataBytes(signSource);
            return SIGN_TYPE_SM.equals(signType)
                    ? mercSignService.verifySm2(sourceData, sign)
                    : mercSignService.verifyRsa(sourceData, sign);
        } catch (Exception e) {
            // 签名无法解码或格式非法同样视为验签不通过
            log.warn("平台通知验签异常(按不通过处理): signType={}, reason={}", signType, e.getMessage());
            return false;
        }
    }
}

6.6 关键点说明

要点 说明
toSignMap() 顺序无关 SignUtils 内部按 key 字典序重排,Map 放入顺序不影响结果
sign 不参与签名 SignUtils 自动跳过 sign 字段
null 值跳过 字段为 null 不参与拼接
RSA 与 SM2 互斥 实际用哪种取决于发起方商户号配置的签名类型,不要两种都试
SM2 签名值 CFCA p1SignMessage 返回 Base64-DER,需 trim() 去掉空格填充
时间戳格式 timeStamp 为 yyyy-MM-dd HH:mm:ss(含空格),参与签名时保持原样

7. 错误码

SaaS 响应不合法会导致通知失败,银盛按失败处理并重试。常见原因与责任划分:

现象 可能原因 责任方
请求未到达 SaaS URL 不可访问、网络不通、防火墙拦截 SaaS
通知发送超时 SaaS 响应超过约定的超时时间(待定) SaaS
响应报文解析失败 响应体不是合法 JSON 信封;businessData 写成了对象而非字符串 SaaS
响应验签不通过 签名原文、算法、私钥与提供公钥不匹配 SaaS
响应验签异常 签名格式非法(未 Base64、乱码) SaaS
通知处理异常 SaaS 服务内部异常(5xx) SaaS
发起方商户不存在或未配置签名类型 / 商户证书不存在 / 通知加签失败 银盛侧配置缺失 银盛(联系运营)

7.1 超时与重试约定

项 约定
超时时间 待定(联调前由银盛确认后补充)
重试机制 核销、代扣结果、签约通知失败后,银盛落差错任务,由定时任务按退避策略自动重试
重试上限 默认 5 次(可配置),超过后转人工介入
幂等要求 SaaS 必须实现幂等。同一业务主键重复通知时,须返回与首次一致的 retCode,不得重复记账

判重键建议

通知类型 判重键
主动支付核销 orderNo + billKey
代扣结果 outRequestNo(或 signRecordNo + payDate)
代扣签约 / 解约 signRecordNo
通用 reqId

8. 联调检查清单

  • [ ] 四个通知 URL 已提供并配置到银盛
  • [ ] 发起方商户号已配置,且已配置签名类型(RSA / SM2)
  • [ ] 平台公钥已获取(验平台通知签名用)
  • [ ] 商户公钥已提供给银盛(验 SaaS 响应签名用)
  • [ ] 通知接收接口能正确解析 bizContent,四类通知字段均能取到
  • [ ] 已实现平台通知验签,验签失败返回 4xx
  • [ ] 响应体为 {"businessData":"...","sign":"..."} 格式,businessData 是字符串
  • [ ] 响应签名原文为 businessData=<值>,算法与配置一致,Base64 编码
  • [ ] 响应耗时在约定超时时间内(超时时间待定)
  • [ ] 已实现幂等(按业务主键或 reqId 去重)
  • [ ] 核销通知的 retCode 四值(SUCCESS / DUPLICATE / FAIL / ERROR)语义正确,尤其 ERROR 触发退款
  • [ ] 敏感字段(手机号/身份证/姓名)已加密存储,未落明文日志

9. 常见问题

Q1:为什么我的响应总是报「响应报文解析失败」? A:最常见是把 businessData 直接写成了 JSON 对象。必须是 JSON 字符串:

// ❌ 错误
{ "businessData": {"retCode":"SUCCESS"}, "sign":"..." }

// ✅ 正确
{ "businessData": "{\"retCode\":\"SUCCESS\"}", "sign":"..." }

Q2:验签总是不通过? A:逐项核对:

  1. 验平台通知时,原文是否为 bizContent=...&charset=UTF-8&reqId=...&src=life&timeStamp=...(字典序、& 分隔、不含 sign);
  2. 对响应加签时,原文是否为 businessData= + 值(连等号一起签);
  3. 算法是 SHA256withRSA 还是 SM3withSM2,是否与商户配置一致;
  4. 私钥与提供给银盛的公钥是否同一对;
  5. 签名结果是否做了 Base64。

Q3:核销通知返回 ERROR 后发生了什么? A:银盛会立即向微信发起退款申请,把用户缴的钱退回。因此只有确认该账单此前已被缴清时才可返回 ERROR,否则会造成错误退款。

Q4:同一笔账单收到多次核销通知怎么办? A:说明前次通知可能超时或被判定失败。SaaS 应按 orderNo + billKey 判重,已处理过的直接返回 DUPLICATE。

Q5:代扣结果通知里 orderNo 为空是正常的吗? A:正常。扣费失败场景下银盛可能未生成支付流水号,此时 deductStatus=FAIL、orderNo 为空、failReason 有值。

Q6:bizContent 需要 URL 解码吗? A:不需要。请求体是 JSON,bizContent 是标准 JSON 字符串,HTTP 层不会额外编码。


附录 A:枚举值汇总

核销结果码 retCode

值 含义 银盛动作
SUCCESS 核销成功 回传微信成功
DUPLICATE 重复核销 回传微信成功
FAIL 业务失败 回传成功 + 重试
ERROR 用户已完成缴费,自动执行退款请求 发起退款

代扣/签约响应码 retCode

值 含义 银盛动作
SUCCESS 处理成功 流程结束
FAIL 处理失败 落差错重试

代扣状态 deductStatus

值 含义
SUCCESS 扣费成功
FAIL 扣费失败

变更类型 changeType

值 含义
ADD 签约
DELETE 解约

签名类型(发起方商户号配置)

值 算法
RSA SHA256withRSA
SM SM3withSM2

results matching ""

    No results matching ""