front/track_push.py
2026-01-24 11:55:01 +08:00

1000 lines
53 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import tkinter as tk
from tkinter import ttk, scrolledtext, filedialog, messagebox
import hashlib
import base64
import json
import urllib.parse
import requests
import re
import time
from datetime import datetime
import openpyxl
from openpyxl.styles import Font, PatternFill
from openpyxl.worksheet.datavalidation import DataValidation
import os
from pathlib import Path
class TrackPushApp:
def __init__(self, root):
self.root = root
self.root.title("轨迹推送")
self.root.geometry("1100x1100")
# 主容器
main_frame = tk.Frame(root, padx=20, pady=20)
main_frame.pack(fill=tk.BOTH, expand=True)
row = 0
# SecretKey
tk.Label(main_frame, text="SecretKey:", width=15, anchor='w').grid(row=row, column=0, padx=5, pady=5, sticky='w')
self.secret_key_entry = tk.Entry(main_frame, width=100)
self.secret_key_entry.insert(0, "y299lC8N2d8ebm4001MHFq507kPDT458")
self.secret_key_entry.grid(row=row, column=1, padx=5, pady=5, sticky='w')
row += 1
# url(支持下拉选择)
tk.Label(main_frame, text="url:", width=15, anchor='w').grid(row=row, column=0, padx=5, pady=5, sticky='w')
url_values = [
"https://link.cainiao.com/gateway/link.do",
"https://prelink.cainiao.com/gateway/link.do",
]
self.url_var = tk.StringVar(value=url_values[1])
self.url_entry = ttk.Combobox(main_frame, textvariable=self.url_var, values=url_values, width=96, state='readonly')
self.url_entry.grid(row=row, column=1, padx=5, pady=5, sticky='w')
row += 1
# 分隔线
tk.Label(main_frame, text="公共参数", font=("Arial", 10, "bold")).grid(row=row, column=0, columnspan=2, padx=5, pady=(15, 5), sticky='w')
row += 1
# logistic_provider_id
tk.Label(main_frame, text="资源code:", width=15, anchor='w').grid(row=row, column=0, padx=5, pady=5, sticky='w')
logistic_provider_id_values = [
"带电渠道-干线[TRUNK_31726544]",
"带电渠道-关务[GATE_31726757]",
"带电渠道-配送[DISTRIBUTOR_31724076]",
"普货渠道-干线[TRUNK_31724331]",
"普货渠道-关务[GATE_31724116]",
"普货渠道-配送[DISTRIBUTOR_31724341]",
]
self.logistic_provider_id_var = tk.StringVar(value=logistic_provider_id_values[0])
logistic_provider_id_combo = ttk.Combobox(main_frame, textvariable=self.logistic_provider_id_var, width=94, state='readonly')
logistic_provider_id_combo['values'] = logistic_provider_id_values
logistic_provider_id_combo.grid(row=row, column=1, padx=5, pady=5, sticky='w')
row += 1
# msg_type
tk.Label(main_frame, text="接口名称:", width=15, anchor='w').grid(row=row, column=0, padx=5, pady=5, sticky='w')
# msg_type映射:显示中文,传值用code
self.msg_type_map = {
"CAINIAO_GLOBAL_LINEHAUL_HOIN_CALLBACK": "收货",
"CAINIAO_GLOBAL_LINEHAUL_HOAIRLINE_CALLBACK": "交航",
"CAINIAO_GLOBAL_LINEHAUL_DEPARTURE_CALLBACK": "起飞",
"CAINIAO_GLOBAL_LINEHAUL_ARRIVAL_CALLBACK": "落地",
"CAINIAO_GLOBAL_CCIM_CALLBACK": "清关完成",
"CAINIAO_GLOBAL_CCIM_ABNORMAL_CALLBACK": "清关异常",
"CAINIAO_GLOBAL_LASTMILE_GTMSACCEPT_CALLBACK": "配送接收",
"CAINIAO_GLOBAL_LASTMILE_GTMSSIGN_CALLBACK": "签收",
"CAINIAO_GLOBAL_LINEHAUL_UNREACHABLERETURN_CALLBACK": "干线退回发起",
"CAINIAO_GLOBAL_LINEHAUL_UNREACHABLERETURNPROCESS_CALLBACK": "干线退回处理",
"CAINIAO_GLOBAL_LINEHAUL_UNREACHABLERETURN_HANDOVEROUT_CALLBACK": "干线退回交接",
"CAINIAO_GLOBAL_CUSTOMS_UNREACHABLERETURN_CALLBACK": "清关失败"
}
# msg_type到opCode的映射(根据中文描述)
self.msg_type_to_opcode = {
"包裹信息不全": "5247",
"申报信息错误/缺失": "5562",
"消费者拒收": "5792",
"收货地址未知或错误": "5525"
}
# 添加需要自动填充opCode的msg_type选项(包含完整列表)
msg_type_list = [
("CAINIAO_GLOBAL_LINEHAUL_HOIN_CALLBACK", "收货"),
("CAINIAO_GLOBAL_LINEHAUL_HOAIRLINE_CALLBACK", "交航"),
("CAINIAO_GLOBAL_LINEHAUL_DEPARTURE_CALLBACK", "起飞"),
("CAINIAO_GLOBAL_LINEHAUL_ARRIVAL_CALLBACK", "落地"),
("CAINIAO_GLOBAL_CCIM_CALLBACK", "清关完成"),
("CAINIAO_GLOBAL_CCIM_ABNORMAL_CALLBACK", "清关异常"),
("CAINIAO_GLOBAL_LASTMILE_GTMSACCEPT_CALLBACK", "配送接收"),
("CAINIAO_GLOBAL_LASTMILE_GTMSSIGN_CALLBACK", "签收"),
("CAINIAO_GLOBAL_LINEHAUL_UNREACHABLERETURN_CALLBACK", "干线退回发起"),
("CAINIAO_GLOBAL_LINEHAUL_UNREACHABLERETURNPROCESS_CALLBACK", "干线退回处理"),
("CAINIAO_GLOBAL_LINEHAUL_UNREACHABLERETURN_HANDOVEROUT_CALLBACK", "干线退回交接"),
("CAINIAO_GLOBAL_LINEHAUL_HOAIRLINE_CALLBACK", "包裹信息不全"),
("CAINIAO_GLOBAL_CCIM_CALLBACK", "申报信息错误/缺失"),
("CAINIAO_GLOBAL_LASTMILE_GTMSSIGN_CALLBACK", "消费者拒收"),
("CAINIAO_GLOBAL_LASTMILE_GTMSSIGN_CALLBACK", "收货地址未知或错误"),
("CAINIAO_GLOBAL_CUSTOMS_UNREACHABLERETURN_CALLBACK", "清关失败")
]
msg_type_values = [f"{code} - {desc}" for code, desc in msg_type_list]
self.msg_type_var = tk.StringVar(value=msg_type_values[1]) # 默认选择"交航"
msg_type_combo = ttk.Combobox(main_frame, textvariable=self.msg_type_var, width=94, state='readonly')
msg_type_combo['values'] = msg_type_values
msg_type_combo.grid(row=row, column=1, padx=5, pady=5, sticky='w')
msg_type_combo.bind('<<ComboboxSelected>>', self.on_msg_type_changed)
row += 1
# 分隔线
tk.Label(main_frame, text="logistics_interface 参数", font=("Arial", 10, "bold")).grid(row=row, column=0, columnspan=2, padx=5, pady=(15, 5), sticky='w')
row += 1
# logisticsOrderCode
tk.Label(main_frame, text="AP单号:", width=15, anchor='w').grid(row=row, column=0, padx=5, pady=5, sticky='w')
self.logistics_order_code_entry = tk.Entry(main_frame, width=100)
self.logistics_order_code_entry.grid(row=row, column=1, padx=5, pady=5, sticky='w')
row += 1
# opCode
tk.Label(main_frame, text="作业结果:", width=15, anchor='w').grid(row=row, column=0, padx=5, pady=5, sticky='w')
op_code_frame = tk.Frame(main_frame)
op_code_frame.grid(row=row, column=1, padx=5, pady=5, sticky='w')
self.op_code_entry = tk.Entry(op_code_frame, width=80)
self.op_code_entry.pack(side=tk.LEFT)
tk.Label(op_code_frame, text="(0=成功, 1=失败, 其他=异常code)", font=("Arial", 8), fg="gray").pack(side=tk.LEFT, padx=(5, 0))
row += 1
# opLocation
tk.Label(main_frame, text="操作地:", width=15, anchor='w').grid(row=row, column=0, padx=5, pady=5, sticky='w')
self.op_location_entry = tk.Entry(main_frame, width=100)
self.op_location_entry.grid(row=row, column=1, padx=5, pady=5, sticky='w')
row += 1
# opRemark
tk.Label(main_frame, text="备注:", width=15, anchor='w').grid(row=row, column=0, padx=5, pady=5, sticky='w')
self.op_remark_entry = tk.Entry(main_frame, width=100)
self.op_remark_entry.grid(row=row, column=1, padx=5, pady=5, sticky='w')
row += 1
# opTime
tk.Label(main_frame, text="操作时间:", width=15, anchor='w').grid(row=row, column=0, padx=5, pady=5, sticky='w')
op_time_frame = tk.Frame(main_frame)
op_time_frame.grid(row=row, column=1, padx=5, pady=5, sticky='w')
self.op_time_entry = tk.Entry(op_time_frame, width=80)
self.op_time_entry.insert(0, datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
self.op_time_entry.pack(side=tk.LEFT)
update_time_btn = tk.Button(op_time_frame, text="更新为当前时间", command=self.update_op_time)
update_time_btn.pack(side=tk.LEFT, padx=(5, 0))
row += 1
# relabelTrackingNumber
tk.Label(main_frame, text="转单号:", width=15, anchor='w').grid(row=row, column=0, padx=5, pady=5, sticky='w')
self.relabel_tracking_number_entry = tk.Entry(main_frame, width=100)
self.relabel_tracking_number_entry.grid(row=row, column=1, padx=5, pady=5, sticky='w')
row += 1
# timeZone
tk.Label(main_frame, text="时区:", width=15, anchor='w').grid(row=row, column=0, padx=5, pady=5, sticky='w')
self.time_zone_entry = tk.Entry(main_frame, width=100)
self.time_zone_entry.insert(0, "8")
self.time_zone_entry.grid(row=row, column=1, padx=5, pady=5, sticky='w')
row += 1
# transportType (固定为5,不显示输入框)
# self.transport_type_entry 在代码中固定使用 "5"
row += 1
# parcel字段(默认隐藏,只有选择"交航"时显示)
self.parcel_frame = tk.Frame(main_frame)
self.parcel_frame.grid(row=row, column=0, columnspan=2, padx=5, pady=5, sticky='w')
self.parcel_frame.grid_remove() # 默认隐藏
# weight
tk.Label(self.parcel_frame, text="重量(weight):", width=15, anchor='w').grid(row=0, column=0, padx=5, pady=5, sticky='w')
self.parcel_weight_entry = tk.Entry(self.parcel_frame, width=30)
self.parcel_weight_entry.insert(0, "1450")
self.parcel_weight_entry.grid(row=0, column=1, padx=5, pady=5, sticky='w')
# weightUnit
tk.Label(self.parcel_frame, text="重量单位(weightUnit):", width=20, anchor='w').grid(row=0, column=2, padx=5, pady=5, sticky='w')
weight_unit_values = ["g", "kg"]
self.parcel_weight_unit_var = tk.StringVar(value="g")
weight_unit_combo = ttk.Combobox(self.parcel_frame, textvariable=self.parcel_weight_unit_var, values=weight_unit_values, width=10, state='readonly')
weight_unit_combo.grid(row=0, column=3, padx=5, pady=5, sticky='w')
# length
tk.Label(self.parcel_frame, text="长度(length):", width=15, anchor='w').grid(row=1, column=0, padx=5, pady=5, sticky='w')
self.parcel_length_entry = tk.Entry(self.parcel_frame, width=30)
self.parcel_length_entry.insert(0, "100")
self.parcel_length_entry.grid(row=1, column=1, padx=5, pady=5, sticky='w')
# width
tk.Label(self.parcel_frame, text="宽度(width):", width=15, anchor='w').grid(row=1, column=2, padx=5, pady=5, sticky='w')
self.parcel_width_entry = tk.Entry(self.parcel_frame, width=30)
self.parcel_width_entry.insert(0, "100")
self.parcel_width_entry.grid(row=1, column=3, padx=5, pady=5, sticky='w')
# height
tk.Label(self.parcel_frame, text="高度(height):", width=15, anchor='w').grid(row=2, column=0, padx=5, pady=5, sticky='w')
self.parcel_height_entry = tk.Entry(self.parcel_frame, width=30)
self.parcel_height_entry.insert(0, "100")
self.parcel_height_entry.grid(row=2, column=1, padx=5, pady=5, sticky='w')
# dimensionUnit
tk.Label(self.parcel_frame, text="尺寸单位(dimensionUnit):", width=20, anchor='w').grid(row=2, column=2, padx=5, pady=5, sticky='w')
dimension_unit_values = ["cm", "m"]
self.parcel_dimension_unit_var = tk.StringVar(value="cm")
dimension_unit_combo = ttk.Combobox(self.parcel_frame, textvariable=self.parcel_dimension_unit_var, values=dimension_unit_values, width=10, state='readonly')
dimension_unit_combo.grid(row=2, column=3, padx=5, pady=5, sticky='w')
row += 1
# undeliverableOption和returnTo(默认隐藏,只有选择"干线退回发起"时显示)
self.undeliverable_return_frame = tk.Frame(main_frame)
self.undeliverable_return_frame.grid(row=row, column=0, columnspan=2, padx=5, pady=5, sticky='w')
self.undeliverable_return_frame.grid_remove() # 默认隐藏
# undeliverableOption
tk.Label(self.undeliverable_return_frame, text="处理方式:", width=15, anchor='w').grid(row=0, column=0, padx=5, pady=5, sticky='w')
undeliverable_option_values = [
"不选",
"退回 Return",
"销毁 Discard"
]
self.undeliverable_option_var = tk.StringVar(value=undeliverable_option_values[0])
undeliverable_option_combo = ttk.Combobox(self.undeliverable_return_frame, textvariable=self.undeliverable_option_var, width=94, state='readonly')
undeliverable_option_combo['values'] = undeliverable_option_values
undeliverable_option_combo.grid(row=0, column=1, padx=5, pady=5, sticky='w')
# returnTo
tk.Label(self.undeliverable_return_frame, text="退回方式:", width=15, anchor='w').grid(row=1, column=0, padx=5, pady=5, sticky='w')
return_to_values = [
"不选",
"首公里 FirstMile",
"快递 Express"
]
self.return_to_var = tk.StringVar(value=return_to_values[0])
return_to_combo = ttk.Combobox(self.undeliverable_return_frame, textvariable=self.return_to_var, width=94, state='readonly')
return_to_combo['values'] = return_to_values
return_to_combo.grid(row=1, column=1, padx=5, pady=5, sticky='w')
row += 1
# 干线退回处理专用字段(默认隐藏)
self.return_process_frame = tk.Frame(main_frame)
self.return_process_frame.grid(row=row, column=0, columnspan=2, padx=5, pady=5, sticky='w')
self.return_process_frame.grid_remove() # 默认隐藏
# processType
tk.Label(self.return_process_frame, text="退回方式(processType):", width=20, anchor='w').grid(row=0, column=0, padx=5, pady=5, sticky='w')
process_type_values = [
"销毁 00",
"退回首公里 01",
"寄送 02",
"菜鸟快递寄送 03",
"退集运仓(非快递)04"
]
self.process_type_var = tk.StringVar(value="菜鸟快递寄送 03")
process_type_combo = ttk.Combobox(self.return_process_frame, textvariable=self.process_type_var, width=70, state='readonly')
process_type_combo['values'] = process_type_values
process_type_combo.grid(row=0, column=1, padx=5, pady=5, sticky='w')
# returnPickupAdressProvince
tk.Label(self.return_process_frame, text="退回省份:", width=20, anchor='w').grid(row=1, column=0, padx=5, pady=5, sticky='w')
self.return_pickup_address_province_entry = tk.Entry(self.return_process_frame, width=70)
self.return_pickup_address_province_entry.insert(0, "广东")
self.return_pickup_address_province_entry.grid(row=1, column=1, padx=5, pady=5, sticky='w')
# returnPickupAdressCity
tk.Label(self.return_process_frame, text="退回城市:", width=20, anchor='w').grid(row=2, column=0, padx=5, pady=5, sticky='w')
self.return_pickup_address_city_entry = tk.Entry(self.return_process_frame, width=70)
self.return_pickup_address_city_entry.insert(0, "深圳")
self.return_pickup_address_city_entry.grid(row=2, column=1, padx=5, pady=5, sticky='w')
# returnPickupAdressDistrict
tk.Label(self.return_process_frame, text="退回区县:", width=20, anchor='w').grid(row=3, column=0, padx=5, pady=5, sticky='w')
self.return_pickup_address_district_entry = tk.Entry(self.return_process_frame, width=70)
self.return_pickup_address_district_entry.insert(0, "宝安区")
self.return_pickup_address_district_entry.grid(row=3, column=1, padx=5, pady=5, sticky='w')
# returnPickupAdressDetail
tk.Label(self.return_process_frame, text="退回详细地址:", width=20, anchor='w').grid(row=4, column=0, padx=5, pady=5, sticky='w')
self.return_pickup_address_detail_entry = tk.Entry(self.return_process_frame, width=70)
self.return_pickup_address_detail_entry.insert(0, "怀德翠岗工业区五区36栋二楼")
self.return_pickup_address_detail_entry.grid(row=4, column=1, padx=5, pady=5, sticky='w')
# returnPickupPhone
tk.Label(self.return_process_frame, text="退回电话:", width=20, anchor='w').grid(row=5, column=0, padx=5, pady=5, sticky='w')
self.return_pickup_phone_entry = tk.Entry(self.return_process_frame, width=70)
self.return_pickup_phone_entry.insert(0, "19513356304")
self.return_pickup_phone_entry.grid(row=5, column=1, padx=5, pady=5, sticky='w')
# returnPickupSender
tk.Label(self.return_process_frame, text="退回发件人:", width=20, anchor='w').grid(row=6, column=0, padx=5, pady=5, sticky='w')
self.return_pickup_sender_entry = tk.Entry(self.return_process_frame, width=70)
self.return_pickup_sender_entry.insert(0, "蒋彬")
self.return_pickup_sender_entry.grid(row=6, column=1, padx=5, pady=5, sticky='w')
row += 1
# 按钮区域
button_frame = tk.Frame(main_frame)
button_frame.grid(row=row, column=0, columnspan=2, padx=5, pady=20)
push_button = tk.Button(button_frame, text="推送", bg="#4CAF50", fg="white",
width=12, height=2, command=self.on_push_click)
push_button.pack(side=tk.LEFT, padx=5)
export_button = tk.Button(button_frame, text="导出模板", bg="#2196F3", fg="white",
width=12, height=2, command=self.export_template)
export_button.pack(side=tk.LEFT, padx=5)
import_button = tk.Button(button_frame, text="导入轨迹", bg="#FF9800", fg="white",
width=12, height=2, command=self.import_and_push)
import_button.pack(side=tk.LEFT, padx=5)
row += 1
# 推送日志
log_label = tk.Label(main_frame, text="推送日志:", anchor='w', font=("Arial", 10, "bold"))
log_label.grid(row=row, column=0, columnspan=2, padx=5, pady=(10, 5), sticky='w')
row += 1
self.log_text = scrolledtext.ScrolledText(main_frame, height=25, width=140, wrap=tk.WORD)
self.log_text.grid(row=row, column=0, columnspan=2, padx=5, pady=5, sticky='nsew')
# 配置网格权重
main_frame.grid_columnconfigure(1, weight=1)
def do_sign(self, content, charset, keys):
"""签名算法:MD5(content + keys) 然后转base64"""
try:
sign_content = content + keys
md5_hash = hashlib.md5(sign_content.encode(charset)).digest()
sign = base64.b64encode(md5_hash).decode(charset)
return sign
except Exception as e:
raise Exception(f"签名失败: {str(e)}")
def update_op_time(self):
"""更新opTime为当前时间"""
self.op_time_entry.delete(0, tk.END)
self.op_time_entry.insert(0, datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
def on_msg_type_changed(self, event=None):
"""msg_type选择改变时,自动填充opCode并显示/隐藏相关字段"""
selected = self.msg_type_var.get()
if " - " in selected:
code = selected.split(" - ")[0]
desc = selected.split(" - ")[1]
# 如果选择交航,显示parcel字段
if code == "CAINIAO_GLOBAL_LINEHAUL_HOAIRLINE_CALLBACK":
self.parcel_frame.grid()
else:
self.parcel_frame.grid_remove()
# 如果选择干线退回发起,显示处理方式和退回方式
if code == "CAINIAO_GLOBAL_LINEHAUL_UNREACHABLERETURN_CALLBACK":
self.undeliverable_return_frame.grid()
else:
self.undeliverable_return_frame.grid_remove()
# 如果选择干线退回处理,显示相关字段
if code == "CAINIAO_GLOBAL_LINEHAUL_UNREACHABLERETURNPROCESS_CALLBACK":
self.return_process_frame.grid()
else:
self.return_process_frame.grid_remove()
# 如果选择特定的msg_type,opCode默认为0
default_opcode_0_types = [
"CAINIAO_GLOBAL_LINEHAUL_HOIN_CALLBACK",
"CAINIAO_GLOBAL_LINEHAUL_HOAIRLINE_CALLBACK",
"CAINIAO_GLOBAL_LINEHAUL_DEPARTURE_CALLBACK",
"CAINIAO_GLOBAL_LINEHAUL_ARRIVAL_CALLBACK",
"CAINIAO_GLOBAL_CCIM_CALLBACK",
"CAINIAO_GLOBAL_LASTMILE_GTMSSIGN_CALLBACK",
"CAINIAO_GLOBAL_LASTMILE_GTMSACCEPT_CALLBACK"
]
if code in default_opcode_0_types:
self.op_code_entry.delete(0, tk.END)
self.op_code_entry.insert(0, "0")
elif desc in self.msg_type_to_opcode:
# 如果有特殊映射,使用特殊映射的值
self.op_code_entry.delete(0, tk.END)
self.op_code_entry.insert(0, self.msg_type_to_opcode[desc])
def log(self, message):
"""添加日志"""
self.log_text.insert(tk.END, f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - {message}\n")
self.log_text.see(tk.END)
self.root.update()
def export_template(self):
"""导出Excel模板"""
try:
# 获取桌面路径
desktop_path = Path.home() / "Desktop"
if not desktop_path.exists():
# 如果Desktop不存在,尝试使用中文桌面
desktop_path = Path.home() / "桌面"
# 生成文件名:菜鸟轨迹模板+时间戳
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
default_filename = f"菜鸟轨迹模板_{timestamp}.xlsx"
default_path = desktop_path / default_filename
file_path = filedialog.asksaveasfilename(
defaultextension=".xlsx",
filetypes=[("Excel files", "*.xlsx"), ("All files", "*.*")],
title="保存模板文件",
initialdir=str(desktop_path),
initialfile=default_filename
)
if not file_path:
return
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "轨迹推送模板"
# 设置表头样式
header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
header_font = Font(bold=True, color="FFFFFF")
# 公共参数表头(移除SecretKey和url)
headers = [
"资源code", "接口名称",
"AP单号", "作业结果", "操作地", "备注", "操作时间",
"转单号", "时区", "处理方式", "退回方式",
"重量(weight)", "重量单位(weightUnit)", "长度(length)", "宽度(width)", "高度(height)", "尺寸单位(dimensionUnit)"
]
# 写入表头
for col, header in enumerate(headers, 1):
cell = ws.cell(row=1, column=col, value=header)
cell.fill = header_fill
cell.font = header_font
# 资源code下拉选项
logistic_provider_id_options = [
"带电渠道-干线[TRUNK_31726544]",
"带电渠道-关务[GATE_31726757]",
"带电渠道-配送[DISTRIBUTOR_31724076]",
"普货渠道-干线[TRUNK_31724331]",
"普货渠道-关务[GATE_31724116]",
"普货渠道-配送[DISTRIBUTOR_31724341]"
]
# 接口名称下拉选项
msg_type_options = [
"CAINIAO_GLOBAL_LINEHAUL_HOIN_CALLBACK - 收货",
"CAINIAO_GLOBAL_LINEHAUL_HOAIRLINE_CALLBACK - 交航",
"CAINIAO_GLOBAL_LINEHAUL_DEPARTURE_CALLBACK - 起飞",
"CAINIAO_GLOBAL_LINEHAUL_ARRIVAL_CALLBACK - 落地",
"CAINIAO_GLOBAL_CCIM_CALLBACK - 清关完成",
"CAINIAO_GLOBAL_CCIM_ABNORMAL_CALLBACK - 清关异常",
"CAINIAO_GLOBAL_LASTMILE_GTMSACCEPT_CALLBACK - 配送接收",
"CAINIAO_GLOBAL_LASTMILE_GTMSSIGN_CALLBACK - 签收",
"CAINIAO_GLOBAL_LINEHAUL_UNREACHABLERETURN_CALLBACK - 干线退回发起",
"CAINIAO_GLOBAL_LINEHAUL_UNREACHABLERETURNPROCESS_CALLBACK - 干线退回处理",
"CAINIAO_GLOBAL_LINEHAUL_UNREACHABLERETURN_HANDOVEROUT_CALLBACK - 干线退回交接",
"CAINIAO_GLOBAL_LINEHAUL_HOAIRLINE_CALLBACK - 包裹信息不全",
"CAINIAO_GLOBAL_CCIM_CALLBACK - 申报信息错误/缺失",
"CAINIAO_GLOBAL_LASTMILE_GTMSSIGN_CALLBACK - 消费者拒收",
"CAINIAO_GLOBAL_LASTMILE_GTMSSIGN_CALLBACK - 收货地址未知或错误",
"CAINIAO_GLOBAL_CUSTOMS_UNREACHABLERETURN_CALLBACK - 清关失败"
]
# 处理方式下拉选项
undeliverable_option_options = ["不选", "退回 Return", "销毁 Discard"]
# 退回方式下拉选项
return_to_options = ["不选", "首公里 FirstMile", "快递 Express"]
# 在“下拉选项”工作表中导出所有下拉值,方便在Excel中查看和下拉选择
options_ws = wb.create_sheet("下拉选项")
options_ws.cell(row=1, column=1, value="资源code")
options_ws.cell(row=1, column=2, value="接口名称")
# 写入资源code选项
for idx, value in enumerate(logistic_provider_id_options, start=2):
options_ws.cell(row=idx, column=1, value=value)
# 写入接口名称选项
for idx, value in enumerate(msg_type_options, start=2):
options_ws.cell(row=idx, column=2, value=value)
# 为资源code列添加数据验证(下拉列表),引用“下拉选项”工作表
dv1 = DataValidation(
type="list",
formula1=f"=下拉选项!$A$2:$A${len(logistic_provider_id_options) + 1}",
allow_blank=True,
)
dv1.error = '请从下拉列表中选择'
dv1.errorTitle = '输入错误'
ws.add_data_validation(dv1)
dv1.add("A2:A1000") # 资源code列(第1列)
# 为接口名称列添加数据验证(下拉列表)
dv2 = DataValidation(
type="list",
formula1=f"=下拉选项!$B$2:$B${len(msg_type_options) + 1}",
allow_blank=True,
)
dv2.error = '请从下拉列表中选择'
dv2.errorTitle = '输入错误'
ws.add_data_validation(dv2)
dv2.add("B2:B1000") # 接口名称列(第2列)
# 为处理方式列添加数据验证(仍使用内联列表)
dv3 = DataValidation(
type="list",
formula1=f'"{";".join(undeliverable_option_options)}"',
allow_blank=True,
)
dv3.error = '请从下拉列表中选择'
dv3.errorTitle = '输入错误'
ws.add_data_validation(dv3)
dv3.add("K2:K1000") # 处理方式列(第11列)
# 为退回方式列添加数据验证
dv4 = DataValidation(
type="list",
formula1=f'"{";".join(return_to_options)}"',
allow_blank=True,
)
dv4.error = '请从下拉列表中选择'
dv4.errorTitle = '输入错误'
ws.add_data_validation(dv4)
dv4.add("L2:L1000") # 退回方式列(第12列)
# 设置列宽
column_widths = [30, 50, 20, 15, 20, 20, 20, 20, 10, 20, 20, 15, 15, 15, 15, 15, 15]
for col, width in enumerate(column_widths, 1):
ws.column_dimensions[openpyxl.utils.get_column_letter(col)].width = width
# 添加示例数据(第二行)
example_data = [
"带电渠道-干线[TRUNK_31726544]", # 资源code
"CAINIAO_GLOBAL_LINEHAUL_HOAIRLINE_CALLBACK - 交航", # 接口名称
"AP00790279362887", # AP单号
"0", # 作业结果
"US", # 操作地
"交航", # 备注
datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # 操作时间
"20201080021", # 转单号
"8", # 时区
"不选", # 处理方式
"不选", # 退回方式
"1450", # 重量(weight)
"g", # 重量单位(weightUnit)
"100", # 长度(length)
"100", # 宽度(width)
"100", # 高度(height)
"cm" # 尺寸单位(dimensionUnit)
]
for col, value in enumerate(example_data, 1):
ws.cell(row=2, column=col, value=value)
wb.save(file_path)
messagebox.showinfo("成功", f"模板已导出到:\n{file_path}")
except Exception as e:
messagebox.showerror("错误", f"导出模板失败:\n{str(e)}")
def import_and_push(self):
"""导入Excel并批量推送"""
try:
file_path = filedialog.askopenfilename(
filetypes=[("Excel files", "*.xlsx"), ("All files", "*.*")],
title="选择要导入的Excel文件"
)
if not file_path:
return
self.log_text.delete(1.0, tk.END)
self.log("开始导入文件...")
# 读取Excel
wb = openpyxl.load_workbook(file_path)
# 优先选择名称为“轨迹推送模板”的工作表,避免当前激活的是“下拉选项”导致表头不一致
try:
ws = wb["轨迹推送模板"]
except KeyError:
ws = wb.active
# 读取表头
headers = [cell.value for cell in ws[1]]
# 为兼容可能存在的空格/隐藏字符,预先定位各字段的列索引
header_index_map = {}
for idx, h in enumerate(headers):
name = str(h).strip() if h is not None else ""
if not name:
continue
header_index_map[name] = idx
# 关键字段的索引(如果找不到就为None)
ap_col_index = next((i for n, i in header_index_map.items() if "AP单号" in n), None)
resource_col_index = next((i for n, i in header_index_map.items() if "资源code" in n), None)
msg_type_col_index = next((i for n, i in header_index_map.items() if "接口名称" in n), None)
op_result_col_index = next((i for n, i in header_index_map.items() if "作业结果" in n), None)
op_loc_col_index = next((i for n, i in header_index_map.items() if "操作地" in n), None)
remark_col_index = next((i for n, i in header_index_map.items() if "备注" in n), None)
op_time_col_index = next((i for n, i in header_index_map.items() if "操作时间" in n), None)
relabel_col_index = next((i for n, i in header_index_map.items() if "转单号" in n), None)
tz_col_index = next((i for n, i in header_index_map.items() if "时区" in n), None)
parcel_weight_col_index = next((i for n, i in header_index_map.items() if "重量" in n and "weight" in n.lower()), None)
parcel_weight_unit_col_index = next((i for n, i in header_index_map.items() if "重量单位" in n and "weightunit" in n.lower()), None)
parcel_length_col_index = next((i for n, i in header_index_map.items() if "长度" in n and "length" in n.lower()), None)
parcel_width_col_index = next((i for n, i in header_index_map.items() if "宽度" in n and "width" in n.lower()), None)
parcel_height_col_index = next((i for n, i in header_index_map.items() if "高度" in n and "height" in n.lower()), None)
parcel_dimension_unit_col_index = next((i for n, i in header_index_map.items() if "尺寸单位" in n and "dimensionunit" in n.lower()), None)
# 从界面读取SecretKey和url
secret_key = self.secret_key_entry.get().strip()
url = self.url_entry.get().strip()
if not secret_key:
messagebox.showerror("错误", "请先在界面上填写SecretKey!")
return
if not url:
url = "https://prelink.cainiao.com/gateway/link.do"
# 读取数据(从第2行开始,第1行是表头)
data_rows = []
for row in ws.iter_rows(min_row=2, values_only=True):
if any(cell for cell in row): # 跳过空行
data_rows.append(row)
if not data_rows:
messagebox.showwarning("警告", "Excel文件中没有数据!")
return
self.log(f"共读取到 {len(data_rows)} 条数据")
self.log(f"使用SecretKey:{secret_key[:20]}...")
self.log(f"使用URL:{url}")
self.log("=" * 80)
# 批量推送
success_count = 0
fail_count = 0
for idx, row_data in enumerate(data_rows, 1):
try:
# 构建数据字典(用于除AP单号外的其它字段)
row_dict = dict(zip(headers, row_data))
# 获取AP单号:优先按列索引读取,避免表头名字有细微差异
ap_number = ""
if ap_col_index is not None and ap_col_index < len(row_data):
cell_value = row_data[ap_col_index]
if cell_value is not None:
ap_number = str(cell_value).strip()
else:
# 兜底:按照键名尝试
ap_number = str(row_dict.get("AP单号", "")).strip()
if not ap_number:
self.log(f"第{idx+1}行:AP单号为空,跳过")
fail_count += 1
continue
self.log(f"\n【第{idx}条】AP单号:{ap_number}")
# 解析资源code
if resource_col_index is not None and resource_col_index < len(row_data):
logistic_provider_id_display = str(row_data[resource_col_index] or "").strip()
else:
logistic_provider_id_display = str(row_dict.get("资源code", "")).strip()
match = re.search(r'\[([^\]]+)\]', logistic_provider_id_display)
logistic_provider_id = match.group(1) if match else logistic_provider_id_display
# 解析接口名称
if msg_type_col_index is not None and msg_type_col_index < len(row_data):
msg_type_display = str(row_data[msg_type_col_index] or "").strip()
else:
msg_type_display = str(row_dict.get("接口名称", "")).strip()
msg_type = msg_type_display.split(" - ")[0] if " - " in msg_type_display else msg_type_display
# 构建logistics_interface
logistics_interface = {
"logisticsOrderCode": ap_number,
"opCode": str((row_data[op_result_col_index] if op_result_col_index is not None and op_result_col_index < len(row_data) else row_dict.get("作业结果", "0")) or "0").strip(),
"opLocation": str((row_data[op_loc_col_index] if op_loc_col_index is not None and op_loc_col_index < len(row_data) else row_dict.get("操作地", "")) or "").strip(),
"opRemark": str((row_data[remark_col_index] if remark_col_index is not None and remark_col_index < len(row_data) else row_dict.get("备注", "")) or "").strip(),
"opTime": str((row_data[op_time_col_index] if op_time_col_index is not None and op_time_col_index < len(row_data) else row_dict.get("操作时间", datetime.now().strftime("%Y-%m-%d %Y-%m-%d %H:%M:%S"))) or datetime.now().strftime("%Y-%m-%d %H:%M:%S")).strip(),
"operator": logistic_provider_id,
"relabelTrackingNumber": str((row_data[relabel_col_index] if relabel_col_index is not None and relabel_col_index < len(row_data) else row_dict.get("转单号", "")) or "").strip(),
"timeZone": int((row_data[tz_col_index] if tz_col_index is not None and tz_col_index < len(row_data) else row_dict.get("时区", 8)) or 8),
"trackingNumber": ap_number,
"transportType": "5"
}
# 如果是干线退回发起,添加处理方式和退回方式
if msg_type == "CAINIAO_GLOBAL_LINEHAUL_UNREACHABLERETURN_CALLBACK":
# 处理方式
undeliverable_option_display = str(row_dict.get("处理方式", "")).strip()
if undeliverable_option_display and undeliverable_option_display != "不选":
undeliverable_option = undeliverable_option_display.split(" ")[-1] if " " in undeliverable_option_display else undeliverable_option_display
logistics_interface["undeliverableOption"] = undeliverable_option
# 退回方式
return_to_display = str(row_dict.get("退回方式", "")).strip()
if return_to_display and return_to_display != "不选":
return_to = return_to_display.split(" ")[-1] if " " in return_to_display else return_to_display
logistics_interface["returnTo"] = return_to
# 如果是交航,添加parcel字段(注意:只有交航类型才回传parcel,其他类型即使有数据也不回传)
if msg_type == "CAINIAO_GLOBAL_LINEHAUL_HOAIRLINE_CALLBACK":
parcel = {}
weight = ""
if parcel_weight_col_index is not None and parcel_weight_col_index < len(row_data):
weight = str((row_data[parcel_weight_col_index] or "")).strip()
else:
weight = str(row_dict.get("重量(weight)", "")).strip()
if weight:
parcel["weight"] = weight
weight_unit = ""
if parcel_weight_unit_col_index is not None and parcel_weight_unit_col_index < len(row_data):
weight_unit = str((row_data[parcel_weight_unit_col_index] or "")).strip()
else:
weight_unit = str(row_dict.get("重量单位(weightUnit)", "")).strip()
if weight_unit:
parcel["weightUnit"] = weight_unit
length = ""
if parcel_length_col_index is not None and parcel_length_col_index < len(row_data):
length = str((row_data[parcel_length_col_index] or "")).strip()
else:
length = str(row_dict.get("长度(length)", "")).strip()
if length:
parcel["length"] = length
width = ""
if parcel_width_col_index is not None and parcel_width_col_index < len(row_data):
width = str((row_data[parcel_width_col_index] or "")).strip()
else:
width = str(row_dict.get("宽度(width)", "")).strip()
if width:
parcel["width"] = width
height = ""
if parcel_height_col_index is not None and parcel_height_col_index < len(row_data):
height = str((row_data[parcel_height_col_index] or "")).strip()
else:
height = str(row_dict.get("高度(height)", "")).strip()
if height:
parcel["height"] = height
dimension_unit = ""
if parcel_dimension_unit_col_index is not None and parcel_dimension_unit_col_index < len(row_data):
dimension_unit = str((row_data[parcel_dimension_unit_col_index] or "")).strip()
else:
dimension_unit = str(row_dict.get("尺寸单位(dimensionUnit)", "")).strip()
if dimension_unit:
parcel["dimensionUnit"] = dimension_unit
if parcel:
logistics_interface["parcel"] = parcel
# 如果是干线退回处理,添加相关字段
if msg_type == "CAINIAO_GLOBAL_LINEHAUL_UNREACHABLERETURNPROCESS_CALLBACK":
# processType
process_type_display = str(row_dict.get("退回方式(processType)", "")).strip() or "菜鸟快递寄送 03"
process_type = process_type_display.split(" ")[-1] if " " in process_type_display else process_type_display
logistics_interface["processType"] = process_type
# 退回地址相关字段
logistics_interface["returnPickupAdressProvince"] = str(row_dict.get("退回省份", "广东")).strip()
logistics_interface["returnPickupAdressCity"] = str(row_dict.get("退回城市", "深圳")).strip()
logistics_interface["returnPickupAdressDistrict"] = str(row_dict.get("退回区县", "宝安区")).strip()
logistics_interface["returnPickupAdressDetail"] = str(row_dict.get("退回详细地址", "怀德翠岗工业区五区36栋二楼")).strip()
logistics_interface["returnPickupPhone"] = str(row_dict.get("退回电话", "19513356304")).strip()
logistics_interface["returnPickupSender"] = str(row_dict.get("退回发件人", "蒋彬")).strip()
# 转换为JSON字符串
logistics_interface_str = json.dumps(logistics_interface, ensure_ascii=False)
# 生成签名
charset = "UTF-8"
data_digest = self.do_sign(logistics_interface_str, charset, secret_key)
# 构建请求参数
params = {
"logistic_provider_id": logistic_provider_id,
"data_digest": data_digest,
"msg_type": msg_type,
"logistics_interface": logistics_interface_str
}
# 发送请求
headers = {"Accept": "application/json"}
response = requests.post(url, data=params, headers=headers, timeout=30)
# 只打印必要的三行日志:公共参数、请求报文、返回报文
self.log(f"公共参数:{params}")
self.log(f"请求报文:{logistics_interface_str}")
self.log(f"返回报文:{response.text}")
# 内部根据success字段和HTTP状态码统计成功/失败,但不再逐条打印 ✓ / ✗
success_raw = None
try:
resp_json = response.json()
success_raw = resp_json.get("success")
except Exception:
pass
is_success = False
if response.status_code == 200:
if isinstance(success_raw, bool):
is_success = success_raw
elif isinstance(success_raw, str):
is_success = success_raw.lower() == "true"
elif success_raw is None:
# 某些接口可能没有success字段,HTTP 200 即认为成功
is_success = True
if is_success:
success_count += 1
else:
fail_count += 1
# 批量请求时,每条请求间隔1秒(最后一条不需要延时)
if idx < len(data_rows):
time.sleep(1)
except Exception as e:
self.log(f"✗ 错误:{str(e)}")
fail_count += 1
# 汇总
self.log("\n" + "=" * 80)
self.log(f"批量推送完成!")
self.log(f"总计:{len(data_rows)} 条")
self.log(f"成功:{success_count} 条")
self.log(f"失败:{fail_count} 条")
messagebox.showinfo("完成", f"批量推送完成!\n总计:{len(data_rows)} 条\n成功:{success_count} 条\n失败:{fail_count} 条")
except Exception as e:
error_msg = f"导入或推送失败:\n{str(e)}"
self.log(error_msg)
messagebox.showerror("错误", error_msg)
def on_push_click(self):
"""推送按钮点击事件"""
try:
self.log_text.delete(1.0, tk.END)
self.log("开始推送...")
# 获取基本参数
secret_key = self.secret_key_entry.get().strip()
url = self.url_entry.get().strip()
charset = "UTF-8" # 固定使用UTF-8编码
if not secret_key:
self.log("错误: SecretKey不能为空")
return
if not url:
self.log("错误: url不能为空")
return
# 获取公共参数
# 从下拉框获取logistic_provider_id,提取[]内的CODE部分
logistic_provider_id_display = self.logistic_provider_id_var.get()
match = re.search(r'\[([^\]]+)\]', logistic_provider_id_display)
logistic_provider_id = match.group(1) if match else logistic_provider_id_display
# 从下拉框获取msg_type,提取code部分(去掉中文描述)
msg_type_display = self.msg_type_var.get()
msg_type = msg_type_display.split(" - ")[0] if " - " in msg_type_display else msg_type_display
# 获取logisticsOrderCode(用于trackingNumber)
logistics_order_code = self.logistics_order_code_entry.get().strip()
# 更新opTime为当前时间
self.update_op_time()
op_time = self.op_time_entry.get().strip()
# 构建logistics_interface JSON
logistics_interface = {
"logisticsOrderCode": logistics_order_code,
"opCode": self.op_code_entry.get().strip(),
"opLocation": self.op_location_entry.get().strip(),
"opRemark": self.op_remark_entry.get().strip(),
"opTime": op_time,
"operator": logistic_provider_id, # 使用logistic_provider_id的值
"relabelTrackingNumber": self.relabel_tracking_number_entry.get().strip(),
"timeZone": int(self.time_zone_entry.get().strip()) if self.time_zone_entry.get().strip() else 8,
"trackingNumber": logistics_order_code, # 使用logisticsOrderCode的值
"transportType": "5" # 固定为5(空运)
}
# 如果是干线退回发起,添加处理方式和退回方式
if msg_type == "CAINIAO_GLOBAL_LINEHAUL_UNREACHABLERETURN_CALLBACK":
# 获取undeliverableOption的值(只取英文部分),如果不选则不添加
undeliverable_option_display = self.undeliverable_option_var.get()
if undeliverable_option_display != "不选":
undeliverable_option = undeliverable_option_display.split(" ")[-1] if " " in undeliverable_option_display else undeliverable_option_display
logistics_interface["undeliverableOption"] = undeliverable_option
# 获取returnTo的值(只取英文部分),如果不选则不添加
return_to_display = self.return_to_var.get()
if return_to_display != "不选":
return_to = return_to_display.split(" ")[-1] if " " in return_to_display else return_to_display
logistics_interface["returnTo"] = return_to
# 如果是交航,添加parcel字段(注意:只有交航类型才回传parcel,其他类型即使有数据也不回传)
if msg_type == "CAINIAO_GLOBAL_LINEHAUL_HOAIRLINE_CALLBACK":
parcel = {}
weight = self.parcel_weight_entry.get().strip()
if weight:
parcel["weight"] = weight
weight_unit = self.parcel_weight_unit_var.get().strip()
if weight_unit:
parcel["weightUnit"] = weight_unit
length = self.parcel_length_entry.get().strip()
if length:
parcel["length"] = length
width = self.parcel_width_entry.get().strip()
if width:
parcel["width"] = width
height = self.parcel_height_entry.get().strip()
if height:
parcel["height"] = height
dimension_unit = self.parcel_dimension_unit_var.get().strip()
if dimension_unit:
parcel["dimensionUnit"] = dimension_unit
if parcel:
logistics_interface["parcel"] = parcel
# 如果是干线退回处理,添加相关字段
if msg_type == "CAINIAO_GLOBAL_LINEHAUL_UNREACHABLERETURNPROCESS_CALLBACK":
# processType(只取数字部分)
process_type_display = self.process_type_var.get()
process_type = process_type_display.split(" ")[-1] if " " in process_type_display else process_type_display
logistics_interface["processType"] = process_type
# 退回地址相关字段
logistics_interface["returnPickupAdressProvince"] = self.return_pickup_address_province_entry.get().strip()
logistics_interface["returnPickupAdressCity"] = self.return_pickup_address_city_entry.get().strip()
logistics_interface["returnPickupAdressDistrict"] = self.return_pickup_address_district_entry.get().strip()
logistics_interface["returnPickupAdressDetail"] = self.return_pickup_address_detail_entry.get().strip()
logistics_interface["returnPickupPhone"] = self.return_pickup_phone_entry.get().strip()
logistics_interface["returnPickupSender"] = self.return_pickup_sender_entry.get().strip()
# 转换为JSON字符串
logistics_interface_str = json.dumps(logistics_interface, ensure_ascii=False)
self.log(f"logistics_interface: {logistics_interface_str}")
# 生成签名
data_digest = self.do_sign(logistics_interface_str, charset, secret_key)
self.log(f"生成的签名(data_digest): {data_digest}")
# 构建请求参数
params = {
"logistic_provider_id": logistic_provider_id,
"data_digest": data_digest,
"msg_type": msg_type,
"logistics_interface": logistics_interface_str
}
self.log(f"请求URL: {url}")
self.log(f"请求参数: {params}")
# 发送POST请求
headers = {
"Accept": "application/json"
}
self.log("发送POST请求...")
response = requests.post(url, data=params, headers=headers, timeout=30)
# 只打印必要的三行日志:公共参数、请求报文、返回报文
self.log(f"响应状态码: {response.status_code}")
self.log(f"响应内容: {response.text}")
except Exception as e:
self.log(f"错误: {str(e)}")
import traceback
self.log(traceback.format_exc())
if __name__ == "__main__":
root = tk.Tk()
app = TrackPushApp(root)
root.mainloop()