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