微信支付开发前,需要先获取商家信息,包括商户号、AppId、证书和密钥。
- 获取商户号
微信商户平台 申请成为商户 => 提交资料 => 签署协议 => 获取商户号
- 获取AppID
微信公众平台 注册服务号 => 服务号认证 => 获取APPID => 绑定商户号
- 申请商户证书
登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 申请API证书 包括商户证书和商户私钥
- 获取微信的证书
- 获取APIv3秘钥(在微信支付回调通知和商户获取平台证书使用APIv3密钥)
登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 设置APIv3密钥
1.引入pom.xml
com.github.wechatpay-apiv3 wechatpay-apache-httpclient 0.4.2
2.配置商户信息、证书、密钥等。将客户端对象构建到Bean中,方便后续使用。
可以采用两种方式
①.配置application.yml
weixin: appid: wx*************acx # appid mch-serial-no: 3FB18E2*******0127B3*****0053E2 # 证书序列号 private-key-path: D:\wx\pem\apiclient_key.pem # 证书路径 mch-id: 16*****801 # 商户号 key: F8CDeHBc***********2t5nvVeh1 # api秘钥 domain: https://api.mch.weixin.qq.com # 微信服务器地址 notify-domain: https://xw666.mynatapp.cc # 回调,自己的回调地址
然后创建对应的实体
@Configuration @PropertySource("classpath:application.yml") //读取配置文件 @ConfigurationProperties(prefix = "weixin") //读取wxpay节点 @Data //使用set方法将wxpay节点中的值填充到当前类的属性中 @ApiModel("微信支付静态常量类") public class WxPayConfig { @ApiModelProperty("商户号") private String mchId; @ApiModelProperty("商户API证书序列号") private String mchSerialNo; @ApiModelProperty("商户私钥文件") private String privateKeyPath; @ApiModelProperty("APIv3密钥") private String key; @ApiModelProperty("APPID") private String appid; @ApiModelProperty("微信服务器地址") private String domain; @ApiModelProperty("接收结果通知地址") private String notifyDomain; }
②使用数据库存储,在项目启动时加载到redis中,然后从redis中获取
@Configuration @Data @ApiModel("微信支付静态常量类") public class WxPayConstants { @Resource private RedisCache redisCache; @ApiModelProperty("APPID") public String appid; @ApiModelProperty("商户API证书序列号") public String mchSerialNo; @ApiModelProperty("商户私钥文件") public String privateKeyPath; @ApiModelProperty("商户号") public String mchId; @ApiModelProperty("APIv3密钥") public String key; @ApiModelProperty("微信服务器地址") public String domain; @ApiModelProperty("接收结果通知地址") public String notifyDomain; @Resource public void getParam(RedisCache redisCache){ appid = redisCache.getCacheObject("WX_PAY_SAVE_WX_APPID"); mchSerialNo = redisCache.getCacheObject("WX_PAY_SAVE_MCH_SERIAL_NO"); privateKeyPath = redisCache.getCacheObject("WX_PAY_SAVE_PRIVATE_KEY_PATH"); mchId = redisCache.getCacheObject("WX_PAY_SAVE_MCH_ID"); key = redisCache.getCacheObject("WX_PAY_SAVE_KEY"); domain = redisCache.getCacheObject("WX_PAY_SAVE_DOMAIN"); notifyDomain = redisCache.getCacheObject("WX_PAY_SAVE_NOTIFY_DOMAIN"); } }
这两个实体有几个共同的方法,无论使用哪一个,放到下面即可
/** * 获取商户的私钥文件 * * @param filename 证书地址 * @return 私钥文件 */ public PrivateKey getPrivateKey(String filename) { try { return PemUtil.loadPrivateKey(new FileInputStream(filename)); } catch (FileNotFoundException e) { throw new ServiceException("私钥文件不存在"); } } /** * 获取签名验证器 */ @Bean public Verifier getVerifier() { // 获取商户私钥 final PrivateKey privateKey = getPrivateKey(privateKeyPath); // 私钥签名对象 PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey); // 身份认证对象 WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner); // 获取证书管理器实例 CertificatesManager certificatesManager = CertificatesManager.getInstance(); try { // 向证书管理器增加需要自动更新平台证书的商户信息 certificatesManager.putMerchant(mchId, wechatPay2Credentials, key.getBytes(StandardCharsets.UTF_8)); } catch (IOException | GeneralSecurityException | HttpCodeException e) { e.printStackTrace(); } try { return certificatesManager.getVerifier(mchId); } catch (NotFoundException e) { e.printStackTrace(); throw new ServiceException("获取签名验证器失败"); } } /** * 获取微信支付的远程请求对象 * @return Http请求对象 */ @Bean public CloseableHttpClient getWxPayClient() { // 获取签名验证器 Verifier verifier = getVerifier(); // 获取商户私钥 final PrivateKey privateKey = getPrivateKey(privateKeyPath); WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create().withMerchant(mchId, mchSerialNo, privateKey) .withValidator(new WechatPay2Validator(verifier)); return builder.build(); }
3.请求地址枚举类(WxApiConstants)
为了防止微信支付的请求地址前缀发生变化,因此请求前缀存储在application.yml中,请求时进行拼接即可。
import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Getter; //为了防止微信支付的请求地址前缀发生变化,因此请求前缀存储在mysql,redis中,请求时进行拼接即可。 @AllArgsConstructor @Getter @ApiModel("请求地址") public enum WxApiConstants { @ApiModelProperty("Native下单") NATIVE_PAY("/v3/pay/transactions/native"), @ApiModelProperty("jsapi下单") JSAPI_PAY("/v3/pay/transactions/jsapi"), @ApiModelProperty("jsapi下单") H5_PAY("/v3/pay/transactions/h5"), @ApiModelProperty("APP下单") APP_PAY("/v3/pay/transactions/app"), @ApiModelProperty("查询订单") ORDER_QUERY_BY_NO("/v3/pay/transactions/out-trade-no/%s"), @ApiModelProperty("关闭订单") CLOSE_ORDER_BY_NO("/v3/pay/transactions/out-trade-no/%s/close"), @ApiModelProperty("申请退款") DOMESTIC_REFUNDS("/v3/refund/domestic/refunds"), @ApiModelProperty("查询单笔退款") DOMESTIC_REFUNDS_QUERY("/v3/refund/domestic/refunds/%s"), @ApiModelProperty("申请交易账单") TRADE_BILLS("/v3/bill/tradebill"), @ApiModelProperty("申请资金账单") FUND_FLOW_BILLS("/v3/bill/fundflowbill"); @ApiModelProperty("类型") private final String type; }
4.回调地址枚举类(WxChatBasePayDto)
发生请求后微信官方会回调我们传递的地址,这里通过枚举统一管理我们的回调地址,回调地址由application.yml中的
weixin.notify-domain拼接组成。
import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Getter; @Getter @AllArgsConstructor @ApiModel("微信回调地址,根据自己的项目来,不要直接照搬") public enum WxNotifyConstants { @ApiModelProperty("订单支付通知") RUN_ERRANDS_NOTIFY("/wx/order/wxOrderCallBack"), @ApiModelProperty("卡支付成功通知") CAMPUS_CARD_NOTIFY("/wx/campusCardOrder/wxCampusCardOrderCallBack"), @ApiModelProperty("卡退款成功通知") CAMPUS_CARD_REFUND_NOTIFY("/wx/campusCardOrder/refundWechatCallback"); @ApiModelProperty("类型") private final String type; }
5.微信支付基础请求数据对象(WxChatBasePayDto)
import com.xxx.project.wx.constants.WxNotifyConstants; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.math.BigDecimal; @Data @ApiModel("微信支付基础请求数据对象") public class WxChatBasePayDto { @ApiModelProperty("商品描述") private String title; @ApiModelProperty("商家订单号,对应 out_trade_no") private String orderId; @ApiModelProperty("订单金额") private BigDecimal price; @ApiModelProperty("回调地址") private WxNotifyConstants notify; @ApiModelProperty("支付用户的openid") private String openId; }
6.将请求参数封装成Map集合(WxPayCommon)
封装完枚举类后,首先就是请求参数的封装,支付类请求参数都非常相近,我们将都需要的参数提取出来以map的方式进行返回。这里的参数,指每个支付类请求都用到的参数,个别支付需要额外添加数据
import com.google.gson.Gson; import com.xxx.common.exception.ServiceException; import com.xxx.project.wx.constants.WxPayConstants; import com.xxx.project.wx.constants.WxApiConstants; import com.xxx.project.wx.dto.WxChatBasePayDto; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.util.EntityUtils; import java.io.IOException; import java.math.BigDecimal; import java.util.HashMap; import java.util.Map; @Slf4j public class WxPayCommon { /** * 封装基础通用请求数据 * @param wxPayConfig 微信的配置文件 * @param basePayData 微信支付基础请求数据 * @return 封装后的map对象 */ public static MapgetBasePayParams(WxPayConstants wxPayConfig, WxChatBasePayDto basePayData) { Map paramsMap = new HashMap<>(); paramsMap.put("appid", wxPayConfig.getAppid()); paramsMap.put("mchid", wxPayConfig.getMchId()); // 如果商品名称过长则截取 String title = basePayData.getTitle().length() > 62 ? basePayData.getTitle().substring(0, 62) : basePayData.getTitle(); paramsMap.put("description",title); paramsMap.put("out_trade_no", basePayData.getOrderId()); paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(basePayData.getNotify().getType())); Map amountMap = new HashMap<>(); amountMap.put("total", basePayData.getPrice().multiply(new BigDecimal("100")).intValue()); paramsMap.put("amount", amountMap); return paramsMap; } /** * 获取请求对象(Post请求) * @param wxPayConfig 微信配置类 * @param apiType 接口请求地址 * @param paramsMap 请求参数 * @return Post请求对象 */ public static HttpPost getHttpPost(WxPayConstants wxPayConfig, WxApiConstants apiType, Map paramsMap) { // 1.设置请求地址 HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(apiType.getType())); // 2.设置请求数据 Gson gson = new Gson(); String jsonParams = gson.toJson(paramsMap); // 3.设置请求信息 StringEntity entity = new StringEntity(jsonParams, "utf-8"); entity.setContentType("application/json"); httpPost.setEntity(entity); httpPost.setHeader("Accept", "application/json"); return httpPost; } /** * 解析响应数据 * @param response 发送请求成功后,返回的数据 * @return 微信返回的参数 */ public static HashMap resolverResponse(CloseableHttpResponse response) { try { // 1.获取请求码 int statusCode = response.getStatusLine().getStatusCode(); // 2.获取返回值 String 格式 final String bodyAsString = EntityUtils.toString(response.getEntity()); Gson gson = new Gson(); if (statusCode == 200) { // 3.如果请求成功则解析成Map对象返回 HashMap resultMap = gson.fromJson(bodyAsString, HashMap.class); return resultMap; } else { if (StringUtils.isNoneBlank(bodyAsString)) { log.error("微信支付请求失败,提示信息:{}", bodyAsString); // 4.请求码显示失败,则尝试获取提示信息 HashMap resultMap = gson.fromJson(bodyAsString, HashMap.class); throw new ServiceException(resultMap.get("message")); } log.error("微信支付请求失败,未查询到原因,提示信息:{}", response); // 其他异常,微信也没有返回数据,这就需要具体排查了 throw new IOException("request failed"); } } catch (Exception e) { e.printStackTrace(); throw new ServiceException(e.getMessage()); } finally { try { response.close(); } catch (IOException e) { e.printStackTrace(); } } } }
7.创建微信支付订单的三种方式(Native,Jsapi,App)
商户Native支付下单接口,微信后台系统返回链接参数code_url,商户后台系统将code_url值生成二维码图片,用户使用微信客户端扫码后发起支付,也就是说后端只需要返回code_url即可。
官方JSAPI支付开发指引
继续在上方WxPayCommon中加入
/** * 创建微信支付订单-Native方式 * * @param wxPayConfig 微信配置信息 * @param basePayData 基础请求信息,商品标题、商家订单id、订单价格 * @param wxPayClient 微信请求客户端() * @return 微信支付二维码地址 */ public static String wxNativePay(WxPayConstants wxPayConfig, WxChatBasePayDto basePayData, CloseableHttpClient wxPayClient) { // 1.获取请求参数的Map格式 MapparamsMap = getBasePayParams(wxPayConfig, basePayData); // 2.获取请求对象 HttpPost httpPost = getHttpPost(wxPayConfig, WxApiConstants.NATIVE_PAY, paramsMap); // 3.完成签名并执行请求 CloseableHttpResponse response = null; try { response = wxPayClient.execute(httpPost); } catch (IOException e) { e.printStackTrace(); throw new ServiceException("微信支付请求失败"); } // 4.解析response对象 HashMap resultMap = resolverResponse(response); if (resultMap != null) { // native请求返回的是二维码链接,前端将链接转换成二维码即可 return resultMap.get("code_url"); } return null; } /** * 创建微信支付订单-jsapi方式 * @param wxPayConfig 微信配置信息 * @param basePayData 基础请求信息,商品标题、商家订单id、订单价格 * @param openId 通过微信小程序或者公众号获取到用户的openId * @param wxPayClient 微信请求客户端() * @return 微信支付二维码地址 */ public static String wxJsApiPay(WxPayConstants wxPayConfig, WxChatBasePayDto basePayData, String openId, CloseableHttpClient wxPayClient) { // 1.获取请求参数的Map格式 Map paramsMap = getBasePayParams(wxPayConfig, basePayData); // 1.1 添加支付者信息 Map payerMap = new HashMap(); payerMap.put("openid",openId); paramsMap.put("payer",payerMap); // 2.获取请求对象 HttpPost httpPost = getHttpPost(wxPayConfig, WxApiConstants.JSAPI_PAY, paramsMap); // 3.完成签名并执行请求 CloseableHttpResponse response = null; try { response = wxPayClient.execute(httpPost); } catch (IOException e) { e.printStackTrace(); throw new ServiceException("微信支付请求失败"); } // 4.解析response对象 HashMap resultMap = resolverResponse(response); if (resultMap != null) { // native请求返回的是二维码链接,前端将链接转换成二维码即可 return resultMap.get("prepay_id"); } return null; } /** * 创建微信支付订单-APP方式 * * @param wxPayConfig 微信配置信息 * @param basePayData 基础请求信息,商品标题、商家订单id、订单价格 * @param wxPayClient 微信请求客户端() * @return 微信支付二维码地址 */ public static String wxAppPay(WxPayConstants wxPayConfig, WxChatBasePayDto basePayData, CloseableHttpClient wxPayClient) { // 1.获取请求参数的Map格式 Map paramsMap = getBasePayParams(wxPayConfig, basePayData); // 2.获取请求对象 HttpPost httpPost = getHttpPost(wxPayConfig, WxApiConstants.APP_PAY, paramsMap); // 3.完成签名并执行请求 CloseableHttpResponse response = null; try { response = wxPayClient.execute(httpPost); } catch (IOException e) { e.printStackTrace(); throw new ServiceException("微信支付请求失败"); } // 4.解析response对象 HashMap resultMap = resolverResponse(response); if (resultMap != null) { // native请求返回的是二维码链接,前端将链接转换成二维码即可 return resultMap.get("prepay_id"); } return null; }
8.创建实体存储前端微信支付所需参数(WxChatPayDto)
因为前端拉起微信支付需要多个参数,直接用一个实体返回更便捷
@Data @ApiModel("前端微信支付所需参数") public class WxChatPayDto { @ApiModelProperty("需要支付的小程序id") private String appid; @ApiModelProperty("时间戳(当前的时间)") private String timeStamp; @ApiModelProperty("随机字符串,不长于32位。") private String nonceStr; @ApiModelProperty("小程序下单接口返回的prepay_id参数值,提交格式如:prepay_id=***") private String prepayId; @ApiModelProperty("签名类型,默认为RSA,仅支持RSA。") private String signType; @ApiModelProperty("签名,使用字段appId、timeStamp、nonceStr、package计算得出的签名值") private String paySign;
设置一个公共方法pay,每次支付可以直接调用
@Resource private WxPayConstants wxPayConfig; @Resource private CloseableHttpClient wxPayClient; /** * 微信用户调用微信支付 */ @Override public WxChatPayDto pay(WxChatBasePayDto payData) { String prepayId = WxPayCommon.wxJsApiPay(wxPayConfig, payData, payData.getOpenId(), wxPayClient); WxChatPayDto wxChatPayDto = new WxChatPayDto(); wxChatPayDto.setAppid(redisCache.getCacheObject("WX_PAY_SAVE_WX_APPID")); wxChatPayDto.setTimeStamp(String.valueOf(System.currentTimeMillis() / 1000)); wxChatPayDto.setNonceStr(UUID.randomUUID().toString().replaceAll("-", "")); wxChatPayDto.setPrepayId("prepay_id=" + prepayId); wxChatPayDto.setSignType("RSA"); wxChatPayDto.setPaySign(getSign(wxChatPayDto.getNonceStr(),wxChatPayDto.getAppid(),wxChatPayDto.getPrepayId(),Long.parseLong(wxChatPayDto.getTimeStamp()))); return wxChatPayDto; } /** * 获取签名 * @param nonceStr 随机数 * @param appId 微信公众号或者小程序等的appid * @param prepay_id 预支付交易会话ID * @param timestamp 时间戳 10位 * @return String 新签名 */ String getSign(String nonceStr, String appId, String prepay_id, long timestamp) { //从下往上依次生成 String message = buildMessage(appId, timestamp, nonceStr, prepay_id); //签名 try { return sign(message.getBytes("utf-8")); } catch (IOException e) { throw new RuntimeException("签名异常,请检查参数或商户私钥"); } } String sign(byte[] message) { try { //签名方式 Signature sign = Signature.getInstance("SHA256withRSA"); //私钥,通过MyPrivateKey来获取,这是个静态类可以接调用方法 ,需要的是_key.pem文件的绝对路径配上文件名 sign.initSign(PemUtil.loadPrivateKey(new FileInputStream(redisCache.getCacheObject("WX_PAY_SAVE_PRIVATE_KEY_PATH").toString()))); sign.update(message); return Base64.getEncoder().encodeToString(sign.sign()); } catch (Exception e) { throw new RuntimeException("签名异常,请检查参数或商户私钥"); } } /** * 按照前端签名文档规范进行排序,\n是换行 * * @param nonceStr 随机数 * @param appId 微信公众号或者小程序等的appid * @param prepay_id 预支付交易会话ID * @param timestamp 时间戳 10位 * @return String 新签名 */ String buildMessage(String appId, long timestamp, String nonceStr, String prepay_id) { return appId + "\n" + timestamp + "\n" + nonceStr + "\n" + prepay_id + "\n"; }
调用pay,获取前端拉起支付所需要的参数
//支付 WxChatBasePayDto payData = new WxChatBasePayDto(); payData.setTitle("订单支付"); payData.setOrderId(runOrder.getOrderNumber()); payData.setPrice(new BigDecimal(0.01)); payData.setNotify(WxNotifyConstants.RUN_ERRANDS_NOTIFY); payData.setOpenId(runOrder.getOpenId()); WxChatPayDto pay = pay(payData);
给前端直接返回pay这个参数,前端通过里面的参数拉起支付
JSAPI调起支付API
同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
如果在所有通知频率后没有收到微信侧回调,商户应调用查询订单接口确认订单状态。
特别提醒:商户系统对于开启结果通知的内容一定要做签名验证,并校验通知的信息是否与商户侧的信息一致,防止数据泄露导致出现“假通知”,造成资金损失。
该链接是通过基础下单接口中的请求参数“notify_url”来设置的,要求必须为https地址。请确保回调URL是外部可正常访问的,且不能携带后缀参数,否则可能导致商户无法接收到微信的回调通知信息。
import com.alibaba.fastjson.JSONObject; import com.xxx.framework.redis.RedisCache; import com.wechat.pay.contrib.apache.httpclient.auth.Verifier; import com.wechat.pay.contrib.apache.httpclient.util.AesUtil; import org.springframework.stereotype.Component; import javax.annotation.Resource; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.security.GeneralSecurityException; import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; /** * 微信支付回调工具类 */ @Component public class WxPayCallbackUtil { @Resource private RedisCache redisCache; @Resource private Verifier verifier; /** * 获取回调数据 * @param request * @param response * @return */ public MapwxChatPayCallback(HttpServletRequest request, HttpServletResponse response) { //获取报文 String body = getRequestBody(request); //随机串 String nonceStr = request.getHeader("Wechatpay-Nonce"); //微信传递过来的签名 String signature = request.getHeader("Wechatpay-Signature"); //证书序列号(微信平台) String serialNo = request.getHeader("Wechatpay-Serial"); //时间戳 String timestamp = request.getHeader("Wechatpay-Timestamp"); //构造签名串 应答时间戳\n,应答随机串\n,应答报文主体\n String signStr = Stream.of(timestamp, nonceStr, body).collect(Collectors.joining("\n", "", "\n")); Map map = new HashMap<>(2); try { //验证签名是否通过 boolean result = verifiedSign(serialNo, signStr, signature); if(result){ //解密数据 String plainBody = decryptBody(body); return convertWechatPayMsgToMap(plainBody); } } catch (Exception e) { e.printStackTrace(); } return map; } /** * 转换body为map * @param plainBody * @return */ public Map convertWechatPayMsgToMap(String plainBody){ Map paramsMap = new HashMap<>(2); JSONObject jsonObject = JSONObject.parseObject(plainBody); //商户订单号 paramsMap.put("out_trade_no",jsonObject.getString("out_trade_no")); //交易状态 paramsMap.put("trade_state",jsonObject.getString("trade_state")); //附加数据 paramsMap.put("attach",jsonObject.getString("attach")); if (jsonObject.getJSONObject("attach") != null && !jsonObject.getJSONObject("attach").equals("")){ paramsMap.put("account_no",jsonObject.getJSONObject("attach").getString("accountNo")); } return paramsMap; } /** * 解密body的密文 * * "resource": { * "original_type": "transaction", * "algorithm": "AEAD_AES_256_GCM", * "ciphertext": "", * "associated_data": "", * "nonce": "" * } * * @param body * @return */ public String decryptBody(String body) throws UnsupportedEncodingException, GeneralSecurityException { AesUtil aesUtil = new AesUtil(redisCache.getCacheObject("WX_PAY_SAVE_KEY").toString().getBytes("utf-8")); JSONObject object = JSONObject.parseObject(body); JSONObject resource = object.getJSONObject("resource"); String ciphertext = resource.getString("ciphertext"); String associatedData = resource.getString("associated_data"); String nonce = resource.getString("nonce"); return aesUtil.decryptToString(associatedData.getBytes("utf-8"),nonce.getBytes("utf-8"),ciphertext); } /** * 验证签名 * * @param serialNo 微信平台-证书序列号 * @param signStr 自己组装的签名串 * @param signature 微信返回的签名 * @return * @throws UnsupportedEncodingException */ public boolean verifiedSign(String serialNo, String signStr, String signature) throws UnsupportedEncodingException { return verifier.verify(serialNo, signStr.getBytes("utf-8"), signature); } /** * 读取请求数据流 * * @param request * @return */ public String getRequestBody(HttpServletRequest request) { StringBuffer sb = new StringBuffer(); try (ServletInputStream inputStream = request.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); ) { String line; while ((line = reader.readLine()) != null) { sb.append(line); } } catch (IOException e) { e.printStackTrace(); } return sb.toString(); } }
支付成功后的回调方法
/** * 订单支付后回调 */ @Override public MapwxOrderCallBack(HttpServletRequest request, HttpServletResponse response) { Map map = new HashMap<>(2); try { Map stringMap = wxPayCallbackUtil.wxChatPayCallback(request, response); //支付成功 if (stringMap.get("trade_state").equals("SUCCESS")){ //编写支付成功后逻辑 } //响应微信 map.put("code", "SUCCESS"); map.put("message", "成功"); } catch (Exception e) { e.printStackTrace(); } return map; }
微信支付订单号,微信支付订单号和商家订单号二选一,这个是必不可少的,原订单金额也是必填的,微信会做二次验证。
@Data @ApiModel("微信退款对象") public class WxChatRefundDto { @ApiModelProperty("微信支付订单号,微信支付订单号和商家订单号二选一") private String transactionId; @ApiModelProperty("商家订单号,对应 out_trade_no") private String orderId; @ApiModelProperty("商户退款单号,对应out_refund_no") private String refundOrderId; @ApiModelProperty("退款原因,选填") private String reason; @ApiModelProperty("回调地址") private WxNotifyConstants notify; @ApiModelProperty("退款金额") private BigDecimal refundMoney; @ApiModelProperty("原订单金额,必填") private BigDecimal totalMoney;
import com.xxx.common.exception.ServiceException; import com.xxx.project.wx.constants.WxApiConstants; import com.xxx.project.wx.constants.WxPayConstants; import com.xxx.project.wx.dto.WxChatRefundDto; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.CloseableHttpClient; import java.io.IOException; import java.util.HashMap; import java.util.Map; @Slf4j public class WxPayRefundUtil { /** * 封装微信支付申请退款请求参数 * @param dto 微信支付申请退款请求参数 * @return 封装后的map微信支付申请退款请求参数对象 */ private static MapgetRefundParams(WxPayConstants wxPayConstants, WxChatRefundDto dto) { Map paramsMap = new HashMap<>(); if (StringUtils.isNoneBlank(dto.getTransactionId())) { paramsMap.put("transaction_id", dto.getTransactionId()); } else if (StringUtils.isNoneBlank(dto.getOrderId())) { paramsMap.put("out_trade_no", dto.getOrderId()); } else { throw new ServiceException("微信支付订单号和商户订单号必须填写一个"); } paramsMap.put("out_refund_no", dto.getRefundOrderId()); if (StringUtils.isNoneBlank(dto.getReason())) { paramsMap.put("reason", dto.getReason()); } paramsMap.put("notify_url", wxPayConstants.getNotifyDomain().concat(dto.getNotify().getType())); Map amountMap = new HashMap<>(); amountMap.put("refund", dto.getRefundMoney()); amountMap.put("total", dto.getTotalMoney()); amountMap.put("currency", "CNY"); paramsMap.put("amount", amountMap); return paramsMap; } /** * 发起微信退款申请 * * @param wxPayConfig 微信配置信息 * @param param 微信支付申请退款请求参数 * @param wxPayClient 微信请求客户端() * @return 微信支付二维码地址 */ public static String refundPay(WxPayConstants wxPayConfig, WxChatRefundDto param, CloseableHttpClient wxPayClient) { // 1.获取请求参数的Map格式 Map paramsMap = getRefundParams(wxPayConfig, param); // 2.获取请求对象 HttpPost httpPost = WxPayCommon.getHttpPost(wxPayConfig, WxApiConstants.DOMESTIC_REFUNDS, paramsMap); // 3.完成签名并执行请求 CloseableHttpResponse response = null; try { response = wxPayClient.execute(httpPost); } catch (IOException e) { e.printStackTrace(); throw new ServiceException("微信支付请求失败"); } // 4.解析response对象 HashMap resultMap = WxPayCommon.resolverResponse(response); log.info("发起退款参数:{}",resultMap); if (resultMap != null) { // 返回微信支付退款单号 return resultMap.get("refund_id"); } return null; } }
@Resource private WxPayConstants wxPayConstants; @Resource private CloseableHttpClient closeableHttpClient; WxChatRefundDto dto = new WxChatRefundDto(); dto.setOrderId(); //订单号 String refundOrderId = IdWorker.getIdStr(); dto.setRefundOrderId(refundOrderId); dto.setNotify(WxNotifyConstants.CAMPUS_CARD_REFUND_NOTIFY); //回调地址 dto.setRefundMoney(new BigDecimal(1));//单位为分 dto.setTotalMoney(new BigDecimal(1));//单位为分 String s = WxPayRefundUtil.refundPay(wxPayConstants, dto, closeableHttpClient);
注意:
1、交易时间超过一年的订单无法提交退款
2、微信支付退款支持单笔交易分多次退款(不超50次),多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。申请退款总>金额不能超过订单金额。一笔退款失败后重新提交,请不要更换退款单号,请使用原商户退款单号
3、错误或无效请求频率限制:6qps,即每秒钟异常或错误的退款申请请求不超过6次
4、每个支付订单的部分退款次数不能超过50次
5、如果同一个用户有多笔退款,建议分不同批次进行退款,避免并发退款导致退款失败
6、申请退款接口的返回仅代表业务的受理情况,具体退款是否成功,需要通过退款查询接口获取结果
7、一个月之前的订单申请退款频率限制为:5000/min
8、同一笔订单多次退款的请求需相隔1分钟
import cn.hutool.core.date.DateUtil; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.math.BigDecimal; import java.util.Date; @Data @ApiModel("微信退款回调参数") public class WxChatCallbackRefundDto { @ApiModelProperty("商户订单号") private String orderId; @ApiModelProperty("商户退款单号,out_refund_no") private String refundId; @ApiModelProperty("微信支付系统生成的订单号") private String transactionId; @ApiModelProperty("微信支付系统生成的退款订单号") private String transactionRefundId; @ApiModelProperty("退款渠道 1.ORIGINAL:原路退款 2.BALANCE:退回到余额 " + "3.OTHER_BALANCE:原账户异常退到其他余额账户 4.OTHER_BANKCARD:原银行卡异常退到其他银行卡") private String channel; @ApiModelProperty("退款成功时间 当前退款成功时才有此返回值") private Date successTime; @ApiModelProperty("退款状态 退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往商户平台-交易中心,手动处理此笔退款。" + "1.SUCCESS:退款成功 2.CLOSED:退款关闭 3.PROCESSING:退款处理中 4.ABNORMAL:退款异常") private String status; @ApiModelProperty("退款金额") private BigDecimal refundMoney; public Date getSuccessTime() { return successTime; } public void setSuccessTime(String successTime) { // Hutool工具包的方法,自动识别一些常用格式的日期字符串 this.successTime = DateUtil.parse(successTime); } }
import com.xxx.project.wx.dto.WxChatCallbackRefundDto; /** * 退款处理接口,为了防止项目开发人员,不手动判断退款失败的情况 * 退款失败:退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往商户平台-交易中心,手动处理此笔退款 */ public interface WechatRefundCallback { /** * 退款成功处理情况 */ void success(WxChatCallbackRefundDto refundData); /** * 退款失败处理情况 */ void fail(WxChatCallbackRefundDto refundData); }
public class HttpUtils{ /** * 将通知参数转化为字符串 * @param request * @return */ public static String readData(HttpServletRequest request) { BufferedReader br = null; try { StringBuilder result = new StringBuilder(); br = request.getReader(); for (String line; (line = br.readLine()) != null; ) { if (result.length() > 0) { result.append("\n"); } result.append(line); } return result.toString(); } catch (IOException e) { throw new RuntimeException(e); } finally { if (br != null) { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
package com.runerrands.project.wx.util; import com.wechat.pay.contrib.apache.httpclient.auth.Verifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.time.DateTimeException; import java.time.Duration; import java.time.Instant; import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*; public class WechatPayValidatorForRequest { protected static final Logger log = LoggerFactory.getLogger(WechatPayValidatorForRequest.class); /** * 应答超时时间,单位为分钟 */ protected static final long RESPONSE_EXPIRED_MINUTES = 5; protected final Verifier verifier; protected final String body; protected final String requestId; public WechatPayValidatorForRequest(Verifier verifier, String body, String requestId) { this.verifier = verifier; this.body = body; this.requestId = requestId; } protected static IllegalArgumentException parameterError(String message, Object... args) { message = String.format(message, args); return new IllegalArgumentException("parameter error: " + message); } protected static IllegalArgumentException verifyFail(String message, Object... args) { message = String.format(message, args); return new IllegalArgumentException("signature verify fail: " + message); } public final boolean validate(HttpServletRequest request) throws IOException { try { validateParameters(request); String message = buildMessage(request); String serial = request.getHeader(WECHAT_PAY_SERIAL); String signature = request.getHeader(WECHAT_PAY_SIGNATURE); if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) { throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]", serial, message, signature, request.getHeader(REQUEST_ID)); } } catch (IllegalArgumentException e) { log.warn(e.getMessage()); return false; } return true; } protected final void validateParameters(HttpServletRequest request) { // NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP}; String header = null; for (String headerName : headers) { header = request.getHeader(headerName); if (header == null) { throw parameterError("empty [%s], request-id=[%s]", headerName, requestId); } } String timestampStr = header; try { Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr)); // 拒绝过期应答 if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) { throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId); } } catch (DateTimeException | NumberFormatException e) { throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId); } } protected final String buildMessage(HttpServletRequest request) throws IOException { String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP); String nonce = request.getHeader(WECHAT_PAY_NONCE); return timestamp + "\n" + nonce + "\n" + body + "\n"; } }
import com.google.gson.Gson; import com.xxx.common.exception.ServiceException; import com.xxx.common.utils.http.HttpUtils; import com.xxx.project.wx.constants.WxPayConstants; import com.xxx.project.wx.dto.WxChatCallbackRefundDto; import com.xxx.project.wx.service.WechatRefundCallback; import com.wechat.pay.contrib.apache.httpclient.auth.Verifier; import com.wechat.pay.contrib.apache.httpclient.util.AesUtil; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.util.HashMap; import java.util.Map; @Slf4j public class WxPayRefundCallbackUtil { /** * 微信支付申请退款回调方法 * * @param verifier 证书 * @param wxPayConfig 微信配置 * @param refundCallback 回调方法,用于处理业务逻辑,包含退款成功处理于退款失败处理 * @return json格式的string数据,直接返回给微信 */ public static String wxPayRefundCallback(HttpServletRequest request, HttpServletResponse response, Verifier verifier, WxPayConstants wxPayConfig, WechatRefundCallback refundCallback) { Gson gson = new Gson(); // 1.处理通知参数 final String body = HttpUtils.readData(request); HashMapbodyMap = gson.fromJson(body, HashMap.class); // 2.签名验证 WechatPayValidatorForRequest wechatForRequest = new WechatPayValidatorForRequest(verifier, body, (String) bodyMap.get("id")); try { if (!wechatForRequest.validate(request)) { // 通知验签失败 response.setStatus(500); final HashMap map = new HashMap<>(); map.put("code", "ERROR"); map.put("message", "通知验签失败"); return gson.toJson(map); } } catch (Exception e) { e.printStackTrace(); } // 3.获取明文数据 String plainText = decryptFromResource(bodyMap, wxPayConfig); HashMap plainTextMap = gson.fromJson(plainText, HashMap.class); // log.info("退款plainTextMap:{}", plainTextMap); // 4.封装微信返回的数据 WxChatCallbackRefundDto refundData = getRefundCallbackData(plainTextMap); if ("SUCCESS".equals(refundData.getStatus())) { // 执行业务逻辑 refundCallback.success(refundData); } else { // 特殊情况退款失败业务处理,退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往商户平台-交易中心,手动处理此笔退款 refundCallback.fail(refundData); } // 5.成功应答 response.setStatus(200); final HashMap resultMap = new HashMap<>(); resultMap.put("code", "SUCCESS"); resultMap.put("message", "成功"); return gson.toJson(resultMap); } private static WxChatCallbackRefundDto getRefundCallbackData(HashMap plainTextMap) { Gson gson = new Gson(); WxChatCallbackRefundDto refundData = new WxChatCallbackRefundDto(); String successTime = String.valueOf(plainTextMap.get("success_time")); if (StringUtils.isNoneBlank(successTime)) { refundData.setSuccessTime(successTime); } refundData.setOrderId(String.valueOf(plainTextMap.get("out_trade_no"))); refundData.setRefundId(String.valueOf(plainTextMap.get("out_refund_no"))); refundData.setTransactionId(String.valueOf(plainTextMap.get("transaction_id"))); refundData.setTransactionRefundId(String.valueOf(plainTextMap.get("refund_id"))); refundData.setChannel(String.valueOf(plainTextMap.get("channel"))); final String status = String.valueOf(plainTextMap.get("refund_status")); refundData.setStatus(status); String amount = String.valueOf(plainTextMap.get("amount")); HashMap amountMap = gson.fromJson(amount, HashMap.class); String refundMoney = String.valueOf(amountMap.get("refund")); refundData.setRefundMoney(new BigDecimal(refundMoney).movePointLeft(2)); // log.info("refundData:{}", refundData); return refundData; } /** * 对称解密 */ private static String decryptFromResource(HashMap bodyMap, WxPayConstants wxPayConfig) { // 通知数据 Map resourceMap = (Map) bodyMap.get("resource"); // 数据密文 String ciphertext = resourceMap.get("ciphertext"); // 随机串 String nonce = resourceMap.get("nonce"); // 附加数据 String associateData = resourceMap.get("associated_data"); AesUtil aesUtil = new AesUtil(wxPayConfig.getKey().getBytes(StandardCharsets.UTF_8)); try { return aesUtil.decryptToString(associateData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext); } catch (GeneralSecurityException e) { e.printStackTrace(); throw new ServiceException("解密失败"); } } }
@Resource private WxPayConstants wxPayConfig; @Resource private Verifier verifier; @ApiOperation("微信退款回调接口") @PostMapping("/refundWechatCallback") public String refundWechatCallback(HttpServletRequest request, HttpServletResponse response) { return WxPayRefundCallbackUtil.wxPayRefundCallback(request, response, verifier, wxPayConfig, new WechatRefundCallback() { @Override public void success(WxChatCallbackRefundDto refundData) { // TODO 退款成功的业务逻辑,例如更改订单状态为退款成功等 System.out.println("退款成功"); } @Override public void fail(WxChatCallbackRefundDto refundData) { // TODO 特殊情况下退款失败业务处理,例如银行卡冻结需要人工退款,此时可以邮件或短信提醒管理员,并携带退款单号等关键信息 System.out.println("退款失败"); } }); }