new create admin order,express add company

This commit is contained in:
2026-09-02 18:02:57 +08:00
parent 02819d159a
commit e0a63bbef9
38 changed files with 3023 additions and 265 deletions
+555
View File
@@ -0,0 +1,555 @@
package transit
import (
"bytes"
"crypto/hmac"
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"fmt"
"io"
"lone-services/pkg/utils"
"lone-services/services/express/internal/dao"
"net/http"
"strconv"
"strings"
"time"
"unicode/utf8"
jsoniter "github.com/json-iterator/go"
)
// JdClient 顺丰快递客户端
type JdClient struct {
config dao.ExpressSend
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.ExpressSend) *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.DeliveryOrderPrint) (TransitResult, error) {
result, err := c.getCheck(order)
if err != nil {
return result, err
}
if result.ErrorCode != utils.StringStatusOk {
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 != utils.StringStatusOk {
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.DeliveryOrderPrint) (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.DeliveryOrderPrint, products []dao.DeliveryOrderProduct) 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.DeliveryOrderPrint) (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.DeliveryOrderPrint, number int, products []dao.DeliveryOrderProduct) 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 = jsoniter.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 = utils.StringStatusOk
}
res.ApiErrorMsg = utils.GetStringByMap(result, "subMsg")
if _, ok := postData["requestId"]; ok {
res.RequestID = postData["requestId"].(string)
}
// 成功 → 提取运单号
if res.ErrorCode == utils.StringStatusOk {
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 = utils.StringStatusOk
}
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 = jsoniter.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 = utils.StringStatusOk
} 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 = jsoniter.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 = utils.StringStatusOk
}
res.ApiErrorMsg = utils.GetStringByMap(result, "msg")
// 成功 → 获取内容
if res.ErrorCode == utils.StringStatusOk {
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
}