1294 lines
41 KiB
Go
1294 lines
41 KiB
Go
package main
|
||
|
||
/*
|
||
#include <stdlib.h>
|
||
*/
|
||
import "C"
|
||
import (
|
||
"bytes"
|
||
"crypto/md5"
|
||
"encoding/base64"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"fmt"
|
||
"github.com/parnurzeal/gorequest"
|
||
"github.com/tjfoc/gmsm/sm4"
|
||
"net/http"
|
||
"sort"
|
||
"strings"
|
||
"time"
|
||
"unsafe"
|
||
)
|
||
|
||
// ZtoOrderRequest 中通订单请求结构体
|
||
type ZtoOrderRequest struct {
|
||
PartnerType string `json:"partnerType"` // 合作模式 ,1:集团客户;2:非集团客户
|
||
OrderType string `json:"orderType"` // partnerType为1时,orderType:1:全网件 2:预约件。partnerType为2时,orderType:1:全网件 2:预约件(返回运单号) 3:预约件(不返回运单号) 4:星联全网件
|
||
PartnerOrderCode string `json:"partnerOrderCode"` // 合作商订单号
|
||
AccountInfo AccountInfo `json:"accountInfo"` // 账号信息
|
||
BillCode string `json:"billCode"` // 运单号
|
||
SenderInfo SenderInfo `json:"senderInfo"` // 发件人信息
|
||
ReceiveInfo ReceiveInfo `json:"receiveInfo"` // 收件人信息
|
||
OrderVasList []OrderVas `json:"orderVasList"` // 增值服务信息
|
||
HallCode string `json:"hallCode"` // 门店/仓库编码(partnerType为1时可使用)
|
||
SiteCode string `json:"siteCode"` // 网点code(orderVasList.vasType为receiveReturnService必填)
|
||
SiteName string `json:"siteName"` // 网点名称(orderVasList.vasType为receiveReturnService必填)
|
||
SummaryInfo SummaryInfo `json:"summaryInfo"` // 汇总信息
|
||
Remark string `json:"remark"` // 备注
|
||
OrderItems []OrderItem `json:"orderItems"` // 物品信息
|
||
Cabinet Cabinet `json:"cabinet"` // 机柜信息
|
||
}
|
||
|
||
// AccountInfo 账号信息
|
||
type AccountInfo struct {
|
||
AccountId string `json:"accountId"` // 电子面单账号(partnerType为2,orderType传1,2,4时必传)
|
||
AccountPassword string `json:"accountPassword"` // 电子面单密码(测试环境传ZTO123)
|
||
Type int `json:"type"` // 单号类型:1.普通电子面单;74.星联电子面单;默认是1
|
||
CustomerId string `json:"customerId"` // 集团客户编码(partnerType传1时必传)
|
||
}
|
||
|
||
// SenderInfo 发件人信息
|
||
type SenderInfo struct {
|
||
SenderId string `json:"senderId"` // 发件人ID
|
||
SenderName string `json:"senderName"` // 发件人姓名
|
||
SenderPhone string `json:"senderPhone"` // 发件人座机(与senderMobile二者不能同时为空)
|
||
SenderMobile string `json:"senderMobile"` // 发件人手机号(与senderPhone二者不能同时为空)
|
||
SenderProvince string `json:"senderProvince"` // 发件人省
|
||
SenderCity string `json:"senderCity"` // 发件人市
|
||
SenderDistrict string `json:"senderDistrict"` // 发件人区
|
||
SenderAddress string `json:"senderAddress"` // 发件人详细地址
|
||
}
|
||
|
||
// ReceiveInfo 收件人信息
|
||
type ReceiveInfo struct {
|
||
ReceiverName string `json:"receiverName"` // 收件人姓名
|
||
ReceiverPhone string `json:"receiverPhone"` // 收件人座机(与receiverMobile二者不能同时为空)
|
||
ReceiverMobile string `json:"receiverMobile"` // 收件人手机号(与 receiverPhone二者不能同时为空)
|
||
ReceiverProvince string `json:"receiverProvince"` // 收件人省
|
||
ReceiverCity string `json:"receiverCity"` // 收件人市
|
||
ReceiverDistrict string `json:"receiverDistrict"` // 收件人区
|
||
ReceiverAddress string `json:"receiverAddress"` // 收件人详细地址
|
||
}
|
||
|
||
// OrderVas 增值服务
|
||
type OrderVas struct {
|
||
VasType string `json:"vasType"` // 增值类型(COD:代收;vip:中通标快;insured:保价;receiveReturnService:签单返回;twoHour:两小时;standardExpress:中通好快)
|
||
VasAmount int64 `json:"vasAmount"` // 增值价格,如果增值类型涉及金额会校验,vasType为COD、insured时不能为空,单位:分
|
||
VasPrice int64 `json:"vasPrice"` // 增值价格(暂时不用)
|
||
VasDetail string `json:"vasDetail"` // 增值详情
|
||
AccountNo string `json:"accountNo"` // 代收账号(有代收货款增值时必填)
|
||
}
|
||
|
||
// SummaryInfo 汇总信息
|
||
type SummaryInfo struct {
|
||
Size string `json:"size"` // 订单包裹大小(单位:厘米、格式:"长,宽,高",用半角的逗号来分隔)
|
||
Quantity int `json:"quantity"` // 订单包裹内货物总数量
|
||
Price int `json:"price"` // 商品总价值(单位:元)
|
||
Freight int `json:"freight"` // 运输费(单位:元)
|
||
Premium int `json:"premium"` // 险费(单位:元)
|
||
StartTime string `json:"startTime"` // 取件开始时间(传值此字段则需必传vasType:twoHour)
|
||
EndTime string `json:"endTime"` // 取件截止时间(传值此字段则需必传vasType:twoHour)
|
||
}
|
||
|
||
// OrderItem 物品信息
|
||
type OrderItem struct {
|
||
Name string `json:"name"` // 货品名称
|
||
Category string `json:"category"` // 商品分类
|
||
Material string `json:"material"` // 商品材质
|
||
Size string `json:"size"` // 大小(长,宽,高)(单位:厘米), 用半角的逗号来分隔长宽高
|
||
Weight int64 `json:"weight"` // 重量(单位:克)
|
||
Unitprice int `json:"unitprice"` // 单价(单位:元)
|
||
Quantity int `json:"quantity"` // 货品数量
|
||
Remark string `json:"remark"` // 货品备注
|
||
}
|
||
|
||
// Cabinet 机柜信息
|
||
type Cabinet struct {
|
||
Address string `json:"address"` // 地址
|
||
Specification int `json:"specification"` // 格口规格 格口大小(1 大 2 中 3 小)
|
||
Code string `json:"code"` // 开箱码
|
||
}
|
||
|
||
/*
|
||
中通快递--创建订单接口
|
||
请求参数:
|
||
requestJSON 创建订单参数JSON字符串
|
||
appKey app值
|
||
appSecret 密钥
|
||
*/
|
||
func ztoOpenCreateOrder(requestJSON, appKey, appSecret string) (string, error) {
|
||
fmt.Printf("进入中通订单创建接口: %v \n", requestJSON)
|
||
if appKey == "" || appSecret == "" {
|
||
return "", fmt.Errorf("中通API配置错误: appKey或appSecret未设置")
|
||
}
|
||
// 正式环境
|
||
ztoUrl := "https://japi.zto.com/zto.open.createOrder"
|
||
// 测试环境
|
||
//ztoUrl := "https://japi-test.zto.com/zto.open.createOrder"
|
||
|
||
// 生成签名
|
||
dataDigest, err := ztoGenerateSign(requestJSON, appSecret)
|
||
if err != nil {
|
||
return "", fmt.Errorf("生成签名失败: %w", err)
|
||
}
|
||
fmt.Println(dataDigest)
|
||
|
||
// 创建gorequest实例
|
||
req := gorequest.New()
|
||
// 使用gorequest发送POST请求
|
||
resp, body, errs := req.Post(ztoUrl).
|
||
Set("Content-Type", "application/json").
|
||
Set("x-appKey", appKey).
|
||
Set("x-datadigest", dataDigest).
|
||
Set("x-timestamp", fmt.Sprintf("%d", time.Now().Unix())).
|
||
Send(requestJSON).
|
||
End()
|
||
|
||
// 处理错误
|
||
if len(errs) > 0 {
|
||
errStr := ""
|
||
for _, e := range errs {
|
||
errStr += e.Error() + "; "
|
||
}
|
||
return "", fmt.Errorf("发送HTTP请求失败: %s", errStr)
|
||
}
|
||
|
||
// 检查HTTP状态码
|
||
if resp.StatusCode >= 400 {
|
||
return "", fmt.Errorf("HTTP请求失败,状态码: %d", resp.StatusCode)
|
||
}
|
||
|
||
// 解析响应体
|
||
var response map[string]interface{}
|
||
if err := json.Unmarshal([]byte(body), &response); err != nil {
|
||
return "", fmt.Errorf("解析响应失败: %v, 响应内容: %s", err, body)
|
||
}
|
||
|
||
// 转换成json字符串
|
||
responseJSON, err := json.Marshal(response)
|
||
if err != nil {
|
||
return "", fmt.Errorf("JSON序列化失败: %v", err)
|
||
}
|
||
return string(responseJSON), nil
|
||
}
|
||
|
||
// 中通签名生成函数
|
||
func ztoGenerateSign(data, appSecret string) (string, error) {
|
||
// 中通签名生成函数(修正版)
|
||
// 根据文档,签名流程应该是:
|
||
// 1. 请求参数json字符串 + appSecret
|
||
// 2. 进行MD5计算
|
||
// 3. 对MD5结果进行Base64编码
|
||
|
||
// 注意:中通可能要求特定的格式,比如参数需要排序
|
||
var requestData map[string]interface{}
|
||
if err := json.Unmarshal([]byte(data), &requestData); err != nil {
|
||
return "", fmt.Errorf("解析请求参数失败: %w", err)
|
||
}
|
||
|
||
// 将map转换为排序后的JSON字符串(中通可能要求按key排序)
|
||
keys := make([]string, 0, len(requestData))
|
||
for k := range requestData {
|
||
keys = append(keys, k)
|
||
}
|
||
sort.Strings(keys)
|
||
|
||
var buf strings.Builder
|
||
buf.WriteString("{")
|
||
for i, k := range keys {
|
||
if i > 0 {
|
||
buf.WriteString(",")
|
||
}
|
||
buf.WriteString(fmt.Sprintf(`"%s":`, k))
|
||
|
||
// 根据值的类型进行格式化
|
||
switch v := requestData[k].(type) {
|
||
case string:
|
||
buf.WriteString(fmt.Sprintf(`"%s"`, v))
|
||
case float64:
|
||
buf.WriteString(fmt.Sprintf(`%.0f`, v)) // 整数可能以float64形式存在
|
||
default:
|
||
// 其他类型转换为JSON字符串
|
||
valBytes, _ := json.Marshal(v)
|
||
buf.Write(valBytes)
|
||
}
|
||
}
|
||
buf.WriteString("}")
|
||
|
||
sortedJSON := buf.String()
|
||
fmt.Printf("排序后的JSON: %s\n", sortedJSON)
|
||
|
||
// 按文档要求的格式
|
||
signString := sortedJSON + appSecret
|
||
|
||
// 计算MD5
|
||
md5Sum := md5.Sum([]byte(signString))
|
||
|
||
// Base64编码
|
||
sign := base64.StdEncoding.EncodeToString(md5Sum[:])
|
||
fmt.Printf("Base64签名: %s\n", sign)
|
||
return sign, nil
|
||
}
|
||
|
||
/*
|
||
极兔快递--创建订单接口
|
||
请求参数:
|
||
requestJSON 创建订单参数JSON字符串
|
||
appKey app值
|
||
appSecret 密钥
|
||
*/
|
||
func jtOrderAddOrder(requestJSON, apiAccount, privateKey string) (string, error) {
|
||
fmt.Printf("进入极兔订单创建接口: %v \n", requestJSON)
|
||
|
||
// 正式环境
|
||
jtUrl := "https://openapi.bxexpress.com.cn/webopenplatformapi/api/order/addOrder"
|
||
// 测试环境
|
||
//jtUrl := "https://uat-openapi.bxexpress.com.cn/webopenplatformapi/api/order/addOrder?uuid=c71ba79a80fc4307ae81f09e65b89af8"
|
||
|
||
// 生成签名
|
||
digest, err := jtGenerateDigest(requestJSON, privateKey)
|
||
if err != nil {
|
||
return "", fmt.Errorf("生成签名失败: %w", err)
|
||
}
|
||
|
||
// 当前时间戳
|
||
timestamp := fmt.Sprintf("%d", time.Now().Unix())
|
||
|
||
// 创建gorequest实例
|
||
req := gorequest.New()
|
||
// 使用gorequest发送POST请求
|
||
resp, body, errs := req.Post(jtUrl).
|
||
Type("form").
|
||
Set("Content-Type", "application/x-www-form-urlencoded").
|
||
Set("apiAccount", apiAccount).
|
||
Set("digest", digest).
|
||
Set("timestamp", timestamp).
|
||
Set("User-Agent", "ZTO-API-Client/1.0").
|
||
Send(requestJSON).
|
||
End()
|
||
|
||
// 处理错误
|
||
if len(errs) > 0 {
|
||
errStr := ""
|
||
for _, e := range errs {
|
||
errStr += e.Error() + "; "
|
||
}
|
||
return "", fmt.Errorf("发送HTTP请求失败: %s", errStr)
|
||
}
|
||
|
||
// 检查HTTP状态码
|
||
if resp.StatusCode >= 400 {
|
||
return "", fmt.Errorf("HTTP请求失败,状态码: %d", resp.StatusCode)
|
||
}
|
||
|
||
// 解析响应体
|
||
var response map[string]interface{}
|
||
if err := json.Unmarshal([]byte(body), &response); err != nil {
|
||
return "", fmt.Errorf("解析响应失败: %v, 响应内容: %s", err, body)
|
||
}
|
||
|
||
// 转换成json字符串
|
||
responseJSON, err := json.Marshal(response)
|
||
if err != nil {
|
||
return "", fmt.Errorf("JSON序列化失败: %v", err)
|
||
}
|
||
return string(responseJSON), nil
|
||
}
|
||
|
||
// GenerateHeaderDigest 极兔生成Headers中的digest签名
|
||
// bizContent: 业务参数的JSON对象,如map[string]interface{}或结构体
|
||
// privateKey: 平台分配的私钥
|
||
func jtGenerateDigest(bizContent interface{}, privateKey string) (string, error) {
|
||
// 1. 将业务参数转换为JSON字符串
|
||
jsonBytes, err := json.Marshal(bizContent)
|
||
if err != nil {
|
||
return "", fmt.Errorf("JSON序列化失败: %v", err)
|
||
}
|
||
jsonStr := string(jsonBytes)
|
||
|
||
// 2. 拼接JSON字符串和私钥
|
||
signStr := jsonStr + privateKey
|
||
|
||
// 3. 计算MD5
|
||
hash := md5.Sum([]byte(signStr))
|
||
|
||
// 4. Base64编码
|
||
digest := base64.StdEncoding.EncodeToString(hash[:])
|
||
|
||
return digest, nil
|
||
}
|
||
|
||
// GenerateBusinessDigest 极兔生成业务参数中的digest签名
|
||
// customerCode: 客户编码,如"J0086474299"
|
||
// plainPassword: 明文密码,如"H5CD3zE6"
|
||
// privateKey: 平台分配的私钥
|
||
func GenerateBusinessDigest(customerCode, plainPassword, privateKey string) (string, error) {
|
||
// 1. 计算加密后的密码 pwd = MD5(明文密码 + "jadada236t2"),32位大写
|
||
pwdStr := plainPassword + "jadada236t2"
|
||
pwdHash := md5.Sum([]byte(pwdStr))
|
||
pwd := strings.ToUpper(hex.EncodeToString(pwdHash[:]))
|
||
|
||
// 2. 拼接 customerCode + pwd + privateKey
|
||
signStr := customerCode + pwd + privateKey
|
||
|
||
// 3. 计算MD5
|
||
hash := md5.Sum([]byte(signStr))
|
||
|
||
// 4. Base64编码
|
||
digest := base64.StdEncoding.EncodeToString(hash[:])
|
||
|
||
return digest, nil
|
||
}
|
||
|
||
/*
|
||
邮政快递--订单接入接口
|
||
请求参数:
|
||
requestJSON 创建订单参数JSON字符串
|
||
appKey app值
|
||
appSecret 密钥
|
||
*/
|
||
func emsAmpApiOpen(requestJSON, secretKey string) (string, error) {
|
||
// 正式环境
|
||
emsUrl := "https://api.ems.com.cn/amp-prod-api/f/amp/api/open"
|
||
// 测试环境
|
||
//emsUrl := "https://api.ems.com.cn/amp-prod-api/f/amp/api/test"
|
||
|
||
// 当前时间戳
|
||
timestamp := fmt.Sprintf("%d", time.Now().Unix())
|
||
|
||
contentToEncrypt := requestJSON + secretKey
|
||
|
||
signature, err := GenerateSM4Signature(contentToEncrypt, secretKey)
|
||
if err != nil {
|
||
return "", fmt.Errorf("生成签名失败: %v", err)
|
||
}
|
||
|
||
// 准备请求参数
|
||
params := map[string]interface{}{
|
||
"logisticsInterface": string(requestJSON),
|
||
}
|
||
|
||
// 创建gorequest实例
|
||
req := gorequest.New()
|
||
|
||
// 设置超时和重试策略
|
||
req = req.Timeout(30*time.Second).
|
||
Retry(3, 5*time.Second, http.StatusInternalServerError, http.StatusBadGateway)
|
||
|
||
// 使用gorequest发送POST请求
|
||
resp, body, errs := req.Post(emsUrl).
|
||
Set("Content-Type", "application/json").
|
||
Set("apiCode", "020003").
|
||
Set("senderNo", "EMS").
|
||
Set("timestamp", timestamp).
|
||
Set("authorization", signature).
|
||
Set("User-Agent", "ZTO-API-Client/1.0").
|
||
Send(params).
|
||
End()
|
||
|
||
// 处理错误
|
||
if len(errs) > 0 {
|
||
errStr := ""
|
||
for _, e := range errs {
|
||
errStr += e.Error() + "; "
|
||
}
|
||
return "", fmt.Errorf("发送HTTP请求失败: %s", errStr)
|
||
}
|
||
|
||
// 检查HTTP状态码
|
||
if resp.StatusCode >= 400 {
|
||
return "", fmt.Errorf("HTTP请求失败,状态码: %d", resp.StatusCode)
|
||
}
|
||
|
||
// 解析响应体
|
||
var response map[string]interface{}
|
||
if err := json.Unmarshal([]byte(body), &response); err != nil {
|
||
return "", fmt.Errorf("解析响应失败: %v, 响应内容: %s", err, body)
|
||
}
|
||
|
||
// 转换成json字符串
|
||
responseJSON, err := json.Marshal(response)
|
||
if err != nil {
|
||
return "", fmt.Errorf("JSON序列化失败: %v", err)
|
||
}
|
||
return string(responseJSON), nil
|
||
}
|
||
|
||
const (
|
||
CIPHERTEXT_PREFIX_THIRD_SM4ECB = "|$4|"
|
||
)
|
||
|
||
// GenerateSM4Signature 生成SM4签名
|
||
// params: 业务报文JSON字符串
|
||
// key: 从页面生成的秘钥(base64格式)
|
||
// 返回值: 签名后的字符串,格式为 |$4| + base64(SM4加密结果)
|
||
func GenerateSM4Signature(params, key string) (string, error) {
|
||
// 检查输入参数
|
||
if params == "" || key == "" {
|
||
return params, nil
|
||
}
|
||
|
||
// 检查是否已经加密
|
||
if strings.HasPrefix(params, CIPHERTEXT_PREFIX_THIRD_SM4ECB) {
|
||
return params, nil
|
||
}
|
||
|
||
// 拼接待签名的内容:业务报文 + key
|
||
content := params + key
|
||
|
||
// 解码base64格式的密钥
|
||
keyBytes, err := base64.StdEncoding.DecodeString(key)
|
||
if err != nil {
|
||
return "", fmt.Errorf("failed to decode base64 key: %v", err)
|
||
}
|
||
|
||
// SM4 ECB模式加密
|
||
encryptedData, err := sm4ECBEncrypt([]byte(content), keyBytes)
|
||
if err != nil {
|
||
return "", fmt.Errorf("failed to encrypt with SM4: %v", err)
|
||
}
|
||
|
||
// 对加密结果进行base64编码
|
||
encryptedBase64 := base64.StdEncoding.EncodeToString(encryptedData)
|
||
|
||
// 添加前缀
|
||
result := CIPHERTEXT_PREFIX_THIRD_SM4ECB + encryptedBase64
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// SM4ECBDecrypt 验证签名(解密)
|
||
func SM4ECBDecrypt(ciphertext, key string) (string, error) {
|
||
if ciphertext == "" || key == "" {
|
||
return ciphertext, nil
|
||
}
|
||
|
||
// 检查是否包含SM4前缀
|
||
if !strings.HasPrefix(ciphertext, CIPHERTEXT_PREFIX_THIRD_SM4ECB) {
|
||
return ciphertext, nil
|
||
}
|
||
|
||
// 移除前缀
|
||
encryptedBase64 := strings.TrimPrefix(ciphertext, CIPHERTEXT_PREFIX_THIRD_SM4ECB)
|
||
|
||
// 解码base64密文
|
||
encryptedData, err := base64.StdEncoding.DecodeString(encryptedBase64)
|
||
if err != nil {
|
||
return "", fmt.Errorf("failed to decode base64 ciphertext: %v", err)
|
||
}
|
||
|
||
// 解码base64密钥
|
||
keyBytes, err := base64.StdEncoding.DecodeString(key)
|
||
if err != nil {
|
||
return "", fmt.Errorf("failed to decode base64 key: %v", err)
|
||
}
|
||
|
||
// SM4 ECB模式解密
|
||
decryptedData, err := sm4ECBDecrypt(encryptedData, keyBytes)
|
||
if err != nil {
|
||
return "", fmt.Errorf("failed to decrypt with SM4: %v", err)
|
||
}
|
||
|
||
return string(decryptedData), nil
|
||
}
|
||
|
||
// sm4ECBEncrypt SM4 ECB模式加密
|
||
func sm4ECBEncrypt(plaintext, key []byte) ([]byte, error) {
|
||
block, err := sm4.NewCipher(key)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 使用PKCS5Padding填充
|
||
plaintext = pkcs5Padding(plaintext, block.BlockSize())
|
||
|
||
// ECB模式不需要IV
|
||
blockSize := block.BlockSize()
|
||
ciphertext := make([]byte, len(plaintext))
|
||
|
||
// 分块加密
|
||
for start := 0; start < len(plaintext); start += blockSize {
|
||
end := start + blockSize
|
||
block.Encrypt(ciphertext[start:end], plaintext[start:end])
|
||
}
|
||
|
||
return ciphertext, nil
|
||
}
|
||
|
||
// sm4ECBDecrypt SM4 ECB模式解密
|
||
func sm4ECBDecrypt(ciphertext, key []byte) ([]byte, error) {
|
||
block, err := sm4.NewCipher(key)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 检查密文长度
|
||
if len(ciphertext)%block.BlockSize() != 0 {
|
||
return nil, fmt.Errorf("ciphertext length is not a multiple of block size")
|
||
}
|
||
|
||
plaintext := make([]byte, len(ciphertext))
|
||
blockSize := block.BlockSize()
|
||
|
||
// 分块解密
|
||
for start := 0; start < len(ciphertext); start += blockSize {
|
||
end := start + blockSize
|
||
block.Decrypt(plaintext[start:end], ciphertext[start:end])
|
||
}
|
||
|
||
// 去除PKCS5Padding填充
|
||
plaintext = pkcs5UnPadding(plaintext)
|
||
|
||
return plaintext, nil
|
||
}
|
||
|
||
// pkcs5Padding PKCS5填充
|
||
func pkcs5Padding(src []byte, blockSize int) []byte {
|
||
padding := blockSize - len(src)%blockSize
|
||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||
return append(src, padtext...)
|
||
}
|
||
|
||
// pkcs5UnPadding 去除PKCS5填充
|
||
func pkcs5UnPadding(src []byte) []byte {
|
||
length := len(src)
|
||
if length == 0 {
|
||
return src
|
||
}
|
||
|
||
unpadding := int(src[length-1])
|
||
if unpadding > length {
|
||
return src
|
||
}
|
||
|
||
return src[:(length - unpadding)]
|
||
}
|
||
|
||
// VerifySignature 验证签名是否正确
|
||
func VerifySignature(ciphertext, params, key string) (bool, error) {
|
||
// 解密签名
|
||
decrypted, err := SM4ECBDecrypt(ciphertext, key)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
|
||
// 验证解密后的内容是否等于 params + key
|
||
expected := params + key
|
||
return decrypted == expected, nil
|
||
}
|
||
|
||
/*
|
||
申通快递--订单创建接口
|
||
请求参数:
|
||
requestJSON content参数JSON字符串
|
||
fromAppkey 订阅方/请求发起方的应用key
|
||
secretKey 密钥
|
||
fromCode 订阅方/请求发起方的应用资源code
|
||
*/
|
||
func stoOmsExpressOrderCreate(requestJSON, fromAppkey, secretKey, fromCode string) (string, error) {
|
||
// 正式环境
|
||
//ztoUrl := "https://cloudinter-linkgateway.sto.cn/gateway/link.do"
|
||
// 测试环境
|
||
stoUrl := "http://cloudinter-linkgatewaytest.sto.cn/gateway/link.do"
|
||
|
||
signature := stoGenerateSign(requestJSON, secretKey)
|
||
|
||
// 准备请求参数
|
||
params := map[string]string{
|
||
"api_name": "OMS_EXPRESS_ORDER_CREATE",
|
||
"content": requestJSON,
|
||
"from_appkey": fromAppkey,
|
||
"from_code": fromCode,
|
||
"to_appkey": "sto_oms",
|
||
"to_code": "sto_oms",
|
||
"data_digest": signature,
|
||
}
|
||
|
||
// 创建gorequest实例
|
||
req := gorequest.New()
|
||
|
||
// 设置超时和重试策略
|
||
req = req.Timeout(30*time.Second).
|
||
Retry(3, 5*time.Second, http.StatusInternalServerError, http.StatusBadGateway)
|
||
|
||
// 使用gorequest发送POST请求
|
||
req.Post(stoUrl).
|
||
Set("Content-Type", "application/x-www-form-urlencoded;charset=utf-8")
|
||
|
||
// 添加表单参数
|
||
for key, value := range params {
|
||
req.Send(fmt.Sprintf("%s=%s", key, value))
|
||
}
|
||
resp, body, errs := req.End()
|
||
|
||
// 处理错误
|
||
if len(errs) > 0 {
|
||
errStr := ""
|
||
for _, e := range errs {
|
||
errStr += e.Error() + "; "
|
||
}
|
||
return "", fmt.Errorf("发送HTTP请求失败: %s", errStr)
|
||
}
|
||
|
||
// 检查HTTP状态码
|
||
if resp.StatusCode >= 400 {
|
||
return "", fmt.Errorf("HTTP请求失败,状态码: %d", resp.StatusCode)
|
||
}
|
||
|
||
// 解析响应体
|
||
var response map[string]interface{}
|
||
if err := json.Unmarshal([]byte(body), &response); err != nil {
|
||
return "", fmt.Errorf("解析响应失败: %v, 响应内容: %s", err, body)
|
||
}
|
||
|
||
// 转换成json字符串
|
||
responseJSON, err := json.Marshal(response)
|
||
if err != nil {
|
||
return "", fmt.Errorf("JSON序列化失败: %v", err)
|
||
}
|
||
return string(responseJSON), nil
|
||
}
|
||
|
||
// 申通快递计算签名
|
||
func stoGenerateSign(content, secretKey string) string {
|
||
text := content + secretKey
|
||
// 计算 MD5
|
||
hash := md5.Sum([]byte(text))
|
||
// Base64 编码
|
||
return base64.StdEncoding.EncodeToString(hash[:])
|
||
}
|
||
|
||
// 韵达快递--电子面单下单
|
||
// 参数:requestJSON:业务参数JSON字符串 appid:合作商appid(等同app-key) appSecret:签名 partnerID
|
||
func ydCreateBmOrder(requestJSON, appKey, appSecret string) (string, error) {
|
||
// 确定环境
|
||
//baseURL := "https://openapi.yundax.com/openapi-api/v1/accountOrder/createBmOrder"
|
||
// 测试环境
|
||
baseURL := "https://u-openapi.yundasys.com/openapi-api/v1/accountOrder/createBmOrder"
|
||
|
||
// 3. 构建 Headers(根据文档)
|
||
timestamp := time.Now().UnixMilli()
|
||
headers := map[string]string{
|
||
"Content-Type": "application/json; charset=UTF-8",
|
||
"app-key": appKey,
|
||
"req-time": fmt.Sprintf("%d", timestamp),
|
||
"sign": ydGenerateSign(requestJSON, appSecret), // 需要实现签名方法
|
||
}
|
||
|
||
// 4. 发送请求
|
||
request := gorequest.New()
|
||
resp, body, errs := request.Post(baseURL).
|
||
Set("Content-Type", headers["Content-Type"]).
|
||
Set("app-key", headers["app-key"]).
|
||
Set("req-time", headers["req-time"]).
|
||
Set("sign", headers["sign"]).
|
||
Send(requestJSON).
|
||
End()
|
||
if len(errs) > 0 {
|
||
errStr := ""
|
||
for _, e := range errs {
|
||
errStr += e.Error() + "; "
|
||
}
|
||
return "", fmt.Errorf("发送HTTP请求失败: %s", errStr)
|
||
}
|
||
|
||
// 检查HTTP状态码
|
||
if resp.StatusCode >= 400 {
|
||
return "", fmt.Errorf("HTTP请求失败,状态码: %d", resp.StatusCode)
|
||
}
|
||
|
||
// 解析响应体
|
||
var response map[string]interface{}
|
||
if err := json.Unmarshal([]byte(body), &response); err != nil {
|
||
return "", fmt.Errorf("解析响应失败: %v, 响应内容: %s", err, body)
|
||
}
|
||
|
||
// 转换成json字符串
|
||
responseJSON, err := json.Marshal(response)
|
||
if err != nil {
|
||
return "", fmt.Errorf("JSON序列化失败: %v", err)
|
||
}
|
||
return string(responseJSON), nil
|
||
}
|
||
|
||
// 韵达快递计算签名
|
||
func ydGenerateSign(content, secretKey string) string {
|
||
text := content + "_" + secretKey
|
||
// 计算 MD5
|
||
hash := md5.Sum([]byte(text))
|
||
// Base64 编码
|
||
return base64.StdEncoding.EncodeToString(hash[:])
|
||
}
|
||
|
||
// 韵达快递--电子面单打印
|
||
func ydBmGetPdfInfo(requestJSON, appKey, appSecret string) (string, error) {
|
||
// 确定环境
|
||
//baseURL := "https://openapi.yundax.com/openapi-api/v1/accountOrder/createBmOrder"
|
||
// 测试环境
|
||
baseURL := "https://u-openapi.yundasys.com/openapi-api/v1/accountOrder/createBmOrder"
|
||
|
||
// 3. 构建 Headers(根据文档)
|
||
timestamp := time.Now().UnixMilli()
|
||
headers := map[string]string{
|
||
"Content-Type": "application/json; charset=UTF-8",
|
||
"app-key": appKey,
|
||
"req-time": fmt.Sprintf("%d", timestamp),
|
||
"sign": ydGenerateSign(requestJSON, appSecret), // 需要实现签名方法
|
||
}
|
||
|
||
// 4. 发送请求
|
||
request := gorequest.New()
|
||
resp, body, errs := request.Post(baseURL).
|
||
Set("Content-Type", headers["Content-Type"]).
|
||
Set("app-key", headers["app-key"]).
|
||
Set("req-time", headers["req-time"]).
|
||
Set("sign", headers["sign"]).
|
||
Send(requestJSON).
|
||
End()
|
||
if len(errs) > 0 {
|
||
errStr := ""
|
||
for _, e := range errs {
|
||
errStr += e.Error() + "; "
|
||
}
|
||
return "", fmt.Errorf("发送HTTP请求失败: %s", errStr)
|
||
}
|
||
|
||
// 检查HTTP状态码
|
||
if resp.StatusCode >= 400 {
|
||
return "", fmt.Errorf("HTTP请求失败,状态码: %d", resp.StatusCode)
|
||
}
|
||
|
||
// 解析响应体
|
||
var response map[string]interface{}
|
||
if err := json.Unmarshal([]byte(body), &response); err != nil {
|
||
return "", fmt.Errorf("解析响应失败: %v, 响应内容: %s", err, body)
|
||
}
|
||
|
||
// 转换成json字符串
|
||
responseJSON, err := json.Marshal(response)
|
||
if err != nil {
|
||
return "", fmt.Errorf("JSON序列化失败: %v", err)
|
||
}
|
||
return string(responseJSON), nil
|
||
}
|
||
|
||
const (
|
||
ZTO_ORDER_TYPE = "ZTO"
|
||
JT_ORDER_TYPE = "JT"
|
||
EMS_ORDER_TYPE = "EMS"
|
||
STO_ORDER_TYPE = "STO"
|
||
YD_ORDER_TYPE = "YD"
|
||
)
|
||
|
||
/*
|
||
整合所有快递--订单接口
|
||
请求参数:
|
||
orderType 快递类型
|
||
requestJSON 创建订单参数JSON字符串
|
||
*/
|
||
func integrationOrderCreate(orderType, requestJSON, key, secret, fromCode string) (string, error) {
|
||
switch orderType {
|
||
// 中通
|
||
case ZTO_ORDER_TYPE:
|
||
return ztoOpenCreateOrder(requestJSON, key, secret)
|
||
// 极兔
|
||
case JT_ORDER_TYPE:
|
||
return jtOrderAddOrder(requestJSON, key, secret)
|
||
// 邮政
|
||
case EMS_ORDER_TYPE:
|
||
return emsAmpApiOpen(requestJSON, key)
|
||
// 申通
|
||
case STO_ORDER_TYPE:
|
||
return stoOmsExpressOrderCreate(requestJSON, key, secret, fromCode)
|
||
// 韵达
|
||
case YD_ORDER_TYPE:
|
||
return ydCreateBmOrder(requestJSON, key, secret)
|
||
}
|
||
return "", fmt.Errorf("快递类型不匹配: %s", orderType)
|
||
}
|
||
|
||
// ========================== C 导入函数 ===================
|
||
|
||
// ZtoOpenCreateOrder 中通快递--创建订单接口
|
||
//
|
||
//export ZtoOpenCreateOrder
|
||
func ZtoOpenCreateOrder(requestJSON, appKey, appSecret *C.char) *C.char {
|
||
requestJSONStr := C.GoString(requestJSON)
|
||
appKeyStr := C.GoString(appKey)
|
||
appSecretStr := C.GoString(appSecret)
|
||
info, err := ztoOpenCreateOrder(requestJSONStr, appKeyStr, appSecretStr)
|
||
if err != nil {
|
||
return C.CString(err.Error())
|
||
}
|
||
return C.CString(info)
|
||
}
|
||
|
||
// JtOrderAddOrder 极兔快递--创建订单接口
|
||
//
|
||
//export JtOrderAddOrder
|
||
func JtOrderAddOrder(requestJSON, apiAccount, privateKey *C.char) *C.char {
|
||
requestJSONStr := C.GoString(requestJSON)
|
||
apiAccountStr := C.GoString(apiAccount)
|
||
privateKeyStr := C.GoString(privateKey)
|
||
info, err := jtOrderAddOrder(requestJSONStr, apiAccountStr, privateKeyStr)
|
||
if err != nil {
|
||
return C.CString(err.Error())
|
||
}
|
||
return C.CString(info)
|
||
}
|
||
|
||
// EmsAmpApiOpen 邮政快递--订单接入接口
|
||
//
|
||
//export EmsAmpApiOpen
|
||
func EmsAmpApiOpen(requestJSON, secretKey *C.char) *C.char {
|
||
requestJSONStr := C.GoString(requestJSON)
|
||
secretKeyStr := C.GoString(secretKey)
|
||
info, err := emsAmpApiOpen(requestJSONStr, secretKeyStr)
|
||
if err != nil {
|
||
return C.CString(err.Error())
|
||
}
|
||
return C.CString(info)
|
||
}
|
||
|
||
// StoOmsExpressOrderCreate 申通快递--订单接入接口
|
||
//
|
||
//export StoOmsExpressOrderCreate
|
||
func StoOmsExpressOrderCreate(requestJSON, fromAppkey, secretKey, fromCode *C.char) *C.char {
|
||
requestJSONStr := C.GoString(requestJSON)
|
||
fromAppkeyStr := C.GoString(fromAppkey)
|
||
secretKeyStr := C.GoString(secretKey)
|
||
fromCodeStr := C.GoString(fromCode)
|
||
|
||
info, err := stoOmsExpressOrderCreate(requestJSONStr, fromAppkeyStr, secretKeyStr, fromCodeStr)
|
||
if err != nil {
|
||
return C.CString(err.Error())
|
||
}
|
||
return C.CString(info)
|
||
}
|
||
|
||
// YdCreateBmOrder 韵达快递--电子面单下单
|
||
//
|
||
//export YdCreateBmOrder
|
||
func YdCreateBmOrder(requestJSON, appKey, appSecret *C.char) *C.char {
|
||
requestJSONStr := C.GoString(requestJSON)
|
||
appKeyStr := C.GoString(appKey)
|
||
appSecretStr := C.GoString(appSecret)
|
||
info, err := ydCreateBmOrder(requestJSONStr, appKeyStr, appSecretStr)
|
||
if err != nil {
|
||
return C.CString(err.Error())
|
||
}
|
||
return C.CString(info)
|
||
}
|
||
|
||
// YdBmGetPdfInfo 韵达快递--电子面单打印
|
||
//
|
||
//export YdBmGetPdfInfo
|
||
func YdBmGetPdfInfo(requestJSON, appKey, appSecret *C.char) *C.char {
|
||
requestJSONStr := C.GoString(requestJSON)
|
||
appKeyStr := C.GoString(appKey)
|
||
appSecretStr := C.GoString(appSecret)
|
||
info, err := ydBmGetPdfInfo(requestJSONStr, appKeyStr, appSecretStr)
|
||
if err != nil {
|
||
return C.CString(err.Error())
|
||
}
|
||
return C.CString(info)
|
||
}
|
||
|
||
// IntegrationOrderCreate 整合所有快递--订单接口
|
||
//
|
||
//export IntegrationOrderCreate
|
||
func IntegrationOrderCreate(orderType, requestJSON, key, secret, fromCode *C.char) *C.char {
|
||
orderTypeStr := C.GoString(orderType)
|
||
requestJSONStr := C.GoString(requestJSON)
|
||
keyStr := C.GoString(key)
|
||
secretStr := C.GoString(secret)
|
||
fromCodeStr := C.GoString(fromCode)
|
||
info, err := integrationOrderCreate(orderTypeStr, requestJSONStr, keyStr, secretStr, fromCodeStr)
|
||
if err != nil {
|
||
return C.CString(err.Error())
|
||
}
|
||
return C.CString(info)
|
||
}
|
||
|
||
// FreeCString 释放C字符串内存
|
||
//
|
||
//export FreeCString
|
||
func FreeCString(str *C.char) {
|
||
C.free(unsafe.Pointer(str))
|
||
}
|
||
|
||
func getRequestParamter(orderType string) {
|
||
switch orderType {
|
||
case ZTO_ORDER_TYPE:
|
||
|
||
case JT_ORDER_TYPE:
|
||
|
||
case EMS_ORDER_TYPE:
|
||
|
||
case STO_ORDER_TYPE:
|
||
var stoOrder StoOrder
|
||
stoOrder.OrderNo = "OrderNo string 订单号(客户系统自己生成,唯一)"
|
||
}
|
||
|
||
}
|
||
|
||
// StoOrder 订单信息
|
||
type StoOrder struct {
|
||
// OrderNo 订单号(客户系统自己生成,唯一)
|
||
OrderNo string `json:"orderNo"`
|
||
// OrderSource 订单来源(订阅服务时填写的来源编码)
|
||
OrderSource string `json:"orderSource"`
|
||
// BillType 获取面单的类型(00-普通、03-国际、01-代收、02-到付、04-生鲜),默认普通业务,如果有其他业务先与业务方沟通清楚
|
||
BillType string `json:"billType"`
|
||
// OrderType 订单类型(01-普通订单、02-调度订单)默认01-普通订单,如果有散单业务需先业务方沟通清楚
|
||
OrderType string `json:"orderType"`
|
||
// Sender 寄件人信息
|
||
Sender StoSenderInfo `json:"sender"`
|
||
// Receiver 收件人信息
|
||
Receiver ReceiverInfo `json:"receiver"`
|
||
// Cargo 包裹信息
|
||
Cargo CargoInfo `json:"cargo"`
|
||
// Customer 客户信息,在线下单取运单号必填,代单号下单不需要填写,测试账号传值如下,生产账号联系合作业务方提供
|
||
Customer CustomerInfo `json:"customer"`
|
||
// InternationalAnnex 国际订单附属信息(国际业务订单必填,其他业务不要填写)
|
||
InternationalAnnex InternationalInfo `json:"internationalAnnex"`
|
||
// WaybillNo 运单号(下单前已获取运单号时必传,否则不传或传NULL)
|
||
WaybillNo string `json:"waybillNo"`
|
||
// AssignAnnex 指定网点揽收(调度散单业务订单需要传)其他业务不需要
|
||
AssignAnnex AssignInfo `json:"assignAnnex"`
|
||
// CodValue 代收货款金额,单位:元(代收货款业务时必填)
|
||
CodValue string `json:"codValue"`
|
||
// FreightCollectValue 到付运费金额,单位:元(到付业务时必填)
|
||
FreightCollectValue string `json:"freightCollectValue"`
|
||
// TimelessType 时效类型(01-普通)
|
||
TimelessType string `json:"timelessType"`
|
||
// ProductType 产品类型(01-普通、02-冷链、03-生鲜)
|
||
ProductType string `json:"productType"`
|
||
// ServiceTypeList 增值服务(TRACE_PUSH-轨迹回传;PRIVACY_SURFACE_SINGLE-隐私面单标;STO_MARKET_SDD-申咚咚按需派送,STO_HOME_DELIVERY_OP 送货上门开城判断)
|
||
ServiceTypeList []string `json:"serviceTypeList"`
|
||
// ExtendFieldMap 拓展字段 注意事项:属性值有逗号等于号需过滤掉
|
||
ExtendFieldMap map[string]string `json:"extendFieldMap"`
|
||
// Remark 备注
|
||
Remark string `json:"remark"`
|
||
// ExpressDirection 快递流向(01-正向订单)默认01
|
||
ExpressDirection string `json:"expressDirection"`
|
||
// CreateChannel 创建原因(01-客户创建)默认01
|
||
CreateChannel string `json:"createChannel"`
|
||
// RegionType 区域类型(01-国内)默认01
|
||
RegionType string `json:"regionType"`
|
||
// InsuredAnnex 保价模型(保价服务必填)
|
||
InsuredAnnex InsuredInfo `json:"insuredAnnex"`
|
||
// ExpectValue 预估费用(散单业务使用)
|
||
ExpectValue string `json:"expectValue"`
|
||
// PayModel 支付方式(1-现付;2-到付;3-月结)
|
||
PayModel string `json:"payModel"`
|
||
}
|
||
|
||
// StoSenderInfo 寄件人信息
|
||
type StoSenderInfo struct {
|
||
// Name 寄件人名称
|
||
Name string `json:"name"`
|
||
// Tel 寄件人固定电话
|
||
Tel string `json:"tel"`
|
||
// Mobile 寄件人手机号码
|
||
Mobile string `json:"mobile"`
|
||
// PostCode 邮编
|
||
PostCode string `json:"postCode"`
|
||
// Country 国家
|
||
Country string `json:"country"`
|
||
// Province 省
|
||
Province string `json:"province"`
|
||
// City 市
|
||
City string `json:"city"`
|
||
// Area 区
|
||
Area string `json:"area"`
|
||
// Town 镇
|
||
Town string `json:"town"`
|
||
// Address 详细地址
|
||
Address string `json:"address"`
|
||
}
|
||
|
||
// ReceiverInfo 收件人信息
|
||
type ReceiverInfo struct {
|
||
// Name 收件人名称
|
||
Name string `json:"name"`
|
||
// Tel 收件人固定电话
|
||
Tel string `json:"tel"`
|
||
// Mobile 收件人手机号码
|
||
Mobile string `json:"mobile"`
|
||
// PostCode 邮编
|
||
PostCode string `json:"postCode"`
|
||
// Country 国家
|
||
Country string `json:"country"`
|
||
// Province 省
|
||
Province string `json:"province"`
|
||
// City 市
|
||
City string `json:"city"`
|
||
// Area 区
|
||
Area string `json:"area"`
|
||
// Town 镇
|
||
Town string `json:"town"`
|
||
// Address 详细地址
|
||
Address string `json:"address"`
|
||
// SafeNo 安全号码
|
||
SafeNo string `json:"safeNo"`
|
||
}
|
||
|
||
// CargoInfo 包裹信息
|
||
type CargoInfo struct {
|
||
// Battery 带电标识(10/未知 20/带电 30/不带电)
|
||
Battery string `json:"battery"`
|
||
// GoodsType 物品类型(大件、小件、扁平件\文件)
|
||
GoodsType string `json:"goodsType"`
|
||
// GoodsName 物品名称
|
||
GoodsName string `json:"goodsName"`
|
||
// GoodsCount 物品数量
|
||
GoodsCount int `json:"goodsCount"`
|
||
// SpaceX 长(cm)
|
||
SpaceX float64 `json:"spaceX"`
|
||
// SpaceY 宽(cm)
|
||
SpaceY float64 `json:"spaceY"`
|
||
// SpaceZ 高(cm)
|
||
SpaceZ float64 `json:"spaceZ"`
|
||
// Weight 重量(kg)
|
||
Weight float64 `json:"weight"`
|
||
// GoodsAmount 商品金额
|
||
GoodsAmount string `json:"goodsAmount"`
|
||
// CargoItemList 小包信息(国际业务专用,其他业务不需要填写)
|
||
CargoItemList []CargoItem `json:"cargoItemList"`
|
||
}
|
||
|
||
// CargoItem 小包信息
|
||
type CargoItem struct {
|
||
// SerialNumber 小包号
|
||
SerialNumber string `json:"serialNumber"`
|
||
// ReferenceNumber 关联单号
|
||
ReferenceNumber string `json:"referenceNumber"`
|
||
// ProductId 商品ID
|
||
ProductId string `json:"productId"`
|
||
// Name 名称
|
||
Name string `json:"name"`
|
||
// Qty 数量
|
||
Qty int `json:"qty"`
|
||
// UnitPrice 单价
|
||
UnitPrice float64 `json:"unitPrice"`
|
||
// Amount 总价
|
||
Amount float64 `json:"amount"`
|
||
// Currency 币种
|
||
Currency string `json:"currency"`
|
||
// Weight 重量(kg)
|
||
Weight float64 `json:"weight"`
|
||
// Remark 备注
|
||
Remark string `json:"remark"`
|
||
}
|
||
|
||
// CustomerInfo 客户信息
|
||
type CustomerInfo struct {
|
||
// SiteCode 网点编码必传,测试传值"siteCode":"666666"
|
||
SiteCode string `json:"siteCode"`
|
||
// CustomerName 客户编码,测试传值"customerName":"666666000001"
|
||
CustomerName string `json:"customerName"`
|
||
// SitePwd 电子面单密码,测试传值"sitePwd":"abc123"
|
||
SitePwd string `json:"sitePwd"`
|
||
// MonthCustomerCode 月结客户编码(不传单号需调度才传月结编号)如果填写一般和客户编号值相同
|
||
MonthCustomerCode string `json:"monthCustomerCode"`
|
||
}
|
||
|
||
// InternationalInfo 国际订单附属信息
|
||
type InternationalInfo struct {
|
||
// InternationalProductType 国际业务类型(01-国际进口,02-国际保税,03-国际直邮)
|
||
InternationalProductType string `json:"internationalProductType"`
|
||
// CustomsDeclaration 是否报关,默认为否
|
||
CustomsDeclaration bool `json:"customsDeclaration"`
|
||
// SenderCountry 发件人所在国家,国际件为必填字段
|
||
SenderCountry string `json:"senderCountry"`
|
||
// ReceiverCountry 收件人所在国家,国际件为必填字段
|
||
ReceiverCountry string `json:"receiverCountry"`
|
||
}
|
||
|
||
// AssignInfo 指定网点揽收信息
|
||
type AssignInfo struct {
|
||
// TakeCompanyCode 指定取件的网点编号
|
||
TakeCompanyCode string `json:"takeCompanyCode"`
|
||
// TakeUserCode 指定取件的业务员编号(指定业务员时takeCompanyCode可传可不传,若传必须传正确,举例:寄件地址是上海,只能是指定上海业务员取件)
|
||
TakeUserCode string `json:"takeUserCode"`
|
||
}
|
||
|
||
// InsuredInfo 保价信息
|
||
type InsuredInfo struct {
|
||
// InsuredValue 保价金额,单位:元(保价服务时必填,精确到小数点后两位)
|
||
InsuredValue string `json:"insuredValue"`
|
||
// GoodsValue 物品价值,单位:元(保价服务时必填,精确到小数点后两位)
|
||
GoodsValue string `json:"goodsValue"`
|
||
}
|
||
|
||
// 极兔快递配置结构体
|
||
type JituConfig struct {
|
||
APIAccount string // 接入方在平台的api账户标识
|
||
APIKey string // 用于生成digest的密钥
|
||
BaseURL string
|
||
CustomerCode string
|
||
}
|
||
|
||
// 请求头结构体
|
||
type JituHeader struct {
|
||
APIAccount string `json:"apiAccount"`
|
||
Digest string `json:"digest"`
|
||
Timestamp string `json:"timestamp"`
|
||
}
|
||
|
||
// 地址结构体
|
||
type Address struct {
|
||
Name string `json:"name"`
|
||
Company string `json:"company,omitempty"`
|
||
PostCode string `json:"postCode,omitempty"`
|
||
MailBox string `json:"mailBox,omitempty"`
|
||
Mobile string `json:"mobile"`
|
||
Phone string `json:"phone,omitempty"`
|
||
CountryCode string `json:"countryCode"`
|
||
Prov string `json:"prov"`
|
||
City string `json:"city"`
|
||
Area string `json:"area"`
|
||
Town string `json:"town,omitempty"`
|
||
Street string `json:"street,omitempty"`
|
||
Address string `json:"address"`
|
||
}
|
||
|
||
// 商品项结构体
|
||
type Item struct {
|
||
ItemType string `json:"itemType,omitempty"`
|
||
ItemName string `json:"itemName"`
|
||
ChineseName string `json:"chineseName,omitempty"`
|
||
EnglishName string `json:"englishName,omitempty"`
|
||
Number int `json:"number"`
|
||
ItemValue string `json:"itemValue,omitempty"`
|
||
PriceCurrency string `json:"priceCurrency,omitempty"`
|
||
Desc string `json:"desc,omitempty"`
|
||
ItemURL string `json:"itemUrl,omitempty"`
|
||
}
|
||
|
||
// 扩展信息结构体
|
||
type ExtendInfo struct {
|
||
IsFourLevelAddress string `json:"isFourLevelAddress,omitempty"`
|
||
}
|
||
|
||
// 业务内容结构体
|
||
type BizContent struct {
|
||
CustomerCode string `json:"customerCode"`
|
||
Digest string `json:"digest"`
|
||
Network *string `json:"network,omitempty"`
|
||
TxlogisticId string `json:"txlogisticId"`
|
||
ExpressType string `json:"expressType"`
|
||
OrderType string `json:"orderType"`
|
||
ServiceType string `json:"serviceType"`
|
||
DeliveryType string `json:"deliveryType"`
|
||
PayType string `json:"payType"`
|
||
Sender Address `json:"sender"`
|
||
Receiver Address `json:"receiver"`
|
||
SendStartTime string `json:"sendStartTime"`
|
||
SendEndTime string `json:"sendEndTime"`
|
||
GoodsType string `json:"goodsType"`
|
||
Length float64 `json:"length"`
|
||
Width float64 `json:"width"`
|
||
Height float64 `json:"height"`
|
||
Weight string `json:"weight"`
|
||
TotalQuantity int `json:"totalQuantity"`
|
||
ItemsValue *string `json:"itemsValue,omitempty"`
|
||
PriceCurrency *string `json:"priceCurrency,omitempty"`
|
||
OfferFee *string `json:"offerFee,omitempty"`
|
||
Remark *string `json:"remark,omitempty"`
|
||
Items []Item `json:"items"`
|
||
CustomsInfo *string `json:"customsInfo,omitempty"`
|
||
PostSiteCode *string `json:"postSiteCode,omitempty"`
|
||
PostSiteName *string `json:"postSiteName,omitempty"`
|
||
PostSiteAddress *string `json:"postSiteAddress,omitempty"`
|
||
RealName *string `json:"realName,omitempty"`
|
||
ExtendInfo ExtendInfo `json:"extendInfo,omitempty"`
|
||
}
|
||
|
||
// 生成digest
|
||
func generateDigest(data string) string {
|
||
hash := md5.Sum([]byte(data))
|
||
return base64.StdEncoding.EncodeToString(hash[:])
|
||
}
|
||
|
||
// 生成请求头
|
||
func generateHeaders(config *JituConfig) JituHeader {
|
||
timestamp := fmt.Sprintf("%d", time.Now().UnixNano()/int64(time.Millisecond))
|
||
|
||
// 根据极兔API文档,header中的digest通常是apiAccount + apiKey + timestamp的MD5
|
||
headerData := config.APIAccount + config.APIKey + timestamp
|
||
headerDigest := generateDigest(headerData)
|
||
|
||
return JituHeader{
|
||
APIAccount: config.APIAccount,
|
||
Digest: headerDigest,
|
||
Timestamp: timestamp,
|
||
}
|
||
}
|
||
|
||
// 创建订单请求
|
||
func CreateJituOrder(config *JituConfig, bizContent *BizContent) (map[string]interface{}, error) {
|
||
// 设置请求URL--测试地址
|
||
jtUrl := "https://uat-openapi.bxexpress.com.cn/webopenplatformapi/api/order/addOrder?uuid=c71ba79a80fc4307ae81f09e65b89af8"
|
||
// 设置请求URL--正式地址
|
||
//jtUrl := "https://openapi.bxexpress.com.cn/webopenplatformapi/api/order/addOrder"
|
||
|
||
// 生成请求头
|
||
headers := generateHeaders(config)
|
||
|
||
// 设置业务内容中的customerCode
|
||
bizContent.CustomerCode = config.CustomerCode
|
||
|
||
// 生成业务内容的digest(这里需要根据极兔API的实际要求生成)
|
||
// 通常是对bizContent的某些字段进行签名
|
||
bizContentDigest := generateDigest(bizContent.TxlogisticId + config.APIKey)
|
||
bizContent.Digest = bizContentDigest
|
||
|
||
// 将bizContent转换为JSON字符串
|
||
bizContentJSON, err := json.Marshal(bizContent)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("序列化bizContent失败: %v", err)
|
||
}
|
||
|
||
// 构建请求体
|
||
requestBody := map[string]interface{}{
|
||
"bizContent": string(bizContentJSON),
|
||
}
|
||
|
||
// 使用gorequest发送请求
|
||
request := gorequest.New()
|
||
|
||
// 设置请求头
|
||
request.Set("apiAccount", headers.APIAccount)
|
||
request.Set("digest", headers.Digest)
|
||
request.Set("timestamp", headers.Timestamp)
|
||
request.Set("Content-Type", "application/x-www-form-urlencoded")
|
||
|
||
// 发送POST请求
|
||
resp, body, errs := request.Post(jtUrl).
|
||
Type("form").
|
||
Send(requestBody).
|
||
End()
|
||
|
||
if len(errs) > 0 {
|
||
return nil, fmt.Errorf("请求失败: %v", errs)
|
||
}
|
||
|
||
if resp.StatusCode != 200 {
|
||
return nil, fmt.Errorf("请求返回非200状态码: %d, 响应: %s", resp.StatusCode, body)
|
||
}
|
||
|
||
// 解析响应
|
||
var response map[string]interface{}
|
||
if err := json.Unmarshal([]byte(body), &response); err != nil {
|
||
return nil, fmt.Errorf("解析响应失败: %v, 原始响应: %s", err, body)
|
||
}
|
||
|
||
return response, nil
|
||
}
|
||
|
||
// 主函数
|
||
func main() {
|
||
}
|