微信下载对账单官方内容:
应用场景
商户可以通过该接口下载历史交易清单。比如掉单、系统错误等导致商户侧和微信侧数据不一致,通过对账单核对后可校正支付状态。
注意:
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-1016:33:45,`wx2421b1c4370ec43b,`10000100,`0,`1000,`1001690740201411100005734289,`1415640626,`085e9858e3ba5186aafcbaed1,`MICROPAY,`SUCCESS,`CFT,`CNY,`0.01,`0.0,`0,`0,`0,`0,`,`,`被扫支付测试,`订单额外描述,`0,`0.60%
`2014-11-1016:46:14,`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('<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 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 = "<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 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)