package ebaiapi import ( "fmt" "regexp" "strings" "git.rosy.net.cn/baseapi/utils" ) const ( SkuStatusOnline = 1 // 为上架 SkuStatusOffline = 0 // 为下架 SkuStatusDeleted = 2 // 为删除 ) const ( UPCTypeStandard = 1 UPCTypePrivate = 0 ) const ( MaxLeftNum = 99999 MaxSkuNameByteCount = 100 // skuname的最大字节数,注意不是字符数,超长饿百会报错:{"data":"","errno":20200,"error":"invalid param:[name]... MaxStoreSkuBatchSize = 100 // sku.offline, sku.online, sku.price.update.batch和sku.stock.update.batch这些批量操作的最大值 MaxSkuCatRank = 100000 // 商品排序优先级 ,最小值为1,最大值为100000,rank值较大的优先展示 MaxCatCatRank = 100000 // 自定义分类排序优先级 ,最小值为1,最大值为100000,rank值较大的优先展示 MaxSkuListPageSize = 100 //获取数量,1-100,默认20 MaxCategoryNameLen = 12 // 商家分类名最大长度 ) var ( shopSkuBatchFailedSkuReg = regexp.MustCompile(`请核查以下sku:(\{.*\})`) skuExistMsgList = []string{"商品已存在", "shop sku exist", "已被使用,请更换"} skuNotExistMsgList = []string{"sku_id与shop_id不匹配", "SKU不存在或者已经被删除", "商品不存在"} ) type CategoryInfo struct { CategoryID int64 `json:"category_id"` Name string `json:"name"` Rank int `json:"rank"` // 店内分类独有 Children []*CategoryInfo `json:"children"` Level int `json:"level"` } type SkuListParams struct { Page int `json:"page,omitempty"` PageSize int `json:"pagesize,omitempty"` Upc string `json:"upc,omitempty"` SkuID int64 `json:"sku_id,omitempty"` CustomSkuID string `json:"custom_sku_id,omitempty"` //UpcType int `json:"upc_type,omitempty"` GetUncate int `json:"get_uncate,omitempty"` Delete int `json:"delete,omitempty"` Enabled int `json:"enabled,omitempty"` StartTime int `json:"start_time,omitempty"` EndTime int `json:"end_time,omitempty"` SkuIdOffset int `json:"sku_id_offset,omitempty"` } type SkuPhotoInfo struct { IsMaster int `json:"is_master"` URL string `json:"url"` } type SkuCustomCatInfo struct { CustomCatID string `json:"custom_cat_id"` CustomCatName string `json:"custom_cat_name"` } type SkuInfo struct { CateId int `json:"cate_id"` CateId1 int `json:"cate_id1"` CateId2 int `json:"cate_id2"` CateName string `json:"cate_name"` CateName1 string `json:"cate_name1"` CateName2 string `json:"cate_name2"` CustomCatIds string `json:"custom_cat_ids"` CustomSkuId string `json:"custom_sku_id"` DurationSaleFlag bool `json:"duration_sale_flag"` IsInActivity int `json:"is_in_activity"` ItemId int64 `json:"item_id"` LeftNum int `json:"left_num"` MarketPrice int `json:"market_price"` Minimum int `json:"minimum"` Name string `json:"name"` NeedIce string `json:"need_ice"` PackageFlag bool `json:"package_flag"` Photos []struct { IsMaster int `json:"is_master"` Url string `json:"url"` } `json:"photos"` PreminusWeight int `json:"preminus_weight"` PreparationTime int `json:"preparation_time"` PrescriptionType string `json:"prescription_type"` ProcessDetail []interface{} `json:"process_detail"` ProcessType int `json:"process_type"` ProductionAddr1 string `json:"production_addr1"` ProductionAddr2 string `json:"production_addr2"` ProductionAddr3 string `json:"production_addr3"` Rtf string `json:"rtf"` SalePrice int `json:"sale_price"` SaleStep int `json:"sale_step"` SaleUnit string `json:"sale_unit"` SevenDaysNoReason bool `json:"seven_days_no_reason"` SkuId int64 `json:"sku_id"` SkuProperty []interface{} `json:"sku_property"` Status string `json:"status"` Summary string `json:"summary"` Upc string `json:"upc"` UpcType int `json:"upc_type"` Weight string `json:"weight"` WeightFlag int `json:"weight_flag"` CustomCatIDs string `json:"custom_cat_ids"` CustomCatList []*SkuCustomCatInfo `json:"custom_cat_list"` CustomSkuID string `json:"custom_sku_id"` ShelfNumber string `json:"shelf_number"` UpdateTime string `json:"update_time"` } type PageDataInfo struct { Total int `json:"Total"` Page int `json:"Page"` Pages int `json:"Pages"` SkuIdOffset int `json:"sku_id_offset"` List []*SkuInfo `json:"List"` } type BatchOpSkuResult struct { SkuID int64 `json:"sku_id"` Price int `json:"price,omitempty"` ErrorNo int `json:"error_no,omitempty"` ErrorMsg string `json:"error_msg,omitempty"` } type BatchOpResult struct { FailedList []*BatchOpSkuResult `json:"failed_list"` SuccessList []*BatchOpSkuResult `json:"success_list"` } const ( SkuIDTypeUnknown = 0 SkuIDTypeSkuID = 1 SkuIDTypeCustomSkuID = 2 SkuIDTypeUpc = 3 ) var ( priceUpdateKeyIDMap = map[int]string{ SkuIDTypeSkuID: "skuid_price", SkuIDTypeCustomSkuID: "custom_sku_id", SkuIDTypeUpc: "upc_price", } stockUpdateKeyIDMap = map[int]string{ SkuIDTypeSkuID: "skuid_stocks", SkuIDTypeCustomSkuID: "custom_sku_id", SkuIDTypeUpc: "upc_stocks", } ) type ShopSkuInfo struct { SkuID int64 CustomSkuID string Upc string SalePrice int64 MarketPrice int64 Stock int } type ShopSkuInfoList []*ShopSkuInfo func (v *ShopSkuInfo) GuessIDType() int { if v.SkuID > 0 { return SkuIDTypeSkuID } else if v.CustomSkuID != "" { return SkuIDTypeCustomSkuID } return SkuIDTypeUpc } func (v *ShopSkuInfo) ToSkuID(skuIDType int) (str string) { if skuIDType == SkuIDTypeUnknown { skuIDType = v.GuessIDType() } if skuIDType == SkuIDTypeSkuID { str = utils.Int64ToStr(v.SkuID) } else if skuIDType == SkuIDTypeCustomSkuID { str = v.CustomSkuID } else { str = v.Upc } return str } func (v *ShopSkuInfo) PriceString(skuIDType int) (str string) { str = v.ToSkuID(skuIDType) + ":" + utils.Int64ToStr(v.SalePrice) if v.MarketPrice > 0 { str += "," + utils.Int64ToStr(v.MarketPrice) } return str } func (v *ShopSkuInfo) StockString(skuIDType int) (str string) { str = v.ToSkuID(skuIDType) + ":" + utils.Int2Str(v.Stock) return str } func (l ShopSkuInfoList) PriceString(skuIDType int) (str string) { if len(l) > 0 { strList := make([]string, len(l)) for k, v := range l { strList[k] = v.PriceString(skuIDType) } str = strings.Join(strList, ";") } return str } func (l ShopSkuInfoList) StockString(skuIDType int) (str string) { if len(l) > 0 { strList := make([]string, len(l)) for k, v := range l { strList[k] = v.StockString(skuIDType) } str = strings.Join(strList, ";") } return str } func genSkuIDParams(skuIDstr string, customSkuID, upc string) map[string]interface{} { params := map[string]interface{}{} if skuIDstr != "" { params[KeySkuID] = skuIDstr } else if customSkuID != "" { params[KeyCustomSkuID] = customSkuID } else if upc != "" { params[KeyUPC] = upc } else { panic("skuID, customSkuID and upc are all no value!") } return params } // category相关的函数,shop_custom_id可重 func (a *API) ShopCategoryCreate(shopID string, parentID int64, name string, rank int) (catID int64, err error) { result, err := a.AccessAPI("sku.shop.category.create", map[string]interface{}{ KeyShopID: shopID, "parent_category_id": parentID, "name": name, "rank": rank, }) if err == nil { if result.Data != nil { return utils.Interface2Int64WithDefault((result.Data.(map[string]interface{})["category_id"]), 0), nil } } if err != nil && strings.Contains(err.Error(), "新增的店内分类已存在") { errStr := err.Error() startIndex := strings.Index(errStr, "(") endIndex := strings.Index(errStr, ")") return utils.Str2Int64(errStr[startIndex+3 : endIndex]), nil } return 0, err } func (a *API) ShopCategoryGet(shopID string) (cats []*CategoryInfo, err error) { result, err := a.AccessAPI("sku.shop.category.get", utils.Params2Map(KeyShopID, shopID)) if err == nil { if inMap, ok := result.Data.(map[string]interface{}); ok { cats := interface2CatList(inMap["categorys"], 1) return cats, nil } } return nil, err } func (a *API) ShopCategoryUpdate(shopID string, categoryID int64, name string, rank int) (err error) { _, err = a.AccessAPI("sku.shop.category.update", map[string]interface{}{ KeyShopID: shopID, "category_id": categoryID, "name": name, "rank": rank, }) return err } // 饿百的商家分类是标签的概念,不管有没有商品在使用都可以删除,且有子分类的也可以直接删除(子分类也相应一并级连被删除) func (a *API) ShopCategoryDelete(shopID string, categoryID int64) (err error) { _, err = a.AccessAPI("sku.shop.category.delete", map[string]interface{}{ KeyShopID: shopID, "category_id": categoryID, }) return err } func (a *API) SkuGetItemsByCategoryId(shopID string, categoryID int64) (skus []map[string]interface{}, err error) { result, err := a.AccessAPI("sku.getItemsByCategoryId", map[string]interface{}{ KeyShopID: shopID, "category_id": categoryID, }) if err == nil { return utils.Slice2MapSlice(result.Data.([]interface{})), nil } return nil, err } // 此函数查不到结果是不会报错的 func (a *API) SkuList(shopID string, params *SkuListParams) (skuInfo *PageDataInfo, err error) { params.PageSize = 100 paramMap := utils.Struct2FlatMap(params) paramMap[KeyShopID] = shopID paramMap["include_cate_info"] = 1 result, err := a.AccessAPI("sku.list", paramMap) if err == nil { err = utils.Map2StructByJson(result.Data, &skuInfo, true) } return skuInfo, err } // 饿百商品名可以相同,不会报错 func (a *API) SkuCreate(trackInfo, shopID string, customSkuID int64, params map[string]interface{}) (skuID int64, err error) { defParams := map[string]interface{}{ KeyShopID: shopID, KeyCustomSkuID: customSkuID, } if utils.IsNil(params["upc"]) { params["upc"] = "upc-" + utils.Int2Str(int(customSkuID)) } /*if params["brand_id"] == nil { defParams["brand_id"] = 0 } if params["brand_name"] == nil { defParams["brand_name"] = "无" // 很狗血的是,你还必须填个无才行。。。 }*/ params = utils.MergeMaps(params, defParams) result, err := a.AccessAPI2("sku.create", params, trackInfo) if err == nil && result.Data != nil { return utils.Interface2Int64WithDefault(result.Data.(map[string]interface{})[KeySkuID], 0), nil } else if err != nil && strings.Contains(err.Error(), "商品已存在") { skuIDStr := err.Error() start := strings.LastIndex(skuIDStr, "sku_id") end := strings.Index(skuIDStr, "level") return utils.Str2Int64(skuIDStr[start+7 : end-1]), nil } return 0, err } // 饿百商品名可以相同,不会报错 SkuCreate2 customSkuID改为string func (a *API) SkuCreate2(trackInfo, shopID string, customSkuID string, params map[string]interface{}) (skuID int64, err error) { defParams := map[string]interface{}{ KeyShopID: shopID, KeyCustomSkuID: customSkuID, } if utils.IsNil(params["upc"]) { params["upc"] = "upc-" + customSkuID } /*if params["brand_id"] == nil { defParams["brand_id"] = 0 } if params["brand_name"] == nil { defParams["brand_name"] = "无" // 很狗血的是,你还必须填个无才行。。。 }*/ params = utils.MergeMaps(params, defParams) result, err := a.AccessAPI2("sku.create", params, trackInfo) if err == nil && result.Data != nil { return utils.Interface2Int64WithDefault(result.Data.(map[string]interface{})[KeySkuID], 0), nil } else if err != nil && strings.Contains(err.Error(), "商品已存在") { skuIDStr := err.Error() start := strings.LastIndex(skuIDStr, "sku_id") end := strings.Index(skuIDStr, "level") return utils.Str2Int64(skuIDStr[start+7 : end-1]), nil } return 0, err } func (a *API) SkuUpdate(trackInfo, shopID string, ebaiSkuID int64, params map[string]interface{}) (skuID int64, err error) { defParams := map[string]interface{}{ KeyShopID: shopID, KeySkuID: ebaiSkuID, } result, err := a.AccessAPI2("sku.update", utils.MergeMaps(params, defParams), trackInfo) if err == nil { return utils.Interface2Int64WithDefault(result.Data.(map[string]interface{})[KeySkuID], 0), nil } return 0, err } func intIDs2Str(intIDs []int64) (str string) { return strings.Join(utils.Int64Slice2String(intIDs), ",") } func strIDs2Str(strIDs []string) (str string) { return strings.Join(strIDs, ",") } // 之前的处理方法 func handleShopSkuBatchResult(result *ResponseResult) (opResult *BatchOpResult, err error) { if dataStr, ok := result.Data.(string); ok && dataStr != "" { matchList := shopSkuBatchFailedSkuReg.FindStringSubmatch(dataStr) if len(matchList) == 2 { var failedMap map[string]interface{} if err = utils.UnmarshalUseNumber([]byte(dataStr), &failedMap); err == nil && len(failedMap) > 0 { opResult = &BatchOpResult{} for _, v := range failedMap { var skuID int64 if vStr, ok := v.(string); ok { skuID = utils.Str2Int64WithDefault(vStr, 0) } else { skuID = utils.Interface2Int64WithDefault(v, 0) } opResult.FailedList = append(opResult.FailedList, &BatchOpSkuResult{ SkuID: skuID, }) } } } } return opResult, err } func handleShopSkuBatchErr(err error) (opResult *BatchOpResult, outErr error) { outErr = err if errExt, ok := err.(*utils.ErrorWithCode); ok { if errExt.ErrMsg() != "" { var data interface{} if err2 := utils.UnmarshalUseNumber([]byte(errExt.ErrMsg()), &data); err2 == nil { if err2 = utils.Map2StructByJson(data, &opResult, true); err2 == nil { // 将以\u表示的字符串标准化,并去掉成功的 errExt.SetErrMsg(string(utils.MustMarshal(opResult.FailedList))) outErr = errExt } } } } return opResult, outErr } // 门店商品批量修改可售,价格,库存的API,在2019/07/26之前,除了SkuPriceUpdateBatch外,其它API在部分失败时会返回成功, // 失败信息在data中,但2019/07/26之后,行为与SkuPriceUpdateBatch一致了,即完全成功时,返回成功,没有失败成功明细, // 部分失败时,返回失败,详情在error中, // 另外需要注意的是SkuPriceUpdateBatch的失败明细中的skuID是int64,但其它几个是string... // 文档上说支持custom_sku_id,但实际好像只支持skuid // 饿百删除商品后,还要等待5分钟后才会归档商品,才会真正删除生效 func (a *API) SkuDelete(trackInfo, shopID string, skuIDs []int64, customSkuDs []string) (opResult *BatchOpResult, err error) { params := genSkuIDParams(intIDs2Str(skuIDs), strIDs2Str(customSkuDs), "") params[KeyShopID] = shopID result, err := a.AccessAPI2("sku.delete", params, trackInfo) if err == nil { opResult, err = handleShopSkuBatchResult(result) } else { opResult, err = handleShopSkuBatchErr(err) } return opResult, err } func (a *API) SkuOnline(trackInfo, shopID string, skuIDs []int64, customSkuDs, upcs []string) (opResult *BatchOpResult, err error) { params := genSkuIDParams(intIDs2Str(skuIDs), strIDs2Str(customSkuDs), strIDs2Str(upcs)) params[KeyShopID] = shopID result, err := a.AccessAPI2("sku.online", params, trackInfo) if err == nil { opResult, err = handleShopSkuBatchResult(result) } else { opResult, err = handleShopSkuBatchErr(err) } return opResult, err } func (a *API) SkuOnlineOne(trackInfo, shopID string, skuID int64, customSkuID, upc string) (err error) { params := genSkuIDParams(utils.Int64ToStrNoZero(skuID), customSkuID, upc) params[KeyShopID] = shopID _, err = a.AccessAPI2("sku.online.one", params, trackInfo) return err } func (a *API) SkuOffline(trackInfo, shopID string, skuIDs []int64, customSkuDs, upcs []string) (opResult *BatchOpResult, err error) { params := genSkuIDParams(intIDs2Str(skuIDs), strIDs2Str(customSkuDs), strIDs2Str(upcs)) params[KeyShopID] = shopID result, err := a.AccessAPI2("sku.offline", params, trackInfo) if err == nil { opResult, err = handleShopSkuBatchResult(result) } else { opResult, err = handleShopSkuBatchErr(err) } return opResult, err } func (a *API) SkuOfflineOne(trackInfo, shopID string, skuID int64, customSkuID, upc string) (err error) { params := genSkuIDParams(utils.Int64ToStrNoZero(skuID), customSkuID, upc) params[KeyShopID] = shopID _, err = a.AccessAPI2("sku.offline.one", params, trackInfo) return err } func (a *API) SkuPriceUpdateBatch(trackInfo, shopID string, priceList ShopSkuInfoList, skuIDType int) (opResult *BatchOpResult, err error) { params := map[string]interface{}{ KeyShopID: shopID, priceUpdateKeyIDMap[skuIDType]: priceList.PriceString(skuIDType), } result, err := a.AccessAPI2("sku.price.update.batch", params, trackInfo) if err == nil { opResult, err = handleShopSkuBatchResult(result) } else { opResult, err = handleShopSkuBatchErr(err) } return opResult, err } func (a *API) SkuPriceUpdateOne(trackInfo, shopID string, priceInfo *ShopSkuInfo) (opResult *BatchOpResult, err error) { skuIDType := priceInfo.GuessIDType() params := map[string]interface{}{ KeyShopID: shopID, priceUpdateKeyIDMap[skuIDType]: priceInfo.PriceString(skuIDType), } _, err = a.AccessAPI2("sku.price.update.one", params, trackInfo) return opResult, err } func (a *API) SkuStockUpdateBatch(trackInfo, shopID string, stockList ShopSkuInfoList, skuIDType int) (opResult *BatchOpResult, err error) { params := map[string]interface{}{ KeyShopID: shopID, stockUpdateKeyIDMap[skuIDType]: stockList.StockString(skuIDType), } result, err := a.AccessAPI2("sku.stock.update.batch", params, trackInfo) if err == nil { opResult, err = handleShopSkuBatchResult(result) } else { opResult, err = handleShopSkuBatchErr(err) } return opResult, err } func (a *API) SkuStockUpdateOne(trackInfo, shopID string, stockInfo *ShopSkuInfo) (err error) { skuIDType := stockInfo.GuessIDType() params := map[string]interface{}{ KeyShopID: shopID, stockUpdateKeyIDMap[skuIDType]: stockInfo.StockString(skuIDType), } _, err = a.AccessAPI2("sku.stock.update.one", params, trackInfo) return err } func (a *API) SkuShopCategoryMap(shopID string, skuID int64, customSkuID string, categoryID int64, rank int) (err error) { params := map[string]interface{}{ KeyShopID: shopID, KeyCategoryID: categoryID, } if skuID > 0 { params[KeySkuID] = skuID } else { params[KeyCustomSkuID] = customSkuID } if rank > 0 { params["rank"] = rank } _, err = a.AccessAPI("sku.shop.category.map", params) return err } // 饿百的这个API有点怪,虽然有shopID参数,但返回的链接可以跨店使用 func (a *API) SkuUploadRTF(shopID, rtfDetail string) (rtfURL string, err error) { params := map[string]interface{}{ KeyShopID: shopID, "rtf_detail": rtfDetail, } result, err := a.AccessAPI("sku.uploadrtf", params) if err == nil { rtfURL = utils.Interface2String(result.Data.(map[string]interface{})["url"]) } return rtfURL, err } func BuildRFTFromImgs(imgList ...string) string { imgList2 := make([]string, len(imgList)) for index, img := range imgList { imgList2[index] = fmt.Sprintf(`%s`, img, img) } return strings.Join(imgList2, "\n") } // func interface2CatList(data interface{}, level int) (cats []*CategoryInfo) { maps, ok := data.([]interface{}) if ok { cats = make([]*CategoryInfo, len(maps)) for index, v := range maps { cats[index] = interface2Cat(v, level) } } return cats } func interface2Cat(data interface{}, level int) (cat *CategoryInfo) { catMap := data.(map[string]interface{}) cat = &CategoryInfo{ CategoryID: utils.MustInterface2Int64(catMap["category_id"]), Name: utils.Interface2String(catMap["name"]), Rank: int(utils.MustInterface2Int64(catMap["rank"])), Children: interface2CatList(catMap["children"], level+1), Level: level, } return cat } func IsErrCategoryExist(err error) (isExist bool) { return utils.IsErrMatch(err, "1", []string{"商户已存在该自定义分类"}) } func IsErrCategoryNotExist(err error) (isNotExist bool) { return utils.IsErrMatch(err, "", []string{"不存在该自定义分类ID", "商户不存在该自定义分类"}) } func IsErrSkuExist(err error) (isExist bool) { return utils.IsErrMatch(err, "1", skuExistMsgList) } func IsErrSkuNotExist(err error) (isExist bool) { return utils.IsErrMatch(err, "1", skuNotExistMsgList) } func IsMsgSkuExist(msg string) bool { return utils.IsErrMsgMatch(msg, skuExistMsgList) } func IsMsgSkuNotExist(msg string) bool { return utils.IsErrMsgMatch(msg, skuNotExistMsgList) } func (a *API) GetEbaiSkuIDFromCustomID(shopID, customSkuID string) (ebaiSkuID int64) { if skuInfo, err2 := a.SkuList(shopID, &SkuListParams{ CustomSkuID: customSkuID, }); err2 == nil && skuInfo != nil && len(skuInfo.List) > 0 { ebaiSkuID = skuInfo.List[0].SkuId } return ebaiSkuID } func (a *API) GetEbaiCatIDFromName(shopID, catName string) (ebaiCatID int64) { if catList, err2 := a.ShopCategoryGet(shopID); err2 == nil { for _, v := range catList { if v.Name == catName { ebaiCatID = v.CategoryID break } for _, v2 := range v.Children { if v2.Name == catName { ebaiCatID = v2.CategoryID break } } } } return ebaiCatID } // CategoryAttrValueList 根据分类id获取商品属性 //https://open-retail.ele.me/#/apidoc/me.ele.retail:sku.category.property.list-3?aopApiCategory=item_cate&type=item_all func (a *API) CategoryAttrValueList(cat3Id int64, shopId string) (categoryAttrValueListResult []*CategoryAttr, err error) { result, err := a.AccessAPI("sku.category.property.list", map[string]interface{}{ "cat3_id": cat3Id, "shop_id": shopId, }) if err == nil && result.ErrNo == 0 { err = utils.UnmarshalUseNumber(utils.MustMarshal(result.Data), &categoryAttrValueListResult) } return categoryAttrValueListResult, err } type CategoryAttr struct { PropertyId int64 `json:"propertyId"` // 属性ID CategoryId int64 `json:"categoryId"` // 所属叶子类目ID PropertyName string `json:"propertyName"` // 属性名 Required bool `json:"required"` // 是否必选 MultiSelect bool `json:"multiSelect"` // 属性值是否多选 EnumProp bool `json:"enumProp"` // 是否枚举属性 InputProp bool `json:"inputProp"` // 是否可输入属性 SaleProp bool `json:"saleProp"` // 是否销售属性,销售属性只能用于生成多规格 SortOrder bool `json:"sortOrder"` // 排序值 MajorProp bool `json:"majorProp"` // 是否关键属性 PropertyValues []PropertyValue `json:"propertyValues"` // 属性值列表 } type PropertyValue struct { ValueId int64 `json:"ValueId"` // 属性值id ValueData string `json:"valueData"` // 属性值内容 SortOrder int64 `json:"sortOrder"` // 排序码 }