package attachment import ( "bytes" "crypto/md5" "encoding/hex" "encoding/json" "fmt" "io" "mime/multipart" "net/http" "net/url" "os" "strconv" "time" "github.com/duke-git/lancet/v2/retry" "github.com/pkg/errors" ) const ( TokenSalt = "LanSeXiongDi!@#&*(" UrlPreview = "v1/attachment/preview" ) type UploadResp struct { Url string `json:"url"` PreviewUrl string `json:"previewUrl"` } // Upload 上传文件 // 返回值:oss地址,预览地址,错误 func Upload(host, filePath, system, business, fieldFormName string) (*UploadResp, error) { f, err := os.Open(filePath) if err != nil { return nil, errors.WithMessage(err, "打开待上传文件失败") } defer f.Close() body := &bytes.Buffer{} writer := multipart.NewWriter(body) formData := map[string]string{ "system": system, "business": business, } for k, v := range formData { err = writer.WriteField(k, v) if err != nil { return nil, errors.WithMessage(err, "构建form-data失败") } } // 使用给出的属性名paramName和文件名filePath创建一个新的form-data头 part, err := writer.CreateFormFile(fieldFormName, filePath) if err != nil { return nil, errors.WithMessage(err, "创建文件流失败") } // 将源复制到目标,将file写入到part 是按默认的缓冲区32k循环操作的,不会将内容一次性全写入内存中,这样就能解决大文件的问题 _, err = io.Copy(part, f) if err != nil { return nil, errors.WithMessage(err, "复制文件流失败") } err = writer.Close() if err != nil { return nil, errors.WithMessage(err, "close writer失败") } httpClient := http.Client{ Timeout: time.Minute * 10, } url := fmt.Sprintf("%s/v1/attachment/upload", host) req, err := http.NewRequest("POST", url, body) if err != nil { return nil, errors.WithMessage(err, "new request 失败") } req.Header.Set("Content-Type", writer.FormDataContentType()) // 请求服务器 uploadResp := &UploadResp{} requestHttp := func() error { var requestErr error var respHttp *http.Response respHttp, requestErr = httpClient.Do(req) if requestErr != nil { return errors.WithMessage(err, "上传响应失败") } defer respHttp.Body.Close() respBody, requestErr := io.ReadAll(respHttp.Body) if requestErr != nil { return errors.WithMessage(err, "读取响应体失败") } if respHttp.StatusCode != http.StatusOK { respMap := make(map[string]string) _ = json.Unmarshal(respBody, &respMap) if respMap["message"] != "" { // 非正常响应体 return errors.Errorf("上传响应状态异常,响应码:%d,响应体:%s", respHttp.StatusCode, string(respBody)) } return errors.Errorf("响应错误:%s", respMap["message"]) } requestErr = json.Unmarshal(respBody, &uploadResp) if requestErr != nil { // json失败为非正常响应体 if respHttp.StatusCode != http.StatusOK { return errors.Errorf("上传响应状态异常,响应码:%d,响应体:%s", respHttp.StatusCode, string(respBody)) } return errors.WithMessage(err, "json decode响应值失败") } return nil } _ = retry.Retry(func() error { err = requestHttp() return err }, retry.RetryTimes(5), retry.RetryDuration(time.Second*3)) if err != nil { return nil, err } return uploadResp, nil } // GeneratePreviewPrivateUrl 生成私有预览地址 func GeneratePreviewPrivateUrl(domain, param, attachmentUrl, water, fileName string, expireAt int64) string { token := Signature(attachmentUrl, water, expireAt) params := url.Values{} params.Add("url", attachmentUrl) params.Add("water", water) params.Add("token", token) params.Add("param", param) params.Add("fileName", fileName) params.Add("expireAt", strconv.FormatInt(expireAt, 10)) return fmt.Sprintf("%s/%s?%s", domain, UrlPreview, params.Encode()) } // Signature 附件加签 func Signature(attachmentUrl, water string, expireAt int64) string { s := fmt.Sprintf("%s,%s,%s,%d", TokenSalt, attachmentUrl, water, expireAt) return MD5Sign(s) } func MD5Sign(s string) string { sum := md5.Sum([]byte(s)) return hex.EncodeToString(sum[:]) }