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 }