业务示例代码

更新时间:2025.12.04

1. 目标

通过本教程的学习,你应该可以:

2. 业务处理流程

2.1. 申请交易账单并下载账单

微信支付在每日10点后生成昨日交易账单文件,商户可通过接口获取账单下载链接。账单包含交易金额、时间及营销信息,利于订单核对、退款审查及银行到账确认。详细介绍参考: 产品介绍 

1package com.java.downloadbilldemo.ecommerce;
2
3import com.java.demo.DownloadBill; // 下载账单 https://pay.weixin.qq.com/doc/v3/partner/4012124894
4import com.java.demo.GetTradeBill; // 申请交易账单 https://pay.weixin.qq.com/doc/v3/partner/4012760667
5import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/partner/4014985777
6
7import static com.java.demo.DownloadBill.*; // 下载账单 https://pay.weixin.qq.com/doc/v3/partner/4012124894
8import static com.java.demo.GetTradeBill.*; // 申请交易账单 https://pay.weixin.qq.com/doc/v3/partner/4012760667
9
10public class DownloadTradeBillDemo {
11    public static void main(String[] args) {
12        GetTradeBill getTradeBill = new GetTradeBill(
13                "19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
14                "1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4012081992
15                "/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
16                "PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
17                "/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
18        );
19
20        DownloadBill downloadBill = new DownloadBill(
21                "19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
22                "1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4012081992
23                "/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
24                "PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
25                "/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
26        );
27        try {
28            GetTradeBillRequest getTradeBillRequest = new GetTradeBillRequest();
29            getTradeBillRequest.billDate = "2025-08-11"; // 期望申请下载的账单日期
30            getTradeBillRequest.billType = BillType.ALL; // 期望申请下载的账单类型
31            getTradeBillRequest.tarType = GetTradeBill.TarType.GZIP; // 期望申请下载的账单类型
32
33            QueryBillEntity response = getTradeBill.run(getTradeBillRequest);
34
35            System.out.println("Download URL: " + response.downloadUrl);
36
37            DownloadBillRequest downloadBillRequest = new DownloadBillRequest();
38            downloadBillRequest.downloadUrl = response.downloadUrl;
39            downloadBillRequest.localFilePath = "test.csv"; // 期望下载的账单文件路径
40            downloadBillRequest.tarType = DownloadBill.TarType.valueOf(getTradeBillRequest.tarType.name());
41            downloadBillRequest.expectedHashType = DownloadBill.HashType.valueOf(response.hashType.name());
42            downloadBillRequest.expectedHashValue = response.hashValue;
43
44            downloadBill.run(downloadBillRequest);
45
46            System.out.println("File downloaded successfully! Local file path: " + downloadBillRequest.localFilePath);
47            // TODO: 请求成功,继续业务逻辑
48        } catch (WXPayUtility.ApiException e) {
49            // TODO: 请求失败,根据状态码执行不同的逻辑
50            String errorCode = e.getErrorCode();
51            if (errorCode != null && errorCode.equals("NO_STATEMENT_EXIST")) {
52                // 错误:账单不存在
53                // 解决方式:请检查当前商户号在指定日期内是否有成功的交易或退款
54                // 描述:说明指定日期无账单文件生成。若无交易,则无需重试
55            } else if (errorCode != null && errorCode.equals("STATEMENT_CREATING")) {
56                // 错误:账单正在生成中
57                // 解决方式:请检查当前商户号在指定日期内是否有成功的交易或退款
58                // 描述:说明指定日期账单正在生成中。若无交易,则无需重试;若有交易,则在T+1日上午10点后再重新下载
59            } else if (errorCode != null && errorCode.equals("SYSTEM_ERROR")) {
60                // 错误:系统错误
61                // 解决方式:稍后重试
62                // 描述:微信支付系统失败,系统失败直接立即重试大概率还会是系统失败,建议等1分钟后再重试
63            } else if (errorCode != null && errorCode.equals("FREQUENCY_LIMITED")) {
64                // 错误:限频报错
65                // 解决方式:稍后重试
66                // 描述: 接口频率限制,直接立即重试大概率还会是系统失败,建议等1分钟后再重试
67            } else if (errorCode != null && errorCode.equals("SIGN_ERROR")) {
68                // 错误:签名错误
69                // 解决方式:检查平台商户证书序列号,证书私钥文件,公钥ID,公钥文件,同时确认下签名过程是不是按照微信支付的签名方式
70                // 描述:签名报错,需要确认签名材料和签名流程是否正确
71            } else if (errorCode != null && errorCode.equals("PARAM_ERROR")) {
72                // 错误:参数错误
73                // 解决方式:按照报错返回的message,重新输入请求参数
74                // 描述:参数的类型,长度,或者必填选项没有填写等
75            } else if (errorCode != null && errorCode.equals("INVALID_REQUEST")) {
76                // 错误:请求非法,请求参数正确,但是不符合下载账单业务规则
77                // 解决方式:根据具体message查看具体哪里不符合业务规则,如果可以修改参数达到符合业务规则的修改请求符合业务规则之后再重试
78                // 描述:不符合业务规则的场景如:
79                // - 账单时间超过三个月,不允许申请下载账单
80            } else {
81                // 其他类型错误:稍等一会后重试
82            }
83        }
84    }
85}
86

2.2. 申请资金账单并下载账单

微信支付按天提供服务商各账户的资金流水账单文件,服务商可以通过该接口获取账单文件的下载地址。账单文件详细记录了服务商账户资金操作的相关信息,包括业务单号、收支金额及记账时间等,以便服务商进行核对与确认。详细介绍参考: 产品介绍 

1package com.java.downloadbilldemo.ecommerce;
2
3import com.java.demo.DownloadBill; // 下载账单 https://pay.weixin.qq.com/doc/v3/partner/4012124894
4import com.java.demo.DownloadBill.DownloadBillRequest;
5import com.java.demo.GetFundFlowBill; // 申请资金账单 https://pay.weixin.qq.com/doc/v3/partner/4012760672
6import com.java.demo.GetFundFlowBill.FundFlowBillAccountType;
7import com.java.demo.GetFundFlowBill.GetFundFlowBillRequest;
8import com.java.demo.GetFundFlowBill.QueryBillEntity;
9import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/partner/4014985777
10
11public class DownloadFundFlowBillDemo {
12
13    public static void main(String[] args) {
14        String mchCode = "19xxxxxxxx";  // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
15        String mchSerialNo = "1DDE55AD98Exxxxxxxxxx"; // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4012081992
16        String mchPrivateKeyPath = "/path/to/apiclient_key.pem"; // 商户API证书私钥文件路径,本地文件路径
17        String mchPublicKeyId = "PUB_KEY_ID_xxxxxxxxxxxxx"; // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
18        String mchPublicKeyPath = "/path/to/wxp_pub.pem"; // 微信支付公钥文件路径,本地文件路径
19
20        GetFundFlowBill getFundFlowBill = new GetFundFlowBill(
21                mchCode,
22                mchSerialNo,
23                mchPrivateKeyPath,
24                mchPublicKeyId,
25                mchPublicKeyPath
26        );
27
28        DownloadBill downloadBill = new DownloadBill(
29                mchCode,
30                mchSerialNo,
31                mchPrivateKeyPath,
32                mchPublicKeyId,
33                mchPublicKeyPath
34        );
35
36        try {
37            GetFundFlowBillRequest getFundFlowBillRequest = new GetFundFlowBillRequest();
38            getFundFlowBillRequest.billDate = "2025-11-25"; // 期望申请下载的账单日期
39            getFundFlowBillRequest.accountType = FundFlowBillAccountType.BASIC; // 期望申请下载的账单类型
40            getFundFlowBillRequest.tarType = GetFundFlowBill.TarType.GZIP; // 期望申请下载的账单压缩类型
41
42            QueryBillEntity response = getFundFlowBill.run(getFundFlowBillRequest);
43
44            System.out.println("Download URL: " + response.downloadUrl);
45
46            DownloadBillRequest downloadBillRequest = new DownloadBillRequest();
47            downloadBillRequest.downloadUrl = response.downloadUrl;
48            // 期望下载的账单文件路径(当前目录下 mchCode+accountType+bill_date.csv)
49            downloadBillRequest.localFilePath = mchCode + "_" + getFundFlowBillRequest.accountType.name() + "_" + getFundFlowBillRequest.billDate + ".csv";
50            downloadBillRequest.tarType = DownloadBill.TarType.valueOf(getFundFlowBillRequest.tarType.name());
51            downloadBillRequest.expectedHashType = DownloadBill.HashType.valueOf(response.hashType.name());
52            downloadBillRequest.expectedHashValue = response.hashValue;
53
54            downloadBill.run(downloadBillRequest);
55
56            System.out.println("File downloaded successfully! Local file path: " + downloadBillRequest.localFilePath);
57
58        } catch (WXPayUtility.ApiException e) {
59            // TODO: 请求失败,根据状态码执行不同的逻辑
60            String errorCode = e.getErrorCode();
61            if (errorCode != null && errorCode.equals("NO_STATEMENT_EXIST")) {
62                // 错误:账单不存在
63                // 解决方式:请检查当前商户号在指定日期内是否有资金变动
64                // 描述:说明指定日期无账单文件生成。若无资金变动,则无需重试
65            } else if (errorCode != null && errorCode.equals("STATEMENT_CREATING")) {
66                // 错误:账单正在生成中
67                // 解决方式:请检查当前商户号在指定日期内是否有资金变动
68                // 描述:说明指定日期账单正在生成中。若无资金变动,则无需重试;若有资金变动,则在T+1日上午10点后再重新下载
69            } else if (errorCode != null && errorCode.equals("SYSTEM_ERROR")) {
70                // 错误:系统错误
71                // 解决方式:稍后重试
72                // 描述:微信支付系统失败,系统失败直接立即重试大概率还会是系统失败,建议等1分钟后再重试
73            } else if (errorCode != null && errorCode.equals("FREQUENCY_LIMITED")) {
74                // 错误:限频报错
75                // 解决方式:稍后重试
76                // 描述:接口频率限制,直接立即重试大概率还会是系统失败,建议等1分钟后再重试
77            } else if (errorCode != null && errorCode.equals("SIGN_ERROR")) {
78                // 错误:签名错误
79                // 解决方式:检查平台商户证书序列号,证书私钥文件,公钥ID,公钥文件,同时确认下签名过程是不是按照微信支付的签名方式
80                // 描述:签名报错,需要确认签名材料和签名流程是否正确
81            } else if (errorCode != null && errorCode.equals("PARAM_ERROR")) {
82                // 错误:参数错误
83                // 解决方式:按照报错返回的message,重新输入请求参数
84                // 描述:参数的类型,长度,或者必填选项没有填写等
85            } else if (errorCode != null && errorCode.equals("INVALID_REQUEST")) {
86                // 错误:请求非法,请求参数正确,但是不符合下载账单业务规则
87                // 解决方式:根据具体message查看具体哪里不符合业务规则,如果可以修改参数达到符合业务规则的修改请求符合业务规则之后再重试
88                // 描述:不符合业务规则的场景如:
89                // - 账单时间超过三个月,不允许申请下载账单
90            } else {
91                // 其他类型错误:稍等一会后重试
92            }
93        }
94    }
95}
96

2.3. 申请二级资金账单并下载账单

微信支付按天提供微信支付账户的资金流水账单文件,电商平台可以通过该接口获取二级商户账单文件的下载地址。文件内包含电商平台二级商户资金操作相关的业务单号、收支金额、记账时间等信息,供电商平台进行核对。

1package com.java.downloadbilldemo.ecommerce;
2
3import com.java.demo.DownloadBill; // 下载账单 https://pay.weixin.qq.com/doc/v3/partner/4012124894
4import com.java.demo.GetAllSubMchFundFlowBill; // 申请二级商户资金账单 https://pay.weixin.qq.com/doc/v3/partner/4012760697
5import com.java.demo.GetAllSubMchFundFlowBill.EncryptBillEntity;
6import com.java.demo.GetAllSubMchFundFlowBill.GetAllSubMchFundFlowBillRequest;
7import com.java.demo.GetAllSubMchFundFlowBill.QueryEncryptBillEntity;
8import com.java.demo.GetAllSubMchFundFlowBill.SubMerchantFundFlowBillAccountType;
9import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/partner/4014985777
10
11public class DownloadAllSubMchFundFlowBill {
12
13    public static void main(String[] args) {
14        String billDate = "2025-11-26"; // 账单日期,格式为yyyy-MM-dd
15        SubMerchantFundFlowBillAccountType accountType = SubMerchantFundFlowBillAccountType.ALL;
16
17        try {
18            // 初始化API客户端
19            GetAllSubMchFundFlowBill billApi = createBillApi();
20            DownloadBill downloadApi = createDownloadApi();
21
22            // 申请并下载账单
23            QueryEncryptBillEntity response = applyForBill(billApi, accountType, billDate);
24            System.out.println("账单数量: " + response.downloadBillCount);
25
26            // 处理每个账单
27            for (int i = 0; i < response.downloadBillList.size(); i++) {
28                EncryptBillEntity billEntity = response.downloadBillList.get(i);
29                processBill(downloadApi, billEntity, billDate, i + 1, accountType);
30            }
31
32            System.out.println("\n所有账单下载并解密完成!");
33        } catch (WXPayUtility.ApiException e) {
34            handleApiException(e);
35        } catch (Exception e) {
36            System.err.println("程序执行出错:");
37            e.printStackTrace();
38        }
39    }
40
41    /**
42     * 创建账单申请API客户端
43     */
44    private static GetAllSubMchFundFlowBill createBillApi() {
45        return new GetAllSubMchFundFlowBill(
46                EncryptedBillDownloadHelper.getMchCode(),
47                EncryptedBillDownloadHelper.getMchSerialNo(),
48                EncryptedBillDownloadHelper.getMchPrivateKeyPath(),
49                EncryptedBillDownloadHelper.getMchPublicKeyId(),
50                EncryptedBillDownloadHelper.getMchPublicKeyPath()
51        );
52    }
53
54    /**
55     * 创建账单下载API客户端
56     */
57    private static DownloadBill createDownloadApi() {
58        return EncryptedBillDownloadHelper.createDownloadApi();
59    }
60
61    /**
62     * 申请账单
63     */
64    private static QueryEncryptBillEntity applyForBill(
65            GetAllSubMchFundFlowBill billApi,
66            SubMerchantFundFlowBillAccountType accountType,
67            String billDate) throws Exception {
68
69        GetAllSubMchFundFlowBillRequest request = new GetAllSubMchFundFlowBillRequest();
70        request.billDate = billDate;
71        request.accountType = accountType;
72        request.algorithm = GetAllSubMchFundFlowBill.Algorithm.AEAD_AES_256_GCM;
73        request.tarType = null;
74
75        return billApi.run(request);
76    }
77
78    /**
79     * 处理单个账单:下载、解密、校验
80     */
81    private static void processBill(
82            DownloadBill downloadApi,
83            EncryptBillEntity billEntity,
84            String billDate,
85            int billIndex,
86            SubMerchantFundFlowBillAccountType accountType) throws Exception {
87
88        System.out.println("\n下载第 " + billIndex + " 个账单 (序号: " + billEntity.billSequence + ")");
89        System.out.println("Download URL: " + billEntity.downloadUrl);
90
91        // 生成文件路径
92        String encryptedFilePath = generateFilePath(billDate, billEntity.billSequence, accountType, true);
93        String decryptedFilePath = generateFilePath(billDate, billEntity.billSequence, accountType, false);
94
95        // 下载加密账单
96        downloadEncryptedBill(downloadApi, billEntity.downloadUrl, encryptedFilePath);
97
98        // 解密账单
99        decryptBill(encryptedFilePath, decryptedFilePath, billEntity);
100
101        // 校验SHA1
102        verifySha1(decryptedFilePath, billEntity);
103    }
104
105    /**
106     * 生成文件路径
107     */
108    private static String generateFilePath(String billDate, long sequence,
109                                          SubMerchantFundFlowBillAccountType accountType, boolean encrypted) {
110        String suffix = encrypted ? "_encrypted.csv" : ".csv";
111        return EncryptedBillDownloadHelper.getMchCode() + "_" + accountType.name() + "_" + billDate + "_" + sequence + suffix;
112    }
113
114    /**
115     * 下载加密账单
116     */
117    private static void downloadEncryptedBill(
118            DownloadBill downloadApi,
119            String downloadUrl,
120            String filePath) throws Exception {
121        EncryptedBillDownloadHelper.downloadEncryptedBill(downloadApi, downloadUrl, filePath);
122    }
123
124    /**
125     * 解密账单
126     */
127    private static void decryptBill(
128            String encryptedFilePath,
129            String decryptedFilePath,
130            EncryptBillEntity billEntity) throws Exception {
131
132        // 读取加密文件
133        byte[] encryptedData = EncryptedBillDownloadHelper.readFile(encryptedFilePath);
134
135        // 解密账单
136        EncryptedBillDownloadHelper.decryptBill(
137                encryptedFilePath,
138                decryptedFilePath,
139                billEntity.encryptKey,
140                billEntity.nonce,
141                encryptedData
142        );
143    }
144
145    /**
146     * 校验SHA1
147     */
148    private static void verifySha1(String filePath, EncryptBillEntity billEntity) throws Exception {
149        EncryptedBillDownloadHelper.verifySha1(filePath, billEntity.hashValue, billEntity.hashType);
150    }
151
152    /**
153     * 处理API异常
154     */
155    private static void handleApiException(WXPayUtility.ApiException e) {
156        EncryptedBillDownloadHelper.handleApiException(e);
157    }
158}
159

2.4. 申请单个子商户资金账单并下载账单

微信支付按天提供微信支付账户的资金流水账单文件,服务商可以通过该接口获取子商户账单文件的下载地址。文件内包含子商户资金操作相关的业务单号、收支金额、记账时间等信息,供商户进行核对。

1package com.java.downloadbilldemo.ecommerce;
2
3import com.java.demo.DownloadBill; // 下载账单 https://pay.weixin.qq.com/doc/v3/partner/4012124894
4import com.java.demo.GetSingleSubMchFundFlowBill; // 申请单个子商户资金账单 https://pay.weixin.qq.com/doc/v3/partner/4012760249
5import com.java.demo.GetSingleSubMchFundFlowBill.EncryptBillEntity;
6import com.java.demo.GetSingleSubMchFundFlowBill.GetSingleSubMchFundFlowBillRequest;
7import com.java.demo.GetSingleSubMchFundFlowBill.QueryEncryptBillEntity;
8import com.java.demo.GetSingleSubMchFundFlowBill.SubMchFundFlowBillAccountType;
9import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/partner/4014985777
10
11public class DownloadSingleSubMchFundFlowBillDemo {
12
13    public static void main(String[] args) {
14        String subMchid = "2600173132"; // 子商户号
15        String billDate = "2025-11-25"; // 账单日期,格式为yyyy-MM-dd
16        SubMchFundFlowBillAccountType accountType = SubMchFundFlowBillAccountType.BASIC;
17
18        try {
19            // 初始化API客户端
20            GetSingleSubMchFundFlowBill billApi = createBillApi();
21            DownloadBill downloadApi = createDownloadApi();
22
23            // 申请并下载账单
24            QueryEncryptBillEntity response = applyForBill(billApi, subMchid, accountType, billDate);
25            System.out.println("账单数量: " + response.downloadBillCount);
26
27            // 处理每个账单
28            for (int i = 0; i < response.downloadBillList.size(); i++) {
29                EncryptBillEntity billEntity = response.downloadBillList.get(i);
30                processBill(downloadApi, billEntity, subMchid, billDate, i + 1, accountType);
31            }
32
33            System.out.println("\n所有账单下载并解密完成!");
34        } catch (WXPayUtility.ApiException e) {
35            handleApiException(e);
36        } catch (Exception e) {
37            System.err.println("程序执行出错:");
38            e.printStackTrace();
39        }
40    }
41
42    /**
43     * 创建账单申请API客户端
44     */
45    private static GetSingleSubMchFundFlowBill createBillApi() {
46        return new GetSingleSubMchFundFlowBill(
47                EncryptedBillDownloadHelper.getMchCode(),
48                EncryptedBillDownloadHelper.getMchSerialNo(),
49                EncryptedBillDownloadHelper.getMchPrivateKeyPath(),
50                EncryptedBillDownloadHelper.getMchPublicKeyId(),
51                EncryptedBillDownloadHelper.getMchPublicKeyPath()
52        );
53    }
54
55    /**
56     * 创建账单下载API客户端
57     */
58    private static DownloadBill createDownloadApi() {
59        return EncryptedBillDownloadHelper.createDownloadApi();
60    }
61
62    /**
63     * 申请账单
64     */
65    private static QueryEncryptBillEntity applyForBill(
66            GetSingleSubMchFundFlowBill billApi,
67            String subMchid,
68            SubMchFundFlowBillAccountType accountType,
69            String billDate) throws Exception {
70
71        GetSingleSubMchFundFlowBillRequest request = new GetSingleSubMchFundFlowBillRequest();
72        request.subMchid = subMchid;
73        request.billDate = billDate;
74        request.accountType = accountType;
75        request.algorithm = GetSingleSubMchFundFlowBill.Algorithm.AEAD_AES_256_GCM;
76        request.tarType = null;
77
78        return billApi.run(request);
79    }
80
81    /**
82     * 处理单个账单:下载、解密、校验
83     */
84    private static void processBill(
85            DownloadBill downloadApi,
86            EncryptBillEntity billEntity,
87            String subMchid,
88            String billDate,
89            int billIndex,
90            SubMchFundFlowBillAccountType accountType) throws Exception {
91
92        System.out.println("\n下载第 " + billIndex + " 个账单 (序号: " + billEntity.billSequence + ")");
93        System.out.println("Download URL: " + billEntity.downloadUrl);
94
95        // 生成文件路径
96        String encryptedFilePath = generateFilePath(subMchid, billDate, billEntity.billSequence, accountType, true);
97        String decryptedFilePath = generateFilePath(subMchid, billDate, billEntity.billSequence, accountType, false);
98
99        // 下载加密账单
100        downloadEncryptedBill(downloadApi, billEntity.downloadUrl, encryptedFilePath);
101
102        // 解密、解压账单
103        decryptBill(encryptedFilePath, decryptedFilePath, billEntity);
104
105        // 校验SHA1
106        verifySha1(decryptedFilePath, billEntity);
107    }
108
109    /**
110     * 生成文件路径
111     */
112    private static String generateFilePath(String subMchid, String billDate, long sequence,
113                                          SubMchFundFlowBillAccountType accountType, boolean encrypted) {
114        String suffix = encrypted ? "_encrypted.csv" : ".csv";
115        return subMchid + "_" + accountType.name() + "_" + billDate + "_" + sequence + suffix;
116    }
117
118    /**
119     * 下载加密账单
120     */
121    private static void downloadEncryptedBill(
122            DownloadBill downloadApi,
123            String downloadUrl,
124            String filePath) throws Exception {
125        EncryptedBillDownloadHelper.downloadEncryptedBill(downloadApi, downloadUrl, filePath);
126    }
127
128    /**
129     * 解密账单
130     */
131    private static void decryptBill(
132            String encryptedFilePath,
133            String decryptedFilePath,
134            EncryptBillEntity billEntity) throws Exception {
135
136        // 读取加密文件
137        byte[] encryptedData = EncryptedBillDownloadHelper.readFile(encryptedFilePath);
138
139        // 解密账单
140        EncryptedBillDownloadHelper.decryptBill(
141                encryptedFilePath,
142                decryptedFilePath,
143                billEntity.encryptKey,
144                billEntity.nonce,
145                encryptedData
146        );
147    }
148
149    /**
150     * 校验SHA1
151     */
152    private static void verifySha1(String filePath, EncryptBillEntity billEntity) throws Exception {
153        EncryptedBillDownloadHelper.verifySha1(filePath, billEntity.hashValue, billEntity.hashType);
154    }
155
156    /**
157     * 处理API异常
158     */
159    private static void handleApiException(WXPayUtility.ApiException e) {
160        EncryptedBillDownloadHelper.handleApiException(e);
161    }
162}
163

2.5. 加密账单下载工具类

本工具类用于电商平台场景下的加密账单下载、解密和校验。参考下载单个子商户/二级商户资金账单。被 类DownloadAllSubMchFundFlowBill 和 类DownloadSingleSubMchFundFlowBillDemo 使用。

1package com.java.downloadbilldemo.ecommerce;
2
3import com.java.demo.DownloadBill;
4import com.java.demo.DownloadBill.DownloadBillRequest;
5import com.java.utils.WXPayUtility;
6
7import java.io.FileInputStream;
8import java.io.FileOutputStream;
9import java.nio.charset.StandardCharsets;
10import java.security.PrivateKey;
11
12/**
13 * 加密账单下载通用工具类
14 * <p>
15 * 本工具类用于电商平台场景下的加密账单下载、解密和校验。
16 * 主要功能包括:
17 * <ul>
18 *   <li>下载加密账单文件</li>
19 *   <li>使用商户私钥解密账单加密密钥(encrypt_key)</li>
20 *   <li>使用解密后的密钥解密账单内容(AEAD_AES_256_GCM算法)</li>
21 *   <li>校验账单文件的SHA1哈希值</li>
22 * </ul>
23 * </p>
24 * <p>
25 * <b>使用位置:</b>
26 * <ul>
27 *   <li>适用于电商平台(ecommerce)场景下的账单下载</li>
28 *   <li>被 {@link DownloadAllSubMchFundFlowBill} 和 {@link DownloadSingleSubMchFundFlowBillDemo} 使用</li>
29 *   <li>用于处理需要加密的二级商户资金账单</li>
30 * </ul>
31 * </p>
32 * <p>
33 * <b>使用前准备:</b>
34 * <ol>
35 *   <li>修改类中的商户配置信息(MCH_CODE、MCH_SERIAL_NO等)</li>
36 *   <li>确保证书文件路径正确(MCH_PRIVATE_KEY_PATH、MCH_PUBLIC_KEY_PATH)</li>
37 *   <li>确保已正确配置微信支付公钥ID(MCH_PUBLIC_KEY_ID)</li>
38 * </ol>
39 * </p>
40 * <p>
41 * <b>参考文档:</b>
42 * <ul>
43 *   <li>申请二级商户资金账单:https://pay.weixin.qq.com/doc/v3/partner/4012760697</li>
44 *   <li>申请单个子商户资金账单:https://pay.weixin.qq.com/doc/v3/partner/4012760249</li>
45 * </ul>
46 * </p>
47 */
48public class EncryptedBillDownloadHelper {
49
50    // 配置属性
51    private static final String MCH_CODE = "19xxxxxxxx"; // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
52    private static final String MCH_SERIAL_NO = "1DDE55AD98Exxxxxxxxxx";  // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4012081992
53    private static final String MCH_PRIVATE_KEY_PATH = "/path/to/apiclient_key.pem"; // 商户API证书私钥文件路径,本地文件路径
54    private static final String MCH_PUBLIC_KEY_ID = "PUB_KEY_ID_xxxxxxxxxxxxx"; // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
55    private static final String MCH_PUBLIC_KEY_PATH = "/path/to/wxp_pub.pem";  // 微信支付公钥文件路径,本地文件路径
56
57    /**
58     * 获取商户号
59     */
60    public static String getMchCode() {
61        return MCH_CODE;
62    }
63
64    /**
65     * 获取商户证书序列号
66     */
67    public static String getMchSerialNo() {
68        return MCH_SERIAL_NO;
69    }
70
71    /**
72     * 获取商户私钥路径
73     */
74    public static String getMchPrivateKeyPath() {
75        return MCH_PRIVATE_KEY_PATH;
76    }
77
78    /**
79     * 获取微信支付公钥ID
80     */
81    public static String getMchPublicKeyId() {
82        return MCH_PUBLIC_KEY_ID;
83    }
84
85    /**
86     * 获取微信支付公钥路径
87     */
88    public static String getMchPublicKeyPath() {
89        return MCH_PUBLIC_KEY_PATH;
90    }
91
92    /**
93     * 创建账单下载API客户端
94     */
95    public static DownloadBill createDownloadApi() {
96        return new DownloadBill(
97                MCH_CODE, MCH_SERIAL_NO, MCH_PRIVATE_KEY_PATH,
98                MCH_PUBLIC_KEY_ID, MCH_PUBLIC_KEY_PATH
99        );
100    }
101
102    /**
103     * 下载加密账单
104     */
105    public static void downloadEncryptedBill(
106            DownloadBill downloadApi,
107            String downloadUrl,
108            String filePath) throws Exception {
109
110        DownloadBillRequest request = new DownloadBillRequest();
111        request.downloadUrl = downloadUrl;
112        request.localFilePath = filePath;
113        request.tarType = null;
114        request.expectedHashType = null; // 跳过SHA1校验,解密后再校验
115        request.expectedHashValue = null;
116
117        downloadApi.run(request);
118        System.out.println("加密账单下载成功: " + filePath);
119    }
120
121    /**
122     * 解密账单
123     */
124    public static void decryptBill(
125            String encryptedFilePath,
126            String decryptedFilePath,
127            String encryptKey,
128            String nonce,
129            byte[] encryptedData) throws Exception {
130
131        // 解密密钥
132        String billEncryptKey = decryptEncryptKey(encryptKey);
133
134        // 解密内容
135        byte[] billKeyBytes = billEncryptKey.getBytes(StandardCharsets.UTF_8);
136        byte[] nonceBytes = nonce.getBytes(StandardCharsets.UTF_8);
137        String decryptedContent = WXPayUtility.aesAeadDecrypt(billKeyBytes, null, nonceBytes, encryptedData);
138
139        // 保存明文文件
140        writeFile(decryptedFilePath, decryptedContent);
141        System.out.println("账单解密成功: " + decryptedFilePath);
142    }
143
144    /**
145     * 解密encrypt_key
146     */
147    public static String decryptEncryptKey(String encryptKey) throws Exception {
148        PrivateKey privateKey = WXPayUtility.loadPrivateKeyFromPath(MCH_PRIVATE_KEY_PATH);
149        return WXPayUtility.rsaOaepDecrypt(privateKey, encryptKey);
150    }
151
152    /**
153     * 校验SHA1
154     * @param filePath 文件路径
155     * @param expectedHashValue 期望的SHA1值
156     * @param hashType 哈希类型(用于类型检查)
157     */
158    public static void verifySha1(String filePath, String expectedHashValue, Object hashType) throws Exception {
159        // 检查hashType是否为SHA1类型
160        if (hashType == null || !hashType.toString().equals("SHA1")) {
161            return;
162        }
163
164        String actualSha1 = org.apache.commons.codec.digest.DigestUtils.sha1Hex(new FileInputStream(filePath));
165        if (actualSha1.equals(expectedHashValue)) {
166            System.out.println("SHA1 校验通过: " + actualSha1);
167        } else {
168            System.err.println("警告:SHA1 校验失败!");
169            System.err.println("  期望值: " + expectedHashValue);
170            System.err.println("  实际值: " + actualSha1);
171        }
172    }
173
174    /**
175     * 读取文件
176     */
177    public static byte[] readFile(String filePath) throws Exception {
178        FileInputStream fis = new FileInputStream(filePath);
179        byte[] data = fis.readAllBytes();
180        fis.close();
181        return data;
182    }
183
184    /**
185     * 写入文件
186     */
187    public static void writeFile(String filePath, String content) throws Exception {
188        FileOutputStream fos = new FileOutputStream(filePath);
189        fos.write(content.getBytes(StandardCharsets.UTF_8));
190        fos.close();
191    }
192
193    /**
194     * 处理API异常
195     * @param e API异常
196     */
197    public static void handleApiException(WXPayUtility.ApiException e) {
198        // TODO: 请求失败,根据状态码执行不同的逻辑
199        String errorCode = e.getErrorCode();
200        if (errorCode == null) {
201            // API异常,可通过 e.getStatusCode() 和 e.getErrorMessage() 获取详细信息
202        } else if (errorCode.equals("NO_STATEMENT_EXIST")) {
203            // 错误:账单不存在
204            // 解决方式:请检查子商户是否在指定日期有资金变动
205            // 描述:说明指定日期无账单文件生成。若无交易,则无需重试
206        } else if (errorCode.equals("STATEMENT_CREATING")) {
207            // 错误:账单正在生成中
208            // 解决方式:请检查子商户是否在指定日期有资金变动
209            // 描述:说明指定日期账单正在生成中。若无交易,则无需重试;若有交易,则在T+1日上午10点后再重新下载
210        } else if (errorCode.equals("SYSTEM_ERROR")) {
211            // 错误:系统错误
212            // 解决方式:稍后重试
213            // 描述:微信支付系统失败,系统失败直接立即重试大概率还会是系统失败,建议等1分钟后再重试
214        } else if (errorCode.equals("FREQUENCY_LIMITED")) {
215            // 错误:限频报错
216            // 解决方式:稍后重试
217            // 描述:接口频率限制,直接立即重试大概率还会是系统失败,建议等1分钟后再重试
218        } else if (errorCode.equals("SIGN_ERROR")) {
219            // 错误:签名错误
220            // 解决方式:检查平台商户证书序列号,证书私钥文件,公钥ID,公钥文件,同时确认下签名过程是不是按照微信支付的签名方式
221            // 描述:签名报错,需要确认签名材料和签名流程是否正确
222        } else if (errorCode.equals("PARAM_ERROR")) {
223            // 错误:参数错误
224            // 解决方式:按照报错返回的message,重新输入请求参数
225            // 描述:参数的类型,长度,或者必填选项没有填写等
226        } else if (errorCode.equals("INVALID_REQUEST")) {
227            // 错误:请求非法,请求参数正确,但是不符合下载账单业务规则
228            // 解决方式:根据具体message查看具体哪里不符合业务规则,如果可以修改参数达到符合业务规则的修改请求符合业务规则之后再重试
229            // 描述:不符合业务规则的场景如:
230            // - 账单时间超过三个月,不允许申请下载账单
231        } else {
232            // 其他类型错误:稍等一会后重试
233        }
234    }
235}
236