package main // fileapi.go 调 nl-pms-api 的最小客户端:上传图片换取公开访问 URL, // 以及素材库的列表/删除透传(权限由服务端按 用户/团队角色 判定)。 // 服务器地址与密钥来自管理员下发的全局文件存储配置(见 filestorage.go)。 // 请求由 Go 端发起(不走前端 fetch),天然绕开 webview 的 CORS 限制。 import ( "bytes" "encoding/json" "errors" "fmt" "io" "mime/multipart" "net/http" "strconv" "strings" "time" ) var fileAPIClient = &http.Client{Timeout: 30 * time.Second} // currentTeamID 返回客户端当前所在团队 id(未加入团队为 0)。 func (a *App) currentTeamID() int64 { n, _ := strconv.ParseInt(strings.TrimSpace(a.store.Meta("current_team_id")), 10, 64) if n < 0 { return 0 } return n } // fileAPIBase 返回已启用的服务器存储地址(实时读全局配置,离线回本地缓存); // 未启用返回 FILE_API_UNCONFIGURED。 func (a *App) fileAPIBase() (FileStorageConfig, error) { cfg := a.currentFileStorage() if cfg.Mode != "server" || cfg.BaseURL == "" { return cfg, errors.New("FILE_API_UNCONFIGURED") } return cfg, nil } // fileAPIStatusErr 把 nl-pms-api 的非 200 响应映射为客户端错误码。 func fileAPIStatusErr(status int) error { switch status { case http.StatusForbidden: return errors.New("FILE_PERMISSION_DENIED") case http.StatusNotFound: return errors.New("FILE_NOT_FOUND") default: return errors.New("FILE_API_REQUEST_FAILED") } } // uploadImageToServer 把编码好的图片字节上传到 nl-pms-api,返回可公开访问的 http URL。 // 归属:头像记个人(teamId=0),内容图记当前团队,素材库据此划定管理范围。 // 未启用服务器存储时返回 FILE_API_UNCONFIGURED;失败不静默降级,由调用方向用户报错。 func (a *App) uploadImageToServer(data []byte, mime, kind string) (string, error) { cfg, e := a.fileAPIBase() if e != nil { return "", e } ext := ".jpg" switch mime { case "image/png": ext = ".png" case "image/gif": ext = ".gif" case "image/webp": ext = ".webp" } var buf bytes.Buffer w := multipart.NewWriter(&buf) fw, e := w.CreateFormFile("file", "img"+ext) if e != nil { return "", errors.New("IMAGE_UPLOAD_FAILED") } if _, e = fw.Write(data); e != nil { return "", errors.New("IMAGE_UPLOAD_FAILED") } _ = w.WriteField("kind", kind) if id := a.syncUserID(); id > 0 { _ = w.WriteField("userId", strconv.FormatInt(id, 10)) } if kind == "content" { _ = w.WriteField("teamId", strconv.FormatInt(a.currentTeamID(), 10)) } if e = w.Close(); e != nil { return "", errors.New("IMAGE_UPLOAD_FAILED") } req, e := http.NewRequest(http.MethodPost, cfg.BaseURL+"/api/v1/files", &buf) if e != nil { return "", errors.New("IMAGE_UPLOAD_FAILED") } req.Header.Set("Content-Type", w.FormDataContentType()) req.Header.Set("Authorization", "Bearer "+cfg.APIKey) resp, e := fileAPIClient.Do(req) if e != nil { return "", errors.New("IMAGE_UPLOAD_FAILED") } defer resp.Body.Close() body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) if resp.StatusCode != http.StatusOK { return "", errors.New("IMAGE_UPLOAD_FAILED") } var out struct { URL string `json:"url"` } if json.Unmarshal(body, &out) != nil || out.URL == "" { return "", errors.New("IMAGE_UPLOAD_FAILED") } return out.URL, nil } // ListServerFiles 素材库分页列表。scope=mine 看自己;scope=team 看指定团队 // (需为该团队 owner/admin);scope=all 看全部(仅云端账号 id=1)。 func (a *App) ListServerFiles(scope string, teamID int64, page int) (ServerFileList, error) { if e := a.ready(); e != nil { return ServerFileList{}, e } cfg, e := a.fileAPIBase() if e != nil { return ServerFileList{}, e } uid := a.syncUserID() if uid <= 0 { return ServerFileList{}, errors.New("SYNC_NOT_LOGGED_IN") } if page < 1 { page = 1 } u := fmt.Sprintf("%s/api/v1/files?scope=%s&userId=%d&teamId=%d&page=%d&pageSize=24", cfg.BaseURL, scope, uid, teamID, page) req, e := http.NewRequest(http.MethodGet, u, nil) if e != nil { return ServerFileList{}, errors.New("FILE_API_REQUEST_FAILED") } req.Header.Set("Authorization", "Bearer "+cfg.APIKey) resp, e := fileAPIClient.Do(req) if e != nil { return ServerFileList{}, errors.New("FILE_STORAGE_UNREACHABLE") } defer resp.Body.Close() body, _ := io.ReadAll(io.LimitReader(resp.Body, 8<<20)) if resp.StatusCode != http.StatusOK { return ServerFileList{}, fileAPIStatusErr(resp.StatusCode) } var out ServerFileList if json.Unmarshal(body, &out) != nil { return ServerFileList{}, errors.New("FILE_API_REQUEST_FAILED") } return out, nil } // DeleteServerFile 删除素材(记录+服务器磁盘文件)。服务端校验:本人、 // 超管 id=1、或该文件归属团队的 owner/admin;越权返回 FILE_PERMISSION_DENIED。 func (a *App) DeleteServerFile(id int64) error { if e := a.ready(); e != nil { return e } cfg, e := a.fileAPIBase() if e != nil { return e } uid := a.syncUserID() if uid <= 0 { return errors.New("SYNC_NOT_LOGGED_IN") } u := fmt.Sprintf("%s/api/v1/files/%d?userId=%d", cfg.BaseURL, id, uid) req, e := http.NewRequest(http.MethodDelete, u, nil) if e != nil { return errors.New("FILE_API_REQUEST_FAILED") } req.Header.Set("Authorization", "Bearer "+cfg.APIKey) resp, e := fileAPIClient.Do(req) if e != nil { return errors.New("FILE_STORAGE_UNREACHABLE") } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fileAPIStatusErr(resp.StatusCode) } return nil }