后端java开发,前端vue3,完成微信jsapi支付,使用官方SDK,简单明了,直接可用

需求要求:用户点开链接直接进行付款,原来打算使用微信H5进行支付,但是审核太麻烦,审核和多次都没通过,并且H5支付只能通过浏览器打开链接,然后唤醒微信进行支付,如果在微信中点开链接是无法支付的。然后采用微信jsapi方案进行支付。支付前需要申请公众号,支付商户号,商户号和公众号进行绑定。支付整体逻辑,用户点开链接,静默获取用户openId(生成支付订单的时候会用到),生成预支付订单,获取到预支付订单的参数后,前端换起微信支付。

准备阶段,我就直接使用另外一名博主的截图,自己难得取图了,具体代码和这位博主有些区别,因为我是直接使用微信官方的sdk。在实际开发中也遇到一些坑,后面会描述。微信公众号-JSAPI支付(保姆级教程)_微信jsapi支付-CSDN博客

微信公众号-JSAPI支付

1、微信公众号配置

https://mp.weixin.qq.com/cgi-bin/home
① 开通网页授权域名(前置条件:需开通网页授权接口权限)
在这里插入图片描述

②暴露网络授权域名
在这里插入图片描述
③获取基本配置中的AppID、AppSecret
在这里插入图片描述

④微信支付关联商户号
在这里插入图片描述

2、微信支付配置

https://pay.weixin.qq.com/
① 配置API证书 获取证书序列号
在这里插入图片描述
②获取正式序列号
在这里插入图片描述
在这里插入图片描述

③设置APIV3秘钥
在这里插入图片描述
④ 产品中心—开通JSAPI支付

在这里插入图片描述
⑤添加支付域名配置
在这里插入图片描述
⑥暴露域名
在这里插入图片描述
⑦AppId账号设置-商户关联公众号
在这里插入图片描述

程序配置:

①配置yml文件

wx:
  # 公众号appid
  appId:xxxxxd5381a6
  # 公众号 appSecret
  appSecret: ccccccaff013ebe0eff1
  # 商户号
  mchId: 123456
  # 商户APIV3密钥
  apiV3Key: ccccccccJCcMw466spcSAyV
  # 商户证书序列号
  merchantSerialNumber: cccccccF237C29CC
  # 微信回调地址
  v3PayNotifyUrl: https://xxxx.cn/orderPay/payNotify

② 微信支付平台下载的秘钥证书放入项目resources目录下

pom文件需要引入微信sdk

        <!--微信支付-->
        <dependency>
            <groupId>com.github.wechatpay-apiv3</groupId>
            <artifactId>wechatpay-java</artifactId>
            <version>0.2.12</version>
        </dependency>

在生产预支付订单的解密密钥时候我遇到一个很奇怪的报错

经过各方资料查询需要引入这个版本的包确实解决了
 

        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib</artifactId>
            <version>1.3.50</version>
        </dependency>

主要流程及代码实现
①前端通过url获取code
(url参数配置)
Appid:公众号的appid
Redirect_uri:重定向至微信支付页面,携带用户openId
Scope:snsapi_base(授权方式 无需弹窗静默授权)
https://open.weixin.qq.com/connect/oauth2/authorize?appid=xx&redirect_uri=xxx&response_type=code&scope=snsapi_base&state=STATE&connect_redirect=1#wechat_redirect

②通过前端获取的code,传入指定支付接口(Redirect_uri),获取openId
1)通过appID、appSecret、code构建请求URL,解析返回结果获取openId

可参考微信官方文档:网页授权 | 微信开放文档
————————————————

       

    /** 前端域名地址*/
    @Value("${ccsMobileUrl}")
    private String ccsMobileUrl;

/**
     * 通过code获取用户openId,并重定向到支付页面
     * @param request
     * @return
     * @throws Exception
     */
    @GetMapping("/tpPayOrderPage")
    public ResponseEntity<String> tpPayOrderPage(HttpServletRequest request) throws Exception {
        String cardBusinessNo = request.getParameter("cardBusinessNo");
        String code = request.getParameter("code");
        String openId = getOpenId(code);
        String redirectUrl = ccsMobileUrl+"#/payOrder?cardBusinessNo="+cardBusinessNo+"&openId="+openId;
        return ResponseEntity.status(HttpStatus.FOUND).header("Location", redirectUrl).build();
    }

重定向到前端的地址为:https://ccc.cn/mobile/#/payOrder?cardBusinessNo=CBABCX2407220002&openId=12345t

跳转到前端页面后,准备进行支付

整个前端代码 先贴出来

<template>
  <div class="root-layout">
    <div class="title-1">尊敬的客户:您的订单已生成,请确认订单信息</div>
    <div class="title-line"></div>
    <div class="item-title">商品信息</div>
    <div class="item-line" v-if="form.lineList && form.lineList.length>0" :key="key"  v-for="(item, key) in form.lineList">
      <van-image class="item_img"   :src="item.picUrl" />
      <div class="item-content">
        <span class="item-name">{{item.itemName}}</span>
        <span>数量 {{item.quantity}}张</span>
        <span>单价 ¥{{item.unitPrice}}</span>
      </div>
    </div>
    <div v-else>
      <van-empty description="没有行信息" />
    </div>
    <div class="title-line"></div>
    <div class="title-2">
      <div class="price">合计:2839元</div>
      <template>  
    <button class="submit" :class="{ disabled: isPaid }" @click="onClickVersion" :disabled="isPaid">  
      {{ isPaid ? '已支付' : '确认付款' }}  
    </button>  
</template>
    </div>
  </div>

</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import { CellGroup, Field, Image as VanImage } from 'vant'
import { Toast } from 'vant'

declare global {
  interface Window {
    WeixinJSBridge: any;
  }
}
@Component({
  name: 'AboutUs',
  components: {
    [Field.name]: Field,
    [CellGroup.name]: CellGroup,
    [VanImage.name]: VanImage,
  },
})
export default class PayOrder extends Vue {
  form: any = {};
  isPaid = true; // 用于跟踪支付状态  
  mounted() {
    document.title = '订单支付';
    this.queryCardBusinessInfoHandler();
  }
  private onClickVersion() {
    const weixinJSBridge = window.WeixinJSBridge;
    if(typeof weixinJSBridge == "undefined") {
      Toast('请使用微信浏览器打开页面')
    }else{
    // 调用接口生成预支付订单
    this.producePrepayOrder();
    }
  }
  //查询订单详情
  queryCardBusinessInfoHandler() {
    // {
    //   //cardBusinessNo: this.$route.query.cardBusinessNo,
    //   cardBusinessNo: 'CBABCX2407220002',
    // }
    Http.queryCardBusinessInfo<any, any>(this.$route.query.cardBusinessNo).then((res) => {
      if(res.code=== 500) {
        this.$toast(res.chnDesc);
        return
      }
      this.form = res || {};
      if(res.paymentStatus == "1"){
        this.isPaid = false
      }else{
        this.isPaid = true
      }
      console.log('this.form', this.form)
    });
  }
  //查询订单详情
  producePrepayOrder() {
    const weixinJSBridge = window.WeixinJSBridge;
    Http.producePrepayOrder({
      cardBusinessNo: this.$route.query.cardBusinessNo,
      openId: this.$route.query.openId
        }).then((res: any) => {
          if(res.code=== 500) {
        this.$toast(res.chnDesc);
        return
      }
      console.log('this.res', res)
      weixinJSBridge.invoke(
        'getBrandWCPayRequest', {
          "appId": res.appId, // 公众号名称,由商户传入     
          "timeStamp": res.timeStamp, // 时间戳,自1970年以来的秒数     
          "nonceStr": res.nonceStr, // 随机串     
          "package": res.packageVal,
          "signType": res.signType, // 微信签名方式     
          "paySign": res.paySign // 微信签名 
        },
        (res: { err_msg: string }) => { // 指定 res 的类型
          // 支付成功
          if (res.err_msg === "get_brand_wcpay_request:ok") {
            // 支付成功逻辑
            Toast('支付成功')
          }
          // 支付过程中用户取消
          else if (res.err_msg === "get_brand_wcpay_request:cancel") {
            // 取消支付逻辑
            Toast('取消支付')
          }
          // 支付失败
          else if (res.err_msg === "get_brand_wcpay_request:fail") {
            // 失败逻辑
            Toast('支付失败')
          }
          // 其它错误处理
          else if (res.err_msg === "调用支付JSAPI缺少参数:total_fee") {
            // 错误处理逻辑
            Toast('支付错误')
          }
          location.reload();
        }
      );
        });
  }
}
</script>

<style lang="less" scoped>
.item-line{
  margin-top: 5px;
  padding: 0,10px;
  display: flex;
  justify-content: space-between;
  .item_img{
    width: 120px;
  }
  .item-content{
    margin-left: 10px;
    padding: 5px;
    display: flex;
    justify-content: space-evenly;
    flex-direction: column;
    line-height: 30px;
    flex: 1;
    .item-name{
      white-space: nowrap;      /* 确保文本在一行内显示 */
      overflow: hidden;         /* 隐藏超出容器的部分文本 */
      text-overflow: ellipsis;  /* 超出部分显示为省略号 */
      width: 200px;
    }
  }

}
.root-layout{
  padding: 20px 10px
}
.title-1{
  border: 2px solid #CECECE;
  width: 99%;
  height: 100px;
  text-align: center;
  line-height: 6;
}
.title-line{
  border: 2px solid #CECECE;
  height: 22px;
  background: #CECECE;
}
.item-title{
  margin-top: 6%;
  margin-left: 6%;
}
.title-2 {
  height: 60px;
  display: flex; /* 启用Flexbox布局 */
  justify-content: space-between; /* 子元素分布在容器的两端 */
  align-items: center; /* 子元素在交叉轴上的对齐方式,这里为居中 */
  margin-top: 12%;
}

.price {
  /* 移除高度和行高设置,因为Flexbox会自动处理这些 */
  margin-left: 5%; /* 如果需要的话,可以保留 */
  /* 如果你想让价格文本垂直居中,可以不需要设置line-height,因为align-items已经处理了 */
}

.submit {
  text-align: right; /* 文本靠右对齐 */
  padding: 0 2.66667vw; /* 使用视口宽度单位设置内边距 */
  background-color: #E96C2D; /* 背景色 */
  border: 1px solid #ccc; /* 边框色,但通常你会想要与背景色形成对比的边框色 */
  cursor: pointer; /* 鼠标悬停时显示指针样式 */
  height: 100%; /* 占据父容器的全部高度 */
  width: 23%; /* 占据父容器的23%宽度 */
  color: white; /* 文本颜色为白色 */
  display: flex; /* 启用Flexbox布局 */
  align-items: center; /* 使子项(文本)在垂直方向上居中 */
  justify-content: flex-end; /* 如果你还想要文本在水平方向上靠右(尽管text-align: right;已经做了这件事,但在这里是冗余的) */
  border-radius: 10px;

}
.submit.disabled {  
  background-color: #ccc;  
  cursor: not-allowed;  
} 


</style>

1 先queryCardBusinessInfoHandler()查询订单详情,展示订单基本信息

2 点击确认付款按钮进入onClickVersion()准备支付,jsapi只能在微信浏览器中进行支付,所以这里判断了是否是在微信浏览器中操作。

3 方法producePrepayOrder() 生成预支付订单,调用了后端orderPay/prepay 的接口,后端方法后面会贴出来

4 生成预支付订单后,前端weixinJSBridge.invoke换起 微信支付。这里前端的操作基本上就到此结束。

前端换起微信支付可参考微信官方文档:开发指引 - JSAPI支付 | 微信支付服务商文档中心

后端准备开始生成预支付订单

controller

    @PostMapping("/prepay")
    public BaseResponse<OrderWechatPayResponseDTO> wehchatPay(@RequestBody WechatPayReqDTO request) {
        return orderWechatPayFacade.wehchatPay(request);
    }

/** 商户号*/
    @Value("${wx.mchId}")
    private String mchId;

    /** 公众号appid*/
    @Value("${wx.appId}")
    private String appId;

    /** 公众号appSecret*/
    @Value("${wx.appSecret}")
    private String appSecret;

    /** 商户APIV3密钥*/
    @Value("${wx.apiV3Key}")
    private String apiV3Key;

    /** 商户证书序列号 */
    @Value("${wx.merchantSerialNumber}")
    private String merchantSerialNumber;

    /**微信回调地址*/
    @Value("${wx.v3PayNotifyUrl}")
    private String v3PayNotifyUrl;
/**
     * 微信支付生成预支付订单
     * @param reqDTO
     * @return
     */
    public OrderWechatPayResponseDTO wehchatPay(WechatPayReqDTO reqDTO) {
        OrderWechatPayResponseDTO result = new OrderWechatPayResponseDTO();
        OrderCardBusinessHeadResponseDTO orderWechatPayRespDTO = new OrderCardBusinessHeadResponseDTO();
        log.info("wehchatPay-reqDTO===={}",JSON.toJSONString(reqDTO));
        // 支付前校验参数
        checkParms(reqDTO,orderWechatPayRespDTO);

        // 获取apiclient_key.pem文件
        File keyFile = getKeyFile();

        // 订单号
        //元转换为分
        // 一个商户号只能初始化一个配置,否则会因为重复的下载任务报错
        if (config == null) {
            config =new RSAAutoCertificateConfig.Builder()
                    .merchantId(mchId)
                    .privateKeyFromPath(keyFile.getPath())
                    .merchantSerialNumber(merchantSerialNumber)
                    .apiV3Key(apiV3Key)
                    .build();
        }
        // 构建service
        if (service == null) {
            service = new JsapiServiceExtension.Builder().config(config).build();
        }

        //组装预约支付的实体
        // request.setXxx(val)设置所需参数,具体参数可见Request定义
        PrepayRequest request = new PrepayRequest();
        //计算金额
        Amount amount = new Amount();
        // 金额单位为分 需要乘以100

        amount.setTotal(orderWechatPayRespDTO.getOrderTotalPrice().multiply(new BigDecimal("100")).intValue());
        amount.setCurrency("CNY");
        request.setAmount(amount);
        //公众号appId
        request.setAppid(appId);
        //商户号
        request.setMchid(mchId);
        //支付者信息
        Payer payer = new Payer();
        payer.setOpenid(reqDTO.getOpenId());
        request.setPayer(payer);
        //描述
        request.setDescription("支付测试");
        //微信回调地址,需要是https://开头的,必须外网可以正常访问
        //本地测试可以使用内网穿透工具,网上很多的
        request.setNotifyUrl(v3PayNotifyUrl);
        //订单号
        request.setOutTradeNo(orderWechatPayRespDTO.getCardBusinessNo());
        PrepayWithRequestPaymentResponse payment = null;
        String errorMsg = "";
        // 加密
        try {
            payment = service.prepayWithRequestPayment(request);
            log.info("支付成功:{}", JSON.toJSONString(payment));
        }catch (Exception e){
            log.error("支付失败:{}",e.getMessage());
            errorMsg = e.getMessage();
        }finally {
            // 支付前将记录保存到支付日志表中
            saveWechatPayLog(payment,request,errorMsg);
        }
        //默认加密类型为RSA
        payment.setSignType("MD5");
        //返回数据,前端调起支付
        BeanUtils.copyProperties(payment,result);
        return result;
    }

使用官方sdk方法构建config的时候 这里在获取apiclient_key.pem文件 一直遇到一个问题,本地调试可以读取到密钥内容,但是打包后发版到服务器就获取不到了,通过资料查询,打包后的密钥文件打包到class文件下使用FileInputStream读取不到文件。

所以这里单独写了一个读取密钥文件的方法,如果没有读取到就创建文件并保存到服务器指定目录

/**
     * 读取密钥文件
     * @return
     */
    public File getKeyFile() {
        File keyFile = new File(TEMP_DIR, KEY_FILE_NAME);

        // 检查文件是否已存在
        if (keyFile.exists()) {
            return keyFile;
        }

        // 如果不存在,创建并保存文件
        ClassPathResource keyClassPath = new ClassPathResource("key/apiclient_key.pem");
        try (InputStream inputStream = keyClassPath.getInputStream();
             FileOutputStream outputStream = new FileOutputStream(keyFile)) {

            byte[] buffer = new byte[1024];
            int length;
            while ((length = inputStream.read(buffer)) > 0) {
                outputStream.write(buffer, 0, length);
            }
            // 返回新生成的文件
            return keyFile;

        } catch (IOException e) {
            throw new RuntimeException("Unable to load key file", e);
        }
    }

在我的代码中,有一些对订单是否能够完成支付的校验,这个方法看自己是否需要checkParms(reqDTO,orderWechatPayRespDTO);

生产预支付订单方法wehchatPay(),我这里返回的实体类是OrderWechatPayResponseDTO,你可以直接返回微信的实体类 PrepayWithRequestPaymentResponse 是一样的

前端获取到预支付订单,换起微信支付,支付成功后,微信会调用我们回调接口,通知支付成功,这里把后端回调的方法贴出来

controller

@ApiOperation(value = "支付回调")
    @PostMapping("/payNotify")
    public synchronized void payNotify(HttpServletRequest request){
        WechatPayNotifyReqDTO reqDTO = new WechatPayNotifyReqDTO();
        log.info("------收到支付通知controller------");
        try (ServletInputStream inputStream = request.getInputStream();
             BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"))) {
            // 读取请求体
            StringBuilder sb = new StringBuilder();
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                sb.append(line);
            }
            String notify = sb.toString();
            log.info("支付通知内容:" + notify);

            // 请求头处理...
            reqDTO.setSignature(request.getHeader("Wechatpay-Signature"));
            reqDTO.setNonce(request.getHeader("Wechatpay-Nonce"));
            reqDTO.setTimestamp(request.getHeader("Wechatpay-Timestamp"));
            reqDTO.setSerial(request.getHeader("Wechatpay-Serial"));
            reqDTO.setSignType(request.getHeader("Wechatpay-Signature-Type"));
            reqDTO.setNotify(notify);

            log.info("reqDTO====={}", JSON.toJSONString(reqDTO));
            orderWechatPayFacade.payNotify(reqDTO);
        } catch (Exception e) {
            log.error("解析付款通知出错:{}", e.getMessage(), e);
        }

    }

service

/**
     * 支付回调
     * @param request
     */
    public void payNotify(WechatPayNotifyReqDTO request) {
        log.info("------收到支付通知------");
            File keyFile = getKeyFile();


            // 一个商户号只能初始化一个配置,否则会因为重复的下载任务报错
            if (config == null) {
                config =new RSAAutoCertificateConfig.Builder()
                        .merchantId(mchId)
                        .privateKeyFromPath(keyFile.getPath())
                        .merchantSerialNumber(merchantSerialNumber)
                        .apiV3Key(apiV3Key)
                        .build();
            }

            // 请求头Wechatpay-Signature
            String signature = request.getSignature();
            // 请求头Wechatpay-nonce
            String nonce = request.getNonce();
            // 请求头Wechatpay-Timestamp
            String timestamp = request.getTimestamp();
            // 微信支付证书序列号
            String serial = request.getSerial();
            // 签名方式
            String signType = request.getSignType();
            // 构造 RequestParam
            com.wechat.pay.java.core.notification.RequestParam requestParam = new com.wechat.pay.java.core.notification.RequestParam.Builder()
                    .serialNumber(serial)
                    .nonce(nonce)
                    .signature(signature)
                    .timestamp(timestamp)
                    .signType(signType)
                    .body(request.getNotify())
                    .build();
            // 初始化 NotificationParser
            NotificationParser parser = new NotificationParser(config);
            // 以支付通知回调为例,验签、解密并转换成 Transaction
            log.info("验签参数:{}", requestParam);
            Transaction transaction = parser.parse(requestParam, Transaction.class);
            log.info("验签成功!-支付回调结果:{}", transaction.toString());
 

            Map<String, String> returnMap = new HashMap<>(2);
            returnMap.put("code", "FAIL");
            returnMap.put("message", "失败");
            if (Transaction.TradeStateEnum.SUCCESS != transaction.getTradeState()) {
                log.info("内部订单号【{}】,微信支付订单号【{}】支付未成功", 

            }

    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值