diff --git a/docs/ELECTRONIC_INVOICE_API_COMPARISON.md b/docs/ELECTRONIC_INVOICE_API_COMPARISON.md new file mode 100644 index 0000000000..54568878f9 --- /dev/null +++ b/docs/ELECTRONIC_INVOICE_API_COMPARISON.md @@ -0,0 +1,66 @@ +# 服务商电子发票接口体系对比 + +## 结论 + +Gitee Issue `IDMETW` 补充的官方链接 +[获取开通服务商电子发票能力邀请链接](https://pay.weixin.qq.com/doc/v3/partner/4015941495) +与 GitHub Issue [#4066](https://github.com/binarywang/WxJava/issues/4066) 给出的 +[开发接入准备](https://pay.weixin.qq.com/doc/v3/partner/4015792554) 是**同一套**微信支付 +V3 服务商电子发票产品文档:前者是 API 列表中的具体接口,后者是该产品的接入总览。 + +但是,当前 `weixin-java-mp` 的 `WxMpMerchantInvoiceService` 是另一套旧的公众号 +`/card/invoice/*` 接口;不能把它当作这两个 Issue 所要求的支付 V3 能力实现。 + +## 当前仓库既有的公众号体系 + +- 当前代码将该能力归入公众号模块: + `weixin-java-mp` 的 + `WxMpMerchantInvoiceService`。 +- 该接口的 Javadoc 明确引用公众号官方文档: + [商户开票模式说明](https://developers.weixin.qq.com/doc/offiaccount/WeChat_Invoice/E_Invoice/Vendor_and_Invoicing_Platform_Mode_Instruction.html) + 和 + [商户开票接口列表](https://developers.weixin.qq.com/doc/offiaccount/WeChat_Invoice/E_Invoice/Vendor_API_List.html)。 + 后者当前重定向至 + [新版服务号文档](https://developers.weixin.qq.com/doc/service/guide/product/weixin_invoice/E_Invoice/Vendor_API_List.html)。 +- 代码实际请求公众号 API:`/card/invoice/getauthurl`、 + `/card/invoice/getauthdata`、`/card/invoice/makeoutinvoice`、 + `/card/invoice/clearoutinvoice`、`/card/invoice/queryinvoceinfo` 等;见 + `WxMpApiUrl.Invoice`。这套接口围绕授权页、获取用户授权数据、开票、冲红和 + 查询发票信息。 +- 历史提交 `058ce62a2b932633931e30762f44c561481dde5f` 将该服务及其请求/响应 + Bean 加入 `weixin-java-mp`;提交说明关联 #1305。 + +> 说明:调研时 Gitee 页面/API 的 TLS 连接失败,未能独立读取 IDMETW 的评论; +> 但用户随后提供的 `4015941495` 链接已明确该 Issue 实际指向支付 V3 服务商电子发票。 + +## 两个 Issue 共同指向的支付 V3 体系 + +- #4066 链接的[开发接入准备](https://pay.weixin.qq.com/doc/v3/partner/4015792554) + 属于 + “微信电子发票”产品,要求在**微信支付服务商号**申请“服务商电子发票”权限, + 并提到数电发票资源。 +- IDMETW 链接的[获取开通服务商电子发票能力邀请链接](https://pay.weixin.qq.com/doc/v3/partner/4015941495) + 位于相同产品的“API 列表”下,路径为 + `GET /v3/new-tax-control-fapiao/fapiaomerchant/getspinviteurl`。 +- 同一官方文档导航的 API 列表包含:邀请子商户开通、检查子商户开票状态、创建电子 + 发票卡券模板、配置开发选项、抬头填写链接/信息、各行业开票、冲红、查询、下载/上传 + 发票文件、插入用户卡包,以及多个异步通知。 +- 例如“开具通用行业电子发票”接口为 + `POST /v3/new-tax-control-fapiao/fapiao-applications/issue-general`,请求域名为 + `https://api.mch.weixin.qq.com`,并要求微信支付 API 证书签名、服务商模式的 + `sub_mchid` 和唯一开票申请单号 `fapiao_apply_id`;官方文档: + [开具通用行业电子发票](https://pay.weixin.qq.com/doc/v3/partner/4015792574)。 + 该页还规定敏感字段使用微信支付公钥或平台证书加密。 + +## 直接差异 + +| 维度 | 既有公众号能力 | 两个 Issue 指向的微信支付能力 | +| --- | --- | --- | +| SDK 模块 | `weixin-java-mp` | 应位于 `weixin-java-pay` | +| 官方文档产品线 | `developers.weixin.qq.com` 的公众号电子发票 | `pay.weixin.qq.com/doc/v3/partner` 的微信支付合作伙伴电子发票 | +| API 形态 | `/card/invoice/*` | `/v3/new-tax-control-fapiao/*` | +| 身份/鉴权上下文 | 公众号 access token、用户授权页/授权数据 | 微信支付服务商号、子商户号、V3 签名与敏感字段加密 | +| 覆盖范围 | 授权、开票、冲红、查询及公众号商户配置 | 子商户邀约/状态、模板/开发配置、行业开票、文件和卡包、通知 | + +因此,两个 Issue 都是同一个微信支付 V3 服务商电子发票接入需求,当前代码库检索未 +发现对应的 `new-tax-control-fapiao` API 实现。 diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/BuyerInformation.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/BuyerInformation.java new file mode 100644 index 0000000000..ea9686ebe3 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/BuyerInformation.java @@ -0,0 +1,21 @@ +package com.github.binarywang.wxpay.bean.invoice; +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import java.io.Serializable; +/** 电子发票购买方抬头信息。敏感字段 phone、email 由调用方按支付文档加密。 */ +@Data public class BuyerInformation implements Serializable { + private static final long serialVersionUID = 1L; + private String type; + private String name; + @SerializedName("taxpayer_id") private String taxpayerId; + private String address; + private String telephone; + @SerializedName("bank_name") private String bankName; + @SerializedName("bank_account") private String bankAccount; + private String phone; + private String email; + private Integer amount; + @SerializedName("out_trade_no") private String outTradeNo; + @SerializedName("fapiao_bill_type") private String fapiaoBillType; + @SerializedName("user_apply_message") private String userApplyMessage; +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/CardTemplateRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/CardTemplateRequest.java new file mode 100644 index 0000000000..407540eadc --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/CardTemplateRequest.java @@ -0,0 +1,28 @@ +package com.github.binarywang.wxpay.bean.invoice; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import java.io.Serializable; + +/** 创建电子发票卡券模板请求。 */ +@Data +public class CardTemplateRequest implements Serializable { + private static final long serialVersionUID = 1L; + @SerializedName("sub_mchid") private String subMchid; + @SerializedName("card_appid") private String cardAppid; + @SerializedName("card_template_information") private TemplateInformation cardTemplateInformation; + @Data public static class TemplateInformation implements Serializable { + private static final long serialVersionUID = 1L; + @SerializedName("payee_name") private String payeeName; + @SerializedName("logo_url") private String logoUrl; + @SerializedName("custom_cell") private CustomCell customCell; + } + @Data public static class CustomCell implements Serializable { + private static final long serialVersionUID = 1L; + private String words; + private String description; + @SerializedName("jump_url") private String jumpUrl; + @SerializedName("miniprogram_user_name") private String miniprogramUserName; + @SerializedName("miniprogram_path") private String miniprogramPath; + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/CardTemplateResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/CardTemplateResult.java new file mode 100644 index 0000000000..9e998bd8ae --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/CardTemplateResult.java @@ -0,0 +1,10 @@ +package com.github.binarywang.wxpay.bean.invoice; +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import java.io.Serializable; +/** 电子发票卡券模板结果。 */ +@Data public class CardTemplateResult implements Serializable { + private static final long serialVersionUID = 1L; + @SerializedName("card_appid") private String cardAppid; + @SerializedName("card_id") private String cardId; +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/DevelopmentConfigRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/DevelopmentConfigRequest.java new file mode 100644 index 0000000000..8ae4239b3e --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/DevelopmentConfigRequest.java @@ -0,0 +1,12 @@ +package com.github.binarywang.wxpay.bean.invoice; +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import java.io.Serializable; +/** 服务商电子发票开发配置请求。 */ +@Data public class DevelopmentConfigRequest implements Serializable { + private static final long serialVersionUID = 1L; + @SerializedName("callback_url") private String callbackUrl; + @SerializedName("sub_mch_code") private String subMchCode; + @SerializedName("show_fapiao_cell") private Boolean showFapiaoCell; + @SerializedName("support_vat_fapiao") private Boolean supportVatFapiao; +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/DevelopmentConfigResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/DevelopmentConfigResult.java new file mode 100644 index 0000000000..2a865849f2 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/DevelopmentConfigResult.java @@ -0,0 +1,11 @@ +package com.github.binarywang.wxpay.bean.invoice; +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import java.io.Serializable; +/** 服务商电子发票开发配置结果。 */ +@Data public class DevelopmentConfigResult implements Serializable { + private static final long serialVersionUID = 1L; + @SerializedName("callback_url") private String callbackUrl; + @SerializedName("show_fapiao_cell") private Boolean showFapiaoCell; + @SerializedName("support_vat_fapiao") private Boolean supportVatFapiao; +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/GeneralInvoiceRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/GeneralInvoiceRequest.java new file mode 100644 index 0000000000..529609eeaa --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/GeneralInvoiceRequest.java @@ -0,0 +1,80 @@ +package com.github.binarywang.wxpay.bean.invoice; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 服务商开具通用行业电子发票请求。 + * + * @see 开具通用行业电子发票 + */ +@Data +public class GeneralInvoiceRequest implements Serializable { + private static final long serialVersionUID = 1L; + + @SerializedName("sub_mchid") + private String subMchid; + @SerializedName("fapiao_apply_id") + private String fapiaoApplyId; + @SerializedName("buyer_information") + private BuyerInformation buyerInformation; + @SerializedName("fapiao_information") + private FapiaoInformation fapiaoInformation; + + @Data + public static class FapiaoInformation implements Serializable { + private static final long serialVersionUID = 1L; + @SerializedName("fapiao_id") + private String fapiaoId; + @SerializedName("total_amount") + private Integer totalAmount; + private List items; + @SerializedName("export_business_policy_code") + private Integer exportBusinessPolicyCode; + @SerializedName("vat_refund_levy_code") + private Integer vatRefundLevyCode; + @SerializedName("billing_person_id") + private String billingPersonId; + @SerializedName("billing_person") + private String billingPerson; + @SerializedName("fapiao_bill_type") + private String fapiaoBillType; + @SerializedName("transaction_information") + private List transactionInformation; + private String remark; + } + + @Data + public static class InvoiceItem implements Serializable { + private static final long serialVersionUID = 1L; + @SerializedName("tax_code") + private String taxCode; + @SerializedName("goods_name") + private String goodsName; + private String specification; + private String unit; + private Integer quantity; + @SerializedName("total_amount") + private Integer totalAmount; + @SerializedName("tax_rate") + private Integer taxRate; + private Boolean discount; + @SerializedName("preferential_policy_code") + private Integer preferentialPolicyCode; + } + + @Data + public static class TransactionInformation implements Serializable { + private static final long serialVersionUID = 1L; + @SerializedName("pay_channel") + private String payChannel; + @SerializedName("transaction_id") + private String transactionId; + @SerializedName("out_trade_no") + private String outTradeNo; + private Integer amount; + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/IndustryInvoiceRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/IndustryInvoiceRequest.java new file mode 100644 index 0000000000..1275b2805d --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/IndustryInvoiceRequest.java @@ -0,0 +1,12 @@ +package com.github.binarywang.wxpay.bean.invoice; +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import java.io.Serializable; +/** 行业电子发票请求;fapiaoInformation 对应不动产租赁或成品油官方请求对象。 */ +@Data public class IndustryInvoiceRequest implements Serializable { + private static final long serialVersionUID = 1L; + @SerializedName("sub_mchid") private String subMchid; + @SerializedName("fapiao_apply_id") private String fapiaoApplyId; + @SerializedName("buyer_information") private BuyerInformation buyerInformation; + @SerializedName("fapiao_information") private Object fapiaoInformation; +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/InsertCardRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/InsertCardRequest.java new file mode 100644 index 0000000000..0731ff4fad --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/InsertCardRequest.java @@ -0,0 +1,14 @@ +package com.github.binarywang.wxpay.bean.invoice; +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import java.io.Serializable; +import java.util.List; +/** 将电子发票插入微信用户卡包请求。 */ +@Data public class InsertCardRequest implements Serializable { + private static final long serialVersionUID = 1L; + @SerializedName("sub_mchid") private String subMchid; + private String scene; + @SerializedName("fapiao_apply_id") private String fapiaoApplyId; + @SerializedName("buyer_information") private BuyerInformation buyerInformation; + @SerializedName("fapiao_card_information") private List fapiaoCardInformation; +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/InviteMerchantQuery.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/InviteMerchantQuery.java new file mode 100644 index 0000000000..c79d519c04 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/InviteMerchantQuery.java @@ -0,0 +1,13 @@ +package com.github.binarywang.wxpay.bean.invoice; +import lombok.Data; +import java.io.Serializable; +/** 查询服务商邀请开通电子发票能力的商户条件。 */ +@Data public class InviteMerchantQuery implements Serializable { + private static final long serialVersionUID = 1L; + private String queryTimeStart; + private String queryTimeEnd; + private Integer offset; + private Integer limit; + private String inviteCode; + private String mchInviteStatus; +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/InviteMerchantResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/InviteMerchantResult.java new file mode 100644 index 0000000000..84b5a06387 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/InviteMerchantResult.java @@ -0,0 +1,24 @@ +package com.github.binarywang.wxpay.bean.invoice; +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import java.io.Serializable; +import java.util.List; +/** 服务商邀请商户查询结果。 */ +@Data public class InviteMerchantResult implements Serializable { + private static final long serialVersionUID = 1L; + @SerializedName("total_count") private Integer totalCount; + private Integer offset; + private Integer limit; + @SerializedName("mch_invite_result_list") private List mchInviteResultList; + @Data public static class Merchant implements Serializable { + private static final long serialVersionUID = 1L; + @SerializedName("sub_mchid") private String subMchid; + @SerializedName("mch_invite_status") private String mchInviteStatus; + @SerializedName("ep_name") private String epName; + @SerializedName("tax_id") private String taxId; + @SerializedName("invite_code") private String inviteCode; + @SerializedName("operate_time") private String operateTime; + @SerializedName("invite_failed_code") private String inviteFailedCode; + @SerializedName("invite_failed_reason") private String inviteFailedReason; + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/InviteUrlRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/InviteUrlRequest.java new file mode 100644 index 0000000000..c70e10755f --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/InviteUrlRequest.java @@ -0,0 +1,19 @@ +package com.github.binarywang.wxpay.bean.invoice; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** 获取服务商电子发票能力邀请链接请求。 */ +@Data +public class InviteUrlRequest implements Serializable { + private static final long serialVersionUID = 1L; + private String subMchid; + private String operationType; + private String fapiaoMode; + private List fapiaoAbilityTypeList; + private String inviteChannel; + private String operateUser; + private String inviteCode; +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/InviteUrlResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/InviteUrlResult.java new file mode 100644 index 0000000000..dd38601e69 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/InviteUrlResult.java @@ -0,0 +1,19 @@ +package com.github.binarywang.wxpay.bean.invoice; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; + +import java.io.Serializable; + +/** + * 服务商电子发票开通邀请链接。 + * + * @author binarywang + */ +@Data +public class InviteUrlResult implements Serializable { + private static final long serialVersionUID = 1L; + + @SerializedName("invite_url") + private String inviteUrl; +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/InvoiceFileResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/InvoiceFileResult.java new file mode 100644 index 0000000000..3fc18cd7cd --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/InvoiceFileResult.java @@ -0,0 +1,31 @@ +package com.github.binarywang.wxpay.bean.invoice; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 电子发票文件下载信息。 + * + * @see 获取发票下载信息 + */ +@Data +public class InvoiceFileResult implements Serializable { + private static final long serialVersionUID = 1L; + + @SerializedName("fapiao_download_info_list") + private List fapiaoDownloadInfoList; + + @Data + public static class DownloadInfo implements Serializable { + private static final long serialVersionUID = 1L; + + @SerializedName("fapiao_id") + private String fapiaoId; + @SerializedName("download_url") + private String downloadUrl; + private String status; + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/InvoiceFileUploadRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/InvoiceFileUploadRequest.java new file mode 100644 index 0000000000..fb1c667772 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/InvoiceFileUploadRequest.java @@ -0,0 +1,14 @@ +package com.github.binarywang.wxpay.bean.invoice; +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import java.io.File; +import java.io.Serializable; +/** 上传电子发票 PDF 文件请求。digest 为官方要求的 SM3 十六进制摘要。 */ +@Data public class InvoiceFileUploadRequest implements Serializable { + private static final long serialVersionUID = 1L; + private File file; + @SerializedName("sub_mchid") private String subMchid; + @SerializedName("file_type") private String fileType = "PDF"; + @SerializedName("digest_alogrithm") private String digestAlgorithm = "SM3"; + private String digest; +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/InvoiceFileUploadResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/InvoiceFileUploadResult.java new file mode 100644 index 0000000000..a11736d49c --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/InvoiceFileUploadResult.java @@ -0,0 +1,9 @@ +package com.github.binarywang.wxpay.bean.invoice; +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import java.io.Serializable; +/** 上传电子发票文件结果。 */ +@Data public class InvoiceFileUploadResult implements Serializable { + private static final long serialVersionUID = 1L; + @SerializedName("fapiao_media_id") private String fapiaoMediaId; +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/InvoiceResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/InvoiceResult.java new file mode 100644 index 0000000000..e2b2dbd800 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/InvoiceResult.java @@ -0,0 +1,59 @@ +package com.github.binarywang.wxpay.bean.invoice; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 电子发票查询结果。 + * + * @see 查询电子发票 + */ +@Data +public class InvoiceResult implements Serializable { + private static final long serialVersionUID = 1L; + + @SerializedName("total_count") + private Integer totalCount; + @SerializedName("fapiao_information") + private List fapiaoInformation; + + @Data + public static class InvoiceInformation implements Serializable { + private static final long serialVersionUID = 1L; + + @SerializedName("fapiao_id") + private String fapiaoId; + private String status; + @SerializedName("blue_fapiao") + private Fapiao blueFapiao; + @SerializedName("red_fapiao") + private Fapiao redFapiao; + @SerializedName("total_amount") + private Integer totalAmount; + @SerializedName("tax_amount") + private Integer taxAmount; + private Integer amount; + @SerializedName("fapiao_error_code") + private String fapiaoErrorCode; + @SerializedName("fapiao_error_message") + private String fapiaoErrorMessage; + } + + @Data + public static class Fapiao implements Serializable { + private static final long serialVersionUID = 1L; + + @SerializedName("fapiao_code") + private String fapiaoCode; + @SerializedName("fapiao_number") + private String fapiaoNumber; + @SerializedName("check_code") + private String checkCode; + private String password; + @SerializedName("fapiao_time") + private String fapiaoTime; + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/ReverseInvoiceRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/ReverseInvoiceRequest.java new file mode 100644 index 0000000000..85e2a7ec2c --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/ReverseInvoiceRequest.java @@ -0,0 +1,40 @@ +package com.github.binarywang.wxpay.bean.invoice; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 冲红电子发票请求。 + * + * @see 冲红电子发票 + */ +@Data +public class ReverseInvoiceRequest implements Serializable { + private static final long serialVersionUID = 1L; + + @SerializedName("sub_mchid") + private String subMchid; + @Expose(serialize = false) + @SerializedName("fapiao_apply_id") + private String fapiaoApplyId; + @SerializedName("reverse_reason") + private String reverseReason; + @SerializedName("fapiao_information") + private List fapiaoInformation; + + @Data + public static class InvoiceInfo implements Serializable { + private static final long serialVersionUID = 1L; + + @SerializedName("fapiao_id") + private String fapiaoId; + @SerializedName("fapiao_code") + private String fapiaoCode; + @SerializedName("fapiao_number") + private String fapiaoNumber; + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/SubMerchantInvoiceStatus.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/SubMerchantInvoiceStatus.java new file mode 100644 index 0000000000..8a03d3df9c --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/SubMerchantInvoiceStatus.java @@ -0,0 +1,54 @@ +package com.github.binarywang.wxpay.bean.invoice; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** 服务商子商户电子发票能力状态。 */ +@Data +public class SubMerchantInvoiceStatus implements Serializable { + private static final long serialVersionUID = 1L; + @SerializedName("sub_mchid") + private String subMchid; + @SerializedName("third_mode") + private Mode thirdMode; + @SerializedName("digital_tax_mode") + private DigitalTaxMode digitalTaxMode; + + @Data + public static class Mode implements Serializable { + private static final long serialVersionUID = 1L; + private String status; + } + + @Data + public static class DigitalTaxMode extends Mode { + private static final long serialVersionUID = 1L; + @SerializedName("billing_person_info") + private List billingPersonInfo; + @SerializedName("access_time") + private String accessTime; + @SerializedName("expired_time") + private String expiredTime; + @SerializedName("access_fail_reason") + private String accessFailReason; + @SerializedName("ability_info") + private List abilityInfo; + } + + @Data + public static class BillingPerson implements Serializable { + private static final long serialVersionUID = 1L; + private String id; + private String name; + } + + @Data + public static class Ability implements Serializable { + private static final long serialVersionUID = 1L; + private String type; + private String status; + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/TitleUrlRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/TitleUrlRequest.java new file mode 100644 index 0000000000..8bb1f71418 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/TitleUrlRequest.java @@ -0,0 +1,19 @@ +package com.github.binarywang.wxpay.bean.invoice; +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import java.io.Serializable; +/** 获取用户抬头填写链接请求。 */ +@Data public class TitleUrlRequest implements Serializable { + private static final long serialVersionUID = 1L; + @SerializedName("sub_mchid") private String subMchid; + @SerializedName("fapiao_apply_id") private String fapiaoApplyId; + private String source; + private String appid; + private String openid; + @SerializedName("total_amount") private Integer totalAmount; + @SerializedName("seller_name") private String sellerName; + @SerializedName("show_phone_cell") private Boolean showPhoneCell; + @SerializedName("must_input_phone") private Boolean mustInputPhone; + @SerializedName("show_email_cell") private Boolean showEmailCell; + @SerializedName("must_input_email") private Boolean mustInputEmail; +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/TitleUrlResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/TitleUrlResult.java new file mode 100644 index 0000000000..d0bdacbb74 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/TitleUrlResult.java @@ -0,0 +1,12 @@ +package com.github.binarywang.wxpay.bean.invoice; +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import java.io.Serializable; +/** 用户抬头填写小程序跳转信息。 */ +@Data public class TitleUrlResult implements Serializable { + private static final long serialVersionUID = 1L; + @SerializedName("title_url") private String titleUrl; + @SerializedName("miniprogram_appid") private String miniprogramAppid; + @SerializedName("miniprogram_path") private String miniprogramPath; + @SerializedName("miniprogram_user_name") private String miniprogramUserName; +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PartnerInvoiceService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PartnerInvoiceService.java new file mode 100644 index 0000000000..39390ba713 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PartnerInvoiceService.java @@ -0,0 +1,110 @@ +package com.github.binarywang.wxpay.service; + +import com.github.binarywang.wxpay.bean.invoice.InviteUrlResult; +import com.github.binarywang.wxpay.bean.invoice.InviteUrlRequest; +import com.github.binarywang.wxpay.bean.invoice.GeneralInvoiceRequest; +import com.github.binarywang.wxpay.bean.invoice.InvoiceResult; +import com.github.binarywang.wxpay.bean.invoice.InvoiceFileResult; +import com.github.binarywang.wxpay.bean.invoice.ReverseInvoiceRequest; +import com.github.binarywang.wxpay.bean.invoice.SubMerchantInvoiceStatus; +import com.github.binarywang.wxpay.bean.invoice.CardTemplateRequest; +import com.github.binarywang.wxpay.bean.invoice.CardTemplateResult; +import com.github.binarywang.wxpay.bean.invoice.DevelopmentConfigRequest; +import com.github.binarywang.wxpay.bean.invoice.DevelopmentConfigResult; +import com.github.binarywang.wxpay.bean.invoice.TitleUrlRequest; +import com.github.binarywang.wxpay.bean.invoice.TitleUrlResult; +import com.github.binarywang.wxpay.bean.invoice.BuyerInformation; +import com.github.binarywang.wxpay.bean.invoice.IndustryInvoiceRequest; +import com.github.binarywang.wxpay.bean.invoice.InsertCardRequest; +import com.github.binarywang.wxpay.bean.invoice.InviteMerchantQuery; +import com.github.binarywang.wxpay.bean.invoice.InviteMerchantResult; +import com.github.binarywang.wxpay.bean.invoice.InvoiceFileUploadRequest; +import com.github.binarywang.wxpay.bean.invoice.InvoiceFileUploadResult; +import java.io.IOException; +import com.github.binarywang.wxpay.exception.WxPayException; + +/** + * 微信支付服务商电子发票 API。 + * + * @author binarywang + */ +public interface PartnerInvoiceService { + + /** + * 获取开通服务商电子发票能力邀请链接。 + * + * @param subMchId 可选,指定要邀请开通能力的子商户号 + * @return 邀请链接 + * @throws WxPayException 微信支付异常 + * @see 官方文档 + */ + InviteUrlResult getInviteUrl(String subMchId) throws WxPayException; + + InviteUrlResult getInviteUrl(InviteUrlRequest request) throws WxPayException; + + /** + * 开具通用行业电子发票。接口受理成功后,请通过查询电子发票接口获取处理结果。 + * + * @param request 开票申请 + * @throws WxPayException 微信支付异常 + * @see 官方文档 + */ + void issueGeneralInvoice(GeneralInvoiceRequest request) throws WxPayException; + + /** + * 查询电子发票。 + * + * @param fapiaoApplyId 开票申请单号 + * @param subMchId 子商户号 + * @param fapiaoId 可选,商户发票单号 + * @return 发票信息 + * @throws WxPayException 微信支付异常 + */ + InvoiceResult getInvoice(String fapiaoApplyId, String subMchId, String fapiaoId) throws WxPayException; + + /** + * 冲红电子发票。 + * + * @param request 冲红申请 + * @throws WxPayException 微信支付异常 + */ + void reverseInvoice(ReverseInvoiceRequest request) throws WxPayException; + + /** + * 获取发票文件下载信息。 + * + * @param fapiaoApplyId 开票申请单号 + * @param subMchId 子商户号 + * @param fapiaoId 可选,商户发票单号 + * @return 发票文件下载信息 + * @throws WxPayException 微信支付异常 + */ + InvoiceFileResult getInvoiceFileDownloadInfo(String fapiaoApplyId, String subMchId, String fapiaoId) throws WxPayException; + + /** + * 检查子商户开票功能状态。 + * + * @param subMchId 子商户号 + * @return 子商户电子发票能力状态 + * @throws WxPayException 微信支付异常 + */ + SubMerchantInvoiceStatus getSubMerchantInvoiceStatus(String subMchId) throws WxPayException; + + CardTemplateResult createCardTemplate(CardTemplateRequest request) throws WxPayException; + + DevelopmentConfigResult updateDevelopmentConfig(DevelopmentConfigRequest request) throws WxPayException; + + TitleUrlResult getUserTitleUrl(TitleUrlRequest request) throws WxPayException; + + BuyerInformation getUserTitle(String subMchId, String scene, String fapiaoApplyId) throws WxPayException; + + void issueRealEstateLeasingInvoice(IndustryInvoiceRequest request) throws WxPayException; + + void issueRefinedOilInvoice(IndustryInvoiceRequest request) throws WxPayException; + + void insertCards(InsertCardRequest request) throws WxPayException; + + InviteMerchantResult listInviteMerchants(InviteMerchantQuery query) throws WxPayException; + + InvoiceFileUploadResult uploadInvoiceFile(InvoiceFileUploadRequest request) throws WxPayException, IOException; +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java index 9cf5aba4a4..2aec711831 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java @@ -408,6 +408,13 @@ default WxPayService switchoverTo(String mchIdOrConfigKey) { */ MerchantLimitationService getMerchantLimitationService(); + /** + * 获取服务商电子发票服务类。 + * + * @return 服务商电子发票服务 + */ + PartnerInvoiceService getPartnerInvoiceService(); + /** *
    * 查询订单.
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java
index 943894146c..d17c3ae698 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java
@@ -148,6 +148,9 @@ public abstract class BaseWxPayServiceImpl implements WxPayService {
   @Getter
   private final MerchantLimitationService merchantLimitationService = new MerchantLimitationServiceImpl(this);
 
+  @Getter
+  private final PartnerInvoiceService partnerInvoiceService = new PartnerInvoiceServiceImpl(this);
+
   protected Map configMap = new ConcurrentHashMap<>();
 
   @Override
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PartnerInvoiceServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PartnerInvoiceServiceImpl.java
new file mode 100644
index 0000000000..af173851f1
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PartnerInvoiceServiceImpl.java
@@ -0,0 +1,211 @@
+package com.github.binarywang.wxpay.service.impl;
+
+import com.github.binarywang.wxpay.bean.invoice.InviteUrlResult;
+import com.github.binarywang.wxpay.bean.invoice.InviteUrlRequest;
+import com.github.binarywang.wxpay.bean.invoice.GeneralInvoiceRequest;
+import com.github.binarywang.wxpay.bean.invoice.InvoiceResult;
+import com.github.binarywang.wxpay.bean.invoice.InvoiceFileResult;
+import com.github.binarywang.wxpay.bean.invoice.ReverseInvoiceRequest;
+import com.github.binarywang.wxpay.bean.invoice.SubMerchantInvoiceStatus;
+import com.github.binarywang.wxpay.bean.invoice.CardTemplateRequest;
+import com.github.binarywang.wxpay.bean.invoice.CardTemplateResult;
+import com.github.binarywang.wxpay.bean.invoice.DevelopmentConfigRequest;
+import com.github.binarywang.wxpay.bean.invoice.DevelopmentConfigResult;
+import com.github.binarywang.wxpay.bean.invoice.TitleUrlRequest;
+import com.github.binarywang.wxpay.bean.invoice.TitleUrlResult;
+import com.github.binarywang.wxpay.bean.invoice.BuyerInformation;
+import com.github.binarywang.wxpay.bean.invoice.IndustryInvoiceRequest;
+import com.github.binarywang.wxpay.bean.invoice.InsertCardRequest;
+import com.github.binarywang.wxpay.bean.invoice.InviteMerchantQuery;
+import com.github.binarywang.wxpay.bean.invoice.InviteMerchantResult;
+import com.github.binarywang.wxpay.bean.invoice.InvoiceFileUploadRequest;
+import com.github.binarywang.wxpay.bean.invoice.InvoiceFileUploadResult;
+import com.github.binarywang.wxpay.v3.WechatPayUploadHttpPost;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.PartnerInvoiceService;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.google.gson.JsonObject;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.io.UnsupportedEncodingException;
+import me.chanjar.weixin.common.util.json.WxGsonBuilder;
+
+/**
+ * 微信支付服务商电子发票 API 实现。
+ *
+ * @author binarywang
+ */
+@RequiredArgsConstructor
+public class PartnerInvoiceServiceImpl implements PartnerInvoiceService {
+  private static final com.google.gson.Gson GSON = WxGsonBuilder.create();
+  private static final String INVITE_URL_PATH = "/v3/new-tax-control-fapiao/fapiaomerchant/getspinviteurl";
+  private static final String ISSUE_GENERAL_PATH = "/v3/new-tax-control-fapiao/fapiao-applications/issue-general";
+  private static final String FAPIAO_APPLICATIONS_PATH = "/v3/new-tax-control-fapiao/fapiao-applications/";
+
+  private final WxPayService payService;
+
+  @Override
+  public InviteUrlResult getInviteUrl(String subMchId) throws WxPayException {
+    InviteUrlRequest request = new InviteUrlRequest();
+    request.setSubMchid(subMchId);
+    return getInviteUrl(request);
+  }
+
+  @Override
+  public InviteUrlResult getInviteUrl(InviteUrlRequest request) throws WxPayException {
+    String url = this.payService.getPayBaseUrl() + INVITE_URL_PATH;
+    if (StringUtils.isNotBlank(request.getSubMchid())) {
+      url += "?" + query("sub_mchid", request.getSubMchid());
+    }
+    url = appendQuery(url, "operation_type", request.getOperationType());
+    url = appendQuery(url, "fapiao_mode", request.getFapiaoMode());
+    if (request.getFapiaoAbilityTypeList() != null && !request.getFapiaoAbilityTypeList().isEmpty()) {
+      url = appendQuery(url, "fapiao_ability_type_list", String.join(",", request.getFapiaoAbilityTypeList()));
+    }
+    url = appendQuery(url, "invite_channel", request.getInviteChannel());
+    url = appendQuery(url, "operate_user", request.getOperateUser());
+    url = appendQuery(url, "invite_code", request.getInviteCode());
+    String response = this.payService.getV3(url);
+    return GSON.fromJson(response, InviteUrlResult.class);
+  }
+
+  @Override
+  public void issueGeneralInvoice(GeneralInvoiceRequest request) throws WxPayException {
+    String url = this.payService.getPayBaseUrl() + ISSUE_GENERAL_PATH;
+    this.payService.postV3(url, GSON.toJson(request));
+  }
+
+  @Override
+  public InvoiceResult getInvoice(String fapiaoApplyId, String subMchId, String fapiaoId) throws WxPayException {
+    String url = this.payService.getPayBaseUrl() + FAPIAO_APPLICATIONS_PATH + encode(fapiaoApplyId) + "?sub_mchid=" + encode(subMchId);
+    if (StringUtils.isNotBlank(fapiaoId)) {
+      url += "&fapiao_id=" + encode(fapiaoId);
+    }
+    return GSON.fromJson(this.payService.getV3(url), InvoiceResult.class);
+  }
+
+  @Override
+  public void reverseInvoice(ReverseInvoiceRequest request) throws WxPayException {
+    String url = this.payService.getPayBaseUrl() + FAPIAO_APPLICATIONS_PATH + encode(request.getFapiaoApplyId()) + "/reverse";
+    JsonObject body = GSON.toJsonTree(request).getAsJsonObject();
+    body.remove("fapiao_apply_id");
+    this.payService.postV3(url, GSON.toJson(body));
+  }
+
+  @Override
+  public InvoiceFileResult getInvoiceFileDownloadInfo(String fapiaoApplyId, String subMchId, String fapiaoId) throws WxPayException {
+    String url = this.payService.getPayBaseUrl() + FAPIAO_APPLICATIONS_PATH + encode(fapiaoApplyId) + "/fapiao-files?sub_mchid=" + encode(subMchId);
+    if (StringUtils.isNotBlank(fapiaoId)) {
+      url += "&fapiao_id=" + encode(fapiaoId);
+    }
+    return GSON.fromJson(this.payService.getV3(url), InvoiceFileResult.class);
+  }
+
+  @Override
+  public SubMerchantInvoiceStatus getSubMerchantInvoiceStatus(String subMchId) throws WxPayException {
+    String url = this.payService.getPayBaseUrl() + "/v3/new-tax-control-fapiao/merchant/" + encode(subMchId) + "/check-status";
+    return GSON.fromJson(this.payService.getV3(url), SubMerchantInvoiceStatus.class);
+  }
+
+  @Override
+  public CardTemplateResult createCardTemplate(CardTemplateRequest request) throws WxPayException {
+    String url = this.payService.getPayBaseUrl() + "/v3/new-tax-control-fapiao/card-template";
+    return GSON.fromJson(this.payService.postV3(url, GSON.toJson(request)), CardTemplateResult.class);
+  }
+
+  @Override
+  public DevelopmentConfigResult updateDevelopmentConfig(DevelopmentConfigRequest request) throws WxPayException {
+    String url = this.payService.getPayBaseUrl() + "/v3/new-tax-control-fapiao/merchant/development-config";
+    return GSON.fromJson(this.payService.patchV3(url, GSON.toJson(request)), DevelopmentConfigResult.class);
+  }
+
+  @Override
+  public TitleUrlResult getUserTitleUrl(TitleUrlRequest request) throws WxPayException {
+    String url = this.payService.getPayBaseUrl() + "/v3/new-tax-control-fapiao/user-title/title-url?"
+      + query("sub_mchid", request.getSubMchid()) + "&" + query("fapiao_apply_id", request.getFapiaoApplyId())
+      + "&" + query("source", request.getSource()) + "&" + query("appid", request.getAppid())
+      + "&" + query("openid", request.getOpenid()) + "&" + query("total_amount", request.getTotalAmount());
+    url = appendQuery(url, "seller_name", request.getSellerName());
+    url = appendQuery(url, "show_phone_cell", request.getShowPhoneCell());
+    url = appendQuery(url, "must_input_phone", request.getMustInputPhone());
+    url = appendQuery(url, "show_email_cell", request.getShowEmailCell());
+    url = appendQuery(url, "must_input_email", request.getMustInputEmail());
+    return GSON.fromJson(this.payService.getV3(url), TitleUrlResult.class);
+  }
+
+  @Override
+  public BuyerInformation getUserTitle(String subMchId, String scene, String fapiaoApplyId) throws WxPayException {
+    String url = this.payService.getPayBaseUrl() + "/v3/new-tax-control-fapiao/user-title?" + query("sub_mchid", subMchId)
+      + "&" + query("scene", scene) + "&" + query("fapiao_apply_id", fapiaoApplyId);
+    return GSON.fromJson(this.payService.getV3(url), BuyerInformation.class);
+  }
+
+  @Override
+  public void issueRealEstateLeasingInvoice(IndustryInvoiceRequest request) throws WxPayException {
+    this.payService.postV3(this.payService.getPayBaseUrl() + "/v3/new-tax-control-fapiao/fapiao-applications/real-estate-leasing", GSON.toJson(request));
+  }
+
+  @Override
+  public void issueRefinedOilInvoice(IndustryInvoiceRequest request) throws WxPayException {
+    this.payService.postV3(this.payService.getPayBaseUrl() + "/v3/new-tax-control-fapiao/fapiao-applications/issue-refined-oil", GSON.toJson(request));
+  }
+
+  @Override
+  public void insertCards(InsertCardRequest request) throws WxPayException {
+    String url = this.payService.getPayBaseUrl() + FAPIAO_APPLICATIONS_PATH + encode(request.getFapiaoApplyId()) + "/insert-cards";
+    JsonObject body = GSON.toJsonTree(request).getAsJsonObject();
+    body.remove("fapiao_apply_id");
+    this.payService.postV3(url, GSON.toJson(body));
+  }
+
+  @Override
+  public InviteMerchantResult listInviteMerchants(InviteMerchantQuery query) throws WxPayException {
+    String url = this.payService.getPayBaseUrl() + "/v3/new-tax-control-fapiao/fapiaomerchant/listspinvitemchinfo"
+      + "?" + query("query_time_start", query.getQueryTimeStart()) + "&" + query("query_time_end", query.getQueryTimeEnd())
+      + "&" + query("offset", query.getOffset()) + "&" + query("limit", query.getLimit()) + "&" + query("mch_invite_status", query.getMchInviteStatus());
+    if (StringUtils.isNotBlank(query.getInviteCode())) {
+      url += "&invite_code=" + encode(query.getInviteCode());
+    }
+    return GSON.fromJson(this.payService.getV3(url), InviteMerchantResult.class);
+  }
+
+  @Override
+  public InvoiceFileUploadResult uploadInvoiceFile(InvoiceFileUploadRequest request) throws WxPayException, IOException {
+    String url = this.payService.getPayBaseUrl() + FAPIAO_APPLICATIONS_PATH + "upload-fapiao-file";
+    JsonObject meta = new JsonObject();
+    meta.addProperty("sub_mchid", request.getSubMchid());
+    meta.addProperty("file_type", request.getFileType());
+    // 微信支付官方接口字段即为 digest_alogrithm(文档中的既定拼写)。
+    meta.addProperty("digest_alogrithm", request.getDigestAlgorithm());
+    meta.addProperty("digest", request.getDigest());
+    try (FileInputStream inputStream = new FileInputStream(request.getFile())) {
+      WechatPayUploadHttpPost post = new WechatPayUploadHttpPost.Builder(URI.create(url))
+        .withFapiaoFile(request.getFile().getName(), GSON.toJson(meta), inputStream).buildFapiaoFile();
+      return GSON.fromJson(this.payService.postV3(url, post), InvoiceFileUploadResult.class);
+    }
+  }
+
+  private static String query(String key, Object value) {
+    return key + "=" + encode(value);
+  }
+
+  private static String appendQuery(String url, String key, Object value) {
+    return value == null ? url : url + (url.contains("?") ? "&" : "?") + query(key, value);
+  }
+
+  private static String encode(Object value) {
+    if (value == null) {
+      throw new IllegalArgumentException("微信支付接口必填参数不能为空");
+    }
+    try {
+      return URLEncoder.encode(String.valueOf(value), StandardCharsets.UTF_8.name());
+    } catch (UnsupportedEncodingException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java
index 2745617e9b..12b85670f9 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java
@@ -138,7 +138,7 @@ private String requestV3(String url, String requestStr, HttpRequestBase httpRequ
         responseString = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
       }
 
-      if (HttpStatus.SC_OK == statusCode || HttpStatus.SC_NO_CONTENT == statusCode) {
+      if (HttpStatus.SC_OK == statusCode || HttpStatus.SC_NO_CONTENT == statusCode || HttpStatus.SC_ACCEPTED == statusCode) {
         this.logRequestAndResponse(url, requestStr, responseString);
         return responseString;
       }
@@ -210,7 +210,7 @@ public String requestV3(String url, HttpRequestBase httpRequest) throws WxPayExc
         responseString = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
       }
 
-      if (HttpStatus.SC_OK == statusCode || HttpStatus.SC_NO_CONTENT == statusCode) {
+      if (HttpStatus.SC_OK == statusCode || HttpStatus.SC_NO_CONTENT == statusCode || HttpStatus.SC_ACCEPTED == statusCode) {
         log.info("\n【请求地址】:{}\n【响应数据】:{}", url, responseString);
         return responseString;
       }
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceHttpComponentsImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceHttpComponentsImpl.java
index 2cfbac2ea0..cc5423302b 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceHttpComponentsImpl.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceHttpComponentsImpl.java
@@ -135,7 +135,7 @@ private String requestV3(String url, String requestStr, HttpRequestBase httpRequ
         responseString = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
       }
 
-      if (HttpStatus.SC_OK == statusCode || HttpStatus.SC_NO_CONTENT == statusCode) {
+      if (HttpStatus.SC_OK == statusCode || HttpStatus.SC_NO_CONTENT == statusCode || HttpStatus.SC_ACCEPTED == statusCode) {
         this.logRequestAndResponse(url, requestStr, responseString);
         return responseString;
       }
@@ -207,7 +207,7 @@ public String requestV3(String url, HttpRequestBase httpRequest) throws WxPayExc
         responseString = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
       }
 
-      if (HttpStatus.SC_OK == statusCode || HttpStatus.SC_NO_CONTENT == statusCode) {
+      if (HttpStatus.SC_OK == statusCode || HttpStatus.SC_NO_CONTENT == statusCode || HttpStatus.SC_ACCEPTED == statusCode) {
         log.info("\n【请求地址】:{}\n【响应数据】:{}", url, responseString);
         return responseString;
       }
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WechatPayUploadHttpPost.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WechatPayUploadHttpPost.java
index 3387f37e3d..5ce8728fa6 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WechatPayUploadHttpPost.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WechatPayUploadHttpPost.java
@@ -30,6 +30,7 @@ public static class Builder {
     private InputStream fileInputStream;
     private ContentType fileContentType;
     private URI uri;
+    private String customMeta;
 
     public Builder(URI uri) {
       this.uri = uri;
@@ -58,6 +59,32 @@ public Builder withVideo(String fileName, String fileSha256, InputStream inputSt
       return withMedia(fileName, fileSha256, inputStream);
     }
 
+    /**
+     * 服务商电子发票文件上传。该接口签名和 multipart 的 meta 均使用业务元信息。
+     */
+    public Builder withFapiaoFile(String fileName, String meta, InputStream inputStream) {
+      this.fileName = fileName;
+      this.fileInputStream = inputStream;
+      this.customMeta = meta;
+      String mimeType = URLConnection.guessContentTypeFromName(fileName);
+      this.fileContentType = ContentType.create(mimeType == null ? "application/octet-stream" : mimeType);
+      return this;
+    }
+
+    public WechatPayUploadHttpPost buildFapiaoFile() {
+      if (fileName == null || fileInputStream == null || customMeta == null || uri == null) {
+        throw new IllegalArgumentException("缺少电子发票文件上传信息");
+      }
+      WechatPayUploadHttpPost request = new WechatPayUploadHttpPost(uri, customMeta);
+      MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create();
+      entityBuilder.setMode(HttpMultipartMode.RFC6532)
+        .addBinaryBody("file", fileInputStream, fileContentType, fileName)
+        .addTextBody("meta", customMeta, ContentType.APPLICATION_JSON);
+      request.setEntity(entityBuilder.build());
+      request.addHeader("Accept", ContentType.APPLICATION_JSON.toString());
+      return request;
+    }
+
     public WechatPayUploadHttpPost build() {
       if (fileName == null || fileSha256 == null || fileInputStream == null) {
         throw new IllegalArgumentException("缺少待上传文件信息");
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/invoice/GeneralInvoiceRequestTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/invoice/GeneralInvoiceRequestTest.java
new file mode 100644
index 0000000000..bd19f5a18a
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/invoice/GeneralInvoiceRequestTest.java
@@ -0,0 +1,59 @@
+package com.github.binarywang.wxpay.bean.invoice;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import org.testng.annotations.Test;
+
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * {@link GeneralInvoiceRequest} 单元测试。
+ */
+public class GeneralInvoiceRequestTest {
+
+  @Test
+  public void shouldSerializeGeneralInvoiceRequestUsingWechatPayFieldNames() {
+    GeneralInvoiceRequest request = new GeneralInvoiceRequest();
+    request.setSubMchid("1900000109");
+    request.setFapiaoApplyId("apply_20260718_001");
+
+    BuyerInformation buyer = new BuyerInformation();
+    buyer.setType("ORGANIZATION");
+    buyer.setName("示例企业");
+    buyer.setTaxpayerId("91440300TEST00001");
+    request.setBuyerInformation(buyer);
+
+    GeneralInvoiceRequest.InvoiceItem item = new GeneralInvoiceRequest.InvoiceItem();
+    item.setTaxCode("3010101020203000000");
+    item.setGoodsName("技术服务");
+    item.setTotalAmount(100);
+    item.setTaxRate(600);
+    item.setDiscount(false);
+
+    GeneralInvoiceRequest.FapiaoInformation invoice = new GeneralInvoiceRequest.FapiaoInformation();
+    invoice.setFapiaoId("invoice_20260718_001");
+    invoice.setTotalAmount(100);
+    invoice.setItems(Collections.singletonList(item));
+    invoice.setBillingPersonId("billing_person_001");
+    invoice.setFapiaoBillType("COMM_FAPIAO");
+
+    GeneralInvoiceRequest.TransactionInformation transaction = new GeneralInvoiceRequest.TransactionInformation();
+    transaction.setPayChannel("WECHAT_PAY");
+    transaction.setOutTradeNo("order_20260718_001");
+    transaction.setAmount(100);
+    invoice.setTransactionInformation(Collections.singletonList(transaction));
+    request.setFapiaoInformation(invoice);
+
+    JsonObject json = new Gson().toJsonTree(request).getAsJsonObject();
+    assertThat(json.get("sub_mchid").getAsString()).isEqualTo("1900000109");
+    assertThat(json.get("fapiao_apply_id").getAsString()).isEqualTo("apply_20260718_001");
+    assertThat(json.getAsJsonObject("buyer_information").get("taxpayer_id").getAsString())
+      .isEqualTo("91440300TEST00001");
+    assertThat(json.getAsJsonObject("fapiao_information").getAsJsonArray("items").get(0)
+      .getAsJsonObject().get("tax_code").getAsString()).isEqualTo("3010101020203000000");
+    assertThat(json.getAsJsonObject("fapiao_information").getAsJsonArray("transaction_information").get(0)
+      .getAsJsonObject().get("out_trade_no").getAsString()).isEqualTo("order_20260718_001");
+  }
+}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/PartnerInvoiceServiceImplTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/PartnerInvoiceServiceImplTest.java
new file mode 100644
index 0000000000..f0aa2d16a5
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/PartnerInvoiceServiceImplTest.java
@@ -0,0 +1,195 @@
+package com.github.binarywang.wxpay.service.impl;
+
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.github.binarywang.wxpay.bean.invoice.GeneralInvoiceRequest;
+import com.github.binarywang.wxpay.bean.invoice.InvoiceResult;
+import com.github.binarywang.wxpay.bean.invoice.ReverseInvoiceRequest;
+import com.github.binarywang.wxpay.bean.invoice.InvoiceFileResult;
+import com.github.binarywang.wxpay.bean.invoice.SubMerchantInvoiceStatus;
+import com.github.binarywang.wxpay.bean.invoice.TitleUrlRequest;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import java.lang.reflect.Proxy;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * {@link PartnerInvoiceServiceImpl} 测试。
+ */
+public class PartnerInvoiceServiceImplTest {
+
+  @Test
+  public void shouldRequestInviteUrlWithOptionalSubMchId() throws Exception {
+    AtomicReference requestedUrl = new AtomicReference<>();
+    WxPayService payService = (WxPayService) Proxy.newProxyInstance(
+      getClass().getClassLoader(), new Class[]{WxPayService.class}, (proxy, method, args) -> {
+        if ("getPayBaseUrl".equals(method.getName())) {
+          return "https://api.mch.weixin.qq.com";
+        }
+        if ("getV3".equals(method.getName())) {
+          requestedUrl.set((String) args[0]);
+          return "{\"invite_url\":\"https://wxacurl.cn?xxx=\"}";
+        }
+        throw new UnsupportedOperationException(method.getName());
+      });
+
+    PartnerInvoiceServiceImpl service = new PartnerInvoiceServiceImpl(payService);
+
+    Assert.assertEquals(service.getInviteUrl("19998278783").getInviteUrl(), "https://wxacurl.cn?xxx=");
+    Assert.assertEquals(requestedUrl.get(),
+      "https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/fapiaomerchant/getspinviteurl?sub_mchid=19998278783");
+  }
+
+  @Test
+  public void shouldPostGeneralInvoiceToV3Endpoint() throws Exception {
+    AtomicReference requestedUrl = new AtomicReference<>();
+    AtomicReference requestedBody = new AtomicReference<>();
+    WxPayService payService = (WxPayService) Proxy.newProxyInstance(
+      getClass().getClassLoader(), new Class[]{WxPayService.class}, (proxy, method, args) -> {
+        if ("getPayBaseUrl".equals(method.getName())) {
+          return "https://api.mch.weixin.qq.com";
+        }
+        if ("postV3".equals(method.getName())) {
+          requestedUrl.set((String) args[0]);
+          requestedBody.set((String) args[1]);
+          return "";
+        }
+        throw new UnsupportedOperationException(method.getName());
+      });
+    GeneralInvoiceRequest request = new GeneralInvoiceRequest();
+    request.setSubMchid("1900000109");
+    request.setFapiaoApplyId("invoice-001");
+
+    new PartnerInvoiceServiceImpl(payService).issueGeneralInvoice(request);
+
+    Assert.assertEquals(requestedUrl.get(),
+      "https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/fapiao-applications/issue-general");
+    Assert.assertTrue(requestedBody.get().contains("\"fapiao_apply_id\":\"invoice-001\""));
+  }
+
+  @Test
+  public void shouldQueryInvoiceWithRequiredSubMchId() throws Exception {
+    AtomicReference requestedUrl = new AtomicReference<>();
+    WxPayService payService = (WxPayService) Proxy.newProxyInstance(
+      getClass().getClassLoader(), new Class[]{WxPayService.class}, (proxy, method, args) -> {
+        if ("getPayBaseUrl".equals(method.getName())) {
+          return "https://api.mch.weixin.qq.com";
+        }
+        if ("getV3".equals(method.getName())) {
+          requestedUrl.set((String) args[0]);
+          return "{\"total_count\":1,\"fapiao_information\":[{\"fapiao_id\":\"fapiao-001\",\"status\":\"ISSUED\"}]}";
+        }
+        throw new UnsupportedOperationException(method.getName());
+      });
+
+    InvoiceResult result = new PartnerInvoiceServiceImpl(payService)
+      .getInvoice("apply-001", "1900000109", "fapiao-001");
+
+    Assert.assertEquals(requestedUrl.get(), "https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/fapiao-applications/apply-001?sub_mchid=1900000109&fapiao_id=fapiao-001");
+    Assert.assertEquals(result.getTotalCount(), Integer.valueOf(1));
+    Assert.assertEquals(result.getFapiaoInformation().get(0).getStatus(), "ISSUED");
+  }
+
+  @Test
+  public void shouldReverseInvoiceUsingPathApplyId() throws Exception {
+    AtomicReference requestedUrl = new AtomicReference<>();
+    AtomicReference requestedBody = new AtomicReference<>();
+    WxPayService payService = (WxPayService) Proxy.newProxyInstance(
+      getClass().getClassLoader(), new Class[]{WxPayService.class}, (proxy, method, args) -> {
+        if ("getPayBaseUrl".equals(method.getName())) {
+          return "https://api.mch.weixin.qq.com";
+        }
+        if ("postV3".equals(method.getName())) {
+          requestedUrl.set((String) args[0]);
+          requestedBody.set((String) args[1]);
+          return "";
+        }
+        throw new UnsupportedOperationException(method.getName());
+      });
+    ReverseInvoiceRequest request = new ReverseInvoiceRequest();
+    request.setFapiaoApplyId("apply-001");
+    request.setSubMchid("1900000109");
+    request.setReverseReason("SALES_RETURN");
+
+    new PartnerInvoiceServiceImpl(payService).reverseInvoice(request);
+
+    Assert.assertEquals(requestedUrl.get(),
+      "https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/fapiao-applications/apply-001/reverse");
+    Assert.assertTrue(requestedBody.get().contains("\"sub_mchid\":\"1900000109\""));
+    Assert.assertFalse(requestedBody.get().contains("fapiao_apply_id"));
+  }
+
+  @Test
+  public void shouldGetInvoiceFileDownloadInfo() throws Exception {
+    AtomicReference requestedUrl = new AtomicReference<>();
+    WxPayService payService = (WxPayService) Proxy.newProxyInstance(
+      getClass().getClassLoader(), new Class[]{WxPayService.class}, (proxy, method, args) -> {
+        if ("getPayBaseUrl".equals(method.getName())) {
+          return "https://api.mch.weixin.qq.com";
+        }
+        if ("getV3".equals(method.getName())) {
+          requestedUrl.set((String) args[0]);
+          return "{\"fapiao_download_info_list\":[{\"fapiao_id\":\"fapiao-001\",\"download_url\":\"https://download.example.com/file\",\"status\":\"ISSUED\"}]}";
+        }
+        throw new UnsupportedOperationException(method.getName());
+      });
+
+    InvoiceFileResult result = new PartnerInvoiceServiceImpl(payService)
+      .getInvoiceFileDownloadInfo("apply-001", "1900000109", "fapiao-001");
+
+    Assert.assertEquals(requestedUrl.get(), "https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/fapiao-applications/apply-001/fapiao-files?sub_mchid=1900000109&fapiao_id=fapiao-001");
+    Assert.assertEquals(result.getFapiaoDownloadInfoList().get(0).getDownloadUrl(), "https://download.example.com/file");
+  }
+
+  @Test
+  public void shouldCheckSubMerchantInvoiceStatus() throws Exception {
+    AtomicReference requestedUrl = new AtomicReference<>();
+    WxPayService payService = (WxPayService) Proxy.newProxyInstance(
+      getClass().getClassLoader(), new Class[]{WxPayService.class}, (proxy, method, args) -> {
+        if ("getPayBaseUrl".equals(method.getName())) {
+          return "https://api.mch.weixin.qq.com";
+        }
+        if ("getV3".equals(method.getName())) {
+          requestedUrl.set((String) args[0]);
+          return "{\"sub_mchid\":\"1900000109\",\"third_mode\":{\"status\":\"ENABLED\"}}";
+        }
+        throw new UnsupportedOperationException(method.getName());
+      });
+
+    SubMerchantInvoiceStatus result = new PartnerInvoiceServiceImpl(payService).getSubMerchantInvoiceStatus("1900000109");
+
+    Assert.assertEquals(requestedUrl.get(), "https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/merchant/1900000109/check-status");
+    Assert.assertEquals(result.getThirdMode().getStatus(), "ENABLED");
+  }
+
+  @Test
+  public void shouldEncodeTitleUrlParametersAndIncludeOptionalFields() throws Exception {
+    AtomicReference requestedUrl = new AtomicReference<>();
+    WxPayService payService = (WxPayService) Proxy.newProxyInstance(
+      getClass().getClassLoader(), new Class[]{WxPayService.class}, (proxy, method, args) -> {
+        if ("getPayBaseUrl".equals(method.getName())) {
+          return "https://api.mch.weixin.qq.com";
+        }
+        if ("getV3".equals(method.getName())) {
+          requestedUrl.set((String) args[0]);
+          return "{}";
+        }
+        throw new UnsupportedOperationException(method.getName());
+      });
+    TitleUrlRequest request = new TitleUrlRequest();
+    request.setSubMchid("1900000109");
+    request.setFapiaoApplyId("apply|001");
+    request.setSource("WEB");
+    request.setAppid("wx123");
+    request.setOpenid("openid");
+    request.setTotalAmount(100);
+    request.setSellerName("测试 商户");
+    request.setShowPhoneCell(true);
+
+    new PartnerInvoiceServiceImpl(payService).getUserTitleUrl(request);
+
+    Assert.assertTrue(requestedUrl.get().contains("fapiao_apply_id=apply%7C001"));
+    Assert.assertTrue(requestedUrl.get().contains("seller_name=%E6%B5%8B%E8%AF%95+%E5%95%86%E6%88%B7"));
+    Assert.assertTrue(requestedUrl.get().contains("show_phone_cell=true"));
+  }
+}