From f62ad6c3060a78ab62714eb2a4b9e3ec1917d35a Mon Sep 17 00:00:00 2001 From: leiguoqing <191789784@qq.com> Date: Sun, 24 Apr 2022 22:37:10 +0800 Subject: [PATCH 1/3] =?UTF-8?q?:new:=20#2615=20=E3=80=90=E4=BC=81=E4=B8=9A?= =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E3=80=91=E5=A2=9E=E5=8A=A0=E4=BC=81=E4=B8=9A?= =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=BA=94=E7=94=A8=E5=B8=82=E5=9C=BA=E4=BB=98?= =?UTF-8?q?=E8=B4=B9=E8=AE=A2=E5=8D=95=E3=80=81=E7=89=88=E6=9C=AC=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E6=8E=A5=E5=8F=A3=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 获取订单详情 - 获取订单列表 - 延长试用期 --- .../cp/bean/WxCpTpProlongTryResult.java | 49 ++++ .../cp/bean/order/WxCpTpOrderDetails.java | 210 ++++++++++++++++++ .../bean/order/WxCpTpOrderListGetResult.java | 53 +++++ .../weixin/cp/constant/WxCpApiPathConsts.java | 9 + .../cp/tp/service/WxCpTpEditionService.java | 33 +++ .../cp/tp/service/WxCpTpOrderService.java | 44 ++++ .../weixin/cp/tp/service/WxCpTpService.java | 27 +++ .../service/impl/BaseWxCpTpServiceImpl.java | 22 ++ .../impl/WxCpTpEditionServiceImpl.java | 52 +++++ .../service/impl/WxCpTpOrderServiceImpl.java | 71 ++++++ .../impl/WxCpTpEditionServiceImplTest.java | 70 ++++++ .../impl/WxCpTpOrderServiceImplTest.java | 156 +++++++++++++ 12 files changed, 796 insertions(+) create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpProlongTryResult.java create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/order/WxCpTpOrderDetails.java create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/order/WxCpTpOrderListGetResult.java create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpEditionService.java create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpOrderService.java create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpEditionServiceImpl.java create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpOrderServiceImpl.java create mode 100644 weixin-java-cp/src/test/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpEditionServiceImplTest.java create mode 100644 weixin-java-cp/src/test/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpOrderServiceImplTest.java diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpProlongTryResult.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpProlongTryResult.java new file mode 100644 index 0000000000..1429b8296e --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpProlongTryResult.java @@ -0,0 +1,49 @@ +package me.chanjar.weixin.cp.bean; + +import com.google.gson.annotations.SerializedName; +import lombok.Getter; +import lombok.Setter; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +/** + * 应用市场延长试用期结果 + * @author leiguoqing + * @date 2022年4月24日 + */ +@Getter +@Setter +public class WxCpTpProlongTryResult extends WxCpBaseResp { + + /** + * The constant serialVersionUID. + */ + private static final long serialVersionUID = -5028321625140879571L; + + /** + * 延长后的试用到期时间(秒级时间戳) + */ + @SerializedName("try_end_time") + private Long tryEndTime; + + + /** + * From json wx cp tp order list get result. + * + * @param json the json + * @return the wx cp tp order list get result + */ + public static WxCpTpProlongTryResult fromJson(String json) { + return WxCpGsonBuilder.create().fromJson(json, WxCpTpProlongTryResult.class); + } + + /** + * To json string. + * + * @return the string + */ + @Override + public String toJson() { + return WxCpGsonBuilder.create().toJson(this); + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/order/WxCpTpOrderDetails.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/order/WxCpTpOrderDetails.java new file mode 100644 index 0000000000..3b468384b5 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/order/WxCpTpOrderDetails.java @@ -0,0 +1,210 @@ +package me.chanjar.weixin.cp.bean.order; + +import com.google.gson.annotations.SerializedName; +import lombok.Getter; +import lombok.Setter; +import me.chanjar.weixin.cp.bean.WxCpBaseResp; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +/** + * 应用版本付费订单详情 + * + * @author leiguoqing + * @date 2022年4月24日 + */ +@Getter +@Setter +public class WxCpTpOrderDetails extends WxCpBaseResp { + + /** + * The constant serialVersionUID. + */ + private static final long serialVersionUID = -5028321625140879571L; + + /** + * 订单号 + */ + @SerializedName("orderid") + private String orderId; + + /** + * 订单状态。0-未支付,1-已支付,2-已关闭, 3-未支付且已过期, 4-申请退款中, 5-申请退款成功, 6-退款被拒绝 + */ + @SerializedName("order_status") + private Integer orderStatus; + + /** + * 订单类型。0-普通订单,1-扩容订单,2-续期,3-版本变更 + */ + @SerializedName("order_type") + private Integer orderType; + + /** + * 客户企业的corpid + */ + @SerializedName("paid_corpid") + private String paidCorpId; + + /** + * 下单操作人员userid。如果是服务商代下单,没有该字段。 + */ + @SerializedName("operator_id") + private String operatorId; + + + /** + * 应用id + */ + @SerializedName("suiteid") + private String suiteId; + + + /** + * 应用id。(仅旧套件有该字段) + */ + @SerializedName("appid") + private String appId; + + + /** + * 购买版本ID + */ + @SerializedName("edition_id") + private String editionId; + + + /** + * 购买版本名字 + */ + @SerializedName("edition_name") + private String editionName; + + + /** + * 实付款金额,单位分 + */ + @SerializedName("price") + private Long price; + + + /** + * 购买的人数 + */ + @SerializedName("user_count") + private Integer userCount; + + + /** + * 购买的时间,单位天 + */ + @SerializedName("order_period") + private Integer orderPeriod; + + /** + * 下单时间,秒级时间戳 + */ + @SerializedName("order_time") + private Long orderTime; + + /** + * 付款时间,秒级时间戳 + */ + @SerializedName("paid_time") + private Long paidTime; + + + /** + * 购买生效期的开始时间,秒级时间戳 + */ + @SerializedName("begin_time") + private Long beginTime; + + + /** + * 购买生效期的结束时间,秒级时间戳 + */ + @SerializedName("end_time") + private Long endTime; + + /** + * 下单来源。0-客户下单;1-服务商代下单;2-代理商代下单 + */ + @SerializedName("order_from") + private Integer orderFrom; + + + /** + * 下单方corpid + */ + @SerializedName("operator_corpid") + private String operatorCorpId; + + /** + * 服务商分成金额,单位分 + */ + @SerializedName("service_share_amount") + private Long serviceShareAmount; + + + /** + * 平台分成金额,单位分 + */ + @SerializedName("platform_share_amount") + private Long platformShareAmount; + + + /** + * 代理商分成金额,单位分 + */ + @SerializedName("dealer_share_amount") + private Long dealerShareAmount; + + + /** + * 渠道商信息(仅当有渠道商报备后才会有此字段) + */ + @SerializedName("dealer_corp_info") + private DealerCorpInfo dealerCorpInfo; + + + /** + * 渠道商信息(仅当有渠道商报备后才会有此字段) + */ + @Getter + @Setter + public static class DealerCorpInfo { + /** + * 代理商corpid + */ + @SerializedName("corpid") + private String corpId; + + + /** + * 代理商名 + */ + @SerializedName("corp_name") + private String corpName; + } + + /** + * From json wx cp tp order details. + * + * @param json the json + * @return the wx cp tp order details + */ + public static WxCpTpOrderDetails fromJson(String json) { + return WxCpGsonBuilder.create().fromJson(json, WxCpTpOrderDetails.class); + } + + /** + * To json string. + * + * @return the string + */ + @Override + public String toJson() { + return WxCpGsonBuilder.create().toJson(this); + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/order/WxCpTpOrderListGetResult.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/order/WxCpTpOrderListGetResult.java new file mode 100644 index 0000000000..341ba9bc94 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/order/WxCpTpOrderListGetResult.java @@ -0,0 +1,53 @@ +package me.chanjar.weixin.cp.bean.order; + +import com.google.gson.annotations.SerializedName; +import lombok.Getter; +import lombok.Setter; +import me.chanjar.weixin.cp.bean.WxCpBaseResp; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import java.util.List; + +/** + * 应用版本付费订单列表 + * + * @author leiguoqing + * @date 2022年4月24日 + */ +@Getter +@Setter +public class WxCpTpOrderListGetResult extends WxCpBaseResp { + + /** + * The constant serialVersionUID. + */ + private static final long serialVersionUID = -5028321625140879571L; + + /** + * 订单列表 + */ + @SerializedName("order_list") + private List orderList; + + + /** + * From json wx cp tp order list get result. + * + * @param json the json + * @return the wx cp tp order list get result + */ + public static WxCpTpOrderListGetResult fromJson(String json) { + return WxCpGsonBuilder.create().fromJson(json, WxCpTpOrderListGetResult.class); + } + + /** + * To json string. + * + * @return the string + */ + @Override + public String toJson() { + return WxCpGsonBuilder.create().toJson(this); + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpApiPathConsts.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpApiPathConsts.java index 4d5527861c..d821484219 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpApiPathConsts.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpApiPathConsts.java @@ -204,6 +204,15 @@ interface Tp { String CONTACT_SEARCH = "/cgi-bin/service/contact/search"; String GET_ADMIN_LIST = "/cgi-bin/service/get_admin_list"; + // 获取订单详情 + String GET_ORDER = "/cgi-bin/service/get_order"; + + // 获取订单列表 + String GET_ORDER_LIST = "/cgi-bin/service/get_order_list"; + + // 延长试用期 + String PROLONG_TRY = "/cgi-bin/service/prolong_try"; + } interface User { diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpEditionService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpEditionService.java new file mode 100644 index 0000000000..4f88e9471b --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpEditionService.java @@ -0,0 +1,33 @@ +package me.chanjar.weixin.cp.tp.service; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.WxCpTpProlongTryResult; + + +/** + * 应用版本付费版本相关接口 + * + * @author leiguoqing + * @date 2022年4月24日 + */ +public interface WxCpTpEditionService { + + /** + * 延长试用期 + *

+ * 文档地址 + *

+ * 注意: + *

+ * + * @param buyerCorpId 购买方corpId + * @param prolongDays 延长天数 + * @param appId 仅旧套件需要填此参数 + * @return the order + * @throws WxErrorException the wx error exception + */ + WxCpTpProlongTryResult prolongTry(String buyerCorpId, Integer prolongDays, String appId) throws WxErrorException; +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpOrderService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpOrderService.java new file mode 100644 index 0000000000..d2288860cf --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpOrderService.java @@ -0,0 +1,44 @@ +package me.chanjar.weixin.cp.tp.service; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.order.WxCpTpOrderDetails; +import me.chanjar.weixin.cp.bean.order.WxCpTpOrderListGetResult; + +import java.util.Date; + + +/** + * 应用版本付费订单相关接口 + * + * @author leiguoqing + * @date 2022年4月24日 + */ +public interface WxCpTpOrderService { + + /** + * 获取订单详情 + *

+ * 文档地址 + *

+ * + * @param orderId 订单号 + * @return the order + * @throws WxErrorException the wx error exception + */ + WxCpTpOrderDetails getOrder(String orderId) throws WxErrorException; + + + /** + * 获取订单列表 + *

+ * 文档地址 + *

+ * + * @param startTime 起始时间 + * @param endTime 终止时间 + * @param testMode 指定拉取正式或测试模式的订单。默认正式模式。0-正式模式,1-测试模式。 + * @return the order + * @throws WxErrorException the wx error exception + */ + WxCpTpOrderListGetResult getOrderList(Date startTime, Date endTime, Integer testMode) throws WxErrorException; +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpService.java index bd44911feb..37405b2e92 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpService.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpService.java @@ -530,4 +530,31 @@ public interface WxCpTpService { */ void expireProviderToken(); + /** + * 获取应用版本付费订单相关接口服务 + * + * @return the wx cp tp order service + */ + WxCpTpOrderService getWxCpTpOrderService(); + + /** + * 设置应用版本付费订单相关接口服务 + * + * @param wxCpTpOrderService the wx cp tp order service + */ + void setWxCpTpOrderService(WxCpTpOrderService wxCpTpOrderService); + + /** + * 获取应用版本付费版本相关接口服务 + * + * @return the wx cp tp edition service + */ + WxCpTpEditionService getWxCpTpEditionService(); + + /** + * 设置应用版本付费版本相关接口服务 + * + * @param wxCpTpEditionService the wx cp tp edition service + */ + void setWxCpTpOrderService(WxCpTpEditionService wxCpTpEditionService); } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/BaseWxCpTpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/BaseWxCpTpServiceImpl.java index 03316b9ae2..a4dbd12480 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/BaseWxCpTpServiceImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/BaseWxCpTpServiceImpl.java @@ -48,6 +48,8 @@ public abstract class BaseWxCpTpServiceImpl implements WxCpTpService, Requ private WxCpTpMediaService wxCpTpMediaService = new WxCpTpMediaServiceImpl(this); private WxCpTpOAService wxCpTpOAService = new WxCpTpOAServiceImpl(this); private WxCpTpUserService wxCpTpUserService = new WxCpTpUserServiceImpl(this); + private WxCpTpOrderService wxCpTpOrderService = new WxCpTpOrderServiceImpl(this); + private WxCpTpEditionService wxCpTpEditionService = new WxCpTpEditionServiceImpl(this); /** * 全局的是否正在刷新access token的锁. @@ -594,6 +596,26 @@ public void expireProviderToken() { this.configStorage.expireProviderToken(); } + @Override + public WxCpTpOrderService getWxCpTpOrderService() { + return wxCpTpOrderService; + } + + @Override + public void setWxCpTpOrderService(WxCpTpOrderService wxCpTpOrderService) { + this.wxCpTpOrderService = wxCpTpOrderService; + } + + @Override + public WxCpTpEditionService getWxCpTpEditionService() { + return wxCpTpEditionService; + } + + @Override + public void setWxCpTpOrderService(WxCpTpEditionService wxCpTpEditionService) { + this.wxCpTpEditionService = wxCpTpEditionService; + } + private WxJsapiSignature doCreateWxJsapiSignature(String url, String authCorpId, String jsapiTicket) { long timestamp = System.currentTimeMillis() / 1000; String noncestr = RandomUtils.getRandomStr(); diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpEditionServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpEditionServiceImpl.java new file mode 100644 index 0000000000..8a6a71352b --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpEditionServiceImpl.java @@ -0,0 +1,52 @@ +package me.chanjar.weixin.cp.tp.service.impl; + +import com.google.gson.JsonObject; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.WxCpTpProlongTryResult; +import me.chanjar.weixin.cp.tp.service.WxCpTpEditionService; +import me.chanjar.weixin.cp.tp.service.WxCpTpService; + +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Tp.PROLONG_TRY; + +/** + * 应用版本付费版本相关接口实现 + * + * @author leigouqing + * @date 2022年4月24日 + */ +@RequiredArgsConstructor +public class WxCpTpEditionServiceImpl implements WxCpTpEditionService { + + /** + * The Main service. + */ + private final WxCpTpService mainService; + + /** + * 延长试用期 + *

+ * 文档地址 + *

+ *

+ * + * @param buyerCorpId 购买方corpId + * @param prolongDays 延长天数 + * @param appId 仅旧套件需要填此参数 + * @return the order + * @throws WxErrorException the wx error exception + */ + @Override + public WxCpTpProlongTryResult prolongTry(String buyerCorpId, Integer prolongDays, String appId) throws WxErrorException { + String url = mainService.getWxCpTpConfigStorage().getApiUrl(PROLONG_TRY); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("buyer_corpid", buyerCorpId); + jsonObject.addProperty("prolong_days", prolongDays); + jsonObject.addProperty("appid", appId); + String result = mainService.post(url, jsonObject.toString()); + return WxCpTpProlongTryResult.fromJson(result); + } +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpOrderServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpOrderServiceImpl.java new file mode 100644 index 0000000000..6a598e0079 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpOrderServiceImpl.java @@ -0,0 +1,71 @@ +package me.chanjar.weixin.cp.tp.service.impl; + +import com.google.gson.JsonObject; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.order.WxCpTpOrderDetails; +import me.chanjar.weixin.cp.bean.order.WxCpTpOrderListGetResult; +import me.chanjar.weixin.cp.tp.service.WxCpTpOrderService; +import me.chanjar.weixin.cp.tp.service.WxCpTpService; + +import java.util.Date; + +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Tp.GET_ORDER; +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Tp.GET_ORDER_LIST; + +/** + * 应用版本付费订单相关接口实现 + * + * @author leigouqing + * @date 2022年4月24日 + */ +@RequiredArgsConstructor +public class WxCpTpOrderServiceImpl implements WxCpTpOrderService { + + /** + * The Main service. + */ + private final WxCpTpService mainService; + + /** + * 获取订单详情 + *

+ * 文档地址 + *

+ * + * @param orderId 订单号 + * @return the order + * @throws WxErrorException the wx error exception + */ + @Override + public WxCpTpOrderDetails getOrder(String orderId) throws WxErrorException { + String url = mainService.getWxCpTpConfigStorage().getApiUrl(GET_ORDER); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("orderid", orderId); + String result = this.mainService.post(url, jsonObject.toString()); + return WxCpTpOrderDetails.fromJson(result); + } + + /** + * 获取订单列表 + *

+ * 文档地址 + *

+ * + * @param startTime 起始时间 + * @param endTime 终止时间 + * @param testMode 指定拉取正式或测试模式的订单。默认正式模式。0-正式模式,1-测试模式。 + * @return the order list + * @throws WxErrorException the wx error exception + */ + @Override + public WxCpTpOrderListGetResult getOrderList(Date startTime, Date endTime, Integer testMode) throws WxErrorException { + String url = mainService.getWxCpTpConfigStorage().getApiUrl(GET_ORDER_LIST); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("start_time", startTime.getTime() / 1000); + jsonObject.addProperty("end_time", endTime.getTime() / 1000); + jsonObject.addProperty("test_mode", testMode); + String result = this.mainService.post(url, jsonObject.toString()); + return WxCpTpOrderListGetResult.fromJson(result); + } +} diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpEditionServiceImplTest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpEditionServiceImplTest.java new file mode 100644 index 0000000000..b3561a3ae2 --- /dev/null +++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpEditionServiceImplTest.java @@ -0,0 +1,70 @@ +package me.chanjar.weixin.cp.tp.service.impl; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.WxCpTpProlongTryResult; +import me.chanjar.weixin.cp.config.WxCpTpConfigStorage; +import me.chanjar.weixin.cp.config.impl.WxCpTpDefaultConfigImpl; +import me.chanjar.weixin.cp.tp.service.WxCpTpEditionService; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Tp.PROLONG_TRY; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +/** + * 应用版本付费版本相关接口测试 + */ +public class WxCpTpEditionServiceImplTest { + + @Mock + private WxCpTpServiceApacheHttpClientImpl wxCpTpService; + + private WxCpTpConfigStorage configStorage; + + private WxCpTpEditionService wxCpTpEditionService; + + @BeforeClass + public void setUp() { + MockitoAnnotations.initMocks(this); + configStorage = new WxCpTpDefaultConfigImpl(); + when(wxCpTpService.getWxCpTpConfigStorage()).thenReturn(configStorage); + wxCpTpEditionService = new WxCpTpEditionServiceImpl(wxCpTpService); + } + + + /** + * 延长试用期 + */ + @Test + public void testProlongTry() throws WxErrorException { + + String buyerCorpId = "wx7da9abf8ac62baaa"; + Integer prolongDays = 7; + String appId = "1"; + + Long tryEndTime = 1565152189L; + + String result = "" + + " {\n" + + " \"errcode\" : 0,\n" + + " \"errmsg\" : \"ok\",\n" + + " \"try_end_time\" : 1565152189\n" + + " }"; + + String url = configStorage.getApiUrl(PROLONG_TRY); + when(wxCpTpService.post(eq(url), any(String.class))).thenReturn(result); + + final WxCpTpProlongTryResult prolongTryResult = wxCpTpEditionService.prolongTry(buyerCorpId, prolongDays, appId); + + assertNotNull(prolongTryResult); + + assertEquals(prolongTryResult.getTryEndTime(), tryEndTime); + + } +} diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpOrderServiceImplTest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpOrderServiceImplTest.java new file mode 100644 index 0000000000..eb0c5a38a8 --- /dev/null +++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpOrderServiceImplTest.java @@ -0,0 +1,156 @@ +package me.chanjar.weixin.cp.tp.service.impl; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.order.WxCpTpOrderDetails; +import me.chanjar.weixin.cp.bean.order.WxCpTpOrderListGetResult; +import me.chanjar.weixin.cp.config.WxCpTpConfigStorage; +import me.chanjar.weixin.cp.config.impl.WxCpTpDefaultConfigImpl; +import me.chanjar.weixin.cp.tp.service.WxCpTpOrderService; +import org.apache.commons.lang3.time.DateUtils; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.util.Date; +import java.util.List; +import java.util.Objects; + +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Tp.GET_ORDER; +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Tp.GET_ORDER_LIST; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.when; +import static org.testng.Assert.*; + +/** + * 应用版本付费订单相关接口测试 + */ +public class WxCpTpOrderServiceImplTest { + + @Mock + private WxCpTpServiceApacheHttpClientImpl wxCpTpService; + + private WxCpTpConfigStorage configStorage; + + private WxCpTpOrderService wxCpTpOrderService; + + @BeforeClass + public void setUp() { + MockitoAnnotations.initMocks(this); + configStorage = new WxCpTpDefaultConfigImpl(); + when(wxCpTpService.getWxCpTpConfigStorage()).thenReturn(configStorage); + wxCpTpOrderService = new WxCpTpOrderServiceImpl(wxCpTpService); + } + + + /** + * 获取订单详情 + */ + @Test + public void testGetOrder() throws WxErrorException { + String orderId = "2018091822ks1sd3s"; + + String result = "" + + "{\n" + + " \"errcode\" : 0,\n" + + " \"errmsg\" : \"ok\",\n" + + " \"orderid\" : \"2018091822ks1sd3s\",\n" + + " \"order_status\" : 1,\n" + + " \"order_type\" : 1,\n" + + " \"paid_corpid\" : \"wwfedd7e5291d63aaa\",\n" + + " \"operator_id\" : \"zhangsan\",\n" + + " \"suiteid\" : \"wx67cce113441ccaaa\",\n" + + " \"appid\" : 1,\n" + + " \"edition_id\" : \"RLS65535\",\n" + + " \"edition_name\" : \"协同版\",\n" + + " \"price\" : 100,\n" + + " \"user_count\" : 1000,\n" + + " \"order_period\": 365,\n" + + " \"order_time\" : 1533702999,\n" + + " \"paid_time\" : 1533702910,\n" + + " \"begin_time\" : 1533702910,\n" + + " \"end_time\" : 1553515904,\n" + + " \"order_from\" : 1,\n" + + " \"operator_corpid\" : \"wwfedd7e5292d63aaa\",\n" + + " \"service_share_amount\" : 60,\n" + + " \"platform_share_amount\" : 10,\n" + + " \"dealer_share_amount\" : 30,\n" + + " \"dealer_corp_info\":\n" + + " {\n" + + " \"corpid\": \"xxxx\",\n" + + " \"corp_name\": \"name\"\n" + + " }\n" + + " }"; + String url = configStorage.getApiUrl(GET_ORDER); + when(wxCpTpService.post(eq(url), any(String.class))).thenReturn(result); + + final WxCpTpOrderDetails orderDetails = wxCpTpOrderService.getOrder(orderId); + + assertNotNull(orderDetails); + + assertEquals(orderDetails.getOrderId(), orderId); + + } + + /** + * 获取订单列表 + */ + @Test + public void testGetOrderList() throws WxErrorException { + String orderId = "2018091822ks1sd3s"; + Date startTime = new Date(); + Date endTime = DateUtils.addDays(startTime, 5); + Integer testMode = 0; + + String result = "" + + " {\n" + + " \"errcode\" : 0,\n" + + " \"errmsg\" : \"ok\",\n" + + " \"order_list\": [\n" + + " {\n" + + " \"orderid\" : \"2018091822ks1sd3s\",\n" + + " \"order_status\" : 1,\n" + + " \"order_type\" : 1,\n" + + " \"paid_corpid\" : \"wwfedd7e5292d63aaa\",\n" + + " \"operator_id\" : \"zhangsan\",\n" + + " \"suiteid\" : \"wx67cce113441cc7a6\",\n" + + " \"appid\" : 1,\n" + + " \"edition_id\" : \"RLS65535\",\n" + + " \"edition_name\" : \"协同版\",\n" + + " \"price\" : 100,\n" + + " \"user_count\" : 1000,\n" + + " \"order_period\": 365,\n" + + " \"order_time\" : 1533702999,\n" + + " \"paid_time\" : 1533702910,\n" + + " \"begin_time\" : 1533702910,\n" + + " \"end_time\" : 1553515904,\n" + + " \"order_from\" : 1,\n" + + " \"operator_corpid\" : \"wwfedd7e5292d63aaa\",\n" + + " \"service_share_amount\" : 60,\n" + + " \"platform_share_amount\" : 10,\n" + + " \"dealer_share_amount\" : 30,\n" + + " \"dealer_corp_info\":\n" + + " {\n" + + " \"corpid\": \"xxxx\",\n" + + " \"corp_name\": \"name\"\n" + + " }\n" + + " }]\n" + + " }"; + + String url = configStorage.getApiUrl(GET_ORDER_LIST); + when(wxCpTpService.post(eq(url), any(String.class))).thenReturn(result); + + final WxCpTpOrderListGetResult orderList = wxCpTpOrderService.getOrderList(startTime, endTime, testMode); + + assertNotNull(orderList); + + final List detailsList = orderList.getOrderList(); + + assertTrue(Objects.nonNull(detailsList) && !detailsList.isEmpty()); + + assertEquals(detailsList.get(0).getOrderId(), orderId); + } + + +} From 5758cdcfd159f6f6c9d57b966ae0057202304f18 Mon Sep 17 00:00:00 2001 From: leiguoqing <191789784@qq.com> Date: Sun, 24 Apr 2022 22:54:41 +0800 Subject: [PATCH 2/3] =?UTF-8?q?:new:=20#2615=20=E3=80=90=E4=BC=81=E4=B8=9A?= =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E3=80=91=E8=8E=B7=E5=8F=96=E4=BC=81=E4=B8=9A?= =?UTF-8?q?=E6=B0=B8=E4=B9=85=E6=8E=88=E6=9D=83=E7=A0=81=E3=80=81=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E4=BC=81=E4=B8=9A=E6=8E=88=E6=9D=83=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E8=A1=A5=E5=85=85=E7=89=88=E6=9C=AC=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E8=BF=94=E5=9B=9E=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../me/chanjar/weixin/cp/bean/WxCpTpAuthInfo.java | 12 ++++++++++++ .../weixin/cp/bean/WxCpTpPermanentCodeInfo.java | 14 ++++++++++++++ .../tp/service/impl/BaseWxCpTpServiceImplTest.java | 12 +++++++++--- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpAuthInfo.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpAuthInfo.java index 8249cd50bd..27f12dac5b 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpAuthInfo.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpAuthInfo.java @@ -244,6 +244,17 @@ public static class Agent implements Serializable { @SerializedName("expired_time") private Long expiredTime; + /** + * 是否虚拟版本 + */ + @SerializedName("is_virtual_version") + private Boolean isVirtualVersion; + + /** + * 是否由互联企业分享安装。详见 企业互联 + */ + @SerializedName("is_shared_from_other_corp") + private Boolean isSharedFromOtherCorp; } /** @@ -290,6 +301,7 @@ public static WxCpTpAuthInfo fromJson(String json) { return WxCpGsonBuilder.create().fromJson(json, WxCpTpAuthInfo.class); } + @Override public String toJson() { return WxCpGsonBuilder.create().toJson(this); } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpPermanentCodeInfo.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpPermanentCodeInfo.java index 7a22310ee6..44d7a76b90 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpPermanentCodeInfo.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpPermanentCodeInfo.java @@ -240,6 +240,19 @@ public static class Agent implements Serializable { */ @SerializedName("expired_time") private Long expiredTime; + + /** + * 是否虚拟版本 + */ + @SerializedName("is_virtual_version") + private Boolean isVirtualVersion; + + /** + * 是否由互联企业分享安装。详见 企业互联 + */ + @SerializedName("is_shared_from_other_corp") + private Boolean isSharedFromOtherCorp; + } /** @@ -304,6 +317,7 @@ public static WxCpTpPermanentCodeInfo fromJson(String json) { return WxCpGsonBuilder.create().fromJson(json, WxCpTpPermanentCodeInfo.class); } + @Override public String toJson() { return WxCpGsonBuilder.create().toJson(this); } diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/tp/service/impl/BaseWxCpTpServiceImplTest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/tp/service/impl/BaseWxCpTpServiceImplTest.java index d181fac105..8a29a1130a 100644 --- a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/tp/service/impl/BaseWxCpTpServiceImplTest.java +++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/tp/service/impl/BaseWxCpTpServiceImplTest.java @@ -201,7 +201,9 @@ public void testGetPermanentCodeInfo() throws WxErrorException { " \"edition_name\":\"协同版\",\n" + " \"app_status\":3,\n" + " \"user_limit\":200,\n" + - " \"expired_time\":1541990791\n" + + " \"expired_time\":1541990791,\n" + + " \"is_virtual_version\":false,\n" + + " \"is_shared_from_other_corp\":true\n" + " }\n" + " ]\n" + " }\n" + @@ -295,7 +297,9 @@ public void testGetAuthInfo() throws WxErrorException { " \"edition_name\":\"协同版\",\n" + " \"app_status\":3,\n" + " \"user_limit\":200,\n" + - " \"expired_time\":1541990791\n" + + " \"expired_time\":1541990791,\n" + + " \"is_virtual_version\":false,\n" + + " \"is_shared_from_other_corp\":true\n" + " },\n" + " {\n" + " \"agentid\":1,\n" + @@ -303,7 +307,9 @@ public void testGetAuthInfo() throws WxErrorException { " \"edition_name\":\"协同版\",\n" + " \"app_status\":3,\n" + " \"user_limit\":200,\n" + - " \"expired_time\":1541990791\n" + + " \"expired_time\":1541990791,\n" + + " \"is_virtual_version\":false,\n" + + " \"is_shared_from_other_corp\":true\n" + " }\n" + " ]\n" + " }\n" + From 320be021f02daec3e5f7785520378dfc62fbdef6 Mon Sep 17 00:00:00 2001 From: leiguoqing <191789784@qq.com> Date: Thu, 27 Aug 2026 15:07:10 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat(invoice):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=97=85=E5=AE=A2=E8=BF=90=E8=BE=93=E8=A1=8C=E4=B8=9A=E7=94=B5?= =?UTF-8?q?=E5=AD=90=E5=8F=91=E7=A5=A8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 PartnerInvoiceService 中添加 issuePassengerTransportInvoice 方法 - 实现旅客运输发票开具接口的具体逻辑 - 添加 PassengerTransportInvoiceRequest 数据结构及其嵌套类 - 为新功能创建完整的单元测试用例 - 更新文档说明,明确 new-tax-control-fapiao API 的实现情况 - 添加旅客运输发票相关字段的序列化支持 --- docs/ELECTRONIC_INVOICE_API_COMPARISON.md | 4 +- .../PassengerTransportInvoiceRequest.java | 364 ++++++++++++++++++ .../wxpay/service/PartnerInvoiceService.java | 13 + .../impl/PartnerInvoiceServiceImpl.java | 8 + .../PassengerTransportInvoiceRequestTest.java | 94 +++++ .../impl/PartnerInvoiceServiceImplTest.java | 39 ++ 6 files changed, 520 insertions(+), 2 deletions(-) create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/PassengerTransportInvoiceRequest.java create mode 100644 weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/invoice/PassengerTransportInvoiceRequestTest.java diff --git a/docs/ELECTRONIC_INVOICE_API_COMPARISON.md b/docs/ELECTRONIC_INVOICE_API_COMPARISON.md index 54568878f9..aed15d321d 100644 --- a/docs/ELECTRONIC_INVOICE_API_COMPARISON.md +++ b/docs/ELECTRONIC_INVOICE_API_COMPARISON.md @@ -62,5 +62,5 @@ V3 服务商电子发票产品文档:前者是 API 列表中的具体接口, | 身份/鉴权上下文 | 公众号 access token、用户授权页/授权数据 | 微信支付服务商号、子商户号、V3 签名与敏感字段加密 | | 覆盖范围 | 授权、开票、冲红、查询及公众号商户配置 | 子商户邀约/状态、模板/开发配置、行业开票、文件和卡包、通知 | -因此,两个 Issue 都是同一个微信支付 V3 服务商电子发票接入需求,当前代码库检索未 -发现对应的 `new-tax-control-fapiao` API 实现。 +因此,两个 Issue 都是同一个微信支付 V3 服务商电子发票接入需求,当前由 +`weixin-java-pay` 的 `PartnerInvoiceService` 提供对应的 `new-tax-control-fapiao` API 实现。 diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/PassengerTransportInvoiceRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/PassengerTransportInvoiceRequest.java new file mode 100644 index 0000000000..4898773ddf --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/invoice/PassengerTransportInvoiceRequest.java @@ -0,0 +1,364 @@ +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 PassengerTransportInvoiceRequest implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 子商户号。微信支付分配的子商户号,必填,最长32个字符。 + */ + @SerializedName("sub_mchid") + private String subMchid; + + /** + * 发票申请单号。唯一标识一次开票行为并关联唯一的购买方信息,必填,最长32个字符; + * 微信支付账单开票场景下填写微信支付交易单号。 + */ + @SerializedName("fapiao_apply_id") + private String fapiaoApplyId; + + /** + * 购买方信息,即发票抬头,必填。 + */ + @SerializedName("buyer_information") + private BuyerInformation buyerInformation; + + /** + * 需要开具的旅客运输行业数电发票信息,必填。 + */ + @SerializedName("fapiao_information") + private FapiaoInformation fapiaoInformation; + + /** + * 购买方信息,即发票抬头。 + */ + @Data + public static class BuyerInformation implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 购买方类型,必填。可选值:INDIVIDUAL(个人、其他)、ORGANIZATION(单位)。 + */ + private String type; + + /** + * 购买方名称,必填,最长256个字符。 + */ + private String name; + + /** + * 购买方纳税人识别号,最长32个字符;购买方类型为 ORGANIZATION 时必填。 + */ + @SerializedName("taxpayer_id") + private String taxpayerId; + + /** + * 购买方地址,最长128个字符。 + */ + private String address; + + /** + * 购买方电话,最长32个字符。 + */ + private String telephone; + + /** + * 购买方开户银行名称,最长128个字符。 + */ + @SerializedName("bank_name") + private String bankName; + + /** + * 购买方银行账号,最长32个字符。 + */ + @SerializedName("bank_account") + private String bankAccount; + + /** + * 用户手机号。该字段为密文字段,调用方需使用微信支付公钥或微信支付平台证书公钥加密后传入。 + */ + private String phone; + + /** + * 用户邮箱地址。该字段为密文字段,调用方需使用微信支付公钥或微信支付平台证书公钥加密后传入。 + */ + private String email; + + /** + * 微信支付订单金额,仅在微信支付场景下有效,单位:分。 + */ + private Long amount; + + /** + * 商户订单号,仅在微信支付场景下有效,最长32个字符。 + */ + @SerializedName("out_trade_no") + private String outTradeNo; + + /** + * 用户选择开具的发票类型。可选值:COMM_FAPIAO(增值税普通发票)、VAT_FAPIAO(增值税专用发票)。 + */ + @SerializedName("fapiao_bill_type") + private String fapiaoBillType; + + /** + * 用户申请开票时提交的留言信息,最长100个字符。 + */ + @SerializedName("user_apply_message") + private String userApplyMessage; + } + + /** + * 需要开具的旅客运输行业数电发票信息。 + */ + @Data + public static class FapiaoInformation implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 商户发票单号,必填,最长32个字符,在每个商户下必须唯一。 + * 仅支持字母、数字、中划线、下划线、竖线和星号;开票失败或发票已冲红时可更换后重试, + * 同一发票申请单最多支持5个商户发票单号。 + */ + @SerializedName("fapiao_id") + private String fapiaoId; + + /** + * 总价税合计,必填,单位:分。值为所有发票行单行金额合计之和,且全部发票的总价税合计 + * 不能超过交易总金额。 + */ + @SerializedName("total_amount") + private Long totalAmount; + + /** + * 发票行信息,必填;单张发票最多包含8行。 + */ + private List items; + + /** + * 出口业务适用政策代码。可选值:1(退税政策)、2(免税政策)、3(征税政策)。 + */ + @SerializedName("export_business_policy_code") + private Long exportBusinessPolicyCode; + + /** + * 增值税即征即退代码。可选值:1(软件产品)、2(资源综合利用产品)、3(管道运输服务)、 + * 4(有形动产融资租赁服务)、5(有形动产融资性售后回租服务)、6(新型墙体材料)、 + * 7(风力发电产品)、8(光伏发电产品)、9(动漫软件产品)、10(飞机维修劳务)、 + * 11(黄金)、12(铂金)。 + */ + @SerializedName("vat_refund_levy_code") + private Long vatRefundLevyCode; + + /** + * 开票人ID,必填,最长64个字符,为税局乐企系统登记的开票人ID。 + */ + @SerializedName("billing_person_id") + private String billingPersonId; + + /** + * 开票人名称,最长64个字符,为税局乐企系统登记的脱敏后名称;格式为脱敏姓名、空格和身份证后四位。 + */ + @SerializedName("billing_person") + private String billingPerson; + + /** + * 发票类型,必填。可选值:COMM_FAPIAO(增值税普通发票)、VAT_FAPIAO(增值税专用发票)。 + */ + @SerializedName("fapiao_bill_type") + private String fapiaoBillType; + + /** + * 发票对应的交易信息,必填,最多支持10条。 + */ + @SerializedName("transaction_information") + private List transactionInformation; + + /** + * 发票备注。 + */ + private String remark; + } + + /** + * 发票行信息。 + */ + @Data + public static class InvoiceItem implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 税局侧规定的货物或应税劳务、服务税收分类编码,必填,长度为19个字符; + * 旅客运输业务仅支持以301开头的税收分类编码。 + */ + @SerializedName("tax_code") + private String taxCode; + + /** + * 货物或应税劳务、服务名称,必填,由商户自定义,最长128个字符。 + */ + @SerializedName("goods_name") + private String goodsName; + + /** + * 规格型号,展示在发票规格型号列,最长20个字符。 + */ + private String specification; + + /** + * 单位,展示在发票单位列;折扣行不填写。 + */ + private String unit; + + /** + * 数量,单位为10的负8次方,100000000表示数量1。非折扣行不填写时默认为100000000; + * 折扣行不填写。 + */ + private Long quantity; + + /** + * 单行金额合计,必填,单位:分。折扣行为负数,非折扣行为正数。 + */ + @SerializedName("total_amount") + private Long totalAmount; + + /** + * 税率,必填,单位为万分之一,例如1300表示13%。当前支持0、1%、1.5%、3%、5%、6%、 + * 9%、10%、11%、13%、16%和17%。 + */ + @SerializedName("tax_rate") + private Long taxRate; + + /** + * 是否为折扣行,必填;折扣行必须紧跟在被折扣行之后。 + */ + private Boolean discount; + + /** + * 优惠政策标识。可选值:1(简易征收)、2(稀土产品)、3(免税)、4(不征税)、 + * 5(先征后退)、6(100%先征后退)、7(50%先征后退)、8(按3%简易征收)、 + * 9(按5%简易征收)、10(按5%简易征收减按1.5%计征)、11(即征即退30%)、 + * 12(即征即退50%)、13(即征即退70%)、14(即征即退100%)、 + * 15(超税负3%即征即退)、16(超税负8%即征即退)、17(超税负12%即征即退)、 + * 18(超税负6%即征即退)。 + */ + @SerializedName("preferential_policy_code") + private Long preferentialPolicyCode; + + /** + * 出行人额外信息,可选;传入时其内部必填字段必须完整填写。 + */ + @SerializedName("passenger_information") + private PassengerInformation passengerInformation; + } + + /** + * 出行人额外信息。证件号码由调用方按微信支付文档加密。 + */ + @Data + public static class PassengerInformation implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 出行人姓名;填写出行人额外信息时必填,最长20个字符。 + */ + private String name; + + /** + * 出行人证件类型;填写出行人额外信息时必填。可选值:IDENTITY_CARD(居民身份证)、 + * PASSPORT(护照)、HONG_KONG_PERMIT(中国香港居民来往内地通行证)、 + * MACAO_PERMIT(中国澳门居民来往内地通行证)、TAIWAN_PERMIT(中国台湾居民来往大陆通行证)、 + * FOREIGNER_RESIDENCE_PERMIT(外国人居留证)、HONG_KONG_RESIDENT_CARD(香港居民证)、 + * MACAO_RESIDENT_CARD(澳门居民证)、TAIWAN_RESIDENT_CARD(台湾居民证)、 + * MILITARY_OFFICER_CARD(军官证)、ARMED_POLICE_OFFICER_CARD(武警警官证)、 + * SOLDIER_CARD(士兵证)、HOMECOMING_CERT(港澳同胞回乡证)、 + * TAIWAN_COMPATRIOT_CERT(台胞证)。 + */ + @SerializedName("certificate_type") + private String certificateType; + + /** + * 出行人证件号码;填写出行人额外信息时必填。该字段为密文字段,调用方需使用微信支付公钥 + * 或微信支付平台证书公钥加密后传入。 + */ + @SerializedName("certificate_number") + private String certificateNumber; + + /** + * 出行日期;填写出行人额外信息时必填,使用 RFC3339 格式:yyyy-MM-DDTHH:mm:ss+TIMEZONE。 + */ + @SerializedName("departure_date") + private String departureDate; + + /** + * 出发地详细地址;填写出行人额外信息时必填,最长80个字符。 + */ + @SerializedName("departure_place") + private String departurePlace; + + /** + * 目的地详细地址;填写出行人额外信息时必填,最长80个字符。 + */ + private String destination; + + /** + * 交通工具类型;填写出行人额外信息时必填。可选值:LONG_DISTANCE_BUS(长途汽车)、 + * PUBLIC_TRANSPORTATION(公共交通)、CAR(汽车)、SHIP(船舶)、 + * OTHER_TRANSPORTATION(其他交通工具)。 + */ + @SerializedName("transportation_type") + private String transportationType; + + /** + * 交通工具等级。交通工具为 SHIP 时必填,可选值:SHIP_FIRST_CLASS_CABIN(船舶一等舱)、 + * SHIP_SECOND_CLASS_CABIN(船舶二等舱)、SHIP_THIRD_CLASS_CABIN(船舶三等舱); + * 其他交通工具不填写。 + */ + @SerializedName("transportation_classes") + private String transportationClasses; + } + + /** + * 发票对应的交易信息。 + */ + @Data + public static class TransactionInformation implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 支付渠道,必填。当前可选值:WECHAT_PAY(微信支付)。 + */ + @SerializedName("pay_channel") + private String payChannel; + + /** + * 支付订单号,最长64个字符。支付渠道为 WECHAT_PAY 时,本字段与商户订单号至少填写一个。 + */ + @SerializedName("transaction_id") + private String transactionId; + + /** + * 支付商户订单号,最长64个字符。支付渠道为 WECHAT_PAY 时,本字段与支付订单号至少填写一个。 + */ + @SerializedName("out_trade_no") + private String outTradeNo; + + /** + * 交易金额,必填,单位:分。 + */ + private Long amount; + } +} 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 index 39390ba713..233278ce83 100644 --- 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 @@ -3,6 +3,7 @@ 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.PassengerTransportInvoiceRequest; import com.github.binarywang.wxpay.bean.invoice.InvoiceResult; import com.github.binarywang.wxpay.bean.invoice.InvoiceFileResult; import com.github.binarywang.wxpay.bean.invoice.ReverseInvoiceRequest; @@ -51,6 +52,18 @@ public interface PartnerInvoiceService { */ void issueGeneralInvoice(GeneralInvoiceRequest request) throws WxPayException; + /** + * 开具旅客运输行业电子发票。 + * + *

接口受理成功时返回 HTTP 202 Accepted,无应答包体。受理成功不代表开票完成, + * 请通过开票结果回调或查询电子发票接口获取处理结果。

+ * + * @param request 开票申请 + * @throws WxPayException 微信支付异常 + * @see 官方文档 + */ + void issuePassengerTransportInvoice(PassengerTransportInvoiceRequest request) throws WxPayException; + /** * 查询电子发票。 * 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 index af173851f1..c51f8006e4 100644 --- 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 @@ -3,6 +3,7 @@ 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.PassengerTransportInvoiceRequest; import com.github.binarywang.wxpay.bean.invoice.InvoiceResult; import com.github.binarywang.wxpay.bean.invoice.InvoiceFileResult; import com.github.binarywang.wxpay.bean.invoice.ReverseInvoiceRequest; @@ -45,6 +46,7 @@ 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 ISSUE_PASSENGER_TRANSPORT_PATH = "/v3/new-tax-control-fapiao/fapiao-applications/issue-passenger-transport"; private static final String FAPIAO_APPLICATIONS_PATH = "/v3/new-tax-control-fapiao/fapiao-applications/"; private final WxPayService payService; @@ -80,6 +82,12 @@ public void issueGeneralInvoice(GeneralInvoiceRequest request) throws WxPayExcep this.payService.postV3(url, GSON.toJson(request)); } + @Override + public void issuePassengerTransportInvoice(PassengerTransportInvoiceRequest request) throws WxPayException { + String url = this.payService.getPayBaseUrl() + ISSUE_PASSENGER_TRANSPORT_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); diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/invoice/PassengerTransportInvoiceRequestTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/invoice/PassengerTransportInvoiceRequestTest.java new file mode 100644 index 0000000000..eacbbef316 --- /dev/null +++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/invoice/PassengerTransportInvoiceRequestTest.java @@ -0,0 +1,94 @@ +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 PassengerTransportInvoiceRequest} 单元测试。 + */ +public class PassengerTransportInvoiceRequestTest { + + @Test + public void shouldSerializePassengerTransportInvoiceRequestUsingWechatPayFieldNames() { + PassengerTransportInvoiceRequest request = new PassengerTransportInvoiceRequest(); + request.setSubMchid("1900000109"); + request.setFapiaoApplyId("apply_20260827_001"); + + PassengerTransportInvoiceRequest.BuyerInformation buyer = + new PassengerTransportInvoiceRequest.BuyerInformation(); + buyer.setType("INDIVIDUAL"); + buyer.setName("示例旅客"); + buyer.setPhone("encrypted-phone"); + buyer.setEmail("encrypted-email"); + buyer.setAmount(1000L); + request.setBuyerInformation(buyer); + + PassengerTransportInvoiceRequest.PassengerInformation passenger = + new PassengerTransportInvoiceRequest.PassengerInformation(); + passenger.setName("张**"); + passenger.setCertificateType("IDENTITY_CARD"); + passenger.setCertificateNumber("encrypted-certificate-number"); + passenger.setDepartureDate("2026-08-27T10:00:00+08:00"); + passenger.setDeparturePlace("重庆市渝北区示例出发地"); + passenger.setDestination("重庆市两江新区示例目的地"); + passenger.setTransportationType("SHIP"); + passenger.setTransportationClasses("SHIP_FIRST_CLASS_CABIN"); + + PassengerTransportInvoiceRequest.InvoiceItem item = new PassengerTransportInvoiceRequest.InvoiceItem(); + item.setTaxCode("3010101020100000000"); + item.setGoodsName("旅客运输服务"); + item.setQuantity(2200000000L); + item.setTotalAmount(1000L); + item.setTaxRate(300L); + item.setDiscount(false); + item.setPassengerInformation(passenger); + + PassengerTransportInvoiceRequest.TransactionInformation transaction = + new PassengerTransportInvoiceRequest.TransactionInformation(); + transaction.setPayChannel("WECHAT_PAY"); + transaction.setOutTradeNo("order_20260827_001"); + transaction.setAmount(1000L); + + PassengerTransportInvoiceRequest.FapiaoInformation fapiao = + new PassengerTransportInvoiceRequest.FapiaoInformation(); + fapiao.setFapiaoId("invoice_20260827_001"); + fapiao.setTotalAmount(1000L); + fapiao.setItems(Collections.singletonList(item)); + fapiao.setBillingPersonId("billing_person_001"); + fapiao.setFapiaoBillType("COMM_FAPIAO"); + fapiao.setTransactionInformation(Collections.singletonList(transaction)); + request.setFapiaoInformation(fapiao); + + JsonObject json = new Gson().toJsonTree(request).getAsJsonObject(); + JsonObject passengerJson = json.getAsJsonObject("fapiao_information") + .getAsJsonArray("items").get(0).getAsJsonObject() + .getAsJsonObject("passenger_information"); + + assertThat(json.get("sub_mchid").getAsString()).isEqualTo("1900000109"); + assertThat(json.get("fapiao_apply_id").getAsString()).isEqualTo("apply_20260827_001"); + assertThat(json.getAsJsonObject("buyer_information").get("phone").getAsString()) + .isEqualTo("encrypted-phone"); + assertThat(json.getAsJsonObject("fapiao_information").getAsJsonArray("items").get(0) + .getAsJsonObject().get("quantity").getAsLong()).isEqualTo(2200000000L); + assertThat(passengerJson.get("name").getAsString()).isEqualTo("张**"); + assertThat(passengerJson.get("certificate_type").getAsString()).isEqualTo("IDENTITY_CARD"); + assertThat(passengerJson.get("certificate_number").getAsString()) + .isEqualTo("encrypted-certificate-number"); + assertThat(passengerJson.get("departure_date").getAsString()) + .isEqualTo("2026-08-27T10:00:00+08:00"); + assertThat(passengerJson.get("departure_place").getAsString()) + .isEqualTo("重庆市渝北区示例出发地"); + assertThat(passengerJson.get("destination").getAsString()) + .isEqualTo("重庆市两江新区示例目的地"); + assertThat(passengerJson.get("transportation_type").getAsString()).isEqualTo("SHIP"); + assertThat(passengerJson.get("transportation_classes").getAsString()) + .isEqualTo("SHIP_FIRST_CLASS_CABIN"); + assertThat(json.getAsJsonObject("fapiao_information").getAsJsonArray("transaction_information").get(0) + .getAsJsonObject().get("out_trade_no").getAsString()).isEqualTo("order_20260827_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 index f0aa2d16a5..342667225a 100644 --- 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 @@ -2,6 +2,7 @@ import com.github.binarywang.wxpay.service.WxPayService; import com.github.binarywang.wxpay.bean.invoice.GeneralInvoiceRequest; +import com.github.binarywang.wxpay.bean.invoice.PassengerTransportInvoiceRequest; import com.github.binarywang.wxpay.bean.invoice.InvoiceResult; import com.github.binarywang.wxpay.bean.invoice.ReverseInvoiceRequest; import com.github.binarywang.wxpay.bean.invoice.InvoiceFileResult; @@ -67,6 +68,44 @@ public void shouldPostGeneralInvoiceToV3Endpoint() throws Exception { Assert.assertTrue(requestedBody.get().contains("\"fapiao_apply_id\":\"invoice-001\"")); } + @Test + public void shouldPostPassengerTransportInvoiceToV3Endpoint() 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 null; + } + throw new UnsupportedOperationException(method.getName()); + }); + PassengerTransportInvoiceRequest request = new PassengerTransportInvoiceRequest(); + request.setSubMchid("1900000109"); + request.setFapiaoApplyId("invoice-002"); + PassengerTransportInvoiceRequest.PassengerInformation passenger = + new PassengerTransportInvoiceRequest.PassengerInformation(); + passenger.setCertificateNumber("encrypted-certificate-number"); + PassengerTransportInvoiceRequest.InvoiceItem item = new PassengerTransportInvoiceRequest.InvoiceItem(); + item.setPassengerInformation(passenger); + PassengerTransportInvoiceRequest.FapiaoInformation fapiao = + new PassengerTransportInvoiceRequest.FapiaoInformation(); + fapiao.setItems(java.util.Collections.singletonList(item)); + request.setFapiaoInformation(fapiao); + + new PartnerInvoiceServiceImpl(payService).issuePassengerTransportInvoice(request); + + Assert.assertEquals(requestedUrl.get(), + "https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/fapiao-applications/issue-passenger-transport"); + Assert.assertTrue(requestedBody.get().contains("\"fapiao_apply_id\":\"invoice-002\"")); + Assert.assertTrue(requestedBody.get().contains("\"passenger_information\"")); + Assert.assertTrue(requestedBody.get().contains("\"certificate_number\":\"encrypted-certificate-number\"")); + } + @Test public void shouldQueryInvoiceWithRequiredSubMchId() throws Exception { AtomicReference requestedUrl = new AtomicReference<>();