# 代扣支付结果回调


## 基本信息

| 名称 | 描述 |
|-|-|
| HTTP URL | 开发者配置的回调地址（创建协议支付订单时 pay_notify_url 参数指定） |
| HTTP Method | POST |
| 回调类型（type） | sign_pay_callback |
| Content-Type | application/json |

## 接口说明

协议支付（代扣）完成后，豆包平台会向开发者在创建协议支付订单时配置的回调地址（pay_notify_url）发送代扣支付结果通知。开发者收到通知后需验证签名并返回标准响应。

回调方向说明： 本接口为回调接口，由豆包平台主动调用开发者服务器，而非开发者调用平台。开发者需在自己的服务端实现一个 HTTP POST 接口来接收并处理该回调。

## 回调 Header

豆包平台在发送回调请求时，会在 HTTP Header 中携带以下字段：

| 字段名 | 类型 | 是否必填 | 字段说明 | 取值范例 |
|-|-|-|-|-|
| Content-Type | string | 是 | 固定值：application/json | application/json |
| X-DB-Logid | string | 是 | 开平统一日志 ID，当出现问题时可以提供此 ID 给研发人员协助定位问题 | 20230504163800ABA7347CE4F5 |
| X-DB-Authorization | string | 是 | 签名认证信息，开发者需验证签名确保请求来自豆包平台 |  |

## 回调 Body 参数

### 外层结构

| 字段名 | 必填 | 类型 | 说明 | 示例 |
|-|-|-|-|-|
| type | 是 | string | 回调类型枚举，与接口一一对应 | sign_pay_callback |
| version | 是 | string | 固定值 1.0。回调版本，用于开发者识别回调参数的变更 | 1.0 |
| msg | 是 | string | 具体回调内容，JSON 字符串，解析后结构见下方 |  |

### msg 字段解析（type = sign_pay_callback）

当 type 为 sign_pay_callback 时，msg 字段为 JSON 字符串，解析后包含以下字段：

| 字段名 | 必填 | 类型 | 说明 | 示例 |
|-|-|-|-|-|
| app_id | 是 | string | 应用 ID | tt1234567890\*\*\*\*\*\* |
| auth_order_id | 是 | string | 平台侧签约单号，标识该代扣订单对应的签约关系 | 80214322040407612\*\*\* |
| pay_order_id | 是 | string | 平台代扣单号 | 90214322040407612\*\*\* |
| out_pay_order_no | 是 | string | 商户代扣单号，与 pay_order_id 一一对应 | SIGNPAY2024010100\*\*\* |
| pay_status | 是 | string | 支付结果状态，枚举值：SUCCESS - 支付成功；FAIL - 支付失败 | SUCCESS |
| total_amount | 是 | string | 订单总金额，单位：分 | 1000 |
| event_time | 是 | string | 代扣支付事件发生时间，精度：毫秒 | 1692775192000 |
| merchant_uid | 是 | string | 商户用户标识 | MER_USER_001\*\*\* |
| pay_success_source | 否 | integer | 支付成功来源：1 - 用户主动支付成功；2 - 代扣协议扣款成功 |  |
| pay_type | 否 | string | 支付类型，枚举值：ALI_PAY - 支付宝；WECHAT_PAY - 微信；BYTE_PAY - 抖音支付。支付成功时返回 | BYTE_PAY |
| pay_order_no | 否 | string | 支付单号。支付成功时返回 | PAY20240101000\*\*\* |
| sign_pay_fail_code | 否 | string | 失败错误码。支付失败时返回 |  |
| sign_pay_fail_msg | 否 | string | 失败原因描述。支付失败时返回 |  |

## 请求示例

以下为豆包平台发送到开发者回调地址的完整请求 Body 示例：

### 代扣支付成功回调

```json
{
    "type": "sign_pay_callback",
    "version": "1.0",
    "msg": "{\"app_id\":\"tt1234567890******\",\"auth_order_id\":\"80214322040407612***\",\"pay_order_id\":\"90214322040407612***\",\"out_pay_order_no\":\"SIGNPAY2024010100***\",\"pay_status\":\"SUCCESS\",\"total_amount\":\"1000\",\"event_time\":\"1692775192000\",\"merchant_uid\":\"MER_USER_001***\",\"pay_type\":\"BYTE_PAY\",\"pay_order_no\":\"PAY20240101000***\"}"
}
```

### 代扣支付失败回调

```json
{
    "type": "sign_pay_callback",
    "version": "1.0",
    "msg": "{\"app_id\":\"tt1234567890******\",\"auth_order_id\":\"80214322040407612***\",\"pay_order_id\":\"90214322040407612***\",\"out_pay_order_no\":\"SIGNPAY2024010100***\",\"pay_status\":\"FAIL\",\"total_amount\":\"1000\",\"event_time\":\"1692776192000\",\"merchant_uid\":\"MER_USER_001***\",\"fail_code\":\"PAY_RISK_REJECT\",\"fail_msg\":\"用户支付风险校验未通过\"}"
}
```

## 开发者响应

开发者在收到回调并处理完成后，需返回以下标准响应。

重要： 正常返回时一定要保证 code 和 msg 按示例标准返回，不然都认为失败，将会重试。

### 响应参数

| 字段名 | 必填 | 类型 | 说明 | 示例 |
|-|-|-|-|-|
| code | 是 | int32 | 响应状态码，成功时为 0 | 0 |
| msg | 是 | string | 响应消息，成功时为 success | success |

### 响应示例

```json
{
    "code": 0,
    "msg": "success"
}
```

## 代码示例

以下示例展示开发者服务端如何接收并处理豆包平台发送的代扣支付结果回调，包括签名验证、Body 解析、业务处理及标准响应返回。

### Go

```go
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
)

// CallbackRequest 回调外层结构
type CallbackRequest struct {
    Type    string `json:"type"`
    Version string `json:"version"`
    Msg     string `json:"msg"`
}

// SignPayCallbackMsg 代扣支付回调 msg 解析结构
type SignPayCallbackMsg struct {
    AppID          string `json:"app_id"`
    AuthOrderID    string `json:"auth_order_id"`
    PayOrderID     string `json:"pay_order_id"`
    OutPayOrderNo  string `json:"out_pay_order_no"`
    PayStatus      string `json:"pay_status"`
    TotalAmount    string `json:"total_amount"`
    EventTime      string `json:"event_time"`
    MerchantUID    string `json:"merchant_uid"`
    PayType        string `json:"pay_type,omitempty"`
    PayOrderNo     string `json:"pay_order_no,omitempty"`
    FailCode       string `json:"fail_code,omitempty"`
    FailMsg        string `json:"fail_msg,omitempty"`
}

// CallbackResponse 开发者标准响应
type CallbackResponse struct {
    Code int32  `json:"code"`
    Msg  string `json:"msg"`
}

// verifySignature 验证 X-DB-Authorization 签名
func verifySignature(authorization string, body []byte, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expectedSig := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expectedSig), []byte(authorization))
}

func signPayCallbackHandler(w http.ResponseWriter, r *http.Request) {
    // 1. 读取请求 Body
    body, err := io.ReadAll(r.Body)
    if err != nil {
        log.Printf("读取请求 Body 失败: %v", err)
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
    defer r.Body.Close()

    // 2. 验证签名
    authorization := r.Header.Get("X-DB-Authorization")
    logID := r.Header.Get("X-DB-Logid")
    log.Printf("收到回调请求, LogID: %s", logID)

    appSecret := "your_app_secret" // 替换为实际的应用密钥
    if !verifySignature(authorization, body, appSecret) {
        log.Printf("签名验证失败, LogID: %s", logID)
        w.WriteHeader(http.StatusUnauthorized)
        json.NewEncoder(w).Encode(CallbackResponse{Code: 500000003, Msg: "VerifySignFailError"})
        return
    }

    // 3. 解析外层结构
    var callbackReq CallbackRequest
    if err := json.Unmarshal(body, &callbackReq); err != nil {
        log.Printf("解析回调请求失败: %v", err)
        w.WriteHeader(http.StatusBadRequest)
        json.NewEncoder(w).Encode(CallbackResponse{Code: 500000001, Msg: "ParamError"})
        return
    }

    // 4. 根据 type 处理不同回调
    if callbackReq.Type == "sign_pay_callback" {
        // 5. 解析 msg JSON 字符串
        var signPayMsg SignPayCallbackMsg
        if err := json.Unmarshal([]byte(callbackReq.Msg), &signPayMsg); err != nil {
            log.Printf("解析 msg 字段失败: %v", err)
            w.WriteHeader(http.StatusBadRequest)
            json.NewEncoder(w).Encode(CallbackResponse{Code: 500000001, Msg: "ParamError"})
            return
        }

        // 6. 处理业务逻辑（幂等处理）
        log.Printf("代扣支付回调: PayOrderID=%s, OutPayOrderNo=%s, PayStatus=%s, Amount=%s分, MerchantUID=%s, EventTime=%s",
            signPayMsg.PayOrderID, signPayMsg.OutPayOrderNo, signPayMsg.PayStatus,
            signPayMsg.TotalAmount, signPayMsg.MerchantUID, signPayMsg.EventTime)

        if signPayMsg.PayStatus == "SUCCESS" {
            log.Printf("代扣支付成功: PayType=%s, PayOrderNo=%s", signPayMsg.PayType, signPayMsg.PayOrderNo)
            // TODO: 根据 out_pay_order_no 查询本地代扣订单，判断是否已处理（幂等）
            // TODO: 更新订单状态为已支付
        } else if signPayMsg.PayStatus == "FAIL" {
            log.Printf("代扣支付失败: FailCode=%s, FailMsg=%s", signPayMsg.FailCode, signPayMsg.FailMsg)
            // TODO: 根据 out_pay_order_no 查询本地代扣订单，判断是否已处理（幂等）
            // TODO: 更新订单状态为支付失败，记录失败原因
        }
    }

    // 7. 返回标准成功响应
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(CallbackResponse{Code: 0, Msg: "success"})
}

func main() {
    http.HandleFunc("/api/sign_pay/callback", signPayCallbackHandler)
    fmt.Println("服务启动，监听端口 8080...")
    log.Fatal(http.ListenAndServe(":8080", nil))
}
```

### Java

```java
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/sign_pay")
public class SignPayCallbackController {

    private static final ObjectMapper objectMapper = new ObjectMapper();
    private static final String APP_SECRET = "your_app_secret"; // 替换为实际的应用密钥

    /**
     * 验证 X-DB-Authorization 签名
     */
    private boolean verifySignature(String authorization, String body, String secret) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKeySpec = new SecretKeySpec(
                secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
            mac.init(secretKeySpec);
            byte[] hash = mac.doFinal(body.getBytes(StandardCharsets.UTF_8));
            StringBuilder hexString = new StringBuilder();
            for (byte b : hash) {
                String hex = Integer.toHexString(0xff & b);
                if (hex.length() == 1) hexString.append('0');
                hexString.append(hex);
            }
            return hexString.toString().equals(authorization);
        } catch (Exception e) {
            return false;
        }
    }

    @PostMapping("/callback")
    public ResponseEntity<Map<String, Object>> handleCallback(
            HttpServletRequest request,
            @RequestBody String body) {

        Map<String, Object> response = new HashMap<>();

        // 1. 获取 Header 信息
        String authorization = request.getHeader("X-DB-Authorization");
        String logId = request.getHeader("X-DB-Logid");
        System.out.println("收到回调请求, LogID: " + logId);

        // 2. 验证签名
        if (!verifySignature(authorization, body, APP_SECRET)) {
            System.out.println("签名验证失败, LogID: " + logId);
            response.put("code", 500000003);
            response.put("msg", "VerifySignFailError");
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
        }

        try {
            // 3. 解析外层结构
            JsonNode callbackReq = objectMapper.readTree(body);
            String type = callbackReq.get("type").asText();
            String version = callbackReq.get("version").asText();
            String msg = callbackReq.get("msg").asText();

            // 4. 根据 type 处理不同回调
            if ("sign_pay_callback".equals(type)) {
                // 5. 解析 msg JSON 字符串
                JsonNode signPayMsg = objectMapper.readTree(msg);

                String appId = signPayMsg.get("app_id").asText();
                String authOrderId = signPayMsg.get("auth_order_id").asText();
                String payOrderId = signPayMsg.get("pay_order_id").asText();
                String outPayOrderNo = signPayMsg.get("out_pay_order_no").asText();
                String payStatus = signPayMsg.get("pay_status").asText();
                String totalAmount = signPayMsg.get("total_amount").asText();
                String eventTime = signPayMsg.get("event_time").asText();
                String merchantUid = signPayMsg.get("merchant_uid").asText();

                // 6. 处理业务逻辑（幂等处理）
                System.out.printf("代扣支付回调: PayOrderID=%s, OutPayOrderNo=%s, PayStatus=%s, " +
                        "Amount=%s分, MerchantUID=%s, EventTime=%s%n",
                        payOrderId, outPayOrderNo, payStatus, totalAmount, merchantUid, eventTime);

                if ("SUCCESS".equals(payStatus)) {
                    String payType = signPayMsg.has("pay_type") ? signPayMsg.get("pay_type").asText() : "";
                    String payOrderNo = signPayMsg.has("pay_order_no") ? signPayMsg.get("pay_order_no").asText() : "";
                    System.out.printf("代扣支付成功: PayType=%s, PayOrderNo=%s%n", payType, payOrderNo);
                    // TODO: 根据 outPayOrderNo 查询本地代扣订单，判断是否已处理（幂等）
                    // TODO: 更新订单状态为已支付
                } else if ("FAIL".equals(payStatus)) {
                    String failCode = signPayMsg.has("fail_code") ? signPayMsg.get("fail_code").asText() : "";
                    String failMsg = signPayMsg.has("fail_msg") ? signPayMsg.get("fail_msg").asText() : "";
                    System.out.printf("代扣支付失败: FailCode=%s, FailMsg=%s%n", failCode, failMsg);
                    // TODO: 根据 outPayOrderNo 查询本地代扣订单，判断是否已处理（幂等）
                    // TODO: 更新订单状态为支付失败，记录失败原因
                }
            }

            // 7. 返回标准成功响应
            response.put("code", 0);
            response.put("msg", "success");
            return ResponseEntity.ok(response);

        } catch (Exception e) {
            System.out.println("解析回调请求失败: " + e.getMessage());
            response.put("code", 500000001);
            response.put("msg", "ParamError");
            return ResponseEntity.badRequest().body(response);
        }
    }
}
```

### Node.js

```javascript
const express = require('express');
const crypto = require('crypto');

const app = express();
app.use(express.json({ type: 'application/json' }));

const APP_SECRET = 'your_app_secret'; // 替换为实际的应用密钥

/**
 * 验证 X-DB-Authorization 签名
 */
function verifySignature(authorization, body, secret) {
    const hmac = crypto.createHmac('sha256', secret);
    hmac.update(JSON.stringify(body));
    const expectedSig = hmac.digest('hex');
    return crypto.timingSafeEqual(
        Buffer.from(expectedSig),
        Buffer.from(authorization)
    );
}

/**
 * 代扣支付结果回调处理
 */
app.post('/api/sign_pay/callback', (req, res) => {
    // 1. 获取 Header 信息
    const authorization = req.headers['x-db-authorization'];
    const logId = req.headers['x-db-logid'];
    console.log(`收到回调请求, LogID: ${logId}`);

    // 2. 验证签名
    try {
        if (!verifySignature(authorization, req.body, APP_SECRET)) {
            console.log(`签名验证失败, LogID: ${logId}`);
            return res.status(401).json({
                code: 500000003,
                msg: 'VerifySignFailError'
            });
        }
    } catch (err) {
        console.log(`签名验证异常: ${err.message}`);
        return res.status(401).json({
            code: 500000003,
            msg: 'VerifySignFailError'
        });
    }

    // 3. 解析外层结构
    const { type, version, msg } = req.body;

    // 4. 根据 type 处理不同回调
    if (type === 'sign_pay_callback') {
        try {
            // 5. 解析 msg JSON 字符串
            const signPayMsg = JSON.parse(msg);

            const {
                app_id: appId,
                auth_order_id: authOrderId,
                pay_order_id: payOrderId,
                out_pay_order_no: outPayOrderNo,
                pay_status: payStatus,
                total_amount: totalAmount,
                event_time: eventTime,
                merchant_uid: merchantUid,
                pay_type: payType,
                pay_order_no: payOrderNo,
                fail_code: failCode,
                fail_msg: failMsg
            } = signPayMsg;

            // 6. 处理业务逻辑（幂等处理）
            console.log(`代扣支付回调: PayOrderID=${payOrderId}, OutPayOrderNo=${outPayOrderNo}, ` +
                `PayStatus=${payStatus}, Amount=${totalAmount}分, MerchantUID=${merchantUid}, EventTime=${eventTime}`);

            if (payStatus === 'SUCCESS') {
                console.log(`代扣支付成功: PayType=${payType}, PayOrderNo=${payOrderNo}`);
                // TODO: 根据 outPayOrderNo 查询本地代扣订单，判断是否已处理（幂等）
                // TODO: 更新订单状态为已支付
            } else if (payStatus === 'FAIL') {
                console.log(`代扣支付失败: FailCode=${failCode}, FailMsg=${failMsg}`);
                // TODO: 根据 outPayOrderNo 查询本地代扣订单，判断是否已处理（幂等）
                // TODO: 更新订单状态为支付失败，记录失败原因
            }

        } catch (err) {
            console.log(`解析 msg 字段失败: ${err.message}`);
            return res.status(400).json({
                code: 500000001,
                msg: 'ParamError'
            });
        }
    }

    // 7. 返回标准成功响应
    res.status(200).json({
        code: 0,
        msg: 'success'
    });
});

app.listen(8080, () => {
    console.log('服务启动，监听端口 8080...');
});
```

### Python

```python
import hashlib
import hmac
import json
from flask import Flask, request, jsonify

app = Flask(__name__)

APP_SECRET = 'your_app_secret'  # 替换为实际的应用密钥

def verify_signature(authorization: str, body: bytes, secret: str) -> bool:
    """验证 X-DB-Authorization 签名"""
    expected_sig = hmac.new(
        secret.encode('utf-8'),
        body,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected_sig, authorization)

@app.route('/api/sign_pay/callback', methods=['POST'])
def sign_pay_callback():
    # 1. 获取 Header 信息
    authorization = request.headers.get('X-DB-Authorization', '')
    log_id = request.headers.get('X-DB-Logid', '')
    print(f'收到回调请求, LogID: {log_id}')

    # 2. 验证签名
    raw_body = request.get_data()
    if not verify_signature(authorization, raw_body, APP_SECRET):
        print(f'签名验证失败, LogID: {log_id}')
        return jsonify({'code': 500000003, 'msg': 'VerifySignFailError'}), 401

    try:
        # 3. 解析外层结构
        callback_req = request.get_json()
        callback_type = callback_req.get('type')
        version = callback_req.get('version')
        msg = callback_req.get('msg')

        # 4. 根据 type 处理不同回调
        if callback_type == 'sign_pay_callback':
            # 5. 解析 msg JSON 字符串
            sign_pay_msg = json.loads(msg)

            app_id = sign_pay_msg.get('app_id')
            auth_order_id = sign_pay_msg.get('auth_order_id')
            pay_order_id = sign_pay_msg.get('pay_order_id')
            out_pay_order_no = sign_pay_msg.get('out_pay_order_no')
            pay_status = sign_pay_msg.get('pay_status')
            total_amount = sign_pay_msg.get('total_amount')
            event_time = sign_pay_msg.get('event_time')
            merchant_uid = sign_pay_msg.get('merchant_uid')
            pay_type = sign_pay_msg.get('pay_type')
            pay_order_no = sign_pay_msg.get('pay_order_no')
            fail_code = sign_pay_msg.get('fail_code')
            fail_msg = sign_pay_msg.get('fail_msg')

            # 6. 处理业务逻辑（幂等处理）
            print(
                f'代扣支付回调: PayOrderID={pay_order_id}, OutPayOrderNo={out_pay_order_no}, '
                f'PayStatus={pay_status}, Amount={total_amount}分, '
                f'MerchantUID={merchant_uid}, EventTime={event_time}'
            )

            if pay_status == 'SUCCESS':
                print(f'代扣支付成功: PayType={pay_type}, PayOrderNo={pay_order_no}')
                # TODO: 根据 out_pay_order_no 查询本地代扣订单，判断是否已处理（幂等）
                # TODO: 更新订单状态为已支付
            elif pay_status == 'FAIL':
                print(f'代扣支付失败: FailCode={fail_code}, FailMsg={fail_msg}')
                # TODO: 根据 out_pay_order_no 查询本地代扣订单，判断是否已处理（幂等）
                # TODO: 更新订单状态为支付失败，记录失败原因

    except (json.JSONDecodeError, KeyError) as e:
        print(f'解析回调请求失败: {e}')
        return jsonify({'code': 500000001, 'msg': 'ParamError'}), 400

    # 7. 返回标准成功响应
    return jsonify({'code': 0, 'msg': 'success'}), 200

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8080)
```

## 注意事项

幂等处理： 回调通知可能会多次发送，开发者需要做好幂等处理，避免重复业务处理。

签名验证： 开发者需验证 X-DB-Authorization 签名，确保请求来自豆包平台。

标准响应： 开发者必须在收到回调后返回 {"code": 0, "msg": "success"}，否则平台会认为回调失败并重试。

订单状态确认： 不可只依赖回调判断代扣支付状态，订单状态需以后端查询协议支付订单接口为主。

HTTPS 要求： 回调地址需为 HTTPS 类型。

失败处理： 当 pay_status 为 FAIL 时，fail_code 和 fail_msg 字段包含失败原因信息，开发者可据此进行错误排查或差异化处理（如提示用户重新签约、更换支付方式等）。
