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

This commit is contained in:
Mario Luo 2020-05-12 18:17:17 +08:00 committed by GitHub
parent 609b38a9db
commit 058ce62a2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1078 additions and 14 deletions

View File

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

View File

@ -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

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
}

View File

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

View File

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

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
/**
* 01 默认为0
*/
private Integer isRequire;
/**
* 提示文案
*/
private String notice;
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

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

View File

@ -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 getLockByKey(String key){
public Lock getComponentAccessTokenLock() {
if (this.accessTokenLockInstance == null) {
synchronized (this) {
if (this.accessTokenLockInstance == null) {
this.accessTokenLockInstance = getLockByKey("componentAccessTokenLock");
}
}
}
return this.accessTokenLockInstance;
}
@Override
public Lock getLockByKey(String key) {
Lock lock = locks.get(key);
if (lock == null) {
synchronized (WxOpenInMemoryConfigStorage.class){
synchronized (this) {
lock = locks.get(key);
if (lock == null) {
lock = new ReentrantLock();