Python3实现微信支付对账单下载导出CSV

微信下载对账单官方内容:

应用场景

商户可以通过该接口下载历史交易清单。比如掉单、系统错误等导致商户侧和微信侧数据不一致,通过对账单核对后可校正支付状态。

注意:

1、微信侧未成功下单的交易不会出现在对账单中。支付成功后撤销的交易会出现在对账单中,跟原支付单订单号一致;

2、微信在次日9点启动生成前一天的对账单,建议商户10点后再获取;

3、对账单中涉及金额的字段单位为“元”。

4、对账单接口只能下载三个月以内的账单。
接口链接

https://api.mch.weixin.qq.com/pay/downloadbill
是否需要证书

不需要。
请求参数

字段名 变量名 必填 类型 示例值 描述
公众账号ID appid String(32) wx8888888888888888 微信分配的公众账号ID(企业号corpid即为此appId)
商户号 mch_id String(32) 1900000109 微信支付分配的商户号
设备号 device_info String(32) 13467007045764 微信支付分配的终端设备号
随机字符串 nonce_str String(32) 5K8264ILTKCH16CQ2502SI8ZNMTM67VS 随机字符串,不长于32位。推荐随机数生成算法
签名 sign String(32) C380BEC2BFD727A4B6845133519F3AD6 签名,详见签名生成算法
签名类型 sign_type String(32) HMAC-SHA256 签名类型,目前支持HMAC-SHA256和MD5,默认为MD5
对账单日期 bill_date String(8) 20140603 下载对账单的日期,格式:20140603
账单类型 bill_type String(8) ALL ALL,返回当日所有订单信息,默认值
SUCCESS,返回当日成功支付的订单
REFUND,返回当日退款订单
RECHARGE_REFUND,返回当日充值退款订单(相比其他对账单多一栏“返还手续费”)
压缩账单 tar_type String(8) GZIP 非必传参数,固定值:GZIP,返回格式为.gzip的压缩包账单。不传则默认为数据流形式。

<xml>
  <appid>wx2421b1c4370ec43bappid>
  <bill_date>20141110bill_date>
  <bill_type>ALLbill_type>
  <mch_id>10000100mch_id>
  <nonce_str>21df7dc9cd8616b56919f20d9f679233nonce_str>
  <sign>332F17B766FC787203EBE9D6E40457A1sign>
xml>

返回结果
失败时,返回以下字段

字段名 变量名 必填 类型 示例值 描述
返回状态码 return_code String(16) FAIL FAIL
返回信息 return_msg String(128) 签名失败 返回信息,如非空,为错误原因
如:签名失败、参数格式错误等。

成功时,数据以文本表格的方式返回,第一行为表头,后面各行为对应的字段内容,字段内容跟查询订单或退款结果一致,具体字段说明可查阅相应接口。

第一行为表头,根据请求下载的对账单类型不同而不同(由bill_type决定),目前有:
当日所有订单

交易时间,公众账号ID,商户号,子商户号,设备号,微信订单号,商户订单号,用户标识,交易类型,交易状态,付款银行,货币种类,总金额,代金券或立减优惠金额,微信退款单号,商户退款单号,退款金额,代金券或立减优惠退款金额,退款类型,退款状态,商品名称,商户数据包,手续费,费率
当日成功支付的订单

交易时间,公众账号ID,商户号,子商户号,设备号,微信订单号,商户订单号,用户标识,交易类型,交易状态,付款银行,货币种类,总金额,代金券或立减优惠金额,商品名称,商户数据包,手续费,费率
当日退款的订单

交易时间,公众账号ID,商户号,子商户号,设备号,微信订单号,商户订单号,用户标识,交易类型,交易状态,付款银行,货币种类,总金额,代金券或立减优惠金额,退款申请时间,退款成功时间,微信退款单号,商户退款单号,退款金额,代金券或立减优惠退款金额,退款类型,退款状态,商品名称,商户数据包,手续费,费率

从第二行起,为数据记录,各参数以逗号分隔,参数前增加`符号,为标准键盘1左边键的字符,字段顺序与表头一致。

倒数第二行为订单统计标题,最后一行为统计数据

总交易单数,总交易额,总退款金额,总代金券或立减优惠退款金额,手续费总金额

举例如下:

交易时间,公众账号ID,商户号,子商户号,设备号,微信订单号,商户订单号,用户标识,交易类型,交易状态,付款银行,货币种类,总金额,代金券或立减优惠金额,微信退款单号,商户退款单号,退款金额,代金券或立减优惠退款金额,退款类型,退款状态,商品名称,商户数据包,手续费,费率
`2014-11-10163345,`wx2421b1c4370ec43b,`10000100,`0,`1000,`1001690740201411100005734289,`1415640626,`085e9858e3ba5186aafcbaed1,`MICROPAY,`SUCCESS,`CFT,`CNY,`0.01,`0.0,`0,`0,`0,`0,`,`,`被扫支付测试,`订单额外描述,`0,`0.60%
`2014-11-10164614,`wx2421b1c4370ec43b,`10000100,`0,`1000,`1002780740201411100005729794,`1415635270,`085e9858e90ca40c0b5aee463,`MICROPAY,`SUCCESS,`CFT,`CNY,`0.01,`0.0,`0,`0,`0,`0,`,`,`被扫支付测试,`订单额外描述,`0,`0.60%
总交易单数,总交易额,总退款金额,总代金券或立减优惠退款金额,手续费总金额
`2,`0.02,`0.0,`0.0,`0 

错误码

名称 描述 原因 解决方案
SYSTEMERROR 下载失败 系统超时 请尝试再次查询。
invalid bill_type 参数错误 请求参数未按指引进行填写 参数错误,请重新检查
data format error
missing parameter
SIGN ERROR
NO Bill Exist 账单不存在 当前商户号没有已成交的订单,不生成对账单 请检查当前商户号在指定日期内是否有成功的交易。
Bill Creating 账单未生成 当前商户号没有已成交的订单或对账单尚未生成 请先检查当前商户号在指定日期内是否有成功的交易,如指定日期有交易则表示账单正在生成中,请在上午10点以后再下载。
CompressGZip Error 账单压缩失败 账单压缩失败,请稍后重试 账单压缩失败,请稍后重试
UnCompressGZip Error 账单解压失败 账单解压失败,请稍后重试 账单解压失败,请稍后重试

main.py:

# -*- coding: utf-8 -*-

import os
import subprocess
import sys
import time
import logging
import threading
from datetime import datetime, timedelta


from tkinter import *

from tkinter import filedialog, Frame, Tk, N, W, messagebox
from tkinter import font

from tkinter.ttk import Progressbar

from weixin_mch_api import download_bill

import config


logging.basicConfig(level=logging.DEBUG)

try:
    cur_path = os.path.dirname(os.path.abspath(__file__))
except NameError:
    cur_path = os.path.dirname(os.path.abspath(sys.argv[0]))

class Application(Frame):
    def run(self):
        appid = self.var_appid.get().strip()
        if len(appid) == 0:
            messagebox.showinfo(title=u"错误", message="请输入公众账号ID")
            return

        mch_id = self.var_mch_id.get().strip()
        if len(mch_id) == 0:
            messagebox.showinfo(title=u"错误", message="请输入商户号")
            return

        mch_key = self.var_mch_key.get().strip()
        if len(mch_key) == 0:
            messagebox.showinfo(title=u"错误", message="请输入商户密钥")
            return

        bill_date_from = self.var_bill_date_from.get().strip()
        if len(bill_date_from) == 0:
            messagebox.showinfo(title=u"错误", message="请输入开始日期")
            return

        path = self.var_path.get().strip()
        if len(path) == 0:
            messagebox.showinfo(title=u"错误", message="请选择输出目录")
            return

        sub_mch_id = self.txt_sub_mch_id.get(1.0, END)
        if len(sub_mch_id) > 0:
            sub_mch_ids = list(set(sub_mch_id.split()))
        else:
            sub_mch_ids = []

        # save settings    
        config.save(appid, mch_id, mch_key, sub_mch_id, path)

        bill_date_to = self.var_bill_date_to.get().strip()
        if len(bill_date_to) == 0:
            bill_date_to = (datetime.now() + timedelta(days=-1)).strftime("%Y%m%d")

        s = datetime.strptime(bill_date_from, "%Y%m%d")
        e = datetime.strptime(bill_date_to, "%Y%m%d")

        def work_proc(self, path, s, e, sub_mch_ids, event):

            self.btn_run.config(state='disabled')

            days = (e - s).days + 1
            max = days * (len(sub_mch_ids) if len(sub_mch_ids) > 0 else 1)

            self.prg_bar.config({'maximum': max})

            while s <= e:
                if len(sub_mch_ids) > 0:
                    for sub_mch_id in sub_mch_ids:
                        r = download_bill(mch_key, appid, mch_id, s, sub_mch_id)
                        self.save_bill(r, s, mch_id, sub_mch_id)
                        self.prg_bar.step()

                else:
                    r = download_bill(mch_key, appid, mch_id, s)
                    print(type(r))
                    self.save_bill(r, s, mch_id)
                    self.prg_bar.step()

                s += timedelta(days=1)

            self.btn_run.config(state='normal')

            event.set()

        self.running = True

        work_thread = threading.Thread(target=work_proc, args=(self, path, s, e, sub_mch_ids, self.event))
        work_thread.start()

    def save_bill(self, text, bill_date, mch_id, sub_mch_id=None):
        bill_date = bill_date.strftime("%Y%m%d")
        if text.decode('utf-8').startswith(''):
            f = '%s_%s_%s.xml' % (bill_date, mch_id, sub_mch_id) \
                if sub_mch_id \
                else '%s_%s_.xml' % (bill_date, mch_id)
        else:
            f = '%s_%s_%s.csv' % (bill_date, mch_id, sub_mch_id) \
                if sub_mch_id \
                else '%s_%s_.csv' % (bill_date, mch_id)

        fullname = os.path.join(self.var_path.get().strip(), f)
        with open(fullname, 'wb') as csv:
            csv.write(text)

    def select_path(self):
        self.var_path.set(filedialog.askdirectory())

    def createWidgets(self, settings):
        row = 0

        # 公众号ID
        self.lbl_appid = Label(self, text=u"公众帐号ID", fg='red')
        self.lbl_appid.grid(column=0, row=row, sticky=(E, N))

        self.var_appid = StringVar(self, value=settings.get('appid')) 
        self.txt_appid = Entry(self, textvariable=self.var_appid, width=60, font=self.font)
        self.txt_appid.grid(column=1, row=row, columnspan=2, sticky=(W, N))

        row += 1


        # 商户号
        self.lbl_mch_id = Label(self, text=u"商户号", fg='red')
        self.lbl_mch_id.grid(column=0, row=row, sticky=(E, N))

        self.var_mch_id = StringVar(self, value=settings.get('mch_id')) 
        self.txt_mch_id = Entry(self, textvariable=self.var_mch_id, width=60, font=self.font)
        self.txt_mch_id.grid(column=1, row=row, columnspan=2, sticky=(W, N))

        row += 1

        # 商户密钥
        self.lbl_mch_key = Label(self, text=u"商户密钥", fg='red')
        self.lbl_mch_key.grid(column=0, row=row, sticky=(E, N))

        self.var_mch_key = StringVar(self, value=settings.get('mch_key')) 
        self.txt_mch_key = Entry(self, textvariable=self.var_mch_key, width=60, font=self.font)
        self.txt_mch_key.grid(column=1, row=row, columnspan=2, sticky=(W, N))

        row += 1

        # 子商户号
        self.lbl_sub_mch_id = Label(self, text=u"子商户号")
        self.lbl_sub_mch_id.grid(column=0, row=row, sticky=(E, N))

        self.txt_sub_mch_id = Text(self, height=5, width=60, font=self.font)
        if  settings.get('sub_mch_id'):
            self.txt_sub_mch_id.insert(END, settings.get('sub_mch_id'))
        self.txt_sub_mch_id.grid(column=1, row=row, columnspan=2, sticky=(W, N))

        row += 1

        # 开始日期
        self.lbl_bill_date_from = Label(self, text=u"开始日期", fg='red')
        self.lbl_bill_date_from.grid(column=0, row=row, sticky=(E, N))

        self.var_bill_date_from = StringVar() 
        self.var_bill_date_from.set((datetime.now() - timedelta(days=1)).strftime("%Y%m%d")) 
        self.txt_bill_date_from = Entry(self, width=8, textvariable=self.var_bill_date_from, font=self.font)
        self.txt_bill_date_from.grid(column=1, row=row, columnspan=2, sticky=(W, N))

        row += 1

        # 结束日期
        self.lbl_bill_date_to = Label(self, text=u"结束日期")
        self.lbl_bill_date_to.grid(column=0, row=row, sticky=(E, N))

        self.var_bill_date_to = StringVar() 
        self.txt_bill_date_to = Entry(self, width=8, textvariable=self.var_bill_date_to, font=self.font)
        self.txt_bill_date_to.grid(column=1, row=row, columnspan=2, sticky=(W, N))

        row += 1

        # 输出目录
        self.lbl_path = Label(self, text=u"输出目录", fg='red')
        self.lbl_path.grid(column=0, row=row, sticky=(E, N))

        path = settings.get('path')
        self.var_path = StringVar(self, value=path if path else os.path.dirname(os.path.abspath(cur_path))) 
        self.txt_path = Entry(self, textvariable=self.var_path, width=55, font=self.font)
        self.txt_path.grid(column=1, row=row, sticky=(W, N, E))

        self.btn_select_path = Button(self, text=u"...", command=self.select_path)
        self.btn_select_path.grid(column=2, row=row, sticky=(E))

        row += 1

        # 进度条
        self.prg_bar = Progressbar(self)
        self.prg_bar.grid(column=0, row=row, columnspan=3, sticky=(W, E), pady=(10, 0))

        row += 1

        # 执行按钮
        buttonFrame = Frame(self)
        buttonFrame.grid(column=0, row=row, columnspan=3, pady=(15, 0), sticky=(W, E))

        self.btn_run = Button(buttonFrame, width=20, text=u"下载", fg='blue', command=self.run)
        self.btn_run.pack()

    def loop(self):
        if self.running and self.event.is_set():
            self.running = False
            self.event.clear()

            messagebox.showinfo(title=u"成功", message="账单下载结束")
            subprocess.call('explorer "%s"' % self.var_path.get().strip().replace('/', '\\'), shell=True)

        self.master.after(100, self.loop)


    def __init__(self, master=None):
        Frame.__init__(self, master)

        self.font = font.nametofont("TkDefaultFont")
        self.font.configure(family='MS Gothic', size=9)

        self.grid(column=0, row=0, sticky=(N, W, E, S), padx=10, pady=10)

        self.rowconfigure(0, weight=1)
        self.columnconfigure (0, weight=1)

        settings = config.load()
        self.createWidgets(settings)

        self.event = threading.Event()

        self.running = False
        self.loop()


root = Tk()
root.title(u"微信商户平台对账单下载")
root.rowconfigure(0, weight=1)
root.columnconfigure(0, weight=1)

root.resizable(width=False, height=False)
app = Application(master=root)
root.mainloop()

weixin_mch_api.py:

# -*- coding: utf-8 -*-

import cgi
import json
import logging
import random
import string
import urllib

from hashlib import md5

from datetime import datetime, date
from urllib import request


def dict2xml(params):
    xml = ""
    for k, v in params.items():
        '''对于已编码的字节字符串,文本字符串的许多特性和方法已经不能使用。
        '''
        v =v.encode('utf-8').decode('utf-8')
        k = k.encode('utf-8').decode('utf-8')
        xml += "<" + k + ">" + cgi.escape(v) + " + k + ">"

    xml += ""
    print(xml)
    return xml

def get_nonce_str(length=32):
    rule = string.ascii_letters + string.digits
    str = random.sample(rule, length)

    return "".join(str)

def sign(params, key=None):
    """
    md5签名
    https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3
    """
    l = ['%s=%s' % (k, params.get(k, '').strip()) for k in sorted(params.keys())]
    s = '&'.join(l)
    if key:
        s += '&key=' + key

    return md5(s.encode('utf-8')).hexdigest().upper()

def download_bill(key, appid, mch_id, bill_date, sub_mch_id=None):
    """
    下载对账单
    :param bill_date: 下载对账单的日期
    :return: 返回的结果数据
    """
    while True:
        try:
            if isinstance(bill_date, (datetime, date)):
                bill_date = bill_date.strftime('%Y%m%d')

            data = {
                'appid': appid,
                'mch_id': mch_id,
                'bill_date': bill_date,
                'nonce_str': get_nonce_str(),
                'bill_type': 'ALL',
            }

            if sub_mch_id:
                data['sub_mch_id'] = sub_mch_id

            data["sign"] = sign(data, key)

            url = 'https://api.mch.weixin.qq.com/pay/downloadbill'

            req = request.Request(url)
            req.data =dict2xml(data).encode('utf-8')

            r = urllib.request.urlopen(req)
            logging.debug('downloaded.')
            return r.read()
        except Exception as e:
            import sys
            import traceback

            exc_info = sys.exc_info()
            traceback.print_exception(*exc_info)

config.py代码:

# -*- coding: utf-8 -*-
from configparser import ConfigParser

SETTINGS_FILENAME = 'settings.ini'


def load():
    config = ConfigParser()
    config.read(SETTINGS_FILENAME)

    if config.has_section('main'):
        return {
            'appid': config.get('main', 'appid'),
            'mch_id': config.get('main', 'mch_id'),
            'mch_key': config.get('main', 'mch_key'),
            'sub_mch_id': config.get('main', 'sub_mch_id'),
            'path': config.get('main', 'path'),
        }

    return {}

def save(appid, mch_id, mch_key, sub_mch_id, path):
    config = ConfigParser()

    config.add_section('main')
    config.set('main', 'appid', appid)
    config.set('main', 'mch_id', mch_id)
    config.set('main', 'mch_key', mch_key)
    config.set('main', 'sub_mch_id', sub_mch_id)
    config.set('main', 'path', path)

    with open(SETTINGS_FILENAME, 'w') as f:
        config.write(f)

运行结果如图:

Python3实现微信支付对账单下载导出CSV_第1张图片


这里写图片描述


Python3实现微信支付对账单下载导出CSV_第2张图片

你可能感兴趣的:(Python开发,微信)