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