181 lines
5.1 KiB
Go
181 lines
5.1 KiB
Go
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))
|
||
}
|