git-subtree-dir: server git-subtree-mainline:c4e617ada7git-subtree-split:e64421295f
209 lines
6.0 KiB
Go
209 lines
6.0 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/md5"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gogf/gf/v2/frame/g"
|
|
)
|
|
|
|
// 淘宝联盟适配器(电商类目)
|
|
// 接口以 eco.taobao.com TOP 开放平台为准:taobao.tbk.dg.material.optional(选品)/ taobao.tbk.tpwd.create(淘口令转链)
|
|
type tbProvider struct{}
|
|
|
|
func (tbProvider) Source() string { return "tb_ecom" }
|
|
|
|
func (tbProvider) Enabled() bool {
|
|
ctx := context.Background()
|
|
return g.Cfg().MustGet(ctx, "cps.tb_appkey", "").String() != "" &&
|
|
g.Cfg().MustGet(ctx, "cps.tb_secret", "").String() != "" &&
|
|
g.Cfg().MustGet(ctx, "cps.tb_pid", "").String() != ""
|
|
}
|
|
|
|
func (tbProvider) apiBase(ctx context.Context) string {
|
|
return strings.TrimRight(g.Cfg().MustGet(ctx, "cps.tb_base",
|
|
"https://eco.taobao.com/router/rest").String(), "/")
|
|
}
|
|
|
|
// SyncProducts 选品(按类目,cat 传淘宝叶子类目 ID)
|
|
func (p tbProvider) SyncProducts(ctx context.Context, city, catCode string) ([]CpsProduct, error) {
|
|
biz := map[string]any{
|
|
"adzone_id": g.Cfg().MustGet(ctx, "cps.tb_adzone_id", "").String(),
|
|
"cat": catCode,
|
|
"page_no": 1,
|
|
"page_size": 20,
|
|
"sort": "total_sales_des",
|
|
}
|
|
resp, err := p.doRequest(ctx, "taobao.tbk.dg.material.optional", biz)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return p.parseProducts(resp, catCode)
|
|
}
|
|
|
|
// Search 关键词实时搜索
|
|
func (p tbProvider) Search(ctx context.Context, keyword, catCode string, page int) ([]CpsProduct, error) {
|
|
biz := map[string]any{
|
|
"adzone_id": g.Cfg().MustGet(ctx, "cps.tb_adzone_id", "").String(),
|
|
"q": keyword,
|
|
"page_no": page,
|
|
"page_size": 20,
|
|
"sort": "total_sales_des",
|
|
}
|
|
resp, err := p.doRequest(ctx, "taobao.tbk.dg.material.optional", biz)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return p.parseProducts(resp, catCode)
|
|
}
|
|
|
|
// GetLink 淘口令转链(pid 归因;返回口令文本,客户端复制跳转)
|
|
func (p tbProvider) GetLink(ctx context.Context, outerId string) (string, error) {
|
|
biz := map[string]any{
|
|
"text": "好物分享",
|
|
"url": "https://item.taobao.com/item.htm?id=" + outerId,
|
|
"user_id": g.Cfg().MustGet(ctx, "cps.tb_pid", "").String(),
|
|
}
|
|
resp, err := p.doRequest(ctx, "taobao.tbk.tpwd.create", biz)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
var d struct {
|
|
Data struct {
|
|
Model string `json:"model"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(resp, &d); err != nil {
|
|
return "", err
|
|
}
|
|
if d.Data.Model == "" {
|
|
return "", fmt.Errorf("淘宝转链返回空")
|
|
}
|
|
return d.Data.Model, nil
|
|
}
|
|
|
|
// doRequest 淘宝 TOP 签名请求(sign = HMAC-MD5(参数键排序拼接, secret),大写)
|
|
// 公共参数与业务参数统一排序拼接,sign_method=hmac
|
|
func (p tbProvider) doRequest(ctx context.Context, method string, biz map[string]any) ([]byte, error) {
|
|
appKey := g.Cfg().MustGet(ctx, "cps.tb_appkey", "").String()
|
|
secret := g.Cfg().MustGet(ctx, "cps.tb_secret", "").String()
|
|
|
|
params := map[string]string{
|
|
"method": method,
|
|
"app_key": appKey,
|
|
"timestamp": time.Now().Format("2006-01-02 15:04:05"),
|
|
"format": "json",
|
|
"v": "2.0",
|
|
"sign_method": "hmac",
|
|
}
|
|
for k, v := range biz {
|
|
params[k] = fmt.Sprint(v)
|
|
}
|
|
keys := make([]string, 0, len(params))
|
|
for k := range params {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
var sb strings.Builder
|
|
for _, k := range keys {
|
|
sb.WriteString(k)
|
|
sb.WriteString(params[k])
|
|
}
|
|
mac := hmac.New(md5.New, []byte(secret))
|
|
mac.Write([]byte(sb.String()))
|
|
params["sign"] = strings.ToUpper(hex.EncodeToString(mac.Sum(nil)))
|
|
|
|
form := url.Values{}
|
|
for k, v := range params {
|
|
form.Set(k, v)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
|
|
p.apiBase(ctx), strings.NewReader(form.Encode()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
client := &http.Client{Timeout: 15 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("淘宝接口 %s 返回 %d: %s", method, resp.StatusCode, string(body))
|
|
}
|
|
// TOP 错误响应 {error_response:{code,msg}} 以 HTTP 200 返回,必须显式拦截
|
|
var er struct {
|
|
ErrorResponse struct {
|
|
Code int `json:"code"`
|
|
Msg string `json:"msg"`
|
|
} `json:"error_response"`
|
|
}
|
|
if json.Unmarshal(body, &er) == nil && er.ErrorResponse.Code != 0 {
|
|
return nil, fmt.Errorf("淘宝接口 %s 错误 %d: %s", method, er.ErrorResponse.Code, er.ErrorResponse.Msg)
|
|
}
|
|
return body, nil
|
|
}
|
|
|
|
func (p tbProvider) parseProducts(body []byte, catCode string) ([]CpsProduct, error) {
|
|
var d struct {
|
|
Resp struct {
|
|
ResultList struct {
|
|
MapData []struct {
|
|
NumIID int64 `json:"num_iid"`
|
|
Title string `json:"title"`
|
|
PictURL string `json:"pict_url"`
|
|
ZkFinalPrice string `json:"zk_final_price"`
|
|
ShopTitle string `json:"shop_title"`
|
|
CommissionRate string `json:"commission_rate"`
|
|
} `json:"map_data"`
|
|
} `json:"result_list"`
|
|
} `json:"tbk_dg_material_optional_response"`
|
|
}
|
|
if err := json.Unmarshal(body, &d); err != nil {
|
|
return nil, fmt.Errorf("淘宝选品响应解析失败: %v", err)
|
|
}
|
|
out := make([]CpsProduct, 0, len(d.Resp.ResultList.MapData))
|
|
for _, it := range d.Resp.ResultList.MapData {
|
|
// commission_rate 是百分比字符串(如 "3.5" = 3.5%),转万分比
|
|
rate := parseRateWanfen(it.CommissionRate)
|
|
out = append(out, CpsProduct{
|
|
Source: p.Source(),
|
|
OuterId: strconv.FormatInt(it.NumIID, 10),
|
|
CategoryCode: catCode,
|
|
Name: it.Title,
|
|
CoverUrl: it.PictURL,
|
|
PriceFen: parseFen(it.ZkFinalPrice),
|
|
ShopName: it.ShopTitle,
|
|
CommissionRate: rate,
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// parseRateWanfen 佣金百分比字符串("3.5" 表示 3.5%)→ 万分比(350)
|
|
func parseRateWanfen(percent string) int {
|
|
f, err := strconv.ParseFloat(strings.TrimSpace(percent), 64)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return int(f * 100)
|
|
}
|