微信支付介绍
支付大家应该都不陌生了,在现实生活中经常购买商品并且使用支付功能来付款,在付款的时候可能使用比较多的就是微信支付和支付宝支付了。在苍穹外卖项目中,选择的就是微信支付这种支付方式。
要实现微信支付就需要注册微信支付的一个商户号,这个商户号是必须要有一家企业并且有正规的营业执照。只有具备了这些资质之后,才可以去注册商户号,才能开通支付权限。
个人不具备这种资质,所以我们在学习微信支付时,最重要的是了解微信支付的流程,并且能够阅读微信官方提供的接口文档,能够和第三方支付平台对接起来就可以了。
微信支付产品:
小程序支付
产品中心 - 微信支付商户平台
微信支付接入流程:
微信小程序支付时序图:
微信支付相关接口:
**JSAPI下单:**商户系统调用该接口在微信支付服务后台生成预支付交易单(对应时序图的第5步)
**微信小程序调起支付:**通过JSAPI下单接口获取到发起支付的必要参数prepay_id,然后使用微信支付提供的小程序方法调起小程序支付(对应时序图的第10步)
微信支付准备工作
如何保证数据安全?
完成微信支付有两个关键的步骤:
第一个就是需要在商户系统当中调用微信后台的一个下单接口,就是生成预支付交易单。
第二个就是支付成功之后微信后台会给推送消息。
这两个接口数据的安全性,要求其实是非常高的。
**解决:**微信提供的方式就是对数据进行加密、解密、签名多种方式。要完成数据加密解密,需要提前准备相应的一些文件,其实就是一些证书。
获取微信支付平台证书、商户私钥文件:
如何调用到商户系统
微信后台会调用到商户系统给推送支付的结果,在这里我们就会遇到一个问题,就是微信后台怎么就能调用到我们这个商户系统呢?因为这个调用过程,其实本质上也是一个HTTP请求。
代码示例Java-Demo
微信支付相关配置
sky:
wechat:
appid:
secret:
mchid :
mchSerialNo:
privateKeyFilePath:
apiV3Key:
weChatPayCertFilePath:
notifyUrl:
refundNotifyUrl:
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "sky.wechat")
@Data
public class WeChatProperties {
private String appid; //小程序的appid
private String secret; //小程序的秘钥
private String mchid; //商户号
private String mchSerialNo; //商户API证书的证书序列号
private String privateKeyFilePath; //商户私钥文件
private String apiV3Key; //证书解密的密钥
private String weChatPayCertFilePath; //平台证书
private String notifyUrl; //支付成功的回调地址
private String refundNotifyUrl; //退款成功的回调地址
}
Util
/**
* 微信支付工具类
*/
@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);
}
}
Mapper
@Mapper
public interface OrderMapper {
/**
* 根据订单号和用户id查询订单
* @param orderNumber
* @param userId
*/
@Select("select * from orders where number = #{orderNumber} and user_id= #{userId}")
Orders getByNumberAndUserId(String orderNumber, Long userId);
/**
* 修改订单信息
* @param orders
*/
void update(Orders orders);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.OrderMapper">
<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>
</mapper>
Service
public interface OrderService {
/**
* 订单支付
* @param ordersPaymentDTO
* @return
*/
OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception;
/**
* 支付成功,修改订单状态
* @param outTradeNo
*/
void paySuccess(String outTradeNo);
}
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Autowired
private UserMapper userMapper;
@Autowired
private WeChatPayUtil weChatPayUtil;
/**
* 订单支付
*
* @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) {
// 当前登录用户id
Long userId = BaseContext.getCurrentId();
// 根据订单号查询当前用户的订单
Orders ordersDB = orderMapper.getByNumberAndUserId(outTradeNo, userId);
// 根据订单id更新订单的状态、支付方式、支付状态、结账时间
Orders orders = Orders.builder()
.id(ordersDB.getId())
.status(Orders.TO_BE_CONFIRMED)
.payStatus(Orders.PAID)
.checkoutTime(LocalDateTime.now())
.build();
orderMapper.update(orders);
}
}
Controller
/**
* 订单接口
*/
@RestController("userOrderController")
@RequestMapping("/user/order")
@Api(tags = "订单接口")
@Slf4j
public class OrderController {
/**
* 订单支付
*
* @param ordersPaymentDTO
* @return
*/
@PutMapping("/payment")
@ApiOperation("订单支付")
public Result<OrderPaymentVO> payment(@RequestBody OrdersPaymentDTO ordersPaymentDTO) throws Exception {
log.info("订单支付:{}", ordersPaymentDTO);
OrderPaymentVO orderPaymentVO = orderService.payment(ordersPaymentDTO);
log.info("生成预支付交易单:{}", orderPaymentVO);
return Result.success(orderPaymentVO);
}
}
/**
* 支付回调相关接口
*/
@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();
}
}