459 lines
13 KiB
Go
459 lines
13 KiB
Go
package transit
|
|
|
|
import (
|
|
"base-service/app/dao"
|
|
"base-service/utils"
|
|
"crypto/md5"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"math/rand"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// SfClient 顺丰快递客户端
|
|
type SfClient struct {
|
|
config dao.TransitSend
|
|
}
|
|
|
|
const (
|
|
SfServiceOrder = "EXP_RECE_CREATE_ORDER" //下订单
|
|
SfServiceOrderConfirm = "EXP_RECE_UPDATE_ORDER" //取消订单
|
|
SfServiceOrderQueryByMailNo = "EXP_RECE_SEARCH_ROUTES" //路由查询-通过运单号-通过订单号
|
|
SfServiceOrderEstimated = "EXP_RECE_SEARCH_PROMITM" //预计派送时间
|
|
SfServiceOrderWaybills = "COM_RECE_CLOUD_PRINT_WAYBILLS" //快递单下载
|
|
|
|
SfContactTypeA = 1 //发货方
|
|
SfContactTypeB = 2 //收货方
|
|
SfCN = "CN"
|
|
SfLanguage = "zh-CN"
|
|
SfWaybillTypeMaster = 1 //母订单
|
|
SfWaybillTypeSublevel = 2 //子订单
|
|
SfTrackingType = 2 //顾客订单号
|
|
|
|
SfServiceOrderConfirmKey = 2 //取消订单
|
|
SfServiceCheckTypeCard = 2 //月结卡
|
|
|
|
// 以下是签收code
|
|
sfStatusThirdPartySign = "980"
|
|
sfStatusArriveHoldLost = "8000"
|
|
sfStatusExternalSign = "935"
|
|
sfStatusMultiPieceTakePhoto = "701"
|
|
sfStatusPartnerDelivered = "658"
|
|
sfStatusCustomerPickUp = "128"
|
|
sfStatusSigned = "80"
|
|
)
|
|
|
|
// SfSingedCodeMap 已签收CODE https://open.sf-express.com/developSupport/734349?activeIndex=589678
|
|
var SfSingedCodeMap = map[string]bool{
|
|
sfStatusThirdPartySign: true,
|
|
sfStatusArriveHoldLost: true,
|
|
sfStatusExternalSign: true,
|
|
sfStatusMultiPieceTakePhoto: true,
|
|
sfStatusPartnerDelivered: true,
|
|
sfStatusCustomerPickUp: true,
|
|
sfStatusSigned: true,
|
|
}
|
|
|
|
// NewSfClient 初始化顺丰客户端
|
|
func NewSfClient(info dao.TransitSend) *SfClient {
|
|
return &SfClient{info}
|
|
}
|
|
|
|
func (c *SfClient) Create(order dao.DeliverGoodsPrint) (TransitResult, error) {
|
|
//生成map对象
|
|
info := c.getCreateData(order)
|
|
//生成需要提交的数据
|
|
postData := c.getPostData(SfServiceOrder, info)
|
|
//发送请求
|
|
result, err := c.send(postData)
|
|
if err != nil {
|
|
utils.Logger.Error("SF Create Order:", err)
|
|
return result, nil
|
|
}
|
|
result.PostInfo = info
|
|
return result, nil
|
|
}
|
|
|
|
// Query 查询物流
|
|
func (c *SfClient) Query(orderId string) (TransitResult, error) {
|
|
data := map[string]interface{}{
|
|
//1:根据顺丰运单号查询,trackingNumber将被当作顺丰运单号处理
|
|
//2:根据客户订单号查询,trackingNumber将被当作客户订单号处理
|
|
"trackingType": SfTrackingType,
|
|
"trackingNumber": orderId,
|
|
}
|
|
bytes, _ := json.Marshal(data)
|
|
//生成需要提交的数据
|
|
postData := c.getPostData(SfServiceOrderQueryByMailNo, string(bytes))
|
|
//发送请求
|
|
result, err := c.send(postData)
|
|
if err != nil {
|
|
utils.Logger.Error("SF Query Order:", err)
|
|
return result, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// DownSheet 下载面单
|
|
func (c *SfClient) DownSheet(orders []string, info dao.DeliverGoodsPrint) (TransitResult, error) {
|
|
var documents []map[string]interface{}
|
|
sum := len(orders)
|
|
for key, order := range orders {
|
|
tmp := make(map[string]interface{})
|
|
tmp["masterWaybillNo"] = order
|
|
if sum > 1 {
|
|
tmp["branchWaybillNo"] = order
|
|
tmp["sum"] = sum
|
|
tmp["seq"] = key + 1
|
|
}
|
|
documents = append(documents, tmp)
|
|
if utils.GetConfigBool("transit.sf_test") {
|
|
break
|
|
}
|
|
}
|
|
|
|
data := map[string]interface{}{
|
|
"templateCode": c.config.PdfCode,
|
|
"documents": documents,
|
|
"version": "2.0",
|
|
"sync": true,
|
|
}
|
|
bytes, _ := json.Marshal(data)
|
|
//生成需要提交的数据
|
|
postData := c.getPostData(SfServiceOrderWaybills, string(bytes))
|
|
//发送请求
|
|
result, err := c.sendDown(postData)
|
|
if err != nil {
|
|
utils.Logger.Error("SF Create Order:", err)
|
|
return result, err
|
|
}
|
|
result.PostInfo = string(bytes)
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// Estimated 获取预计时间
|
|
func (c *SfClient) Estimated(orderId, card string) (TransitResult, error) {
|
|
data := map[string]interface{}{
|
|
"checkType": SfServiceCheckTypeCard,
|
|
"searchNo": orderId,
|
|
"checkNos": []string{card},
|
|
}
|
|
bytes, _ := json.Marshal(data)
|
|
//生成需要提交的数据
|
|
postData := c.getPostData(SfServiceOrderEstimated, string(bytes))
|
|
//发送请求
|
|
result, err := c.send(postData)
|
|
if err != nil {
|
|
utils.Logger.Error("SF estimated Order:", err)
|
|
return result, err
|
|
}
|
|
result.PostInfo = string(bytes)
|
|
return result, nil
|
|
}
|
|
|
|
// Cancel 取消订单
|
|
func (c *SfClient) Cancel(orderId string) (TransitResult, error) {
|
|
data := map[string]interface{}{
|
|
"dealType": SfServiceOrderConfirmKey,
|
|
"orderId": orderId,
|
|
}
|
|
bytes, _ := json.Marshal(data)
|
|
//生成需要提交的数据
|
|
postData := c.getPostData(SfServiceOrderConfirm, string(bytes))
|
|
//发送请求
|
|
result, err := c.send(postData)
|
|
if err != nil {
|
|
utils.Logger.Error("SF cancel Order:", err)
|
|
return result, err
|
|
}
|
|
result.PostInfo = string(bytes)
|
|
return result, nil
|
|
}
|
|
|
|
func (c *SfClient) createUUID() string {
|
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
uniq := fmt.Sprintf("%d%d", r.Int(), time.Now().UnixNano())
|
|
chars := fmt.Sprintf("%x", md5.Sum([]byte(uniq)))
|
|
|
|
// 格式:8-4-4-4-12
|
|
return fmt.Sprintf("%s-%s-%s-%s-%s",
|
|
chars[0:8],
|
|
chars[8:12],
|
|
chars[12:16],
|
|
chars[16:20],
|
|
chars[20:32],
|
|
)
|
|
}
|
|
|
|
func (c *SfClient) sendPost(apiUrl string, postData map[string]interface{}) (string, error) {
|
|
values := make(url.Values)
|
|
for k, v := range postData {
|
|
values.Set(k, fmt.Sprintf("%v", v))
|
|
}
|
|
|
|
client := &http.Client{Timeout: time.Duration(15*60) * time.Second}
|
|
req, _ := http.NewRequest("POST", apiUrl, strings.NewReader(values.Encode()))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded;charset=utf-8")
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
utils.Logger.Error("Sf:", err)
|
|
return "", err
|
|
}
|
|
defer func(Body io.ReadCloser) {
|
|
err := Body.Close()
|
|
if err != nil {
|
|
utils.Logger.Error(err)
|
|
}
|
|
}(resp.Body)
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return string(body), nil
|
|
}
|
|
|
|
func (c *SfClient) getCreateData(order dao.DeliverGoodsPrint) string {
|
|
name := utils.GetConfigString("transit.name")
|
|
if name == "" {
|
|
name = "化妆品"
|
|
}
|
|
// 生成顺丰标准请求参数
|
|
data := map[string]interface{}{
|
|
"cargoDesc": name,
|
|
"expressTypeId": dao.NumberTwo, //顺丰顺丰标快
|
|
"monthlyCard": c.config.CardNumber, // 你的月结卡号
|
|
"orderId": order.OrderSn, // 你的订单号
|
|
"language": SfLanguage,
|
|
"parcelQty": order.Number,
|
|
"isDocall": utils.GetConfigInt("transit.sf_call_type"),
|
|
"isReturnRoutelabel": dao.NumberOne, //是否返回路由标签: 默认1, 1:返回路由标签, 0:不返回;除部分特殊用户外,其余用户都默认返回
|
|
"payMethod": dao.NumberOne, //付款方式,支持以下值: 1:寄方付 2:收方付 3:第三方付
|
|
"waybillNoInfoList": []map[string]interface{}{},
|
|
"cargoDetails": []map[string]interface{}{},
|
|
// 寄件 + 收件
|
|
"contactInfoList": []map[string]interface{}{
|
|
// 寄件方 contactType=1
|
|
{
|
|
"country": SfCN,
|
|
"company": c.config.CompanyName,
|
|
"province": c.config.Province,
|
|
"city": c.config.City,
|
|
"county": c.config.County,
|
|
"address": c.config.Address,
|
|
"contact": c.config.Contact,
|
|
"mobile": c.config.Mobile,
|
|
"contactType": SfContactTypeA,
|
|
},
|
|
// 收件方 contactType=2
|
|
{
|
|
"country": SfCN,
|
|
"province": order.Province,
|
|
"city": order.City,
|
|
"county": order.County,
|
|
"address": order.Address,
|
|
"contact": order.Addressee,
|
|
"mobile": order.Mobile,
|
|
"contactType": SfContactTypeB,
|
|
},
|
|
},
|
|
"totalWeight": order.Weight, // 用你的真实重量
|
|
}
|
|
var nameInfo []string
|
|
if order.Number > dao.NumberZero {
|
|
num := utils.NumberZero
|
|
for _, item := range order.ProductItems {
|
|
nameInfo = append(nameInfo, fmt.Sprintf("%s*%d", item.ProductName, item.ProductNumber))
|
|
for i := utils.NumberZero; i < item.ProductNumber; i++ {
|
|
Type := SfWaybillTypeMaster
|
|
if num > utils.NumberZero {
|
|
Type = SfWaybillTypeSublevel
|
|
}
|
|
info := make(map[string]interface{})
|
|
info["waybillType"] = Type
|
|
if utils.GetConfigBool("transit.sf_test") {
|
|
info["waybillNo"] = "SF3191177193750"
|
|
}
|
|
info["boxNo"] = "Box" + fmt.Sprintf("%03d", num+1)
|
|
info["width"] = item.ProductWidth
|
|
info["height"] = item.ProductHeight
|
|
info["weight"] = item.ProductWeight
|
|
info["volume"] = item.ProductCubage
|
|
info["length"] = item.ProductLength
|
|
data["waybillNoInfoList"] = append(data["waybillNoInfoList"].([]map[string]interface{}), info)
|
|
num++
|
|
if utils.GetConfigBool("transit.sf_test") {
|
|
break
|
|
}
|
|
}
|
|
if utils.GetConfigBool("transit.sf_test") {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
cargoDetails := make(map[string]interface{})
|
|
cargoDetails["name"] = name
|
|
if len(nameInfo) > dao.NumberZero {
|
|
cargoDetails["name"] = strings.Join(nameInfo, ",")
|
|
data["remark"] = strings.Join(nameInfo, ",")
|
|
}
|
|
cargoDetails["count"] = order.Number
|
|
data["cargoDetails"] = append(data["cargoDetails"].([]map[string]interface{}), cargoDetails)
|
|
|
|
bytes, _ := json.Marshal(data)
|
|
return string(bytes)
|
|
}
|
|
|
|
func (c *SfClient) getPostData(serviceCode string, msgData string) map[string]interface{} {
|
|
requestID := c.createUUID()
|
|
timestamp := fmt.Sprintf("%d", time.Now().Unix())
|
|
|
|
// 签名逻辑:md5( urlencode(msgData + timestamp + code ) ) → base64
|
|
raw := url.QueryEscape(msgData + timestamp + c.config.AppSecretKey)
|
|
md5Val := md5.Sum([]byte(raw))
|
|
msgDigest := base64.StdEncoding.EncodeToString(md5Val[:])
|
|
|
|
return map[string]interface{}{
|
|
"partnerID": c.config.AppCode,
|
|
"requestID": requestID,
|
|
"serviceCode": serviceCode,
|
|
"timestamp": timestamp,
|
|
"msgDigest": msgDigest,
|
|
"msgData": msgData,
|
|
}
|
|
}
|
|
|
|
func (c *SfClient) send(postData map[string]interface{}) (TransitResult, error) {
|
|
res := TransitResult{}
|
|
// 发送请求
|
|
resultStr, err := c.sendPost(c.config.AppUrl, postData)
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
|
|
// 解析第一层JSON
|
|
var result map[string]interface{}
|
|
if err := json.Unmarshal([]byte(resultStr), &result); err != nil {
|
|
return res, err
|
|
}
|
|
|
|
res.RequestID = postData["requestID"].(string)
|
|
res.RequestInfo = resultStr
|
|
// 读取 apiResultData
|
|
if apiResultData, ok := result["apiResultData"].(string); ok && apiResultData != "" {
|
|
var dataMap map[string]interface{}
|
|
if err := json.Unmarshal([]byte(apiResultData), &dataMap); err != nil {
|
|
return res, err
|
|
}
|
|
|
|
res.ErrorCode = dataMap["errorCode"].(string)
|
|
res.ApiErrorMsg = utils.GetStringByMap(dataMap, "errorMsg")
|
|
res.ApiResponseID = utils.GetStringByMap(result, "apiResponseID")
|
|
// 成功 → 提取运单号
|
|
if res.ErrorCode == "S0000" {
|
|
res.ErrorCode = dao.StatusOkString
|
|
if msgData, ok := dataMap["msgData"].(map[string]interface{}); ok {
|
|
res.RequestQuery = msgData
|
|
if list, ok := msgData["waybillNoInfoList"].([]interface{}); ok {
|
|
for _, item := range list {
|
|
if m, ok := item.(map[string]interface{}); ok {
|
|
no := utils.GetStringByMap(m, "waybillNo")
|
|
if no != "" {
|
|
res.WaybillNo = append(res.WaybillNo, no)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// 错误情况
|
|
res.ErrorCode = utils.GetStringByMap(result, "apiResultCode")
|
|
if res.ErrorCode == "" {
|
|
res.ErrorCode = "A11111"
|
|
}
|
|
res.ApiErrorMsg = utils.GetStringByMap(result, "apiErrorMsg")
|
|
res.ApiResponseID = utils.GetStringByMap(result, "apiResponseID")
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func (c *SfClient) sendDown(postData map[string]interface{}) (TransitResult, error) {
|
|
res := TransitResult{}
|
|
// 发送请求
|
|
resultStr, err := c.sendPost(c.config.AppUrl, 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.RequestID = utils.GetStringByMap(postData, "requestID")
|
|
res.RequestInfo = resultStr
|
|
res.ApiResponseID = utils.GetStringByMap(result, "apiResponseID")
|
|
res.ApiErrorMsg = utils.GetStringByMap(result, "apiErrorMsg")
|
|
|
|
// 外层状态
|
|
apiResultCode := utils.GetStringByMap(result, "apiResultCode")
|
|
if apiResultCode == "A1000" {
|
|
// 接口调用成功
|
|
res.ErrorCode = dao.StatusOkString
|
|
} else {
|
|
res.ErrorCode = apiResultCode
|
|
return res, nil
|
|
}
|
|
|
|
apiResultData := utils.GetStringByMap(result, "apiResultData")
|
|
if apiResultData == "" {
|
|
return res, nil
|
|
}
|
|
|
|
var dataMap map[string]interface{}
|
|
if err := json.Unmarshal([]byte(apiResultData), &dataMap); err != nil {
|
|
return res, err
|
|
}
|
|
|
|
// ======================
|
|
// 正确解析顺丰云打印结构
|
|
// ======================
|
|
success := false
|
|
if val, ok := dataMap["success"].(bool); ok {
|
|
success = val
|
|
}
|
|
if success {
|
|
res.ErrorCode = dao.StatusOkString
|
|
// 读取 obj -> files
|
|
if obj, ok := dataMap["obj"].(map[string]interface{}); ok {
|
|
if files, ok := obj["files"].([]interface{}); ok {
|
|
for _, item := range files {
|
|
if m, ok := item.(map[string]interface{}); ok {
|
|
|
|
waybillNo := utils.GetStringByMap(m, "waybillNo")
|
|
res.Files = append(res.Files, TransitFiles{
|
|
File: utils.GetStringByMap(m, "waybillNo"),
|
|
Url: utils.GetStringByMap(m, "url"),
|
|
Token: utils.GetStringByMap(m, "token"),
|
|
WaybillNo: waybillNo,
|
|
})
|
|
|
|
res.WaybillNo = append(res.WaybillNo, waybillNo)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
res.ErrorCode = "FAIL"
|
|
}
|
|
|
|
return res, nil
|
|
}
|