# 支付结果回调


## 基本信息

| 名称 | 描述 |
|-|-|
| HTTP URL | 开发者配置的回调地址（预下单时 payNotifyUrl 参数指定） |
| HTTP Method | POST |
| 回调类型（type） | payment |
| Content-Type | application/json |

## 接口说明

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

回调方向说明： 本接口为回调接口，由豆包平台主动调用开发者服务器，而非开发者调用平台。开发者需在自己的服务端实现一个 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 | 回调类型枚举，与接口一一对应 | payment |
| version | 是 | string | 固定值 "1.0"。回调版本，用于开发者识别回调参数的变更 | 1.0 |
| msg | 是 | string | 具体回调内容，JSON 字符串，解析后结构见下方 |  |

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

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

| 字段名 | 必填 | 类型 | 说明 | 示例 |
|-|-|-|-|-|
| app_id | 是 | string | 应用 ID |  |
| order_id | 是 | string | 交易订单号 |  |
| out_order_no | 是 | string | 开发者侧订单号，与 order_id 一一对应 |  |
| merchant_uid | 是 | string | 收款商户uid |  |
| pay_status | 是 | string | 支付结果状态，目前有一种状态："SUCCESS"（支付成功） | SUCCESS |
| total_amount | 是 | string | 订单总金额，单位：分 | 1000 |
| event_time | 是 | string | 支付结果事件发生时间，精度：毫秒 | 1692775192000 |
| pay_type | 是 | string | 支付类型枚举：ALI_PAY（支付宝）、WECHAT_PAY（微信）、BYTE_PAY（抖音支付） | BYTE_PAY |
| pay_order_no | 是 | string | 支付单号 |  |

## 请求示例

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

```json
{
    "type": "payment",
    "version": "1.0",
    "msg": "{\"app_id\":\"xxx\",\"out_order_no\":\"ext_order_123\",\"merchant_uid\":\"test_merchant_1\",\"order_id\":\"xxx\",\"pay_status\":\"SUCCESS\",\"total_amount\":\"1000\",\"pay_type\":\"BYTE_PAY\",\"pay_order_no\":\"xxx\",\"event_time\":\"1692775192000\"}"
}
```

## 开发者响应

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

重要： 正常返回时一定要保证 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"`
}

// PaymentMsg 支付回调 msg 解析结构
type PaymentMsg struct {
        AppID        string `json:"app_id"`
        OrderID      string `json:"order_id"`
        OutOrderNo   string `json:"out_order_no"`
        PayStatus    string `json:"pay_status"`
        TotalAmount  string `json:"total_amount"`
        EventTime    string `json:"event_time"`
        PayType      string `json:"pay_type"`
        PayOrderNo   string `json:"pay_order_no"`
}

// 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 paymentCallbackHandler(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 == "payment" {
                // 5. 解析 msg JSON 字符串
                var paymentMsg PaymentMsg
                if err := json.Unmarshal([]byte(callbackReq.Msg), &paymentMsg); err != nil {
                        log.Printf("解析 msg 字段失败: %v", err)
                        w.WriteHeader(http.StatusBadRequest)
                        json.NewEncoder(w).Encode(CallbackResponse{Code: 500000001, Msg: "ParamError"})
                        return
                }

                // 6. 处理业务逻辑（幂等处理）
                log.Printf("支付回调: OrderID=%s, OutOrderNo=%s, PayStatus=%s, Amount=%s分, PayType=%s, EventTime=%s",
                        paymentMsg.OrderID, paymentMsg.OutOrderNo, paymentMsg.PayStatus,
                        paymentMsg.TotalAmount, paymentMsg.PayType, paymentMsg.EventTime)

                // TODO: 根据 out_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/payment/callback", paymentCallbackHandler)
        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;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api/payment")
public class PaymentCallbackController {

    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 ("payment".equals(type)) {
                // 5. 解析 msg JSON 字符串
                JsonNode paymentMsg = objectMapper.readTree(msg);

                String appId = paymentMsg.get("app_id").asText();
                String orderId = paymentMsg.get("order_id").asText();
                String outOrderNo = paymentMsg.get("out_order_no").asText();
                String payStatus = paymentMsg.get("pay_status").asText();
                String totalAmount = paymentMsg.get("total_amount").asText();
                String eventTime = paymentMsg.get("event_time").asText();
                String payType = paymentMsg.get("pay_type").asText();
                String payOrderNo = paymentMsg.get("pay_order_no").asText();

                // 6. 处理业务逻辑（幂等处理）
                System.out.printf("支付回调: OrderID=%s, OutOrderNo=%s, PayStatus=%s, " +
                        "Amount=%s分, PayType=%s, EventTime=%s%n",
                        orderId, outOrderNo, payStatus, totalAmount, payType, eventTime);

                // TODO: 根据 outOrderNo 查询本地订单，判断是否已处理（幂等）
                // 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/payment/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 === 'payment') {
        try {
            // 5. 解析 msg JSON 字符串
            const paymentMsg = JSON.parse(msg);

            const {
                app_id: appId,
                order_id: orderId,
                out_order_no: outOrderNo,
                pay_status: payStatus,
                total_amount: totalAmount,
                event_time: eventTime,
                pay_type: payType,
                pay_order_no: payOrderNo
            } = paymentMsg;

            // 6. 处理业务逻辑（幂等处理）
            console.log(`支付回调: OrderID=${orderId}, OutOrderNo=${outOrderNo}, ` +
                `PayStatus=${payStatus}, Amount=${totalAmount}分, ` +
                `PayType=${payType}, EventTime=${eventTime}`);

            // TODO: 根据 outOrderNo 查询本地订单，判断是否已处理（幂等）
            // 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/payment/callback", methods=["POST"])
def payment_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 == "payment":
            # 5. 解析 msg JSON 字符串
            payment_msg = json.loads(msg)

            app_id = payment_msg.get("app_id")
            order_id = payment_msg.get("order_id")
            out_order_no = payment_msg.get("out_order_no")
            pay_status = payment_msg.get("pay_status")
            total_amount = payment_msg.get("total_amount")
            event_time = payment_msg.get("event_time")
            pay_type = payment_msg.get("pay_type")
            pay_order_no = payment_msg.get("pay_order_no")

            # 6. 处理业务逻辑（幂等处理）
            print(
                f"支付回调: OrderID={order_id}, OutOrderNo={out_order_no}, "
                f"PayStatus={pay_status}, Amount={total_amount}分, "
                f"PayType={pay_type}, EventTime={event_time}"
            )

            # TODO: 根据 out_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 类型。
