wxpay-sdk/src/main/java/com/weixin/pay/util/WXUtils.java

624 lines
25 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

package com.weixin.pay.util;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.weixin.pay.card.CardBgColorEnum;
import com.weixin.pay.constants.WXConstants;
import com.weixin.pay.constants.WXPayConstants;
import com.weixin.pay.constants.WXURL;
import com.weixin.pay.constants.WeChatURL;
import com.weixin.pay.redis.RedisKeyEnum;
import com.weixin.pay.redis.RedisKeyUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* 微信小工具类
*
* @author yclimb
* @date 2018/8/17
*/
@Slf4j
@Component
public class WXUtils {
@Resource
private RestTemplate restTemplate;
@Resource
private RedisTemplate redisTemplate;
/**
* 获取微信全局accessToken
*
* @param code 标识
* @return accessToken
*/
public String getAccessToken(String code) {
// 取redis数据
String key = WXConstants.WECHAT_ACCESSTOKEN + code;
String accessToken = (String) redisTemplate.opsForValue().get(key);
if (accessToken != null) {
return accessToken;
}
// 通过接口取得access_token
JSONObject jsonObject = restTemplate.getForObject(MessageFormat.format(WXURL.BASE_ACCESS_TOKEN, WXPayConstants.APP_ID, WXPayConstants.SECRET), JSONObject.class);
String token = (String) jsonObject.get("access_token");
if (StringUtils.isNotBlank(token)) {
// 存储redis
redisTemplate.opsForValue().set(key, token, 7000, TimeUnit.SECONDS);
return token;
} else {
log.error("获取微信accessToken出错微信返回信息为[{}]", jsonObject.toString());
}
return null;
}
/**
* 获取小程序静默登录返回信息
*
* @param code code
* @param appId appId
* @param appSecret appSecret
* @return json
*/
public JSONObject getMiniBaseUserInfo(String code, String appId, String appSecret) {
log.info("getMiniBaseUserInfo:params:[{}]", code);
String data = restTemplate.getForObject(WXURL.WX_MINI_LOGIN, String.class, appId, appSecret, code);
log.info("getMiniBaseUserInfo:result:[{}]", data);
return JSONObject.parseObject(data);
}
/**
* 网页授权获取用户信息时用于获取access_token以及openid
* 请求路径https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code(最后一个参数不变)
* @param code c
* @return access_token json obj
*
* @author yclimb
* @date 2018/7/30
*/
public JSONObject getJsapiAccessTokenByCode(String code) {
if (StringUtils.isBlank(code)) {
return null;
}
try {
// 获取access_token
String access_token_json = restTemplate.getForObject(WXURL.OAUTH_ACCESS_TOKEN_URL, String.class,
WXPayConstants.APP_ID_XXX, WXPayConstants.SECRET_XXX, code);
log.info("getAccessToken:access_token_json:{}", access_token_json);
if (StringUtils.isBlank(access_token_json)) {
return null;
}
JSONObject jsonObject = JSON.parseObject(access_token_json);
if (StringUtils.isBlank(jsonObject.getString("access_token"))) {
return null;
}
return jsonObject;
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return null;
}
/**
* 通过access_token和openid请求获取用户信息
* 请求路径https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN
* @param access_token t
* @param openid o
* @return userinfo json obj
*
* @author yclimb
* @date 2018/7/30
*/
public JSONObject getJsapiUserinfo(String access_token, String openid) {
if (StringUtils.isBlank(access_token) || StringUtils.isBlank(openid)) {
return null;
}
try {
// 获取access_token和openid
String userinfo_json = restTemplate.getForObject(WXURL.OAUTH_GET_USERINFO_URL, String.class, access_token, openid);
log.info("getUserinfo:userinfo_json:{}", userinfo_json);
if (StringUtils.isBlank(userinfo_json)) {
return null;
}
JSONObject jsonObject = JSON.parseObject(userinfo_json);
if (0 != jsonObject.getIntValue("errcode")) {
return null;
}
return jsonObject;
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return null;
}
/**
* 生成带参数的小程序二维码[]
*
* @param scene 参数
* @param page 小程序页面
* @return img path
*/
public String getWxMiniQRImg(String scene, String page) {
InputStream inputStream = null;
String imgUrl = "";
try {
// redis key
String redisKey = RedisKeyUtil.keyBuilder(RedisKeyEnum.XXX_MINI_WX_CODE, scene + RedisKeyUtil.KEY_SPLIT_CHAR + page);
// 从redis中获取缓存图片
Object obj = redisTemplate.opsForValue().get(redisKey);
if (obj != null) {
return obj.toString();
}
// 获取微信永久无限制二维码
byte[] code = this.getwxacodeunlimit(scene, page);
if (code == null || code.length <= 0) {
return imgUrl;
}
// 将返回字节数组转为输入流
inputStream = new ByteArrayInputStream(code);
// 取得uuid的文件名称
String newFileName = UUID.randomUUID().toString().replaceAll("-", "").replace(".", "") + ".png";
log.info("getWxMiniQRImg:fileName:" + newFileName);
// 上传图片到OSS服务器
// imgUrl = ossUtils.uploadOss(inputStream, ossUtils.getImgPathYYYYMMDD(), newFileName);
// 图片为空直接返回
if (StringUtils.isBlank(imgUrl)) {
return imgUrl;
}
// 设置到redis中下次取直接拿缓存即可防止多次生成
redisTemplate.opsForValue().set(redisKey, imgUrl);
} catch (Exception e) {
log.error("getWxMiniQRImg:调用小程序生成微信永久小程序码URL接口异常", e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
}
return imgUrl;
}
/**
* 获取 application/json;charset=UTF-8 的 HttpHeaders 对象
*
* @return HttpHeaders
* @author yclimb
* @date 2018/7/18
*/
public HttpHeaders getHttpHeadersUTF8JSON() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
headers.add("Accept", MediaType.APPLICATION_JSON_VALUE);
return headers;
}
/**
* 作用:生成永久无限制微信二维码<br>
* 场景:微信二维码生成,根据参数和页面配置微信二维码,返回二维码字节流
* 接口链接https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=ACCESS_TOKEN
* 接口文档地址https://developers.weixin.qq.com/miniprogram/dev/api/qrcode.html
*
* @param scene 最大32个可见字符只支持数字大小写英文以及部分特殊字符!#$&'()*+,/:;=?@-._~,其它字符请自行编码为合法字符(因不支持%,中文无法使用 urlencode 处理,请使用其他编码方式)
* @param page 必须是已经发布的小程序存在的页面(否则报错),例如 "pages/index/index" ,根路径前不要填加'/',不能携带参数参数请放在scene字段里如果不填写这个字段默认跳主页面
* @return 二维码字节流
* @author yclimb
* @date 2018/7/18
*/
public byte[] getwxacodeunlimit(String scene, String page) {
try {
// 获取access token
String accessToken = this.getAccessToken("xxx");
// 拼接传入参数
Map<String, Object> param = new HashMap<>(5);
param.put("scene", scene);
param.put("page", page);
// 默认430二维码的宽度最小为280
param.put("width", 280);
// 默认false自动配置线条颜色如果颜色依然是黑色则说明不建议配置主色调
param.put("auto_color", false);
// 默认:{"r":"0","g":"0","b":"0"}二维码图片颜色参数auto_color 为 false 时生效,使用 rgb 设置颜色 例如 {"r":"xxx","g":"xxx","b":"xxx"} 十进制表示
Map<String, Object> line_color = new HashMap<>(3);
line_color.put("r", 0);
line_color.put("g", 0);
line_color.put("b", 0);
param.put("line_color", line_color);
// map转换为json传输
String jsonParam = JSON.toJSONString(param);
log.info("getwxacodeunlimit:param:" + jsonParam);
// 请求微信接口,得到返回结果[二进制流]
HttpEntity<String> entity = new HttpEntity<>(jsonParam, this.getHttpHeadersUTF8JSON());
ResponseEntity<byte[]> responseEntity = restTemplate.postForEntity(WXURL.WX_MINI_QR_CODE_URL, entity, byte[].class, accessToken);
// return byte[]
return responseEntity.getBody();
} catch (Exception e) {
log.error("getwxacodeunlimit:postForEntity:" + e.getMessage(), e);
}
return null;
}
/**
* 创建支付后领取立减金活动接口
* 通过此接口创建立减金活动。
* 将已创建的代金券cardid、跳转小程序appid、发起支付的商户号等信息通过此接口创建立减金活动成功返回活动id即为创建成功。
* 接口地址https://mp.weixin.qq.com/wiki?t=resource/res_main&id=21515658940X5pIn
*
* @param begin_time 活动开始时间,精确到秒
* @param end_time 活动结束时间,精确到秒
* @param gift_num 单个礼包社交立减金数量3-15个
* @param max_partic_times_act 每个用户活动期间最大领取次数,最大为50默认为1
* @param max_partic_times_one_day 每个用户活动期间单日最大领取次数,最大为50默认为1
* @param card_id 卡券ID
* @param min_amt 最少支付金额,单位是元
* @param membership_appid 奖品指定的会员卡appid。如用户标签有选择商户会员则需要填写会员卡appid该appid需要跟所有发放商户号有绑定关系。
* @param new_tinyapp_user 可以指定为是否小程序新用户membership_appid为空、new_tinyapp_user为false时指定为所有用户
* @return json
* @author yclimb
* @date 2018/9/18
*/
public JSONObject createCardActivity(String begin_time, String end_time, int gift_num, int max_partic_times_act,
int max_partic_times_one_day, String card_id, String min_amt,
String membership_appid, boolean new_tinyapp_user) {
try {
// 创建活动接口之前的验证
String msg = checkCardActivity(begin_time, end_time, gift_num, max_partic_times_act, max_partic_times_one_day, min_amt);
if (null != msg) {
JSONObject resultJson = new JSONObject(2);
resultJson.put("errcode", "1");
resultJson.put("errmsg", msg);
return resultJson;
}
// 获取[爱上悦店]公众号的 access_token
String accessToken = this.getAccessToken(WXConstants.WX_MINI_PROGRAM_CODE);
// 调用接口传入参数
JSONObject paramJson = new JSONObject(1);
// info 包含 basic_info、card_info_list、custom_info
JSONObject info = new JSONObject(3);
// 基础信息对象
JSONObject basic_info = new JSONObject(8);
// activity_bg_color 是 活动封面的背景颜色,可参考:选取卡券背景颜色
basic_info.put("activity_bg_color", CardBgColorEnum.COLOR_090.getBgName());
// activity_tinyappid 是 用户点击链接后可静默添加到列表的小程序appid
basic_info.put("activity_tinyappid", WXPayConstants.APP_ID);
// mch_code 是 支付商户号
basic_info.put("mch_code", WXPayConstants.MCH_ID);
// begin_time 是 活动开始时间精确到秒unix时间戳
basic_info.put("begin_time", DateTimeUtil.getTenTimeByDate(begin_time));
// end_time 是 活动结束时间精确到秒unix时间戳
basic_info.put("end_time", DateTimeUtil.getTenTimeByDate(end_time));
// gift_num 是 单个礼包社交立减金数量3-15个
basic_info.put("gift_num", gift_num);
// max_partic_times_act 否 每个用户活动期间最大领取次数,最大为50不填默认为1
basic_info.put("max_partic_times_act", max_partic_times_act);
// max_partic_times_one_day 否 每个用户活动期间单日最大领取次数,最大为50不填默认为1
basic_info.put("max_partic_times_one_day", max_partic_times_one_day);
// card_info_list 是 可以配置两种发放规则:小程序新老用户、新老会员
JSONArray card_info_list = new JSONArray(1);
JSONObject card_info = new JSONObject(3);
// card_id 是 卡券ID
card_info.put("card_id", card_id);
// min_amt 是 最少支付金额,单位是分
card_info.put("min_amt", String.valueOf(new BigDecimal(min_amt).multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue()));
/*
* membership_appid 是 奖品指定的会员卡appid。如用户标签有选择商户会员则需要填写会员卡appid该appid需要跟所有发放商户号有绑定关系。
* new_tinyapp_user 是 可以指定为是否小程序新用户
* total_user 是 可以指定为所有用户
* membership_appid、new_tinyapp_user、total_user以上字段3选1未选择请勿填不必故意填写false
*/
if (StringUtils.isNotBlank(membership_appid)) {
card_info.put("membership_appid", membership_appid);
} else {
if (new_tinyapp_user) {
card_info.put("new_tinyapp_user", true);
} else {
card_info.put("total_user", true);
}
}
card_info_list.add(card_info);
// 自定义字段,表示支付后领券
JSONObject custom_info = new JSONObject(1);
custom_info.put("type", "AFTER_PAY_PACKAGE");
// 拼装json对象
info.put("basic_info", basic_info);
info.put("card_info_list", card_info_list);
info.put("custom_info", custom_info);
paramJson.put("info", info);
// 请求微信接口,得到返回结果[json]
HttpEntity<JSONObject> entity = new HttpEntity<>(paramJson, this.getHttpHeadersUTF8JSON());
JSONObject resultJson = restTemplate.postForObject(WeChatURL.WX_CARD_ACTIVITY_CREATE_URL, entity, JSONObject.class, accessToken);
// {"errcode":0,"errmsg":"ok","activity_id":"4728935"}
System.out.println(resultJson.toJSONString());
return resultJson;
} catch (Exception e) {
WXPayUtil.getLogger().error(e.getMessage(), e);
}
return null;
}
/**
* 创建活动接口之前的验证
*
* @param begin_time 活动开始时间,精确到秒
* @param end_time 活动结束时间,精确到秒
* @param gift_num 单个礼包社交立减金数量3-15个
* @param max_partic_times_act 每个用户活动期间最大领取次数,最大为50默认为1
* @param max_partic_times_one_day 每个用户活动期间单日最大领取次数,最大为50默认为1
* @param min_amt 最少支付金额,单位是元
* @return msg str
* @author yclimb
* @date 2018/9/18
*/
public String checkCardActivity(String begin_time, String end_time, int gift_num, int max_partic_times_act,
int max_partic_times_one_day, String min_amt) {
// 开始时间不能小于结束时间
if (DateTimeUtil.latterThan(end_time, begin_time, DateTimeUtil.TIME_FORMAT_NORMAL)) {
return "活动开始时间不能小于活动结束时间";
}
// 单个礼包社交立减金数量3-15个
if (gift_num < 3 || gift_num > 15) {
return "单个礼包社交立减金数量3-15个";
}
// 每个用户活动期间最大领取次数,最大为50默认为1
if (max_partic_times_act <= 0 || max_partic_times_act > 50) {
return "每个用户活动期间最大领取次数,最大为50默认为1";
}
// 每个用户活动期间单日最大领取次数,最大为50默认为1
if (max_partic_times_one_day <= 0 || max_partic_times_one_day > 50) {
return "每个用户活动期间单日最大领取次数,最大为50默认为1";
}
// 最少支付金额,单位是元
if (BigDecimal.ONE.compareTo(new BigDecimal(min_amt)) > 0) {
return "最少支付金额必须大于1元";
}
return null;
}
/**
* 获取卡券 api_ticket 的 api
* 请求路径https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token={0}&type=wx_card
*
* @param access_token token
* @return api_ticket json obj
* @author yclimb
* @date 2018/9/21
*/
public String getWxCardApiTicket(String access_token) {
if (StringUtils.isBlank(access_token)) {
return null;
}
try {
// redis key
String redisKey = RedisKeyUtil.keyBuilder(RedisKeyEnum.IMALL_WXCARD_APITICKET, access_token);
// 从redis中获取缓存
Object obj = redisTemplate.opsForValue().get(redisKey);
if (obj != null) {
return obj.toString();
}
// 获取卡券 api_ticket
String api_ticket = restTemplate.getForObject(WeChatURL.BASE_API_TICKET, String.class, access_token);
WXPayUtil.getLogger().info("getWxCardApiTicket:api_ticket:{}", api_ticket);
if (StringUtils.isBlank(api_ticket)) {
return null;
}
JSONObject jsonObject = JSON.parseObject(api_ticket);
if (0 != jsonObject.getIntValue("errcode")) {
return null;
}
// 设置到redis中下次取直接拿缓存即可防止多次生成
String ticket = jsonObject.getString("ticket");
redisTemplate.opsForValue().set(redisKey, ticket, jsonObject.getIntValue("expires_in"), TimeUnit.SECONDS);
return ticket;
} catch (Exception e) {
WXPayUtil.getLogger().error(e.getMessage(), e);
}
return null;
}
/**
* 获取卡券 api_ticket 的 api
* 请求路径https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token={0}&type=jsapi
*
* @param access_token token
* @return api_ticket json obj
* @author yclimb
* @date 2018/9/25
*/
public String getWxApiTicket(String access_token) {
if (StringUtils.isBlank(access_token)) {
return null;
}
try {
// redis key
String redisKey = RedisKeyUtil.keyBuilder(RedisKeyEnum.IMALL_WX_APITICKET, access_token);
// 从redis中获取缓存
Object obj = redisTemplate.opsForValue().get(redisKey);
if (obj != null) {
return obj.toString();
}
// 获取 api_ticket
String api_ticket = restTemplate.getForObject(WeChatURL.BASE_JSAPI_TICKET, String.class, access_token);
WXPayUtil.getLogger().info("getWxApiTicket:api_ticket:{}", api_ticket);
if (StringUtils.isBlank(api_ticket)) {
return null;
}
JSONObject jsonObject = JSON.parseObject(api_ticket);
if (0 != jsonObject.getIntValue("errcode")) {
return null;
}
// 设置到redis中下次取直接拿缓存即可防止多次生成
String ticket = jsonObject.getString("ticket");
redisTemplate.opsForValue().set(redisKey, ticket, jsonObject.getIntValue("expires_in"), TimeUnit.SECONDS);
return ticket;
} catch (Exception e) {
WXPayUtil.getLogger().error(e.getMessage(), e);
}
return null;
}
/**
* 根据代金券批次ID得到组合的cardList
*
* @param cardId 卡包ID
* @return cardList
* @author yclimb
* @date 2018/9/21
*/
public JSONArray getCardList(String cardId) {
if (StringUtils.isBlank(cardId)) {
return null;
}
try {
// 获取[爱上悦店]公众号的 access_token
String accessToken = this.getAccessToken(WXConstants.WX_MINI_PROGRAM_CODE);
String timestamp = String.valueOf(WXPayUtil.getCurrentTimestamp());
String nonce_str = WXPayUtil.generateNonceStr();
// 卡券的扩展参数。需进行 JSON 序列化为字符串传入
JSONObject cardExt = new JSONObject();
//cardExt.put("code", "");
//cardExt.put("openid", "");
//cardExt.put("fixed_begintimestamp", "");
//cardExt.put("outer_str", "");
cardExt.put("timestamp", timestamp);
cardExt.put("nonce_str", nonce_str);
/**
* 1.将 api_ticket、timestamp、card_id、code、openid、nonce_str的value值进行字符串的字典序排序。
* 2.将所有参数字符串拼接成一个字符串进行sha1加密得到signature。
* 3.signature中的timestampnonce字段和card_ext中的timestampnonce_str字段必须保持一致。
*/
Map<String, String> map = new HashMap<>(8);
//map.put("code", "");
//map.put("openid", "");
map.put("api_ticket", this.getWxCardApiTicket(accessToken));
map.put("timestamp", timestamp);
map.put("card_id", cardId);
map.put("nonce_str", nonce_str);
cardExt.put("signature", WXPayUtil.SHA1(WXPayUtil.dictionaryOrder(map, 2)));
// 卡券对象
JSONObject cardInfo = new JSONObject();
cardInfo.put("cardId", cardId);
cardInfo.put("cardExt", cardExt.toJSONString());
// 需要添加的卡券列表
JSONArray cardList = new JSONArray(1);
cardList.add(cardInfo);
return cardList;
} catch (Exception e) {
WXPayUtil.getLogger().error(e.getMessage(), e);
}
return null;
}
/**
* 获取微信签名信息
*
* @param requestUrl 请求页面地址
* @param appid appid
* @param code code
* @return 返回mapnoncestr:随机字符串timestamp签名时间戳appid;微信公众号Id;signature:签名串
* @author yclimb
* @date 2018/9/25
*/
public Map<String, Object> getSignature(String requestUrl, String appid, String code) {
Map<String, Object> map = new HashMap<>();
try {
// 获取公众号的 access_token、jsapi_ticket
String accessToken = this.getAccessToken(code);
String jsapi_ticket = this.getWxApiTicket(accessToken);
String nonce_str = WXPayUtil.generateNonceStr();
String timestamp = Long.toString(WXPayUtil.getCurrentTimestamp());
// 注意这里参数名必须全部小写,且必须有序
String dataStr = "jsapi_ticket=" + jsapi_ticket +
"&noncestr=" + nonce_str +
"&timestamp=" + timestamp +
"&url=" + requestUrl;
WXPayUtil.getLogger().info(dataStr);
String signature = WXPayUtil.SHA1(dataStr);
map.put("noncestr", nonce_str);
map.put("timestamp", timestamp);
map.put("appid", appid);
map.put("signature", signature);
} catch (Exception e) {
WXPayUtil.getLogger().error(e.getMessage(), e);
}
return map;
}
}