·微信支付介绍
·微信支付流程
首先第一步微信用户需要进入我们的小程序,点商品下单,下单的时候小程序就会发送一个请求来调用我们后端服务的某个接口,我们前面已经实现了用户下单这个接口,所以就会调用用户下单这个接口。调用完用户下单这个接口后,会给小程序返回一些数据,其中就包括订单号、订单id、下单的时间、订单总金额,有了这些数据小程序就可以申请微信支付,小程序会发起一次请求,请求我们的后端服务,后端服务就会去调用微信后台的一个下单接口。
下单接口的参数:
mchid:商户号。
out_trade_no:订单号,一般使用我们业务系统里面的这个订单号,我们这个项目使用系统的时间戳作为订单号。
appid:应用的id,要实现这个功能需要让这个appid跟商户号进行绑定,在商户平台去完成绑定,绑定之后就可以使用这个appid。
description:其实就是一个描述。
notify_url:回调地址,当用户通过微信支付付款成功之后,微信后台会调用这个地址,通过这个地址来通知我们的程序,这个地址一般是我们商户系统的一个访问地址。
amount:代表的是金额
total:代表的是具体的金额这个数字。
currency:代表的是币种,比如说人民币或者美元等等这些。
payer:代表支付者。
openid:代表我们当前付款的那个微信用户他的openid,用户进行微信登录的时候就获取到了用户的openid。
由openid这个微信用户去进行付款,付款金额由total决定,付给商户,通过商户号就可以唯一锁定这个商户,然后这个钱就会付到对应的商户的银行账户里面去。
我们调用这个接口其实是生成了预支付交易单,并没有真的去完成支付,就是我们提前通知微信,我们希望有这么一份交易,先记录一下,一会儿微信用户会通过小程序来完成这份交易,这个就叫做预支付交易单。相当于在微信这边进行一个备案,然后我们调用完这个接口后,微信会给我们返回一个字符串叫做预支付交易标识,我们拿到这个字符串之后需要进行一系列的数据处理,把这些数据进行处理并且签名,签名是为了安全,因为我们的数据在网络上传输有可能会被别人截获,所以我们会对这些数据进行签名,保证我们的数据的安全。这些数据准备好之后,我们后端系统就会把这些数据给它响应到小程序这一端,因为是小程序先请求的商户系统,然后我们进行一系列的处理之后,会给它封装这些参数,返回给小程序。
返回给小程序之后用户需要来确认支付,这个时候它就会调用wx.requestPayment这个方法,它是小程序端的一个方法,只要调这个方法用户手机上就会出现确认支付的页面,这其实就是调起了微信支付。wx.requestPayment方法的参数其实就是将组合数据再次签名这里来的,把这些数据进行处理最终返回给小程序,小程序直接使用这些数据,这些数据其实全都是后端返回给小程序的,小程序把它作为参数来调用这个方法。用户点击确认支付输入密码完成点击确定之后,它其实就会发起一个请求,请求微信后台,微信后台就会进行真正的付款操作。付款成功之后,就会给小程序返回支付结果,那么在小程序这一端会显示支付结果,到这实际上用户已经完成了付款,这个时候就已经把钱打到了我们这个商户那个银行卡里面去了。我们前面是通过微信小程序支付,直接跟微信后台来进行交互,来完成付款,这个过程我们的商户系统并不知道。至于刚才的微信用户有没有真的付款,付款成功了还是失败了,商户系统根本就不知道。所以在完成真正付款之后还有两个流程,就是微信后台会给商户系统也就是我们的后台服务推送支付结果。微信后台会调用notify_url这个回调地址也就是商户系统的服务地址,这样就会调用到我们的商户系统,我们的商户系统接收到通知之后,就可以去更新我们这个订单的状态。
·微信支付相关接口
·微信支付准备工作
1.完成微信支付其中有一个关键的步骤,就是需要在我们这个商户系统当中,来调用微信后台的一个下单接口,就是生成我们这个预支付交易单。由于这个接口是跟我们支付相关的,所以这个接口的安全性要求是非常高的,那我们如何来保证我们调用过程当中,这个数据的安全性。不仅仅是这一次接口调用,支付成功之后微信后台会给我们推送消息,也就是说微信后台会调用我们这个商户系统,这个交互过程当中也会涉及到一些数据的交互,数据的传输,那这个数据如何保证安全。我们需要来解决这个问题,其实微信给我们提供的方式就是对数据进行加密、解密还有签名多种方式。要完成数据加密解密,就需要准备相应的一些文件,其实就是一些证书。
这两个文件是从商户平台下载下来的。
2.微信后台会调用我们的商户系统给我们推送支付的结果,那这个地方我们就会遇到一个问题,就是微信后台它怎么就能调用到我们的商户系统,它这个调用过程本质上也是一个Http请求,我们当前商户系统的ip地址就是自己的笔记本的ip地址,而我们自己的笔记本ip地址只是一个局域网内的一个ip地址,微信后台其实是调用不到的。如果微信后台想要调用我们的商户系统,就需要我们当前这个笔记本能够获取一个公网的ip。我们可以通过内网穿透,也就是我们可以获得一个临时的ip地址,而这个ip地址是一个公网的ip,这样的话微信后台就可以请求到我们当前的这个商户系统了。
它就会给我们生成一个文件,这个文件就是我们当前这个内网穿透工具它的一个配置文件,我们只需要执行一次就可以了。
·获取临时域名
获取成功
这个域名它其实就映射到了我们本地的8080端口,现在我们通过这个域名就可以访问到我们当前这台笔记本。
右键点标记要复制的内容再点右键就可以复制上了。
成功访问到
·导入订单支付功能代码
注意:我们每次使用内网穿透工具获取到的临时域名是不一样的,我们要使用最新的临时域名。
微信后台调用这个地址,最终请求到我们相应的服务。
这是我们一会儿要导入的某个control它的请求路径,也就是微信后台通过请求这个地址,最终就能请求到我们自己的某个control的方法,所以说这块对应的是我们control的这个请求路径。
导入微信支付功能代码,这块代码比较固定,所以不用手敲了。
/**
* 订单支付
*
* @param ordersPaymentDTO
* @return
*/
public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception {
// 当前登录用户id
Long userId = BaseContext.getCurrentId();
User user = userMapper.getById(userId);
//调用微信支付接口,生成预支付交易单
JSONObject jsonObject = weChatPayUtil.pay(
ordersPaymentDTO.getOrderNumber(), //商户订单号
new BigDecimal(0.01), //支付金额,单位 元
"苍穹外卖订单", //商品描述
user.getOpenid() //微信用户的openid
);
if (jsonObject.getString("code") != null && jsonObject.getString("code").equals("ORDERPAID")) {
throw new OrderBusinessException("该订单已支付");
}
OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class);
vo.setPackageStr(jsonObject.getString("package"));
return vo;
}
/**
* 支付成功,修改订单状态
*
* @param outTradeNo
*/
public void paySuccess(String outTradeNo) {
// 根据订单号查询订单
Orders ordersDB = orderMapper.getByNumber(outTradeNo);
// 根据订单id更新订单的状态、支付方式、支付状态、结账时间
Orders orders = Orders.builder()
.id(ordersDB.getId())
.status(Orders.TO_BE_CONFIRMED)
.payStatus(Orders.PAID)
.checkoutTime(LocalDateTime.now())
.build();
orderMapper.update(orders);
}
package com.sky.utils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.sky.properties.WeChatProperties;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.http.HttpHeaders;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.math.BigDecimal;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
/**
* 微信支付工具类
*/
@Component
public class WeChatPayUtil {
//微信支付下单接口地址
public static final String JSAPI = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";
//申请退款接口地址
public static final String REFUNDS = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds";
@Autowired
private WeChatProperties weChatProperties;
/**
* 获取调用微信接口的客户端工具对象
*
* @return
*/
private CloseableHttpClient getClient() {
PrivateKey merchantPrivateKey = null;
try {
//merchantPrivateKey商户API私钥,如何加载商户API私钥请看常见问题
merchantPrivateKey = PemUtil.loadPrivateKey(new FileInputStream(new File(weChatProperties.getPrivateKeyFilePath())));
//加载平台证书文件
X509Certificate x509Certificate = PemUtil.loadCertificate(new FileInputStream(new File(weChatProperties.getWeChatPayCertFilePath())));
//wechatPayCertificates微信支付平台证书列表。你也可以使用后面章节提到的“定时更新平台证书功能”,而不需要关心平台证书的来龙去脉
List<X509Certificate> wechatPayCertificates = Arrays.asList(x509Certificate);
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(weChatProperties.getMchid(), weChatProperties.getMchSerialNo(), merchantPrivateKey)
.withWechatPay(wechatPayCertificates);
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签
CloseableHttpClient httpClient = builder.build();
return httpClient;
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
}
}
/**
* 发送post方式请求
*
* @param url
* @param body
* @return
*/
private String post(String url, String body) throws Exception {
CloseableHttpClient httpClient = getClient();
HttpPost httpPost = new HttpPost(url);
httpPost.addHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.toString());
httpPost.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString());
httpPost.addHeader("Wechatpay-Serial", weChatProperties.getMchSerialNo());
httpPost.setEntity(new StringEntity(body, "UTF-8"));
CloseableHttpResponse response = httpClient.execute(httpPost);
try {
String bodyAsString = EntityUtils.toString(response.getEntity());
return bodyAsString;
} finally {
httpClient.close();
response.close();
}
}
/**
* 发送get方式请求
*
* @param url
* @return
*/
private String get(String url) throws Exception {
CloseableHttpClient httpClient = getClient();
HttpGet httpGet = new HttpGet(url);
httpGet.addHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.toString());
httpGet.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString());
httpGet.addHeader("Wechatpay-Serial", weChatProperties.getMchSerialNo());
CloseableHttpResponse response = httpClient.execute(httpGet);
try {
String bodyAsString = EntityUtils.toString(response.getEntity());
return bodyAsString;
} finally {
httpClient.close();
response.close();
}
}
/**
* jsapi下单
*
* @param orderNum 商户订单号
* @param total 总金额
* @param description 商品描述
* @param openid 微信用户的openid
* @return
*/
private String jsapi(String orderNum, BigDecimal total, String description, String openid) throws Exception {
JSONObject jsonObject = new JSONObject();
jsonObject.put("appid", weChatProperties.getAppid());
jsonObject.put("mchid", weChatProperties.getMchid());
jsonObject.put("description", description);
jsonObject.put("out_trade_no", orderNum);
jsonObject.put("notify_url", weChatProperties.getNotifyUrl());
JSONObject amount = new JSONObject();
amount.put("total", total.multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue());
amount.put("currency", "CNY");
jsonObject.put("amount", amount);
JSONObject payer = new JSONObject();
payer.put("openid", openid);
jsonObject.put("payer", payer);
String body = jsonObject.toJSONString();
return post(JSAPI, body);
}
/**
* 小程序支付
*
* @param orderNum 商户订单号
* @param total 金额,单位 元
* @param description 商品描述
* @param openid 微信用户的openid
* @return
*/
public JSONObject pay(String orderNum, BigDecimal total, String description, String openid) throws Exception {
//统一下单,生成预支付交易单
String bodyAsString = jsapi(orderNum, total, description, openid);
//解析返回结果
JSONObject jsonObject = JSON.parseObject(bodyAsString);
System.out.println(jsonObject);
String prepayId = jsonObject.getString("prepay_id");
if (prepayId != null) {
String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
String nonceStr = RandomStringUtils.randomNumeric(32);
ArrayList<Object> list = new ArrayList<>();
list.add(weChatProperties.getAppid());
list.add(timeStamp);
list.add(nonceStr);
list.add("prepay_id=" + prepayId);
//二次签名,调起支付需要重新签名
StringBuilder stringBuilder = new StringBuilder();
for (Object o : list) {
stringBuilder.append(o).append("\n");
}
String signMessage = stringBuilder.toString();
byte[] message = signMessage.getBytes();
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(PemUtil.loadPrivateKey(new FileInputStream(new File(weChatProperties.getPrivateKeyFilePath()))));
signature.update(message);
String packageSign = Base64.getEncoder().encodeToString(signature.sign());
//构造数据给微信小程序,用于调起微信支付
JSONObject jo = new JSONObject();
jo.put("timeStamp", timeStamp);
jo.put("nonceStr", nonceStr);
jo.put("package", "prepay_id=" + prepayId);
jo.put("signType", "RSA");
jo.put("paySign", packageSign);
return jo;
}
return jsonObject;
}
/**
* 申请退款
*
* @param outTradeNo 商户订单号
* @param outRefundNo 商户退款单号
* @param refund 退款金额
* @param total 原订单金额
* @return
*/
public String refund(String outTradeNo, String outRefundNo, BigDecimal refund, BigDecimal total) throws Exception {
JSONObject jsonObject = new JSONObject();
jsonObject.put("out_trade_no", outTradeNo);
jsonObject.put("out_refund_no", outRefundNo);
JSONObject amount = new JSONObject();
amount.put("refund", refund.multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue());
amount.put("total", total.multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue());
amount.put("currency", "CNY");
jsonObject.put("amount", amount);
jsonObject.put("notify_url", weChatProperties.getRefundNotifyUrl());
String body = jsonObject.toJSONString();
//调用申请退款接口
return post(REFUNDS, body);
}
}
/**
* 根据订单号查询订单
* @param orderNumber
*/
@Select("select * from orders where number = #{orderNumber}")
Orders getByNumber(String orderNumber);
/**
* 修改订单信息
* @param orders
*/
void update(Orders orders);
@Update("update orders set status = #{orderStatus},pay_status = #{orderPaidStatus},checkout_time = #{check_out_time} where id = #{id}")
void updateStatus(Integer orderStatus, Integer orderPaidStatus, LocalDateTime check_out_time,Long id);
<update id="update" parameterType="com.sky.entity.Orders">
update orders
<set>
<if test="cancelReason != null and cancelReason!='' "> cancel_reason=#{cancelReason}, </if>
<if test="rejectionReason != null and rejectionReason!='' "> rejection_reason=#{rejectionReason}, </if>
<if test="cancelTime != null"> cancel_time=#{cancelTime}, </if>
<if test="payStatus != null"> pay_status=#{payStatus}, </if>
<if test="payMethod != null"> pay_method=#{payMethod}, </if>
<if test="checkoutTime != null"> checkout_time=#{checkoutTime}, </if>
<if test="status != null"> status = #{status}, </if>
<if test="deliveryTime != null"> delivery_time = #{deliveryTime} </if>
</set>
where id = #{id}
</update>
package com.sky.controller.nofity;
import com.alibaba.druid.support.json.JSONUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.sky.properties.WeChatProperties;
import com.sky.service.OrderService;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.entity.ContentType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
/**
* 支付回调相关接口
*/
@RestController
@RequestMapping("/notify")
@Slf4j
public class PayNotifyController {
@Autowired
private OrderService orderService;
@Autowired
private WeChatProperties weChatProperties;
/**
* 支付成功回调
*
* @param request
*/
@RequestMapping("/paySuccess")
public void paySuccessNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
//读取数据
String body = readData(request);
log.info("支付成功回调:{}", body);
//数据解密
String plainText = decryptData(body);
log.info("解密后的文本:{}", plainText);
JSONObject jsonObject = JSON.parseObject(plainText);
String outTradeNo = jsonObject.getString("out_trade_no");//商户平台订单号
String transactionId = jsonObject.getString("transaction_id");//微信支付交易号
log.info("商户平台订单号:{}", outTradeNo);
log.info("微信支付交易号:{}", transactionId);
//业务处理,修改订单状态、来单提醒
orderService.paySuccess(outTradeNo);
//给微信响应
responseToWeixin(response);
}
/**
* 读取数据
*
* @param request
* @return
* @throws Exception
*/
private String readData(HttpServletRequest request) throws Exception {
BufferedReader reader = request.getReader();
StringBuilder result = new StringBuilder();
String line = null;
while ((line = reader.readLine()) != null) {
if (result.length() > 0) {
result.append("\n");
}
result.append(line);
}
return result.toString();
}
/**
* 数据解密
*
* @param body
* @return
* @throws Exception
*/
private String decryptData(String body) throws Exception {
JSONObject resultObject = JSON.parseObject(body);
JSONObject resource = resultObject.getJSONObject("resource");
String ciphertext = resource.getString("ciphertext");
String nonce = resource.getString("nonce");
String associatedData = resource.getString("associated_data");
AesUtil aesUtil = new AesUtil(weChatProperties.getApiV3Key().getBytes(StandardCharsets.UTF_8));
//密文解密
String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
nonce.getBytes(StandardCharsets.UTF_8),
ciphertext);
return plainText;
}
/**
* 给微信响应
* @param response
*/
private void responseToWeixin(HttpServletResponse response) throws Exception{
response.setStatus(200);
HashMap<Object, Object> map = new HashMap<>();
map.put("code", "SUCCESS");
map.put("message", "SUCCESS");
response.setHeader("Content-type", ContentType.APPLICATION_JSON.toString());
response.getOutputStream().write(JSONUtils.toJSONString(map).getBytes(StandardCharsets.UTF_8));
response.flushBuffer();
}
}
·功能测试
因为我们是用个人注册的微信小程序,所以没有权限实现微信支付。
这里我参考了别人的代码,直接点确认支付让它跳转到支付成功的页面,绕过微信后台,直接默认点确认支付就支付成功,把订单的状态直接更新到数据库里面。