优化,添加115接口文档

This commit is contained in:
truewhile
2026-09-05 14:32:58 +08:00
parent d00b14df55
commit 0e9fb2c937
57 changed files with 5682 additions and 76 deletions
@@ -6,6 +6,7 @@ import (
"net/http/httptest"
"net/url"
"strings"
"sync"
"testing"
"time"
)
@@ -472,3 +473,65 @@ func TestFsListRefreshContinue(t *testing.T) {
t.Fatalf("want 1 file, got %d", len(files))
}
}
// TestGetDownloadURLsBatch 验证批量换链:多个 pick_code 合并为一次逗号分隔
// 请求;响应按文件 ID 为键、以条目内 pick_code 映射回请求侧;已缓存的
// pick_code 不再发起请求;缺失项(空 URL)不出现在结果中。
func TestGetDownloadURLsBatch(t *testing.T) {
var mu sync.Mutex
var requests []string
mockAPI(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/open/ufile/downurl" {
t.Errorf("unexpected path %s", r.URL.Path)
}
pc := r.PostFormValue("pick_code")
mu.Lock()
requests = append(requests, pc)
mu.Unlock()
w.Write([]byte(`{"state":true,"data":{
"111":{"pick_code":"batch-pc-a","url":{"url":"https://cdn/a.mkv"}},
"222":{"pick_code":"batch-pc-b","url":{"url":"https://cdn/b.jpg"}},
"333":{"pick_code":"batch-pc-empty","url":{"url":""}}}}`))
})
t.Cleanup(func() {
ClearDownloadURLCache("batch-pc-a")
ClearDownloadURLCache("batch-pc-b")
ClearDownloadURLCache("batch-pc-empty")
})
c := NewOpenClient("100195125", "at1", "rt1")
urls, err := c.GetDownloadURLsBatch(context.Background(),
[]string{"batch-pc-a", "batch-pc-b", "batch-pc-empty", "", "batch-pc-a"}, "")
if err != nil {
t.Fatalf("batch downurl: %v", err)
}
if urls["batch-pc-a"] != "https://cdn/a.mkv" || urls["batch-pc-b"] != "https://cdn/b.jpg" {
t.Fatalf("bad urls: %#v", urls)
}
if _, ok := urls["batch-pc-empty"]; ok {
t.Fatalf("empty-url entry should be absent: %#v", urls)
}
if _, ok := urls[""]; ok {
t.Fatalf("empty pickcode should be absent: %#v", urls)
}
mu.Lock()
if len(requests) != 1 || requests[0] != "batch-pc-a,batch-pc-b,batch-pc-empty" {
mu.Unlock()
t.Fatalf("unexpected downurl requests: %v", requests)
}
mu.Unlock()
// 第二次调用全部命中缓存:不再发任何请求
urls2, err := c.GetDownloadURLsBatch(context.Background(), []string{"batch-pc-a", "batch-pc-b"}, "")
if err != nil {
t.Fatalf("cached batch downurl: %v", err)
}
if urls2["batch-pc-a"] != "https://cdn/a.mkv" {
t.Fatalf("cached url lost: %#v", urls2)
}
mu.Lock()
defer mu.Unlock()
if len(requests) != 1 {
t.Fatalf("cache hit should not issue requests, got %v", requests)
}
}
+35
View File
@@ -0,0 +1,35 @@
// 115 开放平台删除类 API:元数据覆盖上传前清理远端旧文件。
package cloud115
import (
"context"
"strings"
)
// DeleteFiles 批量删除 115 文件(官方接口 POST /open/ufile/delete)。
// 删除为异步执行,文件移入回收站。parentID 为待删除文件所在父目录 ID
//(可选提示,空串省略)。fileIDs 中的空项自动忽略,全为空时直接返回成功。
func (c *OpenClient) DeleteFiles(ctx context.Context, parentID string, fileIDs ...string) error {
ids := make([]string, 0, len(fileIDs))
for _, id := range fileIDs {
if id = strings.TrimSpace(id); id != "" {
ids = append(ids, id)
}
}
if len(ids) == 0 {
return nil
}
params := map[string]string{"file_ids": strings.Join(ids, ",")}
if parentID = strings.TrimSpace(parentID); parentID != "" {
params["parent_id"] = parentID
}
resp, err := c.doAuthJSON(ctx, "POST", ProAPIBase+"/open/ufile/delete", params, 2)
if err != nil {
return err
}
// doJSON 已把 state=false 转为错误返回,这里兜底防御响应外壳异常
if !resp.State {
return NewOpenAPIResponseError(resp.Code, resp.Errno, resp.Message, resp.Error, "115 删除文件失败")
}
return nil
}
+56
View File
@@ -225,6 +225,62 @@ func (c *OpenClient) GetDownloadURLWithUA(ctx context.Context, pickCode, ua stri
return first.URL.URL, nil
}
// downurlBatchSize 单次批量换取直链的 pick_code 数上限。官方 /open/ufile/downurl
// 支持逗号分隔多个 pick_code,批量可大幅降低元数据下载的换链请求量;大小取
// 保守值,减小单个违规/异常文件导致整批失败的爆炸半径。
const downurlBatchSize = 10
// GetDownloadURLsBatch 批量获取下载直链(pickcode → URL)。先查进程内缓存,
// 仅对未命中的 pick_code 分片发起批量请求;单个分片失败时返回已解析的部分与
// 错误,调用方对缺失项回退到逐个 GetDownloadURLWithUA。UA 语义与单个换取
// 一致:直链绑定换取时的 UA,后续下载必须携带同一 UA。
func (c *OpenClient) GetDownloadURLsBatch(ctx context.Context, pickCodes []string, ua string) (map[string]string, error) {
ua = strings.TrimSpace(ua)
out := make(map[string]string, len(pickCodes))
seen := make(map[string]struct{}, len(pickCodes))
missing := make([]string, 0, len(pickCodes))
for _, pc := range pickCodes {
pc = strings.TrimSpace(pc)
if pc == "" {
continue
}
if _, dup := seen[pc]; dup {
continue
}
seen[pc] = struct{}{}
if cached := GetDownloadURLCache(pc, ua); cached != "" {
out[pc] = cached
continue
}
missing = append(missing, pc)
}
for start := 0; start < len(missing); start += downurlBatchSize {
end := start + downurlBatchSize
if end > len(missing) {
end = len(missing)
}
chunk := missing[start:end]
params := map[string]string{"pick_code": strings.Join(chunk, ",")}
resp, err := c.doAuthJSONWithUA(ctx, "POST", ProAPIBase+"/open/ufile/downurl", params, 1, ua)
if err != nil {
return out, err
}
var data map[string]downloadURLData
if err := json.Unmarshal(resp.Data, &data); err != nil {
return out, fmt.Errorf("115: 解析下载地址失败:%w", err)
}
// 响应以文件 ID 为键,条目内的 pick_code 用于映射回请求侧
for _, item := range data {
if item.PickCode == "" || item.URL.URL == "" {
continue
}
SetDownloadURLCache(item.PickCode, item.URL.URL, ua)
out[item.PickCode] = item.URL.URL
}
}
return out, nil
}
// ─── 授权(设备码扫码) ──────────────────────────────────────────────────────
// QrCodeScanStatus 扫码状态。