一、微信小程序支付流程及接入准备
1.接入流程示意图
2. 前往微信支付官网开通商户支付能力
由公司法人去微信支付官网填写公司相关信息,并且开通微信支付功能
在API安全处设置支付秘钥
获取商户号
二、java后端实现步骤及代码
导入Maven依赖
<!-- 微信支付API -->
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.20</version>
<scope>compile</scope>
</dependency>
添加配置信息
# 微信支付配置
pay:
appId: xxx #应用id
mchId: xxx #商户id
notifyUrl: https://服务器ip或对应域名/wxpay/weixin/callback #支付回调地址
所需要的实体类
预下单成功之后返回结果对象 OrderReturnInfo
package com.ruoyi.system.weixinpay.domain;
import lombok.Data;
/**
* 预下单成功之后返回结果对象
* @author kun
* {@code @date} 2024/1/30
*/
@Data
public class OrderReturnInfo {
/** 返回状态码 */
private String return_code;
/** 返回信息 */
private String return_msg;
/** 业务结果 */
private String result_code;
/** 小程序appID */
private String appid;
/** 商户号 */
private String mch_id;
/** 随机字符串 */
private String nonce_str;
/** 签名 */
private String sign;
/** 预支付交易会话标识。用于后续接口调用中使用,该值有效期为2小时 */
private String prepay_id;
/** 交易类型 */
private String trade_type;
}
查询订单返回实体类 QueryReturnInfo
package com.ruoyi.system.weixinpay.domain;
import lombok.Data;
/**
* 查询订单返回实体类
* 微信官方文档地址:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_7&index=8
* @author kun
* {@code @date} 2024/1/30
*/
@Data
public class QueryReturnInfo {
/** 返回状态码 */
private String return_code;
/** 返回信息 */
private String return_msg;
/** 业务结果 */
private String result_code;
/** 错误代码 */
private String err_code;
/** 错误代码描述 */
private String err_code_des;
/** 小程序appID */
private String appid;
/** 商户号 */
private String mch_id;
/** 随机字符串 */
private String nonce_str;
/** 签名 */
private String sign;
/** 签名类型 */
private String sign_type;
private String prepay_id;
/** 交易类型 */
private String trade_type;
/** 设备号 */
private String device_info;
/** 用户标识 */
private String openid;
/** 是否关注公众账号 */
private String is_subscribe;
private String trade_state;
/** 付款银行 */
private String bank_type;
/** 订单金额 */
private int total_fee;
/** 应结订单金额 */
private int settlement_total_fee;
/** 货币种类 */
private String fee_type;
/** 现金支付金额 */
private int cash_fee;
/** 现金支付货币类型 */
private String cash_fee_type;
/** 总代金券金额 */
private int coupon_fee;
/** 代金券使用数量 */
private int coupon_count;
/** 代金券类型 */
private String coupon_type_$n;
/** 代金券ID */
private String coupon_id_$n;
/** 单个代金券支付金额 */
private String coupon_fee_$n;
/** 微信支付订单号 */
private String transaction_id;
/** 商户订单号 */
private String out_trade_no;
/** 支付完成时间 */
private String time_end;
private String trade_state_desc;
/** 商家数据包 */
private String attach;
}
微信支付,商品信息对象 PayParameterVO
package com.ruoyi.system.weixinpay.domain;
import lombok.Data;
/**
* 微信支付,商品信息对象
* @author kun
* {@code @date} 2024/1/30
*/
@Data
public class PayParameterVO {
/** 商品价格(单位:分) */
private String price;
/** 微信openId */
private String wxOpenId;
/** 商品描述 */
private String goodsTitle;
}
签名实体类 SignInfo
package com.ruoyi.system.weixinpay.domain;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import lombok.Data;
/**
* 签名实体类
* @author kun
* {@code @date} 2024/1/30
*/
@Data
public class SignInfo {
private String appId;//小程序ID
private String timeStamp;//时间戳
private String nonceStr;//随机串
@XStreamAlias("package")
private String repay_id;
private String signType;//签名方式
public String getAppId() {
return appId;
}
public void setAppId(String appId) {
this.appId = appId;
}
public String getTimeStamp() {
return timeStamp;
}
public void setTimeStamp(String timeStamp) {
this.timeStamp = timeStamp;
}
public String getNonceStr() {
return nonceStr;
}
public void setNonceStr(String nonceStr) {
this.nonceStr = nonceStr;
}
public String getRepay_id() {
return repay_id;
}
public void setRepay_id(String repay_id) {
this.repay_id = repay_id;
}
public String getSignType() {
return signType;
}
public void setSignType(String signType) {
this.signType = signType;
}
}
微信支付预下单实体类 WeChatPay
package com.ruoyi.system.weixinpay.domain;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 微信支付预下单实体类
* @author kun
* {@code @date} 2024/1/30
*/
@Data
@Accessors(chain = true)
public class WeChatPay {
/**
* 返回状态码 此字段是通信标识,非交易标识,交易是否成功需要查看result_code来判断
*/
public String return_code;
/**
* 返回信息 当return_code为FAIL时返回信息为错误原因 ,例如 签名失败 参数格式校验错误
*/
private String return_msg;
/**
* 公众账号ID 调用接口提交的公众账号ID
*/
private String appid;
/**
* 商户号 调用接口提交的商户号
*/
private String mch_id;
/**
* api密钥 详见:https://pay.weixin.qq.com/index.php/extend/employee
*/
private String api_key;
/**
* 设备号 自定义参数,可以为请求支付的终端设备号等
*/
private String device_info;
/**
* 随机字符串 5K8264ILTKCH16CQ2502SI8ZNMTM67VS 微信返回的随机字符串
*/
private String nonce_str;
/**
* 签名 微信返回的签名值,详见签名算法:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=4_3
*/
private String sign;
/**
* 签名类型
*/
private String sign_type;
/**
* 业务结果 SUCCESS SUCCESS/FAIL
*/
private String result_code;
/**
* 错误代码 当result_code为FAIL时返回错误代码,详细参见下文错误列表
*/
private String err_code;
/**
* 错误代码描述 当result_code为FAIL时返回错误描述,详细参见下文错误列表
*/
private String err_code_des;
/**
* 交易类型 JSAPI JSAPI -JSAPI支付 NATIVE -Native支付 APP -APP支付 说明详见;https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=4_2
*/
private String trade_type;
/**
* 预支付交易会话标识 微信生成的预支付会话标识,用于后续接口调用中使用,该值有效期为2小时
*/
private String prepay_id;
/**
* 二维码链接 weixin://wxpay/bizpayurl/up?pr=NwY5Mz9&groupid=00 trade_type=NATIVE时有返回,此url用于生成支付二维码,然后提供给用户进行扫码支付。注意:code_url的值并非固定,使用时按照URL格式转成二维码即可
*/
private String code_url;
/**
* 商品描述 商品简单描述,该字段请按照规范传递,具体请见 https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=4_2
*/
private String body;
/**
* 商家订单号 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|* 且在同一个商户号下唯一。详见商户订单号 https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=4_2
*/
private String out_trade_no;
/**
* 标价金额 订单总金额,单位为分,详见支付金额 https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=4_2
*/
private String total_fee;
/**
* 终端IP 支持IPV4和IPV6两种格式的IP地址。用户的客户端IP
*/
private String spbill_create_ip;
/**
* 通知地址 异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。公网域名必须为https,如果是走专线接入,使用专线NAT IP或者私有回调域名可使用http
*/
private String notify_url;
/**
* 子商户号 sub_mch_id 非必填(商户不需要传入,服务商模式才需要传入) 微信支付分配的子商户号
*/
private String sub_mch_id;
/**
* 附加数据,在查询API和支付通知中原样返回,该字段主要用于商户携带订单的自定义数据
*/
private String attach;
/**
* 商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。
*/
private String out_refund_no;
/**
* 退款总金额,单位为分,只能为整数,可部分退款。详见支付金额 https://pay.weixin.qq.com/wiki/doc/api/native_sl.php?chapter=4_2
*/
private String refund_fee;
/**
* 退款原因 若商户传入,会在下发给用户的退款消息中体现退款原因 注意:若订单退款金额≤1元,且属于部分退款,则不会在退款消息中体现退款原因
*/
private String refund_desc;
/**
* 交易结束时间 订单失效时间,格式为yyyyMMddHHmmss,如2009年12月27日9点10分10秒表示为20091227091010。其他详见时间规则 注意:最短失效时间间隔必须大于5分钟
*/
private String time_expire;
/**
* 用户标识 trade_type=JSAPI,此参数必传,用户在主商户appid下的唯一标识。openid和sub_openid可以选传其中之一,如果选择传sub_openid,则必须传sub_appid。下单前需要调用【网页授权获取用户信息: https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html 】接口获取到用户的Openid。
*/
private String openid;
/**
* 时间戳
*/
private String time_stamp;
/**
* 会员类型
*/
private String memberShipType;
}
微信支付API地址常量 WeChatPayUrlConstants
package com.ruoyi.system.weixinpay.domain;
/**
* 微信支付API地址常量
* @author kun
* {@code @date} 2024/1/30
*/
public class WeChatPayUrlConstants {
/**
* 统一下单预下单接口url
*/
public static final String Uifiedorder = "https://api.mch.weixin.qq.com/pay/unifiedorder";
/**
* 订单状态查询接口URL
*/
public static final String Orderquery = "https://api.mch.weixin.qq.com/pay/orderquery";
/**
* 订单申请退款
*/
public static final String Refund = "https://api.mch.weixin.qq.com/secapi/pay/refund";
/**
* 付款码 支付
*/
public static final String MicroPay = "https://api.mch.weixin.qq.com/pay/micropay";
/**
* 微信网页授权 获取“code”请求地址
*/
public static final String GainCodeUrl = "https://open.weixin.qq.com/connect/oauth2/authorize";
/**
* 微信网页授权 获取“code” 回调地址
*/
public static final String GainCodeRedirect_uri = "http://i5jmxe.natappfree.cc/boss/WeChatPayMobile/SkipPage.html";
}
微信支付配置 WxPayConfig
package com.ruoyi.system.weixinpay.domain;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
/**
* 微信支付配置
* @author kun
* {@code @date} 2024/1/30
*/
@Data
@Component
@Configuration
@ConfigurationProperties(prefix = "pay")
public class WxPayConfig {
/**
* 微信小程序appid
*/
private String appId;
/**
* 小程序设置的API v2密钥
*/
private String apiKey;
/**
* 微信商户平台 商户id
*/
private String mchId;
/**
*小程序密钥
*/
private String appSecret;
/**
* 小程序支付异步回调地址
*/
private String notifyUrl;
}
微信支付秘钥配置类 Configure
package com.ruoyi.system.weixinpay.config;
import lombok.Getter;
/**
* 微信支付配置类
* @author kun
* {@code @date} 2024/1/30
*/
public class Configure {
/**
* 商户支付秘钥
*/
@Getter
private static String key = "此处填写秘钥";
public static void setKey(String key) {
Configure.key = key;
}
}
使用到的工具类
Http工具类 HttpRequest
package com.ruoyi.system.weixinpay.util;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;
import com.thoughtworks.xstream.io.xml.XmlFriendlyNameCoder;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
/**
* Http工具类
* @author kun
* {@code @date} 2024/1/30
*/
public class HttpRequest {
//连接超时时间,默认10秒
private static final int socketTimeout = 10000;
//传输超时时间,默认30秒
private static final int connectTimeout = 30000;
/**
* post请求
*
* @throws IOException
* @throws ClientProtocolException
* @throws NoSuchAlgorithmException
* @throws KeyStoreException
* @throws KeyManagementException
* @throws UnrecoverableKeyException
*/
public static String sendPost(String url, Object xmlObj) throws ClientProtocolException, IOException, UnrecoverableKeyException, KeyManagementException, KeyStoreException, NoSuchAlgorithmException {
HttpPost httpPost = new HttpPost(url);
//解决XStream对出现双下划线的bug
XStream xStreamForRequestPostData = new XStream(new DomDriver("UTF-8", new XmlFriendlyNameCoder("-_", "_")));
xStreamForRequestPostData.alias("xml", xmlObj.getClass());
//将要提交给API的数据对象转换成XML格式数据Post给API
String postDataXML = xStreamForRequestPostData.toXML(xmlObj);
//得指明使用UTF-8编码,否则到API服务器XML的中文不能被成功识别
StringEntity postEntity = new StringEntity(postDataXML, "UTF-8");
httpPost.addHeader("Content-Type", "text/xml");
httpPost.setEntity(postEntity);
//设置请求器的配置
RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout).setConnectTimeout(connectTimeout).build();
httpPost.setConfig(requestConfig);
HttpClient httpClient = HttpClients.createDefault();
HttpResponse response = httpClient.execute(httpPost);
HttpEntity entity = response.getEntity();
String result = EntityUtils.toString(entity, "UTF-8");
return result;
}
/**
* 自定义证书管理器,信任所有证书
*
* @author pc
*/
public static class MyX509TrustManager implements X509TrustManager {
@Override
public void checkClientTrusted(
java.security.cert.X509Certificate[] arg0, String arg1)
throws CertificateException {
}
@Override
public void checkServerTrusted(
java.security.cert.X509Certificate[] arg0, String arg1)
throws CertificateException {
}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return null;
}
}
}
Map转为java对象 MapToObject
package com.ruoyi.system.weixinpay.util;
import java.lang.reflect.Field;
import java.util.Map;
/**
* Map转为java对象
* @author kun
* {@code @date} 2024/1/30
*/
public class MapToObject {
/**
* Map数据转为java对象
* @param map map数据
* @param targetType 对象类型
* @return
* @param <T>
* @throws IllegalAccessException
* @throws InstantiationException
*/
public static <T> T convertMapToObject(Map<String, String> map, Class<T> targetType) throws IllegalAccessException, InstantiationException {
T targetObject = targetType.newInstance();
for (Map.Entry<String, String> entry : map.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
try {
// 使用反射获取字段
Field field = targetType.getDeclaredField(key);
// 设置字段可访问(如果是私有字段)
field.setAccessible(true);
// 获取字段的类型
Class<?> fieldType = field.getType();
// 将字符串值转换为字段类型
Object convertedValue = convertStringToType(value, fieldType);
// 设置字段的值
field.set(targetObject, convertedValue);
} catch (NoSuchFieldException e) {
// 处理字段不存在的异常
e.printStackTrace();
}
}
return targetObject;
}
private static Object convertStringToType(String value, Class<?> targetType) {
if (targetType == int.class || targetType == Integer.class) {
return Integer.parseInt(value);
}
// 添加其他可能的类型转换逻辑,例如 double、float、Date 等
// 默认情况下,返回字符串值
return value;
}
}
MD5加密工具类 MD5
package com.ruoyi.system.weixinpay.util;
import java.security.MessageDigest;
/**
* MD5加密工具类
* @author kun
* {@code @date} 2024/1/30
*/
public class MD5 {
private final static String[] hexDigits = {"0", "1", "2", "3", "4", "5", "6", "7",
"8", "9", "a", "b", "c", "d", "e", "f"};
/**
* 转换字节数组为16进制字串
*
* @param b 字节数组
* @return 16进制字串
*/
public static String byteArrayToHexString(byte[] b) {
StringBuilder resultSb = new StringBuilder();
for (byte aB : b) {
resultSb.append(byteToHexString(aB));
}
return resultSb.toString();
}
/**
* 转换byte到16进制
*
* @param b 要转换的byte
* @return 16进制格式
*/
private static String byteToHexString(byte b) {
int n = b;
if (n < 0) {
n = 256 + n;
}
int d1 = n / 16;
int d2 = n % 16;
return hexDigits[d1] + hexDigits[d2];
}
/**
* MD5编码
*
* @param origin 原始字符串
* @return 经过MD5加密之后的结果
*/
public static String MD5Encode(String origin) {
String resultString = null;
try {
resultString = origin;
MessageDigest md = MessageDigest.getInstance("MD5");
resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
} catch (Exception e) {
e.printStackTrace();
}
return resultString;
}
}
微信签名工具类 SignUtils
package com.ruoyi.system.weixinpay.util;
import com.ruoyi.system.weixinpay.config.Configure;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
/**
* 微信签名
* @author kun
* {@code @date} 2024/1/30
*/
@Slf4j
public class SignUtils {
/**
* 签名算法
*
* @param o 要参与签名的数据对象
* @return 签名
* @throws IllegalAccessException
*/
public static String getSign(Object o) throws IllegalAccessException {
ArrayList<String> list = new ArrayList<String>();
Class cls = o.getClass();
Field[] fields = cls.getDeclaredFields();
for (Field f : fields) {
f.setAccessible(true);
if (f.get(o) != null && f.get(o) != "") {
String name = f.getName();
XStreamAlias anno = f.getAnnotation(XStreamAlias.class);
if (anno != null) {
name = anno.value();
}
list.add(name + "=" + f.get(o) + "&");
}
}
int size = list.size();
String[] arrayToSort = list.toArray(new String[size]);
Arrays.sort(arrayToSort, String.CASE_INSENSITIVE_ORDER);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < size; i++) {
sb.append(arrayToSort[i]);
}
String result = sb.toString();
result += "key=" + Configure.getKey();
log.info("签名数据:" + result);
result = MD5.MD5Encode(result).toUpperCase();
return result;
}
public static String getSign(Map<String, Object> map) {
ArrayList<String> list = new ArrayList<String>();
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (entry.getValue() != "") {
list.add(entry.getKey() + "=" + entry.getValue() + "&");
}
}
int size = list.size();
String[] arrayToSort = list.toArray(new String[size]);
Arrays.sort(arrayToSort, String.CASE_INSENSITIVE_ORDER);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < size; i++) {
sb.append(arrayToSort[i]);
}
String result = sb.toString();
result += "key=" + Configure.getKey();
//Util.log("Sign Before MD5:" + result);
result = MD5.MD5Encode(result).toUpperCase();
//Util.log("Sign Result:" + result);
return result;
}
}
业务层代码
接口代码 WxPayController
此处分别提供三个接口
1.小程序支付下单接口:通过此接口传入商品信息和当前支付的小程序用户的wxOpenId来创建预支付订单。
2.查询订单:通过此订单来查询订单的当前状态和订单的完整信息。
3.微信小程序支付成功回调:此接口需要通过预支付订单里边设置的回调地址来接收微信的支付反馈信息,当用户支付成功之后微信会调用此接口把支付成功的信息推送过来,方便做业务的后续处理。
package com.ruoyi.web.controller.system;
import com.github.wxpay.sdk.WXPayUtil;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.system.weixinpay.domain.PayParameterVO;
import com.ruoyi.system.weixinpay.domain.QueryReturnInfo;
import com.ruoyi.system.weixinpay.service.WxPayInfoService;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/system/wxpay")
public class WxPayController {
@Autowired
private WxPayInfoService wxPayInfoService;
/**
* 小程序支付下单接口
* @return 返回结果
*/
@ApiOperation("小程序支付功能")
@PostMapping("/pay")
public AjaxResult wxPay(@RequestBody PayParameterVO payParameterVO){
PayParameterVO parameterVO = new PayParameterVO();
parameterVO.setWxOpenId(SecurityUtils.getLoginUser().getUser().getWxOpenId());
parameterVO.setPrice("1");
parameterVO.setGoodsTitle("垂钓星球秘典三天会员");
HashMap<String, String> payHistory = wxPayInfoService.insertPayRecord(parameterVO);
return AjaxResult.success("success",payHistory);
}
/**
* 查询订单
*/
@ApiOperation("订单查询")
@PostMapping("/wx/query")
public AjaxResult orderQuery(@RequestParam("out_trade_no") String out_trade_no) {
QueryReturnInfo queryReturnInfo = wxPayInfoService.orderQuery(out_trade_no);
return AjaxResult.success(queryReturnInfo.getTrade_state_desc(), queryReturnInfo);
}
/**
* 微信小程序支付成功回调
* @param request 请求
* @param response 响应
* @return 返回结果
* @throws Exception 异常处理
*/
@RequestMapping("/weixin/callback")
public String callBack(HttpServletRequest request, HttpServletResponse response) throws Exception {
log.info("接收到微信支付回调信息");
String notifyXml = IOUtils.toString(request.getInputStream(), StandardCharsets.UTF_8);
// 解析返回结果
Map<String, String> notifyMap = WXPayUtil.xmlToMap(notifyXml);
// 判断支付是否成功
if ("SUCCESS".equals(notifyMap.get("result_code"))) {
//支付成功时候,处理业务逻辑
wxPayInfoService.payCallbackSuccess(notifyMap);
//返回处理成功的格式数据,避免微信重复回调
return "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"
+ "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ";
}
// 创建响应对象:微信接收到校验失败的结果后,会反复的调用当前回调函数
Map<String, String> returnMap = new HashMap<>();
returnMap.put("return_code", "FAIL");
returnMap.put("return_msg", "");
String returnXml = WXPayUtil.mapToXml(returnMap);
response.setContentType("text/xml");
System.out.println("校验失败");
return returnXml;
}
}
微信小程序支付-业务接口层 WxPayInfoService
package com.ruoyi.system.weixinpay.service;
import com.ruoyi.system.weixinpay.domain.PayParameterVO;
import com.ruoyi.system.weixinpay.domain.QueryReturnInfo;
import java.util.HashMap;
import java.util.Map;
/**
* 微信小程序支付-业务接口层
* @author kun
* {@code @date} 2024/1/30
*/
public interface WxPayInfoService {
/**
* 创建统一支付订单
*/
HashMap<String, String> insertPayRecord(PayParameterVO payParameterVO);
/**
* 查询订单
* @param out_trade_no 订单号
* @return 返回结果
*/
QueryReturnInfo orderQuery(String out_trade_no);
/**
* 微信小程序支付成功回调
* @param notifyMap
*/
void payCallbackSuccess(Map<String, String> notifyMap);
}
微信小程序支付-业务接口实现层 WxPayInfoServiceImpl
代码都有详细注释,根据注释查看代码逻辑即可
package com.ruoyi.system.weixinpay.service.impl;
import com.alibaba.fastjson2.JSON;
import com.github.wxpay.sdk.WXPayConstants;
import com.github.wxpay.sdk.WXPayUtil;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.ip.IpUtils;
import com.ruoyi.system.service.IFPayOrderService;
import com.ruoyi.system.weixinpay.domain.*;
import com.ruoyi.system.weixinpay.service.WxPayInfoService;
import com.ruoyi.system.weixinpay.util.HttpRequest;
import com.ruoyi.system.weixinpay.util.MapToObject;
import com.ruoyi.system.weixinpay.util.SignUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.util.*;
/**
* 微信小程序支付-业务接口实现层
* @author kun
* {@code @date} 2024/1/30
*/
@Service
@Slf4j
public class WxPayInfoServiceImpl implements WxPayInfoService {
//这是注入的业务处理类
@Autowired
private IFPayOrderService ifPayOrderService;
@Resource
private WxPayConfig payProperties;
private static final DecimalFormat df = new DecimalFormat("#");
/**
* 创建统一支付订单
* @param payParameterVO 商品信息
* @return 返回结果
*/
@Override
@Transactional
public HashMap<String, String> insertPayRecord(PayParameterVO payParameterVO) {
String title = payParameterVO.getGoodsTitle();
//金额 * 100 以分为单位
BigDecimal fee = BigDecimal.valueOf(1);
BigDecimal RMB = new BigDecimal(payParameterVO.getPrice());
BigDecimal totalFee = fee.multiply(RMB);
try {
WeChatPay weChatPay = new WeChatPay();
weChatPay.setAppid(payProperties.getAppId());
weChatPay.setMch_id(payProperties.getMchId());
weChatPay.setNonce_str(getRandomStringByLength(32));
weChatPay.setBody(title);
weChatPay.setOut_trade_no(getRandomStringByLength(32));
weChatPay.setTotal_fee(df.format(Double.parseDouble(String.valueOf(totalFee))));
weChatPay.setSpbill_create_ip(IpUtils.getHostIp());
weChatPay.setNotify_url(payProperties.getNotifyUrl());
weChatPay.setTrade_type("JSAPI");
//这里直接使用当前用户的openid
weChatPay.setOpenid(payParameterVO.getWxOpenId());
weChatPay.setSign_type("MD5");
//生成签名
String sign = SignUtils.getSign(weChatPay);
weChatPay.setSign(sign);
log.info("订单号:" + weChatPay.getOut_trade_no());
//向微信发送下单请求
String result = HttpRequest.sendPost(WeChatPayUrlConstants.Uifiedorder, weChatPay);
//将返回结果从xml格式转换为map格式
Map<String, String> wxResultMap = WXPayUtil.xmlToMap(result);
if (StringUtils.isNotEmpty(wxResultMap.get("return_code")) && wxResultMap.get("return_code").equals("SUCCESS")){
if (wxResultMap.get("result_code").equals("FAIL")){
log.error("微信统一下单失败!");
return null;
}
}
OrderReturnInfo returnInfo = MapToObject.convertMapToObject(wxResultMap, OrderReturnInfo.class);
// 二次签名
if ("SUCCESS".equals(returnInfo.getReturn_code()) && returnInfo.getReturn_code().equals(returnInfo.getResult_code())) {
SignInfo signInfo = new SignInfo();
signInfo.setAppId(payProperties.getAppId());
long time = System.currentTimeMillis() / 1000;
signInfo.setTimeStamp(String.valueOf(time));
signInfo.setNonceStr(WXPayUtil.generateNonceStr());
signInfo.setRepay_id("prepay_id=" + returnInfo.getPrepay_id());
signInfo.setSignType("MD5");
//生成签名
String sign1 = SignUtils.getSign(signInfo);
HashMap<String, String> payInfo = new HashMap<>();
payInfo.put("timeStamp", signInfo.getTimeStamp());
payInfo.put("nonceStr", signInfo.getNonceStr());
payInfo.put("package", signInfo.getRepay_id());
payInfo.put("signType", signInfo.getSignType());
payInfo.put("paySign", sign1);
payInfo.put("placeOrderJsonMsg", JSON.toJSONString(weChatPay));
payInfo.put("orderNum", weChatPay.getOut_trade_no());
// 业务逻辑结束 回传给小程序端唤起支付
return payInfo;
}
return null;
} catch (Exception e) {
log.error(e.getMessage());
}
return null;
}
/**
* 查询订单
* @param out_trade_no 订单号
* @return 返回结果
*/
@Override
public QueryReturnInfo orderQuery(String out_trade_no){
try {
WeChatPay weChatPay = new WeChatPay();
weChatPay.setAppid(payProperties.getAppId());
weChatPay.setMch_id(payProperties.getMchId());
weChatPay.setNonce_str(WXPayUtil.generateNonceStr());
weChatPay.setOut_trade_no(out_trade_no);
//order.setSign_type("MD5");
//生成签名
String sign = SignUtils.getSign(weChatPay);
weChatPay.setSign(sign);
//向微信发送查询订单详情请求
String result = HttpRequest.sendPost(WXPayConstants.ORDERQUERY_URL, weChatPay);
Map<String, String> xmlToMap = WXPayUtil.xmlToMap(result);
// 将 Map 转换为对象
return MapToObject.convertMapToObject(xmlToMap, QueryReturnInfo.class);
} catch (Exception e) {
log.error("查询支付订单失败:[{}]",e.getMessage());
}
return null;
}
/**
* 微信小程序支付成功回调
* @param notifyMap 回调Map数据
*/
@Override
public void payCallbackSuccess(Map<String, String> notifyMap) {
//保存相关支付数据
try {
QueryReturnInfo queryReturnInfo = MapToObject.convertMapToObject(notifyMap, QueryReturnInfo.class);
log.info("支付回调信息:" + queryReturnInfo);
//处理回调信息,此处根据自己的项目业务进行处理
ifPayOrderService.receivePayCallback(queryReturnInfo);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 获取一定长度的随机字符串
*
* @param length 指定字符串长度
* @return 一定长度的字符串
*/
public static String getRandomStringByLength(int length) {
String base = "abcdefghijklmnopqrstuvwxyz0123456789";
Random random = new Random();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
int number = random.nextInt(base.length());
sb.append(base.charAt(number));
}
return sb.toString();
}
}
三、前端实现步骤及代码
前端代码较为简单,主要调用后端接口发送支付请求即可。( 前端写的比较潦草,看主要功能即可 O(∩_∩)O~)
<template>
<view class="container">
<view style="display: flex;justify-content: center;align-items: center;background: #454545;margin: 10px;padding: 10px;border-radius: 5px;">
<view v-if="vuex_user.user.vipLevel == 0" style="color: #FFD700;font-size: 16px">开通会员享受众多福利</view>
<view v-if="vuex_user.user.vipLevel == 1" style="color: #FFD700;font-size: 16px">会员到期时间 {{vuex_user.user.vipEndTime.slice(0, 10)}}</view>
</view>
<view style="display: flex;flex-direction: column;margin: 10px;padding: 10px;border-radius: 5px;background: #454545;">
<text class="award-text" style="font-size: 20px;padding: 0 0 5px 0 ">Vip特权</text>
<text class="award-text">1. 隐藏所有广告</text>
<text class="award-text">2. 点位上传快速审核通道</text>
<text class="award-text">3. 每日签到积分翻倍</text>
<text class="award-text">4. 每日免费幸运抽奖免看广告</text>
<text class="award-text">更多福利开发中。。。</text>
</view>
<u-scroll-list :indicator="false">
<view v-for="(item, index) in goodsList" :key="index">
<view style="" class="card" :class="{ 'selected': selectedCard === index }" @tap="selectCard(index)">
<text style="font-size: 16px;padding: 0 0 5px 0;font-weight: bold;">{{ item.goodsTitle }}</text>
<image style="height: 50px;width: 50px;border-radius: 5px;" :src="item.imageUrl"></image>
<text style="color: #dd514c;font-size: 16px;">¥{{ item.goodsPrice/100 }}</text>
<text style="text-decoration: line-through;font-size: 15px;">¥{{ item.originalPrice/100 }}</text>
</view>
</view>
</u-scroll-list>
<view class="fixed-button">
<view style="background: #454545;border-radius: 5px;height: 45px;width: 60%;margin-left: 10px;margin-right: 10px;display: flex;justify-content: center;align-items: center" @click="goPay">
<view style="display: flex;justify-content: center;align-items: center;width: 100%">
<u-icon name="level" color="#FFD700" size="28"></u-icon>
<view v-if="vuex_user.user.vipLevel == 0" style="color: #FFD700;font-size: 16px">立即支付</view>
<view v-if="vuex_user.user.vipLevel == 1" style="color: #FFD700;font-size: 16px">续费会员</view>
</view>
</view>
</view>
</view>
</template>
<script>
import common from 'common/common.js';
//导出一个默认的对象
export default {
//定义data属性
data() {
return {
//商品列表
goodsList: [],
selectedCard: null,
nowGoodsId: '',
//设备系统信息
osType: '',
}
},
//页面加载时执行的函数
onLoad(e) {
const that = this
that.getSystemInfo();
that.appGetGoodsList();
},
//页面显示时执行的函数
onShow() {
},
//页面挂载时执行的函数
mounted() {},
//定义方法
methods: {
/**
* 获取设备系统信息
*/
getSystemInfo() {
const that = this
uni.getSystemInfo({
success: (res) => {
// res.platform 可能的值有:"android"、"ios"、"devtools"等
if (res.platform.toLowerCase() === 'android') {
that.osType = 'Android';
} else if (res.platform.toLowerCase() === 'ios') {
that.osType = 'IOS';
} else {
that.osType = '其他';
}
console.log("设备系统信息:",that.osType)
},
fail: (error) => {
console.error('获取系统信息失败', error);
},
});
},
/**
* 选择商品卡片
* @param index
*/
selectCard(index) {
const that = this
console.log("选择的商品卡片索引",index)
that.selectedCard = index;
that.nowGoodsId = that.goodsList[index].id
},
//支付函数
goPay () {
console.log('goPay');
const that = this
//向服务器发送下单请求
uni.request({
url: common.api_base_url + "/system/payOrder/appCreatePayOrder",
header: {
"Content-Type": "application/json",
"Authorization": "Bearer " + uni.getStorageSync('token'),
},
method: 'POST',
data: {
goodsId: that.nowGoodsId,
},
success(res) {
console.log("下单信息结果",res)
if (res.data.code == 200){
//否则,调用微信支付接口进行支付
uni.requestPayment({
provider: 'wxpay',
timeStamp: res.data.data.timeStamp,
nonceStr: res.data.data.nonceStr,
package: res.data.data.package,
signType: res.data.data.signType,
paySign: res.data.data.paySign,
// appId: app.globalData.appid,
success: function (ress) {
console.log("支付完成:",ress)
//支付成功后,弹出提示并在2秒后跳转到订单详情页
uni.showToast({
title: '支付成功',
duration: 2000
});
},
fail: function (err) {
uni.showToast({
title: '支付失败!',err,
icon:"none",
duration: 2000
});
}
});
}else {
//弹出下单失败的提示
uni.showToast({
title:res.data.msg,
icon:"none"
});
}
}
})
},
/**
* 获取商品列表
*/
appGetGoodsList(){
let that = this
uni.request({
url: common.api_base_url + '/system/goods/appGetList',
header: {
'content-type': 'application/x-www-form-urlencoded',
"Authorization": "Bearer " + uni.getStorageSync('token'),
},
method: 'GET',
data: {
"pageNum": 1,
"pageSize": 999
},
success: res => {
console.log(res)
if (res.data.code == 200){
that.goodsList = res.data.rows
that.selectCard(1)
}else if (res.data.code == 500){
uni.showToast({
icon: 'none',
title: res.data.msg,
duration: 1500
})
return
}
},
});
},
}
}
</script>
<style lang="scss" scoped>
.container {
}
.bottom {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background: #FFFFFF;
box-shadow: 0rpx -4rpx 22rpx 0rpx rgba(0, 0, 0, 0.07);
.content {
padding: 16rpx 36rpx;
display: flex;
.finallyPrice {
font-size: 24rpx;
color: #333333;
height: 88rpx;
line-height: 88rpx;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-right: 20rpx;
text {
color: #F8C85D;
font-size: 36rpx;
}
}
.goPay {
padding: 0 134rpx;
height: 88rpx;
line-height: 88rpx;
text-align: center;
background: #D8D8D8 linear-gradient(75deg, #6F9A45 0%, #B4DC8E 100%);
border-radius: 20rpx;
font-size: 28rpx;
color: #FFFFFF;
}
}
}
.card {
display: inline-block;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
width: 150px;
height: 200px;
margin: 10px;
padding: 15px;
background-color: #ccc;
border-radius: 8px;
transition: all 0.2s; /* 添加过渡效果 */
}
.card.selected {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); /* 添加阴影以模拟放大效果 */
scale: 1.05; /* 缩放效果 */
}
.award-text{
color: #DAA520;
font-size: 15px;
}
.fixed-button {
position: fixed;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
bottom: 0;
z-index: 99;
margin-bottom: 40px;
}
</style>
支付展示
小程序体验
四、流程总结
微信支付功能主要功能大部分在后端实现,核心点在于创建支付订单以及返回给前端支付所需的参数,在实际开发中,支付模块一般是单独拆分出来提供给其他业务使用,所以大家可根据实际应用场景做相应更改。另外在支付过程中,用户对订单的各种操作状态也需要做对应处理,例如超过设定时间需要关闭订单,用户拉起支付之后又取消支付等,这就要根据实际场景做对应的处理了。