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 }