git-subtree-dir: server git-subtree-mainline:c4e617ada7git-subtree-split:e64421295f
234 lines
6.4 KiB
Go
234 lines
6.4 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"crypto/md5"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gogf/gf/v2/frame/g"
|
|
)
|
|
|
|
// 京东联盟适配器(电商类目)
|
|
// 接口以 api.jd.com 开放平台为准:jd.union.open.goods.query(选品)/ jd.union.open.promotion.common.get(转链)
|
|
type jdProvider struct{}
|
|
|
|
func (jdProvider) Source() string { return "jd_ecom" }
|
|
|
|
func (jdProvider) Enabled() bool {
|
|
ctx := context.Background()
|
|
return g.Cfg().MustGet(ctx, "cps.jd_appkey", "").String() != "" &&
|
|
g.Cfg().MustGet(ctx, "cps.jd_secret", "").String() != ""
|
|
}
|
|
|
|
func (jdProvider) apiBase(ctx context.Context) string {
|
|
return strings.TrimRight(g.Cfg().MustGet(ctx, "cps.jd_base",
|
|
"https://api.jd.com/routerjson").String(), "/")
|
|
}
|
|
|
|
// SyncProducts 商品选品(按类目)
|
|
func (p jdProvider) SyncProducts(ctx context.Context, city, catCode string) ([]CpsProduct, error) {
|
|
biz := map[string]any{
|
|
"goodsReqDTO": map[string]any{
|
|
"cid1": catCode,
|
|
"pageIndex": 1,
|
|
"pageSize": 20,
|
|
"eliteId": 1,
|
|
"sortName": "inOrderCount30Days",
|
|
"sort": "desc",
|
|
"fields": "skuId,skuName,imageUrl,priceInfo,shopName,commissionInfo,categoryInfo",
|
|
},
|
|
}
|
|
resp, err := p.doRequest(ctx, "jd.union.open.goods.query", biz)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return p.parseProducts(resp, catCode)
|
|
}
|
|
|
|
// Search 关键词实时搜索
|
|
func (p jdProvider) Search(ctx context.Context, keyword, catCode string, page int) ([]CpsProduct, error) {
|
|
biz := map[string]any{
|
|
"goodsReqDTO": map[string]any{
|
|
"keyword": keyword,
|
|
"pageIndex": page,
|
|
"pageSize": 20,
|
|
"sortName": "inOrderCount30Days",
|
|
"sort": "desc",
|
|
"fields": "skuId,skuName,imageUrl,priceInfo,shopName,commissionInfo,categoryInfo",
|
|
},
|
|
}
|
|
resp, err := p.doRequest(ctx, "jd.union.open.goods.query", biz)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return p.parseProducts(resp, catCode)
|
|
}
|
|
|
|
// GetLink 转链(pid 归因)
|
|
func (p jdProvider) GetLink(ctx context.Context, outerId string) (string, error) {
|
|
biz := map[string]any{
|
|
"promotionCodeReq": map[string]any{
|
|
"materialId": "https://item.jd.com/" + outerId + ".html",
|
|
"siteId": g.Cfg().MustGet(ctx, "cps.jd_site_id", "").String(),
|
|
"positionId": g.Cfg().MustGet(ctx, "cps.jd_pid", "").String(),
|
|
"type": 1,
|
|
},
|
|
}
|
|
resp, err := p.doRequest(ctx, "jd.union.open.promotion.common.get", biz)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
var d struct {
|
|
Result string `json:"jd_union_open_promotion_common_get_responce"`
|
|
}
|
|
if err := json.Unmarshal(resp, &d); err != nil {
|
|
return "", err
|
|
}
|
|
var inner struct {
|
|
Result []struct {
|
|
Data struct {
|
|
ClickURL string `json:"clickURL"`
|
|
} `json:"data"`
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
} `json:"result"`
|
|
}
|
|
if err := json.Unmarshal([]byte(d.Result), &inner); err != nil {
|
|
return "", err
|
|
}
|
|
if len(inner.Result) == 0 || inner.Result[0].Data.ClickURL == "" {
|
|
return "", fmt.Errorf("京东转链失败: %s", firstResultMsg(inner.Result))
|
|
}
|
|
return inner.Result[0].Data.ClickURL, nil
|
|
}
|
|
|
|
func firstResultMsg(results []struct {
|
|
Data struct {
|
|
ClickURL string `json:"clickURL"`
|
|
} `json:"data"`
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
}) string {
|
|
if len(results) == 0 {
|
|
return "empty result"
|
|
}
|
|
return results[0].Message
|
|
}
|
|
|
|
// doRequest 京东签名请求(sign = MD5(secret + 参数键值排序拼接 + secret),大写)
|
|
func (p jdProvider) doRequest(ctx context.Context, method string, biz map[string]any) ([]byte, error) {
|
|
appKey := g.Cfg().MustGet(ctx, "cps.jd_appkey", "").String()
|
|
secret := g.Cfg().MustGet(ctx, "cps.jd_secret", "").String()
|
|
payload, err := json.Marshal(biz)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
params := map[string]string{
|
|
"method": method,
|
|
"app_key": appKey,
|
|
"timestamp": time.Now().Format("2006-01-02 15:04:05"),
|
|
"format": "json",
|
|
"v": "1.0",
|
|
"sign_method": "md5",
|
|
"360buy_param_json": string(payload),
|
|
}
|
|
keys := make([]string, 0, len(params))
|
|
for k := range params {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
var sb strings.Builder
|
|
sb.WriteString(secret)
|
|
for _, k := range keys {
|
|
sb.WriteString(k)
|
|
sb.WriteString(params[k])
|
|
}
|
|
sb.WriteString(secret)
|
|
sum := md5.Sum([]byte(sb.String()))
|
|
params["sign"] = strings.ToUpper(hex.EncodeToString(sum[:]))
|
|
|
|
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))
|
|
}
|
|
return body, nil
|
|
}
|
|
|
|
func (p jdProvider) parseProducts(body []byte, catCode string) ([]CpsProduct, error) {
|
|
var d struct {
|
|
Result string `json:"jd_union_open_goods_query_responce"`
|
|
}
|
|
if err := json.Unmarshal(body, &d); err != nil {
|
|
return nil, fmt.Errorf("京东选品响应解析失败: %v", err)
|
|
}
|
|
var inner struct {
|
|
Result []struct {
|
|
SkuID int64 `json:"skuId"`
|
|
SkuName string `json:"skuName"`
|
|
ImageURL string `json:"imageUrl"`
|
|
ShopName string `json:"shopName"`
|
|
PriceInfo struct {
|
|
Price float64 `json:"price"`
|
|
} `json:"priceInfo"`
|
|
CommissionInfo struct {
|
|
Commission float64 `json:"commission"`
|
|
} `json:"commissionInfo"`
|
|
CategoryInfo struct {
|
|
Cid1 int64 `json:"cid1"`
|
|
} `json:"categoryInfo"`
|
|
} `json:"result"`
|
|
}
|
|
if err := json.Unmarshal([]byte(d.Result), &inner); err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]CpsProduct, 0, len(inner.Result))
|
|
for _, it := range inner.Result {
|
|
rate := 0
|
|
if it.PriceInfo.Price > 0 {
|
|
rate = int(it.CommissionInfo.Commission / it.PriceInfo.Price * 10000)
|
|
}
|
|
out = append(out, CpsProduct{
|
|
Source: p.Source(),
|
|
OuterId: strconv.FormatInt(it.SkuID, 10),
|
|
CategoryCode: catCode,
|
|
Name: it.SkuName,
|
|
CoverUrl: it.ImageURL,
|
|
PriceFen: int64(it.PriceInfo.Price * 100),
|
|
ShopName: it.ShopName,
|
|
CommissionRate: rate,
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|