From 4c161e4aed6eca1219c25419bfe39f23cccf7e0f Mon Sep 17 00:00:00 2001 From: yclimb Date: Fri, 17 Aug 2018 16:24:56 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BD=91=E9=A1=B5=E3=80=81=E5=B0=8F=E7=A8=8B?= =?UTF-8?q?=E5=BA=8F=E6=94=AF=E4=BB=98=E9=93=BE=E6=8E=A5=E7=AD=89=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=EF=BC=9B=E6=96=B0=E5=A2=9E=E6=B5=8B=E8=AF=95=E9=89=B4?= =?UTF-8?q?=E6=9D=83=E3=80=81=E6=94=AF=E4=BB=98demo=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 26 ++ .../com/weixin/pay/constants/WXConstants.java | 47 +++ .../java/com/weixin/pay/constants/WXURL.java | 90 ++++++ .../com/weixin/pay/redis/RedisKeyEnum.java | 76 +++++ .../com/weixin/pay/redis/RedisKeyUtil.java | 122 +++++++ .../java/com/weixin/pay/util/WXUserUtil.java | 41 +++ .../java/com/weixin/pay/util/WXUtils.java | 279 ++++++++++++++++ .../test/controller/WXAuthController.java | 107 +++++++ src/main/test/controller/WXPayController.java | 302 ++++++++++++++++++ 9 files changed, 1090 insertions(+) create mode 100644 src/main/java/com/weixin/pay/constants/WXConstants.java create mode 100644 src/main/java/com/weixin/pay/constants/WXURL.java create mode 100644 src/main/java/com/weixin/pay/redis/RedisKeyEnum.java create mode 100644 src/main/java/com/weixin/pay/redis/RedisKeyUtil.java create mode 100644 src/main/java/com/weixin/pay/util/WXUserUtil.java create mode 100644 src/main/java/com/weixin/pay/util/WXUtils.java create mode 100644 src/main/test/controller/WXAuthController.java create mode 100755 src/main/test/controller/WXPayController.java diff --git a/pom.xml b/pom.xml index 721860c..0223efe 100644 --- a/pom.xml +++ b/pom.xml @@ -72,6 +72,32 @@ 3.1.0 + + org.projectlombok + lombok + 1.16.10 + + + + org.apache.commons + commons-lang3 + 3.1 + + + + + org.springframework + spring-web + 4.3.14.RELEASE + + + + + org.springframework.data + spring-data-redis + 2.0.3.RELEASE + + diff --git a/src/main/java/com/weixin/pay/constants/WXConstants.java b/src/main/java/com/weixin/pay/constants/WXConstants.java new file mode 100644 index 0000000..048e0aa --- /dev/null +++ b/src/main/java/com/weixin/pay/constants/WXConstants.java @@ -0,0 +1,47 @@ +package com.weixin.pay.constants; + +/** + * 微信基础常量类 + * + * @author yclimb + * @date 2018/8/17 + */ +public class WXConstants { + + /** + * 对于前端访问返回参数,本例使用string,推荐自主封装json对象 + */ + public static final String SUCCESS = "success"; + public static final String ERROR = "error"; + + /** + * 授权作用域 不弹出授权页面,直接跳转,只能获取用户openid + **/ + public static final String OAUTH_BASE_SCOPE = "snsapi_base"; + + /** + * 授权作用域 弹出授权页面 能获取昵称、头像等信息 + **/ + public static final String OAUTH_USERINFO_SCOPE = "snsapi_userinfo"; + + /** + * 网页授权 重定向后会带上state参数 + */ + public static final String OAUTH_STATE = "xxx"; + + /** + * 微信全局accessToken + */ + public static final String WECHAT_ACCESSTOKEN = OAUTH_STATE + ":wx:accessToken:"; + + /** + * 微信全局accessTokenLock + */ + public static final String WECHAT_ACCESSTOKEN_LOCK = OAUTH_STATE + ":wx:accessTokenLock:"; + + /** + * 微信网页授权openid,时限:7200秒 + */ + public static final String WECHAT_JSAPI_OPENID = OAUTH_STATE + ":wx:jsapi:openid:"; + +} diff --git a/src/main/java/com/weixin/pay/constants/WXURL.java b/src/main/java/com/weixin/pay/constants/WXURL.java new file mode 100644 index 0000000..9edcc02 --- /dev/null +++ b/src/main/java/com/weixin/pay/constants/WXURL.java @@ -0,0 +1,90 @@ +package com.weixin.pay.constants; + +/** + * 微信基础URL链接 + * + * @author yclimb + * @date 2018/8/17 + */ +public class WXURL { + + /** + * 请求URL之获取jsapi_ticket + */ + public static final String PAGE_URL_SIGN = "jsapi_ticket={0}&noncestr={1}×tamp={2}&url={3}"; + + /** + * 请求URL之获取access_token + */ + public static final String BASE_ACCESS_TOKEN = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}"; + + /** + * 请求URL之获取jsapi_ticket + */ + public static final String BASE_JSAPI_TICKET = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token={0}&type=jsapi"; + + /** + * 请求URL之创建菜单 + */ + public static final String MENU_CREATE = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token={0}"; + + /** + * 请求URL之查询菜单 + */ + public static final String MENU_QUERY = "https://api.weixin.qq.com/cgi-bin/menu/get?access_token={0}"; + + /** + * 请求URL之删除菜单 + */ + public static final String MENU_DELETE = "https://api.weixin.qq.com/cgi-bin/menu/delete?access_token={0}"; + + /** + * 页面授权获取code地址 + */ + public static final String OAUTH_CODE_URL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=" + WXConstants.OAUTH_STATE + "#wechat_redirect"; + + /** + * 通过code换取网页授权access_token + */ + public static final String OAUTH_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid={0}&secret={1}&code={2}&grant_type=authorization_code"; + + /** + * 页面授权获取指定微信号的基础信息 + */ + public static final String OAUTH_GET_USERINFO_URL = "https://api.weixin.qq.com/sns/userinfo?access_token={0}&openid={1}&lang=zh_CN"; + + /** + * 获取指定微信号的基础信息 通过全局access_token + */ + public static final String GET_USERINFO_URL = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN"; + + /** + * 微信模板消息发送 + */ + public static final String WX_TEMPLATE_SEND_URL = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token={0}"; + + /** + * 微信客户消息发送 + */ + public static final String WX_CUSTMOER_SERVICE_SEND_URL = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token={0}"; + + /*** + * 微信创建二维码ticket + */ + public static final String WX_TICKET_CREATE = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={0}"; + + /** + * 小程序登录校验 + */ + public static final String WX_MINI_LOGIN = "https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code"; + + /** + * 小程序模板信息 + */ + public static final String WX_MINI_TEMPLATE_MSG = "https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token={0}"; + + /** + * 获取小程序二维码,通过该接口生成的小程序码,永久有效,数量暂无限制 + */ + public static final String WX_MINI_QR_CODE_URL = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={0}"; +} diff --git a/src/main/java/com/weixin/pay/redis/RedisKeyEnum.java b/src/main/java/com/weixin/pay/redis/RedisKeyEnum.java new file mode 100644 index 0000000..bfe8a97 --- /dev/null +++ b/src/main/java/com/weixin/pay/redis/RedisKeyEnum.java @@ -0,0 +1,76 @@ +package com.weixin.pay.redis; + +/** + * Redis 枚举类 + * + * @author yclimb + * @date 2018/4/19 + */ +public enum RedisKeyEnum { + + /** + * 生成带参数的小程序二维码KEY + */ + XXX_MINI_WX_CODE(RedisKeyUtil.KEY_PREFIX, "mini", "getwxacodeunlimit", "生成永久无限制微信二维码") + + ; + + /** + * 系统标识 + */ + private String keyPrefix; + + /** + * 模块名称 + */ + private String module; + + /** + * 方法名称 + */ + private String func; + + /** + * remark + */ + private String remark; + + RedisKeyEnum(String keyPrefix, String module, String func, String remark) { + this.keyPrefix = keyPrefix; + this.module = module; + this.func = func; + this.remark = remark; + } + + public String getKeyPrefix() { + return keyPrefix; + } + + public void setKeyPrefix(String keyPrefix) { + this.keyPrefix = keyPrefix; + } + + public String getModule() { + return module; + } + + public void setModule(String module) { + this.module = module; + } + + public String getFunc() { + return func; + } + + public void setFunc(String func) { + this.func = func; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } +} diff --git a/src/main/java/com/weixin/pay/redis/RedisKeyUtil.java b/src/main/java/com/weixin/pay/redis/RedisKeyUtil.java new file mode 100644 index 0000000..716c706 --- /dev/null +++ b/src/main/java/com/weixin/pay/redis/RedisKeyUtil.java @@ -0,0 +1,122 @@ +package com.weixin.pay.redis; + +/** + * Redis 工具类 + * + * @author yclimb + * @date 2018/4/19 + */ +public class RedisKeyUtil { + + /** + * 主数据系统标识 + */ + public static final String KEY_PREFIX = "xxx"; + + /** + * 分割字符,默认[:] + */ + public static final String KEY_SPLIT_CHAR = ":"; + + /** + * redis的key键规则定义 + * + * @param module 模块名称 + * @param func 方法名称 + * @return key + */ + public static String keyBuilder(String module, String func) { + return keyBuilder(null, module, func, (String[]) null); + } + + /** + * redis的key键规则定义 + * + * @param module 模块名称 + * @param func 方法名称 + * @param args 参数.. + * @return key + */ + public static String keyBuilder(String module, String func, String... args) { + return keyBuilder(null, module, func, args); + } + + /** + * redis的key键规则定义 + * + * @param module 模块名称 + * @param func 方法名称 + * @param objStr 对象.toString() + * @return key + */ + public static String keyBuilder(String module, String func, String objStr) { + return keyBuilder(null, module, func, new String[]{objStr}); + } + + /** + * redis的key键规则定义 + * + * @param prefix 项目前缀 + * @param module 模块名称 + * @param func 方法名称 + * @param objStr 对象.toString() + * @return key + */ + public static String keyBuilder(String prefix, String module, String func, String objStr) { + return keyBuilder(prefix, module, func, new String[]{objStr}); + } + + /** + * redis的key键规则定义 + * + * @param prefix 项目前缀 + * @param module 模块名称 + * @param func 方法名称 + * @param args 参数.. + * @return key + */ + public static String keyBuilder(String prefix, String module, String func, String... args) { + // 项目前缀 + if (prefix == null) { + prefix = KEY_PREFIX; + } + + StringBuilder key = new StringBuilder(prefix); + // KEY_SPLIT_CHAR 为分割字符 + key.append(KEY_SPLIT_CHAR).append(module).append(KEY_SPLIT_CHAR).append(func); + + // args 为空时不需要循环 + if (args == null || args.length <= 0) { + return key.toString(); + } + + // args 不为空时循环拼接字符 + for (String arg : args) { + key.append(KEY_SPLIT_CHAR).append(arg); + } + + return key.toString(); + } + + /** + * redis的key键规则定义 + * + * @param redisEnum 枚举对象 + * @return key + */ + public static String keyBuilder(RedisKeyEnum redisEnum) { + return keyBuilder(redisEnum.getKeyPrefix(), redisEnum.getModule(), redisEnum.getFunc(), (String[]) null); + } + + /** + * redis的key键规则定义 + * + * @param redisEnum 枚举对象 + * @param objStr 对象.toString() + * @return key + */ + public static String keyBuilder(RedisKeyEnum redisEnum, String objStr) { + return keyBuilder(redisEnum.getKeyPrefix(), redisEnum.getModule(), redisEnum.getFunc(), objStr); + } + +} diff --git a/src/main/java/com/weixin/pay/util/WXUserUtil.java b/src/main/java/com/weixin/pay/util/WXUserUtil.java new file mode 100644 index 0000000..c437bc0 --- /dev/null +++ b/src/main/java/com/weixin/pay/util/WXUserUtil.java @@ -0,0 +1,41 @@ +package com.weixin.pay.util; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.codec.binary.Base64; + +import java.io.UnsupportedEncodingException; + +/** + * 对于微信用户名称emoji等特殊字符处理 + * + * @author yclimb + * @date 2018/8/17 + */ +@Slf4j +public class WXUserUtil { + + /** + * 编码用户昵称 + * + * @param nickName 未编码等名称 + * @return base64 str + */ + public static String encodeNickName(String nickName) { + try { + return Base64.encodeBase64String(nickName.toString().getBytes("utf-8")); + } catch (UnsupportedEncodingException e) { + log.error("编码用户昵称报错", e); + } + return null; + } + + /** + * 解码用户昵称 + * + * @param nickName base64 str + * @return 原始名称 + */ + public static String decodeNickName(String nickName) { + return new String(Base64.decodeBase64(nickName)); + } +} diff --git a/src/main/java/com/weixin/pay/util/WXUtils.java b/src/main/java/com/weixin/pay/util/WXUtils.java new file mode 100644 index 0000000..b72fd43 --- /dev/null +++ b/src/main/java/com/weixin/pay/util/WXUtils.java @@ -0,0 +1,279 @@ +package com.weixin.pay.util; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.weixin.pay.constants.WXConstants; +import com.weixin.pay.constants.WXPayConstants; +import com.weixin.pay.constants.WXURL; +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.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 + * 测试环境只能通过生产接口调用获取accessToken 不然会存在accessToken相互覆盖 导致互相不可用 + * + * @param code + * @return + * @throws Exception + */ + public String getAccessToken(String code) throws Exception { + + // 取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; + } + +} diff --git a/src/main/test/controller/WXAuthController.java b/src/main/test/controller/WXAuthController.java new file mode 100644 index 0000000..08df23b --- /dev/null +++ b/src/main/test/controller/WXAuthController.java @@ -0,0 +1,107 @@ +package controller; + +import com.alibaba.fastjson.JSONObject; +import com.weixin.pay.constants.WXConstants; +import com.weixin.pay.util.WXUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; + +/** + * 微信用户授权控制类 + * + * @author yclimb + * @date 2018/7/30 + */ +@Slf4j +@RestController +@RequestMapping("/weixin/auth") +public class WXAuthController { + + @Resource + private WXUtils wxUtils; + + /** + * 微信网页授权 + * https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842 + * 第一步:用户同意授权,获取code + * 第二步:通过code换取网页授权access_token + * @return str + * + * @author yclimb + * @date 2018/7/30 + */ + /*@ApiOperation(value = "微信支付|网页授权", httpMethod = "GET", notes = "获取前端微信用户的网页授权,得到用户基础信息")*/ + @GetMapping("/authorize") + public String authorize(HttpServletRequest request) throws Exception { + + // 跳转页面标识 + String state = request.getParameter("state"); + // 通过code获取access_token + String code = request.getParameter("code"); + log.info("authorize:code:{}", code); + + // 获取access_token和openid + JSONObject jsonToken = wxUtils.getJsapiAccessTokenByCode(code); + if (null == jsonToken) { + return WXConstants.ERROR; + } + + return WXConstants.SUCCESS; + } + + /** + * 通过access_token和openid请求获取用户信息(需scope为 snsapi_userinfo) + * @return str + * + * @author yclimb + * @date 2018/7/31 + */ + /*@ApiOperation(value = "微信支付|通过access_token和openid请求获取用户信息", httpMethod = "POST", notes = "通过access_token和openid请求获取用户信息")*/ + @PostMapping("/userinfo/{access_token}/{openid}") + public String userinfo(@PathVariable String access_token, @PathVariable String openid) { + + // 通过access_token和openid请求获取用户信息 + JSONObject jsonUserinfo = wxUtils.getJsapiUserinfo(access_token, openid); + if (null == jsonUserinfo) { + return WXConstants.ERROR; + } + + // 判断用户是否在系统中是一个用户 + String unionid = jsonUserinfo.getString("unionid"); + if (StringUtils.isBlank(unionid)) { + return WXConstants.ERROR; + } + + /* + // 存储用户信息到数据库 + User user = userService.queryByUnionId(unionid); + if (user == null) { + user = JSON.parseObject(jsonUserinfo.toJSONString(), User.class); + user.setUserId(user.getId()); + user.setCreateDate(new Date()); + user.setIsDel(CommonConstantEnum.UNDELETED.getCode()); + // 处理微信昵称emoji表情 + if (StringUtils.isNotBlank(user.getNickName())) { + // 编码Base64.decodeBase64() + user.setNickName(UserNickUtil.encodeNickName(user.getNickName())); + } + userService.createEntity(user); + } + + // 用户账户信息 + Map map = new HashMap<>(2); + // 用户名称解码 + user.setNickName(UserNickUtil.decodeNickName(user.getNickName())); + UserAccount userAccount = userAccountService.queryByUserId(user.getId()); + map.put("user", user); + + return AppMessage.success(map);*/ + + return WXConstants.SUCCESS; + } + +} diff --git a/src/main/test/controller/WXPayController.java b/src/main/test/controller/WXPayController.java new file mode 100755 index 0000000..e75c4b7 --- /dev/null +++ b/src/main/test/controller/WXPayController.java @@ -0,0 +1,302 @@ +package controller; + +import com.weixin.pay.constants.WXConstants; +import com.weixin.pay.constants.WXPayConstants; +import com.weixin.pay.util.AESUtil; +import com.weixin.pay.util.WXPayUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +/** + * 微信支付Controller + * + * @author yclimb + * @date 2018/6/15 + */ +@Slf4j +@RestController +@RequestMapping("/weixin/pay") +public class WXPayController { + + /** + * 返回成功xml + */ + private String resSuccessXml = ""; + + /** + * 返回失败xml + */ + private String resFailXml = ""; + + /** + * 该链接是通过【统一下单API】中提交的参数notify_url设置,如果链接无法访问,商户将无法接收到微信通知。 + * 通知url必须为直接可访问的url,不能携带参数。示例:notify_url:“https://pay.weixin.qq.com/wxpay/pay.action” + *

+ * 支付完成后,微信会把相关支付结果和用户信息发送给商户,商户需要接收处理,并返回应答。 + * 对后台通知交互时,如果微信收到商户的应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。 + * (通知频率为15/15/30/180/1800/1800/1800/1800/3600,单位:秒) + * 注意:同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 + * 推荐的做法是,当收到通知进行处理时,首先检查对应业务数据的状态,判断该通知是否已经处理过,如果没有处理过再进行处理,如果处理过直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。 + * 特别提醒:商户系统对于支付结果通知的内容一定要做签名验证,防止数据泄漏导致出现“假通知”,造成资金损失。 + * + * @author yclimb + * @date 2018/6/15 + */ + /*@ApiOperation(value = "微信支付|支付回调接口", httpMethod = "POST", notes = "该链接是通过【统一下单API】中提交的参数notify_url设置,如果链接无法访问,商户将无法接收到微信通知。")*/ + @RequestMapping("/wxnotify") + public void wxnotify(HttpServletRequest request, HttpServletResponse response) { + + String resXml = ""; + InputStream inStream; + try { + + inStream = request.getInputStream(); + ByteArrayOutputStream outSteam = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int len = 0; + while ((len = inStream.read(buffer)) != -1) { + outSteam.write(buffer, 0, len); + } + + WXPayUtil.getLogger().info("wxnotify:微信支付----start----"); + + // 获取微信调用我们notify_url的返回信息 + String result = new String(outSteam.toByteArray(), "utf-8"); + WXPayUtil.getLogger().info("wxnotify:微信支付----result----=" + result); + + // 关闭流 + outSteam.close(); + inStream.close(); + + // xml转换为map + Map map = WXPayUtil.xmlToMap(result); + boolean isSuccess = false; + if (WXPayConstants.SUCCESS.equalsIgnoreCase(map.get(WXPayConstants.RESULT_CODE))) { + + WXPayUtil.getLogger().info("wxnotify:微信支付----返回成功"); + + if (WXPayUtil.isSignatureValid(map, WXPayConstants.API_KEY)) { + + // 订单处理 操作 orderconroller 的回写操作? + WXPayUtil.getLogger().info("wxnotify:微信支付----验证签名成功"); + + // 通知微信.异步确认成功.必写.不然会一直通知后台.八次之后就认为交易失败了. + resXml = resSuccessXml; + isSuccess = true; + + } else { + WXPayUtil.getLogger().error("wxnotify:微信支付----判断签名错误"); + } + + } else { + WXPayUtil.getLogger().error("wxnotify:支付失败,错误信息:" + map.get(WXPayConstants.ERR_CODE_DES)); + resXml = resFailXml; + } + + /*// 根据付款单号查询付款记录 + Payment payment = paymentService.queryPaymentByFlowNumer(map.get("out_trade_no"), PaymentConstantEnum.PAYMENT_TYPE_ORDER.getCode()); + + // 付款记录修改 & 记录付款日志 + int resultPay = paymentService.modifyPaymentByWxnotify(payment, isSuccess); + if (resultPay > 0) { + // 处理业务 - 修改订单状态 + WXPayUtil.getLogger().info("wxnotify:微信支付回调:修改的订单===>" + map.get("out_trade_no")); + int updateResult = tradeService.modifyWxnotifyByRelationId(payment.getRelationId(), payment.getPrepayId(), isSuccess); + if (updateResult > 0) { + WXPayUtil.getLogger().info("wxnotify:微信支付回调:修改订单支付状态成功"); + } else { + WXPayUtil.getLogger().error("wxnotify:微信支付回调:修改订单支付状态失败"); + } + }*/ + + } catch (Exception e) { + WXPayUtil.getLogger().error("wxnotify:支付回调发布异常:", e); + } finally { + try { + // 处理业务完毕 + BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream()); + out.write(resXml.getBytes()); + out.flush(); + out.close(); + } catch (IOException e) { + WXPayUtil.getLogger().error("wxnotify:支付回调发布异常:out:", e); + } + } + + } + + /** + * 退款结果通知 + *

+ * 在申请退款接口中上传参数“notify_url”以开通该功能 + * 如果链接无法访问,商户将无法接收到微信通知。 + * 通知url必须为直接可访问的url,不能携带参数。示例:notify_url:“https://pay.weixin.qq.com/wxpay/pay.action” + *

+ * 当商户申请的退款有结果后,微信会把相关结果发送给商户,商户需要接收处理,并返回应答。 + * 对后台通知交互时,如果微信收到商户的应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。 + * (通知频率为15/15/30/180/1800/1800/1800/1800/3600,单位:秒) + * 注意:同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 + * 推荐的做法是,当收到通知进行处理时,首先检查对应业务数据的状态,判断该通知是否已经处理过,如果没有处理过再进行处理,如果处理过直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。 + * 特别说明:退款结果对重要的数据进行了加密,商户需要用商户秘钥进行解密后才能获得结果通知的内容 + * @param request req + * @param response resp + * @return res xml + * + * @author yclimb + * @date 2018/6/21 + */ + /*@ApiOperation(value = "微信支付|微信退款回调接口", httpMethod = "POST", notes = "该链接是通过【微信退款API】中提交的参数notify_url设置,如果参数中传了notify_url,则商户平台上配置的回调地址将不会生效。")*/ + @RequestMapping("/refund") + public void refund(HttpServletRequest request, HttpServletResponse response) { + + String resXml = ""; + InputStream inStream; + try { + + inStream = request.getInputStream(); + ByteArrayOutputStream outSteam = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int len = 0; + while ((len = inStream.read(buffer)) != -1) { + outSteam.write(buffer, 0, len); + } + WXPayUtil.getLogger().info("refund:微信退款----start----"); + + // 获取微信调用我们notify_url的返回信息 + String result = new String(outSteam.toByteArray(), "utf-8"); + WXPayUtil.getLogger().info("refund:微信退款----result----=" + result); + + // 关闭流 + outSteam.close(); + inStream.close(); + + // xml转换为map + Map map = WXPayUtil.xmlToMap(result); + boolean isSuccess = false; + if (WXPayConstants.SUCCESS.equalsIgnoreCase(map.get(WXPayConstants.RETURN_CODE))) { + + WXPayUtil.getLogger().info("refund:微信退款----返回成功"); + + /*if (WXPayUtil.isSignatureValid(map, WXPayConstants.API_KEY)) {*/ + + /** 以下字段在return_code为SUCCESS的时候有返回: **/ + // 加密信息:加密信息请用商户秘钥进行解密,详见解密方式 + String req_info = map.get("req_info"); + + /** + * 解密方式 + * 解密步骤如下: + * (1)对加密串A做base64解码,得到加密串B + * (2)对商户key做md5,得到32位小写key* ( key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置 ) + * (3)用key*对加密串B做AES-256-ECB解密(PKCS7Padding) + */ + String resultStr = AESUtil.decryptData(req_info); + + // WXPayUtil.getLogger().info("refund:解密后的字符串:" + resultStr); + Map aesMap = WXPayUtil.xmlToMap(resultStr); + + + /** 以下为返回的加密字段: **/ + // 商户退款单号 是 String(64) 1.21775E+27 商户退款单号 + String out_refund_no = aesMap.get("out_refund_no"); + // 退款状态 是 String(16) SUCCESS SUCCESS-退款成功、CHANGE-退款异常、REFUNDCLOSE—退款关闭 + String refund_status = aesMap.get("refund_status"); + /*// 微信订单号 是 String(32) 1.21775E+27 微信订单号 + String transaction_id = null; + // 商户订单号 是 String(32) 1.21775E+27 商户系统内部的订单号 + String out_trade_no = null; + // 微信退款单号 是 String(32) 1.21775E+27 微信退款单号 + String refund_id = null; + // 订单金额 是 Int 100 订单总金额,单位为分,只能为整数,详见支付金额 + String total_fee = null; + // 应结订单金额 否 Int 100 当该订单有使用非充值券时,返回此字段。应结订单金额=订单金额-非充值代金券金额,应结订单金额<=订单金额。 + String settlement_total_fee = null; + // 申请退款金额 是 Int 100 退款总金额,单位为分 + String refund_fee = null; + // 退款金额 是 Int 100 退款金额=申请退款金额-非充值代金券退款金额,退款金额<=申请退款金额 + String settlement_refund_fee = null;*/ + + // 退款是否成功 + if (!WXPayConstants.SUCCESS.equals(refund_status)) { + resXml = resFailXml; + } else { + // 通知微信.异步确认成功.必写.不然会一直通知后台.八次之后就认为交易失败了. + resXml = resSuccessXml; + isSuccess = true; + } + + /*// 根据付款单号查询付款记录 + Payment payment = paymentService.queryPaymentByFlowNumer(out_refund_no, PaymentConstantEnum.PAYMENT_TYPE_REFUND.getCode()); + + // 付款记录修改 & 记录付款日志 + int resultPay = paymentService.modifyPaymentByWxnotify(payment, isSuccess); + if (resultPay > 0) { + + // 退款订单记录 + List paymentOrderRefundList = paymentOrderRefundService.queryListByPaymentId(payment.getId()); + + // 处理业务 - 修改订单状态 + WXPayUtil.getLogger().info("refund:微信支付回调:修改的订单===>" + out_refund_no); + int updateResult = tradeService.modifyWxrefundByRelationId(payment.getRelationId(), paymentOrderRefundList, isSuccess); + if (updateResult > 0) { + WXPayUtil.getLogger().info("refund:微信支付回调:修改订单支付状态成功"); + } else { + WXPayUtil.getLogger().error("refund:微信支付回调:修改订单支付状态失败"); + } + }*/ + + /*} else { + WXPayUtil.getLogger().error("refund:微信支付----判断签名错误"); + }*/ + + } else { + WXPayUtil.getLogger().error("refund:支付失败,错误信息:" + map.get(WXPayConstants.RETURN_MSG)); + resXml = resFailXml; + } + + } catch (Exception e) { + WXPayUtil.getLogger().error("refund:微信退款回调发布异常:", e); + } finally { + try { + // 处理业务完毕 + BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream()); + out.write(resXml.getBytes()); + out.flush(); + out.close(); + } catch (IOException e) { + WXPayUtil.getLogger().error("refund:微信退款回调发布异常:out:", e); + } + } + } + + /** + * 企业付款到零钱 + * @return msg + * + * @author yclimb + * @date 2018/7/30 + */ + /*@Token(remove = true) + @ApiOperation(value = "微信支付|企业付款到零钱", httpMethod = "POST", notes = "用于企业向微信用户个人付款")*/ + @PostMapping("/transfers") + public String transfers(HttpServletRequest request) { + try { + String remoteAddr = request.getRemoteAddr(); + return WXConstants.SUCCESS; + } catch (Exception e) { + WXPayUtil.getLogger().error("transfers:微信提现支付异常:", e); + } + return WXConstants.ERROR; + } + +}