第22章 自定义支付渠道(Pro 版)
自定义支付渠道 Pro 版 本节介绍 Pro 版功能。 除了 Cloudreve 已支持的支付渠道,你还可以通过实现 Cloudreve 的支付接口,自行集成或桥接其他第三方支付平台。 要实现自定义支付接口,你需要一个独立的 HTTP 服务,提供以下能力: 一个 API 端点,用于: 处理 Cloudreve 的创建支付请求,并返回支付页面 URL; 处理 Cloudreve 的订单支付状态查询请求; 在用户完成支付后,向指定 URL 发送 HTTP 请求,通知 Cloudreve 支付已完成。 实现支付接口 Implement 按照本章规范实现支付接口,部署该接口,并确保它能与 Cloudreve 通过网络正常通信。 警告 版本 3 的自定义支付 API 与版本 4 不兼容。 创建订单 POST
{ "name": "Unlimited Storage", "order_no": "20230209190648343421", "notify_url": "http://demo.cloudreve.org/api/v4/callback/custom/20230209190648343421", "amount": 8900, "currency": "CNY" }期望响应 Expected Response 无论订单创建是否成功,HTTP 响应码均应为 200,成功或失败由响应体中的 code 字段判定。 httpHTTP/1.1 200 OK
{
// 成功响应的 code 始终为 0
"code": 0,
// 支付收银台页面的 URL,该 URL 将生成二维码供用户扫描,用户也可以直接打开此 URL
"data": "https://examplepayment.com/checkout/26544743"
}查询订单状态 GET
{ "code": 0, // PAID - 已支付 // 其他值 - 未支付 "data": "PAID" }验签 Verify Signature 你可以在 Cloudreve 的支付设置中配置通讯密钥。 Cloudreve 的创建支付请求会使用此密钥进行签名,并将其放在请求中: 对于创建支付请求,签名放在 Authorization 请求头中,以 Bearer Cr 为前缀,验证时应去除该前缀(注意前缀后不应有空格); 对于查询订单状态请求,签名放在 URL 参数 sign 中。 使用以下算法验证签名: 从请求中提取签名,按 : 分割字符串,第二部分为签名过期时间戳,记为 timestamp。 验证其是否大于当前时间戳。 : 之前的部分记为 signature; go// 针对创建支付请求 authHeader := r.Header.Get("Authorization") if !strings.HasPrefix(authHeader, "Bearer Cr ") { return fmt.Errorf("invalid Authorization header format") }
signaturePart := strings.TrimPrefix(authHeader, "Bearer Cr ") parts := strings.Split(signaturePart, ":") if len(parts) != 2 { return fmt.Errorf("invalid signature format in header") } signature, timestampStr := parts[0], parts[1]
// 针对查询订单状态请求 signaturePart := r.URL.Query().Get("sign") parts := strings.Split(signaturePart, ":") if len(parts) != 2 { return fmt.Errorf("invalid signature format in URL") } signature, timestampStr := parts[0], parts[1]验证 timestamp 是否大于当前时间戳。 若小于当前时间戳,则返回错误。 gotimestamp, err := strconv.ParseInt(timestampStr, 10, 64) if err != nil { return fmt.Errorf("invalid timestamp: %w", err) }
if time.Now().Unix() > timestamp { return fmt.Errorf("signature expired") }获取待签名字符串: 对于创建支付请求: 遍历所有请求头,过滤出以 X-Cr- 开头的头,将其转换为 key=value 格式,排序后使用 & 连接,形成字符串 signedHeaderStr。 govar signedHeader []string for k, _ := range r.Header { if strings.HasPrefix(k, "X-Cr-") { signedHeader = append(signedHeader, fmt.Sprintf("%s=%s", k, r.Header.Get(k))) } } sort.Strings(signedHeader) signedHeaderStr := strings.Join(signedHeader, "&")将请求 URL 的 Path 部分、请求体以及 signedHeaderStr 编码为 JSON 字符串 signContent。 若 Path 为空,则使用 / 代替。 gotype RequestRawSign struct { Path string Header string Body string }
path := r.URL.Path if path == "" { path = "/" }
signContent, err := json.Marshal(RequestRawSign{ Path: path, Header: signedHeaderStr, Body: string(r.Body), })对于查询订单状态请求: 直接使用请求 URL 的 Path 部分(不含 Query 参数)作为 signContent。 若 Path 为空,则使用 / 代替。 gopath := r.URL.Path if path == "" { path = "/" }使用 : 连接 signContent 和 timestamp,形成字符串 signContentFinal,然后使用 HMAC 算法和通讯密钥计算 signContentFinal 的签名,记为 signActual。 签名使用 URL-safe Base64 编码。 gosignContentFinal := fmt.Sprintf("%s:%s", signContent, timestampStr) h := hmac.New(sha256.New, []byte(communicationKey)) h.Write([]byte(signContentFinal)) signActual := base64.URLEncoding.EncodeToString(h.Sum(nil))比较 signActual 与 signature,检查是否一致。 goif signActual != signature { return fmt.Errorf("invalid signature") }发送回调 Send Callback 用户完成支付后,你需要向创建支付时指定的 notify_url 发送 GET 请求,通知 Cloudreve 用户已完成支付。 若回调请求失败,除非响应明确返回错误信息和代码,否则应以指数退避方式重试。 Cloudreve 的响应示例如下: httpHTTP/1.1 200 OK
{ // 成功响应的 code 始终为 0 "code": 0 }