package service import ( "context" "crypto/hmac" "crypto/sha1" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "net/url" "sort" "strings" "time" "github.com/google/uuid" "github.com/topfans/backend/gateway/config" ) // IvpdClient 阿里云 IVPD SegmentImage(服务端 AK,不暴露给客户端) type IvpdClient struct { accessKeyID string accessKeySecret string regionID string client *http.Client } func NewIvpdClient(ossCfg config.OSSConfig) *IvpdClient { region := strings.TrimSpace(ossCfg.Region) region = strings.TrimPrefix(region, "oss-") if region == "" { region = "cn-shanghai" } return &IvpdClient{ accessKeyID: strings.TrimSpace(ossCfg.AccessKeyID), accessKeySecret: strings.TrimSpace(ossCfg.AccessKeySecret), regionID: region, client: &http.Client{Timeout: 120 * time.Second}, } } func (c *IvpdClient) enabled() bool { return c.accessKeyID != "" && c.accessKeySecret != "" && c.accessKeyID != "your-access-key-id" } // SegmentImageURL 调用 IVPD SegmentImage,返回结果图 HTTP URL func (c *IvpdClient) SegmentImageURL(ctx context.Context, imageURL string) (string, error) { if !c.enabled() { return "", fmt.Errorf("IVPD 未配置 OSS AccessKey") } imageURL = strings.TrimSpace(imageURL) if !strings.HasPrefix(imageURL, "http://") && !strings.HasPrefix(imageURL, "https://") { return "", fmt.Errorf("IVPD 需要可访问的图片 URL") } params := map[string]string{ "Format": "JSON", "Version": "2019-06-25", "AccessKeyId": c.accessKeyID, "SignatureMethod": "HMAC-SHA1", "Timestamp": time.Now().UTC().Format("2006-01-02T15:04:05Z"), "SignatureVersion": "1.0", "SignatureNonce": uuid.NewString(), "Action": "SegmentImage", "RegionId": c.regionID, "Url": imageURL, } keys := make([]string, 0, len(params)) for k := range params { keys = append(keys, k) } sort.Strings(keys) pairs := make([]string, 0, len(keys)) for _, k := range keys { pairs = append(pairs, percentEncode(k)+"="+percentEncode(params[k])) } canonicalized := strings.Join(pairs, "&") stringToSign := "POST&" + percentEncode("/") + "&" + percentEncode(canonicalized) mac := hmac.New(sha1.New, []byte(c.accessKeySecret+"&")) mac.Write([]byte(stringToSign)) signature := base64.StdEncoding.EncodeToString(mac.Sum(nil)) body := canonicalized + "&Signature=" + percentEncode(signature) host := fmt.Sprintf("ivpd.%s.aliyuncs.com", c.regionID) req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://"+host+"/", strings.NewReader(body)) if err != nil { return "", err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") res, err := c.client.Do(req) if err != nil { return "", err } defer res.Body.Close() raw, err := io.ReadAll(io.LimitReader(res.Body, 1<<20)) if err != nil { return "", err } if res.StatusCode < 200 || res.StatusCode >= 300 { return "", mapIvpdError(res.StatusCode, string(raw)) } var data map[string]interface{} if err := json.Unmarshal(raw, &data); err != nil { return "", fmt.Errorf("IVPD 返回非 JSON") } if code, ok := data["Code"]; ok { if s := fmt.Sprint(code); s != "0" && s != "" { msg, _ := data["Message"].(string) return "", mapIvpdBizError(s, msg) } } if code, ok := data["code"]; ok { if s := fmt.Sprint(code); s != "0" && s != "" { msg, _ := data["message"].(string) return "", mapIvpdBizError(s, msg) } } outURL := pickIvpdResultURL(data) if outURL == "" { return "", fmt.Errorf("IVPD 未返回结果图 URL") } return outURL, nil } func pickIvpdResultURL(data map[string]interface{}) string { if result, ok := data["Result"].(map[string]interface{}); ok { if u, ok := result["Url"].(string); ok && isHTTPURL(u) { return u } if u, ok := result["URL"].(string); ok && isHTTPURL(u) { return u } } if result, ok := data["result"].(map[string]interface{}); ok { if u, ok := result["url"].(string); ok && isHTTPURL(u) { return u } } if u, ok := data["Url"].(string); ok && isHTTPURL(u) { return u } return pickModelScopeImageURL(data) } func percentEncode(s string) string { escaped := url.QueryEscape(s) escaped = strings.ReplaceAll(escaped, "+", "%20") escaped = strings.ReplaceAll(escaped, "*", "%2A") escaped = strings.ReplaceAll(escaped, "%7E", "~") return escaped } func mapIvpdBizError(code, message string) error { if code == "NeedOpen" || strings.Contains(strings.ToLower(message), "please open service") { return fmt.Errorf("IVPD(智能视觉生产)尚未开通:请登录阿里云控制台搜索「智能视觉生产」或「视觉智能开放平台」,选择华东2(上海)开通后再试") } if message != "" { return fmt.Errorf("IVPD %s: %s", code, message) } return fmt.Errorf("IVPD 错误: %s", code) } func mapIvpdError(httpCode int, raw string) error { var data map[string]interface{} if err := json.Unmarshal([]byte(raw), &data); err == nil { code := fmt.Sprint(data["Code"]) msg, _ := data["Message"].(string) if code != "" && code != "" { return mapIvpdBizError(code, msg) } } return fmt.Errorf("IVPD HTTP %d: %s", httpCode, truncate(raw, 240)) }