1
0
mirror of synced 2026-10-17 04:46:10 +08:00

🆕 #4070【微信支付】新增 V3服务商电子发票能力封装,覆盖邀约、抬头、开票、冲红、文件上传/下载等核心接口

This commit is contained in:
Binary Wang
2026-07-18 20:54:53 +08:00
committed by GitHub
parent 6d5843489d
commit 2d496ed309
30 changed files with 1183 additions and 4 deletions

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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 <a href="https://pay.weixin.qq.com/doc/v3/partner/4015792574">开具通用行业电子发票</a>
*/
@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<InvoiceItem> 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> 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;
}
}

View File

@@ -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;
}

View File

@@ -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<Object> fapiaoCardInformation;
}

View File

@@ -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;
}

View File

@@ -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<Merchant> 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;
}
}

View File

@@ -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<String> fapiaoAbilityTypeList;
private String inviteChannel;
private String operateUser;
private String inviteCode;
}

View File

@@ -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;
}

View File

@@ -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 <a href="https://pay.weixin.qq.com/doc/v3/partner/4015792576">获取发票下载信息</a>
*/
@Data
public class InvoiceFileResult implements Serializable {
private static final long serialVersionUID = 1L;
@SerializedName("fapiao_download_info_list")
private List<DownloadInfo> 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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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 <a href="https://pay.weixin.qq.com/doc/v3/partner/4015792567">查询电子发票</a>
*/
@Data
public class InvoiceResult implements Serializable {
private static final long serialVersionUID = 1L;
@SerializedName("total_count")
private Integer totalCount;
@SerializedName("fapiao_information")
private List<InvoiceInformation> 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;
}
}

View File

@@ -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 <a href="https://pay.weixin.qq.com/doc/v3/partner/4015792575">冲红电子发票</a>
*/
@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<InvoiceInfo> 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;
}
}

View File

@@ -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<BillingPerson> 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<Ability> 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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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 <a href="https://pay.weixin.qq.com/doc/v3/partner/4015941495">官方文档</a>
*/
InviteUrlResult getInviteUrl(String subMchId) throws WxPayException;
InviteUrlResult getInviteUrl(InviteUrlRequest request) throws WxPayException;
/**
* 开具通用行业电子发票。接口受理成功后,请通过查询电子发票接口获取处理结果。
*
* @param request 开票申请
* @throws WxPayException 微信支付异常
* @see <a href="https://pay.weixin.qq.com/doc/v3/partner/4015792574">官方文档</a>
*/
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;
}

View File

@@ -408,6 +408,13 @@ public interface WxPayService {
*/
MerchantLimitationService getMerchantLimitationService();
/**
* 获取服务商电子发票服务类。
*
* @return 服务商电子发票服务
*/
PartnerInvoiceService getPartnerInvoiceService();
/**
* <pre>
* 查询订单.

View File

@@ -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<String, WxPayConfig> configMap = new ConcurrentHashMap<>();
@Override

View File

@@ -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);
}
}
}

View File

@@ -138,7 +138,7 @@ public class WxPayServiceApacheHttpImpl extends BaseWxPayServiceImpl {
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 class WxPayServiceApacheHttpImpl extends BaseWxPayServiceImpl {
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;
}

View File

@@ -135,7 +135,7 @@ public class WxPayServiceHttpComponentsImpl extends BaseWxPayServiceImpl {
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 class WxPayServiceHttpComponentsImpl extends BaseWxPayServiceImpl {
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;
}

View File

@@ -30,6 +30,7 @@ public class WechatPayUploadHttpPost extends HttpPost {
private InputStream fileInputStream;
private ContentType fileContentType;
private URI uri;
private String customMeta;
public Builder(URI uri) {
this.uri = uri;
@@ -58,6 +59,32 @@ public class WechatPayUploadHttpPost extends HttpPost {
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("缺少待上传文件信息");

View File

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

View File

@@ -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<String> 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<String> requestedUrl = new AtomicReference<>();
AtomicReference<String> 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<String> 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<String> requestedUrl = new AtomicReference<>();
AtomicReference<String> 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<String> 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<String> 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<String> 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"));
}
}