271 lines
7.3 KiB
Go
271 lines
7.3 KiB
Go
package coach_client
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/url"
|
|
"os"
|
|
"time"
|
|
|
|
"gitea.timerzz.com/kedaya_haitao/common/pkg/proxy"
|
|
restry_pool "gitea.timerzz.com/kedaya_haitao/common/pkg/restry-pool"
|
|
"github.com/go-resty/resty/v2"
|
|
"github.com/golang/glog"
|
|
"github.com/pkg/errors"
|
|
proxy2 "github.com/timerzz/proxypool/pkg/proxy"
|
|
)
|
|
|
|
const (
|
|
BaseUrl_Coach = "https://www.coach.com"
|
|
BaseUrl_CoachOutlet = "https://www.coachoutlet.com"
|
|
)
|
|
|
|
const (
|
|
USClient_Type_Coach = "coach"
|
|
USClient_Type_Coach_Outlet = "coach_outlet"
|
|
)
|
|
|
|
const (
|
|
USClient_Type_Env_Key = "COACH_CLIENT_TYPE"
|
|
)
|
|
|
|
func LoadUSClientType() string {
|
|
clientType := os.Getenv(USClient_Type_Env_Key)
|
|
if clientType == "" {
|
|
glog.Fatal("加载coach客户端类型失败")
|
|
}
|
|
if clientType != USClient_Type_Coach && clientType != USClient_Type_Coach_Outlet {
|
|
glog.Fatal("加载coach客户端类型不正确")
|
|
}
|
|
return clientType
|
|
}
|
|
|
|
type USClientBuilder struct {
|
|
pool *proxy.ProxyPool
|
|
}
|
|
|
|
func US(pool *proxy.ProxyPool) *USClientBuilder {
|
|
return &USClientBuilder{
|
|
pool: pool,
|
|
}
|
|
}
|
|
|
|
func (u *USClientBuilder) Coach() USClient {
|
|
return &us{
|
|
pool: u.pool,
|
|
defaultTimeOut: 10 * time.Minute,
|
|
baseUrl: BaseUrl_Coach,
|
|
}
|
|
}
|
|
|
|
func (u *USClientBuilder) CoachOutlet() USClient {
|
|
return &us{
|
|
pool: u.pool,
|
|
defaultTimeOut: 10 * time.Minute,
|
|
baseUrl: BaseUrl_CoachOutlet,
|
|
}
|
|
}
|
|
|
|
func (u *USClientBuilder) CreateByType(clientType string) USClient {
|
|
if clientType == USClient_Type_Coach {
|
|
return u.Coach()
|
|
} else if clientType == USClient_Type_Coach_Outlet {
|
|
return u.CoachOutlet()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type USClient interface {
|
|
RequestInventory(ctx context.Context, pid string) (inv Inventory, err error)
|
|
RequestProductDetail(ctx context.Context, pid string) (data ProductData, err error)
|
|
ViewAllBags(ctx context.Context, page int) (resp PageDataResponse, err error)
|
|
BaseUrl() string
|
|
}
|
|
|
|
type us struct {
|
|
pool *proxy.ProxyPool
|
|
defaultTimeOut time.Duration
|
|
baseUrl string
|
|
}
|
|
|
|
func tryRequest(ctx context.Context, base, urlPath string, respData any, proxyGetter func() proxy2.Proxy) error {
|
|
for p := proxyGetter(); p != nil; p = proxyGetter() {
|
|
select {
|
|
case <-ctx.Done():
|
|
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
|
return fmt.Errorf("超时未获取到结果,链接:%s", urlPath)
|
|
}
|
|
return nil
|
|
default:
|
|
|
|
}
|
|
resp, err := callByProxy(ctx, p, base, urlPath, respData)
|
|
if err != nil {
|
|
slog.Debug(err.Error())
|
|
continue
|
|
}
|
|
if resp.StatusCode() != 200 {
|
|
slog.Debug(fmt.Errorf("请求错误:%d, %s", resp.StatusCode(), resp.Body()).Error())
|
|
continue
|
|
}
|
|
return nil
|
|
}
|
|
return errors.New("未获取到可用的代理")
|
|
}
|
|
|
|
func callByProxy(ctx context.Context, p proxy2.Proxy, base, urlPath string, result any) (*resty.Response, error) {
|
|
addr, err := proxy.UrlToMetadata(urlPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
subCtx, cancel := context.WithTimeout(ctx, time.Second*10)
|
|
defer cancel()
|
|
conn, err := proxy.ConnFromProxy(subCtx, p, addr)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "创建conn失败")
|
|
}
|
|
defer conn.Close()
|
|
|
|
var cli = restry_pool.GetRestyClient(conn)
|
|
defer restry_pool.PutRestyClient(cli)
|
|
return cli.SetBaseURL(base).R().SetResult(result).Get(urlPath)
|
|
}
|
|
|
|
type Inventory struct {
|
|
Id string `json:"id"`
|
|
Ats int `json:"ats"`
|
|
PreOrderable bool `json:"preorderable"`
|
|
BackOrderable bool `json:"backorderable"`
|
|
Orderable bool `json:"orderable"`
|
|
AllocationResetDate time.Time `json:"allocationResetDate,omitempty"`
|
|
Perpetual bool `json:"perpetual"`
|
|
StockLevel int `json:"stockLevel"`
|
|
}
|
|
|
|
type InventoryResponse struct {
|
|
Inventory struct {
|
|
Status string `json:"status"`
|
|
InventoryListID string `json:"inventoryListID"`
|
|
InventoryInfo Inventory `json:"inventoryInfo"`
|
|
} `json:"inventory"`
|
|
}
|
|
|
|
func (c *us) BaseUrl() string {
|
|
return c.baseUrl
|
|
}
|
|
|
|
func (c *us) RequestInventory(ctx context.Context, pid string) (inv Inventory, err error) {
|
|
sctx, cancel := context.WithTimeout(ctx, c.defaultTimeOut)
|
|
defer cancel()
|
|
var resp InventoryResponse
|
|
urlPath := fmt.Sprintf("/api/inventory?vgId=%s&includeVariantData=false", url.QueryEscape(pid))
|
|
err = tryRequest(sctx, c.baseUrl, urlPath, &resp, c.pool.RandomIterator())
|
|
inv = resp.Inventory.InventoryInfo
|
|
return
|
|
}
|
|
|
|
type ProductData struct {
|
|
Id string `json:"id"`
|
|
Name string `json:"name"`
|
|
Brand string `json:"brand"`
|
|
Inventory Inventory `json:"inventory"`
|
|
Url string `json:"url"`
|
|
MasterId string `json:"masterId"`
|
|
ImageGroups []ImageGroup `json:"imageGroups"`
|
|
Prices struct {
|
|
CurrentPrice float64 `json:"currentPrice"`
|
|
} `json:"prices"`
|
|
Remark string `json:"-"`
|
|
}
|
|
type ImageGroup struct {
|
|
Images []struct {
|
|
Src string `json:"src"`
|
|
Title string `json:"title"`
|
|
Alt string `json:"alt"`
|
|
} `json:"images"`
|
|
ViewType string `json:"viewType"`
|
|
}
|
|
type ProductDataResponse struct {
|
|
ProductData []*ProductData `json:"productsData"`
|
|
}
|
|
|
|
func (c *us) RequestProductDetail(ctx context.Context, pid string) (data ProductData, err error) {
|
|
sctx, cancel := context.WithTimeout(ctx, c.defaultTimeOut)
|
|
defer cancel()
|
|
var resp ProductDataResponse
|
|
urlPath := fmt.Sprintf("/api/get-products?ids=%s&includeInventory=false", url.QueryEscape(pid))
|
|
err = tryRequest(sctx, c.baseUrl, urlPath, &resp, c.pool.RandomIterator())
|
|
if len(resp.ProductData) == 0 && err != nil {
|
|
err = fmt.Errorf("获取详情信息为空")
|
|
return
|
|
}
|
|
if len(resp.ProductData) > 0 {
|
|
data = *resp.ProductData[0]
|
|
}
|
|
return
|
|
}
|
|
|
|
type PageDataResponse struct {
|
|
PageData struct {
|
|
Total int `json:"total"`
|
|
Page int `json:"page"`
|
|
TotalPages int `json:"totalPages"`
|
|
PageSize int `json:"pageSize"`
|
|
Products []Product `json:"products"`
|
|
} `json:"pageData"`
|
|
}
|
|
|
|
type Product struct {
|
|
Name string `json:"name"`
|
|
Colors []Color `json:"colors"`
|
|
VariantsOnSale []Variant `json:"variantsOnSale"`
|
|
}
|
|
|
|
type Color struct {
|
|
Id string `json:"id"` //"id": "IMDQC",
|
|
Text string `json:"text"` //颜色的描述
|
|
Orderable bool `json:"orderable"`
|
|
Media Media `json:"media"`
|
|
Url string `json:"url"` //"/products/eliza-flap-crossbody-bag-in-signature-canvas/CP009-IMDQC.html",
|
|
VgId string `json:"vgId"` // "vgId": "CP009-IMDQC",
|
|
MasterId string `json:"masterId"` //"masterId": "CP009"
|
|
}
|
|
|
|
type Media struct {
|
|
Thumbnail Thumbnail `json:"thumbnail"`
|
|
}
|
|
|
|
type Thumbnail struct {
|
|
Src string `json:"src"`
|
|
}
|
|
|
|
type Variant struct {
|
|
Id string `json:"id"`
|
|
OnSale bool `json:"onSale"`
|
|
Price Price `json:"price"`
|
|
}
|
|
|
|
type Price struct {
|
|
Sales Sales `json:"sales"`
|
|
MarkdownDiscPercent int `json:"markdownDiscPercent"`
|
|
}
|
|
|
|
type Sales struct {
|
|
Value float64 `json:"value"`
|
|
Currency string `json:"currency"`
|
|
Formatted string `json:"formatted"`
|
|
DecimalPrice string `json:"decimalPrice"`
|
|
}
|
|
|
|
func (c *us) ViewAllBags(ctx context.Context, page int) (resp PageDataResponse, err error) {
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
sctx, cancel := context.WithTimeout(ctx, c.defaultTimeOut)
|
|
defer cancel()
|
|
urlPath := fmt.Sprintf("/api/get-shop/bags/view-all?page=%d", page)
|
|
err = tryRequest(sctx, c.baseUrl, urlPath, &resp, c.pool.RandomIterator())
|
|
return
|
|
}
|