Browse Source

:bug: #1546 修复WxRedisOps问题, #1548 修复WxOpenInMemoryConfigStorage锁问题,#1305 增加商户电子发票功能

Mario Luo 5 years ago
parent
commit
058ce62a2b
25 changed files with 1078 additions and 14 deletions
  1. 5 1
      weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/JedisWxRedisOps.java
  2. 5 1
      weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedisTemplateWxRedisOps.java
  3. 10 2
      weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedissonWxRedisOps.java
  4. 51 0
      weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/CommonWxRedisOpsTest.java
  5. 21 0
      weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/JedisWxRedisOpsTest.java
  6. 26 0
      weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/RedisTemplateWxRedisOpsTest.java
  7. 27 0
      weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/RedissonWxRedisOpsTest.java
  8. 85 0
      weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpMerchantInvoiceService.java
  9. 10 4
      weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/BaseWxMpServiceImpl.java
  10. 119 0
      weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpMerchantInvoiceServiceImpl.java
  11. 50 0
      weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/ClearOutInvoiceRequest.java
  12. 23 0
      weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/InvoiceAuthDataRequest.java
  13. 66 0
      weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/InvoiceAuthDataResult.java
  14. 52 0
      weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/InvoiceAuthPageRequest.java
  15. 22 0
      weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/InvoiceAuthPageResult.java
  16. 61 0
      weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/InvoiceAuthPageSetting.java
  17. 30 0
      weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/InvoiceRejectRequest.java
  18. 52 0
      weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/InvoiceResult.java
  19. 200 0
      weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/MakeOutInvoiceRequest.java
  20. 22 0
      weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/MerchantContactInfo.java
  21. 16 0
      weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/MerchantContactInfoWrapper.java
  22. 19 0
      weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/MerchantInvoicePlatformInfo.java
  23. 15 0
      weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/invoice/merchant/MerchantInvoicePlatformInfoWrapper.java
  24. 74 0
      weixin-java-mp/src/main/java/me/chanjar/weixin/mp/enums/WxMpApiUrl.java
  25. 17 6
      weixin-java-open/src/main/java/me/chanjar/weixin/open/api/impl/WxOpenInMemoryConfigStorage.java

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

+ 5 - 1
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

+ 10 - 2
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

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

+ 21 - 0
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();
+  }
+}

+ 26 - 0
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() {
+  }
+}

+ 27 - 0
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();
+  }
+}

+ 85 - 0
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.*;
+
+/**
+ * 商户电子发票相关的接口
+ * <p>
+ * 重要!!!, 根据不同开票平台, 以下错误码可能开票成功(开票,冲红), 内部暂时未处理:
+ * 73105: 开票平台开票中,请使用相同的发票请求流水号重试开票
+ * 73107: 发票请求流水正在被处理,请通过查询接口获取结果
+ * 73100: 开票平台错误
+ * <p>
+ * 流程文档: 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;
+
+  /**
+   * 拒绝开票
+   * <p>
+   * 场景: 用户授权填写数据无效
+   * 结果: 用户会收到一条开票失败提示
+   */
+  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;
+}

+ 10 - 4
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<H, P> 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<String, WxMpConfigStorage> configStorageMap;
 
   private int retrySleepMillis = 1000;
@@ -359,11 +365,11 @@ public abstract class BaseWxMpServiceImpl<H, P> 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();

+ 119 - 0
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> T doCommonInvoiceHttpPost(WxMpApiUrl url, Object data, Class<T> 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);
+  }
+}

+ 50 - 0
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;
+
+  }
+}

+ 23 - 0
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;
+
+}

+ 66 - 0
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<KeyValuePair> 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<KeyValuePair> customField;
+  }
+
+  @Data
+  public static class KeyValuePair implements Serializable {
+    private String key;
+    private String value;
+  }
+}

+ 52 - 0
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;
+}

+ 22 - 0
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;
+}

+ 61 - 0
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<InvoiceAuthDataResult.KeyValuePair> 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<InvoiceAuthDataResult.KeyValuePair> customField;
+  }
+
+  @Data
+  public static class CustomField implements Serializable {
+    /**
+     * 字段名
+     */
+    private String key;
+    /**
+     * 0:否,1:是, 默认为0
+     */
+    private Integer isRequire;
+    /**
+     * 提示文案
+     */
+    private String notice;
+  }
+}

+ 30 - 0
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;
+
+}

+ 52 - 0
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;
+
+  }
+
+}

+ 200 - 0
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<InvoiceDetailItem> 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;
+
+  }
+
+}

+ 22 - 0
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;
+
+}

+ 16 - 0
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;
+
+
+}

+ 19 - 0
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;
+}

+ 15 - 0
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;
+
+}

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

+ 17 - 6
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<String, Token> cardApiTickets = new ConcurrentHashMap<>();
   private Map<String, Lock> 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 getComponentAccessTokenLock() {
+    if (this.accessTokenLockInstance == null) {
+      synchronized (this) {
+        if (this.accessTokenLockInstance == null) {
+          this.accessTokenLockInstance = getLockByKey("componentAccessTokenLock");
+        }
+      }
+    }
+    return this.accessTokenLockInstance;
+  }
+
   @Override
-  public Lock getLockByKey(String key){
+  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();