package mtwmapi import ( "regexp" "strings" "git.rosy.net.cn/baseapi/utils" ) const ( MaxSkuNameCharCount = 30 // SkuName的最大字符数 MaxStoreSkuBatchSize = 200 // retail/sku/stock, retail/sku/sellStatus和retail/sku/price这些批量操作的最大值 MaxRetailListPageSize = 200 MaxBatchDeleteSize = 100 // retailCat/batchdelete/catandretail这个接口的批量最大值 ) const ( SellStatusOnline = 0 // 上架 SellStatusOffline = 1 // 下架 ) var ( retailBatchFailedSkuReg = regexp.MustCompile(`((?:\d+;)+)`) ) type RetailCategoryInfo struct { Name string `json:"name"` Code string `json:"code"` Sequence int `json:"sequence"` Level int `json:"level"` Children []*RetailCategoryInfo `json:"children"` } type RetailTag struct { ID int64 `json:"id"` Name string `json:"name"` Level int `json:"level"` NamePath string `json:"namePath"` } type BareStoreSkuInfo struct { SkuID string `json:"sku_id"` Price string `json:"price,omitempty"` Stock string `json:"stock,omitempty"` } type BareStoreFoodInfo struct { AppFoodCode string `json:"app_food_code"` Skus []*BareStoreSkuInfo `json:"skus"` } type AvailableTimesInfo struct { Friday string `json:"friday"` Monday string `json:"monday"` Saturday string `json:"saturday"` Sunday string `json:"sunday"` Thursday string `json:"thursday"` Tuesday string `json:"tuesday"` Wednesday string `json:"wednesday"` } type SkuInfo struct { AvailableTimes *AvailableTimesInfo `json:"available_times"` BoxNum string `json:"box_num"` BoxPrice string `json:"box_price"` LocationCode string `json:"location_code"` Price string `json:"price"` SkuID string `json:"sku_id"` Spec string `json:"spec"` Stock string `json:"stock"` Upc string `json:"upc"` Weight int `json:"weight"` } type AppFood struct { AppFoodCode string `json:"app_food_code"` AppPoiCode string `json:"app_poi_code"` BoxNum float64 `json:"box_num"` BoxPrice float64 `json:"box_price"` CategoryCode string `json:"category_code"` CategoryName string `json:"category_name"` Ctime int `json:"ctime"` Description string `json:"description"` IsSp int `json:"isSp"` IsSoldOut int `json:"is_sold_out"` IsSpecialty int `json:"is_specialty"` MinOrderCount int `json:"min_order_count"` Name string `json:"name"` Picture string `json:"picture"` PictureList []string `json:"pictureList"` PictureContents string `json:"picture_contents"` Price float64 `json:"price"` SecondaryCategoryCode string `json:"secondary_category_code"` SecondaryCategoryName string `json:"secondary_category_name"` Sequence int `json:"sequence"` Skus string `json:"skus"` SkuList []*SkuInfo `json:"skuList"` TagID int `json:"tag_id"` Unit string `json:"unit"` Utime int `json:"utime"` ZhName string `json:"zh_name"` } type AppFoodResult struct { AppFoodCode string `json:"app_food_code"` ErrorMsg string `json:"error_msg"` } type AppFoodResult4SellStatus struct { AppFoodCode string `json:"appFoodCode"` Msg string `json:"msg"` } type Param4UpdateCat struct { CategoryCodeOrigin string `json:"category_code_origin,omitempty"` CategoryNameOrigin string `json:"category_name_origin,omitempty"` CategoryCode string `json:"category_code,omitempty"` SecondaryCategoryCode string `json:"secondary_category_code,omitempty"` SecondaryCategoryName string `json:"secondary_category_name,omitempty"` Sequence int `json:"sequence,omitempty"` TargetLevel string `json:"target_level,omitempty"` TargetParentName string `json:"target_parent_name,omitempty"` TopFlag string `json:"top_flag,omitempty"` WeeksTime string `json:"weeks_time,omitempty"` Period string `json:"period,omitempty"` } // 美团分类没有ID,就以名字为唯一标识,不论级别都必须不能重名 // name(和originName)的长度不能超过10个字符(字符,不是字节) // 创建一级分类,originName为空,name为新分类名,secondaryName为空 // 修改一级分类,originName为分类名,name为分类新名,secondaryName为空 // 创建二级分类,secondaryName为二级分类名, // (如果originName为空,同时创建一级分类,所以如果只是创建二级分类,originName与name要填一样的,此时sequence指的二级分类的sequence,一级分类的sequence为缺省值) // 修改二级分类,originName为二级分类名,name为二级分类新名,secondaryName为空 // https://developer.waimai.meituan.com/home/docDetail/71 // https://developer.waimai.meituan.com/home/questionDetail/4669 func (a *API) RetailCatUpdate(poiCode, catName string, updateParams *Param4UpdateCat) (err error) { if updateParams != nil { if updateParams.CategoryCodeOrigin != "" { updateParams.CategoryNameOrigin = "" } } params := utils.Struct2MapByJson(updateParams) params[KeyAppPoiCode] = poiCode params["category_name"] = catName _, err = a.AccessAPI("retailCat/update", false, params) return err } // 删除商品分类 // 当分类下存在子级分类或商品时,不允许直接删除此分类。 func (a *API) RetailCatDelete(poiCode, code, name string) (err error) { params := map[string]interface{}{ KeyAppPoiCode: poiCode, } if code != "" { params["category_code"] = code } else { params["category_name"] = name } _, err = a.AccessAPI("retailCat/delete", false, params) return err } func (a *API) RetailCatList(poiCode string) (retailCatList []*RetailCategoryInfo, err error) { result, err := a.AccessAPI("retailCat/list", true, map[string]interface{}{ KeyAppPoiCode: poiCode, }) if err == nil { return interface2CatList(result, 1, nil), nil } return nil, err } // offset 从0开始,limit最大不能超过200 // 返回的app_poi_code始终是空,手动建的商品app_food_code也为空(导致无法通过API删除) func (a *API) RetailList(poiCode string, offset, limit int) (foodList []*AppFood, err error) { result, err := a.AccessAPI("retail/list", true, map[string]interface{}{ KeyAppPoiCode: poiCode, "offset": offset, "limit": limit, }) if err == nil { if err = utils.Map2StructByJson(result, &foodList, true); err == nil { for _, food := range foodList { utils.UnmarshalUseNumber([]byte(food.Skus), &food.SkuList) food.PictureList = strings.Split(food.Picture, ",") } } } return foodList, err } func (a *API) RetailListAll(poiCode string) (foodList []*AppFood, err error) { offset := 0 for { batchList, err2 := a.RetailList(poiCode, offset, GeneralMaxLimit) if err = err2; err == nil { foodList = append(foodList, batchList...) } if len(batchList) < GeneralMaxLimit { break } offset += GeneralMaxLimit } return foodList, err } func handleRetailBatchResult(result interface{}) (failedFoodList []*AppFoodResult, err error) { if msg, ok := result.(string); ok && msg != "" { err = utils.UnmarshalUseNumber([]byte(msg), &failedFoodList) } return failedFoodList, err } func handleRetailBatchResultByRegexp(result interface{}) (failedFoodList []*AppFoodResult, err error) { if msg, ok := result.(string); ok && msg != "" { findList := retailBatchFailedSkuReg.FindStringSubmatch(msg) if len(findList) == 2 { ids := strings.Split(strings.Trim(findList[1], ";"), ";") if len(ids) > 0 { failedFoodList = make([]*AppFoodResult, len(ids)) for k, v := range ids { failedFoodList[k] = &AppFoodResult{ AppFoodCode: v, ErrorMsg: "", } } } } } return failedFoodList, err } // 商品名最长30个字符(非字节) // 此函数可能创建相同foodCode的商品,如下问题的回复中有提到 // https://developer.waimai.meituan.com/home/myquestionDetail/6716 // 另外这个接口即使不指定operate_type为1,也可能报错:”商品spu名称在该店内分类中已存在“,原因就是已经存在两个相同的SKU了 func (a *API) RetailInitData(trackInfo, poiCode, foodCode string, params map[string]interface{}) (err error) { _, err = a.AccessAPI2("retail/initdata", false, utils.MergeMaps(map[string]interface{}{ KeyAppPoiCode: poiCode, KeyAppFoodCode: foodCode, }, params), resultKeyData, trackInfo) return err } func (a *API) RetailBatchInitData(trackInfo, poiCode string, foodDataList []map[string]interface{}) (failedFoodList []*AppFoodResult, err error) { result, err := a.AccessAPI2("retail/batchinitdata", false, map[string]interface{}{ KeyAppPoiCode: poiCode, "food_data": string(utils.MustMarshal(foodDataList)), }, resultKeyMsg, trackInfo) if err == nil { failedFoodList, err = handleRetailBatchResult(result) } return failedFoodList, err } func (a *API) RetailSkuPrice(trackInfo, poiCode string, foodData []*BareStoreFoodInfo) (failedFoodList []*AppFoodResult, err error) { result, err := a.AccessAPI2("retail/sku/price", false, map[string]interface{}{ KeyAppPoiCode: poiCode, "food_data": string(utils.MustMarshal(foodData)), }, resultKeyMsg, trackInfo) if err == nil { failedFoodList, err = handleRetailBatchResult(result) } return failedFoodList, err } func (a *API) RetailSkuStock(trackInfo, poiCode string, foodData []*BareStoreFoodInfo) (failedFoodList []*AppFoodResult, err error) { result, err := a.AccessAPI2("retail/sku/stock", false, map[string]interface{}{ KeyAppPoiCode: poiCode, "food_data": string(utils.MustMarshal(foodData)), }, resultKeyMsg, trackInfo) if err == nil { failedFoodList, err = handleRetailBatchResult(result) } return failedFoodList, err } // retail/sku/sellStatus在部分失败时会返回错误,其它相应的批处理函数则会返回成功 // 此接口已准备废弃 // 2019年9月17日开放平台已上线新接口【retail/sellStatus】,用于零售类商家批量更新商品售卖状态。请已接入老接口(retail/sku/sellStatus)的开发者在2019年10月31日前完成接口迁移,使用新接口的请求地址https://waimaiopen.meituan.com/api/v1/retail/sellStatus。 // 开放平台将于2019年11月1日开始全面下线老接口(retail/sku/sellStatus),如开发者逾期未完成接口迁移,调用老接口失败所造成的相关问题或损失由商家自行承担。 func (a *API) RetailSkuSellStatus(trackInfo, poiCode string, foodData []*BareStoreFoodInfo, sellStatus int) (failedFoodList []*AppFoodResult, err error) { _, err = a.AccessAPI2("retail/sku/sellStatus", false, map[string]interface{}{ KeyAppPoiCode: poiCode, "food_data": string(utils.MustMarshal(foodData)), "sell_status": sellStatus, }, resultKeyMsg, trackInfo) if err != nil { if errExt, ok := err.(*utils.ErrorWithCode); ok { failedFoodList, _ = handleRetailBatchResultByRegexp(errExt.ErrMsg()) } } return failedFoodList, err } // 此接口部分失败也返回成功,但错误消息格式(errorMsg, appFoodCode)与其它两个不一样 func (a *API) RetailSellStatus(trackInfo, poiCode string, foodData []*BareStoreFoodInfo, sellStatus int) (failedFoodList []*AppFoodResult, err error) { result, err := a.AccessAPI2("retail/sellStatus", false, map[string]interface{}{ KeyAppPoiCode: poiCode, "food_data": string(utils.MustMarshal(foodData)), "sell_status": sellStatus, }, resultKeyMsg, trackInfo) if err == nil { var tmpFailedFoodList []*AppFoodResult4SellStatus if msg, ok := result.(string); ok && msg != "" { if err = utils.UnmarshalUseNumber([]byte(msg), &tmpFailedFoodList); err == nil { for _, v := range tmpFailedFoodList { failedFoodList = append(failedFoodList, &AppFoodResult{ AppFoodCode: v.AppFoodCode, ErrorMsg: v.Msg, }) } } } } return failedFoodList, err } func (a *API) RetailGet(poiCode, foodCode string) (food *AppFood, err error) { result, err := a.AccessAPI("retail/get", true, map[string]interface{}{ KeyAppPoiCode: poiCode, KeyAppFoodCode: foodCode, }) if err == nil { if err = utils.Map2StructByJson(result, &food, true); err == nil && food.Skus != "" { utils.UnmarshalUseNumber([]byte(food.Skus), &food.SkuList) food.PictureList = strings.Split(food.Picture, ",") } } return food, err } func (a *API) RetailSkuSave(poiCode, foodCode string, standardSkus, unstandardSkus []map[string]interface{}) (err error) { _, err = a.AccessAPI("retail/sku/save", false, map[string]interface{}{ KeyAppPoiCode: poiCode, KeyAppFoodCode: foodCode, "standard_skus": string(utils.MustMarshal(standardSkus)), "unstandard_skus": string(utils.MustMarshal(unstandardSkus)), }) return err } func (a *API) RetailDelete(trackInfo, poiCode, foodCode string) (err error) { _, err = a.AccessAPI2("retail/delete", false, map[string]interface{}{ KeyAppPoiCode: poiCode, KeyAppFoodCode: foodCode, }, resultKeyData, trackInfo) return err } func (a *API) RetailSkuDelete(trackInfo, poiCode, foodCode, skuID string) (err error) { _, err = a.AccessAPI2("retail/sku/delete", false, map[string]interface{}{ KeyAppPoiCode: poiCode, KeyAppFoodCode: foodCode, "sku_id": skuID, }, resultKeyData, trackInfo) return err } // 就是厂商商品类别 func (a *API) RetailGetSpTagIds() (tagIds []*RetailTag, err error) { result, err := a.AccessAPI("retail/getSpTagIds", true, nil) if err == nil { if err = utils.Map2StructByJson(result, &tagIds, false); err == nil { return tagIds, nil } } return nil, err } // 此接口将申请授权后方可接入 func (a *API) RetailCatSkuBatchDelete(trackInfo, poiCode string, catNames []string, secondaryCatNames []string, foodCodes []string) (err error) { params := map[string]interface{}{ KeyAppPoiCode: poiCode, } if len(catNames) > 0 { params["category_names"] = strings.Join(catNames, ",") } if len(secondaryCatNames) > 0 { params["secondary_category_names"] = strings.Join(secondaryCatNames, ",") } if len(foodCodes) > 0 { params["app_food_codes"] = strings.Join(foodCodes, ",") } _, err = a.AccessAPI2("retailCat/batchdelete/catandretail", false, params, resultKeyData, trackInfo) return err } // 批量删除商品分类及商品 // https://developer.waimai.meituan.com/home/docDetail/286 func (a *API) RetailCatSkuBatchDelete2(trackInfo, poiCode string, catCodes, catNames, secondaryCatCodes, secondaryCatNames, foodCodes []string) (err error) { params := map[string]interface{}{ KeyAppPoiCode: poiCode, } if len(catCodes) > 0 { params["category_codes"] = strings.Join(catCodes, ",") } else if len(catNames) > 0 { params["category_names"] = strings.Join(catNames, ",") } if len(secondaryCatCodes) > 0 { params["secondary_category_codes"] = strings.Join(secondaryCatCodes, ",") } else if len(secondaryCatNames) > 0 { params["secondary_category_names"] = strings.Join(secondaryCatNames, ",") } if len(foodCodes) > 0 { params["app_food_codes"] = strings.Join(foodCodes, ",") } _, err = a.AccessAPI2("retailCat/batchdelete/catandretail", false, params, resultKeyData, trackInfo) return err } ////////////////////// // 私有辅助函数 func interface2Cat(data interface{}, level int) (cat *RetailCategoryInfo) { mapData, ok := data.(map[string]interface{}) if ok { cat = &RetailCategoryInfo{ Name: utils.Interface2String(mapData["name"]), Code: utils.Interface2String(mapData["code"]), Sequence: int(utils.Interface2Int64WithDefault(mapData["sequence"], 0)), Level: level, } children := mapData["children"] if children != nil { cat.Children = interface2CatList(children, level+1, nil) } } return cat } func interface2CatList(data interface{}, level int, interface2CatHandler func(data interface{}, level int) (cat *RetailCategoryInfo)) (cats []*RetailCategoryInfo) { if interface2CatHandler == nil { interface2CatHandler = interface2Cat } maps, ok := data.([]interface{}) if ok { cats = make([]*RetailCategoryInfo, len(maps)) for index, v := range maps { cats[index] = interface2CatHandler(v, level) } } return cats } func IsErrCategoryExist(err error) (isExist bool) { return utils.IsErrMatch(err, utils.Int2Str(ErrCodeSkuCategoryExist), nil) } func IsErrCategoryNotExist(err error) (isNotExist bool) { return utils.IsErrMatch(err, utils.Int2Str(ErrCodeSkuCategoryNotExist), nil) || utils.IsErrMatch(err, utils.Int2Str(ErrCodeParameterFormatWrong), []string{ "门店内不存在该分类", }) } func IsErrSkuNotExist(err error) (isExist bool) { return utils.IsErrMatch(err, utils.Int2Str(ErrCodeNoAppFood), nil) }