Files
lone-services/utils/transit/jd.go
T
2026-08-05 17:37:32 +08:00

555 lines
16 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 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-C2C1-B2C2-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时,枚举值填11代表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
}