topfans/backend/gateway/service/ivpd_client.go
2026-06-03 22:19:22 +08:00

181 lines
5.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 != "<nil>" {
return mapIvpdBizError(code, msg)
}
}
return fmt.Errorf("IVPD HTTP %d: %s", httpCode, truncate(raw, 240))
}