第十五章 SpringBoot+小程序微信支付的编程实践

亮子 2021-08-26 09:41:11 18277 0 0 0

1、注册公众号

https://mp.weixin.qq.com/

2、注册小程序

https://mp.weixin.qq.com/

3、申请微信支付

图片alt

4、设置证书和秘钥

微信证书

5、设置服务器域名

在小程序中的开发管理-开发设置-服务器域名

小程序设置服务器域名

6、服务端开发

1)、添加依赖

        <!-- 微信支付 -->
        <dependency>
            <groupId>com.github.wechatpay-apiv3</groupId>
            <artifactId>wechatpay-apache-httpclient</artifactId>
            <version>0.2.2</version>
        </dependency>

2)、配置文件

以下配置文件中的秘钥信息为模拟数据。

## weixin pay
# 商户号
weixin.merchantId=161000000
# 证书序列号
weixin.merchantSerialNumber=69AA2BEARNUV13B20A9CEBAAAAABBBBBCCCCCCDDDD
# 证书秘钥文件
weixin.privateKeyPath=/server/shenma/apiclient_key.pem
# v3密码
weixin.apiV3Key=ujn5kzjxjoe
# appid
weixin.appId=wx0999e67698231456
# 支付成功回调地址
weixin.notifyUrl=https://www.www.com/wxpay/orderPayNotify

3)、生成预订单函数

    public String prepayIdByOrderInfo(String orderId, String product, Long amount, String openId) throws IOException {

        //--1 读取证书
        PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
                new FileInputStream(privateKeyPath));

        //--2 构建请求
        AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
                new WechatPay2Credentials(merchantId, new PrivateKeySigner(merchantSerialNumber, merchantPrivateKey)),
                apiV3Key.getBytes("utf-8"));

        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                .withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey)
                .withValidator(new WechatPay2Validator(verifier));

        //--3 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
        HttpClient httpClient = builder.build();

        //--4 调用统一下单接口
        // https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi
        HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi");
        httpPost.addHeader("Accept", "application/json");
        httpPost.addHeader("Content-type","application/json; charset=utf-8");

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectMapper objectMapper = new ObjectMapper();

        ObjectNode rootNode = objectMapper.createObjectNode();
        rootNode.put("mchid",merchantId)
                .put("appid", appId)
                .put("description", product)
                .put("notify_url", notifyUrl)
                .put("out_trade_no", orderId);
        rootNode.putObject("amount")
                .put("total", amount);
        rootNode.putObject("payer")
                .put("openid", openId);

        objectMapper.writeValue(bos, rootNode);

        httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8"));
        CloseableHttpResponse response = (CloseableHttpResponse) httpClient.execute(httpPost);

        String bodyAsString = EntityUtils.toString(response.getEntity());
        System.out.println(bodyAsString);

        return bodyAsString;

    }

4)、签名生成函数

    public String getSignString(String prepayId) throws FileNotFoundException, NoSuchAlgorithmException, InvalidKeyException, JsonProcessingException, UnsupportedEncodingException, SignatureException {

        //--1 获取个人私钥
        PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
                new FileInputStream(privateKeyPath));

        //--2 组装支付签名消息
        //wx8888888888888888
        //1414561699
        //5K8264ILTKCH16CQ2502SI8ZNMTM67VS
        //prepay_id=wx201410272009395522657a690389285100
        String nonceStr = UUID.randomUUID().toString().replace("-", "");
        long timestamp = System.currentTimeMillis() / 1000;

        String message = appId;
        message += "\n";

        message += timestamp;
        message += "\n";

        message += nonceStr;
        message += "\n";

        message += "prepay_id=";
        message += prepayId;
        message += "\n";

        //--3 计算签名字符串
        Signature sign = Signature.getInstance("SHA256withRSA");
        sign.initSign(merchantPrivateKey);
        sign.update(message.getBytes("utf-8"));

        String paySign = Base64.getEncoder().encodeToString(sign.sign());

        //--4 生产json字符串
        HashMap<String, String> hashMap = new HashMap<>();
        hashMap.put("timeStamp", String.valueOf(timestamp));
        hashMap.put("nonceStr", nonceStr);
        hashMap.put("package", "prepay_id="+prepayId);
        hashMap.put("signType", "RSA");
        hashMap.put("paySign", paySign);

        ObjectMapper objectMapper = new ObjectMapper();
        String data = objectMapper.writeValueAsString(hashMap);

        //--5 返回json字符串
        return data;
    }

5)、支付成功回调函数

  • 回调信息处理函数

   @Override
    public ResponseResult orderPayNotify(WxSuccessCallBackObj body) {
        String associated_data =  body.getResource().getAssociated_data();
        String nonce = body.getResource().getNonce();
        String ciphertext = body.getResource().getCiphertext();
        WeiXinAesUtil aesUtil = new WeiXinAesUtil(weiXinPayUtil.getApiV3Key().getBytes());
        try {
            //--1 解密数据
            String json = aesUtil.decryptToString(associated_data.getBytes(), nonce.getBytes(), ciphertext);
            log.info(json);

            //--2 反序列化
            ObjectMapper objectMapper = new ObjectMapper();
            WxSuccessCallBackData wxSuccessCallBackData = objectMapper.readValue(json, WxSuccessCallBackData.class);

            log.info("trade_no="+wxSuccessCallBackData.getOut_trade_no());
            log.info("transaction_id="+wxSuccessCallBackData.getTransaction_id());
            log.info("trade_state="+wxSuccessCallBackData.getTrade_state());
            log.info("appid="+wxSuccessCallBackData.getAppid());

            //--3 修改订单状态
            if(!wxSuccessCallBackData.getTrade_state().equals("SUCCESS")) {
                return ResponseResult.FAIL("订单失败");
            }
            TbOrder tbOrder = tbOrderMapper.selectById(wxSuccessCallBackData.getOut_trade_no());
            if(tbOrder == null) {
                return ResponseResult.FAIL("订单不存在");
            }
            if(!tbOrder.getOrderStatus().equals(OrderStatusEnum.NOPAY.getCode())) {
                return ResponseResult.FAIL("订单状态错误");
            }

            tbOrder.setOrderStatus(OrderStatusEnum.PAYED.getCode());
            tbOrderMapper.updateById(tbOrder);

            //--4 修改用户积分
            TbUser tbUser = tbUserMapper.selectById(tbOrder.getUserId());
            Long score = tbOrder.getAmount()*100;
            Long oldScore = tbUser.getUserScore();
            Long newScore = oldScore + score;

            tbUser.setUserScore(newScore);
            tbUserMapper.updateById(tbUser);

            //--5 记录积分变化
            IdWorker idWorker = new IdWorker();
            TbUserScoreRecord record = new TbUserScoreRecord();
            record.setRecordId(idWorker.nextId());
            record.setUserId(tbUser.getUserId());
            record.setChangeScore(score);
            record.setOldScore(oldScore);
            record.setNewScore(newScore);
            record.setChangeReason("充值");

            tbUserScoreRecordMapper.insert(record);

            return ResponseResult.SUCCESS();
        } catch (GeneralSecurityException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return ResponseResult.FAIL("未知错误");
    }
  • 回调函数的参数接收实体类

@Data
public class WxSuccessCallBackObj implements Serializable {
    private static final long serialVersionUID = 3099698871017317731L;

    private String summary;
    private String event_type;
    private String create_time;
    private String resource_type;
    private String id;
    private WxResource resource;

}
  • 回调数据解密后的实体类

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class WxSuccessCallBackData {
    private String transaction_id;
    private String mchid;
    private String trade_state;
    private String bank_type;
    private String success_time;
    private String out_trade_no;
    private String appid;
    private String trade_state_desc;
    private String trade_type;
}
  • 回调数据解密的工具类

package com.shenmazong.shenmacodeserver.util;

import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

/**
 * @author 军哥
 * @version 1.0
 * @description: 微信支付回调函数解密
 * @date 2021/8/25 12:15
 */

public class WeiXinAesUtil {
    static final int KEY_LENGTH_BYTE = 32;
    static final int TAG_LENGTH_BIT = 128;
    private final byte[] aesKey;

    public WeiXinAesUtil(byte[] key) {
        if (key.length != KEY_LENGTH_BYTE) {
            throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节");
        }
        this.aesKey = key;
    }

    public String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext) throws GeneralSecurityException, IOException {
        try {
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");

            SecretKeySpec key = new SecretKeySpec(aesKey, "AES");
            GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);

            cipher.init(Cipher.DECRYPT_MODE, key, spec);
            cipher.updateAAD(associatedData);

            return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), "utf-8");
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
            throw new IllegalStateException(e);
        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
            throw new IllegalArgumentException(e);
        }
    }
}

7、小程序端开发

  • 绑定支付按钮事件

    <button type="primary" bindtap="handleGetPrepayId">我要充值</button>
  • 定义支付事件
handleGetPrepayId(e) {
    let that = this
    let userId = wx.getStorageSync('userId')
    console.log(userId)
    wx.request({
      url: 'https://www.xxx.com/wx/getOrderPrepayId',
      method: 'POST',
      data: {
        userId: userId,
        productId: 1
      },
      success (res) {
        console.log(res.data)
        if(res.data.code == 0) {
          let prepay = JSON.parse(res.data.data)
          console.log(prepay.prepay_id)
          that.getPayOrderSign(prepay.prepay_id)
        }
        else {
          console.log(res.message)
        }
      }
    })
  }
  • 定义支付请求签名

getPayOrderSign(prepayId) {
    let that = this
    let userId = wx.getStorageSync('userId')
    console.log(userId)
    wx.request({
      url: 'https://www.xxx.com/wx/getOrderRequestSign',
      method: 'POST',
      data: {
        prepayId: prepayId
      },
      success (res) {
        console.log(res.data)
        if(res.data.code == 0) {
          let param = JSON.parse(res.data.data)
          wx.requestPayment(
            {
            "timeStamp": param.timeStamp,
            "nonceStr": param.nonceStr,
            "package": param.package,
            "signType": "RSA",
            "paySign": param.paySign,
            "success":function(res){
              console.log(res)
            },
            "fail":function(res){
              console.log(res)
            },
            "complete":function(res){
              console.log(res)
            }
            })
        }
        else {
          console.log(res.message)
        }
      }
    })