555 lines
16 KiB
Go
555 lines
16 KiB
Go
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
|
||
}
|