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;
}
/**
* 作用:生成永久无限制微信二维码
* 场景:微信二维码生成,根据参数和页面配置微信二维码,返回二维码字节流
* 接口链接: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 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 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 entity = new HttpEntity<>(jsonParam, this.getHttpHeadersUTF8JSON());
ResponseEntity 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 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中的timestamp,nonce字段和card_ext中的timestamp,nonce_str字段必须保持一致。
*/
Map 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 返回map:noncestr:随机字符串;timestamp:签名时间戳;appid;微信公众号Id;signature:签名串
* @author yclimb
* @date 2018/9/25
*/
public Map getSignature(String requestUrl, String appid, String code) {
Map 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 +
"×tamp=" + 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;
}
}