代码 微信 v3 账单要二次签名、支付宝账单是 GBK 的 zip:对账脚本先过这三关

2026-10-24 21:31:39

自己拉微信/支付宝账单做对账:签名、GBK、元转分这几处最容易“假通过”

支付回调不能当作对账依据。网络抖动、服务重启、重试队列丢失都会让回调缺失;同一个回调重复投递时,又需要靠本地幂等去重;退款回调、服务费、分润、券这些条目,与支付回调根本不在同一条消息流里。平台账单是唯一与资金实际变动一致的数据来源。

微信侧的官方约定说得很直接:对账须先申请账单、再下载并核验 hash_value;交易账单须与本地订单逐笔核对,微信侧成功而本地未成功的必须有处理动作。只下载不核对,等于没做。

下面按微信 v3、支付宝、落库比对、坑清单走一遍。

微信支付 APIv3 交易账单

申请账单

GET https://api.mch.weixin.qq.com/v3/bill/tradebill?bill_date=2019-06-11&bill_type=ALL&tar_type=GZIP
Authorization: WECHATPAY2-SHA256-RSA2048 ...
Accept: application/json

参数约束:

参数说明
bill_date必填,yyyy-MM-DD。仅支持三个月内,不能是当天(当日不可下载,次日 10 点后生成;微信次日 9 点开始生成)
bill_typeALL(当日所有订单,不含充值退款)/ SUCCESS(成功支付)/ REFUND(退款单)
tar_typeGZIP 或不传(返回字符串流)

应答里三个字段:

  • hash_type:固定 SHA1
  • hash_value:账单文件的 SHA1 摘要
  • download_url:5 分钟内有效

download_url 拿到手还要再签一次

这是第一个容易踩的点:download_url 不能直接丢进浏览器或 wget。下载请求同样按 v3 规则重新生成签名,GET,path + query 参与签名。

GET
/v3/billdownload/file?token=xxx&t=xxx
1554208460
593BEC0C930BF1AFEB40B4A08C8FB242
(空行)

签名串拼好后用商户私钥 SHA256-RSA 签名,塞进 Authorization。

curl -sS "$DOWNLOAD_URL" \
  -H "Authorization: WECHATPAY2-SHA256-RSA2048 mchid=\"$MCHID\",nonce_str=\"$NONCE\",signature=\"$SIG\",timestamp=\"$TS\",serial_no=\"$SERIAL\"" \
  -o bill.gzip

下载侧几个细节:

  • 响应头不含签名,因此下载响应无需验签;
  • 账单内容默认按 utf-8 读取;
  • tar_type=GZIP 时返回 .gzip 流,先解压再解析;
  • 下载完自己算一遍 SHA1,与申请接口返回的 hash_value 比对。
raw, _ := io.ReadAll(resp.Body) // 已是解压后的字节时跳过 gzip 步骤
sum := sha1.Sum(raw)
if hex.EncodeToString(sum[:]) != hashValue {
    return fmt.Errorf("bill hash mismatch")
}

如果响应是 gzip 流:

gz, err := gzip.NewReader(bytes.NewReader(raw))
if err != nil { return err }
plain, err := io.ReadAll(gz)

错误码

  • 400 INVALID_REQUEST
  • 400 NO_STATEMENT_EXIST:账单文件不存在,或该日无交易、无退款
  • 400 STATEMENT_CREATING:账单生成中,T+1 上午 10 点后再试
  • 429 FREQUENCY_LIMITED

其余限制

  • 账单内只有支付成功的订单;
  • 账单金额单位是「元」,不是分;
  • 仅可下载三个月内账单。

支付宝账单下载

接口:alipay.data.dataservice.bill.downloadurl.query。

bill_type 取值:

值含义
trade交易业务账单,对应商家平台-交易账单
signcustomer资金账单,账户收支
merchant_act—
trade_zft_merchant—
zft_acc—
settlementMerge结算到卡批次明细 + 汇总

bill_date:

  • 日账单 yyyy-MM-dd,T+1,当日数据次日 9 点前生成,不支持当日;
  • 月账单 yyyy-MM,次月 3 号左右生成;
  • 接口仅支持近 6 年。

响应里 bill_download_url 30 秒内未下载即失效;bill_file_code=EMPTY_DATA_WITH_BILL_FILE 表示该周期无数据的空账单。

zip 内是两个 csv,编码不是 UTF-8

下载得到 zip 流,解压后含两个文件:xxx_业务明细.csv 与 xxx_业务明细(汇总).csv。

默认 GBK 编码(有资料写 GB2312)。注意两处编码都要指定:

  1. csv 内容本身;
  2. zip 内文件名含中文,zip 流的编码也得指定——Java 是 new ZipInputStream(fis, Charset.forName("gbk")),.NET 是 Encoding.GetEncoding("GB2312") 并先注册 CodePagesEncodingProvider。

PHP 里逐个文件转:

$zip = new ZipArchive();
$zip->open($path);
for ($i = 0; $i < $zip->numFiles; $i++) {
    $name = $zip->getNameIndex($i); // 中文名注意编码
    $fh = $zip->getStream($name);
    while (($line = fgets($fh)) !== false) {
        $line = mb_convert_encoding($line, 'UTF-8', 'GBK');
        if ($line[0] === '#') continue; // 说明行与结尾行
        // parse csv...
    }
}

明细 csv 结构

前 4~5 行是 # 开头的说明:

#支付宝业务明细查询
#账号
#起始日期 终止日期
#----业务明细列表----

第 5 行是表头。末几行也是 # 开头:#业务明细列表结束 / #交易合计:N笔… / #退款合计 / #导出时间。解析时统一跳过 # 行,不要按固定行号切。

表头字段:

支付宝交易号,商户订单号,业务类型,商品名称,创建时间,完成时间,门店编号,门店名称,操作员,终端号,对方账户,订单金额(元),商家实收(元),支付宝红包(元),集分宝(元),支付宝优惠(元),商家优惠(元),券核销金额(元),券名称,商家红包消费金额(元),卡消费金额(元),退款批次号/请求号,服务费(元),分润(元),备注

业务类型为「交易」或「退款」;退款行金额为负数。退款批次号/请求号对应退款接口的 out_request_no——同一交易多次部分退款,每次必须用不同的 out_request_no。

字段对应关系:

账单列系统字段
支付宝交易号trade_no
商户订单号out_trade_no
订单金额(元)total_amount
商家实收(元)receipt_amount(= 订单金额 - 商家出资优惠)
服务费(元)charge_amount
退款批次号/请求号batch_no

解压失败

多数是下载不完整,重下即可。另一个原因是文件过大:接口与商家平台不支持 1G 以上文件,月账单过大时按日账单分批下。

落库比对

金额只转一次

接口侧用整数分,账单里是「元」的十进制字符串。进入系统时只做一次转换:字符串 → 分。用 decimal,或直接按小数点拆整数与小数部分,别用 float。

function yuanToFen(string $s): int {
    [$i, $d] = array_pad(explode('.', $s, 2), 2, '');
    $d = str_pad(substr($d, 0, 2), 2, '0');
    return (int)$i * 100 + (int)$d;
}

元与分混用是对账差异里最常见的假阳性:账单没变,代码换了个单位,第二天报表多出一堆「金额不一致」。

差异分类

情况处理
一致跳过
平台有本地无漏单,补单 + 告警
本地有平台无可能未支付成功或已关单,需查单
金额不一致高优告警,人工介入

退款单必须按 out_request_no 做唯一键,否则同一笔交易的部分退款多次会被折叠成一条。

下载与存储

download_url 过期(微信 5 分钟、支付宝 30 秒)就不要复用,重新申请。下载逻辑要幂等:同日重复下载覆盖同一存储路径,避免堆出一堆半成品文件。hash_value 一并落库,作为账单版本标记——重下时能立刻判断内容有没有变。

常见坑清单

  • 微信账单当天不可下载,bill_date 传今天只会拿到错误;次日 9 点开始生成,10 点后再试。
  • STATEMENT_CREATING 不是失败,是还没好,别当异常报警。
  • download_url 直接浏览器打开——少了 v3 签名,必然失败。
  • 忘了 path + query 一起参与签名,签出来的东西服务端不认。
  • 微信金额是元,接口金额是分,转换做两遍就永远对不上。
  • 微信账单只含支付成功订单,ALL 也不含充值退款;本地未支付的单本来就不会出现在账单里,别当成漏单。
  • 支付宝 bill_download_url 只有 30 秒,串行下载多个账单时很容易超时,申请完立刻下。
  • zip 内文件名是中文,只设了内容编码没设 zip 流编码,解压出来的文件名是乱码。
  • csv 用 GBK/GB2312 读,按固定行号切表头,遇到末行 # 注释就 panic。
  • 退款行金额是负数,做汇总时符号处理错,退款越多账越"平"。
  • 部分退款不按 out_request_no 建唯一键,多次退款被折叠成一条。
  • 不校验 hash_value,截断的文件照样入库,最后表现为零星的金额差异。

推荐文章

程序员茄子在线接单