快递的物流地图是怎么实现的

9,859 阅读18分钟

踩坑总结

1.接口都是使用的表单数据、且推送接口表单数据带空格需要处理一下、(当时怎么都获取不到数据)

2.地图并不是查询的实时的、而是根据你订阅接口发送的发货地和收货地来绘制的、如果想要获取准确的地图需要额外处理。

3.推送接口推的数据经常不返回地图的url、这里可以处理一下、增加个判断、这样快递100会等待半个小时之后再推送一次。

4.顺丰快递需要手机号才能查到

5.订阅的时候resultv2的值别用默认的、可以用5或者7

6.我们使用的快递100的api、地图url在未签收前有三天有效期、签收后就只有15天了、如果想要持久化物流信息的话、最好还是拿他们返回的地理位置自己进行渲染、如果觉得不影响、可以直接使用他们返回的url。

if param.LastResult_.TrailUrl == "" {
    ctx.JSON(500, gin.H{
        "result":     false,
        "returnCode": "500",
        "message":    "trailUrl数据为空",
    })

1、基本信息

接口文档

api.kuaidi100.com/document/60…

一个订阅接口一个推送接口

image-SdUV.png

1.1订阅接口

需要发送字段

参数名是否必填类型说明
keystring授权码,请申请企业版获取
companystring订阅的快递公司的编码,一律用小写字母
numberstring订阅的快递单号,单号的最大长度是40个字符
fromstring快递寄件地址
tostring快递收件地址
parametersObject辅助参数

parameters数据结构:

参数名是否必填类型说明
callbackurlstring回调接口的地址
saltstring签名用随机字符串
phonestring收寄件人的移动电话号码(只能填写一个,顺丰速运和丰网速运必填,其他快递公司选填)
ordertimestring订单下单时间,格式“yyyy-MM-dd HH”
resultv2string添加此字段表示开通行政区域解析功能。0:关闭(默认),1:开通行政区域解析功能以及物流轨迹增加物流状态值,2:开通行政解析功能以及物流轨迹增加物流状态值并且返回出发、目的及当前城市信息 4:开通行政解析功能以及物流轨迹增加物流高级状态名称并且返回出发、目的及当前城市信息 6:开通行政解析功能以及物流轨迹增加物流高级状态名称、状态值并且返回出发、目的及当前城市信息

1.2推送接口

接收的字段

项目值/说明
请求方式POST
Header参数Content-Type: application/x-www-form-urlencoded
Body参数见下表

Body参数

参数名必填类型说明
signstring加密签名(需转32位大写),算法:md5(param+salt)(仅当salt值非空时推送)
paramObject主体参数对象

param对象结构

字段名必填类型说明可选值
statusString监控状态polling(监控中)、shutdown(结束)、abort(中止)、updateall(重新推送)
billstatusString已弃用,忽略即可got, sending, check
messageString状态消息(如:3天查询无记录, 60天无变化
autoCheckString快递公司编码纠错标记0(原始编码)、1(纠正后的编码)
comOldString原始快递公司编码(当autoCheck=1或status=abort时有效;国际版接口无此字段)如:yuantong
comNewString纠正后的快递公司编码(当autoCheck=1时有效;国际版接口无此字段)如:ems
lastResultObject最新查询结果数据

lastResult对象结构

字段名类型说明特殊条件
messageString消息体(忽略)
stateString核心状态:0在途, 1揽收, 2疑难, 3签收, 4退签, 5派件, 8清关, 14拒签
statusString通讯状态(忽略)
conditionString明细状态标记(未实现)
ischeckInteger签收标记(0未签收, 1已签收,建议用state字段替代)
comString快递公司编码(小写)
nuString快递单号
trailUrlString轨迹地图链接resultv2=7时值为null
arrivalTimeString预计到达时间
totalTimeString平均耗时
remainTimeString剩余时间
isLoopBoolean是否存在运输环路
routeInfoObject行政区划路由信息
dataArray物流轨迹详情(倒序排列)
predictedRouteArray物流节点预测数据需传参resultv2=7

routeInfo(行政区划路由)

字段名子字段类型说明
fromnumberString出发地行政区编码
nameString出发地名称
curnumberString当前地行政区编码
nameString当前地名称
tonumberString目的地行政区编码
nameString目的地名称

data(物流轨迹详情)

字段名类型说明触发条件
contextString物流信息内容
timeString原始时间(如:2025-04-16 10:36:46)
ftimeString格式化后时间
statusString物流状态名称resultv2=3或5
areaCodeString行政区域编码resultv2=3或5
areaNameString行政区域名称resultv2=3或5
statusCodeString高级物流状态值resultv2=5
areaCenterString行政区域经纬度(高德坐标)resultv2=5
locationString快件当前位置resultv2=5
areaPinYinString行政区域拼音resultv2=5

predictedRoute(物流节点预测)

字段名类型说明示例值
arriveTimeString到达节点时间2025-04-16 10:36:46
leaveTimeString离开节点时间2025-04-16 10:39:03
provinceString节点所在省湖北
cityString节点所在市宜昌市
districtString节点所在区点军区
nameString节点名称武汉转运中心
stateString节点状态:已经过/当前停留/预估途径
typeString节点类型:转运中心/网点
locationString经纬度坐标(高德)

1.2.1关键业务逻辑说明(如果业务要求不高就暂时不需要处理)

  1. 状态关联
  • 当快递签收 → status="shutdown"
  • 当连续60天无变化/3天无记录 → status="abort"(需特殊处理)
  1. 公司编码纠错
  • 连续3天查不到结果时:
  • 若原始编码正确 → 推送 autoCheck=0
  • 若编码错误 → 自动用新编码订阅,并推送:
    autoCheck=1, comOld=原编码, comNew=新编码
  1. 高级字段触发
  • 需传参 resultv2=3/5/7 才会返回高级字段(如statusCode/predictedRoute)。

2、表结构

2.1订阅接口

为什么要这么设计

一般来说、当我们把商品出仓或者入仓的时候、需要追踪物流、来查看当前商品的运输情况、这里只讨论出仓的时候、入仓也是一样的逻辑。

2.1.1.基础字段

key、company、number、from、to、parameters、Object、callbackurl(地址我做了三级下拉框所以一共是6个字段)

这几个是必填的、需要我们发送到快递100那边、对快递进行订阅、

id 、created_at 、updated_at 、created_user_id 、created_user_nickname 、updated_user_id 、updated_user_nickname 、deleted 、organization_id

这几个字段是基础字段、一般创建表的时候都要包含这几个字段

2.1.2特殊字段

另外还有一些特殊字段、因为出库单的上游可能是不同类型的单据、比如销售的、采购退货的、所以需要一个type来表示不同的单据、一个relevance_id来跟其他单据做关联、另外需要记录一下你推送给快递100的推送状态、所以我设计了map_tracker_status 这个字段、下图为出库单的表结构(按需修改)

create table sims_enter_leave_inventory_bill
(
  id                      bigint auto_increment
  primary key,
  created_at              datetime       default CURRENT_TIMESTAMP null,
  updated_at              datetime       default CURRENT_TIMESTAMP null,
  created_user_id         bigint                                   null,
  created_user_nickname   varchar(500)                             null,
  updated_user_id         bigint                                   null,
  updated_user_nickname   varchar(500)                             null,
  deleted                 int(1)         default 0                 null,
  organization_id         bigint                                   null,

  name                    varchar(200)                             null comment '单据名称',
  code                    varchar(500)                             null,
  direction               varchar(10)                              null comment '0:入库 1:出库',
  type                    varchar(10)                              null comment '【入库】0:进销存售后单 1:采购订单 2:人工新批次 3:人工已出批次 4:组合单 5:销售订单耗用 6:商城售后单 【出库】0:进销存订单 1:采购售后 2:人工出库 3:组合单 5:销售订单耗用 6:商城订单',
  relevance_id            bigint                                   null,
  relevance_code          varchar(500)                             null,
  counterparty_id         bigint                                   null comment '供应商Id',
  status                  varchar(10)                              null comment '【入库】0:待确认 1:待入库 2:已完成 3:取消 4:失败  【出库】0:待确认 1:待出库 2:待收货 3:已完成 4:取消 5:失败',
  warehouse_id            bigint                                   null,
  product_num             bigint                                   null,
  delivery_company        varchar(500)                             null,
  delivery_num            varchar(500)                             null,
  map_tracker_status      varchar(20)    default '0'               null comment '地图轨迹推送状态: 0=未推送, 1=已推送, 2=推送失败',
  message                 text                                     null,
  remark                  varchar(200)                             null comment '备注',
  operate_date            varchar(50)                              null comment '操作时间',
  price_tax_expenses      decimal(18, 2) default 0.00              null comment '整单分摊的商品价格+税',
  additional_tax_expenses decimal(18, 2) default 0.00              null comment '整单分摊的附加费+税',
  product_tax_expenses    decimal(18, 2) default 0.00              null comment '整单分摊的商品税',
  purchase_tax_expenses   decimal(18, 2) default 0.00              null comment '整单分摊的附加费税',
  sale_price              varchar(250)   default '0.00'            null comment '销售单价',
  sale_price_decimal      decimal(18, 2) default 0.00              null comment '售后-销售总价',
  income_expend_type      varchar(4)                               null comment '类型 1-非订单收入 2-股东投入分配 3-非订单费用支出',
  total_amount            decimal(18, 2) default 0.00              null comment '人工出入库单据总金额',
  product_total_amount    decimal(18, 2) default 0.00              null comment '出入库单据商品总金额',
    order_expense_category  varchar(255)                             null comment '订单支出类别',
    delivery_province_code  varchar(255)                             null comment '收货地址省份编码',
    delivery_city_code      varchar(255)                             null comment '收货地址城市编码',
    delivery_area_code      varchar(255)                             null comment '收货地址区县编码',
    ship_province_code      varchar(255)                             null comment '发货地址省份编码',
    ship_city_code          varchar(255)                             null comment '发货地址城市编码',
    ship_area_code          varchar(255)                             null comment '发货地址区县编码',
    phone_number            varchar(255)                             null comment '物流电话'
)
    charset = utf8mb4;

create index relevance_type_idx
    on sims_enter_leave_inventory_bill (direction, type, relevance_id);

2.2推送接口

2.2.1基础字段

同订阅接口

2.2.2特殊字段

create table soms.express_tracking
(
    id                        bigint unsigned auto_increment comment '主键ID'
        primary key,
    express_no                varchar(40)                                                                  not null comment '快递单号',
    express_company           varchar(20)                                                                  not null comment '快递公司编码',
    request_sign              varchar(64)                                                                  null comment '请求签名(md5(param+salt))',
    auto_check                tinyint                                            default 0                 null comment '公司编码是否纠正(0:未纠正,1:已纠正)',
    original_express_company  varchar(20)                                                                  null comment '原始快递公司编码',
    corrected_express_company varchar(20)                                                                  null comment '纠正后的快递公司编码',
    track_status              enum ('polling', 'shutdown', 'abort', 'updateall') default 'polling'         null comment '跟踪状态',
    track_message             varchar(255)                                                                 null comment '跟踪状态消息',
    express_state             int                                                                          null comment '运单状态值',
    advanced_status           varchar(50)                                                                  null comment '高级状态名称',
    advanced_status_code      int                                                                          null comment '高级状态值',
    current_location          varchar(100)                                                                 null comment '当前位置描述',
    area_code                 varchar(20)                                                                  null comment '行政区编码',
    area_name                 varchar(50)                                                                  null comment '行政区名称',
    route_from_code           varchar(20)                                                                  null comment '出发地行政区编码',
    route_from_name           varchar(50)                                                                  null comment '出发地行政区名称',
    route_to_code             varchar(20)                                                                  null comment '目的地行政区编码',
    route_to_name             varchar(50)                                                                  null comment '目的地行政区名称',
    route_current_code        varchar(20)                                                                  null comment '当前行政区编码',
    route_current_name        varchar(50)                                                                  null comment '当前行政区名称',
    estimated_arrival_time    datetime                                                                     null comment '预计到达时间',
    tracking_updated_at       datetime                                                                     not null comment '最后跟踪时间',
    trail_url                 varchar(255)                                                                 null comment '轨迹地图URL',
    created_at                datetime                                           default CURRENT_TIMESTAMP null comment '创建时间',
    updated_at                datetime                                           default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间',
    created_user_id           bigint                                                                       null comment '创建人ID',
    updated_user_id           bigint                                                                       null comment '更新人ID',
    created_user_nickname     varchar(100)                                                                 null comment '创建人昵称',
    updated_user_nickname     varchar(100)                                                                 null comment '更新人昵称',
    deleted                   tinyint                                            default 0                 null comment '删除标记(0:未删除,1:已删除)'
)
    comment '快递轨迹跟踪表' charset = utf8mb4;

create index idx_express_company
    on soms.express_tracking (express_company);

create index idx_express_no
    on soms.express_tracking (express_no);

create index idx_track_status
    on soms.express_tracking (track_status);
create table soms.tracking_points
(
    id                    bigint unsigned auto_increment comment '主键ID'
        primary key,
    tracking_id           bigint unsigned                    not null comment '关联跟踪记录ID',
    point_description     text                               not null comment '轨迹点描述',
    point_time            datetime                           null comment '轨迹点时间',
    point_status          varchar(50)                        null comment '状态名称',
    status_code           int                                null comment '状态代码',
    area_code             varchar(20)                        null comment '区域编码',
    area_name             varchar(50)                        null comment '区域名称',
    created_at            datetime default CURRENT_TIMESTAMP null comment '创建时间',
    updated_at            datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间',
    created_user_id       bigint                             null comment '创建人ID',
    updated_user_id       bigint                             null comment '更新人ID',
    created_user_nickname varchar(100)                       null comment '创建人昵称',
    updated_user_nickname varchar(100)                       null comment '更新人昵称',
    deleted               tinyint  default 0                 null comment '删除标记(0:未删除,1:已删除)',
    constraint tracking_points_ibfk_1
        foreign key (tracking_id) references soms.express_tracking (id)
            on delete cascade
)
    comment '快递轨迹点明细表' charset = utf8mb4;

create index idx_point_time
    on soms.tracking_points (point_time);

create index idx_tracking_id
    on soms.tracking_points (tracking_id);
create table soms.tracking_special_status
(
    id                    bigint unsigned auto_increment comment '主键ID'
        primary key,
    tracking_id           bigint unsigned                                        not null comment '关联跟踪记录ID',
    status_type           enum ('abort-3day', 'company-corrected', 'suspicious') null comment '特殊状态类型',
    action_taken          enum ('resubmitted', 'marked-fake', 'updated-company') null comment '已采取的行动',
    action_date           datetime                                               null comment '行动日期',
    resubmit_count        tinyint  default 0                                     null comment '重新提交次数',
    created_at            datetime default CURRENT_TIMESTAMP                     null comment '创建时间',
    updated_at            datetime default CURRENT_TIMESTAMP                     null on update CURRENT_TIMESTAMP comment '更新时间',
    created_user_id       bigint                                                 null comment '创建人ID',
    updated_user_id       bigint                                                 null comment '更新人ID',
    created_user_nickname varchar(100)                                           null comment '创建人昵称',
    updated_user_nickname varchar(100)                                           null comment '更新人昵称',
    deleted               tinyint  default 0                                     null comment '删除标记(0:未删除,1:已删除)',
    constraint tracking_special_status_ibfk_1
        foreign key (tracking_id) references soms.express_tracking (id)
            on delete cascade
)
    comment '特殊状态处理记录表' charset = utf8mb4;

create index idx_status_type
    on soms.tracking_special_status (status_type);

create index tracking_id
    on soms.tracking_special_status (tracking_id);

3.接口的实现

3.1订阅接口

订阅接口很简单、从表里面查出需要的数据进行组装、发送给快递100、发送形式为表单类型的数据、订阅成功之后快递100官方会调用你的回调地址把物流相关信息返回给你、可以查看官方文档。

需要注意的点:

顺丰快递需要发手机号、salt表示使用加密签名、resultv2(3、5、7返回的信息越来越详细)

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"net/url"
	"time"
)

// 快递100订阅接口配置
const (
	SubscribeURL = "http://poll.kuaidi100.com/pollmap"
	APIKey       = "YOUR_API_KEY_HERE" // 替换为实际API密钥
	CallbackURL  = "https://yourdomain.com/callback"
	Salt         = "your_random_salt"
	ResultV2     = "5" // 使用5获取更多物流详情
)

// 订阅请求数据结构
type SubscribeRequest struct {
	Key      string                 `json:"key"`
	Company  string                 `json:"company"`
	Number   string                 `json:"number"`
	From     string                 `json:"from"`
	To       string                 `json:"to"`
	Params   SubscribeParams        `json:"parameters"`
}

type SubscribeParams struct {
	CallbackURL string `json:"callbackurl"`
	Salt        string `json:"salt,omitempty"`
	Phone       string `json:"phone,omitempty"` // 顺丰快递需要此参数
	ResultV2    string `json:"resultv2,omitempty"`
}

// 订阅响应数据结构
type SubscribeResponse struct {
	Result     bool   `json:"result"`
	ReturnCode string `json:"returnCode"`
	Message    string `json:"message"`
}

func main() {
	// 模拟需要订阅的订单数据
	orders := []struct {
		ID             int
		DeliveryCompany string
		DeliveryNum     string
		FromAddress    string
		ToAddress      string
		Phone          string
	}{
		{
			ID:             1,
			DeliveryCompany: "yuantong",
			DeliveryNum:     "YT1234567890",
			FromAddress:    "上海市浦东新区",
			ToAddress:      "北京市朝阳区",
			Phone:          "13800138000", // 顺丰快递需要手机号
		},
		// 可以添加更多订单...
	}

	// 遍历订单进行订阅
	for _, order := range orders {
		err := subscribeExpress(order)
		if err != nil {
			log.Printf("订单 %d 订阅失败: %v", order.ID, err)
			continue
		}
		log.Printf("订单 %d 订阅成功", order.ID)
	}
}

// 订阅快递单号
func subscribeExpress(order struct {
	ID             int
	DeliveryCompany string
	DeliveryNum     string
	FromAddress    string
	ToAddress      string
	Phone          string
}) error {
	// 构建订阅请求
	req := SubscribeRequest{
		Key:     APIKey,
		Company: order.DeliveryCompany,
		Number:  order.DeliveryNum,
		From:    order.FromAddress,
		To:      order.ToAddress,
		Params: SubscribeParams{
			CallbackURL: CallbackURL,
			Salt:        Salt,
			Phone:       order.Phone,
			ResultV2:    ResultV2,
		},
	}

	// 转换为JSON
	paramJSON, err := json.Marshal(req)
	if err != nil {
		return fmt.Errorf("JSON编码失败: %v", err)
	}

	// 构建表单数据
	formData := url.Values{}
	formData.Set("schema", "json")
	formData.Set("param", string(paramJSON))

	// 发送HTTP请求
	resp, err := http.PostForm(SubscribeURL, formData)
	if err != nil {
		return fmt.Errorf("HTTP请求失败: %v", err)
	}
	defer resp.Body.Close()

	// 解析响应
	var result SubscribeResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return fmt.Errorf("响应解析失败: %v", err)
	}

	// 检查订阅结果
	if result.ReturnCode != "200" {
		return fmt.Errorf("订阅失败: %s", result.Message)
	}

	return nil
}

// 注意:实际应用中应该添加重试机制和更完善的错误处理

3.2推送接口

推送接口就是快递100把物流相关信息推送过来、trailUrl就是我们需要的地图url、可以直接引入。

因为推送数据包含基础信息、和物流轨迹信息、还有一个特殊状态、所以我是设计了3个表(根据官方文档按需要设计自己的表)

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"
)

// 推送数据结构
type PushData struct {
	Sign  string `json:"sign"`
	Param string `json:"param"`
}

type PushParam struct {
	Status    string     `json:"status"`
	AutoCheck string     `json:"autoCheck"`
	ComOld    string     `json:"comOld,omitempty"`
	ComNew    string     `json:"comNew,omitempty"`
	LastResult LastResult `json:"lastResult"`
}

type LastResult struct {
	TrailUrl string `json:"trailUrl"`
	// 其他字段根据需要添加
}

func main() {
	r := gin.Default()
	
	// 推送回调接口
	r.POST("/callback", handleCallback)
	
	log.Println("启动快递100推送服务,监听端口:8080")
	log.Fatal(r.Run(":8080"))
}

// 回调处理函数
func handleCallback(c *gin.Context) {
	// 1. 读取原始请求体
	body, err := io.ReadAll(c.Request.Body)
	if err != nil {
		log.Printf("读取请求体失败: %v\n", err)
		c.JSON(400, gin.H{"error": "读取请求体失败"})
		return
	}
	
	// 打印原始请求体(调试用)
	log.Printf("原始请求体: %s\n", string(body))
	
	// 重置请求体,以便后续处理
	c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
	
	// 2. 尝试获取sign和param参数
	sign := c.PostForm("sign")
	paramStr := c.PostForm("param")
	
	// 3. 兼容字段名带空白符的情况
	if paramStr == "" || sign == "" {
		for key, values := range c.Request.Form {
			trimmedKey := strings.TrimSpace(key)
			if trimmedKey == "param" && paramStr == "" && len(values) > 0 {
				paramStr = values[0]
				log.Printf("通过遍历Form找到param: %s\n", paramStr)
			}
			if trimmedKey == "sign" && sign == "" && len(values) > 0 {
				sign = values[0]
				log.Printf("通过遍历Form找到sign: %s\n", sign)
			}
		}
	}
	
	// 4. 尝试其他方式获取参数
	if paramStr == "" {
		// 尝试从Query参数获取
		paramStr = c.Query("param")
		log.Printf("从Query参数获取param: %s\n", paramStr)
	}
	
	if paramStr == "" {
		// 尝试从JSON body获取
		var jsonBody map[string]interface{}
		if err := c.ShouldBindJSON(&jsonBody); err == nil {
			if param, ok := jsonBody["param"].(string); ok {
				paramStr = param
				log.Printf("从JSON body获取param: %s\n", paramStr)
			}
		}
	}
	
	// 5. 尝试从原始body中解析
	if paramStr == "" && len(body) > 0 {
		log.Printf("尝试从原始body解析form数据\n")
		// 手动解析form数据
		if err := c.Request.ParseForm(); err == nil {
			paramStr = c.Request.FormValue("param")
			sign = c.Request.FormValue("sign")
			log.Printf("从手动解析的form获取: sign=%s, param=%s\n", sign, paramStr)
		}
	}
	
	// 6. 检查参数是否存在
	if paramStr == "" {
		log.Printf("param参数缺失,返回错误\n")
		c.JSON(400, gin.H{"msg": "param参数缺失"})
		return
	}
	
	// 7. 解析param字段(JSON字符串)
	var param PushParam
	if err := json.Unmarshal([]byte(paramStr), &param); err != nil {
		log.Printf("param解析失败: %v, paramStr: %s\n", err, paramStr)
		c.JSON(400, gin.H{"msg": "param解析失败"})
		return
	}
	
	// 8. 打印解析后的完整数据结构
	log.Printf("解析后的快递100推送数据:sign=%s, param=%+v\n", sign, param)
	
	// 9. 业务处理(这里简化为打印和保存)
	processPushData(sign, param)
	
	// 10. 检查地图URL是否存在
	if param.LastResult.TrailUrl == "" {
		log.Println("trailUrl数据为空")
		c.JSON(500, gin.H{
			"result":     false,
			"returnCode": "500",
			"message":    "trailUrl数据为空",
		})
	} else {
		log.Println("处理成功")
		// 返回成功响应
		c.JSON(200, gin.H{
			"result":     true,
			"returnCode": "200",
			"message":    "success",
		})
	}
}

// 处理推送数据
func processPushData(sign string, param PushParam) {
	// 这里可以:
	// 1. 验证签名(param+Config.Salt)
	// 2. 保存物流信息到数据库
	// 3. 处理特殊状态(签收/中止等)
	
	log.Printf("收到推送: sign=%s, status=%s, trailUrl=%s", 
		sign, param.Status, param.LastResult.TrailUrl)
	
	// 示例:打印所有数据
	log.Printf("完整推送数据: %+v", param)
	
	// 公司编码纠错处理
	if param.AutoCheck == "1" {
		log.Printf("公司编码纠错: %s -> %s", param.ComOld, param.ComNew)
	}
	
	// 处理特殊状态
	switch param.Status {
	case "shutdown":
		log.Println("快递已签收")
	case "abort":
		log.Println("快递状态中止(60天无变化/3天无记录)")
	case "updateall":
		log.Println("重新推送所有数据")
	}
	
	// 保存到数据库(伪代码)
	// saveToDatabase(param)
}

// 模拟保存到数据库
func saveToDatabase(param PushParam) {
	log.Printf("保存物流信息到数据库: 单号=%s, 状态=%s", param.LastResult.TrailUrl, param.Status)
	// 实际实现数据库保存逻辑
}

需要注意的点:

推送接口的数据的数据不是使用的json、是表单格式、单纯请求表单请求不到、需要兼容带空格的数据

重点是下面这一段

// 兼容字段名带空白符的情况
if paramStr == "" || sign == "" {
    for key, values := range ctx.Request.Form {
        trimmedKey := strings.TrimSpace(key)
        if trimmedKey == "param" && paramStr == "" && len(values) > 0 {
            paramStr = values[0]
            log.Printf("通过遍历Form找到param: %s\n", paramStr)
        }
        if trimmedKey == "sign" && sign == "" && len(values) > 0 {
            sign = values[0]
            log.Printf("通过遍历Form找到sign: %s\n", sign)
        }
    }
}

3.3获取物流信息

这个就很简单了、在单据上面加一个按钮、点击按钮获取物流信息跟物流轨迹、地图直接嵌入trailurl(都是从表里面拿的)在页面上显示

	// 1. 查询所有与商城订单ID相关的出库单
	param := &repo.SimsEnterLeaveInventoryBillDBDataParam{
		SimsEnterLeaveInventoryBillDBData: repo.SimsEnterLeaveInventoryBillDBData{
			Type:        "6",
			RelevanceId: orderId,
		},
	}
	dbList, count, err := s.billRepo.List(ctx, "", 0, 0, param)
	if err != nil {
		return nil, err
	}
	resp := &api.BatchGetExpressTrackingResponse{
		Code:    "200",
		Message: "success",
	}

	if dbList == nil || len(*dbList) == 0 {
		log.Printf("GetExpressTrackingByOrderId: 未查到出库单")
		resp.Code = "404"
		resp.Message = "未查到出库单"
		resp.Data = nil
		return resp, nil
	}

	var data []*api.ExpressTrackingInfo
	var failedNos []string
	var successCount, failureCount int32

	for _, dbData := range *dbList {
		if dbData.DeliveryNum == "" {
			log.Printf("GetExpressTrackingByOrderId: 出库单ID=%d 未填写快递单号", dbData.Id)
			failedNos = append(failedNos, "")
			failureCount++
			continue
		}
		// 2. 根据快递单号获取物流信息
		expressData, err := s.trackingRepo.GetByExpressNo(ctx, dbData.DeliveryNum)
		if err != nil || expressData == nil {
			log.Printf("GetExpressTrackingByOrderId: 出库单ID=%d 未查到物流信息", dbData.Id)
			failedNos = append(failedNos, dbData.DeliveryNum)
			failureCount++
			continue
		}
		// 3. 组装数据
		pointsData, _ := s.pointsRepo.GetByTrackingID(ctx, expressData.Id)
		specialData, _ := s.specialRepo.GetByTrackingID(ctx, expressData.Id)
		info := stru.ConvertToExpressTrackingInfo(expressData, pointsData, specialData)
		data = append(data, info)
		successCount++
	}

	resp.Data = data
	resp.SuccessCount = successCount
	resp.FailureCount = failureCount
	resp.FailedExpressNos = failedNos
	resp.ResponseAt = time.Now().Format("2006-01-02 15:04:05")
	resp.ProcessingTimeMs = 0 // 可选

	if len(data) == 0 {
		resp.Code = "404"
		resp.Message = "未查到任何物流信息"
	}

	return resp, nil
}