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

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
}