diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/JedisWxRedisOps.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/JedisWxRedisOps.java
index ede2fcd8b..b42142943 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/JedisWxRedisOps.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/JedisWxRedisOps.java
@@ -23,7 +23,11 @@ public class JedisWxRedisOps implements WxRedisOps {
@Override
public void setValue(String key, String value, int expire, TimeUnit timeUnit) {
try (Jedis jedis = this.jedisPool.getResource()) {
- jedis.psetex(key, timeUnit.toMillis(expire), value);
+ if (expire <= 0) {
+ jedis.set(key, value);
+ } else {
+ jedis.psetex(key, timeUnit.toMillis(expire), value);
+ }
}
}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedisTemplateWxRedisOps.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedisTemplateWxRedisOps.java
index 068703287..652cec84a 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedisTemplateWxRedisOps.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedisTemplateWxRedisOps.java
@@ -19,7 +19,11 @@ public class RedisTemplateWxRedisOps implements WxRedisOps {
@Override
public void setValue(String key, String value, int expire, TimeUnit timeUnit) {
- redisTemplate.opsForValue().set(key, value, expire, timeUnit);
+ if (expire <= 0) {
+ redisTemplate.opsForValue().set(key, value);
+ } else {
+ redisTemplate.opsForValue().set(key, value, expire, timeUnit);
+ }
}
@Override
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedissonWxRedisOps.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedissonWxRedisOps.java
index e06db302e..d51cd3e1a 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedissonWxRedisOps.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedissonWxRedisOps.java
@@ -19,12 +19,20 @@ public class RedissonWxRedisOps implements WxRedisOps {
@Override
public void setValue(String key, String value, int expire, TimeUnit timeUnit) {
- redissonClient.getBucket(key).set(value, expire, timeUnit);
+ if (expire <= 0) {
+ redissonClient.getBucket(key).set(value);
+ } else {
+ redissonClient.getBucket(key).set(value, expire, timeUnit);
+ }
}
@Override
public Long getExpire(String key) {
- return redissonClient.getBucket(key).remainTimeToLive();
+ long expire = redissonClient.getBucket(key).remainTimeToLive();
+ if (expire > 0) {
+ expire = expire / 1000;
+ }
+ return expire;
}
@Override
diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/CommonWxRedisOpsTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/CommonWxRedisOpsTest.java
new file mode 100644
index 000000000..96ba20ba2
--- /dev/null
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/CommonWxRedisOpsTest.java
@@ -0,0 +1,51 @@
+package me.chanjar.weixin.common.redis;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import java.util.concurrent.TimeUnit;
+
+public class CommonWxRedisOpsTest {
+
+ protected WxRedisOps wxRedisOps;
+ private String key = "access_token";
+ private String value = String.valueOf(System.currentTimeMillis());
+
+ @Test
+ public void testGetValue() {
+ wxRedisOps.setValue(key, value, 3, TimeUnit.SECONDS);
+ Assert.assertEquals(wxRedisOps.getValue(key), value);
+ }
+
+ @Test
+ public void testSetValue() {
+ String key = "access_token", value = String.valueOf(System.currentTimeMillis());
+ wxRedisOps.setValue(key, value, -1, TimeUnit.SECONDS);
+ wxRedisOps.setValue(key, value, 0, TimeUnit.SECONDS);
+ wxRedisOps.setValue(key, value, 1, TimeUnit.SECONDS);
+ }
+
+ @Test
+ public void testGetExpire() {
+ String key = "access_token", value = String.valueOf(System.currentTimeMillis());
+ wxRedisOps.setValue(key, value, -1, TimeUnit.SECONDS);
+ Assert.assertTrue(wxRedisOps.getExpire(key) < 0);
+ wxRedisOps.setValue(key, value, 4, TimeUnit.SECONDS);
+ Long expireSeconds = wxRedisOps.getExpire(key);
+ Assert.assertTrue(expireSeconds <= 4 && expireSeconds >= 0);
+ }
+
+ @Test
+ public void testExpire() {
+ String key = "access_token", value = String.valueOf(System.currentTimeMillis());
+ wxRedisOps.setValue(key, value, -1, TimeUnit.SECONDS);
+ wxRedisOps.expire(key, 4, TimeUnit.SECONDS);
+ Long expireSeconds = wxRedisOps.getExpire(key);
+ Assert.assertTrue(expireSeconds <= 4 && expireSeconds >= 0);
+ }
+
+ @Test
+ public void testGetLock() {
+ Assert.assertNotNull(wxRedisOps.getLock("access_token_lock"));
+ }
+}
diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/JedisWxRedisOpsTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/JedisWxRedisOpsTest.java
new file mode 100644
index 000000000..2ff2c37b8
--- /dev/null
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/JedisWxRedisOpsTest.java
@@ -0,0 +1,21 @@
+package me.chanjar.weixin.common.redis;
+
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.BeforeTest;
+import redis.clients.jedis.JedisPool;
+
+public class JedisWxRedisOpsTest extends CommonWxRedisOpsTest {
+
+ JedisPool jedisPool;
+
+ @BeforeTest
+ public void init() {
+ this.jedisPool = new JedisPool("127.0.0.1", 6379);
+ this.wxRedisOps = new JedisWxRedisOps(jedisPool);
+ }
+
+ @AfterTest
+ public void destroy() {
+ this.jedisPool.close();
+ }
+}
diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/RedisTemplateWxRedisOpsTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/RedisTemplateWxRedisOpsTest.java
new file mode 100644
index 000000000..bf3b35a7c
--- /dev/null
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/RedisTemplateWxRedisOpsTest.java
@@ -0,0 +1,26 @@
+package me.chanjar.weixin.common.redis;
+
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.BeforeTest;
+
+public class RedisTemplateWxRedisOpsTest extends CommonWxRedisOpsTest {
+
+ StringRedisTemplate redisTemplate;
+
+ @BeforeTest
+ public void init() {
+ JedisConnectionFactory connectionFactory = new JedisConnectionFactory();
+ connectionFactory.setHostName("127.0.0.1");
+ connectionFactory.setPort(6379);
+ connectionFactory.afterPropertiesSet();
+ StringRedisTemplate redisTemplate = new StringRedisTemplate(connectionFactory);
+ this.redisTemplate = redisTemplate;
+ this.wxRedisOps = new RedisTemplateWxRedisOps(this.redisTemplate);
+ }
+
+ @AfterTest
+ public void destroy() {
+ }
+}
diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/RedissonWxRedisOpsTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/RedissonWxRedisOpsTest.java
new file mode 100644
index 000000000..48cf7b29b
--- /dev/null
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/RedissonWxRedisOpsTest.java
@@ -0,0 +1,27 @@
+package me.chanjar.weixin.common.redis;
+
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.redisson.config.TransportMode;
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.BeforeTest;
+
+public class RedissonWxRedisOpsTest extends CommonWxRedisOpsTest {
+
+ RedissonClient redissonClient;
+
+ @BeforeTest
+ public void init() {
+ Config config = new Config();
+ config.useSingleServer().setAddress("redis://127.0.0.1:6379");
+ config.setTransportMode(TransportMode.NIO);
+ this.redissonClient = Redisson.create(config);
+ this.wxRedisOps = new RedissonWxRedisOps(this.redissonClient);
+ }
+
+ @AfterTest
+ public void destroy() {
+ this.redissonClient.shutdown();
+ }
+}
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpMerchantInvoiceService.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpMerchantInvoiceService.java
new file mode 100644
index 000000000..294fba85b
--- /dev/null
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpMerchantInvoiceService.java
@@ -0,0 +1,85 @@
+package me.chanjar.weixin.mp.api;
+
+
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.mp.bean.invoice.merchant.*;
+
+/**
+ * 商户电子发票相关的接口
+ *
+ * 重要!!!, 根据不同开票平台, 以下错误码可能开票成功(开票,冲红), 内部暂时未处理:
+ * 73105: 开票平台开票中,请使用相同的发票请求流水号重试开票
+ * 73107: 发票请求流水正在被处理,请通过查询接口获取结果
+ * 73100: 开票平台错误
+ *
+ * 流程文档: https://developers.weixin.qq.com/doc/offiaccount/WeChat_Invoice/E_Invoice/Vendor_and_Invoicing_Platform_Mode_Instruction.html
+ * 接口文档: https://developers.weixin.qq.com/doc/offiaccount/WeChat_Invoice/E_Invoice/Vendor_API_List.html
+ */
+public interface WxMpMerchantInvoiceService {
+
+ /**
+ * 获取开票授权页链接
+ */
+ InvoiceAuthPageResult getAuthPageUrl(InvoiceAuthPageRequest params) throws WxErrorException;
+
+ /**
+ * 获得用户授权数据
+ */
+ InvoiceAuthDataResult getAuthData(InvoiceAuthDataRequest params) throws WxErrorException;
+
+ /**
+ * 拒绝开票
+ *
+ * 场景: 用户授权填写数据无效
+ * 结果: 用户会收到一条开票失败提示
+ */
+ void rejectInvoice(InvoiceRejectRequest params) throws WxErrorException;
+
+ /**
+ * 开具电子发票
+ */
+ void makeOutInvoice(MakeOutInvoiceRequest params) throws WxErrorException;
+
+ /**
+ * 发票冲红
+ */
+ void clearOutInvoice(ClearOutInvoiceRequest params) throws WxErrorException;
+
+ /**
+ * 查询发票信息
+ *
+ * @param fpqqlsh 发票请求流水号
+ * @param nsrsbh 纳税人识别号
+ */
+ InvoiceResult queryInvoiceInfo(String fpqqlsh, String nsrsbh) throws WxErrorException;
+
+ /**
+ * 设置商户联系方式, 获取授权链接前需要设置商户联系信息
+ */
+ void setMerchantContactInfo(MerchantContactInfo contact) throws WxErrorException;
+
+ /**
+ * 获取商户联系方式
+ */
+ MerchantContactInfo getMerchantContactInfo() throws WxErrorException;
+
+ /**
+ * 配置授权页面字段
+ */
+ void setAuthPageSetting(InvoiceAuthPageSetting authPageSetting) throws WxErrorException;
+
+ /**
+ * 获取授权页面配置
+ */
+ InvoiceAuthPageSetting getAuthPageSetting() throws WxErrorException;
+
+ /**
+ * 设置商户开票平台信息
+ */
+ void setMerchantInvoicePlatform(MerchantInvoicePlatformInfo merchantInvoicePlatformInfo) throws WxErrorException;
+
+ /**
+ * 获取商户开票平台信息
+ */
+ MerchantInvoicePlatformInfo getMerchantInvoicePlatform(MerchantInvoicePlatformInfo merchantInvoicePlatformInfo) throws WxErrorException;
+}
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/BaseWxMpServiceImpl.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/BaseWxMpServiceImpl.java
index fb4ae5d0d..d6a625c9b 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/BaseWxMpServiceImpl.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/BaseWxMpServiceImpl.java
@@ -6,10 +6,13 @@ import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
+import lombok.Getter;
+import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.WxType;
import me.chanjar.weixin.common.bean.WxJsapiSignature;
import me.chanjar.weixin.common.bean.WxNetCheckResult;
+import me.chanjar.weixin.common.enums.TicketType;
import me.chanjar.weixin.common.error.WxError;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.session.StandardSessionManager;
@@ -26,7 +29,6 @@ import me.chanjar.weixin.mp.bean.result.WxMpOAuth2AccessToken;
import me.chanjar.weixin.mp.bean.result.WxMpSemanticQueryResult;
import me.chanjar.weixin.mp.bean.result.WxMpUser;
import me.chanjar.weixin.mp.config.WxMpConfigStorage;
-import me.chanjar.weixin.common.enums.TicketType;
import me.chanjar.weixin.mp.enums.WxMpApiUrl;
import me.chanjar.weixin.mp.util.WxMpConfigStorageHolder;
import org.apache.commons.lang3.StringUtils;
@@ -70,6 +72,10 @@ public abstract class BaseWxMpServiceImpl implements WxMpService, RequestH
private WxMpOcrService ocrService = new WxMpOcrServiceImpl(this);
private WxMpImgProcService imgProcService = new WxMpImgProcServiceImpl(this);
+ @Getter
+ @Setter
+ private WxMpMerchantInvoiceService merchantInvoiceService = new WxMpMerchantInvoiceServiceImpl(this, this.cardService);
+
private Map configStorageMap;
private int retrySleepMillis = 1000;
@@ -359,11 +365,11 @@ public abstract class BaseWxMpServiceImpl implements WxMpService, RequestH
// 强制设置wxMpConfigStorage它的access token过期了,这样在下一次请求里就会刷新access token
Lock lock = this.getWxMpConfigStorage().getAccessTokenLock();
lock.lock();
- try{
- if(StringUtils.equals(this.getWxMpConfigStorage().getAccessToken(), accessToken)){
+ try {
+ if (StringUtils.equals(this.getWxMpConfigStorage().getAccessToken(), accessToken)) {
this.getWxMpConfigStorage().expireAccessToken();
}
- } catch (Exception ex){
+ } catch (Exception ex) {
this.getWxMpConfigStorage().expireAccessToken();
} finally {
lock.unlock();
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpMerchantInvoiceServiceImpl.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpMerchantInvoiceServiceImpl.java
new file mode 100644
index 000000000..ffae3ddf1
--- /dev/null
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpMerchantInvoiceServiceImpl.java
@@ -0,0 +1,119 @@
+package me.chanjar.weixin.mp.api.impl;
+
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonObject;
+import lombok.AllArgsConstructor;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.mp.api.WxMpCardService;
+import me.chanjar.weixin.mp.api.WxMpMerchantInvoiceService;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.bean.invoice.merchant.*;
+import me.chanjar.weixin.mp.enums.WxMpApiUrl;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static me.chanjar.weixin.mp.enums.WxMpApiUrl.Invoice.*;
+
+
+@AllArgsConstructor
+public class WxMpMerchantInvoiceServiceImpl implements WxMpMerchantInvoiceService {
+
+ private WxMpService wxMpService;
+ private WxMpCardService wxMpCardService;
+
+ private final static Gson gson;
+
+ static {
+ gson = new GsonBuilder()
+ .disableHtmlEscaping()
+ .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+ .create();
+ }
+
+ @Override
+ public InvoiceAuthPageResult getAuthPageUrl(InvoiceAuthPageRequest params) throws WxErrorException {
+ String ticket = wxMpCardService.getCardApiTicket();
+ params.setTicket(ticket);
+ return doCommonInvoiceHttpPost(GET_AUTH_URL, params, InvoiceAuthPageResult.class);
+ }
+
+ @Override
+ public InvoiceAuthDataResult getAuthData(InvoiceAuthDataRequest params) throws WxErrorException {
+ return doCommonInvoiceHttpPost(GET_AUTH_DATA, params, InvoiceAuthDataResult.class);
+ }
+
+ @Override
+ public void rejectInvoice(InvoiceRejectRequest params) throws WxErrorException {
+ doCommonInvoiceHttpPost(REJECT_INSERT, params, null);
+ }
+
+ @Override
+ public void makeOutInvoice(MakeOutInvoiceRequest params) throws WxErrorException {
+ doCommonInvoiceHttpPost(MAKE_OUT_INVOICE, params, null);
+ }
+
+ @Override
+ public void clearOutInvoice(ClearOutInvoiceRequest params) throws WxErrorException {
+ doCommonInvoiceHttpPost(CLEAR_OUT_INVOICE, params, null);
+ }
+
+ @Override
+ public InvoiceResult queryInvoiceInfo(String fpqqlsh, String nsrsbh) throws WxErrorException {
+ Map data = new HashMap();
+ data.put("fpqqlsh", fpqqlsh);
+ data.put("nsrsbh", nsrsbh);
+ return doCommonInvoiceHttpPost(QUERY_INVOICE_INFO, data, InvoiceResult.class);
+ }
+
+ @Override
+ public void setMerchantContactInfo(MerchantContactInfo contact) throws WxErrorException {
+ MerchantContactInfoWrapper data = new MerchantContactInfoWrapper();
+ data.setContact(contact);
+ doCommonInvoiceHttpPost(SET_CONTACT_SET_BIZ_ATTR, data, null);
+ }
+
+ @Override
+ public MerchantContactInfo getMerchantContactInfo() throws WxErrorException {
+ MerchantContactInfoWrapper merchantContactInfoWrapper = doCommonInvoiceHttpPost(GET_CONTACT_SET_BIZ_ATTR, null, MerchantContactInfoWrapper.class);
+ return merchantContactInfoWrapper == null ? null : merchantContactInfoWrapper.getContact();
+ }
+
+ @Override
+ public void setAuthPageSetting(InvoiceAuthPageSetting authPageSetting) throws WxErrorException {
+ doCommonInvoiceHttpPost(SET_AUTH_FIELD_SET_BIZ_ATTR, authPageSetting, null);
+ }
+
+ @Override
+ public InvoiceAuthPageSetting getAuthPageSetting() throws WxErrorException {
+ return doCommonInvoiceHttpPost(GET_AUTH_FIELD_SET_BIZ_ATTR, new JsonObject(), InvoiceAuthPageSetting.class);
+ }
+
+ @Override
+ public void setMerchantInvoicePlatform(MerchantInvoicePlatformInfo paymchInfo) throws WxErrorException {
+ MerchantInvoicePlatformInfoWrapper data = new MerchantInvoicePlatformInfoWrapper();
+ data.setPaymchInfo(paymchInfo);
+ doCommonInvoiceHttpPost(SET_PAY_MCH_SET_BIZ_ATTR, data, null);
+ }
+
+ @Override
+ public MerchantInvoicePlatformInfo getMerchantInvoicePlatform(MerchantInvoicePlatformInfo merchantInvoicePlatformInfo) throws WxErrorException {
+ MerchantInvoicePlatformInfoWrapper result = doCommonInvoiceHttpPost(GET_PAY_MCH_SET_BIZ_ATTR, new JsonObject(), MerchantInvoicePlatformInfoWrapper.class);
+ return result == null ? null : result.getPaymchInfo();
+ }
+
+ /**
+ * 电子发票公用post请求方法
+ */
+ private T doCommonInvoiceHttpPost(WxMpApiUrl url, Object data, Class resultClass) throws WxErrorException {
+ String json = "";
+ if (data != null) {
+ json = gson.toJson(data);
+ }
+ String responseText = wxMpService.post(url, json);
+ if (resultClass == null) return null;
+ return gson.fromJson(responseText, resultClass);
+ }
+}
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/ClearOutInvoiceRequest.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/ClearOutInvoiceRequest.java
new file mode 100644
index 000000000..108e76ca2
--- /dev/null
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/ClearOutInvoiceRequest.java
@@ -0,0 +1,50 @@
+package me.chanjar.weixin.mp.bean.invoice.merchant;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 发票充红请求参数
+ */
+@Data
+public class ClearOutInvoiceRequest implements Serializable {
+
+
+ private ClearOutInvoiceInfo invoiceinfo;
+
+ @Data
+ public static class ClearOutInvoiceInfo implements Serializable {
+
+ /**
+ * 用户的openid 用户知道是谁在开票
+ */
+ private String wxopenid;
+
+ /**
+ * 发票请求流水号,唯一查询发票的流水号
+ */
+ private String fpqqlsh;
+
+ /**
+ * 纳税人识别码
+ */
+ private String nsrsbh;
+
+ /**
+ * 纳税人名称
+ */
+ private String nsrmc;
+
+ /**
+ * 原发票代码,即要冲红的蓝票的发票代码
+ */
+ private String yfpdm;
+
+ /**
+ * 原发票号码,即要冲红的蓝票的发票号码
+ */
+ private String yfphm;
+
+ }
+}
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/InvoiceAuthDataRequest.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/InvoiceAuthDataRequest.java
new file mode 100644
index 000000000..092e16410
--- /dev/null
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/InvoiceAuthDataRequest.java
@@ -0,0 +1,23 @@
+package me.chanjar.weixin.mp.bean.invoice.merchant;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 获取电子开票用户授权数据
+ */
+@Data
+public class InvoiceAuthDataRequest implements Serializable {
+
+ /**
+ * 开票平台在微信的标识号,商户需要找开票平台提供
+ */
+ private String sPappid;
+
+ /**
+ * 订单id,在商户内单笔开票请求的唯一识别号
+ */
+ private String orderId;
+
+}
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/InvoiceAuthDataResult.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/InvoiceAuthDataResult.java
new file mode 100644
index 000000000..99d8f57cc
--- /dev/null
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/InvoiceAuthDataResult.java
@@ -0,0 +1,66 @@
+package me.chanjar.weixin.mp.bean.invoice.merchant;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 用户开票认证信息返回结果DTO
+ */
+@Data
+public class InvoiceAuthDataResult implements Serializable {
+
+ /**
+ * 订单授权状态,当errcode为0时会出现
+ */
+ private String invoiceStatus;
+
+ /**
+ * 授权时间,为十位时间戳(utc+8),当errcode为0时会出现
+ */
+ private Long authTime;
+
+ /**
+ * 用户授权信息
+ */
+ private UserAuthInfo userAuthInfo;
+
+ @Data
+ public static class UserAuthInfo implements Serializable {
+ /**
+ * 个人抬头
+ */
+ private UserField userField;
+
+ /**
+ * 单位抬头
+ */
+ private BizField bizField;
+ }
+
+ @Data
+ public static class UserField implements Serializable {
+ private String title;
+ private String phone;
+ private String email;
+ private List customField;
+ }
+
+ @Data
+ public static class BizField implements Serializable {
+ private String title;
+ private String taxNo;
+ private String addr;
+ private String phone;
+ private String bankType;
+ private String bankNo;
+ private List customField;
+ }
+
+ @Data
+ public static class KeyValuePair implements Serializable {
+ private String key;
+ private String value;
+ }
+}
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/InvoiceAuthPageRequest.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/InvoiceAuthPageRequest.java
new file mode 100644
index 000000000..07a8a24e5
--- /dev/null
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/InvoiceAuthPageRequest.java
@@ -0,0 +1,52 @@
+package me.chanjar.weixin.mp.bean.invoice.merchant;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 获取授权地址的输入参数
+ */
+@Data
+public class InvoiceAuthPageRequest implements Serializable {
+
+ /**
+ * 开票平台在微信的标识号,商户需要找开票平台提供
+ */
+ private String sPappid;
+
+ /**
+ * 订单id,在商户内单笔开票请求的唯一识别号
+ */
+ private String orderId;
+
+ /**
+ * 订单金额,以分为单位
+ */
+ private Long money;
+
+ /**
+ * 开票来源
+ */
+ private String source;
+
+ /**
+ * 授权成功后跳转页面。本字段只有在source为H5的时候需要填写,引导用户在微信中进行下一步流程。app开票因为从外部app拉起微信授权页,授权完成后自动回到原来的app,故无需填写。
+ */
+ private String redirectUrl;
+
+ /**
+ * 授权类型,0:开票授权,1:填写字段开票授权,2:领票授权
+ */
+ private Integer type;
+
+ /**
+ * 时间戳单位s
+ */
+ private Long timestamp;
+
+ /**
+ * 内部填充(请务设置)
+ */
+ private String ticket;
+}
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/InvoiceAuthPageResult.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/InvoiceAuthPageResult.java
new file mode 100644
index 000000000..d137adb49
--- /dev/null
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/InvoiceAuthPageResult.java
@@ -0,0 +1,22 @@
+package me.chanjar.weixin.mp.bean.invoice.merchant;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 获取授权链接返回结果DTO
+ */
+@Data
+public class InvoiceAuthPageResult implements Serializable {
+
+ /**
+ * 授权页地址
+ */
+ private String authUrl;
+
+ /**
+ * 当发起端为小程序时, 返回
+ */
+ private String appid;
+}
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/InvoiceAuthPageSetting.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/InvoiceAuthPageSetting.java
new file mode 100644
index 000000000..dbc04816e
--- /dev/null
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/InvoiceAuthPageSetting.java
@@ -0,0 +1,61 @@
+package me.chanjar.weixin.mp.bean.invoice.merchant;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+public class InvoiceAuthPageSetting implements Serializable {
+
+ private AuthField authField;
+
+ @Data
+ public static class AuthField implements Serializable {
+ private UserField userField;
+ private BizField bizField;
+ }
+
+ @Data
+ public static class UserField implements Serializable {
+ private Integer showTitle;
+ private Integer showPhone;
+ private Integer showEmail;
+ private Integer requirePhone;
+ private Integer requireEmail;
+ private List customField;
+ }
+
+ @Data
+ public static class BizField implements Serializable {
+ private Integer showTitle;
+ private Integer showTaxNo;
+ private Integer showAddr;
+ private Integer showPhone;
+ private Integer showBankType;
+ private Integer showBankNo;
+
+ private Integer requireTaxNo;
+ private Integer requireAddr;
+ private Integer requirePhone;
+ private Integer requireBankType;
+ private Integer requireBankNo;
+ private List customField;
+ }
+
+ @Data
+ public static class CustomField implements Serializable {
+ /**
+ * 字段名
+ */
+ private String key;
+ /**
+ * 0:否,1:是, 默认为0
+ */
+ private Integer isRequire;
+ /**
+ * 提示文案
+ */
+ private String notice;
+ }
+}
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/InvoiceRejectRequest.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/InvoiceRejectRequest.java
new file mode 100644
index 000000000..9048ceb05
--- /dev/null
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/InvoiceRejectRequest.java
@@ -0,0 +1,30 @@
+package me.chanjar.weixin.mp.bean.invoice.merchant;
+
+import java.io.Serializable;
+
+/**
+ * 拒绝开票请求参数
+ */
+public class InvoiceRejectRequest implements Serializable {
+
+ /**
+ * 开票平台标示
+ */
+ private String sPappid;
+
+ /**
+ * 订单id
+ */
+ private String orderId;
+
+ /**
+ * 拒绝原因
+ */
+ private String reason;
+
+ /**
+ * 引导用户跳转url
+ */
+ private String url;
+
+}
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/InvoiceResult.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/InvoiceResult.java
new file mode 100644
index 000000000..6f4da63a2
--- /dev/null
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/InvoiceResult.java
@@ -0,0 +1,52 @@
+package me.chanjar.weixin.mp.bean.invoice.merchant;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 电子发票信息查询结果
+ */
+@Data
+public class InvoiceResult implements Serializable {
+
+ /**
+ * 发票相关信息
+ */
+ private InvoiceDetail invoicedetail;
+
+ @Data
+ public static class InvoiceDetail implements Serializable {
+ /**
+ * 发票流水号
+ */
+ private String fpqqlsh;
+
+ /**
+ * 检验码
+ */
+ private String jym;
+
+ /**
+ * 校验码
+ */
+ private String kprq;
+
+ /**
+ * 发票代码
+ */
+ private String fpdm;
+
+ /**
+ * 发票号码
+ */
+ private String fphm;
+
+ /**
+ * 发票url
+ */
+ private String pdfurl;
+
+ }
+
+}
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/MakeOutInvoiceRequest.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/MakeOutInvoiceRequest.java
new file mode 100644
index 000000000..d9336eac1
--- /dev/null
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/MakeOutInvoiceRequest.java
@@ -0,0 +1,200 @@
+package me.chanjar.weixin.mp.bean.invoice.merchant;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 开票信息请求参数
+ */
+@Data
+public class MakeOutInvoiceRequest implements Serializable {
+
+ private InvoiceInfo invoiceinfo;
+
+ /**
+ * 发票信息
+ */
+ @Data
+ public static class InvoiceInfo implements Serializable {
+ /**
+ * 维修openid
+ */
+ private String wxopenid;
+
+ /**
+ * 订单号
+ */
+ private String ddh;
+
+ /**
+ * 发票请求流水号,唯一识别开票请求的流水号
+ */
+ private String fpqqlsh;
+
+ /**
+ * 纳税人识别码
+ */
+ private String nsrsbh;
+
+ /**
+ * 纳税人名称
+ */
+ private String nsrmc;
+
+ /**
+ * 纳税人地址
+ */
+ private String nsrdz;
+
+ /**
+ * 纳税人电话
+ */
+ private String nsrdh;
+
+ /**
+ * 纳税人开户行
+ */
+ private String nsrbank;
+
+ /**
+ * 纳税人银行账号
+ */
+ private String nsrbankid;
+
+ /**
+ * 购货方名称
+ */
+ private String ghfnsrsbh;
+
+ /**
+ * 购货方识别号
+ */
+ private String ghfmc;
+
+ /**
+ * 购货方地址
+ */
+ private String ghfdz;
+
+ /**
+ * 购货方电话
+ */
+ private String ghfdh;
+
+ /**
+ * 购货方开户行
+ */
+ private String ghfbank;
+
+ /**
+ * 购货方银行帐号
+ */
+ private String ghfbankid;
+
+ /**
+ * 开票人
+ */
+ private String kpr;
+
+ /**
+ * 收款人
+ */
+ private String skr;
+
+ /**
+ * 复核人
+ */
+ private String fhr;
+
+ /**
+ * 价税合计
+ */
+ private String jshj;
+
+ /**
+ * 合计金额
+ */
+ private String hjje;
+
+ /**
+ * 合计税额
+ */
+ private String hjse;
+
+ /**
+ * 备注
+ */
+ private String bz;
+
+ /**
+ * 行业类型 0 商业 1其它
+ */
+ private String hylx;
+
+ /**
+ * 发票商品条目
+ */
+ private List invoicedetailList;
+
+ }
+
+ /**
+ * 发票条目
+ */
+ @Data
+ public static class InvoiceDetailItem implements Serializable {
+ /**
+ * 发票性质
+ */
+ private String fphxz;
+
+ /**
+ * 19位税收分类编码
+ */
+ private String spbm;
+
+ /**
+ * 项目名称
+ */
+ private String xmmc;
+
+ /**
+ * 计量单位
+ */
+ private String dw;
+
+ /**
+ * 规格型号
+ */
+ private String ggxh;
+
+ /**
+ * 项目数量
+ */
+ private String xmsl;
+
+ /**
+ * 项目单价
+ */
+ private String xmdj;
+
+ /**
+ * 项目金额 不含税,单位元 两位小数
+ */
+ private String xmje;
+
+ /**
+ * 税率 精确到两位小数 如0.01
+ */
+ private String sl;
+
+ /**
+ * 税额 单位元 两位小数
+ */
+ private String se;
+
+ }
+
+}
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/MerchantContactInfo.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/MerchantContactInfo.java
new file mode 100644
index 000000000..569fffa6b
--- /dev/null
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/MerchantContactInfo.java
@@ -0,0 +1,22 @@
+package me.chanjar.weixin.mp.bean.invoice.merchant;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 商户联系信息
+ */
+@Data
+public class MerchantContactInfo implements Serializable {
+ /**
+ * 联系电话
+ */
+ private String phone;
+
+ /**
+ * 开票超时时间
+ */
+ private Integer timeout;
+
+}
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/MerchantContactInfoWrapper.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/MerchantContactInfoWrapper.java
new file mode 100644
index 000000000..3ceed2b6d
--- /dev/null
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/MerchantContactInfoWrapper.java
@@ -0,0 +1,16 @@
+package me.chanjar.weixin.mp.bean.invoice.merchant;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 设置商户联系信息和发票过时时间参数
+ */
+@Data
+public class MerchantContactInfoWrapper implements Serializable {
+
+ private MerchantContactInfo contact;
+
+
+}
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/MerchantInvoicePlatformInfo.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/MerchantInvoicePlatformInfo.java
new file mode 100644
index 000000000..f7a64edfc
--- /dev/null
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/MerchantInvoicePlatformInfo.java
@@ -0,0 +1,19 @@
+package me.chanjar.weixin.mp.bean.invoice.merchant;
+
+import java.io.Serializable;
+
+/**
+ * 商户的开票平台信息
+ */
+public class MerchantInvoicePlatformInfo implements Serializable {
+
+ /**
+ * 微信支付商户号
+ */
+ private String mchid;
+
+ /**
+ * 为该商户提供开票服务的开票平台 id ,由开票平台提供给商户
+ */
+ private String sPappid;
+}
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/MerchantInvoicePlatformInfoWrapper.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/MerchantInvoicePlatformInfoWrapper.java
new file mode 100644
index 000000000..035146616
--- /dev/null
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/MerchantInvoicePlatformInfoWrapper.java
@@ -0,0 +1,15 @@
+package me.chanjar.weixin.mp.bean.invoice.merchant;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 设置商户联系信息和发票过时时间参数
+ */
+@Data
+public class MerchantInvoicePlatformInfoWrapper implements Serializable {
+
+ private MerchantInvoicePlatformInfo paymchInfo;
+
+}
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/enums/WxMpApiUrl.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/enums/WxMpApiUrl.java
index cbe3d4df9..9bc0e1de0 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/enums/WxMpApiUrl.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/enums/WxMpApiUrl.java
@@ -1082,4 +1082,78 @@ public interface WxMpApiUrl {
}
}
+ @AllArgsConstructor
+ enum Invoice implements WxMpApiUrl {
+
+ /**
+ * 获取用户开票授权地址
+ */
+ GET_AUTH_URL(API_DEFAULT_HOST_URL, "/card/invoice/getauthurl"),
+
+ /**
+ * 获取用户开票授权信息
+ */
+ GET_AUTH_DATA(API_DEFAULT_HOST_URL, "/card/invoice/getauthdata"),
+
+ /**
+ * 拒绝为用户开票
+ */
+ REJECT_INSERT(API_DEFAULT_HOST_URL, "/card/invoice/rejectinsert"),
+
+ /**
+ * 开票
+ */
+ MAKE_OUT_INVOICE(API_DEFAULT_HOST_URL, "/card/invoice/makeoutinvoice"),
+
+ /**
+ * 发票冲红
+ */
+ CLEAR_OUT_INVOICE(API_DEFAULT_HOST_URL, "/card/invoice/clearoutinvoice"),
+
+ /**
+ * 查询发票信息
+ */
+ QUERY_INVOICE_INFO(API_DEFAULT_HOST_URL, "/card/invoice/queryinvoceinfo"),
+
+ /**
+ * 设置商户信息联系
+ */
+ SET_CONTACT_SET_BIZ_ATTR(API_DEFAULT_HOST_URL, "/card/invoice/setbizattr?action=set_contact"),
+
+ /**
+ * 获取商户联系信息
+ */
+ GET_CONTACT_SET_BIZ_ATTR(API_DEFAULT_HOST_URL, "/card/invoice/setbizattr?action=get_contact"),
+
+ /**
+ * 设置授权页面字段
+ */
+ SET_AUTH_FIELD_SET_BIZ_ATTR(API_DEFAULT_HOST_URL, "/card/invoice/setbizattr?action=set_auth_field"),
+
+ /**
+ * 获取授权页面字段
+ */
+ GET_AUTH_FIELD_SET_BIZ_ATTR(API_DEFAULT_HOST_URL, "/card/invoice/setbizattr?action=get_auth_field"),
+
+ /**
+ * 设置关联商户
+ */
+ SET_PAY_MCH_SET_BIZ_ATTR(API_DEFAULT_HOST_URL, "/card/invoice/setbizattr?action=set_pay_mch"),
+
+ /**
+ * 获取关联商户
+ */
+ GET_PAY_MCH_SET_BIZ_ATTR(API_DEFAULT_HOST_URL, "/card/invoice/setbizattr?action=get_pay_mch"),
+ ;
+ private String prefix;
+ private String path;
+
+ @Override
+ public String getUrl(WxMpConfigStorage config) {
+ if (null == config) {
+ return buildUrl(null, prefix, path);
+ }
+ return buildUrl(config.getHostConfig(), prefix, path);
+ }
+ }
}
diff --git a/weixin-java-open/src/main/java/me/chanjar/weixin/open/api/impl/WxOpenInMemoryConfigStorage.java b/weixin-java-open/src/main/java/me/chanjar/weixin/open/api/impl/WxOpenInMemoryConfigStorage.java
index bcd6001e5..723ec3806 100644
--- a/weixin-java-open/src/main/java/me/chanjar/weixin/open/api/impl/WxOpenInMemoryConfigStorage.java
+++ b/weixin-java-open/src/main/java/me/chanjar/weixin/open/api/impl/WxOpenInMemoryConfigStorage.java
@@ -4,10 +4,10 @@ package me.chanjar.weixin.open.api.impl;
import cn.binarywang.wx.miniapp.config.WxMaConfig;
import lombok.Data;
import me.chanjar.weixin.common.bean.WxAccessToken;
+import me.chanjar.weixin.common.enums.TicketType;
import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder;
import me.chanjar.weixin.mp.bean.WxMpHostConfig;
import me.chanjar.weixin.mp.config.WxMpConfigStorage;
-import me.chanjar.weixin.common.enums.TicketType;
import me.chanjar.weixin.open.api.WxOpenConfigStorage;
import me.chanjar.weixin.open.bean.WxOpenAuthorizerAccessToken;
import me.chanjar.weixin.open.bean.WxOpenComponentAccessToken;
@@ -46,9 +46,6 @@ public class WxOpenInMemoryConfigStorage implements WxOpenConfigStorage {
private Map cardApiTickets = new ConcurrentHashMap<>();
private Map locks = new ConcurrentHashMap<>();
- private Lock componentAccessTokenLock = getLockByKey("componentAccessTokenLock");
-
-
@Override
public boolean isComponentAccessTokenExpired() {
return System.currentTimeMillis() > componentExpiresTime;
@@ -64,11 +61,25 @@ public class WxOpenInMemoryConfigStorage implements WxOpenConfigStorage {
updateComponentAccessToken(componentAccessToken.getComponentAccessToken(), componentAccessToken.getExpiresIn());
}
+ private Lock accessTokenLockInstance;
+
@Override
- public Lock getLockByKey(String key){
+ public Lock getComponentAccessTokenLock() {
+ if (this.accessTokenLockInstance == null) {
+ synchronized (this) {
+ if (this.accessTokenLockInstance == null) {
+ this.accessTokenLockInstance = getLockByKey("componentAccessTokenLock");
+ }
+ }
+ }
+ return this.accessTokenLockInstance;
+ }
+
+ @Override
+ public Lock getLockByKey(String key) {
Lock lock = locks.get(key);
if (lock == null) {
- synchronized (WxOpenInMemoryConfigStorage.class){
+ synchronized (this) {
lock = locks.get(key);
if (lock == null) {
lock = new ReentrantLock();