# 退款结果回调


## 基本信息

| 名称 | 描述 |
|-|-|
| HTTP URL | 开发者配置的回调地址（退款申请时 notify_url 参数指定） |
| HTTP Method | POST |
| 回调类型（type） | refund |
| Content-Type | application/json |

## 接口说明

退款处理完成后，豆包平台会向开发者在退款申请时配置的回调地址（notify_url）发送退款结果通知。开发者收到通知后需验证签名并返回标准响应。

注意：该接口为回调接口，由豆包平台主动调用开发者服务器，与开发者主动调用平台的 OpenAPI 接口不同。

## 回调 Header

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

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

## 回调 Body 参数

### 外层结构

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

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

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

| 字段名 | 必填 | 类型 | 说明 | 示例 |
|-|-|-|-|-|
| app_id | 是 | string | 应用 ID |  |
| order_id | 是 | string | 交易订单号 |  |
| refund_id | 是 | string | 平台侧退款单号 |  |
| out_refund_no | 是 | string | 开发者退款单号 | 530402398023 |
| refund_status | 是 | string | 退款状态枚举：SUCCESS（退款成功）、FAIL（退款失败） | SUCCESS |
| refund_total_amount | 是 | string | 退款总金额，单位：分 | 100 |
| event_time | 是 | string | 退款结果事件发生时间，精度：毫秒 | 1643185934447 |

## 请求示例

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

```json
{
    "type": "refund",
    "version": "1.0",
    "msg": "{\"app_id\":\"xxx\",\"order_id\":\"xxx\",\"refund_id\":\"xxx\",\"out_refund_no\":\"530402398023\",\"refund_status\":\"SUCCESS\",\"refund_total_amount\":\"100\",\"event_time\":\"1643185934447\"}"
}
```

## 开发者响应

### 响应参数

开发者收到回调后，需返回 HTTP 状态码 200 及以下 JSON 响应体：

| 字段名 | 必填 | 类型 | 说明 | 示例 |
|-|-|-|-|-|
| code | 是 | int32 | 返回码，成功时为 0 | 0 |
| msg | 是 | string | 返回信息，成功时为 "success" | success |

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

### 响应示例

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

## 代码示例

### Go

```go
package main

import (
        "encoding/json"
        "fmt"
        "io"
        "log"
        "net/http"
)

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

// RefundMsg 退款回调 msg 解析结构
type RefundMsg struct {
        AppID             string `json:"app_id"`
        OrderID           string `json:"order_id"`
        RefundID          string `json:"refund_id"`
        OutRefundNo       string `json:"out_refund_no"`
        RefundStatus      string `json:"refund_status"`
        RefundTotalAmount string `json:"refund_total_amount"`
        EventTime         string `json:"event_time"`
}

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

func refundCallbackHandler(w http.ResponseWriter, r *http.Request) {
        // 1. 验证签名，确保请求来自豆包平台
        authorization := r.Header.Get("X-DB-Authorization")
        logID := r.Header.Get("X-DB-Logid")
        if authorization == "" {
                log.Printf("missing X-DB-Authorization, logid: %s", logID)
                w.WriteHeader(http.StatusUnauthorized)
                json.NewEncoder(w).Encode(CallbackResponse{Code: 500000003, Msg: "VerifySignFailError"})
                return
        }
        // TODO: 实际签名验证逻辑
        // if !verifySignature(authorization, body) { ... }

        // 2. 读取并解析请求 Body
        body, err := io.ReadAll(r.Body)
        if err != nil {
                log.Printf("read body error: %v, logid: %s", err, logID)
                w.WriteHeader(http.StatusBadRequest)
                json.NewEncoder(w).Encode(CallbackResponse{Code: 500000001, Msg: "ParamError"})
                return
        }
        defer r.Body.Close()

        var callbackReq CallbackRequest
        if err := json.Unmarshal(body, &callbackReq); err != nil {
                log.Printf("parse callback request error: %v, logid: %s", err, logID)
                w.WriteHeader(http.StatusBadRequest)
                json.NewEncoder(w).Encode(CallbackResponse{Code: 500000001, Msg: "ParamError"})
                return
        }

        // 3. 确认回调类型
        if callbackReq.Type != "refund" {
                log.Printf("unexpected callback type: %s, logid: %s", callbackReq.Type, logID)
                w.WriteHeader(http.StatusBadRequest)
                json.NewEncoder(w).Encode(CallbackResponse{Code: 500000001, Msg: "ParamError"})
                return
        }

        // 4. 解析 msg JSON 字符串
        var refundMsg RefundMsg
        if err := json.Unmarshal([]byte(callbackReq.Msg), &refundMsg); err != nil {
                log.Printf("parse refund msg error: %v, logid: %s", err, logID)
                w.WriteHeader(http.StatusBadRequest)
                json.NewEncoder(w).Encode(CallbackResponse{Code: 500000001, Msg: "ParamError"})
                return
        }

        // 5. 处理业务逻辑（注意幂等处理）
        fmt.Printf("收到退款回调 - 订单号: %s, 退款单号: %s, 状态: %s, 金额: %s分, 时间: %s\n",
                refundMsg.OrderID, refundMsg.RefundID, refundMsg.RefundStatus,
                refundMsg.RefundTotalAmount, refundMsg.EventTime)

        switch refundMsg.RefundStatus {
        case "SUCCESS":
                // TODO: 更新退款状态为成功，执行相关业务处理
                log.Printf("退款成功 - refund_id: %s, out_refund_no: %s", refundMsg.RefundID, refundMsg.OutRefundNo)
        case "FAIL":
                // TODO: 更新退款状态为失败，执行相关业务处理
                log.Printf("退款失败 - refund_id: %s, out_refund_no: %s", refundMsg.RefundID, refundMsg.OutRefundNo)
        }

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

func main() {
        http.HandleFunc("/callback/refund", refundCallbackHandler)
        log.Println("Server started on :8080")
        log.Fatal(http.ListenAndServe(":8080", nil))
}
```

### Java

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

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/callback")
public class RefundCallbackController {

    private static final ObjectMapper objectMapper = new ObjectMapper();

    // 回调请求外层结构
    public static class CallbackRequest {
        public String type;
        public String version;
        public String msg;
    }

    // 退款回调 msg 解析结构
    public static class RefundMsg {
        @JsonProperty("app_id")
        public String appId;
        @JsonProperty("order_id")
        public String orderId;
        @JsonProperty("refund_id")
        public String refundId;
        @JsonProperty("out_refund_no")
        public String outRefundNo;
        @JsonProperty("refund_status")
        public String refundStatus;
        @JsonProperty("refund_total_amount")
        public String refundTotalAmount;
        @JsonProperty("event_time")
        public String eventTime;
    }

    @PostMapping("/refund")
    public ResponseEntity<Map<String, Object>> handleRefundCallback(
            @RequestHeader(value = "X-DB-Authorization", required = false) String authorization,
            @RequestHeader(value = "X-DB-Logid", required = false) String logId,
            @RequestBody String body) {

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

        try {
            // 1. 验证签名，确保请求来自豆包平台
            if (authorization == null || authorization.isEmpty()) {
                System.out.println("missing X-DB-Authorization, logid: " + logId);
                response.put("code", 500000003);
                response.put("msg", "VerifySignFailError");
                return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
            }
            // TODO: 实际签名验证逻辑
            // if (!verifySignature(authorization, body)) { ... }

            // 2. 解析请求 Body
            CallbackRequest callbackReq = objectMapper.readValue(body, CallbackRequest.class);

            // 3. 确认回调类型
            if (!"refund".equals(callbackReq.type)) {
                System.out.println("unexpected callback type: " + callbackReq.type + ", logid: " + logId);
                response.put("code", 500000001);
                response.put("msg", "ParamError");
                return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
            }

            // 4. 解析 msg JSON 字符串
            RefundMsg refundMsg = objectMapper.readValue(callbackReq.msg, RefundMsg.class);

            // 5. 处理业务逻辑（注意幂等处理）
            System.out.printf("收到退款回调 - 订单号: %s, 退款单号: %s, 状态: %s, 金额: %s分, 时间: %s%n",
                    refundMsg.orderId, refundMsg.refundId, refundMsg.refundStatus,
                    refundMsg.refundTotalAmount, refundMsg.eventTime);

            switch (refundMsg.refundStatus) {
                case "SUCCESS":
                    // TODO: 更新退款状态为成功，执行相关业务处理
                    System.out.println("退款成功 - refund_id: " + refundMsg.refundId
                            + ", out_refund_no: " + refundMsg.outRefundNo);
                    break;
                case "FAIL":
                    // TODO: 更新退款状态为失败，执行相关业务处理
                    System.out.println("退款失败 - refund_id: " + refundMsg.refundId
                            + ", out_refund_no: " + refundMsg.outRefundNo);
                    break;
            }

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

        } catch (Exception e) {
            System.out.println("处理退款回调异常: " + e.getMessage() + ", logid: " + logId);
            response.put("code", 500000000);
            response.put("msg", "InternalError");
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
        }
    }
}
```

### Node.js

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

app.use(express.json());

// 退款回调处理接口
app.post('/callback/refund', (req, res) => {
    const authorization = req.headers['x-db-authorization'];
    const logId = req.headers['x-db-logid'];

    // 1. 验证签名，确保请求来自豆包平台
    if (!authorization) {
        console.log(`missing X-DB-Authorization, logid: ${logId}`);
        return res.status(401).json({ code: 500000003, msg: 'VerifySignFailError' });
    }
    // TODO: 实际签名验证逻辑
    // if (!verifySignature(authorization, req.body)) { ... }

    try {
        const callbackReq = req.body;

        // 2. 确认回调类型
        if (callbackReq.type !== 'refund') {
            console.log(`unexpected callback type: ${callbackReq.type}, logid: ${logId}`);
            return res.status(400).json({ code: 500000001, msg: 'ParamError' });
        }

        // 3. 解析 msg JSON 字符串
        const refundMsg = JSON.parse(callbackReq.msg);

        // 4. 处理业务逻辑（注意幂等处理）
        console.log(`收到退款回调 - 订单号: ${refundMsg.order_id}, 退款单号: ${refundMsg.refund_id}, ` +
            `状态: ${refundMsg.refund_status}, 金额: ${refundMsg.refund_total_amount}分, ` +
            `时间: ${refundMsg.event_time}`);

        switch (refundMsg.refund_status) {
            case 'SUCCESS':
                // TODO: 更新退款状态为成功，执行相关业务处理
                console.log(`退款成功 - refund_id: ${refundMsg.refund_id}, ` +
                    `out_refund_no: ${refundMsg.out_refund_no}`);
                break;
            case 'FAIL':
                // TODO: 更新退款状态为失败，执行相关业务处理
                console.log(`退款失败 - refund_id: ${refundMsg.refund_id}, ` +
                    `out_refund_no: ${refundMsg.out_refund_no}`);
                break;
        }

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

    } catch (error) {
        console.log(`处理退款回调异常: ${error.message}, logid: ${logId}`);
        return res.status(500).json({ code: 500000000, msg: 'InternalError' });
    }
});

app.listen(8080, () => {
    console.log('Server started on :8080');
});
```

### Python

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

app = Flask(__name__)

@app.route('/callback/refund', methods=['POST'])
def handle_refund_callback():
    authorization = request.headers.get('X-DB-Authorization')
    log_id = request.headers.get('X-DB-Logid')

    # 1. 验证签名，确保请求来自豆包平台
    if not authorization:
        print(f'missing X-DB-Authorization, logid: {log_id}')
        return jsonify({'code': 500000003, 'msg': 'VerifySignFailError'}), 401
    # TODO: 实际签名验证逻辑
    # if not verify_signature(authorization, request.data):
    #     return jsonify({'code': 500000003, 'msg': 'VerifySignFailError'}), 401

    try:
        # 2. 解析请求 Body
        callback_req = request.get_json()

        # 3. 确认回调类型
        if callback_req.get('type') != 'refund':
            print(f"unexpected callback type: {callback_req.get('type')}, logid: {log_id}")
            return jsonify({'code': 500000001, 'msg': 'ParamError'}), 400

        # 4. 解析 msg JSON 字符串
        refund_msg = json.loads(callback_req['msg'])

        # 5. 处理业务逻辑（注意幂等处理）
        print(f"收到退款回调 - 订单号: {refund_msg['order_id']}, "
              f"退款单号: {refund_msg['refund_id']}, "
              f"状态: {refund_msg['refund_status']}, "
              f"金额: {refund_msg['refund_total_amount']}分, "
              f"时间: {refund_msg['event_time']}")

        refund_status = refund_msg['refund_status']
        if refund_status == 'SUCCESS':
            # TODO: 更新退款状态为成功，执行相关业务处理
            print(f"退款成功 - refund_id: {refund_msg['refund_id']}, "
                  f"out_refund_no: {refund_msg['out_refund_no']}")
        elif refund_status == 'FAIL':
            # TODO: 更新退款状态为失败，执行相关业务处理
            print(f"退款失败 - refund_id: {refund_msg['refund_id']}, "
                  f"out_refund_no: {refund_msg['out_refund_no']}")

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

    except Exception as e:
        print(f'处理退款回调异常: {str(e)}, logid: {log_id}')
        return jsonify({'code': 500000000, 'msg': 'InternalError'}), 500

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

## 注意事项

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

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

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

仅通知最终结果：退款回调仅通知最终结果（SUCCESS 或 FAIL），不会通知中间状态 PROCESSING。

回调地址要求：回调地址需为 HTTPS 类型，与退款申请时 notify_url 参数一致。
