package service import ( "context" "crypto/hmac" "crypto/sha1" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "sort" "strings" "time" "github.com/google/uuid" "github.com/topfans/backend/gateway/config" ) // ImagesegClient 视觉智能开放平台 — 分割抠图(imageseg) type ImagesegClient struct { accessKeyID string accessKeySecret string regionID string client *http.Client } func NewImagesegClient(ossCfg config.OSSConfig) *ImagesegClient { region := strings.TrimSpace(ossCfg.Region) region = strings.TrimPrefix(region, "oss-") if region == "" { region = "cn-shanghai" } return &ImagesegClient{ accessKeyID: strings.TrimSpace(ossCfg.AccessKeyID), accessKeySecret: strings.TrimSpace(ossCfg.AccessKeySecret), regionID: region, client: &http.Client{Timeout: 120 * time.Second}, } } func (c *ImagesegClient) enabled() bool { return c.accessKeyID != "" && c.accessKeySecret != "" && c.accessKeyID != "your-access-key-id" } // SegmentHDBodyURL 人体高清抠图,返回结果图 URL(对应控制台已开通的 SegmentHDBody) func (c *ImagesegClient) SegmentHDBodyURL(ctx context.Context, imageURL string) (string, error) { return c.callPOP(ctx, "SegmentHDBody", imageURL) } // SegmentCommonImageURL 通用分割抠图(异形主体可备选) func (c *ImagesegClient) SegmentCommonImageURL(ctx context.Context, imageURL string) (string, error) { return c.callPOP(ctx, "SegmentCommonImage", imageURL) } func (c *ImagesegClient) callPOP(ctx context.Context, action, imageURL string) (string, error) { if !c.enabled() { return "", fmt.Errorf("分割抠图未配置 OSS AccessKey") } imageURL = strings.TrimSpace(imageURL) if !strings.HasPrefix(imageURL, "http://") && !strings.HasPrefix(imageURL, "https://") { return "", fmt.Errorf("分割抠图需要可访问的图片 URL") } params := map[string]string{ "Format": "JSON", "Version": "2019-12-30", "AccessKeyId": c.accessKeyID, "SignatureMethod": "HMAC-SHA1", "Timestamp": time.Now().UTC().Format("2006-01-02T15:04:05Z"), "SignatureVersion": "1.0", "SignatureNonce": uuid.NewString(), "Action": action, "ImageURL": 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("imageseg.%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 "", mapImagesegHTTPError(res.StatusCode, string(raw)) } var data map[string]interface{} if err := json.Unmarshal(raw, &data); err != nil { return "", fmt.Errorf("分割抠图返回非 JSON") } if code, ok := data["Code"]; ok { if s := fmt.Sprint(code); s != "" && s != "0" && !strings.EqualFold(s, "success") { msg, _ := data["Message"].(string) return "", mapImagesegBizError(s, msg) } } flat := data if inner, ok := data["Data"].(string); ok && inner != "" { var parsed map[string]interface{} if err := json.Unmarshal([]byte(inner), &parsed); err == nil { for k, v := range parsed { flat[k] = v } } } if inner, ok := data["Data"].(map[string]interface{}); ok { for k, v := range inner { flat[k] = v } } outURL := pickImagesegResultURL(flat) if outURL == "" { return "", fmt.Errorf("分割抠图未返回结果图 URL") } return outURL, nil } func pickImagesegResultURL(data map[string]interface{}) string { for _, key := range []string{"ImageURL", "imageURL", "Url", "url", "ResultImageURL"} { if u, ok := data[key].(string); ok && isHTTPURL(u) { return u } } return pickModelScopeImageURL(data) } func mapImagesegBizError(code, message string) error { lower := strings.ToLower(message) if code == "InvalidApi.NotPurchase" || strings.Contains(lower, "not purchase") || strings.Contains(lower, "notpurchase") { return fmt.Errorf("分割抠图能力未开通:请在视觉智能开放平台控制台确认 SegmentHDBody 已开通(华东2-上海)") } if code == "Forbidden" || strings.Contains(lower, "no permission") { return fmt.Errorf("分割抠图无权限:请为 RAM 用户添加 AliyunVIAPIFullAccess 策略") } if message != "" { return fmt.Errorf("分割抠图 %s: %s", code, message) } return fmt.Errorf("分割抠图错误: %s", code) } func mapImagesegHTTPError(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 mapImagesegBizError(code, msg) } } return fmt.Errorf("分割抠图 HTTP %d: %s", httpCode, truncate(raw, 240)) }