Files
baseapi/platformapi/mtwmapi/retail.go
2019-11-14 11:20:25 +08:00

467 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,omitempty"`
}
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 string `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
// 此接口并发访问,可能导致创建相同的分类,见如下问题回复
// https://developer.waimai.meituan.com/home/myquestionDetail/6931
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)
}