| @@ -23,7 +23,11 @@ public class JedisWxRedisOps implements WxRedisOps { | |||||
| @Override | @Override | ||||
| public void setValue(String key, String value, int expire, TimeUnit timeUnit) { | public void setValue(String key, String value, int expire, TimeUnit timeUnit) { | ||||
| try (Jedis jedis = this.jedisPool.getResource()) { | 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); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -19,7 +19,11 @@ public class RedisTemplateWxRedisOps implements WxRedisOps { | |||||
| @Override | @Override | ||||
| public void setValue(String key, String value, int expire, TimeUnit timeUnit) { | 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 | @Override | ||||
| @@ -19,12 +19,20 @@ public class RedissonWxRedisOps implements WxRedisOps { | |||||
| @Override | @Override | ||||
| public void setValue(String key, String value, int expire, TimeUnit timeUnit) { | 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 | @Override | ||||
| public Long getExpire(String key) { | 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 | @Override | ||||
| @@ -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")); | |||||
| } | |||||
| } | |||||
| @@ -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(); | |||||
| } | |||||
| } | |||||
| @@ -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() { | |||||
| } | |||||
| } | |||||
| @@ -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(); | |||||
| } | |||||
| } | |||||
| @@ -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; | |||||
| } | |||||
| @@ -6,10 +6,13 @@ import com.google.gson.JsonArray; | |||||
| import com.google.gson.JsonElement; | import com.google.gson.JsonElement; | ||||
| import com.google.gson.JsonObject; | import com.google.gson.JsonObject; | ||||
| import com.google.gson.JsonParser; | import com.google.gson.JsonParser; | ||||
| import lombok.Getter; | |||||
| import lombok.Setter; | |||||
| import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||
| import me.chanjar.weixin.common.WxType; | import me.chanjar.weixin.common.WxType; | ||||
| import me.chanjar.weixin.common.bean.WxJsapiSignature; | import me.chanjar.weixin.common.bean.WxJsapiSignature; | ||||
| import me.chanjar.weixin.common.bean.WxNetCheckResult; | 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.WxError; | ||||
| import me.chanjar.weixin.common.error.WxErrorException; | import me.chanjar.weixin.common.error.WxErrorException; | ||||
| import me.chanjar.weixin.common.session.StandardSessionManager; | 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.WxMpSemanticQueryResult; | ||||
| import me.chanjar.weixin.mp.bean.result.WxMpUser; | import me.chanjar.weixin.mp.bean.result.WxMpUser; | ||||
| import me.chanjar.weixin.mp.config.WxMpConfigStorage; | 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.enums.WxMpApiUrl; | ||||
| import me.chanjar.weixin.mp.util.WxMpConfigStorageHolder; | import me.chanjar.weixin.mp.util.WxMpConfigStorageHolder; | ||||
| import org.apache.commons.lang3.StringUtils; | 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 WxMpOcrService ocrService = new WxMpOcrServiceImpl(this); | ||||
| private WxMpImgProcService imgProcService = new WxMpImgProcServiceImpl(this); | private WxMpImgProcService imgProcService = new WxMpImgProcServiceImpl(this); | ||||
| @Getter | |||||
| @Setter | |||||
| private WxMpMerchantInvoiceService merchantInvoiceService = new WxMpMerchantInvoiceServiceImpl(this, this.cardService); | |||||
| private Map<String, WxMpConfigStorage> configStorageMap; | private Map<String, WxMpConfigStorage> configStorageMap; | ||||
| private int retrySleepMillis = 1000; | private int retrySleepMillis = 1000; | ||||
| @@ -359,11 +365,11 @@ public abstract class BaseWxMpServiceImpl<H, P> implements WxMpService, RequestH | |||||
| // 强制设置wxMpConfigStorage它的access token过期了,这样在下一次请求里就会刷新access token | // 强制设置wxMpConfigStorage它的access token过期了,这样在下一次请求里就会刷新access token | ||||
| Lock lock = this.getWxMpConfigStorage().getAccessTokenLock(); | Lock lock = this.getWxMpConfigStorage().getAccessTokenLock(); | ||||
| lock.lock(); | lock.lock(); | ||||
| try{ | |||||
| if(StringUtils.equals(this.getWxMpConfigStorage().getAccessToken(), accessToken)){ | |||||
| try { | |||||
| if (StringUtils.equals(this.getWxMpConfigStorage().getAccessToken(), accessToken)) { | |||||
| this.getWxMpConfigStorage().expireAccessToken(); | this.getWxMpConfigStorage().expireAccessToken(); | ||||
| } | } | ||||
| } catch (Exception ex){ | |||||
| } catch (Exception ex) { | |||||
| this.getWxMpConfigStorage().expireAccessToken(); | this.getWxMpConfigStorage().expireAccessToken(); | ||||
| } finally { | } finally { | ||||
| lock.unlock(); | lock.unlock(); | ||||
| @@ -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); | |||||
| } | |||||
| } | |||||
| @@ -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; | |||||
| } | |||||
| } | |||||
| @@ -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; | |||||
| } | |||||
| @@ -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; | |||||
| } | |||||
| } | |||||
| @@ -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; | |||||
| } | |||||
| @@ -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; | |||||
| } | |||||
| @@ -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; | |||||
| } | |||||
| } | |||||
| @@ -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; | |||||
| } | |||||
| @@ -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; | |||||
| } | |||||
| } | |||||
| @@ -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; | |||||
| } | |||||
| } | |||||
| @@ -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; | |||||
| } | |||||
| @@ -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; | |||||
| } | |||||
| @@ -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; | |||||
| } | |||||
| @@ -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; | |||||
| } | |||||
| @@ -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); | |||||
| } | |||||
| } | |||||
| } | } | ||||
| @@ -4,10 +4,10 @@ package me.chanjar.weixin.open.api.impl; | |||||
| import cn.binarywang.wx.miniapp.config.WxMaConfig; | import cn.binarywang.wx.miniapp.config.WxMaConfig; | ||||
| import lombok.Data; | import lombok.Data; | ||||
| import me.chanjar.weixin.common.bean.WxAccessToken; | 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.common.util.http.apache.ApacheHttpClientBuilder; | ||||
| import me.chanjar.weixin.mp.bean.WxMpHostConfig; | import me.chanjar.weixin.mp.bean.WxMpHostConfig; | ||||
| import me.chanjar.weixin.mp.config.WxMpConfigStorage; | 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.api.WxOpenConfigStorage; | ||||
| import me.chanjar.weixin.open.bean.WxOpenAuthorizerAccessToken; | import me.chanjar.weixin.open.bean.WxOpenAuthorizerAccessToken; | ||||
| import me.chanjar.weixin.open.bean.WxOpenComponentAccessToken; | 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, Token> cardApiTickets = new ConcurrentHashMap<>(); | ||||
| private Map<String, Lock> locks = new ConcurrentHashMap<>(); | private Map<String, Lock> locks = new ConcurrentHashMap<>(); | ||||
| private Lock componentAccessTokenLock = getLockByKey("componentAccessTokenLock"); | |||||
| @Override | @Override | ||||
| public boolean isComponentAccessTokenExpired() { | public boolean isComponentAccessTokenExpired() { | ||||
| return System.currentTimeMillis() > componentExpiresTime; | return System.currentTimeMillis() > componentExpiresTime; | ||||
| @@ -64,11 +61,25 @@ public class WxOpenInMemoryConfigStorage implements WxOpenConfigStorage { | |||||
| updateComponentAccessToken(componentAccessToken.getComponentAccessToken(), componentAccessToken.getExpiresIn()); | 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 | @Override | ||||
| public Lock getLockByKey(String key){ | |||||
| public Lock getLockByKey(String key) { | |||||
| Lock lock = locks.get(key); | Lock lock = locks.get(key); | ||||
| if (lock == null) { | if (lock == null) { | ||||
| synchronized (WxOpenInMemoryConfigStorage.class){ | |||||
| synchronized (this) { | |||||
| lock = locks.get(key); | lock = locks.get(key); | ||||
| if (lock == null) { | if (lock == null) { | ||||
| lock = new ReentrantLock(); | lock = new ReentrantLock(); | ||||