common/pkg/coach-client/us.go

235 lines
6.5 KiB
Go

package coach_client
import (
"context"
"fmt"
"log/slog"
"net/url"
"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/pkg/errors"
proxy2 "github.com/timerzz/proxypool/pkg/proxy"
)
const (
BaseUrl_Coach = "https://www.coach.com"
BaseUrl_CoachOutlet = "https://www.coachoutlet.com"
)
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,
}
}
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)
}
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) 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
}