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"));
+ }
+}