package mtpsapi import ( "crypto/sha1" "fmt" "git.rosy.net.cn/baseapi/platformapi" "git.rosy.net.cn/baseapi/utils" "net/http" "net/url" "sort" "strconv" "strings" ) const ( mtpsAPIURL = "https://peisongopen.meituan.com/api" signKey = "sign" ) const ( OrderStatusWaitingForSchedule = 0 // 0:待调度 OrderStatusAccepted = 20 // 20:已接单 OrderStatusPickedUp = 30 // 30:已取货 OrderStatusDeliverred = 50 // 50:已送达 OrderStatusCanceled = 99 // 99:已取消 ) const ( OrderExceptCodePhonePoweroff = 10001 OrderExceptCodePhoneSuspended = 10002 OrderExceptCodePhoneNobody = 10003 OrderExceptCodePhoneEmpty = 10004 OrderExceptCodePhoneWrong = 10005 OrderExceptCodePhoneOther = 10006 OrderExceptCodeAddressWrong = 10101 OrderExceptCodeAddressOutOfService = 10201 OrderExceptCodeUserReject = 10202 OrderExceptCodeUserRequestDelay = 10203 OrderExceptCodeStoreClosed = 10401 ) var ( OrderStatusName = map[int]string{ OrderExceptCodePhonePoweroff: "电话关机", OrderExceptCodePhoneSuspended: "电话停机", OrderExceptCodePhoneNobody: "无人接听", OrderExceptCodePhoneEmpty: "电话空号", OrderExceptCodePhoneWrong: "电话错误", OrderExceptCodePhoneOther: "电话其它原因", OrderExceptCodeAddressWrong: "地址错误", OrderExceptCodeAddressOutOfService: "地址超区", OrderExceptCodeUserReject: "用户拒收", OrderExceptCodeUserRequestDelay: "要求延迟配送", OrderExceptCodeStoreClosed: "门店关店", } ) const ( DeliveryServiceCodeSuperRapid = 4002 // 飞速达 DeliveryServiceCodeRapid = 100004 // 快速达 DeliveryServiceCodeIntime = 100006 // 及时达 DeliveryServiceCodeTogether = 4013 // 集中送 ) const ( PickupTypeClientSendToStation = 1 PickupTypeMtPick = 2 ) const ( OrderTypeASAP = 0 OrderTypeBook = 1 ) const ( CoordinateTypeMars = 0 // 火星坐标(高德,腾讯地图均采用火星坐标),此为默认项 CoordinateTypeBaidu = 1 // 百度坐标 ) // 错误码 const ( ResponseCodeSuccess = 0 ResponseCodeInvalidParam = 4 // 参数格式错误 ResponseCodeFlowControl = 11 // 接口流控 ResponseCodeShopNotExist = 112 // 门店不存在 ) // 取消原因 const ( CancelReasonClientActive = 101 CancelReasonClientChangeTimeOrAddress = 102 CancelReasonGoodsRelated = 103 CancelReasonMerchantOther = 199 CancelReasonMtpsAttitude = 201 CancelReasonRidderSendNotIntime = 202 CancelReasonRideerGetGoodNotIntime = 203 CancelReasonRideerMtpsOther = 299 CancelReasonRideerOther = 399 ) type OrderInfoCommon struct { DeliveryID int64 MtPeisongID string OrderID string CourierName string CourierPhone string } type OrderInfo struct { OrderInfoCommon Status int OperateTime int CancelReasonID int CancelReason string } type OrderResponse struct { MtPeisongID string `json:"mt_peisong_id"` DeliveryID int64 `json:"delivery_id"` OrderID string `json:"order_id"` DeliveryFee float64 `json:"delivery_fee"` } type ResponseResult struct { Code int `json:"code"` Message string `json:"message"` Data map[string]interface{} `json:"data"` } type CreateOrderByShopParam struct { // 以下为必要参数 DeliveryID int64 `json:"delivery_id"` OrderID string `json:"order_id"` ShopID string `json:"shop_id"` DeliveryServiceCode int `json:"delivery_service_code"` ReceiverName string `json:"receiver_name"` ReceiverAddress string `json:"receiver_address"` ReceiverPhone string `json:"receiver_phone"` ReceiverLng int `json:"receiver_lng"` ReceiverLat int `json:"receiver_lat"` GoodsValue float64 `json:"goods_value"` GoodsWeight float64 `json:"goods_weight"` // 以下为可选参数 CoordinateType int `json:"coordinate_type"` GoodsHeight float64 `json:"goods_height,omitempty"` GoodsWidth float64 `json:"goods_width,omitempty"` GoodsLength float64 `json:"goods_length,omitempty"` GoodsDetail string `json:"goods_detail,omitempty"` GoodsPickupInfo string `json:"goods_pickup_info,omitempty"` GoodsDeliveryInfo string `json:"goods_delivery_info,omitempty"` ExpectedPickupTime int64 `json:"expected_pickup_time,omitempty"` // 期望取货时间,时区为GMT+8,当前距离Epoch(1970年1月1日) 以秒计算的时间,即unix-timestamp。 // 期望送达时间,时区为GMT+8,当前距离Epoch(1970年1月1日) 以秒计算的时间,即unix-timestamp // 即时单:以发单时间 + 服务包时效作为期望送达时间(当天送服务包需客户指定期望送达时间) // 预约单:以客户传参数据为准(预约时间必须大于当前下单时间+服务包时效+3分钟) ExpectedDeliveryTime int64 `json:"expected_delivery_time,omitempty"` OrderType int `json:"order_type,omitempty"` PoiSeq string `json:"poi_seq,omitempty"` Note string `json:"note,omitempty"` CashOnDelivery float64 `json:"cash_on_delivery,omitempty"` CashOnPickup float64 `json:"cash_on_pickup,omitempty"` InvoiceTitle string `json:"invoice_title,omitempty"` } type GoodsItem struct { GoodCount int `json:"goodCount"` GoodName string `json:"goodName"` GoodPrice float64 `json:"goodPrice"` GoodUnit string `json:"goodUnit"` } type GoodsDetail struct { Goods []*GoodsItem `json:"goods"` } type API struct { platformapi.APICookie appKey string secret string token string client *http.Client config *platformapi.APIConfig } var ( exceedLimitCodes = map[int]int{ ResponseCodeFlowControl: 1, } ) func New(appKey, secret string, config ...*platformapi.APIConfig) *API { curConfig := platformapi.DefAPIConfig if len(config) > 0 { curConfig = *config[0] } return &API{ appKey: appKey, secret: secret, client: &http.Client{Timeout: curConfig.ClientTimeout}, config: &curConfig, } } func (a *API) signParams(params url.Values) string { keys := make([]string, 0) for k := range params { if k != signKey { keys = append(keys, k) } } sort.Strings(keys) finalStr := a.secret for _, key := range keys { valStr := strings.Join(params[key], "") if valStr != "" { finalStr += key + valStr } } // baseapi.SugarLogger.Debug(finalStr) return fmt.Sprintf("%x", sha1.Sum([]byte(finalStr))) } func (a *API) AccessAPI2(baseURL, action string, params map[string]interface{}) (retVal *ResponseResult, err error) { // if params == nil { // panic("params is nil!") // } params2 := utils.Map2URLValues(params) if baseURL == mtpsAPIURL { params2.Set("appkey", a.appKey) params2.Set("timestamp", utils.Int64ToStr(utils.GetCurTimestamp())) params2.Set("version", "1.0") params2.Set(signKey, a.signParams(params2)) } else if a.GetCookieCount() == 0 { return nil, fmt.Errorf("需要设置Store Cookie才能使用此方法") } // baseapi.SugarLogger.Debug(params2.Encode()) err = platformapi.AccessPlatformAPIWithRetry(a.client, func() *http.Request { request, _ := http.NewRequest(http.MethodPost, baseURL+"/"+action, strings.NewReader(params2.Encode())) request.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") request.Header.Set("Referer", "https://page.peisong.meituan.com/open/admin/poilist") if baseURL != mtpsAPIURL { request.Header.Set("csrfToken", a.token) a.FillRequestCookies(request) } return request }, a.config, func(response *http.Response, bodyStr string, jsonResult1 map[string]interface{}) (errLevel string, err error) { if jsonResult1 == nil { return platformapi.ErrLevelRecoverableErr, fmt.Errorf("mapData is nil") } code := int(utils.MustInterface2Int64(jsonResult1["code"])) retVal = &ResponseResult{ Code: code, } if code == ResponseCodeSuccess { if innerData, ok := jsonResult1["data"]; ok { retVal.Data, _ = innerData.(map[string]interface{}) } return platformapi.ErrLevelSuccess, nil } msgKey := "message" if baseURL != mtpsAPIURL { msgKey = "msg" } retVal.Message, _ = jsonResult1[msgKey].(string) newErr := utils.NewErrorIntCode(retVal.Message, code) if _, ok := exceedLimitCodes[code]; ok { return platformapi.ErrLevelExceedLimit, newErr } return platformapi.ErrLevelCodeIsNotOK, newErr }) return retVal, err } func (a *API) AccessAPI(action string, params map[string]interface{}) (retVal *ResponseResult, err error) { return a.AccessAPI2(mtpsAPIURL, action, params) } // 获取美团配送内部订单id,外部系统订单id,美团配送费用 func (a *API) result2OrderResponse(result *ResponseResult) (order *OrderResponse) { order = new(OrderResponse) order.MtPeisongID = result.Data["mt_peisong_id"].(string) order.DeliveryID = utils.MustInterface2Int64(result.Data["delivery_id"]) order.OrderID = result.Data["order_id"].(string) order.DeliveryFee = utils.MustInterface2Float64(result.Data["delivery_fee"]) return order } func (a *API) CreateOrderByShop(basicParams *CreateOrderByShopParam, addParams map[string]interface{}) (order *OrderResponse, err error) { params := utils.Struct2MapByJson(basicParams) params["goods_value"] = strconv.FormatFloat(basicParams.GoodsValue, 'f', 2, 64) params["goods_weight"] = strconv.FormatFloat(basicParams.GoodsWeight, 'f', 2, 64) allParams := utils.MergeMaps(params, addParams) if params["order_type"] != utils.Int2Str(OrderTypeBook) { delete(allParams, "expected_delivery_time") } if result, err := a.AccessAPI("order/createByShop", allParams); err != nil { return nil, err } else { return a.result2OrderResponse(result), nil } } // 创建订单 func (a *API) CreateOrderByShop2(basicParams *CreateOrderByShopParam) (order *OrderResponse, err error) { params := utils.Struct2MapByJson(basicParams) if basicParams.OrderType != OrderTypeBook { delete(params, "expected_delivery_time") } if result, err := a.AccessAPI("order/createByShop", params); err != nil { return nil, err } else { return a.result2OrderResponse(result), nil } } func (a *API) QueryOrderStatus(deliveryId int64, mtPeiSongId string) (retVal map[string]interface{}, err error) { params := map[string]interface{}{ "delivery_id": deliveryId, "mt_peisong_id": mtPeiSongId, } if result, err := a.AccessAPI("order/status/query", params); err != nil { return nil, err } else { return result.Data, nil } } func (a *API) CancelOrder(deliveryId int64, mtPeiSongId string, cancelReasonId int, cancelReason string) (result *OrderResponse, err error) { params := map[string]interface{}{ "delivery_id": deliveryId, "mt_peisong_id": mtPeiSongId, "cancel_reason_id": cancelReasonId, "cancel_reason": cancelReason, } if result, err := a.AccessAPI("order/delete", params); err != nil { return nil, err } else { return a.result2OrderResponse(result), nil } } func (a *API) simulateOrderBehavior(action string, deliveryId int64, mtPeiSongId string) (err error) { params := map[string]interface{}{ "delivery_id": deliveryId, "mt_peisong_id": mtPeiSongId, } _, err = a.AccessAPI("test/order/"+action, params) return err } func (a *API) SimulateArrange(deliveryId int64, mtPeiSongId string) (err error) { return a.simulateOrderBehavior("arrange", deliveryId, mtPeiSongId) } func (a *API) SimulatePickup(deliveryId int64, mtPeiSongId string) (err error) { return a.simulateOrderBehavior("pickup", deliveryId, mtPeiSongId) } func (a *API) SimulateDeliver(deliveryId int64, mtPeiSongId string) (err error) { return a.simulateOrderBehavior("deliver", deliveryId, mtPeiSongId) } func (a *API) SimulateRearrange(deliveryId int64, mtPeiSongId string) (err error) { return a.simulateOrderBehavior("rearrange", deliveryId, mtPeiSongId) } func (a *API) SimulateReportException(deliveryId int64, mtPeiSongId string) (err error) { return a.simulateOrderBehavior("reportException", deliveryId, mtPeiSongId) } //评价骑手 func (a *API) EvaluateRider(deliveryId int64, mtPeiSongId string, score int, commentContent string) (err error) { params := map[string]interface{}{ "delivery_id": deliveryId, "mt_peisong_id": mtPeiSongId, "score": score, "comment_content": commentContent, } _, err = a.AccessAPI("order/evaluate", params) return err } // 获取骑手当前位置 // https://peisong.meituan.com/open/doc#section2-9 func (a *API) RiderLocation(deliveryId int64, mtPeiSongId string) (lng, lat int, err error) { params := map[string]interface{}{ "delivery_id": deliveryId, "mt_peisong_id": mtPeiSongId, } result, err := a.AccessAPI("order/rider/location", params) if err == nil { lng = int(utils.ForceInterface2Int64(result.Data["lng"])) lat = int(utils.ForceInterface2Int64(result.Data["lat"])) } return lng, lat, err } type PreCreateByShopParam struct { DeliveryID int64 `json:"delivery_id"` OrderID string `json:"order_id"` OuterOrderSourceDesc string `json:"outer_order_source_desc"` //101.美团(外卖&闪购) 102.饿了么 103.京东到家 OuterOrderSourceNo string `json:"outer_order_source_no"` ShopID string `json:"shop_id"` DeliveryServiceCode int `json:"delivery_service_code"` ReceiverName string `json:"receiver_name"` ReceiverAddress string `json:"receiver_address"` ReceiverPhone string `json:"receiver_phone"` ReceiverLng int `json:"receiver_lng"` ReceiverLat int `json:"receiver_lat"` PayTypeCode int `json:"pay_type_code"` GoodsValue float64 `json:"goods_value"` GoodsWeight float64 `json:"goods_weight"` ExpectedDeliveryTime int64 `json:"expected_delivery_time,omitempty"` // 以下为可选参数 CoordinateType int `json:"coordinate_type"` GoodsHeight float64 `json:"goods_height,omitempty"` GoodsWidth float64 `json:"goods_width,omitempty"` GoodsLength float64 `json:"goods_length,omitempty"` GoodsDetail string `json:"goods_detail,omitempty"` GoodsPickupInfo string `json:"goods_pickup_info,omitempty"` GoodsDeliveryInfo string `json:"goods_delivery_info,omitempty"` ExpectedPickupTime int64 `json:"expected_pickup_time,omitempty"` // 期望取货时间,时区为GMT+8,当前距离Epoch(1970年1月1日) 以秒计算的时间,即unix-timestamp。 // 期望送达时间,时区为GMT+8,当前距离Epoch(1970年1月1日) 以秒计算的时间,即unix-timestamp // 即时单:以发单时间 + 服务包时效作为期望送达时间(当天送服务包需客户指定期望送达时间) // 预约单:以客户传参数据为准(预约时间必须大于当前下单时间+服务包时效+3分钟) OrderType int `json:"order_type,omitempty"` PoiSeq string `json:"poi_seq,omitempty"` Note string `json:"note,omitempty"` CashOnDelivery float64 `json:"cash_on_delivery,omitempty"` CashOnPickup float64 `json:"cash_on_pickup,omitempty"` InvoiceTitle string `json:"invoice_title,omitempty"` } // 美团预发单 func (a *API) PreCreateByShop(basicParams *PreCreateByShopParam) (deliveryFee, baseDeliveryFee int64, err error) { params := utils.Struct2MapByJson(basicParams) result, err := a.AccessAPI("order/preCreateByShop", params) if err != nil { return 0, 0, err } deliveryFee = utils.Float64TwoInt64(utils.MustInterface2Float64(result.Data["delivery_fee"]) * 100) // 订单配送价格 baseDeliveryFee = utils.Float64TwoInt64(utils.MustInterface2Float64(result.Data["base_delivery_fee"]) * 100) // 订单优惠价格 return deliveryFee, baseDeliveryFee, nil } // 预发单 func (a *API) CheckOrder(shopID, receiver_address string, receiver_lng, receiver_lat, mock_order_time int64) (result *ResponseResult, err error) { result, err = a.AccessAPI("order/check", map[string]interface{}{ "shop_id": shopID, "delivery_service_code": DeliveryServiceCodeRapid, "receiver_address": receiver_address, "receiver_lng": receiver_lng, "receiver_lat": receiver_lat, "mock_order_time": mock_order_time, "check_type": 1, }) return result, err } func IsOpen(err error) bool { if err == nil { return true } else if strings.Contains(err.Error(), "门店非营业状态") { return false } return false } func IsExist(err error) bool { if err == nil { return true } else if strings.Contains(err.Error(), "门店不存在") { return false } return false }