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

来源:互联网 发布:解方程组矩阵的方法 编辑:程序博客网 时间:2024/06/02 09:40

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

应用场景

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

注意:

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>wx2421b1c4370ec43b</appid>  <bill_date>20141110</bill_date>  <bill_type>ALL</bill_type>  <mch_id>10000100</mch_id>  <nonce_str>21df7dc9cd8616b56919f20d9f679233</nonce_str>  <sign>332F17B766FC787203EBE9D6E40457A1</sign></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 osimport subprocessimport sysimport timeimport loggingimport threadingfrom datetime import datetime, timedeltafrom tkinter import *from tkinter import filedialog, Frame, Tk, N, W, messageboxfrom tkinter import fontfrom tkinter.ttk import Progressbarfrom weixin_mch_api import download_billimport configlogging.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('<xml>'):            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 cgiimport jsonimport loggingimport randomimport stringimport urllibfrom hashlib import md5from datetime import datetime, datefrom urllib import requestdef dict2xml(params):    xml = "<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 += "</xml>"    print(xml)    return xmldef 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 ConfigParserSETTINGS_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)

运行结果如图:

这里写图片描述


这里写图片描述


这里写图片描述

原创粉丝点击