0


SpringBoot(JAVA)整合小程序支付和退款功能详解

个人建议支付和退款不要自己去封装请求,因为有很多坑!而且还不一定能弄成功!代码复制直接可用。
需要注意有可能在执行编译证书的时候可能会报一个长度异常,是因为jdk1.8对密匙长度有限制,用jdk大于1.8的版本就能正常编译。

官方文档不是很清晰,都是零零散散的,但有总比没有好。
微信小程序支付文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_1.shtml
微信小程序退款文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_9.shtml

微信小程序支付

在进行对接微信支付之前,我们首先需要将以下几点准备好:

  • 申请APPID
  • 申请商户号
  • 小程序开通微信支付,绑定已经申请好的商户号。登录小程序后台(mp.weixin.qq.com)。点击左侧导航栏的微信支付,在页面中进行开通。(开通申请要求小程序已发布上线)

支付流程图:
在这里插入图片描述

微信小程序退款

当交易发生之后一年内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付金额退还给买家,微信支付将在收到退款请求并且验证成功之后,将支付款按原路退还至买家账号上。

(1)微信退款所需要的配置!,退款只需要证书即可。微信退款需要证书:资金发生变化需要证书。支付接口不需要。点击证书使用。按照步骤:下载证书
(2)使用API证书
apiclient_cert.p12是商户证书文件,除PHP外的开发均使用此证书文件。
◆ 商户如果使用.NET环境开发,请确认Framework版本大于2.0,必须在操作系统上双击安装证书apiclient_cert.p12后才能被正常调用。
◆ API证书调用或安装需要使用到密码,该密码的值为微信商户号(mch_id)
(3)API证书安全
1.证书文件不能放在web服务器虚拟目录,应放在有访问权限控制的目录中,防止被他人下载;
2.建议将证书文件名改为复杂且不容易猜测的文件名;
3.商户服务器要做好病毒和木马防护工作,不被非法侵入者窃取证书文件。
证书相关API:

退款注意事项
1、交易时间超过一年的订单无法提交退款。
2、微信支付退款支持单笔交易分多次退款,多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。申请退款总金额不能超过订单金额。 一笔退款失败后重新提交,请不要更换退款单号,请使用原商户退款单号。
3、请求频率限制:150qps,即每秒钟正常的申请退款请求次数不超过150次。
4、每个支付订单的部分退款次数不能超过50次。
5、如果同一个用户有多笔退款,建议分不同批次进行退款,避免并发退款导致退款失败。
6、申请退款接口的返回仅代表业务的受理情况,具体退款是否成功,需要通过退款查询接口获取结果。
7、一个月之前的订单申请退款频率限制为:5000/min。
8、同一笔订单多次退款的请求需相隔1分钟。

导入依赖

  1. <dependency><groupId>com.github.wechatpay-apiv3</groupId><artifactId>wechatpay-java</artifactId><version>0.2.11</version></dependency>

在application.yml添加对应配置

  1. # 微信小程序支付配置信息
  2. wx:
  3. # 微信小程序appid
  4. app-id:
  5. # 小程序密钥
  6. app-secret:
  7. # 商户号
  8. mch-id:
  9. # 证书序列号
  10. mch-serial-no:
  11. # api密钥
  12. api-key:
  13. # 回调接口地址
  14. notify-url:
  15. # 证书地址
  16. key-path:D:\pay\apiclient_key.pem

这里要说下回调地址和证书地址,回调地址是当创建订单后前端调用回调地址查看支付状态修改订单状态,但是要注意回调地址要放开token校验,证书地址是需要在微信API安全去下载一个证书,这个位置就是你证书所放的位置。如果需要退款回调地址就在添加一个参数即可

yml配置对应实体

  1. @Component@ConfigurationProperties(prefix ="wx")@Data@ToStringpublicclassWxPayV3Bean{//小程序appidprivateString appId;//商户号privateString mchId;//证书序列号privateString mchSerialNo;//小程序秘钥privateString appSecret;//api秘钥privateString apiKey;//回调接口地址privateString notifyUrl;//证书地址privateString keyPath;}

Util工具类

  1. publicclassWXPayUtil{privatestaticfinalStringSYMBOLS="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";privatestaticfinalRandomRANDOM=newSecureRandom();publicstaticStringgetSign(String signatureStr,String privateKey)throwsInvalidKeyException,NoSuchAlgorithmException,SignatureException,IOException,URISyntaxException{//replace 根据实际情况,不一定都需要String replace = privateKey.replace("\\n","\n");PrivateKey merchantPrivateKey =PemUtil.loadPrivateKeyFromPath(replace);Signature sign =Signature.getInstance("SHA256withRSA");
  2. sign.initSign(merchantPrivateKey);
  3. sign.update(signatureStr.getBytes(StandardCharsets.UTF_8));returnBase64Utils.encodeToString(sign.sign());}/**
  4. * 获取随机字符串 Nonce Str
  5. *
  6. * @return String 随机字符串
  7. */publicstaticStringgenerateNonceStr(){char[] nonceChars =newchar[32];for(int index =0; index < nonceChars.length;++index){
  8. nonceChars[index]=SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));}returnnewString(nonceChars);}}

预支付实体类

字段根据实际需求而变化

  1. @Data@Accessors(chain =true)publicclassWXPayOrderReqVO{@ApiModelProperty(value ="订单支付类型(商品订单;预约订单)",required =true)@NotBlank(message ="订单支付类型不能为空!")privateString orderType;//附加数据,回调时可根据这个数据辨别订单类型或其他@ApiModelProperty(value ="订单定金金额(单位:分)",required =true)@NotNull(message ="订单定金金额不能为空!")// @DecimalFormat(precision = 2)privateInteger amount;@ApiModelProperty(value ="商品描述",required =true)@NotBlank(message ="商品描述不能为空!")privateString description;@ApiModelProperty(value ="openId",required =true)@NotBlank(message ="openId不能为空!")privateString openId;}

controller

Result 是自定义返回类,修改成自己的。

  1. @Api@RestController@Slf4j@RequestMapping("/order")publicclassOrderController{@AutowiredprivateWxPayService wxPayService;/**
  2. * 微信预支付
  3. *
  4. * @param req
  5. * @param request
  6. * @return
  7. * @throws Exception
  8. */@ApiOperation(value ="微信预支付", notes ="微信预支付")@PostMapping("/createOrder")publicResultcreateOrder(@RequestBody@ValidatedWXPayOrderReqVO req,HttpServletRequest request)throwsException{
  9. log.info("传入参数:"+JSONObject.toJSONString(req));return wxPayService.createOrder(req, request);}/**
  10. * 微信支付回调
  11. *
  12. * @param request
  13. * @return
  14. * @throws Exception
  15. */@ApiOperation(value ="微信支付回调", notes ="微信支付回调")@PostMapping("/payNotify")publicResultpayNotify(HttpServletRequest request)throwsException{
  16. log.info("-----------------------微信支付回调通知-----------------------");//注意:回调接口需要暴露到公网上,且要放开token验证return wxPayService.payNotify(request);}/**
  17. * 退款
  18. *
  19. * @param
  20. * @return
  21. * @throws Exception
  22. */@ApiOperation(value ="退款", notes ="退款")@PostMapping("/refund")publicResultrefund(@RequestParam("orderId")String orderId){if(orderId ==null||"".equals(orderId)){returnResult.error("参数为空");}return wxPayService.refund(orderId,"退款");}/**
  23. * 微信退款回调
  24. *
  25. * @return
  26. */@ApiOperation(value ="微信退款回调", notes ="微信退款回调")@PostMapping("/refund/payNotify")publicResultrefundPayNotify(HttpServletRequest request)throwsException{//注意:回调接口需要暴露到公网上,且要放开token验证return wxPayService.refundNotify(request);}}

service

  1. publicinterfaceWxPayService{ResultcreateOrder(WXPayOrderReqVO req,HttpServletRequest request)throwsException;ResultpayNotify(HttpServletRequest request)throwsException;Resultrefund(String orderId,String refundReason);ResultrefundNotify(HttpServletRequest request);}

serviceImpl

  1. @Slf4j@ServicepublicclassWxPayServiceImplimplementsWxPayService{//注入支付对应配置@AutowiredprivateWxPayV3Bean wxPayV3Bean;@AutowiredprivateSysUserMapper userMapper;@AutowiredprivateMachineOrderMapper orderMapper;privatestaticbyte[] certData;/**
  2. * 预支付接口
  3. * @param req
  4. * @param request
  5. * @return
  6. * @throws Exception
  7. */@Override@Transactional(rollbackFor =Exception.class)publicResultcreateOrder(WXPayOrderReqVO req,HttpServletRequest request)throwsException{Map<String,Object> params =newHashMap<>();synchronized(this){SysUser user = userMapper.getUserByOpenId(req.getOpenId());if(user ==null||StringUtil.isEmpty(user.getOpenid())){returnResult.error("无效用户");}//生成商户订单号String tradeNo =getTradeNo();// 使用自动更新平台证书的RSA配置,配置微信支付的自动证书管理功能Config config =newRSAAutoCertificateConfig.Builder().merchantId(wxPayV3Bean.getMchId()).privateKeyFromPath(wxPayV3Bean.getKeyPath()).merchantSerialNumber(wxPayV3Bean.getMchSerialNo()).apiV3Key(wxPayV3Bean.getApiKey()).build();// 构建service,用于处理JSAPI支付相关的操作JsapiService service =newJsapiService.Builder().config(config).build();// 创建预支付订单的请求对象PrepayRequest prepayRequest =newPrepayRequest();Amount amount =newAmount();
  8. amount.setTotal(req.getAmount());
  9. prepayRequest.setAmount(amount);
  10. prepayRequest.setAppid(wxPayV3Bean.getAppId());
  11. prepayRequest.setMchid(wxPayV3Bean.getMchId());
  12. prepayRequest.setNotifyUrl(wxPayV3Bean.getNotifyUrl());
  13. prepayRequest.setDescription(req.getDescription());
  14. prepayRequest.setOutTradeNo(tradeNo);
  15. prepayRequest.setAttach(req.getOrderType());Payer payer =newPayer();
  16. payer.setOpenid(user.getOpenid());
  17. prepayRequest.setPayer(payer);// 调用下单方法,得到应答PrepayResponse response = service.prepay(prepayRequest);
  18. log.info("调用下单方法请求返回:"+ response);Long timeStamp =System.currentTimeMillis()/1000;
  19. params.put("timeStamp", timeStamp);String substring =UUID.randomUUID().toString().replaceAll("-","").substring(0,32);
  20. params.put("nonceStr", substring);String signatureStr =Stream.of(wxPayV3Bean.getAppId(),String.valueOf(timeStamp), substring,"prepay_id="+ response.getPrepayId()).collect(Collectors.joining("\n","","\n"));String sign =WXPayUtil.getSign(signatureStr, wxPayV3Bean.getKeyPath());
  21. params.put("paySign", sign);
  22. params.put("package","prepay_id="+ response.getPrepayId());
  23. params.put("tradeNo", tradeNo);//保存订单信息try{MachineOrder order =newMachineOrder();//自己的业务代码
  24. orderMapper.insert(order);}catch(Exception e){
  25. e.printStackTrace();}}returnResult.OK(params);}/**
  26. * 支付回调
  27. *
  28. * @param request
  29. * @return
  30. * @throws Exception
  31. */@Override@Transactional(rollbackFor =Exception.class)publicResultpayNotify(HttpServletRequest request)throwsException{//读取请求体的信息ServletInputStream inputStream = request.getInputStream();StringBuffer stringBuffer =newStringBuffer();BufferedReader bufferedReader =newBufferedReader(newInputStreamReader(inputStream));String s;//读取回调请求体while((s = bufferedReader.readLine())!=null){
  32. stringBuffer.append(s);}String s1 = stringBuffer.toString();String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);String nonce = request.getHeader(WECHAT_PAY_NONCE);String signType = request.getHeader("Wechatpay-Signature-Type");String serialNo = request.getHeader(WECHAT_PAY_SERIAL);String signature = request.getHeader(WECHAT_PAY_SIGNATURE);// 构造一个RSAAutoCertificateConfigNotificationConfig config =newRSAAutoCertificateConfig.Builder().merchantId(wxPayV3Bean.getMchId()).privateKeyFromPath(wxPayV3Bean.getKeyPath()).merchantSerialNumber(wxPayV3Bean.getMchSerialNo()).apiV3Key(wxPayV3Bean.getApiKey()).build();// 初始化 NotificationParserNotificationParser parser =newNotificationParser(config);RequestParam requestParam =newRequestParam.Builder().serialNumber(serialNo).nonce(nonce).signature(signature).timestamp(timestamp)// 若未设置signType,默认值为 WECHATPAY2-SHA256-RSA2048.signType(signType).body(s1).build();Transaction parse = parser.parse(requestParam,Transaction.class);
  33. log.info("支付回调信息:"+JSONObject.toJSONString(parse));try{/**
  34. * SUCCESS:支付成功
  35. * REFUND:转入退款
  36. * NOTPAY:未支付
  37. * CLOSED:已关闭
  38. * REVOKED:已撤销(付款码支付)
  39. * USERPAYING:用户支付中(付款码支付)
  40. * PAYERROR:支付失败(其他原因,如银行返回失败)
  41. *///修改订单状态方法updateAccountDetail(parse);// 这里可以根据状态做不同的处理,我这里只处理支付成功if("SUCCESS".equals(parse.getTradeState())){//你的业务代码
  42. log.info("该订单:"+ parse.getOutTradeNo()+"已经进行回调,不可重复回调");returnResult.ok();}}catch(Exception e){
  43. e.printStackTrace();}returnResult.ok();}/**
  44. * 微信小程序退款
  45. *
  46. * @param orderId 订单id
  47. * @param refundReason 退款原因
  48. * @return
  49. */@Override@Transactional(rollbackFor =Exception.class)publicResultrefund(String orderId,String refundReason){synchronized(this){MachineOrder order = orderMapper.selectById(orderId);if(order ==null){returnResult.error("无订单信息");}try{// 使用自动更新平台证书的RSA配置// 一个商户号只能初始化一个配置,否则会因为重复的下载任务报错Config config =newRSAAutoCertificateConfig.Builder().merchantId(wxPayV3Bean.getMchId())//使用 SDK 不需要计算请求签名和验证应答签名// 使用 com.wechat.pay.java.core.util 中的函数从本地文件中加载商户私钥,商户私钥会用来生成请求的签名.privateKeyFromPath(wxPayV3Bean.getKeyPath()).merchantSerialNumber(wxPayV3Bean.getMchSerialNo()).apiV3Key(wxPayV3Bean.getApiKey()).build();// 构建退款serviceRefundService service =newRefundService.Builder().config(config).build();//构建退款请求CreateRequest request =newCreateRequest();// request.setXxx(val)设置所需参数,具体参数可见Request定义//构建订单金额信息AmountReq amountReq =newAmountReq();//退款金额
  50. amountReq.setRefund(Long.valueOf((int)(order.getPaymentMoney()*100)));//原订单金额
  51. amountReq.setTotal(Long.valueOf((int)(order.getPaymentMoney()*100)));//货币类型(默认人民币)
  52. amountReq.setCurrency("CNY");
  53. request.setAmount(amountReq);
  54. request.setOutTradeNo(order.getOrderNum());
  55. request.setReason("退款");//商户退款单号
  56. request.setOutRefundNo(String.valueOf(order.getOrderNum()));//退款通知回调地址,开设可不设,我是没有设置的//如果要设置就在配置加上对应的参数和参数值即可// request.setNotifyUrl(wxPayV3Bean.getRefundNotify());// 调用微信sdk退款接口Refund refund = service.create(request);
  57. log.info("请求退款返回:"+ refund);//接收退款返回参数// Status status = refund.getStatus();if(Status.SUCCESS.equals(refund.getStatus().SUCCESS)){//说明退款成功,开始接下来的业务操作 //你的业务代码,根据请求返回状态修改对应订单状态returnResult.ok("退款成功");}if(Status.PROCESSING.equals(refund.getStatus().PROCESSING)){//你的业务代码,根据请求返回状态修改对应订单状态returnResult.OK("退款中");}if(Status.ABNORMAL.equals(refund.getStatus().ABNORMAL)){//你的业务代码,根据请求返回状态修改对应订单状态returnResult.error("退款异常");}if(Status.CLOSED.equals(refund.getStatus().CLOSED)){//你的业务代码,根据请求返回状态修改对应订单状态returnResult.error("退款关闭");}}catch(Exception e){
  58. e.printStackTrace();}}returnResult.error("退款失败");}/**
  59. * 退款回调
  60. * @param request
  61. * @return
  62. */@OverridepublicResultrefundNotify(HttpServletRequest request){try{//读取请求体的信息ServletInputStream inputStream = request.getInputStream();StringBuffer stringBuffer =newStringBuffer();BufferedReader bufferedReader =newBufferedReader(newInputStreamReader(inputStream));String s;//读取回调请求体while((s = bufferedReader.readLine())!=null){
  63. stringBuffer.append(s);}String s1 = stringBuffer.toString();String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);String nonce = request.getHeader(WECHAT_PAY_NONCE);String signType = request.getHeader("Wechatpay-Signature-Type");String serialNo = request.getHeader(WECHAT_PAY_SERIAL);String signature = request.getHeader(WECHAT_PAY_SIGNATURE);// 如果已经初始化了 RSAAutoCertificateConfig,可直接使用// 没有的话,则构造一个NotificationConfig config =newRSAAutoCertificateConfig.Builder().merchantId(wxPayV3Bean.getMchId()).privateKeyFromPath(wxPayV3Bean.getKeyPath()).merchantSerialNumber(wxPayV3Bean.getMchSerialNo()).apiV3Key(wxPayV3Bean.getApiKey()).build();// 初始化 NotificationParserNotificationParser parser =newNotificationParser(config);RequestParam requestParam =newRequestParam.Builder().serialNumber(serialNo).nonce(nonce).signature(signature).timestamp(timestamp)// 若未设置signType,默认值为 WECHATPAY2-SHA256-RSA2048.signType(signType).body(s1).build();RefundNotification parse = parser.parse(requestParam,RefundNotification.class);System.out.println("parse = "+ parse);//parse.getRefundStatus().equals("SUCCESS");说明退款成功//这里和上面退款返回差不多的处理,可以抽成一个公共的方法if(Status.SUCCESS.equals(parse.getRefundStatus().SUCCESS)){//你的业务代码,根据请求返回状态修改对应订单状态returnResult.ok("退款成功");}if(Status.PROCESSING.equals(parse.getRefundStatus().PROCESSING)){//你的业务代码,根据请求返回状态修改对应订单状态returnResult.OK("退款中");}if(Status.ABNORMAL.equals(parse.getRefundStatus().ABNORMAL)){//你的业务代码,根据请求返回状态修改对应订单状态returnResult.error("退款异常");}if(Status.CLOSED.equals(parse.getRefundStatus().CLOSED)){//你的业务代码,根据请求返回状态修改对应订单状态returnResult.error("退款关闭");}}catch(Exception e){
  64. e.printStackTrace();}returnResult.error("回调失败");}/**
  65. * 修改订单状态
  66. *
  67. */publicvoidupdateAccountDetail(Transaction parse){//根据自己业务修改Map<String,Object> map =newHashMap<>();
  68. map.put("order_num", parse.getOutTradeNo().toString());
  69. map.put("transactionId", parse.getTransactionId().toString());
  70. map.put("tradeState", parse.getTradeState().toString());
  71. log.info("更新订单数据:"+JSONObject.toJSONString(map));
  72. orderMapper.updateOrderStatus(map);}/**
  73. * 时间+id为订单号
  74. *
  75. * @param
  76. * @return
  77. */publicStringgetTradeNo(){String idStr =WXPayUtil.generateNonceStr();// long timestamp = DateUtils.getCurrentTimestamp();// //序列号是为了保证生成的订单号的唯一性AtomicInteger sequence =newAtomicInteger(0);int nextSequence = sequence.getAndIncrement();try{MessageDigest md =MessageDigest.getInstance("MD5");byte[] messageDigest = md.digest(String.valueOf(nextSequence).getBytes());BigInteger no =newBigInteger(1, messageDigest);String encrypted = no.toString(10);// 将十六进制转为十进制表示的字符串// 如果加密结果长度超过20位,则截取前20位if(encrypted.length()>20){
  78. encrypted = encrypted.substring(0,20);}// String tradeNo = timestamp + idStr;return idStr;}catch(Exception e){
  79. e.printStackTrace();}returnnull;}}

本文转载自: https://blog.csdn.net/weixin_46204877/article/details/136630669
版权归原作者 刻苦的刘同学 所有, 如有侵权,请联系我们删除。

“SpringBoot(JAVA)整合小程序支付和退款功能详解”的评论:

还没有评论