Files
slogan/server/styleagent/agent/cps_meituan.go
T
admin a6de9ebd12 Add 'server/' from commit 'e64421295fff83acbb6d6ab3d3b27f3ef8368f00'
git-subtree-dir: server
git-subtree-mainline: c4e617ada7
git-subtree-split: e64421295f
2026-08-04 15:02:35 +08:00

178 lines
4.8 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 agent
import (
"context"
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/gogf/gf/v2/frame/g"
)
// 美团联盟适配器(到店 OTA 类目:丽人/服装/餐厅/酒店/票务)
// 接口以 union.meituan.com 开放平台为准:选品(商品/POI 搜索)+ 转链(生成带 pid 的推广链接)
type meituanProvider struct{}
func (meituanProvider) Source() string { return "meituan_ota" }
func (meituanProvider) Enabled() bool {
ctx := context.Background()
return g.Cfg().MustGet(ctx, "cps.meituan_appkey", "").String() != "" &&
g.Cfg().MustGet(ctx, "cps.meituan_pid", "").String() != ""
}
func (meituanProvider) apiBase(ctx context.Context) string {
return strings.TrimRight(g.Cfg().MustGet(ctx, "cps.meituan_base",
"https://openapi.meituan.com").String(), "/")
}
// SyncProducts 到店 POI/商品选品(按类目 + 城市)
func (p meituanProvider) SyncProducts(ctx context.Context, city, catCode string) ([]CpsProduct, error) {
biz := map[string]any{
"cityName": city,
"categoryId": catCode,
"pageNo": 1,
"pageSize": 50,
"isActivity": 0,
"promotionPid": g.Cfg().MustGet(ctx, "cps.meituan_pid", "").String(),
}
resp, err := p.doRequest(ctx, "union/search", biz)
if err != nil {
return nil, err
}
return p.parseProducts(resp, catCode, city)
}
// Search 实时搜索兜底
func (p meituanProvider) Search(ctx context.Context, keyword, catCode string, page int) ([]CpsProduct, error) {
biz := map[string]any{
"keyword": keyword,
"categoryId": catCode,
"pageNo": page,
"pageSize": 20,
"promotionPid": g.Cfg().MustGet(ctx, "cps.meituan_pid", "").String(),
}
resp, err := p.doRequest(ctx, "union/search", biz)
if err != nil {
return nil, err
}
return p.parseProducts(resp, catCode, "")
}
// GetLink 转链(pid 归因)
func (p meituanProvider) GetLink(ctx context.Context, outerId string) (string, error) {
biz := map[string]any{
"poiId": outerId,
"promotionPid": g.Cfg().MustGet(ctx, "cps.meituan_pid", "").String(),
}
resp, err := p.doRequest(ctx, "union/link", biz)
if err != nil {
return "", err
}
var d struct {
Data struct {
Link string `json:"link"`
} `json:"data"`
}
if err := json.Unmarshal(resp, &d); err != nil {
return "", err
}
if d.Data.Link == "" {
return "", fmt.Errorf("美团转链返回空")
}
return d.Data.Link, nil
}
// doRequest 美团联盟签名请求(sign = MD5(appkey + secret + ts)
func (p meituanProvider) doRequest(ctx context.Context, path string, biz map[string]any) ([]byte, error) {
appKey := g.Cfg().MustGet(ctx, "cps.meituan_appkey", "").String()
secret := g.Cfg().MustGet(ctx, "cps.meituan_secret", "").String()
ts := fmt.Sprint(time.Now().Unix())
payload, err := json.Marshal(biz)
if err != nil {
return nil, err
}
h := md5.New()
io.WriteString(h, appKey+secret+ts)
sign := hex.EncodeToString(h.Sum(nil))
form := url.Values{}
form.Set("appkey", appKey)
form.Set("ts", ts)
form.Set("sign", sign)
form.Set("biz", string(payload))
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
p.apiBase(ctx)+"/"+path, 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", path, resp.StatusCode, string(body))
}
return body, nil
}
func (p meituanProvider) parseProducts(body []byte, catCode, city string) ([]CpsProduct, error) {
var d struct {
Data struct {
List []struct {
PoiId string `json:"poiId"`
Title string `json:"title"`
ImageUrl string `json:"imageUrl"`
LowPrice string `json:"lowPrice"`
ShopName string `json:"shopName"`
CpsCoupon int `json:"cpsCoupon"`
Category string `json:"category"`
} `json:"list"`
} `json:"data"`
}
if err := json.Unmarshal(body, &d); err != nil {
return nil, fmt.Errorf("美团选品响应解析失败: %v", err)
}
out := make([]CpsProduct, 0, len(d.Data.List))
for _, it := range d.Data.List {
price := parseFen(it.LowPrice)
out = append(out, CpsProduct{
Source: p.Source(),
OuterId: it.PoiId,
CategoryCode: catCode,
Name: it.Title,
CoverUrl: it.ImageUrl,
PriceFen: price,
ShopName: it.ShopName,
CommissionRate: it.CpsCoupon,
City: city,
})
}
return out, nil
}
// parseFen 金额字符串(元)→ 分
func parseFen(amount string) int64 {
f, err := strconv.ParseFloat(amount, 64)
if err != nil {
return 0
}
return int64(f * 100)
}