业务示例代码

更新时间:2026.01.22

1. 目标

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

  • 邀请商户开通微信支付电子发票能力

  • 设置商户的开发配置选项

  • 让用户通过该商户的支付凭证开具微信支付电子发票

  • 通过用户主动授权,使商户开具微信支付电子发票

  • 将自建/第三方电子发票平台的发票,通过卡包交付给用户电子发票

  • 开具或冲红数电发票,并下载微信支付电子发票文件

2. 业务处理流程

2.1 邀请商户开通微信支付电子发票

2.1.1 业务描述

通过该业务,可以实现邀请未开通电子发票功能的商户开通微信支付电子发票。也可以邀请已经开通过电子发票功能的商户与原服务商/合作伙伴解除关联关系,并与新的服务商/合作伙伴进行绑定。还可以对已经开通了微信支付电子发票能力——微信电子发票数电模式的商户,追加邀请开通开票能力。

2.1.2 业务示例代码

1package com.java.partner;
2
3import com.java.demo.CheckSubMchStatus; // https://pay.weixin.qq.com/doc/v3/partner/4015792561
4import com.java.demo.CheckSubMchStatus.CheckSubMchStatusRequest;
5import com.java.demo.CheckSubMchStatus.CheckSubMchStatusResponse;
6import com.java.demo.CheckSubMchStatus.DigitalTaxAbilityInfo;
7import com.java.demo.CheckSubMchStatus.DigitalTaxApplyStatus;
8import com.java.demo.CheckSubMchStatus.DigitalTaxBillingPersonInfo;
9import com.java.demo.CheckSubMchStatus.DigitalTaxMode;
10import com.java.demo.CheckSubMchStatus.ThirdMode;
11import com.java.demo.CheckSubMchStatus.ThirdModeStatus;
12import com.java.demo.GetSpInviteUrl; // https://pay.weixin.qq.com/doc/v3/partner/4015941495
13import com.java.demo.GetSpInviteUrl.FapiaoAbilityType;
14import com.java.demo.GetSpInviteUrl.GetSpInviteUrlRequest;
15import com.java.demo.GetSpInviteUrl.GetSpInviteUrlResp;
16import com.java.demo.GetSpInviteUrl.InviteOperationType;
17import com.java.demo.GetSpInviteUrl.InvoiceMode;
18import com.java.demo.ListSpInviteMchInfo;  // https://pay.weixin.qq.com/doc/v3/partner/4015941524
19import com.java.demo.ListSpInviteMchInfo.ListSpInviteMchInfoRequest;
20import com.java.demo.ListSpInviteMchInfo.ListSpInviteMchInfoResp;
21import com.java.demo.ListSpInviteMchInfo.MchInviteResultInfo;
22import com.java.demo.ListSpInviteMchInfo.MchInviteStatus;
23import com.java.utils.WXPayUtility;
24import java.time.LocalDateTime;
25import java.time.ZoneId;
26import java.time.format.DateTimeFormatter;
27import java.util.ArrayList;
28import java.util.List;
29import java.util.Objects;
30
31public class InviteMerchantDemo {
32
33    public static void main(String[] args) {
34        // TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/partner/4013080340
35        GetSpInviteUrl getInvite = new GetSpInviteUrl(
36                "19xxxxxxxx",
37                // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
38                "1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
39                "/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
40                "PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
41                "/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
42        );
43
44        ListSpInviteMchInfo listSpInviteMchInfo = new ListSpInviteMchInfo(
45                "19xxxxxxxx",
46                // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
47                "1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
48                "/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
49                "PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
50                "/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
51        );
52
53        CheckSubMchStatus checkSubMchStatus = new CheckSubMchStatus(
54                "19xxxxxxxx",
55                // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
56                "1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
57                "/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
58                "PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
59                "/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
60        );
61
62        GetSpInviteUrlRequest inviteRequest = new GetSpInviteUrlRequest();
63        inviteRequest.operationType = InviteOperationType.AUTH_BINDING;
64        inviteRequest.fapiaoMode = new ArrayList<>();
65        {
66            inviteRequest.fapiaoMode.add(InvoiceMode.THIRD_OR_SELF_FAPIAO);
67        }
68        inviteRequest.fapiaoAbilityTypeList = new ArrayList<>();
69        {
70            inviteRequest.fapiaoAbilityTypeList.add(FapiaoAbilityType.BASE_ABILITY);
71        }
72        // 服务商设置标识此次邀请链接生成的渠道
73        inviteRequest.inviteChannel = "miniprogram";
74        // 服务商设置标识此次邀请链接生成的操作人员
75        inviteRequest.operateUser = "mI7HGEJ4Q2B91IGjHZu/Gthm87Szv0MK2AoC0/3ZMDgltMtdoY6O0qZ4F1iXiwCuqkkBe+9M4ggvdzRVVVB9s+zLEQ8" +
76        	"nv74vsgl77MZx14nd5obtCcfAvPfDJob3oG7FqlThmYKJqjiOwBvvQse7p9R8onj/POzSrbM8re8ZYGp4LcehXopTLdk2ZVWRv8bnJgKZeY4vpMmq4xuRTYk6xNXvowBBKLu4oaNFNdBO3fip1a1rFW0vRw==";
77        inviteRequest.inviteCode = "code_20200101_123";
78        // 子商户号非必填,如果填入表示这个链接只能邀请这个商户号,如果未填表示商户扫码后,需要手动输入商户号或者微信支付订单号来进行开通
79        inviteRequest.subMchid = "19998278783";
80
81        ListSpInviteMchInfoRequest listSpInviteMchInfoRequest = new ListSpInviteMchInfoRequest();
82        final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(
83                ZoneId.of("Asia/Shanghai"));
84        LocalDateTime now = LocalDateTime.now();
85        // 定时查询当前时间过去10分钟之内邀请的商户信息
86        listSpInviteMchInfoRequest.queryTimeStart = dateTimeFormatter.format(now.minusMinutes(10).atOffset(ZoneOffset.ofHours(8)));
87        // 查询时间间隔不能查过7天
88        listSpInviteMchInfoRequest.queryTimeEnd = dateTimeFormatter.format(now.atOffset(ZoneOffset.ofHours(8)));
89        // 查询开始的起始位置
90        listSpInviteMchInfoRequest.offset = 0L;
91        listSpInviteMchInfoRequest.limit = 10L;
92        // 与获取邀请链接时传入的inviteCode保持一致
93        listSpInviteMchInfoRequest.inviteCode = "code_20200101_123";
94        // 只有邀请成功的商户信息,才会同步返回商户号
95        listSpInviteMchInfoRequest.mchInviteStatus = MchInviteStatus.MCH_INVITE_SUCC;
96
97        try {
98            GetSpInviteUrlResp response = getInvite.run(inviteRequest);
99
100            //将inviteUrl转换为二维码,商户通过微信扫码后,
101            //可以跳转到微信支付服务商电子发票能力开通页面,引导用户完成授权开通服务商电子发票能力。
102            System.out.println("inviteUrl: " + response.inviteUrl);
103            //通过指引https://developers.weixin.qq.com/miniprogram/dev/api/navigate/wx.navigateToMiniProgram.html,使用miniprogramAppid,miniprogramPath
104            //引导商户跳转到微信支付服务商电子发票能力开通页面,完成授权开通服务商电子发票能力。
105            System.out.println("miniprogramAppid: " + response.miniprogramAppid);
106            System.out.println("miniprogramPath: " + response.miniprogramPath);
107
108            // 等待商户操作完成开通
109            // 如果服务商不知晓邀请商户的商户号,可以通过定时任务获取邀请确认授权成功的商户的商户号信息
110            ListSpInviteMchInfoResp listSpInviteMchInfoResp = listSpInviteMchInfo.run(listSpInviteMchInfoRequest);
111            List<MchInviteResultInfo> mchInviteResultList = listSpInviteMchInfoResp.mchInviteResultList;
112            for (MchInviteResultInfo mchInviteResultInfo : mchInviteResultList) {
113                if (mchInviteResultInfo.mchInviteStatus == MchInviteStatus.MCH_INVITE_SUCC) {
114                    // 如果是邀请成功状态的商户,可以获取到商户的商户号,用于检查商户的开通授权状态
115                    String subMchid = mchInviteResultInfo.subMchid;
116
117                    CheckSubMchStatusRequest checkSubMchStatusRequest = new CheckSubMchStatusRequest();
118                    checkSubMchStatusRequest.subMchid = subMchid;
119                    
120                    // 服务商获取商户的商户号信息后,请求【检查子商户开票功能状态】(https://pay.weixin.qq.com/doc/v3/partner/4015792561)接口获取商户开通状态
121                    CheckSubMchStatusResponse checkSubMchStatusResponse = checkSubMchStatus.run(
122                            checkSubMchStatusRequest);
123                    if (subMchid.equals(checkSubMchStatusResponse.subMchid)) {
124                        ThirdMode thirdMode = checkSubMchStatusResponse.thirdMode;
125                        DigitalTaxMode digitalTaxMode = checkSubMchStatusResponse.digitalTaxMode;
126                        // 【获取邀请开通的商户信息】(https://pay.weixin.qq.com/doc/v3/partner/4015941524)邀请开通 【THIRD_OR_SELF_FAPIAO:第三方/自建发票】。检查该模式开通状态
127                        if (Objects.nonNull(thirdMode)) {
128                            ThirdModeStatus status = thirdMode.status;
129                        }
130
131                        // 获取邀请开通的商户信息】(https://pay.weixin.qq.com/doc/v3/partner/4015941524)邀请开通 【TENCENT_DIGITAL_TAX:腾讯数电发票】。检查该模式开通状态
132                        if (Objects.nonNull(digitalTaxMode)) {
133                            // 数电发票接入状态,根据【检查子商户开票功能状态】(https://pay.weixin.qq.com/doc/v3/partner/4015792561)状态描述判断用户开通的情况
134                            DigitalTaxApplyStatus digitalTaxApplyStatus = digitalTaxMode.status;
135                            List<DigitalTaxBillingPersonInfo> billingPersonInfoList = digitalTaxMode.billingPersonInfo;
136                            for (DigitalTaxBillingPersonInfo digitalTaxBillingPersonInfo : billingPersonInfoList) {
137                                // 商户在乐企平台上设置了开票员信息,才会返回对应开票员的开票员ID,用于开具数电发票接口,请求入参
138                                String billingPersonId = digitalTaxBillingPersonInfo.id;
139                            }
140                            // 商户完成所有操作后,完成接入腾讯数电后的时间
141                            String accessTime = digitalTaxMode.accessTime;
142                            String expiredTime = digitalTaxMode.expiredTime;
143                            String accessFailReason = digitalTaxMode.accessFailReason;
144                            List<DigitalTaxAbilityInfo> abilityInfo = digitalTaxMode.abilityInfo;
145                        }
146                    }
147                } else {
148                    // 如果是邀请失败状态的商户,获取商户的商户名称,以及邀请开通失败的原因。
149                    // 根据开通小程序页面下的帮助指引,指导商户解决问题重试开通授权流程
150                    String epName = mchInviteResultInfo.epName;
151                    String inviteFailedReason = mchInviteResultInfo.inviteFailedReason;
152                }
153            }
154        } catch (WXPayUtility.ApiException e) {
155            // 处理异常处理逻辑
156            if (e.getErrorCode().equals("SYSTEM_ERROR")) {
157                // 错误:系统错误
158                // 解决方式:稍后重试
159                // 描述:微信支付系统失败,建议等1分钟后再重试
160            } else if (e.getErrorCode().equals("FREQUENCY_LIMITED")) {
161                // 错误:限频报错
162                // 解决方式:稍后重试
163                // 描述: 接口频率限制,一定要原单重试,建议等1分钟后再重试
164            } else if (e.getErrorCode().equals("ERROR")) {
165                // 错误:系统错误
166                // 解决方式:系统繁忙,请稍后重试
167                // 描述:微信支付系统失败,建议等1分钟后再重试
168            } else if (e.getErrorCode().equals("NO_AUTH")) {
169                // 错误:服务商无权限
170                // 解决方式:根据具体message,查看指引解决问题
171                // 描述:
172                // - 商户无权限访问,请确认是否已开通相关权限
173                // - 服务商无电子发票权限,请联系支持开通服务商电子发票
174                // - 乐企数电安全校验时间已过期,请登录电子税局重新设置数电安全校验时间
175            } else if (e.getErrorCode().equals("SIGN_ERROR")) {
176                // 错误:签名错误
177                // 解决方式:检查平台商户证书序列号,证书私钥文件,公钥ID,公钥文件,同时确认下签名过程是不是按照微信支付的签名方式
178                // 描述:签名报错,需要确认签名材料和签名流程是否正确
179            } else if (e.getErrorCode().equals("PARAM_ERROR")) {
180                // 错误:参数错误
181                // 解决方式:按照报错返回的message,重新输入请求参数
182                // 描述:参数的类型,长度,或者必填选项没有填写等
183            } else if (e.getErrorCode().equals("INVALID_REQUEST")) {
184                // 错误:请求非法,请求参数正确,但是不符合业务规则
185                // 解决方式:根据具体message查看具体哪里不符合业务规则,如果可以修改参数达到符合业务规则的修改请求符合业务规则之后重试
186                // 描述:不符合业务规则的场景如:
187                // - 查询起止时间间隔不能大于7天
188                // - 查询起始时间不能大于查询结束时间
189            } else {
190                // 其他类型错误:稍等一会后原单重试
191            }
192        }
193    }
194}

2.1.3 商户操作

商户扫码生成的二维码或者点击按钮跳转到指定的小程序后,商户通过填写对应的微信支付商户号或者在商户交易的微信支付交易订单号后(如邀请填写了商户号,则不需要商户填写),即可按照指引进行微信支付电子发票能力的授权与开通

法人扫脸

非法人扫脸

数电发票接入状态状态机

2.2 设置商户开票配置

2.2.1 业务示例代码

商户完成接入微信支付电子发票能力后,需要通过此流程,完成通知回调地址、是否支持开具专用发票等开票配置。

回调地址 by 服务商维度设置,设置一次即可。账单是否展示开发票入口,是否开专票 by 商户号设置。

1package com.java.partner;
2
3import com.java.demo.CreateFapiaoCardTemplate;  // https://pay.weixin.qq.com/doc/v3/partner/4015792562
4import com.java.demo.CreateFapiaoCardTemplate.CardTemplateInfo;
5import com.java.demo.CreateFapiaoCardTemplate.CreateFapiaoCardTemplateRequest;
6import com.java.demo.CreateFapiaoCardTemplate.CustomCell;
7import com.java.demo.CreateFapiaoCardTemplate.FapiaoCardTemplateEntity;
8import com.java.demo.UpdateDevelopmentConfig; // https://pay.weixin.qq.com/doc/v3/partner/4015792563
9import com.java.demo.UpdateDevelopmentConfig.DevelopmentConfigEntity;
10import com.java.demo.UpdateDevelopmentConfig.UpdateDevelopmentConfigRequest;
11import com.java.utils.WXPayUtility;
12
13public class SetMerchantInvoiceConfigDemo {
14
15    public static void main(String[] args) {
16        // TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/partner/4013080340
17        CreateFapiaoCardTemplate createFapiaoCardTemplate = new CreateFapiaoCardTemplate(
18                "19xxxxxxxx",
19                // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
20                "1DDE55AD98Exxxxxxxxxx",
21                // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
22                "/path/to/apiclient_key.pem",     // 商户API证书私钥文件路径,本地文件路径
23                "PUB_KEY_ID_xxxxxxxxxxxxx",      // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
24                "/path/to/wxp_pub.pem"           // 微信支付公钥文件路径,本地文件路径
25        );
26
27        UpdateDevelopmentConfig updateDevelopmentConfig = new UpdateDevelopmentConfig(
28                "19xxxxxxxx",
29                // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
30                "1DDE55AD98Exxxxxxxxxx",
31                // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
32                "/path/to/apiclient_key.pem",     // 商户API证书私钥文件路径,本地文件路径
33                "PUB_KEY_ID_xxxxxxxxxxxxx",      // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
34                "/path/to/wxp_pub.pem"           // 微信支付公钥文件路径,本地文件路径
35        );
36
37        CreateFapiaoCardTemplateRequest createFapiaoCardTemplateRequest = new CreateFapiaoCardTemplateRequest();
38        createFapiaoCardTemplateRequest.subMchid = "1900000109";
39        createFapiaoCardTemplateRequest.cardAppid = "wxb1170446a4c0a5a2";
40        createFapiaoCardTemplateRequest.cardTemplateInformation = new CardTemplateInfo();
41        // 具体字段展示对应卡包展示内容在代码后有示例。
42        createFapiaoCardTemplateRequest.cardTemplateInformation.payeeName = "某公司";
43        createFapiaoCardTemplateRequest.cardTemplateInformation.logoUrl = "http://mmbiz.qpic.cn/mmbiz/iaL1LJM1mF9aRKPZJkmG8xXhiaHqkKSVMMWeN3hLut7X7hicFNjakmxibMLGWpXrEXB33367o7zHN0CwngnQY7zb7g/0";
44        createFapiaoCardTemplateRequest.cardTemplateInformation.customCell = new CustomCell();
45        createFapiaoCardTemplateRequest.cardTemplateInformation.customCell.words = "电子发票";
46        createFapiaoCardTemplateRequest.cardTemplateInformation.customCell.description = "查看发票";
47        // 点击卡券详情页自定义cell位后跳转的页面链接,用户跳转时会自动在链接后加上参数encrypt_code与card_id,商户需要调用code解码接口解码encrypt_code得到真实的code
48        // https://developers.weixin.qq.com/doc/service/guide/product/card/Redeeming_a_coupon_voucher_or_card.html#5 2.2节 Code解码接口 解码encrypt_code
49        // 通过【查询电子发票】接口,获取card_information.card_code 与 解密的Code进行比对,确认是否为该商户的电子发票。
50        createFapiaoCardTemplateRequest.cardTemplateInformation.customCell.jumpUrl = "http://www.qq.com";
51        createFapiaoCardTemplateRequest.cardTemplateInformation.customCell.miniprogramUserName = "gh_86a091e50ad4@app";
52        createFapiaoCardTemplateRequest.cardTemplateInformation.customCell.miniprogramPath = "pages/xxxPage";
53
54        UpdateDevelopmentConfigRequest updateDevelopmentConfigRequest = new UpdateDevelopmentConfigRequest();
55        // 设置服务商的可以接受回调请求的地址,回调设置会校验接口是否能够正确返回http code 200,如果无法返回200,会返回错误。
56        updateDevelopmentConfigRequest.callbackUrl = "https://pay.weixin.qq.com/callback";
57        updateDevelopmentConfigRequest.subMchCode = "1900000109";
58        updateDevelopmentConfigRequest.showFapiaoCell = false;
59        updateDevelopmentConfigRequest.supportVatFapiao = false;
60
61        try {
62            FapiaoCardTemplateEntity fapiaoCardTemplateEntity = createFapiaoCardTemplate.run(
63                    createFapiaoCardTemplateRequest);
64            System.out.println("card_appid:" + fapiaoCardTemplateEntity.cardAppid);
65            // 用于请求申请开具电子发票时,卡券模板ID参数
66            System.out.println("card_id:" + fapiaoCardTemplateEntity.cardId);
67
68            DevelopmentConfigEntity developmentConfigEntity = updateDevelopmentConfig.run(
69                    updateDevelopmentConfigRequest);
70            // 服务商申请接受回调请求的地址
71            System.out.println("callback_url:" + developmentConfigEntity.callbackUrl);
72            // 属于商户的微信支付账单凭证,是否展示开发票入口
73            System.out.println("show_fapiao_cell:" + developmentConfigEntity.showFapiaoCell);
74            // 商户是否支持开具增值税专用发票。如果设置为ture,针对商户所有的开票申请,填写抬头页面是否展示选择电子发票类型选项
75            System.out.println("support_vat_fapiao:" + developmentConfigEntity.supportVatFapiao);
76        } catch (WXPayUtility.ApiException e) {
77            // 处理异常处理逻辑
78            if (e.getErrorCode().equals("SYSTEM_ERROR")) {
79                // 错误:系统错误
80                // 解决方式:稍后重试
81                // 描述:微信支付系统失败,建议等1分钟后再重试
82            } else if (e.getErrorCode().equals("FREQUENCY_LIMITED")) {
83                // 错误:限频报错
84                // 解决方式:稍后重试
85                // 描述: 接口频率限制,一定要原单重试,建议等1分钟后再重试
86            } else if (e.getErrorCode().equals("ERROR")) {
87                // 错误:系统错误
88                // 解决方式:系统繁忙,请稍后重试
89                // 描述:微信支付系统失败,建议等1分钟后再重试
90            } else if (e.getErrorCode().equals("NO_AUTH")) {
91                // 错误:服务商无权限
92                // 解决方式:开通微信支付电子发票服务商权限,并且检查商户是否接受邀请开通授权微信支付电子发票能力,等1分钟后重试。
93                // 描述:商户无权限访问,请确认是否已开通相关权限
94            } else if (e.getErrorCode().equals("SIGN_ERROR")) {
95                // 错误:签名错误
96                // 解决方式:检查平台商户证书序列号,证书私钥文件,公钥ID,公钥文件,同时确认下签名过程是不是按照微信支付的签名方式
97                // 描述:签名报错,需要确认签名材料和签名流程是否正确
98            } else if (e.getErrorCode().equals("PARAM_ERROR")) {
99                // 错误:参数错误
100                // 解决方式:按照报错返回的message,重新输入请求参数
101                // 描述:参数的类型,长度,或者必填选项没有填写等
102            } else if (e.getErrorCode().equals("INVALID_REQUEST")) {
103                // 错误:请求非法,请求参数正确,但是不符合业务规则
104                // 解决方式:根据具体message查看具体哪里不符合业务规则,如果可以修改参数达到符合业务规则的修改请求符合业务规则之后重试
105                // 描述:不符合业务规则的场景如:
106                // - 创建发票卡券模板失败,请稍候重试
107                // - 传入的参数“点击cell位跳转的地址”以及“点击cell位跳转的小程序的用户名/页面路径”均为空(位于/body/card_template_info/custom_cell/jump_url|miniprogram_user_name|miniprogram_page_path),
108              	//   请检查参数格式是否符合要求
109            } else {
110                // 其他类型错误:稍等一会后原单重试
111            }
112        }
113    }
114}

设置商户支持开具专用发票后,用户填写抬头信息页面会支持选择开具的发票类型,服务商可以通过获取用户填写抬头信息获取到用户提交的申请开具发票的类型。

2.3 电子发票状态状态机

2.4 通过微信支付交易凭证开票

2.4.1 业务描述

通过微信支付交易凭证,实现用户主动申请开具微信支付电子发票并插卡,以及用户未授权商户主动申请开具微信支付电子发票并插卡。

2.4.2 用户操作

2.4.3 处理用户填写抬头通知

用户提交开票申请后,如果通过【配置开发选项】接口,成功设置了回调地址,会接受到此次用户提交对应的填写抬头通知。

1package com.tencent.wxpay.fapiao;
2
3import com.google.gson.annotations.SerializedName;
4import com.tencent.wxpay.utils.WXPayUtility;
5import com.tencent.wxpay.utils.WXPayUtility.Notification;
6import java.security.PublicKey;
7import okhttp3.Headers;
8import okhttp3.Response;
9
10public class UserTitleNotifyDemo {
11    // TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/partner/4013080340
12    // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考
13    // https://pay.weixin.qq.com/doc/v3/merchant/4013070756
14    private static String mchid = "19xxxxxxxx";
15    // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
16    private static String certificateSerialNo = "1DDE55AD98Exxxxxxxxxx";
17    // 商户API证书私钥文件路径,本地文件路径
18    private static String privateKeyFilePath = "/path/to/apiclient_key.pem";
19    // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
20    private static String wechatPayPublicKeyId = "PUB_KEY_ID_xxxxxxxxxxxxx";
21    // 微信支付公钥文件路径,本地文件路径
22    private static String wechatPayPublicKeyFilePath = "/path/to/wxp_pub.pem";
23    // 商户APIv3密钥,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
24    private static String apiv3Key = "your apiv3 key";
25
26    public static class UserTitle {
27        @SerializedName("mchid")
28        public String mchid;
29
30        @SerializedName("sub_mchid")
31        public String subMchId;
32
33        @SerializedName("fapiao_apply_id")
34        public String fapiaoApplyId;
35
36        @SerializedName("apply_time")
37        public String applyTime;
38    }
39
40    public void userTitleNotify(Response httpResponse) {
41        String refundNotifyStr = WXPayUtility.extractBody(httpResponse); // 用户填写抬头通知的原始字符串
42        Headers headers = httpResponse.headers(); // 用户填写抬头通知的Headers
43        PublicKey wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
44
45        Notification notification = WXPayUtility.parseNotification(
46                apiv3Key,
47                wechatPayPublicKeyId, wechatPayPublicKey, headers,
48                refundNotifyStr);
49
50        // 解析用户填写抬头信息
51        UserTitle userTitle = WXPayUtility.fromJson(notification.getResource().getCiphertext(), UserTitle.class);
52
53        String notifyMchId = userTitle.mchid;
54        if (mchid.equals(notifyMchId)) {
55            // 用于服务商获取用户填写抬头的具体信息
56            String subMchId = userTitle.subMchId;
57            String fapiaoApplyId = userTitle.fapiaoApplyId;
58        }
59    }
60}

2.4.4 业务示例代码

1package com.java.partner;
2
3import com.java.demo.GetFapiaoApplications;  // https://pay.weixin.qq.com/doc/v3/partner/4015792567
4import com.java.demo.GetFapiaoApplications.FapiaoApplicationsEntity;
5import com.java.demo.GetFapiaoApplications.FapiaoEntity;
6import com.java.demo.GetFapiaoApplications.FapiaoInfo;
7import com.java.demo.GetFapiaoApplications.GetFapiaoApplicationsRequest;
8import com.java.demo.GetFapiaoFileDownloadInfo;  // https://pay.weixin.qq.com/doc/v3/partner/4015792576
9import com.java.demo.GetFapiaoFileDownloadInfo.FapiaoDownloadInfo;
10import com.java.demo.GetFapiaoFileDownloadInfo.GetFapiaoFileDownloadInfoRequest;
11import com.java.demo.GetFapiaoFileDownloadInfo.GetFapiaoFileDownloadInfoResponse;
12import com.java.demo.GetUserTitle; // https://pay.weixin.qq.com/doc/v3/partner/4015784260
13import com.java.demo.GetUserTitle.BuyerType;
14import com.java.demo.GetUserTitle.GetUserTitleRequest;
15import com.java.demo.GetUserTitle.Scene;
16import com.java.demo.GetUserTitle.UserTitleEntity;
17import com.java.demo.IssueGeneralIndustryFapiaoApplications; // https://pay.weixin.qq.com/doc/v3/partner/4015792574
18import com.java.demo.IssueGeneralIndustryFapiaoApplications.FapiaoBillType;
19import com.java.demo.IssueGeneralIndustryFapiaoApplications.IssueGeneralFapiaoInfo;
20import com.java.demo.IssueGeneralIndustryFapiaoApplications.IssueGeneralFapiaoItem;
21import com.java.demo.IssueGeneralIndustryFapiaoApplications.IssueGeneralFapiaoRequest;
22import com.java.demo.IssueGeneralIndustryFapiaoApplications.PayChannel;
23import com.java.demo.IssueGeneralIndustryFapiaoApplications.TransactionInfo;
24import com.java.utils.WXPayUtility.ApiException;
25import java.util.ArrayList;
26import java.util.Objects;
27
28public class IssueByTransactionVoucherDemo {
29
30    public static void main(String[] args) {
31        // TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/partner/4013080340
32        GetUserTitle getUserTitle = new GetUserTitle(
33                "19xxxxxxxx",
34                // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
35                "1DDE55AD98Exxxxxxxxxx",
36                // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
37                "/path/to/apiclient_key.pem",     // 商户API证书私钥文件路径,本地文件路径
38                "PUB_KEY_ID_xxxxxxxxxxxxx",      // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
39                "/path/to/wxp_pub.pem"           // 微信支付公钥文件路径,本地文件路径
40        );
41
42        IssueGeneralIndustryFapiaoApplications issueGeneralIndustryFapiaoApplications = new IssueGeneralIndustryFapiaoApplications(
43                "19xxxxxxxx",
44                // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
45                "1DDE55AD98Exxxxxxxxxx",
46                // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
47                "/path/to/apiclient_key.pem",     // 商户API证书私钥文件路径,本地文件路径
48                "PUB_KEY_ID_xxxxxxxxxxxxx",      // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
49                "/path/to/wxp_pub.pem"           // 微信支付公钥文件路径,本地文件路径
50        );
51
52        GetFapiaoApplications getFapiaoApplications = new GetFapiaoApplications(
53                "19xxxxxxxx",
54                // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
55                "1DDE55AD98Exxxxxxxxxx",
56                // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
57                "/path/to/apiclient_key.pem",     // 商户API证书私钥文件路径,本地文件路径
58                "PUB_KEY_ID_xxxxxxxxxxxxx",      // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
59                "/path/to/wxp_pub.pem"           // 微信支付公钥文件路径,本地文件路径
60        );
61
62        GetFapiaoFileDownloadInfo client = new GetFapiaoFileDownloadInfo(
63                "19xxxxxxxx",
64                // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
65                "1DDE55AD98Exxxxxxxxxx",
66                // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
67                "/path/to/apiclient_key.pem",     // 商户API证书私钥文件路径,本地文件路径
68                "PUB_KEY_ID_xxxxxxxxxxxxx",      // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
69                "/path/to/wxp_pub.pem"           // 微信支付公钥文件路径,本地文件路径
70        );
71
72        GetUserTitleRequest getUserTitleRequest = new GetUserTitleRequest();
73        getUserTitleRequest.subMchid = "1900000109";
74        getUserTitleRequest.scene = Scene.WITH_WECHATPAY;
75        // 通过处理用户抬头填写完成回调通知,获取此次申请的申请单号,交易凭证开票默认申请单号为此次交易的交易单号
76        getUserTitleRequest.fapiaoApplyId = "4200000444201910177461284488";
77
78        IssueGeneralFapiaoRequest issueGeneralFapiaoRequest = new IssueGeneralFapiaoRequest();
79        issueGeneralFapiaoRequest.subMchid = "1900000109";
80        issueGeneralFapiaoRequest.fapiaoApplyId = getUserTitleRequest.fapiaoApplyId;
81        issueGeneralFapiaoRequest.buyerInformation = new IssueGeneralIndustryFapiaoApplications.UserTitleEntity();
82
83        issueGeneralFapiaoRequest.fapiaoInformation = new IssueGeneralFapiaoInfo();
84        // 此次申请开票下,申请开具的一张发票的唯一标识,如果需要红冲后换开发票、开票失败后重开发票,使用相同的fapiao_apply_id,更换不同的fapiao_id即可
85        issueGeneralFapiaoRequest.fapiaoInformation.fapiaoId = "20200701123456";
86        // 申请开票的金额,支持小于本次申请开票对应的所有交易信息的交易金额之和
87        issueGeneralFapiaoRequest.fapiaoInformation.totalAmount = 1123L;
88        issueGeneralFapiaoRequest.fapiaoInformation.items = new ArrayList<>();
89        {
90            IssueGeneralFapiaoItem itemsItem = new IssueGeneralFapiaoItem();
91            itemsItem.taxCode = "3010101020203000000";
92            itemsItem.goodsName = "*xxx*服务";
93            itemsItem.specification = "米";
94            itemsItem.unit = "米";
95            itemsItem.quantity = 100000000L;
96            itemsItem.totalAmount = 429900L;
97            itemsItem.taxRate = 1300L;
98            itemsItem.discount = false;
99            itemsItem.preferentialPolicyCode = 1L;
100            issueGeneralFapiaoRequest.fapiaoInformation.items.add(itemsItem);
101        }
102        ;
103        issueGeneralFapiaoRequest.fapiaoInformation.exportBusinessPolicyCode = 1L;
104        issueGeneralFapiaoRequest.fapiaoInformation.vatRefundLevyCode = 1L;
105        issueGeneralFapiaoRequest.fapiaoInformation.billingPersonId = "128279891283";
106        issueGeneralFapiaoRequest.fapiaoInformation.billingPerson = "example_billing_person";
107        issueGeneralFapiaoRequest.fapiaoInformation.fapiaoBillType = FapiaoBillType.COMM_FAPIAO;
108
109        issueGeneralFapiaoRequest.fapiaoInformation.remark = "备注";
110
111        GetFapiaoApplicationsRequest getFapiaoApplicationsRequest = new GetFapiaoApplicationsRequest();
112        getFapiaoApplicationsRequest.fapiaoApplyId = getUserTitleRequest.fapiaoApplyId;
113        getFapiaoApplicationsRequest.subMchid = "1900000109";
114        getFapiaoApplicationsRequest.fapiaoId = issueGeneralFapiaoRequest.fapiaoInformation.fapiaoId;
115
116        GetFapiaoFileDownloadInfoRequest getFapiaoFileDownloadInfoRequest = new GetFapiaoFileDownloadInfoRequest();
117        getFapiaoFileDownloadInfoRequest.fapiaoApplyId = getUserTitleRequest.fapiaoApplyId;
118        getFapiaoFileDownloadInfoRequest.subMchid = "1900000109";
119        getFapiaoFileDownloadInfoRequest.fapiaoId = issueGeneralFapiaoRequest.fapiaoInformation.fapiaoId;
120
121        try {
122            // 获取用户填写的抬头信息,用于申请开票接口。如果返回的字段内容为空,则开票接口不用传
123            UserTitleEntity userTitleEntity = getUserTitle.run(getUserTitleRequest);
124            // 申请开票的用户,提交的留言信息
125            System.out.println("userApplyMessage:" + userTitleEntity.userApplyMessage);
126            if (userTitleEntity.type.equals(BuyerType.INDIVIDUAL)) {
127                issueGeneralFapiaoRequest.buyerInformation.type = IssueGeneralIndustryFapiaoApplications.BuyerType.INDIVIDUAL;
128            } else {
129                issueGeneralFapiaoRequest.buyerInformation.type = IssueGeneralIndustryFapiaoApplications.BuyerType.ORGANIZATION;
130            }
131            issueGeneralFapiaoRequest.buyerInformation.name = userTitleEntity.name;
132            issueGeneralFapiaoRequest.buyerInformation.taxpayerId = userTitleEntity.taxpayerId;
133            issueGeneralFapiaoRequest.buyerInformation.address = userTitleEntity.address;
134            issueGeneralFapiaoRequest.buyerInformation.telephone = userTitleEntity.telephone;
135            issueGeneralFapiaoRequest.buyerInformation.bankName = userTitleEntity.bankName;
136            issueGeneralFapiaoRequest.buyerInformation.bankAccount = userTitleEntity.bankAccount;
137            issueGeneralFapiaoRequest.buyerInformation.phone = issueGeneralIndustryFapiaoApplications.encrypt(
138                    userTitleEntity.phone);
139            issueGeneralFapiaoRequest.buyerInformation.email = issueGeneralIndustryFapiaoApplications.encrypt(
140                    userTitleEntity.email);
141            // 返回当前交易信息对应的交易金额
142            issueGeneralFapiaoRequest.buyerInformation.amount = userTitleEntity.amount;
143            if (userTitleEntity.fapiaoBillType.equals(GetUserTitle.FapiaoBillType.COMM_FAPIAO)) {
144                issueGeneralFapiaoRequest.buyerInformation.fapiaoBillType = FapiaoBillType.COMM_FAPIAO;
145            } else {
146                issueGeneralFapiaoRequest.buyerInformation.fapiaoBillType = FapiaoBillType.VAT_FAPIAO;
147            }
148
149            issueGeneralFapiaoRequest.fapiaoInformation.transactionInformation = new ArrayList<>();
150            {
151                TransactionInfo transactionInformationItem = new TransactionInfo();
152                transactionInformationItem.payChannel = PayChannel.WECHAT_PAY;
153                transactionInformationItem.transactionId = getUserTitleRequest.fapiaoApplyId;
154                transactionInformationItem.amount = userTitleEntity.amount;
155                issueGeneralFapiaoRequest.fapiaoInformation.transactionInformation.add(transactionInformationItem);
156            }
157          	;
158
159            // 服务商通过匹配交易的商品、服务内容等信息,根据税局的要求,自行选择需要调用的行业开票接口。本示例以通用行业接口为例
160            // 无返回内容 http code202即为请求成功
161            issueGeneralIndustryFapiaoApplications.run(issueGeneralFapiaoRequest);
162
163            // 申请开具通用行业数电发票
164            FapiaoApplicationsEntity fapiaoApplicationsEntity = getFapiaoApplications.run(getFapiaoApplicationsRequest);
165            String fapiaoErrorCode = "";
166            String fapiaoErrorMessage = "";
167            for (FapiaoEntity fapiaoEntity : fapiaoApplicationsEntity.fapiaoInformation) {
168                switch (fapiaoEntity.status) {
169                    case ISSUED:
170                        // 发票开具成功,可以获取蓝票信息
171                        FapiaoInfo fapiaoInfo = fapiaoEntity.blueFapiao;
172                        break;
173                    case REVERSED:
174                        // 发票已红冲,可以获取红票信息
175                        FapiaoInfo redFapiaoInfo = fapiaoEntity.redFapiao;
176                        break;
177                    case ISSUE_ACCEPTED:
178                        // 发票开票中,等待一段时间后,重试查询电子发票
179                        break;
180                    case REVERSE_ACCEPTED:
181                        // 发票红冲中,等待一段时间后,重试查询电子发票
182                        break;
183                    case ISSUE_FAILED:
184                        // 蓝票开具失败,根据错误原因,解决问题后,换fapiao_id 进行重试开票
185                        // 常见的蓝票开具失败原因如:
186                        // - 开票参数不符合。税收编码[ 1030201000000000000 ]为汇总项,不可开具,请修改后再试。
187                        // - 开票失败乐企发票开具失败:他用或联用企业开票人未授权开票,请修改后再试。或联系主管税务机关。
188                        // - 开票预扣授信额度失败,授信额度不足本次开票金额:9.71;剩余可用额度:0.00 —— 在税局重新申请额度后,换fapiao_id 重试
189                        fapiaoErrorCode = fapiaoEntity.fapiaoErrorCode;
190                        fapiaoErrorMessage = fapiaoEntity.fapiaoErrorMessage;
191                        break;
192                    case REVERSE_FAILED:
193                        // 蓝票开具失败,根据错误原因,解决问题后,直接进行重试红冲
194                        // 常见的红票开具失败原因如:
195                        // - 该发票存在进行中的红字确认单 —— 电子税局已存在处理中的红字确认单,商户登录电子税局继续红冲,或者撤销红字确认单后,再重试请求红茶
196                        fapiaoErrorCode = fapiaoEntity.fapiaoErrorCode;
197                        fapiaoErrorMessage = fapiaoEntity.fapiaoErrorMessage;
198                        break;
199                    default:
200                        break;
201                }
202            }
203            System.out.println("fapiaoErrorCode:" + fapiaoErrorCode);
204            System.out.println("fapiaoErrorMessage:" + fapiaoErrorMessage);
205
206            // 获取电子发票下载信息
207            GetFapiaoFileDownloadInfoResponse getFapiaoFileDownloadInfoResponse = client.run(
208                    getFapiaoFileDownloadInfoRequest);
209            for (FapiaoDownloadInfo fapiaoDownloadInfo : getFapiaoFileDownloadInfoResponse.fapiaoDownloadInfoList) {
210                if (Objects.equals(getFapiaoFileDownloadInfoRequest.fapiaoId, fapiaoDownloadInfo.fapiaoId)) {
211                    // 根据【下载发票文件】(https://pay.weixin.qq.com/doc/v3/partner/4015792569)指引,添加参数到downloadUrl,通过GET请求downloadUrl即可获取发票文件流
212                    String downloadUrl = fapiaoDownloadInfo.downloadUrl;
213                }
214            }
215        } catch (ApiException e) {
216            // 异常处理逻辑
217            if (e.getErrorCode().equals("SYSTEM_ERROR")) {
218                // 错误:系统错误
219                // 解决方式:稍后重试
220                // 描述:微信支付系统失败,建议等1分钟后再重试
221            } else if (e.getErrorCode().equals("FREQUENCY_LIMITED")) {
222                // 错误:限频报错
223                // 解决方式:稍后重试
224                // 描述: 接口频率限制,一定要原单重试,建议等1分钟后再重试
225            } else if (e.getErrorCode().equals("ERROR")) {
226                // 错误:系统错误
227                // 解决方式:系统繁忙,请稍后重试
228                // 描述:微信支付系统失败,建议等1分钟后再重试
229            } else if (e.getErrorCode().equals("NO_AUTH")) {
230                // 错误:服务商无权限
231                // 解决方式:根据具体message查看具体没有什么权限
232                // 描述:无权限的场景如:
233                // - 检查服务商商户号是否开通微信支付电子发票服务商权限,检查商户是否接受邀请开通授权微信支付电子发票能力,等待1分钟后重试
234                // - 开票申请单不是当前服务商创建,无权限进行当前操作
235                // - 商户乐企发票能力未授权,请授权相关发票能力 —— 确认通过【获取邀请商户开通服务商电子发票能力】(https://pay.weixin.qq.com/doc/v3/partner/4015941495)邀请追加能力开通,并且商户在电子税局乐企系统上完成授权
236                // - 商户未开通数电发票能力,请开通数电发票 —— 确认通过【获取邀请商户开通服务商电子发票能力】(https://pay.weixin.qq.com/doc/v3/partner/4015941495)邀请能力开通,并且商户在电子税局乐企系统上完成授权
237            } else if (e.getErrorCode().equals("SIGN_ERROR")) {
238                // 错误:签名错误
239                // 解决方式:检查平台商户证书序列号,证书私钥文件,公钥ID,公钥文件,同时确认下签名过程是不是按照微信支付的签名方式
240                // 描述:签名报错,需要确认签名材料和签名流程是否正确
241            } else if (e.getErrorCode().equals("PARAM_ERROR")) {
242                // 错误:参数错误
243                // 解决方式:按照报错返回的message,重新输入请求参数
244                // 描述:参数的类型,长度,或者必填选项没有填写等
245            } else if (e.getErrorCode().equals("RESOURCE_NOT_EXISTS")) {
246                // 错误:资源错误
247                // 解决方式:请检查发票申请单号是否正确,或在确认用户已提交抬头后重新调用
248                // 描述:根据发票申请单查询不到用户填写的抬头
249            } else if (e.getErrorCode().equals("RULE_LIMIT")) {
250                // 错误:条件限制
251                // 解决方式:按照报错返回的message接引操作后,重新调用
252                // 描述:需要按照指引重新处理的情况如:
253                // - 当前开票场景与发票申请单来源不匹配,请确认参数中的“开票场景”与“发票申请单号”是否符合要求。确认参数中的“开票场景”与“发票申请单号”是否对应
254                // - 商户尚未配置电子发票卡券模版信息,请先调用【创建电子发票卡券模板】(https://pay.weixin.qq.com/doc/v3/partner/4015792562)接口成功后再重新调用
255                // - 商户提交的购买方信息与用户填写的发票抬头不一致,请检查参数中的购买方信息与【获取用户填写抬头信息】(https://pay.weixin.qq.com/doc/v3/partner/4015784260)接口返回的发票抬头信息是否一致
256                // - 开票总金额不能超过发票申请单的总金额,请检查参数中各张发票的“总价税合计”,若是微信支付场景,不能超过微信支付订单的金额,若是非微信支付场景,
257                //   不能超过【获取用户抬头填写链接】(https://pay.weixin.qq.com/doc/v3/partner/4015770776)接口中指定的总金额
258            } else if (e.getErrorCode().equals("ORDER_CLOSED")) {
259                // 错误:订单错误
260                // 解决方式:联系商户,重新申请开具发票
261                // 描述:开票申请已被商户拒绝,无法进行当前操作
262            } else if (e.getErrorCode().equals("RESOURCE_ALREADY_EXISTS")) {
263                // 错误:资源已存在
264                // 解决方式:调用【查询电子发票】接口获取开票结果
265                // 描述:开票请求已被受理,请稍后调用【查询电子发票】接口获取开票结果
266            } else if (e.getErrorCode().equals("INVALID_REQUEST")) {
267                // 错误:请求非法,请求参数正确,但是不符合业务规则
268                // 解决方式:根据具体message查看具体哪里不符合业务规则,如果可以修改参数达到符合业务规则的修改请求符合业务规则之后重试
269                // 描述:不符合业务规则的场景如:
270                // - 交易信息中,交易金额必须大于0
271                // - 交易渠道非微信支付时,交易商户订单号必填
272                // - 微信支付交易信息金额错误
273                // - 多个微信支付渠道交易合并开票时,必须是同一个用户的所属交易账单
274                // - 交易单号在支付宝渠道下非法
275                // - 发票申请单申请开票的总金额,不能超过交易信息的总金额
276                // - 非折扣行金额必须为正数,请使用正确的参数重新调用
277                // - 总价税合计与所有发票行单行金额的累加和不相等,请使用正确的参数重新调用
278                // - 折扣行不能作为首行
279                // - 第{}行是折扣行,数电模式下,“单位”,“规格”必须为空,请使用正确的参数重新调用
280                // - 第{}行是折扣行,数电模式下,“货物或应税劳务、服务名称”,“税率”,“税收分类编码”,“优惠政策标识” 必须和被折扣行相同
281                // - 折扣行必须在被折扣行的下一行,请使用正确的参数重新调用
282                // - 折扣行金额必须为负数,请使用正确的参数重新调用
283                // - 折扣行金额绝对值不能超过被折扣行金额,请使用正确的参数重新调用
284                // - 折扣行的数量值非法,请使用正确的参数重新调用
285                // - 当前发票开具中,不可申请重开发票
286                // - 当前发票红冲中,不可申请重开发票
287                // - 申请开票次数,超过最大限制
288                // - 开票请求已被受理,请调用【查询电子发票】(https://pay.weixin.qq.com/doc/v3/partner/4015792567)接口获取开票结果
289                // - 交易单已申请开票,两次申请的交易金额不相同,修改交易金额重试
290                // - 交易可剩余开票金额不足,修改交易订单或者开票金额重试
291                // - 仅在微信支付侧开具的电子发票才允许下载发票文件
292            } else {
293                // 其他类型错误:联系获取支持
294            }
295        }
296    }
297}
298

2.4.5 处理开票成功通知

1package com.tencent.wxpay.fapiao;
2
3import com.google.gson.annotations.SerializedName;
4import com.tencent.wxpay.utils.WXPayUtility;
5import com.tencent.wxpay.utils.WXPayUtility.Notification;
6import java.security.PublicKey;
7import java.util.List;
8import okhttp3.Headers;
9import okhttp3.Response;
10
11public class FapiaoIssuedNotifyDemo {
12    // TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/partner/4013080340
13    // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考
14    // https://pay.weixin.qq.com/doc/v3/merchant/4013070756
15    private static String mchid = "19xxxxxxxx";
16    // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
17    private static String certificateSerialNo = "1DDE55AD98Exxxxxxxxxx";
18    // 商户API证书私钥文件路径,本地文件路径
19    private static String privateKeyFilePath = "/path/to/apiclient_key.pem";
20    // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
21    private static String wechatPayPublicKeyId = "PUB_KEY_ID_xxxxxxxxxxxxx";
22    // 微信支付公钥文件路径,本地文件路径
23    private static String wechatPayPublicKeyFilePath = "/path/to/wxp_pub.pem";
24    // 商户APIv3密钥,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
25    private static String apiv3Key = "your apiv3 key";
26
27    public static class FapiaoIssued {
28        @SerializedName("mchid")
29        public String mchid;
30
31        @SerializedName("sub_mchid")
32        public String subMchId;
33
34        @SerializedName("fapiao_apply_id")
35        public String fapiaoApplyId;
36        
37        @SerializedName("fapiao_information")
38        public List<FapiaoInfo> fapiaoInformation;
39    }
40    
41    public static class FapiaoInfo {
42        @SerializedName("fapiao_id")
43        public String fapiaoId;
44        
45        @SerializedName("fapiao_status")
46        public Status fapiaoStatus;
47        
48        @SerializedName("card_status")
49        public CardStatus cardStatus;
50    }
51
52    public enum Status {
53        @SerializedName("ISSUE_ACCEPTED")
54        ISSUE_ACCEPTED,
55        @SerializedName("ISSUED")
56        ISSUED,
57        @SerializedName("REVERSE_ACCEPTED")
58        REVERSE_ACCEPTED,
59        @SerializedName("REVERSED")
60        REVERSED,
61    }
62
63    public enum CardStatus {
64        @SerializedName("INSERT_ACCEPTED")
65        INSERT_ACCEPTED,
66        @SerializedName("INSERTED")
67        INSERTED,
68        @SerializedName("DISCARD_ACCEPTED")
69        DISCARD_ACCEPTED,
70        @SerializedName("DISCARDED")
71        DISCARDED
72    }
73
74    public void FapiaoIssuedNotify(Response httpResponse) {
75        String refundNotifyStr = WXPayUtility.extractBody(httpResponse); // 用户填写抬头通知的原始字符串
76        Headers headers = httpResponse.headers(); // 用户填写抬头通知的Headers
77        PublicKey wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
78
79        Notification notification = WXPayUtility.parseNotification(
80                apiv3Key,
81                wechatPayPublicKeyId, wechatPayPublicKey, headers,
82                refundNotifyStr);
83
84        // 解析用户填写抬头信息
85        FapiaoIssued fapiaoIssued = WXPayUtility.fromJson(notification.getResource().getCiphertext(), FapiaoIssued.class);
86
87        String notifyMchId = fapiaoIssued.mchid;
88        if (mchid.equals(notifyMchId)) {
89            // 用于服务商获取电子发票信息
90            String subMchId = fapiaoIssued.subMchId;
91            String fapiaoApplyId = fapiaoIssued.fapiaoApplyId;
92            for (FapiaoInfo fapiaoInfo : fapiaoIssued.fapiaoInformation) {
93                Status fapiaoStatus = fapiaoInfo.fapiaoStatus;
94                switch (fapiaoStatus) {
95                    case ISSUED:
96                        // 使用回调的fapiao_apply_id,调用【查询电子发票】接口,获取发票详情
97                        break;
98                    case ISSUE_ACCEPTED:
99                        // 等待开票成功回调
100                        break;
101                    default:
102                        break;
103                }
104            }
105        }
106    }
107}

2.5 扫码/小程序开票(用户主动授权开票)

2.5.1 业务描述

用户通过商户系统,提供的二维码,或者商户小程序,使用微信支付订单或者非微信支付订单,进行开票的流程。

2.5.2 业务示例代码

1package com.java.partner;
2
3import com.java.demo.AcquireFapiaoTitleUrl;  // https://pay.weixin.qq.com/doc/v3/partner/4015770776
4import com.java.demo.AcquireFapiaoTitleUrl.AcquireFapiaoTitleUrlRequest;
5import com.java.demo.AcquireFapiaoTitleUrl.AcquireFapiaoTitleUrlResponse;
6import com.java.demo.AcquireFapiaoTitleUrl.Source;
7import com.java.demo.GetFapiaoApplications;  // https://pay.weixin.qq.com/doc/v3/partner/4015792567
8import com.java.demo.GetFapiaoApplications.FapiaoApplicationsEntity;
9import com.java.demo.GetFapiaoApplications.FapiaoEntity;
10import com.java.demo.GetFapiaoApplications.FapiaoInfo;
11import com.java.demo.GetFapiaoApplications.GetFapiaoApplicationsRequest;
12import com.java.demo.GetFapiaoFileDownloadInfo;  // https://pay.weixin.qq.com/doc/v3/partner/4015792576
13import com.java.demo.GetFapiaoFileDownloadInfo.FapiaoDownloadInfo;
14import com.java.demo.GetFapiaoFileDownloadInfo.GetFapiaoFileDownloadInfoRequest;
15import com.java.demo.GetFapiaoFileDownloadInfo.GetFapiaoFileDownloadInfoResponse;
16import com.java.demo.GetUserTitle;  // https://pay.weixin.qq.com/doc/v3/partner/4015784260
17import com.java.demo.GetUserTitle.BuyerType;
18import com.java.demo.GetUserTitle.GetUserTitleRequest;
19import com.java.demo.GetUserTitle.Scene;
20import com.java.demo.GetUserTitle.UserTitleEntity;  
21import com.java.demo.IssueGeneralIndustryFapiaoApplications;  // https://pay.weixin.qq.com/doc/v3/partner/4015792574
22import com.java.demo.IssueGeneralIndustryFapiaoApplications.FapiaoBillType;
23import com.java.demo.IssueGeneralIndustryFapiaoApplications.IssueGeneralFapiaoInfo;
24import com.java.demo.IssueGeneralIndustryFapiaoApplications.IssueGeneralFapiaoItem;
25import com.java.demo.IssueGeneralIndustryFapiaoApplications.IssueGeneralFapiaoRequest;
26import com.java.demo.IssueGeneralIndustryFapiaoApplications.PayChannel;
27import com.java.demo.IssueGeneralIndustryFapiaoApplications.TransactionInfo;
28import com.java.utils.WXPayUtility;
29import java.util.ArrayList;
30import java.util.Objects;
31
32public class IssueByMerchantQrCodeDemo {
33
34    public static void main(String[] args) {
35        // TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/partner/4013080340
36        AcquireFapiaoTitleUrl acquireFapiaoTitleUrl = new AcquireFapiaoTitleUrl(
37                "19xxxxxxxx",
38                // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
39                "1DDE55AD98Exxxxxxxxxx",
40                // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
41                "/path/to/apiclient_key.pem",     // 商户API证书私钥文件路径,本地文件路径
42                "PUB_KEY_ID_xxxxxxxxxxxxx",      // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
43                "/path/to/wxp_pub.pem"           // 微信支付公钥文件路径,本地文件路径
44        );
45
46        GetUserTitle getUserTitle = new GetUserTitle(
47                "19xxxxxxxx",
48                // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
49                "1DDE55AD98Exxxxxxxxxx",
50                // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
51                "/path/to/apiclient_key.pem",     // 商户API证书私钥文件路径,本地文件路径
52                "PUB_KEY_ID_xxxxxxxxxxxxx",      // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
53                "/path/to/wxp_pub.pem"           // 微信支付公钥文件路径,本地文件路径
54        );
55
56        IssueGeneralIndustryFapiaoApplications issueGeneralIndustryFapiaoApplications = new IssueGeneralIndustryFapiaoApplications(
57                "19xxxxxxxx",
58                // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
59                "1DDE55AD98Exxxxxxxxxx",
60                // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
61                "/path/to/apiclient_key.pem",     // 商户API证书私钥文件路径,本地文件路径
62                "PUB_KEY_ID_xxxxxxxxxxxxx",      // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
63                "/path/to/wxp_pub.pem"           // 微信支付公钥文件路径,本地文件路径
64        );
65
66        GetFapiaoApplications getFapiaoApplications = new GetFapiaoApplications(
67                "19xxxxxxxx",
68                // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
69                "1DDE55AD98Exxxxxxxxxx",
70                // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
71                "/path/to/apiclient_key.pem",     // 商户API证书私钥文件路径,本地文件路径
72                "PUB_KEY_ID_xxxxxxxxxxxxx",      // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
73                "/path/to/wxp_pub.pem"           // 微信支付公钥文件路径,本地文件路径
74        );
75
76        GetFapiaoFileDownloadInfo getFapiaoFileDownloadInfo = new GetFapiaoFileDownloadInfo(
77                "19xxxxxxxx",
78                // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
79                "1DDE55AD98Exxxxxxxxxx",
80                // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
81                "/path/to/apiclient_key.pem",     // 商户API证书私钥文件路径,本地文件路径
82                "PUB_KEY_ID_xxxxxxxxxxxxx",      // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
83                "/path/to/wxp_pub.pem"           // 微信支付公钥文件路径,本地文件路径
84        );
85
86        AcquireFapiaoTitleUrlRequest acquireFapiaoTitleUrlRequest = new AcquireFapiaoTitleUrlRequest();
87        acquireFapiaoTitleUrlRequest.subMchid = "1900000109";
88        // 服务商每次申请开票的唯一标识
89        acquireFapiaoTitleUrlRequest.fapiaoApplyId = "fapiao_20200701_123456";
90        acquireFapiaoTitleUrlRequest.source = Source.WEB;
91        acquireFapiaoTitleUrlRequest.appid = "wxb1170446a4c0a5a2";
92        acquireFapiaoTitleUrlRequest.openid = "plN5twRbHym_j-QcqCzstl0HmwEs";
93        acquireFapiaoTitleUrlRequest.totalAmount = 382895L;
94        acquireFapiaoTitleUrlRequest.sellerName = "深圳市南山区测试商户";
95        acquireFapiaoTitleUrlRequest.showPhoneCell = true;
96        acquireFapiaoTitleUrlRequest.mustInputPhone = true;
97        acquireFapiaoTitleUrlRequest.showEmailCell = true;
98        acquireFapiaoTitleUrlRequest.mustInputEmail = true;
99
100        GetUserTitleRequest getUserTitleRequest = new GetUserTitleRequest();
101        getUserTitleRequest.subMchid = "1900000109";
102        getUserTitleRequest.scene = Scene.WITHOUT_WECHATPAY;
103        getUserTitleRequest.fapiaoApplyId = acquireFapiaoTitleUrlRequest.fapiaoApplyId;
104
105        IssueGeneralFapiaoRequest issueGeneralFapiaoRequest = new IssueGeneralFapiaoRequest();
106        issueGeneralFapiaoRequest.subMchid = "1900000109";
107        issueGeneralFapiaoRequest.fapiaoApplyId = acquireFapiaoTitleUrlRequest.fapiaoApplyId;
108        issueGeneralFapiaoRequest.buyerInformation = new IssueGeneralIndustryFapiaoApplications.UserTitleEntity();
109
110        issueGeneralFapiaoRequest.fapiaoInformation = new IssueGeneralFapiaoInfo();
111        // 此次申请开票下,申请开具的一张发票的唯一标识,如果需要红冲后换开发票、开票失败后重开发票,使用相同的fapiao_apply_id,更换不同的fapiao_id即可
112        issueGeneralFapiaoRequest.fapiaoInformation.fapiaoId = "20200701123456";
113        // 申请开票的金额,支持小于本次申请开票对应的所有交易信息的交易金额之和
114        issueGeneralFapiaoRequest.fapiaoInformation.totalAmount = 1123L;
115        issueGeneralFapiaoRequest.fapiaoInformation.items = new ArrayList<>();
116        {
117            IssueGeneralFapiaoItem itemsItem = new IssueGeneralFapiaoItem();
118            itemsItem.taxCode = "3010101020203000000";
119            itemsItem.goodsName = "*xxx*服务";
120            itemsItem.specification = "米";
121            itemsItem.unit = "米";
122            itemsItem.quantity = 100000000L;
123            itemsItem.totalAmount = 429900L;
124            itemsItem.taxRate = 1300L;
125            itemsItem.discount = false;
126            itemsItem.preferentialPolicyCode = 1L;
127            issueGeneralFapiaoRequest.fapiaoInformation.items.add(itemsItem);
128        }
129      	;
130
131        issueGeneralFapiaoRequest.fapiaoInformation.exportBusinessPolicyCode = 1L;
132        issueGeneralFapiaoRequest.fapiaoInformation.vatRefundLevyCode = 1L;
133        issueGeneralFapiaoRequest.fapiaoInformation.billingPersonId = "128279891283";
134        issueGeneralFapiaoRequest.fapiaoInformation.billingPerson = "example_billing_person";
135        issueGeneralFapiaoRequest.fapiaoInformation.fapiaoBillType = FapiaoBillType.COMM_FAPIAO;
136        issueGeneralFapiaoRequest.fapiaoInformation.remark = "备注";
137
138        GetFapiaoApplicationsRequest getFapiaoApplicationsRequest = new GetFapiaoApplicationsRequest();
139        getFapiaoApplicationsRequest.fapiaoApplyId = acquireFapiaoTitleUrlRequest.fapiaoApplyId;
140        getFapiaoApplicationsRequest.subMchid = "1900000109";
141        getFapiaoApplicationsRequest.fapiaoId = issueGeneralFapiaoRequest.fapiaoInformation.fapiaoId;
142
143        GetFapiaoFileDownloadInfoRequest getFapiaoFileDownloadInfoRequest = new GetFapiaoFileDownloadInfoRequest();
144        getFapiaoFileDownloadInfoRequest.fapiaoApplyId = acquireFapiaoTitleUrlRequest.fapiaoApplyId;
145        getFapiaoFileDownloadInfoRequest.subMchid = "1900000109";
146        getFapiaoFileDownloadInfoRequest.fapiaoId = issueGeneralFapiaoRequest.fapiaoInformation.fapiaoId;
147
148        try {
149            AcquireFapiaoTitleUrlResponse acquireFapiaoTitleUrlResponse = acquireFapiaoTitleUrl.run(
150                    acquireFapiaoTitleUrlRequest);
151            // 通过指引https://developers.weixin.qq.com/miniprogram/dev/api/navigate/wx.navigateToMiniProgram.html,使用返回的参数,跳转到指定的小程序,展示给用户
152            String miniprogramAppid = acquireFapiaoTitleUrlResponse.miniprogramAppid;
153            String miniprogramPath = acquireFapiaoTitleUrlResponse.miniprogramPath;
154            String miniprogramUserName = acquireFapiaoTitleUrlResponse.miniprogramUserName;
155
156            // 用户填写抬头信息完成后,会发送回调请求,然后就可以获取用户填写的抬头信息
157            UserTitleEntity userTitleEntity = getUserTitle.run(getUserTitleRequest);
158            // 申请开票的用户,提交的留言信息
159            System.out.println("userApplyMessage:" + userTitleEntity.userApplyMessage);
160            if (userTitleEntity.type.equals(BuyerType.INDIVIDUAL)) {
161                issueGeneralFapiaoRequest.buyerInformation.type = IssueGeneralIndustryFapiaoApplications.BuyerType.INDIVIDUAL;
162            } else {
163                issueGeneralFapiaoRequest.buyerInformation.type = IssueGeneralIndustryFapiaoApplications.BuyerType.ORGANIZATION;
164            }
165            issueGeneralFapiaoRequest.buyerInformation.name = userTitleEntity.name;
166            issueGeneralFapiaoRequest.buyerInformation.taxpayerId = userTitleEntity.taxpayerId;
167            issueGeneralFapiaoRequest.buyerInformation.address = userTitleEntity.address;
168            issueGeneralFapiaoRequest.buyerInformation.telephone = userTitleEntity.telephone;
169            issueGeneralFapiaoRequest.buyerInformation.bankName = userTitleEntity.bankName;
170            issueGeneralFapiaoRequest.buyerInformation.bankAccount = userTitleEntity.bankAccount;
171            issueGeneralFapiaoRequest.buyerInformation.phone = issueGeneralIndustryFapiaoApplications.encrypt(
172                    userTitleEntity.phone);
173            issueGeneralFapiaoRequest.buyerInformation.email = issueGeneralIndustryFapiaoApplications.encrypt(
174                    userTitleEntity.email);
175            // 返回当前交易信息对应的交易金额
176            issueGeneralFapiaoRequest.buyerInformation.amount = userTitleEntity.amount;
177            if (userTitleEntity.fapiaoBillType.equals(GetUserTitle.FapiaoBillType.COMM_FAPIAO)) {
178                issueGeneralFapiaoRequest.buyerInformation.fapiaoBillType = FapiaoBillType.COMM_FAPIAO;
179            } else {
180                issueGeneralFapiaoRequest.buyerInformation.fapiaoBillType = FapiaoBillType.VAT_FAPIAO;
181            }
182
183            issueGeneralFapiaoRequest.fapiaoInformation.transactionInformation = new ArrayList<>();
184            {
185                TransactionInfo transactionInformationItem = new TransactionInfo();
186                transactionInformationItem.payChannel = PayChannel.WECHAT_PAY;
187                // 服务商系统自行收集该笔发票申请对应交易信息的交易单号以及交易金额
188                transactionInformationItem.transactionId = "x23189jojfiujei1o08";
189                transactionInformationItem.amount = 1231243L;
190                issueGeneralFapiaoRequest.fapiaoInformation.transactionInformation.add(transactionInformationItem);
191            }
192            ;
193
194            // 服务商通过匹配交易的商品、服务内容等信息,根据税局的要求,自行选择需要调用的行业开票接口。本示例以通用行业接口为例
195            // 无返回内容 http code202即为请求成功
196            issueGeneralIndustryFapiaoApplications.run(issueGeneralFapiaoRequest);
197
198            FapiaoApplicationsEntity fapiaoApplicationsEntity = getFapiaoApplications.run(getFapiaoApplicationsRequest);
199            String fapiaoErrorCode = "";
200            String fapiaoErrorMessage = "";
201            for (FapiaoEntity fapiaoEntity : fapiaoApplicationsEntity.fapiaoInformation) {
202                switch (fapiaoEntity.status) {
203                    case ISSUED:
204                        // 发票开具成功,可以获取蓝票信息
205                        FapiaoInfo fapiaoInfo = fapiaoEntity.blueFapiao;
206                        break;
207                    case REVERSED:
208                        // 发票已红冲,可以获取红票信息
209                        FapiaoInfo redFapiaoInfo = fapiaoEntity.redFapiao;
210                        break;
211                    case ISSUE_ACCEPTED:
212                        // 发票开票中,等待一段时间后,重试查询电子发票
213                        break;
214                    case REVERSE_ACCEPTED:
215                        // 发票红冲中,等待一段时间后,重试查询电子发票
216                        break;
217                    case ISSUE_FAILED:
218                        // 蓝票开具失败,根据错误原因,解决问题后,换fapiao_id 进行重试开票
219                        // 常见的蓝票开具失败原因如:
220                        // - 开票参数不符合。税收编码[ 1030201000000000000 ]为汇总项,不可开具,请修改后再试。
221                        // - 开票失败乐企发票开具失败:他用或联用企业开票人未授权开票,请修改后再试。或联系主管税务机关。
222                        // - 开票预扣授信额度失败,授信额度不足本次开票金额:9.71;剩余可用额度:0.00 —— 在税局重新申请额度后,换fapiao_id 重试
223                        fapiaoErrorCode = fapiaoEntity.fapiaoErrorCode;
224                        fapiaoErrorMessage = fapiaoEntity.fapiaoErrorMessage;
225                        break;
226                    case REVERSE_FAILED:
227                        // 蓝票开具失败,根据错误原因,解决问题后,直接进行重试红冲
228                        // 常见的红票开具失败原因如:
229                        // - 该发票存在进行中的红字确认单 —— 电子税局已存在处理中的红字确认单,商户登录电子税局继续红冲,或者撤销红字确认单后,再重试请求红茶
230                        fapiaoErrorCode = fapiaoEntity.fapiaoErrorCode;
231                        fapiaoErrorMessage = fapiaoEntity.fapiaoErrorMessage;
232                        break;
233                    default:
234                        break;
235                }
236            }
237            System.out.println("fapiaoErrorCode:" + fapiaoErrorCode);
238            System.out.println("fapiaoErrorMessage:" + fapiaoErrorMessage);
239
240            // 获取电子发票下载信息
241            GetFapiaoFileDownloadInfoResponse getFapiaoFileDownloadInfoResponse = getFapiaoFileDownloadInfo.run(
242                    getFapiaoFileDownloadInfoRequest);
243            for (FapiaoDownloadInfo fapiaoDownloadInfo : getFapiaoFileDownloadInfoResponse.fapiaoDownloadInfoList) {
244                if (Objects.equals(getFapiaoFileDownloadInfoRequest.fapiaoId, fapiaoDownloadInfo.fapiaoId)) {
245                    // 根据【下载发票文件】(https://pay.weixin.qq.com/doc/v3/partner/4015792569)指引,添加参数到downloadUrl,通过GET请求downloadUrl即可获取发票文件流
246                    String downloadUrl = fapiaoDownloadInfo.downloadUrl;
247                }
248            }
249        } catch (WXPayUtility.ApiException e) {
250            // 处理异常处理逻辑
251            if (e.getErrorCode().equals("SYSTEM_ERROR")) {
252                // 错误:系统错误
253                // 解决方式:稍后重试
254                // 描述:微信支付系统失败,建议等1分钟后再重试
255            } else if (e.getErrorCode().equals("FREQUENCY_LIMITED")) {
256                // 错误:限频报错
257                // 解决方式:稍后重试
258                // 描述: 接口频率限制,一定要原单重试,建议等1分钟后再重试
259            } else if (e.getErrorCode().equals("ERROR")) {
260                // 错误:系统错误
261                // 解决方式:系统繁忙,请稍后重试
262                // 描述:微信支付系统失败,建议等1分钟后再重试
263            } else if (e.getErrorCode().equals("NO_AUTH")) {
264                // 错误:商户无权限
265                // 解决方式:按照报错返回的message,参照指引解决问题
266                // 描述:无权限的场景如下:
267                // - 检查服务商商户号是否开通微信支付电子发票服务商权限,检查商户是否接受邀请开通授权微信支付电子发票能力,等待1分钟后重试
268                // - 开票申请单不是当前服务商创建,无权限进行当前操作
269                // - 商户乐企发票能力未授权,请授权相关发票能力 —— 确认通过【获取邀请商户开通服务商电子发票能力】邀请能力开通,并且商户在电子税局乐企系统上完成授权
270                // - 商户未开通数电发票能力,请开通数电发票 —— 确认通过【获取邀请商户开通服务商电子发票能力】邀请能力开通,并且商户在电子税局乐企系统上完成授权
271            } else if (e.getErrorCode().equals("SIGN_ERROR")) {
272                // 错误:签名错误
273                // 解决方式:检查平台商户证书序列号,证书私钥文件,公钥ID,公钥文件,同时确认下签名过程是不是按照微信支付的签名方式
274                // 描述:签名报错,需要确认签名材料和签名流程是否正确
275            } else if (e.getErrorCode().equals("PARAM_ERROR")) {
276                // 错误:参数错误
277                // 解决方式:按照报错返回的message,重新输入请求参数
278                // 描述:参数的类型,长度,或者必填选项没有填写等
279            } else if (e.getErrorCode().equals("RULE_LIMIT")) {
280                // 错误:条件限制
281                // 解决方式:按照报错返回的message接引操作后,重新调用
282                // 描述:需要按照指引重新处理的情况如:
283                // - 商户获取抬头链接指定的用户与已提交抬头的用户不一致,请确认发票申请单号对应的用户是否正确
284                // - 当前开票场景与发票申请单来源不匹配,请确认参数中的“开票场景”与“发票申请单号”是否符合要求。确认参数中的“开票场景”与“发票申请单号”是否对应
285                // - 商户尚未配置电子发票卡券模版信息,请先调用【创建电子发票卡券模板】接口成功后再重新调用
286                // - 商户提交的购买方信息与用户填写的发票抬头不一致,请检查参数中的购买方信息与【获取用户填写的抬头】接口返回的发票抬头信息是否一致
287                // - 开票总金额不能超过发票申请单的总金额,请检查参数中各张发票的“总价税合计”,若是微信支付场景,不能超过微信支付订单的金额,若是非微信支付场景,不能超过【获取抬头填写链接】接口中指定的总金额
288            } else if (e.getErrorCode().equals("INVALID_REQUEST")) {
289                // 错误:请求非法,请求参数正确,但是不符合业务规则
290                // 解决方式:根据具体message查看具体哪里不符合业务规则,如果可以修改参数达到符合业务规则的修改请求符合业务规则之后重试
291                // 描述:不符合业务规则的场景如:
292                // - 传入的参数“用户是否必须填写手机号”在“是否需要展示手机号填写栏”为false时不能为true(位于/url/must_input_phone),请检查参数格式是否符合要求
293                // - 传入的参数“用户是否必须填写邮箱地址”在“是否需要展示邮箱地址填写栏”为false时不能为true(位于/url/must_input_email),请检查参数格式是否符合要求
294                // - 交易信息中,交易金额必须大于0
295                // - 交易渠道非微信支付时,交易商户订单号必填
296                // - 微信支付交易信息金额错误
297                // - 多个微信支付渠道交易合并开票时,必须是同一个用户的所属交易账单
298                // - 交易单号在支付宝渠道下非法
299                // - 发票申请单申请开票的总金额,不能超过交易信息的总金额
300                // - 非折扣行金额必须为正数,请使用正确的参数重新调用
301                // - 总价税合计与所有发票行单行金额的累加和不相等,请使用正确的参数重新调用
302                // - 折扣行不能作为首行
303                // - 第{}行是折扣行,数电模式下,“单位”,“规格”必须为空,请使用正确的参数重新调用
304                // - 第{}行是折扣行,数电模式下,“货物或应税劳务、服务名称”,“税率”,“税收分类编码”,“优惠政策标识” 必须和被折扣行相同
305                // - 折扣行必须在被折扣行的下一行,请使用正确的参数重新调用
306                // - 折扣行金额必须为负数,请使用正确的参数重新调用
307                // - 折扣行金额绝对值不能超过被折扣行金额,请使用正确的参数重新调用
308                // - 折扣行的数量值非法,请使用正确的参数重新调用
309                // - 当前发票开具中,不可申请重开发票
310                // - 当前发票红冲中,不可申请重开发票
311                // - 申请开票次数,超过最大限制
312                // - 开票请求已被受理,请调用【查询电子发票】接口获取开票结果
313                // - 交易单已申请开票,两次申请的交易金额不相同,修改交易金额重试
314                // - 交易可剩余开票金额不足,修改交易订单或者开票金额重试
315            } else {
316                // 其他类型错误:联系支持
317            }
318        }
319    }
320}

2.5.3 用户操作

用户完成操作后,即可通过2.3.3处理用户填写抬头完成通知,然后获取到用户填写的对应的抬头信息。

2.6 第三方/自建模式插卡

2.6.1 业务描述

商户通过【获取用户填写抬头链接】的方式或者通过自建发票系统收集用户抬头信息,在第三方、自建的发票平台上,成功开具发票。可以通过微信支付,将该发票交付到用户的微信卡包中。如果是微信支付的交易订单开票场景,无需用户授权,即可插卡。

2.6.2 业务示例代码

1package com.java.partner;
2
3import com.java.demo.InsertCards;  // https://pay.weixin.qq.com/doc/v3/partner/4015792579
4import com.java.demo.InsertCards.BuyerType;
5import com.java.demo.InsertCards.ExtraInfo;
6import com.java.demo.InsertCards.FapiaoCardInfo;
7import com.java.demo.InsertCards.FapiaoItem;
8import com.java.demo.InsertCards.InsertCardsRequest;
9import com.java.demo.InsertCards.Scene;
10import com.java.demo.InsertCards.SellerInfo;
11import com.java.demo.InsertCards.TaxPreferMark;
12import com.java.demo.InsertCards.UserTitleEntity;
13import com.java.demo.UploadFapiaoFile;  // https://pay.weixin.qq.com/doc/v3/partner/4015792580
14import com.java.demo.UploadFapiaoFile.DigestType;
15import com.java.demo.UploadFapiaoFile.FapiaoFileType;
16import com.java.demo.UploadFapiaoFile.FileMeta;
17import com.java.demo.UploadFapiaoFile.UploadFapiaoFileRequest;
18import com.java.demo.UploadFapiaoFile.UploadFapiaoFileResponse;
19import com.java.utils.WXPayUtility;
20import java.util.ArrayList;
21
22public class InsertInvoiceCardDemo {
23
24    public static void main(String[] args) {
25        // TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/partner/4013080340
26        UploadFapiaoFile uploadFapiaoFile = new UploadFapiaoFile(
27                "19xxxxxxxx",
28                // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
29                "1DDE55AD98Exxxxxxxxxx",
30                // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
31                "/path/to/apiclient_key.pem",     // 商户API证书私钥文件路径,本地文件路径
32                "PUB_KEY_ID_xxxxxxxxxxxxx",      // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
33                "/path/to/wxp_pub.pem"           // 微信支付公钥文件路径,本地文件路径
34        );
35
36        InsertCards insertCards = new InsertCards(
37                "19xxxxxxxx",
38                // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
39                "1DDE55AD98Exxxxxxxxxx",
40                // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
41                "/path/to/apiclient_key.pem",     // 商户API证书私钥文件路径,本地文件路径
42                "PUB_KEY_ID_xxxxxxxxxxxxx",      // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
43                "/path/to/wxp_pub.pem"           // 微信支付公钥文件路径,本地文件路径
44        );
45
46        UploadFapiaoFileRequest uploadFapiaoFileRequest = new UploadFapiaoFileRequest();
47        uploadFapiaoFileRequest.file = "file_bytes".getBytes();
48        uploadFapiaoFileRequest.meta = new FileMeta();
49        uploadFapiaoFileRequest.meta.subMchid = "1900000109";
50        uploadFapiaoFileRequest.meta.fileType = FapiaoFileType.PDF;
51        uploadFapiaoFileRequest.meta.digestAlogrithm = DigestType.SM3;
52        uploadFapiaoFileRequest.meta.digest = "addca90a6a290b9642dbaccffdf01e8c22aa348940b2e96754169ba08c19e5db";
53
54        InsertCardsRequest insertCardsRequest = new InsertCardsRequest();
55        // 申请插卡的唯一标识
56        insertCardsRequest.fapiaoApplyId = "4200000444201910177461284488";
57        insertCardsRequest.subMchid = "1900000109";
58        insertCardsRequest.scene = Scene.WITH_WECHATPAY;
59        insertCardsRequest.buyerInformation = new UserTitleEntity();
60        insertCardsRequest.buyerInformation.type = BuyerType.INDIVIDUAL;
61        insertCardsRequest.buyerInformation.name = "深圳市南山区测试企业";
62        insertCardsRequest.buyerInformation.taxpayerId = "202003261233701778";
63        insertCardsRequest.buyerInformation.address = "深圳市南山区深南大道10000号";
64        insertCardsRequest.buyerInformation.telephone = "075512345678";
65        insertCardsRequest.buyerInformation.bankName = "测试银行";
66        insertCardsRequest.buyerInformation.bankAccount = "62001234567890";
67        insertCardsRequest.buyerInformation.phone = insertCards.encrypt("phone");
68        insertCardsRequest.buyerInformation.email = insertCards.encrypt("email");
69        insertCardsRequest.buyerInformation.amount = 1000L;
70
71        try {
72            UploadFapiaoFileResponse uploadFapiaoFileResponse = uploadFapiaoFile.run(uploadFapiaoFileRequest);
73
74            insertCardsRequest.fapiaoCardInformation = new ArrayList<>();
75            {
76                FapiaoCardInfo fapiaoCardInformationItem = new FapiaoCardInfo();
77                // 【上传电子发票文件】调用成功后,会立即返回对应文件的fapiaoMediaId
78                fapiaoCardInformationItem.fapiaoMediaId = uploadFapiaoFileResponse.fapiaoMediaId;
79                //  申请插卡请求的发票信息,要和上传的发票文件票面内容保持一致
80                fapiaoCardInformationItem.fapiaoNumber = "12897794";
81                fapiaoCardInformationItem.fapiaoCode = "044001911211";
82                fapiaoCardInformationItem.fapiaoTime = "2020-07-01T12:00:00+08:00";
83                fapiaoCardInformationItem.checkCode = "69001808340631374774";
84                fapiaoCardInformationItem.password = "006>299-375/326>2+7/_*0-+<351059<80<4*_/5>+<11631+*3030/5*37+/-243159658+013>3409*044>4-/1+/9->*>69501*6++1997--21";
85                fapiaoCardInformationItem.totalAmount = 382895L;
86                fapiaoCardInformationItem.taxAmount = 44050L;
87                fapiaoCardInformationItem.amount = 338845L;
88                fapiaoCardInformationItem.sellerInformation = new SellerInfo();
89                fapiaoCardInformationItem.sellerInformation.name = "深圳市南山区测试公司";
90                fapiaoCardInformationItem.sellerInformation.taxpayerId = "202003261233701778";
91                fapiaoCardInformationItem.sellerInformation.address = "深圳市南山区深南大道10000号";
92                fapiaoCardInformationItem.sellerInformation.telephone = "075512345678";
93                fapiaoCardInformationItem.sellerInformation.bankName = "测试银行";
94                fapiaoCardInformationItem.sellerInformation.bankAccount = "62001234567890";
95                fapiaoCardInformationItem.extraInformation = new ExtraInfo();
96                fapiaoCardInformationItem.extraInformation.drawer = "张三";
97                fapiaoCardInformationItem.items = new ArrayList<>();
98                {
99                    FapiaoItem itemsItem = new FapiaoItem();
100                    itemsItem.taxCode = "3010101020203000000";
101                    itemsItem.goodsName = "出租汽车客运服务";
102                    itemsItem.specification = "A4";
103                    itemsItem.unit = "次";
104                    itemsItem.quantity = 100000000L;
105                    itemsItem.unitPrice = 380442000000L;
106                    itemsItem.amount = 380442L;
107                    itemsItem.taxAmount = 49458L;
108                    itemsItem.totalAmount = 429900L;
109                    itemsItem.taxRate = 1300L;
110                    itemsItem.taxPreferMark = TaxPreferMark.NO_FAVORABLE;
111                    itemsItem.discount = false;
112                    fapiaoCardInformationItem.items.add(itemsItem);
113                }
114                fapiaoCardInformationItem.remark = "备注";
115                insertCardsRequest.fapiaoCardInformation.add(fapiaoCardInformationItem);
116            }
117            insertCards.run(insertCardsRequest);
118        } catch (WXPayUtility.ApiException e) {
119            // 处理异常处理逻辑
120            if (e.getErrorCode().equals("SYSTEM_ERROR")) {
121                // 错误:系统错误
122                // 解决方式:稍后重试
123                // 描述:微信支付系统失败,建议等1分钟后再重试
124            } else if (e.getErrorCode().equals("FREQUENCY_LIMITED")) {
125                // 错误:限频报错
126                // 解决方式:稍后重试
127                // 描述: 接口频率限制,一定要原单重试,建议等1分钟后再重试
128            } else if (e.getErrorCode().equals("ERROR")) {
129                // 错误:系统错误
130                // 解决方式:系统繁忙,请稍后重试
131                // 描述:微信支付系统失败,建议等1分钟后再重试
132            } else if (e.getErrorCode().equals("NO_AUTH")) {
133                // 错误:商户无权限
134                // 解决方式:按照报错返回的message,参照指引解决问题
135                // 描述:商户无权限访问,请确认是否已开通相关权限 —— 开通微信支付电子发票服务商权限,并且检查商户是否接受邀请开通授权微信支付电子发票能力,等1分钟后重试。
136                // - 根据发票申请单查询不到用户提交的授权,请检查发票申请单号是否正确,或在确认用户已提交授权后重新调用
137            } else if (e.getErrorCode().equals("SIGN_ERROR")) {
138                // 错误:签名错误
139                // 解决方式:检查平台商户证书序列号,证书私钥文件,公钥ID,公钥文件,同时确认下签名过程是不是按照微信支付的签名方式
140                // 描述:签名报错,需要确认签名材料和签名流程是否正确
141            } else if (e.getErrorCode().equals("PARAM_ERROR")) {
142                // 错误:参数错误
143                // 解决方式:按照报错返回的message,重新输入请求参数
144                // 描述:参数的类型,长度,或者必填选项没有填写等
145                // - 传入的参数“电子发票文件”长度超过限制(位于/body/file),请检查参数格式是否符合要求
146                // - 传入的参数“文件摘要”与上传的电子发票文件不匹配(位于/body/meta/digest),请检查参数格式是否符合要求
147            } else if (e.getErrorCode().equals("RULE_LIMIT")) {
148                // 错误:条件限制
149                // 解决方式:按照报错返回的message接引操作后,重新调用
150                // 描述:需要按照指引重新处理的情况如:
151                // - 商户尚未配置电子发票卡券模版信息,请先调用【创建电子发票卡券模板】(https://pay.weixin.qq.com/doc/v3/partner/4015792562)接口成功后再重新调用
152                // - 当前开票场景与发票申请单来源不匹配,请确认参数中的“开票场景”与“发票申请单号”是否符合要求
153                // - 商户提交的购买方信息与用户填写的发票抬头不一致,请检查参数中的购买方信息与【获取用户填写抬头信息】(https://pay.weixin.qq.com/doc/v3/partner/4015784260)接口返回的发票抬头信息是否一致
154                // - 微信支付订单状态不满足开票要求,请确认微信支付订单已完成支付
155                // - 该间连商户未绑定可开票的直连商户
156                // - 微信支付订单所属商户与当前开票商户不一致,请检查微信支付订单号是否属于商户,若是服务商模式则检查是否属于子商户
157                // - 开票总金额不能超过微信支付订单的总金额,请检查参数中各张发票的“总价税合计”
158            } else if (e.getErrorCode().equals("INVALID_REQUEST")) {
159                // 错误:请求非法,请求参数正确,但是不符合业务规则
160                // 解决方式:根据具体message查看具体哪里不符合业务规则,如果可以修改参数达到符合业务规则的修改请求符合业务规则之后重试
161                // 描述:不符合业务规则的场景如:
162                // - 电子发票文件内容无法解析,请检查文件格式是否正确
163                // - 请求中的发票代码、发票号码或校验码与电子发票文件不匹配,请使用正确的参数重新调用
164            } else {
165                // 其他类型错误:联系获取支持
166            }
167        }
168    }
169}
170

插入成功后,用户会在微信上收到卡包通知

2.6.3 处理发票插入用户卡包成功通知

1package com.tencent.wxpay.fapiao;
2
3import com.google.gson.annotations.SerializedName;
4import com.tencent.wxpay.fapiao.FapiaoIssuedNotifyDemo.CardStatus;
5import com.tencent.wxpay.fapiao.FapiaoIssuedNotifyDemo.FapiaoInfo;
6import com.tencent.wxpay.fapiao.FapiaoIssuedNotifyDemo.Status;
7import com.tencent.wxpay.fapiao.UserTitleNotifyDemo.UserTitle;
8import com.tencent.wxpay.utils.WXPayUtility;
9import com.tencent.wxpay.utils.WXPayUtility.Notification;
10import java.security.PublicKey;
11import java.util.List;
12import okhttp3.Headers;
13import okhttp3.Response;
14
15public class CardInsertedNotifyDemo {
16    // TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/partner/4013080340
17    // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考
18    // https://pay.weixin.qq.com/doc/v3/merchant/4013070756
19    private static String mchid = "19xxxxxxxx";
20    // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
21    private static String certificateSerialNo = "1DDE55AD98Exxxxxxxxxx";
22    // 商户API证书私钥文件路径,本地文件路径
23    private static String privateKeyFilePath = "/path/to/apiclient_key.pem";
24    // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
25    private static String wechatPayPublicKeyId = "PUB_KEY_ID_xxxxxxxxxxxxx";
26    // 微信支付公钥文件路径,本地文件路径
27    private static String wechatPayPublicKeyFilePath = "/path/to/wxp_pub.pem";
28    // 商户APIv3密钥,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
29    private static String apiv3Key = "your apiv3 key";
30
31    public static class CardInserted {
32        @SerializedName("mchid")
33        public String mchid;
34
35        @SerializedName("sub_mchid")
36        public String subMchId;
37
38        @SerializedName("fapiao_apply_id")
39        public String fapiaoApplyId;
40
41        @SerializedName("fapiao_information")
42        public List<FapiaoInfo> fapiaoInformation;
43    }
44
45    public static class FapiaoInfo {
46        @SerializedName("fapiao_id")
47        public String fapiaoId;
48
49        @SerializedName("fapiao_status")
50        public Status fapiaoStatus;
51
52        @SerializedName("card_status")
53        public CardStatus cardStatus;
54    }
55
56    public enum Status {
57        @SerializedName("ISSUE_ACCEPTED")
58        ISSUE_ACCEPTED,
59        @SerializedName("ISSUED")
60        ISSUED,
61        @SerializedName("REVERSE_ACCEPTED")
62        REVERSE_ACCEPTED,
63        @SerializedName("REVERSED")
64        REVERSED,
65    }
66
67    public enum CardStatus {
68        @SerializedName("INSERT_ACCEPTED")
69        INSERT_ACCEPTED,
70        @SerializedName("INSERTED")
71        INSERTED,
72        @SerializedName("DISCARD_ACCEPTED")
73        DISCARD_ACCEPTED,
74        @SerializedName("DISCARDED")
75        DISCARDED
76    }
77
78    public void cardInsertedNotify(Response httpResponse) {
79        String refundNotifyStr = WXPayUtility.extractBody(httpResponse); // 用户填写抬头通知的原始字符串
80        Headers headers = httpResponse.headers(); // 用户填写抬头通知的Headers
81        PublicKey wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
82
83        Notification notification = WXPayUtility.parseNotification(
84                apiv3Key,
85                wechatPayPublicKeyId, wechatPayPublicKey, headers,
86                refundNotifyStr);
87
88        // 解析用户填写抬头信息
89        CardInserted cardInserted = WXPayUtility.fromJson(notification.getResource().getCiphertext(), CardInserted.class);
90
91        String notifyMchId = cardInserted.mchid;
92        if (mchid.equals(notifyMchId)) {
93            for (FapiaoInfo fapiaoInfo : cardInserted.fapiaoInformation) {
94                CardStatus cardStatus = fapiaoInfo.cardStatus;
95                switch (cardStatus) {
96                    case INSERTED:
97                        // 插卡成功
98                        break;
99                    case DISCARDED:
100                        // 发票卡劵已作废,已从用户卡包移除
101                        break;
102                    case INSERT_ACCEPTED:
103                        // 插卡已受理,等待插卡成功回调
104                        break;
105                    case DISCARD_ACCEPTED:
106                        // 作废申请已受理
107                        break;
108                    default:
109                        break;
110                }
111            }
112        }
113    }
114}

2.7 开具/冲红电子发票

2.7.1 业务描述

申请对商户在微信支付电子发票系统申请开具并且已经开具成功的电子发票进行冲红,然后自动作废已经插入用户卡包的发票。

2.7.2 业务示例代码

1package com.java.partner;
2
3import com.java.demo.GetFapiaoApplications; // https://pay.weixin.qq.com/doc/v3/partner/4015792576
4import com.java.demo.GetFapiaoApplications.FapiaoApplicationsEntity;
5import com.java.demo.GetFapiaoApplications.FapiaoEntity;
6import com.java.demo.GetFapiaoApplications.FapiaoInfo;
7import com.java.demo.GetFapiaoApplications.GetFapiaoApplicationsRequest;
8import com.java.demo.ReverseFapiaoApplications;  // https://pay.weixin.qq.com/doc/v3/partner/4015792575
9import com.java.demo.ReverseFapiaoApplications.ReverseFapiaoApplicationsRequest;
10import com.java.demo.ReverseFapiaoApplications.ReverseFapiaoInfo;
11import com.java.utils.WXPayUtility;
12import java.util.ArrayList;
13
14public class ReverseFapiaoDemo {
15
16    public static void main(String[] args) {
17        // TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/partner/4013080340
18        ReverseFapiaoApplications reverseFapiaoApplications = new ReverseFapiaoApplications(
19                "19xxxxxxxx",
20                // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
21                "1DDE55AD98Exxxxxxxxxx",
22                // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
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
28        GetFapiaoApplications getFapiaoApplications = new GetFapiaoApplications(
29                "19xxxxxxxx",
30                // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
31                "1DDE55AD98Exxxxxxxxxx",
32                // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
33                "/path/to/apiclient_key.pem",     // 商户API证书私钥文件路径,本地文件路径
34                "PUB_KEY_ID_xxxxxxxxxxxxx",      // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
35                "/path/to/wxp_pub.pem"           // 微信支付公钥文件路径,本地文件路径
36        );
37
38        // 现阶段红冲只支持全额红冲,不支持部分红冲
39        ReverseFapiaoApplicationsRequest reverseFapiaoApplicationsRequest = new ReverseFapiaoApplicationsRequest();
40        // 申请红冲发票,对应蓝票开票时申请开票的唯一标识
41        reverseFapiaoApplicationsRequest.fapiaoApplyId = "4200000444201910177461284488";
42        reverseFapiaoApplicationsRequest.subMchid = "1900000109";
43        // ISSUE_ERROR: 开票有误
44        // SALES_RETURN: 销货退回
45        // SERVICE_SUSPENTION: 服务中止
46        // SALES_DISCOUNT: 销售折让
47        reverseFapiaoApplicationsRequest.reverseReason = "ISSUE_ERROR";
48        reverseFapiaoApplicationsRequest.fapiaoInformation = new ArrayList<>();
49        {
50            ReverseFapiaoInfo fapiaoInformationItem = new ReverseFapiaoInfo();
51            // 申请红冲的发票对应蓝票的fapiaoId
52            fapiaoInformationItem.fapiaoId = "20200701123456";
53            fapiaoInformationItem.fapiaoNumber = "12897794";
54            reverseFapiaoApplicationsRequest.fapiaoInformation.add(fapiaoInformationItem);
55        };
56
57        GetFapiaoApplicationsRequest getFapiaoApplicationsRequest = new GetFapiaoApplicationsRequest();
58        getFapiaoApplicationsRequest.fapiaoApplyId = reverseFapiaoApplicationsRequest.fapiaoApplyId;
59        getFapiaoApplicationsRequest.subMchid = "1900000109";
60        try {
61            // 无返回内容, http code 202即为申请红冲成功
62            reverseFapiaoApplications.run(reverseFapiaoApplicationsRequest);
63
64            // 申请开具通用行业数电发票
65            FapiaoApplicationsEntity fapiaoApplicationsEntity = getFapiaoApplications.run(getFapiaoApplicationsRequest);
66            String fapiaoErrorCode = "";
67            String fapiaoErrorMessage = "";
68            for (FapiaoEntity fapiaoEntity : fapiaoApplicationsEntity.fapiaoInformation) {
69                switch (fapiaoEntity.status) {
70                    case ISSUED:
71                        // 发票开具成功,可以获取蓝票信息
72                        FapiaoInfo fapiaoInfo = fapiaoEntity.blueFapiao;
73                        break;
74                    case REVERSED:
75                        // 发票已红冲,可以获取红票信息
76                        FapiaoInfo redFapiaoInfo = fapiaoEntity.redFapiao;
77                        break;
78                    case ISSUE_ACCEPTED:
79                        // 发票开票中,等待一段时间后,重试查询电子发票
80                        break;
81                    case REVERSE_ACCEPTED:
82                        // 发票红冲中,等待一段时间后,重试查询电子发票
83                        break;
84                    case ISSUE_FAILED:
85                        // 蓝票开具失败,根据错误原因,解决问题后,换fapiao_id 进行重试开票
86                        // 常见的蓝票开具失败原因如:
87                        // - 开票参数不符合。税收编码[ 1030201000000000000 ]为汇总项,不可开具,请修改后再试。
88                        // - 开票失败乐企发票开具失败:他用或联用企业开票人未授权开票,请修改后再试。或联系主管税务机关。
89                        // - 开票预扣授信额度失败,授信额度不足本次开票金额:9.71;剩余可用额度:0.00 —— 在税局重新申请额度后,换fapiao_id 重试
90                        fapiaoErrorCode = fapiaoEntity.fapiaoErrorCode;
91                        fapiaoErrorMessage = fapiaoEntity.fapiaoErrorMessage;
92                        break;
93                    case REVERSE_FAILED:
94                        // 蓝票开具失败,根据错误原因,解决问题后,直接进行重试红冲
95                        // 常见的红票开具失败原因如:
96                        // - 该发票存在进行中的红字确认单 —— 电子税局已存在处理中的红字确认单,商户登录电子税局继续红冲,或者撤销红字确认单后,再重试请求冲红
97                        fapiaoErrorCode = fapiaoEntity.fapiaoErrorCode;
98                        fapiaoErrorMessage = fapiaoEntity.fapiaoErrorMessage;
99                        break;
100                    default:
101                        break;
102                }
103            }
104            System.out.println("fapiaoErrorCode:" + fapiaoErrorCode);
105            System.out.println("fapiaoErrorMessage:" + fapiaoErrorMessage);
106        } catch (WXPayUtility.ApiException e) {
107            // 异常处理逻辑
108            if (e.getErrorCode().equals("SYSTEM_ERROR")) {
109                // 错误:系统错误
110                // 解决方式:稍后重试
111                // 描述:微信支付系统失败,建议等1分钟后再重试
112            } else if (e.getErrorCode().equals("FREQUENCY_LIMITED")) {
113                // 错误:限频报错
114                // 解决方式:稍后重试
115                // 描述: 接口频率限制,一定要原单重试,建议等1分钟后再重试
116            } else if (e.getErrorCode().equals("ERROR")) {
117                // 错误:系统错误
118                // 解决方式:系统繁忙,请稍后重试
119                // 描述:微信支付系统失败,建议等1分钟后再重试
120            } else if (e.getErrorCode().equals("NO_AUTH")) {
121                // 错误:无权限
122                // 解决方式:按照报错返回的message,查看对应的解决指引
123                // 描述:无权限的情况如:
124                // - 商户无权限访问,请确认是否已开通相关权限。—— 开通微信支付电子发票服务商权限,并且检查商户是否接受邀请开通授权微信支付电子发票能力,等1分钟后重试。
125                // - 开票申请单不是当前服务商创建,无权限进行当前操作
126            } else if (e.getErrorCode().equals("SIGN_ERROR")) {
127                // 错误:签名错误
128                // 解决方式:检查平台商户证书序列号,证书私钥文件,公钥ID,公钥文件,同时确认下签名过程是不是按照微信支付的签名方式
129                // 描述:签名报错,需要确认签名材料和签名流程是否正确
130            } else if (e.getErrorCode().equals("PARAM_ERROR")) {
131                // 错误:参数错误
132                // 解决方式:按照报错返回的message,重新输入请求参数
133                // 描述:参数的类型,长度,或者必填选项没有填写,
134                // - 传入的参数“需要冲红的发票信息”为空(位于/body/fapiao_information),请检查参数格式是否符合要求
135            } else if (e.getErrorCode().equals("RESOURCE_NOT_EXISTS")) {
136                // 错误:资源错误
137                // 解决方式:请检查发票申请单号是否正确,或在确认用户已提交抬头后重新调用
138                // 描述:根据发票申请单查询不到用户填写的抬头
139            } else if (e.getErrorCode().equals("ORDER_CLOSED")) {
140                // 错误:订单错误
141                // 解决方式:联系商户,重新申请开具发票
142                // 描述:开票申请已被商户拒绝,无法进行当前操作
143            } else if (e.getErrorCode().equals("RESOURCE_ALREADY_EXISTS")) {
144                // 错误:资源已存在
145                // 解决方式:调用【查询电子发票】接口获取开票结果
146                // 描述:开票请求已被受理,请稍后调用【查询电子发票】接口获取开票结果
147            } else if (e.getErrorCode().equals("INVALID_REQUEST")) {
148                // 错误:请求非法,请求参数正确,但是不符合业务规则
149                // 解决方式:根据具体message查看具体哪里不符合业务规则,如果可以修改参数达到符合业务规则的修改请求符合业务规则之后重试
150                // 描述:不符合业务规则的场景如:
151                // - 仅在微信支付侧开具的电子发票才允许发起冲红
152                // - 传入的参数“商户发票单号”出现重复(位于/body/fapiao_information/fapiao_id),请使用正确的参数重新调用
153                // - 传入的参数“发票代码”及“发票号码”出现重复(位于/body/fapiao_information/fapiao_code|fapiao_number),请使用正确的参数重新调用
154            } else {
155                // 其他类型错误:联系获取支持
156            }
157        }
158    }
159
160}
161

2.7.3 处理发票冲红成功通知

1package com.tencent.wxpay.fapiao;
2
3import com.google.gson.annotations.SerializedName;
4import com.tencent.wxpay.utils.WXPayUtility;
5import com.tencent.wxpay.utils.WXPayUtility.Notification;
6import java.security.PublicKey;
7import java.util.List;
8import okhttp3.Headers;
9import okhttp3.Response;
10
11public class ReversedNotifyDemo {
12    // TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/partner/4013080340
13    // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考
14    // https://pay.weixin.qq.com/doc/v3/merchant/4013070756
15    private static String mchid = "19xxxxxxxx";
16    // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
17    private static String certificateSerialNo = "1DDE55AD98Exxxxxxxxxx";
18    // 商户API证书私钥文件路径,本地文件路径
19    private static String privateKeyFilePath = "/path/to/apiclient_key.pem";
20    // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
21    private static String wechatPayPublicKeyId = "PUB_KEY_ID_xxxxxxxxxxxxx";
22    // 微信支付公钥文件路径,本地文件路径
23    private static String wechatPayPublicKeyFilePath = "/path/to/wxp_pub.pem";
24    // 商户APIv3密钥,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
25    private static String apiv3Key = "your apiv3 key";
26
27    public static class Reversed {
28        @SerializedName("mchid")
29        public String mchid;
30
31        @SerializedName("sub_mchid")
32        public String subMchId;
33
34        @SerializedName("fapiao_apply_id")
35        public String fapiaoApplyId;
36
37        @SerializedName("fapiao_information")
38        public List<FapiaoInfo> fapiaoInformation;
39    }
40
41    public static class FapiaoInfo {
42        @SerializedName("fapiao_id")
43        public String fapiaoId;
44
45        @SerializedName("fapiao_status")
46        public Status fapiaoStatus;
47
48        @SerializedName("card_status")
49        public CardStatus cardStatus;
50    }
51
52    public enum Status {
53        @SerializedName("ISSUE_ACCEPTED")
54        ISSUE_ACCEPTED,
55        @SerializedName("ISSUED")
56        ISSUED,
57        @SerializedName("REVERSE_ACCEPTED")
58        REVERSE_ACCEPTED,
59        @SerializedName("REVERSED")
60        REVERSED,
61    }
62
63    public enum CardStatus {
64        @SerializedName("INSERT_ACCEPTED")
65        INSERT_ACCEPTED,
66        @SerializedName("INSERTED")
67        INSERTED,
68        @SerializedName("DISCARD_ACCEPTED")
69        DISCARD_ACCEPTED,
70        @SerializedName("DISCARDED")
71        DISCARDED
72    }
73
74    public void cardInsertedNotify(Response httpResponse) {
75        String refundNotifyStr = WXPayUtility.extractBody(httpResponse); // 用户填写抬头通知的原始字符串
76        Headers headers = httpResponse.headers(); // 用户填写抬头通知的Headers
77        PublicKey wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
78
79        Notification notification = WXPayUtility.parseNotification(
80                apiv3Key,
81                wechatPayPublicKeyId, wechatPayPublicKey, headers,
82                refundNotifyStr);
83
84        // 解析用户填写抬头信息
85        Reversed reversed = WXPayUtility.fromJson(notification.getResource().getCiphertext(), Reversed.class);
86
87        String notifyMchId = reversed.mchid;
88        if (mchid.equals(notifyMchId)) {
89          	String fapiaoApplyId = reversed.fapiaoApplyId;
90            for (FapiaoInfo fapiaoInfo : reversed.fapiaoInformation) {
91                Status status = fapiaoInfo.fapiaoStatus;
92                switch (status) {
93                    case REVERSED:
94                        // 使用回调的fapiao_apply_id,调用【查询电子发票】接口,获取发票详情
95                        break;
96                    case REVERSE_ACCEPTED:
97                        // 发票红冲中,继续等待发票红冲成功通知
98                        break;
99                    default:
100                        break;
101                }
102            }
103        }
104    }
105}

2.7.4 处理发票卡券作废通知

1package com.tencent.wxpay.fapiao;
2
3import com.google.gson.annotations.SerializedName;
4import com.tencent.wxpay.fapiao.ReversedNotifyDemo.CardStatus;
5import com.tencent.wxpay.fapiao.ReversedNotifyDemo.FapiaoInfo;
6import com.tencent.wxpay.fapiao.ReversedNotifyDemo.Reversed;
7import com.tencent.wxpay.fapiao.ReversedNotifyDemo.Status;
8import com.tencent.wxpay.utils.WXPayUtility;
9import com.tencent.wxpay.utils.WXPayUtility.Notification;
10import java.security.PublicKey;
11import java.util.List;
12import okhttp3.Headers;
13import okhttp3.Response;
14
15public class CardDiscarded {
16    // TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/partner/4013080340
17    // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考
18    // https://pay.weixin.qq.com/doc/v3/merchant/4013070756
19    private static String mchid = "19xxxxxxxx";
20    // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
21    private static String certificateSerialNo = "1DDE55AD98Exxxxxxxxxx";
22    // 商户API证书私钥文件路径,本地文件路径
23    private static String privateKeyFilePath = "/path/to/apiclient_key.pem";
24    // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
25    private static String wechatPayPublicKeyId = "PUB_KEY_ID_xxxxxxxxxxxxx";
26    // 微信支付公钥文件路径,本地文件路径
27    private static String wechatPayPublicKeyFilePath = "/path/to/wxp_pub.pem";
28    // 商户APIv3密钥,如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
29    private static String apiv3Key = "your apiv3 key";
30
31    public static class Reversed {
32        @SerializedName("mchid")
33        public String mchid;
34
35        @SerializedName("sub_mchid")
36        public String subMchId;
37
38        @SerializedName("fapiao_apply_id")
39        public String fapiaoApplyId;
40
41        @SerializedName("fapiao_information")
42        public List<FapiaoInfo> fapiaoInformation;
43    }
44
45    public static class FapiaoInfo {
46        @SerializedName("fapiao_id")
47        public String fapiaoId;
48
49        @SerializedName("fapiao_status")
50        public Status fapiaoStatus;
51
52        @SerializedName("card_status")
53        public CardStatus cardStatus;
54    }
55
56    public enum Status {
57        @SerializedName("ISSUE_ACCEPTED")
58        ISSUE_ACCEPTED,
59        @SerializedName("ISSUED")
60        ISSUED,
61        @SerializedName("REVERSE_ACCEPTED")
62        REVERSE_ACCEPTED,
63        @SerializedName("REVERSED")
64        REVERSED,
65    }
66
67    public enum CardStatus {
68        @SerializedName("INSERT_ACCEPTED")
69        INSERT_ACCEPTED,
70        @SerializedName("INSERTED")
71        INSERTED,
72        @SerializedName("DISCARD_ACCEPTED")
73        DISCARD_ACCEPTED,
74        @SerializedName("DISCARDED")
75        DISCARDED
76    }
77
78    public void cardDiscardedNotify(Response httpResponse) {
79        String refundNotifyStr = WXPayUtility.extractBody(httpResponse); // 用户填写抬头通知的原始字符串
80        Headers headers = httpResponse.headers(); // 用户填写抬头通知的Headers
81        PublicKey wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
82
83        Notification notification = WXPayUtility.parseNotification(
84                apiv3Key,
85                wechatPayPublicKeyId, wechatPayPublicKey, headers,
86                refundNotifyStr);
87
88        // 解析用户填写抬头信息
89        CardDiscarded cardDiscarded = WXPayUtility.fromJson(notification.getResource().getCiphertext(), CardDiscarded.class);
90
91        String notifyMchId = cardDiscarded.mchid;
92        if (mchid.equals(notifyMchId)) {
93            for (FapiaoInfo fapiaoInfo : cardDiscarded.fapiaoInformation) {
94                CardStatus cardStatus = fapiaoInfo.cardStatus;
95                switch (cardStatus) {
96                    case DISCARD_ACCEPTED:
97                        // 发票作废已申请,等待发票作废成功通知
98                        break;
99                    case DISCARDED:
100                        // 发票已作废
101                        break;
102                    default:
103                        break;
104                }
105            }
106        }
107    }
108}
109