跳转至

顺丰丰桥对接笔记

下单、筛单、路由查询、运单打印

city_code.json点我下载

import os
import json
import codecs
import base64
import requests
import hashlib
from xml.etree import ElementTree
from xml.etree.ElementTree import Element, SubElement
from django.conf import settings

from user.models import Image
from django.core.files.base import ContentFile

# logger
import logging

logger = logging.getLogger("info")


class Singleton(type):
    def __init__(cls, name, bases, dict):
        super(Singleton, cls).__init__(name, bases, dict)
        cls.instance = None

    def __call__(cls, *args, **kw):
        if cls.instance is None:
            cls.instance = super(Singleton, cls).__call__(*args, **kw)
        return cls.instance


class SF(metaclass=Singleton):
    domain = 'https://bsp-oisp.sf-express.com/bsp-oisp/sfexpressService'
    client_code = ''
    check_word = ''
    city_data = None

    def call_service(self, xml):
        # 拼接校验码
        combine_str = xml + self.check_word
        # md5
        md5 = hashlib.md5()
        md5.update(combine_str.encode('utf-8'))
        md5_str = md5.digest()
        # base64
        verify_code = base64.b64encode(md5_str).decode('utf-8')
        # call service
        data = {"xml": xml, "verifyCode": verify_code}
        response = requests.post(self.domain, data=data)
        return response

    def order_create(self, order_id, month_card, province, city, district, detailed_address, company, contact, tel,
                     paster, packet):
        """
            快速下单
            返回参数依次为 发货状态 快递单号 错误信息
        """
        root = Element('Request', service='OrderService', lang="zh-CN")
        head = SubElement(root, 'Head')
        head.text = self.client_code
        body = SubElement(root, 'Body')
        order_attributes = {
            'orderid': order_id,
            'is_gen_bill_no': '1',
            'express_type': '2',
            # 'j_province': '江苏省',
            # 'j_city': '南京市',
            # 'j_company': '雨花台区',
            'j_address': '软件谷科创城D2南',
            'j_contact': '殷先生',
            'j_tel': '17749503263',
            'd_province': province,
            'd_city': city,
            'd_county': district,
            'd_address': detailed_address,
            'd_company': company,
            'd_contact': contact,
            'd_tel': tel,
            'parcel_quantity': '1',
            'custid': month_card,
            'pay_method': '1',
            'customs_batchs': ''
        }
        order = SubElement(body, 'Order', attrib=order_attributes)
        SubElement(order, 'Cargo', attrib={'name': '中药(帖)', 'count': str(paster), 'unit': '帖'})
        SubElement(order, 'Cargo', attrib={'name': '中药(包)', 'count': str(packet), 'unit': '包'})
        xml = ElementTree.tostring(root, encoding='utf8').decode()
        response = self.call_service(xml)
        if response.status_code == 200:
            et = ElementTree.fromstring(response.text)
            head = et.find('Head').text
            if head == 'OK':
                return True, et.find('Body').find('OrderResponse').attrib['mailno']
            else:
                return False, et.find('ERROR').text
        else:
            return False, '顺丰服务器无响应'

    def order_filter(self, province, city, district, address, tel):
        """筛单 需要开启订单筛选接口 判断判断客户的收、派地址是否属于顺丰的收派范围"""
        root = Element('Request', service='OrderFilterService', lang="zh-CN")
        head = SubElement(root, 'Head')
        head.text = self.client_code
        body = SubElement(root, 'Body')
        SubElement(body, 'OrderFilter', attrib={
            'filter_type': '2',
            'd_address': address
        })
        SubElement(body, 'OrderFilterOption', attrib={
            'd_country': '中国',
            'd_province': province,
            'd_city': city,
            'd_county': district,
            'd_tel': tel
        })
        xml = ElementTree.tostring(root, encoding='utf8').decode()
        response = self.call_service(xml)
        if response.status_code == 200:
            et = ElementTree.fromstring(response.text)
            head = et.find('Head').text
            if head == 'OK':
                return et.find('Body').find('OrderFilterResponse').attrib['filter_result']
        return None

    def router_query(self, order_id):
        """路由查询 快递信息查询"""
        root = Element('Request', service='RouteService', lang="zh-CN")
        head = SubElement(root, 'Head')
        head.text = self.client_code
        body = SubElement(root, 'Body')
        SubElement(body, 'RouteRequest', attrib={
            'tracking_type': '2',
            'tracking_number': order_id,
            'method_type': '1'
        })
        xml = ElementTree.tostring(root, encoding='utf8').decode()
        response = self.call_service(xml)
        if response.status_code == 200:
            et = ElementTree.fromstring(response.text)
            head = et.find('Head').text
            if head == 'OK':
                routers = et.find('Body').find('RouteResponse').findall('Route')
                data = []
                for router in routers:
                    router_data = {}
                    for attr in router.attrib:
                        router_data[attr] = router.attrib[attr]
                    data.append(router_data)
                return data
        return None

    def city_to_code(self, city):
        """城市转顺丰编码"""
        if self.city_data is None:
            path = os.path.join(settings.BASE_DIR, 'common/city_code.json')
            with codecs.open(path, 'r', 'utf-8') as f:
                self.city_data = json.load(f)
        for x in self.city_data:
            if x['city'] == city:
                return x['code']
        return None

    def waybill(self, mail_no, month_card, province, city, district, detailed_address, company, contact, tel,
                paster, packet):
        """电子运单图片下载"""
        data = {
            'mailNo': mail_no,
            'expressType': 2,
            'payMethod': 1,
            'returnTrackingNo': None,
            'monthAccount': month_card,
            'orderNo': None,
            'zipCode': self.city_to_code('南京市'),
            'destCode': self.city_to_code(city),
            'payArea': None,
            # 寄件人信息
            'deliverCompany': '南京诗远启科技有限公司',
            'deliverName': '殷先生',
            'deliverMobile': '17749503263',
            'deliverTel': None,
            'deliverProvince': '江苏省',
            'deliverCity': '南京市',
            'deliverCounty': '雨花台区',
            'deliverAddress': '大周路软件谷科创城D2南',
            'deliverShipperCode': None,
            # 收件人信息
            'consignerCompany': company,
            'consignerName': contact,
            'consignerMobile': tel,
            'consignerTel': None,
            'consignerProvince': province,
            'consignerCity': city,
            'consignerCounty': district,
            'consignerAddress': detailed_address,
            'consignerShipperCode': None,
            # logo相关
            'logo': None,
            'sftelLogo': None,
            'topLogo': None,
            'topsftelLogo': None,
            # 其他信息
            'totalFee': None,
            'appId': '必填',
            'appKey': '必填',
            'electric': 'E',
            'insureValue': None,
            'insureFee': None,
            'codValue': None,
            'codMonthAccount': None,
            # 丰密运单相关配置
            'abFlag': None,
            'xbFlag': None,
            'proCode': None,
            'destRouteLabel': None,
            'destTeamCode': None,
            'codingMapping': None,
            'codingMappingOut': None,
            'sourceTransferCode': None,
            'printIcon': None,
            'qrcode': None,
            # 加密
            'encryptMobile': False,
            'encryptCustName': False,
            # 货物信息
            'cargoInfoDtoList': [
                {
                    'cargo': '中药(帖)',
                    'parcelQuantity': None,
                    'cargoCount': paster,
                    'cargoUnit': '帖',
                    'cargoWeight': None,
                    'cargoAmount': None,
                    'cargoTotalWeight': None,
                    'remark': '内有液体 小心轻放',
                    'sku': None
                },
                {
                    'cargo': '中药(包)',
                    'parcelQuantity': None,
                    'cargoCount': packet,
                    'cargoUnit': '包',
                    'cargoWeight': None,
                    'cargoAmount': None,
                    'cargoTotalWeight': None,
                    'remark': '',
                    'sku': None
                }
            ]
        }
        url = 'http://localhost:4040/sf/waybill/print?type=V2.1_poster_100mm150mm&output=image'
        response = requests.post(url, data='[{}]'.format(json.dumps(data)))
        if response.json()['code'] == 'SYS_CODE_QIAO_0200':
            images = []
            for image_base64 in response.json()['result']:
                _image = ContentFile(base64.b64decode(image_base64), name='express_waybill.jpg')
                image = Image.objects.create(image=_image, purpose=2)
                images.append(image)
            return images
        return None

Ubuntu14.04安装JDK1.7

官网下载安装包

前往官网下载安装包,将安装包拷贝至主机待用。

创建文件夹、解压文件

创建文件夹

sudo mkdir /usr/lib/java

解压压缩包至文件夹

sudo tar zxvf jdk-7u80-linux-x64.tar.gz -C /usr/lib/java

设置环境变量

sudo nano /etc/profile 

文件结尾加入环境变量内容:

#set java environment
export JAVA_HOME=/usr/lib/java/jdk1.7.0_80
export JRE_HOME=${JAVA_HOME}/jre 
export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib 
export PATH=${JAVA_HOME}/bin:$PATH 

设置完环境变量之后执行命令让设置生效

source /etc/profile 

设置默认的JDK

sudo update-alternatives --install /usr/bin/java  java  /usr/lib/java/jdk1.7.0_80/bin/java 300
sudo update-alternatives --install /usr/bin/javac javac /usr/lib/java/jdk1.7.0_80/bin/javac 300

检查Ubuntu的JDK版本

dreamgo@dreamgo:/usr/lib/java$ java -version
java version "1.7.0_80"
Java(TM) SE Runtime Environment (build 1.7.0_80-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.80-b11, mixed mode)

运单面单自助打印SDK

运行服务

  • 拷贝csim_waybill_print_service_V1.0.5.jar至服务器
  • chmod -R 777 csim_waybill_print_service_V1.0.5.jar 对文件授权,以确保该jar能正确生成日志文件。
  • sudo java -jar csim_waybill_print_service_V1.0.5.jar 8888 #端口号不填时默认为4040,请检查防火墙状态。(多次反复启动\关闭会导致服务开启是卡在第3步,已提交顺丰技术,等待验证解决。)

python请求接口

使用的100x150mm,转base64的接口,代码参见最上方。可结合需求和nodejs以及java版本demo自行修改。

安装字体

安装字体是为了解决在Linux系统上,生成的快递面单无法正常显示目的地编码的问题。

下载字体

字体包simhei.ttf可在SF-CSIM-PRINTER-SDK-V1.0.5压缩包中的linux系统启动注意事项文件夹中找到。

查看当前系统中已安装的中文字体

fc-list :lang=zh

复制simsun.ttf字体文件

mkdir -p /usr/share/fonts/my_fonts

拷贝字体文件至该文件夹

cd /usr/share/fonts/my_fonts
apt-get install xfonts-utils
mkfontscale 
mkfontdir #注意 是 mkfontdir 不是 mkfontsdir,此处顺丰文档有错误。

ll # 应该可以看到对应的fonts.dir 和 fonts.scale文件

再次查看已安装的字体

fc-list :lang=zh

自动开启自助面单打印服务

supervisord

fengqiao.conf:

[program:feng_qiao]
command=java -jar csim_waybill_print_service_V1.0.5.jar
directory=/django/fengqiao
stdout_logfile=/django/fengqiao/out.log
stderr_logfile=/django/fengqiao/error.log
autostart=true
autorestart=true
user=root
startsecs=10

用户user必须是root,超管名称不是root的也写root,不然会权限不足。 supervisord相关操作,部署请参考Supervisor使用笔记及celery生产部署

作为服务 暂未测试通过

#!/bin/sh
SERVICE_NAME=fengqiao
PATH_TO_JAR=/django/fengqiao/csim_waybill_print_service_V1.0.5.jar
PID_PATH_NAME=/tmp/fengqiao-pid
case $1 in
    start)
        echo "Starting $SERVICE_NAME ..."
        if [ ! -f $PID_PATH_NAME ]; then
            nohup java -jar $PATH_TO_JAR /tmp 2>> /dev/null >> /dev/null &
            echo $! > $PID_PATH_NAME
            echo "$SERVICE_NAME started ..."
        else
            echo "$SERVICE_NAME is already running ..."
        fi
    ;;
    stop)
        if [ -f $PID_PATH_NAME ]; then
            PID=$(cat $PID_PATH_NAME);
            echo "$SERVICE_NAME stoping ..."
            kill $PID;
            echo "$SERVICE_NAME stopped ..."
            rm $PID_PATH_NAME
        else
            echo "$SERVICE_NAME is not running ..."
        fi
    ;;
    restart)
        if [ -f $PID_PATH_NAME ]; then
            PID=$(cat $PID_PATH_NAME);
            echo "$SERVICE_NAME stopping ...";
            kill $PID;
            echo "$SERVICE_NAME stopped ...";
            rm $PID_PATH_NAME
            echo "$SERVICE_NAME starting ..."
            nohup java -jar $PATH_TO_JAR /tmp 2>> /dev/null >> /dev/null &
            echo $! > $PID_PATH_NAME
            echo "$SERVICE_NAME started ..."
        else
            echo "$SERVICE_NAME is not running ..."
        fi
    ;;
esac