springboot+vue实现微信扫描支付

8 篇文章 1 订阅
8 篇文章 0 订阅

Springboot+vue实现微信扫码支付

整体思路

按API要求组装参数,以XML方式发送(POST)给微信支付接口(URL),微信支付接口也是以XML方式给予响应。程序根据返回的结果(其中包括支付URL)生成二维码或判断订单状态。
在线微信支付开发文档:
https://pay.weixin.qq.com/wiki/doc/api/index.html
1.appid:微信公众账号或开放平台APP的唯一标识
2.mch_id:商户号 (配置文件中的partner)
3.partnerkey:商户密钥
4.sign:数字签名, 根据微信官方提供的密钥和一套算法生成的一个加密信息, 就是为了保证交易的安全性

前期准备

1.添加maven依赖:

<dependency>
		<groupId>com.github.wxpay</groupId>
		<artifactId>wxpay-sdk</artifactId>
		<version>0.0.3</version>
	</dependency>

我们主要会用到微信支付SDK的以下功能:
(1)获取随机字符串:

WXPayUtil.generateNonceStr()

(2)MAP转换为XML字符串(自动添加签名):

 WXPayUtil.generateSignedXml(param, partnerkey)

(3)XML字符串转换为MAP:

WXPayUtil.xmlToMap(result)

微信支付开发

1.开发模式:
模式一:商户在后台给你生成二维码,用户打开扫一扫
模式二:商户后台系统调用微信支付【统一下单API】生成预付交易,将接口返回的链接生成二维码,用户扫码后输入密码完成支付交易。注意:该模式的预付单有效期为2小时,过期后无法支付。
微信支付:生成xml发送请求。
2.在对应的模块中添加配置(采用尚硅谷的配置):

#关联的公众号appid
weixin.pay.appid=wx74862e0dfcf69954
#商户号
weixin.pay.partner=1558950191
#商户key
weixin.pay.partnerkey=T6m9iK73b0kn9g5v426MKfHQH7X8rKwb

3.引入工具类(也可以不用):

package com.tan.user.utils;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * @author tanxiangwen
 * @date 2023/1/28
 * @描述
 */
@Component
public class WxUtil implements InitializingBean {
    @Value("${weixin.appid}")
    private String appid;

    @Value("${weixin.partner}")
    private String partner;

    @Value("${weixin.partnerkey}")
    private String partnerkey;

    public static String APPID;
    public static String PARTNER;
    public static String PARTNERKEY;
    @Override
    public void afterPropertiesSet() throws Exception {
        APPID = appid;
        PARTNER = partner;
        PARTNERKEY = partnerkey;
    }

}

package com.tan.user.utils;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.*;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;

import javax.net.ssl.SSLContext;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.KeyStore;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * @author tanxiangwen
 * @date 2023/1/28
 * @描述
 */
public class HttpClient {
    private String url;
    private Map<String, String> param;
    private int statusCode;
    private String content;
    private String xmlParam;
    private boolean isHttps;
    private boolean isCert = false;
    //证书密码 微信商户号(mch_id)
    private String certPassword;



    public boolean isHttps() {
        return isHttps;
    }
    public void setHttps(boolean isHttps) {
        this.isHttps = isHttps;
    }
    public boolean isCert() {
        return isCert;
    }
    public void setCert(boolean cert) {
        isCert = cert;
    }
    public String getXmlParam() {
        return xmlParam;
    }
    public void setXmlParam(String xmlParam) {
        this.xmlParam = xmlParam;
    }
    public HttpClient(String url, Map<String, String> param) {
        this.url = url;
        this.param = param;
    }
    public HttpClient(String url) {
        this.url = url;
    }
    public String getCertPassword() {
        return certPassword;
    }
    public void setCertPassword(String certPassword) {
        this.certPassword = certPassword;
    }
    public void setParameter(Map<String, String> map) {
        param = map;
    }
    public void addParameter(String key, String value) {
        if (param == null)
            param = new HashMap<String, String>();
        param.put(key, value);
    }
    public void post() throws ClientProtocolException, IOException {
        HttpPost http = new HttpPost(url);
        setEntity(http);
        execute(http);
    }
    public void put() throws ClientProtocolException, IOException {
        HttpPut http = new HttpPut(url);
        setEntity(http);
        execute(http);
    }
    public void get() throws ClientProtocolException, IOException {
        if (param != null) {
            StringBuilder url = new StringBuilder(this.url);
            boolean isFirst = true;
            for (String key : param.keySet()) {
                if (isFirst)
                    url.append("?");
                else
                    url.append("&");
                url.append(key).append("=").append(param.get(key));
            }
            this.url = url.toString();
        }
        HttpGet http = new HttpGet(url);
        execute(http);
    }
    /**
     * set http post,put param
     */
    private void setEntity(HttpEntityEnclosingRequestBase http) {
        if (param != null) {
            List<NameValuePair> nvps = new LinkedList<NameValuePair>();
            for (String key : param.keySet())
                nvps.add(new BasicNameValuePair(key, param.get(key))); // 参数
            http.setEntity(new UrlEncodedFormEntity(nvps, Consts.UTF_8)); // 设置参数
        }
        if (xmlParam != null) {
            http.setEntity(new StringEntity(xmlParam, Consts.UTF_8));
        }
    }
    private void execute(HttpUriRequest http) throws ClientProtocolException,
            IOException {
        CloseableHttpClient httpClient = null;
        try {
            if (isHttps) {
                if(isCert) {
                    //TODO证书路径
                    FileInputStream inputStream = new FileInputStream(new File(ConstantWxPropertiesUtils.CERT));
                    KeyStore keystore = KeyStore.getInstance("PKCS12");
                    char[] partnerId2charArray = certPassword.toCharArray();
                    keystore.load(inputStream, partnerId2charArray);
                    SSLContext sslContext = SSLContexts.custom().loadKeyMaterial(keystore, partnerId2charArray).build();
                    SSLConnectionSocketFactory sslsf =
                            new SSLConnectionSocketFactory(sslContext,
                                    new String[] { "TLSv1" },
                                    null,
                                    SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
                    httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
                } else {
                    SSLContext sslContext = new SSLContextBuilder()
                            .loadTrustMaterial(null, new TrustStrategy() {
                                // 信任所有
                                public boolean isTrusted(X509Certificate[] chain,
                                                         String authType)
                                        throws CertificateException {
                                    return true;
                                }
                            }).build();
                    SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
                            sslContext);
                    httpClient = HttpClients.custom().setSSLSocketFactory(sslsf)
                            .build();
                }
            } else {
                httpClient = HttpClients.createDefault();
            }
            CloseableHttpResponse response = httpClient.execute(http);
            try {
                if (response != null) {
                    if (response.getStatusLine() != null)
                        statusCode = response.getStatusLine().getStatusCode();
                    HttpEntity entity = response.getEntity();
                    // 响应内容
                    content = EntityUtils.toString(entity, Consts.UTF_8);
                }
            } finally {
                response.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            httpClient.close();
        }
    }
    public int getStatusCode() {
        return statusCode;
    }
    public String getContent() throws ParseException, IOException {
        return content;
    }

}

4.生成微信支付二维码:
4.1接口编写:

package com.tan.user.service;

import java.util.Map;

/**
 * @author tanxiangwen
 * @date 2023/1/28
 * @描述
 */
public interface WeixinService {
    Map createNative(Long orderId) throws Exception;

}

WeixinServiceImpl:

public class WeixinServiceImpl implements WeixinService {
   @Autowired
    orderService orderService;
    @Autowired
    PaymentService paymentService;
    @Autowired
    RedisTemplate redisTemplate;
    @Autowired
    RefundInfoService refundInfoService;
    //生成微信支付二维码
    @Override
    public Map createNative(Long orderId) throws Exception {
        //先从redis中获取
        Map payMap = (Map) redisTemplate.opsForValue().get(orderId.toString());
        if(payMap!=null){
            return payMap;
        }
        //根据订单id获取订单信息
        OrderInfo orderInfo = orderService.getById(orderId);
        //向支付表中添加记录
        paymentService.savePaymentInfo(orderInfo, PaymentTypeEnum.WEIXIN.getStatus());
        //设置二维码参数
        //把参数转化为xml格式,用商户key进行加密,生成签名
        //把参数和签名放到map中返回
        Map paramMap = new HashMap<>();
        //设置参数(固定)
        //商户id
        paramMap.put("appid", WxUtil.APPID);
        //商户号
        paramMap.put("mch_id", WxUtil.PARTNER);
        //随机字符串(唯一)
        paramMap.put("nonce_str", WXPayUtil.generateNonceStr());
        //商品描述
        String body = orderInfo.getReserveDate() + "就诊"+ orderInfo.getDepname();
        paramMap.put("body", body);
        //商户订单号
        paramMap.put("out_trade_no", orderInfo.getOutTradeNo());
        //paramMap.put("total_fee", order.getAmount().multiply(new BigDecimal("100")).longValue()+"");
        //订单金额(0.01)
        paramMap.put("total_fee", "1");
        //当前IP
        paramMap.put("spbill_create_ip", "127.0.0.1");
        //回调地址
        paramMap.put("notify_url", "http://guli.shop/api/order/weixinPay/weixinNotify");
        //交易类型:    NATIVE--原生扫码支付
        paramMap.put("trade_type", "NATIVE");
        //调用微信接口,生成二维码
        HttpClient httpClient = new HttpClient("https://api.mch.weixin.qq.com/pay/unifiedorder");
        httpClient.setXmlParam(WXPayUtil.generateSignedXml(paramMap, WxUtil.PARTNERKEY));
        httpClient.setHttps(true);//使用https
        httpClient.post();
        //微信会返回数据,xml格式
        String content = httpClient.getContent();
        //xml转换为map
        Map<String, String> resultMap = WXPayUtil.xmlToMap(content);
        //4、封装返回结果集
        Map map = new HashMap<>();
        //订单号
        map.put("orderId", orderId);
        //总金额
        map.put("totalFee", orderInfo.getAmount());
        //状态码
        map.put("resultCode", resultMap.get("result_code"));
        //二维码地址
        map.put("codeUrl", resultMap.get("code_url"));
        //存储到redis中120分钟有效
        if(resultMap.get("result_code")!=null){
          redisTemplate.opsForValue().set(orderId.toString(),map,120, TimeUnit.MINUTES);
        }
        return map;
    }

注:其中的paymentInfo和orderInfo分别是支付表和订单表,根据自己的实际情况实现。
流程:
因为采用的是模式2开发,存在有效为两小时的问题,所以把信息存放到redis中,设置过期时间为两个小时。
第一步先从redis中取数据,看是否存在,如果存在就返回redis中存在的数据,不存在就生成,首先根据订单id查询出订单信息,然后保存自己的支付记录表。接下来就是生成一些二维码的参数,用map来存储,
代码里的参数都是固定的。
paramMap.put(“total_fee”, “1”)是订单金额,为了方便测试value为1就是1分钱,然后用HttpClient来调用微信接口。

httpClient.setXmlParam(WXPayUtil.generateSignedXml(paramMap, WxUtil.PARTNERKEY));
        httpClient.setHttps(true);//使用https
        httpClient.post();

因为调用微信支付接口参数需要xml格式,微信支付的WXPayUtil.generateSignedXml可以自动转成xml格式,并且用商户号进行加密,然后开启https请求,post方法。
请求后微信会给我们返回数据,数据同样是xml格式,我们需要转成map来进行操作:

 //微信会返回数据,xml格式
        String content = httpClient.getContent();
        //xml转换为map
        Map<String, String> resultMap = WXPayUtil.xmlToMap(content);
``
最后从`resultMap中获取自己想要的数据,再次用map封装起来给前端。

```java
//二维码地址
        map.put("codeUrl", resultMap.get("code_url"));

这个一定要传,用于前端生成二维码。最后存储到redis中设置两个小时有效。

Vue

<template>
  <!-- header -->
  <div class="nav-container page-component">
    <!--左侧导航 #start -->
    <div class="nav left-nav">
      <div class="nav-item ">
        <span class="v-link clickable dark" onclick="javascript:window.location='/user'">实名认证 </span>
      </div>
      <div class="nav-item selected">
        <span class="v-link selected dark" onclick="javascript:window.location='/order'"> 挂号订单 </span>
      </div>
      <div class="nav-item ">
        <span class="v-link clickable dark" onclick="javascript:window.location='/patient'"> 就诊人管理 </span>
      </div>
      <div class="nav-item ">
        <span class="v-link clickable dark"> 修改账号信息 </span>
      </div>
      <div class="nav-item ">
        <span class="v-link clickable dark"> 意见反馈 </span>
      </div>
    </div>
    <!-- 左侧导航 #end -->
    <!-- 右侧内容 #start -->
    <div class="page-container">
      <div class="order-detail">
        <div class="title"> 挂号详情</div>
        <div class="status-bar">
          <div class="left-wrapper">
            <div class="status-wrapper BOOKING_SUCCESS">
              <span class="iconfont"></span> {{ orderInfo.param.orderStatusString }}
            </div>
          </div>
          <div class="right-wrapper">
            <img src="@/assets/images/1314.png" class="code-img">
            <div class="content-wrapper">
              <div> 微信<span class="iconfont"></span>关注“综合HOSP助手”</div>
              <div class="watch-wrapper"> 获取最新健康咨询</div>
            </div>
          </div>
        </div>
        <div class="info-wrapper">
          <div class="title-wrapper">
            <div class="block"></div>
            <div>挂号信息</div>
          </div>
          <div class="info-form">
            <el-form ref="form" :model="form">
              <el-form-item label="就诊人信息:">
                <div class="content"><span>{{ orderInfo.patientName }}</span></div>
              </el-form-item>
              <el-form-item label="就诊日期:">
                <div class="content"><span>{{ orderInfo.reserveDate }} {{ orderInfo.reserveTime == 0 ? '上午' : '下午' }}</span></div>
              </el-form-item>
              <el-form-item label="就诊医院:">
                <div class="content"><span>{{ orderInfo.hosname }} </span></div>
              </el-form-item>
              <el-form-item label="就诊科室:">
                <div class="content"><span>{{ orderInfo.depname }} </span></div>
              </el-form-item>
              <el-form-item label="医生职称:">
                <div class="content"><span>{{ orderInfo.title }} </span></div>
              </el-form-item>
              <el-form-item label="医事服务费:">
                <div class="content">
                  <div class="fee">{{ orderInfo.amount }}</div>
                </div>
              </el-form-item>
              <el-form-item label="挂号单号:">
                <div class="content"><span>{{ orderInfo.outTradeNo }} </span></div>
              </el-form-item>
              <el-form-item label="挂号时间:">
                <div class="content"><span>{{ orderInfo.createTime }}</span></div>
              </el-form-item>
            </el-form>
          </div>
        </div>
        <div class="rule-wrapper mt40">
          <div class="rule-title"> 注意事项</div>
          <div>1、请确认就诊人信息是否准确,若填写错误将无法取号就诊,损失由本人承担;<br>
            <span style="color:red">2、【取号】就诊当天需在{{ orderInfo.fetchTime }}在医院取号,未取号视为爽约,该号不退不换;</span><br>
            3、【退号】在{{ orderInfo.quitTime }}前可在线退号 ,逾期将不可办理退号退费;<br>
            4、请于就诊当日,携带预约挂号所使用的有效身份证件到院取号;<br>
            5、请注意北京市医保患者在住院期间不能使用社保卡在门诊取号。
          </div>
        </div>
        <div class="bottom-wrapper mt60" v-if="orderInfo.orderStatus == 0 || orderInfo.orderStatus == 1">
          <div class="button-wrapper">
            <div class="v-button white" @click="cancelOrder()">取消预约</div>
          </div>
          <div class="button-wrapper ml20" v-if="orderInfo.orderStatus == 0">
            <div class="v-button" @click="pay()">支付</div>
          </div>
        </div>
      </div>
    </div>
    <!-- 右侧内容 #end -->
    <!-- 微信支付弹出框 -->
    <el-dialog :visible.sync="dialogPayVisible" style="text-align: left" :append-to-body="true" width="500px" @close="closeDialog">
      <div class="container">
        <div class="operate-view" style="height: 450px;">
          <div class="wrapper wechat">
            <div>

              <qriously :value="payObj.codeUrl" :size="200" style="padding-top: 200px"/>
              <div style="text-align: center;line-height: 25px;margin-bottom: 350px;">
                请使用微信扫一扫<br/>
                扫描二维码支付
              </div>
            </div>
          </div>
        </div>
      </div>
    </el-dialog>
  </div>
  <!-- footer -->
</template>
<script>
import '~/assets/css/hospital_personal.css'
import '~/assets/css/hospital.css'

import orderInfoApi from '@/api/orderInfo'
import weixinApi from '@/api/WeixinPay'
export default {
  data() {
    return {
      orderId: null,
      orderInfo: {
        param: {}
      },
      dialogPayVisible: false,
      payObj: {
        codeUrl: ''

      },
      timer: null  // 定时器名称
    }
  },
  created() {
    this.orderId = this.$route.query.orderId
    this.init()
  },
  methods: {
    init() {
      orderInfoApi.getOrders(this.orderId).then(response => {
        console.log(response.data);
        this.orderInfo = response.data
      })
    },
     pay() {
      this.dialogPayVisible = true
      weixinApi.createNative(this.orderId).then(response => {
        this.payObj = response.data
        if(this.payObj.codeUrl == '') {
          this.dialogPayVisible = false
          this.$message.error("支付错误")
        } else {
          this.timer = setInterval(() => {
            this.queryPayStatus(this.orderId)
          }, 3000);   //3000表每隔3秒
        }
      })
    },
    queryPayStatus(orderId) {
      weixinApi.queryPayStatus(orderId).then(response => {
        if (response.message == '支付中') {
          //继续查询
          return
        }
        // 支付成功,关闭定时器,刷新页面
        clearInterval(this.timer);
        window.location.reload()
      })
    },
    closeDialog() {
      if(this.timer) {
        clearInterval(this.timer);
      }
    },
    cancelOrder() {
      this.$confirm('确定取消预约吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => { // promise
                      // 点击确定,远程调用
        return orderInfoApi.cancelOrder(this.orderId)
      }).then((response) => {
        this.$message.success('取消成功')
        this.init()
      }).catch(() => {
        this.$message.info('已取消预约')
      })
    }

  }
}
</script>
<style>
.info-wrapper {
  padding-left: 0;
  padding-top: 0;
}
.content-wrapper {
  color: #333;
  font-size: 14px;
  padding-bottom: 0;
}
.bottom-wrapper {
  width: 100%;
}
.button-wrapper {
  margin: 0;
}
.el-form-item {
  margin-bottom: 5px;
}
.bottom-wrapper .button-wrapper {
  margin-top: 0;
}
</style>

主要看支付按钮,用户一点击就会调用pay方法,请求后端拿到微信二维码数据。二维码以弹出层的方式出现。

  <el-dialog :visible.sync="dialogPayVisible" style="text-align: left" :append-to-body="true" width="500px" @close="closeDialog">
      <div class="container">
        <div class="operate-view" style="height: 450px;">
          <div class="wrapper wechat">
            <div>

              <qriously :value="payObj.codeUrl" :size="200" style="padding-top: 200px"/>
              <div style="text-align: center;line-height: 25px;margin-bottom: 350px;">
                请使用微信扫一扫<br/>
                扫描二维码支付
              </div>
            </div>
          </div>
        </div>
      </div>
    </el-dialog>
 <qriously :value="payObj.codeUrl" :size="200" style="padding-top: 200px"/>
 

这里边会生成二维码。前端生成二维码的时候,使用了vue-qriously自行百度。关于支付成功后的逻辑可以根据自己的需求实现。我的逻辑就是用户在点击支付的同时会设置定时器调用另一个api来根据订单号查询这个订单的支付状态,如果为支付成功就清除定时器,关闭弹出层,刷新页面。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值