502 lines
17 KiB
Go
502 lines
17 KiB
Go
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) result2OrderCancelOrderRes(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)
|
||
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.result2OrderCancelOrderRes(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"`
|
||
|
||
// 以下为可选参数
|
||
|
||
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
|
||
}
|