package transit import ( "base-service/app/dao" "base-service/utils" "bytes" "crypto/hmac" "crypto/md5" "crypto/sha1" "crypto/sha256" "crypto/sha512" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "strconv" "strings" "time" "unicode/utf8" ) // JdClient 顺丰快递客户端 type JdClient struct { config dao.TransitSend accessToken string customerCode string } const ( JdServicePrecheckOrder = "/ecap/v1/orders/precheck" //预处理创建订单 JdServiceOrder = "/ecap/v1/orders/create" //下订单 JdServiceOrderConfirm = "/ecap/v1/orders/cancel" //取消订单 JdServiceOrderQueryByMailNo = "/ecap/v1/orders/trace/query" //路由查询-通过运单号-通过订单号 JdServiceOrderEstimated = "/ecap/v1/orders/info/query" //预计派送时间 JdServiceOrderWaybills = "/cloud/print/render" //快递单下载 JdServiceBackUrl = "/jd/tracking/subscribe" //回调订阅地址 JdAlgorithm = "md5-salt" //加密方式 JdVersion = "2.0" JdBtoC = 1 JdCtoB = 2 JdSettleType = 3 //付款方式-月结 JdCodeFail = "fail" //失败标识 JdMaxOrderNumber = 8 //最多箱子数 JdProductCode = "ed-m-0001" //京东标快 支持 0-C2C;1-B2C;2-C2B JdRemarkMaxLength = 200 //最大备注长度 JdSheet76x130 = "jdkd76x130" // 以下是签收code JdStatusDeliverySign = "10034" JdStatusPartialDelivery = "10042" JdStatusDeliveryBigSign = "300015" JdLopDNBackECAP = "Tracking_JD" JdOrderECAP = "ECAP" JdPrintECAP = "jdcloudprint" ) // JdSingedCodeMap 已签收CODE https://open.sf-express.com/developSupport/734349?activeIndex=589678 var JdSingedCodeMap = map[string]bool{ JdStatusDeliverySign: true, JdStatusPartialDelivery: true, JdStatusDeliveryBigSign: true, } // NewJdClient 初始化顺丰客户端 func NewJdClient(info dao.TransitSend) *JdClient { other := utils.StrToStrArr(info.Other, "|") var accessToken, customerCode string for i := utils.NumberZero; i < len(other); i++ { tmp := strings.Split(other[i], ":") if len(tmp) != utils.NumberTwo { continue } switch tmp[0] { case "token": accessToken = tmp[1] break case "code": customerCode = tmp[1] break } } return &JdClient{info, accessToken, customerCode} } func (c *JdClient) Create(order dao.DeliverGoodsPrint) (TransitResult, error) { result, err := c.getCheck(order) if err != nil { return result, err } if result.ErrorCode != dao.StatusOkString { return result, nil } info := c.getCreateData(order, order.ProductItems) //发送请求 result, err = c.send(JdServiceOrder, info) if err != nil { utils.Logger.Error("Jd Create Order:", err) return result, err } if result.ErrorCode != dao.StatusOkString { utils.Logger.Error("Jd Create Order:", err) return result, nil } c.subscribe(result.WaybillNo[utils.NumberZero]) result.PostInfo = utils.StructToJson(info) return result, nil } // Query 查询物流 func (c *JdClient) Query(waybillCode string) (TransitResult, error) { data := map[string]interface{}{ //1:根据顺丰运单号查询,trackingNumber将被当作顺丰运单号处理 //2:根据客户订单号查询,trackingNumber将被当作客户订单号处理 //"trackingType": JdTrackingType, "waybillCode": waybillCode, "orderOrigin": JdBtoC, } //发送请求 result, err := c.send(JdServiceOrderQueryByMailNo, data) if err != nil { utils.Logger.Error("Jd Query Order:", err) return result, err } return result, nil } // DownSheet 下载面单 func (c *JdClient) DownSheet(orders []string, info dao.DeliverGoodsPrint) (TransitResult, error) { data := map[string]interface{}{ "customerCode": c.customerCode, "templateCode": JdSheet76x130, "operator": info.AdminName, //自定义操作人 "taskId": strconv.FormatInt(time.Now().UnixMilli(), 10), //自定义任务ID "printData": map[string]interface{}{ "orderNumber": info.OrderSn, //订单号 "carrierCode": "JD", "billCodeValue": orders[0], "billCodeType": "waybillCode", "customerPrintData": map[string]interface{}{ "productCode": JdProductCode, }, }, "outputConfig": map[string]interface{}{ "outputType": "1", //文件 "dataFormat": "1", //当fileFormat=1时,枚举值填1(1代表URL) "fileFormat": "1", //PDF }, } //生成需要提交的数据 //发送请求 result, err := c.sendDown(JdServiceOrderWaybills, data, orders[0]) if err != nil { utils.Logger.Error("Jd Create Order:", err) return result, err } result.PostInfo = utils.StructToJson(data) return result, nil } // Estimated 获取预计时间 func (c *JdClient) Estimated(orderId, card string) (TransitResult, error) { data := map[string]interface{}{ "customerCode": c.customerCode, "deliveryId": orderId, } utils.Logger.Error(data) //发送请求 result, err := c.send(JdServiceOrderEstimated, data) if err != nil { utils.Logger.Error("Jd estimated Order:", err) return result, err } utils.Logger.Error(result) result.PostInfo = utils.StructToJson(data) return result, nil } // Cancel 取消订单 func (c *JdClient) Cancel(waybillCode string) (TransitResult, error) { data := map[string]interface{}{ "waybillCode": waybillCode, "orderOrigin": JdBtoC, "cancelReason": "快递数据同步失败", "cancelReasonCode": "1", } //发送请求 result, err := c.send(JdServiceOrderConfirm, data) if err != nil { utils.Logger.Error("Jd cancel Order:", err) return result, err } result.PostInfo = utils.StructToJson(data) return result, nil } // Edit 修改订单 func (c *JdClient) Edit(waybillCode string) (TransitResult, error) { data := map[string]interface{}{ "waybillCode": waybillCode, "orderOrigin": JdBtoC, "cancelReason": "快递数据同步失败", "cancelReasonCode": "1", } //发送请求 result, err := c.send(JdServiceOrderConfirm, data) if err != nil { utils.Logger.Error("Jd cancel Order:", err) return result, err } result.PostInfo = utils.StructToJson(data) return result, nil } func (c *JdClient) sendPost(apiUri, lop string, postData map[string]interface{}) (string, error) { client := http.Client{} baseUri := c.config.AppUrl bodyArr := []map[string]interface{}{ postData, } body := utils.StructToJson(bodyArr) timestamp := time.Now().Format(time.DateTime) content := strings.Join([]string{ c.config.AppSecretKey, "access_token", c.accessToken, "app_key", c.config.AppCode, "method", apiUri, "param_json", body, "timestamp", timestamp, "v", JdVersion, c.config.AppSecretKey, }, "") sign, err := c.sign(JdAlgorithm, []byte(content), c.config.AppSecretKey) if err != nil { utils.Logger.Error("Jd:", err) return "", err } postUrl := baseUri + apiUri httpRequest, err := http.NewRequest("POST", postUrl, bytes.NewReader([]byte(body))) if err != nil { utils.Logger.Error("Jd:", err) return "", err } query := httpRequest.URL.Query() query.Add("LOP-DN", lop) query.Add("access_token", c.accessToken) query.Add("app_key", c.config.AppCode) query.Add("timestamp", timestamp) query.Add("v", JdVersion) query.Add("sign", sign) query.Add("algorithm", JdAlgorithm) httpRequest.URL.RawQuery = query.Encode() _, offset := time.Now().Zone() // lop-tz代表时区,为接口调用当地的时区;删去后默认为东八区 httpRequest.Header.Add("lop-tz", fmt.Sprintf("%d", offset/3600)) // 用于开放平台识别客户调用API方式,客户无需修改 httpRequest.Header.Add("User-Agent", "lop-http/go") httpRequest.Header.Add("content-type", "application/json;charset=utf-8") httpResponse, err := client.Do(httpRequest) if err != nil { utils.Logger.Error("Jd:", err) return "", err } defer func(Body io.ReadCloser) { err := Body.Close() if err != nil { utils.Logger.Error(err) } }(httpResponse.Body) b, err := io.ReadAll(httpResponse.Body) if err != nil { utils.Logger.Error("Jd:", err) return "", err } return string(b), nil } func (c *JdClient) sign(algorithm string, data []byte, secret string) (string, error) { if algorithm == "md5-salt" { h := md5.New() if _, err := h.Write(data); err != nil { return "", err } return fmt.Sprintf("%x", h.Sum(nil)), nil } else if algorithm == "HMacMD5" { h := hmac.New(md5.New, []byte(secret)) if _, err := h.Write(data); err != nil { return "", err } return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil } else if algorithm == "HMacSHA1" { h := hmac.New(sha1.New, []byte(secret)) if _, err := h.Write(data); err != nil { return "", err } return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil } else if algorithm == "HMacSHA256" { h := hmac.New(sha256.New, []byte(secret)) if _, err := h.Write(data); err != nil { return "", err } return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil } else if algorithm == "HMacSHA512" { h := hmac.New(sha512.New, []byte(secret)) if _, err := h.Write(data); err != nil { return "", err } return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil } return "", fmt.Errorf("algorithm %s not supported yet", algorithm) } func (c *JdClient) getCreateData(order dao.DeliverGoodsPrint, products []dao.DeliverGoodsProduct) map[string]interface{} { var weight, length, width, hight, volume float32 var remark []string for _, item := range products { weight += item.ProductWeight * float32(item.ProductNumber) length += item.ProductLength * float32(item.ProductNumber) width += item.ProductWidth * float32(item.ProductNumber) hight += item.ProductHeight * float32(item.ProductNumber) volume += item.ProductCubage * float32(item.ProductNumber) remark = append(remark, fmt.Sprintf("%s×%d", item.ProductName, item.ProductNumber)) } remarkStr := strings.Join(remark, ",") if utf8.RuneCountInString(remarkStr) > JdRemarkMaxLength { remarkStr = remarkStr[:JdRemarkMaxLength] } name := utils.GetConfigString("transit.name") if name == "" { name = "化妆品" } // 生成顺丰标准请求参数 data := map[string]interface{}{ "orderId": order.OrderSn, //订单号 "senderContact": map[string]interface{}{ //发货人信息 "name": c.config.Contact, "mobile": c.config.Mobile, "fullAddress": c.config.Address, }, "receiverContact": map[string]interface{}{ //收货人信息 "name": order.Addressee, "mobile": order.Mobile, "fullAddress": order.Address, }, "orderOrigin": JdBtoC, "customerCode": c.customerCode, // "productsReq": map[string]interface{}{ "productCode": JdProductCode, }, "settleType": JdSettleType, "cargoes": []map[string]interface{}{ { "name": name, "quantity": order.Number, "weight": weight, "length": length, "width": width, "hight": hight, "volume": volume, }, }, "extendProps": map[string]interface{}{ "autoSubscribe": "1", //1为订阅推送,0为不订阅推送 }, "remark": remarkStr, } return data } func (c *JdClient) getCheck(order dao.DeliverGoodsPrint) (TransitResult, error) { //生成map对象 info := c.getCheckCreateData(order, utils.NumberOne, order.ProductItems[:1]) //发送请求 result, err := c.send(JdServicePrecheckOrder, info) result.PostInfo = utils.StructToJson(info) return result, err } func (c *JdClient) getCheckCreateData(order dao.DeliverGoodsPrint, number int, products []dao.DeliverGoodsProduct) map[string]interface{} { var weight, length, width, hight, volume float32 for _, item := range products { weight += item.ProductWeight length += item.ProductLength width += item.ProductWidth hight += item.ProductHeight volume += item.ProductCubage } name := utils.GetConfigString("transit.name") if name == "" { name = "化妆品" } // 生成顺丰标准请求参数 data := map[string]interface{}{ "senderContact": map[string]interface{}{ "name": c.config.Contact, "mobile": c.config.Mobile, "fullAddress": c.config.Address, }, "receiverContact": map[string]interface{}{ "name": order.Addressee, "mobile": order.Mobile, "fullAddress": order.Address, }, "orderOrigin": JdBtoC, "customerCode": c.customerCode, // 你的订单号 "settleType": JdSettleType, "cargoes": []map[string]interface{}{ { "name": name, "quantity": number, "weight": weight, "length": length, "width": width, "hight": hight, "volume": volume, }, }, } return data } func (c *JdClient) send(uri string, postData map[string]interface{}) (TransitResult, error) { res := TransitResult{} // 发送请求 resultStr, err := c.sendPost(uri, JdOrderECAP, postData) if err != nil { utils.Logger.Error(err) return res, err } var result map[string]interface{} if err = json.Unmarshal([]byte(resultStr), &result); err != nil { utils.Logger.Error(err) return res, err } res.RequestInfo = resultStr res.ErrorCode = JdCodeFail if _, ok := result["code"]; ok { if result["success"].(bool) { res.ErrorCode = dao.StatusOkString } res.ApiErrorMsg = utils.GetStringByMap(result, "subMsg") if _, ok := postData["requestId"]; ok { res.RequestID = postData["requestId"].(string) } // 成功 → 提取运单号 if res.ErrorCode == dao.StatusOkString { res.WaybillNo = append(res.WaybillNo, utils.GetStringByMap(result["data"].(map[string]interface{}), "waybillCode")) } } else if _, ok := result["statusCode"]; ok { if result["statusCode"].(float64) == utils.NumberZero { res.ErrorCode = dao.StatusOkString } res.ApiErrorMsg = utils.GetStringByMap(result, "statusMessage") } if msgData, ok := result["data"].(map[string]interface{}); ok { res.RequestQuery = msgData } utils.Logger.Error(res) utils.Logger.Error(result) return res, nil } func (c *JdClient) subscribe(referenceNumber string) bool { data := map[string]interface{}{ "referenceNumber": referenceNumber, "referenceType": "20000", } res := TransitResult{} // 发送请求 resultStr, err := c.sendPost(JdServiceBackUrl, JdLopDNBackECAP, data) utils.Logger.Error(data) if err != nil { utils.Logger.Error(err) return false } var result map[string]interface{} if err = json.Unmarshal([]byte(resultStr), &result); err != nil { utils.Logger.Error(err) return false } res.RequestInfo = resultStr res.ErrorCode = JdCodeFail if _, ok := result["code"]; ok { if result["code"].(float64) == 1000 { res.ErrorCode = dao.StatusOkString } else { utils.Logger.Error(result) } res.ApiErrorMsg = utils.GetStringByMap(result, "msg") } utils.Logger.Error(resultStr) return true } func (c *JdClient) sendDown(uri string, postData map[string]interface{}, billCodeValue string) (TransitResult, error) { res := TransitResult{} // 发送请求 resultStr, err := c.sendPost(uri, JdPrintECAP, postData) if err != nil { return res, err } var result map[string]interface{} if err = json.Unmarshal([]byte(resultStr), &result); err != nil { return res, err } res.RequestInfo = resultStr res.ErrorCode = JdCodeFail if _, ok := result["code"]; ok { if result["code"].(float64) == 1000 { res.ErrorCode = dao.StatusOkString } res.ApiErrorMsg = utils.GetStringByMap(result, "msg") // 成功 → 获取内容 if res.ErrorCode == dao.StatusOkString { resultArr, ok := result["result"].([]interface{}) if ok && len(resultArr) > utils.NumberZero { firstItem, ok := resultArr[utils.NumberZero].(map[string]interface{}) if ok { // 3. 从第一条数据里取出 url url := utils.GetStringByMap(firstItem, "url") res.Files = append(res.Files, TransitFiles{ Url: url, WaybillNo: billCodeValue, }) } } } } if msgData, ok := result["data"].(map[string]interface{}); ok { res.RequestQuery = msgData } return res, nil }