从零玩转系列之微信支付实战PC端接口搭建
一、前言
halo各位大佬很久没更新了最近在搞微信支付,因商户号审核了我半个月和小程序认证也找了资料并且将商户号和小程序进行关联,至此微信支付Native支付完成.此篇文章过长我将分几个阶段的文章发布(项目源码都有,小程序和PC端)
在此之前已经更新了微信支付开篇、微信支付安全、微信实战基础框架搭建、本次更新为微信支付实战PC端接口搭建,实战篇分为几个章节因为代码量确实有点多哈.
开源仓库,对您有帮助的话请给我一个star 谢谢
本次项目使用技术栈
后端: SpringBoot3.1.x、Mysql8.0、MybatisPlus
前端: Vue3、Vite、ElementPlus
小程序: Uniapp、Uview
问题微信添加: BN_Tang
备注: 微信支付
二、Native模式
在com.yby6.service包下创建接口 WxPayService
package com.yby6.service;
import cn.hutool.json.JSONUtil;
import com.yby6.config.WxPayConfig;
import com.yby6.enums.WxApiType;
import com.yby6.utils.OrderNoUtils;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Service
@Slf4j
@RequiredArgsConstructor
public class WxPayService {
private final WxPayConfig wxPayConfig;
/**
* 会进行验证签名
* 发送请求
*/
private final CloseableHttpClient wxPayClient;
/**
* 统一调用下单API,生成支付二维码
*
* @param productId 商户ID
*/
@SneakyThrows
public Map<String, Object> nativePay(Long productId) {
log.info("调用统一下单API");
//调用统一下单API
HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));
// 请求body参数
Map<String, Object> paramsMap = builderRequestParams("测试页面统一下单", 1);
//将参数转换成json字符串
String jsonParams = JSONUtil.toJsonStr(paramsMap);
log.info("请求参数 ===> {}" + jsonParams);
StringEntity entity = new StringEntity(jsonParams, "utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
// 完成签名并执行请求
try (CloseableHttpResponse response = wxPayClient.execute(httpPost)) {
// 获取响应参数
Map resultMap = buildBodyParams(response, Map.class);
// 二维码
String codeUrl = (String) resultMap.get("code_url");
// 保存二维码
return new HashMap<>() {{
put("codeUrl", codeUrl);
}};
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 构建器请求参数
*
* @param description 描述
* @param total 总
* @return {@link Map}<{@link String}, {@link Object}>
*/
private Map<String, Object> builderRequestParams(String description, int total) {
Map<String, Object> paramsMap = new HashMap<>(14);
paramsMap.put("appid", wxPayConfig.getAppid());
paramsMap.put("mchid", wxPayConfig.getMchId());
paramsMap.put("description", description);
paramsMap.put("out_trade_no", OrderNoUtils.getOrderNo());
paramsMap.put("notify_url", wxPayConfig.getNotifyDomain()); // 微信回调通知 支付通知 和退款通知都调用这个接口
Map<String, Object> amountMap = new HashMap<>();
amountMap.put("total", total); // 分
amountMap.put("currency", "CNY");
// 设置金额
paramsMap.put("amount", amountMap);
return paramsMap;
}
/**
* 解析响应参数
*/
private <T> T buildBodyParams(CloseableHttpResponse response, Class<T> tClass) throws IOException {
T bodyAsString = null;
if (null != response.getEntity()) {
String json = EntityUtils.toString(response.getEntity());//响应体
bodyAsString = JSONUtil.toBean(json, tClass);
}
int statusCode = response.getStatusLine().getStatusCode();//响应状态码
if (statusCode == 200) { //处理成功
log.info("成功, 返回结果 = " + bodyAsString);
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功");
} else if (statusCode == 404) { //处理成功,无返回Body
log.info("没找到订单...");
} else {
log.info("响应:{}, {}", response.getEntity(), response.getStatusLine());
log.info("失败,响应码 = " + statusCode + ",返回结果 = " + bodyAsString);
throw new IOException("request failed");
}
return bodyAsString;
}
}
在com.yby6.controller包下创建 WechatNativeController
package com.yby6.controller;
import com.yby6.config.WxPayConfig;
import com.yby6.reponse.R;
import com.yby6.service.WxPayService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
*
* @author Yang Shuai
* Create By 2023/6/8
*/
@Slf4j
@RestController
@RequestMapping("/api/wx-pay/native")
@RequiredArgsConstructor
public class WechatNativeController {
private final WxPayService wxPayService;
/**
* 调用统一下单API,生成支付二维码
*
* @param productId 产品id
* @return {@link R}
*/
@PostMapping("/native/{productId}")
public R<Map<String, Object>> nativePay(@PathVariable Long productId) {
log.info("发起支付请求 v3, 返回支付二维码连接和订单号");
return R.ok(wxPayService.nativePay(productId));
}
}
启动程序 请求下单接口 /api/wx-pay/native/native/1
复制返回的微信二维码地址
进入 https://cli.im/url 生成扫描二维码 使用微信扫描
结果可以正常扫码并且支付
🌈 接入商品和订单存储数据库 (重点)
修改 WxPayService 引入订单服务
/**
* 订单服务
*/
private final OrderInfoService orderInfoService;
修改 OrderInfoService 新增 createOrderByProductId
方法
package com.yby6.service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yby6.domain.OrderInfo;
import com.yby6.domain.Product;
import com.yby6.enums.OrderStatus;
import com.yby6.mapper.OrderInfoMapper;
import com.yby6.utils.OrderNoUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderInfoService extends ServiceImpl<OrderInfoMapper, OrderInfo> {
private final ProductService productService;
/**
* 创建订单产品id
*
* @param productId 产品id
* @param nickName 支付者名称 (用于小程序)
* @return {@link OrderInfo}
*/
public OrderInfo createOrderByProductId(Long productId, String nickName) {
// 查找已存在但未支付的订单
OrderInfo orderInfo = this.lambdaQuery().eq(OrderInfo::getProductId, productId).eq(OrderInfo::getOrderStatus, OrderStatus.NOTPAY.getType()).one();
if (orderInfo != null) {
return orderInfo;
}
// 根据商品ID 获取商品信息
final Product product = productService.lambdaQuery().eq(Product::getId, productId).one();
// 创建订单信息
orderInfo = new OrderInfo();
String productTitle = product.getTitle();
if (nickName != null) {
productTitle = productTitle.concat("-" + nickName);
}
orderInfo.setTitle(productTitle);
orderInfo.setOrderNo(OrderNoUtils.getOrderNo());
orderInfo.setProductId(productId);
orderInfo.setTotalFee(product.getPrice()); // 分
orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType());
// 保存订单信息
save(orderInfo);
return orderInfo;
}
}
修改 nativePay 方法并且优化部分代码提公共
/**
* 统一调用下单API,生成支付二维码
*
* @param productId 商户ID
*/
@SneakyThrows
public Map<String, Object> nativePay(Long productId) {
log.info("生成订单");
// 生成订单
OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId, null);
String codeUrl = orderInfo.getCodeUrl();
// 下下面这段代码限制了商品只能购买一次在有效期期间不能继续创建订单
if (StrUtil.isNotEmpty(codeUrl)) {
log.info("订单已存在,二维码已保存");
// 返回二维码
Map<String, Object> map = new HashMap<>();
map.put("codeUrl", codeUrl);
map.put("orderNo", orderInfo.getOrderNo());
return map;
}
log.info("调用统一下单API");
//调用统一下单API
HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));
// 请求body参数
builderRequestParams(httpPost, orderInfo.getTitle(), orderInfo.getTotalFee(), orderInfo.getOrderNo());
// 完成签名并执行请求
try (CloseableHttpResponse response = wxPayClient.execute(httpPost)) {
// 获取响应参数
Map resultMap = buildBodyParams(response, Map.class);
// 二维码
String url = (String) resultMap.get("code_url");
// 保存二维码
return saveCodeUrl(orderInfo, url);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 保存二维码
*
* @param orderInfo 订单信息
* @param codeUrl 保存二维码
*/
private Map<String, Object> saveCodeUrl(OrderInfo orderInfo, String codeUrl) {
//保存二维码
orderInfoService.lambdaUpdate().eq(OrderInfo::getOrderNo, orderInfo.getOrderNo()).set(OrderInfo::getCodeUrl, codeUrl).update();
//返回二维码
Map<String, Object> map = new HashMap<>();
map.put("codeUrl", codeUrl);
map.put("orderNo", orderInfo.getOrderNo());
return map;
}
/**
* 构建器请求参数
*
* @param httpPost 构建请求
* @param description 描述
* @param total 总
* @param orderNo 交易订单号
* @return {@link Map}<{@link String}, {@link Object}>
*/
private void builderRequestParams(HttpPost httpPost, String description, int total, String orderNo) {
Map<String, Object> paramsMap = new HashMap<>(8);
paramsMap.put("appid", wxPayConfig.getAppid());
paramsMap.put("mchid", wxPayConfig.getMchId());
paramsMap.put("description", description);
paramsMap.put("out_trade_no", orderNo);
// 微信回调通知 支付通知 和退款通知都调用这个接口
paramsMap.put("notify_url", wxPayConfig.getNotifyDomain());
Map<String, Object> amountMap = new HashMap<>();
amountMap.put("total", total); // 分
amountMap.put("currency", "CNY");
// 设置金额
paramsMap.put("amount", amountMap);
// 将参数转换成json字符串
String jsonParams = JSONUtil.toJsonStr(paramsMap);
log.info("请求参数 ===> {}" + jsonParams);
StringEntity entity = new StringEntity(jsonParams, "utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
}
/**
* 解析响应参数
*/
private <T> T buildBodyParams(CloseableHttpResponse response, Class<T> tClass) throws IOException {
T bodyAsString = null;
if (null != response.getEntity()) {
String json = EntityUtils.toString(response.getEntity());//响应体
bodyAsString = JSONUtil.toBean(json, tClass);
}
int statusCode = response.getStatusLine().getStatusCode();//响应状态码
if (statusCode == 200) { //处理成功
log.info("成功, 返回结果 = " + bodyAsString);
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功");
} else if (statusCode == 404) { //处理成功,无返回Body
log.info("没找到订单...");
} else {
log.info("响应:{}, {}", response.getEntity(), response.getStatusLine());
log.info("失败,响应码 = " + statusCode + ",返回结果 = " + bodyAsString);
throw new IOException("request failed");
}
return bodyAsString;
}
上面代码新增了生成订单保存到数据库的过程并且优化了部分代码
启动程序 请求下单接口 /api/wx-pay/native/native/{productId}
{productId} 查看商品表数据的ID
复制返回的微信二维码地址
进入 https://cli.im/url 生成扫描二维码 使用微信扫描
你的压力来源于无法自律,只是假装努力,现状跟不上内心欲望,所以你焦虑又恐慌。——杨不易